Giter Site home page Giter Site logo

handlebars.scala's Introduction

A Scala implementation of Handlebars, an extension to and superset of the Mustache templating language. Currently implements version 1.0.0 of the JavaScript version.

This project began as an attempt to learn Scala and to experiment with Scala's Parser Combinators in an attempt to get handlebars.js templates working in Scala.

Installation

If you're using SBT you can add this line to your build.sbt file.

libraryDependencies += "com.gilt" %% "handlebars-scala" % "2.1.1"

Usage

Given a template:

val template = """
  <p>Hello, my name is {{name}}. I am from {{hometown}}. I have {{kids.length}} kids:</p>
  <ul>
    {{#kids}}<li>{{name}} is {{age}}</li>{{/kids}}
  </ul>
"""

And an arbitrary Scala object:

object Guy {
  val name = "Alan"
  val hometown = "Somewhere, TX"
  val kids = Seq(Map(
    "name" -> "Jimmy",
    "age" -> "12"
  ), Map(
    "name" -> "Sally",
    "age" -> "4"
  ))
}

Pass those into Handlebars like so:

scala> import com.gilt.handlebars.scala.binding.dynamic._
import com.gilt.handlebars.scala.binding.dynamic._

scala> import com.gilt.handlebars.scala.Handlebars
import com.gilt.handlebars.scala.Handlebars

scala> val t = Handlebars(template)
t: com.gilt.handlebars.scala.Handlebars = com.gilt.handlebars.scala.Handlebars@496d864e

scala> t(Guy)
res0: String =
"
      <p>Hello, my name is Alan. I am from Somewhere, TX. I have 2 kids:</p>
      <ul>
        <li>Jimmy is 12</li><li>Sally is 4</li>
      </ul>
    "

Handlebars.scala will work just fine for Mustache templates, but includes features such as Paths and Helpers.

The example above demonstrates the apply method of a Handlebars instance, which should be familiar to Scala-fans. apply takes additional optional arguments:

  • data additional custom data to be referenced by @variable private variables.
  • partials custom partials in addition to the globally defined ones. These partials will override the globally provided ones.
  • helpers custom helpers in addition to the globally defined ones. These helpers will override the globally provided ones.

The signature for apply looks like this:

def apply[T](context: T,
      data: Map[String, Binding[T]] = Map.empty,
      partials: Map[String, Handlebars[T]] = Map.empty,
      helpers: Map[String, Helper[T]] = Map.empty): String

Bindings

In order to facilitate multiple ways of interacting with data, Handlebars provides a data-binding facility. Handlebars ships with a default binding strategy, DynamicBinding, which uses Scala reflection to work with scala standard-library data structures and primitives. You can implement your own Binding strategies by implementing the following traits:

  • com.gilt.handlebars.scala.binding.FullBinding
  • com.gilt.handlebars.scala.binding.BindingFactory

Provide the implicit BindingFactory which uses your new binding. If you need an example, see the source code in binding/dynamic/DynamicBinding.scala.

Binding Interface

  • def get: T - Retrieve the contents of the binding. Throws runtime exception if in the void.

  • def getOrElse(default: => T): T - Get the contents of the binding if full; else is VoidBinding return default

  • def toOption: Option[T] - Similar to the Option constructor, returns Some(value) where value is defined

  • def render: String - Returns a string representation for the object, returning empty string where value is not defined.

  • def traverse(key: String, args: List[Binding[T]] = List.empty): Binding[T] For dictionaries / objects, traverse into named key, returning a binding for the matched value, VoidBinding if key is not declared.

    Important! Take note of the difference here:

    val binding = DynamicBinding(Map("a" -> null))
    binding.traverse("a") // => DynamicBinding(null)
    binding.traverse("b") // => VoidBinding
  • def isTruthy: Boolean - Returns whether the bound value evaluate to truth in handlebars if expressions?

  • def isCollection: Boolean - Returns whether the bound value is an iterable (and not a dictionary)

  • def isDictionary: Boolean - Returns whether the bound value is a dictionary

  • def isPrimitive - Returns whether bound value is neither collection or dictionary

  • def asOption: Option[Binding[T]] - If value is defined, returns Some(this), else None

  • def asCollection: Iterable[Binding[T]] - Returns List of bindings if isCollection; else empty List

  • def asDictionaryCollection: Iterable[(String, Binding[T])] - returns List of key-value tuples if isDictionary; else empty list

