Задача: Переработать Markdown-компонент и унифицировать рендеринг с MDXEditor
Переработать Markdown-компонент и унифицировать рендеринг с MDXEditor
Устранить несовместимость между 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.