dc41dd1445df645e14c410e789c2f92c785fc890
[booh] / lib / booh / rexml / quickpath.rb
1 require 'booh/rexml/functions'
2 require 'booh/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