diff --git a/cms/djangoapps/pipeline_js/js/xmodule.js b/cms/djangoapps/pipeline_js/js/xmodule.js index 8a19355c1a93..643b640670c8 100644 --- a/cms/djangoapps/pipeline_js/js/xmodule.js +++ b/cms/djangoapps/pipeline_js/js/xmodule.js @@ -16,36 +16,42 @@ define( window.$ = $; window._ = _; + window.MathJax = { + tex: { + inlineMath: [ + ['\\(', '\\)'], + ['[mathjaxinline]', '[/mathjaxinline]'] + ], + displayMath: [ + ['\\[', '\\]'], + ['[mathjax]', '[/mathjax]'] + ], + autoload: { + color: [], + colorv2: ['color'] + }, + packages: {'[+]': ['noerrors']} + }, + options: { + ignoreHtmlClass: 'tex2jax_ignore', + processHtmlClass: 'tex2jax_process', + menuOptions: { + settings: { + collapsible: true, + explorer: true + // autocollapse: false, // Not found in v3 + }, + }, + }, + loader: { + load: ['input/asciimath', '[tex]/noerrors'] + } + }; $script( - 'https://cdn.jsdelivr.net/npm/mathjax@2.7.5/MathJax.js' - + '?config=TeX-MML-AM_SVG&delayStartupUntil=configured', + 'https://cdn.jsdelivr.net/npm/mathjax@4.1.2/tex-mml-chtml.js', 'mathjax', function() { - window.MathJax.Hub.Config({ - tex2jax: { - inlineMath: [ - ['\\(', '\\)'], - ['[mathjaxinline]', '[/mathjaxinline]'] - ], - displayMath: [ - ['\\[', '\\]'], - ['[mathjax]', '[/mathjax]'] - ] - }, - CommonHTML: {linebreaks: {automatic: true}}, - SVG: {linebreaks: {automatic: true}}, - 'HTML-CSS': {linebreaks: {automatic: true}}, - }); - - // In order to eliminate all flashing during interactive - // preview, it is necessary to set processSectionDelay to 0 - // (remove delay between input and output phases). This - // effectively disables fast preview, regardless of - // the fast preview setting as shown in the context menu. - window.MathJax.Hub.processSectionDelay = 0; - window.MathJax.Hub.Configured(); - window.addEventListener('resize', MJrenderer); let t = -1; @@ -60,7 +66,8 @@ define( if (oldWidth !== document.documentElement.scrollWidth) { t = window.setTimeout(function() { oldWidth = document.documentElement.scrollWidth; - MathJax.Hub.Queue(['Rerender', MathJax.Hub]); + MathJax.typesetClear(); + MathJax.typesetPromise(); t = -1; }, delay); } diff --git a/cms/static/cms/js/require-config.js b/cms/static/cms/js/require-config.js index d2bc6341ebba..a8edb19c9f2b 100644 --- a/cms/static/cms/js/require-config.js +++ b/cms/static/cms/js/require-config.js @@ -47,19 +47,34 @@ } }); - // MathJax Fast Preview was introduced in 2.5. However, it - // causes undesirable flashing/font size changes when - // MathJax is used for interactive preview (equation editor). - // Setting processSectionDelay to 0 (see below) fully eliminates - // fast preview, but to reduce confusion, we are also setting - // the option as displayed in the context menu to false. - // When upgrading to 2.6, check if this variable name changed. window.MathJax = { - menuSettings: { - CHTMLpreview: false, - collapsible: true, - autocollapse: false, - explorer: true + tex: { + inlineMath: [ + ['\\(', '\\)'], + ['[mathjaxinline]', '[/mathjaxinline]'] + ], + displayMath: [ + ['\\[', '\\]'], + ['[mathjax]', '[/mathjax]'] + ], + autoload: { + color: [], + colorv2: ['color'] + }, + packages: {'[+]': ['noerrors']} + }, + options: { + ignoreHtmlClass: 'tex2jax_ignore', + processHtmlClass: 'tex2jax_process', + menuOptions: { + settings: { + collapsible: true, + explorer: true + }, + }, + }, + loader: { + load: ['input/asciimath', '[tex]/noerrors'] } }; }; @@ -137,7 +152,7 @@ 'jquery_extend_patch': 'js/src/jquery_extend_patch', // externally hosted files - mathjax: 'https://cdn.jsdelivr.net/npm/mathjax@2.7.5/MathJax.js?config=TeX-MML-AM_SVG&delayStartupUntil=configured', // eslint-disable-line max-len + mathjax: 'https://cdn.jsdelivr.net/npm/mathjax@4.1.2/tex-mml-chtml.js?noext', 'youtube': [ // youtube URL does not end in '.js'. We add '?noext' to the path so // that require.js adds the '.js' to the query component of the URL, @@ -249,27 +264,6 @@ }, 'mathjax': { exports: 'MathJax', - init: function() { - window.MathJax.Hub.Config({ - tex2jax: { - inlineMath: [ - ['\\(', '\\)'], - ['[mathjaxinline]', '[/mathjaxinline]'] - ], - displayMath: [ - ['\\[', '\\]'], - ['[mathjax]', '[/mathjax]'] - ] - } - }); - // In order to eliminate all flashing during interactive - // preview, it is necessary to set processSectionDelay to 0 - // (remove delay between input and output phases). This - // effectively disables fast preview, regardless of - // the fast preview setting as shown in the context menu. - window.MathJax.Hub.processSectionDelay = 0; - window.MathJax.Hub.Configured(); - } }, 'URI': { exports: 'URI' diff --git a/cms/static/cms/js/spec/main.js b/cms/static/cms/js/spec/main.js index 019fb2b67887..764248f5fe56 100644 --- a/cms/static/cms/js/spec/main.js +++ b/cms/static/cms/js/spec/main.js @@ -19,6 +19,37 @@ window.edx.HtmlUtils = HtmlUtils; window.edx.StringUtils = StringUtils; }); + + window.MathJax = { + tex: { + inlineMath: [ + ['\\(', '\\)'], + ['[mathjaxinline]', '[/mathjaxinline]'] + ], + displayMath: [ + ['\\[', '\\]'], + ['[mathjax]', '[/mathjax]'] + ], + autoload: { + color: [], + colorv2: ['color'] + }, + packages: {'[+]': ['noerrors']} + }, + options: { + ignoreHtmlClass: 'tex2jax_ignore', + processHtmlClass: 'tex2jax_process', + menuOptions: { + settings: { + collapsible: true, + explorer: true + }, + }, + }, + loader: { + load: ['input/asciimath', '[tex]/noerrors'] + } + }; } requirejs.config({ @@ -69,7 +100,7 @@ 'domReady': 'xmodule_js/common_static/js/vendor/domReady', 'URI': 'xmodule_js/common_static/js/vendor/URI.min', 'mock-ajax': 'xmodule_js/common_static/js/vendor/mock-ajax', - mathjax: 'https://cdn.jsdelivr.net/npm/mathjax@2.7.5/MathJax.js?config=TeX-MML-AM_SVG&delayStartupUntil=configured', // eslint-disable-line max-len + mathjax: 'https://cdn.jsdelivr.net/npm/mathjax@4.1.2/tex-mml-chtml.js?noext', 'youtube': '//www.youtube.com/player_api?noext', 'js/src/ajax_prefix': 'xmodule_js/common_static/js/src/ajax_prefix', 'js/spec/test_utils': 'js/spec/test_utils' @@ -172,15 +203,6 @@ }, 'mathjax': { exports: 'MathJax', - init: function() { - window.MathJax.Hub.Config({ - tex2jax: { - inlineMath: [['\\(', '\\)'], ['[mathjaxinline]', '[/mathjaxinline]']], - displayMath: [['\\[', '\\]'], ['[mathjax]', '[/mathjax]']] - } - }); - return window.MathJax.Hub.Configured(); - } }, 'accessibility': { exports: 'accessibility', diff --git a/cms/static/cms/js/spec/main_squire.js b/cms/static/cms/js/spec/main_squire.js index ace0f36b1f2d..e3545f2202a9 100644 --- a/cms/static/cms/js/spec/main_squire.js +++ b/cms/static/cms/js/spec/main_squire.js @@ -5,6 +5,38 @@ 'use strict'; var i, specHelpers, testFiles; + if (window) { + window.MathJax = { + tex: { + inlineMath: [ + ['\\(', '\\)'], + ['[mathjaxinline]', '[/mathjaxinline]'] + ], + displayMath: [ + ['\\[', '\\]'], + ['[mathjax]', '[/mathjax]'] + ], + autoload: { + color: [], + colorv2: ['color'] + }, + packages: {'[+]': ['noerrors']} + }, + options: { + ignoreHtmlClass: 'tex2jax_ignore', + processHtmlClass: 'tex2jax_process', + menuOptions: { + settings: { + collapsible: true, + explorer: true + }, + }, + }, + loader: { + load: ['input/asciimath', '[tex]/noerrors'] + } + }; + } requirejs.config({ baseUrl: '/base/', @@ -48,7 +80,7 @@ 'draggabilly': 'xmodule_js/common_static/js/vendor/draggabilly', 'domReady': 'xmodule_js/common_static/js/vendor/domReady', 'URI': 'xmodule_js/common_static/js/vendor/URI.min', - mathjax: 'https://cdn.jsdelivr.net/npm/mathjax@2.7.5/MathJax.js?config=TeX-MML-AM_SVG&delayStartupUntil=configured', // eslint-disable-line max-len + mathjax: 'https://cdn.jsdelivr.net/npm/mathjax@4.1.2/tex-mml-chtml.js?noext', 'youtube': '//www.youtube.com/player_api?noext', 'js/src/ajax_prefix': 'xmodule_js/common_static/js/src/ajax_prefix' }, @@ -142,15 +174,6 @@ }, 'mathjax': { exports: 'MathJax', - init: function() { - window.MathJax.Hub.Config({ - tex2jax: { - inlineMath: [['\\(', '\\)'], ['[mathjaxinline]', '[/mathjaxinline]']], - displayMath: [['\\[', '\\]'], ['[mathjax]', '[/mathjax]']] - } - }); - window.MathJax.Hub.Configured(); - } }, 'URI': { exports: 'URI' diff --git a/cms/static/sass/studio-main-v1.scss b/cms/static/sass/studio-main-v1.scss index a325010a7c61..9c5b4cbf4818 100644 --- a/cms/static/sass/studio-main-v1.scss +++ b/cms/static/sass/studio-main-v1.scss @@ -20,3 +20,16 @@ @import 'vendor/bi-app/bi-app-ltr'; // set the layout for left to right languages @import 'build-v1'; // shared app style assets/rendering @import '_builtin-block-variables'; + + +mjx-container { + max-width: 100%; + overflow-x: auto; + overflow-y: hidden; +} + +table>tbody>tr>td>mjx-container { + max-width: inherit; + overflow-x: auto; + overflow-y: hidden; +} diff --git a/common/static/common/js/discussion/mathjax_include.js b/common/static/common/js/discussion/mathjax_include.js index 074354566e1b..468ad7046ade 100644 --- a/common/static/common/js/discussion/mathjax_include.js +++ b/common/static/common/js/discussion/mathjax_include.js @@ -1,56 +1,35 @@ -// See common/templates/mathjax_include.html for info on Fast Preview mode. -var disableFastPreview = true, - vendorScript; if (typeof MathJax === 'undefined') { - if (disableFastPreview) { - window.MathJax = { - menuSettings: {CHTMLpreview: false} - }; - } - - vendorScript = document.createElement('script'); - vendorScript.onload = function() { - 'use strict'; - - var MathJax = window.MathJax, - setMathJaxDisplayDivSettings; - MathJax.Hub.Config({ - tex2jax: { - inlineMath: [ - ['\\(', '\\)'], - ['[mathjaxinline]', '[/mathjaxinline]'] - ], - displayMath: [ - ['\\[', '\\]'], - ['[mathjax]', '[/mathjax]'] - ] - } - }); - if (disableFastPreview) { - MathJax.Hub.processSectionDelay = 0; - } - MathJax.Hub.signal.Interest(function(message) { - if (message[0] === 'End Math') { - setMathJaxDisplayDivSettings(); - } - }); - setMathJaxDisplayDivSettings = function() { - $('.MathJax_Display').each(function() { - this.setAttribute('tabindex', '0'); - this.setAttribute('aria-live', 'off'); - this.removeAttribute('role'); - this.removeAttribute('aria-readonly'); - }); - }; - }; - // Automatic loading of Mathjax accessibility files window.MathJax = { - menuSettings: { - collapsible: true, - autocollapse: false, - explorer: true + tex: { + inlineMath: [ + ['\\(', '\\)'], + ['[mathjaxinline]', '[/mathjaxinline]'] + ], + displayMath: [ + ['\\[', '\\]'], + ['[mathjax]', '[/mathjax]'] + ], + autoload: { + color: [], + colorv2: ['color'] + }, + packages: {'[+]': ['noerrors']} + }, + options: { + ignoreHtmlClass: 'tex2jax_ignore', + processHtmlClass: 'tex2jax_process', + menuOptions: { + settings: { + collapsible: true, + explorer: true + }, + }, + }, + loader: { + load: ['input/asciimath', '[tex]/noerrors'] } }; - vendorScript.src = 'https://cdn.jsdelivr.net/npm/mathjax@2.7.5/MathJax.js?config=TeX-MML-AM_HTMLorMML'; + var vendorScript = document.createElement('script'); + vendorScript.src = 'https://cdn.jsdelivr.net/npm/mathjax@4.1.2/tex-mml-chtml.js'; document.body.appendChild(vendorScript); } diff --git a/common/static/common/js/discussion/utils.js b/common/static/common/js/discussion/utils.js index 3f0a7a448438..23ff811648f1 100644 --- a/common/static/common/js/discussion/utils.js +++ b/common/static/common/js/discussion/utils.js @@ -493,8 +493,9 @@ }; DiscussionUtil.typesetMathJax = function(element) { - if (typeof MathJax !== 'undefined' && MathJax !== null && typeof MathJax.Hub !== 'undefined') { - MathJax.Hub.Queue(['Typeset', MathJax.Hub, element[0]]); + if (typeof MathJax !== 'undefined' && MathJax !== null && typeof MathJax.startup !== 'undefined') { + MathJax.startup.promise + .then(() => MathJax.typesetPromise([element[0]])); } }; diff --git a/common/templates/mathjax_include.html b/common/templates/mathjax_include.html index ead5a935ee94..fa9a7d4a0192 100644 --- a/common/templates/mathjax_include.html +++ b/common/templates/mathjax_include.html @@ -5,92 +5,76 @@ ## MathJax configuration for Studio lives in ## cms/js/require-config.js. -<%page args="disable_fast_preview=True" expression_filter="h"/> - ## Avoid loading mathjax if already loaded on the page +<%page expression_filter="h"/> %if context.get('load_mathjax', True): -%if disable_fast_preview: +%if mathjax_mode is not Undefined and mathjax_mode == 'wiki': -%endif + window.MathJax = { + tex: { + inlineMath: [ ['$','$'], ["\\(","\\)"]], + displayMath: [ ['$$','$$'], ["\\[","\\]"]], + autoload: { + color: [], + colorv2: ['color'] + }, + packages: {'[+]': ['noerrors']} + }, + options: { + ignoreHtmlClass: 'tex2jax_ignore', + processHtmlClass: 'tex2jax_process', + menuOptions: { + settings: { + collapsible: true, + explorer: true + }, + }, + }, + loader: { + load: ['input/asciimath', '[tex]/noerrors'] + } + }; -%if mathjax_mode is not Undefined and mathjax_mode == 'wiki': - %else: - %endif - + %endif diff --git a/common/templates/xblock_v2/xblock_iframe.html b/common/templates/xblock_v2/xblock_iframe.html index 358765c85eb4..d2452aae9c1f 100644 --- a/common/templates/xblock_v2/xblock_iframe.html +++ b/common/templates/xblock_v2/xblock_iframe.html @@ -122,22 +122,38 @@ - - - - + {{ fragment.head_html | safe }} diff --git a/lms/djangoapps/courseware/tests/test_views.py b/lms/djangoapps/courseware/tests/test_views.py index 3091387cd622..e184559fedb9 100644 --- a/lms/djangoapps/courseware/tests/test_views.py +++ b/lms/djangoapps/courseware/tests/test_views.py @@ -2851,7 +2851,7 @@ def test_mathjax_detection(self): url = reverse("render_xblock", kwargs={'usage_key_string': str(usage_key)}) response = self.client.get(url) assert response.status_code == 200 - assert b"MathJax.Hub.Config" in response.content + assert b"window.MathJax" in response.content # Check the one without Math... url = reverse("render_xblock", kwargs={ @@ -2859,7 +2859,7 @@ def test_mathjax_detection(self): }) response = self.client.get(url) assert response.status_code == 200 - assert b"MathJax.Hub.Config" not in response.content + assert b"window.MathJax" not in response.content # The containing vertical should still return MathJax (for now) url = reverse("render_xblock", kwargs={ @@ -2867,7 +2867,7 @@ def test_mathjax_detection(self): }) response = self.client.get(url) assert response.status_code == 200 - assert b"MathJax.Hub.Config" in response.content + assert b"window.MathJax" in response.content @override_waffle_flag(COURSEWARE_OPTIMIZED_RENDER_XBLOCK, False) def test_mathjax_detection_disabled(self): @@ -2878,7 +2878,7 @@ def test_mathjax_detection_disabled(self): }) response = self.client.get(url) assert response.status_code == 200 - assert b"MathJax.Hub.Config" in response.content + assert b"window.MathJax" in response.content @ddt.ddt diff --git a/lms/static/js/customwmd.js b/lms/static/js/customwmd.js index 0f4cfccaeecb..8027cc15fb30 100644 --- a/lms/static/js/customwmd.js +++ b/lms/static/js/customwmd.js @@ -42,9 +42,6 @@ Mostly adapted from math.stackexchange.com: http://cdn.sstatic.net/js/mathjax-ed MathJaxProcessor.prototype.processMath = function(start, last, preProcess) { var block, i, j, ref, ref1; block = this.blocks.slice(start, last + 1).join("").replace(/&/g, "&").replace(//g, ">"); - if (MathJax.Hub.Browser.isMSIE) { - block = block.replace(/(%[^\n]*)\n/g, "$1
\n"); - } for (i = j = ref = start + 1, ref1 = last; ref <= ref1 ? j <= ref1 : j >= ref1; i = ref <= ref1 ? ++j : --j) { this.blocks[i] = ""; } diff --git a/lms/static/js/mathjax_delay_renderer.js b/lms/static/js/mathjax_delay_renderer.js index e7b10b4073e8..6517dff14a9c 100644 --- a/lms/static/js/mathjax_delay_renderer.js +++ b/lms/static/js/mathjax_delay_renderer.js @@ -13,8 +13,6 @@ MathJaxDelayRenderer.prototype.maxDelay = 3000; - MathJaxDelayRenderer.prototype.mathjaxRunning = false; - MathJaxDelayRenderer.prototype.elapsedTime = 0; MathJaxDelayRenderer.prototype.mathjaxDelay = 0; @@ -59,8 +57,15 @@ if (preprocessor != null) { text = preprocessor(text); } + if (typeof MathJax !== 'undefined' && MathJax !== null && typeof MathJax.typesetClear === 'function') { + MathJax.typesetClear([$(elem)[0]]); + } $(elem).html(text); // xss-lint: disable=javascript-jquery-html - return MathJax.Hub.Queue(["Typeset", MathJax.Hub, $(elem).attr("id")]); + if (typeof MathJax !== 'undefined' && MathJax !== null && MathJax.startup && + MathJax.startup.promise && typeof MathJax.typesetPromise === "function") { + return MathJax.startup.promise + .then(() => MathJax.typesetPromise([$(elem)[0]])); + } } else { if (this.mathjaxTimeout) { window.clearTimeout(this.mathjaxTimeout); @@ -70,31 +75,38 @@ renderer = (function(_this) { return function() { var curTime, prevTime; - if (_this.mathjaxRunning) { - return; - } prevTime = getTime(); if (preprocessor != null) { text = preprocessor(text); } + if (typeof MathJax !== 'undefined' && MathJax !== null && typeof MathJax.typesetClear === 'function') { + MathJax.typesetClear([_this.$buffer[0]]); + } _this.$buffer.html(text); // xss-lint: disable=javascript-jquery-html curTime = getTime(); _this.elapsedTime = curTime - prevTime; - if (typeof MathJax !== "undefined" && MathJax !== null) { + if (typeof MathJax !== "undefined" && MathJax !== null && MathJax.startup && + MathJax.startup.promise && typeof MathJax.typesetPromise === "function") { prevTime = getTime(); - _this.mathjaxRunning = true; - return MathJax.Hub.Queue(["Typeset", MathJax.Hub, _this.$buffer.attr("id")], function() { - _this.mathjaxRunning = false; - curTime = getTime(); - _this.mathjaxDelay = curTime - prevTime; - if (previewSetter) { - return previewSetter($(_this.$buffer).html()); - } else { - return $(elem).html($(_this.$buffer).html()); // xss-lint: disable=javascript-jquery-html - } - }); + return MathJax.startup.promise + .then( + () => MathJax.typesetPromise([_this.$buffer[0]]).then(() => { + curTime = getTime(); + _this.mathjaxDelay = curTime - prevTime; + if (previewSetter) { + return previewSetter($(_this.$buffer).html()); + } else { + return $(elem).html($(_this.$buffer).html()); // xss-lint: disable=javascript-jquery-html + } + }) + ); } else { - return _this.mathjaxDelay = 0; + _this.mathjaxDelay = 0; + if (previewSetter) { + return previewSetter($(_this.$buffer).html()); + } else { + return $(elem).html($(_this.$buffer).html()); // xss-lint: disable=javascript-jquery-html + } } }; })(this); diff --git a/lms/static/lms/js/spec/main.js b/lms/static/lms/js/spec/main.js index d8bc1417e86e..61e58e794f43 100644 --- a/lms/static/lms/js/spec/main.js +++ b/lms/static/lms/js/spec/main.js @@ -5,6 +5,38 @@ 'use strict'; var i, specHelpers, testFiles; + if (window) { + window.MathJax = { + tex: { + inlineMath: [ + ['\\(', '\\)'], + ['[mathjaxinline]', '[/mathjaxinline]'] + ], + displayMath: [ + ['\\[', '\\]'], + ['[mathjax]', '[/mathjax]'] + ], + autoload: { + color: [], + colorv2: ['color'] + }, + packages: {'[+]': ['noerrors']} + }, + options: { + ignoreHtmlClass: 'tex2jax_ignore', + processHtmlClass: 'tex2jax_process', + menuOptions: { + settings: { + collapsible: true, + explorer: true + }, + }, + }, + loader: { + load: ['input/asciimath', '[tex]/noerrors'] + } + }; + } // TODO: how can we share the vast majority of this config that is in common with CMS? requirejs.config({ @@ -56,7 +88,7 @@ 'squire': 'common/js/vendor/Squire', 'jasmine-imagediff': 'xmodule_js/common_static/js/vendor/jasmine-imagediff', 'domReady': 'xmodule_js/common_static/js/vendor/domReady', - mathjax: 'https://cdn.jsdelivr.net/npm/mathjax@2.7.5/MathJax.js?config=TeX-MML-AM_HTMLorMML&delayStartupUntil=configured', // eslint-disable-line max-len + mathjax: 'https://cdn.jsdelivr.net/npm/mathjax@4.1.2/tex-mml-chtml.js?noext', 'youtube': '//www.youtube.com/player_api?noext', 'js/src/ajax_prefix': 'xmodule_js/common_static/js/src/ajax_prefix', 'js/instructor_dashboard/student_admin': 'js/instructor_dashboard/student_admin', @@ -234,15 +266,6 @@ }, 'mathjax': { exports: 'MathJax', - init: function() { - MathJax.Hub.Config({ - tex2jax: { - inlineMath: [['\\(', '\\)'], ['[mathjaxinline]', '[/mathjaxinline]']], - displayMath: [['\\[', '\\]'], ['[mathjax]', '[/mathjax]']] - } - }); - return MathJax.Hub.Configured(); - } }, 'URI': { exports: 'URI' diff --git a/lms/static/sass/lms-course.scss b/lms/static/sass/lms-course.scss index 738d892257dc..1923d8a321b1 100644 --- a/lms/static/sass/lms-course.scss +++ b/lms/static/sass/lms-course.scss @@ -34,3 +34,15 @@ width: 54px !important; } } + +mjx-container { + max-width: 100%; + overflow-x: auto; + overflow-y: hidden; +} + +table>tbody>tr>td>mjx-container { + max-width: inherit; + overflow-x: auto; + overflow-y: hidden; +} diff --git a/lms/templates/courseware/courseware-chromeless.html b/lms/templates/courseware/courseware-chromeless.html index deeda26c431d..e45b586f4dcb 100644 --- a/lms/templates/courseware/courseware-chromeless.html +++ b/lms/templates/courseware/courseware-chromeless.html @@ -68,7 +68,7 @@ <%static:js group='courseware'/> % if enable_mathjax: - <%include file="/mathjax_include.html" args="disable_fast_preview=True"/> + <%include file="/mathjax_include.html" /> % endif % if staff_access: <%include file="xqa_interface.html"/> diff --git a/lms/templates/courseware/static_tab.html b/lms/templates/courseware/static_tab.html index cf79fb8948f7..cbce51b62223 100644 --- a/lms/templates/courseware/static_tab.html +++ b/lms/templates/courseware/static_tab.html @@ -18,7 +18,7 @@ <%block name="footer_extra"> -<%include file="/mathjax_include.html" args="disable_fast_preview=True"/> +<%include file="/mathjax_include.html" /> ${HTML(fragment.foot_html())} diff --git a/lms/templates/courseware/tab-view.html b/lms/templates/courseware/tab-view.html index 319861c8ffb6..07d982a25359 100644 --- a/lms/templates/courseware/tab-view.html +++ b/lms/templates/courseware/tab-view.html @@ -26,7 +26,7 @@ <%block name="footer_extra"> -<%include file="/mathjax_include.html" args="disable_fast_preview=True"/> +<%include file="/mathjax_include.html" /> ${HTML(fragment.foot_html())} diff --git a/lms/templates/discussion/_js_body_dependencies.html b/lms/templates/discussion/_js_body_dependencies.html index d10b6918b62d..bd91f8cc9fd4 100644 --- a/lms/templates/discussion/_js_body_dependencies.html +++ b/lms/templates/discussion/_js_body_dependencies.html @@ -1,14 +1,14 @@ ## mako -<%namespace name='static' file='/static_content.html'/> +<%page expression_filter="h"/> -<%page args="disable_fast_preview=True" expression_filter="h"/> +<%namespace name='static' file='/static_content.html'/> <%! from openedx.core.djangolib.js_utils import js_escaped_string %> -<%include file="/mathjax_include.html" args="disable_fast_preview=disable_fast_preview"/> +<%include file="/mathjax_include.html"/> <%static:js group='discussion'/> diff --git a/xmodule/js/spec/capa/display_spec.js b/xmodule/js/spec/capa/display_spec.js index f44d3c185168..5cc9985c5467 100644 --- a/xmodule/js/spec/capa/display_spec.js +++ b/xmodule/js/spec/capa/display_spec.js @@ -10,11 +10,28 @@ describe("Problem", function () { beforeEach(function () { // Stub MathJax window.MathJax = { - Hub: jasmine.createSpyObj("MathJax.Hub", ["getAllJax", "Queue"]), - Callback: jasmine.createSpyObj("MathJax.Callback", ["After"]), + startup: { + document: jasmine.createSpyObj("MathJax.startup.document", ["getMathItemsWithin"]), + promise: jasmine.createSpyObj("MathJax.startup.promise", ["then"]), + toMML: jasmine.createSpy("MathJax.startup.toMML"), + }, + typesetClear: jasmine.createSpy("MathJax.typesetClear"), + typesetPromise: jasmine.createSpy("MathJax.typesetPromise"), }; - this.stubbedJax = { root: jasmine.createSpyObj("jax.root", ["toMathML"]) }; - MathJax.Hub.getAllJax.and.returnValue([this.stubbedJax]); + MathJax.startup.promise.then.and.callFake(function (callback) { + callback(); + return Promise.resolve(); + }); + MathJax.typesetPromise.and.callFake(function () { + return { + then(callback) { + callback(); + return Promise.resolve(); + }, + }; + }); + this.stubbedJax = { root: jasmine.createSpy("jax.root") }; + MathJax.startup.document.getMathItemsWithin.and.returnValue([this.stubbedJax]); window.update_schematics = function () {}; spyOn(SR, "readText"); spyOn(SR, "readTexts"); @@ -57,11 +74,11 @@ data-url='/problem/quiz/'> \ describe("bind", function () { beforeEach(function () { spyOn(window, "update_schematics"); - MathJax.Hub.getAllJax.and.returnValue([this.stubbedJax]); + MathJax.startup.document.getMathItemsWithin.and.returnValue([this.stubbedJax]); this.problem = new Problem($(".xblock-student_view")); }); - it("set mathjax typeset", () => expect(MathJax.Hub.Queue).toHaveBeenCalled()); + it("set mathjax typeset", () => expect(MathJax.typesetPromise).toHaveBeenCalled()); it("update schematics", () => expect(window.update_schematics).toHaveBeenCalled()); @@ -88,12 +105,22 @@ data-url='/problem/quiz/'> \ it("bind the math input", function () { expect($("input.math")).toHandleWith("keyup", this.problem.refreshMath); }); + + it("bind the formulaequationinput", function () { + this.problem.el.find(".problem").prepend( + '
' + + ' ' + + '
' + ); + this.problem.bind(); + expect($("#input_fe_bind")).toHandleWith("keyup", this.problem.refreshMath); + }); }); describe("bind_with_custom_input_id", function () { beforeEach(function () { spyOn(window, "update_schematics"); - MathJax.Hub.getAllJax.and.returnValue([this.stubbedJax]); + MathJax.startup.document.getMathItemsWithin.and.returnValue([this.stubbedJax]); this.problem = new Problem($(".xblock-student_view")); return $(this).html(readFixtures("problem_content_1240.html")); }); @@ -994,31 +1021,90 @@ data-url='/problem/quiz/'> \ describe("refreshMath", function () { beforeEach(function () { this.problem = new Problem($(".xblock-student_view")); - // Reset Queue spy so that bind()'s Queue call ([fn, null, domEl]) is not - // included when toHaveBeenCalledWith scans recorded calls. In Jasmine 2.99, - // toHaveBeenCalledWith iterates ALL recorded calls' args element-by-element - // (even mismatched ones, for diff output). jasmine-jquery's custom equality - // tester calls $(domEl).is(anyString) when comparing a DOM node against a - // string — which throws a Sizzle syntax error if the string isn't a valid - // CSS selector (e.g. "E=mc^2"). Resetting here isolates this describe to - // testing only what refreshMath itself queues. - MathJax.Hub.Queue.calls.reset(); + MathJax.typesetClear.calls.reset(); + MathJax.typesetPromise.calls.reset(); + MathJax.startup.document.getMathItemsWithin.calls.reset(); + MathJax.startup.document.getMathItemsWithin.and.returnValue([this.stubbedJax]); $("#input_example_1").val("E=mc^2"); this.problem.refreshMath({ target: $("#input_example_1").get(0) }); }); - it("should queue the conversion and MathML element update", function () { - expect(MathJax.Hub.Queue).toHaveBeenCalledWith( - ["Text", this.stubbedJax, "E=mc^2"], - [this.problem.updateMathML, this.stubbedJax, $("#input_example_1").get(0)], - ); + it("should clear and typeset the math element", function () { + var math = document.querySelector("#display_example_1"); + expect(MathJax.typesetClear).toHaveBeenCalledWith([math]); + expect(MathJax.typesetPromise).toHaveBeenCalledWith([math]); + }); + + it("wraps calculator input in backticks for input.math", function () { + var math = document.querySelector("#display_example_1"); + expect(math.textContent).toBe("`E=mc^2`"); + }); + }); + + describe("refreshMath formula and delimiter handling", function () { + var formulaHTML = '' + + '
' + + ' ' + + '
' + + '
'; + + beforeEach(function () { + this.problem = new Problem($(".xblock-student_view")); + MathJax.typesetClear.calls.reset(); + MathJax.typesetPromise.calls.reset(); + MathJax.startup.document.getMathItemsWithin.calls.reset(); + MathJax.startup.document.getMathItemsWithin.and.returnValue([this.stubbedJax]); + this.problem.el.find(".problem").prepend(formulaHTML); + }); + + it("wraps calculator syntax in backticks for formulaequationinput", function () { + $("#input_test_fe").val("R_1*R_2/R_3"); + this.problem.refreshMath({ target: $("#input_test_fe").get(0) }); + var math = document.querySelector("#input_test_fe_preview"); + expect(math.textContent).toBe("`R_1*R_2/R_3`"); + }); + + it("preserves explicit TeX delimiters \\(...\\)", function () { + $("#input_test_fe").val("\\\(E=mc^2\\\)"); + this.problem.refreshMath({ target: $("#input_test_fe").get(0) }); + var math = document.querySelector("#input_test_fe_preview"); + expect(math.textContent).toBe("\\\(E=mc^2\\\)"); + }); + + it("preserves display math TeX delimiters \\[\\]", function () { + $("#input_test_fe").val("\\\[E=mc^2\\\]"); + this.problem.refreshMath({ target: $("#input_test_fe").get(0) }); + var math = document.querySelector("#input_test_fe_preview"); + expect(math.textContent).toBe("\\\[E=mc^2\\\]"); + }); + + it("preserves double-dollar delimiters", function () { + $("#input_test_fe").val("$$E=mc^2$$").trigger("keyup"); + this.problem.refreshMath({ target: $("#input_test_fe").get(0) }); + var math = document.querySelector("#input_test_fe_preview"); + expect(math.textContent).toBe("$$E=mc^2$$"); + }); + + it("preserves single-dollar delimiters", function () { + $("#input_test_fe").val("$E=mc^2$").trigger("keyup"); + this.problem.refreshMath({ target: $("#input_test_fe").get(0) }); + var math = document.querySelector("#input_test_fe_preview"); + expect(math.textContent).toBe("$E=mc^2$"); + }); + + it("clears preview and skips typesetPromise on empty input", function () { + $("#input_test_fe").val(""); + this.problem.refreshMath({ target: $("#input_test_fe").get(0) }); + var math = document.querySelector("#input_test_fe_preview"); + expect(math.textContent).toBe(""); + expect(MathJax.typesetPromise).not.toHaveBeenCalled(); }); }); describe("updateMathML", function () { beforeEach(function () { this.problem = new Problem($(".xblock-student_view")); - this.stubbedJax.root.toMathML.and.returnValue(""); + MathJax.startup.toMML.and.returnValue(""); }); describe("when there is no exception", function () { @@ -1033,12 +1119,14 @@ data-url='/problem/quiz/'> \ beforeEach(function () { const error = new Error(); error.restart = true; - this.stubbedJax.root.toMathML.and.throwError(error); + MathJax.startup.toMML.and.throwError(error); + spyOn(this.problem, "refreshMath"); this.problem.updateMathML(this.stubbedJax, $("#input_example_1").get(0)); }); it("should queue up the exception", function () { - expect(MathJax.Callback.After).toHaveBeenCalledWith([this.problem.refreshMath, this.stubbedJax], true); + expect(MathJax.startup.promise.then).toHaveBeenCalled(); + expect(this.problem.refreshMath).toHaveBeenCalledWith(null, $("#input_example_1").get(0)); }); }); }); diff --git a/xmodule/js/src/capa/display.js b/xmodule/js/src/capa/display.js index fea25a654049..756a771e4dfe 100644 --- a/xmodule/js/src/capa/display.js +++ b/xmodule/js/src/capa/display.js @@ -20,6 +20,31 @@ return -1; }; + /** + * Check whether MathJax has been fully loaded and initialized (not just the config object). + * In v3/v4, startup.promise is only set after the MathJax component loader completes. + */ + var isMathJaxTypesetReady = function () { + return typeof MathJax !== "undefined" && MathJax !== null && + typeof MathJax.startup !== "undefined" && MathJax.startup !== null && + typeof MathJax.startup.promise !== "undefined" && + typeof MathJax.typesetPromise === "function"; + }; + + var isMathJaxRefreshReady = function () { + return isMathJaxTypesetReady() && + typeof MathJax.typesetClear === "function" && + typeof MathJax.startup.document !== "undefined" && MathJax.startup.document !== null && + typeof MathJax.startup.document.getMathItemsWithin === "function"; + }; + + var isMathJaxMathMLReady = function () { + return typeof MathJax !== "undefined" && MathJax !== null && + typeof MathJax.startup !== "undefined" && MathJax.startup !== null && + typeof MathJax.startup.promise !== "undefined" && + typeof MathJax.startup.toMML === "function"; + }; + this.Problem = function () { function Problem(element) { var that = this; @@ -164,9 +189,9 @@ Problem.prototype.bind = function () { var problemPrefix, that = this; - if (typeof MathJax !== "undefined" && MathJax !== null) { + if (isMathJaxTypesetReady()) { this.el.find(".problem > div").each(function (index, element) { - return MathJax.Hub.Queue(["Typeset", MathJax.Hub, element]); + return MathJax.startup.promise.then(() => MathJax.typesetPromise([element])); }); } if (window.hasOwnProperty("update_schematics")) { @@ -215,10 +240,10 @@ this.submitAnswersAndSubmitButton(true); } Collapsible.setCollapsibles(this.el); - this.$("input.math").keyup(this.refreshMath); - if (typeof MathJax !== "undefined" && MathJax !== null) { - this.$("input.math").each(function (index, element) { - return MathJax.Hub.Queue([that.refreshMath, null, element]); + this.$("input.math, .formulaequationinput input").keyup(this.refreshMath); + if (isMathJaxTypesetReady()) { + this.$("input.math, .formulaequationinput input").each(function (index, element) { + return MathJax.startup.promise.then(() => that.refreshMath(null, element)); }); } }; @@ -820,9 +845,9 @@ } return results; }); - if (typeof MathJax !== "undefined" && MathJax !== null) { + if (isMathJaxTypesetReady()) { that.el.find(".problem > div").each(function (index, element) { - return MathJax.Hub.Queue(["Typeset", MathJax.Hub, element]); + return MathJax.startup.promise.then(() => MathJax.typesetPromise([element])); }); } that.el.find(".show").attr("disabled", "disabled"); @@ -876,35 +901,78 @@ }; Problem.prototype.refreshMath = function (event, element) { - var elid, eqn, jax, mathjaxPreprocessor, preprocessorTag, target; + var elid, + eqn, + jax, + mathjaxPreprocessor, + preprocessorTag, + isTexDelimited; if (!element) { element = event.target; // eslint-disable-line no-param-reassign } elid = element.id.replace(/^input_/, ""); - target = "display_" + elid; - // MathJax preprocessor is loaded by 'setupInputTypes' preprocessorTag = "inputtype_" + elid; mathjaxPreprocessor = this.inputtypeDisplays[preprocessorTag]; - if (typeof MathJax !== "undefined" && MathJax !== null && MathJax.Hub.getAllJax(target)[0]) { - jax = MathJax.Hub.getAllJax(target)[0]; + if (isMathJaxRefreshReady()) { + var math = document.getElementById("display_" + elid); + if (!math) { + math = document.getElementById(element.id + "_preview"); + } + if (!math) { + return; + } eqn = $(element).val(); if (mathjaxPreprocessor) { eqn = mathjaxPreprocessor(eqn); } - MathJax.Hub.Queue(["Text", jax, eqn], [this.updateMathML, jax, element]); + MathJax.typesetClear([math]); + if (!eqn) { + math.textContent = ""; + var dynInput = document.getElementById(element.id + "_dynamath"); + if (dynInput) { dynInput.value = ""; } + return; + } + // Determine rendering mode: + // isTexDelimited -> preserve user-written TeX delimiters as-is + // else -> calculator/AsciiMath syntax via backtick delimiters + isTexDelimited = ( + (/^\s*\\\(/.test(eqn) && /\\\)\s*$/.test(eqn)) || + (/^\s*\\\[/.test(eqn) && /\\\]\s*$/.test(eqn)) || + (/^\s*\$\$/.test(eqn) && /\$\$\s*$/.test(eqn)) || + (/^\s*\$[^$]/.test(eqn) && /[^$]\$\s*$/.test(eqn)) + ); + if (isTexDelimited) { + math.textContent = eqn; + } else { + // Calculator/AsciiMath syntax: backtick-delimited for AsciiMath input jax + math.textContent = "`" + eqn + "`"; + } + MathJax.typesetPromise([math]).then(function () { + jax = MathJax.startup.document.getMathItemsWithin(math)[0]; + if (jax) { + this.updateMathML(jax, element); + } + }.bind(this)).catch(function () { + var dynInput = document.getElementById(element.id + "_dynamath"); + if (dynInput) { dynInput.value = ""; } + }); } }; Problem.prototype.updateMathML = function (jax, element) { - try { - $("#" + element.id + "_dynamath").val(jax.root.toMathML("")); - } catch (exception) { - if (!exception.restart) { - throw exception; - } - if (typeof MathJax !== "undefined" && MathJax !== null) { - MathJax.Callback.After([this.refreshMath, jax], exception.restart); + if (isMathJaxMathMLReady()) { + try { + var dynInput = document.getElementById(element.id + "_dynamath"); + if (dynInput) { dynInput.value = MathJax.startup.toMML(jax.root); } + } catch (exception) { + if (!exception.restart) { + throw exception; + } + MathJax.startup.promise + .then(function () { + this.refreshMath(null, element); + }.bind(this)); } } }; @@ -1391,7 +1459,10 @@ hintMsgContainer = that.$(".problem-hint .notification-message"); hintContainer.attr("hint_index", response.hint_index); edx.HtmlUtils.setHtml(hintMsgContainer, edx.HtmlUtils.HTML(response.msg)); - MathJax.Hub.Queue(["Typeset", MathJax.Hub, hintContainer[0]]); + if (isMathJaxTypesetReady()) { + MathJax.startup.promise + .then(() => MathJax.typesetPromise([hintContainer[0]])); + } if (response.should_enable_next_hint) { that.hintButton.removeAttr("disabled"); } else { diff --git a/xmodule/js/src/html/display.js b/xmodule/js/src/html/display.js index ec3e0f3097fb..b94d86058788 100644 --- a/xmodule/js/src/html/display.js +++ b/xmodule/js/src/html/display.js @@ -7,8 +7,10 @@ this.el = $(this.element); JavascriptLoader.executeModuleScripts(this.el); Collapsible.setCollapsibles(this.el); - if (typeof MathJax !== "undefined" && MathJax !== null) { - MathJax.Hub.Queue(["Typeset", MathJax.Hub, this.el[0]]); + if (typeof MathJax !== "undefined" && MathJax !== null && MathJax.startup && + MathJax.startup.promise && typeof MathJax.typesetPromise === "function") { + MathJax.startup.promise + .then(() => MathJax.typesetPromise([this.el[0]])); } if (typeof setupFullScreenModal !== "undefined" && setupFullScreenModal !== null) { setupFullScreenModal();