diff --git a/mustache-sharp.test/FormatCompilerTester.cs b/mustache-sharp.test/FormatCompilerTester.cs index 71aaf22..148ba2b 100644 --- a/mustache-sharp.test/FormatCompilerTester.cs +++ b/mustache-sharp.test/FormatCompilerTester.cs @@ -332,7 +332,7 @@ Content"; /// registering with the PlaceholderFound event. /// [TestMethod] - public void TestCompile_FindsKeys_RecordsKeys() + public void TestCompile_FindsPlaceholders_RecordsPlaceholders() { FormatCompiler compiler = new FormatCompiler(); HashSet keys = new HashSet(); @@ -346,6 +346,28 @@ Content"; CollectionAssert.AreEqual(expected, actual, "Not all placeholders were found."); } + /// + /// We can determine the context in which a placeholder is found by looking at the provided context array. + /// + [TestMethod] + public void TestCompile_FindsPlaceholders_ProvidesContext() + { + FormatCompiler compiler = new FormatCompiler(); + Context[] context = null; + compiler.PlaceholderFound += (o, e) => + { + context = e.Context; + }; + compiler.Compile(@"{{#with Address}}{{ZipCode}}{{/with}}"); + + Assert.IsNotNull(context, "The context was not set."); + Assert.AreEqual(2, context.Length, "The context did not contain the right number of items."); + Assert.AreEqual(String.Empty, context[0].Tag.Name, "The top-most context had the wrong tag type."); + Assert.AreEqual("this", context[0].Argument, "The top-level argument should always be 'this'."); + Assert.AreEqual("with", context[1].Tag.Name, "The inner context should have been a 'with' tag."); + Assert.AreEqual("Address", context[1].Argument, "The inner context argument was wrong."); + } + #endregion #region Comment diff --git a/mustache-sharp.test/Properties/AssemblyInfo.cs b/mustache-sharp.test/Properties/AssemblyInfo.cs index b5286ae..8a79168 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.0.6.0")] -[assembly: AssemblyFileVersion("0.0.6.0")] +[assembly: AssemblyVersion("0.0.7.0")] +[assembly: AssemblyFileVersion("0.0.7.0")] diff --git a/mustache-sharp/ArgumentCollection.cs b/mustache-sharp/ArgumentCollection.cs index 835810a..a216ab3 100644 --- a/mustache-sharp/ArgumentCollection.cs +++ b/mustache-sharp/ArgumentCollection.cs @@ -1,56 +1,74 @@ -using System; -using System.Collections.Generic; - -namespace mustache -{ - /// - /// Associates parameters to their argument values. - /// - internal sealed class ArgumentCollection - { - private readonly Dictionary _argumentLookup; - - /// - /// Initializes a new instance of an ArgumentCollection. - /// - public ArgumentCollection() - { - _argumentLookup = new Dictionary(); - } - - /// - /// Associates the given parameter to the key placeholder. - /// - /// The parameter to associate the key with. - /// The key placeholder used as the argument. - /// If the key is null, the default value of the parameter will be used. - public void AddArgument(TagParameter parameter, string key) - { - _argumentLookup.Add(parameter, key); - } - - /// - /// Substitutes the key placeholders with their respective values. - /// - /// The current lexical scope. - /// A dictionary associating the parameter name to the associated value. - public Dictionary GetArguments(KeyScope scope) - { - Dictionary arguments = new Dictionary(); - foreach (KeyValuePair pair in _argumentLookup) - { - object value; - if (pair.Value == null) - { - value = pair.Key.DefaultValue; - } - else - { - value = scope.Find(pair.Value); - } - arguments.Add(pair.Key.Name, value); - } - return arguments; - } - } -} +using System; +using System.Collections.Generic; +using System.Linq; + +namespace mustache +{ + /// + /// Associates parameters to their argument values. + /// + internal sealed class ArgumentCollection + { + private readonly Dictionary _argumentLookup; + + /// + /// Initializes a new instance of an ArgumentCollection. + /// + public ArgumentCollection() + { + _argumentLookup = new Dictionary(); + } + + /// + /// Associates the given parameter to the key placeholder. + /// + /// The parameter to associate the key with. + /// The key placeholder used as the argument. + /// If the key is null, the default value of the parameter will be used. + public void AddArgument(TagParameter parameter, string key) + { + _argumentLookup.Add(parameter, key); + } + + /// + /// Gets the key that will be used to find the substitute value. + /// + /// The name of the parameter. + public string GetKey(TagParameter parameter) + { + string key; + if (_argumentLookup.TryGetValue(parameter, out key)) + { + return key; + } + else + { + return null; + } + } + + /// + /// Substitutes the key placeholders with their respective values. + /// + /// The current lexical scope. + /// A dictionary associating the parameter name to the associated value. + public Dictionary GetArguments(KeyScope scope) + { + Dictionary arguments = new Dictionary(); + foreach (KeyValuePair pair in _argumentLookup) + { + object value; + if (pair.Value == null) + { + value = pair.Key.DefaultValue; + } + else + { + value = scope.Find(pair.Value); + } + arguments.Add(pair.Key.Name, value); + } + return arguments; + } + } +} diff --git a/mustache-sharp/CompoundGenerator.cs b/mustache-sharp/CompoundGenerator.cs index b641026..c01add3 100644 --- a/mustache-sharp/CompoundGenerator.cs +++ b/mustache-sharp/CompoundGenerator.cs @@ -1,105 +1,119 @@ -using System; -using System.Collections.Generic; -using System.IO; - -namespace mustache -{ - /// - /// Builds text by combining the output of other generators. - /// - internal sealed class CompoundGenerator : IGenerator - { - private readonly TagDefinition _definition; - private readonly ArgumentCollection _arguments; - private readonly LinkedList _primaryGenerators; - private IGenerator _subGenerator; - - /// - /// Initializes a new instance of a CompoundGenerator. - /// - /// The tag that the text is being generated for. - /// The arguments that were passed to the tag. - public CompoundGenerator(TagDefinition definition, ArgumentCollection arguments) - { - _definition = definition; - _arguments = arguments; - _primaryGenerators = new LinkedList(); - } - - /// - /// Adds the given generator. - /// - /// The generator to add. - public void AddGenerator(IGenerator generator) - { - addGenerator(generator, false); - } - - /// - /// Adds the given generator, determining whether the generator should - /// be part of the primary generators or added as an secondary generator. - /// - /// The tag that the generator is generating text for. - /// The generator to add. - public void AddGenerator(TagDefinition definition, IGenerator generator) - { - bool isSubGenerator = _definition.ShouldCreateSecondaryGroup(definition); - addGenerator(generator, isSubGenerator); - } - - /// - /// Creates a StaticGenerator from the given value and adds it. - /// - /// The static generators to add. - public void AddStaticGenerators(IEnumerable generators) - { - foreach (StaticGenerator generator in generators) - { - LinkedListNode node = _primaryGenerators.AddLast(generator); - generator.Node = node; - } - } - - private void addGenerator(IGenerator generator, bool isSubGenerator) - { - if (isSubGenerator) - { - _subGenerator = generator; - } - else - { - _primaryGenerators.AddLast(generator); - } - } - - void IGenerator.GetText(KeyScope scope, TextWriter writer) - { - Dictionary arguments = _arguments.GetArguments(scope); - IEnumerable contexts = _definition.GetChildContext(writer, scope, arguments); - LinkedList generators; - if (_definition.ShouldGeneratePrimaryGroup(arguments)) - { - generators = _primaryGenerators; - } - else - { - generators = new LinkedList(); - if (_subGenerator != null) - { - generators.AddLast(_subGenerator); - } - } - foreach (NestedContext context in contexts) - { - foreach (IGenerator generator in generators) - { - generator.GetText(context.KeyScope ?? scope, context.Writer ?? writer); - if (context.WriterNeedsConsidated) - { - writer.Write(_definition.ConsolidateWriter(context.Writer ?? writer, arguments)); - } - } - } - } - } +using System; +using System.Collections.Generic; +using System.IO; + +namespace mustache +{ + /// + /// Builds text by combining the output of other generators. + /// + internal sealed class CompoundGenerator : IGenerator + { + private readonly TagDefinition _definition; + private readonly ArgumentCollection _arguments; + private readonly LinkedList _primaryGenerators; + private IGenerator _subGenerator; + + /// + /// Initializes a new instance of a CompoundGenerator. + /// + /// The tag that the text is being generated for. + /// The arguments that were passed to the tag. + public CompoundGenerator(TagDefinition definition, ArgumentCollection arguments) + { + _definition = definition; + _arguments = arguments; + _primaryGenerators = new LinkedList(); + } + + /// + /// Gets the argument that will act as the context for the content. + /// + /// The argument that will act as the context for the content. + public string GetContextArgument() + { + TagParameter parameter = _definition.GetChildContextParameter(); + if (parameter == null) + { + return null; + } + return _arguments.GetKey(parameter); + } + + /// + /// Adds the given generator. + /// + /// The generator to add. + public void AddGenerator(IGenerator generator) + { + addGenerator(generator, false); + } + + /// + /// Adds the given generator, determining whether the generator should + /// be part of the primary generators or added as an secondary generator. + /// + /// The tag that the generator is generating text for. + /// The generator to add. + public void AddGenerator(TagDefinition definition, IGenerator generator) + { + bool isSubGenerator = _definition.ShouldCreateSecondaryGroup(definition); + addGenerator(generator, isSubGenerator); + } + + /// + /// Creates a StaticGenerator from the given value and adds it. + /// + /// The static generators to add. + public void AddStaticGenerators(IEnumerable generators) + { + foreach (StaticGenerator generator in generators) + { + LinkedListNode node = _primaryGenerators.AddLast(generator); + generator.Node = node; + } + } + + private void addGenerator(IGenerator generator, bool isSubGenerator) + { + if (isSubGenerator) + { + _subGenerator = generator; + } + else + { + _primaryGenerators.AddLast(generator); + } + } + + void IGenerator.GetText(KeyScope scope, TextWriter writer) + { + Dictionary arguments = _arguments.GetArguments(scope); + IEnumerable contexts = _definition.GetChildContext(writer, scope, arguments); + LinkedList generators; + if (_definition.ShouldGeneratePrimaryGroup(arguments)) + { + generators = _primaryGenerators; + } + else + { + generators = new LinkedList(); + if (_subGenerator != null) + { + generators.AddLast(_subGenerator); + } + } + foreach (NestedContext context in contexts) + { + foreach (IGenerator generator in generators) + { + generator.GetText(context.KeyScope ?? scope, context.Writer ?? writer); + if (context.WriterNeedsConsidated) + { + writer.Write(_definition.ConsolidateWriter(context.Writer ?? writer, arguments)); + } + } + } + } + } } \ No newline at end of file diff --git a/mustache-sharp/ConditionTagDefinition.cs b/mustache-sharp/ConditionTagDefinition.cs index 8eae974..6e08a54 100644 --- a/mustache-sharp/ConditionTagDefinition.cs +++ b/mustache-sharp/ConditionTagDefinition.cs @@ -1,92 +1,101 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; - -namespace mustache -{ - /// - /// Defines a tag that conditionally prints its content. - /// - internal abstract class ConditionTagDefinition : ContentTagDefinition - { - private const string conditionParameter = "condition"; - - /// - /// Initializes a new instance of a ConditionTagDefinition. - /// - /// The name of the tag. - protected ConditionTagDefinition(string tagName) - : base(tagName, true) - { - } - - /// - /// Gets the parameters that can be passed to the tag. - /// - /// The parameters. - protected override IEnumerable GetParameters() - { - return new TagParameter[] { new TagParameter(conditionParameter) { IsRequired = true } }; - } - - /// - /// Gets the tags that come into scope within the context of the current tag. - /// - /// The child tag definitions. - protected override IEnumerable GetChildTags() - { - return new string[] { "elif", "else" }; - } - - /// - /// Gets whether the given tag's generator should be used for a secondary (or substitute) text block. - /// - /// The tag to inspect. - /// True if the tag's generator should be used as a secondary generator. - public override bool ShouldCreateSecondaryGroup(TagDefinition definition) - { - return new string[] { "elif", "else" }.Contains(definition.Name); - } - - /// - /// Gets whether the primary generator group should be used to render the tag. - /// - /// The arguments passed to the tag. - /// - /// True if the primary generator group should be used to render the tag; - /// otherwise, false to use the secondary group. - /// - public override bool ShouldGeneratePrimaryGroup(Dictionary arguments) - { - object condition = arguments[conditionParameter]; - return isConditionSatisfied(condition); - } - - private bool isConditionSatisfied(object condition) - { - if (condition == null) - { - return false; - } - IEnumerable enumerable = condition as IEnumerable; - if (enumerable != null) - { - return enumerable.Cast().Any(); - } - if (condition is Char) - { - return (Char)condition != '\0'; - } - try - { - decimal number = (decimal)Convert.ChangeType(condition, typeof(decimal)); - return number != 0.0m; - } - catch - { - return true; - } - } - } -} +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace mustache +{ + /// + /// Defines a tag that conditionally prints its content. + /// + internal abstract class ConditionTagDefinition : ContentTagDefinition + { + private const string conditionParameter = "condition"; + + /// + /// Initializes a new instance of a ConditionTagDefinition. + /// + /// The name of the tag. + protected ConditionTagDefinition(string tagName) + : base(tagName, true) + { + } + + /// + /// Gets the parameters that can be passed to the tag. + /// + /// The parameters. + protected override IEnumerable GetParameters() + { + return new TagParameter[] { new TagParameter(conditionParameter) { IsRequired = true } }; + } + + /// + /// Gets the tags that come into scope within the context of the current tag. + /// + /// The child tag definitions. + protected override IEnumerable GetChildTags() + { + return new string[] { "elif", "else" }; + } + + /// + /// Gets whether the given tag's generator should be used for a secondary (or substitute) text block. + /// + /// The tag to inspect. + /// True if the tag's generator should be used as a secondary generator. + public override bool ShouldCreateSecondaryGroup(TagDefinition definition) + { + return new string[] { "elif", "else" }.Contains(definition.Name); + } + + /// + /// Gets whether the primary generator group should be used to render the tag. + /// + /// The arguments passed to the tag. + /// + /// True if the primary generator group should be used to render the tag; + /// otherwise, false to use the secondary group. + /// + public override bool ShouldGeneratePrimaryGroup(Dictionary arguments) + { + object condition = arguments[conditionParameter]; + return isConditionSatisfied(condition); + } + + private bool isConditionSatisfied(object condition) + { + if (condition == null) + { + return false; + } + IEnumerable enumerable = condition as IEnumerable; + if (enumerable != null) + { + return enumerable.Cast().Any(); + } + if (condition is Char) + { + return (Char)condition != '\0'; + } + try + { + decimal number = (decimal)Convert.ChangeType(condition, typeof(decimal)); + return number != 0.0m; + } + catch + { + return true; + } + } + + /// + /// Gets the parameter that is used to create a new child context. + /// + /// The parameter that is used to create a new child context. + public override TagParameter GetChildContextParameter() + { + return null; + } + } +} diff --git a/mustache-sharp/Context.cs b/mustache-sharp/Context.cs new file mode 100644 index 0000000..b9d97b5 --- /dev/null +++ b/mustache-sharp/Context.cs @@ -0,0 +1,31 @@ +using System; + +namespace mustache +{ + /// + /// Represents a context within a template. + /// + public sealed class Context + { + /// + /// Initializes a new instance of a Context. + /// + /// The definition of tag that created the context. + /// The argument used to create the context. + internal Context(TagDefinition definition, string argument) + { + Tag = definition; + Argument = argument; + } + + /// + /// Gets the tag that created the context. + /// + public TagDefinition Tag { get; private set; } + + /// + /// Gets the argument used to create the context. + /// + public string Argument { get; private set; } + } +} diff --git a/mustache-sharp/EachTagDefinition.cs b/mustache-sharp/EachTagDefinition.cs index 983593d..05c512d 100644 --- a/mustache-sharp/EachTagDefinition.cs +++ b/mustache-sharp/EachTagDefinition.cs @@ -1,71 +1,81 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.IO; - -namespace mustache -{ - /// - /// Defines a tag that can iterate over a collection of items and render - /// the content using each item as the context. - /// - internal sealed class EachTagDefinition : ContentTagDefinition - { - private const string collectionParameter = "collection"; - - /// - /// Initializes a new instance of an EachTagDefinition. - /// - public EachTagDefinition() - : base("each", true) - { - } - - /// - /// Gets whether the tag only exists within the scope of its parent. - /// - protected override bool GetIsContextSensitive() - { - return false; - } - - /// - /// Gets the parameters that can be passed to the tag. - /// - /// The parameters. - protected override IEnumerable GetParameters() - { - return new TagParameter[] { new TagParameter(collectionParameter) { IsRequired = true } }; - } - - /// - /// Gets the context to use when building the inner text of the tag. - /// - /// The text writer passed - /// 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) - { - object value = arguments[collectionParameter]; - IEnumerable enumerable = value as IEnumerable; - if (enumerable == null) - { - yield break; - } - foreach (object item in enumerable) - { - yield return new NestedContext() { KeyScope = scope.CreateChildScope(item), Writer = writer }; - } - } - - /// - /// Gets the tags that are in scope under this tag. - /// - /// The name of the tags that are in scope. - protected override IEnumerable GetChildTags() - { - return new string[] { }; - } - } -} +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; + +namespace mustache +{ + /// + /// Defines a tag that can iterate over a collection of items and render + /// the content using each item as the context. + /// + internal sealed class EachTagDefinition : ContentTagDefinition + { + private const string collectionParameter = "collection"; + private static readonly TagParameter collection = new TagParameter(collectionParameter) { IsRequired = true }; + + /// + /// Initializes a new instance of an EachTagDefinition. + /// + public EachTagDefinition() + : base("each", true) + { + } + + /// + /// Gets whether the tag only exists within the scope of its parent. + /// + protected override bool GetIsContextSensitive() + { + return false; + } + + /// + /// Gets the parameters that can be passed to the tag. + /// + /// The parameters. + protected override IEnumerable GetParameters() + { + return new TagParameter[] { collection }; + } + + /// + /// Gets the context to use when building the inner text of the tag. + /// + /// The text writer passed + /// 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) + { + object value = arguments[collectionParameter]; + IEnumerable enumerable = value as IEnumerable; + if (enumerable == null) + { + yield break; + } + foreach (object item in enumerable) + { + yield return new NestedContext() { KeyScope = scope.CreateChildScope(item), Writer = writer }; + } + } + + /// + /// Gets the tags that are in scope under this tag. + /// + /// The name of the tags that are in scope. + protected override IEnumerable GetChildTags() + { + return new string[] { }; + } + + /// + /// Gets the parameter that is used to create a new child context. + /// + /// The parameter that is used to create a new child context. + public override TagParameter GetChildContextParameter() + { + return collection; + } + } +} diff --git a/mustache-sharp/ElseTagDefinition.cs b/mustache-sharp/ElseTagDefinition.cs index f3b1d1e..35a0868 100644 --- a/mustache-sharp/ElseTagDefinition.cs +++ b/mustache-sharp/ElseTagDefinition.cs @@ -1,35 +1,44 @@ -using System; -using System.Collections.Generic; - -namespace mustache -{ - /// - /// Defines a tag that renders its content if all preceding if and elif tags. - /// - internal sealed class ElseTagDefinition : ContentTagDefinition - { - /// - /// Initializes a new instance of a ElseTagDefinition. - /// - public ElseTagDefinition() - : base("else", true) - { - } - - /// - /// Gets whether the tag only exists within the scope of its parent. - /// - protected override bool GetIsContextSensitive() - { - return true; - } - - /// - /// Gets the tags that indicate the end of the current tag's content. - /// - protected override IEnumerable GetClosingTags() - { - return new string[] { "if" }; - } - } -} +using System; +using System.Collections.Generic; + +namespace mustache +{ + /// + /// Defines a tag that renders its content if all preceding if and elif tags. + /// + internal sealed class ElseTagDefinition : ContentTagDefinition + { + /// + /// Initializes a new instance of a ElseTagDefinition. + /// + public ElseTagDefinition() + : base("else", true) + { + } + + /// + /// Gets whether the tag only exists within the scope of its parent. + /// + protected override bool GetIsContextSensitive() + { + return true; + } + + /// + /// Gets the tags that indicate the end of the current tag's content. + /// + protected override IEnumerable GetClosingTags() + { + return new string[] { "if" }; + } + + /// + /// Gets the parameter that is used to create a new child context. + /// + /// The parameter that is used to create a new child context. + public override TagParameter GetChildContextParameter() + { + return null; + } + } +} diff --git a/mustache-sharp/FormatCompiler.cs b/mustache-sharp/FormatCompiler.cs index 2f483e1..4910c00 100644 --- a/mustache-sharp/FormatCompiler.cs +++ b/mustache-sharp/FormatCompiler.cs @@ -74,7 +74,8 @@ namespace mustache } CompoundGenerator generator = new CompoundGenerator(_masterDefinition, new ArgumentCollection()); Trimmer trimmer = new Trimmer(); - int formatIndex = buildCompoundGenerator(_masterDefinition, generator, trimmer, format, 0); + List context = new List() { new Context(_masterDefinition, "this") }; + int formatIndex = buildCompoundGenerator(_masterDefinition, context, generator, trimmer, format, 0); string trailing = format.Substring(formatIndex); generator.AddStaticGenerators(trimmer.RecordText(trailing, false, false)); trimmer.Trim(); @@ -165,7 +166,8 @@ namespace mustache } private int buildCompoundGenerator( - TagDefinition tagDefinition, + TagDefinition tagDefinition, + List context, CompoundGenerator generator, Trimmer trimmer, string format, int formatIndex) @@ -193,7 +195,7 @@ namespace mustache string key = match.Groups["key"].Value; string alignment = match.Groups["alignment"].Value; string formatting = match.Groups["format"].Value; - PlaceholderFoundEventArgs args = new PlaceholderFoundEventArgs(key, alignment, formatting); + PlaceholderFoundEventArgs args = new PlaceholderFoundEventArgs(key, alignment, formatting, context.ToArray()); if (PlaceholderFound != null) { PlaceholderFound(this, args); @@ -216,7 +218,12 @@ namespace mustache generator.AddStaticGenerators(trimmer.RecordText(leading, true, false)); ArgumentCollection arguments = getArguments(nextDefinition, match); CompoundGenerator compoundGenerator = new CompoundGenerator(nextDefinition, arguments); - formatIndex = buildCompoundGenerator(nextDefinition, compoundGenerator, trimmer, format, formatIndex); + string newContext = compoundGenerator.GetContextArgument(); + if (newContext != null) + { + context.Add(new Context(nextDefinition, newContext)); + } + formatIndex = buildCompoundGenerator(nextDefinition, context, compoundGenerator, trimmer, format, formatIndex); generator.AddGenerator(nextDefinition, compoundGenerator); } else diff --git a/mustache-sharp/Generator.cs b/mustache-sharp/Generator.cs index 0d92685..f182251 100644 --- a/mustache-sharp/Generator.cs +++ b/mustache-sharp/Generator.cs @@ -11,7 +11,8 @@ namespace mustache public sealed class Generator { private readonly IGenerator _generator; - private readonly List> _handlers; + private readonly List> _foundHandlers; + private readonly List> _notFoundHandlers; /// /// Initializes a new instance of a Generator. @@ -20,16 +21,26 @@ namespace mustache internal Generator(IGenerator generator) { _generator = generator; - _handlers = new List>(); + _foundHandlers = new List>(); + _notFoundHandlers = new List>(); + } + + /// + /// Occurs when a key/property is found. + /// + public event EventHandler KeyFound + { + add { _foundHandlers.Add(value); } + remove { _foundHandlers.Remove(value); } } /// /// Occurs when a key/property is not found in the object graph. /// - public event EventHandler KeyNotFound + public event EventHandler KeyNotFound { - add { _handlers.Add(value); } - remove { _handlers.Remove(value); } + add { _notFoundHandlers.Add(value); } + remove { _notFoundHandlers.Remove(value); } } /// @@ -60,7 +71,11 @@ namespace mustache private string render(IFormatProvider provider, object source) { KeyScope scope = new KeyScope(source); - foreach (EventHandler handler in _handlers) + foreach (EventHandler handler in _foundHandlers) + { + scope.KeyFound += handler; + } + foreach (EventHandler handler in _notFoundHandlers) { scope.KeyNotFound += handler; } diff --git a/mustache-sharp/InlineTagDefinition.cs b/mustache-sharp/InlineTagDefinition.cs index e087e64..264e35e 100644 --- a/mustache-sharp/InlineTagDefinition.cs +++ b/mustache-sharp/InlineTagDefinition.cs @@ -1,40 +1,49 @@ -using System; -using System.Collections.Generic; -using System.IO; - -namespace mustache -{ - /// - /// Defines a tag that cannot contain inner text. - /// - public abstract class InlineTagDefinition : TagDefinition - { - /// - /// Initializes a new instance of an InlineTagDefinition. - /// - /// The name of the tag being defined. - protected InlineTagDefinition(string tagName) - : base(tagName) - { - } - - /// - /// Initializes a new instance of an InlineTagDefinition. - /// - /// The name of the tag being defined. - /// Specifies whether the tag is a built-in tag. - internal InlineTagDefinition(string tagName, bool isBuiltin) - : base(tagName, isBuiltin) - { - } - - /// - /// Gets or sets whether the tag can have content. - /// - /// True if the tag can have a body; otherwise, false. - protected override bool GetHasContent() - { - return false; - } - } -} +using System; +using System.Collections.Generic; +using System.IO; + +namespace mustache +{ + /// + /// Defines a tag that cannot contain inner text. + /// + public abstract class InlineTagDefinition : TagDefinition + { + /// + /// Initializes a new instance of an InlineTagDefinition. + /// + /// The name of the tag being defined. + protected InlineTagDefinition(string tagName) + : base(tagName) + { + } + + /// + /// Initializes a new instance of an InlineTagDefinition. + /// + /// The name of the tag being defined. + /// Specifies whether the tag is a built-in tag. + internal InlineTagDefinition(string tagName, bool isBuiltin) + : base(tagName, isBuiltin) + { + } + + /// + /// Gets or sets whether the tag can have content. + /// + /// True if the tag can have a body; otherwise, false. + protected override bool GetHasContent() + { + return false; + } + + /// + /// Gets the parameter that is used to create a child context. + /// + /// The parameter that is used to create a child context. + public override TagParameter GetChildContextParameter() + { + return null; + } + } +} diff --git a/mustache-sharp/KeyFoundEventArgs.cs b/mustache-sharp/KeyFoundEventArgs.cs new file mode 100644 index 0000000..e3c2251 --- /dev/null +++ b/mustache-sharp/KeyFoundEventArgs.cs @@ -0,0 +1,29 @@ +using System; + +namespace mustache +{ + /// + /// Holds the information about a key that was found. + /// + public class KeyFoundEventArgs : EventArgs + { + /// + /// Initializes a new instance of a KeyFoundEventArgs. + /// + /// The fully-qualified key. + internal KeyFoundEventArgs(string key, object value) + { + Key = key; + } + + /// + /// Gets the fully-qualified key. + /// + public string Key { get; private set; } + + /// + /// Gets or sets the object to use as the substitute. + /// + public object Substitute { get; set; } + } +} diff --git a/mustache-sharp/MissingKeyEventArgs.cs b/mustache-sharp/KeyNotFoundEventArgs.cs similarity index 84% rename from mustache-sharp/MissingKeyEventArgs.cs rename to mustache-sharp/KeyNotFoundEventArgs.cs index f2f3fd4..2a54208 100644 --- a/mustache-sharp/MissingKeyEventArgs.cs +++ b/mustache-sharp/KeyNotFoundEventArgs.cs @@ -5,14 +5,14 @@ namespace mustache /// /// Holds the information needed to handle a missing key. /// - public class MissingKeyEventArgs : EventArgs + public class KeyNotFoundEventArgs : EventArgs { /// - /// Initializes a new instance of a MissingKeyEventArgs. + /// Initializes a new instance of a KeyNotFoundEventArgs. /// /// The fully-qualified key. /// The part of the key that could not be found. - internal MissingKeyEventArgs(string key, string missingMember) + internal KeyNotFoundEventArgs(string key, string missingMember) { Key = key; MissingMember = missingMember; diff --git a/mustache-sharp/KeyScope.cs b/mustache-sharp/KeyScope.cs index ec893c7..9f5bf02 100644 --- a/mustache-sharp/KeyScope.cs +++ b/mustache-sharp/KeyScope.cs @@ -34,10 +34,15 @@ namespace mustache _source = source; } + /// + /// Occurs when a key/property is found in the object graph. + /// + public event EventHandler KeyFound; + /// /// Occurs when a key/property is not found in the object graph. /// - public event EventHandler KeyNotFound; + public event EventHandler KeyNotFound; /// /// Creates a child scope that searches for keys in the given object. @@ -47,6 +52,7 @@ namespace mustache public KeyScope CreateChildScope(object source) { KeyScope scope = new KeyScope(source, this); + scope.KeyFound = KeyFound; scope.KeyNotFound = KeyNotFound; return scope; } @@ -75,6 +81,12 @@ namespace mustache nextLevel = handleKeyNotFound(name, member); } } + if (KeyFound != null) + { + KeyFoundEventArgs args = new KeyFoundEventArgs(name, nextLevel); + KeyFound(this, args); + nextLevel = args.Substitute; + } return nextLevel; } @@ -94,7 +106,7 @@ namespace mustache private object handleKeyNotFound(string fullName, string memberName) { - MissingKeyEventArgs args = new MissingKeyEventArgs(fullName, memberName); + KeyNotFoundEventArgs args = new KeyNotFoundEventArgs(fullName, memberName); if (KeyNotFound != null) { KeyNotFound(this, args); diff --git a/mustache-sharp/MasterTagDefinition.cs b/mustache-sharp/MasterTagDefinition.cs index cd45f00..e55146e 100644 --- a/mustache-sharp/MasterTagDefinition.cs +++ b/mustache-sharp/MasterTagDefinition.cs @@ -1,36 +1,45 @@ -using System; -using System.Collections.Generic; - -namespace mustache -{ - /// - /// Defines a pseudo tag that wraps the entire content of a format string. - /// - internal sealed class MasterTagDefinition : ContentTagDefinition - { - /// - /// Initializes a new instance of a MasterTagDefinition. - /// - public MasterTagDefinition() - : base(String.Empty, true) - { - } - - /// - /// Gets whether the tag only exists within the scope of its parent. - /// - protected override bool GetIsContextSensitive() - { - return true; - } - - /// - /// Gets the name of the tags that indicate that the tag's context is closed. - /// - /// The tag names. - protected override IEnumerable GetClosingTags() - { - return new string[] { }; - } - } -} +using System; +using System.Collections.Generic; + +namespace mustache +{ + /// + /// Defines a pseudo tag that wraps the entire content of a format string. + /// + internal sealed class MasterTagDefinition : ContentTagDefinition + { + /// + /// Initializes a new instance of a MasterTagDefinition. + /// + public MasterTagDefinition() + : base(String.Empty, true) + { + } + + /// + /// Gets whether the tag only exists within the scope of its parent. + /// + protected override bool GetIsContextSensitive() + { + return true; + } + + /// + /// Gets the name of the tags that indicate that the tag's context is closed. + /// + /// The tag names. + protected override IEnumerable GetClosingTags() + { + return new string[] { }; + } + + /// + /// Gets the parameter that is used to create a new child context. + /// + /// The parameter that is used to create a new child context. + public override TagParameter GetChildContextParameter() + { + return null; + } + } +} diff --git a/mustache-sharp/PlaceholderFoundEventArgs.cs b/mustache-sharp/PlaceholderFoundEventArgs.cs index 8849994..9f55b04 100644 --- a/mustache-sharp/PlaceholderFoundEventArgs.cs +++ b/mustache-sharp/PlaceholderFoundEventArgs.cs @@ -14,11 +14,13 @@ namespace mustache /// The key that was found. /// The alignment that will be applied to the substitute value. /// The formatting that will be applied to the substitute value. - internal PlaceholderFoundEventArgs(string key, string alignment, string formatting) + /// The context where the placeholder was found. + internal PlaceholderFoundEventArgs(string key, string alignment, string formatting, Context[] context) { Key = key; Alignment = alignment; Formatting = formatting; + Context = context; } /// @@ -35,5 +37,10 @@ namespace mustache /// Gets or sets the formatting that will be applied to the substitute value. /// public string Formatting { get; set; } + + /// + /// Gets the context where the placeholder was found. + /// + public Context[] Context { get; private set; } } } diff --git a/mustache-sharp/Properties/AssemblyInfo.cs b/mustache-sharp/Properties/AssemblyInfo.cs index d72b0f3..1103a73 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.0.6.0")] -[assembly: AssemblyFileVersion("0.0.6.0")] +[assembly: AssemblyVersion("0.0.7.0")] +[assembly: AssemblyFileVersion("0.0.7.0")] [assembly: InternalsVisibleTo("mustache-sharp.test")] \ No newline at end of file diff --git a/mustache-sharp/TagDefinition.cs b/mustache-sharp/TagDefinition.cs index 9566afd..89f56a0 100644 --- a/mustache-sharp/TagDefinition.cs +++ b/mustache-sharp/TagDefinition.cs @@ -1,184 +1,190 @@ -using System; -using System.Collections.Generic; -using mustache.Properties; -using System.IO; - -namespace mustache -{ - /// - /// Defines the attributes of a custom tag. - /// - public abstract class TagDefinition - { - private readonly string _tagName; - - /// - /// Initializes a new instance of a TagDefinition. - /// - /// The name of the tag. - /// The name of the tag is null or blank. - protected TagDefinition(string tagName) - : this(tagName, false) - { - } - - /// - /// Initializes a new instance of a TagDefinition. - /// - /// The name of the tag. - /// Specifies whether the tag is built-in or not. Checks are not performed on the names of built-in tags. - internal TagDefinition(string tagName, bool isBuiltIn) - { - if (!isBuiltIn && !RegexHelper.IsValidIdentifier(tagName)) - { - throw new ArgumentException(Resources.BlankTagName, "tagName"); - } - _tagName = tagName; - } - - /// - /// Gets the name of the tag. - /// - public string Name - { - get { return _tagName; } - } - - /// - /// Gets whether the tag is limited to the parent tag's context. - /// - internal bool IsContextSensitive - { - get { return GetIsContextSensitive(); } - } - - /// - /// Gets whether a tag is limited to the parent tag's context. - /// - protected virtual bool GetIsContextSensitive() - { - return false; - } - - /// - /// Gets the parameters that are defined for the tag. - /// - internal IEnumerable Parameters - { - get { return GetParameters(); } - } - - /// - /// Specifies which parameters are passed to the tag. - /// - /// The tag parameters. - protected virtual IEnumerable GetParameters() - { - return new TagParameter[] { }; - } - - /// - /// Gets whether the tag contains content. - /// - internal bool HasContent - { - get { return GetHasContent(); } - } - - /// - /// Gets whether tag has content. - /// - /// True if the tag has content; otherwise, false. - protected abstract bool GetHasContent(); - - /// - /// Gets the tags that can indicate that the tag has closed. - /// This field is only used if no closing tag is expected. - /// - internal IEnumerable ClosingTags - { - get { return GetClosingTags(); } - } - - protected virtual IEnumerable GetClosingTags() - { - if (HasContent) - { - return new string[] { Name }; - } - else - { - return new string[] { }; - } - } - - /// - /// Gets the tags that are in scope within the current tag. - /// - internal IEnumerable ChildTags - { - get { return GetChildTags(); } - } - - /// - /// Specifies which tags are scoped under the current tag. - /// - /// The child tag definitions. - protected virtual IEnumerable GetChildTags() - { - return new string[] { }; - } - - /// - /// Gets the context to use when building the inner text of the tag. - /// - /// The text writer passed - /// The current 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) - { - yield return new NestedContext() { KeyScope = scope, Writer = writer }; - } - - /// - /// Applies additional formatting to the inner text of the tag. - /// - /// The text writer to write to. - /// The arguments passed to the tag. - public virtual void GetText(TextWriter writer, Dictionary arguments) - { - } - - /// - /// Consolidates the text in the given writer to a string, using the given arguments as necessary. - /// - /// The writer containing the text to consolidate. - /// The arguments passed to the tag. - /// The consolidated string. - public virtual string ConsolidateWriter(TextWriter writer, Dictionary arguments) - { - return writer.ToString(); - } - - /// - /// Requests which generator group to associate the given tag type. - /// - /// The child tag definition being grouped. - /// The name of the group to associate the given tag with. - public virtual bool ShouldCreateSecondaryGroup(TagDefinition definition) - { - return false; - } - - /// - /// Gets whether the group with the given name should have text generated for them. - /// - /// The arguments passed to the tag. - /// True if text should be generated for the group; otherwise, false. - public virtual bool ShouldGeneratePrimaryGroup(Dictionary arguments) - { - return true; - } - } -} +using System; +using System.Collections.Generic; +using mustache.Properties; +using System.IO; + +namespace mustache +{ + /// + /// Defines the attributes of a custom tag. + /// + public abstract class TagDefinition + { + private readonly string _tagName; + + /// + /// Initializes a new instance of a TagDefinition. + /// + /// The name of the tag. + /// The name of the tag is null or blank. + protected TagDefinition(string tagName) + : this(tagName, false) + { + } + + /// + /// Initializes a new instance of a TagDefinition. + /// + /// The name of the tag. + /// Specifies whether the tag is built-in or not. Checks are not performed on the names of built-in tags. + internal TagDefinition(string tagName, bool isBuiltIn) + { + if (!isBuiltIn && !RegexHelper.IsValidIdentifier(tagName)) + { + throw new ArgumentException(Resources.BlankTagName, "tagName"); + } + _tagName = tagName; + } + + /// + /// Gets the name of the tag. + /// + public string Name + { + get { return _tagName; } + } + + /// + /// Gets whether the tag is limited to the parent tag's context. + /// + internal bool IsContextSensitive + { + get { return GetIsContextSensitive(); } + } + + /// + /// Gets whether a tag is limited to the parent tag's context. + /// + protected virtual bool GetIsContextSensitive() + { + return false; + } + + /// + /// Gets the parameters that are defined for the tag. + /// + internal IEnumerable Parameters + { + get { return GetParameters(); } + } + + /// + /// Specifies which parameters are passed to the tag. + /// + /// The tag parameters. + protected virtual IEnumerable GetParameters() + { + return new TagParameter[] { }; + } + + /// + /// Gets whether the tag contains content. + /// + internal bool HasContent + { + get { return GetHasContent(); } + } + + /// + /// Gets whether tag has content. + /// + /// True if the tag has content; otherwise, false. + protected abstract bool GetHasContent(); + + /// + /// Gets the tags that can indicate that the tag has closed. + /// This field is only used if no closing tag is expected. + /// + internal IEnumerable ClosingTags + { + get { return GetClosingTags(); } + } + + protected virtual IEnumerable GetClosingTags() + { + if (HasContent) + { + return new string[] { Name }; + } + else + { + return new string[] { }; + } + } + + /// + /// Gets the tags that are in scope within the current tag. + /// + internal IEnumerable ChildTags + { + get { return GetChildTags(); } + } + + /// + /// Specifies which tags are scoped under the current tag. + /// + /// The child tag definitions. + protected virtual IEnumerable GetChildTags() + { + return new string[] { }; + } + + /// + /// Gets the parameter that will be used to create a new child scope. + /// + /// The parameter that will be used to create a new child scope -or- null if no new scope is created. + public abstract TagParameter GetChildContextParameter(); + + /// + /// Gets the context to use when building the inner text of the tag. + /// + /// The text writer passed + /// The current 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) + { + yield return new NestedContext() { KeyScope = scope, Writer = writer }; + } + + /// + /// Applies additional formatting to the inner text of the tag. + /// + /// The text writer to write to. + /// The arguments passed to the tag. + public virtual void GetText(TextWriter writer, Dictionary arguments) + { + } + + /// + /// Consolidates the text in the given writer to a string, using the given arguments as necessary. + /// + /// The writer containing the text to consolidate. + /// The arguments passed to the tag. + /// The consolidated string. + public virtual string ConsolidateWriter(TextWriter writer, Dictionary arguments) + { + return writer.ToString(); + } + + /// + /// Requests which generator group to associate the given tag type. + /// + /// The child tag definition being grouped. + /// The name of the group to associate the given tag with. + public virtual bool ShouldCreateSecondaryGroup(TagDefinition definition) + { + return false; + } + + /// + /// Gets whether the group with the given name should have text generated for them. + /// + /// The arguments passed to the tag. + /// True if text should be generated for the group; otherwise, false. + public virtual bool ShouldGeneratePrimaryGroup(Dictionary arguments) + { + return true; + } + } +} diff --git a/mustache-sharp/WithGenerator.cs b/mustache-sharp/WithGenerator.cs index 8fa3a67..6525f57 100644 --- a/mustache-sharp/WithGenerator.cs +++ b/mustache-sharp/WithGenerator.cs @@ -1,52 +1,62 @@ -using System; -using System.Collections.Generic; -using System.IO; - -namespace mustache -{ - /// - /// Defines a tag that changes the scope to the object passed as an argument. - /// - internal sealed class WithTagDefinition : ContentTagDefinition - { - private const string contextParameter = "context"; - - /// - /// Initializes a new instance of a WithTagDefinition. - /// - public WithTagDefinition() - : base("with", true) - { - } - - /// - /// Gets whether the tag only exists within the scope of its parent. - /// - protected override bool GetIsContextSensitive() - { - return false; - } - - /// - /// Gets the parameters that can be passed to the tag. - /// - /// The parameters. - protected override IEnumerable GetParameters() - { - return new TagParameter[] { new TagParameter(contextParameter) { IsRequired = true } }; - } - - /// - /// Gets the context to use when building the inner text of the tag. - /// - /// The text writer passed - /// 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) - { - object context = arguments[contextParameter]; - yield return new NestedContext() { KeyScope = scope.CreateChildScope(context), Writer = writer }; - } - } -} +using System; +using System.Collections.Generic; +using System.IO; + +namespace mustache +{ + /// + /// Defines a tag that changes the scope to the object passed as an argument. + /// + internal sealed class WithTagDefinition : ContentTagDefinition + { + private const string contextParameter = "context"; + private static readonly TagParameter context = new TagParameter(contextParameter) { IsRequired = true }; + + /// + /// Initializes a new instance of a WithTagDefinition. + /// + public WithTagDefinition() + : base("with", true) + { + } + + /// + /// Gets whether the tag only exists within the scope of its parent. + /// + protected override bool GetIsContextSensitive() + { + return false; + } + + /// + /// Gets the parameters that can be passed to the tag. + /// + /// The parameters. + protected override IEnumerable GetParameters() + { + return new TagParameter[] { context }; + } + + /// + /// Gets the parameter that is used to create a new child context. + /// + /// The parameter that is used to create a new child context. + public override TagParameter GetChildContextParameter() + { + return context; + } + + /// + /// Gets the context to use when building the inner text of the tag. + /// + /// The text writer passed + /// 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) + { + object context = arguments[contextParameter]; + yield return new NestedContext() { KeyScope = scope.CreateChildScope(context), Writer = writer }; + } + } +} diff --git a/mustache-sharp/mustache-sharp.csproj b/mustache-sharp/mustache-sharp.csproj index 97b2235..fefc132 100644 --- a/mustache-sharp/mustache-sharp.csproj +++ b/mustache-sharp/mustache-sharp.csproj @@ -38,6 +38,8 @@ + + @@ -50,7 +52,7 @@ - +