Unit vs null vs None vs VoidBinding

In order to preserve the signal of "a value was defined in your model", vs., "you traversed outside the space covered by your model", bindings are monadic and capture whether they've a value from your model or not: a FullBinding if bound against a value from your model, a VoidBinding is you traversed outside the space of your model.

If the model contained a null, Unit, or None

isDefined is true if the bound value is within the space of the model, and it evaluates to some value other than . VoidBinding is, naturally, never defined, and always returns isDefined as false.

Pattern matching value extraction

You can extract the bound value by matching FullBinding, like so:

DynamicBinding(1) match {
  case FullBinding(value) => value
  case VoidBinding => Unit
}

Helpers

The trait for a helper looks like this:

trait Helper[Any] {
  def apply(binding: Binding[Any], options: HelperOptions[Any]): String
}
  • binding the binding for the model in the context from which the helper was called.
  • options provides helper functions to interact with the context and evaluate the body of the helper, if present.

You can define a new helper by extending the trait above, or you can use companion obejct apply method to define one on the fly:

val fullNameHelper = Helper[Any] {
  (binding, options) =>
    "%s %s".format(options.lookup("firstName").renderString, options.lookup("lastName").renderString)
}

If you know that the information you need is on binding, you can do the same thing by accessing first and last name on the data directly. However, you will be responsible for casting model to the correct type.

val fullNameHelper = Helper[Any] {
  (binding, options) =>
    val person = binding.get.asInstanceOf[Person]
    "%s %s".format(person.firstName, person.lastName)
}

HelperOptions

The HelperOption[T] object gives you the tools you need to get things done in your helper. The primary methods are:

  • def argument(index: Int): Binding[T] Retrieve an argument from the list provided to the helper by its index.
  • def data(key: String): Binding[T] Retrieve data provided to the Handlebars template by its key.
  • def visit(binding: Binding[T]): String Evaluates the body of the helper using a context with the provided binding.
  • def inverse(binding: Binding[T]): String Evaluate the inverse of body of the helper using the provided model as a context.
  • def lookup(path: String): Binding[T] Look up a value for a path in the the current context. The one in which the helper was called.

Caveats when using DynamicBinding

Implicit conversions will not work in a template. Because Handlebars.scala makes heavy use of reflection. Bummer, I know. This leads me too...

Handlebars.scala makes heavy use of reflection. This means that there could be unexpected behavior. Method overloading will behave in bizarre ways. There is likely a performance penalty. I'm not sophisticated enough in the arts of the JVM to know the implications of this.

Not everything from the JavaScript handlebars is supported. See NOTSUPPORTED for a list of the unsupported features. There are some things JavaScript can do that simply does not make sense to do in Scala.

Play-Json integration

If you wish for more type-safety, Handlebars-scala comes with integration for play-json. Using PlayJson AST data structures with handlebars provides identical truthy / collection / traversal behavior that you would find using JavaScript values in handlebars-js.

To use:

libraryDependencies += "com.gilt" %% "handlebars-scala-play-json" % "2.1.1"

Example / usage

Thanks

Special thanks to the fine folks working on Scalate whose Mustache parser was my primary source of inspiration. Tom Dale and Yehuda Katz who inceptioned the idea of writing a Handlebars implementation for the JVM. The UI team at Gilt who insisted on using Handlebars and not Mustache for client-side templating. And finally, the denizens of the Scala 2.9.1 chat room at Gilt for answering my questions with enthusiastic aplomb.

Build

The project uses sbt. Assuming you have sbt you can clone the repo, and run:

sbt test

Build Status

Copyright and License

Copyright 2014 Mark Wunsch and Gilt Groupe, Inc.

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

handlebars.scala's People

