rbTenjin User's Guide

release: 0.7.1

Overview

Tenjin is a very fast and full-featured template engine implemented in pure Ruby.

Features

Install

$ gem install tenjin

or

$ tar xzf rbtenjin-X.X.X.tar.gz
$ cd rbtenjin-X.X.X/
$ mkdir -p $HOME/bin
$ mkdir -p $HOME/lib/ruby
$ cp bin/rbtenjin $HOME/bin
$ cp lib/tenjin.rb $HOME/lib/ruby
$ export PATH=$HOME/bin:$PATH
$ export RUBYLIB=$HOME/lib/ruby

Benchmark

MacOS X 10.6 Snow Leopard, Intel CoreDuo2 2GHz, Memory 2GB
$ ruby -v
ruby 1.8.7 (2011-02-18 patchlevel 334) [i686-darwin10.7.0]
$ cd benchmark
$ ruby bench.rb -n 1000
                          user     system      total        real
erb                   1.510000   0.030000   1.540000 (  1.555130)
erb-cache             0.570000   0.030000   0.600000 (  0.606775)
erb-reuse             0.550000   0.000000   0.550000 (  0.548511)
erb-defmethod         0.380000   0.000000   0.380000 (  0.398116)
erubis                0.950000   0.030000   0.980000 (  0.979889)
erubis-cache          0.580000   0.030000   0.610000 (  0.614893)
erubis-reuse          0.390000   0.000000   0.390000 (  0.432674)
tenjin                0.560000   0.040000   0.600000 (  0.606353)
tenjin-nocache        0.850000   0.040000   0.890000 (  0.890926)
tenjin-reuse          0.370000   0.010000   0.380000 (  0.393109)

Basic Examples

This section describes basics of Tenjin.

Template Syntax

Notation:

<?rb ... ?>
Ruby statements
${...}
Ruby expression (with HTML escape)
#{...}
Ruby expression (without HTML escape)
views/page.rbhtml: html template
<h2>${@title}</h2>
<table>
  <?rb i = 0 ?>
  <?rb for item in @items ?>
  <?rb   i += 1 ?>
  <?rb   klass = i % 2 == 1 ? 'odd' : 'even' ?>
  <tr class="#{klass}">
    <td>${item}</td>
  </tr>
  <?rb end ?>
</table>
TIPS:

You can check template syntax by 'rbtenjin -z'.

