feat: 将主题令牌接入 DOCX 动态模板

This commit is contained in:
SkyJourney
2026-07-31 08:43:26 +08:00
parent 459a079f3b
commit 6e40374200
33 changed files with 1478 additions and 56 deletions
@@ -0,0 +1,105 @@
import type {
DocxThemeTokenSet,
DocxThemeStyleCaptureAdapter
} from "@md-to-pdf/docx-theme-engine";
import {
DOCX_STYLE_SLOTS,
DocxThemeStyleSnapshotCache,
createDocxThemeStyleFingerprint,
normalizeDocxThemeMappingConfig,
resolveDocxThemeTokens,
type DocxThemeStyleSnapshot
} from "@md-to-pdf/docx-theme-engine";
import type { PreparedDocxExport } from "./application-service.js";
export interface PreparedDocxThemeTokens {
tokens: DocxThemeTokenSet;
warnings: string[];
}
export interface DocxThemeTokenProvider {
prepare(
prepared: PreparedDocxExport,
signal?: AbortSignal
): Promise<PreparedDocxThemeTokens>;
}
function createExplicitSnapshot(
themeId: string,
themeFingerprint: string
): DocxThemeStyleSnapshot {
return {
schemaVersion: 1,
themeId,
themeFingerprint,
viewport: {
widthPx: 794,
heightPx: 1123,
deviceScaleFactor: 1
},
rootFontSizePx: 16,
slots: DOCX_STYLE_SLOTS.map(({ name }) => ({
slot: name,
matched: false
}))
};
}
export class DocxThemeTokenService
implements DocxThemeTokenProvider
{
private readonly snapshots: DocxThemeStyleSnapshotCache;
constructor(
private readonly adapter: DocxThemeStyleCaptureAdapter
) {
this.snapshots = new DocxThemeStyleSnapshotCache(adapter);
}
async prepare(
prepared: PreparedDocxExport,
signal?: AbortSignal
): Promise<PreparedDocxThemeTokens> {
signal?.throwIfAborted();
const config = normalizeDocxThemeMappingConfig(
prepared.theme.manifest
);
const themeFingerprint =
await createDocxThemeStyleFingerprint(
prepared.theme.manifest.id,
prepared.theme.css
);
signal?.throwIfAborted();
const snapshot =
config.mode === "explicit"
? createExplicitSnapshot(
prepared.theme.manifest.id,
themeFingerprint
)
: await this.snapshots.capture(
{
themeId: prepared.theme.manifest.id,
themeFingerprint,
themeCss: prepared.theme.css,
baseUrl: this.adapter.themeStyleBaseUrl
},
signal
);
const tokens = resolveDocxThemeTokens({
snapshot,
config
});
return {
tokens,
warnings: tokens.diagnostics
.filter(
(diagnostic) => diagnostic.severity !== "info"
)
.map((diagnostic) => diagnostic.message)
};
}
invalidate(themeId?: string) {
this.snapshots.invalidate(themeId);
}
}