Парсинг Markdown в MDAST

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

Установка и базовая настройка

Для работы с Remark достаточно установить пакет через npm:

npm install remark

Импортировать библиотеку в проект:

import {remark} from 'remark';

Преобразование Markdown в MDAST

Основной метод для парсинга — remark().parse(). Он принимает текст Markdown и возвращает дерево MDAST:

import {remark} from 'remark';

const markdownText = `
# Заголовок 1

Это параграф с *курсивом* и **жирным текстом**.
`;

const tree = remark().parse(markdownText);

console.log(tree);

Структура MDAST состоит из узлов, каждый из которых имеет тип (type) и массив дочерних узлов (children). Пример дерева для вышеуказанного Markdown:

{
  "type": "root",
  "children": [
    {
      "type": "heading",
      "depth": 1,
      "children": [{ "type": "text", "value": "Заголовок 1" }]
    },
    {
      "type": "paragraph",
      "children": [
        { "type": "text", "value": "Это параграф с " },
        { "type": "emphasis", "children": [{ "type": "text", "value": "курсивом" }] },
        { "type": "text", "value": " и " },
        { "type": "strong", "children": [{ "type": "text", "value": "жирным текстом" }] },
        { "type": "text", "value": "." }
      ]
    }
  ]
}

Типы узлов MDAST

Основные типы узлов включают:

  • root — корневой элемент дерева.
  • heading — заголовки Markdown (#, ##, …), с атрибутом depth.
  • paragraph — абзацы текста.
  • text — простой текст.
  • emphasis — выделение курсивом.
  • strong — выделение жирным.
  • link — ссылки с атрибутами url и title.
  • list и listItem — списки и элементы списка.
  • code — блоки кода с атрибутом lang для указания языка.

Каждый узел может содержать дополнительные поля, например, position, где хранится информация о номере строки и позиции в исходном Markdown.

Работа с деревом MDAST

Для обработки дерева используется унифицированная концепция AST. Например, можно рекурсивно обходить дерево и изменять узлы:

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

visit(tree, 'text', (node) => {
  node.value = node.value.replace(/Markdown/g, 'MD');
});

visit позволяет выполнять действия только на узлах определённого типа, что упрощает фильтрацию и модификацию дерева.

Применение плагинов

Remark поддерживает плагины для расширения функциональности. Например, плагин remark-parse добавляет поддержку расширенного синтаксиса:

import remarkParse from 'remark-parse';
import {remark} from 'remark';

const processor = remark().use(remarkParse);
const tree = processor.parse(markdownText);

Другие популярные плагины:

  • remark-gfm — поддержка GitHub Flavored Markdown (таблицы, чекбоксы, автоссылки).
  • remark-frontmatter — для парсинга YAML- и TOML-фронтматтеров.
  • remark-slug — автоматическая генерация id для заголовков.

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

После преобразования Markdown в MDAST часто возникает задача конвертировать дерево в HTML. Для этого используется Rehype, который работает с HAST (HTML AST):

import {remark} from 'remark';
import rehype from 'rehype';
import remark2rehype from 'remark-rehype';
import html from 'rehype-stringify';

const markdown = '# Заголовок';

const htmlContent = await remark()
  .use(remark2rehype)
  .use(html)
  .process(markdown);

console.log(String(htmlContent));

Этот процесс включает два шага:

  1. Преобразование MDAST в HAST с помощью remark-rehype.
  2. Генерация HTML через rehype-stringify.

Особенности работы с MDAST

  • Immutable nodes: Многие плагины ожидают, что узлы не будут напрямую изменяться, а создаются новые объекты при модификации.
  • Position tracking: Узлы сохраняют информацию о позиции текста для точного отображения ошибок и аннотаций.
  • Nested structures: MDAST поддерживает вложенные элементы (списки, цитаты, вложенные параграфы), что позволяет строить сложные трансформации.

Пример практической задачи: подсветка кода

Для подсветки синтаксиса в Markdown можно использовать комбинацию MDAST и Rehype с rehype-prism:

import remark from 'remark';
import remark2rehype from 'remark-rehype';
import rehypeStringify from 'rehype-stringify';
import rehypePrism from 'rehype-prism-plus';

const markdown = '```javascript\nconsole.log("Hello");\n```';

const result = await remark()
  .use(remark2rehype)
  .use(rehypePrism)
  .use(rehypeStringify)
  .process(markdown);

console.log(String(result));

Такой подход позволяет не только парсить Markdown, но и преобразовывать его в стилизованный HTML с подсветкой синтаксиса.

Рекурсивный обход и анализ MDAST

Для сложных трансформаций часто используют рекурсивный обход дерева:

function analyzeNode(node) {
  if (node.type === 'heading' && node.depth === 1) {
    console.log('Найден главный заголовок:', node.children[0].value);
  }

  if (node.children) {
    node.children.forEach(analyzeNode);
  }
}

analyzeNode(tree);

Такой метод полезен для генерации оглавлений, проверки структуры документа и создания кастомных плагинов для Remark.