Implemented better custom tag handling.

I needed to make it easier to handle scopes and define custom tags,
including context-sensitive tags.
This commit is contained in:
Travis Parks 2013-01-12 14:53:12 -05:00
parent 49f9478c79
commit 7d75c7a2e4
22 changed files with 1394 additions and 547 deletions

View File

@ -1,10 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<TestSettings name="Local" id="2bc42439-1bb6-4112-9c20-eca1ffcae064" xmlns="http://microsoft.com/schemas/VisualStudio/TeamTest/2010"> <TestSettings name="Local" id="2bc42439-1bb6-4112-9c20-eca1ffcae064" xmlns="http://microsoft.com/schemas/VisualStudio/TeamTest/2010">
<Description>These are default test settings for a local test run.</Description> <Description>These are default test settings for a local test run.</Description>
<Deployment enabled="false" />
<Execution> <Execution>
<TestTypeSpecific /> <TestTypeSpecific>
<AgentRule name="Execution Agents"> <UnitTestRunConfig testTypeId="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b">
<AssemblyResolution>
<TestDirectory useLoadContext="true" />
</AssemblyResolution>
</UnitTestRunConfig>
</TestTypeSpecific>
<AgentRule name="LocalMachineDefaultRole">
<DataCollectors>
<DataCollector uri="datacollector://microsoft/CodeCoverage/1.0" assemblyQualifiedName="Microsoft.VisualStudio.TestTools.CodeCoverage.CoveragePlugIn, Microsoft.VisualStudio.QualityTools.Plugins.CodeCoverage, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" friendlyName="Code Coverage">
<Configuration>
<CodeCoverage xmlns="">
<Regular>
<CodeCoverageItem binaryFile="mustache-sharp\bin\Debug\mustache-sharp.dll" pdbFile="mustache-sharp\bin\Debug\mustache-sharp.pdb" instrumentInPlace="true" />
</Regular>
</CodeCoverage>
</Configuration>
</DataCollector>
</DataCollectors>
</AgentRule> </AgentRule>
</Execution> </Execution>
</TestSettings> </TestSettings>

View File

