Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MathJAX Typeset Queue Error when loading new data into DOM (Cannot read property 'contains' of null) #2071

Closed
MathTV opened this issue Oct 24, 2018 · 8 comments

Comments

@MathTV
Copy link

MathTV commented Oct 24, 2018

My website uses a javascript function to load (via AJAX) math markup into the DOM and then render it with MathJAX.

If the function is called a second time (i.e., to load some new math markup into a DOM element):

  • All works well if MathJAX has finished processing its Typeset queue prior to the second function call.

  • An error appears if MathJAX has NOT finished processing its Typeset queue prior to the second function call.

    MathJax.js?config=TeX-MML-AM_CHTML&latest:19 Uncaught TypeError: Cannot read property 'contains' of null

Once the error occurs, MathJAX is in a bad state and will not function until entirely reloaded (e.g., via a page reload).

It seems like the easiest solution to this problem would be to clear the MathJAX Typeset queue before loading new math via AJAX, but I can find no method in the MathJAX API that clears the Typeset queue.

Here's a stripped-down version of the function (using jQuery syntax) I'm using:

window.loadNewMath = function() {
	// I want to clear the MathJAX Typeset queue before making the AJAX call

	$.ajax({
		dataType: "json",
		type: "POST",
		url: "/getNewMath",
		data: [],
		success: function(data) {
			if (data['success']) {
				// load math into DOM
				$("#math-panel").html(data.new_math);
				MathJax.Hub.Queue(["Typeset", MathJax.Hub, 'math-panel']);
			} else {
				$("#math-panel").html('oops. there was a database error');
			}
		},
		error: function(data) {
			$("#math-panel").html('oops. there was an ajax error');
		}
	})
}

This scenario seems like a common use case, so I hope someone there is a way of avoiding this error.

@dpvc
Copy link
Member

dpvc commented Oct 25, 2018

