File: element.rb

package info (click to toggle)
ruby-ox 2.14.23-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 3,504 kB
  • sloc: xml: 39,683; ansic: 9,626; ruby: 6,441; sh: 47; makefile: 2
file content (464 lines) | stat: -rw-r--r-- 17,365 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
module Ox
  # An Element represents a element of an XML document. It has a name,
  # attributes, and sub-nodes.
  #
  # To access the child elements or attributes there are several options. One
  # is to walk the nodes and attributes. Another is to use the locate()
  # method. The easiest for simple regularly formatted XML is to reference the
  # sub elements or attributes simply by name. Repeating elements with the
  # same name can be referenced with an element count as well. A few examples
  # should explain the 'easy' API more clearly.
  #
  # *Example*
  #
  #   doc = Ox.parse(%{
  #   <?xml?>
  #   <People>
  #     <Person age="58">
  #       <given>Peter</given>
  #       <surname>Ohler</surname>
  #     </Person>
  #     <Person>
  #       <given>Makie</given>
  #       <surname>Ohler</surname>
  #     </Person>
  #   </People>
  #   })
  #
  #   doc.People.Person.given.text
  #   => "Peter"
  #   doc.People.Person(1).given.text
  #   => "Makie"
  #   doc.People.Person.age
  #   => "58"
  class Element < Node
    include HasAttrs

    # Creates a new Element with the specified name.
    # - +name+ [String] name of the Element
    def initialize(name)
      super
      @attributes = {}
      @nodes = []
    end
    alias name value
    alias name= value=

    # Returns the Element's nodes array. These are the sub-elements of this
    # Element.
    # *return* [Array] all child Nodes.
    def nodes
      @nodes = [] if !instance_variable_defined?(:@nodes) or @nodes.nil?
      @nodes
    end

    # Appends a Node to the Element's nodes array. Returns the element itself
    # so multiple appends can be chained together.
    # - +node+ [Node] Node to append to the nodes array
    def <<(node)
      raise 'argument to << must be a String or Ox::Node.' unless node.is_a?(String) or node.is_a?(Node)

      @nodes = [] if !instance_variable_defined?(:@nodes) or @nodes.nil?
      @nodes << node
      self
    end

    # Prepend a Node to the Element's nodes array. Returns the element itself
    # so multiple appends can be chained together.
    # - +node+ [Node] Node to prepend to the nodes array
    def prepend_child(node)
      raise 'argument to << must be a String or Ox::Node.' unless node.is_a?(String) or node.is_a?(Node)

      @nodes = [] if !instance_variable_defined?(:@nodes) or @nodes.nil?
      @nodes.unshift(node)
      self
    end

    # Returns true if this Object and other are of the same type and have the
    # equivalent value and the equivalent elements otherwise false is returned.
    # - +other+ [Object] Object compare _self_ to.
    # *return* [Boolean] true if both Objects are equivalent, otherwise false.
    def eql?(other)
      return false unless super
      return false unless attributes == other.attributes
      return false unless nodes == other.nodes

      true
    end
    alias == eql?

    # Returns the first String in the elements nodes array or nil if there is
    # no String node.
    def text
      nodes.each { |n| return n if n.is_a?(String) }
      nil
    end

    # Clears any child nodes of an element and replaces those with a single Text
    # (String) node. Note the existing nodes array is modified and not replaced.
    # - +txt+ [String] to become the only element of the nodes array
    def replace_text(txt)
      raise 'the argument to replace_text() must be a String' unless txt.is_a?(String)

      if !instance_variable_defined?(:@nodes) or @nodes.nil?
        @node = []
      else
        @nodes.clear
      end
      @nodes << txt
    end

    # Return true if all the key-value pairs in the cond Hash match the
    # @attributes key-values.
    def attr_match(cond)
      cond.each_pair { |k, v| return false unless v == @attributes[k.to_sym] || v == @attributes[k.to_s] }
      true
    end

    # Iterate over each child of the instance yielding according to the cond
    # argument value. If the cond argument is nil then all child nodes are
    # yielded to. If cond is a string then only the child Elements with a
    # matching name will be yielded to. If the cond is a Hash then the
    # keys-value pairs in the cond must match the child attribute values with
    # the same keys. Any other cond type will yield to nothing.
    def each(cond=nil, &block)
      build_enumerator(cond).each(&block)
    end

    # Returns an array of Nodes or Strings that correspond to the locations
    # specified by the path parameter. The path parameter describes the path
    # to the return values which can be either nodes in the XML or
    # attributes. The path is a relative description. There are similarities
    # between the locate() method and XPath but locate does not follow the
    # same rules as XPath. The syntax is meant to be simpler and more Ruby
    # like.
    #
    # Like XPath the path delimiters are the slash (/) character. The path is
    # split on the delimiter and each element of the path then describes the
    # child of the current Element to traverse.
    #
    # Attributes are specified with an @ prefix.
    #
    # Each element name in the path can be followed by a bracket expression
    # that narrows the paths to traverse. Supported expressions are numbers
    # with a preceeding qualifier. Qualifiers are -, +, <, and >. The +
    # qualifier is the default. A - qualifier indicates the index begins at
    # the end of the children just like for Ruby Arrays. The < and >
    # qualifiers indicates all elements either less than or greater than
    # should be matched. Note that unlike XPath, the element index starts at 0
    # similar to Ruby be contrary to XPath.
    #
    # Element names can also be wildcard characters. A * indicates any decendent should be followed. A ? indicates any
    # single Element can match the wildcard. A ^ character followed by the name of a Class will match any node of the
    # specified class. Valid class names are Element, Comment, String (or Text), CData, DocType.
    #
    # Examples are:
    # * <code>element.locate("Family/Pete/*")</code> returns all children of the Pete Element.
    # * <code>element.locate("Family/?[1]")</code> returns the first element in the Family Element.
    # * <code>element.locate("Family/?[<3]")</code> returns the first 3 elements in the Family Element.
    # * <code>element.locate("Family/?[@age]")</code> returns the elements with an age attribute defined in the Family Element.
    # * <code>element.locate("Family/Kid[@age]")</code> returns the Kid elements with an age attribute defined in the Family Element.
    # * <code>element.locate("Family/?[@age=32]")</code> returns the elements with an age attribute equal to 32 in the Family Element.
    # * <code>element.locate("Family/Kid[@age=32]")</code> returns the Kid elements with an age attribute equal to 32 in the Family Element.
    # * <code>element.locate("Family/?/@age")</code> returns the arg attribute for each child in the Family Element.
    # * <code>element.locate("Family/*/@type")</code> returns the type attribute value for decendents of the Family.
    # * <code>element.locate("Family/^Comment")</code> returns any comments that are a child of Family.
    #
    # - +path+ [String] path to the Nodes to locate
    def locate(path)
      return [self] if path.nil?

      found = []
      pa = path.split('/')
      if '*' == path[0]
        # a bit of a hack but it allows self to be checked as well
        e = Element.new('')
        e << self
        e.alocate(pa, found)
      else
        alocate(pa, found)
      end
      found
    end

    # Remove all the children matching the path provided
    #
    # Examples are:
    # * <code>element.remove_children(Ox:Element)</code> removes the element passed as argument if child of the element.
    # * <code>element.remove_children(Ox:Element, Ox:Element)</code> removes the list of elements passed as argument if children of the element.
    #
    # - +children+ [Array] array of OX
    def remove_children(*children)
      return self if children.compact.empty?

      recursive_children_removal(children.compact.map { |c| c.object_id })
      self
    end

    # Remove all the children matching the path provided
    #
    # Examples are:
    # * <code>element.remove_children_by_path("*")</code> removes all children attributes.
    # * <code>element.remove_children_by_path("Family/Kid[@age=32]")</code> removes the Kid elements with an age attribute equal to 32 in the Family Element.
    #
    # - +path+ [String] path to the Nodes to locate
    def remove_children_by_path(path)
      del_locate(path.split('/')) unless path.nil?
      self
    end

    # Handles the 'easy' API that allows navigating a simple XML by
    # referencing elements and attributes by name.
    # - +id+ [Symbol] element or attribute name
    # *return* [Element|Node|String|nil] the element, attribute value, or Node identifed by the name
    #
    # _raise_ [NoMethodError] if no match is found
    def method_missing(id, *args, &block)
      has_some = false
      ids = id.to_s
      i = args[0].to_i # will be 0 if no arg or parsing fails
      nodes.each do |n|
        unless (n.is_a?(Element) || n.is_a?(Instruct)) && (n.value == id || n.value == ids || name_matchs?(n.value, ids))
          next
        end
        return n if 0 == i

        has_some = true
        i -= 1
      end
      if instance_variable_defined?(:@attributes)
        return @attributes[id] if @attributes.has_key?(id)
        return @attributes[ids] if @attributes.has_key?(ids)
      end
      return nil if has_some

      raise NoMethodError.new("#{ids} not found", name)
    end

    # - +id+ [String|Symbol] identifer of the attribute or method
    # - +ignored+ inc_all [Boolean]
    # *return* true if the element has a member that matches the provided name.
    def respond_to?(id, inc_all=false)
      return true if super

      id_str = id.to_s
      id_sym = id.to_sym
      nodes.each do |n|
        next if n.is_a?(String)
        return true if n.value == id_str || n.value == id_sym || name_matchs?(n.value, id_str)
      end
      if instance_variable_defined?(:@attributes) && !@attributes.nil?
        return true if @attributes.has_key?(id_str)
        return true if @attributes.has_key?(id_sym)
      end
      false
    end

    # - +path+ [Array] array of steps in a path
    # - +found+ [Array] matching nodes
    def alocate(path, found)
      step = path[0]
      if step.start_with?('@') # attribute
        raise InvalidPath.new(path) unless 1 == path.size

        if instance_variable_defined?(:@attributes)
          step = step[1..-1]
          sym_step = step.to_sym
          @attributes.each do |k, v|
            found << v if ('?' == step or k == step or k == sym_step)
          end
        end
      else # element name
        if (i = step.index('[')).nil? # just name
          name = step
          qual = nil
        else
          name = step[0..i-1]
          raise InvalidPath.new(path) unless step.end_with?(']')

          i += 1
          qual = step[i..i] # step[i] would be better but some rubies (jruby, ree, rbx) take that as a Fixnum.
          if qual.between?('0', '9')
            qual = '+'
          else
            i += 1
          end
          index = step[i..-2].to_i
        end
        if ['?', '*'].include?(name)
          match = nodes
        elsif '^' == name[0..0] # 1.8.7 thinks name[0] is a fixnum
          case name[1..-1]
          when 'Element'
            match = nodes.select { |e| e.is_a?(Element) }
          when 'String', 'Text'
            match = nodes.select { |e| e.is_a?(String) }
          when 'Comment'
            match = nodes.select { |e| e.is_a?(Comment) }
          when 'CData'
            match = nodes.select { |e| e.is_a?(CData) }
          when 'DocType'
            match = nodes.select { |e| e.is_a?(DocType) }
          else
            # puts "*** no match on #{name}"
            match = []
          end
        else
          match = nodes.select { |e| e.is_a?(Element) and name == e.name }
        end
        unless qual.nil? or match.empty?
          case qual
          when '+'
            match = index < match.size ? [match[index]] : []
          when '-'
            match = index <= match.size ? [match[-index]] : []
          when '<'
            match = 0 < index ? match[0..index - 1] : []
          when '>'
            match = index <= match.size ? match[index + 1..-1] : []
          when '@'
            k, v = step[i..-2].split('=')
            if v
              match = match.select { |n| n.is_a?(Element) && (v == n.attributes[k.to_sym] || v == n.attributes[k]) }
            else
              match = match.select { |n| n.is_a?(Element) && (n.attributes[k.to_sym] || n.attributes[k]) }
            end
          else
            raise InvalidPath.new(path)
          end
        end
        if (1 == path.size)
          match.each { |n| found << n }
        elsif '*' == name
          match.each { |n| n.alocate(path, found) if n.is_a?(Element) }
          match.each { |n| n.alocate(path[1..-1], found) if n.is_a?(Element) }
        else
          match.each { |n| n.alocate(path[1..-1], found) if n.is_a?(Element) }
        end
      end
    end

    # - +path+ [Array] array of steps in a path
    def del_locate(path)
      step = path[0]
      if step.start_with?('@') # attribute
        raise InvalidPath.new(path) unless 1 == path.size

        if instance_variable_defined?(:@attributes)
          step = step[1..-1]
          sym_step = step.to_sym
          @attributes.delete_if { |k, v| '?' == step || k.to_sym == sym_step }
        end
      else # element name
        if (i = step.index('[')).nil? # just name
          name = step
          qual = nil
        else
          name = step[0..i-1]
          raise InvalidPath.new(path) unless step.end_with?(']')

          i += 1
          qual = step[i..i] # step[i] would be better but some rubies (jruby, ree, rbx) take that as a Fixnum.
          if qual.between?('0', '9')
            qual = '+'
          else
            i += 1
          end
          index = step[i..-2].to_i
        end
        if ['?', '*'].include?(name)
          match = nodes
        elsif '^' == name[0..0] # 1.8.7 thinks name[0] is a fixnum
          case name[1..-1]
          when 'Element'
            match = nodes.select { |e| e.is_a?(Element) }
          when 'String', 'Text'
            match = nodes.select { |e| e.is_a?(String) }
          when 'Comment'
            match = nodes.select { |e| e.is_a?(Comment) }
          when 'CData'
            match = nodes.select { |e| e.is_a?(CData) }
          when 'DocType'
            match = nodes.select { |e| e.is_a?(DocType) }
          else
            # puts "*** no match on #{name}"
            match = []
          end
        else
          match = nodes.select { |e| e.is_a?(Element) and name == e.name }
        end
        unless qual.nil? or match.empty?
          case qual
          when '+'
            match = index < match.size ? [match[index]] : []
          when '-'
            match = index <= match.size ? [match[-index]] : []
          when '<'
            match = 0 < index ? match[0..index - 1] : []
          when '>'
            match = index <= match.size ? match[index + 1..-1] : []
          when '@'
            k, v = step[i..-2].split('=')
            if v
              match = match.select { |n| n.is_a?(Element) && (v == n.attributes[k.to_sym] || v == n.attributes[k]) }
            else
              match = match.select { |n| n.is_a?(Element) && (n.attributes[k.to_sym] || n.attributes[k]) }
            end
          else
            raise InvalidPath.new(path)
          end
        end
        if (1 == path.size)
          nodes.delete_if { |n| match.include?(n) }
        elsif '*' == name
          match.each { |n| n.del_locate(path) if n.is_a?(Element) }
          match.each { |n| n.del_locate(path[1..-1]) if n.is_a?(Element) }
        else
          match.each { |n| n.del_locate(path[1..-1]) if n.is_a?(Element) }
        end
      end
    end

    private

    # Builds an enumerator for use in `#each` call
    #
    # - +cond+ [Hash, String, nil] an element filter
    def build_enumerator(cond)
      if cond.nil?
        nodes.each
      else
        cond = cond.to_s if cond.is_a?(Symbol)
        Enumerator.new do |yielder|
          if cond.is_a?(String)
            nodes.each { |n| yielder.yield(n) if n.is_a?(Element) && cond == n.name }
          elsif cond.is_a?(Hash)
            nodes.each { |n| yielder.yield(n) if n.is_a?(Element) && n.attr_match(cond) }
          end
        end
      end
    end

    # Removes recursively children for nodes and sub_nodes
    #
    # - +found+ [Array] An array of Ox::Element
    def recursive_children_removal(found)
      return if found.empty?

      nodes.tap do |ns|
        # found.delete(n.object_id) stops looking for an already found object_id
        ns.delete_if { |n| found.include?(n.object_id) ? found.delete(n.object_id) : false }
        nodes.each do |n|
          n.send(:recursive_children_removal, found) if n.is_a?(Ox::Element)
        end
      end
    end

    def name_matchs?(pat, id)
      return false unless pat.length == id.length

      pat.length.times { |i| return false unless '_' == id[i] || pat[i] == id[i] }
      true
    end
  end # Element
end # Ox