Kodokon kodokon.com

От HTML к DOM: разбор и восстановление после ошибок

Пойми, как браузер превращает любой поток HTML, даже некорректный, в детерминированное дерево DOM.

10 мин · 3 вопросов

Открыть этот урок в Kodokon

В отличие от XML, в HTML не существует такого понятия, как фатальная ошибка: спецификация WHATWG описывает, байт за байтом, как любой входной поток - даже некорректный - должен превратиться в дерево DOM. Парсер работает в два этапа: токенизатор нарезает поток на токены (теги, текст, комментарии), затем построитель дерева вставляет их согласно конечному автомату, состояния которого называются режимами вставки (in body, in table и так далее). У каждой «ошибки разбора» есть стандартизированное восстановление: два соответствующих спецификации браузера дают ровно одинаковый DOM.

HTML
<table>
  <tr><td>Cell</td></tr>
</table>
Полученный DOM содержит tbody, который ты никогда не писал.

Парсер вставляет неявные теги: вокруг строк появляется <tbody>, а <li>, <p> или <option> закрываются автоматически перед определёнными открывающими тегами. Ещё радикальнее: любое содержимое, найденное внутри <table> там, где оно не разрешено, выносится из неё и вставляется прямо перед ней - этот механизм называется foster parenting. Твои CSS-селекторы и твои скрипты всегда работают с исправленным DOM, а не с твоим исходником.

JAVASCRIPT
const html =
  "<table><div>out</div><tr><td>ok</td></tr></table>";
const doc = new DOMParser()
  .parseFromString(html, "text/html");
console.log(doc.body.innerHTML);
// <div>out</div><table>...</table>
Foster parenting, наблюдаемый через DOMParser.

Неправильно вложенные теги форматирования запускают алгоритм агентства по усыновлению (adoption agency algorithm) - одну из самых коварных частей спецификации: парсер клонирует и перераспределяет активные элементы форматирования, чтобы получить корректно вложенное дерево. Перекрещенная разметка ниже показывает результат.

HTML
<p>
  <b>One <i>two</b> three</i>
</p>
Полученный DOM: <b>One <i>two</i></b><i> three</i> - элемент i разделён на два.

Парсер работает потоково: он строит дерево по мере поступления байтов. Синхронный <script> приостанавливает его - скрипт может вызвать document.write и вставить токены прямо в поток, - но сканер предзагрузки продолжает просматривать оставшийся HTML, чтобы заранее загрузить изображения, таблицы стилей и скрипты. Вызванный после окончания разбора, document.write запускает document.open и стирает всю страницу: это одна из самых старых ловушек платформы.

Проверка знаний

Убедись, что запомнил ключевые моменты этого урока.

  1. Что произойдёт с <div> в <table><div>Out</div><tr><td>A</td></tr></table>?
    • Он молча удаляется из DOM
    • Он перемещается перед таблицей механизмом foster parenting
    • Он оборачивается в созданную для него ячейку <td>
    • Он остаётся прямым потомком таблицы
  2. Как спецификация HTML обрабатывает некорректный документ?
    • Она позволяет каждому браузеру выбрать свою стратегию восстановления
    • Она требует переключиться в режим совместимости (quirks mode)
    • Она задаёт детерминированный алгоритм восстановления, одинаковый во всех соответствующих спецификации браузерах
    • Она требует остановить разбор на первой ошибке
  3. Какой DOM даёт фрагмент <p><div>X</div></p>?
    • <p></p><div>X</div><p></p>: div закрывает первый p, а осиротевший </p> создаёт второй, пустой
    • <p><div>X</div></p>, сохранённый как есть
    • <div><p>X</p></div>, парсер меняет вложенность местами