@ -0,0 +1,921 @@
using System;
using System.Globalization;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Collections.Generic;
namespace mustache.test
{
/// <summary>
/// Tests the FormatParser class.
/// </summary>
[TestClass]
public class FormatCompilerTester
{
#region Tagless Formats
/// <summary>
/// If the given format is null, an exception should be thrown.
/// </summary>
[TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void TestCompile_NullFormat_Throws()
{
FormatCompiler compiler = new FormatCompiler();
compiler.Compile(null);
}
/// <summary>
/// If the format string contains no tag, then the given format string
/// should be printed.
/// </summary>
[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.");
}
/// <summary>
/// If a line is just whitespace, it should be printed out as is.
/// </summary>
[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.");
}
/// <summary>
/// If a line has output, then the next line is blank, then both lines
/// should be printed.
/// </summary>
[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
/// <summary>
/// Replaces placeholds with the actual value.
/// </summary>
[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.");
}
/// <summary>
/// If we pass null as the source object and the format string contains "this",
/// then nothing should be printed.
/// </summary>
[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.");
}
/// <summary>
/// If we try to print a key that doesn't exist, an exception should be thrown.
/// </summary>
[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());
}
/// <summary>
/// If we specify an alignment with a key, the alignment should
/// be used when rending the value.
/// </summary>
[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.");
}
/// <summary>
/// If we specify an alignment with a key, the alignment should
/// be used when rending the value.
/// </summary>
[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.");
}
/// <summary>
/// If we specify a positive alignment with a key with an optional + character,
/// the alignment should be used when rending the value.
/// </summary>
[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.");
}
/// <summary>
/// If we specify an format with a key, the format should
/// be used when rending the value.
/// </summary>
[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.");
}
/// <summary>
/// If we specify an alignment with a key, the alignment should
/// be used when rending the value.
/// </summary>
[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.");
}
/// <summary>
/// If we dot separate keys, the value will be found by searching
/// through the properties.
/// </summary>
[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.");
}
/// <summary>
/// If a line has output, then the next line is blank, then both lines
/// should be printed.
/// </summary>
[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.");
}
/// <summary>
/// If there is a line followed by a line with a key, both lines should be
/// printed.
/// </summary>
[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.");
}
/// <summary>
/// If there is a no-output line followed by a line with a key, the first line
/// should be removed.
/// </summary>
[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.");
}
/// <summary>
/// If there is a comment on one line followed by a line with a key, the first line
/// should be removed.
/// </summary>
[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
/// <summary>
/// Removes comments from the middle of text.
/// </summary>
[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.");
}
/// <summary>
/// Removes comments surrounding text.
/// </summary>
[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.");
}
/// <summary>
/// If blank space is surrounded by comments, the line should be removed.
/// </summary>
[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.");
}
/// <summary>
/// If a comment follows text, the comment should be removed.
/// </summary>
[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.");
}
/// <summary>
/// If a comment follows text, the comment should be removed.
/// </summary>
[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.");
}
/// <summary>
/// If a comment makes up the entire format string, the nothing should be printed out.
/// </summary>
[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.");
}
/// <summary>
/// If a comment is on a line by itself, irrespective of leading or trailing whitespace,
/// the line should be removed from output.
/// </summary>
[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.");
}
/// <summary>
/// If multiple comments are on a line by themselves, irrespective of whitespace,
/// the line should be removed from output.
/// </summary>
[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.");
}
/// <summary>
/// If comments are on a multiple lines by themselves, irrespective of whitespace,
/// the lines should be removed from output.
/// </summary>
[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.");
}
/// <summary>
/// If a comment is followed by text, the line should be printed.
/// </summary>
[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.");
}
/// <summary>
/// If a comment is followed by the last line in a format string,
/// the comment line should be eliminated and the last line printed.
/// </summary>
[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.");
}
/// <summary>
/// If a comment is followed by the last line in a format string,
/// the comment line should be eliminated and the last line printed.
/// </summary>
[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.");
}
/// <summary>
/// If a line with content is followed by a line with a comment, the first line should
/// be printed.
/// </summary>
[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.");
}
/// <summary>
/// If a line has a comment, followed by line with content, followed by a line with a comment, only
/// the content should be printed.
/// </summary>
[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.");
}
/// <summary>
/// If there are lines with content, then a comment, then content, then a comment, only
/// the content should be printed.
/// </summary>
[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.");
}
/// <summary>
/// If there is content and a comment on a line, followed by a comment,
/// only the content should be printed.
/// </summary>
[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
/// <summary>
/// If the condition evaluates to false, the content of an if statement should not be printed.
/// </summary>
[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.");
}
/// <summary>
/// If the condition evaluates to false, the content of an if statement should not be printed.
/// </summary>
[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.");
}
/// <summary>
/// If the header and footer appear on lines by themselves, they should not generate new lines.
/// </summary>
[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.");
}
/// <summary>
/// If the header and footer appear on lines by themselves, they should not generate new lines.
/// </summary>
[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.");
}
/// <summary>
/// If the footer has content in front of it, the content should be printed.
/// </summary>
[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.");
}
/// <summary>
/// If the header has content after it, the content should be printed.
/// </summary>
[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.");
}
/// <summary>
/// If the header has content after it, the content should be printed.
/// </summary>
[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.");
}
/// <summary>
/// If the header and footer are adjacent, then there is no content.
/// </summary>
[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.");
}
/// <summary>
/// If the header and footer are adjacent, then there is no inner content.
/// </summary>
[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.");
}
/// <summary>
/// If the header and footer are adjacent, then there is no inner content.
/// </summary>
[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.");
}
/// <summary>
/// If the a header follows a footer, it shouldn't generate a new line.
/// </summary>
[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.");
}
/// <summary>
/// If the content separates two if statements, it should be unaffected.
/// </summary>
[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.");
}
/// <summary>
/// If there is trailing text of any kind, the newline after content should be preserved.
/// </summary>
[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
/// <summary>
/// If the condition evaluates to false, the content of an else statement should be printed.
/// </summary>
[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.");
}
/// <summary>
/// If the condition evaluates to true, the content of an if statement should be printed.
/// </summary>
[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.");
}
/// <summary>
/// Second else blocks will be interpreted as just another piece of text.
/// </summary>
[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
/// <summary>
/// If the if statement evaluates to true, its block should be printed.
/// </summary>
[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.");
}
/// <summary>
/// If the elif statement evaluates to true, its block should be printed.
/// </summary>
[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.");
}
/// <summary>
/// If the elif statement evaluates to false, the else block should be printed.
/// </summary>
[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
/// <summary>
/// If the elif statement evaluates to false and there is no else statement, nothing should be printed.
/// </summary>
[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.");
}
/// <summary>
/// If there are two elif statements and the first is false, the second elif block should be printed.
/// </summary>
[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
/// <summary>
/// If we pass an empty collection to an each statement, the content should not be printed.
/// </summary>
[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.");
}
/// <summary>
/// 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.
/// </summary>
[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
/// <summary>
/// The object replacing the placeholder should be used as the context of a with statement.
/// </summary>
[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
/// <summary>
/// If a tag is defined with a default parameter, the default value
/// should be returned if an argument is not provided.
/// </summary>
[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<TagParameter> GetParameters()
{
return new TagParameter[] { new TagParameter("param") { IsRequired = false, DefaultValue = 123 } };
}
public override string Decorate(IFormatProvider provider, Dictionary<string, object> arguments)
{
return arguments["param"].ToString();
}
}
#endregion
}
}

View File

@ -1,209 +0,0 @@
using System;
using System.Globalization;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace mustache.test
{
/// <summary>
/// Tests the FormatParser class.
/// </summary>
[TestClass]
public class FormatParserTester
{
/// <summary>
/// Replaces placeholds with the actual value.
/// </summary>
[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.");
}
/// <summary>
/// Removes comments from the output.
/// </summary>
[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.");
}
/// <summary>
/// If the condition evaluates to false, the content of an if statement should not be printed.
/// </summary>
[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.");
}
/// <summary>
/// If the condition evaluates to false, the content of an if statement should not be printed.
/// </summary>
[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.");
}
/// <summary>
/// If the condition evaluates to false, the content of an else statement should be printed.
/// </summary>
[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.");
}
/// <summary>
/// If the condition evaluates to true, the content of an if statement should be printed.
/// </summary>
[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.");
}
/// <summary>
/// Second else blocks will be interpreted as just another piece of text.
/// </summary>
[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.");
}
/// <summary>
/// If the if statement evaluates to true, its block should be printed.
/// </summary>
[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.");
}
/// <summary>
/// If the elif statement evaluates to true, its block should be printed.
/// </summary>
[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.");
}
/// <summary>
/// If the elif statement evaluates to false, the else block should be printed.
/// </summary>
[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.");
}
/// <summary>
/// If the elif statement evaluates to false and there is no else statement, nothing should be printed.
/// </summary>
[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.");
}
/// <summary>
/// If there are two elif statements and the first is false, the second elif block should be printed.
/// </summary>
[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.");
}
/// <summary>
/// If we pass an empty collection to an each statement, the content should not be printed.
/// </summary>
[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.");
}
/// <summary>
/// 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.
/// </summary>
[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.");
}
/// <summary>
/// The object replacing the placeholder should be used as the context of a with statement.
/// </summary>
[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.");
}
}
}

View File

@ -43,7 +43,7 @@
</CodeAnalysisDependentAssemblyPaths> </CodeAnalysisDependentAssemblyPaths>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Include="FormatParserTester.cs" /> <Compile Include="FormatCompilerTester.cs" />
<Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -11,7 +11,7 @@ namespace mustache
{ {
private readonly TagDefinition _definition; private readonly TagDefinition _definition;
private readonly ArgumentCollection _arguments; private readonly ArgumentCollection _arguments;
private readonly List<IGenerator> _primaryGenerators; private readonly LinkedList<IGenerator> _primaryGenerators;
private IGenerator _subGenerator; private IGenerator _subGenerator;
/// <summary> /// <summary>
@ -23,7 +23,7 @@ namespace mustache
{ {
_definition = definition; _definition = definition;
_arguments = arguments; _arguments = arguments;
_primaryGenerators = new List<IGenerator>(); _primaryGenerators = new LinkedList<IGenerator>();
} }
/// <summary> /// <summary>
@ -47,6 +47,19 @@ namespace mustache
addGenerator(generator, isSubGenerator); addGenerator(generator, isSubGenerator);
} }
/// <summary>
/// Creates a StaticGenerator from the given value and adds it.
/// </summary>
/// <param name="generators">The static generators to add.</param>
public void AddStaticGenerators(IEnumerable<StaticGenerator> generators)
{
foreach (StaticGenerator generator in generators)
{
LinkedListNode<IGenerator> node = _primaryGenerators.AddLast(generator);
generator.Node = node;
}
}
private void addGenerator(IGenerator generator, bool isSubGenerator) private void addGenerator(IGenerator generator, bool isSubGenerator)
{ {
if (isSubGenerator) if (isSubGenerator)
@ -55,7 +68,7 @@ namespace mustache
} }
else else
{ {
_primaryGenerators.Add(generator); _primaryGenerators.AddLast(generator);
} }
} }
@ -64,17 +77,17 @@ namespace mustache
StringBuilder builder = new StringBuilder(); StringBuilder builder = new StringBuilder();
Dictionary<string, object> arguments = _arguments.GetArguments(scope); Dictionary<string, object> arguments = _arguments.GetArguments(scope);
IEnumerable<KeyScope> scopes = _definition.GetChildScopes(scope, arguments); IEnumerable<KeyScope> scopes = _definition.GetChildScopes(scope, arguments);
List<IGenerator> generators; LinkedList<IGenerator> generators;
if (_definition.ShouldGeneratePrimaryGroup(arguments)) if (_definition.ShouldGeneratePrimaryGroup(arguments))
{ {
generators = _primaryGenerators; generators = _primaryGenerators;
} }
else else
{ {
generators = new List<IGenerator>(); generators = new LinkedList<IGenerator>();
if (_subGenerator != null) if (_subGenerator != null)
{ {
generators.Add(_subGenerator); generators.AddLast(_subGenerator);
} }
} }
foreach (KeyScope childScope in scopes) foreach (KeyScope childScope in scopes)

View File

@ -8,7 +8,7 @@ namespace mustache
/// <summary> /// <summary>
/// Defines a tag that conditionally prints its content. /// Defines a tag that conditionally prints its content.
/// </summary> /// </summary>
internal abstract class ConditionTagDefinition : TagDefinition internal abstract class ConditionTagDefinition : ContentTagDefinition
{ {
private const string conditionParameter = "condition"; private const string conditionParameter = "condition";
@ -25,30 +25,18 @@ namespace mustache
/// Gets the parameters that can be passed to the tag. /// Gets the parameters that can be passed to the tag.
/// </summary> /// </summary>
/// <returns>The parameters.</returns> /// <returns>The parameters.</returns>
protected override TagParameter[] GetParameters() protected override IEnumerable<TagParameter> GetParameters()
{ {
return new TagParameter[] { new TagParameter(conditionParameter) { IsRequired = true } }; return new TagParameter[] { new TagParameter(conditionParameter) { IsRequired = true } };
} }
/// <summary>
/// Gets whether the tag will contain content.
/// </summary>
public override bool HasBody
{
get { return true; }
}
/// <summary> /// <summary>
/// Gets the tags that come into scope within the context of the current tag. /// Gets the tags that come into scope within the context of the current tag.
/// </summary> /// </summary>
/// <returns>The child tag definitions.</returns> /// <returns>The child tag definitions.</returns>
protected override TagDefinition[] GetChildTags() protected override IEnumerable<string> GetChildTags()
{ {
return new TagDefinition[] return new string[] { "elif", "else" };
{
new ElifTagDefinition(),
new ElseTagDefinition(),
};
} }
/// <summary> /// <summary>
@ -58,7 +46,7 @@ namespace mustache
/// <returns>True if the tag's generator should be used as a secondary generator.</returns> /// <returns>True if the tag's generator should be used as a secondary generator.</returns>
public override bool ShouldCreateSecondaryGroup(TagDefinition definition) public override bool ShouldCreateSecondaryGroup(TagDefinition definition)
{ {
return (definition is ElifTagDefinition) || (definition is ElseTagDefinition); return new string[] { "elif", "else" }.Contains(definition.Name);
} }
/// <summary> /// <summary>

View File

@ -0,0 +1,38 @@
using System;
namespace mustache
{
/// <summary>
/// Defines a tag that can contain inner text.
/// </summary>
public abstract class ContentTagDefinition : TagDefinition
{
/// <summary>
/// Initializes a new instance of a ContentTagDefinition.
/// </summary>
/// <param name="tagName">The name of the tag being defined.</param>
protected ContentTagDefinition(string tagName)
: base(tagName)
{
}
/// <summary>
/// Initializes a new instance of a ContentTagDefinition.
/// </summary>
/// <param name="tagName">The name of the tag being defined.</param>
/// <param name="isBuiltin">Specifies whether the tag is a built-in tag.</param>
internal ContentTagDefinition(string tagName, bool isBuiltin)
: base(tagName, isBuiltin)
{
}
/// <summary>
/// Gets or sets whether the tag can have content.
/// </summary>
/// <returns>True if the tag can have a body; otherwise, false.</returns>
protected override bool GetHasContent()
{
return true;
}
}
}

View File

@ -8,7 +8,7 @@ namespace mustache
/// Defines a tag that can iterate over a collection of items and render /// Defines a tag that can iterate over a collection of items and render
/// the content using each item as the context. /// the content using each item as the context.
/// </summary> /// </summary>
internal sealed class EachTagDefinition : TagDefinition internal sealed class EachTagDefinition : ContentTagDefinition
{ {
private const string collectionParameter = "collection"; private const string collectionParameter = "collection";
@ -20,32 +20,23 @@ namespace mustache
{ {
} }
/// <summary>
/// Gets whether the tag only exists within the scope of its parent.
/// </summary>
protected override bool GetIsContextSensitive()
{
return false;
}
/// <summary> /// <summary>
/// Gets the parameters that can be passed to the tag. /// Gets the parameters that can be passed to the tag.
/// </summary> /// </summary>
/// <returns>The parameters.</returns> /// <returns>The parameters.</returns>
protected override TagParameter[] GetParameters() protected override IEnumerable<TagParameter> GetParameters()
{ {
return new TagParameter[] { new TagParameter(collectionParameter) { IsRequired = true } }; return new TagParameter[] { new TagParameter(collectionParameter) { IsRequired = true } };
} }
/// <summary>
/// Gets whether the tag has content.
/// </summary>
public override bool HasBody
{
get { return true; }
}
/// <summary>
/// Gets the tags that come into scope within the context of the tag.
/// </summary>
/// <returns>The tag definitions.</returns>
protected override TagDefinition[] GetChildTags()
{
return new TagDefinition[0];
}
/// <summary> /// <summary>
/// Gets the scopes for each of the items found in the argument. /// Gets the scopes for each of the items found in the argument.
/// </summary> /// </summary>
@ -65,5 +56,14 @@ namespace mustache
yield return scope.CreateChildScope(item); yield return scope.CreateChildScope(item);
} }
} }
/// <summary>
/// Gets the tags that are in scope under this tag.
/// </summary>
/// <returns>The name of the tags that are in scope.</returns>
protected override IEnumerable<string> GetChildTags()
{
return new string[] { };
}
} }
} }

View File

@ -16,12 +16,20 @@ namespace mustache
{ {
} }
/// <summary>
/// Gets whether the tag only exists within the scope of its parent.
/// </summary>
protected override bool GetIsContextSensitive()
{
return true;
}
/// <summary> /// <summary>
/// Gets the tags that indicate the end of the current tags context. /// Gets the tags that indicate the end of the current tags context.
/// </summary> /// </summary>
public override IEnumerable<TagDefinition> ClosingTags protected override IEnumerable<string> GetClosingTags()
{ {
get { return new TagDefinition[] { new IfTagDefinition() }; } return new string[] { "if" };
} }
} }
} }

