支持数学公式的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 赵巍໖.

当前为 2023-02-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.5
  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.  
  25. var new_replacements = [
  26. //['\\', '\\\\', 'backslash'], //Don't need this any more cause it would be checked.
  27. ['`', '\\`', 'codeblocks'],
  28. ['*', '\\*', 'asterisk'],
  29. ['_', '\\_', 'underscores'],
  30. ['{', '\\{', 'crulybraces'],
  31. ['}', '\\}', 'crulybraces'],
  32. ['[', '\\[', 'square brackets'],
  33. [']', '\\]', 'square brackets'],
  34. ['(', '\\(', 'parentheses'],
  35. [')', '\\)', 'parentheses'],
  36. ['#', '\\#', 'number signs'],
  37. ['+', '\\+', 'plussign'],
  38. ['-', '\\-', 'hyphen'],
  39. ['.', '\\.', 'dot'],
  40. ['!', '\\!', 'exclamation mark'],
  41. ['>', '\\>', 'angle brackets']
  42. ];
  43.  
  44. // A State Machine used to match string and do replacement
  45. function replacementSkippingMath(string, char_pattern, replacement) {
  46. var inEquationState = 0; // 0:not in equation, 1:inline equation expecting $, 2: line euqation expecting $$
  47. var result = "";
  48. for (let i = 0; i < string.length; i++) {
  49. if(string[i] == '\\'){
  50. result += string[i];
  51. if (i+1 < string.length) result += string[i+1];
  52. i++; // one more add to skip escaped char
  53. continue;
  54. }
  55. switch(inEquationState){
  56. case 1:
  57. result += string[i];
  58. if(string[i] === '$'){
  59. inEquationState = 0; //simply exit and don't do further check
  60. continue;
  61. }
  62. break;
  63. case 2:
  64. result += string[i];
  65. if(string[i] === '$'){
  66. if (i+1 < string.length && string[i+1] === '$'){ //matched $$
  67. result += '$';
  68. inEquationState = 0;
  69. i++; // one more add
  70. }
  71. //else is unexpected behavior
  72. continue;
  73. }
  74. break;
  75. default:
  76. if(string[i] === '$'){
  77. if (i+1 < string.length && string[i+1] === '$'){//matched $$
  78. result += '$$';
  79. inEquationState = 2;
  80. i++; // one more add
  81. }else{ //matched $
  82. result += '$';
  83. inEquationState = 1;
  84. }
  85. continue;
  86. }else if(string[i] === char_pattern[0]){ //do replacement
  87. result += replacement;
  88. }else{
  89. result += string[i];
  90. }
  91. }
  92. }
  93.  
  94. return result;
  95. }
  96.  
  97. function markdownEscape(string, skips) {
  98. skips = skips || []
  99. //reduce function applied the function in the first with the second as input
  100. //this applies across the array with the first element inside as the initial 2nd param for the reduce func.
  101. return new_replacements.reduce(function (string, replacement) {
  102. var name = replacement[2]
  103. if (name && skips.indexOf(name) !== -1) {
  104. return string;
  105. } else {
  106. return replacementSkippingMath(string, replacement[0], replacement[1]);
  107. }
  108. }, string)
  109. }
  110.  
  111. function replaceInnerNode(element) {
  112. if (element.outerHTML) {
  113. var htmlBak = element.outerHTML;
  114. if(mathFixEnabled){
  115. //replace mathjax stuff
  116. var mathjaxBeginRegExp = /(<span class="MathJax_Preview".*?)<scr/s; //this is lazy
  117. var match = mathjaxBeginRegExp.exec(htmlBak);
  118. while(match){
  119. htmlBak = htmlBak.replace(match[1], '');
  120. //repalace math equations
  121. var latexMath;
  122. //match new line equations first
  123. var latexMathNLRegExp = /<script type="math\/tex; mode=display" id="MathJax-Element-\d+">(.*?)<\/script>/s;
  124. match = latexMathNLRegExp.exec(htmlBak);
  125. if(match){
  126. latexMath = "$$" + match[1] + "$$";
  127. htmlBak = htmlBak.replace(match[0], latexMath);
  128. }else{
  129. //then inline equations
  130. var latexMathRegExp = /<script type="math\/tex" id="MathJax-Element-\d+">(.*?)<\/script>/s;
  131. match = latexMathRegExp.exec(htmlBak);
  132. if(match){
  133. latexMath = "$" + match[1] + "$";
  134. htmlBak = htmlBak.replace(match[0], latexMath);
  135. }
  136. }
  137. match = mathjaxBeginRegExp.exec(htmlBak);
  138. }
  139. }
  140.  
  141. var parser = new DOMParser();
  142. //default code block replacement
  143. var nextDomString = htmlBak.replace(/<code>([\w\s-]*)<\/code>/g, (match) => {
  144. var doc = parser.parseFromString(match, "text/html");
  145. return "`" + (doc.body.textContent) + "`";
  146. });
  147. return parser.parseFromString(nextDomString, "text/html").body.children[0];
  148. }
  149. return element;
  150. }
  151.  
  152. var elementMap = {
  153. "P": function (element, result) {
  154. let p = replaceInnerNode(element);
  155. result += markdownEscape(p.textContent, ["codeblocks", "number signs"]);
  156. result += `\n\n`;
  157. return result;
  158. },
  159. //this should be unordered!
  160. "UL": function (element, result) {
  161. let ul = replaceInnerNode(element);
  162. Array.from(ul.querySelectorAll("li")).forEach((li, index) => {
  163. result += `- ${markdownEscape(li.textContent, ["codeblocks", "number signs"])}`;
  164. result += `\n`;
  165. });
  166. result += `\n\n`;
  167. return result;
  168. },
  169. "OL": function (element, result) {
  170. let ol = replaceInnerNode(element);
  171. var olStart = parseInt(ol.getAttribute("start") || "1"); //bug fix thanks to original author
  172. Array.from(ol.querySelectorAll("li")).forEach((li, index) => {
  173. result += `${index + olStart}. ${markdownEscape(li.textContent, ["codeblocks", "number signs"])}`;
  174. result += `\n`;
  175. });
  176. result += `\n\n`;
  177. return result;
  178. },
  179. "PRE": function (element, result) {
  180. var codeBlocks = element.querySelectorAll("code");
  181. //first get class name
  182. var regex = /^language-/;
  183. var codeType = '';
  184. for(var c of codeBlocks){
  185. var classNameStr = c.className.split(' ')[2];
  186. if (regex.test(classNameStr)){
  187. codeType = classNameStr.substr(9);
  188. }
  189. }
  190. //then generate the markdown codeblock
  191. result += "```" + codeType + "\n";
  192. Array.from(codeBlocks).forEach(block => {
  193. result += `${block.textContent}`;
  194. });
  195. result += "```\n";
  196. result += `\n\n`;
  197. return result;
  198. }
  199. };
  200. var TEXT_BLOCKS = Object.keys(elementMap);
  201.  
  202. var mdContent = chatBlocks.reduce((result, nextBlock, i) => {
  203. if (i % 2 === 0) { // title
  204. let p = replaceInnerNode(nextBlock);
  205. result += `> ${markdownEscape(p.textContent, ["codeblocks", "number signs"])}`;
  206. result += `\n\n`;
  207. }else{
  208. //try to parse the block
  209. var iterator = document.createNodeIterator(
  210. nextBlock,
  211. NodeFilter.SHOW_ELEMENT,
  212. {
  213. acceptNode: element => TEXT_BLOCKS.indexOf(element.tagName.toUpperCase()) >= 0
  214. },
  215. false,
  216. );
  217. let next = iterator.nextNode();
  218. while (next) {
  219. result = elementMap[next.tagName.toUpperCase()](next, result);
  220. next = iterator.nextNode();
  221. }
  222. }
  223. return result;
  224. }, "");
  225. return mdContent;
  226. }
  227. //for copy button
  228. //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>`;
  229. // for copy function
  230. //var copyElement = document.createElement("div");
  231. //document.body.appendChild(copyElement);
  232. //copyElement.outerHTML = copyHtml;
  233.  
  234. // listen and add element
  235. // select the body element
  236. var body = document.querySelector('body');
  237.  
  238. // create a new MutationObserver instance
  239. var observer = new MutationObserver(function(mutations) {
  240. // iterate over the mutations array
  241. mutations.forEach(function(mutation) {
  242. // if a div element was added to the body
  243. if (mutation.type === 'childList'){
  244. //TypeError: undefined is not an object (evaluating 'mutation.addedNodes[0].nodeName')
  245. if(mutation.addedNodes[0] && mutation.addedNodes[0].nodeName === 'DIV'
  246. && mutation.addedNodes[0].id === 'headlessui-portal-root') {
  247. // do something
  248. setTimeout(function(){var navListHidden = document.querySelector('#headlessui-portal-root').querySelector('div > div > div > div.flex > div.flex > div.flex > nav');
  249. addCopyButton(navListHidden);},300);
  250. }
  251. }
  252. });
  253. });
  254.  
  255. // set the observer options
  256. var options = {
  257. childList: true, // listen for changes to child nodes
  258. subtree: true // listen for changes in all descendant nodes
  259. };
  260.  
  261. // start observing the body element
  262. observer.observe(body, options);
  263.  
  264. function addCopyButton(navigationList) {
  265. if(navigationList.childNodes[2].text == 'Copy .md'){ //avoid duplicate
  266. return;
  267. }
  268. var date = new Date();
  269. var time = date.getTime();
  270. var id = "__copy__" + time;
  271. var copyButton = document.createElement("a");
  272. copyButton.id = id;
  273. 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>'
  274. +'<span>Copy .md</span>';
  275. 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';
  276. navigationList.insertBefore(copyButton, navigationList.childNodes[2]);
  277.  
  278. //for anchor
  279. var copyAnchor = document.getElementById(id);
  280. copyAnchor.addEventListener("click", () => {
  281. // Get the `span` element inside the `div`
  282. let span = copyAnchor.querySelector("span");
  283.  
  284. // Change the text of the `span` to "Done"
  285. span.innerText = "Copied!";
  286.  
  287. // Use `setTimeout` to change the text back to its original value after 3 seconds
  288. setTimeout(() => {
  289. span.innerText = "Copy .md";
  290. }, 1000);
  291.  
  292. // Perform the rest of the original code
  293. navigator.clipboard.writeText(toMarkdown()).then(() => {
  294. //alert("done");
  295. });
  296. });
  297. }
  298. //default case
  299. setTimeout(function(){
  300. var navList = document.querySelector('#__next').querySelector("div > div.hidden > div > div > nav");
  301. addCopyButton(navList);
  302. },500);
  303. //ensure next conversation works.
  304. setTimeout(function(){
  305. var nextConversationObserver = new MutationObserver(function(mutations) {
  306. mutations.forEach(function(mutation) {
  307. //console.log(" Mutation detected. Trying to add copy button...");
  308. });
  309. setTimeout(function(){
  310. var navList = document.querySelector('#__next').querySelector("div > div.hidden > div > div > nav");
  311. addCopyButton(navList);
  312. },300);
  313. });
  314. //console.log("Trying to setup observation...");
  315. nextConversationObserver.observe(document.querySelector("#__next"), { childList: true });
  316. //console.log("Over.");
  317. },1000);
  318. /**
  319. window.addEventListener("load", function (event) {
  320. // Your code here, for example:
  321. console.log("Page loaded");
  322. });
  323. **/
  324. })();

QingJ © 2025

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