Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add block params support and tests #69

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/main/scala/com/gilt/handlebars/scala/Handlebars.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand Down
12 changes: 9 additions & 3 deletions src/main/scala/com/gilt/handlebars/scala/helper/EachHelper.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -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]
Expand All @@ -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))
""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,47 +50,45 @@ 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) )) ^^ {
case (name ~ contextOpt) => Partial(name, contextOpt)
}

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(_) }
Expand All @@ -115,24 +113,23 @@ 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)
case (mustache ~ programOpt ~ _) => success((mustache, programOpt))
}
}

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
Expand All @@ -150,7 +147,7 @@ close.string)

val EQUALS = "="

val ID = """[^\s!"#%-,\.\/;->@\[-\^`\{-~]+""".r | ("[" ~> """[^\]]*""".r <~ "]") | ident
val ID = not(AS) ~> """[^\s!"#%-,\.\/;->@\[-\^`\{-~]+""".r | ("[" ~> """[^\]]*""".r <~ "]") | ident

val SEPARATOR = "/" | "."

Expand All @@ -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("") }
}
18 changes: 18 additions & 0 deletions src/main/scala/com/gilt/handlebars/scala/parser/Node.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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]
}
Loading