@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,967 @@
1
+ import {
2
+ createEditorState,
3
+ createPersistedEditorSnapshot,
4
+ deriveDocumentStats,
5
+ type CanonicalDocumentEnvelope,
6
+ type CommentEntryRecord,
7
+ type CommentThreadRecord,
8
+ type CompatibilityFeatureEntry as InternalCompatibilityFeatureEntry,
9
+ type CompatibilityReport as InternalCompatibilityReport,
10
+ type EditorError as InternalEditorError,
11
+ type EditorState,
12
+ type EditorWarning as InternalEditorWarning,
13
+ } from "../core/state/editor-state.ts";
14
+ import type {
15
+ AddCommentParams,
16
+ CommentSidebarSnapshot,
17
+ CommentSidebarThreadSnapshot,
18
+ CompatibilityReport,
19
+ EditorAnchorProjection,
20
+ EditorError,
21
+ EditorWarning,
22
+ ExportDocxOptions,
23
+ ExportResult,
24
+ PersistedEditorSnapshot,
25
+ RuntimeRenderSnapshot,
26
+ SelectionSnapshot,
27
+ TrackedChangeEntrySnapshot,
28
+ TrackedChangesSnapshot,
29
+ WordReviewEditorEvent,
30
+ } from "../api/public-types";
31
+ import {
32
+ executeEditorCommand,
33
+ selectionChanged,
34
+ type CommandOrigin,
35
+ type EditorCommand,
36
+ type EditorTransaction,
37
+ } from "../core/commands/index.ts";
38
+ import {
39
+ createDetachedAnchor,
40
+ createNodeAnchor,
41
+ createRangeAnchor,
42
+ type EditorAnchorProjection as InternalEditorAnchorProjection,
43
+ } from "../core/selection/mapping.ts";
44
+ import { createCommentSidebarProjection } from "../review/store/comment-store.ts";
45
+ import { createCommentStoreFromRuntimeComments } from "../review/store/runtime-comment-store.ts";
46
+ import {
47
+ createRevisionSidebarProjection,
48
+ type RevisionStore,
49
+ } from "../review/store/revision-store.ts";
50
+ import { buildCompatibilityReport } from "../validation/compatibility-engine.ts";
51
+ import { createEditorSurfaceSnapshot } from "./surface-projection.ts";
52
+
53
+ export type Unsubscribe = () => void;
54
+
55
+ export interface DocumentRuntime {
56
+ subscribe(listener: () => void): Unsubscribe;
57
+ subscribeToEvents(listener: (event: WordReviewEditorEvent) => void): Unsubscribe;
58
+ getRenderSnapshot(): RuntimeRenderSnapshot;
59
+ dispatch(command: EditorCommand): void;
60
+ undo(): void;
61
+ redo(): void;
62
+ focus(): void;
63
+ blur(): void;
64
+ addComment(params: AddCommentParams): string;
65
+ openComment(commentId: string): void;
66
+ resolveComment(commentId: string): void;
67
+ reopenComment(commentId: string): void;
68
+ addCommentReply(commentId: string, body: string, authorId?: string): void;
69
+ editCommentBody(commentId: string, body: string): void;
70
+ acceptChange(changeId: string): void;
71
+ rejectChange(changeId: string): void;
72
+ acceptAllChanges(): void;
73
+ rejectAllChanges(): void;
74
+ getPersistedSnapshot(): PersistedEditorSnapshot;
75
+ getCompatibilityReport(): CompatibilityReport;
76
+ getWarnings(): EditorWarning[];
77
+ exportDocx(options?: ExportDocxOptions): Promise<ExportResult>;
78
+ }
79
+
80
+ export interface CreateDocumentRuntimeOptions {
81
+ documentId: string;
82
+ initialSnapshot?: PersistedEditorSnapshot;
83
+ initialCanonicalDocument?: CanonicalDocumentEnvelope;
84
+ sourceLabel?: string;
85
+ sourceKind?: "docx" | "snapshot" | "datastore" | "canonical";
86
+ readOnly?: boolean;
87
+ editorBuild?: string;
88
+ defaultAuthorId?: string;
89
+ fatalError?: EditorError;
90
+ clock?: () => string;
91
+ exportDocx?: (
92
+ snapshot: PersistedEditorSnapshot,
93
+ options?: ExportDocxOptions,
94
+ ) => Promise<ExportResult>;
95
+ onEvent?: (event: WordReviewEditorEvent) => void;
96
+ onWarning?: (warning: EditorWarning) => void;
97
+ onError?: (error: EditorError) => void;
98
+ }
99
+
100
+ interface HistoryState {
101
+ past: EditorState[];
102
+ future: EditorState[];
103
+ }
104
+
105
+ export function createDocumentRuntime(
106
+ options: CreateDocumentRuntimeOptions,
107
+ ): DocumentRuntime {
108
+ const clock = options.clock ?? (() => new Date().toISOString());
109
+ const editorBuild = options.editorBuild ?? "dev";
110
+ const sessionId = createSessionId(options.documentId, clock());
111
+ const listeners = new Set<() => void>();
112
+ const eventListeners = new Set<(event: WordReviewEditorEvent) => void>();
113
+ const history: HistoryState = {
114
+ past: [],
115
+ future: [],
116
+ };
117
+
118
+ let state = createEditorState({
119
+ documentId: options.documentId,
120
+ sessionId,
121
+ sourceLabel: options.sourceLabel,
122
+ readOnly: options.readOnly,
123
+ persistedSnapshot: options.initialSnapshot as never,
124
+ canonicalDocument: options.initialCanonicalDocument,
125
+ fatalError: options.fatalError as never,
126
+ });
127
+ let cachedRenderSnapshot = createPublicRenderSnapshot(state, history);
128
+
129
+ emit({
130
+ type: "ready",
131
+ documentId: state.documentId,
132
+ sessionId: state.sessionId,
133
+ source: options.sourceKind ?? (options.initialSnapshot ? "snapshot" : "canonical"),
134
+ stats: toPublicDocumentStats(state),
135
+ compatibility: toPublicCompatibilityReport(createDerivedCompatibility(state)),
136
+ });
137
+ if (options.fatalError) {
138
+ emit({
139
+ type: "error",
140
+ documentId: state.documentId,
141
+ error: options.fatalError,
142
+ });
143
+ }
144
+
145
+ return {
146
+ subscribe(listener) {
147
+ listeners.add(listener);
148
+ return () => {
149
+ listeners.delete(listener);
150
+ };
151
+ },
152
+ subscribeToEvents(listener) {
153
+ eventListeners.add(listener);
154
+ return () => {
155
+ eventListeners.delete(listener);
156
+ };
157
+ },
158
+ getRenderSnapshot() {
159
+ return cachedRenderSnapshot;
160
+ },
161
+ dispatch(command) {
162
+ if (command.type === "history.undo") {
163
+ applyHistory("undo");
164
+ return;
165
+ }
166
+
167
+ if (command.type === "history.redo") {
168
+ applyHistory("redo");
169
+ return;
170
+ }
171
+
172
+ try {
173
+ const transaction = executeEditorCommand(state, command, {
174
+ timestamp: command.origin?.timestamp ?? clock(),
175
+ });
176
+ commit(transaction);
177
+ } catch (error) {
178
+ emitError(toRuntimeError(error));
179
+ }
180
+ },
181
+ undo() {
182
+ this.dispatch({
183
+ type: "history.undo",
184
+ origin: createOrigin("runtime", clock()),
185
+ });
186
+ },
187
+ redo() {
188
+ this.dispatch({
189
+ type: "history.redo",
190
+ origin: createOrigin("runtime", clock()),
191
+ });
192
+ },
193
+ focus() {
194
+ this.dispatch({
195
+ type: "runtime.focus",
196
+ focused: true,
197
+ origin: createOrigin("api", clock()),
198
+ });
199
+ },
200
+ blur() {
201
+ this.dispatch({
202
+ type: "runtime.focus",
203
+ focused: false,
204
+ origin: createOrigin("api", clock()),
205
+ });
206
+ },
207
+ addComment(params) {
208
+ const commentId = createEntityId("comment", state.document.review.comments, clock());
209
+ const anchor = params.anchor
210
+ ? toInternalAnchorProjection(params.anchor)
211
+ : state.selection.activeRange;
212
+ const authorId = params.authorId ?? options.defaultAuthorId ?? "unknown";
213
+ const createdAt = clock();
214
+ const entries: CommentEntryRecord[] = [
215
+ {
216
+ entryId: `${commentId}-entry-1`,
217
+ authorId,
218
+ body: params.body ?? "",
219
+ createdAt,
220
+ },
221
+ ];
222
+ const comment: CommentThreadRecord = {
223
+ commentId,
224
+ anchor,
225
+ createdAt,
226
+ createdBy: authorId,
227
+ authorId,
228
+ body: params.body ?? "",
229
+ entries,
230
+ status: anchor.kind === "detached" ? "detached" : "open",
231
+ warningIds: [],
232
+ isResolved: false,
233
+ metadata: {
234
+ source: "runtime",
235
+ },
236
+ };
237
+
238
+ this.dispatch({
239
+ type: "comment.add",
240
+ comment,
241
+ origin: createOrigin("api", clock()),
242
+ });
243
+
244
+ return commentId;
245
+ },
246
+ openComment(commentId) {
247
+ this.dispatch({
248
+ type: "comment.open",
249
+ commentId,
250
+ origin: createOrigin("api", clock()),
251
+ });
252
+ },
253
+ resolveComment(commentId) {
254
+ this.dispatch({
255
+ type: "comment.resolve",
256
+ commentId,
257
+ resolvedBy: options.defaultAuthorId ?? "unknown",
258
+ origin: createOrigin("api", clock()),
259
+ });
260
+ },
261
+ reopenComment(commentId) {
262
+ this.dispatch({
263
+ type: "comment.reopen",
264
+ commentId,
265
+ origin: createOrigin("api", clock()),
266
+ });
267
+ },
268
+ addCommentReply(commentId, body, authorId) {
269
+ this.dispatch({
270
+ type: "comment.add-reply",
271
+ commentId,
272
+ body,
273
+ authorId: authorId ?? options.defaultAuthorId,
274
+ origin: createOrigin("api", clock()),
275
+ });
276
+ },
277
+ editCommentBody(commentId, body) {
278
+ this.dispatch({
279
+ type: "comment.edit-body",
280
+ commentId,
281
+ body,
282
+ origin: createOrigin("api", clock()),
283
+ });
284
+ },
285
+ acceptChange(changeId) {
286
+ this.dispatch({
287
+ type: "change.accept",
288
+ changeId,
289
+ origin: createOrigin("api", clock()),
290
+ });
291
+ },
292
+ rejectChange(changeId) {
293
+ this.dispatch({
294
+ type: "change.reject",
295
+ changeId,
296
+ origin: createOrigin("api", clock()),
297
+ });
298
+ },
299
+ acceptAllChanges() {
300
+ this.dispatch({
301
+ type: "change.accept-all",
302
+ origin: createOrigin("api", clock()),
303
+ });
304
+ },
305
+ rejectAllChanges() {
306
+ this.dispatch({
307
+ type: "change.reject-all",
308
+ origin: createOrigin("api", clock()),
309
+ });
310
+ },
311
+ getPersistedSnapshot() {
312
+ const compatibility = createDerivedCompatibility(state);
313
+ return createPersistedEditorSnapshot(state, {
314
+ editorBuild,
315
+ savedAt: clock(),
316
+ compatibility,
317
+ }) as unknown as PersistedEditorSnapshot;
318
+ },
319
+ getCompatibilityReport() {
320
+ return toPublicCompatibilityReport(createDerivedCompatibility(state));
321
+ },
322
+ getWarnings() {
323
+ return state.warnings.map((warning) => toPublicWarning(warning));
324
+ },
325
+ async exportDocx(exportOptions) {
326
+ if (!options.exportDocx) {
327
+ const error: InternalEditorError = {
328
+ errorId: createEntityId("error", {}, clock()),
329
+ code: "export_failed",
330
+ isFatal: false,
331
+ message: "DOCX export requires an injected exporter until the IO substrate lands.",
332
+ source: "export",
333
+ details: {
334
+ requestedOptions: exportOptions ?? {},
335
+ },
336
+ };
337
+ emitError(error);
338
+ throw new Error(error.message);
339
+ }
340
+
341
+ const result = await options.exportDocx(
342
+ createPersistedEditorSnapshot(state, {
343
+ editorBuild,
344
+ savedAt: clock(),
345
+ compatibility: createDerivedCompatibility(state),
346
+ }) as unknown as PersistedEditorSnapshot,
347
+ exportOptions,
348
+ );
349
+
350
+ emit({
351
+ type: "export_completed",
352
+ documentId: state.documentId,
353
+ result,
354
+ });
355
+
356
+ return result;
357
+ },
358
+ };
359
+
360
+ function applyHistory(direction: "undo" | "redo"): void {
361
+ const source = direction === "undo" ? history.past : history.future;
362
+ const target = source.pop();
363
+
364
+ if (!target) {
365
+ return;
366
+ }
367
+
368
+ const counterpart = direction === "undo" ? history.future : history.past;
369
+ counterpart.push(state);
370
+
371
+ const previous = state;
372
+ // Undo/redo changes the document — must mint a new revisionToken so
373
+ // autosave/export checkpoint dedup treats it as fresh content.
374
+ state = finalizeState(target, true, clock());
375
+ cachedRenderSnapshot = createPublicRenderSnapshot(state, history);
376
+ notify(previous, state, {
377
+ nextState: state,
378
+ mapping: { steps: [] },
379
+ effects: {
380
+ warningsAdded: [],
381
+ warningsCleared: [],
382
+ },
383
+ historyBoundary: "skip",
384
+ markDirty: true,
385
+ });
386
+ }
387
+
388
+ function commit(transaction: EditorTransaction): void {
389
+ const previous = state;
390
+
391
+ if (transaction.historyBoundary === "push") {
392
+ history.past.push(state);
393
+ history.future = [];
394
+ }
395
+
396
+ state = finalizeState(transaction.nextState, transaction.markDirty, clock());
397
+ cachedRenderSnapshot = createPublicRenderSnapshot(state, history);
398
+ notify(previous, state, transaction);
399
+ }
400
+
401
+ function notify(
402
+ previous: EditorState,
403
+ next: EditorState,
404
+ transaction: EditorTransaction,
405
+ ): void {
406
+ if (previous.isDirty !== next.isDirty) {
407
+ emit({
408
+ type: "dirty_changed",
409
+ documentId: next.documentId,
410
+ isDirty: next.isDirty,
411
+ });
412
+ }
413
+
414
+ if (selectionChanged(previous.selection, next.selection)) {
415
+ emit({
416
+ type: "selection_changed",
417
+ documentId: next.documentId,
418
+ selection: toPublicSelectionSnapshot(next.selection),
419
+ });
420
+ }
421
+
422
+ if (transaction.effects.commentAdded) {
423
+ emit({
424
+ type: "comment_added",
425
+ documentId: next.documentId,
426
+ commentId: transaction.effects.commentAdded.commentId,
427
+ anchor: toPublicAnchorProjection(transaction.effects.commentAdded.anchor),
428
+ });
429
+ }
430
+
431
+ if (transaction.effects.commentResolved) {
432
+ emit({
433
+ type: "comment_resolved",
434
+ documentId: next.documentId,
435
+ commentId: transaction.effects.commentResolved.commentId,
436
+ });
437
+ }
438
+
439
+ if (transaction.effects.changeAccepted) {
440
+ emit({
441
+ type: "change_accepted",
442
+ documentId: next.documentId,
443
+ changeId: transaction.effects.changeAccepted.changeId,
444
+ });
445
+ }
446
+
447
+ if (transaction.effects.changeRejected) {
448
+ emit({
449
+ type: "change_rejected",
450
+ documentId: next.documentId,
451
+ changeId: transaction.effects.changeRejected.changeId,
452
+ });
453
+ }
454
+
455
+ for (const warning of transaction.effects.warningsAdded) {
456
+ const publicWarning = toPublicWarning(warning);
457
+ emit({
458
+ type: "warning_added",
459
+ documentId: next.documentId,
460
+ warning: publicWarning,
461
+ });
462
+ options.onWarning?.(publicWarning);
463
+ }
464
+
465
+ for (const cleared of transaction.effects.warningsCleared) {
466
+ emit({
467
+ type: "warning_cleared",
468
+ documentId: next.documentId,
469
+ warningId: cleared.warningId,
470
+ code: cleared.code,
471
+ });
472
+ }
473
+
474
+ for (const listener of listeners) {
475
+ listener();
476
+ }
477
+ }
478
+
479
+ function emit(event: WordReviewEditorEvent): void {
480
+ options.onEvent?.(event);
481
+ for (const listener of eventListeners) {
482
+ listener(event);
483
+ }
484
+ }
485
+
486
+ function emitError(error: InternalEditorError): void {
487
+ const nextState: EditorState = {
488
+ ...state,
489
+ phase: error.isFatal ? "error" : state.phase,
490
+ fatalError: error.isFatal ? error : state.fatalError,
491
+ };
492
+ state = nextState;
493
+ cachedRenderSnapshot = createPublicRenderSnapshot(state, history);
494
+ const publicError = toPublicError(error);
495
+ options.onError?.(publicError);
496
+ emit({
497
+ type: "error",
498
+ documentId: state.documentId,
499
+ error: publicError,
500
+ });
501
+ for (const listener of listeners) {
502
+ listener();
503
+ }
504
+ }
505
+ }
506
+
507
+ function createSessionId(documentId: string, timestamp: string): string {
508
+ return `session-${documentId}-${timestamp.replace(/[^0-9]/gu, "")}`;
509
+ }
510
+
511
+ function createOrigin(
512
+ source: CommandOrigin["source"],
513
+ timestamp: string,
514
+ ): CommandOrigin {
515
+ return {
516
+ source,
517
+ timestamp,
518
+ };
519
+ }
520
+
521
+ function createEntityId(
522
+ prefix: string,
523
+ existing: Record<string, unknown>,
524
+ timestamp: string,
525
+ ): string {
526
+ let counter = Object.keys(existing).length;
527
+ let nextId = `${prefix}-${timestamp.replace(/[^0-9]/gu, "")}-${counter}`;
528
+
529
+ while (existing[nextId]) {
530
+ counter += 1;
531
+ nextId = `${prefix}-${timestamp.replace(/[^0-9]/gu, "")}-${counter}`;
532
+ }
533
+
534
+ return nextId;
535
+ }
536
+
537
+ function finalizeState(
538
+ state: EditorState,
539
+ markDirty: boolean,
540
+ timestamp: string,
541
+ ): EditorState {
542
+ // Only increment revision on actual document mutations (markDirty=true).
543
+ // Selection-only changes must not churn the revisionToken, which would
544
+ // cause autosave/checkpoint dedup to treat cursor movement as new content.
545
+ const revision = markDirty ? state.revision + 1 : state.revision;
546
+
547
+ return {
548
+ ...state,
549
+ document: {
550
+ ...state.document,
551
+ updatedAt: markDirty ? timestamp : state.document.updatedAt,
552
+ },
553
+ selection: state.selection,
554
+ revision,
555
+ revisionToken: `${state.sessionId}:${revision}`,
556
+ isDirty: state.isDirty || markDirty,
557
+ };
558
+ }
559
+
560
+ function toRuntimeError(error: unknown): InternalEditorError {
561
+ if (typeof error === "object" && error && "message" in error) {
562
+ return {
563
+ errorId: createSessionId("runtime-error", new Date().toISOString()),
564
+ code: "internal_invariant",
565
+ isFatal: false,
566
+ message: String((error as { message?: unknown }).message ?? "Runtime error"),
567
+ source: "runtime",
568
+ };
569
+ }
570
+
571
+ return {
572
+ errorId: createSessionId("runtime-error", new Date().toISOString()),
573
+ code: "internal_invariant",
574
+ isFatal: false,
575
+ message: "Runtime error",
576
+ source: "runtime",
577
+ };
578
+ }
579
+
580
+ function createPublicRenderSnapshot(
581
+ state: EditorState,
582
+ history: HistoryState,
583
+ ): RuntimeRenderSnapshot {
584
+ const compatibility = createDerivedCompatibility(state);
585
+ const surface = createEditorSurfaceSnapshot(state.document, state.selection);
586
+ const comments = toPublicCommentSidebarSnapshot(state);
587
+ const trackedChanges = toPublicTrackedChangesSnapshot(state, surface.plainText);
588
+
589
+ return {
590
+ documentId: state.documentId,
591
+ sessionId: state.sessionId,
592
+ sourceLabel: state.sourceLabel,
593
+ revisionToken: state.revisionToken,
594
+ isReady: state.phase === "ready",
595
+ isDirty: state.isDirty,
596
+ readOnly: state.readOnly,
597
+ selection: toPublicSelectionSnapshot(state.selection),
598
+ documentStats: toPublicDocumentStats(state),
599
+ comments,
600
+ trackedChanges,
601
+ compatibility: {
602
+ blockExport: compatibility.blockExport,
603
+ blockExportReasons: listBlockExportReasons(compatibility),
604
+ warningCount: compatibility.warnings.length,
605
+ errorCount: compatibility.errors.length,
606
+ featureEntries: compatibility.featureEntries.map((entry) =>
607
+ toPublicCompatibilityFeatureEntry(entry),
608
+ ),
609
+ },
610
+ warnings: state.warnings.map((warning) => toPublicWarning(warning)),
611
+ fatalError: state.fatalError ? toPublicError(state.fatalError) : undefined,
612
+ commandState: {
613
+ canUndo: history.past.length > 0,
614
+ canRedo: history.future.length > 0,
615
+ readOnly: state.readOnly,
616
+ },
617
+ surface,
618
+ };
619
+ }
620
+
621
+ function toPublicDocumentStats(state: Pick<EditorState, "document">) {
622
+ const stats = deriveDocumentStats(state);
623
+ return {
624
+ storyLength: stats.characterCount,
625
+ commentCount: stats.commentCount,
626
+ revisionCount: stats.revisionCount,
627
+ opaqueFragmentCount: countOpaqueFragments(state.document.preservation.opaqueFragments),
628
+ };
629
+ }
630
+
631
+ function toPublicSelectionSnapshot(
632
+ selection: EditorState["selection"],
633
+ ): SelectionSnapshot {
634
+ return {
635
+ anchor: selection.anchor,
636
+ head: selection.head,
637
+ isCollapsed: selection.isCollapsed,
638
+ activeRange: toPublicAnchorProjection(selection.activeRange),
639
+ };
640
+ }
641
+
642
+ function toPublicAnchorProjection(
643
+ anchor: InternalEditorAnchorProjection,
644
+ ): EditorAnchorProjection {
645
+ switch (anchor.kind) {
646
+ case "range":
647
+ return {
648
+ kind: "range",
649
+ from: anchor.range.from,
650
+ to: anchor.range.to,
651
+ assoc: anchor.assoc,
652
+ };
653
+ case "node":
654
+ return {
655
+ kind: "node",
656
+ at: anchor.at,
657
+ assoc: anchor.assoc,
658
+ };
659
+ case "detached":
660
+ return {
661
+ kind: "detached",
662
+ lastKnownRange: anchor.lastKnownRange,
663
+ reason: anchor.reason,
664
+ };
665
+ }
666
+ }
667
+
668
+ function toInternalAnchorProjection(
669
+ anchor: EditorAnchorProjection,
670
+ ): InternalEditorAnchorProjection {
671
+ switch (anchor.kind) {
672
+ case "range":
673
+ return createRangeAnchor(anchor.from, anchor.to, anchor.assoc);
674
+ case "node":
675
+ return createNodeAnchor(anchor.at, anchor.assoc);
676
+ case "detached":
677
+ return createDetachedAnchor(anchor.lastKnownRange, anchor.reason);
678
+ }
679
+ }
680
+
681
+ function toPublicCompatibilityReport(
682
+ report: InternalCompatibilityReport,
683
+ ): CompatibilityReport {
684
+ return {
685
+ reportVersion: report.reportVersion,
686
+ generatedAt: report.generatedAt,
687
+ blockExport: report.blockExport,
688
+ featureEntries: report.featureEntries.map((entry) =>
689
+ toPublicCompatibilityFeatureEntry(entry),
690
+ ),
691
+ warnings: report.warnings.map((warning) => toPublicWarning(warning)),
692
+ errors: report.errors.map((error) => toPublicError(error)),
693
+ };
694
+ }
695
+
696
+ function toPublicCompatibilityFeatureEntry(
697
+ entry: InternalCompatibilityFeatureEntry,
698
+ ) {
699
+ return {
700
+ ...entry,
701
+ affectedAnchor: entry.affectedAnchor
702
+ ? toPublicAnchorProjection(entry.affectedAnchor)
703
+ : undefined,
704
+ };
705
+ }
706
+
707
+ function toPublicWarning(warning: InternalEditorWarning): EditorWarning {
708
+ return {
709
+ ...warning,
710
+ affectedAnchor: warning.affectedAnchor
711
+ ? toPublicAnchorProjection(warning.affectedAnchor)
712
+ : undefined,
713
+ };
714
+ }
715
+
716
+ function toPublicError(error: InternalEditorError): EditorError {
717
+ return { ...error };
718
+ }
719
+
720
+ function countOpaqueFragments(opaqueFragments: Record<string, unknown>): number {
721
+ return Object.keys(opaqueFragments).length;
722
+ }
723
+
724
+ function createDerivedCompatibility(state: EditorState): InternalCompatibilityReport {
725
+ return buildCompatibilityReport({
726
+ document: state.document,
727
+ warnings: state.warnings,
728
+ fatalError: state.fatalError,
729
+ generatedAt: state.document.updatedAt,
730
+ });
731
+ }
732
+
733
+ function toPublicCommentSidebarSnapshot(
734
+ state: EditorState,
735
+ ): CommentSidebarSnapshot {
736
+ const projection = createCommentSidebarProjection(
737
+ createCommentStoreFromRuntimeComments(state.document.review.comments),
738
+ state.runtime.activeCommentId,
739
+ );
740
+
741
+ return {
742
+ activeCommentId: state.runtime.activeCommentId,
743
+ openCommentIds: projection.openCommentIds,
744
+ resolvedCommentIds: projection.resolvedCommentIds,
745
+ detachedCommentIds: projection.detachedCommentIds,
746
+ totalCount: projection.totalCount,
747
+ threads: projection.threads.map((thread): CommentSidebarThreadSnapshot => {
748
+ const sourceThread = state.document.review.comments[thread.commentId];
749
+ const projectedEntries =
750
+ sourceThread?.entries?.map((entry) => ({
751
+ entryId: entry.entryId,
752
+ authorId: entry.authorId,
753
+ body: entry.body,
754
+ createdAt: entry.createdAt,
755
+ })) ??
756
+ (sourceThread?.body
757
+ ? [
758
+ {
759
+ entryId: `${thread.commentId}-entry-1`,
760
+ authorId:
761
+ sourceThread.authorId ?? sourceThread.createdBy ?? "unknown",
762
+ body: sourceThread.body,
763
+ createdAt: sourceThread.createdAt,
764
+ },
765
+ ]
766
+ : []);
767
+
768
+ return {
769
+ commentId: thread.commentId,
770
+ status: thread.status,
771
+ anchor: toPublicAnchorProjection(
772
+ sourceThread?.anchor ??
773
+ createDetachedAnchor({ from: 0, to: 0 }, "importAmbiguity"),
774
+ ),
775
+ excerpt: thread.excerpt,
776
+ entryCount: thread.entryCount,
777
+ createdAt: thread.createdAt,
778
+ createdBy: thread.createdBy,
779
+ warningCount: thread.warningCount,
780
+ anchorLabel: thread.anchorLabel,
781
+ isActive: thread.isActive,
782
+ resolvedAt: thread.resolvedAt,
783
+ resolvedBy: thread.resolvedBy,
784
+ entries: projectedEntries,
785
+ };
786
+ }),
787
+ };
788
+ }
789
+
790
+ function toPublicTrackedChangesSnapshot(
791
+ state: EditorState,
792
+ surfaceText = "",
793
+ ): TrackedChangesSnapshot {
794
+ const projection = createRevisionSidebarProjection(
795
+ createRevisionStoreFromDocument(state),
796
+ );
797
+
798
+ return {
799
+ pendingChangeIds: projection.activeRevisionIds,
800
+ acceptedChangeIds: projection.acceptedRevisionIds,
801
+ rejectedChangeIds: projection.rejectedRevisionIds,
802
+ detachedChangeIds: projection.detachedRevisionIds,
803
+ actionableChangeIds: projection.actionableRevisionIds,
804
+ preserveOnlyChangeIds: projection.preserveOnlyRevisionIds,
805
+ totalCount: projection.totalCount,
806
+ revisions: projection.revisions.map((revision): TrackedChangeEntrySnapshot => {
807
+ const sourceRevision = state.document.review.revisions[revision.revisionId];
808
+ const preview = describeRevisionPreview(
809
+ revision,
810
+ sourceRevision?.anchor ??
811
+ createDetachedAnchor({ from: 0, to: 0 }, "importAmbiguity"),
812
+ surfaceText,
813
+ );
814
+
815
+ return {
816
+ revisionId: revision.revisionId,
817
+ kind: revision.kind,
818
+ label: revision.label,
819
+ status: revision.status,
820
+ actionability: revision.actionability,
821
+ anchor: toPublicAnchorProjection(
822
+ sourceRevision?.anchor ??
823
+ createDetachedAnchor({ from: 0, to: 0 }, "importAmbiguity"),
824
+ ),
825
+ anchorLabel: revision.anchorLabel,
826
+ createdAt: revision.createdAt,
827
+ authorId: revision.authorId,
828
+ warningCount: revision.warningCount,
829
+ canAccept: revision.canAccept,
830
+ canReject: revision.canReject,
831
+ importedRevisionForm: sourceRevision?.metadata?.importedRevisionForm,
832
+ preserveOnlyReason: revision.preserveOnlyReason,
833
+ excerpt: preview.excerpt,
834
+ detail: preview.detail,
835
+ };
836
+ }),
837
+ };
838
+ }
839
+
840
+ function createRevisionStoreFromDocument(
841
+ state: Pick<EditorState, "document">,
842
+ ): RevisionStore {
843
+ return {
844
+ version: "revision-store/1",
845
+ revisions: Object.fromEntries(
846
+ Object.values(state.document.review.revisions).map((revision) => [
847
+ revision.changeId,
848
+ {
849
+ revisionId: revision.changeId,
850
+ kind: revision.kind,
851
+ anchor: revision.anchor,
852
+ authorId: revision.authorId ?? "unknown",
853
+ createdAt: revision.createdAt,
854
+ status:
855
+ revision.status === "open"
856
+ ? "active"
857
+ : revision.status,
858
+ warningIds: [...(revision.warningIds ?? [])],
859
+ metadata: {
860
+ source: revision.metadata?.source ?? "runtime",
861
+ preserveOnlyReason: revision.metadata?.preserveOnlyReason,
862
+ importedRevisionForm: revision.metadata?.importedRevisionForm,
863
+ originalRevisionType: revision.metadata?.originalRevisionType,
864
+ ooxmlRevisionId: revision.metadata?.ooxmlRevisionId,
865
+ },
866
+ },
867
+ ]),
868
+ ),
869
+ };
870
+ }
871
+
872
+ function listBlockExportReasons(
873
+ report: InternalCompatibilityReport,
874
+ ): string[] {
875
+ return [
876
+ ...report.featureEntries
877
+ .filter((entry) => entry.featureClass === "unsupported-fatal")
878
+ .map((entry) => entry.message),
879
+ ...report.errors
880
+ .filter((error) => error.isFatal)
881
+ .map((error) => error.message),
882
+ ];
883
+ }
884
+
885
+ function describeRevisionPreview(
886
+ revision: ReturnType<typeof createRevisionSidebarProjection>["revisions"][number],
887
+ anchor: InternalEditorAnchorProjection,
888
+ plainText: string,
889
+ ): { excerpt: string; detail: string } {
890
+ const { from, to } = toAnchorBounds(anchor);
891
+ const excerpt = summarizeRevisionExcerpt(plainText, from, to, revision.label);
892
+
893
+ if (revision.actionability === "preserve-only") {
894
+ return {
895
+ excerpt,
896
+ detail:
897
+ revision.preserveOnlyReason ??
898
+ "Visible for review, but this change remains preserve-only in the current runtime.",
899
+ };
900
+ }
901
+
902
+ if (revision.status === "accepted") {
903
+ return {
904
+ excerpt,
905
+ detail: "Accepted in the live review runtime and retained here for audit visibility.",
906
+ };
907
+ }
908
+
909
+ if (revision.status === "rejected") {
910
+ return {
911
+ excerpt,
912
+ detail: "Rejected in the live review runtime and retained here for audit visibility.",
913
+ };
914
+ }
915
+
916
+ return {
917
+ excerpt,
918
+ detail:
919
+ revision.kind === "deletion"
920
+ ? "Deleted content stays reviewable here until it is accepted or rejected."
921
+ : "Runtime-backed change. Review it here or reopen the anchor in the canvas.",
922
+ };
923
+ }
924
+
925
+ function toAnchorBounds(anchor: InternalEditorAnchorProjection): { from: number; to: number } {
926
+ switch (anchor.kind) {
927
+ case "range":
928
+ return {
929
+ from: Math.min(anchor.range.from, anchor.range.to),
930
+ to: Math.max(anchor.range.from, anchor.range.to),
931
+ };
932
+ case "node":
933
+ return {
934
+ from: anchor.at,
935
+ to: anchor.at + 1,
936
+ };
937
+ case "detached":
938
+ return {
939
+ from: Math.min(anchor.lastKnownRange.from, anchor.lastKnownRange.to),
940
+ to: Math.max(anchor.lastKnownRange.from, anchor.lastKnownRange.to),
941
+ };
942
+ }
943
+ }
944
+
945
+ function summarizeRevisionExcerpt(
946
+ plainText: string,
947
+ from: number,
948
+ to: number,
949
+ fallback: string,
950
+ ): string {
951
+ const normalizedFrom = Math.max(0, Math.min(from, plainText.length));
952
+ const normalizedTo = Math.max(normalizedFrom, Math.min(to, plainText.length));
953
+ const collapsed = plainText
954
+ .slice(normalizedFrom, normalizedTo)
955
+ .replace(/\s+/g, " ")
956
+ .trim();
957
+
958
+ if (!collapsed) {
959
+ return fallback;
960
+ }
961
+
962
+ return collapsed.length > 96 ? `${collapsed.slice(0, 93)}...` : collapsed;
963
+ }
964
+
965
+ function isRecord(value: unknown): value is Record<string, unknown> {
966
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
967
+ }