class Nokogiri::XML::Range

Constants

END_TO_END
END_TO_START
START_TO_END
START_TO_START
VERSION

nokogiri-xml-range version

Attributes

end_container[R]
end_node[R]
end_offset[R]
start_container[R]
start_node[R]
start_offset[R]

Public Class Methods

compare_points(node1, offset1, node2, offset2) click to toggle source
# File lib/nokogiri/xml/range.rb, line 24
def compare_points(node1, offset1, node2, offset2)
  return unless node1.document == node2.document

  case node1 <=> node2
  when 0
    offset1 <=> offset2
  when 1
    compare_points(node2, offset2, node1, offset1) * -1
  else
    ancestors = node2.ancestors_to(node1) # nil or [node2, parent of node2, ..., child of node1, node1]
    if ancestors
      child = nil
      ancestors.reverse_each do |anc|
        child = anc if anc.parent == node1
      end
      if node1.children.index(child) < offset1
        1
      else
        -1
      end
    else
      -1
    end
  end
end
new(start_container, start_offset, end_container, end_offset) click to toggle source
# File lib/nokogiri/xml/range.rb, line 55
def initialize(start_container, start_offset, end_container, end_offset)
  @start_container, @start_offset, @end_container, @end_offset =
    start_container, start_offset, end_container, end_offset
end

Public Instance Methods

clone_contents() click to toggle source
# File lib/nokogiri/xml/range.rb, line 289
def clone_contents
  fragment = DocumentFragment.new(document)
  return fragment if collapsed?

  if @start_container == @end_container and @start_container.replacable?
    cloned = @start_container.clone(0)
    cloned.content = @start_container.substring_data(@start_offset, @end_offset - @start_offset)
    fragment << cloned
    return fragment
  end

  common_ancestor = common_ancestor_container
  first_partially_contained_child = nil
  @end_node_ancestors = [@end_container] + @end_container.ancestors
  unless @end_node_ancestors.include?(@start_container)
    first_partially_contained_child = common_ancestor.children.find {|child|
      partially_contain_node? child
    }
  end
  last_partially_contained_child = nil
  unless ([@start_container] + @start_container.ancestors).include? @end_container
    last_partially_contained_child = common_ancestor.children.reverse_each.find {|child|
      partially_contain_node? child
    }
  end

  contained_children = common_ancestor.children.select {|child|
    contain_node? child
  }

  raise HierarchyRequestError if contained_children.any? {|child|
    child.type == Node::DOCUMENT_TYPE_NODE
  }

  if first_partially_contained_child && first_partially_contained_child.replacable?

    cloned = @start_container.clone(0)
    cloned.content = @start_container.substring_data(@start_offset, @start_container.length - @start_offset)
    fragment << cloned
  elsif first_partially_contained_child
    cloned =first_partially_contained_child.clone(0)
    fragment << cloned
    subrange = self.class.new(@start_container, @start_offset, first_partially_contained_child, first_partially_contained_child.length)
    subfragment = subrange.clone_contents
    cloned << subfragment
  end

  contained_children.each do |contained_child|
    cloned = contained_child.clone(1)
    fragment << cloned
  end

  if last_partially_contained_child && last_partially_contained_child.replacable?
    cloned = @end_container.clone(0)
    cloned.content = @end_container.substring_data(0, @end_offset)
    fragment << cloned
  elsif last_partially_contained_child
    cloned = last_partially_contained_child.clone(0)
    fragment << cloned
    subrange = self.class.new(last_partially_contained_child, 0, @end_container, @end_offset)
    subfragment = subrange.clone_contents
    cloned << subfragment
  end

  fragment
end
collapse(to_start=false) click to toggle source
# File lib/nokogiri/xml/range.rb, line 125
def collapse(to_start=false)
  to_start ? set_end(*start_point) : set_start(*end_point)
end
Also aliased as: collapse!
collapse!(to_start=false)
Alias for: collapse
collapsed?() click to toggle source
# File lib/nokogiri/xml/range.rb, line 120
def collapsed?
  @start_offset == @end_offset and
    @start_container == @end_container
end
common_ancestor_container() click to toggle source
# File lib/nokogiri/xml/range.rb, line 130
def common_ancestor_container
  container = @start_container
  ancestors_of_end = [@end_container] + @end_container.ancestors
  until ancestors_of_end.include?(container)
    container = container.parent
  end
  container
