GitHub README tab render

Native github tab rendering and switching of README in multiple languages without forcing page reloads

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

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.

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

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

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         GitHub README tab render
// @namespace    http://tampermonkey.net/
// @version      1.2.0
// @description  Native github tab rendering and switching of README in multiple languages without forcing page reloads
// @author       Longlone & Gemini
// @license      MIT
// @match        https://github.com/*/*
// @icon         https://github.githubassets.com/pinned-octocat.svg
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    // 注入自定义样式
    if (!document.getElementById('custom-readme-tab-styles')) {
        const style = document.createElement('style');
        style.id = 'custom-readme-tab-styles';
        style.innerHTML = `
            nav[aria-label="Repository files"] {
                min-width: 0 !important;
            }
            nav[aria-label="Repository files"] ul {
                flex-wrap: nowrap !important;
                overflow-x: auto !important;
                overflow-y: hidden !important;
                scrollbar-width: none !important;
            }
            nav[aria-label="Repository files"] ul::-webkit-scrollbar {
                display: none !important;
            }
            nav[aria-label="Repository files"] ul li {
                flex-shrink: 0 !important;
            }
            nav[aria-label="Repository files"] ul a[aria-current="page"]::after,
            nav[aria-label="Repository files"] ul a[aria-selected="true"]::after {
                bottom: 0 !important;
                transform: translateX(50%) !important;
            }
        `;
        document.head.appendChild(style);
    }

    // 全局缓存
    const globalCache = {
        repoPath: '',
        files: [],
        defaultHTML: null,
        langs: {}
    };

    function getRepoInfo() {
        const parts = window.location.pathname.split('/').filter(Boolean);
        if (parts.length >= 2) return { owner: parts[0], repo: parts[1] };
        return null;
    }

    function scanLinks(repoInfo) {
        const links = Array.from(document.querySelectorAll('a[href]'));
        const seen = new Set();
        const results = [];
        const regex = new RegExp(`^/${repoInfo.owner}/${repoInfo.repo}/blob/[^/]+/readme.*\\.md$`, 'i');

        links.forEach(a => {
            const href = a.getAttribute('href') || '';
            if (regex.test(href)) {
                const name = href.split('/').pop();
                if (!seen.has(name.toLowerCase()) && name.toLowerCase() !== 'readme.md') {
                    seen.add(name.toLowerCase());
                    results.push({ name: name, url: href });
                }
            }
        });
        return results;
    }

    // 安全等待 DOM 元素出现(避免因为 GitHub 路由延迟导致找不到节点)
    async function waitForElement(selector, timeout = 3000) {
        const start = Date.now();
        while (Date.now() - start < timeout) {
            const el = document.querySelector(selector);
            if (el) return el;
            await new Promise(resolve => setTimeout(resolve, 100));
        }
        return null;
    }

    // 获取 Loading 动画 HTML
    function getLoadingHTML() {
        return `
            <div style="text-align:center; padding: 80px 20px; color: var(--color-fg-muted);">
                <svg style="animation: rotate 2s linear infinite; margin-bottom: 12px; fill: currentColor;" aria-hidden="true" height="32" viewBox="0 0 16 16" width="32" class="octicon octicon-sync">
                    <path d="M1.705 8.005a.75.75 0 0 1 .834.656 5.5 5.5 0 0 0 9.592 2.97l-1.204-1.204a.25.25 0 0 1 .177-.427h3.646a.25.25 0 0 1 .25.25v3.646a.25.25 0 0 1-.427.177l-1.38-1.38A7.002 7.002 0 0 1 1.05 8.84a.75.75 0 0 1 .656-.834ZM8 2.5a5.487 5.487 0 0 0-4.131 1.869l1.204 1.204A.25.25 0 0 1 4.896 6H1.25A.25.25 0 0 1 1 5.75V2.104a.25.25 0 0 1 .427-.177l1.38 1.38A7.002 7.002 0 0 1 14.95 7.16a.75.75 0 0 1-1.49.178A5.5 5.5 0 0 0 8 2.5Z"></path>
                </svg>
                <style>@keyframes rotate { 100% { transform: rotate(360deg); } }</style>
                <br><span style="font-weight: 500;">Loading ...</span>
            </div>`;
    }

    async function fetchAndRender(lang, url, targetTab) {
        // 尝试获取 Markdown 容器,如果暂未渲染则等待最多 3 秒
        const article = await waitForElement('article.markdown-body');
        if (!article) {
            console.warn("未找到 Markdown 渲染容器,取消当前操作。");
            return; // 彻底移除了这里的 window.location.href 跳转
        }

        // 切换 Tab 高亮状态
        const allTabs = document.querySelectorAll('nav[aria-label="Repository files"] ul a');
        allTabs.forEach(a => a.removeAttribute('aria-current'));
        targetTab.setAttribute('aria-current', 'page');
        targetTab.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' });

        // 如果有缓存,直接渲染缓存
        if (globalCache.langs[lang]) {
            article.innerHTML = globalCache.langs[lang];
            return;
        }

        const currentHTML = article.innerHTML;
        article.innerHTML = getLoadingHTML();

        try {
            // 后台拉取对应语言的 README 页面并解析 DOM
            const res = await fetch(url);
            const text = await res.text();
            const doc = new DOMParser().parseFromString(text, 'text/html');
            const newMarkdown = doc.querySelector('article.markdown-body');

            if (newMarkdown) {
                article.innerHTML = newMarkdown.innerHTML;
                globalCache.langs[lang] = newMarkdown.innerHTML;
            } else {
                throw new Error("README parse error");
            }
        } catch(err) {
            article.innerHTML = currentHTML; // 失败则恢复原状
            console.error(`Loading ${lang} Error:`, err);
        }
    }

    async function restoreDefault(nativeTab) {
        const article = await waitForElement('article.markdown-body');
        if (!article) return;

        const allTabs = document.querySelectorAll('nav[aria-label="Repository files"] ul a');
        allTabs.forEach(a => a.removeAttribute('aria-current'));
        nativeTab.setAttribute('aria-current', 'page');
        nativeTab.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' });

        // 如果原内容有缓存,直接恢复
        if (globalCache.defaultHTML) {
            article.innerHTML = globalCache.defaultHTML;
        } else {
            // 如果缓存丢失,不要重新加载页面!改为动态 fetch 原生的 URL
            article.innerHTML = getLoadingHTML();
            try {
                const res = await fetch(nativeTab.href);
                const text = await res.text();
                const doc = new DOMParser().parseFromString(text, 'text/html');
                const nativeMarkdown = doc.querySelector('article.markdown-body');
                if (nativeMarkdown) {
                    article.innerHTML = nativeMarkdown.innerHTML;
                    globalCache.defaultHTML = nativeMarkdown.innerHTML;
                }
            } catch(e) {
                console.error("Failed to restore default README", e);
            }
        }
    }

    // 事件委托拦截点击
    if (!window._readmeTabsDelegated) {
        document.addEventListener('click', (e) => {
            const customTab = e.target.closest('.custom-readme-tab-link');
            if (customTab) {
                e.preventDefault();
                e.stopPropagation();
                fetchAndRender(customTab.dataset.lang, customTab.dataset.url, customTab);
                return;
            }

            const nativeLink = e.target.closest('a');
            if (nativeLink) {
                const nav = nativeLink.closest('nav[aria-label="Repository files"]');
                if (nav && !nativeLink.classList.contains('custom-readme-tab-link')) {
                    const span = nativeLink.querySelector('span[data-component="text"]');
                    const activeCustom = document.querySelector('.custom-readme-tab-link[aria-current="page"]');

                    if (span && span.textContent.trim().toUpperCase() === 'README') {
                        if (activeCustom) {
                            e.preventDefault();
                            e.stopPropagation();
                            restoreDefault(nativeLink);
                        }
                    } else {
                        if (activeCustom) {
                            activeCustom.removeAttribute('aria-current');
                        }
                    }
                }
            }
        }, true);
        window._readmeTabsDelegated = true;
    }

    // DOM 监听:注入标签并记录初始状态
    const observer = new MutationObserver(() => {
        const repoInfo = getRepoInfo();
        if (!repoInfo) return;

        const currentPath = `${repoInfo.owner}/${repoInfo.repo}`;
        if (globalCache.repoPath !== currentPath) {
            globalCache.repoPath = currentPath;
            globalCache.files = [];
            globalCache.defaultHTML = null;
            globalCache.langs = {};
        }

        if (globalCache.files.length === 0) {
            globalCache.files = scanLinks(repoInfo);
        }

        if (globalCache.files.length === 0) return;

        const navUl = document.querySelector('nav[aria-label="Repository files"] ul');
        if (!navUl) return;

        const nativeTabLink = Array.from(navUl.querySelectorAll('a')).find(a => {
            const span = a.querySelector('span[data-component="text"]');
            return span && span.textContent.trim().toUpperCase() === 'README' && !a.classList.contains('custom-readme-tab-link');
        });

        if (!nativeTabLink) return;
        const nativeTabLi = nativeTabLink.closest('li');

        // 记录原生 README 内容(防止切换后丢失)
        if (nativeTabLink.hasAttribute('aria-current')) {
            const article = document.querySelector('article.markdown-body');
            if (article && !globalCache.defaultHTML && !article.innerHTML.includes('Loading ...')) {
                globalCache.defaultHTML = article.innerHTML;
            }
        }

        let insertRef = nativeTabLi;
        globalCache.files.forEach(file => {
            let lbl = file.name.replace(/^readme[-_.]?/i, '').replace(/\.md$/i, '').toUpperCase() || file.name;

            const existing = document.querySelector(`.custom-readme-tab-link[data-lang="${lbl}"]`);
            if (!existing) {
                const li = document.createElement('li');
                li.className = 'prc-UnderlineNav-UnderlineNavItem-syRjR custom-readme-li';

                const a = document.createElement('a');
                a.href = '#';
                a.className = 'prc-components-UnderlineItem-7fP-n custom-readme-tab-link';
                a.dataset.lang = lbl;
                a.dataset.url = file.url;

                a.innerHTML = `
                    <span data-component="icon">
                        <svg data-component="Octicon" aria-hidden="true" focusable="false" class="octicon octicon-book" viewBox="0 0 16 16" width="16" height="16" fill="currentColor" display="inline-block" overflow="visible" style="vertical-align: text-bottom;">
                            <path d="M0 1.75A.75.75 0 0 1 .75 1h4.253c1.227 0 2.317.59 3 1.501A3.743 3.743 0 0 1 11.006 1h4.245a.75.75 0 0 1 .75.75v10.5a.75.75 0 0 1-.75.75h-4.507a2.25 2.25 0 0 0-1.591.659l-.622.621a.75.75 0 0 1-1.06 0l-.622-.621A2.25 2.25 0 0 0 5.258 13H.75a.75.75 0 0 1-.75-.75Zm7.251 10.324.004-5.073-.002-2.253A2.25 2.25 0 0 0 5.003 2.5H1.5v9h3.757a3.75 3.75 0 0 1 1.994.574ZM8.755 4.75l-.004 7.322a3.752 3.752 0 0 1 1.992-.572H14.5v-9h-3.495a2.25 2.25 0 0 0-2.25 2.25Z"></path>
                        </svg>
                    </span>
                    <span data-component="text" data-content="${lbl}">${lbl}</span>
                `;
                li.appendChild(a);
                insertRef.after(li);
                insertRef = li;
            }
        });
    });

    observer.observe(document.body, { childList: true, subtree: true });

})();