FormulaCopier

Copy LaTeX formulas when copying text on Zhihu, Wikipedia, OpenReview and ChatGPT.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, Greasemonkey alebo Violentmonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, % alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey alebo Userscripts.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie správcu používateľských skriptov.

(Už mám správcu používateľských skriptov, nechajte ma ho nainštalovať!)

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

(Už mám správcu používateľských štýlov, nechajte ma ho nainštalovať!)

// ==UserScript==
// @name         FormulaCopier
// @namespace    http://tampermonkey.net/
// @license      MIT
// @version      0.5.1
// @description  Copy LaTeX formulas when copying text on Zhihu, Wikipedia, OpenReview and ChatGPT.
// @author       Yuhang Chen(github.com/yuhangchen0), Dramwig(github.com/Dramwig)
// @match        https://www.zhihu.com/*
// @match        https://zhuanlan.zhihu.com/p/*
// @match        https://*.wikipedia.org/*
// @match        https://openreview.net/*
// @match        https://chatgpt.com/*
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    document.addEventListener('copy', function(event) {
        // ⭐ ChatGPT 特殊处理:复制前先把 LaTeX 写到 data-latex 上
        if (window.location.hostname.includes('chatgpt.com')) {
            cacheChatGPTLatex();
        }

        let selectedHtml = getSelectionHtml();

        if (window.location.hostname.includes('zhihu.com')) {
            handleZhihu(selectedHtml, event);
        } else if (window.location.hostname.includes('wikipedia.org')) {
            handleWiki(selectedHtml, event);
        } else if (window.location.hostname.includes('openreview.net')) {
            handleOpenReview(selectedHtml, event);
        } else if (window.location.hostname.includes('chatgpt.com')) {
            handleChatGPT(selectedHtml, event);
        }
    });

    document.addEventListener('selectionchange', function() {
        let formulaSelector = null;

        if (window.location.hostname.includes('zhihu.com')) {
            formulaSelector = '.ztext-math';
        } else if (window.location.hostname.includes('wikipedia.org')) {
            formulaSelector = '.mwe-math-element';
        } else if (window.location.hostname.includes('openreview.net')) {
            formulaSelector = 'mjx-container';
        } else if (window.location.hostname.includes('chatgpt.com')) {
            formulaSelector = '.katex';
        }

        if (!formulaSelector) {
            return;
        }

        const allFormulas = document.querySelectorAll(formulaSelector);
        allFormulas.forEach(removeHighlightStyle);

        const sel = window.getSelection();
        if (!sel.rangeCount) {
            return;
        }

        for (let i = 0; i < sel.rangeCount; i++) {
            const range = sel.getRangeAt(i);
            allFormulas.forEach(formula => {
                if (range.intersectsNode(formula)) {
                    applyHighlightStyle(formula);
                }
            });
        }
    });

    function cacheChatGPTLatex() {
        const formulas = document.querySelectorAll('.katex');
        formulas.forEach(formula => {
            if (!formula.hasAttribute('data-latex')) {
                const annotation = formula.querySelector('annotation[encoding="application/x-tex"]');
                if (annotation) {
                    formula.setAttribute('data-latex', annotation.textContent.trim());
                }
            }
        });
    }


    function handleZhihu(selectedHtml, event) {
        if (selectedHtml.includes('data-tex')) {
            const container = document.createElement('div');
            container.innerHTML = selectedHtml;
            replaceZhihuFormulas(container);
            setClipboardData(event, container.textContent);
        }
    }

    function handleWiki(selectedHtml, event) {
        if (selectedHtml.includes('mwe-math-element')) {
            const container = document.createElement('div');
            container.innerHTML = selectedHtml;
            replaceWikipediaFormulas(container);
            setClipboardData(event, container.textContent);
        }
    }

    function handleOpenReview(selectedHtml, event) {
        if (selectedHtml.includes('mjx-container')) {
            const container = document.createElement('div');
            container.innerHTML = selectedHtml;
            replaceOpenReviewFormulas(container);
            setClipboardData(event, container.textContent);
        }
    }

    function handleChatGPT(selectedHtml, event) {
        if (selectedHtml.includes('katex')) {
            const container = document.createElement('div');
            container.innerHTML = selectedHtml;
            replaceChatGPTFormulas(container);
            setClipboardData(event, container.textContent);
        }
    }

    function applyHighlightStyle(formula) {
        const mathJaxSVG = formula.querySelector('.MathJax_SVG');
        if (mathJaxSVG && mathJaxSVG.style) {
            mathJaxSVG.style.backgroundColor = 'lightblue';
            return;
        }

        const mathJaxCHTML = formula.querySelector('mjx-math');
        if (mathJaxCHTML && mathJaxCHTML.style) {
            mathJaxCHTML.style.backgroundColor = 'lightblue';
            return;
        }

        const katexHtml = formula.querySelector('.katex-html');
        if (katexHtml && katexHtml.style) {
            katexHtml.style.backgroundColor = 'lightblue';
            return;
        }

        if (formula && formula.style) {
            formula.style.backgroundColor = 'lightblue';
        }
    }

    function removeHighlightStyle(formula) {
        const mathJaxSVG = formula.querySelector('.MathJax_SVG');
        if (mathJaxSVG && mathJaxSVG.style) {
            mathJaxSVG.style.backgroundColor = '';
        }

        const mathJaxCHTML = formula.querySelector('mjx-math');
        if (mathJaxCHTML && mathJaxCHTML.style) {
            mathJaxCHTML.style.backgroundColor = '';
        }

        const katexHtml = formula.querySelector('.katex-html');
        if (katexHtml && katexHtml.style) {
            katexHtml.style.backgroundColor = '';
        }

        if (formula && formula.style) {
            formula.style.backgroundColor = '';
        }
    }

    function getLatexFromMjxContainer(container) {
        if (!window.MathJax?.startup?.document) return null;

        const counter = container.getAttribute('ctxtmenu_counter');
        const target = counter
            ? document.querySelector(`mjx-container[ctxtmenu_counter="${counter}"]`) || container
            : container;

        const mathItems = MathJax.startup.document.getMathItemsWithin(target);
        const math = mathItems[0]?.math;
        return typeof math === 'string' ? math.trim() : null;
    }

    function replaceOpenReviewFormulas(container) {
        container.querySelectorAll('mjx-container').forEach(formula => {
            const texCode = getLatexFromMjxContainer(formula);
            if (texCode) {
                formula.replaceWith(document.createTextNode('$' + texCode + '$'));
            }
        });
    }

    function replaceWikipediaFormulas(container) {
        const formulas = container.querySelectorAll('.mwe-math-element');
        formulas.forEach(formula => {
            const annotation = formula.querySelector('annotation[encoding="application/x-tex"]');
            if (annotation) {
                const texCode = annotation.textContent;
                const texNode = document.createTextNode('$' + texCode + '$');
                formula.replaceWith(texNode);
            }
        });
    }

    function replaceChatGPTFormulas(container) {
        container.querySelectorAll('.katex').forEach(formula => {
            // ⭐ 优先使用复制前缓存好的 data-latex
            let texCode = formula.getAttribute('data-latex');

            // 兼容:如果选区里恰好带了 annotation,也可以直接用
            if (!texCode) {
                const annotation = formula.querySelector('annotation[encoding="application/x-tex"]');
                if (annotation) {
                    texCode = annotation.textContent.trim();
                }
            }

            if (texCode) {
                formula.replaceWith(document.createTextNode('$' + texCode + '$'));
            }
            // 如果依然拿不到 texCode,就不动这个节点,退回原生复制行为
        });
    }


    function replaceZhihuFormulas(container) {
        const formulas = container.querySelectorAll('.ztext-math');
        formulas.forEach(formula => {
            const texCode = formula.getAttribute('data-tex');
            const texNode = document.createTextNode('$' + texCode + '$');
            formula.replaceWith(texNode);
        });
    }

    function setClipboardData(event, text) {
        event.clipboardData.setData('text/plain', text);
        event.preventDefault();
    }

    function convertLineBreaks(node) {
        if (node.nodeName === 'BR') {
            node.parentNode.replaceChild(document.createTextNode('\n'), node);
        } else if (node.nodeName === 'P' && node.nextElementSibling) {
            // Add a newline after the section
            node.appendChild(document.createTextNode('\n\n'));
        } else {
            const children = Array.from(node.childNodes);
            for (let child of children) {
                convertLineBreaks(child);
            }
        }
    }

    function getSelectionHtml() {
        const sel = window.getSelection();
        if (sel.rangeCount) {
            const container = document.createElement('div');
            for (let i = 0, len = sel.rangeCount; i < len; ++i) {
                container.appendChild(sel.getRangeAt(i).cloneContents());
            }

            // Keep newline
            convertLineBreaks(container);
            return container.innerHTML;
        }
        return '';
    }
})();