Skip to content
Open
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
20 changes: 20 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,26 @@ Change Log
Unreleased
**********

0.17.1 - 2026-06-29
**********************************************

Fixed
=====

* Upgrade MathJax from v2.7.5 to v4.1.2 across Problem, Discussion,
and Html XBlocks.
* Replace MathJax.Hub.Queue/Callback API with v4 typesetPromise,
typesetClear, and startup.promise with readiness guards.
* Preserve loading spinner icon in formula equation preview by
using a stable span.mathjax-preview child element.
* Use getElementById instead of CSS selectors for IDs containing
characters like ``:`` or ``.``.
* Replace jQuery ID selector writes to hidden dynamath fields with
getElementById guarded assignments.
* Remove v2-specific config (disableFastPreview, Hub.signal.Interest,
mathjaxRunning flag).
* Update JS test stubs and assertions for v4 API shape.

0.17.0 - 2026-06-11

Changed
Expand Down
2 changes: 1 addition & 1 deletion xblocks_contrib/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@
from .video import VideoBlock
from .word_cloud import WordCloudBlock

__version__ = "0.17.0"
__version__ = "0.17.1"
Original file line number Diff line number Diff line change
@@ -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);
}
8 changes: 6 additions & 2 deletions xblocks_contrib/discussion/assets/static/js/common/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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]]));
}
};

Expand Down
2 changes: 1 addition & 1 deletion xblocks_contrib/discussion/assets/static/js/customwmd.js
Original file line number Diff line number Diff line change
Expand Up @@ -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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
if (MathJax.Hub.Browser.isMSIE) {
if (window.navigator && /msie|trident/i.test(window.navigator.userAgent)) {
block = block.replace(/(%[^\n]*)\n/g, "$1<br/>\n");
}
for (i = j = ref = start + 1, ref1 = last; ref <= ref1 ? j <= ref1 : j >= ref1; i = ref <= ref1 ? ++j : --j) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@

MathJaxDelayRenderer.prototype.maxDelay = 3000;

MathJaxDelayRenderer.prototype.mathjaxRunning = false;

MathJaxDelayRenderer.prototype.elapsedTime = 0;

MathJaxDelayRenderer.prototype.mathjaxDelay = 0;
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand Down
12 changes: 11 additions & 1 deletion xblocks_contrib/html/html.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
47 changes: 28 additions & 19 deletions xblocks_contrib/problem/assets/spec/display_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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("<MathML>");
MathJax.typesetPromise.and.returnValue(Promise.resolve());
window.update_schematics = function () {};
spyOn(SR, "readText");
spyOn(SR, "readTexts");
Expand Down Expand Up @@ -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());

Expand Down Expand Up @@ -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"));
});
Expand Down Expand Up @@ -999,38 +1006,40 @@ 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("<MathML>");
});

describe("when there is no exception", function () {
beforeEach(function () {
this.problem.updateMathML(this.stubbedJax, $("#input_example_1").get(0));
});

it("convert jax to MathML", () => expect($("#input_example_1_dynamath")).toHaveValue("<MathML>"));
it("convert jax to MathML via startup.toMML", function () {
expect(MathJax.startup.toMML).toHaveBeenCalledWith(this.stubbedJax.root);
expect($("#input_example_1_dynamath")).toHaveValue("<MathML>");
});
});

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();
});
});
});
Expand Down
Loading