MathJax icon indicating copy to clipboard operation
MathJax copied to clipboard

Speech generation error for empty mtr

Open TylerZeroMaster opened this issue 4 months ago • 3 comments

Issue Summary

When math input for tex-mml-svg/MathJax.mathml2svgPromise contains at least one empty mtr and speech is enabled, a speech generation error is logged to the console (not thrown), and speechis not generated.

Steps to Reproduce:

  1. Follow steps to load tex-mml-svg component in node
  2. Call MathJax.mathml2svgPromise with sample math and display: false
  3. See error logged in console

Sample math that is affected by this issue:

  <m:math xmlns="http://www.w3.org/1999/xhtml" xmlns:m="http://www.w3.org/1998/Math/MathML" display="inline">
    <m:semantics>
      <m:mrow>
        <m:mrow data-sm="./modules/m82530/index.cnxml:873:42">
          <m:mspace width="4em" data-sm="./modules/m82530/index.cnxml:873:50" />
          <m:mtable data-sm="./modules/m82530/index.cnxml:873:73">
            <!-- Empty mtr -->
            <m:mtr data-sm="./modules/m82530/index.cnxml:873:83" />
            <m:mtr data-sm="./modules/m82530/index.cnxml:873:91">
              <m:mtd columnalign="right" data-sm="./modules/m82530/index.cnxml:873:98">
                <m:msup data-sm="./modules/m82530/index.cnxml:873:125">
                  <m:mi data-sm="./modules/m82530/index.cnxml:873:133">n</m:mi>
                  <m:mn data-sm="./modules/m82530/index.cnxml:873:147">2</m:mn>
                </m:msup>
                <m:mo data-sm="./modules/m82530/index.cnxml:873:170">+</m:mo>
                <m:mi data-sm="./modules/m82530/index.cnxml:873:184">n</m:mi>
              </m:mtd>
              <m:mtd columnalign="left" data-sm="./modules/m82530/index.cnxml:873:206">
                <m:mo data-sm="./modules/m82530/index.cnxml:873:232">=</m:mo>
              </m:mtd>
              <m:mtd columnalign="left" data-sm="./modules/m82530/index.cnxml:873:254">
                <m:mn data-sm="./modules/m82530/index.cnxml:873:280">132</m:mn>
              </m:mtd>
            </m:mtr>
          </m:mtable>
        </m:mrow>
      </m:mrow><m:annotation-xml encoding="MathML-Content">
        <m:mrow data-sm="./modules/m82530/index.cnxml:873:42">
          <m:mspace width="4em" data-sm="./modules/m82530/index.cnxml:873:50" />
          <m:mtable data-sm="./modules/m82530/index.cnxml:873:73">
            <m:mtr data-sm="./modules/m82530/index.cnxml:873:83" />
            <m:mtr data-sm="./modules/m82530/index.cnxml:873:91">
              <m:mtd columnalign="right" data-sm="./modules/m82530/index.cnxml:873:98">
                <m:msup data-sm="./modules/m82530/index.cnxml:873:125">
                  <m:mi data-sm="./modules/m82530/index.cnxml:873:133">n</m:mi>
                  <m:mn data-sm="./modules/m82530/index.cnxml:873:147">2</m:mn>
                </m:msup>
                <m:mo data-sm="./modules/m82530/index.cnxml:873:170">+</m:mo>
                <m:mi data-sm="./modules/m82530/index.cnxml:873:184">n</m:mi>
              </m:mtd>
              <m:mtd columnalign="left" data-sm="./modules/m82530/index.cnxml:873:206">
                <m:mo data-sm="./modules/m82530/index.cnxml:873:232">=</m:mo>
              </m:mtd>
              <m:mtd columnalign="left" data-sm="./modules/m82530/index.cnxml:873:254">
                <m:mn data-sm="./modules/m82530/index.cnxml:873:280">132</m:mn>
              </m:mtd>
            </m:mtr>
          </m:mtable>
        </m:mrow>
      </m:annotation-xml>
    </m:semantics>
  </m:math>
  • At first I thought this was more closely related to SRE, however, when I run SRE.toSpeech on the sample math, it works.
  • I believe this is a bug because removing the empty mtr element fixes the issue; however, empty mtr is valid MathML.
  • I would expect the speech to generate normally, as it does when the mtr elements have been removed, without any errors appearing in the console. ("1 lines Line 1: n squared plus n equals 132")

Technical details:

  • MathJax Version: 4.0.0
  • Client OS: Mac OS X 14.7.8
  • Browser: N/A (liteDOM adaptor)
  • Node Version: 22.16.0

I am using the following MathJax configuration:

