feat: 接入可选字体包同源渲染链

This commit is contained in:
SkyJourney
2026-07-31 21:15:45 +08:00
parent c76454be9d
commit 9f96d51922
17 changed files with 720 additions and 24 deletions
@@ -13,6 +13,7 @@ import {
} from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { createHash } from "node:crypto";
import {
ApplicationRequestError,
createApplicationService
@@ -126,6 +127,64 @@ async function createThemeFixture() {
return { bundledRoot, localRoot };
}
function sha256(content: Uint8Array | string) {
return createHash("sha256").update(content).digest("hex");
}
async function createFontPackFixture(
options: { corruptWebHash?: boolean; minimumAppVersion?: string } = {}
) {
const root = join(temporaryDirectory!, "font-packs");
const directory = join(root, "test-font-pack", "1.0.0");
const web = Buffer.from([10, 11, 12, 13, 14]);
const docx = Buffer.from([20, 21, 22, 23, 24, 25]);
const license = "SIL Open Font License 1.1";
await mkdir(join(directory, "fonts"), { recursive: true });
await writeFile(join(directory, "fonts", "test.woff2"), web);
await writeFile(join(directory, "fonts", "test.ttf"), docx);
await writeFile(join(directory, "OFL.txt"), license, "utf8");
await writeFile(
join(directory, "font-pack.json"),
JSON.stringify({
manifestVersion: 1,
id: "test-font-pack",
version: "1.0.0",
name: "测试字体包",
description: "应用服务字体包测试",
license: "OFL-1.1",
licenseFile: "OFL.txt",
licenseSha256: sha256(license),
licenseBytes: Buffer.byteLength(license),
compatibility: {
minimumAppVersion: options.minimumAppVersion ?? "0.6.0",
maximumAppVersionExclusive: "0.7.0"
},
faces: [
{
id: "test-regular",
targets: ["Test"],
weight: 400,
style: "normal",
web: {
path: "fonts/test.woff2",
format: "woff2",
bytes: web.byteLength,
sha256: options.corruptWebHash ? "0".repeat(64) : sha256(web)
},
docx: {
path: "fonts/test.ttf",
format: "truetype",
bytes: docx.byteLength,
sha256: sha256(docx)
}
}
]
}),
"utf8"
);
return { root, web, docx };
}
describe("共享应用服务", () => {
it("渲染安全 Markdown 文档", async () => {
const roots = await createThemeFixture();
@@ -350,6 +409,98 @@ describe("共享应用服务", () => {
]);
});
it("为主题 CSS 与 DOCX 选择同一字体包的双资源", async () => {
const roots = await createThemeFixture();
const fontPack = await createFontPackFixture();
const service = createApplicationService({
...roots,
fontPacks: {
roots: [fontPack.root],
appVersion: "0.6.0",
createAssetUrl: (packId, version, faceId, kind, hash) =>
`mdpdf://font-pack/${packId}/${version}/${faceId}/${kind}?v=${hash}`
}
});
const css = await service.getThemeCss("test-theme");
expect(css).toContain("md-to-pdf-font-packs:test-font-pack@1.0.0");
expect(css).toContain(
"mdpdf://font-pack/test-font-pack/1.0.0/test-regular/web?v="
);
const prepared = await service.prepareDocxExport({
markdown: "# 字体包",
fileName: "字体包.md",
exportConfig: {
...defaultExportConfig,
themeId: "test-theme"
}
});
expect(prepared.theme.fonts[0]).toEqual(
expect.objectContaining({
family: "Test",
source: "font-pack:test-font-pack/1.0.0/test-regular/docx",
license: "OFL-1.1",
content: new Uint8Array(fontPack.docx)
})
);
expect(prepared.theme.fontPack?.appliedFaces).toEqual([
expect.objectContaining({
family: "Test",
packId: "test-font-pack",
faceId: "test-regular"
})
]);
await expect(
service.getFontPackAsset(
"test-font-pack",
"1.0.0",
"test-regular",
"web"
)
).resolves.toEqual({
contentType: "font/woff2",
content: new Uint8Array(fontPack.web),
sha256: sha256(fontPack.web)
});
});
it("字体包损坏时记录诊断并回退到主题字体", async () => {
const roots = await createThemeFixture();
const fontPack = await createFontPackFixture({ corruptWebHash: true });
const onWarning = vi.fn();
const service = createApplicationService({
...roots,
onWarning,
fontPacks: {
roots: [fontPack.root],
appVersion: "0.6.0"
}
});
const prepared = await service.prepareDocxExport({
markdown: "# 回退",
fileName: "回退.md",
exportConfig: {
...defaultExportConfig,
themeId: "test-theme"
}
});
expect(prepared.theme.css).not.toContain("md-to-pdf-font-packs:");
expect(prepared.theme.fonts[0]).toEqual(
expect.objectContaining({
family: "Test",
source: "fonts/test.woff2",
content: Buffer.from([0, 1, 2, 3])
})
);
expect(prepared.theme.fontPack?.diagnostics[0]?.code).toBe(
"MANIFEST_INVALID"
);
expect(onWarning).toHaveBeenCalledWith(
expect.stringContaining("字体包 MANIFEST_INVALID")
);
});
it("以稳定错误码拒绝非法 DOCX 配置和缺失主题", async () => {
const roots = await createThemeFixture();
const service = createApplicationService(roots);