tenjin 0.6.2 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (282) hide show
  1. data/CHANGES.txt +163 -0
  2. data/MIT-LICENSE +16 -17
  3. data/README.txt +5 -5
  4. data/benchmark/Makefile +9 -0
  5. data/benchmark/bench.rb +3 -2
  6. data/bin/rbtenjin +6 -3
  7. data/doc/docstyle.css +25 -4
  8. data/doc/users-guide.html +2088 -1563
  9. data/lib/tenjin.rb +784 -158
  10. data/public_html/_layout.rbhtml +33 -0
  11. data/public_html/css/style.css +77 -0
  12. data/public_html/env.rbhtml +15 -0
  13. data/public_html/favicon.ico +0 -0
  14. data/public_html/hello.rbhtml +14 -0
  15. data/public_html/index.rbhtml +39 -0
  16. data/public_html/rbtenjin.cgi +188 -0
  17. data/tenjin.gemspec +9 -6
  18. data/test/data/examples/preprocessing/main.rb +1 -1
  19. data/test/data/examples/preprocessing/select.rbhtml +1 -1
  20. data/test/data/faq/{ex11-bench.rb → ex10-bench.rb} +3 -3
  21. data/test/data/faq/ex10-content.rbhtml +8 -11
  22. data/test/data/faq/{ex11-layout1.rbhtml → ex10-layout1.rbhtml} +0 -0
  23. data/test/data/faq/{ex11-layout2.rbhtml → ex10-layout2.rbhtml} +0 -0
  24. data/test/data/faq/{ex11.rb → ex10.rb} +1 -1
  25. data/test/data/faq/{ex11.rbhtml → ex10.rbhtml} +0 -0
  26. data/test/data/faq/{ex11.source → ex10.source} +2 -2
  27. data/test/data/faq/{ex11_arraybuffer.result → ex10_arraybuffer.result} +1 -1
  28. data/test/data/faq/{ex5.rbhtml → ex2.rbhtml} +0 -0
  29. data/test/data/faq/{ex5_template_args.source → ex2_template_args.source} +1 -1
  30. data/test/data/faq/{ex7-expr-pattern.rb → ex4-expr-pattern.rb} +1 -1
  31. data/test/data/faq/{ex7-expr-pattern.rbhtml → ex4-expr-pattern.rbhtml} +0 -0
  32. data/test/data/faq/{ex7_expr_pattern.result → ex4_expr_pattern.result} +1 -1
  33. data/test/data/faq/{ex6-content.rhtml → ex5-content.rhtml} +0 -0
  34. data/test/data/faq/{ex6-layout.rhtml → ex5-layout.rhtml} +0 -0
  35. data/test/data/faq/{ex6.rb → ex5.rb} +2 -2
  36. data/test/data/faq/{ex6_eruby.result → ex5_eruby.result} +1 -1
  37. data/test/data/faq/{ex2-content.rbhtml → ex6-content.rbhtml} +0 -0
  38. data/test/data/faq/{ex2-layout.rbhtml → ex6-layout.rbhtml} +0 -0
  39. data/test/data/faq/{ex2_removenl.result → ex6_removenl.result} +1 -1
  40. data/test/data/faq/ex7-m18n.rb +48 -0
  41. data/test/data/faq/{ex8-m18n.rbhtml → ex7-m18n.rbhtml} +0 -0
  42. data/test/data/faq/{ex8_m18n.result → ex7_m18n.result} +1 -1
  43. data/test/data/faq/ex8-baselayout.rbhtml +8 -0
  44. data/test/data/faq/ex8-content.rbhtml +6 -0
  45. data/test/data/faq/{ex9-mylayout.rbhtml → ex8-mylayout.rbhtml} +0 -0
  46. data/test/data/faq/{ex9_changelayout.result → ex8_changelayout.result} +1 -1
  47. data/test/data/faq/ex9-baselayout.rbhtml +24 -5
  48. data/test/data/faq/ex9-content.rbhtml +12 -6
  49. data/test/data/faq/{ex10-customlayout.rbhtml → ex9-customlayout.rbhtml} +1 -1
  50. data/test/data/faq/{ex10_inherit.result → ex9_inherit.result} +1 -1
  51. data/test/data/faq/helpers1.rb +17 -0
  52. data/test/data/faq/helpers1.rbhtml +4 -0
  53. data/test/data/faq/helpers1.result +11 -0
  54. data/test/data/faq/helpers2.rb +21 -0
  55. data/test/data/faq/helpers2.rbhtml +4 -0
  56. data/test/data/faq/helpers2.result +11 -0
  57. data/test/data/faq/weekday1.rb +6 -0
  58. data/test/data/faq/weekday1.rbhtml +9 -0
  59. data/test/data/faq/weekday1.result +23 -0
  60. data/test/data/faq/weekday2.rb +8 -0
  61. data/test/data/faq/weekday2.rbhtml +10 -0
  62. data/test/data/faq/weekday2.result +24 -0
  63. data/test/data/faq/weekday3.rb +22 -0
  64. data/test/data/faq/weekday3.rbhtml +3 -0
  65. data/test/data/faq/weekday3.result +28 -0
  66. data/test/data/users_guide/test_010/main.rb +14 -0
  67. data/test/data/users_guide/test_010/result.output +13 -0
  68. data/test/data/users_guide/test_010/views/page.rbhtml +11 -0
  69. data/test/data/users_guide/test_011/main.rb +14 -0
  70. data/test/data/users_guide/test_011/result.output +2 -0
  71. data/test/data/users_guide/test_011/views/page.rbhtml +11 -0
  72. data/test/data/users_guide/test_020/main.rb +14 -0
  73. data/test/data/users_guide/test_020/result.output +13 -0
  74. data/test/data/users_guide/test_020/views/page.rbhtml +11 -0
  75. data/test/data/users_guide/test_021/main.rb +12 -0
  76. data/test/data/users_guide/test_021/result.output +12 -0
  77. data/test/data/users_guide/test_021/views/page.rbhtml +11 -0
  78. data/test/data/users_guide/test_030/main.rb +14 -0
  79. data/test/data/users_guide/test_030/result.output +23 -0
  80. data/test/data/users_guide/test_030/views/_layout.rbhtml +10 -0
  81. data/test/data/users_guide/test_030/views/page.rbhtml +11 -0
  82. data/test/data/users_guide/test_040/main.rb +14 -0
  83. data/test/data/users_guide/test_040/result.output +23 -0
  84. data/test/data/users_guide/test_040/views/_layout.rbhtml +10 -0
  85. data/test/data/users_guide/test_040/views/page.rbhtml +12 -0
  86. data/test/data/users_guide/test_050/main.rb +14 -0
  87. data/test/data/users_guide/test_050/result.output +14 -0
  88. data/test/data/users_guide/test_050/views/_layout.rbhtml +10 -0
  89. data/test/data/users_guide/test_050/views/page.rbhtml +13 -0
  90. data/test/data/users_guide/test_051/main.rb +14 -0
  91. data/test/data/users_guide/test_051/result.output +12 -0
  92. data/test/data/users_guide/test_051/views/_layout.rbhtml +11 -0
  93. data/test/data/users_guide/test_051/views/page.rbhtml +13 -0
  94. data/test/data/users_guide/test_060/main.rb +14 -0
  95. data/test/data/users_guide/test_060/result.output +29 -0
  96. data/test/data/users_guide/test_060/views/_footer.rbhtml +3 -0
  97. data/test/data/users_guide/test_060/views/_header.rbhtml +3 -0
  98. data/test/data/users_guide/test_060/views/_layout.rbhtml +13 -0
  99. data/test/data/users_guide/test_060/views/page.rbhtml +13 -0
  100. data/test/data/users_guide/test_070/main.rb +14 -0
  101. data/test/data/users_guide/test_070/result.output +29 -0
  102. data/test/data/users_guide/test_070/views/_footer.rbhtml +3 -0
  103. data/test/data/users_guide/test_070/views/_header.rbhtml +3 -0
  104. data/test/data/users_guide/test_070/views/_layout.rbhtml +13 -0
  105. data/test/data/users_guide/test_070/views/page.rbhtml +13 -0
  106. data/test/data/users_guide/test_capturing/main.rb +24 -0
  107. data/test/data/users_guide/test_capturing/result.output +28 -0
  108. data/test/data/users_guide/test_capturing/views/_layout.rbhtml +21 -0
  109. data/test/data/users_guide/test_capturing/views/blog-post.rbhtml +13 -0
  110. data/test/data/users_guide/test_context/context.rb +5 -0
  111. data/test/data/users_guide/test_context/context.yaml +4 -0
  112. data/test/data/users_guide/test_context/example.rbhtml +5 -0
  113. data/test/data/users_guide/test_context/result1.output +6 -0
  114. data/test/data/users_guide/test_context/result2.output +6 -0
  115. data/test/data/users_guide/test_context/result3.output +6 -0
  116. data/test/data/users_guide/test_context/result4.output +6 -0
  117. data/test/data/users_guide/test_convert/example.rbhtml +5 -0
  118. data/test/data/users_guide/test_convert/result1.output +7 -0
  119. data/test/data/users_guide/test_convert/result2.output +6 -0
  120. data/test/data/users_guide/test_escape/main.rb +4 -0
  121. data/test/data/users_guide/test_escape/result.output +5 -0
  122. data/test/data/users_guide/test_escape/views/page.rbhtml +4 -0
  123. data/test/data/users_guide/test_execute/example.rbhtml +6 -0
  124. data/test/data/users_guide/test_execute/result.output +6 -0
  125. data/test/data/users_guide/test_fragmentcache/cache.d/items/1 +5 -0
  126. data/test/data/users_guide/test_fragmentcache/main.rb +21 -0
  127. data/test/data/users_guide/test_fragmentcache/result.output +8 -0
  128. data/test/data/users_guide/test_fragmentcache/result2.output +6 -0
  129. data/test/data/users_guide/test_fragmentcache/views/items.rbhtml +10 -0
  130. data/test/data/users_guide/test_logging/ex-logger.rb +11 -0
  131. data/test/data/users_guide/test_logging/example.rbhtml +0 -0
  132. data/test/data/users_guide/test_logging/result1.output +3 -0
  133. data/test/data/users_guide/test_logging/result2.output +2 -0
  134. data/test/data/users_guide/test_m17n/m17n.rb +44 -0
  135. data/test/data/users_guide/test_m17n/m17n.rbhtml +5 -0
  136. data/test/data/users_guide/test_m17n/result.output +9 -0
  137. data/test/data/users_guide/test_m17n/result_en.output +4 -0
  138. data/test/data/users_guide/test_m17n/result_fr.output +4 -0
  139. data/test/data/users_guide/test_nested/main.rb +8 -0
  140. data/test/data/users_guide/test_nested/result.output +15 -0
  141. data/test/data/users_guide/test_nested/views/_blog_layout.rbhtml +5 -0
  142. data/test/data/users_guide/{layout8_html.rbhtml → test_nested/views/_site_layout.rbhtml} +0 -0
  143. data/test/data/users_guide/test_nested/views/blog_post.rbhtml +4 -0
  144. data/test/data/users_guide/test_preprocessing/pp-example1.rb +14 -0
  145. data/test/data/users_guide/test_preprocessing/result1a.output +11 -0
  146. data/test/data/users_guide/test_preprocessing/result1b.output +5 -0
  147. data/test/data/users_guide/test_preprocessing/result1c.output +6 -0
  148. data/test/data/users_guide/test_preprocessing/result2a.output +10 -0
  149. data/test/data/users_guide/test_preprocessing/result2b.output +10 -0
  150. data/test/data/users_guide/test_preprocessing/result3a.output +2 -0
  151. data/test/data/users_guide/test_preprocessing/result3b.output +2 -0
  152. data/test/data/users_guide/test_preprocessing/views/pp-example1.rbhtml +4 -0
  153. data/test/data/users_guide/{example12.rbhtml → test_preprocessing/views/pp-example2.rbhtml} +4 -4
  154. data/test/data/users_guide/test_preprocessing/views/pp-example3.rbhtml +7 -0
  155. data/test/data/users_guide/test_retrieve/example.rbhtml +10 -0
  156. data/test/data/users_guide/test_retrieve/result1.output +11 -0
  157. data/test/data/users_guide/test_retrieve/result2.output +11 -0
  158. data/test/data/users_guide/test_retrieve/result3.output +11 -0
  159. data/test/data/users_guide/test_retrieve/result4.output +8 -0
  160. data/test/data/users_guide/test_retrieve/result5.output +5 -0
  161. data/test/data/users_guide/test_safe/result.output +6 -0
  162. data/test/data/users_guide/test_safe/safe-test.rb +21 -0
  163. data/test/data/users_guide/test_safehelper/main.rb +16 -0
  164. data/test/data/users_guide/test_safehelper/result.output +8 -0
  165. data/test/data/users_guide/{example3.rbhtml → test_syntax_check/example.rbhtml} +0 -0
  166. data/test/data/users_guide/test_syntax_check/result.output +2 -0
  167. data/test/data/users_guide/test_trace/layout.rbhtml +7 -0
  168. data/test/data/users_guide/test_trace/main.rbhtml +5 -0
  169. data/test/data/users_guide/test_trace/result.output +16 -0
  170. data/test/data/users_guide/test_trace/trace-example.rb +4 -0
  171. data/test/oktest.rb +755 -0
  172. data/test/test_all.rb +24 -14
  173. data/test/test_engine.rb +628 -63
  174. data/test/test_engine.yaml +40 -3
  175. data/test/test_examples.rb +14 -12
  176. data/test/test_faq.rb +17 -12
  177. data/test/test_htmlhelper.rb +104 -33
  178. data/test/test_main.rb +32 -21
  179. data/test/test_main.yaml +2 -2
  180. data/test/test_safe.rb +206 -0
  181. data/test/test_store.rb +220 -0
  182. data/test/test_tcache.rb +94 -0
  183. data/test/test_template.rb +65 -23
  184. data/test/test_template.yaml +7 -7
  185. data/test/test_users_guide.rb +75 -29
  186. data/test/testcase-helper.rb +20 -18
  187. data/test/testunit-assertions.rb +71 -0
  188. metadata +185 -159
  189. data/doc-api/classes/Tenjin.html +0 -141
  190. data/doc-api/classes/Tenjin/ArrayBufferTemplate.html +0 -270
  191. data/doc-api/classes/Tenjin/BaseContext.html +0 -329
  192. data/doc-api/classes/Tenjin/Context.html +0 -126
  193. data/doc-api/classes/Tenjin/ContextHelper.html +0 -461
  194. data/doc-api/classes/Tenjin/Engine.html +0 -616
  195. data/doc-api/classes/Tenjin/ErubisTemplate.html +0 -166
  196. data/doc-api/classes/Tenjin/HtmlHelper.html +0 -359
  197. data/doc-api/classes/Tenjin/Preprocessor.html +0 -242
  198. data/doc-api/classes/Tenjin/Template.html +0 -916
  199. data/doc-api/created.rid +0 -1
  200. data/doc-api/files/README_txt.html +0 -188
  201. data/doc-api/files/lib/tenjin_rb.html +0 -136
  202. data/doc-api/fr_class_index.html +0 -36
  203. data/doc-api/fr_file_index.html +0 -28
  204. data/doc-api/fr_method_index.html +0 -91
  205. data/doc-api/index.html +0 -24
  206. data/doc-api/rdoc-style.css +0 -208
  207. data/doc/examples.html +0 -312
  208. data/doc/faq.html +0 -909
  209. data/examples/preprocessing/select.rbhtml.cache +0 -17
  210. data/test/Rookbook.yaml +0 -14
  211. data/test/assert-text-equal.rb +0 -45
  212. data/test/data/faq/ex10-baselayout.rbhtml +0 -27
  213. data/test/data/faq/ex11-content.rbhtml +0 -9
  214. data/test/data/faq/ex8-m18n.rb +0 -77
  215. data/test/data/users_guide/content6.rbhtml +0 -3
  216. data/test/data/users_guide/content7.rbhtml +0 -5
  217. data/test/data/users_guide/content8.rbhtml +0 -2
  218. data/test/data/users_guide/contextdata.rb +0 -7
  219. data/test/data/users_guide/datafile.rb +0 -5
  220. data/test/data/users_guide/datafile.yaml +0 -10
  221. data/test/data/users_guide/ex.rbhtml +0 -6
  222. data/test/data/users_guide/ex.result +0 -7
  223. data/test/data/users_guide/ex.script +0 -5
  224. data/test/data/users_guide/ex_script.result +0 -7
  225. data/test/data/users_guide/ex_source.result +0 -8
  226. data/test/data/users_guide/example1.rbhtml +0 -12
  227. data/test/data/users_guide/example1.result +0 -17
  228. data/test/data/users_guide/example10.rbhtml +0 -4
  229. data/test/data/users_guide/example10_template_args.result +0 -6
  230. data/test/data/users_guide/example11.rbhtml +0 -5
  231. data/test/data/users_guide/example11_template_args_result +0 -2
  232. data/test/data/users_guide/example12_preprocessed.result +0 -10
  233. data/test/data/users_guide/example12_preprocessed_source.result +0 -10
  234. data/test/data/users_guide/example13.rbhtml +0 -6
  235. data/test/data/users_guide/example13_preprocessed.result +0 -2
  236. data/test/data/users_guide/example13_preprocessed_source.result +0 -2
  237. data/test/data/users_guide/example14.rb +0 -32
  238. data/test/data/users_guide/example14.rbhtml +0 -6
  239. data/test/data/users_guide/example14_tmplclass.result +0 -15
  240. data/test/data/users_guide/example15.rb +0 -10
  241. data/test/data/users_guide/example15_escapefunc.result +0 -14
  242. data/test/data/users_guide/example16.rbhtml +0 -4
  243. data/test/data/users_guide/example16a.rb +0 -10
  244. data/test/data/users_guide/example16a.result +0 -4
  245. data/test/data/users_guide/example16b.rb +0 -13
  246. data/test/data/users_guide/example16b.result +0 -4
  247. data/test/data/users_guide/example16c.rb +0 -12
  248. data/test/data/users_guide/example16c.result +0 -4
  249. data/test/data/users_guide/example16d.rb +0 -27
  250. data/test/data/users_guide/example16d.result +0 -4
  251. data/test/data/users_guide/example1_S.result +0 -14
  252. data/test/data/users_guide/example1_SXNC.result +0 -10
  253. data/test/data/users_guide/example1_source.result +0 -14
  254. data/test/data/users_guide/example2.rbhtml +0 -3
  255. data/test/data/users_guide/example2_sb.result2 +0 -9
  256. data/test/data/users_guide/example3_syntaxcheck.result +0 -2
  257. data/test/data/users_guide/example4.rbhtml +0 -13
  258. data/test/data/users_guide/example4_datafile_rb.result +0 -13
  259. data/test/data/users_guide/example4_yaml.result +0 -13
  260. data/test/data/users_guide/example5.rbhtml +0 -9
  261. data/test/data/users_guide/example5_datastr_rb.result +0 -9
  262. data/test/data/users_guide/example5_datastr_yaml.result +0 -9
  263. data/test/data/users_guide/example6.rbhtml +0 -19
  264. data/test/data/users_guide/example6_layout.result +0 -29
  265. data/test/data/users_guide/example6_nested.result +0 -28
  266. data/test/data/users_guide/example7_layout2.result +0 -13
  267. data/test/data/users_guide/example8_layout3.result +0 -8
  268. data/test/data/users_guide/example9.rbhtml +0 -18
  269. data/test/data/users_guide/example9_capture.result +0 -26
  270. data/test/data/users_guide/footer.html +0 -5
  271. data/test/data/users_guide/footer.rbhtml +0 -4
  272. data/test/data/users_guide/layout6.rbhtml +0 -17
  273. data/test/data/users_guide/layout7.rbhtml +0 -9
  274. data/test/data/users_guide/layout8_xhtml.rbhtml +0 -6
  275. data/test/data/users_guide/layout9.rbhtml +0 -25
  276. data/test/data/users_guide/sidemenu.rbhtml +0 -5
  277. data/test/data/users_guide/user_app.cgi +0 -39
  278. data/test/data/users_guide/user_app.result +0 -30
  279. data/test/data/users_guide/user_create.rbhtml +0 -6
  280. data/test/data/users_guide/user_edit.rbhtml +0 -7
  281. data/test/data/users_guide/user_form.rbhtml +0 -10
  282. data/test/data/users_guide/user_layout.rbhtml +0 -16
