feat: 实现 DOCX 主题令牌归一化

This commit is contained in:
SkyJourney
2026-07-30 22:57:46 +08:00
parent d2acc9c5ce
commit 7f72258126
18 changed files with 1571 additions and 25 deletions
@@ -7,7 +7,9 @@ import {
DOCX_STYLE_SLOT_NAMES,
createDocxThemeStyleCaptureScript,
createDocxThemeStyleFingerprint,
parseDocxThemeStyleRuntimeCapture
normalizeDocxThemeMappingConfig,
parseDocxThemeStyleRuntimeCapture,
resolveDocxThemeTokens
} from "@md-to-pdf/docx-theme-engine";
import { app, BrowserWindow } from "electron";
@@ -113,8 +115,9 @@ try {
.filter((theme) => theme.source === "bundled")
.sort((first, second) =>
first.id.localeCompare(second.id, "en")
);
);
const snapshots = [];
const tokenSets = [];
for (const theme of bundledThemes) {
console.error(`[Electron DOCX theme styles] capturing ${theme.id}`);
const themeCss = await application.getThemeCss(theme.id);
@@ -144,11 +147,67 @@ try {
`主题 ${theme.id} 存在未命中槽位`
);
snapshots.push(snapshot);
tokenSets.push(
resolveDocxThemeTokens({
snapshot,
config: normalizeDocxThemeMappingConfig(theme)
})
);
}
assert(
snapshots.length === 14,
`内置主题数量应为 14,实际为 ${snapshots.length}`
);
const invalidDiagnostics = tokenSets.flatMap((tokens) =>
tokens.diagnostics.filter(
(diagnostic) =>
diagnostic.severity === "error" ||
diagnostic.code === "css-value-invalid"
)
);
assert(
invalidDiagnostics.length === 0,
`DOCX 样式令牌存在 ${invalidDiagnostics.length} 条无效诊断`
);
const playwrightReport = JSON.parse(
fs.readFileSync(
path.join(outputDirectory, "snapshots.json"),
"utf8"
)
);
const playwrightTokensByTheme = new Map(
playwrightReport.tokenSets.map((tokens) => [
tokens.themeId,
tokens
])
);
const crossEngineMismatches = [];
for (const electronTokens of tokenSets) {
const playwrightTokens = playwrightTokensByTheme.get(
electronTokens.themeId
);
assert(
playwrightTokens,
`Playwright 报告缺少主题 ${electronTokens.themeId}`
);
for (const electronSlot of electronTokens.slots) {
const playwrightSlot = playwrightTokens.slots.find(
(slot) => slot.slot === electronSlot.slot
);
if (
JSON.stringify(playwrightSlot) !==
JSON.stringify(electronSlot)
) {
crossEngineMismatches.push(
`${electronTokens.themeId}:${electronSlot.slot}`
);
}
}
}
assert(
crossEngineMismatches.length === 0,
`Playwright/Electron 令牌不一致:${crossEngineMismatches.join("、")}`
);
fs.mkdirSync(outputDirectory, { recursive: true });
fs.writeFileSync(
path.join(outputDirectory, "electron-snapshots.json"),
@@ -159,7 +218,10 @@ try {
chromiumVersion: process.versions.chrome,
themeCount: snapshots.length,
slotCount: DOCX_STYLE_SLOT_NAMES.length,
snapshots
crossEngineTokenMismatches:
crossEngineMismatches.length,
snapshots,
tokenSets
},
null,
2
@@ -173,7 +235,14 @@ try {
themes: snapshots.length,
slotsPerTheme: DOCX_STYLE_SLOT_NAMES.length,
totalSlots:
snapshots.length * DOCX_STYLE_SLOT_NAMES.length
snapshots.length * DOCX_STYLE_SLOT_NAMES.length,
tokenSets: tokenSets.length,
crossEngineTokenMismatches:
crossEngineMismatches.length,
diagnostics: tokenSets.reduce(
(count, tokens) => count + tokens.diagnostics.length,
0
)
})
);
} finally {
@@ -16,6 +16,7 @@ const computed: DocxComputedStyle = {
lineHeight: "normal",
letterSpacing: "normal",
textAlign: "start",
direction: "ltr",
textIndent: "0px",
textDecorationLine: "none",
marginTop: "0px",
@@ -32,10 +33,17 @@ const computed: DocxComputedStyle = {
borderLeft: "0px none rgb(0, 0, 0)",
width: "640px",
maxWidth: "none",
minHeight: "0px",
height: "24px",
breakBefore: "auto",
breakAfter: "auto",
breakInside: "auto",
display: "block"
display: "block",
flexDirection: "row",
alignItems: "normal",
justifyContent: "normal",
outline: "rgb(0, 0, 0) none 0px",
outlineOffset: "0px"
};
const request: DocxThemeStyleCaptureRequest = {
@@ -5,7 +5,9 @@ import { fileURLToPath } from "node:url";
import { createApplicationService } from "@md-to-pdf/application";
import {
DOCX_STYLE_SLOT_NAMES,
createDocxThemeStyleFingerprint
createDocxThemeStyleFingerprint,
normalizeDocxThemeMappingConfig,
resolveDocxThemeTokens
} from "@md-to-pdf/docx-theme-engine";
import { chromium } from "playwright";
import { captureDocxThemeStyleWithPlaywrightPage } from "../dist/playwright-docx-theme-style.js";
@@ -99,8 +101,9 @@ try {
.filter((theme) => theme.source === "bundled")
.sort((first, second) =>
first.id.localeCompare(second.id, "en")
);
);
const snapshots = [];
const tokenSets = [];
for (const theme of bundledThemes) {
console.error(`[DOCX theme styles] capturing ${theme.id}`);
const themeCss = await application.getThemeCss(theme.id);
@@ -126,11 +129,28 @@ try {
.join("、")}`
);
snapshots.push(snapshot);
tokenSets.push(
resolveDocxThemeTokens({
snapshot,
config: normalizeDocxThemeMappingConfig(theme)
})
);
}
assert(
snapshots.length === 14,
`内置主题数量应为 14,实际为 ${snapshots.length}`
);
const invalidDiagnostics = tokenSets.flatMap((tokens) =>
tokens.diagnostics.filter(
(diagnostic) =>
diagnostic.severity === "error" ||
diagnostic.code === "css-value-invalid"
)
);
assert(
invalidDiagnostics.length === 0,
`DOCX 样式令牌存在 ${invalidDiagnostics.length} 条无效诊断`
);
const paragraphStyles = new Set(
snapshots.map(
(snapshot) =>
@@ -151,7 +171,8 @@ try {
chromiumVersion: browser.version(),
themeCount: snapshots.length,
slotCount: DOCX_STYLE_SLOT_NAMES.length,
snapshots
snapshots,
tokenSets
},
null,
2
@@ -164,7 +185,12 @@ try {
themes: snapshots.length,
slotsPerTheme: DOCX_STYLE_SLOT_NAMES.length,
totalSlots:
snapshots.length * DOCX_STYLE_SLOT_NAMES.length
snapshots.length * DOCX_STYLE_SLOT_NAMES.length,
tokenSets: tokenSets.length,
diagnostics: tokenSets.reduce(
(count, tokens) => count + tokens.diagnostics.length,
0
)
})
);
await context.close();
@@ -16,6 +16,7 @@ const computed: DocxComputedStyle = {
lineHeight: "normal",
letterSpacing: "normal",
textAlign: "start",
direction: "ltr",
textIndent: "0px",
textDecorationLine: "none",
marginTop: "0px",
@@ -32,10 +33,17 @@ const computed: DocxComputedStyle = {
borderLeft: "0px none rgb(0, 0, 0)",
width: "640px",
maxWidth: "none",
minHeight: "0px",
height: "24px",
breakBefore: "auto",
breakAfter: "auto",
breakInside: "auto",
display: "block"
display: "block",
flexDirection: "row",
alignItems: "normal",
justifyContent: "normal",
outline: "rgb(0, 0, 0) none 0px",
outlineOffset: "0px"
};
const request: DocxThemeStyleCaptureRequest = {