import { POST_MEDIA_PUBLIC_PREFIX } from "../constants/media"; const MD_IMG_RE = /!\[[^\]]*]\(([^)]+)\)/g; export type MergePostMediaUrlsOptions = { /** * 允许识别为「本站绝对地址」的 origin 列表(每项可为完整 URL,仅取其 `origin` 参与匹配)。 * 为空时:不将含域名的绝对 URL 计为本站资源(仅 `${POST_MEDIA_PUBLIC_PREFIX}...` 相对路径有效)。 */ allowedAssetOrigins?: readonly string[]; }; function stripQueryHash(path: string): string { return path.split("?")[0]?.split("#")[0] ?? path; } function buildAllowedOriginSet(entries: readonly string[] | undefined): Set { const s = new Set(); if (!entries?.length) { return s; } for (const e of entries) { const t = e.trim(); if (!t) { continue; } try { s.add(new URL(t).origin); } catch { /* skip */ } } return s; } /** 统一为 `${POST_MEDIA_PUBLIC_PREFIX}`,供 `publicAssetUrlToStorageKey` 与引用同步使用。 */ function normalizeUrl(raw: string, allowedOrigins: Set): string { const t = raw.trim().replace(/^<|>$/g, "").split(/\s+/)[0] ?? ""; if (t.startsWith(POST_MEDIA_PUBLIC_PREFIX)) { return stripQueryHash(t); } if (allowedOrigins.size === 0) { return ""; } try { const u = new URL(t); if (!allowedOrigins.has(u.origin)) { return ""; } const path = stripQueryHash(u.pathname); if (path.startsWith(POST_MEDIA_PUBLIC_PREFIX)) { return path; } } catch { /* 非绝对 URL */ } return ""; } /** 从 markdown 中提取本站图片 URL(去重,顺序稳定) */ export function extractMediaUrlsFromMarkdown(markdown: string, options?: MergePostMediaUrlsOptions): string[] { const allowed = buildAllowedOriginSet(options?.allowedAssetOrigins); const out: string[] = []; const seen = new Set(); let m: RegExpExecArray | null; const re = new RegExp(MD_IMG_RE.source, MD_IMG_RE.flags); while ((m = re.exec(markdown)) !== null) { const u = normalizeUrl(m[1] ?? "", allowed); if (u && !seen.has(u)) { seen.add(u); out.push(u); } } return out; } export function extractMediaUrlsFromCover( coverUrl: string | null | undefined, options?: MergePostMediaUrlsOptions, ): string[] { if (!coverUrl) { return []; } const allowed = buildAllowedOriginSet(options?.allowedAssetOrigins); const u = normalizeUrl(coverUrl, allowed); return u ? [u] : []; } export function mergePostMediaUrls( bodyMarkdown: string, coverUrl: string | null | undefined, options?: MergePostMediaUrlsOptions, ): string[] { const a = extractMediaUrlsFromMarkdown(bodyMarkdown, options); const b = extractMediaUrlsFromCover(coverUrl, options); const seen = new Set(a); for (const u of b) { if (!seen.has(u)) { seen.add(u); a.push(u); } } return a; } export function mergeProfileMediaUrls( bioMarkdown: string | null | undefined, avatar: string | null | undefined, options?: MergePostMediaUrlsOptions, ): string[] { return mergePostMediaUrls(bioMarkdown ?? "", avatar ?? null, options); } /** `${POST_MEDIA_PUBLIC_PREFIX}foo.webp` → `foo.webp` */ export function publicAssetUrlToStorageKey(url: string): string | null { const t = url.trim(); if (!t.startsWith(POST_MEDIA_PUBLIC_PREFIX)) { return null; } const key = t.slice(POST_MEDIA_PUBLIC_PREFIX.length).replace(/^\/+/, ""); if (!key || key.includes("..") || key.includes("/") || key.includes("\\")) { return null; } return key; }