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