@docen/docx 0.3.5 → 0.4.0

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 (102) hide show
  1. package/README.md +1 -1
  2. package/dist/{blockquote-DY80QC06.d.mts → blockquote-D-1aSxEn.d.mts} +5 -1
  3. package/dist/converters/docx.d.mts +40 -66
  4. package/dist/converters/docx.mjs +248 -665
  5. package/dist/converters/html.d.mts +1 -1
  6. package/dist/converters/html.mjs +3 -4
  7. package/dist/converters/markdown.d.mts +1 -1
  8. package/dist/converters/markdown.mjs +1 -1
  9. package/dist/converters/patch.d.mts +1 -1
  10. package/dist/converters/prepare.d.mts +1 -1
  11. package/dist/converters/styles.d.mts +58 -14
  12. package/dist/converters/styles.mjs +124 -16
  13. package/dist/{core-DC0_-WcE.d.mts → core-BqyLL84S.d.mts} +34 -13
  14. package/dist/{core-BnF8XhVE.mjs → core-wNNPJiKr.mjs} +357 -25
  15. package/dist/core.d.mts +1 -1
  16. package/dist/core.mjs +1 -1
  17. package/dist/details-DsJhDP5K.d.mts +31 -0
  18. package/dist/editor.d.mts +1 -1
  19. package/dist/editor.mjs +1 -1
  20. package/dist/extensions/blockquote.d.mts +2 -3
  21. package/dist/extensions/blockquote.mjs +50 -2
  22. package/dist/extensions/bullet-list.mjs +1 -1
  23. package/dist/extensions/code-block.d.mts +2 -2
  24. package/dist/extensions/code-block.mjs +51 -5
  25. package/dist/extensions/column-break.d.mts +2 -2
  26. package/dist/extensions/column-break.mjs +2 -2
  27. package/dist/extensions/details.d.mts +2 -3
  28. package/dist/extensions/details.mjs +48 -2
  29. package/dist/extensions/document.mjs +1 -1
  30. package/dist/extensions/extensions.d.mts +13 -9
  31. package/dist/extensions/extensions.mjs +7 -3
  32. package/dist/extensions/formatting-marks.d.mts +1 -1
  33. package/dist/extensions/formatting-marks.mjs +1 -1
  34. package/dist/extensions/heading.d.mts +2 -2
  35. package/dist/extensions/heading.mjs +61 -5
  36. package/dist/extensions/image.d.mts +2 -2
  37. package/dist/extensions/image.mjs +23 -4
  38. package/dist/extensions/index.d.mts +14 -10
  39. package/dist/extensions/index.mjs +8 -4
  40. package/dist/extensions/link.d.mts +2 -2
  41. package/dist/extensions/link.mjs +30 -2
  42. package/dist/extensions/list-aggregator.d.mts +8 -0
  43. package/dist/extensions/list-aggregator.mjs +2 -0
  44. package/dist/extensions/marks.d.mts +2 -0
  45. package/dist/extensions/marks.mjs +41 -0
  46. package/dist/extensions/mention.d.mts +2 -3
  47. package/dist/extensions/mention.mjs +16 -2
  48. package/dist/extensions/ordered-list.mjs +1 -1
  49. package/dist/extensions/page-break.d.mts +2 -2
  50. package/dist/extensions/page-break.mjs +2 -2
  51. package/dist/extensions/paragraph.mjs +3 -3
  52. package/dist/extensions/passthrough.d.mts +1 -1
  53. package/dist/extensions/passthrough.mjs +1 -1
  54. package/dist/extensions/scroll.d.mts +1 -1
  55. package/dist/extensions/scroll.mjs +30 -5
  56. package/dist/extensions/section-break.d.mts +1 -1
  57. package/dist/extensions/section-break.mjs +1 -1
  58. package/dist/extensions/strike.d.mts +2 -2
  59. package/dist/extensions/strike.mjs +5 -24
  60. package/dist/extensions/tab.d.mts +2 -2
  61. package/dist/extensions/tab.mjs +2 -2
  62. package/dist/extensions/table-cell.mjs +2 -2
  63. package/dist/extensions/table-header.mjs +2 -2
  64. package/dist/extensions/table-row.mjs +2 -2
  65. package/dist/extensions/table.d.mts +2 -2
  66. package/dist/extensions/table.mjs +122 -11
  67. package/dist/extensions/task-item.d.mts +2 -3
  68. package/dist/extensions/task-item.mjs +2 -2
  69. package/dist/extensions/text-style.d.mts +2 -2
  70. package/dist/extensions/text-style.mjs +27 -28
  71. package/dist/extensions/toc-field.d.mts +2 -2
  72. package/dist/extensions/toc-field.mjs +2 -2
  73. package/dist/extensions/track-change.d.mts +2 -0
  74. package/dist/extensions/track-change.mjs +2 -0
  75. package/dist/extensions/types.d.mts +127 -8
  76. package/dist/extensions/utils.d.mts +2 -2
  77. package/dist/extensions/utils.mjs +74 -1
  78. package/dist/extensions/wpg-group.d.mts +2 -2
  79. package/dist/extensions/wpg-group.mjs +2 -2
  80. package/dist/extensions/wps-shape.d.mts +2 -2
  81. package/dist/extensions/wps-shape.mjs +2 -2
  82. package/dist/heading-Bwpa8iZY.d.mts +24 -0
  83. package/dist/index.d.mts +29 -11
  84. package/dist/index.mjs +9 -5
  85. package/dist/{link-BawPjQZR.d.mts → link-gUqW45mE.d.mts} +3 -1
  86. package/dist/marks-Dz9Vb22Q.d.mts +10 -0
  87. package/dist/{mention-BGLzLVYw.d.mts → mention-CkONDrw9.d.mts} +5 -1
  88. package/dist/{scroll-ZNeThJsJ.d.mts → scroll-BARiZ5Gm.d.mts} +9 -3
  89. package/dist/strike-Brn9sWFy.d.mts +16 -0
  90. package/dist/table-CdcjR6HD.d.mts +18 -0
  91. package/dist/{task-item-B0ntvQ1Y.d.mts → task-item-CCAC4QLi.d.mts} +3 -1
  92. package/dist/text-style-BzfcbufI.d.mts +4 -0
  93. package/dist/{utils-BJwDQts7.d.mts → utils-CfwwOowz.d.mts} +25 -1
  94. package/package.json +1 -1
  95. package/dist/details-Dd5MqqmR.d.mts +0 -17
  96. package/dist/extensions/tiptap.d.mts +0 -2
  97. package/dist/extensions/tiptap.mjs +0 -31
  98. package/dist/heading-BvqBD2zX.d.mts +0 -8
  99. package/dist/strike-BgWGvjKr.d.mts +0 -33
  100. package/dist/table-BFkfeRp9.d.mts +0 -9
  101. package/dist/text-style-BHdtXkMb.d.mts +0 -8
  102. package/dist/tiptap-BKqn41uT.d.mts +0 -31
@@ -1,30 +1,16 @@
1
- import { BLOCKQUOTE_BORDER, applyBlockquoteStyle } from "../extensions/blockquote.mjs";
2
- import { renderDocx } from "../extensions/code-block.mjs";
1
+ import { alignmentFromCss } from "../extensions/utils.mjs";
2
+ import { buildTextBlock } from "./styles.mjs";
3
+ import { applyBlockquoteStyle } from "../extensions/blockquote.mjs";
4
+ import { h as docxExtensions } from "../core-wNNPJiKr.mjs";
3
5
  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";
6
+ import { createTaskCheckbox } from "../extensions/task-item.mjs";
7
+ import { createMention } from "../extensions/mention.mjs";
7
8
  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
