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"?>
<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>
<Deployment enabled="false" />
<Execution>
<TestTypeSpecific />
<AgentRule name="Execution Agents">
<TestTypeSpecific>
<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>
</Execution>
</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>
</ItemGroup>
<ItemGroup>
<Compile Include="FormatParserTester.cs" />
<Compile Include="FormatCompilerTester.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>

View File

@ -11,7 +11,7 @@ namespace mustache
{
private readonly TagDefinition _definition;
private readonly ArgumentCollection _arguments;
private readonly List<IGenerator> _primaryGenerators;
private readonly LinkedList<IGenerator> _primaryGenerators;
private IGenerator _subGenerator;
/// <summary>
@ -23,7 +23,7 @@ namespace mustache
{
_definition = definition;
_arguments = arguments;
_primaryGenerators = new List<IGenerator>();
_primaryGenerators = new LinkedList<IGenerator>();
}
/// <summary>
@ -47,6 +47,19 @@ namespace mustache
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)
{
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<string, object> arguments = _arguments.GetArguments(scope);
IEnumerable<KeyScope> scopes = _definition.GetChildScopes(scope, arguments);
List<IGenerator> generators;
LinkedList<IGenerator> generators;
if (_definition.ShouldGeneratePrimaryGroup(arguments))
{
generators = _primaryGenerators;
}
else
{
generators = new List<IGenerator>();
generators = new LinkedList<IGenerator>();
if (_subGenerator != null)
{
generators.Add(_subGenerator);
generators.AddLast(_subGenerator);
}
}
foreach (KeyScope childScope in scopes)

View File

@ -8,7 +8,7 @@ namespace mustache
/// <summary>
/// Defines a tag that conditionally prints its content.
/// </summary>
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.
/// </summary>
/// <returns>The parameters.</returns>
protected override TagParameter[] GetParameters()
protected override IEnumerable<TagParameter> GetParameters()
{
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>
/// Gets the tags that come into scope within the context of the current tag.
/// </summary>
/// <returns>The child tag definitions.</returns>
protected override TagDefinition[] GetChildTags()
protected override IEnumerable<string> GetChildTags()
{
return new TagDefinition[]
{
new ElifTagDefinition(),
new ElseTagDefinition(),
};
return new string[] { "elif", "else" };
}
/// <summary>
@ -58,7 +46,7 @@ namespace mustache
/// <returns>True if the tag's generator should be used as a secondary generator.</returns>
public override bool ShouldCreateSecondaryGroup(TagDefinition definition)
{
return (definition is ElifTagDefinition) || (definition is ElseTagDefinition);
return new string[] { "elif", "else" }.Contains(definition.Name);
}
/// <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
/// the content using each item as the context.
/// </summary>
internal sealed class EachTagDefinition : TagDefinition
internal sealed class EachTagDefinition : ContentTagDefinition
{
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>
/// Gets the parameters that can be passed to the tag.
/// </summary>
/// <returns>The parameters.</returns>
protected override TagParameter[] GetParameters()
protected override IEnumerable<TagParameter> GetParameters()
{
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>
/// Gets the scopes for each of the items found in the argument.
/// </summary>
@ -65,5 +56,14 @@ namespace mustache
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>
/// Gets the tags that indicate the end of the current tags context.
/// </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>
/// Defines a tag that renders its content if all preceding if and elif tags.
/// </summary>
internal sealed class ElseTagDefinition : TagDefinition
internal sealed class ElseTagDefinition : ContentTagDefinition
{
/// <summary>
/// Initializes a new instance of a ElseTagDefinition.
@ -17,37 +17,19 @@ namespace mustache
}
/// <summary>
/// Gets the parameters that can be passed to the tag.
/// Gets whether the tag only exists within the scope of its parent.
/// </summary>
/// <returns>The parameters.</returns>
protected override TagParameter[] GetParameters()
protected override bool GetIsContextSensitive()
{
return new TagParameter[0];
}
/// <summary>
/// Gets whether the tag contains content.
/// </summary>
public override bool HasBody
{
get { return true; }
return true;
}
/// <summary>
/// Gets the tags that indicate the end of the current tag's content.
/// </summary>
public override IEnumerable<TagDefinition> ClosingTags
protected override IEnumerable<string> GetClosingTags()
{
get { return new TagDefinition[] { new IfTagDefinition() }; }
}
/// <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];
return new string[] { "if" };
}
}
}

View File

@ -12,33 +12,48 @@ namespace mustache
/// </summary>
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<string, TagDefinition> _tagLookup;
private readonly Dictionary<string, Regex> _regexLookup;
private readonly MasterTagDefinition _masterDefinition;
/// <summary>
/// Initializes a new instance of a FormatCompiler.
/// </summary>
public FormatCompiler()
{
_master = new MasterTagDefinition();
_tagScope = new TagScope();
registerTags(_master, _tagScope);
_tagLookup = new Dictionary<string, TagDefinition>();
_regexLookup = new Dictionary<string, Regex>();
_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>
/// Registers the given tag definition with the parser.
/// </summary>
/// <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)
{
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>
@ -48,46 +63,61 @@ namespace mustache
/// <returns>The text generator.</returns>
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);
}
Regex regex = prepareRegex(definition);
return regex.Match(format, formatIndex);
}
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>();
matches.Add(getKeyRegex());
matches.Add(getCommentTagRegex());
foreach (TagDefinition closingTag in definition.ClosingTags)
foreach (string closingTag in definition.ClosingTags)
{
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) + ")}}";
Regex regex = new Regex(match);
return regex.Match(format, formatIndex);
regex = new Regex(match, RegexOptions.Compiled);
_regexLookup.Add(definition.Name, regex);
}
return regex;
}
private static string getClosingTagRegex(TagDefinition definition)
private static string getClosingTagRegex(string tagName)
{
StringBuilder regexBuilder = new StringBuilder();
regexBuilder.Append(@"(?<close>(/(?<name>");
regexBuilder.Append(definition.Name);
regexBuilder.Append(tagName);
regexBuilder.Append(@")\s*?))");
return regexBuilder.ToString();
}
@ -99,7 +129,7 @@ namespace mustache
private static string getKeyRegex()
{
return @"((?<key>" + compoundKey + @")(,(?<alignment>(-)?[\d]+))?(:(?<format>.*?))?)";
return @"((?<key>" + RegexHelper.CompoundKey + @")(,(?<alignment>(\+|-)?[\d]+))?(:(?<format>.*?))?)";
}
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(@"(?<argument>");
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;
}
}

