1require 'rexml/functions' 2require 'rexml/xmltokens' 3 4module REXML 5 class QuickPath 6 include Functions 7 include XMLTokens 8 9 # A base Hash object to be used when initializing a 10 # default empty namespaces set. 11 EMPTY_HASH = {} 12 13 def QuickPath::first element, path, namespaces=EMPTY_HASH 14 match(element, path, namespaces)[0] 15 end 16 17 def QuickPath::each element, path, namespaces=EMPTY_HASH, &block 18 path = "*" unless path 19 match(element, path, namespaces).each( &block ) 20 end 21 22 def QuickPath::match element, path, namespaces=EMPTY_HASH 23 raise "nil is not a valid xpath" unless path 24 results = nil 25 Functions::namespace_context = namespaces 26 case path 27 when /^\/([^\/]|$)/u 28 # match on root 29 path = path[1..-1] 30 return [element.root.parent] if path == '' 31 results = filter([element.root], path) 32 when /^[-\w]*::/u 33 results = filter([element], path) 34 when /^\*/u 35 results = filter(element.to_a, path) 36 when /^[\[!\w:]/u 37 # match on child 38 children = element.to_a 39 results = filter(children, path) 40 else 41 results = filter([element], path) 42 end 43 return results 44 end 45 46 # Given an array of nodes it filters the array based on the path. The 47 # result is that when this method returns, the array will contain elements 48 # which match the path 49 def QuickPath::filter elements, path 50 return elements if path.nil? or path == '' or elements.size == 0 51 case path 52 when /^\/\//u # Descendant 53 return axe( elements, "descendant-or-self", $' ) 54 when /^\/?\b(\w[-\w]*)\b::/u # Axe 55 return axe( elements, $1, $' ) 56 when /^\/(?=\b([:!\w][-\.\w]*:)?[-!\*\.\w]*\b([^:(]|$)|\*)/u # Child 57 rest = $' 58 results = [] 59 elements.each do |element| 60 results |= filter( element.to_a, rest ) 61 end 62 return results 63 when /^\/?(\w[-\w]*)\(/u # / Function 64 return function( elements, $1, $' ) 65 when Namespace::NAMESPLIT # Element name 66 name = $2 67 ns = $1 68 rest = $' 69 elements.delete_if do |element| 70 !(element.kind_of? Element and 71 (element.expanded_name == name or 72 (element.name == name and 73 element.namespace == Functions.namespace_context[ns]))) 74 end 75 return filter( elements, rest ) 76 when /^\/\[/u 77 matches = [] 78 elements.each do |element| 79 matches |= predicate( element.to_a, path[1..-1] ) if element.kind_of? Element 80 end 81 return matches 82 when /^\[/u # Predicate 83 return predicate( elements, path ) 84 when /^\/?\.\.\./u # Ancestor 85 return axe( elements, "ancestor", $' ) 86 when /^\/?\.\./u # Parent 87 return filter( elements.collect{|e|e.parent}, $' ) 88 when /^\/?\./u # Self 89 return filter( elements, $' ) 90 when /^\*/u # Any 91 results = [] 92 elements.each do |element| 93 results |= filter( [element], $' ) if element.kind_of? Element 94 #if element.kind_of? Element 95 # children = element.to_a 96 # children.delete_if { |child| !child.kind_of?(Element) } 97 # results |= filter( children, $' ) 98 #end 99 end 100 return results 101 end 102 return [] 103 end 104 105 def QuickPath::axe( elements, axe_name, rest ) 106 matches = [] 107 matches = filter( elements.dup, rest ) if axe_name =~ /-or-self$/u 108 case axe_name 109 when /^descendant/u 110 elements.each do |element| 111 matches |= filter( element.to_a, "descendant-or-self::#{rest}" ) if element.kind_of? Element 112 end 113 when /^ancestor/u 114 elements.each do |element| 115 while element.parent 116 matches << element.parent 117 element = element.parent 118 end 119 end 120 matches = filter( matches, rest ) 121 when "self" 122 matches = filter( elements, rest ) 123 when "child" 124 elements.each do |element| 125 matches |= filter( element.to_a, rest ) if element.kind_of? Element 126 end 127 when "attribute" 128 elements.each do |element| 129 matches << element.attributes[ rest ] if element.kind_of? Element 130 end 131 when "parent" 132 matches = filter(elements.collect{|element| element.parent}.uniq, rest) 133 when "following-sibling" 134 matches = filter(elements.collect{|element| element.next_sibling}.uniq, 135 rest) 136 when "previous-sibling" 137 matches = filter(elements.collect{|element| 138 element.previous_sibling}.uniq, rest ) 139 end 140 return matches.uniq 141 end 142 143 OPERAND_ = '((?=(?:(?!and|or).)*[^\s<>=])[^\s<>=]+)' 144 # A predicate filters a node-set with respect to an axis to produce a 145 # new node-set. For each node in the node-set to be filtered, the 146 # PredicateExpr is evaluated with that node as the context node, with 147 # the number of nodes in the node-set as the context size, and with the 148 # proximity position of the node in the node-set with respect to the 149 # axis as the context position; if PredicateExpr evaluates to true for 150 # that node, the node is included in the new node-set; otherwise, it is 151 # not included. 152 # 153 # A PredicateExpr is evaluated by evaluating the Expr and converting 154 # the result to a boolean. If the result is a number, the result will 155 # be converted to true if the number is equal to the context position 156 # and will be converted to false otherwise; if the result is not a 157 # number, then the result will be converted as if by a call to the 158 # boolean function. Thus a location path para[3] is equivalent to 159 # para[position()=3]. 160 def QuickPath::predicate( elements, path ) 161 ind = 1 162 bcount = 1 163 while bcount > 0 164 bcount += 1 if path[ind] == ?[ 165 bcount -= 1 if path[ind] == ?] 166 ind += 1 167 end 168 ind -= 1 169 predicate = path[1..ind-1] 170 rest = path[ind+1..-1] 171 172 # have to change 'a [=<>] b [=<>] c' into 'a [=<>] b and b [=<>] c' 173 # 174 predicate.gsub!( 175 /#{OPERAND_}\s*([<>=])\s*#{OPERAND_}\s*([<>=])\s*#{OPERAND_}/u, 176 '\1 \2 \3 and \3 \4 \5' ) 177 # Let's do some Ruby trickery to avoid some work: 178 predicate.gsub!( /&/u, "&&" ) 179 predicate.gsub!( /=/u, "==" ) 180 predicate.gsub!( /@(\w[-\w.]*)/u, 'attribute("\1")' ) 181 predicate.gsub!( /\bmod\b/u, "%" ) 182 predicate.gsub!( /\b(\w[-\w.]*\()/u ) { 183 fname = $1 184 fname.gsub( /-/u, "_" ) 185 } 186 187 Functions.pair = [ 0, elements.size ] 188 results = [] 189 elements.each do |element| 190 Functions.pair[0] += 1 191 Functions.node = element 192 res = eval( predicate ) 193 case res 194 when true 195 results << element 196 when Fixnum 197 results << element if Functions.pair[0] == res 198 when String 199 results << element 200 end 201 end 202 return filter( results, rest ) 203 end 204 205 def QuickPath::attribute( name ) 206 return Functions.node.attributes[name] if Functions.node.kind_of? Element 207 end 208 209 def QuickPath::name() 210 return Functions.node.name if Functions.node.kind_of? Element 211 end 212 213 def QuickPath::method_missing( id, *args ) 214 begin 215 Functions.send( id.id2name, *args ) 216 rescue Exception 217 raise "METHOD: #{id.id2name}(#{args.join ', '})\n#{$!.message}" 218 end 219 end 220 221 def QuickPath::function( elements, fname, rest ) 222 args = parse_args( elements, rest ) 223 Functions.pair = [0, elements.size] 224 results = [] 225 elements.each do |element| 226 Functions.pair[0] += 1 227 Functions.node = element 228 res = Functions.send( fname, *args ) 229 case res 230 when true 231 results << element 232 when Fixnum 233 results << element if Functions.pair[0] == res 234 end 235 end 236 return results 237 end 238 239 def QuickPath::parse_args( element, string ) 240 # /.*?(?:\)|,)/ 241 arguments = [] 242 buffer = "" 243 while string and string != "" 244 c = string[0] 245 string.sub!(/^./u, "") 246 case c 247 when ?, 248 # if depth = 1, then we start a new argument 249 arguments << evaluate( buffer ) 250 #arguments << evaluate( string[0..count] ) 251 when ?( 252 # start a new method call 253 function( element, buffer, string ) 254 buffer = "" 255 when ?) 256 # close the method call and return arguments 257 return arguments 258 else 259 buffer << c 260 end 261 end 262 "" 263 end 264 end 265end 266