Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 1 addition & 6 deletions devtools/test_dashboard/index-mathjax3chtml.html
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,7 @@
options.format = svgDocument.inputJax[0].name;
return svgDocument.convert(math, options);
};
/*
MathJax.tex2svgPromise = (math, options = {}) => {
options.format = svgDocument.inputJax[0].name;
return mathjax.handleRetriesFor(() => svgDocument.convert(math, options));
};
*/

MathJax.svgStylesheet = () => svgOutput.styleSheet(svgDocument);
}
}
Expand Down
2 changes: 1 addition & 1 deletion devtools/test_dashboard/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
<!-- Update UI elements if strict mode is enabled -->
<script src="./strict.js"></script>

<script src="../../node_modules/@plotly/mathjax-v2/MathJax.js?config=TeX-AMS-MML_SVG"></script>
<script src="../../node_modules/@plotly/mathjax-v4/tex-svg.js"></script>
<script charset="utf-8" id="source" src="../../build/plotly.js"></script>
<script charset="utf-8" src="../../build/test_dashboard-bundle.js"></script>
</body>
Expand Down
29 changes: 20 additions & 9 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,8 @@
},
"devDependencies": {
"@biomejs/biome": "2.2.0",
"@plotly/mathjax-v2": "npm:mathjax@2.7.5",
"@plotly/mathjax-v3": "npm:mathjax@^3.2.2",
"@plotly/mathjax-v3": "npm:mathjax@3.2.2",
"@plotly/mathjax-v4": "npm:mathjax@^4.1.2",
Comment on lines +118 to +119

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The packages looks quite confusing.
#7394 seems to hide a security issue?
Is that still relevant?

"@types/d3": "3.5.34",
"@types/node": "^24.10.0",
"amdefine": "^1.0.1",
Expand Down
216 changes: 93 additions & 123 deletions src/lib/svg_text_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,22 +74,29 @@ exports.convertToTspans = function(_context, gd, _callback) {
}

if(tex) {
((gd && gd._promises) || []).push(new Promise(function(resolve) {
_context.style('display', 'none');
var fontSize = parseInt(_context.node().style.fontSize, 10);
var config = {fontSize: fontSize};

_context.style('display', 'none');
var fontSize = parseInt(_context.node().style.fontSize, 10);
var config = {fontSize: fontSize};
// capture the anchor now: the callback runs async, by which point a
// positioning pass may have changed text-anchor and would mis-place the svg
var textAnchor = _context.attr('text-anchor');

((gd && gd._promises) || []).push(
texToSVG(tex[2], config, function(_svgEl, _glyphDefs, _svgBBox) {
parent.selectAll('svg.' + svgClass).remove();
parent.selectAll('g.' + svgClass + '-group').remove();

var newSvg = _svgEl && _svgEl.select('svg');
if(!newSvg || !newSvg.node()) {
showText();
resolve();
return;
}

// Re-hide the source text. It should already be hidden, but it's possible
// another code path has made it visible again before the async render finishes,
// so we hide it again here now that the MathJax has actually been rendered
_context.style('display', 'none');

var mathjaxGroup = parent.append('g')
.classed(svgClass + '-group', true)
.attr({
Expand Down Expand Up @@ -151,11 +158,9 @@ exports.convertToTspans = function(_context, gd, _callback) {
x = 0;
y = dy;
} else {
var anchor = _context.attr('text-anchor');

x = x - w * (
anchor === 'middle' ? 0.5 :
anchor === 'end' ? 1 : 0
textAnchor === 'middle' ? 0.5 :
textAnchor === 'end' ? 1 : 0
);
y = y + dy - h / 2;
}
Expand All @@ -166,9 +171,8 @@ exports.convertToTspans = function(_context, gd, _callback) {
});

if(_callback) _callback.call(_context, mathjaxGroup);
resolve(mathjaxGroup);
});
}));
})
);
} else showText();

