fix(welcome): 欢迎页 - 兼容 README HTML 贡献者表并恢复卡片
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
c5d73037be
commit
d70cc1baf2
3 files changed
+148
-56
No files matched your search
@@ -0,0 +1,90 @@
|
|||||||
|
export type WelcomeContributor = {
|
||||||
|
name: string;
|
||||||
|
avatar: string;
|
||||||
|
signature: string;
|
||||||
|
email?: string;
|
||||||
|
wechat?: string;
|
||||||
|
isFounder?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
function plainText(value: string) {
|
||||||
|
return value
|
||||||
|
.replace(/<br\s*\/?>/gi, " · ")
|
||||||
|
.replace(/<[^>]+>/g, "")
|
||||||
|
.replace(/\[([^\]]+)]\([^)]+\)/g, "$1")
|
||||||
|
.replace(/\s+/g, " ")
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function wechatFrom(details: string) {
|
||||||
|
return details.match(/微信\s*[::]\s*([^·]+)/)?.[1]?.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function parsePipeContributor(line: string, resolveAvatar: (path: string) => string | undefined): WelcomeContributor[] {
|
||||||
|
const columns = line
|
||||||
|
.slice(1, line.lastIndexOf("|"))
|
||||||
|
.split("|")
|
||||||
|
.map((column) => column.trim());
|
||||||
|
const image = columns[0]?.match(/src="([^"]+)"[^>]*alt="([^"]*)"/i);
|
||||||
|
const nameColumn = columns[1] ?? "";
|
||||||
|
const name = plainText(nameColumn.split(/<br\s*\/?>/i)[0] ?? image?.[2] ?? "");
|
||||||
|
const avatarPath = image?.[1];
|
||||||
|
if (!avatarPath || !name) return [];
|
||||||
|
|
||||||
|
const avatar = resolveAvatar(avatarPath);
|
||||||
|
if (!avatar) return [];
|
||||||
|
|
||||||
|
const nameDetails = plainText(nameColumn);
|
||||||
|
const email = plainText(columns[2] ?? "");
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name,
|
||||||
|
avatar,
|
||||||
|
signature: plainText(columns[3] ?? ""),
|
||||||
|
email: email || undefined,
|
||||||
|
wechat: wechatFrom(nameDetails),
|
||||||
|
isFounder: nameDetails.includes("项目发起者"),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseHtmlContributor(cell: string, resolveAvatar: (path: string) => string | undefined): WelcomeContributor[] {
|
||||||
|
const avatarPath = cell.match(/<img\s[^>]*\bsrc="([^"]+)"/i)?.[1];
|
||||||
|
const alt = cell.match(/<img\s[^>]*\balt="([^"]*)"/i)?.[1] ?? "";
|
||||||
|
const strong = cell.match(/<strong>([\s\S]*?)<\/strong>/i)?.[1] ?? "";
|
||||||
|
const name = plainText(strong.split(/<br\s*\/?>/i)[0] ?? "") || plainText(alt);
|
||||||
|
if (!avatarPath || !name) return [];
|
||||||
|
|
||||||
|
const avatar = resolveAvatar(avatarPath);
|
||||||
|
if (!avatar) return [];
|
||||||
|
|
||||||
|
const nameDetails = plainText(strong) || name;
|
||||||
|
const email = decodeURIComponent(cell.match(/mailto:([^"'>\s]+)/i)?.[1] ?? "") || plainText(cell.match(/<a\b[^>]*>([\s\S]*?)<\/a>/i)?.[1] ?? "");
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name,
|
||||||
|
avatar,
|
||||||
|
signature: plainText(cell.match(/<em>([\s\S]*?)<\/em>/i)?.[1] ?? ""),
|
||||||
|
email: email || undefined,
|
||||||
|
wechat: wechatFrom(nameDetails),
|
||||||
|
isFounder: nameDetails.includes("项目发起者"),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseContributors(markdown: string, resolveAvatar: (path: string) => string | undefined): WelcomeContributor[] {
|
||||||
|
const section = markdown.match(/## 贡献者与团队([\s\S]*?)(?=\n##\s|$)/)?.[1];
|
||||||
|
if (!section) return [];
|
||||||
|
|
||||||
|
const htmlCells = section.match(/<td\b[\s\S]*?<\/td>/gi) ?? [];
|
||||||
|
if (htmlCells.length) {
|
||||||
|
return htmlCells.flatMap((cell) => parseHtmlContributor(cell, resolveAvatar));
|
||||||
|
}
|
||||||
|
|
||||||
|
return section
|
||||||
|
.split("\n")
|
||||||
|
.filter((line) => /^\|\s*<img\s/i.test(line))
|
||||||
|
.flatMap((line) => parsePipeContributor(line, resolveAvatar));
|
||||||
|
}
|
||||||
@@ -1,65 +1,13 @@
|
|||||||
import readmeMarkdown from "../../../../README.md?raw";
|
import readmeMarkdown from "../../../../README.md?raw";
|
||||||
|
|
||||||
|
import { parseContributors } from "./contributors-parse";
|
||||||
|
|
||||||
const contributorAvatarModules = import.meta.glob("../../../../assets/user-*", {
|
const contributorAvatarModules = import.meta.glob("../../../../assets/user-*", {
|
||||||
eager: true,
|
eager: true,
|
||||||
import: "default",
|
import: "default",
|
||||||
query: "?url",
|
query: "?url",
|
||||||
}) as Record<string, string>;
|
}) as Record<string, string>;
|
||||||
|
|
||||||
export type WelcomeContributor = {
|
export type { WelcomeContributor } from "./contributors-parse";
|
||||||
name: string;
|
|
||||||
avatar: string;
|
|
||||||
signature: string;
|
|
||||||
email?: string;
|
|
||||||
wechat?: string;
|
|
||||||
isFounder?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
function plainText(value: string) {
|
export const welcomeContributors = parseContributors(readmeMarkdown, (avatarPath) => contributorAvatarModules[`../../../../${avatarPath}`]);
|
||||||
return value
|
|
||||||
.replace(/<br\s*\/?>/gi, " · ")
|
|
||||||
.replace(/<[^>]+>/g, "")
|
|
||||||
.replace(/\[([^\]]+)]\([^)]+\)/g, "$1")
|
|
||||||
.replace(/\s+/g, " ")
|
|
||||||
.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseContributors(markdown: string): WelcomeContributor[] {
|
|
||||||
const section = markdown.match(/## 贡献者与团队([\s\S]*?)(?=\n##\s)/)?.[1];
|
|
||||||
if (!section) return [];
|
|
||||||
|
|
||||||
return section
|
|
||||||
.split("\n")
|
|
||||||
.filter((line) => /^\|\s*<img\s/i.test(line))
|
|
||||||
.flatMap((line) => {
|
|
||||||
const columns = line
|
|
||||||
.slice(1, line.lastIndexOf("|"))
|
|
||||||
.split("|")
|
|
||||||
.map((column) => column.trim());
|
|
||||||
const image = columns[0]?.match(/src="([^"]+)"[^>]*alt="([^"]*)"/i);
|
|
||||||
const nameColumn = columns[1] ?? "";
|
|
||||||
const name = plainText(nameColumn.split(/<br\s*\/?>/i)[0] ?? image?.[2] ?? "");
|
|
||||||
const avatarPath = image?.[1];
|
|
||||||
if (!avatarPath || !name) return [];
|
|
||||||
|
|
||||||
const avatar = contributorAvatarModules[`../../../../${avatarPath}`];
|
|
||||||
if (!avatar) return [];
|
|
||||||
|
|
||||||
const nameDetails = plainText(nameColumn);
|
|
||||||
const email = plainText(columns[2] ?? "");
|
|
||||||
const wechat = nameDetails.match(/微信\s*[::]\s*([^·]+)/)?.[1]?.trim();
|
|
||||||
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
name,
|
|
||||||
avatar,
|
|
||||||
signature: plainText(columns[3] ?? ""),
|
|
||||||
email: email || undefined,
|
|
||||||
wechat,
|
|
||||||
isFounder: nameDetails.includes("项目发起者"),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export const welcomeContributors = parseContributors(readmeMarkdown);
|
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { expect, test } from "bun:test";
|
||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
import { resolve } from "node:path";
|
||||||
|
|
||||||
|
import { parseContributors } from "../src/pages/welcome/contributors-parse";
|
||||||
|
|
||||||
|
const identityAvatar = (path: string) => path;
|
||||||
|
|
||||||
|
test("welcome contributors parse the README HTML table", () => {
|
||||||
|
const readme = readFileSync(resolve(import.meta.dir, "../../README.md"), "utf8");
|
||||||
|
const people = parseContributors(readme, identityAvatar);
|
||||||
|
|
||||||
|
expect(people.length).toBeGreaterThanOrEqual(20);
|
||||||
|
expect(people[0]).toMatchObject({
|
||||||
|
name: "ddCat",
|
||||||
|
avatar: "assets/user-ddcat.jpg",
|
||||||
|
email: "ddcat666@126.com",
|
||||||
|
wechat: "ddcat0829",
|
||||||
|
isFounder: true,
|
||||||
|
});
|
||||||
|
expect(people[0]?.signature).toContain("没有任何黑魔法");
|
||||||
|
expect(people.some((person) => person.name === "爱笑的毛毛虫")).toBe(true);
|
||||||
|
expect(people.filter((person) => person.isFounder)).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("welcome contributors still parse markdown pipe tables", () => {
|
||||||
|
const markdown = `## 贡献者与团队
|
||||||
|
|
||||||
|
| 头像 | 昵称 | 邮箱 | 签名 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| <img src="assets/user-ddcat.jpg" alt="ddCat"> | ddCat<br>项目发起者 · 微信:ddcat0829 | founder@example.com | 把故事搬上银幕 |
|
||||||
|
| <img src="assets/user-ken.jpg" alt="ken"> | ken | ken@example.com | 走自己的路 |
|
||||||
|
|
||||||
|
## 下一章
|
||||||
|
`;
|
||||||
|
expect(parseContributors(markdown, identityAvatar)).toEqual([
|
||||||
|
{
|
||||||
|
name: "ddCat",
|
||||||
|
avatar: "assets/user-ddcat.jpg",
|
||||||
|
signature: "把故事搬上银幕",
|
||||||
|
email: "founder@example.com",
|
||||||
|
wechat: "ddcat0829",
|
||||||
|
isFounder: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ken",
|
||||||
|
avatar: "assets/user-ken.jpg",
|
||||||
|
signature: "走自己的路",
|
||||||
|
email: "ken@example.com",
|
||||||
|
wechat: undefined,
|
||||||
|
isFounder: false,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
Reference in new issue
Block a user