Работа с классами и стилями

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: [...]
}
  • properties.className — массив строк, каждая из которых соответствует отдельному классу элемента.
  • properties.style — строка CSS-стилей, записанных в стандартном формате property: value;.

В MDAST (Markdown AST) классы и стили напрямую не поддерживаются стандартом, но они могут быть добавлены через расширения, например, remark-directive, позволяющее добавлять атрибуты к элементам Markdown.

Пример использования directive для добавления класса:

:::note.important
Это заметка с классом important
:::

После обработки через remark-directive можно получить HAST с соответствующим className.


Работа с классами через Rehype

Для добавления или изменения классов у элементов в 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');
  }
});

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

  • visit позволяет обходить все узлы AST и выполнять операции на узлах с конкретным типом (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

Поскольку 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.


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

  • Соблюдать совместимость с HTML: при добавлении классов и стилей убедиться, что они корректно сериализуются в class и style.
  • Избегать мутаций без инициализации: всегда проверять существование properties, className или style.
  • Использовать утилиты: unist-util-visit и hast-util-to-string упрощают обход дерева и извлечение содержимого.
  • Конкатенация стилей и классов: при динамическом добавлении нескольких значений удобно использовать массивы для классов и объект для стилей.

Расширенные возможности

  • Условное добавление классов: позволяет динамически определять, какой класс применять в зависимости от содержимого узла.
  • Комбинирование с плагинами: многие плагины remark/re-hype предоставляют опции для автоматического присвоения стилей элементам, например, подсветка синтаксиса для блоков кода с помощью rehype-highlight.
  • Интеграция с CSS-модулями: свойства className можно связывать с локальными классами через импорт CSS в проекте на Node.js или Vite/Webpack.

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