# frozen_string_literal: true module Nokogiri module XML #### # A NodeSet contains a list of Nokogiri::XML::Node objects. Typically # a NodeSet is return as a result of searching a Document via # Nokogiri::XML::Searchable#css or Nokogiri::XML::Searchable#xpath class NodeSet include Nokogiri::XML::Searchable include Enumerable # The Document this NodeSet is associated with attr_accessor :document alias :clone :dup # Create a NodeSet with +document+ defaulting to +list+ def initialize document, list = [] @document = document document.decorate(self) list.each { |x| self << x } yield self if block_given? end ### # Get the first element of the NodeSet. def first n = nil return self[0] unless n list = [] [n, length].min.times { |i| list << self[i] } list end ### # Get the last element of the NodeSet. def last self[-1] end ### # Is this NodeSet empty? def empty? length == 0 end ### # Returns the index of the first node in self that is == to +node+ or meets the given block. Returns nil if no match is found. def index(node = nil) if node warn "given block not used" if block_given? each_with_index { |member, j| return j if member == node } elsif block_given? each_with_index { |member, j| return j if yield(member) } end nil end ### # Insert +datum+ before the first Node in this NodeSet def before datum first.before datum end ### # Insert +datum+ after the last Node in this NodeSet def after datum last.after datum end alias :<< :push alias :remove :unlink ### # call-seq: css *rules, [namespace-bindings, custom-pseudo-class] # # Search this node set for CSS +rules+. +rules+ must be one or more CSS # selectors. For example: # # For more information see Nokogiri::XML::Searchable#css def css *args rules, handler, ns, _ = extract_params(args) paths = css_rules_to_xpath(rules, ns) inject(NodeSet.new(document)) do |set, node| set + xpath_internal(node, paths, handler, ns, nil) end end ### # call-seq: xpath *paths, [namespace-bindings, variable-bindings, custom-handler-class] # # Search this node set for XPath +paths+. +paths+ must be one or more XPath # queries. # # For more information see Nokogiri::XML::Searchable#xpath def xpath *args paths, handler, ns, binds = extract_params(args) inject(NodeSet.new(document)) do |set, node| set + xpath_internal(node, paths, handler, ns, binds) end end ### # Search this NodeSet's nodes' immediate children using CSS selector +selector+ def > selector ns = document.root.namespaces xpath CSS.xpath_for(selector, :prefix => "./", :ns => ns).first end ### # call-seq: search *paths, [namespace-bindings, xpath-variable-bindings, custom-handler-class] # # Search this object for +paths+, and return only the first # result. +paths+ must be one or more XPath or CSS queries. # # See Searchable#search for more information. # # Or, if passed an integer, index into the NodeSet: # # node_set.at(3) # same as node_set[3] # def at *args if args.length == 1 && args.first.is_a?(Numeric) return self[args.first] end super(*args) end alias :% :at ### # Filter this list for nodes that match +expr+ def filter expr find_all { |node| node.matches?(expr) } end ### # Add the class attribute +name+ to all Node objects in the # NodeSet. # # See Nokogiri::XML::Node#add_class for more information. def add_class name each do |el| el.add_class(name) end self end ### # Append the class attribute +name+ to all Node objects in the # NodeSet. # # See Nokogiri::XML::Node#append_class for more information. def append_class name each do |el| el.append_class(name) end self end ### # Remove the class attribute +name+ from all Node objects in the # NodeSet. # # See Nokogiri::XML::Node#remove_class for more information. def remove_class name = nil each do |el| el.remove_class(name) end self end ### # Set attributes on each Node in the NodeSet, or get an # attribute from the first Node in the NodeSet. # # To get an attribute from the first Node in a NodeSet: # # node_set.attr("href") # => "https://www.nokogiri.org" # # Note that an empty NodeSet will return nil when +#attr+ is called as a getter. # # To set an attribute on each node, +key+ can either be an # attribute name, or a Hash of attribute names and values. When # called as a setter, +#attr+ returns the NodeSet. # # If +key+ is an attribute name, then either +value+ or +block+ # must be passed. # # If +key+ is a Hash then attributes will be set for each # key/value pair: # # node_set.attr("href" => "https://www.nokogiri.org", "class" => "member") # # If +value+ is passed, it will be used as the attribute value # for all nodes: # # node_set.attr("href", "https://www.nokogiri.org") # # If +block+ is passed, it will be called on each Node object in # the NodeSet and the return value used as the attribute value # for that node: # # node_set.attr("class") { |node| node.name } # def attr key, value = nil, &block unless key.is_a?(Hash) || (key && (value || block)) return first ? first.attribute(key) : nil end hash = key.is_a?(Hash) ? key : { key => value } hash.each do |k,v| each do |node| node[k] = v || block.call(node) end end self end alias :set :attr alias :attribute :attr ### # Remove the attributed named +name+ from all Node objects in the NodeSet def remove_attr name each { |el| el.delete name } self end alias remove_attribute remove_attr ### # Iterate over each node, yielding to +block+ def each return to_enum unless block_given? 0.upto(length - 1) do |x| yield self[x] end self end ### # Get the inner text of all contained Node objects # # Note: This joins the text of all Node objects in the NodeSet: # # doc = Nokogiri::XML('foobar') # doc.css('d').text # => "foobar" # # Instead, if you want to return the text of all nodes in the NodeSet: # # doc.css('d').map(&:text) # => ["foo", "bar"] # # See Nokogiri::XML::Node#content for more information. def inner_text collect(&:inner_text).join('') end alias :text :inner_text ### # Get the inner html of all contained Node objects def inner_html *args collect{|j| j.inner_html(*args) }.join('') end ### # Wrap this NodeSet with +html+ def wrap html map { |node| node.wrap html } end ### # Convert this NodeSet to a string. def to_s map(&:to_s).join end ### # Convert this NodeSet to HTML def to_html *args if Nokogiri.jruby? options = args.first.is_a?(Hash) ? args.shift : {} if !options[:save_with] options[:save_with] = Node::SaveOptions::NO_DECLARATION | Node::SaveOptions::NO_EMPTY_TAGS | Node::SaveOptions::AS_HTML end args.insert(0, options) end map { |x| x.to_html(*args) }.join end ### # Convert this NodeSet to XHTML def to_xhtml *args map { |x| x.to_xhtml(*args) }.join end ### # Convert this NodeSet to XML def to_xml *args map { |x| x.to_xml(*args) }.join end alias :size :length alias :to_ary :to_a ### # Removes the last element from set and returns it, or +nil+ if # the set is empty def pop return nil if length == 0 delete last end ### # Returns the first element of the NodeSet and removes it. Returns # +nil+ if the set is empty. def shift return nil if length == 0 delete first end ### # Equality -- Two NodeSets are equal if the contain the same number # of elements and if each element is equal to the corresponding # element in the other NodeSet def == other return false unless other.is_a?(Nokogiri::XML::NodeSet) return false unless length == other.length each_with_index do |node, i| return false unless node == other[i] end true end ### # Returns a new NodeSet containing all the children of all the nodes in # the NodeSet def children node_set = NodeSet.new(document) each do |node| node.children.each { |n| node_set.push(n) } end node_set end ### # Returns a new NodeSet containing all the nodes in the NodeSet # in reverse order def reverse node_set = NodeSet.new(document) (length - 1).downto(0) do |x| node_set.push self[x] end node_set end ### # Return a nicely formated string representation def inspect "[#{map(&:inspect).join ', '}]" end alias :+ :| # @private IMPLIED_XPATH_CONTEXTS = [ './/'.freeze, 'self::'.freeze ].freeze # :nodoc: end end end