From eb5a84be9a4257d42816fa2016567ad56ffd630b Mon Sep 17 00:00:00 2001 From: Oleh Formaniuk Date: Sun, 12 Jan 2020 06:24:25 -0800 Subject: [PATCH 1/2] Support for BlockParams --- .../Handlebars.Test/BasicIntegrationTests.cs | 66 +++- source/Handlebars.Test/HelperTests.cs | 27 ++ source/Handlebars/BuiltinHelpers.cs | 8 +- .../Handlebars/Compiler/ExpressionBuilder.cs | 1 + .../BlockHelperAccumulatorContext.cs | 3 +- .../IteratorBlockAccumulatorContext.cs | 7 +- .../Lexer/Converter/BlockParamsConverter.cs | 44 +++ .../Lexer/Parsers/BlockParamsParser.cs | 39 +++ source/Handlebars/Compiler/Lexer/Tokenizer.cs | 50 +-- .../Lexer/Tokens/BlockParameterToken.cs | 19 + .../Handlebars/Compiler/Lexer/Tokens/Token.cs | 5 + .../Compiler/Lexer/Tokens/TokenType.cs | 3 +- .../Compiler/Structure/BindingContext.cs | 109 +++--- .../Structure/BindingContextValueProvider.cs | 39 +++ .../Structure/BlockHelperExpression.cs | 21 ++ .../Structure/BlockParamsExpression.cs | 43 +++ .../Structure/BlockParamsValueProvider.cs | 69 ++++ .../Structure/HandlebarsExpression.cs | 17 +- .../Compiler/Structure/IValueProvider.cs | 8 + .../Compiler/Structure/IteratorExpression.cs | 11 +- .../Expression/BlockHelperFunctionBinder.cs | 13 +- .../Expression/HandlebarsExpressionVisitor.cs | 116 +++---- .../Translation/Expression/IteratorBinder.cs | 327 ++++++++++-------- .../Translation/Expression/PartialBinder.cs | 36 +- .../Translation/Expression/PathBinder.cs | 8 +- source/Handlebars/Handlebars.csproj | 2 +- source/Handlebars/HelperOptions.cs | 25 +- 27 files changed, 770 insertions(+), 346 deletions(-) create mode 100644 source/Handlebars/Compiler/Lexer/Converter/BlockParamsConverter.cs create mode 100644 source/Handlebars/Compiler/Lexer/Parsers/BlockParamsParser.cs create mode 100644 source/Handlebars/Compiler/Lexer/Tokens/BlockParameterToken.cs create mode 100644 source/Handlebars/Compiler/Structure/BindingContextValueProvider.cs create mode 100644 source/Handlebars/Compiler/Structure/BlockParamsExpression.cs create mode 100644 source/Handlebars/Compiler/Structure/BlockParamsValueProvider.cs create mode 100644 source/Handlebars/Compiler/Structure/IValueProvider.cs diff --git a/source/Handlebars.Test/BasicIntegrationTests.cs b/source/Handlebars.Test/BasicIntegrationTests.cs index 42442cd4..85a4e632 100644 --- a/source/Handlebars.Test/BasicIntegrationTests.cs +++ b/source/Handlebars.Test/BasicIntegrationTests.cs @@ -144,18 +144,9 @@ public void AssertHandlebarsUndefinedBindingException() } }; - try - { - template(data); - } - catch (HandlebarsUndefinedBindingException ex) - { - Assert.Equal("person.lastname", ex.Path); - Assert.Equal("lastname", ex.MissingKey); - return; - } - - Assert.False(true, "Exception is expected."); + var exception = Assert.Throws(() => template(data)); + Assert.Equal("person.lastname", exception.Path); + Assert.Equal("lastname", exception.MissingKey); } [Fact] @@ -365,6 +356,23 @@ public void BasicWith() var result = template(data); Assert.Equal("Hello, my good friend Erik!", result); } + + [Fact] + public void WithWithBlockParams() + { + var source = "{{#with person as |person|}}{{person.name}} is {{age}} years old{{/with}}."; + var template = Handlebars.Compile(source); + var data = new + { + person = new + { + name = "Erik", + age = 42 + } + }; + var result = template(data); + Assert.Equal("Erik is 42 years old.", result); + } [Fact] public void BasicWithInversion() @@ -466,6 +474,23 @@ public void BasicObjectEnumeratorWithKey() var result = template(data); Assert.Equal("foo: hello bar: world ", result); } + + [Fact] + public void ObjectEnumeratorWithBlockParams() + { + var source = "{{#each enumerateMe as |item val|}}{{@item}}: {{@val}} {{/each}}"; + var template = Handlebars.Compile(source); + var data = new + { + enumerateMe = new + { + foo = "hello", + bar = "world" + } + }; + var result = template(data); + Assert.Equal("hello: foo world: bar ", result); + } [Fact] public void BasicDictionaryEnumerator() @@ -484,6 +509,23 @@ public void BasicDictionaryEnumerator() Assert.Equal("hello world ", result); } + [Fact] + public void DictionaryEnumeratorWithBlockParams() + { + var source = "{{#each enumerateMe as |item val|}}{{item}} {{val}} {{/each}}"; + var template = Handlebars.Compile(source); + var data = new + { + enumerateMe = new Dictionary + { + { "foo", "hello"}, + { "bar", "world"} + } + }; + var result = template(data); + Assert.Equal("hello foo world bar ", result); + } + [Fact] public void DictionaryWithLastEnumerator() { diff --git a/source/Handlebars.Test/HelperTests.cs b/source/Handlebars.Test/HelperTests.cs index 38c9dae3..3cab30f1 100644 --- a/source/Handlebars.Test/HelperTests.cs +++ b/source/Handlebars.Test/HelperTests.cs @@ -2,6 +2,8 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Linq; +using HandlebarsDotNet.Compiler; namespace HandlebarsDotNet.Test { @@ -28,6 +30,31 @@ public void HelperWithLiteralArguments() Assert.Equal(expected, output); } + + [Fact] + public void BlockHelperWithBlockParams() + { + Handlebars.RegisterHelper("myHelper", (writer, options, context, args) => { + var count = 0; + options.BlockParams((parameters, binder) => + binder(parameters.ElementAtOrDefault(0), () => ++count)); + + foreach(var arg in args) + { + options.Template(writer, arg); + } + }); + + var source = "Here are some things: {{#myHelper 'foo' 'bar' as |counter|}}{{counter}}:{{this}}\n{{/myHelper}}"; + + var template = Handlebars.Compile(source); + + var output = template(new { }); + + var expected = "Here are some things: 1:foo\n2:bar\n"; + + Assert.Equal(expected, output); + } [Fact] public void HelperWithLiteralArgumentsWithQuotes() diff --git a/source/Handlebars/BuiltinHelpers.cs b/source/Handlebars/BuiltinHelpers.cs index 046ab415..937a3708 100644 --- a/source/Handlebars/BuiltinHelpers.cs +++ b/source/Handlebars/BuiltinHelpers.cs @@ -1,12 +1,13 @@ -using System; +using System; using System.IO; using System.Reflection; using System.Collections.Generic; +using System.Linq; using HandlebarsDotNet.Compiler; namespace HandlebarsDotNet { - internal static class BuiltinHelpers + internal class BuiltinHelpers { [Description("with")] public static void With(TextWriter output, HelperOptions options, dynamic context, params object[] arguments) @@ -16,6 +17,9 @@ public static void With(TextWriter output, HelperOptions options, dynamic contex throw new HandlebarsException("{{with}} helper must have exactly one argument"); } + options.BlockParams((parameters, binder) => + binder(parameters.ElementAtOrDefault(0), () => arguments[0])); + if (HandlebarsUtils.IsTruthyOrNonEmpty(arguments[0])) { options.Template(output, arguments[0]); diff --git a/source/Handlebars/Compiler/ExpressionBuilder.cs b/source/Handlebars/Compiler/ExpressionBuilder.cs index 02f2dddd..732638b2 100644 --- a/source/Handlebars/Compiler/ExpressionBuilder.cs +++ b/source/Handlebars/Compiler/ExpressionBuilder.cs @@ -21,6 +21,7 @@ public IEnumerable ConvertTokensToExpressions(IEnumerable to tokens = LiteralConverter.Convert(tokens); tokens = HashParameterConverter.Convert(tokens); tokens = PathConverter.Convert(tokens); + tokens = BlockParamsConverter.Convert(tokens); tokens = SubExpressionConverter.Convert(tokens); tokens = HashParametersAccumulator.Accumulate(tokens); tokens = PartialConverter.Convert(tokens); diff --git a/source/Handlebars/Compiler/Lexer/Converter/BlockAccumulators/BlockHelperAccumulatorContext.cs b/source/Handlebars/Compiler/Lexer/Converter/BlockAccumulators/BlockHelperAccumulatorContext.cs index 4c16b38a..f6cb0f20 100644 --- a/source/Handlebars/Compiler/Lexer/Converter/BlockAccumulators/BlockHelperAccumulatorContext.cs +++ b/source/Handlebars/Compiler/Lexer/Converter/BlockAccumulators/BlockHelperAccumulatorContext.cs @@ -73,7 +73,8 @@ public override Expression GetAccumulatedBlock() var resultExpr = HandlebarsExpression.BlockHelper( _startingNode.HelperName, - _startingNode.Arguments, + _startingNode.Arguments.Where(o => o.NodeType != (ExpressionType)HandlebarsExpressionType.BlockParamsExpression), + _startingNode.Arguments.OfType().SingleOrDefault() ?? BlockParamsExpression.Empty(), _accumulatedBody, _accumulatedInversion, _startingNode.IsRaw); diff --git a/source/Handlebars/Compiler/Lexer/Converter/BlockAccumulators/IteratorBlockAccumulatorContext.cs b/source/Handlebars/Compiler/Lexer/Converter/BlockAccumulators/IteratorBlockAccumulatorContext.cs index b88f8ff8..89c57a23 100644 --- a/source/Handlebars/Compiler/Lexer/Converter/BlockAccumulators/IteratorBlockAccumulatorContext.cs +++ b/source/Handlebars/Compiler/Lexer/Converter/BlockAccumulators/IteratorBlockAccumulatorContext.cs @@ -25,7 +25,8 @@ public override void HandleElement(Expression item) if (IsElseBlock(item)) { _accumulatedExpression = HandlebarsExpression.Iterator( - _startingNode.Arguments.Single(), + _startingNode.Arguments.Single(o => o.NodeType != (ExpressionType)HandlebarsExpressionType.BlockParamsExpression), + _startingNode.Arguments.OfType().SingleOrDefault() ?? BlockParamsExpression.Empty(), Expression.Block(_body)); _body = new List(); } @@ -44,13 +45,15 @@ public override bool IsClosingElement(Expression item) if (_accumulatedExpression == null) { _accumulatedExpression = HandlebarsExpression.Iterator( - _startingNode.Arguments.Single(), + _startingNode.Arguments.Single(o => o.NodeType != (ExpressionType)HandlebarsExpressionType.BlockParamsExpression), + _startingNode.Arguments.OfType().SingleOrDefault() ?? BlockParamsExpression.Empty(), Expression.Block(bodyStatements)); } else { _accumulatedExpression = HandlebarsExpression.Iterator( ((IteratorExpression)_accumulatedExpression).Sequence, + ((IteratorExpression)_accumulatedExpression).BlockParams, ((IteratorExpression)_accumulatedExpression).Template, Expression.Block(bodyStatements)); } diff --git a/source/Handlebars/Compiler/Lexer/Converter/BlockParamsConverter.cs b/source/Handlebars/Compiler/Lexer/Converter/BlockParamsConverter.cs new file mode 100644 index 00000000..b654ac38 --- /dev/null +++ b/source/Handlebars/Compiler/Lexer/Converter/BlockParamsConverter.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using HandlebarsDotNet.Compiler.Lexer; + +namespace HandlebarsDotNet.Compiler +{ + internal class BlockParamsConverter : TokenConverter + { + public static IEnumerable Convert(IEnumerable sequence) + { + return new BlockParamsConverter().ConvertTokens(sequence).ToList(); + } + + private BlockParamsConverter() + { + } + + public override IEnumerable ConvertTokens(IEnumerable sequence) + { + var result = new List(); + bool foundBlockParams = false; + foreach (var item in sequence) + { + if (item is BlockParameterToken blockParameterToken) + { + if(foundBlockParams) throw new HandlebarsCompilerException("multiple blockParams expressions are not supported"); + + foundBlockParams = true; + if(!(result.Last() is PathExpression pathExpression)) throw new HandlebarsCompilerException("blockParams definition has incorrect syntax"); + if(!string.Equals("as", pathExpression.Path, StringComparison.OrdinalIgnoreCase)) throw new HandlebarsCompilerException("blockParams definition has incorrect syntax"); + + result[result.Count - 1] = HandlebarsExpression.BlockParams(pathExpression.Path, blockParameterToken.Value); + } + else + { + result.Add(item); + } + } + + return result; + } + } +} \ No newline at end of file diff --git a/source/Handlebars/Compiler/Lexer/Parsers/BlockParamsParser.cs b/source/Handlebars/Compiler/Lexer/Parsers/BlockParamsParser.cs new file mode 100644 index 00000000..cdd08b8a --- /dev/null +++ b/source/Handlebars/Compiler/Lexer/Parsers/BlockParamsParser.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace HandlebarsDotNet.Compiler.Lexer +{ + internal class BlockParamsParser : Parser + { + public override Token Parse(TextReader reader) + { + var buffer = AccumulateWord(reader); + return !string.IsNullOrEmpty(buffer) + ? Token.BlockParams(buffer) + : null; + } + + private static string AccumulateWord(TextReader reader) + { + var buffer = new StringBuilder(); + + if (reader.Peek() != '|') return null; + + reader.Read(); + + while (reader.Peek() != '|') + { + buffer.Append((char) reader.Read()); + } + + reader.Read(); + + var accumulateWord = buffer.ToString().Trim(); + if(string.IsNullOrEmpty(accumulateWord)) throw new HandlebarsParserException($"BlockParams expression is not valid"); + + return accumulateWord; + } + } +} \ No newline at end of file diff --git a/source/Handlebars/Compiler/Lexer/Tokenizer.cs b/source/Handlebars/Compiler/Lexer/Tokenizer.cs index 7326dd8e..2dafffa8 100644 --- a/source/Handlebars/Compiler/Lexer/Tokenizer.cs +++ b/source/Handlebars/Compiler/Lexer/Tokenizer.cs @@ -10,11 +10,12 @@ internal class Tokenizer { private readonly HandlebarsConfiguration _configuration; - private static Parser _wordParser = new WordParser(); - private static Parser _literalParser = new LiteralParser(); - private static Parser _commentParser = new CommentParser(); - private static Parser _partialParser = new PartialParser(); - private static Parser _blockWordParser = new BlockWordParser(); + private static readonly Parser WordParser = new WordParser(); + private static readonly Parser LiteralParser = new LiteralParser(); + private static readonly Parser CommentParser = new CommentParser(); + private static readonly Parser PartialParser = new PartialParser(); + private static readonly Parser BlockWordParser = new BlockWordParser(); + private static readonly Parser BlockParamsParser = new BlockParamsParser(); //TODO: structure parser public Tokenizer(HandlebarsConfiguration configuration) @@ -65,23 +66,24 @@ private IEnumerable Parse(TextReader source) } Token token = null; - token = token ?? _wordParser.Parse(source); - token = token ?? _literalParser.Parse(source); - token = token ?? _commentParser.Parse(source); - token = token ?? _partialParser.Parse(source); - token = token ?? _blockWordParser.Parse(source); + token = token ?? WordParser.Parse(source); + token = token ?? LiteralParser.Parse(source); + token = token ?? CommentParser.Parse(source); + token = token ?? PartialParser.Parse(source); + token = token ?? BlockWordParser.Parse(source); + token = token ?? BlockParamsParser.Parse(source); if (token != null) { - yield return token; - - if ((char)source.Peek() == '=') - { - source.Read(); - yield return Token.Assignment(); - continue; - } - } + yield return token; + + if ((char)source.Peek() == '=') + { + source.Read(); + yield return Token.Assignment(); + continue; + } + } if ((char)node == '}' && (char)source.Read() == '}') { bool escaped = true; @@ -90,11 +92,11 @@ private IEnumerable Parse(TextReader source) { node = source.Read(); escaped = false; - } - if ((char)source.Peek() == '}') - { - node = source.Read(); - raw = true; + } + if ((char)source.Peek() == '}') + { + node = source.Read(); + raw = true; } node = source.Read(); yield return Token.EndExpression(escaped, trimWhitespace, raw); diff --git a/source/Handlebars/Compiler/Lexer/Tokens/BlockParameterToken.cs b/source/Handlebars/Compiler/Lexer/Tokens/BlockParameterToken.cs new file mode 100644 index 00000000..7572a499 --- /dev/null +++ b/source/Handlebars/Compiler/Lexer/Tokens/BlockParameterToken.cs @@ -0,0 +1,19 @@ +using System; + +namespace HandlebarsDotNet.Compiler.Lexer +{ + internal class BlockParameterToken : Token + { + public BlockParameterToken(string value) + { + Value = value; + } + + public override TokenType Type + { + get { return TokenType.BlockParams; } + } + + public override string Value { get; } + } +} \ No newline at end of file diff --git a/source/Handlebars/Compiler/Lexer/Tokens/Token.cs b/source/Handlebars/Compiler/Lexer/Tokens/Token.cs index 2acd5a3f..56e0b6b3 100644 --- a/source/Handlebars/Compiler/Lexer/Tokens/Token.cs +++ b/source/Handlebars/Compiler/Lexer/Tokens/Token.cs @@ -62,5 +62,10 @@ public static AssignmentToken Assignment() { return new AssignmentToken(); } + + public static BlockParameterToken BlockParams(string blockParams) + { + return new BlockParameterToken(blockParams); + } } } diff --git a/source/Handlebars/Compiler/Lexer/Tokens/TokenType.cs b/source/Handlebars/Compiler/Lexer/Tokens/TokenType.cs index e325e1ba..2e93156d 100644 --- a/source/Handlebars/Compiler/Lexer/Tokens/TokenType.cs +++ b/source/Handlebars/Compiler/Lexer/Tokens/TokenType.cs @@ -15,7 +15,8 @@ internal enum TokenType Layout = 8, StartSubExpression = 9, EndSubExpression = 10, - Assignment = 11 + Assignment = 11, + BlockParams = 12 } } diff --git a/source/Handlebars/Compiler/Structure/BindingContext.cs b/source/Handlebars/Compiler/Structure/BindingContext.cs index 849e3032..b8baa8c7 100644 --- a/source/Handlebars/Compiler/Structure/BindingContext.cs +++ b/source/Handlebars/Compiler/Structure/BindingContext.cs @@ -7,23 +7,8 @@ namespace HandlebarsDotNet.Compiler { internal class BindingContext { - private readonly object _value; - private readonly BindingContext _parent; - - public string TemplatePath { get; private set; } - - public EncodedTextWriter TextWriter { get; private set; } - - public IDictionary> InlinePartialTemplates { get; private set; } - - public Action PartialBlockTemplate { get; private set; } - - public bool SuppressEncoding - { - get { return TextWriter.SuppressEncoding; } - set { TextWriter.SuppressEncoding = value; } - } - + private readonly List _valueProviders = new List(); + public BindingContext(object value, EncodedTextWriter writer, BindingContext parent, string templatePath, IDictionary> inlinePartialTemplates) : this(value, writer, parent, templatePath, null, null, inlinePartialTemplates) { } @@ -32,10 +17,12 @@ public BindingContext(object value, EncodedTextWriter writer, BindingContext par public BindingContext(object value, EncodedTextWriter writer, BindingContext parent, string templatePath, Action partialBlockTemplate, BindingContext current, IDictionary> inlinePartialTemplates) { + RegisterValueProvider(new BindingContextValueProvider(this)); + TemplatePath = parent != null ? (parent.TemplatePath ?? templatePath) : templatePath; TextWriter = writer; - _value = value; - _parent = parent; + Value = value; + ParentContext = parent; //Inline partials cannot use the Handlebars.RegisteredTemplate method //because it pollutes the static dictionary and creates collisions @@ -70,69 +57,59 @@ public BindingContext(object value, EncodedTextWriter writer, BindingContext par PartialBlockTemplate = partialBlockTemplate; } - public virtual object Value - { - get { return _value; } - } + public string TemplatePath { get; } + + public EncodedTextWriter TextWriter { get; } - public virtual BindingContext ParentContext + public IDictionary> InlinePartialTemplates { get; } + + public Action PartialBlockTemplate { get; } + + public bool SuppressEncoding { - get { return _parent; } + get => TextWriter.SuppressEncoding; + set => TextWriter.SuppressEncoding = value; } + + public virtual object Value { get; } + + public virtual BindingContext ParentContext { get; } - public virtual object Root + public virtual object Root => ParentContext?.Root ?? this; + + public void RegisterValueProvider(IValueProvider valueProvider) { - get - { - var currentContext = this; - while (currentContext.ParentContext != null) - { - currentContext = currentContext.ParentContext; - } - return currentContext.Value; - } + if(valueProvider == null) throw new ArgumentNullException(nameof(valueProvider)); + + _valueProviders.Add(valueProvider); } public virtual object GetContextVariable(string variableName) { - var target = this; + // accessing value providers in reverse order as it gives more probability of hit + for (var index = _valueProviders.Count - 1; index >= 0; index--) + { + if (_valueProviders[index].TryGetValue(variableName, out var value)) return value; + } - return GetContextVariable(variableName, target) - ?? GetContextVariable(variableName, target.Value); + return null; } - - private object GetContextVariable(string variableName, object target) + + public virtual object GetVariable(string variableName) { - object returnValue; - variableName = variableName.TrimStart('@'); - var member = target.GetType().GetMember(variableName, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase); - if (member.Length > 0) - { - if (member[0] is PropertyInfo) - { - returnValue = ((PropertyInfo)member[0]).GetValue(target, null); - } - else if (member[0] is FieldInfo) - { - returnValue = ((FieldInfo)member[0]).GetValue(target); - } - else - { - throw new HandlebarsRuntimeException("Context variable references a member that is not a field or property"); - } - } - else if (_parent != null) + // accessing value providers in reverse order as it gives more probability of hit + for (var index = _valueProviders.Count - 1; index >= 0; index--) { - returnValue = _parent.GetContextVariable(variableName); + var valueProvider = _valueProviders[index]; + if(!valueProvider.ProvidesNonContextVariables) continue; + + if (valueProvider.TryGetValue(variableName, out var value)) return value; } - else - { - returnValue = null; - } - return returnValue; + + return ParentContext?.GetVariable(variableName); } - private IDictionary GetContextDictionary(object target) + private static IDictionary GetContextDictionary(object target) { var dict = new Dictionary(); diff --git a/source/Handlebars/Compiler/Structure/BindingContextValueProvider.cs b/source/Handlebars/Compiler/Structure/BindingContextValueProvider.cs new file mode 100644 index 00000000..d3725c99 --- /dev/null +++ b/source/Handlebars/Compiler/Structure/BindingContextValueProvider.cs @@ -0,0 +1,39 @@ +using System.Reflection; + +namespace HandlebarsDotNet.Compiler +{ + internal class BindingContextValueProvider : IValueProvider + { + private readonly BindingContext _context; + + public BindingContextValueProvider(BindingContext context) + { + _context = context; + } + + public bool ProvidesNonContextVariables { get; } = false; + + public bool TryGetValue(string memberName, out object value) + { + value = GetContextVariable(memberName, _context) ?? GetContextVariable(memberName, _context.Value); + return value != null; + } + + private object GetContextVariable(string variableName, object target) + { + variableName = variableName.TrimStart('@'); + var member = target.GetType().GetMember(variableName, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase); + if (member.Length <= 0) return _context.ParentContext?.GetContextVariable(variableName); + + switch (member[0]) + { + case PropertyInfo propertyInfo: + return propertyInfo.GetValue(target, null); + case FieldInfo fieldInfo: + return fieldInfo.GetValue(target); + default: + throw new HandlebarsRuntimeException("Context variable references a member that is not a field or property"); + } + } + } +} \ No newline at end of file diff --git a/source/Handlebars/Compiler/Structure/BlockHelperExpression.cs b/source/Handlebars/Compiler/Structure/BlockHelperExpression.cs index 5273bc5b..da158ad2 100644 --- a/source/Handlebars/Compiler/Structure/BlockHelperExpression.cs +++ b/source/Handlebars/Compiler/Structure/BlockHelperExpression.cs @@ -1,6 +1,7 @@ using System; using System.Linq.Expressions; using System.Collections.Generic; +using System.Linq; namespace HandlebarsDotNet.Compiler { @@ -8,6 +9,7 @@ internal class BlockHelperExpression : HelperExpression { private readonly Expression _body; private readonly Expression _inversion; + private readonly BlockParamsExpression _blockParams; public BlockHelperExpression( string helperName, @@ -15,10 +17,24 @@ public BlockHelperExpression( Expression body, Expression inversion, bool isRaw = false) + : this(helperName, arguments, BlockParamsExpression.Empty(), body, inversion, isRaw) + { + _body = body; + _inversion = inversion; + } + + public BlockHelperExpression( + string helperName, + IEnumerable arguments, + BlockParamsExpression blockParams, + Expression body, + Expression inversion, + bool isRaw = false) : base(helperName, arguments, isRaw) { _body = body; _inversion = inversion; + _blockParams = blockParams; } public Expression Body @@ -31,6 +47,11 @@ public Expression Inversion get { return _inversion; } } + public BlockParamsExpression BlockParams + { + get { return _blockParams; } + } + public override ExpressionType NodeType { get { return (ExpressionType)HandlebarsExpressionType.BlockExpression; } diff --git a/source/Handlebars/Compiler/Structure/BlockParamsExpression.cs b/source/Handlebars/Compiler/Structure/BlockParamsExpression.cs new file mode 100644 index 00000000..5401d177 --- /dev/null +++ b/source/Handlebars/Compiler/Structure/BlockParamsExpression.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; + +namespace HandlebarsDotNet.Compiler +{ + internal class BlockParamsExpression : HandlebarsExpression + { + public new static BlockParamsExpression Empty() => new BlockParamsExpression(null); + + private readonly BlockParam _blockParam; + + private BlockParamsExpression(BlockParam blockParam) + { + _blockParam = blockParam; + } + + public BlockParamsExpression(string action, string blockParams) + :this(new BlockParam + { + Action = action, + Parameters = blockParams.Split(new char[] {' '}, StringSplitOptions.RemoveEmptyEntries) + }) + { + } + + public override ExpressionType NodeType { get; } = (ExpressionType)HandlebarsExpressionType.BlockParamsExpression; + + public override Type Type { get; } = typeof(BlockParam); + + protected override Expression Accept(ExpressionVisitor visitor) + { + return visitor.Visit(Expression.Convert(Constant(_blockParam), typeof(BlockParam))); + } + } + + internal class BlockParam + { + public string Action { get; set; } + public string[] Parameters { get; set; } + } +} \ No newline at end of file diff --git a/source/Handlebars/Compiler/Structure/BlockParamsValueProvider.cs b/source/Handlebars/Compiler/Structure/BlockParamsValueProvider.cs new file mode 100644 index 00000000..1d19da85 --- /dev/null +++ b/source/Handlebars/Compiler/Structure/BlockParamsValueProvider.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace HandlebarsDotNet.Compiler +{ + /// + /// Parameters passed to BlockParams. + /// Function that perform binding of parameter to . + public delegate void ConfigureBlockParams(string[] parameters, ValueBinder valueBinder); + + /// + /// Function that perform binding of parameter to . + /// + /// Variable name that would be added to the . + /// Variable value provider that would be invoked when is requested. + public delegate void ValueBinder(string variableName, Func valueProvider); + + /// + internal class BlockParamsValueProvider : IValueProvider + { + private readonly BlockParam _params; + private readonly Action> _invoker; + private readonly Dictionary> _accessors; + + public BlockParamsValueProvider(BindingContext context, BlockParam @params) + { + _params = @params; + _invoker = action => action(context); + _accessors = new Dictionary>(StringComparer.OrdinalIgnoreCase); + + context.RegisterValueProvider(this); + } + + public bool ProvidesNonContextVariables { get; } = true; + + /// + /// Configures behavior of BlockParams. + /// + public void Configure(ConfigureBlockParams blockParamsConfiguration) + { + if(_params == null) return; + + void BlockParamsAction(BindingContext context) + { + void ValueBinder(string name, Func value) + { + if (!string.IsNullOrEmpty(name)) _accessors[name] = value; + } + + blockParamsConfiguration.Invoke(_params.Parameters, ValueBinder); + } + + _invoker(BlockParamsAction); + } + + public bool TryGetValue(string param, out object value) + { + if (_accessors.TryGetValue(param, out var valueProvider)) + { + value = valueProvider(); + return true; + } + + value = null; + return false; + } + } +} \ No newline at end of file diff --git a/source/Handlebars/Compiler/Structure/HandlebarsExpression.cs b/source/Handlebars/Compiler/Structure/HandlebarsExpression.cs index 38326a89..01f89456 100644 --- a/source/Handlebars/Compiler/Structure/HandlebarsExpression.cs +++ b/source/Handlebars/Compiler/Structure/HandlebarsExpression.cs @@ -17,7 +17,8 @@ internal enum HandlebarsExpressionType SubExpression = 6009, HashParameterAssignmentExpression = 6010, HashParametersExpression = 6011, - CommentExpression = 6012 + CommentExpression = 6012, + BlockParamsExpression = 6013 } internal abstract class HandlebarsExpression : Expression @@ -35,17 +36,23 @@ public static HelperExpression Helper(string helperName, bool isRaw = false) public static BlockHelperExpression BlockHelper( string helperName, IEnumerable arguments, + BlockParamsExpression blockParams, Expression body, Expression inversion, bool isRaw = false) { - return new BlockHelperExpression(helperName, arguments, body, inversion, isRaw); + return new BlockHelperExpression(helperName, arguments, blockParams, body, inversion, isRaw); } public static PathExpression Path(string path) { return new PathExpression(path); } + + public static BlockParamsExpression BlockParams(string action, string blockParams) + { + return new BlockParamsExpression(action, blockParams); + } public static StaticExpression Static(string value) { @@ -59,17 +66,19 @@ public static StatementExpression Statement(Expression body, bool isEscaped, boo public static IteratorExpression Iterator( Expression sequence, + BlockParamsExpression blockParams, Expression template) { - return new IteratorExpression(sequence, template); + return new IteratorExpression(sequence, blockParams, template, Empty()); } public static IteratorExpression Iterator( Expression sequence, + BlockParamsExpression blockParams, Expression template, Expression ifEmpty) { - return new IteratorExpression(sequence, template, ifEmpty); + return new IteratorExpression(sequence, blockParams, template, ifEmpty); } public static DeferredSectionExpression DeferredSection( diff --git a/source/Handlebars/Compiler/Structure/IValueProvider.cs b/source/Handlebars/Compiler/Structure/IValueProvider.cs new file mode 100644 index 00000000..f2bd176e --- /dev/null +++ b/source/Handlebars/Compiler/Structure/IValueProvider.cs @@ -0,0 +1,8 @@ +namespace HandlebarsDotNet.Compiler +{ + internal interface IValueProvider + { + bool ProvidesNonContextVariables { get; } + bool TryGetValue(string memberName, out object value); + } +} \ No newline at end of file diff --git a/source/Handlebars/Compiler/Structure/IteratorExpression.cs b/source/Handlebars/Compiler/Structure/IteratorExpression.cs index b42498aa..60c77b25 100644 --- a/source/Handlebars/Compiler/Structure/IteratorExpression.cs +++ b/source/Handlebars/Compiler/Structure/IteratorExpression.cs @@ -1,9 +1,11 @@ using System; +using System.Collections.Generic; +using System.Linq; using System.Linq.Expressions; namespace HandlebarsDotNet.Compiler { - internal class IteratorExpression : HandlebarsExpression + internal class IteratorExpression : BlockHelperExpression { private readonly Expression _sequence; private readonly Expression _template; @@ -16,6 +18,13 @@ public IteratorExpression(Expression sequence, Expression template) } public IteratorExpression(Expression sequence, Expression template, Expression ifEmpty) + :this(sequence, BlockParamsExpression.Empty(), template, ifEmpty) + { + + } + + public IteratorExpression(Expression sequence, BlockParamsExpression blockParams, Expression template, Expression ifEmpty) + :base("each", Enumerable.Empty(), blockParams, template, ifEmpty, false) { _sequence = sequence; _template = template; diff --git a/source/Handlebars/Compiler/Translation/Expression/BlockHelperFunctionBinder.cs b/source/Handlebars/Compiler/Translation/Expression/BlockHelperFunctionBinder.cs index 46a44b20..4c208ef1 100644 --- a/source/Handlebars/Compiler/Translation/Expression/BlockHelperFunctionBinder.cs +++ b/source/Handlebars/Compiler/Translation/Expression/BlockHelperFunctionBinder.cs @@ -1,4 +1,7 @@ -using System.Linq.Expressions; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; using System.Reflection; namespace HandlebarsDotNet.Compiler @@ -38,8 +41,11 @@ protected override Expression VisitBlockHelperExpression(BlockHelperExpression b Expression.Property( CompilationContext.BindingContext, typeof(BindingContext).GetProperty("Value")); + + var ctor = typeof(BlockParamsValueProvider).GetConstructors().Single(); + var blockParamsExpression = Expression.New(ctor, CompilationContext.BindingContext, bhex.BlockParams); - var body = fb.Compile(((BlockExpression)bhex.Body).Expressions, CompilationContext.BindingContext); + var body = fb.Compile(((BlockExpression) bhex.Body).Expressions, CompilationContext.BindingContext); var inversion = fb.Compile(((BlockExpression)bhex.Inversion).Expressions, CompilationContext.BindingContext); var helper = CompilationContext.Configuration.BlockHelpers[bhex.HelperName.Replace("#", "")]; var arguments = new Expression[] @@ -50,7 +56,8 @@ protected override Expression VisitBlockHelperExpression(BlockHelperExpression b Expression.New( typeof(HelperOptions).GetConstructors(BindingFlags.Instance | BindingFlags.NonPublic)[0], body, - inversion), + inversion, + blockParamsExpression), //this next arg is usually data, like { first: "Marc" } //but for inline partials this is the complete BindingContext. bindingContext, diff --git a/source/Handlebars/Compiler/Translation/Expression/HandlebarsExpressionVisitor.cs b/source/Handlebars/Compiler/Translation/Expression/HandlebarsExpressionVisitor.cs index 41d70b78..7078043a 100644 --- a/source/Handlebars/Compiler/Translation/Expression/HandlebarsExpressionVisitor.cs +++ b/source/Handlebars/Compiler/Translation/Expression/HandlebarsExpressionVisitor.cs @@ -59,9 +59,9 @@ public override Expression Visit(Expression exp) protected virtual Expression VisitStatementExpression(StatementExpression sex) { Expression body = Visit(sex.Body); - if (body != sex.Body) - { - return HandlebarsExpression.Statement(body, sex.IsEscaped, sex.TrimBefore, sex.TrimAfter); + if (body != sex.Body) + { + return HandlebarsExpression.Statement(body, sex.IsEscaped, sex.TrimBefore, sex.TrimAfter); } return sex; } @@ -72,23 +72,23 @@ protected virtual Expression VisitPathExpression(PathExpression pex) } protected virtual Expression VisitHelperExpression(HelperExpression hex) - { - var arguments = VisitExpressionList(hex.Arguments); - if (arguments != hex.Arguments) - { - return HandlebarsExpression.Helper(hex.HelperName, arguments, hex.IsRaw); + { + var arguments = VisitExpressionList(hex.Arguments); + if (arguments != hex.Arguments) + { + return HandlebarsExpression.Helper(hex.HelperName, arguments, hex.IsRaw); } return hex; } protected virtual Expression VisitBlockHelperExpression(BlockHelperExpression bhex) - { + { var arguments = VisitExpressionList(bhex.Arguments); - // Don't visit Body/Inversion - they will be compiled separately - - if (arguments != bhex.Arguments) - { - return HandlebarsExpression.BlockHelper(bhex.HelperName, arguments, bhex.Body, bhex.Inversion, bhex.IsRaw); + // Don't visit Body/Inversion - they will be compiled separately + + if (arguments != bhex.Arguments) + { + return HandlebarsExpression.BlockHelper(bhex.HelperName, arguments, bhex.BlockParams, bhex.Body, bhex.Inversion, bhex.IsRaw); } return bhex; } @@ -103,9 +103,9 @@ protected virtual Expression VisitIteratorExpression(IteratorExpression iex) Expression sequence = Visit(iex.Sequence); // Don't visit Template/IfEmpty - they will be compiled separately - if (sequence != iex.Sequence) - { - return HandlebarsExpression.Iterator(sequence, iex.Template, iex.IfEmpty); + if (sequence != iex.Sequence) + { + return HandlebarsExpression.Iterator(sequence, iex.BlockParams, iex.Template, iex.IfEmpty); } return iex; } @@ -115,9 +115,9 @@ protected virtual Expression VisitDeferredSectionExpression(DeferredSectionExpre PathExpression path = (PathExpression)Visit(dsex.Path); // Don't visit Body/Inversion - they will be compiled separately - if (path != dsex.Path) - { - return HandlebarsExpression.DeferredSection(path, dsex.Body, dsex.Inversion); + if (path != dsex.Path) + { + return HandlebarsExpression.DeferredSection(path, dsex.Body, dsex.Inversion); } return dsex; } @@ -129,9 +129,9 @@ protected virtual Expression VisitPartialExpression(PartialExpression pex) // Don't visit Fallback - it will be compiled separately if (partialName != pex.PartialName - || argument != pex.Argument) - { - return HandlebarsExpression.Partial(partialName, argument, pex.Fallback); + || argument != pex.Argument) + { + return HandlebarsExpression.Partial(partialName, argument, pex.Fallback); } return pex; } @@ -139,9 +139,9 @@ protected virtual Expression VisitPartialExpression(PartialExpression pex) protected virtual Expression VisitBoolishExpression(BoolishExpression bex) { Expression condition = Visit(bex.Condition); - if (condition != bex.Condition) - { - return HandlebarsExpression.Boolish(condition); + if (condition != bex.Condition) + { + return HandlebarsExpression.Boolish(condition); } return bex; } @@ -149,9 +149,9 @@ protected virtual Expression VisitBoolishExpression(BoolishExpression bex) protected virtual Expression VisitSubExpression(SubExpressionExpression subex) { Expression expression = Visit(subex.Expression); - if (expression != subex.Expression) - { - return HandlebarsExpression.SubExpression(expression); + if (expression != subex.Expression) + { + return HandlebarsExpression.SubExpression(expression); } return subex; } @@ -176,35 +176,35 @@ protected virtual Expression VisitHashParametersExpression(HashParametersExpress return hpex; } - IEnumerable VisitExpressionList(IEnumerable original) - { - if (original == null) - { - return original; - } - - var originalAsList = original as IReadOnlyList ?? original.ToArray(); - List list = null; - for (int i = 0, n = originalAsList.Count; i < n; i++) - { - Expression p = Visit(originalAsList[i]); - if (list != null) - { - list.Add(p); - } - else if (p != originalAsList[i]) - { - list = new List(n); - for (int j = 0; j < i; j++) - { - list.Add(originalAsList[j]); - } - list.Add(p); - } - } - if (list != null) - return list.ToArray(); - return original; + IEnumerable VisitExpressionList(IEnumerable original) + { + if (original == null) + { + return original; + } + + var originalAsList = original as IReadOnlyList ?? original.ToArray(); + List list = null; + for (int i = 0, n = originalAsList.Count; i < n; i++) + { + Expression p = Visit(originalAsList[i]); + if (list != null) + { + list.Add(p); + } + else if (p != originalAsList[i]) + { + list = new List(n); + for (int j = 0; j < i; j++) + { + list.Add(originalAsList[j]); + } + list.Add(p); + } + } + if (list != null) + return list.ToArray(); + return original; } } } diff --git a/source/Handlebars/Compiler/Translation/Expression/IteratorBinder.cs b/source/Handlebars/Compiler/Translation/Expression/IteratorBinder.cs index cdbebfd5..986571c2 100644 --- a/source/Handlebars/Compiler/Translation/Expression/IteratorBinder.cs +++ b/source/Handlebars/Compiler/Translation/Expression/IteratorBinder.cs @@ -24,11 +24,16 @@ private IteratorBinder(CompilationContext context) protected override Expression VisitIteratorExpression(IteratorExpression iex) { var iteratorBindingContext = Expression.Variable(typeof(BindingContext), "context"); + var blockParamsValueBinder = Expression.Variable(typeof(BlockParamsValueProvider), "blockParams"); + var ctor = typeof(BlockParamsValueProvider).GetConstructors().Single(); + return Expression.Block( - new ParameterExpression[] + new[] { - iteratorBindingContext + iteratorBindingContext, blockParamsValueBinder }, + Expression.Assign(iteratorBindingContext, CompilationContext.BindingContext), + Expression.Assign(blockParamsValueBinder, Expression.New(ctor, iteratorBindingContext, iex.BlockParams)), Expression.IfThenElse( Expression.TypeIs(iex.Sequence, typeof(IEnumerable)), Expression.IfThenElse( @@ -37,109 +42,93 @@ protected override Expression VisitIteratorExpression(IteratorExpression iex) #else Expression.Call(new Func(IsNonListDynamic).Method, new[] { iex.Sequence }), #endif - GetDynamicIterator(iteratorBindingContext, iex), + GetDynamicIterator(iteratorBindingContext, blockParamsValueBinder, iex), Expression.IfThenElse( #if netstandard Expression.Call(new Func(IsGenericDictionary).GetMethodInfo(), new[] { iex.Sequence }), #else Expression.Call(new Func(IsGenericDictionary).Method, new[] { iex.Sequence }), #endif - GetDictionaryIterator(iteratorBindingContext, iex), - GetEnumerableIterator(iteratorBindingContext, iex))), - GetObjectIterator(iteratorBindingContext, iex)) + GetDictionaryIterator(iteratorBindingContext, blockParamsValueBinder, iex), + GetEnumerableIterator(iteratorBindingContext, blockParamsValueBinder, iex))), + GetObjectIterator(iteratorBindingContext, blockParamsValueBinder, iex)) ); } - private Expression GetEnumerableIterator(Expression contextParameter, IteratorExpression iex) + private Expression GetEnumerableIterator(Expression contextParameter, Expression blockParamsParameter, IteratorExpression iex) { var fb = new FunctionBuilder(CompilationContext.Configuration); - return Expression.Block( - Expression.Assign(contextParameter, - Expression.New( - typeof(IteratorBindingContext).GetConstructor(new[] { typeof(BindingContext) }), - new Expression[] { CompilationContext.BindingContext })), - Expression.Call( + return Expression.Call( #if netstandard - new Action, Action>(Iterate).GetMethodInfo(), + new Action, Action>(IterateEnumerable).GetMethodInfo(), #else - new Action, Action>(Iterate).Method, + new Action, Action>(IterateEnumerable).Method, #endif - new Expression[] - { - Expression.Convert(contextParameter, typeof(IteratorBindingContext)), - Expression.Convert(iex.Sequence, typeof(IEnumerable)), - fb.Compile(new [] { iex.Template }, contextParameter), - fb.Compile(new [] { iex.IfEmpty }, CompilationContext.BindingContext) - })); + new Expression[] + { + contextParameter, + blockParamsParameter, + Expression.Convert(iex.Sequence, typeof(IEnumerable)), + fb.Compile(new [] { iex.Template }, contextParameter), + fb.Compile(new [] { iex.IfEmpty }, CompilationContext.BindingContext) + }); } - private Expression GetObjectIterator(Expression contextParameter, IteratorExpression iex) + private Expression GetObjectIterator(Expression contextParameter, Expression blockParamsParameter, IteratorExpression iex) { var fb = new FunctionBuilder(CompilationContext.Configuration); - return Expression.Block( - Expression.Assign(contextParameter, - Expression.New( - typeof(ObjectEnumeratorBindingContext).GetConstructor(new[] { typeof(BindingContext) }), - new Expression[] { CompilationContext.BindingContext })), - Expression.Call( + return Expression.Call( #if netstandard - new Action, Action>(Iterate).GetMethodInfo(), + new Action, Action>(IterateObject).GetMethodInfo(), #else - new Action, Action>(Iterate).Method, + new Action, Action>(IterateObject).Method, #endif - new Expression[] - { - Expression.Convert(contextParameter, typeof(ObjectEnumeratorBindingContext)), - iex.Sequence, - fb.Compile(new [] { iex.Template }, contextParameter), - fb.Compile(new [] { iex.IfEmpty }, CompilationContext.BindingContext) - })); + new[] + { + contextParameter, + blockParamsParameter, + iex.Sequence, + fb.Compile(new [] { iex.Template }, contextParameter), + fb.Compile(new [] { iex.IfEmpty }, CompilationContext.BindingContext) + }); } - private Expression GetDictionaryIterator(Expression contextParameter, IteratorExpression iex) + private Expression GetDictionaryIterator(Expression contextParameter, Expression blockParamsParameter, IteratorExpression iex) { var fb = new FunctionBuilder(CompilationContext.Configuration); - return Expression.Block( - Expression.Assign(contextParameter, - Expression.New( - typeof(ObjectEnumeratorBindingContext).GetConstructor(new[] { typeof(BindingContext) }), - new Expression[] { CompilationContext.BindingContext })), - Expression.Call( + return Expression.Call( #if netstandard - new Action, Action>(Iterate).GetMethodInfo(), + new Action, Action>(IterateDictionary).GetMethodInfo(), #else - new Action, Action>(Iterate).Method, + new Action, Action>(IterateDictionary).Method, #endif - new Expression[] - { - Expression.Convert(contextParameter, typeof(ObjectEnumeratorBindingContext)), - Expression.Convert(iex.Sequence, typeof(IEnumerable)), - fb.Compile(new [] { iex.Template }, contextParameter), - fb.Compile(new [] { iex.IfEmpty }, CompilationContext.BindingContext) - })); + new[] + { + contextParameter, + blockParamsParameter, + Expression.Convert(iex.Sequence, typeof(IEnumerable)), + fb.Compile(new[] {iex.Template}, contextParameter), + fb.Compile(new[] {iex.IfEmpty}, CompilationContext.BindingContext) + }); } - private Expression GetDynamicIterator(Expression contextParameter, IteratorExpression iex) + private Expression GetDynamicIterator(Expression contextParameter, Expression blockParamsParameter, IteratorExpression iex) { var fb = new FunctionBuilder(CompilationContext.Configuration); - return Expression.Block( - Expression.Assign(contextParameter, - Expression.New( - typeof(ObjectEnumeratorBindingContext).GetConstructor(new[] { typeof(BindingContext) }), - new Expression[] { CompilationContext.BindingContext })), - Expression.Call( + return Expression.Call( #if netstandard - new Action, Action>(Iterate).GetMethodInfo(), + new Action,Action>(IterateDynamic).GetMethodInfo(), #else - new Action, Action>(Iterate).Method, + new Action, Action>(IterateDynamic).Method, #endif - new Expression[] - { - Expression.Convert(contextParameter, typeof(ObjectEnumeratorBindingContext)), - Expression.Convert(iex.Sequence, typeof(IDynamicMetaObjectProvider)), - fb.Compile(new [] { iex.Template }, contextParameter), - fb.Compile(new [] { iex.IfEmpty }, CompilationContext.BindingContext) - })); + new[] + { + contextParameter, + blockParamsParameter, + Expression.Convert(iex.Sequence, typeof(IDynamicMetaObjectProvider)), + fb.Compile(new[] {iex.Template}, contextParameter), + fb.Compile(new[] {iex.IfEmpty}, CompilationContext.BindingContext) + }); } private static bool IsNonListDynamic(object target) @@ -164,31 +153,40 @@ private static bool IsGenericDictionary(object target) .Any(i => i.GetGenericTypeDefinition() == typeof(IDictionary<,>)); } - private static void Iterate( - ObjectEnumeratorBindingContext context, + private static void IterateObject( + BindingContext context, + BlockParamsValueProvider blockParamsValueProvider, object target, Action template, Action ifEmpty) { if (HandlebarsUtils.IsTruthy(target)) { - context.Index = 0; + var objectEnumerator = new ObjectEnumeratorValueProvider(); + context.RegisterValueProvider(objectEnumerator); + blockParamsValueProvider.Configure((parameters, binder) => + { + binder(parameters.ElementAtOrDefault(0), () => objectEnumerator.Value); + binder(parameters.ElementAtOrDefault(1), () => objectEnumerator.Key); + }); + + objectEnumerator.Index = 0; var targetType = target.GetType(); var properties = targetType.GetProperties(BindingFlags.Instance | BindingFlags.Public).OfType(); var fields = targetType.GetFields(BindingFlags.Public | BindingFlags.Instance); foreach (var enumerableValue in new ExtendedEnumerable(properties.Concat(fields))) { var member = enumerableValue.Value; - context.Key = member.Name; - var value = AccessMember(target, member); - context.First = enumerableValue.IsFirst; - context.Last = enumerableValue.IsLast; - context.Index = enumerableValue.Index; + objectEnumerator.Key = member.Name; + objectEnumerator.Value = AccessMember(target, member); + objectEnumerator.First = enumerableValue.IsFirst; + objectEnumerator.Last = enumerableValue.IsLast; + objectEnumerator.Index = enumerableValue.Index; - template(context.TextWriter, value); + template(context.TextWriter, objectEnumerator.Value); } - if (context.Index == 0) + if (objectEnumerator.Index == 0) { ifEmpty(context.TextWriter, context.Value); } @@ -199,42 +197,47 @@ private static void Iterate( } } - private static void Iterate( - ObjectEnumeratorBindingContext context, + private static void IterateDictionary( + BindingContext context, + BlockParamsValueProvider blockParamsValueProvider, IEnumerable target, Action template, Action ifEmpty) { if (HandlebarsUtils.IsTruthy(target)) { - context.Index = 0; + var objectEnumerator = new ObjectEnumeratorValueProvider(); + context.RegisterValueProvider(objectEnumerator); + blockParamsValueProvider.Configure((parameters, binder) => + { + binder(parameters.ElementAtOrDefault(0), () => objectEnumerator.Value); + binder(parameters.ElementAtOrDefault(1), () => objectEnumerator.Key); + }); + + objectEnumerator.Index = 0; var targetType = target.GetType(); #if netstandard var keysProperty = targetType.GetRuntimeProperty("Keys"); #else var keysProperty = targetType.GetProperty("Keys"); #endif - if (keysProperty != null) + if (keysProperty?.GetGetMethod().Invoke(target, null) is IEnumerable keys) { - var keys = keysProperty.GetGetMethod().Invoke(target, null) as IEnumerable; - if (keys != null) + var getItemMethodInfo = targetType.GetMethod("get_Item"); + var parameters = new object[1]; + foreach (var enumerableValue in new ExtendedEnumerable(keys)) { - var getItemMethodInfo = targetType.GetMethod("get_Item"); - var parameters = new object[1]; - foreach (var enumerableValue in new ExtendedEnumerable(keys)) - { - var key = parameters[0] = enumerableValue.Value; - context.Key = key.ToString(); - var value = getItemMethodInfo.Invoke(target, parameters); - context.First = enumerableValue.IsFirst; - context.Last = enumerableValue.IsLast; - context.Index = enumerableValue.Index; - - template(context.TextWriter, value); - } + var key = parameters[0] = enumerableValue.Value; + objectEnumerator.Key = key.ToString(); + objectEnumerator.Value = getItemMethodInfo.Invoke(target, parameters); + objectEnumerator.First = enumerableValue.IsFirst; + objectEnumerator.Last = enumerableValue.IsLast; + objectEnumerator.Index = enumerableValue.Index; + + template(context.TextWriter, objectEnumerator.Value); } } - if (context.Index == 0) + if (objectEnumerator.Index == 0) { ifEmpty(context.TextWriter, context.Value); } @@ -245,29 +248,38 @@ private static void Iterate( } } - private static void Iterate( - ObjectEnumeratorBindingContext context, + private static void IterateDynamic( + BindingContext context, + BlockParamsValueProvider blockParamsValueProvider, IDynamicMetaObjectProvider target, Action template, Action ifEmpty) { if (HandlebarsUtils.IsTruthy(target)) { - context.Index = 0; + var objectEnumerator = new ObjectEnumeratorValueProvider(); + context.RegisterValueProvider(objectEnumerator); + blockParamsValueProvider.Configure((parameters, binder) => + { + binder(parameters.ElementAtOrDefault(0), () => objectEnumerator.Value); + binder(parameters.ElementAtOrDefault(1), () => objectEnumerator.Key); + }); + + objectEnumerator.Index = 0; var meta = target.GetMetaObject(Expression.Constant(target)); foreach (var enumerableValue in new ExtendedEnumerable(meta.GetDynamicMemberNames())) { var name = enumerableValue.Value; - context.Key = name; - var value = GetProperty(target, name); - context.First = enumerableValue.IsFirst; - context.Last = enumerableValue.IsLast; - context.Index = enumerableValue.Index; + objectEnumerator.Key = name; + objectEnumerator.Value = GetProperty(target, name); + objectEnumerator.First = enumerableValue.IsFirst; + objectEnumerator.Last = enumerableValue.IsLast; + objectEnumerator.Index = enumerableValue.Index; - template(context.TextWriter, value); + template(context.TextWriter, objectEnumerator.Value); } - if (context.Index == 0) + if (objectEnumerator.Index == 0) { ifEmpty(context.TextWriter, context.Value); } @@ -278,24 +290,33 @@ private static void Iterate( } } - private static void Iterate( - IteratorBindingContext context, + private static void IterateEnumerable( + BindingContext context, + BlockParamsValueProvider blockParamsValueProvider, IEnumerable sequence, Action template, Action ifEmpty) { - context.Index = 0; + var iterator = new IteratorValueProvider(); + context.RegisterValueProvider(iterator); + blockParamsValueProvider.Configure((parameters, binder) => + { + binder(parameters.ElementAtOrDefault(0), () => iterator.Value); + binder(parameters.ElementAtOrDefault(1), () => iterator.Index); + }); + + iterator.Index = 0; foreach (var enumeratorValue in new ExtendedEnumerable(sequence)) { - var item = enumeratorValue.Value; - context.First = enumeratorValue.IsFirst; - context.Last = enumeratorValue.IsLast; - context.Index = enumeratorValue.Index; + iterator.Value = enumeratorValue.Value; + iterator.First = enumeratorValue.IsFirst; + iterator.Last = enumeratorValue.IsLast; + iterator.Index = enumeratorValue.Index; - template(context.TextWriter, item); + template(context.TextWriter, iterator.Value); } - if (context.Index == 0) + if (iterator.Index == 0) { ifEmpty(context.TextWriter, context.Value); } @@ -308,41 +329,71 @@ private static object GetProperty(object target, string name) return site.Target(site, target); } - private class IteratorBindingContext : BindingContext + private class IteratorValueProvider : IValueProvider { - public IteratorBindingContext(BindingContext context) - : base(context.Value, context.TextWriter, context.ParentContext, context.TemplatePath, context.InlinePartialTemplates) - { - } - + public object Value { get; set; } + public int Index { get; set; } public bool First { get; set; } public bool Last { get; set; } - } - private class ObjectEnumeratorBindingContext : IteratorBindingContext - { - public ObjectEnumeratorBindingContext(BindingContext context) - : base(context) + public bool ProvidesNonContextVariables { get; } = false; + + public virtual bool TryGetValue(string memberName, out object value) { + switch (memberName.ToLowerInvariant()) + { + case "index": + value = Index; + return true; + case "first": + value = First; + return true; + case "last": + value = Last; + return true; + case "value": + value = Value; + return true; + + default: + value = null; + return false; + } } - + } + + private class ObjectEnumeratorValueProvider : IteratorValueProvider + { public string Key { get; set; } + + public override bool TryGetValue(string memberName, out object value) + { + switch (memberName.ToLowerInvariant()) + { + case "key": + value = Key; + return true; + + default: + return base.TryGetValue(memberName, out value); + } + } } private static object AccessMember(object instance, MemberInfo member) { - if (member is PropertyInfo) - { - return ((PropertyInfo)member).GetValue(instance, null); - } - if (member is FieldInfo) + switch (member) { - return ((FieldInfo)member).GetValue(instance); + case PropertyInfo propertyInfo: + return propertyInfo.GetValue(instance, null); + case FieldInfo fieldInfo: + return fieldInfo.GetValue(instance); + default: + throw new InvalidOperationException("Requested member was not a field or property"); } - throw new InvalidOperationException("Requested member was not a field or property"); } } } diff --git a/source/Handlebars/Compiler/Translation/Expression/PartialBinder.cs b/source/Handlebars/Compiler/Translation/Expression/PartialBinder.cs index d7a1aae1..9cfa52b5 100644 --- a/source/Handlebars/Compiler/Translation/Expression/PartialBinder.cs +++ b/source/Handlebars/Compiler/Translation/Expression/PartialBinder.cs @@ -19,9 +19,9 @@ private PartialBinder(CompilationContext context) { } - protected override Expression VisitBlockHelperExpression(BlockHelperExpression bhex) - { - return bhex; + protected override Expression VisitBlockHelperExpression(BlockHelperExpression bhex) + { + return bhex; } protected override Expression VisitStatementExpression(StatementExpression sex) @@ -47,7 +47,7 @@ protected override Expression VisitPartialExpression(PartialExpression pex) { bindingContext = Expression.Call( bindingContext, - typeof(BindingContext).GetMethod("CreateChildContext"), + typeof(BindingContext).GetMethod(nameof(BindingContext.CreateChildContext)), pex.Argument ?? Expression.Constant(null), partialBlockTemplate ?? Expression.Constant(null, typeof(Action))); } @@ -72,17 +72,17 @@ private static void InvokePartialWithFallback( { if (!InvokePartial(partialName, context, configuration)) { - if (context.PartialBlockTemplate == null) + if (context.PartialBlockTemplate == null) { - if (configuration.MissingPartialTemplateHandler != null) - { - configuration.MissingPartialTemplateHandler.Handle(configuration, partialName, context.TextWriter); - return; + if (configuration.MissingPartialTemplateHandler != null) + { + configuration.MissingPartialTemplateHandler.Handle(configuration, partialName, context.TextWriter); + return; + } + else + { + throw new HandlebarsRuntimeException(string.Format("Referenced partial name {0} could not be resolved", partialName)); } - else - { - throw new HandlebarsRuntimeException(string.Format("Referenced partial name {0} could not be resolved", partialName)); - } } context.PartialBlockTemplate(context.TextWriter, context); @@ -115,11 +115,11 @@ private static bool InvokePartial( // Partial is not found, so call the resolver and attempt to load it. if (configuration.RegisteredTemplates.ContainsKey(partialName) == false) { - if (configuration.PartialTemplateResolver == null - || configuration.PartialTemplateResolver.TryRegisterPartial(Handlebars.Create(configuration), partialName, context.TemplatePath) == false) - { - // Template not found. - return false; + if (configuration.PartialTemplateResolver == null + || configuration.PartialTemplateResolver.TryRegisterPartial(Handlebars.Create(configuration), partialName, context.TemplatePath) == false) + { + // Template not found. + return false; } } diff --git a/source/Handlebars/Compiler/Translation/Expression/PathBinder.cs b/source/Handlebars/Compiler/Translation/Expression/PathBinder.cs index 41a6c893..091ed0a9 100644 --- a/source/Handlebars/Compiler/Translation/Expression/PathBinder.cs +++ b/source/Handlebars/Compiler/Translation/Expression/PathBinder.cs @@ -166,7 +166,8 @@ private object ResolveValue(BindingContext context, object instance, string segm } else { - resolvedValue = AccessMember(instance, segment); + var variable = context.GetVariable(segment); + resolvedValue = variable ?? AccessMember(instance, segment); } return resolvedValue; } @@ -177,6 +178,11 @@ private object AccessMember(object instance, string memberName) { if (instance == null) return new UndefinedBindingResult(memberName, CompilationContext.Configuration); + + if (instance is BindingContext context) + { + return AccessMember(context.Value, memberName); + } var resolvedMemberName = ResolveMemberName(instance, memberName); var instanceType = instance.GetType(); diff --git a/source/Handlebars/Handlebars.csproj b/source/Handlebars/Handlebars.csproj index 494e3ba0..40ef2a43 100644 --- a/source/Handlebars/Handlebars.csproj +++ b/source/Handlebars/Handlebars.csproj @@ -4,7 +4,7 @@ portable true net452;netstandard1.3;netstandard2.0 - 1.10.2 + 1.10.3 7 diff --git a/source/Handlebars/HelperOptions.cs b/source/Handlebars/HelperOptions.cs index ab9efb40..9e03b656 100644 --- a/source/Handlebars/HelperOptions.cs +++ b/source/Handlebars/HelperOptions.cs @@ -1,30 +1,27 @@ using System; +using System.Collections.Generic; using System.IO; +using HandlebarsDotNet.Compiler; namespace HandlebarsDotNet { public sealed class HelperOptions { - private readonly Action _template; - private readonly Action _inverse; - internal HelperOptions( Action template, - Action inverse) + Action inverse, + BlockParamsValueProvider blockParamsValueProvider) { - _template = template; - _inverse = inverse; + Template = template; + Inverse = inverse; + BlockParams = blockParamsValueProvider.Configure; } - public Action Template - { - get { return _template; } - } + public Action Template { get; } - public Action Inverse - { - get { return _inverse; } - } + public Action Inverse { get; } + + public Action BlockParams { get; } } } From bef02f9a179d9514b6d58fbe6a40f20f5b4a0822 Mon Sep 17 00:00:00 2001 From: Oleh Formaniuk Date: Sun, 19 Jan 2020 14:04:27 -0800 Subject: [PATCH 2/2] Fix nested block params definition --- .../Handlebars.Test/BasicIntegrationTests.cs | 29 +++++++++++++++++ .../Lexer/Converter/BlockParamsConverter.cs | 31 +++++++++++++------ 2 files changed, 50 insertions(+), 10 deletions(-) diff --git a/source/Handlebars.Test/BasicIntegrationTests.cs b/source/Handlebars.Test/BasicIntegrationTests.cs index 85a4e632..29e95e08 100644 --- a/source/Handlebars.Test/BasicIntegrationTests.cs +++ b/source/Handlebars.Test/BasicIntegrationTests.cs @@ -526,6 +526,35 @@ public void DictionaryEnumeratorWithBlockParams() Assert.Equal("hello foo world bar ", result); } + [Fact] + public void DictionaryEnumeratorWithInnerBlockParams() + { + var source = "{{#each enumerateMe as |item k|}}{{#each item as |item1 k|}}{{item1}} {{k}} {{/each}}{{/each}}"; + var template = Handlebars.Compile(source); + var data = new + { + enumerateMe = new Dictionary + { + { + "foo", new Dictionary + { + {"foo1", "hello1"}, + {"bar1", "world1"} + } + }, + { + "bar", new Dictionary + { + {"foo2", "hello2"}, + {"bar2", "world2"} + } + } + } + }; + var result = template(data); + Assert.Equal("hello1 foo1 world1 bar1 hello2 foo2 world2 bar2 ", result); + } + [Fact] public void DictionaryWithLastEnumerator() { diff --git a/source/Handlebars/Compiler/Lexer/Converter/BlockParamsConverter.cs b/source/Handlebars/Compiler/Lexer/Converter/BlockParamsConverter.cs index b654ac38..4f625f7e 100644 --- a/source/Handlebars/Compiler/Lexer/Converter/BlockParamsConverter.cs +++ b/source/Handlebars/Compiler/Lexer/Converter/BlockParamsConverter.cs @@ -22,19 +22,30 @@ public override IEnumerable ConvertTokens(IEnumerable sequence) bool foundBlockParams = false; foreach (var item in sequence) { - if (item is BlockParameterToken blockParameterToken) + switch (item) { - if(foundBlockParams) throw new HandlebarsCompilerException("multiple blockParams expressions are not supported"); + case BlockParameterToken blockParameterToken when foundBlockParams: + throw new HandlebarsCompilerException("multiple blockParams expressions are not supported"); - foundBlockParams = true; - if(!(result.Last() is PathExpression pathExpression)) throw new HandlebarsCompilerException("blockParams definition has incorrect syntax"); - if(!string.Equals("as", pathExpression.Path, StringComparison.OrdinalIgnoreCase)) throw new HandlebarsCompilerException("blockParams definition has incorrect syntax"); + case BlockParameterToken blockParameterToken: + foundBlockParams = true; + if (!(result.Last() is PathExpression pathExpression)) + throw new HandlebarsCompilerException("blockParams definition has incorrect syntax"); + if (!string.Equals("as", pathExpression.Path, StringComparison.OrdinalIgnoreCase)) + throw new HandlebarsCompilerException("blockParams definition has incorrect syntax"); + + result[result.Count - 1] = + HandlebarsExpression.BlockParams(pathExpression.Path, blockParameterToken.Value); + break; - result[result.Count - 1] = HandlebarsExpression.BlockParams(pathExpression.Path, blockParameterToken.Value); - } - else - { - result.Add(item); + case EndExpressionToken _: + foundBlockParams = false; + result.Add(item); + break; + + default: + result.Add(item); + break; } }