Skip to content
Edytor
Esc
↑↓navigate↵open⌘Jpreview
On this page

Equation

The equation plugin adds Notion's block and inline equations, TeX drawn by KaTeX loaded from a CDN on demand, edited in a field under the equation with a live preview.

createEquationPlugin adds Notion’s equations: a block equation (equation) and an inline equation in text (inlineEquation), each holding its TeX source and drawn by KaTeX. It is not a default plugin: list it.

<script lang="ts">
	import { Edytor, createEquationPlugin } from 'edytor';

	const equations = createEquationPlugin();
</script>

<Edytor plugins={[equations]} />

Your app installs, bundles and imports nothing for KaTeX. The first time an equation is drawn in a browser, the plugin loads KaTeX from jsDelivr: its module (katex.mjs, about 75 KB compressed) and its stylesheet (katex.min.css, added once to the page’s head, which loads KaTeX’s fonts as a formula needs them). Pages without math download none of it. The version is pinned: KATEX_VERSION (the KaTeX edytor is tested with), at KATEX_CDN, both exported. Every view and every equation plugin on a page share one load.

Until KaTeX loads, in a server render, and when it fails to load (offline, a blocked CDN), an equation shows its TeX source; nothing throws, and the console says why.

Where KaTeX comes from

The katex option picks the source:

katex KaTeX comes from
absent jsDelivr: KATEX_CDN, the default
a URL another copy of KaTeX’s dist folder (your CDN, your own static files): it must hold katex.mjs, katex.min.css and the fonts/ the stylesheet names
() => import('katex') your bundle: install katex (an optional peer dependency, >=0.16) and import its stylesheet, katex/dist/katex.min.css, yourself
false nowhere: equations show their TeX source
import { KATEX_VERSION, createEquationPlugin } from 'edytor';

// Your own copy of KaTeX's `dist` folder, the same version.
export const selfHosted = createEquationPlugin({
	katex: `https://static.example.com/katex@${KATEX_VERSION}/dist/`
});

// No equation drawing at all: the TeX source shows.
export const sourceOnly = createEquationPlugin({ katex: false });

A bundled KaTeX loads as a chunk of your app, the first time an equation shows:

<script lang="ts">
	import 'katex/dist/katex.min.css';
	import { Edytor, createEquationPlugin } from 'edytor';

	const equations = createEquationPlugin({ katex: () => import('katex') });
</script>

<Edytor plugins={[equations]} />

Content Security Policy

With the default, a page with a Content Security Policy allows jsDelivr for the module, the stylesheet and the fonts:

script-src 'self' https://cdn.jsdelivr.net;
style-src 'self' https://cdn.jsdelivr.net;
font-src 'self' https://cdn.jsdelivr.net;

With a URL, allow its host instead. A policy that allows no other host (or that requires subresource integrity, which a module import cannot carry) bundles KaTeX with katex: () => import('katex').

Creating equations

  • Block equation: the slash menu’s “Block equation” (group Advanced blocks, keywords math, tex, latex, katex, formula, equation). Like a divider it replaces an empty line and goes after a line that holds text; its editor opens at once.
  • Inline equation: the slash menu’s “Inline equation” (group Inline), or Mod + Shift + E. Over selected text in one block, the text becomes the equation’s source, in one undo step: the text’s removal leads the insertInline operation in one plan, so a hook that refuses either keeps the text; at a caret, an empty equation is inserted and its editor opens.
  • Typed: $$E=mc^2$$ in text becomes an inline equation when you type the last $, the caret after it. One undo step gives the typed text back. Input rules do not run in code blocks.

Editing

A click on an equation in an editable view, or Enter on a selected one, opens its editor under it: the TeX source in a field, and Done. Every keystroke writes the source (data.expression), so the equation itself is the live preview, and the typing is one undo step. TeX KaTeX cannot read shows the source in red, titled “Invalid equation” with KaTeX’s message, which the editor also shows under the field.

Key In the editor
Enter Done: closes the editor
Shift + Enter A new line in a block equation; Done in an inline one
Escape Closes the editor, the source as typed

A press or a focus outside the editor closes it too. After it closes, a block equation stays selected and the caret goes after an inline one. An inline equation closed empty is removed, as in Notion. A readonly view draws equations and opens no editor.

Options

createEquationPlugin(options) takes an EquationPluginOptions:

PropType
katex?KatexLoader | string | false

Where KaTeX comes from, the first time an equation shows in a browser: absent, jsDelivr (`KATEX_CDN`); a URL, another copy of KaTeX's `dist` folder; a loader, your bundle (`() => import('katex')`, its stylesheet imported by your app); `false`, none (the TeX source shows). See [Where KaTeX comes from](#where-katex-comes-from).

