Greasy Fork is available in English.
让 ChatGPT 的“Ask ChatGPT”引用和普通复制,对包含 KaTeX 的选区优先返回原始 LaTeX($...$ / $$...$$),不改动 DOM,不叠加多余文本,跨行选区也保持稳定;整合了 TexCopyer 的双击复制功能。
// ==UserScript==
// @name ChatGPT TeX Copy & Quote 整合版
// @namespace http://tampermonkey.net/
// @version 1.0.0
// @description 让 ChatGPT 的“Ask ChatGPT”引用和普通复制,对包含 KaTeX 的选区优先返回原始 LaTeX($...$ / $$...$$),不改动 DOM,不叠加多余文本,跨行选区也保持稳定;整合了 TexCopyer 的双击复制功能。
// @author schweigen
// @match https://chatgpt.com/*
// @license GPL-3.0-or-later
// @run-at document-start
// ==/UserScript==
(function () {
'use strict';
// 本脚本在原「ChatGPT TeX Copy & Quote 兼容版」基础上,
// 仅整合了「TexCopyer」的双击复制交互(及相关提示样式),其余功能保持原样。
// 组合来源:
// - ChatGPT Better TeX Quote, by schweigen(MIT License)
// - TexCopyer, by yjy | blime(GPLv3), http://greasyfork.icu/en/scripts/499346-texcopyer
// 合并脚本整体以 GPLv3 或更高版本发布。
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const RangeCtor = window.Range;
if (!RangeCtor || !RangeCtor.prototype || RangeCtor.prototype._btqPatched) {
return;
}
const rangeProto = RangeCtor.prototype;
const nativeCloneContents = rangeProto.cloneContents;
const nativeRangeToString = rangeProto.toString;
function findTexFromKatex(katexEl) {
if (!katexEl || !(katexEl instanceof Element)) return null;
try {
const ann = katexEl.querySelector('annotation[encoding="application/x-tex"], annotation');
if (!ann || !ann.textContent) return null;
const raw = ann.textContent.trim();
if (!raw) return null;
const isDisplay = katexEl.classList.contains('katex-display');
return isDisplay ? `$$${raw}$$` : `$${raw}$`;
} catch (_) {
return null;
}
}
function transformFragment(frag) {
if (!frag || !frag.querySelectorAll) {
return { fragment: frag, changed: false };
}
const list = frag.querySelectorAll('.katex');
if (!list.length) {
return { fragment: frag, changed: false };
}
let changed = false;
for (const el of Array.from(list)) {
const tex = findTexFromKatex(el);
if (!tex) continue;
const textNode = frag.ownerDocument.createTextNode(tex);
el.replaceWith(textNode);
changed = true;
}
return { fragment: frag, changed };
}
rangeProto.cloneContents = function btq_cloneContents() {
const frag = nativeCloneContents.call(this);
try {
return transformFragment(frag).fragment;
} catch (e) {
console.warn('[BetterTeXQuote] cloneContents transform error:', e);
return frag;
}
};
rangeProto.toString = function btq_rangeToString() {
try {
const frag = nativeCloneContents.call(this);
const res = transformFragment(frag);
if (res.changed) {
return res.fragment.textContent || '';
}
} catch (e) {
console.warn('[BetterTeXQuote] range.toString transform error:', e);
}
return nativeRangeToString.call(this);
};
rangeProto._btqPatched = true;
const SelCtor = window.Selection;
if (SelCtor && SelCtor.prototype && typeof SelCtor.prototype.toString === 'function') {
const selProto = SelCtor.prototype;
const nativeSelToString = selProto.toString;
selProto.toString = function btq_selectionToString() {
try {
if (this.rangeCount > 0) {
const range = this.getRangeAt(0);
if (range && typeof range.toString === 'function') {
return range.toString();
}
}
} catch (e) {
console.warn('[BetterTeXQuote] selection.toString error:', e);
}
return nativeSelToString.call(this);
};
}
function btq_setupDblClickCopy() {
if (!document || !document.body) return;
try {
const css =
'.btq-latex-tooltip{position:fixed;background-color:rgba(0,0,0,0.7);color:#fff;padding:4px 8px;border-radius:4px;font-size:11px;z-index:1000;opacity:0;transition:opacity 0.15s;pointer-events:none;}' +
'.btq-latex-copy-success{position:fixed;bottom:10%;left:50%;transform:translateX(-50%);background-color:rgba(0,0,0,0.7);color:#fff;padding:8px 16px;border-radius:4px;font-size:12px;z-index:1000;opacity:1;transition:opacity 0.2s;pointer-events:none;}';
if (document.head) {
const styleEl = document.createElement('style');
styleEl.type = 'text/css';
styleEl.textContent = css;
document.head.appendChild(styleEl);
}
const tooltip = document.createElement('div');
tooltip.className = 'btq-latex-tooltip';
tooltip.style.display = 'none';
document.body.appendChild(tooltip);
let tooltipTimer = null;
let lastKatex = null;
function btq_copyTexToClipboard(tex, onDone) {
if (!tex) return;
const done = typeof onDone === 'function' ? onDone : function () {};
try {
if (navigator && navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
navigator.clipboard.writeText(tex).then(done).catch(function () {
btq_fallbackCopy(tex);
done();
});
} else {
btq_fallbackCopy(tex);
done();
}
} catch (_) {
btq_fallbackCopy(tex);
done();
}
}
function btq_fallbackCopy(tex) {
try {
if (!document || !document.body || !document.createElement) return;
const ta = document.createElement('textarea');
ta.value = tex;
ta.setAttribute('readonly', 'readonly');
ta.style.position = 'fixed';
ta.style.top = '0';
ta.style.left = '-9999px';
document.body.appendChild(ta);
ta.select();
try {
document.execCommand('copy');
} catch (_) {
}
document.body.removeChild(ta);
} catch (_) {
}
}
function btq_showCopySuccess() {
if (!document || !document.body) return;
const el = document.createElement('div');
el.className = 'btq-latex-copy-success';
el.textContent = '已复制 LaTeX 公式';
document.body.appendChild(el);
setTimeout(function () {
el.style.opacity = '0';
setTimeout(function () {
if (el.parentNode) {
el.parentNode.removeChild(el);
}
}, 200);
}, 1000);
}
function btq_showTooltip(katexEl, tex) {
if (!katexEl || !tex) return;
try {
const rect = katexEl.getBoundingClientRect();
tooltip.textContent = tex;
tooltip.style.left = rect.left + 'px';
const top = rect.top - 24;
tooltip.style.top = (top < 0 ? 0 : top) + 'px';
tooltip.style.display = 'block';
tooltip.style.opacity = '0.8';
} catch (_) {
}
}
function btq_hideTooltip() {
tooltip.style.display = 'none';
tooltip.style.opacity = '0';
}
document.addEventListener(
'mouseover',
function (ev) {
const target = ev && ev.target;
if (!target || !(target instanceof Element)) return;
const katexEl = target.closest('.katex');
if (!katexEl) return;
katexEl.style.cursor = 'pointer';
lastKatex = katexEl;
if (tooltipTimer) {
clearTimeout(tooltipTimer);
}
tooltipTimer = setTimeout(function () {
const tex = findTexFromKatex(katexEl);
if (!tex) return;
btq_showTooltip(katexEl, tex);
}, 800);
},
true
);
document.addEventListener(
'mouseout',
function (ev) {
const from = ev && ev.target;
if (!from || !(from instanceof Element)) return;
const fromKatex = from.closest('.katex');
if (!fromKatex) return;
const to = ev.relatedTarget;
if (to && to instanceof Element) {
const toKatex = to.closest('.katex');
if (toKatex === fromKatex) {
return;
}
}
if (tooltipTimer) {
clearTimeout(tooltipTimer);
tooltipTimer = null;
}
btq_hideTooltip();
},
true
);
document.addEventListener(
'dblclick',
function (ev) {
const target = ev && ev.target;
if (!target || !(target instanceof Element)) return;
const katexEl = target.closest('.katex');
if (!katexEl) return;
const tex = findTexFromKatex(katexEl);
if (!tex) return;
btq_copyTexToClipboard(tex, btq_showCopySuccess);
},
false
);
} catch (e) {
console.warn('[BetterTeXQuote] dblclick copy setup error:', e);
}
}
if (typeof document !== 'undefined') {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', btq_setupDblClickCopy);
} else {
btq_setupDblClickCopy();
}
}
})();