@beyondwork/docx-react-component 1.0.1 → 1.0.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 (172) hide show
  1. package/README.md +44 -104
  2. package/package.json +76 -46
  3. package/src/README.md +85 -0
  4. package/src/api/README.md +22 -0
  5. package/src/api/public-types.ts +525 -0
  6. package/src/compare/diff-engine.ts +530 -0
  7. package/src/compare/export-redlines.ts +162 -0
  8. package/src/compare/snapshot.ts +37 -0
  9. package/src/component-inventory.md +99 -0
  10. package/src/core/README.md +10 -0
  11. package/src/core/commands/README.md +3 -0
  12. package/src/core/commands/formatting-commands.ts +161 -0
  13. package/src/core/commands/image-commands.ts +144 -0
  14. package/src/core/commands/index.ts +1013 -0
  15. package/src/core/commands/list-commands.ts +370 -0
  16. package/src/core/commands/review-commands.ts +108 -0
  17. package/src/core/commands/text-commands.ts +119 -0
  18. package/src/core/schema/README.md +3 -0
  19. package/src/core/schema/text-schema.ts +512 -0
  20. package/src/core/selection/README.md +3 -0
  21. package/src/core/selection/mapping.ts +238 -0
  22. package/src/core/selection/review-anchors.ts +94 -0
  23. package/src/core/state/README.md +3 -0
  24. package/src/core/state/editor-state.ts +580 -0
  25. package/src/core/state/text-transaction.ts +276 -0
  26. package/src/formats/xlsx/io/parse-shared-strings.ts +41 -0
  27. package/src/formats/xlsx/io/parse-sheet.ts +289 -0
  28. package/src/formats/xlsx/io/parse-styles.ts +57 -0
  29. package/src/formats/xlsx/io/parse-workbook.ts +75 -0
  30. package/src/formats/xlsx/io/xlsx-session.ts +306 -0
  31. package/src/formats/xlsx/model/cell.ts +189 -0
  32. package/src/formats/xlsx/model/sheet.ts +244 -0
  33. package/src/formats/xlsx/model/styles.ts +118 -0
  34. package/src/formats/xlsx/model/workbook.ts +449 -0
  35. package/src/index.ts +45 -0
  36. package/src/io/README.md +10 -0
  37. package/src/io/docx-session.ts +1763 -0
  38. package/src/io/export/README.md +3 -0
  39. package/src/io/export/export-session.ts +165 -0
  40. package/src/io/export/minimal-docx.ts +115 -0
  41. package/src/io/export/reattach-preserved-parts.ts +54 -0
  42. package/src/io/export/serialize-comments.ts +876 -0
  43. package/src/io/export/serialize-footnotes.ts +217 -0
  44. package/src/io/export/serialize-headers-footers.ts +200 -0
  45. package/src/io/export/serialize-main-document.ts +982 -0
  46. package/src/io/export/serialize-numbering.ts +97 -0
  47. package/src/io/export/serialize-revisions.ts +389 -0
  48. package/src/io/export/serialize-runtime-revisions.ts +265 -0
  49. package/src/io/export/serialize-tables.ts +147 -0
  50. package/src/io/export/split-review-boundaries.ts +194 -0
  51. package/src/io/normalize/README.md +3 -0
  52. package/src/io/normalize/normalize-text.ts +437 -0
  53. package/src/io/ooxml/README.md +3 -0
  54. package/src/io/ooxml/parse-comments.ts +779 -0
  55. package/src/io/ooxml/parse-complex-content.ts +287 -0
  56. package/src/io/ooxml/parse-fields.ts +438 -0
  57. package/src/io/ooxml/parse-footnotes.ts +403 -0
  58. package/src/io/ooxml/parse-headers-footers.ts +483 -0
  59. package/src/io/ooxml/parse-inline-media.ts +431 -0
  60. package/src/io/ooxml/parse-main-document.ts +1846 -0
  61. package/src/io/ooxml/parse-numbering.ts +425 -0
  62. package/src/io/ooxml/parse-revisions.ts +658 -0
  63. package/src/io/ooxml/parse-shapes.ts +271 -0
  64. package/src/io/ooxml/parse-tables.ts +568 -0
  65. package/src/io/ooxml/parse-theme.ts +314 -0
  66. package/src/io/ooxml/part-manifest.ts +136 -0
  67. package/src/io/ooxml/revision-boundaries.ts +351 -0
  68. package/src/io/opc/README.md +3 -0
  69. package/src/io/opc/corrupt-package.ts +166 -0
  70. package/src/io/opc/docx-package.ts +74 -0
  71. package/src/io/opc/package-reader.ts +320 -0
  72. package/src/io/opc/package-writer.ts +273 -0
  73. package/src/legal/bookmarks.ts +196 -0
  74. package/src/legal/cross-references.ts +356 -0
  75. package/src/legal/defined-terms.ts +203 -0
  76. package/src/model/README.md +3 -0
  77. package/src/model/canonical-document.ts +1911 -0
  78. package/src/model/cds-1.0.0.ts +196 -0
  79. package/src/model/snapshot.ts +393 -0
  80. package/src/preservation/README.md +3 -0
  81. package/src/preservation/markup-compatibility.ts +48 -0
  82. package/src/preservation/opaque-fragment-store.ts +89 -0
  83. package/src/preservation/opaque-region.ts +233 -0
  84. package/src/preservation/package-preservation.ts +120 -0
  85. package/src/preservation/preserved-part-manifest.ts +56 -0
  86. package/src/preservation/relationship-retention.ts +57 -0
  87. package/src/preservation/store.ts +185 -0
  88. package/src/review/README.md +16 -0
  89. package/src/review/store/README.md +3 -0
  90. package/src/review/store/comment-anchors.ts +70 -0
  91. package/src/review/store/comment-remapping.ts +154 -0
  92. package/src/review/store/comment-store.ts +331 -0
  93. package/src/review/store/comment-thread.ts +109 -0
  94. package/src/review/store/revision-actions.ts +394 -0
  95. package/src/review/store/revision-store.ts +303 -0
  96. package/src/review/store/revision-types.ts +168 -0
  97. package/src/review/store/runtime-comment-store.ts +43 -0
  98. package/src/runtime/README.md +3 -0
  99. package/src/runtime/ai-action-policy.ts +764 -0
  100. package/src/runtime/document-runtime.ts +967 -0
  101. package/src/runtime/read-only-diagnostics-runtime.ts +232 -0
  102. package/src/runtime/review-runtime.ts +44 -0
  103. package/src/runtime/revision-runtime.ts +107 -0
  104. package/src/runtime/session-capabilities.ts +138 -0
  105. package/src/runtime/surface-projection.ts +570 -0
  106. package/src/runtime/table-commands.ts +87 -0
  107. package/src/runtime/table-schema.ts +140 -0
  108. package/src/runtime/virtualized-rendering.ts +258 -0
  109. package/src/ui/README.md +30 -0
  110. package/src/ui/WordReviewEditor.tsx +1504 -0
  111. package/src/ui/comments/README.md +3 -0
  112. package/src/ui/compatibility/README.md +3 -0
  113. package/src/ui/editor-surface/README.md +3 -0
  114. package/src/ui/headless/comment-decoration-model.ts +124 -0
  115. package/src/ui/headless/revision-decoration-model.ts +128 -0
  116. package/src/ui/headless/selection-helpers.ts +34 -0
  117. package/src/ui/headless/use-editor-keyboard.ts +98 -0
  118. package/src/ui/review/README.md +3 -0
  119. package/src/ui/shared/revision-filters.ts +31 -0
  120. package/src/ui/status/README.md +3 -0
  121. package/src/ui/theme/README.md +3 -0
  122. package/src/ui/toolbar/README.md +3 -0
  123. package/src/ui-tailwind/chrome/tw-alert-banner.tsx +48 -0
  124. package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +44 -0
  125. package/src/ui-tailwind/chrome/tw-unsaved-modal.tsx +58 -0
  126. package/src/ui-tailwind/chrome/use-before-unload.ts +20 -0
  127. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +139 -0
  128. package/src/ui-tailwind/editor-surface/pm-decorations.ts +98 -0
  129. package/src/ui-tailwind/editor-surface/pm-position-map.ts +123 -0
  130. package/src/ui-tailwind/editor-surface/pm-schema.ts +452 -0
  131. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +327 -0
  132. package/src/ui-tailwind/editor-surface/search-plugin.ts +157 -0
  133. package/src/ui-tailwind/editor-surface/tw-caret.tsx +12 -0
  134. package/src/ui-tailwind/editor-surface/tw-editor-surface.tsx +150 -0
  135. package/src/ui-tailwind/editor-surface/tw-inline-token.tsx +118 -0
  136. package/src/ui-tailwind/editor-surface/tw-opaque-block.tsx +52 -0
  137. package/src/ui-tailwind/editor-surface/tw-paragraph-block.tsx +151 -0
  138. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +215 -0
  139. package/src/ui-tailwind/editor-surface/tw-segment-view.tsx +111 -0
  140. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +122 -0
  141. package/src/ui-tailwind/index.ts +61 -0
  142. package/src/ui-tailwind/review/tw-comment-sidebar.tsx +276 -0
  143. package/src/ui-tailwind/review/tw-health-panel.tsx +120 -0
  144. package/src/ui-tailwind/review/tw-review-rail.tsx +120 -0
  145. package/src/ui-tailwind/review/tw-revision-sidebar.tsx +164 -0
  146. package/src/ui-tailwind/status/tw-status-bar.tsx +58 -0
  147. package/src/ui-tailwind/theme/editor-theme.css +190 -0
  148. package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +48 -0
  149. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +231 -0
  150. package/src/ui-tailwind/tw-review-workspace.tsx +140 -0
  151. package/src/validation/README.md +3 -0
  152. package/src/validation/compatibility-engine.ts +317 -0
  153. package/src/validation/compatibility-report.ts +160 -0
  154. package/src/validation/diagnostics.ts +203 -0
  155. package/src/validation/import-diagnostics.ts +128 -0
  156. package/src/validation/low-priority-word-surfaces.ts +373 -0
  157. package/dist/chunk-32W6IVQE.js +0 -7725
  158. package/dist/chunk-32W6IVQE.js.map +0 -1
  159. package/dist/index.cjs +0 -23722
  160. package/dist/index.cjs.map +0 -1
  161. package/dist/index.d.cts +0 -7
  162. package/dist/index.d.ts +0 -7
  163. package/dist/index.js +0 -16011
  164. package/dist/index.js.map +0 -1
  165. package/dist/public-types-DqCURAz8.d.cts +0 -1152
  166. package/dist/public-types-DqCURAz8.d.ts +0 -1152
  167. package/dist/tailwind.cjs +0 -8295
  168. package/dist/tailwind.cjs.map +0 -1
  169. package/dist/tailwind.d.cts +0 -323
  170. package/dist/tailwind.d.ts +0 -323
  171. package/dist/tailwind.js +0 -553
  172. package/dist/tailwind.js.map +0 -1
