@docen/docx 0.3.4 → 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 +251 -666
  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 +3 -3
  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 = [];
@@ -343,12 +394,13 @@ var DocxManager = class {
343
394
  compiledCells.push({
344
395
  verticalMerge: "continue",
345
396
  columnSpan: span.colspan,
397
+ ...span.borders ? { borders: span.borders } : {},
346
398
  children: [{ paragraph: "" }]
347
399
  });
348
400
  colIdx += span.colspan;
349
401
  spanIdx++;
350
402
  } else {
351
- const cell = this.compileTableCellNode(pmCells[cellIdx], tableCellMargins);
403
+ const cell = this.compileTableCellNode(pmCells[cellIdx], tableCellMargins, insideH, insideV);
352
404
  const cs = cell.columnSpan ?? 1;
353
405
  const rs = cell.rowSpan ?? 1;
354
406
  if (rs > 1) {
@@ -357,7 +409,8 @@ var DocxManager = class {
357
409
  newSpans.push({
358
410
  colStart: colIdx,
359
411
  colspan: cs,
360
- remainingRows: rs - 1
412
+ remainingRows: rs - 1,
413
+ borders: cell.borders
361
414
  });
362
415
  }
363
416
  compiledCells.push(cell);
@@ -444,16 +497,35 @@ var DocxManager = class {
444
497
  }
445
498
  };
446
499
  }
447
- compileTableCellNode(cellNode, tableMargins) {
448
- const cellOpts = cellNode.type === "tableHeader" ? renderDocx$7(cellNode) : renderDocx$6(cellNode);
500
+ compileTableCellNode(cellNode, tableMargins, insideH, insideV) {
501
+ const cellOpts = this.renderNodeOpts(cellNode);
449
502
  if (tableMargins && cellOpts.margins && sameCellMargins(cellOpts.margins, tableMargins)) delete cellOpts.margins;
450
- 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);
451
512
  const cellChildren = [];
452
- for (const paraNode of cellNode.content ?? []) {
453
- const para = this.compileParagraphNode(paraNode);
454
- const paraObj = typeof para === "string" ? { text: para } : para;
455
- if (cellAlign && !paraObj.alignment) paraObj.alignment = cellAlign;
456
- 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);
457
529
  }
458
530
  if (cellChildren.length > 0) cellOpts.children = cellChildren;
459
531
  return cellOpts;
@@ -580,7 +652,7 @@ var DocxManager = class {
580
652
  break;
581
653
  }
582
654
  case "image": {
583
- const imageRun = renderDocx$2(node);
655
+ const imageRun = this.nodeRender.get(node.type)?.(node) ?? null;
584
656
  if (imageRun) children.push(imageRun);
585
657
  break;
586
658
  }
@@ -628,69 +700,65 @@ var DocxManager = class {
628
700
  /** Emit a single run for `text` with all inline marks applied. */
629
701
  compileTextRun(text, marks, children) {
630
702
  const runOpts = { text };
631
- for (const mark of marks ?? []) switch (mark.type) {
632
- case "bold":
633
- runOpts.bold = true;
634
- break;
635
- case "italic":
636
- runOpts.italic = true;
637
- break;
638
- case "underline":
639
- runOpts.underline = { type: "single" };
640
- break;
641
- case "strike": {
642
- const strikeProps = renderDocx$4(mark.attrs ?? {});
643
- Object.assign(runOpts, strikeProps);
644
- break;
645
- }
646
- case "subscript":
647
- runOpts.subScript = true;
648
- break;
649
- case "superscript":
650
- runOpts.superScript = true;
651
- break;
652
- case "highlight":
653
- runOpts.highlight = mark.attrs?.color ?? "yellow";
654
- break;
655
- case "code":
656
- runOpts.style = "CodeChar";
657
- runOpts.font = "Consolas";
658
- break;
659
- case "textStyle": {
660
- const tsProps = renderDocx$9(mark.attrs ?? {});
661
- Object.assign(runOpts, tsProps);
662
- break;
663
- }
664
- case "link": {
665
- const href = mark.attrs?.href;
666
- if (href) {
667
- const { text: _, ...runWithoutText } = runOpts;
668
- const linkChildren = [];
669
- if (text) linkChildren.push({
670
- ...runWithoutText,
671
- text
672
- });
673
- children.push({ hyperlink: {
674
- link: href.startsWith("#") ? void 0 : href,
675
- anchor: href.startsWith("#") ? href.slice(1) : void 0,
676
- children: linkChildren
677
- } });
678
- 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;
679
722
  }
680
- break;
723
+ case "insertion":
724
+ case "deletion":
725
+ children.push(this.compileTrackedChangeRun(mark.type, mark.attrs, text, runOpts));
726
+ return;
681
727
  }
728
+ const render = this.markRender.get(mark.type);
729
+ if (render) Object.assign(runOpts, render(mark.attrs ?? {}));
682
730
  }
683
731
  children.push(runOpts);
684
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
+ }
685
755
  resolveSectionChild(child) {
686
- if ("paragraph" in child) return this.resolveParagraph(child.paragraph);
687
- if ("table" in child) return this.resolveTable(child.table);
688
- if ("sdt" in child) {
689
- const sdt = child.sdt;
690
- if (sdt.properties?.tag === "docen-details") return this.resolveDetailsSdt(sdt);
691
- 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;
692
760
  }
693
- if ("toc" in child) return this.resolveToc(child.toc);
761
+ if ("paragraph" in child) return this.resolveParagraph(child.paragraph);
694
762
  return this.resolvePassthrough(child);
695
763
  }
696
764
  /** Wrap an opaque SectionChild in a passthrough atom (attrs.data = JSON). */
@@ -700,116 +768,15 @@ var DocxManager = class {
700
768
  attrs: { data: JSON.stringify(child) }
701
769
  };
702
770
  }
