@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
@@ -0,0 +1,334 @@
1
+ import { sourceDocumentOf, styleResolverOf } from "../ai-edits/snapshot.js";
2
+ import { expectHyperlinkMarkAttrs, expectRunPropertyChangeMarkAttrs, expectTableCellAttrs } from "../prosemirror/attrs/index.js";
3
+ import { getDocumentStyleResolver } from "../prosemirror/plugins/documentStyles.js";
4
+ import { applyMarksToRunFormattingRepresentation, expandRunFormattingCarrier, runFormattingCarrierReviewText } from "../prosemirror/runFormattingInlineCarriers.js";
5
+ import { readAuthoredRunFormatting, reconcileRunFormattingMarks } from "../prosemirror/runFormattingReconciliation.js";
6
+ import { paragraphRunStyleContextAt } from "../prosemirror/runStyleFormatting.js";
7
+ import { isTableCellRetainedInReviewView } from "../prosemirror/tableCellRevisionVisibility.js";
8
+ import { canonicalJson } from "../utils/canonicalJson.js";
9
+ //#region src/compare/inline-provenance.ts
10
+ const isDeleted = (carrier) => carrier.representations.some(({ node }) => node.marks.some(({ type }) => type.name === "deletion"));
11
+ const hyperlinkOf = (node) => node.marks.find(({ type }) => type.name === "hyperlink");
12
+ const hyperlinkIdentity = (node) => {
13
+ const mark = hyperlinkOf(node);
14
+ if (!mark) return "null";
15
+ const { href, tooltip } = expectHyperlinkMarkAttrs(mark);
16
+ return canonicalJson({
17
+ href,
18
+ tooltip
19
+ });
20
+ };
21
+ const isInserted = (node) => node.marks.some(({ type }) => type.name === "insertion");
22
+ const carrierRepresentations = (carrier) => carrier.representations.map((representation) => ({
23
+ ...representation,
24
+ from: representation.position,
25
+ to: representation.position + representation.node.nodeSize
26
+ }));
27
+ const targetBlockIdLookup = (anchors) => {
28
+ const ordered = Object.values(anchors).toSorted((left, right) => left.from - right.from || left.to - right.to);
29
+ const active = [];
30
+ let nextAnchor = 0;
31
+ return ({ position, nodeSize }) => {
32
+ while (nextAnchor < ordered.length) {
33
+ const anchor = ordered.at(nextAnchor);
34
+ if (!anchor || anchor.from > position) break;
35
+ active.push(anchor);
36
+ nextAnchor++;
37
+ }
38
+ const end = position + nodeSize;
39
+ let owner;
40
+ for (let index = active.length - 1; index >= 0; index--) {
41
+ const anchor = active[index];
42
+ if (!anchor) continue;
43
+ if (anchor.to < end) {
44
+ active.splice(index, 1);
45
+ continue;
46
+ }
47
+ if (anchor.from <= position && end <= anchor.to && (owner === void 0 || anchor.to - anchor.from < owner.to - owner.from)) owner = anchor;
48
+ }
49
+ return owner?.id;
50
+ };
51
+ };
52
+ const collectCarriers = ({ doc, targetSnapshot }) => {
53
+ const carriers = [];
54
+ const targetBlockIdAt = targetSnapshot ? targetBlockIdLookup(targetSnapshot.anchors) : void 0;
55
+ let unanchoredTargetCarrier = false;
56
+ doc.descendants((node, position) => {
57
+ if ((node.type.name === "tableCell" || node.type.name === "tableHeader") && !isTableCellRetainedInReviewView(expectTableCellAttrs(node).cellMarker?.kind, "final")) return false;
58
+ if (!node.isInline) return true;
59
+ const carrier = expandRunFormattingCarrier(node, position);
60
+ if (!carrier) return !node.isAtom;
61
+ if (isDeleted(carrier)) return false;
62
+ const targetBlockId = targetBlockIdAt?.({
63
+ position,
64
+ nodeSize: node.nodeSize
65
+ });
66
+ if (targetSnapshot && targetBlockId === void 0) {
67
+ unanchoredTargetCarrier = true;
68
+ return false;
69
+ }
70
+ carriers.push({
71
+ carrier,
72
+ text: runFormattingCarrierReviewText(carrier),
73
+ targetBlockId
74
+ });
75
+ return false;
76
+ });
77
+ return unanchoredTargetCarrier ? null : carriers;
78
+ };
79
+ const sameCarrierShape = (left, right) => {
80
+ if (left.carrier.disposition !== right.carrier.disposition || left.text !== right.text) return false;
81
+ const leftRepresentations = left.carrier.representations;
82
+ const rightRepresentations = right.carrier.representations;
83
+ return leftRepresentations.length === rightRepresentations.length && leftRepresentations.every((representation, index) => {
84
+ const target = rightRepresentations.at(index);
85
+ return target !== void 0 && representation.role === target.role && representation.node.type === target.node.type;
86
+ });
87
+ };
88
+ /**
89
+ * Match whole formatting carriers while permitting only text runs to be cut at
90
+ * UTF-16 boundaries. Non-text carriers are serialization atoms and therefore
91
+ * must line up exactly on both sides.
92
+ */
93
+ const matchCarrierStreams = ({ live, target }) => {
94
+ const matched = [];
95
+ let liveIndex = 0;
96
+ let targetIndex = 0;
97
+ let liveOffset = 0;
98
+ let targetOffset = 0;
99
+ while (liveIndex < live.length || targetIndex < target.length) {
100
+ const liveCarrier = live.at(liveIndex);
101
+ const targetCarrier = target.at(targetIndex);
102
+ if (!liveCarrier || !targetCarrier || targetCarrier.targetBlockId === void 0) return null;
103
+ const liveText = liveCarrier.text.slice(liveOffset);
104
+ const targetText = targetCarrier.text.slice(targetOffset);
105
+ const liveIsText = liveCarrier.carrier.disposition === "text-run";
106
+ const targetIsText = targetCarrier.carrier.disposition === "text-run";
107
+ if (!liveIsText || !targetIsText) {
108
+ if (liveOffset !== 0 || targetOffset !== 0 || !sameCarrierShape(liveCarrier, targetCarrier)) return null;
109
+ const liveRepresentations = carrierRepresentations(liveCarrier.carrier);
110
+ const targetRepresentations = carrierRepresentations(targetCarrier.carrier);
111
+ for (const [index, liveRepresentation] of liveRepresentations.entries()) {
112
+ const targetRepresentation = targetRepresentations.at(index);
113
+ if (!targetRepresentation) return null;
114
+ matched.push({
115
+ live: liveRepresentation,
116
+ target: targetRepresentation,
117
+ targetBlockId: targetCarrier.targetBlockId
118
+ });
119
+ }
120
+ liveIndex++;
121
+ targetIndex++;
122
+ continue;
123
+ }
124
+ const sharedLength = Math.min(liveText.length, targetText.length);
125
+ if (sharedLength === 0 || liveText.slice(0, sharedLength) !== targetText.slice(0, sharedLength)) return null;
126
+ const liveRepresentation = carrierRepresentations(liveCarrier.carrier).at(0);
127
+ const targetRepresentation = carrierRepresentations(targetCarrier.carrier).at(0);
128
+ if (!liveRepresentation || !targetRepresentation) return null;
129
+ matched.push({
130
+ live: {
131
+ ...liveRepresentation,
132
+ from: liveRepresentation.from + liveOffset,
133
+ to: liveRepresentation.from + liveOffset + sharedLength
134
+ },
135
+ target: {
136
+ ...targetRepresentation,
137
+ from: targetRepresentation.from + targetOffset,
138
+ to: targetRepresentation.from + targetOffset + sharedLength
139
+ },
140
+ targetBlockId: targetCarrier.targetBlockId
141
+ });
142
+ liveOffset += sharedLength;
143
+ targetOffset += sharedLength;
144
+ if (liveOffset === liveCarrier.text.length) {
145
+ liveIndex++;
146
+ liveOffset = 0;
147
+ }
148
+ if (targetOffset === targetCarrier.text.length) {
149
+ targetIndex++;
150
+ targetOffset = 0;
151
+ }
152
+ }
153
+ return matched;
154
+ };
155
+ const authoredFormattingAt = ({ doc, representation, styleResolver }) => readAuthoredRunFormatting({
156
+ context: paragraphRunStyleContextAt({
157
+ doc,
158
+ pos: representation.from,
159
+ styleResolver
160
+ }),
161
+ marks: representation.node.marks,
162
+ styleResolver
163
+ });
164
+ const changeWithCurrentFormatting = ({ change, formatting }) => ({
165
+ ...change,
166
+ currentFormatting: formatting
167
+ });
168
+ /**
169
+ * Reconcile direct inline run formatting after the comparison plan has aligned
170
+ * text. It deliberately performs all stream, revision-slot, and range-budget
171
+ * checks before creating a transaction step.
172
+ */
173
+ const matchInlineProvenance = ({ state, targetSnapshot, revisionStamp, originalRevisionIdSeed, author, maxRanges }) => {
174
+ const targetStyleResolver = styleResolverOf(targetSnapshot);
175
+ if (!Number.isSafeInteger(maxRanges) || maxRanges < 0) return { status: "budget-exceeded" };
176
+ const targetDocument = sourceDocumentOf(targetSnapshot);
177
+ const liveCarriers = collectCarriers({ doc: state.doc });
178
+ const targetCarriers = collectCarriers({
179
+ doc: targetDocument,
180
+ targetSnapshot
181
+ });
182
+ if (!liveCarriers || !targetCarriers) return { status: "unalignable" };
183
+ const matched = matchCarrierStreams({
184
+ live: liveCarriers,
185
+ target: targetCarriers
186
+ });
187
+ if (!matched) return { status: "unalignable" };
188
+ const propertyChangeType = state.schema.marks["runPropertyChange"];
189
+ const liveStyleResolver = getDocumentStyleResolver(state);
190
+ const planned = [];
191
+ for (const segment of matched) {
192
+ const liveFormatting = authoredFormattingAt({
193
+ doc: state.doc,
194
+ representation: segment.live,
195
+ styleResolver: liveStyleResolver
196
+ });
197
+ const targetFormatting = authoredFormattingAt({
198
+ doc: targetDocument,
199
+ representation: segment.target,
200
+ styleResolver: targetStyleResolver
201
+ });
202
+ const hyperlinkChange = hyperlinkIdentity(segment.live.node) === hyperlinkIdentity(segment.target.node) ? "unchanged" : "replace";
203
+ if (hyperlinkChange === "replace" && !segment.live.node.isText) return { status: "unalignable" };
204
+ if (hyperlinkChange === "unchanged" && canonicalJson(liveFormatting) === canonicalJson(targetFormatting)) continue;
205
+ const existingPropertyChange = propertyChangeType ? segment.live.node.marks.find((mark) => mark.type === propertyChangeType) : void 0;
206
+ const propertyChange = existingPropertyChange && expectRunPropertyChangeMarkAttrs(existingPropertyChange).changes.length > 0 ? existingPropertyChange : void 0;
207
+ if (propertyChange && expectRunPropertyChangeMarkAttrs(propertyChange).changes.some(({ info }) => info.id < originalRevisionIdSeed)) return { status: "unalignable" };
208
+ if (!isInserted(segment.live.node) && !propertyChangeType) return { status: "unalignable" };
209
+ planned.push({
210
+ ...segment,
211
+ formatting: targetFormatting,
212
+ previousFormatting: liveFormatting,
213
+ inserted: isInserted(segment.live.node),
214
+ propertyChange,
215
+ hyperlinkChange
216
+ });
217
+ }
218
+ if (planned.length > maxRanges) return { status: "budget-exceeded" };
219
+ const transaction = state.tr;
220
+ let nextRevisionId = revisionStamp.idSeed;
221
+ const changedTargetBlockIds = /* @__PURE__ */ new Set();
222
+ for (const change of planned.toReversed()) {
223
+ const context = paragraphRunStyleContextAt({
224
+ doc: state.doc,
225
+ pos: change.live.from,
226
+ styleResolver: liveStyleResolver
227
+ });
228
+ const formattingMarks = reconcileRunFormattingMarks({
229
+ authoredFormatting: change.formatting,
230
+ context,
231
+ node: change.live.node,
232
+ styleResolver: liveStyleResolver
233
+ });
234
+ let marks = formattingMarks;
235
+ if (change.hyperlinkChange === "replace") {
236
+ const hyperlinkType = state.schema.marks["hyperlink"];
237
+ const insertionType = state.schema.marks["insertion"];
238
+ const deletionType = state.schema.marks["deletion"];
239
+ if (!hyperlinkType || !insertionType || !deletionType) return { status: "unalignable" };
240
+ marks = hyperlinkType.removeFromSet(marks);
241
+ const targetLink = hyperlinkOf(change.target.node);
242
+ if (targetLink) {
243
+ const { href, tooltip } = expectHyperlinkMarkAttrs(targetLink);
244
+ marks = hyperlinkType.create({
245
+ href,
246
+ tooltip
247
+ }).addToSet(marks);
248
+ }
249
+ if (!change.inserted) {
250
+ if (propertyChangeType) marks = propertyChangeType.removeFromSet(marks);
251
+ marks = insertionType.create({
252
+ revisionId: nextRevisionId++,
253
+ author,
254
+ date: revisionStamp.date
255
+ }).addToSet(marks);
256
+ const text = state.doc.textBetween(change.live.from, change.live.to);
257
+ transaction.addMark(change.live.from, change.live.to, deletionType.create({
258
+ revisionId: nextRevisionId++,
259
+ author,
260
+ date: revisionStamp.date
261
+ }));
262
+ transaction.insert(change.live.to, state.schema.text(text, marks));
263
+ changedTargetBlockIds.add(change.targetBlockId);
264
+ continue;
265
+ }
266
+ }
267
+ if (!change.inserted && propertyChangeType) if (change.propertyChange) {
268
+ const attrs = expectRunPropertyChangeMarkAttrs(change.propertyChange);
269
+ marks = propertyChangeType.create({
270
+ ...change.propertyChange.attrs,
271
+ changes: attrs.changes.map((entry) => changeWithCurrentFormatting({
272
+ change: entry,
273
+ formatting: change.formatting
274
+ }))
275
+ }).addToSet(formattingMarks);
276
+ } else {
277
+ const propertyChange = {
278
+ type: "runPropertyChange",
279
+ info: {
280
+ id: nextRevisionId++,
281
+ author,
282
+ date: revisionStamp.date
283
+ },
284
+ ...Object.keys(change.previousFormatting).length > 0 ? { previousFormatting: change.previousFormatting } : {}
285
+ };
286
+ marks = propertyChangeType.create({ changes: [propertyChange] }).addToSet(formattingMarks);
287
+ }
288
+ applyMarksToRunFormattingRepresentation({
289
+ tr: transaction,
290
+ representation: change.live,
291
+ marks
292
+ });
293
+ changedTargetBlockIds.add(change.targetBlockId);
294
+ }
295
+ return {
296
+ status: "matched",
297
+ transaction,
298
+ nextRevisionId,
299
+ changedTargetBlockIds: [...changedTargetBlockIds],
300
+ rangeCount: planned.length
301
+ };
302
+ };
303
+ /** Compare full authored run-property provenance in two snapshot source documents. */
304
+ const sameAuthoredInlineProvenance = (baseSnapshot, targetSnapshot) => {
305
+ const baseDocument = sourceDocumentOf(baseSnapshot);
306
+ const targetDocument = sourceDocumentOf(targetSnapshot);
307
+ const baseCarriers = collectCarriers({
308
+ doc: baseDocument,
309
+ targetSnapshot: baseSnapshot
310
+ });
311
+ const targetCarriers = collectCarriers({
312
+ doc: targetDocument,
313
+ targetSnapshot
314
+ });
315
+ if (!baseCarriers || !targetCarriers) return false;
316
+ const matched = matchCarrierStreams({
317
+ live: baseCarriers,
318
+ target: targetCarriers
319
+ });
320
+ if (!matched) return false;
321
+ const baseStyleResolver = styleResolverOf(baseSnapshot);
322
+ const targetStyleResolver = styleResolverOf(targetSnapshot);
323
+ return matched.every(({ live, target }) => hyperlinkIdentity(live.node) === hyperlinkIdentity(target.node) && canonicalJson(authoredFormattingAt({
324
+ doc: baseDocument,
325
+ representation: live,
326
+ styleResolver: baseStyleResolver
327
+ })) === canonicalJson(authoredFormattingAt({
328
+ doc: targetDocument,
329
+ representation: target,
330
+ styleResolver: targetStyleResolver
331
+ })));
332
+ };
333
+ //#endregion
334
+ export { matchInlineProvenance, sameAuthoredInlineProvenance };
@@ -1,8 +1,9 @@
1
1
  import { FolioAIEditOperation, FolioAIEditSnapshot } from "../ai-edits/types.js";
