Обнаружение окончания абзаца HTML5 Связано с сериализацией HTML

Я хотел бы написать программу на Perl, которая делает разумную HTML5-разметку в $_ лучше («более валидной» — я знаю, это звучит как «более насыщенной»). В частности, я хочу попытаться правильно закрыть абзацы с тегами </p> именно там, где их закроют браузеры. Это шаг на пути к преобразованию html в xhtml. Это помогает мне в последующем текстовом анализе полных абзацев.

Спецификация HTML5 говорит, что

  1. Элемент p должен иметь начальный тег.

  2. Конечный тег элемента p может быть опущен, если за элементом p сразу следуют address, article, aside, blockquote, dir, div, dl, fieldset, footer, form, h1, h2, h3, h4, h5, h6, header , hr, menu, nav, ol, p, pre, section, table или ul элемент,

  3. или если в родительском элементе больше нет содержимого и родительский элемент не является элементом a.

Проблемы:

  1. Я считаю, что можно увидеть параграфы, где это не соответствует действительности. Браузеры HTML выводят и вставляют <p> сами по себе. Например, <h1>HEADER</h1> Now is… вставит <p> непосредственно перед Now is…. Я ошибаюсь?

  2. Предположим, что создатель HTML-контента уже правильно вставил <p>. Теперь мне нужно искать вперед, пока он не закончится. Обнаружить открытие из списка из 26 тегов, закрывающих абзац, легко.

  3. Но как я могу определить, есть ли в родительском абзаце больше контента? Могу ли я просто искать следующий </…> из набора вышеуказанных 26 тегов, или мне нужно закодировать машину с полным стеком (предполагая, что все содержимое самих абзацев является допустимым XHTML), чтобы обнаружить конец вмещающего контейнера?

Благодаря @Palec я теперь понимаю, что абзацы — странная концепция в HTML. Попробуй это:

<!DOCTYPE html>
<html>
<head>
<style>
    p { color: blue; }
    p:before { content:"[SP]"; }
    p:after { content:"[EP]"; }
</style>
</head>

<body>

l0

<h1> h1 </h1>

l0

<p> para

<p> para </p>

l0

<p>para
<ol>
<li> l0 <p> para </li>
</ol>
l0

</body>
</html>

Это показывает, что не весь текст представляет собой хотя бы абзац. Я действительно перепутал это с концепцией LaTeX… и думал, что все, что находится на «уровне 0», по умолчанию является абзацем. Нет.


person ivo Welch    schedule 08.02.2014    source источник
comment
Взгляните на HTML::Tidy, возможно, он уже делает все, что вам нужно (и даже больше).   -  person Steffen Ullrich    schedule 09.02.2014
comment
Для начала вам нужно определить, что вы подразумеваете под «абзацем». Он имеет довольно особое (и неясное) значение в черновиках HTML5. Вы действительно это имеете в виду или имеете в виду p элементы?   -  person Jukka K. Korpela    schedule 09.02.2014
comment
спасибо, стеффен. html::tidy работает с html4 и ругает теги html5 (например, детали). спасибо юкка. ответы ниже затрагивают основные моменты.   -  person ivo Welch    schedule 09.02.2014
comment
@ivoWelch Кстати, я думаю, что концепцию абзаца действительно сложно понять на технической практике. Он может содержать странные вещи и при этом восприниматься читателем как один абзац. Я отредактировал свой ответ, чтобы показать, чем абзац TeX отличается от логического абзаца. Сначала я думал, что они одинаковые, но не рассматривал их внимательно.   -  person Palec    schedule 09.02.2014


Ответы (4)


Три концепции абзаца

В HTML 5 есть два отдельных понятия: элемент p и абзац. Я буду называть этот абзац структурным абзацем. В реальном мире я нашел как минимум два других связанных понятия: логический абзац и типографский абзац.

p элемент понятно. Вы это знаете, вы уже цитировали его описание из спецификации.

(структурный) абзац несколько странный концепция для меня. Может быть, он используется программами чтения с экрана или кем-то еще. Его определение в основном говорит, что это непустая серия фразирующего контента не прерывается другими типами контента (без учета a, ins, del и map).

Логический абзац — это то, что люди считают абзацем. Это единица текста, несущая одну мысль. Когда начинается другая (возможно, родственная) мысль, абзац обрывается и начинается новый. Оно составлено из последовательности предложений.

