diff --git a/server/utils/post-media-urls.test.ts b/server/utils/post-media-urls.test.ts new file mode 100644 index 0000000..5f83ee0 --- /dev/null +++ b/server/utils/post-media-urls.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, test } from "bun:test"; +import { extractMediaUrlsFromMarkdown, publicAssetUrlToStorageKey } from "./post-media-urls"; + +describe("extractMediaUrlsFromMarkdown", () => { + test("accepts site-relative /public/assets/ URL", () => { + expect(extractMediaUrlsFromMarkdown("![](/public/assets/a.webp)")).toEqual(["/public/assets/a.webp"]); + }); + + test("accepts absolute URL with same path and normalizes to relative", () => { + expect( + extractMediaUrlsFromMarkdown("![](https://blog.example.com/public/assets/b.webp)"), + ).toEqual(["/public/assets/b.webp"]); + }); + + test("strips query on relative asset URL", () => { + expect(extractMediaUrlsFromMarkdown("![](/public/assets/c.webp?v=1)")).toEqual(["/public/assets/c.webp"]); + }); + + test("strips query on absolute asset URL via pathname", () => { + expect( + extractMediaUrlsFromMarkdown("![](https://x.example/public/assets/d.webp?cache=1)"), + ).toEqual(["/public/assets/d.webp"]); + }); + + test("ignores non-asset absolute URLs", () => { + expect(extractMediaUrlsFromMarkdown("![](https://evil.com/other.png)")).toEqual([]); + }); +}); + +describe("publicAssetUrlToStorageKey", () => { + test("maps normalized path to storage key", () => { + expect(publicAssetUrlToStorageKey("/public/assets/z.webp")).toBe("z.webp"); + }); +}); diff --git a/server/utils/post-media-urls.ts b/server/utils/post-media-urls.ts index c7a529f..ee667a0 100644 --- a/server/utils/post-media-urls.ts +++ b/server/utils/post-media-urls.ts @@ -1,11 +1,25 @@ -import { POST_MEDIA_PUBLIC_PREFIX } from "#server/constants/media"; +import { POST_MEDIA_PUBLIC_PREFIX } from "../constants/media"; const MD_IMG_RE = /!\[[^\]]*]\(([^)]+)\)/g; +function stripQueryHash(path: string): string { + return path.split("?")[0]?.split("#")[0] ?? path; +} + +/** 统一为 `/public/assets/`,供 `publicAssetUrlToStorageKey` 与引用同步使用。 */ function normalizeUrl(raw: string): string { const t = raw.trim().replace(/^<|>$/g, "").split(/\s+/)[0] ?? ""; if (t.startsWith(POST_MEDIA_PUBLIC_PREFIX)) { - return t; + return stripQueryHash(t); + } + try { + const u = new URL(t); + const path = stripQueryHash(u.pathname); + if (path.startsWith(POST_MEDIA_PUBLIC_PREFIX)) { + return path; + } + } catch { + /* 非绝对 URL */ } return ""; }