{
  loader: {
    paths: { mathjax: 'mathjax' },
    load: [ 'adaptors/liteDOM', 'output/svg' ],
    require: [Function: require]
  },
  output: { linebreaks: { inline: true }, font: 'mathjax-stix2' },
  tex: {
    formatError (_, error) {
      throw new Error(error.message)
    }
  },
  startup: {
    ready () {
      // https://github.com/mathjax/MathJax/issues/3185
      const { MmlMath } = MathJax._.core.MmlTree.MmlNodes.math
      const { MmlMstyle } = MathJax._.core.MmlTree.MmlNodes.mstyle
      MmlMath.defaults.scriptsizemultiplier = MmlMstyle.defaults.scriptsizemultiplier = 0.8

      MathJax.startup.defaultReady()
    }
  },
  options: { sre: { locale: 'en', braille: 'nemeth' } },
  svg: { fontCache: 'local', blacker: 0, scale: 1.15 }
}

and loading MathJax via

global.MathJax = this.config // (the above config)
require(this.lib) // (e.g. tex-mml-svg)
await MathJax.startup.promise

Supporting information:

The specific error logged is:

TypeError: Cannot read properties of undefined (reading 'mathmlTree')
    at Rp.applicable (<node_modules>/mathjax/tex-mml-svg.js:1:1687515)
    at Object.run (<node_modules>/mathjax/tex-mml-svg.js:1:1635743)
    at kp.tableToMultiline (<node_modules>/mathjax/tex-mml-svg.js:1:1635908)
    at Ip.table_ (<node_modules>/mathjax/tex-mml-svg.js:1:1672675)
    at Ip.parse (<node_modules>/mathjax/tex-mml-svg.js:1:1671311)
    at Ip.parseList (<node_modules>/mathjax/tex-mml-svg.js:1:1631689)
    at Ip.rows_ (<node_modules>/mathjax/tex-mml-svg.js:1:1671840)
    at Ip.parse (<node_modules>/mathjax/tex-mml-svg.js:1:1671311)
    at Ip.rows_ (<node_modules>/mathjax/tex-mml-svg.js:1:1671688)
    at Ip.parse (<node_modules>/mathjax/tex-mml-svg.js:1:1671311)
Speech generation error: Cannot read properties of null (reading 'indexOf')

Removing the empty mtr resolves the issue (but it does create a slight difference visually). Right now, to avoid the visual differences, I am using a separate version of the mathml for speech generation in these cases.

// first try normally
// ... MathJax bits ...
// Speech generation error is logged, but no error is thrown
// Try to get speech from node
filterNode(adaptor, node, speech, braille);
// Use lack of speech to detect error condition
if (speech.length === 0 && isMml) {
  const parsed = adaptor.parse(item);
  const toRemove = [];
  let mathNode;
  const filterMath = (node) => {
    const { kind, children } = node;
    if (mathNode === undefined && kind.endsWith('math')) mathNode = node;
    if (Array.isArray(children) && children.length > 0) children.forEach(filterMath);
    else if (kind.endsWith('mtr')) toRemove.push(node);
  };
  filterMath(parsed.body);
  toRemove.forEach(adaptor.remove.bind(adaptor));
  if (mathNode) {
    const speechReadyItem = adaptor.outerHTML(mathNode);
    const speechNode = await MathJax.mathml2svgPromise(speechReadyItem, options);
    filterNode(adaptor, speechNode, speech, braille);
  }
}
// ... and so on

I do not think this problem is in my configuration or usage, but the most relevant part of the script would be here if anyone is looking for more context.

TylerZeroMaster avatar Oct 21 '25 21:10 TylerZeroMaster

Thanks for the report. You are correct that it is not your configuration or usage.

@zorkow, here is a minimal example. The empty row must be followed by a row with at least two entries.

<math xmlns="http://www.w3.org/1999/xhtml">
  <mtable>
    <mtr />
    <mtr>
      <mtd></mtd>
      <mtd></mtd>
    </mtr>
  </mtable>
</math>

@TylerZeroMaster, your code includes the comment

// Speech generation error is logged, but no error is thrown

Note that the speech component has a speechError() configuration parameter that you can use to trap any speech errors. See the documentation, though it is incorrectly referred to as enrichError() lower down on that page.

dpvc avatar Oct 21 '25 23:10 dpvc

PS, an easier mechanism for removing the empty mtr nodes would be to incorporate

  MathJax = {
    mml: {
      mmlFilters: [
        function ({data}) {
          Array.from(data.querySelectorAll('mtr:empty')).forEach((mtr) => mtr.remove());
        },
      ],
    }
 };

into your MathJax configuration.

dpvc avatar Oct 21 '25 23:10 dpvc

One of SRE's heuristics did not consider the case that it might come across and empty row. PR 827 fixes this.

zorkow avatar Oct 23 '25 09:10 zorkow