支持数学公式的ChatGPT Markdown一键复制

Copy the chatGPT Q&A content as a markdown text, with MathJax Render Support, based on 'chatGPT Markdown' by 赵巍໖, you can use this together with 'OpenAI-ChatGPT LaTeX Auto Render (with MathJax V2)' that adds support for math render.

当前为 2022-12-10 提交的版本,查看 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         ChatGPT Copy as Markdown with MathJax Support
// @name:zh-CN  支持数学公式的ChatGPT Markdown一键复制
// @namespace    http://tampermonkey.net/
// @version      0.2
// @description  Copy the chatGPT Q&A content as a markdown text, with MathJax Render Support, based on 'chatGPT Markdown' by 赵巍໖, you can use this together with 'OpenAI-ChatGPT LaTeX Auto Render (with MathJax V2)' that adds support for math render.
// @description:zh-cn  将chatGPT问答内容复制成markdown文本,并支持MathJax渲染内容导出,基于赵巍໖的'chatGPT Markdown',与'OpenAI-ChatGPT LaTeX Auto Render(with MathJax V2)'一起使用可以渲染公式。
// @license MIT
// @author       jbji
// @match        https://chat.openai.com/chat
// @icon         https://chat.openai.com/favicon-32x32.png
// @grant        none
// ==/UserScript==