end
compare_boundary_points(how, source_range) click to toggle source
# File lib/nokogiri/xml/range.rb, line 153
def compare_boundary_points(how, source_range)
  raise WrongDocumentError, 'different document' unless source_range.document == document
  this_point, other_point =
    case how
    when START_TO_START
      [start_point, source_range.start_point]
    when START_TO_END
      [end_point, source_range.start_point]
    when END_TO_END
      [end_point, source_range.end_point]
    when END_TO_START
      [start_point, source_range.end_point]
    else
      raise NotSupportedError, 'unsupported way to compare'
    end
  self.class.compare_points(this_point[0], this_point[1], other_point[0], other_point[1])
end
compare_point(node, offset) click to toggle source
# File lib/nokogiri/xml/range.rb, line 517
def compare_point(node, offset)
  raise WrongDocumentError unless node.ancestors.last == @start_container.ancestors.last
  raise InvalidNodeTypeError if node.type == Node::DOCUMENT_TYPE_NODE
  raise IndexSizeError if offset > node.length
  return -1 if self.class.compare_points(node, offset, @start_container, @start_offset) == -1
  return 1 if self.class.compare_points(node, offset, @end_container, @end_offset) == 1
  0
end
contain_node?(node) click to toggle source
# File lib/nokogiri/xml/range.rb, line 477
def contain_node?(node)
  document == node.document and
    self.class.compare_points(node, 0, @start_container, @start_offset) == 1 and
    self.class.compare_points(node, node.length, @end_container, @end_offset) == -1
end
Also aliased as: include_node?, cover_node?
containing_nodes() click to toggle source
# File lib/nokogiri/xml/range.rb, line 485
def containing_nodes
  nodes = NodeSet.new(document)
  select_containing_nodes common_ancestor_container, nodes

  nodes
end
cover_node?(node)
Alias for: contain_node?
delete_contents() click to toggle source
# File lib/nokogiri/xml/range.rb, line 171
def delete_contents
  return if collapsed?

  if @start_container == @end_container and @start_container.replacable?
    @start_container.replace_data @start_offset, @end_offset - @start_offset, ''
    return
  end

  nodes_to_remove = NodeSet.new(document)
  common_ancestor = common_ancestor_container
  select_containing_children common_ancestor, nodes_to_remove

  if @end_container.ancestors_to @start_container
    new_node, new_offset = @start_container, @start_offset
  else
    reference_node = @start_container
    parent = reference_node.parent
    while parent and !@end_container.ancestors_to(parent)
      reference_node = parent
      parent = reference_node.parent
    end
    new_node = parent
    new_offset = parent.children.index(reference_node) + 1
  end

  if @start_container.replacable?
    @start_container.replace_data @start_offset, @start_container.length - @start_offset, ''
  end

  nodes_to_remove.each &:remove

  if @end_container.replacable?
    @end_container.replace_data 0, @end_offset, ''
  end

  @start_container = @end_container = new_node
  @start_offset = @end_offset = new_offset
end
document() click to toggle source
# File lib/nokogiri/xml/range.rb, line 68
def document
  @start_container.document
end
end=(node, offset)
Alias for: set_end
end_point() click to toggle source
# File lib/nokogiri/xml/range.rb, line 64
def end_point
  [@end_container, @end_offset]
