開發歷程:讓 LLM 畫的圖一鍵變成真的圖——Mermaid 編輯器

這是 LocalPapa Notes 開發紀錄系列的第八篇。前面幾篇圍著手機感測器轉;這一篇走進一個很「AI 時代」的需求——把 Mermaid 線上編輯器 做出來。

為什麼這個工具在 LLM 時代特別有用

你大概遇過:問 ChatGPT 或 Claude「幫我畫一張流程圖」,它回給你一段 Mermaid 語法——一堆 graph TD; A-->B; 的純文字。語法是對的,但你看不到圖。得自己找個地方把它渲染出來。

這個工具就是那個「地方」:左邊貼上 LLM 給的 Mermaid 文字,右邊即時變成圖。更進一步,AI agent 甚至可以直接產生一條「打開就看到圖」的分享連結。

而且——圖表內容 100% 在你瀏覽器本地渲染,從不上傳

即時預覽的隱藏難題:競態

「邊打字邊渲染」聽起來簡單,但藏著一個經典陷阱:競態條件(race condition)

Mermaid 的 render() 是非同步的。你快速打字時,會連續觸發好幾次渲染;如果第 2 次的結果比第 3 次晚回來,畫面上就會出現「舊圖蓋掉新圖」的鬼打牆。

我們的解法是給每次渲染一個遞增的序號 renderSeqawait mermaid.render() 回來後,如果序號已經過期(使用者又輸入了),就直接丟棄這個結果。永遠只讓最新的那次渲染上畫面。

還有兩個體驗細節:

  • 語法錯誤時,刻意保留上一張成功的圖,只在旁邊提示錯誤——而不是讓畫面瞬間空白。你還在打、圖還在,編輯體驗才穩。
  • 主動清掉 Mermaid 殘留在 document.body 的暫存節點,避免快速輸入累積一堆隱形 DOM。

匯出要「離開頁面也不走樣」

Mermaid 把樣式 <style> 內嵌進它輸出的 SVG。直接存下來常常會掉色、尺寸跑掉。所以匯出時我們做了幾道手腳:

  • SVG:clone 一份、補上 xmlns、用 viewBox 推算明確的寬高再序列化——確保在任何地方打開都正確。
  • PNG:把 SVG 畫進 <canvas>,先填白底(不然透明背景在某些檢視器會變黑),再依 1x/2x/3x × 裝置像素比放大;若面積超過上限就等比縮回,最後 toBlob 下載。

目標只有一個:你匯出的圖,離開這個頁面後長得跟預覽一模一樣

把一整張圖塞進一條網址

分享功能是這個工具的亮點:不需要伺服器,就能分享。

作法是把你的 Mermaid 程式碼用 pako 做 deflate 壓縮,轉成 base64url,加上 pako: 前綴編進網址的 # 片段。對方打開連結,前端自己解碼、還原、渲染——整張圖就「藏」在網址裡,沒有任何後端參與。(pako 載入失敗時退回純 base64、前綴 code:,確保功能不中斷。)

當然,網址不能無限長。所以加了務實的上限:解碼後內容超過 100KB、或產生的網址超過 8000 字元就提醒改用匯出。遇到亂碼 hash 也不拋例外,靜靜退回草稿或範本——別讓一條壞連結弄垮整個工具。

安全:渲染別人的圖,得小心

分享連結意味著你可能會渲染別人給的 Mermaid 程式碼。Mermaid 圖表裡其實可以藏 script——這就是 XSS 的溫床。

所以我們開了 securityLevel: 'strict' 擋下圖表內嵌的腳本,並用 suppressErrorRendering 把錯誤顯示收回由本工具自管。接受外部輸入的工具,預設就要假設輸入是有敵意的。

載入也要穩:鎖版本 + 優雅降級

  • <script type="module"> 搭配動態 import(),從 jsDelivr 取鎖定版mermaid@11.4.1——避免上游 minor 更新偷偷改了語法行為、把使用者的圖弄壞。
  • 整個載入用 try/catch 包住;CDN 掛掉時,預覽區顯示友善錯誤與「重試」鈕,而且編輯與複製程式碼仍然可用,不會整頁癱瘓。

最後沿用 tools-common.js 既有的 getTranslationdownloadBlob 等共用函式,並監聽主題切換在 dark/light 間用對應的 mermaid 主題重渲染;草稿則 debounce 自動存進 localStorage,載入優先序是「分享連結 > 草稿 > 預設範本」。

學到的事

  • 即時 = 要處理競態。 任何「邊輸入邊運算」的功能,序號丟棄這類防競態手法幾乎是必備。
  • 匯出的保真度是魔鬼細節。 內嵌樣式、白底、xmlns、尺寸——少一個,圖離開頁面就走樣。
  • 無伺服器分享,靠的是把狀態編進 URL。 壓縮 + hash 片段,讓一條連結自帶全部內容。
  • 凡是接受外部輸入,先假設它有惡意。 securityLevel: 'strict' 不是選配。
  • 為 AI 而生的工具,正在變多。 LLM 產文字、工具把文字變成可看可用的東西——這個介面層會越來越重要。

一以貫之的問題

這個工具把系列的問題接到了 AI:當 LLM 給你一段它「畫」好的文字,誰來把它變成你真正看得到的圖?

答案是:一個 100% 在你瀏覽器裡跑、還能把圖塞進一條連結的網頁。

