Gemini GDScript Syntax Highlighter

Highlights GDScript code in the Gemini web interface using Prism.js.

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

You will need to install an extension such as Tampermonkey to install this script.

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==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);

})();