end
extract_contents() click to toggle source
# File lib/nokogiri/xml/range.rb, line 210
def extract_contents
  fragment = DocumentFragment.new(document)
  return fragment if collapsed?

  if @start_container == @end_container and @start_container.replacable?
    cloned = @start_container.clone(0)
    cloned.content = @start_container.substring_data(@start_offset, @end_offset - @start_offset)
    fragment << cloned
    @start_container.replace_data @start_offset, @end_offset - @start_offset, ''
    return fragment
  end
  common_ancestor = common_ancestor_container
  end_node_ancestors = [@end_container] + @end_container.ancestors
  first_partially_contained_child =nil
  unless end_node_ancestors.include? @start_container
    first_partially_contained_child = common_ancestor.children.find {|child|
      partially_contain_node? child
    }
  end
  last_partially_contained_child = nil
  unless @start_container.ancestors_to @end_container
    last_partially_contained_child = common_ancestor.children.reverse_each.find {|child|
      partially_contain_node? child
    }
  end
  contained_children = common_ancestor.children.select {|child|
    contain_node? child
  }
  raise HierarchyRequestError if contained_children.any? {|child|
    child.type == Node::DOCUMENT_TYPE_NODE
  }

  if end_node_ancestors.include? @start_container
    new_node, new_offset = @start_container, @start_offset
  else
    reference_node = @start_container
    parent = reference_node.parent
    while parent and !end_node_ancestors.include?(parent)
      reference_node = reference_node.parent
      parent = reference_node.parent
    end
    new_node = parent
    new_offset = parent.children.index(reference_node) + 1
  end

  if first_partially_contained_child && first_partially_contained_child.replacable?

    cloned = @start_container.clone(0)
    cloned.content = @start_container.substring_data(@start_offset, @start_container.length - @start_offset)
    fragment << cloned
    @start_container.replace_data @start_offset, @start_container.length - @start_offset, ''
  elsif first_partially_contained_child
    cloned = first_partially_contained_child.clone(0)
    fragment << cloned
    subrange = Range.new(@start_container, @start_offset, first_partially_contained_child, first_partially_contained_child.length)
    subfragment = subrange.extract_contents
    cloned << subfragment
  end
  contained_children.each do |contained_child|
    fragment << contained_child
  end
  if last_partially_contained_child && last_partially_contained_child.replacable?

    cloned = @end_container.clone(0)
    cloned.content = @end_container.substring_data(0, @end_offset)
    fragment << cloned
    @end_container.replace_data 0, @end_offset, ''
  elsif last_partially_contained_child
    cloned = last_partially_contained_child.clone(0)
    fragment << cloned
    subrange = Range.new(last_partially_contained_child, 0, @end_container, @end_offset)
    subfragment = subrange.extract_contents
    cloned << subfragment
  end
  @start_container = @end_container = new_node
  @start_offset = @end_offset = new_offset
  fragment
end
include_node?(node)
Alias for: contain_node?
insert_node(node) click to toggle source
# File lib/nokogiri/xml/range.rb, line 356
def insert_node(node)
  if [Node::PI_NODE, Node::COMMENT_NODE].include?(@start_container.type) or
    @start_container.text? && @start_container.parent.nil?
    raise HierarchyRequestError
  end
  reference_node = nil
  if @start_container.text?
    reference_node = @start_container
  else
    reference_node = @start_container.children[@start_offset]
  end
  if reference_node
    parent = reference_node.parent
  else
    parent = @start_container
  end
  node.validate_pre_insertion parent, reference_node
  if @start_container.text?
    #7 reference_node = @start_container.split()
  end
  # Nokogiri doesn't support serial text node,
  # so we need to handle it ourselves
  split_node = nil
  if @start_container.text?
    split_node = self.class.new(@start_container, @start_offset, @start_container, @start_container.length).extract_contents
    reference_node = split_node
  end
  if node == reference_node
    reference_node = node.next_sibling
  end
  if node.parent
    node.remove
  end
  if reference_node
    if split_node
      @start_container.parent.children.index(@start_container) + 1
    else
      new_offset = reference_node.parent.children.index(reference_node)
    end
  else
    new_offset = parent.length
  end

  # pre-insert
  if split_node
    # pre-insert validation node parent reference_node(@start_container or split_node)
    unless [Node::DOCUMENT_NODE, Node::DOCUMENT_FRAG_NODE, Node::ELEMENT_NODE].include? parent.type
      raise HierarchyRequestError
    end
    raise hierarchyrequesterror if parent.host_including_inclusive_ancestor? node
    raise Hierarchyrequesterror if reference_node and @start_container.parent != parent
    unless [Node::DOCUMENT_FRAG_NODE, Node::DOCUMENT_TYPE_NODE, Node::ELEMENT_NODE, Node::TEXT_NODE, Node::PI_NODE, Node::COMMENT_NODE].include? node.type
      raise Hierarchyrequesterror
    end
    raise HierarchyRequestError if node.text? && parent.document?
    raise HierarchyRequestError if node.type == Node::DOCUMENT_TYPE_NODE and !parent.document?
    if parent.document?
      case node.type
      when Node::DOCUMENT_FRAG_NODE
        child_element_count = 0
        node.children.each do |n|
          raise HierarchyRequestError if n.text?
          child_element_count += 1 if n.element?
          raise HierarchyRequestError if child_element_count > 1
        end
        if child_element_count == 1
          raise HierarchyRequestError if parent.children.any?(&:element?)
          if reference_node
            raise HierarchyRequestError if reference_node.type == Node::DOCUMENT_TYPE_NODE
            raise HierarchyRequestError if @start_container.following_node.type == Node::DOCUMENT_TYPE_NODE
          end
        end
      when Node::ELEMENT_NODE
        raise HierarchyRequestError if parent.children.any?(&:element?)
        if reference_node
          raise HierarchyRequestError if reference_node.child == Node::DOCUMENT_TYPE_NODE
          raise HierarchyRequestError if @start_container.following_node.type == Node::DOCUMENT_TYPE_NODE
        end
      when Node::DOCUMENT_TYPE_NODE
        raise HierarchyRequestError if parent.children.any? {|n|
          n.type == Node::DOCUMENT_TYPE_NODE
        }
        if reference_node
          raise HierarchyRequestError if @start_container.preceding_node.element?
          raise HierarchyRequestError if parent.children.any?(&:element?)
        end
      end
    end

    reference_child = @start_container
    if reference_child == parent
      reference_child = split_node
    end
    parent.document.adopt node
    @start_container.after node
    node.after split_node
  else
    node.validate_pre_insertion parent, reference_node
    reference_child = reference_node
    if reference_child == parent
      reference_child = parent.next_sibling
    end
    parent.document.adopt node
    reference_child.before node
  end

  if collapsed?
    @end_container, @end_offset = parent, new_offset
  end
