@stll/folio-core 0.33.0 → 0.33.2

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 (98) hide show
  1. package/dist/ai-edits/apply.d.ts +15 -0
  2. package/dist/ai-edits/apply.js +135 -24
  3. package/dist/ai-edits/headless.d.ts +47 -3
  4. package/dist/ai-edits/headless.js +53 -3
  5. package/dist/ai-edits/index.d.ts +1 -1
  6. package/dist/ai-edits/snapshot.d.ts +19 -1
  7. package/dist/ai-edits/snapshot.js +25 -3
  8. package/dist/ai-edits/table-geometry.d.ts +56 -0
  9. package/dist/ai-edits/table-geometry.js +217 -0
  10. package/dist/ai-edits/table-row-column-mutations.d.ts +34 -16
  11. package/dist/ai-edits/table-row-column-mutations.js +12 -4
  12. package/dist/ai-edits/table-template.d.ts +60 -0
  13. package/dist/ai-edits/table-template.js +176 -0
  14. package/dist/ai-edits/types.d.ts +7 -0
  15. package/dist/compare/__fixtures__/body-sequence.d.ts +86 -4
  16. package/dist/compare/__fixtures__/body-sequence.js +169 -12
  17. package/dist/compare/compare.d.ts +15 -8
  18. package/dist/compare/compare.js +138 -34
  19. package/dist/compare/plan.d.ts +31 -1
  20. package/dist/compare/plan.js +277 -28
  21. package/dist/compare/reproducible-package.d.ts +7 -3
  22. package/dist/compare/reproducible-package.js +20 -3
  23. package/dist/compare/types.d.ts +20 -3
  24. package/dist/compare/types.js +13 -1
  25. package/dist/compare/verification.d.ts +49 -2
  26. package/dist/compare/verification.js +115 -1
  27. package/dist/compat/eigenpal.d.ts +5 -5
  28. package/dist/compat/eigenpal.js +3 -3
  29. package/dist/document-operations.d.ts +10 -1
  30. package/dist/document-operations.js +3 -2
  31. package/dist/docx/appVersionNormalization.d.ts +49 -0
  32. package/dist/docx/appVersionNormalization.js +74 -0
  33. package/dist/docx/blockContentParser.js +3 -2
  34. package/dist/docx/drawingUtils.js +4 -3
  35. package/dist/docx/hyperlinkParser.d.ts +9 -1
  36. package/dist/docx/hyperlinkParser.js +19 -13
  37. package/dist/docx/paraIdRangeNormalization.d.ts +40 -0
  38. package/dist/docx/paraIdRangeNormalization.js +64 -0
  39. package/dist/docx/paragraphParser.js +82 -6
  40. package/dist/docx/revisionIdNormalization.d.ts +15 -1
  41. package/dist/docx/revisionIdNormalization.js +22 -4
  42. package/dist/docx/rezip.d.ts +13 -1
  43. package/dist/docx/rezip.js +80 -21
  44. package/dist/docx/runParser.js +9 -8
  45. package/dist/docx/sdtProperties.js +4 -3
  46. package/dist/docx/selectiveSave.js +7 -6
  47. package/dist/docx/serializer/commentSerializer.d.ts +1 -1
  48. package/dist/docx/serializer/commentSerializer.js +43 -19
  49. package/dist/docx/serializer/documentSerializer.d.ts +3 -1
  50. package/dist/docx/serializer/documentSerializer.js +42 -78
  51. package/dist/docx/serializer/fontTableSerializer.js +8 -5
  52. package/dist/docx/serializer/headerFooterSerializer.d.ts +4 -1
  53. package/dist/docx/serializer/headerFooterSerializer.js +34 -29
  54. package/dist/docx/serializer/noteSerializer.js +34 -30
  55. package/dist/docx/serializer/numberingSerializer.js +8 -4
  56. package/dist/docx/serializer/paragraphSerializer.js +45 -21
  57. package/dist/docx/serializer/partNamespaces.d.ts +78 -0
  58. package/dist/docx/serializer/partNamespaces.js +324 -0
  59. package/dist/docx/serializer/runSerializer.js +1 -1
  60. package/dist/docx/serializer/settingsSerializer.js +8 -2
  61. package/dist/docx/serializer/stylesSerializer.js +8 -5
  62. package/dist/docx/serializer/tableSerializer.js +107 -27
  63. package/dist/docx/serializer/themeSerializer.js +10 -2
  64. package/dist/docx/server/build.d.ts +1 -1
  65. package/dist/docx/strictValueEncodings.gen.d.ts +15 -0
  66. package/dist/docx/strictValueEncodings.gen.js +275 -0
  67. package/dist/docx/tableParser.d.ts +0 -6
  68. package/dist/docx/tableParser.js +52 -7
  69. package/dist/docx/transitionalSpelling.d.ts +23 -0
  70. package/dist/docx/transitionalSpelling.js +36 -0
  71. package/dist/docx/universalMeasure.d.ts +20 -0
  72. package/dist/docx/universalMeasure.js +32 -0
  73. package/dist/docx/verbatimCapture.d.ts +20 -0
  74. package/dist/docx/verbatimCapture.js +131 -0
  75. package/dist/docx/vmlImageParser.js +4 -3
  76. package/dist/docx/watermarkParser.js +4 -3
  77. package/dist/docx/xmlParser.d.ts +23 -2
  78. package/dist/docx/xmlParser.js +43 -11
  79. package/dist/index.d.ts +5 -5
  80. package/dist/index.js +3 -3
  81. package/dist/model.d.ts +3 -3
  82. package/dist/model.js +2 -2
  83. package/dist/prosemirror/commands/comments.js +8 -2
  84. package/dist/prosemirror/containerFinalParagraph.d.ts +47 -0
  85. package/dist/prosemirror/containerFinalParagraph.js +92 -0
  86. package/dist/prosemirror/conversion/fromProseDoc.d.ts +8 -1
  87. package/dist/prosemirror/conversion/fromProseDoc.js +15 -4
  88. package/dist/prosemirror/conversion/toProseDoc.js +12 -3
  89. package/dist/prosemirror/extensions/nodes/TableExtension.js +5 -0
  90. package/dist/prosemirror/schema/marks.d.ts +1 -1
  91. package/dist/prosemirror/schema/nodes.d.ts +22 -0
  92. package/dist/server.d.ts +1 -1
  93. package/dist/types/block-id.d.ts +12 -1
  94. package/dist/types/block-id.js +17 -1
  95. package/dist/utils/canonicalJson.d.ts +14 -0
  96. package/dist/utils/canonicalJson.js +19 -0
  97. package/dist/utils/formatToStyle.d.ts +1 -1
  98. package/package.json +2 -2
@@ -1,3 +1,4 @@
1
+ import { FolioTableTemplates } from "./table-template.js";
1
2
  import { FolioAIEditApplyMode, FolioAIEditApplyResult, FolioAIEditOperation, FolioAIEditSnapshot } from "./types.js";
