@docen/docx 0.3.1 → 0.3.3

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.
Files changed (114) hide show
  1. package/dist/converters/docx.d.mts +220 -1
  2. package/dist/converters/docx.mjs +1352 -1
  3. package/dist/converters/html.d.mts +28 -1
  4. package/dist/converters/html.mjs +103 -1
  5. package/dist/converters/markdown.d.mts +12 -1
  6. package/dist/converters/markdown.mjs +17 -1
  7. package/dist/converters/patch.d.mts +52 -1
  8. package/dist/converters/patch.mjs +35 -1
  9. package/dist/converters/prepare.d.mts +62 -1
  10. package/dist/converters/prepare.mjs +121 -1
  11. package/dist/converters/styles.d.mts +91 -2
  12. package/dist/converters/styles.mjs +308 -2
  13. package/dist/core-B8ba_FNi.mjs +717 -0
  14. package/dist/core-DRaLI8nd.d.mts +203 -0
  15. package/dist/core.d.mts +1 -1
  16. package/dist/core.mjs +1 -1
  17. package/dist/document-Cws7XTYL.d.mts +28 -0
  18. package/dist/editor.d.mts +20 -1
  19. package/dist/editor.mjs +20 -1
  20. package/dist/extensions/blockquote.d.mts +1 -1
  21. package/dist/extensions/blockquote.mjs +30 -2
  22. package/dist/extensions/code-block.d.mts +1 -1
  23. package/dist/extensions/code-block.mjs +22 -1
  24. package/dist/extensions/column-break.d.mts +1 -1
  25. package/dist/extensions/column-break.mjs +1 -1
  26. package/dist/extensions/details.d.mts +1 -1
  27. package/dist/extensions/details.mjs +17 -2
  28. package/dist/extensions/document.d.mts +1 -1
  29. package/dist/extensions/document.mjs +45 -1
  30. package/dist/extensions/extensions.d.mts +4 -5
  31. package/dist/extensions/extensions.mjs +13 -14
  32. package/dist/extensions/formatting-marks.d.mts +7 -1
  33. package/dist/extensions/formatting-marks.mjs +1 -1
  34. package/dist/extensions/heading.mjs +187 -1
  35. package/dist/extensions/image.d.mts +1 -1
  36. package/dist/extensions/image.mjs +395 -1
  37. package/dist/extensions/index.d.mts +4 -3
  38. package/dist/extensions/index.mjs +4 -3
  39. package/dist/extensions/mention.d.mts +1 -1
  40. package/dist/extensions/mention.mjs +43 -2
  41. package/dist/extensions/ordered-list.d.mts +1 -1
  42. package/dist/extensions/ordered-list.mjs +40 -2
  43. package/dist/extensions/page-break.d.mts +1 -33
  44. package/dist/extensions/page-break.mjs +1 -1
  45. package/dist/extensions/paragraph.mjs +145 -1
  46. package/dist/extensions/passthrough.d.mts +1 -1
  47. package/dist/extensions/passthrough.mjs +1 -1
  48. package/dist/extensions/section-break.d.mts +19 -2
  49. package/dist/extensions/section-break.mjs +1 -1
  50. package/dist/extensions/strike.mjs +45 -1
  51. package/dist/extensions/table-cell.mjs +106 -1
  52. package/dist/extensions/table-header.mjs +106 -1
  53. package/dist/extensions/table-row.mjs +78 -1
  54. package/dist/extensions/table.mjs +154 -1
  55. package/dist/extensions/task-item.d.mts +1 -1
  56. package/dist/extensions/task-item.mjs +36 -2
  57. package/dist/extensions/text-style.mjs +145 -1
  58. package/dist/extensions/tiptap.d.mts +2 -2
  59. package/dist/extensions/tiptap.mjs +31 -2
  60. package/dist/extensions/toc-field.d.mts +2 -0
  61. package/dist/extensions/toc-field.mjs +2 -0
  62. package/dist/extensions/utils.d.mts +2 -62
  63. package/dist/extensions/utils.mjs +508 -2
  64. package/dist/extensions/wpg-group.d.mts +2 -0
  65. package/dist/extensions/wpg-group.mjs +2 -0
  66. package/dist/extensions/wps-shape.d.mts +2 -0
  67. package/dist/extensions/wps-shape.mjs +2 -0
  68. package/dist/index.d.mts +101 -29
  69. package/dist/index.mjs +13 -10
  70. package/dist/{tiptap-TErPjuNJ.d.mts → tiptap-pZsNPsvV.d.mts} +1 -3
  71. package/dist/utils-BJwDQts7.d.mts +137 -0
  72. package/package.json +39 -40
  73. package/dist/blockquote-Ky9xD_Q-.mjs +0 -31
  74. package/dist/code-block-BgNjhADy.mjs +0 -23
  75. package/dist/core-DEqBHRzE.mjs +0 -406
  76. package/dist/core-fBMCCj4D.d.mts +0 -97
  77. package/dist/details-Dh8luWb2.mjs +0 -18
  78. package/dist/dist-B6B3HnVZ.mjs +0 -1838
  79. package/dist/document-BH1y4qHM.d.mts +0 -6
  80. package/dist/document-CvTym-FM.mjs +0 -50
  81. package/dist/docx-C5wyjI-0.d.mts +0 -200
  82. package/dist/docx-CONyAedl.mjs +0 -1231
  83. package/dist/editor-Bf3CxQnY.mjs +0 -21
  84. package/dist/editor-DReY3Rb9.d.mts +0 -21
  85. package/dist/emoji-BZ1dJD7d.d.mts +0 -17
  86. package/dist/emoji-C2OsTWR7.mjs +0 -27
  87. package/dist/extensions/emoji.d.mts +0 -2
  88. package/dist/extensions/emoji.mjs +0 -2
  89. package/dist/extensions/image-group.d.mts +0 -6
  90. package/dist/extensions/image-group.mjs +0 -2
  91. package/dist/heading-DAubgJJN.mjs +0 -166
  92. package/dist/html-DCC_DSHv.d.mts +0 -18
  93. package/dist/html-DedDWPoO.mjs +0 -66
  94. package/dist/image-CuEIXoZl.mjs +0 -334
  95. package/dist/markdown-BzJmRsBs.mjs +0 -18
  96. package/dist/markdown-D0k0sRYS.d.mts +0 -13
  97. package/dist/mention-C-dTR6MK.mjs +0 -44
  98. package/dist/ordered-list-V-3xFQmV.mjs +0 -41
  99. package/dist/paragraph-BrWH-BOV.mjs +0 -147
  100. package/dist/patch-Cdp5WKJz.d.mts +0 -53
  101. package/dist/patch-IZ13tXTu.mjs +0 -36
  102. package/dist/prepare-Bdk8Cjkf.mjs +0 -122
  103. package/dist/prepare-CPAvcv6q.d.mts +0 -63
  104. package/dist/strike-hWxShOgb.mjs +0 -50
  105. package/dist/styles-QSX3RhxK.mjs +0 -285
  106. package/dist/styles-ugspkrxz.d.mts +0 -85
  107. package/dist/table-DrA12lxX.mjs +0 -159
  108. package/dist/table-cell-DmkfYwDc.mjs +0 -112
  109. package/dist/table-header-Bwr5GKWg.mjs +0 -112
  110. package/dist/table-row-vew1y7WH.mjs +0 -84
  111. package/dist/task-item-BnlXGrIG.mjs +0 -37
  112. package/dist/text-style-tMeBq5XQ.mjs +0 -151
  113. package/dist/tiptap-D7pl4I5U.mjs +0 -33
  114. package/dist/utils-BPT4tjcn.mjs +0 -333