Contributors

adrianlyjak avatar chrhicks avatar craftybeaver avatar dgouyette avatar fbjon avatar jm-agrimap avatar mwunsch avatar ntbrock avatar theshortcut avatar timcharper avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

handlebars.scala's Issues

Do you need a new maintainer?

I'm noticing that a lot of the pull-requests and issues are building up. Do you need a new maintainer for this project?

(I mean this in the kindest way possible, having been myself guilty of neglecting pull-requests on my projects at times)

HelperOptions arguments list

HelperOptions[T] trait provides the argument method, which returns a single argument value. Is there any way to get a list of all argument values?

My specific use-case is building an i18n helper method which will accept a key, a language, and a variable list of arguments to substitute in the message.

Strong performance issues on Scala 2.11.4/Google App Engine (any restricted environments?)

I upgraded my app from Scala 2.10 to 2.11 and found that now it's working much more slowly.

For example for a template like Hello {{name}}, You have just won ${{value}}! it takes about 300 ms now. And for my real templates (~500 lines with a few simple placeholders) it takes about ~1 min.
It's really slow now for template parsing (not for rendering, I'd checked it).

Is it only for me or something happened (changed) with 2.11 parsers?

Handlebars.apply on empty string results in parse error

If you pass an empty string to Handlebars.apply, it results in a parse error:

scala> import com.gilt.handlebars.Handlebars
import com.gilt.handlebars.Handlebars

scala> Handlebars("")
java.lang.RuntimeException: Could not parse template:


scala.util.parsing.input.CharSequenceReader@673c8b91
        at scala.sys.package$.error(package.scala:27)
        at com.gilt.handlebars.parser.ProgramHelper$$anonfun$programFromString$1.apply(ProgramHelper.scala:12)
        at com.gilt.handlebars.parser.ProgramHelper$$anonfun$programFromString$1.apply(ProgramHelper.scala:12)
        at scala.util.parsing.combinator.Parsers$ParseResult.getOrElse(Parsers.scala:123)
        at com.gilt.handlebars.parser.ProgramHelper$class.programFromString(ProgramHelper.scala:11)
        at com.gilt.handlebars.DefaultHandlebarsBuilder$.programFromString(HandlebarsBuilder.scala:32)
        at com.gilt.handlebars.DefaultHandlebarsBuilder$.apply(HandlebarsBuilder.scala:34)
        at com.gilt.handlebars.Handlebars$.createBuilder(Handlebars.scala:40)
        at com.gilt.handlebars.Handlebars$.apply(Handlebars.scala:37)

Partial with argument

Hello,

Is there a way to use partial with arguments ?

I tried with the following syntax :

{{> header scheduling = 1}}

And i got the error msg :

[2.39] failure:}}' expected but =' found {{> header scheduling = 1}}

this error msg is stored in parseResult.msg but it's not displayed.
It would be great to display the complete error msg.
I can provide a PR if you want

Way to access visitor errors

In the current implementation, errors are logged using slf4j. For example, if a value cannot be found in the context, it is just omitted from the result. I would like to obtain a list of the values that could not be found.

What would be the best way to implement this in your opinion?

let's tune performance

Is there some comparison/benchmarking results? I see, benchmarking is very subjective, but being accompanied with test conditions they have some sense :) So, some simple comparisons with, say, freemarker and scalate would be informative.

Type safe compilation of templates

handlebars.scala is rather slow in environments where performance is critical. The default behavior is to use reflection on the fly to traverse the context given to the template. It would be great if we knew the type of the context before hand we could resolve errors at compile time and make rendering really fast!

Remove slf4j-simple dependency

The handlebars-scala project depends on both slf4j-api and slf4j-simple. The latter is an SLF4J binding. This means that any project depending on handlebars-scala transitively depends on slf4j-simple, which conflicts with any other SLF4J binding they might want to use (e.g., Logback, Log4J).

The recommended approach for libraries is to depend on slf4j-api only. Could you remove the slf4j-simple dependency? If you need it for tests, you can always mark it with the test scope.

Problem multiple Objects

