@docen/docx 0.3.1 → 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 (109) hide show
  1. package/dist/converters/docx.d.mts +199 -1
  2. package/dist/converters/docx.mjs +1273 -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 +84 -1
  12. package/dist/converters/styles.mjs +284 -1
  13. package/dist/core-C9VunJ8o.d.mts +174 -0
  14. package/dist/core-DT9IrTzN.mjs +670 -0
  15. package/dist/core.d.mts +1 -1
  16. package/dist/core.mjs +1 -1
  17. package/dist/editor.d.mts +20 -1
  18. package/dist/editor.mjs +20 -1
  19. package/dist/extensions/blockquote.d.mts +1 -1
  20. package/dist/extensions/blockquote.mjs +30 -2
  21. package/dist/extensions/code-block.d.mts +1 -1
  22. package/dist/extensions/code-block.mjs +22 -1
  23. package/dist/extensions/column-break.d.mts +1 -1
  24. package/dist/extensions/column-break.mjs +1 -1
  25. package/dist/extensions/details.d.mts +1 -1
  26. package/dist/extensions/details.mjs +17 -2
  27. package/dist/extensions/document.mjs +49 -1
  28. package/dist/extensions/extensions.d.mts +3 -4
  29. package/dist/extensions/extensions.mjs +13 -14
  30. package/dist/extensions/formatting-marks.d.mts +7 -1
  31. package/dist/extensions/formatting-marks.mjs +1 -1
  32. package/dist/extensions/heading.mjs +165 -1
  33. package/dist/extensions/image.d.mts +1 -1
  34. package/dist/extensions/image.mjs +395 -1
  35. package/dist/extensions/index.d.mts +3 -2
  36. package/dist/extensions/index.mjs +4 -3
  37. package/dist/extensions/mention.d.mts +1 -1
  38. package/dist/extensions/mention.mjs +43 -2
  39. package/dist/extensions/ordered-list.d.mts +1 -1
  40. package/dist/extensions/ordered-list.mjs +40 -2
  41. package/dist/extensions/page-break.d.mts +1 -33
  42. package/dist/extensions/page-break.mjs +1 -1
  43. package/dist/extensions/paragraph.mjs +150 -1
  44. package/dist/extensions/passthrough.d.mts +1 -1
  45. package/dist/extensions/passthrough.mjs +1 -1
  46. package/dist/extensions/section-break.d.mts +19 -2
  47. package/dist/extensions/section-break.mjs +1 -1
  48. package/dist/extensions/strike.mjs +49 -1
  49. package/dist/extensions/table-cell.mjs +111 -1
  50. package/dist/extensions/table-header.mjs +111 -1
  51. package/dist/extensions/table-row.mjs +83 -1
  52. package/dist/extensions/table.mjs +159 -1
  53. package/dist/extensions/task-item.d.mts +1 -1
  54. package/dist/extensions/task-item.mjs +36 -2
  55. package/dist/extensions/text-style.mjs +150 -1
  56. package/dist/extensions/tiptap.d.mts +2 -2
  57. package/dist/extensions/tiptap.mjs +31 -2
  58. package/dist/extensions/utils.d.mts +2 -62
  59. package/dist/extensions/utils.mjs +499 -2
  60. package/dist/extensions/wpg-group.d.mts +2 -0
  61. package/dist/extensions/wpg-group.mjs +2 -0
  62. package/dist/extensions/wps-shape.d.mts +2 -0
  63. package/dist/extensions/wps-shape.mjs +2 -0
  64. package/dist/index.d.mts +86 -27
  65. package/dist/index.mjs +13 -10
  66. package/dist/{tiptap-TErPjuNJ.d.mts → tiptap-pZsNPsvV.d.mts} +1 -3
  67. package/dist/utils-D87vukzp.d.mts +128 -0
  68. package/package.json +39 -40
  69. package/dist/blockquote-Ky9xD_Q-.mjs +0 -31
  70. package/dist/code-block-BgNjhADy.mjs +0 -23
  71. package/dist/core-DEqBHRzE.mjs +0 -406
  72. package/dist/core-fBMCCj4D.d.mts +0 -97
  73. package/dist/details-Dh8luWb2.mjs +0 -18
  74. package/dist/dist-B6B3HnVZ.mjs +0 -1838
  75. package/dist/document-CvTym-FM.mjs +0 -50
  76. package/dist/docx-C5wyjI-0.d.mts +0 -200
  77. package/dist/docx-CONyAedl.mjs +0 -1231
  78. package/dist/editor-Bf3CxQnY.mjs +0 -21
  79. package/dist/editor-DReY3Rb9.d.mts +0 -21
  80. package/dist/emoji-BZ1dJD7d.d.mts +0 -17
  81. package/dist/emoji-C2OsTWR7.mjs +0 -27
  82. package/dist/extensions/emoji.d.mts +0 -2
  83. package/dist/extensions/emoji.mjs +0 -2
  84. package/dist/extensions/image-group.d.mts +0 -6
  85. package/dist/extensions/image-group.mjs +0 -2
  86. package/dist/heading-DAubgJJN.mjs +0 -166
  87. package/dist/html-DCC_DSHv.d.mts +0 -18
  88. package/dist/html-DedDWPoO.mjs +0 -66
  89. package/dist/image-CuEIXoZl.mjs +0 -334
  90. package/dist/markdown-BzJmRsBs.mjs +0 -18
  91. package/dist/markdown-D0k0sRYS.d.mts +0 -13
  92. package/dist/mention-C-dTR6MK.mjs +0 -44
  93. package/dist/ordered-list-V-3xFQmV.mjs +0 -41
  94. package/dist/paragraph-BrWH-BOV.mjs +0 -147
  95. package/dist/patch-Cdp5WKJz.d.mts +0 -53
  96. package/dist/patch-IZ13tXTu.mjs +0 -36
  97. package/dist/prepare-Bdk8Cjkf.mjs +0 -122
  98. package/dist/prepare-CPAvcv6q.d.mts +0 -63
  99. package/dist/strike-hWxShOgb.mjs +0 -50
  100. package/dist/styles-QSX3RhxK.mjs +0 -285
  101. package/dist/styles-ugspkrxz.d.mts +0 -85
  102. package/dist/table-DrA12lxX.mjs +0 -159
  103. package/dist/table-cell-DmkfYwDc.mjs +0 -112
  104. package/dist/table-header-Bwr5GKWg.mjs +0 -112
  105. package/dist/table-row-vew1y7WH.mjs +0 -84
  106. package/dist/task-item-BnlXGrIG.mjs +0 -37
  107. package/dist/text-style-tMeBq5XQ.mjs +0 -151
  108. package/dist/tiptap-D7pl4I5U.mjs +0 -33
  109. package/dist/utils-BPT4tjcn.mjs +0 -333