下一篇開發紀錄,我們會走進另一條長線——把整個網站變成 AI agent 讀得懂的知識包(OKF)。想追蹤系列?把 LocalPapa Notes 加入書籤吧。

Dev Log: Turning LLM-Drawn Diagrams Real in One Click — the Mermaid Editor

This is the eighth post in the LocalPapa Notes dev-log series. The last few orbited phone sensors; this one steps into a very "AI-era" need — building the Mermaid Online Editor.

Why this tool is especially useful in the LLM era

You've probably hit this: you ask ChatGPT or Claude "draw me a flowchart," and it hands back a chunk of Mermaid syntax — plain text full of graph TD; A-->B;. The syntax is right, but you can't see the diagram. You have to find somewhere to render it.

This tool is that "somewhere": paste the Mermaid text from the LLM on the left, and it becomes a diagram on the right, live. Better still, an AI agent can directly produce a "open and see the diagram" share link.

And — the diagram content is rendered 100% locally in your browser, never uploaded.

The hidden problem with live preview: race conditions

"Render as you type" sounds simple, but hides a classic trap: a race condition.

Mermaid's render() is asynchronous. When you type fast, you trigger several renders in a row; if the 2nd result comes back later than the 3rd, you get the maddening "old diagram overwriting the new one" on screen.

Our fix is to give each render an incrementing sequence number renderSeq: after await mermaid.render() returns, if the sequence is already stale (the user typed again), discard the result. Only ever let the latest render reach the screen.

Two more experience details:

  • On a syntax error, deliberately keep the last successful diagram, showing the error beside it — rather than blanking the screen. You're still typing, the diagram's still there; that's what makes editing stable.
  • Proactively clean up the temporary nodes Mermaid leaves in document.body, so fast typing doesn't accumulate piles of invisible DOM.

Exports must "not warp once they leave the page"

Mermaid embeds its <style> into the SVG it outputs. Save it directly and colours often drop or sizes go wrong. So on export we do a few things:

  • SVG: clone it, add xmlns, derive explicit width/height from the viewBox, then serialize — so it's correct wherever it's opened.
  • PNG: draw the SVG into a <canvas>, first fill a white background (otherwise transparent backgrounds turn black in some viewers), scale by 1x/2x/3x × device pixel ratio, clamp back proportionally if the area exceeds a limit, then toBlob to download.

The goal is singular: the image you export looks exactly like the preview once it leaves this page.

Stuffing a whole diagram into a single URL

The share feature is this tool's highlight: sharing without a server.

We deflate your Mermaid code with pako, convert to base64url, add a pako: prefix, and encode it into the URL's # fragment. The recipient opens the link, and the frontend decodes, restores and renders it itself — the whole diagram is "hidden" in the URL, with no backend involved. (If pako fails to load, it falls back to plain base64 with a code: prefix, so the feature doesn't break.)

URLs can't be infinitely long, of course. So we added pragmatic limits: if decoded content exceeds 100KB, or the generated URL exceeds 8000 characters, we suggest exporting instead. A garbage hash doesn't throw either — it quietly falls back to the draft or a template. Don't let one bad link take down the whole tool.

Security: rendering someone else's diagram needs care

Share links mean you might render Mermaid code someone else gave you. Mermaid diagrams can actually hide scripts — a breeding ground for XSS.

So we enable securityLevel: 'strict' to block scripts embedded in diagrams, and use suppressErrorRendering to take error display back under the tool's own control. A tool that accepts external input should assume by default that the input is hostile.

Loading has to be robust too: pin the version, degrade gracefully

  • Use <script type="module"> with dynamic import() to fetch a pinned mermaid@11.4.1 from jsDelivr — so an upstream minor update can't quietly change syntax behaviour and break users' diagrams.
  • Wrap the whole load in try/catch; if the CDN goes down, the preview shows a friendly error and a "retry" button, and editing and copying the code still work — no full-page failure.

Finally we reuse tools-common.js's shared getTranslation / downloadBlob, listen for theme switches to re-render with the matching mermaid theme in dark/light, and debounce-autosave drafts to localStorage, with a load priority of "share link > draft > default template."

What we learned

  • Live = you must handle races. For any "compute as you type" feature, sequence-discarding is almost mandatory.
  • Export fidelity is a devil-in-the-details game. Embedded styles, white background, xmlns, dimensions — miss one and the diagram warps off-page.
  • Server-less sharing means encoding state into the URL. Compression + hash fragment lets a single link carry all its content.
  • Anything that accepts external input, assume malice first. securityLevel: 'strict' isn't optional.
  • Tools built for AI are multiplying. LLMs produce text; tools turn that text into something viewable and usable — this interface layer will only grow more important.

The same question, wired to AI

This tool connects the series' question to AI: when an LLM hands you text it has "drawn," who turns it into a diagram you can actually see?

The answer: a web page that runs 100% in your browser — and can even tuck the diagram into a link.

In the next dev log, we step into another long thread — turning the whole site into a knowledge pack AI agents can read (OKF). Want to follow the series? Bookmark LocalPapa Notes.

探索 39+ 個隱私優先的瀏覽器工具
全程本地運算、檔案不上傳。
前往 LocalPapa →
Explore 39+ privacy-first browser tools
Everything runs locally — your files never leave your device.
Visit LocalPapa →

想看英文版?點右上角 EN 切換語言。

Prefer Chinese? Tap at the top-right to switch.