@stll/folio-core 0.35.1 → 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.
Files changed (236) hide show
  1. package/README.md +59 -1
  2. package/dist/ai-edits/apply.js +878 -377
  3. package/dist/ai-edits/block-identity.d.ts +4 -1
  4. package/dist/ai-edits/block-identity.js +4 -1
  5. package/dist/ai-edits/blockRange.js +14 -14
  6. package/dist/ai-edits/clean-text.d.ts +40 -1
  7. package/dist/ai-edits/clean-text.js +52 -2
  8. package/dist/ai-edits/headless.d.ts +3 -1
  9. package/dist/ai-edits/headless.js +141 -73
  10. package/dist/ai-edits/index.d.ts +3 -3
  11. package/dist/ai-edits/index.js +2 -2
  12. package/dist/ai-edits/read.js +38 -20
  13. package/dist/ai-edits/snapshot.d.ts +14 -6
  14. package/dist/ai-edits/snapshot.js +105 -20
  15. package/dist/ai-edits/table-cell-mutations.js +1 -1
  16. package/dist/ai-edits/table-row-column-mutations.js +7 -0
  17. package/dist/ai-edits/table-template.d.ts +11 -1
  18. package/dist/ai-edits/table-template.js +49 -17
  19. package/dist/ai-edits/types.d.ts +62 -61
  20. package/dist/ai-edits/word-diff.d.ts +18 -3
  21. package/dist/ai-edits/word-diff.js +556 -67
  22. package/dist/ai-suggestions/conflict.js +1 -4
  23. package/dist/ai-suggestions/text-positions.d.ts +8 -0
  24. package/dist/ai-suggestions/text-positions.js +27 -5
  25. package/dist/compare/__fixtures__/body-sequence.js +3 -4
  26. package/dist/compare/column-alignment.d.ts +13 -13
  27. package/dist/compare/column-alignment.js +78 -39
  28. package/dist/compare/compare.d.ts +4 -3
  29. package/dist/compare/compare.js +48 -21
  30. package/dist/compare/content-alignment.d.ts +95 -0
  31. package/dist/compare/content-alignment.js +1450 -0
  32. package/dist/compare/content-types.d.ts +100 -0
  33. package/dist/compare/content-types.js +0 -0
  34. package/dist/compare/content.d.ts +209 -0
  35. package/dist/compare/content.js +885 -0
  36. package/dist/compare/formatting.d.ts +5 -5
  37. package/dist/compare/formatting.js +6 -4
  38. package/dist/compare/plan.d.ts +6 -1
  39. package/dist/compare/plan.js +171 -566
  40. package/dist/compare/scenario.d.ts +2 -2
  41. package/dist/compare/types.d.ts +3 -3
  42. package/dist/compare/verification.d.ts +8 -9
  43. package/dist/compare/verification.js +72 -50
  44. package/dist/compat/eigenpal.d.ts +5 -3
  45. package/dist/compat/eigenpal.js +3 -2
  46. package/dist/controller/headerFooterEditorManager.js +18 -10
  47. package/dist/controller/hiddenEditorApi.js +1 -1
  48. package/dist/controller/hiddenEditorManager.d.ts +7 -2
  49. package/dist/controller/hiddenEditorManager.js +15 -6
  50. package/dist/controller/layoutPipeline.js +5 -3
  51. package/dist/controller/noteEditorManager.js +12 -7
  52. package/dist/display-list/build/headerFooterPrimitives.js +1 -0
  53. package/dist/display-list/build/storyPrimitives.d.ts +5 -2
  54. package/dist/display-list/build/storyPrimitives.js +60 -3
  55. package/dist/display-list/build/tablePrimitives.js +30 -0
  56. package/dist/display-list/primitives.d.ts +1 -1
  57. package/dist/document-operations.d.ts +8 -4
  58. package/dist/document-operations.js +104 -12
  59. package/dist/docx/blockContentParser.js +6 -27
  60. package/dist/docx/commentParser.js +2 -4
  61. package/dist/docx/commentRangeIntegrity.js +2 -4
  62. package/dist/docx/documentClone.d.ts +2 -0
  63. package/dist/docx/documentClone.js +2 -0
  64. package/dist/docx/ensureParaIds.js +9 -4
  65. package/dist/docx/headerFooterParser.js +2 -4
  66. package/dist/docx/normalizeBaseDirection.js +5 -7
  67. package/dist/docx/numberingParser.d.ts +3 -1
  68. package/dist/docx/numberingParser.js +4 -1
  69. package/dist/docx/paragraphParser.js +34 -35
  70. package/dist/docx/paragraphPropertySource.d.ts +49 -0
  71. package/dist/docx/paragraphPropertySource.js +113 -0
  72. package/dist/docx/paragraphTraversal.js +30 -34
  73. package/dist/docx/rezip.d.ts +0 -8
  74. package/dist/docx/rezip.js +41 -16
  75. package/dist/docx/runConsolidator.js +2 -4
  76. package/dist/docx/runParser.js +6 -17
  77. package/dist/docx/sectionParser.js +1 -15
  78. package/dist/docx/serializer/paragraphSerializer.js +289 -30
  79. package/dist/docx/serializer/runSerializer.js +6 -12
  80. package/dist/docx/serializer/sectionPropertiesSerializer.js +2 -10
  81. package/dist/docx/serializer/tableSerializer.js +4 -23
  82. package/dist/docx/serializer/trackedChangeAttributes.d.ts +11 -0
  83. package/dist/docx/serializer/trackedChangeAttributes.js +31 -0
  84. package/dist/docx/server/build.d.ts +1 -1
  85. package/dist/docx/server/createBilingualDocument.js +8 -10
  86. package/dist/docx/server/evaluateDocxXmlPatchProposal.js +22 -13
  87. package/dist/docx/server/validateDocxConformance.js +17 -13
  88. package/dist/docx/tableParser.js +1 -37
  89. package/dist/docx/trackedChangeInfo.d.ts +11 -0
  90. package/dist/docx/trackedChangeInfo.js +38 -0
  91. package/dist/docx/verbatimCapture.d.ts +20 -9
  92. package/dist/docx/verbatimCapture.js +208 -8
  93. package/dist/docx/xmlParser.d.ts +3 -1
  94. package/dist/docx/xmlParser.js +6 -1
  95. package/dist/docx/xmlSafety.d.ts +1 -1
  96. package/dist/docx/xmlSafety.js +157 -0
  97. package/dist/headless-layout.js +5 -4
  98. package/dist/index.d.ts +5 -3
  99. package/dist/index.js +3 -2
  100. package/dist/internal/headlessRevisionResolution.d.ts +23 -0
  101. package/dist/internal/headlessRevisionResolution.js +155 -0
  102. package/dist/internal/headlessRevisionResolutionGuard.d.ts +12 -0
  103. package/dist/internal/headlessRevisionResolutionGuard.js +28 -0
  104. package/dist/internal/pageBreakRunDescendantIndex.d.ts +9 -0
  105. package/dist/internal/pageBreakRunDescendantIndex.js +27 -0
  106. package/dist/internal/pageBreakRunPartition.d.ts +26 -0
  107. package/dist/internal/pageBreakRunPartition.js +35 -0
  108. package/dist/internal/pageBreakRunSourceDescendantIndex.d.ts +15 -0
  109. package/dist/internal/pageBreakRunSourceDescendantIndex.js +126 -0
  110. package/dist/internal/sectionEndpointResolution.d.ts +32 -0
  111. package/dist/internal/sectionEndpointResolution.js +38 -0
  112. package/dist/layout-bridge/convert/footnoteLayout.js +1 -0
  113. package/dist/layout-bridge/convert/headerFooterLayout.d.ts +16 -4
  114. package/dist/layout-bridge/convert/headerFooterLayout.js +52 -9
  115. package/dist/layout-bridge/convert/toFlowBlocks.d.ts +4 -0
  116. package/dist/layout-bridge/convert/toFlowBlocks.js +391 -82
  117. package/dist/layout-bridge/dom/clickToPositionDom.d.ts +8 -1
  118. package/dist/layout-bridge/dom/clickToPositionDom.js +114 -40
  119. package/dist/layout-bridge/dom/textStreamDom.d.ts +24 -0
  120. package/dist/layout-bridge/dom/textStreamDom.js +75 -0
  121. package/dist/layout-bridge/headerFooterLayout.js +16 -30
  122. package/dist/layout-engine/index.js +12 -3
  123. package/dist/layout-engine/measure/tableFragmentBorderGeometry.d.ts +17 -0
  124. package/dist/layout-engine/measure/tableFragmentBorderGeometry.js +24 -0
  125. package/dist/layout-engine/renderedBreakReconciliation.js +1 -1
  126. package/dist/layout-engine/types.d.ts +7 -0
  127. package/dist/layout-painter/imageLayout.d.ts +1 -1
  128. package/dist/layout-painter/renderTable.js +20 -0
  129. package/dist/markdown/renderBlock.js +7 -10
  130. package/dist/model.d.ts +1 -1
  131. package/dist/paged-layout/rangeProjection.js +6 -9
  132. package/dist/paged-layout/transactionDirtyRange.d.ts +3 -1
  133. package/dist/paged-layout/transactionDirtyRange.js +23 -1
  134. package/dist/prosemirror/attrs/index.d.ts +7 -3
  135. package/dist/prosemirror/attrs/index.js +128 -7
  136. package/dist/prosemirror/bookmarkBoundaryIntegrity.d.ts +9 -0
  137. package/dist/prosemirror/bookmarkBoundaryIntegrity.js +22 -0
  138. package/dist/prosemirror/commands/comments.d.ts +10 -1
  139. package/dist/prosemirror/commands/comments.js +551 -87
  140. package/dist/prosemirror/commands/formatPainter.d.ts +45 -10
  141. package/dist/prosemirror/commands/formatPainter.js +319 -63
  142. package/dist/prosemirror/commands/index.d.ts +2 -2
  143. package/dist/prosemirror/commands/pageBreak.d.ts +5 -2
  144. package/dist/prosemirror/commands/pageBreak.js +18 -36
  145. package/dist/prosemirror/commands/propertyChangeScope.d.ts +27 -3
  146. package/dist/prosemirror/commands/propertyChangeScope.js +85 -14
  147. package/dist/prosemirror/conversion/fromProseDoc.d.ts +8 -5
  148. package/dist/prosemirror/conversion/fromProseDoc.js +935 -223
  149. package/dist/prosemirror/conversion/runShadingMark.js +2 -2
  150. package/dist/prosemirror/conversion/toProseDoc.d.ts +26 -1
  151. package/dist/prosemirror/conversion/toProseDoc.js +439 -291
  152. package/dist/prosemirror/extensions/StarterKit.js +4 -0
  153. package/dist/prosemirror/extensions/core/ParagraphExtension.js +54 -6
  154. package/dist/prosemirror/extensions/features/AutoBidiDetectionExtension.js +7 -1
  155. package/dist/prosemirror/extensions/features/ListExtension.js +95 -123
  156. package/dist/prosemirror/extensions/features/ParaIdAllocatorExtension.js +19 -6
  157. package/dist/prosemirror/extensions/features/ParagraphChangeTrackerExtension.d.ts +27 -1
  158. package/dist/prosemirror/extensions/features/ParagraphChangeTrackerExtension.js +134 -30
  159. package/dist/prosemirror/extensions/features/pasteCleanup.js +9 -15
  160. package/dist/prosemirror/extensions/marks/BoldExtension.js +4 -3
  161. package/dist/prosemirror/extensions/marks/CharacterStyleExtension.d.ts +4 -7
  162. package/dist/prosemirror/extensions/marks/CharacterStyleExtension.js +5 -11
  163. package/dist/prosemirror/extensions/marks/FontSizeExtension.js +3 -3
  164. package/dist/prosemirror/extensions/marks/ItalicExtension.js +4 -3
  165. package/dist/prosemirror/extensions/marks/PageBreakRunOwnerExtension.d.ts +13 -0
  166. package/dist/prosemirror/extensions/marks/PageBreakRunOwnerExtension.js +39 -0
  167. package/dist/prosemirror/extensions/marks/RunFormattingOverrideExtension.d.ts +7 -1
  168. package/dist/prosemirror/extensions/marks/RunFormattingOverrideExtension.js +67 -21
  169. package/dist/prosemirror/extensions/marks/TrackedChangeExtensions.js +2 -0
  170. package/dist/prosemirror/extensions/marks/markUtils.d.ts +8 -1
  171. package/dist/prosemirror/extensions/marks/markUtils.js +88 -2
  172. package/dist/prosemirror/extensions/nodes/BookmarkBoundaryExtension.js +44 -3
  173. package/dist/prosemirror/extensions/nodes/FieldExtension.js +1 -1
  174. package/dist/prosemirror/extensions/nodes/HardBreakExtension.js +15 -4
  175. package/dist/prosemirror/extensions/nodes/PageBreakExtension.d.ts +6 -0
  176. package/dist/prosemirror/extensions/nodes/PageBreakExtension.js +6 -0
  177. package/dist/prosemirror/extensions/nodes/PageBreakRunExtension.d.ts +6 -0
  178. package/dist/prosemirror/extensions/nodes/PageBreakRunExtension.js +36 -0
  179. package/dist/prosemirror/findReplaceSelection.js +8 -3
  180. package/dist/prosemirror/index.d.ts +3 -3
  181. package/dist/prosemirror/listMarker.d.ts +26 -1
  182. package/dist/prosemirror/listMarker.js +108 -14
  183. package/dist/prosemirror/pageBreakRunProjection.d.ts +28 -0
  184. package/dist/prosemirror/pageBreakRunProjection.js +47 -0
  185. package/dist/prosemirror/paraText.js +6 -0
  186. package/dist/prosemirror/paragraphAlignment.d.ts +12 -0
  187. package/dist/prosemirror/paragraphAlignment.js +13 -0
  188. package/dist/prosemirror/paragraphSpacing.d.ts +29 -0
  189. package/dist/prosemirror/paragraphSpacing.js +125 -0
  190. package/dist/prosemirror/plugins/documentNumbering.d.ts +8 -0
  191. package/dist/prosemirror/plugins/documentNumbering.js +18 -0
  192. package/dist/prosemirror/plugins/pmTextScan.d.ts +3 -2
  193. package/dist/prosemirror/plugins/pmTextScan.js +14 -4
  194. package/dist/prosemirror/plugins/suggestionMode.js +9 -11
  195. package/dist/prosemirror/rebaseParagraphRunFormatting.d.ts +16 -0
  196. package/dist/prosemirror/rebaseParagraphRunFormatting.js +61 -0
  197. package/dist/prosemirror/runFormattingInlineCarriers.d.ts +64 -0
  198. package/dist/prosemirror/runFormattingInlineCarriers.js +121 -0
  199. package/dist/prosemirror/runFormattingProvenance.d.ts +12 -0
  200. package/dist/prosemirror/runFormattingProvenance.js +48 -0
  201. package/dist/prosemirror/runFormattingReconciliation.d.ts +25 -0
  202. package/dist/prosemirror/runFormattingReconciliation.js +54 -0
  203. package/dist/prosemirror/runPropertyChangeResolution.d.ts +18 -0
  204. package/dist/prosemirror/runPropertyChangeResolution.js +18 -0
  205. package/dist/prosemirror/runStyleFormatting.d.ts +50 -0
  206. package/dist/prosemirror/runStyleFormatting.js +105 -0
  207. package/dist/prosemirror/schema/index.d.ts +3 -3
  208. package/dist/prosemirror/schema/marks.d.ts +84 -9
  209. package/dist/prosemirror/schema/marks.js +73 -1
  210. package/dist/prosemirror/schema/nodes.d.ts +30 -6
  211. package/dist/prosemirror/styles/resolvedStyleAttrs.d.ts +14 -2
  212. package/dist/prosemirror/styles/resolvedStyleAttrs.js +45 -23
  213. package/dist/prosemirror/styles/styleResolver.d.ts +10 -13
  214. package/dist/prosemirror/styles/styleResolver.js +3 -3
  215. package/dist/prosemirror/styles/styleToggleCascade.d.ts +8 -1
  216. package/dist/prosemirror/styles/styleToggleCascade.js +24 -1
  217. package/dist/prosemirror/tableGridMutation.d.ts +20 -0
  218. package/dist/prosemirror/tableGridMutation.js +30 -0
  219. package/dist/prosemirror/trackedRunInlineAtoms.d.ts +31 -0
  220. package/dist/prosemirror/trackedRunInlineAtoms.js +39 -0
  221. package/dist/prosemirror/utils/visualLineNavigation.js +27 -31
  222. package/dist/prosemirror/validation.js +23 -4
  223. package/dist/redline.js +4 -1
  224. package/dist/render-dom/HeaderFooterSelectionOverlay.js +20 -20
  225. package/dist/render-dom/RenderedDomContext.js +17 -34
  226. package/dist/server.d.ts +4 -4
  227. package/dist/server.js +2 -2
  228. package/dist/style-engine/styleEngine.d.ts +1 -1
  229. package/dist/utils/clipboard.js +1 -1
  230. package/dist/utils/findReplace.js +38 -21
  231. package/dist/utils/mergeDocumentContent.js +3 -3
  232. package/dist/utils/replaceText.js +2 -1
  233. package/dist/version-comparison.d.ts +38 -44
  234. package/dist/version-comparison.js +270 -399
  235. package/dist/watermark/index.js +2 -4
  236. package/package.json +8 -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 };