@stll/folio-core 0.53.0 → 0.54.0

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 (148) hide show
  1. package/dist/ai-edits/apply.js +390 -166
  2. package/dist/ai-edits/batch-claims.d.ts +21 -6
  3. package/dist/ai-edits/batch-claims.js +47 -16
  4. package/dist/ai-edits/headless.d.ts +20 -0
  5. package/dist/ai-edits/headless.js +140 -11
  6. package/dist/ai-edits/pending-suggestions.js +1 -1
  7. package/dist/ai-edits/read.d.ts +5 -3
  8. package/dist/ai-edits/read.js +97 -47
  9. package/dist/ai-edits/revisionStretches.d.ts +17 -0
  10. package/dist/ai-edits/revisionStretches.js +80 -0
  11. package/dist/ai-edits/snapshot.js +3 -2
  12. package/dist/ai-edits/table-cell-mutations.d.ts +3 -1
  13. package/dist/ai-edits/table-cell-mutations.js +2 -1
  14. package/dist/ai-edits/table-row-column-mutations.d.ts +6 -1
  15. package/dist/ai-edits/table-row-column-mutations.js +27 -10
  16. package/dist/compare/content-alignment.d.ts +5 -1
  17. package/dist/compare/content-alignment.js +39 -32
  18. package/dist/compare/inline-provenance.js +1 -1
  19. package/dist/controller/layoutPipeline.js +3 -2
  20. package/dist/docx/archiveInflation.d.ts +127 -0
  21. package/dist/docx/archiveInflation.js +181 -0
  22. package/dist/docx/metadataPrivacy.js +24 -1
  23. package/dist/docx/noteReferenceMark.d.ts +15 -0
  24. package/dist/docx/noteReferenceMark.js +48 -0
  25. package/dist/docx/paragraphParser.js +5 -3
  26. package/dist/docx/paragraphPropertySource.d.ts +1 -1
  27. package/dist/docx/paragraphPropertySource.js +3 -1
  28. package/dist/docx/parser.js +2 -2
  29. package/dist/docx/rezip.js +4 -2
  30. package/dist/docx/selectiveXmlPatch.d.ts +3 -1
  31. package/dist/docx/selectiveXmlPatch.js +81 -3
  32. package/dist/docx/server/boundedArchive.d.ts +8 -13
  33. package/dist/docx/server/boundedArchive.js +70 -59
  34. package/dist/docx/server/extractDocxText.d.ts +8 -2
  35. package/dist/docx/server/extractDocxText.js +2 -2
  36. package/dist/docx/server/validateDocxConformance.d.ts +1 -1
  37. package/dist/docx/server/validateDocxConformance.js +1 -0
  38. package/dist/docx/unzip.d.ts +19 -2
  39. package/dist/docx/unzip.js +135 -24
  40. package/dist/fonts/embeddedFonts.js +1 -1
  41. package/dist/i18n/messages/catalogs.gen.d.ts +34 -34
  42. package/dist/i18n/messages/catalogs.gen.js +34 -34
  43. package/dist/i18n/messages/messages.gen.d.ts +2 -2
  44. package/dist/internal/wholeStoryRevisionResolution.js +95 -25
  45. package/dist/layout-engine/measure/measureParagraph.js +2 -2
  46. package/dist/layout-engine/measure/paragraphMeasureShared.d.ts +12 -1
  47. package/dist/layout-engine/measure/paragraphMeasureShared.js +14 -1
  48. package/dist/markdown/escape.d.ts +2 -2
  49. package/dist/markdown/escape.js +4 -5
  50. package/dist/markdown/renderBlock.js +91 -25
  51. package/dist/markdown/renderRuns.d.ts +10 -1
  52. package/dist/markdown/renderRuns.js +320 -59
  53. package/dist/prosemirror/clearRunColor.d.ts +6 -0
  54. package/dist/prosemirror/clearRunColor.js +46 -0
  55. package/dist/prosemirror/commands/comments.js +56 -21
  56. package/dist/prosemirror/commands/formatPainter.js +1 -1
  57. package/dist/prosemirror/commands/hyperlink.js +2 -1
  58. package/dist/prosemirror/commands/propertyChangeScope.d.ts +3 -2
  59. package/dist/prosemirror/commands/propertyChangeScope.js +24 -3
  60. package/dist/prosemirror/commands/resolveAllTableChanges.js +46 -41
  61. package/dist/prosemirror/commands/resolveParagraphProperties.js +20 -6
  62. package/dist/prosemirror/commands/tableCellMergeResolution.d.ts +8 -1
  63. package/dist/prosemirror/commands/tableCellMergeResolution.js +23 -7
  64. package/dist/prosemirror/containerFinalParagraph.d.ts +11 -5
  65. package/dist/prosemirror/containerFinalParagraph.js +7 -6
  66. package/dist/prosemirror/conversion/fromProseDoc.d.ts +2 -13
  67. package/dist/prosemirror/conversion/fromProseDoc.js +12 -590
  68. package/dist/prosemirror/conversion/toProseDoc.d.ts +4 -11
  69. package/dist/prosemirror/conversion/toProseDoc.js +31 -150
  70. package/dist/prosemirror/documentSchema.d.ts +8 -0
  71. package/dist/prosemirror/documentSchema.js +18 -0
  72. package/dist/prosemirror/extensions/StarterKit.js +2 -0
  73. package/dist/prosemirror/extensions/core/ParagraphExtension.js +104 -63
  74. package/dist/prosemirror/extensions/features/BaseKeymapExtension.js +1 -1
  75. package/dist/prosemirror/extensions/features/EmptyParagraphFormatExtension.js +24 -3
  76. package/dist/prosemirror/extensions/features/JoinedRunStyleExtension.d.ts +20 -0
  77. package/dist/prosemirror/extensions/features/JoinedRunStyleExtension.js +43 -0
  78. package/dist/prosemirror/extensions/features/ListExtension.js +10 -9
  79. package/dist/prosemirror/extensions/features/pastedHeadingStyles.js +1 -1
  80. package/dist/prosemirror/extensions/features/pastedHtmlLists.js +2 -4
  81. package/dist/prosemirror/extensions/marks/FootnoteRefExtension.js +14 -8
  82. package/dist/prosemirror/extensions/marks/HyperlinkExtension.js +8 -4
  83. package/dist/prosemirror/extensions/marks/StrikeExtension.js +2 -2
  84. package/dist/prosemirror/extensions/marks/SubscriptExtension.js +2 -2
  85. package/dist/prosemirror/extensions/marks/SuperscriptExtension.js +2 -2
  86. package/dist/prosemirror/extensions/marks/TextColorExtension.js +4 -3
  87. package/dist/prosemirror/extensions/marks/markUtils.d.ts +8 -6
  88. package/dist/prosemirror/extensions/marks/markUtils.js +17 -8
  89. package/dist/prosemirror/extensions/marks/noteReferenceDeletion.d.ts +3 -1
  90. package/dist/prosemirror/extensions/marks/noteReferenceDeletion.js +12 -1
  91. package/dist/prosemirror/extensions/nodes/CommentReferenceExtension.js +2 -2
  92. package/dist/prosemirror/extensions/nodes/HardBreakExtension.js +5 -3
  93. package/dist/prosemirror/extensions/nodes/TableExtension.js +104 -210
  94. package/dist/prosemirror/extensions/types.d.ts +7 -0
  95. package/dist/prosemirror/hyperlinkRemoval.d.ts +9 -0
  96. package/dist/prosemirror/hyperlinkRemoval.js +129 -0
  97. package/dist/prosemirror/index.d.ts +2 -1
  98. package/dist/prosemirror/index.js +2 -1
  99. package/dist/prosemirror/listNumbering.d.ts +3 -21
  100. package/dist/prosemirror/listNumbering.js +10 -25
  101. package/dist/prosemirror/listRendering.d.ts +27 -0
  102. package/dist/prosemirror/listRendering.js +27 -0
  103. package/dist/prosemirror/markupViewNotes.d.ts +8 -0
  104. package/dist/prosemirror/markupViewNotes.js +49 -0
  105. package/dist/prosemirror/markupViewProjection.d.ts +3 -1
  106. package/dist/prosemirror/markupViewProjection.js +7 -2
  107. package/dist/prosemirror/noteReferenceReview.d.ts +77 -0
  108. package/dist/prosemirror/noteReferenceReview.js +332 -0
  109. package/dist/prosemirror/paragraphIndentation.js +5 -2
  110. package/dist/prosemirror/paragraphMarkJoin.d.ts +6 -4
  111. package/dist/prosemirror/paragraphMarkJoin.js +21 -19
  112. package/dist/prosemirror/paragraphPropertyCarry.d.ts +64 -0
  113. package/dist/prosemirror/paragraphPropertyCarry.js +158 -0
  114. package/dist/prosemirror/plugins/documentStyleState.d.ts +26 -0
  115. package/dist/prosemirror/plugins/documentStyleState.js +32 -0
  116. package/dist/prosemirror/plugins/documentStyles.d.ts +2 -21
  117. package/dist/prosemirror/plugins/documentStyles.js +66 -29
  118. package/dist/prosemirror/plugins/index.d.ts +2 -1
  119. package/dist/prosemirror/plugins/index.js +2 -1
  120. package/dist/prosemirror/plugins/paragraphStyleResolution.d.ts +15 -0
  121. package/dist/prosemirror/plugins/paragraphStyleResolution.js +230 -0
  122. package/dist/prosemirror/plugins/suggestionMode.d.ts +12 -2
  123. package/dist/prosemirror/plugins/suggestionMode.js +391 -28
  124. package/dist/prosemirror/rebaseParagraphRunFormatting.d.ts +22 -2
  125. package/dist/prosemirror/rebaseParagraphRunFormatting.js +39 -13
  126. package/dist/prosemirror/rebaseParagraphRuns.d.ts +36 -0
  127. package/dist/prosemirror/rebaseParagraphRuns.js +99 -0
  128. package/dist/prosemirror/runFormattingFromMarks.d.ts +18 -0
  129. package/dist/prosemirror/runFormattingFromMarks.js +567 -0
  130. package/dist/prosemirror/runFormattingReconciliation.js +10 -9
  131. package/dist/prosemirror/schema/nodes.d.ts +2 -0
  132. package/dist/prosemirror/styles/paragraphStyleCascade.d.ts +59 -0
  133. package/dist/prosemirror/styles/paragraphStyleCascade.js +145 -0
  134. package/dist/prosemirror/styles/resolvedStyleAttrs.d.ts +21 -2
  135. package/dist/prosemirror/styles/resolvedStyleAttrs.js +38 -1
  136. package/dist/prosemirror/styles/tableStyleRegions.d.ts +31 -0
  137. package/dist/prosemirror/styles/tableStyleRegions.js +55 -0
  138. package/dist/prosemirror/tableCellPaste.d.ts +74 -0
  139. package/dist/prosemirror/tableCellPaste.js +525 -0
  140. package/dist/prosemirror/tableGridMutation.d.ts +53 -14
  141. package/dist/prosemirror/tableGridMutation.js +166 -18
  142. package/dist/prosemirror/tableRunIn.d.ts +51 -0
  143. package/dist/prosemirror/tableRunIn.js +165 -0
  144. package/dist/prosemirror/textInput.js +11 -2
  145. package/dist/prosemirror/trackedRevisionPath.d.ts +17 -0
  146. package/dist/prosemirror/trackedRevisionPath.js +32 -0
  147. package/dist/server.d.ts +2 -2
  148. package/package.json +5 -5
