0.04
A long-lived project that still receives updates
FlavourSaver is a pure-ruby implementation of the Handlebars templating language
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Development

>= 0
>= 0

Runtime

~> 2.6
 Project Readme

FlavourSaver

Gem Version CI License: MIT

Handlebars templates in pure Ruby. Use the same templates on the server and in the browser.

  • Works with Rails, and with Sinatra or anything else that uses Tilt
  • One runtime dependency (tilt), no native extensions
  • Ruby 3.1+, tested on 3.1 through 4.0

Installation

# Gemfile
gem "flavour_saver"

Quick start

require "flavour_saver"

Person = Struct.new(:name, :admin)

FlavourSaver.evaluate("Hello {{name}}!", Person.new("Ana", true))
# => "Hello Ana!"

FS is an alias for FlavourSaver.

The context can be any object. Templates read its public methods: {{name}} calls context.name. Use a Struct, a Data class, a model or a presenter.

Note

A plain Hash doesn't work as the context, because {{name}} calls a method. Hash values can be read with segment literals: {{settings.[theme]}} calls settings["theme"] (string keys only).

With Tilt

template = Tilt["handlebars"].new { "{{greeting}}, {{name}}!" }
template.render(Struct.new(:greeting, :name).new("Hi", "Ana"))
# => "Hi, Ana!"

.hbs and .handlebars files are registered with Tilt, so Tilt.new("welcome.hbs") works too.

With Rails

Put templates in app/views with a .hbs or .handlebars extension. See Rails.

Syntax

Syntax Example
Expression (HTML-escaped) {{name}}
Raw output {{{bio}}} or {{&bio}}
Path {{author.name}}
Array index / hash key {{posts.[0].title}}, {{settings.[theme]}}
Parent context {{../title}}
Root context {{@root.title}}
Comment {{! hidden }}, {{!-- hidden --}}
Section / inverted section {{#posts}}…{{/posts}}, {{^posts}}none{{/posts}}
Helper with arguments {{link "Home" href="/"}}
Subexpression {{sum 1 (sum 2 3)}}
Partial {{> user_card author}}
Raw block (not parsed) {{{{raw}}}} {{left as is}} {{{{/raw}}}}

Built-in helpers

Helper Does
#if / #unless Renders the block when the value is truthy / falsy. Supports {{else}}. nil, false, 0 and empty collections are falsy.
#each Renders the block for each item. Exposes @index, @first, @last, and @key for hashes.
#with Renders the block with the value as the context.
this The current context.
log Writes to FlavourSaver.logger at debug level.
{{#each posts}}
  {{@index}}. {{title}}{{#if @last}} (latest){{/if}}
{{/each}}

{{#if author.admin}}
  Admin
{{else}}
  Member
{{/if}}

On Rails, FlavourSaver.logger is Rails.logger. Elsewhere, set one before using log:

require "logger"
FlavourSaver.logger = Logger.new($stdout)

Custom helpers

Register a helper with a block. Positional arguments come first; key=value arguments arrive as a final hash with symbol keys.

FS.register_helper(:shout) { |text| text.upcase }
FS.register_helper(:greet) { |name, options| "#{options[:greeting]}, #{name}" }
{{shout "hi"}}                  {{! HI }}
{{greet "Ana" greeting="Hola"}} {{! Hola, Ana }}

Helper output is HTML-escaped. Return an html_safe string (from ActiveSupport) to skip escaping.

Block helpers

A block helper takes the template block as &block. block.call.contents renders the main section, optionally with a new context. block.call.inverse renders the {{else}} section.

FS.register_helper(:admins) do |people, &block|
  admins = people.select(&:admin)
  if admins.any?
    admins.map { |person| block.call.contents(person) }.join
  else
    block.call.inverse
  end
end
{{#admins people}}
  <li>{{name}}</li>
{{else}}
  <li>No admins</li>
{{/admins}}

You can also register a method. Inside a method, yield works the same way as block.call:

def twice
  yield.contents * 2
end

FS.register_helper(method(:twice))

Subexpressions

Wrap a helper call in parentheses to use its result as an argument:

FS.register_helper(:sum) { |a, b| a + b }
{{sum (sum 5 10) (sum 2 3)}}     {{! 20 }}
{{chart total=(sum boys girls)}}

Partials

FS.register_partial(:user_card, "<b>{{name}}</b>")
{{#each people}}{{> user_card this}}{{/each}}

A partial can also be a block that receives the context and returns a string:

FS.register_partial(:user_card) { |person| "<b>#{person.name}</b>" }

On Rails, partials come from your views instead. See below.

Rails

FlavourSaver registers .hbs and .handlebars template handlers with Action View.

No instance variables. Templates can't see controller instance variables. This keeps them portable to Handlebars.js. Expose data through helper methods or a presenter:

# app/helpers/application_helper.rb
module ApplicationHelper
  def current_user
    Current.user
  end
end
{{#if current_user}}
  Welcome back, {{current_user.first_name}}!
{{/if}}

Call view helpers through @root inside blocks. The view is the root context. Inside {{#each}} or {{#with}}, names are looked up on the current item, so {{post_url id}} renders blank. Use @root to reach the view:

{{#each posts}}
  <a href="{{@root.post_url id}}">{{title}}</a>
{{/each}}

Partials use render. You don't register partials in Rails. The partial syntax maps to Rails partials, and the argument is passed as object::

Template Rails call
{{> user_card}} render partial: "user_card", object: <current context>
{{> user_card author}} render partial: "user_card", object: author

Errors

Invalid templates raise a subclass of FlavourSaver::Error:

begin
  FS.evaluate("{{#if ok}}unclosed", context)
rescue FlavourSaver::Error => e
  # FlavourSaver::Parser::NotInLanguage, ::UnbalancedBlockError,
  # or FlavourSaver::Lexer::LexingError
end

Security

A template can only call registered helpers and the public methods of its context. Methods every object inherits, like send, instance_eval or class, raise FlavourSaver::ForbiddenMethodException. Private methods like system and eval can't be reached at all. Even so, only pass objects you're comfortable exposing to template authors.

Not supported yet

These Handlebars features don't parse or don't behave like Handlebars.js yet. Pull requests are welcome.

  • Whitespace control ({{~foo~}})
  • {{else if …}} chains
  • Block parameters ({{#each items as |item|}})
  • {{else}} inside {{#each}} and {{#with}}
  • Inline partials, partial blocks, dynamic partials, and hash arguments on partials
  • The lookup helper
  • ./ paths

Contributing

Bug reports and pull requests are welcome on GitHub.

git clone https://github.com/FlavourSaver/FlavourSaver.git
cd FlavourSaver
bundle install
bundle exec rspec

Changes are listed in the CHANGELOG.

License

MIT. Copyright (c) 2013 Resistor Limited. See LICENSE.