Парсинг кастомных блоков

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


Понимание структуры AST

Remark превращает Markdown в абстрактное синтаксическое дерево (AST, Abstract Syntax Tree). Узлы дерева имеют типы root, paragraph, heading, list, code и другие. Для обработки кастомных блоков важно понимать:

  • Тип узла (type) — определяет роль элемента (например, code для блоков кода).
  • Дочерние узлы (children) — содержат вложенные элементы.
  • Свойства (data, value) — могут хранить дополнительную информацию, например, язык блока кода или метаданные.

Для Rehype структура AST аналогична, но предназначена для HTML: узлы имеют tagName, properties и children.


Определение кастомного синтаксиса

Кастомные блоки часто оформляются специальными маркерами. Например:

:::tip
Это полезный совет
:::

Чтобы Remark распознавал такой блок, нужно создать плагин:

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

export function remarkCustomBlocks() {
  return (tree) => {
    visit(tree, 'paragraph', (node, index, parent) => {
      const match = /^:::(\w+)\s*$/.exec(node.children[0].value);
      if (match) {
        const blockType = match[1];
        const contentNodes = [];
        let i = index + 1;

        while (i < parent.children.length) {
          const nextNode = parent.children[i];
          if (/^:::$/.test(nextNode.children?.[0]?.value)) break;
          contentNodes.push(nextNode);
          i++;
        }

        const customBlockNode = {
          type: 'customBlock',
          data: { blockType },
          children: contentNodes,
        };

        parent.children.splice(index, i - index + 1, customBlockNode);
      }
    });
  };
}

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

  • visit(tree, 'paragraph', ...) — обход всех параграфов.
  • Использование регулярного выражения для распознавания начала блока.
  • Создание нового узла customBlock с типом блока и содержимым.
  • Замена исходных узлов дерева на новый узел.

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

После парсинга кастомного блока Markdown можно трансформировать его в HTML через Rehype. Для этого Remark-узлы нужно конвертировать в HTML-узлы:

import { unified } from 'unified';
import remarkParse from 'remark-parse';
import remarkRehype from 'remark-rehype';
import rehypeStringify from 'rehype-stringify';
import { remarkCustomBlocks } from './remarkCustomBlocks.js';

unified()
  .use(remarkParse)
  .use(remarkCustomBlocks)
  .use(remarkRehype, { allowDangerousHtml: true })
  .use(rehypeStringify)
  .process(':::tip\nЭто полезный совет\n:::')
  .then((file) => {
    console.log(String(file));
  });

В результате customBlock можно преобразовать в HTML с нужным тегом:

export function rehypeCustomBlocks() {
  return (tree) => {
    visit(tree, 'element', (node) => {
      if (node.tagName === 'customBlock') {
        node.tagName = 'div';
        node.properties = { className: [node.data.blockType] };
      }
    });
  };
}

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

  1. Вложенные блоки: Можно поддерживать блоки внутри блоков, рекурсивно обрабатывая дочерние узлы.
  2. Свойства блоков: Через data можно хранить метаданные (уровень важности, автор заметки, дата).
  3. Пользовательские рендеры: Rehype позволяет напрямую управлять генерацией HTML, включая добавление классов, стилей и атрибутов.

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

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

Примеры кастомных блоков

Тип блока Markdown HTML после Rehype
tip :::tip\nСовет\n::: <div class="tip">Совет</div>
warning :::warning\nПредупреждение\n::: <div class="warning">Предупреждение</div>
note :::note\nЗаметка\n::: <div class="note">Заметка</div>

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