Вы здесь

DOMDocument некорректно добавляет дочерний элемент

0

Вопрос не столько по Drupal, но я в своем кастомчике спустя, почти год, словил первую ошибку поведения. И не могу понять, что происходит не так.

Чтобы не вдаваться в подробности кастом модуля я просто адаптирую куски кода которые пашут некорректно. Допустим, имеем такой html

<h3>Title</h3>

<table>
  <tbody>
    <tr>
      <td>One</td>
      <td>Two</td>
      <td>Three</td>
    </tr>
  </tbody>
</table>

Ничего заурядного в нем нет. Он полностью валидный. Далее скармливаем его этому коду (сразу привожу пример с html в переменной чтобы, если захотите, было проще копипастнуть для теста):

$html = "<h3>Title</h3>

<table>
    <tbody>
        <tr>
            <td>One</td>
            <td>Two</td>
            <td>Three</td>
        </tr>
    </tbody>
</table>
";
$dom = new DOMDocument(NULL, 'UTF-8');
$dom->encoding = 'UTF-8';
$dom->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'), LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);

$wrapper = $dom->createElement('div');
$wrapper->setAttribute('class', 'wrapper');

$tables = $dom->getElementsByTagName('table');
foreach ($tables as $table) {
  $table->setAttribute('class', 'table');
  $wrapper_clone = $wrapper->cloneNode();
  $table->parentNode->replaceChild($wrapper_clone, $table);
  $wrapper_clone->appendChild($table);
}
$html = $dom->saveHTML();

Код делает простые вещи.

  1. Загружает HTML в DOMDocument.
  2. Создает виртуальный html элемент <div class="wrapper"></div>.
  3. Далее находит все элементы типа table.
  4. Циклом проходится по каждому из них.
  5. В цикле добавляет класс table, создает копию виртуального враппера, заменяет таблицу на враппер, затем во враппер, который вставлен вместо таблицы, вставляет саму таблицу.
  6. Результат сохранятся в $html.

Всё работает корректно, но по каким-то причинам если над таблицей стоит какой-то тег, он его воспринимате неправильно.

Результат выходит следующий:

<h3>Title<div class="wrapper"><table class="table">
    <tbody>
        <tr>
            <td>One</td>
            <td>Two</td>
            <td>Three</td>
        </tr>
    </tbody>
</table></div></h3>

То есть все оборачивается как нужно, но по какой-то невнятной причине всё это ещё вставляется в тег выше. Я не могу понять почему так происходит и почему это всплыло только сейчас на одном проекте. Причем всплыло далеко не сразу.

Я пробовал и регресивно гонять элементы вместо foreach и что только не пробовал. Результат всегда один и тот же, он зачем-то лезет в h3. НО! Если, допустим, скопировать html и в ставить его друг за другом 2-3 и более раз, то баг проявляется исключительно в первом случае, все остальные оборачиваются правильно.

Версия Drupal: 
7.x
Вопрос задан 29.06.2017 - 09:47
Аватар пользователя Niklan
Niklan
445

Проблема в том что parentNode отсутствует, так как это просто html а не полноценный документ с body и html. Он возвращает 0, а replaceChild, по всей видимости, думает что это индекс элемента в документе, а 0 какраз h3. Буду пробовать оборачивать и удалять обертку.

Комментарий оставлен 29.06.2017 - 10:22

Ответы

1

Косячит "parentNode" он выдаёт "documentElement" если нет родителя, а когда нет "documentElement" на самом деле это любой первый элемент по логике "DOMDocument" (то есть ваш h3).

Добавляем wrapper любой и он становится "documentElement".

<?php
$html_origin = "<h3>Title</h3>

<table>
    <tbody>
        <tr>
            <td>One</td>
            <td>Two</td>
            <td>Three</td>
        </tr>
    </tbody>
</table>

<table>
    <tbody>
        <tr>
            <td>Two</td>
            <td>Three</td>
        </tr>
    </tbody>
</table>";


$html = "<div>$html_origin</div>";

$dom = new DOMDocument(NULL, 'UTF-8');
$dom->encoding = 'UTF-8';
$dom->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'), LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);

// Find
$wrapper = $dom->createElement('div');
$wrapper->setAttribute('class', 'wrapper');

$tables = $dom->getElementsByTagName('table');
foreach ($tables as $table) {
  $table->setAttribute('class', 'table');
  $wrapper_clone = $wrapper->cloneNode();
  $dom->documentElement->replaceChild($wrapper_clone, $table);
  $wrapper_clone->appendChild($table);
}

// Save
$innerHTML = '';
foreach ($dom->documentElement->childNodes as $childNode) {
    $innerHTML .= $childNode->ownerDocument->saveHTML($childNode);
}

$html = $innerHTML;
Ответ дан 29.06.2017 - 11:27
0

Решил следующим образом, хотя мне решение не очень нравится. Если есть лучше решение, с радостью заменю.

При загрузке html я из строки

$dom->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'), LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);

Убрал метку LIBXML_HTML_NOIMPLIED. Таким образом весь переданный html оборачивается в <html><body>$html</html></body>.

И в конце сделал удаление данных тегов из строки обрезанием строки:

$text = $dom->saveHTML();
// 12 is length of <html><body>, 14 - closing tags.
$text = substr($text, 12, -14);

Регулярки тут не вариант, вдруг внутри окажется <pre><code>, тогда всё полетит в бездну. Решение работает, но все равно, кажется оно каким-то грязным. Но видимо в условиях DOMDocument иначе никак, а тащить из-за такой мелочи либы типа simplehtmldom не хочется, хотя уверен что она такое может провернуть без проблем.

Ответ дан 29.06.2017 - 10:57
Аватар пользователя Niklan
Niklan
445