WP_HTML_Processor{}WP 6.4.0└─ 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 «Хлебные крошки» — это список всех тегов от корня документа до места где находится курсор процессора.

Крошки отражают вложенность, поэтому их можно представить в виде 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'] — точный путь, исключит частичные совпадения.

Примеры:

  1. Картинка прямо в figure:

    // <figure><img src="pic.jpg"></figure>
    
    $processor->next_tag([ 'breadcrumbs' => ['FIGURE','IMG'] ]);
    // курсор окажется на <img>
  2. Поиск <em> внутри <figcaption>:

    // <figure><figcaption>A <em>nice</em> day</figcaption></figure>
    
    $processor->next_tag([ 'breadcrumbs' => ['FIGURE','FIGCAPTION','EM'] ]);
    // курсор окажется на <em>
  3. Поиск картинок, которые находятся прямо в <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>

Примеры

0

#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>
*/
0

#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 );
0

#3 Проверяем, что тег соответствует цепочке «DIV > P > IMG»

$pr = WP_HTML_Processor::create_fragment( $html );

if( $pr && $pr->next_tag( [ 'breadcrumbs' => [ 'div', 'p', 'img' ] ] ) ){
	// нашли картинку внутри абзаца в DIV
}
0

#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();
0

#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 свой парсер

  1. Безопасное серверное редактирование.
    Блоки Gutenberg и плагины всё чаще требуют вставить <picture>, обернуть узел и т.д.; регэкспы ломаются на кривом HTML.

  2. Ноль сторонних зависимостей.
    Чистый PHP, работает даже на минимальном хостинге без libxml, tidy и прочего.

  3. Принцип «не навреди».
    Поддерживается только безопасное подмножество HTML5; при спорных конструкциях парсер останавливается, вместо того чтобы испортить документ.

  4. Единая 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 и ряд сторонних пакетов — ради одной функции пришлось бы подключить пол-фреймворка.

Методы (часто-используемые)

Создание процессора:

Навигация по дереву:

  • next_tag() — перейти к следующему тегу (с фильтрами).
  • set_bookmark() — поставить закладку.
  • seek() — вернуться к закладке.
  • get_breadcrumbs() — получить цепочку родителей текущего узла.

Чтение информации о текущем узле:

  • get_tag() — имя тега под курсором.
  • is_tag_closer() — открывающий или закрывающий тег.
  • get_attribute() — значение конкретного атрибута.
  • has_class() — проверить наличие класса.
  • class_list() — получить список классов.

Изменение узла:

Диагностика:

  • get_last_error() — узнать, почему парсер остановился или вернул null.

Методы (все)

  1. public static create_fragment( $html, $context = '', $encoding = 'UTF-8' )
  2. public static create_full_parser( $html, $known_definite_encoding = 'UTF-8' )
  3. public __construct( $html, $use_the_static_create_methods_instead = null )
  4. public get_last_error()
  5. public get_unsupported_exception()
  6. public next_tag( $query = null )
  7. public next_token()
  8. public is_tag_closer()
  9. public matches_breadcrumbs( $breadcrumbs )
  10. public expects_closer( ?WP_HTML_Token $node = null )
  11. public step( $node_to_process = self::PROCESS_NEXT_NODE )
  12. public get_breadcrumbs()
  13. public get_current_depth()
  14. public static normalize( string $html )
  15. public serialize()
  16. public get_namespace()
  17. public get_tag()
  18. public has_self_closing_flag()
  19. public get_token_name()
  20. public get_token_type()
  21. public get_attribute( $name )
  22. public set_attribute( $name, $value )
  23. public remove_attribute( $name )
  24. public get_attribute_names_with_prefix( $prefix )
  25. public add_class( $class_name )
  26. public remove_class( $class_name )
  27. public has_class( $wanted_class )
  28. public class_list()
  29. public get_modifiable_text()
  30. public get_comment_type()
  31. public release_bookmark( $bookmark_name )
  32. public seek( $bookmark_name )
  33. public set_bookmark( $bookmark_name )
  34. public has_bookmark( $bookmark_name )
  35. public static is_special( $tag_name )
  36. public static is_void( $tag_name )
  37. private create_fragment_at_current_node( string $html )
  38. private bail( string $message )
  39. private next_visitable_token()
  40. private is_virtual()
  41. protected serialize_token()
  42. private step_initial()
  43. private step_before_html()
  44. private step_before_head()
  45. private step_in_head()
  46. private step_in_head_noscript()
  47. private step_after_head()
  48. private step_in_body()
  49. private step_in_table()
  50. private step_in_table_text()
  51. private step_in_caption()
  52. private step_in_column_group()
  53. private step_in_table_body()
  54. private step_in_row()
  55. private step_in_cell()
  56. private step_in_select()
  57. private step_in_select_in_table()
  58. private step_in_template()
  59. private step_after_body()
  60. private step_in_frameset()
  61. private step_after_frameset()
  62. private step_after_after_body()
  63. private step_after_after_frameset()
  64. private step_in_foreign_content()
  65. private bookmark_token()
  66. private close_a_p_element()
  67. private generate_implied_end_tags( ?string $except_for_this_element = null )
  68. private generate_implied_end_tags_thoroughly()
  69. private get_adjusted_current_node()
  70. private reconstruct_active_formatting_elements()
  71. private reset_insertion_mode_appropriately()
  72. private run_adoption_agency_algorithm()
  73. private close_cell()
  74. private insert_html_element( WP_HTML_Token $token )
  75. private insert_foreign_element( WP_HTML_Token $token, bool $only_add_to_element_stack )
  76. private insert_virtual_node( $token_name, $bookmark_name = null )
  77. private is_mathml_integration_point()
  78. private is_html_integration_point()
  79. protected static get_encoding( string $label )

Заметки

Список изменений

С версии 6.4.0 Введена.

Код WP_HTML_Processor{} WP 6.8.2

Код слишком большой. Смотрите его здесь: wp-includes/html-api/class-wp-html-processor.php