Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Adding a XML Element to a Nokogiri::XML::Builder document

Tags:

xml

ruby

nokogiri

How can I add a Nokogiri::XML::Element to a XML document that is being created with Nokogiri::XML::Buider?

My current solution is to serialize the element and use the << method to have the Builder reinterpret it.

orig_doc = Nokogiri::XML('<root xmlns="foobar"><a>test</a></root>')
node = orig_doc.at('/*/*[1]')

puts Nokogiri::XML::Builder.new do |doc|
    doc.another {
        # FIXME: this is the round-trip I would like to avoid
        xml_text = node.to_xml(:skip_instruct => true).to_s
        doc << xml_text

        doc.second("hi")
    }
end.to_xml

# The expected result is
#
# <another>
#    <a xmlns="foobar">test</a>
#    <second>hi</second>
# </another>

However the Nokogiri::XML::Element is a quite big node (in the order of kilobytes and thousands of nodes) and this code is in the hot path. Profiling shows that the serialization/parsing round trip is very expensive.

How can I instruct the Nokogiri Builder to add the existing XML element node in the "current" position?

like image 583
gioele Avatar asked Feb 03 '15 17:02

gioele


2 Answers

Without using a private method you can get a handle on the current parent element using the parent method of the Builder instance. Then you can append an element to that (even from another document). For example:

require 'nokogiri'
doc1 = Nokogiri.XML('<r><a>success!</a></r>')
a = doc1.at('a')

# note that `xml` is not a Nokogiri::XML::Document,
#  but rather a Nokogiri::XML::Builder instance.
doc2 = Nokogiri::XML::Builder.new do |xml|
  xml.some do
    xml.more do
      xml.parent << a
    end
  end
end.doc

puts doc2
#=> <?xml version="1.0"?>
#=> <some>
#=>   <more>
#=>     <a>success!</a>
#=>   </more>
#=> </some>
like image 192
Phrogz Avatar answered Nov 06 '22 13:11

Phrogz


After looking at the Nokogiri source I have found this fragile solution: using the protected #insert(node) method.

The code, modified to use that private method looks like this:

doc.another {
    xml_text = node.to_xml(:skip_instruct => true).to_s
    doc.send('insert', xml_text) # <= use `#insert` instead of `<<`

    doc.second("hi")
}
like image 38
gioele Avatar answered Nov 06 '22 12:11

gioele