mathjax / MathJax

Beautiful and accessible math in all browsers
http://www.mathjax.org/
Apache License 2.0
10.18k stars 1.16k forks source link

`MathDocument.convert()` does not associate a `MathItem` with the `MathDocument` #2814

Open rhansen opened 2 years ago

rhansen commented 2 years ago

Issue Summary

When I follow the documentation to convert a math string to HTML or SVG, no MathItem is associated with the MathDocument (as far as I can tell). In particular, MathJax.startup.document.getMathItemsWithin() does not return anything for the element or its containers.

Is this intentional? If so, how do I access the MathItem?

Steps to Reproduce:

https://jsfiddle.net/9rLhjskz/

<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-svg.js"></script>
<script>
  const elt = MathJax.tex2svg('e^{i\\pi}=-1');
  document.body.append(elt);
  const [mathItem] = MathJax.startup.document.getMathItemsWithin(document.body);
  if (mathItem == null) alert('no MathItem!');
</script>

I get the same result if I instead use MathDocument.convert() directly:

<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-svg.js"></script>
<script>
  const elt = MathJax.startup.document.convert('e^{i\\pi}=-1');
  document.body.append(elt);
  const [mathItem] = MathJax.startup.document.getMathItemsWithin(document.body);
  if (mathItem == null) alert('no MathItem!');
</script>

Technical details:

dpvc commented 2 years ago

The MathDocument.convert() method uses a temporary MathItem to do its work, and it is not retained after the conversion is complete. Since the mathml2...() methods are basically just shells around convert(), that applies to them as well.

Can you say more about the use-case you have in mind?

The convert() method is implemented here and you could certainly make a version of it that returns the MathItem instead. It could be slimmed down if you know the options you intend to be using, for example.

rhansen commented 2 years ago

The MathDocument.convert() method uses a temporary MathItem to do its work, and it is not retained after the conversion is complete.

Is there a reason the MathItem isn't attached to the MathDocument like other MathItems are?

Can you say more about the use-case you have in mind?

I am interested in creating a plugin for Etherpad that uses MathJax. Due to limitations in the way Etherpad works, it is currently not feasible to let MathJax manipulate the DOM. Instead, the plugin would hook into Etherpad at these points:

I'm sure I can find workarounds for this issue, such as saving a copy of the math string in a data-* attribute on the DOM element returned from MathDocument.convert(), or adding a custom RenderAction to the MathDocument, but such workarounds feel like I'm holding MathJax wrong. Also, I'm not sure if references would work.

dpvc commented 2 years ago

Is there a reason the MathItem isn't attached to the MathDocument like other MathItems are?

Yes. The MathItem list in the MathDocument represents the active math items in the document itself, for use with tools that need to process all the math expressions on the page (like the assistive tools), and to allow the math menu to work (e.g., get the TeX or MathML versions, change the renderer, and so on). Since the value of convert() is not placed in the document, it is not part of the MathDocument math list. If you add the result of convert() to the page, MathJax does not know about that, and you have to manage that yourself.

I am interested in creating a plugin for Etherpad that uses MathJax.

OK, that looks interesting, but I'm not sure I understand your bullet points. At a quick glance, Etherpad seems to be a collaborative environment, and you are trying to share the mathematics. My confusion lies in how the sharing is done. Are you intending that everyone in the collaboration will have your plugin in order for it to work, or is it supposed to work if only one person has it? If everyone does, then are you planning to pass the original TeX code to the collaborators' plugins and have MathJax render it separately for each, or are you passing a serialized DOM representation among the collaborators?

My suggestion would be to do the former, if you can, as the latter may introduce more problems (e.g., you need to maintain the CSS style sheet that MathJax uses as well as the DOM elements for the typeset math).

Your first bullet point seems to be adequately handled by convert() as that does convert a TeX input sting to DOM elements.

The second bullet point I don't understand. Inserting a new DOM element where? Are you talking about changing a math expression's output? Or are you talking about putting the MathJax output into the Etherpad DOM?

