Skip to content

Add math in labels: zero-dep lookup tier + opt-in Typst tier#9

Open
gabo515 wants to merge 1 commit into
devfrom
feat/typst-math
Open

Add math in labels: zero-dep lookup tier + opt-in Typst tier#9
gabo515 wants to merge 1 commit into
devfrom
feat/typst-math

Conversation

@gabo515

@gabo515 gabo515 commented Jun 10, 2026

Copy link
Copy Markdown
Owner

Any label may embed $...$ math (LaTeX-ish syntax). Rendering has two tiers that share $-detection and the command→symbol table (src/render/math.rs):

Lookup tier — always compiled, zero dependencies:

  • to_unicode lowers math to inline Unicode: Greek, operators, super/sub (all-or-nothing, falling back to a clean ^(2q) when a glyph is missing), \frac{a}{b}a/b, \sqrt{x}√(…). Never emits a stray \ or $.
  • Baseline for every backend; the only tier the terminal can use. SVG, PNG, PDF, and terminal all route plain $...$ labels through it.

Typst tier — feature math, opt-in:

  • Links the Typst compiler as a library (minimal World: bundled fonts, no package/network access) and typesets the whole label (text + math) into one SVG fragment (SVG/PDF) or pixmap (PNG), embedded into kuva output. Real 2-D math: stacked fractions, radicals, large operators with limits.
  • Whole-label rendering means no fontdue text measurement and no per-segment layout — svg_math just places one fragment; raster blits/rotate-blits one pixmap. Math color follows the label color (injected into the Typst source).
  • On compile failure (e.g. Typst's mcm c) the label degrades to the lookup tier with a one-time warning.
  • Bundles only New Computer Modern Math (~1 MB, gzipped) + reuses the bundled DejaVu Sans for text, instead of the ~15 MB typst-assets. Excluded from full so cargo ci-test and example builds stay lean; enable explicitly, e.g. --features math,png.

Also adds the typst markup backend (TypstBackend): emits a CETZ-based .typ document for external typst compile, sharing the $...$ → Typst-math translation with the math tier.

Tests: exhaustive exact-match units on the lookup tier (render::math); structural tests on the typst tier (SVG embed, PNG composite incl. rotated y-labels, PDF via usvg, color injection). Docs: Reference → Math in Labels.

Description

Type of change

  • New plot type
  • New feature / API addition
  • Bug fix
  • Documentation / assets only
  • Refactor / housekeeping

Checklist

Library (new plot type)

  • src/plot/<name>.rs — struct + builder methods
  • src/plot/mod.rspub mod + re-export
  • src/render/plots.rsPlot enum variant + bounds() / colorbar_info() / set_color()
  • src/render/render.rsrender_<name>(), added to render_multiple() match, skip_axes if pixel-space
  • src/render/layout.rsauto_from_plots() extended if categories needed

Tests

  • New test file in tests/ with ≥ basic render + SVG content + legend tests
  • cargo test --features cli,full — all existing tests still pass

CLI (if applicable)

  • src/bin/kuva/<name>.rs — Args struct (with /// doc comment) + run()
  • src/bin/kuva/main.rs — module, Commands variant, match arm
  • scripts/smoke_tests.sh — at least one invocation
  • tests/cli_basic.rs — SVG output test + content verification test
  • docs/src/cli/index.md — subcommand entry
  • man/kuva.1 — regenerated (./target/debug/kuva man > man/kuva.1)

Documentation

  • examples/<name>.rs — Rust example for doc asset generation
  • scripts/gen_docs.sh — invocations added; bash scripts/gen_docs.sh runs clean
  • docs/src/plots/<name>.md — documentation page with embedded SVGs
  • docs/src/SUMMARY.md — link added
  • docs/src/gallery.md — gallery card added
  • README.md — plot types table updated

Visual inspection

  • Opened test_outputs/ — new plot SVGs look correct
  • Scanned neighbouring plots in test_outputs/ for layout regressions
  • bash scripts/smoke_tests.sh — all existing smoke test outputs still look correct
  • No text clipped, no legend overlap, no spurious axes on pixel-space plots

Housekeeping

  • CHANGELOG.md — entry added under ## [Unreleased]
  • README.md — item marked done in TODO section if applicable

@gabo515

gabo515 commented Jun 10, 2026

Copy link
Copy Markdown
Owner Author

@claude review this jawn plz

@claude

claude Bot commented Jun 10, 2026

Copy link
Copy Markdown

Claude finished @gabo515's task in 3m 33s —— View job


PR Review: Math in Labels (lookup + Typst tiers)

Read the full diff against origin/dev. Overall this is a high-quality, well-architected PR — the two-tier design is clean, the feature gating is careful, and the lookup tier is exhaustively unit-tested. Notes below; most are minor. Couldn't run cargo test/cargo build (sandbox blocks cargo here), so the structural/runtime findings are from reading.

Strengths

  • Tier split is clean. to_unicode (zero-dep, always-on) and the #[cfg(feature="math")] Typst tier share $-detection (contains_math/split_segments) and the symbol table (command_to_unicode), diverging only on structure. Graceful fall-through to the lookup tier on compile failure in all three backends (svg.rs:234, raster.rs:2023, terminal always-lookup).
  • Feature hygiene. math is deliberately excluded from full (Cargo.toml:42-43) with a clear rationale comment; fonts mod and unpremultiply are correctly gated. today() returns a fixed date (typst.rs:763) → deterministic output. Fonts/Library/FontBook cached via OnceLock.
  • Lookup tier is genuinely robust — all-or-nothing super/subscript fallback, the "never emit a stray \ or $" guarantee, and ~25 exact-match unit tests including the quadratic-formula full chain.

Issues

1. [Medium] Duplicate element IDs across embedded Typst SVG fragments.
When a plot has more than one $...$ label, each fragment from extract_inner (render/math.rs:675) carries its own <defs id="glyph"> plus content-hashed <symbol id="g…">. I confirmed this in the committed asset — docs/src/assets/math/mixed.svg has two id="glyph" and shared g<hash> symbol ids:

2 id="glyph"
1 id="gF742789BD91EBE9EAEBCF0F8597DBBA1"
...

Because the symbol ids are content-hashed, identical glyphs collide to identical content, so browsers/usvg render correctly (first def wins, and it's the same path). But it's technically-invalid SVG (duplicate id), which strict validators/sanitizers and some downstream consumers may reject or mangle. Recommend namespacing per fragment — prefix each fragment's ids with a per-label counter and rewrite the matching href="#…" in embed_label.

2. [Medium] Two divergent Typst escapers; the markup backend's is incomplete.

  • render::math::escape_markup (render/math.rs:661) escapes # $ * _ \ < > @ \ " [ ]` ✅
  • backend/typst.rs::write_typst_escaped (typst.rs:480) escapes only \ # [ ] $ @missing _, *, `, <, >.

So a label like a_b, *x*, or rate < 5 rendered through the typst markup backend would be mis-typeset as emphasis/strong/label syntax, while the same label through the math tier escapes correctly. Recommend consolidating onto one shared escaper. Fix this →

3. [Low / perf] No memoization of Typst compiles. render_label_svg/render_label_pixmap run a full typst::compile for every math label (render/math.rs:561,573). Fonts/Library/Book are cached, but a plot with many math tick labels recompiles per-label each render. Consider a process cache keyed by (label, size_bits, color).

4. [Low] Non-rotated raster blit lacks the inward-nudge clamp. blit_pixmap_rotated (raster.rs:979) and SVG embed_label (svg_math.rs:35) both nudge an over-margin label back inside the clip, but the non-rotated blit_pixmap path (raster.rs:2043-2050) just rounds and blits — a wide non-rotated math label near the left/top edge can clip at the canvas boundary. Inconsistent across the three paths.

5. [Low] Color injection drops named CSS colors. typst_fill (render/math.rs:650) handles only Color::Rgb and Color::Css starting with #; anything else → None → black. A label colored with a named color (e.g. "red") renders black in the math tier but correctly-colored via the lookup/plain path. Map the common CSS names (you already have a palette in typst.rs:517).

Nits

  • warn_once grows unbounded & isn't truly "once". render/math.rs:684 keeps a HashSet<String> of every distinct failing label for the process lifetime; the doc/comment says "one-time warning" but it's once per distinct label. Minor.
  • Doc drift: typst.rs module example shows @preview/cetz:0.3.1 (typst.rs:26) but Default pins "0.5.2" (typst.rs:73).
  • extract_inner fragility: uses the first > to find the end of <svg …> (render/math.rs:676). Safe for typst-svg output, but a one-line comment noting the "no > inside attribute values" assumption would help.
  • Edge case in to_unicode: a bare braceless command as a script operand, e.g. $x^\alpha$, has read_script_group grab only \, leaving alpha as literal text → xalpha. $x^{\alpha}$ works. Unusual input; worth a test if you care.

Tests / docs

Coverage is strong: exhaustive lookup-tier exact-match units, structural Typst-tier tests (SVG embed, PNG composite incl. rotated y-labels, PDF via usvg, color injection), plus the Reference → Math doc page. Nothing missing for the scope.

None of these are blockers. #1 and #2 are the two I'd address before merge (invalid-SVG output and the markup-backend escaping gap); the rest are polish.
· branch feat/typst-math

Any label may embed `$...$` math (LaTeX-ish syntax). Rendering has two tiers
that share `$`-detection and the command→symbol table (`src/render/math.rs`):

Lookup tier — always compiled, zero dependencies:
- `to_unicode` lowers math to inline Unicode: Greek, operators, super/sub
  (all-or-nothing, falling back to a clean `^(2q)` when a glyph is missing),
  `\frac{a}{b}`→`a/b`, `\sqrt{x}`→`√(…)`. Never emits a stray `\` or `$`.
- Baseline for every backend; the only tier the terminal can use. SVG, PNG,
  PDF, and terminal all route plain `$...$` labels through it.

Typst tier — feature `math`, opt-in:
- Links the Typst compiler as a library (minimal `World`: bundled fonts, no
  package/network access) and typesets the *whole label* (text + math) into
  one SVG fragment (SVG/PDF) or pixmap (PNG), embedded into kuva output.
  Real 2-D math: stacked fractions, radicals, large operators with limits.
- Whole-label rendering means no fontdue text measurement and no per-segment
  layout — `svg_math` just places one fragment; raster blits/rotate-blits one
  pixmap. Math color follows the label color (injected into the Typst source).
- On compile failure (e.g. Typst's `mc` ≠ `m c`) the label degrades to the
  lookup tier with a one-time warning.
- Bundles only New Computer Modern Math (~1 MB, gzipped) + reuses the bundled
  DejaVu Sans for text, instead of the ~15 MB `typst-assets`. Excluded from
  `full` so `cargo ci-test` and example builds stay lean; enable explicitly,
  e.g. `--features math,png`.

Also adds the `typst` markup backend (`TypstBackend`): emits a CETZ-based
`.typ` document for external `typst compile`, sharing the `$...$` → Typst-math
translation with the `math` tier.

Tests: exhaustive exact-match units on the lookup tier (`render::math`);
structural tests on the typst tier (SVG embed, PNG composite incl. rotated
y-labels, PDF via usvg, color injection). Docs: Reference → Math in Labels.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant