tenjin 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/MIT-LICENSE +20 -0
- data/README.txt +54 -0
- data/benchmark/bench.rb +502 -0
- data/benchmark/bench_context.rb +17 -0
- data/benchmark/bench_context.yaml +141 -0
- data/benchmark/templates/_footer.html +4 -0
- data/benchmark/templates/_header.html +52 -0
- data/benchmark/templates/bench_eruby.rhtml +29 -0
- data/benchmark/templates/bench_tenjin.rbhtml +29 -0
- data/bin/rbtenjin +449 -0
- data/doc-api/classes/Tenjin.html +141 -0
- data/doc-api/classes/Tenjin/ArrayBufferTemplate.html +270 -0
- data/doc-api/classes/Tenjin/BaseContext.html +312 -0
- data/doc-api/classes/Tenjin/Context.html +126 -0
- data/doc-api/classes/Tenjin/ContextHelper.html +433 -0
- data/doc-api/classes/Tenjin/Engine.html +616 -0
- data/doc-api/classes/Tenjin/ErubisTemplate.html +166 -0
- data/doc-api/classes/Tenjin/HtmlHelper.html +359 -0
- data/doc-api/classes/Tenjin/Preprocessor.html +242 -0
- data/doc-api/classes/Tenjin/Template.html +916 -0
- data/doc-api/created.rid +1 -0
- data/doc-api/files/README_txt.html +185 -0
- data/doc-api/files/lib/tenjin_rb.html +136 -0
- data/doc-api/fr_class_index.html +36 -0
- data/doc-api/fr_file_index.html +28 -0
- data/doc-api/fr_method_index.html +89 -0
- data/doc-api/index.html +24 -0
- data/doc-api/rdoc-style.css +208 -0
- data/doc/docstyle.css +188 -0
- data/doc/examples.html +312 -0
- data/doc/faq.html +909 -0
- data/doc/users-guide.html +1691 -0
- data/lib/tenjin.rb +959 -0
- data/setup.rb +1331 -0
- data/tenjin.gemspec +58 -0
- data/test/assert-text-equal.rb +45 -0
- data/test/data/examples/form/create.rbhtml +4 -0
- data/test/data/examples/form/form.rbhtml +14 -0
- data/test/data/examples/form/layout.rbhtml +8 -0
- data/test/data/examples/form/main.rb +9 -0
- data/test/data/examples/form/main.result +21 -0
- data/test/data/examples/form/update.rbhtml +4 -0
- data/test/data/examples/preprocessing/helper.rb +16 -0
- data/test/data/examples/preprocessing/main.rb +11 -0
- data/test/data/examples/preprocessing/main.result +17 -0
- data/test/data/examples/preprocessing/select.rbhtml +15 -0
- data/test/data/examples/preprocessing/select_P.result +18 -0
- data/test/data/examples/table/table.rb +9 -0
- data/test/data/examples/table/table.rbhtml +16 -0
- data/test/data/examples/table/table.result +20 -0
- data/test/data/examples/table/table_s.result +18 -0
- data/test/data/faq/ex1.rbhtml +5 -0
- data/test/data/faq/ex10-baselayout.rbhtml +27 -0
- data/test/data/faq/ex10-content.rbhtml +12 -0
- data/test/data/faq/ex10-customlayout.rbhtml +11 -0
- data/test/data/faq/ex10_inherit.result +25 -0
- data/test/data/faq/ex11-bench.rb +28 -0
- data/test/data/faq/ex11-content.rbhtml +9 -0
- data/test/data/faq/ex11-layout1.rbhtml +5 -0
- data/test/data/faq/ex11-layout2.rbhtml +6 -0
- data/test/data/faq/ex11.rb +5 -0
- data/test/data/faq/ex11.rbhtml +8 -0
- data/test/data/faq/ex11.source +11 -0
- data/test/data/faq/ex11_arraybuffer.result +15 -0
- data/test/data/faq/ex1_chksyntax.result +3 -0
- data/test/data/faq/ex2-content.rbhtml +3 -0
- data/test/data/faq/ex2-layout.rbhtml +11 -0
- data/test/data/faq/ex2_removenl.result +19 -0
- data/test/data/faq/ex3.rb +4 -0
- data/test/data/faq/ex3.rbhtml +1 -0
- data/test/data/faq/ex3_escapefunc1.result +2 -0
- data/test/data/faq/ex3_escapefunc2.result +2 -0
- data/test/data/faq/ex5.rbhtml +7 -0
- data/test/data/faq/ex5_template_args.source +9 -0
- data/test/data/faq/ex6-content.rhtml +6 -0
- data/test/data/faq/ex6-layout.rhtml +6 -0
- data/test/data/faq/ex6.rb +10 -0
- data/test/data/faq/ex6_eruby.result +12 -0
- data/test/data/faq/ex7-expr-pattern.rb +34 -0
- data/test/data/faq/ex7-expr-pattern.rbhtml +3 -0
- data/test/data/faq/ex7_expr_pattern.result +4 -0
- data/test/data/faq/ex8-m18n.rb +77 -0
- data/test/data/faq/ex8-m18n.rbhtml +4 -0
- data/test/data/faq/ex8_m18n.result +10 -0
- data/test/data/faq/ex9-baselayout.rbhtml +8 -0
- data/test/data/faq/ex9-content.rbhtml +6 -0
- data/test/data/faq/ex9-mylayout.rbhtml +5 -0
- data/test/data/faq/ex9_changelayout.result +11 -0
- data/test/data/users_guide/content6.rbhtml +3 -0
- data/test/data/users_guide/content7.rbhtml +5 -0
- data/test/data/users_guide/content8.rbhtml +2 -0
- data/test/data/users_guide/contextdata.rb +7 -0
- data/test/data/users_guide/datafile.rb +5 -0
- data/test/data/users_guide/datafile.yaml +10 -0
- data/test/data/users_guide/ex.rbhtml +6 -0
- data/test/data/users_guide/ex.result +7 -0
- data/test/data/users_guide/ex.script +5 -0
- data/test/data/users_guide/ex_script.result +7 -0
- data/test/data/users_guide/ex_source.result +8 -0
- data/test/data/users_guide/example1.rbhtml +12 -0
- data/test/data/users_guide/example1.result +17 -0
- data/test/data/users_guide/example10.rbhtml +4 -0
- data/test/data/users_guide/example10_template_args.result +6 -0
- data/test/data/users_guide/example11.rbhtml +5 -0
- data/test/data/users_guide/example11_template_args_result +2 -0
- data/test/data/users_guide/example12.rbhtml +12 -0
- data/test/data/users_guide/example12_preprocessed.result +10 -0
- data/test/data/users_guide/example12_preprocessed_source.result +10 -0
- data/test/data/users_guide/example13.rbhtml +6 -0
- data/test/data/users_guide/example13_preprocessed.result +2 -0
- data/test/data/users_guide/example13_preprocessed_source.result +2 -0
- data/test/data/users_guide/example14.rb +32 -0
- data/test/data/users_guide/example14.rbhtml +6 -0
- data/test/data/users_guide/example14_tmplclass.result +15 -0
- data/test/data/users_guide/example15.rb +10 -0
- data/test/data/users_guide/example15_escapefunc.result +14 -0
- data/test/data/users_guide/example16.rbhtml +4 -0
- data/test/data/users_guide/example16a.rb +10 -0
- data/test/data/users_guide/example16a.result +4 -0
- data/test/data/users_guide/example16b.rb +13 -0
- data/test/data/users_guide/example16b.result +4 -0
- data/test/data/users_guide/example16c.rb +12 -0
- data/test/data/users_guide/example16c.result +4 -0
- data/test/data/users_guide/example1_S.result +14 -0
- data/test/data/users_guide/example1_SXNC.result +10 -0
- data/test/data/users_guide/example1_source.result +14 -0
- data/test/data/users_guide/example2.rbhtml +3 -0
- data/test/data/users_guide/example2_sb.result2 +9 -0
- data/test/data/users_guide/example3.rbhtml +5 -0
- data/test/data/users_guide/example3_syntaxcheck.result +2 -0
- data/test/data/users_guide/example4.rbhtml +13 -0
- data/test/data/users_guide/example4_datafile_rb.result +13 -0
- data/test/data/users_guide/example4_yaml.result +13 -0
- data/test/data/users_guide/example5.rbhtml +9 -0
- data/test/data/users_guide/example5_datastr_rb.result +9 -0
- data/test/data/users_guide/example5_datastr_yaml.result +9 -0
- data/test/data/users_guide/example6.rbhtml +19 -0
- data/test/data/users_guide/example6_layout.result +29 -0
- data/test/data/users_guide/example6_nested.result +28 -0
- data/test/data/users_guide/example7_layout2.result +13 -0
- data/test/data/users_guide/example8_layout3.result +8 -0
- data/test/data/users_guide/example9.rbhtml +18 -0
- data/test/data/users_guide/example9_capture.result +26 -0
- data/test/data/users_guide/footer.html +5 -0
- data/test/data/users_guide/footer.rbhtml +4 -0
- data/test/data/users_guide/layout6.rbhtml +17 -0
- data/test/data/users_guide/layout7.rbhtml +9 -0
- data/test/data/users_guide/layout8_html.rbhtml +5 -0
- data/test/data/users_guide/layout8_xhtml.rbhtml +6 -0
- data/test/data/users_guide/layout9.rbhtml +25 -0
- data/test/data/users_guide/sidemenu.rbhtml +5 -0
- data/test/data/users_guide/user_app.cgi +39 -0
- data/test/data/users_guide/user_app.result +30 -0
- data/test/data/users_guide/user_create.rbhtml +6 -0
- data/test/data/users_guide/user_edit.rbhtml +7 -0
- data/test/data/users_guide/user_form.rbhtml +10 -0
- data/test/data/users_guide/user_layout.rbhtml +16 -0
- data/test/test_all.rb +23 -0
- data/test/test_engine.rb +526 -0
- data/test/test_engine.yaml +2039 -0
- data/test/test_examples.rb +81 -0
- data/test/test_faq.rb +60 -0
- data/test/test_htmlhelper.rb +78 -0
- data/test/test_main.rb +564 -0
- data/test/test_main.yaml +174 -0
- data/test/test_template.rb +113 -0
- data/test/test_template.yaml +1244 -0
- data/test/test_users_guide.rb +75 -0
- data/test/testcase-helper.rb +166 -0
- metadata +226 -0
data/lib/tenjin.rb
ADDED
|
@@ -0,0 +1,959 @@
|
|
|
1
|
+
##
|
|
2
|
+
## copyright(c) 2007 kuwata-lab all rights reserved.
|
|
3
|
+
##
|
|
4
|
+
## Permission is hereby granted, free of charge, to any person obtaining
|
|
5
|
+
## a copy of this software and associated documentation files (the
|
|
6
|
+
## "Software"), to deal in the Software without restriction, including
|
|
7
|
+
## without limitation the rights to use, copy, modify, merge, publish,
|
|
8
|
+
## distribute, sublicense, and/or sell copies of the Software, and to
|
|
9
|
+
## permit persons to whom the Software is furnished to do so, subject to
|
|
10
|
+
## the following conditions:
|
|
11
|
+
##
|
|
12
|
+
## The above copyright notice and this permission notice shall be
|
|
13
|
+
## included in all copies or substantial portions of the Software.
|
|
14
|
+
##
|
|
15
|
+
## THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
16
|
+
## EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
17
|
+
## MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
18
|
+
## NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
19
|
+
## LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
20
|
+
## OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
21
|
+
## WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
22
|
+
##
|
|
23
|
+
|
|
24
|
+
##
|
|
25
|
+
## Tenjin module
|
|
26
|
+
##
|
|
27
|
+
## $Rev: 59 $
|
|
28
|
+
## $Release: 0.6.0 $
|
|
29
|
+
##
|
|
30
|
+
|
|
31
|
+
module Tenjin
|
|
32
|
+
|
|
33
|
+
RELEASE = ('$Release: 0.6.0 $' =~ /[\d.]+/) && $&
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
##
|
|
37
|
+
## helper module for Context class
|
|
38
|
+
##
|
|
39
|
+
module HtmlHelper
|
|
40
|
+
|
|
41
|
+
module_function
|
|
42
|
+
|
|
43
|
+
XML_ESCAPE_TABLE = { '&'=>'&', '<'=>'<', '>'=>'>', '"'=>'"', "'"=>''' }
|
|
44
|
+
|
|
45
|
+
def escape_xml(s)
|
|
46
|
+
#return s.gsub(/[&<>"]/) { XML_ESCAPE_TABLE[$&] }
|
|
47
|
+
return s.gsub(/[&<>"]/) { |s| XML_ESCAPE_TABLE[s] }
|
|
48
|
+
##
|
|
49
|
+
#s = s.gsub(/&/, '&')
|
|
50
|
+
#s.gsub!(/</, '<')
|
|
51
|
+
#s.gsub!(/>/, '>')
|
|
52
|
+
#s.gsub!(/"/, '"')
|
|
53
|
+
#return s
|
|
54
|
+
##
|
|
55
|
+
#return s.gsub(/&/, '&').gsub(/</, '<').gsub(/>/, '>').gsub(/"/, '"')
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
alias escape escape_xml
|
|
59
|
+
|
|
60
|
+
## (experimental) return ' name="value"' if expr is not false nor nil.
|
|
61
|
+
## if value is nil or false then expr is used as value.
|
|
62
|
+
def tagattr(name, expr, value=nil, escape=true)
|
|
63
|
+
if !expr
|
|
64
|
+
return ''
|
|
65
|
+
elsif escape
|
|
66
|
+
return " #{name}=\"#{escape_xml((value || expr).to_s)}\""
|
|
67
|
+
else
|
|
68
|
+
return " #{name}=\"#{value || expr}\""
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
## return ' checked="checked"' if expr is not false or nil
|
|
73
|
+
def checked(expr)
|
|
74
|
+
return expr ? ' checked="checked"' : ''
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
## return ' selected="selected"' if expr is not false or nil
|
|
78
|
+
def selected(expr)
|
|
79
|
+
return expr ? ' selected="selected"' : ''
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
## return ' disabled="disabled"' if expr is not false or nil
|
|
83
|
+
def disabled(expr)
|
|
84
|
+
return expr ? ' disabled="disabled"' : ''
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
## convert "\n" into "<br />\n"
|
|
88
|
+
def nl2br(text)
|
|
89
|
+
return text.to_s.gsub(/\n/, "<br />\n")
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
## convert "\n" and " " into "<br />\n" and " "
|
|
93
|
+
def text2html(text)
|
|
94
|
+
return nl2br(escape_xml(text.to_s).gsub(/ /, ' '))
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
##
|
|
101
|
+
## helper module for BaseContext class
|
|
102
|
+
##
|
|
103
|
+
module ContextHelper
|
|
104
|
+
|
|
105
|
+
attr_accessor :_buf, :_engine, :_layout
|
|
106
|
+
|
|
107
|
+
## include template. 'template_name' can be filename or short name.
|
|
108
|
+
def import(template_name, _append_to_buf=true)
|
|
109
|
+
_buf = self._buf
|
|
110
|
+
output = self._engine.render(template_name, context=self, layout=false)
|
|
111
|
+
_buf << output if _append_to_buf
|
|
112
|
+
return output
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
## add value into _buf. this is equivarent to '#{value}'.
|
|
116
|
+
def echo(value)
|
|
117
|
+
self._buf << value
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
##
|
|
121
|
+
## start capturing.
|
|
122
|
+
## returns captured string if block given, else return nil.
|
|
123
|
+
## if block is not given, calling stop_capture() is required.
|
|
124
|
+
##
|
|
125
|
+
## ex. list.rbhtml
|
|
126
|
+
## <html><body>
|
|
127
|
+
## <h1><?rb start_capture(:title) do ?>Document Title<?rb end ?></h1>
|
|
128
|
+
## <?rb start_capture(:content) ?>
|
|
129
|
+
## <ul>
|
|
130
|
+
## <?rb for item in list do ?>
|
|
131
|
+
## <li>${item}</li>
|
|
132
|
+
## <?rb end ?>
|
|
133
|
+
## </ul>
|
|
134
|
+
## <?rb stop_capture() ?>
|
|
135
|
+
## </body></html>
|
|
136
|
+
##
|
|
137
|
+
## ex. layout.rbhtml
|
|
138
|
+
## <?xml version="1.0" ?>
|
|
139
|
+
## <html xml:lang="en">
|
|
140
|
+
## <head>
|
|
141
|
+
## <title>${@title}</title>
|
|
142
|
+
## </head>
|
|
143
|
+
## <body>
|
|
144
|
+
## <h1>${@title}</h1>
|
|
145
|
+
## <div id="content">
|
|
146
|
+
## <?rb echo(@content) ?>
|
|
147
|
+
## </div>
|
|
148
|
+
## </body>
|
|
149
|
+
## </html>
|
|
150
|
+
##
|
|
151
|
+
def start_capture(varname=nil)
|
|
152
|
+
@_capture_varname = varname
|
|
153
|
+
@_start_position = self._buf.length
|
|
154
|
+
if block_given?
|
|
155
|
+
yield
|
|
156
|
+
output = stop_capture()
|
|
157
|
+
return output
|
|
158
|
+
else
|
|
159
|
+
return nil
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
##
|
|
164
|
+
## stop capturing.
|
|
165
|
+
## returns captured string.
|
|
166
|
+
## see start_capture()'s document.
|
|
167
|
+
##
|
|
168
|
+
def stop_capture(store_to_context=true)
|
|
169
|
+
output = self._buf[@_start_position..-1]
|
|
170
|
+
self._buf[@_start_position..-1] = ''
|
|
171
|
+
@_start_position = nil
|
|
172
|
+
if @_capture_varname
|
|
173
|
+
self.instance_variable_set("@#{@_capture_varname}", output) if store_to_context
|
|
174
|
+
@_capture_varname = nil
|
|
175
|
+
end
|
|
176
|
+
return output
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
##
|
|
180
|
+
## if captured string is found then add it to _buf and return true,
|
|
181
|
+
## else return false.
|
|
182
|
+
## this is a helper method for layout template.
|
|
183
|
+
##
|
|
184
|
+
def captured_as(name)
|
|
185
|
+
str = self.instance_variable_get("@#{name}")
|
|
186
|
+
return false unless str
|
|
187
|
+
@_buf << str
|
|
188
|
+
return true
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
##
|
|
192
|
+
## ex. _p("item['name']") => #{item['name']}
|
|
193
|
+
##
|
|
194
|
+
def _p(arg)
|
|
195
|
+
return "<`\##{arg}\#`>" # decoded into #{...} by preprocessor
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
##
|
|
199
|
+
## ex. _P("item['name']") => ${item['name']}
|
|
200
|
+
##
|
|
201
|
+
def _P(arg)
|
|
202
|
+
return "<`$#{arg}$`>" # decoded into ${...} by preprocessor
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
##
|
|
206
|
+
## decode <`#...#`> and <`$...$`> into #{...} and ${...}
|
|
207
|
+
##
|
|
208
|
+
def _decode_params(s)
|
|
209
|
+
require 'cgi'
|
|
210
|
+
return s unless s.is_a?(String)
|
|
211
|
+
s = s.dup
|
|
212
|
+
s.gsub!(/%3C%60%23(.*?)%23%60%3E/im) { "\#\{#{CGI::unescape($1)}\}" }
|
|
213
|
+
s.gsub!(/%3C%60%24(.*?)%24%60%3E/im) { "\$\{#{CGI::unescape($1)}\}" }
|
|
214
|
+
s.gsub!(/<`\#(.*?)\#`>/m) { "\#\{#{CGI::unescapeHTML($1)}\}" }
|
|
215
|
+
s.gsub!(/<`\$(.*?)\$`>/m) { "\$\{#{CGI::unescapeHTML($1)}\}" }
|
|
216
|
+
s.gsub!(/<`\#(.*?)\#`>/m, '#{\1}')
|
|
217
|
+
s.gsub!(/<`\$(.*?)\$`>/m, '${\1}')
|
|
218
|
+
return s
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
##
|
|
225
|
+
## base class for Context class
|
|
226
|
+
##
|
|
227
|
+
class BaseContext
|
|
228
|
+
include Enumerable
|
|
229
|
+
include ContextHelper
|
|
230
|
+
|
|
231
|
+
def initialize(vars=nil)
|
|
232
|
+
update(vars) if vars.is_a?(Hash)
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def [](key)
|
|
236
|
+
instance_variable_get("@#{key}")
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def []=(key, val)
|
|
240
|
+
instance_variable_set("@#{key}", val)
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def escape(val)
|
|
244
|
+
return val
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def update(hash)
|
|
248
|
+
hash.each do |key, val|
|
|
249
|
+
self[key] = val
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def key?(key)
|
|
254
|
+
return self.instance_variables.include?("@#{key}")
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def each()
|
|
258
|
+
instance_variables().each do |name|
|
|
259
|
+
val = instance_variable_get(name)
|
|
260
|
+
key = name[1..-1]
|
|
261
|
+
yield([key, val]) if name != '@_buf' && name != '@_engine'
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
##
|
|
269
|
+
## context class for Template
|
|
270
|
+
##
|
|
271
|
+
class Context < BaseContext
|
|
272
|
+
include HtmlHelper
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
##
|
|
277
|
+
## template class
|
|
278
|
+
##
|
|
279
|
+
## ex. file 'example.rbhtml'
|
|
280
|
+
## <html>
|
|
281
|
+
## <body>
|
|
282
|
+
## <h1>${@title}</h1>
|
|
283
|
+
## <ul>
|
|
284
|
+
## <?rb i = 0 ?>
|
|
285
|
+
## <?rb for item in @items ?>
|
|
286
|
+
## <?rb i += 1 ?>
|
|
287
|
+
## <li>#{i} : ${item}</li>
|
|
288
|
+
## <?rb end ?>
|
|
289
|
+
## </ul>
|
|
290
|
+
## </body>
|
|
291
|
+
## </html>
|
|
292
|
+
##
|
|
293
|
+
## ex. convertion
|
|
294
|
+
## require 'tenjin'
|
|
295
|
+
## template = Tenjin::Template.new('example.rbhtml')
|
|
296
|
+
## print template.script
|
|
297
|
+
## ## or
|
|
298
|
+
## # template = Tenjin::Template.new()
|
|
299
|
+
## # print template.convert_file('example.rbhtml')
|
|
300
|
+
## ## or
|
|
301
|
+
## # template = Tenjin::Template.new()
|
|
302
|
+
## # fname = 'example.rbhtml'
|
|
303
|
+
## # print template.convert(File.read(fname), fname) # filename is optional
|
|
304
|
+
##
|
|
305
|
+
## ex. evaluation
|
|
306
|
+
## context = {:title=>'Tenjin Example', :items=>['foo', 'bar', 'baz'] }
|
|
307
|
+
## output = template.render(context)
|
|
308
|
+
## ## or
|
|
309
|
+
## # context = Tenjin::Context(:title=>'Tenjin Example', :items=>['foo','bar','baz'])
|
|
310
|
+
## # output = template.render(context)
|
|
311
|
+
## ## or
|
|
312
|
+
## # output = template.render(:title=>'Tenjin Example', :items=>['foo','bar','baz'])
|
|
313
|
+
## print output
|
|
314
|
+
##
|
|
315
|
+
class Template
|
|
316
|
+
|
|
317
|
+
ESCAPE_FUNCTION = 'escape' # or 'Eruby::Helper.escape'
|
|
318
|
+
|
|
319
|
+
##
|
|
320
|
+
## initializer of Template class.
|
|
321
|
+
##
|
|
322
|
+
## options:
|
|
323
|
+
## :escapefunc :: function name to escape value (default 'escape')
|
|
324
|
+
## :preamble :: preamble such as "_buf = ''" (default nil)
|
|
325
|
+
## :postamble :: postamble such as "_buf.to_s" (default nil)
|
|
326
|
+
##
|
|
327
|
+
def initialize(filename=nil, options={})
|
|
328
|
+
if filename.is_a?(Hash)
|
|
329
|
+
options = filename
|
|
330
|
+
filename = nil
|
|
331
|
+
end
|
|
332
|
+
@filename = filename
|
|
333
|
+
@escapefunc = options[:escapefunc] || ESCAPE_FUNCTION
|
|
334
|
+
@preamble = options[:preamble] == true ? "_buf = #{init_buf_expr()}; " : options[:preamble]
|
|
335
|
+
@postamble = options[:postamble] == true ? "_buf.to_s" : options[:postamble]
|
|
336
|
+
@args = nil # or array of argument names
|
|
337
|
+
convert_file(filename) if filename
|
|
338
|
+
end
|
|
339
|
+
attr_accessor :filename, :escapefunc, :initbuf, :newline
|
|
340
|
+
attr_accessor :timestamp, :args
|
|
341
|
+
attr_accessor :script #,:bytecode
|
|
342
|
+
|
|
343
|
+
## convert file into ruby code
|
|
344
|
+
def convert_file(filename)
|
|
345
|
+
return convert(File.read(filename), filename)
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
## convert string into ruby code
|
|
349
|
+
def convert(input, filename=nil)
|
|
350
|
+
@input = input
|
|
351
|
+
@filename = filename
|
|
352
|
+
@proc = nil
|
|
353
|
+
pos = input.index(?\n)
|
|
354
|
+
if pos && input[pos-1] == ?\r
|
|
355
|
+
@newline = "\r\n"
|
|
356
|
+
@newlinestr = '\\r\\n'
|
|
357
|
+
else
|
|
358
|
+
@newline = "\n"
|
|
359
|
+
@newlinestr = '\\n'
|
|
360
|
+
end
|
|
361
|
+
before_convert()
|
|
362
|
+
parse_stmts(input)
|
|
363
|
+
after_convert()
|
|
364
|
+
return @script
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
protected
|
|
368
|
+
|
|
369
|
+
## hook method called before convert()
|
|
370
|
+
def before_convert()
|
|
371
|
+
@script = ''
|
|
372
|
+
@script << @preamble if @preamble
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
## hook method called after convert()
|
|
376
|
+
def after_convert()
|
|
377
|
+
@script << @newline unless @script[-1] == ?\n
|
|
378
|
+
@script << @postamble << @newline if @postamble
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
def self.compile_stmt_pattern(pi)
|
|
382
|
+
return /<\?#{pi}( |\t|\r?\n)(.*?) ?\?>([ \t]*\r?\n)?/m
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
STMT_PATTERN = self.compile_stmt_pattern('rb')
|
|
386
|
+
|
|
387
|
+
def stmt_pattern
|
|
388
|
+
STMT_PATTERN
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
## parse statements ('<?rb ... ?>')
|
|
392
|
+
def parse_stmts(input)
|
|
393
|
+
return unless input
|
|
394
|
+
is_bol = true
|
|
395
|
+
prev_rspace = nil
|
|
396
|
+
pos = 0
|
|
397
|
+
input.scan(stmt_pattern()) do |mspace, code, rspace|
|
|
398
|
+
m = Regexp.last_match
|
|
399
|
+
text = input[pos, m.begin(0) - pos]
|
|
400
|
+
pos = m.end(0)
|
|
401
|
+
## detect spaces at beginning of line
|
|
402
|
+
lspace = nil
|
|
403
|
+
if rspace.nil?
|
|
404
|
+
# nothing
|
|
405
|
+
elsif text.empty?
|
|
406
|
+
lspace = "" if is_bol
|
|
407
|
+
elsif text[-1] == ?\n
|
|
408
|
+
lspace = ""
|
|
409
|
+
else
|
|
410
|
+
rindex = text.rindex(?\n)
|
|
411
|
+
if rindex
|
|
412
|
+
s = text[rindex+1..-1]
|
|
413
|
+
if s =~ /\A[ \t]*\z/
|
|
414
|
+
lspace = s
|
|
415
|
+
text = text[0..rindex]
|
|
416
|
+
#text[rindex+1..-1] = ''
|
|
417
|
+
end
|
|
418
|
+
else
|
|
419
|
+
if is_bol && text =~ /\A[ \t]*\z/
|
|
420
|
+
lspace = text
|
|
421
|
+
text = nil
|
|
422
|
+
#lspace = text.dup
|
|
423
|
+
#text[0..-1] = ''
|
|
424
|
+
end
|
|
425
|
+
end
|
|
426
|
+
end
|
|
427
|
+
is_bol = rspace ? true : false
|
|
428
|
+
##
|
|
429
|
+
text.insert(0, prev_rspace) if prev_rspace
|
|
430
|
+
parse_exprs(text)
|
|
431
|
+
code.insert(0, mspace) if mspace != ' '
|
|
432
|
+
if lspace
|
|
433
|
+
assert if rspace.nil?
|
|
434
|
+
code.insert(0, lspace)
|
|
435
|
+
code << rspace
|
|
436
|
+
#add_stmt(code)
|
|
437
|
+
prev_rspace = nil
|
|
438
|
+
else
|
|
439
|
+
code << ';' unless code[-1] == ?\n
|
|
440
|
+
#add_stmt(code)
|
|
441
|
+
prev_rspace = rspace
|
|
442
|
+
end
|
|
443
|
+
if code
|
|
444
|
+
code = statement_hook(code)
|
|
445
|
+
add_stmt(code)
|
|
446
|
+
end
|
|
447
|
+
end
|
|
448
|
+
#rest = $' || input
|
|
449
|
+
rest = pos > 0 ? input[pos..-1] : input
|
|
450
|
+
rest.insert(0, prev_rspace) if prev_rspace
|
|
451
|
+
parse_exprs(rest) if rest && !rest.empty?
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
def expr_pattern
|
|
455
|
+
#return /([\#$])\{(.*?)\}/
|
|
456
|
+
return /(\$)\{(.*?)\}/m
|
|
457
|
+
#return /\$\{.*?\}/
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
## ex. get_expr_and_escapeflag('$', 'item[:name]') => 'item[:name]', true
|
|
461
|
+
def get_expr_and_escapeflag(matched)
|
|
462
|
+
return matched[2], matched[1] == '$'
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
## parse expressions ('#{...}' and '${...}')
|
|
466
|
+
def parse_exprs(input)
|
|
467
|
+
return if !input or input.empty?
|
|
468
|
+
pos = 0
|
|
469
|
+
start_text_part()
|
|
470
|
+
input.scan(expr_pattern()) do
|
|
471
|
+
m = Regexp.last_match
|
|
472
|
+
text = input[pos, m.begin(0) - pos]
|
|
473
|
+
pos = m.end(0)
|
|
474
|
+
expr, flag_escape = get_expr_and_escapeflag(m)
|
|
475
|
+
#m = Regexp.last_match
|
|
476
|
+
#start = m.begin(0)
|
|
477
|
+
#stop = m.end(0)
|
|
478
|
+
#text = input[pos, start - pos]
|
|
479
|
+
#expr = input[start+2, stop-start-3]
|
|
480
|
+
#pos = stop
|
|
481
|
+
add_text(text)
|
|
482
|
+
add_expr(expr, flag_escape)
|
|
483
|
+
end
|
|
484
|
+
rest = $' || input
|
|
485
|
+
#if !rest || rest.empty?
|
|
486
|
+
# @script << '`; '
|
|
487
|
+
#elsif rest[-1] == ?\n
|
|
488
|
+
# rest.chomp!
|
|
489
|
+
# @script << escape_str(rest) << @newlinestr << '`' << @newline
|
|
490
|
+
#else
|
|
491
|
+
# @script << escape_str(rest) << '`; '
|
|
492
|
+
#end
|
|
493
|
+
flag_newline = input[-1] == ?\n
|
|
494
|
+
add_text(rest, true)
|
|
495
|
+
stop_text_part()
|
|
496
|
+
@script << (flag_newline ? @newline : '; ')
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
## expand macros and parse '#@ARGS' in a statement.
|
|
500
|
+
def statement_hook(stmt)
|
|
501
|
+
## macro expantion
|
|
502
|
+
#macro_pattern = /\A\s*(\w+)\((.*?)\);?(\s*)\z/
|
|
503
|
+
#if macro_pattern =~ stmt
|
|
504
|
+
# name = $1; arg = $2; rspace = $3
|
|
505
|
+
# handler = get_macro_handler(name)
|
|
506
|
+
# ret = handler ? handler.call(arg) + $3 : stmt
|
|
507
|
+
# return ret
|
|
508
|
+
#end
|
|
509
|
+
## arguments declaration
|
|
510
|
+
if @args.nil?
|
|
511
|
+
args_pattern = /\A *\#@ARGS([ \t]+(.*?))?(\s*)\z/ #
|
|
512
|
+
if args_pattern =~ stmt
|
|
513
|
+
@args = []
|
|
514
|
+
declares = ''
|
|
515
|
+
rspace = $3
|
|
516
|
+
if $2
|
|
517
|
+
for s in $2.split(/,/)
|
|
518
|
+
arg = s.strip()
|
|
519
|
+
next if s.empty?
|
|
520
|
+
arg =~ /\A[a-zA-Z_]\w*\z/ or raise ArgumentError.new("#{arg}: invalid template argument.")
|
|
521
|
+
@args << arg
|
|
522
|
+
declares << " #{arg} = @#{arg};"
|
|
523
|
+
end
|
|
524
|
+
end
|
|
525
|
+
declares << rspace
|
|
526
|
+
return declares
|
|
527
|
+
end
|
|
528
|
+
end
|
|
529
|
+
##
|
|
530
|
+
return stmt
|
|
531
|
+
end
|
|
532
|
+
|
|
533
|
+
#MACRO_HANDLER_TABLE = {
|
|
534
|
+
# "echo" => proc { |arg|
|
|
535
|
+
# " _buf << (#{arg});"
|
|
536
|
+
# },
|
|
537
|
+
# "import" => proc { |arg|
|
|
538
|
+
# " _buf << @_engine.render(#{arg}, self, false);"
|
|
539
|
+
# },
|
|
540
|
+
# "start_capture" => proc { |arg|
|
|
541
|
+
# " _buf_bkup = _buf; _buf = \"\"; _capture_varname = #{arg};"
|
|
542
|
+
# },
|
|
543
|
+
# "stop_capture" => proc { |arg|
|
|
544
|
+
# " self[_capture_varname] = _buf; _buf = _buf_bkup;"
|
|
545
|
+
# },
|
|
546
|
+
# "start_placeholder" => proc { |arg|
|
|
547
|
+
# " if self[#{arg}] then _buf << self[#{arg}] else;"
|
|
548
|
+
# },
|
|
549
|
+
# "stop_placeholder" => proc { |arg|
|
|
550
|
+
# " end;"
|
|
551
|
+
# },
|
|
552
|
+
#}
|
|
553
|
+
#
|
|
554
|
+
#def get_macro_handler(name)
|
|
555
|
+
# return MACRO_HANDLER_TABLE[name]
|
|
556
|
+
#end
|
|
557
|
+
|
|
558
|
+
## start text part
|
|
559
|
+
def start_text_part()
|
|
560
|
+
@script << " _buf << %Q`"
|
|
561
|
+
end
|
|
562
|
+
|
|
563
|
+
## stop text part
|
|
564
|
+
def stop_text_part()
|
|
565
|
+
@script << '`'
|
|
566
|
+
end
|
|
567
|
+
|
|
568
|
+
## add text string
|
|
569
|
+
def add_text(text, encode_newline=false)
|
|
570
|
+
return unless text && !text.empty?
|
|
571
|
+
if encode_newline && text[-1] == ?\n
|
|
572
|
+
text.chomp!
|
|
573
|
+
@script << escape_str(text) << @newlinestr
|
|
574
|
+
else
|
|
575
|
+
@script << escape_str(text)
|
|
576
|
+
end
|
|
577
|
+
end
|
|
578
|
+
|
|
579
|
+
## escape '\\' and '`' into '\\\\' and '\`'
|
|
580
|
+
def escape_str(str)
|
|
581
|
+
str.gsub!(/[`\\]/, '\\\\\&')
|
|
582
|
+
str.gsub!(/\r\n/, "\\r\r\n") if @newline == "\r\n"
|
|
583
|
+
return str
|
|
584
|
+
end
|
|
585
|
+
|
|
586
|
+
## add expression code
|
|
587
|
+
def add_expr(code, flag_escape=nil)
|
|
588
|
+
return if !code || code.empty?
|
|
589
|
+
@script << (flag_escape ? "\#{#{@escapefunc}((#{code}).to_s)}" : "\#{#{code}}")
|
|
590
|
+
end
|
|
591
|
+
|
|
592
|
+
## add statement code
|
|
593
|
+
def add_stmt(code)
|
|
594
|
+
@script << code
|
|
595
|
+
end
|
|
596
|
+
|
|
597
|
+
private
|
|
598
|
+
|
|
599
|
+
## create proc object
|
|
600
|
+
def _render() # :nodoc:
|
|
601
|
+
return eval("proc { |_context| self._buf = _buf = #{init_buf_expr()}; #{@script}; _buf.to_s }".untaint, nil, @filename || '(tenjin)')
|
|
602
|
+
end
|
|
603
|
+
|
|
604
|
+
public
|
|
605
|
+
|
|
606
|
+
def init_buf_expr() # :nodoc:
|
|
607
|
+
return "''"
|
|
608
|
+
end
|
|
609
|
+
|
|
610
|
+
## evaluate converted ruby code and return it.
|
|
611
|
+
## argument '_context' should be a Hash object or Context object.
|
|
612
|
+
def render(context=Context.new)
|
|
613
|
+
context = Context.new(context) if context.is_a?(Hash)
|
|
614
|
+
@proc ||= _render()
|
|
615
|
+
return context.instance_eval(&@proc)
|
|
616
|
+
end
|
|
617
|
+
|
|
618
|
+
end
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
##
|
|
622
|
+
## preprocessor class
|
|
623
|
+
##
|
|
624
|
+
class Preprocessor < Template
|
|
625
|
+
|
|
626
|
+
protected
|
|
627
|
+
|
|
628
|
+
STMT_PATTERN = compile_stmt_pattern('RB')
|
|
629
|
+
|
|
630
|
+
def stmt_pattern
|
|
631
|
+
return STMT_PATTERN
|
|
632
|
+
end
|
|
633
|
+
|
|
634
|
+
def expr_pattern
|
|
635
|
+
return /([\#$])\{\{(.*?)\}\}/m
|
|
636
|
+
end
|
|
637
|
+
|
|
638
|
+
#--
|
|
639
|
+
#def get_expr_and_escapeflag(matched)
|
|
640
|
+
# return matched[2], matched[1] == '$'
|
|
641
|
+
#end
|
|
642
|
+
#++
|
|
643
|
+
|
|
644
|
+
def escape_str(str)
|
|
645
|
+
str.gsub!(/[\\`\#]/, '\\\\\&')
|
|
646
|
+
str.gsub!(/\r\n/, "\\r\r\n") if @newline == "\r\n"
|
|
647
|
+
return str
|
|
648
|
+
end
|
|
649
|
+
|
|
650
|
+
def add_expr(code, flag_escape=nil)
|
|
651
|
+
return if !code || code.empty?
|
|
652
|
+
super("_decode_params((#{code}))", flag_escape)
|
|
653
|
+
end
|
|
654
|
+
|
|
655
|
+
end
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
##
|
|
659
|
+
## (experimental) fast template class which use Array buffer and Array#push()
|
|
660
|
+
##
|
|
661
|
+
## ex. ('foo.rb')
|
|
662
|
+
## require 'tenjin'
|
|
663
|
+
## engine = Tenjin::Engine.new(:templateclass=>Tenjin::ArrayBufferTemplate)
|
|
664
|
+
## template = engine.get_template('foo.rbhtml')
|
|
665
|
+
## puts template.script
|
|
666
|
+
##
|
|
667
|
+
## result:
|
|
668
|
+
## $ cat foo.rbhtml
|
|
669
|
+
## <ul>
|
|
670
|
+
## <?rb for item in items ?>
|
|
671
|
+
## <li>#{item}</li>
|
|
672
|
+
## <?rb end ?>
|
|
673
|
+
## </ul>
|
|
674
|
+
## $ ruby foo.rb
|
|
675
|
+
## _buf.push('<ul>
|
|
676
|
+
## '); for item in items
|
|
677
|
+
## _buf.push(' <li>', (item).to_s, '</li>
|
|
678
|
+
## '); end
|
|
679
|
+
## _buf.push('</ul>
|
|
680
|
+
## ');
|
|
681
|
+
##
|
|
682
|
+
class ArrayBufferTemplate < Template
|
|
683
|
+
|
|
684
|
+
protected
|
|
685
|
+
|
|
686
|
+
def expr_pattern
|
|
687
|
+
return /([\#$])\{(.*?)\}/
|
|
688
|
+
end
|
|
689
|
+
|
|
690
|
+
## parse expressions ('#{...}' and '${...}')
|
|
691
|
+
def parse_exprs(input)
|
|
692
|
+
return if !input or input.empty?
|
|
693
|
+
pos = 0
|
|
694
|
+
items = []
|
|
695
|
+
input.scan(expr_pattern()) do
|
|
696
|
+
prefix, expr = $1, $2
|
|
697
|
+
m = Regexp.last_match
|
|
698
|
+
text = input[pos, m.begin(0) - pos]
|
|
699
|
+
pos = m.end(0)
|
|
700
|
+
items << quote_str(text) if text && !text.empty?
|
|
701
|
+
items << quote_expr(expr, prefix == '$') if expr && !expr.empty?
|
|
702
|
+
end
|
|
703
|
+
rest = $' || input
|
|
704
|
+
items << quote_str(rest) if rest && !rest.empty?
|
|
705
|
+
@script << " _buf.push(" << items.join(", ") << "); " unless items.empty?
|
|
706
|
+
end
|
|
707
|
+
|
|
708
|
+
def quote_str(text)
|
|
709
|
+
text.gsub!(/[\'\\]/, '\\\\\&')
|
|
710
|
+
return "'#{text}'"
|
|
711
|
+
end
|
|
712
|
+
|
|
713
|
+
def quote_expr(expr, flag_escape)
|
|
714
|
+
return flag_escape ? "#{@escapefunc}((#{expr}).to_s)" : "(#{expr}).to_s" # or "(#{expr})"
|
|
715
|
+
end
|
|
716
|
+
|
|
717
|
+
#--
|
|
718
|
+
#def get_macro_handler(name)
|
|
719
|
+
# if name == "start_capture"
|
|
720
|
+
# return proc { |arg|
|
|
721
|
+
# " _buf_bkup = _buf; _buf = []; _capture_varname = #{arg};"
|
|
722
|
+
# }
|
|
723
|
+
# elsif name == "stop_capture"
|
|
724
|
+
# return proc { |arg|
|
|
725
|
+
# " self[_capture_varname] = _buf.join; _buf = _buf_bkup;"
|
|
726
|
+
# }
|
|
727
|
+
# else
|
|
728
|
+
# return super
|
|
729
|
+
# end
|
|
730
|
+
#end
|
|
731
|
+
#++
|
|
732
|
+
|
|
733
|
+
public
|
|
734
|
+
|
|
735
|
+
def init_buf_expr() # :nodoc:
|
|
736
|
+
return "[]"
|
|
737
|
+
end
|
|
738
|
+
|
|
739
|
+
end
|
|
740
|
+
|
|
741
|
+
|
|
742
|
+
##
|
|
743
|
+
## template class to use eRuby template file (*.rhtml) instead of
|
|
744
|
+
## Tenjin template file (*.rbhtml).
|
|
745
|
+
## requires 'erubis' (http://www.kuwata-lab.com/erubis).
|
|
746
|
+
##
|
|
747
|
+
## ex.
|
|
748
|
+
## require 'erubis'
|
|
749
|
+
## require 'tenjin'
|
|
750
|
+
## engine = Tenjin::Engine.new(:templateclass=>Tenjin::ErubisTemplate)
|
|
751
|
+
##
|
|
752
|
+
class ErubisTemplate < Tenjin::Template
|
|
753
|
+
|
|
754
|
+
protected
|
|
755
|
+
|
|
756
|
+
def parse_stmts(input)
|
|
757
|
+
eruby = Erubis::Eruby.new(input, :preamble=>false, :postamble=>false)
|
|
758
|
+
@script << eruby.src
|
|
759
|
+
end
|
|
760
|
+
|
|
761
|
+
end
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
##
|
|
765
|
+
## engine class for templates
|
|
766
|
+
##
|
|
767
|
+
## Engine class supports the followings.
|
|
768
|
+
## * template caching
|
|
769
|
+
## * partial template
|
|
770
|
+
## * layout template
|
|
771
|
+
## * capturing (experimental)
|
|
772
|
+
##
|
|
773
|
+
## ex. file 'ex_list.rbhtml'
|
|
774
|
+
## <ul>
|
|
775
|
+
## <?rb for item in @items ?>
|
|
776
|
+
## <li>#{item}</li>
|
|
777
|
+
## <?rb end ?>
|
|
778
|
+
## </ul>
|
|
779
|
+
##
|
|
780
|
+
## ex. file 'ex_layout.rbhtml'
|
|
781
|
+
## <html>
|
|
782
|
+
## <body>
|
|
783
|
+
## <h1>${@title}</li>
|
|
784
|
+
## #{@_content}
|
|
785
|
+
## <?rb import 'footer.rbhtml' ?>
|
|
786
|
+
## </body>
|
|
787
|
+
## </html>
|
|
788
|
+
##
|
|
789
|
+
## ex. file 'main.rb'
|
|
790
|
+
## require 'tenjin'
|
|
791
|
+
## options = {:prefix=>'ex_', :postfix=>'.rbhtml', :layout=>'ex_layout.rbhtml'}
|
|
792
|
+
## engine = Tenjin::Engine.new(options)
|
|
793
|
+
## context = {:title=>'Tenjin Example', :items=>['foo', 'bar', 'baz']}
|
|
794
|
+
## output = engine.render(:list, context) # or 'ex_list.rbhtml'
|
|
795
|
+
## print output
|
|
796
|
+
##
|
|
797
|
+
class Engine
|
|
798
|
+
|
|
799
|
+
##
|
|
800
|
+
## initializer of Engine class.
|
|
801
|
+
##
|
|
802
|
+
## options:
|
|
803
|
+
## :prefix :: prefix string for template name (ex. 'template/')
|
|
804
|
+
## :postfix :: postfix string for template name (ex. '.rbhtml')
|
|
805
|
+
## :layout :: layout template name (default nil)
|
|
806
|
+
## :path :: array of directory name (default nil)
|
|
807
|
+
## :cache :: save converted ruby code into file or not (default true)
|
|
808
|
+
## :path :: list of directory (default nil)
|
|
809
|
+
## :preprocess :: flag to activate preprocessing (default nil)
|
|
810
|
+
## :templateclass :: template class object (default Tenjin::Template)
|
|
811
|
+
##
|
|
812
|
+
def initialize(options={})
|
|
813
|
+
@prefix = options[:prefix] || ''
|
|
814
|
+
@postfix = options[:postfix] || ''
|
|
815
|
+
@layout = options[:layout]
|
|
816
|
+
@cache = options.fetch(:cache, true)
|
|
817
|
+
@path = options[:path]
|
|
818
|
+
@preprocess = options.fetch(:preprocess, nil)
|
|
819
|
+
@templateclass = options.fetch(:templateclass, Template)
|
|
820
|
+
@init_opts_for_template = options
|
|
821
|
+
@templates = {} # filename->template
|
|
822
|
+
end
|
|
823
|
+
|
|
824
|
+
## convert short name into filename (ex. ':list' => 'template/list.rb.html')
|
|
825
|
+
def to_filename(template_name)
|
|
826
|
+
name = template_name
|
|
827
|
+
return name.is_a?(Symbol) ? "#{@prefix}#{name}#{@postfix}" : name
|
|
828
|
+
end
|
|
829
|
+
|
|
830
|
+
## find template filename
|
|
831
|
+
def find_template_file(template_name)
|
|
832
|
+
filename = to_filename(template_name)
|
|
833
|
+
if @path
|
|
834
|
+
for dir in @path
|
|
835
|
+
filepath = "#{dir}#{File::SEPARATOR}#{filename}"
|
|
836
|
+
return filepath if test(?f, filepath.untaint)
|
|
837
|
+
end
|
|
838
|
+
else
|
|
839
|
+
return filename if test(?f, filename.dup.untaint) # dup is required for frozen string
|
|
840
|
+
end
|
|
841
|
+
raise Errno::ENOENT.new("#{filename} (path=#{@path.inspect})")
|
|
842
|
+
end
|
|
843
|
+
|
|
844
|
+
## read template file and preprocess it
|
|
845
|
+
def read_template_file(filename, _context)
|
|
846
|
+
return File.read(filename) if !@preprocess
|
|
847
|
+
_context ||= {}
|
|
848
|
+
if _context.is_a?(Hash) || _context._engine.nil?
|
|
849
|
+
_context = hook_context(_context)
|
|
850
|
+
end
|
|
851
|
+
preprocessor = Preprocessor.new(filename)
|
|
852
|
+
return preprocessor.render(_context)
|
|
853
|
+
end
|
|
854
|
+
|
|
855
|
+
## register template object
|
|
856
|
+
def register_template(template_name, template)
|
|
857
|
+
#template.timestamp = Time.new unless template.timestamp
|
|
858
|
+
@templates[template_name] = template
|
|
859
|
+
end
|
|
860
|
+
|
|
861
|
+
def cachename(filename)
|
|
862
|
+
return (filename + '.cache').untaint
|
|
863
|
+
end
|
|
864
|
+
|
|
865
|
+
## create template object from file
|
|
866
|
+
def create_template(filename, _context=nil)
|
|
867
|
+
template = @templateclass.new(nil, @init_opts_for_template)
|
|
868
|
+
template.timestamp = Time.now()
|
|
869
|
+
cache_filename = cachename(filename)
|
|
870
|
+
_context = hook_context(Context.new) if _context.nil?
|
|
871
|
+
if !@cache
|
|
872
|
+
input = read_template_file(filename, _context)
|
|
873
|
+
template.convert(input, filename)
|
|
874
|
+
elsif !test(?f, cache_filename) || File.mtime(cache_filename) < File.mtime(filename)
|
|
875
|
+
#$stderr.puts "*** debug: load original"
|
|
876
|
+
input = read_template_file(filename, _context)
|
|
877
|
+
template.convert(input, filename)
|
|
878
|
+
store_cachefile(cache_filename, template)
|
|
879
|
+
else
|
|
880
|
+
#$stderr.puts "*** debug: load cache"
|
|
881
|
+
template.filename = filename
|
|
882
|
+
load_cachefile(cache_filename, template)
|
|
883
|
+
end
|
|
884
|
+
return template
|
|
885
|
+
end
|
|
886
|
+
|
|
887
|
+
## store template into cache file
|
|
888
|
+
def store_cachefile(cache_filename, template)
|
|
889
|
+
s = template.script
|
|
890
|
+
s = "\#@ARGS #{template.args.join(',')}\n#{s}" if template.args
|
|
891
|
+
File.open(cache_filename, 'w') do |f|
|
|
892
|
+
f.flock(File::LOCK_EX)
|
|
893
|
+
f.write(s)
|
|
894
|
+
#f.lock(FIle::LOCK_UN) # File#close() unlocks automatically
|
|
895
|
+
end
|
|
896
|
+
end
|
|
897
|
+
|
|
898
|
+
## load template from cache file
|
|
899
|
+
def load_cachefile(cache_filename, template)
|
|
900
|
+
s = File.read(cache_filename)
|
|
901
|
+
if s.sub!(/\A\#\@ARGS (.*?)\r?\n/, '')
|
|
902
|
+
template.args = $1.split(',')
|
|
903
|
+
end
|
|
904
|
+
template.script = s
|
|
905
|
+
end
|
|
906
|
+
|
|
907
|
+
## get template object
|
|
908
|
+
def get_template(template_name, _context=nil)
|
|
909
|
+
template = @templates[template_name]
|
|
910
|
+
t = template
|
|
911
|
+
unless t && t.timestamp && t.filename && t.timestamp >= File.mtime(t.filename)
|
|
912
|
+
filename = find_template_file(template_name)
|
|
913
|
+
template = create_template(filename, _context) # _context is passed only for preprocessor
|
|
914
|
+
register_template(template_name, template)
|
|
915
|
+
end
|
|
916
|
+
return template
|
|
917
|
+
end
|
|
918
|
+
|
|
919
|
+
## get template object and evaluate it with context object.
|
|
920
|
+
## if argument 'layout' is true then default layout file (specified at
|
|
921
|
+
## initializer) is used as layout template, else if false then no layout
|
|
922
|
+
## template is used.
|
|
923
|
+
## if argument 'layout' is string, it is regarded as layout template name.
|
|
924
|
+
def render(template_name, context=Context.new, layout=true)
|
|
925
|
+
#context = Context.new(context) if context.is_a?(Hash)
|
|
926
|
+
context = hook_context(context)
|
|
927
|
+
while true
|
|
928
|
+
template = get_template(template_name, context) # context is passed only for preprocessor
|
|
929
|
+
_buf = context._buf
|
|
930
|
+
output = template.render(context)
|
|
931
|
+
context._buf = _buf
|
|
932
|
+
unless context['_layout'].nil?
|
|
933
|
+
layout = context['_layout']
|
|
934
|
+
context['_layout'] = nil
|
|
935
|
+
end
|
|
936
|
+
layout = @layout if layout == true or layout.nil?
|
|
937
|
+
break unless layout
|
|
938
|
+
template_name = layout
|
|
939
|
+
layout = false
|
|
940
|
+
context['_content'] = output
|
|
941
|
+
end
|
|
942
|
+
return output
|
|
943
|
+
end
|
|
944
|
+
|
|
945
|
+
def hook_context(context)
|
|
946
|
+
if !context
|
|
947
|
+
context = Context.new
|
|
948
|
+
elsif context.is_a?(Hash)
|
|
949
|
+
context = Context.new(context)
|
|
950
|
+
end
|
|
951
|
+
context._engine = self
|
|
952
|
+
context._layout = nil
|
|
953
|
+
return context
|
|
954
|
+
end
|
|
955
|
+
|
|
956
|
+
end
|
|
957
|
+
|
|
958
|
+
|
|
959
|
+
end
|