@stll/folio-core 0.48.0 → 0.49.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.
- package/dist/ai-edits/apply.js +565 -121
- package/dist/ai-edits/minimal-replacement.d.ts +64 -0
- package/dist/ai-edits/minimal-replacement.js +282 -0
- package/dist/controller/layoutPipeline.js +11 -1
- package/dist/display-list/build/paragraphPrimitives.js +63 -7
- package/dist/display-list/build/textBoxPrimitives.js +1 -1
- package/dist/display-list/build/unsupported.d.ts +2 -2
- package/dist/display-list/build/unsupported.js +0 -0
- package/dist/display-list/dom/renderDisplayListToDom.js +0 -1
- package/dist/docx/blockContentParser.js +2 -2
- package/dist/docx/bulletMarkers.d.ts +18 -2
- package/dist/docx/bulletMarkers.js +27 -2
- package/dist/docx/drawingGroupChildren.d.ts +60 -0
- package/dist/docx/drawingGroupChildren.js +176 -0
- package/dist/docx/drawingUtils.js +3 -0
- package/dist/docx/groupDrawingParser.d.ts +7 -1
- package/dist/docx/groupDrawingParser.js +123 -32
- package/dist/docx/packageParts.d.ts +45 -1
- package/dist/docx/packageParts.js +93 -2
- package/dist/docx/paraIdAttribute.d.ts +3 -2
- package/dist/docx/paragraphParser.js +91 -7
- package/dist/docx/paragraphTextBoxEnrichment.js +126 -4
- package/dist/docx/runParser.js +10 -0
- package/dist/docx/selectiveSaveFlags.d.ts +10 -3
- package/dist/docx/selectiveSaveFlags.js +1 -1
- package/dist/docx/selectiveXmlPatch.d.ts +56 -34
- package/dist/docx/selectiveXmlPatch.js +222 -115
- package/dist/docx/serializer/groupTextBoxWriteBack.d.ts +12 -0
- package/dist/docx/serializer/groupTextBoxWriteBack.js +101 -0
- package/dist/docx/serializer/paragraphSerializer.js +5 -4
- package/dist/docx/serializer/runSerializer.d.ts +5 -1
- package/dist/docx/serializer/runSerializer.js +4 -3
- package/dist/docx/shapeAlternateContent.d.ts +25 -0
- package/dist/docx/shapeAlternateContent.js +52 -0
- package/dist/docx/textBoxParser.d.ts +10 -2
- package/dist/docx/textBoxParser.js +9 -3
- package/dist/internal/headlessRevisionResolution.js +50 -13
- package/dist/layout-bridge/convert/toFlowBlocks.js +26 -4
- package/dist/layout-engine/index.d.ts +2 -2
- package/dist/layout-engine/index.js +33 -4
- package/dist/layout-engine/keep-together.d.ts +5 -2
- package/dist/layout-engine/keep-together.js +5 -1
- package/dist/layout-engine/measure/advanceComposition.js +49 -3
- package/dist/layout-engine/measure/measureBlocks.js +5 -3
- package/dist/layout-engine/measure/measureContainer.js +46 -3
- package/dist/layout-engine/measure/measureHelpers.js +13 -3
- package/dist/layout-engine/measure/measureParagraph.js +44 -81
- package/dist/layout-engine/measure/smallCapsCasing.d.ts +56 -0
- package/dist/layout-engine/measure/smallCapsCasing.js +88 -0
- package/dist/layout-engine/paginator.d.ts +2 -0
- package/dist/layout-engine/paginator.js +5 -1
- package/dist/layout-engine/paragraphSpacing.d.ts +4 -2
- package/dist/layout-engine/paragraphSpacing.js +2 -1
- package/dist/layout-engine/tableRowBreak.d.ts +8 -1
- package/dist/layout-engine/tableRowBreak.js +22 -1
- package/dist/layout-engine/types.d.ts +21 -2
- package/dist/layout-painter/renderParagraph.js +41 -4
- package/dist/layout-painter/renderTextBox.d.ts +12 -2
- package/dist/layout-painter/renderTextBox.js +26 -2
- package/dist/prosemirror/alternateContentAttrs.d.ts +9 -0
- package/dist/prosemirror/alternateContentAttrs.js +35 -0
- package/dist/prosemirror/attrs/index.js +90 -0
- package/dist/prosemirror/commands/comments.js +51 -41
- package/dist/prosemirror/contentControlRevisions.d.ts +43 -0
- package/dist/prosemirror/contentControlRevisions.js +84 -0
- package/dist/prosemirror/conversion/fromProseDoc.js +46 -22
- package/dist/prosemirror/conversion/toProseDoc.js +42 -19
- package/dist/prosemirror/extensions/features/AutoBidiDetectionExtension.d.ts +7 -7
- package/dist/prosemirror/extensions/features/AutoBidiDetectionExtension.js +7 -7
- package/dist/prosemirror/extensions/nodes/FieldExtension.js +6 -3
- package/dist/prosemirror/extensions/nodes/SdtExtension.js +9 -2
- package/dist/prosemirror/extensions/nodes/ShapeExtension.js +1 -0
- package/dist/prosemirror/extensions/nodes/TextBoxExtension.js +3 -0
- package/dist/prosemirror/listMarker.js +2 -2
- package/dist/prosemirror/paragraphDirection.d.ts +29 -2
- package/dist/prosemirror/paragraphDirection.js +21 -2
- package/dist/prosemirror/plugins/suggestionMode.js +37 -8
- package/dist/prosemirror/rejoinRunCarriers.d.ts +27 -0
- package/dist/prosemirror/rejoinRunCarriers.js +66 -0
- package/dist/prosemirror/runIdentityAcrossRevisions.d.ts +10 -0
- package/dist/prosemirror/runIdentityAcrossRevisions.js +90 -0
- package/dist/prosemirror/schema/nodes.d.ts +37 -5
- package/dist/types/content.d.ts +2 -2
- package/package.json +2 -2
- package/dist/layout-engine/justifiedLineFit.d.ts +0 -7
- package/dist/layout-engine/justifiedLineFit.js +0 -6
package/dist/ai-edits/apply.js
CHANGED
|
@@ -3,6 +3,7 @@ import { formattingEquals } from "../docx/runConsolidator.js";
|
|
|
3
3
|
import { expectParagraphAttrs, expectRunPropertyChangeMarkAttrs } from "../prosemirror/attrs/index.js";
|
|
4
4
|
import { hasSerializableParagraphPropertyChange, paragraphPropertiesSnapshot } from "../prosemirror/commands/propertyChangeScope.js";
|
|
5
5
|
import { addedBreakCarrierBefore, finalParagraphsOf, paragraphEndsItsContainer } from "../prosemirror/containerFinalParagraph.js";
|
|
6
|
+
import { encloseWholeControls } from "../prosemirror/contentControlRevisions.js";
|
|
6
7
|
import { requestDeterministicParaIds } from "../prosemirror/extensions/features/ParaIdAllocatorExtension.js";
|
|
7
8
|
import { markStructuralChange } from "../prosemirror/extensions/features/ParagraphChangeTrackerExtension.js";
|
|
8
9
|
import { CLEARED_LIST_RENDERING_ATTRS } from "../prosemirror/listMarker.js";
|
|
@@ -12,7 +13,7 @@ import { directParagraphIndentation, paragraphIndentationAttrPatch, paragraphInd
|
|
|
12
13
|
import { directParagraphSpacing, paragraphSpacingAttrPatch, paragraphSpacingEqual, paragraphSpacingFromFormatting, withDirectParagraphSpacing } from "../prosemirror/paragraphSpacing.js";
|
|
13
14
|
import { getDocumentNumbering } from "../prosemirror/plugins/documentNumbering.js";
|
|
14
15
|
import { getDocumentStyleResolver } from "../prosemirror/plugins/documentStyles.js";
|
|
15
|
-
import { addCarriedMarks, annotatedReplacement, hasReplacedAnnotations, inheritedReplacementMarks, surveyReplacedAnnotations } from "../prosemirror/replacedAnnotations.js";
|
|
16
|
+
import { NON_INCLUSIVE_MARK_DISPOSITION, addCarriedMarks, annotatedReplacement, hasReplacedAnnotations, inheritedReplacementMarks, surveyReplacedAnnotations } from "../prosemirror/replacedAnnotations.js";
|
|
16
17
|
import { applyMarksToRunFormattingRepresentation, runFormattingInlineControlNodeName, selectRunFormattingCarrierRepresentations } from "../prosemirror/runFormattingInlineCarriers.js";
|
|
17
18
|
import { readAuthoredRunFormatting, reconcileRunFormattingMarks } from "../prosemirror/runFormattingReconciliation.js";
|
|
18
19
|
import { paragraphRunStyleContext, paragraphRunStyleContextAt } from "../prosemirror/runStyleFormatting.js";
|
|
@@ -22,6 +23,7 @@ import { getFolioParaIdFromBlockId } from "../types/block-id.js";
|
|
|
22
23
|
import { stripBlockIdentityAttrs } from "./block-identity.js";
|
|
23
24
|
import { buildCleanBlockText, resolveCleanTextRange } from "./clean-text.js";
|
|
24
25
|
import { hasInlineEmphasis, parseInlineEmphasisRuns, stripInlineEmphasisMarkers } from "./inline-emphasis.js";
|
|
26
|
+
import { applyTextChanges, changesFromSegments, planTextChanges, widenChangesToAtomicSpans } from "./minimal-replacement.js";
|
|
25
27
|
import { hashFolioAIBlockStructuralBoundaries, hashFolioAIBlockText, isHiddenTableRow, normalizeFolioAIBlockText } from "./snapshot.js";
|
|
26
28
|
import { mergeTableRectangle, mergeTrackedVerticalTableCells, splitTableRectangle, splitTrackedVerticalTableCell } from "./table-cell-mutations.js";
|
|
27
29
|
import { planTableMutations } from "./table-mutation-plan.js";
|
|
@@ -612,7 +614,14 @@ const clearReplacementBackground = ({ tr, schema, from, to, mode, replacementBac
|
|
|
612
614
|
...styleResolver !== void 0 ? { styleResolver } : {}
|
|
613
615
|
});
|
|
614
616
|
};
|
|
615
|
-
const applyTrackedInlineFormatting = ({
|
|
617
|
+
const applyTrackedInlineFormatting = ({ from, to, ...options }) => applyTrackedInlineFormattingToRanges({
|
|
618
|
+
...options,
|
|
619
|
+
ranges: [{
|
|
620
|
+
from,
|
|
621
|
+
to
|
|
622
|
+
}]
|
|
623
|
+
});
|
|
624
|
+
const applyTrackedInlineFormattingToRanges = ({ tr, schema, doc = tr.doc, ranges, formatting, revisionIdSeed, author, date, initials, suggestionId = null, styleResolver }) => {
|
|
616
625
|
const propertyChangeType = schema.marks["runPropertyChange"];
|
|
617
626
|
if (!propertyChangeType) return {
|
|
618
627
|
type: "applied",
|
|
@@ -622,11 +631,11 @@ const applyTrackedInlineFormatting = ({ tr, schema, doc, from, to, formatting, r
|
|
|
622
631
|
};
|
|
623
632
|
const segments = [];
|
|
624
633
|
let hasPendingRunPropertyChange = false;
|
|
625
|
-
const representations = selectRunFormattingCarrierRepresentations({
|
|
634
|
+
const representations = ranges.flatMap(({ from, to }) => selectRunFormattingCarrierRepresentations({
|
|
626
635
|
doc,
|
|
627
636
|
from,
|
|
628
637
|
to
|
|
629
|
-
});
|
|
638
|
+
}));
|
|
630
639
|
for (const representation of representations) {
|
|
631
640
|
const { node } = representation;
|
|
632
641
|
const styleContext = paragraphRunStyleContextAt({
|
|
@@ -717,36 +726,17 @@ const applyTrackedInlineFormatting = ({ tr, schema, doc, from, to, formatting, r
|
|
|
717
726
|
* `Date.now()` seed per applyAIEditOperations call would collide
|
|
718
727
|
* across batches that fire within the same millisecond (the panel's
|
|
719
728
|
* Accept-all loop does exactly that — multiple calls in tight
|
|
720
|
-
* succession).
|
|
721
|
-
*
|
|
729
|
+
* succession).
|
|
730
|
+
*
|
|
731
|
+
* A batch without a revision stamp allocates from it as it writes and claims
|
|
732
|
+
* each operation's ids once they are written, rather than reserving a guess
|
|
733
|
+
* up front: how many revisions an operation records is known only once it has
|
|
734
|
+
* recorded them (clearing a background records one run-property change per
|
|
735
|
+
* carrier). The claim also comes before anything that may start another
|
|
736
|
+
* batch, a comment-id callback or the dispatch, so batches in one realm never
|
|
737
|
+
* share an id and follow one another without a gap.
|
|
722
738
|
*/
|
|
723
739
|
let revisionIdCursor = Date.now() * 1e3;
|
|
724
|
-
const nextRevisionSeed = (revisionIdCount) => {
|
|
725
|
-
const start = revisionIdCursor;
|
|
726
|
-
revisionIdCursor += Math.max(revisionIdCount, 1);
|
|
727
|
-
return start;
|
|
728
|
-
};
|
|
729
|
-
/**
|
|
730
|
-
* Ids one resolved operation may allocate from the shared revision-id range
|
|
731
|
-
* reserved by `nextRevisionSeed`. Most operation types allocate at most
|
|
732
|
-
* three (delete + insert + background-format clearing); reserving four per
|
|
733
|
-
* operation is a safe cushion above that. A multi-paragraph
|
|
734
|
-
* `insertAfterBlock` / `insertBeforeBlock` (`text` split on line breaks,
|
|
735
|
-
* see `splitInsertParagraphTexts`) allocates one id per paragraph in
|
|
736
|
-
* tracked-changes mode plus one for each paragraph MARK it brings, so it
|
|
737
|
-
* needs more than four once split into more than two paragraphs. Rotating a
|
|
738
|
-
* terminal run can add one paragraph-property revision per inserted paragraph;
|
|
739
|
-
* reserving less than that would let a later `nextRevisionSeed` call reuse an
|
|
740
|
-
* id this operation already stamped on the document.
|
|
741
|
-
*/
|
|
742
|
-
const REVISION_IDS_PER_OPERATION = 4;
|
|
743
|
-
const estimateRevisionIdReservation = (item) => {
|
|
744
|
-
if (item.operation.type === "insertAfterBlock" || item.operation.type === "insertBeforeBlock") {
|
|
745
|
-
const paragraphCount = item.insertTexts?.length ?? 1;
|
|
746
|
-
return Math.max(paragraphCount * 3, REVISION_IDS_PER_OPERATION);
|
|
747
|
-
}
|
|
748
|
-
return REVISION_IDS_PER_OPERATION;
|
|
749
|
-
};
|
|
750
740
|
/**
|
|
751
741
|
* Walk the live doc once and bucket every textblock by its
|
|
752
742
|
* normalised text hash. Resolution then maps each snapshot anchor
|
|
@@ -1404,9 +1394,19 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
|
|
|
1404
1394
|
});
|
|
1405
1395
|
}
|
|
1406
1396
|
let tr = view.state.tr;
|
|
1407
|
-
const revisionIdReservation = executableResolved.reduce((total, item) => total + estimateRevisionIdReservation(item), 0);
|
|
1408
1397
|
const ownsSharedRevisionIdCursor = revisionIdSeed === void 0 && revisionStamp === void 0;
|
|
1409
|
-
let revisionSeed = revisionIdSeed ?? revisionStamp?.idSeed ??
|
|
1398
|
+
let revisionSeed = revisionIdSeed ?? revisionStamp?.idSeed ?? revisionIdCursor;
|
|
1399
|
+
/**
|
|
1400
|
+
* Claim the shared ids below `nextRevisionId` for this batch. A seeded or
|
|
1401
|
+
* stamped batch allocates from its own range and claims nothing.
|
|
1402
|
+
*/
|
|
1403
|
+
const claimSharedRevisionIds = (nextRevisionId) => {
|
|
1404
|
+
if (ownsSharedRevisionIdCursor) revisionIdCursor = Math.max(revisionIdCursor, nextRevisionId);
|
|
1405
|
+
};
|
|
1406
|
+
/** Continue past any shared id a batch the host started meanwhile claimed. */
|
|
1407
|
+
const continueSharedRevisionIds = () => {
|
|
1408
|
+
if (ownsSharedRevisionIdCursor) revisionSeed = Math.max(revisionSeed, revisionIdCursor);
|
|
1409
|
+
};
|
|
1410
1410
|
const date = revisionStamp?.date ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
1411
1411
|
const insertedColumnCounts = /* @__PURE__ */ new Map();
|
|
1412
1412
|
const executionOrder = executableResolved.toSorted((left, right) => {
|
|
@@ -1437,6 +1437,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
|
|
|
1437
1437
|
for (let executionIndex = 0; executionIndex < executionOrder.length; executionIndex++) {
|
|
1438
1438
|
const item = executionOrder[executionIndex];
|
|
1439
1439
|
if (!item) panic("The operation execution index exceeded the resolved plan", { executionIndex });
|
|
1440
|
+
continueSharedRevisionIds();
|
|
1440
1441
|
if (mode === "tracked-changes" && writesParagraphPropertyChange(item)) {
|
|
1441
1442
|
const livePosition = tr.mapping.map(item.blockFrom);
|
|
1442
1443
|
const liveBlock = tr.doc.nodeAt(livePosition) ?? item.blockNode;
|
|
@@ -1475,6 +1476,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
|
|
|
1475
1476
|
numbering
|
|
1476
1477
|
});
|
|
1477
1478
|
revisionSeed = built.nextRevisionId;
|
|
1479
|
+
claimSharedRevisionIds(revisionSeed);
|
|
1478
1480
|
nodeGroups.push(built.nodes);
|
|
1479
1481
|
runApplied.push({
|
|
1480
1482
|
id: insertion.operation.id,
|
|
@@ -1522,9 +1524,58 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
|
|
|
1522
1524
|
switch (item.operation.type) {
|
|
1523
1525
|
case "replaceInBlock":
|
|
1524
1526
|
case "replaceRange": {
|
|
1527
|
+
if (mode === "direct" && !hasInlineEmphasis(item.operation.replace)) {
|
|
1528
|
+
const minimal = applyMinimalDirectReplacement({
|
|
1529
|
+
tr,
|
|
1530
|
+
item,
|
|
1531
|
+
replacement: item.operation.replace,
|
|
1532
|
+
commentMark,
|
|
1533
|
+
replacementBackground,
|
|
1534
|
+
...styleResolver !== void 0 ? { styleResolver } : {}
|
|
1535
|
+
});
|
|
1536
|
+
if (minimal !== null) {
|
|
1537
|
+
tr = minimal;
|
|
1538
|
+
break;
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1525
1541
|
const revisionIdDelete = operationRevisionSeed;
|
|
1526
1542
|
const revisionIdInsert = producesTrackedChanges ? operationRevisionSeed + 1 : operationRevisionSeed;
|
|
1527
1543
|
const revisionIdBackgroundSeed = producesTrackedChanges ? operationRevisionSeed + 2 : operationRevisionSeed;
|
|
1544
|
+
if (producesTrackedChanges) {
|
|
1545
|
+
const tracked = applyMinimalTrackedReplacement({
|
|
1546
|
+
tr,
|
|
1547
|
+
item,
|
|
1548
|
+
replacement: stripInlineEmphasisMarkers(item.operation.replace),
|
|
1549
|
+
commentMark,
|
|
1550
|
+
replacementBackground,
|
|
1551
|
+
...styleResolver !== void 0 ? { styleResolver } : {},
|
|
1552
|
+
diffText,
|
|
1553
|
+
revisionIdDelete,
|
|
1554
|
+
revisionIdInsert,
|
|
1555
|
+
revisionIdBackgroundSeed,
|
|
1556
|
+
author,
|
|
1557
|
+
date,
|
|
1558
|
+
initials,
|
|
1559
|
+
suggestionId
|
|
1560
|
+
});
|
|
1561
|
+
if (tracked?.type === "pendingRunPropertyChange") {
|
|
1562
|
+
skipped.push({
|
|
1563
|
+
id: item.operation.id,
|
|
1564
|
+
reason: tracked.type
|
|
1565
|
+
});
|
|
1566
|
+
continue;
|
|
1567
|
+
}
|
|
1568
|
+
if (tracked) {
|
|
1569
|
+
tr = tracked.transaction;
|
|
1570
|
+
operationRevisionSeed = tracked.nextRevisionId;
|
|
1571
|
+
appliedRevisionIds = [
|
|
1572
|
+
revisionIdDelete,
|
|
1573
|
+
revisionIdInsert,
|
|
1574
|
+
...tracked.backgroundRevisionIds
|
|
1575
|
+
];
|
|
1576
|
+
break;
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1528
1579
|
const stepsBeforeBackgroundClear = tr.steps.length;
|
|
1529
1580
|
const backgroundResult = clearReplacementBackground({
|
|
1530
1581
|
tr,
|
|
@@ -1560,8 +1611,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
|
|
|
1560
1611
|
revisionIdInsert,
|
|
1561
1612
|
commentMark,
|
|
1562
1613
|
suggestionId,
|
|
1563
|
-
initials
|
|
1564
|
-
diffText
|
|
1614
|
+
initials
|
|
1565
1615
|
});
|
|
1566
1616
|
if (producesTrackedChanges) appliedRevisionIds = [
|
|
1567
1617
|
revisionIdDelete,
|
|
@@ -1652,7 +1702,44 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
|
|
|
1652
1702
|
}
|
|
1653
1703
|
let clearedBackground = false;
|
|
1654
1704
|
let backgroundRevisionIds = [];
|
|
1655
|
-
|
|
1705
|
+
const minimal = changesText && mode === "direct" ? applyMinimalDirectReplacement({
|
|
1706
|
+
tr,
|
|
1707
|
+
item,
|
|
1708
|
+
replacement: item.operation.text,
|
|
1709
|
+
commentMark,
|
|
1710
|
+
replacementBackground,
|
|
1711
|
+
...styleResolver !== void 0 ? { styleResolver } : {}
|
|
1712
|
+
}) : null;
|
|
1713
|
+
const tracked = changesText && producesTrackedChanges ? applyMinimalTrackedReplacement({
|
|
1714
|
+
tr,
|
|
1715
|
+
item,
|
|
1716
|
+
replacement: stripInlineEmphasisMarkers(item.operation.text),
|
|
1717
|
+
commentMark,
|
|
1718
|
+
replacementBackground,
|
|
1719
|
+
...styleResolver !== void 0 ? { styleResolver } : {},
|
|
1720
|
+
diffText,
|
|
1721
|
+
revisionIdDelete,
|
|
1722
|
+
revisionIdInsert,
|
|
1723
|
+
revisionIdBackgroundSeed,
|
|
1724
|
+
author,
|
|
1725
|
+
date,
|
|
1726
|
+
initials,
|
|
1727
|
+
suggestionId
|
|
1728
|
+
}) : null;
|
|
1729
|
+
if (tracked?.type === "pendingRunPropertyChange") {
|
|
1730
|
+
skipped.push({
|
|
1731
|
+
id: item.operation.id,
|
|
1732
|
+
reason: tracked.type
|
|
1733
|
+
});
|
|
1734
|
+
continue;
|
|
1735
|
+
}
|
|
1736
|
+
if (minimal !== null) tr = minimal;
|
|
1737
|
+
else if (tracked !== null) {
|
|
1738
|
+
tr = tracked.transaction;
|
|
1739
|
+
operationRevisionSeed = tracked.nextRevisionId;
|
|
1740
|
+
clearedBackground = tracked.backgroundRevisionIds.length > 0;
|
|
1741
|
+
backgroundRevisionIds = tracked.backgroundRevisionIds;
|
|
1742
|
+
} else if (changesText) {
|
|
1656
1743
|
const stepsBeforeBackgroundClear = tr.steps.length;
|
|
1657
1744
|
const backgroundResult = clearReplacementBackground({
|
|
1658
1745
|
tr,
|
|
@@ -1689,8 +1776,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
|
|
|
1689
1776
|
revisionIdInsert,
|
|
1690
1777
|
commentMark,
|
|
1691
1778
|
suggestionId,
|
|
1692
|
-
initials
|
|
1693
|
-
diffText
|
|
1779
|
+
initials
|
|
1694
1780
|
});
|
|
1695
1781
|
}
|
|
1696
1782
|
const allocateStyleRevisionInfo = () => ({
|
|
@@ -1993,6 +2079,12 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
|
|
|
1993
2079
|
tr = tr.addMark(item.from, item.to, deletionMark);
|
|
1994
2080
|
const atomRanges = undeletedContentAtomRanges(tr, item.blockFrom);
|
|
1995
2081
|
for (const { from, to } of atomRanges) tr = tr.addMark(from, to, deletionMark);
|
|
2082
|
+
encloseWholeControls({
|
|
2083
|
+
tr,
|
|
2084
|
+
from: item.blockFrom,
|
|
2085
|
+
to: item.blockTo,
|
|
2086
|
+
revisionId
|
|
2087
|
+
});
|
|
1996
2088
|
const inlineRevisionApplied = item.from < item.to || atomRanges.length > 0;
|
|
1997
2089
|
appliedRevisionIds = inlineRevisionApplied ? [revisionId] : [];
|
|
1998
2090
|
const markPosition = tr.mapping.map(item.blockFrom);
|
|
@@ -2263,6 +2355,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
|
|
|
2263
2355
|
operationId: item.operation.id,
|
|
2264
2356
|
operationType: item.operation.type
|
|
2265
2357
|
});
|
|
2358
|
+
claimSharedRevisionIds(operationRevisionSeed);
|
|
2266
2359
|
committedCommentId = allocateCommittedCommentId(item.commentText);
|
|
2267
2360
|
tr = commitProvisionalComment({
|
|
2268
2361
|
tr,
|
|
@@ -2272,6 +2365,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
|
|
|
2272
2365
|
});
|
|
2273
2366
|
}
|
|
2274
2367
|
revisionSeed = operationRevisionSeed;
|
|
2368
|
+
claimSharedRevisionIds(revisionSeed);
|
|
2275
2369
|
applied.push({
|
|
2276
2370
|
id: item.operation.id,
|
|
2277
2371
|
...committedCommentId !== void 0 && { commentId: committedCommentId },
|
|
@@ -2285,6 +2379,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
|
|
|
2285
2379
|
if (tr.docChanged) {
|
|
2286
2380
|
const batchRevisionIds = new Set(applied.flatMap(({ revisionIds }) => revisionIds ?? []));
|
|
2287
2381
|
tr = withoutRevisionsOnZeroWidthAnchors(tr, batchRevisionIds);
|
|
2382
|
+
continueSharedRevisionIds();
|
|
2288
2383
|
const rotated = withRotatedAddedFinalBreaks({
|
|
2289
2384
|
tr,
|
|
2290
2385
|
batchRevisionIds,
|
|
@@ -2309,7 +2404,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
|
|
|
2309
2404
|
revisionIds: [...receipt.revisionIds, revisionId]
|
|
2310
2405
|
};
|
|
2311
2406
|
}
|
|
2312
|
-
|
|
2407
|
+
claimSharedRevisionIds(revisionSeed);
|
|
2313
2408
|
if (revisionStamp) requestDeterministicParaIds(tr, `${revisionStamp.date}:${String(revisionStamp.idSeed)}`);
|
|
2314
2409
|
view.dispatch(tr);
|
|
2315
2410
|
}
|
|
@@ -2389,7 +2484,7 @@ const insertCleanText = ({ tr, from, to, text, standsInFor }) => {
|
|
|
2389
2484
|
end: start + contentSize
|
|
2390
2485
|
};
|
|
2391
2486
|
};
|
|
2392
|
-
const applyTextReplacement = ({ tr, item, mode, author, date, revisionIdDelete, revisionIdInsert, commentMark, suggestionId = null, initials
|
|
2487
|
+
const applyTextReplacement = ({ tr, item, mode, author, date, revisionIdDelete, revisionIdInsert, commentMark, suggestionId = null, initials }) => {
|
|
2393
2488
|
let nextTr = tr;
|
|
2394
2489
|
const replacement = stripInlineEmphasisMarkers((() => {
|
|
2395
2490
|
if (item.operation.type === "replaceInBlock" || item.operation.type === "replaceRange") return item.operation.replace;
|
|
@@ -2435,86 +2530,6 @@ const applyTextReplacement = ({ tr, item, mode, author, date, revisionIdDelete,
|
|
|
2435
2530
|
...initialsAttr,
|
|
2436
2531
|
...suggestionAttrs
|
|
2437
2532
|
};
|
|
2438
|
-
const cleanBlock = item.blockNode.content.size === item.blockNode.textContent.length ? buildCleanBlockText(item.blockNode, item.blockFrom) : null;
|
|
2439
|
-
let sourceText = null;
|
|
2440
|
-
let sourceCleanStart = 0;
|
|
2441
|
-
if (cleanBlock !== null) {
|
|
2442
|
-
if (item.operation.type === "replaceInBlock") {
|
|
2443
|
-
sourceText = item.operation.find;
|
|
2444
|
-
sourceCleanStart = cleanBlock.text.indexOf(item.operation.find);
|
|
2445
|
-
if (sourceCleanStart === -1) sourceText = null;
|
|
2446
|
-
} else if (item.operation.type === "replaceRange") {
|
|
2447
|
-
sourceCleanStart = item.operation.range.startOffset;
|
|
2448
|
-
sourceText = cleanBlock.text.slice(sourceCleanStart, item.operation.range.endOffset);
|
|
2449
|
-
} else if (item.operation.type === "replaceBlock") {
|
|
2450
|
-
sourceText = cleanBlock.text;
|
|
2451
|
-
sourceCleanStart = 0;
|
|
2452
|
-
}
|
|
2453
|
-
}
|
|
2454
|
-
if (sourceText !== null && cleanBlock !== null) {
|
|
2455
|
-
const segments = diffText(sourceText, replacement);
|
|
2456
|
-
const offsets = cleanBlock.offsets;
|
|
2457
|
-
const offsetAt = (cleanOffset) => offsets[cleanOffset] ?? null;
|
|
2458
|
-
const steps = [];
|
|
2459
|
-
let cursor = 0;
|
|
2460
|
-
let allPositionsResolved = true;
|
|
2461
|
-
for (const seg of segments) {
|
|
2462
|
-
if (seg.type === "equal") {
|
|
2463
|
-
cursor += seg.text.length;
|
|
2464
|
-
continue;
|
|
2465
|
-
}
|
|
2466
|
-
if (seg.type === "del") {
|
|
2467
|
-
const pmFrom = offsetAt(sourceCleanStart + cursor);
|
|
2468
|
-
const pmTo = offsetAt(sourceCleanStart + cursor + seg.text.length);
|
|
2469
|
-
if (pmFrom === null || pmTo === null) {
|
|
2470
|
-
allPositionsResolved = false;
|
|
2471
|
-
break;
|
|
2472
|
-
}
|
|
2473
|
-
steps.push({
|
|
2474
|
-
kind: "del",
|
|
2475
|
-
from: pmFrom,
|
|
2476
|
-
to: pmTo
|
|
2477
|
-
});
|
|
2478
|
-
cursor += seg.text.length;
|
|
2479
|
-
continue;
|
|
2480
|
-
}
|
|
2481
|
-
const pmAt = offsetAt(sourceCleanStart + cursor);
|
|
2482
|
-
if (pmAt === null) {
|
|
2483
|
-
allPositionsResolved = false;
|
|
2484
|
-
break;
|
|
2485
|
-
}
|
|
2486
|
-
steps.push({
|
|
2487
|
-
kind: "ins",
|
|
2488
|
-
at: pmAt,
|
|
2489
|
-
text: seg.text
|
|
2490
|
-
});
|
|
2491
|
-
}
|
|
2492
|
-
if (allPositionsResolved) {
|
|
2493
|
-
for (const step of steps.toReversed()) {
|
|
2494
|
-
if (step.kind === "del" && deletionType) {
|
|
2495
|
-
nextTr = nextTr.addMark(step.from, step.to, deletionType.create(delAttrs));
|
|
2496
|
-
if (commentMark) nextTr = nextTr.addMark(step.from, step.to, commentMark);
|
|
2497
|
-
continue;
|
|
2498
|
-
}
|
|
2499
|
-
if (step.kind === "ins" && insertionType) {
|
|
2500
|
-
const inserted = insertCleanText({
|
|
2501
|
-
tr: nextTr,
|
|
2502
|
-
from: step.at,
|
|
2503
|
-
to: step.at,
|
|
2504
|
-
text: step.text,
|
|
2505
|
-
standsInFor: {
|
|
2506
|
-
from: item.from,
|
|
2507
|
-
to: item.to
|
|
2508
|
-
}
|
|
2509
|
-
});
|
|
2510
|
-
nextTr = inserted.transaction;
|
|
2511
|
-
nextTr = nextTr.addMark(inserted.start, inserted.end, insertionType.create(insAttrs));
|
|
2512
|
-
if (commentMark) nextTr = nextTr.addMark(inserted.start, inserted.end, commentMark);
|
|
2513
|
-
}
|
|
2514
|
-
}
|
|
2515
|
-
return nextTr;
|
|
2516
|
-
}
|
|
2517
|
-
}
|
|
2518
2533
|
if (replacement.length > 0 && insertionType) {
|
|
2519
2534
|
const inserted = insertCleanText({
|
|
2520
2535
|
tr: nextTr,
|
|
@@ -2536,6 +2551,435 @@ const applyTextReplacement = ({ tr, item, mode, author, date, revisionIdDelete,
|
|
|
2536
2551
|
}
|
|
2537
2552
|
return nextTr;
|
|
2538
2553
|
};
|
|
2554
|
+
/**
|
|
2555
|
+
* The clean-text span a text replacement matched: where it starts in the
|
|
2556
|
+
* block's clean text and what it says. `null` when the operation does not
|
|
2557
|
+
* replace text or its match is no longer in the block.
|
|
2558
|
+
*/
|
|
2559
|
+
const replacedCleanSpan = (item, cleanBlock) => {
|
|
2560
|
+
switch (item.operation.type) {
|
|
2561
|
+
case "replaceInBlock": {
|
|
2562
|
+
const start = cleanBlock.text.indexOf(item.operation.find);
|
|
2563
|
+
return start === -1 ? null : {
|
|
2564
|
+
start,
|
|
2565
|
+
text: item.operation.find
|
|
2566
|
+
};
|
|
2567
|
+
}
|
|
2568
|
+
case "replaceRange": {
|
|
2569
|
+
const { startOffset, endOffset } = item.operation.range;
|
|
2570
|
+
return {
|
|
2571
|
+
start: startOffset,
|
|
2572
|
+
text: cleanBlock.text.slice(startOffset, endOffset)
|
|
2573
|
+
};
|
|
2574
|
+
}
|
|
2575
|
+
case "replaceBlock": return {
|
|
2576
|
+
start: 0,
|
|
2577
|
+
text: cleanBlock.text
|
|
2578
|
+
};
|
|
2579
|
+
default: return null;
|
|
2580
|
+
}
|
|
2581
|
+
};
|
|
2582
|
+
const BACKGROUND_MARK_NAMES = /* @__PURE__ */ new Set(["highlight", "runShading"]);
|
|
2583
|
+
/**
|
|
2584
|
+
* Formatting, as opposed to a mark naming something outside the text or
|
|
2585
|
+
* describing one character (see `NON_INCLUSIVE_MARK_DISPOSITION`).
|
|
2586
|
+
*/
|
|
2587
|
+
const isFormattingMark = (mark) => mark.type.spec.inclusive !== false;
|
|
2588
|
+
const isCarriedMark = (mark) => NON_INCLUSIVE_MARK_DISPOSITION[mark.type.name] === "carry";
|
|
2589
|
+
/**
|
|
2590
|
+
* Map a text replacement's changes onto the document; `minimal-replacement.ts`
|
|
2591
|
+
* states the contract. `null` when the matched span cannot be mapped onto the
|
|
2592
|
+
* document character by character, or the changes do not produce the
|
|
2593
|
+
* replacement.
|
|
2594
|
+
*/
|
|
2595
|
+
const planDocumentReplacement = ({ doc, item, replacement, planChanges }) => {
|
|
2596
|
+
const cleanBlock = buildCleanBlockText(item.blockNode, item.blockFrom);
|
|
2597
|
+
const span = replacedCleanSpan(item, cleanBlock);
|
|
2598
|
+
if (span === null) return null;
|
|
2599
|
+
const spanStart = span.start;
|
|
2600
|
+
const spanEnd = span.start + span.text.length;
|
|
2601
|
+
const atomicSpans = cleanBlock.structuralBoundaries.flatMap((boundary) => boundary.type === "field" ? [{
|
|
2602
|
+
offset: boundary.offset - spanStart,
|
|
2603
|
+
length: boundary.length
|
|
2604
|
+
}] : []);
|
|
2605
|
+
const changes = widenChangesToAtomicSpans(span.text, planChanges(span.text), atomicSpans);
|
|
2606
|
+
if (applyTextChanges(span.text, changes) !== replacement) return null;
|
|
2607
|
+
const unitAt = (index) => {
|
|
2608
|
+
const from = cleanBlock.offsets[index];
|
|
2609
|
+
const node = from === void 0 ? null : doc.nodeAt(from);
|
|
2610
|
+
if (from === void 0 || !node) return null;
|
|
2611
|
+
return {
|
|
2612
|
+
from,
|
|
2613
|
+
to: node.isText ? from + 1 : from + node.nodeSize
|
|
2614
|
+
};
|
|
2615
|
+
};
|
|
2616
|
+
const matchedFrom = item.from;
|
|
2617
|
+
const matchedTo = item.to;
|
|
2618
|
+
const holdsMatch = (start, end) => matchedFrom >= start && matchedTo <= end;
|
|
2619
|
+
const climbAfter = (position) => {
|
|
2620
|
+
let at = position;
|
|
2621
|
+
for (let $at = doc.resolve(at);; $at = doc.resolve(at)) {
|
|
2622
|
+
if (!$at.parent.isInline || at !== $at.end() || holdsMatch($at.start(), $at.end())) return at;
|
|
2623
|
+
at = $at.after();
|
|
2624
|
+
}
|
|
2625
|
+
};
|
|
2626
|
+
const climbBefore = (position) => {
|
|
2627
|
+
let at = position;
|
|
2628
|
+
for (let $at = doc.resolve(at);; $at = doc.resolve(at)) {
|
|
2629
|
+
if (!$at.parent.isInline || at !== $at.start() || holdsMatch($at.start(), $at.end())) return at;
|
|
2630
|
+
at = $at.before();
|
|
2631
|
+
}
|
|
2632
|
+
};
|
|
2633
|
+
const isNoteReference = (index) => {
|
|
2634
|
+
const unit = unitAt(index);
|
|
2635
|
+
return (unit === null ? null : doc.nodeAt(unit.from))?.marks.some((mark) => mark.type.name === "footnoteRef") ?? false;
|
|
2636
|
+
};
|
|
2637
|
+
const formattingSourceBefore = (index) => {
|
|
2638
|
+
let source = index;
|
|
2639
|
+
while (source >= 0 && isNoteReference(source)) source--;
|
|
2640
|
+
return source;
|
|
2641
|
+
};
|
|
2642
|
+
const formattingSourceAfter = (index) => {
|
|
2643
|
+
let source = index;
|
|
2644
|
+
while (source < spanEnd && isNoteReference(source)) source++;
|
|
2645
|
+
return source;
|
|
2646
|
+
};
|
|
2647
|
+
const replacementStyleSource = (change) => {
|
|
2648
|
+
const first = spanStart + change.start;
|
|
2649
|
+
const source = formattingSourceAfter(first);
|
|
2650
|
+
return unitAt(source < spanStart + change.end ? source : first);
|
|
2651
|
+
};
|
|
2652
|
+
const matchCarried = (() => {
|
|
2653
|
+
let carried = null;
|
|
2654
|
+
for (let index = spanStart; index < spanEnd; index++) {
|
|
2655
|
+
const unit = unitAt(index);
|
|
2656
|
+
const marks = (unit === null ? null : doc.nodeAt(unit.from))?.marks ?? [];
|
|
2657
|
+
carried = (carried ?? marks.filter(isCarriedMark)).filter((mark) => mark.isInSet(marks));
|
|
2658
|
+
if (carried.length === 0) break;
|
|
2659
|
+
}
|
|
2660
|
+
return carried ?? [];
|
|
2661
|
+
})();
|
|
2662
|
+
const planned = [];
|
|
2663
|
+
for (const change of changes) {
|
|
2664
|
+
const pieces = [];
|
|
2665
|
+
for (let index = spanStart + change.start; index < spanStart + change.end; index++) {
|
|
2666
|
+
const unit = unitAt(index);
|
|
2667
|
+
if (unit === null) return null;
|
|
2668
|
+
const last = pieces.at(-1);
|
|
2669
|
+
if (last !== void 0 && unit.from < last.to) continue;
|
|
2670
|
+
if (last !== void 0 && last.to === unit.from) last.to = unit.to;
|
|
2671
|
+
else pieces.push({ ...unit });
|
|
2672
|
+
}
|
|
2673
|
+
if (pieces.length > 0) {
|
|
2674
|
+
planned.push({
|
|
2675
|
+
change,
|
|
2676
|
+
pieces,
|
|
2677
|
+
text: change.text
|
|
2678
|
+
});
|
|
2679
|
+
continue;
|
|
2680
|
+
}
|
|
2681
|
+
const at = spanStart + change.start;
|
|
2682
|
+
if (span.text.length === 0) {
|
|
2683
|
+
planned.push({
|
|
2684
|
+
change,
|
|
2685
|
+
pieces,
|
|
2686
|
+
text: change.text,
|
|
2687
|
+
insertion: {
|
|
2688
|
+
at: matchedFrom,
|
|
2689
|
+
marks: (current) => current.resolve(matchedFrom).marks()
|
|
2690
|
+
}
|
|
2691
|
+
});
|
|
2692
|
+
continue;
|
|
2693
|
+
}
|
|
2694
|
+
if (at > spanStart) {
|
|
2695
|
+
const previous = unitAt(at - 1);
|
|
2696
|
+
if (previous === null) return null;
|
|
2697
|
+
const styleSource = unitAt(formattingSourceBefore(at - 1));
|
|
2698
|
+
planned.push({
|
|
2699
|
+
change,
|
|
2700
|
+
pieces,
|
|
2701
|
+
text: change.text,
|
|
2702
|
+
insertion: {
|
|
2703
|
+
at: climbAfter(previous.to),
|
|
2704
|
+
marks: (current) => addCarriedMarks(styleSource === null ? [] : current.resolve(styleSource.to).marks(), matchCarried)
|
|
2705
|
+
}
|
|
2706
|
+
});
|
|
2707
|
+
continue;
|
|
2708
|
+
}
|
|
2709
|
+
const first = unitAt(at);
|
|
2710
|
+
if (first === null) return null;
|
|
2711
|
+
const styleSource = unitAt(formattingSourceAfter(at));
|
|
2712
|
+
planned.push({
|
|
2713
|
+
change,
|
|
2714
|
+
pieces,
|
|
2715
|
+
text: change.text,
|
|
2716
|
+
insertion: {
|
|
2717
|
+
at: climbBefore(first.from),
|
|
2718
|
+
marks: (current) => addCarriedMarks(styleSource === null ? [] : (current.nodeAt(styleSource.from)?.marks ?? []).filter(isFormattingMark), matchCarried)
|
|
2719
|
+
}
|
|
2720
|
+
});
|
|
2721
|
+
}
|
|
2722
|
+
return {
|
|
2723
|
+
changes,
|
|
2724
|
+
planned,
|
|
2725
|
+
spanStart,
|
|
2726
|
+
spanEnd,
|
|
2727
|
+
matchedFrom,
|
|
2728
|
+
matchedTo,
|
|
2729
|
+
unitAt,
|
|
2730
|
+
replacementStyleSource
|
|
2731
|
+
};
|
|
2732
|
+
};
|
|
2733
|
+
/**
|
|
2734
|
+
* Apply a direct-mode text replacement as the difference between the matched
|
|
2735
|
+
* text and its replacement; `minimal-replacement.ts` states the contract.
|
|
2736
|
+
*
|
|
2737
|
+
* Returns `null`, before touching the transaction, when the matched span
|
|
2738
|
+
* cannot be mapped onto the document character by character. The caller then
|
|
2739
|
+
* replaces the matched span whole, as it always did.
|
|
2740
|
+
*/
|
|
2741
|
+
const applyMinimalDirectReplacement = ({ tr, item, replacement, commentMark, replacementBackground, styleResolver }) => {
|
|
2742
|
+
const plan = planDocumentReplacement({
|
|
2743
|
+
doc: tr.doc,
|
|
2744
|
+
item,
|
|
2745
|
+
replacement,
|
|
2746
|
+
planChanges: (source) => planTextChanges(source, replacement)
|
|
2747
|
+
});
|
|
2748
|
+
if (plan === null || plan.changes.length === 0) return null;
|
|
2749
|
+
const { planned, matchedFrom, matchedTo } = plan;
|
|
2750
|
+
let nextTr = tr;
|
|
2751
|
+
if (replacementBackground === "clear") nextTr = clearTouchedBackground({
|
|
2752
|
+
tr: nextTr,
|
|
2753
|
+
ranges: unitRanges(touchedBackgroundIndices({
|
|
2754
|
+
doc: nextTr.doc,
|
|
2755
|
+
plan
|
|
2756
|
+
}), plan.unitAt),
|
|
2757
|
+
...styleResolver !== void 0 ? { styleResolver } : {}
|
|
2758
|
+
});
|
|
2759
|
+
const stepsBefore = nextTr.steps.length;
|
|
2760
|
+
const written = [];
|
|
2761
|
+
const schema = nextTr.doc.type.schema;
|
|
2762
|
+
for (const change of planned.toReversed()) {
|
|
2763
|
+
if (change.insertion !== void 0) {
|
|
2764
|
+
const { at, marks } = change.insertion;
|
|
2765
|
+
nextTr = nextTr.insert(at, cleanTextInlineNodes({
|
|
2766
|
+
schema,
|
|
2767
|
+
text: change.text,
|
|
2768
|
+
marks: marks(nextTr.doc)
|
|
2769
|
+
}));
|
|
2770
|
+
written.push({
|
|
2771
|
+
from: at,
|
|
2772
|
+
to: at + change.text.length,
|
|
2773
|
+
step: nextTr.steps.length
|
|
2774
|
+
});
|
|
2775
|
+
continue;
|
|
2776
|
+
}
|
|
2777
|
+
const [first, ...rest] = change.pieces;
|
|
2778
|
+
if (first === void 0) continue;
|
|
2779
|
+
const stepsBeforeWrite = nextTr.steps.length;
|
|
2780
|
+
if (change.text.length > 0) {
|
|
2781
|
+
const replaced = insertCleanText({
|
|
2782
|
+
tr: nextTr,
|
|
2783
|
+
from: first.from,
|
|
2784
|
+
to: first.to,
|
|
2785
|
+
text: change.text,
|
|
2786
|
+
standsInFor: {
|
|
2787
|
+
from: first.from,
|
|
2788
|
+
to: rest.at(-1)?.to ?? first.to
|
|
2789
|
+
}
|
|
2790
|
+
});
|
|
2791
|
+
nextTr = replaced.transaction;
|
|
2792
|
+
written.push({
|
|
2793
|
+
from: replaced.start,
|
|
2794
|
+
to: replaced.end,
|
|
2795
|
+
step: nextTr.steps.length
|
|
2796
|
+
});
|
|
2797
|
+
} else nextTr = nextTr.delete(first.from, first.to);
|
|
2798
|
+
const afterWrite = nextTr.mapping.slice(stepsBeforeWrite);
|
|
2799
|
+
for (const piece of rest.toReversed()) nextTr = nextTr.delete(afterWrite.map(piece.from, 1), afterWrite.map(piece.to, -1));
|
|
2800
|
+
}
|
|
2801
|
+
if (commentMark && replacement.length > 0) {
|
|
2802
|
+
const { mapping } = nextTr;
|
|
2803
|
+
const throughEdit = mapping.slice(stepsBefore);
|
|
2804
|
+
let from = throughEdit.map(matchedFrom, -1);
|
|
2805
|
+
let to = throughEdit.map(matchedTo, 1);
|
|
2806
|
+
for (const range of written) {
|
|
2807
|
+
const afterRange = mapping.slice(range.step);
|
|
2808
|
+
from = Math.min(from, afterRange.map(range.from, -1));
|
|
2809
|
+
to = Math.max(to, afterRange.map(range.to, 1));
|
|
2810
|
+
}
|
|
2811
|
+
if (to > from) nextTr = nextTr.addMark(from, to, commentMark);
|
|
2812
|
+
}
|
|
2813
|
+
return nextTr;
|
|
2814
|
+
};
|
|
2815
|
+
/**
|
|
2816
|
+
* Apply a tracked-changes or suggested text replacement as its redline: each
|
|
2817
|
+
* change marks exactly the characters it removes as a deletion and writes
|
|
2818
|
+
* exactly its new characters as an insertion after them. Characters and
|
|
2819
|
+
* inline content outside every change carry no revision, and background
|
|
2820
|
+
* clearing records a property change only on the untouched characters of a
|
|
2821
|
+
* highlighted stretch a change touches (removed characters keep theirs, and
|
|
2822
|
+
* the new text never takes it). `minimal-replacement.ts` states the rest.
|
|
2823
|
+
*
|
|
2824
|
+
* Returns `null`, before touching the transaction, when the matched span
|
|
2825
|
+
* cannot be mapped onto the document character by character; the caller then
|
|
2826
|
+
* marks the whole match as replaced, as it always did.
|
|
2827
|
+
*/
|
|
2828
|
+
const applyMinimalTrackedReplacement = ({ tr, item, replacement, commentMark, replacementBackground, styleResolver, diffText, revisionIdDelete, revisionIdInsert, revisionIdBackgroundSeed, author, date, initials, suggestionId }) => {
|
|
2829
|
+
const schema = tr.doc.type.schema;
|
|
2830
|
+
const insertionType = schema.marks["insertion"];
|
|
2831
|
+
const deletionType = schema.marks["deletion"];
|
|
2832
|
+
if (!insertionType || !deletionType) return null;
|
|
2833
|
+
const plan = planDocumentReplacement({
|
|
2834
|
+
doc: tr.doc,
|
|
2835
|
+
item,
|
|
2836
|
+
replacement,
|
|
2837
|
+
planChanges: (source) => changesFromSegments(diffText(source, replacement))
|
|
2838
|
+
});
|
|
2839
|
+
if (plan === null) return null;
|
|
2840
|
+
let nextTr = tr;
|
|
2841
|
+
let backgroundRevisionIds = [];
|
|
2842
|
+
let nextRevisionId = revisionIdBackgroundSeed;
|
|
2843
|
+
const clearsBackground = replacementBackground === "clear";
|
|
2844
|
+
if (clearsBackground) {
|
|
2845
|
+
const removed = new Set(plan.changes.flatMap((change) => Array.from({ length: change.end - change.start }, (_, offset) => plan.spanStart + change.start + offset)));
|
|
2846
|
+
const kept = [...touchedBackgroundIndices({
|
|
2847
|
+
doc: nextTr.doc,
|
|
2848
|
+
plan
|
|
2849
|
+
})].filter((index) => !removed.has(index));
|
|
2850
|
+
const ranges = unitRanges(new Set(kept), plan.unitAt);
|
|
2851
|
+
if (ranges.length > 0) {
|
|
2852
|
+
const cleared = applyTrackedInlineFormattingToRanges({
|
|
2853
|
+
tr: nextTr,
|
|
2854
|
+
schema,
|
|
2855
|
+
ranges,
|
|
2856
|
+
formatting: REPLACEMENT_BACKGROUND_CLEAR_FORMATTING,
|
|
2857
|
+
revisionIdSeed: revisionIdBackgroundSeed,
|
|
2858
|
+
author,
|
|
2859
|
+
date,
|
|
2860
|
+
initials,
|
|
2861
|
+
suggestionId,
|
|
2862
|
+
...styleResolver !== void 0 ? { styleResolver } : {}
|
|
2863
|
+
});
|
|
2864
|
+
if (cleared.type === "pendingRunPropertyChange") return { type: "pendingRunPropertyChange" };
|
|
2865
|
+
nextTr = cleared.transaction;
|
|
2866
|
+
backgroundRevisionIds = cleared.revisionIds;
|
|
2867
|
+
nextRevisionId = cleared.nextRevisionId;
|
|
2868
|
+
}
|
|
2869
|
+
}
|
|
2870
|
+
const suggestionAttrs = suggestionId === null ? {} : {
|
|
2871
|
+
provenance: "suggested",
|
|
2872
|
+
suggestionId
|
|
2873
|
+
};
|
|
2874
|
+
const revisionAttrs = {
|
|
2875
|
+
author,
|
|
2876
|
+
date,
|
|
2877
|
+
...initials ? { initials } : {},
|
|
2878
|
+
...suggestionAttrs
|
|
2879
|
+
};
|
|
2880
|
+
const deletion = deletionType.create({
|
|
2881
|
+
revisionId: revisionIdDelete,
|
|
2882
|
+
...revisionAttrs
|
|
2883
|
+
});
|
|
2884
|
+
const insertion = insertionType.create({
|
|
2885
|
+
revisionId: revisionIdInsert,
|
|
2886
|
+
...revisionAttrs
|
|
2887
|
+
});
|
|
2888
|
+
const insertedMarks = (marks) => marks.filter((mark) => !REVISION_MARK_NAMES.has(mark.type.name) && mark.type.name !== "runPropertyChange" && !(clearsBackground && BACKGROUND_MARK_NAMES.has(mark.type.name)));
|
|
2889
|
+
for (const change of plan.planned.toReversed()) {
|
|
2890
|
+
const doc = nextTr.doc;
|
|
2891
|
+
let at;
|
|
2892
|
+
let marks;
|
|
2893
|
+
if (change.insertion !== void 0) {
|
|
2894
|
+
at = change.insertion.at;
|
|
2895
|
+
marks = change.insertion.marks(doc);
|
|
2896
|
+
} else {
|
|
2897
|
+
const first = change.pieces.at(0);
|
|
2898
|
+
const last = change.pieces.at(-1);
|
|
2899
|
+
const styleSource = plan.replacementStyleSource(change.change);
|
|
2900
|
+
if (first === void 0 || last === void 0 || styleSource === null) panic("A planned replacement lost the characters it removes");
|
|
2901
|
+
at = last.to;
|
|
2902
|
+
marks = addCarriedMarks(inheritedReplacementMarks(doc, styleSource.from, styleSource.to), surveyReplacedAnnotations(doc, first.from, last.to).carried);
|
|
2903
|
+
for (const piece of change.pieces) {
|
|
2904
|
+
nextTr = nextTr.addMark(piece.from, piece.to, deletion);
|
|
2905
|
+
if (commentMark) nextTr = nextTr.addMark(piece.from, piece.to, commentMark);
|
|
2906
|
+
}
|
|
2907
|
+
}
|
|
2908
|
+
if (change.text.length === 0) continue;
|
|
2909
|
+
const nodes = cleanTextInlineNodes({
|
|
2910
|
+
schema,
|
|
2911
|
+
text: change.text,
|
|
2912
|
+
marks: insertedMarks(marks)
|
|
2913
|
+
});
|
|
2914
|
+
nextTr = nextTr.insert(at, nodes);
|
|
2915
|
+
const end = at + Fragment.fromArray(nodes).size;
|
|
2916
|
+
nextTr = nextTr.addMark(at, end, insertion);
|
|
2917
|
+
if (commentMark) nextTr = nextTr.addMark(at, end, commentMark);
|
|
2918
|
+
}
|
|
2919
|
+
return {
|
|
2920
|
+
type: "applied",
|
|
2921
|
+
transaction: nextTr,
|
|
2922
|
+
backgroundRevisionIds,
|
|
2923
|
+
nextRevisionId
|
|
2924
|
+
};
|
|
2925
|
+
};
|
|
2926
|
+
/**
|
|
2927
|
+
* The clean-text characters of every background stretch a change touches,
|
|
2928
|
+
* within the matched span.
|
|
2929
|
+
*
|
|
2930
|
+
* Text written onto a highlighted value is new text, so the marker saying
|
|
2931
|
+
* "fill this in" must not survive on it, and changing one digit of a
|
|
2932
|
+
* highlighted amount still fills the whole amount. Background elsewhere in the
|
|
2933
|
+
* match is left alone.
|
|
2934
|
+
*/
|
|
2935
|
+
const touchedBackgroundIndices = ({ doc, plan: { changes, spanStart, spanEnd, unitAt } }) => {
|
|
2936
|
+
const hasBackground = (index) => {
|
|
2937
|
+
const unit = unitAt(index);
|
|
2938
|
+
return (unit === null ? null : doc.nodeAt(unit.from))?.marks.some((mark) => BACKGROUND_MARK_NAMES.has(mark.type.name)) ?? false;
|
|
2939
|
+
};
|
|
2940
|
+
const touched = /* @__PURE__ */ new Set();
|
|
2941
|
+
for (const change of changes) {
|
|
2942
|
+
const start = spanStart + change.start;
|
|
2943
|
+
const end = spanStart + change.end;
|
|
2944
|
+
const touchedFrom = start === end ? Math.max(start - 1, spanStart) : start;
|
|
2945
|
+
const touchedTo = start === end ? Math.min(end + 1, spanEnd) : end;
|
|
2946
|
+
for (let index = touchedFrom; index < touchedTo; index++) {
|
|
2947
|
+
if (touched.has(index) || !hasBackground(index)) continue;
|
|
2948
|
+
touched.add(index);
|
|
2949
|
+
for (let left = index - 1; left >= spanStart && hasBackground(left); left--) touched.add(left);
|
|
2950
|
+
for (let right = index + 1; right < spanEnd && hasBackground(right); right++) touched.add(right);
|
|
2951
|
+
}
|
|
2952
|
+
}
|
|
2953
|
+
return touched;
|
|
2954
|
+
};
|
|
2955
|
+
/** The document ranges owning clean characters `indices`, merged where they meet. */
|
|
2956
|
+
const unitRanges = (indices, unitAt) => {
|
|
2957
|
+
const ranges = [];
|
|
2958
|
+
for (const index of [...indices].toSorted((a, b) => a - b)) {
|
|
2959
|
+
const unit = unitAt(index);
|
|
2960
|
+
if (unit === null) continue;
|
|
2961
|
+
const last = ranges.at(-1);
|
|
2962
|
+
if (last !== void 0 && unit.from <= last.to) last.to = Math.max(last.to, unit.to);
|
|
2963
|
+
else ranges.push({ ...unit });
|
|
2964
|
+
}
|
|
2965
|
+
return ranges;
|
|
2966
|
+
};
|
|
2967
|
+
/**
|
|
2968
|
+
* Clear the highlight and shading of `ranges` (see
|
|
2969
|
+
* {@link touchedBackgroundIndices}) before a direct change is written. Only
|
|
2970
|
+
* marks change, so no position moves.
|
|
2971
|
+
*/
|
|
2972
|
+
const clearTouchedBackground = ({ tr, ranges, styleResolver }) => {
|
|
2973
|
+
let nextTr = tr;
|
|
2974
|
+
for (const range of ranges) nextTr = applyInlineFormatting({
|
|
2975
|
+
tr: nextTr,
|
|
2976
|
+
from: range.from,
|
|
2977
|
+
to: range.to,
|
|
2978
|
+
formatting: REPLACEMENT_BACKGROUND_CLEAR_FORMATTING,
|
|
2979
|
+
...styleResolver !== void 0 ? { styleResolver } : {}
|
|
2980
|
+
});
|
|
2981
|
+
return nextTr;
|
|
2982
|
+
};
|
|
2539
2983
|
const resolveStableBlock = ({ snapshot, blockId, liveBlocks, liveBlocksByParaId }) => {
|
|
2540
2984
|
const anchor = snapshot.anchors[blockId];
|
|
2541
2985
|
if (!anchor) return {
|