# Web::Template # # Author:: MoonWolf (mailto:moonwolf@moonwolf.com) # Copyright:: Copyright (c) 2003-2004 MoonWolf # License:: Distributes under the same terms as Ruby require "web/escape" module Web class Template CACHE = Hash.new def initialize(opt={}) @param = {} @tree = nil @filename = nil # if filename=opt['filename'] path = opt['path'] || ['.'] if opt['cache'] file = Parser.find_file(filename, path) raise Errno::ENOENT,filename unless file if tmp = CACHE[file] mtime, tree, proc = tmp if File.mtime(file) == mtime @tree = tree @proc = proc return else #p "cache expired" end end end parser = Parser.new @filename, tmpl = parser.load_file(filename,path) @tree = parser.parse(tmpl) writer = RubyWriter.new src = "Proc.new {|param,io|\n" # } writer.out = src writer.run(@tree) src << "io\n" src << "}\n" src.untaint @src = src @proc = eval(src) if opt['cache'] self.class.cache[@filename] = [File.mtime(@filename), @tree, @proc] end end end # syntax tree attr_accessor :tree # execute template and return result string def output(io='') raise "not parsed" unless @tree begin @proc.call(@param,io) rescue puts @src raise end io end # clear params def clear_params @param = {} end # def param(hash=nil) if hash @param.update(hash) self else @param end end # set params def param=(hash) @param = hash end class Node def initialize(value) @value = value @children = [] end # node value attr_reader :value # chilren nodes(Array) attr_reader :children # append child node def <<(node) @children << node self end def accept(visitor) name = self.class.name name.sub!(/.+::/,'') method = "visit_#{name}" visitor.send(method, self) end class RootNode < Node end class TextNode < Node end class VarNode < Node end class IfNode < Node end class UnlessNode < Node end class BlockNode < Node end class LoopNode < Node end end class Parser def self::find_file(filename, path=['.']) path.each {|dir| file = File.expand_path(filename,dir).untaint return file if FileTest.exist? file } nil end def initialize @tokens = nil @tree = nil end def load_file(filename,path=['.']) file = self.class::find_file(filename,path) raise Errno::ENOENT,filename unless file tmpl = nil open(file,'rb') {|f| tmpl = f.read || '' } tmpl = filter(tmpl,path) [file, tmpl] end def filter(tmpl,path) tmpl.gsub(/\{\{INCLUDE\s*(.+?)\}\}/i) {|match| attr = $1 attrs = {} attr.scan(/(\w+)=(?:(?:"(.*?)")|(?:'(.*?)')|(\w+))/) { name = $1.downcase value = $2 || $3 || $4 attrs[name] = value } filename = attrs['name'] file, tmp = load_file(filename, path) tmp } end def parse(src) @tokens = tokenize(src) #@tokens.each {|token| # type,value = token # puts "#{type}\t#{value.inspect}" #} @tree = root end def next_token token = @tokens.shift end def tokenize(src) tokens = [] text = '' until src.empty? if src =~ %r!\A\{\{([a-zA-Z]+)(\s.+?)?\}\}! src = $' attrs = {} case $1.downcase when 'var' type = :TMPL_VAR attrs['escape']='HTML' when 'if' type = :TMPL_IF when 'unless' type = :TMPL_UNLESS when 'else' type = :TMPL_ELSE when 'loop' type = :TMPL_LOOP end attr = $2 if attr attr.scan(/(\w+)=(?:(?:"(.*?)")|(?:'(.*?)')|([0-9a-zA-Z_$\-]+))/) { name = $1.downcase value = $2 || $3 || $4 attrs[name] = value } end unless text.empty? tokens << [:TEXT, text] text = '' end tokens << [type, attrs] elsif src =~ %r!\A\{\{/([a-zA-Z]+)\}\}! src = $' type = $1.downcase.intern unless text.empty? tokens << [:TEXT, text] text = '' end tokens << [:CLOSE, type] elsif src =~ %r!\A[^{]+! src = $' text << $& elsif src =~ %r!\A.! src = $' text << $& end end unless text.empty? tokens << [:TEXT, text] end tokens end def root node = Node::RootNode.new(nil) while token = next_token type, value = token case type when :TEXT node << Node::TextNode.new(value) when :TMPL_VAR node << Node::VarNode.new([value['name'],value['escape'] ? value['escape'].upcase : nil]) when :TMPL_IF node << if_node(token) when :TMPL_UNLESS node << unless_node(token) when :TMPL_LOOP node << loop_node(token) else p node raise type.to_s end end node end def if_node(token) type, value = token name = value['name'] node = Node::IfNode.new(name) node << then_block = Node::BlockNode.new(nil) node << else_block = Node::BlockNode.new(nil) catch(:END_IF) { while token = next_token type, value = token case type when :TEXT then_block << Node::TextNode.new(value) when :TMPL_VAR then_block << Node::VarNode.new([value['name'],value['escape'] ? value['escape'].upcase : nil]) when :TMPL_IF then_block << if_node(token) when :TMPL_UNLESS then_block << unless_node(token) when :TMPL_ELSE break when :TMPL_LOOP then_block << loop_node(token) when :CLOSE throw :END_IF else p type,value raise end end while token = next_token type, value = token case type when :TEXT else_block << Node::TextNode.new(value) when :TMPL_VAR else_block << Node::VarNode.new([value['name'],value['escape'] ? value['escape'].upcase : nil]) when :TMPL_IF else_block << if_node(token) when :TMPL_UNLESS else_block << unless_node(token) when :TMPL_LOOP else_block << loop_node(token) when :CLOSE throw :END_IF else p type,value raise end end } node end def unless_node(token) type, value = token name = value['name'] node = Node::UnlessNode.new(name) node << then_block = Node::BlockNode.new(nil) node << else_block = Node::BlockNode.new(nil) catch(:END_IF) { while token = next_token type, value = token case type when :TEXT then_block << Node::TextNode.new(value) when :TMPL_VAR then_block << Node::VarNode.new([value['name'],value['escape'] ? value['escape'].upcase : nil]) when :TMPL_IF then_block << if_node(token) when :TMPL_UNLESS then_block << unless_node(token) when :TMPL_ELSE break when :TMPL_LOOP then_block << loop_node(token) when :CLOSE throw :END_IF else p type,value raise end end while token = next_token type, value = token case type when :TEXT else_block << Node::TextNode.new(value) when :TMPL_VAR else_block << Node::VarNode.new([value['name'],value['escape'] ? value['escape'].upcase : nil]) when :TMPL_IF else_block << if_node(token) when :TMPL_UNLESS else_block << unless_node(token) when :TMPL_LOOP else_block << loop_node(token) when :CLOSE throw :END_IF else p type,value raise end end } node end def loop_node(token) type, value = token name = value['name'] node = Node::LoopNode.new(name) while token = next_token type, value = token case type when :TEXT node << Node::TextNode.new(value) when :TMPL_VAR node << Node::VarNode.new([value['name'],value['escape'] ? value['escape'].upcase : nil]) when :TMPL_IF node << if_node(token) when :TMPL_UNLESS node << unless_node(token) when :TMPL_LOOP node << loop_node(token) when :CLOSE break else p node raise type.to_s end end node end end # Parser class RubyWriter def initialize @out = '' end # output stream(String or IO) attr_accessor :out # execute tree def run(tree) tree.accept(self) @out end def visit_children(node) node.children.each {|n| n.accept(self) } end def visit_RootNode(node) @out << "root = param\n" @out << "stack = []\n" @out << "list = []\n" @out << "index = 0\n" visit_children(node) @out << "io\n" end def visit_TextNode(node) @out << "io << #{node.value.dump}\n" end def visit_VarNode(node) key, escape = node.value if key=~/\A\$(\w+)/ key = $1 case escape when "HTML" @out << "io << Web::escapeHTML(root[#{key.dump}])\n" when "URL" @out << "io << Web::escape(root[#{key.dump}])\n" else @out << "io << root[#{key.dump}]\n" end else case escape when "HTML" @out << "io << Web::escapeHTML(param[#{key.dump}])\n" when "URL" @out << "io << Web::escape(param[#{key.dump}])\n" else @out << "io << param[#{key.dump}]\n" end end end def visit_IfNode(node) key = node.value case key when "__FIRST__" @out << "if index==0\n" when "__LAST__" @out << "if index==list.size-1\n" when "__INNER__" @out << "if !((index==0) || (index==list.size-1))\n" when "__ODD__" @out << "if index % 2 == 0\n" else if key=~/\A\$(\w+)/ key = $1 @out << "if root[#{key.dump}]\n" else @out << "if param[#{key.dump}]\n" end end n = node.children[0] n.accept(self) if (n = node.children[1]) && (n.children.size > 0) @out << "else\n" n.accept(self) end @out << "end\n" end def visit_UnlessNode(node) key = node.value case key when "__FIRST__" @out << "unless index==0\n" when "__LAST__" @out << "unless index==list.size-1\n" when "__INNER__" @out << "unless !((index==0) || (index==list.size-1))\n" when "__ODD__" @out << "unless index % 2 == 0\n" else if key=~/\A\$(\w+)/ key = $1 @out << "unless root[#{key.dump}]\n" else @out << "unless param[#{key.dump}]\n" end end n = node.children[0] n.accept(self) if (n = node.children[1]) && (n.children.size > 0) @out << "else\n" n.accept(self) end @out << "end\n" end def visit_BlockNode(node) visit_children(node) end def visit_LoopNode(node) key = node.value @out << "stack.push [param, list, index]\n" if key=~/\A\$(\w+)/ key = $1 @out << "list = root[#{key.dump}]\n" else @out << "list = param[#{key.dump}]\n" end @out << "index = 0\n" @out << "list.each {|param|\n" visit_children(node) @out << "index += 1\n" @out << "}\n" @out << "param,list,index = stack.pop\n" end end # RubyWriter end # Template end # Web