Каждое предложение может иметь не только свою языковую структуру, но и может содержать форматирование. Форматирование не ограничивается тем, что HTML называет фразовым содержимым, но я добавлю, по крайней мере, многострочные предварительно отформатированные фрагменты кода, списки, математические формулы (возможно, занимающие несколько строк, отображающие математику из TeX) и все остальное, что можно использовать в середине. предложения или между предложениями, не нарушая при этом хода мысли. Эту большую разницу между логическим абзацем и двумя другими концепциями можно увидеть в моем вопросе Список или более длинный фрагмент кода внутри абзаца.

Типографский абзац состоит из последовательности строк, а не предложений, и может содержать все, что типографская система может обработать внутри. Первоначально я думал, что это точно такая же концепция, как логический абзац, но это не так.

Это пришло мне в голову, когда я думал о tex. Вы можете узнать об этом из latex, который представляет собой просто большой набор определений для TeX и имеет такое же понятие абзаца. Содержимое буферизуется до тех пор, пока не встретится \par (или пустая строка, которая внутренне преобразуется в \par), затем оно сбрасывается на вывод как один абзац. То, что выглядит как один (логический) абзац, может быть внутренне несколькими абзацами, поскольку его нужно использовать для реализации более сложного поведения алгоритма набора текста. С этой точки зрения он больше напоминает структурный абзац.

Ответы на ваши вопросы

  1. Абзац (структурный) начинается после элемента h1, если присутствует только текстовый узел. Но это не элемент p. Его нельзя стилизовать в CSS с помощью селектора p, его нет в дереве DOM документа и т. д.

    Есть определенные места, где теги элементов отсутствуют в разметке, но элементы все равно создаются. Это относится к тем элементам, у которых начальный тег может быть опущен. Это html, head, body, colgroup и tbody. (По крайней мере, tbody раньше вел себя по-другому в HTML 4, это поведение исходит из XHTML. В HTML его просто не должно быть.) Однако элемент p не тот случай.

  2. Если создатель контента неправильно вставил <p> (это был недействительный HTML 5), как вы могли бы это исправить? Как только это неверно, вы не можете вообще ничего об этом предполагать. Кроме того, опускание конечного тега не является неправильным! На самом деле это не вопрос в этом пункте списка, так что идем дальше…

  3. Вы действительно предполагаете действительный XHTML 5 (т.е. XML-сериализацию HTML 5, в частности, все теги закрыты)? Хорошо, тогда вам нужно отслеживать информацию о глубине дерева документов (или складывать, если вам нужны данные в структурированной форме). В противном случае вам придется реализовать полный синтаксический анализ HTML 5, поскольку может быть, например. option с опущенным конечным тегом внутри (внутри select). Это нарушит отслеживание глубины.

    Абзац закрывается, когда начинается один из именованных элементов, или когда встречается закрывающий тег </p>, или когда встречается конец родительского элемента. Мммм. Когда вы предполагаете, что XHTML действителен только внутри, вам все равно нужно реализовать правила закрытия для всех элементов, чтобы иметь возможность определять конец родительского элемента… Это будет непросто.

Преобразование сериализации HTML в XML HTML 5

В комментарии вы сказали, что преобразование HTML 5 в XHTML 5 является вашим вариантом использования.

Не используйте регулярные выражения!

Регулярные выражения не были предназначены для выполнения таких сложных задач, как синтаксический анализ HTML. Все, что вы попробуете, будет просто эвристикой. Настоящие регулярные выражения вообще не могут анализировать HTML, потому что HTML не является обычный язык. Забудем о том, что perlre намного мощнее; с большой силой приходит большая ответственность, и вы не должны использовать силу, когда она неправильная. Здесь на SO есть чрезвычайно известный ответ на вопрос по этой теме, настоящее произведение искусства. Джефф Этвуд написал подробнее на эту тему, цитируя этот ответ в начале и объясняя важность понимания ваших инструментов в остальной части статьи.

Я считаю, что текстовый подход к этой цели плох. HTML часто называют супом тегов, и, в отличие от того, что говорит Википедия, я встречал этот термин используется в отношении текстового подхода к его созданию и изменению в целом (а именно document.write() и element.innerHTML).

Кстати, это одна из проблем, которую XHTML очень хорошо решил путем отмены. В JavaScript вы не можете использовать document.write() с XHTML. Если это работает, вы используете анализатор HTML с документом XHTML — используйте Content-Type HTTP-заголовок с application/xhtml+xml; charset=utf-8 вместо используемого вами типа text/html MIME.

