您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Features: 1. Click formula to copy LaTeX 2. Copy text with formula converted to LaTeX
// ==UserScript== // @name Copy LaTeX in Gemini // @namespace http://tampermonkey.net/ // @version 1.0.1 // @description Features: 1. Click formula to copy LaTeX 2. Copy text with formula converted to LaTeX // @author Cesar // @match https://gemini.google.com/app/* // @grant none // @license MIT // @run-at document-start // ==/UserScript== 'use strict'; /** * @description Set these to how you want inline and display math to be delimited. */ const defaultCopyDelimiters = { inline: ['$', '$'], // alternative: ['\(', '\)'] display: ['\\[ ', ' \\]'], // alternative: ['\[', '\]'] }; const allKatex = {}; // 1. 先尝试 Hook window.katex 的赋值 let originalKatex = window.katex; // 2. 如果 katex 已存在,直接 Hook if (originalKatex) { hookKatexRender(originalKatex); } else { Object.defineProperty(window, 'katex', { set: function(newKatex) { console.log('Detected katex assignment, hooking render...'); originalKatex = newKatex; hookKatexRender(originalKatex); // 对新 katex 对象进行 Hook return originalKatex; }, get: function() { return originalKatex; }, configurable: true }); } // 核心 Hook 函数 function hookKatexRender(katexObj) { if (!katexObj || typeof katexObj.render !== 'function') { console.warn('katex.render not found, skipping hook'); return; } const originalRender = katexObj.render; katexObj.render = new Proxy(originalRender, { apply: function(target, thisArg, args) { let result = target.apply(thisArg, args); if (args.length >= 2) { const latexStr = args[0]; const element = args[1]; const katexHtml = element.querySelector('.katex-html'); if (element instanceof Element && katexHtml !== null) { allKatex[katexHtml.outerHTML] = latexStr; } else { console.warn('katex.render: 2nd arg is not a DOM element'); } } return result; } }); console.log('Successfully hooked katex.render'); } // 添加点击事件监听器 function handleKatexClick(event) { const katexHtmlElement = event.target.closest('.katex-html'); if (katexHtmlElement) { const latexFormula = allKatex[katexHtmlElement.outerHTML]; if (latexFormula) { navigator.clipboard.writeText(latexFormula).then(() => { console.log('LaTeX formula copied to clipboard:', latexFormula); // 可选:添加视觉反馈 const originalNone = katexHtmlElement.cloneNode(true); katexHtmlElement.textContent = 'Copied!'; setTimeout(() => { katexHtmlElement.replaceWith(originalNone); }, 700); }).catch(err => { console.error('Failed to copy LaTeX formula:', err); }); } else { console.warn('No LaTeX formula found for this element'); } } } // 监听文档加载完成后添加点击事件 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { document.addEventListener('click', handleKatexClick); }); } else { document.addEventListener('click', handleKatexClick); } /** * @description Replace .katex elements with their TeX source (<annotation> element). * @param {DocumentFragment} fragment * @param {CopyDelimiters} copyDelimiters * @returns {DocumentFragment} */ function katexReplaceWithTex( fragment, copyDelimiters = defaultCopyDelimiters ) { // Replace .katex-html elements with their latex (by creating a new annotation element) // descendant, with inline delimiters. const katexHtml = fragment.querySelectorAll('.katex-html'); for (let i = 0; i < katexHtml.length; i++) { const element = katexHtml[i]; const texSource = document.createElement('annotation'); if (element.outerHTML && allKatex[element.outerHTML]) { texSource.textContent = allKatex[element.outerHTML]; } else { continue; } if (texSource) { if (element.replaceWith) { element.replaceWith(texSource); } else if (element.parentNode) { element.parentNode.replaceChild(texSource, element); } if (texSource.closest('.katex-display')) { texSource.textContent = `\n${copyDelimiters.display[0]}${texSource.textContent}${copyDelimiters.display[1]}\n`; } else { texSource.textContent = `${copyDelimiters.inline[0]}${texSource.textContent}${copyDelimiters.inline[1]}`; } } } return fragment; } /** * @description Return <div class="katex"> element containing node, or null if not found. * @param {Node} node * @returns {Element|null} */ function closestKatex(node) { // If node is a Text Node, for example, go up to containing Element, // where we can apply the `closest` method. const element = (node instanceof Element ? node : node.parentElement); return element && element.closest('.katex'); } /** * @description Global copy handler to modify behavior on/within .katex elements. * @param {ClipboardEvent} event */ document.addEventListener('copy', function(event) { const selection = window.getSelection(); if (!selection || selection.isCollapsed || !event.clipboardData) { return; // default action OK if selection is empty or unchangeable } const clipboardData = event.clipboardData; const range = selection.getRangeAt(0); // When start point is within a formula, expand to entire formula. const startKatex = closestKatex(range.startContainer); if (startKatex) { range.setStartBefore(startKatex); } // Similarly, when end point is within a formula, expand to entire formula. const endKatex = closestKatex(range.endContainer); if (endKatex) { range.setEndAfter(endKatex); } const fragment = range.cloneContents(); if (!fragment.querySelector('.katex-html')) { return; // default action OK if no .katex-mathml elements } const htmlContents = Array.prototype.map.call(fragment.childNodes, (el) => (el instanceof Text ? el.textContent : el.outerHTML) ).join(''); // Preserve usual HTML copy/paste behavior. clipboardData.setData('text/html', htmlContents); // Rewrite plain-text version. const textContent = katexReplaceWithTex(fragment).textContent; if (textContent) { clipboardData.setData('text/plain', textContent); } // 用于 debug 哪里的代码导致 setData 被覆盖 // clipboardData.setData = new Proxy(clipboardData.setData, { // apply: function(target, thisArg, args) { // console.log('clipboardData.setData', args); // return target.apply(thisArg, args); // } // }); // Prevent normal copy handling. event.preventDefault(); // Gemini 的 copy 事件中会 setData,导致这里的 setData 被覆盖,所以需要 stopImmediatePropagation event.stopImmediatePropagation(); });