end
intersect_node?(node) click to toggle source
# File lib/nokogiri/xml/range.rb, line 526
def intersect_node?(node)
  return false unless node.ancestors.last == @start_container.ancestors.last
  return true unless node.respond_to?(:parent)
  parent = node.parent
  return true unless parent
  offset = parent.children.index(node)
  (self.class.compare_points(parent, offset, @end_container, @end_offset) == -1) and
    (self.class.compare_points(parent, offset + 1, @start_container, @start_offset) == 1)
end
partially_contain_node?(node) click to toggle source
# File lib/nokogiri/xml/range.rb, line 492
def partially_contain_node?(node)
  path_to_start = @start_container.ancestors_to(node)
  path_to_end = @end_container.ancestors_to(node)
  !path_to_start.nil? && path_to_end.nil? or
    path_to_start.nil? && !path_to_end.nil?
end
partially_containing_nodes() click to toggle source
# File lib/nokogiri/xml/range.rb, line 501
def partially_containing_nodes
  inclusive_ancestors_of_start = @start_container.inclusive_ancestors
  inclusive_ancestors_of_end = @end_container.inclusive_ancestors
  (inclusive_ancestors_of_start | inclusive_ancestors_of_end) -
    (inclusive_ancestors_of_start & inclusive_ancestors_of_end)
end
partially_cover_node?(node)
partially_include_node?(node)
point_in_range?(node, offset) click to toggle source
# File lib/nokogiri/xml/range.rb, line 508
def point_in_range?(node, offset)
  return false unless node.ancestors.last == @start_container.ancestors.last
  raise InvalidNodeTypeError if node.type == Node::DOCUMENT_TYPE_NODE
  raise IndexSizeError if offset > node.length
  return false if self.class.compare_points(node, offset, @start_container, @start_offset) == -1
  return false if self.class.compare_points(node, offset, @end_container, @end_offset) == 1
  true
end
root() click to toggle source
# File lib/nokogiri/xml/range.rb, line 72
def root
  document.root
end
select_node(node) click to toggle source
# File lib/nokogiri/xml/range.rb, line 139
def select_node(node)
  parent = node.parent
  raise InvalidNodeTypeError, 'parent node is empty' unless parent
  index = parent.children.index(node)
  set_start parent, index
  set_end parent, index + 1
end
select_node_contents(node) click to toggle source
# File lib/nokogiri/xml/range.rb, line 147
def select_node_contents(node)
  raise InvalidNodeTypeError, 'document type declaration is passed' if node.type == Node::DOCUMENT_TYPE_NODE
  set_start node, 0
  set_end node, node.length