View File

@ -6,7 +6,7 @@ namespace mustache
/// <summary> /// <summary>
/// Defines a tag that renders its content if all preceding if and elif tags. /// Defines a tag that renders its content if all preceding if and elif tags.
/// </summary> /// </summary>
internal sealed class ElseTagDefinition : TagDefinition internal sealed class ElseTagDefinition : ContentTagDefinition
{ {
/// <summary> /// <summary>
/// Initializes a new instance of a ElseTagDefinition. /// Initializes a new instance of a ElseTagDefinition.
@ -17,37 +17,19 @@ namespace mustache
} }
/// <summary> /// <summary>
/// Gets the parameters that can be passed to the tag. /// Gets whether the tag only exists within the scope of its parent.
/// </summary> /// </summary>
/// <returns>The parameters.</returns> protected override bool GetIsContextSensitive()
protected override TagParameter[] GetParameters()
{ {
return new TagParameter[0]; return true;
}
/// <summary>
/// Gets whether the tag contains content.
/// </summary>
public override bool HasBody
{
get { return true; }
} }
/// <summary> /// <summary>
/// Gets the tags that indicate the end of the current tag's content. /// Gets the tags that indicate the end of the current tag's content.
/// </summary> /// </summary>
public override IEnumerable<TagDefinition> ClosingTags protected override IEnumerable<string> GetClosingTags()
{ {
get { return new TagDefinition[] { new IfTagDefinition() }; } return new string[] { "if" };
}
/// <summary>
/// Gets the tags that come into scope within the context of the tag.
/// </summary>
/// <returns>The tag definitions.</returns>
protected override TagDefinition[] GetChildTags()
{
return new TagDefinition[0];
} }
} }
} }

