fix: 完善本地与网络图片渲染

This commit is contained in:
SkyJourney
2026-07-27 22:12:16 +08:00
parent 06b30d084e
commit 32a37293d0
31 changed files with 1204 additions and 71 deletions
@@ -80,7 +80,7 @@ describe("共享应用服务", () => {
const roots = await createThemeFixture();
const service = createApplicationService(roots);
const document = service.render({
const document = await service.render({
markdown: "# 文档\n\n<script>alert('xss')</script>",
language: "zh-CN"
});
@@ -94,7 +94,7 @@ describe("共享应用服务", () => {
const roots = await createThemeFixture();
const service = createApplicationService(roots);
expect(() => service.render({ markdown: 42 })).toThrow(
await expect(service.render({ markdown: 42 })).rejects.toEqual(
expect.objectContaining<ApplicationRequestError>({
statusCode: 400,
code: "INVALID_MARKDOWN"
@@ -102,6 +102,39 @@ describe("共享应用服务", () => {
);
});
it("解析上传素材与桌面文档目录中的相对图片", async () => {
const roots = await createThemeFixture();
const service = createApplicationService(roots);
const image = Buffer.from([
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a
]);
const assetRoot = join(temporaryDirectory!, "document");
await mkdir(join(assetRoot, "文档.assets"), { recursive: true });
await writeFile(join(assetRoot, "文档.assets", "本地.png"), image);
const local = await service.render(
{
markdown:
"![本地](./%E6%96%87%E6%A1%A3.assets/%E6%9C%AC%E5%9C%B0.png)"
},
{ localRoot: assetRoot }
);
const uploaded = await service.render({
markdown: "![网页](文档.assets/网页.png)",
resources: [
{
path: "文档.assets/网页.png",
data: image.toString("base64")
}
]
});
expect(local.articleHtml).toContain("data:image/png;base64,");
expect(uploaded.articleHtml).toContain("data:image/png;base64,");
expect(local.articleHtml).toContain("md-document-image");
expect(uploaded.warnings).toEqual([]);
});
it("列出主题并使用调用方提供的资源 URL", async () => {
const roots = await createThemeFixture();
const service = createApplicationService({
@@ -0,0 +1,62 @@
import { describe, expect, it, vi } from "vitest";
import {
createImageResourceResolver,
downloadRemoteImage,
normalizeDocumentAssetPath
} from "../src/index.js";
const png = Buffer.from([
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a
]);
describe("Markdown 图片资源安全", () => {
it("解码合法相对路径并拒绝越界与绝对路径", () => {
expect(
normalizeDocumentAssetPath(
"./%E6%96%87%E6%A1%A3.assets/a%20b.png?raw=1"
)
).toBe("文档.assets/a b.png");
expect(() => normalizeDocumentAssetPath("../secret.png")).toThrow(
"越过文档目录"
);
expect(() => normalizeDocumentAssetPath("C:/secret.png")).toThrow(
"绝对图片路径"
);
});
it("在发起请求前拒绝直接指向本机的远程地址", async () => {
const fetcher = vi.fn<typeof fetch>();
await expect(
downloadRemoteImage("http://127.0.0.1/private.png", fetcher)
).rejects.toThrow("非公网 IP");
expect(fetcher).not.toHaveBeenCalled();
});
it("缓存成功的远程图片并把失败转换为可诊断占位", async () => {
const remoteLoader = vi
.fn()
.mockResolvedValueOnce({
content: png,
contentType: "image/png"
})
.mockRejectedValueOnce(new Error("网络不可用"));
const resolve = createImageResourceResolver({ remoteLoader });
const first = await resolve(
"![a](https://example.com/a.png)\n![a2](https://example.com/a.png)",
undefined
);
const failed = await resolve(
"![b](https://example.com/b.png)",
undefined
);
expect(remoteLoader).toHaveBeenCalledTimes(2);
expect(first.sources.get("https://example.com/a.png")).toMatch(
/^data:image\/png;base64,/u
);
expect(failed.sources.get("https://example.com/b.png")).toMatch(
/^data:image\/svg\+xml;base64,/u
);
expect(failed.warnings[0]).toContain("网络不可用");
});
});