## ## copyright(c) 2007 kuwata-lab 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 module ## ## $Rev: 59 $ ## $Release: 0.6.0 $ ## module Tenjin RELEASE = ('$Release: 0.6.0 $' =~ /[\d.]+/) && $& ## ## helper module for Context class ## module HtmlHelper module_function XML_ESCAPE_TABLE = { '&'=>'&', '<'=>'<', '>'=>'>', '"'=>'"', "'"=>''' } def escape_xml(s) #return s.gsub(/[&<>"]/) { XML_ESCAPE_TABLE[$&] } return s.gsub(/[&<>"]/) { |s| XML_ESCAPE_TABLE[s] } ## #s = s.gsub(/&/, '&') #s.gsub!(//, '>') #s.gsub!(/"/, '"') #return s ## #return s.gsub(/&/, '&').gsub(//, '>').gsub(/"/, '"') end alias escape escape_xml ## (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 " #{name}=\"#{escape_xml((value || expr).to_s)}\"" else return " #{name}=\"#{value || expr}\"" end end ## return ' checked="checked"' if expr is not false or nil def checked(expr) return expr ? ' checked="checked"' : '' end ## return ' selected="selected"' if expr is not false or nil def selected(expr) return expr ? ' selected="selected"' : '' end ## return ' disabled="disabled"' if expr is not false or nil def disabled(expr) return expr ? ' disabled="disabled"' : '' end ## convert "\n" into "
\n" def nl2br(text) return text.to_s.gsub(/\n/, "
\n") end ## convert "\n" and " " into "
\n" and "  " def text2html(text) return nl2br(escape_xml(text.to_s).gsub(/ /, '  ')) end end ## ## helper module for BaseContext class ## module ContextHelper attr_accessor :_buf, :_engine, :_layout ## 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) _buf << output if _append_to_buf return output end ## add value into _buf. this is equivarent to '#{value}'. def echo(value) self._buf << value 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 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 escape(val) return 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 def each() instance_variables().each do |name| val = instance_variable_get(name) key = name[1..-1] yield([key, val]) if name != '@_buf' && name != '@_engine' end end end ## ## context class for Template ## class Context < BaseContext include HtmlHelper end ## ## template class ## ## ex. file 'example.rbhtml' ## ## ##

${@title}

## ## ## ## ## 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' ## ## 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 ? "_buf.to_s" : options[:postamble] @args = nil # or array of argument names convert_file(filename) if filename end attr_accessor :filename, :escapefunc, :initbuf, :newline attr_accessor :timestamp, :args attr_accessor :script #,:bytecode ## 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 /<\?#{pi}( |\t|\r?\n)(.*?) ?\?>([ \t]*\r?\n)?/m 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 |mspace, code, rspace| m = Regexp.last_match text = input[pos, m.begin(0) - pos] pos = m.end(0) ## detect spaces at beginning of line lspace = nil if rspace.nil? # nothing elsif text.empty? lspace = "" if is_bol elsif text[-1] == ?\n lspace = "" else rindex = text.rindex(?\n) if rindex s = text[rindex+1..-1] if s =~ /\A[ \t]*\z/ lspace = s text = text[0..rindex] #text[rindex+1..-1] = '' end else if is_bol && text =~ /\A[ \t]*\z/ lspace = text text = nil #lspace = text.dup #text[0..-1] = '' end end end is_bol = rspace ? true : false ## text.insert(0, prev_rspace) if prev_rspace parse_exprs(text) code.insert(0, mspace) if mspace != ' ' if lspace assert if rspace.nil? code.insert(0, lspace) code << rspace #add_stmt(code) prev_rspace = nil else code << ';' unless code[-1] == ?\n #add_stmt(code) prev_rspace = rspace end if code code = statement_hook(code) add_stmt(code) end 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("proc { |_context| self._buf = _buf = #{init_buf_expr()}; #{@script}; _buf.to_s }".untaint, nil, @filename || '(tenjin)') end public def init_buf_expr() # :nodoc: return "''" 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() return context.instance_eval(&@proc) 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(' ## '); ## class ArrayBufferTemplate < Template 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 #-- #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 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 ## ## 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] @cache = options.fetch(:cache, true) @path = options[:path] @preprocess = options.fetch(:preprocess, nil) @templateclass = options.fetch(:templateclass, Template) @init_opts_for_template = options @templates = {} # filename->template end ## convert short name into filename (ex. ':list' => 'template/list.rb.html') def to_filename(template_name) name = template_name return name.is_a?(Symbol) ? "#{@prefix}#{name}#{@postfix}" : name end ## find template filename def find_template_file(template_name) filename = to_filename(template_name) if @path for dir in @path filepath = "#{dir}#{File::SEPARATOR}#{filename}" return filepath if test(?f, filepath.untaint) end else return filename if test(?f, filename.dup.untaint) # dup is required for frozen string end raise Errno::ENOENT.new("#{filename} (path=#{@path.inspect})") end ## read template file and preprocess it def read_template_file(filename, _context) return File.read(filename) if !@preprocess _context ||= {} if _context.is_a?(Hash) || _context._engine.nil? _context = hook_context(_context) end preprocessor = Preprocessor.new(filename) return preprocessor.render(_context) end ## register template object def register_template(template_name, template) #template.timestamp = Time.new unless template.timestamp @templates[template_name] = template end def cachename(filename) return (filename + '.cache').untaint end ## create template object from file def create_template(filename, _context=nil) template = @templateclass.new(nil, @init_opts_for_template) template.timestamp = Time.now() cache_filename = cachename(filename) _context = hook_context(Context.new) if _context.nil? if !@cache input = read_template_file(filename, _context) template.convert(input, filename) elsif !test(?f, cache_filename) || File.mtime(cache_filename) < File.mtime(filename) #$stderr.puts "*** debug: load original" input = read_template_file(filename, _context) template.convert(input, filename) store_cachefile(cache_filename, template) else #$stderr.puts "*** debug: load cache" template.filename = filename load_cachefile(cache_filename, template) end return template end ## store template into cache file def store_cachefile(cache_filename, template) s = template.script s = "\#@ARGS #{template.args.join(',')}\n#{s}" if template.args File.open(cache_filename, 'w') do |f| f.flock(File::LOCK_EX) f.write(s) #f.lock(FIle::LOCK_UN) # File#close() unlocks automatically end end ## load template from cache file def load_cachefile(cache_filename, template) s = File.read(cache_filename) if s.sub!(/\A\#\@ARGS (.*?)\r?\n/, '') template.args = $1.split(',') end template.script = s end ## get template object def get_template(template_name, _context=nil) template = @templates[template_name] t = template unless t && t.timestamp && t.filename && t.timestamp >= File.mtime(t.filename) filename = find_template_file(template_name) template = create_template(filename, _context) # _context is passed only for preprocessor register_template(template_name, template) end return template end ## 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) #context = Context.new(context) if context.is_a?(Hash) context = hook_context(context) while true template = get_template(template_name, context) # context is passed only for preprocessor _buf = context._buf output = template.render(context) context._buf = _buf unless context['_layout'].nil? layout = context['_layout'] context['_layout'] = nil end layout = @layout if layout == true or layout.nil? break unless layout template_name = layout layout = false context['_content'] = output end return output end def hook_context(context) if !context context = Context.new elsif context.is_a?(Hash) context = Context.new(context) end context._engine = self context._layout = nil return context end end end