diff --git a/devtools/test_dashboard/index-mathjax3chtml.html b/devtools/test_dashboard/index-mathjax3chtml.html index 795c5688703..75561236224 100644 --- a/devtools/test_dashboard/index-mathjax3chtml.html +++ b/devtools/test_dashboard/index-mathjax3chtml.html @@ -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); } } diff --git a/devtools/test_dashboard/index.html b/devtools/test_dashboard/index.html index de0841cc650..521077098c0 100644 --- a/devtools/test_dashboard/index.html +++ b/devtools/test_dashboard/index.html @@ -27,7 +27,7 @@ - + diff --git a/package-lock.json b/package-lock.json index 1bcc20791d3..ebaf7dc0bc9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -55,8 +55,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", "@types/d3": "3.5.34", "@types/node": "^24.10.0", "amdefine": "^1.0.1", @@ -650,6 +650,13 @@ "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-3.0.0.tgz", "integrity": "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==" }, + "node_modules/@mathjax/mathjax-newcm-font": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@mathjax/mathjax-newcm-font/-/mathjax-newcm-font-4.1.2.tgz", + "integrity": "sha512-lZHMjNP2XbABHA3kVn40rbse5ERUeMEmrGH03qLkCwxq4/5Z/eNLr0akw1MmQcqTwCbvkx1BFcmJ7RCfbRlw3Q==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -753,13 +760,6 @@ "node": ">=6.4.0" } }, - "node_modules/@plotly/mathjax-v2": { - "name": "mathjax", - "version": "2.7.5", - "resolved": "https://registry.npmjs.org/mathjax/-/mathjax-2.7.5.tgz", - "integrity": "sha512-OzsJNitEHAJB3y4IIlPCAvS0yoXwYjlo2Y4kmm9KQzyIBZt2d8yKRalby3uTRNN4fZQiGL2iMXjpdP1u2Rq2DQ==", - "dev": true - }, "node_modules/@plotly/mathjax-v3": { "name": "mathjax", "version": "3.2.2", @@ -767,6 +767,17 @@ "integrity": "sha512-Bt+SSVU8eBG27zChVewOicYs7Xsdt40qm4+UpHyX7k0/O9NliPc+x77k1/FEsPsjKPZGJvtRZM1vO+geW0OhGw==", "dev": true }, + "node_modules/@plotly/mathjax-v4": { + "name": "mathjax", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/mathjax/-/mathjax-4.1.2.tgz", + "integrity": "sha512-EQDS8xBpVg179BXoLeZ9JlwUFftOC5qylw20UlAMDhrTuooENigOocY79aNkkFSyvj/AST/89ZAo12+r5bPI4w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@mathjax/mathjax-newcm-font": "^4.1.2" + } + }, "node_modules/@plotly/point-cluster": { "version": "3.1.9", "resolved": "https://registry.npmjs.org/@plotly/point-cluster/-/point-cluster-3.1.9.tgz", diff --git a/package.json b/package.json index a62cdd26db4..350c4680463 100644 --- a/package.json +++ b/package.json @@ -117,8 +117,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", "@types/d3": "3.5.34", "@types/node": "^24.10.0", "amdefine": "^1.0.1", diff --git a/src/lib/svg_text_utils.js b/src/lib/svg_text_utils.js index d4bd953684b..91b3c5697c8 100644 --- a/src/lib/svg_text_utils.js +++ b/src/lib/svg_text_utils.js @@ -188,42 +188,22 @@ function cleanEscapesForTex(s) { var inlineMath = [['$', '$'], ['\\(', '\\)']]; 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); + Lib.warn('Unsupported MathJax version:', MathJax.version); return; } - var originalRenderer, - originalConfig, - originalProcessSectionDelay, + var originalConfig, 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() { + const setConfig = function() { originalConfig = Lib.extendDeepAll({}, MathJax.config); if(!MathJax.config.tex) { @@ -231,24 +211,14 @@ function texToSVG(_texString, _config, _callback) { } MathJax.config.tex.inlineMath = inlineMath; - }; - - var setRenderer2 = function() { - originalRenderer = MathJax.Hub.config.menuSettings.renderer; - if(originalRenderer !== 'SVG') { - return MathJax.Hub.setRenderer('SVG'); - } - }; - var setRenderer3 = function() { - originalRenderer = MathJax.config.startup.output; - if(originalRenderer !== 'svg') { + if(MathJax.config.startup.output !== 'svg') { MathJax.config.startup.output = 'svg'; } }; - 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({ @@ -258,81 +228,45 @@ 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() { + // Restore the original state of the global MathJax config we mutated above + // This also restores the renderer to its original value + const resetConfig = 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(); - }); - } + // Set up MathJax, render tex, then clean up and return + setConfig(); + MathJax.startup.defaultReady(); + MathJax.startup.promise + .then(initiateMathJax) + .then(finalizeMathJax) + .catch((err) => { + Lib.log('MathJax typesetting failed.', _texString, err); + if(tmpDiv) tmpDiv.remove(); + _callback(); + }) + .then(resetConfig); } var TAG_STYLES = { diff --git a/src/plot_api/plot_config.js b/src/plot_api/plot_config.js index b76a2cd3a17..27933854d8e 100644 --- a/src/plot_api/plot_config.js +++ b/src/plot_api/plot_config.js @@ -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(' ') }, diff --git a/src/types/generated/schema.d.ts b/src/types/generated/schema.d.ts index f927fc11032..e6400436394 100644 --- a/src/types/generated/schema.d.ts +++ b/src/types/generated/schema.d.ts @@ -16842,7 +16842,7 @@ export interface ConfigBase { */ topojsonURL?: string; /** - * Determines whether math should be typeset or not, when MathJax (either v2 or v3) is present on the page. + * Determines whether math should be typeset or not, when MathJax (either v3 or v4) is present on the page. * @default true */ typesetMath?: boolean; diff --git a/test/image/baselines/legend_mathjax_title_and_items.png b/test/image/baselines/legend_mathjax_title_and_items.png index 0b3181e088d..1e1a6def423 100644 Binary files a/test/image/baselines/legend_mathjax_title_and_items.png and b/test/image/baselines/legend_mathjax_title_and_items.png differ diff --git a/test/image/baselines/mathjax.png b/test/image/baselines/mathjax.png index dc454c412b8..50fa2c6ed5d 100644 Binary files a/test/image/baselines/mathjax.png and b/test/image/baselines/mathjax.png differ diff --git a/test/image/baselines/parcats_grid_subplots.png b/test/image/baselines/parcats_grid_subplots.png index 0925b959212..27899d0ff86 100644 Binary files a/test/image/baselines/parcats_grid_subplots.png and b/test/image/baselines/parcats_grid_subplots.png differ diff --git a/test/image/baselines/table_latex_multitrace_scatter.png b/test/image/baselines/table_latex_multitrace_scatter.png index 973dba8886c..63054172192 100644 Binary files a/test/image/baselines/table_latex_multitrace_scatter.png and b/test/image/baselines/table_latex_multitrace_scatter.png differ diff --git a/test/image/baselines/table_plain_birds.png b/test/image/baselines/table_plain_birds.png index 37a4a5533a5..e81986abec5 100644 Binary files a/test/image/baselines/table_plain_birds.png and b/test/image/baselines/table_plain_birds.png differ diff --git a/test/image/baselines/table_wrapped_birds.png b/test/image/baselines/table_wrapped_birds.png index fc53fb898f5..bc65500e10a 100644 Binary files a/test/image/baselines/table_wrapped_birds.png and b/test/image/baselines/table_wrapped_birds.png differ diff --git a/test/image/baselines/ternary-mathjax-title-place-subtitle.png b/test/image/baselines/ternary-mathjax-title-place-subtitle.png index 980a5beb679..8fd743fae90 100644 Binary files a/test/image/baselines/ternary-mathjax-title-place-subtitle.png and b/test/image/baselines/ternary-mathjax-title-place-subtitle.png differ diff --git a/test/image/baselines/ternary-mathjax.png b/test/image/baselines/ternary-mathjax.png index 0eb463e26eb..2723d691ed8 100644 Binary files a/test/image/baselines/ternary-mathjax.png and b/test/image/baselines/ternary-mathjax.png differ diff --git a/test/image/make_baseline.py b/test/image/make_baseline.py index 0c0934adc2c..8bce6c7e5ba 100644 --- a/test/image/make_baseline.py +++ b/test/image/make_baseline.py @@ -36,8 +36,6 @@ print("output to", dirOut) -mathjax_version = 2 -mathjax = None if "mathjax3" in sys.argv or "mathjax3=" in sys.argv: # until https://github.com/plotly/Kaleido/issues/124 is addressed # we are uanble to use local mathjax v3 installed in node_modules @@ -46,6 +44,11 @@ mathjax_version = 3 print("Kaleido using MathJax v3") +else: + mathjax_version = 4 + # Kaleido still defaults to MathJax v2, so we need to explicitly specify the path to MathJax v4 + mathjax = "https://cdn.jsdelivr.net/npm/mathjax@4.1.2/tex-svg.js" + print("Kaleido using MathJax v4") virtual_webgl_version = 0 # i.e. virtual-webgl is not used if "virtual-webgl" in sys.argv or "virtual-webgl=" in sys.argv: diff --git a/test/jasmine/bundle_tests/mathjax_config_test.js b/test/jasmine/bundle_tests/mathjax_config_test.js index 1d80b6ca933..9cc0e318a1f 100644 --- a/test/jasmine/bundle_tests/mathjax_config_test.js +++ b/test/jasmine/bundle_tests/mathjax_config_test.js @@ -16,20 +16,18 @@ describe('Test MathJax v' + mathjaxVersion + ' config test:', function() { beforeAll(function(done) { gd = createGraphDiv(); - if(mathjaxVersion === 3) { - window.MathJax = { - startup: { - output: 'chtml', - tex: { - inlineMath: [['|', '|']] - } + window.MathJax = { + startup: { + output: 'chtml', + tex: { + inlineMath: [['|', '|']] } - }; - } + } + }; - var src = mathjaxVersion === 3 ? + const src = mathjaxVersion === 3 ? '/base/node_modules/@plotly/mathjax-v3/es5/tex-svg.js' : - '/base/node_modules/@plotly/mathjax-v2/MathJax.js?config=TeX-AMS_SVG'; + '/base/node_modules/@plotly/mathjax-v4/tex-svg.js'; loadScript(src, done); }); @@ -37,25 +35,9 @@ describe('Test MathJax v' + mathjaxVersion + ' config test:', function() { afterAll(destroyGraphDiv); it('should maintain startup renderer & inlineMath after SVG rendering', function(done) { - if(mathjaxVersion === 2) { - window.MathJax.Hub.Config({ - tex2jax: { - inlineMath: [['|', '|']] - } - }); - - window.MathJax.Hub.setRenderer('CHTML'); - } - // before plot - if(mathjaxVersion === 3) { - expect(window.MathJax.config.startup.output).toEqual('chtml'); - expect(window.MathJax.config.startup.tex.inlineMath).toEqual([['|', '|']]); - } - if(mathjaxVersion === 2) { - expect(window.MathJax.Hub.config.menuSettings.renderer).toEqual(''); - expect(window.MathJax.Hub.config.tex2jax.inlineMath).toEqual([['|', '|']]); - } + expect(window.MathJax.config.startup.output).toEqual('chtml'); + expect(window.MathJax.config.startup.tex.inlineMath).toEqual([['|', '|']]); Plotly.newPlot(gd, { data: [{ @@ -69,19 +51,8 @@ describe('Test MathJax v' + mathjaxVersion + ' config test:', function() { }) .then(function() { // after plot - if(mathjaxVersion === 3) { - expect(window.MathJax.config.startup.output).toEqual('chtml'); - expect(window.MathJax.config.startup.tex.inlineMath).toEqual([['|', '|']]); - } - if(mathjaxVersion === 2) { - expect(window.MathJax.Hub.config.menuSettings.renderer).toEqual(''); - } - }) - .then(delay(1000)) // TODO: why we need this delay for mathjax v2 here? - .then(function() { - if(mathjaxVersion === 2) { - expect(window.MathJax.Hub.config.tex2jax.inlineMath).toEqual([['|', '|']]); - } + expect(window.MathJax.config.startup.output).toEqual('chtml'); + expect(window.MathJax.config.startup.tex.inlineMath).toEqual([['|', '|']]); }) .then(done, done.fail); }); diff --git a/test/jasmine/bundle_tests/mathjax_test.js b/test/jasmine/bundle_tests/mathjax_test.js index 4b5d03a4b4b..2602ff3818c 100644 --- a/test/jasmine/bundle_tests/mathjax_test.js +++ b/test/jasmine/bundle_tests/mathjax_test.js @@ -10,10 +10,13 @@ var mathjaxVersion = __karma__.config.mathjaxVersion; describe('Test MathJax v' + mathjaxVersion + ':', function() { beforeAll(function(done) { - var src = mathjaxVersion === 3 ? + const src = mathjaxVersion === 3 ? '/base/node_modules/@plotly/mathjax-v3/es5/tex-svg.js' : - '/base/node_modules/@plotly/mathjax-v2/MathJax.js?config=TeX-AMS-MML_SVG'; + '/base/node_modules/@plotly/mathjax-v4/tex-svg.js'; + // TODO: `?config=` is not needed for MathJax v3 and onward, + // should we adjust these tests? + // N.B. we have to load MathJax "dynamically" as Karma // does not undefined the MathJax's `?config=` parameter. // diff --git a/test/jasmine/karma.conf.js b/test/jasmine/karma.conf.js index cc42795d29f..9efefe7b623 100644 --- a/test/jasmine/karma.conf.js +++ b/test/jasmine/karma.conf.js @@ -69,7 +69,7 @@ if (argv.info) { '', 'Other options:', ' - `--info`: show this info message', - ' - `--mathjax3`: to load mathjax v3 in relevant test', + ' - `--mathjax3`: to load mathjax v3 in relevant test (otherwise mathjax v4 is loaded)', ' - `--Chrome` (alias `--chrome`): run test in (our custom) Chrome browser', ' - `--Firefox` (alias `--FF`, `--firefox`): run test in (our custom) Firefox browser', ' - `--nowatch (dflt: `false`, `true` on CI)`: run karma w/o `autoWatch` / multiple run mode', @@ -128,8 +128,8 @@ if (isFullSuite) { var pathToCustomMatchers = path.join(__dirname, 'assets', 'custom_matchers.js'); var pathToTopojsonDist = path.join(__dirname, '..', '..', 'topojson', 'dist'); -var pathToMathJax2 = path.join(__dirname, '..', '..', 'node_modules', '@plotly/mathjax-v2'); var pathToMathJax3 = path.join(__dirname, '..', '..', 'node_modules', '@plotly/mathjax-v3'); +var pathToMathJax4 = path.join(__dirname, '..', '..', 'node_modules', '@plotly/mathjax-v4'); var pathToVirtualWebgl = path.join(__dirname, '..', '..', 'node_modules', 'virtual-webgl', 'src', 'virtual-webgl.js'); var reporters = []; @@ -200,10 +200,10 @@ func.defaultConfig = { // N.B. the rest of this field is filled below files: [ pathToCustomMatchers, - // available to fetch from /base/node_modules/@plotly/mathjax-v2/ + // available to fetch from /base/node_modules/@plotly/mathjax-v3/ // more info: http://karma-runner.github.io/3.0/config/files.html - { pattern: pathToMathJax2 + '/**', included: false, watched: false, served: true }, { pattern: pathToMathJax3 + '/**', included: false, watched: false, served: true }, + { pattern: pathToMathJax4 + '/**', included: false, watched: false, served: true }, { pattern: pathToTopojsonDist + '/**', included: false, watched: false, served: true } ], @@ -308,7 +308,7 @@ func.defaultConfig = { tagPrefix: '@', skipTags: isCI ? 'noCI' : null, - mathjaxVersion: argv.mathjax3 ? 3 : 2, + mathjaxVersion: argv.mathjax3 ? 3 : 4, // See https://jasmine.github.io/api/3.4/Configuration.html jasmine: { diff --git a/test/plot-schema.json b/test/plot-schema.json index 160a59285ca..108a3ec2721 100644 --- a/test/plot-schema.json +++ b/test/plot-schema.json @@ -346,7 +346,7 @@ "valType": "string" }, "typesetMath": { - "description": "Determines whether math should be typeset or not, when MathJax (either v2 or v3) is present on the page.", + "description": "Determines whether math should be typeset or not, when MathJax (either v3 or v4) is present on the page.", "dflt": true, "valType": "boolean" },