Gemini GDScript Syntax Highlighter

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

Du musst eine Erweiterung wie Tampermonkey, Greasemonkey oder Violentmonkey installieren, um dieses Skript zu installieren.

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

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

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

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

Sie müssten eine Skript Manager Erweiterung installieren damit sie dieses Skript installieren können

(Ich habe schon ein Skript Manager, Lass mich es installieren!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

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

})();