@@ -2,10 +2,12 @@ import { paragraphNumberingReference, paragraphNumberingReferenceId } from "../d
2
2
  import { formattingEquals } from "../docx/runConsolidator.js";
3
3
  import { isStyleSourcedParagraphNumbering } from "../internal/paragraphFormattingSerialization.js";
4
4
  import { deleteTextBoxesAnchoredAt, droppedTextBoxAnchorIds, endOfParagraphWithItsTextBoxes, gatherTextBoxesAfterParagraph, textBoxAnchorIdOf, textBoxAnchorIdsBetween } from "../prosemirror/anchoredTextBoxes.js";
5
- import { expectParagraphAttrs, expectRunPropertyChangeMarkAttrs } from "../prosemirror/attrs/index.js";
5
+ import { expectParagraphAttrs, expectRunPropertyChangeMarkAttrs, expectTableRowAttrs } from "../prosemirror/attrs/index.js";
6
6
  import { hasSerializableParagraphPropertyChange, paragraphPropertiesSnapshot } from "../prosemirror/commands/propertyChangeScope.js";
7
+ import { resolveParagraphChangeAttrs } from "../prosemirror/commands/resolveParagraphProperties.js";
7
8
  import { addedBreakCarrierBefore, finalParagraphsOf, paragraphEndsItsContainer } from "../prosemirror/containerFinalParagraph.js";
8
9
  import { encloseWholeControls } from "../prosemirror/contentControlRevisions.js";
10
+ import { JOINED_RUNS_RESTYLED_META } from "../prosemirror/extensions/features/JoinedRunStyleExtension.js";
9
11
  import { requestDeterministicParaIds } from "../prosemirror/extensions/features/ParaIdAllocatorExtension.js";
10
12
  import { markStructuralChange } from "../prosemirror/extensions/features/ParagraphChangeTrackerExtension.js";
11
13
  import "../prosemirror/extensions/nodes/TextBoxAnchorExtension.js";
@@ -15,9 +17,11 @@ import { directParagraphAlignment } from "../prosemirror/paragraphAlignment.js";
15
17
  import { styleResolvedParagraphFormatting } from "../prosemirror/paragraphFormattingProvenance.js";
16
18
  import { directParagraphIndentation, paragraphIndentationAttrPatch, paragraphIndentationEqual, paragraphIndentationFromFormatting, withDirectParagraphIndentation } from "../prosemirror/paragraphIndentation.js";
17
19
  import { joinAtParagraphMark } from "../prosemirror/paragraphMarkJoin.js";
20
+ import { carryParagraphProperties, paragraphLeftAfter, propertiesSetInBatch, recordReplacedParagraphProperties } from "../prosemirror/paragraphPropertyCarry.js";
18
21
  import { directParagraphSpacing, paragraphSpacingAttrPatch, paragraphSpacingEqual, paragraphSpacingFromFormatting, withDirectParagraphSpacing } from "../prosemirror/paragraphSpacing.js";
19
22
  import { getDocumentNumbering, getDocumentNumberingInstanceIds } from "../prosemirror/plugins/documentNumbering.js";
20
- import { getDocumentStyleDefinitions, getDocumentStyleResolver } from "../prosemirror/plugins/documentStyles.js";
23
+ import { getDocumentStyleDefinitions, getDocumentStyleResolver } from "../prosemirror/plugins/documentStyleState.js";
24
+ import { rebaseParagraphRuns } from "../prosemirror/rebaseParagraphRuns.js";
21
25
  import { NON_INCLUSIVE_MARK_DISPOSITION, addCarriedMarks, annotatedReplacement, commentsAcrossBlockBoundary, commentsForTrackedInsertion, hasReplacedAnnotations, inheritedReplacementMarks, placeTrackedInsertion, surveyReplacedAnnotations } from "../prosemirror/replacedAnnotations.js";
22
26
  import { applyMarksToRunFormattingRepresentation, runFormattingInlineControlNodeName, selectRunFormattingCarrierRepresentations } from "../prosemirror/runFormattingInlineCarriers.js";
23
27
  import { RUN_FORMATTING_MARK_NAMES } from "../prosemirror/runFormattingMarkNames.js";
@@ -25,6 +29,7 @@ import { readAuthoredRunFormatting, reconcileRunFormattingMarks } from "../prose
25
29
  import { paragraphRunStyleContext, paragraphRunStyleContextAt, resolveParagraphBodyRunFormatting } from "../prosemirror/runStyleFormatting.js";
