Google Gemini/AI Studio 公式复制

在Google Gemini/AI Studio中点击公式,即可复制MathML,可以直接粘贴到word中。

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Google Gemini/AI Studio 公式复制
// @version      0.68
// @description  在Google Gemini/AI Studio中点击公式,即可复制MathML,可以直接粘贴到word中。
// @author       Riverstar
// @match        https://gemini.google.com/*
// @match        https://aistudio.google.com/*
// @icon         https://www.gstatic.com/lamda/images/gemini_sparkle_aurora_33f86dc0c0257da337c63.svg
// @require      https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js
// @grant        GM_addStyle
// @namespace https://greasyfork.org/users/899678
// ==/UserScript==

(function() {
    'use strict';

    let ttPolicy = null;

    function initTrustedTypes() {
        if (window.trustedTypes && window.trustedTypes.createPolicy) {
            try {
                if (!window.trustedTypes.defaultPolicy) {
                    window.trustedTypes.createPolicy('default', {
                        createHTML: (string) => string,
                        createScript: (string) => string,
                        createScriptURL: (string) => string,
                    });
                }
            } catch (e) {
                console.warn(e);
            }

            try {
                ttPolicy = window.trustedTypes.createPolicy('gemini-math-copy-' + Math.random().toString(36).substring(2), {
                    createHTML: (string) => string,
                });
            } catch (e) {
                console.warn(e);
            }
        }
    }

    function getTrustedHTML(htmlString) {
        if (window.trustedTypes) {
            if (window.trustedTypes.defaultPolicy) {
                return htmlString;
            }
            if (ttPolicy) {
                return ttPolicy.createHTML(htmlString);
            }
        }
        return htmlString;
    }

    initTrustedTypes();

    if (typeof MathJax !== 'undefined') {
        MathJax.config = MathJax.config || {};
        MathJax.config.startup = MathJax.config.startup || {};
        MathJax.config.startup.typeset = false;
    }

    const LARGE_OPS = new Set(['∑', '∏', '∐', '⋃', '⋂', '⨁', '⨂', '∫', '∮', '∬', '∭', '⨌']);
    const NS_MATHML = 'http://www.w3.org/1998/Math/MathML';

    const SELECTOR_BASE = '.math-block, ms-katex';
    const SELECTOR_HOVER = '.math-block:hover, ms-katex:hover';

    GM_addStyle(`
        ${SELECTOR_BASE} {
            cursor: pointer !important;
            border-radius: 4px;
            transition: background-color 0.1s ease, box-shadow 0.1s ease;
        }
        ${SELECTOR_HOVER} {
            background-color: rgba(26, 115, 232, 0.15) !important;
            box-shadow: 0 0 0 2px #1a73e8 !important;
            z-index: 10;
            position: relative;
        }
        #gas-toast-container { position: fixed; bottom: 24px; left: 24px; z-index: 99999; display: flex; flex-direction: column; gap: 10px; pointer-events: none; }
        .gas-toast { pointer-events: auto; background: white; color: #3c4043; padding: 12px 16px; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); display: flex; align-items: center; gap: 12px; font-family: Roboto, sans-serif; font-size: 14px; animation: toast-in 0.3s cubic-bezier(0,0,0.2,1); min-width: 300px; border: 1px solid rgba(0,0,0,0.05); }
        .gas-toast.leaving { animation: toast-out 0.2s forwards; }
        .gas-toast .icon { font-size: 20px; }
        .gas-toast .close-btn { margin-left: auto; cursor: pointer; color: #5f6368; display: flex; align-items: center; border: none; background: none; }
        @keyframes toast-in { from { opacity: 0; transform: translateY(20px) scale(0.9); } to { opacity: 1; transform: translateY(0) scale(1); } }
        @keyframes toast-out { to { opacity: 0; transform: scale(0.9); } }
    `);

    const $el = (tag, { cls, text, style, ...attrs } = {}, children = []) => {
        const el = document.createElement(tag);
        if (cls) el.className = cls;
        if (text) el.textContent = text;
        if (style) Object.assign(el.style, style);
        Object.entries(attrs).forEach(([key, val]) => {
            if (key.startsWith('on') && typeof val === 'function') el.addEventListener(key.substring(2).toLowerCase(), val);
            else el.setAttribute(key, val);
        });
        children.forEach(child => el.append(typeof child === 'string' ? document.createTextNode(child) : child));
        return el;
    };

    const UI = {
        container: null,
        getContainer() {
            if (!this.container) {
                this.container = $el('div', { id: 'gas-toast-container' });
                document.body.appendChild(this.container);
            }
            return this.container;
        },
        showToast(msg, type = 'success') {
            const isSuccess = type === 'success';
            const toast = $el('div', { cls: 'gas-toast' }, [
                $el('span', { cls: 'icon', style: { color: isSuccess ? '#137333' : '#d93025' }, text: isSuccess ? '✔' : 'ℹ' }),
                $el('span', { text: msg }),
                $el('button', { cls: 'close-btn', type: 'button', onClick: () => this.remove(toast) }, [$el('span', { text: '✕' })])
            ]);
            this.getContainer().appendChild(toast);
            setTimeout(() => this.remove(toast), 3000);
        },
        remove(el) {
            if (!el || el.classList.contains('leaving')) return;
            el.classList.add('leaving');
            el.addEventListener('animationend', () => el.remove(), { once: true });
        }
    };

    function createSpacingNode() {
        const mrow = document.createElementNS(NS_MATHML, 'mrow');
        const mo = document.createElementNS(NS_MATHML, 'mo');
        mo.textContent = '\u200A';
        mrow.appendChild(mo);
        return mrow;
    }

    function processMathML(mathRoot) {
        const operators = mathRoot.querySelectorAll('mo');
        operators.forEach(mo => {
            const opText = mo.textContent.trim();
            if (LARGE_OPS.has(opText)) {
                mo.setAttribute('data-mjx-texclass', 'OP');
                if (opText !== '∫' && opText !== '∮') mo.setAttribute('movablelimits', 'false');

                let parentBlock = mo.parentElement;
                while (parentBlock && parentBlock !== mathRoot) {
                    const tag = parentBlock.tagName.toLowerCase();
                    if (['munderover', 'munder', 'mover', 'msubsup', 'msub', 'msup'].includes(tag)) break;
                    parentBlock = parentBlock.parentElement;
                }

                if (parentBlock && parentBlock.parentElement) {
                    const nextSibling = parentBlock.nextElementSibling;
                    const isSpacingAlready = nextSibling && nextSibling.textContent === '\u200A';
                    if (!isSpacingAlready) {
                        const spacer = createSpacingNode();
                        parentBlock.parentElement.insertBefore(spacer, parentBlock.nextSibling);
                    }
                }
            }
        });
        return mathRoot;
    }

    async function ensureMathJaxReady() {
        if (typeof MathJax === 'undefined') throw new Error("MathJax 库未加载");
        const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error("MathJax 加载超时")), 5000));
        const ready = MathJax.startup && MathJax.startup.promise ? MathJax.startup.promise : Promise.resolve();
        await Promise.race([ready, timeout]);
    }

    async function handleCopy(element) {
        try {
            await ensureMathJaxReady();

            let latexSource = null;
            if (element.dataset.math) {
                latexSource = element.dataset.math;
            } else {
                const annotation = element.querySelector('annotation[encoding="application/x-tex"]');
                if (annotation) latexSource = annotation.textContent;
            }
            if (!latexSource) {
                 const inner = element.querySelector('[data-math]');
                 if (inner) latexSource = inner.dataset.math;
            }

            if (!latexSource) throw new Error("无法提取 LaTeX 源码");

            const rawMml = MathJax.tex2mml(latexSource, { display: true });

            const parser = new DOMParser();
            const trustedHtml = getTrustedHTML(rawMml);
            const doc = parser.parseFromString(trustedHtml, "text/xml");

            if (doc.querySelector("parsererror")) {
                throw new Error("Parser Error");
            }

            const processedMath = processMathML(doc.querySelector('math'));
            const finalMathML = processedMath.outerHTML;

            const htmlWrapper = `<!DOCTYPE html>
<html xmlns:m="http://schemas.microsoft.com/office/2004/12/omml">
<head><meta charset="utf-8"></head>
<body>${finalMathML}</body>
</html>`;

            const blobHtml = new Blob([htmlWrapper], { type: "text/html" });
            const blobText = new Blob([finalMathML], { type: "text/plain" });

            await navigator.clipboard.write([
                new ClipboardItem({ "text/html": blobHtml, "text/plain": blobText })
            ]);

            UI.showToast("公式已复制", "success");

        } catch (e) {
            console.error(e);
            UI.showToast("复制失败: " + e.message, "error");
        }
    }

    function initGlobalListener() {
        document.addEventListener('click', (e) => {
            const target = e.target.closest(SELECTOR_BASE);

            if (target) {
                e.stopPropagation();
                e.preventDefault();
                handleCopy(target);
            }
        }, true);
    }

    initGlobalListener();

})();