@stll/folio-core 0.33.1 → 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 (91) hide show
  1. package/dist/ai-edits/apply.d.ts +15 -0
  2. package/dist/ai-edits/apply.js +134 -25
  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 +61 -2
  16. package/dist/compare/__fixtures__/body-sequence.js +94 -6
  17. package/dist/compare/compare.d.ts +14 -7
  18. package/dist/compare/compare.js +136 -38
  19. package/dist/compare/plan.d.ts +31 -1
  20. package/dist/compare/plan.js +233 -66
  21. package/dist/compare/types.d.ts +9 -7
  22. package/dist/compare/types.js +7 -5
  23. package/dist/compare/verification.d.ts +31 -12
  24. package/dist/compare/verification.js +88 -17
  25. package/dist/compat/eigenpal.d.ts +4 -4
  26. package/dist/compat/eigenpal.js +2 -2
  27. package/dist/display-list/primitives.d.ts +1 -1
  28. package/dist/document-operations.d.ts +10 -1
  29. package/dist/document-operations.js +3 -2
  30. package/dist/docx/appVersionNormalization.d.ts +49 -0
  31. package/dist/docx/appVersionNormalization.js +74 -0
  32. package/dist/docx/blockContentParser.js +3 -2
  33. package/dist/docx/drawingUtils.js +4 -3
  34. package/dist/docx/paragraphParser.js +3 -2
  35. package/dist/docx/rezip.d.ts +10 -4
  36. package/dist/docx/rezip.js +54 -18
  37. package/dist/docx/runParser.js +9 -8
  38. package/dist/docx/sdtProperties.js +4 -3
  39. package/dist/docx/selectiveSave.js +7 -6
  40. package/dist/docx/serializer/commentSerializer.d.ts +1 -1
  41. package/dist/docx/serializer/commentSerializer.js +43 -19
  42. package/dist/docx/serializer/documentSerializer.d.ts +3 -1
  43. package/dist/docx/serializer/documentSerializer.js +42 -78
  44. package/dist/docx/serializer/fontTableSerializer.js +8 -5
  45. package/dist/docx/serializer/headerFooterSerializer.d.ts +4 -1
  46. package/dist/docx/serializer/headerFooterSerializer.js +34 -29
  47. package/dist/docx/serializer/noteSerializer.js +34 -30
  48. package/dist/docx/serializer/numberingSerializer.js +8 -4
  49. package/dist/docx/serializer/paragraphSerializer.js +3 -3
  50. package/dist/docx/serializer/partNamespaces.d.ts +78 -0
  51. package/dist/docx/serializer/partNamespaces.js +324 -0
  52. package/dist/docx/serializer/runSerializer.js +1 -1
  53. package/dist/docx/serializer/settingsSerializer.js +8 -2
  54. package/dist/docx/serializer/stylesSerializer.js +8 -5
  55. package/dist/docx/serializer/tableSerializer.js +83 -18
  56. package/dist/docx/serializer/themeSerializer.js +10 -2
  57. package/dist/docx/server/build.d.ts +1 -1
  58. package/dist/docx/strictValueEncodings.gen.d.ts +15 -0
  59. package/dist/docx/strictValueEncodings.gen.js +275 -0
  60. package/dist/docx/tableParser.d.ts +0 -6
  61. package/dist/docx/tableParser.js +52 -7
  62. package/dist/docx/transitionalSpelling.d.ts +23 -0
  63. package/dist/docx/transitionalSpelling.js +36 -0
  64. package/dist/docx/universalMeasure.d.ts +20 -0
  65. package/dist/docx/universalMeasure.js +32 -0
  66. package/dist/docx/verbatimCapture.d.ts +20 -0
  67. package/dist/docx/verbatimCapture.js +131 -0
  68. package/dist/docx/vmlImageParser.js +4 -3
  69. package/dist/docx/watermarkParser.js +4 -3
  70. package/dist/docx/xmlParser.d.ts +23 -2
  71. package/dist/docx/xmlParser.js +43 -11
  72. package/dist/index.d.ts +4 -4
  73. package/dist/index.js +2 -2
  74. package/dist/model.d.ts +3 -3
  75. package/dist/model.js +2 -2
  76. package/dist/prosemirror/commands/comments.js +2 -1
  77. package/dist/prosemirror/containerFinalParagraph.d.ts +47 -0
  78. package/dist/prosemirror/containerFinalParagraph.js +92 -0
  79. package/dist/prosemirror/conversion/fromProseDoc.d.ts +8 -1
  80. package/dist/prosemirror/conversion/fromProseDoc.js +15 -4
  81. package/dist/prosemirror/conversion/toProseDoc.js +12 -3
  82. package/dist/prosemirror/extensions/nodes/TableExtension.js +5 -0
  83. package/dist/prosemirror/schema/marks.d.ts +1 -1
  84. package/dist/prosemirror/schema/nodes.d.ts +22 -0
  85. package/dist/server.d.ts +1 -1
  86. package/dist/types/block-id.d.ts +12 -1
  87. package/dist/types/block-id.js +17 -1
  88. package/dist/utils/canonicalJson.d.ts +14 -0
  89. package/dist/utils/canonicalJson.js +19 -0
  90. package/dist/utils/formatToStyle.d.ts +1 -1
  91. package/package.json +2 -2
