@stll/folio-core 0.66.1 → 0.66.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 (80) hide show
  1. package/dist/ai-edits/apply.d.ts +13 -1
  2. package/dist/ai-edits/apply.js +533 -136
  3. package/dist/ai-edits/batch-claims.d.ts +26 -0
  4. package/dist/ai-edits/batch-claims.js +30 -5
  5. package/dist/ai-edits/headless.d.ts +6 -0
  6. package/dist/ai-edits/headless.js +25 -3
  7. package/dist/ai-edits/newListNumbering.d.ts +4 -1
  8. package/dist/ai-edits/newListNumbering.js +4 -2
  9. package/dist/ai-edits/types.d.ts +1 -1
  10. package/dist/document-operations.d.ts +13 -1
  11. package/dist/document-operations.js +11 -4
  12. package/dist/docx/listNumberingInstances.d.ts +6 -4
  13. package/dist/docx/listNumberingInstances.js +12 -11
  14. package/dist/docx/numberingIds.d.ts +10 -0
  15. package/dist/docx/numberingIds.js +5 -1
  16. package/dist/docx/paragraphParser.js +16 -3
  17. package/dist/docx/paragraphProperties.js +2 -1
  18. package/dist/docx/serializer/paragraphSerializer.js +86 -66
  19. package/dist/i18n/messages/ar.gen.js +1 -0
  20. package/dist/i18n/messages/catalogs.gen.d.ts +17 -0
  21. package/dist/i18n/messages/catalogs.gen.js +17 -0
  22. package/dist/i18n/messages/cs.gen.js +1 -0
  23. package/dist/i18n/messages/de.gen.js +1 -0
  24. package/dist/i18n/messages/en.gen.js +1 -0
  25. package/dist/i18n/messages/es.gen.js +1 -0
  26. package/dist/i18n/messages/et.gen.js +1 -0
  27. package/dist/i18n/messages/fr.gen.js +1 -0
  28. package/dist/i18n/messages/he.gen.js +1 -0
  29. package/dist/i18n/messages/hi.gen.js +1 -0
  30. package/dist/i18n/messages/hu.gen.js +1 -0
  31. package/dist/i18n/messages/lt.gen.js +1 -0
  32. package/dist/i18n/messages/lv.gen.js +1 -0
  33. package/dist/i18n/messages/messages.gen.d.ts +1 -0
  34. package/dist/i18n/messages/pl.gen.js +1 -0
  35. package/dist/i18n/messages/pt-BR.gen.js +1 -0
  36. package/dist/i18n/messages/sk.gen.js +1 -0
  37. package/dist/i18n/messages/tr.gen.js +1 -0
  38. package/dist/i18n/messages/zh-CN.gen.js +1 -0
  39. package/dist/internal/wholeStoryRevisionResolution.d.ts +1 -1
  40. package/dist/internal/wholeStoryRevisionResolution.js +24 -9
  41. package/dist/layout-bridge/convert/runMarkFormatting.js +1 -0
  42. package/dist/markdown/renderRuns.js +68 -12
  43. package/dist/prosemirror/commands/comments.js +2 -0
  44. package/dist/prosemirror/commands/resolveParagraphProperties.js +15 -2
  45. package/dist/prosemirror/conversion/fromProseDoc.js +55 -18
  46. package/dist/prosemirror/conversion/hyphenTextCarriers.d.ts +23 -1
  47. package/dist/prosemirror/conversion/hyphenTextCarriers.js +46 -1
  48. package/dist/prosemirror/conversion/toProseDoc.js +15 -9
  49. package/dist/prosemirror/extensions/StarterKit.js +1 -1
  50. package/dist/prosemirror/extensions/features/pasteCleanup.js +6 -0
  51. package/dist/prosemirror/extensions/features/pastedHtmlLists.js +2 -1
  52. package/dist/prosemirror/extensions/markRegistry.d.ts +3 -1
  53. package/dist/prosemirror/extensions/markRegistry.js +3 -0
  54. package/dist/prosemirror/extensions/marks/HyphenElementExtension.d.ts +6 -0
  55. package/dist/prosemirror/extensions/marks/HyphenElementExtension.js +54 -0
  56. package/dist/prosemirror/fieldRepresentation.js +2 -1
  57. package/dist/prosemirror/hyphenElementAttrs.d.ts +10 -0
  58. package/dist/prosemirror/hyphenElementAttrs.js +32 -0
  59. package/dist/prosemirror/keptRangeBoundaries.d.ts +2 -0
  60. package/dist/prosemirror/keptRangeBoundaries.js +1 -1
  61. package/dist/prosemirror/listNumbering.js +3 -2
  62. package/dist/prosemirror/paragraphIndentation.js +2 -1
  63. package/dist/prosemirror/paragraphPropertyCarry.d.ts +1 -14
  64. package/dist/prosemirror/paragraphPropertyCarry.js +4 -28
  65. package/dist/prosemirror/plugins/suggestionMode.js +19 -6
  66. package/dist/prosemirror/replacedAnnotations.d.ts +1 -0
  67. package/dist/prosemirror/replacedAnnotations.js +1 -0
  68. package/dist/prosemirror/sectionCarrier.d.ts +8 -0
  69. package/dist/prosemirror/sectionCarrier.js +29 -1
  70. package/dist/prosemirror/styles/paragraphStyleCascade.d.ts +10 -0
  71. package/dist/prosemirror/styles/paragraphStyleCascade.js +12 -2
  72. package/dist/prosemirror/tableRunIn.d.ts +24 -9
  73. package/dist/prosemirror/tableRunIn.js +107 -21
  74. package/dist/prosemirror/textInput.js +3 -1
  75. package/dist/prosemirror/validation.js +6 -0
  76. package/dist/utils/createDocument.js +11 -8
  77. package/dist/utils/formatToStyle.js +2 -2
  78. package/dist/utils/units.d.ts +5 -0
  79. package/dist/utils/units.js +6 -1
  80. package/package.json +3 -3
@@ -1,3 +1,4 @@
1
+ import { NO_NUMBERING_IDS_IN_USE } from "../docx/numberingIds.js";
1
2
  import { isNumberingReference } from "../docx/numberingReference.js";
2
3
  import { formattingEquals } from "../docx/runConsolidator.js";
3
4
  import { isStyleSourcedParagraphNumbering } from "../internal/paragraphFormattingSerialization.js";
@@ -20,7 +21,7 @@ import { directParagraphAlignment } from "../prosemirror/paragraphAlignment.js";
20
21
  import { styleResolvedParagraphFormatting } from "../prosemirror/paragraphFormattingProvenance.js";
21
22
  import { directParagraphIndentation, paragraphIndentationAttrPatch, paragraphIndentationEqual, paragraphIndentationFromFormatting, withDirectParagraphIndentation } from "../prosemirror/paragraphIndentation.js";
22
23
  import { joinAtParagraphMark } from "../prosemirror/paragraphMarkJoin.js";
23
- import { carriedParagraphProperties, carryParagraphProperties, paragraphLeftAfter, propertiesSetInBatch, recordReplacedParagraphProperties } from "../prosemirror/paragraphPropertyCarry.js";
24
+ import { carryParagraphProperties, paragraphLeftAfter, recordReplacedParagraphProperties } from "../prosemirror/paragraphPropertyCarry.js";
24
25
  import { directParagraphSpacing, paragraphSpacingAttrPatch, paragraphSpacingEqual, paragraphSpacingFromFormatting, withDirectParagraphSpacing } from "../prosemirror/paragraphSpacing.js";
25
26
  import { getDocumentNumbering, getDocumentNumberingInstanceIds } from "../prosemirror/plugins/documentNumbering.js";
26
27
  import { getDocumentStyleDefinitions, getDocumentStyleResolver } from "../prosemirror/plugins/documentStyleState.js";
