@docen/docx 0.3.0 → 0.3.2

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 (74) hide show
  1. package/README.md +2 -3
  2. package/dist/converters/docx.d.mts +95 -5
  3. package/dist/converters/docx.mjs +732 -82
  4. package/dist/converters/html.d.mts +20 -5
  5. package/dist/converters/html.mjs +94 -8
  6. package/dist/converters/markdown.d.mts +1 -1
  7. package/dist/converters/markdown.mjs +1 -1
  8. package/dist/converters/patch.d.mts +1 -1
  9. package/dist/converters/patch.mjs +4 -5
  10. package/dist/converters/prepare.d.mts +15 -3
  11. package/dist/converters/prepare.mjs +50 -4
  12. package/dist/converters/styles.d.mts +85 -0
  13. package/dist/converters/styles.mjs +285 -0
  14. package/dist/core-C9VunJ8o.d.mts +174 -0
  15. package/dist/core-DT9IrTzN.mjs +670 -0
  16. package/dist/core.d.mts +1 -1
  17. package/dist/core.mjs +1 -1
  18. package/dist/document-BH1y4qHM.d.mts +6 -0
  19. package/dist/editor.d.mts +1 -1
  20. package/dist/editor.mjs +3 -2
  21. package/dist/extensions/blockquote.d.mts +23 -0
  22. package/dist/extensions/blockquote.mjs +31 -0
  23. package/dist/extensions/code-block.d.mts +19 -0
  24. package/dist/extensions/code-block.mjs +23 -0
  25. package/dist/extensions/column-break.d.mts +31 -0
  26. package/dist/extensions/column-break.mjs +2 -0
  27. package/dist/extensions/details.d.mts +19 -0
  28. package/dist/extensions/details.mjs +18 -0
  29. package/dist/extensions/document.d.mts +2 -0
  30. package/dist/extensions/document.mjs +50 -0
  31. package/dist/extensions/extensions.d.mts +5 -5
  32. package/dist/extensions/extensions.mjs +4 -3
  33. package/dist/extensions/formatting-marks.d.mts +38 -0
  34. package/dist/extensions/formatting-marks.mjs +2 -0
  35. package/dist/extensions/heading.mjs +24 -3
  36. package/dist/extensions/image.d.mts +2 -2
  37. package/dist/extensions/image.mjs +244 -49
  38. package/dist/extensions/index.d.mts +4 -2
  39. package/dist/extensions/index.mjs +4 -2
  40. package/dist/extensions/mention.d.mts +25 -0
  41. package/dist/extensions/mention.mjs +44 -0
  42. package/dist/extensions/ordered-list.d.mts +25 -0
  43. package/dist/extensions/ordered-list.mjs +41 -0
  44. package/dist/extensions/page-break.d.mts +2 -0
  45. package/dist/extensions/page-break.mjs +2 -0
  46. package/dist/extensions/paragraph.d.mts +1 -1
  47. package/dist/extensions/paragraph.mjs +39 -4
  48. package/dist/extensions/passthrough.d.mts +2 -0
  49. package/dist/extensions/passthrough.mjs +2 -0
  50. package/dist/extensions/section-break.d.mts +44 -0
  51. package/dist/extensions/section-break.mjs +2 -0
  52. package/dist/extensions/table.mjs +29 -3
  53. package/dist/extensions/task-item.d.mts +27 -0
  54. package/dist/extensions/task-item.mjs +37 -0
  55. package/dist/extensions/text-style.mjs +20 -3
  56. package/dist/extensions/tiptap.d.mts +2 -2
  57. package/dist/extensions/tiptap.mjs +1 -3
  58. package/dist/extensions/types.d.mts +7 -5
  59. package/dist/extensions/utils.d.mts +2 -49
  60. package/dist/extensions/utils.mjs +222 -12
  61. package/dist/extensions/wpg-group.d.mts +2 -0
  62. package/dist/extensions/wpg-group.mjs +2 -0
  63. package/dist/extensions/wps-shape.d.mts +2 -0
  64. package/dist/extensions/wps-shape.mjs +2 -0
  65. package/dist/index.d.mts +127 -18
  66. package/dist/index.mjs +7 -2
  67. package/dist/paragraph-D8mpHo_o.d.mts +8 -0
  68. package/dist/{tiptap-TErPjuNJ.d.mts → tiptap-pZsNPsvV.d.mts} +1 -3
  69. package/dist/utils-D87vukzp.d.mts +128 -0
  70. package/package.json +40 -40
  71. package/dist/core-CFIQVRfx.mjs +0 -88
  72. package/dist/core-omBKMRtl.d.mts +0 -34
  73. package/dist/image-Ge1y6uam.d.mts +0 -47
  74. package/dist/paragraph-fhEXtAN2.d.mts +0 -16
@@ -1,14 +1,22 @@
1
- import { parseDocx, renderDocx } from "../extensions/heading.mjs";
2
- import { parseDocx as parseDocx$1, renderDocx as renderDocx$1 } from "../extensions/image.mjs";
3
- import { parseDocx as parseDocx$2, renderDocx as renderDocx$2 } from "../extensions/paragraph.mjs";
4
- import { parseDocx as parseDocx$3, renderDocx as renderDocx$3 } from "../extensions/strike.mjs";
5
- import { parseDocx as parseDocx$4, renderDocx as renderDocx$4 } from "../extensions/table.mjs";
6
- import { parseDocx as parseDocx$5, renderDocx as renderDocx$5 } from "../extensions/table-cell.mjs";
7
- import { parseDocx as parseDocx$6, renderDocx as renderDocx$6 } from "../extensions/table-header.mjs";
8
- import { parseDocx as parseDocx$7, renderDocx as renderDocx$7 } from "../extensions/table-row.mjs";
9
- import { parseDocx as parseDocx$8, renderDocx as renderDocx$8 } from "../extensions/text-style.mjs";
1
+ import { BLOCKQUOTE_BORDER, applyBlockquoteStyle } from "../extensions/blockquote.mjs";
2
+ import { renderDocx } from "../extensions/code-block.mjs";
3
+ import { DETAILS_SUMMARY_STYLE, DETAILS_TAG } from "../extensions/details.mjs";
4
+ import { parseDocx, renderDocx as renderDocx$1 } from "../extensions/heading.mjs";
5
+ import { parseDocx as parseDocx$1, renderDocx as renderDocx$2 } from "../extensions/image.mjs";
6
+ import { createMention, isMention, readMention } from "../extensions/mention.mjs";
7
+ import { ORDERED_REFERENCE_PREFIX, buildOrderedLevels } from "../extensions/ordered-list.mjs";
8
+ import { parseDocx as parseDocx$2, renderDocx as renderDocx$3 } from "../extensions/paragraph.mjs";
9
+ import { parseDocx as parseDocx$3, renderDocx as renderDocx$4 } from "../extensions/strike.mjs";
10
+ import { parseDocx as parseDocx$4, renderDocx as renderDocx$5 } from "../extensions/table.mjs";
11
+ import { parseDocx as parseDocx$5, renderDocx as renderDocx$6 } from "../extensions/table-cell.mjs";
12
+ import { parseDocx as parseDocx$6, renderDocx as renderDocx$7 } from "../extensions/table-header.mjs";
13
+ import { parseDocx as parseDocx$7, renderDocx as renderDocx$8 } from "../extensions/table-row.mjs";
14
+ import { createTaskCheckbox, isTaskCheckbox, readCheckboxState } from "../extensions/task-item.mjs";
15
+ import { parseDocx as parseDocx$8, renderDocx as renderDocx$9 } from "../extensions/text-style.mjs";
10
16
  import { prepareDocument } from "./prepare.mjs";
