Как поместить группу ‹p› внутри ‹div›

Я хотел бы найти способ получить результат HTML (упомянутый ниже), используя следующий код Ruby и Нокогири:

require 'rubygems'
require 'nokogiri'

value = Nokogiri::HTML.parse(<<-HTML_END)
  "<html>
    <body>
      <p id='1'>A</p>
      <p id='2'>B</p>
      <h1>Bla</h1>
      <p id='3'>C</p>
      <p id='4'>D</p>
      <p id='5'>E</p>
    </body>
  </html>"
HTML_END

# The selected-array is given by the application.
# It consists of a sorted array with all ids of 
# <p> that need to be enclosed by the <div>
selected = ["2","3","4"]
first_p = selected.first
last_p = selected.last

#
# WHAT RUBY CODE DO I NEED TO INSERT HERE TO GET
# THE RESULTING HTML AS SEEN BELOW?
#

Результирующий HTML должен выглядеть так (обратите внимание на вставленный <div id='XYZ'>):

<html>
  <body>
    <p id='1'>A</p>
    <div id='XYZ'>
      <p id='2'>B</p>
      <h1>Bla</h1>
      <p id='3'>C</p>
      <p id='4'>D</p>
    </div>
    <p id='5'>E</p>
  </body>
</html>

person Javier    schedule 11.03.2009    source источник


Ответы (2)


В таких ситуациях вы обычно хотите использовать любой SAX-интерфейс, предлагаемый базовой библиотекой, чтобы просматривать и переписывать входной XML (или XHTML) с сохранением состояния и последовательно:

require 'nokogiri'
require 'CGI'

Nokogiri::XML::SAX::Parser.new(
  Class.new(Nokogiri::XML::SAX::Document) {
    def initialize first_p, last_p
      @first_p, @last_p = first_p, last_p
    end

    def start_document
      puts '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">'
    end

    def start_element name, attrs = []
      attrs = Hash[*attrs]
      @depth += 1 unless @depth.nil?
      print '<div>' if name=='p' && attrs['id'] == @first_p
      @depth = 1    if name=='p' && attrs['id'] == @last_p && @depth.nil?
      print "<#{ [ name, attrs.collect { |k,v| "#{k}=\"#{CGI::escapeHTML(v)}\"" } ].flatten.join(' ') }>"
    end

    def end_element name
      @depth -= 1 unless @depth.nil?
      print "</#{name}>"
      if @depth == 0
        print '</div>'
        @depth = nil
      end
    end

    def cdata_block string
      print "<![CDATA[#{CGI::escapeHTML(string)}]]>"
    end

    def characters string
      print CGI::escapeHTML(string)
    end

    def comment string
      print "<!--#{string}-->"
    end
  }.new('2', '4')
).parse(<<-HTML_END)
  <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
  <html>
    <body>
      <!-- comment -->
      <![CDATA[
        cdata goes here
      ]]>
      &quot;special&quot; entities 
      <p id="1">A</p>
      <p id="2">B</p>
      <p id="3">C</p>
      <p id="4">D</p>
      <p id="5">E</p>
      <emptytag/>
    </body>
  </html>
HTML_END

Кроме того, вы также можете использовать интерфейс модели DOM (вместо интерфейса SAX) для загрузки весь документ в память (так же, как вы начали делать в исходном вопросе), а затем выполните манипуляции с узлами (вставка и удаление) следующим образом:

require 'rubygems'
require 'nokogiri'

doc = Nokogiri::HTML.parse(<<-HTML_END)
  <html>
    <body>
      <p id='1'>A</p>
      <p id='2'>B</p>
      <p id='3'>C</p>
      <p id='4'>D</p>
      <p id='5'>E</p>
    </body>
  </html>
HTML_END

first_p = "2"
last_p = "4"

doc.css("p[id=\"#{first_p}\"] ~ p[id=\"#{last_p}\"]").each { |node|
  div_node = nil
  node.parent.children.each { |sibling_node|
    if sibling_node.name == 'p' && sibling_node['id'] == first_p
      div_node = Nokogiri::XML::Node.new('div', doc)
      sibling_node.add_previous_sibling(div_node)
    end
    unless div_node.nil?
      sibling_node.remove
      div_node << sibling_node
    end
    if sibling_node.name == 'p' && sibling_node['id'] == last_p
      div_node = nil
    end
  }
}

