@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
@@ -10,6 +10,14 @@ type PositionalText = {
10
10
  * preceding block.
11
11
  */
12
12
  pmPositionAt: (textIndex: number) => number;
13
+ /**
14
+ * Resolve a text span without absorbing an explicit zero-width structure.
15
+ * Returns `null` when the span crosses one.
16
+ */
17
+ pmRangeAt: (startTextIndex: number, endTextIndex: number) => {
18
+ from: number;
19
+ to: number;
20
+ } | null;
13
21
  };
14
22
  /**
15
23
  * Walk a slice of the document, returning its text plus a position
@@ -8,9 +8,18 @@ const BLOCK_SEPARATOR = "\n";
8
8
  function buildPositionalText(doc, from = 0, to = doc.content.size) {
9
9
  const chunks = [];
10
10
  const offsets = [];
11
+ const structuralBoundaries = [];
11
12
  let textLength = 0;
12
13
  let lastBlockEnd = null;
13
14
  doc.nodesBetween(from, to, (node, pos) => {
15
+ if (node.type.name === "pageBreakRun") {
16
+ structuralBoundaries.push({
17
+ textOffset: textLength,
18
+ from: pos,
19
+ to: pos + node.nodeSize
20
+ });
21
+ return false;
22
+ }
14
23
  if (node.isText) {
15
24
  const text = node.text ?? "";
16
25
  const startInNode = Math.max(from, pos);
@@ -35,12 +44,25 @@ function buildPositionalText(doc, from = 0, to = doc.content.size) {
35
44
  }
36
45
  return true;
37
46
  });
47
+ const text = chunks.join("");
48
+ const pmPositionAt = (textIndex) => {
49
+ if (textIndex < 0) return offsets[0] ?? from;
50
+ if (textIndex >= offsets.length) return (offsets.at(-1) ?? from) + 1;
51
+ return offsets[textIndex] ?? from;
52
+ };
38
53
  return {
39
- text: chunks.join(""),
40
- pmPositionAt: (textIndex) => {
41
- if (textIndex < 0) return offsets[0] ?? from;
42
- if (textIndex >= offsets.length) return (offsets.at(-1) ?? from) + 1;
43
- return offsets[textIndex] ?? from;
54
+ text,
55
+ pmPositionAt,
56
+ pmRangeAt: (startTextIndex, endTextIndex) => {
57
+ if (!Number.isInteger(startTextIndex) || !Number.isInteger(endTextIndex) || startTextIndex < 0 || endTextIndex <= startTextIndex || endTextIndex > text.length || structuralBoundaries.some(({ textOffset }) => textOffset > startTextIndex && textOffset < endTextIndex)) return null;
58
+ const rangeFrom = pmPositionAt(startTextIndex);
59
+ const finalCharacter = offsets[endTextIndex - 1];
60
+ if (finalCharacter === void 0) return null;
61
+ const rangeTo = finalCharacter + 1;
62
+ return rangeFrom <= rangeTo ? {
63
+ from: rangeFrom,
64
+ to: rangeTo
65
+ } : null;
44
66
  }
45
67
  };
46
68
  }
@@ -88,10 +88,9 @@ const EMPTY_PARAGRAPH = {
88
88
  text: ""
89
89
  };
90
90
  /**
91
- * A container may not end with a table: the format requires a paragraph after
92
- * one, and a body's section properties do not supply it. One rule for both
93
- * containers, because a fixture that is well formed in a cell and malformed in
94
- * the body would be measuring two different things.
91
+ * Most fixtures add a paragraph after a table so later paragraph insertions
92
+ * have an explicit anchor. Tests for a schema-valid terminal table remove that
93
+ * convenience paragraph deliberately.
95
94
  */