9
  import { prepareDocument } from "./prepare.mjs";
17
- import { indexParagraphStyles } from "./styles.mjs";
10
+ import { flattenExtensions, getExtensionField, getSchema } from "@tiptap/core";
18
11
  import { emojis, shortcodeToEmoji } from "@tiptap/extension-emoji";
19
12
  import { generateDocument, generateDocumentStream, generateDocumentSync, parseDocument } from "@office-open/docx";
20
- import { encodeBase64 } from "@office-open/core";
21
13
  //#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
14
  /** True when two cell-margin sets (w:tcMar / w:tblCellMar) match on every side's
29
15
  * size — used to detect a cell that merely echoes the table's default so its
30
16
  * redundant tcMar can be dropped for a near-identity round-trip. */
@@ -37,6 +23,14 @@ function sameCellMargins(a, b) {
37
23
  "left"
38
24
  ].every((k) => sz(a, k) === sz(b, k));
39
25
  }
26
+ /** True when two single-side borders match on style/size/color — used to detect
27
+ * a cell side that merely echoes the table's insideHorizontal/insideVertical
28
+ * so it can be dropped for a near-identity round-trip (resolveTable pushes
29
+ * those table-level grid lines onto cell sides lacking their own tcBorder). */
30
+ function sameBorder(a, b) {
31
+ if (!a || !b) return false;
32
+ return a.style === b.style && a.size === b.size && a.color === b.color;
33
+ }
40
34
  /** Keys round-tripped between DocumentOptions core properties and `doc.attrs.core`. */
