Оптимистичные трансформации

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

Ключевой момент: Remark превращает Markdown в AST (abstract syntax tree), Rehype — HTML в AST. После трансформации AST можно модифицировать с помощью плагинов и затем генерировать обратно Markdown или HTML.


Работа с AST

В основе Remark и Rehype лежит единая структура unist, состоящая из узлов с полями:

  • type — тип узла (text, paragraph, heading, element и т. д.)
  • children — массив дочерних узлов
  • value — значение узла (только для листовых узлов, например, text)

Пример структуры узла для абзаца Markdown:

{
  "type": "paragraph",
  "children": [
    {
      "type": "text",
      "value": "Это пример текста в абзаце."
    }
  ]
}

Для HTML элемент в Rehype выглядит как:

{
  "type": "element",
  "tagName": "p",
  "properties": {},
  "children": [
    {
      "type": "text",
      "value": "Это пример параграфа HTML."
    }
  ]
}

Настройка Remark

Remark предоставляет удобный API для обработки Markdown. Основные методы:

  • remark().use(plugin) — подключение плагина
  • processSync(markdown) — синхронная обработка Markdown
  • process(markdown) — асинхронная обработка

Пример простой трансформации:

import { remark } from 'remark';
import remarkGfm from 'remark-gfm';

const markdown = '# Заголовок\n\nНекоторый текст';
const result = remark()
  .use(remarkGfm)
  .processSync(markdown)
  .toString();

console.log(result);

Ключевой момент: Плагины могут изменять AST, добавлять узлы, модифицировать текст или внедрять новые возможности, такие как таблицы или чекбоксы.


Настройка Rehype

Rehype работает аналогично, но с HTML:

  • rehype().use(plugin) — подключение плагина
  • parse(html) — парсинг HTML в AST
  • stringify(tree) — генерация HTML из AST

Пример:

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

const html = '<h1>Заголовок</h1><p>Текст абзаца</p>';

const result = rehype()
  .use(rehypeParse, { fragment: true })
  .use(rehypeStringify)
  .processSync(html)
  .toString();

console.log(result);

Оптимистичные трансформации

Оптимистичная трансформация означает изменение AST с минимальными проверками на существование узлов и типов, с ожиданием, что структура документа в целом корректна. Подобный подход ускоряет обработку и уменьшает количество кода для проверок.

Принцип работы

  1. Преобразование узлов по типу: для каждого узла определённого типа выполняется функция трансформации.
  2. Рекурсивная обработка: дочерние узлы обрабатываются тем же алгоритмом.
  3. Добавление новых узлов: новые элементы можно добавлять без строгой проверки контекста.

Пример: добавление CSS-класса ко всем параграфам в HTML через Rehype:

import { visit } from 'unist-util-visit';
import { rehype } from 'rehype';
import rehypeParse from 'rehype-parse';
import rehypeStringify from 'rehype-stringify';

const html = '<p>Первый абзац</p><p>Второй абзац</p>';

function addClassToParagraphs() {
  return (tree) => {
    visit(tree, 'element', (node) => {
      if (node.tagName === 'p') {
        node.properties = node.properties || {};
        node.properties.className = 'highlight';
      }
    });
  };
}

const result = rehype()
  .use(rehypeParse, { fragment: true })
  .use(addClassToParagraphs)
  .use(rehypeStringify)
  .processSync(html)
  .toString();

console.log(result);

Пояснение: Здесь нет проверок на наличие дочерних узлов children, потому что visit обходит дерево и корректно применяет функцию только к элементам p. Такой подход позволяет быстро вносить массовые изменения.


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

Для полного цикла обработки Markdown → HTML → модификации → HTML используют комбинацию:

  • remark для Markdown
  • remark-rehype для конвертации в HTML
  • rehype для дополнительных трансформаций
  • rehype-stringify для генерации HTML

Пример цепочки:

import { remark } from 'remark';
import remarkGfm from 'remark-gfm';
import remarkRehype from 'remark-rehype';
import rehypeStringify from 'rehype-stringify';

const markdown = '# Заголовок\n\nТекст абзаца';

const result = remark()
  .use(remarkGfm)
  .use(remarkRehype)
  .use(() => (tree) => {
    // Оптимистичная трансформация: все h1 становятся h2
    tree.children.forEach(node => {
      if (node.tagName === 'h1') node.tagName = 'h2';
    });
  })
  .use(rehypeStringify)
  .processSync(markdown)
  .toString();

console.log(result);

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


Утилиты для обхода AST

  • unist-util-visit — рекурсивно обходит все узлы указанного типа
  • unist-util-map — создает новое дерево с изменениями без мутаций
  • unist-util-filter — фильтрует узлы по условию

Пример использования visit для изменения текста всех заголовков:

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

function emphasizeHeadings() {
  return (tree) => {
    visit(tree, 'heading', node => {
      node.children.forEach(child => {
        if (child.type === 'text') child.value = child.value.toUpperCase();
      });
    });
  };
}

Практические советы

  • Минимизировать проверки типов: оптимистичные трансформации работают быстрее и проще.
  • Использовать visit для обхода: это стандартный подход для массовых изменений AST.
  • Комбинировать Remark и Rehype: Markdown → AST → HTML → модификации → финальный HTML.
  • Создавать мелкие плагины: удобнее тестировать и повторно использовать.

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