Преобразование пробелов между тегами PRE через DOM-парсер

Regex была моей оригинальной идеей в качестве решения, хотя вскоре стало очевидно, что парсер DOM будет более уместным … Я хотел бы преобразовать пробелы в   между тегами PRE внутри строки текста HTML. Например:

 <table atrr="zxzx"><tr> <td>adfa a adfadfaf></td><td><br /> dfa dfa</td> </tr></table> <pre class="abc" id="abc"> abc 123 <span class="abc">abc 123</span> </pre> <pre>123 123</pre> 

в (обратите внимание, что пространство в атрибуте тега span сохраняется):

 <table atrr="zxzx"><tr> <td>adfa a adfadfaf></td><td><br /> dfa dfa</td> </tr></table> <pre class="abc" id="abc"> abc&nbsp;123 <span class="abc">abc&nbsp;123</span> </pre> <pre>123 123</pre> 

Результат должен быть сериализован обратно в строковый формат, для использования в другом месте.

Это несколько сложно, если вы хотите вставить &nbsp; Объекты без DOM, преобразующие амперсанд в &amp; поскольку объекты – это узлы и пробелы – это только символьные данные. Вот как это сделать:

 $dom = new DOMDocument; $dom->loadHtml($html); $xp = new DOMXPath($dom); foreach ($xp->query('//text()[ancestor::pre]') as $textNode) { $remaining = $textNode; while (($nextSpace = strpos($remaining->wholeText, ' ')) !== FALSE) { $remaining = $remaining->splitText($nextSpace); $remaining->nodeValue = substr($remaining->nodeValue, 1); $remaining->parentNode->insertBefore( $dom->createEntityReference('nbsp'), $remaining ); } } 

Получение всех элементов pre и работа с их nodeValues ​​не работает здесь, потому что атрибут nodeValue будет содержать объединенные значения DOMText всех дочерних элементов, например, он будет включать в себя nodeValue дочерних элементов span. Установка nodeValue в элементе pre приведет к их удалению.

Поэтому вместо того, чтобы извлекать предварительные узлы, мы извлекаем все узлы DOMText, у которых родительский элемент pre размещен где-то на своей оси:

 DOMElement pre DOMText "abc 123" <-- picking this DOMElement span DOMText "abc 123" <-- and this one DOMElement DOMText "123 123" <-- and this one 

Затем мы проходим через каждый из этих узлов DOMText и разбиваем их на отдельные узлы DOMText в каждом пространстве. Мы удаляем пространство и вставляем узел Entity Entity перед разделенным узлом, поэтому в конце вы получаете дерево, подобное

 DOMElement pre DOMText "abc" DOMEntity nbsp DOMText "123" DOMElement span DOMText "abc" DOMEntity nbsp DOMText "123" DOMElement DOMText "123" DOMEntity nbsp DOMText "123" 

Поскольку мы работали только с узлами DOMText, любые DOMElements остаются нетронутыми и поэтому сохраняют элементы span внутри элемента pre.

Предостережение:

Ваш фрагмент недействителен, поскольку он не имеет корневого элемента. При использовании loadHTML libxml добавит недостающую структуру в DOM, что означает, что вы получите свой фрагмент, включая тег DOCTYPE, html и body.

Если вы хотите вернуть оригинальный фрагмент, вам нужно будет получитьElementsByTagName узел тела и получить все дети, чтобы получить innerHTML . К сожалению, в реализации PHP DOM нет функции или свойства innerHTML , поэтому мы должны сделать это вручную:

 $innerHtml = ''; foreach ($dom->getElementsByTagName('body')->item(0)->childNodes as $child) { $tmp_doc = new DOMDocument(); $tmp_doc->appendChild($tmp_doc->importNode($child,true)); $innerHtml .= $tmp_doc->saveHTML(); } echo $innerHtml; 

Также см

Я вижу короткое пришествие моего предыдущего ответа. Ниже приведено обходное решение для сохранения тегов внутри <pre> :

 <?php $test = file_get_contents('input.html'); $dom = new DOMDocument('1.0'); $dom->loadHTML($test); $xpath = new DOMXpath($dom); $pre = $xpath->query('//pre//text()'); // manipulate nodes of type XML_TEXT_NODE foreach($pre as $e) { $e->nodeValue = str_replace(' ', '__REPLACEMELATER__', $e->nodeValue); // when you attempt to write &nbsp; in a dom node // the & will be converted to &amp; :( } $temp = $dom->saveHTML(); $temp = str_replace('<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">', '', $temp); $temp = str_replace('<html>', '', $temp); $temp = str_replace('<body>', '', $temp); $temp = str_replace('</body>', '', $temp); $temp = str_replace('</html>', '', $temp); $temp = str_replace('__REPLACEMELATER__', '&nbsp;', $temp); echo $temp; ?> 

вход

 <p>paragraph 1 remains untouched</p> <pre>preformatted 1</pre> <div> <pre>preformatted 2</pre> </div> <div> <pre>preformatted 3 <span class="foo">span text</span> preformatted 3</pre> </div> <div> <pre>preformatted 4 <span class="foo">span <b class="bla">bold test</b> text</span> preformatted 3</pre> </div> 

Вывод

 <p>paragraph 1 remains untouched</p> <pre>preformatted&nbsp;1</pre> <div> <pre>preformatted&nbsp;2</pre> </div> <div> <pre>preformatted&nbsp;3&nbsp;<span class="foo">span&nbsp;text</span>&nbsp;preformatted&nbsp;3</pre> </div> <div> <pre>preformatted&nbsp;4&nbsp;<span class="foo">span&nbsp;<b class="bla">bold&nbsp;test</b>&nbsp;text</span>&nbsp;preformatted&nbsp;3</pre> </div> 

Примечание №1

DOMDocument::saveHTML() в PHP> = 5.3.6 позволяет указать узел для вывода. В противном случае вы можете использовать str_replace() или preg_replace() чтобы исключить теги doctype, html и body.

Заметка 2

Этот трюк, похоже, работает и приводит к еще одной строке кода, но я не уверен, что он будет работать:

 $e->nodeValue = utf8_encode(str_replace(' ', "\xA0", $e->nodeValue)); // dom library will attempt to convert 0xA0 to &nbsp; // nodeValue expects utf-8 encoded data but 0xA0 is not valid in this encoding // hence replaced string must be utf-8 encoded