@stll/folio-core 0.37.1 → 0.37.3

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 (63) hide show
  1. package/dist/ai-edits/headless.d.ts +24 -1
  2. package/dist/ai-edits/headless.js +108 -71
  3. package/dist/ai-edits/read.d.ts +9 -1
  4. package/dist/ai-edits/read.js +34 -16
  5. package/dist/ai-edits/snapshot.d.ts +7 -5
  6. package/dist/ai-edits/snapshot.js +39 -12
  7. package/dist/ai-edits/table-cell-mutations.js +10 -5
  8. package/dist/ai-edits/table-template.js +19 -5
  9. package/dist/compare/compare.d.ts +2 -3
  10. package/dist/compare/compare.js +41 -64
  11. package/dist/compare/content-alignment.js +0 -1
  12. package/dist/compare/scenario.d.ts +11 -2
  13. package/dist/compare/scenario.js +6 -2
  14. package/dist/controller/hiddenEditorManager.js +11 -0
  15. package/dist/controller/layoutPipeline.js +1 -1
  16. package/dist/display-list/build/textBoxPrimitives.js +34 -1
  17. package/dist/display-list/dom/renderDisplayListToDom.js +5 -2
  18. package/dist/display-list/primitives.d.ts +1 -1
  19. package/dist/display-list/types.d.ts +8 -4
  20. package/dist/docx/paragraphPropertySource.d.ts +82 -2
  21. package/dist/docx/paragraphPropertySource.js +569 -3
  22. package/dist/docx/parser.js +9 -0
  23. package/dist/docx/server/materializeYjsDocx.d.ts +1 -1
  24. package/dist/docx/server/materializeYjsDocx.js +21 -2
  25. package/dist/headless-layout.js +1 -1
  26. package/dist/layout-bridge/convert/headerFooterLayout.d.ts +7 -1
  27. package/dist/layout-bridge/convert/headerFooterLayout.js +20 -3
  28. package/dist/layout-bridge/convert/toFlowBlocks.js +43 -12
  29. package/dist/layout-engine/index.js +20 -10
  30. package/dist/layout-engine/measure/measureBlocks.d.ts +7 -3
  31. package/dist/layout-engine/measure/measureBlocks.js +10 -7
  32. package/dist/layout-engine/measure/measureParagraph.d.ts +2 -0
  33. package/dist/layout-engine/measure/measureParagraph.js +1 -1
  34. package/dist/layout-engine/paginator.d.ts +1 -1
  35. package/dist/layout-engine/paginator.js +41 -4
  36. package/dist/layout-engine/textBoxFlow.d.ts +2 -0
  37. package/dist/layout-engine/textBoxFlow.js +9 -5
  38. package/dist/layout-engine/types.d.ts +6 -0
  39. package/dist/layout-painter/documentColors.d.ts +10 -1
  40. package/dist/layout-painter/documentColors.js +16 -1
  41. package/dist/layout-painter/renderParagraph.js +9 -24
  42. package/dist/layout-painter/renderTable.js +3 -3
  43. package/dist/layout-painter/renderTextBox.js +2 -1
  44. package/dist/pdf/pageSpace.d.ts +12 -1
  45. package/dist/pdf/pageSpace.js +24 -1
  46. package/dist/pdf/paint.js +2 -2
  47. package/dist/prosemirror/attrs/index.js +19 -8
  48. package/dist/prosemirror/commands/comments.js +12 -3
  49. package/dist/prosemirror/commands/tableCellMergeResolution.js +18 -11
  50. package/dist/prosemirror/conversion/fromProseDoc.js +94 -21
  51. package/dist/prosemirror/conversion/toProseDoc.js +100 -8
  52. package/dist/prosemirror/extensions/core/DocExtension.js +2 -0
  53. package/dist/prosemirror/extensions/core/ParagraphExtension.js +2 -0
  54. package/dist/prosemirror/extensions/features/ParaIdAllocatorExtension.d.ts +5 -1
  55. package/dist/prosemirror/extensions/features/ParaIdAllocatorExtension.js +132 -41
  56. package/dist/prosemirror/extensions/nodes/TextBoxExtension.d.ts +2 -0
  57. package/dist/prosemirror/extensions/nodes/TextBoxExtension.js +7 -0
  58. package/dist/prosemirror/schema/nodes.d.ts +3 -1
  59. package/dist/prosemirror/yjsParagraphSourceContract.d.ts +9 -0
  60. package/dist/prosemirror/yjsParagraphSourceContract.js +26 -0
  61. package/dist/utils/rotationBoundingBox.d.ts +5 -1
  62. package/dist/utils/rotationBoundingBox.js +9 -1
  63. package/package.json +2 -2
@@ -1,3 +1,4 @@
1
+ import { decodeTableCellParagraphSourcePayload, restoreTableCellsWithParagraphPropertySources, transportTableCellsWithParagraphPropertySources } from "../docx/paragraphPropertySource.js";
1
2
  import { expectTableCellAttrs } from "../prosemirror/attrs/index.js";
2
3
  import { standaloneTableCellFromProseMirror } from "../prosemirror/conversion/fromProseDoc.js";
