Support scoped variables

I have been slowly working toward supporting variables that can be
declared/updated during output generation. This code also cleans up the
code for searching within a scope.
This commit is contained in:
Travis Parks 2013-10-28 15:58:50 -04:00
parent f136dd61a5
commit 6e74fa1fcc
12 changed files with 233 additions and 69 deletions

View File

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

View File

@ -1185,6 +1185,51 @@ Your order total was: $7.50";
Assert.AreEqual(expected, actual, "The numbers were not valid.");
}
/// <summary>
/// I can set context variables to control the flow of the code generation.
/// </summary>
[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.");
}
/// <summary>
/// I can set context variables to control the flow of the code generation.
/// It should even support nested context variables... for some reason.
/// </summary>
[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
}
}

View File

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

View File

@ -76,14 +76,9 @@ namespace Mustache
return arguments;
}
public Dictionary<string, object> GetArguments()
public Dictionary<string, object> GetArgumentKeyNames()
{
Dictionary<string, object> arguments = new Dictionary<string, object>();
foreach (KeyValuePair<TagParameter, string> pair in _argumentLookup)
{
arguments.Add(pair.Key.Name, pair.Value);
}
return arguments;
return _argumentLookup.ToDictionary(p => p.Key.Name, p => (object)p.Value);
}
}
}

View File

@ -152,10 +152,8 @@ 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(RegexHelper.Key);
regexBuilder.Append(@")))");
if (!parameter.IsRequired)
{

View File

@ -13,6 +13,7 @@ namespace Mustache
private readonly IGenerator _generator;
private readonly List<EventHandler<KeyFoundEventArgs>> _foundHandlers;
private readonly List<EventHandler<KeyNotFoundEventArgs>> _notFoundHandlers;
private readonly List<EventHandler<ValueRequestEventArgs>> _valueRequestedHandlers;
/// <summary>
/// Initializes a new instance of a Generator.
@ -23,6 +24,7 @@ namespace Mustache
_generator = generator;
_foundHandlers = new List<EventHandler<KeyFoundEventArgs>>();
_notFoundHandlers = new List<EventHandler<KeyNotFoundEventArgs>>();
_valueRequestedHandlers = new List<EventHandler<ValueRequestEventArgs>>();
}
/// <summary>
@ -43,6 +45,15 @@ namespace Mustache
remove { _notFoundHandlers.Remove(value); }
}
/// <summary>
/// Occurs when a setter is encountered and requires a value to be provided.
/// </summary>
public event EventHandler<ValueRequestEventArgs> ValueRequested
{
add { _valueRequestedHandlers.Add(value); }
remove { _valueRequestedHandlers.Remove(value); }
}
/// <summary>
/// Gets the text that is generated for the given object.
/// </summary>
@ -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<string, object>());
foreach (EventHandler<KeyFoundEventArgs> handler in _foundHandlers)
{
scope.KeyFound += handler;
keyScope.KeyFound += handler;
contextScope.KeyFound += handler;
}
foreach (EventHandler<KeyNotFoundEventArgs> handler in _notFoundHandlers)
{
scope.KeyNotFound += handler;
keyScope.KeyNotFound += handler;
contextScope.KeyNotFound += handler;
}
foreach (EventHandler<ValueRequestEventArgs> handler in _valueRequestedHandlers)
{
contextScope.ValueRequested += handler;
}
StringWriter writer = new StringWriter(provider);
Scope contextScope = new Scope(new Dictionary<string, object>());
_generator.GetText(scope, writer, contextScope);
_generator.GetText(keyScope, writer, contextScope);
return writer.ToString();
}
}

View File

@ -28,7 +28,7 @@ namespace Mustache
Dictionary<string, object> arguments;
if (_definition.IsSetter)
{
arguments = _arguments.GetArguments();
arguments = _arguments.GetArgumentKeyNames();
}
else
{

View File

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

View File

@ -44,6 +44,11 @@ namespace Mustache
/// </summary>
public event EventHandler<KeyNotFoundEventArgs> KeyNotFound;
/// <summary>
/// Occurs when a setter is encountered and requires a value to be provided.
/// </summary>
public event EventHandler<ValueRequestEventArgs> ValueRequested;
/// <summary>
/// Creates a child scope that searches for keys in a default dictionary of key/value pairs.
/// </summary>
@ -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
/// <exception cref="System.Collections.Generic.KeyNotFoundException">A key with the given name could not be found.</exception>
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<string, object> 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<string, object> context = new Dictionary<string, object>();
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<string, object> 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<string, object> 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<string, object> 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; }
}
}

View File

@ -39,9 +39,7 @@ namespace Mustache
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);
contextScope.Set(name);
}
}
}

View File

@ -0,0 +1,15 @@
using System;
namespace Mustache
{
/// <summary>
/// Holds the value that a context variable is set to.
/// </summary>
public class ValueRequestEventArgs : EventArgs
{
/// <summary>
/// Gets or sets the value being set.
/// </summary>
public object Value { get; set; }
}
}

View File

@ -70,6 +70,7 @@
<Compile Include="TagDefinition.cs" />
<Compile Include="TagParameter.cs" />
<Compile Include="Scope.cs" />
<Compile Include="ValueRequestEventArgs.cs" />
<Compile Include="WithTagDefinition.cs" />
</ItemGroup>
<ItemGroup>