26
30
  import { sectionPropertiesOf } from "../prosemirror/sectionCarrier.js";
27
31
  import { listAttrsFromNumbering, listLevelAttrPatch } from "../prosemirror/styles/resolvedStyleAttrs.js";
32
+ import { removeRowsWithoutCellsAfterBatch } from "../prosemirror/tableGridMutation.js";
28
33
  import { isZeroWidthAnchor } from "../prosemirror/zeroWidthAnchors.js";
29
34
  import { getFolioParaIdFromBlockId } from "../types/block-id.js";
30
35
  import { BatchClaims } from "./batch-claims.js";
@@ -35,8 +40,9 @@ import { hasInlineEmphasis, parseInlineEmphasisRuns, stripInlineEmphasisMarkers
35
40
  import { applyTextChanges, changesFromSegments, keepAtomicSpans, planTextChanges, widenChangesToAtomicSpans } from "./minimal-replacement.js";
36
41
  import { concreteListReference, resolveNewListOperations } from "./newListNumbering.js";
37
42
  import { cutsIntoNoteReference, noteReferenceEditsAllowed, noteReferenceOf, noteReferenceSpansWithin, planChangesAroundNoteReferences, segmentsAroundNoteReferences } from "./note-references.js";
43
+ import { separateRevisionStretches } from "./revisionStretches.js";
38
44
  import { collectNoteReferenceLabels, hashFolioAIBlockStructuralBoundaries, hashFolioAIBlockText, isHiddenTableRow, normalizeFolioAIBlockText } from "./snapshot.js";
39
- import { mergeTableRectangle, mergeTrackedVerticalTableCells, splitTableRectangle, splitTrackedVerticalTableCell } from "./table-cell-mutations.js";
45
+ import { isEmptyTableCell, mergeTableRectangle, mergeTrackedVerticalTableCells, splitTableRectangle, splitTrackedVerticalTableCell } from "./table-cell-mutations.js";
40
46
  import { planTableMutations } from "./table-mutation-plan.js";
41
47
  import { applyTableColumnDeletion, applyTableColumnInsertion, applyTableRowDeletion, applyTableRowInsertion, describeUnplacedCellTexts, findTableColumnInsertion, findTableRowInsertion, getTableColumnCoordinateKey, markTableRowContent, splitCellParagraphTexts } from "./table-row-column-mutations.js";
42
48
  import { findEnclosingTableCell, findEnclosingTableRow, findOutermostTableBoundary, tableRectangleCutsMergedCell } from "./table-targets.js";
@@ -346,13 +352,23 @@ const undeletedContentAtomRanges = (tr, blockFrom) => {
346
352
  });
347
353
  return ranges;
348
354
  };