703
- /**
704
- * Resolve a table of contents into an editable `tableOfContents` container:
705
- * `attrs.options` carries the field switches, `content` is the entry
706
- * paragraphs. Each entry's inner HYPERLINK field has content-less runs that
707
- * office-open parses as `null`; resolving the entries through
708
- * `resolveSectionChild` → `resolveParagraphChildren` drops those nulls, so the
709
- * TOC no longer reaches the generate path as an opaque blob of nulls (the
710
- * `stringifyRunInline(null).break` crash). When `entries` is absent/empty
711
- * (a fresh, unrendered TOC), keep the node valid for `content: "block+"` with
712
- * a placeholder empty paragraph.
713
- */
714
- resolveToc(toc) {
715
- const { entries, ...options } = toc;
716
- const content = [];
717
- for (const entry of entries ?? []) {
718
- const node = this.resolveSectionChild(entry);
719
- if (!node) continue;
720
- if (Array.isArray(node)) content.push(...node);
721
- else content.push(node);
722
- }
723
- if (content.length === 0) content.push({ type: "paragraph" });
724
- const node = {
725
- type: "tocField",
726
- content
727
- };
728
- const cleanOptions = cleanAttrs(options);
729
- if (Object.keys(cleanOptions).length > 0) node.attrs = { options: cleanOptions };
730
- return node;
731
- }
732
- /**
733
- * Resolve a details group-SDT: the summary-style paragraph becomes
734
- * detailsSummary, the remaining blocks fold into detailsContent.
735
- */
736
- resolveDetailsSdt(sdt) {
737
- const content = [];
738
- let summary = null;
739
- for (const child of sdt.children ?? []) {
740
- if ("paragraph" in child) {
741
- const para = child.paragraph;
742
- if (para.style === "DocenDetailsSummary") {
743
- summary = this.resolveInlineContent(para);
744
- continue;
745
- }
746
- }
747
- const node = this.resolveSectionChild(child);
748
- if (!node) continue;
749
- if (Array.isArray(node)) content.push(...node);
750
- else content.push(node);
751
- }
752
- const details = {
753
- type: "details",
754
- content: []
755
- };
756
- if (summary !== null) details.content.push({
757
- type: "detailsSummary",
758
- content: summary
759
- });
760
- if (content.length > 0) details.content.push({
761
- type: "detailsContent",
762
- content
763
- });
764
- return details;
765
- }
766
771
  resolveParagraph(opts) {
767
772
  const resolved = typeof opts === "string" ? { text: opts } : opts;
768
773
  if (resolved.thematicBreak) return { type: "horizontalRule" };
769
- if (resolved.style === "Code") return this.resolveCodeBlock(resolved);
770
- const headingLevel = this.detectHeadingLevel(resolved);
771
- const nodeType = headingLevel ? "heading" : "paragraph";
772
- const attrs = headingLevel ? parseDocx(resolved) : parseDocx$2(resolved);
773
- if (headingLevel && attrs.level == null) attrs.level = headingLevel;
774
- const content = this.resolveInlineContent(resolved);
775
- const cleanAttrsObj = cleanAttrs(attrs);
776
- const node = { type: nodeType };
777
- if (Object.keys(cleanAttrsObj).length > 0) node.attrs = cleanAttrsObj;
778
- if (content.length > 0) node.content = content;
779
- return node;
780
- }
781
- /** Heading level (1-9) for a paragraph, or undefined when it isn't a heading.
782
- * DOCX marks a heading several ways, checked in priority order:
783
- * 1. office-open lifts a HeadingLevel pStyle ("Heading1".."Title") into `heading`.
784
- * 2. An explicit `outlineLevel` (0-8 → 1-9) — Word's outline/TOC key off this
785
- * even without a heading pStyle; the Heading1-9 styles carry outlineLvl 0-8.
786
- * 3. A pStyle that names a heading style: directly ("Heading7", which stays on
787
- * `style` because office-open's HeadingLevel type caps at 6), by localized
788
- * NAME ("heading 1"/"标题 1"), or via the `basedOn` chain (a custom style
789
- * "MyTitle" basedOn="Heading1"). `heading` and `style` carry the same pStyle.
790
- * `outlineLevel` is read loosely — office-open's public type omits the field
791
- * even though it round-trips (w:outlineLvl) at runtime. */
792
- detectHeadingLevel(resolved) {
793
- if (resolved.heading) {
794
- const lvl = HEADING_LEVEL_MAP[resolved.heading];
795
- if (lvl) return lvl;
796
- }
797
- const outline = resolved.outlineLevel;
798
- if (typeof outline === "number" && outline >= 0 && outline <= 8) return outline + 1;
799
- const styleId = resolved.style;
800
- if (!styleId || !this.resolveStyles) return void 0;
801
- const byId = indexParagraphStyles(this.resolveStyles);
802
- const visited = /* @__PURE__ */ new Set();
803
- let curId = styleId;
804
- while (curId && !visited.has(curId)) {
805
- visited.add(curId);
806
- if (HEADING_LEVEL_MAP[curId]) return HEADING_LEVEL_MAP[curId];
807
- const style = byId.get(curId);
808
- if (!style) break;
809
- const lvl = headingLevelFromName(style.name);
810
- if (lvl) return lvl;
811
- 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;
812
778
  }
779
+ return buildTextBlock("paragraph", resolved, ctx);
813
780
  }
