Добавление класса к элементу с помощью Nokogiri

По-видимому, метод Nokogiri add_class работает только с NodeList s, что делает этот код недействительным:

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

Что я могу сделать, чтобы этот код работал? Я решил, что это будет что-то вроде

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

но это тоже не работает. Скажите, мне не нужно реализовывать собственные add_class для одиночных узлов!

Ответ 1

Класс CSS - это еще один атрибут элемента:

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

Поскольку классы CSS ограничены пространством в атрибуте, если вы не уверены, что один или несколько классов могут уже существовать, вам понадобится что-то вроде

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

Вам нужно явно установить атрибут с помощью = вместо того, чтобы просто изменять строку, возвращаемую для атрибута. Это, например, не изменит DOM:

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

Несмотря на то, что это приводит к большей работе, я бы сделал это так:

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

Если вы не хотите, чтобы обезьяна-патч класса, вы могли бы альтернативно:

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"

Изменить. Вы можете видеть, что это в основном то, что Nokogiri делает внутренне для найденного метода add_class, нажав на класс для просмотра источника:

# 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

Ответ 2

Nokogiri add_class работает над узлом NodeSet, как вы его нашли. Попытка добавления класса внутри блока each работать не будет, потому что в этот момент вы работаете с отдельным node.

Вместо

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

Какие выходы:

# >> 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>

Метод tap, реализованный в Ruby 1.9+, предоставляет доступ к самому нодлисту, позволяя add_class методу добавить класс "boo" в теги <p>.