Syntax check of template files
$ rbtenjin -z views/*.rbhtml
views/page.rbhtml: Syntax OK

Render Template

This is an example code to render template file.

main.rb: main program
require 'tenjin'

## context data
context = {
  :title => 'Tenjin Example',
  :items => ['<AAA>', 'B&B', '"CCC"'],
}

## create engine object
engine = Tenjin::Engine.new(:path=>['views'])

## render template with context data
html = engine.render('page.rbhtml', context)
print html
result
$ ruby main.rb
<h2>Tenjin Example</h2>
<table>
  <tr class="odd">
    <td>&lt;AAA&gt;</td>
  </tr>
  <tr class="even">
    <td>B&amp;B</td>
  </tr>
  <tr class="odd">
    <td>&quot;CCC&quot;</td>
  </tr>
</table>
TIPS:

Hash context data is converted into Tenjin::Context object. It is possible to use your own class as context data object instead of Tenjin::Context.

require 'tenjin'
class FooController < Controller
  include Tenjin::ContextHelper
  include Tenjin::HtmlHelper
  #include Tenjin::SafeHelper      # optional
  #include Tenjin::HtmlTagHelper   # optional
  @@engine = Tenjin::Engine.new(:path=>['views'])

  def index
    @items = ['AAA', 'BBB', 'CCC']
    html = @@engine.render('foo/index.rbhtml', self)
    return html
  end
end

Show Converted Source Code

rbTenjin converts template files into Ruby script and executes it. Compiled Ruby script is saved into cache file automatically.

Show cached file
$ ls views/page.rbhtml*
views/page.rbhtml       views/page.rbhtml.cache
$ file views/page.rbhtml.cache
views/page.rbhtml.cache: text

You can get converted script by 'rbtenjin -s'.

Show Ruby code
$ rbtenjin -s views/page.rbhtml
_buf = '';  _buf << %Q`<h2>#{escape((@title).to_s)}</h2>
<table>\n`
  i = 0
  for item in @items
    i += 1
    klass = i % 2 == 1 ? 'odd' : 'even'
 _buf << %Q`  <tr class="#{klass}">
    <td>#{escape((item).to_s)}</td>
  </tr>\n`
  end
 _buf << %Q`</table>\n`
_buf.to_s

If you specify '-sb' instead of '-s', neither preamble (= '@_buf = _buf = "";') nor postamble (= '_buf.to_s') are printed. See Retrieve Embedded Code section for more information.

Or:

How to convert template file into Ruby script
require 'tenjin'
template = Tenjin::Template.new('views/page.rbhtml')
puts template.script

### or:
#template = Tenjin::Template.new
#filename = 'views/page.rbhtml'
#print(template.convert(File.read(filename), filename))

### or:
#engine = Tenjin::Engine.new(:path=>['views'])
#print(engine.get_template('page.rbhtml').script)

Layout Template

Layout template will help you to use common HTML design for all pages.

views/_layout.rbhtml
<!DOCTYPE>
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <title>${@title}</title>
  </head>
  <body>
#{@_content}
  </body>
</html>
main.rb
require 'tenjin'

## context data
context = {
  :title => 'Tenjin Example',
  :items => ['<AAA>', 'B&B', '"CCC"'],
}

## cleate engine object
engine = Tenjin::Engine.new(:path=>['views'], :layout=>'_layout.rbhtml')

## render template with context data
html = engine.render('page.rbhtml', context)
print html
Result
$ ruby main.rb
<!DOCTYPE>
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <title>Tenjin Example</title>
  </head>
  <body>
<h2>Tenjin Example</h2>
<table>
  <tr class="odd">
    <td>&lt;AAA&gt;</td>
  </tr>
  <tr class="even">
    <td>B&amp;B</td>
  </tr>
  <tr class="odd">
    <td>&quot;CCC&quot;</td>
  </tr>
</table>

  </body>
</html>

You can specify other layout template file with render() method.

## use other layout template file
engine.render('page.rbhtml', context, '_other_layout.jshtml')

## don't use layout template file
engine.render('page.pyhtml', context, false)

Tenjin supports nested layout template. See Nested Layout Template section for details.

Context Variables

In Tenjin, Instance variables are shared between template files. If you set instance variables in a template, it is available in layout template.

views/_layout.rbhtml
<!DOCTYPE>
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <title>${@page_title}</title>
  </head>
  <body>
#{@_content}
  </body>
</html>
views/page.rbhtml: pass page title date from template to layout template
<?rb @page_title = 'Tenjin: Layout Template Example' ?>
<h2>${@title}</h2>
<table>
<?rb i = 0 ?>
<?rb for item in @items ?>
<?rb   i += 1 ?>
<?rb   klass = i % 2 == 1 ? 'odd' : 'even' ?>
  <tr class="#{klass}">
    <td>${item}</td>
  </tr>
<?rb end ?>
</table>
Result
$ ruby main.rb
<!DOCTYPE>
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <title>Tenjin: Layout Template Example</title>
  </head>
  <body>
<h2>Tenjin Example</h2>
<table>
  <tr class="odd">
    <td>&lt;AAA&gt;</td>
  </tr>
  <tr class="even">
    <td>B&amp;B</td>
  </tr>
  <tr class="odd">
    <td>&quot;CCC&quot;</td>
  </tr>
</table>

  </body>
</html>

Template Arguments

For readability, it is recommended to declare context variables in your template files.

views/page.rbhtml
<?rb #@ARGS title, items ?>
<?rb @page_title = 'Tenjin: Layout Template Example' ?>
<h2>${title}</h2>
<table>
<?rb i = 0 ?>
<?rb for item in items ?>
<?rb   i += 1 ?>
<?rb   klass = i % 2 == 1 ? 'odd' : 'even' ?>
  <tr class="#{klass}">
    <td>${item}</td>
  </tr>
<?rb end ?>
</table>
Converted Ruby script
$ rbtenjin -sb views/page.rbhtml
 title = @title; items = @items;
@page_title = 'Tenjin: Layout Template Example'
 _buf << %Q`<h2>#{escape((title).to_s)}</h2>
<table>\n`
i = 0
for item in items
  i += 1
  klass = i % 2 == 1 ? 'odd' : 'even'
 _buf << %Q`  <tr class="#{klass}">
    <td>#{escape((item).to_s)}</td>
  </tr>\n`
end
 _buf << %Q`</table>\n`
views/_layout.rbhtml
<?rb #@ARGS _content, page_title ?>
<!DOCTYPE>
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <title>${@page_title}</title>
  </head>
  <body>
#{@_content}
  </body>
</html>
Converted Ruby script
$ rbtenjin -sb views/_layout.rbhtml
 _content = @_content; page_title = @page_title;
 _buf << %Q`<!DOCTYPE>
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <title>#{escape((@page_title).to_s)}</title>
  </head>
  <body>
#{@_content}
  </body>
</html>\n`

Include Partial Template

You can include other template files by import() helper method.

import(template-name)
Import other template. template-name can be file name or template short name.

In the following example, layout template includes header and footer templates into it.

views/_layout.rbhtml
<?rb #@ARGS _content, page_title ?>
<!DOCTYPE>
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <title>${@page_title}</title>
  </head>
  <body>
<?rb import('_header.rbhtml') ?>
#{@_content}
<?rb import('_footer.rbhtml') ?>
  </body>
</html>
views/_header.rbhtml
<div class="header">
  <h1>${@page_title}</h1>
</div>
views/_footer.rbhtml
<address>
  copyright(c) 2010 kuwata-lab.com all rights reserved
</address>

Output result shows that header and footer templates are included as you expect.

Result
$ ruby main.rb
<!DOCTYPE>
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <title>Tenjin: Layout Template Example</title>
  </head>
  <body>
<div class="header">
  <h1>Tenjin: Layout Template Example</h1>
</div>
<h2>Tenjin Example</h2>
<table>
  <tr class="odd">
    <td>&lt;AAA&gt;</td>
  </tr>
  <tr class="even">
    <td>B&amp;B</td>
  </tr>
  <tr class="odd">
    <td>&quot;CCC&quot;</td>
  </tr>
</table>

<address>
  copyright(c) 2010 kuwata-lab.com all rights reserved
</address>
  </body>
</html>

Template Short Name

If you set template postfix, you can specify template by short name such as ':page' instead of 'page.rbhtml'. Notice that template short name should be Symbol.

main.rb
require 'tenjin'

## context data
context = {
  :title => 'Tenjin Example',
  :items => ['<AAA>', 'B&B', '"CCC"'],
}

## cleate engine object
engine = Tenjin::Engine.new(:path=>['views'], :postfix=>'.rbhtml', :layout=>:_layout)

## render template with context data
html = engine.render(:page, context)
puts html
views/_layout.rbhtml
<?rb #@ARGS _content, page_title ?>
<!DOCTYPE>
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <title>${@page_title}</title>
  </head>
  <body>
<?rb import(:_header) ?>
#{@_content}
<?rb import(:_footer) ?>
  </body>
</html>

Helper Function

Tenjin provides some helper methods.

TIPS:

These are defined in helper modules. If you want use them in your class, just include them.

class FooController < Controller
  include Tenjin::ContextHelper
  include Tenjin::HtmlHelper
  include Tenjin::HtmlTagHelper
  include Tenjin::SafeHelper

  @@engine = Tenjin::Engine.new(:path=>'views', :postfix=>'.rbhtml', :layout=>:_layout)

  def index()
    @title = "Tenjin Example"
    @items = ["AAA", "BBB", "CCC"]
    html = @@engine.render(:index, self)
    return html
  end

end

Tenjin::ContextHelper

import(template_name, _append_to_buf=true)
Include other template. 'template_name' can be filename or short name. See Include Partial Template section for details.
echo(value)
Add value into _buf. This is equivarent to '#{value}'.
start_capture(name=nil, &block)
start capturing. returns captured string if block given, else return nil. if block is not given, calling stop_capture() is required. See Capturing section for details.
ex. list.rbhtml
<html><body>
  <h1><?rb start_capture(:title) do ?>Document Title<?rb end ?></h1>
  <?rb start_capture(:content) ?>
  <ul>
   <?rb for item in list do ?>
    <li>${item}</li>
   <?rb end ?>
  </ul>
  <?rb stop_capture() ?>
</body></html>
ex. layout.rbhtml
<?xml version="1.0" ?>
<html xml:lang="en">
 <head>
  <title>${@title}</title>
 </head>
 <body>
  <h1>${@title}</h1>
  <div id="content">
   <?rb echo(@content) ?>
  </div>
 </body>
</html>
stop_capture(store_to_context=true)
Stop capturing. Returns captured string. Necessary only when start_capture() doesn't take a block.
captured_as(name)
If captured string is found then add it to _buf and return true, else return false. This is a helper method for layout template. See Capturing section for details.
<?py ## layout template example ?>
<html>
<body>
  <div id="main">
  <?rb ## if captured by 'start_capture(:main)', use it. ?>
  <?rb ## if not captured, use this block. ?>
  <?rb if not captured_as(:main) ?>
    <p>content not found</p>
  <?rb end ?>
  </div>
</body>
</html
_p(str)
See Preprocessing section.
_P(str)
See Preprocessing section.
cache_with(key, lifetime=nil)
Cache html fragment. See Fragment Cache section for details.

Tenjin::HtmlHelper

escape_html(str)
Alias to escape_html().
escape_xml(str)
Escape '&', '<', '>', and '"' into '&amp;', '&lt;', '&gt;', and '&quot;' respectively.
escape_html(str)
Similar to escape_xml(), but much faster when str contains a lot of '&<>"' characters.

Tenjin::SafeHelper

safe_str(str)
Return SafeString object. See Safe Template section for details.
safe_str?(str)
Return true if s is SafeString object. See Safe Template section for details.
safe_escape(str)
Escape str only if val str not SafeString object, and return SafeString object. See Safe Template section for details.

Tenjin::HtmlTagHelper

checked(value)
Return ' checked="checked" if value is true.
value = "1"
checked(value=="1")  #=> ' checked="checked"'
checked(value=="0")  #=> ''
selected(value)
Return ' selected="selected" if value is true.
value = "1"
selected(value=="1")  #=> ' selected="selected"'
selected(value=="0")  #=> ''
disabled(value)
Return ' disabled="disabled"' if value is true.
value = "1"
disabled(value=="1")  #=> ' disabled="disabled"'
disabled(value=="0")  #=> ''
tagattr(name, expr, value=nil, escape=true)
(experimental) Return ' name="value"' if expr is not false nor nil. If value is nil or false then expr is used as value.
tagattr('name', 'val')  #=> ' name="val"'
tagattr('name', nil)    #=> ''
urlpath = "/blog/"
tagattr('class', urlpath=='/blog/', 'current')  #=> ' class="current"'
tagattr('class', urlpath=='/',      'current')  #=> ''
nv(name, value, separator=nil):
Return 'name="name" value="value"' string. If separator is specfied, id attribute is added.
nv('user', 'guest');       #=> ' name="user" value="guest"'
nv('user', 'guest', '-');  #=> ' name="user" value="guest" id="user-guest"'
js_link(label, onclick, tags={}):
Return '<a href="" onclick="onclick">label</a>' string.
js_link('Show', '$().show()')
    #=> '<a href="javascript:undefined" onclick="$().show();return false">Show</a>'
js_link('Show', '$().show()', :class=>"link")
    #=> '<a href="javascript:undefined" onclick="$().show();return false" class="link">Show</a>'
nl2br(text)
Convert "\n" into "<br />\n".
nl2br("foo\nbar\n")  #=> "foo<br />\nbar<br />\n"
text2html(text)
Convert "\n" and " " into "<br />\n" and " &nbsp;".
text2html("  foo\n  bar\n")  #=> " &nbsp;foo<br />\n &nbsp;bar<br />\n"
new_cycle(*values)
Return Cycle object.
cycle = new_cycle('odd', 'even')
"#{cycle}"   #=> 'odd'
"#{cycle}"   #=> 'even'
"#{cycle}"   #=> 'odd'

Advanced Features

Safe Template

(EXPERIMENTAL)

Tenjin provides SafeTemplate and SafeEngine class which enforces HTML escape. These are similar to Django's feature or Jinja2's autoescape feature.

The point of these classes is that you can control whether escape html or not by data type, not by embedded notation.

See the following example.

safe-test.rb
require 'tenjin'
include Tenjin::HtmlHelper   # defines escape()
include Tenjin::SafeHelper   # defines safe_escape(), safe_str(), safe_str?()

## both are same notation
input = <<END
s1 = ${@s1}
s2 = ${@s2}
END

## but passed different data type
context = {
  :s1 => "<b>SOS</b>",
  :s2 => safe_str("<b>SOS</b>"),
}

## SafeTemplate will escape 's1' but not 's2'
template = Tenjin::SafeTemplate.new(:input=>input)
print(template.script)
print("---------------------\n")
print(template.render(context))

The follwoing output shows that:

  • SafeTemplate and SafeEngine classes uses safe_escape() instead of escape() to escape value.
  • Normal string (= "<b>SOS</b>") is escaped automatically, but the other string which is marked as escaped (= safe_str("<b>SOS</b>")) is not escaped. This means that you can controll escape by data type, not embedded notation.
Output example
$ ruby safe-test.rb
 _buf << %Q`s1 = #{safe_escape((@s1).to_s)}
s2 = #{safe_escape((@s2).to_s)}\n`
---------------------
s1 = &lt;b&gt;SOS&lt;/b&gt;
s2 = <b>SOS</b>

In addition, SafeTemplate/SafeEngine classes inhibits #{...} because some people mistake it with ${...} and it can be XSS security hole in the result. Use ${...} in each case.

#{@value}    # Not available with SafeTemplate/SafeEngine!
${@value}    # Use this in each case.
TIPS:

Implementation of SafeTemplate and SafeEngine is very small. In other words, it is easy to implement your own SafeTemplate/SafeEngine class. See source code of Tenjin for details.

There are three helper methods defined in Tenjin::SafeHelper.

safe_escape(str)
  • If str is normal String, escape it and return SafeString object.
  • If str is SafeString object, just return it.
safe_str(str)
  • If str is normal String, return SafeString object WITHOUT escaping.
  • If str is SafeString object, just return it.
safe_str?(str)
  • If str is normla String, return false.
  • If str is SafeString object, return true.
Example of safe helpers
require 'tenjin'
include Tenjin::HtmlHelper
include Tenjin::SafeHelper
#
s = "<AAA>"
puts safe_str?(s)     #=> false
s = safe_escape(s)    # same as SafeString.new(escape(s))
puts safe_str?(s)     #=> true
puts s                #=> &lt;AAA&gt;
puts safe_escape(s)   #=> &lt;AAA&gt;
#
s = "<AAA>"
s = safe_str(s)       # same as SafeString.new(s)
puts safe_str?(s)     #=> true
puts s                #=> <AAA>
puts safe_escape(s)   #=> <AAA>

Nested Layout Template

It is able to nest several layout template files.

views/_site_layout.rbhtml
<html>
  <body>
#{@_content}
  </body>
</html>
views/_blog_layout.rbhtml
<?rb @_layout = '_site_layout.rbhtml' ?>
<h2>${@title}</h2>
<!-- content -->
#{@_content}
<!-- /content -->
views/blog_post.rbhtml
<?rb @_layout = '_blog_layout.rbhtml' ?>
<div class="article">
#{text2html(@post_content)}
</div>
main.rb
require 'tenjin'
engine = Tenjin::Engine.new(:path=>['views'])
context = {
  :title => 'Blog Post Test',
  :post_content => "Foo\nBar\nBaz",
}
html = engine.render('blog_post.rbhtml', context)
print(html)
Result
$ ruby main.rb
<html>                        # by _layout.rbhtml
  <body>                      #       :
<h2>Blog Post Test</h2>       #   by _blog_layout.rbhtml
<!-- content -->              #         :
<div class="article">         #     by blog_post.rbhtml
Foo<br />                     #           :
Bar<br />                     #           :
Baz                           #           :
</div>                        #           :
                              #           :
<!-- /content -->             #   by _blog_layout.rbhtml
                              #         :
  </body>                     # by _layout.rbhtml
</html>                       #       :

Trace Templates

If you pass ':trace=>true' to Tenjin::Template class or Tenjin::Engine class, Template class will print template file name at the beginning and end of output.

For example:

trace-example.rb
require 'tenjin'
engine = Tenjin::Engine.new(:layout=>'layout.rbhtml', :trace=>true)
output = engine.render('main.rbhtml', {'items'=>['A','B','C']})
puts output

Will print:

$ ruby trace-example.rb
<!-- ***** begin: layout.rbhtml ***** -->
<html>
  <body>
    <div class="content">
<!-- ***** begin: main.rbhtml ***** -->
<ul>
  <li>A</li>
  <li>B</li>
  <li>C</li>
</ul>
<!-- ***** end: main.rbhtml ***** -->
    </div>
  </body>
</html>
<!-- ***** end: layout.rbhtml ***** -->

This feature is very useful when debugging to detect template file name from HTML output.

If you like it, you can make it always enabled.

## trace is always enabled
Tenjin::Engine.TRACE = true

Capturing

It is able to capture parital of output. You can use this feature as an alternative of Django's template-inheritance.

views/blog-post.rbhtml : one partial capture ('sidebar')
<h2>#{@blog_post[:title]}</h2>
<div class="blog-post">
#{text2html(@blog_post[:content])}
</div>

<?rb start_capture('sidebar') do ?>
<h3>Recent Posts</h3>
<ul>
<?rb for post in @recent_posts ?>
  <a href="/blog/#{post[:id]}">${post[:title]}</a>
<?rb end ?>
</ul>
<?rb end ?>
views/_layout.rbhtml : two placeholders ('header' and 'sidebar')
<html>
  <body>
    <div id="header-part">
    <?rb if ! captured_as(:header) ?>
      <h1>My Great Blog</h1>
    <?rb end ?>
    </div>
    <div id="main-content">
#{@_content}
    </div>
    <div id="sidebar-part">
    <?rb if ! captured_as(:sidebar) ?>
      <h3>Links</h3>
      <ul>
        <a href="http://google.com/">Google</a>
        <a href="http://yahoo.com/">Yahoo!</a>
      </ul>
    <?rb end ?>
    </div>
  </body>
</html>
main.rb
## context data
post_content = <<END
Tenjin has great features.
- Very Fast
- Full Featured
- Easy to Use
END
context = {
  :blog_post => {
    :title   => 'Tenjin is Great',
    :content => post_content,
  },
  :recent_posts => [
    {:id=>1, :title=>'Tenjin is Fast' },
    {:id=>2, :title=>'Tenjin is Full-Featured' },
    {:id=>3, :title=>'Tenjin is Easy-to-Use' },
  ]
}

## render template
require 'tenjin'
engine = Tenjin::Engine.new(:path=>['views'], :layout=>'_layout.rbhtml')
html = engine.render('blog-post.rbhtml', context)
puts html

The result shows that captured string (with name 'sidebar') overwrites layout template content.

Result : only 'sidebar' placeholder is overwritten by capturing.
$ ruby main.rb
<html>
  <body>
    <div id="header-part">
      <h1>My Great Blog</h1>
    </div>
    <div id="main-content">
<h2>Tenjin is Great</h2>
<div class="blog-post">
Tenjin has great features.<br />
- Very Fast<br />
- Full Featured<br />
- Easy to Use<br />

</div>


    </div>
    <div id="sidebar-part">
<h3>Recent Posts</h3>
<ul>
  <a href="/blog/1">Tenjin is Fast</a>
  <a href="/blog/2">Tenjin is Full-Featured</a>
  <a href="/blog/3">Tenjin is Easy-to-Use</a>
</ul>
    </div>
  </body>
</html>

Template Cache

Tenjin converts template file into Ruby script and save it as cache file. By default, it is saved as template-filename + '.cache' in bytecode format. You can change this behaviour by setting Tenjin::Engine.template_cache or passing cache object to Tenjin::Engine.new().

example to change template caching
## assume that you implimented MemcachedTemplateCache package
class MemcachedTemplaceCache < Tenjin::TemplateCache
  def save(cachepath, template) ... end
  def load(cachepath, timestamp=nil) ... end
  end
end

## change to store template cache into memcached
Tenjin::Engine.template_cache = MemcachedTemplateCache.new();
my $engine = Tenjin::Engine.new();

## or
my $engine = Tenjin::Engine.new(:cache=>MemcachedTemplateCache.new());

Fragment Cache

You can cache a certain part of HTML to improve performance. This is called as Fragment Cache.

views/items.rbhtml
<div>
  <?rb # fragment cache with key ('items/1') and lifetime (60sec) ?>
  <?rb cache_with('items/1', 60) do ?>
  <ul>
    <?rb for item in @get_items.call() ?>
    <li>${item}</li>
    <?rb end ?>
  </ul>
  <?rb end ?>
</div>

Tenjin stores fragments caches into memory by default. If you want to change or customize cache store, see the following example.

main.rb
require 'tenjin'

## create key-value store object
Dir.mkdir('cache.d') unless File.exist?('cache.d')
kv_store = Tenjin::FileBaseStore.new('cache.d')      # file based

## set key-value store into tenjin.helpers.fagment_cache object
Tenjin::Engine.data_cache = kv_store

## context data
## (it is strongly recommended to create Proc object
##  to provide pull-style context data)
get_items = proc {    # called only when cache is expired
  ['AAA', 'BBB', 'CCC']
}
context = {:get_items => get_items}

## render html
engine = Tenjin::Engine.new(:path=>['views'])
html = engine.render('items.rbhtml', context)
puts html
Result
$ ruby main.rb
<div>
  <ul>
    <li>AAA</li>
    <li>BBB</li>
    <li>CCC</li>
  </ul>
</div>

You'll find that HTML fragment is cached into cache directory. This cache data will be expired at 60 seconds after.

$ cat cache.d/items/1
  <ul>
    <li>AAA</li>
    <li>BBB</li>
    <li>CCC</li>
  </ul>

Logging

If you set logging object to Tenjin.logger, rbTenjin will report loading template files.

For example:

ex-logger.rb
require 'tenjin'

## set logger object
require 'logger'
Tenjin.logger = Logger.new($stdout)
Tenjin.logger.datetime_format = ""

engine = Tenjin::Engine.new()
context = {:name => 'World'}
html = engine.render('example.rbhtml', context)
#print html

If you run it first time, Tenjin will report that template object is stored into cache file.

$ ruby ex-logger.rb
D, [#3700] DEBUG -- : [tenjin.rb:1022] cache not found (cachefile="example.rbhtml.cache").
D, [#3700] DEBUG -- : [tenjin.rb:1005] cache saved (cachefile="example.rbhtml.cache").

And if you run it again, Tenjin will report that template object is loaded from cache file.

$ ruby ex-logger.rb
D, [#3730] DEBUG -- : [tenjin.rb:1028] cache found (cachefile="example.rbhtml.cache").

Preprocessing

Tenjin supports preprocessing of template. Preprocessing executes some logics when templates are loaded and that logics are not executed when rendering. Preprocessing makes your application much faster.

Basics of Preprocessing

Notation of preprocessing:

<?RB ... ?>
Preprocessing statement.
#{{...}}
Preprocessing expression (without HTML escape)
${{...}}
Preprocessing expression (with HTML escape)

The following shows difference between ${...} and ${{...}}.

views/pp-example1.rbhtml
## normal expression
value = ${$VALUE}
## with preprocessing
value = ${{$VALUE}}
pp-example1.rb
$VALUE = 'My Great Example'

## create engine object with preprocessing enabled
require 'tenjin'
engine = Tenjin::Engine.new(:path=>['views'], :preprocess=>true)

## print Python script code
puts "------ converted script ------"
puts engine.get_template('pp-example1.rbhtml').script

## render html
html = engine.render('pp-example1.rbhtml', {})
puts "------ rendered html ------"
puts html
Result: notice that ${{...}} is evaluated at template converting stage.
$ ruby pp-example1.rb
------ converted script ------
 _buf << %Q`## normal expression
value = #{escape(($VALUE).to_s)}
## with preprocessing
value = My Great Example\n`
------ rendered html ------
## normal expression
value = My Great Example
## with preprocessing
value = My Great Example

You can confirm preprocessed template by 'rbtenjin -P' command.

Preprocessed template
$ rbtenjin -P -c '$VALUE="My Great Example"' views/pp-example1.rbhtml
## normal expression
value = ${$VALUE}
## with preprocessing
value = My Great Example

If you want to see preprocessing script (not preprocessed script), use 'rbtenjin -sP' command.

$ rbtenjin -sP -c '$VALUE="My Great Example"' views/pp-example1.rbhtml
_buf = '';  _buf << %Q`\#\# normal expression
value = ${$VALUE}
\#\# with preprocessing
value = #{escape((_decode_params(($VALUE))).to_s)}\n`
_buf.to_s

Loop Expantion

It is possible to evaluate some logics by '<?RB ... ?>' when convert template into Ruby script code. For example, you can expand loop in advance to improve performance.

views/pp-example2.rbhtml
<?RB states = { "CA" => "California", ?>
<?RB            "NY" => "New York", ?>
<?RB            "FL" => "Florida",  ?>
<?RB            "TX" => "Texas",  ?>
<?RB            "HI" => "Hawaii", } ?>
<?rb chk = { params['state'] => ' selected="selected"' } ?>
<select name="state">
  <option value="">-</option>
  <?RB for code in states.keys.sort ?>
  <option value="#{{code}}"#{chk['#{{code}}']}>${{states[code]}}</option>
  <?RB end ?>
</select>

Preprocessed script code shows that loop is expanded in advance. It means that loop is not executed when rendering template.

Preprocessed template
$ rbtenjin -P views/pp-example2.rbhtml
<?rb chk = { params['state'] => ' selected="selected"' } ?>
<select name="state">
  <option value="">-</option>
  <option value="CA"#{chk['CA']}>California</option>
  <option value="FL"#{chk['FL']}>Florida</option>
  <option value="HI"#{chk['HI']}>Hawaii</option>
  <option value="NY"#{chk['NY']}>New York</option>
  <option value="TX"#{chk['TX']}>Texas</option>
</select>

Parameters

Assume that link_to() is a helper method which takes label and url and generate <a></a> tag. In this case, label and url can be parameterized by _p("...") and _P("..."). The former is converted into #{...} and the latter converted into ${...} by preprocessor.

views/pp-example3.rbhtml
<?RB
## ex. link_to('Show', '/show/1')  => <a href="/show/1">Show</a>
def link_to(label, url)
    return "<a href=\"#{url}\">#{label}</a>"
end
?>
#{{link_to('Show '+_P('params["name"]'), '/items/show/'+_p('params["id"]'))}}

The following shows that _P('...') and _p('...') are converted into ${...} and #{...} respectively.

Preprocessed template:
$ rbtenjin -P views/pp-example3.rbhtml
<a href="/items/show/#{params["id"]}">Show ${params["name"]}</a>

There are many web-application framework and they provides helper functions. These helper functions are divided into two groups. link_to() or _() (function for M17N) return the same result when the same arguments are passed. These functions can be expanded by preprocessor. Some functions return the different result even if the same arguments are passed. These functions can't be expaned by preprocessor.

Preprocessor has the power to make view-layer much faster, but it may make the debugging difficult. You should use it carefully.

M17N Page

If you have M17N site, you can make your site faster by Tenjin.

In M17N-ed site, message translation function (such as _('message-key')) is called many times. Therefore if you can eliminate calling that function, your site can be faster. And Tenjin can do it by preprocessing.

The points are:

  • Change cache filename according to language. For example, create cache file 'file.rbhtml.en.cache', 'file.rbhtml.fr.cache', 'file.rbhtml.it.cache', and so on from a template file 'file.rbhtml'. This is done by Tenjin automatically if you pass ':lang=>"en"' or 'lang="fr"' option to Engine class.
  • Create Engine object for each language and pass :lang option respectively.
  • Enable preprocessing to create different cache content for each language.

The following is an example to generate M17N pages from a template file.

m17n.rbhtml:
<div>
<?RB ## '_()' represents translator method ?>
<?RB _ = @_ ?>
 <p>${{_['Hello']}} ${@username}!</p>
</div>
m17n.rb:
# -*- coding: utf-8 -*-
require 'tenjin'

##
## message catalog to translate message
##
MESSAGE_CATALOG = {
  'en' => { 'Hello'=>"Hello",   'Good bye'=>"Good bye" },
  'fr' => { 'Hello'=>"Bonjour", 'Good bye'=>"Au revoir" },
}

##
## create translation function and return it.
## ex.
##    _ = create_m17n_obj('fr')
##    print _['Hello']   #=> 'Bonjour'
##
def create_m17n_obj(lang)
  hash = MESSAGE_CATALOG[lang]  or
    raise Error.new("#{lang}: unknown lang.")
  _ = proc {|message_key| hash[message_key] }
  return _
end

##
## test program
##
if __FILE__ == $0
  ## render html for English
  engine_en = Tenjin::Engine.new(:preprocess=>true, :lang=>'en')
  context = { :username => 'World' }
  context[:_] = create_m17n_obj('en')
  html = engine_en.render('m17n.rbhtml', context)
  puts "--- lang: en ---"
  puts html

  ## render html for French
  engine_fr = Tenjin::Engine.new(:preprocess=>true, :lang=>'fr')
  context = { :username => 'World' }
  context[:_] = create_m17n_obj('fr')
  html = engine_fr.render('m17n.rbhtml', context)
  puts "--- lang: fr ---"
  puts html
end
Result:
$ ruby m17n.rb
--- lang: en ---
<div>
 <p>Hello World!</p>
</div>
--- lang: fr ---
<div>
 <p>Bonjour World!</p>
</div>

After that, you can find two cache files are created.

$ ls m17n.rbhtml*
m17n.rbhtml    m17n.rbhtml.en.cache    m17n.rbhtml.fr.cache

And each cache files have different content.

_('Hello') is translated into "Hello" in Engilish cache file
$ cat m17n.rbhtml.en.cache
 _buf << %Q`<div>
 <p>Hello #{escape((@username).to_s)}!</p>
</div>\n`
_('Hello') is translated into "Bonjour" in French cache file
$ cat m17n.rbhtml.fr.cache
 _buf << %Q`<div>
 <p>Bonjour #{escape((@username).to_s)}!</p>
</div>\n`

Make Tenjin as PHP-like Tool

Tenjin provides 'rbtenjin.cgi' CGI script which makes Tenjin as PHP-like tool.

Setup
$ htdocs=/usr/local/apache2/htdocs   # or $HOME/public_html
$ tar xzf rbtenjin-X.X.X.tar.gz
$ cd rbtenjin-X.X.X/
$ cp lib/tenjin.rb            $htdocs
$ cp public_html/rbtenjin.cgi $htdocs
$ cp public_html/.htaccess    $htdocs
$ cd public_html/*.rbhtml     $htdocs
$ chmod a+x $htdocs/rbtenjin.cgi

After that, access to http://localhost/ (or http://localhost/~yourname/) and you'll see example page.

Tips

Specify Function Names of escape() and to_str()

It is able to specify function names of html escape.

main.rb
require 'tenjin'
require 'erb'
engine = Tenjin::Engine.new(:path=>['views'], :escapefunc=>'ERB::Util.h')
puts engine.get_template('page.rbhtml').script
views/page.rbhtml
<p>
  escaped:     ${@value}
  not escaped: #{@value}
</p>
Result
$ ruby main.rb
 _buf << %Q`<p>
  escaped:     #{ERB::Util.h((@value).to_s)}
  not escaped: #{@value}
</p>\n`

Template Inheritance

Tenjin doesn't support Template Inheritance which Django template engine does. But you can emulate it by capturing(*1). See this section for details.

(*1)
Notice that capturing is useful but not so powerful than template inheritance.

rbtenjin Command

See 'rbtenjin -h' for details.

Syntax Check

Command-line option '-z' checks syntax of template files. In addition '-w' option sets warning level to 2. It is recommended to use '-w' when you specify '-z' option.

example.rbhtml
<ul>
<?rb for item in items ?>
  <li>${item}</li>
<?rb ende ?>
</ul>
Result:
$ rbtenjin -wz example.rbhtml
example.rbhtml:5: syntax error, unexpected $end, expecting kEND

Command-line option '-wz' is more convenient than 'rbtenjin -s file | ruby -wc' because the former can take several filenames.

Command-line option '-q' (quiet-mode) prints nothing if there are no syntax errors.

Convert Template into Ruby Script

Command-line option '-s' converts template file into Ruby script code.

example.rbhtml
<ul>
  <?rb for item in @items ?>
  <li>${item}</li>
  <?rb end ?>
</ul>
Result (-s)
$ rbtenjin -s example.rbhtml
_buf = '';  _buf << %Q`<ul>\n`
  for item in @items
 _buf << %Q`  <li>#{escape((item).to_s)}</li>\n`
  end
 _buf << %Q`</ul>\n`
_buf.to_s

Option '-b' removes preamble ('_buf = ''') and postamble ('_buf.to_s)').

Result (-sb)
$ rbtenjin -sb example.rbhtml
 _buf << %Q`<ul>\n`
  for item in @items
 _buf << %Q`  <li>#{escape((item).to_s)}</li>\n`
  end
 _buf << %Q`</ul>\n`

Retrieve Embedded Code

Tenjin allows you to retrieve embedded code from template files in order to help template debugging.

It is hard to debug large template files because HTML and embedded code are mixed in a file. Retrieving embedded code from template files will help you to debug large template files.

Assume the following template file.

example.rbhtml
<table>
  <?rb i = 0 ?>
  <?rb for item in @items ?>
  <?rb   i += 1 ?>
  <tr>
    <td>#{i}</td>
    <td>${item}</td>
  </tr>
  <?rb end ?>
</table>

Option '-S' (or '-a retrieve') retrieves embedded codes.

Result (-Sb)
$ rbtenjin -Sb example.rbhtml

  i = 0
  for item in @items
    i += 1


        escape((item).to_s); 

  end

Option '-X' (or '-a statements') retrieves only statements.

Result (-Xb)
$ rbtenjin -Xb example.rbhtml

  i = 0
  for item in @items
    i += 1


        

  end

Option '-N' adds line numbers.

Result (-NXb)
$ rbtenjin -NXb example.rbhtml
    1:  
    2:    i = 0
    3:    for item in @items
    4:      i += 1
    5:  
    6:  
    7:          
    8:  
    9:    end
   10:  

Option '-U' (unique) compress empty lines.

Result (-UNXb)
$ rbtenjin -UNXb example.rbhtml

    2:    i = 0
    3:    for item in @items
    4:      i += 1

    9:    end

Option '-C' (compact) removes empty lines.

Result (-CNXb)
$ rbtenjin -CNXb example.rbhtml
    2:    i = 0
    3:    for item in @items
    4:      i += 1
    9:    end

Execute Template File

You can execute template file in command-line.

example.rbhtml
<?rb @items = ['<AAA>', 'B&B', '"CCC"'] ?>
<ul>
  <?rb for item in @items ?>
  <li>${item}</li>
  <?rb end ?>
</ul>
Result
$ rbtenjin example.rbhtml
<ul>
  <li>&lt;AAA&gt;</li>
  <li>B&amp;B</li>
  <li>&quot;CCC&quot;</li>
</ul>

Context Data

You can specify context data with command-line option '-c'.

example.rbhtml
<ul>
  <?rb for item in @items ?>
  <li>${item}</li>
  <?rb end ?>
</ul>
Result
$ rbtenjin -c '@items=["A","B","C"]' example.rbhtml
<ul>
  <li>A</li>
  <li>B</li>
  <li>C</li>
</ul>

If you want to specify several values, separate them by ';' such as '-c "@x=10;@y=20"'.

If you installed yaml library, you can specify context data in YAML format. Tenjin regards context data string as YAML format if it starts with '{'.

Result
$ rbtenjin -c '{items: [A, B, C]}' example.rbhtml
<ul>
  <li>A</li>
  <li>B</li>
  <li>C</li>
</ul>

In addition, Tenjin supports context data file in Ruby format or YAML format.

context.rb
@items = [
   "AAA",
   123,
   true,
]
Result
$ rbtenjin -f context.rb example.rbhtml
<ul>
  <li>AAA</li>
  <li>123</li>
  <li>true</li>
</ul>
context.yaml
items:
  - AAA
  - 123
  - true
Result
$ rbtenjin -f context.yaml example.rbhtml
<ul>
  <li>AAA</li>
  <li>123</li>
  <li>true</li>
</ul>

Trouble shooting

(not documented yet)

Customization Examples

This section shows how to customize rbTenjin.

Notice that these customization may be changed in the future release.

(not documented yet)