Как вы форматируете структуры DOM в PHP?

Моей первой предположения были классы PHP DOM (с параметром formatOutput ). Тем не менее, я не могу заставить этот блок HTML форматироваться и выводить правильно. Как вы можете видеть, отступ и выравнивание неверны.

$html = ' <html> <body> <div> <div> <div> <p>My Last paragraph</p> <div> This is another text block and some other stuff.<br><br> Again we will start a new paragraph and some other stuff <br> </div> </div> <div> <div> <h1>Another Title</h1> </div> <p>Some text again <b>for sure</b></p> </div> </div> <div> <pre><code> <span>&lt;html&gt;</span> <span>&lt;head&gt;</span> <span>&lt;title&gt;</span> Page Title <span>&lt;/title&gt;</span> <span>&lt;/head&gt;</span> <span>&lt;/html&gt;</span> </code></pre> </div> </div> </body> </html>'; header('Content-Type: text/plain'); libxml_use_internal_errors(TRUE); $dom = new DOMDocument; $dom->preserveWhiteSpace = false; $dom->formatOutput = true; $dom->loadHTML($html); print $dom->saveHTML(); 

Обновление: я добавил в пример предварительно форматированный блок кода.

Вот некоторые улучшения по поводу ответа @hijarian:

Ошибки LibXML

Если вы не вызываете libxml_use_internal_errors(true) , PHP выведет все найденные ошибки HTML. Однако, если вы вызываете эту функцию, ошибки не будут подавлены, вместо этого они отправятся в кучу, которую вы можете проверить, вызвав libxml_get_errors() . Проблема в том, что он ест память, и DOMDocument, как известно, очень придирчив. Если вы обрабатываете большое количество файлов в пакетном режиме, в конечном итоге у вас не хватит памяти. Для этого есть два решения:

 if (libxml_use_internal_errors(true) === true) { libxml_clear_errors(); } 

Поскольку libxml_use_internal_errors(true) возвращает предыдущее значение этого параметра (по умолчанию false ), это приводит к только устранению ошибок при запуске более одного раза (как в пакетной обработке).

Другой вариант – передать LIBXML_NOERROR | LIBXML_NOWARNING Флаги loadHTML() метода loadHTML() . К сожалению, по неизвестным мне причинам это все еще оставляет пару ошибок.

Не забывайте, что DOMDocument всегда выводит ошибку (даже при использовании внутренних ошибок libxml и установке libxml флагов), если вы передадите пустую (или пустую ) строку методам load*() .

Regex

Регулярное выражение />\s*</im не имеет большого смысла, лучше использовать ~>[[:space:]]++<~m также catch \v (вертикальные вкладки) и заменять только если на самом деле существуют пространства ( + вместо * ), не возвращая ( ++ ) – что быстрее – и отбрасывать служебные данные insensitve (так как пробелы не имеют случая).

Вы также можете нормализовать новые строки для \n и других управляющих символов (особенно если источник HTML неизвестен), так как a \r вернется как &#23; после saveXML() например.

DOMDocument::$preserveWhitespace бесполезно и ненужно после запуска указанного выше регулярного выражения.

О, и я не вижу необходимости защищать пустые пре-подобные теги здесь. Простые пробелы являются бесполезными.

