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 V2 vs V3 rendering #3095

Open
SakshamGambhir opened this issue Aug 24, 2023 · 8 comments
Open

MathJax V2 vs V3 rendering #3095

SakshamGambhir opened this issue Aug 24, 2023 · 8 comments
Labels
Accepted Issue has been reproduced by MathJax team Code Example Contains an illustrative code example, solution, or work-around Merged Merged into develop branch Test Needed v3
Milestone

Comments

@SakshamGambhir
Copy link

SakshamGambhir commented Aug 24, 2023

Hi,

We have identified a difference in the output of the MathML tags with v2 and v3 for the same equation.

With v2:
image

With v3:
image

Issue: There is a space after i(superscript), G(subscript), and C(subscript) in v2, whereas in v3 the space has been removed.

MathML Equation:

<mml:math display="block"><mml:msub><mml:mrow><mml:mo movablelimits="false">Hom</mml:mo></mml:mrow><mml:mrow><mml:mi mathvariant="italic">G</mml:mi></mml:mrow></mml:msub><mml:mo mathvariant="normal" fence="true" stretchy="false">(</mml:mo><mml:mi mathvariant="italic">V</mml:mi><mml:mo mathvariant="normal">,</mml:mo><mml:mi mathvariant="italic">W</mml:mi><mml:mo mathvariant="normal" fence="true" stretchy="false">)</mml:mo><mml:msup><mml:mrow><mml:mo stretchy="false">↪</mml:mo></mml:mrow><mml:mrow><mml:mi mathvariant="italic">i</mml:mi></mml:mrow></mml:msup><mml:msub><mml:mrow><mml:mo movablelimits="false">Hom</mml:mo></mml:mrow><mml:mrow><mml:mi mathvariant="double-struck">C</mml:mi></mml:mrow></mml:msub><mml:mo mathvariant="normal" fence="true" stretchy="false">(</mml:mo><mml:mi mathvariant="italic">V</mml:mi><mml:mo mathvariant="normal">,</mml:mo><mml:mi mathvariant="italic">W</mml:mi><mml:mo mathvariant="normal" fence="true" stretchy="false">)</mml:mo><mml:mover><mml:mrow><mml:mo movablelimits="false">⟶</mml:mo></mml:mrow><mml:mrow><mml:mi mathvariant="normal">Φ</mml:mi></mml:mrow></mml:mover><mml:msub><mml:mrow><mml:mo movablelimits="false">Hom</mml:mo></mml:mrow><mml:mrow><mml:mi mathvariant="italic">G</mml:mi></mml:mrow></mml:msub><mml:mo mathvariant="normal" fence="true" stretchy="false">(</mml:mo><mml:mi mathvariant="italic">V</mml:mi><mml:mo mathvariant="normal">,</mml:mo><mml:mi mathvariant="italic">W</mml:mi><mml:mo mathvariant="normal" fence="true" stretchy="false">)</mml:mo><mml:msup><mml:mrow><mml:mo stretchy="false">↪</mml:mo></mml:mrow><mml:mrow><mml:mi mathvariant="italic">i</mml:mi></mml:mrow></mml:msup><mml:msub><mml:mrow><mml:mo movablelimits="false">Hom</mml:mo></mml:mrow><mml:mrow><mml:mi mathvariant="double-struck">C</mml:mi></mml:mrow></mml:msub><mml:mo mathvariant="normal" fence="true" stretchy="false">(</mml:mo><mml:mi mathvariant="italic">V</mml:mi><mml:mo mathvariant="normal">,</mml:mo><mml:mi mathvariant="italic">W</mml:mi><mml:mo mathvariant="normal" fence="true" stretchy="false">)</mml:mo><mml:mo mathvariant="normal">,</mml:mo></mml:math>

Is there any reason for the difference that has been observed?

Thanks,
Saksham Gambhir

@dpvc
Copy link
Member

dpvc commented Aug 27, 2023

Here's what's happening. By default, MathJax uses TeX spacing rules rather than MathML spacing rules, and TeX determines spacing by applying one of eight TeX classes to every item in the expression, and uses the pair of classed to determine the spacing between adjacent items. When MathJax processes an <mo> node, it looks up the operator in the MathML operator dictionary to find the TeX class to assign it, but if it doesn't appear in the dictionary, it gets TeX class REL (for relation). Because Hom is not in the dictionary, it gets TeX class REL via this rule. Note that the arrows in your expression also are TeX class REL. TeX sets the spacing between two relations to zero (so that something like <= will have to space between the two characters), which is why you are getting no space there.

