472 lines
15 KiB
Ruby
472 lines
15 KiB
Ruby
# frozen_string_literal: false
|
|
require_relative "security"
|
|
require_relative "element"
|
|
require_relative "xmldecl"
|
|
require_relative "source"
|
|
require_relative "comment"
|
|
require_relative "doctype"
|
|
require_relative "instruction"
|
|
require_relative "rexml"
|
|
require_relative "parseexception"
|
|
require_relative "output"
|
|
require_relative "parsers/baseparser"
|
|
require_relative "parsers/streamparser"
|
|
require_relative "parsers/treeparser"
|
|
|
|
module REXML
|
|
# Represents an XML document.
|
|
#
|
|
# A document may have:
|
|
#
|
|
# - A single child that may be accessed via method #root.
|
|
# - An XML declaration.
|
|
# - A document type.
|
|
# - Processing instructions.
|
|
#
|
|
# == In a Hurry?
|
|
#
|
|
# If you're somewhat familiar with XML
|
|
# and have a particular task in mind,
|
|
# you may want to see the
|
|
# {tasks pages}[../doc/rexml/tasks/tocs/master_toc_rdoc.html],
|
|
# and in particular, the
|
|
# {tasks page for documents}[../doc/rexml/tasks/tocs/document_toc_rdoc.html].
|
|
#
|
|
class Document < Element
|
|
# A convenient default XML declaration. Use:
|
|
#
|
|
# mydoc << XMLDecl.default
|
|
#
|
|
DECLARATION = XMLDecl.default
|
|
|
|
# :call-seq:
|
|
# new(string = nil, context = {}) -> new_document
|
|
# new(io_stream = nil, context = {}) -> new_document
|
|
# new(document = nil, context = {}) -> new_document
|
|
#
|
|
# Returns a new \REXML::Document object.
|
|
#
|
|
# When no arguments are given,
|
|
# returns an empty document:
|
|
#
|
|
# d = REXML::Document.new
|
|
# d.to_s # => ""
|
|
#
|
|
# When argument +string+ is given, it must be a string
|
|
# containing a valid XML document:
|
|
#
|
|
# xml_string = '<root><foo>Foo</foo><bar>Bar</bar></root>'
|
|
# d = REXML::Document.new(xml_string)
|
|
# d.to_s # => "<root><foo>Foo</foo><bar>Bar</bar></root>"
|
|
#
|
|
# When argument +io_stream+ is given, it must be an \IO object
|
|
# that is opened for reading, and when read must return a valid XML document:
|
|
#
|
|
# File.write('t.xml', xml_string)
|
|
# d = File.open('t.xml', 'r') do |io|
|
|
# REXML::Document.new(io)
|
|
# end
|
|
# d.to_s # => "<root><foo>Foo</foo><bar>Bar</bar></root>"
|
|
#
|
|
# When argument +document+ is given, it must be an existing
|
|
# document object, whose context and attributes (but not children)
|
|
# are cloned into the new document:
|
|
#
|
|
# d = REXML::Document.new(xml_string)
|
|
# d.children # => [<root> ... </>]
|
|
# d.context = {raw: :all, compress_whitespace: :all}
|
|
# d.add_attributes({'bar' => 0, 'baz' => 1})
|
|
# d1 = REXML::Document.new(d)
|
|
# d1.children # => []
|
|
# d1.context # => {:raw=>:all, :compress_whitespace=>:all}
|
|
# d1.attributes # => {"bar"=>bar='0', "baz"=>baz='1'}
|
|
#
|
|
# When argument +context+ is given, it must be a hash
|
|
# containing context entries for the document;
|
|
# see {Element Context}[../doc/rexml/context_rdoc.html]:
|
|
#
|
|
# context = {raw: :all, compress_whitespace: :all}
|
|
# d = REXML::Document.new(xml_string, context)
|
|
# d.context # => {:raw=>:all, :compress_whitespace=>:all}
|
|
#
|
|
def initialize( source = nil, context = {} )
|
|
@entity_expansion_count = 0
|
|
@entity_expansion_limit = Security.entity_expansion_limit
|
|
@entity_expansion_text_limit = Security.entity_expansion_text_limit
|
|
super()
|
|
@context = context
|
|
# `source = ""` is an invalid usage because no root element XML is an invalid XML.
|
|
# But we accept `""` for backward compatibility.
|
|
return if source.nil? or source == ""
|
|
if source.kind_of? Document
|
|
@context = source.context
|
|
super source
|
|
else
|
|
build( source )
|
|
end
|
|
end
|
|
|
|
# :call-seq:
|
|
# node_type -> :document
|
|
#
|
|
# Returns the symbol +:document+.
|
|
#
|
|
def node_type
|
|
:document
|
|
end
|
|
|
|
# :call-seq:
|
|
# clone -> new_document
|
|
#
|
|
# Returns the new document resulting from executing
|
|
# <tt>Document.new(self)</tt>. See Document.new.
|
|
#
|
|
def clone
|
|
Document.new self
|
|
end
|
|
|
|
# :call-seq:
|
|
# expanded_name -> empty_string
|
|
#
|
|
# Returns an empty string.
|
|
#
|
|
def expanded_name
|
|
''
|
|
#d = doc_type
|
|
#d ? d.name : "UNDEFINED"
|
|
end
|
|
alias :name :expanded_name
|
|
|
|
# :call-seq:
|
|
# add(xml_decl) -> self
|
|
# add(doc_type) -> self
|
|
# add(object) -> self
|
|
#
|
|
# Adds an object to the document; returns +self+.
|
|
#
|
|
# When argument +xml_decl+ is given,
|
|
# it must be an REXML::XMLDecl object,
|
|
# which becomes the XML declaration for the document,
|
|
# replacing the previous XML declaration if any:
|
|
#
|
|
# d = REXML::Document.new
|
|
# d.xml_decl.to_s # => ""
|
|
# d.add(REXML::XMLDecl.new('2.0'))
|
|
# d.xml_decl.to_s # => "<?xml version='2.0'?>"
|
|
#
|
|
# When argument +doc_type+ is given,
|
|
# it must be an REXML::DocType object,
|
|
# which becomes the document type for the document,
|
|
# replacing the previous document type, if any:
|
|
#
|
|
# d = REXML::Document.new
|
|
# d.doctype.to_s # => ""
|
|
# d.add(REXML::DocType.new('foo'))
|
|
# d.doctype.to_s # => "<!DOCTYPE foo>"
|
|
#
|
|
# When argument +object+ (not an REXML::XMLDecl or REXML::DocType object)
|
|
# is given it is added as the last child:
|
|
#
|
|
# d = REXML::Document.new
|
|
# d.add(REXML::Element.new('foo'))
|
|
# d.to_s # => "<foo/>"
|
|
#
|
|
def add( child )
|
|
if child.kind_of? XMLDecl
|
|
if @children[0].kind_of? XMLDecl
|
|
@children[0] = child
|
|
else
|
|
@children.unshift child
|
|
end
|
|
child.parent = self
|
|
elsif child.kind_of? DocType
|
|
# Find first Element or DocType node and insert the decl right
|
|
# before it. If there is no such node, just insert the child at the
|
|
# end. If there is a child and it is an DocType, then replace it.
|
|
insert_before_index = @children.find_index { |x|
|
|
x.kind_of?(Element) || x.kind_of?(DocType)
|
|
}
|
|
if insert_before_index # Not null = not end of list
|
|
if @children[ insert_before_index ].kind_of? DocType
|
|
@children[ insert_before_index ] = child
|
|
else
|
|
@children[ insert_before_index-1, 0 ] = child
|
|
end
|
|
else # Insert at end of list
|
|
@children << child
|
|
end
|
|
child.parent = self
|
|
else
|
|
rv = super
|
|
raise "attempted adding second root element to document" if @elements.size > 1
|
|
rv
|
|
end
|
|
end
|
|
alias :<< :add
|
|
|
|
# :call-seq:
|
|
# add_element(name_or_element = nil, attributes = nil) -> new_element
|
|
#
|
|
# Adds an element to the document by calling REXML::Element.add_element:
|
|
#
|
|
# REXML::Element.add_element(name_or_element, attributes)
|
|
def add_element(arg=nil, arg2=nil)
|
|
rv = super
|
|
raise "attempted adding second root element to document" if @elements.size > 1
|
|
rv
|
|
end
|
|
|
|
# :call-seq:
|
|
# root -> root_element or nil
|
|
#
|
|
# Returns the root element of the document, if it exists, otherwise +nil+:
|
|
#
|
|
# d = REXML::Document.new('<root></root>')
|
|
# d.root # => <root/>
|
|
# d = REXML::Document.new('')
|
|
# d.root # => nil
|
|
#
|
|
def root
|
|
elements[1]
|
|
#self
|
|
#@children.find { |item| item.kind_of? Element }
|
|
end
|
|
|
|
# :call-seq:
|
|
# doctype -> doc_type or nil
|
|
#
|
|
# Returns the DocType object for the document, if it exists, otherwise +nil+:
|
|
#
|
|
# d = REXML::Document.new('<!DOCTYPE document SYSTEM "subjects.dtd">')
|
|
# d.doctype.class # => REXML::DocType
|
|
# d = REXML::Document.new('')
|
|
# d.doctype.class # => nil
|
|
#
|
|
def doctype
|
|
@children.find { |item| item.kind_of? DocType }
|
|
end
|
|
|
|
# :call-seq:
|
|
# xml_decl -> xml_decl
|
|
#
|
|
# Returns the XMLDecl object for the document, if it exists,
|
|
# otherwise the default XMLDecl object:
|
|
#
|
|
# d = REXML::Document.new('<?xml version="1.0" encoding="UTF-8"?>')
|
|
# d.xml_decl.class # => REXML::XMLDecl
|
|
# d.xml_decl.to_s # => "<?xml version='1.0' encoding='UTF-8'?>"
|
|
# d = REXML::Document.new('')
|
|
# d.xml_decl.class # => REXML::XMLDecl
|
|
# d.xml_decl.to_s # => ""
|
|
#
|
|
def xml_decl
|
|
rv = @children[0]
|
|
return rv if rv.kind_of? XMLDecl
|
|
@children.unshift(XMLDecl.default)[0]
|
|
end
|
|
|
|
# :call-seq:
|
|
# version -> version_string
|
|
#
|
|
# Returns the XMLDecl version of this document as a string,
|
|
# if it has been set, otherwise the default version:
|
|
#
|
|
# d = REXML::Document.new('<?xml version="2.0" encoding="UTF-8"?>')
|
|
# d.version # => "2.0"
|
|
# d = REXML::Document.new('')
|
|
# d.version # => "1.0"
|
|
#
|
|
def version
|
|
xml_decl().version
|
|
end
|
|
|
|
# :call-seq:
|
|
# encoding -> encoding_string
|
|
#
|
|
# Returns the XMLDecl encoding of the document,
|
|
# if it has been set, otherwise the default encoding:
|
|
#
|
|
# d = REXML::Document.new('<?xml version="1.0" encoding="UTF-16"?>')
|
|
# d.encoding # => "UTF-16"
|
|
# d = REXML::Document.new('')
|
|
# d.encoding # => "UTF-8"
|
|
#
|
|
def encoding
|
|
xml_decl().encoding
|
|
end
|
|
|
|
# :call-seq:
|
|
# stand_alone?
|
|
#
|
|
# Returns the XMLDecl standalone value of the document as a string,
|
|
# if it has been set, otherwise the default standalone value:
|
|
#
|
|
# d = REXML::Document.new('<?xml standalone="yes"?>')
|
|
# d.stand_alone? # => "yes"
|
|
# d = REXML::Document.new('')
|
|
# d.stand_alone? # => nil
|
|
#
|
|
def stand_alone?
|
|
xml_decl().stand_alone?
|
|
end
|
|
|
|
# :call-seq:
|
|
# doc.write(output=$stdout, indent=-1, transitive=false, ie_hack=false, encoding=nil)
|
|
# doc.write(options={:output => $stdout, :indent => -1, :transitive => false, :ie_hack => false, :encoding => nil})
|
|
#
|
|
# Write the XML tree out, optionally with indent. This writes out the
|
|
# entire XML document, including XML declarations, doctype declarations,
|
|
# and processing instructions (if any are given).
|
|
#
|
|
# A controversial point is whether Document should always write the XML
|
|
# declaration (<?xml version='1.0'?>) whether or not one is given by the
|
|
# user (or source document). REXML does not write one if one was not
|
|
# specified, because it adds unnecessary bandwidth to applications such
|
|
# as XML-RPC.
|
|
#
|
|
# Accept Nth argument style and options Hash style as argument.
|
|
# The recommended style is options Hash style for one or more
|
|
# arguments case.
|
|
#
|
|
# _Examples_
|
|
# Document.new("<a><b/></a>").write
|
|
#
|
|
# output = ""
|
|
# Document.new("<a><b/></a>").write(output)
|
|
#
|
|
# output = ""
|
|
# Document.new("<a><b/></a>").write(:output => output, :indent => 2)
|
|
#
|
|
# See also the classes in the rexml/formatters package for the proper way
|
|
# to change the default formatting of XML output.
|
|
#
|
|
# _Examples_
|
|
#
|
|
# output = ""
|
|
# tr = Transitive.new
|
|
# tr.write(Document.new("<a><b/></a>"), output)
|
|
#
|
|
# output::
|
|
# output an object which supports '<< string'; this is where the
|
|
# document will be written.
|
|
# indent::
|
|
# An integer. If -1, no indenting will be used; otherwise, the
|
|
# indentation will be twice this number of spaces, and children will be
|
|
# indented an additional amount. For a value of 3, every item will be
|
|
# indented 3 more levels, or 6 more spaces (2 * 3). Defaults to -1
|
|
# transitive::
|
|
# If transitive is true and indent is >= 0, then the output will be
|
|
# pretty-printed in such a way that the added whitespace does not affect
|
|
# the absolute *value* of the document -- that is, it leaves the value
|
|
# and number of Text nodes in the document unchanged.
|
|
# ie_hack::
|
|
# This hack inserts a space before the /> on empty tags to address
|
|
# a limitation of Internet Explorer. Defaults to false
|
|
# encoding::
|
|
# Encoding name as String. Change output encoding to specified encoding
|
|
# instead of encoding in XML declaration.
|
|
# Defaults to nil. It means encoding in XML declaration is used.
|
|
def write(*arguments)
|
|
if arguments.size == 1 and arguments[0].class == Hash
|
|
options = arguments[0]
|
|
|
|
output = options[:output]
|
|
indent = options[:indent]
|
|
transitive = options[:transitive]
|
|
ie_hack = options[:ie_hack]
|
|
encoding = options[:encoding]
|
|
else
|
|
output, indent, transitive, ie_hack, encoding, = *arguments
|
|
end
|
|
|
|
output ||= $stdout
|
|
indent ||= -1
|
|
transitive = false if transitive.nil?
|
|
ie_hack = false if ie_hack.nil?
|
|
encoding ||= xml_decl.encoding
|
|
|
|
if encoding != 'UTF-8' && !output.kind_of?(Output)
|
|
output = Output.new( output, encoding )
|
|
end
|
|
formatter = if indent > -1
|
|
if transitive
|
|
require_relative "formatters/transitive"
|
|
REXML::Formatters::Transitive.new( indent, ie_hack )
|
|
else
|
|
REXML::Formatters::Pretty.new( indent, ie_hack )
|
|
end
|
|
else
|
|
REXML::Formatters::Default.new( ie_hack )
|
|
end
|
|
formatter.write( self, output )
|
|
end
|
|
|
|
|
|
def Document::parse_stream( source, listener )
|
|
Parsers::StreamParser.new( source, listener ).parse
|
|
end
|
|
|
|
# Set the entity expansion limit. By default the limit is set to 10000.
|
|
#
|
|
# Deprecated. Use REXML::Security.entity_expansion_limit= instead.
|
|
def Document::entity_expansion_limit=( val )
|
|
Security.entity_expansion_limit = val
|
|
end
|
|
|
|
# Get the entity expansion limit. By default the limit is set to 10000.
|
|
#
|
|
# Deprecated. Use REXML::Security.entity_expansion_limit= instead.
|
|
def Document::entity_expansion_limit
|
|
Security.entity_expansion_limit
|
|
end
|
|
|
|
# Set the entity expansion limit. By default the limit is set to 10240.
|
|
#
|
|
# Deprecated. Use REXML::Security.entity_expansion_text_limit= instead.
|
|
def Document::entity_expansion_text_limit=( val )
|
|
Security.entity_expansion_text_limit = val
|
|
end
|
|
|
|
# Get the entity expansion limit. By default the limit is set to 10240.
|
|
#
|
|
# Deprecated. Use REXML::Security.entity_expansion_text_limit instead.
|
|
def Document::entity_expansion_text_limit
|
|
Security.entity_expansion_text_limit
|
|
end
|
|
|
|
attr_reader :entity_expansion_count
|
|
attr_writer :entity_expansion_limit
|
|
attr_accessor :entity_expansion_text_limit
|
|
|
|
def record_entity_expansion
|
|
@entity_expansion_count += 1
|
|
if @entity_expansion_count > @entity_expansion_limit
|
|
raise "number of entity expansions exceeded, processing aborted."
|
|
end
|
|
end
|
|
|
|
def document
|
|
self
|
|
end
|
|
|
|
private
|
|
|
|
attr_accessor :namespaces_cache
|
|
|
|
# New document level cache is created and available in this block.
|
|
# This API is thread unsafe. Users can't change this document in this block.
|
|
def enable_cache
|
|
@namespaces_cache = {}
|
|
begin
|
|
yield
|
|
ensure
|
|
@namespaces_cache = nil
|
|
end
|
|
end
|
|
|
|
def build( source )
|
|
Parsers::TreeParser.new( source, self ).parse
|
|
end
|
|
end
|
|
end
|