|
|
|
@ -2,18 +2,51 @@ import { POST_MEDIA_PUBLIC_PREFIX } from "../constants/media"; |
|
|
|
|
|
|
|
const MD_IMG_RE = /!\[[^\]]*]\(([^)]+)\)/g; |
|
|
|
|
|
|
|
export type MergePostMediaUrlsOptions = { |
|
|
|
/** |
|
|
|
* 允许识别为「本站绝对地址」的 origin 列表(每项可为完整 URL,仅取其 `origin` 参与匹配)。 |
|
|
|
* 为空时:不将含域名的绝对 URL 计为本站资源(仅 `/public/assets/...` 相对路径有效)。 |
|
|
|
*/ |
|
|
|
allowedAssetOrigins?: readonly string[]; |
|
|
|
}; |
|
|
|
|
|
|
|
function stripQueryHash(path: string): string { |
|
|
|
return path.split("?")[0]?.split("#")[0] ?? path; |
|
|
|
} |
|
|
|
|
|
|
|
function buildAllowedOriginSet(entries: readonly string[] | undefined): Set<string> { |
|
|
|
const s = new Set<string>(); |
|
|
|
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; |
|
|
|
} |
|
|
|
|
|
|
|
/** 统一为 `/public/assets/<key>`,供 `publicAssetUrlToStorageKey` 与引用同步使用。 */ |
|
|
|
function normalizeUrl(raw: string): string { |
|
|
|
function normalizeUrl(raw: string, allowedOrigins: Set<string>): 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; |
|
|
|
@ -25,13 +58,14 @@ function normalizeUrl(raw: string): string { |
|
|
|
} |
|
|
|
|
|
|
|
/** 从 markdown 中提取本站图片 URL(去重,顺序稳定) */ |
|
|
|
export function extractMediaUrlsFromMarkdown(markdown: string): string[] { |
|
|
|
export function extractMediaUrlsFromMarkdown(markdown: string, options?: MergePostMediaUrlsOptions): string[] { |
|
|
|
const allowed = buildAllowedOriginSet(options?.allowedAssetOrigins); |
|
|
|
const out: string[] = []; |
|
|
|
const seen = new Set<string>(); |
|
|
|
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] ?? ""); |
|
|
|
const u = normalizeUrl(m[1] ?? "", allowed); |
|
|
|
if (u && !seen.has(u)) { |
|
|
|
seen.add(u); |
|
|
|
out.push(u); |
|
|
|
@ -40,17 +74,25 @@ export function extractMediaUrlsFromMarkdown(markdown: string): string[] { |
|
|
|
return out; |
|
|
|
} |
|
|
|
|
|
|
|
export function extractMediaUrlsFromCover(coverUrl: string | null | undefined): string[] { |
|
|
|
export function extractMediaUrlsFromCover( |
|
|
|
coverUrl: string | null | undefined, |
|
|
|
options?: MergePostMediaUrlsOptions, |
|
|
|
): string[] { |
|
|
|
if (!coverUrl) { |
|
|
|
return []; |
|
|
|
} |
|
|
|
const u = normalizeUrl(coverUrl); |
|
|
|
const allowed = buildAllowedOriginSet(options?.allowedAssetOrigins); |
|
|
|
const u = normalizeUrl(coverUrl, allowed); |
|
|
|
return u ? [u] : []; |
|
|
|
} |
|
|
|
|
|
|
|
export function mergePostMediaUrls(bodyMarkdown: string, coverUrl: string | null | undefined): string[] { |
|
|
|
const a = extractMediaUrlsFromMarkdown(bodyMarkdown); |
|
|
|
const b = extractMediaUrlsFromCover(coverUrl); |
|
|
|
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<string>(a); |
|
|
|
for (const u of b) { |
|
|
|
if (!seen.has(u)) { |
|
|
|
@ -61,8 +103,12 @@ export function mergePostMediaUrls(bodyMarkdown: string, coverUrl: string | null |
|
|
|
return a; |
|
|
|
} |
|
|
|
|
|
|
|
export function mergeProfileMediaUrls(bioMarkdown: string | null | undefined, avatar: string | null | undefined): string[] { |
|
|
|
return mergePostMediaUrls(bioMarkdown ?? "", avatar ?? null); |
|
|
|
export function mergeProfileMediaUrls( |
|
|
|
bioMarkdown: string | null | undefined, |
|
|
|
avatar: string | null | undefined, |
|
|
|
options?: MergePostMediaUrlsOptions, |
|
|
|
): string[] { |
|
|
|
return mergePostMediaUrls(bioMarkdown ?? "", avatar ?? null, options); |
|
|
|
} |
|
|
|
|
|
|
|
/** `/public/assets/foo.webp` → `foo.webp` */ |
|
|
|
|