814
781
  /** reference → level-0 format/start, for classifying numbering paragraphs. */
815
782
  buildNumberingLookup(docOpts) {
@@ -825,309 +792,43 @@ var DocxManager = class {
825
792
  return lookup;
826
793
  }
827
794
  /**
828
- * Walk section children, grouping consecutive list paragraphs into nested
829
- * Tiptap lists. Non-list children resolve individually. DOCX flattens lists
830
- * 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.
831
802
  */
832
- resolveSectionChildren(children, numberingLookup) {
803
+ resolveSectionChildren(children) {
804
+ const ctx = this.resolveCtx;
833
805
  const content = [];
834
806
  let i = 0;
835
807
  while (i < children.length) {
836
808
  const child = children[i];
837
809
  const firstPara = "paragraph" in child ? child.paragraph : null;
838
- if (!(firstPara && typeof firstPara !== "string" ? this.detectList(firstPara, numberingLookup) : null)) {
839
- 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) {
840
813
  const group = [];
841
814
  while (i < children.length) {
842
815
  const member = children[i];
843
816
  if (!("paragraph" in member)) break;
844
817
  const para = member.paragraph;
845
- if (typeof para === "string" || !this.detectBlockquote(para)) break;
818
+ if (typeof para === "string" || !rule.rule.belongs(para, ctx)) break;
846
819
  group.push(para);
847
820
  i++;
848
821
  }
849
- content.push(this.buildBlockquote(group));
822
+ content.push(...rule.rule.build(group, ctx));
850
823
  continue;
851
824
  }
852
- const node = this.resolveSectionChild(child);
853
- if (node) content.push(node);
854
- i++;
855
- continue;
856
- }
857
- const group = [];
858
- while (i < children.length) {
859
- const member = children[i];
860
- if (!("paragraph" in member)) break;
861
- const para = member.paragraph;
862
- if (typeof para === "string") break;
863
- const info = this.detectList(para, numberingLookup);
864
- if (!info) break;
865
- group.push({
866
- para,
867
- info
868
- });
869
- i++;
870
825
  }
871
- content.push(...this.buildListTree(group));
826
+ const node = this.resolveSectionChild(child);
827
+ if (node) content.push(node);
828
+ i++;
872
829
  }
873
830
  return content;
874
831
  }
875
- /** Classify a paragraph as a list item, or null if it isn't one. */
876
- detectList(para, lookup) {
877
- const p = para;
878
- const numbering = p.numbering;
879
- const bullet = p.bullet;
880
- let kind;
881
- let level;
882
- let reference;
883
- let start;
884
- if (numbering) {
885
- reference = numbering.reference;
886
- level = numbering.level ?? 0;
887
- const cfg = reference ? lookup.get(reference) : void 0;
888
- if (cfg && cfg.format && cfg.format !== "bullet") {
889
- kind = "ordered";
890
- start = cfg.start;
891
- } else kind = "bullet";
892
- } else if (bullet) {
893
- kind = "bullet";
894
- level = bullet.level ?? 0;
895
- } else return null;
896
- const first = p.children?.[0];
897
- return {
898
- kind: isTaskCheckbox(first) ? "task" : kind,
899
- level,
900
- reference,
901
- start,
902
- checked: readCheckboxState(first)
903
- };
904
- }
905
- /**
906
- * Rebuild nested Tiptap lists from a flat run of list paragraphs. Stack-based:
907
- * each frame is an active list at a given depth; the `key` (level:type:
908
- * reference) decides whether a paragraph continues the top list, starts a
909
- * nested list, or splits off a new sibling list.
910
- */
911
- buildListTree(group) {
912
- const topLevel = [];
913
- const stack = [];
914
- for (const { para, info } of group) {
915
- const listType = info.kind === "ordered" ? "orderedList" : info.kind === "task" ? "taskList" : "bulletList";
916
- const itemType = info.kind === "task" ? "taskItem" : "listItem";
917
- const key = `${info.level}:${listType}:${info.reference ?? ""}`;
918
- while (stack.length > 0) {
919
- const top = stack[stack.length - 1];
920
- if (top.level > info.level || top.level === info.level && top.key !== key) {
921
- stack.pop();
922
- continue;
923
- }
924
- break;
925
- }
926
- const newItem = {
927
- type: itemType,
928
- content: [this.resolveListItemParagraph(para, info)]
929
- };
930
- if (itemType === "taskItem") newItem.attrs = { checked: info.checked };
931
- const top = stack[stack.length - 1];
932
- if (top && top.level === info.level && top.key === key) {
933
- top.listNode.content.push(newItem);
934
- top.currentItem = newItem;
935
- } else {
936
- const newList = {
937
- type: listType,
938
- content: [newItem]
939
- };
940
- const listAttrs = {};
941
- if (listType === "orderedList" && info.level === 0 && typeof info.start === "number" && info.start !== 1) listAttrs.start = info.start;
942
- if (info.reference) listAttrs.numbering = info.reference;
943
- if (Object.keys(listAttrs).length > 0) newList.attrs = listAttrs;
944
- if (top) top.currentItem.content.push(newList);
945
- else topLevel.push(newList);
946
- stack.push({
947
- level: info.level,
948
- key,
949
- listNode: newList,
950
- currentItem: newItem
951
- });
952
- }
953
- }
954
- return topLevel;
955
- }
956
- /** Classify a paragraph as a blockquote member by its signature. */
957
- detectBlockquote(para) {
958
- const p = para;
959
- const indent = p.indent;
960
- const border = p.border;
961
- if (!indent || indent.left !== 720) return false;
962
- const bl = border?.left;
963
- if (!bl) return false;
964
- const sig = BLOCKQUOTE_BORDER;
965
- return bl.style === sig.style && bl.size === sig.size && bl.space === sig.space && bl.color === sig.color;
966
- }
967
- /**
968
- * Rebuild a blockquote node from a run of signature-carrying paragraphs,
969
- * stripping the indent/border signature so child paragraphs render clean.
970
- */
971
- buildBlockquote(group) {
972
- const content = [];
973
- for (const para of group) {
974
- const node = this.resolveParagraph(para);
975
- const attrs = node.attrs;
976
- if (attrs) {
977
- if (attrs.indent) {
978
- const indent = { ...attrs.indent };
979
- delete indent.left;
980
- attrs.indent = Object.keys(indent).length > 0 ? indent : void 0;
981
- }
982
- if (attrs.border) {
983
- const border = { ...attrs.border };
984
- delete border.left;
985
- attrs.border = Object.keys(border).length > 0 ? border : void 0;
986
- }
987
- const cleaned = cleanAttrs(attrs);
988
- if (Object.keys(cleaned).length > 0) node.attrs = cleaned;
989
- else delete node.attrs;
990
- }
991
- content.push(node);
992
- }
993
- return {
994
- type: "blockquote",
995
- content
996
- };
997
- }
998
- /**
999
- * Resolve a list-item paragraph to a Tiptap paragraph/heading node, stripping
1000
- * the list marker (bullet/numbering) and the leading task checkbox — those
1001
- * are expressed at the list/item level, not inside the paragraph.
1002
- */
1003
- resolveListItemParagraph(para, info) {
1004
- const resolved = typeof para === "string" ? { text: para } : para;
1005
- const headingLevel = this.detectHeadingLevel(resolved);
1006
- const nodeType = headingLevel ? "heading" : "paragraph";
1007
- const attrs = headingLevel ? parseDocx(resolved) : parseDocx$2(resolved);
1008
- if (headingLevel && attrs.level == null) attrs.level = headingLevel;
1009
- const stripped = info.kind === "task" ? this.stripTaskCheckbox(resolved) : resolved;
1010
- const content = this.resolveInlineContent(stripped);
1011
- const node = { type: nodeType };
1012
- const cleanAttrsObj = cleanAttrs(attrs);
1013
- if (Object.keys(cleanAttrsObj).length > 0) node.attrs = cleanAttrsObj;
1014
- if (content.length > 0) node.content = content;
1015
- return node;
1016
- }
1017
- /** Return a copy of `para` with its leading docen-task checkbox SDT removed. */
1018
- stripTaskCheckbox(para) {
1019
- const children = para.children;
1020
- if (Array.isArray(children) && children.length > 0 && isTaskCheckbox(children[0])) return {
1021
- ...para,
1022
- children: children.slice(1)
1023
- };
1024
- return para;
1025
- }
1026
- resolveCodeBlock(opts) {
1027
- const children = opts.children;
1028
- const content = [];
1029
- if (children) {
1030
- for (const child of children) if (typeof child === "string") {
1031
- if (child) content.push({
1032
- type: "text",
1033
- text: child
1034
- });
1035
- } else if (typeof child === "object" && child !== null) {
1036
- if ("break" in child) {
1037
- const prev = content[content.length - 1];
1038
- if (prev && prev.type === "text") prev.text = (prev.text ?? "") + "\n";
1039
- else content.push({
1040
- type: "text",
1041
- text: "\n"
1042
- });
1043
- } else if ("text" in child) {
1044
- const marks = this.resolveMarks(child);
1045
- const textNode = {
1046
- type: "text",
1047
- text: child.text
1048
- };
1049
- if (marks) textNode.marks = marks;
1050
- content.push(textNode);
1051
- }
1052
- }
1053
- } else if (opts.text) content.push({
1054
- type: "text",
1055
- text: opts.text
1056
- });
1057
- const node = { type: "codeBlock" };
1058
- if (content.length > 0) node.content = content;
1059
- return node;
1060
- }
1061
- resolveTable(tableOpts) {
1062
- const attrs = parseDocx$4(tableOpts);
1063
- const rows = tableOpts.rows ?? [];
1064
- const content = [];
1065
- const tableCellMargins = tableOpts.cellMargin ?? tableOpts.margins ?? null;
1066
- let activeSpans = /* @__PURE__ */ new Map();
1067
- for (const row of rows) {
1068
- const rowAttrs = parseDocx$7(row);
1069
- const cells = row.cells ?? [];
1070
- const cellNodes = [];
1071
- const nextActiveSpans = /* @__PURE__ */ new Map();
1072
- let colIdx = 0;
1073
- for (const cell of cells) {
1074
- const cellColspan = cell.columnSpan ?? 1;
1075
- const vMerge = cell.verticalMerge;
1076
- if (vMerge === "continue") {
1077
- const owner = activeSpans.get(colIdx);
1078
- if (owner) {
1079
- const ownerAttrs = owner.attrs ??= {};
1080
- ownerAttrs.rowspan = (ownerAttrs.rowspan ?? 1) + 1;
1081
- for (let c = colIdx; c < colIdx + cellColspan; c++) nextActiveSpans.set(c, owner);
1082
- }
1083
- colIdx += cellColspan;
1084
- continue;
1085
- }
1086
- const isHeader = row.tableHeader;
1087
- const cellAttrs = isHeader ? parseDocx$6(cell) : parseDocx$5(cell);
1088
- delete cellAttrs.verticalMerge;
1089
- if (!cellAttrs.margins && tableCellMargins) cellAttrs.margins = tableCellMargins;
1090
- const cellChildren = cell.children ?? [];
1091
- const cellContent = [];
1092
- for (const cc of cellChildren) {
1093
- const resolved = this.resolveSectionChild(cc);
1094
- if (resolved) cellContent.push(resolved);
1095
- }
1096
- const cellNode = { type: isHeader ? "tableHeader" : "tableCell" };
1097
- if (Object.keys(cellAttrs).length > 0) cellNode.attrs = cleanAttrs(cellAttrs);
1098
- if (cellContent.length > 0) cellNode.content = cellContent;
1099
- else cellNode.content = [{ type: "paragraph" }];
1100
- if (vMerge === "restart") for (let c = colIdx; c < colIdx + cellColspan; c++) nextActiveSpans.set(c, cellNode);
1101
- cellNodes.push(cellNode);
1102
- colIdx += cellColspan;
1103
- }
1104
- const gridAfter = row.gridAfter ?? 0;
1105
- if (gridAfter > 0) {
1106
- const trailingType = row.tableHeader ? "tableHeader" : "tableCell";
1107
- const nilBorders = {
1108
- top: { style: "nil" },
1109
- right: { style: "nil" },
1110
- bottom: { style: "nil" },
1111
- left: { style: "nil" }
1112
- };
1113
- for (let c = 0; c < gridAfter; c++) cellNodes.push({
1114
- type: trailingType,
1115
- attrs: { borders: nilBorders },
1116
- content: [{ type: "paragraph" }]
1117
- });
1118
- colIdx += gridAfter;
1119
- }
1120
- activeSpans = nextActiveSpans;
1121
- const rowNode = { type: "tableRow" };
1122
- if (Object.keys(rowAttrs).length > 0) rowNode.attrs = cleanAttrs(rowAttrs);
1123
- if (cellNodes.length > 0) rowNode.content = cellNodes;
1124
- content.push(rowNode);
1125
- }
1126
- const node = { type: "table" };
1127
- if (Object.keys(attrs).length > 0) node.attrs = cleanAttrs(attrs);
1128
- if (content.length > 0) node.content = content;
1129
- return node;
1130
- }
1131
832
  /**
1132
833
  * Resolve a paragraph's inline content. @office-open collapses a plain-text
1133
834
  * paragraph (a single run with no properties) to a bare string or a `{ text }`
@@ -1166,70 +867,17 @@ var DocxManager = class {
1166
867
  return nodes;
1167
868
  }
1168
869
  resolveParagraphChild(child) {
1169
- if ("tab" in child) return { type: "tab" };
1170
- if ("text" in child || "children" in child || "break" in child) return this.resolveRun(child);
1171
- if ("image" in child) return this.resolveImage(child.image);
1172
- if ("wpgGroup" in child) return {
1173
- type: "wpgGroup",
1174
- attrs: { wpgGroup: child.wpgGroup }
1175
- };
1176
- if ("wpsShape" in child) {
1177
- const ws = child.wpsShape;
1178
- const content = [];
1179
- if (ws?.children) for (const para of ws.children) {
1180
- if (typeof para !== "object" || para === null) {
1181
- const node = this.resolveParagraph(para);
1182
- if (node) content.push(node);
1183
- continue;
1184
- }
1185
- const defRPr = para.run ?? {};
1186
- const children = Array.isArray(para.children) ? para.children.map((c) => typeof c !== "object" || c === null ? {
1187
- ...defRPr,
1188
- text: c
1189
- } : {
1190
- ...defRPr,
1191
- ...c
1192
- }) : void 0;
1193
- const node = this.resolveParagraph({
1194
- ...para,
1195
- run: void 0,
1196
- ...children ? { children } : {}
1197
- });
1198
- if (node) content.push(node);
1199
- }
1200
- if (content.length === 0) content.push({ type: "paragraph" });
1201
- const { children: _omit, ...geometry } = ws ?? {};
1202
- const node = {
1203
- type: "wpsShape",
1204
- content
1205
- };
1206
- const cleanGeometry = cleanAttrs(geometry);
1207
- if (Object.keys(cleanGeometry).length > 0) node.attrs = { wpsShape: cleanGeometry };
1208
- 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;
1209
874
  }
1210
- if ("sdt" in child) return this.resolveInlineSdt(child);
1211
- if ("hyperlink" in child) return this.resolveHyperlink(child.hyperlink);
1212
- if ("pageBreak" in child) return { type: "pageBreak" };
1213
- if ("columnBreak" in child) return { type: "columnBreak" };
875
+ if ("text" in child || "children" in child || "break" in child) return this.resolveRun(child);
1214
876
  return {
1215
877
  type: "inlinePassthrough",
1216
878
  attrs: { data: JSON.stringify(child) }
1217
879
  };
1218
880
  }
1219
- /** Resolve an inline SDT (mention carrier; other inline SDTs unsupported). */
1220
- resolveInlineSdt(child) {
1221
- if (isMention(child)) {
1222
- const { id, label } = readMention(child);
1223
- return {
1224
- type: "mention",
1225
- attrs: {
1226
- id,
1227
- label
1228
- }
1229
- };
1230
- }
1231
- return null;
1232
- }
1233
881
  resolveRun(opts) {
1234
882
  if (opts.break && opts.text === void 0 && !opts.children) return { type: "hardBreak" };
1235
883
  const text = opts.text;
@@ -1251,18 +899,21 @@ var DocxManager = class {
1251
899
  };
1252
900
  for (const c of opts.children) if (typeof c === "string") parts.push(c);
1253
901
  else if (c && typeof c === "object") {
1254
- if ("pageBreak" in c) {
1255
- flushText();
1256
- nodes.push({ type: "pageBreak" });
1257
- } else if ("columnBreak" in c) {
1258
- flushText();
1259
- nodes.push({ type: "columnBreak" });
1260
- } 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) {
1261
915
  flushText();
1262
916
  nodes.push({ type: "hardBreak" });
1263
- } else if ("tab" in c) {
1264
- flushText();
1265
- nodes.push({ type: "tab" });
1266
917
  }
1267
918
  }
1268
919
  flushText();
@@ -1285,98 +936,32 @@ var DocxManager = class {
1285
936
  }
1286
937
  resolveMarks(opts) {
1287
938
  const marks = [];
1288
- if (opts.bold) marks.push({ type: "bold" });
1289
- if (opts.italic) marks.push({ type: "italic" });
1290
- if (opts.underline) marks.push({ type: "underline" });
1291
- if (opts.strike) {
1292
- const strikeAttrs = parseDocx$3(opts);
1293
- marks.push({
1294
- type: "strike",
1295
- attrs: { doubleStrike: strikeAttrs.doubleStrike ?? null }
1296
- });
1297
- }
1298
- if (opts.subScript) marks.push({ type: "subscript" });
1299
- if (opts.superScript) marks.push({ type: "superscript" });
1300
- if (opts.highlight) marks.push({
1301
- type: "highlight",
1302
- attrs: { color: opts.highlight }
1303
- });
1304
- if (opts.style === "CodeChar") marks.push({ type: "code" });
1305
- const textStyleAttrs = parseDocx$8(opts);
1306
- if (opts.style === "CodeChar") {
1307
- delete textStyleAttrs.font;
1308
- 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 });
1309
946
  }
