tenjin 0.6.2 → 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGES.txt +163 -0
- data/MIT-LICENSE +16 -17
- data/README.txt +5 -5
- data/benchmark/Makefile +9 -0
- data/benchmark/bench.rb +3 -2
- data/bin/rbtenjin +6 -3
- data/doc/docstyle.css +25 -4
- data/doc/users-guide.html +2088 -1563
- data/lib/tenjin.rb +784 -158
- data/public_html/_layout.rbhtml +33 -0
- data/public_html/css/style.css +77 -0
- data/public_html/env.rbhtml +15 -0
- data/public_html/favicon.ico +0 -0
- data/public_html/hello.rbhtml +14 -0
- data/public_html/index.rbhtml +39 -0
- data/public_html/rbtenjin.cgi +188 -0
- data/tenjin.gemspec +9 -6
- data/test/data/examples/preprocessing/main.rb +1 -1
- data/test/data/examples/preprocessing/select.rbhtml +1 -1
- data/test/data/faq/{ex11-bench.rb → ex10-bench.rb} +3 -3
- data/test/data/faq/ex10-content.rbhtml +8 -11
- data/test/data/faq/{ex11-layout1.rbhtml → ex10-layout1.rbhtml} +0 -0
- data/test/data/faq/{ex11-layout2.rbhtml → ex10-layout2.rbhtml} +0 -0
- data/test/data/faq/{ex11.rb → ex10.rb} +1 -1
- data/test/data/faq/{ex11.rbhtml → ex10.rbhtml} +0 -0
- data/test/data/faq/{ex11.source → ex10.source} +2 -2
- data/test/data/faq/{ex11_arraybuffer.result → ex10_arraybuffer.result} +1 -1
- data/test/data/faq/{ex5.rbhtml → ex2.rbhtml} +0 -0
- data/test/data/faq/{ex5_template_args.source → ex2_template_args.source} +1 -1
- data/test/data/faq/{ex7-expr-pattern.rb → ex4-expr-pattern.rb} +1 -1
- data/test/data/faq/{ex7-expr-pattern.rbhtml → ex4-expr-pattern.rbhtml} +0 -0
- data/test/data/faq/{ex7_expr_pattern.result → ex4_expr_pattern.result} +1 -1
- data/test/data/faq/{ex6-content.rhtml → ex5-content.rhtml} +0 -0
- data/test/data/faq/{ex6-layout.rhtml → ex5-layout.rhtml} +0 -0
- data/test/data/faq/{ex6.rb → ex5.rb} +2 -2
- data/test/data/faq/{ex6_eruby.result → ex5_eruby.result} +1 -1
- data/test/data/faq/{ex2-content.rbhtml → ex6-content.rbhtml} +0 -0
- data/test/data/faq/{ex2-layout.rbhtml → ex6-layout.rbhtml} +0 -0
- data/test/data/faq/{ex2_removenl.result → ex6_removenl.result} +1 -1
- data/test/data/faq/ex7-m18n.rb +48 -0
- data/test/data/faq/{ex8-m18n.rbhtml → ex7-m18n.rbhtml} +0 -0
- data/test/data/faq/{ex8_m18n.result → ex7_m18n.result} +1 -1
- data/test/data/faq/ex8-baselayout.rbhtml +8 -0
- data/test/data/faq/ex8-content.rbhtml +6 -0
- data/test/data/faq/{ex9-mylayout.rbhtml → ex8-mylayout.rbhtml} +0 -0
- data/test/data/faq/{ex9_changelayout.result → ex8_changelayout.result} +1 -1
- data/test/data/faq/ex9-baselayout.rbhtml +24 -5
- data/test/data/faq/ex9-content.rbhtml +12 -6
- data/test/data/faq/{ex10-customlayout.rbhtml → ex9-customlayout.rbhtml} +1 -1
- data/test/data/faq/{ex10_inherit.result → ex9_inherit.result} +1 -1
- data/test/data/faq/helpers1.rb +17 -0
- data/test/data/faq/helpers1.rbhtml +4 -0
- data/test/data/faq/helpers1.result +11 -0
- data/test/data/faq/helpers2.rb +21 -0
- data/test/data/faq/helpers2.rbhtml +4 -0
- data/test/data/faq/helpers2.result +11 -0
- data/test/data/faq/weekday1.rb +6 -0
- data/test/data/faq/weekday1.rbhtml +9 -0
- data/test/data/faq/weekday1.result +23 -0
- data/test/data/faq/weekday2.rb +8 -0
- data/test/data/faq/weekday2.rbhtml +10 -0
- data/test/data/faq/weekday2.result +24 -0
- data/test/data/faq/weekday3.rb +22 -0
- data/test/data/faq/weekday3.rbhtml +3 -0
- data/test/data/faq/weekday3.result +28 -0
- data/test/data/users_guide/test_010/main.rb +14 -0
- data/test/data/users_guide/test_010/result.output +13 -0
- data/test/data/users_guide/test_010/views/page.rbhtml +11 -0
- data/test/data/users_guide/test_011/main.rb +14 -0
- data/test/data/users_guide/test_011/result.output +2 -0
- data/test/data/users_guide/test_011/views/page.rbhtml +11 -0
- data/test/data/users_guide/test_020/main.rb +14 -0
- data/test/data/users_guide/test_020/result.output +13 -0
- data/test/data/users_guide/test_020/views/page.rbhtml +11 -0
- data/test/data/users_guide/test_021/main.rb +12 -0
- data/test/data/users_guide/test_021/result.output +12 -0
- data/test/data/users_guide/test_021/views/page.rbhtml +11 -0
- data/test/data/users_guide/test_030/main.rb +14 -0
- data/test/data/users_guide/test_030/result.output +23 -0
- data/test/data/users_guide/test_030/views/_layout.rbhtml +10 -0
- data/test/data/users_guide/test_030/views/page.rbhtml +11 -0
- data/test/data/users_guide/test_040/main.rb +14 -0
- data/test/data/users_guide/test_040/result.output +23 -0
- data/test/data/users_guide/test_040/views/_layout.rbhtml +10 -0
- data/test/data/users_guide/test_040/views/page.rbhtml +12 -0
- data/test/data/users_guide/test_050/main.rb +14 -0
- data/test/data/users_guide/test_050/result.output +14 -0
- data/test/data/users_guide/test_050/views/_layout.rbhtml +10 -0
- data/test/data/users_guide/test_050/views/page.rbhtml +13 -0
- data/test/data/users_guide/test_051/main.rb +14 -0
- data/test/data/users_guide/test_051/result.output +12 -0
- data/test/data/users_guide/test_051/views/_layout.rbhtml +11 -0
- data/test/data/users_guide/test_051/views/page.rbhtml +13 -0
- data/test/data/users_guide/test_060/main.rb +14 -0
- data/test/data/users_guide/test_060/result.output +29 -0
- data/test/data/users_guide/test_060/views/_footer.rbhtml +3 -0
- data/test/data/users_guide/test_060/views/_header.rbhtml +3 -0
- data/test/data/users_guide/test_060/views/_layout.rbhtml +13 -0
- data/test/data/users_guide/test_060/views/page.rbhtml +13 -0
- data/test/data/users_guide/test_070/main.rb +14 -0
- data/test/data/users_guide/test_070/result.output +29 -0
- data/test/data/users_guide/test_070/views/_footer.rbhtml +3 -0
- data/test/data/users_guide/test_070/views/_header.rbhtml +3 -0
- data/test/data/users_guide/test_070/views/_layout.rbhtml +13 -0
- data/test/data/users_guide/test_070/views/page.rbhtml +13 -0
- data/test/data/users_guide/test_capturing/main.rb +24 -0
- data/test/data/users_guide/test_capturing/result.output +28 -0
- data/test/data/users_guide/test_capturing/views/_layout.rbhtml +21 -0
- data/test/data/users_guide/test_capturing/views/blog-post.rbhtml +13 -0
- data/test/data/users_guide/test_context/context.rb +5 -0
- data/test/data/users_guide/test_context/context.yaml +4 -0
- data/test/data/users_guide/test_context/example.rbhtml +5 -0
- data/test/data/users_guide/test_context/result1.output +6 -0
- data/test/data/users_guide/test_context/result2.output +6 -0
- data/test/data/users_guide/test_context/result3.output +6 -0
- data/test/data/users_guide/test_context/result4.output +6 -0
- data/test/data/users_guide/test_convert/example.rbhtml +5 -0
- data/test/data/users_guide/test_convert/result1.output +7 -0
- data/test/data/users_guide/test_convert/result2.output +6 -0
- data/test/data/users_guide/test_escape/main.rb +4 -0
- data/test/data/users_guide/test_escape/result.output +5 -0
- data/test/data/users_guide/test_escape/views/page.rbhtml +4 -0
- data/test/data/users_guide/test_execute/example.rbhtml +6 -0
- data/test/data/users_guide/test_execute/result.output +6 -0
- data/test/data/users_guide/test_fragmentcache/cache.d/items/1 +5 -0
- data/test/data/users_guide/test_fragmentcache/main.rb +21 -0
- data/test/data/users_guide/test_fragmentcache/result.output +8 -0
- data/test/data/users_guide/test_fragmentcache/result2.output +6 -0
- data/test/data/users_guide/test_fragmentcache/views/items.rbhtml +10 -0
- data/test/data/users_guide/test_logging/ex-logger.rb +11 -0
- data/test/data/users_guide/test_logging/example.rbhtml +0 -0
- data/test/data/users_guide/test_logging/result1.output +3 -0
- data/test/data/users_guide/test_logging/result2.output +2 -0
- data/test/data/users_guide/test_m17n/m17n.rb +44 -0
- data/test/data/users_guide/test_m17n/m17n.rbhtml +5 -0
- data/test/data/users_guide/test_m17n/result.output +9 -0
- data/test/data/users_guide/test_m17n/result_en.output +4 -0
- data/test/data/users_guide/test_m17n/result_fr.output +4 -0
- data/test/data/users_guide/test_nested/main.rb +8 -0
- data/test/data/users_guide/test_nested/result.output +15 -0
- data/test/data/users_guide/test_nested/views/_blog_layout.rbhtml +5 -0
- data/test/data/users_guide/{layout8_html.rbhtml → test_nested/views/_site_layout.rbhtml} +0 -0
- data/test/data/users_guide/test_nested/views/blog_post.rbhtml +4 -0
- data/test/data/users_guide/test_preprocessing/pp-example1.rb +14 -0
- data/test/data/users_guide/test_preprocessing/result1a.output +11 -0
- data/test/data/users_guide/test_preprocessing/result1b.output +5 -0
- data/test/data/users_guide/test_preprocessing/result1c.output +6 -0
- data/test/data/users_guide/test_preprocessing/result2a.output +10 -0
- data/test/data/users_guide/test_preprocessing/result2b.output +10 -0
- data/test/data/users_guide/test_preprocessing/result3a.output +2 -0
- data/test/data/users_guide/test_preprocessing/result3b.output +2 -0
- data/test/data/users_guide/test_preprocessing/views/pp-example1.rbhtml +4 -0
- data/test/data/users_guide/{example12.rbhtml → test_preprocessing/views/pp-example2.rbhtml} +4 -4
- data/test/data/users_guide/test_preprocessing/views/pp-example3.rbhtml +7 -0
- data/test/data/users_guide/test_retrieve/example.rbhtml +10 -0
- data/test/data/users_guide/test_retrieve/result1.output +11 -0
- data/test/data/users_guide/test_retrieve/result2.output +11 -0
- data/test/data/users_guide/test_retrieve/result3.output +11 -0
- data/test/data/users_guide/test_retrieve/result4.output +8 -0
- data/test/data/users_guide/test_retrieve/result5.output +5 -0
- data/test/data/users_guide/test_safe/result.output +6 -0
- data/test/data/users_guide/test_safe/safe-test.rb +21 -0
- data/test/data/users_guide/test_safehelper/main.rb +16 -0
- data/test/data/users_guide/test_safehelper/result.output +8 -0
- data/test/data/users_guide/{example3.rbhtml → test_syntax_check/example.rbhtml} +0 -0
- data/test/data/users_guide/test_syntax_check/result.output +2 -0
- data/test/data/users_guide/test_trace/layout.rbhtml +7 -0
- data/test/data/users_guide/test_trace/main.rbhtml +5 -0
- data/test/data/users_guide/test_trace/result.output +16 -0
- data/test/data/users_guide/test_trace/trace-example.rb +4 -0
- data/test/oktest.rb +755 -0
- data/test/test_all.rb +24 -14
- data/test/test_engine.rb +628 -63
- data/test/test_engine.yaml +40 -3
- data/test/test_examples.rb +14 -12
- data/test/test_faq.rb +17 -12
- data/test/test_htmlhelper.rb +104 -33
- data/test/test_main.rb +32 -21
- data/test/test_main.yaml +2 -2
- data/test/test_safe.rb +206 -0
- data/test/test_store.rb +220 -0
- data/test/test_tcache.rb +94 -0
- data/test/test_template.rb +65 -23
- data/test/test_template.yaml +7 -7
- data/test/test_users_guide.rb +75 -29
- data/test/testcase-helper.rb +20 -18
- data/test/testunit-assertions.rb +71 -0
- metadata +185 -159
- data/doc-api/classes/Tenjin.html +0 -141
- data/doc-api/classes/Tenjin/ArrayBufferTemplate.html +0 -270
- data/doc-api/classes/Tenjin/BaseContext.html +0 -329
- data/doc-api/classes/Tenjin/Context.html +0 -126
- data/doc-api/classes/Tenjin/ContextHelper.html +0 -461
- data/doc-api/classes/Tenjin/Engine.html +0 -616
- data/doc-api/classes/Tenjin/ErubisTemplate.html +0 -166
- data/doc-api/classes/Tenjin/HtmlHelper.html +0 -359
- data/doc-api/classes/Tenjin/Preprocessor.html +0 -242
- data/doc-api/classes/Tenjin/Template.html +0 -916
- data/doc-api/created.rid +0 -1
- data/doc-api/files/README_txt.html +0 -188
- data/doc-api/files/lib/tenjin_rb.html +0 -136
- data/doc-api/fr_class_index.html +0 -36
- data/doc-api/fr_file_index.html +0 -28
- data/doc-api/fr_method_index.html +0 -91
- data/doc-api/index.html +0 -24
- data/doc-api/rdoc-style.css +0 -208
- data/doc/examples.html +0 -312
- data/doc/faq.html +0 -909
- data/examples/preprocessing/select.rbhtml.cache +0 -17
- data/test/Rookbook.yaml +0 -14
- data/test/assert-text-equal.rb +0 -45
- data/test/data/faq/ex10-baselayout.rbhtml +0 -27
- data/test/data/faq/ex11-content.rbhtml +0 -9
- data/test/data/faq/ex8-m18n.rb +0 -77
- data/test/data/users_guide/content6.rbhtml +0 -3
- data/test/data/users_guide/content7.rbhtml +0 -5
- data/test/data/users_guide/content8.rbhtml +0 -2
- data/test/data/users_guide/contextdata.rb +0 -7
- data/test/data/users_guide/datafile.rb +0 -5
- data/test/data/users_guide/datafile.yaml +0 -10
- data/test/data/users_guide/ex.rbhtml +0 -6
- data/test/data/users_guide/ex.result +0 -7
- data/test/data/users_guide/ex.script +0 -5
- data/test/data/users_guide/ex_script.result +0 -7
- data/test/data/users_guide/ex_source.result +0 -8
- data/test/data/users_guide/example1.rbhtml +0 -12
- data/test/data/users_guide/example1.result +0 -17
- data/test/data/users_guide/example10.rbhtml +0 -4
- data/test/data/users_guide/example10_template_args.result +0 -6
- data/test/data/users_guide/example11.rbhtml +0 -5
- data/test/data/users_guide/example11_template_args_result +0 -2
- data/test/data/users_guide/example12_preprocessed.result +0 -10
- data/test/data/users_guide/example12_preprocessed_source.result +0 -10
- data/test/data/users_guide/example13.rbhtml +0 -6
- data/test/data/users_guide/example13_preprocessed.result +0 -2
- data/test/data/users_guide/example13_preprocessed_source.result +0 -2
- data/test/data/users_guide/example14.rb +0 -32
- data/test/data/users_guide/example14.rbhtml +0 -6
- data/test/data/users_guide/example14_tmplclass.result +0 -15
- data/test/data/users_guide/example15.rb +0 -10
- data/test/data/users_guide/example15_escapefunc.result +0 -14
- data/test/data/users_guide/example16.rbhtml +0 -4
- data/test/data/users_guide/example16a.rb +0 -10
- data/test/data/users_guide/example16a.result +0 -4
- data/test/data/users_guide/example16b.rb +0 -13
- data/test/data/users_guide/example16b.result +0 -4
- data/test/data/users_guide/example16c.rb +0 -12
- data/test/data/users_guide/example16c.result +0 -4
- data/test/data/users_guide/example16d.rb +0 -27
- data/test/data/users_guide/example16d.result +0 -4
- data/test/data/users_guide/example1_S.result +0 -14
- data/test/data/users_guide/example1_SXNC.result +0 -10
- data/test/data/users_guide/example1_source.result +0 -14
- data/test/data/users_guide/example2.rbhtml +0 -3
- data/test/data/users_guide/example2_sb.result2 +0 -9
- data/test/data/users_guide/example3_syntaxcheck.result +0 -2
- data/test/data/users_guide/example4.rbhtml +0 -13
- data/test/data/users_guide/example4_datafile_rb.result +0 -13
- data/test/data/users_guide/example4_yaml.result +0 -13
- data/test/data/users_guide/example5.rbhtml +0 -9
- data/test/data/users_guide/example5_datastr_rb.result +0 -9
- data/test/data/users_guide/example5_datastr_yaml.result +0 -9
- data/test/data/users_guide/example6.rbhtml +0 -19
- data/test/data/users_guide/example6_layout.result +0 -29
- data/test/data/users_guide/example6_nested.result +0 -28
- data/test/data/users_guide/example7_layout2.result +0 -13
- data/test/data/users_guide/example8_layout3.result +0 -8
- data/test/data/users_guide/example9.rbhtml +0 -18
- data/test/data/users_guide/example9_capture.result +0 -26
- data/test/data/users_guide/footer.html +0 -5
- data/test/data/users_guide/footer.rbhtml +0 -4
- data/test/data/users_guide/layout6.rbhtml +0 -17
- data/test/data/users_guide/layout7.rbhtml +0 -9
- data/test/data/users_guide/layout8_xhtml.rbhtml +0 -6
- data/test/data/users_guide/layout9.rbhtml +0 -25
- data/test/data/users_guide/sidemenu.rbhtml +0 -5
- data/test/data/users_guide/user_app.cgi +0 -39
- data/test/data/users_guide/user_app.result +0 -30
- data/test/data/users_guide/user_create.rbhtml +0 -6
- data/test/data/users_guide/user_edit.rbhtml +0 -7
- data/test/data/users_guide/user_form.rbhtml +0 -10
- data/test/data/users_guide/user_layout.rbhtml +0 -16
data/lib/tenjin.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
##
|
2
|
-
## copyright(c) 2007-
|
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
|
25
|
+
## Tenjin -- a very fast and full-featured template engine
|
26
26
|
##
|
27
|
-
## $
|
28
|
-
##
|
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.
|
34
|
+
RELEASE = ('$Release: 0.7.0 $' =~ /[\d.]+/) && $&
|
34
35
|
|
35
36
|
|
36
37
|
##
|
37
|
-
##
|
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 = { '&'=>'&', '<'=>'<', '>'=>'>', '"'=>'"', "'"=>''' }
|
44
60
|
|
61
|
+
## escapes '&', '<', '>', and '"'
|
45
62
|
def escape_xml(s)
|
46
|
-
#
|
47
|
-
|
48
|
-
##
|
49
|
-
#s = s.gsub(/&/, '&')
|
50
|
-
#s.gsub!(/</, '<')
|
51
|
-
#s.gsub!(/>/, '>')
|
52
|
-
#s.gsub!(/"/, '"')
|
53
|
-
#return s
|
54
|
-
##
|
55
|
-
#return s.gsub(/&/, '&').gsub(/</, '<').gsub(/>/, '>').gsub(/"/, '"')
|
63
|
+
#s.gsub(/[&<>"]/) { XML_ESCAPE_TABLE[$&] }
|
64
|
+
s.gsub(/[&<>"]/) {|s| XML_ESCAPE_TABLE[s] }
|
56
65
|
end
|
57
66
|
|
58
|
-
|
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}=\"#{
|
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 " "
|
93
155
|
def text2html(text)
|
94
|
-
return nl2br(
|
156
|
+
return nl2br(safe_escape(text.to_s).gsub(/ /, ' '))
|
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 ?
|
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
|
-
|
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
|
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
|
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
|
-
|
441
|
-
code
|
442
|
-
if lspace
|
443
|
-
|
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 <<
|
450
|
-
|
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};
|
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
|
-
|
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
|
-
@
|
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
|
-
|
841
|
-
|
842
|
-
|
843
|
-
if
|
844
|
-
|
845
|
-
|
846
|
-
|
847
|
-
|
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
|
-
|
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
|
-
|
855
|
-
|
856
|
-
return
|
857
|
-
|
858
|
-
if
|
859
|
-
|
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
|
-
|
862
|
-
return
|
863
|
-
|
864
|
-
|
865
|
-
|
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
|
-
|
872
|
-
return (filename + '.cache').untaint
|
873
|
-
end
|
1461
|
+
public
|
874
1462
|
|
875
|
-
|
876
|
-
|
877
|
-
|
878
|
-
template
|
879
|
-
|
880
|
-
|
881
|
-
if
|
882
|
-
|
883
|
-
|
884
|
-
|
885
|
-
|
886
|
-
|
887
|
-
|
888
|
-
|
889
|
-
|
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
|
-
|
898
|
-
|
899
|
-
|
900
|
-
|
901
|
-
|
902
|
-
|
903
|
-
|
904
|
-
|
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
|
-
|
909
|
-
|
910
|
-
|
911
|
-
|
912
|
-
|
913
|
-
|
914
|
-
|
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
|
-
|
918
|
-
|
919
|
-
|
920
|
-
|
921
|
-
|
922
|
-
|
923
|
-
|
924
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
956
|
-
|
957
|
-
|
958
|
-
|
959
|
-
|
960
|
-
|
961
|
-
|
962
|
-
|
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
|