Nhiệm vụ: Thêm component tương thích Markdown nội bộ
Thêm component tương thích Markdown nội bộ
Trước khi tái cấu trúc Markdown trong core haih-agent, hãy thêm vào BiznesHelper một renderer nội bộ tương thích với định dạng Markdown thực tế được lưu bởi MDXEditor.
Bối cảnh
Core của react-markdown và @mdxeditor/editor biên dịch các phần của Markdown khác nhau, đặc biệt là khoảng trắng/thụt lề (whitespace/indentation). Do đó, nội dung kèm HTML hợp lệ sau khi qua MDXEditor có thể bị biến thành code block trong quá trình đọc SSR.
Cần làm gì
- Tạm thời thêm một component Markdown nội bộ / tầng chuẩn hóa (normalization layer) cho BiznesHelper theo giải pháp đã áp dụng trên VietnamGuru.
- Khắc phục các trường hợp sử dụng bốn dấu cách và tab mà không làm hỏng các code block thực sự.
- Không chuyển giải pháp này vào core cho đến khi hoàn thành nhiệm vụ R&D riêng của haih-agent.
- Thay thế component nội bộ bằng component chung sau khi có giải pháp cấp nền tảng (platform solution).
Kết quả
Markdown được nhập và cập nhật bởi AI sẽ hiển thị chính xác trên BiznesHelper thông qua SSR mà không bị chuyển đổi nhầm HTML/nội dung thành đoạn mã nguồn (code listing).
Ворклоги
Chuẩn hóa HTML cũ: thay thế <p> bằng <div> khi nhập liệu
Khi di chuyển nội dung cũ, một xung đột khác giữa định dạng WYSIWYG cũ và cách dựng hình React/SSR mới đã được phát hiện. Trình soạn thảo cũ đã bọc các đoạn HTML lớn trong thẻ <p>. Trong phiên bản mới, HTML được nhập như vậy có thể nằm bên trong một đoạn văn đã tồn tại từ trước, khiến React nhận được cấu trúc HTML không hợp lệ và cảnh báo:
In HTML, <p> cannot be a descendant of <p>. This will cause a hydration error.
Nguyên nhân
Vấn đề không nằm ở phần văn bản cụ thể, mà ở ngữ nghĩa của thẻ <p>: HTML không cho phép các đoạn văn lồng nhau. WYSIWYG cũ đã sử dụng <p> làm thẻ bao bọc khối đa năng, mặc dù một vùng chứa trung tính như <div> an toàn hơn cho nội dung lồng nhau tùy ý.
Điều này đặc biệt phiền toái trong SSR: trình duyệt có thể tự sửa cấu trúc HTML không hợp lệ theo cách mà React không mong đợi, và cấu trúc DOM kết quả sau khi phân tích cú pháp sẽ khác với định dạng phía máyδ chủ. Điều này dẫn đến lỗi/sai lệch kết quả hiển thị (hydration mismatch/error).
Giải pháp được chọn
Một hàm hỗ trợ (helper) sử dụng cheerio đã được thêm vào trình nhập liệu, giúp thay thế tất cả các thẻ <p> thành <div> trước khi lưu HTML cũ, đồng thời giữ nguyên HTML bên trong và các thuộc tính:
import * as cheerio from 'cheerio'
export function replacePWithDiv(html: string): string {
const $ = cheerio.load(html, { xml: false }, false)
$('p').each((_, el) => {
const $el = $(el)
const div = $('<div></div>')
div.html($el.html() || '')
for (const attr of el.attributes) {
div.attr(attr.name, attr.value)
}
$el.replaceWith(div)
})
return $.html()
}
Tại sao quá trình chuẩn hóa được thực hiện trong trình nhập liệu
Đây là vấn đề đặc thù của dữ liệu nguồn cũ (legacy), vì vậy sẽ an toàn hơn nếu sửa nó ngay tại đầu vào của hệ thống mới, thay vì bắt trình dựng hình Markdown/React chung phải liên tục bù đắp cho định dạng WYSIWYG cũ.
Trình nhập liệu ở đây đóng vai trò là lớp chuẩn hóa (normalization layer): giữ nguyên ý nghĩa của nội dung cũ, nhưng loại bỏ cấu trúc chắc chắn gây xung đột với HTML hợp lệ và quá trình hydration của React.
Ý nghĩa đối với tác vụ Markdown chung
Trường hợp này bổ sung cho vấn đề khoảng trắng/thụt lề (whitespace/indentation): đường ống (pipeline) xử lý mới phải tính đến không chỉ cú pháp Markdown, mà cả chất lượng của HTML được nhúng. Ngay cả khi Markdown về mặt hình thức là giống nhau, HTML cũ bên trong nó có thể có cấu trúc mà các chuỗi trình phân tích cú pháp/dựng hình (parser/renderer) khác nhau xử lý theo các cách khác nhau.
Giải pháp cục bộ này vẫn là một phần của lớp tương thích BiznesHelper cho đến khi một đường ống chuẩn hóa/kiểm tra tính hợp lệ tổng quát hơn xuất hiện trong haih-agent.
Trường hợp mới: chuẩn hóa legacy HTML trước khi render MDX/Markdown
Khi import nội dung từ CKEditor cũ, một loại không tương thích mới đã xuất hiện: ngay cả sau khi loại bỏ các thẻ <p> lồng nhau, một phần HTML vẫn hợp lệ về mặt cú pháp đối với trình duyệt, nhưng lại xử lý rất kém khi qua MDX parser và quá trình SSR render tiếp theo.
Triệu chứng
MDX parser bị lỗi với các thông báo kiểu như:
Error parsing markdown: Expected the closing tag `</p>` either after the end of `paragraph` ...
Error parsing markdown: Expected the closing tag `</div>` either after the end of `paragraph` ...
Một đoạn legacy tiêu biểu trông như thế này:
<p style="text-align:justify"><img alt="" src="/ckeditor_assets/pictures/40/content_oil.png" style="border-style:solid; border-width:1px; height:503px; width:703px"><br>
<span style="font-family:trebuchet ms,helvetica,sans-serif; font-size:14px">Văn bản...</span></p>
Ở đây có sự kết hợp đồng thời của nhiều vấn đề:
<br>được viết ở dạng HTML thường, nhưng pipeline MDX/JSX lại mong đợi dạng tự đóng (self-closing);- WYSIWYG cũ sử dụng tích cực thẻ
<p>như một container đa năng; - Khoảng trắng và ngắt dòng giữa các thẻ HTML có thể không còn được MDX diễn giải là định dạng trung tính của mã nguồn nữa, mà bị coi là ranh giới của các đoạn markdown (paragraph).
Những gì đã được kiểm tra
Ban đầu, việc chuẩn hóa thông qua htmlparser2 + dom-serializer đã được xem xét, nhưng phương án này tỏ ra bất tiện cho bài toán: các thẻ tự đóng không được chuẩn hóa theo đúng cách mà MDX yêu cầu, và văn bản có thể được serialize kèm theo các HTML entities không mong muốn.
Phương án khả thi hóa ra lại là rehype, cho phép đưa legacy HTML về dạng serialize ổn định hơn trước, sau đó mới chuyển kết quả sang pipeline chuẩn hóa Markdown chung.
Pipeline hiện tại
Quá trình chuẩn hóa hiện bao gồm một số bước tuần tự.
1. Các thẻ <p> cũ được thay thế bằng <div>
Điều này loại bỏ tình trạng lồng nhau không hợp lệ của các đoạn văn và giảm thiểu rủi ro lệch hydration (hydration mismatch):
function replacePWithDiv(html: string): string {
const $ = cheerio.load(html, { xml: false }, false)
$('p').each((_, el) => {
const $el = $(el)
const div = $('<div></div>')
div.html($el.html() || '')
for (const attr of el.attributes) {
div.attr(attr.name, attr.value)
}
$el.replaceWith(div)
})
return $.html()
}
2. HTML được chạy qua rehype
rehype được sử dụng làm parser/stringifier để sửa lỗi và chuẩn hóa cấu trúc legacy:
const normalized = await rehype()
.data('settings', {
fragment: true,
closeSelfClosing: true,
})
.process(html)
Hiệu ứng cốt lõi là các phần tử tự đóng như <br> được đưa về dạng đi qua quá trình xử lý của MDX/JSX một cách an toàn hơn.
3. Xóa khoảng trắng giữa các thẻ liền kề
Sau khi serialize, các dấu xuống dòng và khoảng trắng giữa các thẻ HTML sẽ tiếp tục bị thu gọn lại:
html = String(normalized).replace(/>\s+</g, '><')
Điều này cực kỳ quan trọng đối với MDX: việc xuống dòng giữa các nút HTML dạng inline/block có thể ảnh hưởng đến quá trình phân tích cú pháp markdown và tạo ra ranh giới paragraph bất ngờ. Trong HTML của trình duyệt, khoảng trắng như vậy thường không ảnh hưởng gì, nhưng trong định dạng kết hợp Markdown+HTML thì không còn như vậy nữa.
Triển khai tổng thể
Logic được gom vào helper cleanupOldContent, hoạt động như một tầng chuẩn hóa riêng biệt giữa legacy CKEditor HTML và hệ thống Concept/Markdown mới:
import * as cheerio from 'cheerio'
import { rehype } from 'rehype'
import { normalizeMarkdownContent } from '../../KBConcept/helpers/normalizeMarkdownContent'
function replacePWithDiv(html: string): string {
const $ = cheerio.load(html, { xml: false }, false)
$('p').each((_, el) => {
const $el = $(el)
const div = $('<div></div>')
div.html($el.html() || '')
for (const attr of el.attributes) {
div.attr(attr.name, attr.value)
}
$el.replaceWith(div)
})
return $.html()
}
export async function cleanupOldContent(html: string): Promise<string> {
html = html.replaceAll(/<u>([^<]+)?<\/u>:/g, '<u>$1:</u>')
html = replacePWithDiv(html)
try {
const normalized = await rehype()
.data('settings', {
fragment: true,
closeSelfClosing: true,
})
.process(html)
html = String(normalized).replace(/>\s+</g, '><')
return await normalizeMarkdownContent(html)
} catch (error) {
console.error(error)
console.error('cleanupOldContent html', html)
throw error
}
}
Kết luận kiến trúc
Trường hợp này cho thấy tầng tương thích cục bộ (local compatibility layer) cho BiznesHelper không chỉ phải chuẩn hóa khoảng trắng trong Markdown mà còn phải chuẩn hóa cả embedded legacy HTML trước khi nội dung đi vào pipeline MDX/SSR.
Nói cách khác, luồng import thực tế hiện tại trông như sau:
legacy CKEditor HTML
→ Dọn dẹp ở cấp độ DOM
→ Chuẩn hóa HTML thông qua rehype
→ Chuẩn hóa khoảng trắng
→ normalizeMarkdownContent chung
→ Lưu trữ Concept
Đây là một sự phân tách trách nhiệm hữu ích: phần đánh dấu cũ dù dễ dãi với trình duyệt nhưng không ổn định sẽ được sửa một lần ngay từ đầu vào, thay vì phải bù đắp vô thời hạn ở bộ render công khai.
Trạng thái
Vấn đề cụ thể với khoảng ngắt giữa các thẻ và HTML tự đóng hiện đã được giải quyết bằng helper chuẩn hóa cục bộ. Vấn đề tương thích chung của MDXEditor / Markdown renderer vẫn là đối tượng của một nhiệm vụ R&D riêng biệt trong haih-agent.