T-JPB0-md-engine-math
State: closed/done · Impact: high · Complexity: medium
Math is the one v2 construct with no Lezer nodes: @codemirror/lang-markdown parses tables and
HTML blocks but not $…$ or $$…$$, so the editor cannot even see math. This task adds the
mathSyntax Lezer extension beside obsidianSyntax and the math plugin whose MathWidget
renders KaTeX markup from a cache.
| Location | Role today |
|---|---|
packages/ts/obsidian-editor/src/markdown-extension.ts#obsidianSyntax | Wikilinks, embeds, tags, highlight; no math nodes |
packages/ts/obsidian-editor/src/decorations.ts | No math handling; $x^2$ renders as plain text in live mode |
packages/ts/obsidian-md/src/vue/components/MathBlock.ts | Read-mode KaTeX component; T-RCWI-md-obsidian-md-sync-helpers extracts renderMath from it |
solutions/determined/experiences/notes/nuxt.config.ts | Hosts import katex/dist/katex.min.css; unchanged by this task |
packages/ts/obsidian-editor/test/helpers.ts#makeState | Headless state builder for the parse and plugin tests |
The plugin roster comes from T-4TE2-md-engine-plugin-contract; the math.md corpus fixture
and requestRedraw from T-0GV4-md-engine-line-endings-corpus.
Proposed
Section titled “Proposed”mathSyntax: MarkdownConfig in the markdown-extension module, exported beside obsidianSyntax:
node names InlineMath ($…$, no line break inside), BlockMath ($$…$$, spanning lines) and
MathMark for the delimiters. It is the math plugin’s syntax contribution — the D-7VMX
parser seam — so omit: ['core.math'] removes the dialect with the plugin.
packages/ts/obsidian-editor/src/plugins/math.ts:
- Inactive
InlineMath→ inlineDecoration.replacewidget; inactiveBlockMath→ block widget. Active line → delimiters and TeX source visible,cm-math-sourcemark. MathWidgetfills from a cache keyed bydisplayMode + latex. On a miss it shows the raw TeX, callsrenderMath(latex, displayMode)(async KaTeX from T-RCWI-md-obsidian-md-sync-helpers,throwOnError: false) and issuesrequestRedraw(view)when the markup resolves, so invalid TeX still renders KaTeX’s error markup instead of throwing.- Hosts keep importing
katex/dist/katex.min.css; the plugin ships no CSS beyond--obsmd-*hooks for the widget container.
Approach
Section titled “Approach”-
Write
packages/ts/obsidian-editor/test/syntax/math.test.tsfirst:$x$parses toInlineMathwith twoMathMarkchildren; a$$fence across three lines parses toBlockMath; a$inside inline code or a fenced block produces no math node;$ x$with a leading space is notInlineMath. -
Write
packages/ts/obsidian-editor/test/plugins/math.plugin.test.ts: inactive inline math →replace widget; before
renderMathresolves the widget shows the TeX source; after resolution plusrequestRedrawthe widget DOM contains KaTeX markup; the cursor entering the range restores the source; two rebuilds callrenderMathonce per distinct formula (spy). -
Implement
mathSyntaxin the markdown-extension module. -
Implement the plugin: widget class, cache, redraw-on-resolve; add
mathtoBUILTIN_PLUGINS. -
Export
mathSyntaxfrom the package entry;bun test,tsc --noEmit,moon run obsidian-editor:bench.
Files to touch
Section titled “Files to touch”| Location | Kind | Change |
|---|---|---|
packages/ts/obsidian-editor/src/markdown-extension.ts | modify | mathSyntax with InlineMath, BlockMath, MathMark |
packages/ts/obsidian-editor/src/plugins/math.ts | new | math plugin: MathWidget, cache, redraw on resolve |
packages/ts/obsidian-editor/src/plugins/index.ts | new | Created by T-4TE2-md-engine-plugin-contract; math joins BUILTIN_PLUGINS |
packages/ts/obsidian-editor/src/index.ts | modify | Export mathSyntax |
packages/ts/obsidian-editor/test/syntax/math.test.ts | new | Lezer node tests |
packages/ts/obsidian-editor/test/plugins/math.plugin.test.ts | new | Widget, cache and active-line tests |
Acceptance criteria
Section titled “Acceptance criteria”- AC-1:
bun test packages/ts/obsidian-editorpasses, including the two new suites. - AC-2: the four parse cases in Approach step 1 hold: inline and block nodes exist with
MathMarkdelimiters, and no math node appears inside inline code or a fence. - AC-3: after
renderMathresolves and arequestRedraw, the widget DOM contains an element with akatexclass; before resolution the widget shows the raw TeX. - AC-4: invalid TeX renders (no throw escapes the widget) — asserted with a formula KaTeX
rejects under
throwOnError: true. - AC-5: two decoration rebuilds with an unchanged document call
renderMathonce per distinct formula. - AC-6:
bun test packages/ts/obsidian-editor/test/swap-writer.test.tspasses unchanged overpackages/ts/obsidian-editor/test/fixtures/corpus/math.md. - AC-7:
moon run obsidian-editor:benchstays within 1.5× of the recorded baseline.
Out of scope
Section titled “Out of scope”- Bundling KaTeX CSS; hosts import
katex/dist/katex.min.cssexactly as today. - Read-mode math (
MathBlockis rewritten overrenderMathby T-RCWI-md-obsidian-md-sync-helpers, not here). - A math slash or completion entry (T-N58P-md-engine-slash-and-heading-completion owns completion).
Dependencies
Section titled “Dependencies”- T-RCWI-md-obsidian-md-sync-helpers for
renderMathand thekatexdependency on the editor manifest. - T-4TE2-md-engine-plugin-contract for the plugin contract;
T-0GV4-md-engine-line-endings-corpus for
requestRedraw, the corpus and the bench. - D-7VMX: math enters the grammar as a
MarkdownConfigextension of the one parser, never a second parse.
Discovery context
Section titled “Discovery context”Member of milestone M-931L; engine step E7 of decision D-K3EM-markdown-editor-chrome-and-plugin-environment.
Depends on
Section titled “Depends on”T-RCWI-md-obsidian-md-sync-helpers