release: 发布 v0.6.1 DOCX 视觉一致性修复
新增通用 CSS 到 OOXML 翻译修复,统一字体、字距、精确行距、段落、列表、表格、引用、代码块与行内代码连续性,不引入按主题 ID 分支。 新增 MdTP Mono 并统一 Serif、Sans、Mono 三字体包的 Chromium 与 DOCX 使用链;字体声明、嵌入部件和 Word/WPS 实际采用均进入硬门禁。 重建封面整页及正文语义块视觉差分,14 套主题、纵横两个方向、五组页边距共 140 个真实场景全部通过,阻断失败和诊断失败均为零。 源码服务、Docker Web API 与实际安装 Desktop 的 red-briefing 导出均包含 5 个字体部件;Word/WPS 原生渲染和逐页复核通过。修复 Docker 构建上下文与运行层复用软链接,并完善 v0.6.1 版本、发行说明和发布归集。 验证:npm test(116 个文件、616 项测试)、npm run typecheck、npm run build、git diff --check 全部通过。Desktop 安装器与 ZIP、Docker v0.6.1 镜像已生成;Windows 产物仍为未签名内部发行。
This commit is contained in:
@@ -11,6 +11,9 @@ 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");
|
||||
@@ -32,6 +35,37 @@ const fontPackRoot = path.resolve(
|
||||
);
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
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\`单元格后 |
|
||||
`;
|
||||
|
||||
const contentTypes = new Map([
|
||||
[".css", "text/css; charset=utf-8"],
|
||||
[".html", "text/html; charset=utf-8"],
|
||||
@@ -226,6 +260,35 @@ function inspectDocx(content) {
|
||||
count + (paragraph.match(/<w:tab\s*\/>/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(/<w:t(?:\s[^>]*)?>([\s\S]*?)<\/w:t>/gu)]
|
||||
.map((match) => match[1])
|
||||
.join("");
|
||||
const codeRunPattern = new RegExp(
|
||||
`<w:r(?:\\s|>)[\\s\\S]*?<w:rStyle\\s+w:val="VerbatimChar"\\s*\\/>[\\s\\S]*?<w:t(?:\\s[^>]*)?>${probe.code}<\\/w:t>[\\s\\S]*?<\\/w:r>`,
|
||||
"u"
|
||||
);
|
||||
const hardBreakCount =
|
||||
paragraph.match(/<w:br(?:\s[^>]*)?\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)
|
||||
};
|
||||
});
|
||||
return {
|
||||
bytes: content.byteLength,
|
||||
sha256: sha256(content),
|
||||
@@ -247,6 +310,10 @@ function inspectDocx(content) {
|
||||
officialIssueRowRightTabCount:
|
||||
officialIssueRowsWithRightTab.length,
|
||||
officialIssueRowContentTabCount,
|
||||
inlineCodeContinuity,
|
||||
inlineCodeContinuityPassed: inlineCodeContinuity.every(
|
||||
(probe) => probe.passed
|
||||
),
|
||||
requiredStylesPresent: [
|
||||
"Normal",
|
||||
"Heading1",
|
||||
@@ -366,13 +433,22 @@ 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: "0.6.0"
|
||||
appVersion
|
||||
}
|
||||
});
|
||||
registerWebRuntime(app);
|
||||
@@ -390,10 +466,11 @@ try {
|
||||
);
|
||||
|
||||
for (const theme of themes) {
|
||||
capturedDocxDocumentLayout = undefined;
|
||||
process.stderr.write(`[DOCX R4 matrix] generating ${theme.id}\n`);
|
||||
const samplePath = path.join(samplesDirectory, `${theme.id}.md`);
|
||||
assert(fs.existsSync(samplePath), `主题缺少验收示例:${theme.id}`);
|
||||
const markdown = fs.readFileSync(samplePath, "utf8");
|
||||
const markdown = `${fs.readFileSync(samplePath, "utf8").trimEnd()}${inlineCodeContinuityMarkdown}`;
|
||||
const exportConfig = mergeExportConfig({
|
||||
...defaultExportConfig,
|
||||
name: `${theme.name} R4 生产链验收`,
|
||||
@@ -449,6 +526,10 @@ try {
|
||||
inspection.requiredStylesPresent,
|
||||
`主题 ${theme.id} 缺少标准 Word 样式`
|
||||
);
|
||||
assert(
|
||||
inspection.inlineCodeContinuityPassed,
|
||||
`主题 ${theme.id} 的行内代码连续性门禁失败:${JSON.stringify(inspection.inlineCodeContinuity)}`
|
||||
);
|
||||
const standard = standardByTheme.get(theme.id);
|
||||
assert(standard, `标准矩阵缺少主题 ${theme.id}`);
|
||||
assert(
|
||||
@@ -504,6 +585,7 @@ try {
|
||||
docx: {
|
||||
outputFile: path.relative(repositoryDirectory, docxPath),
|
||||
inspection,
|
||||
documentLayout: capturedDocxDocumentLayout ?? { tables: [] },
|
||||
diagnostics: responseDiagnostics(docx.response, "docx")
|
||||
},
|
||||
standardInspection: standard.inspection,
|
||||
|
||||
Reference in New Issue
Block a user