@@ -1,5 +1,5 @@
1
1
  ##
2
- ## copyright(c) 2007-2008 kuwata-lab.com all rights reserved.
2
+ ## copyright(c) 2007-2011 kuwata-lab.com all rights reserved
3
3
  ##
4
4
  ## Permission is hereby granted, free of charge, to any person obtaining
5
5
  ## a copy of this software and associated documentation files (the
@@ -22,19 +22,35 @@
22
22
  ##
23
23
 
24
24
  ##
25
- ## Tenjin module
25
+ ## Tenjin -- a very fast and full-featured template engine
26
26
  ##
27
- ## $Rev: 65 $
28
- ## $Release: 0.6.2 $
27
+ ## $Release: 0.7.0 $
28
+ ## copyright(c) 2007-2011 kuwata-lab.com all rights reserved
29
+ ## $License: MIT License $
29
30
  ##
30
31
 
31
32
  module Tenjin
32
33
 
33
- RELEASE = ('$Release: 0.6.2 $' =~ /[\d.]+/) && $&
34
+ RELEASE = ('$Release: 0.7.0 $' =~ /[\d.]+/) && $&
34
35
 
35
36
 
36
37
  ##
37
- ## helper module for Context class
38
+ ## logger
39
+ ##
40
+ @logger = nil
41
+
42
+ def self.logger
43
+ return @logger
44
+ end
45
+
46
+ def self.logger=(logger)
47
+ @logger = logger
48
+ end
49
+
50
+
51
+ ##
52
+ ## helper module for Context class.
53
+ ## depends on SafeHelper.
38
54
  ##
