@beyondwork/docx-react-component 1.0.28 → 1.0.30

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 (92) hide show
  1. package/package.json +26 -37
  2. package/src/api/public-types.ts +531 -0
  3. package/src/api/session-state.ts +2 -0
  4. package/src/core/commands/index.ts +201 -79
  5. package/src/core/commands/table-structure-commands.ts +138 -5
  6. package/src/core/state/text-transaction.ts +370 -3
  7. package/src/index.ts +41 -0
  8. package/src/io/docx-session.ts +318 -25
  9. package/src/io/export/serialize-footnotes.ts +41 -46
  10. package/src/io/export/serialize-headers-footers.ts +36 -40
  11. package/src/io/export/serialize-main-document.ts +55 -89
  12. package/src/io/export/serialize-numbering.ts +104 -4
  13. package/src/io/export/serialize-runtime-revisions.ts +196 -2
  14. package/src/io/export/split-story-blocks-for-runtime-revisions.ts +252 -0
  15. package/src/io/export/table-properties-xml.ts +318 -0
  16. package/src/io/normalize/normalize-text.ts +34 -3
  17. package/src/io/ooxml/parse-comments.ts +6 -0
  18. package/src/io/ooxml/parse-footnotes.ts +69 -13
  19. package/src/io/ooxml/parse-headers-footers.ts +54 -11
  20. package/src/io/ooxml/parse-main-document.ts +112 -42
  21. package/src/io/ooxml/parse-numbering.ts +341 -26
  22. package/src/io/ooxml/parse-revisions.ts +118 -4
  23. package/src/io/ooxml/parse-styles.ts +176 -0
  24. package/src/io/ooxml/parse-tables.ts +34 -25
  25. package/src/io/ooxml/revision-boundaries.ts +127 -3
  26. package/src/io/ooxml/workflow-payload.ts +544 -0
  27. package/src/model/canonical-document.ts +91 -1
  28. package/src/model/snapshot.ts +112 -1
  29. package/src/preservation/store.ts +73 -3
  30. package/src/review/store/comment-store.ts +19 -1
  31. package/src/review/store/revision-actions.ts +29 -0
  32. package/src/review/store/revision-store.ts +12 -1
  33. package/src/review/store/revision-types.ts +11 -0
  34. package/src/runtime/context-analytics.ts +824 -0
  35. package/src/runtime/document-locations.ts +521 -0
  36. package/src/runtime/document-navigation.ts +14 -1
  37. package/src/runtime/document-outline.ts +440 -0
  38. package/src/runtime/document-runtime.ts +941 -45
  39. package/src/runtime/event-refresh-hints.ts +137 -0
  40. package/src/runtime/numbering-prefix.ts +67 -39
  41. package/src/runtime/page-layout-estimation.ts +100 -7
  42. package/src/runtime/resolved-numbering-geometry.ts +293 -0
  43. package/src/runtime/session-capabilities.ts +2 -2
  44. package/src/runtime/suggestions-snapshot.ts +137 -0
  45. package/src/runtime/surface-projection.ts +223 -27
  46. package/src/runtime/table-style-resolver.ts +409 -0
  47. package/src/runtime/view-state.ts +17 -1
  48. package/src/runtime/workflow-markup.ts +54 -14
  49. package/src/ui/WordReviewEditor.tsx +1269 -87
  50. package/src/ui/editor-command-bag.ts +7 -0
  51. package/src/ui/editor-runtime-boundary.ts +111 -10
  52. package/src/ui/editor-shell-view.tsx +17 -15
  53. package/src/ui/editor-surface-controller.tsx +5 -0
  54. package/src/ui/headless/selection-tool-context.ts +19 -0
  55. package/src/ui/headless/selection-tool-resolver.ts +752 -0
  56. package/src/ui/headless/selection-tool-types.ts +129 -0
  57. package/src/ui/headless/selection-toolbar-model.ts +10 -33
  58. package/src/ui/runtime-shortcut-dispatch.ts +365 -0
  59. package/src/ui-tailwind/chrome/chrome-preset-model.ts +107 -0
  60. package/src/ui-tailwind/chrome/chrome-preset-toolbar.tsx +15 -0
  61. package/src/ui-tailwind/chrome/review-queue-bar.tsx +97 -0
  62. package/src/ui-tailwind/chrome/tw-context-analytics-summary.tsx +122 -0
  63. package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +1 -9
  64. package/src/ui-tailwind/chrome/tw-object-context-toolbar.tsx +1 -5
  65. package/src/ui-tailwind/chrome/tw-page-ruler.tsx +8 -29
  66. package/src/ui-tailwind/chrome/tw-selection-tool-blocked.tsx +23 -0
  67. package/src/ui-tailwind/chrome/tw-selection-tool-comment.tsx +35 -0
  68. package/src/ui-tailwind/chrome/tw-selection-tool-formatting.tsx +37 -0
  69. package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +298 -0
  70. package/src/ui-tailwind/chrome/tw-selection-tool-structure.tsx +116 -0
  71. package/src/ui-tailwind/chrome/tw-selection-tool-suggestion.tsx +29 -0
  72. package/src/ui-tailwind/chrome/tw-selection-tool-workflow.tsx +27 -0
  73. package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +3 -3
  74. package/src/ui-tailwind/chrome/tw-suggestion-card.tsx +3 -3
  75. package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +86 -14
  76. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +57 -52
  77. package/src/ui-tailwind/editor-surface/pm-decorations.ts +36 -52
  78. package/src/ui-tailwind/editor-surface/pm-schema.ts +56 -5
  79. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +87 -24
  80. package/src/ui-tailwind/editor-surface/surface-build-keys.ts +4 -0
  81. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +135 -32
  82. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +74 -7
  83. package/src/ui-tailwind/review/tw-comment-sidebar.tsx +17 -17
  84. package/src/ui-tailwind/review/tw-review-rail.tsx +19 -17
  85. package/src/ui-tailwind/review/tw-revision-sidebar.tsx +10 -10
  86. package/src/ui-tailwind/status/tw-status-bar.tsx +10 -6
  87. package/src/ui-tailwind/theme/editor-theme.css +58 -40
  88. package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +4 -4
  89. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +250 -181
  90. package/src/ui-tailwind/tw-review-workspace.tsx +323 -280
  91. package/src/validation/compatibility-engine.ts +246 -2
  92. package/src/validation/docx-comment-proof.ts +24 -11