2
2
  import { TableGeometryPairing } from "../ai-edits/table-geometry.js";
3
3
  import { FolioDocumentStoryHandle } from "../ai-edits/headless.js";
4
- import { FolioContentComparisonWorkSession } from "./content.js";
4
+ import { CompareVerificationFailure } from "./verification.js";
5
5
  import { CompareChange } from "./types.js";
6
+ import { FolioContentComparisonWorkSession } from "./content.js";
6
7
  //#region src/compare/plan.d.ts
7
8
  /**
8
9
  * A table the target document already holds, which the operation naming it
@@ -23,6 +24,8 @@ type CompareTableTemplateRequest = {
23
24
  type CompareStoryPlan = {
24
25
  changes: CompareChange[];
25
26
  operations: FolioAIEditOperation[];
27
+ /** Differences the operation grammar cannot safely project. */
28
+ verificationFailures: CompareVerificationFailure[];
26
29
  /**
27
30
  * Where an operation's table comes from. Kept beside the operations rather
28
31
  * than inside them because a table node is not JSON, and the serialized
@@ -221,8 +221,10 @@ const shareAContainer = (left, right) => {
221
221
  const toFolioAIBlockParagraphProperties = (properties) => ({
222
222
  ...properties.styleId !== void 0 && { styleId: properties.styleId },
223
223
  ...properties.listLevel !== void 0 && { listLevel: properties.listLevel },
224
+ ...properties.listReference !== void 0 && { numbering: properties.listReference },
224
225
  ...properties.alignment !== void 0 && { alignment: properties.alignment },
225
- ...properties.spacing !== void 0 && { spacing: properties.spacing }
226
+ ...properties.spacing !== void 0 && { spacing: properties.spacing },
227
+ ...properties.indentation !== void 0 && { indentation: properties.indentation }
226
228
  });
227
229
  /**
228
230
  * For each step, the id of the next base block at or after it — the anchor a
@@ -484,6 +486,10 @@ const withTrailingDeletionRules = ({ baseSnapshot, targetSnapshot, operations, t
484
486
  ...insert.moveId !== void 0 && { moveId: insert.moveId },
485
487
  styleId: insert.styleId ?? null,
486
488
  listLevel: insert.listLevel ?? null,
489
+ numbering: insert.numbering ?? null,
490
+ indentation: insert.indentation ?? null,
491
+ ...insert.lineBreakMode !== void 0 && { lineBreakMode: insert.lineBreakMode },
492
+ ...insert.hardPageBreak !== void 0 && { hardPageBreak: insert.hardPageBreak },
487
493
  alignment: insert.alignment ?? null,
488
494
  spacing: insert.spacing ?? null
489
495
  };
@@ -557,6 +563,7 @@ const planStoryCompare = ({ story, baseSnapshot, targetSnapshot, maxOperations,
557
563
  const anchorIds = nextBaseBlockIdByStep(steps);
558
564
  const changes = [];
559
565
  const operations = [];
566
+ const verificationFailures = [];
560
567
  const tableTemplates = [];
561
568
  /**
562
569
  * The anchor everything past the base document's content hangs from: its
@@ -582,13 +589,19 @@ const planStoryCompare = ({ story, baseSnapshot, targetSnapshot, maxOperations,
582
589
  const moveIdOf = (baseBlockId) => `move-${baseBlockId}`;
583
590
  const pushInsertOperation = (block, anchorId) => {
584
591
  const moveSourceId = moveSourceByTargetBlockId.get(block.id);
592
+ const structuralBoundary = block.structuralBoundaries?.at(0);
593
+ const hardPageBreak = block.text.length === 0 && block.structuralBoundaries?.length === 1 && structuralBoundary?.type === "pageBreak" && structuralBoundary.offset === 0 ? { ...structuralBoundary.clear !== void 0 && { clear: structuralBoundary.clear } } : void 0;
585
594
  const shared = {
586
595
  text: block.text,
596
+ ...(block.text.includes(" ") || block.text.includes("\n")) && { lineBreakMode: "inline" },
587
597
  ...moveSourceId !== void 0 && { moveId: moveIdOf(moveSourceId) },
598
+ ...hardPageBreak !== void 0 && { hardPageBreak },
588
599
  styleId: block.styleId ?? null,
589
600
  listLevel: block.listLevel ?? null,
601
+ numbering: block.listReference ?? null,
590
602
  alignment: block.directAlignment ?? null,
591
- spacing: block.directSpacing ?? null
603
+ spacing: block.directSpacing ?? null,
604
+ indentation: block.directIndentation ?? null
592
605
  };
593
606
  if (anchorId !== null) {
594
607
  operations.push({
@@ -712,12 +725,26 @@ const planStoryCompare = ({ story, baseSnapshot, targetSnapshot, maxOperations,
712
725
  });
713
726
  break;
714
727
  }
715
- const segments = inlineFormattingSegments({
728
+ const formattingComparison = inlineFormattingSegments({
716
729
  baseBlock,
717
730
  targetBlock,
718
731
  maxSegments: maxOperations
719
732
  });
720
- if (segments === null) return null;
733
+ switch (formattingComparison.status) {
734
+ case "compared": break;
735
+ case "budget-exceeded": return null;
736
+ case "unalignable":
737
+ verificationFailures.push({
738
+ invariant: "accept-reproduces-target",
739
+ cause: "inline-formatting",
740
+ story,
741
+ detail: `supported inline formatting could not be aligned (${formattingComparison.side})`
742
+ });
743
+ break;
744
+ default: panic("Unhandled inline formatting comparison result", { result: formattingComparison });
745
+ }
746
+ if (formattingComparison.status !== "compared") break;
747
+ const { segments } = formattingComparison;
721
748
  if (segments.length === 0) break;
722
749
  changes.push({
723
750
  kind: "format",
@@ -935,6 +962,7 @@ const planStoryCompare = ({ story, baseSnapshot, targetSnapshot, maxOperations,
935
962
  return planned.length > maxOperations ? null : {
936
963
  changes,
937
964
  operations: planned,
965
+ verificationFailures,
938
966
  tableTemplates,
939
967
  tableGeometryPairings: tableGeometryPairingsOf(steps)
940
968
  };
@@ -1,4 +1,5 @@
1
- import { FolioAIEditOperation, FolioAIInlineFormattingPatch } from "../ai-edits/types.js";
1
+ import { FolioContentFormatRange } from "./content-types.js";
2
+ import { FolioAIEditOperation } from "../ai-edits/types.js";
2
3
  import { CompareDocxParseError, CompareDocxSerializeError } from "./types.js";
3
4
  import { Result } from "better-result";
4
5
  //#region src/compare/scenario.d.ts
@@ -45,13 +46,10 @@ type EditScriptStep = {
45
46
  {
46
47
  type: "mergeParagraphs";
47
48
  blockIndex: number;
48
- } | {
49
+ } | ({
49
50
  type: "formatRange";
50
51
  blockIndex: number;
51
- startOffset: number;
52
- endOffset: number;
53
- formatting: FolioAIInlineFormattingPatch;
54
- } | {
52
+ } & FolioContentFormatRange) | {
55
53
  type: "insertTableRow";
56
54
  blockIndex: number;
57
55
  position?: TableRowPosition;
@@ -0,0 +1,63 @@
1
+ import { document_d_exports } from "../types/document.js";
2
+ import { Node } from "prosemirror-model";
3
+ //#region src/compare/section-boundary-properties.d.ts
4
+ type SectionBoundaryPropertyChange = {
5
+ position: number;
6
+ current: document_d_exports.SectionProperties;
7
+ target: document_d_exports.SectionProperties;
8
+ };
9
+ type SectionBoundaryPropertiesComparison = {
10
+ status: "matched";
11
+ changes: readonly SectionBoundaryPropertyChange[];
12
+ } | {
13
+ status: "unalignable";
14
+ detail: string;
15
+ };
16
+ /**
17
+ * Compare section properties attached to retained top-level paragraph endpoints.
18
+ * A section endpoint cannot be added to or removed from a retained paragraph
19
+ * with `sectPrChange`: its previous payload is still a section endpoint.
20
+ */
21
+ declare const compareSectionBoundaryProperties: ({ current, target, mapTargetProperties }: {
22
+ current: Node;
23
+ target: Node;
24
+ mapTargetProperties?: (properties: document_d_exports.SectionProperties) => document_d_exports.SectionProperties | null;
25
+ }) => SectionBoundaryPropertiesComparison;
26
+ type MappedSectionBoundaryProperties = {
27
+ kind: "inserted";
28
+ target: document_d_exports.SectionProperties;
29
+ } | {
30
+ kind: "retained";
31
+ previous: document_d_exports.SectionProperties;
32
+ target: document_d_exports.SectionProperties;
33
+ };
34
+ type StageSectionBoundaryPropertiesResult = {
35
+ status: "matched";
36
+ transaction: import("prosemirror-state").Transaction;
37
+ nextRevisionId: number;
38
+ rangeCount: number;
39
+ } | {
40
+ status: "unalignable";
41
+ detail: string;
42
+ } | {
43
+ status: "budget-exceeded";
44
+ };
45
+ /** Stage section deltas after document operations, against the accepted projection. */
46
+ declare const stageSectionBoundaryProperties: ({ state, target, originalRevisionIdSeed, revisionStamp, author, maxRanges, mapTargetProperties }: {
47
+ state: import("prosemirror-state").EditorState;
48
+ target: Node;
49
+ originalRevisionIdSeed: number;
50
+ revisionStamp: {
51
+ idSeed: number;
52
+ date: string;
53
+ };
54
+ author: string;
55
+ maxRanges: number;
56
+ mapTargetProperties: (args: {
57
+ kind: "inserted" | "retained";
58
+ current: document_d_exports.SectionProperties | undefined;
59
+ target: document_d_exports.SectionProperties;
60
+ }) => MappedSectionBoundaryProperties | null;
61
+ }) => StageSectionBoundaryPropertiesResult;
62
+ //#endregion
63
+ export { SectionBoundaryPropertiesComparison, SectionBoundaryPropertyChange, StageSectionBoundaryPropertiesResult, compareSectionBoundaryProperties, stageSectionBoundaryProperties };