41
35
  const CORE_PROPERTY_KEYS = [
42
36
  "title",
@@ -55,7 +49,7 @@ const CORE_PROPERTY_KEYS = [
55
49
  * carries in dedicated attrs (styles/background/core). Excluded from the
56
50
  * documentExtras pass-through so they aren't duplicated.
57
51
  */
58
- const COMPILE_OWNED_KEYS = new Set([
52
+ const COMPILE_OWNED_KEYS = /* @__PURE__ */ new Set([
59
53
  "sections",
60
54
  "numbering",
61
55
  "styles",
@@ -72,21 +66,6 @@ function extractCoreProperties(docOpts) {
72
66
  }
73
67
  return Object.keys(core).length > 0 ? core : null;
74
68
  }
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
69
  /**
91
70
  * Manages DOCX serialization (Tiptap JSON ↔ DocumentOptions).
92
71
  *
@@ -97,6 +76,63 @@ var DocxManager = class {
97
76
  numberingConfigs = [];
98
77
  orderedInstanceCounter = 0;
99
78
  resolveStyles;
79
+ resolveNumberingLookup;
80
+ markRender = /* @__PURE__ */ new Map();
81
+ markParse = [];
82
+ nodeRender = /* @__PURE__ */ new Map();
83
+ nodeParse = /* @__PURE__ */ new Map();
84
+ blockRules = [];
85
+ inlineRules = [];
86
+ paragraphRules = [];
87
+ aggregatorRules = [];
88
+ resolveCtx;
89
+ constructor(extensions = docxExtensions) {
90
+ const schema = getSchema(extensions);
91
+ for (const ext of flattenExtensions(extensions)) {
92
+ const name = ext.name;
93
+ if (!name) continue;
94
+ if (schema.marks[name]) {
95
+ const render = getExtensionField(ext, "renderDocx");
96
+ const parse = getExtensionField(ext, "parseDocx");
97
+ if (render) this.markRender.set(name, render);
98
+ if (parse) this.markParse.push({
99
+ name,
100
+ parse
101
+ });
102
+ } else if (schema.nodes[name]) {
103
+ const render = getExtensionField(ext, "renderDocx");
104
+ const parse = getExtensionField(ext, "parseDocx");
105
+ if (render) this.nodeRender.set(name, render);
106
+ if (parse) this.nodeParse.set(name, parse);
107
+ const blockRule = getExtensionField(ext, "parseDocxBlock");
108
+ if (blockRule) this.blockRules.push({
109
+ name,
110
+ rule: blockRule
111
+ });
112
+ const paraRule = getExtensionField(ext, "parseDocxParagraph");
113
+ if (paraRule) this.paragraphRules.push({
114
+ name,
115
+ rule: paraRule
116
+ });
117
+ }
118
+ const inlineRule = getExtensionField(ext, "parseDocxInline");
119
+ if (inlineRule) this.inlineRules.push({
120
+ name,
121
+ rule: inlineRule
122
+ });
123
+ const aggregator = getExtensionField(ext, "parseDocxAggregator");
124
+ if (aggregator) this.aggregatorRules.push({
125
+ name,
126
+ rule: aggregator
127
+ });
128
+ }
129
+ }
130
+ /** Reflective node renderDocx lookup: the node's DOCX opts, or {} when the
131
+ * node type has no renderDocx hook (degrades to a plain paragraph). node.type
132
+ * is optional on JSONContent — an absent type simply misses the map. */
133
+ renderNodeOpts(node) {
134
+ return this.nodeRender.get(node.type ?? "")?.(node) ?? {};
135
+ }
100
136
  compile(json) {
101
137
  this.numberingConfigs = [];
102
138
  this.orderedInstanceCounter = 0;
@@ -170,7 +206,7 @@ var DocxManager = class {
170
206
  * Resolve a section's header/footer group (SectionChild[] per slot) into
171
207
  * Tiptap JSON slots. Returns null when no slot has content.
172
208
  */
173
- resolveHeaderFooter(group, numberingLookup) {
209
+ resolveHeaderFooter(group) {
174
210
  if (!group) return null;
175
211
  const slots = {};
176
212
  for (const slot of [
@@ -179,7 +215,7 @@ var DocxManager = class {
179
215
  "even"
180
216
  ]) {
181
217
  const children = group[slot];
182
- if (children?.length) slots[slot] = this.resolveSectionChildren(children, numberingLookup);
218
+ if (children?.length) slots[slot] = this.resolveSectionChildren(children);
183
219
  }
184
220
  return Object.keys(slots).length > 0 ? slots : null;
185
221
  }
@@ -190,17 +226,29 @@ var DocxManager = class {
190
226
  type: "doc",
191
227
  content: [{ type: "paragraph" }]
192
228
  };
193
- const numberingLookup = this.buildNumberingLookup(docOpts);
229
+ this.resolveNumberingLookup = this.buildNumberingLookup(docOpts);
230
+ const styles = this.resolveStyles;
231
+ this.resolveCtx = {
232
+ resolveBlockStream: (children) => this.resolveSectionChildren(children),
233
+ resolveBlock: (child) => this.resolveSectionChild(child),
234
+ resolveInlineContent: (para) => this.resolveInlineContent(para),
235
+ resolveInlineChildren: (children) => this.resolveParagraphChildren(children),
236
+ resolveParagraph: (para) => this.resolveParagraph(para),
237
+ parseNodeAttrs: (type, opts) => this.nodeParse.get(type)?.(opts) ?? {},
238
+ resolveMarks: (opts) => this.resolveMarks(opts),
239
+ styles,
240
+ numberingLookup: this.resolveNumberingLookup
241
+ };
194
242
  const content = [];
195
243
  const lastIndex = sections.length - 1;
196
244
  for (let i = 0; i < sections.length; i++) {
197
245
  const section = sections[i];
198
- const sectionContent = this.resolveSectionChildren(section.children ?? [], numberingLookup);
246
+ const sectionContent = this.resolveSectionChildren(section.children ?? []);
199
247
  if (i < lastIndex) {
200
248
  const sectAttrs = {
201
249
  sectionProperties: section.properties ?? null,
202
- sectionHeaders: this.resolveHeaderFooter(section.headers, numberingLookup),
203
- sectionFooters: this.resolveHeaderFooter(section.footers, numberingLookup)
250
+ sectionHeaders: this.resolveHeaderFooter(section.headers),
251
+ sectionFooters: this.resolveHeaderFooter(section.footers)
204
252
  };
205
253
  const last = sectionContent[sectionContent.length - 1];
206
254
  if (last?.type === "paragraph") last.attrs = {
@@ -226,9 +274,9 @@ var DocxManager = class {
226
274
  if (core) attrs.core = core;
227
275
  const lastSection = sections[lastIndex];
228
276
  if (lastSection.properties) attrs.sectionProperties = lastSection.properties;
229
- const lastHeaders = this.resolveHeaderFooter(lastSection.headers, numberingLookup);
277
+ const lastHeaders = this.resolveHeaderFooter(lastSection.headers);
230
278
  if (lastHeaders) attrs.sectionHeaders = lastHeaders;
231
- const lastFooters = this.resolveHeaderFooter(lastSection.footers, numberingLookup);
279
+ const lastFooters = this.resolveHeaderFooter(lastSection.footers);
232
280
  if (lastFooters) attrs.sectionFooters = lastFooters;
233
281
  const documentExtras = {};
234
282
  for (const [k, v] of Object.entries(docOpts)) {
@@ -255,7 +303,7 @@ var DocxManager = class {
255
303
  return items.length > 0 ? items : null;
256
304
  }
257
305
  case "codeBlock": {
258
- const opts = renderDocx(node);
306
+ const opts = this.renderNodeOpts(node);
259
307
  const childList = this.compileInlineContent(node.content);
260
308
  if (childList.length > 0) opts.children = childList;
261
309
  return { paragraph: this.simplifyParagraph(opts) };
@@ -263,7 +311,7 @@ var DocxManager = class {
263
311
  case "horizontalRule": return { paragraph: { thematicBreak: true } };
264
312
  case "table": return { table: this.compileTableNode(node) };
265
313
  case "image": {
266
- const imageRun = renderDocx$2(node);
314
+ const imageRun = this.nodeRender.get(node.type)?.(node) ?? null;
267
315
  if (!imageRun) return null;
268
316
  return { paragraph: { children: [imageRun] } };
269
317
  }
@@ -297,13 +345,13 @@ var DocxManager = class {
297
345
  }
298
346
  }
299
347
  compileParagraphNode(node) {
300
- const opts = renderDocx$3(node);
348
+ const opts = this.renderNodeOpts(node);
301
349
  const childList = this.compileInlineContent(node.content);
302
350
  if (childList.length > 0) opts.children = childList;
303
351
  return this.simplifyParagraph(opts);
304
352
  }
305
353
  compileHeadingNode(node) {
306
- const opts = renderDocx$1(node);
354
+ const opts = this.renderNodeOpts(node);
307
355
  const childList = this.compileInlineContent(node.content);
308
356
  if (childList.length > 0) opts.children = childList;
309
357
  return this.simplifyParagraph(opts);
@@ -321,14 +369,17 @@ var DocxManager = class {
321
369
  return opts;
322
370
  }
323
371
  compileTableNode(node) {
324
- const opts = renderDocx$5(node);
372
+ const opts = this.renderNodeOpts(node);
325
373
  const colCount = this.getTableColumnCount(node);
326
374
  const tableCellMargins = opts.cellMargin ?? opts.margins ?? null;
375
+ const tableBorders = opts.borders ?? null;
376
+ const insideH = tableBorders?.insideHorizontal ?? null;
377
+ const insideV = tableBorders?.insideVertical ?? null;
327
378
  const rows = [];
328
379
  let activeSpans = [];
329
380
  for (const rowNode of node.content ?? []) {
330
381
  if (rowNode.type !== "tableRow") continue;
331
- const rowOpts = renderDocx$8(rowNode);
382
+ const rowOpts = this.nodeRender.get(rowNode.type)?.(rowNode) ?? {};
332
383
  const pmCells = (rowNode.content ?? []).filter((c) => c.type === "tableCell" || c.type === "tableHeader");
333
384
  const currentSpans = [...activeSpans].sort((a, b) => a.colStart - b.colStart);
334
385
  const newSpans = [];
@@ -349,7 +400,7 @@ var DocxManager = class {
349
400
  colIdx += span.colspan;
350
401
  spanIdx++;
351
402
  } else {
352
- const cell = this.compileTableCellNode(pmCells[cellIdx], tableCellMargins);
403
+ const cell = this.compileTableCellNode(pmCells[cellIdx], tableCellMargins, insideH, insideV);
353
404
  const cs = cell.columnSpan ?? 1;
354
405
  const rs = cell.rowSpan ?? 1;
355
406
  if (rs > 1) {
@@ -446,16 +497,35 @@ var DocxManager = class {
446
497
  }
447
498
  };
448
499
  }
449
- compileTableCellNode(cellNode, tableMargins) {
450
- const cellOpts = cellNode.type === "tableHeader" ? renderDocx$7(cellNode) : renderDocx$6(cellNode);
500
+ compileTableCellNode(cellNode, tableMargins, insideH, insideV) {
501
+ const cellOpts = this.renderNodeOpts(cellNode);
451
502
  if (tableMargins && cellOpts.margins && sameCellMargins(cellOpts.margins, tableMargins)) delete cellOpts.margins;
452
- const cellAlign = cellNode.attrs?.align ?? null;
503
+ if ((insideH || insideV) && cellOpts.borders) {
504
+ const b = cellOpts.borders;
505
+ if (insideH && sameBorder(b.top, insideH)) delete b.top;
506
+ if (insideH && sameBorder(b.bottom, insideH)) delete b.bottom;
507
+ if (insideV && sameBorder(b.left, insideV)) delete b.left;
508
+ if (insideV && sameBorder(b.right, insideV)) delete b.right;
509
+ if (Object.keys(b).length === 0) delete cellOpts.borders;
510
+ }
511
+ const cellAlign = alignmentFromCss(cellNode.attrs?.align ?? null);
453
512
  const cellChildren = [];
454
- for (const paraNode of cellNode.content ?? []) {
455
- const para = this.compileParagraphNode(paraNode);
456
- const paraObj = typeof para === "string" ? { text: para } : para;
457
- if (cellAlign && !paraObj.alignment) paraObj.alignment = cellAlign;
458
- cellChildren.push({ paragraph: paraObj });
513
+ const pushChild = (child) => {
514
+ if (cellAlign && typeof child === "object" && child !== null && "paragraph" in child) {
515
+ const p = child.paragraph;
516
+ if (typeof p === "string") child.paragraph = {
517
+ text: p,
518
+ alignment: cellAlign
519
+ };
520
+ else if (p && typeof p === "object" && !p.alignment) p.alignment = cellAlign;
521
+ }
522
+ cellChildren.push(child);
523
+ };
524
+ for (const childNode of cellNode.content ?? []) {
525
+ const compiled = this.compileSectionChild(childNode);
526
+ if (compiled == null) continue;
527
+ if (Array.isArray(compiled)) compiled.forEach(pushChild);
528
+ else pushChild(compiled);
459
529
  }
460
530
  if (cellChildren.length > 0) cellOpts.children = cellChildren;
461
531
  return cellOpts;
@@ -582,7 +652,7 @@ var DocxManager = class {
582
652
  break;
583
653
  }
584
654
  case "image": {
585
- const imageRun = renderDocx$2(node);
655
+ const imageRun = this.nodeRender.get(node.type)?.(node) ?? null;
586
656
  if (imageRun) children.push(imageRun);
587
657
  break;
588
658
  }
@@ -630,69 +700,65 @@ var DocxManager = class {
630
700
  /** Emit a single run for `text` with all inline marks applied. */
631
701
  compileTextRun(text, marks, children) {
632
702
  const runOpts = { text };
633
- for (const mark of marks ?? []) switch (mark.type) {
634
- case "bold":
635
- runOpts.bold = true;
636
- break;
637
- case "italic":
638
- runOpts.italic = true;
639
- break;
640
- case "underline":
641
- runOpts.underline = { type: "single" };
642
- break;
643
- case "strike": {
644
- const strikeProps = renderDocx$4(mark.attrs ?? {});
645
- Object.assign(runOpts, strikeProps);
646
- break;
647
- }
648
- case "subscript":
649
- runOpts.subScript = true;
650
- break;
651
- case "superscript":
652
- runOpts.superScript = true;
653
- break;
654
- case "highlight":
655
- runOpts.highlight = mark.attrs?.color ?? "yellow";
656
- break;
657
- case "code":
658
- runOpts.style = "CodeChar";
659
- runOpts.font = "Consolas";
660
- break;
661
- case "textStyle": {
662
- const tsProps = renderDocx$9(mark.attrs ?? {});
663
- Object.assign(runOpts, tsProps);
664
- break;
665
- }
666
- case "link": {
667
- const href = mark.attrs?.href;
668
- if (href) {
669
- const { text: _, ...runWithoutText } = runOpts;
670
- const linkChildren = [];
671
- if (text) linkChildren.push({
672
- ...runWithoutText,
673
- text
674
- });
675
- children.push({ hyperlink: {
676
- link: href.startsWith("#") ? void 0 : href,
677
- anchor: href.startsWith("#") ? href.slice(1) : void 0,
678
- children: linkChildren
679
- } });
680
- return;
703
+ for (const mark of marks ?? []) {
704
+ switch (mark.type) {
705
+ case "link": {
706
+ const href = mark.attrs?.href;
707
+ if (href) {
708
+ const { text: _, ...runWithoutText } = runOpts;
709
+ const linkChildren = [];
710
+ if (text) linkChildren.push({
711
+ ...runWithoutText,
712
+ text
713
+ });
714
+ children.push({ hyperlink: {
715
+ link: href.startsWith("#") ? void 0 : href,
716
+ anchor: href.startsWith("#") ? href.slice(1) : void 0,
717
+ children: linkChildren
718
+ } });
719
+ return;
720
+ }
721
+ break;
681
722
  }
682
- break;
723
+ case "insertion":
724
+ case "deletion":
725
+ children.push(this.compileTrackedChangeRun(mark.type, mark.attrs, text, runOpts));
726
+ return;
683
727
  }
728
+ const render = this.markRender.get(mark.type);
729
+ if (render) Object.assign(runOpts, render(mark.attrs ?? {}));
684
730
  }
685
731
  children.push(runOpts);
686
732
  }
733
+ /**
734
+ * Wrap a run back into a w:ins/w:del container — the reverse of
735
+ * resolveTrackedChange. A literal-key ternary (`{insertion: body}` /
736
+ * `{deletion: body}`) lets TS narrow to the ParagraphChild branch without a
737
+ * cast, and `typeof` guards read `attrs` type-safely (no `as number/string`).
738
+ * stringifyDeletedRun emits `<w:delText>` automatically for deletion children.
739
+ */
740
+ compileTrackedChangeRun(type, attrs, text, runOpts) {
741
+ const { text: _, ...runWithoutText } = runOpts;
742
+ const trackChildren = [];
743
+ if (text) trackChildren.push({
744
+ ...runWithoutText,
745
+ text
746
+ });
747
+ const body = {
748
+ id: typeof attrs?.id === "number" ? attrs.id : 0,
749
+ author: typeof attrs?.author === "string" ? attrs.author : "",
750
+ date: typeof attrs?.date === "string" ? attrs.date : "",
751
+ children: trackChildren
752
+ };
753
+ return type === "insertion" ? { insertion: body } : { deletion: body };
754
+ }
687
755
  resolveSectionChild(child) {
688
- if ("paragraph" in child) return this.resolveParagraph(child.paragraph);
689
- if ("table" in child) return this.resolveTable(child.table);
690
- if ("sdt" in child) {
691
- const sdt = child.sdt;
692
- if (sdt.properties?.tag === "docen-details") return this.resolveDetailsSdt(sdt);
693
- return this.resolvePassthrough(child);
756
+ const ctx = this.resolveCtx;
757
+ for (const { rule } of this.blockRules) if (rule.match(child, ctx)) {
758
+ const node = rule.convert(child, ctx);
759
+ if (node) return node;
694
760
  }
695
- if ("toc" in child) return this.resolveToc(child.toc);
761
+ if ("paragraph" in child) return this.resolveParagraph(child.paragraph);
696
762
  return this.resolvePassthrough(child);
697
763
  }
698
764
  /** Wrap an opaque SectionChild in a passthrough atom (attrs.data = JSON). */
@@ -702,116 +768,15 @@ var DocxManager = class {
702
768
  attrs: { data: JSON.stringify(child) }
703
769
  };
704
770
  }
705
- /**
706
- * Resolve a table of contents into an editable `tableOfContents` container:
707
- * `attrs.options` carries the field switches, `content` is the entry
708
- * paragraphs. Each entry's inner HYPERLINK field has content-less runs that
709
- * office-open parses as `null`; resolving the entries through
710
- * `resolveSectionChild` → `resolveParagraphChildren` drops those nulls, so the
711
- * TOC no longer reaches the generate path as an opaque blob of nulls (the
712
- * `stringifyRunInline(null).break` crash). When `entries` is absent/empty
713
- * (a fresh, unrendered TOC), keep the node valid for `content: "block+"` with
714
- * a placeholder empty paragraph.
715
- */
716
- resolveToc(toc) {
717
- const { entries, ...options } = toc;
718
- const content = [];
719
- for (const entry of entries ?? []) {
720
- const node = this.resolveSectionChild(entry);
721
- if (!node) continue;
722
- if (Array.isArray(node)) content.push(...node);
723
- else content.push(node);
724
- }
725
- if (content.length === 0) content.push({ type: "paragraph" });
726
- const node = {
727
- type: "tocField",
728
- content
729
- };
730
- const cleanOptions = cleanAttrs(options);
731
- if (Object.keys(cleanOptions).length > 0) node.attrs = { options: cleanOptions };
732
- return node;
733
- }
734
- /**
735
- * Resolve a details group-SDT: the summary-style paragraph becomes
736
- * detailsSummary, the remaining blocks fold into detailsContent.
737
- */
738
- resolveDetailsSdt(sdt) {
739
- const content = [];
740
- let summary = null;
741
- for (const child of sdt.children ?? []) {
742
- if ("paragraph" in child) {
743
- const para = child.paragraph;
744
- if (para.style === "DocenDetailsSummary") {
745
- summary = this.resolveInlineContent(para);
746
- continue;
747
- }
748
- }
749
- const node = this.resolveSectionChild(child);
750
- if (!node) continue;
751
- if (Array.isArray(node)) content.push(...node);
752
- else content.push(node);
753
- }
754
- const details = {
755
- type: "details",
756
- content: []
757
- };
758
- if (summary !== null) details.content.push({
759
- type: "detailsSummary",
760
- content: summary
761
- });
762
- if (content.length > 0) details.content.push({
763
- type: "detailsContent",
764
- content
765
- });
766
- return details;
767
- }
768
771
  resolveParagraph(opts) {
769
772
  const resolved = typeof opts === "string" ? { text: opts } : opts;
770
773
  if (resolved.thematicBreak) return { type: "horizontalRule" };
771
- if (resolved.style === "Code") return this.resolveCodeBlock(resolved);
772
- const headingLevel = this.detectHeadingLevel(resolved);
773
- const nodeType = headingLevel ? "heading" : "paragraph";
774
- const attrs = headingLevel ? parseDocx(resolved) : parseDocx$2(resolved);
775
- if (headingLevel && attrs.level == null) attrs.level = headingLevel;
776
- const content = this.resolveInlineContent(resolved);
777
- const cleanAttrsObj = cleanAttrs(attrs);
778
- const node = { type: nodeType };
779
- if (Object.keys(cleanAttrsObj).length > 0) node.attrs = cleanAttrsObj;
780
- if (content.length > 0) node.content = content;
781
- return node;
782
- }
783
- /** Heading level (1-9) for a paragraph, or undefined when it isn't a heading.
784
- * DOCX marks a heading several ways, checked in priority order:
785
- * 1. office-open lifts a HeadingLevel pStyle ("Heading1".."Title") into `heading`.
786
- * 2. An explicit `outlineLevel` (0-8 → 1-9) — Word's outline/TOC key off this
787
- * even without a heading pStyle; the Heading1-9 styles carry outlineLvl 0-8.
788
- * 3. A pStyle that names a heading style: directly ("Heading7", which stays on
789
- * `style` because office-open's HeadingLevel type caps at 6), by localized
790
- * NAME ("heading 1"/"标题 1"), or via the `basedOn` chain (a custom style
791
- * "MyTitle" basedOn="Heading1"). `heading` and `style` carry the same pStyle.
792
- * `outlineLevel` is read loosely — office-open's public type omits the field
793
- * even though it round-trips (w:outlineLvl) at runtime. */
794
- detectHeadingLevel(resolved) {
795
- if (resolved.heading) {
796
- const lvl = HEADING_LEVEL_MAP[resolved.heading];
797
- if (lvl) return lvl;
798
- }
799
- const outline = resolved.outlineLevel;
800
- if (typeof outline === "number" && outline >= 0 && outline <= 8) return outline + 1;
801
- const styleId = resolved.style;
802
- if (!styleId || !this.resolveStyles) return void 0;
803
- const byId = indexParagraphStyles(this.resolveStyles);
804
- const visited = /* @__PURE__ */ new Set();
805
- let curId = styleId;
806
- while (curId && !visited.has(curId)) {
807
- visited.add(curId);
808
- if (HEADING_LEVEL_MAP[curId]) return HEADING_LEVEL_MAP[curId];
809
- const style = byId.get(curId);
810
- if (!style) break;
811
- const lvl = headingLevelFromName(style.name);
812
- if (lvl) return lvl;
813
- curId = style.basedOn ?? void 0;
774
+ const ctx = this.resolveCtx;
775
+ for (const { rule } of this.paragraphRules) if (rule.match(resolved, ctx)) {
776
+ const node = rule.convert(resolved, ctx);
777
+ if (node) return node;
814
778
  }
779
+ return buildTextBlock("paragraph", resolved, ctx);
815
780
  }
816
781
  /** reference → level-0 format/start, for classifying numbering paragraphs. */
817
782
  buildNumberingLookup(docOpts) {
@@ -827,309 +792,43 @@ var DocxManager = class {
827
792
  return lookup;
828
793
  }
829
794
  /**
830
- * Walk section children, grouping consecutive list paragraphs into nested
831
- * Tiptap lists. Non-list children resolve individually. DOCX flattens lists
832
- * to a paragraph sequence (depth carried by `level`); this rebuilds the tree.
795
+ * Walk a SectionChild[] block stream a section's body, a header/footer
796
+ * slot, or a table cell's children (a cell is just another block stream). An
797
+ * aggregator rule (list/blockquote) claims consecutive paragraphs sharing its
798
+ * predicate and rebuilds them as a composite; everything else resolves
799
+ * individually via resolveSectionChild. The manager owns only the generic
800
+ * group-by loop — the predicate (belongs) + builder (build) come from each
801
+ * rule, so a custom composite plugs in by declaring parseDocxAggregator.
833
802
  */
834
- resolveSectionChildren(children, numberingLookup) {
803
+ resolveSectionChildren(children) {
804
+ const ctx = this.resolveCtx;
835
805
  const content = [];
836
806
  let i = 0;
837
807
  while (i < children.length) {
838
808
  const child = children[i];
839
809
  const firstPara = "paragraph" in child ? child.paragraph : null;
840
- if (!(firstPara && typeof firstPara !== "string" ? this.detectList(firstPara, numberingLookup) : null)) {
841
- if (firstPara && typeof firstPara !== "string" && this.detectBlockquote(firstPara)) {
810
+ if (firstPara && typeof firstPara !== "string") {
811
+ const rule = this.aggregatorRules.find((r) => r.rule.belongs(firstPara, ctx));
812
+ if (rule) {
842
813
  const group = [];
843
814
  while (i < children.length) {
844
815
  const member = children[i];
845
816
  if (!("paragraph" in member)) break;
846
817
  const para = member.paragraph;
847
- if (typeof para === "string" || !this.detectBlockquote(para)) break;
818
+ if (typeof para === "string" || !rule.rule.belongs(para, ctx)) break;
848
819
  group.push(para);
849
820
  i++;
850
821
  }
851
- content.push(this.buildBlockquote(group));
822
+ content.push(...rule.rule.build(group, ctx));
852
823
  continue;
853
824
  }
854
- const node = this.resolveSectionChild(child);
855
- if (node) content.push(node);
856
- i++;
857
- continue;
858
- }
859
- const group = [];
860
- while (i < children.length) {
861
- const member = children[i];
862
- if (!("paragraph" in member)) break;
863
- const para = member.paragraph;
864
- if (typeof para === "string") break;
865
- const info = this.detectList(para, numberingLookup);
866
- if (!info) break;
867
- group.push({
868
- para,
869
- info
870
- });
871
- i++;
872
825
  }
873
- content.push(...this.buildListTree(group));
826
+ const node = this.resolveSectionChild(child);
827
+ if (node) content.push(node);
828
+ i++;
874
829
  }
875
830
  return content;
876
831
  }
877
- /** Classify a paragraph as a list item, or null if it isn't one. */
878
- detectList(para, lookup) {
879
- const p = para;
880
- const numbering = p.numbering;
881
- const bullet = p.bullet;
882
- let kind;
883
- let level;
884
- let reference;
885
- let start;
886
- if (numbering) {
887
- reference = numbering.reference;
888
- level = numbering.level ?? 0;
889
- const cfg = reference ? lookup.get(reference) : void 0;
890
- if (cfg && cfg.format && cfg.format !== "bullet") {
891
- kind = "ordered";
892
- start = cfg.start;
893
- } else kind = "bullet";
894
- } else if (bullet) {
895
- kind = "bullet";
896
- level = bullet.level ?? 0;
897
- } else return null;
898
- const first = p.children?.[0];
899
- return {
900
- kind: isTaskCheckbox(first) ? "task" : kind,
901
- level,
902
- reference,
903
- start,
904
- checked: readCheckboxState(first)
905
- };
906
- }
907
- /**
908
- * Rebuild nested Tiptap lists from a flat run of list paragraphs. Stack-based:
909
- * each frame is an active list at a given depth; the `key` (level:type:
910
- * reference) decides whether a paragraph continues the top list, starts a
911
- * nested list, or splits off a new sibling list.
912
- */
913
- buildListTree(group) {
914
- const topLevel = [];
915
- const stack = [];
916
- for (const { para, info } of group) {
917
- const listType = info.kind === "ordered" ? "orderedList" : info.kind === "task" ? "taskList" : "bulletList";
918
- const itemType = info.kind === "task" ? "taskItem" : "listItem";
919
- const key = `${info.level}:${listType}:${info.reference ?? ""}`;
920
- while (stack.length > 0) {
921
- const top = stack[stack.length - 1];
922
- if (top.level > info.level || top.level === info.level && top.key !== key) {
923
- stack.pop();
924
- continue;
925
- }
926
- break;
927
- }
928
- const newItem = {
929
- type: itemType,
930
- content: [this.resolveListItemParagraph(para, info)]
931
- };
932
- if (itemType === "taskItem") newItem.attrs = { checked: info.checked };
933
- const top = stack[stack.length - 1];
934
- if (top && top.level === info.level && top.key === key) {
935
- top.listNode.content.push(newItem);
936
- top.currentItem = newItem;
937
- } else {
938
- const newList = {
939
- type: listType,
940
- content: [newItem]
941
- };
942
- const listAttrs = {};
943
- if (listType === "orderedList" && info.level === 0 && typeof info.start === "number" && info.start !== 1) listAttrs.start = info.start;
944
- if (info.reference) listAttrs.numbering = info.reference;
945
- if (Object.keys(listAttrs).length > 0) newList.attrs = listAttrs;
946
- if (top) top.currentItem.content.push(newList);
947
- else topLevel.push(newList);
948
- stack.push({
949
- level: info.level,
950
- key,
951
- listNode: newList,
952
- currentItem: newItem
953
- });
954
- }
955
- }
956
- return topLevel;
957
- }
958
- /** Classify a paragraph as a blockquote member by its signature. */
959
- detectBlockquote(para) {
960
- const p = para;
961
- const indent = p.indent;
962
- const border = p.border;
963
- if (!indent || indent.left !== 720) return false;
964
- const bl = border?.left;
965
- if (!bl) return false;
966
- const sig = BLOCKQUOTE_BORDER;
967
- return bl.style === sig.style && bl.size === sig.size && bl.space === sig.space && bl.color === sig.color;
968
- }
969
- /**
970
- * Rebuild a blockquote node from a run of signature-carrying paragraphs,
971
- * stripping the indent/border signature so child paragraphs render clean.
972
- */
973
- buildBlockquote(group) {
974
- const content = [];
975
- for (const para of group) {
976
- const node = this.resolveParagraph(para);
977
- const attrs = node.attrs;
978
- if (attrs) {
979
- if (attrs.indent) {
980
- const indent = { ...attrs.indent };
981
- delete indent.left;
982
- attrs.indent = Object.keys(indent).length > 0 ? indent : void 0;
983
- }
984
- if (attrs.border) {
985
- const border = { ...attrs.border };
986
- delete border.left;
987
- attrs.border = Object.keys(border).length > 0 ? border : void 0;
988
- }
989
- const cleaned = cleanAttrs(attrs);
990
- if (Object.keys(cleaned).length > 0) node.attrs = cleaned;
991
- else delete node.attrs;
992
- }
993
- content.push(node);
994
- }
995
- return {
996
- type: "blockquote",
997
- content
998
- };
999
- }
1000
- /**
1001
- * Resolve a list-item paragraph to a Tiptap paragraph/heading node, stripping
1002
- * the list marker (bullet/numbering) and the leading task checkbox — those
1003
- * are expressed at the list/item level, not inside the paragraph.
1004
- */
1005
- resolveListItemParagraph(para, info) {
1006
- const resolved = typeof para === "string" ? { text: para } : para;
1007
- const headingLevel = this.detectHeadingLevel(resolved);
1008
- const nodeType = headingLevel ? "heading" : "paragraph";
1009
- const attrs = headingLevel ? parseDocx(resolved) : parseDocx$2(resolved);
1010
- if (headingLevel && attrs.level == null) attrs.level = headingLevel;
1011
- const stripped = info.kind === "task" ? this.stripTaskCheckbox(resolved) : resolved;
1012
- const content = this.resolveInlineContent(stripped);
1013
- const node = { type: nodeType };
1014
- const cleanAttrsObj = cleanAttrs(attrs);
1015
- if (Object.keys(cleanAttrsObj).length > 0) node.attrs = cleanAttrsObj;
1016
- if (content.length > 0) node.content = content;
1017
- return node;
1018
- }
1019
- /** Return a copy of `para` with its leading docen-task checkbox SDT removed. */
1020
- stripTaskCheckbox(para) {
1021
- const children = para.children;
1022
- if (Array.isArray(children) && children.length > 0 && isTaskCheckbox(children[0])) return {
1023
- ...para,
1024
- children: children.slice(1)
1025
- };
1026
- return para;
1027
- }
1028
- resolveCodeBlock(opts) {
1029
- const children = opts.children;
1030
- const content = [];
1031
- if (children) {
1032
- for (const child of children) if (typeof child === "string") {
1033
- if (child) content.push({
1034
- type: "text",
1035
- text: child
1036
- });
1037
- } else if (typeof child === "object" && child !== null) {
1038
- if ("break" in child) {
1039
- const prev = content[content.length - 1];
1040
- if (prev && prev.type === "text") prev.text = (prev.text ?? "") + "\n";
1041
- else content.push({
1042
- type: "text",
1043
- text: "\n"
1044
- });
1045
- } else if ("text" in child) {
1046
- const marks = this.resolveMarks(child);
1047
- const textNode = {
1048
- type: "text",
1049
- text: child.text
1050
- };
1051
- if (marks) textNode.marks = marks;
1052
- content.push(textNode);
1053
- }
1054
- }
1055
- } else if (opts.text) content.push({
1056
- type: "text",
1057
- text: opts.text
1058
- });
1059
- const node = { type: "codeBlock" };
1060
- if (content.length > 0) node.content = content;
1061
- return node;
1062
- }
1063
- resolveTable(tableOpts) {
1064
- const attrs = parseDocx$4(tableOpts);
1065
- const rows = tableOpts.rows ?? [];
1066
- const content = [];
1067
- const tableCellMargins = tableOpts.cellMargin ?? tableOpts.margins ?? null;
1068
- let activeSpans = /* @__PURE__ */ new Map();
1069
- for (const row of rows) {
1070
- const rowAttrs = parseDocx$7(row);
1071
- const cells = row.cells ?? [];
1072
- const cellNodes = [];
1073
- const nextActiveSpans = /* @__PURE__ */ new Map();
1074
- let colIdx = 0;
1075
- for (const cell of cells) {
1076
- const cellColspan = cell.columnSpan ?? 1;
1077
- const vMerge = cell.verticalMerge;
1078
- if (vMerge === "continue") {
1079
- const owner = activeSpans.get(colIdx);
1080
- if (owner) {
1081
- const ownerAttrs = owner.attrs ??= {};
1082
- ownerAttrs.rowspan = (ownerAttrs.rowspan ?? 1) + 1;
1083
- for (let c = colIdx; c < colIdx + cellColspan; c++) nextActiveSpans.set(c, owner);
1084
- }
1085
- colIdx += cellColspan;
1086
- continue;
1087
- }
1088
- const isHeader = row.tableHeader;
1089
- const cellAttrs = isHeader ? parseDocx$6(cell) : parseDocx$5(cell);
1090
- delete cellAttrs.verticalMerge;
1091
- if (!cellAttrs.margins && tableCellMargins) cellAttrs.margins = tableCellMargins;
1092
- const cellChildren = cell.children ?? [];
1093
- const cellContent = [];
1094
- for (const cc of cellChildren) {
1095
- const resolved = this.resolveSectionChild(cc);
1096
- if (resolved) cellContent.push(resolved);
1097
- }
1098
- const cellNode = { type: isHeader ? "tableHeader" : "tableCell" };
1099
- if (Object.keys(cellAttrs).length > 0) cellNode.attrs = cleanAttrs(cellAttrs);
1100
- if (cellContent.length > 0) cellNode.content = cellContent;
1101
- else cellNode.content = [{ type: "paragraph" }];
1102
- if (vMerge === "restart") for (let c = colIdx; c < colIdx + cellColspan; c++) nextActiveSpans.set(c, cellNode);
1103
- cellNodes.push(cellNode);
1104
- colIdx += cellColspan;
1105
- }
1106
- const gridAfter = row.gridAfter ?? 0;
1107
- if (gridAfter > 0) {
1108
- const trailingType = row.tableHeader ? "tableHeader" : "tableCell";
1109
- const nilBorders = {
1110
- top: { style: "nil" },
1111
- right: { style: "nil" },
1112
- bottom: { style: "nil" },
1113
- left: { style: "nil" }
1114
- };
1115
- for (let c = 0; c < gridAfter; c++) cellNodes.push({
1116
- type: trailingType,
1117
- attrs: { borders: nilBorders },
1118
- content: [{ type: "paragraph" }]
1119
- });
1120
- colIdx += gridAfter;
1121
- }
1122
- activeSpans = nextActiveSpans;
1123
- const rowNode = { type: "tableRow" };
1124
- if (Object.keys(rowAttrs).length > 0) rowNode.attrs = cleanAttrs(rowAttrs);
1125
- if (cellNodes.length > 0) rowNode.content = cellNodes;
1126
- content.push(rowNode);
1127
- }
1128
- const node = { type: "table" };
1129
- if (Object.keys(attrs).length > 0) node.attrs = cleanAttrs(attrs);
1130
- if (content.length > 0) node.content = content;
1131
- return node;
1132
- }
1133
832
  /**
1134
833
  * Resolve a paragraph's inline content. @office-open collapses a plain-text
1135
834
  * paragraph (a single run with no properties) to a bare string or a `{ text }`
@@ -1168,70 +867,17 @@ var DocxManager = class {
1168
867
  return nodes;
1169
868
  }
1170
869
  resolveParagraphChild(child) {
1171
- if ("tab" in child) return { type: "tab" };
1172
- if ("text" in child || "children" in child || "break" in child) return this.resolveRun(child);
1173
- if ("image" in child) return this.resolveImage(child.image);
1174
- if ("wpgGroup" in child) return {
1175
- type: "wpgGroup",
1176
- attrs: { wpgGroup: child.wpgGroup }
1177
- };
1178
- if ("wpsShape" in child) {
1179
- const ws = child.wpsShape;
1180
- const content = [];
1181
- if (ws?.children) for (const para of ws.children) {
1182
- if (typeof para !== "object" || para === null) {
1183
- const node = this.resolveParagraph(para);
1184
- if (node) content.push(node);
1185
- continue;
1186
- }
1187
- const defRPr = para.run ?? {};
1188
- const children = Array.isArray(para.children) ? para.children.map((c) => typeof c !== "object" || c === null ? {
1189
- ...defRPr,
1190
- text: c
1191
- } : {
1192
- ...defRPr,
1193
- ...c
1194
- }) : void 0;
1195
- const node = this.resolveParagraph({
1196
- ...para,
1197
- run: void 0,
1198
- ...children ? { children } : {}
1199
- });
1200
- if (node) content.push(node);
1201
- }
1202
- if (content.length === 0) content.push({ type: "paragraph" });
1203
- const { children: _omit, ...geometry } = ws ?? {};
1204
- const node = {
1205
- type: "wpsShape",
1206
- content
1207
- };
1208
- const cleanGeometry = cleanAttrs(geometry);
1209
- if (Object.keys(cleanGeometry).length > 0) node.attrs = { wpsShape: cleanGeometry };
1210
- return node;
870
+ const ctx = this.resolveCtx;
871
+ for (const { rule } of this.inlineRules) if (rule.match(child, ctx)) {
872
+ const node = rule.convert(child, ctx);
873
+ if (node) return node;
1211
874
  }
1212
- if ("sdt" in child) return this.resolveInlineSdt(child);
1213
- if ("hyperlink" in child) return this.resolveHyperlink(child.hyperlink);
1214
- if ("pageBreak" in child) return { type: "pageBreak" };
1215
- if ("columnBreak" in child) return { type: "columnBreak" };
875
+ if ("text" in child || "children" in child || "break" in child) return this.resolveRun(child);
1216
876
  return {
1217
877
  type: "inlinePassthrough",
1218
878
  attrs: { data: JSON.stringify(child) }
1219
879
  };
1220
880
  }
1221
- /** Resolve an inline SDT (mention carrier; other inline SDTs unsupported). */
1222
- resolveInlineSdt(child) {
1223
- if (isMention(child)) {
1224
- const { id, label } = readMention(child);
1225
- return {
1226
- type: "mention",
1227
- attrs: {
1228
- id,
1229
- label
1230
- }
1231
- };
1232
- }
1233
- return null;
1234
- }
1235
881
  resolveRun(opts) {
1236
882
  if (opts.break && opts.text === void 0 && !opts.children) return { type: "hardBreak" };
1237
883
  const text = opts.text;
@@ -1253,18 +899,21 @@ var DocxManager = class {
1253
899
  };
1254
900
  for (const c of opts.children) if (typeof c === "string") parts.push(c);
1255
901
  else if (c && typeof c === "object") {
1256
- if ("pageBreak" in c) {
1257
- flushText();
1258
- nodes.push({ type: "pageBreak" });
1259
- } else if ("columnBreak" in c) {
1260
- flushText();
1261
- nodes.push({ type: "columnBreak" });
1262
- } else if ("break" in c) {
902
+ const ctx = this.resolveCtx;
903
+ let handled = false;
904
+ for (const { rule } of this.inlineRules) if (rule.match(c, ctx)) {
905
+ const node = rule.convert(c, ctx);
906
+ if (node) {
907
+ flushText();
908
+ nodes.push(...Array.isArray(node) ? node : [node]);
909
+ handled = true;
910
+ break;
911
+ }
912
+ }
913
+ if (handled) continue;
914
+ if ("break" in c) {
1263
915
  flushText();
1264
916
  nodes.push({ type: "hardBreak" });
1265
- } else if ("tab" in c) {
1266
- flushText();
1267
- nodes.push({ type: "tab" });
1268
917
  }
1269
918
  }
1270
919
  flushText();
@@ -1287,98 +936,32 @@ var DocxManager = class {
1287
936
  }
1288
937
  resolveMarks(opts) {
1289
938
  const marks = [];
1290
- if (opts.bold) marks.push({ type: "bold" });
1291
- if (opts.italic) marks.push({ type: "italic" });
1292
- if (opts.underline) marks.push({ type: "underline" });
1293
- if (opts.strike) {
1294
- const strikeAttrs = parseDocx$3(opts);
1295
- marks.push({
1296
- type: "strike",
1297
- attrs: { doubleStrike: strikeAttrs.doubleStrike ?? null }
1298
- });
1299
- }
1300
- if (opts.subScript) marks.push({ type: "subscript" });
1301
- if (opts.superScript) marks.push({ type: "superscript" });
1302
- if (opts.highlight) marks.push({
1303
- type: "highlight",
1304
- attrs: { color: opts.highlight }
1305
- });
1306
- if (opts.style === "CodeChar") marks.push({ type: "code" });
1307
- const textStyleAttrs = parseDocx$8(opts);
1308
- if (opts.style === "CodeChar") {
1309
- delete textStyleAttrs.font;
1310
- delete textStyleAttrs.styleId;
939
+ for (const { name, parse } of this.markParse) {
940
+ const attrs = parse(opts);
941
+ if (attrs === null) continue;
942
+ marks.push(Object.keys(attrs).length ? {
943
+ type: name,
944
+ attrs
945
+ } : { type: name });
1311
946
  }
1312
- if (Object.keys(textStyleAttrs).length > 0) marks.push({
1313
- type: "textStyle",
1314
- attrs: textStyleAttrs
1315
- });
1316
947
  return marks.length > 0 ? marks : void 0;
1317
948
  }
1318
- resolveImage(imageOpts) {
1319
- const attrs = parseDocx$1(imageOpts);
1320
- const data = imageOpts.data;
1321
- const type = imageOpts.type;
1322
- if (data && type) attrs.src = `data:image/${type};base64,${encodeBase64(data instanceof ArrayBuffer ? new Uint8Array(data) : data)}`;
1323
- return {
1324
- type: "image",
1325
- attrs
1326
- };
1327
- }
1328
- resolveHyperlink(hyperlink) {
1329
- const href = hyperlink.link ?? (hyperlink.anchor ? `#${hyperlink.anchor}` : "");
1330
- if (!href) return null;
1331
- const content = this.resolveParagraphChildren((hyperlink.children ?? []).map((c) => c));
1332
- if (content.length > 0) {
1333
- const merged = mergeTextNodes(content);
1334
- for (const node of merged) if (node.type === "text") node.marks = [...node.marks ?? [], {
1335
- type: "link",
1336
- attrs: {
1337
- href,
1338
- target: href.startsWith("#") ? null : "_blank",
1339
- rel: "noopener noreferrer nofollow",
1340
- class: null,
1341
- title: hyperlink.tooltip ?? null
1342
- }
1343
- }];
1344
- return merged;
1345
- }
1346
- return null;
1347
- }
1348
949
  };
1349
- const HEADING_LEVEL_MAP = {
1350
- Heading1: 1,
1351
- Heading2: 2,
1352
- Heading3: 3,
1353
- Heading4: 4,
1354
- Heading5: 5,
1355
- Heading6: 6,
1356
- Heading7: 7,
1357
- Heading8: 8,
1358
- Heading9: 9,
1359
- Title: 1
1360
- };
1361
- /** Heading level (1-9) from a localized style NAME: "heading 1"/"标题 1" → 1,
1362
- * "title" → 1. office-open's built-in names are English ("heading 1"), but
1363
- * zh-CN Word labels the same styles "标题 1"; both map to the same level. */
1364
- function headingLevelFromName(name) {
1365
- if (!name) return void 0;
1366
- const m = /^heading\s+(\d)$/i.exec(name) ?? /^标题\s*(\d)$/.exec(name);
1367
- if (m) {
1368
- const lvl = Number(m[1]);
1369
- if (lvl >= 1 && lvl <= 9) return lvl;
1370
- }
1371
- return /^title$/i.test(name) ? 1 : void 0;
950
+ const defaultManager = new DocxManager(docxExtensions);
951
+ /** Resolve a DocxManager for a conversion call: the shared default singleton
952
+ * when no extensions are given, or a fresh instance bound to custom extensions
953
+ * so user-supplied marks/nodes plug into compile/resolve without a fork. */
954
+ function getDocxManager(extensions) {
955
+ return extensions ? new DocxManager(extensions) : defaultManager;
1372
956
  }
1373
- const defaultManager = new DocxManager();
1374
957
  /**
1375
958
  * Parse a DOCX file into Tiptap JSON (runtime model).
1376
959
  *
1377
960
  * Combines @office-open/docx's `parseDocument` (DOCX binary → DocumentOptions)
1378
961
  * with `DocxManager.resolve` (DocumentOptions → Tiptap JSON).
1379
962
  */
1380
- function parseDOCX(data) {
1381
- return defaultManager.resolve(parseDocument(data));
963
+ function parseDOCX(data, extensions) {
964
+ return getDocxManager(extensions).resolve(parseDocument(data));
1382
965
  }
1383
966
  /**
1384
967
  * Merge {@link DocxGenerateOptions.document} into the `DocumentOptions`
@@ -1409,9 +992,9 @@ function applyDocumentOptions(base, document) {
1409
992
  * in place (http image URLs become embedded data URLs).
1410
993
  */
1411
994
  async function generateDOCX(json, options) {
1412
- const { prepare = true, packer, document } = options ?? {};
995
+ const { prepare = true, packer, document, extensions } = options ?? {};
1413
996
  if (prepare !== false) await prepareDocument(json, prepare === true ? void 0 : prepare);
1414
- return generateDocument(applyDocumentOptions(compileDocument(json), document), packer);
997
+ return generateDocument(applyDocumentOptions(compileDocument(json, extensions), document), packer);
1415
998
  }
1416
999
  /**
1417
1000
  * Generate a DOCX file synchronously — fastest throughput, blocks the event loop.
@@ -1421,8 +1004,8 @@ async function generateDOCX(json, options) {
1421
1004
  * when http images need embedding. `options.document` is still applied.
1422
1005
  */
1423
1006
  function generateDOCXSync(json, options) {
1424
- const { packer, document } = options ?? {};
1425
- return generateDocumentSync(applyDocumentOptions(compileDocument(json), document), packer);
1007
+ const { packer, document, extensions } = options ?? {};
1008
+ return generateDocumentSync(applyDocumentOptions(compileDocument(json, extensions), document), packer);
1426
1009
  }
1427
1010
  /**
1428
1011
  * Generate a DOCX file as a `ReadableStream<Uint8Array>` — for large documents
@@ -1432,21 +1015,21 @@ function generateDOCXSync(json, options) {
1432
1015
  * `DocxManager.compile` → `generateDocumentStream`. Async due to preparation.
1433
1016
  */
1434
1017
  async function generateDOCXStream(json, options) {
1435
- const { prepare = true, packer, document } = options ?? {};
1018
+ const { prepare = true, packer, document, extensions } = options ?? {};
1436
1019
  if (prepare !== false) await prepareDocument(json, prepare === true ? void 0 : prepare);
1437
- return generateDocumentStream(applyDocumentOptions(compileDocument(json), document), packer);
1020
+ return generateDocumentStream(applyDocumentOptions(compileDocument(json, extensions), document), packer);
1438
1021
  }
1439
1022
  /**
1440
1023
  * Convert DocumentOptions (persistence model) to Tiptap JSON (runtime model).
1441
1024
  */
1442
- function resolveDocument(docOpts) {
1443
- return defaultManager.resolve(docOpts);
1025
+ function resolveDocument(docOpts, extensions) {
1026
+ return getDocxManager(extensions).resolve(docOpts);
1444
1027
  }
1445
1028
  /**
1446
1029
  * Convert Tiptap JSON (runtime model) to DocumentOptions (persistence model).
1447
1030
  */
1448
- function compileDocument(json) {
1449
- return defaultManager.compile(json);
1031
+ function compileDocument(json, extensions) {
1032
+ return getDocxManager(extensions).compile(json);
1450
1033
  }
1451
1034
  //#endregion
1452
1035
  export { DocxManager, compileDocument, generateDOCX, generateDOCXStream, generateDOCXSync, parseDOCX, resolveDocument };