@stll/folio-core 0.28.0 → 0.29.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 (56) hide show
  1. package/dist/docx/styleParser.js +15 -14
  2. package/dist/layout-bridge/convert/toFlowBlocks.d.ts +1 -2
  3. package/dist/layout-bridge/convert/toFlowBlocks.js +135 -118
  4. package/dist/layout-engine/footnoteColumnReflow.d.ts +15 -0
  5. package/dist/layout-engine/footnoteColumnReflow.js +74 -0
  6. package/dist/layout-engine/index.d.ts +3 -5
  7. package/dist/layout-engine/index.js +36 -15
  8. package/dist/layout-engine/measure/cache.d.ts +1 -1
  9. package/dist/layout-engine/measure/cache.js +11 -2
  10. package/dist/layout-engine/measure/listMarkerWidth.d.ts +3 -1
  11. package/dist/layout-engine/measure/listMarkerWidth.js +6 -4
  12. package/dist/layout-engine/paginator.d.ts +6 -0
  13. package/dist/layout-engine/paginator.js +30 -11
  14. package/dist/layout-engine/types.d.ts +11 -6
  15. package/dist/markdown/renderParagraph.js +7 -2
  16. package/dist/prosemirror/attrs/index.js +5 -3
  17. package/dist/prosemirror/bookmarkBoundaryAttrs.d.ts +8 -0
  18. package/dist/prosemirror/bookmarkBoundaryAttrs.js +66 -0
  19. package/dist/prosemirror/commands/formatPainter.js +45 -1
  20. package/dist/prosemirror/conversion/fromProseDoc.d.ts +4 -1
  21. package/dist/prosemirror/conversion/fromProseDoc.js +224 -91
  22. package/dist/prosemirror/conversion/toProseDoc.d.ts +4 -1
  23. package/dist/prosemirror/conversion/toProseDoc.js +325 -94
  24. package/dist/prosemirror/extensions/StarterKit.js +11 -3
  25. package/dist/prosemirror/extensions/features/AutoBidiDetectionExtension.js +8 -4
  26. package/dist/prosemirror/extensions/features/PasteCleanupExtension.d.ts +4 -1
  27. package/dist/prosemirror/extensions/features/PasteCleanupExtension.js +9 -5
  28. package/dist/prosemirror/extensions/features/pasteCleanup.d.ts +10 -23
  29. package/dist/prosemirror/extensions/features/pasteCleanup.js +77 -22
  30. package/dist/prosemirror/extensions/marks/RunFormattingOverrideExtension.js +22 -22
  31. package/dist/prosemirror/extensions/nodes/BookmarkBoundaryExtension.d.ts +9 -0
  32. package/dist/prosemirror/extensions/nodes/BookmarkBoundaryExtension.js +67 -0
  33. package/dist/prosemirror/extensions/nodes/FieldExtension.d.ts +10 -4
  34. package/dist/prosemirror/extensions/nodes/FieldExtension.js +77 -59
  35. package/dist/prosemirror/extensions/nodes/TextBoxAnchorExtension.d.ts +4 -1
  36. package/dist/prosemirror/extensions/nodes/TextBoxAnchorExtension.js +4 -3
  37. package/dist/prosemirror/listMarker.d.ts +58 -0
  38. package/dist/prosemirror/listMarker.js +185 -0
  39. package/dist/prosemirror/numberedRefFields.d.ts +24 -0
  40. package/dist/prosemirror/numberedRefFields.js +276 -0
  41. package/dist/prosemirror/paraText.js +56 -17
  42. package/dist/prosemirror/plugins/anonymizationDecorations.js +1 -1
  43. package/dist/prosemirror/plugins/pmTextScan.d.ts +3 -1
  44. package/dist/prosemirror/plugins/pmTextScan.js +28 -7
  45. package/dist/prosemirror/plugins/templateDirectives.js +2 -2
  46. package/dist/prosemirror/schema/index.d.ts +2 -2
  47. package/dist/prosemirror/schema/marks.d.ts +3 -1
  48. package/dist/prosemirror/schema/nodes.d.ts +13 -1
  49. package/dist/prosemirror/styles/styleResolver.d.ts +0 -1
  50. package/dist/prosemirror/styles/styleResolver.js +22 -29
  51. package/dist/prosemirror/styles/styleToggleCascade.d.ts +37 -0
  52. package/dist/prosemirror/styles/styleToggleCascade.js +52 -0
  53. package/dist/prosemirror/validation.js +93 -0
  54. package/dist/utils/textFormattingMerge.d.ts +9 -1
  55. package/dist/utils/textFormattingMerge.js +32 -1
  56. package/package.json +1 -1
@@ -1,4 +1,5 @@
1
1
  import { emuToPixels } from "../utils/units.js";
2
+ import { reflowFootnoteColumns } from "./footnoteColumnReflow.js";
2
3
  import { resolveSectionHeaderFooterRefs } from "./headerFooterRefs.js";
3
4
  import { calculateChainHeight, computeKeepNextChains, getMidChainIndices, hasKeepLines, hasPageBreakBefore } from "./keep-together.js";
4
5
  import { measuredLineAdvance } from "./lineFlow.js";
