oga/lib/oga/xml/node_set.rb

336 lines
7.7 KiB
Ruby

module Oga
module XML
##
# The NodeSet class contains a set of unique {Oga::XML::Node} instances that
# can be queried and modified. Optionally NodeSet instances can take
# ownership of a node (besides just containing it). This allows the nodes to
# query their previous and next elements.
#
# There are two types of sets:
#
# 1. Regular node sets
# 2. Owned node sets
#
# Both behave similar to Ruby's Array class. The difference between an
# owned and regular node set is that an owned set modifies nodes that are
# added or removed by certain operations. For example, when a node is added
# to an owned set the `node_set` attribute of said node points to the set
# it was just added to.
#
# Owned node sets are used when building a DOM tree with
# {Oga::XML::Parser}. By taking ownership of nodes in a set Oga makes it
# possible to use these sets as following:
#
# document = Oga::XML::Document.new
# element = Oga::XML::Element.new
#
# document.children << element
#
# element.node_set == document.children # => true
#
# If ownership was not handled then you'd have to manually set the
# `element` variable's `node_set` attribute after pushing it into a set.
#
# @!attribute [rw] owner
# @return [Oga::XML::Node]
#
class NodeSet
include Enumerable
attr_accessor :owner
##
# @param [Array] nodes The nodes to add to the set.
# @param [Oga::XML::NodeSet] owner The owner of the set.
#
def initialize(nodes = [], owner = nil)
@nodes = nodes
@owner = owner
@nodes.each { |node| take_ownership(node) } if owner
end
##
# Yields the supplied block for every node.
#
# @yieldparam [Oga::XML::Node]
#
def each
@nodes.each { |node| yield node }
end
##
# Returns the last node in the set.
#
# @return [Oga::XML::Node]
#
def last
return @nodes[-1]
end
##
# Returns `true` if the set is empty.
#
# @return [TrueClass|FalseClass]
#
def empty?
return @nodes.empty?
end
##
# Returns the amount of nodes in the set.
#
# @return [Fixnum]
#
def length
return @nodes.length
end
alias_method :count, :length
alias_method :size, :length
##
# Returns the index of the given node.
#
# @param [Oga::XML::Node] node
# @return [Fixnum]
#
def index(node)
return @nodes.index(node)
end
##
# Pushes the node at the end of the set.
#
# @param [Oga::XML::Node] node
#
def push(node)
return if @nodes.include?(node)
@nodes << node
take_ownership(node)
end
alias_method :<<, :push
##
# Pushes the node at the start of the set.
#
# @param [Oga::XML::Node] node
#
def unshift(node)
return if @nodes.include?(node)
@nodes.unshift(node)
take_ownership(node)
end
##
# Shifts a node from the start of the set.
#
# @return [Oga::XML::Node]
#
def shift
node = @nodes.shift
remove_ownership(node)
return node
end
##
# Pops a node from the end of the set.
#
# @return [Oga::XML::Node]
#
def pop
node = @nodes.pop
remove_ownership(node)
return node
end
##
# Inserts a node into the set at the given index.
#
# @param [Fixnum] index The index to insert the node at.
# @param [Oga::XML::Node] node
#
def insert(index, node)
return if @nodes.include?(node)
@nodes.insert(index, node)
take_ownership(node)
end
##
# Returns the node for the given index.
#
# @param [Fixnum] index
# @return [Oga::XML::Node]
#
def [](index)
return @nodes[index]
end
##
# Converts the current set to an Array.
#
# @return [Array]
#
def to_a
return @nodes
end
##
# Creates a new set based on the current and the specified set. The newly
# created set does not inherit ownership rules of the current set.
#
# @param [Oga::XML::NodeSet] other
# @return [Oga::XML::NodeSet]
#
def +(other)
return self.class.new(to_a | other.to_a)
end
##
# Returns `true` if the current node set and the one given in `other` are
# equal to each other.
#
# @param [Oga::XML::NodeSet] other
#
def ==(other)
return other.is_a?(NodeSet) && other.equal_nodes?(@nodes)
end
##
# Returns `true` if the nodes given in `nodes` are equal to those
# specified in the current `@nodes` variable. This method allows two
# NodeSet instances to compare each other without the need of exposing
# `@nodes` to the public.
#
# @param [Array<Oga::XML::Node>] nodes
#
def equal_nodes?(nodes)
return @nodes == nodes
end
##
# Adds the nodes of the given node set to the current node set.
#
# @param [Oga::XML::NodeSet] other
#
def concat(other)
other.each { |node| push(node) }
end
##
# Removes the current nodes from their owning set. The nodes are *not*
# removed from the current set.
#
# This method is intended to remove nodes from an XML document/node.
#
def remove
sets = []
# First we gather all the sets to remove nodse from, then we remove the
# actual nodes. This is done as you can not reliably remove elements
# from an Array while iterating on that same Array.
@nodes.each do |node|
if node.node_set
sets << node.node_set
node.node_set = nil
end
end
sets.each do |set|
@nodes.each { |node| set.delete(node) }
end
end
##
# Removes a node from the current set only.
#
def delete(node)
removed = @nodes.delete(node)
remove_ownership(removed) if removed
return removed
end
##
# Returns the values of the given attribute.
#
# @param [String|Symbol] name The name of the attribute.
# @return [Array]
#
def attribute(name)
values = []
@nodes.each do |node|
if node.respond_to?(:attribute)
values << node.attribute(name)
end
end
return values
end
alias_method :attr, :attribute
##
# Returns the text of all nodes in the set, ignoring comment nodes.
#
# @return [String]
#
def text
text = ''
@nodes.each do |node|
if node.respond_to?(:text) and !node.is_a?(Comment)
text << node.text
end
end
return text
end
##
# @return [String]
#
def inspect
values = @nodes.map(&:inspect).join(', ')
return "NodeSet(#{values})"
end
private
##
# Takes ownership of the given node. This only occurs when the current
# set has an owner.
#
# @param [Oga::XML::Node] node
#
def take_ownership(node)
node.node_set = self if owner
end
##
# Removes ownership of the node if it belongs to the current set.
#
# @param [Oga::XML::Node] node
#
def remove_ownership(node)
node.node_set = nil if node.node_set == self
end
end # NodeSet
end # XML
end # Oga