clamsy 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,21 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+
21
+ ## PROJECT::SPECIFIC
data/HISTORY.txt ADDED
@@ -0,0 +1,6 @@
1
+ === 0.0.1 (Apr 21, 2010)
2
+
3
+ = 1st official (yet embarrassing) release
4
+
5
+ * support to generate a single pdf from multiple contexts using a single odt template [#ngty]
6
+
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 NgTzeYang
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,78 @@
1
+ = clamsy
2
+
3
+ Ruby wrapper for generating a single pdf for multiple contexts from an odt template.
4
+
5
+ clamsy = clumsy + shellish
6
+ | |
7
+ | |-- under the hood, we are making system calls like ooffice,
8
+ | ps2pdf & pdf2ps, etc.
9
+ |
10
+ .-- setup isn't straight forward, need to install a couple of packages
11
+ (how bad it is depends on ur platform) & even manual setting up of
12
+ cups printer is needed
13
+
14
+ == Using It
15
+
16
+ #1. Generating single context pdf:
17
+
18
+ Clamsy.process(
19
+ {:someone => 'Peter', :mood => 'Happy'},
20
+ path_to_template_odt,
21
+ path_to_final_pdf
22
+ )
23
+
24
+ #2. Generating multi-contexts pdf:
25
+
26
+ Clamsy.process(
27
+ [
28
+ {:someone => 'Peter', :mood => 'Happy'},
29
+ {:someone => 'Jane', :mood => 'Sad'}
30
+ ],
31
+ path_to_template_odt,
32
+ path_to_final_pdf
33
+ )
34
+
35
+ == Pre-requisites
36
+
37
+ #1. Archlinux
38
+
39
+ * Installing packages:
40
+ $ sudo pacman -S ghostscript cups cups-pdf go-openoffice
41
+
42
+ * Setting up the cups-pdf virtual printer by following instructions @
43
+ http://wiki.archlinux.org/index.php/CUPS#Configuring_CUPS-PDF_virtual_printer
44
+
45
+ * Making sure cups is running:
46
+ $ sudo /etc/rc.d/cups start
47
+
48
+ #2. Ubuntu (to-be-updated)
49
+
50
+ #3. Mac (to-be-updated)
51
+
52
+ == TODO
53
+
54
+ * add support for configuration, eg. Clamsy.configure {|config| ... }
55
+
56
+ * implement strategy to check for missing commands & prompt user to install packages
57
+
58
+ * instead of using system to call the various commands, maybe we write extensions
59
+ for these foreign function calls (eg. using ffi ??)
60
+
61
+ == Note on Patches/Pull Requests
62
+
63
+ * Fork the project.
64
+ * Make your feature addition or bug fix.
65
+ * Add tests for it. This is important so I don't break it in a
66
+ future version unintentionally.
67
+ * Commit, do not mess with rakefile, version, or history. (if you want to have your own
68
+ version, that is fine but bump version in a commit by itself I can ignore when I pull)
69
+ * Send me a pull request. Bonus points for topic branches.
70
+
71
+ == Contacts
72
+
73
+ Written 2010 by:
74
+
75
+ #1. NgTzeYang, contact ngty77[at]gmail.com or http://github.com/ngty
76
+
77
+ Released under the MIT license
78
+
data/Rakefile ADDED
@@ -0,0 +1,80 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "clamsy"
8
+ gem.summary = %Q{A clumsily shellish way to generate a single pdf for multiple contexts from an odt template}
9
+ gem.description = %Q{}
10
+ gem.email = "ngty77@gmail.com"
11
+ gem.homepage = "http://github.com/ngty/clamsy"
12
+ gem.authors = ["NgTzeYang"]
13
+ gem.add_dependency "rubyzip", "= 0.9.4"
14
+ gem.add_development_dependency "bacon", ">= 1.1.0"
15
+ gem.add_development_dependency "differ", ">= 0.1.1"
16
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
17
+ end
18
+ Jeweler::GemcutterTasks.new
19
+ rescue LoadError
20
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
21
+ end
22
+
23
+ require 'rake/testtask'
24
+ Rake::TestTask.new(:spec) do |spec|
25
+ spec.libs << 'lib' << 'spec'
26
+ spec.pattern = 'spec/**/*_spec.rb'
27
+ spec.verbose = true
28
+ end
29
+
30
+ begin
31
+ require 'rcov/rcovtask'
32
+ Rcov::RcovTask.new do |spec|
33
+ spec.libs << 'spec'
34
+ spec.pattern = 'spec/**/*_spec.rb'
35
+ spec.verbose = true
36
+ end
37
+ rescue LoadError
38
+ task :rcov do
39
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
40
+ end
41
+ end
42
+
43
+ task :spec => :check_dependencies
44
+
45
+ begin
46
+ require 'reek/adapters/rake_task'
47
+ Reek::RakeTask.new do |t|
48
+ t.fail_on_error = true
49
+ t.verbose = false
50
+ t.source_files = 'lib/**/*.rb'
51
+ end
52
+ rescue LoadError
53
+ task :reek do
54
+ abort "Reek is not available. In order to run reek, you must: sudo gem install reek"
55
+ end
56
+ end
57
+
58
+ begin
59
+ require 'roodi'
60
+ require 'roodi_task'
61
+ RoodiTask.new do |t|
62
+ t.verbose = false
63
+ end
64
+ rescue LoadError
65
+ task :roodi do
66
+ abort "Roodi is not available. In order to run roodi, you must: sudo gem install roodi"
67
+ end
68
+ end
69
+
70
+ task :default => :spec
71
+
72
+ require 'rake/rdoctask'
73
+ Rake::RDocTask.new do |rdoc|
74
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
75
+
76
+ rdoc.rdoc_dir = 'rdoc'
77
+ rdoc.title = "clamsy #{version}"
78
+ rdoc.rdoc_files.include('README*')
79
+ rdoc.rdoc_files.include('lib/**/*.rb')
80
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.1
data/clamsy.gemspec ADDED
@@ -0,0 +1,70 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{clamsy}
8
+ s.version = "0.0.1"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["NgTzeYang"]
12
+ s.date = %q{2010-04-21}
13
+ s.description = %q{}
14
+ s.email = %q{ngty77@gmail.com}
15
+ s.extra_rdoc_files = [
16
+ "LICENSE",
17
+ "README.rdoc"
18
+ ]
19
+ s.files = [
20
+ ".document",
21
+ ".gitignore",
22
+ "HISTORY.txt",
23
+ "LICENSE",
24
+ "README.rdoc",
25
+ "Rakefile",
26
+ "VERSION",
27
+ "clamsy.gemspec",
28
+ "lib/clamsy.rb",
29
+ "lib/clamsy/tenjin.rb",
30
+ "spec/clamsy_spec.rb",
31
+ "spec/data/embedded_ruby_example.odt",
32
+ "spec/data/embedded_ruby_example.pdf",
33
+ "spec/data/escaped_text_example.odt",
34
+ "spec/data/escaped_text_example.pdf",
35
+ "spec/data/multiple_contexts_example.odt",
36
+ "spec/data/multiple_contexts_example.pdf",
37
+ "spec/data/plain_text_example.odt",
38
+ "spec/data/plain_text_example.pdf",
39
+ "spec/spec_helper.rb"
40
+ ]
41
+ s.homepage = %q{http://github.com/ngty/clamsy}
42
+ s.rdoc_options = ["--charset=UTF-8"]
43
+ s.require_paths = ["lib"]
44
+ s.rubygems_version = %q{1.3.6}
45
+ s.summary = %q{A clumsily shellish way to generate a single pdf for multiple contexts from an odt template}
46
+ s.test_files = [
47
+ "spec/clamsy_spec.rb",
48
+ "spec/spec_helper.rb"
49
+ ]
50
+
51
+ if s.respond_to? :specification_version then
52
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
53
+ s.specification_version = 3
54
+
55
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
56
+ s.add_runtime_dependency(%q<rubyzip>, ["= 0.9.4"])
57
+ s.add_development_dependency(%q<bacon>, [">= 1.1.0"])
58
+ s.add_development_dependency(%q<differ>, [">= 0.1.1"])
59
+ else
60
+ s.add_dependency(%q<rubyzip>, ["= 0.9.4"])
61
+ s.add_dependency(%q<bacon>, [">= 1.1.0"])
62
+ s.add_dependency(%q<differ>, [">= 0.1.1"])
63
+ end
64
+ else
65
+ s.add_dependency(%q<rubyzip>, ["= 0.9.4"])
66
+ s.add_dependency(%q<bacon>, [">= 1.1.0"])
67
+ s.add_dependency(%q<differ>, [">= 0.1.1"])
68
+ end
69
+ end
70
+
data/lib/clamsy.rb ADDED
@@ -0,0 +1,130 @@
1
+ require 'ftools'
2
+ require 'digest/md5'
3
+ require 'tempfile'
4
+ require 'zip/zip'
5
+ require 'clamsy/tenjin'
6
+
7
+ module Clamsy
8
+
9
+ class << self
10
+
11
+ def process(contexts, template_odt, final_pdf)
12
+ begin
13
+ @template_odt = TemplateOdt.new(template_odt)
14
+ odts = [contexts].flatten.map {|ctx| @template_odt.render(ctx) }
15
+ Shell.print_odts_to_pdf(odts, final_pdf)
16
+ ensure
17
+ @template_odt.trash_tmp_files
18
+ end
19
+ end
20
+
21
+ end
22
+
23
+ private
24
+
25
+ module HasTrashableTempFiles
26
+
27
+ def trash_tmp_files
28
+ (@trashable_tmp_files || []).select {|f| f.path }.map(&:unlink)
29
+ end
30
+
31
+ def get_tmp_file(file_name)
32
+ ((@trashable_tmp_files ||= []) << Tempfile.new(file_name))[-1]
33
+ end
34
+
35
+ end
36
+
37
+ class TemplateOdt
38
+
39
+ include HasTrashableTempFiles
40
+
41
+ def initialize(template_odt)
42
+ @template_odt = template_odt
43
+ end
44
+
45
+ def render(context)
46
+ @context_id = Digest::MD5.hexdigest(context.to_s)
47
+ Zip::ZipFile.open(working_odt.path) do |zip|
48
+ zip.select {|entry| entry.file? && entry.to_s =~ /\.xml$/ }.each do |entry|
49
+ zip.get_output_stream(entry.to_s) {|io| io.write(workers[entry].render(context)) }
50
+ end
51
+ end
52
+ working_odt
53
+ end
54
+
55
+ private
56
+
57
+ def working_odt
58
+ (@working_odts ||= {})[@context_id] ||=
59
+ begin
60
+ dest_odt = get_tmp_file(@context_id)
61
+ File.copy(@template_odt, dest_odt.path) ; dest_odt
62
+ end
63
+ end
64
+
65
+ def workers
66
+ lambda do |entry|
67
+ (@workers ||= {})[entry.to_s] ||=
68
+ begin
69
+ tmp_file = get_tmp_file("#{@context_id}.#{File.basename(entry.to_s)}")
70
+ tmp_file.write(entry.get_input_stream.read)
71
+ tmp_file.close
72
+ Tenjin::Template.new(tmp_file.path)
73
+ end
74
+ end
75
+ end
76
+
77
+ end
78
+
79
+ class Shell
80
+
81
+ # The folder where cups-pdf generated pdfs are stored:
82
+ # * in archlinux, this is specified in /etc/cups/cups-pdf.conf
83
+ PDF_OUTPUT_DIR = "/tmp/cups-pdf/#{`whoami`.strip}"
84
+
85
+ # The openoffice command to print odt to pdf, requires package cups-pdf & 'Cups-PDF' printer
86
+ # to be set up in cups.
87
+ ODT_TO_PDF_CMD = "ooffice -norestore -nofirststartwizard -nologo -headless -pt Cups-PDF"
88
+
89
+ # PDF to PS & vice versa
90
+ PDF_TO_PS_CMD = "pdf2ps"
91
+ PS_TO_PDF_CMD = "ps2pdf"
92
+
93
+ # Misc commands
94
+ CAT_CMD = 'cat'
95
+
96
+ class << self
97
+
98
+ include HasTrashableTempFiles
99
+
100
+ def print_odts_to_pdf(from_odts, to_pdf)
101
+ begin
102
+ tmp_ps = get_tmp_file(File.basename(to_pdf, '.pdf')).path
103
+ system([
104
+ "#{CAT_CMD} #{convert_odts_to_pss(from_odts).join(' ')} > #{tmp_ps}",
105
+ "#{PS_TO_PDF_CMD} #{tmp_ps} #{to_pdf}"
106
+ ].join(' && '))
107
+ ensure
108
+ trash_tmp_files
109
+ end
110
+ end
111
+
112
+ private
113
+
114
+ def convert_odts_to_pss(odts)
115
+ odts.map(&:path).map do |odt_file|
116
+ ps_file = get_tmp_file(basename = File.basename(odt_file, '.odt')).path
117
+ pdf_file = File.join(PDF_OUTPUT_DIR, basename) + '.pdf'
118
+ system("#{ODT_TO_PDF_CMD} #{odt_file}")
119
+ # Abit clumsy ... but prevents occasional error for subsequent PDF_TO_PS_CMD
120
+ 0.upto(10) {|_| File.exists?(pdf_file) ? break : sleep(1) }
121
+ system("#{PDF_TO_PS_CMD} #{pdf_file} #{ps_file}")
122
+ ps_file
123
+ end
124
+ end
125
+
126
+ end
127
+
128
+ end
129
+
130
+ end
@@ -0,0 +1,978 @@
1
+ module Clamsy
2
+
3
+ # NOTE: This is a slightly hacked version of the awesome tenjin template engine
4
+ # (http://github.com/kwatch/tenjin) to support the following:
5
+ # * {? ... ?} instead of <?rb ... ?>
6
+ # * {?? ... ??} instead of <?RB ... ?>
7
+
8
+ ##
9
+ ## $Copyright$
10
+ ##
11
+ ## Permission is hereby granted, free of charge, to any person obtaining
12
+ ## a copy of this software and associated documentation files (the
13
+ ## "Software"), to deal in the Software without restriction, including
14
+ ## without limitation the rights to use, copy, modify, merge, publish,
15
+ ## distribute, sublicense, and/or sell copies of the Software, and to
16
+ ## permit persons to whom the Software is furnished to do so, subject to
17
+ ## the following conditions:
18
+ ##
19
+ ## The above copyright notice and this permission notice shall be
20
+ ## included in all copies or substantial portions of the Software.
21
+ ##
22
+ ## THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
23
+ ## EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
24
+ ## MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
25
+ ## NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
26
+ ## LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
27
+ ## OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
28
+ ## WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
29
+ ##
30
+
31
+ ##
32
+ ## Tenjin module
33
+ ##
34
+ ## $Rev$
35
+ ## $Release: 0.0.0 $
36
+ ##
37
+
38
+ module Tenjin
39
+
40
+ RELEASE = ('$Release: 0.0.0 $' =~ /[\d.]+/) && $&
41
+
42
+
43
+ ##
44
+ ## helper module for Context class
45
+ ##
46
+ module HtmlHelper
47
+
48
+ module_function
49
+
50
+ XML_ESCAPE_TABLE = { '&'=>'&amp;', '<'=>'&lt;', '>'=>'&gt;', '"'=>'&quot;', "'"=>'&#039;' }
51
+
52
+ def escape_xml(s)
53
+ #return s.gsub(/[&<>"]/) { XML_ESCAPE_TABLE[$&] }
54
+ return s.gsub(/[&<>"]/) { |s| XML_ESCAPE_TABLE[s] }
55
+ ##
56
+ #s = s.gsub(/&/, '&amp;')
57
+ #s.gsub!(/</, '&lt;')
58
+ #s.gsub!(/>/, '&gt;')
59
+ #s.gsub!(/"/, '&quot;')
60
+ #return s
61
+ ##
62
+ #return s.gsub(/&/, '&amp;').gsub(/</, '&lt;').gsub(/>/, '&gt;').gsub(/"/, '&quot;')
63
+ end
64
+
65
+ alias escape escape_xml
66
+
67
+ ## (experimental) return ' name="value"' if expr is not false nor nil.
68
+ ## if value is nil or false then expr is used as value.
69
+ def tagattr(name, expr, value=nil, escape=true)
70
+ if !expr
71
+ return ''
72
+ elsif escape
73
+ return " #{name}=\"#{escape_xml((value || expr).to_s)}\""
74
+ else
75
+ return " #{name}=\"#{value || expr}\""
76
+ end
77
+ end
78
+
79
+ ## return ' checked="checked"' if expr is not false or nil
80
+ def checked(expr)
81
+ return expr ? ' checked="checked"' : ''
82
+ end
83
+
84
+ ## return ' selected="selected"' if expr is not false or nil
85
+ def selected(expr)
86
+ return expr ? ' selected="selected"' : ''
87
+ end
88
+
89
+ ## return ' disabled="disabled"' if expr is not false or nil
90
+ def disabled(expr)
91
+ return expr ? ' disabled="disabled"' : ''
92
+ end
93
+
94
+ ## convert "\n" into "<br />\n"
95
+ def nl2br(text)
96
+ return text.to_s.gsub(/\n/, "<br />\n")
97
+ end
98
+
99
+ ## convert "\n" and " " into "<br />\n" and " &nbsp;"
100
+ def text2html(text)
101
+ return nl2br(escape_xml(text.to_s).gsub(/ /, ' &nbsp;'))
102
+ end
103
+
104
+ end
105
+
106
+
107
+ ##
108
+ ## helper module for BaseContext class
109
+ ##
110
+ module ContextHelper
111
+
112
+ attr_accessor :_buf, :_engine, :_layout
113
+
114
+ ## escape value. this method should be overrided in subclass.
115
+ def escape(val)
116
+ return val
117
+ end
118
+
119
+ ## include template. 'template_name' can be filename or short name.
120
+ def import(template_name, _append_to_buf=true)
121
+ _buf = self._buf
122
+ output = self._engine.render(template_name, context=self, layout=false)
123
+ _buf << output if _append_to_buf
124
+ return output
125
+ end
126
+
127
+ ## add value into _buf. this is equivarent to '#{value}'.
128
+ def echo(value)
129
+ self._buf << value
130
+ end
131
+
132
+ ##
133
+ ## start capturing.
134
+ ## returns captured string if block given, else return nil.
135
+ ## if block is not given, calling stop_capture() is required.
136
+ ##
137
+ ## ex. list.rbhtml
138
+ ## <html><body>
139
+ ## <h1>{? start_capture(:title) do ?}Document Title{? end ?}</h1>
140
+ ## {? start_capture(:content) ?}
141
+ ## <ul>
142
+ ## {? for item in list do ?}
143
+ ## <li>${item}</li>
144
+ ## {? end ?}
145
+ ## </ul>
146
+ ## {? stop_capture() ?}
147
+ ## </body></html>
148
+ ##
149
+ ## ex. layout.rbhtml
150
+ ## <?xml version="1.0" ?>
151
+ ## <html xml:lang="en">
152
+ ## <head>
153
+ ## <title>${@title}</title>
154
+ ## </head>
155
+ ## <body>
156
+ ## <h1>${@title}</h1>
157
+ ## <div id="content">
158
+ ## {? echo(@content) ?}
159
+ ## </div>
160
+ ## </body>
161
+ ## </html>
162
+ ##
163
+ def start_capture(varname=nil)
164
+ @_capture_varname = varname
165
+ @_start_position = self._buf.length
166
+ if block_given?
167
+ yield
168
+ output = stop_capture()
169
+ return output
170
+ else
171
+ return nil
172
+ end
173
+ end
174
+
175
+ ##
176
+ ## stop capturing.
177
+ ## returns captured string.
178
+ ## see start_capture()'s document.
179
+ ##
180
+ def stop_capture(store_to_context=true)
181
+ output = self._buf[@_start_position..-1]
182
+ self._buf[@_start_position..-1] = ''
183
+ @_start_position = nil
184
+ if @_capture_varname
185
+ self.instance_variable_set("@#{@_capture_varname}", output) if store_to_context
186
+ @_capture_varname = nil
187
+ end
188
+ return output
189
+ end
190
+
191
+ ##
192
+ ## if captured string is found then add it to _buf and return true,
193
+ ## else return false.
194
+ ## this is a helper method for layout template.
195
+ ##
196
+ def captured_as(name)
197
+ str = self.instance_variable_get("@#{name}")
198
+ return false unless str
199
+ @_buf << str
200
+ return true
201
+ end
202
+
203
+ ##
204
+ ## ex. _p("item['name']") => #{item['name']}
205
+ ##
206
+ def _p(arg)
207
+ return "<`\##{arg}\#`>" # decoded into #{...} by preprocessor
208
+ end
209
+
210
+ ##
211
+ ## ex. _P("item['name']") => ${item['name']}
212
+ ##
213
+ def _P(arg)
214
+ return "<`$#{arg}$`>" # decoded into ${...} by preprocessor
215
+ end
216
+
217
+ ##
218
+ ## decode <`#...#`> and <`$...$`> into #{...} and ${...}
219
+ ##
220
+ def _decode_params(s)
221
+ require 'cgi'
222
+ return s unless s.is_a?(String)
223
+ s = s.dup
224
+ s.gsub!(/%3C%60%23(.*?)%23%60%3E/im) { "\#\{#{CGI::unescape($1)}\}" }
225
+ s.gsub!(/%3C%60%24(.*?)%24%60%3E/im) { "\$\{#{CGI::unescape($1)}\}" }
226
+ s.gsub!(/&lt;`\#(.*?)\#`&gt;/m) { "\#\{#{CGI::unescapeHTML($1)}\}" }
227
+ s.gsub!(/&lt;`\$(.*?)\$`&gt;/m) { "\$\{#{CGI::unescapeHTML($1)}\}" }
228
+ s.gsub!(/<`\#(.*?)\#`>/m, '#{\1}')
229
+ s.gsub!(/<`\$(.*?)\$`>/m, '${\1}')
230
+ return s
231
+ end
232
+
233
+ end
234
+
235
+
236
+ ##
237
+ ## base class for Context class
238
+ ##
239
+ class BaseContext
240
+ include Enumerable
241
+ include ContextHelper
242
+
243
+ def initialize(vars=nil)
244
+ update(vars) if vars.is_a?(Hash)
245
+ end
246
+
247
+ def [](key)
248
+ instance_variable_get("@#{key}")
249
+ end
250
+
251
+ def []=(key, val)
252
+ instance_variable_set("@#{key}", val)
253
+ end
254
+
255
+ def update(hash)
256
+ hash.each do |key, val|
257
+ self[key] = val
258
+ end
259
+ end
260
+
261
+ def key?(key)
262
+ return self.instance_variables.include?("@#{key}")
263
+ end
264
+ if Object.respond_to?('instance_variable_defined?')
265
+ def key?(key)
266
+ return self.instance_variable_defined?("@#{key}")
267
+ end
268
+ end
269
+
270
+ alias has_key? key?
271
+
272
+ def each()
273
+ instance_variables().each do |name|
274
+ if name != '@_buf' && name != '@_engine'
275
+ val = instance_variable_get(name)
276
+ key = name[1..-1]
277
+ yield([key, val])
278
+ end
279
+ end
280
+ end
281
+
282
+ end
283
+
284
+
285
+ ##
286
+ ## context class for Template
287
+ ##
288
+ class Context < BaseContext
289
+ include HtmlHelper
290
+ end
291
+
292
+
293
+ ##
294
+ ## template class
295
+ ##
296
+ ## ex. file 'example.rbhtml'
297
+ ## <html>
298
+ ## <body>
299
+ ## <h1>${@title}</h1>
300
+ ## <ul>
301
+ ## {? i = 0 ?}
302
+ ## {? for item in @items ?}
303
+ ## {? i += 1 ?}
304
+ ## <li>#{i} : ${item}</li>
305
+ ## {? end ?}
306
+ ## </ul>
307
+ ## </body>
308
+ ## </html>
309
+ ##
310
+ ## ex. convertion
311
+ ## require 'tenjin'
312
+ ## template = Tenjin::Template.new('example.rbhtml')
313
+ ## print template.script
314
+ ## ## or
315
+ ## # template = Tenjin::Template.new()
316
+ ## # print template.convert_file('example.rbhtml')
317
+ ## ## or
318
+ ## # template = Tenjin::Template.new()
319
+ ## # fname = 'example.rbhtml'
320
+ ## # print template.convert(File.read(fname), fname) # filename is optional
321
+ ##
322
+ ## ex. evaluation
323
+ ## context = {:title=>'Tenjin Example', :items=>['foo', 'bar', 'baz'] }
324
+ ## output = template.render(context)
325
+ ## ## or
326
+ ## # context = Tenjin::Context(:title=>'Tenjin Example', :items=>['foo','bar','baz'])
327
+ ## # output = template.render(context)
328
+ ## ## or
329
+ ## # output = template.render(:title=>'Tenjin Example', :items=>['foo','bar','baz'])
330
+ ## print output
331
+ ##
332
+ class Template
333
+
334
+ ESCAPE_FUNCTION = 'escape' # or 'Eruby::Helper.escape'
335
+
336
+ ##
337
+ ## initializer of Template class.
338
+ ##
339
+ ## options:
340
+ ## :escapefunc :: function name to escape value (default 'escape')
341
+ ## :preamble :: preamble such as "_buf = ''" (default nil)
342
+ ## :postamble :: postamble such as "_buf.to_s" (default nil)
343
+ ##
344
+ def initialize(filename=nil, options={})
345
+ if filename.is_a?(Hash)
346
+ options = filename
347
+ filename = nil
348
+ end
349
+ @filename = filename
350
+ @escapefunc = options[:escapefunc] || ESCAPE_FUNCTION
351
+ @preamble = options[:preamble] == true ? "_buf = #{init_buf_expr()}; " : options[:preamble]
352
+ @postamble = options[:postamble] == true ? "_buf.to_s" : options[:postamble]
353
+ @args = nil # or array of argument names
354
+ convert_file(filename) if filename
355
+ end
356
+ attr_accessor :filename, :escapefunc, :initbuf, :newline
357
+ attr_accessor :timestamp, :args
358
+ attr_accessor :script #,:bytecode
359
+
360
+ ## convert file into ruby code
361
+ def convert_file(filename)
362
+ return convert(File.read(filename), filename)
363
+ end
364
+
365
+ ## convert string into ruby code
366
+ def convert(input, filename=nil)
367
+ @input = input
368
+ @filename = filename
369
+ @proc = nil
370
+ pos = input.index(?\n)
371
+ if pos && input[pos-1] == ?\r
372
+ @newline = "\r\n"
373
+ @newlinestr = '\\r\\n'
374
+ else
375
+ @newline = "\n"
376
+ @newlinestr = '\\n'
377
+ end
378
+ before_convert()
379
+ parse_stmts(input)
380
+ after_convert()
381
+ return @script
382
+ end
383
+
384
+ protected
385
+
386
+ ## hook method called before convert()
387
+ def before_convert()
388
+ @script = ''
389
+ @script << @preamble if @preamble
390
+ end
391
+
392
+ ## hook method called after convert()
393
+ def after_convert()
394
+ @script << @newline unless @script[-1] == ?\n
395
+ @script << @postamble << @newline if @postamble
396
+ end
397
+
398
+ def self.compile_stmt_pattern(pi)
399
+ return /\{\?#{pi}( |\t|\r?\n)(.*?) ?#{pi}\?\}([ \t]*\r?\n)?/m
400
+ end
401
+
402
+ STMT_PATTERN = self.compile_stmt_pattern('')
403
+
404
+ def stmt_pattern
405
+ STMT_PATTERN
406
+ end
407
+
408
+ ## parse statements ('{? ... ?}')
409
+ def parse_stmts(input)
410
+ return unless input
411
+ is_bol = true
412
+ prev_rspace = nil
413
+ pos = 0
414
+ input.scan(stmt_pattern()) do |mspace, code, rspace|
415
+ m = Regexp.last_match
416
+ text = input[pos, m.begin(0) - pos]
417
+ pos = m.end(0)
418
+ ## detect spaces at beginning of line
419
+ lspace = nil
420
+ if rspace.nil?
421
+ # nothing
422
+ elsif text.empty?
423
+ lspace = "" if is_bol
424
+ elsif text[-1] == ?\n
425
+ lspace = ""
426
+ else
427
+ rindex = text.rindex(?\n)
428
+ if rindex
429
+ s = text[rindex+1..-1]
430
+ if s =~ /\A[ \t]*\z/
431
+ lspace = s
432
+ text = text[0..rindex]
433
+ #text[rindex+1..-1] = ''
434
+ end
435
+ else
436
+ if is_bol && text =~ /\A[ \t]*\z/
437
+ lspace = text
438
+ text = nil
439
+ #lspace = text.dup
440
+ #text[0..-1] = ''
441
+ end
442
+ end
443
+ end
444
+ is_bol = rspace ? true : false
445
+ ##
446
+ text.insert(0, prev_rspace) if prev_rspace
447
+ parse_exprs(text)
448
+ code.insert(0, mspace) if mspace != ' '
449
+ if lspace
450
+ assert if rspace.nil?
451
+ code.insert(0, lspace)
452
+ code << rspace
453
+ #add_stmt(code)
454
+ prev_rspace = nil
455
+ else
456
+ code << ';' unless code[-1] == ?\n
457
+ #add_stmt(code)
458
+ prev_rspace = rspace
459
+ end
460
+ if code
461
+ code = statement_hook(code)
462
+ add_stmt(code)
463
+ end
464
+ end
465
+ #rest = $' || input
466
+ rest = pos > 0 ? input[pos..-1] : input
467
+ rest.insert(0, prev_rspace) if prev_rspace
468
+ parse_exprs(rest) if rest && !rest.empty?
469
+ end
470
+
471
+ def expr_pattern
472
+ #return /([\#$])\{(.*?)\}/
473
+ return /(\$)\{(.*?)\}/m
474
+ #return /\$\{.*?\}/
475
+ end
476
+
477
+ ## ex. get_expr_and_escapeflag('$', 'item[:name]') => 'item[:name]', true
478
+ def get_expr_and_escapeflag(matched)
479
+ return matched[2], matched[1] == '$'
480
+ end
481
+
482
+ ## parse expressions ('#{...}' and '${...}')
483
+ def parse_exprs(input)
484
+ return if !input or input.empty?
485
+ pos = 0
486
+ start_text_part()
487
+ input.scan(expr_pattern()) do
488
+ m = Regexp.last_match
489
+ text = input[pos, m.begin(0) - pos]
490
+ pos = m.end(0)
491
+ expr, flag_escape = get_expr_and_escapeflag(m)
492
+ #m = Regexp.last_match
493
+ #start = m.begin(0)
494
+ #stop = m.end(0)
495
+ #text = input[pos, start - pos]
496
+ #expr = input[start+2, stop-start-3]
497
+ #pos = stop
498
+ add_text(text)
499
+ add_expr(expr, flag_escape)
500
+ end
501
+ rest = $' || input
502
+ #if !rest || rest.empty?
503
+ # @script << '`; '
504
+ #elsif rest[-1] == ?\n
505
+ # rest.chomp!
506
+ # @script << escape_str(rest) << @newlinestr << '`' << @newline
507
+ #else
508
+ # @script << escape_str(rest) << '`; '
509
+ #end
510
+ flag_newline = input[-1] == ?\n
511
+ add_text(rest, true)
512
+ stop_text_part()
513
+ @script << (flag_newline ? @newline : '; ')
514
+ end
515
+
516
+ ## expand macros and parse '#@ARGS' in a statement.
517
+ def statement_hook(stmt)
518
+ ## macro expantion
519
+ #macro_pattern = /\A\s*(\w+)\((.*?)\);?(\s*)\z/
520
+ #if macro_pattern =~ stmt
521
+ # name = $1; arg = $2; rspace = $3
522
+ # handler = get_macro_handler(name)
523
+ # ret = handler ? handler.call(arg) + $3 : stmt
524
+ # return ret
525
+ #end
526
+ ## arguments declaration
527
+ if @args.nil?
528
+ args_pattern = /\A *\#@ARGS([ \t]+(.*?))?(\s*)\z/ #
529
+ if args_pattern =~ stmt
530
+ @args = []
531
+ declares = ''
532
+ rspace = $3
533
+ if $2
534
+ for s in $2.split(/,/)
535
+ arg = s.strip()
536
+ next if s.empty?
537
+ arg =~ /\A[a-zA-Z_]\w*\z/ or raise ArgumentError.new("#{arg}: invalid template argument.")
538
+ @args << arg
539
+ declares << " #{arg} = @#{arg};"
540
+ end
541
+ end
542
+ declares << rspace
543
+ return declares
544
+ end
545
+ end
546
+ ##
547
+ return stmt
548
+ end
549
+
550
+ #MACRO_HANDLER_TABLE = {
551
+ # "echo" => proc { |arg|
552
+ # " _buf << (#{arg});"
553
+ # },
554
+ # "import" => proc { |arg|
555
+ # " _buf << @_engine.render(#{arg}, self, false);"
556
+ # },
557
+ # "start_capture" => proc { |arg|
558
+ # " _buf_bkup = _buf; _buf = \"\"; _capture_varname = #{arg};"
559
+ # },
560
+ # "stop_capture" => proc { |arg|
561
+ # " self[_capture_varname] = _buf; _buf = _buf_bkup;"
562
+ # },
563
+ # "start_placeholder" => proc { |arg|
564
+ # " if self[#{arg}] then _buf << self[#{arg}] else;"
565
+ # },
566
+ # "stop_placeholder" => proc { |arg|
567
+ # " end;"
568
+ # },
569
+ #}
570
+ #
571
+ #def get_macro_handler(name)
572
+ # return MACRO_HANDLER_TABLE[name]
573
+ #end
574
+
575
+ ## start text part
576
+ def start_text_part()
577
+ @script << " _buf << %Q`"
578
+ end
579
+
580
+ ## stop text part
581
+ def stop_text_part()
582
+ @script << '`'
583
+ end
584
+
585
+ ## add text string
586
+ def add_text(text, encode_newline=false)
587
+ return unless text && !text.empty?
588
+ if encode_newline && text[-1] == ?\n
589
+ text.chomp!
590
+ @script << escape_str(text) << @newlinestr
591
+ else
592
+ @script << escape_str(text)
593
+ end
594
+ end
595
+
596
+ ## escape '\\' and '`' into '\\\\' and '\`'
597
+ def escape_str(str)
598
+ str.gsub!(/[`\\]/, '\\\\\&')
599
+ str.gsub!(/\r\n/, "\\r\r\n") if @newline == "\r\n"
600
+ return str
601
+ end
602
+
603
+ ## add expression code
604
+ def add_expr(code, flag_escape=nil)
605
+ return if !code || code.empty?
606
+ @script << (flag_escape ? "\#{#{@escapefunc}((#{code}).to_s)}" : "\#{#{code}}")
607
+ end
608
+
609
+ ## add statement code
610
+ def add_stmt(code)
611
+ @script << code
612
+ end
613
+
614
+ private
615
+
616
+ ## create proc object
617
+ def _render() # :nodoc:
618
+ return eval("proc { |_context| self._buf = _buf = #{init_buf_expr()}; #{@script}; _buf.to_s }".untaint, nil, @filename || '(tenjin)')
619
+ end
620
+
621
+ public
622
+
623
+ def init_buf_expr() # :nodoc:
624
+ return "''"
625
+ end
626
+
627
+ ## evaluate converted ruby code and return it.
628
+ ## argument '_context' should be a Hash object or Context object.
629
+ def render(_context=Context.new)
630
+ _context = Context.new(_context) if _context.is_a?(Hash)
631
+ @proc ||= _render()
632
+ return _context.instance_eval(&@proc)
633
+ end
634
+
635
+ end
636
+
637
+
638
+ ##
639
+ ## preprocessor class
640
+ ##
641
+ class Preprocessor < Template
642
+
643
+ protected
644
+
645
+ STMT_PATTERN = compile_stmt_pattern('\\?')
646
+
647
+ def stmt_pattern
648
+ return STMT_PATTERN
649
+ end
650
+
651
+ def expr_pattern
652
+ return /([\#$])\{\{(.*?)\}\}/m
653
+ end
654
+
655
+ #--
656
+ #def get_expr_and_escapeflag(matched)
657
+ # return matched[2], matched[1] == '$'
658
+ #end
659
+ #++
660
+
661
+ def escape_str(str)
662
+ str.gsub!(/[\\`\#]/, '\\\\\&')
663
+ str.gsub!(/\r\n/, "\\r\r\n") if @newline == "\r\n"
664
+ return str
665
+ end
666
+
667
+ def add_expr(code, flag_escape=nil)
668
+ return if !code || code.empty?
669
+ super("_decode_params((#{code}))", flag_escape)
670
+ end
671
+
672
+ end
673
+
674
+
675
+ ##
676
+ ## (experimental) fast template class which use Array buffer and Array#push()
677
+ ##
678
+ ## ex. ('foo.rb')
679
+ ## require 'tenjin'
680
+ ## engine = Tenjin::Engine.new(:templateclass=>Tenjin::ArrayBufferTemplate)
681
+ ## template = engine.get_template('foo.rbhtml')
682
+ ## puts template.script
683
+ ##
684
+ ## result:
685
+ ## $ cat foo.rbhtml
686
+ ## <ul>
687
+ ## {? for item in items ?}
688
+ ## <li>#{item}</li>
689
+ ## {? end ?}
690
+ ## </ul>
691
+ ## $ ruby foo.rb
692
+ ## _buf.push('<ul>
693
+ ## '); for item in items
694
+ ## _buf.push(' <li>', (item).to_s, '</li>
695
+ ## '); end
696
+ ## _buf.push('</ul>
697
+ ## ');
698
+ ##
699
+ class ArrayBufferTemplate < Template
700
+
701
+ protected
702
+
703
+ def expr_pattern
704
+ return /([\#$])\{(.*?)\}/
705
+ end
706
+
707
+ ## parse expressions ('#{...}' and '${...}')
708
+ def parse_exprs(input)
709
+ return if !input or input.empty?
710
+ pos = 0
711
+ items = []
712
+ input.scan(expr_pattern()) do
713
+ prefix, expr = $1, $2
714
+ m = Regexp.last_match
715
+ text = input[pos, m.begin(0) - pos]
716
+ pos = m.end(0)
717
+ items << quote_str(text) if text && !text.empty?
718
+ items << quote_expr(expr, prefix == '$') if expr && !expr.empty?
719
+ end
720
+ rest = $' || input
721
+ items << quote_str(rest) if rest && !rest.empty?
722
+ @script << " _buf.push(" << items.join(", ") << "); " unless items.empty?
723
+ end
724
+
725
+ def quote_str(text)
726
+ text.gsub!(/[\'\\]/, '\\\\\&')
727
+ return "'#{text}'"
728
+ end
729
+
730
+ def quote_expr(expr, flag_escape)
731
+ return flag_escape ? "#{@escapefunc}((#{expr}).to_s)" : "(#{expr}).to_s" # or "(#{expr})"
732
+ end
733
+
734
+ #--
735
+ #def get_macro_handler(name)
736
+ # if name == "start_capture"
737
+ # return proc { |arg|
738
+ # " _buf_bkup = _buf; _buf = []; _capture_varname = #{arg};"
739
+ # }
740
+ # elsif name == "stop_capture"
741
+ # return proc { |arg|
742
+ # " self[_capture_varname] = _buf.join; _buf = _buf_bkup;"
743
+ # }
744
+ # else
745
+ # return super
746
+ # end
747
+ #end
748
+ #++
749
+
750
+ public
751
+
752
+ def init_buf_expr() # :nodoc:
753
+ return "[]"
754
+ end
755
+
756
+ end
757
+
758
+
759
+ ##
760
+ ## template class to use eRuby template file (*.rhtml) instead of
761
+ ## Tenjin template file (*.rbhtml).
762
+ ## requires 'erubis' (http://www.kuwata-lab.com/erubis).
763
+ ##
764
+ ## ex.
765
+ ## require 'erubis'
766
+ ## require 'tenjin'
767
+ ## engine = Tenjin::Engine.new(:templateclass=>Tenjin::ErubisTemplate)
768
+ ##
769
+ class ErubisTemplate < Tenjin::Template
770
+
771
+ protected
772
+
773
+ def parse_stmts(input)
774
+ eruby = Erubis::Eruby.new(input, :preamble=>false, :postamble=>false)
775
+ @script << eruby.src
776
+ end
777
+
778
+ end
779
+
780
+
781
+ ##
782
+ ## engine class for templates
783
+ ##
784
+ ## Engine class supports the followings.
785
+ ## * template caching
786
+ ## * partial template
787
+ ## * layout template
788
+ ## * capturing (experimental)
789
+ ##
790
+ ## ex. file 'ex_list.rbhtml'
791
+ ## <ul>
792
+ ## {? for item in @items ?}
793
+ ## <li>#{item}</li>
794
+ ## {? end ?}
795
+ ## </ul>
796
+ ##
797
+ ## ex. file 'ex_layout.rbhtml'
798
+ ## <html>
799
+ ## <body>
800
+ ## <h1>${@title}</li>
801
+ ## #{@_content}
802
+ ## {? import 'footer.rbhtml' ?}
803
+ ## </body>
804
+ ## </html>
805
+ ##
806
+ ## ex. file 'main.rb'
807
+ ## require 'tenjin'
808
+ ## options = {:prefix=>'ex_', :postfix=>'.rbhtml', :layout=>'ex_layout.rbhtml'}
809
+ ## engine = Tenjin::Engine.new(options)
810
+ ## context = {:title=>'Tenjin Example', :items=>['foo', 'bar', 'baz']}
811
+ ## output = engine.render(:list, context) # or 'ex_list.rbhtml'
812
+ ## print output
813
+ ##
814
+ class Engine
815
+
816
+ ##
817
+ ## initializer of Engine class.
818
+ ##
819
+ ## options:
820
+ ## :prefix :: prefix string for template name (ex. 'template/')
821
+ ## :postfix :: postfix string for template name (ex. '.rbhtml')
822
+ ## :layout :: layout template name (default nil)
823
+ ## :path :: array of directory name (default nil)
824
+ ## :cache :: save converted ruby code into file or not (default true)
825
+ ## :path :: list of directory (default nil)
826
+ ## :preprocess :: flag to activate preprocessing (default nil)
827
+ ## :templateclass :: template class object (default Tenjin::Template)
828
+ ##
829
+ def initialize(options={})
830
+ @prefix = options[:prefix] || ''
831
+ @postfix = options[:postfix] || ''
832
+ @layout = options[:layout]
833
+ @cache = options.fetch(:cache, true)
834
+ @path = options[:path]
835
+ @preprocess = options.fetch(:preprocess, nil)
836
+ @templateclass = options.fetch(:templateclass, Template)
837
+ @init_opts_for_template = options
838
+ @templates = {} # filename->template
839
+ end
840
+
841
+ ## convert short name into filename (ex. ':list' => 'template/list.rb.html')
842
+ def to_filename(template_name)
843
+ name = template_name
844
+ return name.is_a?(Symbol) ? "#{@prefix}#{name}#{@postfix}" : name
845
+ end
846
+
847
+ ## find template filename
848
+ def find_template_file(template_name)
849
+ filename = to_filename(template_name)
850
+ if @path
851
+ for dir in @path
852
+ filepath = "#{dir}#{File::SEPARATOR}#{filename}"
853
+ return filepath if test(?f, filepath.untaint)
854
+ end
855
+ else
856
+ return filename if test(?f, filename.dup.untaint) # dup is required for frozen string
857
+ end
858
+ raise Errno::ENOENT.new("#{filename} (path=#{@path.inspect})")
859
+ end
860
+
861
+ ## read template file and preprocess it
862
+ def read_template_file(filename, _context)
863
+ return File.read(filename) if !@preprocess
864
+ _context ||= {}
865
+ _context = hook_context(_context) if _context.is_a?(Hash) || _context._engine.nil?
866
+ _buf = _context._buf
867
+ _context._buf = ""
868
+ begin
869
+ return Preprocessor.new(filename).render(_context)
870
+ ensure
871
+ _context._buf = _buf
872
+ end
873
+ end
874
+
875
+ ## register template object
876
+ def register_template(template_name, template)
877
+ #template.timestamp = Time.new unless template.timestamp
878
+ @templates[template_name] = template
879
+ end
880
+
881
+ def cachename(filename)
882
+ return (filename + '.cache').untaint
883
+ end
884
+
885
+ ## create template object from file
886
+ def create_template(filename, _context=nil)
887
+ template = @templateclass.new(nil, @init_opts_for_template)
888
+ template.timestamp = Time.now()
889
+ cache_filename = cachename(filename)
890
+ _context = hook_context(Context.new) if _context.nil?
891
+ if !@cache
892
+ input = read_template_file(filename, _context)
893
+ template.convert(input, filename)
894
+ elsif !test(?f, cache_filename) || File.mtime(cache_filename) < File.mtime(filename)
895
+ #$stderr.puts "*** debug: load original"
896
+ input = read_template_file(filename, _context)
897
+ template.convert(input, filename)
898
+ store_cachefile(cache_filename, template)
899
+ else
900
+ #$stderr.puts "*** debug: load cache"
901
+ template.filename = filename
902
+ load_cachefile(cache_filename, template)
903
+ end
904
+ return template
905
+ end
906
+
907
+ ## store template into cache file
908
+ def store_cachefile(cache_filename, template)
909
+ s = template.script
910
+ s = "\#@ARGS #{template.args.join(',')}\n#{s}" if template.args
911
+ tmp_filename = "#{cache_filename}.#{rand()}"
912
+ File.open(tmp_filename, 'w') {|f| f.write(s) }
913
+ File.rename(tmp_filename, cache_filename)
914
+ end
915
+
916
+ ## load template from cache file
917
+ def load_cachefile(cache_filename, template)
918
+ s = File.read(cache_filename)
919
+ if s.sub!(/\A\#\@ARGS (.*?)\r?\n/, '')
920
+ template.args = $1.split(',')
921
+ end
922
+ template.script = s
923
+ end
924
+
925
+ ## get template object
926
+ def get_template(template_name, _context=nil)
927
+ template = @templates[template_name]
928
+ t = template
929
+ unless t && t.timestamp && t.filename && t.timestamp >= File.mtime(t.filename)
930
+ filename = find_template_file(template_name)
931
+ template = create_template(filename, _context) # _context is passed only for preprocessor
932
+ register_template(template_name, template)
933
+ end
934
+ return template
935
+ end
936
+
937
+ ## get template object and evaluate it with context object.
938
+ ## if argument 'layout' is true then default layout file (specified at
939
+ ## initializer) is used as layout template, else if false then no layout
940
+ ## template is used.
941
+ ## if argument 'layout' is string, it is regarded as layout template name.
942
+ def render(template_name, context=Context.new, layout=true)
943
+ #context = Context.new(context) if context.is_a?(Hash)
944
+ context = hook_context(context)
945
+ while true
946
+ template = get_template(template_name, context) # context is passed only for preprocessor
947
+ _buf = context._buf
948
+ output = template.render(context)
949
+ context._buf = _buf
950
+ unless context._layout.nil?
951
+ layout = context._layout
952
+ context._layout = nil
953
+ end
954
+ layout = @layout if layout == true or layout.nil?
955
+ break unless layout
956
+ template_name = layout
957
+ layout = false
958
+ context.instance_variable_set('@_content', output)
959
+ end
960
+ return output
961
+ end
962
+
963
+ def hook_context(context)
964
+ if !context
965
+ context = Context.new
966
+ elsif context.is_a?(Hash)
967
+ context = Context.new(context)
968
+ end
969
+ context._engine = self
970
+ context._layout = nil
971
+ return context
972
+ end
973
+
974
+ end
975
+
976
+ end
977
+
978
+ end