@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,580 @@
1
+ import {
2
+ DEFAULT_BOUNDARY_ASSOC,
3
+ createDetachedAnchor,
4
+ createRangeAnchor,
5
+ type EditorAnchorProjection,
6
+ } from "../selection/mapping.ts";
7
+ import {
8
+ assertPersistedEditorSnapshot as assertModelPersistedEditorSnapshot,
9
+ createPersistedEditorSnapshot as createModelPersistedEditorSnapshot,
10
+ type PersistedEditorSnapshot as ModelPersistedEditorSnapshot,
11
+ } from "../../model/snapshot.ts";
12
+ import {
13
+ assertCanonicalDocument,
14
+ type CanonicalDocument,
15
+ type CommentEntry as ModelCommentEntry,
16
+ type CommentResolution as ModelCommentResolution,
17
+ type CommentThread as ModelCommentThread,
18
+ type RevisionMetadataRecord as ModelRevisionMetadataRecord,
19
+ type RevisionRecord as ModelRevisionRecord,
20
+ } from "../../model/canonical-document.ts";
21
+
22
+ export type RuntimePhase = "loading" | "ready" | "error";
23
+ export type EditorWarningCode =
24
+ | "unsupported_ooxml_preserved"
25
+ | "unsupported_ooxml_locked"
26
+ | "import_normalized"
27
+ | "export_roundtrip_risk"
28
+ | "comment_anchor_detached"
29
+ | "revision_anchor_detached"
30
+ | "large_document_degraded"
31
+ | "font_substitution"
32
+ | "image_missing";
33
+
34
+ export interface EditorWarning {
35
+ warningId: string;
36
+ code: EditorWarningCode;
37
+ severity: "info" | "warning";
38
+ message: string;
39
+ source:
40
+ | "import"
41
+ | "runtime"
42
+ | "review"
43
+ | "preservation"
44
+ | "validation"
45
+ | "export";
46
+ affectedAnchor?: EditorAnchorProjection;
47
+ featureEntryId?: string;
48
+ details?: Record<string, unknown>;
49
+ }
50
+
51
+ export interface EditorError {
52
+ errorId: string;
53
+ code:
54
+ | "import_failed"
55
+ | "export_failed"
56
+ | "package_corrupt"
57
+ | "validation_failed"
58
+ | "datastore_failed"
59
+ | "internal_invariant";
60
+ message: string;
61
+ isFatal: boolean;
62
+ source: "import" | "runtime" | "validation" | "datastore" | "export";
63
+ details?: Record<string, unknown>;
64
+ }
65
+
66
+ export type CompatibilityFeatureClass =
67
+ | "supported-roundtrip"
68
+ | "preserve-only"
69
+ | "unsupported-fatal";
70
+
71
+ export interface CompatibilityFeatureEntry {
72
+ featureEntryId: string;
73
+ featureKey: string;
74
+ featureClass: CompatibilityFeatureClass;
75
+ message: string;
76
+ affectedAnchor?: EditorAnchorProjection;
77
+ details?: Record<string, unknown>;
78
+ }
79
+
80
+ export interface CompatibilityReport {
81
+ reportVersion: "compatibility-report/1";
82
+ generatedAt: string;
83
+ blockExport: boolean;
84
+ featureEntries: CompatibilityFeatureEntry[];
85
+ warnings: EditorWarning[];
86
+ errors: EditorError[];
87
+ }
88
+
89
+ export type CommentEntryRecord = ModelCommentEntry;
90
+ export type CommentResolutionRecord = ModelCommentResolution;
91
+ export type CommentThreadRecord = ModelCommentThread;
92
+ export type RevisionMetadataRecord = ModelRevisionMetadataRecord;
93
+ export type RevisionRecord = ModelRevisionRecord;
94
+ export type ReviewStore = CanonicalDocument["review"];
95
+ export type CanonicalDocumentEnvelope = CanonicalDocument;
96
+
97
+ export interface SelectionSnapshot {
98
+ anchor: number;
99
+ head: number;
100
+ isCollapsed: boolean;
101
+ activeRange: EditorAnchorProjection;
102
+ }
103
+
104
+ export interface DocumentStats {
105
+ paragraphCount: number;
106
+ wordCount: number;
107
+ characterCount: number;
108
+ commentCount: number;
109
+ revisionCount: number;
110
+ }
111
+
112
+ export interface CommentSidebarSnapshot {
113
+ activeCommentId?: string;
114
+ openThreadCount: number;
115
+ resolvedThreadCount: number;
116
+ threads: CommentThreadRecord[];
117
+ }
118
+
119
+ export interface TrackedChangesSnapshot {
120
+ totalCount: number;
121
+ pendingCount: number;
122
+ acceptedCount: number;
123
+ rejectedCount: number;
124
+ }
125
+
126
+ export interface CompatibilityPanelSnapshot {
127
+ report: CompatibilityReport;
128
+ blockExport: boolean;
129
+ totalEntries: number;
130
+ }
131
+
132
+ export interface CommandStateSnapshot {
133
+ canUndo: boolean;
134
+ canRedo: boolean;
135
+ canAddComment: boolean;
136
+ canAcceptAllChanges: boolean;
137
+ canRejectAllChanges: boolean;
138
+ }
139
+
140
+ export interface RuntimeRenderSnapshot {
141
+ documentId: string;
142
+ sessionId: string;
143
+ sourceLabel?: string;
144
+ revisionToken: string;
145
+ isReady: boolean;
146
+ isDirty: boolean;
147
+ readOnly: boolean;
148
+ selection: SelectionSnapshot;
149
+ documentStats: DocumentStats;
150
+ comments: CommentSidebarSnapshot;
151
+ trackedChanges: TrackedChangesSnapshot;
152
+ compatibility: CompatibilityPanelSnapshot;
153
+ warnings: EditorWarning[];
154
+ fatalError?: EditorError;
155
+ commandState: CommandStateSnapshot;
156
+ }
157
+
158
+ export interface PersistedEditorSnapshot
159
+ extends Omit<
160
+ ModelPersistedEditorSnapshot,
161
+ "canonicalDocument" | "compatibility" | "warningLog"
162
+ > {
163
+ canonicalDocument: CanonicalDocumentEnvelope;
164
+ compatibility: CompatibilityReport;
165
+ warningLog: EditorWarning[];
166
+ }
167
+
168
+ export interface EditorRuntimeState {
169
+ hasFocus: boolean;
170
+ activeCommentId?: string;
171
+ }
172
+
173
+ export interface EditorState {
174
+ phase: RuntimePhase;
175
+ documentId: string;
176
+ sessionId: string;
177
+ sourceLabel?: string;
178
+ revision: number;
179
+ revisionToken: string;
180
+ isDirty: boolean;
181
+ readOnly: boolean;
182
+ document: CanonicalDocumentEnvelope;
183
+ selection: SelectionSnapshot;
184
+ warnings: EditorWarning[];
185
+ compatibility: CompatibilityReport;
186
+ fatalError?: EditorError;
187
+ runtime: EditorRuntimeState;
188
+ }
189
+
190
+ export interface CreateEditorStateOptions {
191
+ documentId: string;
192
+ sessionId: string;
193
+ sourceLabel?: string;
194
+ readOnly?: boolean;
195
+ canonicalDocument?: CanonicalDocumentEnvelope;
196
+ persistedSnapshot?: PersistedEditorSnapshot;
197
+ compatibility?: CompatibilityReport;
198
+ warnings?: EditorWarning[];
199
+ fatalError?: EditorError;
200
+ }
201
+
202
+ export function createEmptyReviewStore(): ReviewStore {
203
+ return {
204
+ comments: {},
205
+ revisions: {},
206
+ };
207
+ }
208
+
209
+ export function createDefaultCanonicalDocument(
210
+ documentId: string,
211
+ timestamp: string,
212
+ ): CanonicalDocumentEnvelope {
213
+ return {
214
+ schemaVersion: "cds/1.0.0",
215
+ docId: createCanonicalDocumentId(documentId),
216
+ createdAt: timestamp,
217
+ updatedAt: timestamp,
218
+ metadata: {
219
+ customProperties: {},
220
+ },
221
+ styles: {
222
+ paragraphs: {},
223
+ characters: {},
224
+ tables: {},
225
+ },
226
+ numbering: {
227
+ abstractDefinitions: {},
228
+ instances: {},
229
+ },
230
+ media: {
231
+ items: {},
232
+ },
233
+ content: {
234
+ type: "doc",
235
+ children: [{ type: "paragraph", children: [] }],
236
+ },
237
+ review: createEmptyReviewStore(),
238
+ preservation: {
239
+ opaqueFragments: {},
240
+ packageParts: {},
241
+ },
242
+ diagnostics: {
243
+ warnings: [],
244
+ errors: [],
245
+ },
246
+ };
247
+ }
248
+
249
+ export function createSelectionSnapshot(anchor = 0, head = anchor): SelectionSnapshot {
250
+ return {
251
+ anchor,
252
+ head,
253
+ isCollapsed: anchor === head,
254
+ activeRange: createRangeAnchor(anchor, head, DEFAULT_BOUNDARY_ASSOC),
255
+ };
256
+ }
257
+
258
+ export function createEmptyCompatibilityReport(generatedAt: string): CompatibilityReport {
259
+ return {
260
+ reportVersion: "compatibility-report/1",
261
+ generatedAt,
262
+ blockExport: false,
263
+ featureEntries: [],
264
+ warnings: [],
265
+ errors: [],
266
+ };
267
+ }
268
+
269
+ export function createEditorState(options: CreateEditorStateOptions): EditorState {
270
+ const timestamp = new Date(0).toISOString();
271
+ if (options.persistedSnapshot) {
272
+ assertModelPersistedEditorSnapshot(options.persistedSnapshot as unknown);
273
+ }
274
+ if (options.canonicalDocument) {
275
+ assertCanonicalDocument(options.canonicalDocument as unknown);
276
+ }
277
+
278
+ const normalizedDocument = options.persistedSnapshot
279
+ ? structuredClone(options.persistedSnapshot.canonicalDocument)
280
+ : options.canonicalDocument
281
+ ? structuredClone(options.canonicalDocument)
282
+ : createDefaultCanonicalDocument(options.documentId, timestamp);
283
+ const warnings = options.persistedSnapshot?.warningLog ?? options.warnings ?? [];
284
+ const compatibility =
285
+ options.persistedSnapshot?.compatibility ??
286
+ options.compatibility ??
287
+ createEmptyCompatibilityReport(normalizedDocument.updatedAt);
288
+
289
+ return {
290
+ phase: options.fatalError ? "error" : "ready",
291
+ documentId: options.documentId,
292
+ sessionId: options.sessionId,
293
+ sourceLabel: options.sourceLabel,
294
+ revision: 0,
295
+ revisionToken: `${options.sessionId}:0`,
296
+ isDirty: false,
297
+ readOnly: options.readOnly ?? false,
298
+ document: normalizedDocument,
299
+ selection: createSelectionSnapshot(),
300
+ warnings,
301
+ compatibility,
302
+ fatalError: options.fatalError,
303
+ runtime: {
304
+ hasFocus: false,
305
+ },
306
+ };
307
+ }
308
+
309
+ export function deriveDocumentStats(state: Pick<EditorState, "document">): DocumentStats {
310
+ const serializedContent = extractText(state.document.content);
311
+
312
+ return {
313
+ paragraphCount: estimateParagraphCount(state.document.content),
314
+ wordCount: serializedContent.trim().length === 0 ? 0 : serializedContent.trim().split(/\s+/u).length,
315
+ characterCount: Array.from(serializedContent).length,
316
+ commentCount: Object.keys(state.document.review.comments).length,
317
+ revisionCount: Object.keys(state.document.review.revisions).length,
318
+ };
319
+ }
320
+
321
+ export function deriveRenderSnapshot(
322
+ state: EditorState,
323
+ history: { canUndo: boolean; canRedo: boolean },
324
+ ): RuntimeRenderSnapshot {
325
+ const comments = Object.values(state.document.review.comments).sort((left, right) =>
326
+ left.createdAt.localeCompare(right.createdAt),
327
+ );
328
+ const revisions = Object.values(state.document.review.revisions);
329
+
330
+ return {
331
+ documentId: state.documentId,
332
+ sessionId: state.sessionId,
333
+ sourceLabel: state.sourceLabel,
334
+ revisionToken: state.revisionToken,
335
+ isReady: state.phase === "ready",
336
+ isDirty: state.isDirty,
337
+ readOnly: state.readOnly,
338
+ selection: state.selection,
339
+ documentStats: deriveDocumentStats(state),
340
+ comments: {
341
+ activeCommentId: state.runtime.activeCommentId,
342
+ openThreadCount: comments.filter((comment) => comment.status === "open").length,
343
+ resolvedThreadCount: comments.filter((comment) => comment.status === "resolved").length,
344
+ threads: comments,
345
+ },
346
+ trackedChanges: {
347
+ totalCount: revisions.length,
348
+ pendingCount: revisions.filter((revision) => revision.status === "open").length,
349
+ acceptedCount: revisions.filter((revision) => revision.status === "accepted").length,
350
+ rejectedCount: revisions.filter((revision) => revision.status === "rejected").length,
351
+ },
352
+ compatibility: {
353
+ report: state.compatibility,
354
+ blockExport: state.compatibility.blockExport,
355
+ totalEntries: state.compatibility.featureEntries.length,
356
+ },
357
+ warnings: state.warnings,
358
+ fatalError: state.fatalError,
359
+ commandState: {
360
+ canUndo: history.canUndo,
361
+ canRedo: history.canRedo,
362
+ canAddComment: !state.readOnly,
363
+ canAcceptAllChanges: revisions.some((revision) => revision.status === "open"),
364
+ canRejectAllChanges: revisions.some((revision) => revision.status === "open"),
365
+ },
366
+ };
367
+ }
368
+
369
+ export function createPersistedEditorSnapshot(
370
+ state: EditorState,
371
+ options: {
372
+ editorBuild: string;
373
+ savedAt: string;
374
+ compatibility?: CompatibilityReport;
375
+ },
376
+ ): PersistedEditorSnapshot {
377
+ const snapshot = createModelPersistedEditorSnapshot({
378
+ documentId: state.documentId,
379
+ savedAt: options.savedAt,
380
+ editorBuild: options.editorBuild,
381
+ canonicalDocument: state.document,
382
+ compatibility: (options.compatibility ?? state.compatibility) as never,
383
+ warningLog: state.warnings as never,
384
+ });
385
+ return snapshot as PersistedEditorSnapshot;
386
+ }
387
+
388
+ function estimateParagraphCount(content: unknown): number {
389
+ if (Array.isArray(content)) {
390
+ return content.length;
391
+ }
392
+
393
+ if (content && typeof content === "object" && Array.isArray((content as { blocks?: unknown[] }).blocks)) {
394
+ return ((content as { blocks: unknown[] }).blocks).length;
395
+ }
396
+
397
+ return extractText(content).length > 0 ? 1 : 0;
398
+ }
399
+
400
+ function extractText(value: unknown): string {
401
+ if (typeof value === "string") {
402
+ return value;
403
+ }
404
+
405
+ if (Array.isArray(value)) {
406
+ return value.map((item) => extractText(item)).join(" ");
407
+ }
408
+
409
+ if (!value || typeof value !== "object") {
410
+ return "";
411
+ }
412
+
413
+ return Object.values(value as Record<string, unknown>)
414
+ .map((item) => extractText(item))
415
+ .filter(Boolean)
416
+ .join(" ");
417
+ }
418
+
419
+ function isRecord(value: unknown): value is Record<string, unknown> {
420
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
421
+ }
422
+
423
+ export function createCanonicalDocumentId(documentId: string): string {
424
+ const hashWord = (seed: string): string => {
425
+ let hash = 0x811c9dc5;
426
+ for (const char of seed) {
427
+ hash ^= char.charCodeAt(0);
428
+ hash = Math.imul(hash, 0x01000193);
429
+ }
430
+ return (hash >>> 0).toString(16).padStart(8, "0");
431
+ };
432
+
433
+ const raw =
434
+ hashWord(`${documentId}:0`) +
435
+ hashWord(`${documentId}:1`) +
436
+ hashWord(`${documentId}:2`) +
437
+ hashWord(`${documentId}:3`);
438
+
439
+ return [
440
+ raw.slice(0, 8),
441
+ raw.slice(8, 12),
442
+ `4${raw.slice(13, 16)}`,
443
+ `8${raw.slice(17, 20)}`,
444
+ raw.slice(20, 32),
445
+ ].join("-");
446
+ }
447
+
448
+ export function normalizeCommentThreadRecord(value: unknown): CommentThreadRecord {
449
+ const record = isRecord(value) ? value : {};
450
+ const authorId =
451
+ typeof record.createdBy === "string" && record.createdBy.length > 0
452
+ ? record.createdBy
453
+ : typeof record.authorId === "string" && record.authorId.length > 0
454
+ ? record.authorId
455
+ : "unknown";
456
+ const createdAt =
457
+ typeof record.createdAt === "string" && record.createdAt.length > 0
458
+ ? record.createdAt
459
+ : new Date(0).toISOString();
460
+ const entries = Array.isArray(record.entries)
461
+ ? record.entries
462
+ .filter((entry): entry is Record<string, unknown> => isRecord(entry))
463
+ .map((entry, index) => ({
464
+ entryId:
465
+ typeof entry.entryId === "string" && entry.entryId.length > 0
466
+ ? entry.entryId
467
+ : `${String(record.commentId ?? "comment")}-entry-${index + 1}`,
468
+ authorId:
469
+ typeof entry.authorId === "string" && entry.authorId.length > 0
470
+ ? entry.authorId
471
+ : authorId,
472
+ body: typeof entry.body === "string" ? entry.body : "",
473
+ createdAt:
474
+ typeof entry.createdAt === "string" && entry.createdAt.length > 0
475
+ ? entry.createdAt
476
+ : createdAt,
477
+ metadata: isRecord(entry.metadata)
478
+ ? {
479
+ ooxmlCommentId:
480
+ typeof entry.metadata.ooxmlCommentId === "string"
481
+ ? entry.metadata.ooxmlCommentId
482
+ : undefined,
483
+ paraId:
484
+ typeof entry.metadata.paraId === "string"
485
+ ? entry.metadata.paraId
486
+ : undefined,
487
+ parentParaId:
488
+ typeof entry.metadata.parentParaId === "string"
489
+ ? entry.metadata.parentParaId
490
+ : undefined,
491
+ durableId:
492
+ typeof entry.metadata.durableId === "string"
493
+ ? entry.metadata.durableId
494
+ : undefined,
495
+ initials:
496
+ typeof entry.metadata.initials === "string"
497
+ ? entry.metadata.initials
498
+ : undefined,
499
+ }
500
+ : undefined,
501
+ }))
502
+ : [
503
+ {
504
+ entryId: `${String(record.commentId ?? "comment")}-entry-1`,
505
+ authorId,
506
+ body: typeof record.body === "string" ? record.body : "",
507
+ createdAt,
508
+ },
509
+ ];
510
+ const resolution = isRecord(record.resolution)
511
+ ? {
512
+ resolvedAt:
513
+ typeof record.resolution.resolvedAt === "string"
514
+ ? record.resolution.resolvedAt
515
+ : typeof record.resolvedAt === "string"
516
+ ? record.resolvedAt
517
+ : createdAt,
518
+ resolvedBy:
519
+ typeof record.resolution.resolvedBy === "string" &&
520
+ record.resolution.resolvedBy.length > 0
521
+ ? record.resolution.resolvedBy
522
+ : authorId,
523
+ }
524
+ : typeof record.resolvedAt === "string"
525
+ ? {
526
+ resolvedAt: record.resolvedAt,
527
+ resolvedBy: authorId,
528
+ }
529
+ : undefined;
530
+ const normalizedStatus =
531
+ typeof record.status === "string"
532
+ ? record.status
533
+ : record.anchor && isRecord(record.anchor) && record.anchor.kind === "detached"
534
+ ? "detached"
535
+ : resolution || record.isResolved
536
+ ? "resolved"
537
+ : "open";
538
+
539
+ return {
540
+ commentId:
541
+ typeof record.commentId === "string" && record.commentId.length > 0
542
+ ? record.commentId
543
+ : "comment-generated",
544
+ anchor:
545
+ isRecord(record.anchor) && typeof record.anchor.kind === "string"
546
+ ? (record.anchor as unknown as EditorAnchorProjection)
547
+ : createDetachedAnchor({ from: 0, to: 0 }, "importAmbiguity"),
548
+ createdAt,
549
+ createdBy: authorId,
550
+ authorId,
551
+ body: entries.map((entry) => entry.body).join("\n"),
552
+ entries,
553
+ status:
554
+ normalizedStatus === "resolved" || normalizedStatus === "detached"
555
+ ? normalizedStatus
556
+ : "open",
557
+ resolution,
558
+ resolvedAt: resolution?.resolvedAt,
559
+ warningIds: Array.isArray(record.warningIds)
560
+ ? record.warningIds.filter((warningId): warningId is string => typeof warningId === "string")
561
+ : [],
562
+ isResolved: normalizedStatus === "resolved",
563
+ metadata: isRecord(record.metadata)
564
+ ? {
565
+ source:
566
+ record.metadata.source === "import" || record.metadata.source === "runtime"
567
+ ? record.metadata.source
568
+ : undefined,
569
+ rootOoxmlCommentId:
570
+ typeof record.metadata.rootOoxmlCommentId === "string"
571
+ ? record.metadata.rootOoxmlCommentId
572
+ : undefined,
573
+ rootParaId:
574
+ typeof record.metadata.rootParaId === "string"
575
+ ? record.metadata.rootParaId
576
+ : undefined,
577
+ }
578
+ : undefined,
579
+ };
580
+ }