feat: 完成 DOCX 通用结构映射与严格收口
This commit is contained in:
@@ -0,0 +1,239 @@
|
||||
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("将公文分组投影为固定 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",
|
||||
children: [
|
||||
text("official-secrecy", "秘密"),
|
||||
text("official-urgency", "特急")
|
||||
]
|
||||
},
|
||||
text("official-issuer", "示例单位"),
|
||||
{
|
||||
kind: "group",
|
||||
role: "official-issue-row",
|
||||
children: [
|
||||
text("official-number", "示例〔2026〕1号"),
|
||||
text(
|
||||
"official-signatory",
|
||||
"张三",
|
||||
"签发人:"
|
||||
)
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
text("official-title", "关于开展工作的通知")
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const plan = createPandocStructurePlan(model, tokens(), {
|
||||
markerSeed: "official"
|
||||
});
|
||||
|
||||
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",
|
||||
segments: ["秘密", "特急"],
|
||||
separator: "tab"
|
||||
},
|
||||
{
|
||||
kind: "paragraph",
|
||||
styleId: "MdOfficialIssuer",
|
||||
segments: ["示例单位"]
|
||||
},
|
||||
{
|
||||
kind: "paragraph",
|
||||
styleId: "MdOfficialIssueRow",
|
||||
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,
|
||||
titlePolicy: {
|
||||
metadataTitle: "emit",
|
||||
firstBodyHeading: "keep"
|
||||
},
|
||||
prefix: [],
|
||||
suffix: []
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user