tenjin 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (170) hide show
  1. data/MIT-LICENSE +20 -0
  2. data/README.txt +54 -0
  3. data/benchmark/bench.rb +502 -0
  4. data/benchmark/bench_context.rb +17 -0
  5. data/benchmark/bench_context.yaml +141 -0
  6. data/benchmark/templates/_footer.html +4 -0
  7. data/benchmark/templates/_header.html +52 -0
  8. data/benchmark/templates/bench_eruby.rhtml +29 -0
  9. data/benchmark/templates/bench_tenjin.rbhtml +29 -0
  10. data/bin/rbtenjin +449 -0
  11. data/doc-api/classes/Tenjin.html +141 -0
  12. data/doc-api/classes/Tenjin/ArrayBufferTemplate.html +270 -0
  13. data/doc-api/classes/Tenjin/BaseContext.html +312 -0
  14. data/doc-api/classes/Tenjin/Context.html +126 -0
  15. data/doc-api/classes/Tenjin/ContextHelper.html +433 -0
  16. data/doc-api/classes/Tenjin/Engine.html +616 -0
  17. data/doc-api/classes/Tenjin/ErubisTemplate.html +166 -0
  18. data/doc-api/classes/Tenjin/HtmlHelper.html +359 -0
  19. data/doc-api/classes/Tenjin/Preprocessor.html +242 -0
  20. data/doc-api/classes/Tenjin/Template.html +916 -0
  21. data/doc-api/created.rid +1 -0
  22. data/doc-api/files/README_txt.html +185 -0
  23. data/doc-api/files/lib/tenjin_rb.html +136 -0
  24. data/doc-api/fr_class_index.html +36 -0
  25. data/doc-api/fr_file_index.html +28 -0
  26. data/doc-api/fr_method_index.html +89 -0
  27. data/doc-api/index.html +24 -0
  28. data/doc-api/rdoc-style.css +208 -0
  29. data/doc/docstyle.css +188 -0
  30. data/doc/examples.html +312 -0
  31. data/doc/faq.html +909 -0
  32. data/doc/users-guide.html +1691 -0
  33. data/lib/tenjin.rb +959 -0
  34. data/setup.rb +1331 -0
  35. data/tenjin.gemspec +58 -0
  36. data/test/assert-text-equal.rb +45 -0
  37. data/test/data/examples/form/create.rbhtml +4 -0
  38. data/test/data/examples/form/form.rbhtml +14 -0
  39. data/test/data/examples/form/layout.rbhtml +8 -0
  40. data/test/data/examples/form/main.rb +9 -0
  41. data/test/data/examples/form/main.result +21 -0
  42. data/test/data/examples/form/update.rbhtml +4 -0
  43. data/test/data/examples/preprocessing/helper.rb +16 -0
  44. data/test/data/examples/preprocessing/main.rb +11 -0
  45. data/test/data/examples/preprocessing/main.result +17 -0
  46. data/test/data/examples/preprocessing/select.rbhtml +15 -0
  47. data/test/data/examples/preprocessing/select_P.result +18 -0
  48. data/test/data/examples/table/table.rb +9 -0
  49. data/test/data/examples/table/table.rbhtml +16 -0
  50. data/test/data/examples/table/table.result +20 -0
  51. data/test/data/examples/table/table_s.result +18 -0
  52. data/test/data/faq/ex1.rbhtml +5 -0
  53. data/test/data/faq/ex10-baselayout.rbhtml +27 -0
  54. data/test/data/faq/ex10-content.rbhtml +12 -0
  55. data/test/data/faq/ex10-customlayout.rbhtml +11 -0
  56. data/test/data/faq/ex10_inherit.result +25 -0
  57. data/test/data/faq/ex11-bench.rb +28 -0
  58. data/test/data/faq/ex11-content.rbhtml +9 -0
  59. data/test/data/faq/ex11-layout1.rbhtml +5 -0
  60. data/test/data/faq/ex11-layout2.rbhtml +6 -0
  61. data/test/data/faq/ex11.rb +5 -0
  62. data/test/data/faq/ex11.rbhtml +8 -0
  63. data/test/data/faq/ex11.source +11 -0
  64. data/test/data/faq/ex11_arraybuffer.result +15 -0
  65. data/test/data/faq/ex1_chksyntax.result +3 -0
  66. data/test/data/faq/ex2-content.rbhtml +3 -0
  67. data/test/data/faq/ex2-layout.rbhtml +11 -0
  68. data/test/data/faq/ex2_removenl.result +19 -0
  69. data/test/data/faq/ex3.rb +4 -0
  70. data/test/data/faq/ex3.rbhtml +1 -0
  71. data/test/data/faq/ex3_escapefunc1.result +2 -0
  72. data/test/data/faq/ex3_escapefunc2.result +2 -0
  73. data/test/data/faq/ex5.rbhtml +7 -0
  74. data/test/data/faq/ex5_template_args.source +9 -0
  75. data/test/data/faq/ex6-content.rhtml +6 -0
  76. data/test/data/faq/ex6-layout.rhtml +6 -0
  77. data/test/data/faq/ex6.rb +10 -0
  78. data/test/data/faq/ex6_eruby.result +12 -0
  79. data/test/data/faq/ex7-expr-pattern.rb +34 -0
  80. data/test/data/faq/ex7-expr-pattern.rbhtml +3 -0
  81. data/test/data/faq/ex7_expr_pattern.result +4 -0
  82. data/test/data/faq/ex8-m18n.rb +77 -0
  83. data/test/data/faq/ex8-m18n.rbhtml +4 -0
  84. data/test/data/faq/ex8_m18n.result +10 -0
  85. data/test/data/faq/ex9-baselayout.rbhtml +8 -0
  86. data/test/data/faq/ex9-content.rbhtml +6 -0
  87. data/test/data/faq/ex9-mylayout.rbhtml +5 -0
  88. data/test/data/faq/ex9_changelayout.result +11 -0
  89. data/test/data/users_guide/content6.rbhtml +3 -0
  90. data/test/data/users_guide/content7.rbhtml +5 -0
  91. data/test/data/users_guide/content8.rbhtml +2 -0
  92. data/test/data/users_guide/contextdata.rb +7 -0
  93. data/test/data/users_guide/datafile.rb +5 -0
  94. data/test/data/users_guide/datafile.yaml +10 -0
  95. data/test/data/users_guide/ex.rbhtml +6 -0
  96. data/test/data/users_guide/ex.result +7 -0
  97. data/test/data/users_guide/ex.script +5 -0
  98. data/test/data/users_guide/ex_script.result +7 -0
  99. data/test/data/users_guide/ex_source.result +8 -0
  100. data/test/data/users_guide/example1.rbhtml +12 -0
  101. data/test/data/users_guide/example1.result +17 -0
  102. data/test/data/users_guide/example10.rbhtml +4 -0
  103. data/test/data/users_guide/example10_template_args.result +6 -0
  104. data/test/data/users_guide/example11.rbhtml +5 -0
  105. data/test/data/users_guide/example11_template_args_result +2 -0
  106. data/test/data/users_guide/example12.rbhtml +12 -0
  107. data/test/data/users_guide/example12_preprocessed.result +10 -0
  108. data/test/data/users_guide/example12_preprocessed_source.result +10 -0
  109. data/test/data/users_guide/example13.rbhtml +6 -0
  110. data/test/data/users_guide/example13_preprocessed.result +2 -0
  111. data/test/data/users_guide/example13_preprocessed_source.result +2 -0
  112. data/test/data/users_guide/example14.rb +32 -0
  113. data/test/data/users_guide/example14.rbhtml +6 -0
  114. data/test/data/users_guide/example14_tmplclass.result +15 -0
  115. data/test/data/users_guide/example15.rb +10 -0
  116. data/test/data/users_guide/example15_escapefunc.result +14 -0
  117. data/test/data/users_guide/example16.rbhtml +4 -0
  118. data/test/data/users_guide/example16a.rb +10 -0
  119. data/test/data/users_guide/example16a.result +4 -0
  120. data/test/data/users_guide/example16b.rb +13 -0
  121. data/test/data/users_guide/example16b.result +4 -0
  122. data/test/data/users_guide/example16c.rb +12 -0
  123. data/test/data/users_guide/example16c.result +4 -0
  124. data/test/data/users_guide/example1_S.result +14 -0
  125. data/test/data/users_guide/example1_SXNC.result +10 -0
  126. data/test/data/users_guide/example1_source.result +14 -0
  127. data/test/data/users_guide/example2.rbhtml +3 -0
  128. data/test/data/users_guide/example2_sb.result2 +9 -0
  129. data/test/data/users_guide/example3.rbhtml +5 -0
  130. data/test/data/users_guide/example3_syntaxcheck.result +2 -0
  131. data/test/data/users_guide/example4.rbhtml +13 -0
  132. data/test/data/users_guide/example4_datafile_rb.result +13 -0
  133. data/test/data/users_guide/example4_yaml.result +13 -0
  134. data/test/data/users_guide/example5.rbhtml +9 -0
  135. data/test/data/users_guide/example5_datastr_rb.result +9 -0
  136. data/test/data/users_guide/example5_datastr_yaml.result +9 -0
  137. data/test/data/users_guide/example6.rbhtml +19 -0
  138. data/test/data/users_guide/example6_layout.result +29 -0
  139. data/test/data/users_guide/example6_nested.result +28 -0
  140. data/test/data/users_guide/example7_layout2.result +13 -0
  141. data/test/data/users_guide/example8_layout3.result +8 -0
  142. data/test/data/users_guide/example9.rbhtml +18 -0
  143. data/test/data/users_guide/example9_capture.result +26 -0
  144. data/test/data/users_guide/footer.html +5 -0
  145. data/test/data/users_guide/footer.rbhtml +4 -0
  146. data/test/data/users_guide/layout6.rbhtml +17 -0
  147. data/test/data/users_guide/layout7.rbhtml +9 -0
  148. data/test/data/users_guide/layout8_html.rbhtml +5 -0
  149. data/test/data/users_guide/layout8_xhtml.rbhtml +6 -0
  150. data/test/data/users_guide/layout9.rbhtml +25 -0
  151. data/test/data/users_guide/sidemenu.rbhtml +5 -0
  152. data/test/data/users_guide/user_app.cgi +39 -0
  153. data/test/data/users_guide/user_app.result +30 -0
  154. data/test/data/users_guide/user_create.rbhtml +6 -0
  155. data/test/data/users_guide/user_edit.rbhtml +7 -0
  156. data/test/data/users_guide/user_form.rbhtml +10 -0
  157. data/test/data/users_guide/user_layout.rbhtml +16 -0
  158. data/test/test_all.rb +23 -0
  159. data/test/test_engine.rb +526 -0
  160. data/test/test_engine.yaml +2039 -0
  161. data/test/test_examples.rb +81 -0
  162. data/test/test_faq.rb +60 -0
  163. data/test/test_htmlhelper.rb +78 -0
  164. data/test/test_main.rb +564 -0
  165. data/test/test_main.yaml +174 -0
  166. data/test/test_template.rb +113 -0
  167. data/test/test_template.yaml +1244 -0
  168. data/test/test_users_guide.rb +75 -0
  169. data/test/testcase-helper.rb +166 -0
  170. 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 = { '&'=>'&amp;', '<'=>'&lt;', '>'=>'&gt;', '"'=>'&quot;', "'"=>'&#039;' }
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(/&/, '&amp;')
50
+ #s.gsub!(/</, '&lt;')
51
+ #s.gsub!(/>/, '&gt;')
52
+ #s.gsub!(/"/, '&quot;')
53
+ #return s
54
+ ##
55
+ #return s.gsub(/&/, '&amp;').gsub(/</, '&lt;').gsub(/>/, '&gt;').gsub(/"/, '&quot;')
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 " &nbsp;"
93
+ def text2html(text)
94
+ return nl2br(escape_xml(text.to_s).gsub(/ /, ' &nbsp;'))
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!(/&lt;`\#(.*?)\#`&gt;/m) { "\#\{#{CGI::unescapeHTML($1)}\}" }
215
+ s.gsub!(/&lt;`\$(.*?)\$`&gt;/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