1310
- if (Object.keys(textStyleAttrs).length > 0) marks.push({
1311
- type: "textStyle",
1312
- attrs: textStyleAttrs
1313
- });
1314
947
  return marks.length > 0 ? marks : void 0;
1315
948
  }
1316
- resolveImage(imageOpts) {
1317
- const attrs = parseDocx$1(imageOpts);
1318
- const data = imageOpts.data;
1319
- const type = imageOpts.type;
1320
- if (data && type) attrs.src = `data:image/${type};base64,${encodeBase64(data instanceof ArrayBuffer ? new Uint8Array(data) : data)}`;
1321
- return {
1322
- type: "image",
1323
- attrs
1324
- };
1325
- }
1326
- resolveHyperlink(hyperlink) {
1327
- const href = hyperlink.link ?? (hyperlink.anchor ? `#${hyperlink.anchor}` : "");
1328
- if (!href) return null;
1329
- const content = this.resolveParagraphChildren((hyperlink.children ?? []).map((c) => c));
1330
- if (content.length > 0) {
1331
- const merged = mergeTextNodes(content);
1332
- for (const node of merged) if (node.type === "text") node.marks = [...node.marks ?? [], {
1333
- type: "link",
1334
- attrs: {
1335
- href,
1336
- target: href.startsWith("#") ? null : "_blank",
1337
- rel: "noopener noreferrer nofollow",
1338
- class: null,
1339
- title: hyperlink.tooltip ?? null
1340
- }
1341
- }];
1342
- return merged;
1343
- }
1344
- return null;
1345
- }
1346
949
  };
