diff --git a/mustache-sharp.test/FormatParserTester.cs b/mustache-sharp.test/FormatParserTester.cs new file mode 100644 index 0000000..47dbbbb --- /dev/null +++ b/mustache-sharp.test/FormatParserTester.cs @@ -0,0 +1,209 @@ +using System; +using System.Globalization; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace mustache.test +{ + /// + /// Tests the FormatParser class. + /// + [TestClass] + public class FormatParserTester + { + /// + /// Replaces placeholds with the actual value. + /// + [TestMethod] + public void TestBuild_Key_ReplacesWithValue() + { + FormatCompiler parser = new FormatCompiler(); + const string format = @"Hello, {{Name}}!!!"; + Generator generator = parser.Compile(format); + string result = generator.Render(new { Name = "Bob" }); + Assert.AreEqual("Hello, Bob!!!", result, "The wrong text was generated."); + } + + /// + /// Removes comments from the output. + /// + [TestMethod] + public void TestBuild_Comment_RemovesComment() + { + FormatCompiler parser = new FormatCompiler(); + const string format = "Before{{#! This is a comment }}After"; + Generator generator = parser.Compile(format); + string result = generator.Render(new object()); + Assert.AreEqual("BeforeAfter", result, "The wrong text was generated."); + } + + /// + /// If the condition evaluates to false, the content of an if statement should not be printed. + /// + [TestMethod] + public void TestBuild_If_EvaluatesToFalse_SkipsContent() + { + FormatCompiler parser = new FormatCompiler(); + const string format = "Before{{#if this}}Content{{/if}}After"; + Generator generator = parser.Compile(format); + string result = generator.Render(false); + Assert.AreEqual("BeforeAfter", result, "The wrong text was generated."); + } + + /// + /// If the condition evaluates to false, the content of an if statement should not be printed. + /// + [TestMethod] + public void TestBuild_If_EvaluatesToTrue_PrintsContent() + { + FormatCompiler parser = new FormatCompiler(); + const string format = "Before{{#if this}}Content{{/if}}After"; + Generator generator = parser.Compile(format); + string result = generator.Render(true); + Assert.AreEqual("BeforeContentAfter", result, "The wrong text was generated."); + } + + /// + /// If the condition evaluates to false, the content of an else statement should be printed. + /// + [TestMethod] + public void TestBuild_IfElse_EvaluatesToFalse_PrintsElse() + { + FormatCompiler parser = new FormatCompiler(); + const string format = "Before{{#if this}}Yay{{#else}}Nay{{/if}}After"; + Generator generator = parser.Compile(format); + string result = generator.Render(false); + Assert.AreEqual("BeforeNayAfter", result, "The wrong text was generated."); + } + + /// + /// If the condition evaluates to true, the content of an if statement should be printed. + /// + [TestMethod] + public void TestBuild_IfElse_EvaluatesToTrue_PrintsIf() + { + FormatCompiler parser = new FormatCompiler(); + const string format = "Before{{#if this}}Yay{{#else}}Nay{{/if}}After"; + Generator generator = parser.Compile(format); + string result = generator.Render(true); + Assert.AreEqual("BeforeYayAfter", result, "The wrong text was generated."); + } + + /// + /// Second else blocks will be interpreted as just another piece of text. + /// + [TestMethod] + public void TestBuild_IfElse_TwoElses_IncludesSecondElseInElse() + { + FormatCompiler parser = new FormatCompiler(); + const string format = "Before{{#if this}}Yay{{#else}}Nay{{#else}}Bad{{/if}}After"; + Generator generator = parser.Compile(format); + string result = generator.Render(false); + Assert.AreEqual("BeforeNay{{#else}}BadAfter", result, "The wrong text was generated."); + } + + /// + /// If the if statement evaluates to true, its block should be printed. + /// + [TestMethod] + public void TestBuild_IfElifElse_IfTrue_PrintsIf() + { + FormatCompiler parser = new FormatCompiler(); + const string format = "Before{{#if First}}First{{#elif Second}}Second{{#else}}Third{{/if}}After"; + Generator generator = parser.Compile(format); + string result = generator.Render(new { First = true, Second = true }); + Assert.AreEqual("BeforeFirstAfter", result, "The wrong text was generated."); + } + + /// + /// If the elif statement evaluates to true, its block should be printed. + /// + [TestMethod] + public void TestBuild_IfElifElse_ElifTrue_PrintsIf() + { + FormatCompiler parser = new FormatCompiler(); + const string format = "Before{{#if First}}First{{#elif Second}}Second{{#else}}Third{{/if}}After"; + Generator generator = parser.Compile(format); + string result = generator.Render(new { First = false, Second = true }); + Assert.AreEqual("BeforeSecondAfter", result, "The wrong text was generated."); + } + + /// + /// If the elif statement evaluates to false, the else block should be printed. + /// + [TestMethod] + public void TestBuild_IfElifElse_ElifFalse_PrintsElse() + { + FormatCompiler parser = new FormatCompiler(); + const string format = "Before{{#if First}}First{{#elif Second}}Second{{#else}}Third{{/if}}After"; + Generator generator = parser.Compile(format); + string result = generator.Render(new { First = false, Second = false }); + Assert.AreEqual("BeforeThirdAfter", result, "The wrong text was generated."); + } + + /// + /// If the elif statement evaluates to false and there is no else statement, nothing should be printed. + /// + [TestMethod] + public void TestBuild_IfElif_ElifFalse_PrintsNothing() + { + FormatCompiler parser = new FormatCompiler(); + const string format = "Before{{#if First}}First{{#elif Second}}Second{{/if}}After"; + Generator generator = parser.Compile(format); + string result = generator.Render(new { First = false, Second = false }); + Assert.AreEqual("BeforeAfter", result, "The wrong text was generated."); + } + + /// + /// If there are two elif statements and the first is false, the second elif block should be printed. + /// + [TestMethod] + public void TestBuild_IfElifElif_ElifFalse_PrintsSecondElif() + { + FormatCompiler parser = new FormatCompiler(); + const string format = "Before{{#if First}}First{{#elif Second}}Second{{#elif Third}}Third{{/if}}After"; + Generator generator = parser.Compile(format); + string result = generator.Render(new { First = false, Second = false, Third = true }); + Assert.AreEqual("BeforeThirdAfter", result, "The wrong text was generated."); + } + + /// + /// If we pass an empty collection to an each statement, the content should not be printed. + /// + [TestMethod] + public void TestBuild_Each_EmptyCollection_SkipsContent() + { + FormatCompiler parser = new FormatCompiler(); + const string format = "Before{{#each this}}{{this}}{{/each}}After"; + Generator generator = parser.Compile(format); + string result = generator.Render(new int[0]); + Assert.AreEqual("BeforeAfter", result, "The wrong text was generated."); + } + + /// + /// If we pass a populated collection to an each statement, the content should be printed + /// for each item in the collection, using that item as the new scope context. + /// + [TestMethod] + public void TestBuild_Each_PopulatedCollection_PrintsContentForEach() + { + FormatCompiler parser = new FormatCompiler(); + const string format = "Before{{#each this}}{{this}}{{/each}}After"; + Generator generator = parser.Compile(format); + string result = generator.Render(new int[] { 1, 2, 3 }); + Assert.AreEqual("Before123After", result, "The wrong text was generated."); + } + + /// + /// The object replacing the placeholder should be used as the context of a with statement. + /// + [TestMethod] + public void TestBuild_With_AddsScope() + { + FormatCompiler parser = new FormatCompiler(); + const string format = "Before{{#with Nested}}{{this}}{{/with}}After"; + Generator generator = parser.Compile(format); + string result = generator.Render(new { Nested = "Hello" }); + Assert.AreEqual("BeforeHelloAfter", result, "The wrong text was generated."); + } + } +} diff --git a/mustache-sharp.test/mustache-sharp.test.csproj b/mustache-sharp.test/mustache-sharp.test.csproj index afb4a59..66435c1 100644 --- a/mustache-sharp.test/mustache-sharp.test.csproj +++ b/mustache-sharp.test/mustache-sharp.test.csproj @@ -43,6 +43,7 @@ + diff --git a/mustache-sharp/ArgumentCollection.cs b/mustache-sharp/ArgumentCollection.cs new file mode 100644 index 0000000..835810a --- /dev/null +++ b/mustache-sharp/ArgumentCollection.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; + +namespace mustache +{ + /// + /// Associates parameters to their argument values. + /// + internal sealed class ArgumentCollection + { + private readonly Dictionary _argumentLookup; + + /// + /// Initializes a new instance of an ArgumentCollection. + /// + public ArgumentCollection() + { + _argumentLookup = new Dictionary(); + } + + /// + /// Associates the given parameter to the key placeholder. + /// + /// The parameter to associate the key with. + /// The key placeholder used as the argument. + /// If the key is null, the default value of the parameter will be used. + public void AddArgument(TagParameter parameter, string key) + { + _argumentLookup.Add(parameter, key); + } + + /// + /// Substitutes the key placeholders with their respective values. + /// + /// The current lexical scope. + /// A dictionary associating the parameter name to the associated value. + public Dictionary GetArguments(KeyScope scope) + { + Dictionary arguments = new Dictionary(); + foreach (KeyValuePair pair in _argumentLookup) + { + object value; + if (pair.Value == null) + { + value = pair.Key.DefaultValue; + } + else + { + value = scope.Find(pair.Value); + } + arguments.Add(pair.Key.Name, value); + } + return arguments; + } + } +} diff --git a/mustache-sharp/CompoundGenerator.cs b/mustache-sharp/CompoundGenerator.cs index 8a3556c..d5ba34c 100644 --- a/mustache-sharp/CompoundGenerator.cs +++ b/mustache-sharp/CompoundGenerator.cs @@ -4,30 +4,85 @@ using System.Text; namespace mustache { + /// + /// Builds text by combining the output of other generators. + /// internal sealed class CompoundGenerator : IGenerator { - private readonly List _generators; + private readonly TagDefinition _definition; + private readonly ArgumentCollection _arguments; + private readonly List _primaryGenerators; + private IGenerator _subGenerator; - public CompoundGenerator() + /// + /// Initializes a new instance of a CompoundGenerator. + /// + /// The tag that the text is being generated for. + /// The arguments that were passed to the tag. + public CompoundGenerator(TagDefinition definition, ArgumentCollection arguments) { - _generators = new List(); + _definition = definition; + _arguments = arguments; + _primaryGenerators = new List(); } - public void AddGenerator(StaticGenerator generator) + /// + /// Adds the given generator. + /// + /// The generator to add. + public void AddGenerator(IGenerator generator) { - _generators.Add(generator); + addGenerator(generator, false); } - string IGenerator.GetText(object source) + /// + /// Adds the given generator, determining whether the generator should + /// be part of the primary generators or added as an secondary generator. + /// + /// The tag that the generator is generating text for. + /// The generator to add. + public void AddGenerator(TagDefinition definition, IGenerator generator) + { + bool isSubGenerator = _definition.ShouldCreateSecondaryGroup(definition); + addGenerator(generator, isSubGenerator); + } + + private void addGenerator(IGenerator generator, bool isSubGenerator) + { + if (isSubGenerator) + { + _subGenerator = generator; + } + else + { + _primaryGenerators.Add(generator); + } + } + + string IGenerator.GetText(IFormatProvider provider, KeyScope scope) { StringBuilder builder = new StringBuilder(); - foreach (IGenerator generator in _generators) + Dictionary arguments = _arguments.GetArguments(scope); + IEnumerable scopes = _definition.GetChildScopes(scope, arguments); + List generators; + if (_definition.ShouldGeneratePrimaryGroup(arguments)) { - builder.Append(generator.GetText(source)); + generators = _primaryGenerators; + } + else + { + generators = new List() { _subGenerator }; + } + foreach (KeyScope childScope in scopes) + { + foreach (IGenerator generator in generators) + { + builder.Append(generator.GetText(provider, childScope)); + } } string innerText = builder.ToString(); - // TODO - process with tag's custom handler - return innerText; + string outerText = _definition.Decorate(provider, innerText, arguments); + return outerText; } } } \ No newline at end of file diff --git a/mustache-sharp/ConditionTagDefinition.cs b/mustache-sharp/ConditionTagDefinition.cs new file mode 100644 index 0000000..f45e42d --- /dev/null +++ b/mustache-sharp/ConditionTagDefinition.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace mustache +{ + /// + /// Defines a tag that conditionally prints its content. + /// + internal abstract class ConditionTagDefinition : TagDefinition + { + private const string conditionParameter = "condition"; + + /// + /// Initializes a new instance of a ConditionTagDefinition. + /// + /// The name of the tag. + protected ConditionTagDefinition(string tagName) + : base(tagName, true) + { + } + + /// + /// Gets the parameters that can be passed to the tag. + /// + /// The parameters. + protected override TagParameter[] GetParameters() + { + return new TagParameter[] { new TagParameter(conditionParameter) { IsRequired = true } }; + } + + /// + /// Gets whether the tag will contain content. + /// + public override bool HasBody + { + get { return true; } + } + + /// + /// Gets the tags that come into scope within the context of the current tag. + /// + /// The child tag definitions. + protected override TagDefinition[] GetChildTags() + { + return new TagDefinition[] + { + new ElifTagDefinition(), + new ElseTagDefinition(), + }; + } + + /// + /// Gets whether the given tag's generator should be used for a secondary (or substitute) text block. + /// + /// The tag to inspect. + /// True if the tag's generator should be used as a secondary generator. + public override bool ShouldCreateSecondaryGroup(TagDefinition definition) + { + return (definition is ElifTagDefinition) || (definition is ElseTagDefinition); + } + + /// + /// Gets whether the primary generator group should be used to render the tag. + /// + /// The arguments passed to the tag. + /// + /// True if the primary generator group should be used to render the tag; + /// otherwise, false to use the secondary group. + /// + public override bool ShouldGeneratePrimaryGroup(Dictionary arguments) + { + object condition = arguments[conditionParameter]; + return isConditionSatisfied(condition); + } + + private bool isConditionSatisfied(object condition) + { + if (condition == null) + { + return false; + } + IEnumerable enumerable = condition as IEnumerable; + if (enumerable != null) + { + return enumerable.Cast().Any(); + } + if (condition is Char) + { + return (Char)condition != '\0'; + } + try + { + decimal number = (decimal)Convert.ChangeType(condition, typeof(decimal)); + return number != 0.0m; + } + catch + { + return true; + } + } + } +} diff --git a/mustache-sharp/EachTagDefinition.cs b/mustache-sharp/EachTagDefinition.cs new file mode 100644 index 0000000..1d259a2 --- /dev/null +++ b/mustache-sharp/EachTagDefinition.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +namespace mustache +{ + /// + /// Defines a tag that can iterate over a collection of items and render + /// the content using each item as the context. + /// + internal sealed class EachTagDefinition : TagDefinition + { + private const string collectionParameter = "collection"; + + /// + /// Initializes a new instance of an EachTagDefinition. + /// + public EachTagDefinition() + : base("each", true) + { + } + + /// + /// Gets the parameters that can be passed to the tag. + /// + /// The parameters. + protected override TagParameter[] GetParameters() + { + return new TagParameter[] { new TagParameter(collectionParameter) { IsRequired = true } }; + } + + /// + /// Gets whether the tag has content. + /// + public override bool HasBody + { + get { return true; } + } + + /// + /// Gets the tags that come into scope within the context of the tag. + /// + /// The tag definitions. + protected override TagDefinition[] GetChildTags() + { + return new TagDefinition[0]; + } + + /// + /// Gets the scopes for each of the items found in the argument. + /// + /// The current scope. + /// The arguments passed to the tag. + /// The scopes for each of the items found in the argument. + public override IEnumerable GetChildScopes(KeyScope scope, Dictionary arguments) + { + object value = arguments[collectionParameter]; + IEnumerable enumerable = value as IEnumerable; + if (enumerable == null) + { + yield break; + } + foreach (object item in enumerable) + { + yield return scope.CreateChildScope(item); + } + } + } +} diff --git a/mustache-sharp/ElifTagDefinition.cs b/mustache-sharp/ElifTagDefinition.cs new file mode 100644 index 0000000..f9aafa6 --- /dev/null +++ b/mustache-sharp/ElifTagDefinition.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; + +namespace mustache +{ + /// + /// Defines a tag that conditionally renders its content if preceding if and elif tags fail. + /// + internal sealed class ElifTagDefinition : ConditionTagDefinition + { + /// + /// Initializes a new instance of an ElifTagDefinition. + /// + public ElifTagDefinition() + : base("elif") + { + } + + /// + /// Gets the tags that indicate the end of the current tags context. + /// + public override IEnumerable ClosingTags + { + get { return new TagDefinition[] { new IfTagDefinition() }; } + } + } +} diff --git a/mustache-sharp/ElseTagDefinition.cs b/mustache-sharp/ElseTagDefinition.cs new file mode 100644 index 0000000..6cfaff9 --- /dev/null +++ b/mustache-sharp/ElseTagDefinition.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; + +namespace mustache +{ + /// + /// Defines a tag that renders its content if all preceding if and elif tags. + /// + internal sealed class ElseTagDefinition : TagDefinition + { + /// + /// Initializes a new instance of a ElseTagDefinition. + /// + public ElseTagDefinition() + : base("else", true) + { + } + + /// + /// Gets the parameters that can be passed to the tag. + /// + /// The parameters. + protected override TagParameter[] GetParameters() + { + return new TagParameter[0]; + } + + /// + /// Gets whether the tag contains content. + /// + public override bool HasBody + { + get { return true; } + } + + /// + /// Gets the tags that indicate the end of the current tag's content. + /// + public override IEnumerable ClosingTags + { + get { return new TagDefinition[] { new IfTagDefinition() }; } + } + + /// + /// Gets the tags that come into scope within the context of the tag. + /// + /// The tag definitions. + protected override TagDefinition[] GetChildTags() + { + return new TagDefinition[0]; + } + } +} diff --git a/mustache-sharp/FormatCompiler.cs b/mustache-sharp/FormatCompiler.cs new file mode 100644 index 0000000..842e39f --- /dev/null +++ b/mustache-sharp/FormatCompiler.cs @@ -0,0 +1,232 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using mustache.Properties; + +namespace mustache +{ + /// + /// Parses a format string and returns a text generator. + /// + public sealed class FormatCompiler + { + private const string key = @"[_\w][_\w\d]*"; + private const string compoundKey = key + @"(\." + key + ")*"; + + private readonly MasterTagDefinition _master; + private readonly TagScope _tagScope; + + /// + /// Initializes a new instance of a FormatCompiler. + /// + public FormatCompiler() + { + _master = new MasterTagDefinition(); + _tagScope = new TagScope(); + registerTags(_master, _tagScope); + } + + /// + /// Registers the given tag definition with the parser. + /// + /// The tag definition to register. + public void RegisterTag(TagDefinition definition) + { + if (definition == null) + { + throw new ArgumentNullException("definition"); + } + _tagScope.AddTag(definition); + } + + /// + /// Builds a text generator based on the given format. + /// + /// The format to parse. + /// The text generator. + public Generator Compile(string format) + { + CompoundGenerator generator = new CompoundGenerator(_master, new ArgumentCollection()); + int formatIndex = buildCompoundGenerator(_master, _tagScope, generator, format, 0); + string trailing = format.Substring(formatIndex); + StaticGenerator staticGenerator = new StaticGenerator(trailing); + generator.AddGenerator(staticGenerator); + return new Generator(generator); + } + + private static void registerTags(TagDefinition definition, TagScope scope) + { + foreach (TagDefinition childTag in definition.ChildTags) + { + scope.AddTag(childTag); + } + } + + private static Match findNextTag(TagDefinition definition, string format, int formatIndex) + { + List matches = new List(); + matches.Add(getKeyRegex()); + matches.Add(getCommentTagRegex()); + foreach (TagDefinition closingTag in definition.ClosingTags) + { + matches.Add(getClosingTagRegex(closingTag)); + } + foreach (TagDefinition childTag in definition.ChildTags) + { + matches.Add(getTagRegex(childTag)); + } + string match = "{{(" + String.Join("|", matches) + ")}}"; + Regex regex = new Regex(match); + return regex.Match(format, formatIndex); + } + + private static string getClosingTagRegex(TagDefinition definition) + { + StringBuilder regexBuilder = new StringBuilder(); + regexBuilder.Append(@"(?(/(?"); + regexBuilder.Append(definition.Name); + regexBuilder.Append(@")\s*?))"); + return regexBuilder.ToString(); + } + + private static string getCommentTagRegex() + { + return @"(?#!.*?)"; + } + + private static string getKeyRegex() + { + return @"((?" + compoundKey + @")(,(?(-)?[\d]+))?(:(?.*?))?)"; + } + + private static string getTagRegex(TagDefinition definition) + { + StringBuilder regexBuilder = new StringBuilder(); + regexBuilder.Append(@"(?(#(?"); + regexBuilder.Append(definition.Name); + regexBuilder.Append(@")"); + foreach (TagParameter parameter in definition.Parameters) + { + regexBuilder.Append(@"\s+?"); + regexBuilder.Append(@"(?"); + regexBuilder.Append(compoundKey); + regexBuilder.Append(@")"); + } + regexBuilder.Append(@"\s*?))"); + return regexBuilder.ToString(); + } + + private static int buildCompoundGenerator( + TagDefinition tagDefinition, + TagScope scope, + CompoundGenerator generator, + string format, int formatIndex) + { + while (true) + { + Match match = findNextTag(tagDefinition, format, formatIndex); + + if (!match.Success) + { + if (tagDefinition.ClosingTags.Any()) + { + string message = String.Format(Resources.MissingClosingTag, tagDefinition.Name); + throw new FormatException(message); + } + break; + } + + string leading = format.Substring(formatIndex, match.Index - formatIndex); + StaticGenerator staticGenerator = new StaticGenerator(leading); + generator.AddGenerator(staticGenerator); + + if (match.Groups["key"].Success) + { + formatIndex = match.Index + match.Length; + string key = match.Groups["key"].Value; + string alignment = match.Groups["alignment"].Value; + string formatting = match.Groups["format"].Value; + KeyGenerator keyGenerator = new KeyGenerator(key, alignment, formatting); + generator.AddGenerator(keyGenerator); + } + else if (match.Groups["open"].Success) + { + formatIndex = match.Index + match.Length; + string tagName = match.Groups["name"].Value; + TagDefinition nextDefinition = scope.Find(tagName); + if (nextDefinition == null) + { + string message = String.Format(Resources.UnknownTag, tagName); + throw new FormatException(message); + } + if (nextDefinition.HasBody) + { + ArgumentCollection arguments = getArguments(nextDefinition, match); + CompoundGenerator compoundGenerator = new CompoundGenerator(nextDefinition, arguments); + TagScope nextScope = new TagScope(scope); + registerTags(nextDefinition, nextScope); + formatIndex = buildCompoundGenerator(nextDefinition, nextScope, compoundGenerator, format, formatIndex); + generator.AddGenerator(nextDefinition, compoundGenerator); + } + else + { + Match nextMatch = findNextTag(nextDefinition, format, formatIndex); + ArgumentCollection arguments = getArguments(nextDefinition, nextMatch); + InlineGenerator inlineGenerator = new InlineGenerator(nextDefinition, arguments); + generator.AddGenerator(inlineGenerator); + } + } + else if (match.Groups["close"].Success) + { + string tagName = match.Groups["name"].Value; + formatIndex = match.Index; + if (tagName == tagDefinition.Name) + { + formatIndex += match.Length; + } + break; + } + else if (match.Groups["comment"].Success) + { + formatIndex = match.Index + match.Length; + } + } + return formatIndex; + } + + private static ArgumentCollection getArguments(TagDefinition definition, Match match) + { + ArgumentCollection collection = new ArgumentCollection(); + List captures = match.Groups["argument"].Captures.Cast().ToList(); + List parameters = definition.Parameters.ToList(); + if (captures.Count > parameters.Count) + { + string message = String.Format(Resources.WrongNumberOfArguments, definition.Name); + throw new FormatException(message); + } + if (captures.Count < parameters.Count) + { + captures.AddRange(Enumerable.Repeat((Capture)null, parameters.Count - captures.Count)); + } + foreach (var pair in parameters.Zip(captures, (p, c) => new { Capture = c, Parameter = p })) + { + if (pair.Capture == null) + { + if (pair.Parameter.IsRequired) + { + string message = String.Format(Resources.WrongNumberOfArguments, definition.Name); + throw new FormatException(message); + } + collection.AddArgument(pair.Parameter, null); + } + else + { + collection.AddArgument(pair.Parameter, pair.Capture.Value); + } + } + return collection; + } + } +} diff --git a/mustache-sharp/FormatParser.cs b/mustache-sharp/FormatParser.cs deleted file mode 100644 index 1966508..0000000 --- a/mustache-sharp/FormatParser.cs +++ /dev/null @@ -1,150 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; -using System.Text.RegularExpressions; - -namespace mustache -{ - /// - /// Parses a format string and returns the text generator. - /// - internal sealed class FormatParser - { - private const string key = @"[_\w][_\w\d]*"; - private const string compoundKey = key + @"(\." + key + ")*"; - - /// - /// Initializes a new instance of a FormatParser. - /// - public FormatParser() - { - } - - /// - /// Builds a text generator based on the given format. - /// - /// The format to parse. - /// The text generator. - public IGenerator Build(string format) - { - TagDefinition definition = new TagDefinition("builtins"); - definition.HasBody = true; - CompoundGenerator generator = new CompoundGenerator(); - TagScope tagScope = new TagScope(); - registerTags(definition, tagScope); - Match match = findNextTag(definition, format, 0); - buildCompoundGenerator(definition, tagScope, generator, format, 0, match); - return generator; - } - - private static void registerTags(TagDefinition definition, TagScope scope) - { - foreach (TagDefinition childTag in definition.ChildTags) - { - scope.AddTag(childTag); - } - } - - private static Match findNextTag(TagDefinition definition, string format, int formatIndex) - { - List matches = new List(); - matches.Add(getKeyRegex()); - matches.Add(getClosingTagRegex(definition)); - matches.Add(getCommentTagRegex()); - foreach (TagDefinition childTag in definition.ChildTags) - { - matches.Add(getTagRegex(childTag)); - } - string match = "{{(" + String.Join("|", matches) + ")}}"; - Regex regex = new Regex(match); - return regex.Match(format, formatIndex); - } - - private static string getClosingTagRegex(TagDefinition definition) - { - StringBuilder regexBuilder = new StringBuilder(); - regexBuilder.Append(@"(?(/"); - regexBuilder.Append(definition.Name); - regexBuilder.Append(@"\s*?))"); - return regexBuilder.ToString(); - } - - private static string getCommentTagRegex() - { - return @"(?#!.*?)"; - } - - private static string getKeyRegex() - { - return @"((?" + compoundKey + @")(,(?(-)?[\d]+))?(:(?.*?))?)"; - } - - private static string getTagRegex(TagDefinition definition) - { - StringBuilder regexBuilder = new StringBuilder(); - regexBuilder.Append(@"(?(#(?"); - regexBuilder.Append(definition.Name); - regexBuilder.Append(@")"); - foreach (TagParameter parameter in definition.Parameters) - { - regexBuilder.Append(@"\s+?"); - regexBuilder.Append(@"(?"); - regexBuilder.Append(compoundKey); - regexBuilder.Append(@")"); - } - regexBuilder.Append(@"\s*?))"); - return regexBuilder.ToString(); - } - - private static int buildCompoundGenerator(TagDefinition tagDefinition, TagScope scope, CompoundGenerator generator, string format, int formatIndex, Match match) - { - bool done = false; - while (!done) - { - string leading = format.Substring(formatIndex, match.Index - formatIndex); - formatIndex = match.Index + match.Length; - - if (match.Groups["comment"].Success) - { - // TODO - process comment - } - else if (match.Groups["close"].Success) - { - // TODO - process closing tag - done = true; - } - else if (match.Groups["open"].Success) - { - string tagName = match.Groups["name"].Value; - TagDefinition nextDefinition = scope.Find(tagName); - if (nextDefinition == null) - { - // TODO - handle missing tag definition - } - if (nextDefinition.HasBody) - { - CompoundGenerator nextGenerator = new CompoundGenerator(); - TagScope nextScope = new TagScope(scope); - registerTags(nextDefinition, nextScope); - Match nextMatch = findNextTag(nextDefinition, format, formatIndex); - formatIndex = buildCompoundGenerator(nextDefinition, nextScope, nextGenerator, format, formatIndex, nextMatch); - // TODO - grab the generated text and parameters and pass it to the tag's processor - // TODO - a parameter can be a key or a default value - } - else - { - // TODO - grab all of the parameters and pass them to the tag's generator - // TODO - a parameter can be a key or a default value - } - } - else if (match.Groups["key"].Success) - { - string alignment = match.Groups["alignment"].Value; - string formatting = match.Groups["format"].Value; - // TODO - create a key generator - } - } - return formatIndex; - } - } -} diff --git a/mustache-sharp/Generator.cs b/mustache-sharp/Generator.cs new file mode 100644 index 0000000..7f3852e --- /dev/null +++ b/mustache-sharp/Generator.cs @@ -0,0 +1,53 @@ +using System; +using System.Globalization; + +namespace mustache +{ + /// + /// Generates text by substituting an object's values for placeholders. + /// + public sealed class Generator + { + private readonly IGenerator _generator; + + /// + /// Initializes a new instance of a Generator. + /// + /// The text generator to wrap. + internal Generator(IGenerator generator) + { + _generator = generator; + } + + /// + /// Gets the text that is generated for the given object. + /// + /// The object to generate the text with. + /// The text generated for the given object. + public string Render(object source) + { + return render(CultureInfo.CurrentCulture, source); + } + + /// + /// Gets the text that is generated for the given object. + /// + /// The format provider to use. + /// The object to generate the text with. + /// The text generated for the given object. + public string Render(IFormatProvider provider, object source) + { + if (provider == null) + { + provider = CultureInfo.CurrentCulture; + } + return render(provider, source); + } + + private string render(IFormatProvider provider, object source) + { + KeyScope scope = new KeyScope(source); + return _generator.GetText(provider, scope); + } + } +} diff --git a/mustache-sharp/IGenerator.cs b/mustache-sharp/IGenerator.cs index f155055..6ee122d 100644 --- a/mustache-sharp/IGenerator.cs +++ b/mustache-sharp/IGenerator.cs @@ -8,10 +8,11 @@ namespace mustache internal interface IGenerator { /// - /// Generates the text when the values of the given object are applied to the format plan. + /// Generates the text when applying the format plan. /// - /// The object whose values should be used to generate the text. + /// The format provider to use when formatting the keys. + /// The current lexical scope of the keys. /// The generated text. - string GetText(object source); + string GetText(IFormatProvider provider, KeyScope scope); } } diff --git a/mustache-sharp/IfTagDefinition.cs b/mustache-sharp/IfTagDefinition.cs new file mode 100644 index 0000000..6e56397 --- /dev/null +++ b/mustache-sharp/IfTagDefinition.cs @@ -0,0 +1,19 @@ +using System; + +namespace mustache +{ + /// + /// Defines a tag that renders its content depending on the truthyness + /// of its argument, with optional elif and else nested tags. + /// + internal sealed class IfTagDefinition : ConditionTagDefinition + { + /// + /// Initializes a new instance of a IfTagDefinition. + /// + public IfTagDefinition() + : base("if") + { + } + } +} diff --git a/mustache-sharp/InlineGenerator.cs b/mustache-sharp/InlineGenerator.cs new file mode 100644 index 0000000..0a64e18 --- /dev/null +++ b/mustache-sharp/InlineGenerator.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; + +namespace mustache +{ + /// + /// Generates the text for a tag that only exists on a single line. + /// + internal sealed class InlineGenerator : IGenerator + { + private readonly TagDefinition _definition; + private readonly ArgumentCollection _arguments; + + /// + /// Initializes a new instance of an InlineGenerator. + /// + /// The tag to render the text for. + /// The arguments passed to the tag. + public InlineGenerator(TagDefinition definition, ArgumentCollection arguments) + { + _definition = definition; + _arguments = arguments; + } + + string IGenerator.GetText(IFormatProvider provider, KeyScope scope) + { + Dictionary arguments = _arguments.GetArguments(scope); + return _definition.Decorate(provider, String.Empty, arguments); + } + } +} diff --git a/mustache-sharp/KeyGenerator.cs b/mustache-sharp/KeyGenerator.cs new file mode 100644 index 0000000..09b8a9c --- /dev/null +++ b/mustache-sharp/KeyGenerator.cs @@ -0,0 +1,50 @@ +using System; +using System.Text; + +namespace mustache +{ + /// + /// Substitutes a key placeholder with the textual representation of the associated object. + /// + internal sealed class KeyGenerator : IGenerator + { + private readonly string _key; + private readonly string _format; + + /// + /// Initializes a new instance of a KeyGenerator. + /// + /// The key to substitute with its value. + /// The alignment specifier. + /// The format specifier. + public KeyGenerator(string key, string alignment, string formatting) + { + _key = key; + _format = getFormat(alignment, formatting); + } + + private static string getFormat(string alignment, string formatting) + { + StringBuilder formatBuilder = new StringBuilder(); + formatBuilder.Append("{0"); + if (!String.IsNullOrWhiteSpace(alignment)) + { + formatBuilder.Append(","); + formatBuilder.Append(alignment); + } + if (!String.IsNullOrWhiteSpace(formatting)) + { + formatBuilder.Append(":"); + formatBuilder.Append(formatting); + } + formatBuilder.Append("}"); + return formatBuilder.ToString(); + } + + string IGenerator.GetText(IFormatProvider provider, KeyScope scope) + { + object value = scope.Find(_key); + return String.Format(provider, _format, value); + } + } +} diff --git a/mustache-sharp/KeyScope.cs b/mustache-sharp/KeyScope.cs index 22ab489..b6ede57 100644 --- a/mustache-sharp/KeyScope.cs +++ b/mustache-sharp/KeyScope.cs @@ -38,7 +38,7 @@ namespace mustache /// /// The object to search for keys in. /// The new child scope. - internal KeyScope CreateChildScope(object source) + public KeyScope CreateChildScope(object source) { KeyScope scope = new KeyScope(source, this); return scope; @@ -50,7 +50,7 @@ namespace mustache /// The name of the key. /// The value associated with the key with the given name. /// A key with the given name could not be found. - public object Find(string name) + internal object Find(string name) { string[] names = name.Split('.'); string member = names[0]; diff --git a/mustache-sharp/MasterTagDefinition.cs b/mustache-sharp/MasterTagDefinition.cs new file mode 100644 index 0000000..b9d5ad7 --- /dev/null +++ b/mustache-sharp/MasterTagDefinition.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; + +namespace mustache +{ + /// + /// Defines a pseudo tag that wraps the entire content of a format string. + /// + internal sealed class MasterTagDefinition : TagDefinition + { + /// + /// Initializes a new instance of a MasterTagDefinition. + /// + public MasterTagDefinition() + : base(String.Empty, true) + { + } + + /// + /// Gets the parameters that can be passed to the tag. + /// + /// The parameters. + protected override TagParameter[] GetParameters() + { + return new TagParameter[0]; + } + + /// + /// Gets whether the tag has content. + /// + public override bool HasBody + { + get { return true; } + } + + /// + /// Gets the tags that indicate the end of the tags context. + /// + public override IEnumerable ClosingTags + { + get { return new TagDefinition[0]; } + } + + /// + /// Gets the tags that come into scope within the context of the tag. + /// + /// The tags. + protected override TagDefinition[] GetChildTags() + { + return new TagDefinition[] + { + new IfTagDefinition(), + new EachTagDefinition(), + new WithTagDefinition(), + }; + } + } +} diff --git a/mustache-sharp/Properties/Resources.Designer.cs b/mustache-sharp/Properties/Resources.Designer.cs index 62c3781..db3399d 100644 --- a/mustache-sharp/Properties/Resources.Designer.cs +++ b/mustache-sharp/Properties/Resources.Designer.cs @@ -87,6 +87,15 @@ namespace mustache.Properties { } } + /// + /// Looks up a localized string similar to The {0} tag has already been registered.. + /// + internal static string DuplicateTagDefinition { + get { + return ResourceManager.GetString("DuplicateTagDefinition", resourceCulture); + } + } + /// /// Looks up a localized string similar to The key {0} could not be found.. /// @@ -95,5 +104,32 @@ namespace mustache.Properties { return ResourceManager.GetString("KeyNotFound", resourceCulture); } } + + /// + /// Looks up a localized string similar to Expected a matching {0} tag but none was found.. + /// + internal static string MissingClosingTag { + get { + return ResourceManager.GetString("MissingClosingTag", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Encountered an unknown tag {0}.. + /// + internal static string UnknownTag { + get { + return ResourceManager.GetString("UnknownTag", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The wrong number of arguments were passed to an {0} tag.. + /// + internal static string WrongNumberOfArguments { + get { + return ResourceManager.GetString("WrongNumberOfArguments", resourceCulture); + } + } } } diff --git a/mustache-sharp/Properties/Resources.resx b/mustache-sharp/Properties/Resources.resx index 6f8484a..94277ae 100644 --- a/mustache-sharp/Properties/Resources.resx +++ b/mustache-sharp/Properties/Resources.resx @@ -126,7 +126,19 @@ A parameter with the same name already exists within the tag. + + The {0} tag has already been registered. + The key {0} could not be found. + + Expected a matching {0} tag but none was found. + + + Encountered an unknown tag {0}. + + + The wrong number of arguments were passed to an {0} tag. + \ No newline at end of file diff --git a/mustache-sharp/StaticGenerator.cs b/mustache-sharp/StaticGenerator.cs index e8aaa2e..0d16e75 100644 --- a/mustache-sharp/StaticGenerator.cs +++ b/mustache-sharp/StaticGenerator.cs @@ -1,17 +1,25 @@ using System; +using System.Collections.Generic; namespace mustache { + /// + /// Generates a static block of text. + /// internal sealed class StaticGenerator : IGenerator { private readonly string _value; + /// + /// Initializes a new instance of a StaticGenerator. + /// + /// The string to return. public StaticGenerator(string value) { _value = value; } - string IGenerator.GetText(object source) + string IGenerator.GetText(IFormatProvider provider, KeyScope scope) { return _value; } diff --git a/mustache-sharp/TagDefinition.cs b/mustache-sharp/TagDefinition.cs index c0aaa6e..f6538b5 100644 --- a/mustache-sharp/TagDefinition.cs +++ b/mustache-sharp/TagDefinition.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Linq; using mustache.Properties; namespace mustache @@ -9,27 +8,32 @@ namespace mustache /// /// Defines the attributes of a custom tag. /// - public sealed class TagDefinition + public abstract class TagDefinition { private readonly string _tagName; - private readonly List _parameters; - private readonly List _childTagDefinitions; - private TagParameter _scopeParameter; /// /// Initializes a new instance of a TagDefinition. /// /// The name of the tag. /// The name of the tag is null or blank. - public TagDefinition(string tagName) + protected TagDefinition(string tagName) + : this(tagName, false) { - if (!RegexHelper.IsValidIdentifier(tagName)) + } + + /// + /// Initializes a new instance of a TagDefinition. + /// + /// The name of the tag. + /// Specifies whether the tag is built-in or not. Checks are not performed on the names of built-in tags. + internal TagDefinition(string tagName, bool isBuiltIn) + { + if (!isBuiltIn && !RegexHelper.IsValidIdentifier(tagName)) { throw new ArgumentException(Resources.BlankTagName, "tagName"); } _tagName = tagName; - _parameters = new List(); - _childTagDefinitions = new List(); } /// @@ -40,66 +44,45 @@ namespace mustache get { return _tagName; } } - /// - /// Specifies that the tag expects the given parameter information. - /// - /// The parameter to add. - /// The parameter is null. - /// A parameter with the same name already exists. - public void AddParameter(TagParameter parameter) - { - if (parameter == null) - { - throw new ArgumentNullException("parameter"); - } - if (_parameters.Any(p => p.Name == parameter.Name)) - { - throw new ArgumentException(Resources.DuplicateParameter, "parameter"); - } - _parameters.Add(parameter); - if (parameter.IsScopeContext) - { - _scopeParameter = parameter; - } - } - /// /// Gets the parameters that are defined for the tag. /// public IEnumerable Parameters { - get { return new ReadOnlyCollection(_parameters); } + get { return new ReadOnlyCollection(GetParameters()); } } /// - /// Gets or sets whether the tag contains content. + /// Specifies which parameters are passed to the tag. /// - public bool HasBody + /// The tag parameters. + protected abstract TagParameter[] GetParameters(); + + /// + /// Gets whether the tag contains content. + /// + public abstract bool HasBody { get; - set; } /// - /// Gets or sets whether the tag defines a new scope based on an argument. + /// Gets the tags that can indicate that the tag has closed. + /// This field is only used if no closing tag is expected. /// - public bool IsScoped + public virtual IEnumerable ClosingTags { - get; - set; - } - - /// - /// Specifies that the given tag is in scope within the current tag. - /// - /// The tag that is in scope within the current tag. - public void AddChildTag(TagDefinition childTag) - { - if (childTag == null) + get { - throw new ArgumentNullException("childTag"); + if (HasBody) + { + return new TagDefinition[] { this }; + } + else + { + return new TagDefinition[0]; + } } - _childTagDefinitions.Add(childTag); } /// @@ -107,7 +90,56 @@ namespace mustache /// public IEnumerable ChildTags { - get { return new ReadOnlyCollection(_childTagDefinitions); } + get { return new ReadOnlyCollection(GetChildTags()); } + } + + /// + /// Specifies which tags are scoped under the current tag. + /// + /// The child tag definitions. + protected abstract TagDefinition[] GetChildTags(); + + /// + /// Gets the scope to use when building the inner text of the tag. + /// + /// The current scope. + /// The arguments passed to the tag. + /// The scope to use when building the inner text of the tag. + public virtual IEnumerable GetChildScopes(KeyScope scope, Dictionary arguments) + { + yield return scope; + } + + /// + /// Applies additional formatting to the inner text of the tag. + /// + /// The format provider to use. + /// The inner text of the tag. + /// The arguments passed to the tag. + /// The decorated inner text. + public virtual string Decorate(IFormatProvider provider, string innerText, Dictionary arguments) + { + return innerText; + } + + /// + /// Requests which generator group to associate the given tag type. + /// + /// The child tag definition being grouped. + /// The name of the group to associate the given tag with. + public virtual bool ShouldCreateSecondaryGroup(TagDefinition definition) + { + return false; + } + + /// + /// Gets whether the group with the given name should have text generated for them. + /// + /// The arguments passed to the tag. + /// True if text should be generated for the group; otherwise, false. + public virtual bool ShouldGeneratePrimaryGroup(Dictionary arguments) + { + return true; } } } diff --git a/mustache-sharp/TagParameter.cs b/mustache-sharp/TagParameter.cs index f838379..5b141c5 100644 --- a/mustache-sharp/TagParameter.cs +++ b/mustache-sharp/TagParameter.cs @@ -32,15 +32,6 @@ namespace mustache get { return _name; } } - /// - /// Gets or sets whether the parameter should be used to define the parameter. - /// - public bool IsScopeContext - { - get; - set; - } - /// /// Gets or sets whether the field is required. /// diff --git a/mustache-sharp/TagScope.cs b/mustache-sharp/TagScope.cs index 8f20127..0a0ddd2 100644 --- a/mustache-sharp/TagScope.cs +++ b/mustache-sharp/TagScope.cs @@ -1,29 +1,55 @@ using System; using System.Collections.Generic; +using mustache.Properties; namespace mustache { + /// + /// Represents a scope of tags. + /// internal sealed class TagScope { private readonly TagScope _parent; private readonly Dictionary _tagLookup; + /// + /// Initializes a new instance of a TagScope. + /// public TagScope() : this(null) { } + /// + /// Initializes a new instance of a TagScope. + /// + /// The parent scope to search for tag definitions. public TagScope(TagScope parent) { _parent = parent; _tagLookup = new Dictionary(); } - public void AddTag(TagDefinition tagDefinition) + /// + /// Registers the tag in the current scope. + /// + /// The tag to add to the current scope. + /// The tag already exists at the current scope. + public void AddTag(TagDefinition definition) { - _tagLookup.Add(tagDefinition.Name, tagDefinition); + if (Find(definition.Name) != null) + { + string message = String.Format(Resources.DuplicateTagDefinition, definition.Name); + throw new ArgumentException(Resources.DuplicateTagDefinition, "definition"); + } + _tagLookup.Add(definition.Name, definition); } + /// + /// Finds the tag definition with the given name. + /// + /// The name of the tag definition to search for. + /// The tag definition with the name -or- null if it does not exist. public TagDefinition Find(string tagName) { TagDefinition definition; diff --git a/mustache-sharp/WithGenerator.cs b/mustache-sharp/WithGenerator.cs new file mode 100644 index 0000000..ea1d8ac --- /dev/null +++ b/mustache-sharp/WithGenerator.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; + +namespace mustache +{ + /// + /// Defines a tag that changes the scope to the object passed as an argument. + /// + internal sealed class WithTagDefinition : TagDefinition + { + private const string contextParameter = "context"; + + /// + /// Initializes a new instance of a WithTagDefinition. + /// + public WithTagDefinition() + : base("with", true) + { + } + + /// + /// Gets the parameters that can be passed to the tag. + /// + /// The parameters. + protected override TagParameter[] GetParameters() + { + return new TagParameter[] { new TagParameter(contextParameter) { IsRequired = true } }; + } + + /// + /// Gets whether the tag has content. + /// + public override bool HasBody + { + get { return true; } + } + + /// + /// Gets the tags that come into scope within the tag. + /// + /// The child tag. + protected override TagDefinition[] GetChildTags() + { + return new TagDefinition[0]; + } + + /// + /// Gets the scopes to use for generating the tag's content. + /// + /// The current scope. + /// The arguments that were passed to the tag. + /// The scopes to use for generating the tag's content. + public override IEnumerable GetChildScopes(KeyScope scope, Dictionary arguments) + { + object context = arguments[contextParameter]; + yield return scope.CreateChildScope(context); + } + } +} diff --git a/mustache-sharp/mustache-sharp.csproj b/mustache-sharp/mustache-sharp.csproj index 72626ea..5dea571 100644 --- a/mustache-sharp/mustache-sharp.csproj +++ b/mustache-sharp/mustache-sharp.csproj @@ -34,9 +34,19 @@ + - + + + + + + + + + + True @@ -50,6 +60,7 @@ +