Greasy Fork is available in English.
Native github tab rendering and switching of README in multiple languages present in the project
// ==UserScript==
// @name GitHub README tab render
// @namespace http://tampermonkey.net/
// @version 1.1.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';
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;
}
async function fetchAndRender(lang, url, targetTab) {
let article = document.querySelector('article.markdown-body');
if (!article) {
const navUl = document.querySelector('nav[aria-label="Repository files"] ul');
if (!navUl) {
window.location.href = url;
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) {
// 模拟点击切回原生 README,触发 GitHub 的 PJAX/React 路由
nativeTabLink.click();
// 轮询等待 Markdown 容器被 GitHub 渲染出来
let retry = 0;
const timer = setInterval(() => {
if (document.querySelector('article.markdown-body') || retry > 50) { // 最多等 5 秒
clearInterval(timer);
// 重新获取 DOM 节点(防止被 React 销毁)
const newTargetTab = document.querySelector(`.custom-readme-tab-link[data-lang="${lang}"]`) || targetTab;
if (document.querySelector('article.markdown-body')) {
// 容器就绪,重新执行渲染逻辑
fetchAndRender(lang, url, newTargetTab);
} else {
window.location.href = url; // 超时保底
}
}
retry++;
}, 100);
return;
} else {
window.location.href = url; // 保底跳转
return;
}
}
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 = `
<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');
nativeTab.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' });
if (globalCache.defaultHTML) {
article.innerHTML = globalCache.defaultHTML;
} else {
window.location.reload();
}
}
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;
}
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');
if (nativeTabLink.hasAttribute('aria-current')) {
const article = document.querySelector('article.markdown-body');
if (article && !globalCache.defaultHTML) {
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 });
})();