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

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 提交的版本,查看 最新版本

  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.3
  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. // @icon https://chat.openai.com/favicon-32x32.png
  12. // @grant none
  13. // ==/UserScript==
  14.  
  15. (function () {
  16. 'use strict';
  17. var mathFixEnabled = true;
  18. function toMarkdown() {
  19. var main = document.querySelector("main");
  20. var article = main.querySelector("div > div > div > div");
  21. var chatBlocks = Array.from(article.children)
  22. .filter(v => v.getAttribute("class").indexOf("border") >= 0);
  23.  
  24. var new_replacements = [
  25. //['\\', '\\\\', 'backslash'], //Don't need this any more cause it would be checked.
  26. ['`', '\\`', 'codeblocks'],
  27. ['*', '\\*', 'asterisk'],
  28. ['_', '\\_', 'underscores'],
  29. ['{', '\\{', 'crulybraces'],
  30. ['}', '\\}', 'crulybraces'],
  31. ['[', '\\[', 'square brackets'],
  32. [']', '\\]', 'square brackets'],
  33. ['(', '\\(', 'parentheses'],
  34. [')', '\\)', 'parentheses'],
  35. ['#', '\\#', 'number signs'],
  36. ['+', '\\+', 'plussign'],
  37. ['-', '\\-', 'hyphen'],
  38. ['.', '\\.', 'dot'],
  39. ['!', '\\!', 'exclamation mark'],
  40. ['>', '\\>', 'angle brackets']
  41. ];
  42.  
  43. // A State Machine used to match string and do replacement
  44. function replacementSkippingMath(string, char_pattern, replacement) {
  45. var inEquationState = 0; // 0:not in equation, 1:inline equation expecting $, 2: line euqation expecting $$
  46. var result = "";
  47. for (let i = 0; i < string.length; i++) {
  48. if(string[i] == '\\'){
  49. result += string[i];
  50. if (i+1 < string.length) result += string[i+1];
  51. i++; // one more add to skip escaped char
  52. continue;
  53. }
  54. switch(inEquationState){
  55. case 1:
  56. result += string[i];
  57. if(string[i] === '$'){
  58. inEquationState = 0; //simply exit and don't do further check
  59. continue;
  60. }
  61. break;
  62. case 2:
  63. result += string[i];
  64. if(string[i] === '$'){
  65. if (i+1 < string.length && string[i+1] === '$'){ //matched $$
  66. result += '$';
  67. inEquationState = 0;
  68. i++; // one more add
  69. }
  70. //else is unexpected behavior
  71. continue;
  72. }
  73. break;
  74. default:
  75. if(string[i] === '$'){
  76. if (i+1 < string.length && string[i+1] === '$'){//matched $$
  77. result += '$$';
  78. inEquationState = 2;
  79. i++; // one more add
  80. }else{ //matched $
  81. result += '$';
  82. inEquationState = 1;
  83. }
  84. continue;
  85. }else if(string[i] === char_pattern[0]){ //do replacement
  86. result += replacement;
  87. }else{
  88. result += string[i];
  89. }
  90. }
  91. }
  92.  
  93. return result;
  94. }
  95.  
  96. function markdownEscape(string, skips) {
  97. skips = skips || []
  98. //reduce function applied the function in the first with the second as input
  99. //this applies across the array with the first element inside as the initial 2nd param for the reduce func.
  100. return new_replacements.reduce(function (string, replacement) {
  101. var name = replacement[2]
  102. if (name && skips.indexOf(name) !== -1) {
  103. return string;
  104. } else {
  105. return replacementSkippingMath(string, replacement[0], replacement[1]);
  106. }
  107. }, string)
  108. }
  109.  
  110. function replaceInnerNode(element) {
  111. if (element.outerHTML) {
  112. var htmlBak = element.outerHTML;
  113. if(mathFixEnabled){
  114. //replace mathjax stuff
  115. var mathjaxBeginRegExp = /(<span class="MathJax_Preview".*?)<scr/s; //this is lazy
  116. var match = mathjaxBeginRegExp.exec(htmlBak);
  117. while(match){
  118. htmlBak = htmlBak.replace(match[1], '');
  119. //repalace math equations
  120. var latexMath;
  121. //match new line equations first
  122. var latexMathNLRegExp = /<script type="math\/tex; mode=display" id="MathJax-Element-\d+">(.*?)<\/script>/s;
  123. match = latexMathNLRegExp.exec(htmlBak);
  124. if(match){
  125. latexMath = "$$" + match[1] + "$$";
  126. htmlBak = htmlBak.replace(match[0], latexMath);
  127. }else{
  128. //then inline equations
  129. var latexMathRegExp = /<script type="math\/tex" id="MathJax-Element-\d+">(.*?)<\/script>/s;
  130. match = latexMathRegExp.exec(htmlBak);
  131. if(match){
  132. latexMath = "$" + match[1] + "$";
  133. htmlBak = htmlBak.replace(match[0], latexMath);
  134. }
  135. }
  136. match = mathjaxBeginRegExp.exec(htmlBak);
  137. }
  138. }
  139.  
  140. var parser = new DOMParser();
  141. //default code block replacement
  142. var nextDomString = htmlBak.replace(/<code>([\w\s-]*)<\/code>/g, (match) => {
  143. var doc = parser.parseFromString(match, "text/html");
  144. return "`" + (doc.body.textContent) + "`";
  145. });
  146. return parser.parseFromString(nextDomString, "text/html").body.children[0];
  147. }
  148. return element;
  149. }
  150.  
  151. var elementMap = {
  152. "P": function (element, result) {
  153. let p = replaceInnerNode(element);
  154. result += markdownEscape(p.textContent, ["codeblocks", "number signs"]);
  155. result += `\n\n`;
  156. return result;
  157. },
  158. //this should be unordered!
  159. "UL": function (element, result) {
  160. let ul = replaceInnerNode(element);
  161. Array.from(ul.querySelectorAll("li")).forEach((li, index) => {
  162. result += `- ${markdownEscape(li.textContent, ["codeblocks", "number signs"])}`;
  163. result += `\n`;
  164. });
  165. result += `\n\n`;
  166. return result;
  167. },
  168. "OL": function (element, result) {
  169. let ol = replaceInnerNode(element);
  170. Array.from(ol.querySelectorAll("li")).forEach((li, index) => {
  171. result += `${index + 1}. ${markdownEscape(li.textContent, ["codeblocks", "number signs"])}`;
  172. result += `\n`;
  173. });
  174. result += `\n\n`;
  175. return result;
  176. },
  177. "PRE": function (element, result) {
  178. var codeBlocks = element.querySelectorAll("code");
  179. //first get class name
  180. var regex = /^language-/;
  181. var codeType = '';
  182. for(var c of codeBlocks){
  183. var classNameStr = c.className.split(' ')[2];
  184. if (regex.test(classNameStr)){
  185. codeType = classNameStr.substr(9);
  186. }
  187. }
  188. //then generate the markdown codeblock
  189. result += "```" + codeType + "\n";
  190. Array.from(codeBlocks).forEach(block => {
  191. result += `${block.textContent}`;
  192. });
  193. result += "```\n";
  194. result += `\n\n`;
  195. return result;
  196. }
  197. };
  198. var TEXT_BLOCKS = Object.keys(elementMap);
  199.  
  200. var mdContent = chatBlocks.reduce((result, nextBlock, i) => {
  201. if (i % 2 === 0) { // title
  202. let p = replaceInnerNode(nextBlock);
  203. result += `> ${markdownEscape(p.textContent, ["codeblocks", "number signs"])}`;
  204. result += `\n\n`;
  205. }else{
  206. //try to parse the block
  207. var iterator = document.createNodeIterator(
  208. nextBlock,
  209. NodeFilter.SHOW_ELEMENT,
  210. {
  211. acceptNode: element => TEXT_BLOCKS.indexOf(element.tagName.toUpperCase()) >= 0
  212. },
  213. false,
  214. );
  215. let next = iterator.nextNode();
  216. while (next) {
  217. result = elementMap[next.tagName.toUpperCase()](next, result);
  218. next = iterator.nextNode();
  219. }
  220. }
  221. return result;
  222. }, "");
  223. return mdContent;
  224. }
  225. //for copy button
  226. 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>`;
  227. // for copy function
  228. var copyElement = document.createElement("div");
  229. document.body.appendChild(copyElement);
  230. copyElement.outerHTML = copyHtml;
  231. // for button style
  232. document.querySelector('#__copy__').addEventListener('mouseenter', function() {
  233. this.style.background = '#555555';
  234. this.style.color = 'white';
  235. });
  236. document.querySelector('#__copy__').addEventListener('mouseleave', function() {
  237. this.style.background = '#333333';
  238. this.style.color = 'white';
  239. });
  240. document.querySelector('#__copy__').addEventListener('mousedown', function() {
  241. this.style.boxShadow = '2px 2px 2px #333333';
  242. });
  243. document.querySelector('#__copy__').addEventListener('mouseup', function() {
  244. this.style.boxShadow = 'none';
  245. });
  246. //for anchor
  247. var copyAnchor = document.getElementById("__copy__");
  248. copyAnchor.addEventListener("click", () => {
  249. // Get the `span` element inside the `div`
  250. let span = copyAnchor.querySelector("span");
  251.  
  252. // Change the text of the `span` to "Done"
  253. span.innerText = "Copied!";
  254.  
  255. // Use `setTimeout` to change the text back to its original value after 3 seconds
  256. setTimeout(() => {
  257. span.innerText = "Copy .md";
  258. }, 1000);
  259.  
  260. // Perform the rest of the original code
  261. navigator.clipboard.writeText(toMarkdown()).then(() => {
  262. //alert("done");
  263. });
  264. });
  265. })();