1347
- const HEADING_LEVEL_MAP = {
1348
- Heading1: 1,
1349
- Heading2: 2,
1350
- Heading3: 3,
1351
- Heading4: 4,
1352
- Heading5: 5,
1353
- Heading6: 6,
1354
- Heading7: 7,
1355
- Heading8: 8,
1356
- Heading9: 9,
1357
- Title: 1
1358
- };
1359
- /** Heading level (1-9) from a localized style NAME: "heading 1"/"标题 1" → 1,
1360
- * "title" → 1. office-open's built-in names are English ("heading 1"), but
1361
- * zh-CN Word labels the same styles "标题 1"; both map to the same level. */
1362
- function headingLevelFromName(name) {
1363
- if (!name) return void 0;
1364
- const m = /^heading\s+(\d)$/i.exec(name) ?? /^标题\s*(\d)$/.exec(name);
1365
- if (m) {
1366
- const lvl = Number(m[1]);
1367
- if (lvl >= 1 && lvl <= 9) return lvl;
1368
- }
1369
- 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;
1370
956
  }
1371
- const defaultManager = new DocxManager();
1372
957
  /**
1373
958
  * Parse a DOCX file into Tiptap JSON (runtime model).
1374
959
  *
1375
960
  * Combines @office-open/docx's `parseDocument` (DOCX binary → DocumentOptions)
1376
961
  * with `DocxManager.resolve` (DocumentOptions → Tiptap JSON).
1377
962
  */
