diff --git a/xblocks_contrib/__init__.py b/xblocks_contrib/__init__.py index e7be4706..8c0c5922 100644 --- a/xblocks_contrib/__init__.py +++ b/xblocks_contrib/__init__.py @@ -9,4 +9,4 @@ from .video import VideoBlock from .word_cloud import WordCloudBlock -__version__ = "0.17.0" +__version__ = "0.17.1" diff --git a/xblocks_contrib/discussion/assets/static/js/common/mathjax_include.js b/xblocks_contrib/discussion/assets/static/js/common/mathjax_include.js index 07435456..468ad704 100644 --- a/xblocks_contrib/discussion/assets/static/js/common/mathjax_include.js +++ b/xblocks_contrib/discussion/assets/static/js/common/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/xblocks_contrib/discussion/assets/static/js/common/utils.js b/xblocks_contrib/discussion/assets/static/js/common/utils.js index 3f0a7a44..cc09e13f 100644 --- a/xblocks_contrib/discussion/assets/static/js/common/utils.js +++ b/xblocks_contrib/discussion/assets/static/js/common/utils.js @@ -493,8 +493,12 @@ }; 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 !== null && + typeof MathJax.startup.promise !== 'undefined' && + typeof MathJax.typesetPromise === 'function') { + MathJax.startup.promise + .then(() => MathJax.typesetPromise([element[0]])); } }; diff --git a/xblocks_contrib/discussion/assets/static/js/customwmd.js b/xblocks_contrib/discussion/assets/static/js/customwmd.js index 0f4cfcca..13d898a5 100644 --- a/xblocks_contrib/discussion/assets/static/js/customwmd.js +++ b/xblocks_contrib/discussion/assets/static/js/customwmd.js @@ -42,7 +42,7 @@ 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) { + if (window.navigator && /msie|trident/i.test(window.navigator.userAgent)) { 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) { diff --git a/xblocks_contrib/discussion/assets/static/js/mathjax_delay_renderer.js b/xblocks_contrib/discussion/assets/static/js/mathjax_delay_renderer.js index e7b10b40..6517dff1 100644 --- a/xblocks_contrib/discussion/assets/static/js/mathjax_delay_renderer.js +++ b/xblocks_contrib/discussion/assets/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/xblocks_contrib/html/html.py b/xblocks_contrib/html/html.py index 2d3b85a0..ada7cd82 100644 --- a/xblocks_contrib/html/html.py +++ b/xblocks_contrib/html/html.py @@ -191,7 +191,17 @@ def student_view(self, _context): """Return a fragment that contains the html for the student view.""" frag = Fragment(self.get_html()) frag.add_css(resource_loader.load_unicode("static/css/html.css")) - frag.add_javascript("""function HtmlBlock(runtime, element){}""") + frag.add_javascript(""" +function HtmlBlock(runtime, element) { + if (typeof MathJax !== "undefined" && MathJax !== null && + MathJax.startup && MathJax.startup.promise && + typeof MathJax.typesetPromise === "function") { + MathJax.startup.promise.then(function() { + MathJax.typesetPromise([element]); + }); + } +} +""") frag.initialize_js("HtmlBlock") return frag diff --git a/xblocks_contrib/problem/assets/spec/display_spec.js b/xblocks_contrib/problem/assets/spec/display_spec.js index 843ea6a9..e8a6481e 100644 --- a/xblocks_contrib/problem/assets/spec/display_spec.js +++ b/xblocks_contrib/problem/assets/spec/display_spec.js @@ -9,13 +9,22 @@ describe("Problem", function () { var mockRuntime = {}; beforeEach(function () { - // Stub MathJax + // Stub MathJax v3/v4 API window.MathJax = { - Hub: jasmine.createSpyObj("MathJax.Hub", ["getAllJax", "Queue"]), - Callback: jasmine.createSpyObj("MathJax.Callback", ["After"]), + startup: { + promise: { then: function(cb) { cb(); return this; }, catch: function() { return this; } }, + document: { + getMathItemsWithin: jasmine.createSpy("getMathItemsWithin"), + }, + toMML: jasmine.createSpy("startup.toMML"), + }, + typesetPromise: jasmine.createSpy("typesetPromise"), + typesetClear: jasmine.createSpy("typesetClear"), }; - this.stubbedJax = { root: jasmine.createSpyObj("jax.root", ["toMathML"]) }; - MathJax.Hub.getAllJax.and.returnValue([this.stubbedJax]); + this.stubbedJax = { root: {} }; + MathJax.startup.document.getMathItemsWithin.and.returnValue([this.stubbedJax]); + MathJax.startup.toMML.and.returnValue(""); + MathJax.typesetPromise.and.returnValue(Promise.resolve()); window.update_schematics = function () {}; spyOn(SR, "readText"); spyOn(SR, "readTexts"); @@ -58,11 +67,10 @@ data-url='/problem/quiz/'> \ describe("bind", function () { beforeEach(function () { spyOn(window, "update_schematics"); - MathJax.Hub.getAllJax.and.returnValue([this.stubbedJax]); this.problem = new Problem(mockRuntime, $(".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()); @@ -94,7 +102,6 @@ data-url='/problem/quiz/'> \ describe("bind_with_custom_input_id", function () { beforeEach(function () { spyOn(window, "update_schematics"); - MathJax.Hub.getAllJax.and.returnValue([this.stubbedJax]); this.problem = new Problem(mockRuntime, $(".xblock-student_view")); return $(this).html(readFixtures("problem_content_1240.html")); }); @@ -999,18 +1006,15 @@ data-url='/problem/quiz/'> \ 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 trigger MathJax v4 typesetting via typesetClear + typesetPromise", function () { + expect(MathJax.typesetClear).toHaveBeenCalled(); + expect(MathJax.typesetPromise).toHaveBeenCalled(); }); }); describe("updateMathML", function () { beforeEach(function () { this.problem = new Problem(mockRuntime, $(".xblock-student_view")); - this.stubbedJax.root.toMathML.and.returnValue(""); }); describe("when there is no exception", function () { @@ -1018,19 +1022,24 @@ data-url='/problem/quiz/'> \ this.problem.updateMathML(this.stubbedJax, $("#input_example_1").get(0)); }); - it("convert jax to MathML", () => expect($("#input_example_1_dynamath")).toHaveValue("")); + it("convert jax to MathML via startup.toMML", function () { + expect(MathJax.startup.toMML).toHaveBeenCalledWith(this.stubbedJax.root); + expect($("#input_example_1_dynamath")).toHaveValue(""); + }); }); - describe("when there is an exception", function () { + describe("when there is an exception with restart", function () { beforeEach(function () { const error = new Error(); error.restart = true; - this.stubbedJax.root.toMathML.and.throwError(error); + MathJax.startup.toMML.and.throwError(error); 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); + it("should catch the error and not propagate", function () { + // v4: caught by try/catch, restart=true → startup.promise.then schedules refreshMath. + // Minimal assertion: toMML was invoked, no uncaught exception. + expect(MathJax.startup.toMML).toHaveBeenCalled(); }); }); }); diff --git a/xblocks_contrib/problem/assets/static/js/display.js b/xblocks_contrib/problem/assets/static/js/display.js index 96fd56b2..5ed836b8 100644 --- a/xblocks_contrib/problem/assets/static/js/display.js +++ b/xblocks_contrib/problem/assets/static/js/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(runtime, 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,81 @@ }; 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()) { + // Use getElementById so IDs with : . etc. don't break as CSS selectors + 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 dynEl = document.getElementById(element.id + "_dynamath"); + if (dynEl) { dynEl.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 () { + // clear hidden field on MathJax failure so stale value isn't sent + var dynEl = document.getElementById(element.id + "_dynamath"); + if (dynEl) { dynEl.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 dynEl = document.getElementById(element.id + "_dynamath"); + if (dynEl) { dynEl.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 +1462,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/xblocks_contrib/problem/capa/static/js/capa/spec/formula_equation_preview_spec.js b/xblocks_contrib/problem/capa/static/js/capa/spec/formula_equation_preview_spec.js index dc4e1ff9..ded798ee 100644 --- a/xblocks_contrib/problem/capa/static/js/capa/spec/formula_equation_preview_spec.js +++ b/xblocks_contrib/problem/capa/static/js/capa/spec/formula_equation_preview_spec.js @@ -80,23 +80,18 @@ describe('Formula Equation Preview', function() { ajaxTimes.push(Date.now()); }); - // Spy on MathJax - this.jax = 'OUTPUT_JAX'; + // Spy on MathJax (v3/v4 API) this.oldMathJax = window.MathJax; - window.MathJax = {Hub: {}}; - window.MathJax.Hub.getAllJax = jasmine.createSpy('MathJax.Hub.getAllJax') - .and.returnValue([this.jax]); - window.MathJax.Hub.Queue = function(callback) { - if (typeof callback === 'function') { - callback(); - } + // Synchronous mock promise — .then executes callback immediately like old Hub.Queue + var mockPromise = { + then: function(callback) { callback(); return this; }, + catch: function() { return this; } }; - spyOn(window.MathJax.Hub, 'Queue').and.callThrough(); - window.MathJax.Hub.Startup = jasmine.createSpy('MathJax.Hub.Startup'); - window.MathJax.Hub.Startup.signal = jasmine.createSpy('MathJax.Hub.Startup.signal'); - window.MathJax.Hub.Startup.signal.Interest = function(callback) { - callback('End'); + window.MathJax = { + startup: { promise: mockPromise }, + typesetClear: jasmine.createSpy('MathJax.typesetClear'), + typesetPromise: jasmine.createSpy('MathJax.typesetPromise') }; }); @@ -184,7 +179,7 @@ describe('Formula Equation Preview', function() { jasmine.waitUntil(function() { // (Short circuit if `inputAjax` is indeed called) return window.Problem.inputAjax.calls.count() > 0 - || window.MathJax.Hub.Queue.calls.count() > 0; + || window.MathJax.typesetPromise.calls.count() > 0; }).then(function() { // Expect the request not to have been called. expect(window.Problem.inputAjax).not.toHaveBeenCalled(); @@ -256,8 +251,6 @@ describe('Formula Equation Preview', function() { it('updates MathJax and loading icon on callback', function(done) { formulaEquationPreview.enable(); - var jax = this.jax; - jasmine.waitUntil(function() { return window.Problem.inputAjax.calls.count() > 0; }).then(function() { @@ -268,21 +261,19 @@ describe('Formula Equation Preview', function() { request_start: args[3].request_start }); - // The only request returned--it should hide the loading icon. - expect($('img.loading').css('visibility')).toEqual('hidden'); + // The only request returned—mathjax-preview span is inserted, img.loading preserved. + expect($('img.loading').length).toEqual(1); - // We should look in the preview div for the MathJax. + // Clear and typeset the stable mathjax-preview span. var previewDiv = $('#input_THE_ID_preview')[0]; - expect(window.MathJax.Hub.getAllJax).toHaveBeenCalledWith(previewDiv); - - // Refresh the MathJax. - expect(window.MathJax.Hub.Queue).toHaveBeenCalledWith( - ['Text', jax, 'THE_FORMULA'] - ); + var mathSpan = previewDiv.querySelector('.mathjax-preview'); + expect(window.MathJax.typesetClear).toHaveBeenCalledWith([mathSpan]); + expect(mathSpan.textContent).toEqual('\\(THE_FORMULA\\)'); + expect(window.MathJax.typesetPromise).toHaveBeenCalledWith([mathSpan]); }).always(done); }); - it('finds alternatives if MathJax hasn\'t finished loading', function(done) { + it('falls back to text-only when MathJax startup is not ready', function(done) { formulaEquationPreview.enable(); $('#input_THE_ID').val('user_input').trigger('input'); @@ -292,8 +283,8 @@ describe('Formula Equation Preview', function() { var args = window.Problem.inputAjax.calls.mostRecent().args; var callback = args[4]; - // Cannot find MathJax. - window.MathJax.Hub.getAllJax.and.returnValue([]); + // Simulate MathJax startup not ready yet. + window.MathJax.startup = null; spyOn(console, 'log'); callback({ @@ -302,22 +293,23 @@ describe('Formula Equation Preview', function() { }); // Tests. - expect(console.log).toHaveBeenCalled(); + expect(console.log).toHaveBeenCalledWith( + '[FormulaEquationInput] Oops no mathjax for ', 'THE_FORMULA' + ); - // We should look in the preview div for the MathJax. + // Preview span should contain the raw LaTeX. var previewElement = $('#input_THE_ID_preview')[0]; - expect(previewElement.firstChild.data).toEqual('\\(THE_FORMULA\\)'); + var mathSpan = previewElement.querySelector('.mathjax-preview'); + expect(mathSpan.textContent).toEqual('\\(THE_FORMULA\\)'); - // Refresh the MathJax. - expect(window.MathJax.Hub.Queue).toHaveBeenCalledWith( - ['Typeset', jasmine.any(Object), jasmine.any(Element)] - ); + // typesetClear/typesetPromise should NOT be called (fallback path). + expect(window.MathJax.typesetClear).not.toHaveBeenCalled(); + expect(window.MathJax.typesetPromise).not.toHaveBeenCalled(); }).always(done); }); it('displays errors from the server well', function(done) { var $img = $('img.loading'); - var jax = this.jax; formulaEquationPreview.enable(); jasmine.waitUntil(function() { @@ -329,16 +321,18 @@ describe('Formula Equation Preview', function() { error: 'OOPSIE', request_start: args[3].request_start }); - expect(window.MathJax.Hub.Queue).not.toHaveBeenCalled(); + expect(window.MathJax.typesetClear).not.toHaveBeenCalled(); expect($img.css('visibility')).toEqual('visible'); }).then(function() { jasmine.waitUntil(function() { - return window.MathJax.Hub.Queue.calls.count() > 0; + return window.MathJax.typesetPromise.calls.count() > 0; }).then(function() { - // Refresh the MathJax. - expect(window.MathJax.Hub.Queue).toHaveBeenCalledWith( - ['Text', jax, '\\text{OOPSIE}'] - ); + // Error text should be displayed. + var previewDiv = $('#input_THE_ID_preview')[0]; + var mathSpan = previewDiv.querySelector('.mathjax-preview'); + expect(window.MathJax.typesetClear).toHaveBeenCalledWith([mathSpan]); + expect(mathSpan.textContent).toEqual('\\(\\text{OOPSIE}\\)'); + expect(window.MathJax.typesetPromise).toHaveBeenCalledWith([mathSpan]); expect($img.css('visibility')).toEqual('hidden'); }).then(done); }); @@ -376,59 +370,70 @@ describe('Formula Equation Preview', function() { it('updates requests sequentially', function() { var $img = $('img.loading'); + var previewDiv = $('#input_THE_ID_preview')[0]; expect($img.css('visibility')).toEqual('visible'); this.callbacks[0](this.responses[0]); - expect(window.MathJax.Hub.Queue).toHaveBeenCalledWith( - ['Text', this.jax, 'THE_FORMULA_0'] - ); + expect(window.MathJax.typesetClear.calls.count()).toEqual(1); + expect(window.MathJax.typesetPromise.calls.count()).toEqual(1); + var mathSpan = previewDiv.querySelector('.mathjax-preview'); + expect(mathSpan.textContent).toEqual('\\(THE_FORMULA_0\\)'); expect($img.css('visibility')).toEqual('visible'); this.callbacks[1](this.responses[1]); - expect(window.MathJax.Hub.Queue).toHaveBeenCalledWith( - ['Text', this.jax, 'THE_FORMULA_1'] - ); + expect(window.MathJax.typesetClear.calls.count()).toEqual(2); + expect(window.MathJax.typesetPromise.calls.count()).toEqual(2); + expect(mathSpan.textContent).toEqual('\\(THE_FORMULA_1\\)'); expect($img.css('visibility')).toEqual('hidden'); }); it("doesn't display outdated information", function() { var $img = $('img.loading'); + var previewDiv = $('#input_THE_ID_preview')[0]; expect($img.css('visibility')).toEqual('visible'); // Switch the order (1 returns before 0) this.callbacks[1](this.responses[1]); - expect(window.MathJax.Hub.Queue).toHaveBeenCalledWith( - ['Text', this.jax, 'THE_FORMULA_1'] - ); + expect(window.MathJax.typesetClear.calls.count()).toEqual(1); + expect(window.MathJax.typesetPromise.calls.count()).toEqual(1); + var mathSpan = previewDiv.querySelector('.mathjax-preview'); + expect(mathSpan.textContent).toEqual('\\(THE_FORMULA_1\\)'); expect($img.css('visibility')).toEqual('hidden'); - window.MathJax.Hub.Queue.calls.reset(); + window.MathJax.typesetClear.calls.reset(); + window.MathJax.typesetPromise.calls.reset(); this.callbacks[0](this.responses[0]); - expect(window.MathJax.Hub.Queue).not.toHaveBeenCalled(); + expect(window.MathJax.typesetClear).not.toHaveBeenCalled(); + expect(window.MathJax.typesetPromise).not.toHaveBeenCalled(); expect($img.css('visibility')).toEqual('hidden'); }); it("doesn't show an error if the responses are close together", function(done) { + var previewDiv = $('#input_THE_ID_preview')[0]; + this.callbacks[0]({ error: 'OOPSIE', request_start: this.responses[0].request_start }); - expect(window.MathJax.Hub.Queue).not.toHaveBeenCalled(); + expect(window.MathJax.typesetClear).not.toHaveBeenCalled(); - // Error message waiting to be displayed + // Error message waiting to be displayed, then a good response arrives first. this.callbacks[1](this.responses[1]); - expect(window.MathJax.Hub.Queue).toHaveBeenCalledWith( - ['Text', this.jax, 'THE_FORMULA_1'] - ); - - // Make sure that it doesn't indeed show up later - window.MathJax.Hub.Queue.calls.reset(); + var mathSpan = previewDiv.querySelector('.mathjax-preview'); + expect(window.MathJax.typesetClear).toHaveBeenCalledWith([mathSpan]); + expect(mathSpan.textContent).toEqual('\\(THE_FORMULA_1\\)'); + expect(window.MathJax.typesetPromise).toHaveBeenCalledWith([mathSpan]); + + // Make sure that the error doesn't show up later. + window.MathJax.typesetClear.calls.reset(); + window.MathJax.typesetPromise.calls.reset(); jasmine.waitUntil(function() { return formulaEquationPreview.errorDelay * 1.1; }).then(function() { - expect(window.MathJax.Hub.Queue).not.toHaveBeenCalled(); + expect(window.MathJax.typesetClear).not.toHaveBeenCalled(); + expect(window.MathJax.typesetPromise).not.toHaveBeenCalled(); }).then(done); }); }); diff --git a/xblocks_contrib/problem/capa/static/js/capa/src/formula_equation_preview.js b/xblocks_contrib/problem/capa/static/js/capa/src/formula_equation_preview.js index ba0f7fc4..db533bd9 100644 --- a/xblocks_contrib/problem/capa/static/js/capa/src/formula_equation_preview.js +++ b/xblocks_contrib/problem/capa/static/js/capa/src/formula_equation_preview.js @@ -169,30 +169,38 @@ formulaEquationPreview.enable = function() { } function display(latex) { - MathJax.Hub.Startup.signal.Interest(function(message) { - if (message === 'End') { - var previewElement = inputData.$preview[0]; - MathJax.Hub.Queue(function() { - inputData.jax = MathJax.Hub.getAllJax(previewElement)[0]; - }); - - MathJax.Hub.Queue(function() { - // Check if MathJax is loaded - if (inputData.jax) { - // Set the text as the latex code, and then update the MathJax. - MathJax.Hub.Queue( - ['Text', inputData.jax, latex] - ); - } else if (latex) { - console.log('[FormulaEquationInput] Oops no mathjax for ', latex); - // Fall back to modifying the actual element. - var textNode = previewElement.childNodes[0]; - textNode.data = '\\(' + latex + '\\)'; - MathJax.Hub.Queue(['Typeset', MathJax.Hub, previewElement]); - } - }); + var previewElement = inputData.$preview[0]; + // Preserve only span.mathjax-preview and img.loading; drop everything else + var mathSpan = previewElement.querySelector('.mathjax-preview'); + var img = previewElement.querySelector('img.loading'); + var childNodes = previewElement.childNodes; + for (var i = childNodes.length - 1; i >= 0; i--) { + var node = childNodes[i]; + if (node.nodeType !== 1 || (node !== mathSpan && node !== img)) { + previewElement.removeChild(node); } - }); + } + if (!mathSpan) { + mathSpan = document.createElement('span'); + mathSpan.className = 'mathjax-preview'; + if (img) { + previewElement.insertBefore(mathSpan, img); + } else { + previewElement.appendChild(mathSpan); + } + } + if (typeof MathJax !== 'undefined' && MathJax.startup && MathJax.startup.promise) { + MathJax.startup.promise.then(function() { + MathJax.typesetClear([mathSpan]); + mathSpan.textContent = '\\(' + (latex || '') + '\\)'; + return MathJax.typesetPromise([mathSpan]); + }).catch(function(err) { + console.log('[FormulaEquationInput] MathJax error for "' + latex + '":', err); + }); + } else if (latex) { + console.log('[FormulaEquationInput] Oops no mathjax for ', latex); + mathSpan.textContent = '\\(' + latex + '\\)'; + } } if (response.error) {