GitHub README tab render

Native github tab rendering and switching of README in multiple languages present in the project

Fra 16.05.2026. Se den seneste versjonen.

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.0.0
// @description  Native github tab rendering and switching of README in multiple languages present in the project
// @author       Longlone & Gemini
// @license      MIT
// @match        https://github.com/*/*
// @icon         https://github.githubassets.com/pinned-octocat.svg
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    // 全局页面缓存池
    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;
    }

    // 扫描存在的 README 链接
    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;
    }

    // 抓取并渲染指定的语言内容
    async function fetchAndRender(lang, url, targetTab) {
        const article = document.querySelector('article.markdown-body');
        if (!article) return;

        // 切换 UI 高亮状态
        const allTabs = document.querySelectorAll('nav[aria-label="Repository files"] ul a');
        allTabs.forEach(a => a.removeAttribute('aria-current'));
        targetTab.setAttribute('aria-current', 'page');

        if (globalCache.langs[lang]) {
            article.innerHTML = globalCache.langs[lang];
            return;
        }

        const currentHTML = article.innerHTML;
        article.innerHTML = `
            <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>`;

        try {
            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;
            alert(`Loading ${lang} Error, please check network`);
        }
    }

    function restoreDefault(nativeTab) {
        const article = document.querySelector('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');

        if (globalCache.defaultHTML) {
            article.innerHTML = globalCache.defaultHTML;
        } else {
            window.location.reload();
        }
    }

    // 绑定全局点击事件 (不受 React 节点销毁的影响)
    if (!window._readmeTabsDelegated) {
        document.addEventListener('click', (e) => {
            // 1. 监听我们自己添加的自定义 Tab 的点击
            const customTab = e.target.closest('.custom-readme-tab-link');
            if (customTab) {
                e.preventDefault();
                e.stopPropagation();
                fetchAndRender(customTab.dataset.lang, customTab.dataset.url, customTab);
                return;
            }

            // 2. 监听原生 README Tab 的点击
            const nativeLink = e.target.closest('a');
            if (nativeLink) {
                const nav = nativeLink.closest('nav[aria-label="Repository files"]');
                if (nav) {
                    const span = nativeLink.querySelector('span[data-component="text"]');
                    if (span && span.textContent.trim().toUpperCase() === 'README' && !nativeLink.classList.contains('custom-readme-tab-link')) {
                        const activeCustom = document.querySelector('.custom-readme-tab-link[aria-current="page"]');
                        if (activeCustom) {
                            e.preventDefault();
                            e.stopPropagation();
                            restoreDefault(nativeLink);
                        }
                    }
                }
            }
        }, true);
        window._readmeTabsDelegated = true;
    }

    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;

        // 定位原生的 README 标签
        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');

        if (nativeTabLink.hasAttribute('aria-current')) {
            const article = document.querySelector('article.markdown-body');
            if (article && !globalCache.defaultHTML) {
                globalCache.defaultHTML = article.innerHTML;
            }
        }

        // 将缺失的多语言 Tab 挨个注入到原生 README 的右侧
        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 });

})();