Here is a function that will copy the current selection to the clipboard (as both plain text and rich text), but replacing the typeset math by its original TeX code in MathJax v3:
function copyOriginalSource() {
const selection = document.getSelection();
const nodes = document.createElement('div');
for (let i = 0; i < selection.rangeCount; i++) {
nodes.append(selection.getRangeAt(i).cloneContents());
const math = Array.from(nodes.querySelectorAll('mjx-container'));
let id = math[i].getAttribute('ctxtmenu_counter');
for (const item of MathJax.startup.document.math) {
if (item.typesetRoot?.getAttribute('ctxtmenu_counter') === id) {
const tex = item.start.delim + item.math + item.end.delim;
const mjx = nodes.querySelector('mjx-container');
mjx.replaceWith(document.createTextNode(tex));
id = math[++i]?.getAttribute('ctxtmenu_counter');
new Blob([nodes.textContent], {type: 'text/plain'}),
new Blob([nodes.innerHTML], {type: 'text/html'})
]).then(([text, html]) => {
const data = new ClipboardItem({
navigator.clipboard.write([data]);
There are a couple of caveats:
First, it doesn't properly handle the "escaped" output. That is, if the original source had \$ in order to get an explicit dollar sign rather than an in-line math delimiter, then the backslash is not put back in place during the copy. If you need that, it would take a bit more work.
Second, this only works for TeX or AsciiMath input, not MathML input, which would need to be handled more carefully. If you need that, the code would need to check for that and handle it differently. It can be done, but I didn't want to complicate the situation unnecessarily.
Third, this function has to be called from within an event handler like a button click or a keypress (or a menu selection). You can probably attach an event listener to the "copy" event and call this function from that, while preventing the default, though I haven't tried that, and you may have to take more care about when to call it. but that might work.
Hope that gives you what you need.
Davide