@beyondwork/docx-react-component 1.0.1 → 1.0.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 (172) hide show
  1. package/README.md +44 -104
  2. package/package.json +50 -30
  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 +325 -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 +1506 -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,530 @@
1
+ import {
2
+ createCanonicalDocumentSignature,
3
+ projectCanonicalDocument,
4
+ type BlockNode,
5
+ type CanonicalDocument,
6
+ type HyperlinkNode,
7
+ type InlineNode,
8
+ type ParagraphNode,
9
+ type TextNode,
10
+ } from "../model/canonical-document.ts";
11
+ import type { DocumentVersionSnapshot } from "./snapshot.ts";
12
+
13
+ export interface CompareVersionRef {
14
+ versionId: string;
15
+ name: string;
16
+ signature: string;
17
+ }
18
+
19
+ export type CompareChangeKind =
20
+ | "paragraph-insertion"
21
+ | "paragraph-deletion"
22
+ | "structural-insertion"
23
+ | "structural-deletion";
24
+
25
+ export interface CompareChange {
26
+ changeId: string;
27
+ kind: CompareChangeKind;
28
+ tracked: boolean;
29
+ blockType: BlockNode["type"];
30
+ beforeText?: string;
31
+ afterText?: string;
32
+ targetIndex: number;
33
+ }
34
+
35
+ export interface VersionCompareResult {
36
+ compareVersion: "version-compare-result/1";
37
+ comparedAt: string;
38
+ base: CompareVersionRef;
39
+ target: CompareVersionRef;
40
+ changes: CompareChange[];
41
+ comparedDocument: CanonicalDocument;
42
+ }
43
+
44
+ export interface CompareDocumentVersionsOptions {
45
+ comparedAt?: string;
46
+ authorId?: string;
47
+ }
48
+
49
+ interface BuildEntry {
50
+ block: BlockNode;
51
+ trackChange?: "insertion" | "deletion";
52
+ tracked: boolean;
53
+ blockType: BlockNode["type"];
54
+ beforeText?: string;
55
+ afterText?: string;
56
+ }
57
+
58
+ interface PendingRevision {
59
+ changeId: string;
60
+ kind: "insertion" | "deletion";
61
+ paragraphIndex: number;
62
+ }
63
+
64
+ interface LcsMatch {
65
+ baseIndex: number;
66
+ targetIndex: number;
67
+ }
68
+
69
+ export function compareDocumentSnapshots(
70
+ base: DocumentVersionSnapshot,
71
+ target: DocumentVersionSnapshot,
72
+ options: CompareDocumentVersionsOptions = {},
73
+ ): VersionCompareResult {
74
+ const comparedAt = options.comparedAt ?? target.createdAt;
75
+ const authorId = options.authorId ?? "Version Compare";
76
+ const baseDocument = base.canonicalDocument;
77
+ const targetDocument = target.canonicalDocument;
78
+ const baseBlocks = baseDocument.content.children;
79
+ const targetBlocks = targetDocument.content.children;
80
+ const matches = computeLcs(
81
+ baseBlocks.map(getComparableBlockKey),
82
+ targetBlocks.map(getComparableBlockKey),
83
+ );
84
+
85
+ const buildEntries: BuildEntry[] = [];
86
+ const changes: CompareChange[] = [];
87
+ let nextBaseIndex = 0;
88
+ let nextTargetIndex = 0;
89
+ let nextChangeNumber = 1;
90
+
91
+ for (const match of [...matches, { baseIndex: baseBlocks.length, targetIndex: targetBlocks.length }]) {
92
+ while (nextBaseIndex < match.baseIndex || nextTargetIndex < match.targetIndex) {
93
+ const deleteBlock = nextBaseIndex < match.baseIndex ? baseBlocks[nextBaseIndex] : undefined;
94
+ const insertBlock = nextTargetIndex < match.targetIndex ? targetBlocks[nextTargetIndex] : undefined;
95
+
96
+ if (deleteBlock && insertBlock) {
97
+ const deletionId = `change-${nextChangeNumber}`;
98
+ nextChangeNumber += 1;
99
+ addTrackedOrStructuralEntry(
100
+ buildEntries,
101
+ changes,
102
+ deletionId,
103
+ deleteBlock,
104
+ "deletion",
105
+ nextTargetIndex,
106
+ );
107
+
108
+ const insertionId = `change-${nextChangeNumber}`;
109
+ nextChangeNumber += 1;
110
+ addTrackedOrStructuralEntry(
111
+ buildEntries,
112
+ changes,
113
+ insertionId,
114
+ insertBlock,
115
+ "insertion",
116
+ nextTargetIndex,
117
+ );
118
+ nextBaseIndex += 1;
119
+ nextTargetIndex += 1;
120
+ continue;
121
+ }
122
+
123
+ if (deleteBlock) {
124
+ const changeId = `change-${nextChangeNumber}`;
125
+ nextChangeNumber += 1;
126
+ addTrackedOrStructuralEntry(
127
+ buildEntries,
128
+ changes,
129
+ changeId,
130
+ deleteBlock,
131
+ "deletion",
132
+ nextTargetIndex,
133
+ );
134
+ nextBaseIndex += 1;
135
+ continue;
136
+ }
137
+
138
+ if (insertBlock) {
139
+ const changeId = `change-${nextChangeNumber}`;
140
+ nextChangeNumber += 1;
141
+ addTrackedOrStructuralEntry(
142
+ buildEntries,
143
+ changes,
144
+ changeId,
145
+ insertBlock,
146
+ "insertion",
147
+ nextTargetIndex,
148
+ );
149
+ nextTargetIndex += 1;
150
+ }
151
+ }
152
+
153
+ if (match.baseIndex < baseBlocks.length && match.targetIndex < targetBlocks.length) {
154
+ buildEntries.push({
155
+ block: cloneBlock(targetBlocks[match.targetIndex]),
156
+ tracked: false,
157
+ blockType: targetBlocks[match.targetIndex].type,
158
+ beforeText: getBlockDisplayText(baseBlocks[match.baseIndex]),
159
+ afterText: getBlockDisplayText(targetBlocks[match.targetIndex]),
160
+ });
161
+ nextBaseIndex = match.baseIndex + 1;
162
+ nextTargetIndex = match.targetIndex + 1;
163
+ }
164
+ }
165
+
166
+ const built = buildComparedDocument(baseDocument, targetDocument, buildEntries, comparedAt, authorId);
167
+ return {
168
+ compareVersion: "version-compare-result/1",
169
+ comparedAt,
170
+ base: {
171
+ versionId: base.versionId,
172
+ name: base.name,
173
+ signature: base.documentSignature,
174
+ },
175
+ target: {
176
+ versionId: target.versionId,
177
+ name: target.name,
178
+ signature: target.documentSignature,
179
+ },
180
+ changes,
181
+ comparedDocument: built,
182
+ };
183
+ }
184
+
185
+ function addTrackedOrStructuralEntry(
186
+ buildEntries: BuildEntry[],
187
+ changes: CompareChange[],
188
+ changeId: string,
189
+ block: BlockNode,
190
+ direction: "insertion" | "deletion",
191
+ targetIndex: number,
192
+ ): void {
193
+ const tracked = block.type === "paragraph";
194
+ const cloned = cloneBlock(block);
195
+ const text = getBlockDisplayText(block);
196
+
197
+ buildEntries.push({
198
+ block: cloned,
199
+ ...(tracked ? { trackChange: direction } : {}),
200
+ tracked,
201
+ blockType: cloned.type,
202
+ ...(direction === "deletion" ? { beforeText: text } : { afterText: text }),
203
+ });
204
+
205
+ changes.push({
206
+ changeId,
207
+ kind:
208
+ block.type === "paragraph"
209
+ ? direction === "insertion"
210
+ ? "paragraph-insertion"
211
+ : "paragraph-deletion"
212
+ : direction === "insertion"
213
+ ? "structural-insertion"
214
+ : "structural-deletion",
215
+ tracked,
216
+ blockType: cloned.type,
217
+ ...(direction === "deletion" ? { beforeText: text } : { afterText: text }),
218
+ targetIndex,
219
+ });
220
+ }
221
+
222
+ function buildComparedDocument(
223
+ base: CanonicalDocument,
224
+ target: CanonicalDocument,
225
+ entries: readonly BuildEntry[],
226
+ comparedAt: string,
227
+ authorId: string,
228
+ ): CanonicalDocument {
229
+ const contentChildren: BlockNode[] = [];
230
+ const pendingRevisions: PendingRevision[] = [];
231
+ const diagnosticsWarnings = [...target.diagnostics.warnings];
232
+ let paragraphIndex = 0;
233
+ let warningNumber = 1;
234
+
235
+ for (let index = 0; index < entries.length; index += 1) {
236
+ const entry = entries[index];
237
+ if (!entry) {
238
+ continue;
239
+ }
240
+
241
+ contentChildren.push(entry.block);
242
+ if (entry.block.type === "paragraph") {
243
+ if (entry.trackChange) {
244
+ pendingRevisions.push({
245
+ changeId: `change-${pendingRevisions.length + 1}`,
246
+ kind: entry.trackChange,
247
+ paragraphIndex,
248
+ });
249
+ }
250
+ paragraphIndex += 1;
251
+ continue;
252
+ }
253
+
254
+ if (!entry.tracked) {
255
+ diagnosticsWarnings.push({
256
+ diagnosticId: `diagnostic:compare-structural-${warningNumber}`,
257
+ warningId: `warning:compare-structural-${warningNumber}`,
258
+ source: "review",
259
+ message: `Version compare preserved a ${entry.blockType} ${entry.beforeText ? "deletion" : "insertion"} without a Word revision wrapper.`,
260
+ });
261
+ warningNumber += 1;
262
+ }
263
+ }
264
+
265
+ const revisions = createParagraphRevisionRecords(
266
+ contentChildren,
267
+ pendingRevisions,
268
+ comparedAt,
269
+ authorId,
270
+ );
271
+
272
+ return {
273
+ ...projectCanonicalDocument(target),
274
+ updatedAt: comparedAt,
275
+ styles: mergeStyleCatalogs(base.styles, target.styles),
276
+ numbering: mergeRecordCatalog(base.numbering, target.numbering),
277
+ media: mergeRecordCatalog(base.media, target.media),
278
+ content: {
279
+ type: "doc",
280
+ children: contentChildren,
281
+ },
282
+ review: {
283
+ comments: {},
284
+ revisions,
285
+ },
286
+ preservation: mergePreservationStores(base.preservation, target.preservation),
287
+ diagnostics: {
288
+ warnings: diagnosticsWarnings,
289
+ errors: [...target.diagnostics.errors],
290
+ },
291
+ ...(target.subParts ?? base.subParts ? { subParts: projectValue(target.subParts ?? base.subParts) } : {}),
292
+ };
293
+ }
294
+
295
+ function createParagraphRevisionRecords(
296
+ children: readonly BlockNode[],
297
+ pendingRevisions: readonly PendingRevision[],
298
+ createdAt: string,
299
+ authorId: string,
300
+ ): CanonicalDocument["review"]["revisions"] {
301
+ const positionByParagraphIndex = new Map<number, number>();
302
+ let cursor = 0;
303
+ let paragraphIndex = 0;
304
+ let previousWasParagraph = false;
305
+
306
+ for (const block of children) {
307
+ if (block.type === "paragraph") {
308
+ if (previousWasParagraph) {
309
+ cursor += 1;
310
+ }
311
+ cursor += getParagraphLength(block);
312
+ positionByParagraphIndex.set(paragraphIndex, cursor);
313
+ paragraphIndex += 1;
314
+ previousWasParagraph = true;
315
+ continue;
316
+ }
317
+
318
+ cursor += 1;
319
+ previousWasParagraph = false;
320
+ }
321
+
322
+ return Object.fromEntries(
323
+ pendingRevisions
324
+ .map((revision, index) => {
325
+ const position = positionByParagraphIndex.get(revision.paragraphIndex);
326
+ if (position === undefined) {
327
+ return undefined;
328
+ }
329
+
330
+ const changeId = `change-${index + 1}`;
331
+ return [
332
+ changeId,
333
+ {
334
+ changeId,
335
+ kind: revision.kind,
336
+ anchor: {
337
+ kind: "range" as const,
338
+ range: { from: position, to: position },
339
+ assoc: { start: -1 as const, end: 1 as const },
340
+ },
341
+ authorId,
342
+ createdAt,
343
+ status: "open" as const,
344
+ warningIds: [],
345
+ metadata: {
346
+ source: "runtime" as const,
347
+ importedRevisionForm:
348
+ revision.kind === "insertion" ? "paragraph-insertion" : "paragraph-deletion",
349
+ },
350
+ },
351
+ ] as const;
352
+ })
353
+ .filter((entry): entry is readonly [string, CanonicalDocument["review"]["revisions"][string]] => Boolean(entry)),
354
+ );
355
+ }
356
+
357
+ function getComparableBlockKey(block: BlockNode): string {
358
+ if (block.type === "paragraph") {
359
+ return `paragraph:${createCanonicalDocumentSignature({
360
+ ...block,
361
+ children: normalizeInlineChildren(block.children),
362
+ })}`;
363
+ }
364
+
365
+ return `${block.type}:${createCanonicalDocumentSignature(block)}`;
366
+ }
367
+
368
+ function normalizeInlineChildren(children: readonly InlineNode[]): InlineNode[] {
369
+ return children.map((child) => {
370
+ if (child.type === "hyperlink") {
371
+ return {
372
+ ...child,
373
+ children: normalizeInlineChildren(child.children) as HyperlinkNode["children"],
374
+ };
375
+ }
376
+ if (child.type === "field") {
377
+ return {
378
+ ...child,
379
+ children: normalizeInlineChildren(child.children),
380
+ };
381
+ }
382
+ return projectValue(child);
383
+ });
384
+ }
385
+
386
+ function computeLcs(baseKeys: readonly string[], targetKeys: readonly string[]): LcsMatch[] {
387
+ const widths = targetKeys.length + 1;
388
+ const matrix = Array.from({ length: baseKeys.length + 1 }, () =>
389
+ Array.from({ length: widths }, () => 0),
390
+ );
391
+
392
+ for (let baseIndex = baseKeys.length - 1; baseIndex >= 0; baseIndex -= 1) {
393
+ for (let targetIndex = targetKeys.length - 1; targetIndex >= 0; targetIndex -= 1) {
394
+ matrix[baseIndex][targetIndex] =
395
+ baseKeys[baseIndex] === targetKeys[targetIndex]
396
+ ? matrix[baseIndex + 1][targetIndex + 1] + 1
397
+ : Math.max(matrix[baseIndex + 1][targetIndex], matrix[baseIndex][targetIndex + 1]);
398
+ }
399
+ }
400
+
401
+ const matches: LcsMatch[] = [];
402
+ let baseIndex = 0;
403
+ let targetIndex = 0;
404
+ while (baseIndex < baseKeys.length && targetIndex < targetKeys.length) {
405
+ if (baseKeys[baseIndex] === targetKeys[targetIndex]) {
406
+ matches.push({ baseIndex, targetIndex });
407
+ baseIndex += 1;
408
+ targetIndex += 1;
409
+ continue;
410
+ }
411
+
412
+ if (matrix[baseIndex + 1][targetIndex] >= matrix[baseIndex][targetIndex + 1]) {
413
+ baseIndex += 1;
414
+ } else {
415
+ targetIndex += 1;
416
+ }
417
+ }
418
+
419
+ return matches;
420
+ }
421
+
422
+ function getParagraphLength(paragraph: ParagraphNode): number {
423
+ return paragraph.children.reduce((length, child) => length + getInlineLength(child), 0);
424
+ }
425
+
426
+ function getInlineLength(node: InlineNode): number {
427
+ switch (node.type) {
428
+ case "text":
429
+ return Array.from(node.text).length;
430
+ case "hyperlink":
431
+ return node.children.reduce((length, child) => length + getInlineLength(child), 0);
432
+ case "field":
433
+ return node.children.reduce((length, child) => length + getInlineLength(child), 0);
434
+ case "tab":
435
+ case "hard_break":
436
+ case "column_break":
437
+ case "image":
438
+ case "opaque_inline":
439
+ case "footnote_ref":
440
+ case "bookmark_start":
441
+ case "bookmark_end":
442
+ return 1;
443
+ }
444
+ }
445
+
446
+ function getBlockDisplayText(block: BlockNode): string {
447
+ switch (block.type) {
448
+ case "paragraph":
449
+ return block.children.map(getInlineDisplayText).join("");
450
+ case "table":
451
+ return "[Table]";
452
+ case "sdt":
453
+ return "[Structured content]";
454
+ case "custom_xml":
455
+ return "[Custom XML block]";
456
+ case "alt_chunk":
457
+ return "[Alt chunk]";
458
+ case "section_break":
459
+ return "[Section break]";
460
+ case "opaque_block":
461
+ return "[Preserved block]";
462
+ }
463
+ }
464
+
465
+ function getInlineDisplayText(node: InlineNode): string {
466
+ switch (node.type) {
467
+ case "text":
468
+ return node.text;
469
+ case "tab":
470
+ return "\t";
471
+ case "hard_break":
472
+ case "column_break":
473
+ return "\n";
474
+ case "hyperlink":
475
+ return node.children.map(getInlineDisplayText).join("");
476
+ case "field":
477
+ return node.children.map(getInlineDisplayText).join("");
478
+ case "image":
479
+ return node.altText ?? "[Image]";
480
+ case "opaque_inline":
481
+ return "[Preserved]";
482
+ case "footnote_ref":
483
+ return "[Footnote]";
484
+ case "bookmark_start":
485
+ case "bookmark_end":
486
+ return "";
487
+ }
488
+ }
489
+
490
+ function mergeStyleCatalogs(
491
+ base: CanonicalDocument["styles"],
492
+ target: CanonicalDocument["styles"],
493
+ ): CanonicalDocument["styles"] {
494
+ return {
495
+ paragraphs: { ...base.paragraphs, ...target.paragraphs },
496
+ characters: { ...base.characters, ...target.characters },
497
+ tables: { ...base.tables, ...target.tables },
498
+ ...((base.latentStyles || target.latentStyles)
499
+ ? { latentStyles: { ...(base.latentStyles ?? {}), ...(target.latentStyles ?? {}) } }
500
+ : {}),
501
+ };
502
+ }
503
+
504
+ function mergePreservationStores(
505
+ base: CanonicalDocument["preservation"],
506
+ target: CanonicalDocument["preservation"],
507
+ ): CanonicalDocument["preservation"] {
508
+ return {
509
+ opaqueFragments: {
510
+ ...base.opaqueFragments,
511
+ ...target.opaqueFragments,
512
+ },
513
+ packageParts: {
514
+ ...base.packageParts,
515
+ ...target.packageParts,
516
+ },
517
+ };
518
+ }
519
+
520
+ function mergeRecordCatalog<T extends Record<string, unknown>>(base: T, target: T): T {
521
+ return projectValue({ ...base, ...target });
522
+ }
523
+
524
+ function cloneBlock<T extends BlockNode>(block: T): T {
525
+ return projectValue(block);
526
+ }
527
+
528
+ function projectValue<T>(value: T): T {
529
+ return structuredClone(value);
530
+ }
@@ -0,0 +1,162 @@
1
+ import { TextEncoder } from "node:util";
2
+
3
+ import type { OpcRelationship } from "../io/ooxml/part-manifest.ts";
4
+ import { serializeMainDocument } from "../io/export/serialize-main-document.ts";
5
+ import {
6
+ WORD_NUMBERING_CONTENT_TYPE,
7
+ serializeNumberingXml,
8
+ } from "../io/export/serialize-numbering.ts";
9
+ import { serializeRuntimeRevisionsIntoDocumentXml } from "../io/export/serialize-runtime-revisions.ts";
10
+ import { createExportSession } from "../io/export/export-session.ts";
11
+ import {
12
+ DOCX_DOCUMENT_CONTENT_TYPE,
13
+ DOCX_MIME_TYPE,
14
+ writeDocxPackage,
15
+ } from "../io/opc/docx-package.ts";
16
+ import type { OpcPackage } from "../io/opc/package-reader.ts";
17
+ import { readOpcPackage } from "../io/opc/package-reader.ts";
18
+ import type { RevisionRecord as ReviewRevisionRecord } from "../review/store/revision-types.ts";
19
+ import type { VersionCompareResult } from "./diff-engine.ts";
20
+
21
+ const MAIN_DOCUMENT_PATH = "/word/document.xml";
22
+ const NUMBERING_PART_PATH = "/word/numbering.xml";
23
+ const NUMBERING_RELATIONSHIP_TYPE =
24
+ "http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering";
25
+
26
+ export interface ExportComparedDocumentOptions {
27
+ fileName?: string;
28
+ sourcePackage?: OpcPackage;
29
+ }
30
+
31
+ export interface ExportComparedDocumentResult {
32
+ bytes: Uint8Array;
33
+ mimeType: string;
34
+ fileName: string;
35
+ }
36
+
37
+ export function exportComparedDocumentRedlines(
38
+ result: VersionCompareResult,
39
+ options: ExportComparedDocumentOptions = {},
40
+ ): ExportComparedDocumentResult {
41
+ const sourcePackage =
42
+ options.sourcePackage ??
43
+ readOpcPackage(
44
+ writeDocxPackage({
45
+ documentXml: `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:body><w:p/><w:sectPr/></w:body></w:document>`,
46
+ }),
47
+ );
48
+ const existingRelationships =
49
+ sourcePackage.parts.get(MAIN_DOCUMENT_PATH)?.relationships ?? [];
50
+
51
+ const serialized = serializeMainDocument(
52
+ result.comparedDocument.content,
53
+ result.comparedDocument.preservation,
54
+ existingRelationships,
55
+ {
56
+ media: result.comparedDocument.media,
57
+ },
58
+ );
59
+ const actionableRevisions = toReviewRevisionRecords(result.comparedDocument.review.revisions);
60
+ const revisionDocument = serializeRuntimeRevisionsIntoDocumentXml(
61
+ serialized.documentXml,
62
+ actionableRevisions,
63
+ serialized.paragraphBoundaries,
64
+ );
65
+ if (revisionDocument.skippedRevisionIds.length > 0) {
66
+ throw new Error(
67
+ `Compare export skipped ${revisionDocument.skippedRevisionIds.length} revision markers.`,
68
+ );
69
+ }
70
+
71
+ const numberingXml = hasNumberingEntries(result.comparedDocument.numbering)
72
+ ? serializeNumberingXml(result.comparedDocument.numbering)
73
+ : undefined;
74
+ const relationships = withOptionalNumberingRelationship(
75
+ serialized.relationships,
76
+ existingRelationships,
77
+ Boolean(numberingXml),
78
+ );
79
+ const exportSession = createExportSession(sourcePackage, [
80
+ MAIN_DOCUMENT_PATH,
81
+ ...(numberingXml ? [NUMBERING_PART_PATH] : []),
82
+ ]);
83
+
84
+ exportSession.replaceOwnedPart({
85
+ path: MAIN_DOCUMENT_PATH,
86
+ bytes: new TextEncoder().encode(revisionDocument.documentXml),
87
+ contentType: DOCX_DOCUMENT_CONTENT_TYPE,
88
+ relationships,
89
+ });
90
+
91
+ if (numberingXml) {
92
+ exportSession.replaceOwnedPart({
93
+ path: NUMBERING_PART_PATH,
94
+ bytes: new TextEncoder().encode(numberingXml),
95
+ contentType:
96
+ sourcePackage.parts.get(NUMBERING_PART_PATH)?.contentType ?? WORD_NUMBERING_CONTENT_TYPE,
97
+ });
98
+ }
99
+
100
+ return {
101
+ bytes: exportSession.serialize(),
102
+ mimeType: DOCX_MIME_TYPE,
103
+ fileName: options.fileName ?? `${result.target.name}-compared.docx`,
104
+ };
105
+ }
106
+
107
+ function toReviewRevisionRecords(
108
+ revisions: VersionCompareResult["comparedDocument"]["review"]["revisions"],
109
+ ): ReviewRevisionRecord[] {
110
+ return Object.values(revisions).map((revision) => ({
111
+ revisionId: revision.changeId,
112
+ kind: revision.kind,
113
+ anchor: revision.anchor,
114
+ authorId: revision.authorId ?? "Version Compare",
115
+ createdAt: revision.createdAt,
116
+ warningIds: [...(revision.warningIds ?? [])],
117
+ metadata: {
118
+ source: revision.metadata?.source ?? "runtime",
119
+ preserveOnlyReason: revision.metadata?.preserveOnlyReason,
120
+ importedRevisionForm: revision.metadata?.importedRevisionForm,
121
+ originalRevisionType: revision.metadata?.originalRevisionType,
122
+ ooxmlRevisionId: revision.metadata?.ooxmlRevisionId,
123
+ },
124
+ status: revision.status === "open" ? "active" : revision.status,
125
+ }));
126
+ }
127
+
128
+ function hasNumberingEntries(
129
+ numbering: VersionCompareResult["comparedDocument"]["numbering"],
130
+ ): boolean {
131
+ return (
132
+ Object.keys(numbering.abstractDefinitions).length > 0 ||
133
+ Object.keys(numbering.instances).length > 0
134
+ );
135
+ }
136
+
137
+ function withOptionalNumberingRelationship(
138
+ relationships: readonly OpcRelationship[],
139
+ existingRelationships: readonly OpcRelationship[],
140
+ includeNumbering: boolean,
141
+ ): OpcRelationship[] {
142
+ const next = relationships.map((relationship) => ({ ...relationship }));
143
+ if (!includeNumbering) {
144
+ return next;
145
+ }
146
+
147
+ const existing = next.find((relationship) => relationship.type === NUMBERING_RELATIONSHIP_TYPE);
148
+ if (existing) {
149
+ return next;
150
+ }
151
+
152
+ const fallbackId =
153
+ existingRelationships.find((relationship) => relationship.type === NUMBERING_RELATIONSHIP_TYPE)?.id ??
154
+ `rId${next.length + 1}`;
155
+ next.push({
156
+ id: fallbackId,
157
+ type: NUMBERING_RELATIONSHIP_TYPE,
158
+ target: "numbering.xml",
159
+ targetMode: "internal",
160
+ });
161
+ return next;
162
+ }