(function () {
    'use strict';
    var mathFixEnabled = true;
    function toMarkdown() {
        var main = document.querySelector("main");
        var article = main.querySelector("div > div > div > div");
        var chatBlocks = Array.from(article.children)
            .filter(v => v.getAttribute("class").indexOf("border") >= 0);

        var replacements = [
            [/\*/g, '\\*', 'asterisks'],
            [/#/g, '\\#', 'number signs'],
            [/\//g, '\\/', 'slashes'],
            //[/\(/g, '\\(', 'parentheses'], //this breaks math euqations
            //[/\)/g, '\\)', 'parentheses'], //this breaks math euqations
            [/\[/g, '\\[', 'square brackets'],
            [/\]/g, '\\]', 'square brackets'],
            [/</g, '&lt;', 'angle brackets'],
            //[/>/g, '&gt;', 'angle brackets'], //breaks math
            //[/_/g, '\\_', 'underscores'], //this breaks math euqations
            [/`/g, '\\`', 'codeblocks']
        ];

        function markdownEscape(string, skips) {
            skips = skips || []
            //reduce function applied the function in the first with the second as input
            //this applies across the array with the first element inside as the initial 2nd param for the reduce func.
            return replacements.reduce(function (string, replacement) {
                var name = replacement[2]
                return name && skips.indexOf(name) !== -1
                    ? string
                    : string.replace(replacement[0], replacement[1])
            }, string)
        }

        function replaceInnerNode(element) {
            if (element.outerHTML) {
                var htmlBak = element.outerHTML;
                if(mathFixEnabled){
                    //replace mathjax stuff
                    var mathjaxBeginRegExp = /(<span class="MathJax_Preview".*?)<scr/s; //this is lazy
                    var match = mathjaxBeginRegExp.exec(htmlBak);
                    while(match){
                        htmlBak = htmlBak.replace(match[1], '');
                        //repalace math equations
                        var latexMath;
                        //match new line equations first
                        var latexMathNLRegExp = /<script type="math\/tex; mode=display" id="MathJax-Element-\d+">(.*?)<\/script>/s;
                        match = latexMathNLRegExp.exec(htmlBak);
                        if(match){
                            latexMath = "$$" + match[1] + "$$";
                            htmlBak = htmlBak.replace(match[0], latexMath);
                        }else{
                            //then inline equations
                            var latexMathRegExp = /<script type="math\/tex" id="MathJax-Element-\d+">(.*?)<\/script>/s;
                            match = latexMathRegExp.exec(htmlBak);
                            if(match){
                                latexMath = "$" + match[1] + "$";
                                htmlBak = htmlBak.replace(match[0], latexMath);
                            }
                        }
                        match = mathjaxBeginRegExp.exec(htmlBak);
                    }
                }

                var parser = new DOMParser();
                //default code block replacement
                var nextDomString = htmlBak.replace(/<code>([\w\s-]*)<\/code>/g, (match) => {
                    var doc = parser.parseFromString(match, "text/html");
                    return "`" + (doc.body.textContent) + "`";
                });
                return parser.parseFromString(nextDomString, "text/html").body.children[0];
            }
            return element;
        }

        var elementMap = {
            "P": function (element, result) {
                let p = replaceInnerNode(element);
                result += markdownEscape(p.textContent, ["codeblocks", "number signs"]);
                result += `\n\n`;
                return result;
            },
            //this should be unordered!
            "UL": function (element, result) {
                let ul = replaceInnerNode(element);
                Array.from(ul.querySelectorAll("li")).forEach((li, index) => {
                    result += `- ${markdownEscape(li.textContent, ["codeblocks", "number signs"])}`;
                    result += `\n`;
                });
                result += `\n\n`;
                return result;
            },
            "OL": function (element, result) {
                let ol = replaceInnerNode(element);
                Array.from(ol.querySelectorAll("li")).forEach((li, index) => {
                    result += `${index + 1}. ${markdownEscape(li.textContent, ["codeblocks", "number signs"])}`;
                    result += `\n`;
                });
                result += `\n\n`;
                return result;
            },
            "PRE": function (element, result) {
                var codeBlocks = element.querySelectorAll("code");
                //first get class name
                var regex = /^language-/;
                var codeType = '';
                for(var c of codeBlocks){
                    var classNameStr = c.className.split(' ')[2];
                    if (regex.test(classNameStr)){
                        codeType = classNameStr.substr(9);
                    }
                }
                //then generate the markdown codeblock
                result += "```" + codeType + "\n";
                Array.from(codeBlocks).forEach(block => {
                    result += `${block.textContent}`;
                });
                result += "```\n";
                result += `\n\n`;
                return result;
            }
        };
        var TEXT_BLOCKS = Object.keys(elementMap);

        var mdContent = chatBlocks.reduce((result, nextBlock, i) => {
            if (i % 2 === 0) { // title
                let p = replaceInnerNode(nextBlock);
                result += `> ${markdownEscape(p.textContent, ["codeblocks", "number signs"])}`;
                result += `\n\n`;
            }else{
                //try to parse the block
                var iterator = document.createNodeIterator(
                    nextBlock,
                    NodeFilter.SHOW_ELEMENT,
                    {
                        acceptNode: element => TEXT_BLOCKS.indexOf(element.tagName.toUpperCase()) >= 0
                    },
                    false,
                );
                let next = iterator.nextNode();
                while (next) {
                    result = elementMap[next.tagName.toUpperCase()](next, result);
                    next = iterator.nextNode();
                }
            }
            return result;
        }, "");
        return mdContent;
    }
    //for copy button
    var copyHtml = `<div id="__copy__" style="cursor:pointer;position: fixed;bottom: 20px;left: 20px;width: 100px;height: 35px;background: #333333;border: 1px solid #555555;border-radius: 5px;color: white;display: flex;justify-content: center;align-items: center;transition: all 0.2s ease-in-out;"><span>Copy .md</span></div>`;
    // for copy function
    var copyElement = document.createElement("div");
    document.body.appendChild(copyElement);
    copyElement.outerHTML = copyHtml;
    // for button style
    document.querySelector('#__copy__').addEventListener('mouseenter', function() {
        this.style.background = '#555555';
        this.style.color = 'white';
    });
    document.querySelector('#__copy__').addEventListener('mouseleave', function() {
        this.style.background = '#333333';
        this.style.color = 'white';
    });
    document.querySelector('#__copy__').addEventListener('mousedown', function() {
        this.style.boxShadow = '2px 2px 2px #333333';
    });
    document.querySelector('#__copy__').addEventListener('mouseup', function() {
        this.style.boxShadow = 'none';
    });
    //for anchor
    var copyAnchor = document.getElementById("__copy__");
    copyAnchor.addEventListener("click", () => {
        // Get the `span` element inside the `div`
        let span = copyAnchor.querySelector("span");

        // Change the text of the `span` to "Done"
        span.innerText = "Copied!";

        // Use `setTimeout` to change the text back to its original value after 3 seconds
        setTimeout(() => {
            span.innerText = "Copy .md";
        }, 1000);

        // Perform the rest of the original code
        navigator.clipboard.writeText(toMarkdown()).then(() => {
            //alert("done");
        });
    });
})();