end
set_end(node, offset) click to toggle source
# File lib/nokogiri/xml/range.rb, line 86
def set_end(node, offset)
  validate_boundary_point node, offset
  if document != node.document or
    self.class.compare_points(node, offset, @start_container, @start_offset) == -1
    set_start node, offset
  end
  @end_container, @end_offset = node, offset
end
Also aliased as: end=
set_end_after(node) click to toggle source
# File lib/nokogiri/xml/range.rb, line 114
def set_end_after(node)
  parent = node.parent
  raise InvalidNodeTypeError, 'parent node is empty' unless parent
  set_end(parent, parent.children.index(node) + 1)
end
set_end_before(node) click to toggle source
# File lib/nokogiri/xml/range.rb, line 108
def set_end_before(node)
  parent = node.parent
  raise InvalidNodeTypeError, 'parent node is empty' unless parent
  set_end(parent, parent.children.index(node))
end
set_start(node, offset) click to toggle source
# File lib/nokogiri/xml/range.rb, line 76
def set_start(node, offset)
  validate_boundary_point node, offset
  if document != node.document or
    self.class.compare_points(node, offset, @end_container, @end_offset) == 1
    set_end node, offset
  end
  @start_container, @start_offset = node, offset
end
Also aliased as: start=
set_start_after(node) click to toggle source
# File lib/nokogiri/xml/range.rb, line 102
def set_start_after(node)
  parent = node.parent
  raise InvalidNodeTypeError, 'parent node is empty' unless parent
  set_start(parent, parent.children.index(node) + 1)
end
set_start_before(node) click to toggle source
# File lib/nokogiri/xml/range.rb, line 96
def set_start_before(node)
  parent = node.parent
  raise InvalidNodeTypeError, 'parent node is empty' unless parent
  set_start(parent, parent.children.index(node))
end
start=(node, offset)
Alias for: set_start
start_point() click to toggle source
# File lib/nokogiri/xml/range.rb, line 60
def start_point
  [@start_container, @start_offset]
end
surround_contents(new_parent) click to toggle source
# File lib/nokogiri/xml/range.rb, line 467
def surround_contents(new_parent)
  raise InvalidStateError unless partially_containing_nodes.all?(&:text?)
  raise InvalidNodeTypeError if [Node::DOCUMENT_NODE, Node::DOCUMENT_TYPE_NODE, Node::DOCUMENT_FRAG_NODE].include? new_parent.type
  fragment = extract_contents
  new_parent.replace_all_with nil if new_parent.child
  insert_node new_parent
  new_parent << fragment
  select_node new_parent
end
to_s() click to toggle source
# File lib/nokogiri/xml/range.rb, line 536
def to_s
  s = ''
  if @start_container == @end_container and @start_container.text?
    return @start_container.substring_data(@start_offset, @end_offset - @start_offset)
  end
  if @start_container.text?
    s << @start_container.substring_data(@start_offset, @start_container.length)
  end
  containing_nodes.reduce s do |concatenated, node|
    concatenated << node.content if node.text?
    concatenated
  end
  if @end_container.text?
    s << @end_container.substring_data(0, @end_offset)
  end
  s
end

Private Instance Methods

select_containing_children(node, node_set) click to toggle source

@note depth first order @note modifies node_set

# File lib/nokogiri/xml/range.rb, line 563
def select_containing_children(node, node_set)
  if contain_node?(node)
    node_set << node
  else
    node.children.each do |child|
      select_containing_children child, node_set
    end
  end
end
select_containing_nodes(node, node_set) click to toggle source

@note depth first order @note modifies node_set

# File lib/nokogiri/xml/range.rb, line 575
def select_containing_nodes(node, node_set)
  node_set << node if contain_node?(node)
  node.children.each do |child|
    select_containing_nodes child, node_set
  end
end
validate_boundary_point(node, offset) click to toggle source
# File lib/nokogiri/xml/range.rb, line 556
def validate_boundary_point(node, offset)
  raise InvalidNodeTypeError, 'document type declaration cannot be a boundary point' if node.type == Node::DOCUMENT_TYPE_NODE
  raise IndexSizeError, 'offset is greater than node length' if offset > node.length
end