TypeKatexLoader | string | false
macros?Record<string, string>

TeX macros every equation reads: `{ '\\RR': '\\mathbb{R}' }`.

TypeRecord<string, string>
panel?Snippet<[EquationEditor]>

The editor's panel, under the edited equation (without it, nothing opens); `{@attach editor.field}` on your source field keeps its focus and keys. See [Menus and handles](/docs/customization/menus#equation-editor).

TypeSnippet<[EquationEditor]>
placeholder?Snippet<[EquationPlaceholderPayload]>

An empty equation's markup, replacing the default (`√x` and the view's placeholder): `{ display, label }`, `display` true for a block equation, `label` the view's `equation.blockPlaceholder` or `inlinePlaceholder`. A click on it opens the editor, as on any equation.

TypeSnippet<[EquationPlaceholderPayload]>
error?Snippet<[EquationErrorPayload]>

The markup of an equation KaTeX cannot read, replacing the default (its source, KaTeX's message in its title): `{ display, expression, error, label }`, `error` KaTeX's message, `label` the view's `equation.invalid`.

TypeSnippet<[EquationErrorPayload]>

KaTeX runs untrusted (trust: false): \href, \url, \includegraphics and the HTML extensions draw nothing that links or loads, so its markup is safe to show for any peer’s TeX. strict warnings are off.

import { createEquationPlugin } from 'edytor';

export const equations = createEquationPlugin({
	macros: { '\\RR': '\\mathbb{R}' }
});

The placeholder and the error are drawn inside the equation, which keeps its click, its selection and its void element: your snippets draw only what shows.

<script lang="ts">
	import {
		Edytor,
		createEquationPlugin,
		type EquationErrorPayload,
		type EquationPlaceholderPayload
	} from 'edytor';

	const equations = createEquationPlugin({ placeholder, error });
</script>

{#snippet placeholder(empty: EquationPlaceholderPayload)}
	<span class="math-empty">∑ {empty.label}</span>
{/snippet}

{#snippet error(failed: EquationErrorPayload)}
	<code class="math-error" title={failed.error}>{failed.expression}</code>
{/snippet}

<Edytor plugins={[equations]} />

The kinds

Kind Where Data Created by
equation a block (div) { expression: string } “Block equation” (block.equation, √x, Advanced blocks)
inlineEquation an inline atom in text { expression: string } “Inline equation” (equation.inline), Mod + Shift + E, $$…$$

The block equation is void and shows no text of its own, like a divider; its role is in defaultSemantics (equationKinds), so the room and headless documents read it alike. The inline equation is an atom: the caret steps over it and a selection takes it whole.

import type { JSONDoc } from 'edytor';

export const value: JSONDoc = {
	children: [
		{
			type: 'paragraph',
			content: [
				{ text: 'Euler: ' },
				{ type: 'inlineEquation', data: { expression: 'e^{i\\pi} + 1 = 0' } }
			]
		},
		{ type: 'equation', data: { expression: '\\int_0^1 x^2\\,dx = \\frac{1}{3}' } }
	]
};

Clipboard

Copying writes each equation as MathML whose application/x-tex annotation is its source, inside KaTeX’s katex class (a block equation’s <math display="block"> in a div[data-edytor-equation]), so other apps paste math, and the plain text is the source. HTML import reads equations back from KaTeX’s markup (.katex, .katex-display) or a bare <math> with a TeX annotation (or an alttext): a display="block" one as a block equation, any other as an inline equation in its line. Without the plugin, math imports nothing.

Styling

Selector Matches
[data-edytor-equation] A block equation’s drawing ([data-empty] when it has no source, [data-editing] while edited)
[data-edytor-inline-equation] An inline equation ([data-empty], [data-editing], [data-selected])
[data-edytor-equation-placeholder] An empty equation’s placeholder (the default markup)
[data-edytor-equation-error] TeX KaTeX cannot read, shown as its source (the default markup)
[data-edytor-equation-source] The source shown while KaTeX loads, or without it
[data-edytor-equation-editor] The editor, in the overlay (data-display="block" or "inline"); --edytor-equation-editor-background

The plugin’s own stylesheet only lets a wide block equation scroll sideways; the rest (the hover and editing shade, the centred drawing, the placeholder’s panel, the source’s monospace font, the error’s red) is the Notion theme’s. KaTeX’s own classes (.katex, .katex-display) style the drawing, from KaTeX’s stylesheet: the plugin adds it to the page’s head with the CDN default or a URL (link[data-edytor-katex]); with your own loader, your app imports it.

Was this page helpful?