# -*- Mode: ruby; indent-tabs-mode: nil -*- # # $Id: expander.rb,v 1.24 2004/02/06 05:54:25 hisa Exp $ # # Copyright (c) 2003 FUJIMOTO Hisakuni # # This program is free software. # You can distribute/modify this program under the terms of # the GNU Lesser General Public License version 2. # require 'rexml/document' require 'tempura/charconv' module Tempura class Expander attr_reader :safe_level attr_accessor :default_action attr_accessor :default_event_key def self.pre_expand(rexml_element, bang_p = false) elm = bang_p ? rexml_element : rexml_element.deep_clone elm.each_element_with_attribute('_nil_') do |child| _expand_nil(elm, child) end elm.each_element do |child| pre_expand( child, true ) end return elm end def pre_expand(rexml_element, bang_p = false) self.class.pre_expand(rexml_element, bang_p) end def self.url_encode(s) s.to_s.gsub(/([^ a-zA-Z0-9_.-]+)/n) do '%' + $1.unpack('H2' * $1.size).join('%').upcase end.tr(' ', '+') end def url_encode(s) self.class.url_encode(s) end def initialize(safe_level = nil) @safe_level = safe_level @param_filter = nil @action_filter = nil @default_action = "" @default_event_key = "event" end def expand(context, rexml_element, charconv, bang_p = false) charconv = charconv || Tempura::CharConvDefault elm = bang_p ? rexml_element : rexml_element.deep_clone return _expand_main( elm, _tempura_context(context), charconv ) end def expand_to_string(context, rexml_element, charconv, bang_p = false) charconv = charconv || Tempura::CharConvDefault elm = expand( context, rexml_element, charconv, bang_p ) result = "" elm.write( result ) return charconv.from_u8( result ) end def param_filter(&proc) @param_filter = proc end def action_filter(&proc) @action_filter = proc end def default_param_filter( evt, param, bind ) param[@default_event_key] = evt return param end def default_action_filter( evt, param, bind ) return @default_action end ########## protected def _handle_expand_event(elm, action, param) case elm.name.downcase when 'a' then ary = param.map { |key,val| "#{url_encode(key)}=#{url_encode(val)}" } elm.attributes['href'] = "#{action}?#{ary.join(';')}" when 'form' then elm.attributes['method'] = 'POST' if elm.attributes['method'].nil? elm.attributes['action'] = action param.each do |key,val| elm.push( _new_input_element(key, val) ) end end end #### def _expand_block(_elm_, _attr_, _bind_, _charconv_) _rcv_expr_, _method_, _args_ = _elm_.attributes[_attr_].split('//') _elm_.delete_attribute( _attr_ ) _proc_expr_ = "proc do |#{_args_}| _next_elm_ = _elm_.deep_clone _expand_main( _next_elm_, binding, _charconv_ ) _elm_.parent.insert_before( _elm_, _next_elm_ ) end" _proc_ = _tempura_eval( _proc_expr_, binding ) _rcv_ = _tempura_eval( _rcv_expr_, _bind_ ) _rcv_.send(_method_, &_proc_) _elm_.parent.delete_element( _elm_ ) end def _expand_child_main(elm, attr, bind, charconv, xml_parse_p) val = elm.attributes[ attr ] val = _tempura_eval( val, bind ) if val then elm.delete_if { true } if val.is_a? REXML::Child then val = val.root if val.is_a? REXML::Document elm.add( val ) else if xml_parse_p then str = "#{charconv.to_u8(val.to_s)}" REXML::Document.new(str).root.each do |item| elm.add( item ) end else val = REXML::Text.new( charconv.to_u8(val.to_s) ) elm.add( val ) end end elm.delete_attribute( attr ) elsif val.nil? then elm.remove else elm.delete_attribute( attr ) end return (val != false) end def _expand_child(elm, attr, bind, charconv) _expand_child_main( elm, attr, bind, charconv, false ) end def _expand_child_xml(elm, attr, bind, charconv) _expand_child_main( elm, attr, bind, charconv, true ) end def _expand_self(elm, attr, bind, charconv) if _expand_child_main( elm, attr, bind, charconv, false ) then elm.name = "__delete_me__" end end def _expand_self_xml(elm, attr, bind, charconv) if _expand_child_main( elm, attr, bind, charconv, true ) then elm.name = "__delete_me__" end end def _expand_self_bool(elm, attr, bind, bool) val = elm.attributes[ attr ] val = _tempura_eval( val, bind ) del_p = (bool && !val) || (!bool && val) elm.delete_if { del_p } elm.name = '__delete_me__' end def _expand_attribute(elm, attr, new_attr, bind, charconv) val = elm.attributes[ attr ] val = _tempura_eval( val, bind ) elm.delete_attribute( attr ) if !new_attr.empty? then case val when nil then elm.delete_attribute( new_attr ) when false then ; # nop when REXML::Child then elm.add_attribute( new_attr, val.to_s ) else elm.add_attribute( new_attr, charconv.to_u8(val.to_s) ) end end end def _expand_event(elm, attr, bind, charconv) evt, param = _parse_event_value( elm.attributes[attr], bind, charconv ) elm.delete_attribute( attr ) param = _array_to_hash( param ) if elm.attributes['_action_'] then action = elm.attributes['_action_'] elm.delete_attribute('_action_') else action = (@action_filter && @action_filter.call(evt, param, bind)) || default_action_filter(evt, param, bind) end param = (@param_filter && @param_filter.call(evt, param, bind)) || default_param_filter(evt, param, bind) _handle_expand_event(elm, action, param) end def self._expand_nil(parent, elm) parent = elm.parent if parent.nil? child = elm index = parent.index( child ) next_child = parent[ index + 1 ] if next_child.is_a? REXML::Text and next_child.to_s == "\n" then parent.delete( next_child ) end parent.delete_element( child ) end def _expand_nil(parent, elm) self.class._expand_nil(parent, elm) end ########## def _expand_main(elm, bind, charconv) attr = elm.attributes if attr['_block_'] then _expand_block( elm, '_block_', bind, charconv ) else attr.keys.each do |attr| case attr when '_if_' then _expand_self_bool( elm, attr, bind, true ) when '_unless_' then _expand_self_bool( elm, attr, bind, false ) when '_child_' then _expand_child( elm, attr, bind, charconv ) when '_child_xml_' then _expand_child_xml( elm, attr, bind, charconv ) when '_self_' then _expand_self( elm, attr, bind, charconv ) when '_self_xml_' then _expand_self_xml( elm, attr, bind, charconv ) when /^_attr_(.*)$/ then _expand_attribute( elm, attr, $+, bind, charconv ) when '_event_' then _expand_event( elm, attr, bind, charconv ) when '_nil_' then _expand_nil( nil, elm ) end end # traverse into children if elm.has_elements? then elm.each_element { |child| _expand_main( child, bind, charconv ) } end # remove span tag without attribute name = elm.name.downcase if (name == 'span' && !elm.has_attributes?) || (name == '__delete_me__') then parent = elm.parent elm.to_a.each do |child| parent.insert_before(elm, child) if parent end elm.remove end end return elm end ####### def _tempura_context( _context_ ) if _context_.is_a? Binding then return _context_ else return _context_.instance_eval { binding } end end def _parse_event_value(attr_value, bind, charconv) event, *args = attr_value.split('//') event.strip! args.map! { |entry| key, val = entry.split('==>') key.strip! val = _tempura_eval( val, bind ) val = charconv.to_u8(val.to_s) unless val.nil? [ key, val ] } return event, args end def _array_to_hash(ary) param = {} ary.each { |k,v| param[k] = v } return param end def _new_input_element(name, value) elm = REXML::Element.new('input') elm.attributes['type'] = 'hidden' elm.attributes['name'] = name elm.attributes['value'] = value return elm end # eval wrapper def _tempura_eval( _expr_, _bind_ ) _fname_ = (_expr_.strip)[0,36].inspect if @safe_level then return Thread.start { $SAFE = @safe_level eval(_expr_, _bind_, _fname_) }.value else return eval(_expr_, _bind_, _fname_) end end end # class Expander end