MathJax icon indicating copy to clipboard operation
MathJax copied to clipboard

MathJax v4 error: this.parent(...) is null

Open hbghlyj opened this issue 2 years ago • 1 comments

I'm loading MathJax in a simple HTML webpage, via jsdelivr

<!DOCTYPE html>
<html>
<head>
  <script>window.MathJax = {
      startup: {
        pageReady: function () {
          MathJax.typesetPromise([document.body]);
          return MathJax.startup.defaultPageReady();
        }
      }
    };
  </script>
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/tex-mml-chtml.js"></script>
</head>

<body>
  <div>\(\mathbb{Q}\)
  </div>
</body>

</html>

Firefox throws error at tex-mml-chtml.js line 1 column 41740:

image

Uncaught (in promise) TypeError: this.parent(...) is null
    replace https://cdn.jsdelivr.net/npm/[email protected]/tex-mml-chtml.js:1
    updateDocument https://cdn.jsdelivr.net/npm/[email protected]/tex-mml-chtml.js:1
    updateDocument https://cdn.jsdelivr.net/npm/[email protected]/tex-mml-chtml.js:1
    updateDocument https://cdn.jsdelivr.net/npm/[email protected]/tex-mml-chtml.js:1
    updateDocument https://cdn.jsdelivr.net/npm/[email protected]/tex-mml-chtml.js:1
    updateDocument https://cdn.jsdelivr.net/npm/[email protected]/tex-mml-chtml.js:1
    methodActions https://cdn.jsdelivr.net/npm/[email protected]/tex-mml-chtml.js:1
    renderDoc https://cdn.jsdelivr.net/npm/[email protected]/tex-mml-chtml.js:1
    render https://cdn.jsdelivr.net/npm/[email protected]/tex-mml-chtml.js:1
    n https://cdn.jsdelivr.net/npm/[email protected]/tex-mml-chtml.js:1
    t https://cdn.jsdelivr.net/npm/[email protected]/tex-mml-chtml.js:1
    t https://cdn.jsdelivr.net/npm/[email protected]/tex-mml-chtml.js:1
    promise callback*t https://cdn.jsdelivr.net/npm/[email protected]/tex-mml-chtml.js:1
    t https://cdn.jsdelivr.net/npm/[email protected]/tex-mml-chtml.js:1
    promise callback*t https://cdn.jsdelivr.net/npm/[email protected]/tex-mml-chtml.js:1
    Mn https://cdn.jsdelivr.net/npm/[email protected]/tex-mml-chtml.js:1
    n https://cdn.jsdelivr.net/npm/[email protected]/tex-mml-chtml.js:1
    defaultPageReady https://cdn.jsdelivr.net/npm/[email protected]/tex-mml-chtml.js:1
    pageReady file:///this file.htm:9
    defaultReady https://cdn.jsdelivr.net/npm/[email protected]/tex-mml-chtml.js:1
    promise callback*e.defaultReady https://cdn.jsdelivr.net/npm/[email protected]/tex-mml-chtml.js:1
    defaultReady https://cdn.jsdelivr.net/npm/[email protected]/tex-mml-chtml.js:1
    <anonymous> https://cdn.jsdelivr.net/npm/[email protected]/tex-mml-chtml.js:1
    promise callback* https://cdn.jsdelivr.net/npm/[email protected]/tex-mml-chtml.js:1
    <anonymous> https://cdn.jsdelivr.net/npm/[email protected]/tex-mml-chtml.js:1
    loadFont https://cdn.jsdelivr.net/npm/[email protected]/tex-mml-chtml.js:1
    <anonymous> https://cdn.jsdelivr.net/npm/[email protected]/tex-mml-chtml.js:1
    <anonymous> https://cdn.jsdelivr.net/npm/[email protected]/tex-mml-chtml.js:1
    <anonymous> https://cdn.jsdelivr.net/npm/[email protected]/tex-mml-chtml.js:1
    <anonymous> https://cdn.jsdelivr.net/npm/[email protected]/tex-mml-chtml.js:1

Chrome also throws error: Uncaught (in promise) TypeError: Cannot read properties of null (reading 'replaceChild')

image Uncaught (in promise) TypeError: Cannot read properties of null (reading 'replaceChild')