17
+ import { emojis, shortcodeToEmoji } from "@tiptap/extension-emoji";
11
18
  import { generateDocument, generateDocumentStream, generateDocumentSync, parseDocument } from "@office-open/docx";
19
+ import { encodeBase64 } from "@office-open/core";
12
20
  //#region src/converters/docx.ts
13
21
  /** Remove keys with null/undefined values */
14
22
  function cleanAttrs(attrs) {
@@ -16,6 +24,53 @@ function cleanAttrs(attrs) {
16
24
  for (const [key, value] of Object.entries(attrs)) if (value !== null && value !== void 0) result[key] = value;
17
25
  return result;
18
26
  }
27
+ /** True when two cell-margin sets (w:tcMar / w:tblCellMar) match on every side's
28
+ * size — used to detect a cell that merely echoes the table's default so its
29
+ * redundant tcMar can be dropped for a near-identity round-trip. */
30
+ function sameCellMargins(a, b) {
31
+ const sz = (m, k) => m[k]?.size ?? null;
32
+ return [
33
+ "top",
34
+ "right",
35
+ "bottom",
36
+ "left"
37
+ ].every((k) => sz(a, k) === sz(b, k));
38
+ }
39
+ /** Keys round-tripped between DocumentOptions core properties and `doc.attrs.core`. */
40
+ const CORE_PROPERTY_KEYS = [
41
+ "title",
42
+ "subject",
43
+ "creator",
44
+ "keywords",
45
+ "description",
46
+ "lastModifiedBy",
47
+ "lastPrinted",
48
+ "created",
49
+ "modified",
50
+ "revision"
51
+ ];
52
+ /**
53
+ * DocumentOptions keys that DocxManager reconstructs (sections/numbering) or
54
+ * carries in dedicated attrs (styles/background/core). Excluded from the
55
+ * documentExtras pass-through so they aren't duplicated.
56
+ */
57
+ const COMPILE_OWNED_KEYS = new Set([
58
+ "sections",
59
+ "numbering",
60
+ "styles",
61
+ "background",
62
+ ...CORE_PROPERTY_KEYS
63
+ ]);
64
+ /** Collect core properties present on DocumentOptions into a plain object. */
65
+ function extractCoreProperties(docOpts) {
66
+ const source = docOpts;
67
+ const core = {};
68
+ for (const key of CORE_PROPERTY_KEYS) {
69
+ const value = source[key];
70
+ if (value !== void 0 && value !== null) core[key] = value;
71
+ }
72
+ return Object.keys(core).length > 0 ? core : null;
73
+ }
19
74
  /** Merge consecutive text nodes with same marks */
20
75
  function mergeTextNodes(nodes) {
21
76
  const result = [];
@@ -38,74 +93,202 @@ function mergeTextNodes(nodes) {
38
93
  * DocxManager handles tree walking, child assembly, and dispatching.
39
94
  */
40
95
  var DocxManager = class {
96
+ numberingConfigs = [];
97
+ orderedInstanceCounter = 0;
98
+ resolveStyles;
41
99
  compile(json) {
42
- const children = [];
100
+ this.numberingConfigs = [];
101
+ this.orderedInstanceCounter = 0;
102
+ const sections = [];
103
+ let currentChildren = [];
43
104
  if (json.content) for (const node of json.content) {
44
105
  const child = this.compileSectionChild(node);
45
- if (!child) continue;
46
- if (Array.isArray(child)) children.push(...child);
47
- else children.push(child);
106
+ if (child) if (Array.isArray(child)) currentChildren.push(...child);
107
+ else currentChildren.push(child);
108
+ if (node.type === "paragraph") {
109
+ const na = node.attrs ?? {};
110
+ if (na.sectionProperties != null) {
111
+ sections.push(this.buildSection(currentChildren, na.sectionProperties, this.compileHeaderFooter(na.sectionHeaders ?? null), this.compileHeaderFooter(na.sectionFooters ?? null)));
112
+ currentChildren = [];
113
+ }
114
+ }
115
+ }
116
+ const docAttrs = json.attrs ?? {};
117
+ sections.push(this.buildSection(currentChildren, docAttrs.sectionProperties ?? null, this.compileHeaderFooter(docAttrs.sectionHeaders ?? null), this.compileHeaderFooter(docAttrs.sectionFooters ?? null)));
118
+ const styles = docAttrs.styles ?? void 0;
119
+ const core = docAttrs.core ?? void 0;
120
+ const background = docAttrs.background ?? void 0;
121
+ const documentExtras = docAttrs.documentExtras ?? void 0;
122
+ return {
123
+ sections,
124
+ ...styles ? { styles } : {},
125
+ ...core,
126
+ ...background ? { background } : {},
127
+ ...documentExtras,
128
+ ...this.numberingConfigs.length > 0 ? { numbering: { config: this.numberingConfigs } } : {}
129
+ };
130
+ }
131
+ /** Assemble a SectionOptions from compiled children + optional layout/headers/footers. */
132
+ buildSection(children, properties, headers, footers) {
133
+ return {
134
+ children,
135
+ ...properties ? { properties } : {},
136
+ ...headers ? { headers } : {},
137
+ ...footers ? { footers } : {}
138
+ };
139
+ }
140
+ /**
141
+ * Compile resolved header/footer slots (JSONContent[] per slot) back into
142
+ * SectionChild[] per slot. Returns undefined when no slot has content.
143
+ */
144
+ compileHeaderFooter(slots) {
145
+ if (!slots) return void 0;
146
+ const group = {};
147
+ for (const slot of [
148
+ "default",
149
+ "first",
150
+ "even"
151
+ ]) {
152
+ const json = slots[slot];
153
+ if (!json?.length) continue;
154
+ const children = [];
155
+ for (const node of json) {
156
+ const child = this.compileSectionChild(node);
157
+ if (!child) continue;
158
+ if (Array.isArray(child)) children.push(...child);
159
+ else children.push(child);
160
+ }
161
+ if (children.length > 0) group[slot] = children;
162
+ }
163
+ return Object.keys(group).length > 0 ? group : void 0;
164
+ }
165
+ /**
166
+ * Resolve a section's header/footer group (SectionChild[] per slot) into
167
+ * Tiptap JSON slots. Returns null when no slot has content.
168
+ */
169
+ resolveHeaderFooter(group, numberingLookup) {
170
+ if (!group) return null;
171
+ const slots = {};
172
+ for (const slot of [
173
+ "default",
174
+ "first",
175
+ "even"
176
+ ]) {
177
+ const children = group[slot];
178
+ if (children?.length) slots[slot] = this.resolveSectionChildren(children, numberingLookup);
48
179
  }
49
- return { sections: [{ children }] };
180
+ return Object.keys(slots).length > 0 ? slots : null;
50
181
  }
51
182
  resolve(docOpts) {
183
+ this.resolveStyles = docOpts.styles ?? void 0;
52
184
  const sections = docOpts.sections ?? [];
53
185
  if (sections.length === 0) return {
54
186
  type: "doc",
55
187
  content: [{ type: "paragraph" }]
56
188
  };
57
- const children = sections[0].children ?? [];
189
+ const numberingLookup = this.buildNumberingLookup(docOpts);
58
190
  const content = [];
59
- for (const child of children) {
60
- const node = this.resolveSectionChild(child);
61
- if (node) content.push(node);
191
+ const lastIndex = sections.length - 1;
192
+ for (let i = 0; i < sections.length; i++) {
193
+ const section = sections[i];
194
+ const sectionContent = this.resolveSectionChildren(section.children ?? [], numberingLookup);
195
+ if (i < lastIndex) {
196
+ const sectAttrs = {
197
+ sectionProperties: section.properties ?? null,
198
+ sectionHeaders: this.resolveHeaderFooter(section.headers, numberingLookup),
199
+ sectionFooters: this.resolveHeaderFooter(section.footers, numberingLookup)
200
+ };
201
+ const last = sectionContent[sectionContent.length - 1];
202
+ if (last?.type === "paragraph") last.attrs = {
203
+ ...last.attrs,
204
+ ...sectAttrs
205
+ };
206
+ else sectionContent.push({
207
+ type: "paragraph",
208
+ attrs: sectAttrs
209
+ });
210
+ }
211
+ content.push(...sectionContent);
62
212
  }
63
- return {
213
+ const doc = {
64
214
  type: "doc",
65
215
  content: content.length > 0 ? content : [{ type: "paragraph" }]
66
216
  };
217
+ const attrs = {};
218
+ if (docOpts.styles) attrs.styles = docOpts.styles;
219
+ if (docOpts.background) attrs.background = docOpts.background;
220
+ const core = extractCoreProperties(docOpts);
221
+ if (core) attrs.core = core;
222
+ const lastSection = sections[lastIndex];
223
+ if (lastSection.properties) attrs.sectionProperties = lastSection.properties;
224
+ const lastHeaders = this.resolveHeaderFooter(lastSection.headers, numberingLookup);
225
+ if (lastHeaders) attrs.sectionHeaders = lastHeaders;
226
+ const lastFooters = this.resolveHeaderFooter(lastSection.footers, numberingLookup);
227
+ if (lastFooters) attrs.sectionFooters = lastFooters;
228
+ const documentExtras = {};
229
+ for (const [k, v] of Object.entries(docOpts)) {
230
+ if (COMPILE_OWNED_KEYS.has(k)) continue;
231
+ if (v === void 0 || v === null) continue;
232
+ documentExtras[k] = v;
233
+ }
234
+ if (Object.keys(documentExtras).length > 0) attrs.documentExtras = documentExtras;
235
+ if (Object.keys(attrs).length > 0) doc.attrs = attrs;
236
+ return doc;
67
237
  }
68
238
  compileSectionChild(node) {
69
239
  switch (node.type) {
70
240
  case "paragraph": return { paragraph: this.compileParagraphNode(node) };
71
241
  case "heading": return { paragraph: this.compileHeadingNode(node) };
72
- case "blockquote":
73
- for (const child of node.content ?? []) if (child.type === "paragraph") return { paragraph: this.compileParagraphNode(child) };
74
- return null;
75
- case "codeBlock": return { paragraph: this.compileCodeBlock(node) };
242
+ case "blockquote": {
243
+ const items = [];
244
+ for (const child of node.content ?? []) if (child.type === "paragraph" || child.type === "heading") {
245
+ const para = child.type === "heading" ? this.compileHeadingNode(child) : this.compileParagraphNode(child);
246
+ const paraObj = typeof para === "string" ? { text: para } : para;
247
+ applyBlockquoteStyle(paraObj);
248
+ items.push({ paragraph: paraObj });
249
+ }
250
+ return items.length > 0 ? items : null;
251
+ }
252
+ case "codeBlock": {
253
+ const opts = renderDocx(node);
254
+ const childList = this.compileInlineContent(node.content);
255
+ if (childList.length > 0) opts.children = childList;
256
+ return { paragraph: this.simplifyParagraph(opts) };
257
+ }
76
258
  case "horizontalRule": return { paragraph: { thematicBreak: true } };
77
259
  case "table": return { table: this.compileTableNode(node) };
78
260
  case "image": {
79
- const imageRun = renderDocx$1(node);
261
+ const imageRun = renderDocx$2(node);
80
262
  if (!imageRun) return null;
81
263
  return { paragraph: { children: [imageRun] } };
82
264
  }
83
265
  case "bulletList":
84
266
  case "orderedList":
85
- case "taskList": return this.compileListFromNode(node);
267
+ case "taskList": return this.compileListFromNode(node, 0);
268
+ case "details": return this.compileDetailsNode(node);
269
+ case "passthrough": {
270
+ const data = node.attrs?.data ?? "{}";
271
+ try {
272
+ return JSON.parse(data);
273
+ } catch {
274
+ return null;
275
+ }
276
+ }
86
277
  default: return null;
87
278
  }
88
279
  }
89
280
  compileParagraphNode(node) {
90
- const opts = renderDocx$2(node);
281
+ const opts = renderDocx$3(node);
91
282
  const childList = this.compileInlineContent(node.content);
92
283
  if (childList.length > 0) opts.children = childList;
93
284
  return this.simplifyParagraph(opts);
94
285
  }
95
286
  compileHeadingNode(node) {
96
- const opts = renderDocx(node);
287
+ const opts = renderDocx$1(node);
97
288
  const childList = this.compileInlineContent(node.content);
98
289
  if (childList.length > 0) opts.children = childList;
99
290
  return this.simplifyParagraph(opts);
100
291
  }
101
- compileCodeBlock(node) {
102
- const text = (node.content ?? []).map((c) => c.text ?? "").join("");
103
- return {
104
- style: "Code",
105
- ...node.attrs?.language ? { run: { font: "Consolas" } } : {},
106
- text
107
- };
108
- }
109
292
  /** Simple text optimization: merge plain runs into text field */
110
293
  simplifyParagraph(opts) {
111
294
  const children = opts.children;
@@ -119,13 +302,14 @@ var DocxManager = class {
119
302
  return opts;
120
303
  }
121
304
  compileTableNode(node) {
122
- const opts = renderDocx$4(node);
305
+ const opts = renderDocx$5(node);
123
306
  const colCount = this.getTableColumnCount(node);
307
+ const tableCellMargins = opts.cellMargin ?? opts.margins ?? null;
124
308
  const rows = [];
125
309
  let activeSpans = [];
126
310
  for (const rowNode of node.content ?? []) {
127
311
  if (rowNode.type !== "tableRow") continue;
128
- const rowOpts = renderDocx$7(rowNode);
312
+ const rowOpts = renderDocx$8(rowNode);
129
313
  const pmCells = (rowNode.content ?? []).filter((c) => c.type === "tableCell" || c.type === "tableHeader");
130
314
  const currentSpans = [...activeSpans].sort((a, b) => a.colStart - b.colStart);
131
315
  const newSpans = [];
@@ -145,7 +329,7 @@ var DocxManager = class {
145
329
  colIdx += span.colspan;
146
330
  spanIdx++;
147
331
  } else {
148
- const cell = this.compileTableCellNode(pmCells[cellIdx]);
332
+ const cell = this.compileTableCellNode(pmCells[cellIdx], tableCellMargins);
149
333
  const cs = cell.columnSpan ?? 1;
150
334
  const rs = cell.rowSpan ?? 1;
151
335
  if (rs > 1) {
@@ -241,8 +425,9 @@ var DocxManager = class {
241
425
  }
242
426
  };
243
427
  }
244
- compileTableCellNode(cellNode) {
245
- const cellOpts = cellNode.type === "tableHeader" ? renderDocx$6(cellNode) : renderDocx$5(cellNode);
428
+ compileTableCellNode(cellNode, tableMargins) {
429
+ const cellOpts = cellNode.type === "tableHeader" ? renderDocx$7(cellNode) : renderDocx$6(cellNode);
430
+ if (tableMargins && cellOpts.margins && sameCellMargins(cellOpts.margins, tableMargins)) delete cellOpts.margins;
246
431
  const cellAlign = cellNode.attrs?.align ?? null;
247
432
  const cellChildren = [];
248
433
  for (const paraNode of cellNode.content ?? []) {
@@ -254,23 +439,95 @@ var DocxManager = class {
254
439
  if (cellChildren.length > 0) cellOpts.children = cellChildren;
255
440
  return cellOpts;
256
441
  }
257
- compileListFromNode(node) {
442
+ compileListFromNode(node, level) {
258
443
  const items = [];
444
+ const isOrdered = node.type === "orderedList";
445
+ const isTask = node.type === "taskList";
446
+ let ordered;
447
+ if (isOrdered) ordered = this.registerOrderedNumbering(node);
259
448
  for (const listItem of node.content ?? []) {
260
449
  if (listItem.type !== "listItem" && listItem.type !== "taskItem") continue;
450
+ const checked = Boolean(listItem.attrs?.checked);
261
451
  for (const child of listItem.content ?? []) if (child.type === "paragraph" || child.type === "heading") {
262
- const para = this.compileParagraphNode(child);
452
+ const para = child.type === "heading" ? this.compileHeadingNode(child) : this.compileParagraphNode(child);
263
453
  const paraObj = typeof para === "string" ? { text: para } : para;
264
- if (node.type === "bulletList") paraObj.bullet = { level: 0 };
265
- else if (node.type === "orderedList") paraObj.numbering = {
266
- reference: "default-numbering",
267
- level: 0
454
+ if (ordered) paraObj.numbering = {
455
+ reference: ordered.reference,
456
+ instance: ordered.instance,
457
+ level
268
458
  };
459
+ else paraObj.bullet = { level };
460
+ if (isTask) this.injectTaskCheckbox(paraObj, checked);
269
461
  items.push({ paragraph: paraObj });
462
+ } else if (child.type === "bulletList" || child.type === "orderedList" || child.type === "taskList") {
463
+ const nested = this.compileListFromNode(child, level + 1);
464
+ if (nested) items.push(...nested);
270
465
  }
271
466
  }
272
467
  return items.length > 0 ? items : null;
273
468
  }
469
+ /**
470
+ * Register (or reuse) an abstractNum for an ordered list's `start`, and
471
+ * return a fresh instance so this list counts independently of other lists
472
+ * that share the same definition.
473
+ */
474
+ registerOrderedNumbering(node) {
475
+ const start = Number(node.attrs?.start ?? 1) || 1;
476
+ let entry = this.numberingConfigs.find((c) => Number(c.levels[0]?.start ?? 1) === start);
477
+ if (!entry) {
478
+ entry = {
479
+ reference: `${ORDERED_REFERENCE_PREFIX}-${this.numberingConfigs.length + 1}`,
480
+ levels: buildOrderedLevels(start)
481
+ };
482
+ this.numberingConfigs.push(entry);
483
+ }
484
+ this.orderedInstanceCounter += 1;
485
+ return {
486
+ reference: entry.reference,
487
+ instance: this.orderedInstanceCounter
488
+ };
489
+ }
490
+ /**
491
+ * Prepend an inline checkbox SDT to a task paragraph. The SDT is tagged
492
+ * "docen-task" so resolve can tell task items apart from ordinary paragraphs
493
+ * that happen to contain an SDT.
494
+ */
495
+ injectTaskCheckbox(paraObj, checked) {
496
+ let existing = [];
497
+ if (Array.isArray(paraObj.children)) existing = paraObj.children;
498
+ else if (typeof paraObj.text === "string") {
499
+ if (paraObj.text) existing = [{ text: paraObj.text }];
500
+ delete paraObj.text;
501
+ }
502
+ paraObj.children = [createTaskCheckbox(checked), ...existing];
503
+ }
504
+ /**
505
+ * details → block-level group-SDT. The summary paragraph is tagged with a
506
+ * fixed style so resolve can split it back out; content blocks flatten in
507
+ * after it. (No native collapse in DOCX — structure round-trips, the view
508
+ * stays expanded.)
509
+ */
510
+ compileDetailsNode(node) {
511
+ const sdtChildren = [];
512
+ for (const child of node.content ?? []) if (child.type === "detailsSummary") {
513
+ const inline = this.compileInlineContent(child.content);
514
+ const summaryPara = { style: DETAILS_SUMMARY_STYLE };
515
+ if (inline.length > 0) summaryPara.children = inline;
516
+ sdtChildren.push({ paragraph: summaryPara });
517
+ } else if (child.type === "detailsContent") for (const block of child.content ?? []) {
518
+ const compiled = this.compileSectionChild(block);
519
+ if (!compiled) continue;
520
+ if (Array.isArray(compiled)) sdtChildren.push(...compiled);
521
+ else sdtChildren.push(compiled);
522
+ }
523
+ return { sdt: {
524
+ properties: {
525
+ tag: DETAILS_TAG,
526
+ group: true
527
+ },
528
+ children: sdtChildren
529
+ } };
530
+ }
274
531
  compileInlineContent(content) {
275
532
  if (!content) return [];
276
533
  const children = [];
@@ -281,20 +538,52 @@ var DocxManager = class {
281
538
  case "hardBreak":
282
539
  children.push({ break: 1 });
283
540
  break;
541
+ case "pageBreak":
542
+ children.push({ pageBreak: true });
543
+ break;
544
+ case "columnBreak":
545
+ children.push({ columnBreak: true });
546
+ break;
284
547
  case "image": {
285
- const imageRun = renderDocx$1(node);
548
+ const imageRun = renderDocx$2(node);
286
549
  if (imageRun) children.push(imageRun);
287
550
  break;
288
551
  }
552
+ case "wpgGroup": {
553
+ const wpgGroup = node.attrs?.wpgGroup;
554
+ if (wpgGroup) children.push({ wpgGroup });
555
+ break;
556
+ }
557
+ case "wpsShape": {
558
+ const wpsShape = node.attrs?.wpsShape;
559
+ if (wpsShape) children.push({ wpsShape });
560
+ break;
561
+ }
562
+ case "mention":
563
+ children.push(createMention(String(node.attrs?.id ?? ""), String(node.attrs?.label ?? "")));
564
+ break;
565
+ case "emoji": {
566
+ const name = String(node.attrs?.name ?? "");
567
+ const text = (name ? shortcodeToEmoji(name, emojis)?.emoji : void 0) ?? (name ? `:${name}:` : "");
568
+ if (text) children.push({ text });
569
+ break;
570
+ }
289
571
  }
290
572
  return children;
291
573
  }
292
574
  compileTextNode(node, children) {
293
575
  const text = node.text ?? "";
294
576
  if (!text) return;
295
- const marks = node.marks ?? [];
577
+ const segments = text.split("\n");
578
+ for (let i = 0; i < segments.length; i++) {
579
+ if (i > 0) children.push({ break: 1 });
580
+ if (segments[i]) this.compileTextRun(segments[i], node.marks, children);
581
+ }
582
+ }
583
+ /** Emit a single run for `text` with all inline marks applied. */
584
+ compileTextRun(text, marks, children) {
296
585
  const runOpts = { text };
297
- for (const mark of marks) switch (mark.type) {
586
+ for (const mark of marks ?? []) switch (mark.type) {
298
587
  case "bold":
299
588
  runOpts.bold = true;
300
589
  break;
@@ -305,7 +594,7 @@ var DocxManager = class {
305
594
  runOpts.underline = { type: "single" };
306
595
  break;
307
596
  case "strike": {
308
- const strikeProps = renderDocx$3(mark.attrs ?? {});
597
+ const strikeProps = renderDocx$4(mark.attrs ?? {});
309
598
  Object.assign(runOpts, strikeProps);
310
599
  break;
311
600
  }
@@ -319,10 +608,11 @@ var DocxManager = class {
319
608
  runOpts.highlight = mark.attrs?.color ?? "yellow";
320
609
  break;
321
610
  case "code":
611
+ runOpts.style = "CodeChar";
322
612
  runOpts.font = "Consolas";
323
613
  break;
324
614
  case "textStyle": {
325
- const tsProps = renderDocx$8(mark.attrs ?? {});
615
+ const tsProps = renderDocx$9(mark.attrs ?? {});
326
616
  Object.assign(runOpts, tsProps);
327
617
  break;
328
618
  }
@@ -350,15 +640,72 @@ var DocxManager = class {
350
640
  resolveSectionChild(child) {
351
641
  if ("paragraph" in child) return this.resolveParagraph(child.paragraph);
352
642
  if ("table" in child) return this.resolveTable(child.table);
353
- return null;
643
+ if ("sdt" in child) {
644
+ const sdt = child.sdt;
645
+ if (sdt.properties?.tag === "docen-details") return this.resolveDetailsSdt(sdt);
646
+ return this.resolvePassthrough(child);
647
+ }
648
+ return this.resolvePassthrough(child);
649
+ }
650
+ /** Wrap an opaque SectionChild in a passthrough atom (attrs.data = JSON). */
651
+ resolvePassthrough(child) {
652
+ return {
653
+ type: "passthrough",
654
+ attrs: { data: JSON.stringify(child) }
655
+ };
656
+ }
657
+ /**
658
+ * Resolve a details group-SDT: the summary-style paragraph becomes
659
+ * detailsSummary, the remaining blocks fold into detailsContent.
660
+ */
661
+ resolveDetailsSdt(sdt) {
662
+ const content = [];
663
+ let summary = null;
664
+ for (const child of sdt.children ?? []) {
665
+ if ("paragraph" in child) {
666
+ const para = child.paragraph;
667
+ if (para.style === "DocenDetailsSummary") {
668
+ summary = this.resolveInlineContent(para);
669
+ continue;
670
+ }
671
+ }
672
+ const node = this.resolveSectionChild(child);
673
+ if (!node) continue;
674
+ if (Array.isArray(node)) content.push(...node);
675
+ else content.push(node);
676
+ }
677
+ const details = {
678
+ type: "details",
679
+ content: []
680
+ };
681
+ if (summary !== null) details.content.push({
682
+ type: "detailsSummary",
683
+ content: summary
684
+ });
685
+ if (content.length > 0) details.content.push({
686
+ type: "detailsContent",
687
+ content
688
+ });
689
+ return details;
354
690
  }
355
691
  resolveParagraph(opts) {
356
692
  const resolved = typeof opts === "string" ? { text: opts } : opts;
357
- const headingLevel = resolved.heading ? HEADING_LEVEL_MAP[resolved.heading] : void 0;
693
+ if (resolved.thematicBreak) return { type: "horizontalRule" };
694
+ if (resolved.style === "Code") return this.resolveCodeBlock(resolved);
695
+ let headingLevel = resolved.heading ? HEADING_LEVEL_MAP[resolved.heading] : void 0;
696
+ if (!headingLevel && resolved.style) {
697
+ const name = this.styleNameOf(resolved.style);
698
+ if (name) {
699
+ const m = /^heading\s+(\d)$/i.exec(name);
700
+ if (m) {
701
+ const lvl = Number(m[1]);
702
+ if (lvl >= 1 && lvl <= 6) headingLevel = lvl;
703
+ } else if (/^title$/i.test(name)) headingLevel = 1;
704
+ }
705
+ }
706
+ if (headingLevel && !resolved.heading) resolved.heading = `Heading${headingLevel}`;
358
707
  const nodeType = headingLevel ? "heading" : "paragraph";
359
708
  const attrs = headingLevel ? parseDocx(resolved) : parseDocx$2(resolved);
360
- if (resolved.bullet) return this.resolveListItem(resolved, "bulletList", attrs);
361
- if (resolved.numbering) return this.resolveListItem(resolved, "orderedList", attrs);
362
709
  const content = this.resolveInlineContent(resolved);
363
710
  const cleanAttrsObj = cleanAttrs(attrs);
364
711
  const node = { type: nodeType };
@@ -366,24 +713,262 @@ var DocxManager = class {
366
713
  if (content.length > 0) node.content = content;
367
714
  return node;
368
715
  }
369
- resolveListItem(opts, listType, paraAttrs) {
370
- const content = this.resolveInlineContent(opts);
371
- const cleanAttrsObj = cleanAttrs(paraAttrs);
372
- const paragraphNode = { type: "paragraph" };
373
- if (Object.keys(cleanAttrsObj).length > 0) paragraphNode.attrs = cleanAttrsObj;
374
- if (content.length > 0) paragraphNode.content = content;
716
+ /** Look up a paragraph style's NAME from its styleId, via the styles table
717
+ * carried on the current resolve(). Returns undefined outside resolve. */
718
+ styleNameOf(styleId) {
719
+ return this.resolveStyles?.paragraphStyles?.find((p) => p.id === styleId)?.name;
720
+ }
721
+ /** reference level-0 format/start, for classifying numbering paragraphs. */
722
+ buildNumberingLookup(docOpts) {
723
+ const lookup = /* @__PURE__ */ new Map();
724
+ const config = docOpts.numbering?.config;
725
+ if (config) for (const entry of config) {
726
+ const lvl0 = entry.levels[0];
727
+ lookup.set(entry.reference, {
728
+ format: lvl0?.format,
729
+ start: lvl0?.start
730
+ });
731
+ }
732
+ return lookup;
733
+ }
734
+ /**
735
+ * Walk section children, grouping consecutive list paragraphs into nested
736
+ * Tiptap lists. Non-list children resolve individually. DOCX flattens lists
737
+ * to a paragraph sequence (depth carried by `level`); this rebuilds the tree.
738
+ */
739
+ resolveSectionChildren(children, numberingLookup) {
740
+ const content = [];
741
+ let i = 0;
742
+ while (i < children.length) {
743
+ const child = children[i];
744
+ const firstPara = "paragraph" in child ? child.paragraph : null;
745
+ if (!(firstPara && typeof firstPara !== "string" ? this.detectList(firstPara, numberingLookup) : null)) {
746
+ if (firstPara && typeof firstPara !== "string" && this.detectBlockquote(firstPara)) {
747
+ const group = [];
748
+ while (i < children.length) {
749
+ const member = children[i];
750
+ if (!("paragraph" in member)) break;
751
+ const para = member.paragraph;
752
+ if (typeof para === "string" || !this.detectBlockquote(para)) break;
753
+ group.push(para);
754
+ i++;
755
+ }
756
+ content.push(this.buildBlockquote(group));
757
+ continue;
758
+ }
759
+ const node = this.resolveSectionChild(child);
760
+ if (node) content.push(node);
761
+ i++;
762
+ continue;
763
+ }
764
+ const group = [];
765
+ while (i < children.length) {
766
+ const member = children[i];
767
+ if (!("paragraph" in member)) break;
768
+ const para = member.paragraph;
769
+ if (typeof para === "string") break;
770
+ const info = this.detectList(para, numberingLookup);
771
+ if (!info) break;
772
+ group.push({
773
+ para,
774
+ info
775
+ });
776
+ i++;
777
+ }
778
+ content.push(...this.buildListTree(group));
779
+ }
780
+ return content;
781
+ }
782
+ /** Classify a paragraph as a list item, or null if it isn't one. */
783
+ detectList(para, lookup) {
784
+ const p = para;
785
+ const numbering = p.numbering;
786
+ const bullet = p.bullet;
787
+ let kind;
788
+ let level;
789
+ let reference;
790
+ let start;
791
+ if (numbering) {
792
+ reference = numbering.reference;
793
+ level = numbering.level ?? 0;
794
+ const cfg = reference ? lookup.get(reference) : void 0;
795
+ if (cfg && cfg.format && cfg.format !== "bullet") {
796
+ kind = "ordered";
797
+ start = cfg.start;
798
+ } else {
799
+ kind = "bullet";
800
+ reference = void 0;
801
+ }
802
+ } else if (bullet) {
803
+ kind = "bullet";
804
+ level = bullet.level ?? 0;
805
+ } else return null;
806
+ const first = p.children?.[0];
375
807
  return {
376
- type: listType,
377
- content: [{
378
- type: "listItem",
379
- content: [paragraphNode]
380
- }]
808
+ kind: isTaskCheckbox(first) ? "task" : kind,
809
+ level,
810
+ reference,
811
+ start,
812
+ checked: readCheckboxState(first)
381
813
  };
382
814
  }
815
+ /**
816
+ * Rebuild nested Tiptap lists from a flat run of list paragraphs. Stack-based:
817
+ * each frame is an active list at a given depth; the `key` (level:type:
818
+ * reference) decides whether a paragraph continues the top list, starts a
819
+ * nested list, or splits off a new sibling list.
820
+ */
821
+ buildListTree(group) {
822
+ const topLevel = [];
823
+ const stack = [];
824
+ for (const { para, info } of group) {
825
+ const listType = info.kind === "ordered" ? "orderedList" : info.kind === "task" ? "taskList" : "bulletList";
826
+ const itemType = info.kind === "task" ? "taskItem" : "listItem";
827
+ const key = `${info.level}:${listType}:${info.reference ?? ""}`;
828
+ while (stack.length > 0) {
829
+ const top = stack[stack.length - 1];
830
+ if (top.level > info.level || top.level === info.level && top.key !== key) {
831
+ stack.pop();
832
+ continue;
833
+ }
834
+ break;
835
+ }
836
+ const newItem = {
837
+ type: itemType,
838
+ content: [this.resolveListItemParagraph(para, info)]
839
+ };
840
+ if (itemType === "taskItem") newItem.attrs = { checked: info.checked };
841
+ const top = stack[stack.length - 1];
842
+ if (top && top.level === info.level && top.key === key) {
843
+ top.listNode.content.push(newItem);
844
+ top.currentItem = newItem;
845
+ } else {
846
+ const newList = {
847
+ type: listType,
848
+ content: [newItem]
849
+ };
850
+ if (listType === "orderedList" && info.level === 0 && typeof info.start === "number" && info.start !== 1) newList.attrs = { start: info.start };
851
+ if (top) top.currentItem.content.push(newList);
852
+ else topLevel.push(newList);
853
+ stack.push({
854
+ level: info.level,
855
+ key,
856
+ listNode: newList,
857
+ currentItem: newItem
858
+ });
859
+ }
860
+ }
861
+ return topLevel;
862
+ }
863
+ /** Classify a paragraph as a blockquote member by its signature. */
864
+ detectBlockquote(para) {
865
+ const p = para;
866
+ const indent = p.indent;
867
+ const border = p.border;
868
+ if (!indent || indent.left !== 720) return false;
869
+ const bl = border?.left;
870
+ if (!bl) return false;
871
+ const sig = BLOCKQUOTE_BORDER;
872
+ return bl.style === sig.style && bl.size === sig.size && bl.space === sig.space && bl.color === sig.color;
873
+ }
874
+ /**
875
+ * Rebuild a blockquote node from a run of signature-carrying paragraphs,
876
+ * stripping the indent/border signature so child paragraphs render clean.
877
+ */
878
+ buildBlockquote(group) {
879
+ const content = [];
880
+ for (const para of group) {
881
+ const node = this.resolveParagraph(para);
882
+ const attrs = node.attrs;
883
+ if (attrs) {
884
+ if (attrs.indent) {
885
+ const indent = { ...attrs.indent };
886
+ delete indent.left;
887
+ attrs.indent = Object.keys(indent).length > 0 ? indent : void 0;
888
+ }
889
+ if (attrs.border) {
890
+ const border = { ...attrs.border };
891
+ delete border.left;
892
+ attrs.border = Object.keys(border).length > 0 ? border : void 0;
893
+ }
894
+ const cleaned = cleanAttrs(attrs);
895
+ if (Object.keys(cleaned).length > 0) node.attrs = cleaned;
896
+ else delete node.attrs;
897
+ }
898
+ content.push(node);
899
+ }
900
+ return {
901
+ type: "blockquote",
902
+ content
903
+ };
904
+ }
905
+ /**
906
+ * Resolve a list-item paragraph to a Tiptap paragraph/heading node, stripping
907
+ * the list marker (bullet/numbering) and the leading task checkbox — those
908
+ * are expressed at the list/item level, not inside the paragraph.
909
+ */
910
+ resolveListItemParagraph(para, info) {
911
+ const resolved = typeof para === "string" ? { text: para } : para;
912
+ const headingLevel = resolved.heading ? HEADING_LEVEL_MAP[resolved.heading] : void 0;
913
+ const nodeType = headingLevel ? "heading" : "paragraph";
914
+ const attrs = headingLevel ? parseDocx(resolved) : parseDocx$2(resolved);
915
+ const stripped = info.kind === "task" ? this.stripTaskCheckbox(resolved) : resolved;
916
+ const content = this.resolveInlineContent(stripped);
917
+ const node = { type: nodeType };
918
+ const cleanAttrsObj = cleanAttrs(attrs);
919
+ if (Object.keys(cleanAttrsObj).length > 0) node.attrs = cleanAttrsObj;
920
+ if (content.length > 0) node.content = content;
921
+ return node;
922
+ }
923
+ /** Return a copy of `para` with its leading docen-task checkbox SDT removed. */
924
+ stripTaskCheckbox(para) {
925
+ const children = para.children;
926
+ if (Array.isArray(children) && children.length > 0 && isTaskCheckbox(children[0])) return {
927
+ ...para,
928
+ children: children.slice(1)
929
+ };
930
+ return para;
931
+ }
932
+ resolveCodeBlock(opts) {
933
+ const children = opts.children;
934
+ const content = [];
935
+ if (children) {
936
+ for (const child of children) if (typeof child === "string") {
937
+ if (child) content.push({
938
+ type: "text",
939
+ text: child
940
+ });
941
+ } else if (typeof child === "object" && child !== null) {
942
+ if ("break" in child) {
943
+ const prev = content[content.length - 1];
944
+ if (prev && prev.type === "text") prev.text = (prev.text ?? "") + "\n";
945
+ else content.push({
946
+ type: "text",
947
+ text: "\n"
948
+ });
949
+ } else if ("text" in child) {
950
+ const marks = this.resolveMarks(child);
951
+ const textNode = {
952
+ type: "text",
953
+ text: child.text
954
+ };
955
+ if (marks) textNode.marks = marks;
956
+ content.push(textNode);
957
+ }
958
+ }
959
+ } else if (opts.text) content.push({
960
+ type: "text",
961
+ text: opts.text
962
+ });
963
+ const node = { type: "codeBlock" };
964
+ if (content.length > 0) node.content = content;
965
+ return node;
966
+ }
383
967
  resolveTable(tableOpts) {
384
968
  const attrs = parseDocx$4(tableOpts);
385
969
  const rows = tableOpts.rows ?? [];
386
970
  const content = [];
971
+ const tableCellMargins = tableOpts.cellMargin ?? tableOpts.margins ?? null;
387
972
  let activeSpans = /* @__PURE__ */ new Map();
388
973
  for (const row of rows) {
389
974
  const rowAttrs = parseDocx$7(row);
@@ -407,6 +992,7 @@ var DocxManager = class {
407
992
  const isHeader = row.tableHeader;
408
993
  const cellAttrs = isHeader ? parseDocx$6(cell) : parseDocx$5(cell);
409
994
  delete cellAttrs.verticalMerge;
995
+ if (!cellAttrs.margins && tableCellMargins) cellAttrs.margins = tableCellMargins;
410
996
  const cellChildren = cell.children ?? [];
411
997
  const cellContent = [];
412
998
  for (const cc of cellChildren) {
@@ -416,10 +1002,27 @@ var DocxManager = class {
416
1002
  const cellNode = { type: isHeader ? "tableHeader" : "tableCell" };
417
1003
  if (Object.keys(cellAttrs).length > 0) cellNode.attrs = cleanAttrs(cellAttrs);
418
1004
  if (cellContent.length > 0) cellNode.content = cellContent;
1005
+ else cellNode.content = [{ type: "paragraph" }];
419
1006
  if (vMerge === "restart") for (let c = colIdx; c < colIdx + cellColspan; c++) nextActiveSpans.set(c, cellNode);
420
1007
  cellNodes.push(cellNode);
421
1008
  colIdx += cellColspan;
422
1009
  }
1010
+ const gridAfter = row.gridAfter ?? 0;
1011
+ if (gridAfter > 0) {
1012
+ const trailingType = row.tableHeader ? "tableHeader" : "tableCell";
1013
+ const nilBorders = {
1014
+ top: { style: "nil" },
1015
+ right: { style: "nil" },
1016
+ bottom: { style: "nil" },
1017
+ left: { style: "nil" }
1018
+ };
1019
+ for (let c = 0; c < gridAfter; c++) cellNodes.push({
1020
+ type: trailingType,
1021
+ attrs: { borders: nilBorders },
1022
+ content: [{ type: "paragraph" }]
1023
+ });
1024
+ colIdx += gridAfter;
1025
+ }
423
1026
  activeSpans = nextActiveSpans;
424
1027
  const rowNode = { type: "tableRow" };
425
1028
  if (Object.keys(rowAttrs).length > 0) rowNode.attrs = cleanAttrs(rowAttrs);
@@ -471,15 +1074,36 @@ var DocxManager = class {
471
1074
  resolveParagraphChild(child) {
472
1075
  if ("text" in child || "children" in child || "break" in child) return this.resolveRun(child);
473
1076
  if ("image" in child) return this.resolveImage(child.image);
474
- if ("hyperlink" in child) return this.resolveHyperlink(child.hyperlink);
475
- if ("pageBreak" in child) return {
476
- type: "paragraph",
477
- attrs: { pageBreak: true }
1077
+ if ("wpgGroup" in child) return {
1078
+ type: "wpgGroup",
1079
+ attrs: { wpgGroup: child.wpgGroup }
478
1080
  };
479
- if ("columnBreak" in child) return { type: "hardBreak" };
1081
+ if ("wpsShape" in child) return {
1082
+ type: "wpsShape",
1083
+ attrs: { wpsShape: child.wpsShape }
1084
+ };
1085
+ if ("sdt" in child) return this.resolveInlineSdt(child);
1086
+ if ("hyperlink" in child) return this.resolveHyperlink(child.hyperlink);
1087
+ if ("pageBreak" in child) return { type: "pageBreak" };
1088
+ if ("columnBreak" in child) return { type: "columnBreak" };
1089
+ return null;
1090
+ }
1091
+ /** Resolve an inline SDT (mention carrier; other inline SDTs unsupported). */
1092
+ resolveInlineSdt(child) {
1093
+ if (isMention(child)) {
1094
+ const { id, label } = readMention(child);
1095
+ return {
1096
+ type: "mention",
1097
+ attrs: {
1098
+ id,
1099
+ label
1100
+ }
1101
+ };
1102
+ }
480
1103
  return null;
481
1104
  }
482
1105
  resolveRun(opts) {
1106
+ if (opts.break && opts.text === void 0 && !opts.children) return { type: "hardBreak" };
483
1107
  const text = opts.text;
484
1108
  if (text === void 0 && !opts.children) return null;
485
1109
  if (opts.children && !text) {
@@ -512,10 +1136,16 @@ var DocxManager = class {
512
1136
  }
513
1137
  if (opts.subScript) marks.push({ type: "subscript" });
514
1138
  if (opts.superScript) marks.push({ type: "superscript" });
515
- if (opts.highlight) marks.push({ type: "highlight" });
516
- if (opts.font === "Consolas" || opts.font === "monospace") marks.push({ type: "code" });
1139
+ if (opts.highlight) marks.push({
1140
+ type: "highlight",
1141
+ attrs: { color: opts.highlight }
1142
+ });
1143
+ if (opts.style === "CodeChar") marks.push({ type: "code" });
517
1144
  const textStyleAttrs = parseDocx$8(opts);
518
- if (opts.font === "Consolas" || opts.font === "monospace") delete textStyleAttrs.font;
1145
+ if (opts.style === "CodeChar") {
1146
+ delete textStyleAttrs.font;
1147
+ delete textStyleAttrs.styleId;
1148
+ }
519
1149
  if (Object.keys(textStyleAttrs).length > 0) marks.push({
520
1150
  type: "textStyle",
521
1151
  attrs: textStyleAttrs
@@ -526,7 +1156,7 @@ var DocxManager = class {
526
1156
  const attrs = parseDocx$1(imageOpts);
527
1157
  const data = imageOpts.data;
528
1158
  const type = imageOpts.type;
529
- if (data && type) attrs.src = `data:image/${type};base64,${typeof btoa !== "undefined" ? btoa(String.fromCharCode(...data instanceof ArrayBuffer ? new Uint8Array(data) : data)) : Buffer.from(data).toString("base64")}`;
1159
+ if (data && type) attrs.src = `data:image/${type};base64,${encodeBase64(data instanceof ArrayBuffer ? new Uint8Array(data) : data)}`;
530
1160
  return {
531
1161
  type: "image",
532
1162
  attrs
@@ -573,6 +1203,25 @@ function parseDOCX(data) {
573
1203
  return defaultManager.resolve(parseDocument(data));
574
1204
  }
575
1205
  /**
1206
+ * Merge {@link DocxGenerateOptions.document} into the `DocumentOptions`
1207
+ * compiled from Tiptap JSON.
1208
+ *
1209
+ * - `sections`/`numbering`: compile-owned (excluded from `document`, never
1210
+ * overridden).
1211
+ * - `styles`/`externalStyles`: option wins over `json.attrs.styles`; the two
1212
+ * are mutually exclusive, so `externalStyles` clears compiled `styles`.
1213
+ * - Everything else (core properties, background, features, …): injected.
1214
+ */
1215
+ function applyDocumentOptions(base, document) {
1216
+ if (!document) return base;
1217
+ const merged = {
1218
+ ...base,
1219
+ ...document
1220
+ };
1221
+ if (document.externalStyles !== void 0) delete merged.styles;
1222
+ return merged;
1223
+ }
1224
+ /**
576
1225
  * Generate a DOCX file from Tiptap JSON (runtime model), asynchronously.
577
1226
  *
578
1227
  * Pipeline: `prepareDocument` (default: fetch http images, in place) →
@@ -582,19 +1231,20 @@ function parseDOCX(data) {
582
1231
  * in place (http image URLs become embedded data URLs).
583
1232
  */
584
1233
  async function generateDOCX(json, options) {
585
- const { prepare = true, packer } = options ?? {};
1234
+ const { prepare = true, packer, document } = options ?? {};
586
1235
  if (prepare !== false) await prepareDocument(json, prepare === true ? void 0 : prepare);
587
- return generateDocument(compileDocument(json), packer);
1236
+ return generateDocument(applyDocumentOptions(compileDocument(json), document), packer);
588
1237
  }
589
1238
  /**
590
1239
  * Generate a DOCX file synchronously — fastest throughput, blocks the event loop.
591
1240
  *
592
1241
  * Pipeline: `DocxManager.compile` → `generateDocumentSync`. Does **not** run
593
1242
  * `prepareDocument` (it is async); call `await prepareDocument(json)` first
594
- * when http images need embedding.
1243
+ * when http images need embedding. `options.document` is still applied.
595
1244
  */
596
- function generateDOCXSync(json, packerOptions) {
597
- return generateDocumentSync(compileDocument(json), packerOptions);
1245
+ function generateDOCXSync(json, options) {
1246
+ const { packer, document } = options ?? {};
1247
+ return generateDocumentSync(applyDocumentOptions(compileDocument(json), document), packer);
598
1248
  }
599
1249
  /**
600
1250
  * Generate a DOCX file as a `ReadableStream<Uint8Array>` — for large documents
@@ -604,9 +1254,9 @@ function generateDOCXSync(json, packerOptions) {
604
1254
  * `DocxManager.compile` → `generateDocumentStream`. Async due to preparation.
605
1255
  */
606
1256
  async function generateDOCXStream(json, options) {
607
- const { prepare = true, packer } = options ?? {};
1257
+ const { prepare = true, packer, document } = options ?? {};
608
1258
  if (prepare !== false) await prepareDocument(json, prepare === true ? void 0 : prepare);
609
- return generateDocumentStream(compileDocument(json), packer);
1259
+ return generateDocumentStream(applyDocumentOptions(compileDocument(json), document), packer);
610
1260
  }
611
1261
  /**
612
1262
  * Convert DocumentOptions (persistence model) to Tiptap JSON (runtime model).