MathJax icon indicating copy to clipboard operation
MathJax copied to clipboard

Aligned Overset

Open FelixBenning opened this issue 9 months ago • 13 comments

Is your feature request related to a problem? Please describe.

It is often nice to write something over the equal sign of an equation, perhaps a reference to a theorem, another equation, etc. which justifies a particular equality. This is what the \overset command accomplishes. When the equal signs are aligned however, then the notes above the equal sign mess with the alignment. For example:

$$ \begin{align} \|x-y\|^2 &= \|x\|^2 + 2\langle x,y\rangle + \|y\|^2 \ &\overset{x\perp y}= \|x\|^2 + \|y\|^2 \end{align} $$

\begin{align}
\|x-y\|^2
&= \|x\|^2 + 2\langle x,y\rangle + \|y\|^2
\\
&\overset{x\perp y}= \|x\|^2 + \|y\|^2
\end{align}

In LaTeX there is this wonderful package aligned-overset that fixes this problem by allowing the alignment & to be placed within the overset, i.e. \overset{x\perp y}&{=} is valid. The & then causes alignment on the second argument of overset.

Describe the solution you'd like

In my notes I started to heavily rely on this LaTeX feature and would be delighted if Mathjax would offer something similar. The package aligned-overset is extremely small. The overset and underset implementation are each only a single page, copied into the documentation.

Describe alternatives you've considered Manual mathclaps or mathllaps can often work. But this is a lot of manual fiddling. It also means that I cannot use the same code for mathjax as for LaTeX

e.g. here I use mathclap

$$ \begin{align} \|x-y\|^2 &= \|x\|^2 + 2\langle x,y\rangle + \|y\|^2 \ &\overset{\mathclap{x\perp y}}= \|x\|^2 + \|y\|^2 \end{align} $$

\begin{align}
\|x-y\|^2
&= \|x\|^2 + 2\langle x,y\rangle + \|y\|^2
\\
&\overset{\mathclap{x\perp y}}= \|x\|^2 + \|y\|^2
\end{align}

observe that this causes the elements on the right of the equation to creep towards the equal sign. If the content is wider you have to add phantoms. mathllap actually works worse:

$$ \begin{align} \|x-y\|^2 &= \|x\|^2 + 2\langle x,y\rangle + \|y\|^2 \ &\overset{\mathllap{x\perp y}}= \|x\|^2 + \|y\|^2 \end{align} $$

FelixBenning avatar May 16 '25 08:05 FelixBenning

Here is a configuration you can use to obtain the result you are looking for.

MathJax = {
  tex: {packages: {'[+]': ['aligned-overset']}},
  startup: {
    ready() {
      const {MacroMap} = MathJax._.input.tex.SymbolMap;
      const {Configuration} = MathJax._.input.tex.Configuration;
      const TexParser = MathJax._.input.tex.TexParser.default;
      const ParseUtil = MathJax._.input.tex.ParseUtil.default;
      const NodeUtil = MathJax._.input.tex.NodeUtil.default;
      new MacroMap('aligned-overset', {
        overset: ['AlignSet', 'mover', 'accent'],
        underset: ['AlignSet', 'munder', 'accentunder'],
      }, {
        AlignSet(parser, name, mtype, atype) {
          const script = parser.ParseArg(name);
          const align = parser.GetNext() === '&';
          if (align) {
            parser.parse('character', [parser, '&']);
            parser.i++;
          }
          const base = parser.ParseArg(name);
          const mo = script.coreMO();
          const accent = mo.isKind('mo') && NodeUtil.getMoAttribute(mo, 'accent') === true;
          ParseUtil.checkMovableLimits(base);
          let node = parser.create('node', mtype, [base, script], { [atype]: accent });
          if (align) {
            node = parser.create('node', 'mrow', [
              parser.create('node', 'mpadded', [
                parser.create('node', 'mphantom', [
                  parser.create('node', 'TeXAtom', [base.copy()], { texClass: 0 })
                ])
              ], { width: '50%' }),
              parser.create('node', 'mpadded', [node], { width: '50%', lspace: '-50%width' })
            ]);
          }
          parser.Push(node);
        }
      });
      Configuration.create('aligned-overset', {
        handler: {macro: ['aligned-overset']}
      });
      MathJax.startup.defaultReady();
    }
  }
};

This redefines \overset and \underset to a new function that looks for & after the first argument and if it is there, it uses an mpadded MathML node with width at 50% and shifted left by 50% around the overset, then puts an mpadded with width 50% around an mphantom containing a copy of the base. This means that the result has the width of the base on the left and the width of the entire overset on the right. So the base is aligned as it would have been with no overset, but the following material will be placed appropriately for the complete overset.

This could be made into a formal extension, but if you manage the pages where you want to use this, you should be able to add this configuration to provide the functionality you are looking for.

dpvc avatar May 16 '25 20:05 dpvc

Thank you so much for the fast response. I don't have a website yet, but have recently seen that "quarto" may be a good basis for a lecture script and eventually even papers if things turn out stable and I thought about the things that might complicate moving to this workflow and this was one of the blockers. So thank you very much - I will try this solution as soon as I get around to it. ❤️

FelixBenning avatar May 17 '25 14:05 FelixBenning

It seems to work well if there is nothing on the left side - I am not sure why the 50% width works (50% of what?). To ensure that it also works when there is also something on the left side (the first equation for example) it is perhaps necessary to add a similar phantom on the left. I tried to figure out how to do this myself but unfortunately I do not understand the code at all.

