@beyondwork/docx-react-component 1.0.21 → 1.0.23

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 (33) hide show
  1. package/README.md +763 -38
  2. package/package.json +25 -36
  3. package/src/api/public-types.ts +66 -1
  4. package/src/core/commands/index.ts +574 -5
  5. package/src/index.ts +5 -0
  6. package/src/io/docx-session.ts +181 -2
  7. package/src/io/export/serialize-main-document.ts +21 -1
  8. package/src/io/normalize/normalize-text.ts +4 -0
  9. package/src/io/ooxml/parse-main-document.ts +88 -7
  10. package/src/model/canonical-document.ts +22 -0
  11. package/src/review/store/revision-store.ts +1 -0
  12. package/src/review/store/revision-types.ts +2 -0
  13. package/src/runtime/document-runtime.ts +503 -51
  14. package/src/runtime/session-capabilities.ts +6 -5
  15. package/src/runtime/surface-projection.ts +2 -0
  16. package/src/runtime/table-schema.ts +2 -0
  17. package/src/runtime/workflow-markup.ts +5 -1
  18. package/src/ui/WordReviewEditor.tsx +661 -132
  19. package/src/ui/editor-runtime-boundary.ts +10 -1
  20. package/src/ui/editor-shell-view.tsx +8 -0
  21. package/src/ui/editor-surface-controller.tsx +5 -0
  22. package/src/ui/headless/selection-toolbar-model.ts +12 -0
  23. package/src/ui-tailwind/chrome/tw-suggestion-card.tsx +139 -0
  24. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +6 -0
  25. package/src/ui-tailwind/editor-surface/pm-decorations.ts +44 -16
  26. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +2 -0
  27. package/src/ui-tailwind/editor-surface/surface-build-keys.ts +4 -0
  28. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +127 -10
  29. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +82 -1
  30. package/src/ui-tailwind/status/tw-status-bar.tsx +4 -1
  31. package/src/ui-tailwind/theme/editor-theme.css +10 -0
  32. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +21 -5
  33. package/src/ui-tailwind/tw-review-workspace.tsx +110 -32
@@ -403,6 +403,10 @@ export function loadDocxEditorSession(
403
403
  normalizedDocument.preservation.opaqueFragments,
404
404
  normalizedRevisions.revisions,
405
405
  );
406
+ const subPartOpaqueState = createSubPartOpaqueImportState(
407
+ normalizedDocument.preservation.opaqueFragments,
408
+ normalizedDocument.diagnostics.warnings,
409
+ );
406
410
  // ---- Parse sub-parts: headers, footers, footnotes, endnotes, theme ----
407
411
  const headerFooterRefs = parseHeaderFooterReferences(sourceDocumentXml);
408
412
  const parsedHeaders: HeaderDocument[] = [];
@@ -439,7 +443,13 @@ export function loadDocxEditorSession(
439
443
  partPath,
440
444
  relationshipId: ref.relationshipId,
441
445
  ...(ref.sectionIndex !== undefined ? { sectionIndex: ref.sectionIndex } : {}),
442
- blocks: parsed.blocks,
446
+ blocks: normalizeSubPartOpaqueBlocks(
447
+ parsed.blocks,
448
+ normalizedDocument.preservation.opaqueFragments,
449
+ normalizedDocument.diagnostics.warnings,
450
+ partPath,
451
+ subPartOpaqueState,
452
+ ),
443
453
  });
444
454
  sourceHeaderPaths.push({ partPath, relationshipId: ref.relationshipId });
445
455
  } else {
@@ -449,7 +459,13 @@ export function loadDocxEditorSession(
449
459
  partPath,
450
460
  relationshipId: ref.relationshipId,
451
461
  ...(ref.sectionIndex !== undefined ? { sectionIndex: ref.sectionIndex } : {}),
452
- blocks: parsed.blocks,
462
+ blocks: normalizeSubPartOpaqueBlocks(
463
+ parsed.blocks,
464
+ normalizedDocument.preservation.opaqueFragments,
465
+ normalizedDocument.diagnostics.warnings,
466
+ partPath,
467
+ subPartOpaqueState,
468
+ ),
453
469
  });