You are right that there is an issue if you make the second function call before MathJax has finished typesetting the first one. That is because you are replacing the portion of the DOM where MathJax is typesetting the math, so you have pulled rug out from under MathJax by removing the DOM nodes that it is working on (MathJax does retain pointers to some DOM nodes that is working with, so those will not be freed by the browser, but the parent nodes will, and that is a problem.

You are asking about clearing the "typeset" queue, but the queue is about more than just typesetting, so clearing the queue is not the right action (and even if you did, that doesn't stop the current action in the queue). You probably mean to ask about canceling a typesetting action that is already initiated, but you are right, there is no API for that (though it is possible to configure one). But even if there were, you would still be in trouble, because such a cancelation would not be immediate (things are operating asynchronously), so you would still ned to be careful how that was done.

The real issue is that you are not synchronizing the changing of the DOM with the typesetting operations. Once you have started a typesetting operation, you should not modify the DOM (at least where the typesetting is occurring) until the typesetting is done. One way to accomplish that is to queue your changes to the DOM as well as the typesetting. That way, the changes will be put off until any previous typesetting is finished. For example,

$.ajax({
	dataType: "json",
	type: "POST",
	url: "/getNewMath",
	data: [],
	success: function(data) {
		if (data['success']) {
			// load math into DOM
			MathJax.Hub.Queue(
				function () {$("#math-panel").html(data.new_math);},
				["Typeset", MathJax.Hub, 'math-panel']
			);
		} else {
			$("#math-panel").html('oops. there was a database error');
		}
	},
	error: function(data) {
		$("#math-panel").html('oops. there was an ajax error');
	}
})

should resolve the timing issue.

Note, however, that your ajax call may complete before the change to the DOM (and subsequent typeset) occurs. So if your ajax call could be made more quickly than MathJax can typeset the changes, this could lead to longer and longer delays between the requested changes and the resulting output on screen. In that case, you might need to be more sophisticated about your handling of the typeset calls. One way would be to keep track of whether a typeset operation is in progress and not queue additional ones until after the typeset is complete; keep track of the last new_math that has been requested (no need to typeset other ones that might have been requested while an earlier one was typeset), and when the typesetting has finished, set the new data and typeset again. You should be able to work out the logic for that.

The best solution, however, might be to disable whatever GUI controls allow the user to request new math during the time that MathJax is processing. Something like

MathJax.Hub.Queue(
	DisableGui,
	["Typeset", MathJax.Hub, 'math_panel'],
	EnableGui
);

where DisableGui() and EndableGui() are functions to disable/enable the controls should do the trick.

@MathTV
Copy link
Author

MathTV commented Oct 25, 2018

Thank you for your detailed reply!

I think I will indeed need to disable GUI controls until the typesetting is complete (at least until I can find a better solution).

Just for the record, I think this is a pretty common use case:

  • user clicks UI button that loads content including math markup via AJAX call and writes it to a DOM element; MathJAX typesetting is initiated;
  • user clicks another UI button to load different content including math markup via AJAX call and writes it to the same DOM element; MathJAX typesetting is initiated;

In our case, the user is navigating the sections of an online math textbook (e.g.: https://www.xyztextbooks.com/ebook/title/college_algebra), and forcing them to wait for all Math to be typeset by MathJAX before they can navigate to another section is not a good solution. We have (in another project) used KaTeX which allows us to pre-render the Math on a server (running NodeJS), and I suppose we could move in that direction (though I'd rather stick with MathJAX).

I'm interested in what you mean when you say "there is no API for that (though it is possible to configure one)". I would dearly like to have a method for clearing out pending MathJAX tasks prior to modifying the DOM.

@dpvc
Copy link
Member

dpvc commented Oct 26, 2018

We have (in another project) used KaTeX which allows us to pre-render the Math on a server (running NodeJS), and I suppose we could move in that direction (though I'd rather stick with MathJAX).

MathJax v3 (currently in beta) is a complete rewrite of MathJax, and it currently operates synchronously (so no queuing needed). You may find that this is easier to work with in the long rung (though there are still features that are being worked on). It also operates in NodeJS as easily as in a browser, so that could be used for back-end processing.

There is also the mathjax-node project that is a server-side implementation of MathJax v2. That is something you could use now if pre-processing by MathJax is a goal for you.

I would dearly like to have a method for clearing out pending MathJAX tasks prior to modifying the DOM.

What I had in mind was a means of telling MathJax to cancel the current typeset operation (which is not the same as clearing the queue, but is more likely to be what you actually need). I wrote the following extension for another project, which implements that:

(function () {
    var HUB = MathJax.Hub;

    if (!HUB.Cancel) {

        HUB.cancelTypeset = false;
        var CANCELMESSAGE = "MathJax Canceled";

        HUB.Register.StartupHook("HTML-CSS Jax Config", function () {
            var HTMLCSS = MathJax.OutputJax["HTML-CSS"],
                TRANSLATE = HTMLCSS.Translate.bind(HTMLCSS);
            HTMLCSS.Augment({
                Translate: function (script, state) {
                    if (HUB.cancelTypeset || state.cancelled) {
                        throw Error(CANCELMESSAGE)
                    }
                    return TRANSLATE(script, state);
                }
            });
        });

        HUB.Register.StartupHook("SVG Jax Config", function () {
            var SVG = MathJax.OutputJax["SVG"],
                TRANSLATE = SVG.Translate.bind(SVG);
            SVG.Augment({
                Translate: function (script, state) {
                    if (HUB.cancelTypeset || state.cancelled) {
                        throw Error(CANCELMESSAGE)
                    }
                    return TRANSLATE(script, state);
                }
            });
        });

        HUB.Register.StartupHook("CommonHTML Jax Config", function () {
            var CHTML = MathJax.OutputJax.CommonHTML,
                TRANSLATE = CHTML.Translate.bind(CHTML);
            CHTML.Augment({
                Translate: function (script, state) {
                    if (HUB.cancelTypeset || state.cancelled) {
                        throw Error(CANCELMESSAGE);
                    }
                    return TRANSLATE(script, state);
                }
            });
        });

        HUB.Register.StartupHook("PreviewHTML Jax Config", function () {
            var PHTML = MathJax.OutputJax.PreviewHTML,
                TRANSLATE = PHTML.Translate.bind(PHTML);
            PHTML.Augment({
                Translate: function (script, state) {
                    if (HUB.cancelTypeset || state.cancelled) {
                        throw Error(CANCELMESSAGE);
                    }
                    return TRANSLATE(script, state);
                }
            });
        });

        HUB.Register.StartupHook("TeX Jax Config", function () {
            var TEX = MathJax.InputJax.TeX,
                TRANSLATE = TEX.Translate.bind(TEX);
            TEX.Augment({
                Translate: function (script, state) {
                    if (HUB.cancelTypeset || state.cancelled) {
                        throw Error(CANCELMESSAGE)
                    }
                    return TRANSLATE(script, state);
                }
            });
        });

        var PROCESSERROR = HUB.processError.bind(HUB);
        HUB.processError = function (error, state, type) {
            if (error.message !== CANCELMESSAGE) {
                return PROCESSERROR(error, state, type)
            }
            MathJax.Message.Clear(0, 0);
            state.jaxIDs = [];
            state.jax = {};
            state.scripts = [];
            state.i = state.j = 0;
            state.cancelled = true;
            return null;
        };

        HUB.Cancel = function () {
            this.cancelTypeset = true;
        };
    }
})();

This implements a function MathJax.Hub.Cancel() that tells MathJax to stop typesetting at the earliest opportunity. Note that it is not necessarily immediate, but as soon as possible. So you still need to use the queue to manage your DOM changes, but you should not have to wait for the entire previous DOM to be processed.

You can also keep track of whether MathJax currently is typesetting, and hold off on changes until the cancel goes through. Something like the following (untested) code:

var mathPending = false;
var newMath = '';

function TypesetPendingMath() {
  if (mathPending) {
    $("#math-panel").html(newMath);
    newMath = '';
    mathPending = false;
    MathJax.Hub.cancelTypeset = false;
    MathJax.Hub.Queue(
        ["Typeset", MathJax.Hub. 'math-panel'],
        TypesetPendingMath
     );
  }
}

function ChangeMath(dom) {
  MathJax.Hub.Cancel();
  newMath = dom;
  if (!pendingMath) {
    pendingMath = true;
    MathJax.Hub.Queue(TypesetPendingMath);
  }
}

This provides a function ChangeMath() that allows you to specify new HTML containing math content. If there is no typesetting currently going on, the DOM will be modified and typeset. Otherwise, if we are in the middle of typesetting, we cache the change and cancel the current typesetting (as soon as possible). If the math changes again before previous typesetting is done, the old change is discarded and the new one cached.

The key to making it work is the TypesetPendingMath() function, which checks if there was cached math to be typeset, and if so, it sets the DOM to the cached math, clears the pending and cancel flags, then starts typesetting the math. The critical step is that it also queues another call to TypesetPendingMath(). If the user hasn't done anything while the typesetting is occurring, when the typesetting is finished, the pendingMath flag is false, and nothing more needs to be done. But if the user requested a change in the math, the pendingMath math flag will have been set (and the math cached in newMath) and the current typesetting operation would have been requested to cancel. In that case, the typesetting will stop as soon as it can, and the queued TypesetPendingMath() call will run. Since pendingMath is true in this case, the cached math will be put in place, the flags will be cleared, and it will be typeset. Afterward, we check if another change has been requested, and make the change and typeset again, otherwise, we just end.

Although I haven't testing this specific incarnation of the code, I have certainly used this technique in the past, and it should get you on the right path, even if it is not quite right as it stands.

@dpvc
Copy link
Member

dpvc commented Oct 26, 2018

PS, if you don't want to support all the output formats, you can remove the unwanted blocks from the cancel code above.

@MathTV
Copy link
Author

MathTV commented Oct 31, 2018

Thanks, again, for taking time to provide a detailed analysis of the situation!

I'm currently testing a much simpler solution that seems to be working:

  • make ajax call to get new math markup
  • disable navigation controls in the UI
  • write the new Math markup to the DOM
  • add the Typeset call to the MathJAX Queue and leverage the Typeset's callback to re-enable the UI controls when typesetting is complete.

Every time I've used this code, and I monitor MathJAX signals in the browser's javascript console, the events occur in the order I want (i.e., typesetting tasks are all completed and then the callback to re-enable UI controls is run).

Here's sample code:

window.loadNewMath = function() {
  $.ajax({
    dataType: "json",
    type: "POST",
    url: "/getNewMath",
    data: [],
    success: function(data) {
      if (data['success']) {

        // disable navigation controls in UI (they are reenabled after mathjax processing, if any)
        enableEbookNavigationControls(false);

        // load math into DOM
        $("#math-panel").html(data.new_math);

        // queue MathJAX Typeset (with callback to re-enable the UI controls)
        MathJax.Hub.Queue(
          function() {
            MathJax.Hub.Typeset('video-panel', function() {
              // re-enable nav controls when typesetting is complete
              enableEbookNavigationControls(true);
            }); 
          }
        );
      } else {
        $("#math-panel").html('oops. there was a database error');
      }
    },
    error: function(data) {
      $("#math-panel").html('oops. there was an ajax error');
    }
  })
}

@dpvc
Copy link
Member

dpvc commented Nov 1, 2018

Yes, your solution should properly synchronize the changes with MathJax. It is essentially what I had recommended at the end of my first response above; I had suggested pushing the re-enable command on the queue rather than use the Typeset callback, but these are essentially equivalent. So you could do this without the additional closure via

      if (data['success']) {
        enableEbookNavigationControls(false);
        $("#math-panel").html(data.new_math);
        MathJax.Hub.Queue(
          ["Typeset", MathJax.Hub, 'video-panel'],
          [enableEBookNavigationControls, true]
        );
      }

@MathTV
Copy link
Author

MathTV commented Nov 1, 2018

Hmm.. Right you are! In my early tests, I could have sworn that the syntax you recommended resulted in things happening in an undesired order, but I just re-ran the tests (observing the signals), and it all works as predicted.

Thanks again for all your help!

@dpvc
Copy link
Member

dpvc commented Nov 1, 2018

No problem. Thanks for reporting your findings. I'm closing this issue, then.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants