支持数学公式的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-13 提交的版本,查看 最新版本

  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.4
  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: 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>`;
  227. // for copy function
  228. //var copyElement = document.createElement("div");
  229. //document.body.appendChild(copyElement);
  230. //copyElement.outerHTML = copyHtml;
  231.  
  232. // listen and add element
  233. // select the body element
  234. var body = document.querySelector('body');
  235.  
  236. // create a new MutationObserver instance
  237. var observer = new MutationObserver(function(mutations) {
  238. // iterate over the mutations array
  239. mutations.forEach(function(mutation) {
  240. // if a div element was added to the body
  241. if (mutation.type === 'childList'
  242. && mutation.addedNodes[0].nodeName === 'DIV'
  243. && mutation.addedNodes[0].id === 'headlessui-portal-root') {
  244. // do something
  245. setTimeout(function(){var navListHidden = document.querySelector('#headlessui-portal-root').querySelector('div > div > div > div.flex > div.flex > div.flex > nav');
  246. addCopyButton(navListHidden);},300);
  247. }
  248. });
  249. });
  250.  
  251. // set the observer options
  252. var options = {
  253. childList: true, // listen for changes to child nodes
  254. subtree: true // listen for changes in all descendant nodes
  255. };
  256.  
  257. // start observing the body element
  258. observer.observe(body, options);
  259.  
  260. function addCopyButton(navigationList) {
  261. var date = new Date();
  262. var time = date.getTime();
  263. var id = "__copy__" + time;
  264. var copyButton = document.createElement("a");
  265. copyButton.id = id;
  266. 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>'
  267. +'<span>Copy .md</span>';
  268. 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';
  269. navigationList.insertBefore(copyButton, navigationList.childNodes[2]);
  270.  
  271. //for anchor
  272. var copyAnchor = document.getElementById(id);
  273. copyAnchor.addEventListener("click", () => {
  274. // Get the `span` element inside the `div`
  275. let span = copyAnchor.querySelector("span");
  276.  
  277. // Change the text of the `span` to "Done"
  278. span.innerText = "Copied!";
  279.  
  280. // Use `setTimeout` to change the text back to its original value after 3 seconds
  281. setTimeout(() => {
  282. span.innerText = "Copy .md";
  283. }, 1000);
  284.  
  285. // Perform the rest of the original code
  286. navigator.clipboard.writeText(toMarkdown()).then(() => {
  287. //alert("done");
  288. });
  289. });
  290. }
  291. setTimeout(function(){var navList = document.querySelector('#__next').querySelector("div > div.hidden > div > div > nav");
  292. addCopyButton(navList);},300);
  293. })();

QingJ © 2025

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