349
- /** The carrier pPr before this batch changed it, for final-mark rotation. */
350
- const paragraphPropertiesBeforeBatch = (node, batchRevisionIds) => {
355
+ /**
356
+ * The carrier pPr before any pending change to it, for final-mark rotation.
357
+ *
358
+ * A paragraph added after the carrier copies its pending property change, as
359
+ * a break typed at a paragraph's end does: rejecting the addition leaves the
360
+ * added paragraph (its mark is the one that stays), so that change is what
361
+ * gives the carrier's words back the properties they had.
362
+ */
363
+ const paragraphPropertiesBeforePendingChanges = (node) => {
351
364
  const propertyChanges = expectParagraphAttrs(node)._propertyChanges;
352
- if (!Array.isArray(propertyChanges)) return paragraphPropertiesSnapshot(node);
353
- const earliestBatchChange = propertyChanges.find(({ info }) => batchRevisionIds.has(info.id));
354
- if (!earliestBatchChange) return paragraphPropertiesSnapshot(node);
355
- return earliestBatchChange.previousFormatting ?? {};
365
+ const earliest = Array.isArray(propertyChanges) ? propertyChanges[0] : void 0;
366
+ return earliest === void 0 ? paragraphPropertiesSnapshot(node) : earliest.previousFormatting ?? {};
367
+ };
368
+ /** The pPr rejecting every pending change of a paragraph's properties leaves. */
369
+ const paragraphPropertiesOnReject = (node) => {
370
+ const [earliest] = expectParagraphAttrs(node)._propertyChanges ?? [];
371
+ return earliest ? earliest.previousFormatting ?? {} : paragraphPropertiesSnapshot(node);
356
372
  };
357
373
  const resolveFormattingFromStyle = ({ attrs, styleId, styleResolver }) => {
358
374
  const resolved = styleResolver?.resolveParagraphStyle(styleId).paragraphFormatting;
@@ -425,6 +441,7 @@ const preserveRunFormattingAcrossTrackedStyleChange = ({ allocateRevisionInfo, n
425
441
  * the change left alone from one it explicitly cleared when rejecting it.
426
442
  */
427
443
  const applyBlockParagraphProperties = ({ tr, position, node, properties, styleResolver, numbering = null, revisionInfo }) => {
444
+ const storedMarks = tr.storedMarks ?? tr.selection.$from.marks();
428
445
  const attrs = expectParagraphAttrs(node);
429
446
  const resolvedFormattingFromStyle = properties.styleId === void 0 ? resolveFormattingFromStyle({
430
447
  attrs,
@@ -476,45 +493,25 @@ const applyBlockParagraphProperties = ({ tr, position, node, properties, styleRe
476
493
  tr,
477
494
  revisionIds: []
478
495
  };
479
- const styled = bridgeResult.tr.setNodeMarkup(position, void 0, nextAttrs);
480
496
  return {
481
- tr: (attrs.styleId ?? null) === (nextStyleId ?? null) ? styled : rebaseParagraphRuns({
482
- tr: styled,
497
+ tr: rebaseParagraphRuns({
498
+ tr: bridgeResult.tr.setNodeMarkup(position, void 0, nextAttrs),
483
499
  position,
484
500
  previous: node,
485
- styleResolver
501
+ styleResolver,
502
+ storedMarks
486
503
  }),
487
504
  changed: true,
488
505
  revisionId: change?.info.id ?? null,
489
506
  revisionIds: change ? [change.info.id, ...bridgeResult.revisionIds] : []
490
507
  };
491
508
  };
492
- /** Rebuild rendered marks after the paragraph's style cascade changes. */
493
- const rebaseParagraphRuns = ({ tr, position, previous, target, styleResolver }) => {
494
- const paragraph = tr.doc.nodeAt(position);
495
- if (!paragraph) return tr;
496
- const sourceContext = paragraphRunStyleContext(previous, styleResolver);
497
- const targetContext = paragraphRunStyleContext(target ?? paragraph, styleResolver);
498
- const representations = selectRunFormattingCarrierRepresentations({
499
- doc: tr.doc,
500
- from: position + 1,
501
- to: position + paragraph.nodeSize - 1
502
- });
503
- for (const representation of representations) applyMarksToRunFormattingRepresentation({
504
- tr,
505
- representation,
506
- marks: reconcileRunFormattingMarks({
507
- authoredFormatting: readAuthoredRunFormatting({
508
- context: sourceContext,
509
- marks: representation.node.marks,
510
- styleResolver
511
- }),
512
- context: targetContext,
513
- node: representation.node,
514
- styleResolver
515
- })
516
- });
517
- return tr;
509
+ /** No inherited formatting: every run-formatting mark reads as authored. */
510
+ const OPERATION_TEXT_CONTEXT = {
511
+ baseParagraphFormatting: void 0,
512
+ paragraphFormatting: void 0,
513
+ paragraphMarkFormatting: void 0,
514
+ paragraphMarkPrecedesStyle: false
518
515
  };
519
516
  const applyReplaceBlockStyleId = ({ item, tr, styleResolver, revisionInfo }) => {
520
517
  if (item.operation.type !== "replaceBlock" || item.operation.styleId === void 0) return {
@@ -529,6 +526,7 @@ const applyReplaceBlockStyleId = ({ item, tr, styleResolver, revisionInfo }) =>
529
526
  revisionId: null,
530
527
  revisionIds: []
531
528
  };
529
+ const storedMarks = tr.storedMarks ?? tr.selection.$from.marks();
532
530
  const attrs = expectParagraphAttrs(block);
533
531
  const resolvedFormattingFromStyle = resolveFormattingFromStyle({
534
532
  attrs,
@@ -573,13 +571,13 @@ const applyReplaceBlockStyleId = ({ item, tr, styleResolver, revisionInfo }) =>
573
571
  tr,
574
572
  revisionIds: []
575
573
  };
576
- const styled = bridgeResult.tr.setNodeMarkup(blockPosition, void 0, nextAttrs);
577
574
  return {
578
- tr: (attrs.styleId ?? null) === item.operation.styleId ? styled : rebaseParagraphRuns({
579
- tr: styled,
575
+ tr: rebaseParagraphRuns({
576
+ tr: bridgeResult.tr.setNodeMarkup(blockPosition, void 0, nextAttrs),
580
577
  position: blockPosition,
581
578
  previous: block,
582
- styleResolver
579
+ styleResolver,
580
+ storedMarks
583
581
  }),
584
582
  revisionId: change?.info.id ?? null,
585
583
  revisionIds: change ? [change.info.id, ...bridgeResult.revisionIds] : []
@@ -978,6 +976,27 @@ const deletedRowAffectedRanges = ({ doc, deletion, row }) => {
978
976
  }
979
977
  return ranges;
980
978
  };
979
+ /**
980
+ * The blank cells a merge folds into the first cell of its rectangle. Only a
981
+ * blank cell can fold away tracked, so an edit that fills one decides whether
982
+ * the merge applies in one mode and not the other; see `batch-claims.ts`. A
983
+ * cell with content folds directly and is refused tracked whatever the batch
984
+ * does inside it.
985
+ */
986
+ const foldedBlankCellRanges = (doc, { tablePosition, rectangle }) => {
987
+ const table = doc.nodeAt(tablePosition);
988
+ if (table?.type.spec["tableRole"] !== "table") return panic("A resolved cell merge lost its table", { position: tablePosition });
989
+ const map = TableMap.get(table);
990
+ const tableStart = tablePosition + 1;
991
+ const kept = map.map[rectangle.top * map.width + rectangle.left];
992
+ return map.cellsInRect(rectangle).flatMap((relative) => {
993
+ const cell = relative === kept ? null : table.nodeAt(relative);
994
+ return cell && isEmptyTableCell(cell) ? [{
995
+ from: tableStart + relative,
996
+ to: tableStart + relative + cell.nodeSize
997
+ }] : [];
998
+ });
999
+ };
981
1000
  /** What `item` claims of the document its batch resolved against; see `batch-claims.ts`. */
982
1001
  const batchClaimOf = ({ item, doc, producesTrackedChanges }) => {
983
1002
  const block = item.blockFrom;
@@ -1028,9 +1047,7 @@ const batchClaimOf = ({ item, doc, producesTrackedChanges }) => {
1028
1047
  type: "deleteBlock",
1029
1048
  block,
1030
1049
  end: item.blockTo,
1031
- keepsParagraph,
1032
- removesNode: !producesTrackedChanges && !keepsParagraph,
1033
- direct: !producesTrackedChanges
1050
+ keepsParagraph
1034
1051
  };
1035
1052
  }
1036
1053
  case "insertAfterBlock":
@@ -1090,10 +1107,12 @@ const batchClaimOf = ({ item, doc, producesTrackedChanges }) => {
1090
1107
  let axis = "cell";
1091
1108
  if (item.operation.type === "insertTableRow") axis = "row";
1092
1109
  if (item.operation.type === "insertTableColumn") axis = "column";
1093
- return target.type === "none" ? { type: "unclaimed" } : {
1110
+ if (target.type === "none") return { type: "unclaimed" };
1111
+ return {
1094
1112
  type: "tableStructure",
1095
1113
  table: target.tablePosition,
1096
- axis
1114
+ axis,
1115
+ folded: target.type === "mergeCells" ? foldedBlankCellRanges(doc, target) : []
1097
1116
  };
1098
1117
  }
1099
1118
  }
@@ -1251,6 +1270,21 @@ const isBatchableParagraphInsertion = (item, mode) => {
1251
1270
  const isEmptyInsert = insertTexts.length === 1 && insertTexts[0]?.length === 0;
1252
1271
  return !(mode === "tracked-changes" && item.operation.pageBreakBefore === true && isEmptyInsert);
1253
1272
  };
1273
+ /** The comments running across both edges of the table at `tablePosition`: over all of it. */
1274
+ const commentsAcrossTable = (doc, tablePosition) => {
1275
+ const table = doc.nodeAt(tablePosition);
1276
+ if (!table) return [];
1277
+ const atEnd = commentsAcrossBlockBoundary(doc, tablePosition + table.nodeSize);
1278
+ return commentsAcrossBlockBoundary(doc, tablePosition).filter((comment) => comment.isInSet(atEnd));
1279
+ };
1280
+ /** `comments` over the inline content of the node at `position`, which an insertion just placed. */
1281
+ const addCommentsOver = (tr, position, comments) => {
1282
+ const node = tr.doc.nodeAt(position);
1283
+ if (!node || !comments) return tr;
1284
+ let next = tr;
1285
+ for (const comment of comments) next = next.addMark(position + 1, position + node.nodeSize - 1, comment);
1286
+ return next;
1287
+ };
1254
1288
  const insertBlocksInsideComments = ({ tr, at, blocks, carriedComments }) => {
1255
1289
  const spanning = carriedComments ?? commentsAcrossBlockBoundary(tr.doc, at);
1256
1290
  const fragment = Fragment.from(blocks);
@@ -1350,6 +1384,20 @@ const holdsOnlyDeletedContent = (paragraph) => {
1350
1384
  });
1351
1385
  return whollyDeleted;
1352
1386
  };
1387
+ /**
1388
+ * Whether everything after `position` in its container is paragraphs pending
1389
+ * deletion: a join there, once accepted, has no words after it.
1390
+ */
1391
+ const followedOnlyByDeletedParagraphs = (doc, position) => {
1392
+ const at = doc.resolve(position);
1393
+ let deletes = false;
1394
+ for (let index = at.index(); index < at.parent.childCount; index++) {
1395
+ const node = at.parent.child(index);
1396
+ if (node.type.name !== "paragraph" || !holdsOnlyDeletedContent(node)) return false;
1397
+ deletes ||= node.content.size > 0 || isPlainDeletedPPrMark(node.attrs["pPrMark"]);
1398
+ }
1399
+ return deletes;
1400
+ };
1353
1401
  /** A paragraph mark that is a plain pending deletion (not a relocation's source). */
1354
1402
  const isPlainDeletedPPrMark = (value) => typeof value === "object" && value !== null && "kind" in value && value.kind === "del";
1355
1403
  /** An emptied paragraph still carries deleted content, rather than being an original blank. */
@@ -1380,11 +1428,30 @@ const deletedFinalParagraphAfter = (at, paragraphTypeName) => {
1380
1428
  * Where the added break of the final paragraph at `at` rotates to: the free
1381
1429
  * mark `addedBreakCarrierBefore` finds, or else a paragraph the batch deletes
1382
1430
  * whole, whose deleted break the added one cancels (`cancelsDeletedBreak`).
1431
+ * A table whose rows all share one insertion revision from this batch is not
1432
+ * a barrier: rejecting that table removes it before the paragraphs join.
1433
+ * Existing and partially inserted tables remain barriers.
1383
1434
  * The cancelling paragraph must be followed directly by the paragraph its
1384
1435
  * words run into.
1385
1436
  */
1386
1437
  const finalBreakCarrierBefore = (at, paragraphTypeName, batchRevisionIds) => {
1387
- const free = addedBreakCarrierBefore(at, paragraphTypeName);
1438
+ const free = addedBreakCarrierBefore({
1439
+ at,
1440
+ paragraphTypeName,
1441
+ canCrossTable: (table) => {
1442
+ if (table.childCount === 0) return false;
1443
+ let revisionId;
1444
+ for (let index = 0; index < table.childCount; index++) {
1445
+ const row = table.child(index);
1446
+ if (row.type.spec["tableRole"] !== "row") return false;
1447
+ const { trIns, trDel } = expectTableRowAttrs(row);
1448
+ if (!trIns || trDel || !batchRevisionIds.has(trIns.revisionId)) return false;
1449
+ revisionId ??= trIns.revisionId;
1450
+ if (trIns.revisionId !== revisionId) return false;
1451
+ }
1452
+ return true;
1453
+ }
1454
+ });
1388
1455
  if (free) return {
1389
1456
  ...free,
1390
1457
  cancelsDeletedBreak: false
@@ -1409,6 +1476,36 @@ const finalBreakCarrierBefore = (at, paragraphTypeName, batchRevisionIds) => {
1409
1476
  }
1410
1477
  return null;
1411
1478
  };
1479
+ /** The preceding paragraph whose break a final deletion can retire. */
1480
+ const finalParagraphPredecessor = (doc, at) => {
1481
+ const emptied = doc.nodeAt(at);
1482
+ if (emptied?.type.name !== "paragraph") return null;
1483
+ const $emptied = doc.resolve(at);
1484
+ if (!paragraphEndsItsContainer($emptied, emptied.type.name)) return null;
1485
+ let position = at;
1486
+ for (let index = $emptied.index() - 1; index >= 0; index--) {
1487
+ const node = $emptied.parent.child(index);
1488
+ position -= node.nodeSize;
1489
+ if (node.type !== emptied.type) return null;
1490
+ if (isPlainDeletedPPrMark(node.attrs["pPrMark"]) && holdsOnlyDeletedContent(node)) continue;
1491
+ return {
1492
+ node,
1493
+ position
1494
+ };
1495
+ }
1496
+ return null;
1497
+ };
1498
+ /** Pending retirements, read after the merge has mapped any inserted separator. */
1499
+ const pendingRemovedFinalBreakPositions = (tr, deletions) => {
1500
+ const positions = /* @__PURE__ */ new Set();
1501
+ for (const deletion of deletions) {
1502
+ const at = tr.mapping.slice(deletion.mappedFrom).map(deletion.position);
1503
+ const predecessor = finalParagraphPredecessor(tr.doc, at);
1504
+ if (!predecessor || predecessor.node.attrs["pPrMark"] != null) continue;
1505
+ positions.add(predecessor.position);
1506
+ }
1507
+ return positions;
1508
+ };
1412
1509
  /**
1413
1510
  * Finish the tracked deletion of every paragraph a container ends with.
1414
1511
  *
@@ -1432,7 +1529,7 @@ const finalBreakCarrierBefore = (at, paragraphTypeName, batchRevisionIds) => {
1432
1529
  * emptied one is settled only then, since the batch runs backwards and an
1433
1530
  * insertion or deletion before it lands after it.
1434
1531
  */
1435
- const withRetiredFinalParagraphs = ({ tr, deletions, appliedIds, batchRevisionIds, revisionSeed, author, date }) => {
1532
+ const withRetiredFinalParagraphs = ({ tr, deletions, mergedPropertyTargets, appliedIds, batchRevisionIds, revisionSeed, author, date, styleResolver, numbering }) => {
1436
1533
  let nextRevisionId = revisionSeed;
1437
1534
  const addedRevisions = [];
1438
1535
  const retractedRevisionIds = [];
@@ -1444,76 +1541,92 @@ const withRetiredFinalParagraphs = ({ tr, deletions, appliedIds, batchRevisionId
1444
1541
  for (const { deletion: { operationId, revisionExtras }, at } of located) {
1445
1542
  const emptied = tr.doc.nodeAt(at);
1446
1543
  if (emptied?.type.name !== "paragraph") continue;
1447
- const $emptied = tr.doc.resolve(at);
1448
- if (!paragraphEndsItsContainer($emptied, emptied.type.name)) continue;
1449
- let position = at;
1450
- for (let index = $emptied.index() - 1; index >= 0; index--) {
1451
- const previous = $emptied.parent.child(index);
1452
- position -= previous.nodeSize;
1453
- if (previous.type.name !== emptied.type.name) break;
1454
- const mark = previous.attrs["pPrMark"];
1455
- if (mark == null) {
1456
- const revisionId = nextRevisionId++;
1457
- tr.setNodeAttribute(position, "pPrMark", {
1458
- kind: "del",
1459
- info: {
1460
- id: revisionId,
1461
- author,
1462
- date,
1463
- ...revisionExtras
1464
- }
1465
- });
1466
- resolvedOperationIds.add(operationId);
1544
+ const predecessor = finalParagraphPredecessor(tr.doc, at);
1545
+ if (!predecessor) continue;
1546
+ const { node: previous, position } = predecessor;
1547
+ const mark = previous.attrs["pPrMark"];
1548
+ if (mark == null) {
1549
+ const revisionId = nextRevisionId++;
1550
+ tr.setNodeAttribute(position, "pPrMark", {
1551
+ kind: "del",
1552
+ info: {
1553
+ id: revisionId,
1554
+ author,
1555
+ date,
1556
+ ...revisionExtras
1557
+ }
1558
+ });
1559
+ const beforeBatch = resolveParagraphChangeAttrs({
1560
+ node: previous,
1561
+ mode: "reject",
1562
+ boundaryCovered: true,
1563
+ revisionSet: batchRevisionIds,
1564
+ styleResolver,
1565
+ numbering
1566
+ });
1567
+ if (!mergedPropertyTargets.some(({ position: target, mappedFrom }) => {
1568
+ const mapped = tr.mapping.slice(mappedFrom).mapResult(target);
1569
+ return !mapped.deleted && mapped.pos === at;
1570
+ })) carryParagraphProperties({
1571
+ tr,
1572
+ position: at,
1573
+ source: beforeBatch ? previous.type.create(beforeBatch, previous.content, previous.marks) : previous,
1574
+ styleResolver,
1575
+ numbering,
1576
+ revision: {
1577
+ id: revisionId,
1578
+ author,
1579
+ date,
1580
+ ...revisionExtras
1581
+ },
1582
+ keep: propertiesSetInBatch(emptied, batchRevisionIds)
1583
+ });
1584
+ resolvedOperationIds.add(operationId);
1585
+ addedRevisions.push({
1586
+ operationId,
1587
+ revisionId
1588
+ });
1589
+ continue;
1590
+ }
1591
+ if (isInsertedPPrMark(mark)) {
1592
+ const following = tr.doc.nodeAt(position + previous.nodeSize);
1593
+ if (following?.type.name !== emptied.type.name) continue;
1594
+ const previousFormatting = paragraphPropertiesBeforePendingChanges(following);
1595
+ const formattingChanges = JSON.stringify(previousFormatting) !== JSON.stringify(paragraphPropertiesSnapshot(previous));
1596
+ const existing = expectParagraphAttrs(previous)._propertyChanges;
1597
+ if (formattingChanges && hasSerializableParagraphPropertyChange(existing)) continue;
1598
+ joinAtParagraphMark({
1599
+ tr,
1600
+ paragraphPos: position,
1601
+ paragraph: previous,
1602
+ next: following,
1603
+ styleResolver
1604
+ });
1605
+ resolvedOperationIds.add(operationId);
1606
+ const retracted = addedBreakRevisionId(mark);
1607
+ if (retracted !== null) retractedRevisionIds.push(retracted);
1608
+ const revisionId = nextRevisionId;
1609
+ if (carryParagraphProperties({
1610
+ tr,
1611
+ position,
1612
+ source: previous,
1613
+ styleResolver,
1614
+ numbering,
1615
+ revision: {
1616
+ id: revisionId,
1617
+ author,
1618
+ date,
1619
+ ...revisionExtras
1620
+ },
1621
+ keep: propertiesSetInBatch(following, batchRevisionIds)
1622
+ }).tracked) {
1623
+ nextRevisionId++;
1467
1624
  addedRevisions.push({
1468
1625
  operationId,
1469
1626
  revisionId
1470
1627
  });
1471
- break;
1472
- }
1473
- if (isPlainDeletedPPrMark(mark)) {
1474
- if (holdsOnlyDeletedContent(previous)) continue;
1475
- break;
1476
1628
  }
1477
- if (isInsertedPPrMark(mark)) {
1478
- const following = tr.doc.nodeAt(position + previous.nodeSize);
1479
- if (following?.type.name !== emptied.type.name) break;
1480
- const previousFormatting = paragraphPropertiesBeforeBatch(following, batchRevisionIds);
1481
- const formattingChanges = JSON.stringify(previousFormatting) !== JSON.stringify(paragraphPropertiesSnapshot(previous));
1482
- const existing = expectParagraphAttrs(previous)._propertyChanges;
1483
- if (formattingChanges && hasSerializableParagraphPropertyChange(existing)) break;
1484
- joinAtParagraphMark({
1485
- tr,
1486
- paragraphPos: position,
1487
- paragraph: previous,
1488
- next: following
1489
- });
1490
- resolvedOperationIds.add(operationId);
1491
- const retracted = addedBreakRevisionId(mark);
1492
- if (retracted !== null) retractedRevisionIds.push(retracted);
1493
- const joined = tr.doc.nodeAt(position);
1494
- if (formattingChanges && joined) {
1495
- const revisionId = nextRevisionId++;
1496
- tr.setNodeMarkup(position, void 0, {
1497
- ...joined.attrs,
1498
- _propertyChanges: [...Array.isArray(existing) ? existing : [], {
1499
- type: "paragraphPropertyChange",
1500
- info: {
1501
- id: revisionId,
1502
- author,
1503
- date,
1504
- ...revisionExtras
1505
- },
1506
- previousFormatting
1507
- }]
1508
- });
1509
- addedRevisions.push({
1510
- operationId,
1511
- revisionId
1512
- });
1513
- }
1514
- break;
1515
- }
1516
- break;
1629
+ continue;
1517
1630
  }
1518
1631
  }
1519
1632
  return {
@@ -1545,6 +1658,8 @@ const withRetiredFinalParagraphs = ({ tr, deletions, appliedIds, batchRevisionId
1545
1658
  * which paragraph ends a container is only settled when the batch is, and an
1546
1659
  * insertion that looked final was undone by the next operation writing a table
1547
1660
  * after it.
1661
+ * A wholly inserted table from this batch can be crossed to reach the carrier;
1662
+ * its removal on reject exposes that same paragraph boundary again.
1548
1663
  *
1549
1664
  * The paragraph the run was appended after may be one the same batch deletes:
1550
1665
  * the insertion landed first, so the deletion took that paragraph's mark as
@@ -1553,7 +1668,7 @@ const withRetiredFinalParagraphs = ({ tr, deletions, appliedIds, batchRevisionId
1553
1668
  * paragraph's words run into the one after it, which ends with the free mark,
1554
1669
  * and neither break is left to resolve.
1555
1670
  */
1556
- const withRotatedAddedFinalBreaks = ({ tr, batchRevisionIds, revisionSeed, author, date, initials }) => {
1671
+ const withRotatedAddedFinalBreaks = ({ tr, batchRevisionIds, revisionSeed, author, date, initials, styleResolver }) => {
1557
1672
  const supersededDeletedBreaks = [];
1558
1673
  const collectSupersededDeletedBreaks = (parent, contentStart) => {
1559
1674
  let previous = null;
@@ -1634,7 +1749,7 @@ const withRotatedAddedFinalBreaks = ({ tr, batchRevisionIds, revisionSeed, autho
1634
1749
  const revisionId = addedBreakRevisionId(node.attrs["pPrMark"]);
1635
1750
  return revisionId === null || !batchRevisionIds.has(revisionId);
1636
1751
  })) continue;
1637
- const previousFormatting = paragraphPropertiesBeforeBatch(carrier.node, batchRevisionIds);
1752
+ const previousFormatting = paragraphPropertiesBeforePendingChanges(carrier.node);
1638
1753
  next = next.setNodeAttribute(finalPosition, "pPrMark", null);
1639
1754
  let carriedMark = finalMark;
1640
1755
  for (let index = 1; index < path.length; index++) {
@@ -1680,7 +1795,7 @@ const withRotatedAddedFinalBreaks = ({ tr, batchRevisionIds, revisionSeed, autho
1680
1795
  paragraphPos: previous.position,
1681
1796
  paragraph: deleted,
1682
1797
  next: following,
1683
- firstIsGoing: true
1798
+ styleResolver
1684
1799
  });
1685
1800
  retractedRevisionIds.push(ownerRevisionId);
1686
1801
  const deletedBreakId = addedOrDeletedBreakRevisionId(displacedMark);
@@ -1875,7 +1990,7 @@ const buildInsertedParagraphs = ({ item, schema, formattingFromStyle, mode, auth
1875
1990
  ...initials ? { initials } : {}
1876
1991
  };
1877
1992
  const paragraph = item.blockNode.type.create(attrs, content);
1878
- const sourceContext = paragraphRunStyleContext(operation.inheritFormatting === false ? item.blockNode.type.create({}) : item.blockNode, styleResolver);
1993
+ const sourceContext = OPERATION_TEXT_CONTEXT;
1879
1994
  const targetContext = paragraphRunStyleContext(paragraph, styleResolver);
1880
1995
  const reconciled = [];
1881
1996
  paragraph.forEach((child) => {
@@ -2125,6 +2240,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations: reques
2125
2240
  const deletedTextBoxAnchorIds = /* @__PURE__ */ new Set();
2126
2241
  /** Container-ending paragraphs a tracked `deleteBlock` emptied: see `withRetiredFinalParagraphs`. */
2127
2242
  const deletedFinalParagraphs = [];
2243
+ const mergedPropertyTargets = [];
2128
2244
  const deferredNoopFinalDeletions = /* @__PURE__ */ new Set();
2129
2245
  const ownsSharedRevisionIdCursor = revisionIdSeed === void 0 && revisionStamp === void 0;
2130
2246
  let revisionSeed = revisionIdSeed ?? revisionStamp?.idSeed ?? revisionIdCursor;
@@ -2141,6 +2257,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations: reques
2141
2257
  };
2142
2258
  const date = revisionStamp?.date ?? (/* @__PURE__ */ new Date()).toISOString();
2143
2259
  const insertedColumnCounts = /* @__PURE__ */ new Map();
2260
+ const emptiedRows = { pending: false };
2144
2261
  const tableAfterDeletedSplit = /* @__PURE__ */ new Map();
2145
2262
  if (mode === "tracked-changes") {
2146
2263
  const afterTables = /* @__PURE__ */ new Map();
@@ -2671,9 +2788,10 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations: reques
2671
2788
  }
2672
2789
  const revision = structuralRevision;
2673
2790
  const template = tableTemplates?.get(item.operation.id);
2791
+ const mappedInsertion = mapTableRowInsertion(tr, insertion);
2674
2792
  const result = applyTableRowInsertion({
2675
2793
  tr,
2676
- insertion: mapTableRowInsertion(tr, insertion),
2794
+ insertion: mappedInsertion,
2677
2795
  cellTexts: item.operation.cellTexts,
2678
2796
  revision,
2679
2797
  ...template !== void 0 && { template }
@@ -2685,7 +2803,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations: reques
2685
2803
  });
2686
2804
  continue;
2687
2805
  }
2688
- tr = result.transaction;
2806
+ tr = addCommentsOver(result.transaction, mappedInsertion.rowPosition, item.tableRowComments);
2689
2807
  if (result.revisionId !== null) {
2690
2808
  operationRevisionSeed++;
2691
2809
  appliedRevisionIds = [result.revisionId];
@@ -2723,11 +2841,9 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations: reques
2723
2841
  });
2724
2842
  continue;
2725
2843
  }
2726
- tr = result.transaction;
2727
- if (result.revisionId !== null) {
2728
- operationRevisionSeed++;
2729
- appliedRevisionIds = [result.revisionId];
2730
- }
2844
+ tr = addCommentsOver(result.transaction, tr.mapping.map(insertion.tablePosition, 1), item.tableColumnComments);
2845
+ operationRevisionSeed += result.revisionIds.length;
2846
+ appliedRevisionIds = [...result.revisionIds];
2731
2847
  const columnKey = getTableColumnCoordinateKey(insertion);
2732
2848
  insertedColumnCounts.set(columnKey, (insertedColumnCounts.get(columnKey) ?? 0) + 1);
2733
2849
  break;
@@ -2747,7 +2863,8 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations: reques
2747
2863
  tr,
2748
2864
  deletion,
2749
2865
  insertedColumnCount: insertedColumnCounts.get(columnKey) ?? 0,
2750
- revision
2866
+ revision,
2867
+ emptiedRows
2751
2868
  });
2752
2869
  if (result.type === "unsupported") {
2753
2870
  skipped.push({
@@ -2757,10 +2874,8 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations: reques
2757
2874
  continue;
2758
2875
  }
2759
2876
  tr = result.transaction;
2760
- if (result.revisionId !== null) {
2761
- operationRevisionSeed++;
2762
- appliedRevisionIds = [result.revisionId];
2763
- }
2877
+ operationRevisionSeed += result.revisionIds.length;
2878
+ appliedRevisionIds = [...result.revisionIds];
2764
2879
  break;
2765
2880
  }
2766
2881
  case "mergeTableCells": {
@@ -2893,8 +3008,26 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations: reques
2893
3008
  });
2894
3009
  continue;
2895
3010
  }
3011
+ const retracted = tr.doc.nodeAt(item.blockFrom);
2896
3012
  tr = tr.delete(item.blockFrom, item.blockTo);
2897
- if (retraction.clearBreakAt !== null) tr = tr.setNodeAttribute(retraction.clearBreakAt, "pPrMark", null);
3013
+ if (retraction.clearBreakAt !== null) {
3014
+ tr = tr.setNodeAttribute(retraction.clearBreakAt, "pPrMark", null);
3015
+ const revisionIdProperties = operationRevisionSeed;
3016
+ if (retracted && recordReplacedParagraphProperties({
3017
+ tr,
3018
+ position: retraction.clearBreakAt,
3019
+ replaced: retracted,
3020
+ revision: {
3021
+ id: revisionIdProperties,
3022
+ author,
3023
+ date,
3024
+ ...trackedRevisionExtras
3025
+ }
3026
+ })) {
3027
+ operationRevisionSeed++;
3028
+ appliedRevisionIds = [revisionIdProperties];
3029
+ }
3030
+ }
2898
3031
  break;
2899
3032
  }
2900
3033
  if (deletionType) {
@@ -2922,14 +3055,36 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations: reques
2922
3055
  const endsItsContainer = paragraphEndsItsContainer(tr.doc.resolve(markPosition), item.blockNode.type.name);
2923
3056
  const deleted = tr.doc.nodeAt(markPosition);
2924
3057
  const following = deleted && tr.doc.nodeAt(markPosition + deleted.nodeSize);
2925
- if (deleted && following?.type === deleted.type && !isPairedMove(item.operation.moveId) && isInsertedPPrMark(deleted.attrs["pPrMark"])) joinAtParagraphMark({
2926
- tr,
2927
- paragraphPos: markPosition,
2928
- paragraph: deleted,
2929
- next: following,
2930
- firstIsGoing: true
2931
- });
2932
- else if (!endsItsContainer && deleted?.attrs["pPrMark"] == null) {
3058
+ if (deleted && following?.type === deleted.type && !isPairedMove(item.operation.moveId) && isInsertedPPrMark(deleted.attrs["pPrMark"])) {
3059
+ const restored = paragraphPropertiesOnReject(deleted);
3060
+ const followingChanges = expectParagraphAttrs(following)._propertyChanges;
3061
+ const recordsRestored = isInsertedPPrMark(following.attrs["pPrMark"]) && !hasSerializableParagraphPropertyChange(followingChanges) && JSON.stringify(restored) !== JSON.stringify(paragraphPropertiesSnapshot(following));
3062
+ joinAtParagraphMark({
3063
+ tr,
3064
+ paragraphPos: markPosition,
3065
+ paragraph: deleted,
3066
+ next: following,
3067
+ styleResolver: styleResolver ?? null
3068
+ });
3069
+ const joined = tr.doc.nodeAt(markPosition);
3070
+ if (recordsRestored && joined) {
3071
+ const propertyRevisionId = operationRevisionSeed++;
3072
+ tr = tr.setNodeMarkup(markPosition, void 0, {
3073
+ ...joined.attrs,
3074
+ _propertyChanges: [...Array.isArray(followingChanges) ? followingChanges : [], {
3075
+ type: "paragraphPropertyChange",
3076
+ info: {
3077
+ id: propertyRevisionId,
3078
+ author,
3079
+ date,
3080
+ ...trackedRevisionExtras
3081
+ },
3082
+ previousFormatting: restored
3083
+ }]
3084
+ });
3085
+ appliedRevisionIds = [...appliedRevisionIds, propertyRevisionId];
3086
+ }
3087
+ } else if (!endsItsContainer && deleted?.attrs["pPrMark"] == null) {
2933
3088
  const markRevisionId = operationRevisionSeed++;
2934
3089
  tr = tr.setNodeAttribute(markPosition, "pPrMark", {
2935
3090
  kind: isPairedMove(item.operation.moveId) ? "moveFrom" : "del",
@@ -3218,8 +3373,17 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations: reques
3218
3373
  break;
3219
3374
  }
3220
3375
  case "mergeBlockWithNext": {
3221
- const separator = item.operation.separator ?? "";
3376
+ if (mode === "direct" && !canJoin(tr.doc, item.blockTo)) {
3377
+ skipped.push({
3378
+ id: item.operation.id,
3379
+ reason: "unsupportedBlock"
3380
+ });
3381
+ continue;
3382
+ }
3383
+ const separator = followedOnlyByDeletedParagraphs(tr.doc, item.blockTo) ? "" : item.operation.separator ?? "";
3222
3384
  const insertAt = item.blockTo - 1;
3385
+ let mergedPropertiesAt = item.blockFrom;
3386
+ let mergedPropertiesRecorded = false;
3223
3387
  const second = tr.doc.nodeAt(item.blockTo);
3224
3388
  const spanningComments = commentsAcrossBlockBoundary(tr.doc, item.blockTo);
3225
3389
  const withSeparator = (transaction) => {
@@ -3253,6 +3417,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations: reques
3253
3417
  target: paragraph,
3254
3418
  styleResolver
3255
3419
  });
3420
+ tr.setMeta(JOINED_RUNS_RESTYLED_META, true);
3256
3421
  tr = tr.join(item.blockTo + separator.length);
3257
3422
  const secondSection = second ? sectionPropertiesOf(second) : null;
3258
3423
  const joined = tr.doc.nodeAt(item.blockFrom);
@@ -3280,42 +3445,77 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations: reques
3280
3445
  tr,
3281
3446
  paragraphPos: item.blockFrom,
3282
3447
  paragraph,
3283
- next
3448
+ next,
3449
+ styleResolver
3284
3450
  });
3285
- const previousFormatting = paragraphPropertiesSnapshot(next);
3286
- const existing = expectParagraphAttrs(paragraph)._propertyChanges;
3287
- if (holdsOnlyInsertedContent(paragraph) && !hasSerializableParagraphPropertyChange(existing) && JSON.stringify(paragraphPropertiesSnapshot(paragraph)) !== JSON.stringify(previousFormatting)) tr = tr.setNodeAttribute(item.blockFrom, "_propertyChanges", [...Array.isArray(existing) ? existing : [], {
3288
- type: "paragraphPropertyChange",
3451
+ const joinedMark = tr.doc.nodeAt(item.blockFrom)?.attrs["pPrMark"];
3452
+ const removedBreakPositions = pendingRemovedFinalBreakPositions(tr, deletedFinalParagraphs);
3453
+ const carryPos = isDeletedPPrMark(joinedMark) || removedBreakPositions.has(item.blockFrom) ? paragraphLeftAfter({
3454
+ doc: tr.doc,
3455
+ paragraphPos: item.blockFrom,
3456
+ removedBreakPositions
3457
+ }) ?? item.blockFrom : item.blockFrom;
3458
+ const carried = carryParagraphProperties({
3459
+ tr,
3460
+ position: carryPos,
3461
+ source: paragraph,
3462
+ styleResolver,
3463
+ numbering,
3464
+ revision: {
3465
+ id: revisionIdMark,
3466
+ author,
3467
+ date,
3468
+ ...trackedRevisionExtras
3469
+ }
3470
+ });
3471
+ mergedPropertiesAt = carryPos;
3472
+ tr = carried.tr;
3473
+ mergedPropertiesRecorded = carried.tracked;
3474
+ if (!carried.tracked) appliedRevisionIds = appliedRevisionIds.filter((id) => id !== revisionIdMark);
3475
+ } else {
3476
+ tr = tr.setNodeAttribute(item.blockFrom, "pPrMark", {
3477
+ kind: "del",
3289
3478
  info: {
3290
3479
  id: revisionIdMark,
3291
3480
  author,
3292
3481
  date,
3293
3482
  ...trackedRevisionExtras
3294
- },
3295
- previousFormatting
3296
- }]);
3297
- else appliedRevisionIds = appliedRevisionIds.filter((id) => id !== revisionIdMark);
3298
- } else tr = tr.setNodeAttribute(item.blockFrom, "pPrMark", {
3299
- kind: "del",
3300
- info: {
3301
- id: revisionIdMark,
3302
- author,
3303
- date,
3304
- ...trackedRevisionExtras
3305
- }
3306
- });
3483
+ }
3484
+ });
3485
+ const survivorPos = paragraphLeftAfter({
3486
+ doc: tr.doc,
3487
+ paragraphPos: item.blockFrom,
3488
+ removedBreakPositions: pendingRemovedFinalBreakPositions(tr, deletedFinalParagraphs)
3489
+ }) ?? joinPos;
3490
+ const carried = carryParagraphProperties({
3491
+ tr,
3492
+ position: survivorPos,
3493
+ source: paragraph,
3494
+ styleResolver,
3495
+ numbering,
3496
+ revision: {
3497
+ id: revisionIdMark,
3498
+ author,
3499
+ date,
3500
+ ...trackedRevisionExtras
3501
+ }
3502
+ });
3503
+ tr = carried.tr;
3504
+ mergedPropertiesRecorded = carried.tracked;
3505
+ mergedPropertiesAt = survivorPos;
3506
+ }
3307
3507
  }
3308
3508
  if (item.operation.mergedParagraphProperties) {
3309
- const mergedParagraph = tr.doc.nodeAt(item.blockFrom);
3509
+ const mergedParagraph = tr.doc.nodeAt(mergedPropertiesAt);
3310
3510
  if (!mergedParagraph) panic("A resolved merge did not produce a paragraph");
3311
3511
  const appliedProperties = applyBlockParagraphProperties({
3312
3512
  tr,
3313
- position: item.blockFrom,
3513
+ position: mergedPropertiesAt,
3314
3514
  node: mergedParagraph,
3315
3515
  properties: item.operation.mergedParagraphProperties,
3316
3516
  styleResolver,
3317
3517
  numbering,
3318
- ...mode === "direct" ? {} : { revisionInfo: () => ({
3518
+ ...mode === "direct" || mergedPropertiesRecorded ? {} : { revisionInfo: () => ({
3319
3519
  id: operationRevisionSeed++,
3320
3520
  author,
3321
3521
  date,
@@ -3325,6 +3525,10 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations: reques
3325
3525
  tr = appliedProperties.tr;
3326
3526
  if (appliedProperties.revisionIds.length > 0) appliedRevisionIds.push(...appliedProperties.revisionIds);
3327
3527
  }
3528
+ if (mode !== "direct") mergedPropertyTargets.push({
3529
+ position: mergedPropertiesAt,
3530
+ mappedFrom: tr.mapping.maps.length
3531
+ });
3328
3532
  break;
3329
3533
  }
3330
3534
  case "commentOnBlock":
@@ -3373,6 +3577,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations: reques
3373
3577
  ...suggestionId !== null && { suggestionId }
3374
3578
  });
3375
3579
  }
3580
+ removeRowsWithoutCellsAfterBatch(tr, emptiedRows);
3376
3581
  if (tr.docChanged || deferredNoopFinalDeletions.size > 0) {
3377
3582
  const batchRevisionIds = new Set(applied.flatMap(({ revisionIds }) => revisionIds ?? []));
3378
3583
  tr = withoutRevisionsOnZeroWidthAnchors(tr, batchRevisionIds, deletedTextBoxAnchorIds);
@@ -3384,18 +3589,22 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations: reques
3384
3589
  revisionSeed,
3385
3590
  author,
3386
3591
  date,
3387
- initials
3592
+ initials,
3593
+ styleResolver: styleResolver ?? null
3388
3594
  });
3389
3595
  tr = rotated.transaction;
3390
3596
  revisionSeed = rotated.nextRevisionId;
3391
3597
  const retired = withRetiredFinalParagraphs({
3392
3598
  tr,
3393
3599
  deletions: deletedFinalParagraphs,
3600
+ mergedPropertyTargets,
3394
3601
  appliedIds: new Set(applied.map(({ id }) => id)),
3395
3602
  batchRevisionIds,
3396
3603
  revisionSeed,
3397
3604
  author,
3398
- date
3605
+ date,
3606
+ styleResolver,
3607
+ numbering
3399
3608
  });
3400
3609
  revisionSeed = retired.nextRevisionId;
3401
3610
  for (const operationId of deferredNoopFinalDeletions) {
@@ -3447,6 +3656,19 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations: reques
3447
3656
  revisionIds
3448
3657
  };
3449
3658
  }
3659
+ const separated = separateRevisionStretches({
3660
+ tr,
3661
+ revisionSeed
3662
+ });
3663
+ revisionSeed = separated.nextRevisionId;
3664
+ for (const { revisionId, from } of separated.minted) {
3665
+ const receiptIndex = applied.findIndex(({ revisionIds }) => revisionIds?.includes(from));
3666
+ const receipt = applied.at(receiptIndex);
3667
+ if (receiptIndex >= 0 && receipt?.revisionIds) applied[receiptIndex] = {
3668
+ ...receipt,
3669
+ revisionIds: [...receipt.revisionIds, revisionId]
3670
+ };
3671
+ }
3450
3672
  claimSharedRevisionIds(revisionSeed);
3451
3673
  if (revisionStamp) requestDeterministicParaIds(tr, `${revisionStamp.date}:${String(revisionStamp.idSeed)}`);
3452
3674
  if (tr.docChanged) view.dispatch(tr);
@@ -4409,7 +4631,8 @@ const resolveOperationTarget = ({ snapshot, operation, liveBlocks, liveBlocksByP
4409
4631
  blockFrom,
4410
4632
  blockTo,
4411
4633
  blockNode,
4412
- tableRowInsertion: insertion
4634
+ tableRowInsertion: insertion,
4635
+ tableRowComments: commentsAcrossBlockBoundary(doc, insertion.rowPosition)
4413
4636
  }
4414
4637
  };
4415
4638
  }
@@ -4457,7 +4680,8 @@ const resolveOperationTarget = ({ snapshot, operation, liveBlocks, liveBlocksByP
4457
4680
  blockFrom,
4458
4681
  blockTo,
4459
4682
  blockNode,
4460
- tableColumnInsertion
4683
+ tableColumnInsertion,
4684
+ tableColumnComments: commentsAcrossTable(doc, tableColumnInsertion.tablePosition)
4461
4685
  }
4462
4686
  };
4463
4687
  }