In version 2 of MathJax, there was an exception made to the rule for mo nodes that aren't in the dictionary: if it is a multi-letter name (like Hom), then it would get TeX class OP (for operator) rather than REL. That rule was not included in v3 (an oversight). I have made a pull request to add that in.

In the meantime, there are several possible approaches you could take. First, you could configure MathJax's output to use MathML spacing rather than TeX spacing, which would include space around any operator that is not in the dictionary.

Another possibility would be to change <mo movablelimits="false">Hom</mo> to <mi>Hom</mi>, which is probably a better encoding anyway. It would be possible to add a MathML filter to the MathML input jax during MathJax's setup that would looks through the expression for such <mo> nodes and convert them to <mi>.

Another possibility would be to add Hom to the dictionary with TeX class OP so that MathJax could find it and apply the needed TeX class automatically, though if you use other operators, they would each have to be added to the dictionary separately.

Finally, you could use the following configuration to patch to add the special rule for multi-letter operators.

MathJax = {
  startup: {
    ready() {
      const {mo} = MathJax._.core.MmlTree.MML.MML;
      const {getRange} = MathJax._.core.MmlTree.OperatorDictionary;
      const {TEXCLASS} = MathJax._.core.MmlTree.MmlNode;
      mo.prototype.checkOperatorTable = function (mo) {
        let [form1, form2, form3] = this.handleExplicitForm(this.getForms());
        this.attributes.setInherited('form', form1);
        let OPTABLE = this.constructor.OPTABLE;
        let def = OPTABLE[form1][mo] || OPTABLE[form2][mo] || OPTABLE[form3][mo];
        let noTexClass = this.getProperty('texClass') === undefined;
        if (def) {
          if (noTexClass) {
            this.texClass = def[2];
          }
          for (const name of Object.keys(def[3] || {})) {
            this.attributes.setInherited(name, def[3][name]);
          }
          this.lspace = (def[0] + 1) / 18;
          this.rspace = (def[1] + 1) / 18;
        } else {
          let limits = this.attributes.get('movablelimits');
          let isOP =  !!mo.match(/^[a-zA-Z]{2,}$/);
          let range = getRange(mo);
          if (range) {
            if (noTexClass) {
              this.texClass = (isOP && range[2] === TEXCLASS.REL) || limits ? TEXCLASS.OP : range[2];
            }
            const spacing = this.constructor.MMLSPACING[range[2]];
            this.lspace = (spacing[0] + 1) / 18;
            this.rspace = (spacing[1] + 1) / 18;
          } else if (noTexClass) {
            this.texClass = isOP || limits ? TEXCLASS.OP : TEXCLASS.REL;
          }
        }
      };
      MathJax.startup.defaultReady();
    }
  }
};

I think that will improve the situation for you.

You might also want to add stretchy="false" to the long right arrow, as right now it is shrinking to a smaller size that is sufficient to cover the capital phi above it.

@dpvc dpvc added the Code Example Contains an illustrative code example, solution, or work-around label Aug 27, 2023
@SakshamGambhir
Copy link
Author

Hi

Thank You for providing the in-depth details.

I have a couple of points to ask for the above solutions provided.

  1. How do we configure MathJax's output to use MathML? is there any specific property we need to add to the mathjax-config.js?
  2. How/Where do we add words to the dictionary with TeX class OP?
  3. We tried to add the patch code in the mathjax.config.js as below, but this didn't help as we observed no change even after adding the code.

image

Best,
Saksham Gambhir

@SakshamGambhir
Copy link
Author

Hi Team,

Could you please update us on the queries posted above?

Best,
Saksham Gambhir

@dpvc
Copy link
Member

dpvc commented Sep 12, 2023 via email

