Обход дерева с unist-util-visit

Библиотеки Remark и Rehype используют абстрактное синтаксическое дерево (AST) для представления Markdown или HTML-документов. Структура AST стандартизирована через спецификацию unist, где каждый узел имеет тип type, а некоторые узлы могут содержать детей в массиве children. Для анализа и модификации AST применяется пакет unist-util-visit, который обеспечивает удобный способ обхода всех узлов дерева.


Установка и подключение

Для работы с обходом дерева необходимо установить соответствующий пакет:

npm install unist-util-visit

Подключение в коде:

const visit = require('unist-util-visit');

Функция visit принимает дерево и функцию-обработчик, которая вызывается для каждого узла определённого типа.


Основной синтаксис

visit(tree, [test], visitor)

Аргументы:

  • tree — корень AST, обычно возвращаемый remark.parse() или rehype.parse().
  • test — опциональный фильтр по типу узла. Может быть строкой (например, 'text'), массивом типов или функцией (node) => boolean.
  • visitor — функция, вызываемая для каждого узла, подходящего под фильтр.

Функция visitor принимает три аргумента:

function visitor(node, index, parent) {
  // node — текущий узел
  // index — позиция узла в родителе
  // parent — родительский узел
}

Пример обхода всех текстовых узлов

const remark = require('remark');

const tree = remark.parse('Пример текста с **жирным** выделением.');

visit(tree, 'text', (node) => {
  console.log(node.value);
});

В результате будут выведены все текстовые фрагменты, включая содержимое элементов Markdown, таких как параграфы или жирный текст.


Фильтрация по нескольким типам узлов

visit поддерживает массив типов узлов для обхода:

visit(tree, ['text', 'emphasis'], (node) => {
  console.log(node.type, node.value || '');
});

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


Использование функции для сложной фильтрации

Можно передавать функцию для динамической фильтрации узлов:

visit(tree, node => node.type === 'link' && node.url.includes('example.com'), (node) => {
  console.log(`Ссылка на example.com: ${node.url}`);
});

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


Модификация узлов во время обхода

visitor позволяет изменять узлы на лету:

visit(tree, 'text', (node) => {
  node.value = node.value.toUpperCase();
});

После такой операции все текстовые узлы AST будут преобразованы в верхний регистр, и при последующем рендеринге Markdown или HTML отобразятся уже изменёнными.


Прерывание обхода и обход дочерних узлов

Функция visitor может возвращать специальные значения для управления обходом:

  • visit.SKIP — пропустить обход дочерних узлов текущего узла.
  • visit.EXIT — завершить обход всего дерева полностью.

Пример:

visit(tree, 'paragraph', (node) => {
  if (node.children.length === 0) {
    return visit.SKIP; // Пропускаем пустые параграфы
  }
});

Глубокий обход с сохранением контекста

Аргументы index и parent позволяют модифицировать дерево безопасно:

visit(tree, 'text', (node, index, parent) => {
  if (node.value.includes('важно')) {
    parent.children[index] = { type: 'strong', children: [node] };
  }
});

В этом примере текстовый узел, содержащий слово “важно”, оборачивается в узел strong. Такой подход применим для автоматического форматирования или генерации аннотаций.


Комбинирование с другими утилитами

unist-util-visit часто используется совместно с другими инструментами:

  • unist-util-select — для выборки узлов по CSS-подобным селекторам.
  • unist-util-visit-parents — для доступа к цепочке родителей, когда необходимо модифицировать узлы на уровне нескольких иерархий.

Пример с visit-parents:

const visitParents = require('unist-util-visit-parents');

visitParents(tree, 'text', (node, ancestors) => {
  const paragraph = ancestors.find(n => n.type === 'paragraph');
  if (paragraph) {
    console.log('Текст внутри параграфа:', node.value);
  }
});

Резюме по ключевым особенностям

  • visit обеспечивает рекурсивный обход AST с фильтрацией по типам узлов.
  • Поддерживает строковые типы, массивы или функции-фильтры.
  • Позволяет безопасно модифицировать дерево во время обхода.
  • Специальные возвращаемые значения SKIP и EXIT дают контроль над обходом.
  • В сочетании с другими утилитами (visit-parents, unist-util-select) открывает мощные возможности для анализа и трансформации Markdown и HTML.

unist-util-visit является фундаментальной утилитой для работы с AST в экосистеме Remark/Rehype, обеспечивая гибкий и эффективный механизм обхода и модификации узлов.