(This is presentation slide for RubyKaigi 2009)
Erubis is very fast and extensible implementation of eRuby. In this slides, I show you features of Erubis, and issues related to eRuby and solution by Erubis. Also I show you some ideas about the future of template system.
1. RubyKaigi2009
All about Erubis
And the future of template system
makoto kuwata <kwa@kuwata-lab.com>
http://www.kuwata-lab.com/
copyright(c) 2009 kuwata-lab.com all rights reserved.
1
2. I have something to say at first...
‣ Thank you for all staff of RubyKaigi!
‣ Thank you for all audience who join this
session!
copyright(c) 2009 kuwata-lab.com all rights reserved.
2
3. Agenda
‣ Part 1. Features of Erubis
‣ Part 2.Issues about eRuby and solutions
by Erubis
‣ Part 3. Future of template system
copyright(c) 2009 kuwata-lab.com all rights reserved.
3
4. Part 1. Features of Erubis
copyright(c) 2009 kuwata-lab.com all rights reserved.
4
5. Introduction to Erubis
‣ Pure Ruby implementation of eRuby
‣ Very fast
• http://jp.rubyist.net/magazine/?0022-FasterThanC
‣ Highly functional
• HTML escape in default
• Changing embedded pattern
• Support PHP, Java, JS, C, Perl, Scheme
• and so on...
copyright(c) 2009 kuwata-lab.com all rights reserved.
5
6. Basically Usage
Ruby program:
require 'rubygems' # if need
require 'erubis'
str = File.read('template.eruby')
eruby = Erubis::Eruby.new(str)
print eruby.result(binding())
command-line:
$ erubis template.eruby # execute
$ erubis -x template.eruby # convert into Ruby
$ erubis -z template.eruby # syntax check
copyright(c) 2009 kuwata-lab.com all rights reserved.
6
7. HTML Escape in Default
str =<<END <%= %> ... WITH escaping,
<%= var %> <%== %> ... WITHOUT escaping
<%== var %>
END
eruby = Erubis::Eruby.new(str, :escape=>true)
puts eruby.result(:var=>"<B&B>")
output:
<B&am;> User can choose escape or not
<B&B> escape in default (choosability)
copyright(c) 2009 kuwata-lab.com all rights reserved.
7
8. Changing Embedded Pattern
‣ ex : use '[% %]' instead of '<% %>'
[% for x in @list %]
<li>[%= x %]</li>
You must escape regexp meta
[% end %]
characters by backslash!
## Ruby
Erubis::Eruby.new(str, :pattern=>'[% %]')
## command-line
$ erubis -p '[% %]' file.eruby
copyright(c) 2009 kuwata-lab.com all rights reserved.
8
9. Use Hash or Object instead of Binding
example of using Hash example of using Object
hash = { @title = "Example"
:title => "Example", @items = [1, 2, 3]
:items => [1, 2, 3], } erubis =
erubis = Erubis::Eruby.new(str)
Erubis::Eruby.new(str) puts erubis.evaluate(self)
puts erubis.result(hash)
<h1><%= title%></h1> <h1><%= @title%></h1>
<% for x in items %> <% for x in @items %>
<% end %> <% end %>
copyright(c) 2009 kuwata-lab.com all rights reserved.
9
10. Enhancer
‣ Ruby modules which enhances Erubis features
## do HTML escape <%= %> in default
module EscapeEnhancer
def add_expr(src, code, indicator)
if indicator == '='
src << " _buf<<escapeXml(#{code})"
elsif indicator == '=='
src << " _buf<<(#{code}).to_s;"
end
end It is easy to override Erubis features
because internal of Erubis is splitted into
end many small methods.
copyright(c) 2009 kuwata-lab.com all rights reserved.
10
11. Enhancer (cont')
### Enhance which prints into stdout
### (you can use print() in statements)
module StdoutEnhancer use _buf=$stdout
def add_preamble(src) instead of _buf=""
src << "_buf = $stdout;"
end
def add_postamble(src)
src << "n""n"
end use "" (empty string)
end instead of _buf.to_s
copyright(c) 2009 kuwata-lab.com all rights reserved.
11
12. Usage of Enhancer
All you have to do is to include
### Ruby or extend ehnacer modules
class MyEruby < Erubis::Eruby
include Erubis::EscapeEnhancer
include Erubis::PercentLineEnhancer
end
puts MyEruby.new(str).result(:items=>[1,2,3])
Specify names with ','
### command-line
$ erubis -E Escape,Percent file.eruby
copyright(c) 2009 kuwata-lab.com all rights reserved.
12
13. Standard Enhancers
‣ EscapeEnhancer : escape html in default
‣ PercentLineEnhancer : recognize lines starting with '%' as
embedded statements
‣ InterporationEnhancer : use _buf<<"#{expr}" for speed
‣ DeleteIndentEnhancer : delete HTML indentation
‣ StdoutEnhancer : use _buf=$stdout instead of _buf=""
‣ ... and so on (you can show list of all by erubis -h)
copyright(c) 2009 kuwata-lab.com all rights reserved.
13
14. Context Data
‣ You can specify data to pass into template
file (context data) in command-line
### command-line
$ erubis -c '{arr: [A, B, C]}' template.eruby # YAML
$ erubis -c '@arr=%w[A B C]' template.eruby # Ruby
<% for x in @arr %> <li>A</li>
<li><%= x %></li> <li>B</li>
<% end %> <li>C</li>
copyright(c) 2009 kuwata-lab.com all rights reserved.
14
15. Context Data File
‣ Load '*.yaml' or '*.rb' as context data file
$ erubis -f data.yaml template.eruby # YAML
$ erubis -f data.rb template.eruby # Ruby
data.yaml data.rb
title: Example @title = "Example"
items: @items =
- name: Foo [ {"name"=>"Foo"},
- name: Bar {"name"=>"Bar"}, ]
copyright(c) 2009 kuwata-lab.com all rights reserved.
15
16. Debug Print
‣ <%=== expr %> represents debug print
<%=== @var %> No need to write the
same expression twice
### Ruby code
$stderr.puts("*** debug: @var=#{@var.inspect}")
### Result
*** debug: @var=["A", "B", "C"]
copyright(c) 2009 kuwata-lab.com all rights reserved.
16
17. Support Other Programming Langs
‣ PHP, Java, JS, C, Perl,Scheme (only for convertion)
<% for (i=0; i<n; i++) { %> (example of C)
<li><%= "%d", i %>
<% } %> same format as printf()
#line 1 "file.ec" (output of erubis -xl c file.ec)
for (i=0; i<n; i++) {
fputs("<li>", stdout); fprintf(stdout, "%d", i);
fputs("n", stdout); }
copyright(c) 2009 kuwata-lab.com all rights reserved.
17
18. Conslution
‣ Erubis is very functional and extensible
• HTML escape in default
• Changing embedded pattern
• Enhancer
• Context data and file
• Debug print
• Support PHP, Java, JS, C, Perl, and Scheme
copyright(c) 2009 kuwata-lab.com all rights reserved.
18
19. Part 2. Issues about eRuby
and solutions by Erubis
copyright(c) 2009 kuwata-lab.com all rights reserved.
19
20. Issue : local variables can be changed
‣ When using binding(), local variables can be
non-local
• Difficult to find if exists
i=0 ### file.erb
str = File.read('file.erb') <% for i in 1..3 %>
ERB.new(str).result(binding) <li><%= i %></li>
p i #=> 3 <% end %>
Changed insidiously!
copyright(c) 2009 kuwata-lab.com all rights reserved.
20
21. Cause of the issue
‣ binding() passes all local variables into
template file
• It is impossible to pass only variables which you
truly want to pass
• It is hard to recognize what variables are passed
b = Bingind.new This is ideal
b[:title] = "Example" but impossible...
b[:items] = [1, 2, 3]
copyright(c) 2009 kuwata-lab.com all rights reserved.
21
22. Solution by ERB
‣ Nothing, but the author of ERB introduced a
solution to define custom Struct
• http://d.hatena.ne.jp/m_seki/20080528/1211909590
Foo = Struct.new(:title, :items)
class Foo
def env; binding(); end
end
ctx = Foo.new("Example", [1,2,3])
ERB.new(str).result(ctx.env)
copyright(c) 2009 kuwata-lab.com all rights reserved.
22
23. Solution by Erubis
‣ Use Hash instead of Binding
It is very clear what
erubis.result(:items=>[1, 2, 3]) data are passed!
def result(b=TOPLEVEL_BINDING)
if b.is_a?(Hash)
s = b.collect{|k,v| "#{k}=b[#{k.inspect}];"}.join
b = binding()
eval s, b Set hash values as local vars
end with creating new Binding
return eval(@src, b)
end
copyright(c) 2009 kuwata-lab.com all rights reserved.
23
24. Solution by Erubis (cont')
‣ Use Object instead of Binding
@items = [1, 2, 3]; <% for x in @items %>
erubis.evaluate(self) <% end %>
def evaluate(ctx) Convert Hash values into
if ctx.is_a?(Hash) instance variables
hash = ctx; ctx = Object.new
hash.each {|k,v|
ctx.instance_variable_set("@#{k}", v) }
end
return ctx.instance_eval(@src)
end copyright(c) 2009 kuwata-lab.com all rights reserved.
24
25. Issue : cost of convertion and parsing
ERB
1. 8.6 Erubis::Eruby
ERB
1. 8.7 Erubis::Eruby
ERB
1.9.1 Erubis::Eruby
0 10 20 30
(sec)
Costs of parsing and Execution
convertion are higher Parsing(by eval)
than of execution Convertion(eRuby to Ruby)
copyright(c) 2009 kuwata-lab.com all rights reserved.
25
26. Solution by ERB
‣ Convertion cost : nothing
‣ Parsing cost : helper to define method
• Usage is much different from normal usage
class Foo
extend ERB::DefMethod
def_erb_method('render', 'template.erb')
end
print Foo.new.render
copyright(c) 2009 kuwata-lab.com all rights reserved.
26
27. Solution by Erubis
‣ Convertion cost : cache Ruby code into file
• 1st time : save converted Ruby code into *.cache file
• 2nd time : read ruby code from *.cache file
eruby = Erubis::Eruby.load_file("file.eruby")
print eruby.result()
Available even in CGI
copyright(c) 2009 kuwata-lab.com all rights reserved.
27
28. Solution by Erubis (cont')
‣ Parsing cost : keep ruby code as Proc object
• The same way to use
• Almost the same speed as defining method
instance_eval can take a
def evaluate(ctx) Proc object as argument
@proc ||= eval(@src) instead of string (ruby code)
ctx.instance_eval(@proc)
end
copyright(c) 2009 kuwata-lab.com all rights reserved.
28
29. Issue: extra line breaks
‣ eRuby outpus extra line breaks
• Big problem for non-HTML text
Extra line break <ul>
<ul>
<% for x in @list %> <li>AAA</li>
<li><%= x %></li>
<% end %> <li>BBB</li>
</ul>
Extra line break
</ul>
copyright(c) 2009 kuwata-lab.com all rights reserved.
29
30. Solution by ERB
‣ Provides various trim mode
• ">" : removes LF at the end of line
• "<>" : removes LF if "<%" is at the beginning of line
and "%>" is at the end of line
• "-" : removes extra spaces and LF around
"<%-" and "-%>"
• "%" : regard lines starting with "%" as embedded
statements
• "%>", "%<>", "-" : combination of "%" and
">"/"<>"/"-"
ERB.new(str, nil, "%<>")
copyright(c) 2009 kuwata-lab.com all rights reserved.
30
31. Solution by Erubis
‣ Change operation between embedded
statement and expression
• <% stmt %> : remove spaces around it
• <%= expr %> : do nothing (leave as it is)
<ul> Remove! <ul>
<% for x in @list %> AAA
<%= x %> BBB
<% end %> CCC
</ul> Leave as it is </ul>
copyright(c) 2009 kuwata-lab.com all rights reserved.
31
32. Comparison of solutions
ERB Erubis
eRuby spec
compatible
× spec) (compatible)
(extends
Spec
simplicity
× opts) (only one rule)
(too much
Easy to
implement
× (very easy)
(complicated)
copyright(c) 2009 kuwata-lab.com all rights reserved.
32
33. Hint to think
‣ "Extra line breaks" problem has been
recognized since early times
• [ruby-list:18894] extra LF in output of eRuby
‣ Nobody hit on the idea of changing
operations between stmts and exprs
• Everybody looks <% %> and <%= %> as same
• It is important to recoginize two things which
looks to be the same things as different things
copyright(c) 2009 kuwata-lab.com all rights reserved.
33
34. Issue : Escape HTML in default
‣ <%= expr %> should be HTML escaped in
default!
• But eRuby is not only for HTML but also for all
of text file
• However security is the most important thing
‣ How to do when not to escape?
copyright(c) 2009 kuwata-lab.com all rights reserved.
34
35. Solution by ERB
‣ Nothing for officially
‣ Unofficial solution
• Define a certain class which represents HTML string
(not to escape) separately from String class
• http://www2a.biglobe.ne.jp/~seki/ruby/erbquote.html
copyright(c) 2009 kuwata-lab.com all rights reserved.
35
36. Solution by Erubis
‣ Enhance embedded pattern and Erubis class
• Fast, and easy to implement
eruby = Erubis::Eruby.new(str, :escape=>true)
# or eruby = Erubis::EscapedEruby.new(str)
puts eruby.evaluate(ctx)
Hi <%= @name %>! # with escape
Hi <%== @name %>! # without escape
copyright(c) 2009 kuwata-lab.com all rights reserved.
36
37. Issue : hard to find syntax error
<% unless @items.blank? %>
<table>
<tbody>
<% @items.each do |item| %>
<tr class="item" id="item-<%=item.id%>">
<td class="item-id"><%= item.id %></td>
<td class="item-name">
<% if item.url && !item.url.empty? %>
<a href="<%= item.url %>"><%=item.name%></a>
<% else %>
<span><%=item.name%></span>
<% end %>
</td>
</tr> • HTML and Ruby code are mixed
<% end %> • It is hard to recognize corresponding
</tbody>
</table>
'end' (because 'do' and 'end' can be
<% end %> separated 100 lines for example)
copyright(c) 2009 kuwata-lab.com all rights reserved.
37
38. Solution by ERB
‣ Nothing but '-x' option
$ erb -x foo.eruby
_erbout = ''; unless @items.blank? ;
_erbout.concat "n"
_erbout.concat "<table>n"
_erbout.concat " <tr class="record">n"
$ erb -x foo.eruby | ruby -wc
Syntax OK
copyright(c) 2009 kuwata-lab.com all rights reserved.
38
39. Solution by Erubis
‣ Provides a lot of command-line options
• -x : show Ruby script
• -X : suppress to print HTML
• -N : print line numbers
• -U : unify consecutive empty lines into a line
• -C : remove consecutive empty lines (compact)
• -z : check template syntax
copyright(c) 2009 kuwata-lab.com all rights reserved.
39
40. $ cat foo.eruby
<% unless @items.blank? %>
<table>
<% @items.each_with_index do|x, i| %>
<tr class="record">
<td><%= i +1 %></td>
<td><%=h x %></td>
</tr>
<% end %>
</table>
<% end %>
copyright(c) 2009 kuwata-lab.com all rights reserved.
40
41. -x : show Ruby script
$ erubis -x foo.eruby
_buf = ''; unless @items.blank?
_buf << '<table>
'; @items.each_with_index do|x, i|
_buf << ' <tr class="record">
<td>'; _buf << ( i +1 ).to_s; _buf << '</td>
<td>'; _buf << (h x ).to_s; _buf << '</td>
</tr>
'; end
_buf << '</table>
'; end
_buf.to_s
copyright(c) 2009 kuwata-lab.com all rights reserved.
41
42. -X : suppress to print HTML
$ erubis -X foo.eruby
_buf = ''; unless @items.blank?
@items.each_with_index do|x, i|
_buf << ( i +1 ).to_s;
_buf << (h x ).to_s;
end
end
_buf.to_s
copyright(c) 2009 kuwata-lab.com all rights reserved.
42
43. -N : print line numbers
$ erubis -XN foo.eruby
1: _buf = ''; unless @items.blank?
2:
3: @items.each_with_index do|x, i|
4:
5: _buf << ( i +1 ).to_s;
6: _buf << (h x ).to_s;
7:
8: end
9:
10: end
11: _buf.to_s
copyright(c) 2009 kuwata-lab.com all rights reserved.
43
44. -U : unifiy consecutive empty
lines into a line
$ erubis -XNU foo.eruby
1: _buf = ''; unless @items.blank?
3: @items.each_with_index do|x, i|
5: _buf << ( i +1 ).to_s;
6: _buf << (h x ).to_s;
8: end
10: end
11: _buf.to_s
copyright(c) 2009 kuwata-lab.com all rights reserved.
44
45. -C : remove empty lines
(compact)
$ erubis -XNC foo.eruby
1: _buf = ''; unless @items.blank?
3: @items.each_with_index do|x, i|
5: _buf << ( i +1 ).to_s;
6: _buf << (h x ).to_s;
8: end
10: end
11: _buf.to_s
copyright(c) 2009 kuwata-lab.com all rights reserved.
45
46. Issue : expr can contain statements
embed return value of
helper method by <%= %>
block contains
statements
<%= form_for :user do %>
<div>
<%= text_field :name %>
</div>
<% end %> beyond of
eRuby spec!
copyright(c) 2009 kuwata-lab.com all rights reserved.
46
47. Cause of Issue
<%= 10.times do %>
Hello <%= expr %> is expected
<% end %> to be completed by itself
Convert
Syntax error!
_buf = "";
_buf << ( 10.times do ).to_s;
_buf << " Hellon";
end
copyright(c) 2009 kuwata-lab.com all rights reserved.
47
48. Solution by ERB+Rails
‣ Change local variable (_erbout) on caller-
size from callee-side agic!!
b lack m
Append to '_erbout' from
Not use <%= %> internal of form_for()
<% form_for do %> _erbout = ""
Hello form_for do
<% end %> _erbout.concat("Hello")
end
copyright(c) 2009 kuwata-lab.com all rights reserved.
48
49. Solution by Erubis+Merb
‣ Extend parser of Erubis
Recognize blok in
embedded expr
<%= form_for do %> @_buf << (form_for do;
Hello @_buf << "Hellon"
<% end =%> end);
Introduce end-of- Change _buf into
block notation instance variable
copyright(c) 2009 kuwata-lab.com all rights reserved.
49
50. Discussion
‣ Extend spec of eRuby
‣ Not use black magic (kool!)
‣ Available only for helper method, in fact
• It is required to manipulate @_buf from internal
of helper method
‣ Difficult to provide general solution
copyright(c) 2009 kuwata-lab.com all rights reserved.
50
51. Conslusion
‣ A lot of issues around eRuby!
• Extra line breaks
• Local variables are not local
• Difficult to specify context variable
• Large cost for convertion and parsing
• HTML escape in default
• Difficult to find syntax error
• Can't embed return value of method with block
copyright(c) 2009 kuwata-lab.com all rights reserved.
51
52. Part 3. Future of template
system
copyright(c) 2009 kuwata-lab.com all rights reserved.
52
53. Template and Programming
‣ Template is also program code
<ul> print "<ul>n"
<% for x in @a %> for x in @a
<li><%=x%></li> Equiv. print "<li>#{x}</li>n"
<% end %> end
</ul> print "</u>n"
Possible to apply programming techniques
or concepts to template system
copyright(c) 2009 kuwata-lab.com all rights reserved.
53
54. Template and Method
Template is a kind of
method definition
<ul> s = File.read('foo.eruby')
<li><%=x%></li> e = Erubis::Eruby.new(s)
</ul> puts e.evaluate(:x=>1)
Context data is actual
Rendering template is a argument for method
kind of method invokation
copyright(c) 2009 kuwata-lab.com all rights reserved.
54
55. Template and formal argument
‣ Formal arguments may be necessary for
template
<%#ARGS: items, name='guest' %>
Hello <%= name %>!
<% for x in items %>
<li><%=x%></li> • Clear context variables
<% end %> • Available default value
copyright(c) 2009 kuwata-lab.com all rights reserved.
55
56. Template and modularity
‣ Also HTML should be Small & Many, not
Single & Large
• Same as principle of method definition
<html> <html> Be benefit for
<body> designer!
<body> </body>
<h1><%=@title%></h1> </html>
<ul id="menulist">
split
<% for x in @items %> <h1><%=@title%></h1>
<li><%=x%></li> <ul id="menulist">
</ul>
<% end %>
</ul> <% for x in @items %>
</body> <li><%= x %></li>
</html> <% end %>
copyright(c) 2009 kuwata-lab.com all rights reserved.
56
57. Template and Object-Oriented
‣ Template inheritance in Django
Parent template
....
Available to overwrite
{% block pagetitle %}
or add contents
<h1>{{title}}</h1> before/after
{% endblock %} (method override)
....
copyright(c) 2009 kuwata-lab.com all rights reserved.
57
58. Template and Aspect-Oriented
‣ Weave some code into other program
• Similar to layer of Photoshop
<table>
"for x in @a" •Enable to split HTML
<tr> and presentation
<td> "print x" logics
•Available to insert a
</tr> logic into several
"end" points (DRY)
</table>
copyright(c) 2009 kuwata-lab.com all rights reserved.
58
59. Template and Data Type
‣ End is coming to escape HTML in view layer
• forget to escape, helper method argument, ...
‣ HTML should be different data type from String
(http://www.oiwa.jp/~yutaka/tdiary/20051229.html)
• No need to take care to escape or not
• Prior art : str and unicode in Python
• "HTML + String" should be String? or HTML?
• Other escaping also should be considered (ex. SQL)
copyright(c) 2009 kuwata-lab.com all rights reserved.
59
60. Conslusion
‣ Template is also programming code
‣ Available to apply programming techniques
and concepts into template system
• Formal argument, Inheritance, AOP, and so on
‣ Template system is still on developing stage
copyright(c) 2009 kuwata-lab.com all rights reserved.
60
61. Bibliography
‣ Introduction to Template System (in Japanese)
• http://jp.rubyist.net/magazine/?0024-TemplateSystem
• http://jp.rubyist.net/magazine/?0024-TemplateSystem2
‣ Erubis
• http://www.kuwata-lab.com/erubis/
‣ Benchmarks of many template systems
• http://www.kuwata-lab.com/tenjin/
copyright(c) 2009 kuwata-lab.com all rights reserved.
61
62. one more thing
copyright(c) 2009 kuwata-lab.com all rights reserved.
62
63. Tenjin - template engine replacing eRuby
‣ Both ERB and Erubis are out of date
• They are merely text-processor
• Less features as template engine
‣ Tenjin : replacer of ERB/Erubis
• Designed and implemented as template engine from the
beginning
• Provides a lot of features required for template engines
- layout template, partial template, and so on
• http://www.kuwata-lab.com/tenjin/
copyright(c) 2009 kuwata-lab.com all rights reserved.
63