@docen/docx 0.3.5 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/README.md +1 -1
  2. package/dist/{blockquote-DY80QC06.d.mts → blockquote-D-1aSxEn.d.mts} +5 -1
  3. package/dist/converters/docx.d.mts +40 -66
  4. package/dist/converters/docx.mjs +248 -665
  5. package/dist/converters/html.d.mts +1 -1
  6. package/dist/converters/html.mjs +3 -4
  7. package/dist/converters/markdown.d.mts +1 -1
  8. package/dist/converters/markdown.mjs +1 -1
  9. package/dist/converters/patch.d.mts +1 -1
  10. package/dist/converters/prepare.d.mts +1 -1
  11. package/dist/converters/styles.d.mts +58 -14
  12. package/dist/converters/styles.mjs +124 -16
  13. package/dist/{core-DC0_-WcE.d.mts → core-BqyLL84S.d.mts} +34 -13
  14. package/dist/{core-BnF8XhVE.mjs → core-wNNPJiKr.mjs} +357 -25
  15. package/dist/core.d.mts +1 -1
  16. package/dist/core.mjs +1 -1
  17. package/dist/details-DsJhDP5K.d.mts +31 -0
  18. package/dist/editor.d.mts +1 -1
  19. package/dist/editor.mjs +1 -1
  20. package/dist/extensions/blockquote.d.mts +2 -3
  21. package/dist/extensions/blockquote.mjs +50 -2
  22. package/dist/extensions/bullet-list.mjs +1 -1
  23. package/dist/extensions/code-block.d.mts +2 -2
  24. package/dist/extensions/code-block.mjs +51 -5
  25. package/dist/extensions/column-break.d.mts +2 -2
  26. package/dist/extensions/column-break.mjs +2 -2
  27. package/dist/extensions/details.d.mts +2 -3
  28. package/dist/extensions/details.mjs +48 -2
  29. package/dist/extensions/document.mjs +1 -1
  30. package/dist/extensions/extensions.d.mts +13 -9
  31. package/dist/extensions/extensions.mjs +7 -3
  32. package/dist/extensions/formatting-marks.d.mts +1 -1
  33. package/dist/extensions/formatting-marks.mjs +1 -1
  34. package/dist/extensions/heading.d.mts +2 -2
  35. package/dist/extensions/heading.mjs +61 -5
  36. package/dist/extensions/image.d.mts +2 -2
  37. package/dist/extensions/image.mjs +23 -4
  38. package/dist/extensions/index.d.mts +14 -10
  39. package/dist/extensions/index.mjs +8 -4
  40. package/dist/extensions/link.d.mts +2 -2
  41. package/dist/extensions/link.mjs +30 -2
  42. package/dist/extensions/list-aggregator.d.mts +8 -0
  43. package/dist/extensions/list-aggregator.mjs +2 -0
  44. package/dist/extensions/marks.d.mts +2 -0
  45. package/dist/extensions/marks.mjs +41 -0
  46. package/dist/extensions/mention.d.mts +2 -3
  47. package/dist/extensions/mention.mjs +16 -2
  48. package/dist/extensions/ordered-list.mjs +1 -1
  49. package/dist/extensions/page-break.d.mts +2 -2
  50. package/dist/extensions/page-break.mjs +2 -2
  51. package/dist/extensions/paragraph.mjs +3 -3
  52. package/dist/extensions/passthrough.d.mts +1 -1
  53. package/dist/extensions/passthrough.mjs +1 -1
  54. package/dist/extensions/scroll.d.mts +1 -1
  55. package/dist/extensions/scroll.mjs +30 -5
  56. package/dist/extensions/section-break.d.mts +1 -1
  57. package/dist/extensions/section-break.mjs +1 -1
  58. package/dist/extensions/strike.d.mts +2 -2
  59. package/dist/extensions/strike.mjs +5 -24
  60. package/dist/extensions/tab.d.mts +2 -2
  61. package/dist/extensions/tab.mjs +2 -2
  62. package/dist/extensions/table-cell.mjs +2 -2
  63. package/dist/extensions/table-header.mjs +2 -2
  64. package/dist/extensions/table-row.mjs +2 -2
  65. package/dist/extensions/table.d.mts +2 -2
  66. package/dist/extensions/table.mjs +122 -11
  67. package/dist/extensions/task-item.d.mts +2 -3
  68. package/dist/extensions/task-item.mjs +2 -2
  69. package/dist/extensions/text-style.d.mts +2 -2
  70. package/dist/extensions/text-style.mjs +27 -28
  71. package/dist/extensions/toc-field.d.mts +2 -2
  72. package/dist/extensions/toc-field.mjs +2 -2
  73. package/dist/extensions/track-change.d.mts +2 -0
  74. package/dist/extensions/track-change.mjs +2 -0
  75. package/dist/extensions/types.d.mts +127 -8
  76. package/dist/extensions/utils.d.mts +2 -2
  77. package/dist/extensions/utils.mjs +74 -1
  78. package/dist/extensions/wpg-group.d.mts +2 -2
  79. package/dist/extensions/wpg-group.mjs +2 -2
  80. package/dist/extensions/wps-shape.d.mts +2 -2
  81. package/dist/extensions/wps-shape.mjs +2 -2
  82. package/dist/heading-Bwpa8iZY.d.mts +24 -0
  83. package/dist/index.d.mts +29 -11
  84. package/dist/index.mjs +9 -5
  85. package/dist/{link-BawPjQZR.d.mts → link-gUqW45mE.d.mts} +3 -1
  86. package/dist/marks-Dz9Vb22Q.d.mts +10 -0
  87. package/dist/{mention-BGLzLVYw.d.mts → mention-CkONDrw9.d.mts} +5 -1
  88. package/dist/{scroll-ZNeThJsJ.d.mts → scroll-BARiZ5Gm.d.mts} +9 -3
  89. package/dist/strike-Brn9sWFy.d.mts +16 -0
  90. package/dist/table-CdcjR6HD.d.mts +18 -0
  91. package/dist/{task-item-B0ntvQ1Y.d.mts → task-item-CCAC4QLi.d.mts} +3 -1
  92. package/dist/text-style-BzfcbufI.d.mts +4 -0
  93. package/dist/{utils-BJwDQts7.d.mts → utils-CfwwOowz.d.mts} +25 -1
  94. package/package.json +1 -1
  95. package/dist/details-Dd5MqqmR.d.mts +0 -17
  96. package/dist/extensions/tiptap.d.mts +0 -2
  97. package/dist/extensions/tiptap.mjs +0 -31
  98. package/dist/heading-BvqBD2zX.d.mts +0 -8
  99. package/dist/strike-BgWGvjKr.d.mts +0 -33
  100. package/dist/table-BFkfeRp9.d.mts +0 -9
  101. package/dist/text-style-BHdtXkMb.d.mts +0 -8
  102. package/dist/tiptap-BKqn41uT.d.mts +0 -31