puts doc
person vladr    schedule 11.03.2009
comment
Это неправильно. ‹div› может содержать другие элементы блочного уровня. - person Zack The Human; 11.03.2009
comment
@zacm моя ошибка, я думал о span вместо div - person vladr; 11.03.2009
comment
Хм, это выглядит довольно сложно. Hpricot предлагает простые способы изменения HTML-кода (wiki.github.com/why/hpricot/hpricot -altering), поэтому я не могу себе представить, чтобы Nokogiri не предложил что-то подобное... плохо документация Nokogiri не так хороша, как у Hpricot. :( - person Javier; 12.03.2009
comment
@Javier, посмотрите мое обновление с DOM-способом ведения дел (например, hpricot) ... не намного проще, учитывая конкретную проблему, которую вы пытаетесь решить (если бы только она поддерживала более продвинутые селекторы CSS3 ...), но все же - person vladr; 12.03.2009
comment
@Vlad: Какие более продвинутые селекторы CSS3 вы хотели бы видеть в поддержке? Поскольку Nokogiri поддерживает CSS3, его разработчик, вероятно, был бы весьма заинтересован в таком запросе функции. - person Javier; 12.03.2009
comment
Кстати: кто-нибудь знает хорошее руководство/учебник по Нокогири? Хотя бы что-то вроде статей hpricot-github-wiki? Я ничего не мог найти, и я действительно изо всех сил пытаюсь правильно использовать Нокогири. - person Javier; 12.03.2009
comment
@Vlad: Пока я не забыл, большое спасибо за обновление вашего ответа! - person Javier; 12.03.2009
comment
@Vlad: я пытаюсь понять ваш код, и это, вероятно, довольно n00b вопрос, но не могли бы вы объяснить, что делает селектор css (на самом деле ~ между двумя абзацами)? Большое Вам спасибо. - person Javier; 12.03.2009
comment
Я только что увидел, что забыл упомянуть, что у меня есть отсортированный массив с выбранными значениями внутри. Я исправил свой вопрос. - person Javier; 12.03.2009
comment
@Javier, спецификации селекторов CSS3 предписывают, что ':not()' может относиться только к простым селекторам; если бы это было не так, все элементы между двумя P с идентификаторами first_p и last_p соответственно могли бы быть получены с помощью одного селектора без всех 'if внутри цикла выше - person vladr; 12.03.2009
comment
Например. 'p#first_p, p#first_p ~ :not(p#last_p ~ *)' выберет и вернет все элементы из p#fisrt_p до p#last_p включительно за один раз, которые вы могли бы затем удалить сразу из DOM и повторно вставлен в DIV, хотя это все равно не было бы тривиальным, учитывая вашу конкретную проблему. - person vladr; 12.03.2009

Это рабочее решение, которое я внедрил в свой проект (Vlad@SO & Whitelist@irc#rubyonrails: Спасибо за вашу помощь и вдохновение.):

require 'rubygems'
require 'nokogiri'

value = Nokogiri::HTML.parse(<<-HTML_END)
  "<html>
    <body>
      <p id='1'>A</p>
      <p id='2'>B</p>
      <h1>Bla</h1>
      <p id='3'>C</p>
      <p id='4'>D</p>
      <p id='5'>E</p>
    </body>
  </html>"
HTML_END

# The selected-array is given by the application.
# It consists of a sorted array with all ids of 
# <p> that need to be enclosed by the <div>
selected = ["2","3","4"]

# We want an elements, not nodesets!
# .first returns Nokogiri::XML::Element instead of Nokogiri::XML::nodeset
first_p = value.css("p##{selected.first}").first
last_p = value.css("p##{selected.last}").first
parent = value.css('body').first

# build and set new div_node
div_node = Nokogiri::XML::Node.new('div', value)
div_node['class'] = 'XYZ'

# add div_node before first_p
first_p.add_previous_sibling(div_node)

selected_node = false

parent.children.each do |tag|
  # if it's the first_p
  selected_node = true if selected.include? tag['id']
  # if it's anything between the first_p and the last_p
  div_node.add_child(tag) if selected_node
  # if it's the last_p
  selected_node = false if selected.last == tag['id']
end

puts value.to_html
person Javier    schedule 13.03.2009