新增通用 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 产物仍为未签名内部发行。
278 lines
6.9 KiB
TypeScript
278 lines
6.9 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
||
import type {
|
||
SemanticDocumentModel,
|
||
SemanticDocumentNode
|
||
} from "@md-to-pdf/core";
|
||
import type {
|
||
DocxResolvedStyleSlot,
|
||
DocxStyleSlotName,
|
||
DocxThemeTokenSet
|
||
} from "@md-to-pdf/docx-theme-engine";
|
||
import {
|
||
createPandocStructurePlan,
|
||
type PandocStructureBlock
|
||
} from "../src/index.js";
|
||
|
||
function slot(
|
||
name: DocxStyleSlotName,
|
||
hidden = false
|
||
): DocxResolvedStyleSlot {
|
||
return {
|
||
slot: name,
|
||
source: "computed-css",
|
||
confidence: "exact",
|
||
style: {
|
||
fontCandidates: [],
|
||
...(hidden ? { hidden: true } : {})
|
||
}
|
||
};
|
||
}
|
||
|
||
function tokens(
|
||
slots: DocxResolvedStyleSlot[] = []
|
||
): DocxThemeTokenSet {
|
||
return {
|
||
schemaVersion: 1,
|
||
themeId: "test-theme",
|
||
themeFingerprint: "c".repeat(64),
|
||
mode: "auto-with-overrides",
|
||
basePreset: "technical",
|
||
slots,
|
||
diagnostics: []
|
||
};
|
||
}
|
||
|
||
function text(
|
||
role: Extract<SemanticDocumentNode, { kind: "text" }>["role"],
|
||
value: string,
|
||
label?: string
|
||
): SemanticDocumentNode {
|
||
return {
|
||
kind: "text",
|
||
role,
|
||
text: value,
|
||
...(label ? { label } : {})
|
||
};
|
||
}
|
||
|
||
function paragraphSegments(block: PandocStructureBlock): string[] {
|
||
if (block.kind === "paragraph") {
|
||
return block.segments;
|
||
}
|
||
if (block.kind === "container") {
|
||
return block.blocks.flatMap(paragraphSegments);
|
||
}
|
||
return [];
|
||
}
|
||
|
||
describe("Pandoc 语义结构投影", () => {
|
||
it("将简报抬头文字映射到父级抬头样式", () => {
|
||
const model: SemanticDocumentModel = {
|
||
schemaVersion: 1,
|
||
profile: "briefing",
|
||
titlePolicy: {
|
||
metadataTitle: "suppress",
|
||
firstBodyHeading: "keep"
|
||
},
|
||
regions: [{
|
||
kind: "prefix",
|
||
nodes: [{
|
||
kind: "group",
|
||
role: "briefing-masthead",
|
||
children: [text("briefing-masthead-text", "工作简报")]
|
||
}]
|
||
}]
|
||
};
|
||
const plan = createPandocStructurePlan(model, tokens(), {
|
||
markerSeed: "briefing"
|
||
});
|
||
expect(plan.prefix[0]).toMatchObject({
|
||
kind: "container",
|
||
blocks: [{
|
||
kind: "paragraph",
|
||
styleId: "MdBriefingMastheadText",
|
||
segments: ["工作简报"]
|
||
}]
|
||
});
|
||
});
|
||
|
||
it("将公文分组投影为固定 Word 样式和可编辑段落", () => {
|
||
const model: SemanticDocumentModel = {
|
||
schemaVersion: 1,
|
||
profile: "official",
|
||
titlePolicy: {
|
||
metadataTitle: "suppress",
|
||
firstBodyHeading: "suppress"
|
||
},
|
||
regions: [
|
||
{
|
||
kind: "prefix",
|
||
nodes: [
|
||
{
|
||
kind: "group",
|
||
role: "official-masthead",
|
||
children: [
|
||
{
|
||
kind: "group",
|
||
role: "official-classification",
|
||
layout: "space-between",
|
||
children: [
|
||
text("official-secrecy", "秘密"),
|
||
text("official-urgency", "特急")
|
||
]
|
||
},
|
||
text("official-issuer", "示例单位"),
|
||
{
|
||
kind: "group",
|
||
role: "official-issue-row",
|
||
layout: "space-between",
|
||
children: [
|
||
text("official-number", "示例〔2026〕1号"),
|
||
text(
|
||
"official-signatory",
|
||
"张三",
|
||
"签发人:"
|
||
)
|
||
]
|
||
}
|
||
]
|
||
},
|
||
text("official-title", "关于开展工作的通知")
|
||
]
|
||
}
|
||
]
|
||
};
|
||
|
||
const plan = createPandocStructurePlan(model, tokens(), {
|
||
markerSeed: "official"
|
||
});
|
||
|
||
expect(plan.metadataPolicy.author).toBe("suppress");
|
||
expect(plan.titlePolicy.firstBodyHeading).toBe("suppress");
|
||
expect(plan.prefix[0]).toMatchObject({
|
||
kind: "container",
|
||
styleId: "MdOfficialMasthead",
|
||
slot: "official-masthead",
|
||
startMarker:
|
||
"MD_TO_PDF_CONTAINER_START_official_1",
|
||
endMarker: "MD_TO_PDF_CONTAINER_END_official_2",
|
||
blocks: [
|
||
{
|
||
kind: "paragraph",
|
||
styleId: "MdOfficialClassification",
|
||
layout: "space-between",
|
||
segments: ["秘密", "特急"],
|
||
separator: "tab"
|
||
},
|
||
{
|
||
kind: "paragraph",
|
||
styleId: "MdOfficialIssuer",
|
||
segments: ["示例单位"]
|
||
},
|
||
{
|
||
kind: "paragraph",
|
||
styleId: "MdOfficialIssueRow",
|
||
layout: "space-between",
|
||
segments: ["示例〔2026〕1号", "签发人:张三"],
|
||
separator: "tab"
|
||
}
|
||
]
|
||
});
|
||
expect(plan.prefix[1]).toMatchObject({
|
||
kind: "paragraph",
|
||
styleId: "MdOfficialTitle",
|
||
segments: ["关于开展工作的通知"]
|
||
});
|
||
});
|
||
|
||
it("按主题隐藏令牌过滤标书字段并保留封面分页", () => {
|
||
const model: SemanticDocumentModel = {
|
||
schemaVersion: 1,
|
||
profile: "tender",
|
||
titlePolicy: {
|
||
metadataTitle: "suppress",
|
||
firstBodyHeading: "suppress"
|
||
},
|
||
regions: [
|
||
{
|
||
kind: "cover",
|
||
section: {
|
||
headerFooter: "none",
|
||
pageNumber: "hidden",
|
||
breakAfter: "next-page",
|
||
followingPageNumberStart: 1
|
||
},
|
||
nodes: [
|
||
{
|
||
kind: "group",
|
||
role: "tender-cover",
|
||
children: [
|
||
text("tender-title", "投标文件"),
|
||
text("tender-bidder", "投标单位"),
|
||
text(
|
||
"tender-representative",
|
||
"授权代表"
|
||
),
|
||
text("tender-date", "2026年7月")
|
||
]
|
||
}
|
||
]
|
||
}
|
||
]
|
||
};
|
||
|
||
const plan = createPandocStructurePlan(
|
||
model,
|
||
tokens([
|
||
slot("tender-bidder", true),
|
||
slot("tender-representative", true)
|
||
]),
|
||
{ markerSeed: "tender" }
|
||
);
|
||
|
||
expect(plan.prefix.at(-1)).toEqual({
|
||
kind: "section-break",
|
||
marker: "MD_TO_PDF_SECTION_BREAK_tender_3",
|
||
headerFooter: "none",
|
||
pageNumber: "hidden",
|
||
followingPageNumberStart: 1
|
||
});
|
||
expect(plan.prefix[0]).toMatchObject({
|
||
kind: "container",
|
||
styleId: "MdTenderCover"
|
||
});
|
||
expect(paragraphSegments(plan.prefix[0]!)).toEqual([
|
||
"投标文件",
|
||
"2026年7月"
|
||
]);
|
||
});
|
||
|
||
it("没有结构区域时只传递标题策略", () => {
|
||
const plan = createPandocStructurePlan(
|
||
{
|
||
schemaVersion: 1,
|
||
titlePolicy: {
|
||
metadataTitle: "emit",
|
||
firstBodyHeading: "keep"
|
||
},
|
||
regions: []
|
||
},
|
||
tokens(),
|
||
{ markerSeed: "empty" }
|
||
);
|
||
|
||
expect(plan).toEqual({
|
||
schemaVersion: 1,
|
||
metadataPolicy: {
|
||
author: "suppress"
|
||
},
|
||
titlePolicy: {
|
||
metadataTitle: "emit",
|
||
firstBodyHeading: "keep"
|
||
},
|
||
prefix: [],
|
||
suffix: []
|
||
});
|
||
});
|
||
});
|