View File

@ -12,33 +12,48 @@ namespace mustache
/// </summary> /// </summary>
public sealed class FormatCompiler public sealed class FormatCompiler
{ {
private const string key = @"[_\w][_\w\d]*"; private readonly Dictionary<string, TagDefinition> _tagLookup;
private const string compoundKey = key + @"(\." + key + ")*"; private readonly Dictionary<string, Regex> _regexLookup;
private readonly MasterTagDefinition _masterDefinition;
private readonly MasterTagDefinition _master;
private readonly TagScope _tagScope;
/// <summary> /// <summary>
/// Initializes a new instance of a FormatCompiler. /// Initializes a new instance of a FormatCompiler.
/// </summary> /// </summary>
public FormatCompiler() public FormatCompiler()
{ {
_master = new MasterTagDefinition(); _tagLookup = new Dictionary<string, TagDefinition>();
_tagScope = new TagScope(); _regexLookup = new Dictionary<string, Regex>();
registerTags(_master, _tagScope); _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);
} }
/// <summary> /// <summary>
/// Registers the given tag definition with the parser. /// Registers the given tag definition with the parser.
/// </summary> /// </summary>
/// <param name="definition">The tag definition to register.</param> /// <param name="definition">The tag definition to register.</param>
public void RegisterTag(TagDefinition definition) /// <param name="isTopLevel">Specifies whether the tag is immediately in scope.</param>
public void RegisterTag(TagDefinition definition, bool isTopLevel)
{ {
if (definition == null) if (definition == null)
{ {
throw new ArgumentNullException("definition"); 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);
} }
/// <summary> /// <summary>
@ -48,46 +63,61 @@ namespace mustache
/// <returns>The text generator.</returns> /// <returns>The text generator.</returns>
public Generator Compile(string format) 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(); 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); string trailing = format.Substring(formatIndex);
StaticGenerator staticGenerator = new StaticGenerator(trailing); generator.AddStaticGenerators(trimmer.RecordText(trailing, false, false));
generator.AddGenerator(staticGenerator); trimmer.Trim();
return new Generator(generator); 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) Regex regex = prepareRegex(definition);
{ return regex.Match(format, formatIndex);
scope.TryAddTag(childTag);
}
} }
private static Match findNextTag(TagDefinition definition, string format, int formatIndex) private Regex prepareRegex(TagDefinition definition)
{
Regex regex;
if (!_regexLookup.TryGetValue(definition.Name, out regex))
{ {
List<string> matches = new List<string>(); List<string> matches = new List<string>();
matches.Add(getKeyRegex()); matches.Add(getKeyRegex());
matches.Add(getCommentTagRegex()); matches.Add(getCommentTagRegex());
foreach (TagDefinition closingTag in definition.ClosingTags) foreach (string closingTag in definition.ClosingTags)
{ {
matches.Add(getClosingTagRegex(closingTag)); matches.Add(getClosingTagRegex(closingTag));
} }
foreach (TagDefinition childTag in definition.ChildTags) foreach (TagDefinition globalDefinition in _tagLookup.Values)
{ {
matches.Add(getTagRegex(childTag)); 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) + ")}}"; string match = "{{(" + String.Join("|", matches) + ")}}";
Regex regex = new Regex(match); regex = new Regex(match, RegexOptions.Compiled);
return regex.Match(format, formatIndex); _regexLookup.Add(definition.Name, regex);
}
return regex;
} }
private static string getClosingTagRegex(TagDefinition definition) private static string getClosingTagRegex(string tagName)
{ {
StringBuilder regexBuilder = new StringBuilder(); StringBuilder regexBuilder = new StringBuilder();
regexBuilder.Append(@"(?<close>(/(?<name>"); regexBuilder.Append(@"(?<close>(/(?<name>");
regexBuilder.Append(definition.Name); regexBuilder.Append(tagName);
regexBuilder.Append(@")\s*?))"); regexBuilder.Append(@")\s*?))");
return regexBuilder.ToString(); return regexBuilder.ToString();
} }
@ -99,7 +129,7 @@ namespace mustache
private static string getKeyRegex() private static string getKeyRegex()
{ {
return @"((?<key>" + compoundKey + @")(,(?<alignment>(-)?[\d]+))?(:(?<format>.*?))?)"; return @"((?<key>" + RegexHelper.CompoundKey + @")(,(?<alignment>(\+|-)?[\d]+))?(:(?<format>.*?))?)";
} }
private static string getTagRegex(TagDefinition definition) private static string getTagRegex(TagDefinition definition)
@ -110,18 +140,21 @@ namespace mustache
regexBuilder.Append(@")"); regexBuilder.Append(@")");
foreach (TagParameter parameter in definition.Parameters) foreach (TagParameter parameter in definition.Parameters)
{ {
regexBuilder.Append(@"\s+?"); regexBuilder.Append(@"(\s+?");
regexBuilder.Append(@"(?<argument>"); regexBuilder.Append(@"(?<argument>");
regexBuilder.Append(compoundKey); regexBuilder.Append(RegexHelper.CompoundKey);
regexBuilder.Append(@")"); regexBuilder.Append(@"))");
if (!parameter.IsRequired)
{
regexBuilder.Append("?");
}
} }
regexBuilder.Append(@"\s*?))"); regexBuilder.Append(@"\s*?))");
return regexBuilder.ToString(); return regexBuilder.ToString();
} }
private static int buildCompoundGenerator( private int buildCompoundGenerator(
TagDefinition tagDefinition, TagDefinition tagDefinition,
TagScope scope,
CompoundGenerator generator, CompoundGenerator generator,
Trimmer trimmer, Trimmer trimmer,
string format, int formatIndex) string format, int formatIndex)
@ -144,7 +177,7 @@ namespace mustache
if (match.Groups["key"].Success) if (match.Groups["key"].Success)
{ {
trimmer.AddStaticGenerator(generator, true, leading); generator.AddStaticGenerators(trimmer.RecordText(leading, true, true));
formatIndex = match.Index + match.Length; formatIndex = match.Index + match.Length;
string key = match.Groups["key"].Value; string key = match.Groups["key"].Value;
string alignment = match.Groups["alignment"].Value; string alignment = match.Groups["alignment"].Value;
@ -156,24 +189,23 @@ namespace mustache
{ {
formatIndex = match.Index + match.Length; formatIndex = match.Index + match.Length;
string tagName = match.Groups["name"].Value; string tagName = match.Groups["name"].Value;
TagDefinition nextDefinition = scope.Find(tagName); TagDefinition nextDefinition = _tagLookup[tagName];
if (nextDefinition == null) if (nextDefinition == null)
{ {
string message = String.Format(Resources.UnknownTag, tagName); string message = String.Format(Resources.UnknownTag, tagName);
throw new FormatException(message); throw new FormatException(message);
} }
trimmer.AddStaticGeneratorBeforeTag(generator, true, leading); if (nextDefinition.HasContent)
if (nextDefinition.HasBody)
{ {
generator.AddStaticGenerators(trimmer.RecordText(leading, true, false));
ArgumentCollection arguments = getArguments(nextDefinition, match); ArgumentCollection arguments = getArguments(nextDefinition, match);
CompoundGenerator compoundGenerator = new CompoundGenerator(nextDefinition, arguments); CompoundGenerator compoundGenerator = new CompoundGenerator(nextDefinition, arguments);
TagScope nextScope = new TagScope(scope); formatIndex = buildCompoundGenerator(nextDefinition, compoundGenerator, trimmer, format, formatIndex);
registerTags(nextDefinition, nextScope);
formatIndex = buildCompoundGenerator(nextDefinition, nextScope, compoundGenerator, trimmer, format, formatIndex);
generator.AddGenerator(nextDefinition, compoundGenerator); generator.AddGenerator(nextDefinition, compoundGenerator);
} }
else else
{ {
generator.AddStaticGenerators(trimmer.RecordText(leading, true, true));
Match nextMatch = findNextTag(nextDefinition, format, formatIndex); Match nextMatch = findNextTag(nextDefinition, format, formatIndex);
ArgumentCollection arguments = getArguments(nextDefinition, nextMatch); ArgumentCollection arguments = getArguments(nextDefinition, nextMatch);
InlineGenerator inlineGenerator = new InlineGenerator(nextDefinition, arguments); InlineGenerator inlineGenerator = new InlineGenerator(nextDefinition, arguments);
@ -182,9 +214,9 @@ namespace mustache
} }
else if (match.Groups["close"].Success) else if (match.Groups["close"].Success)
{ {
generator.AddStaticGenerators(trimmer.RecordText(leading, true, false));
string tagName = match.Groups["name"].Value; string tagName = match.Groups["name"].Value;
TagDefinition nextDefinition = scope.Find(tagName); TagDefinition nextDefinition = _tagLookup[tagName];
trimmer.AddStaticGeneratorBeforeTag(generator, false, leading);
formatIndex = match.Index; formatIndex = match.Index;
if (tagName == tagDefinition.Name) if (tagName == tagDefinition.Name)
{ {
@ -194,7 +226,7 @@ namespace mustache
} }
else if (match.Groups["comment"].Success) else if (match.Groups["comment"].Success)
{ {
trimmer.AddStaticGenerator(generator, false, leading); generator.AddStaticGenerators(trimmer.RecordText(leading, true, false));
formatIndex = match.Index + match.Length; formatIndex = match.Index + match.Length;
} }
} }

View File

@ -15,5 +15,13 @@ namespace mustache
: base("if") : base("if")
{ {
} }
/// <summary>
/// Gets whether the tag only exists within the scope of its parent.
/// </summary>
protected override bool GetIsContextSensitive()
{
return false;
}
} }
} }

