##
## copyright(c) 2007-2011 kuwata-lab.com all rights reserved
##
## Permission is hereby granted, free of charge, to any person obtaining
## a copy of this software and associated documentation files (the
## "Software"), to deal in the Software without restriction, including
## without limitation the rights to use, copy, modify, merge, publish,
## distribute, sublicense, and/or sell copies of the Software, and to
## permit persons to whom the Software is furnished to do so, subject to
## the following conditions:
##
## The above copyright notice and this permission notice shall be
## included in all copies or substantial portions of the Software.
##
## THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
## EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
## MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
## NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
## LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
## OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
## WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
##
##
## Tenjin -- a very fast and full-featured template engine
##
## $Release: 0.7.1 $
## copyright(c) 2007-2011 kuwata-lab.com all rights reserved
## $License: MIT License $
##
module Tenjin
RELEASE = ('$Release: 0.7.1 $' =~ /[\d.]+/) && $&
##
## logger
##
@logger = nil
def self.logger
return @logger
end
def self.logger=(logger)
@logger = logger
end
##
## helper module for Context class.
## depends on SafeHelper.
##
module HtmlHelper
module_function
XML_ESCAPE_TABLE = { '&'=>'&', '<'=>'<', '>'=>'>', '"'=>'"', "'"=>''' }
## escapes '&', '<', '>', and '"'
def escape_xml(s)
#s.gsub(/[&<>"]/) { XML_ESCAPE_TABLE[$&] }
s.gsub(/[&<>"]/) {|s| XML_ESCAPE_TABLE[s] }
end
## escapes '&', '<', '>', '"', and '\''
def escape_html(s)
#s.gsub(/[&<>"']/) { XML_ESCAPE_TABLE[$&] }
s.gsub(/[&<>"']/) {|s| XML_ESCAPE_TABLE[s] }
end
if RUBY_VERSION >= "1.9"
def escape_xml(s)
s.gsub(/[&<>"]/, XML_ESCAPE_TABLE)
end
def escape_html(s)
s.gsub(/[&<>"']/, XML_ESCAPE_TABLE)
end
end
alias escape escape_html
alias h escape_html
end
##
## helper module for html tags.
## depends on HtmlHelper and SafeHelper.
##
module HtmlTagHelper
module_function
## (experimental) return ' name="value"' if expr is not false nor nil.
## if value is nil or false then expr is used as value.
def tagattr(name, expr, value=nil, escape=true)
if !expr
return ''
elsif escape
return safe_str(" #{name}=\"#{safe_escape((value || expr).to_s)}\"")
else
return safe_str(" #{name}=\"#{value || expr}\"")
end
end
## return ' checked="checked"' if expr is not false or nil
def checked(expr)
return expr ? safe_str(' checked="checked"') : ''
end
## return ' selected="selected"' if expr is not false or nil
def selected(expr)
return expr ? safe_str(' selected="selected"') : ''
end
## return ' disabled="disabled"' if expr is not false or nil
def disabled(expr)
return expr ? safe_str(' disabled="disabled"') : ''
end
## (experimental) return ' name="name" value="value"' string.
## if separator specified then id attribute is also added.
def nv(name, value, separator=nil)
nattr = safe_escape(name.to_s)
vattr = safe_escape(value.to_s)
s = separator \
? " name=\"#{nattr}\" value=\"#{vattr}\" id=\"#{nattr}#{separator}#{vattr}\"" \
: " name=\"#{nattr}\" value=\"#{vattr}\""
return safe_str(s)
end
## return 'label'.
def js_link(label, js_code, tags=nil)
return safe_str(%Q`#{safe_escape(label.to_s)}`)
end
def _hash2attr(hash) # :nodoc:
attr = ""
return attr unless hash
hash.each_pair do |k, v|
attr << " #{safe_escape(k.to_s)}=\"#{safe_escape(v.to_s)}\""
end
return attr
end
private :_hash2attr
## convert "\n" into "
\n"
def nl2br(text)
return safe_str(text.to_s.gsub(/\n/, "
\n"))
end
## convert "\n" and " " into "
\n" and " "
def text2html(text)
return nl2br(safe_escape(text.to_s).gsub(/ /, ' '))
end
## cycle values everytime when #to_s() is called
## ex:
## cycle = Cycle.new('odd', 'even')
## "#{cycle}" #=> 'odd'
## "#{cycle}" #=> 'even'
## "#{cycle}" #=> 'odd'
class Cycle
attr_reader :values
def initialize(*values)
@values = values.freeze
@length = values.length
@index = -1
end
attr_reader :index
def next
return @values[(@index += 1) % @length]
end
alias :to_s :next
def count
@index + 1
end
end
def new_cycle(*values)
return Cycle.new(*values)
end
end
##
##
##
class SafeString < String
def to_s
self
end
end
module SafeHelper
#--
#escape() should be defined
#++
module_function
## return SafeString object
def safe_str(s)
SafeString.new(s.to_s)
end
## return true if s is SafeString object
def safe_str?(s)
s.is_a?(SafeString)
end
## escape val only if val is not SafeString object, and return SafeString object
def safe_escape(val)
safe_str?(val) ? val : safe_str(escape(val))
end
end
##
## helper module for BaseContext class
##
module ContextHelper
attr_accessor :_buf, :_engine, :_layout, :_template
## escape value. this method should be overrided in subclass.
def escape(val)
return val
end
## include template. 'template_name' can be filename or short name.
def import(template_name, _append_to_buf=true)
_buf = self._buf
output = self._engine.render(template_name, context=self, layout=false)
self._buf = _buf
_buf << output if _append_to_buf
return output
end
## add value into _buf. this is equivarent to '#{value}'.
def echo(value)
self._buf << value.to_s
end
##
## start capturing.
## returns captured string if block given, else return nil.
## if block is not given, calling stop_capture() is required.
##
## ex. list.rbhtml
##
## Document Title
##
##
##
##
##
## ex. layout.rbhtml
##
##
##
## ${@title}
##
##
## ${@title}
##
##
##
##
##
##
def start_capture(varname=nil)
@_capture_varname = varname
@_start_position = self._buf.length
if block_given?
yield
output = stop_capture()
return output
else
return nil
end
end
##
## stop capturing.
## returns captured string.
## see start_capture()'s document.
##
def stop_capture(store_to_context=true)
output = self._buf[@_start_position..-1]
self._buf[@_start_position..-1] = ''
@_start_position = nil
if @_capture_varname
self.instance_variable_set("@#{@_capture_varname}", output) if store_to_context
@_capture_varname = nil
end
return output
end
##
## if captured string is found then add it to _buf and return true,
## else return false.
## this is a helper method for layout template.
##
def captured_as(name)
str = self.instance_variable_get("@#{name}")
return false unless str
@_buf << str
return true
end
##
## ex. _p("item['name']") => #{item['name']}
##
def _p(arg)
return "<`\##{arg}\#`>" # decoded into #{...} by preprocessor
end
##
## ex. _P("item['name']") => ${item['name']}
##
def _P(arg)
return "<`$#{arg}$`>" # decoded into ${...} by preprocessor
end
##
## decode <`#...#`> and <`$...$`> into #{...} and ${...}
##
def _decode_params(s)
require 'cgi'
return s unless s.is_a?(String)
s = s.dup
s.gsub!(/%3C%60%23(.*?)%23%60%3E/im) { "\#\{#{CGI::unescape($1)}\}" }
s.gsub!(/%3C%60%24(.*?)%24%60%3E/im) { "\$\{#{CGI::unescape($1)}\}" }
s.gsub!(/<`\#(.*?)\#`>/m) { "\#\{#{CGI::unescapeHTML($1)}\}" }
s.gsub!(/<`\$(.*?)\$`>/m) { "\$\{#{CGI::unescapeHTML($1)}\}" }
s.gsub!(/<`\#(.*?)\#`>/m, '#{\1}')
s.gsub!(/<`\$(.*?)\$`>/m, '${\1}')
return s
end
##
## cache fragment data
##
## ex.
## kv_store = Tenjin::FileBaseStore.new("/var/tmp/myapp/dacache")
## Tenjin::Engine.data_cache = kv_store
## engine = Tenjin::Engine.new
## # or engine = Tenjin::Engine.new(:data_cache=>kv_store)
## entries = proc { Entry.find(:all) }
## html = engine.render("index.rbhtml", {:entries => entries})
##
## index.rbhtml:
##
##
##
##
##
##
## - ${entry.title}
##
##
##
##
##
##
def cache_with(cache_key, lifetime=nil)
kv_store = self._engine.data_cache or
raise ArgumentError.new("data_cache object is not set for engine object.")
data = kv_store.get(cache_key, self._template.timestamp)
if data
echo data
else
pos = self._buf.length
yield
data = self._buf[pos..-1]
kv_store.set(cache_key, data, lifetime)
end
nil
end
end
##
## base class for Context class
##
class BaseContext
include Enumerable
include ContextHelper
def initialize(vars=nil)
update(vars) if vars.is_a?(Hash)
end
def [](key)
instance_variable_get("@#{key}")
end
def []=(key, val)
instance_variable_set("@#{key}", val)
end
def update(hash)
hash.each do |key, val|
self[key] = val
end
end
def key?(key)
return self.instance_variables.include?("@#{key}")
end
if Object.respond_to?('instance_variable_defined?')
def key?(key)
return self.instance_variable_defined?("@#{key}")
end
end
alias has_key? key?
def each()
instance_variables().each do |name|
if name != '@_buf' && name != '@_engine'
val = instance_variable_get(name)
key = name[1..-1]
yield([key, val])
end
end
end
end
##
## context class for Template
##
class Context < BaseContext
include HtmlHelper
include HtmlTagHelper
include SafeHelper
end
##
## template class
##
## ex. file 'example.rbhtml'
##
##
## ${@title}
##
##
##
##
## - #{i} : ${item}
##
##
##
##
##
## ex. convertion
## require 'tenjin'
## template = Tenjin::Template.new('example.rbhtml')
## print template.script
## ## or
## # template = Tenjin::Template.new()
## # print template.convert_file('example.rbhtml')
## ## or
## # template = Tenjin::Template.new()
## # fname = 'example.rbhtml'
## # print template.convert(File.read(fname), fname) # filename is optional
##
## ex. evaluation
## context = {:title=>'Tenjin Example', :items=>['foo', 'bar', 'baz'] }
## output = template.render(context)
## ## or
## # context = Tenjin::Context(:title=>'Tenjin Example', :items=>['foo','bar','baz'])
## # output = template.render(context)
## ## or
## # output = template.render(:title=>'Tenjin Example', :items=>['foo','bar','baz'])
## print output
##
class Template
ESCAPE_FUNCTION = 'escape' # or 'Eruby::Helper.escape'
TRACE = false
def self.TRACE=(boolean)
remove_const :TRACE
const_set :TRACE, boolean
end
##
## initializer of Template class.
##
## options:
## :escapefunc :: function name to escape value (default 'escape')
## :preamble :: preamble such as "_buf = ''" (default nil)
## :postamble :: postamble such as "_buf.to_s" (default nil)
##
def initialize(filename=nil, options={})
if filename.is_a?(Hash)
options = filename
filename = nil
end
@filename = filename
@escapefunc = options[:escapefunc] || ESCAPE_FUNCTION
@preamble = options[:preamble] == true ? "_buf = #{init_buf_expr()}; " : options[:preamble]
@postamble = options[:postamble] == true ? finish_buf_expr() : options[:postamble]
@input = options[:input]
@trace = options[:trace] || TRACE
@args = nil # or array of argument names
if @input
convert(@input, filename)
elsif filename
convert_file(filename)
end
end
attr_accessor :filename, :escapefunc, :initbuf, :newline
attr_accessor :timestamp, :args
attr_accessor :script #,:bytecode
attr_accessor :_last_checked_at
## convert file into ruby code
def convert_file(filename)
return convert(File.read(filename), filename)
end
## convert string into ruby code
def convert(input, filename=nil)
@input = input
@filename = filename
@proc = nil
pos = input.index(?\n)
if pos && input[pos-1] == ?\r
@newline = "\r\n"
@newlinestr = '\\r\\n'
else
@newline = "\n"
@newlinestr = '\\n'
end
before_convert()
parse_stmts(input)
after_convert()
return @script
end
protected
## hook method called before convert()
def before_convert()
@script = ''
@script << @preamble if @preamble
end
## hook method called after convert()
def after_convert()
@script << @newline unless @script[-1] == ?\n
@script << @postamble << @newline if @postamble
end
def self.compile_stmt_pattern(pi)
return /(^[ \t]*)?<\?#{pi}(\s)(.*?) ?\?>([ \t]*\r?\n)?/m
end
def capture_stmt(matched)
#: return lspace, mspace, code, and rspace
return matched.captures()
end
STMT_PATTERN = self.compile_stmt_pattern('rb')
def stmt_pattern
STMT_PATTERN
end
## parse statements ('')
def parse_stmts(input)
return unless input
is_bol = true
prev_rspace = nil
pos = 0
input.scan(stmt_pattern()) do
m = Regexp.last_match
lspace, mspace, code, rspace = capture_stmt(m)
text = input[pos, m.begin(0) - pos]
pos = m.end(0)
##
text.insert(0, prev_rspace) if prev_rspace
prev_rspace = nil
code = "#{mspace}#{code}" unless mspace == ' '
if lspace && rspace
code = "#{lspace}#{code}#{rspace}"
else
code << ";" unless code[-1] == ?\n
text << lspace if lspace && !lspace.empty?
prev_rspace = rspace if rspace && !rspace.empty?
end
parse_exprs(text)
add_stmt(statement_hook(code)) if code && !code.empty?
end
#rest = $' || input
rest = pos > 0 ? input[pos..-1] : input
rest.insert(0, prev_rspace) if prev_rspace
parse_exprs(rest) if rest && !rest.empty?
end
def expr_pattern
#return /([\#$])\{(.*?)\}/
return /(\$)\{(.*?)\}/m
#return /\$\{.*?\}/
end
## ex. get_expr_and_escapeflag('$', 'item[:name]') => 'item[:name]', true
def get_expr_and_escapeflag(matched)
return matched[2], matched[1] == '$'
end
## parse expressions ('#{...}' and '${...}')
def parse_exprs(input)
return if !input or input.empty?
pos = 0
start_text_part()
input.scan(expr_pattern()) do
m = Regexp.last_match
text = input[pos, m.begin(0) - pos]
pos = m.end(0)
expr, flag_escape = get_expr_and_escapeflag(m)
#m = Regexp.last_match
#start = m.begin(0)
#stop = m.end(0)
#text = input[pos, start - pos]
#expr = input[start+2, stop-start-3]
#pos = stop
add_text(text)
add_expr(expr, flag_escape)
end
rest = $' || input
#if !rest || rest.empty?
# @script << '`; '
#elsif rest[-1] == ?\n
# rest.chomp!
# @script << escape_str(rest) << @newlinestr << '`' << @newline
#else
# @script << escape_str(rest) << '`; '
#end
flag_newline = input[-1] == ?\n
add_text(rest, true)
stop_text_part()
@script << (flag_newline ? @newline : '; ')
end
## expand macros and parse '#@ARGS' in a statement.
def statement_hook(stmt)
## macro expantion
#macro_pattern = /\A\s*(\w+)\((.*?)\);?(\s*)\z/
#if macro_pattern =~ stmt
# name = $1; arg = $2; rspace = $3
# handler = get_macro_handler(name)
# ret = handler ? handler.call(arg) + $3 : stmt
# return ret
#end
## arguments declaration
if @args.nil?
args_pattern = /\A *\#@ARGS([ \t]+(.*?))?(\s*)\z/ #
if args_pattern =~ stmt
@args = []
declares = ''
rspace = $3
if $2
for s in $2.split(/,/)
arg = s.strip()
next if s.empty?
arg =~ /\A[a-zA-Z_]\w*\z/ or raise ArgumentError.new("#{arg}: invalid template argument.")
@args << arg
declares << " #{arg} = @#{arg};"
end
end
declares << rspace
return declares
end
end
##
return stmt
end
#MACRO_HANDLER_TABLE = {
# "echo" => proc { |arg|
# " _buf << (#{arg});"
# },
# "import" => proc { |arg|
# " _buf << @_engine.render(#{arg}, self, false);"
# },
# "start_capture" => proc { |arg|
# " _buf_bkup = _buf; _buf = \"\"; _capture_varname = #{arg};"
# },
# "stop_capture" => proc { |arg|
# " self[_capture_varname] = _buf; _buf = _buf_bkup;"
# },
# "start_placeholder" => proc { |arg|
# " if self[#{arg}] then _buf << self[#{arg}] else;"
# },
# "stop_placeholder" => proc { |arg|
# " end;"
# },
#}
#
#def get_macro_handler(name)
# return MACRO_HANDLER_TABLE[name]
#end
## start text part
def start_text_part()
@script << " _buf << %Q`"
end
## stop text part
def stop_text_part()
@script << '`'
end
## add text string
def add_text(text, encode_newline=false)
return unless text && !text.empty?
if encode_newline && text[-1] == ?\n
text.chomp!
@script << escape_str(text) << @newlinestr
else
@script << escape_str(text)
end
end
## escape '\\' and '`' into '\\\\' and '\`'
def escape_str(str)
str.gsub!(/[`\\]/, '\\\\\&')
str.gsub!(/\r\n/, "\\r\r\n") if @newline == "\r\n"
return str
end
## add expression code
def add_expr(code, flag_escape=nil)
return if !code || code.empty?
@script << (flag_escape ? "\#{#{@escapefunc}((#{code}).to_s)}" : "\#{#{code}}")
end
## add statement code
def add_stmt(code)
@script << code
end
private
## create proc object
def _render() # :nodoc:
return eval(_to_proc_code(@script).untaint, nil, @filename || '(tenjin)')
end
def _to_proc_code(script)
## take care of magic comment when compiling script into proc object
m = /\A[ \t]*\#.*coding[:=][ \t]*[-.\w]+.*\n/.match(script)
if m
pos = m.end(0)
"#{script[0...pos]}proc do |_context| self._buf = _buf = #{init_buf_expr()}; #{@script[pos..-1]}; #{finish_buf_expr()} end"
else
"proc do |_context| self._buf = _buf = #{init_buf_expr()}; #{@script}; #{finish_buf_expr()} end"
end
end
public
def init_buf_expr() # :nodoc:
return "''"
end
def finish_buf_expr() # :nodoc:
return "_buf.to_s"
end
## evaluate converted ruby code and return it.
## argument '_context' should be a Hash object or Context object.
def render(_context=Context.new)
_context = Context.new(_context) if _context.is_a?(Hash)
@proc ||= _render()
if @trace
s = ""
s << "\n"
s << _context.instance_eval(&@proc)
s << "\n"
return s
else
return _context.instance_eval(&@proc)
end
end
end
##
## preprocessor class
##
class Preprocessor < Template
protected
STMT_PATTERN = compile_stmt_pattern('RB')
def stmt_pattern
return STMT_PATTERN
end
def expr_pattern
return /([\#$])\{\{(.*?)\}\}/m
end
#--
#def get_expr_and_escapeflag(matched)
# return matched[2], matched[1] == '$'
#end
#++
def escape_str(str)
str.gsub!(/[\\`\#]/, '\\\\\&')
str.gsub!(/\r\n/, "\\r\r\n") if @newline == "\r\n"
return str
end
def add_expr(code, flag_escape=nil)
return if !code || code.empty?
super("_decode_params((#{code}))", flag_escape)
end
end
##
## (experimental) fast template class which use Array buffer and Array#push()
##
## ex. ('foo.rb')
## require 'tenjin'
## engine = Tenjin::Engine.new(:templateclass=>Tenjin::ArrayBufferTemplate)
## template = engine.get_template('foo.rbhtml')
## puts template.script
##
## result:
## $ cat foo.rbhtml
##
## $ ruby foo.rb
## _buf.push('
## '); for item in items
## _buf.push(' - ', (item).to_s, '
## '); end
## _buf.push('
## ');
##
class ArrayBufferTemplate < Template
#def initialize(filename=nil, options={})
# options[:postamble] = options[:postamble] == true ? '_buf.join' : options[:postamble]
# super(filename, options)
#end
protected
def expr_pattern
return /([\#$])\{(.*?)\}/
end
## parse expressions ('#{...}' and '${...}')
def parse_exprs(input)
return if !input or input.empty?
pos = 0
items = []
input.scan(expr_pattern()) do
prefix, expr = $1, $2
m = Regexp.last_match
text = input[pos, m.begin(0) - pos]
pos = m.end(0)
items << quote_str(text) if text && !text.empty?
items << quote_expr(expr, prefix == '$') if expr && !expr.empty?
end
rest = $' || input
items << quote_str(rest) if rest && !rest.empty?
@script << " _buf.push(" << items.join(", ") << "); " unless items.empty?
end
def quote_str(text)
text.gsub!(/[\'\\]/, '\\\\\&')
return "'#{text}'"
end
def quote_expr(expr, flag_escape)
return flag_escape ? "#{@escapefunc}((#{expr}).to_s)" : "(#{expr}).to_s" # or "(#{expr})"
end
private
def _render() # :nodoc:
return eval("proc { |_context| self._buf = _buf = #{init_buf_expr()}; #{@script}; _buf.join }".untaint, nil, @filename || '(tenjin)')
end
#--
#def get_macro_handler(name)
# if name == "start_capture"
# return proc { |arg|
# " _buf_bkup = _buf; _buf = []; _capture_varname = #{arg};"
# }
# elsif name == "stop_capture"
# return proc { |arg|
# " self[_capture_varname] = _buf.join; _buf = _buf_bkup;"
# }
# else
# return super
# end
#end
#++
public
def init_buf_expr() # :nodoc:
return "[]"
end
def finish_buf_expr() # :nodoc:
return "_buf.join"
end
end
##
## template class to use eRuby template file (*.rhtml) instead of
## Tenjin template file (*.rbhtml).
## requires 'erubis' (http://www.kuwata-lab.com/erubis).
##
## ex.
## require 'erubis'
## require 'tenjin'
## engine = Tenjin::Engine.new(:templateclass=>Tenjin::ErubisTemplate)
##
class ErubisTemplate < Tenjin::Template
protected
def parse_stmts(input)
eruby = Erubis::Eruby.new(input, :preamble=>false, :postamble=>false)
@script << eruby.src
end
end
##
##
##
class SafeTemplate < Template
ESCAPE_FUNCTION = 'safe_escape'
def initialize(filename=nil, options={})
options, filename = filename, nil if filename.is_a?(Hash)
options[:escapefunc] ||= 'safe_escape'
super(filename, options)
end
## escape '#' in addition '\\' and '`'
def escape_str(str)
str.gsub!(/[`\#\\]/, '\\\\\&')
str.gsub!(/\r\n/, "\\r\r\n") if @newline == "\r\n"
return str
end
end
##
## abstract class for template cache
##
class TemplateCache
def save(cachepath, template)
raise NotImplementedError.new("#{self.class.name}#save(): not implemented yet.")
end
def load(cachepath, timestamp=nil)
raise NotImplementedError.new("#{self.class.name}#load(): not implemented yet.")
end
end
##
## dummy template cache
##
class NullTemplateCache < TemplateCache
def save(cachepath, template)
## do nothing.
end
def load(cachepath, timestamp=nil)
## do nothing.
end
end
##
## file base template cache which saves template script into file
##
class FileBaseTemplateCache < TemplateCache
def save(cachepath, template)
#: save template script and args into cache file.
t = template
tmppath = "#{cachepath}#{rand().to_s[1,8]}"
s = t.args ? "\#@ARGS #{t.args.join(',')}\n" : ''
File.open(tmppath, 'wb') {|f| f.write(s); f.write(t.script) }
#: set cache file's mtime to template timestamp.
File.utime(t.timestamp, t.timestamp, tmppath) if t.timestamp
File.rename(tmppath, cachepath)
Tenjin.logger.debug("[tenjin.rb:#{__LINE__}] cache saved (cachefile=#{cachepath.inspect}).") if Tenjin.logger
end
def load(cachepath, timestamp=nil)
# 'timestamp' argument has mtime of template file
#: load template data from cache file.
begin
#: if template timestamp is specified and different from that of cache file, return nil
mtime = File.mtime(cachepath)
if timestamp && mtime != timestamp
#File.unlink(cachepath)
Tenjin.logger.debug("[tenjin.rb:#{__LINE__}] cache expired (cachefile=#{cachepath.inspect}).") if Tenjin.logger
return nil
end
script = File.open(cachepath, 'rb') {|f| f.read }
rescue Errno::ENOENT => ex
#: if cache file is not found, return nil.
Tenjin.logger.debug("[tenjin.rb:#{__LINE__}] cache not found (cachefile=#{cachepath.inspect}).") if Tenjin.logger
return nil
end
#: get template args data from cached data.
args = script.sub!(/\A\#@ARGS (.*)\r?\n/, '') ? $1.split(/,/) : []
#: return script, template args, and mtime of cache file.
Tenjin.logger.debug("[tenjin.rb:#{__LINE__}] cache found (cachefile=#{cachepath.inspect}).") if Tenjin.logger
return [script, args, mtime]
end
end
##
## abstract class for data cache (= html fragment cache)
##
class KeyValueStore
def get(key, *options)
raise NotImplementedError.new("#{self.class.name}#get(): not implemented yet.")
end
def set(key, value, *options)
raise NotImplementedError.new("#{self.class.name}#set(): not implemented yet.")
end
def del(key, *options)
raise NotImplementedError.new("#{self.class.name}#del(): not implemented yet.")
end
def has?(key, *options)
raise NotImplementedError.new("#{self.class.name}#has(): not implemented yet.")
end
def [](key)
return get(key)
end
def []=(key, value)
return set(key, value)
end
end
##
## memory base data store
##
class MemoryBaseStore < KeyValueStore
def initialize(lifetime=604800)
@values = {}
@lifetime = lifetime
end
attr_accessor :values, :lifetime
def set(key, value, lifetime=nil)
#: store key and value with current and expired timestamp
now = Time.now
@values[key] = [value, now, now + (lifetime || @lifetime)]
end
def get(key, original_timestamp=nil)
#: if cache data is not found, return nil
arr = @values[key]
return nil if arr.nil?
#: if cache data is older than original data, remove it and return nil
value, created_at, timestamp = arr
if original_timestamp && created_at < original_timestamp
del(key)
return nil
end
#: if cache data is expired then remove it and return nil
if timestamp < Time.now
del(key)
return nil
end
#: return cache data
return value
end
def del(key)
#: remove data
#: don't raise error even if key doesn't exist
@values.delete(key)
end
def has?(key)
#: if key exists then return true else return false
return @values.key?(key)
end
end
##
## file base data store
##
class FileBaseStore < KeyValueStore
def initialize(root, lifetime=604800) # = 60*60*24*7
self.root = root
self.lifetime = lifetime
end
attr_accessor :root, :lifetime
def root=(path)
unless File.directory?(path)
raise ArgumentError.new("#{path}: not found.") unless File.exist?(path)
raise ArgumentError.new("#{path}: not a directory.")
end
path = path.chop if path[-1] == ?/
@root = path
end
def filepath(key)
#return File.join(@root, key.gsub(/[^-.\w\/]/, '_'))
return "#{@root}/#{key.gsub(/[^-.\w\/]/, '_')}"
end
def set(key, value, lifetime=nil)
#: create directory for cache
fpath = filepath(key)
dir = File.dirname(fpath)
unless File.exist?(dir)
require 'fileutils' #unless defined?(FileUtils)
FileUtils.mkdir_p(dir)
end
#: create temporary file and rename it to cache file (in order not to flock)
tmppath = "#{fpath}#{rand().to_s[1,8]}"
_write_binary(tmppath, value)
File.rename(tmppath, fpath)
#: set mtime (which is regarded as cache expired timestamp)
timestamp = Time.now + (lifetime || @lifetime)
File.utime(timestamp, timestamp, fpath)
#: return value
return value
end
def get(key, original_timestamp=nil)
#: if cache file is not found, return nil
fpath = filepath(key)
#return nil unless File.exist?(fpath)
stat = _ignore_not_found_error { File.stat(fpath) }
return nil if stat.nil?
#: if cache file is older than original data, remove it and return nil
if original_timestamp && stat.ctime < original_timestamp
del(key)
return nil
end
#: if cache file is expired then remove it and return nil
if stat.mtime < Time.now
del(key)
return nil
end
#: return cache file content
return _ignore_not_found_error { _read_binary(fpath) }
end
def del(key, *options)
#: delete data file
#: if data file doesn't exist, don't raise error
fpath = filepath(key)
_ignore_not_found_error { File.unlink(fpath) }
nil
end
def has?(key)
#: if key exists then return true else return false
return File.exist?(filepath(key))
end
private
if RUBY_PLATFORM =~ /mswin(?!ce)|mingw|cygwin|bccwin/i
def _read_binary(fpath)
File.open(fpath, 'rb') {|f| f.read }
end
else
def _read_binary(fpath)
File.read(fpath)
end
end
def _write_binary(fpath, data)
File.open(fpath, 'wb') {|f| f.write(data) }
end
def _ignore_not_found_error(default=nil)
begin
return yield
rescue Errno::ENOENT => ex
return default
end
end
end
##
##
##
class TemplateNotFoundError < StandardError
end
##
## helper class for Engine to find and read files
##
class FileFinder
def find(filename, dirs=nil)
if dirs
#: if dirs specified then find file from it.
for dir in dirs
filepath = File.join(dir, filename)
return filepath if File.file?(filepath)
end
#found = dirs.find {|dir| File.isfile(File.join(dir, filename)) }
#return File.join(found, filename) if found
else
#: if dirs not specified then return filename if it exists.
return filename if File.file?(filename)
end
#: if file not found then return nil.
return nil
end
def timestamp(filepath)
#: return mtime of filepath.
return File.mtime(filepath)
end
def read(filepath)
begin
#: if file exists then return file content and mtime.
mtime = File.mtime(filepath)
input = File.open(filepath, 'rb') {|f| f.read }
mtime2 = File.mtime(filepath)
if mtime != mtime2
mtime = mtime2
input = File.open(filepath, 'rb') {|f| f.read }
mtime2 = File.mtime(filepath)
if mtime != mtime2
Tenjin.logger.warn("[tenjin.rb:#{__LINE__}] #{self.class.name}#read(): timestamp is changed while reading file.") if Tenjin.logger
end
end
return input, mtime
rescue Errno::ENOENT
#: if file not found then return nil.
return nil
end
end
end
##
## engine class for templates
##
## Engine class supports the followings.
## * template caching
## * partial template
## * layout template
## * capturing (experimental)
##
## ex. file 'ex_list.rbhtml'
##
##
## ex. file 'ex_layout.rbhtml'
##
##
## ${@title}
## #{@_content}
##
##
##
##
## ex. file 'main.rb'
## require 'tenjin'
## options = {:prefix=>'ex_', :postfix=>'.rbhtml', :layout=>'ex_layout.rbhtml'}
## engine = Tenjin::Engine.new(options)
## context = {:title=>'Tenjin Example', :items=>['foo', 'bar', 'baz']}
## output = engine.render(:list, context) # or 'ex_list.rbhtml'
## print output
##
class Engine
##
## initializer of Engine class.
##
## options:
## :prefix :: prefix string for template name (ex. 'template/')
## :postfix :: postfix string for template name (ex. '.rbhtml')
## :layout :: layout template name (default nil)
## :path :: array of directory name (default nil)
## :cache :: save converted ruby code into file or not (default true)
## :path :: list of directory (default nil)
## :preprocess :: flag to activate preprocessing (default nil)
## :templateclass :: template class object (default Tenjin::Template)
##
def initialize(options={})
@prefix = options[:prefix] || ''
@postfix = options[:postfix] || ''
@layout = options[:layout]
@path = options[:path]
@lang = options[:lang]
@finder = options[:finder] || FileFinder.new
@cache = _template_cache(options[:cache])
@preprocess = options.fetch(:preprocess, nil)
@data_cache = options[:data_cache] || @@data_cache
@templateclass = options.fetch(:templateclass, Template)
@init_opts_for_template = options
@_templates = {} # template_name => [template_obj, filepath]
end
attr_accessor :prefix, :postfix, :layout, :path, :lang, :cache
attr_accessor :preprocess, :data_cache, :templateclass
def _template_cache(cache) #:nodoc:
#: if cache is nil or true then return @@template_cache
return @@template_cache if cache.nil? || cache == true
#: if cache is false then return NullTemplateCache object
return NullTemplateCache.new if cache == false
#: if cache is an instnce of TemplateClass then return it
return cache if cache.is_a?(TemplateCache)
#: if else then raises error
raise ArgumentError.new(":cache is expected true, false, or TemplateCache object")
end
private :_template_cache
@@template_cache = FileBaseTemplateCache.new()
def self.template_cache; @@template_cache; end
def self.template_cache=(x); @@template_cache = x; end
@@data_cache = MemoryBaseStore.new()
def self.data_cache; @@data_cache; end
def self.data_cache=(x); @@data_cache = x; end
TIMESTAMP_INTERVAL = 1.0
## register template object
def register_template(template_name, template)
#: register template object without file path.
filename = to_filename(template_name)
@_templates[filename] = [template, nil]
end
## returns cache file path of template file
def cachename(filepath)
#: if lang is provided then add it to cache filename.
if @lang
return "#{filepath}.#{@lang}.cache".untaint
#: return cache file name which is untainted.
else
return "#{filepath}.cache".untaint
end
end
## convert short name into filename (ex. ':list' => 'template/list.rb.html')
def to_filename(template_name)
#: if template_name is a Symbol, add prefix and postfix to it.
#: if template_name is not a Symbol, just return it.
name = template_name
return name.is_a?(Symbol) ? "#{@prefix}#{name}#{@postfix}" : name
end
private
def _timestamp_changed?(template)
#: if checked within a sec, skip timestamp check and return false.
time = template._last_checked_at
now = Time.now
if time && now - time < TIMESTAMP_INTERVAL
return false
end
#: if timestamp is same as file, return false.
filepath = template.filename
if template.timestamp == @finder.timestamp(filepath)
template._last_checked_at = now
return false
#: if timestamp is changed, return true.
else
Tenjin.logger.info("[tenjin.rb:#{__LINE__}] cache expired (template='#{template.filename}')") if Tenjin.logger
return true
end
end
def _get_template_in_memory(filename)
template, filepath = @_templates[filename]
#: if template object is not in memory cache then return nil.
return nil unless template
#: if without filepath, don't check timestamp and return it.
return template unless filepath
#: if timestamp of template file is not changed, return it.
return template unless _timestamp_changed?(template)
#: if timestamp of template file is changed, clear it and return nil.
@_templates.delete(filename)
return nil
end
def _get_template_in_cache(filepath, cachepath)
#: if template is not found in cache file, return nil.
template = @cache.load(cachepath)
return nil unless template
#: if cache returns script and args then build a template object from them.
if template.is_a?(Array)
arr = template
template = create_template(nil, nil)
template.script, template.args, template.timestamp = arr
template.filename = filepath
end
#: if timestamp of template is changed then ignore it.
return nil if _timestamp_changed?(template)
#: if timestamp is not changed then return it.
@tracer.trace("template '#{filename}' found in cache.") if @tracer
return template
end
public
def get_template(template_name, _context=nil)
#: accept template name such as :index.
filename = to_filename(template_name)
#: if template object is in memory cache then return it.
template = _get_template_in_memory(filename)
return template if template
#: if template file is not found then raise TemplateNotFoundError.
filepath = @finder.find(filename, @path) or
raise TemplateNotFoundError.new("#{filename}: template not found (path=#{@path.inspect}).")
#: if template is cached in file then store it into memory and return it.
cachepath = cachename(filepath)
template = _get_template_in_cache(filepath, cachepath)
if template
@_templates[filename] = [template, filepath]
return template
end
#: if template file is not found then raises TemplateNotFoundError.
ret = @finder.read(filepath) or
raise TemplateNotFoundError.new("#{filepath}: template not found.")
input, timestamp = ret
#: if preprocess is enabled then preprocess template file.
input = _preprocess(input, filepath, _context) if @preprocess
#: if template is not found in memory nor cache then create new one.
template = create_template(input, filepath)
template.filename = filepath
template.timestamp = timestamp
template._last_checked_at = Time.now
#: save template object into file cache and memory cache.
@cache.save(cachepath, template) if @cache
@_templates[filename] = [template, filepath]
#: return template object.
return template
end
private
def _preprocess(input, filepath, _context=nil)
#: preprocess input with _context and return result.
_context ||= {}
_context = hook_context(_context) if _context.is_a?(Hash)
_buf = _context._buf
_context._buf = ""
begin
preprocessor = Preprocessor.new(nil)
preprocessor.convert(input, filepath)
return preprocessor.render(_context)
ensure
_context._buf = _buf
end
end
protected
## create template object from file
def create_template(input=nil, filepath=nil)
#: create template object and return it.
template = @templateclass.new(nil, @init_opts_for_template)
#: if input is specified then convert it into script.
template.convert(input, filepath) if input
return template
end
def hook_context(context)
#: if context is nil then create new Context object
if !context
context = Context.new
#: if context is a Hash object then convert it into Context object
elsif context.is_a?(Hash)
context = Context.new(context)
#: if context is an object then use it as context object
else
# nothing
end
#: set _engine attribute
context._engine = self
#: set _layout attribute
context._layout = nil
#: return context object
return context
end
public
## get template object and evaluate it with context object.
## if argument 'layout' is true then default layout file (specified at
## initializer) is used as layout template, else if false then no layout
## template is used.
## if argument 'layout' is string, it is regarded as layout template name.
def render(template_name, context=Context.new, layout=true)
#: if context is a Hash object, convert it into Context object.
context = hook_context(context)
while true
# get template
template = get_template(template_name, context) # context is passed only for preprocessor
#: set template object into context (required for cache_with() helper)
_tmpl = context._template
context._template = template
# render template
output = template.render(context)
# back template
context._template = _tmpl
#: if @_layout is specified, use it as layoute template name
unless context._layout.nil?
layout = context._layout
context._layout = nil
end
#: use default layout template if layout is true or nil
layout = @layout if layout == true || layout.nil?
#: if layout is false then don't use layout template
break unless layout
#: set layout name as next template name
template_name = layout
layout = false
#: set output into @_content for layout template
context.instance_variable_set('@_content', output)
end
return output
end
end
class SafeEngine < Engine
def initialize(options={})
options[:templateclass] = SafeTemplate
super(options)
end
end
end