Использовать DOM

Чистое решение™ — это DOM.

Я считаю, что вам следует реализовать (или использовать другую реализацию) парсер HTML, получите дерево DOM и напишите сериализатор в XHTML. Если входной документ недействителен, отклоните его обработку. Или добавьте в свою программу переключатели, которые сообщат ей, как исправить определенные ошибки, которые алгоритм синтаксического анализа не предназначен для обработки. Способов может быть много.

Я не уверен, какие части спецификации вы можете игнорировать, если они вам не интересны. Алгоритм синтаксического анализа стандартизирован, а также указана обработка ошибок. Вы можете найти ярлык, при котором вам не нужно создавать часть дерева DOM и просто оставить соответствующую часть ввода неразборчивой, но вы должны быть уверены, что продолжаете синтаксический анализ в правильной позиции ввода. Это может запутаться и, безусловно, подвержено ошибкам. Поэтому я рекомендую вам этого не делать.

Практичное решение

На практике кажется, что вы можете использовать как минимум два существующих модуля.

Mojolicious — это веб-фреймворк, содержащий Mojo::DOM. Если вам не нужны манипуляции с DOM и вы хотите просто синтаксический анализ и сериализацию, вы можете использовать базовый Mojo::DOM::HTML. HTML может быть проанализирован Mojo::DOM с использованием my $dom = Mojo::DOM->new($html_markup);, результирующий объект DOM может быть настроен на использование сериализации XML с помощью $dom->xml(1);, а сериализация может быть возвращена как $xhtml_markup = "$dom"; или $xhtml_markup = $dom->to_string();. Из Mojo::DOM POD: «Mojo::DOM — это минималистичный и простой парсер HTML/XML DOM с поддержкой селекторов CSS. Он даже попытается интерпретировать поврежденный XML, поэтому вам не следует использовать его для проверки». Пример использования в ответе от amon. Вы можете использовать это решение, если вы уже используете Mojolicious, в противном случае установка всего большого фреймворка будет излишним для этой работы.

HTML::HTML5::Parser и HTML::HTML5::Writer можно использовать для разбора и сериализации HTML5 соответственно. У них, кажется, есть только несколько зависимостей. Хороший код с их использованием можно найти в ответе автора tobyink. Это должно быть решением для тех, кто еще не использует Mojolicious.

person Palec    schedule 08.02.2014
comment
Кстати, использование регулярных выражений для разбора HTML — это явное отсутствие лени, что является первым из трех великих достоинств программиста, как заявил Ларри Уолл, создатель Perl. - person Palec; 10.02.2014

  1. Теги не выводятся во время синтаксического анализа. Большинство элементов могут содержать текст, не включенный в другой тег. Возможно, вы захотите взглянуть на объектную модель документа, которая лежит в основе синтаксиса HTML. Есть не только узлы элементов, но и текстовые узлы.

  2. Да, это так просто.

  3. Измените порядок задачи таким образом, чтобы тег закрывался не закрывающим тегом, который может отсутствовать, а закрывался, когда больше не осталось входных данных, принадлежащих тегу. Как только тег закрывается, следующий за ним соответствующий закрывающий тег будет отброшен.

Однако не стоит пытаться сделать HTML «более корректным». Либо он действителен, либо нет. HTML5 включает множество правил исправления ошибок (об одном из которых идет речь в этом вопросе). Если в спецификации ничего нет, это, вероятно, означает, что это невозможно исправить.

Кроме того, уже существует много хороших парсеров HTML. Например, с Mojolicious вы можете:

use Mojo;

my $bad_html = <<'END';
<p> foo
<p> bar
END

my $dom = Mojo::DOM->new($bad_html);  # parse it into a data structure
my $good_html = "$dom";  # stringifying the data structure makes it good HTML

Вывод:

