@maxoyed/ode-core 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +51 -0
- package/dist/chunk-OUPZEHJO.js +162 -0
- package/dist/chunk-OUPZEHJO.js.map +1 -0
- package/dist/chunk-SYMAPCOS.js +104 -0
- package/dist/chunk-SYMAPCOS.js.map +1 -0
- package/dist/chunk-TRNXHJAU.js +21 -0
- package/dist/chunk-TRNXHJAU.js.map +1 -0
- package/dist/docx/index.d.ts +44 -0
- package/dist/docx/index.js +540 -0
- package/dist/docx/index.js.map +1 -0
- package/dist/elements-Bvueu_TD.d.ts +69 -0
- package/dist/index.d.ts +176 -0
- package/dist/index.js +608 -0
- package/dist/index.js.map +1 -0
- package/dist/pagination/index.d.ts +131 -0
- package/dist/pagination/index.js +34 -0
- package/dist/pagination/index.js.map +1 -0
- package/dist/spec/index.d.ts +69 -0
- package/dist/spec/index.js +45 -0
- package/dist/spec/index.js.map +1 -0
- package/dist/styles.css +189 -0
- package/package.json +89 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dong
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# @maxoyed/ode-core
|
|
2
|
+
|
|
3
|
+
Headless 的中国党政机关**公文编辑器核心**,默认排版即符合 **GB/T 9704-2012《党政机关公文格式》**,基于 [Tiptap](https://tiptap.dev/)(ProseMirror)构建,与前端框架无关。纯前端、离线可用。
|
|
4
|
+
|
|
5
|
+
> 框架封装见 [`@maxoyed/ode-vue`](https://www.npmjs.com/package/@maxoyed/ode-vue) / [`@maxoyed/ode-react`](https://www.npmjs.com/package/@maxoyed/ode-react)。
|
|
6
|
+
|
|
7
|
+
## 安装
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pnpm add @maxoyed/ode-core
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## 用法
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { createOfficialDocumentEditor } from "@maxoyed/ode-core";
|
|
17
|
+
import "@maxoyed/ode-core/styles.css";
|
|
18
|
+
|
|
19
|
+
const editor = createOfficialDocumentEditor({
|
|
20
|
+
element: document.querySelector("#page")!, // 一个 .odoc-page 容器
|
|
21
|
+
pagination: true, // 可选:编辑器内联实时分页
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
editor.chain().focus().setOfficialRole("title").run();
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
子路径导出:
|
|
28
|
+
|
|
29
|
+
- `@maxoyed/ode-core/spec` — GB/T 9704-2012 版式规范(纯数据,零依赖)
|
|
30
|
+
- `@maxoyed/ode-core/pagination` — headless 分页引擎(纯函数)
|
|
31
|
+
- `@maxoyed/ode-core/docx` — docx 导入/导出(`toDocxBlob` / `fromDocx`)
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
import { paginate, blocksFromDoc } from "@maxoyed/ode-core/pagination";
|
|
35
|
+
import { toDocxBlob, fromDocx } from "@maxoyed/ode-core/docx";
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## 能力
|
|
39
|
+
|
|
40
|
+
- 公文版式渲染与编辑(红头、标题、各级标题、正文、署名、版记、页码…)
|
|
41
|
+
- 精确分页:headless 引擎 + 编辑器内联实时分页(行级跨页断行)+ 打印 / 导出 PDF
|
|
42
|
+
- docx 往返:命名样式无损 · 表格(含合并单元格)· 图片 · 印章 · 版记线
|
|
43
|
+
- 字体插槽 `registerFont`:开源字体兜底 + 用户授权字体注入
|
|
44
|
+
|
|
45
|
+
## 字体与版权
|
|
46
|
+
|
|
47
|
+
本库**不内置/不分发**仿宋_GB2312、方正小标宋等商业字体;以 `local()` 优先引用本机公文字体、开源字体兜底,并提供 `registerFont()` 由使用方提供已授权字体。
|
|
48
|
+
|
|
49
|
+
## 许可
|
|
50
|
+
|
|
51
|
+
[MIT](./LICENSE) · 详见仓库 [official-document-editor](https://github.com/maxoyed/official-document-editor)。
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CHARS_PER_LINE,
|
|
3
|
+
ELEMENT_SPEC,
|
|
4
|
+
LINES_PER_PAGE,
|
|
5
|
+
PT_TO_MM,
|
|
6
|
+
TYPE_AREA_MM,
|
|
7
|
+
toPt
|
|
8
|
+
} from "./chunk-SYMAPCOS.js";
|
|
9
|
+
|
|
10
|
+
// src/pagination/paginate.ts
|
|
11
|
+
var TYPE_AREA_HEIGHT_PT = TYPE_AREA_MM.height / PT_TO_MM;
|
|
12
|
+
var BODY_LINE_HEIGHT_PT = TYPE_AREA_HEIGHT_PT / LINES_PER_PAGE;
|
|
13
|
+
var DEFAULT_ROLE = "body";
|
|
14
|
+
function lineHeightPtFor(role) {
|
|
15
|
+
const spec = ELEMENT_SPEC[role] ?? ELEMENT_SPEC[DEFAULT_ROLE];
|
|
16
|
+
return Math.max(BODY_LINE_HEIGHT_PT, toPt(spec.size) * 1.2);
|
|
17
|
+
}
|
|
18
|
+
function charsPerLineFor(role) {
|
|
19
|
+
const spec = ELEMENT_SPEC[role] ?? ELEMENT_SPEC[DEFAULT_ROLE];
|
|
20
|
+
const ratio = toPt("\u4E09\u53F7") / toPt(spec.size);
|
|
21
|
+
const cols = Math.round(CHARS_PER_LINE * ratio);
|
|
22
|
+
const reserved = (spec.marginLeft ?? 0) + (spec.marginRight ?? 0);
|
|
23
|
+
return Math.max(1, cols - reserved);
|
|
24
|
+
}
|
|
25
|
+
function estimateLines(block) {
|
|
26
|
+
const role = block.role ?? DEFAULT_ROLE;
|
|
27
|
+
const spec = ELEMENT_SPEC[role] ?? ELEMENT_SPEC[DEFAULT_ROLE];
|
|
28
|
+
const text = block.text ?? "";
|
|
29
|
+
const len = [...text].length;
|
|
30
|
+
if (len === 0) return 1;
|
|
31
|
+
const perLine = charsPerLineFor(role);
|
|
32
|
+
const indent = spec.indent ?? 0;
|
|
33
|
+
const firstLineCap = Math.max(1, perLine - indent);
|
|
34
|
+
if (len <= firstLineCap) return 1;
|
|
35
|
+
return 1 + Math.ceil((len - firstLineCap) / perLine);
|
|
36
|
+
}
|
|
37
|
+
function paginate(blocks, options = {}) {
|
|
38
|
+
const pageHeight = options.pageHeightPt ?? TYPE_AREA_HEIGHT_PT;
|
|
39
|
+
const pages = [];
|
|
40
|
+
let current = { pageNo: 1, fragments: [], usedHeightPt: 0 };
|
|
41
|
+
const pushPage = () => {
|
|
42
|
+
pages.push(current);
|
|
43
|
+
current = { pageNo: current.pageNo + 1, fragments: [], usedHeightPt: 0 };
|
|
44
|
+
};
|
|
45
|
+
blocks.forEach((block, blockIndex) => {
|
|
46
|
+
const role = block.role ?? DEFAULT_ROLE;
|
|
47
|
+
const lh = lineHeightPtFor(role);
|
|
48
|
+
const totalLines = estimateLines(block);
|
|
49
|
+
if (block.atomic) {
|
|
50
|
+
const blockHeight = totalLines * lh;
|
|
51
|
+
if (current.usedHeightPt + blockHeight > pageHeight && current.fragments.length > 0) {
|
|
52
|
+
pushPage();
|
|
53
|
+
}
|
|
54
|
+
current.fragments.push({ blockIndex, role, startLine: 0, endLine: totalLines });
|
|
55
|
+
current.usedHeightPt += blockHeight;
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
let line = 0;
|
|
59
|
+
while (line < totalLines) {
|
|
60
|
+
const remainingPt = pageHeight - current.usedHeightPt;
|
|
61
|
+
let fit = Math.floor((remainingPt + 1e-6) / lh);
|
|
62
|
+
if (fit <= 0) {
|
|
63
|
+
if (current.fragments.length > 0) {
|
|
64
|
+
pushPage();
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
fit = 1;
|
|
68
|
+
}
|
|
69
|
+
const take = Math.min(fit, totalLines - line);
|
|
70
|
+
current.fragments.push({ blockIndex, role, startLine: line, endLine: line + take });
|
|
71
|
+
current.usedHeightPt += take * lh;
|
|
72
|
+
line += take;
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
if (current.fragments.length > 0 || pages.length === 0) pages.push(current);
|
|
76
|
+
return pages;
|
|
77
|
+
}
|
|
78
|
+
function countPages(blocks, options) {
|
|
79
|
+
return paginate(blocks, options).length;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// src/pagination/page-number.ts
|
|
83
|
+
var DASH = "\u2014";
|
|
84
|
+
var PAGE_NUMBER_OFFSET_MM = 7;
|
|
85
|
+
function formatPageNumber(pageNo) {
|
|
86
|
+
return `${DASH} ${pageNo} ${DASH}`;
|
|
87
|
+
}
|
|
88
|
+
function pageNumberAlign(pageNo) {
|
|
89
|
+
return pageNo % 2 === 1 ? "right" : "left";
|
|
90
|
+
}
|
|
91
|
+
function pageNumberStyle(pageNo) {
|
|
92
|
+
return {
|
|
93
|
+
text: formatPageNumber(pageNo),
|
|
94
|
+
align: pageNumberAlign(pageNo),
|
|
95
|
+
offsetMm: PAGE_NUMBER_OFFSET_MM,
|
|
96
|
+
insetMm: TYPE_AREA_MM.width / 28
|
|
97
|
+
// 每行 28 字,空一字 = 版心宽/28
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// src/pagination/blocks.ts
|
|
102
|
+
function textOf(node) {
|
|
103
|
+
if (node.type === "text") return node.text ?? "";
|
|
104
|
+
if (!node.content) return "";
|
|
105
|
+
return node.content.map(textOf).join("");
|
|
106
|
+
}
|
|
107
|
+
function blocksFromDoc(doc) {
|
|
108
|
+
const top = doc.type === "doc" ? doc.content ?? [] : [doc];
|
|
109
|
+
return top.map((node) => {
|
|
110
|
+
if (node.type === "horizontalRule") {
|
|
111
|
+
return { atomic: true };
|
|
112
|
+
}
|
|
113
|
+
if (node.type === "table" || node.type === "image") {
|
|
114
|
+
return { atomic: true, text: textOf(node) };
|
|
115
|
+
}
|
|
116
|
+
const role = node.attrs?.officialRole;
|
|
117
|
+
return { role, text: textOf(node) };
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// src/pagination/layout.ts
|
|
122
|
+
function computePageBreaks(blocks, options) {
|
|
123
|
+
const { pageContentPx, breakExtraPx } = options;
|
|
124
|
+
const breaks = [];
|
|
125
|
+
if (blocks.length === 0) return { breaks, pageCount: 1 };
|
|
126
|
+
let pageNo = 1;
|
|
127
|
+
let pageStart = blocks[0].top;
|
|
128
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
129
|
+
const b = blocks[i];
|
|
130
|
+
const relBottom = b.top + b.height - pageStart;
|
|
131
|
+
const isFirstOnPage = b.top <= pageStart + 0.5;
|
|
132
|
+
if (relBottom > pageContentPx && !isFirstOnPage) {
|
|
133
|
+
const remaining = pageContentPx - (b.top - pageStart);
|
|
134
|
+
breaks.push({
|
|
135
|
+
beforeIndex: i,
|
|
136
|
+
pageNo: pageNo + 1,
|
|
137
|
+
spacerPx: Math.max(0, remaining) + breakExtraPx
|
|
138
|
+
});
|
|
139
|
+
pageNo += 1;
|
|
140
|
+
pageStart = b.top;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return { breaks, pageCount: pageNo };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export {
|
|
147
|
+
TYPE_AREA_HEIGHT_PT,
|
|
148
|
+
BODY_LINE_HEIGHT_PT,
|
|
149
|
+
lineHeightPtFor,
|
|
150
|
+
charsPerLineFor,
|
|
151
|
+
estimateLines,
|
|
152
|
+
paginate,
|
|
153
|
+
countPages,
|
|
154
|
+
DASH,
|
|
155
|
+
PAGE_NUMBER_OFFSET_MM,
|
|
156
|
+
formatPageNumber,
|
|
157
|
+
pageNumberAlign,
|
|
158
|
+
pageNumberStyle,
|
|
159
|
+
blocksFromDoc,
|
|
160
|
+
computePageBreaks
|
|
161
|
+
};
|
|
162
|
+
//# sourceMappingURL=chunk-OUPZEHJO.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/pagination/paginate.ts","../src/pagination/page-number.ts","../src/pagination/blocks.ts","../src/pagination/layout.ts"],"sourcesContent":["/**\n * Headless 分页引擎(GB/T 9704-2012「每面 22 行」模型)。\n *\n * 纯函数、零 DOM 依赖:给定公文各块(段落/分隔线等)及其要素角色与文字,\n * 依版心高度与各要素字号,按“行”为粒度精确计算分页结果。\n * 可用于:页数计算、打印/导出分页、服务端校验,亦为编辑器可视化分页提供基准。\n *\n * 说明:浏览器内的所见即所得分页应以真实 DOM 测量为准(见 PaginationView),\n * 本引擎给出与规范一致的确定性估算,二者模型一致、结果相互印证。\n */\nimport { ELEMENT_SPEC, type OfficialElement } from \"../spec/elements\";\nimport { toPt, PT_TO_MM } from \"../spec/font-size\";\nimport { LINES_PER_PAGE, TYPE_AREA_MM, CHARS_PER_LINE } from \"../spec/layout\";\n\n/** 版心高度(pt)。 */\nexport const TYPE_AREA_HEIGHT_PT = TYPE_AREA_MM.height / PT_TO_MM;\n/** 正文标准行高(pt):版心高 / 每面行数。 */\nexport const BODY_LINE_HEIGHT_PT = TYPE_AREA_HEIGHT_PT / LINES_PER_PAGE;\n\n/** 待分页的块(与 Tiptap JSON 的顶层节点对应)。 */\nexport interface PaginationBlock {\n /** 公文要素角色;缺省按正文处理 */\n role?: OfficialElement;\n /** 块内纯文本(用于估算行数);分隔线等可为空 */\n text?: string;\n /** 是否为不可断块(如分隔线、表格),不跨页拆分 */\n atomic?: boolean;\n}\n\n/** 分页结果中某页承载的一个片段。 */\nexport interface PageFragment {\n /** 对应输入块的下标 */\n blockIndex: number;\n role: OfficialElement;\n /** 该片段在本块中的起始行(从 0 计) */\n startLine: number;\n /** 该片段在本块中的结束行(不含) */\n endLine: number;\n}\n\nexport interface Page {\n /** 页码,从 1 开始 */\n pageNo: number;\n fragments: PageFragment[];\n /** 本页已用行高合计(pt) */\n usedHeightPt: number;\n}\n\nexport interface PaginateOptions {\n /** 版心可用高度(pt),默认按规范 225mm。 */\n pageHeightPt?: number;\n}\n\nconst DEFAULT_ROLE: OfficialElement = \"body\";\n\n/** 某要素单行的行高(pt):取正文行高与字号约 1.2 倍中的较大者。 */\nexport function lineHeightPtFor(role: OfficialElement): number {\n const spec = ELEMENT_SPEC[role] ?? ELEMENT_SPEC[DEFAULT_ROLE];\n return Math.max(BODY_LINE_HEIGHT_PT, toPt(spec.size) * 1.2);\n}\n\n/** 某要素每行可容纳的字数(按字号相对版心宽度估算)。 */\nexport function charsPerLineFor(role: OfficialElement): number {\n const spec = ELEMENT_SPEC[role] ?? ELEMENT_SPEC[DEFAULT_ROLE];\n // 正文 3 号字固定为 28 字/行;其余按字号面积比例缩放\n const ratio = toPt(\"三号\") / toPt(spec.size);\n const cols = Math.round(CHARS_PER_LINE * ratio);\n const reserved = (spec.marginLeft ?? 0) + (spec.marginRight ?? 0);\n return Math.max(1, cols - reserved);\n}\n\n/** 估算某块的折行数(至少 1 行)。 */\nexport function estimateLines(block: PaginationBlock): number {\n const role = block.role ?? DEFAULT_ROLE;\n const spec = ELEMENT_SPEC[role] ?? ELEMENT_SPEC[DEFAULT_ROLE];\n const text = block.text ?? \"\";\n const len = [...text].length; // 以码点计,兼顾中英文\n if (len === 0) return 1;\n\n const perLine = charsPerLineFor(role);\n const indent = spec.indent ?? 0;\n const firstLineCap = Math.max(1, perLine - indent);\n if (len <= firstLineCap) return 1;\n return 1 + Math.ceil((len - firstLineCap) / perLine);\n}\n\n/**\n * 执行分页。返回每页承载的片段(块 + 行区间)。\n * 长段落会按行跨页拆分;atomic 块不拆分(放不下则移至下一页)。\n */\nexport function paginate(\n blocks: PaginationBlock[],\n options: PaginateOptions = {},\n): Page[] {\n const pageHeight = options.pageHeightPt ?? TYPE_AREA_HEIGHT_PT;\n const pages: Page[] = [];\n\n let current: Page = { pageNo: 1, fragments: [], usedHeightPt: 0 };\n const pushPage = () => {\n pages.push(current);\n current = { pageNo: current.pageNo + 1, fragments: [], usedHeightPt: 0 };\n };\n\n blocks.forEach((block, blockIndex) => {\n const role = block.role ?? DEFAULT_ROLE;\n const lh = lineHeightPtFor(role);\n const totalLines = estimateLines(block);\n\n if (block.atomic) {\n const blockHeight = totalLines * lh;\n if (current.usedHeightPt + blockHeight > pageHeight && current.fragments.length > 0) {\n pushPage();\n }\n current.fragments.push({ blockIndex, role, startLine: 0, endLine: totalLines });\n current.usedHeightPt += blockHeight;\n return;\n }\n\n // 逐行放置,必要时跨页拆分\n let line = 0;\n while (line < totalLines) {\n const remainingPt = pageHeight - current.usedHeightPt;\n // 加微小 epsilon,避免 22 行恰好填满整页时被浮点误差判为 21 行\n let fit = Math.floor((remainingPt + 1e-6) / lh);\n if (fit <= 0) {\n if (current.fragments.length > 0) {\n pushPage();\n continue;\n }\n fit = 1; // 单行已超过整页高度时,至少放一行避免死循环\n }\n const take = Math.min(fit, totalLines - line);\n current.fragments.push({ blockIndex, role, startLine: line, endLine: line + take });\n current.usedHeightPt += take * lh;\n line += take;\n }\n });\n\n if (current.fragments.length > 0 || pages.length === 0) pages.push(current);\n return pages;\n}\n\n/** 便捷方法:仅返回总页数。 */\nexport function countPages(blocks: PaginationBlock[], options?: PaginateOptions): number {\n return paginate(blocks, options).length;\n}\n","/**\n * 公文页码(GB/T 9704-2012)。\n *\n * 规范:页码用 4 号半角宋体阿拉伯数字,数字左右各放一条“一字线”(即一字宽的横线),\n * 编排在版心下边缘之下、距下边缘 7mm;单页码居右空一字,双页码居左空一字。\n */\nimport { TYPE_AREA_MM } from \"../spec/layout\";\n\n/** 一字线(占一字宽的横线),用全角破折号字符表示。 */\nexport const DASH = \"—\"; // —\n\n/** 页码与版心下边缘的距离(毫米)。 */\nexport const PAGE_NUMBER_OFFSET_MM = 7;\n\nexport type PageNumberAlign = \"left\" | \"right\";\n\n/** 格式化页码,如 1 → “— 1 —”。 */\nexport function formatPageNumber(pageNo: number): string {\n return `${DASH} ${pageNo} ${DASH}`;\n}\n\n/**\n * 页码对齐方式:单页(奇数)居右空一字,双页(偶数)居左空一字。\n * @param pageNo 从 1 开始的页码\n */\nexport function pageNumberAlign(pageNo: number): PageNumberAlign {\n return pageNo % 2 === 1 ? \"right\" : \"left\";\n}\n\nexport interface PageNumberStyle {\n text: string;\n align: PageNumberAlign;\n /** 距版心下边缘的距离(毫米) */\n offsetMm: number;\n /** “空一字”的留白(毫米),等于版心每字网格宽度 */\n insetMm: number;\n}\n\n/** 给定页码,返回其完整排布信息(文本/对齐/偏移/留白)。 */\nexport function pageNumberStyle(pageNo: number): PageNumberStyle {\n return {\n text: formatPageNumber(pageNo),\n align: pageNumberAlign(pageNo),\n offsetMm: PAGE_NUMBER_OFFSET_MM,\n insetMm: TYPE_AREA_MM.width / 28, // 每行 28 字,空一字 = 版心宽/28\n };\n}\n","/**\n * 从 Tiptap/ProseMirror JSON 文档抽取分页所需的块信息。\n */\nimport type { JSONContent } from \"@tiptap/core\";\nimport type { OfficialElement } from \"../spec/elements\";\nimport type { PaginationBlock } from \"./paginate\";\n\nfunction textOf(node: JSONContent): string {\n if (node.type === \"text\") return node.text ?? \"\";\n if (!node.content) return \"\";\n return node.content.map(textOf).join(\"\");\n}\n\n/** 将 doc 的顶层节点转为分页块列表。 */\nexport function blocksFromDoc(doc: JSONContent): PaginationBlock[] {\n const top = doc.type === \"doc\" ? (doc.content ?? []) : [doc];\n return top.map((node) => {\n if (node.type === \"horizontalRule\") {\n return { atomic: true } as PaginationBlock;\n }\n if (node.type === \"table\" || node.type === \"image\") {\n // 表格/图片不参与按行拆分\n return { atomic: true, text: textOf(node) } satisfies PaginationBlock;\n }\n const role = node.attrs?.officialRole as OfficialElement | undefined;\n return { role, text: textOf(node) } satisfies PaginationBlock;\n });\n}\n","/**\n * 断页几何(headless,纯函数,可单测)。\n *\n * 输入各顶层块在“连续排布”下的自然位置(top/height,单位 px),输出应在何处插入\n * 分页间隔(spacer)以将内容可视化为多页。被编辑器内联分页插件用于驱动装饰渲染。\n *\n * v1 以块为断页粒度:跨页边界的块整体移至下一页;超过整页高度的单块允许溢出\n * (留待后续按行拆分)。\n */\n\nexport interface BlockRect {\n /** 块在编辑区内的自然顶端(相对编辑区顶部,px) */\n top: number;\n /** 块高度(px) */\n height: number;\n}\n\nexport interface PageBreakOptions {\n /** 每页版心可用内容高度(px) */\n pageContentPx: number;\n /** 两页之间的视觉间隔(px):上一页地脚 + 页间距 + 下一页天头 */\n breakExtraPx: number;\n}\n\nexport interface PageBreak {\n /** 在第几个块之前插入分隔(块下标) */\n beforeIndex: number;\n /** 新页的页码(从 2 起) */\n pageNo: number;\n /** 需要插入的间隔高度(px) */\n spacerPx: number;\n}\n\nexport interface PageBreakResult {\n breaks: PageBreak[];\n pageCount: number;\n}\n\n/**\n * 计算分页断点。\n */\nexport function computePageBreaks(\n blocks: BlockRect[],\n options: PageBreakOptions,\n): PageBreakResult {\n const { pageContentPx, breakExtraPx } = options;\n const breaks: PageBreak[] = [];\n if (blocks.length === 0) return { breaks, pageCount: 1 };\n\n let pageNo = 1;\n let pageStart = blocks[0].top; // 当前页内容的自然起始 y\n\n for (let i = 0; i < blocks.length; i++) {\n const b = blocks[i];\n const relBottom = b.top + b.height - pageStart;\n const isFirstOnPage = b.top <= pageStart + 0.5;\n\n if (relBottom > pageContentPx && !isFirstOnPage) {\n const remaining = pageContentPx - (b.top - pageStart); // 上一页剩余空白\n breaks.push({\n beforeIndex: i,\n pageNo: pageNo + 1,\n spacerPx: Math.max(0, remaining) + breakExtraPx,\n });\n pageNo += 1;\n pageStart = b.top;\n }\n }\n\n return { breaks, pageCount: pageNo };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// src/spec/layout.ts
|
|
2
|
+
var PAGE_A4_MM = { width: 210, height: 297 };
|
|
3
|
+
var TYPE_AREA_MM = { width: 156, height: 225 };
|
|
4
|
+
var MARGIN_MM = {
|
|
5
|
+
top: 37,
|
|
6
|
+
left: 28,
|
|
7
|
+
right: PAGE_A4_MM.width - 28 - TYPE_AREA_MM.width,
|
|
8
|
+
// 26mm(切口)
|
|
9
|
+
bottom: PAGE_A4_MM.height - 37 - TYPE_AREA_MM.height
|
|
10
|
+
// 35mm(地脚)
|
|
11
|
+
};
|
|
12
|
+
var LINES_PER_PAGE = 22;
|
|
13
|
+
var CHARS_PER_LINE = 28;
|
|
14
|
+
var LINE_HEIGHT_MM = TYPE_AREA_MM.height / LINES_PER_PAGE;
|
|
15
|
+
var CHAR_BOX_MM = TYPE_AREA_MM.width / CHARS_PER_LINE;
|
|
16
|
+
var Layout = {
|
|
17
|
+
page: PAGE_A4_MM,
|
|
18
|
+
typeArea: TYPE_AREA_MM,
|
|
19
|
+
margin: MARGIN_MM,
|
|
20
|
+
linesPerPage: LINES_PER_PAGE,
|
|
21
|
+
charsPerLine: CHARS_PER_LINE,
|
|
22
|
+
lineHeightMm: LINE_HEIGHT_MM,
|
|
23
|
+
charBoxMm: CHAR_BOX_MM
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// src/spec/font-size.ts
|
|
27
|
+
var FONT_SIZE_PT = {
|
|
28
|
+
\u521D\u53F7: 42,
|
|
29
|
+
\u5C0F\u521D: 36,
|
|
30
|
+
\u4E00\u53F7: 26,
|
|
31
|
+
\u5C0F\u4E00: 24,
|
|
32
|
+
\u4E8C\u53F7: 22,
|
|
33
|
+
\u5C0F\u4E8C: 18,
|
|
34
|
+
\u4E09\u53F7: 16,
|
|
35
|
+
\u5C0F\u4E09: 15,
|
|
36
|
+
\u56DB\u53F7: 14,
|
|
37
|
+
\u5C0F\u56DB: 12,
|
|
38
|
+
\u4E94\u53F7: 10.5,
|
|
39
|
+
\u5C0F\u4E94: 9,
|
|
40
|
+
\u516D\u53F7: 7.5,
|
|
41
|
+
\u5C0F\u516D: 6.5,
|
|
42
|
+
\u4E03\u53F7: 5.5,
|
|
43
|
+
\u516B\u53F7: 5
|
|
44
|
+
};
|
|
45
|
+
var PT_TO_MM = 25.4 / 72;
|
|
46
|
+
var PT_TO_PX = 96 / 72;
|
|
47
|
+
function toPt(size) {
|
|
48
|
+
return typeof size === "number" ? size : FONT_SIZE_PT[size];
|
|
49
|
+
}
|
|
50
|
+
function ptToMm(pt) {
|
|
51
|
+
return pt * PT_TO_MM;
|
|
52
|
+
}
|
|
53
|
+
function ptToPx(pt) {
|
|
54
|
+
return pt * PT_TO_PX;
|
|
55
|
+
}
|
|
56
|
+
function toHalfPoint(size) {
|
|
57
|
+
return Math.round(toPt(size) * 2);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// src/spec/elements.ts
|
|
61
|
+
var OFFICIAL_RED = "#e60012";
|
|
62
|
+
var ELEMENT_SPEC = {
|
|
63
|
+
copyNumber: { font: "songti", size: "\u516D\u53F7", align: "left" },
|
|
64
|
+
secrecy: { font: "heiti", size: "\u4E09\u53F7", align: "left" },
|
|
65
|
+
urgency: { font: "heiti", size: "\u4E09\u53F7", align: "left" },
|
|
66
|
+
issuer: { font: "xiaobiaosong", size: "\u521D\u53F7", align: "center", color: OFFICIAL_RED },
|
|
67
|
+
docNumber: { font: "fangsong", size: "\u4E09\u53F7", align: "center" },
|
|
68
|
+
signer: { font: "kaiti", size: "\u4E09\u53F7", align: "right" },
|
|
69
|
+
title: { font: "xiaobiaosong", size: "\u4E8C\u53F7", align: "center" },
|
|
70
|
+
mainRecipient: { font: "fangsong", size: "\u4E09\u53F7", align: "left" },
|
|
71
|
+
body: { font: "fangsong", size: "\u4E09\u53F7", align: "justify", indent: 2 },
|
|
72
|
+
headingLevel1: { font: "heiti", size: "\u4E09\u53F7", indent: 2 },
|
|
73
|
+
headingLevel2: { font: "kaiti", size: "\u4E09\u53F7", indent: 2 },
|
|
74
|
+
headingLevel3: { font: "fangsong", size: "\u4E09\u53F7", bold: true, indent: 2 },
|
|
75
|
+
headingLevel4: { font: "fangsong", size: "\u4E09\u53F7", indent: 2 },
|
|
76
|
+
attachmentNote: { font: "fangsong", size: "\u4E09\u53F7", indent: 2 },
|
|
77
|
+
signature: { font: "fangsong", size: "\u4E09\u53F7", align: "right", marginRight: 4 },
|
|
78
|
+
dateline: { font: "fangsong", size: "\u4E09\u53F7", align: "right", marginRight: 4 },
|
|
79
|
+
note: { font: "fangsong", size: "\u4E09\u53F7", indent: 2 },
|
|
80
|
+
ccOrgan: { font: "fangsong", size: "\u56DB\u53F7", align: "left" },
|
|
81
|
+
printOrgan: { font: "fangsong", size: "\u56DB\u53F7", align: "left" },
|
|
82
|
+
pageNumber: { font: "songti", size: "\u56DB\u53F7" }
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export {
|
|
86
|
+
PAGE_A4_MM,
|
|
87
|
+
TYPE_AREA_MM,
|
|
88
|
+
MARGIN_MM,
|
|
89
|
+
LINES_PER_PAGE,
|
|
90
|
+
CHARS_PER_LINE,
|
|
91
|
+
LINE_HEIGHT_MM,
|
|
92
|
+
CHAR_BOX_MM,
|
|
93
|
+
Layout,
|
|
94
|
+
FONT_SIZE_PT,
|
|
95
|
+
PT_TO_MM,
|
|
96
|
+
PT_TO_PX,
|
|
97
|
+
toPt,
|
|
98
|
+
ptToMm,
|
|
99
|
+
ptToPx,
|
|
100
|
+
toHalfPoint,
|
|
101
|
+
OFFICIAL_RED,
|
|
102
|
+
ELEMENT_SPEC
|
|
103
|
+
};
|
|
104
|
+
//# sourceMappingURL=chunk-SYMAPCOS.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/spec/layout.ts","../src/spec/font-size.ts","../src/spec/elements.ts"],"sourcesContent":["/**\n * GB/T 9704-2012《党政机关公文格式》页面与版心规范。\n *\n * 标准要点(A4 纸,210mm × 297mm):\n * - 版心尺寸:156mm(宽)× 225mm(高)。\n * - 页边距:天头(上白边)37mm,订口(左白边)28mm;\n * 由版心尺寸反推:地脚(下白边)35mm,切口(右白边)26mm。\n * - 正文每面排 22 行、每行 28 个字(3 号仿宋)。\n * - 字间距与行距由版心尺寸均分得到,保证每面行数、每行字数固定。\n */\n\n/** A4 纸张尺寸(毫米)。 */\nexport const PAGE_A4_MM = { width: 210, height: 297 } as const;\n\n/** 版心尺寸(毫米)。 */\nexport const TYPE_AREA_MM = { width: 156, height: 225 } as const;\n\n/**\n * 页边距(毫米)。\n * top = 天头(上白边),left = 订口(左白边),其余由版心尺寸反推。\n */\nexport const MARGIN_MM = {\n top: 37,\n left: 28,\n right: PAGE_A4_MM.width - 28 - TYPE_AREA_MM.width, // 26mm(切口)\n bottom: PAGE_A4_MM.height - 37 - TYPE_AREA_MM.height, // 35mm(地脚)\n} as const;\n\n/** 正文每面行数。 */\nexport const LINES_PER_PAGE = 22;\n/** 正文每行字数。 */\nexport const CHARS_PER_LINE = 28;\n\n/**\n * 正文标准行距(毫米):版心高度 / 每面行数。\n * 225 / 22 ≈ 10.227mm ≈ 28.98pt,实务中常设为固定行距 28.8~29pt。\n */\nexport const LINE_HEIGHT_MM = TYPE_AREA_MM.height / LINES_PER_PAGE;\n\n/**\n * 正文每字标准网格宽度(毫米):版心宽度 / 每行字数。\n * 156 / 28 ≈ 5.571mm。\n */\nexport const CHAR_BOX_MM = TYPE_AREA_MM.width / CHARS_PER_LINE;\n\nexport const Layout = {\n page: PAGE_A4_MM,\n typeArea: TYPE_AREA_MM,\n margin: MARGIN_MM,\n linesPerPage: LINES_PER_PAGE,\n charsPerLine: CHARS_PER_LINE,\n lineHeightMm: LINE_HEIGHT_MM,\n charBoxMm: CHAR_BOX_MM,\n} as const;\n\nexport type LayoutSpec = typeof Layout;\n","/**\n * 公文字号(中文“号”制)与磅值(pt)对照表。\n *\n * 中国大陆排版沿用“号”制字号,GB/T 9704-2012《党政机关公文格式》中\n * 各要素的字号均以“号”表示(如正文为 3 号字)。此处给出号制到磅值的\n * 标准换算,供排版引擎与 docx 导出(OOXML 以半磅为单位)复用。\n */\n\nexport type ChineseFontSizeName =\n | \"初号\"\n | \"小初\"\n | \"一号\"\n | \"小一\"\n | \"二号\"\n | \"小二\"\n | \"三号\"\n | \"小三\"\n | \"四号\"\n | \"小四\"\n | \"五号\"\n | \"小五\"\n | \"六号\"\n | \"小六\"\n | \"七号\"\n | \"八号\";\n\n/** 号制字号 → 磅值(pt) */\nexport const FONT_SIZE_PT: Record<ChineseFontSizeName, number> = {\n 初号: 42,\n 小初: 36,\n 一号: 26,\n 小一: 24,\n 二号: 22,\n 小二: 18,\n 三号: 16,\n 小三: 15,\n 四号: 14,\n 小四: 12,\n 五号: 10.5,\n 小五: 9,\n 六号: 7.5,\n 小六: 6.5,\n 七号: 5.5,\n 八号: 5,\n};\n\n/** 1 磅(pt)= 1/72 英寸 = 25.4/72 毫米 */\nexport const PT_TO_MM = 25.4 / 72;\n/** 1 磅(pt)按 96dpi 屏幕 = 96/72 像素 */\nexport const PT_TO_PX = 96 / 72;\n\n/** 号制字号转磅值;传入数字则原样返回(已是磅值)。 */\nexport function toPt(size: ChineseFontSizeName | number): number {\n return typeof size === \"number\" ? size : FONT_SIZE_PT[size];\n}\n\n/** 号制字号转毫米。 */\nexport function ptToMm(pt: number): number {\n return pt * PT_TO_MM;\n}\n\n/** 号制字号转 CSS px(96dpi)。 */\nexport function ptToPx(pt: number): number {\n return pt * PT_TO_PX;\n}\n\n/** OOXML(docx)中字号以半磅(half-point)为单位,<w:sz w:val=\"32\"/> 表示 16pt。 */\nexport function toHalfPoint(size: ChineseFontSizeName | number): number {\n return Math.round(toPt(size) * 2);\n}\n","/**\n * GB/T 9704-2012 各公文要素的排版规格。\n *\n * 每个要素给出字体角色、字号(号制)、对齐、缩进(以“字”为单位)、颜色等,\n * 供 Tiptap 节点渲染与 docx 导出共享同一份“事实来源”。\n */\nimport type { ChineseFontSizeName } from \"./font-size\";\nimport type { FontRole } from \"./fonts\";\n\nexport interface ElementSpec {\n /** 字体角色 */\n font: FontRole;\n /** 字号(号制) */\n size: ChineseFontSizeName;\n /** 是否加粗 */\n bold?: boolean;\n /** 水平对齐 */\n align?: \"left\" | \"center\" | \"right\" | \"justify\";\n /** 首行缩进(字) */\n indent?: number;\n /** 整体左空(字) */\n marginLeft?: number;\n /** 整体右空(字) */\n marginRight?: number;\n /** 颜色(默认黑色 #000,红头为红色) */\n color?: string;\n}\n\n/** 公文标准红色(发文机关标志、分隔线)。GB/T 9704 未规定具体值,实务常用正红。 */\nexport const OFFICIAL_RED = \"#e60012\";\n\nexport type OfficialElement =\n | \"copyNumber\" // 份号\n | \"secrecy\" // 密级和保密期限\n | \"urgency\" // 紧急程度\n | \"issuer\" // 发文机关标志(红头)\n | \"docNumber\" // 发文字号\n | \"signer\" // 签发人\n | \"title\" // 标题\n | \"mainRecipient\" // 主送机关\n | \"body\" // 正文(默认段落)\n | \"headingLevel1\" // 一级标题 一、\n | \"headingLevel2\" // 二级标题 (一)\n | \"headingLevel3\" // 三级标题 1.\n | \"headingLevel4\" // 四级标题 (1)\n | \"attachmentNote\" // 附件说明\n | \"signature\" // 发文机关署名\n | \"dateline\" // 成文日期\n | \"note\" // 附注\n | \"ccOrgan\" // 版记:抄送机关\n | \"printOrgan\" // 版记:印发机关和印发日期\n | \"pageNumber\"; // 页码\n\nexport const ELEMENT_SPEC: Record<OfficialElement, ElementSpec> = {\n copyNumber: { font: \"songti\", size: \"六号\", align: \"left\" },\n secrecy: { font: \"heiti\", size: \"三号\", align: \"left\" },\n urgency: { font: \"heiti\", size: \"三号\", align: \"left\" },\n issuer: { font: \"xiaobiaosong\", size: \"初号\", align: \"center\", color: OFFICIAL_RED },\n docNumber: { font: \"fangsong\", size: \"三号\", align: \"center\" },\n signer: { font: \"kaiti\", size: \"三号\", align: \"right\" },\n title: { font: \"xiaobiaosong\", size: \"二号\", align: \"center\" },\n mainRecipient: { font: \"fangsong\", size: \"三号\", align: \"left\" },\n body: { font: \"fangsong\", size: \"三号\", align: \"justify\", indent: 2 },\n headingLevel1: { font: \"heiti\", size: \"三号\", indent: 2 },\n headingLevel2: { font: \"kaiti\", size: \"三号\", indent: 2 },\n headingLevel3: { font: \"fangsong\", size: \"三号\", bold: true, indent: 2 },\n headingLevel4: { font: \"fangsong\", size: \"三号\", indent: 2 },\n attachmentNote: { font: \"fangsong\", size: \"三号\", indent: 2 },\n signature: { font: \"fangsong\", size: \"三号\", align: \"right\", marginRight: 4 },\n dateline: { font: \"fangsong\", size: \"三号\", align: \"right\", marginRight: 4 },\n note: { font: \"fangsong\", size: \"三号\", indent: 2 },\n ccOrgan: { font: \"fangsong\", size: \"四号\", align: \"left\" },\n printOrgan: { font: \"fangsong\", size: \"四号\", align: \"left\" },\n pageNumber: { font: \"songti\", size: \"四号\" },\n};\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// src/spec/fonts.ts
|
|
2
|
+
var FONT_STACK = {
|
|
3
|
+
xiaobiaosong: '"\u65B9\u6B63\u5C0F\u6807\u5B8B\u7B80\u4F53", "FZXiaoBiaoSong-B05S", "STZhongsong", "\u534E\u6587\u4E2D\u5B8B", "Source Han Serif SC", "Noto Serif SC", serif',
|
|
4
|
+
fangsong: '"\u4EFF\u5B8B_GB2312", "\u4EFF\u5B8B", "FangSong", "STFangsong", "Source Han Serif SC", "Noto Serif SC", serif',
|
|
5
|
+
heiti: '"\u9ED1\u4F53", "SimHei", "STHeiti", "Source Han Sans SC", "Noto Sans SC", sans-serif',
|
|
6
|
+
kaiti: '"\u6977\u4F53_GB2312", "\u6977\u4F53", "KaiTi", "STKaiti", "Source Han Serif SC", "Noto Serif SC", serif',
|
|
7
|
+
songti: '"\u5B8B\u4F53", "SimSun", "STSong", "Source Han Serif SC", "Noto Serif SC", serif'
|
|
8
|
+
};
|
|
9
|
+
var FONT_CSS_VAR = {
|
|
10
|
+
xiaobiaosong: "--odoc-font-xiaobiaosong",
|
|
11
|
+
fangsong: "--odoc-font-fangsong",
|
|
12
|
+
heiti: "--odoc-font-heiti",
|
|
13
|
+
kaiti: "--odoc-font-kaiti",
|
|
14
|
+
songti: "--odoc-font-songti"
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export {
|
|
18
|
+
FONT_STACK,
|
|
19
|
+
FONT_CSS_VAR
|
|
20
|
+
};
|
|
21
|
+
//# sourceMappingURL=chunk-TRNXHJAU.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/spec/fonts.ts"],"sourcesContent":["/**\n * 公文字体角色(与 GB/T 9704-2012 各要素字体要求对应)。\n *\n * ⚠️ 字体版权说明:\n * 仿宋_GB2312、楷体_GB2312、方正小标宋 等公文常用字体多为商业字体,\n * 本库不内置其字体文件,仅在 CSS 中通过 `local()` 优先引用用户系统已安装的\n * 公文字体,并以可商用开源字体(思源宋体 / 思源黑体等)兜底。\n * 如需在离线环境精确还原,请通过 registerFont() 由使用方自行提供授权字体。\n */\n\nexport type FontRole =\n | \"xiaobiaosong\" // 小标宋体:发文机关标志、标题\n | \"fangsong\" // 仿宋:正文及大多数要素\n | \"heiti\" // 黑体:一级标题、密级、紧急程度\n | \"kaiti\" // 楷体:二级标题、签发人姓名\n | \"songti\"; // 宋体:页码\n\n/** 各字体角色的 CSS font-family 兜底栈(优先本机公文字体 → 开源字体 → 通用族)。 */\nexport const FONT_STACK: Record<FontRole, string> = {\n xiaobiaosong:\n '\"方正小标宋简体\", \"FZXiaoBiaoSong-B05S\", \"STZhongsong\", \"华文中宋\", \"Source Han Serif SC\", \"Noto Serif SC\", serif',\n fangsong:\n '\"仿宋_GB2312\", \"仿宋\", \"FangSong\", \"STFangsong\", \"Source Han Serif SC\", \"Noto Serif SC\", serif',\n heiti:\n '\"黑体\", \"SimHei\", \"STHeiti\", \"Source Han Sans SC\", \"Noto Sans SC\", sans-serif',\n kaiti:\n '\"楷体_GB2312\", \"楷体\", \"KaiTi\", \"STKaiti\", \"Source Han Serif SC\", \"Noto Serif SC\", serif',\n songti:\n '\"宋体\", \"SimSun\", \"STSong\", \"Source Han Serif SC\", \"Noto Serif SC\", serif',\n};\n\n/** CSS 变量名,运行时可被 registerFont() 覆盖以接入授权字体。 */\nexport const FONT_CSS_VAR: Record<FontRole, string> = {\n xiaobiaosong: \"--odoc-font-xiaobiaosong\",\n fangsong: \"--odoc-font-fangsong\",\n heiti: \"--odoc-font-heiti\",\n kaiti: \"--odoc-font-kaiti\",\n songti: \"--odoc-font-songti\",\n};\n"],"mappings":";AAkBO,IAAM,aAAuC;AAAA,EAClD,cACE;AAAA,EACF,UACE;AAAA,EACF,OACE;AAAA,EACF,OACE;AAAA,EACF,QACE;AACJ;AAGO,IAAM,eAAyC;AAAA,EACpD,cAAc;AAAA,EACd,UAAU;AAAA,EACV,OAAO;AAAA,EACP,OAAO;AAAA,EACP,QAAQ;AACV;","names":[]}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { JSONContent } from '@tiptap/core';
|
|
2
|
+
import { O as OfficialElement, F as FontRole } from '../elements-Bvueu_TD.js';
|
|
3
|
+
|
|
4
|
+
declare function styleIdFor(role: OfficialElement): string;
|
|
5
|
+
/** 导出为 Blob(浏览器,可直接触发下载)。 */
|
|
6
|
+
declare function toDocxBlob(doc: JSONContent): Promise<Blob>;
|
|
7
|
+
/** 导出为 Buffer/Uint8Array(Node,便于测试与服务端)。 */
|
|
8
|
+
declare function toDocxBuffer(doc: JSONContent): Promise<Buffer>;
|
|
9
|
+
|
|
10
|
+
/** 解析 docx 数据为 Tiptap JSON 文档。 */
|
|
11
|
+
declare function fromDocx(data: ArrayBuffer | Uint8Array): JSONContent;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 公文字体角色 → Word(docx)字体名映射。
|
|
15
|
+
*
|
|
16
|
+
* 与 CSS 兜底栈不同:docx 面向 Word,目标机器通常已安装公文字体,故此处使用
|
|
17
|
+
* 公文规范字体名本体(如“仿宋_GB2312”)。字体本身仍由使用方环境提供,本库不分发。
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
declare const DOCX_FONT_NAME: Record<FontRole, string>;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 图片工具:data URL 解析、字节 ↔ base64、以及从图片字节读取内置尺寸与类型。
|
|
24
|
+
* 纯前端/Node 通用,无需依赖图片库。
|
|
25
|
+
*/
|
|
26
|
+
type DocxImageType = "png" | "jpg" | "gif" | "bmp";
|
|
27
|
+
interface ParsedImage {
|
|
28
|
+
bytes: Uint8Array;
|
|
29
|
+
mime: string;
|
|
30
|
+
type: DocxImageType;
|
|
31
|
+
}
|
|
32
|
+
/** 解析 data:[mime];base64,xxx;非 base64 data URL 返回 null。 */
|
|
33
|
+
declare function parseDataUrl(src: string): ParsedImage | null;
|
|
34
|
+
/** 由类型与字节拼装 data URL。 */
|
|
35
|
+
declare function toDataUrl(type: DocxImageType, bytes: Uint8Array): string;
|
|
36
|
+
interface ImageSize {
|
|
37
|
+
width: number;
|
|
38
|
+
height: number;
|
|
39
|
+
type?: DocxImageType;
|
|
40
|
+
}
|
|
41
|
+
/** 从图片字节读取内置像素尺寸(支持 PNG / GIF / JPEG / BMP)。 */
|
|
42
|
+
declare function readImageSize(bytes: Uint8Array): ImageSize | null;
|
|
43
|
+
|
|
44
|
+
export { DOCX_FONT_NAME, type DocxImageType, type ImageSize, fromDocx, parseDataUrl, readImageSize, styleIdFor, toDataUrl, toDocxBlob, toDocxBuffer };
|