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.
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