@docen/docx 0.3.1 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/dist/converters/docx.d.mts +220 -1
  2. package/dist/converters/docx.mjs +1352 -1
  3. package/dist/converters/html.d.mts +28 -1
  4. package/dist/converters/html.mjs +103 -1
  5. package/dist/converters/markdown.d.mts +12 -1
  6. package/dist/converters/markdown.mjs +17 -1
  7. package/dist/converters/patch.d.mts +52 -1
  8. package/dist/converters/patch.mjs +35 -1
  9. package/dist/converters/prepare.d.mts +62 -1
  10. package/dist/converters/prepare.mjs +121 -1
  11. package/dist/converters/styles.d.mts +91 -2
  12. package/dist/converters/styles.mjs +308 -2
  13. package/dist/core-B8ba_FNi.mjs +717 -0
  14. package/dist/core-DRaLI8nd.d.mts +203 -0
  15. package/dist/core.d.mts +1 -1
  16. package/dist/core.mjs +1 -1
  17. package/dist/document-Cws7XTYL.d.mts +28 -0
  18. package/dist/editor.d.mts +20 -1
  19. package/dist/editor.mjs +20 -1
  20. package/dist/extensions/blockquote.d.mts +1 -1
  21. package/dist/extensions/blockquote.mjs +30 -2
  22. package/dist/extensions/code-block.d.mts +1 -1
  23. package/dist/extensions/code-block.mjs +22 -1
  24. package/dist/extensions/column-break.d.mts +1 -1
  25. package/dist/extensions/column-break.mjs +1 -1
  26. package/dist/extensions/details.d.mts +1 -1
  27. package/dist/extensions/details.mjs +17 -2
  28. package/dist/extensions/document.d.mts +1 -1
  29. package/dist/extensions/document.mjs +45 -1
  30. package/dist/extensions/extensions.d.mts +4 -5
  31. package/dist/extensions/extensions.mjs +13 -14
  32. package/dist/extensions/formatting-marks.d.mts +7 -1
  33. package/dist/extensions/formatting-marks.mjs +1 -1
  34. package/dist/extensions/heading.mjs +187 -1
  35. package/dist/extensions/image.d.mts +1 -1
  36. package/dist/extensions/image.mjs +395 -1
  37. package/dist/extensions/index.d.mts +4 -3
  38. package/dist/extensions/index.mjs +4 -3
  39. package/dist/extensions/mention.d.mts +1 -1
  40. package/dist/extensions/mention.mjs +43 -2
  41. package/dist/extensions/ordered-list.d.mts +1 -1
  42. package/dist/extensions/ordered-list.mjs +40 -2
  43. package/dist/extensions/page-break.d.mts +1 -33
  44. package/dist/extensions/page-break.mjs +1 -1
  45. package/dist/extensions/paragraph.mjs +145 -1
  46. package/dist/extensions/passthrough.d.mts +1 -1
  47. package/dist/extensions/passthrough.mjs +1 -1
  48. package/dist/extensions/section-break.d.mts +19 -2
  49. package/dist/extensions/section-break.mjs +1 -1
  50. package/dist/extensions/strike.mjs +45 -1
  51. package/dist/extensions/table-cell.mjs +106 -1
  52. package/dist/extensions/table-header.mjs +106 -1
  53. package/dist/extensions/table-row.mjs +78 -1
  54. package/dist/extensions/table.mjs +154 -1
  55. package/dist/extensions/task-item.d.mts +1 -1
  56. package/dist/extensions/task-item.mjs +36 -2
  57. package/dist/extensions/text-style.mjs +145 -1
  58. package/dist/extensions/tiptap.d.mts +2 -2
  59. package/dist/extensions/tiptap.mjs +31 -2
  60. package/dist/extensions/toc-field.d.mts +2 -0
  61. package/dist/extensions/toc-field.mjs +2 -0
  62. package/dist/extensions/utils.d.mts +2 -62
  63. package/dist/extensions/utils.mjs +508 -2
  64. package/dist/extensions/wpg-group.d.mts +2 -0
  65. package/dist/extensions/wpg-group.mjs +2 -0
  66. package/dist/extensions/wps-shape.d.mts +2 -0
  67. package/dist/extensions/wps-shape.mjs +2 -0
  68. package/dist/index.d.mts +101 -29
  69. package/dist/index.mjs +13 -10
  70. package/dist/{tiptap-TErPjuNJ.d.mts → tiptap-pZsNPsvV.d.mts} +1 -3
  71. package/dist/utils-BJwDQts7.d.mts +137 -0
  72. package/package.json +39 -40
  73. package/dist/blockquote-Ky9xD_Q-.mjs +0 -31
  74. package/dist/code-block-BgNjhADy.mjs +0 -23
  75. package/dist/core-DEqBHRzE.mjs +0 -406
  76. package/dist/core-fBMCCj4D.d.mts +0 -97
  77. package/dist/details-Dh8luWb2.mjs +0 -18
  78. package/dist/dist-B6B3HnVZ.mjs +0 -1838
  79. package/dist/document-BH1y4qHM.d.mts +0 -6
  80. package/dist/document-CvTym-FM.mjs +0 -50
  81. package/dist/docx-C5wyjI-0.d.mts +0 -200
  82. package/dist/docx-CONyAedl.mjs +0 -1231
  83. package/dist/editor-Bf3CxQnY.mjs +0 -21
  84. package/dist/editor-DReY3Rb9.d.mts +0 -21
  85. package/dist/emoji-BZ1dJD7d.d.mts +0 -17
  86. package/dist/emoji-C2OsTWR7.mjs +0 -27
  87. package/dist/extensions/emoji.d.mts +0 -2
  88. package/dist/extensions/emoji.mjs +0 -2
  89. package/dist/extensions/image-group.d.mts +0 -6
  90. package/dist/extensions/image-group.mjs +0 -2
  91. package/dist/heading-DAubgJJN.mjs +0 -166
  92. package/dist/html-DCC_DSHv.d.mts +0 -18
  93. package/dist/html-DedDWPoO.mjs +0 -66
  94. package/dist/image-CuEIXoZl.mjs +0 -334
  95. package/dist/markdown-BzJmRsBs.mjs +0 -18
  96. package/dist/markdown-D0k0sRYS.d.mts +0 -13
  97. package/dist/mention-C-dTR6MK.mjs +0 -44
  98. package/dist/ordered-list-V-3xFQmV.mjs +0 -41
  99. package/dist/paragraph-BrWH-BOV.mjs +0 -147
  100. package/dist/patch-Cdp5WKJz.d.mts +0 -53
  101. package/dist/patch-IZ13tXTu.mjs +0 -36
  102. package/dist/prepare-Bdk8Cjkf.mjs +0 -122
  103. package/dist/prepare-CPAvcv6q.d.mts +0 -63
  104. package/dist/strike-hWxShOgb.mjs +0 -50
  105. package/dist/styles-QSX3RhxK.mjs +0 -285
  106. package/dist/styles-ugspkrxz.d.mts +0 -85
  107. package/dist/table-DrA12lxX.mjs +0 -159
  108. package/dist/table-cell-DmkfYwDc.mjs +0 -112
  109. package/dist/table-header-Bwr5GKWg.mjs +0 -112
  110. package/dist/table-row-vew1y7WH.mjs +0 -84
  111. package/dist/task-item-BnlXGrIG.mjs +0 -37
  112. package/dist/text-style-tMeBq5XQ.mjs +0 -151
  113. package/dist/tiptap-D7pl4I5U.mjs +0 -33
  114. package/dist/utils-BPT4tjcn.mjs +0 -333
