Last active
March 21, 2026 14:03
-
-
Save orange030/dd6abf277b6184869ed12ed262fa0ce1 to your computer and use it in GitHub Desktop.
为 Gemini 网页版的 GDScript 代码片段提供语法高亮
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==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); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment