ChatGPT Copy as Markdown with MathJax Support

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

目前為 2022-12-11 提交的版本,檢視 最新版本

// ==UserScript==
// @name         ChatGPT Copy as Markdown with MathJax Support
// @name:zh-CN  支持数学公式的ChatGPT Markdown一键复制
// @namespace    http://tampermonkey.net/
// @version      0.3
// @description  Copy the chatGPT Q&A content as a markdown text, with MathJax Render Support, you can use this together with 'OpenAI-ChatGPT LaTeX Auto Render (with MathJax V2)' that adds support for math render, based on 'chatGPT Markdown' by 赵巍໖.
// @description:zh-cn  将chatGPT问答内容复制成markdown文本,并支持MathJax渲染内容导出,与'OpenAI-ChatGPT LaTeX Auto Render(with MathJax V2)'一起使用可以渲染公式, 基于赵巍໖的'chatGPT Markdown'。
// @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 new_replacements = [
            //['\\', '\\\\', 'backslash'], //Don't need this any more cause it would be checked.
            ['`', '\\`', 'codeblocks'],
            ['*', '\\*', 'asterisk'],
            ['_', '\\_', 'underscores'],
            ['{', '\\{', 'crulybraces'],
            ['}', '\\}', 'crulybraces'],
            ['[', '\\[', 'square brackets'],
            [']', '\\]', 'square brackets'],
            ['(', '\\(', 'parentheses'],
            [')', '\\)', 'parentheses'],
            ['#', '\\#', 'number signs'],
            ['+', '\\+', 'plussign'],
            ['-', '\\-', 'hyphen'],
            ['.', '\\.', 'dot'],
            ['!', '\\!', 'exclamation mark'],
            ['>', '\\>', 'angle brackets']
        ];

        // A State Machine used to match string and do replacement
        function replacementSkippingMath(string, char_pattern, replacement) {
            var inEquationState = 0; // 0:not in equation, 1:inline equation expecting $, 2: line euqation expecting $$
            var result = "";
            for (let i = 0; i < string.length; i++) {
                if(string[i] == '\\'){
                    result += string[i];
                    if (i+1 < string.length) result += string[i+1];
                    i++; // one more add to skip escaped char
                    continue;
                }
                switch(inEquationState){
                    case 1:
                        result += string[i];
                        if(string[i] === '$'){
                            inEquationState = 0; //simply exit and don't do further check
                            continue;
                        }
                        break;
                    case 2:
                        result += string[i];
                        if(string[i] === '$'){
                            if (i+1 < string.length && string[i+1] === '$'){ //matched $$
                                result += '$';
                                inEquationState = 0;
                                i++; // one more add
                            }
                            //else is unexpected behavior
                            continue;
                        }
                        break;
                    default:
                        if(string[i] === '$'){
                            if (i+1 < string.length && string[i+1] === '$'){//matched $$
                                result += '$$';
                                inEquationState = 2;
                                i++; // one more add
                            }else{ //matched $
                                result += '$';
                                inEquationState = 1;
                            }
                            continue;
                        }else if(string[i] === char_pattern[0]){ //do replacement
                            result += replacement;
                        }else{
                            result += string[i];
                        }
                }
            }

            return result;
        }

        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 new_replacements.reduce(function (string, replacement) {
                var name = replacement[2]
                if (name && skips.indexOf(name) !== -1) {
                    return string;
                } else {
                    return replacementSkippingMath(string, 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");
        });
    });
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址