[ Content & formats ]
@sdxc/math
TeX math to MathML Core, with a markdown walk visitor and a component
- Installs with
- @sdxc/markdown@sdxc/result
- Source
- packages/math
TeX math to MathML Core, with a markdown walk visitor and a component.
Browsers render MathML Core natively, so a formula needs no stylesheet, web font or client JavaScript. This package reads a practical subset of TeX into a JSON tree of MathML elements and writes it as markup or as component elements.
Installation
npm add @sdxc/math
Conversions answer a Result from @sdxc/result.
@sdxc/math/markdown walks documents from
@sdxc/markdown, and @sdxc/math/ui renders with
remix/component from remix.
Usage
Convert TeX
import { toMathML } from "@sdxc/math";
import { isFailure } from "@sdxc/result";
let result = toMathML("\\frac{a}{b}", { display: true });
if (isFailure(result)) throw result.error;
result.data;
// <math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics>
// <mfrac><mi>a</mi><mi>b</mi></mfrac>
// <annotation encoding="application/x-tex">\frac{a}{b}</annotation>
// </semantics></math>
Leave display off for a formula inside a sentence. The TeX travels with the formula as an
annotation, which screen readers and copy-paste read back.
Read the tree
import { parseMath } from "@sdxc/math";
let tree = parseMath("x^2");
// success: { type: "element", name: "math", attributes: { xmlns: "…", display: "inline" }, children: [ … ] }
The tree is plain JSON, so it caches and travels in a payload.
Render math in markdown
import { Markdown } from "@sdxc/markdown";
import { toHTML } from "@sdxc/markdown/html";
import { math, renderMath } from "@sdxc/math/markdown";
import { isFailure } from "@sdxc/result";
let fence = "`".repeat(3);
let source = `Euler: $\`e^{i\\pi} + 1 = 0\`$\n\n${fence}math\n\\sum_{i=1}^n i\n${fence}\n`;
let parsed = Markdown.parse(source);
if (isFailure(parsed)) throw parsed.error;
let walked = Markdown.walk(parsed.data.document, math);
if (isFailure(walked)) throw walked.error;
let html = toHTML(walked.data, { tags: { math: renderMath } });
The visitor reads the two forms GitHub renders: a fenced block whose language is math (display),
and inline code wrapped in dollar signs, $`x^2`$ (inline). Each becomes a math tag node with
{ tex, display } attributes.
Render with Remix
import { MathFormula } from "@sdxc/math/ui";
<p>
The area is <MathFormula tex="\pi r^2" />.
</p>;
<MathFormula tex="\int_0^1 x^2 \, dx = \frac{1}{3}" display />;
Render it on the server: the browser's HTML parser places the elements in the MathML namespace.
API
toMathML(tex: string, options?: MathOptions): Result<string, MathError>
Converts TeX to a MathML string with every value escaped and every element explicitly closed, ready for HTML or XHTML.
parseMath(tex: string, options?: MathOptions): Result<MathElement, MathError>
Reads TeX into the tree toMathML serializes: a math element holding a semantics element with
the formula and its TeX annotation.
MathError
The first construct the parser refused. reason says what went wrong; index (0-based), line
and column (1-based) locate it in the TeX, and message ends with line:column.
parseMath("a + \\foo"); // failure: "Unknown command \foo at 1:5"
@sdxc/math/markdown
math
The Markdown.walk visitor with default options. A formula that does not convert fails the walk,
with the MathError as the failure's cause. The walk stays synchronous and returns a Result.
createMathVisitor(options?: MathVisitorOptions)
Builds the visitor. invalid: "fail" (the default) fails the walk on a formula that does not
convert; invalid: "keep" leaves it as the code block or inline code the author wrote.
renderMath
A tag renderer for toHTML's tags option. A tag whose TeX does not convert renders as the
escaped TeX inside <code>.
@sdxc/math/ui
MathFormula
A remix/component component taking tex and display. It builds the MathML as elements, and
renders the TeX in a <code> when it does not convert.
MathTag
MathFormula shaped for toRemix's components option, which passes a walked math tag's
attributes plus children.
Types
MathOptions
display: a block formula. Setsdisplay="block"and places the limits of\sum,\prod,\limand similar operators under and over them. Defaults tofalse.
MathVisitorOptions
invalid:"fail"or"keep", as described undercreateMathVisitor.
MathNode, MathElement, MathText
A node is an element, { type: "element", name, attributes, children } with string attribute
values, or text, { type: "text", value } holding the unescaped characters.
MathFormula.Props
{ tex: string; display?: boolean }, the props MathFormula accepts.
Supported TeX
Numbers, letters (each an identifier), operators
+ - = < > , ; : ! ? / * . ', grouping{}Scripts
^and_, alone or together, with TeX's one-token rule:x^23raises only the 2\frac,\dfrac,\tfrac,\binom,\sqrt{x},\sqrt[n]{x}Greek letters
\alpha…\omega, the\varforms, and uppercase\Gamma…\OmegaOperators, relations and arrows such as
\cdot \times \pm \leq \neq \approx \in,\subseteq \cup \to \Rightarrow \iff \forall \exists \ldots \cdots, and\infty \partial \nablaBig operators
\sum \prod \coprod \bigcup \bigcap \bigoplus \bigotimes \bigvee \bigwedgewith limits in display mode, integrals\int \iint \iiint \ointwith limits beside them, and\limits/\nolimitsFunctions
\sin \cos \tan \log \ln \exp \det \gcd \max \min \sup \inf \limand more, plus\operatorname{…}\left … \rightwith( ) [ ] | / .and\{ \} \langle \rangle \lvert \rvert \| \lfloor \rfloor \lceil \rceil\text{…},\textrm{…},\mbox{…},\mathrm,\mathbf,\mathit,\mathbbAccents
\hat \bar \vec \tilde \dot \ddotSpacing
\, \: \> \; \! \quad \qquad \enspace,~and\Environments
matrix,pmatrix,bmatrix,Bmatrix,vmatrix,Vmatrixandcases, with&between cells and\\between rows
Any other command is a MathError naming it.
Pattern: Validate TeX From A Form
Check a formula before saving it, and point the author at the character that broke it.
import { toMathML } from "@sdxc/math";
import { isFailure } from "@sdxc/result";
function validateFormula(tex: string) {
let result = toMathML(tex, { display: true });
if (isFailure(result)) {
let { reason, line, column } = result.error;
return { ok: false as const, message: `${reason} (line ${line}, column ${column})` };
}
return { ok: true as const, tex, preview: result.data };
}
validateFormula("\\frac{1}{\\foo}");
// { ok: false, message: "Unknown command \foo (line 1, column 10)" }
Pattern: Fail A Build On A Broken Formula
Walk with math so a formula that does not convert stops the build, and report both where it sits
in the document and where the TeX broke.
import { Markdown } from "@sdxc/markdown";
import { toHTML } from "@sdxc/markdown/html";
import { MathError } from "@sdxc/math";
import { math, renderMath } from "@sdxc/math/markdown";
import { isFailure } from "@sdxc/result";
function build(file: string, source: string): string {
let parsed = Markdown.parse(source);
if (isFailure(parsed)) throw parsed.error;
let walked = Markdown.walk(parsed.data.document, math);
if (isFailure(walked)) {
let { cause, position } = walked.error;
if (cause instanceof MathError) {
throw new Error(`${file}:${position?.start.line}: ${cause.message}`, { cause });
}
throw walked.error;
}
return toHTML(walked.data, { tags: { math: renderMath } });
}
Pattern: Keep Building When A Formula Breaks
A preview pane renders what it can while an author types. Walk with invalid: "keep" so a
half-written formula stays code instead of failing the page.
import { Markdown } from "@sdxc/markdown";
import { toHTML } from "@sdxc/markdown/html";
import { createMathVisitor, renderMath } from "@sdxc/math/markdown";
import { isFailure } from "@sdxc/result";
let preview = createMathVisitor({ invalid: "keep" });
function render(source: string): string {
let parsed = Markdown.parse(source);
if (isFailure(parsed)) return "";
let walked = Markdown.walk(parsed.data.document, preview);
if (isFailure(walked)) return "";
return toHTML(walked.data, { tags: { math: renderMath } });
}
Pattern: Render A Markdown Article With Remix
Draw walked math tags with MathTag when the page renders markdown through toRemix.
import type { Handle } from "remix/component";
import { Markdown } from "@sdxc/markdown";
import { toRemix } from "@sdxc/markdown/remix";
import { math } from "@sdxc/math/markdown";
import { MathTag } from "@sdxc/math/ui";
import { isFailure } from "@sdxc/result";
function Article(handle: Handle<{ source: string }>) {
return () => {
let parsed = Markdown.parse(handle.props.source);
if (isFailure(parsed)) return <p>Could not read this article.</p>;
let walked = Markdown.walk(parsed.data.document, math);
if (isFailure(walked)) return <p>{walked.error.message}</p>;
return <article>{toRemix(walked.data, { components: { math: MathTag } })}</article>;
};
}