RunestoneInteractive / RunestoneInteractive/rs
Assignment Builder loads MathJax twice; second load drops ignoreHtmlClass
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 69
- Forks
- 117
- Avg merge
- 6d 4h
- Merged PRs (30d)
- 24
Description
Summary
The Assignment Builder loads MathJax twice, from two different CDNs, and the second load replaces the Runestone MathJax configuration with the builder's own. The surviving configuration drops ignoreHtmlClass entirely, so tex2jax_ignore / ignore-math stop being honored for the rest of the page's life.
This is not currently causing a user-visible bug, but it is a latent hazard — it silently removes a protection that #1248 now depends on. Filing it so it's on the record rather than rediscovered later.
How it happens
-
components/rsptx/templates/common/static_assets_min.html:26-75setswindow.MathJaxto the Runestone config and loads MathJax fromhttps://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js. This config setsignoreHtmlClass: "tex2jax_ignore|ignore-math"andprocessHtmlClass: "process-math", and itspageReadyresolvesrunestoneMathReady. -
MathJaxWrapper(assignment_builder/src/components/routes/AssignmentBuilder/MathJaxWrapper.tsx:72) renders<MathJaxContext config={mathJaxConfig}>.better-react-mathjax@2.1.0does this unconditionally on first render — it never checks whether MathJax is already present:// node_modules/better-react-mathjax/esm/MathJaxContext/MathJaxContext.js function l(t, o) { n && (window.MathJax = n); // n = the config object passed in var e = document.createElement("script"); e.src = f; // f = DEFAULT_V3_SRC when `src` prop is unset ... document.getElementsByTagName("head")[0].appendChild(e); }with
DEFAULT_V3_SRC = "https://cdnjs.cloudflare.com/ajax/libs/mathjax/3.2.2/es5/tex-mml-chtml.js".
So the live window.MathJax — a fully initialized MathJax object — is overwritten with a plain config literal, and a second copy of MathJax is fetched from a different CDN and initialized over the top of the first.
MathJaxWrapper is mounted in four places: ExercisePreview.tsx, StatementPreview.tsx, RunestonePreview.tsx, RunestoneGraderPreview.tsx. A module-level promise guard in the library means the script is only appended once, so this happens once per page load, not four times.
Observed effects
Measured in Chromium against the real CDNs, replaying the exact sequence with both real configs:
before MathJaxContext mounts |
after the second load | |
|---|---|---|
window.MathJax.typesetPromise |
function |
(briefly undefined) → function |
MathJax.config.options.ignoreHtmlClass |
"tex2jax_ignore|ignore-math" |
null |
MathJax.config.options.processHtmlClass |
"process-math" |
"katex" |
Three consequences:
-
ignoreHtmlClassis gone. Any element markedtex2jax_ignoreorignore-mathis no longer skipped. Confirmed: after the second load, atex2jax_ignorediv containing\newcommand{\zz}{\$}gets its\$rewritten to<span>$</span>byprocessEscapes. -
There is a window where
window.MathJaxis a plain object with notypesetPromise.runestonebase.js:591-619(queueMathJax) guards onMathJax.typesetPromisebefore enqueuing, butAutoQueue.dequeuere-reads the global inside anawait, so a component that queues just before the clobber can hitMathJax.typesetPromise is not a functionand have its typeset promise silently rejected. -
Two MathJax versions.
mathjax@3(floating) from jsdelivr vs. pinned3.2.2from cdnjs, plus a second CDN dependency and a second full MathJax download on every builder page load.
Why it matters for #1248
The fix in #1248 keeps the course's latex_macros pristine by putting them in a tex2jax_ignore div, so MathJax's page-load pass doesn't rewrite the \$ in \newcommand{\dollar}{\$}.
That fix is safe today, and I verified it end-to-end with the double load in place: the carrier div survives intact and all macros resolve. It holds for two reasons that are both incidental:
- the only page-wide typeset pass runs under the Runestone config, which still honors
tex2jax_ignore; and - the builder's config sets
startup: { typeset: false }, so the second MathJax never does a page-wide pass.
If either of those changes — someone adds a MathJax.typesetPromise([document.body]), or flips startup.typeset — the tex2jax_ignore guard is inert and #1248 comes straight back.
Suggested direction
Pick one owner for MathJax on the page. Either:
- Don't let
better-react-mathjaxload MathJax. It already exposessrc; there is no supported "use the existing instance" mode, so the cleanest version is probably to dropMathJaxContext/<MathJax>in favor of calling the already-loaded global directly, sincerenderRunestoneComponent→runestonebase.queueMathJaxis doing the real typesetting for previews anyway. The<MathJax>wrappers mostly typeset an empty span on mount. - Or have
MathJaxWrapper's config extend the Runestone config rather than replace it, so at minimumignoreHtmlClassandprocessHtmlClasssurvive, and pointsrcat the same jsdelivr URL to avoid the second download.
The first is the real fix; the second is a cheap stopgap.
Repro pages used for the measurements above are throwaway; happy to attach them if useful.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with components/rsptx/templates/common/static_assets_min.html:26-75 and assignment_builder/src/components/routes/AssignmentBuilder/MathJaxWrapper.tsx:72, then trace its four preview mounts and runestonebase.js:591-619. Reproduce the load sequence in Chromium and choose one MathJax owner; done means no second CDN load or configuration clobber, while ignoreHtmlClass remains honored and preview typesetting still works.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- react, typescript
- Domain
- frontend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100