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:
parent
49f9478c79
commit
7d75c7a2e4
|
@ -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>
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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>
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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[] { };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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" };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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))
|
||||||
{
|
{
|
||||||
|
|
|
@ -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(),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in New Issue