Universe

Author and render PreFigure mathematical diagrams from inside a Typst document. You write PreFigure XML or build a prefigure diagram with native Typst functions. Then PreFigure, running as a WASM plugin, renders the geometry; Typst renders the text and the math.

Three PreFigure diagrams rendered from Typst: the eighth roots of unity on the unit circle, a slope field with solution curves, and a family of level curves — every label and axis number typeset by Typst

If you have an existing diagram saved, you can write:

#import "@preview/prefigure:0.1.0": prefigure

#prefigure(read("diagram.xml"))

The contents of every <label> and <m> is extracted and rendered by Typst; labels and equations match the surrounding font/style.

Authoring inline, without XML

A Typst user might not want to author raw XML. The package re-exportsa tags submodule (via xmlit) with a constructor for every PreFigure element, so a diagram can be written in native Typst syntax — including $...$ for math.

#import "@preview/prefigure:0.1.0": prefigure, tags

#let doc = {
  import tags: *
  diagram(dimensions: (260, 260), {
    show: coordinates.with(bbox: (-4, -4, 4, 4))

    grid-axes(xlabel: "x", ylabel: "y")
    graph(function: "f(x)=0.4*x^2 - 2")
    point(
      p: "(1,f(1))",
      alignment: "southeast",
      $(1, #(0.4 * 1 * 1 - 2))$,
    )
    label(
      p: "(-3, f(-3))",
      alignment: "center",
      clear-background: true,
    )[the *curve* $y = 0.4 x^2 - 2$]
  })
}

#prefigure(doc, width: 8cm)

A parabola with Typst-authored labels: 'the curve y = 0.4x² − 2', 'slope dy/dx', the point (0,−2), and x/y axis labels — all Typst math in the document font

It is good practice to do a spat import (#import tags: *) inside a scoped environemnt (e.g., inside #{...} or #[...]) as some PreFigure tag names conflict with global Typst function names.

When authoring PreFigure XML with Typst functions, boolean values of true/false are automatically converted to strings "yes"/"no". Additionally, arrays are serialized as strings with square brackets. So bbox: (1,2,3,4) becomes bbox: "[1,2,3,4]".

Debugging your PreFigure code

If you ever need to inspect the XML generated, you can use the xml-to-string function to show the xml source directly in your document. You can then copy-and-paste it into the PreFigure Playground for interactive debugging.

#import "@preview/prefigure:0.1.0": tags, xml-to-string

#let doc = {
  import tags: *
  diagram(dimensions: (260, 260), {
    show: coordinates.with(bbox: (-4, -4, 4, 4))
    grid-axes(xlabel: "x", ylabel: "y")
  })
}

// Shows `<diagram dimensions="[260,260]"><coordinates bbox="[-4,-4,4,4]"><grid-axes xlabel="x" ylabel="y" /></coordinates></diagram>`
#xml-to-string(doc)

Fonts

Fonts can be set using Typst’s #set mechanism. However due to current limitations you can only set the font outside the figure itself (i.e., no calling #set text(...) inside a PreFigure <lable>).

#import "@preview/prefigure:0.1.0": prefigure, tags, xml-to-string
#set text(font: "Fira Math")
#show math.equation: set text(font: "Fira Math")

#let doc = {
  import tags: *
  diagram(dimensions: (260, 120), {
    show: coordinates.with(bbox: (-4, -4, 4, 4))

    grid-axes(xlabel: "x", ylabel: "y")
    graph(function: "f(x)=0.4*x^2 - 2")
    point(
      p: "(1,f(1))",
      alignment: "southeast",
      $(1, #(0.4 * 1 * 1 - 2))$,
    )
    label(
      p: "(-3, f(-3))",
      alignment: "center",
      clear-background: true,
    )[the *curve* $y = 0.4 x^2 - 2$]
  })
}

#prefigure(doc, width: 8cm)

The same parabola diagram as above, but with every label and equation set in Fira Math instead of the default font

Occassionally, PreFigure falls back to using a <text> node in the generated <svg>. These nodes are assigned generic font names (sans-serif, …). You can pass a fonts: ... argument to map these to concrete families Typst can measure and render:

#prefigure(read("diagram.xml"), fonts: (sans-serif: "Fira Sans"), width: 10cm)

API

prefigure(
  source,              // XML string, bytes, or an xmlit tree (e.g. tags.diagram(…))
  width: auto,         // e.g. 8cm — otherwise the SVG's own user-unit size (96dpi)
  labels: "native",       // "svg" (baked) or "native" (live Typst text); math ⇒ native
  fonts: none,         // (sans-serif: "…", …) overrides for svg mode
  math-items: (:),     // equations from xmlit's extract-math (auto-filled for a tree)
  handlers: …,         // xmlit content handlers for a tree (default: _→<it>, *→<b>)
  validate: false,     // false = skip (default) · true = show errors inline · "panic" = fail
  ..image-args,        // forwarded to Typst's image() (alt, fit, …)
)

Validation against the PreFigure RELAX NG schema is opt-in. Pass validate: true to render the diagram with any schema errors shown in a red callout beneath it (non-fatal, with a located source snippet), or validate: "panic" to abort the compile on invalid input instead.

The tags submodule exports a constructor for every PreFigure element (tags.diagram, tags.grid-axes, tags.riemann-sum, …). It is a module, so you can either keep the tags. prefix or bring the constructors into scope:

#import "@preview/prefigure:0.1.0": prefigure, tags
#import tags: *                                  // now: diagram(…), graph(…), label(…)
// or import just the ones you use:
#import tags: diagram, coordinates, grid-axes, graph, label

How it works

The plugin contains a WASM compiled version of the PreFigure compiler. When run on an XML file, all <m> tags are extracted and rendered with the mitex plugin. The rendering works in two passes: first the plugin is called with the XML source and returned is a list of items that PreFigure needs to know the layout size of. Typst renders and computes the size of those itmes and passes the metrics back to the PreFigure compiler, which assembles an SVG. The SVG is directly embedded in the Typst document and all pre-rendered labels and math are placed over the SVG where needed.

Building the plugin

The built wasm (src/prefig_typst_plugin.wasm) is checked in, so the package works without a Rust toolchain. To rebuild it you need the wasm32-unknown-unknown target:

cd wasm-interface
./build.sh              # default (~1.7 MiB) — Typst does the math
./build.sh --with-math  # embed RaTeX to bake math into the SVG (~5.4 MiB)

The default build carries no math engine because Typst renders the math; use --with-math only if you need math baked into a self-contained SVG with no live overlay.

Testing

TYPST=/path/to/typst tests/run.sh

This runs the native protocol tests (cargo test, no Typst needed) and, if a typst binary is found, compiles every fixture and asserts the render invariants. Verified against Typst 0.15.1.