@stll/folio-core 0.37.4 → 0.38.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 (131) hide show
  1. package/dist/ai-edits/apply.js +275 -88
  2. package/dist/ai-edits/clean-text.js +12 -1
  3. package/dist/ai-edits/headless.d.ts +110 -0
  4. package/dist/ai-edits/headless.js +569 -40
  5. package/dist/ai-edits/index.d.ts +2 -1
  6. package/dist/ai-edits/snapshot.d.ts +24 -1
  7. package/dist/ai-edits/snapshot.js +206 -9
  8. package/dist/ai-edits/table-geometry.d.ts +7 -7
  9. package/dist/ai-edits/table-geometry.js +43 -27
  10. package/dist/ai-edits/table-template.d.ts +2 -1
  11. package/dist/ai-edits/table-template.js +9 -3
  12. package/dist/ai-edits/types.d.ts +38 -16
  13. package/dist/compare/__fixtures__/body-sequence.d.ts +4 -0
  14. package/dist/compare/__fixtures__/body-sequence.js +5 -3
  15. package/dist/compare/compare.d.ts +26 -2
  16. package/dist/compare/compare.js +444 -16
  17. package/dist/compare/content-alignment.js +216 -137
  18. package/dist/compare/content-types.d.ts +36 -2
  19. package/dist/compare/content.d.ts +13 -10
  20. package/dist/compare/content.js +69 -23
  21. package/dist/compare/formatting.d.ts +4 -22
  22. package/dist/compare/formatting.js +8 -112
  23. package/dist/compare/inline-atom-resources.d.ts +6 -0
  24. package/dist/compare/inline-atom-resources.js +26 -0
  25. package/dist/compare/inline-atoms.d.ts +35 -0
  26. package/dist/compare/inline-atoms.js +383 -0
  27. package/dist/compare/inline-provenance.d.ts +36 -0
  28. package/dist/compare/inline-provenance.js +334 -0
  29. package/dist/compare/plan.d.ts +4 -1
  30. package/dist/compare/plan.js +32 -4
  31. package/dist/compare/scenario.d.ts +4 -6
  32. package/dist/compare/section-boundary-properties.d.ts +63 -0
  33. package/dist/compare/section-boundary-properties.js +235 -0
  34. package/dist/compare/style-resources.d.ts +30 -0
  35. package/dist/compare/style-resources.js +404 -0
  36. package/dist/compare/types.d.ts +50 -11
  37. package/dist/compare/types.js +3 -1
  38. package/dist/compare/verification.d.ts +3 -5
  39. package/dist/compare/verification.js +8 -39
  40. package/dist/compat/eigenpal.d.ts +4 -4
  41. package/dist/compat/eigenpal.js +3 -3
  42. package/dist/document-operations.d.ts +5 -3
  43. package/dist/document-operations.js +112 -5
  44. package/dist/docx/blockContentParser.js +14 -2
  45. package/dist/docx/drawingIdNormalization.js +43 -2
  46. package/dist/docx/drawingRelationships.d.ts +10 -0
  47. package/dist/docx/drawingRelationships.js +28 -0
  48. package/dist/docx/fieldParser.js +1 -1
  49. package/dist/docx/hyperlinkParser.js +3 -5
  50. package/dist/docx/imageRawXml.d.ts +8 -0
  51. package/dist/docx/imageRawXml.js +13 -0
  52. package/dist/docx/newImage.d.ts +2 -2
  53. package/dist/docx/newImage.js +3 -3
  54. package/dist/docx/numberingParser.d.ts +1 -13
  55. package/dist/docx/numberingParser.js +31 -37
  56. package/dist/docx/paragraphParser.js +12 -9
  57. package/dist/docx/paragraphPropertySource.d.ts +8 -5
  58. package/dist/docx/paragraphPropertySource.js +16 -4
  59. package/dist/docx/removeHeaderFooterParts.d.ts +14 -0
  60. package/dist/docx/removeHeaderFooterParts.js +87 -0
  61. package/dist/docx/rezip.d.ts +3 -1
  62. package/dist/docx/rezip.js +143 -41
  63. package/dist/docx/runParser.js +16 -5
  64. package/dist/docx/sectionParser.js +3 -0
  65. package/dist/docx/sectionReferenceHistory.d.ts +17 -0
  66. package/dist/docx/sectionReferenceHistory.js +59 -0
  67. package/dist/docx/selectiveSave.js +11 -31
  68. package/dist/docx/selectiveXmlPatch.d.ts +8 -1
  69. package/dist/docx/selectiveXmlPatch.js +62 -2
  70. package/dist/docx/serializer/borderSerializer.d.ts +4 -1
  71. package/dist/docx/serializer/borderSerializer.js +20 -18
  72. package/dist/docx/serializer/commentSerializer.js +1 -1
  73. package/dist/docx/serializer/numberingSerializer.js +1 -1
  74. package/dist/docx/serializer/paragraphSerializer.js +13 -177
  75. package/dist/docx/serializer/partNamespaces.d.ts +1 -1
  76. package/dist/docx/serializer/partNamespaces.js +4 -0
  77. package/dist/docx/serializer/runSerializer.d.ts +1 -5
  78. package/dist/docx/serializer/runSerializer.js +4 -130
  79. package/dist/docx/serializer/sectionPropertiesSerializer.js +5 -2
  80. package/dist/docx/serializer/stylesSerializer.js +1 -1
  81. package/dist/docx/serializer/textFormattingSerializer.d.ts +17 -0
  82. package/dist/docx/serializer/textFormattingSerializer.js +144 -0
  83. package/dist/docx/tableParser.js +21 -11
  84. package/dist/docx/xmlParser.d.ts +10 -1
  85. package/dist/docx/xmlParser.js +23 -1
  86. package/dist/index.d.ts +4 -4
  87. package/dist/index.js +3 -3
  88. package/dist/internal/compare/inline-presentation.d.ts +20 -0
  89. package/dist/internal/compare/inline-presentation.js +222 -0
  90. package/dist/internal/paragraphFormattingSerialization.d.ts +25 -0
  91. package/dist/internal/paragraphFormattingSerialization.js +164 -0
  92. package/dist/internal/sectionReferenceResolution.d.ts +21 -0
  93. package/dist/internal/sectionReferenceResolution.js +72 -0
  94. package/dist/layout-bridge/convert/toFlowBlocks.js +1 -0
  95. package/dist/markdown/renderRuns.js +1 -0
  96. package/dist/markdown/renderTable.js +1 -0
  97. package/dist/prosemirror/attrs/index.js +20 -1
  98. package/dist/prosemirror/commands/comments.d.ts +11 -1
  99. package/dist/prosemirror/commands/comments.js +68 -6
  100. package/dist/prosemirror/commands/propertyChangeScope.d.ts +9 -3
  101. package/dist/prosemirror/commands/propertyChangeScope.js +4 -10
  102. package/dist/prosemirror/conversion/fromProseDoc.js +22 -22
  103. package/dist/prosemirror/conversion/toProseDoc.js +16 -57
  104. package/dist/prosemirror/extensions/core/ParagraphExtension.js +2 -0
  105. package/dist/prosemirror/extensions/marks/RunFormattingOverrideExtension.js +3 -0
  106. package/dist/prosemirror/imageCommit.js +7 -9
  107. package/dist/prosemirror/paragraphIndentation.d.ts +21 -0
  108. package/dist/prosemirror/paragraphIndentation.js +72 -0
  109. package/dist/prosemirror/plugins/documentNumbering.d.ts +3 -1
  110. package/dist/prosemirror/plugins/documentNumbering.js +7 -1
  111. package/dist/prosemirror/plugins/documentStyles.d.ts +3 -1
  112. package/dist/prosemirror/plugins/documentStyles.js +7 -1
  113. package/dist/prosemirror/rebaseParagraphRunFormatting.d.ts +3 -1
  114. package/dist/prosemirror/rebaseParagraphRunFormatting.js +2 -1
  115. package/dist/prosemirror/runFormattingInlineCarriers.d.ts +15 -2
  116. package/dist/prosemirror/runFormattingInlineCarriers.js +33 -3
  117. package/dist/prosemirror/runStyleFormatting.d.ts +15 -2
  118. package/dist/prosemirror/runStyleFormatting.js +76 -2
  119. package/dist/prosemirror/schema/marks.d.ts +3 -1
  120. package/dist/prosemirror/schema/marks.js +2 -0
  121. package/dist/prosemirror/schema/nodes.d.ts +3 -0
  122. package/dist/prosemirror/tableCellRevisionVisibility.d.ts +6 -0
  123. package/dist/prosemirror/tableCellRevisionVisibility.js +14 -0
  124. package/dist/prosemirror/trackedRunInlineAtoms.d.ts +1 -1
  125. package/dist/prosemirror/trackedRunInlineAtoms.js +1 -1
  126. package/dist/redline.js +15 -2
  127. package/dist/server.d.ts +3 -2
  128. package/dist/server.js +2 -2
  129. package/dist/version-comparison.d.ts +2 -0
  130. package/dist/version-comparison.js +30 -29
  131. package/package.json +2 -2
