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

将chatGPT问答内容复制成markdown文本,并支持MathJax渲染内容导出,与'OpenAI-ChatGPT LaTeX Auto Render(with MathJax V2)'一起使用可以渲染公式, 基于赵巍໖的'chatGPT Markdown'。

  1. // ==UserScript==
  2. // @name ChatGPT Copy as Markdown with MathJax Support
  3. // @name:zh-CN 支持数学公式的ChatGPT Markdown一键复制
  4. // @namespace http://tampermonkey.net/
  5. // @version 0.6
  6. // @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 赵巍໖.
  7. // @description:zh-cn 将chatGPT问答内容复制成markdown文本,并支持MathJax渲染内容导出,与'OpenAI-ChatGPT LaTeX Auto Render(with MathJax V2)'一起使用可以渲染公式, 基于赵巍໖的'chatGPT Markdown'。
  8. // @license MIT
  9. // @author jbji
  10. // @match https://chat.openai.com/chat
  11. // @match https://chat.openai.com/chat/*
  12. // @icon https://chat.openai.com/favicon-32x32.png
  13. // @grant none
  14. // ==/UserScript==
  15.  
  16. (function () {
  17. 'use strict';
  18. var mathFixEnabled = true;
  19. function toMarkdown() {
  20. var main = document.querySelector("main");
  21. var article = main.querySelector("div > div > div > div");
  22. var chatBlocks = Array.from(article.children)
  23. .filter(v => v.getAttribute("class").indexOf("border") >= 0);
  24. // for chatgpt plus
  25. if (chatBlocks.length > 0 && chatBlocks[0].classList.contains("items-center")) {
  26. chatBlocks.shift(); // remove first element from array
  27. }
  28. var new_replacements = [
  29. //['\\', '\\\\', 'backslash'], //Don't need this any more cause it would be checked.
  30. ['`', '\\`', 'codeblocks'],
  31. ['*', '\\*', 'asterisk'],
  32. ['_', '\\_', 'underscores'],
  33. ['{', '\\{', 'crulybraces'],
  34. ['}', '\\}', 'crulybraces'],
  35. ['[', '\\[', 'square brackets'],
  36. [']', '\\]', 'square brackets'],
  37. ['(', '\\(', 'parentheses'],
  38. [')', '\\)', 'parentheses'],
  39. ['#', '\\#', 'number signs'],
  40. ['+', '\\+', 'plussign'],
  41. ['-', '\\-', 'hyphen'],
  42. ['.', '\\.', 'dot'],
  43. ['!', '\\!', 'exclamation mark'],
  44. ['>', '\\>', 'angle brackets']
  45. ];
  46.  
  47. // A State Machine used to match string and do replacement
  48. function replacementSkippingMath(string, char_pattern, replacement) {
  49. var inEquationState = 0; // 0:not in equation, 1:inline equation expecting $, 2: line euqation expecting $$
  50. var result = "";
  51. for (let i = 0; i < string.length; i++) {
  52. if(string[i] == '\\'){
  53. result += string[i];
  54. if (i+1 < string.length) result += string[i+1];
  55. i++; // one more add to skip escaped char
  56. continue;
  57. }
  58. switch(inEquationState){
  59. case 1:
  60. result += string[i];
  61. if(string[i] === '$'){
  62. inEquationState = 0; //simply exit and don't do further check
  63. continue;
  64. }
  65. break;
  66. case 2:
  67. result += string[i];
  68. if(string[i] === '$'){
  69. if (i+1 < string.length && string[i+1] === '$'){ //matched $$
  70. result += '$';
  71. inEquationState = 0;
  72. i++; // one more add
  73. }
  74. //else is unexpected behavior
  75. continue;
  76. }
  77. break;
  78. default:
  79. if(string[i] === '$'){
  80. if (i+1 < string.length && string[i+1] === '$'){//matched $$
  81. result += '$$';
  82. inEquationState = 2;
  83. i++; // one more add
  84. }else{ //matched $
  85. result += '$';
  86. inEquationState = 1;
  87. }
  88. continue;
  89. }else if(string[i] === char_pattern[0]){ //do replacement
  90. result += replacement;
  91. }else{
  92. result += string[i];
  93. }
  94. }
  95. }
  96.  
  97. return result;
  98. }
  99.  
  100. function markdownEscape(string, skips) {
  101. skips = skips || []
  102. //reduce function applied the function in the first with the second as input
  103. //this applies across the array with the first element inside as the initial 2nd param for the reduce func.
  104. return new_replacements.reduce(function (string, replacement) {
  105. var name = replacement[2]
  106. if (name && skips.indexOf(name) !== -1) {
  107. return string;
  108. } else {
  109. return replacementSkippingMath(string, replacement[0], replacement[1]);
  110. }
  111. }, string)
  112. }
  113.  
  114. function replaceInnerNode(element) {
  115. if (element.outerHTML) {
  116. var htmlBak = element.outerHTML;
  117. if(mathFixEnabled){
  118. //replace mathjax stuff
  119. var mathjaxBeginRegExp = /(<span class="MathJax_Preview".*?)<scr/s; //this is lazy
  120. var match = mathjaxBeginRegExp.exec(htmlBak);
  121. while(match){
  122. htmlBak = htmlBak.replace(match[1], '');
  123. //repalace math equations
  124. var latexMath;
  125. //match new line equations first
  126. var latexMathNLRegExp = /<script type="math\/tex; mode=display" id="MathJax-Element-\d+">(.*?)<\/script>/s;
  127. match = latexMathNLRegExp.exec(htmlBak);
  128. if(match){
  129. latexMath = "$$" + match[1] + "$$";
  130. htmlBak = htmlBak.replace(match[0], latexMath);
  131. }else{
  132. //then inline equations
  133. var latexMathRegExp = /<script type="math\/tex" id="MathJax-Element-\d+">(.*?)<\/script>/s;
  134. match = latexMathRegExp.exec(htmlBak);
  135. if(match){
  136. latexMath = "$" + match[1] + "$";
  137. htmlBak = htmlBak.replace(match[0], latexMath);
  138. }
  139. }
  140. match = mathjaxBeginRegExp.exec(htmlBak);
  141. }
  142. }
  143.  
  144. var parser = new DOMParser();
  145. //default code block replacement
  146. var nextDomString = htmlBak.replace(/<code>([\w\s-]*)<\/code>/g, (match) => {
  147. var doc = parser.parseFromString(match, "text/html");
  148. return "`" + (doc.body.textContent) + "`";
  149. });
  150. return parser.parseFromString(nextDomString, "text/html").body.children[0];
  151. }
  152. return element;
  153. }
  154.  
  155. var elementMap = {
  156. "P": function (element, result) {
  157. let p = replaceInnerNode(element);
  158. result += markdownEscape(p.textContent, ["codeblocks", "number signs"]);
  159. result += `\n\n`;
  160. return result;
  161. },
  162. //this should be unordered!
  163. "UL": function (element, result) {
  164. let ul = replaceInnerNode(element);
  165. Array.from(ul.querySelectorAll("li")).forEach((li, index) => {
  166. result += `- ${markdownEscape(li.textContent, ["codeblocks", "number signs"])}`;
  167. result += `\n`;
  168. });
  169. result += `\n\n`;
  170. return result;
  171. },
  172. "OL": function (element, result) {
  173. let ol = replaceInnerNode(element);
  174. var olStart = parseInt(ol.getAttribute("start") || "1"); //bug fix thanks to original author
  175. Array.from(ol.querySelectorAll("li")).forEach((li, index) => {
  176. result += `${index + olStart}. ${markdownEscape(li.textContent, ["codeblocks", "number signs"])}`;
  177. result += `\n`;
  178. });
  179. result += `\n\n`;
  180. return result;
  181. },
  182. "PRE": function (element, result) {
  183. var codeBlocks = element.querySelectorAll("code");
  184. //first get class name
  185. var regex = /^language-/;
  186. var codeType = '';
  187. for(var c of codeBlocks){
  188. var classNameStr = c.className.split(' ')[2];
  189. if (regex.test(classNameStr)){
  190. codeType = classNameStr.substr(9);
  191. }
  192. }
  193. //then generate the markdown codeblock
  194. result += "```" + codeType + "\n";
  195. Array.from(codeBlocks).forEach(block => {
  196. result += `${block.textContent}`;
  197. });
  198. result += "```\n";
  199. result += `\n\n`;
  200. return result;
  201. }
  202. };
  203. var TEXT_BLOCKS = Object.keys(elementMap);
  204.  
  205. var mdContent = chatBlocks.reduce((result, nextBlock, i) => {
  206. if (i % 2 === 0) { // title
  207. let p = replaceInnerNode(nextBlock);
  208. let text = markdownEscape(p.textContent, ["codeblocks", "number signs"]);
  209. let lines = text.split('\n');
  210. for (let j = 0; j < lines.length; j++) {
  211. result += `> ${lines[j]}\n`;
  212. }
  213. result += '\n';
  214. }else{
  215. //try to parse the block
  216. var iterator = document.createNodeIterator(
  217. nextBlock,
  218. NodeFilter.SHOW_ELEMENT,
  219. {
  220. acceptNode: element => TEXT_BLOCKS.indexOf(element.tagName.toUpperCase()) >= 0
  221. },
  222. false,
  223. );
  224. let next = iterator.nextNode();
  225. while (next) {
  226. result = elementMap[next.tagName.toUpperCase()](next, result);
  227. next = iterator.nextNode();
  228. }
  229. }
  230. return result;
  231. }, "");
  232. return mdContent;
  233. }
  234. //for copy button
  235. //var copyHtml = `<div id="__copy__" style="cursor:pointer;position: fixed;bottom: 210px;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>`;
  236. // for copy function
  237. //var copyElement = document.createElement("div");
  238. //document.body.appendChild(copyElement);
  239. //copyElement.outerHTML = copyHtml;
  240.  
  241. // listen and add element
  242. // select the body element
  243. var body = document.querySelector('body');
  244.  
  245. // create a new MutationObserver instance
  246. var observer = new MutationObserver(function(mutations) {
  247. // iterate over the mutations array
  248. mutations.forEach(function(mutation) {
  249. // if a div element was added to the body
  250. if (mutation.type === 'childList'){
  251. //TypeError: undefined is not an object (evaluating 'mutation.addedNodes[0].nodeName')
  252. if(mutation.addedNodes[0] && mutation.addedNodes[0].nodeName === 'DIV'
  253. && mutation.addedNodes[0].id === 'headlessui-portal-root') {
  254. // do something
  255. setTimeout(function(){var navListHidden = document.querySelector('#headlessui-portal-root').querySelector('div > div > div > div.flex > div.flex > div.flex > nav');
  256. addCopyButton(navListHidden);},300);
  257. }
  258. }
  259. });
  260. });
  261.  
  262. // set the observer options
  263. var options = {
  264. childList: true, // listen for changes to child nodes
  265. subtree: true // listen for changes in all descendant nodes
  266. };
  267.  
  268. // start observing the body element
  269. observer.observe(body, options);
  270.  
  271. function addCopyButton(navigationList) {
  272. if(navigationList.childNodes[2].text == 'Copy .md'){ //avoid duplicate
  273. return;
  274. }
  275. var date = new Date();
  276. var time = date.getTime();
  277. var id = "__copy__" + time;
  278. var copyButton = document.createElement("a");
  279. copyButton.id = id;
  280. copyButton.innerHTML = '<svg stroke="currentColor" fill="none" stroke-width="2" viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" class="w-4 h-4" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"></path><rect x="8" y="2" width="8" height="4" rx="1" ry="1"></rect></svg>'
  281. +'<span>Copy .md</span>';
  282. copyButton.className = 'flex py-3 px-3 items-center gap-3 rounded-md hover:bg-gray-500/10 transition-colors duration-200 text-white cursor-pointer text-sm';
  283. navigationList.insertBefore(copyButton, navigationList.childNodes[2]);
  284.  
  285. //for anchor
  286. var copyAnchor = document.getElementById(id);
  287. copyAnchor.addEventListener("click", () => {
  288. // Get the `span` element inside the `div`
  289. let span = copyAnchor.querySelector("span");
  290.  
  291. // Change the text of the `span` to "Done"
  292. span.innerText = "Copied!";
  293.  
  294. // Use `setTimeout` to change the text back to its original value after 3 seconds
  295. setTimeout(() => {
  296. span.innerText = "Copy .md";
  297. }, 1000);
  298.  
  299. // Perform the rest of the original code
  300. navigator.clipboard.writeText(toMarkdown()).then(() => {
  301. //alert("done");
  302. });
  303. });
  304. }
  305. //default case
  306. setTimeout(function(){
  307. var navList = document.querySelector('#__next').querySelector("div > div.hidden > div > div > nav");
  308. addCopyButton(navList);
  309. },600);
  310. //ensure next conversation works.
  311. setTimeout(function(){
  312. var nextConversationObserver = new MutationObserver(function(mutations) {
  313. mutations.forEach(function(mutation) {
  314. //console.log(" Mutation detected. Trying to add copy button...");
  315. });
  316. setTimeout(function(){
  317. var navList = document.querySelector('#__next').querySelector("div > div.hidden > div > div > nav");
  318. addCopyButton(navList);
  319. },400);
  320. });
  321. //console.log("Trying to setup observation...");
  322. nextConversationObserver.observe(document.querySelector("#__next"), { childList: true });
  323. //console.log("Over.");
  324. },1100);
  325. /**
  326. window.addEventListener("load", function (event) {
  327. // Your code here, for example:
  328. console.log("Page loaded");
  329. });
  330. **/
  331. })();