require 'hpricot' require 'erb' require 'enumerator' module Amrita2 module ScalarData def amrita_value Amrita2::Util::sanitize_text(to_s) end end module DictionaryData def amrita_value(name) self.__send__(name) end def self.===(other) if other == nil true else super end end end module Enum end module NullObject def amrita_value(key=nil) nil end def each end end module ElementHelper end end class NilClass include Amrita2::NullObject end class FalseClass include Amrita2::NullObject end class String include Amrita2::ScalarData def amrita_value Amrita2::Util::sanitize_text(self) end end class Integer include Amrita2::ScalarData end class BicDecimal include Amrita2::ScalarData end class Float include Amrita2::ScalarData end class Time include Amrita2::ScalarData end class Array include Amrita2::Enum end class Range include Amrita2::Enum end class Hash include Amrita2::DictionaryData def amrita_value(name) self[name.to_sym] end end class Struct include Amrita2::DictionaryData end class Binding include Amrita2::DictionaryData def amrita_value(name) eval "begin; #{name}; rescue(NameError); @#{name};end;", self end end class Hpricot::Elem include Amrita2::ElementHelper end module Amrita2 FilterMethods = [ :filter_element, :parse_element, :parse_node, :setup_type_renderer, :generate_dynamic_element, :define_element_method, :loop_check_code, :method_body_code, :value_filter_code, :renderer_code, :element_render_code ] module Core end module Util end module Runtime include Amrita2 include Util end module Filters class Base end end module Renderers #:nodoc: all class Base end end module Macro #:nodoc: all module Standard end include Standard class Base end end module CompileTimeContext #:nodoc: all include Filters include Renderers include Macro include Standard end class TemplateError < RuntimeError end module Util # :nodoc: all # Amrita2 sanitize anything except for SanitizedString # If you want to sanitize yourself and don't want to Amrita2 sanitize your object, # pass SanitizedString[x] as model data. class SanitizedString < String def SanitizedString::[](s) new(s.to_s).freeze end def amrita_value self end def to_s self end def *(n) SanitizedString[super] end def inspect %[SanitizedString[#{super}]] end end # This module provide methods for avoid XSS vulnerability # taken from IPA home page(Japanese) # http://www.ipa.go.jp/security/awareness/vendor/programming/a01_02.html NAMECHAR = '[-\w\d\.:]' NAME = "([\\w:]#{NAMECHAR}*)" NOT_REFERENCE = "(?!#{NAME};|&#\\d+;|&#x[0-9a-fA-F]+;)" # borrowed from rexml AMP_WITHOUT_REFRENCE = /&#{NOT_REFERENCE}/ # escape &<> def self.sanitize_text(text) return nil unless text s = text.dup s.gsub!(AMP_WITHOUT_REFRENCE, '&') s.gsub!("<", '<') s.gsub!(">", '>') s end # escape &<>"' def self.sanitize_attribute_value(text) return nil unless text s = text.dup s.gsub!(AMP_WITHOUT_REFRENCE, '&') s.gsub!("<", '<') s.gsub!(">", '>') s.gsub!('"', '"') #s.gsub!("'", ''') s end DefaultAllowedScheme = { 'http' => true, 'https' => true, 'ftp' => true, 'mailto' => true, } #UrlInvalidChar = Regexp.new(%q|[^;/?:@&=+$,A-Za-z0-9\-_.!~*'()%]|) UrlInvalidChar = Regexp.new(%q|[^;/?:@&=+$,A-Za-z0-9\-_.!~*'()%#]|) #' # +sanitize_url+ accepts only these characters # --- http://www.ietf.org/rfc/rfc2396.txt --- # uric = reserved | unreserved | escaped # reserved = ";" | "/" | "?" | ":" | "@" | "&" | "=" | "+" | "$" | "," # unreserved = alphanum | mark # mark = "-" | "_" | "." | "!" | "~" | "*" | "'" | "(" | ")" # escaped = "%" hex hex # # +sanitize_url+ accepts only schems specified by +allowd_scheme+ # # The default is http: https: ftp: mailt: def self.sanitize_url(text, allowd_scheme = DefaultAllowedScheme) # return nil if text has characters not allowd for URL return nil if text =~ UrlInvalidChar # return '' if text has an unknown scheme # --- http://www.ietf.org/rfc/rfc2396.txt --- # scheme = alpha *( alpha | digit | "+" | "-" | "." ) if text =~ %r|^([A-Za-z][A-Za-z0-9+\-.]*):| return nil unless allowd_scheme[$1] end # escape HTML # special = "&" | "<" | ">" | '"' | "'" # But I checked "<" | ">" | '"' before. s = text.dup #s.gsub!("&", '&') s.gsub!("'", ''') s end module OptionSupport # :nodoc: all attr_reader :opt def parse_opt(*args) @opt = {} hash = args.last if hash.kind_of?(Hash) @opt.merge!(hash) args.pop end args.each do |key| @opt[key] = key end @opt end def [](key) @opt[key] end def method_missing(sym, *args, &block) if @opt and @opt.has_key?(sym) @opt[sym] else super end end end class Option include OptionSupport def initialize(*args) parse_opt(*args) end end class Tuple < Array # :nodoc: all include ScalarData def self.[](*args) self.new(args) end end end module Runtime # :nodoc: all def amrita_set_context_value(v) Thread::current[:amrita_context_value] = v end def amrita_get_context_value Thread::current[:amrita_context_value] end def new_element(tag, attrs={}, &block) a = {} attrs.each { |k,v| a[k.to_s] = v } ret = Hpricot::Elem.new(Hpricot::STag.new(tag.to_s, a)) ret.instance_eval(&block) if block_given? ret end def start_tag(e, out="") out << "<#{e.stag.name}" e.attributes.each do |k, v| next unless v out << " " out << "#{k}=\"#{Util::sanitize_attribute_value(v.to_s)}\"" #attr.write(out) end unless e.attributes.empty? out << ">" out end def end_tag(e, out="") out << "</#{e.stag.name}>" out end end module ElementHelper # :nodoc: all def elements children.select do |c| c.kind_of?(Hpricot::Elem) end end def contents if children.size > 0 SanitizedString[children.to_s] else nil end end def set_attribute(key, val) case val when nil, "" delete_attribute(key) else super end end def delete_attribute(key) raw_attributes.delete(key) end def add(elem) insert_after(elem, nil) end def add_text(text) insert_after(Hpricot::Text.new(text), nil) end def as_amrita_dictionary(opt={}) ret = {} attributes.each do |k, v| ret[k.intern] = v end if opt[:use_contents] ret[opt[:use_contents]] = Amrita2::SanitizedString[ children.collect do |c| c.to_s end.join ] end if opt[:use_tag] { name.intern => ret } else ret end end end module Core class CodeGenerator # :nodoc: all def initialize @iehack = true @lines = [] @indent = 0 init_strbuf @module_stack = [CGModule.new('')] @current_stream = "__stream__" end def init_strbuf @strbuf = "" end def code(line) flush @lines << [@indent, line] end def comment(comments) comments.each do |c| @lines << [@indent, "# #{c}"] end end def result flush m = @module_stack.shift if m m.constants.each do |name, defs| code("#{name} = #{defs}") end end @lines.collect do |indent, l| " " * indent + l end.join("\n") end def put_constant(c) @strbuf.concat c.to_s end def flush if @strbuf.size > 0 @lines << [@indent, "#{@current_stream}.concat(#{@strbuf.inspect})"] init_strbuf end end def put_hpricot_node(node) s = "" node.output(s) put_constant s end def level_up @indent += 1 yield flush @indent -= 1 end def if_(cond, &block) code("if #{cond}") level_up do yield end code("end") end def case_(obj, &block) code("case #{obj}") yield code("end") end def when_(obj, &block) code("when #{obj}") level_up do yield end end def else_(&block) code("else") level_up do yield end end def with_stream(new_stream_name=nil) if new_stream_name old_stream = @current_stream code("before_#{new_stream_name} = #@current_stream") @current_stream = new_stream_name.to_s end code("#@current_stream = '' ") yield flush ensure if new_stream_name @current_stream = old_stream code %[Thread::current[:amrita_stream] ||= {} ] code %[Thread::current[:amrita_stream][#{new_stream_name.inspect}] = #{new_stream_name}] code("#@current_stream = before_#{new_stream_name}") end code("#@current_stream") end def put_stream(name) code %[#@current_stream.concat(Thread::current[:amrita_stream][#{name.inspect}])] end def def_method(name, *args) if args.size > 0 code("def #{name}(#{args.join(',')})") else code("def #{name}") end level_up do with_stream do yield flush end end code("end") end def put_static_element(e) attr = e.attributes.collect do |name, value| "#{name} = #{value.inspect}" end if attr.size > 0 put_constant("<#{e.name} #{attr.join(' ')}>") else put_constant("<#{e.name}>") end yield put_constant("</#{e.name}>") end def put_expression(exp) code("#{@current_stream}.concat((#{exp}).to_s)") end def put_string_expression(exp) code("#{@current_stream}.concat(#{exp})") end def define_class(name, &block) define_module_or_class("class", name, &block) end def define_module_or_class(typ, name, &block) @module_stack.unshift CGModule.new(name) code("#{typ} #{name}") level_up do yield m = @module_stack.shift m.end_of_module(self) end code("end") end def define_constant(const_def) @module_stack.first.define_constant(const_def) end def eval(src) code "eval(#{src}, __binding__)" end def eval_and_print(src) put_string_expression "eval(#{src}, __binding__)" end class CGModule # :nodoc: attr_reader :name attr_reader :constants attr_reader :methods def initialize(name) @name = name @constants = [] @methods = [] end def define_constant(const_def) name = nil @constants.each do |n, cdef| if cdef == const_def name = n break end end unless name name = "C#{sprintf("%03d", @constants.size)}" @constants << [name, const_def] end name end def define_method(vars, &block) name = "m#{sprintf("%03d", @methods.size)}" @methods << [name, vars, block] name end def end_of_module(cg) @constants.each do |name, defs| cg.code("#{name} = #{defs}") end @methods.each do |name, vars, body| cg.define_method(name, vars) do body.call end end end end end class BaseNode # :nodoc: all attr_reader :parent def initialize(parent) @parent = parent end def render_me(cg) end def module_src(cg) end def root parent.root end def parent_de parent end def dynamic? false end def has_ruby? false end end class StaticNode < BaseNode # :nodoc: all def initialize(parent, node) super(parent) @node = node end def render_me(cg) #cg.put_string_expression(cg.define_constant(@node.to_s.inspect)) # to keep s = "" @node.output(s, :preserve=>true) cg.put_string_expression(cg.define_constant(s.inspect)) end end class CommentNode < BaseNode # :nodoc: all def initialize(parent, node) super(parent) @node = node end def render_me(cg) #cg.put_string_expression(cg.define_constant(@node.to_s.inspect)) # to keep s = "" @node.output(s, :preserve=>true) cg.put_string_expression(cg.define_constant(s.inspect)) end end class ErbNode < BaseNode # :nodoc: all include Util attr_reader :node def initialize(parent, node) super(parent) @node = node @erb_trim_mode = nil end def dynamic? true end def render_me_old(cg) src = @node.inner_text src.each do |s| cg.code("# #{s.chomp}") end erb = cg.define_constant %[ERB.new(#{with_context_value_erb(src).inspect}, nil, #{@erb_trim_mode.inspect})] cg.code("amrita_set_context_value($_)") cg.put_string_expression %[#{erb}.result(__binding__)] cg.code("$_ = amrita_get_context_value") end def render_me(cg) src = @node.inner_text src.each do |s| cg.code("# #{s.chomp}") end erb = cg.define_constant %[ERB.new(#{with_context_value_erb(src).inspect}, nil, #{@erb_trim_mode.inspect}).src] cg.code("amrita_set_context_value($_)") cg.eval_and_print(erb) cg.code("$_ = amrita_get_context_value") end def has_ruby? true end private def with_context_value_erb(src) [ "<% $_ = amrita_get_context_value %>", src, "<% amrita_set_context_value($_) %>", ].join("") end end class ParentNode < BaseNode # :nodoc: all attr_reader :element, :parent, :children def initialize(parent, element) super(parent) @element = element @children = parent_de.parse_element(@element) end def dynamic? children.any? { |c| c.dynamic? } end def has_dynamic? children.any? { |c| c.dynamic? } end def has_ruby? children.any? { |c| c.has_ruby? } end def each(&block) children.each(&block) end def module_src(cg) each do |c| c.module_src(cg) end end end class CompoundElement < ParentNode # :nodoc: all def render_me(cg) if @element.empty? cg.put_constant(@element.to_s) else @parent.element_render_code(cg, @element) do each do |c| c.render_me(cg) end end end end end class DynamicElement < ParentNode # :nodoc: all include Util include OptionSupport attr_reader :parent, :name, :children, :filter, :renderers, :element, :attrs def self.delegate_to_filter(name) module_eval <<-END def #{name}(*args,&block) @filter.#{name}(self, *args,&block) end END end FilterMethods.each do |m| delegate_to_filter(m) end def initialize(parent, name, element, filters=[]) @parent,@name,@element = parent, name, element parse_opt(root.opt) @attrs = {} delete_am_attrs setup_filter(filters) @element = filter_element(@element) @renderers = [] super(parent, element) setup_type_renderer end def [](key) @attrs[key] end def parent_de self end def dynamic? true end def has_ruby? super or am_for_value or am_skipif_value or am_v_value end def setup_filter(filters) #default_filter = @parent ? @parent.filter : DefaultFilter.new default_filter = DefaultFilter.new default_filter.parse_opt(opt) if filters.size > 0 @filter = current = filters.first for f in filters do current.next_ = f current = f end current.next_ = default_filter else @filter = default_filter end end def class_name if name "XX" + name.capitalize.gsub(/[^\w\d]/, "_") else "XX%d" % [object_id.abs] end end def instance_name class_name + "Instance" end def compile(cg = CodeGenerator.new) define_element_method(cg) do cg.code("$_ = value") loop_check_code(cg) method_body_code(cg) end end def render_me(cg, n = name) if (n) cg.put_string_expression("#{instance_name}.render_with($_.amrita_value(#{n.inspect}), __binding__)") else cg.put_string_expression("#{instance_name}.render_with($_, __binding__)") end end def module_src(cg) cg.define_class(class_name) do cg.code <<-END include Amrita2 include Amrita2::Runtime END compile(cg) super end cg.code("#{instance_name} = #{class_name}.new") end def am_for_value @attrs[am_for.intern] end def am_skipif_value @attrs[am_skipif.intern] end def am_v_value @attrs[am_v.intern] end private def delete_am_attrs [ am_src, am_filter, am_skipif, am_for, am_v, ].each do |key_s| key = key_s.intern @attrs[key] = @element.attributes[key_s] @element.delete_attribute(key_s) end end end class RootElement < DynamicElement # :nodoc: all include Filters attr_reader :xml_decl, :doctype def initialize(elements, opt={}, &block) @xml_decl = @doctype = nil parse_opt(opt) if block_given? @filter_proc = block else @filter_proc = proc do |e, src, filters| end end e = Hpricot.make("<root />", :xml=>true).first elements.each do |c| case c when Hpricot::Elem, Hpricot::Text, Hpricot::Comment, Hpricot::BogusETag e.insert_after(c, nil) when Hpricot::XMLDecl @xml_decl = c when Hpricot::DocType @doctype = c else raise "#{c.class}" end end filters = [] @filter_proc.call(e,nil, filters) super(nil, nil, e, filters) end def root self end def compile(cg) cg.code <<-END include Amrita2 include Amrita2::Runtime END super(cg) @children.each do |c| c.module_src(cg) end end def compile_filters(element, name, *args) src = args.compact.join('|') filters = parse_filter(src) @filter_proc.call(element, name, filters) filters end def parse_filter(src) src.gsub!("\n", " ") case src when "",nil [] else case ret = eval(src, @opt[:compiletime_binding]) when Class [ret.new] when Module [ModuleExtendFilter[ret]] when Filters::Base [ret] when Array case ret.first when Symbol [FunctionFilter[*ret]] else ret end when Symbol [FunctionFilter[ret]] when nil [] else raise TemplateError, "unknown Filter type #{ret.class}" end end rescue ScriptError, NameError raise TemplateError, "error in filters #{src.inspect} #$!" end end class DefaultFilter < Filters::Base include Renderers include Filters include Util::OptionSupport def filter_element(de, element) element end def parse_element(de, element) element.enum_for(:each_child).collect do |node| de.parse_node(node) end end def parse_node(de, node) case node when Hpricot::Elem de.generate_dynamic_element(node) || CompoundElement.new(de, node) when Hpricot::CData ErbNode.new(de, node) when Hpricot::Text StaticNode.new(de, node) when Hpricot::Comment CommentNode.new(de, node) when Hpricot::BogusETag StaticNode.new(de, node) else raise "not implemented #{node.class}" end end def generate_dynamic_element(de, e) src = e.attributes[de.am_src] filters_src = e.attributes[de.am_filter] if src name, fs = src.split('|', 2) name.strip! filters = de.root.compile_filters(e, name, fs, filters_src) DynamicElement.new(de, name , e, filters) elsif filters_src filters = de.root.compile_filters(e, nil, filters_src) DynamicElement.new(de, nil , e, filters) else nil end end def setup_type_renderer(de) if de.has_dynamic? de.renderers << HashRenderer.new unless de.renderers.find {|r| r.kind_of?(HashRenderer) } else de.renderers << ScalarRenderer.new end de.renderers << ElseRenderer.new end def define_element_method(de, cg, &block) cg.def_method("render_with", "value", "__binding__", "__cnt__ = -1 ", &block) end def loop_check_code(de, cg) cg.if_ "$_.kind_of?(Amrita2::Enum) and not $_.kind_of?(ScalarData)" do cg.code("$_.each_with_index do |v, i| ") cg.level_up do cg.put_string_expression("render_with(v, __binding__, i) ") end cg.code("end") cg.code("return __stream__") end end def method_body_code(de, cg) de.value_filter_code(cg, "value") #cg.code("p #{de.element.name.inspect}, $_, stream") de.renderer_code(cg, de.element) #cg.code("p #{de.element.name.inspect}, $_, stream") end def value_filter_code(de, cg, value_name) end def renderer_code(de, cg, element) r = de.renderers case r.size when 0 when 1 r.first.generate_body(cg, de, element) else cg.case_("$_") do r.each do |r1| r1.generate_when(cg) do r1.generate_body(cg, de, element) end end end end end def element_render_code(de, cg, element, &block) if ((element.name == 'span' or element.name == '_' or element.name == 'root') and element.attributes.size == 0) yield else cg.put_static_element(element, &block) end end end # # = using Amrita2::Template # # require "amrita2/template" # include Amrita2 # tmpl = Template.new <<-END # <<html< # <<body< # <<h1 :title>> # <<p :body< # <<:template>> is a html template libraly for <<:lang>> # END # # puts tmpl.render_with(:title=>"hello world", :body=>{ :template=>"Amrita2", :lang=>"Ruby" }) class Template include Amrita2 include Util include OptionSupport include Filters include CompileTimeContext include Runtime # Specify attribute prefix for dynamic element. Defaults to 'am:'. attr_accessor :amrita_prefix # Specify weather use inline ruby. Defaults to +true+ attr_accessor :inline_ruby attr_reader :tracer attr_reader :root # Initialize Template object using +text+ and +opts+. # Optionaly specify block for setting filters. # # t = Amrita2::Template.new(text) do |e, src, filters| # if src == "aaa" # filters << Amrita2::Filters::Default[456] # end # end def initialize(text, *opts, &block) parse_opt(*opts) @text = text.dup @setuped = false @filter_proc = block @text_domain = nil @inline_ruby = true @amrita_prefix = @opt[:amrita_prefix] || "am:" end def amrita_src=(v) @opt[:amrita_src] = v end def amrita_filter=(v) @opt[:amrita_filter] = v end def compiletime_binding=(b) @opt[:compiletime_binding] = b end # Print compiled ruby code and runtime trace to # +io_or_type+ # # tmpl = Template.new "...." # tmpl.set_trace(STDOUT) # puts tmpl.render_with(...) def set_trace(io_or_type, &block) @tracer = Tracer.new(io_or_type, &block) end def setup setup_opt @preprocessor = PreProcessor.new(@opt) cg = CodeGenerator.new preprocessed_text = @preprocessor.process(@text) if bc = @preprocessor.sections[:BeforeCompile] self.instance_eval do eval bc end end filter_setup do |e, src, filters| filters.unshift Trace.new if @tracer and @tracer.auto_trace? filters.unshift InlineRuby.new if @inline_ruby end compile(cg, preprocessed_text) @setuped = true cg.result ensure @tracer.code(cg) if @tracer and @tracer.code_trace? end # render template with +value+ and +binding+. # use TOPLEVEL_BINDING if +binding+ is nil. # def render_with(value, binding_=nil) setup unless @setuped unless binding_ if value.kind_of?(Binding) binding_ = value else binding_ = binding end end ret = nil if @opt[:process_xhtml] ret = "" ret << @root.xml_decl.to_s << "\n" if @root.xml_decl ret << @root.doctype.to_s << "\n" if @root.doctype ret << @cls.new.render_with(value, binding_) else ret = @cls.new.render_with(value, binding_) end SanitizedString[ret] end private def setup_opt @opt[:am_src] = @opt[:amrita_src] || @amrita_prefix + "src" @opt[:am_filter] = @opt[:amrita_filter] || @amrita_prefix + "filter" @opt[:am_skipif] = @opt[:amrita_skipif] || @amrita_prefix + "skipif" @opt[:am_for] = @opt[:amrita_for] || @amrita_prefix + "for" @opt[:am_v] = @opt[:amrita_v] || @amrita_prefix + "v" @opt[:tracer] = @tracer @opt[:compiletime_binding] = @opt[:compiletime_binding] || binding end def compile(cg, text) @elements = Hpricot.make(text, :xml=>true) @root = RootElement.new @elements, opt, &@filter_proc @root.text_domain = @text_domain if @text_domain @root.compile(cg) @cls = Class.new ############################################################################## # very dirty hack # ruby-gettext does not work well with class without a name # #const_name = "T%08x" % Thread.current.object_id const_name = "T%08x" % @cls.object_id Amrita2.module_eval { remove_const(const_name) } if Amrita2.const_defined?(const_name) Amrita2.const_set(const_name, @cls) # This should be modified later ############################################################################## @cls.module_eval cg.result @cls.const_set(:Tracer, @tracer) if @tracer rescue SyntaxError raise RuntimeError,$! end def setup_filter_proc old_proc = @filter_proc @filter_proc = proc do |e, name ,filters| filters.unshift Trace.new if @tracer and @tracer.auto_trace? filters.unshift InlineRuby.new if @inline_ruby old_proc.call(e, name ,filters) if old_proc end end # ment to be used in macro.rb and BeforeCompile section def filter_setup(&block) old_proc = @filter_proc @filter_proc = proc do |e, name ,filters| block.call(e, name, filters) old_proc.call(e, name, filters) if old_proc end end class Tracer def initialize(io_or_type, &block) @auto_trace = false @trace_proc = block case io_or_type when :code, :element, :all @type = io_or_type raise "need block" unless @trace_proc when :all_element @type = :element @auto_trace = true raise "need block" unless @trace_proc else if io_or_type.respond_to?(:<<) @type = :all @auto_trace = true @trace_proc = proc do |msg| io_or_type << msg << "\n" end else raise "unknown type for trace #{io_or_type.inspect}" end end end def auto_trace? @auto_trace end def code_trace? @type == :code or @type == :all end def code(cg) @trace_proc.call(cg.result) if code_trace? end def <<(msg) @trace_proc.call(msg) if @type == :all or @type == :element end end end class PreProcessor # :nodoc: all attr_reader :sections CDataStart = %r[(.*?)(<!\[CDATA\[)(.*)]m CDataEnd = %r[(.*?)(\]\]>)(.*)]m def initialize(opt={}) @opt = opt init() @trace = false #@trace = true end def process(s) init ret = process_block(s) ret = process_erb_and_inline_amxml(ret) ret << pop_tag(0) ret end def process_erb_and_inline_amxml(s) if s =~ CDataStart out_of_cdata, cdata, in_cdata = $1, $2, $3 process_out_of_cdata(out_of_cdata) + cdata + parse_in_cdata(in_cdata) else process_out_of_cdata(s) end end def process_block(s) ret = "" lines = s.to_a while line = lines.shift next if line =~ /^\s*$/ current_indent = line[/^ */].size ret << pop_tag(current_indent) case line when %r[^\s*#] # It's a comment. do nothing when AmXml::R_AMXML_BLOCK_START amxml = $1 while true l = lines.shift case l when %r[(.*)<(?: |-)*$] amxml << $1 break when nil raise "incomplete amxml block start #{amxml}" else amxml << l end end a = AmXml.parse(amxml, @opt) raise "illeagal amxml multi line #{amxml.inspect}" unless a push_tag(current_indent, a.tag) ret << a.result(false) when AmXml::R_AMXML_ERB_LINE mark, src = $1,$2 ret << "<![CDATA[<% " while true case mark when '=' ret << "\n _erbout.concat(eval(%{#{src}}).to_s) " when String ret << "\n#{src}" else break end case l = lines.shift when AmXml::R_AMXML_ERB_LINE mark, src = $1,$2 else mark = nil lines.unshift l end end ret << "\n%>]]>\n" when AmXml::R_AMXML_BLOCK_LINE a = AmXml.parse($1, {:trline=>$2.include?("-")}.merge(@opt)) raise "illeagal amxml block line #{line.inspect}" unless a ret << a.result(false) push_tag(current_indent, a.tag) when %r[(.*)^\s*>>>\s*$]m ret << $1 raise "unmatched >>> " unless @stack.size > 0 ret << pop_tag(current_indent) when %r[^\s*\|\s*([\w:]*)\s*\|\|] lp = LineProcessor.new do |cell_text| PreProcessor.new.process_block(cell_text) end while lp.parse_line(line) line = lines.shift end lines.unshift(line) unless line =~ /^( |-)*$/ ret << lp.get_result_xml ret else ret << line end end ret << pop_tag(0) ret end private def init @sections = {} @stack = [] end def process_out_of_cdata(s) erb_nest=0 ret = "" while s.size > 0 puts "process_out_of_cdata:#{erb_nest}:#{@stack.join('/')}:#{s}" if @trace case s when %r[\A(.*?)(<%|%>)((?=.?))]m case $2 when "<%" ; erb_nest +=1 when "%>" ; erb_nest -= 1 end if $2 == "<%" and erb_nest == 1 if Regexp.last_match.post_match[0] == "("[0] and s =~ %r[\A(.*?)(<%\((\w+?)\))]m ret << process_inline_amxml($1) s = process_section($3.intern, Regexp.last_match.post_match, 1) erb_nest -= 1 else ret << process_inline_amxml($1) << "<![CDATA[<%" s = Regexp.last_match.post_match end elsif $2 == "%>" and erb_nest == 0 ret << $1 << $2 << "]]>" s = Regexp.last_match.post_match else ret << $1 << $2 s = Regexp.last_match.post_match end else ret << process_inline_amxml(s) s = "" end end ret end def parse_in_cdata(s) if s =~ CDataEnd in_cdata, cdata_end, out_of_cdata = $1, $2, $3 in_cdata + cdata_end + process_erb_and_inline_amxml(out_of_cdata) else raise "end of cdata not found in #{s}" end end def process_section(section_id, s, nest) puts "%s:%d:%s" % [section_id, nest, s] if @trace @sections[section_id] ||= "" case s when %r[\A(.*?)(<%|%>)(.*)\Z]m case $2 when "<%" @sections[section_id] << $1 << $2 s = process_section(section_id, $3, nest+1) when "%>" @sections[section_id] << $1 if nest == 1 $3 else @sections[section_id] << $2 s = process_section(section_id, $3, nest-1) end else raise "can't happen" end else raise "can't happen" end end def process_inline_amxml(s) puts "process_inline_amxml:%s" % s if @trace ret = "" s.to_s.scan(%r[(.*?)<<(.*?)>>]m) do |pre, amxml| ret << $1 << parse_inline_amxml(amxml) end if Regexp.last_match ret + Regexp.last_match.post_match else s end end def parse_inline_amxml(s) a = AmXml.parse(s, @opt) raise TemplateError, "illeagal inline amxml #{s}" unless a a.result(true) end def push_tag(indent, tag) @stack.unshift([indent, tag]) end def pop_tag(indent) ret = "" while @stack.size > 0 and @stack.first[0] >= indent tag = @stack.shift[1] if tag == :cdata ret << "]]>" else ret << "</#{tag}>" end end ret end class AmXml attr_reader :tag, :attr, :src_name, :filters attr_reader :skipif, :skipunless, :for, :value # Regexps borrowed from REXML rexml/baseparser.rb NCNAME_STR= '[\w:][\-\w\d.#]*?' NAME_STR= "(?:#{NCNAME_STR}:)?#{NCNAME_STR}" R_TAG_ATTR = %r[\s*(#{NAME_STR})((?:\s*#{NAME_STR}\s*=\s*(["'])(?:.*?)\3)*)]um #" S_NO_SRC = %[\s*] S_SRC_AND_FILTERS = %q[(:([\\w\\d]*))\\s*(\\|.*?)*] S_FOR = %q[( \\[(.+)\\] )] S_COND = %q[\\?(\\!)?\\[(.+)\\]] S_VALUE = %q[\\{(.+)\\}] S_TRAILER = %q[((?: |\\-)*)] S_ERB_IN_AMXML = %q[%=(.*)] S_AMVARS = %[(#{S_FOR}|#{S_COND}|#{S_VALUE}|#{S_ERB_IN_AMXML})?] R_AMXML_WITHOUT_TAG = %r[\A\s*(?:\/)?\s*(?:#{S_NO_SRC}|#{S_SRC_AND_FILTERS})*\s*#{S_AMVARS}\s*\Z]um R_AMXML = %r[\A#{R_TAG_ATTR}\s*(?:\/)?\s*(?:#{S_NO_SRC}|#{S_SRC_AND_FILTERS})*\s*#{S_AMVARS}\s*\Z]um R_AMXML_BLOCK_LINE = %r[^\s*<<\s*?(.*)\s*?<#{S_TRAILER}$] R_AMXML_BLOCK_START = %r[^\s*<<\s*((#{R_TAG_ATTR})?[^<>]*)\s*$] R_AMXML_ERB_LINE = %r[^\s*\%([ =])(.*)$] def self.parse(s, opt={}) self.new(opt).parse(s, opt[:trline]) end def initialize(opt) @opt = opt @tag = "_" @attr = "" @src_name = @filters = nil @skipif = @skipunless = @for = @value = nil end def has_amxml_attrs? @src_name or @filters or @skipif or @skipunless or @for or @value end def parse(s, trline=false) case s when R_AMXML_WITHOUT_TAG if trline @tag = "tr" else @tag = "_" end @src_name = $2.strip if $2 @filters = $3[1..-1].strip if $3 parse_am_vars($4) when %r[\A\s*([\w]+):([\-\w\d.]*)\s*(/?)\s*\Z] # <<aaa:xxx>> if $3 == "/" @tag = "#$1:#$2" else @tag, @src_name = $1, $2 end @tag = "_" if @tag == nil or @tag == "" @attr = "" when R_AMXML @tag, @attr = $1.to_s, $2.to_s src_start = $4 @src_name = $5 @filters = $6[1..-1].strip if $6 parse_am_vars($7) when "%" @tag = :cdata when /=\s*(\w+)\s*/ @tag = [:put_stream,$1.intern] when /\s*(\w+)\s*=/ @tag = [:change_stream,$1.intern] else return nil end parse_css_selecter_like self end def parse_css_selecter_like while true case @tag when /^(\w+)([#\.])([\w-]+)(.*)$/ @tag, cls = $1 + $4, $3 case $2 when "." @attr += " class=\"#{$3}\"" when "#" @attr += " id=\"#{$3}\"" else break end else break end end end def parse_am_vars(s) case s when nil # do nothing when %r[#{S_FOR}]m @for = $2.strip when %r[#{S_COND}]m if $1 == "!" @skipif = $2.strip else @skipunless = $2.strip end when %r[#{S_VALUE}]m @value = $1.strip when %r[#{S_ERB_IN_AMXML}]m @erb = $1.strip else raise "can't happen #{s}" end end def result(single_tag) case @tag when :cdata return "<![CDATA[" when Array case @tag[0] when :change_stream ret = "<_ am:filter='ChangeStream[#{@tag[1].inspect}]'" when :put_stream ret = "<_ am:filter='PutStream[#{@tag[1].inspect}]'" else raise "can't happen #{@tag.inspect}" end if single_tag ret << " />" else ret << " >" @tag = "_" end return ret end ret = "<" if @tag ret << @tag else ret << "_" end if @src_name and @src_name.size > 0 am_src = @opt[:am_src] || "am:src" if @filters ret << make_attr(am_src, "#@src_name|#@filters") else ret << make_attr(am_src, @src_name) end else if @filters am_filter = @opt[:am_filter] || "am:filter" ret << make_attr(am_filter, @filters) end end if @for am_for = @opt[:am_for] || "am:for" ret << make_attr(am_for, @for) end if @skipif am_skipif = @opt[:am_skipif] || "am:skipif" ret << make_attr(am_skipif, @skipif) end if @skipunless am_skipif = @opt[:am_skipunless] || "am:skipif" ret << make_attr(am_skipif, "not(#@skipunless)") end if @value am_v = @opt[:am_v] || "am:v" ret << make_attr(am_v, "HashDelegator.new($_) { {#@value} }") end ret << @attr.to_s if @erb if single_tag ret << "><![CDATA[<%= #{@erb} %>]]></#@tag>" else raise "not implemented" end else if single_tag ret << " />" else ret << " >" end end ret end def make_attr(key, val) if val.include?(?') " #{key}=\"#{val}\"" else " #{key}='#{val}'" end end end class LineProcessor attr_reader :cells def initialize(&block) @cells = [] @cell_proc = block end def parse_line(line) case line when %r[^\s*#] true when %r[^\s*\|\s*([\w:]*)\s*\|\|] parse_cells($1.strip, Regexp.last_match.post_match) true else false end end def parse_cells(attr_key, cells) attr_key = :text if attr_key == "" cnt = 0 splited = "" cells.chomp.scan(%r[([^\|]+?)((?!r\|)\|(\|?)|$)]) do |s, th_flag| if s[-1] == ?\\ splited = s[0..-2] + "|" next else s = splited.to_s + s splited = "" end @cells[cnt] ||= {} cell = @cells[cnt] if cell[attr_key] cell[attr_key] << "\n" << s else cell[attr_key] = s end cell[:tag] = (th_flag == "||" ? :th : :td) cnt += 1 end end def get_result_xml @cells.collect do |c| tag, text = c[:tag], @cell_proc.call(c[:text]) c.delete(:tag) c.delete(:text) if c.size == 0 if text == nil or text.strip == "" "" else "<#{tag}>#{text}</#{tag}>" end else a = attr = c.collect do |k, v| next unless v v.strip! next if v == "" if v.include?(?') "#{k}=\"#{v}\"" else "#{k}='#{v}'" end end.join(" ") "<#{tag} #{a}>#{text}</#{tag}>" end end.join("") end end end class Hook attr_reader :stream, :cnt, :element_s def initialize(&block) @hook_proc = block end def call(stream, cnt, element_s,&block) @stream = stream @cnt = cnt @element_s = element_s @render_me_proc = block instance_eval(&@hook_proc) end def render_me_with(it) stream << @render_me_proc.call(nil, it) end def render_child(name, it) stream << @render_me_proc.call(name, it) end class Renderer < Renderers::Base # :nodoc: all def initialize super("Hook") end def generate_body(cg, de, element) cg.code("e = #{ cg.define_constant(element.to_s.inspect)}") cg.code("$_.call(__stream__, __cnt__, e) do |name, it| ") cg.level_up do cg.case_('name') do cg.when_("nil") do cg.code("render_with(it, __binding__)") end de.children.each do |c| next unless c.kind_of?(DynamicElement) cg.when_(c.name.intern.inspect) do cg.code("#{c.instance_name}.render_with(it, __binding__)") end end end end cg.code("end") end end end end module Renderers # :nodoc: all class Base def initialize(type_name) @type_name = type_name end def generate_when(cg) cg.when_(@type_name) do yield end end end class ScalarRenderer < Renderers::Base def initialize super("ScalarData") end def generate_body(cg, de, element) de.element_render_code(cg, element) do cg.put_string_expression("$_.amrita_value") end end end class NilRenderer < Base def initialize super("nil") end def generate_body(cg, de, element) end end class TrueRenderer < Base def initialize super("true") end def generate_body(cg, de, element) cg.put_hpricot_node(element) end end class HashRenderer < Base def initialize super("DictionaryData") end def generate_body(cg, de, element) de.element_render_code(cg, element) do de.children.each do |c| c.render_me(cg) end end end end class ElseRenderer < Base def initialize super("Object") end def generate_when(cg) cg.else_ do yield end end def generate_body_new(cg, de, element) de.element_render_code(cg, element) do de.children.each do |c| c.render_me(cg) end end end def generate_body(cg, de, element) if de.has_ruby? de.element_render_code(cg, element) do de.children.each do |c| c.render_me(cg) end end else cg.put_expression('raise %[type mismatch, got (#{$_.inspect}) for ' + element.to_s.inspect + ' ]') end end end end module Filters include Renderers class FilterArray < Array # :nodoc: all def initialize(*args) args.each { |a| self << a } end def |(other) case other when Class a = self + [other.new] FilterArray.new(*a) when Filters::Base a = self + [other] FilterArray.new(*a) when Symbol self + FilterArray.new(FunctionFilter.new(other)) when Array case other.first when Symbol self + FilterArray.new(FunctionFilter.new(*other)) else self + other end else raise "not filter #{other.inspect}" end end end class Base # :nodoc: all attr_accessor :next_ def self.filter_method(*m) m.each do |name| module_eval <<-END def #{name}(*args,&block) next_.#{name}(*args,&block) end END end end FilterMethods.each do |m| filter_method(m) end def self.inherited(cls) super def cls.[](*args, &block) new(*args, &block) end def cls.|(other) FilterArray.new(self.new) | other end end def |(other) FilterArray.new(self) | other end def parse_filter_a(f) case f = eval(f) when Class f.new else f end end end class Attr < Base class Renderer < Renderers::Base include Util include OptionSupport def initialize(*args) parse_opt(*args) end def generate_body(cg, de, element) use_body = (not de.has_dynamic?) && @opt.has_key?(:body) body_key = nil if use_body body_key = @opt[:body] body_key = :body unless body_key.kind_of?(Symbol) cg.code("body = $_.amrita_value(#{body_key.inspect})") end if @opt.size > 0 cg.code("a = {}") @opt.each do |key, v| next if key == :body if element.get_attribute(key) cg.case_("v = $_.amrita_value(#{v.inspect})") do cg.when_("true") do cg.code("a[#{key.inspect}] = #{element.get_attribute(key).inspect}") end cg.when_("false, nil") do cg.code("a.delete(#{key.inspect})") end cg.else_ do cg.code("a[#{key.inspect}] = $_.amrita_value(#{v.inspect})") end end else cg.code("v = $_.amrita_value(#{v.inspect})") cg.code("a[#{key.inspect}] = v if v") end end else cg.code("a = $_ ") end a = { } element.attributes.each do |k, v| next if @opt[k.intern] a[k] = v end cg.code("e = new_element(#{element.name.inspect}, #{a.inspect}.merge(a))") if use_body body_for_static(cg) else body_for_dynamic(cg, de, element) end end def body_for_static(cg) cg.if_("body") do cg.put_string_expression("start_tag(e)") cg.put_string_expression("body.amrita_value") cg.put_string_expression("end_tag(e)") cg.else_ do cg.put_string_expression("e.to_s") end end end def body_for_dynamic(cg, de, element) cg.put_string_expression("start_tag(e)") de.children.each do |c| c.render_me(cg) end cg.put_string_expression("end_tag(e)") end end def initialize(*args) @args = args end def setup_type_renderer(de) de.renderers.clear de.renderers << Renderer.new(*@args) end end class Default < Base def initialize(default_value_src) @default_value_src = default_value_src end def value_filter_code(de, cg, value) cg.if_("$_") do super cg.else_ do cg.code("$_ = (#{@default_value_src.inspect})") end end end def value_filter_code_old(de, cg, value) cg.code("$_ = $_ || (#{@default_value_src.inspect})") super end end class Repeat < Base def initialize(cnt) @cnt = cnt.to_i end def value_filter_code(de, cg, value) cg.code("$_ = $_ * #@cnt ") super end end class Format < Base def initialize(format) @format = format end def value_filter_code(de, cg, value) cg.code("$_ = #{@format.inspect} % $_") super end end class AcceptData < Base include Renderers def initialize(*types) @types = types end def setup_type_renderer(de) nil_processed = true_processed = hook_processed = false @types.each do |t| case t when nil, NilClass de.renderers.unshift NilRenderer.new unless nil_processed nil_processed = true when true, TrueClass de.renderers << TrueRenderer.new unless true_processed true_processed = true when :hook de.renderers << Core::Hook::Renderer.new unless hook_processed hook_processed = true else raise ArgumentError,"unknown type for AcceptData #{t.inspect}" end end super end end class NoSanitize < Base def value_filter_code(de, cg, value) super cg.code("$_ = SanitizedString[$_]") end end class Each < Base include Util::OptionSupport def initialize(*args) parse_opt(*args) end def value_filter_code(de, cg, value) var = "__cnt__#{de.class_name}" @opt.each do |k,va| cg.code("#{var} = __cnt__%#{va.size}") va.each_with_index do |v, n| case v when Symbol cg.code("$_[#{k.inspect}] = $_[#{v.inspect}] if #{var} == #{n}") when String cg.code("$_[#{k.inspect}] = #{v.inspect} if #{var} == #{n}") else raise("unknown Each type #{v.inspect}") end end end super end end class ToHash < Base include Util::OptionSupport def initialize(key) @key = key end def value_filter_code(de, cg, value) case @key when Symbol cg.code("$_ = { #{@key.inspect} => $_ }") when Hash cg.code("$_ = { ") @key.each do |k,v| cg.code(" #{k.inspect} => $_.amrita_value(#{v.inspect}),") end cg.code("}") end super end end class InlineRuby < Base # :nodoc: all include Runtime include Renderers include Util def setup_type_renderer(de) de.renderers << HashRenderer.new unless de.renderers.find {|r| r.kind_of?(HashRenderer) } super end def generate_dynamic_element(de, e) a = e.attributes ret = super return ret if ret # check if element has any of inlineruby attributes if %w(am_skipif am_for am_v).any? { |k| a[de.__send__(k)] } filters_src = e.attributes[de.am_filter] filters = de.root.compile_filters(e, nil, filters_src) Core::DynamicElement.new(de, nil , e, filters) else nil end end def loop_check_code(de, cg) foreach_ = de.am_for_value if foreach_ cg.if_("__cnt__ < 0") do #cg.code("$_ = eval(#{foreach_.inspect}, __binding__)") cg.code("$_ = ") cg.eval(foreach_.inspect) super end else super end end def value_filter_code(de, cg, value) cond = de.am_skipif_value value = de.am_v_value if cond cg.code("amrita_set_context_value($_)") #cg.code("return '' if eval(#{with_context_value_expression(cond).inspect},__binding__)") cg.code("return '' if \\") cg.eval(with_context_value_expression(cond).inspect) end if value cg.code("amrita_set_context_value($_)") #cg.code("$_ = eval(#{with_context_value_expression(value).inspect},__binding__)") cg.code("$_ = ") cg.eval(with_context_value_expression(value).inspect) end super end private def with_context_value_expression(src) [ "$_ = amrita_get_context_value", "ret = (#{src})", "amrita_set_context_value($_)", "ret" ].join(";") end end module NVarMixin private def make_tupple_code(cg) init_code = @names.collect do |n| "$_.amrita_value(#{n.inspect})" end.join(",") cg.code("$_ = Tuple[#{init_code}]") end def replace_args(element) element.attributes.each do|k,v| element.set_attribute(k, replace_attr_args(v)) end children = element.children.collect do |c| case c when Hpricot::Elem replace_args(c) when Hpricot::Text Hpricot::Text.new(replace_text_args(c.to_s)) else raise "not implemented #{c.class}" end end element.children.clear children.each do |c| element.children << c end element end def replace_attr_args(s) s.gsub(/(.)?\$(\d)/) do |ss| if $1 == "$" "$#$2" else $1.to_s + '#{Util::sanitize_attribute_value($_[' + ($2.to_i-1).to_s + '].to_s)}' end end end def replace_text_args(s) s.gsub(/(.)?\$(\d)/) do |ss| if $1 == "$" "$#$2" else $1.to_s + '#{$_[' + ($2.to_i-1).to_s + '].amrita_value}' end end end end class NVar < Filters::Base include NVarMixin def initialize(*names) @names = names end def renderer_code(de, cg, element) make_tupple_code(cg) if @names.size > 0 s = replace_args(element).to_s cg.put_string_expression(s.inspect.gsub(/\\#/, "#")) end end class Eval < NVar def initialize(*names) @names = names end private def make_tupple_code(cg) cg.code("amrita_set_context_value($_)") init_code = @names.collect do |n| case n when Symbol "$_.amrita_value(#{n.inspect})" else "eval(#{with_context_value_expression(n).inspect},__binding__)" end end.join(",") cg.code("$_ = Tuple[#{init_code}]") end def with_context_value_expression(src) [ "$_ = amrita_get_context_value", "ret = (#{src})", "amrita_set_context_value($_)", "ret" ].join(";") end end class NVarForAttr < Filters::Base def initialize(*names) @names = names end def renderer_code(de, cg, element) make_tupple_code(cg) if @names.size > 0 super end def element_render_code(de, cg, element, &block) if (element.name == 'span' && element.attributes.size == 0) yield else replace_args(element) if element.children.size > 0 cg.put_string_expression(element.stag.output('').inspect.gsub(/\\#/, "#")) yield cg.put_constant(element.etag.output('')) else cg.put_string_expression(%["#{element.to_s}"]) end end end private def make_tupple_code(cg) init_code = @names.collect do |n| "$_.amrita_value(#{n.inspect})" end.join(",") cg.code("a = [#{init_code}]") end def replace_args(element) element.attributes.each do|k,v| element.set_attribute(k, replace_attr_args(v)) end end def replace_attr_args(s) s.gsub(/\$(\d)/) do |ss| '#{Util::sanitize_attribute_value(a[' + ($1.to_i-1).to_s + '].to_s)}' end end end class FunctionFilter < Filters::Base def initialize(sym, *args) @sym, @args = sym, args end def value_filter_code(de, cg, element) cg.code("$_ = $_.send(#{@sym.inspect}, *#{@args.inspect})") super end end class CommandFilter < Filters::Base def initialize(*args) @args = args.collect do |a| a.inspect end.join(' ') end def define_element_method(de, cg, &block) super do block.call cg.code "pipe = IO.popen(#@args, 'r+')" cg.code "pipe.write __stream__; pipe.close_write" cg.code "__stream__ = pipe.read ; pipe.close " end end end class Trace < Filters::Base SEP = "\n" + "-" * 80 + "\n" def filter_element(de, element) if de.tracer old_s = element.to_s new_element = super new_s = new_element.to_s if old_s != new_s de.tracer << SEP << "before filter:\n" << old_s << SEP << "after filter :\n" << new_s << SEP end new_element else super end end def value_filter_code(de, cg, value) s = de.element.stag.output("") cg.code(%[Tracer << "%s:%s:%s" % [#{s.inspect},"in", $_.inspect] ]) super end def method_body_code(de, cg) super s = de.element.stag.output("") cg.code(%[Tracer << "%s:%s:%s" % [#{s.inspect},"out", __stream__] ]) end end class Join < Base def initialize(sep) @sep = case sep when :br ; "<br />" when :nbsp ; " " else ; sep end end def filter_element(de, e) a = e.children.dup a.each do |n| case n when Hpricot::CData, Hpricot::Elem e.insert_after(Hpricot::Text.new(@sep), n) unless n == a.last when Hpricot::Text e.insert_after(Hpricot::Text.new(@sep), n) unless n == a.last n.content.strip! n.content.gsub!(/\s*\n\s*/, @sep) end end e end end class ChangeStream < Base def initialize(name) @name = name end def method_body_code(de, cg) cg.code("#ChangeStream") cg.with_stream(@name) do super end end end class PutStream < Base def initialize(name) @name = name end def method_body_code(de, cg) cg.code("#PutStream") cg.put_stream(@name) end end class ModuleExtendFilter< Base def initialize(mod) @mod = mod end def value_filter_code(de, cg, element) cg.code("$_.extend(#@mod)") super end end end class HashDelegator include DictionaryData def initialize(parent, &block) @parent = parent @added_data = block.call raise 'block did not return a Hash' unless @added_data.kind_of?(DictionaryData) end def [](key) self.send(key) rescue NoMethodError nil end def id @parent.id end def method_missing(sym, *args, &block) if @added_data.has_key?(sym) @added_data[sym] else if @parent.kind_of?(DictionaryData) @parent.amrita_value(sym) else @parent.send(sym) end end end end SanitizedString = Util::SanitizedString Template = Core::Template Hooki = Core::Hook end