@@ -7,7 +7,8 @@ import { panic } from "better-result";
7
7
  * The block walk is split by structural container before any textual
8
8
  * alignment runs. Body blocks are compared with body blocks; tables are
9
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.
10
+ * structural candidate alignment in one caller-owned work session shares
11
+ * one hard budget; block exact-text anchors are gap-local and non-quadratic.
11
12
  */
12
13
  const MAX_CONTENT_ALIGNMENT_LCS_CELLS = 4e6;
13
14
  const MAX_CONTENT_STRUCTURE_TOKEN_LOOKUPS = 4e6;
@@ -28,6 +29,13 @@ const claimFolioContentAlignmentCells = (baseCount, revisedCount, workSession) =
28
29
  workSession.remainingLcsCells -= baseCount * revisedCount;
29
30
  return true;
30
31
  };
32
+ const NON_WHITESPACE = /\S/u;
33
+ const PARAGRAPH_MARK_KIND_FAMILY = {
34
+ heading: "paragraphMark",
35
+ listItem: "paragraphMark",
36
+ paragraph: "paragraphMark"
37
+ };
38
+ const blockKindsCanPair = (baseKind, revisedKind) => baseKind === revisedKind || Object.hasOwn(PARAGRAPH_MARK_KIND_FAMILY, baseKind) && Object.hasOwn(PARAGRAPH_MARK_KIND_FAMILY, revisedKind);
31
39
  const folioContentIdStability = (block) => block.idStability ?? "stable";
