Задача: Переработать Markdown-компонент и унифицировать рендеринг с MDXEditor

Переработать Markdown-компонент и унифицировать рендеринг с MDXEditor

09.09.2026haih-агент

Устранить несовместимость между react-markdown на чтение и @mdxeditor/editor на редактирование: один Markdown-контент должен одинаково интерпретироваться в редакторе, клиентском рендере и SSR.

Контекст

Сейчас в haih-agent один и тот же формат контента — Markdown — фактически обслуживается двумя разными движками:

  • react-markdown используется для чтения/рендеринга;
  • @mdxeditor/editor используется для редактирования.

На уровне продукта это выглядит как единый формат, но внутри используются разные парсеры, разные AST/serialization pipeline и разные допущения по синтаксису. В результате Markdown, который нормально проходит через редактор и сохраняется им, может иначе интерпретироваться при публичном рендеринге.

Проблема уже проявилась на VietnamGuru и теперь повторяется на BiznesHelper, из-за чего приходится писать локальные кастомные Markdown-компоненты вместо решения на уровне ядра.

Ключевое ограничение: SSR

Для публичного сайта Markdown должен корректно рендериться на сервере. Нельзя просто использовать MDXEditor как единый runtime для чтения: редактор построен поверх клиентского rich-text/Lexical-стека и не является заменой лёгкому SSR-safe Markdown renderer.

Поэтому задача не сводится к «использовать один компонент везде». Нужно добиться одинаковой семантики Markdown при разных runtime, сохранив SSR.

Подтверждённый конфликт: indentation

Один из наиболее неприятных случаев связан с пробелами и табуляцией.

MDXEditor при импорте/экспорте Markdown может нормально относиться к дополнительным отступам и сам сериализовывать содержимое с отступами. Но CommonMark-совместимый renderer воспринимает четыре ведущих пробела или tab stop как синтаксис indented code block. Поэтому визуально и семантически корректный фрагмент с HTML/вложенными тегами после сохранения редактором может на чтении превратиться в <pre><code> вместо HTML/контента.

Это стандартное правило CommonMark: четыре пробела являются синтаксисом indented code block. Значит, проблема не является случайным багом одной страницы — это системная несовместимость round-trip поведения редактора и renderer-а.

Почему это критично

Контент в haih-agent всё чаще становится AI-driven и автоматически модифицируется редакторами/агентами. Если строка Markdown не имеет стабильной интерпретации между edit/read/SSR pipeline, то любой автоматический rewrite может незаметно изменить структуру документа.

Это особенно опасно для:

  • HTML внутри Markdown;
  • custom components/directives;
  • вложенных блоков;
  • табуляции и отступов;
  • списков;
  • fenced/indented code blocks;
  • AI-generated форматирования;
  • round-trip editor -> markdown -> renderer.

Цель

Сделать единый предсказуемый Markdown-контракт для всей платформы: контент, сохранённый MDXEditor или AI-агентом, должен одинаково и безопасно отображаться через SSR/read renderer без необходимости писать project-specific Markdown-компоненты.

Что исследовать

  • Точный parser/serializer stack @mdxeditor/editor: mdast visitors, HTML nodes, directives, JSX/MDX и правила сериализации whitespace.
  • Точный pipeline react-markdown/remark в текущем ядре и его CommonMark/GFM-настройки.
  • Какие различия являются настройками plugins, а какие фундаментальными различиями двух реализаций.
  • Поведение HTML-blocks, inline HTML и вложенного Markdown внутри HTML.
  • Поведение 4-space indentation и tabs после round-trip через MDXEditor.
  • Lists/blockquote/code fence edge cases.
  • Custom tags/components, которые уже используются в haih-проектах.
  • Можно ли вынести общий remark/mdast normalization pipeline, который будет применяться и перед сохранением, и перед SSR-render.
  • Нужен ли canonicalization/normalization Markdown перед записью в БД.
  • Стоит ли запрещать indented code blocks на уровне платформы и использовать только fenced code blocks, чтобы освободить indentation от двусмысленности в нашем контентном подмножестве.
  • Возможность разделить Markdown source contract и конкретные UI-компоненты editor/renderer.

Предпочтительное направление

Нужно мыслить не двумя React-компонентами, а одной спецификацией контента:

Markdown source → единый normalization/validation layer → MDXEditor для редактирования → SSR-safe renderer для чтения

Оба конца должны работать поверх одного согласованного подмножества Markdown и одинаковых mdast/remark-правил настолько, насколько это возможно.

Если MDXEditor при сериализации создаёт синтаксис, который read renderer интерпретирует иначе, это должно исправляться в общем serializer/normalizer, а не в каждом проекте отдельно.

Возможные технические направления

1. Единый Markdown normalization pipeline

Перед сохранением или рендером прогонять документ через общий mdast pipeline, который:

  • нормализует проблемные отступы;
  • сохраняет реальные fenced code blocks;
  • не превращает вложенный HTML в indented code;
  • приводит tabs/whitespace к каноническому виду;
  • валидирует custom nodes/directives.

2. Общий набор remark/rehype plugins

Свести parser options и extensions к максимально общему набору. Все platform-specific расширения Markdown должны регистрироваться централизованно, чтобы editor и renderer понимали один набор возможностей.

3. Явный Markdown dialect haih-agent

Зафиксировать поддерживаемое подмножество синтаксиса. Например, если indented code blocks не нужны продукту, можно канонически использовать только fenced code blocks и безопасно нормализовать неоднозначные четыре пробела в обычном контенте. Это должно быть осознанным platform decision, а не случайным regex-fix.

4. Round-trip test suite

Создать corpus сложных Markdown-примеров и проверять:

source -> MDXEditor -> exported markdown -> SSR renderer

Семантический результат должен сохраняться.

Обязательные тест-кейсы

  • HTML-блок с вложенными строками и четырьмя пробелами.
  • Tabs внутри HTML/markdown blocks.
  • Настоящий indented code block.
  • Fenced code block.
  • Nested lists с четырьмя и более пробелами.
  • Blockquote + list + code.
  • Custom HTML tags.
  • Custom directives/components haih-agent.
  • Markdown после сохранения MDXEditor без ручных изменений.
  • AI-generated Markdown с форматированием.
  • SSR-render и client hydration одного документа.

Результат

В ядре существует один платформенный Markdown-компонент/контракт, который можно использовать во всех проектах без локальных форков. Editor и read renderer могут оставаться технически разными библиотеками, но для поддерживаемого контента дают совместимый результат.

Критерии готовности

  • Один и тот же сохранённый Markdown корректно отображается после MDXEditor round-trip и через SSR renderer.
  • Сценарий с четырьмя пробелами/табами и HTML больше не превращает корректный контент в code listing.
  • SSR сохраняется и не требует browser-only editor runtime.
  • Есть набор regression tests на известные несовместимости.
  • Проекты VietnamGuru и BiznesHelper могут удалить свои project-specific Markdown compatibility-компоненты.
  • Новые extensions Markdown добавляются через единый platform-level механизм.
  • Поведение документировано как Markdown contract haih-agent.

Справка

CommonMark трактует четыре ведущих пробела как indented code block, поэтому такое поведение react-markdown нельзя считать случайной ошибкой renderer-а. MDXEditor, со своей стороны, импортирует и экспортирует Markdown через отдельный набор mdast/Lexical visitors. Значит, устойчивое решение должно находиться на уровне согласованной спецификации/normalization pipeline, а не точечного CSS или UI-fix.