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:
parent
f136dd61a5
commit
6e74fa1fcc
37
README.md
37
README.md
|
@ -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.
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")]
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,7 +28,7 @@ namespace Mustache
|
|||
Dictionary<string, object> arguments;
|
||||
if (_definition.IsSetter)
|
||||
{
|
||||
arguments = _arguments.GetArguments();
|
||||
arguments = _arguments.GetArgumentKeyNames();
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
|
@ -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")]
|
|
@ -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)
|
||||
{
|
||||
return value;
|
||||
}
|
||||
KeyFoundEventArgs args = new KeyFoundEventArgs(name, value);
|
||||
KeyFound(this, args);
|
||||
value = args.Substitute;
|
||||
}
|
||||
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))
|
||||
tryFindFirst(results);
|
||||
}
|
||||
for (int index = 1; results.Found && index < results.Members.Length; ++index)
|
||||
{
|
||||
value = null;
|
||||
return false;
|
||||
results.Lookup = toLookup(results.Value);
|
||||
results.MemberIndex = index;
|
||||
object value;
|
||||
results.Found = results.Lookup.TryGetValue(results.Member, out value);
|
||||
results.Value = value;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
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; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue