Skip to content

feat(mathjax): make the MathJax script URL configurable (from #24) - #29

Merged
pyramation merged 8 commits into
mainfrom
feat/mathjax-script-url
Aug 16, 2026
Merged

feat(mathjax): make the MathJax script URL configurable (from #24)#29
pyramation merged 8 commits into
mainfrom
feat/mathjax-script-url

Conversation

@pyramation

Copy link
Copy Markdown
Collaborator

What

Makes the MathJax script URL configurable, so self-hosted or intranet MathJax installs (and CSP-friendly setups) can be used instead of the hardcoded jsDelivr CDN URL.

  • mathjaxjs: new DEFAULT_SCRIPT_URL constant; loadMathJax(callback, config) accepts a scriptURL override via a new LoadMathJaxConfig / MathJaxConfig type (deep-partial, so you can pass just { scriptURL }).
  • latex2html5: init(config) and render(tex, resolve, config) both forward the config, so the custom URL works through the primary programmatic API, not just init.
  • latex2react / mathjaxjs-react MathJaxProvider: new scriptURL prop with the fallback chain prop > config > default.
  • Re-exports DEFAULT_SCRIPT_URL from mathjaxjs-react.
  • First test suite for mathjaxjs (jest jsdom) covering default/custom URL, partial-config merging, and that scriptURL doesn't leak into the MathJax config object.

Credits

This builds on PR #24 by yerslan/fmkeke — the original commit (author preserved, Enhance MathJax integration by adding DEFAULT_SCRIPT_URL and allowing custom script URLs...) is cherry-picked onto current main as the base of this PR. A second commit carries the review fixes that were needed to make the feature actually usable:

  • Typing: typeof DEFAULT_CONFIG & LoadMathJaxConfig made tex/chtml/startup required, so { scriptURL } alone was a type error. Now a deep-partial MathJaxConfig.
  • Merging: a partial config previously replaced DEFAULT_CONFIG (dropping the ams/tags/equation-numbering setup); it now deep-merges into it.
  • Wiring: render() ignored the config (only init() used it); it now forwards it.
  • Dedupe: the React provider hardcoded its own copy of the CDN URL instead of importing the new constant; the mathjaxjs-react provider declared scriptURL in its config interface but never surfaced it as a prop.

Supersedes #24 (kept open; close it if this looks good).

fmkeke and others added 8 commits August 16, 2026 12:05
… custom script URLs in loadMathJax. Update exports in related packages to include new configuration options.

# Conflicts:
#	packages/html5/src/index.ts
The contributor's feature (DEFAULT_SCRIPT_URL + scriptURL override) was
mostly right, but four things stopped it working end to end:

- loadMathJax typed its config as `typeof DEFAULT_CONFIG & LoadMathJaxConfig`,
  making tex/chtml/startup required — passing just { scriptURL } was a type
  error. The config is now a deep-partial (MathJaxConfig) so callers can
  override only what they want.
- A partial config replaced DEFAULT_CONFIG entirely instead of merging into
  it, so { scriptURL } silently dropped the tex setup (ams, tags, equation
  numbering). loadMathJax now deep-merges the override into DEFAULT_CONFIG.
- render() ignored the config, so the custom URL only worked through init();
  render() now forwards it and init() passes it through.
- The React provider kept its own copy of the CDN URL instead of the new
  shared constant, and the mathjaxjs-react provider declared scriptURL in its
  config interface but never surfaced it as a prop. Both now use the shared
  constant and the prop (prop > config > default).

Adds the first tests for mathjaxjs (jest jsdom): default URL, custom URL,
partial-config merging, and that scriptURL does not leak into the MathJax
config object.
latex2react carried its own MathJaxProvider alongside mathjaxjs-react's,
each with a hand-rolled copy of the loading logic. They had drifted:
the react one loaded the script itself (no guard against double-loading),
hardcoded its own defaults (including a CDN fontURL that would fetch fonts
from the CDN even when the script itself is self-hosted), and a different
MathJaxConfig shape.

The react package already depended on mathjaxjs-react and re-exported its
provider from the package entry, so the local component was dead code.
Delete it: there is now one provider, routed through mathjaxjs's
loadMathJax (single source of truth for the script URL and config merge),
one default config, and one place to fix bugs.
The default URL used the floating `mathjax@3` tag, so the exact build was
whatever jsDelivr served that day. Pin to 3.2.2, the final v3 release,
so the loader is reproducible and the current config (tex/chtml/startup
options, package list) matches a known-good version. MathJax v4 is the
active line but changes config semantics; moving to it is a separate
migration.
An \rput label is centred on its coordinate by measuring the element and
subtracting half its size. That measurement only means anything once MathJax
has replaced the LaTeX source with the typeset formula, because the two have
very different widths.

The check was read once, synchronously, when the label was created:

    const mathJax = window.MathJax;
    if (mathJax && mathJax.typesetPromise) { ... } else { raw HTML }

window.MathJax is present long before it can typeset anything. The loader
assigns the configuration object to the global and only then injects the CDN
script, so between those two moments the global exists and typesetPromise does
not. The label took the else branch, centred itself on the width of the raw
LaTeX — much wider than the formula — and was never measured again once
MathJax arrived and swapped the content. That is why it sat off to the left on
first paint and corrected itself on reload: the reload served the script from
cache, fast enough to win the race.

Measured on the real homepage with the CDN fetch delayed by 800ms: labels
landed at x = -4 and x = -14, off the left edge of the picture. They now centre
within 4px of their coordinate at both 0ms and 800ms of delay.

The wait is bounded, and an absent MathJax is treated differently from one that
is merely not ready yet: a page with no MathJax at all must show its labels
immediately rather than wait out the timeout.

The loader made the same mistake in the other direction. It treated any value
on window.MathJax as a loaded library, so a page that configured MathJax itself
— the documented way to set it up — never got the script injected and never
loaded MathJax at all. It now checks for the capability, leaves an existing
configuration in place, and does not add a second copy of a script someone else
has already requested.
…ndle

Three things, all found reviewing this branch against CI.

The bundle was stale. It still carried the floating `mathjax@3` URL while the
source had been pinned to 3.2.2, so the drift check failed — which is exactly
what that check is for. Rebuilt.

The merge only went one level deep. The config is nested further than that:
chtml.linebreaks holds both `automatic` and `width`, so passing
`{ chtml: { linebreaks: { width: '80%' } } }` silently dropped
`automatic: true`. That is the same defect the one-level merge was added to
fix, one level further in, and the MathJaxConfig type promises callers may
override any subset — so the merge is recursive now and the type is honest.
Arrays still replace rather than concatenate: tex.packages and tex.inlineMath
are whole values, and appending would keep a default the caller meant to drop.

The tests covered the merge only for a config that overrides nothing. The
regression that actually bites is a caller overriding one tex key and losing
`tags: 'ams'` — equation numbering and \label/\ref quietly stopping — so that
is pinned now, along with the nested-sibling case, array replacement, and that
DEFAULT_CONFIG is not mutated. Also a relative scriptURL, which is the common
self-hosted form and the one the absolute-URL assertions could not express.

The comment on DEFAULT_SCRIPT_URL is translated to match the rest of the
codebase, and records why the constant is worth having: MathJax 4 moved its
bundles out of `es5/`, so a major upgrade changes the path shape and not only
the version, and this is the single place that has to change.
The label-centring race and the configurable script URL touch the same
function, and the upgrade to MathJax 4 needs both: without the race fix, 21 of
25 rput labels on the homepage land off-centre under v4 exactly as they did
under v3, which would have read as a v4 regression when it is not one.

The conflict was in how the config object is built. Both sides were right
about different things — one deep-merges the caller's config into the
defaults, the other folds in a configuration the page had already put on the
global — so the resolution applies all three sources, weakest first: our
defaults, then the page's pre-configuration, then this caller's config.
MathJax 4.1.3 is current. The upgrade is one line here because the script URL
became a constant in this branch — which is the argument for that change made
concrete: the path shape moved as well as the version, since v4 dropped the
`es5/` directory, so a hardcoded URL would have needed editing in the loader
rather than swapping a value.

The configuration we ship needed no migration. Verified in a browser against
both pages of latex2js.com, not by reading release notes:

  /index.html           33 formulas, 25 labels, 0 off-centre, 0 errors
  /examples/index.html  50 formulas, 49 labels, 0 off-centre, 0 errors

with no unrendered LaTeX left in the text of either. The `tex` package list,
`tags: 'ams'` numbering and the `chtml` output options all carry over.

Worth recording for anyone bisecting later: under v4 without the label-centring
fix that this branch merges, 21 of those 25 homepage labels sit off-centre.
That is the pre-existing race, not a v4 regression — v4 simply loads on a
different schedule and loses the same race. Both changes are needed together,
which is why they travel on one branch.
@pyramation
pyramation merged commit eb0dbdc into main Aug 16, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants