([\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(//gi) ?? [];
+ if (htmlCells.length) {
+ return htmlCells.flatMap((cell) => parseHtmlContributor(cell, resolveAvatar));
+ }
+
+ return section
+ .split("\n")
+ .filter((line) => /^\|\s* parsePipeContributor(line, resolveAvatar));
+}
diff --git a/web/src/pages/welcome/contributors.ts b/web/src/pages/welcome/contributors.ts
index 96c4aa087..7eb037f67 100644
--- a/web/src/pages/welcome/contributors.ts
+++ b/web/src/pages/welcome/contributors.ts
@@ -1,65 +1,13 @@
import readmeMarkdown from "../../../../README.md?raw";
+import { parseContributors } from "./contributors-parse";
+
const contributorAvatarModules = import.meta.glob("../../../../assets/user-*", {
eager: true,
import: "default",
query: "?url",
}) as Record;
-export type WelcomeContributor = {
- name: string;
- avatar: string;
- signature: string;
- email?: string;
- wechat?: string;
- isFounder?: boolean;
-};
+export type { WelcomeContributor } from "./contributors-parse";
-function plainText(value: string) {
- return value
- .replace(/ /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* {
- 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(/ /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);
+export const welcomeContributors = parseContributors(readmeMarkdown, (avatarPath) => contributorAvatarModules[`../../../../${avatarPath}`]);
diff --git a/web/test/welcome-contributors.test.ts b/web/test/welcome-contributors.test.ts
new file mode 100644
index 000000000..ef34a2e6a
--- /dev/null
+++ b/web/test/welcome-contributors.test.ts
@@ -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 = `## 贡献者与团队
+
+| 头像 | 昵称 | 邮箱 | 签名 |
+| --- | --- | --- | --- |
+| | ddCat 项目发起者 · 微信:ddcat0829 | founder@example.com | 把故事搬上银幕 |
+| | 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,
+ },
+ ]);
+});
|