32
40
  /**
33
41
  * Longest strictly increasing subsequence by revised index.
@@ -91,20 +99,75 @@ const longestIncreasingFolioContentPairs = (pairs) => {
91
99
  }
92
100
  return ordered.toReversed();
93
101
  };
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
- });
102
+ const containerPathKeyOf = ({ containerPath }) => containerPath === void 0 || containerPath.length === 0 ? null : JSON.stringify(containerPath.map(({ kind, id }) => [kind, id]));
103
+ const blockPairingTableFactsOf = ({ table }) => {
104
+ if (!table) return { type: "body" };
105
+ return {
106
+ type: "table",
107
+ outerTableIndex: table.outerTableIndex,
108
+ tableIndex: table.tableIndex,
109
+ rowIndex: table.rowIndex,
110
+ cellIndex: table.cellIndex
111
+ };
112
+ };
113
+ const prepareAlignmentBlocks = ({ baseBlocks, revisedBlocks, idStability }) => {
114
+ const ROOT_CONTAINER_PATH_TOKEN = 0;
115
+ const containerPathTokens = /* @__PURE__ */ new Map();
116
+ let nextContainerPathToken = 1;
117
+ const containerPathTokenOf = (block) => {
118
+ const key = containerPathKeyOf(block);
119
+ if (key === null) return ROOT_CONTAINER_PATH_TOKEN;
120
+ const existing = containerPathTokens.get(key);
121
+ if (existing !== void 0) return existing;
122
+ const token = nextContainerPathToken++;
123
+ containerPathTokens.set(key, token);
124
+ return token;
125
+ };
126
+ const prepare = (blocks) => blocks.map((block, index) => ({
127
+ block,
128
+ index,
129
+ pairingFacts: {
130
+ containerPathToken: containerPathTokenOf(block),
131
+ id: block.id,
132
+ idStability: idStability(block),
133
+ kind: block.kind,
134
+ table: blockPairingTableFactsOf(block)
135
+ }
136
+ }));
137
+ return {
138
+ base: prepare(baseBlocks),
139
+ revised: prepare(revisedBlocks)
140
+ };
141
+ };
142
+ /** Every alignment pass shares this complete structural pairing policy. */
143
+ const blocksCanPair = ({ base, revised, stableIdMismatch, structuralScope }) => {
144
+ if (!blockKindsCanPair(base.kind, revised.kind) || base.containerPathToken !== revised.containerPathToken) return false;
145
+ if (base.table.type !== revised.table.type) return false;
146
+ if (structuralScope === "document" && base.table.type === "table" && revised.table.type === "table" && (base.table.outerTableIndex !== revised.table.outerTableIndex || base.table.tableIndex !== revised.table.tableIndex || base.table.rowIndex !== revised.table.rowIndex || base.table.cellIndex !== revised.table.cellIndex)) return false;
147
+ return !(stableIdMismatch === "separate" && base.idStability === "stable" && revised.idStability === "stable" && base.id !== revised.id);
148
+ };
149
+ const uniqueStableBlocks = (blocks) => {
150
+ const blocksById = /* @__PURE__ */ new Map();
151
+ for (const block of blocks) {
152
+ if (block.pairingFacts.idStability !== "stable") continue;
153
+ blocksById.set(block.pairingFacts.id, blocksById.has(block.pairingFacts.id) ? null : block);
154
+ }
155
+ return blocksById;
156
+ };
157
+ /** Stable identity is authoritative only when it is unique on both sides. */
158
+ const pairByStableId = ({ base, revised, canPair }) => {
159
+ const baseBlockById = uniqueStableBlocks(base);
160
+ const revisedBlockById = uniqueStableBlocks(revised);
99
161
  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
162
+ for (const baseBlock of base) {
163
+ if (baseBlockById.get(baseBlock.pairingFacts.id) !== baseBlock) continue;
164
+ const revisedBlock = revisedBlockById.get(baseBlock.pairingFacts.id);
165
+ if (!revisedBlock) continue;
166
+ if (canPair(baseBlock, revisedBlock)) candidates.push({
167
+ baseIndex: baseBlock.index,
168
+ revisedIndex: revisedBlock.index
106
169
  });
107
- });
170
+ }
108
171
  return longestIncreasingFolioContentPairs(candidates);
109
172
  };
110
173
  /**
@@ -116,180 +179,191 @@ const pairByStableId = (base, revised, idStability) => {
116
179
  * already bounded by stable/exact anchors, so it cannot turn a positional id
117
180
  * into a move or pull a block across an established correspondence.
118
181
  */