3
4
  import { standaloneTableCellToProseMirror } from "../prosemirror/conversion/toProseDoc.js";
@@ -105,7 +106,7 @@ const mergeTrackedVerticalTableCells = ({ tr, tablePosition, table, rectangle, r
105
106
  bottom: row + 1
106
107
  })) return null;
107
108
  const attrs = expectTableCellAttrs(cell);
108
- if (attrs.colspan !== 1 || attrs.rowspan !== 1 || attrs.cellMarker !== void 0 || attrs._docxVMergeContinuationCells !== void 0 || attrs._preserveVMergeRestart === true || attrs._originalFormatting?.vMerge !== void 0) return null;
109
+ if (attrs.colspan !== 1 || attrs.rowspan !== 1 || attrs.cellMarker !== void 0 || attrs._docxVMergeContinuationCells !== void 0 && attrs._docxVMergeContinuationCells !== null || attrs._preserveVMergeRestart === true || attrs._originalFormatting?.vMerge !== void 0) return null;
109
110
  cells.push({
110
111
  position,
111
112
  cell
@@ -139,7 +140,7 @@ const mergeTrackedVerticalTableCells = ({ tr, tablePosition, table, rectangle, r
139
140
  rectangle
140
141
  });
141
142
  if (!nextTr) return null;
142
- nextTr.setNodeAttribute(tablePosition + 1 + origin.position, "_docxVMergeContinuationCells", continuationCells);
143
+ nextTr.setNodeAttribute(tablePosition + 1 + origin.position, "_docxVMergeContinuationCells", transportTableCellsWithParagraphPropertySources(continuationCells));
143
144
  markStructuralChange(nextTr);
144
145
  return nextTr;
145
146
  };
@@ -196,8 +197,10 @@ const splitTrackedVerticalTableCell = ({ tr, tablePosition, table, rectangle, re
196
197
  if (!cell) return null;
197
198
  const attrs = expectTableCellAttrs(cell);
198
199
  if (attrs.colspan !== 1 || attrs.rowspan !== rectangle.bottom - rectangle.top || attrs.cellMarker !== void 0) return null;
199
- const continuationCells = attrs._docxVMergeContinuationCells;
200
+ const continuationPayload = attrs._docxVMergeContinuationCells === void 0 || attrs._docxVMergeContinuationCells === null ? null : decodeTableCellParagraphSourcePayload(attrs._docxVMergeContinuationCells);
201
+ const continuationCells = continuationPayload?.cells;
200
202
  if (continuationCells !== void 0 && continuationCells.length !== rectangle.bottom - rectangle.top - 1) return null;
203
+ const restoredContinuationCells = continuationPayload ? restoreTableCellsWithParagraphPropertySources(continuationPayload) : void 0;
201
204
  const marker = {
202
205
  kind: "merge",
203
206
  info: {
@@ -211,9 +214,11 @@ const splitTrackedVerticalTableCell = ({ tr, tablePosition, table, rectangle, re
211
214
  for (let index = 0; index < rectangle.bottom - rectangle.top - 1; index++) {
212
215
  const source = continuationCells?.[index];
213
216
  if (source?.structuralChange !== void 0) return null;
214
- const insertedCell = source ? trackedSplitCellFromStoredSource({
217
+ const restoredSource = restoredContinuationCells?.[index];
218
+ if (source && !restoredSource) return null;
219
+ const insertedCell = restoredSource ? trackedSplitCellFromStoredSource({
215
220
  origin: cell,
216
- source,
221
+ source: restoredSource,
217
222
  marker
218
223
  }) : cell.type.createAndFill({
219
224
  ...cell.attrs,
@@ -1,3 +1,5 @@
1
+ import { cloneTableCellsWithParagraphPropertyCaptures, decodeTableCellParagraphSourcePayload, recreateProseNodeWithDetachedParagraphPropertySource } from "../docx/paragraphPropertySource.js";
2
+ import { expectTableCellAttrs } from "../prosemirror/attrs/index.js";
1
3
  import { stripBlockIdentityAttrs } from "./block-identity.js";
2
4
  import { Fragment } from "prosemirror-model";
3
5
  //#region src/ai-edits/table-template.ts
@@ -137,10 +139,18 @@ const copiedAttrs = (node, context) => {
137
139
  trIns: context.revision
138
140
  } : attrs;
139
141
  case "cell":
140
- case "header_cell": return context.clampRowSpan ? {
141
- ...attrs,
142
- rowspan: 1
143
- } : attrs;
142
+ case "header_cell": {
143
+ if (context.clampRowSpan) return {
144
+ ...attrs,
145
+ rowspan: 1,
146
+ _docxVMergeContinuationCells: null
147
+ };
148
+ const continuationCells = expectTableCellAttrs(node)._docxVMergeContinuationCells;
149
+ return continuationCells !== void 0 && continuationCells !== null ? {
150
+ ...attrs,
151
+ _docxVMergeContinuationCells: cloneTableCellsWithParagraphPropertyCaptures(decodeTableCellParagraphSourcePayload(continuationCells))
152
+ } : attrs;
153
+ }
144
154
  default: return attrs;
145
155
  }
146
156
  };
@@ -162,7 +172,11 @@ const copyNode = (node, context) => {
162
172
  const copied = copyNode(child, inner);
163
173
  if (copied) content.push(copied);
164
174
  });
165
- return node.type.create(copiedAttrs(node, context), Fragment.fromArray(content), marks);
175
+ return recreateProseNodeWithDetachedParagraphPropertySource(node, {
176
+ attrs: copiedAttrs(node, context),
177
+ content: Fragment.fromArray(content),
178
+ marks
179
+ });
166
180
  };
167
181
  /**
168
182
  * The template table, ready to insert: its properties, grid, rows and cells
@@ -31,7 +31,6 @@ type ParsedComparison = {
31
31
  baseBuffer: ArrayBuffer;
32
32
  baseCarriedRevisions: boolean;
33
33
  reviewer: FolioDocxReviewer;
34
- targetReviewer: FolioDocxReviewer;
35
34
  revisionStamp: FolioRevisionStamp;
36
35
  packageDate: Date;
37
36
  pairs: readonly ComparedStoryPair[];
@@ -50,7 +49,7 @@ type PlannedStoryComparison = {
50
49
  * Stage 2: align every paired story and derive its operations. Pure — no
51
50
  * parsing, no serialization, no clock.
52
51
  */
53
- declare const planComparison: ({ pairs, targetReviewer }: ParsedComparison) => Result<readonly PlannedStoryComparison[], CompareDocxOperationLimitError>;
52
+ declare const planComparison: ({ pairs }: ParsedComparison) => Result<readonly PlannedStoryComparison[], CompareDocxOperationLimitError>;
54
53
  declare const getCompareSkipDisposition: (reason: FolioAIEditSkipReason) => "fatal" | "unwritable";
55
54
  /** What stage 3 produced: the change list, whether it was proven, and whether it wrote anything. */
56
55
  type AppliedComparison = {
@@ -77,7 +76,7 @@ type AppliedComparison = {
77
76
  * what you could not represent" and "give me nothing unless you can prove it"
78
77
  * are both legitimate asks and only the caller knows which one it is making.
79
78
  */
80
- declare const applyComparison: ({ reviewer, targetReviewer, revisionStamp, granularity, numberingChanges }: ParsedComparison, planned: readonly PlannedStoryComparison[]) => Result<AppliedComparison, CompareDocxApplyError>;
79
+ declare const applyComparison: ({ reviewer, revisionStamp, granularity, numberingChanges }: ParsedComparison, planned: readonly PlannedStoryComparison[]) => Result<AppliedComparison, CompareDocxApplyError>;
81
80
  /**
82
81
  * Stage 4: the result package, with every ZIP entry date pinned.
83
82
  *
@@ -1,5 +1,5 @@
1
- import { FolioDocxReviewer } from "../ai-edits/headless.js";
2
- import { numberingReferenceKeysOf } from "../ai-edits/snapshot.js";
1
+ import { FolioDocxReviewer, getFolioDocxComparisonAccess } from "../ai-edits/headless.js";
2
+ import { numberingReferenceKeysOf, storyTablesOf } from "../ai-edits/snapshot.js";
3
3
  import { projectTableGeometry } from "../ai-edits/table-geometry.js";
4
4
  import { tableTemplateCanCrossPackageLosslessly } from "../ai-edits/table-template.js";
5
5
  import { createScopedWordDiffOptions } from "../ai-edits/word-diff.js";
@@ -60,25 +60,6 @@ const parseSide = async (buffer, side, author) => await Result.tryPromise({
60
60
  cause
61
61
  })
62
62
  });
63
- /** Read before either side is resolved, so it describes the package as it arrived. */
64
- const existingRevisionsOf = (reviewer) => {
65
- let highest = 0;
66
- let present = false;
67
- for (const { handle } of reviewer.listStories()) {
68
- const story = reviewer.readReviewedStory({
69
- story: handle,
70
- view: "current-markup"
71
- });
72
- for (const change of story?.changes ?? []) {
73
- highest = Math.max(highest, change.id);
74
- present = true;
75
- }
76
- }
77
- return {
78
- idSeed: highest + 1,
79
- present
80
- };
81
- };
82
63
  /** Verify formatting only where the plan claims a text-equal formatting change. */
83
64
  const formattingRoundTripFailure = ({ invariant, story, changes, actualBlocks, expectedBlocks, expectedBlockId }) => {
84
65
  const expectedIndexById = new Map(expectedBlocks.map(({ id }, index) => [id, index]));
@@ -146,15 +127,20 @@ const parseComparison = async (base, target, options) => {
146
127
  if (targetParse.isErr()) return Result.err(targetParse.error);
147
128
  const reviewer = baseParse.value;
148
129
  const targetReviewer = targetParse.value;
149
- const existing = existingRevisionsOf(reviewer);
150
- for (const { handle } of reviewer.listStories()) reviewer.resolveReviewedStory({
151
- story: handle,
152
- view: "final"
153
- });
154
- for (const { handle } of targetReviewer.listStories()) targetReviewer.resolveReviewedStory({
155
- story: handle,
156
- view: "final"
157
- });
130
+ const baseProjection = getFolioDocxComparisonAccess(reviewer).projectStories("with-revision-census");
131
+ const targetProjection = getFolioDocxComparisonAccess(targetReviewer).projectStories("without-revision-census");
132
+ const baseStories = [];
133
+ const targetStories = [];
134
+ const baseSnapshots = /* @__PURE__ */ new Map();
135
+ const targetSnapshots = /* @__PURE__ */ new Map();
136
+ for (const { handle, snapshot } of baseProjection.stories) {
137
+ baseStories.push(handle);
138
+ baseSnapshots.set(handle, snapshot);
139
+ }
140
+ for (const { handle, snapshot } of targetProjection.stories) {
141
+ targetStories.push(handle);
142
+ targetSnapshots.set(handle, snapshot);
143
+ }
158
144
  const pairs = [];
159
145
  const unsupported = [];
160
146
  const referencedNumberingLevels = /* @__PURE__ */ new Set();
@@ -162,10 +148,10 @@ const parseComparison = async (base, target, options) => {
162
148
  if (!snapshot) return;
163
149
  for (const referenceKey of numberingReferenceKeysOf(snapshot)) referencedNumberingLevels.add(referenceKey);
164
150
  };
165
- for (const { baseStory, revisedStory: targetStory } of pairFolioDocumentStories(reviewer.listStories().map(({ handle }) => handle), targetReviewer.listStories().map(({ handle }) => handle))) {
151
+ for (const { baseStory, revisedStory: targetStory } of pairFolioDocumentStories(baseStories, targetStories)) {
166
152
  if (!baseStory) {
167
153
  if (!targetStory) panic("A story pair contained neither a base nor a target story");
168
- collectNumberingReferences(targetReviewer.snapshotStory(targetStory));
154
+ collectNumberingReferences(targetSnapshots.get(targetStory));
169
155
  unsupported.push({
170
156
  reason: "story-missing-in-base",
171
157
  baseStory: null,
@@ -174,7 +160,7 @@ const parseComparison = async (base, target, options) => {
174
160
  continue;
175
161
  }
176
162
  if (!targetStory) {
177
- collectNumberingReferences(reviewer.snapshotStory(baseStory));
163
+ collectNumberingReferences(baseSnapshots.get(baseStory));
178
164
  unsupported.push({
179
165
  reason: "story-missing-in-target",
180
166
  baseStory,
@@ -182,8 +168,8 @@ const parseComparison = async (base, target, options) => {
182
168
  });
183
169
  continue;
184
170
  }
185
- const baseSnapshot = reviewer.snapshotStory(baseStory);
186
- const targetSnapshot = targetReviewer.snapshotStory(targetStory);
171
+ const baseSnapshot = baseSnapshots.get(baseStory);
172
+ const targetSnapshot = targetSnapshots.get(targetStory);
187
173
  collectNumberingReferences(baseSnapshot);
188
174
  collectNumberingReferences(targetSnapshot);
189
175
  if (!baseSnapshot || !targetSnapshot) {
@@ -204,12 +190,11 @@ const parseComparison = async (base, target, options) => {
204
190
  return Result.ok({
205
191
  granularity: options.granularity ?? "word",
206
192
  baseBuffer: base,
207
- baseCarriedRevisions: existing.present,
193
+ baseCarriedRevisions: baseProjection.revisions.present,
208
194
  reviewer,
209
- targetReviewer,
210
195
  revisionStamp: {
211
196
  date: options.timestamp,
212
- idSeed: existing.idSeed
197
+ idSeed: baseProjection.revisions.highestId + 1
213
198
  },
214
199
  packageDate,
215
200
  pairs,
@@ -217,8 +202,8 @@ const parseComparison = async (base, target, options) => {
217
202
  unsupported
218
203
  });
219
204
  };
220
- const planCopiesNonPortableWholeTable = (targetReviewer, pair, plan) => {
221
- const targetTables = new Map(targetReviewer.storyTables({ story: pair.targetStory }).map(({ index, node }) => [index, node]));
205
+ const planCopiesNonPortableWholeTable = (pair, plan) => {
206
+ const targetTables = new Map(storyTablesOf(pair.targetSnapshot).map(({ index, node }) => [index, node]));
222
207
  return plan.tableTemplates.some(({ targetRowIndex, targetTableIndex }) => {
223
208
  if (targetRowIndex !== void 0) return false;
224
209
  const table = targetTables.get(targetTableIndex);
@@ -229,7 +214,7 @@ const planCopiesNonPortableWholeTable = (targetReviewer, pair, plan) => {
229
214
  * Stage 2: align every paired story and derive its operations. Pure — no
230
215
  * parsing, no serialization, no clock.
231
216
  */
232
- const planComparison = ({ pairs, targetReviewer }) => {
217
+ const planComparison = ({ pairs }) => {
233
218
  const planned = [];
234
219
  const workSession = createContentComparisonWorkSession();
235
220
  let remainingOperations = MAX_COMPARE_OPERATIONS;
@@ -246,7 +231,7 @@ const planComparison = ({ pairs, targetReviewer }) => {
246
231
  wholeTableReplacement: "allow",
247
232
  workSession
248
233
  });
249
- if (plan && planCopiesNonPortableWholeTable(targetReviewer, pair, plan)) {
234
+ if (plan && planCopiesNonPortableWholeTable(pair, plan)) {
250
235
  workSession.alignment.remainingLcsCells = remainingLcsCells;
251
236
  workSession.alignment.remainingStructuralTokenLookups = remainingStructuralTokenLookups;
252
237
  workSession.remainingMoveComparisons = remainingMoveComparisons;
@@ -343,7 +328,8 @@ const resolveTableTemplates = (targetTables, requests) => {
343
328
  * what you could not represent" and "give me nothing unless you can prove it"
344
329
  * are both legitimate asks and only the caller knows which one it is making.
345
330
  */
346
- const applyComparison = ({ reviewer, targetReviewer, revisionStamp, granularity, numberingChanges }, planned) => {
331
+ const applyComparison = ({ reviewer, revisionStamp, granularity, numberingChanges }, planned) => {
332
+ const comparisonAccess = getFolioDocxComparisonAccess(reviewer);
347
333
  const changes = [...numberingChanges];
348
334
  const failures = [];
349
335
  let idSeed = revisionStamp.idSeed;
@@ -352,12 +338,9 @@ const applyComparison = ({ reviewer, targetReviewer, revisionStamp, granularity,
352
338
  for (const { pair, plan } of planned) {
353
339
  changes.push(...plan.changes);
354
340
  if (plan.operations.length === 0 && plan.tableGeometryPairings.length === 0) continue;
355
- const baseBefore = reviewer.readReviewedStory({
356
- story: pair.baseStory,
357
- view: "final"
358
- })?.snapshot.blocks ?? [];
359
- const baseBeforeGeometry = projectTableGeometry(reviewer.storyTables({ story: pair.baseStory }));
360
- const targetTables = new Map(targetReviewer.storyTables({ story: pair.targetStory }).map(({ index, node }) => [index, node]));
341
+ const baseBefore = pair.baseSnapshot.blocks;
342
+ const baseBeforeGeometry = projectTableGeometry(storyTablesOf(pair.baseSnapshot));
343
+ const targetTables = new Map(storyTablesOf(pair.targetSnapshot).map(({ index, node }) => [index, node]));
361
344
  const afterGeometry = reviewer.matchStoryTableGeometry({
362
345
  story: pair.baseStory,
363
346
  targetTables,
@@ -396,14 +379,14 @@ const applyComparison = ({ reviewer, targetReviewer, revisionStamp, granularity,
396
379
  skipped: refused
397
380
  }));
398
381
  }
399
- const acceptedStory = reviewer.readReviewedStory({
382
+ const acceptedSnapshot = comparisonAccess.snapshotReviewedStory({
400
383
  story: pair.baseStory,
401
384
  view: "final"
402
385
  });
403
386
  const acceptFailure = classifyProjectionMismatch({
404
387
  invariant: "accept-reproduces-target",
405
388
  story: pair.baseStory,
406
- actual: acceptedStory?.snapshot.blocks ?? [],
389
+ actual: acceptedSnapshot?.blocks ?? [],
407
390
  expected: pair.targetSnapshot.blocks
408
391
  });
409
392
  if (acceptFailure) failures.push(acceptFailure);
@@ -412,20 +395,20 @@ const applyComparison = ({ reviewer, targetReviewer, revisionStamp, granularity,
412
395
  invariant: "accept-reproduces-target",
413
396
  story: pair.baseStory,
414
397
  changes: plan.changes,
415
- actualBlocks: acceptedStory?.snapshot.blocks ?? [],
398
+ actualBlocks: acceptedSnapshot?.blocks ?? [],
416
399
  expectedBlocks: pair.targetSnapshot.blocks,
417
400
  expectedBlockId: ({ targetBlockId }) => targetBlockId
418
401
  });
419
402
  if (formattingFailure) failures.push(formattingFailure);
420
403
  }
421
- const rejectedStory = reviewer.readReviewedStory({
404
+ const rejectedSnapshot = comparisonAccess.snapshotReviewedStory({
422
405
  story: pair.baseStory,
423
406
  view: "original"
424
407
  });
425
408
  const rejectFailure = classifyProjectionMismatch({
426
409
  invariant: "reject-reproduces-base",
427
410
  story: pair.baseStory,
428
- actual: rejectedStory?.snapshot.blocks ?? [],
411
+ actual: rejectedSnapshot?.blocks ?? [],
429
412
  expected: baseBefore
430
413
  });
431
414
  if (rejectFailure) failures.push(rejectFailure);
@@ -434,7 +417,7 @@ const applyComparison = ({ reviewer, targetReviewer, revisionStamp, granularity,
434
417
  invariant: "reject-reproduces-base",
435
418
  story: pair.baseStory,
436
419
  changes: plan.changes,
437
- actualBlocks: rejectedStory?.snapshot.blocks ?? [],
420
+ actualBlocks: rejectedSnapshot?.blocks ?? [],
438
421
  expectedBlocks: pair.baseSnapshot.blocks,
439
422
  expectedBlockId: ({ baseBlockId }) => baseBlockId
440
423
  });
@@ -443,20 +426,14 @@ const applyComparison = ({ reviewer, targetReviewer, revisionStamp, granularity,
443
426
  const geometryAcceptFailure = classifyGeometryMismatch({
444
427
  invariant: "accept-reproduces-target",
445
428
  story: pair.baseStory,
446
- actual: projectTableGeometry(reviewer.storyTables({
447
- story: pair.baseStory,
448
- view: "final"
449
- })),
450
- expected: projectTableGeometry(targetReviewer.storyTables({ story: pair.targetStory }))
429
+ actual: projectTableGeometry(acceptedSnapshot ? storyTablesOf(acceptedSnapshot) : []),
430
+ expected: projectTableGeometry(storyTablesOf(pair.targetSnapshot))
451
431
  });
452
432
  if (geometryAcceptFailure) failures.push(geometryAcceptFailure);
453
433
  const geometryRejectFailure = classifyGeometryMismatch({
454
434
  invariant: "reject-reproduces-base",
455
435
  story: pair.baseStory,
456
- actual: projectTableGeometry(reviewer.storyTables({
457
- story: pair.baseStory,
458
- view: "original"
459
- })),
436
+ actual: projectTableGeometry(rejectedSnapshot ? storyTablesOf(rejectedSnapshot) : []),
460
437
  expected: baseBeforeGeometry
461
438
  });
462
439
  if (geometryRejectFailure) failures.push(geometryRejectFailure);
@@ -601,7 +601,6 @@ const trustedContentSequencePairs = ({ exactPairs, stablePairs, revisedLength, p
601
601
  return [...primary, ...secondary].toSorted((left, right) => left.baseIndex - right.baseIndex || left.revisedIndex - right.revisedIndex);
602
602
  };
603
603
  const persistedContentSequencePairs = ({ base, revised, anchors, canPair }) => {
604
- if (anchors.length === 0) return /* @__PURE__ */ new Set();
605
604
  const uniqueIndexesByFirstId = (items) => {
606
605
  const indexes = /* @__PURE__ */ new Map();
607
606
  items.forEach(({ profile }, itemIndex) => {
@@ -1,7 +1,15 @@
1
- import { FolioAIInlineFormattingPatch } from "../ai-edits/types.js";
1
+ import { FolioAIEditOperation, FolioAIInlineFormattingPatch } from "../ai-edits/types.js";
2
2
  import { CompareDocxParseError, CompareDocxSerializeError } from "./types.js";
3
3
  import { Result } from "better-result";
4
4
  //#region src/compare/scenario.d.ts
5
+ type InsertTableRowOperation = Extract<FolioAIEditOperation, {
6
+ type: "insertTableRow";
7
+ }>;
8
+ type TableRowPosition = NonNullable<InsertTableRowOperation["position"]>;
9
+ declare const EDIT_SCRIPT_TABLE_ROW_POSITION: {
10
+ readonly after: "after";
11
+ readonly before: "before";
12
+ };
5
13
  type EditScriptStep = {
6
14
  type: "insertParagraphAfter";
7
15
  blockIndex: number;
@@ -46,6 +54,7 @@ type EditScriptStep = {
46
54
  } | {
47
55
  type: "insertTableRow";
48
56
  blockIndex: number;
57
+ position?: TableRowPosition;
49
58
  cellTexts: readonly string[];
50
59
  } | {
51
60
  type: "deleteTableRow";
@@ -74,4 +83,4 @@ type EditScriptResult = {
74
83
  */
75
84
  declare const applyEditScript: (base: ArrayBuffer, script: EditScript) => Promise<Result<EditScriptResult, CompareDocxParseError | CompareDocxSerializeError>>;
76
85
  //#endregion
77
- export { EDIT_SCRIPT_UNRESOLVED_REASONS, EditScript, EditScriptResult, EditScriptStep, EditScriptUnresolvedReason, EditScriptUnresolvedStep, applyEditScript };
86
+ export { EDIT_SCRIPT_TABLE_ROW_POSITION, EDIT_SCRIPT_UNRESOLVED_REASONS, EditScript, EditScriptResult, EditScriptStep, EditScriptUnresolvedReason, EditScriptUnresolvedStep, applyEditScript };
@@ -24,6 +24,10 @@ import { Result, panic } from "better-result";
24
24
  * dropped: a script silently doing less than it says would weaken every
25
25
  * property built on it.
26
26
  */
27
+ const EDIT_SCRIPT_TABLE_ROW_POSITION = {
28
+ after: "after",
29
+ before: "before"
30
+ };
27
31
  const EDIT_SCRIPT_UNRESOLVED_REASONS = Object.freeze([
28
32
  "block-out-of-range",
29
33
  "block-not-in-table",
@@ -160,7 +164,7 @@ const planStep = ({ step, blocks, nextOperationId }) => {
160
164
  id: nextOperationId(),
161
165
  type: "insertTableRow",
162
166
  blockId: block.id,
163
- position: "after",
167
+ position: step.position ?? EDIT_SCRIPT_TABLE_ROW_POSITION.after,
164
168
  cellTexts: [...step.cellTexts]
165
169
  }]
166
170
  } : {
@@ -260,4 +264,4 @@ const applyEditScript = async (base, script) => {
260
264
  });
261
265
  };
262
266
  //#endregion
263
- export { EDIT_SCRIPT_UNRESOLVED_REASONS, applyEditScript };
267
+ export { EDIT_SCRIPT_TABLE_ROW_POSITION, EDIT_SCRIPT_UNRESOLVED_REASONS, applyEditScript };
@@ -7,6 +7,7 @@ import { ensureParaIdsInDoc, ensureParaIdsInState } from "../prosemirror/extensi
7
7
  import { createDocumentNumberingPlugin } from "../prosemirror/plugins/documentNumbering.js";
8
8
  import { createDocumentStylesPlugin } from "../prosemirror/plugins/documentStyles.js";
9
9
  import { schema } from "../prosemirror/schema/index.js";
10
+ import { proseDocumentParagraphSourceContract, readYjsParagraphSourceContract, withParagraphSourceContract, writeYjsParagraphSourceContract } from "../prosemirror/yjsParagraphSourceContract.js";
10
11
  import { createHiddenEditorApi } from "./hiddenEditorApi.js";
11
12
  import { panic } from "better-result";
12
13
  import { EditorState } from "prosemirror-state";
@@ -125,9 +126,18 @@ function createHiddenEditorState(options) {
125
126
  plugins
126
127
  }));
127
128
  collaborationModules.yProseMirror.prosemirrorToYXmlFragment(seedState.doc, collaboration.yXmlFragment);
129
+ const collaborationDocument = collaboration.yXmlFragment.doc;
130
+ if (!collaborationDocument) panic("A collaboration fragment must belong to a Yjs document before seeding.");
131
+ if (proseDocumentParagraphSourceContract(seedState.doc)) writeYjsParagraphSourceContract(collaborationDocument, seedState.doc);
128
132
  collaboration.onSeeded?.();
129
133
  }
130
134
  let { doc } = collaborationModules.yProseMirror.initProseMirrorDoc(collaboration.yXmlFragment, activeSchema);
135
+ const collaborationDocument = collaboration.yXmlFragment.doc;
136
+ if (!collaborationDocument) panic("A collaboration fragment must belong to a Yjs document before loading.");
137
+ const collaborationContract = readYjsParagraphSourceContract(collaborationDocument);
138
+ const localContract = proseDocumentParagraphSourceContract(localDoc);
139
+ if (localContract && collaborationContract !== localContract) panic("The collaboration state belongs to a different paragraph-property source.");
140
+ if (collaborationContract) doc = withParagraphSourceContract(doc, collaborationContract);
131
141
  const initializedState = normalizeSeedState(EditorState.create({
132
142
  doc,
133
143
  schema: activeSchema,
@@ -136,6 +146,7 @@ function createHiddenEditorState(options) {
136
146
  if (!initializedState.doc.eq(doc)) {
137
147
  collaborationModules.yProseMirror.prosemirrorToYXmlFragment(initializedState.doc, collaboration.yXmlFragment);
138
148
  ({doc} = collaborationModules.yProseMirror.initProseMirrorDoc(collaboration.yXmlFragment, activeSchema));
149
+ if (collaborationContract) doc = withParagraphSourceContract(doc, collaborationContract);
139
150
  }
140
151
  const startedAt = performance.now();
141
152
  const state = normalizeSeedState(EditorState.create({
@@ -181,7 +181,7 @@ function runLayoutPipeline(deps, state, options = {}) {
181
181
  const hfPageCountEstimate = layout?.pages.length ?? 1;
182
182
  const hfClock = /* @__PURE__ */ new Date();
183
183
  const buildHfOptions = (pageCount, fieldInputs) => {
184
- const hfMeasureBlocks = (hfBlocks, hfWidth) => measureBlocks(hfBlocks, hfWidth, void 0, void 0, buildHeaderFooterFieldValues(hfBlocks, pageCount, hfClock, fieldInputs));
184
+ const hfMeasureBlocks = (hfBlocks, hfWidth) => measureBlocks(hfBlocks, hfWidth, void 0, void 0, buildHeaderFooterFieldValues(hfBlocks, pageCount, hfClock, fieldInputs), { allowEndTabOverflow: true });
185
185
  return {
186
186
  ...flowOpts.styles ? { styles: flowOpts.styles } : {},
187
187
  ..._theme !== void 0 ? { theme: _theme } : {},
@@ -1,9 +1,10 @@
1
1
  import { layoutTextBoxContent } from "../../layout-engine/measure/textBoxParagraphLayout.js";
2
2
  import { DEFAULT_TEXTBOX_MARGINS } from "../../layout-engine/types.js";
3
+ import { hasHorizontalFlip, hasVerticalFlip, parseRotationDegrees } from "../../utils/rotationBoundingBox.js";
3
4
  import { HIT_REGION_KINDS } from "../primitives.js";
4
5
  import { parseDisplayColor } from "./colors.js";
5
6
  import { paintParagraphFragment } from "./paragraphPrimitives.js";
6
- import { blockRegion } from "./regions.js";
7
+ import { blockRegion, createPageComposer } from "./regions.js";
7
8
  import { resolveBorderStroke } from "./strokes.js";
8
9
  import { paintTableBlock } from "./tablePrimitives.js";
9
10
  import { UNSUPPORTED_CONSTRUCT } from "./unsupported.js";
@@ -15,6 +16,38 @@ import { UNSUPPORTED_CONSTRUCT } from "./unsupported.js";
15
16
  * measurer used, so the paragraphs land where their heights were reserved.
16
17
  */
17
18
  const paintTextBoxFragment = ({ composer, fragment, block, measure, context }) => {
19
+ const rotation = parseRotationDegrees(block.transform);
20
+ const flipH = hasHorizontalFlip(block.transform);
21
+ const flipV = hasVerticalFlip(block.transform);
22
+ if (rotation === 0 && !flipH && !flipV) {
23
+ paintUnrotatedTextBoxFragment({
24
+ composer,
25
+ fragment,
26
+ block,
27
+ measure,
28
+ context
29
+ });
30
+ return;
31
+ }
32
+ const inner = createPageComposer();
33
+ paintUnrotatedTextBoxFragment({
34
+ composer: inner,
35
+ fragment,
36
+ block,
37
+ measure,
38
+ context
39
+ });
40
+ composer.push([{
41
+ kind: "rotateGroup",
42
+ degrees: rotation,
43
+ originXPx: fragment.x + fragment.width / 2,
44
+ originYPx: fragment.y + fragment.height / 2,
45
+ ...flipH ? { scaleX: -1 } : {},
46
+ ...flipV ? { scaleY: -1 } : {},
47
+ children: inner.primitives()
48
+ }]);
49
+ };
50
+ const paintUnrotatedTextBoxFragment = ({ composer, fragment, block, measure, context }) => {
18
51
  const primitives = [];
19
52
  const box = {
20
53
  xPx: fragment.x,
@@ -434,10 +434,13 @@ const paintClipGroup = ({ rect, children, regions }, context) => {
434
434
  regions
435
435
  }, inner);
436
436
  };
437
- const paintRotateGroup = ({ degrees, originXPx, originYPx, children }, context) => {
437
+ const paintRotateGroup = ({ degrees, originXPx, originYPx, scaleX, scaleY, children }, context) => {
438
438
  const element = createTransparentGroupDiv(context);
439
439
  element.style.transformOrigin = `${px(originXPx - context.originXPx)} ${px(originYPx - context.originYPx)}`;
440
- element.style.transform = `rotate(${degrees}deg)`;
440
+ const transforms = [`rotate(${degrees}deg)`];
441
+ if (scaleX === -1) transforms.push("scaleX(-1)");
442
+ if (scaleY === -1) transforms.push("scaleY(-1)");
443
+ element.style.transform = transforms.join(" ");
441
444
  context.parent.append(element);
442
445
  const inner = {
443
446
  ...context,
@@ -15,7 +15,7 @@ import { hasComplexScript, isComplexScriptCodePoint } from "../utils/scriptSegme
15
15
  * advances rightward from its origin whatever the paragraph does.
16
16
  */
17
17
  declare const glyphCellOffsetsPx: (run: DisplayGlyphRun) => readonly number[];
18
- declare const DISPLAY_PRIMITIVE_KINDS: ("image" | "glyphRun" | "rect" | "line" | "clipGroup" | "rotateGroup" | "opacityGroup")[];
18
+ declare const DISPLAY_PRIMITIVE_KINDS: ("glyphRun" | "rect" | "line" | "image" | "clipGroup" | "rotateGroup" | "opacityGroup")[];
19
19
  /**
20
20
  * Dash geometry of each stroke pattern, as multiples of the stroke thickness.
21
21
  *
@@ -309,16 +309,20 @@ type DisplayClipGroup = {
309
309
  readonly regions: readonly DisplayHitRegion[];
310
310
  };
311
311
  /**
312
- * Rotates its children about `originXPx`/`originYPx`. Rotated images,
313
- * watermark text and vertical (`w:textDirection`) table text are all this.
314
- * Rotation is the only transform folio paints; scale is resolved into
315
- * geometry by the producer.
312
+ * Rotates and optionally reflects its children about `originXPx`/`originYPx`.
313
+ * Rotated images, text boxes, watermark text and vertical (`w:textDirection`)
314
+ * table text are all this. A scale of `-1` is a DrawingML flip; the scale is
315
+ * applied before the rotation, matching CSS and OOXML transform order.
316
316
  */
317
317
  type DisplayRotateGroup = {
318
318
  readonly kind: "rotateGroup";
319
319
  readonly degrees: number;
320
320
  readonly originXPx: number;
321
321
  readonly originYPx: number;
322
+ /** Optional horizontal reflection about the transform origin. */
323
+ readonly scaleX?: -1;
324
+ /** Optional vertical reflection about the transform origin. */
325
+ readonly scaleY?: -1;
322
326
  readonly children: readonly DisplayPrimitive[];
323
327
  };
324
328
  /** Uniform alpha over a subtree, for watermark washout and image opacity. */