// ==UserScript==
// @name ChatGPT LaTeX Auto Render (OpenAI, new bing, you, etc.)
// @version 0.6.13
// @author Scruel Tao
// @homepage https://github.com/scruel/tampermonkey-scripts
// @description Auto typeset LaTeX math formulas on ChatGPT pages (OpenAI, new bing, you, etc.).
// @description:zh-CN 自动渲染 ChatGPT 页面 (OpenAI, new bing, you 等) 上的 LaTeX 数学公式。
// @match https://chatgpt.com/*
// @match https://platform.openai.com/playground/*
// @match https://www.bing.com/search?*
// @match https://you.com/search?*&tbm=youchat*
// @match https://www.you.com/search?*&tbm=youchat*
// @namespace http://tampermonkey.net/
// @icon https://chatgpt.com/favicon.ico
// @grant none
// @noframes
// ==/UserScript==
"use strict";
const PARSED_MARK = "_sc_parsed";
const MARKDOWN_RERENDER_MARK = "sc_mktag";
const MARKDOWN_SYMBOL_UNDERLINE = "XXXSCUEDLXXX";
const MARKDOWN_SYMBOL_ASTERISK = "XXXSCAESKXXX";
function queryAddNoParsed(query) {
return query + ":not([" + PARSED_MARK + "])";
}
function showTipsElement() {
const tipsElement = window._sc_ChatLatex.tipsElement;
tipsElement.style.position = "fixed";
tipsElement.style.right = "10px";
tipsElement.style.top = "10px";
tipsElement.style.background = "#333";
tipsElement.style.color = "#fff";
tipsElement.style.zIndex = "999999";
var tipContainer = document.body.querySelector("header");
if (!tipContainer) {
tipContainer = document.body;
}
tipContainer.appendChild(tipsElement);
}
function setTipsElementText(text, errorRaise = false) {
window._sc_ChatLatex.tipsElement.innerHTML = text;
if (errorRaise) {
throw text;
}
console.log(text);
}
async function addScript(url) {
const scriptElement = document.createElement("script");
const headElement =
document.getElementsByTagName("head")[0] || document.documentElement;
if (!headElement.appendChild(scriptElement)) {
// Prevent appendChild overwritten problem.
headElement.append(scriptElement);
}
scriptElement.src = url;
}
function traverseDOM(element, callback, onlySingle = true) {
if (!onlySingle || !element.hasChildNodes()) {
callback(element);
}
element = element.firstChild;
while (element) {
traverseDOM(element, callback, onlySingle);
element = element.nextSibling;
}
}
function getExtraInfoAddedMKContent(content) {
// Ensure that the whitespace before and after the same
content = content.replaceAll(/( *\*+ *)/g, MARKDOWN_SYMBOL_ASTERISK + "$1");
content = content.replaceAll(/( *_+ *)/g, MARKDOWN_SYMBOL_UNDERLINE + "$1");
// Ensure render for single line
content = content.replaceAll(
new RegExp(`^${MARKDOWN_SYMBOL_ASTERISK}(\\*+)`, "gm"),
`${MARKDOWN_SYMBOL_ASTERISK} $1`
);
content = content.replaceAll(
new RegExp(`^${MARKDOWN_SYMBOL_UNDERLINE}(_+)`, "gm"),
`${MARKDOWN_SYMBOL_UNDERLINE} $1`
);
return content;
}
function removeEleMKExtraInfo(ele) {
traverseDOM(ele, function (e) {
if (e.textContent) {
e.textContent = removeMKExtraInfo(e.textContent);
}
});
}
function removeMKExtraInfo(content) {
content = content.replaceAll(MARKDOWN_SYMBOL_UNDERLINE, "");
content = content.replaceAll(MARKDOWN_SYMBOL_ASTERISK, "");
return content;
}
function getLastMKSymbol(ele, defaultSymbol) {
if (!ele) {
return defaultSymbol;
}
const content = ele.textContent.trim();
if (content.endsWith(MARKDOWN_SYMBOL_UNDERLINE)) {
return "_";
}
if (content.endsWith(MARKDOWN_SYMBOL_ASTERISK)) {
return "*";
}
return defaultSymbol;
}
function restoreMarkdown(msgEle, tagName, defaultSymbol) {
const eles = msgEle.querySelectorAll(tagName);
eles.forEach((e) => {
const restoredNodes = document
.createRange()
.createContextualFragment(e.innerHTML);
const fn = restoredNodes.childNodes[0];
const ln = restoredNodes.childNodes[restoredNodes.childNodes.length - 1];
const wrapperSymbol = getLastMKSymbol(e.previousSibling, defaultSymbol);
fn.textContent = wrapperSymbol + fn.textContent;
ln.textContent = ln.textContent + wrapperSymbol;
restoredNodes.prepend(
document.createComment(
MARKDOWN_RERENDER_MARK + "|0|" + tagName + "|" + wrapperSymbol.length
)
);
restoredNodes.append(
document.createComment(MARKDOWN_RERENDER_MARK + "|1|" + tagName)
);
e.parentElement.insertBefore(restoredNodes, e);
e.parentNode.removeChild(e);
});
removeEleMKExtraInfo(msgEle);
}
function restoreAllMarkdown(msgEle) {
restoreMarkdown(msgEle, "em", "_");
}
function rerenderAllMarkdown(msgEle) {
// restore HTML from restored markdown comment info
const startComments = [];
traverseDOM(msgEle, function (n) {
if (n.nodeType !== 8) {
return;
}
const text = n.textContent.trim();
if (!text.startsWith(MARKDOWN_RERENDER_MARK)) {
return;
}
const tokens = text.split("|");
if (tokens[1] === "0") {
startComments.push(n);
}
});
// Reverse to prevent nested elements
startComments.reverse().forEach((n) => {
const tokens = n.textContent.trim().split("|");
const tagName = tokens[2];
const tagRepLen = tokens[3];
const tagEle = document.createElement(tagName);
n.parentElement.insertBefore(tagEle, n);
n.parentNode.removeChild(n);
let subEle = tagEle.nextSibling;
while (subEle) {
if (subEle.nodeType == 8) {
const text = subEle.textContent.trim();
if (
text.startsWith(MARKDOWN_RERENDER_MARK) &&
text.split("|")[1] === "1"
) {
subEle.parentNode.removeChild(subEle);
break;
}
}
tagEle.appendChild(subEle);
subEle = tagEle.nextSibling;
}
// Remove previously added markdown symbols.
tagEle.firstChild.textContent =
tagEle.firstChild.textContent.substring(tagRepLen);
tagEle.lastChild.textContent = tagEle.lastChild.textContent.substring(
0,
tagEle.lastChild.textContent.length - tagRepLen
);
});
}
async function prepareScript() {
window._sc_beforeTypesetMsgEle = (msgEle) => {};
window._sc_afterTypesetMsgEle = (msgEle) => {};
window._sc_typeset = () => {
try {
console.log('[LaTeX] Typesetting...')
const msgEles = window._sc_getMsgEles();
msgEles.forEach((msgEle) => {
restoreAllMarkdown(msgEle);
msgEle.setAttribute(PARSED_MARK, "");
window._sc_beforeTypesetMsgEle(msgEle);
MathJax.typesetPromise([msgEle]);
window._sc_afterTypesetMsgEle(msgEle);
rerenderAllMarkdown(msgEle);
});
} catch (e) {
console.warn(e);
}
};
window._sc_mutationHandler = (mutation) => {
if (mutation.oldValue === "") {
window._sc_typeset();
}
};
window._sc_chatLoaded = () => {
return true;
};
window._sc_getObserveElement = () => {
return null;
};
var observerOptions = {
attributeOldValue: true,
attributeFilter: ["cancelable", "disabled"],
};
var afterMainOvservationStart = () => {
window._sc_typeset();
};
// Handle special cases per site.
if (window.location.host === "www.bing.com") {
window._sc_getObserveElement = () => {
const ele = document.querySelector("#b_sydConvCont > cib-serp");
if (!ele) {
return null;
}
return ele.shadowRoot.querySelector("#cib-action-bar-main");
};
const getContMsgEles = (cont, isInChat = true) => {
if (!cont) {
return [];
}
const allChatTurn = cont.shadowRoot
.querySelector("#cib-conversation-main")
.shadowRoot.querySelectorAll("cib-chat-turn");
var lastChatTurnSR = allChatTurn[allChatTurn.length - 1];
if (isInChat) {
lastChatTurnSR = lastChatTurnSR.shadowRoot;
}
const allCibMsgGroup =
lastChatTurnSR.querySelectorAll("cib-message-group");
const allCibMsg = Array.from(allCibMsgGroup)
.map((e) => Array.from(e.shadowRoot.querySelectorAll("cib-message")))
.flatMap((e) => e);
return Array.from(allCibMsg)
.map((cibMsg) => cibMsg.shadowRoot.querySelector("cib-shared"))
.filter((e) => e);
};
window._sc_getMsgEles = () => {
try {
const convCont = document.querySelector("#b_sydConvCont > cib-serp");
const tigerCont = document.querySelector("#b_sydTigerCont > cib-serp");
return getContMsgEles(convCont).concat(
getContMsgEles(tigerCont, false)
);
} catch (ignore) {
return [];
}
};
} else if (window.location.host === "chat.openai.com") {
window._sc_getObserveElement = () => {
return document.querySelector("main > div > div > div");
};
window._sc_chatLoaded = () => {
return (
document.querySelector("main div.text-sm>svg.animate-spin") === null
);
};
observerOptions = {
attributes: true,
childList: true,
subtree: true,
};
window._sc_mutationHandler = (mutation) => {
if (mutation.removedNodes.length) {
return;
}
const target = mutation.target;
if (!target || target.tagName !== "DIV") {
return;
}
const buttons = target.querySelectorAll("button");
if (buttons.length !== 3 || !target.classList.contains("visible")) {
return;
}
if (
mutation.type === "attributes" ||
(mutation.addedNodes.length && mutation.addedNodes[0] == buttons[1])
) {
window._sc_typeset();
}
};
afterMainOvservationStart = () => {
window._sc_typeset();
// Handle conversation switch
new MutationObserver(async (mutationList) => {
for (var mutation of mutationList) {
if (!mutation.addedNodes.length) {
continue;
}
const addedNode = mutation.addedNodes[0];
// Check if first added node is normal node
if (addedNode.nodeType !== 1) {
return;
}
// console.log(mutation);
const mainNode = addedNode.parentElement;
if (mainNode && mainNode.tagName !== 'MAIN') {
continue;
}
startMainOvservation(
await getMainObserveElement(true),
observerOptions
);
window._sc_typeset();
break;
};
}).observe(document.querySelector("#__next"), { childList: true, subtree: true});
};
window._sc_getMsgEles = () => {
return document.querySelectorAll(
queryAddNoParsed("div.w-full div.text-base div.items-start")
);
};
window._sc_beforeTypesetMsgEle = (msgEle) => {
// Prevent latex typeset conflict
const displayEles = msgEle.querySelectorAll(".math-display");
displayEles.forEach((e) => {
const texEle = e.querySelector(".katex-mathml annotation");
e.removeAttribute("class");
e.textContent = "$$" + texEle.textContent + "$$";
});
const inlineEles = msgEle.querySelectorAll(".math-inline");
inlineEles.forEach((e) => {
const texEle = e.querySelector(".katex-mathml annotation");
e.removeAttribute("class");
// e.textContent = "$" + texEle.textContent + "$";
// Mathjax will typeset this with display mode.
e.textContent = "$$" + texEle.textContent + "$$";
});
};
window._sc_afterTypesetMsgEle = (msgEle) => {
// https://github.com/mathjax/MathJax/issues/3008
msgEle.style.display = "unset";
};
} else if (
window.location.host === "you.com" ||
window.location.host === "www.you.com"
) {
window._sc_getObserveElement = () => {
return document.querySelector("#chatHistory");
};
window._sc_chatLoaded = () => {
return !!document.querySelector(
"#chatHistory div[data-pinnedconversationturnid]"
);
};
observerOptions = { childList: true };
window._sc_mutationHandler = (mutation) => {
mutation.addedNodes.forEach((e) => {
const attr = e.getAttribute("data-testid");
if (attr && attr.startsWith("youchat-convTurn")) {
startTurnAttrObservationForTypesetting(
e,
"data-pinnedconversationturnid"
);
}
});
};
window._sc_getMsgEles = () => {
return document.querySelectorAll(
queryAddNoParsed('#chatHistory div[data-testid="youchat-answer"]')
);
};
}
console.log("Waiting for chat loading...");
const mainElement = await getMainObserveElement();
console.log("Chat loaded.");
startMainOvservation(mainElement, observerOptions);
afterMainOvservationStart();
}
function enbaleResultPatcher() {
// TODO: refractor all code.
if (window.location.host !== "chat.openai.com") {
return;
}
const oldJSONParse = JSON.parse;
JSON.parse = function _parse() {
const res = oldJSONParse.apply(this, arguments);
if (res.hasOwnProperty("message")) {
const message = res.message;
if (message.hasOwnProperty("end_turn") && message.end_turn) {
message.content.parts[0] = getExtraInfoAddedMKContent(
message.content.parts[0]
);
}
}
return res;
};
const responseHandler = (response, result) => {
if (
result.hasOwnProperty("mapping") &&
result.hasOwnProperty("current_node")
) {
Object.keys(result.mapping).forEach((key) => {
const mapObj = result.mapping[key];
if (mapObj.hasOwnProperty("message")) {
if (mapObj.message.author.role === "user") {
return;
}
const contentObj = mapObj.message.content;
contentObj.parts[0] = getExtraInfoAddedMKContent(contentObj.parts[0]);
}
});
}
};
let oldfetch = fetch;
window.fetch = function patchedFetch() {
return new Promise((resolve, reject) => {
oldfetch
.apply(this, arguments)
.then((response) => {
const oldJson = response.json;
response.json = function () {
return new Promise((resolve, reject) => {
oldJson
.apply(this, arguments)
.then((result) => {
try {
responseHandler(response, result);
} catch (e) {
console.warn(e);
}
resolve(result);
})
.catch((e) => reject(e));
});
};
resolve(response);
})
.catch((e) => reject(e));
});
};
// Resote
const oldClipBoardWriteText = navigator.clipboard.writeText;
navigator.clipboard.writeText = function patchedWriteText() {
return new Promise((resolve, reject) => {
arguments[0] = removeMKExtraInfo(arguments[0]);
oldClipBoardWriteText
.apply(this, arguments)
.then((response) => {
resolve(response);
})
.catch((e) => reject(e));
});
};
}
// After output completed, the attribute of turn element will be changed,
// only with observer won't be enough, so we have this function for sure.
function startTurnAttrObservationForTypesetting(element, doneWithAttr) {
const tmpObserver = new MutationObserver((mutationList, observer) => {
mutationList.forEach((mutation) => {
if (mutation.oldValue === null) {
window._sc_typeset();
observer.disconnect;
}
});
});
tmpObserver.observe(element, {
attributeOldValue: true,
attributeFilter: [doneWithAttr],
});
if (element.hasAttribute(doneWithAttr)) {
window._sc_typeset();
tmpObserver.disconnect;
}
}
function getMainObserveElement(chatLoaded = false) {
return new Promise(async (resolve, reject) => {
const resolver = () => {
const ele = window._sc_getObserveElement();
if (ele && (chatLoaded || window._sc_chatLoaded())) {
return resolve(ele);
}
window.setTimeout(resolver, 500);
};
resolver();
});
}
function startMainOvservation(mainElement, observerOptions) {
const callback = (mutationList, observer) => {
mutationList.forEach((mutation) => {
window._sc_mutationHandler(mutation);
});
};
if (window._sc_mainObserver) {
window._sc_mainObserver.disconnect();
}
window._sc_mainObserver = new MutationObserver(callback);
window._sc_mainObserver.observe(mainElement, observerOptions);
}
async function waitMathJaxLoaded() {
while (!MathJax.hasOwnProperty("typeset")) {
if (window._sc_ChatLatex.loadCount > 20000 / 200) {
setTipsElementText("Failed to load MathJax, try refresh.", true);
}
await new Promise((x) => setTimeout(x, 500));
window._sc_ChatLatex.loadCount += 1;
}
}
function hideTipsElement(timeout = 3) {
window.setTimeout(() => {
window._sc_ChatLatex.tipsElement.hidden = true;
}, 3000);
}
async function loadMathJax() {
showTipsElement();
setTipsElementText("Loading MathJax...");
addScript("https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js");
await waitMathJaxLoaded();
setTipsElementText("MathJax Loaded.");
hideTipsElement();
}
(async function () {
window._sc_ChatLatex = {
tipsElement: document.createElement("div"),
loadCount: 0,
};
window.MathJax = {
tex: {
inlineMath: [
["$", "$"],
["\\(", "\\)"],
],
displayMath: [["$$", "$$", ["\\[", "\\]"]]],
},
startup: {
typeset: false,
},
};
enbaleResultPatcher();
await loadMathJax();
await prepareScript();
})();