diff --git a/README.md b/README.md index 35c190b..90bb8c3 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Generating text has always been a chore. Either you're concatenating strings lik [mustache](http://mustache.github.com/) is a really simple tool for generating text. .NET developers already had access to `String.Format` to accomplish pretty much the same thing. The only problem was that `String.Format` used indexes for placeholders: `Hello, {0}!!!`. **mustache** let you use meaningful names for placeholders: `Hello, {{name}}!!!`. -**mustache** is a logic-less text generator. However, almost every time I've ever needed to generate text I needed to turn some of it on or off depending on a value. Not having the ability to turn things off usually meant going back to building my text in parts. +**mustache** is a logic-less text generator. However, almost every time I've ever needed to generate text I needed to turn some of it on or off depending on a value. Not having the ability to turn things off usually meant going back to building my text in parts. Introducing [handlebars.js](http://handlebarsjs.com/)... If you've needed to generate any HTML templates, **handlebars.js** is a really awesome tool. Not only does it support an `if` and `each` tag, it lets you define your own tags! It also makes it easy to reference nested values `{{Customer.Address.ZipCode}}`. @@ -73,7 +73,7 @@ The **if** tag allows you to conditionally include a block of text. The block will be printed if: * The value is a non-empty string. * The value is a non-empty collection. -* The value isn't the NULL char. +* The value isn't the NUL char. * The value is a non-zero number. * The value evaluates to true. @@ -117,6 +117,41 @@ Within a block of text, you may refer to a same top-level placeholder over and o Here, the `Customer.Address` property will be searched first for the placeholders. If a property cannot be found in the `Address` object, it will be searched for in the `Customer` object and on up. +## The 'set' tag +**mustache#** provides limited support for variables through use of the `set` tag. Once a variable is declared, it is visible to all child scopes. Multiple definitions of a variable with the same name cannot be created within the same scope. In fact, I highly recommend making variable names unique to the entire template just to prevent unexpected behavior! + +The following example will print out "EvenOddEvenOdd" by toggling a variable called `even`: + + FormatCompiler compiler = new FormatCompiler(); + const string format = @"{{#set even}} + {{#each this}} + {{#if @even}} + Even + {{#else}} + Odd + {{/if}} + {{#set even}} + {{/each}}"; + Generator generator = compiler.Compile(format); + generator.ValueRequested += (sender, e) => + { + e.Value = !(bool)(e.Value ?? false); + }; + string result = generator.Render(new int[] { 0, 1, 2, 3 }); + +This code works by specifying a function to call whenever a value is needed for the `even` variable. The first time the function is called, `e.Value` will be null. All additional calls will hold the last known value of the variable. + +Notice that when you set the variable, you don't qualify it with an `@`. You only need the `@` when you request its value, like in the `if` statement above. + +You should attempt to limit your use of variables within templates. Instead, perform as many up-front calculations as possible and make sure your view model closely represents its final appearance. In this case, it would make more sense to first convert the array into strings of "Even" and "Odd". + + FormatCompiler compiler = new FormatCompiler(); + const string format = @"{{#each this}}{{this}}{{/each}}"; + Generator generator = compiler.Compile(format); + string result = generator.Render(new string[] { "Even", "Odd", "Even", "Odd" }); + +This code is much easier to read and understand. It is also going to run significantly faster. In cases where you also need the original value, you can create an array containing objects with properties for the original value *and* `Even`/`Odd`. + ## Defining Your Own Tags If you need to define your own tags, **mustache#** has everything you need. diff --git a/mustache-sharp.test/FormatCompilerTester.cs b/mustache-sharp.test/FormatCompilerTester.cs index 3e37c24..e8cf7b7 100644 --- a/mustache-sharp.test/FormatCompilerTester.cs +++ b/mustache-sharp.test/FormatCompilerTester.cs @@ -1185,6 +1185,51 @@ Your order total was: $7.50"; Assert.AreEqual(expected, actual, "The numbers were not valid."); } + /// + /// I can set context variables to control the flow of the code generation. + /// + [TestMethod] + public void TestCompile_CanUseContextVariableToToggle() + { + FormatCompiler compiler = new FormatCompiler(); + const string format = @"{{#set even}}{{#each this}}{{#if @even}}Even {{#else}}Odd {{/if}}{{#set even}}{{/each}}"; + Generator generator = compiler.Compile(format); + generator.ValueRequested += (sender, e) => + { + e.Value = !(bool)(e.Value ?? false); + }; + string actual = generator.Render(new int[] { 1, 1, 1, 1 }); + string expected = "Even Odd Even Odd "; + Assert.AreEqual(expected, actual, "The context variable was not toggled."); + } + + /// + /// I can set context variables to control the flow of the code generation. + /// It should even support nested context variables... for some reason. + /// + [TestMethod] + public void TestCompile_CanUseNestedContextVariableToToggle() + { + FormatCompiler compiler = new FormatCompiler(); + const string format = @"{{#set this.variables.even}} +{{#each this}} +{{#if @variables.even}} +Even +{{#else}} +Odd +{{/if}} +{{#set variables.even}} +{{/each}}"; + Generator generator = compiler.Compile(format); + generator.ValueRequested += (sender, e) => + { + e.Value = !(bool)(e.Value ?? false); + }; + string actual = generator.Render(new int[] { 1, 1, 1, 1 }); + string expected = "EvenOddEvenOdd"; + Assert.AreEqual(expected, actual, "The context variable was not toggled."); + } + #endregion } } diff --git a/mustache-sharp.test/Properties/AssemblyInfo.cs b/mustache-sharp.test/Properties/AssemblyInfo.cs index 972b946..46ca91e 100644 --- a/mustache-sharp.test/Properties/AssemblyInfo.cs +++ b/mustache-sharp.test/Properties/AssemblyInfo.cs @@ -31,5 +31,5 @@ using System.Runtime.InteropServices; // // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: -[assembly: AssemblyVersion("0.2.0.0")] -[assembly: AssemblyFileVersion("0.2.0.0")] +[assembly: AssemblyVersion("0.2.1.0")] +[assembly: AssemblyFileVersion("0.2.1.0")] diff --git a/mustache-sharp/ArgumentCollection.cs b/mustache-sharp/ArgumentCollection.cs index 93e2518..97d41f8 100644 --- a/mustache-sharp/ArgumentCollection.cs +++ b/mustache-sharp/ArgumentCollection.cs @@ -76,14 +76,9 @@ namespace Mustache return arguments; } - public Dictionary GetArguments() + public Dictionary GetArgumentKeyNames() { - Dictionary arguments = new Dictionary(); - foreach (KeyValuePair pair in _argumentLookup) - { - arguments.Add(pair.Key.Name, pair.Value); - } - return arguments; + return _argumentLookup.ToDictionary(p => p.Key.Name, p => (object)p.Value); } } } diff --git a/mustache-sharp/FormatCompiler.cs b/mustache-sharp/FormatCompiler.cs index 198d282..92027f0 100644 --- a/mustache-sharp/FormatCompiler.cs +++ b/mustache-sharp/FormatCompiler.cs @@ -152,10 +152,8 @@ namespace Mustache foreach (TagParameter parameter in definition.Parameters) { regexBuilder.Append(@"(\s+?"); - regexBuilder.Append(@"(?("); + regexBuilder.Append(@"(?(@?"); regexBuilder.Append(RegexHelper.CompoundKey); - regexBuilder.Append("|@"); - regexBuilder.Append(RegexHelper.Key); regexBuilder.Append(@")))"); if (!parameter.IsRequired) { diff --git a/mustache-sharp/Generator.cs b/mustache-sharp/Generator.cs index 803c068..bc2f0c7 100644 --- a/mustache-sharp/Generator.cs +++ b/mustache-sharp/Generator.cs @@ -13,6 +13,7 @@ namespace Mustache private readonly IGenerator _generator; private readonly List> _foundHandlers; private readonly List> _notFoundHandlers; + private readonly List> _valueRequestedHandlers; /// /// Initializes a new instance of a Generator. @@ -23,6 +24,7 @@ namespace Mustache _generator = generator; _foundHandlers = new List>(); _notFoundHandlers = new List>(); + _valueRequestedHandlers = new List>(); } /// @@ -43,6 +45,15 @@ namespace Mustache remove { _notFoundHandlers.Remove(value); } } + /// + /// Occurs when a setter is encountered and requires a value to be provided. + /// + public event EventHandler ValueRequested + { + add { _valueRequestedHandlers.Add(value); } + remove { _valueRequestedHandlers.Remove(value); } + } + /// /// Gets the text that is generated for the given object. /// @@ -70,18 +81,24 @@ namespace Mustache private string render(IFormatProvider provider, object source) { - Scope scope = new Scope(source); + Scope keyScope = new Scope(source); + Scope contextScope = new Scope(new Dictionary()); foreach (EventHandler handler in _foundHandlers) { - scope.KeyFound += handler; + keyScope.KeyFound += handler; + contextScope.KeyFound += handler; } foreach (EventHandler handler in _notFoundHandlers) { - scope.KeyNotFound += handler; + keyScope.KeyNotFound += handler; + contextScope.KeyNotFound += handler; + } + foreach (EventHandler handler in _valueRequestedHandlers) + { + contextScope.ValueRequested += handler; } StringWriter writer = new StringWriter(provider); - Scope contextScope = new Scope(new Dictionary()); - _generator.GetText(scope, writer, contextScope); + _generator.GetText(keyScope, writer, contextScope); return writer.ToString(); } } diff --git a/mustache-sharp/InlineGenerator.cs b/mustache-sharp/InlineGenerator.cs index 7f5ac12..0093d15 100644 --- a/mustache-sharp/InlineGenerator.cs +++ b/mustache-sharp/InlineGenerator.cs @@ -28,7 +28,7 @@ namespace Mustache Dictionary arguments; if (_definition.IsSetter) { - arguments = _arguments.GetArguments(); + arguments = _arguments.GetArgumentKeyNames(); } else { diff --git a/mustache-sharp/Properties/AssemblyInfo.cs b/mustache-sharp/Properties/AssemblyInfo.cs index abef423..fa9b095 100644 --- a/mustache-sharp/Properties/AssemblyInfo.cs +++ b/mustache-sharp/Properties/AssemblyInfo.cs @@ -34,6 +34,6 @@ using System.Runtime.CompilerServices; // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("0.2.0.0")] -[assembly: AssemblyFileVersion("0.2.0.0")] +[assembly: AssemblyVersion("0.2.1.0")] +[assembly: AssemblyFileVersion("0.2.1.0")] [assembly: InternalsVisibleTo("mustache-sharp.test")] \ No newline at end of file diff --git a/mustache-sharp/Scope.cs b/mustache-sharp/Scope.cs index 9b7e850..d453721 100644 --- a/mustache-sharp/Scope.cs +++ b/mustache-sharp/Scope.cs @@ -44,6 +44,11 @@ namespace Mustache /// public event EventHandler KeyNotFound; + /// + /// Occurs when a setter is encountered and requires a value to be provided. + /// + public event EventHandler ValueRequested; + /// /// Creates a child scope that searches for keys in a default dictionary of key/value pairs. /// @@ -63,6 +68,7 @@ namespace Mustache Scope scope = new Scope(source, this); scope.KeyFound = KeyFound; scope.KeyNotFound = KeyNotFound; + scope.ValueRequested = ValueRequested; return scope; } @@ -74,41 +80,43 @@ namespace Mustache /// A key with the given name could not be found. internal object Find(string name) { - string member = null; - object value = null; - if (tryFind(name, ref member, ref value)) + SearchResults results = tryFind(name); + if (results.Found) { - onKeyFound(name, ref value); - return value; + return onKeyFound(name, results.Value); } - if (onKeyNotFound(name, member, ref value)) + object value; + if (onKeyNotFound(name, results.Member, out value)) { return value; } - string message = String.Format(CultureInfo.CurrentCulture, Resources.KeyNotFound, member); + string message = String.Format(CultureInfo.CurrentCulture, Resources.KeyNotFound, results.Member); throw new KeyNotFoundException(message); } - private void onKeyFound(string name, ref object value) + private object onKeyFound(string name, object value) { - if (KeyFound != null) + if (KeyFound == null) { - KeyFoundEventArgs args = new KeyFoundEventArgs(name, value); - KeyFound(this, args); - value = args.Substitute; + return value; } + KeyFoundEventArgs args = new KeyFoundEventArgs(name, value); + KeyFound(this, args); + return args.Substitute; } - private bool onKeyNotFound(string name, string member, ref object value) + private bool onKeyNotFound(string name, string member, out object value) { if (KeyNotFound == null) { + value = null; return false; } KeyNotFoundEventArgs args = new KeyNotFoundEventArgs(name, member); KeyNotFound(this, args); if (!args.Handled) { + value = null; return false; } value = args.Substitute; @@ -125,58 +133,110 @@ namespace Mustache return lookup; } + internal void Set(string key) + { + SearchResults results = tryFind(key); + if (ValueRequested == null) + { + set(results, results.Value); + return; + } + + ValueRequestEventArgs e = new ValueRequestEventArgs(); + if (results.Found) + { + e.Value = results.Value; + } + + ValueRequested(this, e); + set(results, e.Value); + } + internal void Set(string key, object value) { - IDictionary lookup = toLookup(_source); - lookup[key] = value; + SearchResults results = tryFind(key); + set(results, value); + } + + private void set(SearchResults results, object value) + { + // handle setting value in child scope + while (results.MemberIndex < results.Members.Length - 1) + { + Dictionary context = new Dictionary(); + results.Value = context; + results.Lookup[results.Member] = results.Value; + results.Lookup = context; + ++results.MemberIndex; + } + results.Lookup[results.Member] = value; } public bool TryFind(string name, out object value) { - string member = null; - value = null; - return tryFind(name, ref member, ref value); + SearchResults result = tryFind(name); + value = result.Value; + return result.Found; } - private bool tryFind(string name, ref string member, ref object value) + private SearchResults tryFind(string name) { - string[] names = name.Split('.'); - member = names[0]; - value = _source; - if (member != "this") + SearchResults results = new SearchResults(); + results.Members = name.Split('.'); + results.MemberIndex = 0; + if (results.Member == "this") { - if (!tryFindFirst(member, ref value)) - { - return false; - } + results.Found = true; + results.Lookup = toLookup(_source); + results.Value = _source; } - for (int index = 1; index < names.Length; ++index) + else { - IDictionary context = toLookup(value); - member = names[index]; - if (!context.TryGetValue(member, out value)) - { - value = null; - return false; - } + tryFindFirst(results); } - return true; + for (int index = 1; results.Found && index < results.Members.Length; ++index) + { + results.Lookup = toLookup(results.Value); + results.MemberIndex = index; + object value; + results.Found = results.Lookup.TryGetValue(results.Member, out value); + results.Value = value; + } + return results; } - private bool tryFindFirst(string member, ref object value) + private void tryFindFirst(SearchResults results) { - IDictionary lookup = toLookup(_source); - if (lookup.ContainsKey(member)) + results.Lookup = toLookup(_source); + object value; + if (results.Lookup.TryGetValue(results.Member, out value)) { - value = lookup[member]; - return true; + results.Found = true; + results.Value = value; + return; } if (_parent == null) { - value = null; - return false; + results.Found = false; + results.Value = null; + return; } - return _parent.tryFindFirst(member, ref value); + _parent.tryFindFirst(results); } } + + internal class SearchResults + { + public IDictionary Lookup { get; set; } + + public string[] Members { get; set; } + + public int MemberIndex { get; set; } + + public string Member { get { return Members[MemberIndex]; } } + + public bool Found { get; set; } + + public object Value { get; set; } + } } diff --git a/mustache-sharp/SetTagDefinition.cs b/mustache-sharp/SetTagDefinition.cs index 21eede6..7ee73bb 100644 --- a/mustache-sharp/SetTagDefinition.cs +++ b/mustache-sharp/SetTagDefinition.cs @@ -39,9 +39,7 @@ namespace Mustache 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); + contextScope.Set(name); } } } diff --git a/mustache-sharp/ValueRequestEventArgs.cs b/mustache-sharp/ValueRequestEventArgs.cs new file mode 100644 index 0000000..f6e2c9a --- /dev/null +++ b/mustache-sharp/ValueRequestEventArgs.cs @@ -0,0 +1,15 @@ +using System; + +namespace Mustache +{ + /// + /// Holds the value that a context variable is set to. + /// + public class ValueRequestEventArgs : EventArgs + { + /// + /// Gets or sets the value being set. + /// + public object Value { get; set; } + } +} diff --git a/mustache-sharp/mustache-sharp.csproj b/mustache-sharp/mustache-sharp.csproj index 38316ad..55c15ae 100644 --- a/mustache-sharp/mustache-sharp.csproj +++ b/mustache-sharp/mustache-sharp.csproj @@ -70,6 +70,7 @@ +