Diagrams

Emanote bundles lua-filters/diagram.lua, a render-time Pandoc Lua filter that turns fenced code blocks into inline SVG. The filter is vendored from pandoc-ext/diagram and post-processed so SVG diagrams land directly in the page — no --extract-media pass, no JavaScript renderer, no network at view time.

Enabling the filter

Diagrams render only on notes that opt in. Add the bundled filter to a note’s render-time HTML phase in frontmatter:

---
pandoc:
  filters:
    render:
      html:
        - lua-filters/diagram.lua
---

This page declares the filter itself — every fenced block below renders through the same pipeline.

Org notes use the parallel keyword from the Pandoc Lua Filters guide:

#+PANDOC_FILTERS_RENDER_HTML: lua-filters/diagram.lua

Supported engines

The bundled filter recognises seven engine classes from upstream pandoc-ext/diagram; Emanote’s Nix closure pins the binaries for two of them, and leaves the rest as bring-your-own:

EngineClassBinaryBundled by Emanote?
D2d2d2Yes
CeTZcetztypst (+ pre-resolved @preview/cetz package)Yes
MermaidmermaidmmdcNo — set MERMAID_BIN or install on PATH
GraphvizdotdotNo — set DOT_BIN or install on PATH
PlantUMLplantumlplantumlNo — set PLANTUML_BIN or install on PATH
TikZtikzpdflatex (+ inkscape for SVG)No
AsymptoteasymptoteasyNo — set ASYMPTOTE_BIN or install on PATH

A note that fences a class without a corresponding binary renders a Pandoc Lua filter error banner pointing at the failed pandoc.pipe call, so the missing-binary case is loud, not silent.

d2 demo

D2’s declarative syntax is the shortest path from prose to picture. The thought-meandering exercise Richard describes in “Silly or Sensible” — “you will go off into a side branch … and that will branch off into another side branch … and into another and another … and so on” — drops out as a four-step meander followed by the snap-back:

where I startedside branchanother side branchand another … meandermeandermeander notice & snap back

cetz demo

CeTZ shines on figures whose meaning is geometric, not flow-shaped. The temporal contrast Richard draws in “This moment has no duration”time had a periodicity versus the cutting edge where this moment has no duration — is itself a geometric distinction (discrete tick marks vs. an infinitesimally thin line) that cetz can render directly:

canvas and draw are pre-imported by Emanote’s wrapper, so the author writes the figure directly without typing or remembering the cetz version:

Caching

The upstream filter supports a content-addressed disk cache that skips re-running an engine for unchanged source. Enable it once per document via metadata:

diagram:
  cache: true

Cache files land under $XDG_CACHE_HOME/pandoc-diagram-filter/ (typically ~/.cache/pandoc-diagram-filter/), keyed by sha1(fence-body). The cache is shared across notebooks and naturally git-clean. Set diagram.cache-dir: <path> to override the location.

Note

The cache key is the fence body text only — it does not include the engine binary version or the fence’s code-block attributes. Bumping d2 / typst (or changing a {.d2 layout=elk} attribute) does not invalidate previously-cached renders. Bust the cache manually with rm -rf $XDG_CACHE_HOME/pandoc-diagram-filter/ when an engine upgrade should reflow output.

Without caching, every render of a page re-invokes the engine. For static sites this happens once per build; for the live server it happens on each save of a note that contains diagrams.

Limitations

  • Filter declaration must live on the note itself — site-wide cascade from index.yaml is tracked in #263.
  • Other engines (mermaid, dot, plantuml, tikz, asymptote) work if you install the matching binary, but Emanote’s Nix closure does not pin them. Set the engine’s _BIN environment variable (MERMAID_BIN, DOT_BIN, …) to override the executable path explicitly.
  • Diagrams render only when FORMAT == "html", so they have no effect on parse-time concerns like backlinks, tags, search index, or the note model. The fenced source remains in the document for export targets that don’t run the filter.