Begin implementation of context scopes.

Instead of context data being a single object, it now supports the same
scoping rules as keys.
Now the #index tag is always available.
This commit is contained in:
Travis Parks 2013-08-16 23:35:46 -04:00
parent eb7e909f95
commit f136dd61a5
19 changed files with 279 additions and 111 deletions

View File

@ -1093,7 +1093,7 @@ Item Number: foo<br />
return new TagParameter[] { new TagParameter("param") { IsRequired = false, DefaultValue = 123 } };
}
public override void GetText(TextWriter writer, Dictionary<string, object> arguments, object contextData)
public override void GetText(TextWriter writer, Dictionary<string, object> arguments, Scope contextScope)
{
writer.Write(arguments["param"]);
}
@ -1168,5 +1168,23 @@ Your order total was: $7.50";
}
#endregion
#region Context Variables
/// <summary>
/// We will use the index variable to determine whether or not to print out a line.
/// </summary>
[TestMethod]
public void TestCompile_CanUseContextVariablesToMakeDecisions()
{
FormatCompiler compiler = new FormatCompiler();
const string format = @"{{#each this}}{{#if @index}}{{#index}}{{/if}}{{/each}}";
Generator generator = compiler.Compile(format);
string actual = generator.Render(new int[] { 1, 1, 1, 1, });
string expected = "123";
Assert.AreEqual(expected, actual, "The numbers were not valid.");
}
#endregion
}
}

View File

@ -50,9 +50,10 @@ namespace Mustache
/// <summary>
/// Substitutes the key placeholders with their respective values.
/// </summary>
/// <param name="scope">The current lexical scope.</param>
/// <param name="keyScope">The key/value pairs in the current lexical scope.</param>
/// <param name="contextScope">The key/value pairs in current context.</param>
/// <returns>A dictionary associating the parameter name to the associated value.</returns>
public Dictionary<string, object> GetArguments(KeyScope scope)
public Dictionary<string, object> GetArguments(Scope keyScope, Scope contextScope)
{
Dictionary<string, object> arguments = new Dictionary<string,object>();
foreach (KeyValuePair<TagParameter, string> pair in _argumentLookup)
@ -62,13 +63,27 @@ namespace Mustache
{
value = pair.Key.DefaultValue;
}
else if (pair.Value.StartsWith("@"))
{
value = contextScope.Find(pair.Value.Substring(1));
}
else
{
value = scope.Find(pair.Value);
value = keyScope.Find(pair.Value);
}
arguments.Add(pair.Key.Name, value);
}
return arguments;
}
public Dictionary<string, object> GetArguments()
{
Dictionary<string, object> arguments = new Dictionary<string, object>();
foreach (KeyValuePair<TagParameter, string> pair in _argumentLookup)
{
arguments.Add(pair.Key.Name, pair.Value);
}
return arguments;
}
}
}

View File