I've got this Template:

{{#ScreenProperties}}
{
    "controllerName":"{{&AppName}}.controller.{{&name}}",
    "height": "100%",
        "width" : "100%",
    "content": [{
        "title":"{{&name}}",
                "showHeader" : {{&showHeader}},
        "content":[
                {{& ScreenContent}}
         ]
    }]
}
{{/ScreenProperties}}

and this is my JSON:

{
  "AppName": "Demo1",
  "ScreenContent": "Hello World",
  "ScreenProperties": {
    "name": "Screen1",
    "showHeader": true
  }
}

When I try to fill my Template, this is the result:

{
    "controllerName": ".controller.Screen1",
    "height": "100%",
    "width": "100%",
    "content": [
        {
            "title": "Screen1",
            "showHeader": true,
            "content": []
        }
    ]
}

But when I try to fill it via Try Mustache, it will give me the right Result.
Why does the behavior differ in this context?

PS: I know when my Template would look like that:

{
    "controllerName":"{{&AppName}}.controller.{{#ScreenProperties}}{{&name}}",
    "height": "100%",
        "width" : "100%",
    "content": [{
        "title":"{{&name}}",
                "showHeader" : {{&showHeader}},
                 {{/ScreenProperties}}
        "content":[
                {{& ScreenContent}}
         ]
    }]
}

it would work.

PS²:
Well, I got the reason, this post can be closed.

How to get PRs merged

Seems like this project is dead? We would love to get this PR merged (or commented on) #57, because then we don't need to maintain an internal forked version. What can we do to speed up the process? Do you need more maintainers?

Scala 2.11.0

I absolutely love Scandlebars but I can't get it to compile using the latest Scala version 2.11.0 (getting a lot of errors). Any chance it'll get an upgrade?

Readme typo

Looks like a typo in the documentation:

scala> import com.gilt.handlebars.scala.scala.binding.dynamic._
<console>:14: error: object scala is not a member of package com.gilt.handlebars.scala
       import com.gilt.handlebars.scala.scala.binding.dynamic._
scala> import com.gilt.handlebars.scala.binding.dynamic._
import com.gilt.handlebars.scala.binding.dynamic._

THis is not working

Are you sure the example provided is correct, I am getting following error

java.lang.NoSuchMethodError: play.api.libs.json.JsLookup$.$bslash$extension(Lplay/api/libs/json/JsLookupResult;Ljava/lang/String;)Lplay/api/libs/json/JsLookupResult;
at com.gilt.handlebars.scala.binding.playjson.PlayJsonBinding.traverse(PlayJsonBinding.scala:42)
at com.gilt.handlebars.scala.context.Context$class.lookup(Context.scala:73)
at com.gilt.handlebars.scala.context.RootContext.lookup(Context.scala:14)
at com.gilt.handlebars.scala.context.Context$class.lookup(Context.scala:34)
at com.gilt.handlebars.scala.context.RootContext.lookup(Context.scala:14)
at com.gilt.handlebars.scala.visitor.DefaultVisitor$$anonfun$2.apply(DefaultVisitor.scala:74)
at com.gilt.handlebars.scala.visitor.DefaultVisitor$$anonfun$2.apply(DefaultVisitor.scala:74)
at scala.Option.orElse(Option.scala:289)
at com.gilt.handlebars.scala.visitor.DefaultVisitor.visit(DefaultVisitor.scala:72)
at com.gilt.handlebars.scala.visitor.DefaultVisitor.visit(DefaultVisitor.scala:45)
at com.gilt.handlebars.scala.visitor.DefaultVisitor$$anonfun$visit$1.apply(DefaultVisitor.scala:54)
at com.gilt.handlebars.scala.visitor.DefaultVisitor$$anonfun$visit$1.apply(DefaultVisitor.scala:54)
at scala.collection.immutable.List.foreach(List.scala:381)
at com.gilt.handlebars.scala.visitor.DefaultVisitor.visit(DefaultVisitor.scala:54)
at com.gilt.handlebars.scala.HandlebarsImpl.apply(Handlebars.scala:39)
... 43 elided

THis is not working

Are you sure the example provided is correct, I am getting following error when using : handlebars-play-json

java.lang.NoSuchMethodError: play.api.libs.json.JsLookup$.$bslash$extension(Lplay/api/libs/json/JsLookupResult;Ljava/lang/String;)Lplay/api/libs/json/JsLookupResult;
at com.gilt.handlebars.scala.binding.playjson.PlayJsonBinding.traverse(PlayJsonBinding.scala:42)
at com.gilt.handlebars.scala.context.Context$class.lookup(Context.scala:73)
at com.gilt.handlebars.scala.context.RootContext.lookup(Context.scala:14)
at com.gilt.handlebars.scala.context.Context$class.lookup(Context.scala:34)
at com.gilt.handlebars.scala.context.RootContext.lookup(Context.scala:14)
at com.gilt.handlebars.scala.visitor.DefaultVisitor$$anonfun$2.apply(DefaultVisitor.scala:74)
at com.gilt.handlebars.scala.visitor.DefaultVisitor$$anonfun$2.apply(DefaultVisitor.scala:74)
at scala.Option.orElse(Option.scala:289)
at com.gilt.handlebars.scala.visitor.DefaultVisitor.visit(DefaultVisitor.scala:72)
at com.gilt.handlebars.scala.visitor.DefaultVisitor.visit(DefaultVisitor.scala:45)
at com.gilt.handlebars.scala.visitor.DefaultVisitor$$anonfun$visit$1.apply(DefaultVisitor.scala:54)
at com.gilt.handlebars.scala.visitor.DefaultVisitor$$anonfun$visit$1.apply(DefaultVisitor.scala:54)
at scala.collection.immutable.List.foreach(List.scala:381)
at com.gilt.handlebars.scala.visitor.DefaultVisitor.visit(DefaultVisitor.scala:54)
at com.gilt.handlebars.scala.HandlebarsImpl.apply(Handlebars.scala:39)
... 43 elided

Nested conditionals

I appear to have run into a bug with nested conditionals - the inner one seemed to be ignored - and I noticed a comment ("resolve the bugs with nested conditionals") here:

http://tech.gilt.com/2013/08/29/handlebars-scala-a-handlebars-for-scala

Any plans to fix this, or is there a workaround?

Thanks. Here's my template:

Duration - {{#if durationData.durationDays}}{{#if durationData.pluralDays}}{{durationData.durationDays}} days {{else}}{{durationData.durationDays}} day {{/if}}{{/if}}{{#if durationData.durationHours}}{{#if durationData.pluralHours}}{{durationData.durationHours}} hours {{else}}{{durationData.durationHours}} hour {{/if}}{{/if}}{{#if durationData.durationMinutes}}{{#if durationData.pluralMinutes}}{{durationData.durationMinutes}} minutes{{else}}{{durationData.durationMinutes}} minute{{/if}}{{/if}}.

Remove slfj-simple dependency, or make it provided?

The inclusion of "org.slf4j" % "slf4j-simple" % "1.6.4" as a dependency causes problems with multiple StaticLoggerBinder implementation warnings:

SLF4J: Class path contains multiple SLF4J bindings.

Due to the inability to specify which SL4J implementation to use, this means that the first loaded JAR implementation of StaticLoggerBinder wins. In environments where you can't easily control the classpath order (e.g. the universal packaging format output by SBT), this is a problem.

Including the sl4j-api dependency alone will be enough and allow people using this lib to choose their own logging framework! :)

Cross compile to 2.12.0?

Is there a plan to add support for the new Scala version? I don't mind submitting a PR to get it done (assuming it's not blocked), but would like to know that it would get released.

:)

Publish version 1.1.0

The example libraryDependencies line in the README suggests that 1.0.0 is the current version of Handlebars.

However, it looks from the compiler error messages I'm getting that the documentation on building custom bindings is for version 1.1.0.

I've had a look on Sonatype releases and snapshots and it doesn't look like there's a published version of 1.1.0. Can you release this please?

Cheers,

Dave

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.