1378
- function parseDOCX(data) {
1379
- return defaultManager.resolve(parseDocument(data));
963
+ function parseDOCX(data, extensions) {
964
+ return getDocxManager(extensions).resolve(parseDocument(data));
1380
965
  }
1381
966
  /**
1382
967
  * Merge {@link DocxGenerateOptions.document} into the `DocumentOptions`
@@ -1407,9 +992,9 @@ function applyDocumentOptions(base, document) {
1407
992
  * in place (http image URLs become embedded data URLs).
1408
993
  */
1409
994
  async function generateDOCX(json, options) {
1410
- const { prepare = true, packer, document } = options ?? {};
995
+ const { prepare = true, packer, document, extensions } = options ?? {};
1411
996
  if (prepare !== false) await prepareDocument(json, prepare === true ? void 0 : prepare);
1412
- return generateDocument(applyDocumentOptions(compileDocument(json), document), packer);
997
+ return generateDocument(applyDocumentOptions(compileDocument(json, extensions), document), packer);
1413
998
  }
1414
999
  /**
1415
1000
  * Generate a DOCX file synchronously — fastest throughput, blocks the event loop.
@@ -1419,8 +1004,8 @@ async function generateDOCX(json, options) {
1419
1004
  * when http images need embedding. `options.document` is still applied.
1420
1005
  */
1421
1006
  function generateDOCXSync(json, options) {
1422
- const { packer, document } = options ?? {};
1423
- return generateDocumentSync(applyDocumentOptions(compileDocument(json), document), packer);
1007
+ const { packer, document, extensions } = options ?? {};
1008
+ return generateDocumentSync(applyDocumentOptions(compileDocument(json, extensions), document), packer);
1424
1009
  }
1425
1010
  /**
1426
1011
  * Generate a DOCX file as a `ReadableStream<Uint8Array>` — for large documents
@@ -1430,21 +1015,21 @@ function generateDOCXSync(json, options) {
1430
1015
  * `DocxManager.compile` → `generateDocumentStream`. Async due to preparation.
1431
1016
  */
1432
1017
  async function generateDOCXStream(json, options) {
1433
- const { prepare = true, packer, document } = options ?? {};
1018
+ const { prepare = true, packer, document, extensions } = options ?? {};
1434
1019
  if (prepare !== false) await prepareDocument(json, prepare === true ? void 0 : prepare);
1435
- return generateDocumentStream(applyDocumentOptions(compileDocument(json), document), packer);
1020
+ return generateDocumentStream(applyDocumentOptions(compileDocument(json, extensions), document), packer);
1436
1021
  }
1437
1022
  /**
1438
1023
  * Convert DocumentOptions (persistence model) to Tiptap JSON (runtime model).
1439
1024
  */
1440
- function resolveDocument(docOpts) {
1441
- return defaultManager.resolve(docOpts);
1025
+ function resolveDocument(docOpts, extensions) {
1026
+ return getDocxManager(extensions).resolve(docOpts);
1442
1027
  }
1443
1028
  /**
1444
1029
  * Convert Tiptap JSON (runtime model) to DocumentOptions (persistence model).
1445
1030
  */
1446
- function compileDocument(json) {
1447
- return defaultManager.compile(json);
1031
+ function compileDocument(json, extensions) {
1032
+ return getDocxManager(extensions).compile(json);
1448
1033
  }
1449
1034
  //#endregion
1450
1035
  export { DocxManager, compileDocument, generateDOCX, generateDOCXStream, generateDOCXSync, parseDOCX, resolveDocument };