@ -59,10 +59,10 @@ namespace Mustache
}
}
void IGenerator.GetText(KeyScope scope, TextWriter writer, object contextData)
void IGenerator.GetText(Scope keyScope, TextWriter writer, Scope contextScope)
{
Dictionary<string, object> arguments = _arguments.GetArguments(scope);
IEnumerable<NestedContext> contexts = _definition.GetChildContext(writer, scope, arguments);
Dictionary<string, object> arguments = _arguments.GetArguments(keyScope, contextScope);
IEnumerable<NestedContext> contexts = _definition.GetChildContext(writer, keyScope, arguments, contextScope);
List<IGenerator> generators;
if (_definition.ShouldGeneratePrimaryGroup(arguments))
{
@ -80,7 +80,7 @@ namespace Mustache
{
foreach (IGenerator generator in generators)
{
generator.GetText(context.KeyScope ?? scope, context.Writer ?? writer, context.Data);
generator.GetText(context.KeyScope ?? keyScope, context.Writer ?? writer, context.ContextScope);
if (context.WriterNeedsConsidated)
{
writer.Write(_definition.ConsolidateWriter(context.Writer ?? writer, arguments));

View File

@ -43,10 +43,14 @@ namespace Mustache
/// Gets the context to use when building the inner text of the tag.
/// </summary>
/// <param name="writer">The text writer passed</param>
/// <param name="scope">The current scope.</param>
/// <param name="keyScope">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 override IEnumerable<NestedContext> GetChildContext(TextWriter writer, KeyScope scope, Dictionary<string, object> arguments)
public override IEnumerable<NestedContext> GetChildContext(
TextWriter writer,
Scope keyScope,
Dictionary<string, object> arguments,
Scope contextScope)
{
object value = arguments[collectionParameter];
IEnumerable enumerable = value as IEnumerable;
@ -57,7 +61,14 @@ namespace Mustache
int index = 0;
foreach (object item in enumerable)
{
yield return new NestedContext() { KeyScope = scope.CreateChildScope(item), Writer = writer, Data = index };
NestedContext childContext = new NestedContext()
{
KeyScope = keyScope.CreateChildScope(item),
Writer = writer,
ContextScope = contextScope.CreateChildScope(),
};
childContext.ContextScope.Set("index", index);
yield return childContext;
++index;
}
}

View File

@ -39,6 +39,8 @@ namespace Mustache
_tagLookup.Add(withDefinition.Name, withDefinition);
NewlineTagDefinition newlineDefinition = new NewlineTagDefinition();
_tagLookup.Add(newlineDefinition.Name, newlineDefinition);
SetTagDefinition setDefinition = new SetTagDefinition();
_tagLookup.Add(setDefinition.Name, setDefinition);
}
/// <summary>
@ -150,9 +152,11 @@ namespace Mustache
foreach (TagParameter parameter in definition.Parameters)
{
regexBuilder.Append(@"(\s+?");
regexBuilder.Append(@"(?<argument>");
regexBuilder.Append(@"(?<argument>(");
regexBuilder.Append(RegexHelper.CompoundKey);
regexBuilder.Append(@"))");
regexBuilder.Append("|@");
regexBuilder.Append(RegexHelper.Key);
regexBuilder.Append(@")))");
if (!parameter.IsRequired)
{
regexBuilder.Append("?");

View File

@ -70,7 +70,7 @@ namespace Mustache
private string render(IFormatProvider provider, object source)
{
KeyScope scope = new KeyScope(source);
Scope scope = new Scope(source);
foreach (EventHandler<KeyFoundEventArgs> handler in _foundHandlers)
{
scope.KeyFound += handler;
@ -80,7 +80,8 @@ namespace Mustache
scope.KeyNotFound += handler;
}
StringWriter writer = new StringWriter(provider);
_generator.GetText(scope, writer, null);
Scope contextScope = new Scope(new Dictionary<string, object>());
_generator.GetText(scope, writer, contextScope);
return writer.ToString();
}
}

View File

@ -11,10 +11,10 @@ namespace Mustache
/// <summary>
/// Generates the text when applying the format plan.
/// </summary>
/// <param name="scope">The current lexical scope of the keys.</param>
/// <param name="keyScope">The current lexical scope of the keys.</param>
/// <param name="writer">The text writer to send all text to.</param>
/// <param name="contextData">The data associated to the context.</param>
/// <param name="contextScope">The data associated to the context.</param>
/// <returns>The generated text.</returns>
void GetText(KeyScope scope, TextWriter writer, object contextData);
void GetText(Scope keyScope, TextWriter writer, Scope contextScope);
}
}

View File

@ -13,28 +13,23 @@ namespace Mustache
/// Initializes a new instance of an IndexTagDefinition.
/// </summary>
public IndexTagDefinition()
: base("index")
: base("index", true)
{
}
/// <summary>
/// Gets whether the tag only exists within the scope of its parent.
/// </summary>
protected override bool GetIsContextSensitive()
{
return true;
}
/// <summary>
/// Gets the text to output.
/// </summary>
/// <param name="writer">The writer to write the output to.</param>
/// <param name="arguments">The arguments passed to the tag.</param>
/// <param name="contextData">Extra data passed along with the context.</param>
public override void GetText(TextWriter writer, Dictionary<string, object> arguments, object contextData)
/// <param name="contextScope">Extra data passed along with the context.</param>
public override void GetText(TextWriter writer, Dictionary<string, object> arguments, Scope contextScope)
{
int index = (int)contextData;
writer.Write(index);
object index;
if (contextScope.TryFind("index", out index))
{
writer.Write(index);
}
}
}
}

View File

@ -5,7 +5,7 @@ using System.IO;
namespace Mustache
{
/// <summary>
/// Generates the text for a tag that only exists on a single line.
/// Generates the text for a tag that is replaced with its generated text.
/// </summary>
internal sealed class InlineGenerator : IGenerator
{
@ -23,10 +23,18 @@ namespace Mustache
_arguments = arguments;
}
void IGenerator.GetText(KeyScope scope, TextWriter writer, object contextData)
void IGenerator.GetText(Scope scope, TextWriter writer, Scope context)
{
Dictionary<string, object> arguments = _arguments.GetArguments(scope);
_definition.GetText(writer, arguments, contextData);
Dictionary<string, object> arguments;
if (_definition.IsSetter)
{
arguments = _arguments.GetArguments();
}
else
{
arguments = _arguments.GetArguments(scope, context);
}
_definition.GetText(writer, arguments, context);
}
}
}