2
3
  import { WordDiffGranularity } from "./word-diff.js";
3
4
  import { EditorState, Transaction } from "prosemirror-state";
@@ -42,6 +43,20 @@ type ApplyFolioAIEditOperationsOptions = {
42
43
  revisionStamp?: FolioRevisionStamp;
43
44
  /** How a replacement's redline is cut. */
44
45
  wordDiff?: FolioWordDiffOptions;
46
+ /**
47
+ * Tables and rows an `insertTable` / `insertTableRow` operation should place
48
+ * verbatim, by operation id.
49
+ *
50
+ * The operations describe their content as cell texts, which is what a
51
+ * caller writing a table from nothing has. A caller copying one it already
52
+ * holds — a comparison placing the target document's table — has the whole
53
+ * thing: `w:tblPr`, the `w:tblGrid` widths, `w:trPr`, `w:tcPr` with its
54
+ * spans and merges, and cell paragraphs with their own properties. Rebuilt
55
+ * from text, none of that survives. Kept out of the operation payload
56
+ * because it is a document node rather than JSON: the serialized contract
57
+ * still describes a table by its cell texts.
58
+ */
59
+ tableTemplates?: FolioTableTemplates;
45
60
  };
46
61
  /**
47
62
  * How an apply batch cuts the redline for a replacement. Word-level by
@@ -1,5 +1,6 @@
1
1
  import { expectParagraphAttrs, expectRunPropertyChangeMarkAttrs } from "../prosemirror/attrs/index.js";
2
2
  import { PPR_CHANGE_SCOPED_ATTR_KEYS } from "../prosemirror/commands/propertyChangeScope.js";
3
+ import { addedBreakCarrierBefore, finalParagraphsOf, paragraphEndsItsContainer } from "../prosemirror/containerFinalParagraph.js";
3
4
  import { marksToTextFormatting } from "../prosemirror/conversion/fromProseDoc.js";
4
5
  import { requestDeterministicParaIds } from "../prosemirror/extensions/features/ParaIdAllocatorExtension.js";
5
6
  import { markStructuralChange } from "../prosemirror/extensions/features/ParagraphChangeTrackerExtension.js";
@@ -11,8 +12,9 @@ import { hasInlineEmphasis, parseInlineEmphasisRuns, stripInlineEmphasisMarkers
11
12
  import { hashFolioAIBlockText, isHiddenTableRow, normalizeFolioAIBlockText } from "./snapshot.js";
12
13
  import { mergeTableRectangle, mergeTrackedVerticalTableCells, splitTableRectangle, splitTrackedVerticalTableCell } from "./table-cell-mutations.js";
13
14
  import { planTableMutations } from "./table-mutation-plan.js";
14
- import { applyTableColumnDeletion, applyTableColumnInsertion, applyTableRowDeletion, applyTableRowInsertion, findTableColumnInsertion, findTableRowInsertion, getTableColumnCoordinateKey, splitCellParagraphTexts } from "./table-row-column-mutations.js";
15
+ import { applyTableColumnDeletion, applyTableColumnInsertion, applyTableRowDeletion, applyTableRowInsertion, findTableColumnInsertion, findTableRowInsertion, getTableColumnCoordinateKey, markTableRowContent, splitCellParagraphTexts } from "./table-row-column-mutations.js";
15
16
  import { findEnclosingTableCell, findEnclosingTableRow, findOutermostTableBoundary, tableRectangleCutsMergedCell } from "./table-targets.js";
17
+ import { tableFromTemplate } from "./table-template.js";
16
18
  import { diffWordSegments } from "./word-diff.js";
17
19
  import { panic } from "better-result";
18
20
  import { TableMap } from "prosemirror-tables";
@@ -364,9 +366,11 @@ const ordinalAmongSameHash = (snapshot, blockId) => {
364
366
  const SIGNATURE_LINE = "_".repeat(28);
365
367
  /**
366
368
  * A plain table from a grid of cell texts, `null` when the schema has no
367
- * tables. In tracked mode every row carries `trIns`, which is how Word says
368
- * "this table is new": there is no whole-table insertion element, only rows
369
- * that were inserted.
369
+ * tables. In tracked mode every row carries `trIns` and every run inside it
370
+ * carries `w:ins`, which is how the format says "this table is new": there is
371
+ * no whole-table insertion element, only rows that were inserted, and a
372
+ * consumer that reads only run-level revisions keeps the text of a rejected
373
+ * insertion when the row marker stands alone.
370
374
  */
