SVG in Markdown
Showing an SVG (a diagram, a formula, a chart) inside a node's Markdown, the same way in the web UI, meshfox tui, meshfox static and meshfox pdf — with no renderer built into meshfox: any command that can print an SVG will do (Graphviz, Typst, Mermaid, PlantUML, Vega, matplotlib, ...).
An SVG reaches the page as an ordinary Markdown image whose URL is a data:image/svg+xml;base64,… URI, so nothing has to sit next to the canvas. There are two ways to produce it, both on a cached block so the picture is saved in the file under the fence:
output="markdown"— the block's stdout is Markdown; the script prints theline itself (as in pandas-dataframe.canvas.md). Full control, a little boilerplate. See output=markdown below.output="image"— the block's stdout is the SVG; meshfox wraps it into that image line. One-liners likeplantuml -tsvg -pipejust work. See output=image below.
Two more things are covered further down: how an SVG follows the light/dark theme, and the bg= background for diagrams drawn for a white page.
Every block below is self-contained (it prints a small hand-written SVG), so this file runs without any extra tools — except the Real renderers node, whose blocks need the named tool installed (the Python one creates its own virtualenv on first run).
output=markdown
The script prints the whole Markdown image itself. base64 | tr -d '\n' is the portable spelling (GNU base64 wraps lines at 76 columns, the macOS one doesn't).
SVG='<svg xmlns="http://www.w3.org/2000/svg" width="160" height="80" viewBox="0 0 160 80"><rect x="4" y="4" width="152" height="72" rx="10" fill="#ffd166" stroke="#444" stroke-width="3"/><text x="80" y="47" text-anchor="middle" font-family="sans-serif" font-size="18" fill="#222">output=markdown</text></svg>'
echo "Anything Markdown can go around the picture — a **caption**, a table, ..."
echo
printf '\n' "$(printf '%s' "$SVG" | base64 | tr -d '\n')"
Anything Markdown can go around the picture — a caption, a table, ...
output=image
stdout is the SVG and nothing else; meshfox turns it into the image. No base64, no Markdown. stderr (warnings, progress) is still captured separately and shown as its own block above the picture.
echo "this goes to stderr, shown above the image" >&2
cat <<'EOF'
<svg xmlns="http://www.w3.org/2000/svg" width="160" height="80" viewBox="0 0 160 80">
<rect x="4" y="4" width="152" height="72" rx="10" fill="#9bd1a5" stroke="#444" stroke-width="3"/>
<text x="80" y="47" text-anchor="middle" font-family="sans-serif" font-size="18" fill="#222">output=image</text>
</svg>
EOF
this goes to stderr, shown above the image
If the block fails, or prints something that isn't an SVG, there is no broken image: you see what it printed, as plain text (and, for a failure, the exit code).
echo "the renderer said: unexpected token on line 3"
the renderer said: unexpected token on line 3
Theme
An SVG is shown through an <img> (web) or rasterized (TUI), where it can't see the page's CSS — so a diagram drawn in black vanishes on a dark theme. Right before showing it, meshfox rewrites the SVG's text (the file keeps exactly what the tool printed) so it can follow the theme, in two ways:
currentColorbecomes the theme's text color;var(--mf-fg),var(--mf-bg),var(--mf-accent),var(--mf-border)become the theme's colors. Write a fallback —var(--mf-fg, #000)— and a viewer that knows nothing about meshfox (GitHub, a plain browser) still draws sensible colors.
Switch the web UI's theme (or look at the TUI, which is always dark) and compare these three. The first one is the problem; the other two are the fix.
Hard-coded black — doesn't follow anything:
cat <<'EOF'
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="56" viewBox="0 0 200 56">
<rect x="2" y="2" width="196" height="52" rx="8" fill="none" stroke="#000" stroke-width="2"/>
<text x="100" y="33" text-anchor="middle" font-family="sans-serif" font-size="16" fill="#000">hard-coded black</text>
</svg>
EOF
currentColor — the text color of the theme:
cat <<'EOF'
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="56" viewBox="0 0 200 56">
<rect x="2" y="2" width="196" height="52" rx="8" fill="none" stroke="currentColor" stroke-width="2"/>
<text x="100" y="33" text-anchor="middle" font-family="sans-serif" font-size="16" fill="currentColor">currentColor</text>
</svg>
EOF
--mf-* variables with fallbacks — a themed card with an accent dot:
cat <<'EOF'
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="56" viewBox="0 0 200 56">
<rect x="2" y="2" width="196" height="52" rx="8" fill="var(--mf-bg, #fff)" stroke="var(--mf-border, #ccc)" stroke-width="2"/>
<circle cx="22" cy="28" r="8" fill="var(--mf-accent, #ea580c)"/>
<text x="40" y="33" font-family="sans-serif" font-size="16" fill="var(--mf-fg, #000)">--mf-* variables</text>
</svg>
EOF
Only the SVG's text is rewritten, and only for display; a tool's own explicit colors (fill="#ffd166") are never touched. The SVG still goes through <img>, so scripts inside it stay inert.
Background
Many tools draw for a white page: transparent background, dark text, fixed colors that currentColor/--mf-* can't reach (Mermaid, PlantUML, a Typst formula). On a dark theme that is unreadable. Rather than a global rule, you pick a backing per block — bg= accepts #rgb, #rrggbb or transparent, and nothing else (not free-form CSS; the same few words work in the web UI, the TUI, the static site and the PDF).
With output="image", put it in the fence's output-attrs= — the image-attribute syntax (width=/height= as an integer or NN%, bg=), forwarded onto the generated image:
cat <<'EOF'
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="56" viewBox="0 0 200 56">
<rect x="2" y="2" width="196" height="52" rx="8" fill="none" stroke="#000" stroke-width="2"/>
<text x="100" y="33" text-anchor="middle" font-family="sans-serif" font-size="16" fill="#000">black on bg=#fff</text>
</svg>
EOF
Combined with a size (% is the only form the TUI honours — a terminal has no pixel grid):
cat <<'EOF'
<svg xmlns="http://www.w3.org/2000/svg" width="400" height="112" viewBox="0 0 400 112">
<rect x="4" y="4" width="392" height="104" rx="14" fill="#cfe8ff" stroke="#1d4e89" stroke-width="4"/>
<text x="200" y="66" text-anchor="middle" font-family="sans-serif" font-size="30" fill="#1d4e89">width=50% bg=#fff</text>
</svg>
EOF
transparent says "no backing" explicitly (the default anyway):
cat <<'EOF'
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="56" viewBox="0 0 200 56">
<rect x="2" y="2" width="196" height="52" rx="8" fill="none" stroke="currentColor" stroke-width="2"/>
<text x="100" y="33" text-anchor="middle" font-family="sans-serif" font-size="16" fill="currentColor">bg=transparent</text>
</svg>
EOF
With output="markdown" — or in any hand-written Markdown — it's the same syntax straight after the image, no space: {bg=#fff} (also {width=50%}, {width=50% bg=#fff}):
SVG='<svg xmlns="http://www.w3.org/2000/svg" width="200" height="56" viewBox="0 0 200 56"><rect x="2" y="2" width="196" height="52" rx="8" fill="none" stroke="#000" stroke-width="2"/><text x="100" y="33" text-anchor="middle" font-family="sans-serif" font-size="16" fill="#000">{bg=#fff} in Markdown</text></svg>'
printf '{bg=#fff}\n' "$(printf '%s' "$SVG" | base64 | tr -d '\n')"
meshfox validate rejects an output-attrs= that isn't in this grammar (bg=white, color=red) and one used without output="image".
Real renderers
The same output="image" recipe with real tools. None of them is bundled with meshfox, so each block first checks that its tool is on PATH and says so if not — pick the ones you use. All of them print an SVG to stdout, which is all output="image" needs; the bg=#fff is there because these tools draw for a white page (see Background).
Graphviz — a graph from dot:
command -v dot >/dev/null || { echo "graphviz is not installed"; exit 1; }
dot -Tsvg <<'EOF'
digraph { rankdir=LR; node [shape=box, style=rounded]; canvas -> block -> svg -> picture }
EOF
Typst — a formula, rendered by Typst's own math engine. Its glyphs are outlines, so no font is needed to view it. Typst's math syntax, not LaTeX's:
command -v typst >/dev/null || { echo "typst is not installed"; exit 1; }
typst compile --format svg - - <<'EOF'
#set page(width: auto, height: auto, margin: 8pt)
$ integral_0^oo e^(-x^2) dif x = sqrt(pi) / 2 $
EOF
Mermaid (npm i -g @mermaid-js/mermaid-cli; needs a headless Chromium) — mmdc writes the SVG where -o says, here stdout:
command -v mmdc >/dev/null || { echo "mermaid-cli is not installed (npm i -g @mermaid-js/mermaid-cli)"; exit 1; }
mmdc -q -i - -o /dev/stdout -e svg <<'EOF'
flowchart LR
A[Block] -->|prints SVG| B(output=image) --> C{{Picture}}
EOF
Mermaid, without a browser — Pele is a small pure-JS Mermaid renderer: no DOM, no Chromium, a few milliseconds per diagram. It is young (October 2026) and draws with its own layout and style, so a diagram can look different from mmdc's. It is an npm package, so the block needs it installed: node-env.canvas.md shows the whole thing — a @node_env block that installs pele into the canvas's own node_modules, and a plain import { render } from "pele" block with output="image" that prints the SVG.
PlantUML (needs Java) — -pipe reads stdin and writes stdout:
command -v plantuml >/dev/null || { echo "plantuml is not installed"; exit 1; }
plantuml -tsvg -pipe <<'EOF'
@startuml
Alice -> Bob: SVG, please
Bob --> Alice: <svg ...>
@enduml
EOF
Vega-Lite (npm i -g vega-lite vega-cli) — a chart: vl2vg compiles the spec, vg2svg draws it:
command -v vl2vg >/dev/null && command -v vg2svg >/dev/null || { echo "vega is not installed (npm i -g vega-lite vega-cli)"; exit 1; }
vl2vg <<'EOF' | vg2svg
{
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
"data": {"values": [{"x": "a", "y": 3}, {"x": "b", "y": 7}, {"x": "c", "y": 5}]},
"mark": "bar",
"encoding": {"x": {"field": "x", "type": "nominal"}, "y": {"field": "y", "type": "quantitative"}}
}
EOF
matplotlib — savefig(..., format="svg") to a buffer, printed as the SVG (compare the PNG-in-data-URL version in pandas-dataframe.canvas.md). One self-contained block: it creates a private virtualenv next to the canvas on the first run (the same .meshfox/<canvas>.venv place meshfox's @python_venv would use), installs matplotlib into it, and runs the script there. All of that setup chatter goes to stderr, so stdout stays the SVG alone:
set -euo pipefail
VENV=".meshfox/svg.canvas.md.venv"
[ -x "$VENV/bin/python" ] || python3 -m venv "$VENV" >&2
"$VENV/bin/python" -c "import matplotlib" 2>/dev/null || "$VENV/bin/python" -m pip install -q --disable-pip-version-check matplotlib >&2
"$VENV/bin/python" - <<'PY'
import io
import matplotlib
matplotlib.use("Agg") # render to a buffer; no display needed
import matplotlib.pyplot as plt
fig, ax = plt.subplots(figsize=(4, 2.2))
ax.bar(["a", "b", "c"], [3, 7, 5], color="#5b8def")
fig.tight_layout()
buf = io.StringIO()
fig.savefig(buf, format="svg")
print(buf.getvalue())
PY