@@ -0,0 +1,670 @@
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 { Document } from "./extensions/document.mjs";
6
+ import { floatAnchorScope, floatingToStyles, normalizeColorToHex, renderRunStyles } from "./extensions/utils.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, toc, 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/wpg-group.ts
346
+ /**
347
+ * wpgGroup — inline atom carrying a DOCX drawing group (wpg: wordprocessingGroup)
348
+ * as an opaque blob. Mirrors the office-open `WpgGroupRunOptions` / ParagraphChild
349
+ * `wpgGroup` field verbatim in attrs.wpgGroup, so the node name and attr stay
350
+ * aligned with the OOXML concept (CT_WordprocessingGroup).
351
+ *
352
+ * A group bundles pictures (pic), shapes (wps), and nested groups (wpg) behind a
353
+ * shared coordinate space (grpSpPr chOff/chExt → extent). renderHTML lays each
354
+ * child out at its transformed position/size so the group renders as Word draws
355
+ * it (e.g. a row of colored decoration bars), instead of a single picture.
356
+ */
357
+ const EMU_PER_PT = 12700;
358
+ const EMU_PER_PX = 9525;
359
+ /** Solid fill → CSS color (noFill/none/gradient → undefined). */
360
+ function fillToCss(fill) {
361
+ if (!fill || fill.type !== "solid") return void 0;
362
+ return normalizeColorToHex(fill.color?.value);
363
+ }
364
+ /** Shape outline → CSS border (EMU width → pt). noFill → undefined. */
365
+ function outlineToCss(outline) {
366
+ if (!outline || outline.type === "noFill" || outline.type === "none") return void 0;
367
+ const color = normalizeColorToHex(outline.color?.value) ?? "black";
368
+ return `border:${outline.width != null ? `${outline.width / EMU_PER_PT}pt` : "0.75pt"} ${outline.dash === "sysDot" || outline.dash === "sysDash" ? "dashed" : "solid"} ${color}`;
369
+ }
370
+ /** Group extent in px: top-level groups carry {width,height}; nested wpg children
371
+ * carry a ChildTransformation whose pixels/emus give the size. */
372
+ function groupExtent(group) {
373
+ const t = group.transformation;
374
+ return {
375
+ w: t?.width != null ? convertEmuToPixels(t.width) : t?.pixels?.x ?? 0,
376
+ h: t?.height != null ? convertEmuToPixels(t.height) : t?.pixels?.y ?? 0
377
+ };
378
+ }
379
+ /** Child coord (EMU, group's chOff/chExt space) → group-local px. */
380
+ function childBox(child, chOff, scaleX, scaleY) {
381
+ const t = child.transformation;
382
+ const off = t?.offset?.emus ?? {
383
+ x: 0,
384
+ y: 0
385
+ };
386
+ const size = t?.emus ?? {
387
+ x: 0,
388
+ y: 0
389
+ };
390
+ return {
391
+ x: (off.x - chOff.x) * scaleX,
392
+ y: (off.y - chOff.y) * scaleY,
393
+ w: size.x * scaleX,
394
+ h: size.y * scaleY
395
+ };
396
+ }
397
+ /** pic media type + raw bytes → data URL (bytes may arrive as Uint8Array or a
398
+ * plain object of byte values across the JSON boundary). */
399
+ function picSrc(mediaType, data) {
400
+ if (!data) return null;
401
+ return `data:image/${mediaType};base64,${encodeBase64(data instanceof Uint8Array ? data : new Uint8Array(Object.values(data)))}`;
402
+ }
403
+ function renderChild(child, chOff, scaleX, scaleY) {
404
+ const box = childBox(child, chOff, scaleX, scaleY);
405
+ const base = `position:absolute;left:${box.x}px;top:${box.y}px;width:${box.w}px;height:${box.h}px;box-sizing:border-box`;
406
+ if (child.type === "wpg") return renderGroup(child, box.w, box.h, base);
407
+ if (child.type === "wps") {
408
+ const data = child.data ?? {};
409
+ return renderWpsInterior(data, base, {
410
+ rotation: data.bodyProperties?.rotation ?? child.transformation?.rotation,
411
+ attrs: { "data-wpg-wps": "" }
412
+ });
413
+ }
414
+ if (child.type) {
415
+ const src = picSrc(child.type, child.data);
416
+ if (src) return ["img", {
417
+ src,
418
+ alt: "",
419
+ style: base
420
+ }];
421
+ }
422
+ return [];
423
+ }
424
+ /** wps text-body paragraphs (office-open ParagraphOptions[]) → inline HTML runs
425
+ * so a text-box shape shows its text. Each paragraph's run defaults merge with
426
+ * each text run; renderRunStyles converts font/color/size/… to CSS. Advanced run
427
+ * props (shadow w14RawXml, kern) are carried in attrs but not rendered. */
428
+ function renderWpsText(children) {
429
+ if (!Array.isArray(children)) return [];
430
+ return children.map((para) => {
431
+ if (typeof para === "string") return [
432
+ "p",
433
+ { style: "margin:0" },
434
+ para
435
+ ];
436
+ const defaultRun = para.run ?? {};
437
+ const runs = (para.children ?? []).map((r) => {
438
+ if (typeof r === "string") return r;
439
+ const css = renderRunStyles({
440
+ ...defaultRun,
441
+ ...r
442
+ });
443
+ return [
444
+ "span",
445
+ css.length ? { style: css.join(";") } : {},
446
+ r.text ?? ""
447
+ ];
448
+ });
449
+ return [
450
+ "p",
451
+ { style: para.alignment ? `margin:0;text-align:${para.alignment}` : "margin:0" },
452
+ ...runs
453
+ ];
454
+ });
455
+ }
456
+ /**
457
+ * Render a wps shape's interior (fill/outline/insets/rotation/writing-mode + text
458
+ * body) as a positioned div. Shared by the wpg group's inline wps children and
459
+ * the standalone wpsShape node, so a text-box shape renders identically whether
460
+ * it floats alone (wp:anchor > wps:wsp) or sits inside a group. `positionStyle`
461
+ * carries the placement — absolute group coords for a group child, the floating
462
+ * anchor CSS for a standalone shape. `opts.rotation` overrides bodyPr rotation
463
+ * (a group child's rotation may come from the group transform, not bodyPr).
464
+ */
465
+ function renderWpsInterior(data, positionStyle, opts) {
466
+ const styles = [positionStyle];
467
+ const fill = fillToCss(data.fill);
468
+ if (fill) styles.push(`background-color:${fill}`);
469
+ const outline = outlineToCss(data.outline);
470
+ if (outline) styles.push(outline);
471
+ const bp = data.bodyProperties;
472
+ if (bp) {
473
+ const ins = (v) => v != null ? `${(v / EMU_PER_PX).toFixed(1)}px` : "0px";
474
+ styles.push(`padding:${ins(bp.tIns)} ${ins(bp.rIns)} ${ins(bp.bIns)} ${ins(bp.lIns)}`);
475
+ }
476
+ const rotation = opts?.rotation ?? bp?.rotation;
477
+ if (rotation) styles.push(`transform:rotate(${rotation}deg)`);
478
+ const vert = bp?.vert;
479
+ if (vert && vert !== "horz") styles.push(`writing-mode:${vert === "vert270" ? "vertical-lr" : "vertical-rl"}`);
480
+ const attrs = { style: styles.join(";") };
481
+ if (opts?.attrs) Object.assign(attrs, opts.attrs);
482
+ return [
483
+ "div",
484
+ attrs,
485
+ ...renderWpsText(data.children)
486
+ ];
487
+ }
488
+ /** Render a wpg group (top-level or nested) as a positioned container.
489
+ * actualW/H are the group's real pixel size — top-level groups read them from
490
+ * transformation.width/height; nested groups receive the box their parent
491
+ * transform mapped them to. containerStyle (when set) carries the absolute
492
+ * placement for a nested group; otherwise the group is an inline-block. */
493
+ function renderGroup(group, actualW, actualH, containerStyle, extraAttrs) {
494
+ const chOff = group.childOffset ?? {
495
+ x: 0,
496
+ y: 0
497
+ };
498
+ const chExt = group.childExtent ?? {
499
+ cx: 1,
500
+ cy: 1
501
+ };
502
+ const scaleX = actualW / chExt.cx;
503
+ const scaleY = actualH / chExt.cy;
504
+ const style = containerStyle ?? `position:relative;display:inline-block;vertical-align:middle;width:${actualW}px;height:${actualH}px`;
505
+ const children = (group.children ?? []).map((c) => renderChild(c, chOff, scaleX, scaleY)).filter((c) => Array.isArray(c) && c.length > 0);
506
+ const attrs = {
507
+ "data-wpg-group": "",
508
+ style
509
+ };
510
+ if (extraAttrs) Object.assign(attrs, extraAttrs);
511
+ return [
512
+ "span",
513
+ attrs,
514
+ ...children
515
+ ];
516
+ }
517
+ const attrWpgGroup = () => ({
518
+ default: null,
519
+ rendered: false,
520
+ parseHTML: (element) => {
521
+ const raw = element.getAttribute("data-wpg-group");
522
+ if (!raw) return null;
523
+ try {
524
+ return JSON.parse(raw);
525
+ } catch {
526
+ return null;
527
+ }
528
+ }
529
+ });
530
+ const WpgGroup = Node.create({
531
+ name: "wpgGroup",
532
+ group: "inline",
533
+ inline: true,
534
+ atom: true,
535
+ addAttributes() {
536
+ return { wpgGroup: attrWpgGroup() };
537
+ },
538
+ parseHTML() {
539
+ return [{ tag: "span[data-wpg-group]" }];
540
+ },
541
+ renderHTML({ node }) {
542
+ const wpg = node.attrs.wpgGroup ?? {};
543
+ const { w, h } = groupExtent(wpg);
544
+ if (wpg.floating) return renderGroup(wpg, w, h, [
545
+ ...floatingToStyles(wpg.floating, void 0, w),
546
+ `width:${w}px`,
547
+ `height:${h}px`
548
+ ].join(";"), floatAnchorScope(wpg.floating) === "paragraph" ? { "data-float-anchor": "paragraph" } : void 0);
549
+ return renderGroup(wpg, w, h);
550
+ }
551
+ });
552
+ //#endregion
553
+ //#region src/extensions/wps-shape.ts
554
+ const attrWpsShape = () => ({
555
+ default: null,
556
+ rendered: false,
557
+ parseHTML: (element) => {
558
+ const raw = element.getAttribute("data-wps-shape");
559
+ if (!raw) return null;
560
+ try {
561
+ return JSON.parse(raw);
562
+ } catch {
563
+ return null;
564
+ }
565
+ }
566
+ });
567
+ const WpsShape = Node.create({
568
+ name: "wpsShape",
569
+ group: "inline",
570
+ inline: true,
571
+ atom: true,
572
+ addAttributes() {
573
+ return { wpsShape: attrWpsShape() };
574
+ },
575
+ parseHTML() {
576
+ return [{ tag: "div[data-wps-shape]" }];
577
+ },
578
+ renderHTML({ node }) {
579
+ const ws = node.attrs.wpsShape ?? {};
580
+ 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`;
581
+ if (ws.floating) {
582
+ const pos = [...floatingToStyles(ws.floating, void 0, ws.transformation?.width), sizeStyle].join(";");
583
+ const attrs = { "data-wps-shape": "" };
584
+ if (floatAnchorScope(ws.floating) === "paragraph") attrs["data-float-anchor"] = "paragraph";
585
+ return renderWpsInterior(ws, pos, { attrs });
586
+ }
587
+ return renderWpsInterior(ws, `display:inline-block;vertical-align:middle;${sizeStyle}`, { attrs: { "data-wps-shape": "" } });
588
+ }
589
+ });
590
+ //#endregion
591
+ //#region src/extensions/extensions.ts
592
+ const tiptapNodeExtensions = [
593
+ Document,
594
+ Paragraph,
595
+ Text,
596
+ HardBreak,
597
+ PageBreak,
598
+ ColumnBreak,
599
+ SectionBreak,
600
+ Passthrough,
601
+ Blockquote,
602
+ OrderedList,
603
+ BulletList,
604
+ ListItem,
605
+ CodeBlock.configure({ lowlight: createLowlight(all) }),
606
+ Details,
607
+ DetailsSummary,
608
+ DetailsContent,
609
+ Emoji,
610
+ HorizontalRule,
611
+ Image.configure({ inline: true }),
612
+ WpgGroup,
613
+ WpsShape,
614
+ Mathematics,
615
+ Mention,
616
+ Table,
617
+ TableRow,
618
+ TableCell,
619
+ TableHeader,
620
+ TaskList,
621
+ TaskItem,
622
+ Heading,
623
+ TextAlign$1.configure({ types: ["heading", "paragraph"] })
624
+ ];
625
+ const tiptapMarkExtensions = [
626
+ Bold,
627
+ Code,
628
+ Highlight,
629
+ Italic,
630
+ Link,
631
+ Strike,
632
+ Subscript,
633
+ Superscript,
634
+ TextStyle,
635
+ Underline
636
+ ];
637
+ const docxExtensions = [
638
+ ...tiptapNodeExtensions,
639
+ ...tiptapMarkExtensions,
640
+ FormattingMarks
641
+ ];
642
+ const DocxKit = Extension.create({
643
+ name: "docxKit",
644
+ addExtensions() {
645
+ const extensions = [];
646
+ if (this.options.bold !== false) extensions.push(Bold.configure(this.options.bold));
647
+ if (this.options.blockquote !== false) extensions.push(Blockquote.configure(this.options.blockquote));
648
+ if (this.options.bulletList !== false) extensions.push(BulletList.configure(this.options.bulletList));
649
+ if (this.options.code !== false) extensions.push(Code.configure(this.options.code));
650
+ if (this.options.codeBlock !== false) extensions.push(CodeBlockLowlight.configure({
651
+ lowlight: createLowlight(all),
652
+ ...this.options.codeBlock
653
+ }));
654
+ if (this.options.document !== false) extensions.push(Document);
655
+ if (this.options.hardBreak !== false) extensions.push(HardBreak.configure(this.options.hardBreak));
656
+ if (this.options.heading !== false) extensions.push(Heading.configure(this.options.heading));
657
+ if (this.options.horizontalRule !== false) extensions.push(HorizontalRule.configure(this.options.horizontalRule));
658
+ if (this.options.italic !== false) extensions.push(Italic.configure(this.options.italic));
659
+ if (this.options.listItem !== false) extensions.push(ListItem.configure(this.options.listItem));
660
+ if (this.options.link !== false) extensions.push(Link.configure(this.options.link));
661
+ if (this.options.orderedList !== false) extensions.push(OrderedList.configure(this.options.orderedList));
662
+ if (this.options.paragraph !== false) extensions.push(Paragraph.configure(this.options.paragraph));
663
+ if (this.options.strike !== false) extensions.push(Strike.configure(this.options.strike));
664
+ if (this.options.text !== false) extensions.push(Text);
665
+ if (this.options.underline !== false) extensions.push(Underline.configure(this.options.underline));
666
+ return extensions;
667
+ }
668
+ });
669
+ //#endregion
670
+ export { FormattingMarks as _, DocxKit as a, tiptapMarkExtensions as c, WpgGroup as d, renderWpsInterior as f, PageBreak as g, Passthrough as h, Node as i, tiptapNodeExtensions as l, SectionBreak as m, Extension as n, TextAlign as o, renderWpsText as p, Mark as r, docxExtensions as s, Editor as t, WpsShape as u, ColumnBreak as v };
package/dist/core.d.mts CHANGED
@@ -1,2 +1,2 @@
1
- import { a as JSONContent, d as docxExtensions, i as Extensions, n as Editor, o as Mark, r as Extension, s as Node, t as AnyExtension } from "./core-fBMCCj4D.mjs";
1
+ import { a as JSONContent, f as docxExtensions, i as Extensions, n as Editor, o as Mark, r as Extension, s as Node, t as AnyExtension } from "./core-C9VunJ8o.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, r as Mark, s as docxExtensions, t as Editor } from "./core-DEqBHRzE.mjs";
1
+ import { i as Node, n as Extension, r as Mark, s as docxExtensions, t as Editor } from "./core-DT9IrTzN.mjs";
2
2
  export { Editor, Extension, Mark, Node, docxExtensions };