diff --git a/mustache-sharp.test/FormatCompilerTester.cs b/mustache-sharp.test/FormatCompilerTester.cs index 694bb66..3e37c24 100644 --- a/mustache-sharp.test/FormatCompilerTester.cs +++ b/mustache-sharp.test/FormatCompilerTester.cs @@ -1093,7 +1093,7 @@ Item Number: foo
return new TagParameter[] { new TagParameter("param") { IsRequired = false, DefaultValue = 123 } }; } - public override void GetText(TextWriter writer, Dictionary arguments, object contextData) + public override void GetText(TextWriter writer, Dictionary arguments, Scope contextScope) { writer.Write(arguments["param"]); } @@ -1168,5 +1168,23 @@ Your order total was: $7.50"; } #endregion + + #region Context Variables + + /// + /// We will use the index variable to determine whether or not to print out a line. + /// + [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 } } diff --git a/mustache-sharp/ArgumentCollection.cs b/mustache-sharp/ArgumentCollection.cs index 0bdafe2..93e2518 100644 --- a/mustache-sharp/ArgumentCollection.cs +++ b/mustache-sharp/ArgumentCollection.cs @@ -50,9 +50,10 @@ namespace Mustache /// /// Substitutes the key placeholders with their respective values. /// - /// The current lexical scope. + /// The key/value pairs in the current lexical scope. + /// The key/value pairs in current context. /// A dictionary associating the parameter name to the associated value. - public Dictionary GetArguments(KeyScope scope) + public Dictionary GetArguments(Scope keyScope, Scope contextScope) { Dictionary arguments = new Dictionary(); foreach (KeyValuePair 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 GetArguments() + { + Dictionary arguments = new Dictionary(); + foreach (KeyValuePair pair in _argumentLookup) + { + arguments.Add(pair.Key.Name, pair.Value); + } + return arguments; + } } } diff --git a/mustache-sharp/CompoundGenerator.cs b/mustache-sharp/CompoundGenerator.cs index 8cad303..2af4fcb 100644 --- a/mustache-sharp/CompoundGenerator.cs +++ b/mustache-sharp/CompoundGenerator.cs @@ -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 arguments = _arguments.GetArguments(scope); - IEnumerable contexts = _definition.GetChildContext(writer, scope, arguments); + Dictionary arguments = _arguments.GetArguments(keyScope, contextScope); + IEnumerable contexts = _definition.GetChildContext(writer, keyScope, arguments, contextScope); List 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)); diff --git a/mustache-sharp/EachTagDefinition.cs b/mustache-sharp/EachTagDefinition.cs index 30c8e2f..163d7cb 100644 --- a/mustache-sharp/EachTagDefinition.cs +++ b/mustache-sharp/EachTagDefinition.cs @@ -43,10 +43,14 @@ namespace Mustache /// Gets the context to use when building the inner text of the tag. /// /// The text writer passed - /// The current scope. + /// The current scope. /// The arguments passed to the tag. /// The scope to use when building the inner text of the tag. - public override IEnumerable GetChildContext(TextWriter writer, KeyScope scope, Dictionary arguments) + public override IEnumerable GetChildContext( + TextWriter writer, + Scope keyScope, + Dictionary 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; } } diff --git a/mustache-sharp/FormatCompiler.cs b/mustache-sharp/FormatCompiler.cs index 5593fbd..198d282 100644 --- a/mustache-sharp/FormatCompiler.cs +++ b/mustache-sharp/FormatCompiler.cs @@ -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); } /// @@ -150,9 +152,11 @@ namespace Mustache foreach (TagParameter parameter in definition.Parameters) { regexBuilder.Append(@"(\s+?"); - regexBuilder.Append(@"(?"); + regexBuilder.Append(@"(?("); regexBuilder.Append(RegexHelper.CompoundKey); - regexBuilder.Append(@"))"); + regexBuilder.Append("|@"); + regexBuilder.Append(RegexHelper.Key); + regexBuilder.Append(@")))"); if (!parameter.IsRequired) { regexBuilder.Append("?"); diff --git a/mustache-sharp/Generator.cs b/mustache-sharp/Generator.cs index 0553e12..803c068 100644 --- a/mustache-sharp/Generator.cs +++ b/mustache-sharp/Generator.cs @@ -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 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()); + _generator.GetText(scope, writer, contextScope); return writer.ToString(); } } diff --git a/mustache-sharp/IGenerator.cs b/mustache-sharp/IGenerator.cs index 4ddb7ce..6b2fd24 100644 --- a/mustache-sharp/IGenerator.cs +++ b/mustache-sharp/IGenerator.cs @@ -11,10 +11,10 @@ namespace Mustache /// /// Generates the text when applying the format plan. /// - /// The current lexical scope of the keys. + /// The current lexical scope of the keys. /// The text writer to send all text to. - /// The data associated to the context. + /// The data associated to the context. /// The generated text. - void GetText(KeyScope scope, TextWriter writer, object contextData); + void GetText(Scope keyScope, TextWriter writer, Scope contextScope); } } diff --git a/mustache-sharp/IndexTagDefinition.cs b/mustache-sharp/IndexTagDefinition.cs index 86d1759..1d965a7 100644 --- a/mustache-sharp/IndexTagDefinition.cs +++ b/mustache-sharp/IndexTagDefinition.cs @@ -13,28 +13,23 @@ namespace Mustache /// Initializes a new instance of an IndexTagDefinition. /// public IndexTagDefinition() - : base("index") + : base("index", true) { } - /// - /// Gets whether the tag only exists within the scope of its parent. - /// - protected override bool GetIsContextSensitive() - { - return true; - } - /// /// Gets the text to output. /// /// The writer to write the output to. /// The arguments passed to the tag. - /// Extra data passed along with the context. - public override void GetText(TextWriter writer, Dictionary arguments, object contextData) + /// Extra data passed along with the context. + public override void GetText(TextWriter writer, Dictionary arguments, Scope contextScope) { - int index = (int)contextData; - writer.Write(index); + object index; + if (contextScope.TryFind("index", out index)) + { + writer.Write(index); + } } } } diff --git a/mustache-sharp/InlineGenerator.cs b/mustache-sharp/InlineGenerator.cs index cf07ea7..7f5ac12 100644 --- a/mustache-sharp/InlineGenerator.cs +++ b/mustache-sharp/InlineGenerator.cs @@ -5,7 +5,7 @@ using System.IO; namespace Mustache { /// - /// 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. /// 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 arguments = _arguments.GetArguments(scope); - _definition.GetText(writer, arguments, contextData); + Dictionary arguments; + if (_definition.IsSetter) + { + arguments = _arguments.GetArguments(); + } + else + { + arguments = _arguments.GetArguments(scope, context); + } + _definition.GetText(writer, arguments, context); } } } diff --git a/mustache-sharp/KeyGenerator.cs b/mustache-sharp/KeyGenerator.cs index 599ff2a..6f40026 100644 --- a/mustache-sharp/KeyGenerator.cs +++ b/mustache-sharp/KeyGenerator.cs @@ -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); diff --git a/mustache-sharp/NestedContext.cs b/mustache-sharp/NestedContext.cs index 1a0cacb..3c98c8b 100644 --- a/mustache-sharp/NestedContext.cs +++ b/mustache-sharp/NestedContext.cs @@ -19,36 +19,24 @@ namespace Mustache /// Gets or sets the writer to use when generating the child context. /// /// Setting the writer to null will indicate that the tag's writer should be used. - public TextWriter Writer - { - get; - set; - } + public TextWriter Writer { get; set; } /// /// 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. /// - public bool WriterNeedsConsidated - { - get; - set; - } + public bool WriterNeedsConsidated { get; set; } /// - /// 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. /// /// Setting the scope to null will indicate that the current scope should be used. - public KeyScope KeyScope - { - get; - set; - } + public Scope KeyScope { get; set; } /// /// Gets or sets data associated with the context. /// - public object Data { get; set; } + public Scope ContextScope { get; set; } } } diff --git a/mustache-sharp/NewlineTagDefinition.cs b/mustache-sharp/NewlineTagDefinition.cs index e814fbd..dca77e4 100644 --- a/mustache-sharp/NewlineTagDefinition.cs +++ b/mustache-sharp/NewlineTagDefinition.cs @@ -22,8 +22,8 @@ namespace Mustache /// /// The writer to write the output to. /// The arguments passed to the tag. - /// Extra data passed along with the context. - public override void GetText(TextWriter writer, Dictionary arguments, object contextData) + /// Extra data passed along with the context. + public override void GetText(TextWriter writer, Dictionary arguments, Scope context) { writer.Write(Environment.NewLine); } diff --git a/mustache-sharp/RegexHelper.cs b/mustache-sharp/RegexHelper.cs index 58cf7a3..c6738cb 100644 --- a/mustache-sharp/RegexHelper.cs +++ b/mustache-sharp/RegexHelper.cs @@ -8,7 +8,7 @@ namespace Mustache /// public static class RegexHelper { - private const string Key = @"[_\w][_\w\d]*"; + internal const string Key = @"[_\w][_\w\d]*"; internal const string CompoundKey = Key + @"(\." + Key + ")*"; /// diff --git a/mustache-sharp/KeyScope.cs b/mustache-sharp/Scope.cs similarity index 55% rename from mustache-sharp/KeyScope.cs rename to mustache-sharp/Scope.cs index 7e176d9..9b7e850 100644 --- a/mustache-sharp/KeyScope.cs +++ b/mustache-sharp/Scope.cs @@ -9,16 +9,16 @@ namespace Mustache /// /// Represents a scope of keys. /// - public sealed class KeyScope + public sealed class Scope { private readonly object _source; - private readonly KeyScope _parent; + private readonly Scope _parent; /// /// Initializes a new instance of a KeyScope. /// /// The object to search for keys in. - internal KeyScope(object source) + internal Scope(object source) : this(source, null) { } @@ -28,7 +28,7 @@ namespace Mustache /// /// The object to search for keys in. /// The parent scope to search in if the value is not found. - internal KeyScope(object source, KeyScope parent) + internal Scope(object source, Scope parent) { _parent = parent; _source = source; @@ -44,14 +44,23 @@ namespace Mustache /// public event EventHandler KeyNotFound; + /// + /// Creates a child scope that searches for keys in a default dictionary of key/value pairs. + /// + /// The new child scope. + public Scope CreateChildScope() + { + return CreateChildScope(new Dictionary()); + } + /// /// Creates a child scope that searches for keys in the given object. /// /// The object to search for keys in. /// The new child scope. - 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 /// A key with the given name could not be found. 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 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 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 toLookup(object value) @@ -128,5 +124,59 @@ namespace Mustache } return lookup; } + + internal void Set(string key, object value) + { + IDictionary 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 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 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); + } } } diff --git a/mustache-sharp/SetTagDefinition.cs b/mustache-sharp/SetTagDefinition.cs new file mode 100644 index 0000000..21eede6 --- /dev/null +++ b/mustache-sharp/SetTagDefinition.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.IO; + +namespace Mustache +{ + /// + /// Defines a tag that declares a named value in the current context. + /// + internal sealed class SetTagDefinition : InlineTagDefinition + { + private const string nameParameter = "name"; + private static readonly TagParameter name = new TagParameter(nameParameter) { IsRequired = true }; + + /// + /// Initializes a new instance of an SetTagDefinition. + /// + public SetTagDefinition() + : base("set", true) + { + } + + protected override bool GetIsSetter() + { + return true; + } + + protected override IEnumerable GetParameters() + { + return new TagParameter[] { name }; + } + + /// + /// Gets the text to output. + /// + /// The writer to write the output to. + /// The arguments passed to the tag. + /// Extra data passed along with the context. + public override void GetText(TextWriter writer, Dictionary arguments, Scope contextScope) + { + string name = (string)arguments[nameParameter]; + // TODO - get the value for the variable + object value = null; + contextScope.Set(name, value); + } + } +} diff --git a/mustache-sharp/StaticGenerator.cs b/mustache-sharp/StaticGenerator.cs index 8ac8996..4068761 100644 --- a/mustache-sharp/StaticGenerator.cs +++ b/mustache-sharp/StaticGenerator.cs @@ -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); } diff --git a/mustache-sharp/TagDefinition.cs b/mustache-sharp/TagDefinition.cs index dec179f..429b131 100644 --- a/mustache-sharp/TagDefinition.cs +++ b/mustache-sharp/TagDefinition.cs @@ -44,6 +44,16 @@ namespace Mustache get { return _tagName; } } + internal bool IsSetter + { + get { return GetIsSetter(); } + } + + protected virtual bool GetIsSetter() + { + return false; + } + /// /// Gets whether the tag is limited to the parent tag's context. /// @@ -139,12 +149,22 @@ namespace Mustache /// Gets the context to use when building the inner text of the tag. /// /// The text writer passed - /// The current scope. + /// The current key scope. /// The arguments passed to the tag. /// The scope to use when building the inner text of the tag. - public virtual IEnumerable GetChildContext(TextWriter writer, KeyScope scope, Dictionary arguments) + public virtual IEnumerable GetChildContext( + TextWriter writer, + Scope keyScope, + Dictionary 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; } /// @@ -152,8 +172,8 @@ namespace Mustache /// /// The text writer to write to. /// The arguments passed to the tag. - /// The data associated to the context. - public virtual void GetText(TextWriter writer, Dictionary arguments, object contextData) + /// The data associated to the context. + public virtual void GetText(TextWriter writer, Dictionary arguments, Scope context) { } diff --git a/mustache-sharp/WithGenerator.cs b/mustache-sharp/WithTagDefinition.cs similarity index 77% rename from mustache-sharp/WithGenerator.cs rename to mustache-sharp/WithTagDefinition.cs index 9a2c2c8..7ef40b5 100644 --- a/mustache-sharp/WithGenerator.cs +++ b/mustache-sharp/WithTagDefinition.cs @@ -50,13 +50,23 @@ namespace Mustache /// Gets the context to use when building the inner text of the tag. /// /// The text writer passed - /// The current scope. + /// The current key scope. /// The arguments passed to the tag. /// The scope to use when building the inner text of the tag. - public override IEnumerable GetChildContext(TextWriter writer, KeyScope scope, Dictionary arguments) + public override IEnumerable GetChildContext( + TextWriter writer, + Scope keyScope, + Dictionary 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; } } } diff --git a/mustache-sharp/mustache-sharp.csproj b/mustache-sharp/mustache-sharp.csproj index 55d74ea..38316ad 100644 --- a/mustache-sharp/mustache-sharp.csproj +++ b/mustache-sharp/mustache-sharp.csproj @@ -40,6 +40,7 @@ + @@ -68,8 +69,8 @@ - - + +