@@ -1,14 +1,16 @@
1
- import { Blockquote, Bold, Code, CodeBlockLowlight, Details, DetailsContent, DetailsSummary, Emoji, HardBreak, Highlight, HorizontalRule, Italic, ListItem, Mathematics, Mention, Subscript, Superscript, TaskItem, TaskList, Text, TextAlign, Underline } from "./extensions/tiptap.mjs";
2
- import "./extensions/blockquote.mjs";
1
+ import { attrNative, floatAnchorScope, floatingToStyles, normalizeColorToHex, renderRunStyles } from "./extensions/utils.mjs";
2
+ import { buildTextBlock, cleanAttrs, mergeTextNodes } from "./converters/styles.mjs";
3
+ import { Blockquote } from "./extensions/blockquote.mjs";
3
4
  import { BulletList } from "./extensions/bullet-list.mjs";
4
5
  import { CodeBlock } from "./extensions/code-block.mjs";
5
- import "./extensions/details.mjs";
6
- import { attrNative, floatAnchorScope, floatingToStyles, normalizeColorToHex, renderRunStyles } from "./extensions/utils.mjs";
6
+ import { Details, DetailsContent as DetailsContentBase, DetailsSummary as DetailsSummaryBase } from "./extensions/details.mjs";
7
7
  import { Document } from "./extensions/document.mjs";
8
- import { Heading } from "./extensions/heading.mjs";
8
+ import { Heading, detectHeadingLevel } from "./extensions/heading.mjs";
9
9
  import { Image } from "./extensions/image.mjs";
10
10
  import { Link } from "./extensions/link.mjs";
11
- import "./extensions/mention.mjs";
11
+ import { TaskItem as TaskItemBase, isTaskCheckbox, readCheckboxState } from "./extensions/task-item.mjs";
12
+ import { Bold, Code, Highlight, Italic, Subscript, Superscript, Underline } from "./extensions/marks.mjs";
13
+ import { Mention } from "./extensions/mention.mjs";
12
14
  import { OrderedList } from "./extensions/ordered-list.mjs";
13
15
  import { Paragraph } from "./extensions/paragraph.mjs";
14
16
  import { Strike } from "./extensions/strike.mjs";
@@ -16,9 +18,17 @@ import { Table } from "./extensions/table.mjs";
16
18
  import { TableCell } from "./extensions/table-cell.mjs";
17
19
  import { TableHeader } from "./extensions/table-header.mjs";
18
20
  import { TableRow } from "./extensions/table-row.mjs";
19
- import "./extensions/task-item.mjs";
20
21
  import { TextStyle } from "./extensions/text-style.mjs";
21
22
  import { Editor, Extension, Mark, Node } from "@tiptap/core";
23
+ import { CodeBlockLowlight, CodeBlockLowlight as CodeBlockLowlight$1 } from "@tiptap/extension-code-block-lowlight";
24
+ import { Emoji, Emoji as Emoji$1 } from "@tiptap/extension-emoji";
25
+ import { HardBreak, HardBreak as HardBreak$1 } from "@tiptap/extension-hard-break";
26
+ import { HorizontalRule, HorizontalRule as HorizontalRule$1 } from "@tiptap/extension-horizontal-rule";
27
+ import { ListItem, ListItem as ListItem$1 } from "@tiptap/extension-list-item";
28
+ import { Mathematics, Mathematics as Mathematics$1 } from "@tiptap/extension-mathematics";
29
+ import { TaskList, TaskList as TaskList$1 } from "@tiptap/extension-task-list";
30
+ import { Text, Text as Text$1 } from "@tiptap/extension-text";
31
+ import { TextAlign, TextAlign as TextAlign$1 } from "@tiptap/extension-text-align";
22
32
  import { all, createLowlight } from "lowlight";