hbghlyj avatar Nov 12 '23 20:11 hbghlyj

There are several things happening, here, and the main issue is an error with how some promises are being handled internally. There is also a difference between v3 and v4 in how this is handled.

It is important not to have two separate typeset operations going on simultaneously, because if the first one requires dynamically loaded code, it will pause and the second one will run during that pause, which can cause problems (for example, automatically assigned equation numbers can get out of order, or other global values can clash).

Because MathJax.typesetPromise() is performed within a promise, the code that follows it is performed before the typesetting starts. But MathJax.startup.defaultPageReady() also does a promise-based typeset operation (the initial typesetting of the page that is the default for MathJax). That means there are two typeset operations queued up, which can cause the problems I described above.

In v4, with its new larger font coverage, not all the font data is loaded initially, and the use of some characters causes that data to be loaded dynamically. The double-struck characters (like \mathbb{Q}) are ones that require extra data to be loaded. So your expression causes the bad situation that I mentioned where the first typeset operation must pause, and the second begins before the first is done. Although v3 doesn't have dynamic font-loading, you can cause this problem in v3 by using a dynamicaly loaded TeX command, like \cancel, so \cancel{x} instead of \mathbb{Q} would cause the same problem in v3. These issues are discussed in the documentation.

What's happening in your case is that the first typeset identifies \mathbb{Q} as the expression to typeset, but has to wait for the data for double-struck characters to be downloaded, so the second typeset starts, and it also finds the \mathbb{Q} (since it has not yet been replaced by its typeset version) and starts typesetting it. But it also needs to wait for the double-struck data, and so both typesets are paused. When the data are available, the first continues, finished the typesetting and replaces the initial LaTeX text with the typeset version. Then the second typesetting finishes and tries to replace the original text by its typeset version, but that original text is no longer in the document, so has no parent element, which is what your error message is complaining about.

The documentation linked above suggests using the MathJax.startup.promise to serialize your typeset calls. In v3, you had to do this manually, but in v4, this has been incorporated into the promise-based calls themselves, so they serialize automatically. Because the initial MathJax.startup.promise doesn't resolve until after the initial typesetting pass (that is, not until after the pageReady() function is performed and its promise is resolved), that means your MathJax.typesetPromise() will not be performed until after the MathJax.startup.defaultPageReady() function's promise is resolved. And that means that the MathJax.startup.defaultPageReady() is the first typeset to be performed, and MathJax.typesPromise() is the second one.

Because of the internal serialization in v4, your code should work, though in the reverse order of what you might expect. But there is a bug in MathJax.startup.defaultPageReady() that mishandles one of the promises. This line should be

            () => typesetPromise(CONFIG.elements) :

so that the typesetPromise() is not performed until the then() is triggered. Right now, it is performed immediately when defaultPagePromise() is called. (It took me quite a like two track that one down.)

One work-around for now would be of you to use

      pageReady: function () {
         return MathJax.startup.defaultPageReady().then(() => MathJax.typesetPromise([document.body]));
       }

instead, since the typesetPromise() isn't supposed to be performed until after the MathJax.startup.promise resolves, which isn't until after defaultPageReady() resolves.

Alternatively, you could do

      pageReady: function () {
         MathJax.typesetPromise([document.body]);
         return Promise.resolve();
       }

since the defaultPageReady() is not needed if you are already typesetting the entire document.

It's not clear to me what you are actually trying to accomplish with this pageReady() function, since the default action is to typeset the document body, so your MathJax.typesetPromise() call is redundant. I assume that this is not the actual code you are using, and that you have something else in mind.

If you want to have more precise control over the order in which things are typeset, it is probably best to set startup.typeset to false in the MathJax configuration object, and then make pageReady() do whatever typesetting needs to be done in the order you want it to be. E.g.

    window.MathJax = {
      startup: {
        typeset: false,
        pageReady: function () {
          MathJax.typesetPromise([element1]);
          MathJax.typesetPromise([element2]);
          return MathJax.startup.defaultPageReady();
        }
      }
    };

I will make a PR to fix the problem with the defaultPageReady() function.

dpvc avatar Nov 13 '23 15:11 dpvc