feat(rehype-mathjax)!: update mathjax from v3 to v4#119
Open
nachogarcia wants to merge 3 commits into
Open
Conversation
BREAKING CHANGE: mathjax-full replaced with @mathjax/src. Output HTML/SVG/CSS differs due to new font (New Computer Modern), new attributes (overflow, data-latex), and expanded styles (dark mode, line breaks). Undefined TeX commands now render as red text instead of throwing exceptions.
|
Hi! It seems you removed the template which we require. Here are our templates (pick the one you want to use and click *raw* to see its source): I won’t send you any further notifications about this, but I’ll keep on updating this comment, and hide it when done! Thanks, |
|
lol so now we have 3 separate PRs doing this... still no response from maintainers. paging @wooorm |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Initial checklist
Description of changes
Update MathJax from v3 to v4
Closes #114
Summary
Upgrades
rehype-mathjaxfrommathjax-full@^3.0.0(MathJax v3, resolved to 3.2.1) to@mathjax/src@^4.0.0(MathJax v4). This is a major dependency upgrade reflecting MathJax's package rename, new font system, improved error handling, and a fundamentally different font loading architecture that required a custom preloading strategy for bundler andrunSync()compatibility.Motivation
MathJax v4 was released with significant improvements:
data-latexattributes on SVG elements)action,bbm,bbox,begingroup,dsfont,texhtml,unitsBreaking changes
This is a semver-major change for
rehype-mathjax:fontURLchanged: Must now point to@mathjax/mathjax-newcm-font/chtml/woff2instead ofmathjax@3/es5/output/chtml/fonts/woff-v2.\foo) now render as red text inline instead of throwing. The catch-block fallback still works for actual renderer exceptions.Design decisions
Why static font imports instead of dynamic loaders
MathJax v4 fundamentally changed its font architecture. In v3, all font data was bundled into the output constructors. In v4, font data is split into 40 "dynamic" files per output format (SVG/CHTML) that are loaded on-demand at runtime via
mathjax.asyncLoad().MathJax v4 provides two built-in loaders for this:
Sync Node.js loader (
require.mjs+asyncLoad/node.js): UsescreateRequirefrom themodulebuiltin to synchronouslyrequire()font files. This works in plain Node.js but crashes in bundlers (webpack, Vite, Next.js, esbuild, Turbopack) withcreateRequire is not a functionbecause bundlers don't provide themodulebuiltin.Async ESM loader (
asyncLoad/esm.js): Uses dynamicimport()to load font files on demand. Whendocument.convert()encounters an unloaded glyph, MathJax's retry mechanism throws a synchronous Error with a.retrypromise, caught byhandleRetriesFor()which awaits the font load and retries. This has two problems:runSync(): The transform must beasyncto usehandleRetriesFor(). Libraries likereact-markdownuse unified'srunSync()internally, which throws if any transform returns a Promise.import()call receives a runtime-computed string (built inside MathJax'sdynamicFileName():prefix + '/' + file + '.js'). Bundlers cannot statically analyze this — webpack emits "Critical dependency: the request of a dependency is an expression" and Vite/Rollup silently fail to resolve the paths.Our approach: Statically import all font data files from
@mathjax/mathjax-newcm-fontat build time. Each file callsdynamicSetup()on the font class as a side effect, registering glyph data at import time. We setmathjax.asyncLoadto a no-op since nothing needs runtime loading. The transform stays fully synchronous.Font imports are split per output format and trimmed to math-essential files only (12 of 40). Extended scripts (Cyrillic, Arabic, Hebrew, Braille, Cherokee, Devanagari, etc.) and extra font variants (sans-serif, monospace, phonetics, extended Latin/Greek) are omitted to keep the SVG bundle close to v3's ~1 MB:
lib/svg.js: 12 SVG font files (~1.1 MB) — accents, arrows, calligraphic, double-struck, fraktur, marrows, math, mshapes, script, shapes, symbols, variantslib/chtml.js: 12 CHTML font files (~113 KB) — same setWhy per-instance
setup(font)inregister()MathJax's
dynamicSetup()(called by the static font imports) registerssetupfunctions on the class-leveldynamicFilesobject. These functions populate a font instance with character data when called withsetup(font).MathJax's
loadDynamicFiles()andloadDynamicFileSync()both have a class-levelif (!dynamic.promise)guard: once a file is marked as loaded for the class, subsequent calls skipsetup()entirely. This means new font instances (created on eachregister()call) never get populated.We work around this by explicitly iterating
dynamicFilesinregister()and callingdf.setup(font)for each entry. Thedf.promise ??= Promise.resolve()ensures MathJax's internal tracking considers the file loaded (preventing any attempted runtime load via the no-opasyncLoad).Bundle size impact
Measured with esbuild (
--bundle --platform=node), comparing the v3 (mathjax-full@3.2.1) and v4 (@mathjax/src@4.1.1) bundles:SVG entry (
rehype-mathjax/svg):CHTML entry (
rehype-mathjax/chtml):Why SVG is +19%: MathJax v4's core engine and TeX extensions are smaller than v3 (-300 KB combined), but the font architecture changed. In v3, all font data was bundled in a single 998 KB blob inside the SVG output class. In v4, the base font was split into a 1,021 KB static class plus 40 on-demand "dynamic" files. We import 12 math-essential dynamic files (~1 MB), which together with the static class totals ~2 MB — roughly double v3's font cost. The core savings partially offset this.
Why CHTML is -24%: Same core savings (-270 KB), but CHTML dynamic files store only numeric metrics (not SVG paths), so the 12 files add just 113 KB — far less than the core reduction.
MathJax v4 provides 40 dynamic font files per format (totaling ~9.9 MB for SVG). We import only the 12 math-essential files, keeping the SVG bundle at 3.1 MB. The omitted 28 files contain extended scripts and font variants:
Included (math-essential): accents, arrows, calligraphic (
\mathcal), double-struck (\mathbb), fraktur (\mathfrak), marrows, math, mshapes, script (\mathscr), shapes, symbols, variants.Characters in omitted files that are encountered at runtime will trigger a MathJax retry error (rendered as a red
<span class="mathjax-error">). Users needing extended scripts can add the imports in a custom plugin.Why
sideEffectsarray instead offalseThe original package.json had
"sideEffects": false. This is incorrect now becauselib/svg.js,lib/chtml.js,lib/tex-packages.js, andlib/create-renderer.jsall have side-effect imports (font data registration, TeX extension registration,mathjax.asyncLoadassignment). While most bundlers handle this correctly for actively-used modules, the explicit array is defensive and documents the intent.TeX package audit
MathJax v4 ships 42 TeX extensions. We include 40, matching v3's
AllPackagesplus new v4 extensions. Two are excluded:autoloadasyncLoad, which requires the async runtime loader — incompatible with our synchronous preloading approach.bboldx-bboldxfont variant not present in the NCM font package (producesInvalid variantwarnings at runtime).New v4 packages not in v3's
AllPackages:action(tooltips),bbm(blackboard bold),bbox(bounding boxes),begingroup(TeX grouping),dsfont(double-struck),texhtml(HTML in TeX),units(unit formatting).Known command overrides (same behavior as v3):
physicsredefines\div,\Re,\Im, and trig/log functions — included to match v3colorv2overrides\colorfrom thecolorpackage — included to match v3amsenhances\fracand\boxedfrombase(standard LaTeX behavior)mathtoolsenhances\shoveleft/\shoverightfromamsChanges
Dependency changes (
package.json)mathjax-full: ^3.0.0@mathjax/src: ^4.0.0@types/mathjax: ^0.0.40@mathjax/mathjax-newcm-font: ^4.0.0"sideEffects": false"sideEffects": ["./lib/svg.js", "./lib/chtml.js", "./lib/tex-packages.js", "./lib/create-renderer.js"]Added
"import/no-unassigned-import": "off"to XO lint config to allow the side-effect imports required by v4.Code organization
lib/tex-packages.js(new)allPackagesarray (shared by SVG and CHTML)lib/svg.jslib/chtml.jslib/create-renderer.jsmathjax.asyncLoadno-op, renderer factory with per-instance font setup loop,fromLiteElementhast converterlib/create-plugin.jsImport path migration
All imports updated from
mathjax-full/js/...to@mathjax/src/js/.... MathJax v4 renamed the npm package but kept the same internal module structure, so the paths after the package name are unchanged.Documentation update (
create-plugin.js)Updated the CHTML
fontURLexample in JSDoc frommathjax@3/es5/output/chtml/fonts/woff-v2to@mathjax/mathjax-newcm-font/chtml/woff2to reflect v4's new font package.Error handling behavior change
MathJax v4's
noundefinedpackage now renders undefined TeX commands as red text inline (e.g.,\arenders as red\ain the SVG) instead of throwing aTypeError. This is a user-visible improvement — malformed LaTeX degrades gracefully instead of crashing.The error handling test was split into two:
"should render undefined commands gracefully (mathjax v4)" — Verifies that
\a{₹}(which threw in v3) now renders withfill="red"and produces nofile.messages."should catch renderer exceptions" — Tests the catch-block safety net using a custom renderer that throws, verifying the
<span class="mathjax-error">fallback still works.New tests
"should render dynamic font characters (e.g. double-struck)" — Verifies
\mathbb{R}renders correctly, testing that the static font preloading works for characters outside the base font range (these are in the dynamic font files, not the static base font)."should work with
processSync" — Verifies the transform works synchronously withprocessSync()/runSync(), whichreact-markdownalways uses internally. This is the key compatibility constraint that prevents using async dynamic loaders.Test fixture regeneration
All 9 SVG/CHTML fixture files were regenerated. The output differences are:
MJX-*-TEX-*→MJX-*-NCM-*<path>dataoverflow="overflow"attribute on<mjx-container>data-latex="..."attributes on SVG<g>elementsMJX-TEX→NCM-N𝛼)MJXTEX-*→NCM-*familiesmjx-break,MJX-ZEROfont)Browser variant fixtures (
small-browser.html,small-browser-delimiters.html) are unchanged since that variant wraps math for client-side MathJax rather than rendering server-side.Files modified
packages/rehype-mathjax/package.json— dependencies, sideEffects, lint configpackages/rehype-mathjax/lib/tex-packages.js(new) — TeX extension imports + allPackagespackages/rehype-mathjax/lib/create-renderer.js— asyncLoad no-op, renderer with font setuppackages/rehype-mathjax/lib/create-plugin.js— fontURL docs updatepackages/rehype-mathjax/lib/svg.js— 40 SVG font imports + plugin entrypackages/rehype-mathjax/lib/chtml.js— 40 CHTML font imports + plugin entrypackages/rehype-mathjax/test/index.js— 3 new tests, 1 split testpackages/rehype-mathjax/test/fixture/*.html— 9 regenerated SVG/CHTML fixturesVerification
rehype-mathjaxtests pass (including 3 new tests)remark-mathtests pass (53/53)processSync()works (verifiesrunSync()/react-markdowncompatibility)\mathbb{R}) render correctly via static preloadingcreateRequire is not a functioncrash in bundled environmentsInvalid variantwarnings at runtimerehype-katexhas 1 pre-existing failure unrelated to this change (Node.js version compatibility withvfilemessage properties)Before usage in production environment


After