From ac6a4932103e5cc19e5d4d1c741a972018fbcbc7 Mon Sep 17 00:00:00 2001 From: esmael Date: Fri, 11 Nov 2016 09:44:44 -0500 Subject: [PATCH] add block params support and tests --- .../gilt/handlebars/scala/Handlebars.scala | 2 +- .../handlebars/scala/helper/EachHelper.scala | 12 ++- .../scala/helper/HelperOptionsBuilder.scala | 76 +++++++++------- .../handlebars/scala/helper/WithHelper.scala | 5 +- .../scala/parser/HandlebarsGrammar.scala | 49 +++++----- .../gilt/handlebars/scala/parser/Node.scala | 18 ++++ .../scala/visitor/DefaultVisitor.scala | 56 ++++++++---- .../scala/parser/HandlebarsGrammarSpec.scala | 10 +++ .../scala/visitor/BuiltInHelperSpec.scala | 89 +++++++++++++++++++ 9 files changed, 234 insertions(+), 83 deletions(-) diff --git a/src/main/scala/com/gilt/handlebars/scala/Handlebars.scala b/src/main/scala/com/gilt/handlebars/scala/Handlebars.scala index 030e537..575a1ab 100644 --- a/src/main/scala/com/gilt/handlebars/scala/Handlebars.scala +++ b/src/main/scala/com/gilt/handlebars/scala/Handlebars.scala @@ -36,7 +36,7 @@ class HandlebarsImpl[T]( providedPartials: Map[String, Handlebars[T]] = Map.empty[String, Handlebars[T]], providedHelpers: Map[String, Helper[T]] = Map.empty[String, Helper[T]])(implicit c: BindingFactory[T]): String = { - DefaultVisitor(Context(binding), PartialHelper.normalizePartialNames(partials ++ providedPartials), helpers ++ providedHelpers, data).visit(program) + DefaultVisitor(Context(binding), PartialHelper.normalizePartialNames(partials ++ providedPartials), helpers ++ providedHelpers, data, Map.empty[String, Binding[T]]).visit(program) } } diff --git a/src/main/scala/com/gilt/handlebars/scala/helper/EachHelper.scala b/src/main/scala/com/gilt/handlebars/scala/helper/EachHelper.scala index 5738e21..26ca69c 100644 --- a/src/main/scala/com/gilt/handlebars/scala/helper/EachHelper.scala +++ b/src/main/scala/com/gilt/handlebars/scala/helper/EachHelper.scala @@ -11,14 +11,20 @@ class EachHelper[T] extends Helper[T] { options.visit(value, Map( "key" -> contextFactory.bindPrimitive(key), - "index" -> contextFactory.bindPrimitive(idx))) + "index" -> contextFactory.bindPrimitive(idx)), + List( + value, + contextFactory.bindPrimitive(key))) }.mkString else if (arg0.isCollection) arg0.asCollection.zipWithIndex.map { case (value, idx) => options.visit(value, - Map( - "index" -> contextFactory.bindPrimitive(idx))) + Map[String, Binding[T]]( + "index" -> contextFactory.bindPrimitive(idx)), + List[Binding[T]]( + value, + contextFactory.bindPrimitive(idx))) }.mkString else { diff --git a/src/main/scala/com/gilt/handlebars/scala/helper/HelperOptionsBuilder.scala b/src/main/scala/com/gilt/handlebars/scala/helper/HelperOptionsBuilder.scala index 16cb7e0..7f6fbab 100644 --- a/src/main/scala/com/gilt/handlebars/scala/helper/HelperOptionsBuilder.scala +++ b/src/main/scala/com/gilt/handlebars/scala/helper/HelperOptionsBuilder.scala @@ -22,37 +22,27 @@ trait HelperOptions[T] { */ def data(key: String): Binding[T] - /** - * Evaluates the body of the helper using the provided binding as a context. - * @param binding the context for the body of the helper - * @return String result of evaluating the body. - */ - def visit(binding: Binding[T]): String - /** * Evaluates the body of the helper using the provided binding as a context as well as additional data to be combined * with the data provided by Handlebars.apply * @param binding the context for the body of the helper - * @param extraData data provided by the helper to be used while evaluating the body of the helper. + * @param optional extraData data provided by the helper to be used while evaluating the body of the helper. + * @param optional blockParamsBinding provided by the helper to be used while evaluating the body of the helper. * @return String result of evaluating the body. */ - def visit(binding: Binding[T], extraData: Map[String, Binding[T]]): String - - /** - * Evaluate the inverse of body of the helper using the provided binding as a context. - * @param binding the context for the inverse of the body of the helper - * @return String result of evaluating the body. - */ - def inverse(binding: Binding[T]): String + def visit( + binding: Binding[T], + extraData: Map[String, Binding[T]]=Map(), + blockParamsBinding: List[Binding[T]]=List()): String /** * Evaluates the inverse of the body of the helper using the provided binding as a context as well as additional data to * be combined with the data provided by Handlebars.apply * @param binding the context for the inverse of the body of the helper - * @param extraData data provided by the helper to be used while evaluating the inverse of the body of the helper. + * @param optional extraData data provided by the helper to be used while evaluating the inverse of the body of the helper. * @return String result of evaluating the inverse of the body. */ - def inverse(binding: Binding[T], extraData: Map[String, Binding[T]]): String + def inverse(binding: Binding[T], extraData: Map[String, Binding[T]]=Map()): String /** * Look up a path in the the current context. The one in which the helper was called. @@ -62,14 +52,19 @@ trait HelperOptions[T] { def lookup(path: String): Binding[T] val dataMap: Map[String, Binding[T]] + + val blockParams: List[String] } -class HelperOptionsBuilder[T](context: Context[T], - partials: Map[String, Handlebars[T]], - helpers: Map[String, Helper[T]], - data: Map[String, Binding[T]], - program: Node, - args: Seq[Binding[T]])(implicit contextFactory: BindingFactory[T]) extends Loggable { +class HelperOptionsBuilder[T]( + context: Context[T], + partials: Map[String, Handlebars[T]], + helpers: Map[String, Helper[T]], + data: Map[String, Binding[T]], + program: Node, + args: Seq[Binding[T]], + blockParams: List[String], + outerBlockParamsBinding: Map[String, Binding[T]])(implicit contextFactory: BindingFactory[T]) extends Loggable { private val inverseNode: Option[Node] = program match { case p:Program => p.inverse @@ -78,10 +73,11 @@ class HelperOptionsBuilder[T](context: Context[T], } def build: HelperOptions[T] = - new HelperOptionsImpl(args, data) + new HelperOptionsImpl(args, data, blockParams) private class HelperOptionsImpl(args: Seq[Binding[T]], - val dataMap: Map[String, Binding[T]]) extends HelperOptions[T] { + val dataMap: Map[String, Binding[T]], + val blockParams: List[String]) extends HelperOptions[T] { def argument(index: Int): Binding[T] = { args.lift(index) getOrElse VoidBinding[T] @@ -91,20 +87,32 @@ class HelperOptionsBuilder[T](context: Context[T], dataMap.get(key).getOrElse(VoidBinding[T]) } - def visit(binding: Binding[T]): String = visit(binding, Map.empty[String, Binding[T]]) - - def visit(binding: Binding[T], extraData: Map[String, Binding[T]]): String = { + def visit( + binding: Binding[T], + extraData: Map[String, Binding[T]]=Map(), + blockParamsBinding: List[Binding[T]]=List()): String = { val visitorContext = context.childContext(binding) - new DefaultVisitor(visitorContext, partials, helpers, dataMap ++ extraData).visit(program) + val innerBlockParamsBinding = outerBlockParamsBinding ++ blockParams.zip(blockParamsBinding).toMap + new DefaultVisitor( + visitorContext, + partials, + helpers, + dataMap ++ extraData, + innerBlockParamsBinding).visit(program) } - def inverse(binding: Binding[T]): String = inverse(binding, Map.empty[String, Binding[T]]) - - def inverse(binding: Binding[T], extraData: Map[String, Binding[T]]): String = { + def inverse( + binding: Binding[T], + extraData: Map[String, Binding[T]]=Map()): String = { inverseNode.map { node => val visitorContext = context.childContext(binding) - new DefaultVisitor(visitorContext, partials, helpers, dataMap ++ extraData).visit(node) + new DefaultVisitor( + visitorContext, + partials, + helpers, + dataMap ++ extraData, + Map.empty[String, Binding[T]]).visit(node) }.getOrElse { warn("No inverse node found for program: %s".format(program)) "" diff --git a/src/main/scala/com/gilt/handlebars/scala/helper/WithHelper.scala b/src/main/scala/com/gilt/handlebars/scala/helper/WithHelper.scala index 0cdb716..00600df 100644 --- a/src/main/scala/com/gilt/handlebars/scala/helper/WithHelper.scala +++ b/src/main/scala/com/gilt/handlebars/scala/helper/WithHelper.scala @@ -17,10 +17,9 @@ class WithHelper[T] extends Helper[T] with Loggable { def apply(binding: Binding[T], options: HelperOptions[T])(implicit c: BindingFactory[T]): String = { val arg = options.argument(0) if (!arg.isDefined) { - warn("No context provided for with helper") - "" + options.inverse(binding) } else { - options.visit(arg) + options.visit(arg, blockParamsBinding=List(arg)) } } } diff --git a/src/main/scala/com/gilt/handlebars/scala/parser/HandlebarsGrammar.scala b/src/main/scala/com/gilt/handlebars/scala/parser/HandlebarsGrammar.scala index 9877ab6..069ee64 100644 --- a/src/main/scala/com/gilt/handlebars/scala/parser/HandlebarsGrammar.scala +++ b/src/main/scala/com/gilt/handlebars/scala/parser/HandlebarsGrammar.scala @@ -50,18 +50,18 @@ class HandlebarsGrammar(delimiters: (String, String)) extends JavaTokenParsers { def inverseBlock = blockify("^") ^^ { case (stache, Some(prog)) => Block(stache, prog.inverse.getOrElse(Program(Nil)), Some(prog)) - case (stache, None) => Block(stache, Program(Nil), None) + case (stache, None) => Block(stache, Program(Nil)) } def block = blockify("#") ^^ { case (stache, Some(prog)) => Block(stache, prog, prog.inverse) - case (stache, None) => Block(stache, Program(Nil), None) + case (stache, None) => Block(stache, Program(Nil)) } def mustache: Parser[Mustache] = { mustachify(pad(inMustache)) ^^ { mustacheable(_) } | - mustachify("&" ~> pad(inMustache)) ^^ { mustacheable(_, true) } | - mustachify("{" ~> pad(inMustache) <~ "}") ^^ { mustacheable(_, true) } + mustachify("&" ~> pad(inMustache)) ^^ { mustacheable(_, unescape=true) } | + mustachify("{" ~> pad(inMustache) <~ "}") ^^ { mustacheable(_, unescape=true) } } def partial: Parser[Partial] = mustachify(">" ~> pad( partialName ~ opt(whiteSpace ~> path) )) ^^ { @@ -69,28 +69,26 @@ class HandlebarsGrammar(delimiters: (String, String)) extends JavaTokenParsers { } def inMustache: Parser[(IdentifierNode, List[Either[Mustache, ValueNode]], Option[HashNode])] = { - path ~ params ~ hash ^^ { - case (id ~ params ~ hash) => (id, params, Some(hash)) + path ~ opt(params) ~ opt(hash) ^^ { + case (id ~ params ~ hash) => + (id, params.getOrElse(Nil), hash) } | - path ~ hash ^^ { - case (id ~ hash) => (id, Nil, Some(hash)) - } | - path ~ params ^^ { - case (id ~ params) => (id, params, None) - } | - path ^^ { (_ , Nil, None) } | dataName ^^ { (_ , Nil, None) } | failure("Invalid Mustache") } - def params = rep1(whiteSpace ~> paramOrNested) + def params = rep1(whiteSpace ~> not(AS) ~> paramOrNested) + + def blockParams: Parser[BlockParams] = pad(AS ~> "|" ~> opt(whiteSpace) ~> rep1(ID <~ opt(whiteSpace)) <~ "|") ^^ { + BlockParams(_) + } def hash = rep1(whiteSpace ~> hashSegment) ^^ { pairs:List[(String, ValueNode)] => HashNode(pairs.toMap) } def hashSegment = (ID ~ EQUALS ~ param) ^^ { - case (i ~ _ ~ p) => Pair(i, p) + case (i ~ _ ~ p) => (i, p) } def partialName = (path | STRING | INTEGER) ^^ { PartialName(_) } @@ -115,7 +113,7 @@ class HandlebarsGrammar(delimiters: (String, String)) extends JavaTokenParsers { def comment = mustachify("!" ~> CONTENT) ^^ { Comment(_) } - def blockify(prefix: Parser[String]): Parser[Pair[Mustache, Option[Program]]] = { + def blockify(prefix: Parser[String]): Parser[(Mustache, Option[Program])] = { blockstache(prefix) ~ opt(program) ~ mustachify("/" ~> pad(path)) >> { case (mustache ~ _ ~ close) if close != mustache.path => failure(mustache.path.string + " doesn't match " + close.string) @@ -123,16 +121,15 @@ close.string) } } - def blockstache(prefix: Parser[String]) = mustachify(prefix ~> pad(inMustache)) ^^ { - mustacheable(_) + def blockstache(prefix: Parser[String]): Parser[Mustache] = mustachify(prefix ~> pad(inMustache) ~ opt(blockParams)) ^^ { + case tuple ~ blockParams => mustacheable(tuple, blockParams) } - def mustacheable(tuple: (IdentifierNode, List[Either[Mustache, ValueNode]], Option[HashNode]), - unescape: Boolean = false): Mustache = { - tuple match { - case (id, params, Some(hash)) => Mustache(id, params, hash, unescape) - case (id, params, None) => Mustache(id, params, unescaped = unescape) - } + def mustacheable( + tuple: (IdentifierNode, List[Either[Mustache, ValueNode]], Option[HashNode]), + blockParams: Option[BlockParams]=None, + unescape: Boolean=false): Mustache = tuple match { + case (id, params, hash) => Mustache(id, params, hash, blockParams, unescape) } def mustachify[T](parser: Parser[T]): Parser[T] = OPEN ~> parser <~ CLOSE @@ -150,7 +147,7 @@ close.string) val EQUALS = "=" - val ID = """[^\s!"#%-,\.\/;->@\[-\^`\{-~]+""".r | ("[" ~> """[^\]]*""".r <~ "]") | ident + val ID = not(AS) ~> """[^\s!"#%-,\.\/;->@\[-\^`\{-~]+""".r | ("[" ~> """[^\]]*""".r <~ "]") | ident val SEPARATOR = "/" | "." @@ -164,5 +161,7 @@ close.string) val ESCAPE = "\\" + val AS = opt(whiteSpace) ~ "as" ~ whiteSpace + val CONTENT = rep1((ESCAPE ~> (OPEN | CLOSE) | not(OPEN | CLOSE) ~> ".|\r|\n".r)) ^^ { t => t.mkString("") } } diff --git a/src/main/scala/com/gilt/handlebars/scala/parser/Node.scala b/src/main/scala/com/gilt/handlebars/scala/parser/Node.scala index de5d57b..2e6a174 100644 --- a/src/main/scala/com/gilt/handlebars/scala/parser/Node.scala +++ b/src/main/scala/com/gilt/handlebars/scala/parser/Node.scala @@ -20,11 +20,26 @@ case class Program(statements: Seq[Node], inverse: Option[Program] = None) exten case class Mustache(path: IdentifierNode, params: Seq[Either[Mustache, ValueNode]] = Nil, hash: HashNode = HashNode(Map.empty), + blockParams: BlockParams=BlockParams(), unescaped: Boolean = false) extends Node { val eligibleHelper: Boolean = path.isSimple val isHelper: Boolean = eligibleHelper && params.nonEmpty } +object Mustache { + def apply( + path: IdentifierNode, + params: Seq[Either[Mustache, ValueNode]], + hash: Option[HashNode], + blockParams: Option[BlockParams], + unescaped: Boolean): Mustache = Mustache( + path, + params, + hash.getOrElse(HashNode(Map.empty)), + blockParams.getOrElse(BlockParams()), + unescaped) +} + case class Partial(name: PartialName, context: Option[Identifier] = None) extends Node case class Block(mustache: Mustache, @@ -65,3 +80,6 @@ case class Comment(value: String) extends ValueNode { type T = String } +case class BlockParams(value: List[String]=List()) extends ValueNode { + type T = List[String] +} diff --git a/src/main/scala/com/gilt/handlebars/scala/visitor/DefaultVisitor.scala b/src/main/scala/com/gilt/handlebars/scala/visitor/DefaultVisitor.scala index 1585754..25a5cdf 100644 --- a/src/main/scala/com/gilt/handlebars/scala/visitor/DefaultVisitor.scala +++ b/src/main/scala/com/gilt/handlebars/scala/visitor/DefaultVisitor.scala @@ -8,8 +8,13 @@ import com.gilt.handlebars.scala.logging.Loggable import com.gilt.handlebars.scala.parser._ object DefaultVisitor { - def apply[T](base: Context[T], partials: Map[String, Handlebars[T]], helpers: Map[String, Helper[T]], data: Map[String, Binding[T]])(implicit bindingFactory: BindingFactory[T]) = { - new DefaultVisitor(base, partials, helpers, data) + def apply[T]( + base: Context[T], + partials: Map[String, Handlebars[T]], + helpers: Map[String, Helper[T]], + data: Map[String, Binding[T]], + blockParamsBinding: Map[String, Binding[T]])(implicit bindingFactory: BindingFactory[T]) = { + new DefaultVisitor(base, partials, helpers, data, blockParamsBinding) } private val escChars = "<>\"&" @@ -36,7 +41,13 @@ object DefaultVisitor { } } -class DefaultVisitor[T](context: Context[T], partials: Map[String, Handlebars[T]], helpers: Map[String, Helper[T]], data: Map[String, Binding[T]])(implicit val contextFactory: BindingFactory[T]) extends Visitor with Loggable { +class DefaultVisitor[T]( + context: Context[T], + partials: Map[String, Handlebars[T]], + helpers: Map[String, Helper[T]], + data: Map[String, Binding[T]], + blockParamsBinding: Map[String, Binding[T]])(implicit val contextFactory: BindingFactory[T]) extends Visitor with Loggable { + def visit(node: Node): String = { node match { case c: Content => visit(c) @@ -61,26 +72,29 @@ class DefaultVisitor[T](context: Context[T], partials: Map[String, Handlebars[T] def visit(mustache: Mustache): String = { // I. There is no hash present on this {{mustache}} - lazy val paramsList = mustache.params.map{ case Left(n: Mustache) => contextFactory.bindPrimitive(visit(n)) case Right(valueNode) => valueNodeToBindings(valueNode) }.toList + lazy val paramsMap = valueHashToBindingMap(mustache.hash) if (mustache.hash.value.isEmpty) { - // 1. Check if path refers to a helper - val value = helpers.get(mustache.path.string).map { - callHelper(_, mustache, paramsList) + // 1. Check if path refers to blockParams + val value = Binding.mapTraverse(mustache.path.value, blockParamsBinding).asOption.map(_.render).orElse { + // 2. Check if path refers to a helper + helpers.get(mustache.path.string).map { + callHelper(_, mustache, paramsList, mustache.blockParams) + } }.orElse { - // 2. Check if path exists directly in the context + // 3. Check if path exists directly in the context context.lookup(mustache.path, paramsList).asOption.map(_.render) }.orElse { - // 3. Check if path refers to provided data. + // 4. Check if path refers to provided data. data.get(mustache.path.string).map(_.render) }.getOrElse { - // 4. Could not find path in context, helpers or data. + // 5. Could not find path in context, helpers or data. warn(s"Could not find path or helper: ${mustache.path}, context: $context") "" } @@ -89,7 +103,7 @@ class DefaultVisitor[T](context: Context[T], partials: Map[String, Handlebars[T] } else { // II. There is a hash on this {{mustache}}. Start over with the hash information added to 'data'. All of the // data in the hash will be accessible to any child nodes of this {{mustache}}. - new DefaultVisitor(context, partials, helpers, data ++ paramsMap).visit(mustache.copy(hash = HashNode(Map.empty))) + new DefaultVisitor(context, partials, helpers, data ++ paramsMap, blockParamsBinding).visit(mustache.copy(hash = HashNode(Map.empty))) } } @@ -106,7 +120,7 @@ class DefaultVisitor[T](context: Context[T], partials: Map[String, Handlebars[T] val lookedUpCtx = context.lookup(block.mustache.path) // 1. Check if path refers to a helper helpers.get(block.mustache.path.string).map { - callHelper(_, block.program, paramsList) + callHelper(_, block.program, paramsList, block.mustache.blockParams) }.orElse { // 2. Check if path exists directly in the context lookedUpCtx.asOption.map { @@ -120,8 +134,8 @@ class DefaultVisitor[T](context: Context[T], partials: Map[String, Handlebars[T] } else { // II. There is a hash on this block. Start over with the hash information added to 'data'. All of the // data in the hash will be accessible to any child nodes of this block. - def blockWithoutHash = block.copy(mustache = block.mustache.copy(hash = HashNode(Map.empty))) - new DefaultVisitor(context, partials, helpers, data ++ paramsMap).visit(blockWithoutHash) + def blockWithoutHash = block.copy(mustache = block.mustache.copy(hash=HashNode(Map.empty))) + new DefaultVisitor(context, partials, helpers, data ++ paramsMap, blockParamsBinding).visit(blockWithoutHash) } } @@ -174,15 +188,23 @@ class DefaultVisitor[T](context: Context[T], partials: Map[String, Handlebars[T] protected def renderBlock(ctx: Context[T], program: Program, inverse: Option[Program]): String = { if (ctx.truthValue) { ctx.map { (itemContext, idx) => - new DefaultVisitor(itemContext, partials, helpers, data ++ (idx.map { "index" -> contextFactory.bindPrimitive(_) })).visit(program) + new DefaultVisitor(itemContext, partials, helpers, data ++ (idx.map { "index" -> contextFactory.bindPrimitive(_) }), blockParamsBinding).visit(program) }.mkString } else { inverse.map(visit).getOrElse("") } } - protected def callHelper(helper: Helper[T], program: Node, params: Seq[Binding[T]]): String = { - def optionsBuilder = new HelperOptionsBuilder[T](context, partials, helpers, data, program, params) + protected def callHelper(helper: Helper[T], program: Node, params: Seq[Binding[T]], blockParams: BlockParams): String = { + def optionsBuilder = new HelperOptionsBuilder[T]( + context, + partials, + helpers, + data, + program, + params, + blockParams.value, + blockParamsBinding) helper.apply(context.binding, optionsBuilder.build) } } diff --git a/src/test/scala/com/gilt/handlebars/scala/parser/HandlebarsGrammarSpec.scala b/src/test/scala/com/gilt/handlebars/scala/parser/HandlebarsGrammarSpec.scala index 9c25519..6631bf2 100644 --- a/src/test/scala/com/gilt/handlebars/scala/parser/HandlebarsGrammarSpec.scala +++ b/src/test/scala/com/gilt/handlebars/scala/parser/HandlebarsGrammarSpec.scala @@ -372,6 +372,16 @@ class HandlebarsGrammarSpec extends FunSpec with Matchers with ParserMatchers { } } + it("parses block with block params") { + parsers("{{#foo as |bar baz|}}content{{/foo}}") should succeedWithResult { + Program(List( + Block( + Mustache(Identifier(List("foo")), blockParams=BlockParams(List("bar", "baz"))), + Program(List(Content("content"))), + None))) + } + } + it("is unsuccessful if there's a Parse error") { // The reference implementation raises an exception on a parse failure, // but I prefer to use the Parsing lib's solution of a NoSuccess. diff --git a/src/test/scala/com/gilt/handlebars/scala/visitor/BuiltInHelperSpec.scala b/src/test/scala/com/gilt/handlebars/scala/visitor/BuiltInHelperSpec.scala index dbbdb50..e07398c 100644 --- a/src/test/scala/com/gilt/handlebars/scala/visitor/BuiltInHelperSpec.scala +++ b/src/test/scala/com/gilt/handlebars/scala/visitor/BuiltInHelperSpec.scala @@ -31,6 +31,18 @@ class BuiltInHelperSpec extends FunSpec with Matchers { Handlebars(template).apply(ctx) should equal("Alan Johnson") } + it("with with else") { + val template = "{{#with person}}Person is present{{else}}Person is not present{{/with}}" + Handlebars(template).apply(new {}) should equal("Person is not present") + } + + it("with provides block parameter") { + case class Person(first: String, last: String) + case class Ctx(person: Person) + val template = "{{#with person as |foo|}}{{foo.first}} {{last}}{{/with}}" + Handlebars(template).apply(Ctx(Person("Alan", "Johnson"))) should equal("Alan Johnson") + } + it("if") { case class Goodbye(goodbye: Any, world: String) case class World(world: String) @@ -84,6 +96,14 @@ class BuiltInHelperSpec extends FunSpec with Matchers { Handlebars(template).apply(ctx) should equal("0. goodbye! 1. Goodbye! 2. GOODBYE! cruel world!") } + it("each with block params") { + val template = "{{#each goodbyes as |value index|}}{{index}}. {{value.text}}! {{#each ../goodbyes as |childValue childIndex|}} {{index}} {{childIndex}}{{/each}} After {{index}} {{/each}}{{index}}cruel {{world}}!" + val ctx = Map("goodbyes" -> List(Map("text" -> "goodbye"), Map("text" -> "Goodbye")), "world" -> "world") + val result = Handlebars(template).apply(ctx) + + result should equal("0. goodbye! 0 0 0 1 After 0 1. Goodbye! 1 0 1 1 After 1 cruel world!") + } + it("data passed to helpers") { case class Ctx(letters: List[String]) val template = "{{#each letters}}{{this}}{{detectDataInsideEach}}{{/each}}" @@ -395,6 +415,75 @@ class BuiltInHelperSpec extends FunSpec with Matchers { builder.build(Ctx("goodbye", "world")) should equal("GOODBYE cruel WORLD goodbye") } + describe("block params") { + it("should take presedence over context values") { + case class Ctx(value: String) + val template = "{{#goodbyes as |value|}}{{value}}{{/goodbyes}}{{value}}" + val helpers = Map( + "goodbyes" -> Helper[Any] { + (_, options) => { + options.visit(Map("value" -> "bar"), blockParamsBinding=List(1, 2)) + } + } + ) + val builder = Handlebars.createBuilder(template).withHelpers(helpers) + builder.build(Ctx("foo")) should equal("1foo") + } + + it("should take precedence over helper values") { + val template = "{{#goodbyes as |value|}}{{value}}{{/goodbyes}}{{value}}" + val helpers = Map( + "value" -> Helper[Any] { + (_, _) => "foo" + }, + "goodbyes" -> Helper[Any] { + (_, options) => { + options.visit(Map(), blockParamsBinding=List(1, 2)) + } + } + ) + val builder = Handlebars.createBuilder(template).withHelpers(helpers) + builder.build(new {}) should equal("1foo") + } + + it("should not take precedence over pathed values") { + case class Ctx(value: String) + val template = "{{#goodbyes as |value|}}{{./value}}{{/goodbyes}}{{value}}" + val helpers = Map( + "value" -> Helper[Any] { + (_, _) => "foo" + }, + "goodbyes" -> Helper[Any] { + (context, options) => { + options.visit(context, blockParamsBinding=List(1, 2)) + } + } + ) + val builder = Handlebars.createBuilder(template).withHelpers(helpers) + builder.build(Ctx("bar")) should equal("barfoo") + } + + it("should take precedence over parent block params") { + case class Ctx(value: String) + var value = 1 + val template = "{{#goodbyes as |value|}}{{#goodbyes}}{{value}}{{#goodbyes as |value|}}{{value}}{{/goodbyes}}{{/goodbyes}}{{/goodbyes}}{{value}}" + val helpers = Map( + "goodbyes" -> Helper[Any] { + (_, options) => { + options.visit( + Map("value" -> "bar"), + blockParamsBinding=(if (options.blockParams.length == 1) { + value += 2 + List(value - 2, value - 1) + } else Nil)) + } + } + ) + val builder = Handlebars.createBuilder(template).withHelpers(helpers) + builder.build(Ctx("foo")) should equal("13foo") + } + } + it("helpers can take a nested helper") { case class Ctx(worldKey: String) val template = "goodbye {{cruel ( world ) }}"