WP_HTML_Processor{}└─ WP_HTML_Tag_Processor
Класс, используемый для разбора и изменения HTML-документа.
Разбирает HTML-код и позволяет изменить его, не ломая разметку. Использует собственный HTML5-парсер и работает только с «безопасным» набором тегов и правил. Если натыкается на непонятный элемент или сложный случай, просто останавливается, чтобы не повредить исходный код.
Класс может:
- Читать и менять атрибуты и классы.
- Читать и менять содержимое внутри тега.
- Навигация по breadcrumbs.
- Ставить закладки и возвращаться к ним.
- Перемещаться по дереву вверх и в стороны (достигается нестандартнто).
На момент версии WordPress 6.8 класс всё ещё ограничен тем, что умеет то же что и WP_HTML_Tag_Processor + навигация по breadcrumbs.
Важные заметки:
-
Часто лучше использовать WP_HTML_Tag_Processor — он гораздо быстрее. В чем разница, смотрите пример ниже.
-
Можно создать максимум 100 закладок (bookmarks), если создать больше парсер выдаст ошибку.
-
Работает только с входной строкой в кодировке UTF-8.
- Прекращает парсинг, если не получилось распарсить HTML: при появлении
<table>
, SVG/MathML или сложный случай, где браузеры должны «переставлять» узлы (fostering/adoption). Это сделано, чтобы класс никогда не «портил» документ.
Отличие от WP_HTML_Tag_Processor:
-
WP_HTML_Processor — Знает вложенность DOM. Умеет менять документ — добавлять/удалять классы, атрибуты. Останавливается, если встречает SVG, сложные таблицы или другие неподдерживаемые элементы, чтобы не повредить разметку. Тяжеловеснее (~ в 10 раз медленне).
- WP_HTML_Tag_Processor — работает как потоковый сканер: видит только текущий тег и его атрибуты, не хранит дерево и не знает вложенность. Идеален для быстрой проверки, фильтрации и чтения атрибутов «на лету» — минимальные затраты памяти и CPU - (~ в 10 раз быстрее). Непонятные участки просто пропускает; изменить HTML (удалить узел, добавить класс) с его помощью нельзя.
Читайте также:
Использование
Конструктор закрыт и при вызове отдас сообщение doing_it_wrong. Объект создается статическим методом WP_HTML_Processor::create_fragment().
$processor = WP_HTML_Processor::create_fragment( $html, $context, $encoding ); if( $processor && $processor->next_tag() ){ // делаем что нужно } $html = $processor->get_updated_html();
- $html(строка) (обязательный)
- HTML-фрагмент (или весь документ), с которым будем работать. Это должен быть валидный HTML5.
- $context(строка)
- Имя корневого тега, внутри которого мысленно «разворачивается» фрагмент. Нужен для проверки допустимости вложенности. Должен быть валидным элементом верхнего уровня; обычно
<body>
.
По умолчанию: '' - $encoding(строка)
- Кодировка входной строки. Поддерживается только UTF-8, поэтому при другой кодировке текст следует перекодировать заранее.
По умолчанию: 'UTF-8'
Также объект можно создать статическим методом WP_HTML_Processor::create_full_parser(). В чем разница:
-
WP_HTML_Processor::create_fragment() — берёт лишь нужный фрагмент в контексте
<body>
. Игнорирует <!DOCTYPE>, <html>, <head>. Быстрее запускается и тратит меньше памяти. - WP_HTML_Processor::create_full_parser() — разбирает всю страницу целиком, от <!DOCTYPE> до </html>. Доступен и <head>, и корневой <html>. Хуже по ресурсам, но нужен, когда правите метатеги, скрипты или делаете «санити» чужого HTML перед кэшем.
Breadcrumbs «Хлебные крошки»
Breadcrumbs «Хлебные крошки» — это список всех тегов от корня документа до места где находится курсор процессора.
Крошки отражают вложенность, поэтому их можно представить в виде CSS-селектора с >
(прямая вложенность): HTML > BODY > DIV > FIGURE > IMG
.
Зачем нужны:
- Позволяют узнать «где мы находимся» в дереве.
- Позволяют задать точный путь при поиске: процессор найдёт тег, только если вся цепочка родителей совпадёт.
- Дают гарантии, что код не «выскочит» за пределы нужного контейнера.
Чтобы получить текущую чепочку тегов, где сейчас находится курсор, используйте:
$path = $processor->get_breadcrumbs(); // вернёт массив ['HTML','BODY', …]
Неявные теги
Если вы создаёте процессор через create_fragment()
, фрагмент автоматически оборачивается в виртуальные HTML и BODY - <html><body>…</body></html>
. Поэтому любые теги будут иметь в начале пути HTML
и BODY
.
В тоже время это не мешает коротким запросам: можно указать только нужную «хвостовую» часть цепочки.
Короткие и полные цепочки
['IMG']
— совпадёт с любым <img> в документе.['P','IMG']
— найдёт <img> которые находится прямо в <p>.['HTML','BODY','MAIN','UL','LI','A']
— точный путь, исключит частичные совпадения.
Примеры:
-
Картинка прямо в figure:
// <figure><img src="pic.jpg"></figure> $processor->next_tag([ 'breadcrumbs' => ['FIGURE','IMG'] ]); // курсор окажется на <img>
-
Поиск <em> внутри <figcaption>:
// <figure><figcaption>A <em>nice</em> day</figcaption></figure> $processor->next_tag([ 'breadcrumbs' => ['FIGURE','FIGCAPTION','EM'] ]); // курсор окажется на <em>
-
Поиск картинок, которые находятся прямо в <body>:
// <div><img></div><img> $processor->next_tag([ 'breadcrumbs' => ['BODY','IMG'] ]); // совпадёт только со вторым <img>
Особенности парсера
Этот класс реализует лишь часть спецификации HTML5. Если в HTML встретятся неподдерживаемый элемент, HTML Processor полностью прекращает работу. Такая жесткая мера гарантирует, что Processor не сломает HTML, который он не понимает полностью.
Ограничения:
-
Не выводит сообщений об ошибках
Если встречается неподдерживаемая конструкция, инициализация возвращаетnull
- никаких ошибок, исключений или логов. Ошибку можно посмотреть через метод WP_HTML_Processor::get_last_error(). - Не «сливает» атрибуты из дубликатов <html> / <body>
В обычном браузерном парсере лишний <body> может «подарить» свои атрибуты первому. Здесь этого нет: дополнительные теги просто игнорируются, их атрибуты теряются.
Обработке НЕ подлежат:
- Любые теги внутри <table>.
- Элементы «иностранного» контента — SVG, MathML и т. п.
- Узлы, которые парсятся вне режима in body (<!DOCTYPE>, <meta>, <link> и др.).
- Если для корректной структуры требуются перенос узла (fostering/adoption-правила). Например, <div> случайно оказлся внутри <table>, браузер бы его перенес, но процессор завершит работу.
Парсер всё-таки понимает:
- Опущенные обязательные теги:
<p>one<p>two
. - Лишние закрывающие теги:
<p>one </span> more</p>
. - «Самозакрытые» непустые элементы:
<div/>text</div>
. - Заголовки, закрывающие другой уровень:
<h1>Title </h2>
. - Текст, похожий на теги, внутри элементов:
<title>The <img> is plaintext</title>
. - Код в
<script> / `<style>
, содержащий псевдо-HTML:
<script>document.write('<p>Hi</p>');</script>
. -
Экранированные скрипты:
<script><!-- document.write('<script>console.log("hi")</script>') --></script>
Примеры
#1 Добавляем класс к изображению внутри figure
Задача — добавить класс к <img>
внутри <figure>
.
$html = ' <img src="pic2.jpg"> <figure><img src="pic.jpg"></figure> '; $pr = WP_HTML_Processor::create_fragment( $html ); if( $pr && $pr->next_tag( [ 'breadcrumbs' => [ 'FIGURE', 'IMG' ] ] ) ){ $pr->add_class( 'responsive' ); } echo $pr->get_updated_html(); /* <img src="pic2.jpg"> <figure><img class="responsive" src="pic.jpg"></figure> */
Эту задачу можно решить и через WP_HTML_Tag_Processor, однако в этом случае придётся вручную проверять что IMG
находится именно внутри FIGURE
.
$html = ' <img src="pic2.jpg"> <figure><img src="pic.jpg"></figure> '; $pr = new WP_HTML_Tag_Processor( $html ); $inside_figure = false; while( $pr->next_tag( [ 'tag_closers' => 'visit' ] ) ){ // Зашли в <figure> if ( ! $pr->is_tag_closer() && 'FIGURE' === $pr->get_tag() ) { $inside_figure = true; } // Вышли из </figure> if ( $pr->is_tag_closer() && 'FIGURE' === $pr->get_tag() ) { $inside_figure = false; } // Если мы всё ещё внутри FIGURE и это <img>, добавляем класс if ( $inside_figure && ! $pr->is_tag_closer() && 'IMG' === $pr->get_tag() ) { $pr->add_class( 'responsive' ); } } echo $pr->get_updated_html(); /* <img src="pic2.jpg"> <figure><img class="responsive" src="pic.jpg"></figure> */
Пример того, что будет если не проверять вложенность:
$html = ' <img src="pic2.jpg"> <figure><img src="pic.jpg"></figure> '; $pr = new WP_HTML_Tag_Processor( $html ); while( $pr->next_tag( [ 'tag_name' => 'IMG' ] ) ){ $pr->add_class( 'responsive' ); } echo $pr->get_updated_html(); /* <img class="responsive" src="pic2.jpg"> <figure><img class="responsive" src="pic.jpg"></figure> */
#2 Удаляем атрибут style у всех ссылок
$html = ' <a href="https://example.com" style="color:red;">Example</a> <p> <a href="#top" style="text-decoration:none;">Back to top</a> </p> '; $pr = WP_HTML_Processor::create_fragment( $html ); while( $pr && $pr->next_tag( [ 'tag_name' => 'A' ] ) ){ $pr->remove_attribute( 'style' ); } echo $pr->get_updated_html(); /* <a href="https://example.com" >Example</a> <p> <a href="#top" >Back to top</a> </p> */
Это лишь пример. На практике такую простую задачу лучше решать через WP_HTML_Tag_Processor, который раз в 10 быстрее. Для этого нужно просто заменить процессор:
$pr = WP_HTML_Processor::create_fragment( $html ); // на $pr = new WP_HTML_Tag_Processor( $html );
#3 Проверяем, что тег соответствует цепочке «DIV > P > IMG»
$pr = WP_HTML_Processor::create_fragment( $html ); if( $pr && $pr->next_tag( [ 'breadcrumbs' => [ 'div', 'p', 'img' ] ] ) ){ // нашли картинку внутри абзаца в DIV }
#4 Пример перемещения по дереву
Этот пример демонстрирует перемещение по дереву вверх, вниз и в стороны.
WP_HTML_Processor умеет «прыгать» по дереву — вниз, в бок и назад — но делает это не как классический DOM-walker, а через два приёма:
- Фильтр по breadcrumbs (
next_tag()
) — позволяет ходить вниз и вбок. - Закладки
set_bookmark() + seek()
— позволяют ходить вверх - «запомнить точку» и вернуться к ней.
Методы для перемещения по дереву:
- next_tag(…) — перебирает теги вперёд; можно ограничивать путь через breadcrumbs, тем самым «ходить» вглубь или оставаться на текущем уровне.
- set_bookmark( $name ) — ставит закладку в текущей точке.
- seek( $name ) — возвращает курсор к ранее поставленной закладке.
Пример — вниз, вбок и обратно вверх:
$html = ' <figure> <img src="pic.jpg"> <figcaption>Подпись</figcaption> </figure> '; $p = WP_HTML_Processor::create_fragment( $html ); // 1. Ставим закладку на FIGURE (точка возврата «вверх») $p->next_tag( 'figure' ); $p->set_bookmark( 'figure' ); // 2. Спускаемся ВНУТРЬ и ищем FIGCAPTION if ( $p->next_tag( [ 'breadcrumbs' => [ 'FIGURE', 'FIGCAPTION' ] ] ) ) { // «Боковой» переход: находим ближайший соседний тег <em> внутри подписи $p->next_tag( [ 'breadcrumbs' => [ 'FIGURE', 'FIGCAPTION', 'EM' ] ] ); $p->add_class( 'highlight' ); } // 3. Прыжок ВВЕРХ к исходному <figure> $p->seek( 'figure' ); $p->add_class( 'has-caption' ); echo $p->get_updated_html();
#5 Закрывающие теги (</…>) пропускаются по умолчанию
next_tag() по умолчанию останавливается только на открывающих тегах.
Чтобы курсор заходил и на </…>
, нужно указать опцию tag_closers = visit
(значение skip
или пустое означает «пропустить»).
$html = '<div><span>Текст</span></div>'; $p = WP_HTML_Processor::create_fragment( $html ); /* * Проходим документ, ОСТАНАВЛИВАЯСЬ и на закрывающих тегах. * Аргумент 'tag_closers' => 'visit' говорит об этом явно. */ while( $p->next_tag( [ 'tag_closers' => 'visit' ] ) ){ printf( "%s%s\n", $p->get_tag(), $p->is_tag_closer() ? ' (closer)' : '' ); } /* Результат: DIV SPAN SPAN (closer) DIV (closer) */
Зачем WordPress свой парсер
-
Безопасное серверное редактирование.
Блоки Gutenberg и плагины всё чаще требуют вставить <picture>, обернуть узел и т.д.; регэкспы ломаются на кривом HTML. -
Ноль сторонних зависимостей.
Чистый PHP, работает даже на минимальном хостинге без libxml, tidy и прочего. -
Принцип «не навреди».
Поддерживается только безопасное подмножество HTML5; при спорных конструкциях парсер останавливается, вместо того чтобы испортить документ. - Единая HTML-API.
После быстрого WP_HTML_Tag_Processor (6.2) нужен инструмент, который умеет навигировать по дереву и править контент — это и есть WP_HTML_Processor (6.4).
Альтернативы
Почему не подошли следующие альтернативы:
-
Регэкспы и строковые функции
Плюсы: нулевые зависимости.
Не подходит: ломаются на вложенных тегах, скриптах и комментариях; код трудно поддерживать. -
PHP DOMDocument (расширение libxml)
Плюсы: полный DOM, XPath, входит в стандартное расширение PHP.
Не подходит: требует расширение на сервере, ест много памяти, плохо работает с битым HTML и не знает правил HTML5. -
PHP tidy (расширение tidy)
Плюсы: автоматически исправляет разметку.
Не подходит: редко установлен на хостингах, лицензия LGPL. -
Masterminds HTML5-PHP
Плюсы: полноценный современный HTML5-парсер.
Не подходит: крупная библиотека (\~450 КБ), медленнее; внешняя зависимость усложняет безопасность и обновления Core. - Symfony DomCrawler / QueryPath / DiDom
Плюсы: удобный API с CSS-селекторами.
Не подходит: тянут за собой полный DOM и ряд сторонних пакетов — ради одной функции пришлось бы подключить пол-фреймворка.
Методы (часто-используемые)
Создание процессора:
- create_fragment() — разбирает HTML-фрагмент внутри <body>.
- create_full_parser() — парсит полный документ от <!DOCTYPE> до </html>.
Навигация по дереву:
- next_tag() — перейти к следующему тегу (с фильтрами).
- set_bookmark() — поставить закладку.
- seek() — вернуться к закладке.
- get_breadcrumbs() — получить цепочку родителей текущего узла.
Чтение информации о текущем узле:
- get_tag() — имя тега под курсором.
- is_tag_closer() — открывающий или закрывающий тег.
- get_attribute() — значение конкретного атрибута.
- has_class() — проверить наличие класса.
- class_list() — получить список классов.
Изменение узла:
- add_class() — добавить CSS-класс.
- remove_class() — убрать CSS-класс.
- set_attribute() — изменить атрибут.
- remove_attribute() — удалить атрибут.
Диагностика:
- get_last_error() — узнать, почему парсер остановился или вернул null.
Методы (все)
- public static create_fragment( $html, $context = '', $encoding = 'UTF-8' )
- public static create_full_parser( $html, $known_definite_encoding = 'UTF-8' )
- public __construct( $html, $use_the_static_create_methods_instead = null )
- public get_last_error()
- public get_unsupported_exception()
- public next_tag( $query = null )
- public next_token()
- public is_tag_closer()
- public matches_breadcrumbs( $breadcrumbs )
- public expects_closer( ?WP_HTML_Token $node = null )
- public step( $node_to_process = self::PROCESS_NEXT_NODE )
- public get_breadcrumbs()
- public get_current_depth()
- public static normalize( string $html )
- public serialize()
- public get_namespace()
- public get_tag()
- public has_self_closing_flag()
- public get_token_name()
- public get_token_type()
- public get_attribute( $name )
- public set_attribute( $name, $value )
- public remove_attribute( $name )
- public get_attribute_names_with_prefix( $prefix )
- public add_class( $class_name )
- public remove_class( $class_name )
- public has_class( $wanted_class )
- public class_list()
- public get_modifiable_text()
- public get_comment_type()
- public release_bookmark( $bookmark_name )
- public seek( $bookmark_name )
- public set_bookmark( $bookmark_name )
- public has_bookmark( $bookmark_name )
- public static is_special( $tag_name )
- public static is_void( $tag_name )
- private create_fragment_at_current_node( string $html )
- private bail( string $message )
- private next_visitable_token()
- private is_virtual()
- protected serialize_token()
- private step_initial()
- private step_before_html()
- private step_before_head()
- private step_in_head()
- private step_in_head_noscript()
- private step_after_head()
- private step_in_body()
- private step_in_table()
- private step_in_table_text()
- private step_in_caption()
- private step_in_column_group()
- private step_in_table_body()
- private step_in_row()
- private step_in_cell()
- private step_in_select()
- private step_in_select_in_table()
- private step_in_template()
- private step_after_body()
- private step_in_frameset()
- private step_after_frameset()
- private step_after_after_body()
- private step_after_after_frameset()
- private step_in_foreign_content()
- private bookmark_token()
- private close_a_p_element()
- private generate_implied_end_tags( ?string $except_for_this_element = null )
- private generate_implied_end_tags_thoroughly()
- private get_adjusted_current_node()
- private reconstruct_active_formatting_elements()
- private reset_insertion_mode_appropriately()
- private run_adoption_agency_algorithm()
- private close_cell()
- private insert_html_element( WP_HTML_Token $token )
- private insert_foreign_element( WP_HTML_Token $token, bool $only_add_to_element_stack )
- private insert_virtual_node( $token_name, $bookmark_name = null )
- private is_mathml_integration_point()
- private is_html_integration_point()
- protected static get_encoding( string $label )
Заметки
- Смотрите: WP_HTML_Tag_Processor
- Смотрите: https://html.spec.whatwg.org/
Список изменений
С версии 6.4.0 | Введена. |