Gemini GDScript 代码高亮

为 Gemini 网页版的 GDScript 代码片段提供语法高亮,基于 Prism.js。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Gemini GDScript Syntax Highlighter
// @name:zh-CN   Gemini GDScript 代码高亮
// @namespace    http://tampermonkey.net/
// @version      1.3
// @description  Highlights GDScript code in the Gemini web interface using Prism.js.
// @description:zh-CN 为 Gemini 网页版的 GDScript 代码片段提供语法高亮,基于 Prism.js。
// @author       Anonymous
// @match        https://gemini.google.com/*
// @require      https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-gdscript.min.js
// @grant        GM_addStyle
// @run-at       document-end
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    if (typeof Prism === 'undefined') {
        console.error('❌ [GDScript Highlighter] 核心库 Prism.js 未加载。');
        return;
    }

    // 1. 创建 TrustedHTML 策略
    let ttPolicy;
    if (window.trustedTypes && window.trustedTypes.createPolicy) {
        try {
            const policyName = 'gdscript-policy-' + Math.random().toString(36).substring(2, 9);
            ttPolicy = window.trustedTypes.createPolicy(policyName, {
                createHTML: (string) => string
            });
        } catch (e) {
            console.error('❌ [GDScript Highlighter] TrustedTypes 策略创建失败:', e);
        }
    }

    // 2. 实时检测 Gemini 网页的实际主题模式
    function updateThemeMode() {
        const textColor = window.getComputedStyle(document.body).color;
        const match = textColor.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
        let isDarkMode = true;
        if (match) {
            const luma = 0.2126 * parseInt(match[1], 10) + 0.7152 * parseInt(match[2], 10) + 0.0722 * parseInt(match[3], 10);
            isDarkMode = luma > 128;
        }
        document.documentElement.setAttribute('data-gds-theme', isDarkMode ? 'dark' : 'light');
    }

    const themeObserver = new MutationObserver((mutations) => {
        for (let m of mutations) {
            if (m.type === 'attributes' && (m.attributeName === 'class' || m.attributeName === 'style')) {
                updateThemeMode();
                break;
            }
        }
    });
    themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['class', 'style', 'data-theme'] });
    themeObserver.observe(document.body, { attributes: true, attributeFilter: ['class', 'style'] });
    updateThemeMode();

    // 3. 注入分离的深浅色高亮样式
    GM_addStyle(`
        code.gdscript-injected {
            background: transparent !important;
            text-shadow: none !important;
            font-family: inherit !important;
            tab-size: 4 !important;
        }

        /* ====== 浅色主题 ====== */
        html[data-gds-theme="light"] code.gdscript-injected { color: #24292e !important; }
        html[data-gds-theme="light"] code.gdscript-injected .token.comment { color: #6a737d !important; font-style: italic !important; }
        html[data-gds-theme="light"] code.gdscript-injected .token.punctuation { color: #24292e !important; }
        html[data-gds-theme="light"] code.gdscript-injected .token.keyword { color: #d73a49 !important; }
        html[data-gds-theme="light"] code.gdscript-injected .token.function { color: #6f42c1 !important; }
        html[data-gds-theme="light"] code.gdscript-injected .token.string { color: #032f62 !important; }
        html[data-gds-theme="light"] code.gdscript-injected .token.number { color: #005cc5 !important; }
        html[data-gds-theme="light"] code.gdscript-injected .token.class-name,
        html[data-gds-theme="light"] code.gdscript-injected .token.builtin { color: #e36209 !important; }
        html[data-gds-theme="light"] code.gdscript-injected .token.operator { color: #d73a49 !important; }
        html[data-gds-theme="light"] code.gdscript-injected .token.boolean,
        html[data-gds-theme="light"] code.gdscript-injected .token.property,
        html[data-gds-theme="light"] code.gdscript-injected .token.constant { color: #005cc5 !important; }
        html[data-gds-theme="light"] code.gdscript-injected .token.variable { color: #e36209 !important; }

        /* ====== 深色主题 ====== */
        html[data-gds-theme="dark"] code.gdscript-injected { color: #d4d4d4 !important; }
        html[data-gds-theme="dark"] code.gdscript-injected .token.comment { color: #6a9955 !important; font-style: italic !important; }
        html[data-gds-theme="dark"] code.gdscript-injected .token.punctuation { color: #d4d4d4 !important; }
        html[data-gds-theme="dark"] code.gdscript-injected .token.keyword { color: #569cd6 !important; }
        html[data-gds-theme="dark"] code.gdscript-injected .token.function { color: #dcdcaa !important; }
        html[data-gds-theme="dark"] code.gdscript-injected .token.string { color: #ce9178 !important; }
        html[data-gds-theme="dark"] code.gdscript-injected .token.number { color: #b5cea8 !important; }
        html[data-gds-theme="dark"] code.gdscript-injected .token.class-name,
        html[data-gds-theme="dark"] code.gdscript-injected .token.builtin { color: #4ec9b0 !important; }
        html[data-gds-theme="dark"] code.gdscript-injected .token.operator { color: #d4d4d4 !important; }
        html[data-gds-theme="dark"] code.gdscript-injected .token.boolean,
        html[data-gds-theme="dark"] code.gdscript-injected .token.property,
        html[data-gds-theme="dark"] code.gdscript-injected .token.variable { color: #9cdcfe !important; }
        html[data-gds-theme="dark"] code.gdscript-injected .token.constant { color: #4fc1ff !important; }
    `);

    // 4. 执行 GDScript 识别与渲染
    function highlightGDScript() {
        // 去除之前强制的 :not(.gdscript-injected) 过滤,因为我们在内部用 dataset 判断流式输出状态
        const codeBlocks = document.querySelectorAll('code');

        codeBlocks.forEach((block) => {
            // ================= 关键修复 =================
            // 过滤掉聊天文本中的"行内代码" (Inline code)。多行代码块必定在 <pre> 内。
            const preNode = block.closest('pre');
            if (!preNode) return;
            // ==========================================

            let isGDScript = false;
            const className = block.className.toLowerCase();

            // 1. 直接通过类名判断
            if (className.includes('gdscript') || className.includes('language-gd')) {
                isGDScript = true;
            }

            // 2. 精确查找头部工具栏进行判断
            if (!isGDScript) {
                let node = preNode; // 从 pre 标签开始向上找,而不是直接用 parent.textContent
                let depth = 0;

                while (node && depth < 4) {
                    if (node.previousElementSibling) {
                        const siblingText = node.previousElementSibling.textContent.toLowerCase().trim();
                        // 语言栏通常很短,这里限制长度防止误读整段非代码文本
                        if (siblingText.length < 50 && siblingText.includes('gdscript')) {
                            isGDScript = true;
                            break;
                        }
                    }
                    node = node.parentElement;
                    depth++;
                }
            }

            if (isGDScript) {
                const rawCode = block.textContent;

                // 防止在流式输出时发生死循环或重置用户选择状态
                if (block.dataset.rawText === rawCode) return;
                block.dataset.rawText = rawCode;

                block.className = 'gdscript-injected language-gdscript';
                const highlightedHTML = Prism.highlight(rawCode, Prism.languages.gdscript, 'gdscript');

                if (ttPolicy) {
                    block.innerHTML = ttPolicy.createHTML(highlightedHTML);
                } else {
                    block.innerHTML = highlightedHTML;
                }
            }
        });
    }

    // 5. 监听页面 DOM 变化,适配流式输出
    let debounceTimer = null;
    const observer = new MutationObserver(() => {
        clearTimeout(debounceTimer);
        debounceTimer = setTimeout(highlightGDScript, 300);
    });

    observer.observe(document.body, { childList: true, subtree: true, characterData: true });

    setTimeout(highlightGDScript, 500);

})();