@@ -0,0 +1,570 @@
1
+ import type {
2
+ EditorSurfaceSnapshot,
3
+ SurfaceBlockSnapshot,
4
+ SurfaceInlineSegment,
5
+ SurfaceTableCellSnapshot,
6
+ SurfaceTableRowSnapshot,
7
+ } from "../api/public-types";
8
+ import type {
9
+ CanonicalDocumentEnvelope,
10
+ SelectionSnapshot,
11
+ } from "../core/state/editor-state.ts";
12
+ import type {
13
+ BlockNode,
14
+ ChartPreviewNode,
15
+ DocumentRootNode,
16
+ InlineNode,
17
+ ParagraphNode,
18
+ SdtNode,
19
+ ShapeNode,
20
+ SmartArtPreviewNode,
21
+ TableNode,
22
+ TextMark,
23
+ VmlShapeNode,
24
+ WordArtNode,
25
+ } from "../model/canonical-document.ts";
26
+ import {
27
+ describeOpaqueFragment,
28
+ getOpaqueFragment,
29
+ } from "../preservation/store.ts";
30
+
31
+ interface ParagraphAccumulator {
32
+ blockId: string;
33
+ kind: "paragraph";
34
+ from: number;
35
+ to: number;
36
+ styleId?: string;
37
+ numbering?: ParagraphNode["numbering"];
38
+ segments: SurfaceInlineSegment[];
39
+ }
40
+
41
+ export function createEditorSurfaceSnapshot(
42
+ document: CanonicalDocumentEnvelope,
43
+ _selection: SelectionSnapshot,
44
+ ): EditorSurfaceSnapshot {
45
+ const root = normalizeDocumentRoot(document.content);
46
+ const blocks: SurfaceBlockSnapshot[] = [];
47
+ const lockedFragmentIds: string[] = [];
48
+ let cursor = 0;
49
+ const counters = {
50
+ paragraph: 0,
51
+ table: 0,
52
+ opaque: 0,
53
+ sdt: 0,
54
+ customXml: 0,
55
+ altChunk: 0,
56
+ };
57
+
58
+ for (let index = 0; index < root.children.length; index += 1) {
59
+ const surfaceBlock = createSurfaceBlock(root.children[index], document, cursor, counters);
60
+ blocks.push(surfaceBlock.block);
61
+ lockedFragmentIds.push(...surfaceBlock.lockedFragmentIds);
62
+ cursor = surfaceBlock.nextCursor;
63
+ if (index < root.children.length - 1 && root.children[index + 1]?.type === "paragraph") {
64
+ cursor += 1;
65
+ }
66
+ }
67
+
68
+ return {
69
+ storySize: cursor,
70
+ plainText: createPlainText(blocks),
71
+ blocks,
72
+ lockedFragmentIds,
73
+ };
74
+ }
75
+
76
+ function createSurfaceBlock(
77
+ block: BlockNode,
78
+ document: CanonicalDocumentEnvelope,
79
+ cursor: number,
80
+ counters: {
81
+ paragraph: number;
82
+ table: number;
83
+ opaque: number;
84
+ sdt: number;
85
+ customXml: number;
86
+ altChunk: number;
87
+ },
88
+ ): { block: SurfaceBlockSnapshot; lockedFragmentIds: string[]; nextCursor: number } {
89
+ if (block.type === "opaque_block") {
90
+ const fragment = getOpaqueFragment(document.preservation as never, block.fragmentId);
91
+ const descriptor = fragment ? describeOpaqueFragment(fragment) : null;
92
+ const blockId = `opaque-${counters.opaque}`;
93
+ counters.opaque += 1;
94
+ return {
95
+ block: {
96
+ blockId,
97
+ kind: "opaque_block",
98
+ from: cursor,
99
+ to: cursor + 1,
100
+ fragmentId: block.fragmentId,
101
+ warningId: block.warningId,
102
+ label: descriptor?.label ?? "Unsupported OOXML fragment",
103
+ detail:
104
+ descriptor?.detail ??
105
+ "Locked whole-unit to keep unsupported OOXML intact through export.",
106
+ state: "locked-preserve-only",
107
+ },
108
+ lockedFragmentIds: [block.fragmentId],
109
+ nextCursor: cursor + 1,
110
+ };
111
+ }
112
+
113
+ if (block.type === "table") {
114
+ const tableIndex = counters.table;
115
+ counters.table += 1;
116
+ return createTableBlock(tableIndex, block, document, cursor, counters);
117
+ }
118
+
119
+ if (block.type === "sdt") {
120
+ const sdtIndex = counters.sdt;
121
+ counters.sdt += 1;
122
+ return createSdtBlock(sdtIndex, block, document, cursor, counters);
123
+ }
124
+
125
+ if (block.type === "custom_xml") {
126
+ const blockId = `custom-xml-${counters.customXml}`;
127
+ counters.customXml += 1;
128
+ return {
129
+ block: {
130
+ blockId,
131
+ kind: "opaque_block",
132
+ from: cursor,
133
+ to: cursor + 1,
134
+ fragmentId: blockId,
135
+ warningId: blockId,
136
+ label: "Custom XML block",
137
+ detail:
138
+ block.uri || block.element
139
+ ? `Custom XML wrapper ${[block.element, block.uri].filter(Boolean).join(" ")} preserved as a read-only block.`
140
+ : "Custom XML wrapper preserved as a read-only block.",
141
+ state: "locked-preserve-only",
142
+ },
143
+ lockedFragmentIds: [],
144
+ nextCursor: cursor + 1,
145
+ };
146
+ }
147
+
148
+ if (block.type === "alt_chunk") {
149
+ const blockId = `alt-chunk-${counters.altChunk}`;
150
+ counters.altChunk += 1;
151
+ return {
152
+ block: {
153
+ blockId,
154
+ kind: "opaque_block",
155
+ from: cursor,
156
+ to: cursor + 1,
157
+ fragmentId: blockId,
158
+ warningId: blockId,
159
+ label: "AltChunk import",
160
+ detail: `Alternate content import remains read-only through relationship ${block.relationshipId}.`,
161
+ state: "locked-preserve-only",
162
+ },
163
+ lockedFragmentIds: [],
164
+ nextCursor: cursor + 1,
165
+ };
166
+ }
167
+
168
+ const paragraphIndex = counters.paragraph;
169
+ counters.paragraph += 1;
170
+ return createParagraphBlock(paragraphIndex, block, document, cursor);
171
+ }
172
+
173
+ function createTableBlock(
174
+ tableIndex: number,
175
+ table: TableNode,
176
+ document: CanonicalDocumentEnvelope,
177
+ cursor: number,
178
+ counters: {
179
+ paragraph: number;
180
+ table: number;
181
+ opaque: number;
182
+ sdt: number;
183
+ customXml: number;
184
+ altChunk: number;
185
+ },
186
+ ): { block: SurfaceBlockSnapshot; lockedFragmentIds: string[]; nextCursor: number } {
187
+ const lockedFragmentIds: string[] = [];
188
+ let innerCursor = cursor;
189
+ const rows: SurfaceTableRowSnapshot[] = [];
190
+
191
+ for (const row of table.rows) {
192
+ const cells: SurfaceTableCellSnapshot[] = [];
193
+ for (const cell of row.cells) {
194
+ const cellContent: SurfaceBlockSnapshot[] = [];
195
+ for (const child of cell.children) {
196
+ const result = createSurfaceBlock(child, document, innerCursor, counters);
197
+ cellContent.push(result.block);
198
+ lockedFragmentIds.push(...result.lockedFragmentIds);
199
+ innerCursor = result.nextCursor;
200
+ }
201
+ cells.push({
202
+ gridSpan: cell.gridSpan ?? 1,
203
+ verticalMerge: cell.verticalMerge ?? null,
204
+ colspan: cell.gridSpan ?? 1,
205
+ rowspan: cell.verticalMerge === "restart" ? 1 : 1,
206
+ content: cellContent,
207
+ });
208
+ }
209
+ rows.push({ cells });
210
+ }
211
+
212
+ return {
213
+ block: {
214
+ blockId: `table-${tableIndex}`,
215
+ kind: "table",
216
+ from: cursor,
217
+ to: innerCursor,
218
+ styleId: table.styleId,
219
+ gridColumns: table.gridColumns,
220
+ rows,
221
+ },
222
+ lockedFragmentIds,
223
+ nextCursor: innerCursor,
224
+ };
225
+ }
226
+
227
+ function createSdtBlock(
228
+ sdtIndex: number,
229
+ block: SdtNode,
230
+ document: CanonicalDocumentEnvelope,
231
+ cursor: number,
232
+ counters: {
233
+ paragraph: number;
234
+ table: number;
235
+ opaque: number;
236
+ sdt: number;
237
+ customXml: number;
238
+ altChunk: number;
239
+ },
240
+ ): { block: SurfaceBlockSnapshot; lockedFragmentIds: string[]; nextCursor: number } {
241
+ const children: SurfaceBlockSnapshot[] = [];
242
+ const lockedFragmentIds: string[] = [];
243
+ let innerCursor = cursor;
244
+
245
+ for (const child of block.children) {
246
+ const result = createSurfaceBlock(child, document, innerCursor, counters);
247
+ children.push(result.block);
248
+ lockedFragmentIds.push(...result.lockedFragmentIds);
249
+ innerCursor = result.nextCursor;
250
+ }
251
+
252
+ return {
253
+ block: {
254
+ blockId: `sdt-${sdtIndex}`,
255
+ kind: "sdt_block",
256
+ from: cursor,
257
+ to: innerCursor,
258
+ ...(block.properties.sdtType ? { sdtType: block.properties.sdtType } : {}),
259
+ ...(block.properties.alias ? { alias: block.properties.alias } : {}),
260
+ ...(block.properties.tag ? { tag: block.properties.tag } : {}),
261
+ ...(block.properties.lock ? { lock: block.properties.lock } : {}),
262
+ children,
263
+ },
264
+ lockedFragmentIds,
265
+ nextCursor: innerCursor,
266
+ };
267
+ }
268
+
269
+ function createParagraphBlock(
270
+ paragraphIndex: number,
271
+ paragraph: ParagraphNode,
272
+ document: CanonicalDocumentEnvelope,
273
+ start: number,
274
+ ): {
275
+ block: SurfaceBlockSnapshot;
276
+ nextCursor: number;
277
+ lockedFragmentIds: string[];
278
+ } {
279
+ const accumulator: ParagraphAccumulator = {
280
+ blockId: `paragraph-${paragraphIndex}`,
281
+ kind: "paragraph",
282
+ from: start,
283
+ to: start,
284
+ ...(paragraph.styleId ? { styleId: paragraph.styleId } : {}),
285
+ ...(paragraph.numbering ? { numbering: paragraph.numbering } : {}),
286
+ segments: [],
287
+ };
288
+ const lockedFragmentIds: string[] = [];
289
+ let cursor = start;
290
+ const children = Array.isArray(paragraph.children) ? paragraph.children : [];
291
+
292
+ for (const child of children) {
293
+ const result = appendInlineSegments(accumulator, child, document, cursor);
294
+ cursor = result.nextCursor;
295
+ lockedFragmentIds.push(...result.lockedFragmentIds);
296
+ }
297
+
298
+ accumulator.to = cursor;
299
+ return {
300
+ block: accumulator,
301
+ nextCursor: cursor,
302
+ lockedFragmentIds,
303
+ };
304
+ }
305
+
306
+ function appendInlineSegments(
307
+ paragraph: ParagraphAccumulator,
308
+ node: InlineNode,
309
+ document: CanonicalDocumentEnvelope,
310
+ start: number,
311
+ hyperlinkHref?: string,
312
+ ): { nextCursor: number; lockedFragmentIds: string[] } {
313
+ switch (node.type) {
314
+ case "text":
315
+ paragraph.segments.push({
316
+ segmentId: `${paragraph.blockId}-segment-${paragraph.segments.length}`,
317
+ kind: "text",
318
+ from: start,
319
+ to: start + Array.from(node.text).length,
320
+ text: node.text,
321
+ ...(node.marks ? { marks: cloneMarks(node.marks) } : {}),
322
+ ...(hyperlinkHref ? { hyperlinkHref } : {}),
323
+ });
324
+ return { nextCursor: start + Array.from(node.text).length, lockedFragmentIds: [] };
325
+ case "tab":
326
+ paragraph.segments.push({
327
+ segmentId: `${paragraph.blockId}-segment-${paragraph.segments.length}`,
328
+ kind: "tab",
329
+ from: start,
330
+ to: start + 1,
331
+ ...(hyperlinkHref ? { hyperlinkHref } : {}),
332
+ });
333
+ return { nextCursor: start + 1, lockedFragmentIds: [] };
334
+ case "hard_break":
335
+ paragraph.segments.push({
336
+ segmentId: `${paragraph.blockId}-segment-${paragraph.segments.length}`,
337
+ kind: "hard_break",
338
+ from: start,
339
+ to: start + 1,
340
+ ...(hyperlinkHref ? { hyperlinkHref } : {}),
341
+ });
342
+ return { nextCursor: start + 1, lockedFragmentIds: [] };
343
+ case "hyperlink": {
344
+ let cursor = start;
345
+ for (const child of node.children) {
346
+ const result = appendInlineSegments(paragraph, child, document, cursor, node.href);
347
+ cursor = result.nextCursor;
348
+ }
349
+ return { nextCursor: cursor, lockedFragmentIds: [] };
350
+ }
351
+ case "image":
352
+ paragraph.segments.push({
353
+ segmentId: `${paragraph.blockId}-segment-${paragraph.segments.length}`,
354
+ kind: "image",
355
+ from: start,
356
+ to: start + 1,
357
+ mediaId: node.mediaId,
358
+ altText: node.altText,
359
+ state: hasMediaItem(document.media, node.mediaId) ? "editable" : "missing",
360
+ ...(node.display ? { display: node.display } : {}),
361
+ detail:
362
+ node.display === "floating"
363
+ ? createFloatingImageDetail(node.altText, node.floating)
364
+ : node.altText
365
+ ? `Alt text ${node.altText}.`
366
+ : "Inline image remains a whole-unit render surface.",
367
+ });
368
+ return { nextCursor: start + 1, lockedFragmentIds: [] };
369
+ case "opaque_inline": {
370
+ const fragment = getOpaqueFragment(document.preservation as never, node.fragmentId);
371
+ const descriptor = fragment ? describeOpaqueFragment(fragment) : null;
372
+ paragraph.segments.push({
373
+ segmentId: `${paragraph.blockId}-segment-${paragraph.segments.length}`,
374
+ kind: "opaque_inline",
375
+ from: start,
376
+ to: start + 1,
377
+ fragmentId: node.fragmentId,
378
+ warningId: node.warningId,
379
+ label: descriptor?.label ?? "Unsupported inline OOXML",
380
+ detail:
381
+ descriptor?.detail ??
382
+ "Locked whole-unit to keep unsupported inline OOXML intact through export.",
383
+ state: "locked-preserve-only",
384
+ });
385
+ return { nextCursor: start + 1, lockedFragmentIds: [node.fragmentId] };
386
+ }
387
+ case "chart_preview":
388
+ return appendComplexPreviewSegment(paragraph, node, start, "Chart", createChartDetail(node));
389
+ case "smartart_preview":
390
+ return appendComplexPreviewSegment(paragraph, node, start, "SmartArt", createSmartArtDetail(node));
391
+ case "shape":
392
+ return appendComplexPreviewSegment(paragraph, node, start, "Shape", createShapeDetail(node));
393
+ case "wordart":
394
+ return appendComplexPreviewSegment(paragraph, node, start, "WordArt", createWordArtDetail(node));
395
+ case "vml_shape":
396
+ return appendComplexPreviewSegment(paragraph, node, start, "VML shape", createVmlDetail(node));
397
+ }
398
+ }
399
+
400
+ function appendComplexPreviewSegment(
401
+ paragraph: ParagraphAccumulator,
402
+ node: { rawXml: string },
403
+ start: number,
404
+ label: string,
405
+ detail: string,
406
+ ): { nextCursor: number; lockedFragmentIds: string[] } {
407
+ paragraph.segments.push({
408
+ segmentId: `${paragraph.blockId}-segment-${paragraph.segments.length}`,
409
+ kind: "opaque_inline",
410
+ from: start,
411
+ to: start + 1,
412
+ fragmentId: `complex:${label.replace(/\s/g, "-").toLowerCase()}:${start}`,
413
+ warningId: `warning:complex-preview:${start}`,
414
+ label,
415
+ detail,
416
+ state: "locked-preserve-only",
417
+ });
418
+ return { nextCursor: start + 1, lockedFragmentIds: [] };
419
+ }
420
+
421
+ function createChartDetail(node: ChartPreviewNode): string {
422
+ return node.previewMediaId
423
+ ? `Chart read-only preview. Fallback image ${node.previewMediaId}. Original XML preserved for export.`
424
+ : "Chart read-only preview. Original XML preserved for export.";
425
+ }
426
+
427
+ function createSmartArtDetail(node: SmartArtPreviewNode): string {
428
+ return node.previewMediaId
429
+ ? `SmartArt diagram read-only preview. Fallback image ${node.previewMediaId}. Original XML preserved for export.`
430
+ : "SmartArt diagram read-only preview. Original XML preserved for export.";
431
+ }
432
+
433
+ function createShapeDetail(node: ShapeNode): string {
434
+ const parts = ["Shape read-only preview."];
435
+ if (node.geometry) parts.push(`Geometry: ${node.geometry}.`);
436
+ if (node.text) parts.push(`Text: "${node.text}".`);
437
+ parts.push("Original XML preserved for export.");
438
+ return parts.join(" ");
439
+ }
440
+
441
+ function createWordArtDetail(node: WordArtNode): string {
442
+ const parts = ["WordArt read-only preview."];
443
+ if (node.text) parts.push(`Text: "${node.text}".`);
444
+ if (node.geometry) parts.push(`Effect: ${node.geometry}.`);
445
+ parts.push("Original XML preserved for export.");
446
+ return parts.join(" ");
447
+ }
448
+
449
+ function createVmlDetail(node: VmlShapeNode): string {
450
+ const parts = ["VML shape read-only preview."];
451
+ if (node.shapeType) parts.push(`Type: ${node.shapeType}.`);
452
+ if (node.text) parts.push(`Text: "${node.text}".`);
453
+ parts.push("Legacy VML; original XML preserved for export.");
454
+ return parts.join(" ");
455
+ }
456
+
457
+ function createPlainText(
458
+ blocks: SurfaceBlockSnapshot[],
459
+ ): string {
460
+ const text: string[] = [];
461
+ for (const block of blocks) {
462
+ if (block.kind === "opaque_block") {
463
+ text.push("\uFFFA");
464
+ continue;
465
+ }
466
+
467
+ if (block.kind === "table") {
468
+ text.push("\uFFFA"); // placeholder for table in plain text
469
+ continue;
470
+ }
471
+
472
+ if (block.kind === "sdt_block") {
473
+ text.push(createPlainText(block.children));
474
+ continue;
475
+ }
476
+
477
+ for (const segment of block.segments) {
478
+ switch (segment.kind) {
479
+ case "text":
480
+ text.push(segment.text);
481
+ break;
482
+ case "tab":
483
+ text.push("\t");
484
+ break;
485
+ case "hard_break":
486
+ text.push("\n");
487
+ break;
488
+ case "image":
489
+ text.push("\uFFFC");
490
+ break;
491
+ case "opaque_inline":
492
+ text.push("\uFFF9");
493
+ break;
494
+ }
495
+ }
496
+ }
497
+
498
+ return text.join("");
499
+ }
500
+
501
+ function cloneMarks(marks: TextMark[]): Array<"bold" | "italic" | "underline" | "strikethrough"> {
502
+ return marks.map((mark) => mark.type);
503
+ }
504
+
505
+ function normalizeDocumentRoot(content: unknown): DocumentRootNode {
506
+ if (content && typeof content === "object" &&
507
+ ((content as { type?: string }).type === "doc" || (content as { type?: string }).type === "document_root")) {
508
+ return content as DocumentRootNode;
509
+ }
510
+
511
+ if (Array.isArray(content)) {
512
+ return {
513
+ type: "doc",
514
+ children: content.filter(
515
+ (value): value is DocumentRootNode["children"][number] =>
516
+ Boolean(value) &&
517
+ typeof value === "object" &&
518
+ ((value as { type?: string }).type === "paragraph" ||
519
+ (value as { type?: string }).type === "table" ||
520
+ (value as { type?: string }).type === "sdt" ||
521
+ (value as { type?: string }).type === "custom_xml" ||
522
+ (value as { type?: string }).type === "alt_chunk" ||
523
+ (value as { type?: string }).type === "opaque_block"),
524
+ ),
525
+ };
526
+ }
527
+
528
+ return {
529
+ type: "doc",
530
+ children: [{ type: "paragraph", children: [] }],
531
+ };
532
+ }
533
+
534
+ function createFloatingImageDetail(
535
+ altText: string | undefined,
536
+ floating: {
537
+ horizontalPosition?: { relativeFrom?: string; align?: string; offset?: number };
538
+ verticalPosition?: { relativeFrom?: string; align?: string; offset?: number };
539
+ wrap?: string;
540
+ } | undefined,
541
+ ): string {
542
+ const parts = [altText ? `Alt text ${altText}.` : "Floating image rendered inline in the editor."];
543
+ if (floating?.wrap) {
544
+ parts.push(`Wrap ${floating.wrap}.`);
545
+ }
546
+ if (floating?.horizontalPosition?.align || floating?.horizontalPosition?.offset !== undefined) {
547
+ parts.push(
548
+ `Horizontal ${floating.horizontalPosition.align ?? floating.horizontalPosition.offset}.`,
549
+ );
550
+ }
551
+ if (floating?.verticalPosition?.align || floating?.verticalPosition?.offset !== undefined) {
552
+ parts.push(
553
+ `Vertical ${floating.verticalPosition.align ?? floating.verticalPosition.offset}.`,
554
+ );
555
+ }
556
+ return parts.join(" ");
557
+ }
558
+
559
+ function hasMediaItem(media: Record<string, unknown>, mediaId: string): boolean {
560
+ if (mediaId in media) {
561
+ return true;
562
+ }
563
+
564
+ const items = media.items;
565
+ if (!items || typeof items !== "object" || Array.isArray(items)) {
566
+ return false;
567
+ }
568
+
569
+ return mediaId in items;
570
+ }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * ProseMirror table editing commands backed by prosemirror-tables.
3
+ *
4
+ * Each command follows the standard ProseMirror command signature:
5
+ * (state, dispatch?) => boolean
6
+ *
7
+ * The prosemirror-tables commands replace the previous hand-rolled
8
+ * implementations and support merged cells, colspan/rowspan, and
9
+ * the full prosemirror-tables table model including table_header nodes.
10
+ */
11
+
12
+ import type { Command as PMCommand, EditorState } from "prosemirror-state";
13
+ import {
14
+ addColumnAfter as pmAddColumnAfter,
15
+ addColumnBefore as pmAddColumnBefore,
16
+ addRowAfter as pmAddRowAfter,
17
+ addRowBefore as pmAddRowBefore,
18
+ deleteColumn as pmDeleteColumn,
19
+ deleteRow as pmDeleteRow,
20
+ deleteTable,
21
+ goToNextCell,
22
+ mergeCells,
23
+ splitCell,
24
+ toggleHeaderCell,
25
+ toggleHeaderColumn,
26
+ toggleHeaderRow,
27
+ } from "prosemirror-tables";
28
+
29
+ // prosemirror-tables does not export goToPreviousCell; replicate via direction -1.
30
+ const goToPreviousCell: PMCommand = (state, dispatch, view) =>
31
+ goToNextCell(-1)(state, dispatch, view);
32
+
33
+ export type { Command as TableCommand } from "prosemirror-state";
34
+
35
+ function tableAtSelection(state: EditorState) {
36
+ const { $head } = state.selection;
37
+ for (let depth = $head.depth; depth > 0; depth -= 1) {
38
+ const node = $head.node(depth);
39
+ if (node.type.spec.tableRole === "table") {
40
+ return node;
41
+ }
42
+ }
43
+ return null;
44
+ }
45
+
46
+ function withTableGuard(
47
+ canRun: (state: EditorState) => boolean,
48
+ command: PMCommand,
49
+ ): PMCommand {
50
+ return (state, dispatch, view) => {
51
+ if (!canRun(state)) return false;
52
+ return command(state, dispatch, view);
53
+ };
54
+ }
55
+
56
+ export const addRowBefore: PMCommand = (state, dispatch, view) =>
57
+ pmAddRowBefore(state, dispatch, view);
58
+
59
+ export const addRowAfter: PMCommand = (state, dispatch, view) =>
60
+ pmAddRowAfter(state, dispatch, view);
61
+
62
+ export const deleteRow: PMCommand = withTableGuard((state) => {
63
+ const table = tableAtSelection(state);
64
+ return table !== null && table.childCount > 1;
65
+ }, pmDeleteRow);
66
+
67
+ export const addColumnBefore: PMCommand = (state, dispatch, view) =>
68
+ pmAddColumnBefore(state, dispatch, view);
69
+
70
+ export const addColumnAfter: PMCommand = (state, dispatch, view) =>
71
+ pmAddColumnAfter(state, dispatch, view);
72
+
73
+ export const deleteColumn: PMCommand = withTableGuard((state) => {
74
+ const table = tableAtSelection(state);
75
+ return table !== null && table.childCount > 0 && table.firstChild !== null && table.firstChild.childCount > 1;
76
+ }, pmDeleteColumn);
77
+
78
+ export {
79
+ deleteTable,
80
+ mergeCells,
81
+ splitCell,
82
+ toggleHeaderRow,
83
+ toggleHeaderColumn,
84
+ toggleHeaderCell,
85
+ goToNextCell,
86
+ goToPreviousCell,
87
+ };