ChatGPT LaTeX Auto Render (OpenAI, new bing, you, etc.)

自动渲染 ChatGPT 页面 (OpenAI, new bing, you 等) 上的 LaTeX 数学公式。

当前为 2023-04-08 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name ChatGPT LaTeX Auto Render (OpenAI, new bing, you, etc.)
  3. // @version 0.5.8
  4. // @author Scruel Tao
  5. // @homepage https://github.com/scruel/tampermonkey-scripts
  6. // @description Auto typeset LaTeX math formulas on ChatGPT pages (OpenAI, new bing, you, etc.).
  7. // @description:zh-CN 自动渲染 ChatGPT 页面 (OpenAI, new bing, you 等) 上的 LaTeX 数学公式。
  8. // @match https://chat.openai.com/*
  9. // @match https://platform.openai.com/playground/*
  10. // @match https://www.bing.com/search?*
  11. // @match https://you.com/search?*&tbm=youchat*
  12. // @match https://www.you.com/search?*&tbm=youchat*
  13. // @namespace http://tampermonkey.net/
  14. // @icon https://chat.openai.com/favicon.ico
  15. // @grant none
  16. // @noframes
  17. // ==/UserScript==
  18.  
  19. 'use strict';
  20.  
  21. const _parsed_mark = '_sc_parsed';
  22. const MARKDOWN_RERENDER_MARK = 'sc_mktag';
  23. const MARKDOWN_RERENDER_LEN = 'sc_mklen';
  24. const MARKDOWN_RERENDER_REGEX = new RegExp('<!--' + MARKDOWN_RERENDER_MARK + ',(.*?)-->', 'g');
  25.  
  26. function queryAddNoParsed(query) {
  27. return query + ":not([" + _parsed_mark + "])";
  28. }
  29.  
  30. function getAllCommentNodes(ele) {
  31. var comments = [];
  32. var iterator = document.createNodeIterator(ele, NodeFilter.SHOW_COMMENT, () => NodeFilter.FILTER_ACCEPT, false);
  33. var curNode;
  34. while (curNode = iterator.nextNode()) {
  35. comments.push(curNode);
  36. }
  37. return comments;
  38. }
  39.  
  40. function restoreMarkdown(msgEle, tagName, wrapperSymbol) {
  41. const eles = msgEle.querySelectorAll(tagName);
  42. eles.forEach(e => {
  43. const restoredNodes = document.createRange().createContextualFragment(e.innerHTML);
  44. const fn = restoredNodes.childNodes[0];
  45. const ln = restoredNodes.childNodes[restoredNodes.childNodes.length - 1]
  46. fn.textContent = wrapperSymbol + fn.textContent;
  47. ln.textContent = ln.textContent + wrapperSymbol;
  48. restoredNodes.prepend(document.createComment(MARKDOWN_RERENDER_MARK + ",<" + tagName + " " + MARKDOWN_RERENDER_LEN + "=" + wrapperSymbol.length + ">"));
  49. restoredNodes.append(document.createComment(MARKDOWN_RERENDER_MARK + ",</" + tagName + ">"));
  50. e.parentElement.insertBefore(restoredNodes, e);
  51. e.parentNode.removeChild(e);
  52. });
  53. }
  54.  
  55. function restoreAllMarkdown(msgEle) {
  56. restoreMarkdown(msgEle, 'em', '_');
  57. }
  58.  
  59. function rerenderAllMarkdown(msgEle) {
  60. const mjxEles = msgEle.querySelectorAll('mjx-container');
  61. msgEle.innerHTML = msgEle.innerHTML.replaceAll(MARKDOWN_RERENDER_REGEX, '$1');
  62. const eles = document.querySelectorAll('*[' + MARKDOWN_RERENDER_LEN + ']');
  63. eles.forEach(e => {
  64. const wrapperLen = parseInt(e.getAttribute(MARKDOWN_RERENDER_LEN))
  65. e.childNodes[0].textContent = e.childNodes[0].textContent.substring(wrapperLen);
  66. const lastNodeContent = e.childNodes[e.childNodes.length - 1].textContent
  67. e.childNodes[e.childNodes.length - 1].textContent = lastNodeContent.substring(0, lastNodeContent.length - wrapperLen - 1);
  68. });
  69. // Restore mjx elements which have listeners
  70. const newMjxEles = msgEle.querySelectorAll('mjx-container');
  71. for (let i = 0; i < newMjxEles.length; ++i) {
  72. const e = newMjxEles[i];
  73. e.parentElement.insertBefore(mjxEles[i], e);
  74. e.parentNode.removeChild(e);
  75. };
  76. }
  77.  
  78. async function prepareScript() {
  79. window._sc_beforeTypesetMsgEle = (msgEle) => {};
  80. window._sc_afterTypesetMsgEle = (msgEle) => {};
  81. window._sc_typeset = () => {
  82. try {
  83. const msgEles = window._sc_getMsgEles();
  84. msgEles.forEach(msgEle => {
  85. restoreAllMarkdown(msgEle);
  86. msgEle.setAttribute(_parsed_mark,'');
  87.  
  88. window._sc_beforeTypesetMsgEle(msgEle);
  89. MathJax.typesetPromise([msgEle]);
  90. window._sc_afterTypesetMsgEle(msgEle);
  91.  
  92. rerenderAllMarkdown(msgEle);
  93. });
  94. } catch (e) {
  95. console.warn(e);
  96. }
  97. }
  98. window._sc_mutationHandler = (mutation) => {
  99. if (mutation.oldValue === '') {
  100. window._sc_typeset();
  101. }
  102. };
  103. window._sc_chatLoaded = () => { return true; };
  104. window._sc_getObserveElement = () => { return null; };
  105. var observerOptions = {
  106. attributeOldValue : true,
  107. attributeFilter: ['cancelable', 'disabled'],
  108. };
  109. var afterMainOvservationStart = () => { window._sc_typeset(); };
  110.  
  111. // Handle special cases per site.
  112. if (window.location.host == "www.bing.com") {
  113. window._sc_getObserveElement = () => {
  114. const ele = document.querySelector("#b_sydConvCont > cib-serp");
  115. if (!ele) {return null;}
  116. return ele.shadowRoot.querySelector("#cib-action-bar-main");
  117. }
  118.  
  119. const getContMsgEles = (cont, isInChat=true) => {
  120. if (!cont) {
  121. return [];
  122. }
  123. const allChatTurn = cont.shadowRoot.querySelector("#cib-conversation-main").shadowRoot.querySelectorAll("cib-chat-turn");
  124. var lastChatTurnSR = allChatTurn[allChatTurn.length - 1];
  125. if (isInChat) { lastChatTurnSR = lastChatTurnSR.shadowRoot; }
  126. const allCibMsgGroup = lastChatTurnSR.querySelectorAll("cib-message-group");
  127. const allCibMsg = Array.from(allCibMsgGroup).map(e => Array.from(e.shadowRoot.querySelectorAll("cib-message"))).flatMap(e => e);
  128. return Array.from(allCibMsg).map(cibMsg => cibMsg.shadowRoot.querySelector("cib-shared")).filter(e => e);
  129. }
  130. window._sc_getMsgEles = () => {
  131. try {
  132. const convCont = document.querySelector("#b_sydConvCont > cib-serp");
  133. const tigerCont = document.querySelector("#b_sydTigerCont > cib-serp");
  134. return getContMsgEles(convCont).concat(getContMsgEles(tigerCont, false));
  135. } catch (ignore) {
  136. return [];
  137. }
  138. }
  139. }
  140. else if (window.location.host == "chat.openai.com") {
  141. window._sc_getObserveElement = () => {
  142. return document.querySelector("main form textarea+button");
  143. }
  144. window._sc_chatLoaded = () => { return document.querySelector('main div.text-sm>svg.animate-spin') === null; };
  145.  
  146. afterMainOvservationStart = () => {
  147. window._sc_typeset();
  148. // Handle conversation switch
  149. new MutationObserver((mutationList) => {
  150. mutationList.forEach(async (mutation) => {
  151. if (mutation.addedNodes){
  152. window._sc_typeset();
  153. startMainOvservation(await getMainObserveElement(true), observerOptions);
  154. }
  155. });
  156. }).observe(document.querySelector('#__next'), {childList: true});
  157. };
  158.  
  159. window._sc_getMsgEles = () => {
  160. return document.querySelectorAll(queryAddNoParsed("div.w-full div.text-base div.items-start"));
  161. }
  162.  
  163. window._sc_beforeTypesetMsgEle = (msgEle) => {
  164. // Prevent latex typeset conflict
  165. const displayEles = msgEle.querySelectorAll('.math-display');
  166. displayEles.forEach(e => {
  167. const texEle = e.querySelector(".katex-mathml annotation");
  168. e.removeAttribute("class");
  169. e.textContent = "$$" + texEle.textContent + "$$";
  170. });
  171. const inlineEles = msgEle.querySelectorAll('.math-inline');
  172. inlineEles.forEach(e => {
  173. const texEle = e.querySelector(".katex-mathml annotation");
  174. e.removeAttribute("class");
  175. // e.textContent = "$" + texEle.textContent + "$";
  176. // Mathjax will typeset this with display mode.
  177. e.textContent = "$$" + texEle.textContent + "$$";
  178.  
  179. });
  180. };
  181. window._sc_afterTypesetMsgEle = (msgEle) => {
  182. // https://github.com/mathjax/MathJax/issues/3008
  183. msgEle.style.display = 'unset';
  184. }
  185. }
  186. else if (window.location.host == "you.com" || window.location.host == "www.you.com") {
  187. window._sc_getObserveElement = () => {
  188. return document.querySelector('#chatHistory');
  189. };
  190. window._sc_chatLoaded = () => { return document.querySelector('#chatHistory div[data-pinnedconversationturnid]'); };
  191.  
  192. observerOptions = {
  193. childList : true
  194. };
  195.  
  196. window._sc_mutationHandler = (mutation) => {
  197. mutation.addedNodes.forEach(e => {
  198. const attr = e.getAttribute('data-testid')
  199. if (attr && attr.startsWith("youchat-convTurn")) {
  200. startTurnAttrObservationForTypesetting(e, 'data-pinnedconversationturnid');
  201. }
  202. })
  203. };
  204.  
  205. window._sc_getMsgEles = () => {
  206. return document.querySelectorAll(queryAddNoParsed('#chatHistory div[data-testid="youchat-answer"]'));
  207. };
  208. }
  209. console.log('Waiting for chat loading...')
  210. const mainElement = await getMainObserveElement();
  211. console.log('Chat loaded.')
  212. startMainOvservation(mainElement, observerOptions);
  213. afterMainOvservationStart();
  214. }
  215.  
  216. // After output completed, the attribute of turn element will be changed,
  217. // only with observer won't be enough, so we have this function for sure.
  218. function startTurnAttrObservationForTypesetting(element, doneWithAttr) {
  219. const tmpObserver = new MutationObserver((mutationList, observer) => {
  220. mutationList.forEach(mutation => {
  221. if (mutation.oldValue === null) {
  222. window._sc_typeset();
  223. observer.disconnect;
  224. }
  225. })
  226. });
  227. tmpObserver.observe(element, {
  228. attributeOldValue : true,
  229. attributeFilter: [doneWithAttr],
  230. });
  231. if (element.hasAttribute(doneWithAttr)) {
  232. window._sc_typeset();
  233. tmpObserver.disconnect;
  234. }
  235. }
  236.  
  237. function getMainObserveElement(chatLoaded=false) {
  238. return new Promise(async (resolve, reject) => {
  239. const resolver = () => {
  240. const ele = window._sc_getObserveElement();
  241. if (ele && (chatLoaded || window._sc_chatLoaded())) {
  242. return resolve(ele);
  243. }
  244. window.setTimeout(resolver, 500);
  245. }
  246. resolver();
  247. });
  248. }
  249.  
  250. function startMainOvservation(mainElement, observerOptions) {
  251. const callback = (mutationList, observer) => {
  252. mutationList.forEach(mutation => {
  253. window._sc_mutationHandler(mutation);
  254. });
  255. };
  256. if (window._sc_mainObserver) {
  257. window._sc_mainObserver.disconnect();
  258. }
  259. window._sc_mainObserver = new MutationObserver(callback);
  260. window._sc_mainObserver.observe(mainElement, observerOptions);
  261. }
  262.  
  263. async function addScript(url) {
  264. const scriptElement = document.createElement('script');
  265. const headElement = document.getElementsByTagName('head')[0] || document.documentElement;
  266. if (!headElement.appendChild(scriptElement)) {
  267. // Prevent appendChild overwritten problem.
  268. headElement.append(scriptElement);
  269. }
  270. scriptElement.src = url;
  271. }
  272.  
  273. async function waitMathJaxLoaded() {
  274. while (!MathJax.hasOwnProperty('typeset')) {
  275. if (window._sc_ChatLatex.loadCount > 20000 / 200) {
  276. setTipsElementText("Failed to load MathJax, try refresh.", true);
  277. }
  278. await new Promise((x) => setTimeout(x, 500));
  279. window._sc_ChatLatex.loadCount += 1;
  280. }
  281. }
  282.  
  283. function showTipsElement() {
  284. const tipsElement = window._sc_ChatLatex.tipsElement;
  285. tipsElement.style.position = "fixed";
  286. tipsElement.style.right = "10px";
  287. tipsElement.style.top = "10px";
  288. tipsElement.style.background = '#333';
  289. tipsElement.style.color = '#fff';
  290. tipsElement.style.zIndex = '999999';
  291. var tipContainer = document.body.querySelector('header');
  292. if (!tipContainer) {
  293. tipContainer = document.body;
  294. }
  295. tipContainer.appendChild(tipsElement);
  296. }
  297.  
  298. function setTipsElementText(text, errorRaise=false) {
  299. window._sc_ChatLatex.tipsElement.innerHTML = text;
  300. if (errorRaise) {
  301. throw text;
  302. }
  303. console.log(text);
  304. }
  305.  
  306. function hideTipsElement(timeout=3) {
  307. window.setTimeout(() => {window._sc_ChatLatex.tipsElement.hidden=true; }, 3000);
  308. }
  309.  
  310. async function loadMathJax() {
  311. showTipsElement();
  312. setTipsElementText("Loading MathJax...");
  313. addScript('https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js');
  314. await waitMathJaxLoaded();
  315. setTipsElementText("MathJax Loaded.");
  316. hideTipsElement();
  317. }
  318.  
  319. (async function() {
  320. window._sc_ChatLatex = {
  321. tipsElement: document.createElement("div"),
  322. loadCount: 0
  323. };
  324. window.MathJax = {
  325. tex: {
  326. inlineMath: [['$', '$'], ['\\(', '\\)']],
  327. displayMath : [['$$', '$$', ['\\[', '\\]']]]
  328. },
  329. startup: {
  330. typeset: false
  331. }
  332. };
  333.  
  334. await loadMathJax();
  335. await prepareScript();
  336. })();