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

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

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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;
})();