@@ -457,16 +457,8 @@ const isEmptyParagraphNode = (block, snapshot) => {
457
457
  const anchor = snapshot.anchors[block.id] ?? panic("A comparison snapshot block has no matching anchor", { blockId: block.id });
458
458
  return block.text === "" && block.table === void 0 && anchor.to - anchor.from === 2;
459
459
  };
460
- const buildSteps = ({ story, baseSnapshot, targetSnapshot }) => {
461
- const baseBlocks = baseSnapshot.blocks;
462
- const targetBlocks = targetSnapshot.blocks;
463
- const baseLast = baseBlocks.at(-1);
464
- const targetLast = targetBlocks.at(-1);
465
- const terminalCarrierPair = story.type === "main" && baseLast !== void 0 && isEmptyParagraphNode(baseLast, baseSnapshot) && targetLast !== void 0 && isEmptyParagraphNode(targetLast, targetSnapshot) ? {
466
- type: "pair",
467
- baseBlock: baseLast,
468
- targetBlock: targetLast
469
- } : null;
460
+ /** Align both stories, optionally holding the two last paragraphs out as a pair. */
461
+ const alignedSteps = (baseBlocks, targetBlocks, terminalCarrierPair) => {
470
462
  const alignedBaseBlocks = terminalCarrierPair ? baseBlocks.slice(0, -1) : baseBlocks;
471
463
  const alignedTargetBlocks = terminalCarrierPair ? targetBlocks.slice(0, -1) : targetBlocks;
472
464
  const steps = [];
@@ -485,6 +477,65 @@ const buildSteps = ({ story, baseSnapshot, targetSnapshot }) => {
485
477
  return steps;
486
478
  };
487
479
  /**
480
+ * Whether an alignment left the story's last paragraph removed with no
481
+ * paragraph of its own container surviving in front of it.
482
+ *
483
+ * That paragraph's mark is the one a story cannot lose, and a deleted mark
484
+ * joins two paragraphs of ONE container, so with nothing of that container in
485
+ * front of it there is no chain a removal could run down. The paragraph then
486
+ * stays whatever the plan says, and the story ends on a blank line the target
487
+ * does not have — unless the words the target ends with are written into it,
488
+ * which is what reserving it for the target's last paragraph does.
489
+ */
490
+ const terminalCarrierIsStranded = (steps, baseBlocks) => {
491
+ const carrier = baseBlocks.at(-1);
492
+ if (carrier === void 0) return false;
493
+ const removed = new Set(steps.flatMap((step) => step.type === "baseOnly" ? [step.block.id] : []));
494
+ if (!removed.has(carrier.id)) return false;
495
+ const container = containerKeyOf(carrier);
496
+ for (let index = baseBlocks.length - 2; index >= 0; index--) {
497
+ const block = baseBlocks[index];
498
+ if (block === void 0 || containerKeyOf(block) !== container) return true;
499
+ if (!removed.has(block.id)) return false;
500
+ }
501
+ return true;
502
+ };
503
+ /**
504
+ * Whether an alignment ADDED the story's last paragraph with something other
505
+ * than paragraphs between it and the base's.
506
+ *
507
+ * An inserted mark at a container's end has the same problem its deletion
508
+ * does: nothing follows it, so rejecting the addition cannot close the break
509
+ * back over the next paragraph. The applier rotates instead — the paragraph
510
+ * the run was appended after takes the inserted mark, and the last one takes
511
+ * the free mark — and that rotation reaches only across paragraphs. A table
512
+ * among them stops it, and the words the target ends with have to be written
513
+ * into the base's own last paragraph instead.
514
+ */
515
+ const addedTerminalCarrierIsStranded = (steps, baseBlocks, targetBlocks) => {
516
+ const baseLast = baseBlocks.at(-1);
517
+ const targetLast = targetBlocks.at(-1);
518
+ if (baseLast === void 0 || targetLast === void 0) return false;
519
+ if (!steps.some((step) => step.type === "targetOnly" && step.block.id === targetLast.id)) return false;
520
+ const baseLastStep = steps.findIndex((step) => step.type === "pair" && step.baseBlock.id === baseLast.id || step.type === "baseOnly" && step.block.id === baseLast.id);
521
+ return baseLastStep === -1 || steps.slice(baseLastStep + 1).some((step) => step.type !== "targetOnly");
522
+ };
523
+ const buildSteps = ({ baseSnapshot, targetSnapshot }) => {
524
+ const baseBlocks = baseSnapshot.blocks;
525
+ const targetBlocks = targetSnapshot.blocks;
526
+ const baseLast = baseBlocks.at(-1);
527
+ const targetLast = targetBlocks.at(-1);
528
+ const carrierPair = baseLast !== void 0 && targetLast !== void 0 ? {
529
+ type: "pair",
530
+ baseBlock: baseLast,
531
+ targetBlock: targetLast
532
+ } : null;
533
+ const bothStoriesEndBlank = carrierPair !== null && isEmptyParagraphNode(carrierPair.baseBlock, baseSnapshot) && isEmptyParagraphNode(carrierPair.targetBlock, targetSnapshot);
534
+ const steps = alignedSteps(baseBlocks, targetBlocks, bothStoriesEndBlank ? carrierPair : null);
535
+ if (carrierPair === null || bothStoriesEndBlank || !shareAContainer(carrierPair.baseBlock, carrierPair.targetBlock) || !(terminalCarrierIsStranded(steps, baseBlocks) || addedTerminalCarrierIsStranded(steps, baseBlocks, targetBlocks))) return steps;
536
+ return alignedSteps(baseBlocks, targetBlocks, carrierPair);
537
+ };
538
+ /**
488
539
  * Two blocks are in the same container when a paragraph mark between them
489
540
  * exists at all: two body paragraphs, or two paragraphs of one table cell. A
490
541
  * mark cannot span a cell boundary, so a split or a merge across one is not a
@@ -714,67 +765,178 @@ const lastBlockByContainer = (blocks) => {
714
765
  return last;
715
766
  };
716
767
  /**
717
- * The operations that keep a container's final paragraph mark when the plan
718
- * deletes the paragraphs that end it.
768
+ * Rewrite the plan so a container's final paragraph mark survives the removal
769
+ * of the paragraphs that end it.
719
770
  *
720
771
  * A deleted paragraph mark means "merge this paragraph into the following
721
772
  * one". A container's final paragraph has no following one, so the mark cannot
722
773
  * say it: the applier leaves that mark alone, and the removal has to be
723
- * expressed one paragraph earlier. The chain therefore runs from the last
724
- * SURVIVING paragraph forward its mark goes, each removed paragraph's mark
725
- * goes with it, and the container's final paragraph stays as the carrier the
726
- * merged text lands in.
774
+ * expressed one paragraph earlier. The carrier is that final paragraph, and
775
+ * every trailing removal resolves onto it.
776
+ *
777
+ * Nothing is inserted where the run was: the chain runs from the last
778
+ * SURVIVING paragraph forward. Its mark goes, each removed paragraph's mark
779
+ * goes with it, and the carrier loses its words and keeps its mark as the
780
+ * paragraph the merged text lands in. A carrier that has no words to lose is
781
+ * not deleted at all: its removal is entirely the marks in front of it, and an
782
+ * operation that would write no revision belongs nowhere in the plan.
727
783
  *
728
- * The carrier keeps its own mark, and a paragraph's properties live on its
729
- * mark, so the merged paragraph ends up with the carrier's. Those properties
730
- * therefore become the target's, written as `w:pPrChange` so rejecting
731
- * restores what the carrier had. That is bookkeeping for the merge rather than
732
- * an edit of its own, so it adds no entry to the change list: what the reader
733
- * is told is that the paragraphs were removed.
784
+ * Something IS inserted there: the inserted paragraphs stand where the removed
785
+ * ones did, and the last of them lands INSIDE the carrier as inserted runs
786
+ * before its kept mark, so the carrier is the target's last paragraph rather
787
+ * than a blank line after it. The other inserted paragraphs carry inserted
788
+ * marks of their own and sit before the carrier. The mark count then works out
789
+ * on its own one mark deleted for every removed paragraph but the carrier,
790
+ * one inserted for every added paragraph but the last — so the chain is not
791
+ * rotated as well.
734
792
  *
735
- * Nothing is rotated when the plan puts a block inside or after the run. The
736
- * carrier is then no longer what ends the container, its mark is deleted like
737
- * any other, and the count already works out.
793
+ * The carrier keeps its own mark either way, and a paragraph's properties live
794
+ * on its mark, so the merged paragraph ends up with the carrier's. Those
795
+ * properties therefore become the target's, written as `w:pPrChange` so
796
+ * rejecting restores what the carrier had. That is bookkeeping for the merge
797
+ * rather than an edit of its own, so it adds no entry to the change list: what
798
+ * the reader is told is that paragraphs were removed and paragraphs added.
738
799
  */
739
- const trailingDeletionOperations = ({ baseSnapshot, targetSnapshot, operations, nextOperationId }) => {
800
+ const withTrailingDeletionRules = ({ baseSnapshot, targetSnapshot, operations, nextOperationId }) => {
801
+ const plan = [...operations];
802
+ const deletionIndexByBlockId = /* @__PURE__ */ new Map();
803
+ const insertIndexesByAnchor = /* @__PURE__ */ new Map();
804
+ const tableAnchorIds = /* @__PURE__ */ new Set();
805
+ const markedBlockIds = /* @__PURE__ */ new Set();
806
+ for (const [index, operation] of plan.entries()) switch (operation.type) {
807
+ case "deleteBlock":
808
+ deletionIndexByBlockId.set(operation.blockId, index);
809
+ break;
810
+ case "insertBeforeBlock":
811
+ case "insertAfterBlock": {
812
+ const placed = insertIndexesByAnchor.get(operation.blockId) ?? [];
813
+ placed.push(index);
814
+ insertIndexesByAnchor.set(operation.blockId, placed);
815
+ break;
816
+ }
817
+ case "insertTable":
818
+ tableAnchorIds.add(operation.blockId);
819
+ break;
820
+ case "splitBlock":
821
+ case "mergeBlockWithNext":
822
+ markedBlockIds.add(operation.blockId);
823
+ break;
824
+ default: break;
825
+ }
826
+ if (deletionIndexByBlockId.size === 0) return plan;
740
827
  const blocks = baseSnapshot.blocks;
741
- const deletedBlockIds = new Set(operations.flatMap((operation) => operation.type === "deleteBlock" ? [operation.blockId] : []));
742
- if (deletedBlockIds.size === 0) return [];
743
- const placedAtBlockIds = new Set(operations.flatMap((operation) => operation.type === "insertBeforeBlock" || operation.type === "insertAfterBlock" || operation.type === "insertTable" ? [operation.blockId] : []));
744
- const markedBlockIds = new Set(operations.flatMap((operation) => operation.type === "splitBlock" || operation.type === "mergeBlockWithNext" ? [operation.blockId] : []));
745
- const targetLastByContainer = lastBlockByContainer(targetSnapshot.blocks);
746
828
  const indexById = new Map(blocks.map((block, index) => [block.id, index]));
747
- const added = [];
748
- for (const [container, carrier] of lastBlockByContainer(blocks)) {
749
- if (!deletedBlockIds.has(carrier.id)) continue;
750
- let index = indexById.get(carrier.id) ?? panic("A container's last block is not in the snapshot it came from", { blockId: carrier.id });
829
+ /**
830
+ * Walk back from a container's last block over the run of deleted
831
+ * paragraphs it ends with. A block of another container in between — a
832
+ * table between two body paragraphs, a table nested in a cell ends the
833
+ * walk: no mark joins across it.
834
+ */
835
+ const trailingRunOf = (container, carrier) => {
836
+ const blockIds = [];
837
+ const insertIndexes = [];
751
838
  let chainStart = null;
752
- let placedInRun = false;
839
+ let holdsAnInsertedTable = false;
840
+ let index = indexById.get(carrier.id) ?? panic("A container's last block is not in the snapshot it came from", { blockId: carrier.id });
753
841
  for (; index >= 0; index--) {
754
842
  const block = blocks[index];
755
843
  if (block === void 0 || containerKeyOf(block) !== container) break;
756
- if (!deletedBlockIds.has(block.id)) {
844
+ if (!deletionIndexByBlockId.has(block.id)) {
757
845
  chainStart = markedBlockIds.has(block.id) ? null : block;
758
846
  break;
759
847
  }
760
- placedInRun ||= placedAtBlockIds.has(block.id);
848
+ blockIds.push(block.id);
849
+ insertIndexes.push(...insertIndexesByAnchor.get(block.id) ?? []);
850
+ holdsAnInsertedTable ||= tableAnchorIds.has(block.id);
851
+ }
852
+ return {
853
+ blockIds,
854
+ chainStart,
855
+ insertIndexes: insertIndexes.toSorted((left, right) => left - right),
856
+ holdsAnInsertedTable
857
+ };
858
+ };
859
+ const targetLastByContainer = lastBlockByContainer(targetSnapshot.blocks);
860
+ const dropped = /* @__PURE__ */ new Set();
861
+ const appended = [];
862
+ for (const [container, carrier] of lastBlockByContainer(blocks)) {
863
+ const carrierDeletionIndex = deletionIndexByBlockId.get(carrier.id);
864
+ if (carrierDeletionIndex === void 0) continue;
865
+ const run = trailingRunOf(container, carrier);
866
+ if (run.holdsAnInsertedTable) continue;
867
+ const lastInsertIndex = run.insertIndexes.at(-1);
868
+ if (lastInsertIndex === void 0) {
869
+ if (run.chainStart) appended.push({
870
+ id: nextOperationId(),
871
+ type: "mergeBlockWithNext",
872
+ blockId: run.chainStart.id
873
+ });
874
+ if (isEmptyParagraphNode(carrier, baseSnapshot)) dropped.add(carrierDeletionIndex);
875
+ } else {
876
+ const lastInsert = plan[lastInsertIndex];
877
+ if (lastInsert === void 0 || lastInsert.type !== "insertBeforeBlock" && lastInsert.type !== "insertAfterBlock" || lastInsert.moveId !== void 0) continue;
878
+ const carrierDeletion = plan[carrierDeletionIndex];
879
+ if (carrierDeletion?.type !== "deleteBlock" || carrierDeletion.moveId !== void 0) continue;
880
+ dropped.add(lastInsertIndex);
881
+ if (carrier.text === lastInsert.text) dropped.add(carrierDeletionIndex);
882
+ else plan[carrierDeletionIndex] = {
883
+ id: carrierDeletion.id,
884
+ type: "replaceBlock",
885
+ blockId: carrier.id,
886
+ text: lastInsert.text
887
+ };
888
+ for (const index of run.insertIndexes.slice(0, -1)) {
889
+ const insert = plan[index];
890
+ if (insert?.type !== "insertBeforeBlock" && insert?.type !== "insertAfterBlock") continue;
891
+ plan[index] = {
892
+ id: insert.id,
893
+ type: "insertBeforeBlock",
894
+ blockId: carrier.id,
895
+ text: insert.text,
896
+ ...insert.moveId !== void 0 && { moveId: insert.moveId },
897
+ styleId: insert.styleId ?? null,
898
+ listLevel: insert.listLevel ?? null
899
+ };
900
+ }
761
901
  }
762
- if (placedInRun) continue;
763
- if (chainStart) added.push({
764
- id: nextOperationId(),
765
- type: "mergeBlockWithNext",
766
- blockId: chainStart.id
767
- });
768
902
  const targetCarrier = targetLastByContainer.get(container);
769
903
  const properties = targetCarrier && changedParagraphProperties(carrier, targetCarrier);
770
- if (properties) added.push({
904
+ if (properties) appended.push({
771
905
  id: nextOperationId(),
772
906
  type: "setBlockParagraphProperties",
773
907
  blockId: carrier.id,
774
908
  properties
775
909
  });
776
910
  }
777
- return added;
911
+ return [...plan.filter((_, index) => !dropped.has(index)), ...appended];
912
+ };
913
+ const cellCoordinate = ({ tableIndex, rowIndex, cellIndex }) => ({
914
+ tableIndex,
915
+ rowIndex,
916
+ cellIndex
917
+ });
918
+ /**
919
+ * The cells the alignment paired, one entry each. A cell holds several
920
+ * paragraphs and each pairs on its own, so the first pairing of a cell is the
921
+ * one kept: later ones would name the same two cells.
922
+ */
923
+ const tableGeometryPairingsOf = (steps) => {
924
+ const pairings = [];
925
+ const seen = /* @__PURE__ */ new Set();
926
+ for (const step of steps) {
927
+ if (step.type !== "pair") continue;
928
+ const base = step.baseBlock.table;
929
+ const target = step.targetBlock.table;
930
+ if (!base || !target) continue;
931
+ const key = `${String(base.tableIndex)}:${String(base.rowIndex)}:${String(base.cellIndex)}`;
932
+ if (seen.has(key)) continue;
933
+ seen.add(key);
934
+ pairings.push({
935
+ base: cellCoordinate(base),
936
+ target: cellCoordinate(target)
937
+ });
938
+ }
939
+ return pairings;
778
940
  };
779
941
  /**
780
942
  * Plan one story's comparison, or `null` when it needs more operations than
@@ -782,7 +944,6 @@ const trailingDeletionOperations = ({ baseSnapshot, targetSnapshot, operations,
782
944
  */
783
945
  const planStoryCompare = ({ story, baseSnapshot, targetSnapshot, maxOperations }) => {
784
946
  const steps = buildSteps({
785
- story,
786
947
  baseSnapshot,
787
948
  targetSnapshot
788
949
  });
@@ -794,6 +955,7 @@ const planStoryCompare = ({ story, baseSnapshot, targetSnapshot, maxOperations }
794
955
  const anchorIds = nextBaseBlockIdByStep(steps);
795
956
  const changes = [];
796
957
  const operations = [];
958
+ const tableTemplates = [];
797
959
  /**
798
960
  * The anchor everything past the base document's content hangs from: its
799
961
  * last BODY-LEVEL paragraph, which the format guarantees exists because a
@@ -1056,23 +1218,20 @@ const planStoryCompare = ({ story, baseSnapshot, targetSnapshot, maxOperations }
1056
1218
  targetBlockIds: step.blocks.map(({ id }) => id)
1057
1219
  });
1058
1220
  const before = anchorIds[stepIndex] ?? null;
1059
- if (before !== null) {
1060
- operations.push({
1061
- id: nextOperationId(),
1062
- type: "insertTable",
1063
- blockId: before,
1064
- position: "before",
1065
- rows
1066
- });
1067
- break;
1068
- }
1069
- if (tailAnchorId !== null) operations.push({
1070
- id: nextOperationId(),
1221
+ const anchorBlockId = before ?? tailAnchorId;
1222
+ if (anchorBlockId === null) break;
1223
+ const operationId = nextOperationId();
1224
+ operations.push({
1225
+ id: operationId,
1071
1226
  type: "insertTable",
1072
- blockId: tailAnchorId,
1073
- position: "after",
1227
+ blockId: anchorBlockId,
1228
+ position: before === null ? "after" : "before",
1074
1229
  rows
1075
1230
  });
1231
+ tableTemplates.push({
1232
+ operationId,
1233
+ targetTableIndex: step.location.tableIndex
1234
+ });
1076
1235
  break;
1077
1236
  }
1078
1237
  case "targetRow": {
@@ -1093,13 +1252,19 @@ const planStoryCompare = ({ story, baseSnapshot, targetSnapshot, maxOperations }
1093
1252
  for (const block of step.blocks) pushInsertOperation(block, anchorIds[stepIndex] ?? null);
1094
1253
  break;
1095
1254
  }
1255
+ const rowOperationId = nextOperationId();
1096
1256
  operations.push({
1097
- id: nextOperationId(),
1257
+ id: rowOperationId,
1098
1258
  type: "insertTableRow",
1099
1259
  blockId: anchor.blockId,
1100
1260
  position: anchor.position,
1101
1261
  cellTexts: cells
1102
1262
  });
1263
+ tableTemplates.push({
1264
+ operationId: rowOperationId,
1265
+ targetTableIndex: step.location.tableIndex,
1266
+ targetRowIndex: step.location.rowIndex
1267
+ });
1103
1268
  break;
1104
1269
  }
1105
1270
  case "targetColumn":
@@ -1126,15 +1291,17 @@ const planStoryCompare = ({ story, baseSnapshot, targetSnapshot, maxOperations }
1126
1291
  }
1127
1292
  if (operations.length > maxOperations) return null;
1128
1293
  }
1129
- operations.push(...trailingDeletionOperations({
1294
+ const planned = withTrailingDeletionRules({
1130
1295
  baseSnapshot,
1131
1296
  targetSnapshot,
1132
1297
  operations,
1133
1298
  nextOperationId
1134
- }));
1135
- return operations.length > maxOperations ? null : {
1299
+ });
1300
+ return planned.length > maxOperations ? null : {
1136
1301
  changes,
1137
- operations
1302
+ operations: planned,
1303
+ tableTemplates,
1304
+ tableGeometryPairings: tableGeometryPairingsOf(steps)
1138
1305
  };
1139
1306
  };
1140
1307
  //#endregion
@@ -1,7 +1,7 @@
1
1
  import { FolioAIBlockParagraphProperties, FolioAIBlockTableLocation, FolioAIEditSkippedOperation, FolioAIInlineFormatting } from "../ai-edits/types.js";
2
2
  import { WordDiffGranularity } from "../ai-edits/word-diff.js";
3
3
  import { FolioDocumentStoryHandle, FolioNumberingLevel } from "../ai-edits/headless.js";
4
- import { CompareVerification, CompareVerificationCause, CompareVerificationFailure, CompareVerificationInvariant, FinalParagraphMarkDeletion } from "./verification.js";
4
+ import { CompareVerification, CompareVerificationCause, CompareVerificationFailure, CompareVerificationInvariant, FinalParagraphMarkRevision } from "./verification.js";
5
5
  //#region src/compare/types.d.ts
6
6
  /** Everything {@link compareDocx} needs; nothing it reads from the ambient clock. */
7
7
  type CompareDocxOptions = {
@@ -279,18 +279,20 @@ declare class CompareDocxSerializeError extends CompareDocxSerializeError_base<{
279
279
  }> {}
280
280
  declare const CompareDocxFinalParagraphMarkError_base: import("better-result").TaggedErrorClass<"CompareDocxFinalParagraphMarkError">;
281
281
  /**
282
- * A container's final paragraph mark carries a deletion, so the package would
283
- * not open.
282
+ * A container's final paragraph mark carries a revision, so the package would
283
+ * not open, or would open carrying one no reader can resolve.
284
284
  *
285
285
  * A deleted paragraph mark means "merge this paragraph into the following
286
- * one", and a container's last paragraph has no following one. Checked before
287
- * the package is written, and fatal under either `onUnverified` setting: there
288
- * is no redline to emit when a consumer refuses the file.
286
+ * one", an inserted one means the break was added and rejecting it closes the
287
+ * paragraph back over the next one, and a container's last paragraph has no
288
+ * following one either way. Checked before the package is written, and fatal
289
+ * under either `onUnverified` setting: there is no redline to emit when a
290
+ * consumer refuses the file.
289
291
  */
290
292
  declare class CompareDocxFinalParagraphMarkError extends CompareDocxFinalParagraphMarkError_base<{
291
293
  message: string;
292
294
  /** Every container that carries one, each named structurally. */
293
- deletions: readonly FinalParagraphMarkDeletion[];
295
+ revisions: readonly FinalParagraphMarkRevision[];
294
296
  }> {}
295
297
  type CompareDocxError = CompareDocxApplyError | CompareDocxFinalParagraphMarkError | CompareDocxOperationLimitError | CompareDocxParseError | CompareDocxRoundTripError | CompareDocxSerializeError | InvalidCompareDocxOptionsError;
296
298
  //#endregion
@@ -29,13 +29,15 @@ var CompareDocxRoundTripError = class extends TaggedError("CompareDocxRoundTripE
29
29
  var CompareDocxOperationLimitError = class extends TaggedError("CompareDocxOperationLimitError") {};
30
30
  var CompareDocxSerializeError = class extends TaggedError("CompareDocxSerializeError") {};
31
31
  /**
32
- * A container's final paragraph mark carries a deletion, so the package would
33
- * not open.
32
+ * A container's final paragraph mark carries a revision, so the package would
33
+ * not open, or would open carrying one no reader can resolve.
34
34
  *
35
35
  * A deleted paragraph mark means "merge this paragraph into the following
36
- * one", and a container's last paragraph has no following one. Checked before
37
- * the package is written, and fatal under either `onUnverified` setting: there
38
- * is no redline to emit when a consumer refuses the file.
36
+ * one", an inserted one means the break was added and rejecting it closes the
37
+ * paragraph back over the next one, and a container's last paragraph has no
38
+ * following one either way. Checked before the package is written, and fatal
39
+ * under either `onUnverified` setting: there is no redline to emit when a
40
+ * consumer refuses the file.
39
41
  */
40
42
  var CompareDocxFinalParagraphMarkError = class extends TaggedError("CompareDocxFinalParagraphMarkError") {};
41
43
  //#endregion
@@ -1,5 +1,6 @@
1
1
  import { FolioAIBlock } from "../ai-edits/types.js";
2
2
  import { FolioDocumentStoryHandle } from "../ai-edits/headless.js";
3
+ import { ParagraphMarkChangeKind } from "@stll/docx-core/model";
3
4
  //#region src/compare/verification.d.ts
4
5
  /** The two directions of the round trip, each an invariant of its own. */
5
6
  declare const COMPARE_VERIFICATION_INVARIANTS: readonly ["accept-reproduces-target", "reject-reproduces-base"];
@@ -9,7 +10,7 @@ type CompareVerificationInvariant = (typeof COMPARE_VERIFICATION_INVARIANTS)[num
9
10
  * that fits is the one reported, because a block in the wrong container is a
10
11
  * different finding from a block whose words are wrong even when both are true.
11
12
  */
12
- declare const COMPARE_VERIFICATION_CAUSES: readonly ["invisible-structure", "block-count", "container", "style", "list-level", "inline-formatting", "whitespace", "text"];
13
+ declare const COMPARE_VERIFICATION_CAUSES: readonly ["invisible-structure", "block-count", "container", "table-geometry", "style", "list-level", "inline-formatting", "whitespace", "text"];
13
14
  type CompareVerificationCause = (typeof COMPARE_VERIFICATION_CAUSES)[number];
14
15
  /** One invariant that did not hold, in one story. */
15
16
  type CompareVerificationFailure = {
@@ -44,6 +45,13 @@ type ClassifyOptions = {
44
45
  /** What the invariant says it should leave. */
45
46
  expected: readonly string[];
46
47
  };
48
+ /**
49
+ * The failure two table-geometry projections describe, or `null` when they
50
+ * agree. One line per table, so the detail names which table diverged and
51
+ * whether the count itself did — never a property value, which could carry a
52
+ * style name either document chose.
53
+ */
54
+ declare const classifyGeometryMismatch: ({ invariant, story, actual, expected }: ClassifyOptions) => CompareVerificationFailure | null;
47
55
  /**
48
56
  * The failure two projections describe, or `null` when they agree.
49
57
  *
@@ -52,32 +60,43 @@ type ClassifyOptions = {
52
60
  */
53
61
  declare const classifyProjectionMismatch: ({ invariant, story, actual, expected }: ClassifyOptions) => CompareVerificationFailure | null;
54
62
  /**
55
- * A container whose last paragraph carries a deletion on its mark.
63
+ * A container whose last paragraph carries a revision on its mark.
56
64
  *
57
65
  * Structural facts only — a path through the package model and an index — so
58
66
  * the finding is safe to log, report or quote.
59
67
  */
60
- type FinalParagraphMarkDeletion = {
68
+ type FinalParagraphMarkRevision = {
61
69
  /** Where the container sits in the package model, e.g. `package.document.content`. */
62
70
  container: string;
63
71
  /** The paragraph's index among its container's children. */
64
72
  paragraphIndex: number;
65
- /** The mark kind found there: `del`, or `moveFrom` for a relocation's source. */
66
- kind: "del" | "moveFrom";
73
+ /** The mark kind found there, `moveFrom` / `moveTo` for a relocation's ends. */
74
+ kind: ParagraphMarkChangeKind;
67
75
  };
68
76
  /**
69
- * Every container in a package whose final paragraph mark carries a deletion.
77
+ * Every container in a package whose final paragraph mark carries a revision.
70
78
  *
71
- * A deleted paragraph mark says "join this paragraph with the one after it".
72
- * The last paragraph of a body, a table cell, a header or footer, a note or a
73
- * text box has no paragraph after it, so the mark states an edit that cannot
74
- * be carried out, and a consumer refuses the package rather than opening it.
79
+ * A deleted paragraph mark says "join this paragraph with the one after it",
80
+ * and an inserted one says that break was ADDED, so rejecting it closes the
81
+ * paragraph back over the next one. The last paragraph of a body, a table
82
+ * cell, a header or footer, a note or a text box has no paragraph after it, so
83
+ * neither direction states an edit that can be carried out: a consumer refuses
84
+ * the package, or opens it and leaves a revision standing that neither
85
+ * accepting nor rejecting everything can clear. The exception is a cell of a
86
+ * row the package is DELETING: there the mark leaves with its row.
75
87
  *
76
88
  * The walk is over the package model rather than over a list of the containers
77
89
  * known today: a container is any sequence that ends in a paragraph, so a part
78
90
  * the model grows later is covered the day it arrives instead of the day
79
91
  * someone remembers this function.
92
+ *
93
+ * `since` scopes it to the revisions a comparison MINTED: a base may arrive
94
+ * carrying one of these on a paragraph in a part no story mounts, which folio
95
+ * preserves the way it preserves everything else it parses. What this proves
96
+ * is that the comparison writes none of its own.
80
97
  */
81
- declare const deletedFinalParagraphMarks: (packageModel: unknown) => FinalParagraphMarkDeletion[];
98
+ declare const revisedFinalParagraphMarks: (packageModel: unknown, { since }?: {
99
+ since?: number;
100
+ }) => FinalParagraphMarkRevision[];
82
101
  //#endregion
83
- export { COMPARE_VERIFICATION_CAUSES, COMPARE_VERIFICATION_INVARIANTS, CompareVerification, CompareVerificationCause, CompareVerificationFailure, CompareVerificationInvariant, FinalParagraphMarkDeletion, classifyProjectionMismatch, deletedFinalParagraphMarks, projectSupportedInlineFormatting, sameProjection };
102
+ export { COMPARE_VERIFICATION_CAUSES, COMPARE_VERIFICATION_INVARIANTS, CompareVerification, CompareVerificationCause, CompareVerificationFailure, CompareVerificationInvariant, FinalParagraphMarkRevision, classifyGeometryMismatch, classifyProjectionMismatch, projectSupportedInlineFormatting, revisedFinalParagraphMarks, sameProjection };