unist-util-visit: обход узлов

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


Основные концепции

unist-util-visit позволяет рекурсивно проходить через все узлы AST и выполнять заданные действия над узлами определённого типа. Основные моменты:

  • Рекурсивный обход: обход всех дочерних узлов узла.
  • Фильтрация по типу: выборка узлов по их type.
  • Контекст обхода: доступ к родительским узлам и индексу текущего узла.
  • Прерывание обхода: возможность остановить обход на определённых условиях.

Структура функции:

visit(tree, [type], visitor)
  • tree — корневой узел AST.
  • type — строка или массив строк, соответствующих типам узлов, которые нужно обработать.
  • visitor — функция, вызываемая для каждого найденного узла.

Простейший пример обхода

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

const tree = {
  type: 'root',
  children: [
    { type: 'paragraph', children: [{ type: 'text', value: 'Пример текста' }] },
    { type: 'heading', depth: 1, children: [{ type: 'text', value: 'Заголовок' }] }
  ]
};

visit(tree, 'text', node => {
  console.log(node.value); // 'Пример текста' и 'Заголовок'
});

Здесь:

  • tree — корень AST.
  • 'text' — тип узлов, которые нужно обойти.
  • node — текущий узел текста, доступен для модификации или анализа.

Обход с фильтрацией по нескольким типам

type может быть массивом строк. Это удобно, если нужно обработать несколько типов узлов одновременно:

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

Доступ к родителю и индексу узла

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

visit(tree, 'paragraph', (node, index, parent) => {
  console.log(parent.type); // 'root'
  console.log(index);       // индекс текущего параграфа в parent.children
});
  • node — текущий узел.
  • index — индекс узла в массиве parent.children.
  • parent — родительский узел.

Это особенно важно, если требуется модифицировать дерево, удалять или заменять узлы.


Прерывание обхода

Для остановки обхода можно вернуть специальное значение visit.EXIT:

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

visit(tree, 'text', node => {
  if (node.value.includes('стоп')) return EXIT;
});

После этого обход всех дочерних узлов прекращается.


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

unist-util-visit позволяет напрямую изменять узлы. Например, преобразуем все текстовые узлы в верхний регистр:

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

Результатом будет модифицированное дерево:

{
  "type": "root",
  "children": [
    { "type": "paragraph", "children": [{ "type": "text", "value": "ПРИМЕР ТЕКСТА" }] },
    { "type": "heading", "depth": 1, "children": [{ "type": "text", "value": "ЗАГОЛОВОК" }] }
  ]
}

Обход и удаление узлов

Чтобы удалить узлы из дерева, можно использовать parent.children.splice(index, 1):

visit(tree, 'paragraph', (node, index, parent) => {
  if (node.children.length === 0) {
    parent.children.splice(index, 1);
  }
});

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


Рекурсивная фильтрация с условием

Иногда необходимо обрабатывать узлы с более сложными условиями. Можно использовать функцию-фильтр вместо строки или массива типов:

visit(tree, node => node.type === 'text' && node.value.includes('важно'), node => {
  node.value = node.value.replace('важно', 'ВАЖНО');
});

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


Интеграция с Remark и Rehype

В Remark unist-util-visit часто используют для:

  • Изменения текста в Markdown (remark-parse).
  • Добавления атрибутов к заголовкам (remark-slug).
  • Создания кастомных плагинов для обработки AST.

В Rehype аналогично обходят HTML AST для:

  • Изменения тегов.
  • Вставки атрибутов.
  • Манипуляции с содержимым страниц перед рендером.

Пример плагина Remark, заменяющего все ссылки на заголовки:

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

function remarkLinkToHeading() {
  return (tree) => {
    visit(tree, 'link', (node) => {
      if (node.url.startsWith('#')) {
        node.url = node.url.toLowerCase();
      }
    });
  };
}

Основные рекомендации

  • Изменять узлы безопасно: избегать изменения массива children во время обхода без корректного управления индексами.
  • Фильтровать по типу: использовать строку, массив или функцию-фильтр для точной обработки узлов.
  • Использовать parent и index: для сложных операций удаления или вставки узлов.
  • Прерывание обхода: возвращать visit.EXIT при необходимости остановить обработку дерева.

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