clamsy 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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