Remark и Rehype — это библиотеки для работы с деревьями синтаксического анализа Markdown и HTML соответственно. Основная концепция обеих библиотек основана на представлении документа в виде дерева узлов (AST — Abstract Syntax Tree). Каждый узел дерева описывает структуру и содержимое документа, что позволяет проводить сложные трансформации и манипуляции с элементами.
В 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 принимает три параметра:
tree).'text', 'paragraph',
'element' и др.).Для более гибких операций можно использовать
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: 'Обновленный заголовок' }]
};
});
В 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: ' - дополнение' });
}
});
Часто требуется объединить несколько операций: фильтрацию, модификацию и добавление узлов. Эффективная стратегия:
Пример добавления префикса к каждому заголовку и удаления всех пустых параграфов:
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.