View File

@ -0,0 +1,46 @@
using System;
using System.Collections.Generic;
namespace mustache
{
/// <summary>
/// Defines a tag that cannot contain inner text.
/// </summary>
public abstract class InlineTagDefinition : TagDefinition
{
/// <summary>
/// Initializes a new instance of an InlineTagDefinition.
/// </summary>
/// <param name="tagName">The name of the tag being defined.</param>
protected InlineTagDefinition(string tagName)
: base(tagName)
{
}
/// <summary>
/// Initializes a new instance of an InlineTagDefinition.
/// </summary>
/// <param name="tagName">The name of the tag being defined.</param>
/// <param name="isBuiltin">Specifies whether the tag is a built-in tag.</param>
internal InlineTagDefinition(string tagName, bool isBuiltin)
: base(tagName, isBuiltin)
{
}
/// <summary>
/// Gets or sets whether the tag can have content.
/// </summary>
/// <returns>True if the tag can have a body; otherwise, false.</returns>
protected override bool GetHasContent()
{
return false;
}
public sealed override string Decorate(IFormatProvider provider, string innerText, Dictionary<string, object> arguments)
{
return Decorate(provider, arguments);
}
public abstract string Decorate(IFormatProvider provider, Dictionary<string, object> arguments);
}
}

View File

