GitHub README tab render

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

Versão de: 16/05/2026. Veja: a última versão.

Você precisará instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Você precisará instalar uma extensão como o Tampermonkey para instalar este script.

Você precisará instalar um gerenciador de scripts de usuário para instalar este script.

(Eu já tenho um gerenciador de scripts de usuário, me deixe instalá-lo!)

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

(Eu já possuo um gerenciador de estilos de usuário, me deixar fazer a instalação!)

// ==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 });

})();