How to Add Child Nodes in Nodeset Using Nokogiri

Adding a node using Nokogiri

It's easy to add/change/delete HTML:

require 'nokogiri'

doc = Nokogiri::HTML::DocumentFragment.parse('<div class="input">hello</div>')
div = doc.at('div')
div << '<span>Hello</span>'
puts doc.to_html

Which results in:

# >> <div class="input">hello<span>Hello</span>
# >> </div>

Notice that the above code appended a new node to the existing children of the <div>, because of <<, which means they were appended after the text-node containing "hello".

If you want to overwrite the children, you can do that easily using children =:

div.children = '<span>Hello</span>'
puts doc.to_html

Which results in:

# >> <div class="input"><span>Hello</span></div>

children = can take a single Node which can have multiple other nodes nestled under it, or the HTML text of the node(s) being inserted. That's what node_or_tags means when you see it in the documentation.

That said, to change just an embedded <label>, I'd do something like:

doc = Nokogiri::HTML::DocumentFragment.parse('<div class="input"><label>hello</label></div>')
label = doc.at('div label')
label.name = 'span' if label
puts doc.to_html
# >> <div class="input"><span>hello</span></div>

Or:

doc = Nokogiri::HTML::DocumentFragment.parse('<div class="input"><label>hello</label></div>')
label = doc.at('div label')
label.replace("<span>#{ label.text }</span>") if label
puts doc.to_html
# >> <div class="input"><span>hello</span></div>

Nokogiri makes it easy to change the tag's name once you've pointed at it. You can easily change the text inside the <span> by replacing #{ label.text } with whatever you desire.

at('div label') is one way of finding a particular node. It basically means "find the first label tag inside the first div". at means find the first of something, and is similar to using search(...).first. There are CSS and XPath equivalents to both at and search in the Nokogiri::XML::Node documentation if you need those.

How can I add a child to a node at a specific position?

Thanks Pesto for your almost correct solution.

The working solution is:

node.children.first.add_previous_sibling(span_node)

Creating XML nodes using Nokogiri

Adding nodes in Nokogiri is a lot easier than you think it is, but your question isn't very clear.

If you want to duplicate an existing node:

require 'nokogiri'

xml = <<EOT
<mainnode>
<secnode>
<data1></data2>
<data2></data2>
</secnode>
</mainnode>
EOT

doc = Nokogiri::XML(xml)

secnode = doc.at('secnode')
doc.root.add_child secnode.dup
puts doc.to_xml

Which, when run, results in:

<?xml version="1.0"?>
<mainnode>
<secnode>
<data1/>
<data2/>
</secnode>
<secnode>
<data1/>
<data2/>
</secnode></mainnode>

The funky alignment is the result of appending the intervening text nodes used for indentation. The resulting XML is valid.

If you're adding a different set of nodes, it's still easy:

require 'nokogiri'

xml = <<EOT
<mainnode>
<secnode>
<data1></data2>
<data2></data2>
</secnode>
</mainnode>
EOT

node_to_add = <<EOT
<secnode>
<data3 />
<data4 />
</secnode>
EOT

doc = Nokogiri::XML(xml)

doc.root.add_child node_to_add
puts doc.to_xml

Which outputs:

<?xml version="1.0"?>
<mainnode>
<secnode>
<data1/>
<data2/>
</secnode>
<secnode>
<data3/>
<data4/>
</secnode>
</mainnode>

You can use that as a template:

require 'nokogiri'

xml = <<EOT
<mainnode>
<secnode>
<data1></data2>
<data2></data2>
</secnode>
</mainnode>
EOT

v1 = 'foo'
v2 = 'bar'
node_to_add = <<EOT
<secnode>
<data3>#{ v1 }</data3>
<data4>#{ v2 }</data4>
</secnode>
EOT

doc = Nokogiri::XML(xml)

doc.root.add_child node_to_add
puts doc.to_xml

Which looks like:

<?xml version="1.0"?>
<mainnode>
<secnode>
<data1/>
<data2/>
</secnode>
<secnode>
<data3>foo</data3>
<data4>bar</data4>
</secnode>
</mainnode>

Nokogiri makes it very easy to create nodes to be added by using string representations of the XML or HTML, which it then converts on the fly.

How I can add an element?

It's a lot easier than that:

require 'nokogiri'

doc = Nokogiri::XML(<<EOT)
<root>
<node>
</node>
</root>
EOT

doc.at('node').children = '<child>foo</child>'
doc.to_xml
# => "<?xml version=\"1.0\"?>\n<root>\n <node><child>foo</child></node>\n</root>\n"

children= is smart enough to see what you're passing in and will do the dirty work for you. So just use a string to define the new node(s) and tell Nokogiri where to insert it.


doc.at('node').class   # => Nokogiri::XML::Element
doc.at('//node').class # => Nokogiri::XML::Element

doc.search('node').first # => #<Nokogiri::XML::Element:0x3fd1a88c5c08 name="node" children=[#<Nokogiri::XML::Text:0x3fd1a88eda3c "\n ">]>
doc.search('//node').first # => #<Nokogiri::XML::Element:0x3fd1a88c5c08 name="node" children=[#<Nokogiri::XML::Text:0x3fd1a88eda3c "\n ">]>

search is the generic "find a node" method that will take either CSS or XPath selectors. at is equivalent to search('some selector').first. at_css and at_xpath are the specific equivalents of at, just as css and xpath are to search. Use the specific versions if you want, but in general I use the generic versions.


You can't use:

targets = @xml.xpath("./target")
if targets.empty?
targets << Nokogiri::XML::Node.new('target', @xml)
end

targets would be [] (actually an empty NodeSet) if ./target doesn't exist in the DOM. You can't append a node to [], because NodeSet doesn't have an idea of what you're talking about, resulting in a undefined method 'children=' for nil:NilClass (NoMethodError) exception.

Instead you MUST find the specific location where you want to insert the node. at is good for that since it finds just the first location. Of course, if you want to look for multiple places to modify something use search then iterate over the returned NodeSet and modify based on the individual nodes returned.

How can I insert a tree of nodes with namespaces into an existing XML file using Nokogiri?

A nifty little tick if you want to create a builder instance scoped to a namespace is to use Nokogiri::XML::Builder.with(doc.root):

doc = Nokogiri::XML('<?xml version="1.0"?>
<root xmlns:y="foo"></root>')
builder = Nokogiri::XML::Builder.with(doc.root) do |xml|
xml['y'].Shapenode do |sn|
sn.Foo
sn.Bar
end
end

builder.to_xml outputs:

<?xml version="1.0"?>
<root xmlns:y="foo">
<y:Shapenode>
<y:Foo/>
<y:Bar/>
</y:Shapenode>
</root>

Worth noting though is that it mutates doc. If I where to do this I would use Nokogiri::XML::Builder.with(doc.root.dup) which prevents it from mutating the arguments.

You can also just create builders with any arbitrary root with:

builder = Nokogiri::XML::Builder.new do |xml|
xml.root('xmlns:y' => 'bar') do
xml['y'].Shapenode
end
end

builder.doc.xpath('/*').children will slice out the node set.



Related Topics



Leave a reply



Submit