@stll/folio-core 0.36.0 → 0.37.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/README.md +59 -1
- package/dist/ai-edits/apply.js +545 -296
- package/dist/ai-edits/block-identity.d.ts +4 -1
- package/dist/ai-edits/block-identity.js +4 -1
- package/dist/ai-edits/blockRange.js +14 -14
- package/dist/ai-edits/clean-text.d.ts +40 -1
- package/dist/ai-edits/clean-text.js +52 -2
- package/dist/ai-edits/headless.d.ts +3 -1
- package/dist/ai-edits/headless.js +133 -71
- package/dist/ai-edits/index.d.ts +3 -3
- package/dist/ai-edits/index.js +2 -2
- package/dist/ai-edits/read.js +38 -20
- package/dist/ai-edits/snapshot.d.ts +14 -6
- package/dist/ai-edits/snapshot.js +99 -19
- package/dist/ai-edits/table-cell-mutations.js +1 -1
- package/dist/ai-edits/table-row-column-mutations.js +7 -0
- package/dist/ai-edits/table-template.d.ts +11 -1
- package/dist/ai-edits/table-template.js +49 -17
- package/dist/ai-edits/types.d.ts +45 -57
- package/dist/ai-suggestions/conflict.js +1 -4
- package/dist/ai-suggestions/text-positions.d.ts +8 -0
- package/dist/ai-suggestions/text-positions.js +27 -5
- package/dist/compare/__fixtures__/body-sequence.js +3 -4
- package/dist/compare/column-alignment.d.ts +13 -13
- package/dist/compare/column-alignment.js +78 -39
- package/dist/compare/compare.d.ts +4 -3
- package/dist/compare/compare.js +41 -5
- package/dist/compare/content-alignment.d.ts +95 -0
- package/dist/compare/content-alignment.js +1450 -0
- package/dist/compare/content-types.d.ts +100 -0
- package/dist/compare/content-types.js +0 -0
- package/dist/compare/content.d.ts +209 -0
- package/dist/compare/content.js +885 -0
- package/dist/compare/formatting.d.ts +5 -5
- package/dist/compare/formatting.js +6 -4
- package/dist/compare/plan.d.ts +6 -1
- package/dist/compare/plan.js +169 -567
- package/dist/compare/scenario.d.ts +2 -2
- package/dist/compare/types.d.ts +3 -3
- package/dist/compare/verification.d.ts +2 -2
- package/dist/compare/verification.js +25 -8
- package/dist/compat/eigenpal.d.ts +5 -3
- package/dist/compat/eigenpal.js +3 -2
- package/dist/controller/headerFooterEditorManager.js +14 -6
- package/dist/controller/hiddenEditorManager.d.ts +7 -2
- package/dist/controller/hiddenEditorManager.js +15 -6
- package/dist/controller/layoutPipeline.js +5 -3
- package/dist/controller/noteEditorManager.js +12 -7
- package/dist/display-list/build/headerFooterPrimitives.js +1 -0
- package/dist/display-list/build/storyPrimitives.d.ts +5 -2
- package/dist/display-list/build/storyPrimitives.js +60 -3
- package/dist/display-list/build/tablePrimitives.js +30 -0
- package/dist/display-list/primitives.d.ts +1 -1
- package/dist/document-operations.d.ts +6 -4
- package/dist/document-operations.js +77 -12
- package/dist/docx/blockContentParser.js +6 -27
- package/dist/docx/ensureParaIds.js +9 -4
- package/dist/docx/numberingParser.d.ts +3 -1
- package/dist/docx/numberingParser.js +4 -1
- package/dist/docx/paragraphParser.js +7 -6
- package/dist/docx/paragraphTraversal.js +30 -34
- package/dist/docx/rezip.d.ts +0 -8
- package/dist/docx/rezip.js +41 -16
- package/dist/docx/runParser.js +4 -2
- package/dist/docx/serializer/paragraphSerializer.js +2 -0
- package/dist/docx/serializer/runSerializer.js +5 -3
- package/dist/docx/serializer/trackedChangeAttributes.d.ts +3 -1
- package/dist/docx/serializer/trackedChangeAttributes.js +11 -1
- package/dist/docx/server/build.d.ts +1 -1
- package/dist/headless-layout.js +5 -4
- package/dist/index.d.ts +5 -3
- package/dist/index.js +3 -2
- package/dist/internal/headlessRevisionResolution.d.ts +23 -0
- package/dist/internal/headlessRevisionResolution.js +155 -0
- package/dist/internal/headlessRevisionResolutionGuard.d.ts +12 -0
- package/dist/internal/headlessRevisionResolutionGuard.js +28 -0
- package/dist/internal/pageBreakRunDescendantIndex.d.ts +9 -0
- package/dist/internal/pageBreakRunDescendantIndex.js +27 -0
- package/dist/internal/pageBreakRunPartition.d.ts +26 -0
- package/dist/internal/pageBreakRunPartition.js +35 -0
- package/dist/internal/pageBreakRunSourceDescendantIndex.d.ts +15 -0
- package/dist/internal/pageBreakRunSourceDescendantIndex.js +126 -0
- package/dist/internal/sectionEndpointResolution.d.ts +32 -0
- package/dist/internal/sectionEndpointResolution.js +38 -0
- package/dist/layout-bridge/convert/footnoteLayout.js +1 -0
- package/dist/layout-bridge/convert/headerFooterLayout.d.ts +16 -4
- package/dist/layout-bridge/convert/headerFooterLayout.js +48 -5
- package/dist/layout-bridge/convert/toFlowBlocks.d.ts +4 -0
- package/dist/layout-bridge/convert/toFlowBlocks.js +379 -75
- package/dist/layout-bridge/dom/clickToPositionDom.d.ts +8 -1
- package/dist/layout-bridge/dom/clickToPositionDom.js +114 -40
- package/dist/layout-bridge/dom/textStreamDom.d.ts +24 -0
- package/dist/layout-bridge/dom/textStreamDom.js +75 -0
- package/dist/layout-bridge/headerFooterLayout.js +16 -30
- package/dist/layout-engine/index.js +12 -3
- package/dist/layout-engine/measure/tableFragmentBorderGeometry.d.ts +17 -0
- package/dist/layout-engine/measure/tableFragmentBorderGeometry.js +24 -0
- package/dist/layout-engine/renderedBreakReconciliation.js +1 -1
- package/dist/layout-engine/types.d.ts +7 -0
- package/dist/layout-painter/imageLayout.d.ts +1 -1
- package/dist/layout-painter/renderTable.js +20 -0
- package/dist/model.d.ts +1 -1
- package/dist/paged-layout/rangeProjection.js +6 -9
- package/dist/paged-layout/transactionDirtyRange.d.ts +3 -1
- package/dist/paged-layout/transactionDirtyRange.js +23 -1
- package/dist/prosemirror/attrs/index.d.ts +7 -3
- package/dist/prosemirror/attrs/index.js +113 -7
- package/dist/prosemirror/bookmarkBoundaryIntegrity.d.ts +9 -0
- package/dist/prosemirror/bookmarkBoundaryIntegrity.js +22 -0
- package/dist/prosemirror/commands/comments.d.ts +10 -1
- package/dist/prosemirror/commands/comments.js +190 -59
- package/dist/prosemirror/commands/formatPainter.d.ts +45 -10
- package/dist/prosemirror/commands/formatPainter.js +319 -63
- package/dist/prosemirror/commands/index.d.ts +2 -2
- package/dist/prosemirror/commands/pageBreak.d.ts +5 -2
- package/dist/prosemirror/commands/pageBreak.js +18 -36
- package/dist/prosemirror/commands/propertyChangeScope.d.ts +7 -5
- package/dist/prosemirror/commands/propertyChangeScope.js +27 -18
- package/dist/prosemirror/conversion/fromProseDoc.d.ts +7 -2
- package/dist/prosemirror/conversion/fromProseDoc.js +809 -209
- package/dist/prosemirror/conversion/runShadingMark.js +2 -2
- package/dist/prosemirror/conversion/toProseDoc.d.ts +26 -1
- package/dist/prosemirror/conversion/toProseDoc.js +416 -283
- package/dist/prosemirror/extensions/StarterKit.js +4 -0
- package/dist/prosemirror/extensions/core/ParagraphExtension.js +12 -8
- package/dist/prosemirror/extensions/features/ListExtension.js +95 -123
- package/dist/prosemirror/extensions/features/ParaIdAllocatorExtension.js +1 -1
- package/dist/prosemirror/extensions/features/ParagraphChangeTrackerExtension.d.ts +27 -1
- package/dist/prosemirror/extensions/features/ParagraphChangeTrackerExtension.js +134 -30
- package/dist/prosemirror/extensions/features/pasteCleanup.js +9 -15
- package/dist/prosemirror/extensions/marks/BoldExtension.js +4 -3
- package/dist/prosemirror/extensions/marks/CharacterStyleExtension.d.ts +4 -7
- package/dist/prosemirror/extensions/marks/CharacterStyleExtension.js +5 -11
- package/dist/prosemirror/extensions/marks/FontSizeExtension.js +3 -3
- package/dist/prosemirror/extensions/marks/ItalicExtension.js +4 -3
- package/dist/prosemirror/extensions/marks/PageBreakRunOwnerExtension.d.ts +13 -0
- package/dist/prosemirror/extensions/marks/PageBreakRunOwnerExtension.js +39 -0
- package/dist/prosemirror/extensions/marks/RunFormattingOverrideExtension.d.ts +7 -1
- package/dist/prosemirror/extensions/marks/RunFormattingOverrideExtension.js +67 -21
- package/dist/prosemirror/extensions/marks/markUtils.d.ts +8 -1
- package/dist/prosemirror/extensions/marks/markUtils.js +88 -2
- package/dist/prosemirror/extensions/nodes/BookmarkBoundaryExtension.js +44 -3
- package/dist/prosemirror/extensions/nodes/FieldExtension.js +1 -1
- package/dist/prosemirror/extensions/nodes/HardBreakExtension.js +15 -4
- package/dist/prosemirror/extensions/nodes/PageBreakExtension.d.ts +6 -0
- package/dist/prosemirror/extensions/nodes/PageBreakExtension.js +6 -0
- package/dist/prosemirror/extensions/nodes/PageBreakRunExtension.d.ts +6 -0
- package/dist/prosemirror/extensions/nodes/PageBreakRunExtension.js +36 -0
- package/dist/prosemirror/findReplaceSelection.js +8 -3
- package/dist/prosemirror/index.d.ts +3 -3
- package/dist/prosemirror/listMarker.d.ts +26 -1
- package/dist/prosemirror/listMarker.js +108 -14
- package/dist/prosemirror/pageBreakRunProjection.d.ts +28 -0
- package/dist/prosemirror/pageBreakRunProjection.js +47 -0
- package/dist/prosemirror/paraText.js +6 -0
- package/dist/prosemirror/paragraphSpacing.d.ts +29 -0
- package/dist/prosemirror/paragraphSpacing.js +125 -0
- package/dist/prosemirror/plugins/documentNumbering.d.ts +8 -0
- package/dist/prosemirror/plugins/documentNumbering.js +18 -0
- package/dist/prosemirror/plugins/pmTextScan.d.ts +3 -2
- package/dist/prosemirror/plugins/pmTextScan.js +14 -4
- package/dist/prosemirror/plugins/suggestionMode.js +8 -10
- package/dist/prosemirror/rebaseParagraphRunFormatting.d.ts +16 -0
- package/dist/prosemirror/rebaseParagraphRunFormatting.js +61 -0
- package/dist/prosemirror/runFormattingInlineCarriers.d.ts +64 -0
- package/dist/prosemirror/runFormattingInlineCarriers.js +121 -0
- package/dist/prosemirror/runFormattingProvenance.d.ts +12 -0
- package/dist/prosemirror/runFormattingProvenance.js +48 -0
- package/dist/prosemirror/runFormattingReconciliation.d.ts +25 -0
- package/dist/prosemirror/runFormattingReconciliation.js +54 -0
- package/dist/prosemirror/runPropertyChangeResolution.d.ts +18 -0
- package/dist/prosemirror/runPropertyChangeResolution.js +18 -0
- package/dist/prosemirror/runStyleFormatting.d.ts +50 -0
- package/dist/prosemirror/runStyleFormatting.js +105 -0
- package/dist/prosemirror/schema/index.d.ts +3 -3
- package/dist/prosemirror/schema/marks.d.ts +82 -9
- package/dist/prosemirror/schema/marks.js +73 -1
- package/dist/prosemirror/schema/nodes.d.ts +18 -5
- package/dist/prosemirror/styles/resolvedStyleAttrs.d.ts +14 -2
- package/dist/prosemirror/styles/resolvedStyleAttrs.js +44 -23
- package/dist/prosemirror/styles/styleResolver.d.ts +10 -13
- package/dist/prosemirror/styles/styleResolver.js +3 -3
- package/dist/prosemirror/styles/styleToggleCascade.d.ts +8 -1
- package/dist/prosemirror/styles/styleToggleCascade.js +24 -1
- package/dist/prosemirror/tableGridMutation.d.ts +20 -0
- package/dist/prosemirror/tableGridMutation.js +30 -0
- package/dist/prosemirror/trackedRunInlineAtoms.d.ts +31 -0
- package/dist/prosemirror/trackedRunInlineAtoms.js +39 -0
- package/dist/prosemirror/utils/visualLineNavigation.js +27 -31
- package/dist/prosemirror/validation.js +23 -4
- package/dist/render-dom/HeaderFooterSelectionOverlay.js +20 -20
- package/dist/render-dom/RenderedDomContext.js +17 -34
- package/dist/server.d.ts +4 -4
- package/dist/server.js +2 -2
- package/dist/style-engine/styleEngine.d.ts +1 -1
- package/dist/utils/clipboard.js +1 -1
- package/dist/utils/findReplace.js +38 -21
- package/dist/version-comparison.d.ts +38 -44
- package/dist/version-comparison.js +270 -401
- package/package.json +3 -2
|
@@ -0,0 +1,1450 @@
|
|
|
1
|
+
import { alignTableColumns } from "./column-alignment.js";
|
|
2
|
+
import { panic } from "better-result";
|
|
3
|
+
//#region src/compare/content-alignment.ts
|
|
4
|
+
/**
|
|
5
|
+
* Pure alignment for representation-neutral content snapshots.
|
|
6
|
+
*
|
|
7
|
+
* The block walk is split by structural container before any textual
|
|
8
|
+
* alignment runs. Body blocks are compared with body blocks; tables are
|
|
9
|
+
* aligned table by table, then row by row and cell by cell. All quadratic
|
|
10
|
+
* text alignment in one caller-owned work session shares one hard budget.
|
|
11
|
+
*/
|
|
12
|
+
const MAX_CONTENT_ALIGNMENT_LCS_CELLS = 4e6;
|
|
13
|
+
const MAX_CONTENT_STRUCTURE_TOKEN_LOOKUPS = 4e6;
|
|
14
|
+
const createFolioContentAlignmentWorkSession = (options = {}) => {
|
|
15
|
+
const lcsCells = options.lcsCells ?? MAX_CONTENT_ALIGNMENT_LCS_CELLS;
|
|
16
|
+
const structuralTokenLookups = options.structuralTokenLookups ?? MAX_CONTENT_STRUCTURE_TOKEN_LOOKUPS;
|
|
17
|
+
if (!Number.isSafeInteger(lcsCells) || lcsCells < 0 || lcsCells > MAX_CONTENT_ALIGNMENT_LCS_CELLS) return panic("A content-alignment LCS allowance is outside the supported range", { lcsCells });
|
|
18
|
+
if (!Number.isSafeInteger(structuralTokenLookups) || structuralTokenLookups < 0 || structuralTokenLookups > MAX_CONTENT_STRUCTURE_TOKEN_LOOKUPS) return panic("A content-alignment structural token allowance is outside the supported range", { structuralTokenLookups });
|
|
19
|
+
return {
|
|
20
|
+
remainingLcsCells: lcsCells,
|
|
21
|
+
remainingStructuralTokenLookups: structuralTokenLookups
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
const exceedsFolioContentLcsBudget = (baseCount, revisedCount) => baseCount > 0 && revisedCount > 0 && baseCount > Math.floor(MAX_CONTENT_ALIGNMENT_LCS_CELLS / revisedCount);
|
|
25
|
+
const claimFolioContentAlignmentCells = (baseCount, revisedCount, workSession) => {
|
|
26
|
+
if (baseCount === 0 || revisedCount === 0) return true;
|
|
27
|
+
if (exceedsFolioContentLcsBudget(baseCount, revisedCount) || baseCount > Math.floor(workSession.remainingLcsCells / revisedCount)) return false;
|
|
28
|
+
workSession.remainingLcsCells -= baseCount * revisedCount;
|
|
29
|
+
return true;
|
|
30
|
+
};
|
|
31
|
+
const folioContentIdStability = (block) => block.idStability ?? "stable";
|
|
32
|
+
/**
|
|
33
|
+
* Longest strictly increasing subsequence by revised index.
|
|
34
|
+
*
|
|
35
|
+
* The Fenwick tree stores the earliest end index for equal-length candidates,
|
|
36
|
+
* preserving the historical deterministic tie break while reducing the
|
|
37
|
+
* stable-anchor pass from quadratic to O(n log n).
|
|
38
|
+
*/
|
|
39
|
+
const longestIncreasingFolioContentPairs = (pairs) => {
|
|
40
|
+
if (pairs.length === 0) return [];
|
|
41
|
+
const coordinates = [...new Set(pairs.map(({ revisedIndex }) => revisedIndex))].toSorted((left, right) => left - right);
|
|
42
|
+
const rankByCoordinate = new Map(coordinates.map((coordinate, index) => [coordinate, index + 1]));
|
|
43
|
+
const treeLengths = new Int32Array(coordinates.length + 1);
|
|
44
|
+
const treeIndexes = new Int32Array(coordinates.length + 1).fill(-1);
|
|
45
|
+
const lengths = new Int32Array(pairs.length).fill(1);
|
|
46
|
+
const predecessors = new Int32Array(pairs.length).fill(-1);
|
|
47
|
+
const earlier = (leftLength, leftIndex, rightLength, rightIndex) => leftLength > rightLength || leftLength === rightLength && leftLength > 0 && leftIndex < rightIndex;
|
|
48
|
+
let bestEnd = 0;
|
|
49
|
+
let groupStart = 0;
|
|
50
|
+
while (groupStart < pairs.length) {
|
|
51
|
+
const baseIndex = pairs[groupStart]?.baseIndex;
|
|
52
|
+
let groupEnd = groupStart + 1;
|
|
53
|
+
while (groupEnd < pairs.length && pairs[groupEnd]?.baseIndex === baseIndex) groupEnd += 1;
|
|
54
|
+
for (let pairIndex = groupStart; pairIndex < groupEnd; pairIndex++) {
|
|
55
|
+
const pair = pairs[pairIndex];
|
|
56
|
+
const rank = pair === void 0 ? void 0 : rankByCoordinate.get(pair.revisedIndex);
|
|
57
|
+
if (rank === void 0) continue;
|
|
58
|
+
let predecessorLength = 0;
|
|
59
|
+
let predecessorIndex = -1;
|
|
60
|
+
for (let cursor = rank - 1; cursor > 0; cursor -= cursor & -cursor) {
|
|
61
|
+
const candidateLength = treeLengths[cursor] ?? 0;
|
|
62
|
+
const candidateIndex = treeIndexes[cursor] ?? -1;
|
|
63
|
+
if (earlier(candidateLength, candidateIndex, predecessorLength, predecessorIndex)) {
|
|
64
|
+
predecessorLength = candidateLength;
|
|
65
|
+
predecessorIndex = candidateIndex;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
lengths[pairIndex] = predecessorLength + 1;
|
|
69
|
+
predecessors[pairIndex] = predecessorIndex;
|
|
70
|
+
if ((lengths[pairIndex] ?? 0) > (lengths[bestEnd] ?? 0)) bestEnd = pairIndex;
|
|
71
|
+
}
|
|
72
|
+
for (let pairIndex = groupStart; pairIndex < groupEnd; pairIndex++) {
|
|
73
|
+
const pair = pairs[pairIndex];
|
|
74
|
+
const rank = pair === void 0 ? void 0 : rankByCoordinate.get(pair.revisedIndex);
|
|
75
|
+
if (rank === void 0) continue;
|
|
76
|
+
for (let cursor = rank; cursor < treeLengths.length; cursor += cursor & -cursor) {
|
|
77
|
+
const currentLength = treeLengths[cursor] ?? 0;
|
|
78
|
+
const currentIndex = treeIndexes[cursor] ?? -1;
|
|
79
|
+
if (earlier(lengths[pairIndex] ?? 0, pairIndex, currentLength, currentIndex)) {
|
|
80
|
+
treeLengths[cursor] = lengths[pairIndex] ?? 0;
|
|
81
|
+
treeIndexes[cursor] = pairIndex;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
groupStart = groupEnd;
|
|
86
|
+
}
|
|
87
|
+
const ordered = [];
|
|
88
|
+
for (let cursor = bestEnd; cursor !== -1; cursor = predecessors[cursor] ?? -1) {
|
|
89
|
+
const pair = pairs[cursor];
|
|
90
|
+
if (pair) ordered.push(pair);
|
|
91
|
+
}
|
|
92
|
+
return ordered.toReversed();
|
|
93
|
+
};
|
|
94
|
+
const pairByStableId = (base, revised, idStability) => {
|
|
95
|
+
const revisedIndexById = /* @__PURE__ */ new Map();
|
|
96
|
+
revised.forEach((block, revisedIndex) => {
|
|
97
|
+
if (idStability(block) === "stable") revisedIndexById.set(block.id, revisedIndex);
|
|
98
|
+
});
|
|
99
|
+
const candidates = [];
|
|
100
|
+
base.forEach((block, baseIndex) => {
|
|
101
|
+
if (idStability(block) !== "stable") return;
|
|
102
|
+
const revisedIndex = revisedIndexById.get(block.id);
|
|
103
|
+
if (revisedIndex !== void 0) candidates.push({
|
|
104
|
+
baseIndex,
|
|
105
|
+
revisedIndex
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
return longestIncreasingFolioContentPairs(candidates);
|
|
109
|
+
};
|
|
110
|
+
/**
|
|
111
|
+
* Pair ids that became stable when one side was serialized and reopened.
|
|
112
|
+
*
|
|
113
|
+
* An adapter can mark a synthesized id positional in the live snapshot that
|
|
114
|
+
* minted it, then stable after that same id is persisted. This evidence is
|
|
115
|
+
* deliberately weaker than a stable id: it is considered only inside a gap
|
|
116
|
+
* already bounded by stable/exact anchors, so it cannot turn a positional id
|
|
117
|
+
* into a move or pull a block across an established correspondence.
|
|
118
|
+
*/
|
|
119
|
+
const pairByResidualIdContinuity = ({ baseBlocks, revisedBlocks, baseFrom, baseTo, revisedFrom, revisedTo, idStability }) => {
|
|
120
|
+
const uniqueIndexesById = ({ blocks, from, to }) => {
|
|
121
|
+
const indexes = /* @__PURE__ */ new Map();
|
|
122
|
+
for (let index = from; index < to; index++) {
|
|
123
|
+
const id = blocks[index]?.id;
|
|
124
|
+
if (id === void 0) continue;
|
|
125
|
+
indexes.set(id, indexes.has(id) ? null : index);
|
|
126
|
+
}
|
|
127
|
+
return indexes;
|
|
128
|
+
};
|
|
129
|
+
const baseIndexes = uniqueIndexesById({
|
|
130
|
+
blocks: baseBlocks,
|
|
131
|
+
from: baseFrom,
|
|
132
|
+
to: baseTo
|
|
133
|
+
});
|
|
134
|
+
const revisedIndexes = uniqueIndexesById({
|
|
135
|
+
blocks: revisedBlocks,
|
|
136
|
+
from: revisedFrom,
|
|
137
|
+
to: revisedTo
|
|
138
|
+
});
|
|
139
|
+
const candidates = [];
|
|
140
|
+
for (const [id, baseIndex] of baseIndexes) {
|
|
141
|
+
const revisedIndex = revisedIndexes.get(id);
|
|
142
|
+
if (baseIndex === null || revisedIndex === void 0 || revisedIndex === null) continue;
|
|
143
|
+
const baseBlock = baseBlocks[baseIndex];
|
|
144
|
+
const revisedBlock = revisedBlocks[revisedIndex];
|
|
145
|
+
if (!baseBlock || !revisedBlock) continue;
|
|
146
|
+
if (idStability(baseBlock) === idStability(revisedBlock)) continue;
|
|
147
|
+
candidates.push({
|
|
148
|
+
baseIndex,
|
|
149
|
+
revisedIndex
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
return longestIncreasingFolioContentPairs(candidates);
|
|
153
|
+
};
|
|
154
|
+
const pairByExactText = (base, revised, workSession, idStability, stableIdMismatch) => {
|
|
155
|
+
const baseCount = base.length;
|
|
156
|
+
const revisedCount = revised.length;
|
|
157
|
+
if (baseCount === 0 || revisedCount === 0) return [];
|
|
158
|
+
if (!claimFolioContentAlignmentCells(baseCount, revisedCount, workSession)) return [];
|
|
159
|
+
const textKeys = /* @__PURE__ */ new Map();
|
|
160
|
+
let nextTextKey = 0;
|
|
161
|
+
const internText = (text) => {
|
|
162
|
+
const existing = textKeys.get(text);
|
|
163
|
+
if (existing !== void 0) return existing;
|
|
164
|
+
const key = nextTextKey++;
|
|
165
|
+
textKeys.set(text, key);
|
|
166
|
+
return key;
|
|
167
|
+
};
|
|
168
|
+
const baseTextKeys = base.map(({ block }) => internText(block.text));
|
|
169
|
+
const revisedTextKeys = revised.map(({ block }) => internText(block.text));
|
|
170
|
+
const entriesCanPair = (baseIndex, revisedIndex) => {
|
|
171
|
+
const baseBlock = base[baseIndex]?.block;
|
|
172
|
+
const revisedBlock = revised[revisedIndex]?.block;
|
|
173
|
+
if (!baseBlock || !revisedBlock || baseTextKeys[baseIndex] !== revisedTextKeys[revisedIndex]) return false;
|
|
174
|
+
return stableIdMismatch === "pair" || !(idStability(baseBlock) === "stable" && idStability(revisedBlock) === "stable" && baseBlock.id !== revisedBlock.id);
|
|
175
|
+
};
|
|
176
|
+
const stride = revisedCount + 1;
|
|
177
|
+
const lengths = new Int32Array((baseCount + 1) * stride);
|
|
178
|
+
for (let baseIndex = baseCount - 1; baseIndex >= 0; baseIndex--) {
|
|
179
|
+
const rowOffset = baseIndex * stride;
|
|
180
|
+
const nextRowOffset = (baseIndex + 1) * stride;
|
|
181
|
+
for (let revisedIndex = revisedCount - 1; revisedIndex >= 0; revisedIndex--) lengths[rowOffset + revisedIndex] = entriesCanPair(baseIndex, revisedIndex) ? (lengths[nextRowOffset + revisedIndex + 1] ?? 0) + 1 : Math.max(lengths[nextRowOffset + revisedIndex] ?? 0, lengths[rowOffset + revisedIndex + 1] ?? 0);
|
|
182
|
+
}
|
|
183
|
+
const pairs = [];
|
|
184
|
+
let baseIndex = 0;
|
|
185
|
+
let revisedIndex = 0;
|
|
186
|
+
while (baseIndex < baseCount && revisedIndex < revisedCount) {
|
|
187
|
+
const baseEntry = base[baseIndex];
|
|
188
|
+
const revisedEntry = revised[revisedIndex];
|
|
189
|
+
if (!baseEntry || !revisedEntry) break;
|
|
190
|
+
if (entriesCanPair(baseIndex, revisedIndex)) {
|
|
191
|
+
pairs.push({
|
|
192
|
+
baseIndex: baseEntry.index,
|
|
193
|
+
revisedIndex: revisedEntry.index
|
|
194
|
+
});
|
|
195
|
+
baseIndex += 1;
|
|
196
|
+
revisedIndex += 1;
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
if ((lengths[(baseIndex + 1) * stride + revisedIndex] ?? 0) >= (lengths[baseIndex * stride + revisedIndex + 1] ?? 0)) baseIndex += 1;
|
|
200
|
+
else revisedIndex += 1;
|
|
201
|
+
}
|
|
202
|
+
return pairs;
|
|
203
|
+
};
|
|
204
|
+
const alignFolioContentBlocks = (baseBlocks, revisedBlocks, options = {}) => {
|
|
205
|
+
const workSession = options.workSession ?? createFolioContentAlignmentWorkSession();
|
|
206
|
+
const stableIdMismatch = options.stableIdMismatch ?? "separate";
|
|
207
|
+
const idStability = options.idStability ?? folioContentIdStability;
|
|
208
|
+
const stableIdAnchors = pairByStableId(baseBlocks, revisedBlocks, idStability);
|
|
209
|
+
const usedBaseIndexes = new Set(stableIdAnchors.map(({ baseIndex }) => baseIndex));
|
|
210
|
+
const usedRevisedIndexes = new Set(stableIdAnchors.map(({ revisedIndex }) => revisedIndex));
|
|
211
|
+
const baseRemaining = baseBlocks.flatMap((block, index) => usedBaseIndexes.has(index) ? [] : [{
|
|
212
|
+
block,
|
|
213
|
+
index
|
|
214
|
+
}]);
|
|
215
|
+
const revisedRemaining = revisedBlocks.flatMap((block, index) => usedRevisedIndexes.has(index) ? [] : [{
|
|
216
|
+
block,
|
|
217
|
+
index
|
|
218
|
+
}]);
|
|
219
|
+
const exactTextAnchors = pairByExactText(baseRemaining, revisedRemaining, workSession, idStability, stableIdMismatch);
|
|
220
|
+
const anchors = longestIncreasingFolioContentPairs([...stableIdAnchors, ...exactTextAnchors].toSorted((left, right) => left.baseIndex - right.baseIndex));
|
|
221
|
+
const events = [];
|
|
222
|
+
const emitPositionalGap = (baseFrom, baseTo, revisedFrom, revisedTo) => {
|
|
223
|
+
const pairedCount = Math.min(baseTo - baseFrom, revisedTo - revisedFrom);
|
|
224
|
+
for (let offset = 0; offset < pairedCount; offset++) {
|
|
225
|
+
const baseBlock = baseBlocks[baseFrom + offset];
|
|
226
|
+
const revisedBlock = revisedBlocks[revisedFrom + offset];
|
|
227
|
+
if (baseBlock && revisedBlock) if (idStability(baseBlock) === "stable" && idStability(revisedBlock) === "stable" && baseBlock.id !== revisedBlock.id && stableIdMismatch === "separate") {
|
|
228
|
+
events.push({
|
|
229
|
+
type: "baseOnly",
|
|
230
|
+
block: baseBlock
|
|
231
|
+
});
|
|
232
|
+
events.push({
|
|
233
|
+
type: "revisedOnly",
|
|
234
|
+
block: revisedBlock
|
|
235
|
+
});
|
|
236
|
+
} else events.push({
|
|
237
|
+
type: "pair",
|
|
238
|
+
baseBlock,
|
|
239
|
+
revisedBlock
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
for (let index = baseFrom + pairedCount; index < baseTo; index++) {
|
|
243
|
+
const block = baseBlocks[index];
|
|
244
|
+
if (block) events.push({
|
|
245
|
+
type: "baseOnly",
|
|
246
|
+
block
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
for (let index = revisedFrom + pairedCount; index < revisedTo; index++) {
|
|
250
|
+
const block = revisedBlocks[index];
|
|
251
|
+
if (block) events.push({
|
|
252
|
+
type: "revisedOnly",
|
|
253
|
+
block
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
const emitGap = (baseFrom, baseTo, revisedFrom, revisedTo) => {
|
|
258
|
+
if (baseFrom === baseTo || revisedFrom === revisedTo) {
|
|
259
|
+
emitPositionalGap(baseFrom, baseTo, revisedFrom, revisedTo);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
const continuityPairs = pairByResidualIdContinuity({
|
|
263
|
+
baseBlocks,
|
|
264
|
+
revisedBlocks,
|
|
265
|
+
baseFrom,
|
|
266
|
+
baseTo,
|
|
267
|
+
revisedFrom,
|
|
268
|
+
revisedTo,
|
|
269
|
+
idStability
|
|
270
|
+
});
|
|
271
|
+
let baseCursor = baseFrom;
|
|
272
|
+
let revisedCursor = revisedFrom;
|
|
273
|
+
for (const pair of continuityPairs) {
|
|
274
|
+
emitPositionalGap(baseCursor, pair.baseIndex, revisedCursor, pair.revisedIndex);
|
|
275
|
+
const baseBlock = baseBlocks[pair.baseIndex];
|
|
276
|
+
const revisedBlock = revisedBlocks[pair.revisedIndex];
|
|
277
|
+
if (baseBlock && revisedBlock) events.push({
|
|
278
|
+
type: "pair",
|
|
279
|
+
baseBlock,
|
|
280
|
+
revisedBlock
|
|
281
|
+
});
|
|
282
|
+
baseCursor = pair.baseIndex + 1;
|
|
283
|
+
revisedCursor = pair.revisedIndex + 1;
|
|
284
|
+
}
|
|
285
|
+
emitPositionalGap(baseCursor, baseTo, revisedCursor, revisedTo);
|
|
286
|
+
};
|
|
287
|
+
let baseCursor = 0;
|
|
288
|
+
let revisedCursor = 0;
|
|
289
|
+
for (const anchor of anchors) {
|
|
290
|
+
emitGap(baseCursor, anchor.baseIndex, revisedCursor, anchor.revisedIndex);
|
|
291
|
+
const baseBlock = baseBlocks[anchor.baseIndex];
|
|
292
|
+
const revisedBlock = revisedBlocks[anchor.revisedIndex];
|
|
293
|
+
if (baseBlock && revisedBlock) events.push({
|
|
294
|
+
type: "pair",
|
|
295
|
+
baseBlock,
|
|
296
|
+
revisedBlock
|
|
297
|
+
});
|
|
298
|
+
baseCursor = anchor.baseIndex + 1;
|
|
299
|
+
revisedCursor = anchor.revisedIndex + 1;
|
|
300
|
+
}
|
|
301
|
+
emitGap(baseCursor, baseBlocks.length, revisedCursor, revisedBlocks.length);
|
|
302
|
+
return events;
|
|
303
|
+
};
|
|
304
|
+
const BODY_MOVE_BUCKET = 0;
|
|
305
|
+
const scopedAlignmentSteps = (events, context, bucketForBlock) => {
|
|
306
|
+
const steps = [];
|
|
307
|
+
let gap = context.nextGap++;
|
|
308
|
+
for (const event of events) {
|
|
309
|
+
if (event.type === "pair") {
|
|
310
|
+
steps.push(event);
|
|
311
|
+
gap = context.nextGap++;
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
steps.push({
|
|
315
|
+
...event,
|
|
316
|
+
moveScope: {
|
|
317
|
+
bucket: bucketForBlock(event.block),
|
|
318
|
+
gap
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
return steps;
|
|
323
|
+
};
|
|
324
|
+
const containerPathKeyOf = (block) => block.containerPath === void 0 || block.containerPath.length === 0 ? null : JSON.stringify(block.containerPath.map(({ kind, id }) => [kind, id]));
|
|
325
|
+
const contentBlocksShareContainerPath = (left, right) => containerPathKeyOf(left) === containerPathKeyOf(right);
|
|
326
|
+
const splitSegments = (blocks) => {
|
|
327
|
+
const segments = [];
|
|
328
|
+
let currentTableIndex = null;
|
|
329
|
+
for (const block of blocks) {
|
|
330
|
+
const tableIndex = block.table?.outerTableIndex ?? null;
|
|
331
|
+
const containerPathKey = block.table ? null : containerPathKeyOf(block);
|
|
332
|
+
const current = segments.at(-1);
|
|
333
|
+
if (current !== void 0 && currentTableIndex === tableIndex && current.containerPathKey === containerPathKey) {
|
|
334
|
+
current.blocks.push(block);
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
segments.push(block.table ? {
|
|
338
|
+
kind: "table",
|
|
339
|
+
blocks: [block],
|
|
340
|
+
containerPathKey: null,
|
|
341
|
+
structuralKey: JSON.stringify(["table", null])
|
|
342
|
+
} : {
|
|
343
|
+
kind: "body",
|
|
344
|
+
blocks: [block],
|
|
345
|
+
containerPathKey,
|
|
346
|
+
structuralKey: JSON.stringify(["body", containerPathKey])
|
|
347
|
+
});
|
|
348
|
+
currentTableIndex = tableIndex;
|
|
349
|
+
}
|
|
350
|
+
return segments;
|
|
351
|
+
};
|
|
352
|
+
const groupFolioContentTableRows = (blocks) => {
|
|
353
|
+
const rows = /* @__PURE__ */ new Map();
|
|
354
|
+
for (const block of blocks) {
|
|
355
|
+
if (!block.table) continue;
|
|
356
|
+
const key = `${String(block.table.tableIndex)}:${String(block.table.rowIndex)}`;
|
|
357
|
+
const row = rows.get(key);
|
|
358
|
+
if (row) row.push(block);
|
|
359
|
+
else rows.set(key, [block]);
|
|
360
|
+
}
|
|
361
|
+
return [...rows.values()];
|
|
362
|
+
};
|
|
363
|
+
const groupTables = (blocks) => {
|
|
364
|
+
const tables = /* @__PURE__ */ new Map();
|
|
365
|
+
for (const block of blocks) {
|
|
366
|
+
const tableIndex = block.table?.tableIndex;
|
|
367
|
+
if (tableIndex === void 0) continue;
|
|
368
|
+
const table = tables.get(tableIndex);
|
|
369
|
+
if (table) table.push(block);
|
|
370
|
+
else tables.set(tableIndex, [block]);
|
|
371
|
+
}
|
|
372
|
+
return [...tables.values()];
|
|
373
|
+
};
|
|
374
|
+
const CONTENT_STRUCTURE_PAIR_SIMILARITY = .5;
|
|
375
|
+
const CONTENT_STRUCTURE_SIMILARITY_SCALE = 1e3;
|
|
376
|
+
const MAX_CONTENT_STRUCTURE_PROFILE_BLOCKS = 128;
|
|
377
|
+
const MAX_CONTENT_STRUCTURE_PROFILE_TEXT_CODE_UNITS = 16384;
|
|
378
|
+
const MAX_CONTENT_STRUCTURE_PROFILE_TOKENS = 64;
|
|
379
|
+
const MAX_CONTENT_STRUCTURE_PROFILE_KIND_CODE_UNITS = 256;
|
|
380
|
+
const CONTENT_STRUCTURE_ANCHOR_MINIMUM_WORD_COUNT = 3;
|
|
381
|
+
const createContentStructureProfile = ({ blocks, blockStructure, idStability, physicalCellCount, retainContainerIdentity = false }) => {
|
|
382
|
+
let containerIdentity = null;
|
|
383
|
+
if (retainContainerIdentity) if (blocks.length > MAX_CONTENT_STRUCTURE_PROFILE_BLOCKS) containerIdentity = { status: "unavailable" };
|
|
384
|
+
else {
|
|
385
|
+
const blockIds = [];
|
|
386
|
+
const idStabilities = [];
|
|
387
|
+
for (const block of blocks) {
|
|
388
|
+
blockIds.push(block.id);
|
|
389
|
+
idStabilities.push(idStability(block));
|
|
390
|
+
}
|
|
391
|
+
containerIdentity = {
|
|
392
|
+
status: "available",
|
|
393
|
+
blockIds,
|
|
394
|
+
idStabilities
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
const profiledBlockCount = Math.min(blocks.length, MAX_CONTENT_STRUCTURE_PROFILE_BLOCKS);
|
|
398
|
+
const blockIds = [];
|
|
399
|
+
const stableIds = [];
|
|
400
|
+
for (let index = 0; index < profiledBlockCount; index++) {
|
|
401
|
+
const block = blocks[index];
|
|
402
|
+
if (block) {
|
|
403
|
+
blockIds.push(block.id);
|
|
404
|
+
if (idStability(block) === "stable") stableIds.push(block.id);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
if (blocks.length > MAX_CONTENT_STRUCTURE_PROFILE_BLOCKS) return {
|
|
408
|
+
anchorTexts: [],
|
|
409
|
+
blockIds,
|
|
410
|
+
containerIdentity,
|
|
411
|
+
exactSignature: null,
|
|
412
|
+
physicalCellCount: physicalCellCount ?? null,
|
|
413
|
+
stableIds,
|
|
414
|
+
tokenCounts: null,
|
|
415
|
+
tokenCount: 0
|
|
416
|
+
};
|
|
417
|
+
let textCodeUnits = 0;
|
|
418
|
+
for (const block of blocks) {
|
|
419
|
+
if (block.kind.length > MAX_CONTENT_STRUCTURE_PROFILE_KIND_CODE_UNITS || block.text.length > MAX_CONTENT_STRUCTURE_PROFILE_TEXT_CODE_UNITS - textCodeUnits) return {
|
|
420
|
+
anchorTexts: [],
|
|
421
|
+
blockIds,
|
|
422
|
+
containerIdentity,
|
|
423
|
+
exactSignature: null,
|
|
424
|
+
physicalCellCount: physicalCellCount ?? null,
|
|
425
|
+
stableIds,
|
|
426
|
+
tokenCounts: null,
|
|
427
|
+
tokenCount: 0
|
|
428
|
+
};
|
|
429
|
+
textCodeUnits += block.text.length;
|
|
430
|
+
}
|
|
431
|
+
const tokenCounts = /* @__PURE__ */ new Map();
|
|
432
|
+
const anchorTexts = /* @__PURE__ */ new Set();
|
|
433
|
+
let tokenCount = 0;
|
|
434
|
+
let tokensComplete = true;
|
|
435
|
+
const signature = blocks.map((block) => [
|
|
436
|
+
...blockStructure(block),
|
|
437
|
+
block.kind,
|
|
438
|
+
block.text
|
|
439
|
+
]);
|
|
440
|
+
for (const block of blocks) {
|
|
441
|
+
let blockWordCount = 0;
|
|
442
|
+
for (const match of block.text.matchAll(/\S+/gu)) {
|
|
443
|
+
const token = match[0];
|
|
444
|
+
blockWordCount += 1;
|
|
445
|
+
tokenCount += 1;
|
|
446
|
+
if (tokenCount > MAX_CONTENT_STRUCTURE_PROFILE_TOKENS) {
|
|
447
|
+
tokensComplete = false;
|
|
448
|
+
break;
|
|
449
|
+
}
|
|
450
|
+
tokenCounts.set(token, (tokenCounts.get(token) ?? 0) + 1);
|
|
451
|
+
}
|
|
452
|
+
if (blockWordCount >= CONTENT_STRUCTURE_ANCHOR_MINIMUM_WORD_COUNT) anchorTexts.add(block.text);
|
|
453
|
+
if (!tokensComplete) break;
|
|
454
|
+
}
|
|
455
|
+
return {
|
|
456
|
+
anchorTexts: [...anchorTexts],
|
|
457
|
+
blockIds,
|
|
458
|
+
containerIdentity,
|
|
459
|
+
exactSignature: JSON.stringify(signature),
|
|
460
|
+
physicalCellCount: physicalCellCount ?? null,
|
|
461
|
+
stableIds,
|
|
462
|
+
tokenCounts: tokensComplete ? tokenCounts : null,
|
|
463
|
+
tokenCount: tokensComplete ? tokenCount : 0
|
|
464
|
+
};
|
|
465
|
+
};
|
|
466
|
+
const contentStructureProfileSimilarity = (base, revised, workSession) => {
|
|
467
|
+
if (!base.tokenCounts || !revised.tokenCounts || base.tokenCount === 0 || revised.tokenCount === 0) return 0;
|
|
468
|
+
const [tokens, counterparts] = base.tokenCounts.size <= revised.tokenCounts.size ? [base.tokenCounts, revised.tokenCounts] : [revised.tokenCounts, base.tokenCounts];
|
|
469
|
+
if (tokens.size > workSession.remainingStructuralTokenLookups) return 0;
|
|
470
|
+
workSession.remainingStructuralTokenLookups -= tokens.size;
|
|
471
|
+
let shared = 0;
|
|
472
|
+
for (const [token, count] of tokens) shared += Math.min(count, counterparts.get(token) ?? 0);
|
|
473
|
+
return 2 * shared / (base.tokenCount + revised.tokenCount);
|
|
474
|
+
};
|
|
475
|
+
const tableStructureProfile = (blocks, idStability) => {
|
|
476
|
+
const tableOrdinalByIndex = /* @__PURE__ */ new Map();
|
|
477
|
+
return createContentStructureProfile({
|
|
478
|
+
blocks,
|
|
479
|
+
idStability,
|
|
480
|
+
blockStructure: (block) => {
|
|
481
|
+
const table = block.table ?? panic("A table profile contains a body block");
|
|
482
|
+
let tableOrdinal = tableOrdinalByIndex.get(table.tableIndex);
|
|
483
|
+
if (tableOrdinal === void 0) {
|
|
484
|
+
tableOrdinal = tableOrdinalByIndex.size;
|
|
485
|
+
tableOrdinalByIndex.set(table.tableIndex, tableOrdinal);
|
|
486
|
+
}
|
|
487
|
+
return [
|
|
488
|
+
tableOrdinal,
|
|
489
|
+
table.rowIndex,
|
|
490
|
+
table.cellIndex,
|
|
491
|
+
table.gridColumnIndex,
|
|
492
|
+
table.columnSpan,
|
|
493
|
+
table.rowSpan,
|
|
494
|
+
table.paragraphIndex
|
|
495
|
+
];
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
};
|
|
499
|
+
const rowStructureProfile = (blocks, idStability) => {
|
|
500
|
+
let physicalCellCount = 0;
|
|
501
|
+
for (const block of blocks) physicalCellCount = Math.max(physicalCellCount, (block.table?.cellIndex ?? -1) + 1);
|
|
502
|
+
return createContentStructureProfile({
|
|
503
|
+
blocks,
|
|
504
|
+
idStability,
|
|
505
|
+
physicalCellCount,
|
|
506
|
+
retainContainerIdentity: true,
|
|
507
|
+
blockStructure: (block) => {
|
|
508
|
+
const table = block.table ?? panic("A row profile contains a body block");
|
|
509
|
+
return [
|
|
510
|
+
table.cellIndex,
|
|
511
|
+
table.gridColumnIndex,
|
|
512
|
+
table.columnSpan,
|
|
513
|
+
table.rowSpan,
|
|
514
|
+
table.paragraphIndex
|
|
515
|
+
];
|
|
516
|
+
}
|
|
517
|
+
});
|
|
518
|
+
};
|
|
519
|
+
const unpairedContentSequence = (baseItems, revisedItems) => [...baseItems.map((item) => ({
|
|
520
|
+
type: "baseOnly",
|
|
521
|
+
item
|
|
522
|
+
})), ...revisedItems.map((item) => ({
|
|
523
|
+
type: "revisedOnly",
|
|
524
|
+
item
|
|
525
|
+
}))];
|
|
526
|
+
const stableContentSequencePairs = (base, revised) => {
|
|
527
|
+
const uniqueIndexesById = (items) => {
|
|
528
|
+
const indexes = /* @__PURE__ */ new Map();
|
|
529
|
+
items.forEach(({ profile }, itemIndex) => {
|
|
530
|
+
for (const id of profile.stableIds) {
|
|
531
|
+
const existing = indexes.get(id);
|
|
532
|
+
if (existing === void 0) indexes.set(id, itemIndex);
|
|
533
|
+
else if (existing !== itemIndex) indexes.set(id, null);
|
|
534
|
+
}
|
|
535
|
+
});
|
|
536
|
+
return indexes;
|
|
537
|
+
};
|
|
538
|
+
const baseIndexes = uniqueIndexesById(base);
|
|
539
|
+
const revisedIndexes = uniqueIndexesById(revised);
|
|
540
|
+
const pairs = /* @__PURE__ */ new Set();
|
|
541
|
+
for (const [id, baseIndex] of baseIndexes) {
|
|
542
|
+
const revisedIndex = revisedIndexes.get(id);
|
|
543
|
+
if (baseIndex !== null && revisedIndex !== void 0 && revisedIndex !== null) pairs.add(baseIndex * revised.length + revisedIndex);
|
|
544
|
+
}
|
|
545
|
+
return pairs;
|
|
546
|
+
};
|
|
547
|
+
const contentContainerIdentityTransition = (base, revised) => {
|
|
548
|
+
if (base === null || revised === null || base.status !== "available" || revised.status !== "available" || base.blockIds.length === 0 || base.blockIds.length !== revised.blockIds.length) return false;
|
|
549
|
+
let hasStabilityTransition = false;
|
|
550
|
+
for (let index = 0; index < base.blockIds.length; index++) {
|
|
551
|
+
if (base.blockIds[index] !== revised.blockIds[index]) return false;
|
|
552
|
+
hasStabilityTransition ||= base.idStabilities[index] !== revised.idStabilities[index];
|
|
553
|
+
}
|
|
554
|
+
return hasStabilityTransition;
|
|
555
|
+
};
|
|
556
|
+
const uniqueExactContentSequencePairs = ({ baseKeys, revisedKeys }) => {
|
|
557
|
+
const uniqueIndexes = (keys) => {
|
|
558
|
+
const indexes = /* @__PURE__ */ new Map();
|
|
559
|
+
keys.forEach((key, index) => {
|
|
560
|
+
if (key === -1) return;
|
|
561
|
+
indexes.set(key, indexes.has(key) ? null : index);
|
|
562
|
+
});
|
|
563
|
+
return indexes;
|
|
564
|
+
};
|
|
565
|
+
const revisedIndexes = uniqueIndexes(revisedKeys);
|
|
566
|
+
const candidates = [];
|
|
567
|
+
for (const [key, baseIndex] of uniqueIndexes(baseKeys)) {
|
|
568
|
+
const revisedIndex = revisedIndexes.get(key);
|
|
569
|
+
if (baseIndex !== null && revisedIndex !== void 0 && revisedIndex !== null) candidates.push({
|
|
570
|
+
baseIndex,
|
|
571
|
+
revisedIndex
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
return candidates;
|
|
575
|
+
};
|
|
576
|
+
const pairIsCompatibleWithAnchors = (pair, anchors) => {
|
|
577
|
+
let lower = 0;
|
|
578
|
+
let upper = anchors.length;
|
|
579
|
+
while (lower < upper) {
|
|
580
|
+
const middle = lower + Math.floor((upper - lower) / 2);
|
|
581
|
+
const anchor = anchors[middle];
|
|
582
|
+
if (anchor !== void 0 && anchor.baseIndex < pair.baseIndex) lower = middle + 1;
|
|
583
|
+
else upper = middle;
|
|
584
|
+
}
|
|
585
|
+
const next = anchors[lower];
|
|
586
|
+
if (next?.baseIndex === pair.baseIndex) return next.revisedIndex === pair.revisedIndex;
|
|
587
|
+
const previous = anchors[lower - 1];
|
|
588
|
+
return (previous === void 0 || previous.revisedIndex < pair.revisedIndex) && (next === void 0 || pair.revisedIndex < next.revisedIndex);
|
|
589
|
+
};
|
|
590
|
+
const monotoneContentSequencePairs = (candidates) => longestIncreasingFolioContentPairs(candidates.toSorted((left, right) => left.baseIndex - right.baseIndex || left.revisedIndex - right.revisedIndex));
|
|
591
|
+
const trustedContentSequencePairs = ({ exactPairs, stablePairs, revisedLength, primaryEvidence, canPairIndexes }) => {
|
|
592
|
+
const stableCandidates = [...stablePairs].map((pairIndex) => ({
|
|
593
|
+
baseIndex: Math.floor(pairIndex / revisedLength),
|
|
594
|
+
revisedIndex: pairIndex % revisedLength
|
|
595
|
+
})).filter(canPairIndexes);
|
|
596
|
+
const allowedExactPairs = exactPairs.filter(canPairIndexes);
|
|
597
|
+
const primaryCandidates = primaryEvidence === "exact" ? allowedExactPairs : stableCandidates;
|
|
598
|
+
const secondaryCandidates = primaryEvidence === "exact" ? stableCandidates : allowedExactPairs;
|
|
599
|
+
const primary = monotoneContentSequencePairs(primaryCandidates);
|
|
600
|
+
const secondary = monotoneContentSequencePairs(secondaryCandidates.filter((pair) => pairIsCompatibleWithAnchors(pair, primary)));
|
|
601
|
+
return [...primary, ...secondary].toSorted((left, right) => left.baseIndex - right.baseIndex || left.revisedIndex - right.revisedIndex);
|
|
602
|
+
};
|
|
603
|
+
const persistedContentSequencePairs = ({ base, revised, anchors, canPair }) => {
|
|
604
|
+
if (anchors.length === 0) return /* @__PURE__ */ new Set();
|
|
605
|
+
const uniqueIndexesByFirstId = (items) => {
|
|
606
|
+
const indexes = /* @__PURE__ */ new Map();
|
|
607
|
+
items.forEach(({ profile }, itemIndex) => {
|
|
608
|
+
const identity = profile.containerIdentity;
|
|
609
|
+
const firstId = identity?.status === "available" ? identity.blockIds.at(0) : void 0;
|
|
610
|
+
if (firstId !== void 0) indexes.set(firstId, indexes.has(firstId) ? null : itemIndex);
|
|
611
|
+
});
|
|
612
|
+
return indexes;
|
|
613
|
+
};
|
|
614
|
+
const baseIndexesByFirstId = uniqueIndexesByFirstId(base);
|
|
615
|
+
const revisedIndexesByFirstId = uniqueIndexesByFirstId(revised);
|
|
616
|
+
const candidates = [];
|
|
617
|
+
for (const [firstId, baseIndex] of baseIndexesByFirstId) {
|
|
618
|
+
if (baseIndex === null) continue;
|
|
619
|
+
const baseItem = base[baseIndex];
|
|
620
|
+
if (!baseItem) continue;
|
|
621
|
+
const revisedIndex = revisedIndexesByFirstId.get(firstId);
|
|
622
|
+
if (revisedIndex === void 0 || revisedIndex === null) continue;
|
|
623
|
+
const revisedItem = revised[revisedIndex];
|
|
624
|
+
const pair = {
|
|
625
|
+
baseIndex,
|
|
626
|
+
revisedIndex
|
|
627
|
+
};
|
|
628
|
+
if (revisedItem && canPair(baseItem, revisedItem) && contentContainerIdentityTransition(baseItem.profile.containerIdentity, revisedItem.profile.containerIdentity) && pairIsCompatibleWithAnchors(pair, anchors)) candidates.push(pair);
|
|
629
|
+
}
|
|
630
|
+
return new Set(monotoneContentSequencePairs(candidates).map(({ baseIndex, revisedIndex }) => baseIndex * revised.length + revisedIndex));
|
|
631
|
+
};
|
|
632
|
+
const ALIGNMENT_DIRECTION = {
|
|
633
|
+
pair: 1,
|
|
634
|
+
baseOnly: 2,
|
|
635
|
+
revisedOnly: 3
|
|
636
|
+
};
|
|
637
|
+
const alignProfiledContentSequence = ({ base, revised, workSession, canPair = () => true, pairSoleStructuralSlot = false, primaryEvidence = "stable", similarityFactor = () => 1 }) => {
|
|
638
|
+
if (base.length === 0 || revised.length === 0) return unpairedContentSequence(base.map(({ item }) => item), revised.map(({ item }) => item));
|
|
639
|
+
if ((base.length !== 1 || revised.length !== 1) && !claimFolioContentAlignmentCells(base.length, revised.length, workSession)) return unpairedContentSequence(base.map(({ item }) => item), revised.map(({ item }) => item));
|
|
640
|
+
const stablePairs = stableContentSequencePairs(base, revised);
|
|
641
|
+
const provenanceTransitionAtSamePosition = new Uint8Array(Math.min(base.length, revised.length));
|
|
642
|
+
for (let index = 0; index < provenanceTransitionAtSamePosition.length; index++) {
|
|
643
|
+
const baseProfile = base[index]?.profile;
|
|
644
|
+
const revisedProfile = revised[index]?.profile;
|
|
645
|
+
if (!baseProfile || !revisedProfile) continue;
|
|
646
|
+
const hasContainerIdentity = baseProfile.containerIdentity !== null || revisedProfile.containerIdentity !== null;
|
|
647
|
+
let hasProvenanceTransition = contentContainerIdentityTransition(baseProfile.containerIdentity, revisedProfile.containerIdentity);
|
|
648
|
+
if (!hasContainerIdentity) {
|
|
649
|
+
const revisedIds = new Set(revisedProfile.blockIds);
|
|
650
|
+
const baseStableIds = new Set(baseProfile.stableIds);
|
|
651
|
+
const revisedStableIds = new Set(revisedProfile.stableIds);
|
|
652
|
+
hasProvenanceTransition = baseProfile.blockIds.some((id) => revisedIds.has(id) && baseStableIds.has(id) !== revisedStableIds.has(id));
|
|
653
|
+
}
|
|
654
|
+
if (hasProvenanceTransition) provenanceTransitionAtSamePosition[index] = 1;
|
|
655
|
+
}
|
|
656
|
+
const exactSignatureKeys = /* @__PURE__ */ new Map();
|
|
657
|
+
let nextExactSignatureKey = 0;
|
|
658
|
+
const internExactSignature = (signature) => {
|
|
659
|
+
if (signature === null) return -1;
|
|
660
|
+
const existing = exactSignatureKeys.get(signature);
|
|
661
|
+
if (existing !== void 0) return existing;
|
|
662
|
+
const key = nextExactSignatureKey++;
|
|
663
|
+
exactSignatureKeys.set(signature, key);
|
|
664
|
+
return key;
|
|
665
|
+
};
|
|
666
|
+
const baseExactSignatureKeys = base.map(({ profile }) => internExactSignature(profile.exactSignature));
|
|
667
|
+
const revisedExactSignatureKeys = revised.map(({ profile }) => internExactSignature(profile.exactSignature));
|
|
668
|
+
let persistedPairs = /* @__PURE__ */ new Set();
|
|
669
|
+
if (base.some(({ profile }) => profile.containerIdentity !== null) || revised.some(({ profile }) => profile.containerIdentity !== null)) {
|
|
670
|
+
const exactPairs = uniqueExactContentSequencePairs({
|
|
671
|
+
baseKeys: baseExactSignatureKeys,
|
|
672
|
+
revisedKeys: revisedExactSignatureKeys
|
|
673
|
+
});
|
|
674
|
+
const canPairIndexes = ({ baseIndex, revisedIndex }) => {
|
|
675
|
+
const baseItem = base[baseIndex];
|
|
676
|
+
const revisedItem = revised[revisedIndex];
|
|
677
|
+
return baseItem !== void 0 && revisedItem !== void 0 && canPair(baseItem, revisedItem);
|
|
678
|
+
};
|
|
679
|
+
const trustedPairs = trustedContentSequencePairs({
|
|
680
|
+
exactPairs,
|
|
681
|
+
stablePairs,
|
|
682
|
+
revisedLength: revised.length,
|
|
683
|
+
primaryEvidence,
|
|
684
|
+
canPairIndexes
|
|
685
|
+
});
|
|
686
|
+
persistedPairs = persistedContentSequencePairs({
|
|
687
|
+
base,
|
|
688
|
+
revised,
|
|
689
|
+
anchors: trustedPairs,
|
|
690
|
+
canPair
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
const maxPairs = Math.min(base.length, revised.length);
|
|
694
|
+
const continuityBonus = 1;
|
|
695
|
+
const secondaryWeight = maxPairs * 1001 + 1;
|
|
696
|
+
const primaryWeight = maxPairs * (secondaryWeight + CONTENT_STRUCTURE_SIMILARITY_SCALE + continuityBonus) + 1;
|
|
697
|
+
const stableWeight = primaryEvidence === "stable" ? primaryWeight : secondaryWeight;
|
|
698
|
+
const exactWeight = primaryEvidence === "exact" ? primaryWeight : secondaryWeight;
|
|
699
|
+
const pairScores = new Float64Array(base.length * revised.length);
|
|
700
|
+
for (let baseIndex = 0; baseIndex < base.length; baseIndex++) {
|
|
701
|
+
const baseItem = base[baseIndex];
|
|
702
|
+
if (!baseItem) continue;
|
|
703
|
+
for (let revisedIndex = 0; revisedIndex < revised.length; revisedIndex++) {
|
|
704
|
+
const revisedItem = revised[revisedIndex];
|
|
705
|
+
if (!revisedItem) continue;
|
|
706
|
+
if (!canPair(baseItem, revisedItem)) continue;
|
|
707
|
+
const pairIndex = baseIndex * revised.length + revisedIndex;
|
|
708
|
+
const exact = baseExactSignatureKeys[baseIndex] !== -1 && baseExactSignatureKeys[baseIndex] === revisedExactSignatureKeys[revisedIndex];
|
|
709
|
+
const stable = stablePairs.has(pairIndex);
|
|
710
|
+
const profileSimilarity = exact ? 1 : contentStructureProfileSimilarity(baseItem.profile, revisedItem.profile, workSession);
|
|
711
|
+
const similar = Math.max(0, Math.min(1, profileSimilarity * similarityFactor(baseItem, revisedItem)));
|
|
712
|
+
const stableAtSamePosition = stable && baseIndex === revisedIndex;
|
|
713
|
+
const persistedAtSamePosition = baseIndex === revisedIndex && provenanceTransitionAtSamePosition[baseIndex] === 1;
|
|
714
|
+
const shiftedPersisted = baseIndex !== revisedIndex && persistedPairs.has(pairIndex);
|
|
715
|
+
const persisted = persistedAtSamePosition || shiftedPersisted;
|
|
716
|
+
const soleStructuralSlot = pairSoleStructuralSlot && base.length === 1 && revised.length === 1;
|
|
717
|
+
if (!exact && !stableAtSamePosition && !persisted && !soleStructuralSlot && similar < CONTENT_STRUCTURE_PAIR_SIMILARITY) continue;
|
|
718
|
+
pairScores[pairIndex] = (stable ? stableWeight : 0) + (exact ? exactWeight : 0) + (shiftedPersisted && !stable && !exact ? secondaryWeight : 0) + (persistedAtSamePosition ? continuityBonus : 0) + Math.round(similar * CONTENT_STRUCTURE_SIMILARITY_SCALE) + (soleStructuralSlot ? 1 : 0);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
if (base.length === 1 && revised.length === 1) {
|
|
722
|
+
const baseItem = base.at(0);
|
|
723
|
+
const revisedItem = revised.at(0);
|
|
724
|
+
if (!baseItem || !revisedItem) return panic("A singleton content sequence has no item");
|
|
725
|
+
if ((pairScores[0] ?? 0) > 0) return [{
|
|
726
|
+
type: "pair",
|
|
727
|
+
base: baseItem.item,
|
|
728
|
+
revised: revisedItem.item
|
|
729
|
+
}];
|
|
730
|
+
return unpairedContentSequence(base.map(({ item }) => item), revised.map(({ item }) => item));
|
|
731
|
+
}
|
|
732
|
+
const directions = new Uint8Array(base.length * revised.length);
|
|
733
|
+
let nextScores = new Float64Array(revised.length + 1);
|
|
734
|
+
let currentScores = new Float64Array(revised.length + 1);
|
|
735
|
+
for (let baseIndex = base.length - 1; baseIndex >= 0; baseIndex--) {
|
|
736
|
+
currentScores[revised.length] = 0;
|
|
737
|
+
for (let revisedIndex = revised.length - 1; revisedIndex >= 0; revisedIndex--) {
|
|
738
|
+
const pairIndex = baseIndex * revised.length + revisedIndex;
|
|
739
|
+
const evidence = pairScores[pairIndex] ?? 0;
|
|
740
|
+
const pair = evidence > 0 ? evidence + (nextScores[revisedIndex + 1] ?? 0) : -1;
|
|
741
|
+
const baseOnly = nextScores[revisedIndex] ?? 0;
|
|
742
|
+
const revisedOnly = currentScores[revisedIndex + 1] ?? 0;
|
|
743
|
+
if (pair >= baseOnly && pair >= revisedOnly) {
|
|
744
|
+
currentScores[revisedIndex] = pair;
|
|
745
|
+
directions[pairIndex] = ALIGNMENT_DIRECTION.pair;
|
|
746
|
+
} else if (baseOnly >= revisedOnly) {
|
|
747
|
+
currentScores[revisedIndex] = baseOnly;
|
|
748
|
+
directions[pairIndex] = ALIGNMENT_DIRECTION.baseOnly;
|
|
749
|
+
} else {
|
|
750
|
+
currentScores[revisedIndex] = revisedOnly;
|
|
751
|
+
directions[pairIndex] = ALIGNMENT_DIRECTION.revisedOnly;
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
const completedScores = nextScores;
|
|
755
|
+
nextScores = currentScores;
|
|
756
|
+
currentScores = completedScores;
|
|
757
|
+
}
|
|
758
|
+
const aligned = [];
|
|
759
|
+
let baseIndex = 0;
|
|
760
|
+
let revisedIndex = 0;
|
|
761
|
+
let pairCount = 0;
|
|
762
|
+
let solePairIsShifted = false;
|
|
763
|
+
let hasBaseOnly = false;
|
|
764
|
+
let hasRevisedOnly = false;
|
|
765
|
+
while (baseIndex < base.length && revisedIndex < revised.length) {
|
|
766
|
+
const direction = directions[baseIndex * revised.length + revisedIndex];
|
|
767
|
+
const baseItem = base[baseIndex]?.item;
|
|
768
|
+
const revisedItem = revised[revisedIndex]?.item;
|
|
769
|
+
if (direction === ALIGNMENT_DIRECTION.pair && baseItem !== void 0 && revisedItem !== void 0) {
|
|
770
|
+
aligned.push({
|
|
771
|
+
type: "pair",
|
|
772
|
+
base: baseItem,
|
|
773
|
+
revised: revisedItem
|
|
774
|
+
});
|
|
775
|
+
pairCount += 1;
|
|
776
|
+
solePairIsShifted = baseIndex !== revisedIndex;
|
|
777
|
+
baseIndex += 1;
|
|
778
|
+
revisedIndex += 1;
|
|
779
|
+
} else if (direction === ALIGNMENT_DIRECTION.revisedOnly && revisedItem !== void 0) {
|
|
780
|
+
aligned.push({
|
|
781
|
+
type: "revisedOnly",
|
|
782
|
+
item: revisedItem
|
|
783
|
+
});
|
|
784
|
+
hasRevisedOnly = true;
|
|
785
|
+
revisedIndex += 1;
|
|
786
|
+
} else if (baseItem !== void 0) {
|
|
787
|
+
aligned.push({
|
|
788
|
+
type: "baseOnly",
|
|
789
|
+
item: baseItem
|
|
790
|
+
});
|
|
791
|
+
hasBaseOnly = true;
|
|
792
|
+
baseIndex += 1;
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
for (const { item } of base.slice(baseIndex)) {
|
|
796
|
+
aligned.push({
|
|
797
|
+
type: "baseOnly",
|
|
798
|
+
item
|
|
799
|
+
});
|
|
800
|
+
hasBaseOnly = true;
|
|
801
|
+
}
|
|
802
|
+
for (const { item } of revised.slice(revisedIndex)) {
|
|
803
|
+
aligned.push({
|
|
804
|
+
type: "revisedOnly",
|
|
805
|
+
item
|
|
806
|
+
});
|
|
807
|
+
hasRevisedOnly = true;
|
|
808
|
+
}
|
|
809
|
+
if (pairCount === 1 && solePairIsShifted && hasBaseOnly && hasRevisedOnly) return aligned.flatMap((entry) => entry.type === "pair" ? [{
|
|
810
|
+
type: "baseOnly",
|
|
811
|
+
item: entry.base
|
|
812
|
+
}, {
|
|
813
|
+
type: "revisedOnly",
|
|
814
|
+
item: entry.revised
|
|
815
|
+
}] : [entry]);
|
|
816
|
+
return aligned;
|
|
817
|
+
};
|
|
818
|
+
const rowLocation = (row) => row.at(0)?.table ?? null;
|
|
819
|
+
const alignRowCells = ({ baseRow, revisedRow, workSession, moveScopeContext, stableIdMismatch, idStability, baseColumnKeys, revisedColumnKeys }) => {
|
|
820
|
+
const byCell = (row, columnKeys) => {
|
|
821
|
+
const cells = /* @__PURE__ */ new Map();
|
|
822
|
+
for (const block of row) {
|
|
823
|
+
const table = block.table;
|
|
824
|
+
let cellIndex = table?.cellIndex ?? 0;
|
|
825
|
+
if (table && columnKeys) {
|
|
826
|
+
const alignedColumn = columnKeys.get(table.gridColumnIndex);
|
|
827
|
+
if (alignedColumn === void 0) return panic("A paired table cell has no aligned grid column", { gridColumnIndex: table.gridColumnIndex });
|
|
828
|
+
cellIndex = alignedColumn;
|
|
829
|
+
}
|
|
830
|
+
const blocks = cells.get(cellIndex);
|
|
831
|
+
if (blocks) blocks.push(block);
|
|
832
|
+
else cells.set(cellIndex, [block]);
|
|
833
|
+
}
|
|
834
|
+
return cells;
|
|
835
|
+
};
|
|
836
|
+
const baseCells = byCell(baseRow, baseColumnKeys);
|
|
837
|
+
const revisedCells = byCell(revisedRow, revisedColumnKeys);
|
|
838
|
+
const cellIndexes = [.../* @__PURE__ */ new Set([...baseCells.keys(), ...revisedCells.keys()])].toSorted((left, right) => left - right);
|
|
839
|
+
const steps = [];
|
|
840
|
+
for (const cellIndex of cellIndexes) {
|
|
841
|
+
const baseBlocks = baseCells.get(cellIndex) ?? [];
|
|
842
|
+
const revisedBlocks = revisedCells.get(cellIndex) ?? [];
|
|
843
|
+
const aligned = alignFolioContentBlocks(baseBlocks, revisedBlocks, {
|
|
844
|
+
workSession,
|
|
845
|
+
idStability,
|
|
846
|
+
stableIdMismatch
|
|
847
|
+
}).flatMap((event) => {
|
|
848
|
+
if (event.type === "pair" && !contentBlocksShareContainerPath(event.baseBlock, event.revisedBlock)) return [{
|
|
849
|
+
type: "baseOnly",
|
|
850
|
+
block: event.baseBlock
|
|
851
|
+
}, {
|
|
852
|
+
type: "revisedOnly",
|
|
853
|
+
block: event.revisedBlock
|
|
854
|
+
}];
|
|
855
|
+
return [event];
|
|
856
|
+
});
|
|
857
|
+
const bucketByContainerPath = /* @__PURE__ */ new Map();
|
|
858
|
+
const bucketForBlock = (block) => {
|
|
859
|
+
const path = containerPathKeyOf(block);
|
|
860
|
+
const existing = bucketByContainerPath.get(path);
|
|
861
|
+
if (existing !== void 0) return existing;
|
|
862
|
+
const bucket = moveScopeContext.nextTableCellBucket++;
|
|
863
|
+
bucketByContainerPath.set(path, bucket);
|
|
864
|
+
return bucket;
|
|
865
|
+
};
|
|
866
|
+
steps.push(...scopedAlignmentSteps(aligned, moveScopeContext, bucketForBlock));
|
|
867
|
+
}
|
|
868
|
+
return steps;
|
|
869
|
+
};
|
|
870
|
+
const pairTableRows = ({ baseRows, revisedRows, workSession, idStability }) => {
|
|
871
|
+
const profile = (row) => ({
|
|
872
|
+
item: row,
|
|
873
|
+
profile: rowStructureProfile(row, idStability)
|
|
874
|
+
});
|
|
875
|
+
return alignProfiledContentSequence({
|
|
876
|
+
base: baseRows.map(profile),
|
|
877
|
+
revised: revisedRows.map(profile),
|
|
878
|
+
workSession,
|
|
879
|
+
pairSoleStructuralSlot: true,
|
|
880
|
+
primaryEvidence: "exact",
|
|
881
|
+
similarityFactor: (base, revised) => base.profile.physicalCellCount === revised.profile.physicalCellCount ? 1 : .5
|
|
882
|
+
}).map((alignment) => {
|
|
883
|
+
switch (alignment.type) {
|
|
884
|
+
case "pair": return {
|
|
885
|
+
type: "pair",
|
|
886
|
+
baseRow: alignment.base,
|
|
887
|
+
revisedRow: alignment.revised
|
|
888
|
+
};
|
|
889
|
+
case "baseOnly": return {
|
|
890
|
+
type: "baseOnly",
|
|
891
|
+
row: alignment.item
|
|
892
|
+
};
|
|
893
|
+
case "revisedOnly": return {
|
|
894
|
+
type: "revisedOnly",
|
|
895
|
+
row: alignment.item
|
|
896
|
+
};
|
|
897
|
+
default: return panic("Unhandled content row sequence alignment", { alignment });
|
|
898
|
+
}
|
|
899
|
+
});
|
|
900
|
+
};
|
|
901
|
+
const alignTableRows = ({ rows, workSession, moveScopeContext, stableIdMismatch, idStability, baseColumnKeys, revisedColumnKeys }) => {
|
|
902
|
+
const steps = [];
|
|
903
|
+
const pushRow = (row, side) => {
|
|
904
|
+
const location = rowLocation(row);
|
|
905
|
+
if (location) steps.push({
|
|
906
|
+
type: side === "base" ? "baseRow" : "revisedRow",
|
|
907
|
+
blocks: row,
|
|
908
|
+
location
|
|
909
|
+
});
|
|
910
|
+
};
|
|
911
|
+
for (const alignment of rows) switch (alignment.type) {
|
|
912
|
+
case "baseOnly":
|
|
913
|
+
pushRow(alignment.row, "base");
|
|
914
|
+
break;
|
|
915
|
+
case "revisedOnly":
|
|
916
|
+
pushRow(alignment.row, "revised");
|
|
917
|
+
break;
|
|
918
|
+
case "pair":
|
|
919
|
+
steps.push(...alignRowCells({
|
|
920
|
+
baseRow: alignment.baseRow,
|
|
921
|
+
revisedRow: alignment.revisedRow,
|
|
922
|
+
workSession,
|
|
923
|
+
moveScopeContext,
|
|
924
|
+
stableIdMismatch,
|
|
925
|
+
idStability,
|
|
926
|
+
baseColumnKeys,
|
|
927
|
+
revisedColumnKeys
|
|
928
|
+
}));
|
|
929
|
+
break;
|
|
930
|
+
default: panic("Unhandled table row alignment", { alignment });
|
|
931
|
+
}
|
|
932
|
+
return steps;
|
|
933
|
+
};
|
|
934
|
+
const rowCellSpansEqual = (baseRow, revisedRow) => {
|
|
935
|
+
const cells = (row) => {
|
|
936
|
+
const byPhysicalIndex = /* @__PURE__ */ new Map();
|
|
937
|
+
for (const block of row) {
|
|
938
|
+
const table = block.table ?? panic("A table row contains a body block");
|
|
939
|
+
if (!byPhysicalIndex.has(table.cellIndex)) byPhysicalIndex.set(table.cellIndex, table);
|
|
940
|
+
}
|
|
941
|
+
return [...byPhysicalIndex.values()];
|
|
942
|
+
};
|
|
943
|
+
const baseCells = cells(baseRow);
|
|
944
|
+
const revisedCells = cells(revisedRow);
|
|
945
|
+
if (baseCells.length !== revisedCells.length) return false;
|
|
946
|
+
return baseCells.every((base, index) => {
|
|
947
|
+
const revised = revisedCells[index];
|
|
948
|
+
return revised !== void 0 && base.gridColumnIndex === revised.gridColumnIndex && base.columnSpan === revised.columnSpan && base.rowSpan === revised.rowSpan;
|
|
949
|
+
});
|
|
950
|
+
};
|
|
951
|
+
const rowHasVerticalSpan = (row) => row.some(({ table }) => table !== void 0 && table.rowSpan > 1);
|
|
952
|
+
const buildTablePlan = ({ baseBlocks, revisedBlocks, workSession, moveScopeContext, stableIdMismatch, idStability }) => {
|
|
953
|
+
const resolveIdStability = idStability ?? folioContentIdStability;
|
|
954
|
+
const columns = alignTableColumns(baseBlocks, revisedBlocks);
|
|
955
|
+
const rows = pairTableRows({
|
|
956
|
+
baseRows: groupFolioContentTableRows(columns?.baseBlocks ?? baseBlocks),
|
|
957
|
+
revisedRows: groupFolioContentTableRows(columns?.revisedBlocks ?? revisedBlocks),
|
|
958
|
+
workSession,
|
|
959
|
+
idStability: resolveIdStability
|
|
960
|
+
});
|
|
961
|
+
return {
|
|
962
|
+
representable: rows.every((row) => {
|
|
963
|
+
if (row.type === "pair") return columns !== null || rowCellSpansEqual(row.baseRow, row.revisedRow);
|
|
964
|
+
return !rowHasVerticalSpan(row.row);
|
|
965
|
+
}),
|
|
966
|
+
steps: [...columns?.steps ?? [], ...alignTableRows({
|
|
967
|
+
rows,
|
|
968
|
+
workSession,
|
|
969
|
+
moveScopeContext,
|
|
970
|
+
stableIdMismatch,
|
|
971
|
+
idStability: resolveIdStability,
|
|
972
|
+
baseColumnKeys: columns?.baseColumnKeys,
|
|
973
|
+
revisedColumnKeys: columns?.revisedColumnKeys
|
|
974
|
+
})]
|
|
975
|
+
};
|
|
976
|
+
};
|
|
977
|
+
const buildTableSegmentPlan = ({ baseBlocks, revisedBlocks, workSession, moveScopeContext, stableIdMismatch, idStability }) => {
|
|
978
|
+
const baseTables = groupTables(baseBlocks);
|
|
979
|
+
const revisedTables = groupTables(revisedBlocks);
|
|
980
|
+
const resolveIdStability = idStability ?? folioContentIdStability;
|
|
981
|
+
const profile = (blocks) => ({
|
|
982
|
+
item: blocks,
|
|
983
|
+
profile: tableStructureProfile(blocks, resolveIdStability)
|
|
984
|
+
});
|
|
985
|
+
const alignedTables = alignProfiledContentSequence({
|
|
986
|
+
base: baseTables.map(profile),
|
|
987
|
+
revised: revisedTables.map(profile),
|
|
988
|
+
workSession,
|
|
989
|
+
pairSoleStructuralSlot: true
|
|
990
|
+
});
|
|
991
|
+
const steps = [];
|
|
992
|
+
let representable = true;
|
|
993
|
+
for (const alignment of alignedTables) switch (alignment.type) {
|
|
994
|
+
case "pair": {
|
|
995
|
+
const table = buildTablePlan({
|
|
996
|
+
baseBlocks: alignment.base,
|
|
997
|
+
revisedBlocks: alignment.revised,
|
|
998
|
+
workSession,
|
|
999
|
+
moveScopeContext,
|
|
1000
|
+
stableIdMismatch,
|
|
1001
|
+
idStability: resolveIdStability
|
|
1002
|
+
});
|
|
1003
|
+
steps.push(...table.steps);
|
|
1004
|
+
representable &&= table.representable;
|
|
1005
|
+
break;
|
|
1006
|
+
}
|
|
1007
|
+
case "baseOnly":
|
|
1008
|
+
case "revisedOnly": {
|
|
1009
|
+
const location = alignment.item.at(0)?.table;
|
|
1010
|
+
if (location) steps.push({
|
|
1011
|
+
type: alignment.type === "baseOnly" ? "baseTable" : "revisedTable",
|
|
1012
|
+
blocks: alignment.item,
|
|
1013
|
+
location
|
|
1014
|
+
});
|
|
1015
|
+
representable = false;
|
|
1016
|
+
break;
|
|
1017
|
+
}
|
|
1018
|
+
default: panic("Unhandled nested table sequence alignment", { alignment });
|
|
1019
|
+
}
|
|
1020
|
+
return {
|
|
1021
|
+
steps,
|
|
1022
|
+
representable
|
|
1023
|
+
};
|
|
1024
|
+
};
|
|
1025
|
+
const isTableDocumentSegment = (segment) => segment.kind === "table";
|
|
1026
|
+
const profileTableSegments = (segments, idStability) => {
|
|
1027
|
+
const items = [];
|
|
1028
|
+
const ordinalBySegment = /* @__PURE__ */ new Map();
|
|
1029
|
+
for (const segment of segments) {
|
|
1030
|
+
if (!isTableDocumentSegment(segment)) continue;
|
|
1031
|
+
const profile = tableStructureProfile(segment.blocks, idStability);
|
|
1032
|
+
ordinalBySegment.set(segment, items.length);
|
|
1033
|
+
items.push({
|
|
1034
|
+
item: segment,
|
|
1035
|
+
profile
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
1038
|
+
return {
|
|
1039
|
+
items,
|
|
1040
|
+
ordinalBySegment
|
|
1041
|
+
};
|
|
1042
|
+
};
|
|
1043
|
+
const contentStructureEvidenceIndexes = (base, revised) => {
|
|
1044
|
+
const exactKeys = /* @__PURE__ */ new Map();
|
|
1045
|
+
const exactKeyByProfile = /* @__PURE__ */ new Map();
|
|
1046
|
+
let nextExactKey = 0;
|
|
1047
|
+
const internExactSignatures = (items) => {
|
|
1048
|
+
for (const { profile } of items) {
|
|
1049
|
+
if (profile.exactSignature === null) continue;
|
|
1050
|
+
let key = exactKeys.get(profile.exactSignature);
|
|
1051
|
+
if (key === void 0) {
|
|
1052
|
+
key = nextExactKey++;
|
|
1053
|
+
exactKeys.set(profile.exactSignature, key);
|
|
1054
|
+
}
|
|
1055
|
+
exactKeyByProfile.set(profile, key);
|
|
1056
|
+
}
|
|
1057
|
+
};
|
|
1058
|
+
internExactSignatures(base);
|
|
1059
|
+
internExactSignatures(revised);
|
|
1060
|
+
const index = (items) => {
|
|
1061
|
+
const anchorTextIndexes = /* @__PURE__ */ new Map();
|
|
1062
|
+
const exactIndexes = /* @__PURE__ */ new Map();
|
|
1063
|
+
const stableIndexes = /* @__PURE__ */ new Map();
|
|
1064
|
+
items.forEach(({ profile }, itemIndex) => {
|
|
1065
|
+
const exactKey = exactKeyByProfile.get(profile);
|
|
1066
|
+
if (exactKey !== void 0) {
|
|
1067
|
+
const indexes = exactIndexes.get(exactKey);
|
|
1068
|
+
if (indexes) indexes.push(itemIndex);
|
|
1069
|
+
else exactIndexes.set(exactKey, [itemIndex]);
|
|
1070
|
+
}
|
|
1071
|
+
for (const id of profile.stableIds) {
|
|
1072
|
+
const indexes = stableIndexes.get(id);
|
|
1073
|
+
if (indexes) {
|
|
1074
|
+
if (indexes.at(-1) !== itemIndex) indexes.push(itemIndex);
|
|
1075
|
+
} else stableIndexes.set(id, [itemIndex]);
|
|
1076
|
+
}
|
|
1077
|
+
for (const text of profile.anchorTexts) {
|
|
1078
|
+
const indexes = anchorTextIndexes.get(text);
|
|
1079
|
+
if (indexes) {
|
|
1080
|
+
if (indexes.at(-1) !== itemIndex) indexes.push(itemIndex);
|
|
1081
|
+
} else anchorTextIndexes.set(text, [itemIndex]);
|
|
1082
|
+
}
|
|
1083
|
+
});
|
|
1084
|
+
return {
|
|
1085
|
+
anchorTextIndexes,
|
|
1086
|
+
exactIndexes,
|
|
1087
|
+
exactKeyByProfile,
|
|
1088
|
+
stableIndexes
|
|
1089
|
+
};
|
|
1090
|
+
};
|
|
1091
|
+
return {
|
|
1092
|
+
base: index(base),
|
|
1093
|
+
revised: index(revised)
|
|
1094
|
+
};
|
|
1095
|
+
};
|
|
1096
|
+
const sortedIndexesLeaveRange = (indexes, start, end) => indexes !== void 0 && ((indexes.at(0) ?? start) < start || (indexes.at(-1) ?? end - 1) >= end);
|
|
1097
|
+
const profileHasEvidenceOutsideRange = (profile, opposite, start, end) => {
|
|
1098
|
+
const exactKey = opposite.exactKeyByProfile.get(profile);
|
|
1099
|
+
if (exactKey !== void 0 && sortedIndexesLeaveRange(opposite.exactIndexes.get(exactKey), start, end)) return true;
|
|
1100
|
+
if (profile.anchorTexts.some((text) => sortedIndexesLeaveRange(opposite.anchorTextIndexes.get(text), start, end))) return true;
|
|
1101
|
+
return profile.stableIds.some((id) => sortedIndexesLeaveRange(opposite.stableIndexes.get(id), start, end));
|
|
1102
|
+
};
|
|
1103
|
+
const profilesShareContentAnchor = (base, revised) => {
|
|
1104
|
+
const baseTexts = new Set(base.anchorTexts);
|
|
1105
|
+
return revised.anchorTexts.some((text) => baseTexts.has(text));
|
|
1106
|
+
};
|
|
1107
|
+
const isBodyDocumentSegment = (segment) => segment.kind === "body";
|
|
1108
|
+
const profileBodySegments = (segments, idStability) => segments.flatMap((segment) => isBodyDocumentSegment(segment) ? [{
|
|
1109
|
+
item: segment,
|
|
1110
|
+
profile: createContentStructureProfile({
|
|
1111
|
+
blocks: segment.blocks,
|
|
1112
|
+
idStability,
|
|
1113
|
+
blockStructure: () => [segment.containerPathKey]
|
|
1114
|
+
})
|
|
1115
|
+
}] : []);
|
|
1116
|
+
const exactBodyPairsInRange = (base, revised, exactKeyByProfile, baseStart, baseEnd, revisedStart, revisedEnd) => {
|
|
1117
|
+
const uniqueIndexes = (items, start, end) => {
|
|
1118
|
+
const indexes = /* @__PURE__ */ new Map();
|
|
1119
|
+
for (let index = start; index < end; index++) {
|
|
1120
|
+
const profile = items[index]?.profile;
|
|
1121
|
+
const key = profile ? exactKeyByProfile.get(profile) : void 0;
|
|
1122
|
+
if (key === void 0) continue;
|
|
1123
|
+
indexes.set(key, indexes.has(key) ? null : index);
|
|
1124
|
+
}
|
|
1125
|
+
return indexes;
|
|
1126
|
+
};
|
|
1127
|
+
const baseIndexes = uniqueIndexes(base, baseStart, baseEnd);
|
|
1128
|
+
const revisedIndexes = uniqueIndexes(revised, revisedStart, revisedEnd);
|
|
1129
|
+
const candidates = [];
|
|
1130
|
+
for (const [key, baseIndex] of baseIndexes) {
|
|
1131
|
+
const revisedIndex = revisedIndexes.get(key);
|
|
1132
|
+
if (baseIndex !== null && revisedIndex !== void 0 && revisedIndex !== null) candidates.push({
|
|
1133
|
+
baseIndex,
|
|
1134
|
+
revisedIndex
|
|
1135
|
+
});
|
|
1136
|
+
}
|
|
1137
|
+
return longestIncreasingFolioContentPairs(candidates.toSorted((left, right) => left.baseIndex - right.baseIndex || left.revisedIndex - right.revisedIndex));
|
|
1138
|
+
};
|
|
1139
|
+
const trustedBodyPairs = (base, revised) => {
|
|
1140
|
+
const evidence = contentStructureEvidenceIndexes(base, revised);
|
|
1141
|
+
const stableCandidates = [...stableContentSequencePairs(base, revised)].map((pairIndex) => ({
|
|
1142
|
+
baseIndex: Math.floor(pairIndex / revised.length),
|
|
1143
|
+
revisedIndex: pairIndex % revised.length
|
|
1144
|
+
})).filter(({ baseIndex, revisedIndex }) => base[baseIndex]?.item.containerPathKey === revised[revisedIndex]?.item.containerPathKey).toSorted((left, right) => left.baseIndex - right.baseIndex || left.revisedIndex - right.revisedIndex);
|
|
1145
|
+
const stablePairs = longestIncreasingFolioContentPairs(stableCandidates);
|
|
1146
|
+
const pairs = [...stablePairs];
|
|
1147
|
+
let baseStart = 0;
|
|
1148
|
+
let revisedStart = 0;
|
|
1149
|
+
for (const stable of [...stablePairs, {
|
|
1150
|
+
baseIndex: base.length,
|
|
1151
|
+
revisedIndex: revised.length
|
|
1152
|
+
}]) {
|
|
1153
|
+
pairs.push(...exactBodyPairsInRange(base, revised, evidence.base.exactKeyByProfile, baseStart, stable.baseIndex, revisedStart, stable.revisedIndex));
|
|
1154
|
+
baseStart = stable.baseIndex + 1;
|
|
1155
|
+
revisedStart = stable.revisedIndex + 1;
|
|
1156
|
+
}
|
|
1157
|
+
return pairs.toSorted((left, right) => left.baseIndex - right.baseIndex || left.revisedIndex - right.revisedIndex).map(({ baseIndex, revisedIndex }) => ({
|
|
1158
|
+
base: base[baseIndex]?.item ?? panic("A trusted base body segment is missing"),
|
|
1159
|
+
revised: revised[revisedIndex]?.item ?? panic("A trusted revised body segment is missing")
|
|
1160
|
+
}));
|
|
1161
|
+
};
|
|
1162
|
+
const segmentGaps = (segments, anchors) => {
|
|
1163
|
+
const anchorIndexes = new Map(anchors.map((anchor, index) => [anchor, index]));
|
|
1164
|
+
const gaps = /* @__PURE__ */ new Map();
|
|
1165
|
+
let gap = 0;
|
|
1166
|
+
for (const segment of segments) {
|
|
1167
|
+
gaps.set(segment, gap);
|
|
1168
|
+
if (anchorIndexes.get(segment) === gap) gap += 1;
|
|
1169
|
+
}
|
|
1170
|
+
return gaps;
|
|
1171
|
+
};
|
|
1172
|
+
const pairTableSegmentsInGaps = ({ base, revised, baseGaps, revisedGaps, trustedBodyPairCount, workSession }) => {
|
|
1173
|
+
const groupByGap = (items, gaps) => {
|
|
1174
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
1175
|
+
for (const item of items) {
|
|
1176
|
+
const gap = gaps.get(item.item) ?? panic("A table segment has no structural gap");
|
|
1177
|
+
const entries = grouped.get(gap);
|
|
1178
|
+
if (entries) entries.push(item);
|
|
1179
|
+
else grouped.set(gap, [item]);
|
|
1180
|
+
}
|
|
1181
|
+
return grouped;
|
|
1182
|
+
};
|
|
1183
|
+
const baseByGap = groupByGap(base.items, baseGaps);
|
|
1184
|
+
const revisedByGap = groupByGap(revised.items, revisedGaps);
|
|
1185
|
+
const evidence = contentStructureEvidenceIndexes(base.items, revised.items);
|
|
1186
|
+
const baseToRevised = /* @__PURE__ */ new Map();
|
|
1187
|
+
const revisedToBase = /* @__PURE__ */ new Map();
|
|
1188
|
+
for (let gap = 0; gap <= trustedBodyPairCount; gap++) {
|
|
1189
|
+
const baseItems = baseByGap.get(gap) ?? [];
|
|
1190
|
+
const revisedItems = revisedByGap.get(gap) ?? [];
|
|
1191
|
+
if (baseItems.length === 0 || revisedItems.length === 0) continue;
|
|
1192
|
+
const baseStart = base.ordinalBySegment.get(baseItems.at(0)?.item ?? panic("A non-empty base table gap has no first item")) ?? panic("A base table gap item has no ordinal");
|
|
1193
|
+
const revisedStart = revised.ordinalBySegment.get(revisedItems.at(0)?.item ?? panic("A non-empty revised table gap has no first item")) ?? panic("A revised table gap item has no ordinal");
|
|
1194
|
+
const baseEnd = baseStart + baseItems.length;
|
|
1195
|
+
const revisedEnd = revisedStart + revisedItems.length;
|
|
1196
|
+
const baseHasExternalEvidence = new Set(baseItems.filter(({ profile }) => profileHasEvidenceOutsideRange(profile, evidence.revised, revisedStart, revisedEnd)).map(({ item }) => item));
|
|
1197
|
+
const revisedHasExternalEvidence = new Set(revisedItems.filter(({ profile }) => profileHasEvidenceOutsideRange(profile, evidence.base, baseStart, baseEnd)).map(({ item }) => item));
|
|
1198
|
+
const solePairHasContentAnchor = baseItems.length === 1 && revisedItems.length === 1 && profilesShareContentAnchor(baseItems.at(0)?.profile ?? panic("A sole base table has no profile"), revisedItems.at(0)?.profile ?? panic("A sole revised table has no profile"));
|
|
1199
|
+
const alignments = alignProfiledContentSequence({
|
|
1200
|
+
base: baseItems,
|
|
1201
|
+
revised: revisedItems,
|
|
1202
|
+
workSession,
|
|
1203
|
+
canPair: (baseItem, revisedItem) => !baseHasExternalEvidence.has(baseItem.item) && !revisedHasExternalEvidence.has(revisedItem.item),
|
|
1204
|
+
pairSoleStructuralSlot: trustedBodyPairCount > 0 || solePairHasContentAnchor
|
|
1205
|
+
});
|
|
1206
|
+
for (const alignment of alignments) {
|
|
1207
|
+
if (alignment.type !== "pair") continue;
|
|
1208
|
+
baseToRevised.set(alignment.base, alignment.revised);
|
|
1209
|
+
revisedToBase.set(alignment.revised, alignment.base);
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
return {
|
|
1213
|
+
baseToRevised,
|
|
1214
|
+
revisedToBase
|
|
1215
|
+
};
|
|
1216
|
+
};
|
|
1217
|
+
const segmentIndexesByKey = (segments) => {
|
|
1218
|
+
const indexes = /* @__PURE__ */ new Map();
|
|
1219
|
+
segments.forEach((segment, index) => {
|
|
1220
|
+
const key = segment.structuralKey;
|
|
1221
|
+
const existing = indexes.get(key);
|
|
1222
|
+
if (existing) existing.push(index);
|
|
1223
|
+
else indexes.set(key, [index]);
|
|
1224
|
+
});
|
|
1225
|
+
return indexes;
|
|
1226
|
+
};
|
|
1227
|
+
const firstIndexAfter = (indexes, cursor) => {
|
|
1228
|
+
if (!indexes) return null;
|
|
1229
|
+
let low = 0;
|
|
1230
|
+
let high = indexes.length;
|
|
1231
|
+
while (low < high) {
|
|
1232
|
+
const middle = low + Math.floor((high - low) / 2);
|
|
1233
|
+
if ((indexes[middle] ?? -1) <= cursor) low = middle + 1;
|
|
1234
|
+
else high = middle;
|
|
1235
|
+
}
|
|
1236
|
+
return indexes[low] ?? null;
|
|
1237
|
+
};
|
|
1238
|
+
const segmentsCanPair = (base, revised) => {
|
|
1239
|
+
if (base.kind !== revised.kind) return false;
|
|
1240
|
+
if (base.blocks.length === 0 || revised.blocks.length === 0) return true;
|
|
1241
|
+
if (base.containerPathKey === null && revised.containerPathKey === null) return true;
|
|
1242
|
+
return base.containerPathKey === revised.containerPathKey;
|
|
1243
|
+
};
|
|
1244
|
+
const alignSegments = ({ baseSegments, revisedSegments, workSession, idStability }) => {
|
|
1245
|
+
const baseIndexesByKey = segmentIndexesByKey(baseSegments);
|
|
1246
|
+
const revisedIndexesByKey = segmentIndexesByKey(revisedSegments);
|
|
1247
|
+
const baseIndexBySegment = new Map(baseSegments.map((segment, index) => [segment, index]));
|
|
1248
|
+
const revisedIndexBySegment = new Map(revisedSegments.map((segment, index) => [segment, index]));
|
|
1249
|
+
const bodyPairs = trustedBodyPairs(profileBodySegments(baseSegments, idStability), profileBodySegments(revisedSegments, idStability));
|
|
1250
|
+
const baseGaps = segmentGaps(baseSegments, bodyPairs.map(({ base }) => base));
|
|
1251
|
+
const revisedGaps = segmentGaps(revisedSegments, bodyPairs.map(({ revised }) => revised));
|
|
1252
|
+
const baseTableProfiles = profileTableSegments(baseSegments, idStability);
|
|
1253
|
+
const revisedTableProfiles = profileTableSegments(revisedSegments, idStability);
|
|
1254
|
+
const tablePairs = pairTableSegmentsInGaps({
|
|
1255
|
+
base: baseTableProfiles,
|
|
1256
|
+
revised: revisedTableProfiles,
|
|
1257
|
+
baseGaps,
|
|
1258
|
+
revisedGaps,
|
|
1259
|
+
trustedBodyPairCount: bodyPairs.length,
|
|
1260
|
+
workSession
|
|
1261
|
+
});
|
|
1262
|
+
const baseToRevised = /* @__PURE__ */ new Map();
|
|
1263
|
+
const revisedToBase = /* @__PURE__ */ new Map();
|
|
1264
|
+
for (const { base, revised } of bodyPairs) {
|
|
1265
|
+
baseToRevised.set(base, revised);
|
|
1266
|
+
revisedToBase.set(revised, base);
|
|
1267
|
+
}
|
|
1268
|
+
for (const [base, revised] of tablePairs.baseToRevised) {
|
|
1269
|
+
baseToRevised.set(base, revised);
|
|
1270
|
+
revisedToBase.set(revised, base);
|
|
1271
|
+
}
|
|
1272
|
+
const paired = [];
|
|
1273
|
+
let baseCursor = 0;
|
|
1274
|
+
let revisedCursor = 0;
|
|
1275
|
+
while (baseCursor < baseSegments.length && revisedCursor < revisedSegments.length) {
|
|
1276
|
+
const baseSegment = baseSegments[baseCursor];
|
|
1277
|
+
const revisedSegment = revisedSegments[revisedCursor];
|
|
1278
|
+
if (!baseSegment || !revisedSegment) break;
|
|
1279
|
+
const basePartner = baseToRevised.get(baseSegment);
|
|
1280
|
+
const revisedPartner = revisedToBase.get(revisedSegment);
|
|
1281
|
+
if (basePartner === revisedSegment && revisedPartner === baseSegment) {
|
|
1282
|
+
paired.push({
|
|
1283
|
+
baseSegment,
|
|
1284
|
+
revisedSegment
|
|
1285
|
+
});
|
|
1286
|
+
baseCursor += 1;
|
|
1287
|
+
revisedCursor += 1;
|
|
1288
|
+
continue;
|
|
1289
|
+
}
|
|
1290
|
+
if (basePartner) {
|
|
1291
|
+
const partnerIndex = revisedIndexBySegment.get(basePartner) ?? panic("A paired revised segment has no document index");
|
|
1292
|
+
if (partnerIndex <= revisedCursor || revisedPartner) return panic("Precomputed content segment pairs are not monotone", {
|
|
1293
|
+
baseCursor,
|
|
1294
|
+
partnerIndex,
|
|
1295
|
+
revisedCursor
|
|
1296
|
+
});
|
|
1297
|
+
paired.push({
|
|
1298
|
+
baseSegment: null,
|
|
1299
|
+
revisedSegment
|
|
1300
|
+
});
|
|
1301
|
+
revisedCursor += 1;
|
|
1302
|
+
continue;
|
|
1303
|
+
}
|
|
1304
|
+
if (revisedPartner) {
|
|
1305
|
+
const partnerIndex = baseIndexBySegment.get(revisedPartner) ?? panic("A paired base segment has no document index");
|
|
1306
|
+
if (partnerIndex <= baseCursor) return panic("Precomputed content segment pairs are not monotone", {
|
|
1307
|
+
baseCursor,
|
|
1308
|
+
partnerIndex,
|
|
1309
|
+
revisedCursor
|
|
1310
|
+
});
|
|
1311
|
+
paired.push({
|
|
1312
|
+
baseSegment,
|
|
1313
|
+
revisedSegment: null
|
|
1314
|
+
});
|
|
1315
|
+
baseCursor += 1;
|
|
1316
|
+
continue;
|
|
1317
|
+
}
|
|
1318
|
+
if (baseSegment.kind === "body" && revisedSegment.kind === "body" && segmentsCanPair(baseSegment, revisedSegment)) {
|
|
1319
|
+
paired.push({
|
|
1320
|
+
baseSegment,
|
|
1321
|
+
revisedSegment
|
|
1322
|
+
});
|
|
1323
|
+
baseCursor += 1;
|
|
1324
|
+
revisedCursor += 1;
|
|
1325
|
+
continue;
|
|
1326
|
+
}
|
|
1327
|
+
if (!segmentsCanPair(baseSegment, revisedSegment)) {
|
|
1328
|
+
const nextBaseIndex = firstIndexAfter(baseIndexesByKey.get(revisedSegment.structuralKey), baseCursor);
|
|
1329
|
+
const nextRevisedIndex = firstIndexAfter(revisedIndexesByKey.get(baseSegment.structuralKey), revisedCursor);
|
|
1330
|
+
const baseDistance = nextBaseIndex === null ? Number.POSITIVE_INFINITY : nextBaseIndex - baseCursor;
|
|
1331
|
+
if ((nextRevisedIndex === null ? Number.POSITIVE_INFINITY : nextRevisedIndex - revisedCursor) < baseDistance) {
|
|
1332
|
+
paired.push({
|
|
1333
|
+
baseSegment: null,
|
|
1334
|
+
revisedSegment
|
|
1335
|
+
});
|
|
1336
|
+
revisedCursor += 1;
|
|
1337
|
+
} else {
|
|
1338
|
+
paired.push({
|
|
1339
|
+
baseSegment,
|
|
1340
|
+
revisedSegment: null
|
|
1341
|
+
});
|
|
1342
|
+
baseCursor += 1;
|
|
1343
|
+
}
|
|
1344
|
+
continue;
|
|
1345
|
+
}
|
|
1346
|
+
paired.push({
|
|
1347
|
+
baseSegment,
|
|
1348
|
+
revisedSegment: null
|
|
1349
|
+
});
|
|
1350
|
+
baseCursor += 1;
|
|
1351
|
+
}
|
|
1352
|
+
for (const segment of baseSegments.slice(baseCursor)) paired.push({
|
|
1353
|
+
baseSegment: segment,
|
|
1354
|
+
revisedSegment: null
|
|
1355
|
+
});
|
|
1356
|
+
for (const segment of revisedSegments.slice(revisedCursor)) paired.push({
|
|
1357
|
+
baseSegment: null,
|
|
1358
|
+
revisedSegment: segment
|
|
1359
|
+
});
|
|
1360
|
+
return paired;
|
|
1361
|
+
};
|
|
1362
|
+
const unpairedSegmentSteps = (segment, side, moveScopeContext) => {
|
|
1363
|
+
if (segment.kind !== "table") {
|
|
1364
|
+
const gap = moveScopeContext.nextGap++;
|
|
1365
|
+
return segment.blocks.map((block) => side === "base" ? {
|
|
1366
|
+
type: "baseOnly",
|
|
1367
|
+
block,
|
|
1368
|
+
moveScope: {
|
|
1369
|
+
bucket: BODY_MOVE_BUCKET,
|
|
1370
|
+
gap
|
|
1371
|
+
}
|
|
1372
|
+
} : {
|
|
1373
|
+
type: "revisedOnly",
|
|
1374
|
+
block,
|
|
1375
|
+
moveScope: {
|
|
1376
|
+
bucket: BODY_MOVE_BUCKET,
|
|
1377
|
+
gap
|
|
1378
|
+
}
|
|
1379
|
+
});
|
|
1380
|
+
}
|
|
1381
|
+
const location = segment.blocks.at(0)?.table;
|
|
1382
|
+
if (!location) return [];
|
|
1383
|
+
const outerLocation = {
|
|
1384
|
+
...location,
|
|
1385
|
+
tableIndex: location.outerTableIndex
|
|
1386
|
+
};
|
|
1387
|
+
return [{
|
|
1388
|
+
type: side === "base" ? "baseTable" : "revisedTable",
|
|
1389
|
+
blocks: segment.blocks,
|
|
1390
|
+
location: outerLocation
|
|
1391
|
+
}];
|
|
1392
|
+
};
|
|
1393
|
+
const alignFolioContentStructure = ({ baseBlocks, revisedBlocks, workSession = createFolioContentAlignmentWorkSession(), wholeTableReplacement = "allow", stableIdMismatch = "separate", idStability }) => {
|
|
1394
|
+
const canReplaceWholeTable = wholeTableReplacement === "allow";
|
|
1395
|
+
const resolveIdStability = idStability ?? folioContentIdStability;
|
|
1396
|
+
const moveScopeContext = {
|
|
1397
|
+
nextTableCellBucket: 1,
|
|
1398
|
+
nextGap: 0
|
|
1399
|
+
};
|
|
1400
|
+
const steps = [];
|
|
1401
|
+
for (const { baseSegment, revisedSegment } of alignSegments({
|
|
1402
|
+
baseSegments: splitSegments(baseBlocks),
|
|
1403
|
+
revisedSegments: splitSegments(revisedBlocks),
|
|
1404
|
+
workSession,
|
|
1405
|
+
idStability: resolveIdStability
|
|
1406
|
+
})) {
|
|
1407
|
+
if (baseSegment && revisedSegment) {
|
|
1408
|
+
if (baseSegment.kind !== "table") {
|
|
1409
|
+
steps.push(...scopedAlignmentSteps(alignFolioContentBlocks(baseSegment.blocks, revisedSegment.blocks, {
|
|
1410
|
+
workSession,
|
|
1411
|
+
stableIdMismatch,
|
|
1412
|
+
idStability: resolveIdStability
|
|
1413
|
+
}), moveScopeContext, () => BODY_MOVE_BUCKET));
|
|
1414
|
+
continue;
|
|
1415
|
+
}
|
|
1416
|
+
const remainingLcsCells = workSession.remainingLcsCells;
|
|
1417
|
+
const remainingStructuralTokenLookups = workSession.remainingStructuralTokenLookups;
|
|
1418
|
+
const table = buildTableSegmentPlan({
|
|
1419
|
+
baseBlocks: baseSegment.blocks,
|
|
1420
|
+
revisedBlocks: revisedSegment.blocks,
|
|
1421
|
+
workSession,
|
|
1422
|
+
moveScopeContext,
|
|
1423
|
+
stableIdMismatch,
|
|
1424
|
+
idStability: resolveIdStability
|
|
1425
|
+
});
|
|
1426
|
+
if (canReplaceWholeTable && !table.representable) {
|
|
1427
|
+
workSession.remainingLcsCells = remainingLcsCells;
|
|
1428
|
+
workSession.remainingStructuralTokenLookups = remainingStructuralTokenLookups;
|
|
1429
|
+
steps.push(...unpairedSegmentSteps(baseSegment, "base", moveScopeContext));
|
|
1430
|
+
steps.push(...unpairedSegmentSteps(revisedSegment, "revised", moveScopeContext));
|
|
1431
|
+
continue;
|
|
1432
|
+
}
|
|
1433
|
+
steps.push(...table.steps);
|
|
1434
|
+
continue;
|
|
1435
|
+
}
|
|
1436
|
+
if (baseSegment) {
|
|
1437
|
+
steps.push(...unpairedSegmentSteps(baseSegment, "base", moveScopeContext));
|
|
1438
|
+
continue;
|
|
1439
|
+
}
|
|
1440
|
+
if (revisedSegment) steps.push(...unpairedSegmentSteps(revisedSegment, "revised", moveScopeContext));
|
|
1441
|
+
}
|
|
1442
|
+
return steps;
|
|
1443
|
+
};
|
|
1444
|
+
const contentBlocksShareContainer = (left, right) => {
|
|
1445
|
+
if (!contentBlocksShareContainerPath(left, right)) return false;
|
|
1446
|
+
if (!left.table || !right.table) return left.table === void 0 && right.table === void 0;
|
|
1447
|
+
return left.table.tableIndex === right.table.tableIndex && left.table.rowIndex === right.table.rowIndex && left.table.cellIndex === right.table.cellIndex;
|
|
1448
|
+
};
|
|
1449
|
+
//#endregion
|
|
1450
|
+
export { alignFolioContentBlocks, alignFolioContentStructure, contentBlocksShareContainer, createFolioContentAlignmentWorkSession, exceedsFolioContentLcsBudget, groupFolioContentTableRows, longestIncreasingFolioContentPairs };
|