119
- const pairByResidualIdContinuity = ({ baseBlocks, revisedBlocks, baseFrom, baseTo, revisedFrom, revisedTo, idStability }) => {
120
- const uniqueIndexesById = ({ blocks, from, to }) => {
121
- const indexes = /* @__PURE__ */ new Map();
182
+ const pairByResidualIdContinuity = ({ baseBlocks, revisedBlocks, baseFrom, baseTo, revisedFrom, revisedTo, canPair }) => {
183
+ const uniqueBlocksById = ({ blocks, from, to }) => {
184
+ const blocksById = /* @__PURE__ */ new Map();
122
185
  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);
186
+ const block = blocks[index];
187
+ if (!block) continue;
188
+ blocksById.set(block.pairingFacts.id, blocksById.has(block.pairingFacts.id) ? null : block);
126
189
  }
127
- return indexes;
190
+ return blocksById;
128
191
  };
129
- const baseIndexes = uniqueIndexesById({
192
+ const baseBlocksById = uniqueBlocksById({
130
193
  blocks: baseBlocks,
131
194
  from: baseFrom,
132
195
  to: baseTo
133
196
  });
134
- const revisedIndexes = uniqueIndexesById({
197
+ const revisedBlocksById = uniqueBlocksById({
135
198
  blocks: revisedBlocks,
136
199
  from: revisedFrom,
137
200
  to: revisedTo
138
201
  });
139
202
  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];
203
+ for (const [id, baseBlock] of baseBlocksById) {
204
+ const revisedBlock = revisedBlocksById.get(id);
145
205
  if (!baseBlock || !revisedBlock) continue;
146
- if (idStability(baseBlock) === idStability(revisedBlock)) continue;
206
+ if (baseBlock.pairingFacts.idStability === revisedBlock.pairingFacts.idStability || !canPair(baseBlock, revisedBlock)) continue;
147
207
  candidates.push({
148
- baseIndex,
149
- revisedIndex
208
+ baseIndex: baseBlock.index,
209
+ revisedIndex: revisedBlock.index
150
210
  });
151
211
  }
152
212
  return longestIncreasingFolioContentPairs(candidates);
153
213
  };
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);
214
+ /** Exact anchors are gap-local; repeated or blank text is not identity evidence. */
215
+ const pairByUniqueExactText = ({ base, revised, baseFrom, baseTo, revisedFrom, revisedTo, canPair }) => {
216
+ const uniqueBlocks = (blocks, from, to) => {
217
+ const blocksByText = /* @__PURE__ */ new Map();
218
+ for (let index = from; index < to; index++) {
219
+ const block = blocks[index];
220
+ if (!block) continue;
221
+ const text = block.block.text;
222
+ if (!NON_WHITESPACE.test(text)) continue;
223
+ blocksByText.set(text, blocksByText.has(text) ? null : block);
224
+ }
225
+ return blocksByText;
175
226
  };
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);
227
+ const baseBlocksByText = uniqueBlocks(base, baseFrom, baseTo);
228
+ const revisedBlocksByText = uniqueBlocks(revised, revisedFrom, revisedTo);
229
+ const candidates = [];
230
+ for (const [text, baseBlock] of baseBlocksByText) {
231
+ const revisedBlock = revisedBlocksByText.get(text);
232
+ if (!baseBlock || !revisedBlock) continue;
233
+ if (canPair(baseBlock, revisedBlock)) candidates.push({
234
+ baseIndex: baseBlock.index,
235
+ revisedIndex: revisedBlock.index
236
+ });
182
237
  }
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;
238
+ const orderedCandidates = candidates.toSorted((left, right) => left.baseIndex - right.baseIndex || left.revisedIndex - right.revisedIndex);
239
+ if (orderedCandidates.length === 0) return [];
240
+ const tailRevisedIndexes = [];
241
+ const tailCandidateIndexes = [];
242
+ const predecessors = new Int32Array(orderedCandidates.length).fill(-1);
243
+ orderedCandidates.forEach((candidate, candidateIndex) => {
244
+ let lower = 0;
245
+ let upper = tailRevisedIndexes.length;
246
+ while (lower < upper) {
247
+ const middle = lower + Math.floor((upper - lower) / 2);
248
+ if ((tailRevisedIndexes[middle] ?? Number.POSITIVE_INFINITY) < candidate.revisedIndex) lower = middle + 1;
249
+ else upper = middle;
198
250
  }
199
- if ((lengths[(baseIndex + 1) * stride + revisedIndex] ?? 0) >= (lengths[baseIndex * stride + revisedIndex + 1] ?? 0)) baseIndex += 1;
200
- else revisedIndex += 1;
251
+ predecessors[candidateIndex] = lower === 0 ? -1 : tailCandidateIndexes[lower - 1] ?? -1;
252
+ tailRevisedIndexes[lower] = candidate.revisedIndex;
253
+ tailCandidateIndexes[lower] = candidateIndex;
254
+ });
255
+ const pairs = [];
256
+ for (let candidateIndex = tailCandidateIndexes.at(-1) ?? -1; candidateIndex !== -1; candidateIndex = predecessors[candidateIndex] ?? -1) {
257
+ const candidate = orderedCandidates[candidateIndex];
258
+ if (candidate) pairs.push(candidate);
259
+ }
260
+ return pairs.toReversed();
261
+ };
262
+ const pairsInAnchorGaps = ({ baseLength, revisedLength, anchors, pairGap }) => {
263
+ const pairs = [];
264
+ let baseFrom = 0;
265
+ let revisedFrom = 0;
266
+ for (const anchor of [...anchors, {
267
+ baseIndex: baseLength,
268
+ revisedIndex: revisedLength
269
+ }]) {
270
+ pairs.push(...pairGap(baseFrom, anchor.baseIndex, revisedFrom, anchor.revisedIndex));
271
+ baseFrom = anchor.baseIndex + 1;
272
+ revisedFrom = anchor.revisedIndex + 1;
201
273
  }
202
274
  return pairs;
203
275
  };
204
- const alignFolioContentBlocks = (baseBlocks, revisedBlocks, options = {}) => {
205
- const workSession = options.workSession ?? createFolioContentAlignmentWorkSession();
276
+ const alignFolioContentBlocksInScope = (baseBlocks, revisedBlocks, options) => {
206
277
  const stableIdMismatch = options.stableIdMismatch ?? "separate";
207
278
  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));
279
+ const prepared = prepareAlignmentBlocks({
280
+ baseBlocks,
281
+ revisedBlocks,
282
+ idStability
283
+ });
284
+ const canPair = (baseBlock, revisedBlock) => blocksCanPair({
285
+ base: baseBlock.pairingFacts,
286
+ revised: revisedBlock.pairingFacts,
287
+ stableIdMismatch,
288
+ structuralScope: options.structuralScope
289
+ });
290
+ const stableIdAnchors = pairByStableId({
291
+ base: prepared.base,
292
+ revised: prepared.revised,
293
+ canPair
294
+ });
295
+ const exactTextAnchors = pairsInAnchorGaps({
296
+ baseLength: prepared.base.length,
297
+ revisedLength: prepared.revised.length,
298
+ anchors: stableIdAnchors,
299
+ pairGap: (baseFrom, baseTo, revisedFrom, revisedTo) => pairByUniqueExactText({
300
+ base: prepared.base,
301
+ revised: prepared.revised,
302
+ baseFrom,
303
+ baseTo,
304
+ revisedFrom,
305
+ revisedTo,
306
+ canPair
307
+ })
308
+ });
309
+ const exactAndStableAnchors = [...stableIdAnchors, ...exactTextAnchors].toSorted((left, right) => left.baseIndex - right.baseIndex || left.revisedIndex - right.revisedIndex);
310
+ const continuityAnchors = pairsInAnchorGaps({
311
+ baseLength: prepared.base.length,
312
+ revisedLength: prepared.revised.length,
313
+ anchors: exactAndStableAnchors,
314
+ pairGap: (baseFrom, baseTo, revisedFrom, revisedTo) => pairByResidualIdContinuity({
315
+ baseBlocks: prepared.base,
316
+ revisedBlocks: prepared.revised,
317
+ baseFrom,
318
+ baseTo,
319
+ revisedFrom,
320
+ revisedTo,
321
+ canPair
322
+ })
323
+ });
324
+ const anchors = [...exactAndStableAnchors, ...continuityAnchors].toSorted((left, right) => left.baseIndex - right.baseIndex || left.revisedIndex - right.revisedIndex);
221
325
  const events = [];
222
326
  const emitPositionalGap = (baseFrom, baseTo, revisedFrom, revisedTo) => {
223
327
  const pairedCount = Math.min(baseTo - baseFrom, revisedTo - revisedFrom);
224
328
  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") {
329
+ const baseBlock = prepared.base[baseFrom + offset];
330
+ const revisedBlock = prepared.revised[revisedFrom + offset];
331
+ if (baseBlock && revisedBlock) if (!canPair(baseBlock, revisedBlock)) {
228
332
  events.push({
229
333
  type: "baseOnly",
230
- block: baseBlock
334
+ block: baseBlock.block
231
335
  });
232
336
  events.push({
233
337
  type: "revisedOnly",
234
- block: revisedBlock
338
+ block: revisedBlock.block
235
339
  });
236
340
  } else events.push({
237
341
  type: "pair",
238
- baseBlock,
239
- revisedBlock
342
+ baseBlock: baseBlock.block,
343
+ revisedBlock: revisedBlock.block
240
344
  });
241
345
  }
242
346
  for (let index = baseFrom + pairedCount; index < baseTo; index++) {
243
- const block = baseBlocks[index];
347
+ const block = prepared.base[index]?.block;
244
348
  if (block) events.push({
245
349
  type: "baseOnly",
246
350
  block
247
351
  });
248
352
  }
249
353
  for (let index = revisedFrom + pairedCount; index < revisedTo; index++) {
250
- const block = revisedBlocks[index];
354
+ const block = prepared.revised[index]?.block;
251
355
  if (block) events.push({
252
356
  type: "revisedOnly",
253
357
  block
254
358
  });
255
359
  }
256
360
  };
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
361
  let baseCursor = 0;