23
33
  import { Plugin, PluginKey, TextSelection } from "@tiptap/pm/state";
24
34
  import { Decoration, DecorationSet } from "@tiptap/pm/view";
@@ -33,13 +43,14 @@ import { convertEmuToPixels, encodeBase64 } from "@office-open/core";
33
43
  * layout (paged.js multi-column) is a future concern; the node preserves the
34
44
  * break losslessly regardless.
35
45
  *
36
- * The DOCX payload (`{ columnBreak: true }`) is inlined in DocxManager — a
37
- * one-liner with no per-node variance, so no extension helper for it.
38
- *
39
46
  * `setColumnBreak` only inserts the atom (no paragraph split): a column break
40
47
  * does not start a new page, and there is no column layout to reflow yet, so
41
48
  * the node is purely for round-trip fidelity until multi-column lands.
42
49
  */
50
+ const parseDocxInline$4 = {
51
+ match: (child) => "columnBreak" in child,
52
+ convert: () => ({ type: "columnBreak" })
53
+ };
43
54
  const ColumnBreak = Node.create({
44
55
  name: "columnBreak",
45
56
  inline: true,
@@ -54,6 +65,7 @@ const ColumnBreak = Node.create({
54
65
  style: "break-after:column"
55
66
  }];
56
67
  },
68
+ parseDocxInline: parseDocxInline$4,
57
69
  addCommands() {
58
70
  return { setColumnBreak: () => ({ commands }) => commands.insertContent({ type: "columnBreak" }) };
59
71
  }
@@ -146,6 +158,119 @@ const FormattingMarks = Extension.create({
146
158
  }
147
159
  });
148
160
  //#endregion
161
+ //#region src/extensions/list-aggregator.ts
162
+ /** Classify a paragraph as a list item, or null if it isn't one. */
163
+ function detectList(para, ctx) {
164
+ const p = para;
165
+ const numbering = p.numbering;
166
+ const bullet = p.bullet;
167
+ let kind;
168
+ let level;
169
+ let reference;
170
+ let start;
171
+ if (numbering) {
172
+ reference = numbering.reference;
173
+ level = numbering.level ?? 0;
174
+ const cfg = reference ? ctx.numberingLookup?.get(reference) : void 0;
175
+ if (cfg && cfg.format && cfg.format !== "bullet") {
176
+ kind = "ordered";
177
+ start = cfg.start;
178
+ } else kind = "bullet";
179
+ } else if (bullet) {
180
+ kind = "bullet";
181
+ level = bullet.level ?? 0;
182
+ } else return null;
183
+ const first = p.children?.[0];
184
+ return {
185
+ kind: isTaskCheckbox(first) ? "task" : kind,
186
+ level,
187
+ reference,
188
+ start,
189
+ checked: readCheckboxState(first)
190
+ };
191
+ }
192
+ /** Resolve a list-item paragraph to a Tiptap paragraph/heading node, stripping
193
+ * the list marker (bullet/numbering) and the leading task checkbox — those are
194
+ * expressed at the list/item level, not inside the paragraph. */
195
+ function resolveListItemParagraph(para, info, ctx) {
196
+ const resolved = typeof para === "string" ? { text: para } : para;
197
+ const headingLevel = detectHeadingLevel(resolved, ctx.styles);
198
+ return buildTextBlock(headingLevel ? "heading" : "paragraph", resolved, ctx, headingLevel, info.kind === "task" ? stripTaskCheckbox(resolved) : resolved);
199
+ }
200
+ /** Return a copy of `para` with its leading docen-task checkbox SDT removed. */
201
+ function stripTaskCheckbox(para) {
202
+ const children = para.children;
203
+ if (Array.isArray(children) && children.length > 0 && isTaskCheckbox(children[0])) return {
204
+ ...para,
205
+ children: children.slice(1)
206
+ };
207
+ return para;
208
+ }
209
+ /**
210
+ * Rebuild nested Tiptap lists from a flat run of list paragraphs. Stack-based:
211
+ * each frame is an active list at a given depth; the `key` (level:type:
212
+ * reference) decides whether a paragraph continues the top list, starts a nested
213
+ * list, or splits off a new sibling list.
214
+ */
215
+ function buildListTree(group, ctx) {
216
+ const topLevel = [];
217
+ const stack = [];
218
+ for (const { para, info } of group) {
219
+ const listType = info.kind === "ordered" ? "orderedList" : info.kind === "task" ? "taskList" : "bulletList";
220
+ const itemType = info.kind === "task" ? "taskItem" : "listItem";
221
+ const key = `${info.level}:${listType}:${info.reference ?? ""}`;
222
+ while (stack.length > 0) {
223
+ const top = stack[stack.length - 1];
224
+ if (top.level > info.level || top.level === info.level && top.key !== key) {
225
+ stack.pop();
226
+ continue;
227
+ }
228
+ break;
229
+ }
230
+ const newItem = {
231
+ type: itemType,
232
+ content: [resolveListItemParagraph(para, info, ctx)]
233
+ };
234
+ if (itemType === "taskItem") newItem.attrs = { checked: info.checked };
235
+ const top = stack[stack.length - 1];
236
+ if (top && top.level === info.level && top.key === key) {
237
+ top.listNode.content.push(newItem);
238
+ top.currentItem = newItem;
239
+ } else {
240
+ const newList = {
241
+ type: listType,
242
+ content: [newItem]
243
+ };
244
+ const listAttrs = {};
245
+ if (listType === "orderedList" && info.level === 0 && typeof info.start === "number" && info.start !== 1) listAttrs.start = info.start;
246
+ if (info.reference) listAttrs.numbering = info.reference;
247
+ if (Object.keys(listAttrs).length > 0) newList.attrs = listAttrs;
248
+ if (top) top.currentItem.content.push(newList);
249
+ else topLevel.push(newList);
250
+ stack.push({
251
+ level: info.level,
252
+ key,
253
+ listNode: newList,
254
+ currentItem: newItem
255
+ });
256
+ }
257
+ }
258
+ return topLevel;
259
+ }
260
+ const parseDocxAggregator = {
261
+ belongs: (para, ctx) => detectList(para, ctx) != null,
262
+ build: (group, ctx) => {
263
+ return buildListTree(group.map((para) => ({
264
+ para,
265
+ info: detectList(para, ctx)
266
+ })).filter((x) => x.info != null), ctx);
267
+ }
268
+ };
269
+ const ListAggregator = Extension.create({
270
+ name: "listAggregator",
271
+ parseDocxAggregator
272
+ });
273
+ //#endregion
149
274
  //#region src/extensions/page-break.ts