@ -30,7 +30,7 @@ namespace mustache
if (!String.IsNullOrWhiteSpace(alignment)) if (!String.IsNullOrWhiteSpace(alignment))
{ {
formatBuilder.Append(","); formatBuilder.Append(",");
formatBuilder.Append(alignment); formatBuilder.Append(alignment.TrimStart('+'));
} }
if (!String.IsNullOrWhiteSpace(formatting)) if (!String.IsNullOrWhiteSpace(formatting))
{ {

View File

@ -6,7 +6,7 @@ namespace mustache
/// <summary> /// <summary>
/// Defines a pseudo tag that wraps the entire content of a format string. /// Defines a pseudo tag that wraps the entire content of a format string.
/// </summary> /// </summary>
internal sealed class MasterTagDefinition : TagDefinition internal sealed class MasterTagDefinition : ContentTagDefinition
{ {
/// <summary> /// <summary>
/// Initializes a new instance of a MasterTagDefinition. /// Initializes a new instance of a MasterTagDefinition.
@ -17,42 +17,20 @@ namespace mustache
} }
/// <summary> /// <summary>
/// Gets the parameters that can be passed to the tag. /// Gets whether the tag only exists within the scope of its parent.
/// </summary> /// </summary>
/// <returns>The parameters.</returns> protected override bool GetIsContextSensitive()
protected override TagParameter[] GetParameters()
{ {
return new TagParameter[0]; return true;
} }
/// <summary> /// <summary>
/// Gets whether the tag has content. /// Gets the name of the tags that indicate that the tag's context is closed.
/// </summary> /// </summary>
public override bool HasBody /// <returns>The tag names.</returns>
protected override IEnumerable<string> GetClosingTags()
{ {
get { return true; } return new string[] { };
}
/// <summary>
/// Gets the tags that indicate the end of the tags context.
/// </summary>
public override IEnumerable<TagDefinition> ClosingTags
{
get { return new TagDefinition[0]; }
}
/// <summary>
/// Gets the tags that come into scope within the context of the tag.
/// </summary>
/// <returns>The tags.</returns>
protected override TagDefinition[] GetChildTags()
{
return new TagDefinition[]
{
new IfTagDefinition(),
new EachTagDefinition(),
new WithTagDefinition(),
};
} }
} }
} }

View File

@ -8,6 +8,9 @@ namespace mustache
/// </summary> /// </summary>
public static class RegexHelper public static class RegexHelper
{ {
private const string Key = @"[_\w][_\w\d]*";
internal const string CompoundKey = Key + @"(\." + Key + ")*";
/// <summary> /// <summary>
/// Determines whether the given name is a legal identifier. /// Determines whether the given name is a legal identifier.
/// </summary> /// </summary>
@ -15,7 +18,7 @@ namespace mustache
/// <returns>True if the name is a legal identifier; otherwise, false.</returns> /// <returns>True if the name is a legal identifier; otherwise, false.</returns>
public static bool IsValidIdentifier(string name) public static bool IsValidIdentifier(string name)
{ {
Regex regex = new Regex(@"^[_\w][_\w\d]*$"); Regex regex = new Regex("^" + Key + "$");
return regex.IsMatch(name); return regex.IsMatch(name);
} }
} }

View File

@ -8,20 +8,46 @@ namespace mustache
/// </summary> /// </summary>
internal sealed class StaticGenerator : IGenerator internal sealed class StaticGenerator : IGenerator
{ {
private readonly string _value;
/// <summary> /// <summary>
/// Initializes a new instance of a StaticGenerator. /// Initializes a new instance of a StaticGenerator.
/// </summary> /// </summary>
/// <param name="value">The string to return.</param> public StaticGenerator()
public StaticGenerator(string value)
{ {
_value = value; }
/// <summary>
/// Gets or sets the linked list node containing the current generator.
/// </summary>
public LinkedListNode<IGenerator> Node
{
get;
set;
}
/// <summary>
/// Gets or sets the static text.
/// </summary>
public string Value
{
get;
set;
}
/// <summary>
/// Removes the static text from the final output.
/// </summary>
public void Prune()
{
if (Node != null)
{
Node.List.Remove(Node);
Node = null;
}
} }
string IGenerator.GetText(IFormatProvider provider, KeyScope scope) string IGenerator.GetText(IFormatProvider provider, KeyScope scope)
{ {
return _value; return Value;
} }
} }
} }

View File

