HKUST-GPT-CodeEnhancer-Pro

增强HKUST GPT的代码显示(自动语法高亮+一键复制)

当前为 2025-02-27 提交的版本,查看 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         HKUST-GPT-CodeEnhancer-Pro
// @namespace    http://tampermonkey.net/
// @version      1.0.4
// @description  增强HKUST GPT的代码显示(自动语法高亮+一键复制)
// @author       Daisy_Zhang
// @match        https://gpt.hkust-gz.edu.cn/*
// @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-python.min.js
// @resource     prismCSS https://unpkg.com/prism-theme-night-owl@latest/build/style.css
// @grant        GM_addStyle
// @grant        GM_getResourceText
// ==/UserScript==

(function() {
    'use strict';

    const CONFIG = {
        codeSelector: 'pre > code[class^="language-"]',
        copyBtnClass: 'gpt-copy-btn-pro',
        styles: {
            base: `
                position: absolute;
                right: 10px;
                top: 10px;
                background-color: rgba(51, 51, 51, 0.8);
                color:#fff !important;
                border-radius:4px;
                padding:.5em .8em;
                font-size:.85rem;
                cursor:pointer;
                border:none;
                transition:.15s all ease-out;
                z-index:1000`,
            hover: 'background-color:#444!important',
            copied: 'background-color:#27ae60!important'
        },
        observerConfig: {
            childList: true,
            subtree: true,
            attributes: false,
            characterData: false
        }
    };

    //============= Core Functions ===============//

    function initCore() {
        injectPrismTheme();
        document.querySelectorAll(CONFIG.codeSelector).forEach(codeBlock => {
            if (!codeBlock.closest('.processed')) processCodeBlock(codeBlock);
        });
        Prism.highlightAll();
        setupObserver();
        console.log('[CodePro] Initialized');
    }

    function processCodeBlock(codeEl) {
        const wrapper = wrapWithDiv(codeEl.parentElement);
        if (!wrapper.querySelector('.' + CONFIG.copyBtnClass)) {
            attachCopyButton(wrapper);
            codeEl.classList.add('processed');
        }
    }

    function wrapWithDiv(preElement) {
        const wrapper = document.createElement('div');
        Object.assign(wrapper.style, {
            position: 'relative',
            marginTop: '25px',
            backgroundColor: '#2d2d2d',
            borderRadius: '6px'
        });

        preElement.parentNode.insertBefore(wrapper, preElement);
        wrapper.appendChild(preElement);
        return wrapper;
    }

    function attachCopyButton(wrapper) {
        const btn = createCopyButton();
        btn.onclick = handleCopy.bind(null, btn); // Bind the button to the handleCopy function
        wrapper.appendChild(btn);
    }

    function createCopyButton() {
        const btn = document.createElement('button');
        btn.className = CONFIG.copyBtnClass + ' fresh';
        Object.assign(btn.style, styleStringToObject(CONFIG.styles.base));

        //交互效果:
        btn.onmouseenter = () => { Object.assign(btn.style, styleStringToObject(CONFIG.styles.hover)); };
        btn.onmouseleave = () => { Object.assign(btn.style, styleStringToObject(CONFIG.styles.base)); };

        btn.updateText = (text) => {
            btn.textContent = text || '📋 Copy';
            return btn;
        };

        btn.updateText(); // Initialize button text
        return btn;
    }

    async function handleCopy(btn) {
        const codeText = btn.parentNode.querySelector('code').innerText.trim();
        try {
            await navigator.clipboard.writeText(codeText);
            btn.updateText("✅ Copied!");
            setTimeout(() => btn.updateText(), 1500);
        } catch (err) {
            alert('[⚠️ Error] Please copy manually: ' + codeText);
            btn.updateText("❌ Failed");
            setTimeout(() => btn.updateText(), 2000);
        }
    }

    function styleStringToObject(cssStr) {
        return cssStr.split(/;(?!base64)/)
            .filter(s => s.trim())
            .map(pair => pair.split(':').map(p => p.trim()))
            .reduce((obj, [key, val]) => (obj[key] = val, obj), {});
    }

    function injectPrismTheme() {
        try {
            const themeCss = `${GM_getResourceText("prismCSS")} \n/* Custom Overrides */\n pre { background: #2d2d2d !important; }`;
            GM_addStyle(`${themeCss}\n\n${generateCustomStyles()}`);
        } catch (e) {
            console.error('[CSS Error]' + e);
        }
    }

    function generateCustomStyles() {
        return `
            .gpt-copy-btn-pro:hover { opacity: .95 !important; }
            .gpt-copy-btn-pro.fresh::after {
                content: "";
                position: absolute;
                right: -5%;
                top: -30%;
                width: .8em;
                height: .8em;
                border-radius: 50%;
                animation: pulseAnim 1.5s infinite;
            }
            @keyframes pulseAnim {
                50% { box-shadow: 0 0 0.5em #aaa; }
            }
        `;
    }

    let domObserver;

    function setupObserver() {
        domObserver?.disconnect();
        domObserver = new MutationObserver(mutationsHandler);
        domObserver.observe(document.body, CONFIG.observerConfig);
    }

    function mutationsHandler(muts) {
        if (!muts.some(mutationFilter)) return;
        requestIdleCallback(() => initCore(), { timeout: 300 });
    }

    function mutationFilter(mutation) {
        return Array.from(mutation.addedNodes).some(node => {
            // Case 1: The added node itself is a code block element
            if (node.nodeType === Node.ELEMENT_NODE && node.matches(CONFIG.codeSelector)) {
                return true;
            }

            // Case 2: The added container contains code blocks in its descendants
            if (typeof node.querySelector === 'function' && !!node.querySelector(CONFIG.codeSelector)) {
                return true;
            }

            // Edge case: Handle text nodes wrapped in spans/divs etc.
            if (/^(#text|SPAN|DIV)$/.test(node.nodeName) && checkAncestorCodeBlock(node.parentElement)) {
                return true;
            }

            return false;
        });
    }

    // Helper function to check parent chain for existing code blocks
    function checkAncestorCodeBlock(element, depth = 3) {
        while (depth-- > 0 && element) {
            if (element.matches?.(CONFIG.codeSelector)) {
                console.log('Found ancestor:', element);
                return true;
            }
            element = element.parentElement;
        }
        return false;
    }

    // Initialize the script
    initCore();
})();