return _context;
Expand All @@ -187,68 +191,77 @@ function cleanEscapesForTex(s) {

var inlineMath = [['$', '$'], ['\\(', '\\)']];

// Configure MathJax, then build its startup document by calling MathJax.startup.defaultReady().
// Once the startup document is built, we can reset the config immediately,
// and reuse the same document for all render calls.
var mathjaxReadyPromise = null;

function ensureMathJax(MathJaxVersion) {
if(mathjaxReadyPromise) return mathjaxReadyPromise;

var config = MathJax.config;
var origTex = config.tex;
var origSvg = config.svg;
var origOutput = config.startup.output;

try {
// Configure MathJax the way we need it for plotly
config.tex = Lib.extendFlat({}, origTex, {inlineMath: inlineMath});
config.startup.output = 'svg';

// MathJax v4 enables automatic inline linebreaking by default,
// which breaks a lot of our layout assumptions. Disabling it
// gives behavior consistent with v3.
if(MathJaxVersion === 4) {
config.svg = Lib.extendFlat({}, origSvg, {
linebreaks: Lib.extendFlat({}, origSvg && origSvg.linebreaks, {inline: false})
});
}

MathJax.startup.defaultReady();
} finally {
// Reset the config to its original values
config.tex = origTex;
config.svg = origSvg;
config.startup.output = origOutput;
}

mathjaxReadyPromise = MathJax.startup.promise;
return mathjaxReadyPromise;
}

// Serialize MathJax renders in a queue so they don't interfere with each other
var mathjaxQueue = Promise.resolve();

function texToSVG(_texString, _config, _callback) {
var MathJaxVersion = parseInt(
const MathJaxVersion = parseInt(
(MathJax.version || '').split('.')[0]
);

if(
MathJaxVersion !== 2 &&
MathJaxVersion !== 3
MathJaxVersion !== 3 &&
MathJaxVersion !== 4
) {
Lib.warn('No MathJax version:', MathJax.version);
return;
Lib.warn('Unsupported MathJax version:', MathJax.version);
_callback();
return Promise.resolve();
}

var originalRenderer,
originalConfig,
originalProcessSectionDelay,
tmpDiv;

var setConfig2 = function() {
originalConfig = Lib.extendDeepAll({}, MathJax.Hub.config);

originalProcessSectionDelay = MathJax.Hub.processSectionDelay;
if(MathJax.Hub.processSectionDelay !== undefined) {
// MathJax 2.5+ but not 3+
MathJax.Hub.processSectionDelay = 0;
}

return MathJax.Hub.Config({
messageStyle: 'none',
tex2jax: {
inlineMath: inlineMath
},
displayAlign: 'left',
});
};

var setConfig3 = function() {
originalConfig = Lib.extendDeepAll({}, MathJax.config);

if(!MathJax.config.tex) {
MathJax.config.tex = {};
}
var result = mathjaxQueue.then(function() {
return renderTex(_texString, _config, _callback, MathJaxVersion);
});

MathJax.config.tex.inlineMath = inlineMath;
};
// swallow rejections so one failed render doesn't prevent subsequent renders
mathjaxQueue = result.catch(function() {});

var setRenderer2 = function() {
originalRenderer = MathJax.Hub.config.menuSettings.renderer;
if(originalRenderer !== 'SVG') {
return MathJax.Hub.setRenderer('SVG');
}
};
return result;
}

var setRenderer3 = function() {
originalRenderer = MathJax.config.startup.output;
if(originalRenderer !== 'svg') {
MathJax.config.startup.output = 'svg';
}
};
function renderTex(_texString, _config, _callback, MathJaxVersion) {
var tmpDiv;

var initiateMathJax = function() {
var randomID = 'math-output-' + Lib.randstr({}, 64);
const initiateMathJax = function() {
const randomID = 'math-output-' + Lib.randstr({}, 64);
tmpDiv = d3.select('body').append('div')
.attr({id: randomID})
.style({
Expand All @@ -258,81 +271,38 @@ function texToSVG(_texString, _config, _callback) {
})
.text(cleanEscapesForTex(_texString));

var tmpNode = tmpDiv.node();
const tmpNode = tmpDiv.node();

return MathJaxVersion === 2 ?
MathJax.Hub.Typeset(tmpNode) :
MathJax.typeset([tmpNode]);
return MathJax.typesetPromise([tmpNode]);
};

var finalizeMathJax = function() {
var sel = tmpDiv.select(
MathJaxVersion === 2 ? '.MathJax_SVG' : '.MathJax'
);
const finalizeMathJax = function() {
const sel = tmpDiv.select('.MathJax');

var node = !sel.empty() && tmpDiv.select('svg').node();
const node = !sel.empty() && tmpDiv.select('svg').node();
if(!node) {
Lib.log('There was an error in the tex syntax.', _texString);
_callback();
} else {
var nodeBBox = node.getBoundingClientRect();
var glyphDefs;
if(MathJaxVersion === 2) {
glyphDefs = d3.select('body').select('#MathJax_SVG_glyphs');
} else {
glyphDefs = sel.select('defs');
}
const nodeBBox = node.getBoundingClientRect();
const glyphDefs = sel.select('defs');
_callback(sel, glyphDefs, nodeBBox);
}

tmpDiv.remove();
};

var resetRenderer2 = function() {
if(originalRenderer !== 'SVG') {
return MathJax.Hub.setRenderer(originalRenderer);
}
};

var resetRenderer3 = function() {
if(originalRenderer !== 'svg') {
MathJax.config.startup.output = originalRenderer;
}
};

var resetConfig2 = function() {
if(originalProcessSectionDelay !== undefined) {
MathJax.Hub.processSectionDelay = originalProcessSectionDelay;
}
return MathJax.Hub.Config(originalConfig);
};

var resetConfig3 = function() {
MathJax.config = originalConfig;
};

if(MathJaxVersion === 2) {
MathJax.Hub.Queue(
setConfig2,
setRenderer2,
initiateMathJax,
finalizeMathJax,
resetRenderer2,
resetConfig2
);
} else if(MathJaxVersion === 3) {
setConfig3();
setRenderer3();
MathJax.startup.defaultReady();

MathJax.startup.promise.then(function() {
initiateMathJax();
finalizeMathJax();

resetRenderer3();
resetConfig3();
// start from a resolved promise so a synchronous throw in ensureMathJax
// routes to the catch below instead of rejecting the gd._promises entry
return Promise.resolve()
.then(function() { return ensureMathJax(MathJaxVersion); })
.then(initiateMathJax)
.then(finalizeMathJax)
.catch((err) => {
Lib.log('MathJax typesetting failed.', _texString, err);
if(tmpDiv) tmpDiv.remove();
_callback();
});
}
}

var TAG_STYLES = {
Expand Down
2 changes: 1 addition & 1 deletion src/plot_api/plot_config.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ var configAttributes = {
dflt: true,
description: [
'Determines whether math should be typeset or not,',
'when MathJax (either v2 or v3) is present on the page.'
'when MathJax (either v3 or v4) is present on the page.'
].join(' ')
},

Expand Down
Loading
Loading