@@ -90,11 +90,17 @@ export interface ParagraphStyleDefinition {
90
90
  basedOn?: string;
91
91
  nextStyle?: string;
92
92
  outlineLevel?: number;
93
+ numbering?: ParagraphStyleNumberingReference;
93
94
  displayName: string;
94
95
  kind: "paragraph";
95
96
  isDefault: boolean;
96
97
  }
97
98
 
99
+ export interface ParagraphStyleNumberingReference {
100
+ numberingInstanceId: string;
101
+ level?: number;
102
+ }
103
+
98
104
  export interface CharacterStyleDefinition {
99
105
  styleId: string;
100
106
  basedOn?: string;
@@ -109,6 +115,8 @@ export interface TableStyleDefinition {
109
115
  displayName: string;
110
116
  kind: "table";
111
117
  isDefault: boolean;
118
+ formatting?: TableStyleFormatting;
119
+ conditionalFormatting?: Partial<Record<TableStyleConditionalRegion, TableStyleFormatting>>;
112
120
  }
113
121
 
114
122
  export interface LatentStyleDefinition {
@@ -130,6 +138,13 @@ export interface AbstractNumberingDefinition {
130
138
  levels: NumberingLevelDefinition[];
131
139
  }
132
140
 
141
+ export interface NumberingLevelParagraphGeometry {
142
+ justification?: "left" | "center" | "right" | "both" | "distribute";
143
+ spacing?: ParagraphSpacing;
144
+ indentation?: ParagraphIndentation;
145
+ tabStops?: TabStop[];
146
+ }
147
+
133
148
  export interface NumberingLevelDefinition {
134
149
  level: number;
135
150
  format: string;
@@ -138,6 +153,18 @@ export interface NumberingLevelDefinition {
138
153
  paragraphStyleId?: string;
139
154
  isLegalNumbering?: boolean;
140
155
  suffix?: "tab" | "space" | "nothing";
156
+ paragraphGeometry?: NumberingLevelParagraphGeometry;
157
+ }
158
+
159
+ export interface NumberingLevelOverrideDefinition {
160
+ level: number;
161
+ format?: string;
162
+ text?: string;
163
+ startAt?: number;
164
+ paragraphStyleId?: string;
165
+ isLegalNumbering?: boolean;
166
+ suffix?: "tab" | "space" | "nothing";
167
+ paragraphGeometry?: NumberingLevelParagraphGeometry;
141
168
  }
142
169
 
143
170
  export interface NumberingInstance {
@@ -149,6 +176,7 @@ export interface NumberingInstance {
149
176
  export interface NumberingLevelOverride {
150
177
  level: number;
151
178
  startAt?: number;
179
+ levelDefinition?: NumberingLevelOverrideDefinition;
152
180
  }
153
181
 
154
182
  export interface MediaCatalog {
@@ -303,7 +331,7 @@ export interface ParagraphIndentation {
303
331
 
304
332
  export interface TabStop {
305
333
  position: number;
306
- align: "left" | "center" | "right" | "decimal" | "bar" | "clear";
334
+ align: "left" | "center" | "right" | "decimal" | "num" | "bar" | "clear";
307
335
  leader?: "none" | "dot" | "hyphen" | "underscore" | "heavy" | "middleDot";
308
336
  }
309
337
 
@@ -400,6 +428,37 @@ export interface TableLook {
400
428
  noVBand?: boolean;
401
429
  }
402
430
 
431
+ export type TableStyleConditionalRegion =
432
+ | "firstRow"
433
+ | "lastRow"
434
+ | "firstColumn"
435
+ | "lastColumn"
436
+ | "band1Horz"
437
+ | "band2Horz"
438
+ | "band1Vert"
439
+ | "band2Vert";
440
+
441
+ export interface TableStyleFormatting {
442
+ table?: {
443
+ width?: TableWidth;
444
+ alignment?: "left" | "center" | "right";
445
+ borders?: TableBorders;
446
+ cellMargins?: TableCellMargins;
447
+ tblLook?: TableLook;
448
+ };
449
+ row?: {
450
+ height?: number;
451
+ heightRule?: "auto" | "atLeast" | "exact";
452
+ isHeader?: boolean;
453
+ };
454
+ cell?: {
455
+ width?: TableWidth;
456
+ borders?: TableCellBorders;
457
+ shading?: CellShading;
458
+ verticalAlign?: "top" | "center" | "bottom";
459
+ };
460
+ }
461
+
403
462
  export interface TableNode {
404
463
  type: "table";
405
464
  styleId?: string;
@@ -476,6 +535,7 @@ export interface CustomXmlNode {
476
535
  type: "custom_xml";
477
536
  uri?: string;
478
537
  element?: string;
538
+ rawXml?: string;
479
539
  children: BlockNode[];
480
540
  }
481
541
 
@@ -935,6 +995,8 @@ export interface CommentThreadMetadata {
935
995
  source?: "runtime" | "import";
936
996
  rootOoxmlCommentId?: string;
937
997
  rootParaId?: string;
998
+ detachedReason?: "incomplete-markers" | "multi-paragraph" | "opaque-region" | "revision-overlap";
999
+ actionabilityNote?: string;
938
1000
  }
939
1001
 
940
1002
  export interface RevisionPropertyChangeData {
@@ -945,6 +1007,7 @@ export interface RevisionPropertyChangeData {
945
1007
  export interface RevisionMoveData {
946
1008
  moveId: string;
947
1009
  direction: "from" | "to";
1010
+ linkedRevisionId?: string;
948
1011
  }
949
1012
 
950
1013
  export type RevisionStoryTargetRecord =
@@ -979,6 +1042,17 @@ export interface RevisionMetadataRecord {
979
1042
  source?: "runtime" | "import";
980
1043
  storyTarget?: RevisionStoryTargetRecord;
981
1044
  preserveOnlyReason?: string;
1045
+ suggestionId?: string;
1046
+ semanticKind?:
1047
+ | "insertion"
1048
+ | "deletion"
1049
+ | "replacement"
1050
+ | "formatting-change"
1051
+ | "paragraph-property-change"
1052
+ | "structural-change"
1053
+ | "object-change";
1054
+ linkedRevisionIds?: string[];
1055
+ predecessorSuggestionId?: string;
982
1056
  importedRevisionForm?:
983
1057
  | "run-insertion"
984
1058
  | "run-deletion"
@@ -1909,6 +1983,9 @@ function validateRevisionMetadata(
1909
1983
  }
1910
1984
  for (const field of [
1911
1985
  "preserveOnlyReason",
1986
+ "suggestionId",
1987
+ "semanticKind",
1988
+ "predecessorSuggestionId",
1912
1989
  "importedRevisionForm",
1913
1990
  "originalRevisionType",
1914
1991
  "ooxmlRevisionId",
@@ -1918,6 +1995,19 @@ function validateRevisionMetadata(
1918
1995
  }
1919
1996
  }
1920
1997
 
1998
+ if (record.linkedRevisionIds !== undefined) {
1999
+ if (!Array.isArray(record.linkedRevisionIds)) {
2000
+ issues.push({
2001
+ path: `${path}.linkedRevisionIds`,
2002
+ message: "linkedRevisionIds must be an array of strings.",
2003
+ });
2004
+ } else {
2005
+ for (const [index, value] of record.linkedRevisionIds.entries()) {
2006
+ expectString(value, `${path}.linkedRevisionIds.${index}`, issues);
2007
+ }
2008
+ }
2009
+ }
2010
+
1921
2011
  if (record.propertyChangeData !== undefined) {
1922
2012
  const pcd = asPlainObject(record.propertyChangeData, `${path}.propertyChangeData`, issues);
1923
2013
  if (pcd) {
@@ -98,6 +98,30 @@ export interface PersistedSourcePackage {
98
98
  bytesBase64: Base64;
99
99
  }
100
100
 
101
+ export interface PersistedWorkflowMetadataDefinition {
102
+ metadataId: string;
103
+ kind: string;
104
+ label: string;
105
+ color?: string;
106
+ icon?: string;
107
+ persistence: "runtime-only" | "session" | "document-metadata";
108
+ }
109
+
110
+ export interface PersistedWorkflowMetadataEntry {
111
+ entryId: string;
112
+ metadataId: string;
113
+ anchor: Record<string, unknown>;
114
+ storyTarget?: Record<string, unknown>;
115
+ value?: Record<string, unknown>;
116
+ scopeId?: string;
117
+ workItemId?: string;
118
+ }
119
+
120
+ export interface PersistedWorkflowMetadataSnapshot {
121
+ definitions: PersistedWorkflowMetadataDefinition[];
122
+ entries: PersistedWorkflowMetadataEntry[];
123
+ }
124
+
101
125
  export interface PersistedEditorSnapshot {
102
126
  snapshotVersion: PersistedEditorSnapshotVersion;
103
127
  schemaVersion: typeof CDS_SCHEMA_VERSION;
@@ -112,6 +136,7 @@ export interface PersistedEditorSnapshot {
112
136
  warningLog: EditorWarning[];
113
137
  protectionSnapshot?: ProtectionSnapshotRecord;
114
138
  sourcePackage?: PersistedSourcePackage;
139
+ workflowMetadata?: PersistedWorkflowMetadataSnapshot;
115
140
  }
116
141
 
117
142
  const SNAPSHOT_REQUIRED_TOP_LEVEL_KEYS = [
@@ -128,7 +153,7 @@ const SNAPSHOT_REQUIRED_TOP_LEVEL_KEYS = [
128
153
  "warningLog",
129
154
  ] as const;
130
155
 
131
- const SNAPSHOT_OPTIONAL_TOP_LEVEL_KEYS = ["sourcePackage", "protectionSnapshot"] as const;
156
+ const SNAPSHOT_OPTIONAL_TOP_LEVEL_KEYS = ["sourcePackage", "protectionSnapshot", "workflowMetadata"] as const;
132
157
 
133
158
  export interface ProtectionRangeRecord {
134
159
  rangeId: string;
@@ -283,6 +308,9 @@ export function validatePersistedEditorSnapshot(
283
308
  if (record.protectionSnapshot !== undefined) {
284
309
  validateProtectionSnapshot(record.protectionSnapshot, "$.protectionSnapshot", issues);
285
310
  }
311
+ if (record.workflowMetadata !== undefined) {
312
+ validateWorkflowMetadataSnapshot(record.workflowMetadata, "$.workflowMetadata", issues);
313
+ }
286
314
 
287
315
  return issues;
288
316
  }
@@ -614,6 +642,89 @@ function validateProtectionSnapshot(
614
642
  }
615
643
  }
616
644
 
645
+ function validateWorkflowMetadataSnapshot(
646
+ value: unknown,
647
+ path: string,
648
+ issues: ModelValidationIssue[],
649
+ ): void {
650
+ const record = asPlainObject(value, path, issues);
651
+ if (!record) {
652
+ return;
653
+ }
654
+ if (!Array.isArray(record.definitions)) {
655
+ issues.push({ path: `${path}.definitions`, message: "definitions must be an array." });
656
+ } else {
657
+ record.definitions.forEach((definition, index) =>
658
+ validateWorkflowMetadataDefinition(definition, `${path}.definitions[${index}]`, issues),
659
+ );
660
+ }
661
+ if (!Array.isArray(record.entries)) {
662
+ issues.push({ path: `${path}.entries`, message: "entries must be an array." });
663
+ } else {
664
+ record.entries.forEach((entry, index) =>
665
+ validateWorkflowMetadataEntry(entry, `${path}.entries[${index}]`, issues),
666
+ );
667
+ }
668
+ }
669
+
670
+ function validateWorkflowMetadataDefinition(
671
+ value: unknown,
672
+ path: string,
673
+ issues: ModelValidationIssue[],
674
+ ): void {
675
+ const record = asPlainObject(value, path, issues);
676
+ if (!record) {
677
+ return;
678
+ }
679
+ expectString(record.metadataId, `${path}.metadataId`, issues);
680
+ expectString(record.kind, `${path}.kind`, issues);
681
+ expectString(record.label, `${path}.label`, issues);
682
+ if (record.color !== undefined) {
683
+ expectString(record.color, `${path}.color`, issues);
684
+ }
685
+ if (record.icon !== undefined) {
686
+ expectString(record.icon, `${path}.icon`, issues);
687
+ }
688
+ if (
689
+ record.persistence !== "runtime-only" &&
690
+ record.persistence !== "session" &&
691
+ record.persistence !== "document-metadata"
692
+ ) {
693
+ issues.push({
694
+ path: `${path}.persistence`,
695
+ message: "persistence must be runtime-only, session, or document-metadata.",
696
+ });
697
+ }
698
+ }
699
+
700
+ function validateWorkflowMetadataEntry(
701
+ value: unknown,
702
+ path: string,
703
+ issues: ModelValidationIssue[],
704
+ ): void {
705
+ const record = asPlainObject(value, path, issues);
706
+ if (!record) {
707
+ return;
708
+ }
709
+ expectString(record.entryId, `${path}.entryId`, issues);
710
+ expectString(record.metadataId, `${path}.metadataId`, issues);
711
+ if (!asPlainObject(record.anchor, `${path}.anchor`, issues)) {
712
+ return;
713
+ }
714
+ if (record.storyTarget !== undefined) {
715
+ asPlainObject(record.storyTarget, `${path}.storyTarget`, issues);
716
+ }
717
+ if (record.value !== undefined) {
718
+ asPlainObject(record.value, `${path}.value`, issues);
719
+ }
720
+ if (record.scopeId !== undefined) {
721
+ expectString(record.scopeId, `${path}.scopeId`, issues);
722
+ }
723
+ if (record.workItemId !== undefined) {
724
+ expectString(record.workItemId, `${path}.workItemId`, issues);
725
+ }
726
+ }
727
+
617
728
  function validateProtectionRange(
618
729
  value: unknown,
619
730
  path: string,
@@ -1,4 +1,5 @@
1
1
  import type {
2
+ BlockNode,
2
3
  DocRange,
3
4
  OpaqueFragmentRecord,
4
5
  PreservationStore,
@@ -21,6 +22,11 @@ export interface OpaqueFragmentDescriptor {
21
22
  detail: string;
22
23
  }
23
24
 
25
+ const BLOCKED_IMPORT_FEATURE_KEYS = new Set<OpaqueFragmentDescriptor["featureKey"]>([
26
+ "alt-chunk",
27
+ "custom-xml",
28
+ ]);
29
+
24
30
  export function createPreservationStore(
25
31
  seed?: Partial<PreservationStore>,
26
32
  ): PreservationStore {
@@ -122,16 +128,18 @@ export function describeOpaqueFragment(
122
128
  if (/\b(?:w:)?altChunk\b/u.test(xml)) {
123
129
  return {
124
130
  featureKey: "alt-chunk",
125
- label: "Alternate content import",
131
+ label: "AltChunk import",
126
132
  detail,
127
133
  };
128
134
  }
129
135
 
130
136
  if (/\b(?:w:)?object\b|\b(?:o:)?OLEObject\b/u.test(xml)) {
137
+ const isOleObject = /\b(?:o:)?OLEObject\b/u.test(xml);
138
+ const label = isOleObject ? "OLE embedded object" : "Embedded object";
131
139
  return {
132
140
  featureKey: "embedded-objects",
133
- label: "Embedded object",
134
- detail,
141
+ label,
142
+ detail: `${detail} Preserve-only embedded object retained for export and Word reopen.`,
135
143
  };
136
144
  }
137
145
 
@@ -150,6 +158,54 @@ export function describeOpaqueFragment(
150
158
  };
151
159
  }
152
160
 
161
+ export function describeStructuredWrapperBlock(
162
+ block: BlockNode,
163
+ ): OpaqueFragmentDescriptor | null {
164
+ if (block.type === "sdt") {
165
+ if (isTocContentControl(block)) {
166
+ return {
167
+ featureKey: "content-controls",
168
+ label: "TOC content control",
169
+ detail: "TOC content control remains a wrapper-heavy template structure and read-only.",
170
+ };
171
+ }
172
+ if (block.properties.sdtType === "docPartObj") {
173
+ return {
174
+ featureKey: "content-controls",
175
+ label: "Template content control",
176
+ detail: "Template content control remains a wrapper-heavy document part and read-only.",
177
+ };
178
+ }
179
+ }
180
+
181
+ if (block.type === "custom_xml") {
182
+ const descriptor = [block.element, block.uri].filter(Boolean).join(" ");
183
+ return {
184
+ featureKey: "custom-xml",
185
+ label: "Custom XML wrapper",
186
+ detail: descriptor.length > 0
187
+ ? `Custom XML wrapper ${descriptor} remains package-backed and read-only.`
188
+ : "Custom XML wrapper remains package-backed and read-only.",
189
+ };
190
+ }
191
+
192
+ if (block.type === "alt_chunk") {
193
+ return {
194
+ featureKey: "alt-chunk",
195
+ label: "AltChunk import",
196
+ detail: `AltChunk import remains package-backed and blocked through relationship ${block.relationshipId}.`,
197
+ };
198
+ }
199
+
200
+ return null;
201
+ }
202
+
203
+ export function isBlockedImportFeatureKey(
204
+ featureKey: OpaqueFragmentDescriptor["featureKey"],
205
+ ): boolean {
206
+ return BLOCKED_IMPORT_FEATURE_KEYS.has(featureKey);
207
+ }
208
+
153
209
  function createDetail(fragment: OpaqueFragmentRecord): string {
154
210
  const detail = [
155
211
  `Preserved whole-unit from ${fragment.lastKnownRange.from}-${fragment.lastKnownRange.to}.`,
@@ -164,6 +220,20 @@ function createDetail(fragment: OpaqueFragmentRecord): string {
164
220
  : "Preserved whole-unit to keep unsupported OOXML intact.";
165
221
  }
166
222
 
223
+ function isTocContentControl(block: Extract<BlockNode, { type: "sdt" }>): boolean {
224
+ const searchText = [
225
+ block.properties.alias,
226
+ block.properties.tag,
227
+ block.properties.sdtType,
228
+ block.properties.propertiesXml,
229
+ ]
230
+ .filter(Boolean)
231
+ .join(" ")
232
+ .toLowerCase();
233
+
234
+ return searchText.includes("table of contents") || /\btoc\b/u.test(searchText);
235
+ }
236
+
167
237
  function rangesIntersect(left: DocRange, right: DocRange): boolean {
168
238
  return left.from < right.to && right.from < left.to;
169
239
  }
@@ -45,6 +45,8 @@ export interface CommentThreadMetadata {
45
45
  source?: "runtime" | "import";
46
46
  rootOoxmlCommentId?: string;
47
47
  rootParaId?: string;
48
+ detachedReason?: "incomplete-markers" | "multi-paragraph" | "opaque-region" | "revision-overlap";
49
+ actionabilityNote?: string;
48
50
  }
49
51
 
50
52
  export interface CommentStore {
@@ -261,6 +263,7 @@ function toSidebarThread(
261
263
  ): CommentSidebarThread {
262
264
  const firstEntry = thread.entries[0];
263
265
  const anchorSummary = summarizeCommentAnchor(thread.anchor);
266
+ const detachedReason = thread.metadata?.detachedReason;
264
267
 
265
268
  return {
266
269
  commentId: thread.commentId,
@@ -272,7 +275,9 @@ function toSidebarThread(
272
275
  warningCount: thread.warningIds.length,
273
276
  anchorLabel:
274
277
  anchorSummary.state === "detached"
275
- ? `Detached ${anchorSummary.range.from}-${anchorSummary.range.to}`
278
+ ? detachedReason
279
+ ? `Detached (${humanizeDetachedReason(detachedReason)}) ${anchorSummary.range.from}-${anchorSummary.range.to}`
280
+ : `Detached ${anchorSummary.range.from}-${anchorSummary.range.to}`
276
281
  : `Range ${anchorSummary.range.from}-${anchorSummary.range.to}`,
277
282
  isActive: activeCommentId === thread.commentId,
278
283
  resolvedAt: thread.resolution?.resolvedAt,
@@ -310,6 +315,19 @@ function compareSidebarThreads(
310
315
  return left.createdAt.localeCompare(right.createdAt);
311
316
  }
312
317
 
318
+ function humanizeDetachedReason(reason: NonNullable<CommentThreadMetadata["detachedReason"]>): string {
319
+ switch (reason) {
320
+ case "incomplete-markers":
321
+ return "incomplete markers";
322
+ case "multi-paragraph":
323
+ return "multi-paragraph";
324
+ case "opaque-region":
325
+ return "opaque region";
326
+ case "revision-overlap":
327
+ return "revision overlap";
328
+ }
329
+ }
330
+
313
331
  function priorityForStatus(status: CommentThreadStatus): number {
314
332
  switch (status) {
315
333
  case "open":
@@ -98,6 +98,23 @@ export function applyRevisionAction(
98
98
  );
99
99
  }
100
100
 
101
+ if (canResolveImportedDeletionWithoutContentMutation(revision, options.intent)) {
102
+ const resultingStatus = toResultingStatus(options.intent);
103
+ return {
104
+ document: options.document,
105
+ store: setRevisionStatus(options.store, revision.revisionId, resultingStatus),
106
+ mapping: createEmptyMapping(),
107
+ outcome: {
108
+ kind: "applied",
109
+ revisionId: revision.revisionId,
110
+ intent: options.intent,
111
+ resultingStatus,
112
+ contentChanged: false,
113
+ },
114
+ detachedRevisionIds: [],
115
+ };
116
+ }
117
+
101
118
  const story = parseTextStory(options.document.content);
102
119
  const range = normalizeRange(
103
120
  revision.anchor.range.from,
@@ -270,6 +287,18 @@ function requiresContentDeletion(
270
287
  );
271
288
  }
272
289
 
290
+ function canResolveImportedDeletionWithoutContentMutation(
291
+ revision: RevisionRecord,
292
+ intent: RevisionActionIntent,
293
+ ): boolean {
294
+ return (
295
+ revision.kind === "deletion" &&
296
+ intent === "accept" &&
297
+ revision.metadata.source === "import" &&
298
+ revision.metadata.originalRevisionType !== "paragraph-del"
299
+ );
300
+ }
301
+
273
302
  function requiresParagraphBoundaryDeletion(
274
303
  revision: RevisionRecord,
275
304
  intent: RevisionActionIntent,
@@ -87,6 +87,10 @@ export function createRevisionRecord(
87
87
  (getRevisionActionability(params.kind) === "preserve-only"
88
88
  ? "Imported preserve-only revision."
89
89
  : undefined),
90
+ suggestionId: params.metadata?.suggestionId,
91
+ semanticKind: params.metadata?.semanticKind,
92
+ linkedRevisionIds: params.metadata?.linkedRevisionIds,
93
+ predecessorSuggestionId: params.metadata?.predecessorSuggestionId,
90
94
  importedRevisionForm: params.metadata?.importedRevisionForm,
91
95
  originalRevisionType: params.metadata?.originalRevisionType,
92
96
  ooxmlRevisionId: params.metadata?.ooxmlRevisionId,
@@ -220,7 +224,14 @@ function toSidebarEntry(revision: RevisionRecord): RevisionSidebarEntry {
220
224
  return {
221
225
  revisionId: revision.revisionId,
222
226
  kind: revision.kind,
223
- label: describeRevisionKind(revision.kind),
227
+ label:
228
+ revision.kind === "move"
229
+ ? moveData?.direction === "from"
230
+ ? "Move from"
231
+ : moveData?.direction === "to"
232
+ ? "Move to"
233
+ : describeRevisionKind(revision.kind)
234
+ : describeRevisionKind(revision.kind),
224
235
  status: revision.status,
225
236
  actionability,
226
237
  anchorLabel:
@@ -36,6 +36,17 @@ export interface RevisionMetadataEnvelope {
36
36
  source: "runtime" | "import";
37
37
  storyTarget?: RevisionStoryTargetRecord;
38
38
  preserveOnlyReason?: string;
39
+ suggestionId?: string;
40
+ semanticKind?:
41
+ | "insertion"
42
+ | "deletion"
43
+ | "replacement"
44
+ | "formatting-change"
45
+ | "paragraph-property-change"
46
+ | "structural-change"
47
+ | "object-change";
48
+ linkedRevisionIds?: string[];
49
+ predecessorSuggestionId?: string;
39
50
  importedRevisionForm?:
40
51
  | "run-insertion"
41
52
  | "run-deletion"