371
375
  const buildTableNode = ({ schema, rows, revision }) => {
372
376
  const paragraphType = schema.nodes["paragraph"];
@@ -374,7 +378,16 @@ const buildTableNode = ({ schema, rows, revision }) => {
374
378
  const rowType = schema.nodes["tableRow"];
375
379
  const tableType = schema.nodes["table"];
376
380
  if (!paragraphType || !cellType || !rowType || !tableType || rows.length === 0) return null;
377
- const rowNodes = rows.map((cells) => rowType.create(revision ? { trIns: revision } : null, cells.map((text) => cellType.create(null, splitCellParagraphTexts(text).map((line) => paragraphType.create(null, line.length > 0 ? schema.text(line) : null))))));
381
+ const insertionType = schema.marks["insertion"];
382
+ const marks = revision && insertionType ? [insertionType.create({
383
+ revisionId: revision.revisionId,
384
+ author: revision.author,
385
+ date: revision.date,
386
+ ...revision.initials != null && { initials: revision.initials },
387
+ ...revision.provenance != null && { provenance: revision.provenance },
388
+ ...revision.suggestionId != null && { suggestionId: revision.suggestionId }
389
+ })] : void 0;
390
+ const rowNodes = rows.map((cells) => rowType.create(revision ? { trIns: revision } : null, cells.map((text) => cellType.create(null, splitCellParagraphTexts(text).map((line) => paragraphType.create(null, line.length > 0 ? schema.text(line, marks) : null))))));
378
391
  return tableType.create(null, rowNodes);
379
392
  };
380
393
  /**
@@ -468,6 +481,75 @@ const isBatchableParagraphInsertion = (item, mode) => {
468
481
  const isEmptyInsert = insertTexts.length === 1 && insertTexts[0]?.length === 0;
469
482
  return !(mode === "tracked-changes" && item.operation.pageBreakBefore === true && isEmptyInsert);
470
483
  };
484
+ /**
485
+ * Move an ADDED paragraph break off every paragraph its container ends with.
486
+ *
487
+ * A container's final mark carries no revision in either direction: nothing
488
+ * follows it, so "join this paragraph with the one after it" and its mirror
489
+ * both state an edit that cannot be carried out, and a reader is left with a
490
+ * revision neither accepting nor rejecting everything can clear.
491
+ *
492
+ * Appending at a container's end therefore rotates exactly as removing from it
493
+ * does. The break that was added sits between the paragraph the run was
494
+ * appended after and the first appended one, so THAT paragraph's mark is the
495
+ * inserted one, each appended paragraph but the last keeps an inserted mark of
496
+ * its own, and the paragraph the container now ends with takes the free mark
497
+ * the run was appended after. Only which paragraph is left markless changes.
498
+ *
499
+ * A paragraph's properties live on its mark, so the paragraph that inherits
500
+ * the free one records the other's as `w:pPrChange` — what a rejection reads
501
+ * to put the container's ending back the way it was.
502
+ *
503
+ * This runs once over the finished document rather than at each insertion:
504
+ * which paragraph ends a container is only settled when the batch is, and an
505
+ * insertion that looked final was undone by the next operation writing a table
506
+ * after it.
507
+ */
508
+ const withRotatedAddedFinalBreaks = ({ tr, batchRevisionIds, revisionSeed, author, date, initials }) => {
509
+ const paragraphTypeName = tr.doc.type.schema.nodes["paragraph"]?.name ?? "paragraph";
510
+ const rotations = finalParagraphsOf(tr.doc, paragraphTypeName).filter(({ node }) => {
511
+ const mark = node.attrs["pPrMark"];
512
+ if (typeof mark !== "object" || mark === null || !("kind" in mark)) return false;
513
+ if (mark.kind !== "ins" && mark.kind !== "moveTo") return false;
514
+ const info = "info" in mark ? mark.info : void 0;
515
+ const revisionId = typeof info === "object" && info !== null && "id" in info ? info.id : void 0;
516
+ return typeof revisionId === "number" && batchRevisionIds.has(revisionId);
517
+ });
518
+ let next = tr;
519
+ let nextRevisionId = revisionSeed;
520
+ for (const { position: finalPosition, node: final } of rotations) {
521
+ const carrier = addedBreakCarrierBefore(next.doc.resolve(finalPosition), final.type.name);
522
+ if (!carrier) {
523
+ next = next.setNodeAttribute(finalPosition, "pPrMark", null);
524
+ continue;
525
+ }
526
+ next = next.setNodeAttribute(carrier.position, "pPrMark", final.attrs["pPrMark"]);
527
+ const previousFormatting = paragraphPropertiesSnapshot(carrier.node);
528
+ if (JSON.stringify(previousFormatting) === JSON.stringify(paragraphPropertiesSnapshot(final))) {
529
+ next = next.setNodeAttribute(finalPosition, "pPrMark", null);
530
+ continue;
531
+ }
532
+ const existing = expectParagraphAttrs(final)._propertyChanges;
533
+ next = next.setNodeMarkup(finalPosition, void 0, {
534
+ ...final.attrs,
535
+ pPrMark: null,
536
+ _propertyChanges: [...Array.isArray(existing) ? existing : [], {
537
+ type: "paragraphPropertyChange",
538
+ info: {
539
+ id: nextRevisionId++,
540
+ author,
541
+ date,
542
+ ...initials ? { initials } : {}
543
+ },
544
+ previousFormatting
545
+ }]
546
+ });
547
+ }
548
+ return {
549
+ transaction: next,
550
+ nextRevisionId
551
+ };
552
+ };
471
553
  const buildInsertedParagraphs = ({ item, schema, mode, author, date, initials, commentMark, suggestionId, revisionSeed, isPairedMove }) => {
472
554
  const operation = item.operation;
473
555
  if (operation.type !== "insertAfterBlock" && operation.type !== "insertBeforeBlock") panic("Only paragraph insertions can build inserted paragraphs", { type: operation.type });
@@ -551,7 +633,7 @@ const buildInsertedParagraphs = ({ item, schema, mode, author, date, initials, c
551
633
  nextRevisionId
552
634
  };
553
635
  };
554
- const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode = "tracked-changes", author = "AI", initials, createCommentId, revisionStamp, revisionIdSeed, wordDiff }) => {
636
+ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode = "tracked-changes", author = "AI", initials, createCommentId, revisionStamp, revisionIdSeed, wordDiff, tableTemplates }) => {
555
637
  const applied = [];
556
638
  const skipped = [];
557
639
  const normalizations = [];
@@ -978,11 +1060,13 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
978
1060
  continue;
979
1061
  }
980
1062
  const revision = structuralRevision;
1063
+ const template = tableTemplates?.get(item.operation.id);
981
1064
  const result = applyTableRowInsertion({
982
1065
  tr,
983
1066
  insertion,
984
1067
  cellTexts: item.operation.cellTexts,
985
- revision
1068
+ revision,
1069
+ ...template !== void 0 && { template }
986
1070
  });
