@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,185 @@
1
+ import type {
2
+ DocRange,
3
+ OpaqueFragmentRecord,
4
+ PreservationStore,
5
+ PreservedPackagePart,
6
+ } from "../model/canonical-document.ts";
7
+
8
+ export interface OpaqueFragmentDescriptor {
9
+ featureKey:
10
+ | "sections"
11
+ | "tables"
12
+ | "headers-footers"
13
+ | "fields"
14
+ | "content-controls"
15
+ | "custom-xml"
16
+ | "alt-chunk"
17
+ | "embedded-objects"
18
+ | "alternate-content"
19
+ | "unknown-ooxml";
20
+ label: string;
21
+ detail: string;
22
+ }
23
+
24
+ export function createPreservationStore(
25
+ seed?: Partial<PreservationStore>,
26
+ ): PreservationStore {
27
+ return {
28
+ opaqueFragments: { ...(seed?.opaqueFragments ?? {}) },
29
+ packageParts: { ...(seed?.packageParts ?? {}) },
30
+ };
31
+ }
32
+
33
+ export function getOpaqueFragment(
34
+ store: PreservationStore,
35
+ fragmentId: string,
36
+ ): OpaqueFragmentRecord | undefined {
37
+ return normalizeOpaqueFragmentMap(store)[fragmentId];
38
+ }
39
+
40
+ export function listOpaqueFragments(
41
+ store: PreservationStore,
42
+ ): OpaqueFragmentRecord[] {
43
+ return Object.values(normalizeOpaqueFragmentMap(store)).sort((left, right) => {
44
+ return (
45
+ left.lastKnownRange.from - right.lastKnownRange.from ||
46
+ left.fragmentId.localeCompare(right.fragmentId)
47
+ );
48
+ });
49
+ }
50
+
51
+ export function listPreservedPackageParts(
52
+ store: PreservationStore,
53
+ ): PreservedPackagePart[] {
54
+ return Object.values(normalizePackagePartMap(store)).sort((left, right) =>
55
+ left.packagePartName.localeCompare(right.packagePartName),
56
+ );
57
+ }
58
+
59
+ export function findOpaqueFragmentsIntersectingRange(
60
+ store: PreservationStore,
61
+ range: DocRange,
62
+ ): OpaqueFragmentRecord[] {
63
+ return listOpaqueFragments(store).filter((fragment) =>
64
+ rangesIntersect(fragment.lastKnownRange, range),
65
+ );
66
+ }
67
+
68
+ export function describeOpaqueFragment(
69
+ fragment: OpaqueFragmentRecord,
70
+ ): OpaqueFragmentDescriptor {
71
+ const xml = fragment.payloadReference;
72
+ const detail = createDetail(fragment);
73
+
74
+ if (/\b(?:w:)?sectPr\b/u.test(xml)) {
75
+ return {
76
+ featureKey: "sections",
77
+ label: "Section properties",
78
+ detail,
79
+ };
80
+ }
81
+
82
+ if (/\b(?:w:)?tbl\b/u.test(xml)) {
83
+ return {
84
+ featureKey: "tables",
85
+ label: "Preserved table structure",
86
+ detail,
87
+ };
88
+ }
89
+
90
+ if (/\b(?:w:)?hdr\b|\b(?:w:)?ftr\b/u.test(xml)) {
91
+ return {
92
+ featureKey: "headers-footers",
93
+ label: "Header or footer content",
94
+ detail,
95
+ };
96
+ }
97
+
98
+ if (/\b(?:w:)?fldSimple\b|\b(?:w:)?fldChar\b|\b(?:w:)?instrText\b/u.test(xml)) {
99
+ return {
100
+ featureKey: "fields",
101
+ label: "Word field content",
102
+ detail,
103
+ };
104
+ }
105
+
106
+ if (/\b(?:w:)?sdt\b/u.test(xml)) {
107
+ return {
108
+ featureKey: "content-controls",
109
+ label: "Content control",
110
+ detail,
111
+ };
112
+ }
113
+
114
+ if (/\b(?:w:)?customXml\b/u.test(xml)) {
115
+ return {
116
+ featureKey: "custom-xml",
117
+ label: "Custom XML wrapper",
118
+ detail,
119
+ };
120
+ }
121
+
122
+ if (/\b(?:w:)?altChunk\b/u.test(xml)) {
123
+ return {
124
+ featureKey: "alt-chunk",
125
+ label: "Alternate content import",
126
+ detail,
127
+ };
128
+ }
129
+
130
+ if (/\b(?:w:)?object\b|\b(?:o:)?OLEObject\b/u.test(xml)) {
131
+ return {
132
+ featureKey: "embedded-objects",
133
+ label: "Embedded object",
134
+ detail,
135
+ };
136
+ }
137
+
138
+ if (/\b(?:mc:)?AlternateContent\b/u.test(xml)) {
139
+ return {
140
+ featureKey: "alternate-content",
141
+ label: "Markup compatibility block",
142
+ detail,
143
+ };
144
+ }
145
+
146
+ return {
147
+ featureKey: "unknown-ooxml",
148
+ label: "Unsupported OOXML fragment",
149
+ detail,
150
+ };
151
+ }
152
+
153
+ function createDetail(fragment: OpaqueFragmentRecord): string {
154
+ const detail = [
155
+ `Preserved whole-unit from ${fragment.lastKnownRange.from}-${fragment.lastKnownRange.to}.`,
156
+ fragment.packagePartName ? `Part ${fragment.packagePartName}.` : null,
157
+ fragment.relationshipId ? `Relationship ${fragment.relationshipId}.` : null,
158
+ ]
159
+ .filter(Boolean)
160
+ .join(" ");
161
+
162
+ return detail.length > 0
163
+ ? detail
164
+ : "Preserved whole-unit to keep unsupported OOXML intact.";
165
+ }
166
+
167
+ function rangesIntersect(left: DocRange, right: DocRange): boolean {
168
+ return left.from < right.to && right.from < left.to;
169
+ }
170
+
171
+ function normalizeOpaqueFragmentMap(
172
+ store: PreservationStore,
173
+ ): Record<string, OpaqueFragmentRecord> {
174
+ return store && typeof store === "object" && store.opaqueFragments && typeof store.opaqueFragments === "object"
175
+ ? (store.opaqueFragments as Record<string, OpaqueFragmentRecord>)
176
+ : {};
177
+ }
178
+
179
+ function normalizePackagePartMap(
180
+ store: PreservationStore,
181
+ ): Record<string, PreservedPackagePart> {
182
+ return store && typeof store === "object" && store.packageParts && typeof store.packageParts === "object"
183
+ ? (store.packageParts as Record<string, PreservedPackagePart>)
184
+ : {};
185
+ }
@@ -0,0 +1,16 @@
1
+ # Review
2
+
3
+ Comments, anchors, revisions, remap helpers, and accept/reject behavior belong here.
4
+
5
+ Frozen Wave 1 contract:
6
+
7
+ - comments and revisions live in the canonical `review` store, not in the content tree
8
+ - review anchors use canonical position ranges or detached-anchor payloads, never DOM paths
9
+ - all review mutations flow through runtime commands and transactions
10
+ - detached comments or revisions remain addressable with reason metadata; they are not silently dropped
11
+ - v1 authoring is limited to single-paragraph comments, thread replies, tracked insertions, tracked deletions, and accept/reject flows
12
+ - preserve-only review structures such as multi-paragraph editable comment ranges, tracked moves, and structural list/table revisions stay locked and warning-backed
13
+
14
+ Key subdirectories:
15
+
16
+ - `store/`
@@ -0,0 +1,3 @@
1
+ # Review Store
2
+
3
+ Comment threads, revision records, review selectors, and accept/reject state belong here.
@@ -0,0 +1,70 @@
1
+ import {
2
+ createNodeAnchor,
3
+ createRangeAnchor,
4
+ mapAnchor,
5
+ normalizeRange,
6
+ type Assoc,
7
+ type BoundaryAssoc,
8
+ type DocRange,
9
+ type EditorAnchorProjection,
10
+ type TransactionMapping,
11
+ } from "../../core/selection/mapping.ts";
12
+
13
+ export type CommentAnchor = EditorAnchorProjection;
14
+ export type CommentAnchorState = "active" | "detached";
15
+
16
+ export interface CommentAnchorSummary {
17
+ anchor: CommentAnchor;
18
+ state: CommentAnchorState;
19
+ range: DocRange;
20
+ }
21
+
22
+ export function createCommentRangeAnchor(
23
+ from: number,
24
+ to = from,
25
+ assoc?: BoundaryAssoc,
26
+ ): CommentAnchor {
27
+ return createRangeAnchor(from, to, assoc);
28
+ }
29
+
30
+ export function createCommentNodeAnchor(at: number, assoc?: Assoc): CommentAnchor {
31
+ return createNodeAnchor(at, assoc);
32
+ }
33
+
34
+ export function remapCommentAnchor(
35
+ anchor: CommentAnchor,
36
+ mapping: TransactionMapping,
37
+ ): CommentAnchor {
38
+ return mapAnchor(anchor, mapping);
39
+ }
40
+
41
+ export function isDetachedCommentAnchor(anchor: CommentAnchor): boolean {
42
+ return anchor.kind === "detached";
43
+ }
44
+
45
+ export function summarizeCommentAnchor(anchor: CommentAnchor): CommentAnchorSummary {
46
+ if (anchor.kind === "range") {
47
+ return {
48
+ anchor,
49
+ state: "active",
50
+ range: normalizeRange(anchor.range),
51
+ };
52
+ }
53
+
54
+ if (anchor.kind === "node") {
55
+ return {
56
+ anchor,
57
+ state: "active",
58
+ range: {
59
+ from: anchor.at,
60
+ to: anchor.at,
61
+ },
62
+ };
63
+ }
64
+
65
+ return {
66
+ anchor,
67
+ state: "detached",
68
+ range: normalizeRange(anchor.lastKnownRange),
69
+ };
70
+ }
@@ -0,0 +1,154 @@
1
+ import type { CommentThreadRecord, EditorWarning } from "../../core/state/editor-state.ts";
2
+ import {
3
+ detachReviewAnchor,
4
+ getAnchorRange,
5
+ mapReviewAnchor,
6
+ mappingTouchesAnchorContent,
7
+ rangeStaysWithinSingleParagraph,
8
+ type ReviewAnchor,
9
+ } from "../../core/selection/review-anchors.ts";
10
+ import type { TransactionMapping } from "../../core/selection/mapping.ts";
11
+
12
+ export interface RemapCommentThreadsOptions {
13
+ comments: Record<string, CommentThreadRecord>;
14
+ mapping: TransactionMapping;
15
+ nextContent: unknown;
16
+ existingWarnings?: EditorWarning[];
17
+ }
18
+
19
+ export interface RemapCommentThreadsResult {
20
+ comments: Record<string, CommentThreadRecord>;
21
+ warnings: EditorWarning[];
22
+ detachedCommentIds: string[];
23
+ }
24
+
25
+ export function remapCommentThreads(
26
+ options: RemapCommentThreadsOptions,
27
+ ): RemapCommentThreadsResult {
28
+ const comments = Object.fromEntries(
29
+ Object.entries(options.comments).map(([commentId, comment]) => [
30
+ commentId,
31
+ remapCommentThread(comment, options.mapping, options.nextContent),
32
+ ]),
33
+ );
34
+ const detachedCommentIds = Object.values(comments)
35
+ .filter((comment) => comment.anchor.kind === "detached")
36
+ .map((comment) => comment.commentId);
37
+
38
+ return {
39
+ comments,
40
+ warnings: mergeDetachedAnchorWarnings(comments, options.existingWarnings ?? []),
41
+ detachedCommentIds,
42
+ };
43
+ }
44
+
45
+ export function remapCommentThread(
46
+ comment: CommentThreadRecord,
47
+ mapping: TransactionMapping,
48
+ nextContent: unknown,
49
+ ): CommentThreadRecord {
50
+ if (comment.anchor.kind === "detached") {
51
+ return comment;
52
+ }
53
+
54
+ const mappedAnchor = mapReviewAnchor(comment.anchor, mapping);
55
+ const anchor = normalizeCommentAnchor(comment.anchor, mappedAnchor, mapping, nextContent);
56
+
57
+ return {
58
+ ...comment,
59
+ anchor,
60
+ };
61
+ }
62
+
63
+ function normalizeCommentAnchor(
64
+ previousAnchor: ReviewAnchor,
65
+ mappedAnchor: ReviewAnchor,
66
+ mapping: TransactionMapping,
67
+ nextContent: unknown,
68
+ ): ReviewAnchor {
69
+ if (mappedAnchor.kind === "detached") {
70
+ return mappedAnchor;
71
+ }
72
+
73
+ const previousRange = getAnchorRange(previousAnchor);
74
+ const mappedRange = getAnchorRange(mappedAnchor);
75
+
76
+ if (
77
+ previousAnchor.kind === "range" &&
78
+ previousRange.from < previousRange.to &&
79
+ mappedAnchor.kind === "range" &&
80
+ mappedRange.from === mappedRange.to &&
81
+ mappingTouchesAnchorContent(previousAnchor, mapping)
82
+ ) {
83
+ return detachReviewAnchor(previousRange, detachReason(mapping));
84
+ }
85
+
86
+ if (
87
+ mappedAnchor.kind === "range" &&
88
+ !rangeStaysWithinSingleParagraph(nextContent, mappedAnchor.range)
89
+ ) {
90
+ return detachReviewAnchor(previousRange, "invalidatedByStructureChange");
91
+ }
92
+
93
+ return mappedAnchor;
94
+ }
95
+
96
+ function detachReason(
97
+ mapping: TransactionMapping,
98
+ ): "deleted" | "invalidatedByStructureChange" {
99
+ return mapping.metadata?.invalidatesStructures
100
+ ? "invalidatedByStructureChange"
101
+ : "deleted";
102
+ }
103
+
104
+ function mergeDetachedAnchorWarnings(
105
+ comments: Record<string, CommentThreadRecord>,
106
+ existingWarnings: EditorWarning[],
107
+ ): EditorWarning[] {
108
+ const retainedWarnings = existingWarnings.filter(
109
+ (warning) =>
110
+ warning.code !== "comment_anchor_detached" ||
111
+ !warning.details ||
112
+ typeof warning.details.commentId !== "string" ||
113
+ comments[warning.details.commentId]?.anchor.kind === "detached",
114
+ );
115
+
116
+ const knownDetachedIds = new Set(
117
+ retainedWarnings
118
+ .filter((warning) => warning.code === "comment_anchor_detached")
119
+ .map((warning) =>
120
+ typeof warning.details?.commentId === "string"
121
+ ? warning.details.commentId
122
+ : undefined,
123
+ )
124
+ .filter((value): value is string => Boolean(value)),
125
+ );
126
+
127
+ const detachedWarnings = Object.values(comments)
128
+ .filter(
129
+ (comment) =>
130
+ comment.anchor.kind === "detached" && !knownDetachedIds.has(comment.commentId),
131
+ )
132
+ .map((comment) => createDetachedCommentWarning(comment));
133
+
134
+ return [...retainedWarnings, ...detachedWarnings];
135
+ }
136
+
137
+ function createDetachedCommentWarning(
138
+ comment: CommentThreadRecord,
139
+ ): EditorWarning {
140
+ const anchor = comment.anchor.kind === "detached" ? comment.anchor : undefined;
141
+
142
+ return {
143
+ warningId: `warning:comment-anchor-detached:${comment.commentId}`,
144
+ code: "comment_anchor_detached",
145
+ severity: "warning",
146
+ message: `Comment ${comment.commentId} detached after edit remapping.`,
147
+ source: "review",
148
+ affectedAnchor: anchor,
149
+ details: {
150
+ commentId: comment.commentId,
151
+ reason: anchor?.reason,
152
+ },
153
+ };
154
+ }