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