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

Does a render action have access to the previous state of the math object on a rerender? #3177

Open salbeira opened 8 months ago

salbeira commented 8 months ago

Is your feature request related to a problem? Please describe. As this is more of a question than anything else, the relation to a problem is described in additional context.

Describe the solution you'd like Some way to identify what part of a math object had a certain attribute injected into it and reapply the same attribute on the element replacing the old one on a rerender.

Describe alternatives you've considered None.

Additional context We are using a custom plugin for Reveal.js that injects data to math formulas so they can be fragmented and displayed piece by piece. When MathJax rerenders a formula if you for example activate the "Accessibility" from the Math Menu, the DOM elements get recreated. This removes any "data-fragment-index" attributes that former "fragment" items had. As such a rerender of Math Formulas causes the order of elements on a slide to appear in the wrong order. In order to fix this one would have to find out what elements had what "data-fragment-index" attribute before the rerender and apply the exact same attribute to the elements that correspond to their ancestor (in time, not in DOM). This can be arbitrarily deep inside a Math Object. We are only using the SVG renderer.

dpvc commented 8 months ago

Does a render action have access to the previous state of the math object on a rerender?

I suppose the answer depends on where your renderAction is in the list of actions, and what part of the information you are interested in using. For example, I suspect that the old typesetRoot is probably still in place when the current typeset action is called, but I would not count on that, as it could change in the future.

On the other hand, there are two properties that can be used to store arbitrary information for the MathItem: inputData and outputData. So you could use mathitem.inputdata.dataFragments to store your index information (it could be an object that collects the data on the individual fragments).

You don't say much about the renderAction that you have defined, and where it runs. It sounds like it is running after the TYPESET state, and operates on the DOM elements themselves. You might consider moving it earlier in the action list, and have it operate on the internal MathML rather than the DOM elements. You can add data attributes to the MathML and those will be preserved in the output. Since re-rendering the MathItem doesn't re-compile the internal MathML, any changes you made there will be retained during a rerender (an accessibility change doesn't recompile, if I remember correctly).

Alternatively, you could define your own TeX commands that insert the needed attributes (including the needed id numbers) into the internal MathML representation, and then they will not be lost on retypesetting. You could insert them in a pre-processing step, via a TeX input jax pre-filter for example, or a renderAction that runs before the COMPILE state, which could update the MathItem's math property so they are always the same when retypeset.

Without more information, it is hard to give you something more precise.

salbeira commented 8 months ago

There are two ways we add the fragment class to parts of formulas:

Either by surrounding parts of the formula in \fragment{} . I am not familiar enough with how MathJax resolves this but this works out of the box without us adding anything:

$$\begin{eqnarray}
    \dot{\vec{u}} &=& 
    \fragment{-\vec{u}\cdot\grad\vec{u}}
    \fragment{\;-\; \frac{1}{\rho}\grad p}
    \fragment{\;+\; \nu \laplace \vec{u}}
    \fragment{\;+\; \vec{f}} 
    \label{eq:momentum} \\[2mm]
    \grad \cdot \vec{u} &=& 0 
  \end{eqnarray}$$

The other way is we embedd a formula inside a DOM element that has the .math-incremental class:

 [$$
  \begin{eqnarray*}
  a &=& b \\
  a^2 &=& ab \\
  2a^2 &=& a^2 + ab \\
  2a^2-2ab &=& a^2 - ab \\
  2a(a-b) &=& a (a-b) \\
  2a &=& a \\
  2 &=& 1
  \end{eqnarray*}
  $$]{ .math-incremental }

We then use a render action to attach the .fragment class to each line:

function incrementalItem(item, doc) {
  const root = item.typesetRoot;
  if (root && root.closest(".math-incremental")) {
    for (let mrow of root.querySelectorAll(
      'g[data-mml-node="mtable"]:first-of-type > g[data-mml-node="mtr"]'
    )) {
      mrow.classList.add("fragment");
    }
    for (let mrow of document.querySelectorAll(
      'g[data-mml-node="mtable"]:first-of-type g[data-mml-node="mlabeledtr"]'
    )) {
      mrow.classList.add("fragment");
    }
  }
}

The issue is far less the .fragment class, it is that after typesetting and when Reveal is ready, it adds the data-fragment-index to the DOM elements with the .fragment class. Therefore we can not modify the MML, because the addition of the index happens post typesetting by a system we do not have control over.

dpvc commented 8 months ago

Thanks for the additional information. If the addition of the index happens through a system you don't have control over, I'm not sure what you have in mind for resolving this. Are you wanting to add the indices yourself and not mark the result with the fragment class? Would that work, or does fragment have to be there? And if so, will the system override your index when it process the fragment class.

I'm just not quite sure I understand what you want to be done.

Note that there are several reasons that an expression could be re-rendered, and some of them involve the expression being changed (e.g., via an maction node that selects a different part of the expression to be shown). So trying to connect indices from previous HTML DOM nodes to the current ones may not be effective. But if you are not using the action extension, it would probably work.

You could save the current DOM tree as item.outputData.oldRoot = item.typesetRoot at the end of your renderAction, and when it starts, it checks if item.outputData.oldRoot exists, and if so, walks both that three and item.typesetRoot node by node and if the oldRoot node has the index attribute, transfer it to the typesetRoot node. You might want to do some checking that the number of children are the same in both cases, and the node types are the same, just to be sure, but that could be used to do it.