Class: Ox::Element

Inherits:
Node
  • Object
show all
Includes:
HasAttrs
Defined in:
lib/ox/element.rb

Overview

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"

Direct Known Subclasses

Document

Instance Attribute Summary

Attributes inherited from Node

#value

Instance Method Summary collapse

Methods included from HasAttrs

#[], #[]=, #attributes

Constructor Details

#initialize(name) ⇒ Element

Creates a new Element with the specified name.

  • name [String] name of the Element



41
42
43
44
45
# File 'lib/ox/element.rb', line 41

def initialize(name)
  super
  @attributes = {}
  @nodes = []
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(id, *args, &block) ⇒ Object

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

Raises:

  • (NoMethodError)


191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
# File 'lib/ox/element.rb', line 191

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|
    if (n.is_a?(Element) || n.is_a?(Instruct)) && (n.value == id || n.value == ids || name_matchs?(n.value, ids))
      return n if 0 == i
      has_some = true
      i -= 1
    end
  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

Instance Method Details

#<<(node) ⇒ Object

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



59
60
61
62
63
64
# File 'lib/ox/element.rb', line 59

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

#alocate(path, found) ⇒ Object

  • path [Array] array of steps in a path

  • found [Array] matching nodes



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
# File 'lib/ox/element.rb', line 230

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 '0' <= qual and qual <= '9'
        qual = '+'
      else
        i += 1
      end
      index = step[i..-2].to_i
    end
    if '?' == name or '*' == 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

#attr_match(cond) ⇒ Object

Return true if all the key-value pairs in the cond Hash match the



106
107
108
109
# File 'lib/ox/element.rb', line 106

def attr_match(cond)
  cond.each_pair { |k,v| return false unless v == @attributes[k.to_sym] || v == @attributes[k.to_s] }
  true
end

#each(cond = nil) ⇒ Object

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.



117
118
119
120
121
122
123
124
125
126
127
128
# File 'lib/ox/element.rb', line 117

def each(cond=nil)
  if cond.nil?
    nodes.each { |n| yield(n) }
  else
    cond = cond.to_s if cond.is_a?(Symbol)
    if cond.is_a?(String)
      nodes.each { |n| yield(n) if n.is_a?(Element) && cond == n.name }
    elsif cond.is_a?(Hash)
      nodes.each { |n| yield(n) if n.is_a?(Element) && n.attr_match(cond) }
    end
  end
end

#eql?(other) ⇒ Boolean Also known as: ==

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.

Returns:

  • (Boolean)


80
81
82
83
84
85
# File 'lib/ox/element.rb', line 80

def eql?(other)
  return false unless super(other)
  return false unless self.attributes == other.attributes
  return false unless self.nodes == other.nodes
  true
end

#locate(path) ⇒ Object

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:

  • element.locate("Family/Pete/*") returns all children of the Pete Element.

  • element.locate("Family/?[1]") returns the first element in the Family Element.

  • element.locate("Family/?[<3]") returns the first 3 elements in the Family Element.

  • element.locate("Family/?[@age]") returns the elements with an age attribute defined in the Family Element.

  • element.locate("Family/Kid[@age]") returns the Kid elements with an age attribute defined in the Family Element.

  • element.locate("Family/?[@age=32]") returns the elements with an age attribute equal to 32 in the Family Element.

  • element.locate("Family/Kid[@age=32]") returns the Kid elements with an age attribute equal to 32 in the Family Element.

  • element.locate("Family/?/@age") returns the arg attribute for each child in the Family Element.

  • element.locate("Family/*/@type") returns the type attribute value for decendents of the Family.

  • element.locate("Family/^Comment") returns any comments that are a child of Family.

  • path [String] path to the Nodes to locate



170
171
172
173
174
175
176
177
178
179
180
181
182
183
# File 'lib/ox/element.rb', line 170

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

#nodesObject

Returns the Element's nodes array. These are the sub-elements of this Element. return [Array] all child Nodes.



51
52
53
54
# File 'lib/ox/element.rb', line 51

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

#prepend_child(node) ⇒ Object

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



69
70
71
72
73
74
# File 'lib/ox/element.rb', line 69

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

#replace_text(txt) ⇒ Object

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



98
99
100
101
102
# File 'lib/ox/element.rb', line 98

def replace_text(txt)
  raise "the argument to replace_text() must be a String" unless txt.is_a?(String)
  @nodes.clear()
  @nodes << txt
end

#respond_to?(id, inc_all = false) ⇒ Boolean

  • 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.

Returns:

  • (Boolean)


213
214
215
216
217
218
219
220
221
222
223
224
225
226
# File 'lib/ox/element.rb', line 213

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

#textObject

Returns the first String in the elements nodes array or nil if there is no String node.



90
91
92
93
# File 'lib/ox/element.rb', line 90

def text()
  nodes.each { |n| return n if n.is_a?(String) }
  nil
end