<p> foo
</p><p> bar
</p>
person amon    schedule 08.02.2014
comment
только что попробовал Mojo::DOM на некоторых html-документах. Он не производит вывод, который нравится XML2. (Я предполагаю, что он двигает меня в неправильном направлении, когда он также преобразует ‹link .../› в ‹link ...›. :-( - person ivo Welch; 09.02.2014
comment
Лучше использовать Mojo::DOM->new($bad_html)->xml(1);? В документе говорится, что стригификация вызывает метод to_string и to_string делает «Визуализация этого элемента и его содержимого в HTML/XML». Я не нашел другого способа сказать, что мне нужен XML. Mojolicious у меня нет и ставить не хочу, поэтому протестировать не могу. - person Palec; 10.02.2014
comment
Прочитав соответствующие части Mojo::DOM и Mojo::DOM::HTML, я думаю, что добавление ->xml(1) решает проблему. - person Palec; 10.02.2014

Хорошо, это, кажется, работает для меня...

#!/usr/bin/env perl

use strict;
use warnings;
use HTML::HTML5::Parser;
use HTML::HTML5::Writer;

my $parser = HTML::HTML5::Parser->new;
my $writer = HTML::HTML5::Writer->new(polyglot => 1);

my $dom = $parser->load_html(IO => \*DATA);

# Loop through all the elements that contain a paragraph
for my $e ( $dom->findnodes('//*[local-name()="p"]/..') )
{
   # Find any text that's floating around free in that element
   for my $t ( $e->findnodes('./text()') )
   {
      # Strip out excess whitespace
      my $text = $t->data;

      # Create a new paragraph element containing the text
      my $new_node = $e->addNewChild($e->namespaceURI, 'p');
      $new_node->appendText($text);

      # Replace free text with a nice paragraph
      $t->replaceNode($new_node);
   }
}

print $writer->document($dom), "\n";

__DATA__
<!DOCTYPE html>
<html>
<head>
<style>
    p { color: blue; }
    p:before { content:"[SP]"; }
    p:after { content:"[EP]"; }
</style>
</head>

<body>

l0

<h1> h1 </h1>

l0

<p> para

<p> para </p>

l0

<p>para
<ol>
<li> l0 <p> para </li>
</ol>
l0

</body>
</html>
person tobyink    schedule 09.02.2014
comment
Вы должны использовать HTML::HTML5::Writer->new(markup => 'xhtml') вместо HTML::HTML5::Writer->new(polyglot => 1). Последнее подразумевается первым, BTW. - person Palec; 10.02.2014
comment
Различия между markup=>'xhtml' и polyglot=>1 довольно минимальны, как реализовано (изменяется только вывод элементов <script> и <style>). Разметка Polyglot всегда должна быть правильно сформированным XML по определению. Теоретически будущая версия HTML::HTML5::Writer может выдавать такие вещи, как <br></br>, если она будет построена с параметрами polyglot => 0, markup => 'xhtml'. Но, как вы правильно заметили, если параметр полиглота не указан, по умолчанию он равен true, если выходная разметка - XHTML. - person tobyink; 10.02.2014
comment
Я думаю, что markup => 'xhtml' по крайней мере лучше иллюстрирует первоначальный замысел, чем polyglot => 1. И я думаю, что это также более перспективно. HTML & polyglot сообщает модулю, что HTML первичен, а совместимость с XHTML — это просто пожелание. Если вы действительно хотите преобразовать HTML в XHTML, вам нужен настоящий XHTML, а не «HTML, больше похожий на XHTML, чем на исходный HTML». Это достигается с помощью markup => 'xhtml'. Если вы хотите одновременно максимальной совместимости с HTML, лучше установить polyglot => 1 дополнительно. Это разумное значение по умолчанию. - person Palec; 10.02.2014

Я думаю, что следующий Perl-код должен быть достаточно консервативным, чтобы сериализовать множество случаев абзаца без вставки плохих замыкателей. ммм...

  my $list= qr/address|article|aside|blockquote|dir|div|dl|fieldset|footer|form|h1|h2|h3|h4|h5|h6|header|hr|menu|na\
v|ol|p|pre|section|table|ul|html|body|li|dt|dd/;

  my $last=$_;
  while (s/(\<p\b.*?\>)(.*?)(\<\/?$list\b.*?\>)/fixup($1,$2,$3)/gmse) {
    ($last eq $_) and last;
    $last= $_;
  }

  sub fixup {
    my ($a,$b,$c) = @_;
    ($_[2] =~ /\<\/p\>/) and return "$a$b$c";
    return "$a$b\<\/p\>$c"
  }
person ivo Welch    schedule 09.02.2014
comment
Хм. Вместо /\<\/p\>/ можно написать m{</p>} и избежать побега из ада. \< совершенно не нужен, эквивалентен просто <. Аналогично для \\>. И вам точно не нужны такие вещи в строках (возвращаемое значение fixup). Так же на замену рекомендую s{…}{…}. Вы не используете утверждения ^ и $, поэтому модификатор m лишний. Почему вы используете $_[2], если у вас уже есть $c? - person Palec; 10.02.2014