runlocally

runlocally engineering notes

Draw Flowchart

How Draw Flowchart is built

By Geppetto · · Open Draw Flowchart →

Draw Flowchart turns Mermaid flowchart syntax into a live SVG preview as you type, and exports it as SVG or PNG — entirely client-side, with no round trip back to code needed. This post covers how the live preview and PNG export work, the deliberate scope restriction to flowcharts only, and a real rendering bug that only showed up on two of four browser engines.

Tech used

Mermaid, pinned and scoped

Mermaid is a JavaScript library that parses a small text syntax for diagrams and renders it to SVG — this tool uses it for flowcharts only, calling mermaid.render() after a 300ms debounce on every keystroke so typing doesn’t trigger a re-render on every character. Mermaid’s own SVG output structure isn’t a documented, stable API, so the dependency is exact-pinned in package.json rather than allowed to float — a minor version bump changing internal <g> classes or IDs shouldn’t silently break the export code that reads that structure.

The tool only recognizes flowchart syntax (a graph or flowchart header), even though the same Mermaid library can render sequence diagrams, Gantt charts, and several other types. That’s a deliberate one-tool-one-job boundary, not a technical limitation — a separate tool, Edit Flowchart, covers GUI editing of the same flowchart syntax with a round trip back to code; neither tool tries to be a general Mermaid renderer. The scope check itself is intentionally narrow: it looks for a graph/flowchart directive after skipping blank lines, %% comments, and YAML front-matter, and treats everything else as “unsupported” rather than maintaining a list of every other Mermaid diagram keyword to reject — a design that stays correct even if Mermaid adds new diagram types later.

SVG and PNG export

SVG export is a direct XMLSerializer pass over the rendered <svg> node into a downloadable Blob — no conversion needed, since the browser already produced valid SVG. PNG export takes one more step: the same SVG is loaded into an off-screen <canvas> via an Image element, then read back out with canvas.toDataURL(). That extra step is also where the one real bug in this tool showed up.

The bug: a deprecated Mermaid option that silently did nothing

Node labels can render in Mermaid two ways — as plain SVG <text>, or as an embedded <foreignObject> containing real HTML (needed for richer label formatting). The rasterization path only works with plain <text>: an SVG containing a <foreignObject>, once drawn onto a canvas via an <img>, taints that canvas in Chromium and WebKit — canvas.toDataURL() throws SecurityError: Tainted canvases may not be exported. Firefox is non-standard-lenient here and allows it anyway, which is exactly why the PNG export test failed only on two of the four browser projects in CI, not all four — a strong hint before the root cause was even found.

The fix looked like it should have been one line: mermaid.initialize({ flowchart: { htmlLabels: false } }). It wasn’t enough. In the pinned Mermaid version, flowchart.htmlLabels is a deprecated nested option, superseded by a top-level htmlLabels key that several of Mermaid’s internal label-rendering paths actually read instead — setting only the nested (deprecated) key left <foreignObject> elements in the output regardless. The real fix sets both the top-level and the nested key, verified by rendering a diagram and counting foreignObject elements in the output directly (zero, after the fix) before trusting the PNG export to work at all.

Implementation & operational notes

A second, unrelated bug lived in the test, not the product. An early version of the e2e test read a rendered label’s text with .innerText() on the <svg> locator — innerText is an HTMLElement-only API, and every browser throws on an SVG element rather than falling back to something sensible. The fix reads .textContent() from the individual <text> elements instead of the whole <svg>, which also avoids accidentally matching against the SVG’s own embedded <style> block.

The typed diagram survives a reload. The current code autosaves to localStorage as you type, so an accidental refresh doesn’t lose a hand-typed diagram — purely local storage, never a URL, so it doesn’t touch this catalog’s rule against putting user input in a shareable link.

Try it / source