For the third point, it seems to me that transferring the original TeX and having it typeset at each endpoint might be more efficient. (Less to transfer, and less to manage as you don't have to worry about CSS updates or CSS id collisions or other internal issues.) I don't know Etherpad's internals so I can't tell you if that is feasible or not.

If all you need from the MathItem is the original TeX string, then I think your idea of adding a data attribute is probably the way to go (or you could append a <script type="text/x-tex">...</script> to the <mjx-container> that would store the TeX format). Alternatively, you can track the math yourself with your own list of TeX strings and use a data attribute to give an index into the list.

For the last bullet, the usual approach is to remove the original and replace it with new output. MathJax version 2 used to have a means of changing the math associated with a particular typeset expression, but it was not very frequently used, and was not retained in version 3. For your use case, calling convert() on the new TeX string and replacing the existing output seems the natural approach, here. Alternatively, if you have a MathItem whose typeset version is in the DOM, then changing the MathItem.math value and calling MathItem.rerender() would be one way to do it. But since you aren't having MathJax insert into the DOM, the former method seems better.

I think a custom renderAction to add the needed data attribute or script tag would be a good way to handle that.

rhansen commented 2 years ago

Is there a reason the MathItem isn't attached to the MathDocument like other MathItems are?

Yes. The MathItem list in the MathDocument represents the active math items in the document itself, for use with tools that need to process all the math expressions on the page (like the assistive tools), and to allow the math menu to work (e.g., get the TeX or MathML versions, change the renderer, and so on). Since the value of convert() is not placed in the document, it is not part of the MathDocument math list. If you add the result of convert() to the page, MathJax does not know about that, and you have to manage that yourself.

I don't quite understand this line of reasoning. The output of convert() is a DOM element; what is the use of that DOM element if not to insert it into the page? And if it is inserted into the page, why shouldn't it be part of the MathDocument associated with the page? Why shouldn't it benefit from integration with assistive tools, right-click menu, renderer changes, etc.?

I am interested in creating a plugin for Etherpad that uses MathJax.

OK, that looks interesting, but I'm not sure I understand your bullet points. At a quick glance, Etherpad seems to be a collaborative environment, and you are trying to share the mathematics.

Yes.

My confusion lies in how the sharing is done. Are you intending that everyone in the collaboration will have your plugin in order for it to work, or is it supposed to work if only one person has it?

Everyone will have the plugin code because the Etherpad admin installs plugins on the server. A plugin can modify the server-side behavior and/or inject client-side JavaScript to modify client-side behavior. Everyone who visits a pad (document) will get the same client-side code, so everyone will have the same client-side MathJax plugin logic if the MathJax plugin is installed on the server.

If everyone does, then are you planning to pass the original TeX code to the collaborators' plugins and have MathJax render it separately for each, or are you passing a serialized DOM representation among the collaborators?

Pass the original TeX code. (That's what I meant by "math string annotated with metadata that means "this string is MathJax display-mode TeX".)

My suggestion would be to do the former, if you can, as the latter may introduce more problems (e.g., you need to maintain the CSS style sheet that MathJax uses as well as the DOM elements for the typeset math).

Agree 100%.

Your first bullet point seems to be adequately handled by convert() as that does convert a TeX input sting to DOM elements.

The second bullet point I don't understand. Inserting a new DOM element where? Are you talking about changing a math expression's output? Or are you talking about putting the MathJax output into the Etherpad DOM?

I'm referring to the user's actions that lead to the creation of a new DOM element containing a rendered math expression in the pad. This can be done in a few different ways. Some examples:

For the third point, it seems to me that transferring the original TeX and having it typeset at each endpoint might be more efficient. (Less to transfer, and less to manage as you don't have to worry about CSS updates or CSS id collisions or other internal issues.) I don't know Etherpad's internals so I can't tell you if that is feasible or not.

The third point is about taking a newly inserted math item (or a newly modified existing math item) and extracting the TeX from it so that it can be sent over the network to the other users.

Etherpad works by monitoring the DOM for changes and doing the following when it detects a change:

  1. Normalize the DOM elements. (Plugins can hook into this.)
  2. Convert the DOM into an internal representation. (Plugins can hook into this.)
  3. Compute a delta between the previous revision of the internal representation and the new revision.
  4. Transmit the delta to the server.

Step 2 is where I would need to be able to get the original TeX string from the DOM element returned by convert().

It may seem weird to go from TeX string to DOM element back to TeX string, but splitting it up this way makes it possible for users to copy and paste an existing math expression element, or import an HTML file containing rendered math expressions.

If all you need from the MathItem is the original TeX string, then I think your idea of adding a data attribute is probably the way to go (or you could append a <script type="text/x-tex">...</script> to the <mjx-container> that would store the TeX format).

Out of curiosity, why doesn't MathJax do this? My understanding of MathJax is limited, but if MathJax stored the original math string in the rendered elements (e.g., MathItem.math was a getter/setter for the data-math attribute) then would it still need to keep track of any state (other than caches)? It seems that MathJax would be able to simply scan the DOM for all mjx-container elements and reconstruct MathItems from those.

Alternatively, you can track the math yourself with your own list of TeX strings and use a data attribute to give an index into the list.

For the last bullet, the usual approach is to remove the original and replace it with new output. MathJax version 2 used to have a means of changing the math associated with a particular typeset expression, but it was not very frequently used, and was not retained in version 3. For your use case, calling convert() on the new TeX string and replacing the existing output seems the natural approach, here. Alternatively, if you have a MathItem whose typeset version is in the DOM, then changing the MathItem.math value and calling MathItem.rerender() would be one way to do it. But since you aren't having MathJax insert into the DOM, the former method seems better.

I think a custom renderAction to add the needed data attribute or script tag would be a good way to handle that.

OK. Thanks for the help!