@mdit/plugin-mathjax
Plugin to render math expressions with MathJax.
Note
This plugin is based on @mdit/plugin-tex.
Usage No CDN Scripts
import { getMathjax, mathjax } from "@mdit/plugin-mathjax";
import MarkdownIt from "markdown-it";
const md = new MarkdownIt().use(mathjax, { output: "svg" });
const html = md.render("$e^{i\\pi}+1=0$"); // Fully synchronous
const { outputStyle, reset, clearStyle, dispose, warmup } = getMathjax(md);
const css = outputStyle(); // Fully synchronousmathjax works like any other markdown-it plugin: its second argument is the options object, and md.render() returns the rendered HTML immediately. MathJax's official asynchronous flow runs in a worker thread under the hood while the main thread blocks for the result, so await createMathjaxInstance() is no longer needed.
getMathjax(md) returns the MathJax methods of the markdown-it instance and throws if the plugin has not been installed on it. It is a helper instead of a md.mathjax property so that the global MarkdownIt type is not polluted with an extra optional member for every user.
Tips
The synchronous main entry requires Node.js because it relies on worker_threads. In browsers or runtimes without worker threads, use @mdit/plugin-mathjax/async instead.
Browser / Advanced Usage
import { createMathjaxInstance, mathjax } from "@mdit/plugin-mathjax/async";
import MarkdownIt from "markdown-it";
const mathjaxInstance = await createMathjaxInstance({ output: "svg" });
const md = new MarkdownIt().use(mathjax, mathjaxInstance);
const html = md.render("$e^{i\\pi}+1=0$");
const css = await mathjaxInstance.outputStyle();./async keeps the v1 instance API: createMathjaxInstance(options) resolves to a MathJax instance and mathjax(md, instance) installs it on a markdown-it instance. Use it in browsers, in runtimes without worker_threads, or when you need function options that cannot be sent to the synchronous worker. Fonts and TeX packages are still loaded lazily through MathJax's official async API.
Breaking Changes
v2 is a breaking major release
@mdit/plugin-mathjax v2 changes the main entry from an instance-based plugin to a regular synchronous markdown-it plugin. Review the following changes before upgrading:
mathjaxnow takes options as its second argument:mathjax(md, options)instead ofmathjax(md, instance), andmd.render()is synchronous again.createMathjaxInstanceis only available from@mdit/plugin-mathjax/asyncnow. The main entry no longer re-exports it.@mdit/plugin-mathjax/synchas been removed. The main entry itself is synchronous on Node.js.- The synchronous v2 API needs no instance: use
getMathjax(md)to accessoutputStyle,reset,clearStyleanddispose. - The synchronous main entry requires Node.js (
worker_threads). In browsers or runtimes without worker threads, use./async. - Options must be structured-cloneable. Function options are rejected with a clear error, see Options.
- The HTML-related TeX packages (
htmlandtexhtml) are no longer enabled by default. They are added to the default list when markdown-it'shtmloption is enabled, or can be enabled explicitly withtex.packages, see Security. - The font-extension TeX packages (
bbm,bboldxanddsfont) are no longer enabled by default: the bundled NewCM font does not ship their extension data.\mathbbnow uses the standard double-struck variant; commands that need those extensions (\mathbbm,\mathds, ...) are unavailable unless you enable the packages explicitly withtex.packages. See Font and TeX Package Limitations.
Migration
Before (v1):
import { createMathjaxInstance, mathjax } from "@mdit/plugin-mathjax";
import MarkdownIt from "markdown-it";
const mathjaxInstance = await createMathjaxInstance(options);
const md = new MarkdownIt().use(mathjax, mathjaxInstance);
const html = md.render("$E=mc^2$");
const style = await mathjaxInstance.outputStyle();After (v2 main entry, fully synchronous):
import { getMathjax, mathjax } from "@mdit/plugin-mathjax";
import MarkdownIt from "markdown-it";
const md = new MarkdownIt().use(mathjax, options);
const html = md.render("$E=mc^2$");
const style = getMathjax(md).outputStyle();The @mdit/plugin-mathjax-slim package follows the same change: its main entry uses the new synchronous API (with @mathjax/src and @mathjax/mathjax-newcm-font as optional peer dependencies), ./async keeps the instance API, and ./sync is removed. The VuePress @vuepress/plugin-markdown-math package is patched in this repository to use the v2 API until it is released upstream. Its usage is otherwise the same as @mdit/plugin-mathjax.
Syntax
The syntax depends on the delimiters option:
- Default (
"dollars"): Use$tex expression$inline, and$$tex expression$$for block. - LaTeX style (
"brackets"): Use\(tex expression\)inline, and\[tex expression\]for block. - Both (
"all"): Both dollar and bracket syntaxes are supported.
Escaping
You can use
\to escape$\(and\[:Euler’s identity \$e^{i\pi}+1=0$will be
Euler’s identity $e^{i\pi}+1=0$
Options
mathjax(md, options) accepts the following options:
"chtml" | "svg""svg"Output syntax.
"brackets" | "dollars" | "all""dollars"Math delimiter syntax to enable.
"brackets": Use\(...\)for inline math and\[...\]for display math (LaTeX style)."dollars": Use$...$for inline math and$$...$$for display math (common Markdown style)."all": Enable both bracket and dollar syntaxes.
booleanfalseWhether to allow inline math with spaces on ends.
NOT recommended to set this to true, because it will likely break the default usage of $.
booleanfalseWhether parsed fence block with math language to display mode math.
booleantrueWhether to include assistive MathML for screen readers. When disabled, math is still rendered but the hidden <mjx-assistive-mml> output is omitted.
MathJaxTexInputOptionsTeX input options. The default package list excludes HTML-related and font-extension packages, see Font and TeX Package Limitations.
MathjaxCommonHTMLOutputOptionsCommon HTML output options.
MathjaxSVGOutputOptionsSVG output options.
TeXTransformerTransformer on output content. Runs on the main thread after the worker returns the rendered HTML.
booleanfalseEnable debug mode.
"batch" | "immediate""batch"How formulas of a markdown document are converted. With "batch", all formulas are collected and converted in one worker round-trip when the outermost render finishes; nested md.render() / md.renderInline() calls return internal placeholders until that flush. With "immediate", every formula is converted as soon as it is encountered, so nested render calls return final HTML immediately at the cost of one worker round-trip per formula.
number30000Timeout of a single synchronous worker call in milliseconds. Only used by the synchronous main entry.
Function options need the async entry
The synchronous main entry sends options to a worker thread, so all options must be structured-cloneable. The following options are rejected with an explicit error:
tex.formatErrorpreFiltersandpostFiltersinchtml/svgfontData,wrapperFactoryandcssStylesinchtml/svg(custom fonts, wrappers and styles)linebreaks.LinebreakVisitorinchtml/svg
Import from @mdit/plugin-mathjax/async when you need any of them. transformer is the only function option supported by the synchronous entry. Any other non-cloneable value, such as a function in tex.macros, is rejected as well.
Security
The html TeX package provides macros such as \href, \class, \style and \cssId, which can create clickable links and inject attributes into the output; texhtml registers raw <tex-html> support (inert unless MathJax's allowTexHTML option is enabled). Enabling them for untrusted TeX input allows \href{javascript:...} to become a clickable script URL.
The default package list does not enable them: with new MarkdownIt() (raw HTML disabled) the HTML-related packages are not loaded. They are only added to the default list when the markdown-it instance enables raw HTML:
// Default: the HTML-related TeX packages are not loaded, `\href` stays a red macro placeholder.
const safeMd = new MarkdownIt().use(mathjax);
// Enabling raw HTML in Markdown also enables the HTML-related TeX packages by default.
const trustedMd = new MarkdownIt({ html: true }).use(mathjax);An explicit tex.packages list is trusted developer configuration and is used as-is, so the HTML-related packages can also be enabled while keeping raw HTML in Markdown disabled:
const md = new MarkdownIt().use(mathjax, {
tex: { packages: ["ams", "html", "texhtml"] },
});Only enable them when the TeX input is trusted: an explicit configuration does not make untrusted Markdown safe, and \href{javascript:...} remains a script URL.
The html option is read when the plugin is installed (md.use(...)); changing it later with md.set() is not observed.
For the async entry, the html option of the markdown-it instance passed to md.use(mathjax, instance) decides the default list when the instance is installed. A MathJax instance owns shared TeX state, so it can only be installed on one markdown-it instance; use a separate createMathjaxInstance() result for each instance, especially when they have different trust levels.
Font and TeX Package Limitations
The plugin bundles MathJax's NewCM font (@mathjax/mathjax-newcm-font). Standard math variants work out of the box: \mathbb renders as double-struck, \mathcal as calligraphic, \mathfrak as fraktur, and so on. Font files are loaded through MathJax's dynamic font loader.
The bundled font does not ship the font-extension data used by the bbm, bboldx and dsfont TeX packages. These packages are therefore not enabled by default: loading them would replace standard macros (e.g. \mathbb) with missing font variants and fall back to normal glyphs with an Invalid variant warning. If you enable them explicitly through tex.packages, commands such as \mathbbm and \mathds still fall back with a warning because the extension data is missing.
The synchronous main entry sends its options to a worker, so a custom fontData cannot be used there; use @mdit/plugin-mathjax/async if you need a custom font with additional extensions.
MathJax Methods
getMathjax(md) returns the MathjaxMethods object of a markdown-it instance. It throws if mathjax has not been installed on that instance.
() => stringReturn the CSS content collected from all rendered formulas as a string, synchronously.
Font files are loaded on demand before the stylesheet is generated, and the style cache is cleared afterwards, so call it once all rendering is done.
() => voidReset the
The plugin calls it after each document so that state does not leak across renders. To keep macros available in every document, define them with the tex.macros option instead.
() => voidClear the style cache and reset both the input and output jax. It does nothing when the output jax has not been initialized yet.
() => voidTerminate the worker-side session and release its MathJax state.
After calling it, the markdown-it instance can no longer render math and getMathjax(md) throws; create a new MarkdownIt instance to render again.
(options?: WarmupOptions) => voidInitialize the worker and the MathJax session synchronously, so the first document does not pay the one-time initialization cost (~100ms). warmup({ preloadFonts: true }) also loads all dynamic font files, which additionally takes ~73ms for SVG and ~35ms for CHTML on the first call. Because it blocks, call it before a batch of renders when predictable timing matters.
import { getMathjax, mathjax } from "@mdit/plugin-mathjax";
import MarkdownIt from "markdown-it";
const md = new MarkdownIt().use(mathjax, { output: "svg" });
// Render first, then collect the CSS for the rendered formulas.
md.render("$e^{i\\pi}+1=0$");
const { outputStyle, reset, clearStyle, dispose, warmup } = getMathjax(md);
const css = outputStyle();
// Reset TeX state (macros / labels); rendering already does this.
reset();
// Clear the style cache and math jax state.
clearStyle();
// Initialize now instead of on the first rendered document.
warmup();
// Release the worker session when the MarkdownIt instance is no longer used.
dispose();Demo
Euler’s identity
Euler’s identity $e^{i\pi}+1=0$ is a beautiful formula in $\mathbb{R}^2$.
$$
\frac {\partial^r} {\partial \omega^r} \left(\frac {y^{\omega}} {\omega}\right)
= \left(\frac {y^{\omega}} {\omega}\right) \left\{(\log y)^r + \sum_{i=1}^r \frac {(-1)^i r \cdots (r-i+1) (\log y)^{r-i}} {\omega^i} \right\}
$$Support List
Cookbook
See the comparison report for the full upstream comparison.