454
470
  sourceFooterPaths.push({ partPath, relationshipId: ref.relationshipId });
455
471
  }
@@ -481,12 +497,28 @@ export function loadDocxEditorSession(
481
497
  footnoteCollection = parseFootnotesXml(
482
498
  decodeUtf8(sourcePackage.parts.get(footnotesPartPath)?.bytes ?? new Uint8Array()),
483
499
  );
500
+ normalizeFootnoteCollectionOpaqueBlocks(
501
+ footnoteCollection,
502
+ "footnote",
503
+ normalizedDocument.preservation.opaqueFragments,
504
+ normalizedDocument.diagnostics.warnings,
505
+ footnotesPartPath,
506
+ subPartOpaqueState,
507
+ );
484
508
  }
485
509
  if (endnotesPartPath) {
486
510
  footnoteCollection = parseEndnotesXml(
487
511
  decodeUtf8(sourcePackage.parts.get(endnotesPartPath)?.bytes ?? new Uint8Array()),
488
512
  footnoteCollection,
489
513
  );
514
+ normalizeFootnoteCollectionOpaqueBlocks(
515
+ footnoteCollection,
516
+ "endnote",
517
+ normalizedDocument.preservation.opaqueFragments,
518
+ normalizedDocument.diagnostics.warnings,
519
+ endnotesPartPath,
520
+ subPartOpaqueState,
521
+ );
490
522
  }
491
523
 
