require 'stringio' require 'nokogiri/xml/node/save_options' module Nokogiri module XML #### # Nokogiri::XML::Node is your window to the fun filled world of dealing # with XML and HTML tags. A Nokogiri::XML::Node may be treated similarly # to a hash with regard to attributes. For example (from irb): # # irb(main):004:0> node # => link # irb(main):005:0> node['href'] # => "#foo" # irb(main):006:0> node.keys # => ["href", "id"] # irb(main):007:0> node.values # => ["#foo", "link"] # irb(main):008:0> node['class'] = 'green' # => "green" # irb(main):009:0> node # => link # irb(main):010:0> # # See Nokogiri::XML::Node#[] and Nokogiri::XML#[]= for more information. # # Nokogiri::XML::Node also has methods that let you move around your # tree. For navigating your tree, see: # # * Nokogiri::XML::Node#parent # * Nokogiri::XML::Node#children # * Nokogiri::XML::Node#next # * Nokogiri::XML::Node#previous # # You may search this node's subtree using Node#xpath and Node#css class Node include Nokogiri::XML::PP::Node include Enumerable # Element node type, see Nokogiri::XML::Node#element? ELEMENT_NODE = 1 # Attribute node type ATTRIBUTE_NODE = 2 # Text node type, see Nokogiri::XML::Node#text? TEXT_NODE = 3 # CDATA node type, see Nokogiri::XML::Node#cdata? CDATA_SECTION_NODE = 4 # Entity reference node type ENTITY_REF_NODE = 5 # Entity node type ENTITY_NODE = 6 # PI node type PI_NODE = 7 # Comment node type, see Nokogiri::XML::Node#comment? COMMENT_NODE = 8 # Document node type, see Nokogiri::XML::Node#xml? DOCUMENT_NODE = 9 # Document type node type DOCUMENT_TYPE_NODE = 10 # Document fragment node type DOCUMENT_FRAG_NODE = 11 # Notation node type NOTATION_NODE = 12 # HTML document node type, see Nokogiri::XML::Node#html? HTML_DOCUMENT_NODE = 13 # DTD node type DTD_NODE = 14 # Element declaration type ELEMENT_DECL = 15 # Attribute declaration type ATTRIBUTE_DECL = 16 # Entity declaration type ENTITY_DECL = 17 # Namespace declaration type NAMESPACE_DECL = 18 # XInclude start type XINCLUDE_START = 19 # XInclude end type XINCLUDE_END = 20 # DOCB document node type DOCB_DOCUMENT_NODE = 21 def initialize name, document # :nodoc: # ... Ya. This is empty on purpose. end ### # Decorate this node with the decorators set up in this node's Document def decorate! document.decorate(self) end ### # Search this node for +paths+. +paths+ can be XPath or CSS, and an # optional hash of namespaces may be appended. # See Node#xpath and Node#css. def search *paths # TODO use paths, handler, ns, binds = extract_params(paths) ns = paths.last.is_a?(Hash) ? paths.pop : (document.root ? document.root.namespaces : {}) prefix = "#{implied_xpath_context}/" xpath(*(paths.map { |path| path = path.to_s path =~ /^(\.\/|\/)/ ? path : CSS.xpath_for( path, :prefix => prefix, :ns => ns ) }.flatten.uniq) + [ns]) end alias :/ :search ### # call-seq: xpath *paths, [namespace-bindings, variable-bindings, custom-handler-class] # # Search this node for XPath +paths+. +paths+ must be one or more XPath # queries. # # node.xpath('.//title') # # A hash of namespace bindings may be appended. For example: # # node.xpath('.//foo:name', {'foo' => 'http://example.org/'}) # node.xpath('.//xmlns:name', node.root.namespaces) # # A hash of variable bindings may also be appended to the namespace bindings. For example: # # node.xpath('.//address[@domestic=$value]', nil, {:value => 'Yes'}) # # Custom XPath functions may also be defined. To define custom # functions create a class and implement the function you want # to define. The first argument to the method will be the # current matching NodeSet. Any other arguments are ones that # you pass in. Note that this class may appear anywhere in the # argument list. For example: # # node.xpath('.//title[regex(., "\w+")]', Class.new { # def regex node_set, regex # node_set.find_all { |node| node['some_attribute'] =~ /#{regex}/ } # end # }.new) # def xpath *paths return NodeSet.new(document) unless document paths, handler, ns, binds = extract_params(paths) sets = paths.map { |path| ctx = XPathContext.new(self) ctx.register_namespaces(ns) path = path.gsub(/\/xmlns:/,'/:') unless Nokogiri.uses_libxml? binds.each do |key,value| ctx.register_variable key.to_s, value end if binds ctx.evaluate(path, handler) } return sets.first if sets.length == 1 NodeSet.new(document) do |combined| sets.each do |set| set.each do |node| combined << node end end end end ### # call-seq: css *rules, [namespace-bindings, custom-pseudo-class] # # Search this node for CSS +rules+. +rules+ must be one or more CSS # selectors. For example: # # node.css('title') # node.css('body h1.bold') # node.css('div + p.green', 'div#one') # # A hash of namespace bindings may be appended. For example: # # node.css('bike|tire', {'bike' => 'http://schwinn.com/'}) # # Custom CSS pseudo classes may also be defined. To define # custom pseudo classes, create a class and implement the custom # pseudo class you want defined. The first argument to the # method will be the current matching NodeSet. Any other # arguments are ones that you pass in. For example: # # node.css('title:regex("\w+")', Class.new { # def regex node_set, regex # node_set.find_all { |node| node['some_attribute'] =~ /#{regex}/ } # end # }.new) # # Note that the CSS query string is case-sensitive with regards # to your document type. That is, if you're looking for "H1" in # an HTML document, you'll never find anything, since HTML tags # will match only lowercase CSS queries. However, "H1" might be # found in an XML document, where tags names are case-sensitive # (e.g., "H1" is distinct from "h1"). # def css *rules rules, handler, ns, binds = extract_params(rules) prefix = "#{implied_xpath_context}/" rules = rules.map { |rule| CSS.xpath_for(rule, :prefix => prefix, :ns => ns) }.flatten.uniq + [ns, handler, binds].compact xpath(*rules) end ### # Search this node's immediate children using CSS selector +selector+ def > selector ns = document.root.namespaces xpath CSS.xpath_for(selector, :prefix => "./", :ns => ns).first end ### # Search for the first occurrence of +path+. # # Returns nil if nothing is found, otherwise a Node. def at path, ns = document.root ? document.root.namespaces : {} search(path, ns).first end alias :% :at ## # Search this node for the first occurrence of XPath +paths+. # Equivalent to xpath(paths).first # See Node#xpath for more information. # def at_xpath *paths xpath(*paths).first end ## # Search this node for the first occurrence of CSS +rules+. # Equivalent to css(rules).first # See Node#css for more information. # def at_css *rules css(*rules).first end ### # Get the attribute value for the attribute +name+ def [] name return nil unless key?(name.to_s) get(name.to_s) end ### # Add +node_or_tags+ as a child of this Node. # +node_or_tags+ can be a Nokogiri::XML::Node, a ::DocumentFragment, a ::NodeSet, or a string containing markup. # # Returns the reparented node (if +node_or_tags+ is a Node), or NodeSet (if +node_or_tags+ is a DocumentFragment, NodeSet, or string). def add_child node_or_tags node_or_tags = coerce(node_or_tags) if node_or_tags.is_a?(XML::NodeSet) node_or_tags.each { |n| add_child_node n } else add_child_node node_or_tags end node_or_tags end ### # Insert +node_or_tags+ before this Node (as a sibling). # +node_or_tags+ can be a Nokogiri::XML::Node, a ::DocumentFragment, a ::NodeSet, or a string containing markup. # # Returns the reparented node (if +node_or_tags+ is a Node), or NodeSet (if +node_or_tags+ is a DocumentFragment, NodeSet, or string). # # Also see related method +before+. def add_previous_sibling node_or_tags node_or_tags = coerce(node_or_tags) if node_or_tags.is_a?(XML::NodeSet) if text? pivot = Nokogiri::XML::Node.new 'dummy', document add_previous_sibling_node pivot else pivot = self end node_or_tags.each { |n| pivot.send :add_previous_sibling_node, n } pivot.unlink if text? else add_previous_sibling_node node_or_tags end node_or_tags end ### # Insert +node_or_tags+ after this Node (as a sibling). # +node_or_tags+ can be a Nokogiri::XML::Node, a ::DocumentFragment, a ::NodeSet, or a string containing markup. # # Returns the reparented node (if +node_or_tags+ is a Node), or NodeSet (if +node_or_tags+ is a DocumentFragment, NodeSet, or string). # # Also see related method +after+. def add_next_sibling node_or_tags node_or_tags = coerce(node_or_tags) if node_or_tags.is_a?(XML::NodeSet) if text? pivot = Nokogiri::XML::Node.new 'dummy', document add_next_sibling_node pivot else pivot = self end node_or_tags.reverse.each { |n| pivot.send :add_next_sibling_node, n } pivot.unlink if text? else add_next_sibling_node node_or_tags end node_or_tags end #### # Insert +node_or_tags+ before this node (as a sibling). # +node_or_tags+ can be a Nokogiri::XML::Node, a ::DocumentFragment, a ::NodeSet, or a string containing markup. # # Returns self, to support chaining of calls. # # Also see related method +add_previous_sibling+. def before node_or_tags add_previous_sibling node_or_tags self end #### # Insert +node_or_tags+ after this node (as a sibling). # +node_or_tags+ can be a Nokogiri::XML::Node, a Nokogiri::XML::DocumentFragment, or a string containing markup. # # Returns self, to support chaining of calls. # # Also see related method +add_next_sibling+. def after node_or_tags add_next_sibling node_or_tags self end #### # Set the inner html for this Node to +node_or_tags+ # +node_or_tags+ can be a Nokogiri::XML::Node, a Nokogiri::XML::DocumentFragment, or a string containing markup. # # Returns self. # # Also see related method +children=+ def inner_html= node_or_tags self.children = node_or_tags self end #### # Set the inner html for this Node +node_or_tags+ # +node_or_tags+ can be a Nokogiri::XML::Node, a Nokogiri::XML::DocumentFragment, or a string containing markup. # # Returns the reparented node (if +node_or_tags+ is a Node), or NodeSet (if +node_or_tags+ is a DocumentFragment, NodeSet, or string). # # Also see related method +inner_html=+ def children= node_or_tags node_or_tags = coerce(node_or_tags) children.unlink if node_or_tags.is_a?(XML::NodeSet) node_or_tags.each { |n| add_child_node n } else add_child_node node_or_tags end node_or_tags end #### # Replace this Node with +node_or_tags+. # +node_or_tags+ can be a Nokogiri::XML::Node, a ::DocumentFragment, a ::NodeSet, or a string containing markup. # # Returns the reparented node (if +node_or_tags+ is a Node), or NodeSet (if +node_or_tags+ is a DocumentFragment, NodeSet, or string). # # Also see related method +swap+. def replace node_or_tags node_or_tags = coerce(node_or_tags) if node_or_tags.is_a?(XML::NodeSet) if text? replacee = Nokogiri::XML::Node.new 'dummy', document add_previous_sibling_node replacee unlink else replacee = self end node_or_tags.each { |n| replacee.add_previous_sibling n } replacee.unlink else replace_node node_or_tags end node_or_tags end #### # Swap this Node for +node_or_tags+ # +node_or_tags+ can be a Nokogiri::XML::Node, a ::DocumentFragment, a ::NodeSet, or a string containing markup. # # Returns self, to support chaining of calls. # # Also see related method +replace+. def swap node_or_tags replace node_or_tags self end alias :next :next_sibling alias :previous :previous_sibling # :stopdoc: # HACK: This is to work around an RDoc bug alias :next= :add_next_sibling # :startdoc: alias :previous= :add_previous_sibling alias :remove :unlink alias :get_attribute :[] alias :attr :[] alias :set_attribute :[]= alias :text :content alias :inner_text :content alias :has_attribute? :key? alias :<< :add_child alias :name :node_name alias :name= :node_name= alias :type :node_type alias :to_str :text alias :clone :dup alias :elements :element_children #### # Returns a hash containing the node's attributes. The key is # the attribute name without any namespace, the value is a Nokogiri::XML::Attr # representing the attribute. # If you need to distinguish attributes with the same name, with different namespaces # use #attribute_nodes instead. def attributes Hash[*(attribute_nodes.map { |node| [node.node_name, node] }.flatten)] end ### # Get the attribute values for this Node. def values attribute_nodes.map { |node| node.value } end ### # Get the attribute names for this Node. def keys attribute_nodes.map { |node| node.node_name } end ### # Iterate over each attribute name and value pair for this Node. def each &block attribute_nodes.each { |node| block.call([node.node_name, node.value]) } end ### # Remove the attribute named +name+ def remove_attribute name attributes[name].remove if key? name end alias :delete :remove_attribute ### # Returns true if this Node matches +selector+ def matches? selector ancestors.last.search(selector).include?(self) end ### # Create a DocumentFragment containing +tags+ that is relative to _this_ # context node. def fragment tags type = document.html? ? Nokogiri::HTML : Nokogiri::XML type::DocumentFragment.new(document, tags, self) end ### # Parse +string_or_io+ as a document fragment within the context of # *this* node. Returns a XML::NodeSet containing the nodes parsed from # +string_or_io+. def parse string_or_io, options = nil options ||= (document.html? ? ParseOptions::DEFAULT_HTML : ParseOptions::DEFAULT_XML) if Fixnum === options options = Nokogiri::XML::ParseOptions.new(options) end # Give the options to the user yield options if block_given? contents = string_or_io.respond_to?(:read) ? string_or_io.read : string_or_io return Nokogiri::XML::NodeSet.new(document) if contents.empty? ## # This is a horrible hack, but I don't care. See #313 for background. error_count = document.errors.length node_set = in_context(contents, options.to_i) if node_set.empty? and document.errors.length > error_count and options.recover? fragment = Nokogiri::HTML::DocumentFragment.parse contents node_set = fragment.children end node_set end #### # Set the Node's content to a Text node containing +string+. The string gets XML escaped, not interpreted as markup. def content= string self.native_content = encode_special_chars(string.to_s) end ### # Set the parent Node for this Node def parent= parent_node parent_node.add_child(self) parent_node end ### # Returns a Hash of {prefix => value} for all namespaces on this # node and its ancestors. # # This method returns the same namespaces as #namespace_scopes. # # Returns namespaces in scope for self -- those defined on self # element directly or any ancestor node -- as a Hash of # attribute-name/value pairs. Note that the keys in this hash # XML attributes that would be used to define this namespace, # such as "xmlns:prefix", not just the prefix. Default namespace # set on self will be included with key "xmlns". However, # default namespaces set on ancestor will NOT be, even if self # has no explicit default namespace. def namespaces Hash[*namespace_scopes.map { |nd| key = ['xmlns', nd.prefix].compact.join(':') if RUBY_VERSION >= '1.9' && document.encoding begin key.force_encoding document.encoding rescue ArgumentError end end [key, nd.href] }.flatten] end # Returns true if this is a Comment def comment? type == COMMENT_NODE end # Returns true if this is a CDATA def cdata? type == CDATA_SECTION_NODE end # Returns true if this is an XML::Document node def xml? type == DOCUMENT_NODE end # Returns true if this is an HTML::Document node def html? type == HTML_DOCUMENT_NODE end # Returns true if this is a Text node def text? type == TEXT_NODE end # Returns true if this is a DocumentFragment def fragment? type == DOCUMENT_FRAG_NODE end ### # Fetch the Nokogiri::HTML::ElementDescription for this node. Returns # nil on XML documents and on unknown tags. def description return nil if document.xml? Nokogiri::HTML::ElementDescription[name] end ### # Is this a read only node? def read_only? # According to gdome2, these are read-only node types [NOTATION_NODE, ENTITY_NODE, ENTITY_DECL].include?(type) end # Returns true if this is an Element node def element? type == ELEMENT_NODE end alias :elem? :element? ### # Turn this node in to a string. If the document is HTML, this method # returns html. If the document is XML, this method returns XML. def to_s document.xml? ? to_xml : to_html end # Get the inner_html for this node's Node#children def inner_html *args children.map { |x| x.to_html(*args) }.join end # Get the path to this node as a CSS expression def css_path path.split(/\//).map { |part| part.length == 0 ? nil : part.gsub(/\[(\d+)\]/, ':nth-of-type(\1)') }.compact.join(' > ') end ### # Get a list of ancestor Node for this Node. If +selector+ is given, # the ancestors must match +selector+ def ancestors selector = nil return NodeSet.new(document) unless respond_to?(:parent) return NodeSet.new(document) unless parent parents = [parent] while parents.last.respond_to?(:parent) break unless ctx_parent = parents.last.parent parents << ctx_parent end return NodeSet.new(document, parents) unless selector root = parents.last NodeSet.new(document, parents.find_all { |parent| root.search(selector).include?(parent) }) end ### # Adds a default namespace supplied as a string +url+ href, to self. # The consequence is as an xmlns attribute with supplied argument were # present in parsed XML. A default namespace set with this method will # now show up in #attributes, but when this node is serialized to XML an # "xmlns" attribute will appear. See also #namespace and #namespace= def default_namespace= url add_namespace_definition(nil, url) end alias :add_namespace :add_namespace_definition ### # Set the default namespace on this node (as would be defined with an # "xmlns=" attribute in XML source), as a Namespace object +ns+. Note that # a Namespace added this way will NOT be serialized as an xmlns attribute # for this node. You probably want #default_namespace= instead, or perhaps # #add_namespace_definition with a nil prefix argument. def namespace= ns return set_namespace(ns) unless ns unless Nokogiri::XML::Namespace === ns raise TypeError, "#{ns.class} can't be coerced into Nokogiri::XML::Namespace" end if ns.document != document raise ArgumentError, 'namespace must be declared on the same document' end set_namespace ns end #### # Yields self and all children to +block+ recursively. def traverse &block children.each{|j| j.traverse(&block) } block.call(self) end ### # Accept a visitor. This method calls "visit" on +visitor+ with self. def accept visitor visitor.visit(self) end ### # Test to see if this Node is equal to +other+ def == other return false unless other return false unless other.respond_to?(:pointer_id) pointer_id == other.pointer_id end ### # Serialize Node using +options+. Save options can also be set using a # block. See SaveOptions. # # These two statements are equivalent: # # node.serialize(:encoding => 'UTF-8', :save_with => FORMAT | AS_XML) # # or # # node.serialize(:encoding => 'UTF-8') do |config| # config.format.as_xml # end # def serialize *args, &block options = args.first.is_a?(Hash) ? args.shift : { :encoding => args[0], :save_with => args[1] || SaveOptions::FORMAT } encoding = options[:encoding] || document.encoding options[:encoding] = encoding outstring = "" if encoding && outstring.respond_to?(:force_encoding) outstring.force_encoding(Encoding.find(encoding)) end io = StringIO.new(outstring) write_to io, options, &block io.string end ### # Serialize this Node to HTML # # doc.to_html # # See Node#write_to for a list of +options+. For formatted output, # use Node#to_xhtml instead. def to_html options = {} # FIXME: this is a hack around broken libxml versions return dump_html if Nokogiri.uses_libxml? && %w[2 6] === LIBXML_VERSION.split('.')[0..1] options[:save_with] ||= SaveOptions::FORMAT | SaveOptions::NO_DECLARATION | SaveOptions::NO_EMPTY_TAGS | SaveOptions::AS_HTML serialize(options) end ### # Serialize this Node to XML using +options+ # # doc.to_xml(:indent => 5, :encoding => 'UTF-8') # # See Node#write_to for a list of +options+ def to_xml options = {} options[:save_with] ||= SaveOptions::FORMAT | SaveOptions::AS_XML serialize(options) end ### # Serialize this Node to XHTML using +options+ # # doc.to_xhtml(:indent => 5, :encoding => 'UTF-8') # # See Node#write_to for a list of +options+ def to_xhtml options = {} # FIXME: this is a hack around broken libxml versions return dump_html if Nokogiri.uses_libxml? && %w[2 6] === LIBXML_VERSION.split('.')[0..1] options[:save_with] ||= SaveOptions::FORMAT | SaveOptions::NO_DECLARATION | SaveOptions::NO_EMPTY_TAGS | SaveOptions::AS_XHTML serialize(options) end ### # Write Node to +io+ with +options+. +options+ modify the output of # this method. Valid options are: # # * +:encoding+ for changing the encoding # * +:indent_text+ the indentation text, defaults to one space # * +:indent+ the number of +:indent_text+ to use, defaults to 2 # * +:save_with+ a combination of SaveOptions constants. # # To save with UTF-8 indented twice: # # node.write_to(io, :encoding => 'UTF-8', :indent => 2) # # To save indented with two dashes: # # node.write_to(io, :indent_text => '-', :indent => 2 # def write_to io, *options options = options.first.is_a?(Hash) ? options.shift : {} encoding = options[:encoding] || options[0] save_options = options[:save_with] || options[1] || SaveOptions::FORMAT indent_text = options[:indent_text] || ' ' indent_times = options[:indent] || 2 config = SaveOptions.new(save_options.to_i) yield config if block_given? native_write_to(io, encoding, indent_text * indent_times, config.options) end ### # Write Node as HTML to +io+ with +options+ # # See Node#write_to for a list of +options+ def write_html_to io, options = {} # FIXME: this is a hack around broken libxml versions return (io << dump_html) if Nokogiri.uses_libxml? && %w[2 6] === LIBXML_VERSION.split('.')[0..1] options[:save_with] ||= SaveOptions::FORMAT | SaveOptions::NO_DECLARATION | SaveOptions::NO_EMPTY_TAGS | SaveOptions::AS_HTML write_to io, options end ### # Write Node as XHTML to +io+ with +options+ # # See Node#write_to for a list of +options+ def write_xhtml_to io, options = {} # FIXME: this is a hack around broken libxml versions return (io << dump_html) if Nokogiri.uses_libxml? && %w[2 6] === LIBXML_VERSION.split('.')[0..1] options[:save_with] ||= SaveOptions::FORMAT | SaveOptions::NO_DECLARATION | SaveOptions::NO_EMPTY_TAGS | SaveOptions::AS_XHTML write_to io, options end ### # Write Node as XML to +io+ with +options+ # # doc.write_xml_to io, :encoding => 'UTF-8' # # See Node#write_to for a list of options def write_xml_to io, options = {} options[:save_with] ||= SaveOptions::FORMAT | SaveOptions::AS_XML write_to io, options end ### # Compare two Node objects with respect to their Document. Nodes from # different documents cannot be compared. def <=> other return nil unless other.is_a?(Nokogiri::XML::Node) return nil unless document == other.document compare other end private def extract_params params # :nodoc: # Pop off our custom function handler if it exists handler = params.find { |param| ![Hash, String, Symbol].include?(param.class) } params -= [handler] if handler hashes = [] hashes << params.pop while Hash === params.last || params.last.nil? ns, binds = hashes.reverse ns ||= document.root ? document.root.namespaces : {} [params, handler, ns, binds] end def coerce data # :nodoc: return data if data.is_a?(XML::NodeSet) return data.children if data.is_a?(XML::DocumentFragment) return fragment(data).children if data.is_a?(String) if data.is_a?(Document) || !data.is_a?(XML::Node) raise ArgumentError, <<-EOERR Requires a Node, NodeSet or String argument, and cannot accept a #{data.class}. (You probably want to select a node from the Document with at() or search(), or create a new Node via Node.new().) EOERR end data end def implied_xpath_context "./" end def inspect_attributes [:name, :namespace, :attribute_nodes, :children] end end end end