@ -44,60 +44,87 @@ namespace mustache
get { return _tagName; } get { return _tagName; }
} }
/// <summary>
/// Gets whether the tag is limited to the parent tag's context.
/// </summary>
internal bool IsContextSensitive
{
get { return GetIsContextSensitive(); }
}
/// <summary>
/// Gets whether a tag is limited to the parent tag's context.
/// </summary>
protected abstract bool GetIsContextSensitive();
/// <summary> /// <summary>
/// Gets the parameters that are defined for the tag. /// Gets the parameters that are defined for the tag.
/// </summary> /// </summary>
public IEnumerable<TagParameter> Parameters internal IEnumerable<TagParameter> Parameters
{ {
get { return new ReadOnlyCollection<TagParameter>(GetParameters()); } get { return GetParameters(); }
} }
/// <summary> /// <summary>
/// Specifies which parameters are passed to the tag. /// Specifies which parameters are passed to the tag.
/// </summary> /// </summary>
/// <returns>The tag parameters.</returns> /// <returns>The tag parameters.</returns>
protected abstract TagParameter[] GetParameters(); protected virtual IEnumerable<TagParameter> GetParameters()
{
return new TagParameter[] { };
}
/// <summary> /// <summary>
/// Gets whether the tag contains content. /// Gets whether the tag contains content.
/// </summary> /// </summary>
public abstract bool HasBody internal bool HasContent
{ {
get; get { return GetHasContent(); }
} }
/// <summary>
/// Gets whether tag has content.
/// </summary>
/// <returns>True if the tag has content; otherwise, false.</returns>
protected abstract bool GetHasContent();
/// <summary> /// <summary>
/// Gets the tags that can indicate that the tag has closed. /// Gets the tags that can indicate that the tag has closed.
/// This field is only used if no closing tag is expected. /// This field is only used if no closing tag is expected.
/// </summary> /// </summary>
public virtual IEnumerable<TagDefinition> ClosingTags internal IEnumerable<string> ClosingTags
{ {
get get { return GetClosingTags(); }
}
protected virtual IEnumerable<string> GetClosingTags()
{ {
if (HasBody) if (HasContent)
{ {
return new TagDefinition[] { this }; return new string[] { Name };
} }
else else
{ {
return new TagDefinition[0]; return new string[] { };
}
} }
} }
/// <summary> /// <summary>
/// Gets the tags that are in scope within the current tag. /// Gets the tags that are in scope within the current tag.
/// </summary> /// </summary>
public IEnumerable<TagDefinition> ChildTags internal IEnumerable<string> ChildTags
{ {
get { return new ReadOnlyCollection<TagDefinition>(GetChildTags()); } get { return GetChildTags(); }
} }
/// <summary> /// <summary>
/// Specifies which tags are scoped under the current tag. /// Specifies which tags are scoped under the current tag.
/// </summary> /// </summary>
/// <returns>The child tag definitions.</returns> /// <returns>The child tag definitions.</returns>
protected abstract TagDefinition[] GetChildTags(); protected virtual IEnumerable<string> GetChildTags()
{
return new string[] { };
}
/// <summary> /// <summary>
/// Gets the scope to use when building the inner text of the tag. /// Gets the scope to use when building the inner text of the tag.

View File

@ -1,79 +0,0 @@
using System;
using System.Collections.Generic;
using mustache.Properties;
namespace mustache
{
/// <summary>
/// Represents a scope of tags.
/// </summary>
internal sealed class TagScope
{
private readonly TagScope _parent;
private readonly Dictionary<string, TagDefinition> _tagLookup;
/// <summary>
/// Initializes a new instance of a TagScope.
/// </summary>
public TagScope()
: this(null)
{
}
/// <summary>
/// Initializes a new instance of a TagScope.
/// </summary>
/// <param name="parent">The parent scope to search for tag definitions.</param>
public TagScope(TagScope parent)
{
_parent = parent;
_tagLookup = new Dictionary<string, TagDefinition>();
}
/// <summary>
/// Registers the tag in the current scope.
/// </summary>
/// <param name="definition">The tag to add to the current scope.</param>
/// <exception cref="System.ArgumentException">The tag already exists at the current scope.</exception>
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);
}
/// <summary>
/// Trys to register the tag in the current scope.
/// </summary>
/// <param name="definition">The tag to add to the current scope.</param>
public void TryAddTag(TagDefinition definition)
{
if (Find(definition.Name) == null)
{
_tagLookup.Add(definition.Name, definition);
}
}
/// <summary>
/// Finds the tag definition with the given name.
/// </summary>
/// <param name="tagName">The name of the tag definition to search for.</param>
/// <returns>The tag definition with the name -or- null if it does not exist.</returns>
public TagDefinition Find(string tagName)
{
TagDefinition definition;
if (_tagLookup.TryGetValue(tagName, out definition))
{
return definition;
}
if (_parent == null)
{
return null;
}
return _parent.Find(tagName);
}
}
}

View File

