Math
The @domternal/extension-math package adds two LaTeX equation nodes - MathInline (inline, inside a paragraph) and MathBlock (a centered display equation) - rendered through a pluggable renderer. The package does not bundle a math engine: you pass a MathRenderer, and a createKatexRenderer adapter for KaTeX ships in the box. Equations are authored with $...$ / $$ input rules, the slash menu, the toolbar, or the text bubble menu, and edited in a shared popover with a live preview.
When to use
Section titled “When to use”Choose Math when you need:
- Inline and display LaTeX in the same document, both stored as plain
latexsource - A rendering engine you control (KaTeX by default, or any engine that turns LaTeX into an HTML string)
- A keyboard-accessible edit experience (open the editor on a selected equation with
Enter) - MathML output for screen readers and RTL-safe rendering out of the box
Skip it if:
- You only need superscripts/subscripts for simple notation (use the Subscript and Superscript marks)
- You cannot ship a math renderer to the client (the nodes still store and round-trip LaTeX, but render the raw source until a renderer is supplied)
Installation
Section titled “Installation”The renderer is a peer dependency. Install the extension alongside katex (the default engine):
pnpm add @domternal/extension-math katexBoth nodes take a renderer. Build one from KaTeX with createKatexRenderer, then pass it to each node via configure. The shared edit popover (MathEditing) is added automatically by either node and uses the same renderer for its preview.
import katex from 'katex';import 'katex/dist/katex.min.css';import { Document, Paragraph, Text } from '@domternal/core';import { MathInline, MathBlock, createKatexRenderer } from '@domternal/extension-math';import { DomternalEditor } from '@domternal/vanilla';import '@domternal/theme';
const renderer = createKatexRenderer(katex);
const dm = new DomternalEditor(document.getElementById('editor')!, { extensions: [ Document, Paragraph, Text, MathInline.configure({ renderer }), MathBlock.configure({ renderer }), ], content: '<p>Euler: <span data-type="math-inline" data-latex="e^{i\\pi}+1=0"></span></p>',});import { Component, signal } from '@angular/core';import katex from 'katex';import 'katex/dist/katex.min.css';import { DomternalEditorComponent, DomternalToolbarComponent } from '@domternal/angular';import { Editor, Document, Paragraph, Text } from '@domternal/core';import { MathInline, MathBlock, createKatexRenderer } from '@domternal/extension-math';
const renderer = createKatexRenderer(katex);
@Component({ selector: 'app-editor', imports: [DomternalEditorComponent, DomternalToolbarComponent], templateUrl: './editor.html',})export class EditorComponent { editor = signal<Editor | null>(null); extensions = [ Document, Paragraph, Text, MathInline.configure({ renderer }), MathBlock.configure({ renderer }), ];}import katex from 'katex';import 'katex/dist/katex.min.css';import { Domternal } from '@domternal/react';import { Document, Paragraph, Text } from '@domternal/core';import { MathInline, MathBlock, createKatexRenderer } from '@domternal/extension-math';
const renderer = createKatexRenderer(katex);
export default function Editor() { return ( <Domternal extensions={[ Document, Paragraph, Text, MathInline.configure({ renderer }), MathBlock.configure({ renderer }), ]} > <Domternal.Toolbar /> <Domternal.Content /> </Domternal> );}<script setup lang="ts">import katex from 'katex';import 'katex/dist/katex.min.css';import { Domternal } from '@domternal/vue';import { Document, Paragraph, Text } from '@domternal/core';import { MathInline, MathBlock, createKatexRenderer } from '@domternal/extension-math';
const renderer = createKatexRenderer(katex);const extensions = [ Document, Paragraph, Text, MathInline.configure({ renderer }), MathBlock.configure({ renderer }),];</script>
<template> <Domternal :extensions="extensions"> <Domternal.Toolbar /> <Domternal.Content /> </Domternal></template>import katex from 'katex';import { Editor, Document, Paragraph, Text } from '@domternal/core';import { MathInline, MathBlock, createKatexRenderer } from '@domternal/extension-math';
const renderer = createKatexRenderer(katex);
const editor = new Editor({ element: document.getElementById('editor')!, extensions: [ Document, Paragraph, Text, MathInline.configure({ renderer }), MathBlock.configure({ renderer }), ],});
// Insert a block equation, then an inline oneeditor.commands.insertMathBlock('\\int_0^1 x^2\\,dx');editor.commands.insertMathInline('x^2');Schema
Section titled “Schema”Both nodes are atoms: their content is the latex attribute, never editable child text. The visual math is produced by the node view from the renderer.
MathInline
Section titled “MathInline”| Property | Value |
|---|---|
| ProseMirror name | mathInline |
| Type | Node |
| Group | inline |
| Inline | Yes |
| Atom | Yes |
| Selectable | Yes |
| Draggable | No |
| HTML tag | <span data-type="math-inline" data-latex="..."> |
MathBlock
Section titled “MathBlock”| Property | Value |
|---|---|
| ProseMirror name | mathBlock |
| Type | Node |
| Group | block |
| Atom | Yes |
| Selectable | Yes |
| Draggable | Yes |
| HTML tag | <div data-type="math-block" data-latex="..."> |
Attributes
Section titled “Attributes”| Attribute | Type | Default | Description |
|---|---|---|---|
latex | string | '' | Raw LaTeX source (without $/$$ delimiters), serialized as data-latex. An empty value renders the “New equation” placeholder |
Options
Section titled “Options”Both nodes share the same options object.
| Option | Type | Default | Description |
|---|---|---|---|
renderer | MathRenderer | null | null | Engine that turns LaTeX into HTML inside the node view and the edit preview. When null, the node shows the raw LaTeX (or the placeholder when empty) and editing still works, but no math is displayed |
HTMLAttributes | Record<string, unknown> | {} | Extra HTML attributes merged onto the rendered element |
The pluggable renderer
Section titled “The pluggable renderer”A MathRenderer is any object with a single method that turns a LaTeX string into an HTML string:
interface MathRenderer { renderToString(latex: string, options: { displayMode: boolean }): string;}displayMode is true for block (display) math and false for inline. Renderers should not throw on invalid input - they should return markup describing the error instead.
KaTeX adapter
Section titled “KaTeX adapter”createKatexRenderer wraps a KaTeX module as a MathRenderer:
import katex from 'katex';import { createKatexRenderer } from '@domternal/extension-math';
const renderer = createKatexRenderer(katex, { throwOnError: false, // default: render an error node instead of throwing output: 'htmlAndMathml', // default: emit a hidden MathML annotation for a11y});| Option | Type | Default | Description |
|---|---|---|---|
throwOnError | boolean | false | Throw on invalid LaTeX instead of rendering an error node |
output | 'html' | 'mathml' | 'htmlAndMathml' | 'htmlAndMathml' | KaTeX output format. 'htmlAndMathml' emits a hidden MathML annotation alongside the visual HTML for screen readers |
Custom renderer
Section titled “Custom renderer”Any engine that returns an HTML string works. For example, wrapping MathJax or a server-rendered image:
import { MathInline } from '@domternal/extension-math';
const mathjaxRenderer = { renderToString(latex, { displayMode }) { return renderWithMathJax(latex, { display: displayMode }); // returns an HTML string },};
MathInline.configure({ renderer: mathjaxRenderer });Commands
Section titled “Commands”insertMathInline
Section titled “insertMathInline”Insert an inline equation at the cursor.
editor.commands.insertMathInline('x^2');
// With text selected and no argument, the selection becomes the LaTeX source// (Notion-style): select "a^2 + b^2" then run insertMathInline().editor.commands.insertMathInline();With no latex and an empty selection, an empty equation is inserted and the edit popover opens immediately. Returns false inside a code block.
insertMathBlock
Section titled “insertMathBlock”Insert a display equation as its own block.
editor.commands.insertMathBlock('\\int_0^1 x^2\\,dx');
// Empty -> inserts a placeholder block and opens the editoreditor.commands.insertMathBlock();On an empty line the block replaces that line (so no empty paragraph is left behind), except inside a list item or other container that requires a leading paragraph, where it is inserted after instead. Returns false inside a code block.
Input rules
Section titled “Input rules”| Input | Result |
|---|---|
$x^2$ | Inline equation with source x^2 |
$$ | Empty block equation, opens the editor |
The inline rule fires when you close a $...$ span; the block rule fires when you type $$ at the start of an empty textblock. Neither fires inside a code block.
Keyboard shortcuts
Section titled “Keyboard shortcuts”| Key | Context | Action |
|---|---|---|
Enter | A math node is selected (NodeSelection) | Open the edit popover (WCAG 2.1.1: a keyboard path to the same action as a click) |
Inside the popover, Enter applies, Shift+Enter inserts a newline, and Escape cancels.
Authoring UI
Section titled “Authoring UI”Toolbar
Section titled “Toolbar”| Item | Command | Icon | Label | Group |
|---|---|---|---|---|
mathInline | insertMathInline | radical | Inline equation | insert |
mathBlock | insertMathBlock | sigma | Equation | insert |
The inline item is also surfaced in the text bubble menu (bubbleMenu: 'text'), so selecting text and clicking it turns the selection into an equation, while the button stays in the main toolbar for the classic editor.
Slash / floating menu
Section titled “Slash / floating menu”Both nodes register items in the Advanced group of the slash command and floating menu, matched by the keywords math, latex, equation, formula (plus inline / block):
| Item | Command | Description |
|---|---|---|
| Inline equation | insertMathInline | Insert an inline LaTeX formula |
| Equation | insertMathBlock | Insert a block LaTeX formula |
Edit popover
Section titled “Edit popover”Editing is owned by MathEditing, a companion extension that both nodes add automatically (deduplicated by name, so adding both MathInline and MathBlock registers a single shared popover). It is a LaTeX <textarea> with a live preview:
- Open by clicking an equation, or selecting it and pressing
Enter. - Live preview renders as you type, using the same renderer.
- Apply on
Enter, on blur, or on a click outside the popover. - Cancel on
Escape. A freshly inserted, still-empty equation is removed on cancel so no dangling “New equation” atom is left behind. - Empty value deletes the node on apply.
The popover re-anchors to a new node if you click a second equation while one is open, committing the in-flight edit first. Positions are confirmed against the live document before mutating, so an edit never writes onto the wrong node after intervening changes.
Accessibility
Section titled “Accessibility”- Keyboard editing. A node-selected equation opens the editor with
Enter, matching the click affordance (WCAG 2.1.1). - MathML. The default KaTeX adapter uses
output: 'htmlAndMathml', so a hidden MathML annotation accompanies the visual math for screen readers. - RTL. LaTeX source is inherently left-to-right; the theme isolates math (
direction: ltr; unicode-bidi: isolate) so placeholder, raw-source, and error states stay readable in RTL documents.
Styling
Section titled “Styling”Wrapper, state, and popover styles ship in @domternal/theme (_math.scss). The math glyphs themselves come from the renderer’s stylesheet (KaTeX’s katex.min.css).
| Class | Description |
|---|---|
.dm-math | Base class on both nodes |
.dm-math-inline | Inline wrapper (display: inline-block) |
.dm-math-block | Block wrapper (centered, full-width) |
.dm-math-empty | Placeholder state (no LaTeX yet) |
.dm-math-error | Render error state (renderer threw) |
.dm-math-popover | The shared edit popover (mounted to document.body) |
.dm-math-popover-input | LaTeX <textarea> |
.dm-math-popover-preview | Live preview area |
Exports
Section titled “Exports”import { MathInline, MathBlock, MathEditing, createKatexRenderer, mathEditPluginKey, MATH_INLINE_NAME, MATH_BLOCK_NAME,} from '@domternal/extension-math';import type { MathOptions, MathInlineOptions, MathBlockOptions, MathRenderer, KatexLike, KatexRendererOptions, MathEditingOptions, MathEditEvent,} from '@domternal/extension-math';| Export | Type | Description |
|---|---|---|
MathInline / MathBlock | Node extensions | The inline and block equation nodes |
MathEditing | Extension | Shared edit popover (auto-added by both nodes) |
createKatexRenderer | function | Build a MathRenderer from a KaTeX module |
MathRenderer | type | The renderer interface (renderToString) |
mathEditPluginKey | PluginKey | Plugin key that carries the “edit this node” signal |
MATH_INLINE_NAME / MATH_BLOCK_NAME | string consts | The ProseMirror node names |
JSON representation
Section titled “JSON representation”A paragraph containing an inline equation, followed by a block equation:
{ "type": "doc", "content": [ { "type": "paragraph", "content": [ { "type": "text", "text": "Euler's identity: " }, { "type": "mathInline", "attrs": { "latex": "e^{i\\pi} + 1 = 0" } } ] }, { "type": "mathBlock", "attrs": { "latex": "\\int_0^1 x^2\\,dx = \\tfrac{1}{3}" } } ]}Source
Section titled “Source”@domternal/extension-math - MathInline.ts, MathBlock.ts, MathEditing.ts
See also
Section titled “See also”- Subscript and Superscript - simple notation without a math engine
- Code Block Lowlight - the same pluggable-engine pattern (you supply
lowlight) - Image - another atom node with a custom node view