test: 扩展DOCX全主题视觉矩阵
This commit is contained in:
+10
-3
@@ -1492,9 +1492,16 @@ ECharts 第二阶段浏览器与 PDF 验证结果:
|
||||
全项目类型检查和生产构建通过;真实 GitHub 紧凑 Letter 纵向场景观测
|
||||
22 个正文块、输出 102 张失败局部热力图并正确阻断。PDF.js 泛化字体名
|
||||
不作为字体硬门禁,真实字体解析留在阶段 D;
|
||||
- 阶段 C:将语义块门禁扩展为 14 套内置主题 × 纵横两个方向 × 主题默认、
|
||||
标准、紧凑、宽松、非对称装订五组页边距,共 140 个排版场景;主题
|
||||
默认链路即使与显式数值相同也不得去重;
|
||||
- 阶段 C:已完成。矩阵生成器从 14 套内置主题清单生成纵横两个方向 ×
|
||||
主题默认、标准、紧凑、宽松、非对称装订五组页边距,共 140 个唯一
|
||||
排版场景;主题默认链路保持 `marginMode: theme`,即使与显式数值相同
|
||||
也不去重。机器报告记录定义、执行完整度以及主题、方向、边距分层汇总,
|
||||
HTML 报告固定显示 14 行 × 10 列并链接三份引擎详细报告。5 项矩阵结构
|
||||
测试通过;真实 `red-briefing--portrait--theme-default` 验证解析为主题
|
||||
默认 25mm 四边距,14 个正文块中 Word 有 13 个失败、字体部件仍为 0,
|
||||
总计 76 个阻断问题;`formal-feasibility--landscape--binding` 验证 A4
|
||||
横向、非对称装订边距和独立封面整页 strict 门禁,封面因墨迹与边缘
|
||||
IoU 不达标被阻断;全仓库测试、类型检查、生产构建和差异检查通过;
|
||||
- 阶段 D:修复主题封面、块元素样式和字体解析、嵌入、回退;
|
||||
- 阶段 E:完成源码服务、Docker Web API、Desktop 安装版发布后复验;
|
||||
- 阶段 F:完成 `v0.6.1` 版本、发行说明、发布产物、发布提交和标签。
|
||||
|
||||
@@ -116,6 +116,12 @@ flowchart LR
|
||||
Word PDF 和 WPS PDF,并在报告中保留主题、方向、边距场景、导出链和
|
||||
渲染引擎维度,不允许用汇总绿灯掩盖单个失败。
|
||||
|
||||
场景 ID 固定为 `<theme-id>--<orientation>--<margin-scenario>`。总报告同时
|
||||
输出机器可读 JSON 和 14 行 × 10 列 HTML 矩阵;每个单元格必须显示通过、
|
||||
失败或未执行,并链接到 Chromium/Word、Chromium/WPS、Word/WPS 三份
|
||||
详细报告。完整门禁必须执行 140 个场景后才允许标记 `complete: true`;
|
||||
单场景筛选只用于开发复现,不能冒充全矩阵结果。
|
||||
|
||||
## 4. 实施阶段
|
||||
|
||||
1. 阶段 A:修复 failure 聚合策略,补齐四套封面场景和封面正文隔离;
|
||||
|
||||
+3
-2
@@ -34,6 +34,7 @@
|
||||
"desktop:make:with-font-pack": "npm run build:font-pack:windows && npm run build:font-pack:archive && node scripts/build-desktop-with-font-pack.mjs",
|
||||
"release:stage": "node scripts/stage-release.mjs",
|
||||
"test:release-stage": "node --test scripts/stage-release.test.mjs",
|
||||
"test:docx-layout-visual-matrix-cases": "node --test scripts/docx-layout-visual-matrix-cases.test.mjs",
|
||||
"theme:import-typora": "node scripts/import-typora-theme.mjs",
|
||||
"verify:docx-reference": "npm run build -w @md-to-pdf/core && npm run build -w @md-to-pdf/docx-engine && npm run verify:pandoc -w @md-to-pdf/docx-engine",
|
||||
"verify:docx-conversion": "npm run build -w @md-to-pdf/core && npm run build -w @md-to-pdf/docx-engine && npm run verify:conversion -w @md-to-pdf/docx-engine",
|
||||
@@ -45,8 +46,8 @@
|
||||
"verify:docx-r4-matrix": "npm run verify:docx-themes && npm run verify:font-pack && npm run build:web-runtime && npm run build -w @md-to-pdf/server && npm run verify:docx-r4-matrix -w @md-to-pdf/server",
|
||||
"verify:docx-page-decorations": "npm run build:web-runtime && npm run build -w @md-to-pdf/document-visual-diff && npm run build -w @md-to-pdf/server && node scripts/verify-docx-page-decoration-visuals.mjs",
|
||||
"verify:docx-media-visuals": "npm run build:web-runtime && npm run build -w @md-to-pdf/document-visual-diff && npm run build -w @md-to-pdf/server && node scripts/verify-docx-media-visuals.mjs",
|
||||
"verify:docx-layout-visual-matrix": "npm run verify:docx-r4-matrix && npm run build -w @md-to-pdf/document-visual-diff && node scripts/verify-docx-layout-visual-matrix.mjs",
|
||||
"test": "npm run test:release-stage && npm run test -w @md-to-pdf/markdown-echarts && npm run test -w @md-to-pdf/core && npm run build -w @md-to-pdf/core && npm run test -w @md-to-pdf/semantic-document && npm run build -w @md-to-pdf/semantic-document && npm run test -w @md-to-pdf/docx-theme-engine && npm run test -w @md-to-pdf/document-visual-diff && npm run test -w @md-to-pdf/font-pack-registry && npm run build -w @md-to-pdf/font-pack-registry && npm run test -w @md-to-pdf/docx-engine && npm run build -w @md-to-pdf/docx-engine && npm run test -w @md-to-pdf/font-pack-builder && npm run test -w @md-to-pdf/renderer && npm run test -w @md-to-pdf/application && npm run build -w @md-to-pdf/application && npm run test -w @md-to-pdf/preview-engine && npm run build -w @md-to-pdf/preview-engine && npm run test -w @md-to-pdf/web && npm run test -w @md-to-pdf/server && npm run test -w @md-to-pdf/desktop",
|
||||
"verify:docx-layout-visual-matrix": "npm run test:docx-layout-visual-matrix-cases && npm run verify:font-pack && npm run build:web-runtime && npm run build -w @md-to-pdf/document-visual-diff && npm run build -w @md-to-pdf/server && node scripts/verify-docx-layout-visual-matrix.mjs",
|
||||
"test": "npm run test:release-stage && npm run test:docx-layout-visual-matrix-cases && npm run test -w @md-to-pdf/markdown-echarts && npm run test -w @md-to-pdf/core && npm run build -w @md-to-pdf/core && npm run test -w @md-to-pdf/semantic-document && npm run build -w @md-to-pdf/semantic-document && npm run test -w @md-to-pdf/docx-theme-engine && npm run test -w @md-to-pdf/document-visual-diff && npm run test -w @md-to-pdf/font-pack-registry && npm run build -w @md-to-pdf/font-pack-registry && npm run test -w @md-to-pdf/docx-engine && npm run build -w @md-to-pdf/docx-engine && npm run test -w @md-to-pdf/font-pack-builder && npm run test -w @md-to-pdf/renderer && npm run test -w @md-to-pdf/application && npm run build -w @md-to-pdf/application && npm run test -w @md-to-pdf/preview-engine && npm run build -w @md-to-pdf/preview-engine && npm run test -w @md-to-pdf/web && npm run test -w @md-to-pdf/server && npm run test -w @md-to-pdf/desktop",
|
||||
"typecheck": "npm run typecheck -w @md-to-pdf/markdown-echarts && npm run build -w @md-to-pdf/markdown-echarts && npm run typecheck -w @md-to-pdf/core && npm run build -w @md-to-pdf/core && npm run typecheck -w @md-to-pdf/semantic-document && npm run build -w @md-to-pdf/semantic-document && npm run typecheck -w @md-to-pdf/docx-theme-engine && npm run build -w @md-to-pdf/docx-theme-engine && npm run typecheck -w @md-to-pdf/document-visual-diff && npm run typecheck -w @md-to-pdf/font-pack-registry && npm run typecheck -w @md-to-pdf/docx-engine && npm run build -w @md-to-pdf/docx-engine && npm run typecheck -w @md-to-pdf/font-pack-builder && npm run typecheck -w @md-to-pdf/renderer && npm run build -w @md-to-pdf/renderer && npm run typecheck -w @md-to-pdf/application && npm run build -w @md-to-pdf/application && npm run typecheck -w @md-to-pdf/preview-engine && npm run build -w @md-to-pdf/preview-engine && npm run typecheck -w @md-to-pdf/web && npm run typecheck -w @md-to-pdf/server && npm run typecheck -w @md-to-pdf/desktop"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
export const DOCX_LAYOUT_MATRIX_ORIENTATIONS = Object.freeze([
|
||||
"portrait",
|
||||
"landscape"
|
||||
]);
|
||||
|
||||
export const DOCX_LAYOUT_MATRIX_MARGIN_SCENARIOS = Object.freeze([
|
||||
Object.freeze({
|
||||
id: "theme-default",
|
||||
label: "主题默认边距",
|
||||
marginMode: "theme"
|
||||
}),
|
||||
Object.freeze({
|
||||
id: "standard",
|
||||
label: "标准边距",
|
||||
marginMode: "custom",
|
||||
margins: Object.freeze({
|
||||
top: "25.4mm",
|
||||
right: "25.4mm",
|
||||
bottom: "25.4mm",
|
||||
left: "25.4mm"
|
||||
})
|
||||
}),
|
||||
Object.freeze({
|
||||
id: "compact",
|
||||
label: "紧凑边距",
|
||||
marginMode: "custom",
|
||||
margins: Object.freeze({
|
||||
top: "12.7mm",
|
||||
right: "12.7mm",
|
||||
bottom: "12.7mm",
|
||||
left: "12.7mm"
|
||||
})
|
||||
}),
|
||||
Object.freeze({
|
||||
id: "wide",
|
||||
label: "宽松边距",
|
||||
marginMode: "custom",
|
||||
margins: Object.freeze({
|
||||
top: "31.8mm",
|
||||
right: "31.8mm",
|
||||
bottom: "31.8mm",
|
||||
left: "31.8mm"
|
||||
})
|
||||
}),
|
||||
Object.freeze({
|
||||
id: "binding",
|
||||
label: "非对称装订边距",
|
||||
marginMode: "custom",
|
||||
margins: Object.freeze({
|
||||
top: "25.4mm",
|
||||
right: "19.1mm",
|
||||
bottom: "25.4mm",
|
||||
left: "31.8mm"
|
||||
})
|
||||
})
|
||||
]);
|
||||
|
||||
function assert(condition, message) {
|
||||
if (!condition) {
|
||||
throw new Error(message);
|
||||
}
|
||||
}
|
||||
|
||||
function hasIndependentCover(manifest) {
|
||||
const profiles = Array.isArray(manifest.compatibleProfiles)
|
||||
? manifest.compatibleProfiles
|
||||
: [];
|
||||
return profiles.includes("project-report") || profiles.includes("tender");
|
||||
}
|
||||
|
||||
export function readBundledThemeMatrixDefinitions(themesDirectory) {
|
||||
assert(
|
||||
typeof themesDirectory === "string" && themesDirectory.length > 0,
|
||||
"主题目录不能为空"
|
||||
);
|
||||
return fs.readdirSync(themesDirectory, { withFileTypes: true })
|
||||
.filter((entry) => entry.isDirectory())
|
||||
.flatMap((entry) => {
|
||||
const manifestPath = path.join(themesDirectory, entry.name, "theme.json");
|
||||
if (!fs.existsSync(manifestPath)) {
|
||||
return [];
|
||||
}
|
||||
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
||||
if (manifest.bundled !== true) {
|
||||
return [];
|
||||
}
|
||||
assert(
|
||||
typeof manifest.id === "string" && manifest.id.length > 0,
|
||||
`内置主题清单缺少 id:${manifestPath}`
|
||||
);
|
||||
return [{
|
||||
id: manifest.id,
|
||||
name:
|
||||
typeof manifest.name === "string" && manifest.name.length > 0
|
||||
? manifest.name
|
||||
: manifest.id,
|
||||
coverPageCount: hasIndependentCover(manifest) ? 1 : 0,
|
||||
compatibleProfiles: Array.isArray(manifest.compatibleProfiles)
|
||||
? [...manifest.compatibleProfiles]
|
||||
: [],
|
||||
themeDefaultMargins: manifest.pageDefaults?.margins
|
||||
? structuredClone(manifest.pageDefaults.margins)
|
||||
: undefined
|
||||
}];
|
||||
})
|
||||
.sort((left, right) => left.id.localeCompare(right.id, "en"));
|
||||
}
|
||||
|
||||
function createPaperConfig(orientation, marginScenario) {
|
||||
return {
|
||||
format: "A4",
|
||||
orientation,
|
||||
marginMode: marginScenario.marginMode,
|
||||
...(marginScenario.marginMode === "custom"
|
||||
? { margins: structuredClone(marginScenario.margins) }
|
||||
: {})
|
||||
};
|
||||
}
|
||||
|
||||
export function createDocxLayoutVisualMatrixCases(themeDefinitions) {
|
||||
assert(Array.isArray(themeDefinitions), "主题定义必须是数组");
|
||||
const ids = new Set();
|
||||
const cases = [];
|
||||
for (const theme of themeDefinitions) {
|
||||
assert(
|
||||
theme && typeof theme.id === "string" && theme.id.length > 0,
|
||||
"矩阵主题必须包含非空 id"
|
||||
);
|
||||
assert(!ids.has(theme.id), `矩阵主题 ID 重复:${theme.id}`);
|
||||
ids.add(theme.id);
|
||||
for (const orientation of DOCX_LAYOUT_MATRIX_ORIENTATIONS) {
|
||||
for (const marginScenario of DOCX_LAYOUT_MATRIX_MARGIN_SCENARIOS) {
|
||||
const paper = createPaperConfig(orientation, marginScenario);
|
||||
cases.push({
|
||||
id: `${theme.id}--${orientation}--${marginScenario.id}`,
|
||||
themeId: theme.id,
|
||||
themeName: theme.name,
|
||||
orientation,
|
||||
marginScenarioId: marginScenario.id,
|
||||
marginScenarioLabel: marginScenario.label,
|
||||
coverPageCount: theme.coverPageCount,
|
||||
themeDefaultMargins: theme.themeDefaultMargins
|
||||
? structuredClone(theme.themeDefaultMargins)
|
||||
: undefined,
|
||||
paper: { format: "A4", orientation },
|
||||
config: { paper }
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return cases;
|
||||
}
|
||||
|
||||
export function summarizeDocxLayoutVisualMatrix(themeDefinitions, cases) {
|
||||
const themeIds = themeDefinitions.map((theme) => theme.id);
|
||||
const caseIds = cases.map((entry) => entry.id);
|
||||
const expectedCaseCount =
|
||||
themeIds.length *
|
||||
DOCX_LAYOUT_MATRIX_ORIENTATIONS.length *
|
||||
DOCX_LAYOUT_MATRIX_MARGIN_SCENARIOS.length;
|
||||
assert(
|
||||
new Set(caseIds).size === caseIds.length,
|
||||
"DOCX 布局视觉矩阵存在重复场景 ID"
|
||||
);
|
||||
assert(
|
||||
cases.length === expectedCaseCount,
|
||||
`DOCX 布局视觉矩阵场景数错误:${cases.length},期望 ${expectedCaseCount}`
|
||||
);
|
||||
for (const themeId of themeIds) {
|
||||
const themeCaseCount = cases.filter(
|
||||
(entry) => entry.themeId === themeId
|
||||
).length;
|
||||
assert(
|
||||
themeCaseCount ===
|
||||
DOCX_LAYOUT_MATRIX_ORIENTATIONS.length *
|
||||
DOCX_LAYOUT_MATRIX_MARGIN_SCENARIOS.length,
|
||||
`主题 ${themeId} 的矩阵场景数错误:${themeCaseCount}`
|
||||
);
|
||||
}
|
||||
return {
|
||||
themeCount: themeIds.length,
|
||||
themeIds,
|
||||
orientationCount: DOCX_LAYOUT_MATRIX_ORIENTATIONS.length,
|
||||
orientations: [...DOCX_LAYOUT_MATRIX_ORIENTATIONS],
|
||||
marginScenarioCount: DOCX_LAYOUT_MATRIX_MARGIN_SCENARIOS.length,
|
||||
marginScenarios: DOCX_LAYOUT_MATRIX_MARGIN_SCENARIOS.map((scenario) => ({
|
||||
id: scenario.id,
|
||||
label: scenario.label,
|
||||
marginMode: scenario.marginMode,
|
||||
...(scenario.margins
|
||||
? { margins: structuredClone(scenario.margins) }
|
||||
: {})
|
||||
})),
|
||||
expectedCaseCount,
|
||||
coverThemeCount: themeDefinitions.filter(
|
||||
(theme) => theme.coverPageCount === 1
|
||||
).length,
|
||||
coverCaseCount: cases.filter((entry) => entry.coverPageCount === 1).length
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import assert from "node:assert/strict";
|
||||
import path from "node:path";
|
||||
import test from "node:test";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import {
|
||||
createDocxLayoutVisualMatrixCases,
|
||||
DOCX_LAYOUT_MATRIX_MARGIN_SCENARIOS,
|
||||
DOCX_LAYOUT_MATRIX_ORIENTATIONS,
|
||||
readBundledThemeMatrixDefinitions,
|
||||
summarizeDocxLayoutVisualMatrix
|
||||
} from "./docx-layout-visual-matrix-cases.mjs";
|
||||
|
||||
const repositoryDirectory = path.resolve(
|
||||
path.dirname(fileURLToPath(import.meta.url)),
|
||||
".."
|
||||
);
|
||||
const themesDirectory = path.join(repositoryDirectory, "themes");
|
||||
|
||||
function matrix() {
|
||||
const themes = readBundledThemeMatrixDefinitions(themesDirectory);
|
||||
const cases = createDocxLayoutVisualMatrixCases(themes);
|
||||
return { themes, cases, summary: summarizeDocxLayoutVisualMatrix(themes, cases) };
|
||||
}
|
||||
|
||||
test("生成 14×2×5 的完整 DOCX 布局视觉矩阵", () => {
|
||||
const { cases, summary } = matrix();
|
||||
assert.equal(summary.themeCount, 14);
|
||||
assert.equal(summary.orientationCount, 2);
|
||||
assert.equal(summary.marginScenarioCount, 5);
|
||||
assert.equal(summary.expectedCaseCount, 140);
|
||||
assert.equal(cases.length, 140);
|
||||
assert.equal(new Set(cases.map((entry) => entry.id)).size, 140);
|
||||
assert.deepEqual(summary.orientations, ["portrait", "landscape"]);
|
||||
});
|
||||
|
||||
test("每套主题都覆盖两个方向和五组边距", () => {
|
||||
const { themes, cases } = matrix();
|
||||
for (const theme of themes) {
|
||||
const themeCases = cases.filter((entry) => entry.themeId === theme.id);
|
||||
assert.equal(themeCases.length, 10, theme.id);
|
||||
assert.deepEqual(
|
||||
[...new Set(themeCases.map((entry) => entry.orientation))],
|
||||
DOCX_LAYOUT_MATRIX_ORIENTATIONS
|
||||
);
|
||||
assert.deepEqual(
|
||||
[...new Set(themeCases.map((entry) => entry.marginScenarioId))],
|
||||
DOCX_LAYOUT_MATRIX_MARGIN_SCENARIOS.map((scenario) => scenario.id)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("主题默认边距始终保留 theme 模式且不写 custom 数值", () => {
|
||||
const { themes, cases } = matrix();
|
||||
const themeDefaultCases = cases.filter(
|
||||
(entry) => entry.marginScenarioId === "theme-default"
|
||||
);
|
||||
assert.equal(themeDefaultCases.length, 28);
|
||||
for (const entry of themeDefaultCases) {
|
||||
assert.equal(entry.config.paper.marginMode, "theme");
|
||||
assert.equal("margins" in entry.config.paper, false);
|
||||
const theme = themes.find((candidate) => candidate.id === entry.themeId);
|
||||
assert.deepEqual(entry.themeDefaultMargins, theme?.themeDefaultMargins);
|
||||
}
|
||||
});
|
||||
|
||||
test("显式边距场景全部使用 custom 模式并保留稳定数值", () => {
|
||||
const { cases } = matrix();
|
||||
for (const scenario of DOCX_LAYOUT_MATRIX_MARGIN_SCENARIOS.slice(1)) {
|
||||
const scenarioCases = cases.filter(
|
||||
(entry) => entry.marginScenarioId === scenario.id
|
||||
);
|
||||
assert.equal(scenarioCases.length, 28, scenario.id);
|
||||
for (const entry of scenarioCases) {
|
||||
assert.equal(entry.config.paper.marginMode, "custom");
|
||||
assert.deepEqual(entry.config.paper.margins, scenario.margins);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("四套独立封面主题扩展为 40 个严格封面场景", () => {
|
||||
const { cases, summary } = matrix();
|
||||
assert.equal(summary.coverThemeCount, 4);
|
||||
assert.equal(summary.coverCaseCount, 40);
|
||||
assert.deepEqual(
|
||||
[...new Set(
|
||||
cases
|
||||
.filter((entry) => entry.coverPageCount === 1)
|
||||
.map((entry) => entry.themeId)
|
||||
)],
|
||||
[
|
||||
"formal-feasibility",
|
||||
"tender-blind",
|
||||
"tender-business-blue",
|
||||
"tender-classic"
|
||||
]
|
||||
);
|
||||
});
|
||||
@@ -18,6 +18,11 @@ import {
|
||||
formatPageNumber,
|
||||
resolvePageNumberAlignment
|
||||
} from "../packages/preview-engine/dist/index.js";
|
||||
import {
|
||||
createDocxLayoutVisualMatrixCases,
|
||||
readBundledThemeMatrixDefinitions,
|
||||
summarizeDocxLayoutVisualMatrix
|
||||
} from "./docx-layout-visual-matrix-cases.mjs";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
const WORD_NAMESPACE =
|
||||
@@ -39,127 +44,16 @@ const matrixScript = path.join(
|
||||
"verify-docx-r4-matrix.mjs"
|
||||
);
|
||||
const pageSizes = {
|
||||
A4: { portrait: [595.276, 841.89], landscape: [841.89, 595.276] },
|
||||
Letter: { portrait: [612, 792], landscape: [792, 612] }
|
||||
A4: { portrait: [595.276, 841.89], landscape: [841.89, 595.276] }
|
||||
};
|
||||
const cases = [
|
||||
{
|
||||
id: "official-theme-a4-portrait",
|
||||
themeId: "gov-red-standard",
|
||||
coverPageCount: 0,
|
||||
paper: { format: "A4", orientation: "portrait" },
|
||||
config: {}
|
||||
},
|
||||
{
|
||||
id: "official-asymmetric-a4-portrait",
|
||||
themeId: "gov-red-standard",
|
||||
coverPageCount: 0,
|
||||
paper: { format: "A4", orientation: "portrait" },
|
||||
config: {
|
||||
paper: {
|
||||
format: "A4",
|
||||
orientation: "portrait",
|
||||
marginMode: "custom",
|
||||
margins: {
|
||||
top: "18mm",
|
||||
right: "14mm",
|
||||
bottom: "30mm",
|
||||
left: "32mm"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "project-cover-custom-a4-portrait",
|
||||
themeId: "formal-feasibility",
|
||||
coverPageCount: 1,
|
||||
paper: { format: "A4", orientation: "portrait" },
|
||||
config: {
|
||||
paper: {
|
||||
format: "A4",
|
||||
orientation: "portrait",
|
||||
marginMode: "custom",
|
||||
margins: {
|
||||
top: "20mm",
|
||||
right: "18mm",
|
||||
bottom: "25mm",
|
||||
left: "30mm"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "tender-cover-custom-letter-landscape",
|
||||
themeId: "tender-business-blue",
|
||||
coverPageCount: 1,
|
||||
paper: { format: "Letter", orientation: "landscape" },
|
||||
config: {
|
||||
paper: {
|
||||
format: "Letter",
|
||||
orientation: "landscape",
|
||||
marginMode: "custom",
|
||||
margins: {
|
||||
top: "16mm",
|
||||
right: "18mm",
|
||||
bottom: "20mm",
|
||||
left: "24mm"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "tender-blind-cover-a4-portrait",
|
||||
themeId: "tender-blind",
|
||||
coverPageCount: 1,
|
||||
paper: { format: "A4", orientation: "portrait" },
|
||||
config: {}
|
||||
},
|
||||
{
|
||||
id: "tender-classic-cover-a4-portrait",
|
||||
themeId: "tender-classic",
|
||||
coverPageCount: 1,
|
||||
paper: { format: "A4", orientation: "portrait" },
|
||||
config: {}
|
||||
},
|
||||
{
|
||||
id: "formal-custom-a4-landscape",
|
||||
themeId: "formal-regulation",
|
||||
coverPageCount: 0,
|
||||
paper: { format: "A4", orientation: "landscape" },
|
||||
config: {
|
||||
paper: {
|
||||
format: "A4",
|
||||
orientation: "landscape",
|
||||
marginMode: "custom",
|
||||
margins: {
|
||||
top: "15mm",
|
||||
right: "22mm",
|
||||
bottom: "18mm",
|
||||
left: "25mm"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "general-compact-letter-portrait",
|
||||
themeId: "typora-github",
|
||||
coverPageCount: 0,
|
||||
paper: { format: "Letter", orientation: "portrait" },
|
||||
config: {
|
||||
paper: {
|
||||
format: "Letter",
|
||||
orientation: "portrait",
|
||||
marginMode: "custom",
|
||||
margins: {
|
||||
top: "12.7mm",
|
||||
right: "12.7mm",
|
||||
bottom: "12.7mm",
|
||||
left: "12.7mm"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
const themeDefinitions = readBundledThemeMatrixDefinitions(
|
||||
path.join(repositoryDirectory, "themes")
|
||||
);
|
||||
const cases = createDocxLayoutVisualMatrixCases(themeDefinitions);
|
||||
const matrixDefinition = summarizeDocxLayoutVisualMatrix(
|
||||
themeDefinitions,
|
||||
cases
|
||||
);
|
||||
|
||||
const selectedCaseId =
|
||||
process.env.MD_TO_PDF_LAYOUT_VISUAL_CASE?.trim() || undefined;
|
||||
@@ -415,6 +309,83 @@ function reportSummary(report) {
|
||||
};
|
||||
}
|
||||
|
||||
function summarizeDimension(entries, key) {
|
||||
return Object.fromEntries(
|
||||
[...new Set(entries.map((entry) => entry[key]))]
|
||||
.sort((left, right) => left.localeCompare(right, "en"))
|
||||
.map((value) => {
|
||||
const dimensionCases = entries.filter((entry) => entry[key] === value);
|
||||
const failedCases = dimensionCases.filter(
|
||||
(entry) => entry.gateFailures.length > 0
|
||||
);
|
||||
return [value, {
|
||||
caseCount: dimensionCases.length,
|
||||
passedCaseCount: dimensionCases.length - failedCases.length,
|
||||
failedCaseCount: failedCases.length,
|
||||
gatePassed: failedCases.length === 0
|
||||
}];
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function summarizeExecution(entries) {
|
||||
return {
|
||||
byTheme: summarizeDimension(entries, "themeId"),
|
||||
byOrientation: summarizeDimension(entries, "orientation"),
|
||||
byMarginScenario: summarizeDimension(entries, "marginScenarioId")
|
||||
};
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value)
|
||||
.replaceAll("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll('"', """)
|
||||
.replaceAll("'", "'");
|
||||
}
|
||||
|
||||
function renderMatrixSummaryHtml(summary) {
|
||||
const caseByDimension = new Map(
|
||||
summary.cases.map((entry) => [
|
||||
`${entry.themeId}|${entry.orientation}|${entry.marginScenarioId}`,
|
||||
entry
|
||||
])
|
||||
);
|
||||
const columns = summary.matrixDefinition.orientations.flatMap(
|
||||
(orientation) =>
|
||||
summary.matrixDefinition.marginScenarios.map((marginScenario) => ({
|
||||
orientation,
|
||||
marginScenario
|
||||
}))
|
||||
);
|
||||
const header = columns.map(({ orientation, marginScenario }) =>
|
||||
`<th><span>${orientation === "portrait" ? "纵向" : "横向"}</span><small>${escapeHtml(marginScenario.label)}</small></th>`
|
||||
).join("");
|
||||
const rows = summary.matrixDefinition.themeIds.map((themeId) => {
|
||||
const cells = columns.map(({ orientation, marginScenario }) => {
|
||||
const entry = caseByDimension.get(
|
||||
`${themeId}|${orientation}|${marginScenario.id}`
|
||||
);
|
||||
if (!entry) {
|
||||
return '<td class="not-run">未执行</td>';
|
||||
}
|
||||
const failed = entry.gateFailures.length > 0;
|
||||
const base = `./${entry.id}/raster-pages`;
|
||||
return `<td class="${failed ? "failed" : "passed"}"><strong>${failed ? `失败 ${entry.gateFailures.length}` : "通过"}</strong><nav><a href="${base}/word-report.html">Word</a><a href="${base}/wps-report.html">WPS</a><a href="${base}/office-report.html">Office</a></nav></td>`;
|
||||
}).join("");
|
||||
const themeName = summary.cases.find(
|
||||
(entry) => entry.themeId === themeId
|
||||
)?.themeName;
|
||||
return `<tr><th class="theme"><strong>${escapeHtml(themeId)}</strong><small>${escapeHtml(themeName ?? "")}</small></th>${cells}</tr>`;
|
||||
}).join("");
|
||||
return `<!doctype html>
|
||||
<html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>DOCX 视觉门禁矩阵</title>
|
||||
<style>
|
||||
:root{font-family:Inter,"Microsoft YaHei",sans-serif;color:#172033;background:#f3f5f8}body{margin:0;padding:24px}main{max-width:1800px;margin:auto}h1{margin:0 0 8px}.summary{margin:0 0 20px;color:#475467}.table-wrap{overflow:auto;background:#fff;border:1px solid #d0d5dd;border-radius:10px}table{border-collapse:collapse;min-width:1500px;width:100%}th,td{border:1px solid #e4e7ec;padding:9px;text-align:center;vertical-align:top}thead th{position:sticky;top:0;background:#f8fafc;z-index:1}.theme{position:sticky;left:0;background:#fff;text-align:left;min-width:190px;z-index:2}th span,th small,.theme strong,.theme small{display:block}.passed{background:#e6f7ec}.failed{background:#ffe4e4}.not-run{background:#f2f4f7;color:#667085}nav{display:flex;justify-content:center;gap:6px;margin-top:6px}a{font-size:12px;color:#175cd3}small{margin-top:3px;color:#667085;font-weight:400}
|
||||
</style></head><body><main><h1>DOCX 视觉门禁矩阵</h1><p class="summary">定义:${summary.matrixDefinition.themeCount} 套主题 × ${summary.matrixDefinition.orientationCount} 个方向 × ${summary.matrixDefinition.marginScenarioCount} 组页边距 = ${summary.matrixDefinition.expectedCaseCount} 个场景;本次执行 ${summary.execution.executedCaseCount} 个,整体 ${summary.gatePassed ? "通过" : "失败"}。</p><div class="table-wrap"><table><thead><tr><th class="theme">主题</th>${header}</tr></thead><tbody>${rows}</tbody></table></div></main></body></html>`;
|
||||
}
|
||||
|
||||
function writeSnapshotPages(directory, prefix, snapshot) {
|
||||
for (const page of snapshot.pages) {
|
||||
if (page.raster) {
|
||||
@@ -479,6 +450,13 @@ function writeReportArtifacts(directory, prefix, report) {
|
||||
}
|
||||
|
||||
fs.mkdirSync(outputDirectory, { recursive: true });
|
||||
assert(
|
||||
matrixDefinition.themeCount === 14 &&
|
||||
matrixDefinition.orientationCount === 2 &&
|
||||
matrixDefinition.marginScenarioCount === 5 &&
|
||||
matrixDefinition.expectedCaseCount === 140,
|
||||
`布局视觉矩阵定义必须为 14×2×5=140,实际为 ${JSON.stringify(matrixDefinition)}`
|
||||
);
|
||||
assert(
|
||||
selectedCases.length > 0,
|
||||
`视觉矩阵用例不存在:${selectedCaseId}`
|
||||
@@ -604,6 +582,11 @@ for (const caseDefinition of selectedCases) {
|
||||
summaries.push({
|
||||
id: caseDefinition.id,
|
||||
themeId: caseDefinition.themeId,
|
||||
themeName: caseDefinition.themeName,
|
||||
orientation: caseDefinition.orientation,
|
||||
marginScenarioId: caseDefinition.marginScenarioId,
|
||||
marginScenarioLabel: caseDefinition.marginScenarioLabel,
|
||||
themeDefaultMargins: caseDefinition.themeDefaultMargins,
|
||||
paper: caseDefinition.paper,
|
||||
exportConfig: result.exportConfig,
|
||||
coverPageCount: caseDefinition.coverPageCount,
|
||||
@@ -632,7 +615,15 @@ for (const caseDefinition of selectedCases) {
|
||||
const summary = {
|
||||
schemaVersion: 1,
|
||||
generatedAt: new Date().toISOString(),
|
||||
matrixDefinition,
|
||||
selectedCaseId,
|
||||
execution: {
|
||||
mode: selectedCaseId ? "single-case" : "full-matrix",
|
||||
executedCaseCount: summaries.length,
|
||||
expectedFullMatrixCaseCount: matrixDefinition.expectedCaseCount,
|
||||
complete: !selectedCaseId && summaries.length === matrixDefinition.expectedCaseCount
|
||||
},
|
||||
executionSummary: summarizeExecution(summaries),
|
||||
word: wordCapability,
|
||||
wps: wpsCapability,
|
||||
caseCount: summaries.length,
|
||||
@@ -640,8 +631,16 @@ const summary = {
|
||||
gateFailures,
|
||||
cases: summaries
|
||||
};
|
||||
const summaryPath = path.join(outputDirectory, "summary.json");
|
||||
const summaryPath = path.join(
|
||||
outputDirectory,
|
||||
selectedCaseId ? `summary-${selectedCaseId}.json` : "summary.json"
|
||||
);
|
||||
const matrixHtmlPath = path.join(
|
||||
outputDirectory,
|
||||
selectedCaseId ? `matrix-${selectedCaseId}.html` : "matrix.html"
|
||||
);
|
||||
fs.writeFileSync(summaryPath, `${JSON.stringify(summary, null, 2)}\n`, "utf8");
|
||||
fs.writeFileSync(matrixHtmlPath, renderMatrixSummaryHtml(summary), "utf8");
|
||||
process.stdout.write(`${JSON.stringify(summary, null, 2)}\n`);
|
||||
assert(
|
||||
gateFailures.length === 0,
|
||||
|
||||
Reference in New Issue
Block a user