feat: 完成 DOCX R4 元素级视觉门禁
建立 Chromium、Word 与 WPS 的可编辑内容、段落几何、页眉页脚、封面、媒体和逐页视觉比较链路。 支持封面独立分节、正文页码重启、主题页边距与横向纸张,并将自然分页差异降为诊断项。 修正代码块内边距和折叠表格单元格边框映射,14 套主题生产矩阵与六组布局视觉矩阵通过。
This commit is contained in:
@@ -9,7 +9,9 @@ import {
|
||||
import {
|
||||
PandocDocxConverter,
|
||||
PandocRuntime,
|
||||
inspectDocxAcceptance
|
||||
createDocxMediaAcceptanceExpectation,
|
||||
inspectDocxAcceptance,
|
||||
resolveReferencePageOptions
|
||||
} from "@md-to-pdf/docx-engine";
|
||||
import { DOCX_STYLE_SLOT_NAMES } from "@md-to-pdf/docx-theme-engine";
|
||||
|
||||
@@ -49,6 +51,7 @@ function mediaResource(kind, ordinal, dimensions, altText, caption) {
|
||||
kindOrdinal: 1,
|
||||
altText,
|
||||
...(caption ? { caption } : {}),
|
||||
alignment: "center",
|
||||
displayWidthPx: dimensions.width,
|
||||
displayHeightPx: dimensions.height,
|
||||
captureX: 0,
|
||||
@@ -115,6 +118,58 @@ function createThemeTokens(theme) {
|
||||
|
||||
const technicalTheme = loadTheme("typora-like");
|
||||
const officialTheme = loadTheme("gov-red-standard");
|
||||
const coverTheme = loadTheme("formal-feasibility");
|
||||
const semanticDocument = {
|
||||
schemaVersion: 1,
|
||||
titlePolicy: {
|
||||
metadataTitle: "emit",
|
||||
firstBodyHeading: "keep"
|
||||
},
|
||||
regions: []
|
||||
};
|
||||
const coverSemanticDocument = {
|
||||
schemaVersion: 1,
|
||||
profile: "project-report",
|
||||
titlePolicy: {
|
||||
metadataTitle: "suppress",
|
||||
firstBodyHeading: "keep"
|
||||
},
|
||||
regions: [
|
||||
{
|
||||
kind: "cover",
|
||||
nodes: [
|
||||
{
|
||||
kind: "group",
|
||||
role: "project-report-cover",
|
||||
children: [
|
||||
{
|
||||
kind: "text",
|
||||
role: "project-report-project-name",
|
||||
text: "智慧园区建设项目"
|
||||
},
|
||||
{
|
||||
kind: "text",
|
||||
role: "project-report-title",
|
||||
text: "可行性研究报告"
|
||||
},
|
||||
{
|
||||
kind: "text",
|
||||
role: "project-report-owner",
|
||||
label: "建设单位:",
|
||||
text: "示例建设集团"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
section: {
|
||||
headerFooter: "none",
|
||||
pageNumber: "hidden",
|
||||
breakAfter: "next-page",
|
||||
followingPageNumberStart: 1
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
const commonExpectation = {
|
||||
requiredText: [
|
||||
"可编辑的中文正文",
|
||||
@@ -214,6 +269,18 @@ const landscapeLetterConfig = {
|
||||
startFrom: 3
|
||||
}
|
||||
};
|
||||
const coverA4Config = {
|
||||
...defaultExportConfig,
|
||||
name: "可研封面 A4 纵向验收",
|
||||
themeId: coverTheme.id,
|
||||
pageDecorationsMode: "theme",
|
||||
paper: {
|
||||
...defaultExportConfig.paper,
|
||||
format: "A4",
|
||||
orientation: "portrait",
|
||||
marginMode: "theme"
|
||||
}
|
||||
};
|
||||
|
||||
const variants = [
|
||||
{
|
||||
@@ -275,6 +342,41 @@ const variants = [
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "project-cover-a4-portrait",
|
||||
theme: coverTheme,
|
||||
exportConfig: coverA4Config,
|
||||
semanticDocument: coverSemanticDocument,
|
||||
expectation: {
|
||||
...commonExpectation,
|
||||
id: "project-cover-a4-portrait",
|
||||
requiredText: [
|
||||
...commonExpectation.requiredText,
|
||||
"智慧园区建设项目",
|
||||
"可行性研究报告"
|
||||
],
|
||||
page: {
|
||||
widthTwips: 11906,
|
||||
heightTwips: 16838,
|
||||
orientation: "portrait",
|
||||
marginsTwips: {
|
||||
top: 1417,
|
||||
right: 1134,
|
||||
bottom: 1417,
|
||||
left: 1701
|
||||
}
|
||||
},
|
||||
minimumSections: 2,
|
||||
requireFirstSectionWithoutHeaderFooter: true,
|
||||
finalPageNumberStart: 1,
|
||||
requireSectionPagesField: true,
|
||||
requiredParagraphStyleIds: [
|
||||
"MdProjectReportProjectName",
|
||||
"MdProjectReportTitle",
|
||||
"MdProjectReportOwner"
|
||||
]
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
@@ -299,33 +401,51 @@ fs.mkdirSync(outputDirectory, { recursive: true });
|
||||
|
||||
const results = [];
|
||||
for (const variant of variants) {
|
||||
const variantSemanticDocument =
|
||||
variant.semanticDocument ?? semanticDocument;
|
||||
const fileName = `${variant.id}.md`;
|
||||
const metadata = {
|
||||
title: "v0.6.0 DOCX 自动验收",
|
||||
author: "Markdown PDF 导出器研发组",
|
||||
subject: "Word 与 WPS 可编辑性验收",
|
||||
keywords: ["DOCX", "Pandoc", "OOXML"],
|
||||
language: "zh-CN"
|
||||
};
|
||||
const resolvedPage = resolveReferencePageOptions({
|
||||
exportConfig: variant.exportConfig,
|
||||
theme: variant.theme,
|
||||
fileName,
|
||||
metadata
|
||||
});
|
||||
const media = createMedia();
|
||||
const result = await converter.convert({
|
||||
markdown,
|
||||
fileName: `${variant.id}.md`,
|
||||
fileName,
|
||||
language: "zh-CN",
|
||||
exportConfig: variant.exportConfig,
|
||||
theme: variant.theme,
|
||||
themeTokens: createThemeTokens(variant.theme),
|
||||
metadata: {
|
||||
title: "v0.6.0 DOCX 自动验收",
|
||||
author: "Markdown PDF 导出器研发组",
|
||||
subject: "Word 与 WPS 可编辑性验收",
|
||||
keywords: ["DOCX", "Pandoc", "OOXML"],
|
||||
language: "zh-CN"
|
||||
},
|
||||
semanticDocument: {
|
||||
schemaVersion: 1,
|
||||
titlePolicy: {
|
||||
metadataTitle: "emit",
|
||||
firstBodyHeading: "keep"
|
||||
},
|
||||
regions: []
|
||||
},
|
||||
media: createMedia()
|
||||
fonts: [],
|
||||
metadata,
|
||||
semanticDocument: variantSemanticDocument,
|
||||
media
|
||||
});
|
||||
const report = inspectDocxAcceptance(
|
||||
result.docx,
|
||||
variant.expectation
|
||||
{
|
||||
...variant.expectation,
|
||||
media: createDocxMediaAcceptanceExpectation(
|
||||
media,
|
||||
variant.expectation.page
|
||||
),
|
||||
pageDecorations: {
|
||||
exportConfig: {
|
||||
header: resolvedPage.header,
|
||||
footer: resolvedPage.footer
|
||||
},
|
||||
semanticDocument: variantSemanticDocument
|
||||
}
|
||||
}
|
||||
);
|
||||
const outputPath = path.join(outputDirectory, `${variant.id}.docx`);
|
||||
fs.writeFileSync(outputPath, result.docx);
|
||||
|
||||
@@ -56,6 +56,7 @@ function resource(kind, ordinal, dimensions, caption) {
|
||||
? "Mermaid 流程图"
|
||||
: "ECharts 柱状图",
|
||||
...(caption ? { caption } : {}),
|
||||
alignment: "center",
|
||||
displayWidthPx: dimensions.width,
|
||||
displayHeightPx: dimensions.height,
|
||||
captureX: 0,
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
PandocRuntime,
|
||||
resolveReferencePageOptions
|
||||
} from "@md-to-pdf/docx-engine";
|
||||
import { DOCX_STYLE_SLOT_NAMES } from "@md-to-pdf/docx-theme-engine";
|
||||
import { renderMarkdown } from "@md-to-pdf/renderer";
|
||||
import { unzipSync } from "fflate";
|
||||
|
||||
@@ -185,10 +186,17 @@ function inspectDocument(
|
||||
/<w:sectPr(?:\s[^>]*)?>[\s\S]*?<\/w:sectPr>/gu
|
||||
)
|
||||
].map((match) => match[0]);
|
||||
const fullWidthTableCount = countMatches(
|
||||
documentXml,
|
||||
/<w:tblW\s+w:w="5000"\s+w:type="pct"\s*\/>/gu
|
||||
const tables = [
|
||||
...documentXml.matchAll(
|
||||
/<w:tbl(?:\s|>)[\s\S]*?<\/w:tbl>/gu
|
||||
)
|
||||
].map((match) => match[0]);
|
||||
const contentTables = tables.filter((table) =>
|
||||
/<w:tblStyle\s+w:val="Table"\s*\/>/u.test(table)
|
||||
);
|
||||
const fullWidthTableCount = contentTables.filter((table) =>
|
||||
/<w:tblW\s+w:w="5000"\s+w:type="pct"\s*\/>/u.test(table)
|
||||
).length;
|
||||
const internalMarkerCount = countMatches(
|
||||
documentXml,
|
||||
/MD_TO_PDF_(?:CONTAINER|SECTION)_/gu
|
||||
@@ -216,7 +224,9 @@ function inspectDocument(
|
||||
bytes: content.byteLength,
|
||||
page: pageOptions,
|
||||
paragraphCount: countMatches(documentXml, /<w:p(?:\s|>)/gu),
|
||||
tableCount: countMatches(documentXml, /<w:tbl(?:\s|>)/gu),
|
||||
tableCount: tables.length,
|
||||
contentTableCount: contentTables.length,
|
||||
structuralTableCount: tables.length - contentTables.length,
|
||||
drawingCount: countMatches(documentXml, /<w:drawing(?:\s|>)/gu),
|
||||
explicitPageBreaks,
|
||||
sectionCount: sections.length,
|
||||
@@ -280,6 +290,7 @@ function createMediaResource(kind, ordinal, kindOrdinal) {
|
||||
...(kind === "image"
|
||||
? {}
|
||||
: { caption: `${kind} 主题验收图 ${kindOrdinal}` }),
|
||||
alignment: "center",
|
||||
displayWidthPx: dimensions.width,
|
||||
displayHeightPx: dimensions.height,
|
||||
captureX: 0,
|
||||
@@ -345,8 +356,8 @@ function readThemeTokenReport(themes) {
|
||||
const tokensByTheme = new Map(
|
||||
report.tokenSets.map((tokens) => {
|
||||
assert(
|
||||
tokens.slots?.length === 56,
|
||||
`主题 ${tokens.themeId} 的真实令牌槽位不是 56`
|
||||
tokens.slots?.length === DOCX_STYLE_SLOT_NAMES.length,
|
||||
`主题 ${tokens.themeId} 的真实令牌槽位不是 ${DOCX_STYLE_SLOT_NAMES.length}`
|
||||
);
|
||||
assert(
|
||||
/^[a-f0-9]{64}$/u.test(tokens.themeFingerprint ?? ""),
|
||||
@@ -483,7 +494,7 @@ for (const result of results) {
|
||||
`主题 ${result.id} 存在重复标题`
|
||||
);
|
||||
assert(
|
||||
inspection.fullWidthTableCount === inspection.tableCount,
|
||||
inspection.fullWidthTableCount === inspection.contentTableCount,
|
||||
`主题 ${result.id} 存在非内容区全宽表格`
|
||||
);
|
||||
if (inspection.profile) {
|
||||
|
||||
Reference in New Issue
Block a user