150
275
  /**
151
276
  * PageBreak — inline atom node for DOCX page breaks (`<w:br w:type="page"/>`).
@@ -166,6 +291,10 @@ const FormattingMarks = Extension.create({
166
291
  * the paginator's `forcesPageBreakAfter` moves it to the next page (matching
167
292
  * Word's Ctrl+Enter behavior).
168
293
  */
294
+ const parseDocxInline$3 = {
295
+ match: (child) => "pageBreak" in child,
296
+ convert: () => ({ type: "pageBreak" })
297
+ };
169
298
  const PageBreak = Node.create({
170
299
  name: "pageBreak",
171
300
  inline: true,
@@ -180,6 +309,7 @@ const PageBreak = Node.create({
180
309
  style: "break-after:page"
181
310
  }];
182
311
  },
312
+ parseDocxInline: parseDocxInline$3,
183
313
  addProseMirrorPlugins() {
184
314
  return [new Plugin({ appendTransaction: (transactions, _oldState, newState) => {
185
315
  if (!transactions.some((tr) => tr.docChanged)) return null;
@@ -396,6 +526,10 @@ const SectionBreak = Extension.create({
396
526
  * marks where a tab leader (e.g. a TOC's dotted leader) renders. compile turns it
397
527
  * back into `{ tab: true }`.
398
528
  */
529
+ const parseDocxInline$2 = {
530
+ match: (child) => "tab" in child,
531
+ convert: () => ({ type: "tab" })
532
+ };
399
533
  const Tab = Node.create({
400
534
  name: "tab",
401
535
  group: "inline",
@@ -409,7 +543,8 @@ const Tab = Node.create({
409
543
  class: "docx-tab",
410
544
  contenteditable: "false"
411
545
  }];
412
- }
546
+ },
547
+ parseDocxInline: parseDocxInline$2
413
548
  });
414
549
  //#endregion
415
550
  //#region src/extensions/toc-field.ts
@@ -431,14 +566,44 @@ const Tab = Node.create({
431
566
  * whose content-less runs (fldChar begin/separate/end) office-open parses as
432
567
  * `null`. As opaque passthrough those nulls survived verbatim to
433
568
  * `generateDocument`, where office-open's `stringifyRunInline(null).break`
434
- * crashed. Resolving the entries through `resolveParagraphChildren` drops the
435
- * nulls (the existing `child !== null` guard), so compile rebuilds clean entries
436
- * and the generate path never sees a null — no office-open change required.
437
- *
438
- * DOCX serialization is inlined in DocxManager (resolve/compile read/write
439
- * `attrs.options` + the entry content directly), so no renderDocx/parseDocx is
440
- * needed here — the same pattern as the details extension.
569
+ * crashed. The `parseDocxBlock` rule resolves the entries through the shared
570
+ * block-stream path, which drops the nulls (the `child !== null` guard), so
571
+ * compile rebuilds clean entries and the generate path never sees a null — no
572
+ * office-open change required.
441
573
  */
574
+ /**
575
+ * Declarative block parse rule: recognize a table of contents SectionChild and
576
+ * rebuild it as an editable `tocField` container. DocxManager dispatches every
577
+ * SectionChild through this rule before the paragraph/passthrough fallbacks. */
578
+ const parseDocxBlock = {
579
+ match: (child) => "toc" in child,
580
+ convert: (child, ctx) => resolveToc(child.toc, ctx)
581
+ };
582
+ /** Resolve a table of contents into an editable `tocField` container:
583
+ * `attrs.options` carries the field switches, `content` is the entry
584
+ * paragraphs. Each entry's inner HYPERLINK field has content-less runs that
585
+ * office-open parses as `null`; resolving the entries through the shared
586
+ * block-stream path drops those nulls (the `stringifyRunInline(null).break`
587
+ * crash). When `entries` is absent/empty (a fresh, unrendered TOC), keep the
588
+ * node valid for `content: "block+"` with a placeholder empty paragraph. */
589
+ function resolveToc(toc, ctx) {
590
+ const { entries, ...options } = toc;
591
+ const content = [];
592
+ for (const entry of entries ?? []) {
593
+ const node = ctx.resolveBlock(entry);
594
+ if (!node) continue;
595
+ if (Array.isArray(node)) content.push(...node);
596
+ else content.push(node);
597
+ }
598
+ if (content.length === 0) content.push({ type: "paragraph" });
599
+ const node = {
600
+ type: "tocField",
601
+ content
602
+ };
603
+ const cleanOptions = cleanAttrs(options);
604
+ if (Object.keys(cleanOptions).length > 0) node.attrs = { options: cleanOptions };
605
+ return node;
606
+ }
442
607
  const TocField = Node.create({
443
608
  name: "tocField",
444
609
  group: "block",
@@ -455,6 +620,119 @@ const TocField = Node.create({
455
620
  { class: "docx-toc" },
456
621
  0
457
622
  ];
623
+ },
624
+ parseDocxBlock
625
+ });
626
+ //#endregion
627
+ //#region src/extensions/track-change.ts
628
+ /**
629
+ * Track Changes marks (Word revision tracking).
630
+ *
631
+ * OOXML records inline revisions as `<w:ins>` / `<w:del>` containers wrapping
632
+ * runs (carrying w:author / w:date / w:id metadata). office-open models these
633
+ * as `ParagraphChild.insertion` / `.deletion` — `{ id, author, date, children }`
634
+ * — structurally identical to `hyperlink` (an inline container with attrs +
635
+ * child runs). docen mirrors that as two Tiptap marks applied to the contained
636
+ * text:
637
+ *
638
+ * - `insertion` — text added by a reviewer (Word renders colored + underlined)
639
+ * - `deletion` — text marked for removal (Word renders colored + strikethrough;
640
+ * the text stays visible until the change is accepted/rejected)
641
+ *
642
+ * Container-level, NOT rPr-level: like `link`, these wrap child runs, so resolve
643
+ * is declared via parseDocxInline (resolveTrackedChange) and compile via
644
+ * compileTrackedChangeRun (compileTextRun pushes `{insertion|deletion:{...}}`).
645
+ * They do NOT use the renderDocx/parseDocx mark hook — that is for rPr-level
646
+ * marks like strike/bold. The attrs (id/author/date) are round-tripped via
647
+ * resolve/compile and kept out of HTML (`rendered:false`): HTML paste loses
648
+ * the metadata but keeps the native `<ins>`/`<del>` tag (the class is a CSS
649
+ * hook); DOCX round-trip is byte-faithful.
650
+ *
651
+ * HTML tags: `<ins>`/`<del>` are HTML's native editorial-revision elements, so
652
+ * they are used instead of a bare span — semantic, accessible, and matching
653
+ * browser defaults (underlined / struck-through). `<ins>` has no competing
654
+ * mark, so both the classed tag and a bare pasted `<ins>` are claimed. `<del>`
655
+ * is also matched by the base Strike mark, so only the classed tag is claimed
656
+ * to avoid shadowing strike on a bare `<del>`.
657
+ *
658
+ * P1 scope: render + round-trip only. accept/reject commands, nested
659
+ * revisions, block-level revisions, and format-revision (markChange) are out
660
+ * of scope (office-open parses inline w:ins/w:del only).
661
+ */
662
+ const trackChangeAttrs = () => ({
663
+ id: {
664
+ default: null,
665
+ rendered: false
666
+ },
667
+ author: {
668
+ default: null,
669
+ rendered: false
670
+ },
671
+ date: {
672
+ default: null,
673
+ rendered: false
674
+ }
675
+ });
676
+ /** ParagraphChild `{ insertion|deletion: {...} }` → text[] carrying the mark.
677
+ * Mirrors the old DocxManager.resolveTrackedChange: recurse the container's
678
+ * runs via ctx, merge adjacent text, then stamp every text node with the
679
+ * revision mark alongside any existing rPr marks. Returns null for an empty
680
+ * container. */
681
+ function resolveTrackedChange(opts, type, ctx) {
682
+ const content = ctx.resolveInlineChildren((opts.children ?? []).map((c) => c));
683
+ if (content.length === 0) return null;
684
+ const merged = mergeTextNodes(content);
685
+ const mark = {
686
+ type,
687
+ attrs: {
688
+ id: opts.id ?? null,
689
+ author: opts.author ?? null,
690
+ date: opts.date ?? null
691
+ }
692
+ };
693
+ for (const node of merged) if (node.type === "text") node.marks = [...node.marks ?? [], mark];
694
+ return merged;
695
+ }
696
+ const Insertion = Mark.create({
697
+ name: "insertion",
698
+ inclusive: false,
699
+ addAttributes() {
700
+ return trackChangeAttrs();
701
+ },
702
+ renderHTML() {
703
+ return [
704
+ "ins",
705
+ { class: "docen-insertion" },
706
+ 0
707
+ ];
708
+ },
709
+ parseHTML() {
710
+ return [{ tag: "ins.docen-insertion" }, { tag: "ins" }];
711
+ },
712
+ parseDocxInline: {
713
+ match: (child) => "insertion" in child,
714
+ convert: (child, ctx) => resolveTrackedChange(child.insertion, "insertion", ctx)
715
+ }
716
+ });
717
+ const Deletion = Mark.create({
718
+ name: "deletion",
719
+ inclusive: false,
720
+ addAttributes() {
721
+ return trackChangeAttrs();
722
+ },
723
+ renderHTML() {
724
+ return [
725
+ "del",
726
+ { class: "docen-deletion" },
727
+ 0
728
+ ];
729
+ },
730
+ parseHTML() {
731
+ return [{ tag: "del.docen-deletion" }];
732
+ },
733
+ parseDocxInline: {
734
+ match: (child) => "deletion" in child,
735
+ convert: (child, ctx) => resolveTrackedChange(child.deletion, "deletion", ctx)
458
736
  }
459
737
  });
460
738
  //#endregion
@@ -698,6 +976,13 @@ const attrWpgGroup = () => ({
698
976
  }
699
977
  }
700
978
  });
979
+ const parseDocxInline$1 = {
980
+ match: (child) => "wpgGroup" in child,
981
+ convert: (child) => ({
982
+ type: "wpgGroup",
983
+ attrs: { wpgGroup: child.wpgGroup }
984
+ })
985
+ };
701
986
  const WpgGroup = Node.create({
702
987
  name: "wpgGroup",
703
988
  group: "inline",
@@ -718,7 +1003,8 @@ const WpgGroup = Node.create({
718
1003
  `height:${h}px`
719
1004
  ].join(";"), floatAnchorScope(wpg.floating) === "paragraph" ? { "data-float-anchor": "paragraph" } : void 0);
720
1005
  return renderGroup(wpg, w, h);
721
- }
1006
+ },
1007
+ parseDocxInline: parseDocxInline$1
722
1008
  });
723
1009
  //#endregion
724
1010
  //#region src/extensions/wps-shape.ts
@@ -745,6 +1031,48 @@ const attrWpsShape = () => ({
745
1031
  }
746
1032
  }
747
1033
  });
1034
+ /** ParagraphChild `{ wpsShape: {...} }` → wpsShape node. Mirrors the old
1035
+ * DocxManager wpsShape branch: the shape's text body (children) becomes PM
1036
+ * content (one node per paragraph); geometry/styling ride on attrs.wpsShape.
1037
+ * Each paragraph's defRPr (para.run) is merged into its runs then dropped — it
1038
+ * is the box's default run-properties, not the ¶-mark rPr (see inline note). */
1039
+ function resolveWpsShape(ws, ctx) {
1040
+ const content = [];
1041
+ if (ws?.children) for (const para of ws.children) {
1042
+ if (typeof para !== "object" || para === null) {
1043
+ const node = ctx.resolveParagraph(para);
1044
+ if (node) content.push(node);
1045
+ continue;
1046
+ }
1047
+ const defRPr = para.run ?? {};
1048
+ const children = Array.isArray(para.children) ? para.children.map((c) => typeof c !== "object" || c === null ? {
1049
+ ...defRPr,
1050
+ text: c
1051
+ } : {
1052
+ ...defRPr,
1053
+ ...c
1054
+ }) : void 0;
1055
+ const node = ctx.resolveParagraph({
1056
+ ...para,
1057
+ run: void 0,
1058
+ ...children ? { children } : {}
1059
+ });
1060
+ if (node) content.push(node);
1061
+ }
1062
+ if (content.length === 0) content.push({ type: "paragraph" });
1063
+ const { children: _omit, ...geometry } = ws ?? {};
1064
+ const node = {
1065
+ type: "wpsShape",
1066
+ content
1067
+ };
1068
+ const cleanGeometry = cleanAttrs(geometry);
1069
+ if (Object.keys(cleanGeometry).length > 0) node.attrs = { wpsShape: cleanGeometry };
1070
+ return node;
1071
+ }
1072
+ const parseDocxInline = {
1073
+ match: (child) => "wpsShape" in child,
1074
+ convert: (child, ctx) => resolveWpsShape(child.wpsShape, ctx)
1075
+ };
748
1076
  const WpsShape = Node.create({
749
1077
  name: "wpsShape",
750
1078
  group: "inline",
@@ -778,7 +1106,8 @@ const WpsShape = Node.create({
778
1106
  0
779
1107
  ]
780
1108
  ];
781
- }
1109
+ },
1110
+ parseDocxInline
782
1111
  });
783
1112
  //#endregion
784
1113
  //#region src/extensions/extensions.ts
@@ -800,8 +1129,8 @@ const tiptapNodeExtensions = [
800
1129
  ListItem,
801
1130
  CodeBlock.configure({ lowlight: createLowlight(all) }),
802
1131
  Details,
803
- DetailsSummary,
804
- DetailsContent,
1132
+ DetailsSummaryBase,
1133
+ DetailsContentBase,
805
1134
  Emoji,
806
1135
  HorizontalRule,
807
1136
  Image.configure({ inline: true }),
@@ -814,14 +1143,16 @@ const tiptapNodeExtensions = [
814
1143
  TableCell,
815
1144
  TableHeader,
816
1145
  TaskList,
817
- TaskItem,
1146
+ TaskItemBase,
818
1147
  Heading,
819
1148
  TextAlign.configure({ types: ["heading", "paragraph"] })
820
1149
  ];
821
1150
  const tiptapMarkExtensions = [
822
1151
  Bold,
823
1152
  Code,
1153
+ Deletion,
824
1154
  Highlight,
1155
+ Insertion,
825
1156
  Italic,
826
1157
  Link,
827
1158
  Strike,
@@ -833,7 +1164,8 @@ const tiptapMarkExtensions = [
833
1164
  const docxExtensions = [
834
1165
  ...tiptapNodeExtensions,
835
1166
  ...tiptapMarkExtensions,
836
- FormattingMarks
1167
+ FormattingMarks,
1168
+ ListAggregator
837
1169
  ];
838
1170
  const DocxKit = Extension.create({
839
1171
  name: "docxKit",
@@ -863,4 +1195,4 @@ const DocxKit = Extension.create({
863
1195
  }
864
1196
  });
865
1197
  //#endregion
866
- export { ColumnBreak as C, FormattingMarks as S, Tab as _, DocxKit as a, Passthrough as b, tiptapNodeExtensions as c, renderWpsInterior as d, renderWpsText as f, TocField as g, wpsShapeStyles as h, Node as i, WpsShape as l, wpsRotationVert as m, Extension as n, docxExtensions as o, wpsInnerStyle as p, Mark as r, tiptapMarkExtensions as s, Editor as t, WpgGroup as u, SectionBreak as v, PageBreak as x, InlinePassthrough as y };
1198
+ export { parseDocxBlock as A, FormattingMarks as B, renderWpsText as C, Deletion as D, wpsShapeStyles as E, Passthrough as F, parseDocxInline$4 as H, PageBreak as I, parseDocxInline$3 as L, parseDocxInline$2 as M, SectionBreak as N, Insertion as O, InlinePassthrough as P, ListAggregator as R, renderWpsInterior as S, wpsRotationVert as T, ColumnBreak as V, tiptapNodeExtensions as _, CodeBlockLowlight$1 as a, WpgGroup as b, HardBreak$1 as c, Mathematics$1 as d, TaskList$1 as f, tiptapMarkExtensions as g, docxExtensions as h, Node as i, Tab as j, TocField as k, HorizontalRule$1 as l, TextAlign$1 as m, Extension as n, DocxKit as o, Text$1 as p, Mark as r, Emoji$1 as s, Editor as t, ListItem$1 as u, WpsShape as v, wpsInnerStyle as w, parseDocxInline$1 as x, parseDocxInline as y, parseDocxAggregator as z };
package/dist/core.d.mts CHANGED
@@ -1,2 +1,2 @@
1
- import { a as JSONContent, i as Extensions, n as Editor, o as Mark, r as Extension, s as Node, t as AnyExtension, u as docxExtensions } from "./core-DC0_-WcE.mjs";
1
+ import { a as JSONContent, i as Extensions, n as Editor, o as Mark, r as Extension, s as Node, t as AnyExtension, y as docxExtensions } from "./core-BqyLL84S.mjs";
2
2
  export { type AnyExtension, Editor, Extension, type Extensions, type JSONContent, Mark, Node, docxExtensions };
package/dist/core.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { i as Node, n as Extension, o as docxExtensions, r as Mark, t as Editor } from "./core-BnF8XhVE.mjs";
1
+ import { h as docxExtensions, i as Node, n as Extension, r as Mark, t as Editor } from "./core-wNNPJiKr.mjs";
2
2
  export { Editor, Extension, Mark, Node, docxExtensions };
@@ -0,0 +1,31 @@
1
+ import { ParseBlockRule } from "./extensions/types.mjs";
2
+ import { DetailsContent as DetailsContentBase, DetailsSummary as DetailsSummaryBase } from "@tiptap/extension-details";
3
+
4
+ //#region src/extensions/details.d.ts
5
+ /**
6
+ * Details extension — owns the DOCX expression of a collapsible details block.
7
+ *
8
+ * DOCX has no native collapsible region, but a block-level group-SDT is a
9
+ * reversible container. The details maps to one group-SDT tagged "docen-
10
+ * details"; the summary paragraph is marked with a fixed style so resolve can
11
+ * split it back out from the content paragraphs. Structure round-trips fully
12
+ * (summary + content); Word shows it expanded (no collapse) — an inherent
13
+ * DOCX limitation, not data loss.
14
+ *
15
+ * The `parseDocxBlock` rule (below) recognizes a details group-SDT during
16
+ * resolve and rebuilds the details/detailsSummary/detailsContent nodes; the
17
+ * extensions themselves carry no DOCX attrs of their own.
18
+ */
19
+ /** SDT tag marking a details group content control. */
20
+ declare const DETAILS_TAG = "docen-details";
21
+ /** Paragraph style marking the summary line within a details group-SDT. */
22
+ declare const DETAILS_SUMMARY_STYLE = "DocenDetailsSummary";
23
+ /**
24
+ * Declarative block parse rule: recognize a group-SDT tagged "docen-details"
25
+ * and rebuild it as a details node (summary + content). DocxManager dispatches
26
+ * every SectionChild through this rule before the paragraph/passthrough
27
+ * fallbacks; a non-details SDT falls through to passthrough. */
28
+ declare const parseDocxBlock: ParseBlockRule;
29
+ declare const Details$1: import("@tiptap/core").Node<import("@tiptap/extension-details").DetailsOptions, any>;
30
+ //#endregion
31
+ export { DetailsSummaryBase as a, DetailsContentBase as i, DETAILS_TAG as n, parseDocxBlock as o, Details$1 as r, DETAILS_SUMMARY_STYLE as t };
package/dist/editor.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { n as Editor, t as AnyExtension } from "./core-DC0_-WcE.mjs";
1
+ import { n as Editor, t as AnyExtension } from "./core-BqyLL84S.mjs";
2
2
 
3
3
  //#region src/editor.d.ts
4
4
  interface DocxEditorOptions {
package/dist/editor.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { o as docxExtensions, t as Editor } from "./core-BnF8XhVE.mjs";
1
+ import { h as docxExtensions, t as Editor } from "./core-wNNPJiKr.mjs";
2
2
  //#region src/editor.ts
3
3
  /**
4
4
  * Create a Tiptap editor configured for DOCX editing.
@@ -1,3 +1,2 @@
1
- import { t as Blockquote } from "../tiptap-BKqn41uT.mjs";
2
- import { n as BLOCKQUOTE_INDENT_LEFT, r as applyBlockquoteStyle, t as BLOCKQUOTE_BORDER } from "../blockquote-DY80QC06.mjs";
3
- export { BLOCKQUOTE_BORDER, BLOCKQUOTE_INDENT_LEFT, Blockquote, applyBlockquoteStyle };
1
+ import { a as parseDocxAggregator, i as applyBlockquoteStyle, n as BLOCKQUOTE_INDENT_LEFT, r as Blockquote, t as BLOCKQUOTE_BORDER } from "../blockquote-D-1aSxEn.mjs";
2
+ export { BLOCKQUOTE_BORDER, BLOCKQUOTE_INDENT_LEFT, Blockquote, applyBlockquoteStyle, parseDocxAggregator };
@@ -1,4 +1,5 @@
1
- import { Blockquote } from "./tiptap.mjs";
1
+ import { cleanAttrs } from "../converters/styles.mjs";
2
+ import { Blockquote as Blockquote$1 } from "@tiptap/extension-blockquote";
2
3
  //#region src/extensions/blockquote.ts
3
4
  /**
4
5
  * Blockquote extension — owns the DOCX expression of a blockquote.
@@ -27,5 +28,52 @@ function applyBlockquoteStyle(paraObj) {
27
28
  left: BLOCKQUOTE_BORDER
28
29
  };
29
30
  }
31
+ /** Classify a paragraph as a blockquote member by its signature (left indent
32
+ * + left border). compile stamps this via applyBlockquoteStyle; this is the
33
+ * reverse predicate. Pure: reads only the paragraph opts. */
34
+ function detectBlockquote(para) {
35
+ const p = para;
36
+ const indent = p.indent;
37
+ const border = p.border;
38
+ if (!indent || indent.left !== 720) return false;
39
+ const bl = border?.left;
40
+ if (!bl) return false;
41
+ const sig = BLOCKQUOTE_BORDER;
42
+ return bl.style === sig.style && bl.size === sig.size && bl.space === sig.space && bl.color === sig.color;
43
+ }
44
+ /** Rebuild a blockquote node from a run of signature-carrying paragraphs,
45
+ * stripping the indent/border signature so child paragraphs render clean. */
46
+ function buildBlockquote(group, ctx) {
47
+ const content = [];
48
+ for (const para of group) {
49
+ const node = ctx.resolveParagraph(para);
50
+ const attrs = node.attrs;
51
+ if (attrs) {
52
+ if (attrs.indent) {
53
+ const indent = { ...attrs.indent };
54
+ delete indent.left;
55
+ attrs.indent = Object.keys(indent).length > 0 ? indent : void 0;
56
+ }
57
+ if (attrs.border) {
58
+ const border = { ...attrs.border };
59
+ delete border.left;
60
+ attrs.border = Object.keys(border).length > 0 ? border : void 0;
61
+ }
62
+ const cleaned = cleanAttrs(attrs);
63
+ if (Object.keys(cleaned).length > 0) node.attrs = cleaned;
64
+ else delete node.attrs;
65
+ }
66
+ content.push(node);
67
+ }
68
+ return [{
69
+ type: "blockquote",
70
+ content
71
+ }];
72
+ }
73
+ const parseDocxAggregator = {
74
+ belongs: (para) => detectBlockquote(para),
75
+ build: (group, ctx) => buildBlockquote(group, ctx)
76
+ };
77
+ const Blockquote = Blockquote$1.extend({ parseDocxAggregator });
30
78
  //#endregion
31
- export { BLOCKQUOTE_BORDER, BLOCKQUOTE_INDENT_LEFT, Blockquote, applyBlockquoteStyle };
79
+ export { BLOCKQUOTE_BORDER, BLOCKQUOTE_INDENT_LEFT, Blockquote, applyBlockquoteStyle, parseDocxAggregator };
@@ -1,4 +1,4 @@
1
- import { BulletList as BulletList$1 } from "./tiptap.mjs";
1
+ import { BulletList as BulletList$1 } from "@tiptap/extension-bullet-list";
2
2
  //#region src/extensions/bullet-list.ts
3
3
  /**
4
4
  * BulletList — carries the source DOCX abstractNum reference (when the list came
@@ -1,2 +1,2 @@
1
- import { I as CodeBlock, L as renderDocx } from "../core-DC0_-WcE.mjs";
2
- export { CodeBlock, renderDocx };
1
+ import { et as CodeBlock, nt as renderDocx, tt as parseDocxParagraph } from "../core-BqyLL84S.mjs";
2
+ export { CodeBlock, parseDocxParagraph, renderDocx };