MathJax
MathJax copied to clipboard
Math cant be selected in any major browser even if rendered in HTML-CSS
I remember that I used to select texts from math formula rendered by Mathjax 2.75, but on my new website using mathjax 3.0 I can't do that any more using Firefox/Chrome/Safari on MacOS 10.15.1.
The website is rendered using tex-chtml.js and I can comfirm that in the context menu of mathjax the render mode is set to chtml.
The website in question:https://www.eurekz.com/post/steinitz-exchange-lemma/
You are correct that MathJax output can't be copied directly from the page in version 3. Version 3 uses CSS with content
properties in order to insert the characters into the page, and content
text is not selectable in the page.
In general, copying MathJax output would only be reliable with the simplest of expressions (nothing involving super- or subscripts, fractions, roots, arrays, multi-character stretchy delimiters, accents, etc.), so this was never a supported feature for MathJax.
On the other hand, it would be possible for MathJax to insert the characters directly rather than use content
CSS, and an extension to implement that would be possible. I'm marking this as a feature request for that reason.
I was hoping to select some math from a MathJax rendered expression and to map this selection back to the original (TeX or MathMl) expression. Is there a way to achieve that with MathJax
For example, given \int_1^{a+b}\cos(2\times t)d t I would like to be able to select a+b with a mouse or to be able to select the whole integral, or just the cos(2\times t) part...
I can achieve that with javascript with normal text by adding lots of tags with ids and using mouseenter/mouseexit events.
For example, could we get a map between parts of an equation and bounding boxes coordinates. That would make it possible.
@dpvc Would you like to share more details on your comment:
On the other hand, it would be possible for MathJax to insert the characters directly rather than use content CSS, and an extension to implement that would be possible. I'm marking this as a feature request for that reason.
?
If this is a simple fix, I can look into this and give a PR.
Letting MathJax formulas copyable is really desirable in a chat-like context.
@thisiscam:
If this is a simple fix, I can look into this and give a PR.
It's not an easy fix, but I did make a post about it a couple years ago. It includes code for two different approaches (depending on whether you want to copy the original TeX code, or the characters in the typeset formula). The second approach may need some tweaking, as the functions that it modifies may have changed since then, but that is the right idea.
I'm planning to make such a change as part of the font update that we are planning for the end of the summer.
@dpvc Many thanks!
I adapted your post's first solution to SVG output and it appears to work.
Curiously though, the addCopyText
function is called twice, therefore causing the un-rendered tex to be copied twice too.
I wasn't able to figure out why that is happening so I used this hack:
addCopyText(math, doc) {
const adaptor = doc.adaptor;
// For some reason, if we don't use this attribute hack the mjx-copytext
// will be injected twice.
// TODO: investigate why addCopyText is called twice.
if(!adaptor.hasAttribute(math.typesetRoot, "injected_copyable_text")) {
const text = adaptor.node('mjx-copytext', {'aria-hidden': true}, [
adaptor.text(math.start.delim + math.math + math.end.delim)
]);
adaptor.append(math.typesetRoot, text);
adaptor.setAttribute(math.typesetRoot, "injected_copyable_text", "done");
}
},
Also, your second solution will only work for CHTML output am I right?
Curiously though, the
addCopyText
function is called twice
If an extension needs to be loaded for some equation on the page, then the rendering is halted until the extension loads, and then the rendering is performed again. MathJax's internal render actions keep track of this and don't redo work that has already been done, but the simple example that I put together doesn't do that. You could make this more sophisticated by using the MathDocument processed bit-field to record when the text has been added, and the MathItem state variable to record which items have been processed.
your second solution will only work for CHTML output am I right?
Yes, that is CHTML only. The SVG output is not text based (it is path based) so there is no text to select. That would not be easy to change.
@dpvc Thanks! That is very helpful.
Here is a version that implements the processed and state bits:
MathJax = {
options: {
renderActions: {
addCopyText: [156,
(doc) => {
if (!doc.processed.isSet('addtext')) {
for (const math of doc.math) MathJax.config.addCopyText(math, doc);
doc.processed.set('addtext');
}
},
(math, doc) => MathJax.config.addCopyText(math, doc)
]
}
},
addCopyText(math, doc) {
if (math.state() < MathJax.STATE.ADDTEXT) {
const adaptor = doc.adaptor;
const text = adaptor.node('mjx-copytext', {'aria-hidden': true}, [
adaptor.text(math.start.delim + math.math + math.end.delim)
]);
adaptor.append(math.typesetRoot, text);
math.state(MathJax.STATE.ADDTEXT);
}
},
startup: {
ready() {
const {newState, STATE} = MathJax._.core.MathItem;
const {AbstractMathDocument} = MathJax._.core.MathDocument;
const {CHTML} = MathJax._.output.chtml_ts;
newState('ADDTEXT', 156);
AbstractMathDocument.ProcessBits.allocate('addtext');
CHTML.commonStyles['mjx-copytext'] = {
display: 'inline-block',
position: 'absolute',
top: 0, left: 0, width: 0, height: 0,
opacity: 0,
overflow: 'hidden'
};
MathJax.STATE = STATE;
MathJax.startup.defaultReady();
}
}
}
@dpvc Wow thanks! I was just about to do it myself.
One thing I noticed though, is that since you allocated the "mjx-copytext" to be custom block level elements, in my case all the pasted un-rendered TeX items will have a newline appended at the end. This doesn't happen if I paste to my system's text editor, but only when I paste to the same webpage (which contains the equations)'s input box.
I used this to create an inline span node instead:
adaptor.node('span', {'aria-hidden': true, 'class': 'mjx-copytext'}, [
adaptor.text(math.start.delim + math.math + math.end.delim)
]);
Then I added a global style
.mjx-copytext {
font-size: 0;
}
Not sure if you have suggestions to handle this better.
Update: I realized that
MathJax._.output.svg_ts.SVG.commonStyles['.mjx-copytext'] = {
'font-size': 0
};
works fine!
Another minor issue is that the solution doesn't seem to respect processEscapes=true
. If I type \$ x
, the $ sign will be copied twice (one for regular text, and one for the one inside mjx-copytext
.
But since this is a really small problem, I'm satisfied for now.
@dpvc Sorry to bother again.
Is it possible to insert some kind of anchor before and after the rendered equations, so that there's always a selectable surrounding text? Currently if the equation is at begin or end of a line, I can't select it therefore can't do the copy.
I have tried inserting spans with the "zero width space" character "\u200b" but that doesn't seem to work, since I still can't select that empty character.
On the other hand, using something like thin space character "\u2009" does work -- but those spaces will be copied along.
Perhaps a workaround solution that I'm satisfied with would be to only add a thin space before the equation if the equation occurs at begin of line, or add a thin space after the equation if it occurs at the end of line. But a question then is how do I detect the position of the math
, i.e. the position relative to the parent node where I called typeset
on, in a renderAction
? More simply, can I get a handle to the parent node where I called typeset
on in a renderAction
?
@thisiscam, thanks for identifying the issue with processing escapes. That is easily resolved (see below). The issue of selectable text on either side is more difficult. I think that if you can select it, it will be copied as part of the text. It also seems that different browsers handle the U+200B and similar characters differently. In Firefox, for example, I can select them and they get copied.
I'm not sure how to tell if you are at the beginning or ending of the line in any sort of efficient way. But you can use the MathItem.start
and MathItem.end
structures to figure out where you are in the document. MathItem.start.node
points to the (text) node in which the original math was found, and MathItem.start.n
is the position of the math within that text. (Similarly for MathItem.end
, which might point to a different text element, if the math spans more than one consecutive text node.) If MathItem.start.n
is not zero, it certainly is not at the beginning of the line, for example. Perhaps you can make something of that.
Here is the code that handles escaped dollar signs. It also inserts U+200C at the beginning and end of the math, just to show how that can be done using the adaptor
, in case you were doing it by hand.
addCopyText(math, doc) {
if (math.state() < MathJax.STATE.ADDTEXT) {
if (!math.isEscaped) {
const adaptor = doc.adaptor;
const text = adaptor.node('mjx-copytext', {'aria-hidden': true}, [
adaptor.text(math.start.delim + math.math + math.end.delim)
]);
adaptor.append(math.typesetRoot, text);
adaptor.append(math.typesetRoot, adaptor.text('\u200C'));
adaptor.insert(adaptor.text('\u200C'), adaptor.firstChild(math.typesetRoot))
}
math.state(MathJax.STATE.ADDTEXT);
}
},
@dpvc Thanks! Your solution works perfectly. This is what I ended up using.
addCopyText(math, doc) {
if (math.state() < MathJax.STATE.ADDTEXT) {
if (!math.isEscaped) {
const adaptor = doc.adaptor;
const text = adaptor.node('span', {'aria-hidden': true, 'class': 'mathjax_ignore mjx-copytext'}, [
adaptor.text(math.start.delim + math.math + math.end.delim)
]);
adaptor.append(math.typesetRoot, text);
// Insert thin space(s) if math is at begin or end of text
if (math.start.n == 0) {
adaptor.insert(adaptor.text('\u200A'), adaptor.firstChild(math.typesetRoot));
}
if (math.end.n == math.end.node.length) {
adaptor.append(math.typesetRoot, adaptor.text('\u200A'));
}
}
math.state(MathJax.STATE.ADDTEXT);
}
},
It's just naively testing if the math expression is at beginning or end of some text node. This is a sufficiently good workaround that 1) does not introduce too many anchors 2) allows easy copying. Note that I also used '\u200A' (hair space) -- it occurs that in my case these are removed automatically by the browser when a paste happens.
In general, copying MathJax output would only be reliable with the simplest of expressions (nothing involving super- or subscripts, fractions, roots, arrays, multi-character stretchy delimiters, accents, etc.), so this was never a supported feature for MathJax.
As a counter example, while this is true in general, my post about sRGB gamma includes formulae with constants (such as S_0) which would benefit from a limited/broken copying support. Being able to select the entire maths block isn’t the best solution in that case.
@mina86, while @thisiscam chose to do the whole-expression-as-tex approach for his copying solution, the post I linked to above also included one where you could copy portions of the output itself (as the character in the output, not the corresponding TeX code). If you are looking to get the TeX code for subexpressions, that is going to be difficult, as there is not always an obvious map backward to the original TeX.
How about implementing this one?
https://groups.google.com/g/mathjax-users/c/1BzTQFINbqY/m/xGYAlhDNAwAJ
@yuki2006, It is not clear which of the two approaches in the linked discussion you are referring to. The first one (copying the original TeX code) is problematic in a number of situations (as described in the thread you link to), so is not planned for inclusion in MathJax. The long-awaited font update due out this summer will include changes that allow the CHTML output to be copied as HTML (as in the second approach), though this seems useful only in the simplest of expressions.