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