You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
125 lines
3.5 KiB
125 lines
3.5 KiB
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<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;
|
|
}
|
|
|
|
/** 统一为 `${POST_MEDIA_PUBLIC_PREFIX}<key>`,供 `publicAssetUrlToStorageKey` 与引用同步使用。 */
|
|
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;
|
|
}
|
|
} 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<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] ?? "", 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<string>(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;
|
|
}
|
|
|