import { createHash, randomUUID } from "node:crypto";
import fs from "node:fs";
import net from "node:net";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { defaultExportConfig, themeManifestSchema } from "@md-to-pdf/core";
import { unzipSync } from "fflate";
import matter from "gray-matter";
import { buildApp } from "../dist/app.js";
import { createDocxMediaEngine } from "../dist/docx-media-engine.js";
import { createPdfGenerator } from "../dist/pdf-engine.js";
const directory = path.dirname(fileURLToPath(import.meta.url));
const repositoryDirectory = path.resolve(directory, "../../..");
const appVersion = JSON.parse(
fs.readFileSync(path.join(repositoryDirectory, "package.json"), "utf8")
).version;
const themesDirectory = path.join(repositoryDirectory, "themes");
const samplesDirectory = path.join(repositoryDirectory, "samples", "themes");
const webDirectory = path.join(repositoryDirectory, "apps", "web", "dist");
const outputDirectory = path.resolve(
process.env.MD_TO_PDF_R4_OUTPUT_DIR?.trim() ||
path.join(repositoryDirectory, "output", "docx-r4")
);
const pdfDirectory = path.join(outputDirectory, "chromium-pdf");
const docxDirectory = path.join(outputDirectory, "docx");
const standardReportPath = path.join(
repositoryDirectory,
"output",
"docx-theme-matrix",
"theme-matrix-report.json"
);
const fontPackRoot = path.resolve(
process.env.MD_TO_PDF_FONT_PACK_DIR?.trim() ||
path.join(repositoryDirectory, "output", "font-packs", "root")
);
const decoder = new TextDecoder();
function writeArtifactWithBusyFallback(filePath, content) {
try {
fs.writeFileSync(filePath, content);
return filePath;
} catch (error) {
if (error?.code !== "EBUSY") {
throw error;
}
const extension = path.extname(filePath);
const fallbackPath = path.join(
path.dirname(filePath),
`${path.basename(filePath, extension)}-attempt-${randomUUID()}${extension}`
);
fs.writeFileSync(fallbackPath, content);
process.stderr.write(
`[DOCX R4 matrix] 既有产物被 Office 锁定,改写本次尝试文件:${path.basename(fallbackPath)}\n`
);
return fallbackPath;
}
}
const inlineCodeContinuityProbes = [
{
id: "paragraph",
code: "mdtp_ic_p",
text: "段落前mdtp_ic_p段落后"
},
{
id: "list-item",
code: "mdtp_ic_l",
text: "列表前mdtp_ic_l列表后"
},
{
id: "table-cell",
code: "mdtp_ic_c",
text: "单元格前mdtp_ic_c单元格后"
}
];
const inlineCodeContinuityMarkdown = `
## 行内代码连续性门禁
段落前\`mdtp_ic_p\`段落后
- 列表前\`mdtp_ic_l\`列表后
| 门禁 | 内容 | 邻接单元格 |
| --- | --- | --- |
| 行内代码 | 单元格前\`mdtp_ic_c\`单元格后 | 邻甲
邻乙 |
| 相邻隔离 | 旁甲
旁乙 | 附甲 |
| 场景 | 内容 |
| --- | --- |
| 壹式 | 标甲
标乙
标丙
标丁 |
| 贰式 | 大甲
大乙
大丙
大丁 |
| 叁式 | 连甲
连乙 |
| 肆式 |
边界
|
| 伍式 | 转甲\\
转乙 <br> 转丙 |
| 陆式 | 属甲
属乙 |
| 柒式 | 码甲\`
\`码乙 |
表格外保留 外一
外二 原文。
\`\`\`html
围一
围二
\`\`\`
`;
const docxBreakParagraphProbes = [
{
id: "basic",
marker: "标甲",
expectedText: "标甲标乙标丙标丁",
hardBreakCount: 3
},
{
id: "case-insensitive",
marker: "大甲",
expectedText: "大甲大乙大丙大丁",
hardBreakCount: 3
},
{
id: "multiple",
marker: "连甲",
expectedText: "连甲连乙",
hardBreakCount: 2
},
{
id: "boundary",
marker: "边界",
expectedText: "边界",
hardBreakCount: 2
},
{
id: "adjacent-a",
marker: "邻乙",
expectedText: "邻甲邻乙",
hardBreakCount: 1
},
{
id: "adjacent-b",
marker: "旁乙",
expectedText: "旁甲旁乙",
hardBreakCount: 1
},
{
id: "escaped-and-entity",
marker: "转甲",
expectedText: "转甲<br>转乙 <br> 转丙",
hardBreakCount: 0
},
{
id: "attribute",
marker: "属甲",
expectedText: "属甲<br class=\"unsafe\">属乙",
hardBreakCount: 0
},
{
id: "inline-code",
marker: "码甲",
expectedText: "码甲<br>码乙",
hardBreakCount: 0,
requiredCharacterStyle: "VerbatimChar"
},
{
id: "outside-table",
marker: "外一",
expectedText: "表格外保留 外一<br>外二 原文。",
hardBreakCount: 0
},
{
id: "fenced-code",
marker: "围一",
expectedText: "围一<br>围二",
hardBreakCount: 0,
requiredParagraphStyle: "SourceCode"
}
];
const contentTypes = new Map([
[".css", "text/css; charset=utf-8"],
[".html", "text/html; charset=utf-8"],
[".js", "text/javascript; charset=utf-8"],
[".mjs", "text/javascript; charset=utf-8"],
[".png", "image/png"],
[".svg", "image/svg+xml"],
[".ttf", "font/ttf"],
[".woff", "font/woff"],
[".woff2", "font/woff2"]
]);
function assert(condition, message) {
if (!condition) {
throw new Error(message);
}
}
function newestModifiedAt(targetPath) {
const stat = fs.statSync(targetPath);
if (stat.isFile()) {
return stat.mtimeMs;
}
return fs.readdirSync(targetPath, { withFileTypes: true }).reduce(
(newest, entry) =>
Math.max(
newest,
newestModifiedAt(path.join(targetPath, entry.name))
),
stat.mtimeMs
);
}
function assertFreshArtifact(label, sourcePaths, artifactPath) {
assert(fs.existsSync(artifactPath), `${label} 构建产物不存在:${artifactPath}`);
const newestSource = Math.max(...sourcePaths.map(newestModifiedAt));
const artifactModifiedAt = fs.statSync(artifactPath).mtimeMs;
assert(
artifactModifiedAt + 1_500 >= newestSource,
`${label} 构建产物早于源码;请按依赖顺序重新构建后再执行门禁`
);
}
function sha256(content) {
return createHash("sha256").update(content).digest("hex");
}
function readJson(filePath) {
return JSON.parse(fs.readFileSync(filePath, "utf8"));
}
function readExportConfigOverride() {
const source = process.env.MD_TO_PDF_R4_EXPORT_CONFIG_JSON?.trim();
if (!source) {
return undefined;
}
const parsed = JSON.parse(source);
assert(
parsed && typeof parsed === "object" && !Array.isArray(parsed),
"R4 导出配置覆盖必须是 JSON 对象"
);
return parsed;
}
function mergeExportConfig(base, override) {
if (!override) {
return base;
}
return {
...base,
...override,
mermaid: { ...base.mermaid, ...override.mermaid },
paper: {
...base.paper,
...override.paper,
margins: { ...base.paper.margins, ...override.paper?.margins }
},
header: {
...base.header,
...override.header,
left: { ...base.header.left, ...override.header?.left },
center: { ...base.header.center, ...override.header?.center },
right: { ...base.header.right, ...override.header?.right }
},
footer: { ...base.footer, ...override.footer },
metadata: { ...base.metadata, ...override.metadata },
print: { ...base.print, ...override.print }
};
}
function readThemes() {
return fs
.readdirSync(themesDirectory, { withFileTypes: true })
.filter(
(entry) =>
entry.isDirectory() &&
fs.existsSync(path.join(themesDirectory, entry.name, "theme.json"))
)
.map((entry) =>
themeManifestSchema.parse(
readJson(path.join(themesDirectory, entry.name, "theme.json"))
)
)
.sort((first, second) => first.id.localeCompare(second.id, "en"));
}
function reservePort() {
return new Promise((resolve, reject) => {
const server = net.createServer();
server.once("error", reject);
server.listen(0, "127.0.0.1", () => {
const address = server.address();
const port =
typeof address === "object" && address ? address.port : undefined;
server.close((error) =>
error || !port
? reject(error ?? new Error("无法分配 R4 验收端口"))
: resolve(port)
);
});
});
}
function resolveWebFile(relativePath) {
const resolved = path.resolve(webDirectory, relativePath);
const relative = path.relative(webDirectory, resolved);
assert(
relative !== ".." &&
!relative.startsWith(`..${path.sep}`) &&
!path.isAbsolute(relative),
`Web 资源路径越界:${relativePath}`
);
return resolved;
}
function registerWebRuntime(app) {
app.get("/preview-frame.html", async (_request, reply) => {
const content = fs.readFileSync(resolveWebFile("preview-frame.html"));
return reply.type("text/html; charset=utf-8").send(content);
});
app.get("/assets/*", async (request, reply) => {
const relativePath = String(request.params["*"] ?? "");
const filePath = resolveWebFile(path.join("assets", relativePath));
const stat = fs.statSync(filePath);
assert(stat.isFile(), `Web 资源不是普通文件:${relativePath}`);
return reply
.type(
contentTypes.get(path.extname(filePath).toLowerCase()) ||
"application/octet-stream"
)
.send(fs.readFileSync(filePath));
});
}
function inspectDocx(content) {
const entries = unzipSync(content);
const documentXml = decoder.decode(entries["word/document.xml"]);
const stylesXml = decoder.decode(entries["word/styles.xml"]);
const fontTableXml = decoder.decode(entries["word/fontTable.xml"]);
const fontParts = Object.keys(entries).filter((name) =>
name.startsWith("word/fonts/")
);
const fontNames = [
...fontTableXml.matchAll(/ match[1]);
const embeddedFontNames = [
...fontTableXml.matchAll(//gu)
].flatMap((match) => /)[\s\S]*?<\/w:p>/gu)
].map((match) => match[0]);
const authorParagraphCount = paragraphs.filter((paragraph) =>
//u.test(paragraph)
).length;
const officialIssueRows = paragraphs.filter((paragraph) =>
//u.test(
paragraph
)
);
const officialIssueRowsWithRightTab = officialIssueRows.filter(
(paragraph) =>
[...paragraph.matchAll(/]*)?\s*\/>/gu)].some(
(match) =>
/\bw:val="right"/u.test(match[0]) &&
/\bw:pos="\d+"/u.test(match[0])
)
);
const officialIssueRowContentTabCount = officialIssueRows.reduce(
(count, paragraph) =>
count + (paragraph.match(//gu)?.length ?? 0),
0
);
const inlineCodeContinuity = inlineCodeContinuityProbes.map((probe) => {
const matchingParagraphs = paragraphs.filter((paragraph) =>
paragraph.includes(probe.code)
);
const paragraph = matchingParagraphs[0] ?? "";
const text = [...paragraph.matchAll(/]*)?>([\s\S]*?)<\/w:t>/gu)]
.map((match) => match[1])
.join("");
const codeRunPattern = new RegExp(
`)[\\s\\S]*?[\\s\\S]*?]*)?>${probe.code}<\\/w:t>[\\s\\S]*?<\\/w:r>`,
"u"
);
const hardBreakCount =
paragraph.match(/]*)?\s*\/>/gu)?.length ?? 0;
return {
id: probe.id,
code: probe.code,
expectedText: probe.text,
actualText: text,
paragraphCount: matchingParagraphs.length,
hardBreakCount,
characterStylePresent: codeRunPattern.test(paragraph),
passed:
matchingParagraphs.length === 1 &&
text === probe.text &&
hardBreakCount === 0 &&
codeRunPattern.test(paragraph)
};
});
const breakProbes = docxBreakParagraphProbes.map((probe) => {
const matchingParagraphs = paragraphs.map((paragraph) => ({
paragraph,
actualText: [
...paragraph.matchAll(/]*)?>([\s\S]*?)<\/w:t>/gu)
].map((match) => match[1]).join("")
})).filter(({ actualText }) => actualText === probe.expectedText);
const paragraph = matchingParagraphs[0]?.paragraph ?? "";
const actualText = matchingParagraphs[0]?.actualText ?? "";
const hardBreakCount =
paragraph.match(/]*)?\s*\/>/gu)?.length ?? 0;
const runs = paragraph.match(/]*)?>[\s\S]*?<\/w:r>/gu) ?? [];
const characterStylePresent = !probe.requiredCharacterStyle ||
runs.some((run) =>
run.includes(`w:rStyle w:val="${probe.requiredCharacterStyle}"`) &&
run.includes("<br>")
);
const paragraphStylePresent = !probe.requiredParagraphStyle ||
new RegExp(
``,
"u"
).test(paragraph);
return {
id: probe.id,
marker: probe.marker,
expectedText: probe.expectedText,
actualText,
expectedHardBreakCount: probe.hardBreakCount,
hardBreakCount,
paragraphCount: matchingParagraphs.length,
characterStylePresent,
paragraphStylePresent,
passed:
matchingParagraphs.length === 1 &&
actualText === probe.expectedText &&
hardBreakCount === probe.hardBreakCount &&
characterStylePresent &&
paragraphStylePresent
};
});
const adjacentA = breakProbes.find((probe) => probe.id === "adjacent-a");
const adjacentB = breakProbes.find((probe) => probe.id === "adjacent-b");
const tableBreakSemantics = {
probes: breakProbes,
adjacentCellsIsolated:
adjacentA?.paragraphCount === 1 &&
adjacentB?.paragraphCount === 1 &&
adjacentA.actualText !== adjacentB.actualText,
passed:
breakProbes.every((probe) => probe.passed) &&
adjacentA?.paragraphCount === 1 &&
adjacentB?.paragraphCount === 1
};
return {
bytes: content.byteLength,
sha256: sha256(content),
partCount: Object.keys(entries).length,
sectionCount: documentXml.match(/)/gu)?.length ?? 0,
embeddedFontPartCount: fontParts.length,
fontNames: [...new Set(fontNames)].sort((first, second) =>
first.localeCompare(second, "en")
),
embeddedFontNames: [...new Set(embeddedFontNames)].sort((first, second) =>
first.localeCompare(second, "en")
),
optionalFontPackApplied: fontTableXml.includes("MdTP Serif SC"),
hasAltChunk: /)/u.test(documentXml),
internalMarkerCount:
documentXml.match(/MD_TO_PDF_(?:CONTAINER|SECTION)_/gu)?.length ?? 0,
authorParagraphCount,
officialIssueRowCount: officialIssueRows.length,
officialIssueRowRightTabCount:
officialIssueRowsWithRightTab.length,
officialIssueRowContentTabCount,
inlineCodeContinuity,
inlineCodeContinuityPassed: inlineCodeContinuity.every(
(probe) => probe.passed
),
tableBreakSemantics,
requiredStylesPresent: [
"Normal",
"Heading1",
"SourceCode",
"Table",
"Caption"
].every((styleId) => stylesXml.includes(`w:styleId="${styleId}"`))
};
}
async function requestArtifact(origin, route, payload, contentType) {
const response = await fetch(`${origin}${route}`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(payload)
});
const content = new Uint8Array(await response.arrayBuffer());
assert(
response.ok,
`${route} 返回 ${response.status}:${decoder.decode(content)}`
);
assert(
response.headers.get("content-type")?.includes(contentType),
`${route} MIME 无效:${response.headers.get("content-type")}`
);
return { response, content };
}
async function requestJson(origin, route, payload) {
const response = await fetch(`${origin}${route}`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(payload)
});
const text = await response.text();
assert(response.ok, `${route} 返回 ${response.status}:${text}`);
assert(
response.headers.get("content-type")?.includes("application/json"),
`${route} MIME 无效:${response.headers.get("content-type")}`
);
return JSON.parse(text);
}
function inspectHtmlBreakSemantics(articleHtml) {
const cellFor = (marker) => {
const cells = articleHtml.match(/]*)?>[\s\S]*?<\/t[dh]>/gu) ?? [];
return cells.find((cell) => cell.includes(marker)) ?? "";
};
const fragmentFor = (start, end) => {
const startIndex = articleHtml.indexOf(start);
const endIndex = articleHtml.indexOf(end, startIndex + start.length);
return startIndex >= 0 && endIndex >= 0
? articleHtml.slice(startIndex, endIndex + end.length)
: "";
};
const probes = [
{
id: "basic",
passed: cellFor("标甲").includes(
"标甲
标乙
标丙
标丁"
)
},
{
id: "case-insensitive",
passed: cellFor("大甲").includes(
"大甲
大乙
大丙
大丁"
)
},
{
id: "multiple",
passed: cellFor("连甲").includes(
"连甲
连乙"
)
},
{
id: "boundary",
passed: cellFor("边界").includes("
边界
")
},
{
id: "escaped-and-entity",
passed: cellFor("转甲").includes(
"转甲<br>转乙 <br> 转丙"
)
},
{
id: "attribute-literal",
passed: cellFor("属甲").includes(
"属甲<br class=\"unsafe\">属乙"
)
},
{
id: "inline-code-literal",
passed: cellFor("码甲").includes(
"码甲<br>码乙"
)
},
{
id: "outside-table-literal",
passed: fragmentFor("外一", "外二").includes(
"外一<br>外二"
)
},
{
id: "fenced-code-literal",
passed: (() => {
const fragment = fragmentFor("围一", "围二");
return fragment.includes("<") && fragment.includes("br") &&
fragment.includes(">") && !/
)/iu.test(fragment);
})()
}
];
const adjacentA = cellFor("邻甲");
const adjacentB = cellFor("旁甲");
const adjacentCellsIsolated =
adjacentA.includes("邻甲
邻乙") &&
adjacentB.includes("旁甲
旁乙") &&
!adjacentA.includes("旁甲") &&
!adjacentB.includes("邻甲");
return {
probes,
adjacentCellsIsolated,
passed: probes.every((probe) => probe.passed) && adjacentCellsIsolated
};
}
function responseDiagnostics(response, kind) {
return {
serverTiming: response.headers.get("server-timing"),
pageCount:
kind === "pdf"
? Number(response.headers.get("x-pdf-page-count"))
: undefined,
warningCount:
kind === "docx"
? Number(response.headers.get("x-docx-warning-count"))
: undefined,
echartsErrorCount: Number(
response.headers.get("x-echarts-error-count")
),
mermaidErrorCount: Number(
response.headers.get("x-mermaid-error-count")
)
};
}
assert(fs.existsSync(standardReportPath), "缺少本轮标准 DOCX 主题矩阵报告");
assertFreshArtifact(
"Preview Engine",
[path.join(repositoryDirectory, "packages", "preview-engine", "src")],
path.join(
repositoryDirectory,
"packages",
"preview-engine",
"dist",
"semantic-cover-fit.js"
)
);
assertFreshArtifact(
"DOCX Engine",
[path.join(repositoryDirectory, "packages", "docx-engine", "src")],
path.join(
repositoryDirectory,
"packages",
"docx-engine",
"dist",
"document-structure-transform.js"
)
);
assertFreshArtifact(
"Web Preview Runtime",
[
path.join(repositoryDirectory, "apps", "web", "src"),
path.join(
repositoryDirectory,
"packages",
"preview-engine",
"dist",
"semantic-cover-fit.js"
)
],
path.join(webDirectory, "preview-frame.html")
);
assert(
fs.statSync(fontPackRoot).isDirectory(),
`缺少有效字体包根目录:${fontPackRoot}`
);
const allThemes = readThemes();
assert(
allThemes.length === 14,
`内置主题数量应为 14,实际为 ${allThemes.length}`
);
const selectedThemeId =
process.env.MD_TO_PDF_R4_THEME_ID?.trim() || undefined;
const externalMarkdownPath =
process.env.MD_TO_PDF_R4_MARKDOWN_PATH?.trim() || undefined;
const appendInlineCodeProbes =
process.env.MD_TO_PDF_R4_APPEND_INLINE_CODE_PROBES?.trim() !== "0";
const exportConfigOverride = readExportConfigOverride();
const themes = selectedThemeId
? allThemes.filter((theme) => theme.id === selectedThemeId)
: allThemes;
assert(
themes.length > 0,
`R4 验收主题不存在:${selectedThemeId}`
);
const standardReport = readJson(standardReportPath);
assert(
standardReport.themeCount === allThemes.length,
"标准 DOCX 主题矩阵与 R4 主题数量不一致"
);
const standardByTheme = new Map(
standardReport.results.map((result) => [result.id, result])
);
fs.mkdirSync(pdfDirectory, { recursive: true });
fs.mkdirSync(docxDirectory, { recursive: true });
const port = await reservePort();
const origin = `http://127.0.0.1:${port}`;
const pdfGenerator = createPdfGenerator({ renderOrigin: origin });
const docxMediaAdapter = createDocxMediaEngine({ renderOrigin: origin });
const captureDocxMedia = docxMediaAdapter.capture.bind(docxMediaAdapter);
let capturedDocxDocumentLayout;
docxMediaAdapter.capture = async (...args) => {
const output = await captureDocxMedia(...args);
capturedDocxDocumentLayout = structuredClone(
output.plan.documentLayout ?? { tables: [] }
);
return output;
};
const app = buildApp({
logger: { level: "error" },
pdfGenerator,
docxMediaAdapter,
fontPacks: {
roots: [fontPackRoot],
appVersion
}
});
registerWebRuntime(app);
const results = [];
try {
await app.listen({ port, host: "127.0.0.1" });
const capabilityResponse = await fetch(`${origin}/api/docx/capability`);
const capability = await capabilityResponse.json();
assert(
capabilityResponse.ok &&
capability.status === "available" &&
capability.detectedVersion === "3.9.0.2",
`DOCX capability 无效:${JSON.stringify(capability)}`
);
for (const theme of themes) {
capturedDocxDocumentLayout = undefined;
process.stderr.write(`[DOCX R4 matrix] generating ${theme.id}\n`);
const samplePath = externalMarkdownPath
? path.resolve(repositoryDirectory, externalMarkdownPath)
: path.join(samplesDirectory, `${theme.id}.md`);
assert(fs.existsSync(samplePath), `主题缺少验收示例:${theme.id}`);
const sourceMarkdown = fs.readFileSync(samplePath, "utf8");
const markdown = `${sourceMarkdown.trimEnd()}${appendInlineCodeProbes ? inlineCodeContinuityMarkdown : ""}`;
const sourceMetadata = matter(sourceMarkdown).data;
const exportConfig = mergeExportConfig({
...defaultExportConfig,
name: `${theme.name} R4 生产链验收`,
themeId: theme.id,
pageDecorationsMode: "theme",
header: structuredClone(
theme.pageDefaults?.header ?? defaultExportConfig.header
),
footer: structuredClone(
theme.pageDefaults?.footer ?? defaultExportConfig.footer
),
paper: {
...defaultExportConfig.paper,
format: "A4",
orientation: "portrait",
marginMode: "theme",
margins: structuredClone(
theme.pageDefaults?.margins ??
defaultExportConfig.paper.margins
)
}
}, exportConfigOverride);
const payload = {
markdown,
fileName: path.basename(samplePath),
language: "zh-CN",
resources: [],
exportConfig
};
const rendered = await requestJson(origin, "/api/render", payload);
const htmlBreakSemantics = appendInlineCodeProbes
? inspectHtmlBreakSemantics(rendered.articleHtml)
: undefined;
if (appendInlineCodeProbes) {
assert(
htmlBreakSemantics.passed,
`主题 ${theme.id} 的 Chromium br 语义门禁失败:${JSON.stringify(htmlBreakSemantics)}`
);
}
const pdf = await requestArtifact(
origin,
"/api/pdf",
payload,
"application/pdf"
);
assert(
decoder.decode(pdf.content.subarray(0, 5)) === "%PDF-",
`主题 ${theme.id} 的 PDF 签名无效`
);
const docx = await requestArtifact(
origin,
"/api/docx",
payload,
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"
);
const inspection = inspectDocx(docx.content);
assert(!inspection.hasAltChunk, `主题 ${theme.id} 包含 altChunk`);
assert(
inspection.internalMarkerCount === 0,
`主题 ${theme.id} 残留内部结构标记`
);
assert(
inspection.requiredStylesPresent,
`主题 ${theme.id} 缺少标准 Word 样式`
);
if (appendInlineCodeProbes) {
assert(
inspection.inlineCodeContinuityPassed,
`主题 ${theme.id} 的行内代码连续性门禁失败:${JSON.stringify(inspection.inlineCodeContinuity)}`
);
assert(
inspection.tableBreakSemantics.passed,
`主题 ${theme.id} 的表格 br 换行门禁失败:${JSON.stringify(inspection.tableBreakSemantics)}`
);
}
const standard = standardByTheme.get(theme.id);
assert(standard, `标准矩阵缺少主题 ${theme.id}`);
assert(
standard.inspection.structureCoverage === 1 &&
standard.inspection.titlePolicyPassed &&
standard.inspection.titleDeduplicationPassed,
`主题 ${theme.id} 的标准结构门禁未通过`
);
if (standard.inspection.profile) {
assert(
inspection.authorParagraphCount === 0,
`结构化主题 ${theme.id} 残留重复 Author 段落`
);
}
const expectsOfficialDualEndedRow = Boolean(
sourceMetadata.document?.profile === "official" &&
sourceMetadata.document.signatory
);
if (expectsOfficialDualEndedRow) {
assert(
inspection.officialIssueRowCount > 0 &&
inspection.officialIssueRowCount ===
inspection.officialIssueRowRightTabCount &&
inspection.officialIssueRowContentTabCount ===
inspection.officialIssueRowCount,
`主题 ${theme.id} 的公文双端行缺少右制表位或签发人分隔`
);
} else {
assert(
inspection.officialIssueRowRightTabCount === 0 &&
inspection.officialIssueRowContentTabCount === 0,
`主题 ${theme.id} 的单端公文行包含无意义制表位`
);
}
const pdfPath = path.join(pdfDirectory, `${theme.id}.pdf`);
const preferredDocxPath = path.join(docxDirectory, `${theme.id}.docx`);
fs.writeFileSync(pdfPath, pdf.content);
const docxPath = writeArtifactWithBusyFallback(
preferredDocxPath,
docx.content
);
results.push({
id: theme.id,
name: theme.name,
category: theme.category,
compatibleProfiles: theme.compatibleProfiles,
sample: path.relative(repositoryDirectory, samplePath),
exportConfig,
htmlBreakSemantics,
pdf: {
outputFile: path.relative(repositoryDirectory, pdfPath),
bytes: pdf.content.byteLength,
sha256: sha256(pdf.content),
diagnostics: responseDiagnostics(pdf.response, "pdf")
},
docx: {
outputFile: path.relative(repositoryDirectory, docxPath),
inspection,
documentLayout: capturedDocxDocumentLayout ?? { tables: [] },
diagnostics: responseDiagnostics(docx.response, "docx")
},
standardInspection: standard.inspection,
expectsOfficialDualEndedRow
});
}
} finally {
await app.close();
}
const enhancedThemes = results.filter(
(result) => result.docx.inspection.optionalFontPackApplied
);
for (const result of results) {
assert(
result.pdf.diagnostics.pageCount > 0,
`主题 ${result.id} 的 PDF 页数无效`
);
assert(
result.pdf.diagnostics.echartsErrorCount === 0 &&
result.pdf.diagnostics.mermaidErrorCount === 0,
`主题 ${result.id} 的 PDF 图表渲染失败`
);
assert(
result.docx.diagnostics.echartsErrorCount === 0 &&
result.docx.diagnostics.mermaidErrorCount === 0,
`主题 ${result.id} 的 DOCX 图表渲染失败`
);
}
const report = {
schemaVersion: 1,
generatedAt: new Date().toISOString(),
origin,
themeCount: themes.length,
totalThemeCount: allThemes.length,
selectedThemeId,
fontPackRoot,
standardReport: path.relative(repositoryDirectory, standardReportPath),
outputDirectory: path.relative(repositoryDirectory, outputDirectory),
enhancedThemeIds: enhancedThemes.map((result) => result.id),
results
};
const reportPath = path.join(outputDirectory, "r4-matrix-report.json");
fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`, "utf8");
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);