@@ -140,13 +141,23 @@ function applyContextualSpacing(blocks) {
140
141
  /**
141
142
  * Layout a document: convert blocks + measures into pages with positioned fragments.
142
143
  *
143
- * Algorithm:
144
- * 1. Walk blocks in order with their corresponding measures
145
- * 2. For each block, create appropriate fragment(s)
146
- * 3. Use paginator to manage page/column state
147
- * 4. Handle page breaks, section breaks, and keepNext chains
144
+ * A reference discovered after the first column has already been placed can shrink the
145
+ * shared body band below earlier-column fragments. Retry those documents with the observed
146
+ * reservations as page floors so every column participates in the same footnote geometry.
148
147
  */
149
148
  function layoutDocument(blocks, measures, options) {
149
+ const layout = layoutDocumentPass(blocks, measures, options);
150
+ if (!options.footnoteHeightById) return layout;
151
+ return reflowFootnoteColumns({
152
+ initialLayout: layout,
153
+ ...options.footnoteReservedHeights ? { initialReserveFloors: options.footnoteReservedHeights } : {},
154
+ runLayout: (reserveFloors) => layoutDocumentPass(blocks, measures, {
155
+ ...options,
156
+ footnoteReservedHeights: reserveFloors
157
+ })
158
+ });
159
+ }
160
+ function layoutDocumentPass(blocks, measures, options) {
150
161
  if (blocks.length !== measures.length) panic(`layoutDocument: expected one measure per block (blocks=${blocks.length}, measures=${measures.length})`);
151
162
  const pageSize = options.pageSize;
152
163
  const baseMargins = {
@@ -333,6 +344,10 @@ function getLineFootnoteRefs(block, fromRun, toRun, fnHeights) {
333
344
  height
334
345
  };
335
346
  }
347
+ function projectedFootnoteReserveGrowth(state, additionalDemandHeight) {
348
+ const projectedDemand = state.footnoteDemandHeight + additionalDemandHeight;
349
+ return Math.max(state.footnoteHeightFloor, projectedDemand) - state.footnoteHeight;
350
+ }
336
351
  function layoutParagraph({ block, measure, paginator, contentWidth, footnoteHeightById, suppressSpaceBefore }) {
337
352
  const lines = measure.lines;
338
353
  if (lines.length === 0) {
@@ -370,7 +385,7 @@ function layoutParagraph({ block, measure, paginator, contentWidth, footnoteHeig
370
385
  if (currentLineIndex === 0 && state.trailingSpacing > 0 && state.cursorY !== state.topMargin) {
371
386
  const firstLine = lines[0];
372
387
  const firstLineRefs = getLineFootnoteRefs(block, firstLine.fromRun, firstLine.toRun, footnoteHeightById);
373
- const firstLineHeight = measuredLineAdvance(firstLine) + firstLineRefs.height;
388
+ const firstLineHeight = measuredLineAdvance(firstLine) + projectedFootnoteReserveGrowth(state, firstLineRefs.height);
374
389
  const collapsedLead = collapseParagraphSpacing({
375
390
  before: spaceBefore,
376
391
  after: state.trailingSpacing
@@ -399,7 +414,8 @@ function layoutParagraph({ block, measure, paginator, contentWidth, footnoteHeig
399
414
  }
400
415
  const lineRefs = getLineFootnoteRefs(block, line.fromRun, line.toRun, footnoteHeightById);
401
416
  const totalWithLine = linesHeight + lineAdvance;
402
- if (totalWithLine + firstFragmentSpaceBefore + linesFnHeight + lineRefs.height <= availableHeight || fittingLines === 0) {
417
+ const footnoteGrowth = projectedFootnoteReserveGrowth(state, linesFnHeight + lineRefs.height);
418
+ if (totalWithLine + firstFragmentSpaceBefore + footnoteGrowth <= availableHeight || fittingLines === 0) {
403
419
  linesHeight = totalWithLine;
404
420
  linesFnHeight += lineRefs.height;
405
421
  for (const id of lineRefs.ids) linesFnIds.push(id);
@@ -449,8 +465,10 @@ function layoutParagraph({ block, measure, paginator, contentWidth, footnoteHeig
449
465
  ...block.sdtGroups ? { sdtGroups: block.sdtGroups } : {}
450
466
  };
451
467
  if (linesFnHeight > 0) {
452
- paginator.ensureFits(effectiveSpaceBefore + linesHeight + linesFnHeight);
453
- if (paginator.getCurrentState().footnoteHeight === 0) paginator.ensureFits(effectiveSpaceBefore + linesHeight + linesFnHeight + 12);
468
+ const stateBefore = paginator.getCurrentState();
469
+ paginator.ensureFits(effectiveSpaceBefore + linesHeight + projectedFootnoteReserveGrowth(stateBefore, linesFnHeight));
470
+ const stateAfter = paginator.getCurrentState();
471
+ if (stateAfter.footnoteDemandHeight === 0) paginator.ensureFits(effectiveSpaceBefore + linesHeight + projectedFootnoteReserveGrowth(stateAfter, linesFnHeight + 12));
454
472
  }
455
473
  fragment.y = paginator.addFragment(fragment, linesHeight, effectiveSpaceBefore, effectiveSpaceAfter).y;
456
474
  if (linesFnHeight > 0) paginator.addFootnoteHeight(linesFnHeight, linesFnIds);
@@ -650,8 +668,9 @@ function layoutTable(block, measure, paginator, footnoteHeightById) {
650
668
  fragmentFootnoteIds.push(id);
651
669
  rowFootnoteHeight += footnoteHeightById?.get(id) ?? 0;
652
670
  }
653
- const separatorHeight = pageFootnoteIds.size === 0 && fragmentFootnoteHeight + rowFootnoteHeight > 0 ? 12 : 0;
654
- if (rowsHeight + rowHeight + normalHeaderOverhead + fragmentFootnoteHeight + rowFootnoteHeight + separatorHeight <= availableHeight) {
671
+ const candidateFootnoteHeight = fragmentFootnoteHeight + rowFootnoteHeight;
672
+ const footnoteGrowth = projectedFootnoteReserveGrowth(state, candidateFootnoteHeight + (state.footnoteDemandHeight === 0 && candidateFootnoteHeight > 0 ? 12 : 0));
673
+ if (rowsHeight + rowHeight + normalHeaderOverhead + footnoteGrowth <= availableHeight) {
655
674
  rowsHeight += rowHeight;
656
675
  fragmentFootnoteHeight += rowFootnoteHeight;
657
676
  fittingRows++;
@@ -780,7 +799,7 @@ function layoutFloatingTable(block, measure, paginator, contentWidth) {
780
799
  isFloating: true,
781
800
  ...block.sdtGroups ? { sdtGroups: block.sdtGroups } : {}
782
801
  };
783
- state.page.fragments.push(fragment);
802
+ paginator.addUnflowedFragment(fragment);
784
803
  }
785
804
  /**
786
805
  * Layout an image block onto pages.
@@ -825,7 +844,7 @@ function layoutAnchoredImage(block, measure, paginator) {
825
844
  isAnchored: true,
826
845
  zIndex: anchor.behindDoc ? -1 : 1
827
846
  };
828
- state.page.fragments.push(fragment);
847
+ paginator.addUnflowedFragment(fragment);
829
848
  }
830
849
  /**
831
850
  * Layout a text box block onto pages.
@@ -853,10 +872,11 @@ function layoutTextBox(block, measure, { paginator, sectionMarginTop, sectionPag
853
872
  y: state.topMargin + bandTop,
854
873
  width: measure.width,
855
874
  height: measure.height,
875
+ isPositioned: true,
856
876
  ...block.pmStart !== void 0 ? { pmStart: block.pmStart } : {},
857
877
  ...block.pmEnd !== void 0 ? { pmEnd: block.pmEnd } : {}
858
878
  };
859
- state.page.fragments.push(fragment);
879
+ paginator.addUnflowedFragment(fragment);
860
880
  return;
861
881
  }
862
882
  if (block.position !== void 0) {
@@ -882,10 +902,11 @@ function layoutTextBox(block, measure, { paginator, sectionMarginTop, sectionPag
882
902
  y,
883
903
  width: measure.width,
884
904
  height: measure.height,
905
+ isPositioned: true,
885
906
  ...block.pmStart !== void 0 ? { pmStart: block.pmStart } : {},
886
907
  ...block.pmEnd !== void 0 ? { pmEnd: block.pmEnd } : {}
887
908
  };
888
- state.page.fragments.push(fragment);
909
+ paginator.addUnflowedFragment(fragment);
889
910
  return;
890
911
  }
891
912
  const state = paginator.ensureFits(measure.height);
@@ -54,7 +54,7 @@ declare function setFontCacheSize(size: number): void;
54
54
  * Get current font metrics cache size
55
55
  */
56
56
  declare function getFontCacheSize(): number;
57
- /** Serialize the complete paragraph measurement contract into a cache key. */
57
+ /** Serialize the complete, semantically normalized paragraph measurement contract into a cache key. */
58
58
  declare function hashParagraphBlock(block: ParagraphBlock): string;
59
59
  /**
60
60
  * Get cached paragraph measurement or return undefined
@@ -188,11 +188,20 @@ const runMeasureCacheInput = (run) => {
188
188
  default: return run;
189
189
  }
190
190
  };
191
- /** Serialize the complete paragraph measurement contract into a cache key. */
191
+ const paragraphAttrsMeasureCacheInput = (attrs) => {
192
+ if (attrs === void 0) return void 0;
193
+ const { snapToGrid, ...measurementAttrs } = attrs;
194
+ if (measurementAttrs.documentGridLinePitch === void 0) return measurementAttrs;
195
+ return {
196
+ ...measurementAttrs,
197
+ snapToGrid: snapToGrid !== false
198
+ };
199
+ };
200
+ /** Serialize the complete, semantically normalized paragraph measurement contract into a cache key. */
192
201
  function hashParagraphBlock(block) {
193
202
  return JSON.stringify({
194
203
  lineBreakProviderGeneration: getLineBreakProviderGeneration(),
195
- attrs: block.attrs,
204
+ attrs: paragraphAttrsMeasureCacheInput(block.attrs),
196
205
  runs: block.runs.map(runMeasureCacheInput)
197
206
  });
198
207
  }
@@ -27,7 +27,9 @@ declare function resolveListMarkerFont(block: ParagraphBlock): {
27
27
  * - hanging indent — body wraps at `indentLeft`, marker sits at
28
28
  * `indentLeft - hanging`. Width is `hanging` so the marker fills the slot.
29
29
  * - `w:suff` (§17.9.25): `nothing` → natural width, `space` → natural +
30
- * one space glyph, `tab` (default) → grow to the next tab stop.
30
+ * one space glyph, `tab` (default) → grow to the next tab stop. A hanging
31
+ * indent remains the minimum footprint so body text cannot enter the
32
+ * marker slot.
31
33
  * - `w:tabs` on the paragraph: non-`clear`/non-`bar` stops past the marker.
32
34
  * `bar` (§17.3.1.37) is a vertical line and doesn't advance the cursor.
33
35
  * - default tab grid: stops at multiples of `DEFAULT_TAB_STOP_TWIPS`,
@@ -50,7 +50,9 @@ function resolveListMarkerFont(block) {
50
50
  * - hanging indent — body wraps at `indentLeft`, marker sits at
51
51
  * `indentLeft - hanging`. Width is `hanging` so the marker fills the slot.
52
52
  * - `w:suff` (§17.9.25): `nothing` → natural width, `space` → natural +
53
- * one space glyph, `tab` (default) → grow to the next tab stop.
53
+ * one space glyph, `tab` (default) → grow to the next tab stop. A hanging
54
+ * indent remains the minimum footprint so body text cannot enter the
55
+ * marker slot.
54
56
  * - `w:tabs` on the paragraph: non-`clear`/non-`bar` stops past the marker.
55
57
  * `bar` (§17.3.1.37) is a vertical line and doesn't advance the cursor.
56
58
  * - default tab grid: stops at multiples of `DEFAULT_TAB_STOP_TWIPS`,
@@ -67,12 +69,12 @@ function getListMarkerInlineWidth(block) {
67
69
  const naturalWidth = measureTextWidth(attrs.listMarker, style);
68
70
  const markerEndOffset = getMarkerEndOffset(naturalWidth, attrs.listMarkerAlignment);
69
71
  const suffix = attrs.listMarkerSuffix ?? "tab";
70
- if (suffix === "nothing") return markerEndOffset;
71
- if (suffix === "space") return markerEndOffset + measureTextWidth(" ", style);
72
+ const hanging = attrs.indent?.hanging ?? 0;
73
+ if (suffix === "nothing") return Math.max(hanging, markerEndOffset);
74
+ if (suffix === "space") return Math.max(hanging, markerEndOffset + measureTextWidth(" ", style));
72
75
  const indent = attrs.indent;
73
76
  const indentLeft = indent?.left ?? 0;
74
77
  const firstLine = indent?.firstLine ?? 0;
75
- const hanging = indent?.hanging ?? 0;
76
78
  const markerStartPx = hanging > 0 ? indentLeft - hanging : indentLeft + firstLine;
77
79
  const minBodyStart = markerStartPx + markerEndOffset;
78
80
  const customTabs = (attrs.tabs ?? []).filter((t) => t.val !== "clear" && t.val !== "bar").map((t) => t.pos * TWIPS_TO_PX);
@@ -22,6 +22,10 @@ type PageState = {
22
22
  rawContentBottom: number;
23
23
  /** Total height reserved for footnotes on this page (grows as refs are placed). */
24
24
  footnoteHeight: number;
25
+ /** Static reservation supplied by a layout retry. */
26
+ footnoteHeightFloor: number;
27
+ /** Footnote demand discovered while placing reference-bearing content. */
28
+ footnoteDemandHeight: number;
25
29
  /** Accumulated trailing spacing (space after previous block). */
26
30
  trailingSpacing: number;
27
31
  };
@@ -98,6 +102,8 @@ declare function createPaginator(options: PaginatorOptions): {
98
102
  x: number;
99
103
  y: number;
100
104
  };
105
+ /** Add an already-positioned fragment without advancing normal flow. */
106
+ addUnflowedFragment: (fragment: Fragment) => PageState;
101
107
  /** Reserve additional footnote area on the current page. */
102
108
  addFootnoteHeight: (additionalHeight: number, footnoteIds?: number[]) => void;
103
109
  /** Force a page break. */
@@ -99,8 +99,8 @@ function createPaginator(options) {
99
99
  const pageMargins = getPageMargins(pageNumber, logicalNumber);
100
100
  const topMargin = pageMargins.top;
101
101
  const contentBottom = pageSize.h - pageMargins.bottom;
102
- const footnoteHeight = options.footnoteReservedHeights?.get(pageNumber) ?? 0;
103
- const pageContentBottom = contentBottom - footnoteHeight;
102
+ const footnoteHeightFloor = options.footnoteReservedHeights?.get(pageNumber) ?? 0;
103
+ const pageContentBottom = contentBottom - footnoteHeightFloor;
104
104
  const page = {
105
105
  number: pageNumber,
106
106
  logicalNumber,
@@ -108,7 +108,7 @@ function createPaginator(options) {
108
108
  fragments: [],
109
109
  margins: pageMargins,
110
110
  size: { ...pageSize },
111
- ...footnoteHeight > 0 ? { footnoteReservedHeight: footnoteHeight } : {},
111
+ ...footnoteHeightFloor > 0 ? { footnoteReservedHeight: footnoteHeightFloor } : {},
112
112
  ...columns.count > 1 ? { columns: { ...columns } } : {}
113
113
  };
114
114
  applySectionMetadata(page);
@@ -119,7 +119,9 @@ function createPaginator(options) {
119
119
  topMargin,
120
120
  contentBottom: pageContentBottom,
121
121
  rawContentBottom: contentBottom,
122
- footnoteHeight,
122
+ footnoteHeight: footnoteHeightFloor,
123
+ footnoteHeightFloor,
124
+ footnoteDemandHeight: 0,
123
125
  trailingSpacing: 0
124
126
  };
125
127
  pages.push(page);
@@ -197,7 +199,7 @@ function createPaginator(options) {
197
199
  const y = state.cursorY + actualSpaceBefore;
198
200
  fragment.x = x;
199
201
  fragment.y = y;
200
- state.page.fragments.push(fragment);
202
+ commitFragment(state, fragment);
201
203
  state.cursorY = y + height;
202
204
  state.trailingSpacing = spaceAfter;
203
205
  return {
@@ -206,6 +208,18 @@ function createPaginator(options) {
206
208
  y
207
209
  };
208
210
  }
211
+ function commitFragment(state, fragment) {
212
+ if (sectionStartPending && state.page.sectionIndex !== currentSectionIndex) {
213
+ if (currentPageNumbering.type === "restart") nextLogicalPageNumber = currentPageNumbering.start + 1;
214
+ sectionStartPending = false;
215
+ }
216
+ state.page.fragments.push(fragment);
217
+ }
218
+ function addUnflowedFragment(fragment) {
219
+ const state = getCurrentState();
220
+ commitFragment(state, fragment);
221
+ return state;
222
+ }
209
223
  /**
210
224
  * Reserve additional footnote area on the current page.
211
225
  *
@@ -229,8 +243,9 @@ function createPaginator(options) {
229
243
  function addFootnoteHeight(additionalHeight, footnoteIds) {
230
244
  if (!Number.isFinite(additionalHeight) || additionalHeight <= 0) return;
231
245
  const state = getCurrentState();
232
- const separatorOverhead = state.footnoteHeight === 0 ? 12 : 0;
233
- state.footnoteHeight += additionalHeight + separatorOverhead;
246
+ const separatorOverhead = state.footnoteDemandHeight === 0 ? 12 : 0;
247
+ state.footnoteDemandHeight += additionalHeight + separatorOverhead;
248
+ state.footnoteHeight = Math.max(state.footnoteHeightFloor, state.footnoteDemandHeight);
234
249
  state.contentBottom = state.rawContentBottom - state.footnoteHeight;
235
250
  state.page.footnoteReservedHeight = state.footnoteHeight;
236
251
  if (footnoteIds && footnoteIds.length > 0) {
@@ -261,10 +276,10 @@ function createPaginator(options) {
261
276
  const pageMargins = getPageMargins(current.page.number, current.page.logicalNumber);
262
277
  const topMargin = pageMargins.top;
263
278
  const rawContentBottom = pageSize.h - pageMargins.bottom;
264
- const footnoteHeight = options.footnoteReservedHeights?.get(current.page.number) ?? 0;
279
+ const footnoteHeightFloor = options.footnoteReservedHeights?.get(current.page.number) ?? 0;
265
280
  current.page.size = { ...pageSize };
266
281
  current.page.margins = pageMargins;
267
- if (footnoteHeight > 0) current.page.footnoteReservedHeight = footnoteHeight;
282
+ if (footnoteHeightFloor > 0) current.page.footnoteReservedHeight = footnoteHeightFloor;
268
283
  else delete current.page.footnoteReservedHeight;
269
284
  if (columns.count > 1) current.page.columns = { ...columns };
270
285
  else delete current.page.columns;
@@ -272,8 +287,10 @@ function createPaginator(options) {
272
287
  current.cursorY = topMargin;
273
288
  current.columnIndex = 0;
274
289
  current.rawContentBottom = rawContentBottom;
275
- current.footnoteHeight = footnoteHeight;
276
- current.contentBottom = rawContentBottom - footnoteHeight;
290
+ current.footnoteHeight = footnoteHeightFloor;
291
+ current.footnoteHeightFloor = footnoteHeightFloor;
292
+ current.footnoteDemandHeight = 0;
293
+ current.contentBottom = rawContentBottom - footnoteHeightFloor;
277
294
  current.trailingSpacing = 0;
278
295
  columnRegionTop = topMargin;
279
296
  return true;
@@ -370,6 +387,8 @@ function createPaginator(options) {
370
387
  ensureFits,
371
388
  /** Add a fragment to current page. */
372
389
  addFragment,
390
+ /** Add an already-positioned fragment without advancing normal flow. */
391
+ addUnflowedFragment,
373
392
  /** Reserve additional footnote area on the current page. */
374
393
  addFootnoteHeight,
375
394
  /** Force a page break. */
@@ -1059,6 +1059,8 @@ type TextBoxFragment = FragmentBase & {
1059
1059
  kind: "textBox";
1060
1060
  /** Height of the text box. */
1061
1061
  height: number;
1062
+ /** True if this text box is positioned outside normal body flow. */
1063
+ isPositioned?: true;
1062
1064
  };
1063
1065
  /**
1064
1066
  * Union of all fragment types.
@@ -1226,18 +1228,21 @@ type LayoutOptions = {
1226
1228
  evenAndOddHeaders?: boolean;
1227
1229
  /** Swap left/right margins on even physical pages. */
1228
1230
  mirrorMargins?: boolean;
1229
- /** Per-page footnote reserved heights (pageNumber → height in pixels). */
1231
+ /**
1232
+ * Per-page footnote reserved heights (pageNumber → height in pixels).
1233
+ * When `footnoteHeightById` is also supplied, a value is a retry floor only
1234
+ * while that page has assigned footnote IDs; without the dynamic height map,
1235
+ * the value remains a static reservation.
1236
+ */
1230
1237
  footnoteReservedHeights?: Map<number, number>;
1231
1238
  /**
1232
1239
  * Footnote content heights keyed by internal footnote id (the OOXML
1233
1240
  * `<w:footnoteReference w:id>`). When provided, the layout engine
1234
1241
  * tracks footnote demand per body line: each line carrying a fn ref
1235
1242
  * grows its page's reservation by that fn's height before the next
1236
- * line is fitted. This single-pass approach avoids the static-
1237
- * reservation + iterative-convergence loop that produced oscillation
1238
- * (and either body-overflow into the footer or large empty gaps
1239
- * above the fn area) on documents with multiple long footnotes per
1240
- * page.
1243
+ * line is fitted. Multi-column pages may retry with the observed
1244
+ * reservation as a shared floor when a later column invalidates
1245
+ * earlier-column placement.
1241
1246
  */
1242
1247
  footnoteHeightById?: Map<number, number>;
1243
1248
  /** Header/footer references for each document section, by section index. */
@@ -1,4 +1,4 @@
1
- import { resolveListTemplate } from "../layout-bridge/convert/toFlowBlocks.js";
1
+ import { resolveListTemplate } from "../prosemirror/listMarker.js";
2
2
  import { isHeadingStyle, parseHeadingLevel } from "./headings.js";
3
3
  import { renderParagraphInline } from "./renderRuns.js";
4
4
  //#region src/markdown/renderParagraph.ts
@@ -58,7 +58,12 @@ function resolveTemplateMarker(ctx, list) {
58
58
  for (let i = level + 1; i < counters.length; i += 1) counters[i] = 0;
59
59
  ctx.listCounters.set(list.numId, counters);
60
60
  const levelFormats = list.levelNumFmts ?? (list.numFmt ? [list.numFmt] : void 0);
61
- return resolveListTemplate(list.marker, counters, levelFormats, list.isLegal).trim();
61
+ return resolveListTemplate({
62
+ template: list.marker,
63
+ counters,
64
+ levelFormats,
65
+ forceDecimal: list.isLegal
66
+ }).trim();
62
67
  }
63
68
  //#endregion
64
69
  export { renderParagraph };
@@ -77,11 +77,10 @@ const TEXT_BOX_TRACKED_CHANGE_TYPES = [
77
77
  "moveTo"
78
78
  ];
79
79
  const HARD_BREAK_TYPES = ["column"];
80
- const RUN_FORMATTING_OVERRIDE_FALSE_KEYS = [
80
+ const RUN_FORMATTING_OVERRIDE_BOOLEAN_KEYS = [
81
81
  "bold",
82
82
  "italic",
83
83
  "strike",
84
- "doubleStrike",
85
84
  "allCaps",
86
85
  "smallCaps",
87
86
  "hidden",
@@ -90,6 +89,7 @@ const RUN_FORMATTING_OVERRIDE_FALSE_KEYS = [
90
89
  "shadow",
91
90
  "outline"
92
91
  ];
92
+ const RUN_FORMATTING_OVERRIDE_FALSE_KEYS = ["doubleStrike", "rtl"];
93
93
  const SECTION_ORIENTATIONS = ["portrait", "landscape"];
94
94
  const SECTION_START_TYPES = [
95
95
  "continuous",
@@ -371,10 +371,11 @@ const expectImageAttrs = (node) => expectCachedNodeAttrs(node, imageAttrsCache,
371
371
  const readFieldAttrs = (node) => {
372
372
  const attrs = attrsRecord(node.attrs);
373
373
  const issues = [];
374
- expectNodeType(node, "field", issues);
374
+ expectNodeTypeOneOf(node, ["field", "structuredField"], issues);
375
375
  requiredOneOf(attrs, "fieldType", "field.attrs.fieldType", issues, FIELD_TYPE_VALUES);
376
376
  requiredString(attrs, "instruction", "field.attrs.instruction", issues);
377
377
  requiredString(attrs, "displayText", "field.attrs.displayText", issues);
378
+ optionalString(attrs, "_numberedRefBaseline", "field.attrs._numberedRefBaseline", issues);
378
379
  requiredOneOf(attrs, "fieldKind", "field.attrs.fieldKind", issues, FIELD_KINDS);
379
380
  optionalBoolean(attrs, "fldLock", "field.attrs.fldLock", issues);
380
381
  optionalBoolean(attrs, "dirty", "field.attrs.dirty", issues);
@@ -659,6 +660,7 @@ const readRunFormattingOverrideMarkAttrs = (mark) => {
659
660
  const attrs = attrsRecord(mark.attrs);
660
661
  const issues = [];
661
662
  expectMarkType(mark, "runFormattingOverride", issues);
663
+ for (const key of RUN_FORMATTING_OVERRIDE_BOOLEAN_KEYS) optionalBoolean(attrs, key, `runFormattingOverride.attrs.${key}`, issues);
662
664
  for (const key of RUN_FORMATTING_OVERRIDE_FALSE_KEYS) optionalFalse(attrs, key, `runFormattingOverride.attrs.${key}`, issues);
663
665
  optionalBoolean(attrs, "boldCs", "runFormattingOverride.attrs.boldCs", issues);
664
666
  optionalBoolean(attrs, "italicCs", "runFormattingOverride.attrs.italicCs", issues);
@@ -0,0 +1,8 @@
1
+ import { BookmarkBoundaryAttrs } from "./schema/nodes.js";
2
+ import { ReadProseMirrorAttrsResult } from "./attrs/index.js";
3
+ import { Node } from "prosemirror-model";
4
+ //#region src/prosemirror/bookmarkBoundaryAttrs.d.ts
5
+ declare const readBookmarkBoundaryAttrs: (node: Node) => ReadProseMirrorAttrsResult<BookmarkBoundaryAttrs>;
6
+ declare const expectBookmarkBoundaryAttrs: (node: Node) => BookmarkBoundaryAttrs;
7
+ //#endregion
8
+ export { expectBookmarkBoundaryAttrs, readBookmarkBoundaryAttrs };
@@ -0,0 +1,66 @@
1
+ import { panic } from "better-result";
2
+ //#region src/prosemirror/bookmarkBoundaryAttrs.ts
3
+ const attrsCache = /* @__PURE__ */ new WeakMap();
4
+ const readBookmarkBoundaryAttrs = (node) => {
5
+ const issues = [];
6
+ if (node.type.name !== "bookmarkBoundary") issues.push({
7
+ path: "bookmarkBoundary.type.name",
8
+ message: `Expected bookmarkBoundary, got ${node.type.name}.`
9
+ });
10
+ const type = node.attrs["type"];
11
+ if (type !== "start" && type !== "end") issues.push({
12
+ path: "bookmarkBoundary.attrs.type",
13
+ message: "Expected \"start\" or \"end\"."
14
+ });
15
+ const id = node.attrs["id"];
16
+ if (typeof id !== "number" || !Number.isInteger(id) || id < 0) issues.push({
17
+ path: "bookmarkBoundary.attrs.id",
18
+ message: "Expected a non-negative integer."
19
+ });
20
+ const name = node.attrs["name"];
21
+ if (type === "start" && (typeof name !== "string" || name.length === 0)) issues.push({
22
+ path: "bookmarkBoundary.attrs.name",
23
+ message: "Expected a non-empty string for a bookmark start."
24
+ });
25
+ const colFirst = node.attrs["colFirst"];
26
+ if (colFirst !== void 0 && colFirst !== null && (typeof colFirst !== "number" || !Number.isInteger(colFirst) || colFirst < 0)) issues.push({
27
+ path: "bookmarkBoundary.attrs.colFirst",
28
+ message: "Expected a non-negative integer."
29
+ });
30
+ const colLast = node.attrs["colLast"];
31
+ if (colLast !== void 0 && colLast !== null && (typeof colLast !== "number" || !Number.isInteger(colLast) || colLast < 0)) issues.push({
32
+ path: "bookmarkBoundary.attrs.colLast",
33
+ message: "Expected a non-negative integer."
34
+ });
35
+ if (issues.length > 0 || typeof id !== "number") return {
36
+ ok: false,
37
+ issues
38
+ };
39
+ if (type === "start" && typeof name === "string") return {
40
+ ok: true,
41
+ value: {
42
+ type,
43
+ id,
44
+ name,
45
+ ...typeof colFirst === "number" ? { colFirst } : {},
46
+ ...typeof colLast === "number" ? { colLast } : {}
47
+ }
48
+ };
49
+ return {
50
+ ok: true,
51
+ value: {
52
+ type: "end",
53
+ id
54
+ }
55
+ };
56
+ };
57
+ const expectBookmarkBoundaryAttrs = (node) => {
58
+ const cached = attrsCache.get(node);
59
+ if (cached) return cached;
60
+ const result = readBookmarkBoundaryAttrs(node);
61
+ if (!result.ok) panic(`Invalid ProseMirror bookmark boundary attrs:\n${result.issues.map((issue) => `${issue.path}: ${issue.message}`).join("\n")}`);
62
+ attrsCache.set(node, result.value);
63
+ return result.value;
64
+ };
65
+ //#endregion
66
+ export { expectBookmarkBoundaryAttrs, readBookmarkBoundaryAttrs };
@@ -1,3 +1,4 @@
1
+ import { expectCharacterStyleMarkAttrs, expectParagraphAttrs } from "../attrs/index.js";
1
2
  //#region src/prosemirror/commands/formatPainter.ts
2
3
  /**
3
4
  * Character-formatting marks the painter copies. Structural marks — comments,
@@ -64,6 +65,40 @@ function sanitizeOverrideMark(mark) {
64
65
  if (Object.keys(kept).length === 0) return null;
65
66
  return mark.type.create(kept);
66
67
  }
68
+ const PAINTABLE_STYLE_TOGGLE_MARKS = [
69
+ ["allCaps", "allCaps"],
70
+ ["bold", "bold"],
71
+ ["emboss", "emboss"],
72
+ ["imprint", "imprint"],
73
+ ["italic", "italic"],
74
+ ["outline", "textOutline"],
75
+ ["shadow", "textShadow"],
76
+ ["smallCaps", "smallCaps"],
77
+ ["strike", "strike"]
78
+ ];
79
+ const PAINTABLE_COMPLEX_SCRIPT_TOGGLES = ["boldCs", "italicCs"];
80
+ function materializeStyleToggleNegatives({ inheritedFormatting, marks }) {
81
+ const characterStyle = marks.find((mark) => mark.type.name === "characterStyle");
82
+ if (!characterStyle) return [...marks];
83
+ const styleRPr = expectCharacterStyleMarkAttrs(characterStyle)._styleRPr;
84
+ if (!styleRPr) return [...marks];
85
+ const negativeAttrs = {};
86
+ for (const [key, markName] of PAINTABLE_STYLE_TOGGLE_MARKS) if (styleRPr[key] === true && !marks.some((mark) => mark.type.name === markName)) negativeAttrs[key] = false;
87
+ for (const key of PAINTABLE_COMPLEX_SCRIPT_TOGGLES) {
88
+ if (styleRPr[key] === void 0) continue;
89
+ const inheritedValue = inheritedFormatting?.[key] ?? false;
90
+ if (!(styleRPr[key] === true ? !inheritedValue : false)) negativeAttrs[key] = false;
91
+ }
92
+ if (Object.keys(negativeAttrs).length === 0) return [...marks];
93
+ const existingOverride = marks.find((mark) => mark.type.name === "runFormattingOverride");
94
+ const overrideType = existingOverride?.type ?? characterStyle.type.schema.marks["runFormattingOverride"];
95
+ if (!overrideType) return [...marks];
96
+ const override = overrideType.create({
97
+ ...negativeAttrs,
98
+ ...existingOverride?.attrs
99
+ });
100
+ return existingOverride ? marks.map((mark) => mark === existingOverride ? override : mark) : [...marks, override];
101
+ }
67
102
  /**
68
103
  * Marks of the first text node inside [from, to). A word processor's format
69
104
  * brush copies from the start of the source selection, so a mixed selection
@@ -81,6 +116,12 @@ function firstTextMarks(state, from, to) {
81
116
  });
82
117
  return marks ?? [];
83
118
  }
119
+ function paragraphDefaultTextFormatting(position) {
120
+ for (let depth = position.depth; depth >= 0; depth -= 1) {
121
+ const ancestor = position.node(depth);
122
+ if (ancestor.type.name === "paragraph") return expectParagraphAttrs(ancestor).defaultTextFormatting;
123
+ }
124
+ }
84
125
  /**
85
126
  * Capture the paintable character marks active over the current selection.
86
127
  * Returns the marks (with their attrs) to hand to `applyFormatMarks`. An empty
@@ -100,7 +141,10 @@ function captureFormatMarks(state) {
100
141
  }
101
142
  captured.push(mark);
102
143
  }
103
- return captured;
144
+ return materializeStyleToggleNegatives({
145
+ inheritedFormatting: paragraphDefaultTextFormatting($from),
146
+ marks: captured
147
+ });
104
148
  }
105
149
  /**
106
150
  * Apply captured marks onto the current selection's range: clear every paintable
@@ -8,7 +8,10 @@ declare function fromProseDoc(pmDoc: Node, baseDocument?: document_d_exports.Doc
8
8
  /**
9
9
  * Convert ProseMirror marks to TextFormatting
10
10
  */
11
- declare function marksToTextFormatting(marks: readonly Mark[]): document_d_exports.TextFormatting;
11
+ type MarksToTextFormattingOptions = {
12
+ inheritedFormatting: document_d_exports.TextFormatting | undefined;
13
+ };
14
+ declare function marksToTextFormatting(marks: readonly Mark[], options?: MarksToTextFormattingOptions): document_d_exports.TextFormatting;
12
15
  declare const standaloneTableCellFromProseMirror: (node: Node) => document_d_exports.TableCell;
13
16
  /**
14
17
  * Update a Document with content from a ProseMirror document