Error "s.tagName is undefined" when clicking text node in mjx-break
Issue Summary
Clicking on text nodes in SVG causes an exception.
Steps to Reproduce:
- Render the formula
D_i (0 \leq i < n) - Click on the whitespace between the
0and the\leqcharacter - An exception is thrown:
TypeError: can't access property "toLowerCase", s.tagName is undefined
The exception is thrown here: https://github.com/mathjax/MathJax-src/blob/1a2ef74c0ac0620e7b8de46402c9dce3b95ade52/ts/a11y/explorer/KeyExplorer.ts#L1420
This type assertion casts text nodes to elements:
const nodes = Array.from(clicked.childNodes) as HTMLElement[];
See attached screenshot for the whitespace node in the DOM (inside <mjx-break>).
Technical details:
- MathJax Version: 4.4.0
- Client OS: macOS 15.6.1 (24G90)
- Browser: Firefox 143.0.4 (aarch64)
I am using the following MathJax configuration:
window.MathJax = {
startup: {
// Typesetting will be triggered from the React components
typeset: false,
pageReady() {
resolveMathJaxPromise(mathJax);
return MathJax.startup.defaultPageReady();
},
// getComputedStyle returns an empty font size if the node is detached from the DOM
// https://codeexpert.myjetbrains.com/youtrack/issue/cx-3811/MathJax-crash-in-Safari-26
// FIXME: remove this once https://github.com/mathjax/MathJax/issues/3458 is fixed
ready() {
MathJax.startup.defaultReady();
MathJax.startup.document.adaptor.fontSize = function (node: Node) {
const { fontSize }: CSSStyleDeclaration = this.window.getComputedStyle(node);
return fontSize == null || string.isBlank(fontSize) ? 16 : parseFloat(fontSize);
};
},
},
loader: {
load: ['ui/safe'],
source: {
'[tex]/amsCd': '[tex]/amscd',
},
},
output: {
fontPath: `${mathJaxPath}/mathjax-newcm-font`,
},
options: {
safeOptions: {
allow: {
//
// Values can be "all", "safe", or "none"
//
URLs: 'none', // 'none' to prevent cookie theft through external URLs
classes: 'safe', // safe start with mjx- (can be set by pattern below)
cssIDs: 'safe', // safe start with mjx- (can be set by pattern below)
styles: 'safe', // safe are in safeStyles below
},
//
// Which styles are allowed
//
safeStyles: {
color: true,
backgroundColor: true,
border: true,
cursor: true,
margin: true,
padding: true,
textShadow: true,
fontFamily: true,
fontSize: true,
fontStyle: true,
fontWeight: true,
opacity: true,
outline: true,
},
lengthMax: 3, // Largest padding/border/margin, etc. in em's
scriptsizemultiplierRange: [0.6, 1], // Valid range for scriptsizemultiplier
scriptlevelRange: [-2, 2], // Valid range for scriptlevel
classPattern: /^mjx-[-a-zA-Z0-9_.]+$/, // Pattern for allowed class names
idPattern: /^mjx-[-a-zA-Z0-9_.]+$/, // Pattern for allowed ids
dataPattern: /^data-mjx-/, // Pattern for data attributes
},
},
tex: {
inlineMath: [['$', '$']],
displayMath: [['$$', '$$']],
processEscapes: true,
},
};
and loading MathJax via
http://localhost:3020/mathjax/tex-svg.js
Interestingly, the online demo (https://www.mathjax.org/#demo) renders empty mjx-break elements:
When $D_i (0 \leq i < n)$, …
A potential workaround is moving the mjx-break element vertically to preserve the width but prevent it from being detected as click target:
mjx-break {
position: relative;
top: -10em;
}
Since MathJax registers the event listeners on mjx-container in the capture phase, there seems to be no possibility to intercept the event (https://github.com/mathjax/MathJax-src/blob/1a2ef74c0ac0620e7b8de46402c9dce3b95ade52/ts/ui/menu/Menu.ts#L1721).
Thanks for the report. I will make a PR to fix the problem. In the meantime, you can merge the following configuration into yours:
MathJax = {
startup: {
ready() {
const { SpeechExplorer } = MathJax._.a11y.explorer.KeyExplorer;
SpeechExplorer.prototype.findClicked = function (node, x, y) {
const icon = this.document.infoIcon;
if (icon === node || icon.contains(node)) return icon;
if (this.node.getAttribute('jax') !== 'SVG') return node.closest(nav);
let found = null;
let clicked = this.node;
while (clicked) {
if (clicked.matches('[data-speech-node]')) found = clicked;
const nodes = Array.from(clicked.childNodes);
clicked = null;
for (const child of nodes) {
if (child !== this.speech && child !== this.img &&
child.tagName && child.tagName.toLowerCase() !== 'rect') {
const { left, right, top, bottom } = child.getBoundingClientRect();
if (left <= x && x <= right && top <= y && y <= bottom) {
clicked = child;
break;
}
}
}
}
return found;
};
MathJax.startup.defaultReady();
}
}
};
It tests that child.tagName exists before using it.
I've made the PR for it.
@dpvc, thanks a lot! For the moment I have incorporated the changes from your PR using patch-package.
@dpvc, by the way, do you know why the online demo (https://www.mathjax.org/#demo) doesn't render the spaces inside mjx-break in SVG mode? Maybe this should be looked at as well.
do you know why the online demo doesn't render the spaces inside
mjx-breakin SVG mode?
There is a CSP issue: the SVG font for the space is in a data: URL and isn't currently being allowed. I will need to adjust the CSP for the site.
I fixed the CSP on www.mathjax.org, so you should see the proper spacing in SVG output now.