# Typst Math — Joplin Plugin Specification **Version:** 0.1 (draft) **Status:** pre-implementation **Target:** Joplin desktop ≥ 3.0, Joplin mobile ≥ 3.1 --- ## 1. Name and identifiers | Field | Value | |---|---| | Display name | **Typst Math** | | Repository | `joplin-plugin-typst-math` | | Manifest `id` | `com.github..typst-math` | | npm package | `joplin-plugin-typst-math` | | Renderer content script id | `typstMathRenderer` | | Editor content script id | `typstMathEditor` | | CSS / class prefix | `typst-math-` | | Settings section | `typstMath` | **Why this name.** Users find plugins by typing into a search box. "Typst" is the thing they own, "Math" is the thing they want — both keywords hit. It also sets the scope honestly: this is not a general Typst renderer, and the name stops people filing issues about `#import` and page layout. It matches the convention of the existing ecosystem (`Math Mode`, `Mermaid`, `Freehand Drawing`). Rejected alternatives: *Typstex* (implies LaTeX compatibility, which is exactly what this does not have), *TypstJoplin* (redundant), *Tyx* (unsearchable), *Typst Renderer* (over-promises; reserve for a future full-document plugin). --- ## 2. Goals and non-goals ### Goals - Render `$...$` and `$$...$$` in the note viewer using the Typst engine instead of KaTeX. - Work on desktop and mobile, fully offline after first run. - Degrade gracefully: notes containing existing LaTeX math must not break. - Be fast enough that a note with 50 equations does not feel slow on second view. ### Non-goals (v1) - ` ```typst ` fenced blocks, full documents, page layout. - Typst package imports (`@preview`), file access, image inclusion. - LaTeX → Typst conversion. - Rich Text (WYSIWYG) editor support beyond "does not corrupt the note". - Editor-side live preview (deferred to v2, see §14). --- ## 3. Platform constraints These are properties of Joplin, not choices. The design in §5 follows from them. 1. **markdown-it renderers are synchronous; Typst compilation is not.** The renderer rule cannot return finished SVG. It must return a placeholder that a post-render script fills in. Joplin's own Mermaid rule is the reference implementation of this pattern. 2. **Joplin already parses `$` delimiters.** The built-in KaTeX rule emits `math_inline` and `math_block` tokens and registers renderer functions for them. This plugin overrides the renderer functions only — it does not touch parsing. Consequence: **the "Math expressions (katex)" Markdown option must stay enabled**, or no tokens are produced. This must be checked at startup and surfaced to the user. 3. **Content script assets.** A markdown-it content script may declare an `assets` key listing JS/CSS to be loaded into the rendered HTML document. This is how the viewer-side runtime gets in. 4. **Two-way messaging.** Rendered HTML can call `webviewApi.postMessage(contentScriptId, message)`; the plugin side responds via `joplin.contentScripts.onMessage`. This is the channel for fetching WASM and font bytes. 5. **Mobile plugins run in an iframe inside a WebView**, and `manifest.json` must declare `"platforms": ["mobile", "desktop"]`. Resource URL schemes that work on desktop (`joplin-content://`) are known not to work identically on mobile — do not depend on them. 6. **The `joplin-editable` wrapper matters.** The stock inline output is: ```html ESCAPED_SOURCE RENDERED_HTML ``` Preserve this structure exactly, or the Rich Text editor will lose the source on round-trip. --- ## 4. Architecture ``` plugin process │ note viewer (webview / iframe) ──────────────────────────────────┼────────────────────────────────────── src/index.ts │ · registers settings │ · registers content scripts │ · onMessage: serve wasm+fonts │◄──── webviewApi.postMessage ────┐ │ │ src/rendererScript.ts ────────────┼──► emits placeholder HTML │ (markdown-it content script; │ + declares assets │ overrides renderer rules only) │ │ │ assets/typstMath.js ───────────┘ │ · scans for .typst-math[data-state=pending] │ · dedupes + queues │ · compiles in a Worker │ · post-processes SVG │ · swaps into DOM │ assets/typstMath.css │ assets/compiler.worker.js (typst.ts + wasm) ``` ### Repository layout ``` joplin-plugin-typst-math/ ├── src/ │ ├── index.ts # plugin entry: settings, registration, message handler │ ├── rendererScript.ts # markdown-it content script (extraScripts) │ ├── editorScript.ts # CM6 content script, v2 (extraScripts) │ └── shared/wrap.ts # Typst source templating, shared by both sides ├── assets/ │ ├── typstMath.js # viewer runtime │ ├── typstMath.css │ ├── compiler.worker.js │ └── fonts/ # or downloaded at first run — see §10 ├── plugin.config.json # extraScripts: ["rendererScript.ts", "editorScript.ts"] ├── manifest.json └── publish/ # .jpl output ``` --- ## 5. Rendering pipeline 1. Joplin's KaTeX rule parses `$...$` → `math_inline` token (or `$$...$$` → `math_block`). 2. The content script's overriding renderer function runs. It: a. calls the **original** KaTeX renderer and keeps the resulting HTML string; b. computes `hash = sha1(source + mode + fontScale)`; c. returns the placeholder markup from §6, containing both the raw source and the KaTeX HTML as a hidden fallback. 3. The viewer runtime (`typstMath.js`) runs after render. It queries all `[data-state="pending"]` nodes, deduplicates by hash, and enqueues unique sources. 4. Each source is wrapped per §7 and sent to the compiler Worker. 5. The Worker returns SVG (or an error string). 6. The runtime post-processes the SVG per §8 (baseline probe) and §9 (theming), caches it by hash, replaces the placeholder content, sets `data-state="ok"`. 7. On compile error: `data-state="error"`. If `fallbackToKatex` is on, reveal the hidden KaTeX HTML and add a subtle marker; otherwise show the error with the source. ### Why the KaTeX fallback is the central design decision Typst math syntax is not LaTeX. `$\sqrt{3x-1}$` is valid LaTeX and invalid Typst; the Typst equivalent is `$sqrt(3x-1)$`. Every existing equation in the user's notes would break under a naive override — which is why the equivalent Obsidian plugin ships this behaviour disabled by default. Because the KaTeX renderer is synchronous and already available at override time, calling it costs almost nothing and yields a perfect fallback. Result: old notes render exactly as before, new Typst-syntax notes render with Typst, and no migration is required. This should be the default behaviour, not an option. --- ## 6. Placeholder HTML contract Inline: ```html {{escapedSource}} ``` Block: same shape with `
`, `data-mode="block"`, `data-joplin-source-open="$$"`, `data-joplin-source-close="$$"`. States: `pending` → `ok` | `error` | `fallback`. The runtime must be idempotent — Joplin re-renders the whole note on every keystroke, so a node may be seen many times; the hash-keyed cache is what makes this cheap. --- ## 7. Typst source wrapping ```typst #set page(width: auto, height: auto, margin: 0pt, fill: none) #set text(size: {{sizePt}}pt, fill: rgb("#000000")) {{userPreamble}} {{probe}}{{delimited}} ``` - **Inline mode:** `delimited = "$" + src.trim() + "$"` — no space after `$`, which is what makes Typst treat it as inline. - **Block mode:** `delimited = "$ " + src.trim() + " $"` — the surrounding spaces are Typst's own display-mode trigger. The `$...$` / `$$...$$` distinction in Markdown maps onto this cleanly. - `sizePt` comes from the computed font size of the container × the `fontScale` setting, so equations track the reader's zoom. - `fill` is fixed black here and rewritten to `currentColor` in post-processing (§9). - `userPreamble` is the user's setting (§11), inserted verbatim. Document that a broken preamble breaks every equation. --- ## 8. Baseline alignment The hardest correctness problem. An SVG dropped inline defaults to sitting on the text baseline by its bottom edge, so anything with a descender (`$sum_(i=1)^n$`, `$x_j$`) floats visibly high. **Probe method (recommended).** Prepend an inline box of known size to the equation: ```typst #box(width: 0.01pt, height: 0.01pt, fill: rgb("#ff00ff")) ``` An inline box sits with its **bottom edge on the text baseline**. In the emitted SVG, locate the element with that fill, read its bottom `y`, then: ``` baselineFromTop = probeBottomY descent = svgHeightPx - baselineFromTop ``` Remove the probe element from the SVG and apply `style="vertical-align: -{{descent}}px"` to the output node. Choose a fill colour that cannot occur in real equations and strip it unconditionally. **Alternative.** If `typst.ts` exposes frame/baseline metadata in its vector output for your version, read it directly and skip the probe. Verify before committing to the probe — the probe is the fallback that definitely works with plain string/DOM inspection. **Acceptance test.** The sentence `Let $x$, $x^2$, $sum_(i=1)^n a_i$ and $frac(1,2)$ be given.` must show all four equations with baselines within 1px of the surrounding text at 100% and 150% zoom. --- ## 9. SVG post-processing 1. **Colour:** replace the fixed `#000000` fill with `currentColor` so the equation follows the theme, including dark mode. Do this on parsed DOM attributes, not by blind string replace, to avoid touching user-specified colours inside the equation. 2. **Size:** convert the SVG's `pt` dimensions to px and set explicit `width`/`height` so layout does not reflow when the SVG loads. 3. **Strip** the baseline probe (§8), any ``/`<desc>`, and the page background rect if present. 4. **Accessibility:** set `role="math"` and `aria-label` to the raw source. 5. **Block mode:** wrap in a centred, horizontally scrollable container — wide equations must not blow out the note width on mobile. --- ## 10. WASM and font delivery **This is the highest-risk area — spike it before anything else.** Loading a WASM module from inside a Joplin plugin's rendered document has defeated people before; asset URLs resolve differently in the viewer than in a normal page, and the asset loader is built primarily around CSS. Plan: - Ship `compiler.worker.js` as a JS asset; have it request the `.wasm` and font bytes via `webviewApi.postMessage` rather than by URL, so the plugin process controls file access. Fall back to a direct fetch of `pluginAssets/...` if messaging proves slower. - **Fonts:** ship exactly two — a math font (New Computer Modern Math) and its text companion. A math-only plugin does not need a general font set; this is the main size advantage over a full Typst renderer. Expect a few MB rather than tens. - **Decision point:** bundle everything in the `.jpl` (simple, large download, no first-run failure mode) versus fetch on first enable into the plugin data directory (smaller, but adds network dependency and an error path). Recommendation: bundle for v1, revisit if the `.jpl` exceeds ~25 MB. - One compiler instance per viewer session, kept warm in the Worker. Instantiation cost is paid once, not per equation. --- ## 11. Settings | Key | Type | Default | Description | |---|---|---|---| | `enabled` | bool | `true` | Master switch. When off, the override is not installed and KaTeX behaves normally. | | `fallbackToKatex` | bool | `true` | On Typst compile error, show the KaTeX rendering instead of an error. | | `showFallbackMarker` | bool | `true` | Add a subtle indicator when a fallback was used, so failures are not silent. | | `preamble` | string (multiline) | `""` | Typst code inserted before every equation. For `#let` shorthands and `#set` rules. | | `fontScale` | number | `1.0` | Multiplier on the inherited font size. | | `blockAlign` | enum | `center` | `center` / `left` for display equations. | | `cacheSize` | number | `500` | Max cached equations per session. | --- ## 12. Compatibility matrix | Surface | Status | Notes | |---|---|---| | Desktop viewer | Must work | Primary target. | | Mobile viewer | Must work | Iframe-in-WebView; test WASM there early, it is a separate risk from desktop. | | Markdown editor | Unaffected in v1 | Live preview is v2. | | Rich Text editor | Must not corrupt | Guaranteed by preserving the `joplin-editable` wrapper; rendering fidelity there is best-effort. | | PDF / HTML export | Known issue | Export renders the same HTML but may capture the document before async compilation finishes. Mitigation: expose a promise the export path can await, or pre-warm the cache by rendering the note in the viewer first. Document the limitation if unsolved. | | Note history / search snippets | Best-effort | Placeholders may appear unrendered; acceptable. | --- ## 13. Performance targets - Cold: first equation visible < 2 s after note open (includes Worker + WASM init). - Warm, cache hit: < 5 ms per equation, no visible flash. - A note with 50 distinct equations: fully rendered < 3 s cold, < 200 ms warm. - Typing in a note with equations must not re-compile anything (hash cache hit on every keystroke re-render). --- ## 14. Milestones - **M0 — Spike (riskiest first).** Get `typst.ts` to compile `$1+1$` to SVG inside the Joplin desktop viewer from a content script asset. Nothing else. If this fails, the project is not viable as specified. - **M1 — Walking skeleton.** Renderer override, placeholder, inline + block, no baseline correction, no cache, desktop only. - **M2 — Correctness.** Baseline probe, `currentColor` theming, size inheritance, hash cache. - **M3 — Resilience.** KaTeX fallback, error UI, preamble setting, katex-disabled detection. - **M4 — Mobile.** `platforms` manifest, WASM in the mobile WebView, block-equation overflow handling. - **M5 — Release.** README with a syntax-difference warning up front, screenshots, `.jpl` in releases, submission to the Joplin plugin repository. - **v2 — Editor preview.** CM6 content script (`editorScript.ts`) rendering equations inline in the Markdown editor; works on both mobile and desktop. --- ## 15. Test checklist - `$x$` inline, `$$x$$` block, adjacent equations on one line. - Escaped `\$` and a bare `$` in prose are not captured. - Currency: `$5 and $10` on one line must not become an equation. - A LaTeX equation (`$\frac{1}{2}$`) falls back to KaTeX with the marker. - Equations inside tables, list items, blockquotes, and footnotes. - Dark theme, light theme, theme switch without note reload. - Zoom 80% / 100% / 150%. - 200-equation note: scroll performance, memory. - Round-trip through the Rich Text editor leaves the Markdown source unchanged. - Broken preamble produces a readable error, not a blank note. --- ## 16. Open questions 1. Does `typst.ts` expose baseline/frame metadata in the JS API for the target version, making §8's probe unnecessary? 2. Exact token names and registration order in the current `@joplin/renderer` KaTeX rule — verify against the installed version before assuming `math_inline` / `math_block`. 3. Is there a CSP restriction on WASM compilation in the mobile WebView iframe? Resolve during M0/M4. 4. Can the export path be made to await pending compilations, or must the cache be pre-warmed? 5. Should `mhchem`-style chemistry input keep working? It rides on the KaTeX plugin; the fallback covers it, but confirm. --- ## 17. Licensing - Plugin: MIT (matches the Joplin plugin ecosystem norm). - Typst and `typst.ts` are Apache-2.0 — attribution required, redistribution of the compiled WASM is permitted. Verify the exact licence text of the version you vendor. - Fonts (New Computer Modern) ship under their own open font licence — include the licence file in `assets/fonts/` and credit it in the README.