@ -1,90 +1,147 @@
using System; using System;
using System.Collections.Generic;
using System.Linq;
namespace mustache namespace mustache
{ {
/// <summary> /// <summary>
/// Removes unnecessary whitespace from static text. /// Removes unnecessary lines from the final output.
/// </summary> /// </summary>
internal sealed class Trimmer internal sealed class Trimmer
{ {
private bool hasHeader; private readonly LinkedList<LineDetails> _lines;
private bool hasFooter; private LinkedListNode<LineDetails> _currentLine;
private bool hasTag;
private bool canTrim;
/// <summary> /// <summary>
/// Initializes a new instance of a Trimmer. /// Initializes a new instance of a Trimmer.
/// </summary> /// </summary>
public Trimmer() public Trimmer()
{ {
hasTag = false; _lines = new LinkedList<LineDetails>();
canTrim = true; _currentLine = _lines.AddLast(new LineDetails());
} }
/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
/// <param name="generator">The compound generator to add the static generator to.</param> /// <param name="value">The text at the end of the format string.</param>
/// <param name="isHeader">Gets whether we're encountered the header tag.</param> /// <param name="generator">The generator created for the inline tag.</param>
/// <param name="value">The static text to trim.</param> /// <returns>A static generator containing the passed text.</returns>
public void AddStaticGeneratorBeforeTag(CompoundGenerator generator, bool isHeader, string value) public IEnumerable<StaticGenerator> RecordText(string value, bool isTag, bool isOutput)
{ {
string trimmed = processLines(value); int newLineIndex = value.IndexOf(Environment.NewLine);
hasHeader |= isHeader; if (newLineIndex == -1)
hasFooter |= hasHeader && !isHeader;
addStaticGenerator(generator, trimmed);
}
/// <summary>
/// Processes the given text, creating a StaticGenerator and adding it to the current compound generator.
/// </summary>
/// <param name="generator">The compound generator to add the static generator to.</param>
/// <param name="isOutput">Specifies whether the tag results in output.</param>
/// <param name="value">The static text to trim.</param>
public void AddStaticGenerator(CompoundGenerator generator, bool isOutput, string value)
{ {
string trimmed = processLines(value); StaticGenerator generator = new StaticGenerator() { Value = value };
canTrim &= !isOutput; _currentLine.Value.Generators.Add(generator);
addStaticGenerator(generator, trimmed); _currentLine.Value.HasTag |= isTag;
} _currentLine.Value.HasOutput |= !String.IsNullOrWhiteSpace(value);
yield return generator;
private string processLines(string value)
{
string trimmed = value;
int newline = value.IndexOf(Environment.NewLine);
if (newline == -1)
{
canTrim &= String.IsNullOrWhiteSpace(value);
} }
else else
{ {
// finish processing the previous line string[] lines = value.Split(new string[] { Environment.NewLine }, StringSplitOptions.None);
if (canTrim && hasTag && (!hasHeader || !hasFooter))
// 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); string middle = lines[lineIndex];
if (String.IsNullOrWhiteSpace(lineEnd)) StaticGenerator middleGenerator = new StaticGenerator() { Value = middle };
{ LineDetails middleDetails = new LineDetails() { HasTag = false };
trimmed = trimmed.Substring(newline + Environment.NewLine.Length); _currentLine = _lines.AddLast(middleDetails);
} _currentLine.Value.Generators.Add(middleGenerator);
} _currentLine.Value.HasOutput = true;
// start processing the next line yield return middleGenerator;
hasTag = false;
hasHeader = false;
hasFooter = false;
int lastNewline = value.LastIndexOf(Environment.NewLine);
string lineStart = value.Substring(lastNewline + Environment.NewLine.Length);
canTrim = String.IsNullOrWhiteSpace(lineStart);
}
return trimmed;
} }
private static void addStaticGenerator(CompoundGenerator generator, string trimmed) // get the leading generator
{ string leading = lines[lines.Length - 1];
if (trimmed.Length > 0) StaticGenerator leadingGenerator = new StaticGenerator() { Value = leading };
{ LineDetails details = new LineDetails() { HasTag = isTag };
StaticGenerator leading = new StaticGenerator(trimmed); _currentLine = _lines.AddLast(details);
generator.AddGenerator(leading); _currentLine.Value.Generators.Add(leadingGenerator);
_currentLine.Value.HasOutput = !String.IsNullOrWhiteSpace(leading);
yield return leadingGenerator;
} }
if (isOutput)
{
_currentLine.Value.HasOutput = true;
}
}
public void Trim()
{
removeBlankLines();
separateLines();
removeEmptyGenerators();
}
private void removeBlankLines()
{
LinkedListNode<LineDetails> current = _lines.First;
while (current != null)
{
LineDetails details = current.Value;
LinkedListNode<LineDetails> 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<LineDetails> current = _lines.First;
while (current != _lines.Last)
{
List<StaticGenerator> generators = current.Value.Generators;
StaticGenerator lastGenerator = generators[generators.Count - 1];
lastGenerator.Value += Environment.NewLine;
current = current.Next;
}
}
private void removeEmptyGenerators()
{
LinkedListNode<LineDetails> 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<StaticGenerator>();
}
public bool HasTag { get; set; }
public List<StaticGenerator> Generators { get; set; }
public bool HasOutput { get; set; }
} }
} }
} }

View File

@ -6,7 +6,7 @@ namespace mustache
/// <summary> /// <summary>
/// Defines a tag that changes the scope to the object passed as an argument. /// Defines a tag that changes the scope to the object passed as an argument.
/// </summary> /// </summary>
internal sealed class WithTagDefinition : TagDefinition internal sealed class WithTagDefinition : ContentTagDefinition
{ {
private const string contextParameter = "context"; private const string contextParameter = "context";
@ -18,32 +18,23 @@ namespace mustache
{ {
} }
/// <summary>
/// Gets whether the tag only exists within the scope of its parent.
/// </summary>
protected override bool GetIsContextSensitive()
{
return false;
}
/// <summary> /// <summary>
/// Gets the parameters that can be passed to the tag. /// Gets the parameters that can be passed to the tag.
/// </summary> /// </summary>
/// <returns>The parameters.</returns> /// <returns>The parameters.</returns>
protected override TagParameter[] GetParameters() protected override IEnumerable<TagParameter> GetParameters()
{ {
return new TagParameter[] { new TagParameter(contextParameter) { IsRequired = true } }; return new TagParameter[] { new TagParameter(contextParameter) { IsRequired = true } };
} }
/// <summary>
/// Gets whether the tag has content.
/// </summary>
public override bool HasBody
{
get { return true; }
}
/// <summary>
/// Gets the tags that come into scope within the tag.
/// </summary>
/// <returns>The child tag.</returns>
protected override TagDefinition[] GetChildTags()
{
return new TagDefinition[0];
}
/// <summary> /// <summary>
/// Gets the scopes to use for generating the tag's content. /// Gets the scopes to use for generating the tag's content.
/// </summary> /// </summary>

View File

@ -37,6 +37,8 @@
<Compile Include="ArgumentCollection.cs" /> <Compile Include="ArgumentCollection.cs" />
<Compile Include="CompoundGenerator.cs" /> <Compile Include="CompoundGenerator.cs" />
<Compile Include="ConditionTagDefinition.cs" /> <Compile Include="ConditionTagDefinition.cs" />
<Compile Include="ContentTagDefinition.cs" />
<Compile Include="InlineTagDefinition.cs" />
<Compile Include="EachTagDefinition.cs" /> <Compile Include="EachTagDefinition.cs" />
<Compile Include="ElifTagDefinition.cs" /> <Compile Include="ElifTagDefinition.cs" />
<Compile Include="ElseTagDefinition.cs" /> <Compile Include="ElseTagDefinition.cs" />
@ -59,7 +61,6 @@
<Compile Include="TagDefinition.cs" /> <Compile Include="TagDefinition.cs" />
<Compile Include="TagParameter.cs" /> <Compile Include="TagParameter.cs" />
<Compile Include="KeyScope.cs" /> <Compile Include="KeyScope.cs" />
<Compile Include="TagScope.cs" />
<Compile Include="Trimmer.cs" /> <Compile Include="Trimmer.cs" />
<Compile Include="WithGenerator.cs" /> <Compile Include="WithGenerator.cs" />
</ItemGroup> </ItemGroup>