Обход HAST дерева

HAST (Hypertext Abstract Syntax Tree) — это абстрактное синтаксическое дерево, которое представляет структуру HTML-документа. Оно используется в экосистеме Rehype для анализа, трансформации и генерации HTML. Каждая нода HAST содержит тип, свойства и дочерние элементы. Стандартная структура ноды выглядит следующим образом:

{
  type: 'element' | 'text' | 'comment' | ...,
  tagName: 'div' | 'p' | 'span', // для type === 'element'
  properties: { className: ['example'], id: 'unique' },
  children: [ /* массив дочерних нод */ ]
}

Обход дерева нужен для анализа или модификации его структуры. Существует два основных подхода: рекурсивный обход и использование специализированных утилит из экосистемы Unified.


Рекурсивный обход

Рекурсивный метод наиболее прямолинеен. Он позволяет пройтись по каждой ноде и при необходимости изменить её свойства или дочерние элементы.

function walk(node, callback) {
  callback(node);
  
  if (node.children && node.children.length > 0) {
    node.children.forEach(child => walk(child, callback));
  }
}

Пример использования:

walk(tree, node => {
  if (node.type === 'element' && node.tagName === 'p') {
    node.properties.className = [...(node.properties.className || []), 'highlight'];
  }
});

Здесь каждое <p> получает дополнительный класс highlight. Рекурсивный обход полезен, когда структура дерева известна и не слишком сложна.


Использование библиотеки unist-util-visit

Для крупных проектов рекурсивный обход вручную может быть неудобен. Утилита unist-util-visit позволяет выполнять обход дерева в стиле Visitor Pattern.

import { visit } from 'unist-util-visit';

visit(tree, 'element', node => {
  if (node.tagName === 'h1') {
    node.properties.id = node.children[0].value.toLowerCase().replace(/\s+/g, '-');
  }
});

Особенности visit:

  • Первый аргумент — дерево (HAST или MDAST).
  • Второй аргумент — фильтр по типу ноды ('element', 'text', 'comment').
  • Третий аргумент — callback, вызываемый для каждой подходящей ноды.

Можно использовать также фильтр по тегам:

visit(tree, node => node.type === 'element' && node.tagName === 'a', node => {
  node.properties.target = '_blank';
});

Редактирование дерева на лету

HAST дерево допускает модификацию во время обхода. Это позволяет добавлять, удалять или заменять ноды:

visit(tree, 'element', (node, index, parent) => {
  if (node.tagName === 'script') {
    parent.children.splice(index, 1); // удаление скрипта
  }
});

Параметры callback:

  • node — текущая нода.
  • index — индекс ноды в массиве parent.children.
  • parent — родительская нода.

Это важно для безопасного удаления или вставки элементов в дереве.


Обход и фильтрация

Для анализа больших деревьев часто используют комбинированный подход: сначала обход, затем фильтрация интересующих элементов:

const links = [];

visit(tree, 'element', node => {
  if (node.tagName === 'a' && node.properties.href) {
    links.push(node.properties.href);
  }
});

В результате получается массив всех ссылок в HTML, что удобно для генерации карты сайта или проверки валидности ссылок.


Применение в плагинах Rehype

Плагины Rehype используют обход HAST дерева для расширения возможностей:

  • Добавление классов к определённым тегам.
  • Замена устаревших тегов на современные аналоги.
  • Вставка атрибутов безопасности (rel="noopener" для ссылок).
  • Генерация таблиц содержимого на основе заголовков.

Пример простого плагина:

export default function rehypeAddTargetBlank() {
  return (tree) => {
    visit(tree, 'element', node => {
      if (node.tagName === 'a') {
        node.properties.target = '_blank';
      }
    });
  };
}

Рекурсивный обход vs unist-util-visit

Параметр Рекурсивный обход unist-util-visit
Простота Высокая для простых деревьев Высокая для любых деревьев
Управление индексами Нужно вручную Передаётся автоматически
Удаление/вставка Сложнее Легко через параметры callback
Масштабируемость Ограничена большими деревьями Отличная

Советы по обходу HAST дерева

  1. Избегать модификаций во время итерации, если используется обычный forEach — лучше использовать visit с parent и index.
  2. Проверять тип ноды перед доступом к children или properties.
  3. Минимизировать глубокие рекурсии в очень больших деревьях — это может привести к переполнению стека.
  4. Использовать утилиты из unist-util-* для упрощения кода и повышения читаемости.

Обход HAST дерева — фундаментальная операция при работе с HTML в экосистеме Unified. Глубокое понимание структуры нод и эффективные методы обхода позволяют создавать безопасные, масштабируемые и функциональные плагины для Rehype.