39
55
  module HtmlHelper
40
56
 
@@ -42,20 +58,40 @@ module Tenjin
42
58
 
43
59
  XML_ESCAPE_TABLE = { '&'=>'&amp;', '<'=>'&lt;', '>'=>'&gt;', '"'=>'&quot;', "'"=>'&#039;' }
44
60
 
61
+ ## escapes '&', '<', '>', and '"'
45
62
  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;')
63
+ #s.gsub(/[&<>"]/) { XML_ESCAPE_TABLE[$&] }
64
+ s.gsub(/[&<>"]/) {|s| XML_ESCAPE_TABLE[s] }
56
65
  end
57
66
 
58
- alias escape escape_xml
67
+ ## escapes '&', '<', '>', '"', and '\''
68
+ def escape_html(s)
69
+ #s.gsub(/[&<>"']/) { XML_ESCAPE_TABLE[$&] }
70
+ s.gsub(/[&<>"']/) {|s| XML_ESCAPE_TABLE[s] }
71
+ end
72
+
73
+ if RUBY_VERSION >= "1.9"
74
+ def escape_xml(s)
75
+ s.gsub(/[&<>"]/, XML_ESCAPE_TABLE)
76
+ end
77
+ def escape_html(s)
78
+ s.gsub(/[&<>"']/, XML_ESCAPE_TABLE)
79
+ end
80
+ end
81
+
82
+ alias escape escape_html
83
+ alias h escape_html
84
+
85
+ end
86
+
87
+
88
+ ##
89
+ ## helper module for html tags.
90
+ ## depends on HtmlHelper and SafeHelper.
91
+ ##
92
+ module HtmlTagHelper
93
+
94
+ module_function
59
95
 
60
96
  ## (experimental) return ' name="value"' if expr is not false nor nil.
61
97
  ## if value is nil or false then expr is used as value.
@@ -63,35 +99,123 @@ module Tenjin
63
99
  if !expr
64
100
  return ''
65
101
  elsif escape
66
- return " #{name}=\"#{escape_xml((value || expr).to_s)}\""
102
+ return safe_str(" #{name}=\"#{safe_escape((value || expr).to_s)}\"")
67
103
  else
68
- return " #{name}=\"#{value || expr}\""
104
+ return safe_str(" #{name}=\"#{value || expr}\"")
69
105
  end
70
106
  end
71
107
 
72
108
  ## return ' checked="checked"' if expr is not false or nil
73
109
  def checked(expr)
74
- return expr ? ' checked="checked"' : ''
110
+ return expr ? safe_str(' checked="checked"') : ''
75
111
  end
76
112
 
77
113
  ## return ' selected="selected"' if expr is not false or nil
78
114
  def selected(expr)
79
- return expr ? ' selected="selected"' : ''
115
+ return expr ? safe_str(' selected="selected"') : ''
80
116
  end
81
117
 
82
118
  ## return ' disabled="disabled"' if expr is not false or nil
83
119
  def disabled(expr)
84
- return expr ? ' disabled="disabled"' : ''
120
+ return expr ? safe_str(' disabled="disabled"') : ''
85
121
  end
86
122
 
123
+ ## (experimental) return ' name="name" value="value"' string.
124
+ ## if separator specified then id attribute is also added.
125
+ def nv(name, value, separator=nil)
126
+ nattr = safe_escape(name.to_s)
127
+ vattr = safe_escape(value.to_s)
128
+ s = separator \
129
+ ? " name=\"#{nattr}\" value=\"#{vattr}\" id=\"#{nattr}#{separator}#{vattr}\"" \
130
+ : " name=\"#{nattr}\" value=\"#{vattr}\""
131
+ return safe_str(s)
132
+ end
133
+
134
+ ## return '<a href="" onclick="js_code;return">label</a>'.
135
+ def js_link(label, js_code, tags=nil)
136
+ return safe_str(%Q`<a href="javascript:undefined" onclick="#{safe_escape(js_code.to_s)};return false"#{_hash2attr(tags)}>#{safe_escape(label.to_s)}</a>`)
137
+ end
138
+
139
+ def _hash2attr(hash) # :nodoc:
140
+ attr = ""
141
+ return attr unless hash
142
+ hash.each_pair do |k, v|
143
+ attr << " #{safe_escape(k.to_s)}=\"#{safe_escape(v.to_s)}\""
144
+ end
145
+ return attr
146
+ end
147
+ private :_hash2attr
148
+
87
149
  ## convert "\n" into "<br />\n"
88
150
  def nl2br(text)
89
- return text.to_s.gsub(/\n/, "<br />\n")
151
+ return safe_str(text.to_s.gsub(/\n/, "<br />\n"))
90
152
  end
91
153
 
92
154
  ## convert "\n" and " " into "<br />\n" and " &nbsp;"
93
155
  def text2html(text)
94
- return nl2br(escape_xml(text.to_s).gsub(/ /, ' &nbsp;'))
156
+ return nl2br(safe_escape(text.to_s).gsub(/ /, ' &nbsp;'))
157
+ end
158
+
159
+ ## cycle values everytime when #to_s() is called
160
+ ## ex:
161
+ ## cycle = Cycle.new('odd', 'even')
162
+ ## "#{cycle}" #=> 'odd'
163
+ ## "#{cycle}" #=> 'even'
164
+ ## "#{cycle}" #=> 'odd'
165
+ class Cycle
166
+ attr_reader :values
167
+ def initialize(*values)
168
+ @values = values.freeze
169
+ @length = values.length
170
+ @index = -1
171
+ end
172
+ attr_reader :index
173
+ def next
174
+ return @values[(@index += 1) % @length]
175
+ end
176
+ alias :to_s :next
177
+ def count
178
+ @index + 1
179
+ end
180
+ end
181
+
182
+ def new_cycle(*values)
183
+ return Cycle.new(*values)
184
+ end
185
+
186
+ end
187
+
188
+
189
+ ##
190
+ ##
191
+ ##
192
+ class SafeString < String
193
+ def to_s
194
+ self
195
+ end
196
+ end
197
+
198
+
199
+ module SafeHelper
200
+ #--
201
+ #escape() should be defined
202
+ #++
203
+
204
+ module_function
205
+
206
+ ## return SafeString object
207
+ def safe_str(s)
208
+ SafeString.new(s.to_s)
209
+ end
210
+
211
+ ## return true if s is SafeString object
212
+ def safe_str?(s)
213
+ s.is_a?(SafeString)
214
+ end
215
+
216
+ ## escape val only if val is not SafeString object, and return SafeString object
217
+ def safe_escape(val)
218
+ safe_str?(val) ? val : safe_str(escape(val))
95
219
  end
96
220
 
97
221
  end
@@ -102,7 +226,7 @@ module Tenjin
102
226
  ##
103
227
  module ContextHelper
104
228
 
105
- attr_accessor :_buf, :_engine, :_layout
229
+ attr_accessor :_buf, :_engine, :_layout, :_template
106
230
 
107
231
  ## escape value. this method should be overrided in subclass.
108
232
  def escape(val)
@@ -113,13 +237,14 @@ module Tenjin
113
237
  def import(template_name, _append_to_buf=true)
114
238
  _buf = self._buf
115
239
  output = self._engine.render(template_name, context=self, layout=false)
240
+ self._buf = _buf
116
241
  _buf << output if _append_to_buf
117
242
  return output
118
243
  end
119
244
 
120
245
  ## add value into _buf. this is equivarent to '#{value}'.
121
246
  def echo(value)
122
- self._buf << value
247
+ self._buf << value.to_s
123
248
  end
124
249
 
125
250
  ##
@@ -223,6 +348,46 @@ module Tenjin
223
348
  return s
224
349
  end
225
350
 
351
+ ##
352
+ ## cache fragment data
353
+ ##
354
+ ## ex.
355
+ ## kv_store = Tenjin::FileBaseStore.new("/var/tmp/myapp/dacache")
356
+ ## Tenjin::Engine.data_cache = kv_store
357
+ ## engine = Tenjin::Engine.new
358
+ ## # or engine = Tenjin::Engine.new(:data_cache=>kv_store)
359
+ ## entries = proc { Entry.find(:all) }
360
+ ## html = engine.render("index.rbhtml", {:entries => entries})
361
+ ##
362
+ ## index.rbhtml:
363
+ ## <html>
364
+ ## <body>
365
+ ## <?rb cache_with("entries/index", 5*60) do ?>
366
+ ## <?rb entries = @entries.call ?>
367
+ ## <ul>
368
+ ## <?rb for entry in entries ?>
369
+ ## <li>${entry.title}</li>
370
+ ## <?rb end ?>
371
+ ## </ul>
372
+ ## <?rb end ?>
373
+ ## </body>
374
+ ## </html>
375
+ ##
376
+ def cache_with(cache_key, lifetime=nil)
377
+ kv_store = self._engine.data_cache or
378
+ raise ArgumentError.new("data_cache object is not set for engine object.")
379
+ data = kv_store.get(cache_key, self._template.timestamp)
380
+ if data
381
+ echo data
382
+ else
383
+ pos = self._buf.length
384
+ yield
385
+ data = self._buf[pos..-1]
386
+ kv_store.set(cache_key, data, lifetime)
387
+ end
388
+ nil
389
+ end
390
+
226
391
  end
227
392
 
228
393
 
@@ -280,6 +445,8 @@ module Tenjin
280
445
  ##
281
446
  class Context < BaseContext
282
447
  include HtmlHelper
448
+ include HtmlTagHelper
449
+ include SafeHelper
283
450
  end
284
451
 
285
452
 
@@ -325,6 +492,11 @@ module Tenjin
325
492
  class Template
326
493
 
327
494
  ESCAPE_FUNCTION = 'escape' # or 'Eruby::Helper.escape'
495
+ TRACE = false
496
+ def self.TRACE=(boolean)
497
+ remove_const :TRACE
498
+ const_set :TRACE, boolean
499
+ end
328
500
 
329
501
  ##
330
502
  ## initializer of Template class.
@@ -342,13 +514,20 @@ module Tenjin
342
514
  @filename = filename
343
515
  @escapefunc = options[:escapefunc] || ESCAPE_FUNCTION
344
516
  @preamble = options[:preamble] == true ? "_buf = #{init_buf_expr()}; " : options[:preamble]
345
- @postamble = options[:postamble] == true ? "_buf.to_s" : options[:postamble]
517
+ @postamble = options[:postamble] == true ? finish_buf_expr() : options[:postamble]
518
+ @input = options[:input]
519
+ @trace = options[:trace] || TRACE
346
520
  @args = nil # or array of argument names
347
- convert_file(filename) if filename
521
+ if @input
522
+ convert(@input, filename)
523
+ elsif filename
524
+ convert_file(filename)
525
+ end
348
526
  end
349
527
  attr_accessor :filename, :escapefunc, :initbuf, :newline
350
528
  attr_accessor :timestamp, :args
351
529
  attr_accessor :script #,:bytecode
530
+ attr_accessor :_last_checked_at
352
531
 
353
532
  ## convert file into ruby code
354
533
  def convert_file(filename)
@@ -389,7 +568,12 @@ module Tenjin
389
568
  end
390
569
 
391
570
  def self.compile_stmt_pattern(pi)
392
- return /<\?#{pi}( |\t|\r?\n)(.*?) ?\?>([ \t]*\r?\n)?/m
571
+ return /(^[ \t]*)?<\?#{pi}(\s)(.*?) ?\?>([ \t]*\r?\n)?/m
572
+ end
573
+
574
+ def capture_stmt(matched)
575
+ #: return lspace, mspace, code, and rspace
576
+ return matched.captures()
393
577
  end
394
578
 
395
579
  STMT_PATTERN = self.compile_stmt_pattern('rb')
@@ -404,56 +588,24 @@ module Tenjin
404
588
  is_bol = true
405
589
  prev_rspace = nil
406
590
  pos = 0
407
- input.scan(stmt_pattern()) do |mspace, code, rspace|
591
+ input.scan(stmt_pattern()) do
408
592
  m = Regexp.last_match
593
+ lspace, mspace, code, rspace = capture_stmt(m)
409
594
  text = input[pos, m.begin(0) - pos]
410
595
  pos = m.end(0)
411
- ## detect spaces at beginning of line
412
- lspace = nil
413
- if rspace.nil?
414
- # nothing
415
- elsif text.empty?
416
- lspace = "" if is_bol
417
- elsif text[-1] == ?\n
418
- lspace = ""
419
- else
420
- rindex = text.rindex(?\n)
421
- if rindex
422
- s = text[rindex+1..-1]
423
- if s =~ /\A[ \t]*\z/
424
- lspace = s
425
- text = text[0..rindex]
426
- #text[rindex+1..-1] = ''
427
- end
428
- else
429
- if is_bol && text =~ /\A[ \t]*\z/
430
- lspace = text
431
- text = nil
432
- #lspace = text.dup
433
- #text[0..-1] = ''
434
- end
435
- end
436
- end
437
- is_bol = rspace ? true : false
438
596
  ##
439
597
  text.insert(0, prev_rspace) if prev_rspace
440
- parse_exprs(text)
441
- code.insert(0, mspace) if mspace != ' '
442
- if lspace
443
- assert if rspace.nil?
444
- code.insert(0, lspace)
445
- code << rspace
446
- #add_stmt(code)
447
- prev_rspace = nil
598
+ prev_rspace = nil
599
+ code = "#{mspace}#{code}" unless mspace == ' '
600
+ if lspace && rspace
601
+ code = "#{lspace}#{code}#{rspace}"
448
602
  else
449
- code << ';' unless code[-1] == ?\n
450
- #add_stmt(code)
451
- prev_rspace = rspace
452
- end
453
- if code
454
- code = statement_hook(code)
455
- add_stmt(code)
603
+ code << ";" unless code[-1] == ?\n
604
+ text << lspace if lspace && !lspace.empty?
605
+ prev_rspace = rspace if rspace && !rspace.empty?
456
606
  end
607
+ parse_exprs(text)
608
+ add_stmt(statement_hook(code)) if code && !code.empty?
457
609
  end
458
610
  #rest = $' || input
459
611
  rest = pos > 0 ? input[pos..-1] : input
@@ -608,7 +760,7 @@ module Tenjin
608
760
 
609
761
  ## create proc object
610
762
  def _render() # :nodoc:
611
- return eval("proc { |_context| self._buf = _buf = #{init_buf_expr()}; #{@script}; _buf.to_s }".untaint, nil, @filename || '(tenjin)')
763
+ return eval("proc { |_context| self._buf = _buf = #{init_buf_expr()}; #{@script}; #{finish_buf_expr()} }".untaint, nil, @filename || '(tenjin)')
612
764
  end
613
765
 
614
766
  public
@@ -617,12 +769,24 @@ module Tenjin
617
769
  return "''"
618
770
  end
619
771
 
772
+ def finish_buf_expr() # :nodoc:
773
+ return "_buf.to_s"
774
+ end
775
+
620
776
  ## evaluate converted ruby code and return it.
621
777
  ## argument '_context' should be a Hash object or Context object.
622
778
  def render(_context=Context.new)
623
779
  _context = Context.new(_context) if _context.is_a?(Hash)
624
780
  @proc ||= _render()
625
- return _context.instance_eval(&@proc)
781
+ if @trace
782
+ s = ""
783
+ s << "<!-- ***** begin: #{@filename} ***** -->\n"
784
+ s << _context.instance_eval(&@proc)
785
+ s << "<!-- ***** end: #{@filename} ***** -->\n"
786
+ return s
787
+ else
788
+ return _context.instance_eval(&@proc)
789
+ end
626
790
  end
627
791
 
628
792
  end
@@ -691,6 +855,11 @@ module Tenjin
691
855
  ##
692
856
  class ArrayBufferTemplate < Template
693
857
 
858
+ #def initialize(filename=nil, options={})
859
+ # options[:postamble] = options[:postamble] == true ? '_buf.join' : options[:postamble]
860
+ # super(filename, options)
861
+ #end
862
+
694
863
  protected
695
864
 
696
865
  def expr_pattern
@@ -724,6 +893,12 @@ module Tenjin
724
893
  return flag_escape ? "#{@escapefunc}((#{expr}).to_s)" : "(#{expr}).to_s" # or "(#{expr})"
725
894
  end
726
895
 
896
+ private
897
+
898
+ def _render() # :nodoc:
899
+ return eval("proc { |_context| self._buf = _buf = #{init_buf_expr()}; #{@script}; _buf.join }".untaint, nil, @filename || '(tenjin)')
900
+ end
901
+
727
902
  #--
728
903
  #def get_macro_handler(name)
729
904
  # if name == "start_capture"
@@ -746,6 +921,10 @@ module Tenjin
746
921
  return "[]"
747
922
  end
748
923
 
924
+ def finish_buf_expr() # :nodoc:
925
+ return "_buf.join"
926
+ end
927
+
749
928
  end
750
929
 
751
930
 
@@ -771,6 +950,349 @@ module Tenjin
771
950
  end
772
951
 
773
952
 
953
+ ##
954
+ ##
955
+ ##
956
+ class SafeTemplate < Template
957
+
958
+ ESCAPE_FUNCTION = 'safe_escape'
959
+
960
+ def initialize(filename=nil, options={})
961
+ options, filename = filename, nil if filename.is_a?(Hash)
962
+ options[:escapefunc] ||= 'safe_escape'
963
+ super(filename, options)
964
+ end
965
+
966
+ ## escape '#' in addition '\\' and '`'
967
+ def escape_str(str)
968
+ str.gsub!(/[`\#\\]/, '\\\\\&')
969
+ str.gsub!(/\r\n/, "\\r\r\n") if @newline == "\r\n"
970
+ return str
971
+ end
972
+
973
+ end
974
+
975
+
976
+ ##
977
+ ## abstract class for template cache
978
+ ##
979
+ class TemplateCache
980
+
981
+ def save(cachepath, template)
982
+ raise NotImplementedError.new("#{self.class.name}#save(): not implemented yet.")
983
+ end
984
+
985
+ def load(cachepath, timestamp=nil)
986
+ raise NotImplementedError.new("#{self.class.name}#load(): not implemented yet.")
987
+ end
988
+
989
+ end
990
+
991
+
992
+ ##
993
+ ## dummy template cache
994
+ ##
995
+ class NullTemplateCache < TemplateCache
996
+
997
+ def save(cachepath, template)
998
+ ## do nothing.
999
+ end
1000
+
1001
+ def load(cachepath, timestamp=nil)
1002
+ ## do nothing.
1003
+ end
1004
+
1005
+ end
1006
+
1007
+
1008
+ ##
1009
+ ## file base template cache which saves template script into file
1010
+ ##
1011
+ class FileBaseTemplateCache < TemplateCache
1012
+
1013
+ def save(cachepath, template)
1014
+ #: save template script and args into cache file.
1015
+ t = template
1016
+ tmppath = "#{cachepath}#{rand().to_s[1,8]}"
1017
+ s = t.args ? "\#@ARGS #{t.args.join(',')}\n" : ''
1018
+ File.open(tmppath, 'wb') {|f| f.write(s); f.write(t.script) }
1019
+ #: set cache file's mtime to template timestamp.
1020
+ File.utime(t.timestamp, t.timestamp, tmppath) if t.timestamp
1021
+ File.rename(tmppath, cachepath)
1022
+ Tenjin.logger.debug("[tenjin.rb:#{__LINE__}] cache saved (cachefile=#{cachepath.inspect}).") if Tenjin.logger
1023
+ end
1024
+
1025
+ def load(cachepath, timestamp=nil)
1026
+ # 'timestamp' argument has mtime of template file
1027
+ #: load template data from cache file.
1028
+ begin
1029
+ #: if template timestamp is specified and different from that of cache file, return nil
1030
+ mtime = File.mtime(cachepath)
1031
+ if timestamp && mtime != timestamp
1032
+ #File.unlink(cachepath)
1033
+ Tenjin.logger.debug("[tenjin.rb:#{__LINE__}] cache expired (cachefile=#{cachepath.inspect}).") if Tenjin.logger
1034
+ return nil
1035
+ end
1036
+ script = File.open(cachepath, 'rb') {|f| f.read }
1037
+ rescue Errno::ENOENT => ex
1038
+ #: if cache file is not found, return nil.
1039
+ Tenjin.logger.debug("[tenjin.rb:#{__LINE__}] cache not found (cachefile=#{cachepath.inspect}).") if Tenjin.logger
1040
+ return nil
1041
+ end
1042
+ #: get template args data from cached data.
1043
+ args = script.sub!(/\A\#@ARGS (.*)\r?\n/, '') ? $1.split(/,/) : []
1044
+ #: return script, template args, and mtime of cache file.
1045
+ Tenjin.logger.debug("[tenjin.rb:#{__LINE__}] cache found (cachefile=#{cachepath.inspect}).") if Tenjin.logger
1046
+ return [script, args, mtime]
1047
+ end
1048
+
1049
+ end
1050
+
1051
+
1052
+ ##
1053
+ ## abstract class for data cache (= html fragment cache)
1054
+ ##
1055
+ class KeyValueStore
1056
+
1057
+ def get(key, *options)
1058
+ raise NotImplementedError.new("#{self.class.name}#get(): not implemented yet.")
1059
+ end
1060
+
1061
+ def set(key, value, *options)
1062
+ raise NotImplementedError.new("#{self.class.name}#set(): not implemented yet.")
1063
+ end
1064
+
1065
+ def del(key, *options)
1066
+ raise NotImplementedError.new("#{self.class.name}#del(): not implemented yet.")
1067
+ end
1068
+
1069
+ def has?(key, *options)
1070
+ raise NotImplementedError.new("#{self.class.name}#has(): not implemented yet.")
1071
+ end
1072
+
1073
+ def [](key)
1074
+ return get(key)
1075
+ end
1076
+
1077
+ def []=(key, value)
1078
+ return set(key, value)
1079
+ end
1080
+
1081
+ end
1082
+
1083
+
1084
+ ##
1085
+ ## memory base data store
1086
+ ##
1087
+ class MemoryBaseStore < KeyValueStore
1088
+
1089
+ def initialize(lifetime=604800)
1090
+ @values = {}
1091
+ @lifetime = lifetime
1092
+ end
1093
+ attr_accessor :values, :lifetime
1094
+
1095
+ def set(key, value, lifetime=nil)
1096
+ #: store key and value with current and expired timestamp
1097
+ now = Time.now
1098
+ @values[key] = [value, now, now + (lifetime || @lifetime)]
1099
+ end
1100
+
1101
+ def get(key, original_timestamp=nil)
1102
+ #: if cache data is not found, return nil
1103
+ arr = @values[key]
1104
+ return nil if arr.nil?
1105
+ #: if cache data is older than original data, remove it and return nil
1106
+ value, created_at, timestamp = arr
1107
+ if original_timestamp && created_at < original_timestamp
1108
+ del(key)
1109
+ return nil
1110
+ end
1111
+ #: if cache data is expired then remove it and return nil
1112
+ if timestamp < Time.now
1113
+ del(key)
1114
+ return nil
1115
+ end
1116
+ #: return cache data
1117
+ return value
1118
+ end
1119
+
1120
+ def del(key)
1121
+ #: remove data
1122
+ #: don't raise error even if key doesn't exist
1123
+ @values.delete(key)
1124
+ end
1125
+
1126
+ def has?(key)
1127
+ #: if key exists then return true else return false
1128
+ return @values.key?(key)
1129
+ end
1130
+
1131
+ end
1132
+
1133
+
1134
+ ##
1135
+ ## file base data store
1136
+ ##
1137
+ class FileBaseStore < KeyValueStore
1138
+
1139
+ def initialize(root, lifetime=604800) # = 60*60*24*7
1140
+ self.root = root
1141
+ self.lifetime = lifetime
1142
+ end
1143
+ attr_accessor :root, :lifetime
1144
+
1145
+ def root=(path)
1146
+ unless File.directory?(path)
1147
+ raise ArgumentError.new("#{path}: not found.") unless File.exist?(path)
1148
+ raise ArgumentError.new("#{path}: not a directory.")
1149
+ end
1150
+ path = path.chop if path[-1] == ?/
1151
+ @root = path
1152
+ end
1153
+
1154
+ def filepath(key)
1155
+ #return File.join(@root, key.gsub(/[^-.\w\/]/, '_'))
1156
+ return "#{@root}/#{key.gsub(/[^-.\w\/]/, '_')}"
1157
+ end
1158
+
1159
+ def set(key, value, lifetime=nil)
1160
+ #: create directory for cache
1161
+ fpath = filepath(key)
1162
+ dir = File.dirname(fpath)
1163
+ unless File.exist?(dir)
1164
+ require 'fileutils' #unless defined?(FileUtils)
1165
+ FileUtils.mkdir_p(dir)
1166
+ end
1167
+ #: create temporary file and rename it to cache file (in order not to flock)
1168
+ tmppath = "#{fpath}#{rand().to_s[1,8]}"
1169
+ _write_binary(tmppath, value)
1170
+ File.rename(tmppath, fpath)
1171
+ #: set mtime (which is regarded as cache expired timestamp)
1172
+ timestamp = Time.now + (lifetime || @lifetime)
1173
+ File.utime(timestamp, timestamp, fpath)
1174
+ #: return value
1175
+ return value
1176
+ end
1177
+
1178
+ def get(key, original_timestamp=nil)
1179
+ #: if cache file is not found, return nil
1180
+ fpath = filepath(key)
1181
+ #return nil unless File.exist?(fpath)
1182
+ stat = _ignore_not_found_error { File.stat(fpath) }
1183
+ return nil if stat.nil?
1184
+ #: if cache file is older than original data, remove it and return nil
1185
+ if original_timestamp && stat.ctime < original_timestamp
1186
+ del(key)
1187
+ return nil
1188
+ end
1189
+ #: if cache file is expired then remove it and return nil
1190
+ if stat.mtime < Time.now
1191
+ del(key)
1192
+ return nil
1193
+ end
1194
+ #: return cache file content
1195
+ return _ignore_not_found_error { _read_binary(fpath) }
1196
+ end
1197
+
1198
+ def del(key, *options)
1199
+ #: delete data file
1200
+ #: if data file doesn't exist, don't raise error
1201
+ fpath = filepath(key)
1202
+ _ignore_not_found_error { File.unlink(fpath) }
1203
+ nil
1204
+ end
1205
+
1206
+ def has?(key)
1207
+ #: if key exists then return true else return false
1208
+ return File.exist?(filepath(key))
1209
+ end
1210
+
1211
+ private
1212
+
1213
+ if RUBY_PLATFORM =~ /mswin(?!ce)|mingw|cygwin|bccwin/i
1214
+ def _read_binary(fpath)
1215
+ File.open(fpath, 'rb') {|f| f.read }
1216
+ end
1217
+ else
1218
+ def _read_binary(fpath)
1219
+ File.read(fpath)
1220
+ end
1221
+ end
1222
+
1223
+ def _write_binary(fpath, data)
1224
+ File.open(fpath, 'wb') {|f| f.write(data) }
1225
+ end
1226
+
1227
+ def _ignore_not_found_error(default=nil)
1228
+ begin
1229
+ return yield
1230
+ rescue Errno::ENOENT => ex
1231
+ return default
1232
+ end
1233
+ end
1234
+
1235
+ end
1236
+
1237
+
1238
+ ##
1239
+ ##
1240
+ ##
1241
+ class TemplateNotFoundError < StandardError
1242
+ end
1243
+
1244
+
1245
+ ##
1246
+ ## helper class for Engine to find and read files
1247
+ ##
1248
+ class FileFinder
1249
+
1250
+ def find(filename, dirs=nil)
1251
+ if dirs
1252
+ #: if dirs specified then find file from it.
1253
+ for dir in dirs
1254
+ filepath = File.join(dir, filename)
1255
+ return filepath if File.file?(filepath)
1256
+ end
1257
+ #found = dirs.find {|dir| File.isfile(File.join(dir, filename)) }
1258
+ #return File.join(found, filename) if found
1259
+ else
1260
+ #: if dirs not specified then return filename if it exists.
1261
+ return filename if File.file?(filename)
1262
+ end
1263
+ #: if file not found then return nil.
1264
+ return nil
1265
+ end
1266
+
1267
+ def timestamp(filepath)
1268
+ #: return mtime of filepath.
1269
+ return File.mtime(filepath)
1270
+ end
1271
+
1272
+ def read(filepath)
1273
+ begin
1274
+ #: if file exists then return file content and mtime.
1275
+ mtime = File.mtime(filepath)
1276
+ input = File.open(filepath, 'rb') {|f| f.read }
1277
+ mtime2 = File.mtime(filepath)
1278
+ if mtime != mtime2
1279
+ mtime = mtime2
1280
+ input = File.open(filepath, 'rb') {|f| f.read }
1281
+ mtime2 = File.mtime(filepath)
1282
+ if mtime != mtime2
1283
+ Tenjin.logger.warn("[tenjin.rb:#{__LINE__}] #{self.class.name}#read(): timestamp is changed while reading file.") if Tenjin.logger
1284
+ end
1285
+ end
1286
+ return input, mtime
1287
+ rescue Errno::ENOENT
1288
+ #: if file not found then return nil.
1289
+ return nil
1290
+ end
1291
+ end
1292
+
1293
+ end
1294
+
1295
+
774
1296
  ##
775
1297
  ## engine class for templates
776
1298
  ##
@@ -823,144 +1345,248 @@ module Tenjin
823
1345
  @prefix = options[:prefix] || ''
824
1346
  @postfix = options[:postfix] || ''
825
1347
  @layout = options[:layout]
826
- @cache = options.fetch(:cache, true)
827
1348
  @path = options[:path]
1349
+ @lang = options[:lang]
1350
+ @finder = options[:finder] || FileFinder.new
1351
+ @cache = _template_cache(options[:cache])
828
1352
  @preprocess = options.fetch(:preprocess, nil)
1353
+ @data_cache = options[:data_cache] || @@data_cache
829
1354
  @templateclass = options.fetch(:templateclass, Template)
830
1355
  @init_opts_for_template = options
831
- @templates = {} # filename->template
1356
+ @_templates = {} # template_name => [template_obj, filepath]
1357
+ end
1358
+ attr_accessor :prefix, :postfix, :layout, :path, :lang, :cache
1359
+ attr_accessor :preprocess, :data_cache, :templateclass
1360
+
1361
+ def _template_cache(cache) #:nodoc:
1362
+ #: if cache is nil or true then return @@template_cache
1363
+ return @@template_cache if cache.nil? || cache == true
1364
+ #: if cache is false then return NullTemplateCache object
1365
+ return NullTemplateCache.new if cache == false
1366
+ #: if cache is an instnce of TemplateClass then return it
1367
+ return cache if cache.is_a?(TemplateCache)
1368
+ #: if else then raises error
1369
+ raise ArgumentError.new(":cache is expected true, false, or TemplateCache object")
1370
+ end
1371
+ private :_template_cache
1372
+
1373
+ @@template_cache = FileBaseTemplateCache.new()
1374
+ def self.template_cache; @@template_cache; end
1375
+ def self.template_cache=(x); @@template_cache = x; end
1376
+
1377
+ @@data_cache = MemoryBaseStore.new()
1378
+ def self.data_cache; @@data_cache; end
1379
+ def self.data_cache=(x); @@data_cache = x; end
1380
+
1381
+ TIMESTAMP_INTERVAL = 1.0
1382
+
1383
+ ## register template object
1384
+ def register_template(template_name, template)
1385
+ #: register template object without file path.
1386
+ filename = to_filename(template_name)
1387
+ @_templates[filename] = [template, nil]
1388
+ end
1389
+
1390
+ ## returns cache file path of template file
1391
+ def cachename(filepath)
1392
+ #: if lang is provided then add it to cache filename.
1393
+ if @lang
1394
+ return "#{filepath}.#{@lang}.cache".untaint
1395
+ #: return cache file name which is untainted.
1396
+ else
1397
+ return "#{filepath}.cache".untaint
1398
+ end
832
1399
  end
833
1400
 
834
1401
  ## convert short name into filename (ex. ':list' => 'template/list.rb.html')
835
1402
  def to_filename(template_name)
1403
+ #: if template_name is a Symbol, add prefix and postfix to it.
1404
+ #: if template_name is not a Symbol, just return it.
836
1405
  name = template_name
837
1406
  return name.is_a?(Symbol) ? "#{@prefix}#{name}#{@postfix}" : name
838
1407
  end
839
1408
 
840
- ## find template filename
841
- def find_template_file(template_name)
842
- filename = to_filename(template_name)
843
- if @path
844
- for dir in @path
845
- filepath = "#{dir}#{File::SEPARATOR}#{filename}"
846
- return filepath if test(?f, filepath.untaint)
847
- end
1409
+ private
1410
+
1411
+ def _timestamp_changed?(template)
1412
+ #: if checked within a sec, skip timestamp check and return false.
1413
+ time = template._last_checked_at
1414
+ now = Time.now
1415
+ if time && now - time < TIMESTAMP_INTERVAL
1416
+ return false
1417
+ end
1418
+ #: if timestamp is same as file, return false.
1419
+ filepath = template.filename
1420
+ if template.timestamp == @finder.timestamp(filepath)
1421
+ template._last_checked_at = now
1422
+ return false
1423
+ #: if timestamp is changed, return true.
848
1424
  else
849
- return filename if test(?f, filename.dup.untaint) # dup is required for frozen string
1425
+ Tenjin.logger.info("[tenjin.rb:#{__LINE__}] cache expired (template='#{template.filename}')") if Tenjin.logger
1426
+ return true
850
1427
  end
851
- raise Errno::ENOENT.new("#{filename} (path=#{@path.inspect})")
852
1428
  end
853
1429
 
854
- ## read template file and preprocess it
855
- def read_template_file(filename, _context)
856
- return File.read(filename) if !@preprocess
857
- _context ||= {}
858
- if _context.is_a?(Hash) || _context._engine.nil?
859
- _context = hook_context(_context)
1430
+ def _get_template_in_memory(filename)
1431
+ template, filepath = @_templates[filename]
1432
+ #: if template object is not in memory cache then return nil.
1433
+ return nil unless template
1434
+ #: if without filepath, don't check timestamp and return it.
1435
+ return template unless filepath
1436
+ #: if timestamp of template file is not changed, return it.
1437
+ return template unless _timestamp_changed?(template)
1438
+ #: if timestamp of template file is changed, clear it and return nil.
1439
+ @_templates.delete(filename)
1440
+ return nil
1441
+ end
1442
+
1443
+ def _get_template_in_cache(filepath, cachepath)
1444
+ #: if template is not found in cache file, return nil.
1445
+ template = @cache.load(cachepath)
1446
+ return nil unless template
1447
+ #: if cache returns script and args then build a template object from them.
1448
+ if template.is_a?(Array)
1449
+ arr = template
1450
+ template = create_template(nil, nil)
1451
+ template.script, template.args, template.timestamp = arr
1452
+ template.filename = filepath
860
1453
  end
861
- preprocessor = Preprocessor.new(filename)
862
- return preprocessor.render(_context)
863
- end
864
-
865
- ## register template object
866
- def register_template(template_name, template)
867
- #template.timestamp = Time.new unless template.timestamp
868
- @templates[template_name] = template
1454
+ #: if timestamp of template is changed then ignore it.
1455
+ return nil if _timestamp_changed?(template)
1456
+ #: if timestamp is not changed then return it.
1457
+ @tracer.trace("template '#{filename}' found in cache.") if @tracer
1458
+ return template
869
1459
  end
870
1460
 
871
- def cachename(filename)
872
- return (filename + '.cache').untaint
873
- end
1461
+ public
874
1462
 
875
- ## create template object from file
876
- def create_template(filename, _context=nil)
877
- template = @templateclass.new(nil, @init_opts_for_template)
878
- template.timestamp = Time.now()
879
- cache_filename = cachename(filename)
880
- _context = hook_context(Context.new) if _context.nil?
881
- if !@cache
882
- input = read_template_file(filename, _context)
883
- template.convert(input, filename)
884
- elsif !test(?f, cache_filename) || File.mtime(cache_filename) < File.mtime(filename)
885
- #$stderr.puts "*** debug: load original"
886
- input = read_template_file(filename, _context)
887
- template.convert(input, filename)
888
- store_cachefile(cache_filename, template)
889
- else
890
- #$stderr.puts "*** debug: load cache"
891
- template.filename = filename
892
- load_cachefile(cache_filename, template)
1463
+ def get_template(template_name, _context=nil)
1464
+ #: accept template name such as :index.
1465
+ filename = to_filename(template_name)
1466
+ #: if template object is in memory cache then return it.
1467
+ template = _get_template_in_memory(filename)
1468
+ return template if template
1469
+ #: if template file is not found then raise TemplateNotFoundError.
1470
+ filepath = @finder.find(filename, @path) or
1471
+ raise TemplateNotFoundError.new("#{filename}: template not found (path=#{@path.inspect}).")
1472
+ #: if template is cached in file then store it into memory and return it.
1473
+ cachepath = cachename(filepath)
1474
+ template = _get_template_in_cache(filepath, cachepath)
1475
+ if template
1476
+ @_templates[filename] = [template, filepath]
1477
+ return template
893
1478
  end
1479
+ #: if template file is not found then raises TemplateNotFoundError.
1480
+ ret = @finder.read(filepath) or
1481
+ raise TemplateNotFoundError.new("#{filepath}: template not found.")
1482
+ input, timestamp = ret
1483
+ #: if preprocess is enabled then preprocess template file.
1484
+ input = _preprocess(input, filepath, _context) if @preprocess
1485
+ #: if template is not found in memory nor cache then create new one.
1486
+ template = create_template(input, filepath)
1487
+ template.filename = filepath
1488
+ template.timestamp = timestamp
1489
+ template._last_checked_at = Time.now
1490
+ #: save template object into file cache and memory cache.
1491
+ @cache.save(cachepath, template) if @cache
1492
+ @_templates[filename] = [template, filepath]
1493
+ #: return template object.
894
1494
  return template
895
1495
  end
896
1496
 
897
- ## store template into cache file
898
- def store_cachefile(cache_filename, template)
899
- s = template.script
900
- s = "\#@ARGS #{template.args.join(',')}\n#{s}" if template.args
901
- File.open(cache_filename, 'w') do |f|
902
- f.flock(File::LOCK_EX)
903
- f.write(s)
904
- #f.lock(FIle::LOCK_UN) # File#close() unlocks automatically
1497
+ private
1498
+
1499
+ def _preprocess(input, filepath, _context=nil)
1500
+ #: preprocess input with _context and return result.
1501
+ _context ||= {}
1502
+ _context = hook_context(_context) if _context.is_a?(Hash)
1503
+ _buf = _context._buf
1504
+ _context._buf = ""
1505
+ begin
1506
+ preprocessor = Preprocessor.new(nil)
1507
+ preprocessor.convert(input, filepath)
1508
+ return preprocessor.render(_context)
1509
+ ensure
1510
+ _context._buf = _buf
905
1511
  end
906
1512
  end
907
1513
 
908
- ## load template from cache file
909
- def load_cachefile(cache_filename, template)
910
- s = File.read(cache_filename)
911
- if s.sub!(/\A\#\@ARGS (.*?)\r?\n/, '')
912
- template.args = $1.split(',')
913
- end
914
- template.script = s
1514
+ protected
1515
+
1516
+ ## create template object from file
1517
+ def create_template(input=nil, filepath=nil)
1518
+ #: create template object and return it.
1519
+ template = @templateclass.new(nil, @init_opts_for_template)
1520
+ #: if input is specified then convert it into script.
1521
+ template.convert(input, filepath) if input
1522
+ return template
915
1523
  end
916
1524
 
917
- ## get template object
918
- def get_template(template_name, _context=nil)
919
- template = @templates[template_name]
920
- t = template
921
- unless t && t.timestamp && t.filename && t.timestamp >= File.mtime(t.filename)
922
- filename = find_template_file(template_name)
923
- template = create_template(filename, _context) # _context is passed only for preprocessor
924
- register_template(template_name, template)
1525
+ def hook_context(context)
1526
+ #: if context is nil then create new Context object
1527
+ if !context
1528
+ context = Context.new
1529
+ #: if context is a Hash object then convert it into Context object
1530
+ elsif context.is_a?(Hash)
1531
+ context = Context.new(context)
1532
+ #: if context is an object then use it as context object
1533
+ else
1534
+ # nothing
925
1535
  end
926
- return template
1536
+ #: set _engine attribute
1537
+ context._engine = self
1538
+ #: set _layout attribute
1539
+ context._layout = nil
1540
+ #: return context object
1541
+ return context
927
1542
  end
928
1543
 
1544
+ public
1545
+
929
1546
  ## get template object and evaluate it with context object.
930
1547
  ## if argument 'layout' is true then default layout file (specified at
931
1548
  ## initializer) is used as layout template, else if false then no layout
932
1549
  ## template is used.
933
1550
  ## if argument 'layout' is string, it is regarded as layout template name.
934
1551
  def render(template_name, context=Context.new, layout=true)
935
- #context = Context.new(context) if context.is_a?(Hash)
1552
+ #: if context is a Hash object, convert it into Context object.
936
1553
  context = hook_context(context)
937
1554
  while true
1555
+ # get template
938
1556
  template = get_template(template_name, context) # context is passed only for preprocessor
939
- _buf = context._buf
1557
+ #: set template object into context (required for cache_with() helper)
1558
+ _tmpl = context._template
1559
+ context._template = template
1560
+ # render template
940
1561
  output = template.render(context)
941
- context._buf = _buf
1562
+ # back template
1563
+ context._template = _tmpl
1564
+ #: if @_layout is specified, use it as layoute template name
942
1565
  unless context._layout.nil?
943
1566
  layout = context._layout
944
1567
  context._layout = nil
945
1568
  end
946
- layout = @layout if layout == true or layout.nil?
1569
+ #: use default layout template if layout is true or nil
1570
+ layout = @layout if layout == true || layout.nil?
1571
+ #: if layout is false then don't use layout template
947
1572
  break unless layout
1573
+ #: set layout name as next template name
948
1574
  template_name = layout
949
1575
  layout = false
1576
+ #: set output into @_content for layout template
950
1577
  context.instance_variable_set('@_content', output)
951
1578
  end
952
1579
  return output
953
1580
  end
954
1581
 
955
- def hook_context(context)
956
- if !context
957
- context = Context.new
958
- elsif context.is_a?(Hash)
959
- context = Context.new(context)
960
- end
961
- context._engine = self
962
- context._layout = nil
963
- return context
1582
+ end
1583
+
1584
+
1585
+ class SafeEngine < Engine
1586
+
1587
+ def initialize(options={})
1588
+ options[:templateclass] = SafeTemplate
1589
+ super(options)
964
1590
  end
965
1591
 
966
1592
  end