96
95
  const closedSequence = (items) => {
97
96
  const last = items.at(-1);
@@ -1,27 +1,27 @@
1
- import { FolioAIBlock, FolioAIBlockTableLocation } from "../ai-edits/types.js";
1
+ import { FolioContentBlock, FolioContentTableLocation } from "./content-types.js";
2
2
  //#region src/compare/column-alignment.d.ts
3
- type TableColumnAlignmentStep = {
3
+ type TableColumnAlignmentStep<Block extends FolioContentBlock = FolioContentBlock> = {
4
4
  type: "baseColumn";
5
- blocks: readonly FolioAIBlock[];
6
- location: FolioAIBlockTableLocation;
5
+ blocks: readonly Block[];
6
+ location: FolioContentTableLocation;
7
7
  columnIndex: number;
8
8
  } | {
9
- type: "targetColumn";
10
- blocks: readonly FolioAIBlock[];
11
- location: FolioAIBlockTableLocation;
9
+ type: "revisedColumn";
10
+ blocks: readonly Block[];
11
+ location: FolioContentTableLocation;
12
12
  columnIndex: number;
13
13
  anchor: {
14
14
  blockId: string;
15
15
  position: "after" | "before";
16
16
  };
17
17
  };
18
- type TableColumnAlignment = {
19
- steps: TableColumnAlignmentStep[];
20
- baseBlocks: FolioAIBlock[];
21
- targetBlocks: FolioAIBlock[];
18
+ type TableColumnAlignment<Block extends FolioContentBlock = FolioContentBlock> = {
19
+ steps: TableColumnAlignmentStep<Block>[];
20
+ baseBlocks: Block[];
21
+ revisedBlocks: Block[];
22
22
  baseColumnKeys: ReadonlyMap<number, number>;
23
- targetColumnKeys: ReadonlyMap<number, number>;
23
+ revisedColumnKeys: ReadonlyMap<number, number>;
24
24
  };
25
- declare const alignTableColumns: (baseBlocks: readonly FolioAIBlock[], targetBlocks: readonly FolioAIBlock[]) => TableColumnAlignment | null;
25
+ declare const alignTableColumns: <Block extends FolioContentBlock>(baseBlocks: readonly Block[], revisedBlocks: readonly Block[]) => TableColumnAlignment<Block> | null;
26
26
  //#endregion
27
27
  export { TableColumnAlignment, TableColumnAlignmentStep, alignTableColumns };
@@ -2,7 +2,7 @@ import { panic } from "better-result";
2
2
  //#region src/compare/column-alignment.ts
3
3
  const MAX_TABLE_GRID_COLUMNS = 63;
4
4
  const MAX_TABLE_GRID_AREA = 1e6;
5
- const tableGridColumns = (blocks) => {
5
+ const extractTableGrid = (blocks) => {
6
6
  const cellsByPhysicalLocation = /* @__PURE__ */ new Map();
7
7
  let width = 0;
8
8
  let height = 0;
@@ -17,7 +17,7 @@ const tableGridColumns = (blocks) => {
17
17
  }
18
18
  const right = location.gridColumnIndex + location.columnSpan;
19
19
  const bottom = location.rowIndex + location.rowSpan;
20
- if (location.gridColumnIndex < 0 || location.rowIndex < 0 || location.columnSpan < 1 || location.rowSpan < 1 || right > MAX_TABLE_GRID_COLUMNS) return null;
20
+ if (!Number.isSafeInteger(location.gridColumnIndex) || !Number.isSafeInteger(location.rowIndex) || !Number.isSafeInteger(location.columnSpan) || !Number.isSafeInteger(location.rowSpan) || !Number.isSafeInteger(right) || !Number.isSafeInteger(bottom) || location.gridColumnIndex < 0 || location.rowIndex < 0 || location.columnSpan < 1 || location.rowSpan < 1 || right > MAX_TABLE_GRID_COLUMNS) return null;
21
21
  cellsByPhysicalLocation.set(key, {
22
22
  blocks: [block],
23
23
  rowIndex: location.rowIndex,
@@ -29,30 +29,58 @@ const tableGridColumns = (blocks) => {
29
29
  height = Math.max(height, bottom);
30
30
  }
31
31
  if (width === 0 || height === 0 || width * height > MAX_TABLE_GRID_AREA) return null;
32
- const grid = Array.from({ length: height }, () => Array.from({ length: width }));
33
- const ownedCellsByColumn = Array.from({ length: width }, () => []);
34
- for (const cell of cellsByPhysicalLocation.values()) {
35
- ownedCellsByColumn[cell.gridColumnIndex]?.push(cell);
36
- for (let row = cell.rowIndex; row < cell.rowIndex + cell.rowSpan; row++) for (let column = cell.gridColumnIndex; column < cell.gridColumnIndex + cell.columnSpan; column++) {
37
- const gridRow = grid[row];
38
- if (!gridRow || gridRow[column] !== void 0) return null;
39
- gridRow[column] = cell;
32
+ const occupancyEvents = [...cellsByPhysicalLocation.values()].flatMap((cell) => {
33
+ const mask = (1n << BigInt(cell.columnSpan)) - 1n << BigInt(cell.gridColumnIndex);
34
+ return [{
35
+ rowIndex: cell.rowIndex,
36
+ type: "enter",
37
+ mask
38
+ }, {
39
+ rowIndex: cell.rowIndex + cell.rowSpan,
40
+ type: "leave",
41
+ mask
42
+ }];
43
+ });
44
+ occupancyEvents.sort((left, right) => {
45
+ const rowOrder = left.rowIndex - right.rowIndex;
46
+ if (rowOrder !== 0 || left.type === right.type) return rowOrder;
47
+ return left.type === "leave" ? -1 : 1;
48
+ });
49
+ let occupiedColumns = 0n;
50
+ for (const event of occupancyEvents) {
51
+ if (event.type === "leave") {
52
+ occupiedColumns &= ~event.mask;
53
+ continue;
40
54
  }
55
+ if ((occupiedColumns & event.mask) !== 0n) return null;
56
+ occupiedColumns |= event.mask;
41
57
  }
42
- return Array.from({ length: width }, (_unused, columnIndex) => {
43
- const structuralRows = grid.map((row) => {
44
- const cell = row[columnIndex];
45
- return cell ? [
46
- columnIndex - cell.gridColumnIndex,
47
- cell.columnSpan,
48
- cell.rowIndex,
49
- cell.rowSpan,
50
- cell.blocks.map(({ text }) => text)
51
- ] : null;
52
- });
58
+ return {
59
+ cells: [...cellsByPhysicalLocation.values()],
60
+ width,
61
+ height
62
+ };
63
+ };
64
+ const tableGridColumns = (grid, internText) => {
65
+ const coveringCellsByColumn = Array.from({ length: grid.width }, () => []);
66
+ const ownedCellsByColumn = Array.from({ length: grid.width }, () => []);
67
+ const textKeysByCell = /* @__PURE__ */ new Map();
68
+ for (const cell of grid.cells) {
69
+ textKeysByCell.set(cell, cell.blocks.map(({ text }) => internText(text)));
70
+ ownedCellsByColumn[cell.gridColumnIndex]?.push(cell);
71
+ for (let column = cell.gridColumnIndex; column < cell.gridColumnIndex + cell.columnSpan; column++) coveringCellsByColumn[column]?.push(cell);
72
+ }
73
+ return Array.from({ length: grid.width }, (_unused, columnIndex) => {
74
+ const structuralCells = [...coveringCellsByColumn[columnIndex] ?? []].toSorted((left, right) => left.rowIndex - right.rowIndex).map((cell) => [
75
+ columnIndex - cell.gridColumnIndex,
76
+ cell.columnSpan,
77
+ cell.rowIndex,
78
+ cell.rowSpan,
79
+ textKeysByCell.get(cell) ?? panic("A table cell has no interned text signature")
80
+ ]);
53
81
  return {
54
82
  index: columnIndex,
55
- signature: JSON.stringify(structuralRows),
83
+ signature: JSON.stringify([grid.height, structuralCells]),
56
84
  ownedCells: ownedCellsByColumn[columnIndex] ?? []
57
85
  };
58
86
  });
@@ -90,14 +118,25 @@ const columnOwnedBlocks = ({ ownedCells }) => {
90
118
  if (ownedCells.length === 0 || ownedCells.some(({ columnSpan }) => columnSpan !== 1)) return null;
91
119
  return ownedCells.flatMap(({ blocks }) => blocks);
92
120
  };
93
- const alignTableColumns = (baseBlocks, targetBlocks) => {
94
- const baseColumns = tableGridColumns(baseBlocks);
95
- const targetColumns = tableGridColumns(targetBlocks);
96
- if (!baseColumns || !targetColumns || baseColumns.length === targetColumns.length) return null;
97
- const targetIsWider = targetColumns.length > baseColumns.length;
98
- const mapping = uniqueColumnEmbedding(targetIsWider ? baseColumns : targetColumns, targetIsWider ? targetColumns : baseColumns);
121
+ const alignTableColumns = (baseBlocks, revisedBlocks) => {
122
+ const baseGrid = extractTableGrid(baseBlocks);
123
+ const revisedGrid = extractTableGrid(revisedBlocks);
124
+ if (!baseGrid || !revisedGrid || baseGrid.width === revisedGrid.width) return null;
125
+ const textKeys = /* @__PURE__ */ new Map();
126
+ let nextTextKey = 0;
127
+ const internText = (text) => {
128
+ const existing = textKeys.get(text);
129
+ if (existing !== void 0) return existing;
130
+ const key = nextTextKey++;
131
+ textKeys.set(text, key);
132
+ return key;
133
+ };
134
+ const baseColumns = tableGridColumns(baseGrid, internText);
135
+ const revisedColumns = tableGridColumns(revisedGrid, internText);
136
+ const revisedIsWider = revisedColumns.length > baseColumns.length;
137
+ const mapping = uniqueColumnEmbedding(revisedIsWider ? baseColumns : revisedColumns, revisedIsWider ? revisedColumns : baseColumns);
99
138
  if (!mapping) return null;
100
- const widerColumns = targetIsWider ? targetColumns : baseColumns;
139
+ const widerColumns = revisedIsWider ? revisedColumns : baseColumns;
101
140
  const mapped = new Set(mapping);
102
141
  const unmatchedColumns = widerColumns.filter((_column, index) => !mapped.has(index));
103
142
  const ownedColumns = [];
@@ -109,21 +148,21 @@ const alignTableColumns = (baseBlocks, targetBlocks) => {
109
148
  blocks
110
149
  });
111
150
  }
112
- if (ownedColumns.length === 0 || targetIsWider && unmatchedColumns.some(({ ownedCells }) => ownedCells.some(({ rowSpan }) => rowSpan !== 1))) return null;
151
+ if (ownedColumns.length === 0 || revisedIsWider && unmatchedColumns.some(({ ownedCells }) => ownedCells.some(({ rowSpan }) => rowSpan !== 1))) return null;
113
152
  const unmatchedIds = new Set(ownedColumns.flatMap(({ blocks }) => blocks.map(({ id }) => id)));
114
153
  const baseColumnKeys = /* @__PURE__ */ new Map();
115
- const targetColumnKeys = /* @__PURE__ */ new Map();
154
+ const revisedColumnKeys = /* @__PURE__ */ new Map();
116
155
  mapping.forEach((wideIndex, shortIndex) => {
117
- if (targetIsWider) {
156
+ if (revisedIsWider) {
118
157
  baseColumnKeys.set(shortIndex, shortIndex);
119
- targetColumnKeys.set(wideIndex, shortIndex);
158
+ revisedColumnKeys.set(wideIndex, shortIndex);
120
159
  } else {
121
160
  baseColumnKeys.set(wideIndex, shortIndex);
122
- targetColumnKeys.set(shortIndex, shortIndex);
161
+ revisedColumnKeys.set(shortIndex, shortIndex);
123
162
  }
124
163
  });
125
164
  const steps = [];
126
- if (!targetIsWider) for (const { column, blocks } of ownedColumns) {
165
+ if (!revisedIsWider) for (const { column, blocks } of ownedColumns) {
127
166
  const location = blocks.at(0)?.table;
128
167
  if (!location) return null;
129
168
  steps.push({
@@ -140,7 +179,7 @@ const alignTableColumns = (baseBlocks, targetBlocks) => {
140
179
  const anchorBlock = (rightBaseIndex >= 0 ? baseColumns[rightBaseIndex] : baseColumns[leftBaseIndex])?.ownedCells.at(0)?.blocks.at(0);
141
180
  if (!location || !anchorBlock) return null;
142
181
  steps.push({
143
- type: "targetColumn",
182
+ type: "revisedColumn",
144
183
  blocks,
145
184
  location,
146
185
  columnIndex: column.index,
@@ -152,10 +191,10 @@ const alignTableColumns = (baseBlocks, targetBlocks) => {
152
191
  }
153
192
  return {
154
193
  steps,
155
- baseBlocks: baseBlocks.filter(({ id }) => targetIsWider || !unmatchedIds.has(id)),
156
- targetBlocks: targetBlocks.filter(({ id }) => !targetIsWider || !unmatchedIds.has(id)),
194
+ baseBlocks: baseBlocks.filter(({ id }) => revisedIsWider || !unmatchedIds.has(id)),
195
+ revisedBlocks: revisedBlocks.filter(({ id }) => !revisedIsWider || !unmatchedIds.has(id)),
157
196
  baseColumnKeys,
158
- targetColumnKeys
197
+ revisedColumnKeys
159
198
  };
160
199
  };
161
200
  //#endregion
@@ -1,4 +1,4 @@
1
- import { FolioAIEditSnapshot } from "../ai-edits/types.js";
1
+ import { FolioAIEditSkipReason, FolioAIEditSnapshot } from "../ai-edits/types.js";
2
2
  import { WordDiffGranularity } from "../ai-edits/word-diff.js";
3
3
  import { FolioRevisionStamp } from "../ai-edits/apply.js";
4
4
  import { FolioDocumentStoryHandle, FolioDocxReviewer } from "../ai-edits/headless.js";
@@ -50,7 +50,8 @@ type PlannedStoryComparison = {
50
50
  * Stage 2: align every paired story and derive its operations. Pure — no
51
51
  * parsing, no serialization, no clock.
52
52
  */
53
- declare const planComparison: ({ pairs }: ParsedComparison) => Result<readonly PlannedStoryComparison[], CompareDocxOperationLimitError>;
53
+ declare const planComparison: ({ pairs, targetReviewer }: ParsedComparison) => Result<readonly PlannedStoryComparison[], CompareDocxOperationLimitError>;
54
+ declare const getCompareSkipDisposition: (reason: FolioAIEditSkipReason) => "fatal" | "unwritable";
54
55
  /** What stage 3 produced: the change list, whether it was proven, and whether it wrote anything. */
55
56
  type AppliedComparison = {
56
57
  changes: readonly CompareChange[];
@@ -111,4 +112,4 @@ declare const serializeComparison: ({ baseBuffer, baseCarriedRevisions, reviewer
111
112
  */
112
113
  declare const compareDocx: (base: ArrayBuffer, target: ArrayBuffer, options: CompareDocxOptions) => Promise<Result<CompareResult, CompareDocxError>>;
113
114
  //#endregion
114
- export { AppliedComparison, ComparedStoryPair, MAX_COMPARE_OPERATIONS, ParsedComparison, PlannedStoryComparison, applyComparison, compareDocx, parseComparison, planComparison, serializeComparison };
115
+ export { AppliedComparison, ComparedStoryPair, MAX_COMPARE_OPERATIONS, ParsedComparison, PlannedStoryComparison, applyComparison, compareDocx, getCompareSkipDisposition, parseComparison, planComparison, serializeComparison };
@@ -1,8 +1,11 @@
1
1
  import { FolioDocxReviewer } from "../ai-edits/headless.js";
2
2
  import { numberingReferenceKeysOf } from "../ai-edits/snapshot.js";
3
3
  import { projectTableGeometry } from "../ai-edits/table-geometry.js";
4
+ import { tableTemplateCanCrossPackageLosslessly } from "../ai-edits/table-template.js";
5
+ import { createScopedWordDiffOptions } from "../ai-edits/word-diff.js";
4
6
  import "../document-operations.js";
5
7
  import { pairFolioDocumentStories } from "../document-stories.js";
8
+ import { createContentComparisonWorkSession } from "./content.js";
6
9
  import { planStoryCompare } from "./plan.js";
7
10
  import { withFixedPackageDates } from "./reproducible-package.js";
8
11
  import { CompareDocxApplyError, CompareDocxFinalParagraphMarkError, CompareDocxOperationLimitError, CompareDocxParseError, CompareDocxRoundTripError, CompareDocxSerializeError, InvalidCompareDocxOptionsError } from "./types.js";
@@ -76,16 +79,6 @@ const existingRevisionsOf = (reviewer) => {
76
79
  present
77
80
  };
78
81
  };
79
- /**
80
- * One story's text-and-structure projection: every block's text tagged with
81
- * the table cell it sits in. The tag is what makes the self-check below see a
82
- * paragraph that landed beside a table instead of inside it.
83
- */
84
- const projectBlocks = (blocks) => {
85
- return blocks.map(({ text, table, styleId, listLevel }) => {
86
- return `${table ? `t${String(table.tableIndex)}r${String(table.rowIndex)}c${String(table.cellIndex)}g${String(table.gridColumnIndex)}x${String(table.columnSpan)}y${String(table.rowSpan)}p${String(table.paragraphIndex)}` : "body"}|${styleId ?? ""}|${listLevel ?? ""}|${text}`;
87
- });
88
- };
89
82
  /** Verify formatting only where the plan claims a text-equal formatting change. */
90
83
  const formattingRoundTripFailure = ({ invariant, story, changes, actualBlocks, expectedBlocks, expectedBlockId }) => {
91
84
  const expectedIndexById = new Map(expectedBlocks.map(({ id }, index) => [id, index]));
@@ -224,19 +217,49 @@ const parseComparison = async (base, target, options) => {
224
217
  unsupported
225
218
  });
226
219
  };
220
+ const planCopiesNonPortableWholeTable = (targetReviewer, pair, plan) => {
221
+ const targetTables = new Map(targetReviewer.storyTables({ story: pair.targetStory }).map(({ index, node }) => [index, node]));
222
+ return plan.tableTemplates.some(({ targetRowIndex, targetTableIndex }) => {
223
+ if (targetRowIndex !== void 0) return false;
224
+ const table = targetTables.get(targetTableIndex);
225
+ return table === void 0 || !tableTemplateCanCrossPackageLosslessly(table);
226
+ });
227
+ };
227
228
  /**
228
229
  * Stage 2: align every paired story and derive its operations. Pure — no
229
230
  * parsing, no serialization, no clock.
230
231
  */
231
- const planComparison = ({ pairs }) => {
232
+ const planComparison = ({ pairs, targetReviewer }) => {
232
233
  const planned = [];
234
+ const workSession = createContentComparisonWorkSession();
235
+ let remainingOperations = MAX_COMPARE_OPERATIONS;
233
236
  for (const pair of pairs) {
234
- const plan = planStoryCompare({
237
+ const remainingLcsCells = workSession.alignment.remainingLcsCells;
238
+ const remainingStructuralTokenLookups = workSession.alignment.remainingStructuralTokenLookups;
239
+ const remainingMoveComparisons = workSession.remainingMoveComparisons;
240
+ const remainingMoveTokenLookups = workSession.remainingMoveTokenLookups;
241
+ let plan = planStoryCompare({
235
242
  story: pair.baseStory,
236
243
  baseSnapshot: pair.baseSnapshot,
237
244
  targetSnapshot: pair.targetSnapshot,
238
- maxOperations: MAX_COMPARE_OPERATIONS
245
+ maxOperations: remainingOperations,
246
+ wholeTableReplacement: "allow",
247
+ workSession
239
248
  });
249
+ if (plan && planCopiesNonPortableWholeTable(targetReviewer, pair, plan)) {
250
+ workSession.alignment.remainingLcsCells = remainingLcsCells;
251
+ workSession.alignment.remainingStructuralTokenLookups = remainingStructuralTokenLookups;
252
+ workSession.remainingMoveComparisons = remainingMoveComparisons;
253
+ workSession.remainingMoveTokenLookups = remainingMoveTokenLookups;
254
+ plan = planStoryCompare({
255
+ story: pair.baseStory,
256
+ baseSnapshot: pair.baseSnapshot,
257
+ targetSnapshot: pair.targetSnapshot,
258
+ maxOperations: remainingOperations,
259
+ wholeTableReplacement: "avoid",
260
+ workSession
261
+ });
262
+ }
240
263
  if (plan === null) return Result.err(new CompareDocxOperationLimitError({
241
264
  message: "The comparison needs more operations than the engine generates.",
242
265
  limit: MAX_COMPARE_OPERATIONS
@@ -245,6 +268,7 @@ const planComparison = ({ pairs }) => {
245
268
  pair,
246
269
  plan
247
270
  });
271
+ remainingOperations -= plan.operations.length;
248
272
  }
249
273
  return Result.ok(planned);
250
274
  };
@@ -275,10 +299,13 @@ const COMPARE_SKIP_DISPOSITION = {
275
299
  preconditionFailed: "fatal",
276
300
  staleRange: "fatal",
277
301
  emptyOperation: "unwritable",
302
+ pendingParagraphPropertyChange: "unwritable",
303
+ pendingRunPropertyChange: "unwritable",
278
304
  noopOperation: "unwritable",
279
305
  documentVersionMismatch: "fatal",
280
306
  documentNotEditable: "fatal"
281
307
  };
308
+ const getCompareSkipDisposition = (reason) => COMPARE_SKIP_DISPOSITION[reason];
282
309
  /**
283
310
  * The table each `insertTable` / `insertTableRow` in the plan should place,
284
311
  * resolved against the target document the plan named it in.
@@ -321,14 +348,14 @@ const applyComparison = ({ reviewer, targetReviewer, revisionStamp, granularity,
321
348
  const failures = [];
322
349
  let idSeed = revisionStamp.idSeed;
323
350
  let documentChanged = false;
351
+ const wordDiff = createScopedWordDiffOptions({ granularity });
324
352
  for (const { pair, plan } of planned) {
325
353
  changes.push(...plan.changes);
326
354
  if (plan.operations.length === 0 && plan.tableGeometryPairings.length === 0) continue;
327
- const baseBeforeBlocks = reviewer.readReviewedStory({
355
+ const baseBefore = reviewer.readReviewedStory({
328
356
  story: pair.baseStory,
329
357
  view: "final"
330
358
  })?.snapshot.blocks ?? [];
331
- const baseBefore = projectBlocks(baseBeforeBlocks);
332
359
  const baseBeforeGeometry = projectTableGeometry(reviewer.storyTables({ story: pair.baseStory }));
333
360
  const targetTables = new Map(targetReviewer.storyTables({ story: pair.targetStory }).map(({ index, node }) => [index, node]));
334
361
  const afterGeometry = reviewer.matchStoryTableGeometry({
@@ -352,7 +379,7 @@ const applyComparison = ({ reviewer, targetReviewer, revisionStamp, granularity,
352
379
  date: revisionStamp.date,
353
380
  idSeed
354
381
  },
355
- wordDiff: { granularity },
382
+ wordDiff,
356
383
  batch: {
357
384
  version: 1,
358
385
  mode: "tracked-changes",
@@ -363,7 +390,7 @@ const applyComparison = ({ reviewer, targetReviewer, revisionStamp, granularity,
363
390
  if (nextRevisionId === void 0) panic("The applier did not report where it left the revision-id counter", { story: pair.baseStory });
364
391
  idSeed = nextRevisionId;
365
392
  documentChanged = true;
366
- const refused = skipped.filter(({ reason }) => COMPARE_SKIP_DISPOSITION[reason] === "fatal");
393
+ const refused = skipped.filter(({ reason }) => getCompareSkipDisposition(reason) === "fatal");
367
394
  if (refused.length > 0) return Result.err(new CompareDocxApplyError({
368
395
  message: "Some derived operations were refused, so the result would not match the target.",
369
396
  skipped: refused
@@ -376,8 +403,8 @@ const applyComparison = ({ reviewer, targetReviewer, revisionStamp, granularity,
376
403
  const acceptFailure = classifyProjectionMismatch({
377
404
  invariant: "accept-reproduces-target",
378
405
  story: pair.baseStory,
379
- actual: projectBlocks(acceptedStory?.snapshot.blocks ?? []),
380
- expected: projectBlocks(pair.targetSnapshot.blocks)
406
+ actual: acceptedStory?.snapshot.blocks ?? [],
407
+ expected: pair.targetSnapshot.blocks
381
408
  });
382
409
  if (acceptFailure) failures.push(acceptFailure);
383
410
  else {
@@ -398,7 +425,7 @@ const applyComparison = ({ reviewer, targetReviewer, revisionStamp, granularity,
398
425
  const rejectFailure = classifyProjectionMismatch({
399
426
  invariant: "reject-reproduces-base",
400
427
  story: pair.baseStory,
401
- actual: projectBlocks(rejectedStory?.snapshot.blocks ?? []),
428
+ actual: rejectedStory?.snapshot.blocks ?? [],
402
429
  expected: baseBefore
403
430
  });
404
431
  if (rejectFailure) failures.push(rejectFailure);
@@ -519,4 +546,4 @@ const compareDocx = async (base, target, options) => {
519
546
  });
520
547
  };
521
548
  //#endregion
522
- export { MAX_COMPARE_OPERATIONS, applyComparison, compareDocx, parseComparison, planComparison, serializeComparison };
549
+ export { MAX_COMPARE_OPERATIONS, applyComparison, compareDocx, getCompareSkipDisposition, parseComparison, planComparison, serializeComparison };
@@ -0,0 +1,95 @@
1
+ import { FolioContentBlock, FolioContentIdStability, FolioContentTableLocation } from "./content-types.js";
2
+ import { TableColumnAlignmentStep } from "./column-alignment.js";
3
+ //#region src/compare/content-alignment.d.ts
4
+ type FolioContentAlignmentWorkSession = {
5
+ remainingLcsCells: number;
6
+ remainingStructuralTokenLookups: number;
7
+ };
8
+ type CreateFolioContentAlignmentWorkSessionOptions = {
9
+ /** @internal A lower allowance used by focused boundary tests. */
10
+ lcsCells?: number;
11
+ /** @internal A lower allowance used by focused boundary tests. */
12
+ structuralTokenLookups?: number;
13
+ };
14
+ declare const createFolioContentAlignmentWorkSession: (options?: CreateFolioContentAlignmentWorkSessionOptions) => FolioContentAlignmentWorkSession;
15
+ declare const exceedsFolioContentLcsBudget: (baseCount: number, revisedCount: number) => boolean;
16
+ type FolioContentBlockPair = {
17
+ baseIndex: number;
18
+ revisedIndex: number;
19
+ };
20
+ /**
21
+ * Longest strictly increasing subsequence by revised index.
22
+ *
23
+ * The Fenwick tree stores the earliest end index for equal-length candidates,
24
+ * preserving the historical deterministic tie break while reducing the
25
+ * stable-anchor pass from quadratic to O(n log n).
26
+ */
27
+ declare const longestIncreasingFolioContentPairs: (pairs: readonly FolioContentBlockPair[]) => FolioContentBlockPair[];
28
+ type FolioContentAlignedBlockEvent<Block extends FolioContentBlock = FolioContentBlock> = {
29
+ type: "pair";
30
+ baseBlock: Block;
31
+ revisedBlock: Block;
32
+ } | {
33
+ type: "baseOnly";
34
+ block: Block;
35
+ } | {
36
+ type: "revisedOnly";
37
+ block: Block;
38
+ };
39
+ type AlignFolioContentBlocksOptions<Block extends FolioContentBlock> = {
40
+ workSession?: FolioContentAlignmentWorkSession;
41
+ /** Let an already-aligned structural slot outrank differing authored IDs. */
42
+ stableIdMismatch?: "pair" | "separate";
43
+ /** @internal Compatibility hook for adapters whose source model predates `idStability`. */
44
+ idStability?: ((block: Block) => FolioContentIdStability) | undefined;
45
+ };
46
+ declare const alignFolioContentBlocks: <Block extends FolioContentBlock>(baseBlocks: readonly Block[], revisedBlocks: readonly Block[], options?: AlignFolioContentBlocksOptions<Block>) => FolioContentAlignedBlockEvent<Block>[];
47
+ type FolioContentAlignmentStep<Block extends FolioContentBlock = FolioContentBlock> = {
48
+ type: "pair";
49
+ baseBlock: Block;
50
+ revisedBlock: Block;
51
+ } | {
52
+ type: "baseOnly";
53
+ block: Block;
54
+ moveScope: FolioContentMoveScope;
55
+ } | {
56
+ type: "revisedOnly";
57
+ block: Block;
58
+ moveScope: FolioContentMoveScope;
59
+ } | {
60
+ type: "baseRow";
61
+ blocks: readonly Block[];
62
+ location: FolioContentTableLocation;
63
+ } | {
64
+ type: "revisedRow";
65
+ blocks: readonly Block[];
66
+ location: FolioContentTableLocation;
67
+ } | {
68
+ type: "baseTable";
69
+ blocks: readonly Block[];
70
+ location: FolioContentTableLocation;
71
+ } | {
72
+ type: "revisedTable";
73
+ blocks: readonly Block[];
74
+ location: FolioContentTableLocation;
75
+ } | TableColumnAlignmentStep<Block>;
76
+ /** @internal Legal move bucket and alignment gap for one unpaired block. */
77
+ type FolioContentMoveScope = {
78
+ readonly bucket: number;
79
+ readonly gap: number;
80
+ };
81
+ declare const groupFolioContentTableRows: <Block extends FolioContentBlock>(blocks: readonly Block[]) => Block[][];
82
+ type AlignFolioContentStructureOptions<Block extends FolioContentBlock> = {
83
+ baseBlocks: readonly Block[];
84
+ revisedBlocks: readonly Block[];
85
+ workSession?: FolioContentAlignmentWorkSession;
86
+ wholeTableReplacement?: "allow" | "avoid";
87
+ /** Let a structurally aligned slot outrank different stable identifiers. */
88
+ stableIdMismatch?: "pair" | "separate";
89
+ /** @internal Compatibility hook for adapters whose source model predates `idStability`. */
90
+ idStability?: ((block: Block) => FolioContentIdStability) | undefined;
91
+ };
92
+ declare const alignFolioContentStructure: <Block extends FolioContentBlock>({ baseBlocks, revisedBlocks, workSession, wholeTableReplacement, stableIdMismatch, idStability }: AlignFolioContentStructureOptions<Block>) => FolioContentAlignmentStep<Block>[];
93
+ declare const contentBlocksShareContainer: (left: FolioContentBlock, right: FolioContentBlock) => boolean;
94
+ //#endregion
95
+ export { AlignFolioContentBlocksOptions, AlignFolioContentStructureOptions, CreateFolioContentAlignmentWorkSessionOptions, FolioContentAlignedBlockEvent, FolioContentAlignmentStep, FolioContentAlignmentWorkSession, FolioContentBlockPair, FolioContentMoveScope, alignFolioContentBlocks, alignFolioContentStructure, contentBlocksShareContainer, createFolioContentAlignmentWorkSession, exceedsFolioContentLcsBudget, groupFolioContentTableRows, longestIncreasingFolioContentPairs };