288
362
  let revisedCursor = 0;
289
363
  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];
364
+ emitPositionalGap(baseCursor, anchor.baseIndex, revisedCursor, anchor.revisedIndex);
365
+ const baseBlock = prepared.base[anchor.baseIndex]?.block;
366
+ const revisedBlock = prepared.revised[anchor.revisedIndex]?.block;
293
367
  if (baseBlock && revisedBlock) events.push({
294
368
  type: "pair",
295
369
  baseBlock,
@@ -298,9 +372,13 @@ const alignFolioContentBlocks = (baseBlocks, revisedBlocks, options = {}) => {
298
372
  baseCursor = anchor.baseIndex + 1;
299
373
  revisedCursor = anchor.revisedIndex + 1;
300
374
  }
301
- emitGap(baseCursor, baseBlocks.length, revisedCursor, revisedBlocks.length);
375
+ emitPositionalGap(baseCursor, prepared.base.length, revisedCursor, prepared.revised.length);
302
376
  return events;
303
377
  };
378
+ const alignFolioContentBlocks = (baseBlocks, revisedBlocks, options = {}) => alignFolioContentBlocksInScope(baseBlocks, revisedBlocks, {
379
+ ...options,
380
+ structuralScope: "document"
381
+ });
304
382
  const BODY_MOVE_BUCKET = 0;
305
383
  const scopedAlignmentSteps = (events, context, bucketForBlock) => {
306
384
  const steps = [];
@@ -321,7 +399,6 @@ const scopedAlignmentSteps = (events, context, bucketForBlock) => {
321
399
  }
322
400
  return steps;
323
401
  };
324
- const containerPathKeyOf = (block) => block.containerPath === void 0 || block.containerPath.length === 0 ? null : JSON.stringify(block.containerPath.map(({ kind, id }) => [kind, id]));
325
402
  const contentBlocksShareContainerPath = (left, right) => containerPathKeyOf(left) === containerPathKeyOf(right);
326
403
  const splitSegments = (blocks) => {
327
404
  const segments = [];
@@ -633,7 +710,7 @@ const ALIGNMENT_DIRECTION = {
633
710
  baseOnly: 2,
634
711
  revisedOnly: 3
635
712
  };
636
- const alignProfiledContentSequence = ({ base, revised, workSession, canPair = () => true, pairSoleStructuralSlot = false, primaryEvidence = "stable", similarityFactor = () => 1 }) => {
713
+ const alignProfiledContentSequence = ({ base, revised, workSession, canPair = () => true, pairSoleStructuralSlot = false, soleShiftedPairResiduePolicy = "conservative", primaryEvidence = "stable", similarityFactor = () => 1 }) => {
637
714
  if (base.length === 0 || revised.length === 0) return unpairedContentSequence(base.map(({ item }) => item), revised.map(({ item }) => item));
638
715
  if ((base.length !== 1 || revised.length !== 1) && !claimFolioContentAlignmentCells(base.length, revised.length, workSession)) return unpairedContentSequence(base.map(({ item }) => item), revised.map(({ item }) => item));
639
716
  const stablePairs = stableContentSequencePairs(base, revised);
@@ -805,7 +882,7 @@ const alignProfiledContentSequence = ({ base, revised, workSession, canPair = ()
805
882
  });
806
883
  hasRevisedOnly = true;
807
884
  }
808
- if (pairCount === 1 && solePairIsShifted && hasBaseOnly && hasRevisedOnly) return aligned.flatMap((entry) => entry.type === "pair" ? [{
885
+ if ((soleShiftedPairResiduePolicy === "conservative" || base.length === revised.length) && pairCount === 1 && solePairIsShifted && hasBaseOnly && hasRevisedOnly) return aligned.flatMap((entry) => entry.type === "pair" ? [{
809
886
  type: "baseOnly",
810
887
  item: entry.base
811
888
  }, {
@@ -839,10 +916,11 @@ const alignRowCells = ({ baseRow, revisedRow, workSession, moveScopeContext, sta
839
916
  for (const cellIndex of cellIndexes) {
840
917
  const baseBlocks = baseCells.get(cellIndex) ?? [];
841
918
  const revisedBlocks = revisedCells.get(cellIndex) ?? [];
842
- const aligned = alignFolioContentBlocks(baseBlocks, revisedBlocks, {
919
+ const aligned = alignFolioContentBlocksInScope(baseBlocks, revisedBlocks, {
843
920
  workSession,
844
921
  idStability,
845
- stableIdMismatch
922
+ stableIdMismatch,
923
+ structuralScope: "pairedTableCell"
846
924
  }).flatMap((event) => {
847
925
  if (event.type === "pair" && !contentBlocksShareContainerPath(event.baseBlock, event.revisedBlock)) return [{
848
926
  type: "baseOnly",
@@ -876,6 +954,7 @@ const pairTableRows = ({ baseRows, revisedRows, workSession, idStability }) => {
876
954
  revised: revisedRows.map(profile),
877
955
  workSession,
878
956
  pairSoleStructuralSlot: true,
957
+ soleShiftedPairResiduePolicy: "row-count-evidence",
879
958
  primaryEvidence: "exact",
880
959
  similarityFactor: (base, revised) => base.profile.physicalCellCount === revised.profile.physicalCellCount ? 1 : .5
881
960
  }).map((alignment) => {
@@ -1,6 +1,8 @@
1
1
  //#region src/compare/content-types.d.ts
2
2
  /** How callers expect a block identifier to behave across document revisions. */
3
3
  type FolioContentIdStability = "stable" | "positional";
4
+ /** Text-bearing block kinds that share one editable paragraph-mark structure. */
5
+ type FolioContentParagraphKind = "heading" | "listItem" | "paragraph";
4
6
  /** One enclosing structural container, ordered outermost to innermost in a block path. */
5
7
  type FolioContentContainerPathEntry = {
6
8
  kind: string;
@@ -17,7 +19,24 @@ type FolioContentInlineFormatting = Partial<Record<FolioContentInlineBooleanProp
17
19
  * An inline-formatting mutation: `false` is an explicit off value, while
18
20
  * `null` removes the direct property so its inherited value becomes effective.
19
21
  */
20
- type FolioContentInlineFormattingPatch = Omit<FolioContentInlineFormatting, FolioContentInlineBooleanProperty> & Partial<Record<FolioContentInlineBooleanProperty, boolean | null>>;
22
+ type FolioContentInlineFormattingPatch = { [Property in keyof FolioContentInlineFormatting]?: FolioContentInlineFormatting[Property] | null; };
23
+ /** Presentation differences for one UTF-16 range in a text-aligned block pair. */
24
+ type FolioContentFormatRange = {
25
+ startOffset: number;
26
+ endOffset: number;
27
+ formatting: FolioContentInlineFormattingPatch;
28
+ };
29
+ /** A trusted inline projection or an explicit reason no ranges can be used. */
30
+ type FolioContentInlineComparisonResult = {
31
+ status: "compared";
32
+ segments: readonly FolioContentFormatRange[];
33
+ } | {
34
+ status: "unalignable";
35
+ side: "base" | "revised" | "both";
36
+ } | {
37
+ status: "budget-exceeded";
38
+ maximum: number;
39
+ };
21
40
  type FolioContentRun = {
22
41
  text: string;
23
42
  bold?: boolean;
@@ -59,6 +78,18 @@ type FolioContentTableLocation = {
59
78
  type FolioContentParagraphAlignment = "left" | "center" | "right" | "both" | "distribute" | "mediumKashida" | "highKashida" | "lowKashida" | "thaiDistribute";
60
79
  /** Line-height interpretation understood by the neutral comparison model. */
61
80
  type FolioContentLineSpacingRule = "auto" | "exact" | "atLeast";
81
+ /** The complete modeled attribute set of direct paragraph indentation. */
82
+ type FolioContentParagraphIndentation = {
83
+ indentLeft?: number;
84
+ indentRight?: number;
85
+ indentFirstLine?: number;
86
+ hangingIndent?: boolean;
87
+ };
88
+ /** A concrete numbering instance and its zero-based level. */
89
+ type FolioContentListReference = {
90
+ numId: number;
91
+ level: number;
92
+ };
62
93
  /**
63
94
  * The complete modeled attribute set of direct paragraph spacing. Optional
64
95
  * fields distinguish an absent attribute from an explicit zero or false value.
@@ -85,8 +116,11 @@ type FolioContentBlock<Kind extends string = string> = {
85
116
  directAlignment?: FolioContentParagraphAlignment;
86
117
  /** Direct paragraph spacing; absent when every spacing value is inherited. */
87
118
  directSpacing?: FolioContentParagraphSpacing;
119
+ /** Direct paragraph indentation; absent when every indentation value is inherited. */
120
+ directIndentation?: FolioContentParagraphIndentation;
88
121
  /** Zero-based list indent level when the block carries numbering. */
89
122
  listLevel?: number;
123
+ listReference?: FolioContentListReference;
90
124
  previewRuns?: readonly FolioContentRun[];
91
125
  table?: FolioContentTableLocation;
92
126
  /** Structural ancestry, ordered from the outermost to the innermost container. */
@@ -97,4 +131,4 @@ type FolioContentSnapshot<Block extends FolioContentBlock = FolioContentBlock> =
97
131
  blocks: readonly Block[];
98
132
  };
99
133
  //#endregion
100
- export { FolioContentBlock, FolioContentContainerPathEntry, FolioContentIdStability, FolioContentInlineBooleanProperty, FolioContentInlineFormatting, FolioContentInlineFormattingPatch, FolioContentLineSpacingRule, FolioContentParagraphAlignment, FolioContentParagraphSpacing, FolioContentRun, FolioContentSnapshot, FolioContentTableLocation };
134
+ export { FolioContentBlock, FolioContentContainerPathEntry, FolioContentFormatRange, FolioContentIdStability, FolioContentInlineBooleanProperty, FolioContentInlineComparisonResult, FolioContentInlineFormatting, FolioContentInlineFormattingPatch, FolioContentLineSpacingRule, FolioContentListReference, FolioContentParagraphAlignment, FolioContentParagraphIndentation, FolioContentParagraphKind, FolioContentParagraphSpacing, FolioContentRun, FolioContentSnapshot, FolioContentTableLocation };
@@ -1,4 +1,4 @@
1
- import { FolioContentBlock, FolioContentIdStability, FolioContentInlineFormattingPatch, FolioContentParagraphSpacing, FolioContentSnapshot } from "./content-types.js";
1
+ import { FolioContentBlock, FolioContentFormatRange, FolioContentIdStability, FolioContentListReference, FolioContentParagraphIndentation, FolioContentParagraphSpacing, FolioContentSnapshot } from "./content-types.js";
2
2
  import { WordDiffGranularity, createWordDiffSession } from "../ai-edits/word-diff.js";
3
3
  import { FolioContentAlignmentStep, FolioContentAlignmentWorkSession } from "./content-alignment.js";
4
4
  import { Result } from "better-result";
@@ -34,8 +34,15 @@ declare class FolioContentComparisonLimitError extends FolioContentComparisonLim
34
34
  blockIndex?: number;
35
35
  field?: string;
36
36
  }> {}
37
+ declare const FolioContentInlinePresentationProjectionError_base: import("better-result").TaggedErrorClass<"FolioContentInlinePresentationProjectionError">;
38
+ declare class FolioContentInlinePresentationProjectionError extends FolioContentInlinePresentationProjectionError_base<{
39
+ message: string;
40
+ side: "base" | "revised" | "both";
41
+ baseBlockId: string;
42
+ revisedBlockId: string;
43
+ }> {}
37
44
  /** Errors returned by {@link compareContent}. */
38
- type FolioContentComparisonError = InvalidFolioContentComparisonError | FolioContentComparisonLimitError;
45
+ type FolioContentComparisonError = InvalidFolioContentComparisonError | FolioContentComparisonLimitError | FolioContentInlinePresentationProjectionError;
39
46
  /** One Folio word-diff segment with JavaScript-slice-compatible offsets. */
40
47
  type FolioContentTextSegment = {
41
48
  type: "equal" | "del" | "ins";
@@ -49,14 +56,10 @@ type FolioContentTextSegment = {
49
56
  type FolioContentParagraphFormattingPatch = {
50
57
  styleId?: string | null;
51
58
  listLevel?: number | null;
59
+ listReference?: FolioContentListReference | null;
52
60
  alignment?: FolioContentBlock["directAlignment"] | null;
53
61
  spacing?: FolioContentParagraphSpacing | null;
54
- };
55
- /** Presentation differences for one text-aligned block pair. */
56
- type FolioContentFormatRange = {
57
- startOffset: number;
58
- endOffset: number;
59
- formatting: FolioContentInlineFormattingPatch;
62
+ indentation?: FolioContentParagraphIndentation | null;
60
63
  };
61
64
  /** Presentation differences for one text-aligned block pair. */
62
65
  type FolioContentFormattingChange = {
@@ -202,8 +205,8 @@ type CompareAlignedContentOptions<Block extends FolioContentBlock> = {
202
205
  maxChanges: number;
203
206
  idStability?: (block: Block) => FolioContentIdStability;
204
207
  };
205
- declare const compareAlignedFolioContent: <Block extends FolioContentBlock>({ baseBlocks, revisedBlocks, steps, workSession, maxChanges, idStability }: CompareAlignedContentOptions<Block>) => Result<FolioContentComparison<Block>, FolioContentComparisonLimitError>;
208
+ declare const compareAlignedFolioContent: <Block extends FolioContentBlock>({ baseBlocks, revisedBlocks, steps, workSession, maxChanges, idStability }: CompareAlignedContentOptions<Block>) => Result<FolioContentComparison<Block>, FolioContentComparisonLimitError | FolioContentInlinePresentationProjectionError>;
206
209
  /** Compare two representation-neutral ordered content snapshots. */
207
210
  declare const compareContent: <Block extends FolioContentBlock = FolioContentBlock>(options: CompareContentOptions<Block>) => Result<FolioContentComparison<Block>, FolioContentComparisonError>;
208
211
  //#endregion
209
- export { CompareContentOptions, FOLIO_CONTENT_COMPARISON_LIMITS, FolioContentBlockProperty, FolioContentComparison, FolioContentComparisonError, FolioContentComparisonEvent, FolioContentComparisonLimit, FolioContentComparisonLimitError, FolioContentComparisonWorkSession, FolioContentFormatRange, FolioContentFormattingChange, FolioContentParagraphFormattingPatch, FolioContentStructuralChange, FolioContentTextSegment, InvalidFolioContentComparisonError, changedFolioContentParagraphFormatting, compareAlignedFolioContent, compareContent, createContentComparisonWorkSession, detectFolioContentMoves, detectFolioContentParagraphMarkPlans };
212
+ export { CompareContentOptions, FOLIO_CONTENT_COMPARISON_LIMITS, FolioContentBlockProperty, FolioContentComparison, FolioContentComparisonError, FolioContentComparisonEvent, FolioContentComparisonLimit, FolioContentComparisonLimitError, FolioContentComparisonWorkSession, type FolioContentFormatRange, FolioContentFormattingChange, FolioContentInlinePresentationProjectionError, FolioContentParagraphFormattingPatch, FolioContentStructuralChange, FolioContentTextSegment, InvalidFolioContentComparisonError, changedFolioContentParagraphFormatting, compareAlignedFolioContent, compareContent, createContentComparisonWorkSession, detectFolioContentMoves, detectFolioContentParagraphMarkPlans };