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(%{ # # # # Peter # Ohler # # # Makie # Ohler # # # }) # # 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 # 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 # 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(other) return false unless self.attributes == other.attributes return false unless self.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) @nodes.clear() @nodes << txt 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: # * 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 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 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 # 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| if (n.is_a?(Element) || n.is_a?(Instruct)) && (n.value == id || 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 # - +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 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 '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] : [] 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 end # Element end # Ox