Made progress implementing the object model, including all of the built-in tags.

This commit is contained in:
Travis Parks 2013-01-09 21:17:45 -05:00
parent f8628aaf86
commit 0b84ca8877
25 changed files with 1273 additions and 230 deletions

View File

@ -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.");
}
}
}

View File

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

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}
}

View File

@ -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);
}
}
}
}

View File

@ -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() }; }
}
}
}

View File

@ -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];
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}

View File

@ -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")
{
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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];

View File

@ -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(),
};
}
}
}

View File

@ -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);
}
}
}
}

View File

@ -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>

View File

@ -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;
}

View File

@ -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;
get
{
if (HasBody)
{
return new TagDefinition[] { this };
}
/// <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)
else
{
if (childTag == null)
{
throw new ArgumentNullException("childTag");
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;
}
}
}

View File

@ -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>

View File

@ -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;

View File

@ -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);
}
}
}

View File

@ -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">