View File

@ -42,7 +42,7 @@ namespace Mustache
return formatBuilder.ToString();
}
void IGenerator.GetText(KeyScope scope, TextWriter writer, object contextData)
void IGenerator.GetText(Scope scope, TextWriter writer, Scope context)
{
object value = scope.Find(_key);
writer.Write(_format, value);

View File

@ -19,36 +19,24 @@ namespace Mustache
/// Gets or sets the writer to use when generating the child context.
/// </summary>
/// <remarks>Setting the writer to null will indicate that the tag's writer should be used.</remarks>
public TextWriter Writer
{
get;
set;
}
public TextWriter Writer { get; set; }
/// <summary>
/// Gets or sets whether the text sent to the returned writer needs to be added
/// to the parent tag's writer. This should be false if the parent writer is
/// being returned or is being wrapped.
/// </summary>
public bool WriterNeedsConsidated
{
get;
set;
}
public bool WriterNeedsConsidated { get; set; }
/// <summary>
/// Gets or sets the scope to use when generating the child context.
/// Gets or sets the key scope to use when generating the child context.
/// </summary>
/// <remarks>Setting the scope to null will indicate that the current scope should be used.</remarks>
public KeyScope KeyScope
{
get;
set;
}
public Scope KeyScope { get; set; }
/// <summary>
/// Gets or sets data associated with the context.
/// </summary>
public object Data { get; set; }
public Scope ContextScope { get; set; }
}
}

View File

@ -22,8 +22,8 @@ namespace Mustache
/// </summary>
/// <param name="writer">The writer to write the output to.</param>
/// <param name="arguments">The arguments passed to the tag.</param>
/// <param name="contextData">Extra data passed along with the context.</param>
public override void GetText(TextWriter writer, Dictionary<string, object> arguments, object contextData)
/// <param name="context">Extra data passed along with the context.</param>
public override void GetText(TextWriter writer, Dictionary<string, object> arguments, Scope context)
{
writer.Write(Environment.NewLine);
}

View File