View File

@ -15,5 +15,13 @@ namespace mustache
: 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))
{
formatBuilder.Append(",");
formatBuilder.Append(alignment);
formatBuilder.Append(alignment.TrimStart('+'));
}
if (!String.IsNullOrWhiteSpace(formatting))
{

View File

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

View File

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

View File

@ -8,20 +8,46 @@ namespace mustache
/// </summary>
internal sealed class StaticGenerator : IGenerator
{
private readonly string _value;
/// <summary>
/// Initializes a new instance of a StaticGenerator.
/// </summary>
/// <param name="value">The string to return.</param>
public StaticGenerator(string value)
public StaticGenerator()
{
_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)
{
return _value;
return Value;
}
}
}

View File

@ -44,60 +44,87 @@ namespace mustache
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>
/// Gets the parameters that are defined for the tag.
/// </summary>
public IEnumerable<TagParameter> Parameters
internal IEnumerable<TagParameter> Parameters
{
get { return new ReadOnlyCollection<TagParameter>(GetParameters()); }
get { return GetParameters(); }
}
/// <summary>
/// Specifies which parameters are passed to the tag.
/// </summary>
/// <returns>The tag parameters.</returns>
protected abstract TagParameter[] GetParameters();
protected virtual IEnumerable<TagParameter> GetParameters()
{
return new TagParameter[] { };
}
/// <summary>
/// Gets whether the tag contains content.
/// </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>
/// Gets the tags that can indicate that the tag has closed.
/// This field is only used if no closing tag is expected.
/// </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
{
return new TagDefinition[0];
}
return new string[] { };
}
}
/// <summary>
/// Gets the tags that are in scope within the current tag.
/// </summary>
public IEnumerable<TagDefinition> ChildTags
internal IEnumerable<string> ChildTags
{
get { return new ReadOnlyCollection<TagDefinition>(GetChildTags()); }
get { return GetChildTags(); }
}
/// <summary>
/// Specifies which tags are scoped under the current tag.
/// </summary>
/// <returns>The child tag definitions.</returns>
protected abstract TagDefinition[] GetChildTags();
protected virtual IEnumerable<string> GetChildTags()
{
return new string[] { };
}
/// <summary>
/// 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.Collections.Generic;
using System.Linq;
namespace mustache
{
/// <summary>
/// Removes unnecessary whitespace from static text.
/// Removes unnecessary lines from the final output.
/// </summary>
internal sealed class Trimmer
{
private bool hasHeader;
private bool hasFooter;
private bool hasTag;
private bool canTrim;
private readonly LinkedList<LineDetails> _lines;
private LinkedListNode<LineDetails> _currentLine;
/// <summary>
/// Initializes a new instance of a Trimmer.
/// </summary>
public Trimmer()
{
hasTag = false;
canTrim = true;
_lines = new LinkedList<LineDetails>();
_currentLine = _lines.AddLast(new LineDetails());
}
/// <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>
/// <param name="generator">The compound generator to add the static generator to.</param>
/// <param name="isHeader">Gets whether we're encountered the header tag.</param>
/// <param name="value">The static text to trim.</param>
public void AddStaticGeneratorBeforeTag(CompoundGenerator generator, bool isHeader, string value)
/// <param name="value">The text at the end of the format string.</param>
/// <param name="generator">The generator created for the inline tag.</param>
/// <returns>A static generator containing the passed text.</returns>
public IEnumerable<StaticGenerator> RecordText(string value, bool isTag, bool isOutput)
{
string trimmed = processLines(value);
hasHeader |= isHeader;
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)
int newLineIndex = value.IndexOf(Environment.NewLine);
if (newLineIndex == -1)
{
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)
{
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);
}
}
// 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);
}
return trimmed;
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;
}
private static void addStaticGenerator(CompoundGenerator generator, string trimmed)
// 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)
{
if (trimmed.Length > 0)
_currentLine.Value.HasOutput = true;
}
}
public void Trim()
{
StaticGenerator leading = new StaticGenerator(trimmed);
generator.AddGenerator(leading);
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>
/// Defines a tag that changes the scope to the object passed as an argument.
/// </summary>
internal sealed class WithTagDefinition : TagDefinition
internal sealed class WithTagDefinition : ContentTagDefinition
{
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>
/// Gets the parameters that can be passed to the tag.
/// </summary>
/// <returns>The parameters.</returns>
protected override TagParameter[] GetParameters()
protected override IEnumerable<TagParameter> GetParameters()
{
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>
/// Gets the scopes to use for generating the tag's content.
/// </summary>

View File

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