feat: 建立统一语义文档模型
This commit is contained in:
@@ -41,6 +41,14 @@ const request = {
|
|||||||
keywords: [],
|
keywords: [],
|
||||||
language: "zh-CN"
|
language: "zh-CN"
|
||||||
},
|
},
|
||||||
|
semanticDocument: {
|
||||||
|
schemaVersion: 1,
|
||||||
|
titlePolicy: {
|
||||||
|
metadataTitle: "emit",
|
||||||
|
firstBodyHeading: "keep"
|
||||||
|
},
|
||||||
|
regions: []
|
||||||
|
},
|
||||||
features: [],
|
features: [],
|
||||||
warnings: []
|
warnings: []
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -26,6 +26,14 @@ const payload = createPagedDocumentPayload({
|
|||||||
keywords: [],
|
keywords: [],
|
||||||
language: "zh-CN"
|
language: "zh-CN"
|
||||||
},
|
},
|
||||||
|
semanticDocument: {
|
||||||
|
schemaVersion: 1,
|
||||||
|
titlePolicy: {
|
||||||
|
metadataTitle: "suppress",
|
||||||
|
firstBodyHeading: "keep"
|
||||||
|
},
|
||||||
|
regions: []
|
||||||
|
},
|
||||||
features: ["mermaid"],
|
features: ["mermaid"],
|
||||||
warnings: []
|
warnings: []
|
||||||
},
|
},
|
||||||
|
|||||||
+16
-2
@@ -183,6 +183,16 @@ Playwright Chromium 151 与 Electron Chromium 150 分别对 14 套主题生成
|
|||||||
经典标书双层边框与轮廓偏移近似,将在下一阶段由语义文档结构层消费。当前尚未将令牌接入
|
经典标书双层边框与轮廓偏移近似,将在下一阶段由语义文档结构层消费。当前尚未将令牌接入
|
||||||
`reference.docx` 或 Pandoc 转换链路。
|
`reference.docx` 或 Pandoc 转换链路。
|
||||||
|
|
||||||
|
`v0.6.0` 阶段 12A 已建立独立 `packages/semantic-document` 和版本化
|
||||||
|
统一语义文档协议。普通文档、公文、简报、项目报告与标书现在先从
|
||||||
|
Front Matter 和正文首个 H1 构建主题无关的语义树,再由 Renderer 投影为
|
||||||
|
现有 HTML 节点与类名,因此网页预览和 PDF 的 DOM 保持兼容;同一模型也
|
||||||
|
随 `RenderedMarkdownDocument` 进入 `PreparedDocxExport`。模型已表达
|
||||||
|
版头、落款、版记、封面、标题去重策略,以及封面隐藏页眉页脚、隐藏页码、
|
||||||
|
下一页分节和正文页码重启意图。当前尚未改变 Pandoc AST、动态
|
||||||
|
`reference.docx` 或最终 DOCX;下一步阶段 12B 将把已归一化的主题令牌
|
||||||
|
接入动态 Word 模板。
|
||||||
|
|
||||||
## 2. 已完成
|
## 2. 已完成
|
||||||
|
|
||||||
### 2.1 项目骨架
|
### 2.1 项目骨架
|
||||||
@@ -1112,8 +1122,12 @@ ECharts 第二阶段浏览器与 PDF 验证结果:
|
|||||||
Playwright/Electron 真实采集、主题指纹缓存和双引擎一致性矩阵已通过;
|
Playwright/Electron 真实采集、主题指纹缓存和双引擎一致性矩阵已通过;
|
||||||
CSS 计算值到 Word 样式令牌的通用归一化、清单覆盖、预设降级和结构
|
CSS 计算值到 Word 样式令牌的通用归一化、清单覆盖、预设降级和结构
|
||||||
近似诊断已通过 14 主题双引擎矩阵;下一步由统一语义文档模型消费令牌;
|
近似诊断已通过 14 主题双引擎矩阵;下一步由统一语义文档模型消费令牌;
|
||||||
- 阶段 12:建立统一语义文档模型,完成 Front Matter、封面、分节、
|
- 阶段 12A:已建立独立统一语义文档模型,Renderer 与 DOCX 准备结果
|
||||||
表格宽度和分页控制;
|
共用同一语义树,现有 HTML DOM 保持兼容;
|
||||||
|
- 阶段 12B:将主题令牌接入动态 `reference.docx`;
|
||||||
|
- 阶段 12C:完成 Pandoc 结构映射、封面分节、标题去重、表格宽度和
|
||||||
|
分页控制;
|
||||||
|
- 阶段 12D:完成 14 套主题的真实 Word/WPS 视觉与结构回归;
|
||||||
- 阶段 13:完成 Word/WPS 双向互存、外部主题兼容、体积和正式发布验收。
|
- 阶段 13:完成 Word/WPS 双向互存、外部主题兼容、体积和正式发布验收。
|
||||||
|
|
||||||
每个阶段验收通过后创建一个独立提交,再进入下一阶段。当前阶段不得混入
|
每个阶段验收通过后创建一个独立提交,再进入下一阶段。当前阶段不得混入
|
||||||
|
|||||||
Generated
+15
@@ -2207,6 +2207,10 @@
|
|||||||
"resolved": "packages/renderer",
|
"resolved": "packages/renderer",
|
||||||
"link": true
|
"link": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@md-to-pdf/semantic-document": {
|
||||||
|
"resolved": "packages/semantic-document",
|
||||||
|
"link": true
|
||||||
|
},
|
||||||
"node_modules/@md-to-pdf/server": {
|
"node_modules/@md-to-pdf/server": {
|
||||||
"resolved": "apps/server",
|
"resolved": "apps/server",
|
||||||
"link": true
|
"link": true
|
||||||
@@ -9994,6 +9998,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@md-to-pdf/core": "0.1.0",
|
"@md-to-pdf/core": "0.1.0",
|
||||||
"@md-to-pdf/markdown-echarts": "0.1.0",
|
"@md-to-pdf/markdown-echarts": "0.1.0",
|
||||||
|
"@md-to-pdf/semantic-document": "0.1.0",
|
||||||
"@mdit/plugin-anchor": "^1.1.1",
|
"@mdit/plugin-anchor": "^1.1.1",
|
||||||
"@mdit/plugin-footnote": "^1.0.1",
|
"@mdit/plugin-footnote": "^1.0.1",
|
||||||
"@mdit/plugin-katex": "^1.0.1",
|
"@mdit/plugin-katex": "^1.0.1",
|
||||||
@@ -10008,6 +10013,16 @@
|
|||||||
"@types/sanitize-html": "^2.16.0",
|
"@types/sanitize-html": "^2.16.0",
|
||||||
"vitest": "^4.1.10"
|
"vitest": "^4.1.10"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"packages/semantic-document": {
|
||||||
|
"name": "@md-to-pdf/semantic-document",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@md-to-pdf/core": "0.1.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"vitest": "^4.1.10"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-6
@@ -10,10 +10,11 @@
|
|||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "npm run build:web-runtime && npm run build -w @md-to-pdf/server && npm run build -w @md-to-pdf/desktop",
|
"build": "npm run build:web-runtime && npm run build -w @md-to-pdf/server && npm run build -w @md-to-pdf/desktop",
|
||||||
"build:web-runtime": "npm run build -w @md-to-pdf/markdown-echarts && npm run build -w @md-to-pdf/core && npm run build -w @md-to-pdf/docx-theme-engine && npm run build -w @md-to-pdf/docx-engine && npm run build -w @md-to-pdf/renderer && npm run build -w @md-to-pdf/application && npm run build -w @md-to-pdf/preview-engine && npm run build -w @md-to-pdf/web",
|
"build:web-runtime": "npm run build -w @md-to-pdf/markdown-echarts && npm run build -w @md-to-pdf/core && npm run build -w @md-to-pdf/semantic-document && npm run build -w @md-to-pdf/docx-theme-engine && npm run build -w @md-to-pdf/docx-engine && npm run build -w @md-to-pdf/renderer && npm run build -w @md-to-pdf/application && npm run build -w @md-to-pdf/preview-engine && npm run build -w @md-to-pdf/web",
|
||||||
"dev": "npm run build -w @md-to-pdf/markdown-echarts && npm run build -w @md-to-pdf/core && npm run build -w @md-to-pdf/docx-theme-engine && npm run build -w @md-to-pdf/docx-engine && npm run build -w @md-to-pdf/renderer && npm run build -w @md-to-pdf/application && npm run build -w @md-to-pdf/preview-engine && concurrently -k -n markdown-echarts,core,docx-theme-engine,docx-engine,renderer,application,preview-engine,server,web \"npm:dev:markdown-echarts\" \"npm:dev:core\" \"npm:dev:docx-theme-engine\" \"npm:dev:docx-engine\" \"npm:dev:renderer\" \"npm:dev:application\" \"npm:dev:preview-engine\" \"npm:dev:server\" \"npm:dev:web\"",
|
"dev": "npm run build -w @md-to-pdf/markdown-echarts && npm run build -w @md-to-pdf/core && npm run build -w @md-to-pdf/semantic-document && npm run build -w @md-to-pdf/docx-theme-engine && npm run build -w @md-to-pdf/docx-engine && npm run build -w @md-to-pdf/renderer && npm run build -w @md-to-pdf/application && npm run build -w @md-to-pdf/preview-engine && concurrently -k -n markdown-echarts,core,semantic-document,docx-theme-engine,docx-engine,renderer,application,preview-engine,server,web \"npm:dev:markdown-echarts\" \"npm:dev:core\" \"npm:dev:semantic-document\" \"npm:dev:docx-theme-engine\" \"npm:dev:docx-engine\" \"npm:dev:renderer\" \"npm:dev:application\" \"npm:dev:preview-engine\" \"npm:dev:server\" \"npm:dev:web\"",
|
||||||
"dev:markdown-echarts": "npm run dev -w @md-to-pdf/markdown-echarts",
|
"dev:markdown-echarts": "npm run dev -w @md-to-pdf/markdown-echarts",
|
||||||
"dev:core": "npm run dev -w @md-to-pdf/core",
|
"dev:core": "npm run dev -w @md-to-pdf/core",
|
||||||
|
"dev:semantic-document": "npm run dev -w @md-to-pdf/semantic-document",
|
||||||
"dev:docx-theme-engine": "npm run dev -w @md-to-pdf/docx-theme-engine",
|
"dev:docx-theme-engine": "npm run dev -w @md-to-pdf/docx-theme-engine",
|
||||||
"dev:docx-engine": "npm run dev -w @md-to-pdf/docx-engine",
|
"dev:docx-engine": "npm run dev -w @md-to-pdf/docx-engine",
|
||||||
"dev:renderer": "npm run dev -w @md-to-pdf/renderer",
|
"dev:renderer": "npm run dev -w @md-to-pdf/renderer",
|
||||||
@@ -22,18 +23,18 @@
|
|||||||
"dev:server": "npm run dev -w @md-to-pdf/server",
|
"dev:server": "npm run dev -w @md-to-pdf/server",
|
||||||
"dev:web": "npm run dev -w @md-to-pdf/web",
|
"dev:web": "npm run dev -w @md-to-pdf/web",
|
||||||
"dev:desktop": "npm run dev -w @md-to-pdf/desktop",
|
"dev:desktop": "npm run dev -w @md-to-pdf/desktop",
|
||||||
"desktop:dev": "npm run build -w @md-to-pdf/markdown-echarts && npm run build -w @md-to-pdf/core && npm run build -w @md-to-pdf/docx-engine && npm run build -w @md-to-pdf/renderer && npm run build -w @md-to-pdf/application && npm run build -w @md-to-pdf/preview-engine && concurrently -k -n web,desktop \"npm:dev:web\" \"npm:dev:desktop\"",
|
"desktop:dev": "npm run build -w @md-to-pdf/markdown-echarts && npm run build -w @md-to-pdf/core && npm run build -w @md-to-pdf/semantic-document && npm run build -w @md-to-pdf/docx-engine && npm run build -w @md-to-pdf/renderer && npm run build -w @md-to-pdf/application && npm run build -w @md-to-pdf/preview-engine && concurrently -k -n web,desktop \"npm:dev:web\" \"npm:dev:desktop\"",
|
||||||
"desktop:package": "npm run package -w @md-to-pdf/desktop",
|
"desktop:package": "npm run package -w @md-to-pdf/desktop",
|
||||||
"theme:import-typora": "node scripts/import-typora-theme.mjs",
|
"theme:import-typora": "node scripts/import-typora-theme.mjs",
|
||||||
"verify:docx-reference": "npm run build -w @md-to-pdf/core && npm run build -w @md-to-pdf/docx-engine && npm run verify:pandoc -w @md-to-pdf/docx-engine",
|
"verify:docx-reference": "npm run build -w @md-to-pdf/core && npm run build -w @md-to-pdf/docx-engine && npm run verify:pandoc -w @md-to-pdf/docx-engine",
|
||||||
"verify:docx-conversion": "npm run build -w @md-to-pdf/core && npm run build -w @md-to-pdf/docx-engine && npm run verify:conversion -w @md-to-pdf/docx-engine",
|
"verify:docx-conversion": "npm run build -w @md-to-pdf/core && npm run build -w @md-to-pdf/docx-engine && npm run verify:conversion -w @md-to-pdf/docx-engine",
|
||||||
"verify:docx-matrix": "npm run build -w @md-to-pdf/core && npm run build -w @md-to-pdf/docx-engine && npm run verify:acceptance -w @md-to-pdf/docx-engine",
|
"verify:docx-matrix": "npm run build -w @md-to-pdf/core && npm run build -w @md-to-pdf/docx-engine && npm run verify:acceptance -w @md-to-pdf/docx-engine",
|
||||||
"verify:docx-themes": "npm run build:web-runtime && npm run verify:themes -w @md-to-pdf/docx-engine",
|
"verify:docx-themes": "npm run build:web-runtime && npm run verify:themes -w @md-to-pdf/docx-engine",
|
||||||
"verify:docx-theme-styles": "npm run build -w @md-to-pdf/core && npm run build -w @md-to-pdf/docx-theme-engine && npm run build -w @md-to-pdf/renderer && npm run build -w @md-to-pdf/application && npm run build -w @md-to-pdf/server && npm run verify:docx-theme-styles -w @md-to-pdf/server && npm run verify:docx-theme-styles -w @md-to-pdf/desktop",
|
"verify:docx-theme-styles": "npm run build -w @md-to-pdf/core && npm run build -w @md-to-pdf/semantic-document && npm run build -w @md-to-pdf/docx-theme-engine && npm run build -w @md-to-pdf/renderer && npm run build -w @md-to-pdf/application && npm run build -w @md-to-pdf/server && npm run verify:docx-theme-styles -w @md-to-pdf/server && npm run verify:docx-theme-styles -w @md-to-pdf/desktop",
|
||||||
"verify:docx-acceptance": "npm run build:web-runtime && npm run build -w @md-to-pdf/server && npm run build -w @md-to-pdf/desktop && npm run verify:pandoc -w @md-to-pdf/docx-engine && npm run verify:conversion -w @md-to-pdf/docx-engine && npm run verify:acceptance -w @md-to-pdf/docx-engine && npm run verify:docx-http -w @md-to-pdf/server && npm run test -w @md-to-pdf/desktop -- desktop-docx-save.test.ts",
|
"verify:docx-acceptance": "npm run build:web-runtime && npm run build -w @md-to-pdf/server && npm run build -w @md-to-pdf/desktop && npm run verify:pandoc -w @md-to-pdf/docx-engine && npm run verify:conversion -w @md-to-pdf/docx-engine && npm run verify:acceptance -w @md-to-pdf/docx-engine && npm run verify:docx-http -w @md-to-pdf/server && npm run test -w @md-to-pdf/desktop -- desktop-docx-save.test.ts",
|
||||||
"verify:docx-http": "npm run build:web-runtime && npm run build -w @md-to-pdf/server && npm run verify:docx-http -w @md-to-pdf/server",
|
"verify:docx-http": "npm run build:web-runtime && npm run build -w @md-to-pdf/server && npm run verify:docx-http -w @md-to-pdf/server",
|
||||||
"test": "npm run test -w @md-to-pdf/markdown-echarts && npm run test -w @md-to-pdf/core && npm run build -w @md-to-pdf/core && npm run test -w @md-to-pdf/docx-theme-engine && npm run test -w @md-to-pdf/docx-engine && npm run test -w @md-to-pdf/renderer && npm run test -w @md-to-pdf/application && npm run build -w @md-to-pdf/application && npm run test -w @md-to-pdf/preview-engine && npm run build -w @md-to-pdf/preview-engine && npm run test -w @md-to-pdf/web && npm run test -w @md-to-pdf/server && npm run test -w @md-to-pdf/desktop",
|
"test": "npm run test -w @md-to-pdf/markdown-echarts && npm run test -w @md-to-pdf/core && npm run build -w @md-to-pdf/core && npm run test -w @md-to-pdf/semantic-document && npm run build -w @md-to-pdf/semantic-document && npm run test -w @md-to-pdf/docx-theme-engine && npm run test -w @md-to-pdf/docx-engine && npm run test -w @md-to-pdf/renderer && npm run test -w @md-to-pdf/application && npm run build -w @md-to-pdf/application && npm run test -w @md-to-pdf/preview-engine && npm run build -w @md-to-pdf/preview-engine && npm run test -w @md-to-pdf/web && npm run test -w @md-to-pdf/server && npm run test -w @md-to-pdf/desktop",
|
||||||
"typecheck": "npm run typecheck -w @md-to-pdf/markdown-echarts && npm run build -w @md-to-pdf/markdown-echarts && npm run typecheck -w @md-to-pdf/core && npm run build -w @md-to-pdf/core && npm run typecheck -w @md-to-pdf/docx-theme-engine && npm run build -w @md-to-pdf/docx-theme-engine && npm run typecheck -w @md-to-pdf/docx-engine && npm run build -w @md-to-pdf/docx-engine && npm run typecheck -w @md-to-pdf/renderer && npm run build -w @md-to-pdf/renderer && npm run typecheck -w @md-to-pdf/application && npm run build -w @md-to-pdf/application && npm run typecheck -w @md-to-pdf/preview-engine && npm run build -w @md-to-pdf/preview-engine && npm run typecheck -w @md-to-pdf/web && npm run typecheck -w @md-to-pdf/server && npm run typecheck -w @md-to-pdf/desktop"
|
"typecheck": "npm run typecheck -w @md-to-pdf/markdown-echarts && npm run build -w @md-to-pdf/markdown-echarts && npm run typecheck -w @md-to-pdf/core && npm run build -w @md-to-pdf/core && npm run typecheck -w @md-to-pdf/semantic-document && npm run build -w @md-to-pdf/semantic-document && npm run typecheck -w @md-to-pdf/docx-theme-engine && npm run build -w @md-to-pdf/docx-theme-engine && npm run typecheck -w @md-to-pdf/docx-engine && npm run build -w @md-to-pdf/docx-engine && npm run typecheck -w @md-to-pdf/renderer && npm run build -w @md-to-pdf/renderer && npm run typecheck -w @md-to-pdf/application && npm run build -w @md-to-pdf/application && npm run typecheck -w @md-to-pdf/preview-engine && npm run build -w @md-to-pdf/preview-engine && npm run typecheck -w @md-to-pdf/web && npm run typecheck -w @md-to-pdf/server && npm run typecheck -w @md-to-pdf/desktop"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=22"
|
"node": ">=22"
|
||||||
|
|||||||
@@ -295,6 +295,14 @@ describe("共享应用服务", () => {
|
|||||||
resources: []
|
resources: []
|
||||||
});
|
});
|
||||||
expect(prepared.document.metadata.title).toBe("DOCX 文档");
|
expect(prepared.document.metadata.title).toBe("DOCX 文档");
|
||||||
|
expect(prepared.document.semanticDocument).toEqual({
|
||||||
|
schemaVersion: 1,
|
||||||
|
titlePolicy: {
|
||||||
|
metadataTitle: "suppress",
|
||||||
|
firstBodyHeading: "keep"
|
||||||
|
},
|
||||||
|
regions: []
|
||||||
|
});
|
||||||
expect(prepared.theme.manifest.id).toBe("test-theme");
|
expect(prepared.theme.manifest.id).toBe("test-theme");
|
||||||
expect(prepared.theme.source).toBe("bundled");
|
expect(prepared.theme.source).toBe("bundled");
|
||||||
expect(prepared.theme.css).toContain(
|
expect(prepared.theme.css).toContain(
|
||||||
|
|||||||
@@ -59,6 +59,14 @@ const prepared: PreparedDocxExport = {
|
|||||||
keywords: [],
|
keywords: [],
|
||||||
language: "zh-CN"
|
language: "zh-CN"
|
||||||
},
|
},
|
||||||
|
semanticDocument: {
|
||||||
|
schemaVersion: 1,
|
||||||
|
titlePolicy: {
|
||||||
|
metadataTitle: "suppress",
|
||||||
|
firstBodyHeading: "keep"
|
||||||
|
},
|
||||||
|
regions: []
|
||||||
|
},
|
||||||
features: [],
|
features: [],
|
||||||
warnings: []
|
warnings: []
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -70,6 +70,14 @@ const prepared: PreparedDocxExport = {
|
|||||||
keywords: [],
|
keywords: [],
|
||||||
language: "zh-CN"
|
language: "zh-CN"
|
||||||
},
|
},
|
||||||
|
semanticDocument: {
|
||||||
|
schemaVersion: 1,
|
||||||
|
titlePolicy: {
|
||||||
|
metadataTitle: "suppress",
|
||||||
|
firstBodyHeading: "keep"
|
||||||
|
},
|
||||||
|
regions: []
|
||||||
|
},
|
||||||
features: [],
|
features: [],
|
||||||
warnings: ["原始图片已降级"]
|
warnings: ["原始图片已降级"]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { ExportConfig } from "./export-config.js";
|
import type { ExportConfig } from "./export-config.js";
|
||||||
import type { DocumentProfile } from "./document-profile.js";
|
import type { DocumentProfile } from "./document-profile.js";
|
||||||
|
import type { SemanticDocumentModel } from "./semantic-document.js";
|
||||||
import type { ThemeFeature } from "./theme.js";
|
import type { ThemeFeature } from "./theme.js";
|
||||||
|
|
||||||
export interface MarkdownDocumentMetadata {
|
export interface MarkdownDocumentMetadata {
|
||||||
@@ -16,6 +17,7 @@ export interface RenderedMarkdownDocument {
|
|||||||
articleHtml: string;
|
articleHtml: string;
|
||||||
bodyHtml: string;
|
bodyHtml: string;
|
||||||
metadata: MarkdownDocumentMetadata;
|
metadata: MarkdownDocumentMetadata;
|
||||||
|
semanticDocument: SemanticDocumentModel;
|
||||||
features: ThemeFeature[];
|
features: ThemeFeature[];
|
||||||
warnings: string[];
|
warnings: string[];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,4 +4,5 @@ export * from "./docx-style.js";
|
|||||||
export * from "./document-profile.js";
|
export * from "./document-profile.js";
|
||||||
export * from "./document-link.js";
|
export * from "./document-link.js";
|
||||||
export * from "./export-config.js";
|
export * from "./export-config.js";
|
||||||
|
export * from "./semantic-document.js";
|
||||||
export * from "./theme.js";
|
export * from "./theme.js";
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
import { documentProfileNameSchema } from "./document-profile.js";
|
||||||
|
|
||||||
|
export const semanticDocumentRoleSchema = z.enum([
|
||||||
|
"official-masthead",
|
||||||
|
"official-classification",
|
||||||
|
"official-secrecy",
|
||||||
|
"official-urgency",
|
||||||
|
"official-issuer",
|
||||||
|
"official-issue-row",
|
||||||
|
"official-number",
|
||||||
|
"official-signatory",
|
||||||
|
"official-title",
|
||||||
|
"official-signature",
|
||||||
|
"official-signature-issuer",
|
||||||
|
"official-signature-date",
|
||||||
|
"official-edition",
|
||||||
|
"official-copy-to",
|
||||||
|
"official-printing-row",
|
||||||
|
"official-printing-office",
|
||||||
|
"official-printing-date",
|
||||||
|
"briefing-masthead",
|
||||||
|
"briefing-masthead-text",
|
||||||
|
"briefing-meta",
|
||||||
|
"briefing-issue",
|
||||||
|
"briefing-publisher",
|
||||||
|
"briefing-signatory",
|
||||||
|
"briefing-date",
|
||||||
|
"briefing-title",
|
||||||
|
"briefing-contact",
|
||||||
|
"project-report-cover",
|
||||||
|
"project-report-project-name",
|
||||||
|
"project-report-title",
|
||||||
|
"project-report-version",
|
||||||
|
"project-report-owner",
|
||||||
|
"project-report-prepared-by",
|
||||||
|
"project-report-date",
|
||||||
|
"tender-cover",
|
||||||
|
"tender-copy-mark",
|
||||||
|
"tender-project-name",
|
||||||
|
"tender-project-number",
|
||||||
|
"tender-title",
|
||||||
|
"tender-volume",
|
||||||
|
"tender-bidder",
|
||||||
|
"tender-representative",
|
||||||
|
"tender-date"
|
||||||
|
]);
|
||||||
|
|
||||||
|
export type SemanticDocumentRole = z.infer<
|
||||||
|
typeof semanticDocumentRoleSchema
|
||||||
|
>;
|
||||||
|
|
||||||
|
export interface SemanticDocumentTextNode {
|
||||||
|
kind: "text";
|
||||||
|
role: SemanticDocumentRole;
|
||||||
|
text: string;
|
||||||
|
label?: string | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SemanticDocumentGroupNode {
|
||||||
|
kind: "group";
|
||||||
|
role: SemanticDocumentRole;
|
||||||
|
children: SemanticDocumentNode[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SemanticDocumentNode =
|
||||||
|
| SemanticDocumentTextNode
|
||||||
|
| SemanticDocumentGroupNode;
|
||||||
|
|
||||||
|
export const semanticDocumentNodeSchema: z.ZodType<
|
||||||
|
SemanticDocumentNode
|
||||||
|
> = z.lazy(() =>
|
||||||
|
z.discriminatedUnion("kind", [
|
||||||
|
z.object({
|
||||||
|
kind: z.literal("text"),
|
||||||
|
role: semanticDocumentRoleSchema,
|
||||||
|
text: z.string().min(1).max(1000),
|
||||||
|
label: z.string().min(1).max(100).optional()
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
kind: z.literal("group"),
|
||||||
|
role: semanticDocumentRoleSchema,
|
||||||
|
children: z.array(semanticDocumentNodeSchema).min(1).max(30)
|
||||||
|
})
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
export const semanticDocumentRegionKindSchema = z.enum([
|
||||||
|
"cover",
|
||||||
|
"prefix",
|
||||||
|
"suffix"
|
||||||
|
]);
|
||||||
|
|
||||||
|
export type SemanticDocumentRegionKind = z.infer<
|
||||||
|
typeof semanticDocumentRegionKindSchema
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const semanticDocumentSectionIntentSchema = z.object({
|
||||||
|
headerFooter: z.literal("none"),
|
||||||
|
pageNumber: z.literal("hidden"),
|
||||||
|
breakAfter: z.literal("next-page"),
|
||||||
|
followingPageNumberStart: z.number().int().min(0).max(100000)
|
||||||
|
});
|
||||||
|
|
||||||
|
export type SemanticDocumentSectionIntent = z.infer<
|
||||||
|
typeof semanticDocumentSectionIntentSchema
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const semanticDocumentRegionSchema = z.object({
|
||||||
|
kind: semanticDocumentRegionKindSchema,
|
||||||
|
nodes: z.array(semanticDocumentNodeSchema).min(1).max(30),
|
||||||
|
section: semanticDocumentSectionIntentSchema.optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type SemanticDocumentRegion = z.infer<
|
||||||
|
typeof semanticDocumentRegionSchema
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const semanticDocumentTitlePolicySchema = z.object({
|
||||||
|
metadataTitle: z.enum(["emit", "suppress"]),
|
||||||
|
firstBodyHeading: z.enum(["keep", "suppress"])
|
||||||
|
});
|
||||||
|
|
||||||
|
export type SemanticDocumentTitlePolicy = z.infer<
|
||||||
|
typeof semanticDocumentTitlePolicySchema
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const semanticDocumentModelSchema = z.object({
|
||||||
|
schemaVersion: z.literal(1),
|
||||||
|
profile: documentProfileNameSchema.optional(),
|
||||||
|
titlePolicy: semanticDocumentTitlePolicySchema,
|
||||||
|
regions: z.array(semanticDocumentRegionSchema).max(10)
|
||||||
|
});
|
||||||
|
|
||||||
|
export type SemanticDocumentModel = z.infer<
|
||||||
|
typeof semanticDocumentModelSchema
|
||||||
|
>;
|
||||||
@@ -18,6 +18,14 @@ describe("分页文档载荷", () => {
|
|||||||
keywords: [],
|
keywords: [],
|
||||||
language: "zh-CN"
|
language: "zh-CN"
|
||||||
},
|
},
|
||||||
|
semanticDocument: {
|
||||||
|
schemaVersion: 1,
|
||||||
|
titlePolicy: {
|
||||||
|
metadataTitle: "suppress",
|
||||||
|
firstBodyHeading: "keep"
|
||||||
|
},
|
||||||
|
regions: []
|
||||||
|
},
|
||||||
features: ["table"],
|
features: ["table"],
|
||||||
warnings: []
|
warnings: []
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
"@mdit/plugin-tasklist": "^1.0.1",
|
"@mdit/plugin-tasklist": "^1.0.1",
|
||||||
"@md-to-pdf/core": "0.1.0",
|
"@md-to-pdf/core": "0.1.0",
|
||||||
"@md-to-pdf/markdown-echarts": "0.1.0",
|
"@md-to-pdf/markdown-echarts": "0.1.0",
|
||||||
|
"@md-to-pdf/semantic-document": "0.1.0",
|
||||||
"gray-matter": "^4.0.3",
|
"gray-matter": "^4.0.3",
|
||||||
"highlight.js": "^11.11.1",
|
"highlight.js": "^11.11.1",
|
||||||
"markdown-it": "^14.3.0",
|
"markdown-it": "^14.3.0",
|
||||||
|
|||||||
@@ -1,280 +1,226 @@
|
|||||||
import type {
|
import type {
|
||||||
DocumentProfile,
|
SemanticDocumentModel,
|
||||||
MarkdownDocumentMetadata
|
SemanticDocumentNode,
|
||||||
|
SemanticDocumentRole
|
||||||
} from "@md-to-pdf/core";
|
} from "@md-to-pdf/core";
|
||||||
|
|
||||||
export interface RenderedDocumentStructure {
|
export interface RenderedDocumentStructure {
|
||||||
profile: DocumentProfile["profile"];
|
profile: NonNullable<SemanticDocumentModel["profile"]>;
|
||||||
prefixHtml: string;
|
prefixHtml: string;
|
||||||
suffixHtml: string;
|
suffixHtml: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface HtmlRole {
|
||||||
|
tagName: string;
|
||||||
|
className: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const htmlRoles: Readonly<Record<SemanticDocumentRole, HtmlRole>> = {
|
||||||
|
"official-masthead": {
|
||||||
|
tagName: "header",
|
||||||
|
className: "doc-masthead doc-masthead-official"
|
||||||
|
},
|
||||||
|
"official-classification": {
|
||||||
|
tagName: "div",
|
||||||
|
className: "doc-classification"
|
||||||
|
},
|
||||||
|
"official-secrecy": {
|
||||||
|
tagName: "span",
|
||||||
|
className: "doc-secrecy"
|
||||||
|
},
|
||||||
|
"official-urgency": {
|
||||||
|
tagName: "span",
|
||||||
|
className: "doc-urgency"
|
||||||
|
},
|
||||||
|
"official-issuer": {
|
||||||
|
tagName: "p",
|
||||||
|
className: "doc-issuer"
|
||||||
|
},
|
||||||
|
"official-issue-row": {
|
||||||
|
tagName: "div",
|
||||||
|
className: "doc-issue-row"
|
||||||
|
},
|
||||||
|
"official-number": {
|
||||||
|
tagName: "span",
|
||||||
|
className: "doc-number"
|
||||||
|
},
|
||||||
|
"official-signatory": {
|
||||||
|
tagName: "span",
|
||||||
|
className: "doc-signatory"
|
||||||
|
},
|
||||||
|
"official-title": {
|
||||||
|
tagName: "h1",
|
||||||
|
className: "doc-title"
|
||||||
|
},
|
||||||
|
"official-signature": {
|
||||||
|
tagName: "section",
|
||||||
|
className: "doc-signature"
|
||||||
|
},
|
||||||
|
"official-signature-issuer": {
|
||||||
|
tagName: "p",
|
||||||
|
className: "doc-signature-issuer"
|
||||||
|
},
|
||||||
|
"official-signature-date": {
|
||||||
|
tagName: "time",
|
||||||
|
className: "doc-signature-date"
|
||||||
|
},
|
||||||
|
"official-edition": {
|
||||||
|
tagName: "footer",
|
||||||
|
className: "doc-edition"
|
||||||
|
},
|
||||||
|
"official-copy-to": {
|
||||||
|
tagName: "p",
|
||||||
|
className: "doc-copy-to"
|
||||||
|
},
|
||||||
|
"official-printing-row": {
|
||||||
|
tagName: "div",
|
||||||
|
className: "doc-printing-row"
|
||||||
|
},
|
||||||
|
"official-printing-office": {
|
||||||
|
tagName: "span",
|
||||||
|
className: "doc-printing-office"
|
||||||
|
},
|
||||||
|
"official-printing-date": {
|
||||||
|
tagName: "time",
|
||||||
|
className: "doc-printing-date"
|
||||||
|
},
|
||||||
|
"briefing-masthead": {
|
||||||
|
tagName: "header",
|
||||||
|
className: "doc-masthead doc-masthead-briefing"
|
||||||
|
},
|
||||||
|
"briefing-masthead-text": {
|
||||||
|
tagName: "p",
|
||||||
|
className: "doc-briefing-masthead"
|
||||||
|
},
|
||||||
|
"briefing-meta": {
|
||||||
|
tagName: "div",
|
||||||
|
className: "doc-briefing-meta"
|
||||||
|
},
|
||||||
|
"briefing-issue": {
|
||||||
|
tagName: "span",
|
||||||
|
className: "doc-briefing-issue"
|
||||||
|
},
|
||||||
|
"briefing-publisher": {
|
||||||
|
tagName: "span",
|
||||||
|
className: "doc-briefing-publisher"
|
||||||
|
},
|
||||||
|
"briefing-signatory": {
|
||||||
|
tagName: "span",
|
||||||
|
className: "doc-briefing-signatory"
|
||||||
|
},
|
||||||
|
"briefing-date": {
|
||||||
|
tagName: "time",
|
||||||
|
className: "doc-briefing-date"
|
||||||
|
},
|
||||||
|
"briefing-title": {
|
||||||
|
tagName: "h1",
|
||||||
|
className: "doc-title"
|
||||||
|
},
|
||||||
|
"briefing-contact": {
|
||||||
|
tagName: "footer",
|
||||||
|
className: "doc-briefing-contact"
|
||||||
|
},
|
||||||
|
"project-report-cover": {
|
||||||
|
tagName: "header",
|
||||||
|
className: "doc-cover doc-cover-project-report"
|
||||||
|
},
|
||||||
|
"project-report-project-name": {
|
||||||
|
tagName: "p",
|
||||||
|
className: "doc-cover-project-name"
|
||||||
|
},
|
||||||
|
"project-report-title": {
|
||||||
|
tagName: "h1",
|
||||||
|
className: "doc-cover-title"
|
||||||
|
},
|
||||||
|
"project-report-version": {
|
||||||
|
tagName: "p",
|
||||||
|
className: "doc-cover-version"
|
||||||
|
},
|
||||||
|
"project-report-owner": {
|
||||||
|
tagName: "p",
|
||||||
|
className: "doc-cover-owner"
|
||||||
|
},
|
||||||
|
"project-report-prepared-by": {
|
||||||
|
tagName: "p",
|
||||||
|
className: "doc-cover-prepared-by"
|
||||||
|
},
|
||||||
|
"project-report-date": {
|
||||||
|
tagName: "time",
|
||||||
|
className: "doc-cover-date"
|
||||||
|
},
|
||||||
|
"tender-cover": {
|
||||||
|
tagName: "header",
|
||||||
|
className: "doc-cover doc-cover-tender"
|
||||||
|
},
|
||||||
|
"tender-copy-mark": {
|
||||||
|
tagName: "p",
|
||||||
|
className: "doc-cover-copy-mark"
|
||||||
|
},
|
||||||
|
"tender-project-name": {
|
||||||
|
tagName: "p",
|
||||||
|
className: "doc-cover-project-name"
|
||||||
|
},
|
||||||
|
"tender-project-number": {
|
||||||
|
tagName: "p",
|
||||||
|
className: "doc-cover-project-number"
|
||||||
|
},
|
||||||
|
"tender-title": {
|
||||||
|
tagName: "h1",
|
||||||
|
className: "doc-cover-title"
|
||||||
|
},
|
||||||
|
"tender-volume": {
|
||||||
|
tagName: "p",
|
||||||
|
className: "doc-cover-volume"
|
||||||
|
},
|
||||||
|
"tender-bidder": {
|
||||||
|
tagName: "p",
|
||||||
|
className: "doc-cover-bidder"
|
||||||
|
},
|
||||||
|
"tender-representative": {
|
||||||
|
tagName: "p",
|
||||||
|
className: "doc-cover-representative"
|
||||||
|
},
|
||||||
|
"tender-date": {
|
||||||
|
tagName: "time",
|
||||||
|
className: "doc-cover-date"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function renderNode(node: SemanticDocumentNode): string {
|
||||||
|
const role = htmlRoles[node.role];
|
||||||
|
const content =
|
||||||
|
node.kind === "group"
|
||||||
|
? node.children.map(renderNode).join("")
|
||||||
|
: `${node.label ? `<span>${escapeHtml(node.label)}</span>` : ""}${escapeHtml(node.text)}`;
|
||||||
|
return `<${role.tagName} class="${role.className}">${content}</${role.tagName}>`;
|
||||||
|
}
|
||||||
|
|
||||||
export function renderDocumentStructure(
|
export function renderDocumentStructure(
|
||||||
metadata: MarkdownDocumentMetadata
|
model: SemanticDocumentModel
|
||||||
): RenderedDocumentStructure | undefined {
|
): RenderedDocumentStructure | undefined {
|
||||||
const document = metadata.document;
|
if (!model.profile) {
|
||||||
if (!document) {
|
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
const prefixHtml = model.regions
|
||||||
switch (document.profile) {
|
.filter(
|
||||||
case "official":
|
(region) =>
|
||||||
return renderOfficialStructure(metadata.title, document);
|
region.kind === "cover" || region.kind === "prefix"
|
||||||
case "briefing":
|
|
||||||
return renderBriefingStructure(metadata.title, document);
|
|
||||||
case "project-report":
|
|
||||||
return renderProjectReportStructure(metadata.title, document);
|
|
||||||
case "tender":
|
|
||||||
return renderTenderStructure(metadata.title, document);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderOfficialStructure(
|
|
||||||
title: string,
|
|
||||||
document: Extract<DocumentProfile, { profile: "official" }>
|
|
||||||
): RenderedDocumentStructure {
|
|
||||||
const classification = joinParts(
|
|
||||||
[
|
|
||||||
renderText("span", "doc-secrecy", document.secrecy),
|
|
||||||
renderText("span", "doc-urgency", document.urgency)
|
|
||||||
],
|
|
||||||
"doc-classification"
|
|
||||||
);
|
|
||||||
const issueRow = joinParts(
|
|
||||||
[
|
|
||||||
renderText("span", "doc-number", document.number),
|
|
||||||
renderLabeledText(
|
|
||||||
"span",
|
|
||||||
"doc-signatory",
|
|
||||||
"签发人:",
|
|
||||||
document.signatory
|
|
||||||
)
|
)
|
||||||
],
|
.flatMap((region) => region.nodes)
|
||||||
"doc-issue-row"
|
.map(renderNode)
|
||||||
);
|
.join("");
|
||||||
const masthead = joinParts(
|
const suffixHtml = model.regions
|
||||||
[
|
.filter((region) => region.kind === "suffix")
|
||||||
classification,
|
.flatMap((region) => region.nodes)
|
||||||
renderText("p", "doc-issuer", document.issuer),
|
.map(renderNode)
|
||||||
issueRow
|
.join("");
|
||||||
],
|
|
||||||
"doc-masthead doc-masthead-official",
|
|
||||||
"header"
|
|
||||||
);
|
|
||||||
const signature = joinParts(
|
|
||||||
[
|
|
||||||
renderText("p", "doc-signature-issuer", document.issuer),
|
|
||||||
renderTime("doc-signature-date", document.date)
|
|
||||||
],
|
|
||||||
"doc-signature",
|
|
||||||
"section"
|
|
||||||
);
|
|
||||||
const edition = joinParts(
|
|
||||||
[
|
|
||||||
document.copyTo?.length
|
|
||||||
? `<p class="doc-copy-to"><span>抄送:</span>${escapeHtml(document.copyTo.join("、"))}</p>`
|
|
||||||
: "",
|
|
||||||
joinParts(
|
|
||||||
[
|
|
||||||
renderText(
|
|
||||||
"span",
|
|
||||||
"doc-printing-office",
|
|
||||||
document.printingOffice
|
|
||||||
),
|
|
||||||
document.date
|
|
||||||
? `<time class="doc-printing-date">${escapeHtml(document.date)}印发</time>`
|
|
||||||
: ""
|
|
||||||
],
|
|
||||||
"doc-printing-row"
|
|
||||||
)
|
|
||||||
],
|
|
||||||
"doc-edition",
|
|
||||||
"footer"
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
profile: document.profile,
|
profile: model.profile,
|
||||||
prefixHtml:
|
prefixHtml,
|
||||||
masthead + renderText("h1", "doc-title", title),
|
suffixHtml
|
||||||
suffixHtml: signature + edition
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderBriefingStructure(
|
|
||||||
title: string,
|
|
||||||
document: Extract<DocumentProfile, { profile: "briefing" }>
|
|
||||||
): RenderedDocumentStructure {
|
|
||||||
const details = joinParts(
|
|
||||||
[
|
|
||||||
renderText("span", "doc-briefing-issue", document.issue),
|
|
||||||
renderText(
|
|
||||||
"span",
|
|
||||||
"doc-briefing-publisher",
|
|
||||||
document.publisher
|
|
||||||
),
|
|
||||||
renderLabeledText(
|
|
||||||
"span",
|
|
||||||
"doc-briefing-signatory",
|
|
||||||
"签发:",
|
|
||||||
document.signatory
|
|
||||||
),
|
|
||||||
renderTime("doc-briefing-date", document.date)
|
|
||||||
],
|
|
||||||
"doc-briefing-meta"
|
|
||||||
);
|
|
||||||
const masthead = joinParts(
|
|
||||||
[
|
|
||||||
renderText(
|
|
||||||
"p",
|
|
||||||
"doc-briefing-masthead",
|
|
||||||
document.masthead
|
|
||||||
),
|
|
||||||
details
|
|
||||||
],
|
|
||||||
"doc-masthead doc-masthead-briefing",
|
|
||||||
"header"
|
|
||||||
);
|
|
||||||
const contact = renderText(
|
|
||||||
"footer",
|
|
||||||
"doc-briefing-contact",
|
|
||||||
document.contact
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
profile: document.profile,
|
|
||||||
prefixHtml:
|
|
||||||
masthead + renderText("h1", "doc-title", title),
|
|
||||||
suffixHtml: contact
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderProjectReportStructure(
|
|
||||||
title: string,
|
|
||||||
document: Extract<
|
|
||||||
DocumentProfile,
|
|
||||||
{ profile: "project-report" }
|
|
||||||
>
|
|
||||||
): RenderedDocumentStructure {
|
|
||||||
const cover = joinParts(
|
|
||||||
[
|
|
||||||
renderText(
|
|
||||||
"p",
|
|
||||||
"doc-cover-project-name",
|
|
||||||
document.projectName
|
|
||||||
),
|
|
||||||
renderText(
|
|
||||||
"h1",
|
|
||||||
"doc-cover-title",
|
|
||||||
document.documentType || title
|
|
||||||
),
|
|
||||||
renderText("p", "doc-cover-version", document.version),
|
|
||||||
renderLabeledText(
|
|
||||||
"p",
|
|
||||||
"doc-cover-owner",
|
|
||||||
"建设单位:",
|
|
||||||
document.owner
|
|
||||||
),
|
|
||||||
renderLabeledText(
|
|
||||||
"p",
|
|
||||||
"doc-cover-prepared-by",
|
|
||||||
"编制单位:",
|
|
||||||
document.preparedBy
|
|
||||||
),
|
|
||||||
renderTime("doc-cover-date", document.date)
|
|
||||||
],
|
|
||||||
"doc-cover doc-cover-project-report",
|
|
||||||
"header"
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
profile: document.profile,
|
|
||||||
prefixHtml: cover,
|
|
||||||
suffixHtml: ""
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderTenderStructure(
|
|
||||||
title: string,
|
|
||||||
document: Extract<DocumentProfile, { profile: "tender" }>
|
|
||||||
): RenderedDocumentStructure {
|
|
||||||
const cover = joinParts(
|
|
||||||
[
|
|
||||||
renderText("p", "doc-cover-copy-mark", document.copyMark),
|
|
||||||
renderText(
|
|
||||||
"p",
|
|
||||||
"doc-cover-project-name",
|
|
||||||
document.projectName
|
|
||||||
),
|
|
||||||
renderText(
|
|
||||||
"p",
|
|
||||||
"doc-cover-project-number",
|
|
||||||
document.projectNumber
|
|
||||||
),
|
|
||||||
renderText("h1", "doc-cover-title", title),
|
|
||||||
renderText("p", "doc-cover-volume", document.volume),
|
|
||||||
renderLabeledText(
|
|
||||||
"p",
|
|
||||||
"doc-cover-bidder",
|
|
||||||
"投标人:",
|
|
||||||
document.bidder
|
|
||||||
),
|
|
||||||
renderLabeledText(
|
|
||||||
"p",
|
|
||||||
"doc-cover-representative",
|
|
||||||
"法定代表人或授权代表:",
|
|
||||||
document.representative
|
|
||||||
),
|
|
||||||
renderTime("doc-cover-date", document.date)
|
|
||||||
],
|
|
||||||
"doc-cover doc-cover-tender",
|
|
||||||
"header"
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
profile: document.profile,
|
|
||||||
prefixHtml: cover,
|
|
||||||
suffixHtml: ""
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderText(
|
|
||||||
tagName: string,
|
|
||||||
className: string,
|
|
||||||
value: string | undefined
|
|
||||||
) {
|
|
||||||
return value
|
|
||||||
? `<${tagName} class="${className}">${escapeHtml(value)}</${tagName}>`
|
|
||||||
: "";
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderLabeledText(
|
|
||||||
tagName: string,
|
|
||||||
className: string,
|
|
||||||
label: string,
|
|
||||||
value: string | undefined
|
|
||||||
) {
|
|
||||||
return value
|
|
||||||
? `<${tagName} class="${className}"><span>${label}</span>${escapeHtml(value)}</${tagName}>`
|
|
||||||
: "";
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderTime(className: string, value: string | undefined) {
|
|
||||||
return value
|
|
||||||
? `<time class="${className}">${escapeHtml(value)}</time>`
|
|
||||||
: "";
|
|
||||||
}
|
|
||||||
|
|
||||||
function joinParts(
|
|
||||||
parts: readonly string[],
|
|
||||||
className: string,
|
|
||||||
tagName = "div"
|
|
||||||
) {
|
|
||||||
const content = parts.filter(Boolean).join("");
|
|
||||||
return content
|
|
||||||
? `<${tagName} class="${className}">${content}</${tagName}>`
|
|
||||||
: "";
|
|
||||||
}
|
|
||||||
|
|
||||||
function escapeHtml(value: string) {
|
function escapeHtml(value: string) {
|
||||||
return value.replace(
|
return value.replace(
|
||||||
/[&<>"']/gu,
|
/[&<>"']/gu,
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
documentProfileSchema,
|
documentProfileSchema,
|
||||||
isSafeDocumentLink
|
isSafeDocumentLink
|
||||||
} from "@md-to-pdf/core";
|
} from "@md-to-pdf/core";
|
||||||
|
import { createSemanticDocumentModel } from "@md-to-pdf/semantic-document";
|
||||||
import {
|
import {
|
||||||
markdownItECharts,
|
markdownItECharts,
|
||||||
type MarkdownItEChartsErrorContext
|
type MarkdownItEChartsErrorContext
|
||||||
@@ -348,12 +349,17 @@ export function renderMarkdown(
|
|||||||
};
|
};
|
||||||
const rendered = markdown.render(parsed.content, environment);
|
const rendered = markdown.render(parsed.content, environment);
|
||||||
const bodyHtml = sanitizeHtml(rendered, safeHtmlOptions);
|
const bodyHtml = sanitizeHtml(rendered, safeHtmlOptions);
|
||||||
|
const firstBodyHeading = extractFirstHeading(parsed.content);
|
||||||
const metadata = normalizeMetadata(
|
const metadata = normalizeMetadata(
|
||||||
parsed.data,
|
parsed.data,
|
||||||
options.language ?? "zh-CN",
|
options.language ?? "zh-CN",
|
||||||
extractFirstHeading(parsed.content)
|
firstBodyHeading
|
||||||
);
|
);
|
||||||
const structure = renderDocumentStructure(metadata);
|
const semanticDocument = createSemanticDocumentModel({
|
||||||
|
metadata,
|
||||||
|
firstBodyHeading
|
||||||
|
});
|
||||||
|
const structure = renderDocumentStructure(semanticDocument);
|
||||||
const profileAttribute = structure
|
const profileAttribute = structure
|
||||||
? ` data-document-profile="${structure.profile}"`
|
? ` data-document-profile="${structure.profile}"`
|
||||||
: "";
|
: "";
|
||||||
@@ -364,6 +370,7 @@ export function renderMarkdown(
|
|||||||
articleHtml,
|
articleHtml,
|
||||||
bodyHtml,
|
bodyHtml,
|
||||||
metadata,
|
metadata,
|
||||||
|
semanticDocument,
|
||||||
features: detectFeatures(bodyHtml),
|
features: detectFeatures(bodyHtml),
|
||||||
warnings
|
warnings
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -32,6 +32,14 @@ const answer = 42;
|
|||||||
expect(result.bodyHtml).toContain("hljs-keyword");
|
expect(result.bodyHtml).toContain("hljs-keyword");
|
||||||
expect(result.bodyHtml).toContain("footnote");
|
expect(result.bodyHtml).toContain("footnote");
|
||||||
expect(result.metadata.title).toBe("示例文档");
|
expect(result.metadata.title).toBe("示例文档");
|
||||||
|
expect(result.semanticDocument).toEqual({
|
||||||
|
schemaVersion: 1,
|
||||||
|
titlePolicy: {
|
||||||
|
metadataTitle: "suppress",
|
||||||
|
firstBodyHeading: "keep"
|
||||||
|
},
|
||||||
|
regions: []
|
||||||
|
});
|
||||||
expect(result.features).toEqual(
|
expect(result.features).toEqual(
|
||||||
expect.arrayContaining(["code", "table", "task-list", "footnote"])
|
expect.arrayContaining(["code", "table", "task-list", "footnote"])
|
||||||
);
|
);
|
||||||
@@ -152,6 +160,8 @@ document:
|
|||||||
expect(result.articleHtml).toContain(
|
expect(result.articleHtml).toContain(
|
||||||
'<footer class="doc-edition">'
|
'<footer class="doc-edition">'
|
||||||
);
|
);
|
||||||
|
expect(result.semanticDocument.profile).toBe("official");
|
||||||
|
expect(result.semanticDocument.regions).toHaveLength(2);
|
||||||
expect(result.bodyHtml).not.toContain("doc-masthead");
|
expect(result.bodyHtml).not.toContain("doc-masthead");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# @md-to-pdf/semantic-document
|
||||||
|
|
||||||
|
统一语义文档模型构建器。该包把规范化 Front Matter 和正文标题信息转换为
|
||||||
|
主题无关、渲染器无关的结构树,供 HTML/PDF 与 DOCX 共用。
|
||||||
|
|
||||||
|
当前支持:
|
||||||
|
|
||||||
|
- 普通文档标题去重策略;
|
||||||
|
- 政企公文版头、文号、签发人、落款和版记;
|
||||||
|
- 工作简报版头、期号、发布单位、签发和联系信息;
|
||||||
|
- 项目报告独立封面;
|
||||||
|
- 标书独立封面;
|
||||||
|
- 封面无页眉页脚、不显示页码、正文重新从 1 编号的分节意图。
|
||||||
|
|
||||||
|
本包不生成 HTML、不调用 Pandoc、不处理 OOXML,也不包含主题 ID 分支。
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"name": "@md-to-pdf/semantic-document",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"main": "./dist/index.js",
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"import": "./dist/index.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"dev": "tsc -p tsconfig.json --watch --preserveWatchOutput",
|
||||||
|
"build": "tsc -p tsconfig.json",
|
||||||
|
"test": "vitest run",
|
||||||
|
"typecheck": "tsc -p tsconfig.json --noEmit --pretty false"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@md-to-pdf/core": "0.1.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"vitest": "^4.1.10"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./model-builder.js";
|
||||||
@@ -0,0 +1,268 @@
|
|||||||
|
import {
|
||||||
|
semanticDocumentModelSchema,
|
||||||
|
type DocumentProfile,
|
||||||
|
type MarkdownDocumentMetadata,
|
||||||
|
type SemanticDocumentGroupNode,
|
||||||
|
type SemanticDocumentModel,
|
||||||
|
type SemanticDocumentNode,
|
||||||
|
type SemanticDocumentRegion,
|
||||||
|
type SemanticDocumentRole,
|
||||||
|
type SemanticDocumentTextNode
|
||||||
|
} from "@md-to-pdf/core";
|
||||||
|
|
||||||
|
export interface CreateSemanticDocumentModelOptions {
|
||||||
|
metadata: MarkdownDocumentMetadata;
|
||||||
|
firstBodyHeading?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeTitle(value: string | undefined): string {
|
||||||
|
return (value ?? "")
|
||||||
|
.normalize("NFKC")
|
||||||
|
.replace(/\s+/gu, " ")
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function text(
|
||||||
|
role: SemanticDocumentRole,
|
||||||
|
value: string | undefined,
|
||||||
|
label?: string
|
||||||
|
): SemanticDocumentTextNode | undefined {
|
||||||
|
const normalized = value?.trim();
|
||||||
|
if (!normalized) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
kind: "text",
|
||||||
|
role,
|
||||||
|
text: normalized,
|
||||||
|
...(label ? { label } : {})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function group(
|
||||||
|
role: SemanticDocumentRole,
|
||||||
|
children: readonly (SemanticDocumentNode | undefined)[]
|
||||||
|
): SemanticDocumentGroupNode | undefined {
|
||||||
|
const resolved = children.filter(
|
||||||
|
(child): child is SemanticDocumentNode => child !== undefined
|
||||||
|
);
|
||||||
|
return resolved.length
|
||||||
|
? {
|
||||||
|
kind: "group",
|
||||||
|
role,
|
||||||
|
children: resolved
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function region(
|
||||||
|
kind: SemanticDocumentRegion["kind"],
|
||||||
|
nodes: readonly (SemanticDocumentNode | undefined)[],
|
||||||
|
cover = false
|
||||||
|
): SemanticDocumentRegion | undefined {
|
||||||
|
const resolved = nodes.filter(
|
||||||
|
(node): node is SemanticDocumentNode => node !== undefined
|
||||||
|
);
|
||||||
|
if (!resolved.length) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
kind,
|
||||||
|
nodes: resolved,
|
||||||
|
...(cover
|
||||||
|
? {
|
||||||
|
section: {
|
||||||
|
headerFooter: "none",
|
||||||
|
pageNumber: "hidden",
|
||||||
|
breakAfter: "next-page",
|
||||||
|
followingPageNumberStart: 1
|
||||||
|
} as const
|
||||||
|
}
|
||||||
|
: {})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function officialRegions(
|
||||||
|
title: string,
|
||||||
|
document: Extract<DocumentProfile, { profile: "official" }>
|
||||||
|
): SemanticDocumentRegion[] {
|
||||||
|
const classification = group("official-classification", [
|
||||||
|
text("official-secrecy", document.secrecy),
|
||||||
|
text("official-urgency", document.urgency)
|
||||||
|
]);
|
||||||
|
const issueRow = group("official-issue-row", [
|
||||||
|
text("official-number", document.number),
|
||||||
|
text("official-signatory", document.signatory, "签发人:")
|
||||||
|
]);
|
||||||
|
const masthead = group("official-masthead", [
|
||||||
|
classification,
|
||||||
|
text("official-issuer", document.issuer),
|
||||||
|
issueRow
|
||||||
|
]);
|
||||||
|
const signature = group("official-signature", [
|
||||||
|
text("official-signature-issuer", document.issuer),
|
||||||
|
text("official-signature-date", document.date)
|
||||||
|
]);
|
||||||
|
const printingRow = group("official-printing-row", [
|
||||||
|
text("official-printing-office", document.printingOffice),
|
||||||
|
text(
|
||||||
|
"official-printing-date",
|
||||||
|
document.date ? `${document.date}印发` : undefined
|
||||||
|
)
|
||||||
|
]);
|
||||||
|
const edition = group("official-edition", [
|
||||||
|
text(
|
||||||
|
"official-copy-to",
|
||||||
|
document.copyTo?.join("、"),
|
||||||
|
"抄送:"
|
||||||
|
),
|
||||||
|
printingRow
|
||||||
|
]);
|
||||||
|
return [
|
||||||
|
region("prefix", [
|
||||||
|
masthead,
|
||||||
|
text("official-title", title)
|
||||||
|
]),
|
||||||
|
region("suffix", [signature, edition])
|
||||||
|
].filter(
|
||||||
|
(item): item is SemanticDocumentRegion => item !== undefined
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function briefingRegions(
|
||||||
|
title: string,
|
||||||
|
document: Extract<DocumentProfile, { profile: "briefing" }>
|
||||||
|
): SemanticDocumentRegion[] {
|
||||||
|
const details = group("briefing-meta", [
|
||||||
|
text("briefing-issue", document.issue),
|
||||||
|
text("briefing-publisher", document.publisher),
|
||||||
|
text("briefing-signatory", document.signatory, "签发:"),
|
||||||
|
text("briefing-date", document.date)
|
||||||
|
]);
|
||||||
|
const masthead = group("briefing-masthead", [
|
||||||
|
text("briefing-masthead-text", document.masthead),
|
||||||
|
details
|
||||||
|
]);
|
||||||
|
return [
|
||||||
|
region("prefix", [
|
||||||
|
masthead,
|
||||||
|
text("briefing-title", title)
|
||||||
|
]),
|
||||||
|
region("suffix", [
|
||||||
|
text("briefing-contact", document.contact)
|
||||||
|
])
|
||||||
|
].filter(
|
||||||
|
(item): item is SemanticDocumentRegion => item !== undefined
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function projectReportRegions(
|
||||||
|
title: string,
|
||||||
|
document: Extract<
|
||||||
|
DocumentProfile,
|
||||||
|
{ profile: "project-report" }
|
||||||
|
>
|
||||||
|
): SemanticDocumentRegion[] {
|
||||||
|
const cover = group("project-report-cover", [
|
||||||
|
text(
|
||||||
|
"project-report-project-name",
|
||||||
|
document.projectName
|
||||||
|
),
|
||||||
|
text(
|
||||||
|
"project-report-title",
|
||||||
|
document.documentType || title
|
||||||
|
),
|
||||||
|
text("project-report-version", document.version),
|
||||||
|
text("project-report-owner", document.owner, "建设单位:"),
|
||||||
|
text(
|
||||||
|
"project-report-prepared-by",
|
||||||
|
document.preparedBy,
|
||||||
|
"编制单位:"
|
||||||
|
),
|
||||||
|
text("project-report-date", document.date)
|
||||||
|
]);
|
||||||
|
const coverRegion = region("cover", [cover], true);
|
||||||
|
return coverRegion ? [coverRegion] : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function tenderRegions(
|
||||||
|
title: string,
|
||||||
|
document: Extract<DocumentProfile, { profile: "tender" }>
|
||||||
|
): SemanticDocumentRegion[] {
|
||||||
|
const cover = group("tender-cover", [
|
||||||
|
text("tender-copy-mark", document.copyMark),
|
||||||
|
text("tender-project-name", document.projectName),
|
||||||
|
text("tender-project-number", document.projectNumber),
|
||||||
|
text("tender-title", title),
|
||||||
|
text("tender-volume", document.volume),
|
||||||
|
text("tender-bidder", document.bidder, "投标人:"),
|
||||||
|
text(
|
||||||
|
"tender-representative",
|
||||||
|
document.representative,
|
||||||
|
"法定代表人或授权代表:"
|
||||||
|
),
|
||||||
|
text("tender-date", document.date)
|
||||||
|
]);
|
||||||
|
const coverRegion = region("cover", [cover], true);
|
||||||
|
return coverRegion ? [coverRegion] : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function createRegions(
|
||||||
|
metadata: MarkdownDocumentMetadata
|
||||||
|
): SemanticDocumentRegion[] {
|
||||||
|
const document = metadata.document;
|
||||||
|
if (!document) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
switch (document.profile) {
|
||||||
|
case "official":
|
||||||
|
return officialRegions(metadata.title, document);
|
||||||
|
case "briefing":
|
||||||
|
return briefingRegions(metadata.title, document);
|
||||||
|
case "project-report":
|
||||||
|
return projectReportRegions(metadata.title, document);
|
||||||
|
case "tender":
|
||||||
|
return tenderRegions(metadata.title, document);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function emittedTitle(metadata: MarkdownDocumentMetadata): string {
|
||||||
|
const document = metadata.document;
|
||||||
|
if (!document) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return document.profile === "project-report"
|
||||||
|
? document.documentType || metadata.title
|
||||||
|
: metadata.title;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createSemanticDocumentModel(
|
||||||
|
options: CreateSemanticDocumentModelOptions
|
||||||
|
): SemanticDocumentModel {
|
||||||
|
const { metadata } = options;
|
||||||
|
const firstHeading = normalizeTitle(options.firstBodyHeading);
|
||||||
|
const metadataTitle = normalizeTitle(metadata.title);
|
||||||
|
const structuralTitle = normalizeTitle(emittedTitle(metadata));
|
||||||
|
const hasStructure = metadata.document !== undefined;
|
||||||
|
const duplicatedMetadataTitle =
|
||||||
|
Boolean(metadataTitle) && metadataTitle === firstHeading;
|
||||||
|
const duplicatedStructuralTitle =
|
||||||
|
Boolean(structuralTitle) && structuralTitle === firstHeading;
|
||||||
|
return semanticDocumentModelSchema.parse({
|
||||||
|
schemaVersion: 1,
|
||||||
|
...(metadata.document
|
||||||
|
? { profile: metadata.document.profile }
|
||||||
|
: {}),
|
||||||
|
titlePolicy: {
|
||||||
|
metadataTitle:
|
||||||
|
hasStructure || duplicatedMetadataTitle
|
||||||
|
? "suppress"
|
||||||
|
: "emit",
|
||||||
|
firstBodyHeading:
|
||||||
|
hasStructure && duplicatedStructuralTitle
|
||||||
|
? "suppress"
|
||||||
|
: "keep"
|
||||||
|
},
|
||||||
|
regions: createRegions(metadata)
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
import {
|
||||||
|
semanticDocumentModelSchema,
|
||||||
|
type MarkdownDocumentMetadata
|
||||||
|
} from "@md-to-pdf/core";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { createSemanticDocumentModel } from "../src/index.js";
|
||||||
|
|
||||||
|
function metadata(
|
||||||
|
input: Partial<MarkdownDocumentMetadata> = {}
|
||||||
|
): MarkdownDocumentMetadata {
|
||||||
|
return {
|
||||||
|
title: "",
|
||||||
|
author: "",
|
||||||
|
subject: "",
|
||||||
|
keywords: [],
|
||||||
|
language: "zh-CN",
|
||||||
|
...input
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("统一语义文档模型", () => {
|
||||||
|
it("为普通文档记录标题去重策略但不创建结构区域", () => {
|
||||||
|
const model = createSemanticDocumentModel({
|
||||||
|
metadata: metadata({ title: "项目报告" }),
|
||||||
|
firstBodyHeading: " 项目报告 "
|
||||||
|
});
|
||||||
|
expect(model).toEqual({
|
||||||
|
schemaVersion: 1,
|
||||||
|
titlePolicy: {
|
||||||
|
metadataTitle: "suppress",
|
||||||
|
firstBodyHeading: "keep"
|
||||||
|
},
|
||||||
|
regions: []
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("完整表达公文版头、落款和版记的稳定顺序", () => {
|
||||||
|
const model = createSemanticDocumentModel({
|
||||||
|
metadata: metadata({
|
||||||
|
title: "关于推进项目建设的通知",
|
||||||
|
document: {
|
||||||
|
profile: "official",
|
||||||
|
issuer: "示例市人民政府办公室",
|
||||||
|
number: "示例办发〔2026〕12号",
|
||||||
|
secrecy: "内部",
|
||||||
|
urgency: "加急",
|
||||||
|
signatory: "张三",
|
||||||
|
date: "2026年7月30日",
|
||||||
|
copyTo: ["市发展改革委", "市财政局"],
|
||||||
|
printingOffice: "示例市人民政府办公室"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
expect(model.profile).toBe("official");
|
||||||
|
expect(model.regions.map((item) => item.kind)).toEqual([
|
||||||
|
"prefix",
|
||||||
|
"suffix"
|
||||||
|
]);
|
||||||
|
expect(JSON.stringify(model)).toContain("official-masthead");
|
||||||
|
expect(JSON.stringify(model)).toContain("抄送:");
|
||||||
|
expect(JSON.stringify(model)).toContain("2026年7月30日印发");
|
||||||
|
expect(() => semanticDocumentModelSchema.parse(model))
|
||||||
|
.not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("表达简报前后结构并省略空字段", () => {
|
||||||
|
const model = createSemanticDocumentModel({
|
||||||
|
metadata: metadata({
|
||||||
|
title: "工作简报",
|
||||||
|
document: {
|
||||||
|
profile: "briefing",
|
||||||
|
masthead: "项目工作简报",
|
||||||
|
issue: "第12期",
|
||||||
|
contact: "联系人:张三"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
expect(model.regions).toHaveLength(2);
|
||||||
|
expect(JSON.stringify(model)).not.toContain(
|
||||||
|
"briefing-signatory"
|
||||||
|
);
|
||||||
|
expect(JSON.stringify(model)).toContain("briefing-contact");
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
[
|
||||||
|
"project-report",
|
||||||
|
{
|
||||||
|
profile: "project-report",
|
||||||
|
projectName: "智慧园区建设项目",
|
||||||
|
documentType: "可行性研究报告",
|
||||||
|
owner: "示例集团"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"tender",
|
||||||
|
{
|
||||||
|
profile: "tender",
|
||||||
|
copyMark: "正本",
|
||||||
|
projectName: "数据中心建设项目",
|
||||||
|
bidder: "示例科技有限公司"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
] as const)("为 %s 创建独立封面分节意图", (_profile, document) => {
|
||||||
|
const model = createSemanticDocumentModel({
|
||||||
|
metadata: metadata({
|
||||||
|
title: "项目文件",
|
||||||
|
document
|
||||||
|
})
|
||||||
|
});
|
||||||
|
expect(model.regions).toHaveLength(1);
|
||||||
|
expect(model.regions[0]).toMatchObject({
|
||||||
|
kind: "cover",
|
||||||
|
section: {
|
||||||
|
headerFooter: "none",
|
||||||
|
pageNumber: "hidden",
|
||||||
|
breakAfter: "next-page",
|
||||||
|
followingPageNumberStart: 1
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("结构标题与首个 H1 一致时记录正文标题抑制", () => {
|
||||||
|
const model = createSemanticDocumentModel({
|
||||||
|
metadata: metadata({
|
||||||
|
title: "投标文件",
|
||||||
|
document: {
|
||||||
|
profile: "tender",
|
||||||
|
projectName: "示例项目"
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
firstBodyHeading: "投标文件"
|
||||||
|
});
|
||||||
|
expect(model.titlePolicy).toEqual({
|
||||||
|
metadataTitle: "suppress",
|
||||||
|
firstBodyHeading: "suppress"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "NodeNext",
|
||||||
|
"moduleResolution": "NodeNext",
|
||||||
|
"rootDir": "src",
|
||||||
|
"outDir": "dist",
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"sourceMap": true
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src"
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user