Манипуляции с элементами

Remark и Rehype — это библиотеки для работы с деревьями синтаксического анализа Markdown и HTML соответственно. Основная концепция обеих библиотек основана на представлении документа в виде дерева узлов (AST — Abstract Syntax Tree). Каждый узел дерева описывает структуру и содержимое документа, что позволяет проводить сложные трансформации и манипуляции с элементами.

Структура AST

В Remark узлы делятся на типы:

  • root — корневой узел документа.
  • paragraph — параграф текста.
  • heading — заголовок с уровнем (depth: 1-6).
  • text — текстовый узел.
  • link, image — гиперссылки и изображения.
  • list, listItem — списки и элементы списков.

В Rehype структура схожа, но ориентирована на HTML:

  • root — корень документа.
  • element — HTML-тег с атрибутами и дочерними элементами.
  • text — текстовый узел.
  • comment — комментарий HTML.

Каждый узел содержит обязательное поле type, а также специфические поля (children, value, tagName, properties).

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

Для работы с элементами AST удобно использовать unist-util-visit:

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

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

В этом примере обходятся все текстовые узлы и преобразуется их содержимое. visit принимает три параметра:

  1. Дерево узлов (tree).
  2. Тип узлов для обхода ('text', 'paragraph', 'element' и др.).
  3. Функцию обратного вызова для модификации узла.

Для более гибких операций можно использовать unist-util-map, который создает новый AST на основе старого, сохраняя неизменные узлы.

Добавление, удаление и замена узлов

Добавление

Чтобы добавить новый узел в дерево:

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

visit(tree, 'root', (node) => {
  node.children.push({
    type: 'paragraph',
    children: [{ type: 'text', value: 'Новый параграф' }]
  });
});

В Rehype добавление элемента аналогично, только используется tagName и properties:

node.children.push({
  type: 'element',
  tagName: 'p',
  properties: {},
  children: [{ type: 'text', value: 'Новый параграф' }]
});
Удаление

Удаление узла требует фильтрации children родительского элемента:

node.children = node.children.filter(child => child.type !== 'paragraph');

Или с помощью unist-util-visit и функции обратного вызова с index:

visit(tree, 'paragraph', (node, index, parent) => {
  parent.children.splice(index, 1);
});
Замена

Замена узла выполняется через прямое присвоение:

visit(tree, 'heading', (node, index, parent) => {
  parent.children[index] = {
    type: 'heading',
    depth: node.depth,
    children: [{ type: 'text', value: 'Обновленный заголовок' }]
  };
});

Модификация атрибутов HTML-элементов

В Rehype атрибуты элементов хранятся в поле properties. Изменение атрибута class:

visit(tree, 'element', (node) => {
  if (node.tagName === 'div') {
    node.properties.className = ['highlight'];
  }
});

Добавление нового атрибута:

node.properties.id = 'main';

Удаление атрибута:

delete node.properties.style;

Работа с вложенными элементами

AST часто имеет глубокую вложенность, и для корректной модификации необходимо рекурсивно обходить детей. unist-util-visit делает это автоматически, обходя дерево в глубину. Для специализированной работы с детьми можно использовать node.children.forEach():

node.children.forEach(child => {
  if (child.type === 'element' && child.tagName === 'span') {
    child.children.push({ type: 'text', value: ' - дополнение' });
  }
});

Составные трансформации

Часто требуется объединить несколько операций: фильтрацию, модификацию и добавление узлов. Эффективная стратегия:

  1. Обход дерева и сбор узлов.
  2. Модификация существующих узлов.
  3. Добавление новых узлов на нужные позиции.
  4. Фильтрация или удаление нежелательных узлов.

Пример добавления префикса к каждому заголовку и удаления всех пустых параграфов:

visit(tree, 'heading', (node) => {
  node.children.unshift({ type: 'text', value: '? ' });
});

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

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

  • Для крупных документов лучше использовать итеративные методы обхода, чтобы не создавать глубокую рекурсию.
  • Для сложных трансформаций удобно создавать модульные функции, каждая из которых отвечает за отдельный тип изменений.
  • Сохранять исходное дерево можно с помощью unist-util-visit-parents, чтобы иметь доступ к родительским узлам при трансформациях.
  • Всегда проверять наличие поля children, чтобы избежать ошибок при обработке текстовых узлов.

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