release: 发布 v0.6.2 DOCX 真实文档修复
新增能力:将 DOCX 发布验收拆分为四套独立 140,支持真实语料冻结、指纹复用、失败与基础设施错误独立统计,并为表格换行、全 JSON 围栏、代码连续性和长文档分页建立通用门禁。 问题修复:冻结 Paged.js 分片前的逻辑表格列轨并传递打印几何,统一 Markdown 表格换行、代码、段落与 OOXML 翻译;改进 PDF 文本流排序、语义块映射、颜色与栅格比较,消除窄字符重叠和跨行范围符号误报。 兼容与部署:版本统一为 0.6.2;正式 Docker 镜像内置固定 Chromium、Pandoc 3.9.0.2 和 Serif/Sans/Mono 字体;Desktop NSIS 与 ZIP 继续直接内置字体,无需系统字体安装。 验证结果:合成基线与长庆严格 280/280,M4N 140/140;健康数据残余误报 6/115(5.22%),均核查为重复表头自动对齐/取样误报且基础设施错误为 0。全项目测试、类型检查、生产构建和 git diff --check 通过;正式 Docker、NSIS、ZIP、离线镜像、部署包、清单及 SHA-256 均已生成并校验。
This commit is contained in:
@@ -22,6 +22,8 @@ const drawing =
|
||||
"http://schemas.openxmlformats.org/drawingml/2006/main";
|
||||
const picture =
|
||||
"http://schemas.openxmlformats.org/drawingml/2006/picture";
|
||||
const math =
|
||||
"http://schemas.openxmlformats.org/officeDocument/2006/math";
|
||||
|
||||
function generatedFixture() {
|
||||
const baseline = readReferenceDocxPackage(
|
||||
@@ -38,6 +40,14 @@ function generatedFixture() {
|
||||
`<w:p><w:r><w:t>CONTAINER_END</w:t></w:r></w:p>` +
|
||||
`<w:p><w:r><w:t>SECTION_BREAK</w:t></w:r></w:p>` +
|
||||
`<w:p><w:pPr><w:pStyle w:val="Heading1"/></w:pPr><w:r><w:t>正文标题</w:t></w:r></w:p>` +
|
||||
`<w:p><w:pPr><w:pStyle w:val="SourceCode"/></w:pPr>` +
|
||||
`<w:r><w:rPr><w:rStyle w:val="DataTypeTok"/></w:rPr><w:t><</w:t></w:r>` +
|
||||
`<w:r><w:rPr><w:rStyle w:val="KeywordTok"/></w:rPr><w:t>br</w:t></w:r>` +
|
||||
`<w:r><w:rPr><w:rStyle w:val="DataTypeTok"/></w:rPr><w:t>></w:t></w:r></w:p>` +
|
||||
`<w:p><w:pPr><w:pStyle w:val="SourceCode"/></w:pPr>` +
|
||||
`<w:r><w:rPr><w:rStyle w:val="NormalTok"/></w:rPr><w:t xml:space="preserve"> method</w:t></w:r>` +
|
||||
`<w:r><w:rPr><w:rStyle w:val="OperatorTok"/></w:rPr><w:t>:</w:t></w:r>` +
|
||||
`<w:r><w:rPr><w:rStyle w:val="StringTok"/></w:rPr><w:t xml:space="preserve"> "POST"</w:t></w:r></w:p>` +
|
||||
`<w:p><w:pPr><w:pStyle w:val="FirstParagraph"/></w:pPr><w:r><w:drawing>` +
|
||||
`<wp:inline xmlns:wp="${wordprocessingDrawing}"><wp:extent cx="100" cy="200"/><wp:docPr id="1" name="Picture" descr="Mermaid 图表 1" title="mdtp-media:docx-media-1"/>` +
|
||||
`<a:graphic xmlns:a="${drawing}"><a:graphicData uri="${picture}"><pic:pic xmlns:pic="${picture}"><pic:nvPicPr><pic:cNvPr id="0" name="image.png"/></pic:nvPicPr><pic:blipFill><a:blip r:embed="rIdImage"/><a:stretch><a:fillRect/></a:stretch></pic:blipFill><pic:spPr><a:xfrm rot="60000" flipH="1"><a:off x="0" y="0"/><a:ext cx="100" cy="200"/></a:xfrm></pic:spPr></pic:pic></a:graphicData></a:graphic>` +
|
||||
@@ -183,6 +193,46 @@ const tokens: DocxThemeTokenSet = {
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
slot: "code-block-text",
|
||||
source: "computed-css",
|
||||
confidence: "exact",
|
||||
style: {
|
||||
fontCandidates: ["Code Face"],
|
||||
fontSizePt: 9,
|
||||
color: "#24292e"
|
||||
}
|
||||
},
|
||||
{
|
||||
slot: "code-token-punctuation",
|
||||
source: "computed-css",
|
||||
confidence: "exact",
|
||||
style: {
|
||||
fontCandidates: ["Code Face"],
|
||||
fontSizePt: 9,
|
||||
color: "#7a7a7a"
|
||||
}
|
||||
},
|
||||
{
|
||||
slot: "code-token-name",
|
||||
source: "computed-css",
|
||||
confidence: "exact",
|
||||
style: {
|
||||
fontCandidates: ["Code Face"],
|
||||
fontSizePt: 9,
|
||||
color: "#22863a"
|
||||
}
|
||||
},
|
||||
{
|
||||
slot: "code-token-attribute",
|
||||
source: "computed-css",
|
||||
confidence: "exact",
|
||||
style: {
|
||||
fontCandidates: ["Code Face"],
|
||||
fontSizePt: 9,
|
||||
color: "#005cc5"
|
||||
}
|
||||
},
|
||||
{
|
||||
slot: "table-header",
|
||||
source: "computed-css",
|
||||
@@ -292,7 +342,12 @@ describe("生成 DOCX 结构收口", () => {
|
||||
documentXml.match(
|
||||
/<w:spacing[^>]*w:before="0"[^>]*w:after="0"[^>]*w:line="360"[^>]*w:lineRule="exact"/gu
|
||||
)
|
||||
).toHaveLength(2);
|
||||
).toHaveLength(1);
|
||||
expect(
|
||||
documentXml.match(
|
||||
/<w:spacing[^>]*w:before="0"[^>]*w:after="0"[^>]*w:line="360"[^>]*w:lineRule="atLeast"/gu
|
||||
)
|
||||
).toHaveLength(1);
|
||||
expect(documentXml.match(/w:fill="17324D"/gu)).toHaveLength(2);
|
||||
expect(documentXml.match(/w:val="FFFFFF"/gu)).toHaveLength(2);
|
||||
expect(
|
||||
@@ -304,6 +359,12 @@ describe("生成 DOCX 结构收口", () => {
|
||||
expect(tableParagraph).toContain('<w:autoSpaceDE w:val="0"/>');
|
||||
expect(tableParagraph).toContain('<w:autoSpaceDN w:val="0"/>');
|
||||
expect(documentXml).toContain("<w:cantSplit/>");
|
||||
expect(documentXml).toMatch(
|
||||
/<w:p><w:pPr><w:pStyle w:val="SourceCode"\/>[\s\S]*?<w:color w:val="7A7A7A"\/>[\s\S]*?<w:t><<\/w:t>[\s\S]*?<w:color w:val="22863A"\/>[\s\S]*?<w:t>br<\/w:t>[\s\S]*?<w:color w:val="7A7A7A"\/>[\s\S]*?<w:t>><\/w:t>[\s\S]*?<\/w:p>/u
|
||||
);
|
||||
expect(documentXml).toMatch(
|
||||
/<w:p><w:pPr><w:pStyle w:val="SourceCode"\/>[\s\S]*?<w:rStyle w:val="NormalTok"\/>[\s\S]*?<w:color w:val="005CC5"\/>[\s\S]*?<w:t xml:space="preserve"> method<\/w:t>[\s\S]*?<\/w:p>/u
|
||||
);
|
||||
expect(documentXml).toContain(
|
||||
'<w:tab w:val="right" w:pos="9806"/>'
|
||||
);
|
||||
@@ -389,6 +450,98 @@ describe("生成 DOCX 结构收口", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("含行内代码的实测行距允许 Word 扩展行盒", () => {
|
||||
const fixture = readGeneratedDocxPackage(generatedFixture());
|
||||
const documentXml = decoder.decode(
|
||||
fixture.entries.get("word/document.xml")!
|
||||
).replace(
|
||||
"<w:sectPr>",
|
||||
'<w:p><w:r><w:t>前缀</w:t></w:r><w:r><w:rPr><w:rStyle w:val="VerbatimChar"/></w:rPr><w:t>inline</w:t></w:r><w:r><w:t>后缀</w:t></w:r></w:p><w:sectPr>'
|
||||
);
|
||||
fixture.entries.set("word/document.xml", encoder.encode(documentXml));
|
||||
|
||||
const result = finalizeGeneratedDocxStructure(
|
||||
writeGeneratedDocxPackage(fixture.entries),
|
||||
plan,
|
||||
tokens,
|
||||
[],
|
||||
{
|
||||
tables: [],
|
||||
textBlocks: [{
|
||||
ordinal: 1,
|
||||
text: "前缀inline后缀",
|
||||
letterSpacingPt: 0,
|
||||
linePitchPt: 11.15,
|
||||
lineBreakOffsets: []
|
||||
}]
|
||||
}
|
||||
);
|
||||
const outputXml = decoder.decode(
|
||||
readGeneratedDocxPackage(result.content).entries.get(
|
||||
"word/document.xml"
|
||||
)!
|
||||
);
|
||||
|
||||
expect(outputXml).toMatch(
|
||||
/w:line="223" w:lineRule="atLeast"[\s\S]*?<w:t>前缀<\/w:t>[\s\S]*?<w:rStyle w:val="VerbatimChar"\/>/u
|
||||
);
|
||||
});
|
||||
|
||||
it("未匹配实测布局的行内代码段落也不保留 exact 行盒", () => {
|
||||
const fixture = readGeneratedDocxPackage(generatedFixture());
|
||||
const documentXml = decoder.decode(
|
||||
fixture.entries.get("word/document.xml")!
|
||||
).replace(
|
||||
"<w:sectPr>",
|
||||
'<w:p><w:pPr><w:spacing w:line="312" w:lineRule="exact"/></w:pPr><w:r><w:t>前缀</w:t></w:r><w:r><w:rPr><w:rStyle w:val="VerbatimChar"/></w:rPr><w:t>inline</w:t></w:r></w:p><w:sectPr>'
|
||||
);
|
||||
fixture.entries.set("word/document.xml", encoder.encode(documentXml));
|
||||
|
||||
const result = finalizeGeneratedDocxStructure(
|
||||
writeGeneratedDocxPackage(fixture.entries),
|
||||
plan,
|
||||
tokens
|
||||
);
|
||||
const outputXml = decoder.decode(
|
||||
readGeneratedDocxPackage(result.content).entries.get(
|
||||
"word/document.xml"
|
||||
)!
|
||||
);
|
||||
|
||||
expect(outputXml).toMatch(
|
||||
/w:line="312" w:lineRule="atLeast"[\s\S]*?<w:rStyle w:val="VerbatimChar"\/>/u
|
||||
);
|
||||
});
|
||||
|
||||
it("表格纯公式段落加入零宽普通 Run 以服从段落对齐", () => {
|
||||
const fixture = readGeneratedDocxPackage(generatedFixture());
|
||||
const documentXml = decoder.decode(
|
||||
fixture.entries.get("word/document.xml")!
|
||||
).replace(
|
||||
`xmlns:r="${relationships}"`,
|
||||
`xmlns:r="${relationships}" xmlns:m="${math}"`
|
||||
).replace(
|
||||
"<w:p><w:r><w:t>B</w:t></w:r></w:p>",
|
||||
'<w:p><w:pPr><w:jc w:val="left"/></w:pPr><m:oMath><m:r><m:t>≥0.80</m:t></m:r></m:oMath></w:p>'
|
||||
);
|
||||
fixture.entries.set("word/document.xml", encoder.encode(documentXml));
|
||||
|
||||
const result = finalizeGeneratedDocxStructure(
|
||||
writeGeneratedDocxPackage(fixture.entries),
|
||||
plan,
|
||||
tokens
|
||||
);
|
||||
const outputXml = decoder.decode(
|
||||
readGeneratedDocxPackage(result.content).entries.get(
|
||||
"word/document.xml"
|
||||
)!
|
||||
);
|
||||
|
||||
expect(outputXml).toMatch(
|
||||
/<w:jc w:val="left"\/>[\s\S]*?<m:oMath>[\s\S]*?<m:t>≥0\.80<\/m:t>[\s\S]*?<\/m:oMath><w:r><w:rPr><w:noProof\/><\/w:rPr><w:t xml:space="preserve">\u200B<\/w:t><\/w:r>/u
|
||||
);
|
||||
});
|
||||
|
||||
it("为右对齐盒模型保留 CSS 右侧内容内缩", () => {
|
||||
const fixture = readGeneratedDocxPackage(generatedFixture());
|
||||
const documentXml = decoder.decode(
|
||||
@@ -508,6 +661,7 @@ describe("生成 DOCX 结构收口", () => {
|
||||
'<w:tblInd w:w="495" w:type="dxa"/>'
|
||||
);
|
||||
expect(documentXml).toContain('<w:tblLayout w:type="fixed"/>');
|
||||
expect(documentXml).toContain('<w:wordWrap w:val="1"/>');
|
||||
expect(documentXml).toContain('<w:gridCol w:w="2675"/>');
|
||||
expect(documentXml).toContain('<w:gridCol w:w="6240"/>');
|
||||
expect(
|
||||
@@ -525,6 +679,112 @@ describe("生成 DOCX 结构收口", () => {
|
||||
expect(documentXml).toContain('<w:jc w:val="distribute"/>');
|
||||
});
|
||||
|
||||
it("将超出内容盒的实测表格等比收缩到 Word 可用宽度", () => {
|
||||
const result = finalizeGeneratedDocxStructure(
|
||||
generatedFixture(),
|
||||
plan,
|
||||
tokens,
|
||||
[],
|
||||
{
|
||||
tables: [
|
||||
{
|
||||
ordinal: 1,
|
||||
widthPercent: 300,
|
||||
leftOffsetPercent: 0,
|
||||
columnWidthPercents: [40, 10, 10, 10, 10, 20],
|
||||
rows: []
|
||||
}
|
||||
]
|
||||
}
|
||||
);
|
||||
const documentXml = decoder.decode(
|
||||
readGeneratedDocxPackage(result.content).entries.get(
|
||||
"word/document.xml"
|
||||
)!
|
||||
);
|
||||
|
||||
expect(documentXml).toContain(
|
||||
'<w:tblW w:w="9906" w:type="dxa"/>'
|
||||
);
|
||||
expect(documentXml).toContain('<w:gridCol w:w="3962"/>');
|
||||
expect(documentXml).toContain('<w:gridCol w:w="1980"/>');
|
||||
});
|
||||
|
||||
it("按 Chromium 实测 emoji 调色保留可编辑 Unicode 文本", () => {
|
||||
const fixture = readGeneratedDocxPackage(generatedFixture());
|
||||
const documentXml = decoder.decode(
|
||||
fixture.entries.get("word/document.xml")!
|
||||
).replace(
|
||||
'<w:p><w:pPr><w:pStyle w:val="Heading1"/></w:pPr><w:r><w:t>正文标题</w:t></w:r></w:p>',
|
||||
'<w:p><w:pPr><w:pStyle w:val="Heading1"/></w:pPr><w:r><w:t>正文标题</w:t></w:r></w:p>' +
|
||||
'<w:p><w:r><w:t>⭐⭐⭐⭐</w:t></w:r></w:p>'
|
||||
);
|
||||
fixture.entries.set("word/document.xml", encoder.encode(documentXml));
|
||||
|
||||
const result = finalizeGeneratedDocxStructure(
|
||||
writeGeneratedDocxPackage(fixture.entries),
|
||||
plan,
|
||||
tokens,
|
||||
[],
|
||||
{
|
||||
tables: [],
|
||||
emojiRuns: [
|
||||
{ ordinal: 1, text: "⭐⭐⭐⭐", color: "#e7bf36" }
|
||||
]
|
||||
}
|
||||
);
|
||||
const finalizedXml = decoder.decode(
|
||||
readGeneratedDocxPackage(result.content).entries.get(
|
||||
"word/document.xml"
|
||||
)!
|
||||
);
|
||||
|
||||
expect(finalizedXml).toMatch(
|
||||
/<w:r><w:rPr><w:color w:val="E7BF36"\/><\/w:rPr><w:t>⭐⭐⭐⭐<\/w:t><\/w:r>/u
|
||||
);
|
||||
});
|
||||
|
||||
it("实测表格缩进补偿 Word 以单元格内容而非外边框对齐的行为", () => {
|
||||
const paddedTokens = {
|
||||
...tokens,
|
||||
slots: tokens.slots.map((slot) =>
|
||||
slot.slot === "table-cell"
|
||||
? {
|
||||
...slot,
|
||||
style: {
|
||||
...slot.style,
|
||||
paddingPt: { top: 3, right: 6, bottom: 3, left: 6 }
|
||||
}
|
||||
}
|
||||
: slot
|
||||
)
|
||||
} satisfies DocxThemeTokenSet;
|
||||
const result = finalizeGeneratedDocxStructure(
|
||||
generatedFixture(),
|
||||
plan,
|
||||
paddedTokens,
|
||||
[],
|
||||
{
|
||||
tables: [{
|
||||
ordinal: 1,
|
||||
widthPercent: 90,
|
||||
leftOffsetPercent: 5,
|
||||
columnWidthPercents: [30, 70],
|
||||
rows: []
|
||||
}]
|
||||
}
|
||||
);
|
||||
const documentXml = decoder.decode(
|
||||
readGeneratedDocxPackage(result.content).entries.get(
|
||||
"word/document.xml"
|
||||
)!
|
||||
);
|
||||
|
||||
expect(documentXml).toContain(
|
||||
'<w:tblInd w:w="615" w:type="dxa"/>'
|
||||
);
|
||||
});
|
||||
|
||||
it("按稳定媒体计划规范化内联尺寸、比例锁和对齐", () => {
|
||||
const result = finalizeGeneratedDocxStructure(
|
||||
generatedFixture(),
|
||||
@@ -882,6 +1142,36 @@ describe("生成 DOCX 结构收口", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("将 Pandoc 拆分的标准 Alert 正文恢复为引用块样式", () => {
|
||||
const source = readGeneratedDocxPackage(generatedFixture());
|
||||
const entries = new Map(source.entries);
|
||||
const documentXml = decoder.decode(entries.get("word/document.xml")!);
|
||||
entries.set(
|
||||
"word/document.xml",
|
||||
encoder.encode(documentXml.replace(
|
||||
"<w:sectPr>",
|
||||
`<w:p><w:pPr><w:pStyle w:val="FirstParagraph"/></w:pPr><w:r><w:t>Caution</w:t></w:r></w:p>` +
|
||||
`<w:p><w:pPr><w:pStyle w:val="BodyText"/></w:pPr><w:r><w:t>警告正文</w:t></w:r></w:p>` +
|
||||
`<w:sectPr>`
|
||||
))
|
||||
);
|
||||
|
||||
const result = finalizeGeneratedDocxStructure(
|
||||
writeGeneratedDocxPackage(entries),
|
||||
plan,
|
||||
tokens
|
||||
);
|
||||
const finalizedXml = decoder.decode(
|
||||
readGeneratedDocxPackage(result.content).entries.get(
|
||||
"word/document.xml"
|
||||
)!
|
||||
);
|
||||
|
||||
expect(finalizedXml).toMatch(
|
||||
/<w:pPr><w:pStyle w:val="BlockText"\/>[\s\S]*?<w:t>警告正文<\/w:t>/u
|
||||
);
|
||||
});
|
||||
|
||||
it("按最终页面内容区重算右置百分比容器缩进", () => {
|
||||
const relativeTokens: DocxThemeTokenSet = {
|
||||
...tokens,
|
||||
|
||||
Reference in New Issue
Block a user