987
1071
  if (result.type === "unsupported") {
988
1072
  skipped.push({
@@ -1171,7 +1255,8 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1171
1255
  }
1172
1256
  case "deleteBlock":
1173
1257
  if (mode === "direct") {
1174
- tr = tr.delete(item.blockFrom, item.blockTo);
1258
+ const at = tr.doc.resolve(item.blockFrom);
1259
+ tr = !paragraphEndsItsContainer(at, item.blockNode.type.name) || at.nodeBefore?.type.name === item.blockNode.type.name ? tr.delete(item.blockFrom, item.blockTo) : tr.delete(item.blockFrom + 1, item.blockTo - 1);
1175
1260
  break;
1176
1261
  }
1177
1262
  if (deletionType) {
@@ -1189,7 +1274,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1189
1274
  const inlineRevisionApplied = item.from < item.to || atomRanges.length > 0;
1190
1275
  appliedRevisionIds = inlineRevisionApplied ? [revisionId] : [];
1191
1276
  const markPosition = tr.mapping.map(item.blockFrom);
1192
- if (tr.doc.resolve(markPosition).parent.childCount > 1 && tr.doc.nodeAt(markPosition)?.attrs["pPrMark"] == null) {
1277
+ if (!paragraphEndsItsContainer(tr.doc.resolve(markPosition), item.blockNode.type.name) && tr.doc.nodeAt(markPosition)?.attrs["pPrMark"] == null) {
1193
1278
  const markRevisionId = revisionSeed++;
1194
1279
  tr = tr.setNodeAttribute(markPosition, "pPrMark", {
1195
1280
  kind: isPairedMove(item.operation.moveId) ? "moveFrom" : "del",
@@ -1206,15 +1291,21 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1206
1291
  if (commentMark) tr = tr.addMark(item.from, item.to, commentMark);
1207
1292
  break;
1208
1293
  case "insertTable": {
1209
- const table = buildTableNode({
1294
+ const revision = {
1295
+ revisionId: revisionSeed,
1296
+ author,
1297
+ date,
1298
+ ...trackedRevisionExtras
1299
+ };
1300
+ const template = tableTemplates?.get(item.operation.id);
1301
+ const table = (template === void 0 ? null : tableFromTemplate({
1302
+ schema: view.state.schema,
1303
+ template,
1304
+ ...producesTrackedChanges && { revision }
1305
+ })) ?? buildTableNode({
1210
1306
  schema: view.state.schema,
1211
1307
  rows: item.operation.rows,
1212
- ...producesTrackedChanges && { revision: {
1213
- revisionId: revisionSeed,
1214
- author,
1215
- date,
1216
- ...trackedRevisionExtras
1217
- } }
1308
+ ...producesTrackedChanges && { revision }
1218
1309
  });
1219
1310
  if (!table) {
1220
1311
  skipped.push({
@@ -1279,13 +1370,23 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1279
1370
  });
1280
1371
  continue;
1281
1372
  }
1282
- for (const rowPosition of rowPositions.toReversed()) tr = tr.setNodeAttribute(rowPosition, "trDel", revision);
1373
+ for (const rowPosition of rowPositions.toReversed()) {
1374
+ tr = tr.setNodeAttribute(rowPosition, "trDel", revision);
1375
+ markTableRowContent({
1376
+ tr,
1377
+ rowPosition,
1378
+ kind: "deletion",
1379
+ revision
1380
+ });
1381
+ }
1283
1382
  appliedRevisionIds = [revision.revisionId];
1284
1383
  markStructuralChange(tr);
1285
1384
  break;
1286
1385
  }
1287
1386
  case "setBlockParagraphProperties": {
1288
- const patch = paragraphPropertiesPatch(item.blockNode, item.operation.properties);
1387
+ const blockPosition = tr.mapping.map(item.blockFrom);
1388
+ const liveBlock = tr.doc.nodeAt(blockPosition) ?? item.blockNode;
1389
+ const patch = paragraphPropertiesPatch(liveBlock, item.operation.properties);
1289
1390
  if (patch === null) {
1290
1391
  skipped.push({
1291
1392
  id: item.operation.id,
@@ -1294,8 +1395,8 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1294
1395
  continue;
1295
1396
  }
1296
1397
  if (mode === "direct") {
1297
- tr = tr.setNodeMarkup(item.blockFrom, void 0, {
1298
- ...item.blockNode.attrs,
1398
+ tr = tr.setNodeMarkup(blockPosition, void 0, {
1399
+ ...liveBlock.attrs,
1299
1400
  ...patch
1300
1401
  });
1301
1402
  break;
@@ -1309,11 +1410,11 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1309
1410
  date,
1310
1411
  ...initials ? { initials } : {}
1311
1412
  },
1312
- previousFormatting: paragraphPropertiesSnapshot(item.blockNode)
1413
+ previousFormatting: paragraphPropertiesSnapshot(liveBlock)
1313
1414
  };
1314
- const existing = expectParagraphAttrs(item.blockNode)._propertyChanges;
1315
- tr = tr.setNodeMarkup(item.blockFrom, void 0, {
1316
- ...item.blockNode.attrs,
1415
+ const existing = expectParagraphAttrs(liveBlock)._propertyChanges;
1416
+ tr = tr.setNodeMarkup(blockPosition, void 0, {
1417
+ ...liveBlock.attrs,
1317
1418
  ...patch,
1318
1419
  _propertyChanges: [...Array.isArray(existing) ? existing : [], change]
1319
1420
  });
@@ -1408,6 +1509,16 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1408
1509
  if (tr.docChanged) {
1409
1510
  const batchRevisionIds = new Set(applied.flatMap(({ revisionIds }) => revisionIds ?? []));
1410
1511
  tr = withoutRevisionsOnZeroWidthAnchors(tr, batchRevisionIds);
1512
+ const rotated = withRotatedAddedFinalBreaks({
1513
+ tr,
1514
+ batchRevisionIds,
1515
+ revisionSeed,
1516
+ author,
1517
+ date,
1518
+ initials
1519
+ });
1520
+ tr = rotated.transaction;
1521
+ revisionSeed = rotated.nextRevisionId;
1411
1522
  if (revisionStamp) requestDeterministicParaIds(tr, `${revisionStamp.date}:${String(revisionStamp.idSeed)}`);
1412
1523
  view.dispatch(tr);
1413
1524
  }
@@ -1,8 +1,12 @@
1
+ import { document_d_exports } from "../types/document.js";
2
+ import { FolioTableTemplates } from "./table-template.js";
1
3
  import { FolioAIBlock, FolioAIEditApplyMode, FolioAIEditApplyResult, FolioAIEditOperation, FolioAIEditSnapshot } from "./types.js";
2
4
  import { FolioRevisionStamp, FolioWordDiffOptions } from "./apply.js";
3
- import { document_d_exports } from "../types/document.js";
4
5
  import { FolioDocumentOperationBatch, FolioDocumentOperationResult, FolioDocumentOperationUndoHandle, FolioDocumentOperationUndoResult } from "../document-operations.js";
5
6
  import { FolioReviewChange, FolioReviewChangeKind } from "./read.js";
7
+ import { FolioStoryTable } from "./snapshot.js";
8
+ import { TableGeometryPairing } from "./table-geometry.js";
9
+ import { Node } from "prosemirror-model";
6
10
  //#region src/ai-edits/headless.d.ts
7
11
  /** Options for {@link FolioDocxReviewer.fromBuffer}. */
8
12
  type FolioDocxReviewerOptions = {
@@ -76,6 +80,15 @@ type FolioReadReviewedStoryOptions = {
76
80
  story?: FolioEditableDocumentStoryHandle;
77
81
  view?: FolioReviewedView;
78
82
  };
83
+ /** What {@link FolioDocxReviewer.matchStoryTableGeometry} needs to move a table's properties. */
84
+ type FolioMatchStoryTableGeometryOptions = {
85
+ story?: FolioEditableDocumentStoryHandle;
86
+ /** The other document's tables, by the index its snapshot numbers them with. */
87
+ targetTables: ReadonlyMap<number, Node>;
88
+ /** Base cells and the target cells they were aligned with. */
89
+ pairings: readonly TableGeometryPairing[];
90
+ revisionStamp: FolioRevisionStamp;
91
+ };
79
92
  type FolioResolveReviewedStoryOptions = {
80
93
  story?: FolioEditableDocumentStoryHandle;
81
94
  view: FolioResolvedReviewedView;
@@ -100,6 +113,14 @@ declare class FolioDocumentStoryNotFoundError extends FolioDocumentStoryNotFound
100
113
  type FolioApplyDocumentOperationsToStoryOptions = FolioApplyDocumentOperationsOptions & {
101
114
  story: FolioEditableDocumentStoryHandle;
102
115
  batch: FolioDocumentOperationBatch;
116
+ /**
117
+ * Tables and rows an `insertTable` / `insertTableRow` in the batch places
118
+ * verbatim, by operation id. A caller copying a table it already holds —
119
+ * `compareDocx` placing the target document's table — hands it over whole
120
+ * instead of letting the operation rebuild it from its cell texts and lose
121
+ * every table, row and cell property on the way.
122
+ */
123
+ tableTemplates?: FolioTableTemplates;
103
124
  };
104
125
  declare const isFolioReviewedView: (value: unknown) => value is FolioReviewedView;
105
126
  declare const isFolioResolvedReviewedView: (value: unknown) => value is FolioResolvedReviewedView;
@@ -201,6 +222,29 @@ declare class FolioDocxReviewer {
201
222
  getDocumentProperties(): Readonly<NonNullable<document_d_exports.Document["package"]["properties"]>> | null;
202
223
  /** Snapshot one editable story into stable, operation-ready blocks. */
203
224
  snapshotStory(story: FolioEditableDocumentStoryHandle): FolioAIEditSnapshot | null;
225
+ /**
226
+ * One story's tables, in the document order {@link snapshotStory} numbers
227
+ * them with, read through a reviewed view.
228
+ *
229
+ * A block snapshot says which table a paragraph sits in and nothing about
230
+ * the table itself. A caller that has to reproduce one — a comparison
231
+ * copying the target's table into the base, or checking that accepting its
232
+ * own redline reproduced it — needs the node: its `w:tblPr`, its `w:tblGrid`
233
+ * widths, and every row's and cell's properties.
234
+ */
235
+ storyTables({ story, view }?: FolioReadReviewedStoryOptions): readonly FolioStoryTable[];
236
+ /**
237
+ * Move the paired tables' own properties onto this story's tables, as
238
+ * tracked property changes.
239
+ *
240
+ * `w:tblPr`, `w:trPr` and `w:tcPr` belong to no block, so no block operation
241
+ * can carry them: a table that stayed in place while its widths, shading,
242
+ * borders or header row changed reads as unedited. Each difference is
243
+ * written as the target's property set plus a `w:tblPrChange` /
244
+ * `w:trPrChange` / `w:tcPrChange` holding the previous one, which is what a
245
+ * reject restores. A set that already agrees produces no revision.
246
+ */
247
+ matchStoryTableGeometry({ story, targetTables, pairings, revisionStamp }: FolioMatchStoryTableGeometryOptions): number;
204
248
  /** Read one story through an immutable reviewed-view projection. */
205
249
  readReviewedStory(options?: FolioReadReviewedStoryOptions): FolioReviewedStory | null;
206
250
  /** Resolve one editable story to its original or final state. */
@@ -219,7 +263,7 @@ declare class FolioDocxReviewer {
219
263
  */
220
264
  applyDocumentOperations(batch: FolioDocumentOperationBatch, options?: FolioApplyDocumentOperationsOptions): FolioDocumentOperationResult;
221
265
  /** Apply a versioned operation batch to one editable document story. */
222
- applyDocumentOperationsToStory({ story, batch, snapshot, revisionStamp, wordDiff }: FolioApplyDocumentOperationsToStoryOptions): FolioDocumentOperationResult;
266
+ applyDocumentOperationsToStory({ story, batch, snapshot, revisionStamp, wordDiff, tableTemplates }: FolioApplyDocumentOperationsToStoryOptions): FolioDocumentOperationResult;
223
267
  private applyDocumentOperationsInternal;
224
268
  /** Undo the latest unchanged document-operation batch and its created comments. */
225
269
  undoDocumentOperations(undoHandle: FolioDocumentOperationUndoHandle): FolioDocumentOperationUndoResult;
@@ -382,4 +426,4 @@ type ApplyFolioAIEditsToBufferResult = FolioAIEditApplyResult & {
382
426
  */
383
427
  declare const applyFolioAIEditsToBuffer: (buffer: ArrayBuffer, operations: FolioAIEditOperation[], options?: ApplyFolioAIEditsToBufferOptions) => Promise<ApplyFolioAIEditsToBufferResult>;
384
428
  //#endregion
385
- export { ApplyFolioAIEditsToBufferOptions, ApplyFolioAIEditsToBufferResult, FOLIO_RESOLVED_REVIEWED_VIEWS, FOLIO_REVIEWED_VIEWS, FolioApplyDocumentOperationsOptions, FolioApplyDocumentOperationsToStoryOptions, FolioApplyOperationsOptions, FolioDocumentStory, FolioDocumentStoryHandle, FolioDocumentStoryNotFoundError, FolioDocxReviewer, FolioDocxReviewerOptions, FolioEditableDocumentStoryHandle, FolioGetContentAsTextOptions, FolioNumberingLevel, FolioReadReviewedStoryOptions, FolioResolveReviewedStoryOptions, FolioResolvedReviewedView, type FolioReviewChange, FolioReviewChangeFilter, type FolioReviewChangeKind, FolioReviewComment, FolioReviewCommentFilter, FolioReviewCommentReply, FolioReviewReplyInput, FolioReviewedStory, FolioReviewedView, type FolioRevisionStamp, UnsupportedFolioReviewedViewError, applyFolioAIEditsToBuffer, isFolioResolvedReviewedView, isFolioReviewedView };
429
+ export { ApplyFolioAIEditsToBufferOptions, ApplyFolioAIEditsToBufferResult, FOLIO_RESOLVED_REVIEWED_VIEWS, FOLIO_REVIEWED_VIEWS, FolioApplyDocumentOperationsOptions, FolioApplyDocumentOperationsToStoryOptions, FolioApplyOperationsOptions, FolioDocumentStory, FolioDocumentStoryHandle, FolioDocumentStoryNotFoundError, FolioDocxReviewer, FolioDocxReviewerOptions, FolioEditableDocumentStoryHandle, FolioGetContentAsTextOptions, FolioMatchStoryTableGeometryOptions, FolioNumberingLevel, FolioReadReviewedStoryOptions, FolioResolveReviewedStoryOptions, FolioResolvedReviewedView, type FolioReviewChange, FolioReviewChangeFilter, type FolioReviewChangeKind, FolioReviewComment, FolioReviewCommentFilter, FolioReviewCommentReply, FolioReviewReplyInput, FolioReviewedStory, FolioReviewedView, type FolioRevisionStamp, UnsupportedFolioReviewedViewError, applyFolioAIEditsToBuffer, isFolioResolvedReviewedView, isFolioReviewedView };
@@ -14,7 +14,8 @@ import { schema, singletonManager } from "../prosemirror/schema/index.js";
14
14
  import { deterministicHexId } from "../utils/hexId.js";
15
15
  import { buildAnnotatedBlockText } from "./clean-text.js";
16
16
  import { getCommentAnchorsFromDoc, getTrackedChangesFromDoc } from "./read.js";
17
- import { createFolioAIEditSnapshot, isFolioAIContentBlock, normalizeFolioAIBlockText } from "./snapshot.js";
17
+ import { createFolioAIEditSnapshot, folioStoryTables, isFolioAIContentBlock, normalizeFolioAIBlockText } from "./snapshot.js";
18
+ import { matchTableGeometry } from "./table-geometry.js";
18
19
  import { TaggedError } from "better-result";
19
20
  import { Fragment } from "prosemirror-model";
20
21
  import { EditorState } from "prosemirror-state";
@@ -314,6 +315,53 @@ var FolioDocxReviewer = class FolioDocxReviewer {
314
315
  const state = this.getEditableStoryState(story);
315
316
  return state ? createFolioAIEditSnapshot(state.doc) : null;
316
317
  }
318
+ /**
319
+ * One story's tables, in the document order {@link snapshotStory} numbers
320
+ * them with, read through a reviewed view.
321
+ *
322
+ * A block snapshot says which table a paragraph sits in and nothing about
323
+ * the table itself. A caller that has to reproduce one — a comparison
324
+ * copying the target's table into the base, or checking that accepting its
325
+ * own redline reproduced it — needs the node: its `w:tblPr`, its `w:tblGrid`
326
+ * widths, and every row's and cell's properties.
327
+ */
328
+ storyTables({ story = MAIN_STORY, view = "final" } = {}) {
329
+ if (!isFolioReviewedView(view)) throw new UnsupportedFolioReviewedViewError({
330
+ message: "Unsupported reviewed document view.",
331
+ receivedView: view
332
+ });
333
+ const sourceState = this.getEditableStoryState(story);
334
+ return sourceState ? folioStoryTables(resolveReviewedState(sourceState, view).doc) : [];
335
+ }
336
+ /**
337
+ * Move the paired tables' own properties onto this story's tables, as
338
+ * tracked property changes.
339
+ *
340
+ * `w:tblPr`, `w:trPr` and `w:tcPr` belong to no block, so no block operation
341
+ * can carry them: a table that stayed in place while its widths, shading,
342
+ * borders or header row changed reads as unedited. Each difference is
343
+ * written as the target's property set plus a `w:tblPrChange` /
344
+ * `w:trPrChange` / `w:tcPrChange` holding the previous one, which is what a
345
+ * reject restores. A set that already agrees produces no revision.
346
+ */
347
+ matchStoryTableGeometry({ story = MAIN_STORY, targetTables, pairings, revisionStamp }) {
348
+ const state = this.getEditableStoryState(story);
349
+ if (!state || pairings.length === 0) return revisionStamp.idSeed;
350
+ const transaction = state.tr;
351
+ const { nextRevisionId } = matchTableGeometry({
352
+ tr: transaction,
353
+ baseTables: folioStoryTables(state.doc),
354
+ targetTables,
355
+ pairings,
356
+ revision: {
357
+ author: this.author,
358
+ date: revisionStamp.date,
359
+ idSeed: revisionStamp.idSeed
360
+ }
361
+ });
362
+ if (transaction.docChanged) this.setEditableStoryState(story, state.apply(transaction));
363
+ return nextRevisionId;
364
+ }
317
365
  /** Read one story through an immutable reviewed-view projection. */
318
366
  readReviewedStory(options = {}) {
319
367
  const story = options.story ?? MAIN_STORY;
@@ -388,17 +436,18 @@ var FolioDocxReviewer = class FolioDocxReviewer {
388
436
  });
389
437
  }
390
438
  /** Apply a versioned operation batch to one editable document story. */
391
- applyDocumentOperationsToStory({ story, batch, snapshot, revisionStamp, wordDiff }) {
439
+ applyDocumentOperationsToStory({ story, batch, snapshot, revisionStamp, wordDiff, tableTemplates }) {
392
440
  return this.applyDocumentOperationsInternal({
393
441
  story,
394
442
  batch,
395
443
  ...snapshot !== void 0 && { snapshot },
396
444
  ...revisionStamp !== void 0 && { revisionStamp },
397
445
  ...wordDiff !== void 0 && { wordDiff },
446
+ ...tableTemplates !== void 0 && { tableTemplates },
398
447
  createUndoEntry: true
399
448
  });
400
449
  }
401
- applyDocumentOperationsInternal({ story, batch, snapshot, revisionStamp, wordDiff, createUndoEntry }) {
450
+ applyDocumentOperationsInternal({ story, batch, snapshot, revisionStamp, wordDiff, tableTemplates, createUndoEntry }) {
402
451
  const beforeState = this.requireEditableStoryState(story);
403
452
  const createdCommentsLengthBefore = this.createdComments.length;
404
453
  const view = {
@@ -415,6 +464,7 @@ var FolioDocxReviewer = class FolioDocxReviewer {
415
464
  author: this.author,
416
465
  ...revisionStamp !== void 0 && { revisionStamp },
417
466
  ...wordDiff !== void 0 && { wordDiff },
467
+ ...tableTemplates !== void 0 && { tableTemplates },
418
468
  createCommentId: (text) => {
419
469
  const comment = createReviewerComment(this.nextCommentId(), text, this.author);
420
470
  this.createdComments.push(comment);
@@ -5,8 +5,8 @@ import { DocPositionRange, clampRangeToDocSize, resolveFolioAIBlockRange, resolv
5
5
  import { buildAnnotatedBlockText } from "./clean-text.js";
6
6
  import { ApplyFolioDocumentOperationsOptions, FOLIO_DOCUMENT_OPERATION_BATCH_MODES, FOLIO_DOCUMENT_OPERATION_CONTRACT_VERSION, FOLIO_DOCUMENT_OPERATION_MODES, FOLIO_DOCUMENT_OPERATION_MODES_BY_TYPE, FOLIO_DOCUMENT_OPERATION_PRECONDITIONS, FOLIO_DOCUMENT_OPERATION_STORIES, FOLIO_DOCUMENT_OPERATION_TYPES, FolioDocumentOperation, FolioDocumentOperationAffectedTarget, FolioDocumentOperationBatch, FolioDocumentOperationCapabilities, FolioDocumentOperationIssue, FolioDocumentOperationMode, FolioDocumentOperationPrecondition, FolioDocumentOperationReceipt, FolioDocumentOperationRecovery, FolioDocumentOperationResult, FolioDocumentOperationResultBase, FolioDocumentOperationStatus, FolioDocumentOperationStory, FolioDocumentOperationType, FolioDocumentOperationUndoFailureReason, FolioDocumentOperationUndoHandle, FolioDocumentOperationUndoResult, InvalidFolioDocumentOperationBatchError, UnsupportedFolioDocumentOperationVersionError, applyFolioDocumentOperations, assertSupportedFolioDocumentOperationVersion, getFolioDocumentOperationCapabilities, getFolioDocumentOperationIssues, getFolioDocumentOperationReceipts, isFolioDocumentOperationModeSupported, isSupportedFolioDocumentOperationVersion, parseFolioDocumentOperationBatch } from "../document-operations.js";
7
7
  import { FolioCommentAnchor, FolioReviewChange, FolioReviewChangeKind, getCommentAnchorsFromDoc, getTrackedChangesFromDoc } from "./read.js";
8
- import { FOLIO_RESOLVED_REVIEWED_VIEWS, FOLIO_REVIEWED_VIEWS, FolioApplyDocumentOperationsToStoryOptions, FolioDocumentStory, FolioDocumentStoryHandle, FolioDocumentStoryNotFoundError, FolioEditableDocumentStoryHandle, FolioReadReviewedStoryOptions, FolioResolveReviewedStoryOptions, FolioResolvedReviewedView, FolioReviewedStory, FolioReviewedView, UnsupportedFolioReviewedViewError, isFolioResolvedReviewedView, isFolioReviewedView } from "./headless.js";
9
8
  import { createFolioAIEditSnapshot, createFolioAITextRangeHandle, hashFolioAIBlockText, isFolioAIContentBlock, normalizeFolioAIBlockText } from "./snapshot.js";
9
+ import { FOLIO_RESOLVED_REVIEWED_VIEWS, FOLIO_REVIEWED_VIEWS, FolioApplyDocumentOperationsToStoryOptions, FolioDocumentStory, FolioDocumentStoryHandle, FolioDocumentStoryNotFoundError, FolioEditableDocumentStoryHandle, FolioReadReviewedStoryOptions, FolioResolveReviewedStoryOptions, FolioResolvedReviewedView, FolioReviewedStory, FolioReviewedView, UnsupportedFolioReviewedViewError, isFolioResolvedReviewedView, isFolioReviewedView } from "./headless.js";
10
10
  import { getFolioDocumentOutline, readFolioDocumentSection } from "./scoped-reading.js";
11
11
  import { getFolioParaIdFromBlockId } from "../types/block-id.js";
12
12
  export { type ApplyFolioDocumentOperationsOptions, type DocPositionRange, FOLIO_DOCUMENT_OPERATION_BATCH_MODES, FOLIO_DOCUMENT_OPERATION_CONTRACT_VERSION, FOLIO_DOCUMENT_OPERATION_MODES, FOLIO_DOCUMENT_OPERATION_MODES_BY_TYPE, FOLIO_DOCUMENT_OPERATION_PRECONDITIONS, FOLIO_DOCUMENT_OPERATION_STORIES, FOLIO_DOCUMENT_OPERATION_TYPES, FOLIO_RESOLVED_REVIEWED_VIEWS, FOLIO_REVIEWED_VIEWS, type FolioAIBlock, type FolioAIBlockAnchor, type FolioAIBlockKind, type FolioAIBlockPreviewRun, type FolioAIBlockTableLocation, type FolioAIComment, type FolioAIEditAppliedOperation, type FolioAIEditApplyMode, type FolioAIEditApplyOutcome, type FolioAIEditApplyResult, type FolioAIEditNormalization, type FolioAIEditNormalizationCode, type FolioAIEditOperation, type FolioAIEditPrecondition, type FolioAIEditReviewMeta, type FolioAIEditSeverity, type FolioAIEditSkipReason, type FolioAIEditSkippedOperation, type FolioAIEditSnapshot, type FolioAIEditView, type FolioAIInlineFormatting, type FolioAISignatureParty, type FolioAITextRangeHandle, type FolioApplyDocumentOperationsToStoryOptions, type FolioCommentAnchor, type FolioDocumentNavigationTarget, type FolioDocumentOperation, type FolioDocumentOperationAffectedTarget, type FolioDocumentOperationBatch, type FolioDocumentOperationCapabilities, type FolioDocumentOperationIssue, type FolioDocumentOperationMode, type FolioDocumentOperationPrecondition, type FolioDocumentOperationReceipt, type FolioDocumentOperationRecovery, type FolioDocumentOperationResult, type FolioDocumentOperationResultBase, type FolioDocumentOperationStatus, type FolioDocumentOperationStory, type FolioDocumentOperationType, type FolioDocumentOperationUndoFailureReason, type FolioDocumentOperationUndoHandle, type FolioDocumentOperationUndoResult, type FolioDocumentOutline, type FolioDocumentOutlineEntry, type FolioDocumentSection, type FolioDocumentSectionHandle, type FolioDocumentSectionReadResult, type FolioDocumentStory, type FolioDocumentStoryHandle, FolioDocumentStoryNotFoundError, type FolioEditableDocumentStoryHandle, type FolioReadReviewedStoryOptions, type FolioResolveReviewedStoryOptions, type FolioResolvedReviewedView, type FolioReviewChange, type FolioReviewChangeKind, type FolioReviewedStory, type FolioReviewedView, type FolioRevisionStamp, type FolioWordDiffOptions, InvalidFolioDocumentOperationBatchError, UnsupportedFolioDocumentOperationVersionError, UnsupportedFolioReviewedViewError, WORD_DIFF_GRANULARITIES, type WordDiffGranularity, type WordDiffNormalization, type WordDiffOptions, type WordDiffSegment, applyFolioAIEditOperations, applyFolioDocumentOperations, assertSupportedFolioDocumentOperationVersion, buildAnnotatedBlockText, clampRangeToDocSize, createFolioAIEditSnapshot, createFolioAITextRangeHandle, diffWordSegments, getCommentAnchorsFromDoc, getFolioDocumentOperationCapabilities, getFolioDocumentOperationIssues, getFolioDocumentOperationReceipts, getFolioDocumentOutline, getFolioParaIdFromBlockId, getTrackedChangesFromDoc, hashFolioAIBlockText, isFolioAIContentBlock, isFolioDocumentOperationModeSupported, isFolioResolvedReviewedView, isFolioReviewedView, isSupportedFolioDocumentOperationVersion, normalizeFolioAIBlockText, parseFolioDocumentOperationBatch, readFolioDocumentSection, resolveFolioAIBlockRange, resolveFolioAITextRange, resolvePassageRange };
@@ -50,6 +50,24 @@ declare const createFolioAITextRangeHandle: ({ blockId, text, startOffset, endOf
50
50
  * whole subtree, so a table nested inside a hidden row is hidden with it.
51
51
  */
52
52
  declare const isHiddenTableRow: (node: Node) => boolean;
53
+ /** One table of a story, numbered the way {@link createFolioAIEditSnapshot} numbers it. */
54
+ type FolioStoryTable = {
55
+ /** Document-order index over every table, nested ones included. */
56
+ index: number;
57
+ /** The table node's position in the story document. */
58
+ start: number;
59
+ node: Node;
60
+ };
61
+ /**
62
+ * Every table of one story in document order, nested tables included and
63
+ * hidden rows' subtrees excluded.
64
+ *
65
+ * The single place a story's tables are numbered. `table.tableIndex` on a
66
+ * block, the geometry a comparison copies out of the target and the geometry
67
+ * it checks its own work against all read this list, so no second walk can
68
+ * number the same document differently.
69
+ */
70
+ declare const folioStoryTables: (doc: Node) => FolioStoryTable[];
53
71
  declare const createFolioAIEditSnapshot: (doc: Node) => FolioAIEditSnapshot;
54
72
  //#endregion
55
- export { createFolioAIEditSnapshot, createFolioAITextRangeHandle, hashFolioAIBlockText, isFolioAIContentBlock, isHiddenTableRow, normalizeFolioAIBlockText, trailingBodyBlockId };
73
+ export { FolioStoryTable, createFolioAIEditSnapshot, createFolioAITextRangeHandle, folioStoryTables, hashFolioAIBlockText, isFolioAIContentBlock, isHiddenTableRow, normalizeFolioAIBlockText, trailingBodyBlockId };
@@ -69,6 +69,29 @@ const TABLE_ROLE_ROW = "row";
69
69
  */
70
70
  const isHiddenTableRow = (node) => node.type.name === TABLE_ROW_NODE_NAME && node.attrs["hidden"] === true;
71
71
  /**
72
+ * Every table of one story in document order, nested tables included and
73
+ * hidden rows' subtrees excluded.
74
+ *
75
+ * The single place a story's tables are numbered. `table.tableIndex` on a
76
+ * block, the geometry a comparison copies out of the target and the geometry
77
+ * it checks its own work against all read this list, so no second walk can
78
+ * number the same document differently.
79
+ */
80
+ const folioStoryTables = (doc) => {
81
+ const tables = [];
82
+ doc.descendants((node, pos) => {
83
+ if (node.isTextblock) return false;
84
+ if (isHiddenTableRow(node)) return false;
85
+ if (node.type.spec["tableRole"] === TABLE_ROLE_TABLE) tables.push({
86
+ index: tables.length,
87
+ start: pos,
88
+ node
89
+ });
90
+ return true;
91
+ });
92
+ return tables;
93
+ };
94
+ /**
72
95
  * Locate the block inside its innermost enclosing table, or `undefined` when
73
96
  * it sits outside every table.
74
97
  */
@@ -102,7 +125,7 @@ const createFolioAIEditSnapshot = (doc) => {
102
125
  const draftBlocks = [];
103
126
  const hashCounts = /* @__PURE__ */ new Map();
104
127
  const usedBlockIds = /* @__PURE__ */ new Set();
105
- const tableIndexByStart = /* @__PURE__ */ new Map();
128
+ const tableIndexByStart = new Map(folioStoryTables(doc).map(({ start, index }) => [start, index]));
106
129
  const path = [];
107
130
  let blockIndex = 0;
108
131
  let blankIndex = 0;
@@ -110,7 +133,6 @@ const createFolioAIEditSnapshot = (doc) => {
110
133
  while (pos >= (path.at(-1)?.end ?? Number.POSITIVE_INFINITY)) path.pop();
111
134
  if (!node.isTextblock) {
112
135
  if (isHiddenTableRow(node)) return false;
113
- if (node.type.spec["tableRole"] === TABLE_ROLE_TABLE) tableIndexByStart.set(pos, tableIndexByStart.size);
114
136
  if (!node.isLeaf) path.push({
115
137
  node,
116
138
  start: pos,
@@ -332,4 +354,4 @@ const getColorFromAttrs = (attrs) => {
332
354
  const samePreviewRunStyle = (run, style) => run.bold === style.bold && run.italic === style.italic && run.underline === style.underline && run.strike === style.strike && run.fontFamily === style.fontFamily && run.fontSizePt === style.fontSizePt && run.color === style.color;
333
355
  const isUnstyledPreviewRun = ({ bold, italic, underline, strike, fontFamily, fontSizePt, color }) => bold === void 0 && italic === void 0 && underline === void 0 && strike === void 0 && fontFamily === void 0 && fontSizePt === void 0 && color === void 0;
334
356
  //#endregion
335
- export { createFolioAIEditSnapshot, createFolioAITextRangeHandle, hashFolioAIBlockText, isFolioAIContentBlock, isHiddenTableRow, normalizeFolioAIBlockText, trailingBodyBlockId };
357
+ export { createFolioAIEditSnapshot, createFolioAITextRangeHandle, folioStoryTables, hashFolioAIBlockText, isFolioAIContentBlock, isHiddenTableRow, normalizeFolioAIBlockText, trailingBodyBlockId };
@@ -0,0 +1,56 @@
1
+ import { FolioStoryTable } from "./snapshot.js";
2
+ import { Transaction } from "prosemirror-state";
3
+ import { Node } from "prosemirror-model";
4
+ //#region src/ai-edits/table-geometry.d.ts
5
+ /**
6
+ * One line per table, in the document order tables are numbered in: the
7
+ * table's own properties, then each row's and each cell's. Nested tables get
8
+ * their own lines rather than being folded into their parent's, so a
9
+ * difference names the table it is in.
10
+ */
11
+ declare const projectTableGeometry: (tables: readonly FolioStoryTable[]) => string[];
12
+ /** Where a cell sits: which table, which row of it, which cell of that row. */
13
+ type TableCellCoordinate = {
14
+ tableIndex: number;
15
+ rowIndex: number;
16
+ cellIndex: number;
17
+ };
18
+ /** One base cell and the target cell it was aligned with. */
19
+ type TableGeometryPairing = {
20
+ base: TableCellCoordinate;
21
+ target: TableCellCoordinate;
22
+ };
23
+ type MatchTableGeometryOptions = {
24
+ tr: Transaction;
25
+ /** The base story's tables, with the positions the transaction will write at. */
26
+ baseTables: readonly FolioStoryTable[];
27
+ /** The target story's tables, by the same index the pairings name. */
28
+ targetTables: ReadonlyMap<number, Node>;
29
+ pairings: readonly TableGeometryPairing[];
30
+ revision: {
31
+ author: string;
32
+ date: string;
33
+ idSeed: number;
34
+ };
35
+ };
36
+ type MatchTableGeometryResult = {
37
+ /** First revision id a following batch may allocate. */
38
+ nextRevisionId: number;
39
+ /** Tables, rows and cells whose properties the match moved. */
40
+ matched: number;
41
+ };
42
+ /**
43
+ * Copy a paired table's, row's and cell's properties from the document it was
44
+ * compared against, recording the previous set so a reject restores it.
45
+ *
46
+ * Pairings name cells, because that is what the block alignment resolves; the
47
+ * row and the table each cell sits in are matched with it. A property set that
48
+ * already agrees is left alone, so an unchanged table produces no revision.
49
+ *
50
+ * `colspan` / `rowspan` never move: they shape the table map, and changing one
51
+ * without restructuring the rows around it leaves the map inconsistent with
52
+ * its own grid. That is the rule a reject of a `w:tcPrChange` follows too.
53
+ */
54
+ declare const matchTableGeometry: ({ tr, baseTables, targetTables, pairings, revision }: MatchTableGeometryOptions) => MatchTableGeometryResult;
55
+ //#endregion
56
+ export { MatchTableGeometryResult, TableCellCoordinate, TableGeometryPairing, matchTableGeometry, projectTableGeometry };