Made progress implementing the object model, including all of the built-in tags.
This commit is contained in:
parent
f8628aaf86
commit
0b84ca8877
|
@ -0,0 +1,209 @@
|
|||
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,6 +43,7 @@
|
|||
</CodeAnalysisDependentAssemblyPaths>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="FormatParserTester.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace mustache
|
||||
{
|
||||
/// <summary>
|
||||
/// Associates parameters to their argument values.
|
||||
/// </summary>
|
||||
internal sealed class ArgumentCollection
|
||||
{
|
||||
private readonly Dictionary<TagParameter, string> _argumentLookup;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of an ArgumentCollection.
|
||||
/// </summary>
|
||||
public ArgumentCollection()
|
||||
{
|
||||
_argumentLookup = new Dictionary<TagParameter, string>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Associates the given parameter to the key placeholder.
|
||||
/// </summary>
|
||||
/// <param name="parameter">The parameter to associate the key with.</param>
|
||||
/// <param name="key">The key placeholder used as the argument.</param>
|
||||
/// <remarks>If the key is null, the default value of the parameter will be used.</remarks>
|
||||
public void AddArgument(TagParameter parameter, string key)
|
||||
{
|
||||
_argumentLookup.Add(parameter, key);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Substitutes the key placeholders with their respective values.
|
||||
/// </summary>
|
||||
/// <param name="scope">The current lexical scope.</param>
|
||||
/// <returns>A dictionary associating the parameter name to the associated value.</returns>
|
||||
public Dictionary<string, object> GetArguments(KeyScope scope)
|
||||
{
|
||||
Dictionary<string, object> arguments = new Dictionary<string,object>();
|
||||
foreach (KeyValuePair<TagParameter, string> pair in _argumentLookup)
|
||||
{
|
||||
object value;
|
||||
if (pair.Value == null)
|
||||
{
|
||||
value = pair.Key.DefaultValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
value = scope.Find(pair.Value);
|
||||
}
|
||||
arguments.Add(pair.Key.Name, value);
|
||||
}
|
||||
return arguments;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,30 +4,85 @@ using System.Text;
|
|||
|
||||
namespace mustache
|
||||
{
|
||||
/// <summary>
|
||||
/// Builds text by combining the output of other generators.
|
||||
/// </summary>
|
||||
internal sealed class CompoundGenerator : IGenerator
|
||||
{
|
||||
private readonly List<IGenerator> _generators;
|
||||
private readonly TagDefinition _definition;
|
||||
private readonly ArgumentCollection _arguments;
|
||||
private readonly List<IGenerator> _primaryGenerators;
|
||||
private IGenerator _subGenerator;
|
||||
|
||||
public CompoundGenerator()
|
||||
/// <summary>
|
||||
/// Initializes a new instance of a CompoundGenerator.
|
||||
/// </summary>
|
||||
/// <param name="definition">The tag that the text is being generated for.</param>
|
||||
/// <param name="arguments">The arguments that were passed to the tag.</param>
|
||||
public CompoundGenerator(TagDefinition definition, ArgumentCollection arguments)
|
||||
{
|
||||
_generators = new List<IGenerator>();
|
||||
_definition = definition;
|
||||
_arguments = arguments;
|
||||
_primaryGenerators = new List<IGenerator>();
|
||||
}
|
||||
|
||||
public void AddGenerator(StaticGenerator generator)
|
||||
/// <summary>
|
||||
/// Adds the given generator.
|
||||
/// </summary>
|
||||
/// <param name="generator">The generator to add.</param>
|
||||
public void AddGenerator(IGenerator generator)
|
||||
{
|
||||
_generators.Add(generator);
|
||||
addGenerator(generator, false);
|
||||
}
|
||||
|
||||
string IGenerator.GetText(object source)
|
||||
/// <summary>
|
||||
/// Adds the given generator, determining whether the generator should
|
||||
/// be part of the primary generators or added as an secondary generator.
|
||||
/// </summary>
|
||||
/// <param name="definition">The tag that the generator is generating text for.</param>
|
||||
/// <param name="generator">The generator to add.</param>
|
||||
public void AddGenerator(TagDefinition definition, IGenerator generator)
|
||||
{
|
||||
bool isSubGenerator = _definition.ShouldCreateSecondaryGroup(definition);
|
||||
addGenerator(generator, isSubGenerator);
|
||||
}
|
||||
|
||||
private void addGenerator(IGenerator generator, bool isSubGenerator)
|
||||
{
|
||||
if (isSubGenerator)
|
||||
{
|
||||
_subGenerator = generator;
|
||||
}
|
||||
else
|
||||
{
|
||||
_primaryGenerators.Add(generator);
|
||||
}
|
||||
}
|
||||
|
||||
string IGenerator.GetText(IFormatProvider provider, KeyScope scope)
|
||||
{
|
||||
StringBuilder builder = new StringBuilder();
|
||||
foreach (IGenerator generator in _generators)
|
||||
Dictionary<string, object> arguments = _arguments.GetArguments(scope);
|
||||
IEnumerable<KeyScope> scopes = _definition.GetChildScopes(scope, arguments);
|
||||
List<IGenerator> generators;
|
||||
if (_definition.ShouldGeneratePrimaryGroup(arguments))
|
||||
{
|
||||
builder.Append(generator.GetText(source));
|
||||
generators = _primaryGenerators;
|
||||
}
|
||||
else
|
||||
{
|
||||
generators = new List<IGenerator>() { _subGenerator };
|
||||
}
|
||||
foreach (KeyScope childScope in scopes)
|
||||
{
|
||||
foreach (IGenerator generator in generators)
|
||||
{
|
||||
builder.Append(generator.GetText(provider, childScope));
|
||||
}
|
||||
}
|
||||
string innerText = builder.ToString();
|
||||
// TODO - process with tag's custom handler
|
||||
return innerText;
|
||||
string outerText = _definition.Decorate(provider, innerText, arguments);
|
||||
return outerText;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,104 @@
|
|||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace mustache
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines a tag that conditionally prints its content.
|
||||
/// </summary>
|
||||
internal abstract class ConditionTagDefinition : TagDefinition
|
||||
{
|
||||
private const string conditionParameter = "condition";
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of a ConditionTagDefinition.
|
||||
/// </summary>
|
||||
/// <param name="tagName">The name of the tag.</param>
|
||||
protected ConditionTagDefinition(string tagName)
|
||||
: base(tagName, true)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the parameters that can be passed to the tag.
|
||||
/// </summary>
|
||||
/// <returns>The parameters.</returns>
|
||||
protected override TagParameter[] GetParameters()
|
||||
{
|
||||
return new TagParameter[] { new TagParameter(conditionParameter) { IsRequired = true } };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether the tag will contain content.
|
||||
/// </summary>
|
||||
public override bool HasBody
|
||||
{
|
||||
get { return true; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the tags that come into scope within the context of the current tag.
|
||||
/// </summary>
|
||||
/// <returns>The child tag definitions.</returns>
|
||||
protected override TagDefinition[] GetChildTags()
|
||||
{
|
||||
return new TagDefinition[]
|
||||
{
|
||||
new ElifTagDefinition(),
|
||||
new ElseTagDefinition(),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether the given tag's generator should be used for a secondary (or substitute) text block.
|
||||
/// </summary>
|
||||
/// <param name="definition">The tag to inspect.</param>
|
||||
/// <returns>True if the tag's generator should be used as a secondary generator.</returns>
|
||||
public override bool ShouldCreateSecondaryGroup(TagDefinition definition)
|
||||
{
|
||||
return (definition is ElifTagDefinition) || (definition is ElseTagDefinition);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether the primary generator group should be used to render the tag.
|
||||
/// </summary>
|
||||
/// <param name="arguments">The arguments passed to the tag.</param>
|
||||
/// <returns>
|
||||
/// True if the primary generator group should be used to render the tag;
|
||||
/// otherwise, false to use the secondary group.
|
||||
/// </returns>
|
||||
public override bool ShouldGeneratePrimaryGroup(Dictionary<string, object> arguments)
|
||||
{
|
||||
object condition = arguments[conditionParameter];
|
||||
return isConditionSatisfied(condition);
|
||||
}
|
||||
|
||||
private bool isConditionSatisfied(object condition)
|
||||
{
|
||||
if (condition == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
IEnumerable enumerable = condition as IEnumerable;
|
||||
if (enumerable != null)
|
||||
{
|
||||
return enumerable.Cast<object>().Any();
|
||||
}
|
||||
if (condition is Char)
|
||||
{
|
||||
return (Char)condition != '\0';
|
||||
}
|
||||
try
|
||||
{
|
||||
decimal number = (decimal)Convert.ChangeType(condition, typeof(decimal));
|
||||
return number != 0.0m;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace mustache
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines a tag that can iterate over a collection of items and render
|
||||
/// the content using each item as the context.
|
||||
/// </summary>
|
||||
internal sealed class EachTagDefinition : TagDefinition
|
||||
{
|
||||
private const string collectionParameter = "collection";
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of an EachTagDefinition.
|
||||
/// </summary>
|
||||
public EachTagDefinition()
|
||||
: base("each", true)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the parameters that can be passed to the tag.
|
||||
/// </summary>
|
||||
/// <returns>The parameters.</returns>
|
||||
protected override TagParameter[] GetParameters()
|
||||
{
|
||||
return new TagParameter[] { new TagParameter(collectionParameter) { IsRequired = true } };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether the tag has content.
|
||||
/// </summary>
|
||||
public override bool HasBody
|
||||
{
|
||||
get { return true; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the tags that come into scope within the context of the tag.
|
||||
/// </summary>
|
||||
/// <returns>The tag definitions.</returns>
|
||||
protected override TagDefinition[] GetChildTags()
|
||||
{
|
||||
return new TagDefinition[0];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the scopes for each of the items found in the argument.
|
||||
/// </summary>
|
||||
/// <param name="scope">The current scope.</param>
|
||||
/// <param name="arguments">The arguments passed to the tag.</param>
|
||||
/// <returns>The scopes for each of the items found in the argument.</returns>
|
||||
public override IEnumerable<KeyScope> GetChildScopes(KeyScope scope, Dictionary<string, object> arguments)
|
||||
{
|
||||
object value = arguments[collectionParameter];
|
||||
IEnumerable enumerable = value as IEnumerable;
|
||||
if (enumerable == null)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
foreach (object item in enumerable)
|
||||
{
|
||||
yield return scope.CreateChildScope(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace mustache
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines a tag that conditionally renders its content if preceding if and elif tags fail.
|
||||
/// </summary>
|
||||
internal sealed class ElifTagDefinition : ConditionTagDefinition
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of an ElifTagDefinition.
|
||||
/// </summary>
|
||||
public ElifTagDefinition()
|
||||
: base("elif")
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the tags that indicate the end of the current tags context.
|
||||
/// </summary>
|
||||
public override IEnumerable<TagDefinition> ClosingTags
|
||||
{
|
||||
get { return new TagDefinition[] { new IfTagDefinition() }; }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace mustache
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines a tag that renders its content if all preceding if and elif tags.
|
||||
/// </summary>
|
||||
internal sealed class ElseTagDefinition : TagDefinition
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of a ElseTagDefinition.
|
||||
/// </summary>
|
||||
public ElseTagDefinition()
|
||||
: base("else", true)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the parameters that can be passed to the tag.
|
||||
/// </summary>
|
||||
/// <returns>The parameters.</returns>
|
||||
protected override TagParameter[] GetParameters()
|
||||
{
|
||||
return new TagParameter[0];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether the tag contains content.
|
||||
/// </summary>
|
||||
public override bool HasBody
|
||||
{
|
||||
get { return true; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the tags that indicate the end of the current tag's content.
|
||||
/// </summary>
|
||||
public override IEnumerable<TagDefinition> ClosingTags
|
||||
{
|
||||
get { return new TagDefinition[] { new IfTagDefinition() }; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the tags that come into scope within the context of the tag.
|
||||
/// </summary>
|
||||
/// <returns>The tag definitions.</returns>
|
||||
protected override TagDefinition[] GetChildTags()
|
||||
{
|
||||
return new TagDefinition[0];
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,232 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using mustache.Properties;
|
||||
|
||||
namespace mustache
|
||||
{
|
||||
/// <summary>
|
||||
/// Parses a format string and returns a text generator.
|
||||
/// </summary>
|
||||
public sealed class FormatCompiler
|
||||
{
|
||||
private const string key = @"[_\w][_\w\d]*";
|
||||
private const string compoundKey = key + @"(\." + key + ")*";
|
||||
|
||||
private readonly MasterTagDefinition _master;
|
||||
private readonly TagScope _tagScope;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of a FormatCompiler.
|
||||
/// </summary>
|
||||
public FormatCompiler()
|
||||
{
|
||||
_master = new MasterTagDefinition();
|
||||
_tagScope = new TagScope();
|
||||
registerTags(_master, _tagScope);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers the given tag definition with the parser.
|
||||
/// </summary>
|
||||
/// <param name="definition">The tag definition to register.</param>
|
||||
public void RegisterTag(TagDefinition definition)
|
||||
{
|
||||
if (definition == null)
|
||||
{
|
||||
throw new ArgumentNullException("definition");
|
||||
}
|
||||
_tagScope.AddTag(definition);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a text generator based on the given format.
|
||||
/// </summary>
|
||||
/// <param name="format">The format to parse.</param>
|
||||
/// <returns>The text generator.</returns>
|
||||
public Generator Compile(string format)
|
||||
{
|
||||
CompoundGenerator generator = new CompoundGenerator(_master, new ArgumentCollection());
|
||||
int formatIndex = buildCompoundGenerator(_master, _tagScope, generator, format, 0);
|
||||
string trailing = format.Substring(formatIndex);
|
||||
StaticGenerator staticGenerator = new StaticGenerator(trailing);
|
||||
generator.AddGenerator(staticGenerator);
|
||||
return new Generator(generator);
|
||||
}
|
||||
|
||||
private static void registerTags(TagDefinition definition, TagScope scope)
|
||||
{
|
||||
foreach (TagDefinition childTag in definition.ChildTags)
|
||||
{
|
||||
scope.AddTag(childTag);
|
||||
}
|
||||
}
|
||||
|
||||
private static Match findNextTag(TagDefinition definition, string format, int formatIndex)
|
||||
{
|
||||
List<string> matches = new List<string>();
|
||||
matches.Add(getKeyRegex());
|
||||
matches.Add(getCommentTagRegex());
|
||||
foreach (TagDefinition closingTag in definition.ClosingTags)
|
||||
{
|
||||
matches.Add(getClosingTagRegex(closingTag));
|
||||
}
|
||||
foreach (TagDefinition childTag in definition.ChildTags)
|
||||
{
|
||||
matches.Add(getTagRegex(childTag));
|
||||
}
|
||||
string match = "{{(" + String.Join("|", matches) + ")}}";
|
||||
Regex regex = new Regex(match);
|
||||
return regex.Match(format, formatIndex);
|
||||
}
|
||||
|
||||
private static string getClosingTagRegex(TagDefinition definition)
|
||||
{
|
||||
StringBuilder regexBuilder = new StringBuilder();
|
||||
regexBuilder.Append(@"(?<close>(/(?<name>");
|
||||
regexBuilder.Append(definition.Name);
|
||||
regexBuilder.Append(@")\s*?))");
|
||||
return regexBuilder.ToString();
|
||||
}
|
||||
|
||||
private static string getCommentTagRegex()
|
||||
{
|
||||
return @"(?<comment>#!.*?)";
|
||||
}
|
||||
|
||||
private static string getKeyRegex()
|
||||
{
|
||||
return @"((?<key>" + compoundKey + @")(,(?<alignment>(-)?[\d]+))?(:(?<format>.*?))?)";
|
||||
}
|
||||
|
||||
private static string getTagRegex(TagDefinition definition)
|
||||
{
|
||||
StringBuilder regexBuilder = new StringBuilder();
|
||||
regexBuilder.Append(@"(?<open>(#(?<name>");
|
||||
regexBuilder.Append(definition.Name);
|
||||
regexBuilder.Append(@")");
|
||||
foreach (TagParameter parameter in definition.Parameters)
|
||||
{
|
||||
regexBuilder.Append(@"\s+?");
|
||||
regexBuilder.Append(@"(?<argument>");
|
||||
regexBuilder.Append(compoundKey);
|
||||
regexBuilder.Append(@")");
|
||||
}
|
||||
regexBuilder.Append(@"\s*?))");
|
||||
return regexBuilder.ToString();
|
||||
}
|
||||
|
||||
private static int buildCompoundGenerator(
|
||||
TagDefinition tagDefinition,
|
||||
TagScope scope,
|
||||
CompoundGenerator generator,
|
||||
string format, int formatIndex)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
Match match = findNextTag(tagDefinition, format, formatIndex);
|
||||
|
||||
if (!match.Success)
|
||||
{
|
||||
if (tagDefinition.ClosingTags.Any())
|
||||
{
|
||||
string message = String.Format(Resources.MissingClosingTag, tagDefinition.Name);
|
||||
throw new FormatException(message);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
string leading = format.Substring(formatIndex, match.Index - formatIndex);
|
||||
StaticGenerator staticGenerator = new StaticGenerator(leading);
|
||||
generator.AddGenerator(staticGenerator);
|
||||
|
||||
if (match.Groups["key"].Success)
|
||||
{
|
||||
formatIndex = match.Index + match.Length;
|
||||
string key = match.Groups["key"].Value;
|
||||
string alignment = match.Groups["alignment"].Value;
|
||||
string formatting = match.Groups["format"].Value;
|
||||
KeyGenerator keyGenerator = new KeyGenerator(key, alignment, formatting);
|
||||
generator.AddGenerator(keyGenerator);
|
||||
}
|
||||
else if (match.Groups["open"].Success)
|
||||
{
|
||||
formatIndex = match.Index + match.Length;
|
||||
string tagName = match.Groups["name"].Value;
|
||||
TagDefinition nextDefinition = scope.Find(tagName);
|
||||
if (nextDefinition == null)
|
||||
{
|
||||
string message = String.Format(Resources.UnknownTag, tagName);
|
||||
throw new FormatException(message);
|
||||
}
|
||||
if (nextDefinition.HasBody)
|
||||
{
|
||||
ArgumentCollection arguments = getArguments(nextDefinition, match);
|
||||
CompoundGenerator compoundGenerator = new CompoundGenerator(nextDefinition, arguments);
|
||||
TagScope nextScope = new TagScope(scope);
|
||||
registerTags(nextDefinition, nextScope);
|
||||
formatIndex = buildCompoundGenerator(nextDefinition, nextScope, compoundGenerator, format, formatIndex);
|
||||
generator.AddGenerator(nextDefinition, compoundGenerator);
|
||||
}
|
||||
else
|
||||
{
|
||||
Match nextMatch = findNextTag(nextDefinition, format, formatIndex);
|
||||
ArgumentCollection arguments = getArguments(nextDefinition, nextMatch);
|
||||
InlineGenerator inlineGenerator = new InlineGenerator(nextDefinition, arguments);
|
||||
generator.AddGenerator(inlineGenerator);
|
||||
}
|
||||
}
|
||||
else if (match.Groups["close"].Success)
|
||||
{
|
||||
string tagName = match.Groups["name"].Value;
|
||||
formatIndex = match.Index;
|
||||
if (tagName == tagDefinition.Name)
|
||||
{
|
||||
formatIndex += match.Length;
|
||||
}
|
||||
break;
|
||||
}
|
||||
else if (match.Groups["comment"].Success)
|
||||
{
|
||||
formatIndex = match.Index + match.Length;
|
||||
}
|
||||
}
|
||||
return formatIndex;
|
||||
}
|
||||
|
||||
private static ArgumentCollection getArguments(TagDefinition definition, Match match)
|
||||
{
|
||||
ArgumentCollection collection = new ArgumentCollection();
|
||||
List<Capture> captures = match.Groups["argument"].Captures.Cast<Capture>().ToList();
|
||||
List<TagParameter> parameters = definition.Parameters.ToList();
|
||||
if (captures.Count > parameters.Count)
|
||||
{
|
||||
string message = String.Format(Resources.WrongNumberOfArguments, definition.Name);
|
||||
throw new FormatException(message);
|
||||
}
|
||||
if (captures.Count < parameters.Count)
|
||||
{
|
||||
captures.AddRange(Enumerable.Repeat((Capture)null, parameters.Count - captures.Count));
|
||||
}
|
||||
foreach (var pair in parameters.Zip(captures, (p, c) => new { Capture = c, Parameter = p }))
|
||||
{
|
||||
if (pair.Capture == null)
|
||||
{
|
||||
if (pair.Parameter.IsRequired)
|
||||
{
|
||||
string message = String.Format(Resources.WrongNumberOfArguments, definition.Name);
|
||||
throw new FormatException(message);
|
||||
}
|
||||
collection.AddArgument(pair.Parameter, null);
|
||||
}
|
||||
else
|
||||
{
|
||||
collection.AddArgument(pair.Parameter, pair.Capture.Value);
|
||||
}
|
||||
}
|
||||
return collection;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,150 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace mustache
|
||||
{
|
||||
/// <summary>
|
||||
/// Parses a format string and returns the text generator.
|
||||
/// </summary>
|
||||
internal sealed class FormatParser
|
||||
{
|
||||
private const string key = @"[_\w][_\w\d]*";
|
||||
private const string compoundKey = key + @"(\." + key + ")*";
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of a FormatParser.
|
||||
/// </summary>
|
||||
public FormatParser()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a text generator based on the given format.
|
||||
/// </summary>
|
||||
/// <param name="format">The format to parse.</param>
|
||||
/// <returns>The text generator.</returns>
|
||||
public IGenerator Build(string format)
|
||||
{
|
||||
TagDefinition definition = new TagDefinition("builtins");
|
||||
definition.HasBody = true;
|
||||
CompoundGenerator generator = new CompoundGenerator();
|
||||
TagScope tagScope = new TagScope();
|
||||
registerTags(definition, tagScope);
|
||||
Match match = findNextTag(definition, format, 0);
|
||||
buildCompoundGenerator(definition, tagScope, generator, format, 0, match);
|
||||
return generator;
|
||||
}
|
||||
|
||||
private static void registerTags(TagDefinition definition, TagScope scope)
|
||||
{
|
||||
foreach (TagDefinition childTag in definition.ChildTags)
|
||||
{
|
||||
scope.AddTag(childTag);
|
||||
}
|
||||
}
|
||||
|
||||
private static Match findNextTag(TagDefinition definition, string format, int formatIndex)
|
||||
{
|
||||
List<string> matches = new List<string>();
|
||||
matches.Add(getKeyRegex());
|
||||
matches.Add(getClosingTagRegex(definition));
|
||||
matches.Add(getCommentTagRegex());
|
||||
foreach (TagDefinition childTag in definition.ChildTags)
|
||||
{
|
||||
matches.Add(getTagRegex(childTag));
|
||||
}
|
||||
string match = "{{(" + String.Join("|", matches) + ")}}";
|
||||
Regex regex = new Regex(match);
|
||||
return regex.Match(format, formatIndex);
|
||||
}
|
||||
|
||||
private static string getClosingTagRegex(TagDefinition definition)
|
||||
{
|
||||
StringBuilder regexBuilder = new StringBuilder();
|
||||
regexBuilder.Append(@"(?<close>(/");
|
||||
regexBuilder.Append(definition.Name);
|
||||
regexBuilder.Append(@"\s*?))");
|
||||
return regexBuilder.ToString();
|
||||
}
|
||||
|
||||
private static string getCommentTagRegex()
|
||||
{
|
||||
return @"(?<comment>#!.*?)";
|
||||
}
|
||||
|
||||
private static string getKeyRegex()
|
||||
{
|
||||
return @"((?<key>" + compoundKey + @")(,(?<alignment>(-)?[\d]+))?(:(?<format>.*?))?)";
|
||||
}
|
||||
|
||||
private static string getTagRegex(TagDefinition definition)
|
||||
{
|
||||
StringBuilder regexBuilder = new StringBuilder();
|
||||
regexBuilder.Append(@"(?<open>(#(?<name>");
|
||||
regexBuilder.Append(definition.Name);
|
||||
regexBuilder.Append(@")");
|
||||
foreach (TagParameter parameter in definition.Parameters)
|
||||
{
|
||||
regexBuilder.Append(@"\s+?");
|
||||
regexBuilder.Append(@"(?<argument>");
|
||||
regexBuilder.Append(compoundKey);
|
||||
regexBuilder.Append(@")");
|
||||
}
|
||||
regexBuilder.Append(@"\s*?))");
|
||||
return regexBuilder.ToString();
|
||||
}
|
||||
|
||||
private static int buildCompoundGenerator(TagDefinition tagDefinition, TagScope scope, CompoundGenerator generator, string format, int formatIndex, Match match)
|
||||
{
|
||||
bool done = false;
|
||||
while (!done)
|
||||
{
|
||||
string leading = format.Substring(formatIndex, match.Index - formatIndex);
|
||||
formatIndex = match.Index + match.Length;
|
||||
|
||||
if (match.Groups["comment"].Success)
|
||||
{
|
||||
// TODO - process comment
|
||||
}
|
||||
else if (match.Groups["close"].Success)
|
||||
{
|
||||
// TODO - process closing tag
|
||||
done = true;
|
||||
}
|
||||
else if (match.Groups["open"].Success)
|
||||
{
|
||||
string tagName = match.Groups["name"].Value;
|
||||
TagDefinition nextDefinition = scope.Find(tagName);
|
||||
if (nextDefinition == null)
|
||||
{
|
||||
// TODO - handle missing tag definition
|
||||
}
|
||||
if (nextDefinition.HasBody)
|
||||
{
|
||||
CompoundGenerator nextGenerator = new CompoundGenerator();
|
||||
TagScope nextScope = new TagScope(scope);
|
||||
registerTags(nextDefinition, nextScope);
|
||||
Match nextMatch = findNextTag(nextDefinition, format, formatIndex);
|
||||
formatIndex = buildCompoundGenerator(nextDefinition, nextScope, nextGenerator, format, formatIndex, nextMatch);
|
||||
// TODO - grab the generated text and parameters and pass it to the tag's processor
|
||||
// TODO - a parameter can be a key or a default value
|
||||
}
|
||||
else
|
||||
{
|
||||
// TODO - grab all of the parameters and pass them to the tag's generator
|
||||
// TODO - a parameter can be a key or a default value
|
||||
}
|
||||
}
|
||||
else if (match.Groups["key"].Success)
|
||||
{
|
||||
string alignment = match.Groups["alignment"].Value;
|
||||
string formatting = match.Groups["format"].Value;
|
||||
// TODO - create a key generator
|
||||
}
|
||||
}
|
||||
return formatIndex;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
using System;
|
||||
using System.Globalization;
|
||||
|
||||
namespace mustache
|
||||
{
|
||||
/// <summary>
|
||||
/// Generates text by substituting an object's values for placeholders.
|
||||
/// </summary>
|
||||
public sealed class Generator
|
||||
{
|
||||
private readonly IGenerator _generator;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of a Generator.
|
||||
/// </summary>
|
||||
/// <param name="generator">The text generator to wrap.</param>
|
||||
internal Generator(IGenerator generator)
|
||||
{
|
||||
_generator = generator;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the text that is generated for the given object.
|
||||
/// </summary>
|
||||
/// <param name="source">The object to generate the text with.</param>
|
||||
/// <returns>The text generated for the given object.</returns>
|
||||
public string Render(object source)
|
||||
{
|
||||
return render(CultureInfo.CurrentCulture, source);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the text that is generated for the given object.
|
||||
/// </summary>
|
||||
/// <param name="provider">The format provider to use.</param>
|
||||
/// <param name="source">The object to generate the text with.</param>
|
||||
/// <returns>The text generated for the given object.</returns>
|
||||
public string Render(IFormatProvider provider, object source)
|
||||
{
|
||||
if (provider == null)
|
||||
{
|
||||
provider = CultureInfo.CurrentCulture;
|
||||
}
|
||||
return render(provider, source);
|
||||
}
|
||||
|
||||
private string render(IFormatProvider provider, object source)
|
||||
{
|
||||
KeyScope scope = new KeyScope(source);
|
||||
return _generator.GetText(provider, scope);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -8,10 +8,11 @@ namespace mustache
|
|||
internal interface IGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// Generates the text when the values of the given object are applied to the format plan.
|
||||
/// Generates the text when applying the format plan.
|
||||
/// </summary>
|
||||
/// <param name="source">The object whose values should be used to generate the text.</param>
|
||||
/// <param name="provider">The format provider to use when formatting the keys.</param>
|
||||
/// <param name="scope">The current lexical scope of the keys.</param>
|
||||
/// <returns>The generated text.</returns>
|
||||
string GetText(object source);
|
||||
string GetText(IFormatProvider provider, KeyScope scope);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
using System;
|
||||
|
||||
namespace mustache
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines a tag that renders its content depending on the truthyness
|
||||
/// of its argument, with optional elif and else nested tags.
|
||||
/// </summary>
|
||||
internal sealed class IfTagDefinition : ConditionTagDefinition
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of a IfTagDefinition.
|
||||
/// </summary>
|
||||
public IfTagDefinition()
|
||||
: base("if")
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace mustache
|
||||
{
|
||||
/// <summary>
|
||||
/// Generates the text for a tag that only exists on a single line.
|
||||
/// </summary>
|
||||
internal sealed class InlineGenerator : IGenerator
|
||||
{
|
||||
private readonly TagDefinition _definition;
|
||||
private readonly ArgumentCollection _arguments;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of an InlineGenerator.
|
||||
/// </summary>
|
||||
/// <param name="definition">The tag to render the text for.</param>
|
||||
/// <param name="arguments">The arguments passed to the tag.</param>
|
||||
public InlineGenerator(TagDefinition definition, ArgumentCollection arguments)
|
||||
{
|
||||
_definition = definition;
|
||||
_arguments = arguments;
|
||||
}
|
||||
|
||||
string IGenerator.GetText(IFormatProvider provider, KeyScope scope)
|
||||
{
|
||||
Dictionary<string, object> arguments = _arguments.GetArguments(scope);
|
||||
return _definition.Decorate(provider, String.Empty, arguments);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
using System;
|
||||
using System.Text;
|
||||
|
||||
namespace mustache
|
||||
{
|
||||
/// <summary>
|
||||
/// Substitutes a key placeholder with the textual representation of the associated object.
|
||||
/// </summary>
|
||||
internal sealed class KeyGenerator : IGenerator
|
||||
{
|
||||
private readonly string _key;
|
||||
private readonly string _format;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of a KeyGenerator.
|
||||
/// </summary>
|
||||
/// <param name="key">The key to substitute with its value.</param>
|
||||
/// <param name="alignment">The alignment specifier.</param>
|
||||
/// <param name="formatting">The format specifier.</param>
|
||||
public KeyGenerator(string key, string alignment, string formatting)
|
||||
{
|
||||
_key = key;
|
||||
_format = getFormat(alignment, formatting);
|
||||
}
|
||||
|
||||
private static string getFormat(string alignment, string formatting)
|
||||
{
|
||||
StringBuilder formatBuilder = new StringBuilder();
|
||||
formatBuilder.Append("{0");
|
||||
if (!String.IsNullOrWhiteSpace(alignment))
|
||||
{
|
||||
formatBuilder.Append(",");
|
||||
formatBuilder.Append(alignment);
|
||||
}
|
||||
if (!String.IsNullOrWhiteSpace(formatting))
|
||||
{
|
||||
formatBuilder.Append(":");
|
||||
formatBuilder.Append(formatting);
|
||||
}
|
||||
formatBuilder.Append("}");
|
||||
return formatBuilder.ToString();
|
||||
}
|
||||
|
||||
string IGenerator.GetText(IFormatProvider provider, KeyScope scope)
|
||||
{
|
||||
object value = scope.Find(_key);
|
||||
return String.Format(provider, _format, value);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -38,7 +38,7 @@ namespace mustache
|
|||
/// </summary>
|
||||
/// <param name="source">The object to search for keys in.</param>
|
||||
/// <returns>The new child scope.</returns>
|
||||
internal KeyScope CreateChildScope(object source)
|
||||
public KeyScope CreateChildScope(object source)
|
||||
{
|
||||
KeyScope scope = new KeyScope(source, this);
|
||||
return scope;
|
||||
|
@ -50,7 +50,7 @@ namespace mustache
|
|||
/// <param name="name">The name of the key.</param>
|
||||
/// <returns>The value associated with the key with the given name.</returns>
|
||||
/// <exception cref="System.Collections.Generic.KeyNotFoundException">A key with the given name could not be found.</exception>
|
||||
public object Find(string name)
|
||||
internal object Find(string name)
|
||||
{
|
||||
string[] names = name.Split('.');
|
||||
string member = names[0];
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace mustache
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines a pseudo tag that wraps the entire content of a format string.
|
||||
/// </summary>
|
||||
internal sealed class MasterTagDefinition : TagDefinition
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of a MasterTagDefinition.
|
||||
/// </summary>
|
||||
public MasterTagDefinition()
|
||||
: base(String.Empty, true)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the parameters that can be passed to the tag.
|
||||
/// </summary>
|
||||
/// <returns>The parameters.</returns>
|
||||
protected override TagParameter[] GetParameters()
|
||||
{
|
||||
return new TagParameter[0];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether the tag has content.
|
||||
/// </summary>
|
||||
public override bool HasBody
|
||||
{
|
||||
get { return true; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the tags that indicate the end of the tags context.
|
||||
/// </summary>
|
||||
public override IEnumerable<TagDefinition> ClosingTags
|
||||
{
|
||||
get { return new TagDefinition[0]; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the tags that come into scope within the context of the tag.
|
||||
/// </summary>
|
||||
/// <returns>The tags.</returns>
|
||||
protected override TagDefinition[] GetChildTags()
|
||||
{
|
||||
return new TagDefinition[]
|
||||
{
|
||||
new IfTagDefinition(),
|
||||
new EachTagDefinition(),
|
||||
new WithTagDefinition(),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
|
@ -87,6 +87,15 @@ namespace mustache.Properties {
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to The {0} tag has already been registered..
|
||||
/// </summary>
|
||||
internal static string DuplicateTagDefinition {
|
||||
get {
|
||||
return ResourceManager.GetString("DuplicateTagDefinition", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to The key {0} could not be found..
|
||||
/// </summary>
|
||||
|
@ -95,5 +104,32 @@ namespace mustache.Properties {
|
|||
return ResourceManager.GetString("KeyNotFound", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Expected a matching {0} tag but none was found..
|
||||
/// </summary>
|
||||
internal static string MissingClosingTag {
|
||||
get {
|
||||
return ResourceManager.GetString("MissingClosingTag", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Encountered an unknown tag {0}..
|
||||
/// </summary>
|
||||
internal static string UnknownTag {
|
||||
get {
|
||||
return ResourceManager.GetString("UnknownTag", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to The wrong number of arguments were passed to an {0} tag..
|
||||
/// </summary>
|
||||
internal static string WrongNumberOfArguments {
|
||||
get {
|
||||
return ResourceManager.GetString("WrongNumberOfArguments", resourceCulture);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -126,7 +126,19 @@
|
|||
<data name="DuplicateParameter" xml:space="preserve">
|
||||
<value>A parameter with the same name already exists within the tag.</value>
|
||||
</data>
|
||||
<data name="DuplicateTagDefinition" xml:space="preserve">
|
||||
<value>The {0} tag has already been registered.</value>
|
||||
</data>
|
||||
<data name="KeyNotFound" xml:space="preserve">
|
||||
<value>The key {0} could not be found.</value>
|
||||
</data>
|
||||
<data name="MissingClosingTag" xml:space="preserve">
|
||||
<value>Expected a matching {0} tag but none was found.</value>
|
||||
</data>
|
||||
<data name="UnknownTag" xml:space="preserve">
|
||||
<value>Encountered an unknown tag {0}.</value>
|
||||
</data>
|
||||
<data name="WrongNumberOfArguments" xml:space="preserve">
|
||||
<value>The wrong number of arguments were passed to an {0} tag.</value>
|
||||
</data>
|
||||
</root>
|
|
@ -1,17 +1,25 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace mustache
|
||||
{
|
||||
/// <summary>
|
||||
/// Generates a static block of text.
|
||||
/// </summary>
|
||||
internal sealed class StaticGenerator : IGenerator
|
||||
{
|
||||
private readonly string _value;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of a StaticGenerator.
|
||||
/// </summary>
|
||||
/// <param name="value">The string to return.</param>
|
||||
public StaticGenerator(string value)
|
||||
{
|
||||
_value = value;
|
||||
}
|
||||
|
||||
string IGenerator.GetText(object source)
|
||||
string IGenerator.GetText(IFormatProvider provider, KeyScope scope)
|
||||
{
|
||||
return _value;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using mustache.Properties;
|
||||
|
||||
namespace mustache
|
||||
|
@ -9,27 +8,32 @@ namespace mustache
|
|||
/// <summary>
|
||||
/// Defines the attributes of a custom tag.
|
||||
/// </summary>
|
||||
public sealed class TagDefinition
|
||||
public abstract class TagDefinition
|
||||
{
|
||||
private readonly string _tagName;
|
||||
private readonly List<TagParameter> _parameters;
|
||||
private readonly List<TagDefinition> _childTagDefinitions;
|
||||
private TagParameter _scopeParameter;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of a TagDefinition.
|
||||
/// </summary>
|
||||
/// <param name="tagName">The name of the tag.</param>
|
||||
/// <exception cref="System.ArgumentException">The name of the tag is null or blank.</exception>
|
||||
public TagDefinition(string tagName)
|
||||
protected TagDefinition(string tagName)
|
||||
: this(tagName, false)
|
||||
{
|
||||
if (!RegexHelper.IsValidIdentifier(tagName))
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of a TagDefinition.
|
||||
/// </summary>
|
||||
/// <param name="tagName">The name of the tag.</param>
|
||||
/// <param name="isBuiltIn">Specifies whether the tag is built-in or not. Checks are not performed on the names of built-in tags.</param>
|
||||
internal TagDefinition(string tagName, bool isBuiltIn)
|
||||
{
|
||||
if (!isBuiltIn && !RegexHelper.IsValidIdentifier(tagName))
|
||||
{
|
||||
throw new ArgumentException(Resources.BlankTagName, "tagName");
|
||||
}
|
||||
_tagName = tagName;
|
||||
_parameters = new List<TagParameter>();
|
||||
_childTagDefinitions = new List<TagDefinition>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -40,66 +44,45 @@ namespace mustache
|
|||
get { return _tagName; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Specifies that the tag expects the given parameter information.
|
||||
/// </summary>
|
||||
/// <param name="parameter">The parameter to add.</param>
|
||||
/// <exception cref="System.ArgumentNullException">The parameter is null.</exception>
|
||||
/// <exception cref="System.ArgumentException">A parameter with the same name already exists.</exception>
|
||||
public void AddParameter(TagParameter parameter)
|
||||
{
|
||||
if (parameter == null)
|
||||
{
|
||||
throw new ArgumentNullException("parameter");
|
||||
}
|
||||
if (_parameters.Any(p => p.Name == parameter.Name))
|
||||
{
|
||||
throw new ArgumentException(Resources.DuplicateParameter, "parameter");
|
||||
}
|
||||
_parameters.Add(parameter);
|
||||
if (parameter.IsScopeContext)
|
||||
{
|
||||
_scopeParameter = parameter;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the parameters that are defined for the tag.
|
||||
/// </summary>
|
||||
public IEnumerable<TagParameter> Parameters
|
||||
{
|
||||
get { return new ReadOnlyCollection<TagParameter>(_parameters); }
|
||||
get { return new ReadOnlyCollection<TagParameter>(GetParameters()); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether the tag contains content.
|
||||
/// Specifies which parameters are passed to the tag.
|
||||
/// </summary>
|
||||
public bool HasBody
|
||||
/// <returns>The tag parameters.</returns>
|
||||
protected abstract TagParameter[] GetParameters();
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether the tag contains content.
|
||||
/// </summary>
|
||||
public abstract bool HasBody
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether the tag defines a new scope based on an argument.
|
||||
/// Gets the tags that can indicate that the tag has closed.
|
||||
/// This field is only used if no closing tag is expected.
|
||||
/// </summary>
|
||||
public bool IsScoped
|
||||
public virtual IEnumerable<TagDefinition> ClosingTags
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Specifies that the given tag is in scope within the current tag.
|
||||
/// </summary>
|
||||
/// <param name="childTag">The tag that is in scope within the current tag.</param>
|
||||
public void AddChildTag(TagDefinition childTag)
|
||||
{
|
||||
if (childTag == null)
|
||||
get
|
||||
{
|
||||
throw new ArgumentNullException("childTag");
|
||||
if (HasBody)
|
||||
{
|
||||
return new TagDefinition[] { this };
|
||||
}
|
||||
else
|
||||
{
|
||||
return new TagDefinition[0];
|
||||
}
|
||||
}
|
||||
_childTagDefinitions.Add(childTag);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -107,7 +90,56 @@ namespace mustache
|
|||
/// </summary>
|
||||
public IEnumerable<TagDefinition> ChildTags
|
||||
{
|
||||
get { return new ReadOnlyCollection<TagDefinition>(_childTagDefinitions); }
|
||||
get { return new ReadOnlyCollection<TagDefinition>(GetChildTags()); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Specifies which tags are scoped under the current tag.
|
||||
/// </summary>
|
||||
/// <returns>The child tag definitions.</returns>
|
||||
protected abstract TagDefinition[] GetChildTags();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the scope to use when building the inner text of the tag.
|
||||
/// </summary>
|
||||
/// <param name="scope">The current scope.</param>
|
||||
/// <param name="arguments">The arguments passed to the tag.</param>
|
||||
/// <returns>The scope to use when building the inner text of the tag.</returns>
|
||||
public virtual IEnumerable<KeyScope> GetChildScopes(KeyScope scope, Dictionary<string, object> arguments)
|
||||
{
|
||||
yield return scope;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies additional formatting to the inner text of the tag.
|
||||
/// </summary>
|
||||
/// <param name="provider">The format provider to use.</param>
|
||||
/// <param name="innerText">The inner text of the tag.</param>
|
||||
/// <param name="arguments">The arguments passed to the tag.</param>
|
||||
/// <returns>The decorated inner text.</returns>
|
||||
public virtual string Decorate(IFormatProvider provider, string innerText, Dictionary<string, object> arguments)
|
||||
{
|
||||
return innerText;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Requests which generator group to associate the given tag type.
|
||||
/// </summary>
|
||||
/// <param name="definition">The child tag definition being grouped.</param>
|
||||
/// <returns>The name of the group to associate the given tag with.</returns>
|
||||
public virtual bool ShouldCreateSecondaryGroup(TagDefinition definition)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether the group with the given name should have text generated for them.
|
||||
/// </summary>
|
||||
/// <param name="arguments">The arguments passed to the tag.</param>
|
||||
/// <returns>True if text should be generated for the group; otherwise, false.</returns>
|
||||
public virtual bool ShouldGeneratePrimaryGroup(Dictionary<string, object> arguments)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,15 +32,6 @@ namespace mustache
|
|||
get { return _name; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether the parameter should be used to define the parameter.
|
||||
/// </summary>
|
||||
public bool IsScopeContext
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether the field is required.
|
||||
/// </summary>
|
||||
|
|
|
@ -1,29 +1,55 @@
|
|||
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>();
|
||||
}
|
||||
|
||||
public void AddTag(TagDefinition 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)
|
||||
{
|
||||
_tagLookup.Add(tagDefinition.Name, tagDefinition);
|
||||
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>
|
||||
/// 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;
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace mustache
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines a tag that changes the scope to the object passed as an argument.
|
||||
/// </summary>
|
||||
internal sealed class WithTagDefinition : TagDefinition
|
||||
{
|
||||
private const string contextParameter = "context";
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of a WithTagDefinition.
|
||||
/// </summary>
|
||||
public WithTagDefinition()
|
||||
: base("with", true)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the parameters that can be passed to the tag.
|
||||
/// </summary>
|
||||
/// <returns>The parameters.</returns>
|
||||
protected override TagParameter[] GetParameters()
|
||||
{
|
||||
return new TagParameter[] { new TagParameter(contextParameter) { IsRequired = true } };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether the tag has content.
|
||||
/// </summary>
|
||||
public override bool HasBody
|
||||
{
|
||||
get { return true; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the tags that come into scope within the tag.
|
||||
/// </summary>
|
||||
/// <returns>The child tag.</returns>
|
||||
protected override TagDefinition[] GetChildTags()
|
||||
{
|
||||
return new TagDefinition[0];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the scopes to use for generating the tag's content.
|
||||
/// </summary>
|
||||
/// <param name="scope">The current scope.</param>
|
||||
/// <param name="arguments">The arguments that were passed to the tag.</param>
|
||||
/// <returns>The scopes to use for generating the tag's content.</returns>
|
||||
public override IEnumerable<KeyScope> GetChildScopes(KeyScope scope, Dictionary<string, object> arguments)
|
||||
{
|
||||
object context = arguments[contextParameter];
|
||||
yield return scope.CreateChildScope(context);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -34,9 +34,19 @@
|
|||
<Reference Include="System" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="ArgumentCollection.cs" />
|
||||
<Compile Include="CompoundGenerator.cs" />
|
||||
<Compile Include="FormatParser.cs" />
|
||||
<Compile Include="ConditionTagDefinition.cs" />
|
||||
<Compile Include="EachTagDefinition.cs" />
|
||||
<Compile Include="ElifTagDefinition.cs" />
|
||||
<Compile Include="ElseTagDefinition.cs" />
|
||||
<Compile Include="FormatCompiler.cs" />
|
||||
<Compile Include="Generator.cs" />
|
||||
<Compile Include="IfTagDefinition.cs" />
|
||||
<Compile Include="IGenerator.cs" />
|
||||
<Compile Include="InlineGenerator.cs" />
|
||||
<Compile Include="KeyGenerator.cs" />
|
||||
<Compile Include="MasterTagDefinition.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="Properties\Resources.Designer.cs">
|
||||
<AutoGen>True</AutoGen>
|
||||
|
@ -50,6 +60,7 @@
|
|||
<Compile Include="TagParameter.cs" />
|
||||
<Compile Include="KeyScope.cs" />
|
||||
<Compile Include="TagScope.cs" />
|
||||
<Compile Include="WithGenerator.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Properties\Resources.resx">
|
||||
|
|
Loading…
Reference in New Issue