oga/lib/oga/xml/generator.rb

199 lines
5.4 KiB
Ruby

module Oga
module XML
# Class for generating XML as a String based on an existing document.
#
# Basic usage:
#
# element = Oga::XML::Element.new(name: 'root')
# element.inner_text = 'hello'
#
# gen = Oga::XML::Generator.new(element)
#
# gen.to_xml # => "<root>hello</root>"
#
# @private
class Generator
# @param [Oga::XML::Document|Oga::XML::Node] start The node to serialise.
def initialize(root)
@start = root
if @start.respond_to?(:root_node)
@html_mode = @start.root_node.html?
else
@html_mode = false
end
end
# Returns the XML for the current root node.
#
# @return [String]
def to_xml
current = @start
output = ''
while current
children = false
# Determine what callback to use for the current node. The order of
# this statement is based on how likely it is for an arm to match.
case current
when Oga::XML::Element
callback = :on_element
children = true
when Oga::XML::Text
callback = :on_text
when Oga::XML::Cdata
callback = :on_cdata
when Oga::XML::Comment
callback = :on_comment
when Oga::XML::Attribute
callback = :on_attribute
when Oga::XML::ProcessingInstruction
callback = :on_processing_instruction
when Oga::XML::Doctype
callback = :on_doctype
when Oga::XML::XmlDeclaration
callback = :on_xml_declaration
when Oga::XML::Document
callback = :on_document
children = true
else
raise TypeError, "Can't serialize #{current.class} to XML"
end
send(callback, current, output)
if child_node = children && current.children[0]
current = child_node
else
until next_node = current.is_a?(Node) && current.next
if current.is_a?(Node) && current != @start
current = current.parent
end
send(:after_element, current, output) if current.is_a?(Element)
break if current == @start
end
current = next_node
end
end
output
end
# @param [Oga::XML::Text] node
# @param [String] output
def on_text(node, output)
if @html_mode && (parent = node.parent) && parent.literal_html_name?
output << node.text
else
output << Entities.encode(node.text)
end
end
# @param [Oga::XML::Cdata] node
# @param [String] output
def on_cdata(node, output)
output << "<![CDATA[#{node.text}]]>"
end
# @param [Oga::XML::Comment] node
# @param [String] output
def on_comment(node, output)
output << "<!--#{node.text}-->"
end
# @param [Oga::XML::ProcessingInstruction] node
# @param [String] output
def on_processing_instruction(node, output)
output << "<?#{node.name}#{node.text}?>"
end
# @param [Oga::XML::Element] element
# @param [String] body The content of the element.
def on_element(element, output)
name = element.expanded_name
attrs = ''
element.attributes.each do |attr|
attrs << ' '
on_attribute(attr, attrs)
end
if self_closing?(element)
output << "<#{name}#{attrs} />"
else
output << "<#{name}#{attrs}>"
end
end
# @param [Oga::XML::Element] element
# @param [String] output
def after_element(element, output)
output << "</#{element.expanded_name}>" unless self_closing?(element)
end
# @param [Oga::XML::Attribute] attr
# @param [String] output
def on_attribute(attr, output)
name = attr.expanded_name
enc_value = attr.value ? Entities.encode_attribute(attr.value) : nil
output << %Q(#{name}="#{enc_value}")
end
# @param [Oga::XML::Doctype] node
# @param [String] output
def on_doctype(node, output)
output << "<!DOCTYPE #{node.name}"
output << " #{node.type}" if node.type
output << %Q{ "#{node.public_id}"} if node.public_id
output << %Q{ "#{node.system_id}"} if node.system_id
output << " [#{node.inline_rules}]" if node.inline_rules
output << '>'
end
# @param [Oga::XML::Document] node
# @param [String] output
def on_document(doc, output)
if doc.xml_declaration
on_xml_declaration(doc.xml_declaration, output)
output << "\n"
end
if doc.doctype
on_doctype(doc.doctype, output)
output << "\n"
end
end
# @param [Oga::XML::XmlDeclaration] node
# @param [String] output
def on_xml_declaration(node, output)
output << '<?xml'
[:version, :encoding, :standalone].each do |getter|
value = node.send(getter)
output << %Q{ #{getter}="#{value}"} if value
end
output << ' ?>'
end
# @param [Oga::XML::Element] element
# @return [TrueClass|FalseClass]
def self_closing?(element)
if @html_mode && !HTML_VOID_ELEMENTS.allow?(element.name)
false
else
element.children.empty?
end
end
end
end
end