mirror of
https://github.com/XTLS/Xray-docs-next.git
synced 2026-09-22 22:38:05 +03:00
显示页面贡献者
This commit is contained in:
@@ -4,3 +4,4 @@ node_modules/
|
||||
# VitePress
|
||||
.vitepress/dist/
|
||||
.vitepress/cache/
|
||||
.vitepress/.generated/contributors.json
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
<script setup lang="ts">
|
||||
import contributorsMap from "../../.generated/contributors.json";
|
||||
import { useRoute, useData } from "vitepress";
|
||||
import { computed } from "vue";
|
||||
|
||||
type Contributor = { name: string; email?: string; commits: number };
|
||||
|
||||
const route = useRoute();
|
||||
const { lang } = useData();
|
||||
|
||||
const list = computed(() => {
|
||||
const map = contributorsMap as Record<string, Contributor[]>;
|
||||
|
||||
const raw = route.path;
|
||||
|
||||
const candidates = new Set<string>();
|
||||
|
||||
const noHtml = raw.replace(/\.html$/, "");
|
||||
candidates.add(raw);
|
||||
candidates.add(noHtml);
|
||||
|
||||
// /foo -> /foo/ and /foo/ -> /foo
|
||||
for (const p of [raw, noHtml]) {
|
||||
if (p.endsWith("/")) candidates.add(p.slice(0, -1));
|
||||
else candidates.add(p + "/");
|
||||
}
|
||||
|
||||
for (const key of candidates) {
|
||||
if (map[key]?.length) return map[key];
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
const t = computed(() => {
|
||||
const l = (lang.value || "en").toLowerCase();
|
||||
const dict = {
|
||||
en: { contributors: "Contributors", commits: "commits" },
|
||||
zh: { contributors: "贡献者", commits: "次提交" },
|
||||
ru: { contributors: "Участники", commits: "коммитов" },
|
||||
} as const;
|
||||
return (dict as any)[l] || (dict as any)[l.split("-")[0]] || dict.en;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<details open v-if="list.length" class="vp-contributors">
|
||||
<summary class="summary">
|
||||
{{ t.contributors }}
|
||||
<span class="count">({{ list.length }})</span>
|
||||
</summary>
|
||||
|
||||
<ul class="list">
|
||||
<li v-for="c in list" :key="c.email || c.name" class="item">
|
||||
<img class="avatar" :src="c.avatarUrl" :alt="c.name" loading="lazy" />
|
||||
<div class="main">
|
||||
<div class="row">
|
||||
<span class="name">{{ c.name }}</span>
|
||||
<span class="meta">· {{ c.commits }} {{ t.commits }}</span>
|
||||
</div>
|
||||
|
||||
<div class="sub">
|
||||
<a v-if="c.email" class="link" :href="`mailto:${c.email}`">{{
|
||||
c.email
|
||||
}}</a>
|
||||
<span v-if="c.github" class="sep">·</span>
|
||||
<a
|
||||
v-if="c.github"
|
||||
class="link"
|
||||
:href="`https://github.com/${c.github}`"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
@{{ c.github }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</details>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.vp-contributors {
|
||||
margin-top: 28px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid var(--vp-c-divider);
|
||||
font-size: 12px;
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.summary {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
font-weight: 500;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.count {
|
||||
opacity: 0.7;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.list {
|
||||
margin: 10px 0 0;
|
||||
padding-left: 18px;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.meta {
|
||||
opacity: 0.7;
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: flex-start;
|
||||
margin: 6px 0;
|
||||
}
|
||||
.avatar {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 999px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
.main {
|
||||
min-width: 0;
|
||||
}
|
||||
.row {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: baseline;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.sub {
|
||||
opacity: 0.75;
|
||||
font-size: 11px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.link {
|
||||
color: var(--vp-c-text-2);
|
||||
text-decoration: none;
|
||||
}
|
||||
.link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.sep {
|
||||
margin: 0 6px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
</style>
|
||||
@@ -8,6 +8,9 @@ import mediumZoom from "medium-zoom";
|
||||
import { onMounted, watch, nextTick } from "vue";
|
||||
import { useRoute } from "vitepress";
|
||||
|
||||
import { h } from "vue";
|
||||
import PageContributors from "./components/PageContributors.vue";
|
||||
|
||||
export default {
|
||||
extends: DefaultTheme,
|
||||
|
||||
@@ -25,4 +28,10 @@ export default {
|
||||
() => nextTick(() => initZoom())
|
||||
);
|
||||
},
|
||||
|
||||
Layout() {
|
||||
return h(DefaultTheme.Layout, null, {
|
||||
"doc-after": () => h(PageContributors),
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
+4
-4
@@ -6,8 +6,8 @@
|
||||
"vitepress-plugin-mermaid": "^2.0.17"
|
||||
},
|
||||
"scripts": {
|
||||
"docs:dev": "vitepress dev",
|
||||
"docs:build": "vitepress build",
|
||||
"docs:preview": "vitepress preview"
|
||||
"docs:dev": "node scripts/gen-contributors.mjs && vitepress dev",
|
||||
"docs:build": "node scripts/gen-contributors.mjs && vitepress build",
|
||||
"docs:preview": "node scripts/gen-contributors.mjs && vitepress preview"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import { execSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import crypto from "node:crypto";
|
||||
|
||||
const ROOT = process.cwd();
|
||||
const DOCS_DIR = path.resolve(ROOT, "docs");
|
||||
const OUT_DIR = path.resolve(ROOT, ".vitepress/.generated");
|
||||
const OUT_FILE = path.join(OUT_DIR, "contributors.json");
|
||||
|
||||
function walk(dir) {
|
||||
const out = [];
|
||||
for (const name of fs.readdirSync(dir)) {
|
||||
const p = path.join(dir, name);
|
||||
const st = fs.statSync(p);
|
||||
if (st.isDirectory()) out.push(...walk(p));
|
||||
else if (p.endsWith(".md")) out.push(p);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function md5(s) {
|
||||
return crypto.createHash("md5").update(s.trim().toLowerCase()).digest("hex");
|
||||
}
|
||||
|
||||
function parseGithubUsernameFromNoreply(email = "") {
|
||||
// 1) 12345+username@users.noreply.github.com
|
||||
// 2) username@users.noreply.github.com
|
||||
const m1 = email.match(/^[^+]+\+([^@]+)@users\.noreply\.github\.com$/i);
|
||||
if (m1?.[1]) return m1[1];
|
||||
const m2 = email.match(/^([^@]+)@users\.noreply\.github\.com$/i);
|
||||
if (m2?.[1]) return m2[1];
|
||||
return null;
|
||||
}
|
||||
|
||||
function avatarUrlFor(email = "") {
|
||||
const gh = parseGithubUsernameFromNoreply(email);
|
||||
if (gh) {
|
||||
return `https://unavatar.io/github/${encodeURIComponent(gh)}`;
|
||||
}
|
||||
if (email) {
|
||||
return `https://www.gravatar.com/avatar/${md5(email)}?d=identicon&s=64`;
|
||||
}
|
||||
return `https://www.gravatar.com/avatar/?d=identicon&s=64`;
|
||||
}
|
||||
|
||||
function gitContributors(fileAbsPath) {
|
||||
const rel = path.relative(process.cwd(), fileAbsPath).replaceAll("\\", "/");
|
||||
let raw = "";
|
||||
try {
|
||||
raw = execSync(`git log --follow --format="%aN|%aE" -- "${rel}"`, {
|
||||
encoding: "utf8",
|
||||
});
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
|
||||
const map = new Map();
|
||||
raw
|
||||
.split("\n")
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean)
|
||||
.forEach((line) => {
|
||||
const [name, email] = line.split("|");
|
||||
const key = (email || name || "").toLowerCase();
|
||||
if (!map.has(key)) {
|
||||
const github = parseGithubUsernameFromNoreply(email);
|
||||
map.set(key, {
|
||||
name,
|
||||
email,
|
||||
github,
|
||||
avatarUrl: avatarUrlFor(email),
|
||||
commits: 0,
|
||||
});
|
||||
}
|
||||
map.get(key).commits += 1;
|
||||
});
|
||||
|
||||
return [...map.values()].sort((a, b) => b.commits - a.commits);
|
||||
}
|
||||
|
||||
const files = walk(DOCS_DIR);
|
||||
const data = {};
|
||||
|
||||
for (const abs of files) {
|
||||
const relToDocs = path.relative(DOCS_DIR, abs).replaceAll("\\", "/");
|
||||
let route = "/" + relToDocs.replace(/\.md$/, "");
|
||||
route = route.replace(/\/index$/, "/"); // docs/a/index.md -> /a/
|
||||
data[route] = gitContributors(abs);
|
||||
}
|
||||
|
||||
fs.mkdirSync(OUT_DIR, { recursive: true });
|
||||
fs.writeFileSync(OUT_FILE, JSON.stringify(data, null, 2), "utf8");
|
||||
console.log(`Generated contributors for ${files.length} pages -> ${OUT_FILE}`);
|
||||
Reference in New Issue
Block a user