@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,394 @@
1
+ import { createEmptyMapping, type TransactionMapping } from "../../core/selection/mapping.ts";
2
+ import { parseTextStory } from "../../core/schema/text-schema.ts";
3
+ import { createSelectionSnapshot, type CanonicalDocumentEnvelope } from "../../core/state/editor-state.ts";
4
+ import { applyTextTransaction } from "../../core/state/text-transaction.ts";
5
+ import {
6
+ remapRevisionStore,
7
+ setRevisionStatus,
8
+ type RevisionStore,
9
+ } from "./revision-store.ts";
10
+ import { getRevisionActionability, type RevisionRecord } from "./revision-types.ts";
11
+
12
+ export type RevisionActionIntent = "accept" | "reject";
13
+
14
+ export type RevisionActionSkipReason =
15
+ | "missing"
16
+ | "already-resolved"
17
+ | "detached-anchor"
18
+ | "preserve-only"
19
+ | "structural-range"
20
+ | "protected-range"
21
+ | "invalid-range";
22
+
23
+ export interface ApplyRevisionActionOptions {
24
+ document: CanonicalDocumentEnvelope;
25
+ store: RevisionStore;
26
+ revisionId: string;
27
+ intent: RevisionActionIntent;
28
+ timestamp: string;
29
+ }
30
+
31
+ export interface AppliedRevisionAction {
32
+ kind: "applied";
33
+ revisionId: string;
34
+ intent: RevisionActionIntent;
35
+ resultingStatus: "accepted" | "rejected";
36
+ contentChanged: boolean;
37
+ }
38
+
39
+ export interface SkippedRevisionAction {
40
+ kind: "skipped";
41
+ revisionId: string;
42
+ intent: RevisionActionIntent;
43
+ reason: RevisionActionSkipReason;
44
+ detail: string;
45
+ }
46
+
47
+ export type RevisionActionOutcome =
48
+ | AppliedRevisionAction
49
+ | SkippedRevisionAction;
50
+
51
+ export interface ApplyRevisionActionResult {
52
+ document: CanonicalDocumentEnvelope;
53
+ store: RevisionStore;
54
+ mapping: TransactionMapping;
55
+ outcome: RevisionActionOutcome;
56
+ detachedRevisionIds: string[];
57
+ }
58
+
59
+ export function applyRevisionAction(
60
+ options: ApplyRevisionActionOptions,
61
+ ): ApplyRevisionActionResult {
62
+ const revision = options.store.revisions[options.revisionId];
63
+
64
+ if (!revision) {
65
+ return skippedResult(options, "missing", "Revision record was not found.");
66
+ }
67
+
68
+ if (revision.status !== "active") {
69
+ return skippedResult(
70
+ options,
71
+ "already-resolved",
72
+ `Revision is already ${revision.status}.`,
73
+ );
74
+ }
75
+
76
+ if (revision.anchor.kind === "detached") {
77
+ return skippedResult(
78
+ options,
79
+ "detached-anchor",
80
+ "Detached revisions remain visible but cannot be accepted or rejected.",
81
+ );
82
+ }
83
+
84
+ if (getRevisionActionability(revision) !== "actionable") {
85
+ return skippedResult(
86
+ options,
87
+ "preserve-only",
88
+ revision.metadata.preserveOnlyReason ??
89
+ "This revision kind remains preserve-only in the current runtime.",
90
+ );
91
+ }
92
+
93
+ if (revision.anchor.kind !== "range") {
94
+ return skippedResult(
95
+ options,
96
+ "structural-range",
97
+ "Non-range revisions remain preserve-only in the current runtime.",
98
+ );
99
+ }
100
+
101
+ const story = parseTextStory(options.document.content);
102
+ const range = normalizeRange(
103
+ revision.anchor.range.from,
104
+ revision.anchor.range.to,
105
+ );
106
+
107
+ if (range.to > story.size) {
108
+ return skippedResult(
109
+ options,
110
+ "invalid-range",
111
+ `Revision range ${range.from}-${range.to} exceeds story size ${story.size}.`,
112
+ );
113
+ }
114
+
115
+ const paragraphMarkRange = resolveParagraphMarkDeletionRange(story, revision, options.intent);
116
+ if (paragraphMarkRange) {
117
+ const resultingStatus = toResultingStatus(options.intent);
118
+ const textResult = applyTextTransaction(
119
+ options.document,
120
+ createSelectionSnapshot(paragraphMarkRange.from, paragraphMarkRange.to),
121
+ {
122
+ type: "replace",
123
+ range: paragraphMarkRange,
124
+ insertion: [],
125
+ },
126
+ {
127
+ timestamp: options.timestamp,
128
+ },
129
+ );
130
+
131
+ const nextStore = remapRevisionStore(
132
+ setRevisionStatus(options.store, revision.revisionId, resultingStatus),
133
+ textResult.mapping,
134
+ );
135
+
136
+ return {
137
+ document: textResult.document,
138
+ store: nextStore,
139
+ mapping: textResult.mapping,
140
+ outcome: {
141
+ kind: "applied",
142
+ revisionId: revision.revisionId,
143
+ intent: options.intent,
144
+ resultingStatus,
145
+ contentChanged: true,
146
+ },
147
+ detachedRevisionIds: findNewDetachedRevisionIds(options.store, nextStore),
148
+ };
149
+ }
150
+ if (requiresParagraphBoundaryDeletion(revision, options.intent)) {
151
+ return skippedResult(
152
+ options,
153
+ "structural-range",
154
+ "Paragraph-boundary revisions need a stable paragraph break and remain blocked for this shape.",
155
+ );
156
+ }
157
+
158
+ const slice = story.units.slice(range.from, range.to);
159
+ if (
160
+ slice.some(
161
+ (unit) =>
162
+ unit.kind === "paragraph_break" || unit.kind === "opaque_block",
163
+ )
164
+ ) {
165
+ return skippedResult(
166
+ options,
167
+ "structural-range",
168
+ "Paragraph-boundary and structural revisions remain preserve-only.",
169
+ );
170
+ }
171
+
172
+ if (
173
+ slice.some(
174
+ (unit) =>
175
+ unit.kind === "image" || unit.kind === "opaque_inline",
176
+ )
177
+ ) {
178
+ return skippedResult(
179
+ options,
180
+ "protected-range",
181
+ "Revisions touching protected inline content remain preserve-only.",
182
+ );
183
+ }
184
+
185
+ const resultingStatus = toResultingStatus(options.intent);
186
+ const contentChanged = requiresContentDeletion(revision, options.intent);
187
+
188
+ if (!contentChanged) {
189
+ return {
190
+ document: options.document,
191
+ store: setRevisionStatus(options.store, revision.revisionId, resultingStatus),
192
+ mapping: createEmptyMapping(),
193
+ outcome: {
194
+ kind: "applied",
195
+ revisionId: revision.revisionId,
196
+ intent: options.intent,
197
+ resultingStatus,
198
+ contentChanged: false,
199
+ },
200
+ detachedRevisionIds: [],
201
+ };
202
+ }
203
+
204
+ const textResult = applyTextTransaction(
205
+ options.document,
206
+ createSelectionSnapshot(range.from, range.to),
207
+ {
208
+ type: "replace",
209
+ range,
210
+ insertion: [],
211
+ },
212
+ {
213
+ timestamp: options.timestamp,
214
+ },
215
+ );
216
+
217
+ const nextStore = remapRevisionStore(
218
+ setRevisionStatus(options.store, revision.revisionId, resultingStatus),
219
+ textResult.mapping,
220
+ );
221
+
222
+ return {
223
+ document: textResult.document,
224
+ store: nextStore,
225
+ mapping: textResult.mapping,
226
+ outcome: {
227
+ kind: "applied",
228
+ revisionId: revision.revisionId,
229
+ intent: options.intent,
230
+ resultingStatus,
231
+ contentChanged: true,
232
+ },
233
+ detachedRevisionIds: findNewDetachedRevisionIds(options.store, nextStore),
234
+ };
235
+ }
236
+
237
+ function skippedResult(
238
+ options: ApplyRevisionActionOptions,
239
+ reason: RevisionActionSkipReason,
240
+ detail: string,
241
+ ): ApplyRevisionActionResult {
242
+ return {
243
+ document: options.document,
244
+ store: options.store,
245
+ mapping: createEmptyMapping(),
246
+ outcome: {
247
+ kind: "skipped",
248
+ revisionId: options.revisionId,
249
+ intent: options.intent,
250
+ reason,
251
+ detail,
252
+ },
253
+ detachedRevisionIds: [],
254
+ };
255
+ }
256
+
257
+ function toResultingStatus(
258
+ intent: RevisionActionIntent,
259
+ ): "accepted" | "rejected" {
260
+ return intent === "accept" ? "accepted" : "rejected";
261
+ }
262
+
263
+ function requiresContentDeletion(
264
+ revision: Pick<RevisionRecord, "kind">,
265
+ intent: RevisionActionIntent,
266
+ ): boolean {
267
+ return (
268
+ (revision.kind === "insertion" && intent === "reject") ||
269
+ (revision.kind === "deletion" && intent === "accept")
270
+ );
271
+ }
272
+
273
+ function requiresParagraphBoundaryDeletion(
274
+ revision: RevisionRecord,
275
+ intent: RevisionActionIntent,
276
+ ): boolean {
277
+ return (
278
+ (revision.metadata.originalRevisionType === "paragraph-del" && intent === "accept") ||
279
+ (revision.metadata.originalRevisionType === "paragraph-ins" && intent === "reject")
280
+ );
281
+ }
282
+
283
+ function resolveParagraphMarkDeletionRange(
284
+ story: ReturnType<typeof parseTextStory>,
285
+ revision: RevisionRecord,
286
+ intent: RevisionActionIntent,
287
+ ): { from: number; to: number } | undefined {
288
+ const originalRevisionType = revision.metadata.originalRevisionType;
289
+ if (
290
+ revision.anchor.kind !== "range" ||
291
+ (originalRevisionType !== "paragraph-del" && originalRevisionType !== "paragraph-ins")
292
+ ) {
293
+ return undefined;
294
+ }
295
+
296
+ const shouldDeleteBoundary =
297
+ (originalRevisionType === "paragraph-del" && intent === "accept") ||
298
+ (originalRevisionType === "paragraph-ins" && intent === "reject");
299
+ if (!shouldDeleteBoundary) {
300
+ return undefined;
301
+ }
302
+
303
+ const paragraphs = mapParagraphRanges(story);
304
+ const anchorPosition = normalizeRange(
305
+ revision.anchor.range.from,
306
+ revision.anchor.range.to,
307
+ ).from;
308
+ const paragraph = paragraphs.find(
309
+ (candidate) =>
310
+ candidate.end === anchorPosition ||
311
+ (anchorPosition >= candidate.start && anchorPosition <= candidate.end),
312
+ );
313
+ if (!paragraph) {
314
+ return undefined;
315
+ }
316
+
317
+ if (originalRevisionType === "paragraph-del") {
318
+ const boundaryIndex = paragraph.end;
319
+ if (story.units[boundaryIndex]?.kind !== "paragraph_break") {
320
+ return undefined;
321
+ }
322
+
323
+ return {
324
+ from: boundaryIndex,
325
+ to: boundaryIndex + 1,
326
+ };
327
+ }
328
+
329
+ const boundaryIndex = paragraph.start - 1;
330
+ if (boundaryIndex < 0 || story.units[boundaryIndex]?.kind !== "paragraph_break") {
331
+ return undefined;
332
+ }
333
+
334
+ return {
335
+ from: boundaryIndex,
336
+ to: boundaryIndex + 1,
337
+ };
338
+ }
339
+
340
+ function mapParagraphRanges(
341
+ story: ReturnType<typeof parseTextStory>,
342
+ ): Array<{ start: number; end: number }> {
343
+ const paragraphs: Array<{ start: number; end: number }> = [];
344
+ let start = 0;
345
+ let hasPendingParagraph = true;
346
+
347
+ for (let index = 0; index < story.units.length; index += 1) {
348
+ const unit = story.units[index];
349
+ if (unit.kind === "paragraph_break") {
350
+ paragraphs.push({ start, end: index });
351
+ start = index + 1;
352
+ hasPendingParagraph = true;
353
+ continue;
354
+ }
355
+
356
+ if (unit.kind === "opaque_block") {
357
+ if (hasPendingParagraph) {
358
+ paragraphs.push({ start, end: index });
359
+ }
360
+ start = index + 1;
361
+ hasPendingParagraph = Boolean(unit.nextParagraph);
362
+ }
363
+ }
364
+
365
+ if (hasPendingParagraph) {
366
+ paragraphs.push({ start, end: story.units.length });
367
+ }
368
+ return paragraphs;
369
+ }
370
+
371
+ function normalizeRange(from: number, to: number): { from: number; to: number } {
372
+ return {
373
+ from: Math.min(from, to),
374
+ to: Math.max(from, to),
375
+ };
376
+ }
377
+
378
+ function findNewDetachedRevisionIds(
379
+ previousStore: RevisionStore,
380
+ nextStore: RevisionStore,
381
+ ): string[] {
382
+ const detachedRevisionIds: string[] = [];
383
+
384
+ for (const [revisionId, revision] of Object.entries(nextStore.revisions)) {
385
+ if (
386
+ revision.status === "detached" &&
387
+ previousStore.revisions[revisionId]?.status !== "detached"
388
+ ) {
389
+ detachedRevisionIds.push(revisionId);
390
+ }
391
+ }
392
+
393
+ return detachedRevisionIds;
394
+ }
@@ -0,0 +1,303 @@
1
+ import type { TransactionMapping } from "../../core/selection/mapping.ts";
2
+ import {
3
+ describeRevisionKind,
4
+ getRevisionActionability,
5
+ isDetachedRevisionAnchor,
6
+ isRevisionActionable,
7
+ remapRevisionAnchor,
8
+ summarizeRevisionAnchor,
9
+ type RevisionActionability,
10
+ type RevisionKind,
11
+ type RevisionMetadataEnvelope,
12
+ type RevisionRecord,
13
+ type RevisionStatus,
14
+ } from "./revision-types.ts";
15
+
16
+ export interface RevisionStore {
17
+ version: "revision-store/1";
18
+ revisions: Record<string, RevisionRecord>;
19
+ }
20
+
21
+ export interface CreateRevisionRecordParams {
22
+ revisionId: string;
23
+ kind: RevisionKind;
24
+ anchor: RevisionRecord["anchor"];
25
+ authorId: string;
26
+ createdAt: string;
27
+ status?: Exclude<RevisionStatus, "detached">;
28
+ warningIds?: string[];
29
+ metadata?: Partial<RevisionMetadataEnvelope>;
30
+ }
31
+
32
+ export interface RevisionSidebarEntry {
33
+ revisionId: string;
34
+ kind: RevisionKind;
35
+ label: string;
36
+ status: RevisionStatus;
37
+ actionability: RevisionActionability;
38
+ anchorLabel: string;
39
+ createdAt: string;
40
+ authorId: string;
41
+ warningCount: number;
42
+ canAccept: boolean;
43
+ canReject: boolean;
44
+ preserveOnlyReason?: string;
45
+ }
46
+
47
+ export interface RevisionSidebarProjection {
48
+ totalCount: number;
49
+ activeRevisionIds: string[];
50
+ acceptedRevisionIds: string[];
51
+ rejectedRevisionIds: string[];
52
+ detachedRevisionIds: string[];
53
+ actionableRevisionIds: string[];
54
+ preserveOnlyRevisionIds: string[];
55
+ revisions: RevisionSidebarEntry[];
56
+ }
57
+
58
+ export function createRevisionStore(
59
+ revisions: Record<string, RevisionRecord> = {},
60
+ ): RevisionStore {
61
+ return {
62
+ version: "revision-store/1",
63
+ revisions,
64
+ };
65
+ }
66
+
67
+ export function createRevisionRecord(
68
+ params: CreateRevisionRecordParams,
69
+ ): RevisionRecord {
70
+ return normalizeRevisionRecord({
71
+ revisionId: params.revisionId,
72
+ kind: params.kind,
73
+ anchor: params.anchor,
74
+ authorId: params.authorId,
75
+ createdAt: params.createdAt,
76
+ status: params.status ?? "active",
77
+ warningIds: [...(params.warningIds ?? [])],
78
+ metadata: {
79
+ source: params.metadata?.source ?? "runtime",
80
+ preserveOnlyReason:
81
+ params.metadata?.preserveOnlyReason ??
82
+ (getRevisionActionability(params.kind) === "preserve-only"
83
+ ? "Imported preserve-only revision."
84
+ : undefined),
85
+ importedRevisionForm: params.metadata?.importedRevisionForm,
86
+ originalRevisionType: params.metadata?.originalRevisionType,
87
+ ooxmlRevisionId: params.metadata?.ooxmlRevisionId,
88
+ propertyChangeData: params.metadata?.propertyChangeData,
89
+ moveData: params.metadata?.moveData,
90
+ },
91
+ });
92
+ }
93
+
94
+ export function upsertRevisionRecord(
95
+ store: RevisionStore,
96
+ revision: RevisionRecord,
97
+ ): RevisionStore {
98
+ return {
99
+ ...store,
100
+ revisions: {
101
+ ...store.revisions,
102
+ [revision.revisionId]: normalizeRevisionRecord(revision),
103
+ },
104
+ };
105
+ }
106
+
107
+ export function setRevisionStatus(
108
+ store: RevisionStore,
109
+ revisionId: string,
110
+ status: Exclude<RevisionStatus, "detached">,
111
+ ): RevisionStore {
112
+ const existing = store.revisions[revisionId];
113
+ if (!existing) {
114
+ return store;
115
+ }
116
+
117
+ if (existing.status === "detached") {
118
+ return store;
119
+ }
120
+
121
+ if (
122
+ (status === "accepted" || status === "rejected") &&
123
+ getRevisionActionability(existing) !== "actionable"
124
+ ) {
125
+ return store;
126
+ }
127
+
128
+ return upsertRevisionRecord(store, {
129
+ ...existing,
130
+ status,
131
+ });
132
+ }
133
+
134
+ export function setRevisionWarnings(
135
+ store: RevisionStore,
136
+ revisionId: string,
137
+ warningIds: string[],
138
+ ): RevisionStore {
139
+ const existing = store.revisions[revisionId];
140
+ if (!existing) {
141
+ return store;
142
+ }
143
+
144
+ return upsertRevisionRecord(store, {
145
+ ...existing,
146
+ warningIds: [...warningIds],
147
+ });
148
+ }
149
+
150
+ export function remapRevisionStore(
151
+ store: RevisionStore,
152
+ mapping: TransactionMapping,
153
+ ): RevisionStore {
154
+ return createRevisionStore(
155
+ Object.fromEntries(
156
+ Object.entries(store.revisions).map(([revisionId, revision]) => {
157
+ const anchor = remapRevisionAnchor(revision.anchor, mapping);
158
+ const status =
159
+ anchor.kind === "detached"
160
+ ? "detached"
161
+ : revision.status === "accepted" || revision.status === "rejected"
162
+ ? revision.status
163
+ : "active";
164
+
165
+ return [
166
+ revisionId,
167
+ normalizeRevisionRecord({
168
+ ...revision,
169
+ anchor,
170
+ status,
171
+ }),
172
+ ];
173
+ }),
174
+ ),
175
+ );
176
+ }
177
+
178
+ export function createRevisionSidebarProjection(
179
+ store: RevisionStore,
180
+ ): RevisionSidebarProjection {
181
+ const revisions = Object.values(store.revisions)
182
+ .map(toSidebarEntry)
183
+ .sort(compareRevisionEntries);
184
+
185
+ return {
186
+ totalCount: revisions.length,
187
+ activeRevisionIds: revisions
188
+ .filter((revision) => revision.status === "active")
189
+ .map((revision) => revision.revisionId),
190
+ acceptedRevisionIds: revisions
191
+ .filter((revision) => revision.status === "accepted")
192
+ .map((revision) => revision.revisionId),
193
+ rejectedRevisionIds: revisions
194
+ .filter((revision) => revision.status === "rejected")
195
+ .map((revision) => revision.revisionId),
196
+ detachedRevisionIds: revisions
197
+ .filter((revision) => revision.status === "detached")
198
+ .map((revision) => revision.revisionId),
199
+ actionableRevisionIds: revisions
200
+ .filter((revision) => revision.actionability === "actionable")
201
+ .map((revision) => revision.revisionId),
202
+ preserveOnlyRevisionIds: revisions
203
+ .filter((revision) => revision.actionability === "preserve-only")
204
+ .map((revision) => revision.revisionId),
205
+ revisions,
206
+ };
207
+ }
208
+
209
+ function toSidebarEntry(revision: RevisionRecord): RevisionSidebarEntry {
210
+ const anchorSummary = summarizeRevisionAnchor(revision.anchor);
211
+ const actionability = getRevisionActionability(revision);
212
+
213
+ return {
214
+ revisionId: revision.revisionId,
215
+ kind: revision.kind,
216
+ label: describeRevisionKind(revision.kind),
217
+ status: revision.status,
218
+ actionability,
219
+ anchorLabel:
220
+ anchorSummary.state === "detached"
221
+ ? `Detached ${anchorSummary.range.from}-${anchorSummary.range.to}`
222
+ : `Range ${anchorSummary.range.from}-${anchorSummary.range.to}`,
223
+ createdAt: revision.createdAt,
224
+ authorId: revision.authorId,
225
+ warningCount: revision.warningIds.length,
226
+ canAccept: isRevisionActionable(revision),
227
+ canReject: isRevisionActionable(revision),
228
+ preserveOnlyReason: revision.metadata.preserveOnlyReason,
229
+ };
230
+ }
231
+
232
+ function normalizeRevisionRecord(revision: RevisionRecord): RevisionRecord {
233
+ const actionability = getRevisionActionability(revision);
234
+
235
+ if (isDetachedRevisionAnchor(revision.anchor)) {
236
+ return {
237
+ ...revision,
238
+ status: "detached",
239
+ metadata: {
240
+ ...revision.metadata,
241
+ preserveOnlyReason:
242
+ revision.metadata.preserveOnlyReason ??
243
+ (actionability === "preserve-only"
244
+ ? "Imported preserve-only revision."
245
+ : undefined),
246
+ },
247
+ };
248
+ }
249
+
250
+ if (actionability === "preserve-only" && revision.status !== "detached") {
251
+ return {
252
+ ...revision,
253
+ status: revision.status === "accepted" || revision.status === "rejected" ? "active" : revision.status,
254
+ metadata: {
255
+ ...revision.metadata,
256
+ preserveOnlyReason:
257
+ revision.metadata.preserveOnlyReason ?? "Imported preserve-only revision.",
258
+ },
259
+ };
260
+ }
261
+
262
+ return revision;
263
+ }
264
+
265
+ function compareRevisionEntries(
266
+ left: RevisionSidebarEntry,
267
+ right: RevisionSidebarEntry,
268
+ ): number {
269
+ const statusDelta = priorityForStatus(left.status) - priorityForStatus(right.status);
270
+ if (statusDelta !== 0) {
271
+ return statusDelta;
272
+ }
273
+
274
+ const actionabilityDelta =
275
+ priorityForActionability(left.actionability) - priorityForActionability(right.actionability);
276
+ if (actionabilityDelta !== 0) {
277
+ return actionabilityDelta;
278
+ }
279
+
280
+ return left.createdAt.localeCompare(right.createdAt);
281
+ }
282
+
283
+ function priorityForStatus(status: RevisionStatus): number {
284
+ switch (status) {
285
+ case "active":
286
+ return 0;
287
+ case "detached":
288
+ return 1;
289
+ case "accepted":
290
+ return 2;
291
+ case "rejected":
292
+ return 3;
293
+ }
294
+ }
295
+
296
+ function priorityForActionability(actionability: RevisionActionability): number {
297
+ switch (actionability) {
298
+ case "actionable":
299
+ return 0;
300
+ case "preserve-only":
301
+ return 1;
302
+ }
303
+ }