@@ -0,0 +1,717 @@
1
+ import { Blockquote, Bold, BulletList, Code, CodeBlockLowlight, Details, DetailsContent, DetailsSummary, Emoji, HardBreak, Highlight, HorizontalRule, Italic, Link, ListItem, Mathematics, Mention, OrderedList, Subscript, Superscript, TaskItem, TaskList, Text, TextAlign as TextAlign$1, Underline } from "./extensions/tiptap.mjs";
2
+ import "./extensions/blockquote.mjs";
3
+ import { CodeBlock } from "./extensions/code-block.mjs";
4
+ import "./extensions/details.mjs";
5
+ import { attrNative, floatAnchorScope, floatingToStyles, normalizeColorToHex, renderRunStyles } from "./extensions/utils.mjs";
6
+ import { Document } from "./extensions/document.mjs";
7
+ import { Heading } from "./extensions/heading.mjs";
8
+ import { Image } from "./extensions/image.mjs";
9
+ import "./extensions/mention.mjs";
10
+ import "./extensions/ordered-list.mjs";
11
+ import { Paragraph } from "./extensions/paragraph.mjs";
12
+ import { Strike } from "./extensions/strike.mjs";
13
+ import { Table } from "./extensions/table.mjs";
14
+ import { TableCell } from "./extensions/table-cell.mjs";
15
+ import { TableHeader } from "./extensions/table-header.mjs";
16
+ import { TableRow } from "./extensions/table-row.mjs";
17
+ import "./extensions/task-item.mjs";
18
+ import { TextStyle } from "./extensions/text-style.mjs";
19
+ import { Editor, Extension, Mark, Node } from "@tiptap/core";
20
+ import { all, createLowlight } from "lowlight";
21
+ import { TextAlign } from "@tiptap/extension-text-align";
22
+ import { Plugin, PluginKey, TextSelection } from "@tiptap/pm/state";
23
+ import { Decoration, DecorationSet } from "@tiptap/pm/view";
24
+ import { convertEmuToPixels, encodeBase64 } from "@office-open/core";
25
+ //#region src/extensions/column-break.ts
26
+ /**
27
+ * ColumnBreak — inline atom node for DOCX column breaks (`<w:br w:type="column"/>`).
28
+ *
29
+ * Sibling to PageBreak and HardBreak: all three render `<w:br>`, differing only
30
+ * by type. Inline (group "inline") to match the run-internal OOXML position,
31
+ * keeping a column break inside its paragraph on round-trip. Visual column
32
+ * layout (paged.js multi-column) is a future concern; the node preserves the
33
+ * break losslessly regardless.
34
+ *
35
+ * The DOCX payload (`{ columnBreak: true }`) is inlined in DocxManager — a
36
+ * one-liner with no per-node variance, so no extension helper for it.
37
+ *
38
+ * `setColumnBreak` only inserts the atom (no paragraph split): a column break
39
+ * does not start a new page, and there is no column layout to reflow yet, so
40
+ * the node is purely for round-trip fidelity until multi-column lands.
41
+ */
42
+ const ColumnBreak = Node.create({
43
+ name: "columnBreak",
44
+ inline: true,
45
+ group: "inline",
46
+ atom: true,
47
+ parseHTML() {
48
+ return [{ tag: "span[data-type=\"columnBreak\"]" }, { tag: "br[data-type=\"columnBreak\"]" }];
49
+ },
50
+ renderHTML() {
51
+ return ["span", {
52
+ "data-type": "columnBreak",
53
+ style: "break-after:column"
54
+ }];
55
+ },
56
+ addCommands() {
57
+ return { setColumnBreak: () => ({ commands }) => commands.insertContent({ type: "columnBreak" }) };
58
+ }
59
+ });
60
+ //#endregion
61
+ //#region src/extensions/formatting-marks.ts
62
+ /** Plugin key holding {enabled, decos} for the formatting-marks plugin.
63
+ * `enabled` is toggled by a transaction meta; `decos` caches the widget set so
64
+ * a pure selection/cursor transaction (doc unchanged) reuses it instead of
65
+ * re-traversing every textblock. The un-cached version ran that traversal on
66
+ * every selectionUpdate — 200ms+ on a 1000-page document — making the caret
67
+ * and selection feel frozen. */
68
+ const FORMAT_MARKS_KEY = new PluginKey("docen-formatting-marks");
69
+ /** Build a widget set with one paragraph-mark widget at each textblock's
70
+ * closing position. Called only on doc change or marks toggle. */
71
+ function buildParagraphMarkDecos(doc) {
72
+ const decos = [];
73
+ doc.descendants((node, pos) => {
74
+ if (!node.isTextblock) return;
75
+ decos.push(Decoration.widget(pos + node.nodeSize - 1, () => {
76
+ const span = document.createElement("span");
77
+ span.className = "docen-para-mark";
78
+ span.contentEditable = "false";
79
+ span.textContent = "↲";
80
+ return span;
81
+ }, { side: 1 }));
82
+ });
83
+ return DecorationSet.create(doc, decos);
84
+ }
85
+ /**
86
+ * FormattingMarks — paints the non-printing paragraph mark via ProseMirror
87
+ * widget decorations.
88
+ *
89
+ * CSS `p::after { content: "…" }` does NOT work on a ProseMirror-managed
90
+ * paragraph: the view owns the DOM and the trailing-break kludge pushes the
91
+ * pseudo-element off the visible line, so the mark never appears (verified).
92
+ * Marijn Haverbeke endorses widget decorations for exactly this use case
93
+ * (https://discuss.prosemirror.net/t/1442). Each textblock gets a widget at its
94
+ * closing position (inside the textblock, so it stays on the content's line)
95
+ * holding a non-editable `<span class="docen-para-mark">`. `side: 1` keeps the
96
+ * mark to the right of a cursor resting at the paragraph end (the cursor always
97
+ * precedes its own paragraph mark).
98
+ *
99
+ * Glyph: a down-then-left return arrow (↲, U+21B2 "downwards arrow with tip
100
+ * leftwards" — the Enter-key glyph). Word/WPS use the pilcrow ¶ for paragraph
101
+ * marks; that down-then-left arrow is Word's line-break glyph — adopted here as
102
+ * the paragraph mark per the project's visual preference.
103
+ *
104
+ * Performance: the widget set is cached in plugin state and rebuilt only on a
105
+ * doc change or a marks toggle — NOT on every selectionUpdate. Without the
106
+ * cache, each caret move / selection re-traverses the whole document (one
107
+ * widget per textblock) and rebuilds the set, which costs 200ms+ on 1000-page
108
+ * documents. Pure selection transactions now reuse the cached set in O(1).
109
+ */
110
+ const FormattingMarks = Extension.create({
111
+ name: "formattingMarks",
112
+ addProseMirrorPlugins() {
113
+ return [new Plugin({
114
+ key: FORMAT_MARKS_KEY,
115
+ state: {
116
+ init: () => ({
117
+ enabled: false,
118
+ decos: DecorationSet.empty
119
+ }),
120
+ apply: (tr, prev) => {
121
+ const toggled = tr.getMeta(FORMAT_MARKS_KEY) === "toggle";
122
+ const enabled = toggled ? !prev.enabled : prev.enabled;
123
+ if (!enabled) return {
124
+ enabled: false,
125
+ decos: DecorationSet.empty
126
+ };
127
+ if (toggled || tr.docChanged) return {
128
+ enabled,
129
+ decos: buildParagraphMarkDecos(tr.doc)
130
+ };
131
+ return {
132
+ enabled,
133
+ decos: prev.decos
134
+ };
135
+ }
136
+ },
137
+ props: { decorations: (state) => FORMAT_MARKS_KEY.getState(state).decos }
138
+ })];
139
+ },
140
+ addCommands() {
141
+ return { toggleFormattingMarks: () => ({ tr, dispatch }) => {
142
+ if (dispatch) dispatch(tr.setMeta(FORMAT_MARKS_KEY, "toggle"));
143
+ return true;
144
+ } };
145
+ }
146
+ });
147
+ //#endregion
148
+ //#region src/extensions/page-break.ts
149
+ /**
150
+ * PageBreak — inline atom node for DOCX page breaks (`<w:br w:type="page"/>`).
151
+ *
152
+ * OOXML page breaks live inside a run (inline), so the Tiptap node is inline
153
+ * too (group "inline"). This keeps a break inside its paragraph on round-trip
154
+ * — a break mid-paragraph stays mid-paragraph instead of splitting the
155
+ * paragraph into separate blocks (which would lose the original structure).
156
+ * Sibling to HardBreak: both render `<w:br>`, differing only by type.
157
+ *
158
+ * The DOCX payload (`{ pageBreak: true }`) is inlined in DocxManager — a
159
+ * one-liner with no per-node variance, so no extension helper for it.
160
+ *
161
+ * Inserting a page break must also visually reflow (the C-route paginator only
162
+ * forces a new page after a block *containing* a pageBreak atom). So
163
+ * `setPageBreak` inserts the atom then splits the paragraph — the atom lands at
164
+ * the end of the first half, the trailing text becomes a fresh paragraph, and
165
+ * the paginator's `forcesPageBreakAfter` moves it to the next page (matching
166
+ * Word's Ctrl+Enter behavior).
167
+ */
168
+ const PageBreak = Node.create({
169
+ name: "pageBreak",
170
+ inline: true,
171
+ group: "inline",
172
+ atom: true,
173
+ parseHTML() {
174
+ return [{ tag: "span[data-type=\"pageBreak\"]" }, { tag: "br[data-type=\"pageBreak\"]" }];
175
+ },
176
+ renderHTML() {
177
+ return ["span", {
178
+ "data-type": "pageBreak",
179
+ style: "break-after:page"
180
+ }];
181
+ },
182
+ addProseMirrorPlugins() {
183
+ return [new Plugin({ appendTransaction: (transactions, _oldState, newState) => {
184
+ if (!transactions.some((tr) => tr.docChanged)) return null;
185
+ const doc = newState.doc;
186
+ const tr = newState.tr;
187
+ const splits = [];
188
+ doc.descendants((node, pos) => {
189
+ if (!node.isTextblock) return;
190
+ const last = node.childCount - 1;
191
+ node.forEach((child, offset, index) => {
192
+ if (child.type.name === "pageBreak" && index < last) splits.push(pos + 1 + offset + child.nodeSize);
193
+ });
194
+ return false;
195
+ });
196
+ if (splits.length === 0) return null;
197
+ splits.sort((a, b) => b - a);
198
+ for (const splitPos of splits) tr.split(splitPos, 1);
199
+ tr.setSelection(newState.selection.map(tr.doc, tr.mapping));
200
+ return tr;
201
+ } })];
202
+ },
203
+ addCommands() {
204
+ return { setPageBreak: () => ({ tr, state, dispatch }) => {
205
+ if (!dispatch) return true;
206
+ const { $from } = state.selection;
207
+ const depth = $from.depth;
208
+ if (($from.index(depth) > 0 ? $from.parent.child($from.index(depth) - 1) : null)?.type.name === "pageBreak") {
209
+ const after = $from.after(depth);
210
+ const carrier = state.schema.nodes.paragraph.create(null, [state.schema.nodes.pageBreak.create()]);
211
+ tr.insert(after, carrier);
212
+ tr.setSelection(TextSelection.near(tr.doc.resolve(after + carrier.nodeSize)));
213
+ tr.scrollIntoView();
214
+ return true;
215
+ }
216
+ tr.replaceSelectionWith(state.schema.nodes.pageBreak.create());
217
+ tr.split(tr.selection.from, 1);
218
+ tr.scrollIntoView();
219
+ return true;
220
+ } };
221
+ }
222
+ });
223
+ //#endregion
224
+ //#region src/extensions/passthrough.ts
225
+ /**
226
+ * Passthrough — block atom carrying an opaque {@link SectionChild} that has
227
+ * no native Tiptap representation (rawXml, bookmarkStart/End, textbox,
228
+ * altChunk, subDoc, customXml).
229
+ *
230
+ * The full SectionChild is stored as JSON in `attrs.data` so the DOCX→JSON→DOCX
231
+ * round-trip stays byte-faithful: office-open's stringify handles the inner
232
+ * structure verbatim (including a textbox's nested children, which remain as
233
+ * structured ParagraphOptions inside the blob rather than editable Tiptap
234
+ * nodes). The node is not editable — it renders a read-only placeholder in HTML.
235
+ *
236
+ * DOCX serialization is inlined in DocxManager (compile/resolve read/write
237
+ * `attrs.data` directly), so no renderDocx/parseDocx is needed here.
238
+ */
239
+ const Passthrough = Node.create({
240
+ name: "passthrough",
241
+ group: "block",
242
+ atom: true,
243
+ addAttributes() {
244
+ return { data: {
245
+ default: "{}",
246
+ rendered: false
247
+ } };
248
+ },
249
+ parseHTML() {
250
+ return [{ tag: "div[data-passthrough]" }];
251
+ },
252
+ renderHTML({ node }) {
253
+ let label = "DOCX";
254
+ let key = "";
255
+ try {
256
+ const parsed = JSON.parse(node.attrs.data || "{}");
257
+ key = Object.keys(parsed)[0] ?? "";
258
+ if (key) label = key;
259
+ } catch {}
260
+ if (key === "bookmarkStart" || key === "bookmarkEnd") return ["div", {
261
+ "data-passthrough": label,
262
+ contenteditable: "false",
263
+ style: "display:none"
264
+ }];
265
+ return [
266
+ "div",
267
+ {
268
+ "data-passthrough": label,
269
+ contenteditable: "false",
270
+ style: "display:block;padding:0.5em 0.75em;margin:0.5em 0;border:1px dashed #bbb;border-radius:4px;color:#888;font-size:0.85em;background:#fafafa"
271
+ },
272
+ [
273
+ "span",
274
+ {},
275
+ `[${label}]`
276
+ ]
277
+ ];
278
+ }
279
+ });
280
+ //#endregion
281
+ //#region src/extensions/section-break.ts
282
+ /**
283
+ * SectionBreak — command extension that marks a paragraph as a section boundary.
284
+ *
285
+ * OOXML sections (sectPr) attach to a section's LAST paragraph's pPr, NOT a
286
+ * standalone node. So this extension provides only the `setSectionBreak`
287
+ * command (stamps sectionProperties on the current paragraph); the paragraph
288
+ * extension carries the sectionProperties/sectionHeaders/sectionFooters attrs,
289
+ * and DocxManager splits/merges sections in compile/resolve by reading them off
290
+ * the paragraph.
291
+ *
292
+ * The final section's sectPr rides on doc.attrs.sectionProperties (it lives at
293
+ * <w:body>'s end in OOXML). Single-section documents have no section-carrying
294
+ * paragraph at all.
295
+ *
296
+ * Next Page semantics: `setSectionBreak` stamps sectionProperties on the current
297
+ * paragraph (making it the section's last paragraph) AND inserts a fresh empty
298
+ * paragraph after it (the next section's first paragraph), then moves the
299
+ * selection into that new paragraph. The page-plugin's `forcesPageBreakAfter`
300
+ * treats a sectionProperties-bearing paragraph as a page break, so repaginate
301
+ * pushes the new paragraph onto the next page — and the caret follows. This
302
+ * mirrors Word's "Section Break (Next Page)".
303
+ *
304
+ * Split hygiene: pressing Enter inside a section-carrying paragraph must NOT
305
+ * split it. A split would place the new paragraph past the section boundary —
306
+ * forcesPageBreakAfter then pushes it onto the next page (next section), and
307
+ * splitBlock would copy sectionProperties onto it (a second break mark). Word
308
+ * instead inserts a fresh paragraph BEFORE the section's last paragraph, so the
309
+ * new paragraph stays in this section and the stamped paragraph remains last.
310
+ * The Enter shortcut does exactly that; non-section paragraphs fall through to
311
+ * the default Enter unchanged.
312
+ */
313
+ const SectionBreak = Extension.create({
314
+ name: "sectionBreak",
315
+ priority: 1e3,
316
+ addCommands() {
317
+ return { setSectionBreak: () => ({ tr, state, dispatch }) => {
318
+ if (!dispatch) return true;
319
+ const { $from } = tr.selection;
320
+ const para = $from.parent;
321
+ if (para.type.name !== "paragraph") return false;
322
+ const paraPos = $from.before($from.depth);
323
+ tr.setNodeMarkup(paraPos, void 0, {
324
+ ...para.attrs,
325
+ sectionProperties: {}
326
+ });
327
+ const paraEnd = paraPos + para.nodeSize;
328
+ tr.insert(paraEnd, state.schema.nodes.paragraph.create());
329
+ tr.setSelection(TextSelection.near(tr.doc.resolve(paraEnd + 1)));
330
+ tr.scrollIntoView();
331
+ return true;
332
+ } };
333
+ },
334
+ addKeyboardShortcuts() {
335
+ return { Enter: ({ editor }) => {
336
+ const { $from } = editor.state.selection;
337
+ const para = $from.parent;
338
+ if (para.attrs.sectionProperties == null) return false;
339
+ const paraPos = $from.before($from.depth);
340
+ return editor.chain().insertContentAt(paraPos, { type: para.type.name }).setTextSelection(paraPos + 1).run();
341
+ } };
342
+ }
343
+ });
344
+ //#endregion
345
+ //#region src/extensions/toc-field.ts
346
+ /**
347
+ * TOC field (`tocField`) — a block container representing a DOCX table of
348
+ * contents, with the rendered entries as editable `content` and the TOC field
349
+ * switches on `attrs.options`.
350
+ *
351
+ * Named `tocField` (not `tableOfContents`) to avoid colliding with the official
352
+ * `@tiptap/extension-table-of-contents`, which is an `Extension` (a live outline
353
+ * generator injected into the editor) that already owns the `tableOfContents`
354
+ * name. Same-name extensions dedupe in Tiptap, and since the official one is not
355
+ * a node it would erase this node type from the schema. The two coexist for
356
+ * different purposes: this node persists a DOCX's rendered TOC; the official
357
+ * extension drives the heading-outline pane.
358
+ *
359
+ * Structuring the TOC as a node (instead of opaque passthrough) is what fixes
360
+ * the export crash. Each entry paragraph's `w:hyperlink` wraps a HYPERLINK field
361
+ * whose content-less runs (fldChar begin/separate/end) office-open parses as
362
+ * `null`. As opaque passthrough those nulls survived verbatim to
363
+ * `generateDocument`, where office-open's `stringifyRunInline(null).break`
364
+ * crashed. Resolving the entries through `resolveParagraphChildren` drops the
365
+ * nulls (the existing `child !== null` guard), so compile rebuilds clean entries
366
+ * and the generate path never sees a null — no office-open change required.
367
+ *
368
+ * DOCX serialization is inlined in DocxManager (resolve/compile read/write
369
+ * `attrs.options` + the entry content directly), so no renderDocx/parseDocx is
370
+ * needed here — the same pattern as the details extension.
371
+ */
372
+ const TocField = Node.create({
373
+ name: "tocField",
374
+ group: "block",
375
+ content: "block+",
376
+ addAttributes() {
377
+ return { options: attrNative() };
378
+ },
379
+ parseHTML() {
380
+ return [{ tag: "div.docx-toc" }];
381
+ },
382
+ renderHTML() {
383
+ return [
384
+ "div",
385
+ { class: "docx-toc" },
386
+ 0
387
+ ];
388
+ }
389
+ });
390
+ //#endregion
391
+ //#region src/extensions/wpg-group.ts
392
+ /**
393
+ * wpgGroup — inline atom carrying a DOCX drawing group (wpg: wordprocessingGroup)
394
+ * as an opaque blob. Mirrors the office-open `WpgGroupRunOptions` / ParagraphChild
395
+ * `wpgGroup` field verbatim in attrs.wpgGroup, so the node name and attr stay
396
+ * aligned with the OOXML concept (CT_WordprocessingGroup).
397
+ *
398
+ * A group bundles pictures (pic), shapes (wps), and nested groups (wpg) behind a
399
+ * shared coordinate space (grpSpPr chOff/chExt → extent). renderHTML lays each
400
+ * child out at its transformed position/size so the group renders as Word draws
401
+ * it (e.g. a row of colored decoration bars), instead of a single picture.
402
+ */
403
+ const EMU_PER_PT = 12700;
404
+ const EMU_PER_PX = 9525;
405
+ /** Solid fill → CSS color (noFill/none/gradient → undefined). */
406
+ function fillToCss(fill) {
407
+ if (!fill || fill.type !== "solid") return void 0;
408
+ return normalizeColorToHex(fill.color?.value);
409
+ }
410
+ /** Shape outline → CSS border (EMU width → pt). noFill → undefined. */
411
+ function outlineToCss(outline) {
412
+ if (!outline || outline.type === "noFill" || outline.type === "none") return void 0;
413
+ const color = normalizeColorToHex(outline.color?.value) ?? "black";
414
+ return `border:${outline.width != null ? `${outline.width / EMU_PER_PT}pt` : "0.75pt"} ${outline.dash === "sysDot" || outline.dash === "sysDash" ? "dashed" : "solid"} ${color}`;
415
+ }
416
+ /** Group extent in px: top-level groups carry {width,height}; nested wpg children
417
+ * carry a ChildTransformation whose pixels/emus give the size. */
418
+ function groupExtent(group) {
419
+ const t = group.transformation;
420
+ return {
421
+ w: t?.width != null ? convertEmuToPixels(t.width) : t?.pixels?.x ?? 0,
422
+ h: t?.height != null ? convertEmuToPixels(t.height) : t?.pixels?.y ?? 0
423
+ };
424
+ }
425
+ /** Child coord (EMU, group's chOff/chExt space) → group-local px. */
426
+ function childBox(child, chOff, scaleX, scaleY) {
427
+ const t = child.transformation;
428
+ const off = t?.offset?.emus ?? {
429
+ x: 0,
430
+ y: 0
431
+ };
432
+ const size = t?.emus ?? {
433
+ x: 0,
434
+ y: 0
435
+ };
436
+ return {
437
+ x: (off.x - chOff.x) * scaleX,
438
+ y: (off.y - chOff.y) * scaleY,
439
+ w: size.x * scaleX,
440
+ h: size.y * scaleY
441
+ };
442
+ }
443
+ /** pic media type + raw bytes → data URL (bytes may arrive as Uint8Array or a
444
+ * plain object of byte values across the JSON boundary). */
445
+ function picSrc(mediaType, data) {
446
+ if (!data) return null;
447
+ return `data:image/${mediaType};base64,${encodeBase64(data instanceof Uint8Array ? data : new Uint8Array(Object.values(data)))}`;
448
+ }
449
+ function renderChild(child, chOff, scaleX, scaleY) {
450
+ const box = childBox(child, chOff, scaleX, scaleY);
451
+ const base = `position:absolute;left:${box.x}px;top:${box.y}px;width:${box.w}px;height:${box.h}px;box-sizing:border-box`;
452
+ if (child.type === "wpg") return renderGroup(child, box.w, box.h, base);
453
+ if (child.type === "wps") {
454
+ const data = child.data ?? {};
455
+ return renderWpsInterior(data, base, {
456
+ rotation: data.bodyProperties?.rotation ?? child.transformation?.rotation,
457
+ attrs: { "data-wpg-wps": "" }
458
+ });
459
+ }
460
+ if (child.type) {
461
+ const src = picSrc(child.type, child.data);
462
+ if (src) return ["img", {
463
+ src,
464
+ alt: "",
465
+ style: base
466
+ }];
467
+ }
468
+ return [];
469
+ }
470
+ /** wps text-body paragraphs (office-open ParagraphOptions[]) → inline HTML runs
471
+ * so a text-box shape shows its text. Each paragraph's run defaults merge with
472
+ * each text run; renderRunStyles converts font/color/size/… to CSS. Advanced run
473
+ * props (shadow w14RawXml, kern) are carried in attrs but not rendered. */
474
+ function renderWpsText(children) {
475
+ if (!Array.isArray(children)) return [];
476
+ return children.map((para) => {
477
+ if (typeof para === "string") return [
478
+ "p",
479
+ { style: "margin:0" },
480
+ para
481
+ ];
482
+ const defaultRun = para.run ?? {};
483
+ const runs = (para.children ?? []).map((r) => {
484
+ if (typeof r === "string") return r;
485
+ const css = renderRunStyles({
486
+ ...defaultRun,
487
+ ...r
488
+ });
489
+ return [
490
+ "span",
491
+ css.length ? { style: css.join(";") } : {},
492
+ r.text ?? ""
493
+ ];
494
+ });
495
+ return [
496
+ "p",
497
+ { style: para.alignment ? `margin:0;text-align:${para.alignment}` : "margin:0" },
498
+ ...runs
499
+ ];
500
+ });
501
+ }
502
+ /**
503
+ * Render a wps shape's interior (fill/outline/insets/rotation/writing-mode + text
504
+ * body) as a positioned div. Shared by the wpg group's inline wps children and
505
+ * the standalone wpsShape node, so a text-box shape renders identically whether
506
+ * it floats alone (wp:anchor > wps:wsp) or sits inside a group. `positionStyle`
507
+ * carries the placement — absolute group coords for a group child, the floating
508
+ * anchor CSS for a standalone shape. `opts.rotation` overrides bodyPr rotation
509
+ * (a group child's rotation may come from the group transform, not bodyPr).
510
+ */
511
+ function renderWpsInterior(data, positionStyle, opts) {
512
+ const styles = [positionStyle];
513
+ const fill = fillToCss(data.fill);
514
+ if (fill) styles.push(`background-color:${fill}`);
515
+ const outline = outlineToCss(data.outline);
516
+ if (outline) styles.push(outline);
517
+ const bp = data.bodyProperties;
518
+ if (bp) {
519
+ const ins = (v) => v != null ? `${(v / EMU_PER_PX).toFixed(1)}px` : "0px";
520
+ styles.push(`padding:${ins(bp.tIns)} ${ins(bp.rIns)} ${ins(bp.bIns)} ${ins(bp.lIns)}`);
521
+ }
522
+ const rotation = opts?.rotation ?? bp?.rotation;
523
+ if (rotation) styles.push(`transform:rotate(${rotation}deg)`);
524
+ const vert = bp?.vert;
525
+ if (vert && vert !== "horz") styles.push(`writing-mode:${vert === "vert270" ? "vertical-lr" : "vertical-rl"}`);
526
+ const attrs = { style: styles.join(";") };
527
+ if (opts?.attrs) Object.assign(attrs, opts.attrs);
528
+ return [
529
+ "div",
530
+ attrs,
531
+ ...renderWpsText(data.children)
532
+ ];
533
+ }
534
+ /** Render a wpg group (top-level or nested) as a positioned container.
535
+ * actualW/H are the group's real pixel size — top-level groups read them from
536
+ * transformation.width/height; nested groups receive the box their parent
537
+ * transform mapped them to. containerStyle (when set) carries the absolute
538
+ * placement for a nested group; otherwise the group is an inline-block. */
539
+ function renderGroup(group, actualW, actualH, containerStyle, extraAttrs) {
540
+ const chOff = group.childOffset ?? {
541
+ x: 0,
542
+ y: 0
543
+ };
544
+ const chExt = group.childExtent ?? {
545
+ cx: 1,
546
+ cy: 1
547
+ };
548
+ const scaleX = actualW / chExt.cx;
549
+ const scaleY = actualH / chExt.cy;
550
+ const style = containerStyle ?? `position:relative;display:inline-block;vertical-align:middle;width:${actualW}px;height:${actualH}px`;
551
+ const children = (group.children ?? []).map((c) => renderChild(c, chOff, scaleX, scaleY)).filter((c) => Array.isArray(c) && c.length > 0);
552
+ const attrs = {
553
+ "data-wpg-group": "",
554
+ style
555
+ };
556
+ if (extraAttrs) Object.assign(attrs, extraAttrs);
557
+ return [
558
+ "span",
559
+ attrs,
560
+ ...children
561
+ ];
562
+ }
563
+ const attrWpgGroup = () => ({
564
+ default: null,
565
+ rendered: false,
566
+ parseHTML: (element) => {
567
+ const raw = element.getAttribute("data-wpg-group");
568
+ if (!raw) return null;
569
+ try {
570
+ return JSON.parse(raw);
571
+ } catch {
572
+ return null;
573
+ }
574
+ }
575
+ });
576
+ const WpgGroup = Node.create({
577
+ name: "wpgGroup",
578
+ group: "inline",
579
+ inline: true,
580
+ atom: true,
581
+ addAttributes() {
582
+ return { wpgGroup: attrWpgGroup() };
583
+ },
584
+ parseHTML() {
585
+ return [{ tag: "span[data-wpg-group]" }];
586
+ },
587
+ renderHTML({ node }) {
588
+ const wpg = node.attrs.wpgGroup ?? {};
589
+ const { w, h } = groupExtent(wpg);
590
+ if (wpg.floating) return renderGroup(wpg, w, h, [
591
+ ...floatingToStyles(wpg.floating, void 0, w),
592
+ `width:${w}px`,
593
+ `height:${h}px`
594
+ ].join(";"), floatAnchorScope(wpg.floating) === "paragraph" ? { "data-float-anchor": "paragraph" } : void 0);
595
+ return renderGroup(wpg, w, h);
596
+ }
597
+ });
598
+ //#endregion
599
+ //#region src/extensions/wps-shape.ts
600
+ const attrWpsShape = () => ({
601
+ default: null,
602
+ rendered: false,
603
+ parseHTML: (element) => {
604
+ const raw = element.getAttribute("data-wps-shape");
605
+ if (!raw) return null;
606
+ try {
607
+ return JSON.parse(raw);
608
+ } catch {
609
+ return null;
610
+ }
611
+ }
612
+ });
613
+ const WpsShape = Node.create({
614
+ name: "wpsShape",
615
+ group: "inline",
616
+ inline: true,
617
+ atom: true,
618
+ addAttributes() {
619
+ return { wpsShape: attrWpsShape() };
620
+ },
621
+ parseHTML() {
622
+ return [{ tag: "div[data-wps-shape]" }];
623
+ },
624
+ renderHTML({ node }) {
625
+ const ws = node.attrs.wpsShape ?? {};
626
+ const sizeStyle = `width:${ws.transformation?.width != null ? convertEmuToPixels(ws.transformation.width) : 0}px;height:${ws.transformation?.height != null ? convertEmuToPixels(ws.transformation.height) : 0}px;box-sizing:border-box`;
627
+ if (ws.floating) {
628
+ const pos = [...floatingToStyles(ws.floating, void 0, ws.transformation?.width), sizeStyle].join(";");
629
+ const attrs = { "data-wps-shape": "" };
630
+ if (floatAnchorScope(ws.floating) === "paragraph") attrs["data-float-anchor"] = "paragraph";
631
+ return renderWpsInterior(ws, pos, { attrs });
632
+ }
633
+ return renderWpsInterior(ws, `display:inline-block;vertical-align:middle;${sizeStyle}`, { attrs: { "data-wps-shape": "" } });
634
+ }
635
+ });
636
+ //#endregion
637
+ //#region src/extensions/extensions.ts
638
+ const tiptapNodeExtensions = [
639
+ Document,
640
+ Paragraph,
641
+ Text,
642
+ HardBreak,
643
+ PageBreak,
644
+ ColumnBreak,
645
+ SectionBreak,
646
+ Passthrough,
647
+ TocField,
648
+ Blockquote,
649
+ OrderedList,
650
+ BulletList,
651
+ ListItem,
652
+ CodeBlock.configure({ lowlight: createLowlight(all) }),
653
+ Details,
654
+ DetailsSummary,
655
+ DetailsContent,
656
+ Emoji,
657
+ HorizontalRule,
658
+ Image.configure({ inline: true }),
659
+ WpgGroup,
660
+ WpsShape,
661
+ Mathematics,
662
+ Mention,
663
+ Table,
664
+ TableRow,
665
+ TableCell,
666
+ TableHeader,
667
+ TaskList,
668
+ TaskItem,
669
+ Heading,
670
+ TextAlign$1.configure({ types: ["heading", "paragraph"] })
671
+ ];
672
+ const tiptapMarkExtensions = [
673
+ Bold,
674
+ Code,
675
+ Highlight,
676
+ Italic,
677
+ Link,
678
+ Strike,
679
+ Subscript,
680
+ Superscript,
681
+ TextStyle,
682
+ Underline
683
+ ];
684
+ const docxExtensions = [
685
+ ...tiptapNodeExtensions,
686
+ ...tiptapMarkExtensions,
687
+ FormattingMarks
688
+ ];
689
+ const DocxKit = Extension.create({
690
+ name: "docxKit",
691
+ addExtensions() {
692
+ const extensions = [];
693
+ if (this.options.bold !== false) extensions.push(Bold.configure(this.options.bold));
694
+ if (this.options.blockquote !== false) extensions.push(Blockquote.configure(this.options.blockquote));
695
+ if (this.options.bulletList !== false) extensions.push(BulletList.configure(this.options.bulletList));
696
+ if (this.options.code !== false) extensions.push(Code.configure(this.options.code));
697
+ if (this.options.codeBlock !== false) extensions.push(CodeBlockLowlight.configure({
698
+ lowlight: createLowlight(all),
699
+ ...this.options.codeBlock
700
+ }));
701
+ if (this.options.document !== false) extensions.push(Document);
702
+ if (this.options.hardBreak !== false) extensions.push(HardBreak.configure(this.options.hardBreak));
703
+ if (this.options.heading !== false) extensions.push(Heading.configure(this.options.heading));
704
+ if (this.options.horizontalRule !== false) extensions.push(HorizontalRule.configure(this.options.horizontalRule));
705
+ if (this.options.italic !== false) extensions.push(Italic.configure(this.options.italic));
706
+ if (this.options.listItem !== false) extensions.push(ListItem.configure(this.options.listItem));
707
+ if (this.options.link !== false) extensions.push(Link.configure(this.options.link));
708
+ if (this.options.orderedList !== false) extensions.push(OrderedList.configure(this.options.orderedList));
709
+ if (this.options.paragraph !== false) extensions.push(Paragraph.configure(this.options.paragraph));
710
+ if (this.options.strike !== false) extensions.push(Strike.configure(this.options.strike));
711
+ if (this.options.text !== false) extensions.push(Text);
712
+ if (this.options.underline !== false) extensions.push(Underline.configure(this.options.underline));
713
+ return extensions;
714
+ }
715
+ });
716
+ //#endregion
717
+ export { PageBreak as _, DocxKit as a, tiptapMarkExtensions as c, WpgGroup as d, renderWpsInterior as f, Passthrough as g, SectionBreak as h, Node as i, tiptapNodeExtensions as l, TocField as m, Extension as n, TextAlign as o, renderWpsText as p, Mark as r, docxExtensions as s, Editor as t, WpsShape as u, FormattingMarks as v, ColumnBreak as y };