Дополнительные флаги для loadHTML()

  • LIBXML_COMPACT – «это может ускорить ваше приложение без необходимости изменения кода»
  • LIBXML_NOBLANKSнужно выполнить больше тестов на этом
  • LIBXML_NOCDATAнужно выполнить больше тестов на этом
  • LIBXML_NOXMLDECL – документально, но не реализовано = (

UPDATE: установка любого из этих параметров приведет к не форматированию вывода.

На saveXML()

Метод DOMDocument::saveXML() выдаст объявление XML. Мы должны вручную очистить его (поскольку LIBXML_NOXMLDECL не реализован). Для этого мы могли бы использовать комбинацию substr() + strpos() чтобы искать первый разрыв строки или даже использовать регулярное выражение для его очистки.

Другой вариант, который, похоже, имеет дополнительное преимущество :

 $dom->saveXML($dom->documentElement); 

Другое дело, если у вас встроенные теги пусты, например, b , i или li :

 <b class="carret"></b> <i class="icon-dashboard"></i> Dashboard <li class="divider"></li> 

Метод saveXML() серьезно заманит их (поместив следующий элемент внутри пустого), испортив весь ваш HTML. У Tidy также есть аналогичная проблема, за исключением того, что она просто удаляет узел.

Чтобы исправить это, вы можете использовать флаг LIBXML_NOEMPTYTAG вместе с saveXML() :

 $dom->saveXML($dom->documentElement, LIBXML_NOEMPTYTAG); 

Эта опция преобразует пустые (ака самозакрывающиеся) теги в встроенные теги и позволяет также пустые встроенные теги.

Фиксация HTML [5]

Со всем, что мы делали до сих пор, наш вывод HTML имеет две основные проблемы:

  1. нет DOCTYPE (он был удален, когда мы использовали $dom->documentElement )
  2. пустые теги теперь являются встроенными тегами, что означает, что один <br /> превратился в два ( <br></br> ) и так далее

Исправление первого довольно просто, поскольку HTML5 довольно разрешительный:

 "<!DOCTYPE html>\n" . $dom->saveXML($dom->documentElement, LIBXML_NOEMPTYTAG); 

Чтобы вернуть наши пустые теги, выполните следующие действия:

  • area
  • base
  • basefont ( устарел в HTML5 )
  • br
  • col
  • command
  • embed
  • frame ( устаревший в HTML5 )
  • hr
  • img
  • input
  • keygen
  • link
  • meta
  • param
  • source
  • track
  • wbr

Мы можем либо использовать str_[i]replace в цикле:

 foreach (explode('|', 'area|base|basefont|br|col|command|embed|frame|hr|img|input|keygen|link|meta|param|source|track|wbr') as $tag) { $html = str_ireplace('>/<' . $tag . '>', ' />', $html); } 

Или регулярное выражение:

 $html = preg_replace('~></(?:area|base(?:font)?|br|col|command|embed|frame|hr|img|input|keygen|link|meta|param|source|track|wbr)>\b~i', '/>', $html); 

Это дорогостоящая операция, я не тестировал их, поэтому я не могу сказать вам, какой из них лучше, но я бы предположил preg_replace() . Кроме того, я не уверен, нужна ли нечувствительная к регистру версия. У меня такое впечатление, что XML-теги всегда сглажены. UPDATE: Теги всегда имеют нижнее значение.

В <script> и <style>

Эти теги всегда будут иметь свой контент (если он существует), инкапсулированный в (uncommented) блоки CDATA, что, вероятно, нарушит их смысл. Вам нужно будет заменить эти жетоны регулярным выражением.

Реализация

 function DOM_Tidy($html) { $dom = new \DOMDocument(); if (libxml_use_internal_errors(true) === true) { libxml_clear_errors(); } $html = mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'); $html = preg_replace(array('~\R~u', '~>[[:space:]]++<~m'), array("\n", '><'), $html); if ((empty($html) !== true) && ($dom->loadHTML($html) === true)) { $dom->formatOutput = true; if (($html = $dom->saveXML($dom->documentElement, LIBXML_NOEMPTYTAG)) !== false) { $regex = array ( '~' . preg_quote('<![CDATA[', '~') . '~' => '', '~' . preg_quote(']]>', '~') . '~' => '', '~></(?:area|base(?:font)?|br|col|command|embed|frame|hr|img|input|keygen|link|meta|param|source|track|wbr)>~' => ' />', ); return '<!DOCTYPE html>' . "\n" . preg_replace(array_keys($regex), $regex, $html); } } return false; } 

Вот комментарий на php.net: http://ru2.php.net/manual/en/domdocument.save.php#88630

Похоже, когда вы загружаете HTML из строки (как и вы), DOMDocument становится ленивым и не форматирует что-либо в нем.

Вот работающее решение вашей проблемы:

 // Clean your HTML by hand first $html = preg_replace('/>\s*</im', '><', $html); $dom = new DOMDocument; $dom->loadHTML($html); $dom->formatOutput = true; $dom->preserveWhitespace = false; // Use saveXML(), not saveHTML() print $dom->saveXML(); 

В принципе, вы выбрасываете пробелы между тегами и используете saveXML () вместо saveHTML (). saveHTML () просто не работает в этой ситуации. Однако вы получите декларацию XML в первой строке текста.