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
+
-
+
+
+
+
+
+
+
+
+
+ True
@@ -50,6 +60,7 @@
+