Рекурсивная обработка вложенных структур

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

Основные принципы работы с AST

Обе библиотеки используют представление документа в виде абстрактного синтаксического дерева (AST, Abstract Syntax Tree). Каждый узел дерева содержит определённый тип и набор свойств:

  • type — тип узла (root, paragraph, heading, text, link и т.д.)
  • children — массив дочерних узлов для вложенных структур
  • Дополнительные свойства, специфичные для типа узла (value для текста, url для ссылок)

Для обработки таких деревьев применяются функции-плагины, которые получают узел и могут его модифицировать или заменять.

Рекурсивная функция обхода дерева

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

function traverse(node, visitor) {
  if (!node) return;

  visitor(node);

  if (node.children && node.children.length > 0) {
    node.children.forEach(child => traverse(child, visitor));
  }
}

Пояснение:

  • node — текущий узел AST.
  • visitor — функция, вызываемая для каждого узла. Она может проверять тип узла и выполнять трансформации.
  • Рекурсия применяется для всех дочерних элементов, что обеспечивает полный обход вложенных структур любой глубины.

Пример обработки Markdown с Remark

Для превращения Markdown-документа в AST и его рекурсивного обхода используется следующая схема:

import { unified } from 'unified';
import remarkParse from 'remark-parse';
import remarkStringify from 'remark-stringify';

const markdown = `
# Заголовок 1

Параграф с **жирным текстом** и [ссылкой](https://example.com).

- Элемент списка 1
  - Вложенный элемент 1
`;

const processor = unified()
  .use(remarkParse)
  .use(remarkStringify);

const tree = processor.parse(markdown);

traverse(tree, node => {
  if (node.type === 'text') {
    node.value = node.value.toUpperCase();
  }
});

const newMarkdown = processor.stringify(tree);
console.log(newMarkdown);

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

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

Применение с Rehype для HTML

Rehype использует аналогичный подход, но работает с HTML-документами:

import { unified } from 'unified';
import rehypeParse from 'rehype-parse';
import rehypeStringify from 'rehype-stringify';

const html = `
<div>
  <p>Текст <strong>выделенный</strong></p>
  <ul>
    <li>Элемент 1</li>
    <li>Элемент 2 <ul><li>Вложенный элемент</li></ul></li>
  </ul>
</div>
`;

const processor = unified()
  .use(rehypeParse, { fragment: true })
  .use(rehypeStringify);

const tree = processor.parse(html);

traverse(tree, node => {
  if (node.tagName === 'li') {
    node.children.push({ type: 'text', value: ' (обработано)' });
  }
});

const newHtml = processor.stringify(tree);
console.log(newHtml);

Ключевые моменты:

  • Узлы HTML имеют свойство tagName вместо type в Markdown.
  • Вложенные списки и блоки обрабатываются автоматически благодаря рекурсивной функции.

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

Часто требуется обработка только определённых уровней или типов узлов. Для этого можно использовать дополнительные условия:

function traverseFiltered(node, visitor, condition) {
  if (!node) return;

  if (condition(node)) {
    visitor(node);
  }

  if (node.children && node.children.length > 0) {
    node.children.forEach(child => traverseFiltered(child, visitor, condition));
  }
}

// Пример: обработка только параграфов
traverseFiltered(tree, node => {
  node.children.push({ type: 'text', value: ' [добавлено]' });
}, node => node.type === 'paragraph');

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

Советы по оптимизации рекурсивной обработки

  1. Использовать tail recursion при глубокой вложенности, если среда поддерживает оптимизацию.
  2. Кэширование результатов обработки для повторно встречающихся узлов.
  3. Фильтрация узлов по типу перед рекурсивным вызовом, чтобы уменьшить количество обходов.
  4. Разделение обхода и трансформации на две функции, чтобы можно было переиспользовать обход с разными действиями.

Заключение на практике

Рекурсивная обработка вложенных структур позволяет:

  • Модифицировать текст, ссылки, заголовки и списки на любом уровне.
  • Применять сложные трансформации без нарушения структуры документа.
  • Создавать собственные плагины для Remark и Rehype, обрабатывающие документы произвольной глубины вложенности.

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