rbTenjin User's Guide
release: 0.7.1Overview
Tenjin is a very fast and full-featured template engine implemented in pure Ruby.
Features
-
Very fast
- Same as fast as Erubis
- Safe Template
- Full featured
- Easy to learn
- Because you can embed any ruby statements or expression in your template files
- You don't have to study template-specific language
- Compact
- Less than 2000 lines of code
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
$ 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)
<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>
You can check template syntax by 'rbtenjin -z'.
$ rbtenjin -z views/*.rbhtml views/page.rbhtml: Syntax OK
Render Template
This is an example code to render template file.
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
$ ruby main.rb
<h2>Tenjin Example</h2>
<table>
<tr class="odd">
<td><AAA></td>
</tr>
<tr class="even">
<td>B&B</td>
</tr>
<tr class="odd">
<td>"CCC"</td>
</tr>
</table>
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.
$ 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'.
$ 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:
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.
<!DOCTYPE>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>${@title}</title>
</head>
<body>
#{@_content}
</body>
</html>
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
$ 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><AAA></td>
</tr>
<tr class="even">
<td>B&B</td>
</tr>
<tr class="odd">
<td>"CCC"</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.
<!DOCTYPE>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>${@page_title}</title>
</head>
<body>
#{@_content}
</body>
</html>
<?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>
$ 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><AAA></td>
</tr>
<tr class="even">
<td>B&B</td>
</tr>
<tr class="odd">
<td>"CCC"</td>
</tr>
</table>
</body>
</html>
Template Arguments
For readability, it is recommended to declare context variables in your template files.
<?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>
$ 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`
<?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>
$ 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.
<?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>
<div class="header">
<h1>${@page_title}</h1>
</div>
<address> copyright(c) 2010 kuwata-lab.com all rights reserved </address>
Output result shows that header and footer templates are included as you expect.
$ 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><AAA></td>
</tr>
<tr class="even">
<td>B&B</td>
</tr>
<tr class="odd">
<td>"CCC"</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.
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
<?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.
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 '&', '<', '>', and '"' 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 " ".
text2html(" foo\n bar\n") #=> " foo<br />\n 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.
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:
SafeTemplateandSafeEngineclasses usessafe_escape()instead ofescape()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.
$ ruby safe-test.rb
_buf << %Q`s1 = #{safe_escape((@s1).to_s)}
s2 = #{safe_escape((@s2).to_s)}\n`
---------------------
s1 = <b>SOS</b>
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.
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.
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 #=> <AAA> puts safe_escape(s) #=> <AAA> # 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.
<html>
<body>
#{@_content}
</body>
</html>
<?rb @_layout = '_site_layout.rbhtml' ?>
<h2>${@title}</h2>
<!-- content -->
#{@_content}
<!-- /content -->
<?rb @_layout = '_blog_layout.rbhtml' ?>
<div class="article">
#{text2html(@post_content)}
</div>
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)
$ 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:
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.
<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 ?>
<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>
## 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.
$ 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().
## 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.
<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.
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
$ 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:
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 ${{...}}.
## normal expression
value = ${$VALUE}
## with preprocessing
value = ${{$VALUE}}
$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
${{...}} 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.
$ 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.
<?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.
$ 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.
<?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.
$ 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
:langoption respectively. - Enable preprocessing to create different cache content for each language.
The following is an example to generate M17N pages from a template file.
<div>
<?RB ## '_()' represents translator method ?>
<?RB _ = @_ ?>
<p>${{_['Hello']}} ${@username}!</p>
</div>
# -*- 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
$ 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.
$ 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.
require 'tenjin'
require 'erb'
engine = Tenjin::Engine.new(:path=>['views'], :escapefunc=>'ERB::Util.h')
puts engine.get_template('page.rbhtml').script
<p>
escaped: ${@value}
not escaped: #{@value}
</p>
$ 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.
<ul>
<?rb for item in items ?>
<li>${item}</li>
<?rb ende ?>
</ul>
$ 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.
<ul>
<?rb for item in @items ?>
<li>${item}</li>
<?rb end ?>
</ul>
$ 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)').
$ 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.
<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.
$ 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.
$ rbtenjin -Xb example.rbhtml
i = 0
for item in @items
i += 1
end
Option '-N' adds line numbers.
$ 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.
$ rbtenjin -UNXb example.rbhtml
2: i = 0
3: for item in @items
4: i += 1
9: end
Option '-C' (compact) removes empty lines.
$ 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.
<?rb @items = ['<AAA>', 'B&B', '"CCC"'] ?>
<ul>
<?rb for item in @items ?>
<li>${item}</li>
<?rb end ?>
</ul>
$ rbtenjin example.rbhtml <ul> <li><AAA></li> <li>B&B</li> <li>"CCC"</li> </ul>
Context Data
You can specify context data with command-line option '-c'.
<ul>
<?rb for item in @items ?>
<li>${item}</li>
<?rb end ?>
</ul>
$ 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 '{'.
$ 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.
@items = [ "AAA", 123, true, ]
$ rbtenjin -f context.rb example.rbhtml <ul> <li>AAA</li> <li>123</li> <li>true</li> </ul>
items: - AAA - 123 - true
$ 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)