Конфликты спецификаций

Библиотеки Remark и Rehype в экосистеме JavaScript предназначены для обработки разметки Markdown и HTML соответственно. Remark работает с Markdown AST (Abstract Syntax Tree), а Rehype — с HTML AST. Несмотря на их высокую совместимость и возможность конвейерной обработки через unified, возникают ситуации, когда спецификации этих форматов могут конфликтовать, что требует внимательного подхода.


Различия между Markdown и HTML AST

  • Markdown AST (MDAST) строится на основе синтаксиса Markdown. Основные ноды включают paragraph, heading, list, listItem, code и link. Каждая нода несет строго типизированную структуру, где вложенные элементы строго ограничены.
  • HTML AST (HAST) строится на основе HTML-разметки. Ноды включают element, text, comment, с поддержкой произвольных атрибутов. В отличие от MDAST, HAST допускает более свободное вложение элементов.

Конфликты начинаются тогда, когда Markdown-параметры не имеют прямого соответствия в HTML, или наоборот. Например, нода strong в MDAST всегда должна содержать текст, а в HAST strong может быть пустой или содержать вложенные элементы em, span, a.


Конвертация MDAST → HAST

Remark предоставляет плагин remark-rehype, который преобразует MDAST в HAST. Основные шаги этого процесса:

  1. Создание соответствий нод Каждая MDAST-нода сопоставляется с HAST-нодой. Примеры:

    • paragraphp
    • headingh1h6 в зависимости от уровня
    • inlineCodecode
  2. Обработка вложенности MDAST поддерживает ограниченную вложенность (listlistItemparagraph), тогда как HAST допускает более свободное сочетание тегов. Remark использует промежуточные преобразования, чтобы сохранить смысл Markdown, не нарушая структуру HTML.

  3. Событие конфликта типов Если в MDAST встречается нестандартная нода, например кастомный плагин добавил youtubeEmbed, remark-rehype не сможет автоматически создать подходящую HAST-ноду. В этом случае используется h-name и h-properties, позволяющие задать точное имя HTML-тега и атрибуты:

    const remark = require('remark');
    const rehype = require('rehype');
    const remarkRehype = require('remark-rehype');
    
    const processor = remark()
      .use(customPlugin)
      .use(remarkRehype, {
        handlers: {
          youtubeEmbed(node) {
            return {
              type: 'element',
              tagName: 'iframe',
              properties: { src: node.url, frameborder: 0 },
              children: []
            };
          }
        }
      });

Конфликты атрибутов

MDAST не поддерживает произвольные атрибуты HTML. При конверсии в HAST возникают следующие проблемы:

  • Атрибут id и class для заголовков и параграфов должны задаваться через данные свойства (data или hProperties).
  • Markdown-плагины, добавляющие атрибуты, могут конфликтовать с Rehype-плагинами, ожидающими определённую структуру HAST.

Пример разрешения конфликта:

const remark = require('remark');
const rehype = require('rehype');
const remarkRehype = require('remark-rehype');

const processor = remark()
  .use(remarkRehype, {
    handlers: {
      heading(node) {
        const hNode = {
          type: 'element',
          tagName: `h${node.depth}`,
          properties: node.data?.hProperties || {},
          children: node.children.map(child => ({
            type: 'text',
            value: child.value
          }))
        };
        if (!hNode.properties.className) {
          hNode.properties.className = ['default-heading'];
        }
        return hNode;
      }
    }
  });

Здесь мы гарантируем, что каждый заголовок будет иметь хотя бы один CSS-класс, предотвращая конфликт с плагинами Rehype, которые могут манипулировать классами.


Разные правила вложенности

Markdown ограничивает вложенность списков и заголовков строгими правилами синтаксиса, а HTML допускает произвольную вложенность. При конверсии возможны следующие ситуации:

  1. Markdown list → HAST list

    • MDAST требует listItemparagraphtext.
    • HAST допускает li с div и произвольными элементами. Решение: либо оборачивать весь контент listItem в p, либо разрешить прямое вложение блоков.
  2. Markdown link → HTML link с вложенными тегами

    • MDAST link содержит только текст или inlineCode.
    • HAST позволяет вложить strong или em. Решение: использовать hChildren для ручного преобразования вложенных нод.

Практика предотвращения конфликтов

  • Использование hProperties и hName — явное управление именами тегов и атрибутами.
  • Разделение обработки Markdown и HTML — сначала формировать чистый MDAST, затем обрабатывать только те ноды, которые требуют кастомизации.
  • Тестирование конвейеров — проверка, что преобразованный HAST соответствует ожиданиям, особенно для нестандартных плагинов.
  • Согласование плагинов Remark и Rehype — не все плагины совместимы, поэтому лучше использовать совместимые пары или писать адаптеры.

Особенности пользовательских плагинов

  • Пользовательские MDAST-плагины могут добавлять произвольные ноды, которые не имеют стандартного соответствия в HAST.
  • Нужно предоставлять обработчик для каждой такой ноды через remark-rehype handlers.
  • Любые атрибуты, которые Markdown не поддерживает, передаются через data.hProperties, чтобы Rehype мог корректно их использовать.

Проблемы с сериализацией

После конверсии HAST в HTML через rehype-stringify важно учитывать:

  • Пустые ноды element без children могут сериализоваться как <tag></tag> или <tag /> в зависимости от настроек.
  • Атрибуты, переданные через hProperties, должны быть корректно сериализованы в HTML-атрибуты.
  • Несоответствие типов (например, number вместо string в className) может вызвать ошибки в браузере или при рендеринге React.

Итоговая схема предотвращения конфликтов

  1. Чистый Markdown → MDAST.
  2. Расширение MDAST пользовательскими нодами (с data.hProperties).
  3. Преобразование MDAST → HAST с помощью remark-rehype, с явными обработчиками для нестандартных нод.
  4. Настройка HAST через hName, hProperties, проверка вложенности.
  5. Сериализация HAST → HTML через rehype-stringify, проверка корректности атрибутов и структуры.

Эта последовательность минимизирует конфликты спецификаций и обеспечивает стабильный конвейер обработки Markdown и HTML.