@dpvc dpvc added this to the v4.0 milestone Sep 15, 2023
dpvc added a commit to mathjax/MathJax-src that referenced this issue Sep 15, 2023
Set TeX class OP for multi-letter mo elements, as in v2.  (mathjax/MathJax#3095)
@dpvc dpvc added Merged Merged into develop branch and removed Ready for Review labels Sep 15, 2023
@SakshamGambhir
Copy link
Author

Hi,

Just to confirm that we are able to view the updated code and even after clearing the browser cache, we are not able to view the updates we are expecting. As suggested please find the attached mathjax-config file under which I have added the provided solution. Please review and let us know if it still requires more changes to incorporate the changes we are looking for.

Also, suggest a way to add words to the dictionary with TeX class OP, and how do we configure MathJax's output to use MathML? is there any specific property we need to add to the mathjax-config.js?

Best,
Saksham Gambhir
mathjax-config.zip

@dpvc
Copy link
Member

dpvc commented Oct 12, 2023

Just to confirm that we are able to view the updated code and even after clearing the browser cache, we are not able to view the updates we are expecting.

Does this mean that the mathjax-config.js file that you see in your browser does not include the changes that you have made and show in the screen-shot above? If that is the case, then you may need to check that you have actually updated the mathjax-config.js file is in the correct place on the server.

If you mean that the changes are in the mathjax-config.js file that you see in your browser, but the output from MathJax is not as you expect it to be, then that suggests that your mathajx-config.js file is not being processed at the correct time.

I have used your mathjax-config.js file verbatim, only removing the call to document.mathInputProcessed();, and it works for me as expected in this test file:

<!DOCTYPE html>
<html xmlns:mml="http://www.w3.org/1998/Math/MathML">
<head>
<title>Test configuration file</title>
<script src="mathjax-config.js"></script>
<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/mml-chtml.js"></script>
</head>
<body>

<mml:math display="block">
  <mml:msub>
    <mml:mrow>
      <mml:mo movablelimits="false">Hom</mml:mo>
    </mml:mrow>
    <mml:mrow>
      <mml:mi mathvariant="italic">G</mml:mi>
    </mml:mrow>
  </mml:msub>
  <mml:mo mathvariant="normal" fence="true" stretchy="false">(</mml:mo>
  <mml:mi mathvariant="italic">V</mml:mi>
  <mml:mo mathvariant="normal">,</mml:mo>
  <mml:mi mathvariant="italic">W</mml:mi>
  <mml:mo mathvariant="normal" fence="true" stretchy="false">)</mml:mo>
  <mml:msup>
    <mml:mrow>
      <mml:mo stretchy="false"></mml:mo>
    </mml:mrow>
    <mml:mrow>
      <mml:mi mathvariant="italic">i</mml:mi>
    </mml:mrow>
  </mml:msup>
  <mml:msub>
    <mml:mrow>
      <mml:mo movablelimits="false">Hom</mml:mo>
    </mml:mrow>
    <mml:mrow>
      <mml:mi mathvariant="double-struck">C</mml:mi>
    </mml:mrow>
  </mml:msub>
  <mml:mo mathvariant="normal" fence="true" stretchy="false">(</mml:mo>
  <mml:mi mathvariant="italic">V</mml:mi>
  <mml:mo mathvariant="normal">,</mml:mo>
  <mml:mi mathvariant="italic">W</mml:mi>
  <mml:mo mathvariant="normal" fence="true" stretchy="false">)</mml:mo>
  <mml:mover>
    <mml:mrow>
      <mml:mo movablelimits="false" stretchy="false"></mml:mo>
    </mml:mrow>
    <mml:mrow>
      <mml:mi mathvariant="normal">Φ</mml:mi>
    </mml:mrow>
  </mml:mover>
  <mml:msub>
    <mml:mrow>
      <mml:mo movablelimits="false">Hom</mml:mo>
    </mml:mrow>
    <mml:mrow>
      <mml:mi mathvariant="italic">G</mml:mi>
    </mml:mrow>
  </mml:msub>
  <mml:mo mathvariant="normal" fence="true" stretchy="false">(</mml:mo>
  <mml:mi mathvariant="italic">V</mml:mi>
  <mml:mo mathvariant="normal">,</mml:mo>
  <mml:mi mathvariant="italic">W</mml:mi>
  <mml:mo mathvariant="normal" fence="true" stretchy="false">)</mml:mo>
  <mml:msup>
    <mml:mrow>
      <mml:mo stretchy="false"></mml:mo>
    </mml:mrow>
    <mml:mrow>
      <mml:mi mathvariant="italic">i</mml:mi>
    </mml:mrow>
  </mml:msup>
  <mml:msub>
    <mml:mrow>
      <mml:mo movablelimits="false">Hom</mml:mo>
    </mml:mrow>
    <mml:mrow>
      <mml:mi mathvariant="double-struck">C</mml:mi>
    </mml:mrow>
  </mml:msub>
  <mml:mo mathvariant="normal" fence="true" stretchy="false">(</mml:mo>
  <mml:mi mathvariant="italic">V</mml:mi>
  <mml:mo mathvariant="normal">,</mml:mo>
  <mml:mi mathvariant="italic">W</mml:mi>
  <mml:mo mathvariant="normal" fence="true" stretchy="false">)</mml:mo>
  <mml:mo mathvariant="normal">,</mml:mo>
</mml:math>

</body>
</html>

I have formatted the MathML to make it easier to read, and added stretchy="false" to the long right arrow, as I recommended above. Please try this file and see if that works for you. The output for me is

formula

Because this does work for me, I suspect that the issue may be that your configuration is not being processed at the right time. When I mentioned that your screen shot of a snippet of the configuration was not enough to diagnose the problem, I actually meant that we needed to see not only the full configuration file, but also how it is loaded, where it is in your HTML file in comparison to the script that loads MathJax itself, and perhaps other details about your workflow in the browser. The configuration doesn't seem to be the issue.

Did you do the tests that I recommended above? Namely, inserting a console.log() call inside the checkOperatorTable() call to be sure that it is actually being used? I suspect it is not.

[Please] suggest a way to add words to the dictionary with TeX class OP

Here is a configuration that does that for several function names:

MathJax = {
  startup: {
    ready() {
      const {OPTABLE} = MathJax._.core.MmlTree.OperatorDictionary;
      const {TEXCLASS} = MathJax._.core.MmlTree.MmlNode;
      const OP = [1, 2, TEXCLASS.OP];
      [
        'sin', 'cos', 'tan',
        'log', 'ln',
        'Hom'
      ].forEach(fn => OPTABLE.infix[fn] = OP);
      MathJax.startup.defaultReady();
    }
  }
};

You can add more names to the list of functions to be added to the operator dictionary.

how do we configure MathJax's output to use MathML?

I'm a little confused by the question. MathJax v3 and above has two output formats: CommonHTML (CHTML) and SVG. It does not have a native MathML output mode, as v2 did. It does have a MathML input mode, but not an output one.

Since your math is already in MathML format, why would you need MathJax to produce MathML (your math is already MathML). So I'm not quite sure what you are asking for, here.

@SakshamGambhir
Copy link
Author

SakshamGambhir commented Nov 22, 2023

Hi Team,

Thank You for replying back on the queries we have raised so far.

As we are still not able to resolve the issue as per the output we are expecting here are some more pointers to be addressed:

First, I would like to get a confirmation on 1 point from the above example -> "http://www.w3.org/1998/Math/MathML"
The above-mentioned schema is used for rendering according to V2 of MathML - I'm I correct on this point? As I observed this in the above example you have shared. If Yes, we are facing issues in V3 and once we process using V3 the spaces get removed.

Also, we want to execute the conversion as soon as the mathJax is initialized, so we have kept document.mathInputProcessed(); under mathjax-config.js as startup: { ready: function() {document.mathInputProcessed();} } - The original file was shared earlier.

We also added the patch provided by you and wrote console.log() along with it to see if the code is updated and not cached, but the patch is not working as expected even code was updated.

Another point, In the very first comment you mentioned: you could configure MathJax's output to use MathML spacing rather than TeX spacing, which would include space around any operator that is not in the dictionary. - how do we configure and check we are using MathML spacing instead of TeX spacing?

Note: we are looking for a solution that can be implemented from the code side and not from the content (as making any change in the content is not possible for us)

Best,
Saksham Gambhir

@dpvc
Copy link
Member

dpvc commented Nov 24, 2023

As we are still not able to resolve the issue as per the output we are expecting

You didn't respond to whether my test file above (in the October 12th comment) works for you. Does that work or not?

The configuration that changes the OPTABLE should also have fixed the issue. If that didn't work for you, then can you provide an actual example file that includes it that doesn't work for you? It is not enough to simply say "it doesn't work", as I've tested both and they work for me, so there must be something different about what we are doing. I can't tell whether you have made the changes properly without seeing what you are actually doing.


The above-mentioned schema is used for rendering according to V2 of MathML ... ?

Both MathJax v2 and v3 are based on the MathML v3 specification, not MathML v2.


we are facing issues in V3 and once we process using V3 the spaces get removed

Yes, I indicated above why that is the case, and it is a bug in v3. I have made a fix for it that will be included in the next release, and gave you a patch (Aug 27th) that should resolve it for you. I have tested the patch, and it works for me. If that is not working for you, then you need to provide a complete page that illustrated the problem you are having, as there is something that you are doing differently from me. Saying it doesn't work without giving me what you are actually doing (not just the configuration, but the rest of the code that interacts with MathJax) gives me nothing to go on. I used your mathjax-config.js file unchanged except for the removal of document.mathInputProcessed(), which you didn't provide (so I could not test with that), and it works for me on the expression you provided. If that is not working for you, you need to provide a complete example page showing that.


we want to execute the conversion as soon as the mathJax is initialized, so we have kept document.mathInputProcessed(); under mathjax-config.js as startup: { ready: function() {document.mathInputProcessed();} }

MathJax may be initialized before the page is ready to be processed. That is, the ready() function may run before the page content is in place, so if you try to typeset then, you may be doing so before the math is actually available. E.g., if you used the async attribute on the script tag that loads MathJax, then the ready() function may run as soon as MathJax has been loaded, which can happen at any point as your document is being processed. You may either need to use defer instead of async, or use the pageReady() function instead of ready(), since that runs when both MathJax is initialized and the page contents are ready to be processed.

Note also that MathJax will typeset the page automatically as soon as it can, unless to set typeset: false in the startup section of the MathJax configuration. It is not clear whether you have set this, as you only give a screen shot of a portion of your configuration. It is also not clear what your mathInputProcessed() function is doing, since you don't provide it.

I'm assuming that startup: { ready: function() {document.mathInputProcessed();} } is just a short-hand version, not what you are actually doing, as without the MathJax.startup.defaultReady() being called in ready(), you will get no typesetting.


We also added the patch provided by you and wrote console.log() along with it

It is not clear from your comment whether that console log was performed or not: "the patch is not working as expected" could mean that log message didn't appear, or it could mean that it did but that your output still was not what you wanted. Can you clarify that?


how do we configure and check we are using MathML spacing instead of TeX spacing

As described in the documentation, you should combine

MathJax = {
  chtml: {
    mathmlSpacing: true;
  },
  svg: {
    mathmlSpacing: true;
  }
};

into you configuration to have MathJax use MathML spacing rules rather than TeX spacing rules.

You can test this by using

<math xmlns="http://www.w3.org/1998/Math/MathML" display="block">
  <mi>x</mi>
  <mo>+</mo>
  <mo>=</mo>
  <mo>-</mo>
  <mi>y</mi>
</math>

and compare the output to when you don't have mathmlSpacing set. With mathmlspaling: true, you should see

mml-spacing

and without it (or with mathmlspaling: false) you will see

tex-spacing

making any change in the content is not possible for us

Note that MathJax allows you to add pre-filters that can be used to modify the MathML before MathJax processes it, so if there is problematic MathML in your original source files, it may still be possible to fix it on the fly as MathJax typesets it. For example, I had suggested adding stretchy="false" to the long-right arrow; it would be possible to use a pre-filter to look through the MathML for <mo> elements with content equal to U+27F6 (long right arrow), and add the needed attribute. Adding

MathJax = {
  startup: {
    ready() {
      MathJax.startup.defaultReady();
      MathJax.startup.document.inputJax[0].mmlFilters.add(function ({document, data}) {
        const adaptor = document.adaptor;
        for (const mo of adaptor.tags(data, 'mo')) {
          if (adaptor.textContent(mo) === '\u27F6') {
            adaptor.setAttribute(mo, 'stretchy', false);
          }
        }
      });
    }
  }
};

to your configuration will accomplish that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Accepted Issue has been reproduced by MathJax team Code Example Contains an illustrative code example, solution, or work-around Merged Merged into develop branch Test Needed v3
Projects
None yet
Development

No branches or pull requests

2 participants