But this certainly will work for now since it at least prevents compiler errors and works in most cases. Thank you so much!

FelixBenning avatar May 21 '25 20:05 FelixBenning

Ah, I hadn't considered that case. I see what you mean. I have been thinking about how to accomplish it, and haven't come up with a method that works using just the available MathML. I would need to ask the output jax to measure some of the output, but is possible, but not something that I want to do. I will keep thinking about it and see if there isn't a way.

dpvc avatar May 22 '25 14:05 dpvc

No worries - this implementation already means I can use this syntax without compiler errors and covers 90% of the use cases :)

FelixBenning avatar May 22 '25 15:05 FelixBenning

I forgot to answer your question:

50% of what?

The mpadded element allows reference to the size of its contents, so the 50% represents half that size. What was needed to get the alignment to work was to have the left-hand size of the overset item to be at the position of the left of the minus sign (the left of the base) while the right side should be at the right of the whole construction (in your case the overset content).

To do that, I used two elements, one that was half the width of the base, and the other that was half the width of the whole construct. The mpadded element around the whole construct was set to half the width of the whole, and the contents where shifted left by 50% of the width as well, meaning the left hand edge of that mpadded is at the middle of the whole overset construct. This is preceded by another mpadded element around a copy of the base, which is also set to 50% of the width of the base, and its contents is in an mphantom element so it is invisible (it is just for sizing).

Together this means these for an element that ignores the width of the over-script to the left of the base (but not the part to the right).

What is needed for your situation is an element that is the width of that overhang so that it can be placed in the table cell to before the & in order to prevent any material in the previous cell from butting up agains the over-script too closely.

Unfortunately, there doesn't seem to be a good way to get that. One would like to be able to use a width that is -100%, for example, but the MathML specification requires that the width be non-negative, so I'm stuck trying to figure out a way to do it without knowing the explicit sizes of the elements.

dpvc avatar May 22 '25 15:05 dpvc

Is it possible to give padding to the previous node?

i.e.

A B = [node before equal] [node, equal + overset]

If you can obtain the previous node A, you could add positive whitespace to it at the back so that it does not interact with the overset.

FelixBenning avatar May 23 '25 11:05 FelixBenning

It's not a matter of where to add space (that is not a problem). The issue is knowing how much space. The input jax doesn't know what the output jax is or how big any of the results will be, and the only mechanism in MathML that has access to the size of an element is the mpadded element, and I don't see a way to use that to get the width that we need. To get that width, one would need to need to ask the output jax to measure the results, and while that is possible, it is not straight forward, and breaks the separation of the input and output that is in place to make the internal MathML be independent of the output format.

dpvc avatar May 23 '25 11:05 dpvc

So the implicit assumption in my suggestion was that the alignment mechanism works as follows:

You have a row of containers where between certain containers you have alignment. So demoting the containers with capital letters

Right Align Left Align
A B
C D

So if we are in block B with the equal sign in it, then we need to make sure that the equal sign is moved all the way to the left of the container by using the lshift=-50%. Using mpadded and mphantom you can then add the right side of the overset back in as padding, because the alignment only cares about the start. But for the overhang to the left we cannot use mpadded and mphantom because adding this to block B would mean that the equal sign is no longer starting the block and mess with the alignment.

My suggestion is now to use mpadded/mphantom to pad block A, before the alignment. But this assumes access to block A from block B, so that we can use the content of block B (i.e. the content of the overset) in a mpadded to add to block A

FelixBenning avatar May 23 '25 14:05 FelixBenning

Your analysis of the setup and what needs to be done is correct. Because the \overset[...]&{...} occurs in cell A (it happens before the & that ends cell A is processed), there is no problem using it to add space in cell A. The problem is knowing how much space to add. The TeX input jax doesn't have a way to determine the size of anything (that is dependent on the output jax, and those are meant to be independent of each other). The TeX input jax can create mpadded elements, and those can use widths and offset that are based on the width of their content, but they can't produce negative widths. I don't see a way to use that mechanism (and it is the only one with access to the widths of content) to determine the width of the overhang. That is, I can't figure out how to get the proper size of the element to add at the end of A. That's the problem. It could be done by calling the output jax to compute the widths of the pieces, but that breaks the independence of the input and output jax, which is done in only one other extension (the bussproofs extension). It can be done, but I don't like doing it.

dpvc avatar May 25 '25 12:05 dpvc

Huh - I thought the processing would happen in cell B, since you were able to add padding after the alignment.

For the padding before the alignment, can you not just take 50% of ther over content in the overset and add this as positive whitespace before the alignment?

I mean the issue at the moment is, that the content on the left of the equal sign overlaps with the over text. So if you padd the content to the left of the equal sign with positive whitespace, then this overlap does not happen. Or more specifically, the overlap happens with the positive whitespace, which is okay since it is whitespace.

FelixBenning avatar May 26 '25 06:05 FelixBenning

can you not just take 50% of ther over content in the overset and add this as positive whitespace before the alignment?

That is too much space. You need 50% of the whole construct minus 50% of the base in order to get the width of the overhang (and then use that only if it is a positive result). But I can't do the minus using just the MathML elements. That would take knowing the actual widths of the two pieces, which the input jax alone doesn't know.

I understand what needs to be done, but I can't get the correct length to use for the added whitespace (using only the MathML elements that the input jax can produce). As I have said before, that is the only problem.

dpvc avatar May 26 '25 11:05 dpvc

Now I understood it I think - thank you!

FelixBenning avatar May 26 '25 12:05 FelixBenning