Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Add a class to an element with Nokogiri

Tags:

ruby

nokogiri

Apparently Nokogiri's add_class method only works on NodeLists, making this code invalid:

doc.search('a').each do |anchor|
  anchor.inner_text = "hello!"
  anchor.add_class("whatever") # WHOOPS!
end

What can I do to make this code work? I figured it'd be something like

doc.search('a').each do |anchor|
  anchor.inner_text = "hello!"
  Nokogiri::XML::NodeSet.new(anchor).add_class("whatever")
end

but this doesn't work either. Please tell me I don't have to implement my own add_class for single nodes!

like image 853
Tom Lehman Avatar asked Jan 30 '11 04:01

Tom Lehman


3 Answers

A CSS class is just another attribute on an element:

doc.search('a').each do |anchor|
  anchor.inner_text = "hello!"
  anchor['class']="whatever"
end

Since CSS classes are space-delimited in the attribute, if you're not sure if one or more classes might already exist you'll need something like

anchor['class'] ||= ""
anchor['class'] = anchor['class'] << " whatever"

You need to explicitly set the attribute using = instead of just mutating the string returned for the attribute. This, for example, will not change the DOM:

anchor['class'] ||= ""
anchor['class'] << " whatever"

Even though it results in more work being done, I'd probably do this like so:

class Nokogiri::XML::Node
  def add_css_class( *classes )
    existing = (self['class'] || "").split(/\s+/)
    self['class'] = existing.concat(classes).uniq.join(" ")
  end
end

If you don't want to monkey-patch the class, you could alternatively:

module ClassMutator
  def add_css_class( *classes )
    existing = (self['class'] || "").split(/\s+/)
    self['class'] = existing.concat(classes).uniq.join(" ")
  end
end

anchor.extend ClassMutator
anchor.add_css_class "whatever"

Edit: You can see that this is basically what Nokogiri does internally for the add_class method you found by clicking on the class to view the source:

# File lib/nokogiri/xml/node_set.rb, line 136
def add_class name
  each do |el|
    next unless el.respond_to? :get_attribute
    classes = el.get_attribute('class').to_s.split(" ")
    el.set_attribute('class', classes.push(name).uniq.join(" "))
  end
  self
end
like image 74
Phrogz Avatar answered Nov 03 '22 21:11

Phrogz


Nokogiri's add_class, works on a NodeSet, like you found. Trying to add the class inside the each block wouldn't work though, because at that point you are working on an individual node.

Instead:

require 'nokogiri'

html = '<p>one</p><p>two</p>'
doc = Nokogiri::HTML(html)

doc.search('p').tap{ |ns| ns.add_class('boo') }.each do |n|
  puts n.text
end
puts doc.to_html

Which outputs:

# >> one
# >> two
# >> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
# >> <html><body>
# >> <p class="boo">one</p>
# >> <p class="boo">two</p>
# >> </body></html>

The tap method, implemented in Ruby 1.9+, gives access to the nodelist itself, allowing the add_class method to add the "boo" class to the <p> tags.

like image 3
the Tin Man Avatar answered Nov 03 '22 21:11

the Tin Man


Old thread, but it's the top Google hit. You can now do this with the append_class method without having to mess with space-delimiters:

doc.search('a').each do |anchor|
  anchor.inner_text = "hello!"
  anchor.append_class('whatever')
end
like image 2
Heston Hoffman Avatar answered Nov 03 '22 23:11

Heston Hoffman