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

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

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

  1. // ==UserScript==
  2. // @name ChatGPT LaTeX Auto Render (OpenAI, new bing, you, etc.)
  3. // @version 0.6.3
  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.  
  24. const MARKDOWN_SYMBOL_UNDERLINE = 'XXXSCUEDLXXX'
  25. const MARKDOWN_SYMBOL_ASTERISK = 'XXXSCAESKXXX'
  26.  
  27. function queryAddNoParsed(query) {
  28. return query + ":not([" + PARSED_MARK + "])";
  29. }
  30.  
  31. function showTipsElement() {
  32. const tipsElement = window._sc_ChatLatex.tipsElement;
  33. tipsElement.style.position = "fixed";
  34. tipsElement.style.right = "10px";
  35. tipsElement.style.top = "10px";
  36. tipsElement.style.background = '#333';
  37. tipsElement.style.color = '#fff';
  38. tipsElement.style.zIndex = '999999';
  39. var tipContainer = document.body.querySelector('header');
  40. if (!tipContainer) {
  41. tipContainer = document.body;
  42. }
  43. tipContainer.appendChild(tipsElement);
  44. }
  45.  
  46. function setTipsElementText(text, errorRaise=false) {
  47. window._sc_ChatLatex.tipsElement.innerHTML = text;
  48. if (errorRaise) {
  49. throw text;
  50. }
  51. console.log(text);
  52. }
  53.  
  54. async function addScript(url) {
  55. const scriptElement = document.createElement('script');
  56. const headElement = document.getElementsByTagName('head')[0] || document.documentElement;
  57. if (!headElement.appendChild(scriptElement)) {
  58. // Prevent appendChild overwritten problem.
  59. headElement.append(scriptElement);
  60. }
  61. scriptElement.src = url;
  62. }
  63.  
  64. function traverseDOM(element, callback, onlySingle=true) {
  65. if (!onlySingle || !element.hasChildNodes()) {
  66. callback(element);
  67. }
  68. element = element.firstChild;
  69. while (element) {
  70. traverseDOM(element, callback, onlySingle);
  71. element = element.nextSibling;
  72. }
  73. }
  74.  
  75. function getExtraInfoAddedMKContent(content) {
  76. // Ensure that the whitespace before and after the same
  77. content = content.replaceAll(/( *\*+ *)/g, MARKDOWN_SYMBOL_ASTERISK + '$1');
  78. content = content.replaceAll(/( *_+ *)/g, MARKDOWN_SYMBOL_UNDERLINE + '$1');
  79. // Ensure render for single line
  80. content = content.replaceAll(new RegExp(`^${MARKDOWN_SYMBOL_ASTERISK}(\\*+)`, 'gm'), `${MARKDOWN_SYMBOL_ASTERISK} $1`);
  81. content = content.replaceAll(new RegExp(`^${MARKDOWN_SYMBOL_UNDERLINE}(_+)`, 'gm'), `${MARKDOWN_SYMBOL_UNDERLINE} $1`);
  82. return content;
  83. }
  84.  
  85. function removeMKExtraInfo(ele) {
  86. traverseDOM(ele, function(e) {
  87. if (e.textContent){
  88. e.textContent = e.textContent.replaceAll(MARKDOWN_SYMBOL_UNDERLINE, '');
  89. e.textContent = e.textContent.replaceAll(MARKDOWN_SYMBOL_ASTERISK, '');
  90. }
  91. });
  92. }
  93.  
  94. function getLastMKSymbol(ele, defaultSymbol) {
  95. if (!ele) { return defaultSymbol; }
  96. const content = ele.textContent.trim();
  97. if (content.endsWith(MARKDOWN_SYMBOL_UNDERLINE)) { return '_'; }
  98. if (content.endsWith(MARKDOWN_SYMBOL_ASTERISK)) { return '*'; }
  99. return defaultSymbol;
  100. }
  101.  
  102. function restoreMarkdown(msgEle, tagName, defaultSymbol) {
  103. const eles = msgEle.querySelectorAll(tagName);
  104. eles.forEach(e => {
  105. const restoredNodes = document.createRange().createContextualFragment(e.innerHTML);
  106. const fn = restoredNodes.childNodes[0];
  107. const ln = restoredNodes.childNodes[restoredNodes.childNodes.length - 1]
  108. const wrapperSymbol = getLastMKSymbol(e.previousSibling, defaultSymbol);
  109. fn.textContent = wrapperSymbol + fn.textContent;
  110. ln.textContent = ln.textContent + wrapperSymbol;
  111. restoredNodes.prepend(document.createComment(MARKDOWN_RERENDER_MARK + "|0|" + tagName + "|" + wrapperSymbol.length));
  112. restoredNodes.append(document.createComment(MARKDOWN_RERENDER_MARK + "|1|" + tagName));
  113. e.parentElement.insertBefore(restoredNodes, e);
  114. e.parentNode.removeChild(e);
  115. });
  116. removeMKExtraInfo(msgEle);
  117. }
  118.  
  119. function restoreAllMarkdown(msgEle) {
  120. restoreMarkdown(msgEle, 'em', '_');
  121. }
  122.  
  123. function rerenderAllMarkdown(msgEle) {
  124. // restore HTML from restored markdown comment info
  125. const startComments = [];
  126. traverseDOM(msgEle, function(n) {
  127. if (n.nodeType !== 8){
  128. return;
  129. }
  130. const text = n.textContent.trim();
  131. if (!text.startsWith(MARKDOWN_RERENDER_MARK)) {
  132. return;
  133. }
  134. const tokens = text.split('|');
  135. if (tokens[1] === '0'){
  136. startComments.push(n);
  137. }
  138. });
  139. // Reverse to prevent nested elements
  140. startComments.reverse().forEach((n) => {
  141. const tokens = n.textContent.trim().split('|');
  142. const tagName = tokens[2];
  143. const tagRepLen = tokens[3];
  144. const tagEle = document.createElement(tagName);
  145. n.parentElement.insertBefore(tagEle, n);
  146. n.parentNode.removeChild(n);
  147. let subEle = tagEle.nextSibling;
  148. while (subEle){
  149. if (subEle.nodeType == 8) {
  150. const text = subEle.textContent.trim();
  151. if (text.startsWith(MARKDOWN_RERENDER_MARK) && text.split('|')[1] === '1') {
  152. subEle.parentNode.removeChild(subEle);
  153. break;
  154. }
  155. }
  156. tagEle.appendChild(subEle);
  157. subEle = tagEle.nextSibling;
  158. }
  159. // Remove previously added markdown symbols.
  160. tagEle.firstChild.textContent = tagEle.firstChild.textContent.substring(tagRepLen);
  161. tagEle.lastChild.textContent = tagEle.lastChild.textContent.substring(0, tagEle.lastChild.textContent.length - tagRepLen);
  162. });
  163. }
  164.  
  165. async function prepareScript() {
  166. window._sc_beforeTypesetMsgEle = (msgEle) => {};
  167. window._sc_afterTypesetMsgEle = (msgEle) => {};
  168. window._sc_typeset = () => {
  169. try {
  170. const msgEles = window._sc_getMsgEles();
  171. msgEles.forEach(msgEle => {
  172. restoreAllMarkdown(msgEle);
  173. msgEle.setAttribute(_parsed_mark,'');
  174.  
  175. window._sc_beforeTypesetMsgEle(msgEle);
  176. MathJax.typesetPromise([msgEle]);
  177. window._sc_afterTypesetMsgEle(msgEle);
  178.  
  179. rerenderAllMarkdown(msgEle);
  180. });
  181. } catch (e) {
  182. console.warn(e);
  183. }
  184. }
  185. window._sc_mutationHandler = (mutation) => {
  186. if (mutation.oldValue === '') {
  187. window._sc_typeset();
  188. }
  189. };
  190. window._sc_chatLoaded = () => { return true; };
  191. window._sc_getObserveElement = () => { return null; };
  192. var observerOptions = {
  193. attributeOldValue : true,
  194. attributeFilter: ['cancelable', 'disabled'],
  195. };
  196. var afterMainOvservationStart = () => { window._sc_typeset(); };
  197.  
  198. // Handle special cases per site.
  199. if (window.location.host === "www.bing.com") {
  200. window._sc_getObserveElement = () => {
  201. const ele = document.querySelector("#b_sydConvCont > cib-serp");
  202. if (!ele) {return null;}
  203. return ele.shadowRoot.querySelector("#cib-action-bar-main");
  204. }
  205.  
  206. const getContMsgEles = (cont, isInChat=true) => {
  207. if (!cont) {
  208. return [];
  209. }
  210. const allChatTurn = cont.shadowRoot.querySelector("#cib-conversation-main").shadowRoot.querySelectorAll("cib-chat-turn");
  211. var lastChatTurnSR = allChatTurn[allChatTurn.length - 1];
  212. if (isInChat) { lastChatTurnSR = lastChatTurnSR.shadowRoot; }
  213. const allCibMsgGroup = lastChatTurnSR.querySelectorAll("cib-message-group");
  214. const allCibMsg = Array.from(allCibMsgGroup).map(e => Array.from(e.shadowRoot.querySelectorAll("cib-message"))).flatMap(e => e);
  215. return Array.from(allCibMsg).map(cibMsg => cibMsg.shadowRoot.querySelector("cib-shared")).filter(e => e);
  216. }
  217. window._sc_getMsgEles = () => {
  218. try {
  219. const convCont = document.querySelector("#b_sydConvCont > cib-serp");
  220. const tigerCont = document.querySelector("#b_sydTigerCont > cib-serp");
  221. return getContMsgEles(convCont).concat(getContMsgEles(tigerCont, false));
  222. } catch (ignore) {
  223. return [];
  224. }
  225. }
  226. }
  227. else if (window.location.host === "chat.openai.com") {
  228. window._sc_getObserveElement = () => {
  229. return document.querySelector("main > div > div > div");
  230. }
  231. window._sc_chatLoaded = () => { return document.querySelector('main div.text-sm>svg.animate-spin') === null; };
  232.  
  233. observerOptions = {
  234. attributes: true,
  235. childList: true,
  236. subtree: true,
  237. };
  238.  
  239. window._sc_mutationHandler = (mutation) => {
  240. if (mutation.removedNodes.length) {
  241. return;
  242. }
  243. const target = mutation.target;
  244. if (!target || target.tagName !== 'DIV') {
  245. return;
  246. }
  247. const buttons = target.querySelectorAll('button');
  248. if (buttons.length !== 2 || !target.classList.contains('visible')){
  249. return;
  250. }
  251. if (mutation.type === 'attributes' ||
  252. (mutation.addedNodes.length && mutation.addedNodes[0] == buttons[0])) {
  253. window._sc_typeset();
  254. }
  255. };
  256.  
  257. afterMainOvservationStart = () => {
  258. window._sc_typeset();
  259. // Handle conversation switch
  260. new MutationObserver((mutationList) => {
  261. mutationList.forEach(async (mutation) => {
  262. if (mutation.addedNodes){
  263. window._sc_typeset();
  264. startMainOvservation(await getMainObserveElement(true), observerOptions);
  265. }
  266. });
  267. }).observe(document.querySelector('#__next'), {childList: true});
  268. };
  269.  
  270. window._sc_getMsgEles = () => {
  271. return document.querySelectorAll(queryAddNoParsed("div.w-full div.text-base div.items-start"));
  272. }
  273.  
  274. window._sc_beforeTypesetMsgEle = (msgEle) => {
  275. // Prevent latex typeset conflict
  276. const displayEles = msgEle.querySelectorAll('.math-display');
  277. displayEles.forEach(e => {
  278. const texEle = e.querySelector(".katex-mathml annotation");
  279. e.removeAttribute("class");
  280. e.textContent = "$$" + texEle.textContent + "$$";
  281. });
  282. const inlineEles = msgEle.querySelectorAll('.math-inline');
  283. inlineEles.forEach(e => {
  284. const texEle = e.querySelector(".katex-mathml annotation");
  285. e.removeAttribute("class");
  286. // e.textContent = "$" + texEle.textContent + "$";
  287. // Mathjax will typeset this with display mode.
  288. e.textContent = "$$" + texEle.textContent + "$$";
  289.  
  290. });
  291. };
  292. window._sc_afterTypesetMsgEle = (msgEle) => {
  293. // https://github.com/mathjax/MathJax/issues/3008
  294. msgEle.style.display = 'unset';
  295. }
  296. }
  297. else if (window.location.host === "you.com" || window.location.host === "www.you.com") {
  298. window._sc_getObserveElement = () => {
  299. return document.querySelector('#chatHistory');
  300. };
  301. window._sc_chatLoaded = () => { return !!document.querySelector('#chatHistory div[data-pinnedconversationturnid]'); };
  302. observerOptions = { childList : true };
  303.  
  304. window._sc_mutationHandler = (mutation) => {
  305. mutation.addedNodes.forEach(e => {
  306. const attr = e.getAttribute('data-testid')
  307. if (attr && attr.startsWith("youchat-convTurn")) {
  308. startTurnAttrObservationForTypesetting(e, 'data-pinnedconversationturnid');
  309. }
  310. })
  311. };
  312.  
  313. window._sc_getMsgEles = () => {
  314. return document.querySelectorAll(queryAddNoParsed('#chatHistory div[data-testid="youchat-answer"]'));
  315. };
  316. }
  317. console.log('Waiting for chat loading...')
  318. const mainElement = await getMainObserveElement();
  319. console.log('Chat loaded.')
  320. startMainOvservation(mainElement, observerOptions);
  321. afterMainOvservationStart();
  322. }
  323.  
  324. function enbaleResultPatcher() {
  325. // TODO: refractor all code.
  326. if (window.location.host !== "chat.openai.com") {
  327. return;
  328. }
  329. const oldJSONParse = JSON.parse;
  330. JSON.parse = function _parse() {
  331. if (typeof arguments[0] == "object") {
  332. return arguments[0];
  333. }
  334. const res = oldJSONParse.apply(this, arguments);
  335. if (res.hasOwnProperty('message')){
  336. const message = res.message;
  337. if (message.hasOwnProperty('end_turn') && message.end_turn){
  338. message.content.parts[0] = getExtraInfoAddedMKContent(message.content.parts[0]);
  339. }
  340. }
  341. return res;
  342. };
  343.  
  344. const responseHandler = (response, result) => {
  345. if (result.hasOwnProperty('mapping') && result.hasOwnProperty('current_node')){
  346. Object.keys(result.mapping).forEach((key) => {
  347. const mapObj = result.mapping[key];
  348. if (mapObj.hasOwnProperty('message')) {
  349. if (mapObj.message.author.role === 'user'){
  350. return;
  351. }
  352. const contentObj = mapObj.message.content;
  353. contentObj.parts[0] = getExtraInfoAddedMKContent(contentObj.parts[0]);
  354. }
  355. });
  356. }
  357. }
  358. let oldfetch = fetch;
  359. function patchedFetch() {
  360. return new Promise((resolve, reject) => {
  361. oldfetch.apply(this, arguments).then(response => {
  362. const oldJson = response.json;
  363. response.json = function() {
  364. return new Promise((resolve, reject) => {
  365. oldJson.apply(this, arguments).then(result => {
  366. try{
  367. responseHandler(response, result);
  368. } catch (e) {
  369. console.warn(e);
  370. }
  371. resolve(result);
  372. });
  373. });
  374. }
  375. resolve(response);
  376. });
  377. });
  378. }
  379. window.fetch = patchedFetch;
  380. }
  381.  
  382. // After output completed, the attribute of turn element will be changed,
  383. // only with observer won't be enough, so we have this function for sure.
  384. function startTurnAttrObservationForTypesetting(element, doneWithAttr) {
  385. const tmpObserver = new MutationObserver((mutationList, observer) => {
  386. mutationList.forEach(mutation => {
  387. if (mutation.oldValue === null) {
  388. window._sc_typeset();
  389. observer.disconnect;
  390. }
  391. })
  392. });
  393. tmpObserver.observe(element, {
  394. attributeOldValue : true,
  395. attributeFilter: [doneWithAttr],
  396. });
  397. if (element.hasAttribute(doneWithAttr)) {
  398. window._sc_typeset();
  399. tmpObserver.disconnect;
  400. }
  401. }
  402.  
  403. function getMainObserveElement(chatLoaded=false) {
  404. return new Promise(async (resolve, reject) => {
  405. const resolver = () => {
  406. const ele = window._sc_getObserveElement();
  407. if (ele && (chatLoaded || window._sc_chatLoaded())) {
  408. return resolve(ele);
  409. }
  410. window.setTimeout(resolver, 500);
  411. }
  412. resolver();
  413. });
  414. }
  415.  
  416. function startMainOvservation(mainElement, observerOptions) {
  417. const callback = (mutationList, observer) => {
  418. mutationList.forEach(mutation => {
  419. window._sc_mutationHandler(mutation);
  420. });
  421. };
  422. if (window._sc_mainObserver) {
  423. window._sc_mainObserver.disconnect();
  424. }
  425. window._sc_mainObserver = new MutationObserver(callback);
  426. window._sc_mainObserver.observe(mainElement, observerOptions);
  427. }
  428.  
  429. async function waitMathJaxLoaded() {
  430. while (!MathJax.hasOwnProperty('typeset')) {
  431. if (window._sc_ChatLatex.loadCount > 20000 / 200) {
  432. setTipsElementText("Failed to load MathJax, try refresh.", true);
  433. }
  434. await new Promise((x) => setTimeout(x, 500));
  435. window._sc_ChatLatex.loadCount += 1;
  436. }
  437. }
  438.  
  439. function hideTipsElement(timeout=3) {
  440. window.setTimeout(() => {window._sc_ChatLatex.tipsElement.hidden=true; }, 3000);
  441. }
  442.  
  443. async function loadMathJax() {
  444. showTipsElement();
  445. setTipsElementText("Loading MathJax...");
  446. addScript('https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js');
  447. await waitMathJaxLoaded();
  448. setTipsElementText("MathJax Loaded.");
  449. hideTipsElement();
  450. }
  451.  
  452. (async function() {
  453. window._sc_ChatLatex = {
  454. tipsElement: document.createElement("div"),
  455. loadCount: 0
  456. };
  457. window.MathJax = {
  458. tex: {
  459. inlineMath: [['$', '$'], ['\\(', '\\)']],
  460. displayMath : [['$$', '$$', ['\\[', '\\]']]]
  461. },
  462. startup: {
  463. typeset: false
  464. }
  465. };
  466.  
  467. enbaleResultPatcher();
  468. await loadMathJax();
  469. await prepareScript();
  470. })();

QingJ © 2025

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