MathJax icon indicating copy to clipboard operation
MathJax copied to clipboard

v4 svg manual typeset removes equations

Open hurshore opened this issue 2 years ago • 3 comments

Issue Summary

MathJax SVG manual typeset removes equations instead of typesetting them. I am implementing some typing animation, where I call MathJax.typeset() after an equation is typed. But instead of the equations being typeset, they are removed. It worked fine in v3.2.2.

Output

https://github.com/mathjax/MathJax/assets/56561245/8517f4f2-d9ca-40b5-88a7-d581198019d9

Expected Output

Equations should be typeset when MathJax.typeset() is called.

Technical details:

  • MathJax Version: 4.0.0-beta.4
  • Client OS: Android 13

I am using the default MathJax configuration and loading MathJax via

<script src="https://cdn.jsdelivr.net/npm/[email protected]/tex-svg.js"></script>

hurshore avatar Feb 11 '24 15:02 hurshore

There is not enough information here to provide a meaningful answer. In particular, you don't indicate how your animation is being updated, and how you are calling MathJax in relation to that, so it is hard to tell what might be going wrong. Also, are there any error messages showing up in the browser console?

I'm curious what the purpose of watching this typing is supposed to be. Having worked with 300-baud modem connections, I can tell you that waiting for output to occur like this gets tedious very quickly. And seeing the LaTeX notation being typed in seems unusual unless what you are illustrating is what typing an answer in an LMS looks like.

dpvc avatar Feb 12 '24 14:02 dpvc

I get a couple of errors. The first time MathJax.typeset() is called, I see the following error:

Uncaught Error: MathJax retry
    at Object.Bn [as retryAfter] (tex-svg.js:1:125846)
    at Tn.enrich (tex-svg.js:1:1543892)
    at t.enrich (tex-svg.js:1:1545748)
    at Object.renderDoc (tex-svg.js:1:115187)
    at hn.renderDoc (tex-svg.js:1:115299)
    at t.render (tex-svg.js:1:116747)
    at Ei.typeset (tex-svg.js:1:33894)
    at index.js:25:39

On subsequent calls of MathJax.typeset(), I see the following error:

Error: <svg> attribute width: Expected length, "NaNex".

Or sometimes

tex-svg.js:1 Error: <svg> attribute viewBox: Expected number, "0 -833.9 NaN NaN".

The following error also appears:

tex-svg.js:1 Uncaught TypeError: Cannot read properties of null (reading 'replaceChild')
    at Pi.replace (tex-svg.js:1:41922)
    at Tn.updateDocument (tex-svg.js:1:126504)
    at Tn.updateDocument (tex-svg.js:1:1565708)
    at t.updateDocument (tex-svg.js:1:119124)
    at t.updateDocument (tex-svg.js:1:130949)
    at t.updateDocument (tex-svg.js:1:1534253)
    at Object.renderDoc (tex-svg.js:1:115187)
    at hn.renderDoc (tex-svg.js:1:115299)
    at t.render (tex-svg.js:1:116747)
    at Ei.typeset (tex-svg.js:1:33894)

This is a link to the implementation: https://jsbin.com/rulafotusu/edit?html,js,console,output

hurshore avatar Feb 12 '24 17:02 hurshore

OK, there are a number of problems with your code, but the main one is the line

    explanation.textContent += nextChar;

This replaces the content of explanation with a new text node containing the current text plus a new character, removing any previous content. Since the typeset contents is contained in an SVG element, and is not text, it is removed when you replace the explanation text content, and all you are left with is the text that is around the math, but with no math. Since MathJax keeps track of where its entries are stored in the web page (via pointers to the text nodes where the math was found), and since you are removing the original text node and replacing it by a new one, MathJax no longer has access to the DOM nodes where the math is supposed to be displayed. That is the source of the second, third and fourth error messages you are seeing. (The ones about NaN are due to the fact that MathJax can not measure the metrics of the surrounding font once the text node where MathJax found the math is no longer in the DOM when you replace it when the next letter shows up.)

The first message is due to the fact that MathJax needs to load information about the font being used, and that is an asynchronous process. MathJax uses a retry error to mediate that asynchronous loading, and that means your code needs to take that into account. The easiest way is to use the typesetPromise() function rather than typeset(), as the former uses promises to let you know when the math has been typeset.

Here is a rewrite of your code that handles these issues properly.

const text = `
To solve a simple quadratic equation such as \\( ax^2 + bx + c = 0 \\), we can use the
quadratic formula: \\( x = \\frac{{-b \\pm \\sqrt{{b^2 - 4ac}}}}{{2a}} \\). For example,
for the equation \\( 2x^2 + 4x - 6 = 0 \\), we can substitute \\( a = 2 \\), \\( b = 4 \\),
and \\( c = -6 \\) into the formula. This gives us
\\( x = \\frac{{-4 \\pm \\sqrt{{4^2 - 4 \\cdot 2 \\cdot (-6)}}}}{{2 \\cdot 2}} \\), which
simplifies to \\( x = \\frac{{-4 \\pm \\sqrt{{64}}}}{{4}} \\). Therefore, the solutions
are \\( x = \\frac{{-4 + 8}}{{4}} = 1 \\) and \\( x = \\frac{{-4 - 8}}{{4}} = -3 \\). Thus,
the equation \\( 2x^2 + 4x - 6 = 0 \\) has two real roots: \\( x = 1 \\) and \\( x = -3 \\).
`;
let i = 0;
let inLatex = false;

//
//  Rather than use a look-ahead when you see a '\\' character, we use a look-behind on '(' and ')'.
//  This is accomplished by passing the previous character to typeWriter so that it can be checked
//  as part of the delimiter detection.  This allows us to run MathJax when the close delimiter is detected
//  rather than using setTimeout() to allow the ')' to the added on the next pass.
//
function typeWriter(prevChar = '') {
  if (i >= text.length) return;
  const speed = 50;
  const nextChar = text[i++];
  //
  //  Rather than replace explanation.textContent, which wipes out the previous content,
  //  we append a new text node containing the next character, and normalize the content
  //  in order to join adjacent text nodes.
  //
  explanation.appendChild(document.createTextNode(nextChar));
  explanation.normalize();
  //
  //  If we are in LaTeX mode and find the close delimiter,
  //     Call MathJax to typeset using a promise, and after that
  //     resolves, we start the next typeWriter() call.  That guarantees
  //     that typing doesn't continue until after MathJax completes,
  //     even if it has to do asynchronous file loading.
  //     We return after the promise call so that we don't run the
  //     rest of the function.
  //
  if (inLatex && prevChar === '\\' && nextChar === ')') {
    inLatex = false;
    window.MathJax.typesetPromise().then(() => {    
      setTimeout(() => typeWriter(nextChar), speed);
    });
    return;
  }
  //
  //  If we see an open delimiter, we start LaTeX mode.
  //
  if (!inLatex && prevChar === '\\' && nextChar === '(') {
    inLatex = true;
  }
  //
  //  Start the typeWriter() call with the current character.
  //
  setTimeout(() => typeWriter(nextChar), speed);
}

const explanation = document.querySelector('.explanation');

typeWriter();

dpvc avatar Feb 16 '24 21:02 dpvc