Add math in labels (lookup tier + opt-in Typst tier) - #88
Conversation
|
So this is really 3 things in 1
my thoughts:
A few other issues I found in review. Arrow heads as a path primitive would get dropped through the typst backend path so no arrow heads in network, quiver, or other plots that have arrows. It also drops the Clip primitive (less of a big deal but could make for some surprises)
Overall, I think the unicode shorthand should absolutely be pulled into kuva asap. That's a great feature and it already works everywhere including text plots based on how it's implemented. I don't think the typst related stuff is quite there yet, mostly in issues with typst itself than your PR of the feature. (there was a reason I hadn't done it yet myself, besides time). What do you think about what I have said? Perhaps splitting this PR into 2 parts, the unicode shorthand and the typst math layer and backend, and then we can come back to the typst stuff later. Cheers, |
|
Hey James, Thanks for the feedback! Honestly, yea I think I bit off a bit more than I could chew. It felt incomplete with just the lookup but yea the typst stuff is quite heavy for little usability. I am happy to split this into two PRs with just the look up stuff and then everything else typst. Best, |
|
Agreed on all counts — and the split falls out naturally since the two tiers were designed to be independent. The unicode shorthand is now its own PR: #89. Zero deps, no feature flag, no Cargo.toml changes at all. I'm parking the typst work on this branch and converting it to draft. Two notes for whenever we come back to it:
Cheers! |
## Summary This is the **unicode shorthand half** of #88, split out per the review discussion there — the typst math layer and typst backend stay parked on the other branch for later. Any label (title, axis labels, `TextPlot` markdown bodies) may embed `$...$` math written in LaTeX-ish syntax. Math regions are lowered to inline Unicode by every backend, including the terminal: | Input | Output | |-------|--------| | `$\sigma^2$` | σ² | | `$x_i$` | xᵢ | | `$a \leq b \cdot c$` | a ≤ b · c | | `$\frac{a+b}{c}$` | (a+b)/c | | `$\sqrt{x^2+y^2}$` | √(x²+y²) | | `$\sum_{i=1}^{n} x_i$` | ∑ᵢ₌₁ⁿ xᵢ | **Zero dependencies, no feature flag, always on.** No `Cargo.toml` changes at all. ## Design notes - **Inline only, by design.** Fractions and radicals lower to `a/b` and `√(…)`, never stacked 2-D layout — the output is plain text that flows anywhere a label can go (rotated y-axis titles, terminal character grids, markdown bodies). - **All-or-nothing super/subscripts.** `x^{2n}` → x²ⁿ, but if any character in the group lacks a Unicode form (`q`, most capitals), the whole group falls back to a clean `x^(2q)` — never a half-substituted mix. - **Never leaks source.** The lowering never emits a stray `\` or `$`. Literal dollars are written `\$`; an unclosed `$` is left untouched as ordinary text. - **One module, four call sites.** `src/render/math.rs` holds detection (`contains_math`), segmentation, the command table, and `to_unicode`. The SVG, raster (PNG/PDF), and terminal `Text` arms lower at draw time; markdown `TextSpan`s are lowered once after markup parsing, so `**bold** $\sigma^2$` works in `TextPlot` bodies. ## Supported syntax Greek letters (incl. `\varepsilon`/`\varphi` variants), operators/relations/arrows (`\pm \times \cdot \leq \neq \approx \in \partial \nabla \infty \to \degree` …), `\frac{a}{b}`, `\sqrt{x}` / `\sqrt[3]{x}` (→ ³√x), and `^`/`_` scripts with `{...}` groups or braceless `\command` operands (`x^\alpha` → `x^(α)`). ## Testing - **21 unit tests** in `render::math` — substitution, fallbacks, escaped/unclosed dollars, nested chains (quadratic formula end-to-end), no-leak guarantees. - **`tests/math_lookup.rs`** — per-backend integration: terminal, SVG, markdown `TextPlot` body, escaped-dollar literal. - **7 smoke tests** across plot types (scatter/line/bar) and label slots (title, x/y labels, rotated y-label). - **`scripts/terminal_plots.sh`** — math labels entry for visual terminal inspection. - `cargo ci-test` (99 suites), `cargo ci-clippy`, and the full smoke suite (190 checks) all pass. ## Docs - New reference page *Reference → Math in Labels* with the substitution table, supported-syntax list, CLI usage, and generated example SVGs (`examples/math.rs`, committed under `docs/src/assets/math/`). - Crate-level docs section in `lib.rs`; CHANGELOG entry. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
|
Okay, typst might have legs again for the pdf feature. I just updated kuva to use krilla, and figured out how to handle higher MSRV without bumping kuva (using docs). If we keep typst behind the pdf feature, i'm happy to bring all the features you want. Wanna draft up/alter the PR for this using the latest dev changes? |
Rebased onto the krilla-era dev: the zero-dep lookup tier already merged via Psy-Fer#89 and evolved there, so this commit now adds only the typst work. - feature `math`: links the Typst compiler as a library and typesets any label containing $...$ into an SVG fragment (SVG backend) or pixmap (PNG), embedded at the label position with color injection and per-label ID namespacing. Compile failure falls back to the lookup tier with a one-time warning per distinct label. - PDF: math fragments flow through the krilla pipeline unchanged (SvgBackend -> usvg -> krilla-svg draw_svg) — the xlink:href->href rewrite is exactly what usvg needs; no pdf.rs changes required. - feature `typst` (zero deps, now in `full`): TypstBackend emits a CETZ-based .typ document for external `typst compile`; $...$ regions pass through as native Typst math. - bundles NewCM Math (~0.75 MB gzip) instead of the 15 MB typst-assets; fonts module gains newcm_math() behind the `math` gate.
Per James's proposal on Psy-Fer#88: krilla (the pdf backend since dev@e855bf5) is Typst's own PDF stack, so `pdf` was already the crate's one deliberately heavy, higher-MSRV feature. The typst math tier now rides it instead of being a separate `math` feature: - Cargo.toml: `pdf` pulls typst/typst-svg/typst-render/typst-library; the `math` feature is gone. `full` (and cargo ci-test) now compile the typst tree. - MSRV: no floor change — typst 0.14 needs 1.89, under krilla's 1.92 documented in [package.metadata.msrv] pdf_feature. The pdf-msrv CI job now covers typst implicitly; comments updated. - cfg gates and prose flip feature="math" -> feature="pdf" across backends, fonts, render::math, and the math test suites (which now run under cargo ci-test instead of needing a separate invocation). - smoke_tests.sh: --math-bin machinery removed — the standard cli,full binary now exercises the typst tier directly. - PDF embedding needs no code: math fragments flow through SvgBackend -> usvg -> krilla-svg draw_svg unchanged (verified by tests/math_pdf.rs).
Closes the two gaps James flagged in the June review of Psy-Fer#88: - Primitive::Path (arrowheads in quiver/network, chord ribbons, sankey flows, venn outlines) now emits CETZ merge-path calls. Kuva's path vocabulary is closed (absolute M/L/C/Q/A/Z): quadratics are promoted to cubics, arcs converted endpoint->center per SVG F.6.5 and split into <=90-degree cubic approximations. Opacity maps to transparentize(), dasharrays to Typst dash tuples. - ClipStart/ClipEnd now clip: the element stream is split into chunks at clip boundaries; each chunk renders as a #place'd canvas, and clipped chunks sit inside box(clip: true) at the clip rect with the inner canvas shifted back so coordinates stay page-absolute. Scenes without clips keep the old single-canvas output shape. - Every canvas now opens with an invisible full-page rect, anchoring CETZ's auto-fitted bounding box to the page so absolute positions hold even when no drawing touches the page corners. All emitted documents (scatter, quiver, clip, math) verified to compile with typst 0.14 CLI and visually inspected as PNG.
Closes the TextPlot gap from the June review of Psy-Fer#88 ("can't do plot body text"). Design: inline fragment splicing — kuva keeps its own markdown parsing and word-wrapping; each $...$ in a body compiles to a typst fragment that flows through the line-breaker as one unbreakable word. Surrounding text stays real, selectable text in SVG/PDF output. - TextSpan gains a `math` flag; parse_inline_markup splits spans at $...$ boundaries under `pdf` (without it, lookup-tier lowering is unchanged). - wrap_rich_spans sizes a math word by its typeset width in mean-char units, tracks flush adjacency so punctuation after math ("$x^2$.") stays tight, and never breaks inside a fragment. - Lines with fragments taller than the text line grow their leading by the ascent/descent overshoot, so stacked fractions never collide with neighbouring lines. - SVG backend lays math-bearing lines out manually (bundled-font metrics for text runs, fragment width for math) and embeds fragments at the shared baseline; raster blits pixmaps in its pen loop; terminal always lowers math spans to inline Unicode. - render_label_svg is now memoized per process (each fragment is needed twice: wrapping metrics + draw; axis labels repeat across re-renders). - Fragments advance like words: the 0.3em safety margin baked into the fragment page (FRAGMENT_MARGIN_EM) is subtracted from the inline advance and the embed starts a margin early. Verified visually in PNG (raster), SVG (resvg), and PDF (krilla): wrapping, bold+math mixing, tall-fragment leading, punctuation adjacency all correct.
- docs/reference/math.md: restructured around the two tiers — lookup tables stay as the zero-dep reference; new Typst-tier section covers the pdf feature, compile-failure fallback, the not-LaTeX identifier rule, and TextPlot body splicing (with a new committed asset). - examples/math.rs: regenerated all assets with --features full, so the committed SVGs now show typeset math (the docs say so explicitly). Fixed two typst-hostile bodies that would have silently fallen back to the lookup tier: E = mc^2 -> m c^2, 4ac -> 4 a c. Added a TextPlot body-splice example. - CHANGELOG: body-splicing entry; TypstBackend entry now records full primitive coverage (Path via merge-path, clip via box(clip: true)).
page.frame.baseline() returns size.y (the frame height) when unset, which is always the case for page frames. Using it directly placed the fragment's bottom edge at the surrounding text baseline, so every formula floated visibly above the line it was spliced into. The real baseline lives on the inner paragraph-line frame nested inside the page (has_baseline() is true there). Walk the frame tree, take the topmost line-baseline position, add up y offsets on the way. Assets regenerated: math now sits on the text baseline in wrapped TextPlot bodies and in labels; before/after diff is visible on every committed math SVG.
The previous scene put both circles fully inside the clip region, so test_outputs/typst_clip.typ rendered as two dots with a bottom label — technically a correct primitive-emission check, visually useless as a clip demo. Add a large blue circle centered on the clip's left edge so half is clipped away; keep the small red circle fully inside as a sanity control. Test still asserts on the primitive emission.
Rustfmt-clean these files so PR review isn't cluttered by post-merge fmt drift (James's project-wide rustfmt pass on 2026-05-04 established the current style; new work should land fmt-clean).
edc1845 to
bdddfad
Compare
|
Excuse the delay, I have been quite busy with work. |
|
Haha yea same. Thanks, I'll look at this soon. James |

Description
Adds math in labels — any label (axis titles, plot title,
TextPlotbody) may embed$...$math (LaTeX-ish:$\sigma^2$,$\frac{a}{b}$,$\sqrt{x}$). Two tiers that share$-detection and the symbol table, diverging only on structure:render::math::to_unicode) — always compiled, zero deps. Lowers math to inline Unicode (Greek, operators, super/subscripts,\frac→a/b,\sqrt→√(…)). Baseline for every backend and the only tier the terminal can use. All-or-nothing super/subscript fallback (x^(2k)when no Unicode form exists); never emits a stray\or$.math) — links the Typst compiler as a library (no external binary) and typesets the whole label into an SVG fragment / pixmap embedded in SVG/PNG/PDF. Real 2-D math: stacked fractions, radicals, large operators. Math color follows the label color; compile failure falls back to the lookup tier with a one-time warning.Also lands the
typstmarkup backend (featuretypst): emits a CETZ-based.typdocument for externaltypst compile, sharing the$...$→Typst-math translation with themathtier.mathis deliberately excluded fromfull— the Typst compiler is a ~200-crate tree; ordinary builds andcargo ci-teststay lean. Enable explicitly:--features math,png. Bundles only NewCM Math (~1 MB) + reuses the existing DejaVu Sans, not the 15 MBtypst-assets.Type of change
Checklist
Library
src/render/math.rs—to_unicode(lookup tier),to_typst_math, shared$-detection + symbol table;#[cfg(feature="math")]Typst tier (render_label_svg/render_label_pixmap), whole-label compile, no cachesrc/backend/{svg,raster,terminal}.rs— math routing (typst tier → embed/blit; else lookup tier → normal text). Terminal is lookup-only.src/backend/svg_math.rs— fragment placement + per-label ID namespacing (avoids duplicate-ID SVG with multiple math labels)src/backend/typst.rs—TypstBackendmarkup emittersrc/fonts.rs— bundled NewCM Math (gzip);fontsmodule gated onmathtooCargo.toml—math/typstfeatures;mathpullsflate2, excluded fromfullTests
render::math— 23 exact-match lookup-tier unit tests (Greek, operators, frac, sqrt, all-or-nothing super/sub, nested chains, braceless command, quadratic-formula full chain)tests/math_lookup.rs— lookup tier through SVG + terminal + markdowntests/math_smoke.rs/math_png.rs/math_pdf.rs— Typst-tier SVG embed (unique IDs, color injection), PNG composite incl. rotated y-labels, PDF via usvgtests/typst_basic.rs— markup backend incl. escaping regressioncargo test --features cli,full— all suites pass (lookup-tier path)CLI
--x-label/--y-label/--title; no man-page changescripts/smoke_tests.sh— 7 lookup-tier checks (scatter/line/bar) + 3 Typst-tier checks (gated on--math-bin)scripts/terminal_plots.sh— math-labels entryDocumentation
docs/src/reference/math.md— two tiers, supported set, Typst≠LaTeX, embedded SVG examplesexamples/math.rs→docs/src/assets/math/*.svg(8 committed assets);scripts/gen_docs.shwired (--features full,math)docs/src/SUMMARY.md— reference linkHousekeeping
CHANGELOG.md—[Unreleased]→ AddedThe
typstmarkup backend (zero-dep) and themathfeature (Typst-as-library) are bundled here per discussion. Builds + clippy clean acrosscli,full/math,png,pdf/mathalone; doc assets generated against currentdev.