@@ -1,2 +1,1353 @@
1
- import { a as generateDOCXSync, i as generateDOCXStream, n as compileDocument, o as parseDOCX, r as generateDOCX, s as resolveDocument, t as DocxManager } from "../docx-CONyAedl.mjs";
1
+ import { BLOCKQUOTE_BORDER, applyBlockquoteStyle } from "../extensions/blockquote.mjs";
2
+ import { renderDocx } from "../extensions/code-block.mjs";
3
+ import { DETAILS_SUMMARY_STYLE, DETAILS_TAG } from "../extensions/details.mjs";
4
+ import { parseDocx, renderDocx as renderDocx$1 } from "../extensions/heading.mjs";
5
+ import { parseDocx as parseDocx$1, renderDocx as renderDocx$2 } from "../extensions/image.mjs";
6
+ import { createMention, isMention, readMention } from "../extensions/mention.mjs";
7
+ import { ORDERED_REFERENCE_PREFIX, buildOrderedLevels } from "../extensions/ordered-list.mjs";
8
+ import { parseDocx as parseDocx$2, renderDocx as renderDocx$3 } from "../extensions/paragraph.mjs";
9
+ import { parseDocx as parseDocx$3, renderDocx as renderDocx$4 } from "../extensions/strike.mjs";
10
+ import { parseDocx as parseDocx$4, renderDocx as renderDocx$5 } from "../extensions/table.mjs";
11
+ import { parseDocx as parseDocx$5, renderDocx as renderDocx$6 } from "../extensions/table-cell.mjs";
12
+ import { parseDocx as parseDocx$6, renderDocx as renderDocx$7 } from "../extensions/table-header.mjs";
13
+ import { parseDocx as parseDocx$7, renderDocx as renderDocx$8 } from "../extensions/table-row.mjs";
14
+ import { createTaskCheckbox, isTaskCheckbox, readCheckboxState } from "../extensions/task-item.mjs";
15
+ import { parseDocx as parseDocx$8, renderDocx as renderDocx$9 } from "../extensions/text-style.mjs";
16
+ import { prepareDocument } from "./prepare.mjs";
17
+ import { indexParagraphStyles } from "./styles.mjs";
18
+ import { emojis, shortcodeToEmoji } from "@tiptap/extension-emoji";
19
+ import { generateDocument, generateDocumentStream, generateDocumentSync, parseDocument } from "@office-open/docx";
20
+ import { encodeBase64 } from "@office-open/core";
21
+ //#region src/converters/docx.ts
22
+ /** Remove keys with null/undefined values */
23
+ function cleanAttrs(attrs) {
24
+ const result = {};
25
+ for (const [key, value] of Object.entries(attrs)) if (value !== null && value !== void 0) result[key] = value;
26
+ return result;
27
+ }
28
+ /** True when two cell-margin sets (w:tcMar / w:tblCellMar) match on every side's
29
+ * size — used to detect a cell that merely echoes the table's default so its
30
+ * redundant tcMar can be dropped for a near-identity round-trip. */
31
+ function sameCellMargins(a, b) {
32
+ const sz = (m, k) => m[k]?.size ?? null;
33
+ return [
34
+ "top",
35
+ "right",
36
+ "bottom",
37
+ "left"
38
+ ].every((k) => sz(a, k) === sz(b, k));
39
+ }
40
+ /** Keys round-tripped between DocumentOptions core properties and `doc.attrs.core`. */
41
+ const CORE_PROPERTY_KEYS = [
42
+ "title",
43
+ "subject",
44
+ "creator",
45
+ "keywords",
46
+ "description",
47
+ "lastModifiedBy",
48
+ "lastPrinted",
49
+ "created",
50
+ "modified",
51
+ "revision"
52
+ ];
53
+ /**
54
+ * DocumentOptions keys that DocxManager reconstructs (sections/numbering) or
55
+ * carries in dedicated attrs (styles/background/core). Excluded from the
56
+ * documentExtras pass-through so they aren't duplicated.
57
+ */
58
+ const COMPILE_OWNED_KEYS = new Set([
59
+ "sections",
60
+ "numbering",
61
+ "styles",
62
+ "background",
63
+ ...CORE_PROPERTY_KEYS
64
+ ]);
65
+ /** Collect core properties present on DocumentOptions into a plain object. */
66
+ function extractCoreProperties(docOpts) {
67
+ const source = docOpts;
68
+ const core = {};
69
+ for (const key of CORE_PROPERTY_KEYS) {
70
+ const value = source[key];
71
+ if (value !== void 0 && value !== null) core[key] = value;
72
+ }
73
+ return Object.keys(core).length > 0 ? core : null;
74
+ }
75
+ /** Merge consecutive text nodes with same marks */
76
+ function mergeTextNodes(nodes) {
77
+ const result = [];
78
+ for (const node of nodes) {
79
+ if (node.type === "text" && result.length > 0 && result[result.length - 1].type === "text") {
80
+ const prev = result[result.length - 1];
81
+ if (JSON.stringify(prev.marks) === JSON.stringify(node.marks)) {
82
+ prev.text = (prev.text ?? "") + (node.text ?? "");
83
+ continue;
84
+ }
85
+ }
86
+ result.push({ ...node });
87
+ }
88
+ return result;
89
+ }
90
+ /**
91
+ * Manages DOCX serialization (Tiptap JSON ↔ DocumentOptions).
92
+ *
93
+ * Each extension provides renderDocx/parseDocx for its own attrs mapping.
94
+ * DocxManager handles tree walking, child assembly, and dispatching.
95
+ */
96
+ var DocxManager = class {
97
+ numberingConfigs = [];
98
+ orderedInstanceCounter = 0;
99
+ resolveStyles;
100
+ compile(json) {
101
+ this.numberingConfigs = [];
102
+ this.orderedInstanceCounter = 0;
103
+ const sections = [];
104
+ let currentChildren = [];
105
+ if (json.content) for (const node of json.content) {
106
+ const child = this.compileSectionChild(node);
107
+ if (child) if (Array.isArray(child)) currentChildren.push(...child);
108
+ else currentChildren.push(child);
109
+ if (node.type === "paragraph") {
110
+ const na = node.attrs ?? {};
111
+ if (na.sectionProperties != null) {
112
+ sections.push(this.buildSection(currentChildren, na.sectionProperties, this.compileHeaderFooter(na.sectionHeaders ?? null), this.compileHeaderFooter(na.sectionFooters ?? null)));
113
+ currentChildren = [];
114
+ }
115
+ }
116
+ }
117
+ const docAttrs = json.attrs ?? {};
118
+ sections.push(this.buildSection(currentChildren, docAttrs.sectionProperties ?? null, this.compileHeaderFooter(docAttrs.sectionHeaders ?? null), this.compileHeaderFooter(docAttrs.sectionFooters ?? null)));
119
+ const styles = docAttrs.styles ?? void 0;
120
+ const core = docAttrs.core ?? void 0;
121
+ const background = docAttrs.background ?? void 0;
122
+ const documentExtras = docAttrs.documentExtras ?? void 0;
123
+ return {
124
+ sections,
125
+ ...styles ? { styles } : {},
126
+ ...core,
127
+ ...background ? { background } : {},
128
+ ...documentExtras,
129
+ ...this.numberingConfigs.length > 0 ? { numbering: { config: this.numberingConfigs } } : {}
130
+ };
131
+ }
132
+ /** Assemble a SectionOptions from compiled children + optional layout/headers/footers. */
133
+ buildSection(children, properties, headers, footers) {
134
+ return {
135
+ children,
136
+ ...properties ? { properties } : {},
137
+ ...headers ? { headers } : {},
138
+ ...footers ? { footers } : {}
139
+ };
140
+ }
141
+ /**
142
+ * Compile resolved header/footer slots (JSONContent[] per slot) back into
143
+ * SectionChild[] per slot. Returns undefined when no slot has content.
144
+ */
145
+ compileHeaderFooter(slots) {
146
+ if (!slots) return void 0;
147
+ const group = {};
148
+ for (const slot of [
149
+ "default",
150
+ "first",
151
+ "even"
152
+ ]) {
153
+ const json = slots[slot];
154
+ if (!json?.length) continue;
155
+ const children = [];
156
+ for (const node of json) {
157
+ const child = this.compileSectionChild(node);
158
+ if (!child) continue;
159
+ if (Array.isArray(child)) children.push(...child);
160
+ else children.push(child);
161
+ }
162
+ if (children.length > 0) group[slot] = children;
163
+ }
164
+ return Object.keys(group).length > 0 ? group : void 0;
165
+ }
166
+ /**
167
+ * Resolve a section's header/footer group (SectionChild[] per slot) into
168
+ * Tiptap JSON slots. Returns null when no slot has content.
169
+ */
170
+ resolveHeaderFooter(group, numberingLookup) {
171
+ if (!group) return null;
172
+ const slots = {};
173
+ for (const slot of [
174
+ "default",
175
+ "first",
176
+ "even"
177
+ ]) {
178
+ const children = group[slot];
179
+ if (children?.length) slots[slot] = this.resolveSectionChildren(children, numberingLookup);
180
+ }
181
+ return Object.keys(slots).length > 0 ? slots : null;
182
+ }
183
+ resolve(docOpts) {
184
+ this.resolveStyles = docOpts.styles ?? void 0;
185
+ const sections = docOpts.sections ?? [];
186
+ if (sections.length === 0) return {
187
+ type: "doc",
188
+ content: [{ type: "paragraph" }]
189
+ };
190
+ const numberingLookup = this.buildNumberingLookup(docOpts);
191
+ const content = [];
192
+ const lastIndex = sections.length - 1;
193
+ for (let i = 0; i < sections.length; i++) {
194
+ const section = sections[i];
195
+ const sectionContent = this.resolveSectionChildren(section.children ?? [], numberingLookup);
196
+ if (i < lastIndex) {
197
+ const sectAttrs = {
198
+ sectionProperties: section.properties ?? null,
199
+ sectionHeaders: this.resolveHeaderFooter(section.headers, numberingLookup),
200
+ sectionFooters: this.resolveHeaderFooter(section.footers, numberingLookup)
201
+ };
202
+ const last = sectionContent[sectionContent.length - 1];
203
+ if (last?.type === "paragraph") last.attrs = {
204
+ ...last.attrs,
205
+ ...sectAttrs
206
+ };
207
+ else sectionContent.push({
208
+ type: "paragraph",
209
+ attrs: sectAttrs
210
+ });
211
+ }
212
+ content.push(...sectionContent);
213
+ }
214
+ const doc = {
215
+ type: "doc",
216
+ content: content.length > 0 ? content : [{ type: "paragraph" }]
217
+ };
218
+ const attrs = {};
219
+ if (docOpts.styles) attrs.styles = docOpts.styles;
220
+ if (docOpts.background) attrs.background = docOpts.background;
221
+ const core = extractCoreProperties(docOpts);
222
+ if (core) attrs.core = core;
223
+ const lastSection = sections[lastIndex];
224
+ if (lastSection.properties) attrs.sectionProperties = lastSection.properties;
225
+ const lastHeaders = this.resolveHeaderFooter(lastSection.headers, numberingLookup);
226
+ if (lastHeaders) attrs.sectionHeaders = lastHeaders;
227
+ const lastFooters = this.resolveHeaderFooter(lastSection.footers, numberingLookup);
228
+ if (lastFooters) attrs.sectionFooters = lastFooters;
229
+ const documentExtras = {};
230
+ for (const [k, v] of Object.entries(docOpts)) {
231
+ if (COMPILE_OWNED_KEYS.has(k)) continue;
232
+ if (v === void 0 || v === null) continue;
233
+ documentExtras[k] = v;
234
+ }
235
+ if (Object.keys(documentExtras).length > 0) attrs.documentExtras = documentExtras;
236
+ if (Object.keys(attrs).length > 0) doc.attrs = attrs;
237
+ return doc;
238
+ }
239
+ compileSectionChild(node) {
240
+ switch (node.type) {
241
+ case "paragraph": return { paragraph: this.compileParagraphNode(node) };
242
+ case "heading": return { paragraph: this.compileHeadingNode(node) };
243
+ case "blockquote": {
244
+ const items = [];
245
+ for (const child of node.content ?? []) if (child.type === "paragraph" || child.type === "heading") {
246
+ const para = child.type === "heading" ? this.compileHeadingNode(child) : this.compileParagraphNode(child);
247
+ const paraObj = typeof para === "string" ? { text: para } : para;
248
+ applyBlockquoteStyle(paraObj);
249
+ items.push({ paragraph: paraObj });
250
+ }
251
+ return items.length > 0 ? items : null;
252
+ }
253
+ case "codeBlock": {
254
+ const opts = renderDocx(node);
255
+ const childList = this.compileInlineContent(node.content);
256
+ if (childList.length > 0) opts.children = childList;
257
+ return { paragraph: this.simplifyParagraph(opts) };
258
+ }
259
+ case "horizontalRule": return { paragraph: { thematicBreak: true } };
260
+ case "table": return { table: this.compileTableNode(node) };
261
+ case "image": {
262
+ const imageRun = renderDocx$2(node);
263
+ if (!imageRun) return null;
264
+ return { paragraph: { children: [imageRun] } };
265
+ }
266
+ case "bulletList":
267
+ case "orderedList":
268
+ case "taskList": return this.compileListFromNode(node, 0);
269
+ case "details": return this.compileDetailsNode(node);
270
+ case "tocField": {
271
+ const options = node.attrs?.options ?? {};
272
+ const entries = [];
273
+ for (const child of node.content ?? []) {
274
+ const compiled = this.compileSectionChild(child);
275
+ if (!compiled) continue;
276
+ if (Array.isArray(compiled)) entries.push(...compiled);
277
+ else entries.push(compiled);
278
+ }
279
+ return { toc: {
280
+ ...options,
281
+ entries
282
+ } };
283
+ }
284
+ case "passthrough": {
285
+ const data = node.attrs?.data ?? "{}";
286
+ try {
287
+ return JSON.parse(data);
288
+ } catch {
289
+ return null;
290
+ }
291
+ }
292
+ default: return null;
293
+ }
294
+ }
295
+ compileParagraphNode(node) {
296
+ const opts = renderDocx$3(node);
297
+ const childList = this.compileInlineContent(node.content);
298
+ if (childList.length > 0) opts.children = childList;
299
+ return this.simplifyParagraph(opts);
300
+ }
301
+ compileHeadingNode(node) {
302
+ const opts = renderDocx$1(node);
303
+ const childList = this.compileInlineContent(node.content);
304
+ if (childList.length > 0) opts.children = childList;
305
+ return this.simplifyParagraph(opts);
306
+ }
307
+ /** Simple text optimization: merge plain runs into text field */
308
+ simplifyParagraph(opts) {
309
+ const children = opts.children;
310
+ if (!children || children.length === 0) return opts;
311
+ if (children.every((c) => typeof c === "object" && c !== null && "text" in c && Object.keys(c).length === 1)) {
312
+ const combined = children.map((c) => c.text).join("");
313
+ delete opts.children;
314
+ if (combined && Object.keys(opts).length === 0) return combined;
315
+ if (combined) opts.text = combined;
316
+ }
317
+ return opts;
318
+ }
319
+ compileTableNode(node) {
320
+ const opts = renderDocx$5(node);
321
+ const colCount = this.getTableColumnCount(node);
322
+ const tableCellMargins = opts.cellMargin ?? opts.margins ?? null;
323
+ const rows = [];
324
+ let activeSpans = [];
325
+ for (const rowNode of node.content ?? []) {
326
+ if (rowNode.type !== "tableRow") continue;
327
+ const rowOpts = renderDocx$8(rowNode);
328
+ const pmCells = (rowNode.content ?? []).filter((c) => c.type === "tableCell" || c.type === "tableHeader");
329
+ const currentSpans = [...activeSpans].sort((a, b) => a.colStart - b.colStart);
330
+ const newSpans = [];
331
+ const compiledCells = [];
332
+ let colIdx = 0;
333
+ let cellIdx = 0;
334
+ let spanIdx = 0;
335
+ while (spanIdx < currentSpans.length || cellIdx < pmCells.length) {
336
+ const nextSpanCol = spanIdx < currentSpans.length ? currentSpans[spanIdx].colStart : Infinity;
337
+ if (colIdx >= nextSpanCol) {
338
+ const span = currentSpans[spanIdx];
339
+ compiledCells.push({
340
+ verticalMerge: "continue",
341
+ columnSpan: span.colspan,
342
+ children: [{ paragraph: "" }]
343
+ });
344
+ colIdx += span.colspan;
345
+ spanIdx++;
346
+ } else {
347
+ const cell = this.compileTableCellNode(pmCells[cellIdx], tableCellMargins);
348
+ const cs = cell.columnSpan ?? 1;
349
+ const rs = cell.rowSpan ?? 1;
350
+ if (rs > 1) {
351
+ delete cell.rowSpan;
352
+ cell.verticalMerge = "restart";
353
+ newSpans.push({
354
+ colStart: colIdx,
355
+ colspan: cs,
356
+ remainingRows: rs - 1
357
+ });
358
+ }
359
+ compiledCells.push(cell);
360
+ colIdx += cs;
361
+ cellIdx++;
362
+ }
363
+ }
364
+ rowOpts.cells = compiledCells;
365
+ rows.push(rowOpts);
366
+ for (const span of currentSpans) span.remainingRows--;
367
+ activeSpans = [...currentSpans.filter((s) => s.remainingRows > 0), ...newSpans];
368
+ }
369
+ opts.rows = rows;
370
+ if (colCount > 0) {
371
+ const { columnWidths, tableWidth } = this.computeColumnWidths(node, colCount, opts);
372
+ opts.columnWidths = columnWidths;
373
+ if (!opts.width) opts.width = tableWidth;
374
+ if (!opts.layout) opts.layout = "autofit";
375
+ }
376
+ return opts;
377
+ }
378
+ /** Count grid columns from the first table row (summing colspan). */
379
+ getTableColumnCount(tableNode) {
380
+ for (const rowNode of tableNode.content ?? []) {
381
+ if (rowNode.type !== "tableRow") continue;
382
+ let count = 0;
383
+ for (const cell of rowNode.content ?? []) if (cell.type === "tableCell" || cell.type === "tableHeader") count += cell.attrs?.colspan ?? 1;
384
+ return count;
385
+ }
386
+ return 0;
387
+ }
388
+ /**
389
+ * Compute tblGrid column widths (twips) from first-row cell colwidth attrs.
390
+ * Returns columnWidths array and the appropriate tableWidth.
391
+ */
392
+ computeColumnWidths(tableNode, colCount, _opts) {
393
+ const DEFAULT_CONTENT_TWIPS = 9026;
394
+ const tableColWidths = tableNode.attrs?.columnWidths;
395
+ if (tableColWidths && tableColWidths.length > 0) {
396
+ const filled = tableColWidths.slice(0, colCount);
397
+ while (filled.length < colCount) filled.push(filled[filled.length - 1] ?? Math.floor(DEFAULT_CONTENT_TWIPS / colCount));
398
+ return {
399
+ columnWidths: filled,
400
+ tableWidth: {
401
+ size: filled.reduce((a, b) => a + b, 0),
402
+ type: "dxa"
403
+ }
404
+ };
405
+ }
406
+ const widths = Array.from({ length: colCount }, () => null);
407
+ for (const rowNode of tableNode.content ?? []) {
408
+ if (rowNode.type !== "tableRow") continue;
409
+ let colIdx = 0;
410
+ for (const cell of rowNode.content ?? []) {
411
+ if (cell.type !== "tableCell" && cell.type !== "tableHeader") continue;
412
+ const colspan = cell.attrs?.colspan ?? 1;
413
+ const colwidth = cell.attrs?.colwidth;
414
+ if (colwidth) for (let i = 0; i < colspan && colIdx + i < colCount; i++) {
415
+ const px = colwidth[i] ?? colwidth[0];
416
+ if (px) widths[colIdx + i] = px * 15;
417
+ }
418
+ colIdx += colspan;
419
+ }
420
+ break;
421
+ }
422
+ if (!widths.some((w) => w !== null)) {
423
+ const equal = Math.floor(DEFAULT_CONTENT_TWIPS / colCount);
424
+ return {
425
+ columnWidths: Array(colCount).fill(equal),
426
+ tableWidth: {
427
+ size: 5e3,
428
+ type: "pct"
429
+ }
430
+ };
431
+ }
432
+ const explicit = widths.filter((w) => w !== null);
433
+ const avg = Math.floor(explicit.reduce((a, b) => a + b, 0) / explicit.length);
434
+ const filled = widths.map((w) => w ?? avg);
435
+ return {
436
+ columnWidths: filled,
437
+ tableWidth: {
438
+ size: filled.reduce((a, b) => a + b, 0),
439
+ type: "dxa"
440
+ }
441
+ };
442
+ }
443
+ compileTableCellNode(cellNode, tableMargins) {
444
+ const cellOpts = cellNode.type === "tableHeader" ? renderDocx$7(cellNode) : renderDocx$6(cellNode);
445
+ if (tableMargins && cellOpts.margins && sameCellMargins(cellOpts.margins, tableMargins)) delete cellOpts.margins;
446
+ const cellAlign = cellNode.attrs?.align ?? null;
447
+ const cellChildren = [];
448
+ for (const paraNode of cellNode.content ?? []) {
449
+ const para = this.compileParagraphNode(paraNode);
450
+ const paraObj = typeof para === "string" ? { text: para } : para;
451
+ if (cellAlign && !paraObj.alignment) paraObj.alignment = cellAlign;
452
+ cellChildren.push({ paragraph: paraObj });
453
+ }
454
+ if (cellChildren.length > 0) cellOpts.children = cellChildren;
455
+ return cellOpts;
456
+ }
457
+ compileListFromNode(node, level) {
458
+ const items = [];
459
+ const isOrdered = node.type === "orderedList";
460
+ const isTask = node.type === "taskList";
461
+ let ordered;
462
+ if (isOrdered) ordered = this.registerOrderedNumbering(node);
463
+ for (const listItem of node.content ?? []) {
464
+ if (listItem.type !== "listItem" && listItem.type !== "taskItem") continue;
465
+ const checked = Boolean(listItem.attrs?.checked);
466
+ for (const child of listItem.content ?? []) if (child.type === "paragraph" || child.type === "heading") {
467
+ const para = child.type === "heading" ? this.compileHeadingNode(child) : this.compileParagraphNode(child);
468
+ const paraObj = typeof para === "string" ? { text: para } : para;
469
+ if (ordered) paraObj.numbering = {
470
+ reference: ordered.reference,
471
+ instance: ordered.instance,
472
+ level
473
+ };
474
+ else paraObj.bullet = { level };
475
+ if (isTask) this.injectTaskCheckbox(paraObj, checked);
476
+ items.push({ paragraph: paraObj });
477
+ } else if (child.type === "bulletList" || child.type === "orderedList" || child.type === "taskList") {
478
+ const nested = this.compileListFromNode(child, level + 1);
479
+ if (nested) items.push(...nested);
480
+ }
481
+ }
482
+ return items.length > 0 ? items : null;
483
+ }
484
+ /**
485
+ * Register (or reuse) an abstractNum for an ordered list's `start`, and
486
+ * return a fresh instance so this list counts independently of other lists
487
+ * that share the same definition.
488
+ */
489
+ registerOrderedNumbering(node) {
490
+ const start = Number(node.attrs?.start ?? 1) || 1;
491
+ let entry = this.numberingConfigs.find((c) => Number(c.levels[0]?.start ?? 1) === start);
492
+ if (!entry) {
493
+ entry = {
494
+ reference: `${ORDERED_REFERENCE_PREFIX}-${this.numberingConfigs.length + 1}`,
495
+ levels: buildOrderedLevels(start)
496
+ };
497
+ this.numberingConfigs.push(entry);
498
+ }
499
+ this.orderedInstanceCounter += 1;
500
+ return {
501
+ reference: entry.reference,
502
+ instance: this.orderedInstanceCounter
503
+ };
504
+ }
505
+ /**
506
+ * Prepend an inline checkbox SDT to a task paragraph. The SDT is tagged
507
+ * "docen-task" so resolve can tell task items apart from ordinary paragraphs
508
+ * that happen to contain an SDT.
509
+ */
510
+ injectTaskCheckbox(paraObj, checked) {
511
+ let existing = [];
512
+ if (Array.isArray(paraObj.children)) existing = paraObj.children;
513
+ else if (typeof paraObj.text === "string") {
514
+ if (paraObj.text) existing = [{ text: paraObj.text }];
515
+ delete paraObj.text;
516
+ }
517
+ paraObj.children = [createTaskCheckbox(checked), ...existing];
518
+ }
519
+ /**
520
+ * details → block-level group-SDT. The summary paragraph is tagged with a
521
+ * fixed style so resolve can split it back out; content blocks flatten in
522
+ * after it. (No native collapse in DOCX — structure round-trips, the view
523
+ * stays expanded.)
524
+ */
525
+ compileDetailsNode(node) {
526
+ const sdtChildren = [];
527
+ for (const child of node.content ?? []) if (child.type === "detailsSummary") {
528
+ const inline = this.compileInlineContent(child.content);
529
+ const summaryPara = { style: DETAILS_SUMMARY_STYLE };
530
+ if (inline.length > 0) summaryPara.children = inline;
531
+ sdtChildren.push({ paragraph: summaryPara });
532
+ } else if (child.type === "detailsContent") for (const block of child.content ?? []) {
533
+ const compiled = this.compileSectionChild(block);
534
+ if (!compiled) continue;
535
+ if (Array.isArray(compiled)) sdtChildren.push(...compiled);
536
+ else sdtChildren.push(compiled);
537
+ }
538
+ return { sdt: {
539
+ properties: {
540
+ tag: DETAILS_TAG,
541
+ group: true
542
+ },
543
+ children: sdtChildren
544
+ } };
545
+ }
546
+ compileInlineContent(content) {
547
+ if (!content) return [];
548
+ const children = [];
549
+ for (const node of content) switch (node.type) {
550
+ case "text":
551
+ this.compileTextNode(node, children);
552
+ break;
553
+ case "hardBreak":
554
+ children.push({ break: 1 });
555
+ break;
556
+ case "pageBreak":
557
+ children.push({ pageBreak: true });
558
+ break;
559
+ case "columnBreak":
560
+ children.push({ columnBreak: true });
561
+ break;
562
+ case "image": {
563
+ const imageRun = renderDocx$2(node);
564
+ if (imageRun) children.push(imageRun);
565
+ break;
566
+ }
567
+ case "wpgGroup": {
568
+ const wpgGroup = node.attrs?.wpgGroup;
569
+ if (wpgGroup) children.push({ wpgGroup });
570
+ break;
571
+ }
572
+ case "wpsShape": {
573
+ const wpsShape = node.attrs?.wpsShape;
574
+ if (wpsShape) children.push({ wpsShape });
575
+ break;
576
+ }
577
+ case "mention":
578
+ children.push(createMention(String(node.attrs?.id ?? ""), String(node.attrs?.label ?? "")));
579
+ break;
580
+ case "emoji": {
581
+ const name = String(node.attrs?.name ?? "");
582
+ const text = (name ? shortcodeToEmoji(name, emojis)?.emoji : void 0) ?? (name ? `:${name}:` : "");
583
+ if (text) children.push({ text });
584
+ break;
585
+ }
586
+ }
587
+ return children;
588
+ }
589
+ compileTextNode(node, children) {
590
+ const text = node.text ?? "";
591
+ if (!text) return;
592
+ const segments = text.split("\n");
593
+ for (let i = 0; i < segments.length; i++) {
594
+ if (i > 0) children.push({ break: 1 });
595
+ if (segments[i]) this.compileTextRun(segments[i], node.marks, children);
596
+ }
597
+ }
598
+ /** Emit a single run for `text` with all inline marks applied. */
599
+ compileTextRun(text, marks, children) {
600
+ const runOpts = { text };
601
+ for (const mark of marks ?? []) switch (mark.type) {
602
+ case "bold":
603
+ runOpts.bold = true;
604
+ break;
605
+ case "italic":
606
+ runOpts.italic = true;
607
+ break;
608
+ case "underline":
609
+ runOpts.underline = { type: "single" };
610
+ break;
611
+ case "strike": {
612
+ const strikeProps = renderDocx$4(mark.attrs ?? {});
613
+ Object.assign(runOpts, strikeProps);
614
+ break;
615
+ }
616
+ case "subscript":
617
+ runOpts.subScript = true;
618
+ break;
619
+ case "superscript":
620
+ runOpts.superScript = true;
621
+ break;
622
+ case "highlight":
623
+ runOpts.highlight = mark.attrs?.color ?? "yellow";
624
+ break;
625
+ case "code":
626
+ runOpts.style = "CodeChar";
627
+ runOpts.font = "Consolas";
628
+ break;
629
+ case "textStyle": {
630
+ const tsProps = renderDocx$9(mark.attrs ?? {});
631
+ Object.assign(runOpts, tsProps);
632
+ break;
633
+ }
634
+ case "link": {
635
+ const href = mark.attrs?.href;
636
+ if (href) {
637
+ const { text: _, ...runWithoutText } = runOpts;
638
+ const linkChildren = [];
639
+ if (text) linkChildren.push({
640
+ ...runWithoutText,
641
+ text
642
+ });
643
+ children.push({ hyperlink: {
644
+ link: href.startsWith("#") ? void 0 : href,
645
+ anchor: href.startsWith("#") ? href.slice(1) : void 0,
646
+ children: linkChildren
647
+ } });
648
+ return;
649
+ }
650
+ break;
651
+ }
652
+ }
653
+ children.push(runOpts);
654
+ }
655
+ resolveSectionChild(child) {
656
+ if ("paragraph" in child) return this.resolveParagraph(child.paragraph);
657
+ if ("table" in child) return this.resolveTable(child.table);
658
+ if ("sdt" in child) {
659
+ const sdt = child.sdt;
660
+ if (sdt.properties?.tag === "docen-details") return this.resolveDetailsSdt(sdt);
661
+ return this.resolvePassthrough(child);
662
+ }
663
+ if ("toc" in child) return this.resolveToc(child.toc);
664
+ return this.resolvePassthrough(child);
665
+ }
666
+ /** Wrap an opaque SectionChild in a passthrough atom (attrs.data = JSON). */
667
+ resolvePassthrough(child) {
668
+ return {
669
+ type: "passthrough",
670
+ attrs: { data: JSON.stringify(child) }
671
+ };
672
+ }
673
+ /**
674
+ * Resolve a table of contents into an editable `tableOfContents` container:
675
+ * `attrs.options` carries the field switches, `content` is the entry
676
+ * paragraphs. Each entry's inner HYPERLINK field has content-less runs that
677
+ * office-open parses as `null`; resolving the entries through
678
+ * `resolveSectionChild` → `resolveParagraphChildren` drops those nulls, so the
679
+ * TOC no longer reaches the generate path as an opaque blob of nulls (the
680
+ * `stringifyRunInline(null).break` crash). When `entries` is absent/empty
681
+ * (a fresh, unrendered TOC), keep the node valid for `content: "block+"` with
682
+ * a placeholder empty paragraph.
683
+ */
684
+ resolveToc(toc) {
685
+ const { entries, ...options } = toc;
686
+ const content = [];
687
+ for (const entry of entries ?? []) {
688
+ const node = this.resolveSectionChild(entry);
689
+ if (!node) continue;
690
+ if (Array.isArray(node)) content.push(...node);
691
+ else content.push(node);
692
+ }
693
+ if (content.length === 0) content.push({ type: "paragraph" });
694
+ const node = {
695
+ type: "tocField",
696
+ content
697
+ };
698
+ const cleanOptions = cleanAttrs(options);
699
+ if (Object.keys(cleanOptions).length > 0) node.attrs = { options: cleanOptions };
700
+ return node;
701
+ }
702
+ /**
703
+ * Resolve a details group-SDT: the summary-style paragraph becomes
704
+ * detailsSummary, the remaining blocks fold into detailsContent.
705
+ */
706
+ resolveDetailsSdt(sdt) {
707
+ const content = [];
708
+ let summary = null;
709
+ for (const child of sdt.children ?? []) {
710
+ if ("paragraph" in child) {
711
+ const para = child.paragraph;
712
+ if (para.style === "DocenDetailsSummary") {
713
+ summary = this.resolveInlineContent(para);
714
+ continue;
715
+ }
716
+ }
717
+ const node = this.resolveSectionChild(child);
718
+ if (!node) continue;
719
+ if (Array.isArray(node)) content.push(...node);
720
+ else content.push(node);
721
+ }
722
+ const details = {
723
+ type: "details",
724
+ content: []
725
+ };
726
+ if (summary !== null) details.content.push({
727
+ type: "detailsSummary",
728
+ content: summary
729
+ });
730
+ if (content.length > 0) details.content.push({
731
+ type: "detailsContent",
732
+ content
733
+ });
734
+ return details;
735
+ }
736
+ resolveParagraph(opts) {
737
+ const resolved = typeof opts === "string" ? { text: opts } : opts;
738
+ if (resolved.thematicBreak) return { type: "horizontalRule" };
739
+ if (resolved.style === "Code") return this.resolveCodeBlock(resolved);
740
+ const headingLevel = this.detectHeadingLevel(resolved);
741
+ const nodeType = headingLevel ? "heading" : "paragraph";
742
+ const attrs = headingLevel ? parseDocx(resolved) : parseDocx$2(resolved);
743
+ if (headingLevel && attrs.level == null) attrs.level = headingLevel;
744
+ const content = this.resolveInlineContent(resolved);
745
+ const cleanAttrsObj = cleanAttrs(attrs);
746
+ const node = { type: nodeType };
747
+ if (Object.keys(cleanAttrsObj).length > 0) node.attrs = cleanAttrsObj;
748
+ if (content.length > 0) node.content = content;
749
+ return node;
750
+ }
751
+ /** Heading level (1-9) for a paragraph, or undefined when it isn't a heading.
752
+ * DOCX marks a heading several ways, checked in priority order:
753
+ * 1. office-open lifts a HeadingLevel pStyle ("Heading1".."Title") into `heading`.
754
+ * 2. An explicit `outlineLevel` (0-8 → 1-9) — Word's outline/TOC key off this
755
+ * even without a heading pStyle; the Heading1-9 styles carry outlineLvl 0-8.
756
+ * 3. A pStyle that names a heading style: directly ("Heading7", which stays on
757
+ * `style` because office-open's HeadingLevel type caps at 6), by localized
758
+ * NAME ("heading 1"/"标题 1"), or via the `basedOn` chain (a custom style
759
+ * "MyTitle" basedOn="Heading1"). `heading` and `style` carry the same pStyle.
760
+ * `outlineLevel` is read loosely — office-open's public type omits the field
761
+ * even though it round-trips (w:outlineLvl) at runtime. */
762
+ detectHeadingLevel(resolved) {
763
+ if (resolved.heading) {
764
+ const lvl = HEADING_LEVEL_MAP[resolved.heading];
765
+ if (lvl) return lvl;
766
+ }
767
+ const outline = resolved.outlineLevel;
768
+ if (typeof outline === "number" && outline >= 0 && outline <= 8) return outline + 1;
769
+ const styleId = resolved.style;
770
+ if (!styleId || !this.resolveStyles) return void 0;
771
+ const byId = indexParagraphStyles(this.resolveStyles);
772
+ const visited = /* @__PURE__ */ new Set();
773
+ let curId = styleId;
774
+ while (curId && !visited.has(curId)) {
775
+ visited.add(curId);
776
+ if (HEADING_LEVEL_MAP[curId]) return HEADING_LEVEL_MAP[curId];
777
+ const style = byId.get(curId);
778
+ if (!style) break;
779
+ const lvl = headingLevelFromName(style.name);
780
+ if (lvl) return lvl;
781
+ curId = style.basedOn ?? void 0;
782
+ }
783
+ }
784
+ /** reference → level-0 format/start, for classifying numbering paragraphs. */
785
+ buildNumberingLookup(docOpts) {
786
+ const lookup = /* @__PURE__ */ new Map();
787
+ const config = docOpts.numbering?.config;
788
+ if (config) for (const entry of config) {
789
+ const lvl0 = entry.levels[0];
790
+ lookup.set(entry.reference, {
791
+ format: lvl0?.format,
792
+ start: lvl0?.start
793
+ });
794
+ }
795
+ return lookup;
796
+ }
797
+ /**
798
+ * Walk section children, grouping consecutive list paragraphs into nested
799
+ * Tiptap lists. Non-list children resolve individually. DOCX flattens lists
800
+ * to a paragraph sequence (depth carried by `level`); this rebuilds the tree.
801
+ */
802
+ resolveSectionChildren(children, numberingLookup) {
803
+ const content = [];
804
+ let i = 0;
805
+ while (i < children.length) {
806
+ const child = children[i];
807
+ const firstPara = "paragraph" in child ? child.paragraph : null;
808
+ if (!(firstPara && typeof firstPara !== "string" ? this.detectList(firstPara, numberingLookup) : null)) {
809
+ if (firstPara && typeof firstPara !== "string" && this.detectBlockquote(firstPara)) {
810
+ const group = [];
811
+ while (i < children.length) {
812
+ const member = children[i];
813
+ if (!("paragraph" in member)) break;
814
+ const para = member.paragraph;
815
+ if (typeof para === "string" || !this.detectBlockquote(para)) break;
816
+ group.push(para);
817
+ i++;
818
+ }
819
+ content.push(this.buildBlockquote(group));
820
+ continue;
821
+ }
822
+ const node = this.resolveSectionChild(child);
823
+ if (node) content.push(node);
824
+ i++;
825
+ continue;
826
+ }
827
+ const group = [];
828
+ while (i < children.length) {
829
+ const member = children[i];
830
+ if (!("paragraph" in member)) break;
831
+ const para = member.paragraph;
832
+ if (typeof para === "string") break;
833
+ const info = this.detectList(para, numberingLookup);
834
+ if (!info) break;
835
+ group.push({
836
+ para,
837
+ info
838
+ });
839
+ i++;
840
+ }
841
+ content.push(...this.buildListTree(group));
842
+ }
843
+ return content;
844
+ }
845
+ /** Classify a paragraph as a list item, or null if it isn't one. */
846
+ detectList(para, lookup) {
847
+ const p = para;
848
+ const numbering = p.numbering;
849
+ const bullet = p.bullet;
850
+ let kind;
851
+ let level;
852
+ let reference;
853
+ let start;
854
+ if (numbering) {
855
+ reference = numbering.reference;
856
+ level = numbering.level ?? 0;
857
+ const cfg = reference ? lookup.get(reference) : void 0;
858
+ if (cfg && cfg.format && cfg.format !== "bullet") {
859
+ kind = "ordered";
860
+ start = cfg.start;
861
+ } else {
862
+ kind = "bullet";
863
+ reference = void 0;
864
+ }
865
+ } else if (bullet) {
866
+ kind = "bullet";
867
+ level = bullet.level ?? 0;
868
+ } else return null;
869
+ const first = p.children?.[0];
870
+ return {
871
+ kind: isTaskCheckbox(first) ? "task" : kind,
872
+ level,
873
+ reference,
874
+ start,
875
+ checked: readCheckboxState(first)
876
+ };
877
+ }
878
+ /**
879
+ * Rebuild nested Tiptap lists from a flat run of list paragraphs. Stack-based:
880
+ * each frame is an active list at a given depth; the `key` (level:type:
881
+ * reference) decides whether a paragraph continues the top list, starts a
882
+ * nested list, or splits off a new sibling list.
883
+ */
884
+ buildListTree(group) {
885
+ const topLevel = [];
886
+ const stack = [];
887
+ for (const { para, info } of group) {
888
+ const listType = info.kind === "ordered" ? "orderedList" : info.kind === "task" ? "taskList" : "bulletList";
889
+ const itemType = info.kind === "task" ? "taskItem" : "listItem";
890
+ const key = `${info.level}:${listType}:${info.reference ?? ""}`;
891
+ while (stack.length > 0) {
892
+ const top = stack[stack.length - 1];
893
+ if (top.level > info.level || top.level === info.level && top.key !== key) {
894
+ stack.pop();
895
+ continue;
896
+ }
897
+ break;
898
+ }
899
+ const newItem = {
900
+ type: itemType,
901
+ content: [this.resolveListItemParagraph(para, info)]
902
+ };
903
+ if (itemType === "taskItem") newItem.attrs = { checked: info.checked };
904
+ const top = stack[stack.length - 1];
905
+ if (top && top.level === info.level && top.key === key) {
906
+ top.listNode.content.push(newItem);
907
+ top.currentItem = newItem;
908
+ } else {
909
+ const newList = {
910
+ type: listType,
911
+ content: [newItem]
912
+ };
913
+ if (listType === "orderedList" && info.level === 0 && typeof info.start === "number" && info.start !== 1) newList.attrs = { start: info.start };
914
+ if (top) top.currentItem.content.push(newList);
915
+ else topLevel.push(newList);
916
+ stack.push({
917
+ level: info.level,
918
+ key,
919
+ listNode: newList,
920
+ currentItem: newItem
921
+ });
922
+ }
923
+ }
924
+ return topLevel;
925
+ }
926
+ /** Classify a paragraph as a blockquote member by its signature. */
927
+ detectBlockquote(para) {
928
+ const p = para;
929
+ const indent = p.indent;
930
+ const border = p.border;
931
+ if (!indent || indent.left !== 720) return false;
932
+ const bl = border?.left;
933
+ if (!bl) return false;
934
+ const sig = BLOCKQUOTE_BORDER;
935
+ return bl.style === sig.style && bl.size === sig.size && bl.space === sig.space && bl.color === sig.color;
936
+ }
937
+ /**
938
+ * Rebuild a blockquote node from a run of signature-carrying paragraphs,
939
+ * stripping the indent/border signature so child paragraphs render clean.
940
+ */
941
+ buildBlockquote(group) {
942
+ const content = [];
943
+ for (const para of group) {
944
+ const node = this.resolveParagraph(para);
945
+ const attrs = node.attrs;
946
+ if (attrs) {
947
+ if (attrs.indent) {
948
+ const indent = { ...attrs.indent };
949
+ delete indent.left;
950
+ attrs.indent = Object.keys(indent).length > 0 ? indent : void 0;
951
+ }
952
+ if (attrs.border) {
953
+ const border = { ...attrs.border };
954
+ delete border.left;
955
+ attrs.border = Object.keys(border).length > 0 ? border : void 0;
956
+ }
957
+ const cleaned = cleanAttrs(attrs);
958
+ if (Object.keys(cleaned).length > 0) node.attrs = cleaned;
959
+ else delete node.attrs;
960
+ }
961
+ content.push(node);
962
+ }
963
+ return {
964
+ type: "blockquote",
965
+ content
966
+ };
967
+ }
968
+ /**
969
+ * Resolve a list-item paragraph to a Tiptap paragraph/heading node, stripping
970
+ * the list marker (bullet/numbering) and the leading task checkbox — those
971
+ * are expressed at the list/item level, not inside the paragraph.
972
+ */
973
+ resolveListItemParagraph(para, info) {
974
+ const resolved = typeof para === "string" ? { text: para } : para;
975
+ const headingLevel = this.detectHeadingLevel(resolved);
976
+ const nodeType = headingLevel ? "heading" : "paragraph";
977
+ const attrs = headingLevel ? parseDocx(resolved) : parseDocx$2(resolved);
978
+ if (headingLevel && attrs.level == null) attrs.level = headingLevel;
979
+ const stripped = info.kind === "task" ? this.stripTaskCheckbox(resolved) : resolved;
980
+ const content = this.resolveInlineContent(stripped);
981
+ const node = { type: nodeType };
982
+ const cleanAttrsObj = cleanAttrs(attrs);
983
+ if (Object.keys(cleanAttrsObj).length > 0) node.attrs = cleanAttrsObj;
984
+ if (content.length > 0) node.content = content;
985
+ return node;
986
+ }
987
+ /** Return a copy of `para` with its leading docen-task checkbox SDT removed. */
988
+ stripTaskCheckbox(para) {
989
+ const children = para.children;
990
+ if (Array.isArray(children) && children.length > 0 && isTaskCheckbox(children[0])) return {
991
+ ...para,
992
+ children: children.slice(1)
993
+ };
994
+ return para;
995
+ }
996
+ resolveCodeBlock(opts) {
997
+ const children = opts.children;
998
+ const content = [];
999
+ if (children) {
1000
+ for (const child of children) if (typeof child === "string") {
1001
+ if (child) content.push({
1002
+ type: "text",
1003
+ text: child
1004
+ });
1005
+ } else if (typeof child === "object" && child !== null) {
1006
+ if ("break" in child) {
1007
+ const prev = content[content.length - 1];
1008
+ if (prev && prev.type === "text") prev.text = (prev.text ?? "") + "\n";
1009
+ else content.push({
1010
+ type: "text",
1011
+ text: "\n"
1012
+ });
1013
+ } else if ("text" in child) {
1014
+ const marks = this.resolveMarks(child);
1015
+ const textNode = {
1016
+ type: "text",
1017
+ text: child.text
1018
+ };
1019
+ if (marks) textNode.marks = marks;
1020
+ content.push(textNode);
1021
+ }
1022
+ }
1023
+ } else if (opts.text) content.push({
1024
+ type: "text",
1025
+ text: opts.text
1026
+ });
1027
+ const node = { type: "codeBlock" };
1028
+ if (content.length > 0) node.content = content;
1029
+ return node;
1030
+ }
1031
+ resolveTable(tableOpts) {
1032
+ const attrs = parseDocx$4(tableOpts);
1033
+ const rows = tableOpts.rows ?? [];
1034
+ const content = [];
1035
+ const tableCellMargins = tableOpts.cellMargin ?? tableOpts.margins ?? null;
1036
+ let activeSpans = /* @__PURE__ */ new Map();
1037
+ for (const row of rows) {
1038
+ const rowAttrs = parseDocx$7(row);
1039
+ const cells = row.cells ?? [];
1040
+ const cellNodes = [];
1041
+ const nextActiveSpans = /* @__PURE__ */ new Map();
1042
+ let colIdx = 0;
1043
+ for (const cell of cells) {
1044
+ const cellColspan = cell.columnSpan ?? 1;
1045
+ const vMerge = cell.verticalMerge;
1046
+ if (vMerge === "continue") {
1047
+ const owner = activeSpans.get(colIdx);
1048
+ if (owner) {
1049
+ const ownerAttrs = owner.attrs ??= {};
1050
+ ownerAttrs.rowspan = (ownerAttrs.rowspan ?? 1) + 1;
1051
+ for (let c = colIdx; c < colIdx + cellColspan; c++) nextActiveSpans.set(c, owner);
1052
+ }
1053
+ colIdx += cellColspan;
1054
+ continue;
1055
+ }
1056
+ const isHeader = row.tableHeader;
1057
+ const cellAttrs = isHeader ? parseDocx$6(cell) : parseDocx$5(cell);
1058
+ delete cellAttrs.verticalMerge;
1059
+ if (!cellAttrs.margins && tableCellMargins) cellAttrs.margins = tableCellMargins;
1060
+ const cellChildren = cell.children ?? [];
1061
+ const cellContent = [];
1062
+ for (const cc of cellChildren) {
1063
+ const resolved = this.resolveSectionChild(cc);
1064
+ if (resolved) cellContent.push(resolved);
1065
+ }
1066
+ const cellNode = { type: isHeader ? "tableHeader" : "tableCell" };
1067
+ if (Object.keys(cellAttrs).length > 0) cellNode.attrs = cleanAttrs(cellAttrs);
1068
+ if (cellContent.length > 0) cellNode.content = cellContent;
1069
+ else cellNode.content = [{ type: "paragraph" }];
1070
+ if (vMerge === "restart") for (let c = colIdx; c < colIdx + cellColspan; c++) nextActiveSpans.set(c, cellNode);
1071
+ cellNodes.push(cellNode);
1072
+ colIdx += cellColspan;
1073
+ }
1074
+ const gridAfter = row.gridAfter ?? 0;
1075
+ if (gridAfter > 0) {
1076
+ const trailingType = row.tableHeader ? "tableHeader" : "tableCell";
1077
+ const nilBorders = {
1078
+ top: { style: "nil" },
1079
+ right: { style: "nil" },
1080
+ bottom: { style: "nil" },
1081
+ left: { style: "nil" }
1082
+ };
1083
+ for (let c = 0; c < gridAfter; c++) cellNodes.push({
1084
+ type: trailingType,
1085
+ attrs: { borders: nilBorders },
1086
+ content: [{ type: "paragraph" }]
1087
+ });
1088
+ colIdx += gridAfter;
1089
+ }
1090
+ activeSpans = nextActiveSpans;
1091
+ const rowNode = { type: "tableRow" };
1092
+ if (Object.keys(rowAttrs).length > 0) rowNode.attrs = cleanAttrs(rowAttrs);
1093
+ if (cellNodes.length > 0) rowNode.content = cellNodes;
1094
+ content.push(rowNode);
1095
+ }
1096
+ const node = { type: "table" };
1097
+ if (Object.keys(attrs).length > 0) node.attrs = cleanAttrs(attrs);
1098
+ if (content.length > 0) node.content = content;
1099
+ return node;
1100
+ }
1101
+ /**
1102
+ * Resolve a paragraph's inline content. @office-open collapses a plain-text
1103
+ * paragraph (a single run with no properties) to a bare string or a `{ text }`
1104
+ * object with no `children` — recover that text here so it round-trips back to
1105
+ * a text node instead of being dropped.
1106
+ */
1107
+ resolveInlineContent(opts) {
1108
+ const content = this.resolveParagraphChildren(opts.children);
1109
+ if (content.length === 0 && opts.text) {
1110
+ const marks = this.resolveMarks(opts);
1111
+ const node = {
1112
+ type: "text",
1113
+ text: opts.text
1114
+ };
1115
+ if (marks) node.marks = marks;
1116
+ return [node];
1117
+ }
1118
+ return content;
1119
+ }
1120
+ resolveParagraphChildren(children) {
1121
+ if (!children || children.length === 0) return [];
1122
+ const nodes = [];
1123
+ for (const child of children) {
1124
+ if (typeof child === "string") {
1125
+ if (child) nodes.push({
1126
+ type: "text",
1127
+ text: child
1128
+ });
1129
+ continue;
1130
+ }
1131
+ if (typeof child === "object" && child !== null) {
1132
+ const resolved = this.resolveParagraphChild(child);
1133
+ if (resolved) nodes.push(...Array.isArray(resolved) ? resolved : [resolved]);
1134
+ }
1135
+ }
1136
+ return nodes;
1137
+ }
1138
+ resolveParagraphChild(child) {
1139
+ if ("text" in child || "children" in child || "break" in child) return this.resolveRun(child);
1140
+ if ("image" in child) return this.resolveImage(child.image);
1141
+ if ("wpgGroup" in child) return {
1142
+ type: "wpgGroup",
1143
+ attrs: { wpgGroup: child.wpgGroup }
1144
+ };
1145
+ if ("wpsShape" in child) return {
1146
+ type: "wpsShape",
1147
+ attrs: { wpsShape: child.wpsShape }
1148
+ };
1149
+ if ("sdt" in child) return this.resolveInlineSdt(child);
1150
+ if ("hyperlink" in child) return this.resolveHyperlink(child.hyperlink);
1151
+ if ("pageBreak" in child) return { type: "pageBreak" };
1152
+ if ("columnBreak" in child) return { type: "columnBreak" };
1153
+ return null;
1154
+ }
1155
+ /** Resolve an inline SDT (mention carrier; other inline SDTs unsupported). */
1156
+ resolveInlineSdt(child) {
1157
+ if (isMention(child)) {
1158
+ const { id, label } = readMention(child);
1159
+ return {
1160
+ type: "mention",
1161
+ attrs: {
1162
+ id,
1163
+ label
1164
+ }
1165
+ };
1166
+ }
1167
+ return null;
1168
+ }
1169
+ resolveRun(opts) {
1170
+ if (opts.break && opts.text === void 0 && !opts.children) return { type: "hardBreak" };
1171
+ const text = opts.text;
1172
+ if (text === void 0 && !opts.children) return null;
1173
+ if (opts.children && !text) {
1174
+ const parts = [];
1175
+ for (const c of opts.children) if (typeof c === "string") parts.push(c);
1176
+ if (parts.length === 0) return null;
1177
+ return {
1178
+ type: "text",
1179
+ text: parts.join(""),
1180
+ marks: this.resolveMarks(opts)
1181
+ };
1182
+ }
1183
+ return {
1184
+ type: "text",
1185
+ text: text ?? "",
1186
+ marks: this.resolveMarks(opts)
1187
+ };
1188
+ }
1189
+ resolveMarks(opts) {
1190
+ const marks = [];
1191
+ if (opts.bold) marks.push({ type: "bold" });
1192
+ if (opts.italic) marks.push({ type: "italic" });
1193
+ if (opts.underline) marks.push({ type: "underline" });
1194
+ if (opts.strike) {
1195
+ const strikeAttrs = parseDocx$3(opts);
1196
+ marks.push({
1197
+ type: "strike",
1198
+ attrs: { doubleStrike: strikeAttrs.doubleStrike ?? null }
1199
+ });
1200
+ }
1201
+ if (opts.subScript) marks.push({ type: "subscript" });
1202
+ if (opts.superScript) marks.push({ type: "superscript" });
1203
+ if (opts.highlight) marks.push({
1204
+ type: "highlight",
1205
+ attrs: { color: opts.highlight }
1206
+ });
1207
+ if (opts.style === "CodeChar") marks.push({ type: "code" });
1208
+ const textStyleAttrs = parseDocx$8(opts);
1209
+ if (opts.style === "CodeChar") {
1210
+ delete textStyleAttrs.font;
1211
+ delete textStyleAttrs.styleId;
1212
+ }
1213
+ if (Object.keys(textStyleAttrs).length > 0) marks.push({
1214
+ type: "textStyle",
1215
+ attrs: textStyleAttrs
1216
+ });
1217
+ return marks.length > 0 ? marks : void 0;
1218
+ }
1219
+ resolveImage(imageOpts) {
1220
+ const attrs = parseDocx$1(imageOpts);
1221
+ const data = imageOpts.data;
1222
+ const type = imageOpts.type;
1223
+ if (data && type) attrs.src = `data:image/${type};base64,${encodeBase64(data instanceof ArrayBuffer ? new Uint8Array(data) : data)}`;
1224
+ return {
1225
+ type: "image",
1226
+ attrs
1227
+ };
1228
+ }
1229
+ resolveHyperlink(hyperlink) {
1230
+ const href = hyperlink.link ?? (hyperlink.anchor ? `#${hyperlink.anchor}` : "");
1231
+ if (!href) return null;
1232
+ const content = this.resolveParagraphChildren((hyperlink.children ?? []).map((c) => c));
1233
+ if (content.length > 0) {
1234
+ const merged = mergeTextNodes(content);
1235
+ for (const node of merged) if (node.type === "text") node.marks = [...node.marks ?? [], {
1236
+ type: "link",
1237
+ attrs: {
1238
+ href,
1239
+ target: "_blank",
1240
+ rel: "noopener noreferrer nofollow",
1241
+ class: null,
1242
+ title: hyperlink.tooltip ?? null
1243
+ }
1244
+ }];
1245
+ return merged;
1246
+ }
1247
+ return null;
1248
+ }
1249
+ };
1250
+ const HEADING_LEVEL_MAP = {
1251
+ Heading1: 1,
1252
+ Heading2: 2,
1253
+ Heading3: 3,
1254
+ Heading4: 4,
1255
+ Heading5: 5,
1256
+ Heading6: 6,
1257
+ Heading7: 7,
1258
+ Heading8: 8,
1259
+ Heading9: 9,
1260
+ Title: 1
1261
+ };
1262
+ /** Heading level (1-9) from a localized style NAME: "heading 1"/"标题 1" → 1,
1263
+ * "title" → 1. office-open's built-in names are English ("heading 1"), but
1264
+ * zh-CN Word labels the same styles "标题 1"; both map to the same level. */
1265
+ function headingLevelFromName(name) {
1266
+ if (!name) return void 0;
1267
+ const m = /^heading\s+(\d)$/i.exec(name) ?? /^标题\s*(\d)$/.exec(name);
1268
+ if (m) {
1269
+ const lvl = Number(m[1]);
1270
+ if (lvl >= 1 && lvl <= 9) return lvl;
1271
+ }
1272
+ return /^title$/i.test(name) ? 1 : void 0;
1273
+ }
1274
+ const defaultManager = new DocxManager();
1275
+ /**
1276
+ * Parse a DOCX file into Tiptap JSON (runtime model).
1277
+ *
1278
+ * Combines @office-open/docx's `parseDocument` (DOCX binary → DocumentOptions)
1279
+ * with `DocxManager.resolve` (DocumentOptions → Tiptap JSON).
1280
+ */
1281
+ function parseDOCX(data) {
1282
+ return defaultManager.resolve(parseDocument(data));
1283
+ }
1284
+ /**
1285
+ * Merge {@link DocxGenerateOptions.document} into the `DocumentOptions`
1286
+ * compiled from Tiptap JSON.
1287
+ *
1288
+ * - `sections`/`numbering`: compile-owned (excluded from `document`, never
1289
+ * overridden).
1290
+ * - `styles`/`externalStyles`: option wins over `json.attrs.styles`; the two
1291
+ * are mutually exclusive, so `externalStyles` clears compiled `styles`.
1292
+ * - Everything else (core properties, background, features, …): injected.
1293
+ */
1294
+ function applyDocumentOptions(base, document) {
1295
+ if (!document) return base;
1296
+ const merged = {
1297
+ ...base,
1298
+ ...document
1299
+ };
1300
+ if (document.externalStyles !== void 0) delete merged.styles;
1301
+ return merged;
1302
+ }
1303
+ /**
1304
+ * Generate a DOCX file from Tiptap JSON (runtime model), asynchronously.
1305
+ *
1306
+ * Pipeline: `prepareDocument` (default: fetch http images, in place) →
1307
+ * `DocxManager.compile` → @office-open/docx's `generateDocument`. `packer.type`
1308
+ * controls the output format (default: `"nodebuffer"` → Buffer). Non-blocking
1309
+ * (fflate Web Workers). With the default `prepare`, the input `json` is mutated
1310
+ * in place (http image URLs become embedded data URLs).
1311
+ */
1312
+ async function generateDOCX(json, options) {
1313
+ const { prepare = true, packer, document } = options ?? {};
1314
+ if (prepare !== false) await prepareDocument(json, prepare === true ? void 0 : prepare);
1315
+ return generateDocument(applyDocumentOptions(compileDocument(json), document), packer);
1316
+ }
1317
+ /**
1318
+ * Generate a DOCX file synchronously — fastest throughput, blocks the event loop.
1319
+ *
1320
+ * Pipeline: `DocxManager.compile` → `generateDocumentSync`. Does **not** run
1321
+ * `prepareDocument` (it is async); call `await prepareDocument(json)` first
1322
+ * when http images need embedding. `options.document` is still applied.
1323
+ */
1324
+ function generateDOCXSync(json, options) {
1325
+ const { packer, document } = options ?? {};
1326
+ return generateDocumentSync(applyDocumentOptions(compileDocument(json), document), packer);
1327
+ }
1328
+ /**
1329
+ * Generate a DOCX file as a `ReadableStream<Uint8Array>` — for large documents
1330
+ * or streaming HTTP responses.
1331
+ *
1332
+ * Pipeline: `prepareDocument` (default: fetch http images, in place) →
1333
+ * `DocxManager.compile` → `generateDocumentStream`. Async due to preparation.
1334
+ */
1335
+ async function generateDOCXStream(json, options) {
1336
+ const { prepare = true, packer, document } = options ?? {};
1337
+ if (prepare !== false) await prepareDocument(json, prepare === true ? void 0 : prepare);
1338
+ return generateDocumentStream(applyDocumentOptions(compileDocument(json), document), packer);
1339
+ }
1340
+ /**
1341
+ * Convert DocumentOptions (persistence model) to Tiptap JSON (runtime model).
1342
+ */
1343
+ function resolveDocument(docOpts) {
1344
+ return defaultManager.resolve(docOpts);
1345
+ }
1346
+ /**
1347
+ * Convert Tiptap JSON (runtime model) to DocumentOptions (persistence model).
1348
+ */
1349
+ function compileDocument(json) {
1350
+ return defaultManager.compile(json);
1351
+ }
1352
+ //#endregion
2
1353
  export { DocxManager, compileDocument, generateDOCX, generateDOCXStream, generateDOCXSync, parseDOCX, resolveDocument };