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 +5 -0
- data/.gitignore +21 -0
- data/HISTORY.txt +6 -0
- data/LICENSE +20 -0
- data/README.rdoc +78 -0
- data/Rakefile +80 -0
- data/VERSION +1 -0
- data/clamsy.gemspec +70 -0
- data/lib/clamsy.rb +130 -0
- data/lib/clamsy/tenjin.rb +978 -0
- data/spec/clamsy_spec.rb +54 -0
- data/spec/data/embedded_ruby_example.odt +0 -0
- data/spec/data/embedded_ruby_example.pdf +0 -0
- data/spec/data/escaped_text_example.odt +0 -0
- data/spec/data/escaped_text_example.pdf +0 -0
- data/spec/data/multiple_contexts_example.odt +0 -0
- data/spec/data/multiple_contexts_example.pdf +0 -0
- data/spec/data/plain_text_example.odt +0 -0
- data/spec/data/plain_text_example.pdf +0 -0
- data/spec/spec_helper.rb +34 -0
- metadata +124 -0
data/.document
ADDED
data/.gitignore
ADDED
data/HISTORY.txt
ADDED
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 = { '&'=>'&', '<'=>'<', '>'=>'>', '"'=>'"', "'"=>''' }
|
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(/&/, '&')
|
57
|
+
#s.gsub!(/</, '<')
|
58
|
+
#s.gsub!(/>/, '>')
|
59
|
+
#s.gsub!(/"/, '"')
|
60
|
+
#return s
|
61
|
+
##
|
62
|
+
#return s.gsub(/&/, '&').gsub(/</, '<').gsub(/>/, '>').gsub(/"/, '"')
|
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 " "
|
100
|
+
def text2html(text)
|
101
|
+
return nl2br(escape_xml(text.to_s).gsub(/ /, ' '))
|
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!(/<`\#(.*?)\#`>/m) { "\#\{#{CGI::unescapeHTML($1)}\}" }
|
227
|
+
s.gsub!(/<`\$(.*?)\$`>/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
|