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

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, 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);
}
/** `/public/assets/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;
}