Remark и Rehype предоставляют мощные инструменты для анализа и трансформации Markdown и HTML-документов. Одним из ключевых аспектов работы с этими библиотеками является управление классами и стилями элементов через их абстрактные синтаксические деревья — MDAST для Markdown и HAST для HTML.
В HAST (HTML AST) элементы представлены объектами следующего вида:
{
type: 'element',
tagName: 'div',
properties: {
className: ['container', 'highlight'],
style: 'color: red; font-weight: bold;'
},
children: [...]
}
property: value;.В MDAST (Markdown AST) классы и стили напрямую не поддерживаются стандартом, но они могут быть добавлены через расширения, например, remark-directive, позволяющее добавлять атрибуты к элементам Markdown.
Пример использования directive для добавления класса:
:::note.important
Это заметка с классом important
:::
После обработки через remark-directive можно получить
HAST с соответствующим className.
Для добавления или изменения классов у элементов в HAST удобно использовать пакет unist-util-visit:
import { visit } from 'unist-util-visit';
visit(tree, 'element', node => {
if (node.tagName === 'p') {
node.properties = node.properties || {};
node.properties.className = node.properties.className || [];
node.properties.className.push('my-paragraph');
}
});
Ключевые моменты:
element) или тегом
(tagName).className отсутствует, его нужно
инициализировать как массив.push или объединять массивы через concat.Удаление класса выглядит так:
node.properties.className = node.properties.className.filter(c => c !== 'old-class');
Стили в HAST хранятся как одна строка CSS. Для их безопасного редактирования часто используют парсинг строки в объект:
function parseStyle(styleString) {
return styleString.split(';').reduce((acc, rule) => {
const [key, value] = rule.split(':').map(s => s.trim());
if (key && value) acc[key] = value;
return acc;
}, {});
}
function serializeStyle(styleObject) {
return Object.entries(styleObject).map(([k,v]) => `${k}: ${v}`).join('; ');
}
Пример добавления нового стиля:
node.properties.style = node.properties.style || '';
const styles = parseStyle(node.properties.style);
styles['background-color'] = 'yellow';
node.properties.style = serializeStyle(styles);
Таким образом сохраняется существующий стиль и добавляется новый.
Поскольку Remark изначально работает с Markdown, для добавления классов и стилей к HTML-элементам после преобразования используется связка remark → rehype → rehype-stringify. Пример полного конвейера:
import { unified } from 'unified';
import remarkParse from 'remark-parse';
import remarkDirective from 'remark-directive';
import remarkRehype from 'remark-rehype';
import rehypeStringify from 'rehype-stringify';
import { visit } from 'unist-util-visit';
const processor = unified()
.use(remarkParse)
.use(remarkDirective)
.use(remarkRehype)
.use(() => tree => {
visit(tree, 'element', node => {
if (node.tagName === 'p') {
node.properties = node.properties || {};
node.properties.className = ['custom-paragraph'];
}
});
})
.use(rehypeStringify);
const result = await processor.process('Это абзац.');
console.log(String(result));
В результате все параграфы получат класс
custom-paragraph.
class и style.properties, className
или style.unist-util-visit
и hast-util-to-string упрощают обход дерева и извлечение
содержимого.rehype-highlight.className можно связывать с локальными классами через
импорт CSS в проекте на Node.js или Vite/Webpack.Эти подходы позволяют гибко управлять визуальным оформлением контента, генерируемого из Markdown, сохраняя структуру и семантику HTML.