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 }; }