Nokogiri equivalent of jQuery closest() method for finding first matching ancestor in tree
Question
jQuery has a lovely if somewhat misnamed method called closest() that walks up the DOM tree looking for a matching element. For example, if I've got this HTML:
<table src="foo">
<tr>
<td>Yay</td>
</tr>
</table>
Assuming element
is set to <td>
, then I can figure the value of src
like this:
element.closest('table')['src']
And that will cleanly return "undefined" if either of the table element or its src attribute are missing.
Having gotten used to this in Javascriptland, I'd love to find something equivalent for Nokogiri in Rubyland, but the closest I've been able to come up with is this distinctly inelegant hack using ancestors():
ancestors = element.ancestors('table')
src = ancestors.any? ? first['src'] : nil
The ternary is needed because first returns nil if called on an empty array. Better ideas?
Solution
You can call first
on an empty array, the problem is that it will return nil
and you can't say nil['src']
without getting sad. You could do this:
src = (element.ancestors('table').first || { })['src']
And if you're in Rails, you could use try
thusly:
src = element.ancestors('table').first.try(:fetch, 'src')
If you're doing this sort of thing a lot then hide the ugliness in a method:
def closest_attr_from(e, selector, attr)
a = e.closest(selector)
a ? a[attr] : nil
end
and then
src = closest_attr_from(element, 'table', 'src')
You could also patch it right into Nokogiri::XML::Node (but I wouldn't recommend it):
class Nokogiri::XML::Node
def closest(selector)
ancestors(selector).first
end
def closest_attr(selector, attr)
a = closest(selector)
a ? a[attr] : nil
end
end
OTHER TIPS
You can also do this with xpath:
element.xpath('./ancestor::table[1]')
You want the src
attribute of the closest table ancestor, if it exists? Instead of getting an element that might exist via XPath and then maybe getting the attribute via Ruby, ask for the attribute directly in XPath:
./ancestor::table[1]/@src
You'll get either the attribute or nil:
irb(main):001:0> require 'nokogiri'
#=> true
irb(main):002:0> xml = '<r><a/><table src="foo"><tr><td /></tr></table></r>'
#=> "<r><a/><table src=\"foo\"><tr><td /></tr></table></r>"
irb(main):003:0> doc = Nokogiri.XML(xml)
#=> #<Nokogiri::XML::Document:0x195f66c name="document" children=…
irb(main):004:0> doc.at('td').at_xpath( './ancestor::table[1]/@src' )
#=> #<Nokogiri::XML::Attr:0x195f1bc name="src" value="foo">
irb(main):005:0> doc.at('a').at_xpath( './ancestor::table[1]/@src' )
#=> nil