Latexlive公式编辑器输出增强:转 Markdown 格式,适用于 Logseq 等

为中文文本中的公式添加 $$ 符号,以适应 Markdown 或 Latex 格式的需求。并修复常见的图片识别结果中的错误

// ==UserScript==
// @name         Latexlive公式编辑器输出增强:转 Markdown 格式,适用于 Logseq 等
// @namespace    http://tampermonkey.net/
// @version      2.4.2
// @description  为中文文本中的公式添加 $$ 符号,以适应 Markdown 或 Latex 格式的需求。并修复常见的图片识别结果中的错误
// @author       Another_Ghost
// @match        https://*.latexlive.com/*
// @icon         https://img.icons8.com/?size=50&id=1759&format=png
// @grant         GM_registerMenuCommand
// @license      MIT
// ==/UserScript==

//export function correctText(){};

(function () { // 使用匿名函数封装代码,避免变量污染全局环境
    createButton('复制', copyOriginalText, '');
    createButton('转换后复制', convertFormulasToLaTeX, /\\boldsymbol/g);

    /**
     * 创建按钮并添加到指定容器中
     * @param {number} buttonName - 按钮的名字
     * @param {function} convert - 转换函数
     * @param {string} wordsToRemove - 需要移除的字符串
     */
    function createButton(buttonName, convert, wordsToRemove) {
        // 创建一个新按钮
        let button = document.createElement('button');
        button.innerHTML = `${buttonName}`;
        button.className = 'btn btn-light btn-outline-dark';
        //button.id = `copy-btn${buttonId}`;
        // add click handler
        button.onclick = function () {
            //选中输入文本框的所有文本
            var selected = document.querySelector('#txta_input'); 
            //先通过 convert 函数转换文本,再复制
            navigator.clipboard.writeText(convert(selected.value, wordsToRemove));
            displayAlertBox('已复制');
        };
        //输入框上方的容器
        var CONTAINER = "#wrap_immediate > row > div.col-5.col-sm-5.col-md-5.col-lg-5.col-xl-5";
        //等待容器出现并添加按钮
        var interval = setInterval(function () {
            var wrap = document.querySelector(CONTAINER);
            if (wrap) {
                wrap.appendChild(button);
                clearInterval(interval);
            }
        }, 200);
    }

    function displayAlertBox(text) {
        var alertBox = document.createElement('div');
        alertBox.innerHTML = text;
        //alertBox.style.display = none;
        alertBox.style.position = 'fixed';
        alertBox.style.bottom = `20px`;
        alertBox.style.left = `50%`;
        alertBox.style.transform = `translateX(-50%)`;
        alertBox.style.backgroundColor = `#4CAF50`;
        alertBox.style.color = `white`;
        alertBox.style.padding = `12px`;
        alertBox.style.borderRadius = `5px`;
        alertBox.style.zIndex = `1000`;
        alertBox.style.boxShadow = `0px 0px 10px rgba(0,0,0,0.5)`;
        alertBox.style.opacity = '0';
        alertBox.style.transition = 'opacity 0.3s';
        document.body.appendChild(alertBox);
        setTimeout(function () {
            alertBox.style.opacity = '1';
        }, 100);
        setTimeout(function () {
            alertBox.style.opacity = '0';
        }, 1100);
        setTimeout(function () {
            alertBox.remove();
        }, 1500);
    }

    function copyOriginalText(inStr, wordsToRemove = '') {
        return inStr;
    }

    let bRadical = true; //是否是更激进的转换方式
    let bLogseq = false; //是否是为Logseq准备的转换方式
    
    if(typeof GM_registerMenuCommand === 'function'){
        let shortcutKey = null;
        GM_registerMenuCommand('切换激进转换', function (){
            bRadical = !bRadical;
            if(bRadical)
            {
                displayAlertBox("开启激进转换");
            }
            else
            {
                displayAlertBox("关闭激进转换");
            }
        }, shortcutKey);

        GM_registerMenuCommand('切换Logseq格式转换', function (){
            bLogseq = !bLogseq;
            if(bLogseq)
            {
                displayAlertBox("开启Logseq格式转换");
            }
            else
            {
                displayAlertBox("关闭Logseq格式转换");
            }
        }, shortcutKey);
    }
    /**
     * 将字符串中的公式转换为LaTeX格式,用"$$"包围起来。
     */
    function convertFormulasToLaTeX(inStr, wordsToRemove = '') {
    
        // 输入的预处理
        inStr = inStr.trim(); //删除字符串两端的空白字符
        if(bRadical)
        {
            inStr = inStr.replace(/\\begin{array}{[^{}]*}/g, '\\begin{aligned}');
            inStr = inStr.replace(/\\end{array}/g, '\\end{aligned}');
            inStr = inStr.replace(/\\boldsymbol ?/g, '');
            inStr = inStr.replace(/\\mathbf ?/g, '');
            inStr = inStr.replace(/\\mathscr ?/g, '\\mathcal');
        }

        //inStr = inStr.replace(wordsToRemove, '');
        inStr = inStr.replace(/ +/g, ' '); //将多个空格替换为一个空格
        inStr = inStr.replace(/\n+/g, '\n'); //去除重复换行符
        inStr = inStr.replace(/输人/g, "输入");
        inStr = inStr.replace(/存人/g, "存入");
        inStr = inStr.replace(/接人/g, "接入");
        inStr = inStr.replace(/舍人/g, "舍入");
        //inStr = inStr.trim(); 
        
        let nonFormulaChar = /[\u2000-\uffff]/g; //非公式字符的正则表达式
        let outStr = ""; //最终输出的字符串

        let blocks = SplitToBlocks(inStr);

        for(let i = 0; i < blocks.length; i++){
            if(!blocks[i].match(/\\begin\{(.*?)\}([\s\S]*?)\\end\{\1\}/)){ //判断是否多行非全公式块,是则不需做任何处理

                let tempMap = {};
                let index = 0;
            
                // 替换 $\text{...}$ 结构
                let processedBlock = blocks[i].replace(/\$\\text ?\{[^{}]*\}\$/g, match => {
                    let placeholder = `__PLACEHOLDER${index++}__`;
                    tempMap[placeholder] = match;
                    return placeholder;
                });
            
                let parts = processedBlock.split(/([\u2000-\uffff]+)|( +[a-zA-Z]{2,} +)/).filter(part => part !== undefined);
                if(parts.length > 1){
                    blocks[i] = blocks[i].replace(/\\text ?\{([^{}]*)\}/g, '$1'); //非全公式块,去掉\text{}
                    //非公式行,替换中文句尾标点
                    blocks[i] = blocks[i].replace(/, *?/g, ',');
                    blocks[i] = blocks[i].replace(/: *?/g, ':');
                    blocks[i] = blocks[i].replace(/; *?/g, ';');
                    blocks[i] = blocks[i].replace(/\? *?/g, '?');
                    blocks[i] = blocks[i].replace(/([\u2000-\uffff]) ?\(([^()\d]+?)\) ?([\u2000-\uffff])/g, '$1($2)$3'); //? (中 1 文) 情况,为 $z^{-1} ($ 或 $z )$ 的 情况
                    //blocks[i] = blocks[i].replace(/[^\d]\. /g, '。');
                    blocks[i] = blocks[i].replace(/([\u2000-\uffff]) +([\u2000-\uffff])/g, '$1$2');

                    // 在非中文和非单词字符串前后加上$
                    blocks[i] = blocks[i].replace(/[^\u2000-\uffff]+/g, match => {
                        if(match.trim() === '') {
                            return match;
                        }
                        else if(match.match(/^[a-zA-Z]{2,} */) || match.match(/^\d\. /) || match.match(/^\(?\d\) /) || match.match(/([\u2000-\uffff]) *\- *([\u2000-\uffff])/)) { // match.match(/^[a-zA-Z]{2,} */) 为匹配 word 开头的情况
                            return match;
                        }
                        else if(match.match(/ +([a-zA-Z]{2,}) */))
                        {
                            return ' ' + match.trim() + ' ';
                        }
                        else{
                            return ` $` + match.trim() + '$ ';
                        }
                    });                
                }
                else { //单行全公式块,只需整体前后加上$$
                    blocks[i] = AddToStartEnd(blocks[i], "$$"); 
                }
                
            }else{ //多行全公式块,只需整体前后加上$$
                blocks[i] = AddToStartEnd(blocks[i], "$$");
            }
            if(bLogseq)
            {
                if(blocks[i].match(/^\d+\. /))
                {
                    blocks[i] = blocks[i].replace(/^\d+\. /, '');
                    blocks[i] = '- ' + blocks[i] + '\n' + 'logseq.order-list-type:: number';
                }
                else if(blocks[i].match(/^\(\d+\) /))
                {
                    blocks[i] = blocks[i].replace(/^(\()?\d+\) /, '');
                    blocks[i] = '   - ' + blocks[i] + '\n' + 'logseq.order-list-type:: number';
                }
            }
            outStr += blocks[i]+'\n';
        }

        //window.internalFunc = convertFormulasToLaTeX;

        return outStr;
        
        function AddToStartEnd(str, toAdd){
            return toAdd+str.trim()+toAdd;
        }

        // 将字符串分割为块
        function SplitToBlocks(str)
        {
            //先按换行分割
            let splits = str.split(/[\n\r]/g).filter(part => part !== undefined); 
            let i = 0;
            let blocks = [];
            while(i < splits.length)
            {
                //将\begin{x} ... \end{x} 视为一个块,所以需要合并行
                if(splits[i].match(/\\begin/))
                {
                    let j = i + 1;
                    while(j < splits.length && !splits[j].match(/\\end/))
                    {
                        j++;
                    }
                    let tempStr = "";
                    for(let k = i; k < j + 1; k++)
                    {
                        tempStr += splits[k] + "\n";
                    }
                    blocks.push(tempStr);
                    i = j + 1;
                }
                else
                {
                    blocks.push(splits[i]);
                    i++;
                }
            }
            return blocks;
        }
    }
    //myFunction = convertFormulasToLaTeX;
    //window.convertFormulasToLaTeX = convertFormulasToLaTeX;
    correctText = convertFormulasToLaTeX;
})();