Masalahnya bukan parsing Markdown
Merender Markdown dengan react-markdown cukup mudah sampai satu fenced code block harus berubah menjadi komponen interaktif.
Saya ingin code block biasa tetap memiliki syntax highlighting, sedangkan block mermaid berubah menjadi diagram SVG. Diagram yang salah juga harus memiliki fallback tanpa merusak artikel.
Implementasi awal memperlakukan semua code block dengan cara yang sama. Source Mermaid tampil sebagai teks, sedangkan perbaikan CSS berikutnya membuat ukuran diagram tidak konsisten.
Pertanyaan desain yang lebih berguna adalah: di mana proses Markdown berhenti dan renderer diagram mulai bekerja?
Pipeline rendering
Pipeline saat ini memberi satu tanggung jawab pada setiap tahap:
remark-gfm menambahkan table, task list, strikethrough, dan fitur GitHub-flavored Markdown. rehype-sanitize membuang HTML berbahaya sebelum output mencapai React.
rehype-highlight membentuk token untuk code block biasa. Mermaid dimasukkan ke daftar plain text agar source-nya diteruskan utuh, bukan dibungkus span untuk highlighting.
<ReactMarkdown
components={{ pre: MarkdownPre }}
rehypePlugins={[
rehypeSanitize,
[rehypeHighlight, { plainText: ["mermaid", "text", "txt"] }],
]}
remarkPlugins={[remarkGfm]}
>
{source}
</ReactMarkdown>
Raw HTML tetap dinonaktifkan. Konten portfolio tetap merupakan input, meskipun filenya berada di repository yang sama dengan aplikasi.
Mendeteksi Mermaid pada boundary pre
Fenced block dirender menjadi <pre><code class="language-mermaid">...</code></pre>. Jika hanya elemen code yang diganti, diagram tetap berada di dalam pre dan mewarisi spacing serta overflow milik code block.
Karena itu, saya memeriksa child pada boundary pre. Block Mermaid mengembalikan komponen, sedangkan bahasa lain tetap mengembalikan elemen pre semula.
function MarkdownPre({ children, ...props }: PreProps) {
const child = Children.toArray(children)[0];
if (isValidElement<CodeProps>(child)) {
const classes = (child.props.className ?? "").split(/\s+/);
if (classes.includes("language-mermaid")) {
const chart = String(child.props.children ?? "").replace(/\n$/, "");
return <MermaidDiagram chart={chart} />;
}
}
return <pre {...props}>{children}</pre>;
}
Boundary kecil ini mencegah beberapa masalah styling. Code biasa tetap mengikuti GitHub Markdown CSS, sedangkan diagram mendapat container khusus SVG.
Memuat Mermaid hanya di browser
Mermaid jauh lebih besar daripada renderer Markdown dan bergantung pada browser API. Import pada module load akan menambah pekerjaan pada halaman tanpa diagram dan menyulitkan server rendering.
Komponen diagram mengimpor Mermaid di dalam effect. Promise pada level module memastikan library hanya dimuat dan diinisialisasi satu kali.
let mermaidPromise: Promise<MermaidApi> | undefined;
function loadMermaid() {
mermaidPromise ??= import("mermaid").then(({ default: mermaid }) => {
mermaid.initialize({
securityLevel: "strict",
startOnLoad: false,
suppressErrorRendering: true,
theme: "base",
});
return mermaid;
});
return mermaidPromise;
}
Setiap render memakai ID unik. SVG yang dihasilkan dimasukkan ke container khusus, lalu binding function dari Mermaid dijalankan setelah SVG tersedia.
Effect juga menyimpan status aktif komponen. Jika navigasi menghapus komponen sebelum render selesai, hasilnya dibuang dan tidak ditulis ke elemen yang sudah tidak digunakan.
Membuat kegagalan tetap terlihat
Renderer dokumentasi sebaiknya tidak menyembunyikan source yang salah. Jika Mermaid melempar error, komponen menampilkan detail yang dapat dibuka bersama source diagram.
Fallback ini membantu saat menulis. Kesalahan tetap berada pada satu diagram dan artikel lain masih dapat dirender.
Styling tanpa melawan SVG
Artikel memakai github-markdown-css untuk spacing dokumen dan theme GitHub dari Highlight.js untuk warna token. Override hanya digunakan agar font tetap mengikuti IBM Plex milik situs.
Mermaid membutuhkan aturan terpisah. Wrapper mengatur padding, border, dan horizontal overflow. Scope .markdown-body juga menimpa margin horizontal untuk figure dari github-markdown-css; tanpa override ini, diagram selebar artikel akan bergeser keluar pada layar kecil. SVG mengikuti lebar yang tersedia sambil mempertahankan rasio aspek.
.markdown-body .mermaid-wrapper {
width: 100%;
margin: 2rem 0;
overflow-x: auto;
overflow-y: hidden;
}
.markdown-body .mermaid-diagram {
min-width: 0;
width: 100%;
}
.markdown-body .mermaid-diagram svg {
display: block;
width: 100%;
max-width: 100%;
height: auto;
margin-inline: auto;
}
Saya juga menetapkan theme variable Mermaid secara eksplisit. ERD membutuhkan rowOdd dan rowEven; variable atribut yang namanya mirip tidak dipakai oleh versi renderer di project ini.
Detail tersebut mengingatkan saya untuk memeriksa SVG dan source renderer sebelum menambah lebih banyak CSS override.
Bagian yang saya pertahankan
Renderer Markdown tidak perlu mengetahui cara Mermaid menggambar diagram. Ia hanya mengenali satu class bahasa, lalu menyerahkan source kepada komponen lain.
Komponen Mermaid tidak perlu memahami Markdown. Ia menerima chart, mengatur lifecycle browser, lalu mengembalikan SVG atau error.
Pemisahan ini menjaga kedua jalur tetap sederhana. Syntax highlighting tetap bekerja untuk TypeScript dan bahasa lain, sementara Mermaid dapat diubah tanpa mengganti pipeline Markdown.
Pelajaran utamanya bukan urutan plugin tertentu. Hal yang lebih penting adalah memilih satu titik handoff, mempertahankan jalur default, dan membuat kegagalan mudah diperiksa.