diff --git a/web/src/pages/welcome/contributors-parse.ts b/web/src/pages/welcome/contributors-parse.ts new file mode 100644 index 000000000..6f67761cd --- /dev/null +++ b/web/src/pages/welcome/contributors-parse.ts @@ -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(//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(//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(/]*\bsrc="([^"]+)"/i)?.[1]; + const alt = cell.match(/]*\balt="([^"]*)"/i)?.[1] ?? ""; + const strong = cell.match(/([\s\S]*?)<\/strong>/i)?.[1] ?? ""; + const name = plainText(strong.split(//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(/]*>([\s\S]*?)<\/a>/i)?.[1] ?? ""); + + return [ + { + name, + avatar, + signature: plainText(cell.match(/([\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 | ddCat
项目发起者 · 微信:ddcat0829 | founder@example.com | 把故事搬上银幕 | +| 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, + }, + ]); +});