diff --git a/Local.testsettings b/Local.testsettings
index 9654e6a..8ff3125 100644
--- a/Local.testsettings
+++ b/Local.testsettings
@@ -1,10 +1,26 @@
These are default test settings for a local test run.
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/mustache-sharp.test/FormatCompilerTester.cs b/mustache-sharp.test/FormatCompilerTester.cs
new file mode 100644
index 0000000..4514303
--- /dev/null
+++ b/mustache-sharp.test/FormatCompilerTester.cs
@@ -0,0 +1,921 @@
+using System;
+using System.Globalization;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using System.Collections.Generic;
+
+namespace mustache.test
+{
+ ///
+ /// Tests the FormatParser class.
+ ///
+ [TestClass]
+ public class FormatCompilerTester
+ {
+ #region Tagless Formats
+
+ ///
+ /// If the given format is null, an exception should be thrown.
+ ///
+ [TestMethod]
+ [ExpectedException(typeof(ArgumentNullException))]
+ public void TestCompile_NullFormat_Throws()
+ {
+ FormatCompiler compiler = new FormatCompiler();
+ compiler.Compile(null);
+ }
+
+ ///
+ /// If the format string contains no tag, then the given format string
+ /// should be printed.
+ ///
+ [TestMethod]
+ public void TestCompile_NoTags_PrintsFormatString()
+ {
+ FormatCompiler compiler = new FormatCompiler();
+ const string format = "This is an ordinary string.";
+ Generator generator = compiler.Compile(format);
+ string result = generator.Render(null);
+ Assert.AreEqual(format, result, "The generated text was wrong.");
+ }
+
+ ///
+ /// If a line is just whitespace, it should be printed out as is.
+ ///
+ [TestMethod]
+ public void TestCompile_LineAllWhitespace_PrintsWhitespace()
+ {
+ FormatCompiler compiler = new FormatCompiler();
+ const string format = "\t \t";
+ Generator generator = compiler.Compile(format);
+ string result = generator.Render(null);
+ Assert.AreEqual(format, result, "The generated text was wrong.");
+ }
+
+ ///
+ /// If a line has output, then the next line is blank, then both lines
+ /// should be printed.
+ ///
+ [TestMethod]
+ public void TestCompile_OutputNewLineBlank_PrintsBothLines()
+ {
+ FormatCompiler compiler = new FormatCompiler();
+ const string format = @"Hello
+ ";
+ Generator generator = compiler.Compile(format);
+ string result = generator.Render(null);
+ Assert.AreEqual(format, result, "The wrong text was generated.");
+ }
+
+ #endregion
+
+ #region Key
+
+ ///
+ /// Replaces placeholds with the actual value.
+ ///
+ [TestMethod]
+ public void TestCompile_Key_ReplacesWithValue()
+ {
+ FormatCompiler compiler = new FormatCompiler();
+ const string format = @"Hello, {{Name}}!!!";
+ Generator generator = compiler.Compile(format);
+ string result = generator.Render(new { Name = "Bob" });
+ Assert.AreEqual("Hello, Bob!!!", result, "The wrong text was generated.");
+ }
+
+ ///
+ /// If we pass null as the source object and the format string contains "this",
+ /// then nothing should be printed.
+ ///
+ [TestMethod]
+ public void TestCompile_ThisIsNull_PrintsNothing()
+ {
+ FormatCompiler compiler = new FormatCompiler();
+ Generator generator = compiler.Compile("{{this}}");
+ string result = generator.Render(null);
+ Assert.AreEqual(String.Empty, result, "The wrong text was generated.");
+ }
+
+ ///
+ /// If we try to print a key that doesn't exist, an exception should be thrown.
+ ///
+ [TestMethod]
+ [ExpectedException(typeof(KeyNotFoundException))]
+ public void TestCompile_MissingKey_Throws()
+ {
+ FormatCompiler compiler = new FormatCompiler();
+ const string format = @"Hello, {{Name}}!!!";
+ Generator generator = compiler.Compile(format);
+ generator.Render(new object());
+ }
+
+ ///
+ /// If we specify an alignment with a key, the alignment should
+ /// be used when rending the value.
+ ///
+ [TestMethod]
+ public void TestCompile_KeyWithNegativeAlignment_AppliesAlignment()
+ {
+ FormatCompiler compiler = new FormatCompiler();
+ const string format = @"Hello, {{Name,-10}}!!!";
+ Generator generator = compiler.Compile(format);
+ string result = generator.Render(new { Name = "Bob" });
+ Assert.AreEqual("Hello, Bob !!!", result, "The wrong text was generated.");
+ }
+
+ ///
+ /// If we specify an alignment with a key, the alignment should
+ /// be used when rending the value.
+ ///
+ [TestMethod]
+ public void TestCompile_KeyWithPositiveAlignment_AppliesAlignment()
+ {
+ FormatCompiler compiler = new FormatCompiler();
+ const string format = @"Hello, {{Name,10}}!!!";
+ Generator generator = compiler.Compile(format);
+ string result = generator.Render(new { Name = "Bob" });
+ Assert.AreEqual("Hello, Bob!!!", result, "The wrong text was generated.");
+ }
+
+ ///
+ /// If we specify a positive alignment with a key with an optional + character,
+ /// the alignment should be used when rending the value.
+ ///
+ [TestMethod]
+ public void TestCompile_KeyWithPositiveAlignment_OptionalPlus_AppliesAlignment()
+ {
+ FormatCompiler compiler = new FormatCompiler();
+ const string format = @"Hello, {{Name,+10}}!!!";
+ Generator generator = compiler.Compile(format);
+ string result = generator.Render(new { Name = "Bob" });
+ Assert.AreEqual("Hello, Bob!!!", result, "The wrong text was generated.");
+ }
+
+ ///
+ /// If we specify an format with a key, the format should
+ /// be used when rending the value.
+ ///
+ [TestMethod]
+ public void TestCompile_KeyWithFormat_AppliesFormatting()
+ {
+ FormatCompiler compiler = new FormatCompiler();
+ const string format = @"Hello, {{When:yyyyMMdd}}!!!";
+ Generator generator = compiler.Compile(format);
+ string result = generator.Render(new { When = new DateTime(2012, 01, 31) });
+ Assert.AreEqual("Hello, 20120131!!!", result, "The wrong text was generated.");
+ }
+
+ ///
+ /// If we specify an alignment with a key, the alignment should
+ /// be used when rending the value.
+ ///
+ [TestMethod]
+ public void TestCompile_KeyWithAlignmentAndFormat_AppliesBoth()
+ {
+ FormatCompiler compiler = new FormatCompiler();
+ const string format = @"Hello, {{When,10:yyyyMMdd}}!!!";
+ Generator generator = compiler.Compile(format);
+ string result = generator.Render(new { When = new DateTime(2012, 01, 31) });
+ Assert.AreEqual("Hello, 20120131!!!", result, "The wrong text was generated.");
+ }
+
+ ///
+ /// If we dot separate keys, the value will be found by searching
+ /// through the properties.
+ ///
+ [TestMethod]
+ public void TestCompile_NestedKeys_NestedProperties()
+ {
+ FormatCompiler compiler = new FormatCompiler();
+ const string format = @"Hello, {{Top.Middle.Bottom}}!!!";
+ Generator generator = compiler.Compile(format);
+ string result = generator.Render(new { Top = new { Middle = new { Bottom = "Bob" } } });
+ Assert.AreEqual("Hello, Bob!!!", result, "The wrong text was generated.");
+ }
+
+ ///
+ /// If a line has output, then the next line is blank, then both lines
+ /// should be printed.
+ ///
+ [TestMethod]
+ public void TestCompile_OutputNewLineOutput_PrintsBothLines()
+ {
+ FormatCompiler compiler = new FormatCompiler();
+ const string format = @"{{this}}
+After";
+ Generator generator = compiler.Compile(format);
+ string result = generator.Render("Content");
+ const string expected = @"Content
+After";
+ Assert.AreEqual(expected, result, "The wrong text was generated.");
+ }
+
+ ///
+ /// If there is a line followed by a line with a key, both lines should be
+ /// printed.
+ ///
+ [TestMethod]
+ public void TestCompile_EmptyNewLineKey_PrintsBothLines()
+ {
+ FormatCompiler compiler = new FormatCompiler();
+ const string format = @"
+{{this}}";
+ Generator generator = compiler.Compile(format);
+ string result = generator.Render("Content");
+ const string expected = @"
+Content";
+ Assert.AreEqual(expected, result, "The wrong text was generated.");
+ }
+
+ ///
+ /// If there is a no-output line followed by a line with a key, the first line
+ /// should be removed.
+ ///
+ [TestMethod]
+ public void TestCompile_NoOutputNewLineKey_PrintsBothLines()
+ {
+ FormatCompiler compiler = new FormatCompiler();
+ const string format = @"{{#! comment }}
+{{this}}";
+ Generator generator = compiler.Compile(format);
+ string result = generator.Render("Content");
+ const string expected = @"Content";
+ Assert.AreEqual(expected, result, "The wrong text was generated.");
+ }
+
+ ///
+ /// If there is a comment on one line followed by a line with a key, the first line
+ /// should be removed.
+ ///
+ [TestMethod]
+ public void TestCompile_KeyKey_PrintsBothLines()
+ {
+ FormatCompiler compiler = new FormatCompiler();
+ const string format = @"{{this}}
+{{this}}";
+ Generator generator = compiler.Compile(format);
+ string result = generator.Render("Content");
+ const string expected = @"Content
+Content";
+ Assert.AreEqual(expected, result, "The wrong text was generated.");
+ }
+
+ #endregion
+
+ #region Comment
+
+ ///
+ /// Removes comments from the middle of text.
+ ///
+ [TestMethod]
+ public void TestCompile_Comment_RemovesComment()
+ {
+ FormatCompiler compiler = new FormatCompiler();
+ const string format = "Before{{#! This is a comment }}After";
+ Generator generator = compiler.Compile(format);
+ string result = generator.Render(new object());
+ Assert.AreEqual("BeforeAfter", result, "The wrong text was generated.");
+ }
+
+ ///
+ /// Removes comments surrounding text.
+ ///
+ [TestMethod]
+ public void TestCompile_CommentContentComment_RemovesComment()
+ {
+ FormatCompiler compiler = new FormatCompiler();
+ const string format = "{{#! comment }}Middle{{#! comment }}";
+ Generator generator = compiler.Compile(format);
+ string result = generator.Render(new object());
+ Assert.AreEqual("Middle", result, "The wrong text was generated.");
+ }
+
+ ///
+ /// If blank space is surrounded by comments, the line should be removed.
+ ///
+ [TestMethod]
+ public void TestCompile_CommentBlankComment_RemovesLine()
+ {
+ FormatCompiler compiler = new FormatCompiler();
+ const string format = "{{#! comment }} {{#! comment }}";
+ Generator generator = compiler.Compile(format);
+ string result = generator.Render(new object());
+ Assert.AreEqual(String.Empty, result, "The wrong text was generated.");
+ }
+
+ ///
+ /// If a comment follows text, the comment should be removed.
+ ///
+ [TestMethod]
+ public void TestCompile_ContentComment_RemovesComment()
+ {
+ FormatCompiler compiler = new FormatCompiler();
+ const string format = "Front{{#! comment }}";
+ Generator generator = compiler.Compile(format);
+ string result = generator.Render(new object());
+ Assert.AreEqual("Front", result, "The wrong text was generated.");
+ }
+
+ ///
+ /// If a comment follows text, the comment should be removed.
+ ///
+ [TestMethod]
+ public void TestCompile_ContentCommentContentComment_RemovesComments()
+ {
+ FormatCompiler compiler = new FormatCompiler();
+ const string format = "Front{{#! comment }}Middle{{#! comment }}";
+ Generator generator = compiler.Compile(format);
+ string result = generator.Render(new object());
+ Assert.AreEqual("FrontMiddle", result, "The wrong text was generated.");
+ }
+
+ ///
+ /// If a comment makes up the entire format string, the nothing should be printed out.
+ ///
+ [TestMethod]
+ public void TestCompile_CommentAloneOnlyLine__PrintsEmpty()
+ {
+ FormatCompiler compiler = new FormatCompiler();
+ Generator generator = compiler.Compile(" {{#! comment }} ");
+ string result = generator.Render(null);
+ Assert.AreEqual(String.Empty, result, "The wrong text was generated.");
+ }
+
+ ///
+ /// If a comment is on a line by itself, irrespective of leading or trailing whitespace,
+ /// the line should be removed from output.
+ ///
+ [TestMethod]
+ public void TestCompile_ContentNewLineCommentNewLineContent_RemovesLine()
+ {
+ FormatCompiler compiler = new FormatCompiler();
+ const string format = @"Before
+ {{#! This is a comment }}
+After";
+ Generator generator = compiler.Compile(format);
+ string result = generator.Render(new object());
+ const string expected = @"Before
+After";
+ Assert.AreEqual(expected, result, "The wrong text was generated.");
+ }
+
+ ///
+ /// If multiple comments are on a line by themselves, irrespective of whitespace,
+ /// the line should be removed from output.
+ ///
+ [TestMethod]
+ public void TestCompile_ContentNewLineCommentCommentNewLineContent_RemovesLine()
+ {
+ FormatCompiler compiler = new FormatCompiler();
+ const string format = @"Before
+ {{#! This is a comment }} {{#! This is another comment }}
+After";
+ Generator generator = compiler.Compile(format);
+ string result = generator.Render(new object());
+ const string expected = @"Before
+After";
+ Assert.AreEqual(expected, result, "The wrong text was generated.");
+ }
+
+ ///
+ /// If comments are on a multiple lines by themselves, irrespective of whitespace,
+ /// the lines should be removed from output.
+ ///
+ [TestMethod]
+ public void TestCompile_CommentsOnMultipleLines_RemovesLines()
+ {
+ FormatCompiler compiler = new FormatCompiler();
+ const string format = @"Before
+ {{#! This is a comment }}
+ {{#! This is another comment }}
+
+ {{#! This is the final comment }}
+After";
+ Generator generator = compiler.Compile(format);
+ string result = generator.Render(new object());
+ const string expected = @"Before
+
+After";
+ Assert.AreEqual(expected, result, "The wrong text was generated.");
+ }
+
+ ///
+ /// If a comment is followed by text, the line should be printed.
+ ///
+ [TestMethod]
+ public void TestCompile_ContentNewLineCommentContentNewLineContent_PrintsLine()
+ {
+ FormatCompiler compiler = new FormatCompiler();
+ const string format = @"Before
+ {{#! This is a comment }}Extra
+After";
+ Generator generator = compiler.Compile(format);
+ string result = generator.Render(new object());
+ const string expected = @"Before
+ Extra
+After";
+ Assert.AreEqual(expected, result, "The wrong text was generated.");
+ }
+
+ ///
+ /// If a comment is followed by the last line in a format string,
+ /// the comment line should be eliminated and the last line printed.
+ ///
+ [TestMethod]
+ public void TestCompile_CommentNewLineBlank_PrintsBlank()
+ {
+ FormatCompiler compiler = new FormatCompiler();
+ const string format = @" {{#! comment }}
+ ";
+ Generator generator = compiler.Compile(format);
+ string result = generator.Render(null);
+ Assert.AreEqual(" ", result, "The wrong text was generated.");
+ }
+
+ ///
+ /// If a comment is followed by the last line in a format string,
+ /// the comment line should be eliminated and the last line printed.
+ ///
+ [TestMethod]
+ public void TestCompile_CommentNewLineContent_PrintsContent()
+ {
+ FormatCompiler compiler = new FormatCompiler();
+ const string format = @" {{#! comment }}
+After";
+ Generator generator = compiler.Compile(format);
+ string result = generator.Render(null);
+ Assert.AreEqual("After", result, "The wrong text was generated.");
+ }
+
+ ///
+ /// If a line with content is followed by a line with a comment, the first line should
+ /// be printed.
+ ///
+ [TestMethod]
+ public void TestCompile_ContentNewLineComment_PrintsContent()
+ {
+ FormatCompiler compiler = new FormatCompiler();
+ const string format = @"First
+{{#! comment }}";
+ Generator generator = compiler.Compile(format);
+ string result = generator.Render(null);
+ Assert.AreEqual("First", result, "The wrong text was generated.");
+ }
+
+ ///
+ /// If a line has a comment, followed by line with content, followed by a line with a comment, only
+ /// the content should be printed.
+ ///
+ [TestMethod]
+ public void TestCompile_CommentNewLineContentNewLineComment_PrintsContent()
+ {
+ FormatCompiler compiler = new FormatCompiler();
+ const string format = @"{{#! comment }}
+First
+{{#! comment }}";
+ Generator generator = compiler.Compile(format);
+ string result = generator.Render(null);
+ Assert.AreEqual("First", result, "The wrong text was generated.");
+ }
+
+ ///
+ /// If there are lines with content, then a comment, then content, then a comment, only
+ /// the content should be printed.
+ ///
+ [TestMethod]
+ public void TestCompile_ContentNewLineCommentNewLineContentNewLineComment_PrintsContent()
+ {
+ FormatCompiler compiler = new FormatCompiler();
+ const string format = @"First
+ {{#! comment }}
+Middle
+{{#! comment }}";
+ Generator generator = compiler.Compile(format);
+ string result = generator.Render(null);
+ const string expected = @"First
+Middle";
+ Assert.AreEqual(expected, result, "The wrong text was generated.");
+ }
+
+ ///
+ /// If there is content and a comment on a line, followed by a comment,
+ /// only the content should be printed.
+ ///
+ [TestMethod]
+ public void TestCompile_ContentCommentNewLineComment_PrintsContent()
+ {
+ FormatCompiler compiler = new FormatCompiler();
+ const string format = @"First{{#! comment }}
+{{#! comment }}";
+ Generator generator = compiler.Compile(format);
+ string result = generator.Render(null);
+ Assert.AreEqual("First", result, "The wrong text was generated.");
+ }
+
+ #endregion
+
+ #region If
+
+ ///
+ /// If the condition evaluates to false, the content of an if statement should not be printed.
+ ///
+ [TestMethod]
+ public void TestCompile_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 TestCompile_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 header and footer appear on lines by themselves, they should not generate new lines.
+ ///
+ [TestMethod]
+ public void TestCompile_IfNewLineContentNewLineEndIf_PrintsContent()
+ {
+ FormatCompiler parser = new FormatCompiler();
+ const string format = @"{{#if this}}
+Content
+{{/if}}";
+ Generator generator = parser.Compile(format);
+ string result = generator.Render(true);
+ Assert.AreEqual("Content", result, "The wrong text was generated.");
+ }
+
+ ///
+ /// If the header and footer appear on lines by themselves, they should not generate new lines.
+ ///
+ [TestMethod]
+ public void TestCompile_IfNewLineEndIf_PrintsNothing()
+ {
+ FormatCompiler parser = new FormatCompiler();
+ const string format = @"{{#if this}}
+{{/if}}";
+ Generator generator = parser.Compile(format);
+ string result = generator.Render(true);
+ Assert.AreEqual(String.Empty, result, "The wrong text was generated.");
+ }
+
+ ///
+ /// If the footer has content in front of it, the content should be printed.
+ ///
+ [TestMethod]
+ public void TestCompile_IfNewLineContentEndIf_PrintsContent()
+ {
+ FormatCompiler parser = new FormatCompiler();
+ const string format = @"{{#if this}}
+Content{{/if}}";
+ Generator generator = parser.Compile(format);
+ string result = generator.Render(true);
+ Assert.AreEqual("Content", result, "The wrong text was generated.");
+ }
+
+ ///
+ /// If the header has content after it, the content should be printed.
+ ///
+ [TestMethod]
+ public void TestCompile_IfContentNewLineEndIf_PrintsContent()
+ {
+ FormatCompiler parser = new FormatCompiler();
+ const string format = @"{{#if this}}Content
+{{/if}}";
+ Generator generator = parser.Compile(format);
+ string result = generator.Render(true);
+ Assert.AreEqual("Content", result, "The wrong text was generated.");
+ }
+
+ ///
+ /// If the header has content after it, the content should be printed.
+ ///
+ [TestMethod]
+ public void TestCompile_ContentIfNewLineEndIf_PrintsContent()
+ {
+ FormatCompiler parser = new FormatCompiler();
+ const string format = @"Content{{#if this}}
+{{/if}}";
+ Generator generator = parser.Compile(format);
+ string result = generator.Render(true);
+ Assert.AreEqual("Content", result, "The wrong text was generated.");
+ }
+
+ ///
+ /// If the header and footer are adjacent, then there is no content.
+ ///
+ [TestMethod]
+ public void TestCompile_IfEndIf_PrintsNothing()
+ {
+ FormatCompiler parser = new FormatCompiler();
+ const string format = @"{{#if this}}{{/if}}";
+ Generator generator = parser.Compile(format);
+ string result = generator.Render(true);
+ Assert.AreEqual(String.Empty, result, "The wrong text was generated.");
+ }
+
+ ///
+ /// If the header and footer are adjacent, then there is no inner content.
+ ///
+ [TestMethod]
+ public void TestCompile_ContentIfEndIf_PrintsNothing()
+ {
+ FormatCompiler parser = new FormatCompiler();
+ const string format = @"Content{{#if this}}{{/if}}";
+ Generator generator = parser.Compile(format);
+ string result = generator.Render(true);
+ Assert.AreEqual("Content", result, "The wrong text was generated.");
+ }
+
+ ///
+ /// If the header and footer are adjacent, then there is no inner content.
+ ///
+ [TestMethod]
+ public void TestCompile_IfNewLineCommentEndIf_PrintsNothing()
+ {
+ FormatCompiler parser = new FormatCompiler();
+ const string format = @"{{#if this}}
+{{#! comment}}{{/if}}";
+ Generator generator = parser.Compile(format);
+ string result = generator.Render(true);
+ Assert.AreEqual(String.Empty, result, "The wrong text was generated.");
+ }
+
+ ///
+ /// If the a header follows a footer, it shouldn't generate a new line.
+ ///
+ [TestMethod]
+ public void TestCompile_IfNewLineContentNewLineEndIfIfNewLineContenNewLineEndIf_PrintsContent()
+ {
+ FormatCompiler parser = new FormatCompiler();
+ const string format = @"{{#if this}}
+First
+{{/if}}{{#if this}}
+Last
+{{/if}}";
+ Generator generator = parser.Compile(format);
+ string result = generator.Render(true);
+ const string expected = @"First
+Last";
+ Assert.AreEqual(expected, result, "The wrong text was generated.");
+ }
+
+ ///
+ /// If the content separates two if statements, it should be unaffected.
+ ///
+ [TestMethod]
+ public void TestCompile_IfNewLineEndIfNewLineContentNewLineIfNewLineEndIf_PrintsContent()
+ {
+ FormatCompiler parser = new FormatCompiler();
+ const string format = @"{{#if this}}
+{{/if}}
+Content
+{{#if this}}
+{{/if}}";
+ Generator generator = parser.Compile(format);
+ string result = generator.Render(true);
+ const string expected = @"Content";
+ Assert.AreEqual(expected, result, "The wrong text was generated.");
+ }
+
+ ///
+ /// If there is trailing text of any kind, the newline after content should be preserved.
+ ///
+ [TestMethod]
+ public void TestCompile_IfNewLineEndIfNewLineContentNewLineIfNewLineEndIfContent_PrintsContent()
+ {
+ FormatCompiler parser = new FormatCompiler();
+ const string format = @"{{#if this}}
+{{/if}}
+First
+{{#if this}}
+{{/if}}
+Last";
+ Generator generator = parser.Compile(format);
+ string result = generator.Render(true);
+ const string expected = @"First
+Last";
+ Assert.AreEqual(expected, result, "The wrong text was generated.");
+ }
+
+ #endregion
+
+ #region If/Else
+
+ ///
+ /// If the condition evaluates to false, the content of an else statement should be printed.
+ ///
+ [TestMethod]
+ public void TestCompile_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 TestCompile_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 TestCompile_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.");
+ }
+
+ #endregion
+
+ #region If/Elif/Else
+
+ ///
+ /// If the if statement evaluates to true, its block should be printed.
+ ///
+ [TestMethod]
+ public void TestCompile_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 TestCompile_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 TestCompile_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.");
+ }
+
+ #endregion
+
+ #region If/Elif
+
+ ///
+ /// If the elif statement evaluates to false and there is no else statement, nothing should be printed.
+ ///
+ [TestMethod]
+ public void TestCompile_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 TestCompile_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.");
+ }
+
+ #endregion
+
+ #region Each
+
+ ///
+ /// If we pass an empty collection to an each statement, the content should not be printed.
+ ///
+ [TestMethod]
+ public void TestCompile_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 TestCompile_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.");
+ }
+
+ #endregion
+
+ #region With
+
+ ///
+ /// The object replacing the placeholder should be used as the context of a with statement.
+ ///
+ [TestMethod]
+ public void TestCompile_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.");
+ }
+
+ #endregion
+
+ #region Default Parameter
+
+ ///
+ /// If a tag is defined with a default parameter, the default value
+ /// should be returned if an argument is not provided.
+ ///
+ [TestMethod]
+ public void TestCompile_MissingDefaultParameter_ProvidesDefault()
+ {
+ FormatCompiler compiler = new FormatCompiler();
+ compiler.RegisterTag(new DefaultTagDefinition(), true);
+ const string format = @"{{#default}}";
+ Generator generator = compiler.Compile(format);
+ string result = generator.Render(null);
+ Assert.AreEqual("123", result, "The wrong text was generated.");
+ }
+
+ private sealed class DefaultTagDefinition : InlineTagDefinition
+ {
+ public DefaultTagDefinition()
+ : base("default")
+ {
+ }
+
+ protected override bool GetIsContextSensitive()
+ {
+ return false;
+ }
+
+ protected override IEnumerable GetParameters()
+ {
+ return new TagParameter[] { new TagParameter("param") { IsRequired = false, DefaultValue = 123 } };
+ }
+
+ public override string Decorate(IFormatProvider provider, Dictionary arguments)
+ {
+ return arguments["param"].ToString();
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/mustache-sharp.test/FormatParserTester.cs b/mustache-sharp.test/FormatParserTester.cs
deleted file mode 100644
index 47dbbbb..0000000
--- a/mustache-sharp.test/FormatParserTester.cs
+++ /dev/null
@@ -1,209 +0,0 @@
-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 66435c1..81de5b3 100644
--- a/mustache-sharp.test/mustache-sharp.test.csproj
+++ b/mustache-sharp.test/mustache-sharp.test.csproj
@@ -43,7 +43,7 @@
-
+
diff --git a/mustache-sharp/CompoundGenerator.cs b/mustache-sharp/CompoundGenerator.cs
index 7614ceb..c374f2b 100644
--- a/mustache-sharp/CompoundGenerator.cs
+++ b/mustache-sharp/CompoundGenerator.cs
@@ -11,7 +11,7 @@ namespace mustache
{
private readonly TagDefinition _definition;
private readonly ArgumentCollection _arguments;
- private readonly List _primaryGenerators;
+ private readonly LinkedList _primaryGenerators;
private IGenerator _subGenerator;
///
@@ -23,7 +23,7 @@ namespace mustache
{
_definition = definition;
_arguments = arguments;
- _primaryGenerators = new List();
+ _primaryGenerators = new LinkedList();
}
///
@@ -47,6 +47,19 @@ namespace mustache
addGenerator(generator, isSubGenerator);
}
+ ///
+ /// Creates a StaticGenerator from the given value and adds it.
+ ///
+ /// The static generators to add.
+ public void AddStaticGenerators(IEnumerable generators)
+ {
+ foreach (StaticGenerator generator in generators)
+ {
+ LinkedListNode node = _primaryGenerators.AddLast(generator);
+ generator.Node = node;
+ }
+ }
+
private void addGenerator(IGenerator generator, bool isSubGenerator)
{
if (isSubGenerator)
@@ -55,7 +68,7 @@ namespace mustache
}
else
{
- _primaryGenerators.Add(generator);
+ _primaryGenerators.AddLast(generator);
}
}
@@ -64,17 +77,17 @@ namespace mustache
StringBuilder builder = new StringBuilder();
Dictionary arguments = _arguments.GetArguments(scope);
IEnumerable scopes = _definition.GetChildScopes(scope, arguments);
- List generators;
+ LinkedList generators;
if (_definition.ShouldGeneratePrimaryGroup(arguments))
{
generators = _primaryGenerators;
}
else
{
- generators = new List();
+ generators = new LinkedList();
if (_subGenerator != null)
{
- generators.Add(_subGenerator);
+ generators.AddLast(_subGenerator);
}
}
foreach (KeyScope childScope in scopes)
diff --git a/mustache-sharp/ConditionTagDefinition.cs b/mustache-sharp/ConditionTagDefinition.cs
index f45e42d..8eae974 100644
--- a/mustache-sharp/ConditionTagDefinition.cs
+++ b/mustache-sharp/ConditionTagDefinition.cs
@@ -8,7 +8,7 @@ namespace mustache
///
/// Defines a tag that conditionally prints its content.
///
- internal abstract class ConditionTagDefinition : TagDefinition
+ internal abstract class ConditionTagDefinition : ContentTagDefinition
{
private const string conditionParameter = "condition";
@@ -25,30 +25,18 @@ namespace mustache
/// Gets the parameters that can be passed to the tag.
///
/// The parameters.
- protected override TagParameter[] GetParameters()
+ protected override IEnumerable 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()
+ protected override IEnumerable GetChildTags()
{
- return new TagDefinition[]
- {
- new ElifTagDefinition(),
- new ElseTagDefinition(),
- };
+ return new string[] { "elif", "else" };
}
///
@@ -58,7 +46,7 @@ namespace mustache
/// 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);
+ return new string[] { "elif", "else" }.Contains(definition.Name);
}
///
diff --git a/mustache-sharp/ContentTagDefinition.cs b/mustache-sharp/ContentTagDefinition.cs
new file mode 100644
index 0000000..2fe16df
--- /dev/null
+++ b/mustache-sharp/ContentTagDefinition.cs
@@ -0,0 +1,38 @@
+using System;
+
+namespace mustache
+{
+ ///
+ /// Defines a tag that can contain inner text.
+ ///
+ public abstract class ContentTagDefinition : TagDefinition
+ {
+ ///
+ /// Initializes a new instance of a ContentTagDefinition.
+ ///
+ /// The name of the tag being defined.
+ protected ContentTagDefinition(string tagName)
+ : base(tagName)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of a ContentTagDefinition.
+ ///
+ /// The name of the tag being defined.
+ /// Specifies whether the tag is a built-in tag.
+ internal ContentTagDefinition(string tagName, bool isBuiltin)
+ : base(tagName, isBuiltin)
+ {
+ }
+
+ ///
+ /// Gets or sets whether the tag can have content.
+ ///
+ /// True if the tag can have a body; otherwise, false.
+ protected override bool GetHasContent()
+ {
+ return true;
+ }
+ }
+}
diff --git a/mustache-sharp/EachTagDefinition.cs b/mustache-sharp/EachTagDefinition.cs
index 1d259a2..70d5257 100644
--- a/mustache-sharp/EachTagDefinition.cs
+++ b/mustache-sharp/EachTagDefinition.cs
@@ -8,7 +8,7 @@ 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
+ internal sealed class EachTagDefinition : ContentTagDefinition
{
private const string collectionParameter = "collection";
@@ -20,32 +20,23 @@ namespace mustache
{
}
+ ///
+ /// Gets whether the tag only exists within the scope of its parent.
+ ///
+ protected override bool GetIsContextSensitive()
+ {
+ return false;
+ }
+
///
/// Gets the parameters that can be passed to the tag.
///
/// The parameters.
- protected override TagParameter[] GetParameters()
+ protected override IEnumerable 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.
///
@@ -65,5 +56,14 @@ namespace mustache
yield return scope.CreateChildScope(item);
}
}
+
+ ///
+ /// Gets the tags that are in scope under this tag.
+ ///
+ /// The name of the tags that are in scope.
+ protected override IEnumerable GetChildTags()
+ {
+ return new string[] { };
+ }
}
}
diff --git a/mustache-sharp/ElifTagDefinition.cs b/mustache-sharp/ElifTagDefinition.cs
index f9aafa6..38b5601 100644
--- a/mustache-sharp/ElifTagDefinition.cs
+++ b/mustache-sharp/ElifTagDefinition.cs
@@ -15,13 +15,21 @@ namespace mustache
: base("elif")
{
}
+
+ ///
+ /// Gets whether the tag only exists within the scope of its parent.
+ ///
+ protected override bool GetIsContextSensitive()
+ {
+ return true;
+ }
///
/// Gets the tags that indicate the end of the current tags context.
///
- public override IEnumerable ClosingTags
+ protected override IEnumerable GetClosingTags()
{
- get { return new TagDefinition[] { new IfTagDefinition() }; }
+ return new string[] { "if" };
}
}
}
diff --git a/mustache-sharp/ElseTagDefinition.cs b/mustache-sharp/ElseTagDefinition.cs
index 6cfaff9..f3b1d1e 100644
--- a/mustache-sharp/ElseTagDefinition.cs
+++ b/mustache-sharp/ElseTagDefinition.cs
@@ -6,7 +6,7 @@ namespace mustache
///
/// Defines a tag that renders its content if all preceding if and elif tags.
///
- internal sealed class ElseTagDefinition : TagDefinition
+ internal sealed class ElseTagDefinition : ContentTagDefinition
{
///
/// Initializes a new instance of a ElseTagDefinition.
@@ -17,37 +17,19 @@ namespace mustache
}
///
- /// Gets the parameters that can be passed to the tag.
+ /// Gets whether the tag only exists within the scope of its parent.
///
- /// The parameters.
- protected override TagParameter[] GetParameters()
+ protected override bool GetIsContextSensitive()
{
- return new TagParameter[0];
- }
-
- ///
- /// Gets whether the tag contains content.
- ///
- public override bool HasBody
- {
- get { return true; }
+ return true;
}
///
/// Gets the tags that indicate the end of the current tag's content.
///
- public override IEnumerable ClosingTags
+ protected override IEnumerable GetClosingTags()
{
- 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];
+ return new string[] { "if" };
}
}
}
diff --git a/mustache-sharp/FormatCompiler.cs b/mustache-sharp/FormatCompiler.cs
index 73154ec..1f78aa9 100644
--- a/mustache-sharp/FormatCompiler.cs
+++ b/mustache-sharp/FormatCompiler.cs
@@ -12,33 +12,48 @@ namespace mustache
///
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;
+ private readonly Dictionary _tagLookup;
+ private readonly Dictionary _regexLookup;
+ private readonly MasterTagDefinition _masterDefinition;
///
/// Initializes a new instance of a FormatCompiler.
///
public FormatCompiler()
{
- _master = new MasterTagDefinition();
- _tagScope = new TagScope();
- registerTags(_master, _tagScope);
+ _tagLookup = new Dictionary();
+ _regexLookup = new Dictionary();
+ _masterDefinition = new MasterTagDefinition();
+
+ IfTagDefinition ifDefinition = new IfTagDefinition();
+ _tagLookup.Add(ifDefinition.Name, ifDefinition);
+ ElifTagDefinition elifDefinition = new ElifTagDefinition();
+ _tagLookup.Add(elifDefinition.Name, elifDefinition);
+ ElseTagDefinition elseDefinition = new ElseTagDefinition();
+ _tagLookup.Add(elseDefinition.Name, elseDefinition);
+ EachTagDefinition eachDefinition = new EachTagDefinition();
+ _tagLookup.Add(eachDefinition.Name, eachDefinition);
+ WithTagDefinition withDefinition = new WithTagDefinition();
+ _tagLookup.Add(withDefinition.Name, withDefinition);
}
///
/// Registers the given tag definition with the parser.
///
/// The tag definition to register.
- public void RegisterTag(TagDefinition definition)
+ /// Specifies whether the tag is immediately in scope.
+ public void RegisterTag(TagDefinition definition, bool isTopLevel)
{
if (definition == null)
{
throw new ArgumentNullException("definition");
}
- _tagScope.AddTag(definition);
+ if (_tagLookup.ContainsKey(definition.Name))
+ {
+ string message = String.Format(Resources.DuplicateTagDefinition, definition.Name);
+ throw new ArgumentException(message, "definition");
+ }
+ _tagLookup.Add(definition.Name, definition);
}
///
@@ -48,46 +63,61 @@ namespace mustache
/// The text generator.
public Generator Compile(string format)
{
- CompoundGenerator generator = new CompoundGenerator(_master, new ArgumentCollection());
+ if (format == null)
+ {
+ throw new ArgumentNullException("format");
+ }
+ CompoundGenerator generator = new CompoundGenerator(_masterDefinition, new ArgumentCollection());
Trimmer trimmer = new Trimmer();
- int formatIndex = buildCompoundGenerator(_master, _tagScope, generator, trimmer, format, 0);
+ int formatIndex = buildCompoundGenerator(_masterDefinition, generator, trimmer, format, 0);
string trailing = format.Substring(formatIndex);
- StaticGenerator staticGenerator = new StaticGenerator(trailing);
- generator.AddGenerator(staticGenerator);
+ generator.AddStaticGenerators(trimmer.RecordText(trailing, false, false));
+ trimmer.Trim();
return new Generator(generator);
}
- private static void registerTags(TagDefinition definition, TagScope scope)
+ private Match findNextTag(TagDefinition definition, string format, int formatIndex)
{
- foreach (TagDefinition childTag in definition.ChildTags)
- {
- scope.TryAddTag(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);
+ Regex regex = prepareRegex(definition);
return regex.Match(format, formatIndex);
}
- private static string getClosingTagRegex(TagDefinition definition)
+ private Regex prepareRegex(TagDefinition definition)
+ {
+ Regex regex;
+ if (!_regexLookup.TryGetValue(definition.Name, out regex))
+ {
+ List matches = new List();
+ matches.Add(getKeyRegex());
+ matches.Add(getCommentTagRegex());
+ foreach (string closingTag in definition.ClosingTags)
+ {
+ matches.Add(getClosingTagRegex(closingTag));
+ }
+ foreach (TagDefinition globalDefinition in _tagLookup.Values)
+ {
+ if (!globalDefinition.IsContextSensitive)
+ {
+ matches.Add(getTagRegex(globalDefinition));
+ }
+ }
+ foreach (string childTag in definition.ChildTags)
+ {
+ TagDefinition childDefinition = _tagLookup[childTag];
+ matches.Add(getTagRegex(childDefinition));
+ }
+ string match = "{{(" + String.Join("|", matches) + ")}}";
+ regex = new Regex(match, RegexOptions.Compiled);
+ _regexLookup.Add(definition.Name, regex);
+ }
+ return regex;
+ }
+
+ private static string getClosingTagRegex(string tagName)
{
StringBuilder regexBuilder = new StringBuilder();
regexBuilder.Append(@"(?(/(?");
- regexBuilder.Append(definition.Name);
+ regexBuilder.Append(tagName);
regexBuilder.Append(@")\s*?))");
return regexBuilder.ToString();
}
@@ -99,7 +129,7 @@ namespace mustache
private static string getKeyRegex()
{
- return @"((?" + compoundKey + @")(,(?(-)?[\d]+))?(:(?.*?))?)";
+ return @"((?" + RegexHelper.CompoundKey + @")(,(?(\+|-)?[\d]+))?(:(?.*?))?)";
}
private static string getTagRegex(TagDefinition definition)
@@ -110,18 +140,21 @@ namespace mustache
regexBuilder.Append(@")");
foreach (TagParameter parameter in definition.Parameters)
{
- regexBuilder.Append(@"\s+?");
+ regexBuilder.Append(@"(\s+?");
regexBuilder.Append(@"(?");
- regexBuilder.Append(compoundKey);
- regexBuilder.Append(@")");
+ regexBuilder.Append(RegexHelper.CompoundKey);
+ regexBuilder.Append(@"))");
+ if (!parameter.IsRequired)
+ {
+ regexBuilder.Append("?");
+ }
}
regexBuilder.Append(@"\s*?))");
return regexBuilder.ToString();
}
- private static int buildCompoundGenerator(
+ private int buildCompoundGenerator(
TagDefinition tagDefinition,
- TagScope scope,
CompoundGenerator generator,
Trimmer trimmer,
string format, int formatIndex)
@@ -144,7 +177,7 @@ namespace mustache
if (match.Groups["key"].Success)
{
- trimmer.AddStaticGenerator(generator, true, leading);
+ generator.AddStaticGenerators(trimmer.RecordText(leading, true, true));
formatIndex = match.Index + match.Length;
string key = match.Groups["key"].Value;
string alignment = match.Groups["alignment"].Value;
@@ -156,24 +189,23 @@ namespace mustache
{
formatIndex = match.Index + match.Length;
string tagName = match.Groups["name"].Value;
- TagDefinition nextDefinition = scope.Find(tagName);
+ TagDefinition nextDefinition = _tagLookup[tagName];
if (nextDefinition == null)
{
string message = String.Format(Resources.UnknownTag, tagName);
throw new FormatException(message);
}
- trimmer.AddStaticGeneratorBeforeTag(generator, true, leading);
- if (nextDefinition.HasBody)
+ if (nextDefinition.HasContent)
{
+ generator.AddStaticGenerators(trimmer.RecordText(leading, true, false));
ArgumentCollection arguments = getArguments(nextDefinition, match);
CompoundGenerator compoundGenerator = new CompoundGenerator(nextDefinition, arguments);
- TagScope nextScope = new TagScope(scope);
- registerTags(nextDefinition, nextScope);
- formatIndex = buildCompoundGenerator(nextDefinition, nextScope, compoundGenerator, trimmer, format, formatIndex);
+ formatIndex = buildCompoundGenerator(nextDefinition, compoundGenerator, trimmer, format, formatIndex);
generator.AddGenerator(nextDefinition, compoundGenerator);
}
else
{
+ generator.AddStaticGenerators(trimmer.RecordText(leading, true, true));
Match nextMatch = findNextTag(nextDefinition, format, formatIndex);
ArgumentCollection arguments = getArguments(nextDefinition, nextMatch);
InlineGenerator inlineGenerator = new InlineGenerator(nextDefinition, arguments);
@@ -182,9 +214,9 @@ namespace mustache
}
else if (match.Groups["close"].Success)
{
+ generator.AddStaticGenerators(trimmer.RecordText(leading, true, false));
string tagName = match.Groups["name"].Value;
- TagDefinition nextDefinition = scope.Find(tagName);
- trimmer.AddStaticGeneratorBeforeTag(generator, false, leading);
+ TagDefinition nextDefinition = _tagLookup[tagName];
formatIndex = match.Index;
if (tagName == tagDefinition.Name)
{
@@ -194,7 +226,7 @@ namespace mustache
}
else if (match.Groups["comment"].Success)
{
- trimmer.AddStaticGenerator(generator, false, leading);
+ generator.AddStaticGenerators(trimmer.RecordText(leading, true, false));
formatIndex = match.Index + match.Length;
}
}
diff --git a/mustache-sharp/IfTagDefinition.cs b/mustache-sharp/IfTagDefinition.cs
index 6e56397..1287f09 100644
--- a/mustache-sharp/IfTagDefinition.cs
+++ b/mustache-sharp/IfTagDefinition.cs
@@ -15,5 +15,13 @@ namespace mustache
: base("if")
{
}
+
+ ///
+ /// Gets whether the tag only exists within the scope of its parent.
+ ///
+ protected override bool GetIsContextSensitive()
+ {
+ return false;
+ }
}
}
diff --git a/mustache-sharp/InlineTagDefinition.cs b/mustache-sharp/InlineTagDefinition.cs
new file mode 100644
index 0000000..49626b2
--- /dev/null
+++ b/mustache-sharp/InlineTagDefinition.cs
@@ -0,0 +1,46 @@
+using System;
+using System.Collections.Generic;
+
+namespace mustache
+{
+ ///
+ /// Defines a tag that cannot contain inner text.
+ ///
+ public abstract class InlineTagDefinition : TagDefinition
+ {
+ ///
+ /// Initializes a new instance of an InlineTagDefinition.
+ ///
+ /// The name of the tag being defined.
+ protected InlineTagDefinition(string tagName)
+ : base(tagName)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of an InlineTagDefinition.
+ ///
+ /// The name of the tag being defined.
+ /// Specifies whether the tag is a built-in tag.
+ internal InlineTagDefinition(string tagName, bool isBuiltin)
+ : base(tagName, isBuiltin)
+ {
+ }
+
+ ///
+ /// Gets or sets whether the tag can have content.
+ ///
+ /// True if the tag can have a body; otherwise, false.
+ protected override bool GetHasContent()
+ {
+ return false;
+ }
+
+ public sealed override string Decorate(IFormatProvider provider, string innerText, Dictionary arguments)
+ {
+ return Decorate(provider, arguments);
+ }
+
+ public abstract string Decorate(IFormatProvider provider, Dictionary arguments);
+ }
+}
diff --git a/mustache-sharp/KeyGenerator.cs b/mustache-sharp/KeyGenerator.cs
index 09b8a9c..10f97b3 100644
--- a/mustache-sharp/KeyGenerator.cs
+++ b/mustache-sharp/KeyGenerator.cs
@@ -30,7 +30,7 @@ namespace mustache
if (!String.IsNullOrWhiteSpace(alignment))
{
formatBuilder.Append(",");
- formatBuilder.Append(alignment);
+ formatBuilder.Append(alignment.TrimStart('+'));
}
if (!String.IsNullOrWhiteSpace(formatting))
{
diff --git a/mustache-sharp/MasterTagDefinition.cs b/mustache-sharp/MasterTagDefinition.cs
index b9d5ad7..cd45f00 100644
--- a/mustache-sharp/MasterTagDefinition.cs
+++ b/mustache-sharp/MasterTagDefinition.cs
@@ -6,7 +6,7 @@ namespace mustache
///
/// Defines a pseudo tag that wraps the entire content of a format string.
///
- internal sealed class MasterTagDefinition : TagDefinition
+ internal sealed class MasterTagDefinition : ContentTagDefinition
{
///
/// Initializes a new instance of a MasterTagDefinition.
@@ -17,42 +17,20 @@ namespace mustache
}
///
- /// Gets the parameters that can be passed to the tag.
+ /// Gets whether the tag only exists within the scope of its parent.
///
- /// The parameters.
- protected override TagParameter[] GetParameters()
+ protected override bool GetIsContextSensitive()
{
- return new TagParameter[0];
+ return true;
}
///
- /// Gets whether the tag has content.
+ /// Gets the name of the tags that indicate that the tag's context is closed.
///
- public override bool HasBody
+ /// The tag names.
+ protected override IEnumerable GetClosingTags()
{
- 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(),
- };
+ return new string[] { };
}
}
}
diff --git a/mustache-sharp/RegexHelper.cs b/mustache-sharp/RegexHelper.cs
index a267910..38548c6 100644
--- a/mustache-sharp/RegexHelper.cs
+++ b/mustache-sharp/RegexHelper.cs
@@ -8,6 +8,9 @@ namespace mustache
///
public static class RegexHelper
{
+ private const string Key = @"[_\w][_\w\d]*";
+ internal const string CompoundKey = Key + @"(\." + Key + ")*";
+
///
/// Determines whether the given name is a legal identifier.
///
@@ -15,7 +18,7 @@ namespace mustache
/// True if the name is a legal identifier; otherwise, false.
public static bool IsValidIdentifier(string name)
{
- Regex regex = new Regex(@"^[_\w][_\w\d]*$");
+ Regex regex = new Regex("^" + Key + "$");
return regex.IsMatch(name);
}
}
diff --git a/mustache-sharp/StaticGenerator.cs b/mustache-sharp/StaticGenerator.cs
index 0d16e75..e83aab5 100644
--- a/mustache-sharp/StaticGenerator.cs
+++ b/mustache-sharp/StaticGenerator.cs
@@ -8,20 +8,46 @@ namespace mustache
///
internal sealed class StaticGenerator : IGenerator
{
- private readonly string _value;
-
///
/// Initializes a new instance of a StaticGenerator.
///
- /// The string to return.
- public StaticGenerator(string value)
+ public StaticGenerator()
{
- _value = value;
+ }
+
+ ///
+ /// Gets or sets the linked list node containing the current generator.
+ ///
+ public LinkedListNode Node
+ {
+ get;
+ set;
+ }
+
+ ///
+ /// Gets or sets the static text.
+ ///
+ public string Value
+ {
+ get;
+ set;
+ }
+
+ ///
+ /// Removes the static text from the final output.
+ ///
+ public void Prune()
+ {
+ if (Node != null)
+ {
+ Node.List.Remove(Node);
+ Node = null;
+ }
}
string IGenerator.GetText(IFormatProvider provider, KeyScope scope)
{
- return _value;
+ return Value;
}
}
}
diff --git a/mustache-sharp/TagDefinition.cs b/mustache-sharp/TagDefinition.cs
index f6538b5..aba8cd8 100644
--- a/mustache-sharp/TagDefinition.cs
+++ b/mustache-sharp/TagDefinition.cs
@@ -44,60 +44,87 @@ namespace mustache
get { return _tagName; }
}
+ ///
+ /// Gets whether the tag is limited to the parent tag's context.
+ ///
+ internal bool IsContextSensitive
+ {
+ get { return GetIsContextSensitive(); }
+ }
+
+ ///
+ /// Gets whether a tag is limited to the parent tag's context.
+ ///
+ protected abstract bool GetIsContextSensitive();
+
///
/// Gets the parameters that are defined for the tag.
///
- public IEnumerable Parameters
+ internal IEnumerable Parameters
{
- get { return new ReadOnlyCollection(GetParameters()); }
+ get { return GetParameters(); }
}
///
/// Specifies which parameters are passed to the tag.
///
/// The tag parameters.
- protected abstract TagParameter[] GetParameters();
+ protected virtual IEnumerable GetParameters()
+ {
+ return new TagParameter[] { };
+ }
///
/// Gets whether the tag contains content.
///
- public abstract bool HasBody
+ internal bool HasContent
{
- get;
+ get { return GetHasContent(); }
}
+ ///
+ /// Gets whether tag has content.
+ ///
+ /// True if the tag has content; otherwise, false.
+ protected abstract bool GetHasContent();
+
///
/// Gets the tags that can indicate that the tag has closed.
/// This field is only used if no closing tag is expected.
///
- public virtual IEnumerable ClosingTags
+ internal IEnumerable ClosingTags
{
- get
+ get { return GetClosingTags(); }
+ }
+
+ protected virtual IEnumerable GetClosingTags()
+ {
+ if (HasContent)
{
- if (HasBody)
- {
- return new TagDefinition[] { this };
- }
- else
- {
- return new TagDefinition[0];
- }
+ return new string[] { Name };
+ }
+ else
+ {
+ return new string[] { };
}
}
///
/// Gets the tags that are in scope within the current tag.
///
- public IEnumerable ChildTags
+ internal IEnumerable ChildTags
{
- get { return new ReadOnlyCollection(GetChildTags()); }
+ get { return GetChildTags(); }
}
///
/// Specifies which tags are scoped under the current tag.
///
/// The child tag definitions.
- protected abstract TagDefinition[] GetChildTags();
+ protected virtual IEnumerable GetChildTags()
+ {
+ return new string[] { };
+ }
///
/// Gets the scope to use when building the inner text of the tag.
diff --git a/mustache-sharp/TagScope.cs b/mustache-sharp/TagScope.cs
deleted file mode 100644
index 40684b9..0000000
--- a/mustache-sharp/TagScope.cs
+++ /dev/null
@@ -1,79 +0,0 @@
-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();
- }
-
- ///
- /// 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)
- {
- if (Find(definition.Name) != null)
- {
- string message = String.Format(Resources.DuplicateTagDefinition, definition.Name);
- throw new ArgumentException(Resources.DuplicateTagDefinition, "definition");
- }
- _tagLookup.Add(definition.Name, definition);
- }
-
- ///
- /// Trys to register the tag in the current scope.
- ///
- /// The tag to add to the current scope.
- public void TryAddTag(TagDefinition definition)
- {
- if (Find(definition.Name) == null)
- {
- _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;
- if (_tagLookup.TryGetValue(tagName, out definition))
- {
- return definition;
- }
- if (_parent == null)
- {
- return null;
- }
- return _parent.Find(tagName);
- }
- }
-}
diff --git a/mustache-sharp/Trimmer.cs b/mustache-sharp/Trimmer.cs
index 0f078ec..0b17abe 100644
--- a/mustache-sharp/Trimmer.cs
+++ b/mustache-sharp/Trimmer.cs
@@ -1,90 +1,147 @@
using System;
+using System.Collections.Generic;
+using System.Linq;
namespace mustache
{
///
- /// Removes unnecessary whitespace from static text.
+ /// Removes unnecessary lines from the final output.
///
internal sealed class Trimmer
{
- private bool hasHeader;
- private bool hasFooter;
- private bool hasTag;
- private bool canTrim;
+ private readonly LinkedList _lines;
+ private LinkedListNode _currentLine;
///
/// Initializes a new instance of a Trimmer.
///
public Trimmer()
{
- hasTag = false;
- canTrim = true;
+ _lines = new LinkedList();
+ _currentLine = _lines.AddLast(new LineDetails());
}
///
- /// Processes the given text, creating a StaticGenerator and adding it to the current compound generator.
+ /// Updates the state of the trimmer, indicating that the given text was encountered before an inline tag.
///
- /// The compound generator to add the static generator to.
- /// Gets whether we're encountered the header tag.
- /// The static text to trim.
- public void AddStaticGeneratorBeforeTag(CompoundGenerator generator, bool isHeader, string value)
+ /// The text at the end of the format string.
+ /// The generator created for the inline tag.
+ /// A static generator containing the passed text.
+ public IEnumerable RecordText(string value, bool isTag, bool isOutput)
{
- string trimmed = processLines(value);
- hasHeader |= isHeader;
- hasFooter |= hasHeader && !isHeader;
- addStaticGenerator(generator, trimmed);
- }
-
- ///
- /// Processes the given text, creating a StaticGenerator and adding it to the current compound generator.
- ///
- /// The compound generator to add the static generator to.
- /// Specifies whether the tag results in output.
- /// The static text to trim.
- public void AddStaticGenerator(CompoundGenerator generator, bool isOutput, string value)
- {
- string trimmed = processLines(value);
- canTrim &= !isOutput;
- addStaticGenerator(generator, trimmed);
- }
-
- private string processLines(string value)
- {
- string trimmed = value;
- int newline = value.IndexOf(Environment.NewLine);
- if (newline == -1)
+ int newLineIndex = value.IndexOf(Environment.NewLine);
+ if (newLineIndex == -1)
{
- canTrim &= String.IsNullOrWhiteSpace(value);
+ StaticGenerator generator = new StaticGenerator() { Value = value };
+ _currentLine.Value.Generators.Add(generator);
+ _currentLine.Value.HasTag |= isTag;
+ _currentLine.Value.HasOutput |= !String.IsNullOrWhiteSpace(value);
+ yield return generator;
}
else
{
- // finish processing the previous line
- if (canTrim && hasTag && (!hasHeader || !hasFooter))
+ string[] lines = value.Split(new string[] { Environment.NewLine }, StringSplitOptions.None);
+
+ // get the trailing generator
+ string trailing = lines[0];
+ StaticGenerator trailingGenerator = new StaticGenerator() { Value = trailing };
+ _currentLine.Value.Generators.Add(trailingGenerator);
+ _currentLine.Value.HasOutput |= !String.IsNullOrWhiteSpace(trailing);
+ yield return trailingGenerator;
+
+ // get the middle generators
+ for (int lineIndex = 1; lineIndex < lines.Length - 1; ++lineIndex)
{
- string lineEnd = trimmed.Substring(0, newline);
- if (String.IsNullOrWhiteSpace(lineEnd))
- {
- trimmed = trimmed.Substring(newline + Environment.NewLine.Length);
- }
+ string middle = lines[lineIndex];
+ StaticGenerator middleGenerator = new StaticGenerator() { Value = middle };
+ LineDetails middleDetails = new LineDetails() { HasTag = false };
+ _currentLine = _lines.AddLast(middleDetails);
+ _currentLine.Value.Generators.Add(middleGenerator);
+ _currentLine.Value.HasOutput = true;
+ yield return middleGenerator;
}
- // start processing the next line
- hasTag = false;
- hasHeader = false;
- hasFooter = false;
- int lastNewline = value.LastIndexOf(Environment.NewLine);
- string lineStart = value.Substring(lastNewline + Environment.NewLine.Length);
- canTrim = String.IsNullOrWhiteSpace(lineStart);
+
+ // get the leading generator
+ string leading = lines[lines.Length - 1];
+ StaticGenerator leadingGenerator = new StaticGenerator() { Value = leading };
+ LineDetails details = new LineDetails() { HasTag = isTag };
+ _currentLine = _lines.AddLast(details);
+ _currentLine.Value.Generators.Add(leadingGenerator);
+ _currentLine.Value.HasOutput = !String.IsNullOrWhiteSpace(leading);
+ yield return leadingGenerator;
+ }
+ if (isOutput)
+ {
+ _currentLine.Value.HasOutput = true;
}
- return trimmed;
}
- private static void addStaticGenerator(CompoundGenerator generator, string trimmed)
+ public void Trim()
{
- if (trimmed.Length > 0)
+ removeBlankLines();
+ separateLines();
+ removeEmptyGenerators();
+ }
+
+ private void removeBlankLines()
+ {
+ LinkedListNode current = _lines.First;
+ while (current != null)
{
- StaticGenerator leading = new StaticGenerator(trimmed);
- generator.AddGenerator(leading);
+ LineDetails details = current.Value;
+ LinkedListNode temp = current;
+ current = current.Next;
+ if (details.HasTag && !details.HasOutput)
+ {
+ foreach (StaticGenerator generator in temp.Value.Generators)
+ {
+ generator.Prune();
+ }
+ temp.List.Remove(temp);
+ }
}
}
+
+ private void separateLines()
+ {
+ LinkedListNode current = _lines.First;
+ while (current != _lines.Last)
+ {
+ List generators = current.Value.Generators;
+ StaticGenerator lastGenerator = generators[generators.Count - 1];
+ lastGenerator.Value += Environment.NewLine;
+ current = current.Next;
+ }
+ }
+
+ private void removeEmptyGenerators()
+ {
+ LinkedListNode current = _lines.First;
+ while (current != null)
+ {
+ foreach (StaticGenerator generator in current.Value.Generators)
+ {
+ if (generator.Value.Length == 0)
+ {
+ generator.Prune();
+ }
+ }
+ current = current.Next;
+ }
+ }
+
+ private sealed class LineDetails
+ {
+ public LineDetails()
+ {
+ Generators = new List();
+ }
+
+ public bool HasTag { get; set; }
+
+ public List Generators { get; set; }
+
+ public bool HasOutput { get; set; }
+ }
}
-}
\ No newline at end of file
+}
diff --git a/mustache-sharp/WithGenerator.cs b/mustache-sharp/WithGenerator.cs
index ea1d8ac..80bb8d8 100644
--- a/mustache-sharp/WithGenerator.cs
+++ b/mustache-sharp/WithGenerator.cs
@@ -6,7 +6,7 @@ namespace mustache
///
/// Defines a tag that changes the scope to the object passed as an argument.
///
- internal sealed class WithTagDefinition : TagDefinition
+ internal sealed class WithTagDefinition : ContentTagDefinition
{
private const string contextParameter = "context";
@@ -18,32 +18,23 @@ namespace mustache
{
}
+ ///
+ /// Gets whether the tag only exists within the scope of its parent.
+ ///
+ protected override bool GetIsContextSensitive()
+ {
+ return false;
+ }
+
///
/// Gets the parameters that can be passed to the tag.
///
/// The parameters.
- protected override TagParameter[] GetParameters()
+ protected override IEnumerable 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.
///
diff --git a/mustache-sharp/mustache-sharp.csproj b/mustache-sharp/mustache-sharp.csproj
index bc30af7..262b540 100644
--- a/mustache-sharp/mustache-sharp.csproj
+++ b/mustache-sharp/mustache-sharp.csproj
@@ -37,6 +37,8 @@
+
+
@@ -59,7 +61,6 @@
-