@@ -34,6 +35,7 @@ import { paragraphRunStyleContext, paragraphRunStyleContextAt, resolveParagraphB
34
35
  import { sectionPropertiesOf } from "../prosemirror/sectionCarrier.js";
35
36
  import { listAttrsFromNumbering, listLevelAttrPatch, listLevelIndentRemovalPatch } from "../prosemirror/styles/resolvedStyleAttrs.js";
36
37
  import { removeRowsWithoutCellsAfterBatch } from "../prosemirror/tableGridMutation.js";
38
+ import { runInCellPending } from "../prosemirror/tableRunIn.js";
37
39
  import { isZeroWidthAnchor } from "../prosemirror/zeroWidthAnchors.js";
38
40
  import { getFolioParaIdFromBlockId } from "../types/block-id.js";
39
41
  import { CANONICAL_GAP } from "../types/canonicalCapabilities.js";
@@ -59,7 +61,7 @@ import { panic } from "better-result";
59
61
  import { Fragment, Mark } from "prosemirror-model";
60
62
  import { TableMap } from "prosemirror-tables";
61
63
  import { canSplit } from "prosemirror-transform";
62
- import { contentRank, groupInsertionRuns, orderBatchOperations } from "@stll/docx-core/ops";
64
+ import { contentRank, deletionRemovesBlock, groupInsertionRuns, orderAddressingBeforeRemoval, orderBatchOperations } from "@stll/docx-core/ops";
63
65
  // src/ai-edits/apply.ts
64
66
  const coarseWordDiff = (before, after) => {
65
67
  if (before === after) return before.length === 0 ? [] : [{
@@ -377,8 +379,8 @@ const withoutRevisionsOnZeroWidthAnchors = (tr, batchRevisionIds, deletedTextBox
377
379
  * text box's anchor is the exception: it stands for the box's drawing run,
378
380
  * which is the paragraph's content, and the box follows it.
379
381
  */
380
- const undeletedContentAtomRanges = (tr, blockFrom) => {
381
- const position = tr.mapping.map(blockFrom);
382
+ const undeletedContentAtomRanges = (tr, blockFrom, readAt) => {
383
+ const position = tr.mapping.slice(readAt).map(blockFrom);
382
384
  const block = tr.doc.nodeAt(position);
383
385
  if (!block?.isTextblock) return [];
384
386
  const ranges = [];
@@ -567,7 +569,7 @@ const applyReplaceBlockStyleId = ({ item, tr, styleResolver, revisionInfo }) =>
567
569
  revisionId: null,
568
570
  revisionIds: []
569
571
  };
570
- const blockPosition = tr.mapping.map(item.blockFrom, -1);
572
+ const blockPosition = sinceRead(tr, item).map(item.blockFrom, -1);
571
573
  const block = tr.doc.nodeAt(blockPosition);
572
574
  if (!block) return {
573
575
  tr,
@@ -1039,6 +1041,28 @@ const foldedBlankCellRanges = (doc, { tablePosition, rectangle }) => {
1039
1041
  }] : [];
1040
1042
  });
1041
1043
  };
1044
+ /**
1045
+ * The paragraphs before `paragraph`, which ends its container, nearest first,
1046
+ * up to the container's start or another block, past those that go whole, as
1047
+ * `finalParagraphPredecessor` looks past them. See the `deleteBlock` claim's
1048
+ * `paragraphsBefore`.
1049
+ */
1050
+ const paragraphsBeforeFinal = ($paragraph, paragraph) => {
1051
+ const paragraphs = [];
1052
+ let position = $paragraph.pos;
1053
+ for (let index = $paragraph.index() - 1; index >= 0; index--) {
1054
+ const node = $paragraph.parent.child(index);
1055
+ position -= node.nodeSize;
1056
+ if (node.type !== paragraph.type) break;
1057
+ const mark = node.attrs["pPrMark"];
1058
+ if (isPlainDeletedPPrMark(mark) && holdsOnlyDeletedContent(node)) continue;
1059
+ paragraphs.push({
1060
+ block: position,
1061
+ keepsBreak: mark == null
1062
+ });
1063
+ }
1064
+ return paragraphs;
1065
+ };
1042
1066
  /** What `item` claims of the document its batch resolved against; see `batch-claims.ts`. */
1043
1067
  const batchClaimOf = ({ item, doc, producesTrackedChanges }) => {
1044
1068
  const block = item.blockFrom;
@@ -1090,7 +1114,9 @@ const batchClaimOf = ({ item, doc, producesTrackedChanges }) => {
1090
1114
  type: "deleteBlock",
1091
1115
  block,
1092
1116
  end: item.blockTo,
1093
- keepsParagraph
1117
+ keepsParagraph,
1118
+ endsContainer: endsItsContainer,
1119
+ paragraphsBefore: producesTrackedChanges && endsItsContainer ? paragraphsBeforeFinal(at, item.blockNode) : []
1094
1120
  };
1095
1121
  }
1096
1122
  case "insertAfterBlock":
@@ -1308,22 +1334,253 @@ const rebuiltBlockContent = ({ doc, blockFrom, blockTo, content }) => annotatedR
1308
1334
  }).fragment;
1309
1335
  const isBatchableParagraphInsertion = (item, mode) => {
1310
1336
  if (item.operation.type !== "insertAfterBlock" && item.operation.type !== "insertBeforeBlock") return false;
1337
+ if (item.joinSide !== void 0) return false;
1311
1338
  if (item.commentText !== void 0) return false;
1312
1339
  const insertTexts = item.insertTexts ?? [""];
1313
1340
  const isEmptyInsert = insertTexts.length === 1 && insertTexts[0]?.length === 0;
1314
1341
  return !(mode === "tracked-changes" && item.operation.pageBreakBefore === true && isEmptyInsert);
1315
1342
  };
1316
- const insertionWithLiveAnchor = (item, tr) => {
1343
+ const insertionWithLiveAnchor = (item, tr, joinedInto) => {
1317
1344
  if (item.operation.type !== "insertAfterBlock" && item.operation.type !== "insertBeforeBlock") panic("Only paragraph insertions have insertion anchors", { operationId: item.operation.id });
1318
- const mapped = tr.mapping.mapResult(item.blockFrom, 1);
1345
+ const mapped = liveBlockResult(tr, item, 1);
1319
1346
  const anchor = mapped.deletedAcross ? null : tr.doc.nodeAt(mapped.pos);
1320
- const paraId = item.blockNode.attrs["paraId"];
1347
+ let paraId = item.blockNode.attrs["paraId"];
1348
+ while (typeof paraId === "string" && joinedInto.has(paraId)) paraId = joinedInto.get(paraId);
1321
1349
  if (!anchor?.isTextblock || anchor.type !== item.blockNode.type || typeof paraId === "string" && anchor.attrs["paraId"] !== paraId) return null;
1322
1350
  return {
1323
1351
  ...item,
1324
1352
  blockNode: anchor
1325
1353
  };
1326
1354
  };
1355
+ /** Operations that place a block in a gap between blocks. */
1356
+ const BLOCK_INSERTION_TYPES = /* @__PURE__ */ new Set([
1357
+ "insertAfterBlock",
1358
+ "insertBeforeBlock",
1359
+ "insertTable",
1360
+ "insertSignatureTable"
1361
+ ]);
1362
+ /** The steps of `tr` since `item` read its positions. */
1363
+ const sinceRead = (tr, item) => tr.mapping.slice(item.readAt ?? 0);
1364
+ /**
1365
+ * Where the block `item` names stands now, mapped from the document the
1366
+ * batch read, so the result also says whether an earlier step deleted it.
1367
+ */
1368
+ const liveBlockResult = (tr, item, assoc) => tr.mapping.mapResult(item.batchBlockFrom ?? item.blockFrom, assoc);
1369
+ /**
1370
+ * `item` with its positions read in the document as it stands now. A batch
1371
+ * runs its operations in the shared order (`orderAddressingBeforeRemoval`),
1372
+ * which can run an edit before one further left; every operation therefore
1373
+ * reads its positions through the steps already taken rather than the
1374
+ * document the batch read. A block's start stays with its content and its
1375
+ * end with what precedes it.
1376
+ */
1377
+ const readNow = (item, tr) => {
1378
+ const steps = tr.mapping.maps.length;
1379
+ if ((item.readAt ?? 0) === steps) return item;
1380
+ const mapping = sinceRead(tr, item);
1381
+ const blockFrom = mapping.map(item.blockFrom, 1);
1382
+ const blockTo = Math.max(blockFrom, mapping.map(item.blockTo, -1));
1383
+ let from;
1384
+ let to;
1385
+ if (item.from === item.to) {
1386
+ const side = item.from === item.blockTo || BLOCK_INSERTION_TYPES.has(item.operation.type) ? -1 : 1;
1387
+ from = mapping.map(item.from, side);
1388
+ to = from;
1389
+ } else {
1390
+ from = mapping.map(item.from, 1);
1391
+ to = Math.max(from, mapping.map(item.to, -1));
1392
+ }
1393
+ return {
1394
+ ...item,
1395
+ from,
1396
+ to,
1397
+ blockFrom,
1398
+ blockTo,
1399
+ readAt: steps
1400
+ };
1401
+ };
1402
+ /**
1403
+ * The paragraph a pending join makes of the one at `paragraphPos`: where its
1404
+ * break is pending removal while its words stay, accepting runs them into the
1405
+ * paragraph the join leaves, which already carries the joined paragraph's
1406
+ * properties (see `carryParagraphProperties`). Null when the paragraph stands
1407
+ * alone, or goes whole as a pending deletion.
1408
+ */
1409
+ const pendingJoinSurvivor = ({ doc, paragraphPos, cleanText, noteReferences }) => {
1410
+ const paragraph = doc.nodeAt(paragraphPos);
1411
+ if (paragraph?.type.name !== "paragraph" || !isDeletedPPrMark(paragraph.attrs["pPrMark"]) || isPendingDeletion(doc, paragraphPos, paragraph, cleanText)) return null;
1412
+ const survivor = paragraphLeftAfter({
1413
+ doc,
1414
+ paragraphPos
1415
+ });
1416
+ const left = survivor === null ? null : doc.nodeAt(survivor);
1417
+ if (survivor === null || left?.type !== paragraph.type) return null;
1418
+ return isDeletedPPrMark(left.attrs["pPrMark"]) && isPendingDeletion(doc, survivor, left, buildCleanBlockText(left, survivor, {
1419
+ fieldResults: "text",
1420
+ noteReferences
1421
+ }).text) ? null : survivor;
1422
+ };
1423
+ /**
1424
+ * The insertion with the paragraph its new paragraphs copy properties from as
1425
+ * its anchor: a paragraph a pending join runs into another reads as that one,
1426
+ * as the joined paragraph does when the merge is applied directly. A
1427
+ * suggestion is read against the document as it stands, where the join has
1428
+ * not run anything together yet, so it copies the paragraph it names.
1429
+ */
1430
+ const insertionFormattingAnchor = ({ item, tr, mode, noteReferences }) => {
1431
+ if (mode === "suggested") return item;
1432
+ const position = sinceRead(tr, item).map(item.blockFrom, 1);
1433
+ const anchor = tr.doc.nodeAt(position);
1434
+ if (anchor?.type !== item.blockNode.type) return item;
1435
+ const survivor = pendingJoinSurvivor({
1436
+ doc: tr.doc,
1437
+ paragraphPos: position,
1438
+ cleanText: buildCleanBlockText(anchor, position, {
1439
+ fieldResults: "text",
1440
+ noteReferences
1441
+ }).text,
1442
+ noteReferences
1443
+ });
1444
+ const source = survivor === null ? null : tr.doc.nodeAt(survivor);
1445
+ return source ? {
1446
+ ...item,
1447
+ blockNode: source
1448
+ } : item;
1449
+ };
1450
+ const keepsWords = ({ doc, paragraphPos, noteReferences }) => {
1451
+ const paragraph = doc.nodeAt(paragraphPos);
1452
+ if (paragraph?.type.name !== "paragraph") return false;
1453
+ const cleanText = buildCleanBlockText(paragraph, paragraphPos, {
1454
+ fieldResults: "text",
1455
+ noteReferences
1456
+ }).text;
1457
+ return cleanText.length > 0 && !isPendingDeletion(doc, paragraphPos, paragraph, cleanText);
1458
+ };
1459
+ /**
1460
+ * Whether accepting runs the paragraph into the one its pending join leaves:
1461
+ * it keeps words, or it is empty. One whose words are all pending deletion
1462
+ * goes whole instead.
1463
+ */
1464
+ const joinsIn = (options) => options.doc.nodeAt(options.paragraphPos)?.content.size === 0 || keepsWords(options);
1465
+ /**
1466
+ * Whether a pending join runs earlier words on to `paragraphPos`, the start of
1467
+ * a paragraph or a place between blocks: the paragraphs before it, back to one
1468
+ * that keeps words, all have their breaks pending removal.
1469
+ */
1470
+ const joinRunsWordsInto = ({ doc, paragraphPos, noteReferences }) => {
1471
+ const $place = doc.resolve(paragraphPos);
1472
+ let position = paragraphPos;
1473
+ for (let index = $place.index() - 1; index >= 0; index--) {
1474
+ const previous = $place.parent.child(index);
1475
+ position -= previous.nodeSize;
1476
+ if (previous.type.name !== "paragraph" || !isDeletedPPrMark(previous.attrs["pPrMark"])) return false;
1477
+ if (keepsWords({
1478
+ doc,
1479
+ paragraphPos: position,
1480
+ noteReferences
1481
+ })) return true;
1482
+ }
1483
+ return false;
1484
+ };
1485
+ /**
1486
+ * Whether a pending join runs earlier words on to `paragraphPos`, right before
1487
+ * a table. A paragraph put there cannot stand apart once accepted: keeping its
1488
+ * own break, it takes the words, and taking the deleted break from them, it
1489
+ * runs into the table's first cell. Where that cell is itself pending nothing
1490
+ * runs into it, so a paragraph right after the words can take their break and
1491
+ * stand; so can one with a paragraph going whole between it and the table.
1492
+ */
1493
+ const joinStopsAtTableAfter = (options) => {
1494
+ if (!joinRunsWordsInto(options)) return false;
1495
+ const $place = options.doc.resolve(options.paragraphPos);
1496
+ for (let index = $place.index(); index < $place.parent.childCount; index++) {
1497
+ const next = $place.parent.child(index);
1498
+ if (next.type.name === "paragraph") return false;
1499
+ if (next.type.spec["tableRole"] === "table") {
1500
+ const before = $place.nodeBefore;
1501
+ return !runInCellPending(next) || before !== null && holdsOnlyDeletedContent(before);
1502
+ }
1503
+ }
1504
+ return false;
1505
+ };
1506
+ /**
1507
+ * Where a block goes before and after the paragraph at `paragraphPos`, read
1508
+ * as the joined paragraph a pending join makes of it. A join runs the words
1509
+ * of each paragraph whose break is pending removal into the paragraph it
1510
+ * leaves, so a block put between them would take those words once accepted,
1511
+ * as the same edits applied directly never do. Where a paragraph runs into
1512
+ * one the join leaves that keeps words of its own, a block after any
1513
+ * paragraph of the join goes after the paragraph it leaves, and a block
1514
+ * before that one goes before the first paragraph the join takes in, empty
1515
+ * ones included. A join into a paragraph with no words left (an emptied
1516
+ * final carrier, or one going whole) runs nothing together, nor does a
1517
+ * paragraph going whole with no join running through it: blocks keep their
1518
+ * places.
1519
+ */
1520
+ const pendingJoinBounds = (options) => {
1521
+ const { doc, paragraphPos, noteReferences } = options;
1522
+ const paragraph = doc.nodeAt(paragraphPos);
1523
+ let after = paragraphPos;
1524
+ if (paragraph?.type.name === "paragraph" && isDeletedPPrMark(paragraph.attrs["pPrMark"]) && (joinsIn(options) || joinRunsWordsInto(options))) {
1525
+ const survivor = paragraphLeftAfter({
1526
+ doc,
1527
+ paragraphPos
1528
+ });
1529
+ if (survivor !== null && keepsWords({
1530
+ doc,
1531
+ paragraphPos: survivor,
1532
+ noteReferences
1533
+ })) after = survivor;
1534
+ }
1535
+ let before = paragraphPos;
1536
+ if (paragraph?.type.name === "paragraph" && keepsWords(options)) {
1537
+ let start = paragraphPos;
1538
+ for (;;) {
1539
+ const previous = doc.resolve(start).nodeBefore;
1540
+ if (previous?.type !== paragraph.type || !isDeletedPPrMark(previous.attrs["pPrMark"])) break;
1541
+ start -= previous.nodeSize;
1542
+ if (joinsIn({
1543
+ doc,
1544
+ paragraphPos: start,
1545
+ noteReferences
1546
+ })) before = start;
1547
+ }
1548
+ }
1549
+ return {
1550
+ before,
1551
+ after: endOfParagraphWithItsTextBoxes(doc, after)
1552
+ };
1553
+ };
1554
+ /** Where a block insertion lands as it runs (see `joinSide`). */
1555
+ const insertionAt = ({ item, tr, noteReferences }) => {
1556
+ if (item.joinSide === void 0) return item.from;
1557
+ const position = sinceRead(tr, item).map(item.blockFrom, 1);
1558
+ if (tr.doc.nodeAt(position)?.type.name !== "paragraph") return item.from;
1559
+ return pendingJoinBounds({
1560
+ doc: tr.doc,
1561
+ paragraphPos: position,
1562
+ noteReferences
1563
+ })[item.joinSide];
1564
+ };
1565
+ /**
1566
+ * Where a block insertion lands as it runs, with the comments running across
1567
+ * that boundary. They are the ones the batch read there, so an edit that runs
1568
+ * first cannot move a comment's end out from around the block; but where an
1569
+ * earlier edit moved the join the block follows, the block lands at another
1570
+ * boundary, and carries the comments across that one.
1571
+ */
1572
+ const insertionPlacement = (options) => {
1573
+ const { item, tr } = options;
1574
+ const at = insertionAt(options);
1575
+ if (item.joinAt === void 0) return {
1576
+ at,
1577
+ comments: item.boundaryComments
1578
+ };
1579
+ return {
1580
+ at,
1581
+ comments: at === tr.mapping.map(item.joinAt, item.joinSide === "after" ? -1 : 1) ? item.boundaryComments : commentsAcrossBlockBoundary(tr.doc, at)
1582
+ };
1583
+ };
1327
1584
  /** The comments running across both edges of the table at `tablePosition`: over all of it. */
1328
1585
  const commentsAcrossTable = (doc, tablePosition) => {
1329
1586
  const table = doc.nodeAt(tablePosition);
@@ -1351,6 +1608,10 @@ const isInsertedPPrMark = (value) => typeof value === "object" && value !== null
1351
1608
  const isSuggestedParagraphInsert = (paragraph) => typeof paragraph.attrs["_suggestedInsert"] === "object" && paragraph.attrs["_suggestedInsert"] !== null;
1352
1609
  /** A paragraph mark that is a pending deletion, or a relocation's source. */
1353
1610
  const isDeletedPPrMark = (value) => typeof value === "object" && value !== null && "kind" in value && (value.kind === "del" || value.kind === "moveFrom");
1611
+ const pendingJoinInsertionRefusal = {
1612
+ reason: "pendingParagraphMarkDeletion",
1613
+ message: "Accepting the pending paragraph mark deletion before this place would run the paragraph together with the words before it or the table after it. Resolve that change first, or insert elsewhere."
1614
+ };
1354
1615
  const pendingParagraphMarkPropertyRefusal = {
1355
1616
  reason: "pendingParagraphMarkDeletion",
1356
1617
  canonicalRefusal: { gap: CANONICAL_GAP.publicPendingParagraphMarkProperties },
@@ -1614,6 +1875,20 @@ const deletedFinalParagraphAfter = (at, paragraphTypeName, insertedBy) => {
1614
1875
  }
1615
1876
  return null;
1616
1877
  };
1878
+ /** Whether every row of `node` is a table row one insertion revision of this batch added. */
1879
+ const isWhollyInsertedTable = (node, batchRevisionIds) => {
1880
+ if (node.type.spec["tableRole"] !== "table" || node.childCount === 0) return false;
1881
+ let revisionId;
1882
+ for (let index = 0; index < node.childCount; index++) {
1883
+ const row = node.child(index);
1884
+ if (row.type.spec["tableRole"] !== "row") return false;
1885
+ const { trIns, trDel } = expectTableRowAttrs(row);
1886
+ if (!trIns || trDel || !batchRevisionIds.has(trIns.revisionId)) return false;
1887
+ revisionId ??= trIns.revisionId;
1888
+ if (trIns.revisionId !== revisionId) return false;
1889
+ }
1890
+ return true;
1891
+ };
1617
1892
  /**
1618
1893
  * Where the added break of the final paragraph at `at` rotates to: the free
1619
1894
  * mark `addedBreakCarrierBefore` finds, or else a paragraph the batch deletes
@@ -1628,19 +1903,7 @@ const finalBreakCarrierBefore = (at, paragraphTypeName, batchRevisionIds) => {
1628
1903
  const free = addedBreakCarrierBefore({
1629
1904
  at,
1630
1905
  paragraphTypeName,
1631
- canCrossTable: (table) => {
1632
- if (table.childCount === 0) return false;
1633
- let revisionId;
1634
- for (let index = 0; index < table.childCount; index++) {
1635
- const row = table.child(index);
1636
- if (row.type.spec["tableRole"] !== "row") return false;
1637
- const { trIns, trDel } = expectTableRowAttrs(row);
1638
- if (!trIns || trDel || !batchRevisionIds.has(trIns.revisionId)) return false;
1639
- revisionId ??= trIns.revisionId;
1640
- if (trIns.revisionId !== revisionId) return false;
1641
- }
1642
- return true;
1643
- }
1906
+ canCrossTable: (table) => isWhollyInsertedTable(table, batchRevisionIds)
1644
1907
  });
1645
1908
  if (free) return {
1646
1909
  ...free,
@@ -1746,22 +2009,6 @@ const collapsedFinalRunSource = (doc, at, batchRevisionIds) => {
1746
2009
  }
1747
2010
  return source;
1748
2011
  };
1749
- /**
1750
- * Whether a sibling between a final carrier's predecessor and the carrier
1751
- * has its break deleted by this batch.
1752
- */
1753
- const crossesBatchDeletion = ({ doc, from, to, batchRevisionIds }) => {
1754
- const $to = doc.resolve(to);
1755
- let position = to;
1756
- for (let index = $to.index() - 1; index >= 0 && position > from; index--) {
1757
- const node = $to.parent.child(index);
1758
- position -= node.nodeSize;
1759
- const mark = node.attrs["pPrMark"];
1760
- const revisionId = isPlainDeletedPPrMark(mark) ? addedOrDeletedBreakRevisionId(mark) : null;
1761
- if (revisionId !== null && batchRevisionIds.has(revisionId)) return true;
1762
- }
1763
- return false;
1764
- };
1765
2012
  const canRetractFinalBreak = ({ predecessor, target }) => {
1766
2013
  if (target.type !== predecessor.type) return false;
1767
2014
  return !(JSON.stringify(paragraphPropertiesBeforePendingChanges(target)) !== JSON.stringify(paragraphPropertiesSnapshot(predecessor)) && hasSerializableParagraphPropertyChange(expectParagraphAttrs(predecessor)._propertyChanges));
@@ -1778,8 +2025,8 @@ const pendingRemovedFinalBreakPositions = (tr, deletions) => {
1778
2025
  return positions;
1779
2026
  };
1780
2027
  /** An unoccupied final carrier takes admissible writes from the finished batch. */
1781
- const finalParagraphCarry = ({ tr, at, target, predecessor, batchRevisionIds, styleResolver, numbering }) => {
1782
- const { node: previous, position } = predecessor;
2028
+ const finalParagraphCarry = ({ target, predecessor, batchRevisionIds, styleResolver, numbering }) => {
2029
+ const { node: previous } = predecessor;
1783
2030
  const beforeBatch = hasSerializableParagraphPropertyChange(expectParagraphAttrs(target)._propertyChanges) ? resolveParagraphChangeAttrs({
1784
2031
  node: previous,
1785
2032
  mode: "reject",
@@ -1788,15 +2035,7 @@ const finalParagraphCarry = ({ tr, at, target, predecessor, batchRevisionIds, st
1788
2035
  styleResolver,
1789
2036
  numbering
1790
2037
  }) : null;
1791
- return {
1792
- source: beforeBatch ? previous.type.create(beforeBatch, previous.content, previous.marks) : previous,
1793
- keep: crossesBatchDeletion({
1794
- doc: tr.doc,
1795
- from: position + previous.nodeSize,
1796
- to: at,
1797
- batchRevisionIds
1798
- }) ? /* @__PURE__ */ new Set() : propertiesSetInBatch(target, batchRevisionIds)
1799
- };
2038
+ return { source: beforeBatch ? previous.type.create(beforeBatch, previous.content, previous.marks) : previous };
1800
2039
  };
1801
2040
  /** Deferred retirement still owns a slot before a later explicit formatting merge. */
1802
2041
  const hasDeferredParagraphPropertyWrite = ({ tr, deletions, targets, mergedPropertyTargets, batchRevisionIds, styleResolver, numbering }) => {
@@ -1819,26 +2058,19 @@ const hasDeferredParagraphPropertyWrite = ({ tr, deletions, targets, mergedPrope
1819
2058
  }))) continue;
1820
2059
  const transfer = mark == null ? finalParagraphCarry({
1821
2060
  tr,
1822
- at,
1823
2061
  target,
1824
2062
  predecessor,
1825
2063
  batchRevisionIds,
1826
2064
  styleResolver,
1827
2065
  numbering
1828
- }) : {
1829
- source: predecessor.node,
1830
- keep: propertiesSetInBatch(writeTarget, batchRevisionIds)
1831
- };
1832
- if (JSON.stringify(paragraphPropertiesSnapshot(writeTarget)) !== JSON.stringify(carriedParagraphProperties({
1833
- ...transfer,
1834
- target: writeTarget
1835
- }))) return true;
2066
+ }) : { source: predecessor.node };
2067
+ if (JSON.stringify(paragraphPropertiesSnapshot(writeTarget)) !== JSON.stringify(paragraphPropertiesSnapshot(transfer.source))) return true;
1836
2068
  }
1837
2069
  return false;
1838
2070
  };
1839
2071
  /** The paragraph a merge leaves owns its properties, even across pending deleted breaks. */
1840
2072
  const paragraphPropertyRevisionTargets = ({ item, tr, deletedFinalParagraphs }) => {
1841
- const position = tr.mapping.map(item.blockFrom, item.operation.type === "replaceBlock" ? -1 : 1);
2073
+ const position = sinceRead(tr, item).map(item.blockFrom, item.operation.type === "replaceBlock" ? -1 : 1);
1842
2074
  const source = tr.doc.nodeAt(position) ?? item.blockNode;
1843
2075
  const targets = source.type.name === "paragraph" ? [source] : [];
1844
2076
  if (item.operation.type !== "mergeBlockWithNext") return targets;
@@ -1915,8 +2147,7 @@ const withRetiredFinalParagraphs = ({ tr, deletions, mergedPropertyTargets, appl
1915
2147
  author,
1916
2148
  date,
1917
2149
  ...revisionExtras
1918
- },
1919
- keep: /* @__PURE__ */ new Set()
2150
+ }
1920
2151
  });
1921
2152
  if (carried.tracked) {
1922
2153
  nextRevisionId++;
@@ -1950,7 +2181,6 @@ const withRetiredFinalParagraphs = ({ tr, deletions, mergedPropertyTargets, appl
1950
2181
  position: at,
1951
2182
  ...finalParagraphCarry({
1952
2183
  tr,
1953
- at,
1954
2184
  target: emptied,
1955
2185
  predecessor,
1956
2186
  batchRevisionIds,
@@ -1995,7 +2225,6 @@ const withRetiredFinalParagraphs = ({ tr, deletions, mergedPropertyTargets, appl
1995
2225
  doc: tr.doc,
1996
2226
  paragraphPos: position
1997
2227
  }) ?? position : position;
1998
- const carryTarget = tr.doc.nodeAt(carryPosition) ?? following;
1999
2228
  const revisionId = nextRevisionId;
2000
2229
  if (carryParagraphProperties({
2001
2230
  tr,
@@ -2008,8 +2237,7 @@ const withRetiredFinalParagraphs = ({ tr, deletions, mergedPropertyTargets, appl
2008
2237
  author,
2009
2238
  date,
2010
2239
  ...revisionExtras
2011
- },
2012
- keep: propertiesSetInBatch(carryTarget, batchRevisionIds)
2240
+ }
2013
2241
  }).tracked) {
2014
2242
  nextRevisionId++;
2015
2243
  addedRevisions.push({
@@ -2069,6 +2297,7 @@ const withRotatedAddedFinalBreaks = ({ tr, batchRevisionIds, revisionSeed, autho
2069
2297
  let carriedDeletedFormatting = null;
2070
2298
  parent.forEach((node, offset) => {
2071
2299
  const position = contentStart + offset;
2300
+ if (isWhollyInsertedTable(node, batchRevisionIds)) return;
2072
2301
  if (node.type.name !== "paragraph") {
2073
2302
  previous = null;
2074
2303
  carriedDeletedMark = null;
@@ -2215,25 +2444,38 @@ const withRotatedAddedFinalBreaks = ({ tr, batchRevisionIds, revisionSeed, autho
2215
2444
  const liveCurrent = next.doc.nodeAt(current.position);
2216
2445
  if (!liveCurrent || liveCurrent.type.name !== final.type.name) panic("A final-mark rotation lost its paragraph", { position: current.position });
2217
2446
  const existing = expectParagraphAttrs(liveCurrent)._propertyChanges;
2218
- if (hasSerializableParagraphPropertyChange(existing)) panic("A final-mark rotation cannot append a second w:pPrChange", { position: current.position });
2219
- const revisionId = nextRevisionId++;
2220
- next = next.setNodeMarkup(current.position, void 0, {
2221
- ...liveCurrent.attrs,
2222
- _propertyChanges: [...Array.isArray(existing) ? existing : [], {
2223
- type: "paragraphPropertyChange",
2224
- info: {
2225
- id: revisionId,
2226
- author,
2227
- date,
2228
- ...initials ? { initials } : {}
2229
- },
2447
+ if (hasSerializableParagraphPropertyChange(existing)) {
2448
+ const serialized = (existing ?? []).filter(({ info }) => info.provenance !== "suggested");
2449
+ if (!serialized.every(({ info }) => batchRevisionIds.has(info.id))) panic("A final-mark rotation met a w:pPrChange from outside", { position: current.position });
2450
+ const restated = [];
2451
+ for (const change of existing ?? []) restated.push(serialized.includes(change) ? {
2452
+ ...change,
2230
2453
  previousFormatting
2231
- }]
2232
- });
2233
- synthesizedRevisions.push({
2234
- ownerRevisionId,
2235
- revisionId
2236
- });
2454
+ } : change);
2455
+ next = next.setNodeMarkup(current.position, void 0, {
2456
+ ...liveCurrent.attrs,
2457
+ _propertyChanges: restated
2458
+ });
2459
+ } else {
2460
+ const revisionId = nextRevisionId++;
2461
+ next = next.setNodeMarkup(current.position, void 0, {
2462
+ ...liveCurrent.attrs,
2463
+ _propertyChanges: [...Array.isArray(existing) ? existing : [], {
2464
+ type: "paragraphPropertyChange",
2465
+ info: {
2466
+ id: revisionId,
2467
+ author,
2468
+ date,
2469
+ ...initials ? { initials } : {}
2470
+ },
2471
+ previousFormatting
2472
+ }]
2473
+ });
2474
+ synthesizedRevisions.push({
2475
+ ownerRevisionId,
2476
+ revisionId
2477
+ });
2478
+ }
2237
2479
  }
2238
2480
  if (cancels) {
2239
2481
  const deleted = next.doc.nodeAt(previous.position);
@@ -2490,7 +2732,7 @@ const buildInsertedParagraphs = ({ item, schema, formattingFromStyle, mode, auth
2490
2732
  nextRevisionId
2491
2733
  };
2492
2734
  };
2493
- const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations: requestedOperations, mode = "tracked-changes", author = "AI", initials, createCommentId, revisionStamp, revisionIdSeed, wordDiff, wordDiffMode = "bounded", tableTemplates, replacementBackground = "clear", undefinedReferences }) => {
2735
+ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations: requestedOperations, mode = "tracked-changes", author = "AI", initials, createCommentId, revisionStamp, revisionIdSeed, wordDiff, wordDiffMode = "bounded", tableTemplates, replacementBackground = "clear", undefinedReferences, numberingIdsElsewhere }) => {
2494
2736
  seedRevisionIdsFromDoc(view.state.doc);
2495
2737
  const applied = [];
2496
2738
  const skipped = [];
@@ -2571,7 +2813,8 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations: reques
2571
2813
  liveBlocks,
2572
2814
  liveBlocksByParaId,
2573
2815
  noteReferences,
2574
- doc: view.state.doc
2816
+ doc: view.state.doc,
2817
+ mode
2575
2818
  });
2576
2819
  if (resolution.type === "skip") {
2577
2820
  skipped.push({
@@ -2652,7 +2895,8 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations: reques
2652
2895
  "splitTableCell"
2653
2896
  ].includes(operation.type) ? removedBlocksBy.get(targetBlockId) : void 0;
2654
2897
  const continuesMergeChain = operation.type === "mergeBlockWithNext" && removedBy?.type === "mergeBlockWithNext";
2655
- if (removedBy !== void 0 && !continuesMergeChain) {
2898
+ const formatsBeforeRemoval = operation.type === "setBlockParagraphProperties" && (removedBy?.type === "mergeBlockWithNext" || removedBy?.type === "deleteBlock");
2899
+ if (removedBy !== void 0 && !continuesMergeChain && !formatsBeforeRemoval) {
2656
2900
  skipped.push({
2657
2901
  id: operation.id,
2658
2902
  reason: "overlappingOperation",
@@ -2692,6 +2936,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations: reques
2692
2936
  });
2693
2937
  resolved.push({
2694
2938
  ...resolution.operation,
2939
+ batchBlockFrom: resolution.operation.blockFrom,
2695
2940
  originalIndex: index,
2696
2941
  ...commentText !== void 0 && { commentText }
2697
2942
  });
@@ -2813,18 +3058,71 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations: reques
2813
3058
  inputIndex: item.originalIndex
2814
3059
  };
2815
3060
  };
2816
- const executionOrder = orderBatchOperations(executableResolved, batchAddress);
3061
+ const batchDeletions = new Set(executableResolved.flatMap((item) => item.operation.type === "deleteBlock" ? [item.blockFrom] : []));
3062
+ const blockAccess = (item) => {
3063
+ const doc = view.state.doc;
3064
+ if (item.operation.type === "mergeBlockWithNext") {
3065
+ const next = paragraphJoinAfter(doc, item.blockTo);
3066
+ return {
3067
+ addresses: [item.blockFrom],
3068
+ removes: next ? [next.nextPosition] : []
3069
+ };
3070
+ }
3071
+ if (item.operation.type === "deleteTableRow" || item.operation.type === "deleteTable") return {
3072
+ addresses: [],
3073
+ removes: [item.blockFrom]
3074
+ };
3075
+ if (item.tableColumnInsertion ?? item.tableColumnDeletion ?? item.tableCellMerge ?? item.tableCellSplit) return {
3076
+ addresses: [],
3077
+ removes: []
3078
+ };
3079
+ if (item.operation.type !== "deleteBlock") return {
3080
+ addresses: [item.blockFrom],
3081
+ removes: []
3082
+ };
3083
+ const at = doc.resolve(item.blockFrom);
3084
+ let hasKeptParagraphBefore = false;
3085
+ let position = at.pos;
3086
+ for (let index = at.index() - 1; index >= 0; index -= 1) {
3087
+ const previous = at.parent.child(index);
3088
+ position -= previous.nodeSize;
3089
+ if (previous.type !== item.blockNode.type) break;
3090
+ if (!batchDeletions.has(position)) {
3091
+ hasKeptParagraphBefore = true;
3092
+ break;
3093
+ }
3094
+ }
3095
+ const appended = executableResolved.some((other) => other.blockFrom === item.blockFrom && (other.operation.type === "insertAfterBlock" || other.operation.type === "insertTable" && other.operation.position !== "before"));
3096
+ return {
3097
+ addresses: [],
3098
+ removes: deletionRemovesBlock({
3099
+ endsContainer: !appended && paragraphEndsItsContainer(at, item.blockNode.type.name),
3100
+ hasKeptParagraphBefore
3101
+ }) ? [item.blockFrom] : []
3102
+ };
3103
+ };
3104
+ const groupRuns = (items) => groupInsertionRuns(items, (candidate) => isBatchableParagraphInsertion(candidate, mode) ? candidate.from : null, (candidate) => candidate.operation.type === "setBlockParagraphProperties");
3105
+ const executionOrder = orderAddressingBeforeRemoval(groupRuns(orderBatchOperations(executableResolved, batchAddress)), (run) => {
3106
+ const access = run.map(blockAccess);
3107
+ return {
3108
+ addresses: access.flatMap(({ addresses }) => addresses),
3109
+ removes: access.flatMap(({ removes }) => removes)
3110
+ };
3111
+ }).flat();
2817
3112
  const allocateNewLists = ({ items }) => {
2818
- const allocated = resolveNewListOperations(items.map(({ operation }) => operation), numbering);
3113
+ const allocated = resolveNewListOperations(items.map(({ operation }) => operation), numbering, numberingIdsElsewhere);
2819
3114
  numbering = allocated.numbering;
2820
3115
  for (const [index, candidate] of items.entries()) candidate.operation = allocated.operations[index] ?? panic("A will-apply operation lost its new-list allocation", { id: candidate.operation.id });
2821
3116
  };
2822
- const executionGroups = groupInsertionRuns(executionOrder, (candidate) => isBatchableParagraphInsertion(candidate, mode) ? candidate.from : null, (candidate) => candidate.operation.type === "setBlockParagraphProperties");
3117
+ /** Paragraphs a retracted break of this batch joined, to the one it left. */
3118
+ const joinedInto = /* @__PURE__ */ new Map();
3119
+ const executionGroups = groupRuns(executionOrder);
2823
3120
  for (const executionGroup of executionGroups) {
2824
3121
  const insertionState = tr;
2825
- const insertionRun = executionGroup.flatMap((candidate) => {
3122
+ const insertionRun = executionGroup.flatMap((resolvedCandidate) => {
3123
+ const candidate = readNow(resolvedCandidate, insertionState);
2826
3124
  if (candidate.operation.type !== "insertAfterBlock" && candidate.operation.type !== "insertBeforeBlock") return [candidate];
2827
- const liveCandidate = insertionWithLiveAnchor(candidate, insertionState);
3125
+ const liveCandidate = insertionWithLiveAnchor(candidate, insertionState, joinedInto);
2828
3126
  if (!liveCandidate) {
2829
3127
  if (!isSuggested) return [candidate];
2830
3128
  if (candidate.operation.moveId !== void 0) refusedMoveIds.add(candidate.operation.moveId);
@@ -2835,7 +3133,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations: reques
2835
3133
  return [];
2836
3134
  }
2837
3135
  if (isSuggested) {
2838
- const blockFrom = insertionState.mapping.map(candidate.blockFrom, 1);
3136
+ const blockFrom = sinceRead(insertionState, candidate).map(candidate.blockFrom, 1);
2839
3137
  if (isPendingDeletion(insertionState.doc, blockFrom, liveCandidate.blockNode, buildCleanBlockText(liveCandidate.blockNode, blockFrom, {
2840
3138
  fieldResults: "text",
2841
3139
  noteReferences
@@ -2887,10 +3185,16 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations: reques
2887
3185
  const runApplied = [];
2888
3186
  for (const insertion of insertionRun) {
2889
3187
  const insertionSuggestionId = isSuggested ? insertion.operation.suggestionId ?? insertion.operation.id : null;
2890
- const built = buildInsertedParagraphs({
3188
+ const formatted = insertionFormattingAnchor({
2891
3189
  item: insertion,
3190
+ tr,
3191
+ mode,
3192
+ noteReferences
3193
+ });
3194
+ const built = buildInsertedParagraphs({
3195
+ item: formatted,
2892
3196
  schema: view.state.schema,
2893
- formattingFromStyle: formattingFromStyleForInsertion(insertion),
3197
+ formattingFromStyle: formattingFromStyleForInsertion(formatted),
2894
3198
  mode,
2895
3199
  author,
2896
3200
  date,
@@ -2918,7 +3222,11 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations: reques
2918
3222
  for (const group of nodeGroups.toReversed()) for (const node of group) nodes.push(node);
2919
3223
  tr = insertBlocksInsideComments({
2920
3224
  tr,
2921
- at: item.from,
3225
+ at: insertionAt({
3226
+ item,
3227
+ tr,
3228
+ noteReferences
3229
+ }),
2922
3230
  blocks: nodes,
2923
3231
  ...item.boundaryComments !== void 0 && { carriedComments: item.boundaryComments }
2924
3232
  });
@@ -3115,7 +3423,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations: reques
3115
3423
  case "replaceBlock": {
3116
3424
  if (!isResolvedReplaceBlockOperation(item)) panic("Resolved replaceBlock operation lost its impact discriminator");
3117
3425
  const { changesStyle, changesText } = REPLACE_BLOCK_IMPACT[item.replaceBlockImpact];
3118
- if (changesStyle && isDeletedPPrMark(tr.doc.nodeAt(tr.mapping.map(item.blockFrom, -1))?.attrs["pPrMark"])) {
3426
+ if (changesStyle && isDeletedPPrMark(tr.doc.nodeAt(sinceRead(tr, item).map(item.blockFrom, -1))?.attrs["pPrMark"])) {
3119
3427
  skipped.push({
3120
3428
  id: item.operation.id,
3121
3429
  ...pendingParagraphMarkPropertyRefusal
@@ -3286,10 +3594,16 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations: reques
3286
3594
  items: [item]
3287
3595
  });
3288
3596
  assertNewListsAllocated(item.operation);
3289
- const built = buildInsertedParagraphs({
3597
+ const formatted = insertionFormattingAnchor({
3290
3598
  item,
3599
+ tr,
3600
+ mode,
3601
+ noteReferences
3602
+ });
3603
+ const built = buildInsertedParagraphs({
3604
+ item: formatted,
3291
3605
  schema: view.state.schema,
3292
- formattingFromStyle: formattingFromStyleForInsertion(item),
3606
+ formattingFromStyle: formattingFromStyleForInsertion(formatted),
3293
3607
  mode,
3294
3608
  author,
3295
3609
  date,
@@ -3303,11 +3617,16 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations: reques
3303
3617
  });
3304
3618
  operationRevisionSeed = built.nextRevisionId;
3305
3619
  if (built.revisionIds.length > 0) appliedRevisionIds = built.revisionIds;
3620
+ const placement = insertionPlacement({
3621
+ item,
3622
+ tr,
3623
+ noteReferences
3624
+ });
3306
3625
  tr = insertBlocksInsideComments({
3307
3626
  tr,
3308
- at: item.from,
3627
+ at: placement.at,
3309
3628
  blocks: built.nodes,
3310
- ...item.boundaryComments !== void 0 && { carriedComments: item.boundaryComments }
3629
+ ...placement.comments !== void 0 && { carriedComments: placement.comments }
3311
3630
  });
3312
3631
  break;
3313
3632
  }
@@ -3345,7 +3664,11 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations: reques
3345
3664
  }, signatureTable.content, signatureTable.marks);
3346
3665
  appliedRevisionIds = [revisionId];
3347
3666
  }
3348
- tr = tr.insert(item.from, node);
3667
+ tr = tr.insert(insertionAt({
3668
+ item,
3669
+ tr,
3670
+ noteReferences
3671
+ }), node);
3349
3672
  break;
3350
3673
  }
3351
3674
  case "insertTableRow": {
@@ -3617,7 +3940,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations: reques
3617
3940
  to: item.to,
3618
3941
  mark: deletionMark
3619
3942
  });
3620
- const atomRanges = undeletedContentAtomRanges(tr, item.blockFrom);
3943
+ const atomRanges = undeletedContentAtomRanges(tr, item.blockFrom, item.readAt ?? 0);
3621
3944
  for (const { from, to } of atomRanges) addTrackedDeletionMark({
3622
3945
  insertionPolicy: "preserve-pending",
3623
3946
  tr,
@@ -3625,7 +3948,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations: reques
3625
3948
  to,
3626
3949
  mark: deletionMark
3627
3950
  });
3628
- for (const anchorId of textBoxAnchorIdsBetween(tr.doc, tr.mapping.map(item.blockFrom), tr.mapping.map(item.blockTo))) deletedTextBoxAnchorIds.add(anchorId);
3951
+ for (const anchorId of textBoxAnchorIdsBetween(tr.doc, sinceRead(tr, item).map(item.blockFrom), sinceRead(tr, item).map(item.blockTo))) deletedTextBoxAnchorIds.add(anchorId);
3629
3952
  encloseWholeControls({
3630
3953
  tr,
3631
3954
  from: item.blockFrom,
@@ -3634,7 +3957,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations: reques
3634
3957
  });
3635
3958
  const inlineRevisionApplied = item.from < item.to || atomRanges.length > 0;
3636
3959
  appliedRevisionIds = inlineRevisionApplied ? [revisionId] : [];
3637
- let markPosition = tr.mapping.map(item.blockFrom);
3960
+ let markPosition = sinceRead(tr, item).map(item.blockFrom);
3638
3961
  const markOwner = tr.doc.nodeAt(markPosition);
3639
3962
  const across = markOwner && !isPairedMove(item.operation.moveId) && addedBreakRevisionId(markOwner.attrs["pPrMark"]) !== null ? paragraphAcrossVanishingTables(tr.doc, markPosition) : null;
3640
3963
  if (across !== null) markPosition = moveParagraphPastTables(tr, markPosition, across);
@@ -3729,10 +4052,15 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations: reques
3729
4052
  continue;
3730
4053
  }
3731
4054
  if (producesTrackedChanges) appliedRevisionIds = [operationRevisionSeed++];
3732
- const carriedComments = tableAfterDeletedSplit.get(item.blockFrom);
3733
- const usesRetractedSplit = carriedComments !== void 0 && item.from === item.blockTo;
3734
- let at = usesRetractedSplit ? tr.mapping.map(item.blockFrom, -1) : item.from;
3735
- let insertionComments = carriedComments ?? item.boundaryComments;
4055
+ const carriedComments = tableAfterDeletedSplit.get(item.batchBlockFrom ?? item.blockFrom);
4056
+ const usesRetractedSplit = carriedComments !== void 0 && item.from === item.blockTo && liveBlockResult(tr, item, 1).deletedAcross;
4057
+ const placement = insertionPlacement({
4058
+ item,
4059
+ tr,
4060
+ noteReferences
4061
+ });
4062
+ let at = usesRetractedSplit ? sinceRead(tr, item).map(item.blockFrom, -1) : placement.at;
4063
+ let insertionComments = carriedComments ?? placement.comments;
3736
4064
  if (!usesRetractedSplit) {
3737
4065
  const boundary = tr.doc.resolve(at);
3738
4066
  const before = boundary.nodeBefore;
@@ -3857,7 +4185,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations: reques
3857
4185
  break;
3858
4186
  }
3859
4187
  case "setBlockParagraphProperties": {
3860
- const mapped = tr.mapping.mapResult(item.blockFrom);
4188
+ const mapped = liveBlockResult(tr, item);
3861
4189
  const liveBlock = mapped.deletedAcross ? null : tr.doc.nodeAt(mapped.pos);
3862
4190
  const paraId = item.blockNode.attrs["paraId"];
3863
4191
  if (!liveBlock || liveBlock.type !== item.blockNode.type || typeof paraId === "string" && liveBlock.attrs["paraId"] !== paraId) {
@@ -3954,6 +4282,19 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations: reques
3954
4282
  kind: "ins",
3955
4283
  info
3956
4284
  });
4285
+ const second = tr.doc.nodeAt(item.from + 1);
4286
+ const survivor = mode === "tracked-changes" && isDeletedPPrMark(second?.attrs["pPrMark"]) ? paragraphLeftAfter({
4287
+ doc: tr.doc,
4288
+ paragraphPos: item.from + 1
4289
+ }) : null;
4290
+ const source = survivor === null ? null : tr.doc.nodeAt(survivor);
4291
+ if (source) tr = carryParagraphProperties({
4292
+ tr,
4293
+ position: item.blockFrom,
4294
+ source,
4295
+ styleResolver,
4296
+ numbering
4297
+ }).tr;
3957
4298
  }
3958
4299
  const allocateSplitParagraphPropertyRevisionInfo = () => ({
3959
4300
  id: operationRevisionSeed++,
@@ -3987,7 +4328,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations: reques
3987
4328
  break;
3988
4329
  }
3989
4330
  case "mergeBlockWithNext": {
3990
- const mapped = tr.mapping.mapResult(item.blockFrom, 1);
4331
+ const mapped = liveBlockResult(tr, item, 1);
3991
4332
  const live = mapped.deletedAcross ? null : tr.doc.nodeAt(mapped.pos);
3992
4333
  const paraId = item.blockNode.attrs["paraId"];
3993
4334
  if (!live || live.type !== item.blockNode.type || typeof paraId === "string" && live.attrs["paraId"] !== paraId) {
@@ -4102,7 +4443,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations: reques
4102
4443
  const nextPosition = join.nextPosition + separator.length;
4103
4444
  const next = tr.doc.nodeAt(nextPosition);
4104
4445
  if (!paragraph || !next) panic("A resolved merge lost its paragraphs");
4105
- if (isInsertedPPrMark(paragraph.attrs["pPrMark"])) {
4446
+ if (addedBreakRevisionId(paragraph.attrs["pPrMark"]) !== null) {
4106
4447
  joinAtParagraphMark({
4107
4448
  tr,
4108
4449
  paragraphPos: blockFrom,
@@ -4110,6 +4451,9 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations: reques
4110
4451
  next,
4111
4452
  styleResolver
4112
4453
  });
4454
+ const firstId = paragraph.attrs["paraId"];
4455
+ const nextId = next.attrs["paraId"];
4456
+ if (typeof firstId === "string" && typeof nextId === "string") joinedInto.set(firstId, nextId);
4113
4457
  const joinedMark = tr.doc.nodeAt(blockFrom)?.attrs["pPrMark"];
4114
4458
  const removedBreakPositions = pendingRemovedFinalBreakPositions(tr, deletedFinalParagraphs);
4115
4459
  const carryPos = isDeletedPPrMark(joinedMark) || removedBreakPositions.has(blockFrom) ? paragraphLeftAfter({
@@ -4348,8 +4692,16 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations: reques
4348
4692
  nextRevisionId: revisionSeed
4349
4693
  };
4350
4694
  };
4351
- const applyFolioAIEditOperations = (options) => applyFolioAIEditOperationsInternal(options);
4352
- const previewFolioAIEditOperations = (options) => previewFolioAIEditOperationsWithResult(options).outcome;
4695
+ const applyFolioAIEditOperations = (options) => applyFolioAIEditOperationsInternal({
4696
+ ...options,
4697
+ numberingIdsElsewhere: NO_NUMBERING_IDS_IN_USE
4698
+ });
4699
+ /** {@link applyFolioAIEditOperations} for a caller holding every story. Internal. */
4700
+ const applyFolioAIEditOperationsInPackage = (options) => applyFolioAIEditOperationsInternal(options);
4701
+ const previewFolioAIEditOperations = (options) => previewFolioAIEditOperationsWithResult({
4702
+ ...options,
4703
+ numberingIdsElsewhere: NO_NUMBERING_IDS_IN_USE
4704
+ }).outcome;
4353
4705
  /**
4354
4706
  * A preview together with the document it would leave and the provisional
4355
4707
  * comment ids it placed, for a caller that checks the result before reporting
@@ -5228,7 +5580,7 @@ const refuseNoteReferenceEdit = (item, noteReferences) => {
5228
5580
  reason: "protectedReference"
5229
5581
  } : null;
5230
5582
  };
5231
- const resolveOperationTarget = ({ snapshot, operation, liveBlocks, liveBlocksByParaId, noteReferences, doc }) => {
5583
+ const resolveOperationTarget = ({ snapshot, operation, liveBlocks, liveBlocksByParaId, noteReferences, doc, mode }) => {
5232
5584
  const blockId = operation.type === "replaceRange" || operation.type === "commentOnRange" || operation.type === "formatRange" ? operation.range.blockId : operation.blockId;
5233
5585
  const primaryBlock = resolveStableBlock({
5234
5586
  snapshot,
@@ -5243,6 +5595,28 @@ const resolveOperationTarget = ({ snapshot, operation, liveBlocks, liveBlocksByP
5243
5595
  type: "skip",
5244
5596
  reason: "preconditionFailed"
5245
5597
  };
5598
+ const joinBounds = () => mode === "suggested" ? {
5599
+ before: blockFrom,
5600
+ after: endOfParagraphWithItsTextBoxes(doc, blockFrom)
5601
+ } : pendingJoinBounds({
5602
+ doc,
5603
+ paragraphPos: blockFrom,
5604
+ noteReferences
5605
+ });
5606
+ const beside = (side) => {
5607
+ const plain = side === "after" ? endOfParagraphWithItsTextBoxes(doc, blockFrom) : blockFrom;
5608
+ const at = joinBounds()[side];
5609
+ const from = side === "after" ? plain : at;
5610
+ return at === plain ? {
5611
+ from,
5612
+ at
5613
+ } : {
5614
+ from,
5615
+ at,
5616
+ joinSide: side,
5617
+ joinAt: at
5618
+ };
5619
+ };
5246
5620
  if (operation.type === "replaceRange" || operation.type === "commentOnRange" || operation.type === "formatRange") {
5247
5621
  const { startOffset, endOffset, selectedTextHash } = operation.range;
5248
5622
  if (cleanBlock.offsets[startOffset] === void 0 || cleanBlock.offsets[endOffset] === void 0) return {
@@ -5291,11 +5665,20 @@ const resolveOperationTarget = ({ snapshot, operation, liveBlocks, liveBlocksByP
5291
5665
  }
5292
5666
  if (operation.type === "insertAfterBlock" || operation.type === "insertBeforeBlock") {
5293
5667
  const tableBoundary = findOutermostTableBoundary(doc, blockFrom);
5294
- const isInsertAfter = operation.type === "insertAfterBlock";
5295
- let insertFrom;
5296
- if (tableBoundary) insertFrom = isInsertAfter ? tableBoundary.after : tableBoundary.before;
5297
- else insertFrom = isInsertAfter ? endOfParagraphWithItsTextBoxes(doc, blockFrom) : blockFrom;
5298
- const insertTexts = operation.lineBreakMode !== "inline" && LINE_BREAK_PATTERN.test(operation.text) ? splitInsertParagraphTexts(operation.text) : [operation.text];
5668
+ const side = operation.type === "insertAfterBlock" ? "after" : "before";
5669
+ const placed = tableBoundary ? {
5670
+ from: tableBoundary[side],
5671
+ at: tableBoundary[side]
5672
+ } : beside(side);
5673
+ if (joinStopsAtTableAfter({
5674
+ doc,
5675
+ paragraphPos: placed.at,
5676
+ noteReferences
5677
+ })) return {
5678
+ type: "skip",
5679
+ ...pendingJoinInsertionRefusal
5680
+ };
5681
+ const insertFrom = placed.from;
5299
5682
  return {
5300
5683
  type: "resolved",
5301
5684
  operation: {
@@ -5305,8 +5688,12 @@ const resolveOperationTarget = ({ snapshot, operation, liveBlocks, liveBlocksByP
5305
5688
  blockFrom,
5306
5689
  blockTo,
5307
5690
  blockNode,
5308
- insertTexts,
5309
- boundaryComments: commentsAcrossBlockBoundary(doc, insertFrom)
5691
+ insertTexts: operation.lineBreakMode !== "inline" && LINE_BREAK_PATTERN.test(operation.text) ? splitInsertParagraphTexts(operation.text) : [operation.text],
5692
+ boundaryComments: commentsAcrossBlockBoundary(doc, placed.at),
5693
+ ...placed.joinSide !== void 0 && {
5694
+ joinSide: placed.joinSide,
5695
+ joinAt: placed.joinAt
5696
+ }
5310
5697
  }
5311
5698
  };
5312
5699
  }
@@ -5317,18 +5704,23 @@ const resolveOperationTarget = ({ snapshot, operation, liveBlocks, liveBlocksByP
5317
5704
  };
5318
5705
  const position = operation.position ?? "after";
5319
5706
  const tableBoundary = findOutermostTableBoundary(doc, blockFrom);
5320
- let insertFrom;
5321
- if (tableBoundary) insertFrom = position === "after" ? tableBoundary.after : tableBoundary.before;
5322
- else insertFrom = position === "after" ? endOfParagraphWithItsTextBoxes(doc, blockFrom) : blockFrom;
5707
+ const placed = tableBoundary ? {
5708
+ from: tableBoundary[position],
5709
+ at: tableBoundary[position]
5710
+ } : beside(position);
5323
5711
  return {
5324
5712
  type: "resolved",
5325
5713
  operation: {
5326
5714
  operation,
5327
- from: insertFrom,
5328
- to: insertFrom,
5715
+ from: placed.from,
5716
+ to: placed.from,
5329
5717
  blockFrom,
5330
5718
  blockTo,
5331
- blockNode
5719
+ blockNode,
5720
+ ...placed.joinSide !== void 0 && {
5721
+ joinSide: placed.joinSide,
5722
+ joinAt: placed.joinAt
5723
+ }
5332
5724
  }
5333
5725
  };
5334
5726
  }
@@ -5549,21 +5941,26 @@ const resolveOperationTarget = ({ snapshot, operation, liveBlocks, liveBlocksByP
5549
5941
  };
5550
5942
  }
5551
5943
  if (operation.type === "insertTable") {
5552
- const boundary = findOutermostTableBoundary(doc, blockFrom) ?? {
5553
- before: blockFrom,
5554
- after: endOfParagraphWithItsTextBoxes(doc, blockFrom)
5555
- };
5556
- const insertFrom = (operation.position ?? "after") === "after" ? boundary.after : boundary.before;
5944
+ const side = (operation.position ?? "after") === "after" ? "after" : "before";
5945
+ const tableBoundary = findOutermostTableBoundary(doc, blockFrom);
5946
+ const placed = tableBoundary ? {
5947
+ from: tableBoundary[side],
5948
+ at: tableBoundary[side]
5949
+ } : beside(side);
5557
5950
  return {
5558
5951
  type: "resolved",
5559
5952
  operation: {
5560
5953
  operation,
5561
- from: insertFrom,
5562
- to: insertFrom,
5954
+ from: placed.from,
5955
+ to: placed.from,
5563
5956
  blockFrom,
5564
5957
  blockTo,
5565
5958
  blockNode,
5566
- boundaryComments: commentsAcrossBlockBoundary(doc, insertFrom)
5959
+ boundaryComments: commentsAcrossBlockBoundary(doc, placed.at),
5960
+ ...placed.joinSide !== void 0 && {
5961
+ joinSide: placed.joinSide,
5962
+ joinAt: placed.joinAt
5963
+ }
5567
5964
  }
5568
5965
  };
5569
5966
  }
@@ -5947,4 +6344,4 @@ const getOperationQuote = (operation) => {
5947
6344
  if (operation.type === "replaceInBlock") return operation.find;
5948
6345
  if (operation.type === "commentOnBlock") return operation.quote;
5949
6346
  };
5950
- export { applyFolioAIEditOperations, insertCleanText, previewFolioAIEditOperations, previewFolioAIEditOperationsWithResult };
6347
+ export { applyFolioAIEditOperations, applyFolioAIEditOperationsInPackage, insertCleanText, previewFolioAIEditOperations, previewFolioAIEditOperationsWithResult };