Nhiệm vụ: Refactor thành phần Markdown và thống nhất cách render với MDXEditor
Refactor thành phần Markdown và thống nhất cách render với MDXEditor
Khắc phục sự không tương thích giữa react-markdown (dùng để đọc) và @mdxeditor/editor (dùng để chỉnh sửa): một nội dung Markdown duy nhất phải được hiểu giống nhau trong trình soạn thảo, trình render phía client và SSR.
Ngữ cảnh
Hiện tại trong haih-agent, cùng một định dạng nội dung — Markdown — thực tế đang được xử lý bởi hai engine khác nhau:
react-markdownđược sử dụng để đọc/render;@mdxeditor/editorđược sử dụng để chỉnh sửa.
Ở cấp độ sản phẩm, nó trông giống như một định dạng duy nhất, nhưng bên trong lại sử dụng các trình phân tích cú pháp (parser) khác nhau, pipeline AST/serialization khác nhau và các giả định cú pháp khác nhau. Kết quả là Markdown đi qua trình soạn thảo bình thường và được lưu lại có thể được hiểu khác đi khi render công khai.
Vấn đề này đã xuất hiện trên VietnamGuru và giờ lại lặp lại trên BiznesHelper, buộc chúng ta phải viết các thành phần Markdown tùy chỉnh cục bộ thay vì giải pháp ở cấp độ lõi.
Hạn chế cốt lõi: SSR
Đối với trang web công khai, Markdown phải được render chính xác trên server. Không thể đơn giản dùng MDXEditor làm runtime duy nhất để đọc: trình soạn thảo được xây dựng dựa trên ngăn xếp rich-text/Lexical phía client và không thể thay thế một trình render Markdown nhẹ nhàng, an toàn cho SSR.
Do đó, nhiệm vụ không chỉ đơn giản là "sử dụng một thành phần ở mọi nơi". Chúng ta cần đạt được ngữ nghĩa Markdown giống nhau trên các runtime khác nhau, đồng thời vẫn giữ nguyên SSR.
Xung đột đã được xác nhận: Indentation (Thụt lề)
Một trong những trường hợp khó chịu nhất liên quan đến khoảng trắng và tab.
Khi import/export Markdown, MDXEditor có thể xử lý các khoảng thụt lề bổ sung một cách bình thường và tự serialize nội dung có thụt lề. Tuy nhiên, trình render tuân thủ CommonMark lại coi bốn khoảng trắng dẫn đầu hoặc điểm dừng tab là cú pháp indented code block (khối mã thụt lề). Do đó, một đoạn mã có HTML/thẻ lồng nhau đúng về mặt trực quan và ngữ nghĩa, sau khi được trình soạn thảo lưu lại, có thể biến thành <pre><code> thay vì HTML/nội dung thông thường.
Đây là quy tắc chuẩn của CommonMark: bốn khoảng trắng là cú pháp của indented code block. Điều này có nghĩa là vấn đề không phải là một lỗi ngẫu nhiên của một trang đơn lẻ — mà là sự không tương thích có hệ thống giữa hành vi round-trip của trình soạn thảo và trình render.
Tại sao điều này rất quan trọng
Nội dung trong haih-agent ngày càng do AI điều khiển (AI-driven) và được chỉnh sửa tự động bởi các trình biên tập/tác nhân (agents). Nếu một chuỗi Markdown không có cách giải thích ổn định giữa các pipeline edit/read/SSR, thì bất kỳ bản viết lại tự động nào cũng có thể làm thay đổi cấu trúc tài liệu một cách vô hình.
Điều này đặc biệt nguy hiểm đối với:
- HTML bên trong Markdown;
- Các thành phần/chỉ thị tùy chỉnh (custom components/directives);
- Các khối lồng nhau;
- Tab và khoảng thụt lề;
- Danh sách (lists);
- Fenced/indented code blocks;
- Định dạng do AI tạo ra;
- Round-trip
editor -> markdown -> renderer.
Mục tiêu
Tạo ra một hợp đồng (contract) Markdown duy nhất và có thể dự đoán được cho toàn bộ nền tảng: nội dung được lưu bởi MDXEditor hoặc AI agent phải được hiển thị giống nhau và an toàn thông qua SSR/read renderer mà không cần phải viết các thành phần Markdown cụ thể cho từng dự án.
Những gì cần nghiên cứu
- Stack parser/serializer chính xác của
@mdxeditor/editor: mdast visitors, HTML nodes, directives, JSX/MDX và các quy tắc serialization khoảng trắng. - Pipeline
react-markdown/remark chính xác trong core hiện tại và các cài đặt CommonMark/GFM của nó. - Sự khác biệt nào là do cấu hình plugin, sự khác biệt nào là nền tảng cốt lõi của hai triển khai.
- Hành vi của các khối HTML, inline HTML và Markdown lồng bên trong HTML.
- Hành vi của việc thụt lề 4 khoảng trắng và tab sau khi round-trip qua MDXEditor.
- Các trường hợp biên (edge cases) của lists/blockquote/code fence.
- Các thẻ/thành phần tùy chỉnh đã được sử dụng trong các dự án haih.
- Liệu có thể tách một pipeline chuẩn hóa remark/mdast chung để áp dụng cả trước khi lưu và trước khi render SSR hay không.
- Có cần canonicalization/normalization Markdown trước khi ghi vào cơ sở dữ liệu hay không.
- Có nên cấm indented code blocks ở cấp độ nền tảng và chỉ sử dụng fenced code blocks để loại bỏ sự mơ hồ về thụt lề trong tập con nội dung của chúng ta hay không.
- Khả năng tách biệt
Markdown source contractvà các thành phần UI editor/renderer cụ thể.
Hướng đi ưu tiên
Chúng ta không nên suy nghĩ theo hướng hai React component, mà theo một đặc tả nội dung duy nhất:
Markdown source
→ tầng chuẩn hóa/kiểm tra chung (normalization/validation layer)
→ MDXEditor để chỉnh sửa
→ Trình render an toàn cho SSR để đọc
Cả hai đầu phải hoạt động trên cùng một tập con Markdown đã thống nhất và các quy tắc mdast/remark giống nhau nhất có thể.
Nếu MDXEditor trong quá trình serialize tạo ra cú pháp mà read renderer hiểu theo cách khác, điều đó phải được sửa trong serializer/normalizer chung, chứ không phải sửa riêng ở từng dự án.
Các hướng đi kỹ thuật có thể
1. Pipeline chuẩn hóa Markdown thống nhất
Trước khi lưu hoặc render, chạy tài liệu qua một mdast pipeline chung, thực hiện:
- Chuẩn hóa các khoảng thụt lề có vấn đề;
- Giữ nguyên các fenced code blocks thực sự;
- Không biến HTML lồng nhau thành mã thụt lề;
- Đưa các tab/khoảng trắng về dạng chuẩn;
- Xác thực các custom nodes/directives.
2. Bộ plugin remark/rehype chung
Thu gọn các tùy chọn parser và extension về một tập hợp chung nhất có thể. Tất cả các phần mở rộng Markdown đặc thù của nền tảng phải được đăng ký tập trung để cả editor và renderer đều hiểu cùng một tập tính năng.
3. Phương ngữ Markdown rõ ràng của haih-agent
Chốt lại tập con cú pháp được hỗ trợ. Ví dụ, nếu sản phẩm không cần indented code blocks, chúng ta có thể quy chuẩn chỉ dùng fenced code blocks và chuẩn hóa một cách an toàn bốn khoảng trắng mơ hồ trong nội dung thông thường. Đây phải là một quyết định có chủ đích ở cấp độ nền tảng, không phải là sửa lỗi bằng regex ngẫu nhiên.
4. Bộ kiểm thử Round-trip
Tạo một tập hợp (corpus) các ví dụ Markdown phức tạp và kiểm tra:
source -> MDXEditor -> exported markdown -> SSR renderer
Kết quả ngữ nghĩa phải được bảo toàn.
Các Test Case bắt buộc
- Khối HTML với các dòng lồng nhau và bốn khoảng trắng.
- Tab bên trong các khối HTML/markdown.
- Khối indented code block thực sự.
- Khối fenced code block.
- Danh sách lồng nhau (Nested lists) với bốn khoảng trắng trở lên.
- Blockquote + list + code.
- Thẻ HTML tùy chỉnh.
- Các chỉ thị/thành phần tùy chỉnh của haih-agent.
- Markdown sau khi lưu từ MDXEditor mà không chỉnh sửa thủ công.
- Markdown do AI tạo ra có định dạng.
- SSR render và client hydration của cùng một tài liệu.
Kết quả
Trong core tồn tại một thành phần/hợp đồng Markdown duy nhất của nền tảng, có thể được sử dụng trong tất cả các dự án mà không cần fork cục bộ. Editor và read renderer có thể vẫn là các thư viện kỹ thuật khác nhau, nhưng đối với nội dung được hỗ trợ, chúng mang lại kết quả tương thích.
Tiêu chí hoàn thành
- Cùng một Markdown được lưu hiển thị chính xác sau round-trip của MDXEditor và thông qua SSR renderer.
- Kịch bản với bốn khoảng trắng/tab và HTML không còn biến nội dung hợp lệ thành danh sách mã nguồn (code listing).
- SSR được duy trì và không yêu cầu runtime editor chỉ chạy trên trình duyệt.
- Có một bộ regression tests cho các điểm không tương thích đã biết.
- Các dự án VietnamGuru và BiznesHelper có thể xóa các thành phần tương thích Markdown chuyên biệt cho dự án của họ.
- Các extension Markdown mới được thêm vào thông qua một cơ chế duy nhất ở cấp độ nền tảng.
- Hành vi được tài liệu hóa dưới dạng hợp đồng Markdown của haih-agent.
Tham khảo
CommonMark coi bốn khoảng trắng dẫn đầu là indented code block, vì vậy hành vi này của react-markdown không thể coi là lỗi ngẫu nhiên của renderer. Về phần mình, MDXEditor import và export Markdown thông qua một tập hợp riêng các mdast/Lexical visitors. Do đó, một giải pháp vững chắc phải nằm ở cấp độ đặc tả/pipeline chuẩn hóa thống nhất, chứ không phải sửa CSS cục bộ hoặc sửa UI đơn lẻ.