492
524
  const themeRelationship = documentPart.relationships.find(
@@ -753,6 +785,14 @@ function exportDocxEditorSession(
753
785
  const actionableRevisions = currentRevisions.filter(
754
786
  (revision) => getRevisionActionability(revision) === "actionable",
755
787
  );
788
+ const secondaryStoryActionableRevisions = actionableRevisions.filter(
789
+ (revision) => revision.metadata.storyTarget?.kind && revision.metadata.storyTarget.kind !== "main",
790
+ );
791
+ if (secondaryStoryActionableRevisions.length > 0) {
792
+ throw new Error(
793
+ `DOCX export is blocked because ${secondaryStoryActionableRevisions.length} secondary-story tracked changes cannot yet be serialized safely.`,
794
+ );
795
+ }
756
796
  const commentThreads = Object.values(
757
797
  createCommentStoreFromRuntimeComments(currentDocument.review.comments).threads,
758
798
  );
@@ -1096,6 +1136,143 @@ function createImportedCanonicalDocument(input: {
1096
1136
  };
1097
1137
  }
1098
1138
 
1139
+ type SubPartOpaqueImportState = {
1140
+ nextFragmentIndex: number;
1141
+ nextWarningIndex: number;
1142
+ nextDiagnosticIndex: number;
1143
+ cursor: number;
1144
+ };
1145
+
1146
+ function createSubPartOpaqueImportState(
1147
+ opaqueFragments: Record<string, OpaqueFragmentRecord>,
1148
+ warnings: CanonicalDocumentEnvelope["diagnostics"]["warnings"],
1149
+ ): SubPartOpaqueImportState {
1150
+ const maxByPrefix = (values: Iterable<string>, prefix: string): number => {
1151
+ let max = 0;
1152
+ for (const value of values) {
1153
+ const match = new RegExp(`^${prefix}(\\d+)$`).exec(value);
1154
+ if (!match) {
1155
+ continue;
1156
+ }
1157
+ const parsed = Number.parseInt(match[1] ?? "0", 10);
1158
+ if (Number.isFinite(parsed) && parsed > max) {
1159
+ max = parsed;
1160
+ }
1161
+ }
1162
+ return max;
1163
+ };
1164
+
1165
+ const maxCursor = Object.values(opaqueFragments).reduce(
1166
+ (currentMax, fragment) => Math.max(currentMax, fragment.lastKnownRange?.to ?? 0),
1167
+ 0,
1168
+ );
1169
+
1170
+ return {
1171
+ nextFragmentIndex:
1172
+ maxByPrefix(Object.keys(opaqueFragments), "fragment:import-") + 1,
1173
+ nextWarningIndex:
1174
+ maxByPrefix(
1175
+ [
1176
+ ...Object.values(opaqueFragments).map((fragment) => fragment.warningId),
1177
+ ...warnings.map((warning) => warning.warningId),
1178
+ ],
1179
+ "warning:import-",
1180
+ ) + 1,
1181
+ nextDiagnosticIndex:
1182
+ maxByPrefix(warnings.map((warning) => warning.diagnosticId), "diagnostic:import-") + 1,
1183
+ cursor: maxCursor,
1184
+ };
1185
+ }
1186
+
1187
+ function normalizeSubPartOpaqueBlocks(
1188
+ blocks: BlockNode[],
1189
+ opaqueFragments: Record<string, OpaqueFragmentRecord>,
1190
+ warnings: CanonicalDocumentEnvelope["diagnostics"]["warnings"],
1191
+ packagePartName: string,
1192
+ state: SubPartOpaqueImportState,
1193
+ ): BlockNode[] {
1194
+ return blocks.map((block) => {
1195
+ if (block.type !== "opaque_block" || typeof block.rawXml !== "string") {
1196
+ return block;
1197
+ }
1198
+ return recordImportedOpaqueBlock(
1199
+ block.rawXml,
1200
+ opaqueFragments,
1201
+ warnings,
1202
+ packagePartName,
1203
+ state,
1204
+ );
1205
+ });
1206
+ }
1207
+
1208
+ function normalizeFootnoteCollectionOpaqueBlocks(
1209
+ collection: FootnoteCollection | undefined,
1210
+ kind: "footnote" | "endnote",
1211
+ opaqueFragments: Record<string, OpaqueFragmentRecord>,
1212
+ warnings: CanonicalDocumentEnvelope["diagnostics"]["warnings"],
1213
+ packagePartName: string,
1214
+ state: SubPartOpaqueImportState,
1215
+ ): void {
1216
+ if (!collection) {
1217
+ return;
1218
+ }
1219
+ const notes = kind === "footnote" ? collection.footnotes : collection.endnotes;
1220
+ for (const definition of Object.values(notes)) {
1221
+ definition.blocks = normalizeSubPartOpaqueBlocks(
1222
+ definition.blocks,
1223
+ opaqueFragments,
1224
+ warnings,
1225
+ packagePartName,
1226
+ state,
1227
+ );
1228
+ }
1229
+ }
1230
+
1231
+ function recordImportedOpaqueBlock(
1232
+ rawXml: string,
1233
+ opaqueFragments: Record<string, OpaqueFragmentRecord>,
1234
+ warnings: CanonicalDocumentEnvelope["diagnostics"]["warnings"],
1235
+ packagePartName: string,
1236
+ state: SubPartOpaqueImportState,
1237
+ ): BlockNode {
1238
+ const fragmentId = `fragment:import-${state.nextFragmentIndex}`;
1239
+ state.nextFragmentIndex += 1;
1240
+ const warningId = `warning:import-${state.nextWarningIndex}`;
1241
+ state.nextWarningIndex += 1;
1242
+ const diagnosticId = `diagnostic:import-${state.nextDiagnosticIndex}`;
1243
+ state.nextDiagnosticIndex += 1;
1244
+
1245
+ const rangeStart = state.cursor;
1246
+ const rangeEnd = state.cursor + 1;
1247
+ state.cursor = rangeEnd;
1248
+
1249
+ opaqueFragments[fragmentId] = {
1250
+ fragmentId,
1251
+ payloadKind: "xml-subtree",
1252
+ payloadReference: rawXml,
1253
+ featureClass: "preserve-only",
1254
+ lastKnownRange: {
1255
+ from: rangeStart,
1256
+ to: rangeEnd,
1257
+ },
1258
+ warningId,
1259
+ packagePartName,
1260
+ };
1261
+ warnings.push({
1262
+ diagnosticId,
1263
+ warningId,
1264
+ source: "import",
1265
+ message: "Unsupported sub-part OOXML was preserved as an opaque placeholder.",
1266
+ });
1267
+
1268
+ return {
1269
+ type: "opaque_block",
1270
+ fragmentId,
1271
+ warningId,
1272
+ rawXml,
1273
+ };
1274
+ }
1275
+
1099
1276
  // Canonical model styleId validation pattern — styleIds that don't match
1100
1277
  // are excluded from the catalog to avoid snapshot validation failures.
1101
1278
  const VALID_STYLE_ID = /^[A-Za-z_][A-Za-z0-9._-]{0,127}$/;
@@ -1785,6 +1962,7 @@ function toRuntimeRevisionRecords(
1785
1962
  warningIds: [...revision.warningIds],
1786
1963
  metadata: {
1787
1964
  source: revision.metadata.source,
1965
+ storyTarget: revision.metadata.storyTarget,
1788
1966
  preserveOnlyReason: revision.metadata.preserveOnlyReason,
1789
1967
  importedRevisionForm: revision.metadata.importedRevisionForm,
1790
1968
  originalRevisionType: revision.metadata.originalRevisionType,
@@ -1808,6 +1986,7 @@ function toReviewRevisionRecords(
1808
1986
  warningIds: [...(revision.warningIds ?? [])],
1809
1987
  metadata: {
1810
1988
  source: revision.metadata?.source ?? "runtime",
1989
+ storyTarget: revision.metadata?.storyTarget,
1811
1990
  preserveOnlyReason: revision.metadata?.preserveOnlyReason,
1812
1991
  importedRevisionForm: revision.metadata?.importedRevisionForm,
1813
1992
  originalRevisionType: revision.metadata?.originalRevisionType,
@@ -218,7 +218,7 @@ function serializeTableNode(
218
218
  : "";
219
219
  const rowsXml = table.rows
220
220
  .map((row) => {
221
- const rowPropertiesXml = row.propertiesXml ?? "";
221
+ const rowPropertiesXml = row.propertiesXml ?? buildTableRowPropertiesXml(row);
222
222
  const cellsXml = row.cells
223
223
  .map((cell) => serializeTableCellNode(cell, state))
224
224
  .join("");
@@ -280,6 +280,26 @@ function buildCellPropertiesXml(cell: TableCellNode): string {
280
280
  return children.length > 0 ? `<w:tcPr>${children.join("")}</w:tcPr>` : "";
281
281
  }
282
282
 
283
+ function buildTableRowPropertiesXml(row: TableNode["rows"][number]): string {
284
+ const children: string[] = [];
285
+ if (row.gridBefore !== undefined) {
286
+ children.push(`<w:gridBefore w:val="${row.gridBefore}"/>`);
287
+ }
288
+ if (row.widthBefore) {
289
+ children.push(`<w:wBefore w:w="${row.widthBefore.value}" w:type="${row.widthBefore.type}"/>`);
290
+ }
291
+ if (row.gridAfter !== undefined) {
292
+ children.push(`<w:gridAfter w:val="${row.gridAfter}"/>`);
293
+ }
294
+ if (row.widthAfter) {
295
+ children.push(`<w:wAfter w:w="${row.widthAfter.value}" w:type="${row.widthAfter.type}"/>`);
296
+ }
297
+ if (children.length === 0) {
298
+ return "";
299
+ }
300
+ return `<w:trPr>${children.join("")}</w:trPr>`;
301
+ }
302
+
283
303
  function serializeSdtNode(
284
304
  block: SdtNode,
285
305
  state: SerializationState,
@@ -211,6 +211,10 @@ function normalizeTableRow(
211
211
  return {
212
212
  type: "table_row",
213
213
  ...(row.propertiesXml ? { propertiesXml: row.propertiesXml } : {}),
214
+ ...(row.gridBefore !== undefined ? { gridBefore: row.gridBefore } : {}),
215
+ ...(row.widthBefore ? { widthBefore: row.widthBefore } : {}),
216
+ ...(row.gridAfter !== undefined ? { gridAfter: row.gridAfter } : {}),
217
+ ...(row.widthAfter ? { widthAfter: row.widthAfter } : {}),
214
218
  cells,
215
219
  };
216
220
  }
@@ -7,6 +7,7 @@ import type {
7
7
  ParagraphIndentation,
8
8
  TabStop,
9
9
  TableLook,
10
+ TableWidth,
10
11
  SectionProperties,
11
12
  PageSize,
12
13
  PageMargins,
@@ -309,6 +310,10 @@ export interface ParsedTableRowNode {
309
310
  type: "table_row";
310
311
  propertiesXml?: string;
311
312
  cells: ParsedTableCellNode[];
313
+ gridBefore?: number;
314
+ widthBefore?: TableWidth;
315
+ gridAfter?: number;
316
+ widthAfter?: TableWidth;
312
317
  rawXml: string;
313
318
  }
314
319
 
@@ -843,6 +848,10 @@ function parseTableRowElement(
843
848
  sourcePartPath: string,
844
849
  ): ParsedTableRowNode {
845
850
  let propertiesXml: string | undefined;
851
+ let gridBefore: number | undefined;
852
+ let widthBefore: TableWidth | undefined;
853
+ let gridAfter: number | undefined;
854
+ let widthAfter: TableWidth | undefined;
846
855
  const cells: ParsedTableCellNode[] = [];
847
856
 
848
857
  for (const child of node.children) {
@@ -851,6 +860,10 @@ function parseTableRowElement(
851
860
  switch (localName(child.name)) {
852
861
  case "trPr":
853
862
  propertiesXml = sourceXml.slice(child.start, child.end);
863
+ gridBefore = readTableRowGridPosition(child, "gridBefore");
864
+ widthBefore = readTableRowWidth(child, "wBefore");
865
+ gridAfter = readTableRowGridPosition(child, "gridAfter");
866
+ widthAfter = readTableRowWidth(child, "wAfter");
854
867
  break;
855
868
  case "tc":
856
869
  cells.push(parseTableCellElement(child, sourceXml, relationshipMap, relationships, mediaParts, sourcePartPath));
@@ -861,6 +874,10 @@ function parseTableRowElement(
861
874
  return {
862
875
  type: "table_row",
863
876
  ...(propertiesXml ? { propertiesXml } : {}),
877
+ ...(gridBefore !== undefined ? { gridBefore } : {}),
878
+ ...(widthBefore ? { widthBefore } : {}),
879
+ ...(gridAfter !== undefined ? { gridAfter } : {}),
880
+ ...(widthAfter ? { widthAfter } : {}),
864
881
  cells,
865
882
  rawXml: sourceXml.slice(node.start, node.end),
866
883
  };
@@ -1185,14 +1202,13 @@ function tableRequiresOpaquePreservation(rawXml: string): boolean {
1185
1202
  // nested tables, floating images, VML preview atoms, and bounded field
1186
1203
  // families already owned by the current field slice. Risky table-local
1187
1204
  // semantics still fail closed to preserve-only.
1188
- if (/<w:(ins|del|rPrChange|pPrChange|tblPrChange|trPrChange|tcPrChange|sectPrChange|cellIns|cellDel|cellMerge|smartTag|gridAfter|gridBefore|tblCellSpacing)\b/.test(rawXml)) {
1205
+ if (/<w:(ins|del|rPrChange|pPrChange|tblPrChange|trPrChange|tcPrChange|sectPrChange|cellIns|cellDel|cellMerge|smartTag|tblCellSpacing)\b/.test(rawXml)) {
1189
1206
  return true;
1190
1207
  }
1191
1208
 
1192
- const fldSimpleInstructions = [...rawXml.matchAll(/\bw:instr="([^"]*)"/g)].map((match) => match[1] ?? "");
1193
- const complexInstructions = [...rawXml.matchAll(/<(?:\w+:)?instrText\b[^>]*>([\s\S]*?)<\/(?:\w+:)?instrText>/gu)]
1194
- .map((match) => decodeXmlEntities(match[1] ?? ""));
1195
- for (const instruction of [...fldSimpleInstructions, ...complexInstructions]) {
1209
+ const simpleInstructions = [...rawXml.matchAll(/\bw:instr="([^"]*)"/g)].map((match) => match[1] ?? "");
1210
+ const complexInstructions = extractComplexFieldInstructions(rawXml);
1211
+ for (const instruction of [...simpleInstructions, ...complexInstructions]) {
1196
1212
  const classification = classifyFieldInstruction(instruction);
1197
1213
  if (!isSafeMainStoryTableFieldFamily(classification.family)) {
1198
1214
  return true;
@@ -1213,6 +1229,69 @@ function isSafeMainStoryTableFieldFamily(family: string): boolean {
1213
1229
  );
1214
1230
  }
1215
1231
 
1232
+ function extractComplexFieldInstructions(rawXml: string): string[] {
1233
+ const tokenPattern =
1234
+ /<(?:\w+:)?fldChar\b[^>]*?(?:\w+:)?fldCharType="(begin|separate|end)"[^>]*?(?:\/>|>[\s\S]*?<\/(?:\w+:)?fldChar>)|<(?:\w+:)?instrText\b[^>]*>([\s\S]*?)<\/(?:\w+:)?instrText>/gu;
1235
+ const instructions: string[] = [];
1236
+ let activeInstruction = "";
1237
+ let capturingInstruction = false;
1238
+
1239
+ for (const match of rawXml.matchAll(tokenPattern)) {
1240
+ const fldCharType = match[1];
1241
+ const instrText = match[2];
1242
+ if (fldCharType === "begin") {
1243
+ activeInstruction = "";
1244
+ capturingInstruction = true;
1245
+ continue;
1246
+ }
1247
+ if (fldCharType === "separate" || fldCharType === "end") {
1248
+ if (capturingInstruction && activeInstruction.trim().length > 0) {
1249
+ instructions.push(activeInstruction);
1250
+ }
1251
+ activeInstruction = "";
1252
+ capturingInstruction = false;
1253
+ continue;
1254
+ }
1255
+ if (capturingInstruction && instrText !== undefined) {
1256
+ activeInstruction += decodeXmlEntities(instrText);
1257
+ }
1258
+ }
1259
+
1260
+ return instructions;
1261
+ }
1262
+
1263
+ function readTableRowGridPosition(
1264
+ node: XmlElementNode,
1265
+ local: "gridBefore" | "gridAfter",
1266
+ ): number | undefined {
1267
+ const positionNode = node.children.find(
1268
+ (child): child is XmlElementNode => child.type === "element" && localName(child.name) === local,
1269
+ );
1270
+ if (!positionNode) return undefined;
1271
+ const raw = positionNode.attributes["w:val"] ?? positionNode.attributes.val;
1272
+ const value = Number.parseInt(raw ?? "", 10);
1273
+ return Number.isFinite(value) && value > 0 ? value : 0;
1274
+ }
1275
+
1276
+ function readTableRowWidth(
1277
+ node: XmlElementNode,
1278
+ local: "wBefore" | "wAfter",
1279
+ ): TableWidth | undefined {
1280
+ const widthNode = node.children.find(
1281
+ (child): child is XmlElementNode => child.type === "element" && localName(child.name) === local,
1282
+ );
1283
+ if (!widthNode) return undefined;
1284
+ const rawValue = widthNode.attributes["w:w"] ?? widthNode.attributes.w;
1285
+ const value = Number.parseInt(rawValue ?? "", 10);
1286
+ if (!Number.isFinite(value)) {
1287
+ return undefined;
1288
+ }
1289
+ const rawType = (widthNode.attributes["w:type"] ?? widthNode.attributes.type ?? "dxa").toLowerCase();
1290
+ const type: TableWidth["type"] =
1291
+ rawType === "auto" ? "auto" : rawType === "pct" ? "pct" : rawType === "nil" ? "nil" : "dxa";
1292
+ return { value, type };
1293
+ }
1294
+
1216
1295
  function readCellGridSpan(node: XmlElementNode): number | undefined {
1217
1296
  const gridSpanNode = node.children.find(
1218
1297
  (child): child is XmlElementNode => child.type === "element" && localName(child.name) === "gridSpan",
@@ -1801,9 +1880,11 @@ function parseRevisionContainer(
1801
1880
  },
1802
1881
  ];
1803
1882
  case "permStart":
1804
- return [parsePermStartNode(node, sourceXml)];
1883
+ result.push(parsePermStartNode(child, sourceXml));
1884
+ break;
1805
1885
  case "permEnd":
1806
- return [parsePermEndNode(node, sourceXml)];
1886
+ result.push(parsePermEndNode(child, sourceXml));
1887
+ break;
1807
1888
  default:
1808
1889
  return [
1809
1890
  {
@@ -417,6 +417,10 @@ export interface TableRowNode {
417
417
  type: "table_row";
418
418
  propertiesXml?: string;
419
419
  cells: TableCellNode[];
420
+ gridBefore?: number;
421
+ widthBefore?: TableWidth;
422
+ gridAfter?: number;
423
+ widthAfter?: TableWidth;
420
424
  height?: number;
421
425
  heightRule?: "auto" | "atLeast" | "exact";
422
426
  isHeader?: boolean;
@@ -943,6 +947,23 @@ export interface RevisionMoveData {
943
947
  direction: "from" | "to";
944
948
  }
945
949
 
950
+ export type RevisionStoryTargetRecord =
951
+ | { kind: "main" }
952
+ | {
953
+ kind: "header";
954
+ relationshipId: string;
955
+ variant: "default" | "first" | "even";
956
+ sectionIndex?: number;
957
+ }
958
+ | {
959
+ kind: "footer";
960
+ relationshipId: string;
961
+ variant: "default" | "first" | "even";
962
+ sectionIndex?: number;
963
+ }
964
+ | { kind: "footnote"; noteId: string }
965
+ | { kind: "endnote"; noteId: string };
966
+
946
967
  export interface RevisionRecord {
947
968
  changeId: string;
948
969
  kind: "insertion" | "deletion" | "formatting" | "move" | "property-change";
@@ -956,6 +977,7 @@ export interface RevisionRecord {
956
977
 
957
978
  export interface RevisionMetadataRecord {
958
979
  source?: "runtime" | "import";
980
+ storyTarget?: RevisionStoryTargetRecord;
959
981
  preserveOnlyReason?: string;
960
982
  importedRevisionForm?:
961
983
  | "run-insertion"
@@ -81,6 +81,7 @@ export function createRevisionRecord(
81
81
  warningIds: [...(params.warningIds ?? [])],
82
82
  metadata: {
83
83
  source: params.metadata?.source ?? "runtime",
84
+ storyTarget: params.metadata?.storyTarget,
84
85
  preserveOnlyReason:
85
86
  params.metadata?.preserveOnlyReason ??
86
87
  (getRevisionActionability(params.kind) === "preserve-only"
@@ -9,6 +9,7 @@ import {
9
9
  type EditorAnchorProjection,
10
10
  type TransactionMapping,
11
11
  } from "../../core/selection/mapping.ts";
12
+ import type { RevisionStoryTargetRecord } from "../../model/canonical-document.ts";
12
13
 
13
14
  export type RevisionAnchor = EditorAnchorProjection;
14
15
 
@@ -33,6 +34,7 @@ export interface MoveData {
33
34
 
34
35
  export interface RevisionMetadataEnvelope {
35
36
  source: "runtime" | "import";
37
+ storyTarget?: RevisionStoryTargetRecord;
36
38
  preserveOnlyReason?: string;
37
39
  importedRevisionForm?:
38
40
  | "run-insertion"