# -*- ruby -*- # # This is not W3C XPath! # require 'xtemplate' module XTemplate module XPath include Util PathSeparator = '/' PathUnion = '|' PathIfNot = '?' AnyNode = '*' AnyNode0 = '' AnyNode2 = '**' TextNode = nil RootNode = "\000" CurrentNode = "." ParentNode = ".." class Action include XPath include Util def _alt_exist?(name) @alts ||= {} @alts.key?(name) end def _alt(name) @alts ||= {} @alts[name] = true end def _unalt(name) @alts.delete(name) end def alt(val, *args) args.each{|arg| if( _alt_exist?(arg) ) val = nil else _alt(arg) end } val end def unalt(val, *args) args.each{|arg| _unalt(arg) } val end def attr(val, *args) case val when Hash newval = val.dup args.each{|a| a.strip! if( v = newval[a] ) newval.delete(a) newval['@'+a] = v end } newval when Array val.collect{|v| attr(v, *args)} else nil end end def unattr(val, *args) case val when Hash newval = val.dup args.each{|a| a.strip! k = '@'+a if( v = newval[k] ) newval.delete(k) newval[a] = v end } newval when Array val.collect{|v| unattr(v, *args)} else nil end end def node(val) case val when Hash val.reject{|k,v| (k[0] == ?@ || k == TextNode)} when Array val.collect{|v| node(v)} else nil end end def text(val) case val when Hash val[TextNode] when Array val.collect{|v| text(v)} else val end end def copy(val, x, y) case val when Hash newval = val.dup if( v = newval[x] ) newval[y] = v end newval when Array val.collect{|v| copy(v, x, y)} else nil end end def rename(val, x, y) case val when Hash newval = val.dup if( v = newval[x] ) newval[y] = v newval.delete(x) end newval when Array val.collect{|v| rename(v, x, y)} else nil end end def tag(val, tag) case val when Hash newval = val.dup if( v = newval[TextNode] ) newval[tag] = v newval.delete(TextNode) end newval when Array val.collect{|v| tag(v, tag)} else {tag => val} end end def untag(val, tag) case val when Hash newval = val.dup if( v = newval[tag] ) newval[TextNode] = v newval.delete(tag) end newval when Array val.collect{|v| untag(v,tag)} else nil end end def sort(val, path=nil) if( val.is_a?(Array) ) if( path ) newval = val.sort{|x,y| path.split("/").each{|key| key.strip! if( x.is_a?(Hash) && y.is_a?(Hash) ) x,y = x[key], y[key] end } if( x.is_a?(Comparable) && y.is_a?(Comparable) ) x <=> y else 0 end } else newval = val.sort{|x,y| if( x && y ) x <=> y else 0 end } end else val end end def int(val) if( val[TextNode] ) newval = val.dup newval[TextNode] = newval[TextNode].to_i else newval = val.to_i end newval end def float(val) if( val[TextNode] ) newval = val.dup newval[TextNode] = newval[TextNode].to_i else newval = val.to_f end newval end def string(val) if( val[TextNode] ) newval = val.dup newval[TextNode] = newval[TextNode].to_i else newval = val.to_s end newval end def array(val) if( val.is_a?(Enumerable) ) newval = val.to_a else newval = val end newval end def strip(val) case val when Hash if( val[TextNode] ) newval = val.dup() newval[TextNode] = newval[TextNode].strip() else newval = val end when Array newval = val.reject{|v| case v when Hash, Array false else v.to_s() =~ /\A\s*\z/ end } else newval = val.to_s().strip() if( newval.size == 0 ) newval = nil end end newval end def size(val) case val when Array val.size when nil 0 else 1 end end def index(val, tag, ini=0) if( ini ) if( ini =~ /^([1-9]\d*)$/ ) ini = $1.to_i end end case val when Hash newval = val.dup newval[tag] = ini when Array newval = [] idx = ini val.each{|v| case v when Hash v = v.dup v[tag] = idx newval.push(v) else v = {TextNode => v, tag => idx} newval.push(v) end idx = idx.succ } else newval = {tag => ini, TextNode => val} end newval end def flatten(val) warn("The action 'flatten' and 'hash' will be obsoleted.") case val when Array newval = {} text = "" val.each{|elem| case elem when Hash elem.each{|k,v| newval[k] = v } else text.concat(elem.to_s) end } if( text.size > 0 ) newval[TextNode] = text end else newval = val end newval end alias hash flatten def dump(val, name=nil) if( name ) parent = XNode.new(name) else parent = XNode.new() end value_to_xml(val, parent) parent.to_s end alias __sanitize__ sanitize def sanitize(val) s = String.new(val.to_s) __sanitize__(s) end alias __unsanitize__ unsanitize def unsanitize(val) s = SanitizedString[val.to_s] SanitizedString[__unsanitize__(s)] end def reverse(val) if( val.is_a?(Array) ) val.reverse else nil end end def delete(val, *args) case val when Hash newval = val.dup args.each{|s| s.strip! if( newval[s] ) newval.delete(s) end } newval when Array val.collect{|v| delete(v, *args)} else nil end end def push(val, name) stack = ((Thread.current[:xtemplate] ||= {})[name.intern] ||= []) stack.push(val) case val when Hash {} when Array [] else nil end end def pop(val, name) stack = ((Thread.current[:xtemplate] ||= [])[name.intern] ||= []) v = stack.pop [val, v].flatten end def pop_all(val, name) stack = ((Thread.current[:xtemplate] ||= [])[name.intern] ||= []) newval = [val, stack.dup].flatten stack.clear newval end def clear_stack(val, name) stack = ((Thread.current[:xtemplate] ||= [])[name.intern] ||= []) stack.clear val end def clear(val) [] end def import(val, *args) case args[0] when /^xml:\/\/(.+)/ fn = $1 doc = File.open(fn){|f|f.read} parser = XMLParser.new() node = parser.parse(doc) node.prepare() XMLDocument.new(node).to_hash when /^data:\/\/(.+)/ fn = $1 eval(File.open(fn){|f|f.read}) when /^var:(.+)/ eval($1) when /^yaml:\/\/(.+)/ require 'xtemplate/yaml' fn = $1 File.open(fn){|f|YAMLDocument.new(f)} when /^soap:\/\/(.+)/ require 'soap/driver' args.shift endpoint = $1 namespace = args.shift if( namespace =~ /^\s*$/ ) namespace = nil end obj = SOAP::Driver.new(nil, nil, namespace, "http://#{endpoint}") m = args.shift argv = args.collect{|s| eval_expr(s,val,nil) } i = -1 obj.addMethod(m, *(argv.collect{ i+=1; "arg" + i.to_s })) obj.call(m, *argv) when /^xmlrpc:\/\/(.+)/ require 'xmlrpc/client' m = args[1] argv = args[2..-1].collect{|s| eval_expr(s,val,nil) } client = XMLRPC::Client.new2("http://#{$1}") client.call(m, *argv) when /^dbi:(.+)/ require 'dbi' h = DBI.connect("dbi:#{$1}") query = args[1..-1].collect{|s|s.gsub(/\#\{(.+)\}/){eval_expr($1,val,nil).to_s }}.join(',') newval = [] h.execute(query){|sth| while( r = sth.fetch_hash ) newval.push(r) end } newval else eval(args[0]) end end def time(val, fmt, tag=nil) if( tag ) case val when Array val.collect{|v| time(v, fmt, tag)} when Hash str = Time.now.strftime(fmt) newval = val.dup tag ||= TextNode newval[tag] = str newval else if( val ) str = Time.now.strftime(fmt) newval = { tag => str, TextNode => val, } newval else nil end end else Time.now.strftime(fmt) end end alias date time def p(val, out=nil) case out when "stdout" f = $stdout when "stderr" f = $stderr else f = $stdout end f.print(value_inspect(val),"\n") val end end # end of Action def value_inspect(val) case val when Hash val = val.dup val.delete(ParentNode) "{" + val.collect{|k,v| "#{k.inspect}=>#{value_inspect(v)}" }.join(", ") + "}" when Array "#{val.class}[" + val.collect{|v| value_inspect(v)}.join(", ") + "]" when SanitizedString val.to_s.inspect else val.inspect end end def value_depth(val) case val when Hash max = 0 val.each{|key,val| if( key == ParentNode ) next end if( (x = value_depth(val)) > max ) max = x end } max + 1 when Array max = 0 val.each{|val| if( (x = value_depth(val)) > max ) max = x end } max else 0 end end def value_p(val) puts(value_inspect(val)) end def value_to_xml(val, parent) case val when Hash val.each{|k,v| case k when ParentNode # do nothing when TextNode parent.add_child(v) when /^@(.+)/ parent.add_attr($1) parent.add_attrval(v) else node = XNode.new(k) value_to_xml(v,node) parent.add_child(node) end } when Array val = val.collect{|v| case v when Hash, Array v else {TextNode => v} end } val.each{|v| value_to_xml(v,parent) } when nil nil else parent.add_child(val) end end # also implemented in xt.c def path_split(path) i = 0 l = 0 s = 0 ids = [] path.each_byte{|c| case c when ?{, ?[ l += 1 when ?}, ?] l -= 1 when ?/ if( l == 0 ) ids.push(path[s..i].chop) s = i + 1 end end i += 1 } ids.push(path[s..i]) if( path[0] == ?/ ) ids[0] = RootNode end ids end # also implemented in xt.c def args_split(args) args = unsanitize(args) i = 0 l = false s = 0 escape = false inref = false ids = [] args.each_byte{|c| case c when ?', ?" if( escape ) escape = false else if( l ) l = false else l = true end end when ?\\ escape = true when ?, if( !l ) ids.push(args[s..i].chop) s = i + 1 end end i += 1 } ids.push(args[s..i]) ids.collect{|s| s.strip.gsub(/(\A['"])|(["']\z)/,'') }.reject{|s| s.empty? } end # also implemented in xt.c def cond_split(path) i = 0 l = 0 s = 0 xs = [] path.each_byte{|c| case c when ?{, ?[ if( l == 0 ) case i when 0 xs.push("") when s # do nothing else xs.push(path[s..(i-1)]) end s = i end l += 1 when ?}, ?] l -= 1 if( l == 0 ) xs.push(path[s..i]) s = i + 1 end end i += 1 } unless( s == i ) xs.push(path[s..i]) end xs end def xpath(path, data, plugin=nil) plugin ||= Action.new x = value_by_path2(path, data, nil, data, plugin) if( x.nil? ) nil else case x when Array x else [x] end end end # Returns an empty array instead of 'nil' to eliminate a tag. def value_by_path2(path, data, pdata, rdata, plugin) if( path.is_a?(String) ) if( path =~ /([\?\|])/ ) case $1[0] when ?| paths = path.split(PathUnion) return paths.collect{|path| path.strip! value_by_path2(path, data, pdata, rdata, plugin) }.flatten when ?? paths = path.split(PathIfNot) for path in paths path.strip! vals = value_by_path2(path, data, pdata, rdata, plugin) if( vals.nil? || vals.empty? ) next end break end return vals end end ids = path_split(path) else ids = path.dup end x = value_by_path(ids, data, pdata, rdata, plugin) if( x ) case x when Array x.reject!{|e|e.nil?} if( x.size == 1 ) x[0] else x end else x end else [] end end def normalize(val) case val when Hash if( val[TextNode] && val.size == 1 ) val = val[TextNode] end when XArray val = val.reject{|v| v.is_a?(String) && (v =~ /\A\s*\z/) } if( val.size > 1 ) catch(:break){ h = {} node_p = false val.each{|v| case v when Hash v.each{|k,x| if( h[k] ) throw(:break) else h[k] = x unless( k && (k[0] == ?@) ) node_p = true end end } else if( h[TextNode] ) throw(:break) else h[TextNode] = v end end } if( h[TextNode] ) if( h.size == 1 ) h = h[TextNode] else if( node_p ) throw(:break) end end end val.clear val.push(h) } end when Array val.reject!{|x|x.nil?} end val end VALUE_BY_PATH1 = %q` # (1)path (2)current data (3)parent data (4)root data (5)plugin object def value_by_path(ids, data, pdata, rdata, plugin) data[ParentNode] ||= pdata hd,*ids = ids case hd when nil data when RootNode value_by_path(ids, rdata, nil, rdata, plugin) when CurrentNode value_by_path(ids, data, pdata, rdata, plugin) when ParentNode value_by_path(ids, data[ParentNode], nil, rdata, plugin) when AnyNode0 [ value_by_path(ids, data, pdata, rdata, plugin), value_by_path([AnyNode2]+ids, data, pdata, rdata, plugin) ].flatten when /^(@\*|\*\*|\*)([\[\{].+[\]\}])?$/ if( $1 == '@*' ) opt = $2 if( opt ) val = data.keys.select{|x| x[0] == ?@}.collect{|key| key = key + opt value_by_path([key]+ids, data, pdata, rdata, plugin) }.flatten else val = data.keys.select{|x| x[0] == ?@}.collect{|key| value_by_path([key]+ids, data, pdata, rdata, plugin) }.flatten end elsif( $1 == '*' ) opt = $2 if( opt ) val = data.keys.select{|x| !(x == ParentNode || x == TextNode) }.collect{|key| key = key + opt value_by_path([key]+ids, data, pdata, rdata, plugin) }.flatten else val = data.keys.reject{|x| (x == ParentNode || x == TextNode) }.collect{|key| value_by_path([key]+ids, data, pdata, rdata, plugin) }.flatten end else # case '**' opt = $2 ss = [] vals = [] d = value_depth(data) # {'1' => {'2' => {'3' => '...'}}} d = 3 # { * => { * => {'3' => '...'}}} if( opt ) for i in 0..(d - 2) ss.push(AnyNode + opt) v = value_by_path(ss+ids, data, pdata, rdata, plugin) ss.pop ss.push(AnyNode) vals.push(v) end else for i in 0..(d - 2) ss.push(AnyNode) v = value_by_path(ss+ids, data, pdata, rdata, plugin) vals.push(v) end end val = vals.flatten end val else ` # don't eliminate this quote. VALUE_BY_PATH2_1 = %q` case hd[-1] when ?] hd,*opts = cond_split(hd) when ?} hd,*opts = cond_split(hd) else opts = [] end if( hd.size > 0 ) hd,recv,*rargs = hd.split(".") val = data[hd] if( recv ) if( recv =~ /(.+)\((.*)\)/ ) recv = $1 rargs = $2.split(',').collect{|c| eval_expr(c, data, plugin) } else rargs = [] end val = val.__send__(recv,*rargs) end else val = data end if( (val = normalize(val)).nil? ) return nil end for opt in opts case opt when /^\{(.+)\}/ val = eval_action($1.strip, val, plugin) when /^\[(\d+)\.\.\.(\d+)\]/ range = ($1.to_i ... $2.to_i) val = val[range] when /^\[(\d+)\.\.(\d+)\]/ range = ($1.to_i .. $2.to_i) val = val[range] when /^\[(\d+),(\d+)\]/ pos = $1.to_i n = $2.to_i val = val[pos,n] when /^\[(\d+)\]/ pos = $1.to_i val = val[pos] when /^\[(.+)\]/ cond = $1 case val when Array val = val.select{|n| eval_condition(cond, n, plugin) } else unless( eval_condition(cond,val,plugin) ) val = nil end end else raise(RuntimeError, "unknown path components: #{opt}") end if( (val = normalize(val)).nil? ) return nil end end # end of 'for' ` # don't eliminate this quote. VALUE_BY_PATH2_2 = %q` if( hd.size > 0 ) hd,recv,*rargs = hd.split(".") val = data[hd] if( recv ) if( recv =~ /(.+)\((.*)\)/ ) recv = $1 rargs = $2.split(',').collect{|c| eval_expr(c, data, plugin) } else rargs = [] end val = val.__send__(recv,*rargs) end else val = data end val = normalize(val) ` # don't eliminate this quote. VALUE_BY_PATH3 = %q` case val when Hash value_by_path(ids, val, data, rdata, plugin) when Array val.collect{|x| case x when Hash value_by_path(ids, x, data, rdata, plugin) else if( ids.empty? ) x else nil end end }.flatten when nil nil else if( ids.empty? ) # 'val' should be a text node, if 'ids' is empty. val else nil end end end # end of 'case hd' end # end of value_by_path() ` # don't eliminate this quote. def use_default_xpath() XPath.module_eval(VALUE_BY_PATH1 + VALUE_BY_PATH2_1 + VALUE_BY_PATH3) end def use_simple_xpath() XPath.module_eval(VALUE_BY_PATH1 + VALUE_BY_PATH2_2 + VALUE_BY_PATH3) end module_function :use_default_xpath, :use_simple_xpath use_default_xpath() def eval_expr(expr, val, plugin) case expr when "text()" if( val.is_a?(Hash) && val[TextNode] && val.size == 1 ) val[TextNode] else val end when "size()" case val when Array val.size when nil 0 else 1 end when /int\((.+)\)/ eval_expr($1,val,plugin).to_i when /float\((.+)\)/ eval_expr($1,val,plugin).to_f when /^(-?\d+)$/ $1.to_i when /^(-?\d+)\.(\d+)$/ $1.to_f when /^('|"|"|')(.+)('|"|"|')$/ str = $2 str.gsub(/\\./){|m| $1} when /^%q\((.+)\)$/ str = $1 str.gsub(/\\./){|m| $1} when /^%r\((.+)\)$/, %r{/(.+)/} str = $1 str.gsub!(/\\./){|m| $1} Regexp.new(str) when 'nil' nil else path_split(expr).each{|path| case val when Hash val = val[path] if( val.is_a?(Hash) && val[TextNode] && val.size == 1 ) val = val[TextNode] end when Array val = val.collect{|v| eval_expr(path, v, plugin) }.flatten.reject{|v| v.nil?} if( val.size == 0 ) val = nil end else val = nil break end } val end end def eval_condition(expr, val, plugin) if( expr =~ /\s+or\s+/ ) expr.split(/\s+or\s+/).any?{|x| eval_condition(x.strip,val,plugin) } elsif( expr =~ /\s+and\s+/ ) expr.split(/\s+and\s+/).all?{|x| eval_condition(x.strip,val,plugin) } elsif( expr =~ /^not\s+(.+)$/ ) ! eval_condition($1.strip,val,plugin) else case expr when /^([^!=<>~\s\(\)]+(\([^!=<>~\s\(\)]*\))?)\s*(=|!=|<|>|<=|>=|=~|!~|<=?|>=?)\s*([^!=<>~\s\(\)]+(\([^!=<>~\s\(\)]*\))?)$/ lhs = eval_expr($1.strip,val,plugin) op = $3 rhs = eval_expr($4.strip,val,plugin) unless( lhs.nil? || rhs.nil? ) case op when '=' (lhs == rhs) when '<', '<' (lhs < rhs) when '<=', '<=' (lhs <= rhs) when '>', '>' (lhs > rhs) when '>=', '>=' (lhs >= rhs) when '=~' (lhs =~ rhs) when '!~' (lhs !~ rhs) else raise(NotImplementedError, "'#{op}'") end else false end when /^([^=<>~]+)$/ eval_expr($1.strip,val,plugin) else nil end end # end of 'else' end def eval_action(act, val, plugin) newval = nil act.strip! if( act.include?(";") ) newval = val act.split(";").each{|a| newval = eval_action(a.strip, newval, plugin) } return newval end if( act =~ /^([^\(\)]+)\(([^\(\)]*)\)$/ ) func = $1.strip args = args_split($2) newval = plugin.__send__(func,val,*args) else case val when Array newval = val.collect{|x| {act => x} } else newval = {act => val} end end newval end # end of eval_action() end # end of XPath end