Greasy Fork

来自缓存

Greasy Fork is available in English.

GitHub 增强套件

融合 GitHub 增强功能:新窗口打开链接、MD 文件目录化、固定页面头部

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         GitHub 增强套件
// @namespace    http://tampermonkey.net/
// @version      0.8
// @description  融合 GitHub 增强功能:新窗口打开链接、MD 文件目录化、固定页面头部
// @author       lecoler & contributors
// @match        *://github.com/*
// @match        *://gitee.com/*/*
// @match        *://npmjs.com/*/*
// @include      *.md
// @icon         https://github.com/favicon.ico
// @license      GPL-3.0
// @run-at       document-start
// ==/UserScript==

(function() {
    'use strict';

    // === 通用工具和配置 ===
    const githubRegex = /^https?:\/\/(www\.)?github\.com\//i; // GitHub 链接正则
    const processedLinks = new WeakSet(); // 缓存已处理的链接
    const log = (...args) => console.log('[GitHub 增强套件]', ...args); // 简化的日志函数

    // ===========================================================================
    // === 功能1:强制在新窗口打开 GitHub 链接 ===
    // ===========================================================================

    function handleNewWindowClick(e) {
        const link = e.target.closest('a');
        if (!link || processedLinks.has(link)) return;

        const href = link.href;
        if (!href || !githubRegex.test(href) || href.includes('mailto:') || href.includes('#') || link.target === '_blank') return;

        e.preventDefault();
        e.stopPropagation();
        window.open(href, '_blank');
        processedLinks.add(link);
    }

    document.addEventListener('click', handleNewWindowClick, { capture: true, passive: false });

    // ===========================================================================
    // === 功能2:MD 文件目录化 ===
    // ===========================================================================

    let $main, $menu, $button, lastPathName = '', moveStatus = false, titleHeight = 0;

    function createMdDom() {
        const css = document.createElement('style');
        css.innerHTML = `
            .le-md { position: fixed; top: 16%; left: 90%; z-index: 999; }
            .le-md-btn { width: 60px; height: 60px; border-radius: 50%; color: #fff; background: hsla(230,50%,50%,0.6); text-align: center; line-height: 60px; cursor: move; }
            .le-md-btn:hover { background: hsla(220,50%,47%,1); }
            .le-md-btn-hidden { display: none; }
            .hidden { height: 0 !important; min-height: 0 !important; border: 0 !important; }
            .le-md-left { right: 0; margin-right: 100px; } .le-md-right { left: 0; margin-left: 100px; }
            .le-md-top { bottom: 0; } .le-md-bottom { top: 0; }
            .le-md > ul { width: 200px; max-height: 700px; list-style: none; position: absolute; overflow: auto; padding-right: 10px; }
            .le-md > ul a { text-decoration: none; color: #909399; display: block; padding: 5px 10px; background: #f4f4f5; }
            .le-md li.le-md-title-active a { background: linear-gradient(-135deg, #ffcccc 0.6em, #fff 0); }
            .le-md li.le-md-title-active.le-md-title-active-first a { background: linear-gradient(-135deg, #ff9999 0.6em, #fff 0); color: #000; font-weight: 700; }
        `;
        document.head.appendChild(css);

        $main = document.createElement('div');
        $button = document.createElement('div');
        $menu = document.createElement('ul');
        $button.innerHTML = '目录';
        $button.title = '右键返回顶部';
        $button.addEventListener('click', toggleMenu);
        $button.oncontextmenu = e => { scrollTo(0, 0); return false; };
        $main.appendChild($button);
        $main.appendChild($menu);
        $main.className = 'le-md';
        document.body.appendChild($main);

        $button.onmousedown = e => {
            const eleX = e.offsetX, eleY = e.offsetY;
            let count = 0;
            document.onmousemove = ev => {
                if (count++ > 9) moveStatus = true;
                const x = ev.clientX - eleX, y = ev.clientY - eleY;
                const winWidth = document.documentElement.clientWidth, winHeight = document.documentElement.clientHeight;
                $main.style.left = `${(x / winWidth * 100).toFixed(3)}%`;
                $main.style.top = `${(y / winHeight * 100).toFixed(3)}%`;
            };
            $button.onmouseup = $button.onmouseout = () => document.onmousemove = null;
        };

        window.onresize = () => { if (!$menu.className.match(/hidden/)) $menu.className += ' hidden'; };
    }

    function toggleMenu(e) {
        if (moveStatus) { moveStatus = false; return; }
        if ($menu.className.match(/hidden/)) {
            if (lastPathName !== location.pathname) generateMdMenu(true);
            const winWidth = document.documentElement.clientWidth, winHeight = document.documentElement.clientHeight;
            const x = e.clientX, y = e.clientY;
            $menu.className = `${winWidth / 2 - x > 0 ? 'le-md-right' : 'le-md-left'} ${winHeight / 2 - y > 0 ? 'le-md-bottom' : 'le-md-top'}`;
        } else {
            $menu.className += ' hidden';
        }
    }

    function generateMdMenu(flag) {
        lastPathName = location.pathname;
        let $content, list = [];
        const host = location.host;

        if (host === 'github.com') {
            const $parent = document.getElementById('readme') || document.getElementById('wiki-body');
            $content = $parent?.getElementsByClassName('markdown-body')[0];
            titleHeight = ($parent?.parentElement?.getElementsByClassName('js-sticky')[0]?.offsetHeight || 0) + 2;
            !$menu && window.addEventListener('pjax:complete', generateMdMenu);
        } else if (host === 'gitee.com') {
            $content = document.getElementById('tree-content-holder')?.getElementsByClassName('markdown-body')[0];
        } else if (host === 'www.npmjs.com') {
            $content = document.getElementById('readme');
        } else {
            $content = detectMdContent();
        }

        const $children = $content?.children || [];
        for (let $dom of $children) {
            const tag = $dom.tagName;
            if (tag.length === 2 && tag.startsWith('H') && !isNaN(+tag[1])) {
                const value = $dom.innerText.trim();
                const $a = $dom.getElementsByTagName('a')[0];
                if ($a) list.push({ type: +tag[1], value, href: $a.getAttribute('href'), offsetTop: getOffsetTop($a) });
            }
        }

        if ($menu) $menu.innerHTML = '';
        else createMdDom();
        if (!flag) $menu.className = 'hidden';

        if (list.length) {
            list.forEach(i => {
                const li = document.createElement('li');
                li.setAttribute('data-offsetTop', i.offsetTop);
                const a = document.createElement('a');
                a.href = i.href;
                a.title = i.value;
                a.style = `font-size:${1.3 - i.type * 0.1}em;margin-left:${i.type - 1}em;border-left:0.5em groove hsla(200,80%,${45 + i.type * 10}%,0.8);`;
                a.innerText = i.value;
                li.appendChild(a);
                $menu.appendChild(li);
            });
            $button.className = 'le-md-btn';
        } else {
            $button.className = 'le-md-btn le-md-btn-hidden';
        }

        initScrollHighlight();
    }

    function detectMdContent() {
        let tmp = [];
        for (let i = 1; i < 7; i++) {
            const list = document.body.getElementsByTagName(`h${i}`);
            for (let j of list) {
                const parent = j.parentElement;
                const item = tmp.find(k => k.ele.isEqualNode(parent));
                if (item) item.count++; else tmp.push({ ele: parent, count: 1 });
            }
        }
        return tmp.sort((a, b) => b.count - a.count)[0]?.ele || null;
    }

    function getOffsetTop($dom, val = 0) {
        return $dom ? getOffsetTop($dom.offsetParent, ($dom.offsetTop || 0) + val) : val;
    }

    function initScrollHighlight() {
        const update = debounce(() => {
            const scrollTop = (document.documentElement.scrollTop || document.body.scrollTop || 0) + titleHeight;
            const offsetHeight = document.documentElement.clientHeight || document.body.clientHeight || 0;
            const scrollHeight = document.documentElement.scrollHeight || document.body.scrollHeight || 0;
            if ($menu) {
                Array.from($menu.children).forEach((li, i) => {
                    const val = +li.getAttribute('data-offsetTop');
                    const nextVal = $menu.children[i + 1] ? +$menu.children[i + 1].getAttribute('data-offsetTop') : scrollHeight;
                    li.className = '';
                    if (scrollTop <= val && val <= offsetHeight + scrollTop) li.className = 'le-md-title-active';
                    if (scrollTop >= val && nextVal > scrollTop) li.className = 'le-md-title-active le-md-title-active-first';
                });
            }
        }, 500);
        window.onscroll = window.onscroll ? function() { window.onscroll.call(this); update(); } : update;
    }

    function debounce(func, time) {
        let timeId;
        return function() {
            clearTimeout(timeId);
            timeId = setTimeout(() => func.apply(this, arguments), time);
        };
    }

    // ===========================================================================
    // === 功能3:固定页面头部 ===
    // ===========================================================================

    // 固定 GitHub 页面头部
    function fixPageHeader() {
        // 使用更健壮的选择器定位 GitHub 头部(兼容不同页面)
        const header = document.querySelector('.AppHeader, header[role="banner"]');
        if (!header) {
            log('未找到页面头部元素');
            return;
        }

        // 注入固定样式的 CSS
        const css = document.createElement('style');
        css.id = 'fixed-header-style'; // 添加 ID 以便后续更新
        css.innerHTML = `
            .AppHeader, header[role="banner"] {
                position: fixed !important;
                top: 0 !important;
                left: 0 !important;
                width: 100% !important;
                z-index: 10000 !important; /* 高优先级,避免被覆盖 */
                background-color: #24292e !important; /* GitHub 默认头部背景色 */
                box-shadow: 0 2px 4px rgba(0,0,0,0.1); /* 添加阴影,提升视觉效果 */
            }
            body {
                padding-top: ${header.offsetHeight}px !important; /* 根据头部高度调整内容偏移 */
            }
        `;
        document.head.appendChild(css);

        // 监听 PJAX 事件,确保动态加载后更新样式
        window.addEventListener('pjax:complete', () => {
            const newHeader = document.querySelector('.AppHeader, header[role="banner"]');
            if (newHeader) {
                document.body.style.paddingTop = `${newHeader.offsetHeight}px`;
            }
        });

        // 使用 MutationObserver 监听 DOM 变化,确保头部始终固定
        const observer = new MutationObserver(() => {
            const currentHeader = document.querySelector('.AppHeader, header[role="banner"]');
            if (currentHeader && !currentHeader.style.position) {
                document.body.style.paddingTop = `${currentHeader.offsetHeight}px`;
            }
        });
        observer.observe(document.body, { childList: true, subtree: true });
    }

    // ===========================================================================
    // === 初始化脚本 ===
    // ===========================================================================

    document.onreadystatechange = () => {
        if (document.readyState === 'complete') {
            generateMdMenu(); // 初始化 MD 目录
            fixPageHeader();  // 初始化固定页面头部
            log('已加载');
        }
    };
})();