@ -8,7 +8,7 @@ namespace Mustache
/// </summary>
public static class RegexHelper
{
private const string Key = @"[_\w][_\w\d]*";
internal const string Key = @"[_\w][_\w\d]*";
internal const string CompoundKey = Key + @"(\." + Key + ")*";
/// <summary>

View File

@ -9,16 +9,16 @@ namespace Mustache
/// <summary>
/// Represents a scope of keys.
/// </summary>
public sealed class KeyScope
public sealed class Scope
{
private readonly object _source;
private readonly KeyScope _parent;
private readonly Scope _parent;
/// <summary>
/// Initializes a new instance of a KeyScope.
/// </summary>
/// <param name="source">The object to search for keys in.</param>
internal KeyScope(object source)
internal Scope(object source)
: this(source, null)
{
}
@ -28,7 +28,7 @@ namespace Mustache
/// </summary>
/// <param name="source">The object to search for keys in.</param>
/// <param name="parent">The parent scope to search in if the value is not found.</param>
internal KeyScope(object source, KeyScope parent)
internal Scope(object source, Scope parent)
{
_parent = parent;
_source = source;
@ -44,14 +44,23 @@ namespace Mustache
/// </summary>
public event EventHandler<KeyNotFoundEventArgs> KeyNotFound;
/// <summary>
/// Creates a child scope that searches for keys in a default dictionary of key/value pairs.
/// </summary>
/// <returns>The new child scope.</returns>
public Scope CreateChildScope()
{
return CreateChildScope(new Dictionary<string, object>());
}
/// <summary>
/// Creates a child scope that searches for keys in the given object.
/// </summary>
/// <param name="source">The object to search for keys in.</param>
/// <returns>The new child scope.</returns>
public KeyScope CreateChildScope(object source)
public Scope CreateChildScope(object source)
{
KeyScope scope = new KeyScope(source, this);
Scope scope = new Scope(source, this);
scope.KeyFound = KeyFound;
scope.KeyNotFound = KeyNotFound;
return scope;
@ -65,58 +74,45 @@ namespace Mustache
/// <exception cref="System.Collections.Generic.KeyNotFoundException">A key with the given name could not be found.</exception>
internal object Find(string name)
{
string[] names = name.Split('.');
string member = names[0];
object nextLevel = _source;
if (member != "this")
string member = null;
object value = null;
if (tryFind(name, ref member, ref value))
{
nextLevel = find(name, member);
onKeyFound(name, ref value);
return value;
}
for (int index = 1; index < names.Length; ++index)
if (onKeyNotFound(name, member, ref value))
{
IDictionary<string, object> context = toLookup(nextLevel);
member = names[index];
if (!context.TryGetValue(member, out nextLevel))
{
nextLevel = handleKeyNotFound(name, member);
}
return value;
}
string message = String.Format(CultureInfo.CurrentCulture, Resources.KeyNotFound, member);
throw new KeyNotFoundException(message);
}
private void onKeyFound(string name, ref object value)
{
if (KeyFound != null)
{
KeyFoundEventArgs args = new KeyFoundEventArgs(name, nextLevel);
KeyFoundEventArgs args = new KeyFoundEventArgs(name, value);
KeyFound(this, args);
nextLevel = args.Substitute;
value = args.Substitute;
}
return nextLevel;
}
private object find(string fullName, string memberName)
private bool onKeyNotFound(string name, string member, ref object value)
{
IDictionary<string, object> lookup = toLookup(_source);
if (lookup.ContainsKey(memberName))
if (KeyNotFound == null)
{
return lookup[memberName];
return false;
}
if (_parent == null)
KeyNotFoundEventArgs args = new KeyNotFoundEventArgs(name, member);
KeyNotFound(this, args);
if (!args.Handled)
{
return handleKeyNotFound(fullName, memberName);
return false;
}
return _parent.find(fullName, memberName);
}
private object handleKeyNotFound(string fullName, string memberName)
{
KeyNotFoundEventArgs args = new KeyNotFoundEventArgs(fullName, memberName);
if (KeyNotFound != null)
{
KeyNotFound(this, args);
}
if (args.Handled)
{
return args.Substitute;
}
string message = String.Format(CultureInfo.CurrentCulture, Resources.KeyNotFound, memberName);
throw new KeyNotFoundException(message);
value = args.Substitute;
return true;
}
private static IDictionary<string, object> toLookup(object value)
@ -128,5 +124,59 @@ namespace Mustache
}
return lookup;
}
internal void Set(string key, object value)
{
IDictionary<string, object> lookup = toLookup(_source);
lookup[key] = value;
}
public bool TryFind(string name, out object value)
{
string member = null;
value = null;
return tryFind(name, ref member, ref value);
}
private bool tryFind(string name, ref string member, ref object value)
{
string[] names = name.Split('.');
member = names[0];
value = _source;
if (member != "this")
{
if (!tryFindFirst(member, ref value))
{
return false;
}
}
for (int index = 1; index < names.Length; ++index)
{
IDictionary<string, object> context = toLookup(value);
member = names[index];
if (!context.TryGetValue(member, out value))
{
value = null;
return false;
}
}
return true;
}
private bool tryFindFirst(string member, ref object value)
{
IDictionary<string, object> lookup = toLookup(_source);
if (lookup.ContainsKey(member))
{
value = lookup[member];
return true;
}
if (_parent == null)
{
value = null;
return false;
}
return _parent.tryFindFirst(member, ref value);
}
}
}

View File

@ -0,0 +1,47 @@
using System;
using System.Collections.Generic;
using System.IO;
namespace Mustache
{
/// <summary>
/// Defines a tag that declares a named value in the current context.
/// </summary>
internal sealed class SetTagDefinition : InlineTagDefinition
{
private const string nameParameter = "name";
private static readonly TagParameter name = new TagParameter(nameParameter) { IsRequired = true };
/// <summary>
/// Initializes a new instance of an SetTagDefinition.
/// </summary>
public SetTagDefinition()
: base("set", true)
{
}
protected override bool GetIsSetter()
{
return true;
}
protected override IEnumerable<TagParameter> GetParameters()
{
return new TagParameter[] { name };
}
/// <summary>
/// Gets the text to output.
/// </summary>
/// <param name="writer">The writer to write the output to.</param>
/// <param name="arguments">The arguments passed to the tag.</param>
/// <param name="contextScope">Extra data passed along with the context.</param>
public override void GetText(TextWriter writer, Dictionary<string, object> arguments, Scope contextScope)
{
string name = (string)arguments[nameParameter];
// TODO - get the value for the variable
object value = null;
contextScope.Set(name, value);
}
}
}

View File

@ -27,7 +27,7 @@ namespace Mustache
get { return value; }
}
void IGenerator.GetText(KeyScope scope, TextWriter writer, object contextData)
void IGenerator.GetText(Scope scope, TextWriter writer, Scope context)
{
writer.Write(Value);
}

View File

@ -44,6 +44,16 @@ namespace Mustache
get { return _tagName; }
}
internal bool IsSetter
{
get { return GetIsSetter(); }
}
protected virtual bool GetIsSetter()
{
return false;
}
/// <summary>
/// Gets whether the tag is limited to the parent tag's context.
/// </summary>
@ -139,12 +149,22 @@ namespace Mustache
/// Gets the context to use when building the inner text of the tag.
/// </summary>
/// <param name="writer">The text writer passed</param>
/// <param name="scope">The current scope.</param>
/// <param name="keyScope">The current key 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<NestedContext> GetChildContext(TextWriter writer, KeyScope scope, Dictionary<string, object> arguments)
public virtual IEnumerable<NestedContext> GetChildContext(
TextWriter writer,
Scope keyScope,
Dictionary<string, object> arguments,
Scope contextScope)
{
yield return new NestedContext() { KeyScope = scope, Writer = writer };
NestedContext context = new NestedContext()
{
KeyScope = keyScope,
Writer = writer,
ContextScope = contextScope.CreateChildScope()
};
yield return context;
}
/// <summary>
@ -152,8 +172,8 @@ namespace Mustache
/// </summary>
/// <param name="writer">The text writer to write to.</param>
/// <param name="arguments">The arguments passed to the tag.</param>
/// <param name="contextData">The data associated to the context.</param>
public virtual void GetText(TextWriter writer, Dictionary<string, object> arguments, object contextData)
/// <param name="context">The data associated to the context.</param>
public virtual void GetText(TextWriter writer, Dictionary<string, object> arguments, Scope context)
{
}

View File

@ -50,13 +50,23 @@ namespace Mustache
/// Gets the context to use when building the inner text of the tag.
/// </summary>
/// <param name="writer">The text writer passed</param>
/// <param name="scope">The current scope.</param>
/// <param name="keyScope">The current key 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 override IEnumerable<NestedContext> GetChildContext(TextWriter writer, KeyScope scope, Dictionary<string, object> arguments)
public override IEnumerable<NestedContext> GetChildContext(
TextWriter writer,
Scope keyScope,
Dictionary<string, object> arguments,
Scope contextScope)
{
object context = arguments[contextParameter];
yield return new NestedContext() { KeyScope = scope.CreateChildScope(context), Writer = writer };
object contextSource = arguments[contextParameter];
NestedContext context = new NestedContext()
{
KeyScope = keyScope.CreateChildScope(contextSource),
Writer = writer,
ContextScope = contextScope.CreateChildScope()
};
yield return context;
}
}
}

View File

@ -40,6 +40,7 @@
<Compile Include="ContentTagDefinition.cs" />
<Compile Include="Context.cs" />
<Compile Include="ContextParameter.cs" />
<Compile Include="SetTagDefinition.cs" />
<Compile Include="NewlineTagDefinition.cs" />
<Compile Include="IndexTagDefinition.cs" />
<Compile Include="KeyFoundEventArgs.cs" />
@ -68,8 +69,8 @@
<Compile Include="StaticGenerator.cs" />
<Compile Include="TagDefinition.cs" />
<Compile Include="TagParameter.cs" />
<Compile Include="KeyScope.cs" />
<Compile Include="WithGenerator.cs" />
<Compile Include="Scope.cs" />
<Compile Include="WithTagDefinition.cs" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Properties\Resources.resx">