@stll/folio-core 0.49.0 → 0.51.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 (107) hide show
  1. package/dist/ai-edits/headless.d.ts +37 -1
  2. package/dist/ai-edits/headless.js +47 -16
  3. package/dist/ai-edits/word-diff.js +134 -1
  4. package/dist/compare/content-alignment.js +220 -5
  5. package/dist/compare/content.d.ts +1 -0
  6. package/dist/compare/content.js +51 -20
  7. package/dist/display-list/dom/renderDisplayListToDom.d.ts +10 -1
  8. package/dist/display-list/dom/renderDisplayListToDom.js +8 -7
  9. package/dist/display-list/html/renderDisplayListToHtml.d.ts +21 -0
  10. package/dist/display-list/html/renderDisplayListToHtml.js +126 -0
  11. package/dist/display-list/selectDisplayPages.d.ts +6 -0
  12. package/dist/display-list/selectDisplayPages.js +44 -0
  13. package/dist/docx/selectiveSave.d.ts +1 -1
  14. package/dist/docx/selectiveSave.js +16 -6
  15. package/dist/docx/structuralXmlPatch.d.ts +14 -0
  16. package/dist/docx/structuralXmlPatch.js +209 -0
  17. package/dist/export-pdf.d.ts +35 -2
  18. package/dist/export-pdf.js +51 -20
  19. package/dist/fonts/fontsourceFaces.d.ts +47 -0
  20. package/dist/fonts/fontsourceFaces.js +230 -0
  21. package/dist/layout-bridge/convert/flowBlockPostprocessing.d.ts +8 -0
  22. package/dist/layout-bridge/convert/flowBlockPostprocessing.js +106 -0
  23. package/dist/layout-bridge/convert/flowBorders.d.ts +17 -0
  24. package/dist/layout-bridge/convert/flowBorders.js +31 -0
  25. package/dist/layout-bridge/convert/flowConversionShared.d.ts +111 -0
  26. package/dist/layout-bridge/convert/flowConversionShared.js +23 -0
  27. package/dist/layout-bridge/convert/footnoteLayout.d.ts +2 -1
  28. package/dist/layout-bridge/convert/footnoteLayout.js +2 -1
  29. package/dist/layout-bridge/convert/headerFooterLayout.d.ts +2 -1
  30. package/dist/layout-bridge/convert/imageConversion.d.ts +33 -0
  31. package/dist/layout-bridge/convert/imageConversion.js +118 -0
  32. package/dist/layout-bridge/convert/listMarkers.d.ts +28 -0
  33. package/dist/layout-bridge/convert/listMarkers.js +98 -0
  34. package/dist/layout-bridge/convert/noteReferences.d.ts +14 -0
  35. package/dist/layout-bridge/convert/noteReferences.js +29 -0
  36. package/dist/layout-bridge/convert/pageBreakSplitting.d.ts +16 -0
  37. package/dist/layout-bridge/convert/pageBreakSplitting.js +140 -0
  38. package/dist/layout-bridge/convert/paragraphAttrs.d.ts +20 -0
  39. package/dist/layout-bridge/convert/paragraphAttrs.js +234 -0
  40. package/dist/layout-bridge/convert/paragraphConversion.d.ts +12 -0
  41. package/dist/layout-bridge/convert/paragraphConversion.js +112 -0
  42. package/dist/layout-bridge/convert/paragraphRuns.d.ts +15 -0
  43. package/dist/layout-bridge/convert/paragraphRuns.js +281 -0
  44. package/dist/layout-bridge/convert/runFormattingMerge.d.ts +19 -0
  45. package/dist/layout-bridge/convert/runFormattingMerge.js +167 -0
  46. package/dist/layout-bridge/convert/runMarkFormatting.d.ts +11 -0
  47. package/dist/layout-bridge/convert/runMarkFormatting.js +291 -0
  48. package/dist/layout-bridge/convert/sectionBoundaries.d.ts +35 -0
  49. package/dist/layout-bridge/convert/sectionBoundaries.js +114 -0
  50. package/dist/layout-bridge/convert/tableConversion.d.ts +12 -0
  51. package/dist/layout-bridge/convert/tableConversion.js +303 -0
  52. package/dist/layout-bridge/convert/textBoxFill.d.ts +20 -0
  53. package/dist/layout-bridge/convert/textBoxFill.js +42 -0
  54. package/dist/layout-bridge/convert/textFormattingConversion.d.ts +22 -0
  55. package/dist/layout-bridge/convert/textFormattingConversion.js +120 -0
  56. package/dist/layout-bridge/convert/toFlowBlocks.d.ts +5 -113
  57. package/dist/layout-bridge/convert/toFlowBlocks.js +11 -2082
  58. package/dist/layout-bridge/engine/hitTest.js +1 -1
  59. package/dist/layout-bridge/engine/measuring/index.d.ts +2 -1
  60. package/dist/layout-bridge/engine/measuring/index.js +2 -1
  61. package/dist/layout-bridge/engine/selectionRects.js +1 -1
  62. package/dist/layout-engine/imageLayout.d.ts +9 -0
  63. package/dist/layout-engine/imageLayout.js +48 -0
  64. package/dist/layout-engine/index.d.ts +3 -18
  65. package/dist/layout-engine/index.js +12 -937
  66. package/dist/layout-engine/layoutFlowShared.d.ts +6 -0
  67. package/dist/layout-engine/layoutFlowShared.js +8 -0
  68. package/dist/layout-engine/measure/crossRunWords.d.ts +45 -0
  69. package/dist/layout-engine/measure/crossRunWords.js +172 -0
  70. package/dist/layout-engine/measure/floatingLineClearance.d.ts +24 -0
  71. package/dist/layout-engine/measure/floatingLineClearance.js +44 -0
  72. package/dist/layout-engine/measure/inlineObjectMetrics.d.ts +6 -0
  73. package/dist/layout-engine/measure/inlineObjectMetrics.js +52 -0
  74. package/dist/layout-engine/measure/justification.d.ts +32 -0
  75. package/dist/layout-engine/measure/justification.js +83 -0
  76. package/dist/layout-engine/measure/lineFitting.d.ts +48 -0
  77. package/dist/layout-engine/measure/lineFitting.js +145 -0
  78. package/dist/layout-engine/measure/lineTypography.d.ts +44 -0
  79. package/dist/layout-engine/measure/lineTypography.js +124 -0
  80. package/dist/layout-engine/measure/measureBlocks.js +2 -1
  81. package/dist/layout-engine/measure/measureParagraph.d.ts +1 -20
  82. package/dist/layout-engine/measure/measureParagraph.js +32 -734
  83. package/dist/layout-engine/measure/paragraphMeasureShared.d.ts +109 -0
  84. package/dist/layout-engine/measure/paragraphMeasureShared.js +82 -0
  85. package/dist/layout-engine/measure/paragraphTabs.d.ts +18 -0
  86. package/dist/layout-engine/measure/paragraphTabs.js +78 -0
  87. package/dist/layout-engine/paragraphLayout.d.ts +23 -0
  88. package/dist/layout-engine/paragraphLayout.js +204 -0
  89. package/dist/layout-engine/sectionLayout.d.ts +45 -0
  90. package/dist/layout-engine/sectionLayout.js +216 -0
  91. package/dist/layout-engine/tableLayout.d.ts +17 -0
  92. package/dist/layout-engine/tableLayout.js +369 -0
  93. package/dist/layout-engine/textBoxLayout.d.ts +15 -0
  94. package/dist/layout-engine/textBoxLayout.js +137 -0
  95. package/dist/layout-painter/renderPage.js +1 -0
  96. package/dist/managers/editorShortcuts.d.ts +16 -3
  97. package/dist/managers/editorShortcuts.js +6 -4
  98. package/dist/paged-layout/sectionBlockWidths.d.ts +2 -1
  99. package/dist/paged-layout/sectionBlockWidths.js +1 -1
  100. package/dist/prosemirror/extensions/StarterKit.d.ts +3 -0
  101. package/dist/prosemirror/extensions/StarterKit.js +2 -1
  102. package/dist/prosemirror/extensions/core/HistoryExtension.d.ts +9 -1
  103. package/dist/prosemirror/extensions/core/HistoryExtension.js +4 -3
  104. package/dist/prosemirror/extensions/features/ParagraphChangeTrackerExtension.d.ts +12 -5
  105. package/dist/prosemirror/extensions/features/ParagraphChangeTrackerExtension.js +91 -33
  106. package/dist/server.d.ts +3 -2
  107. package/package.json +2 -2
@@ -1,20 +1,21 @@
1
- import { emuToPixels } from "../utils/units.js";
2
1
  import { reflowFootnoteColumns } from "./footnoteColumnReflow.js";
3
2
  import { resolveSectionHeaderFooterRefs } from "./headerFooterRefs.js";
3
+ import { layoutImage } from "./imageLayout.js";
4
4
  import { calculateChainHeight, computeKeepNextChains, getMidChainIndices, hasKeepLines, hasPageBreakBefore } from "./keep-together.js";
5
- import { measuredLineAdvance, measuredLineRangeHeight } from "./lineFlow.js";
6
- import { resolveFloatingTablePageX } from "./measure/floatingTablePosition.js";
7
- import { resolveTableInlineOffset } from "./measure/tableInlinePlacement.js";
5
+ import "./layoutFlowShared.js";
6
+ import { measuredLineAdvance } from "./lineFlow.js";
8
7
  import { annotateNoteSeparators, continuesNoteArea, createNoteAreaContinuation } from "./noteAreaFlow.js";
9
- import { SECTION_START_PLACEMENT, createPaginator } from "./paginator.js";
10
- import { getParagraphFragmentPmRange } from "./paragraphFragmentRange.js";
11
- import { collapseParagraphSpacing, getParagraphSpacingAfter, getParagraphSpacingBefore, isEmptyParagraph, paragraphsShareStyle, resolveEffectiveParagraphSpacingTree } from "./paragraphSpacing.js";
8
+ import { createPaginator } from "./paginator.js";
9
+ import { layoutParagraph } from "./paragraphLayout.js";
10
+ import { paragraphsShareStyle, resolveEffectiveParagraphSpacingTree } from "./paragraphSpacing.js";
12
11
  import { assertExhaustiveFlowBlock, findPageIndexContainingPmPos } from "./pmPageIndex.js";
13
12
  import { INITIAL_RENDERED_BREAK_STATE, PAGE_ADVANCE, reconcileAfterBlock, reconcileBreakBeforeBlock, recordReflowBoundary } from "./renderedBreakReconciliation.js";
14
- import { applyPendingToActive, createInitialSectionState, getEffectiveColumns, getEffectiveMargins, getEffectivePageSize, normalizeSectionBreakType, physicalColumnRegionIsShared, scheduleSectionBreak } from "./section-breaks.js";
13
+ import { applyPendingToActive, createInitialSectionState, getEffectiveColumns, getEffectiveMargins, getEffectivePageSize, normalizeSectionBreakType, scheduleSectionBreak } from "./section-breaks.js";
14
+ import { CONTINUE_PAGE_NUMBERING, DEFAULT_COLUMNS, balancedParagraphSectionHeight, collectSectionConfigs, handleSectionBreak } from "./sectionLayout.js";
15
15
  import { applySectionVerticalAlignment } from "./sectionVerticalAlignment.js";
16
- import { buildTableRowBreakInfo, getRowContinuationSkip, snapRowBreak, tableKeepNextOpeningHeight } from "./tableRowBreak.js";
17
- import { bandFragmentX, bandTopContentY, isPageFrameRelativeAnchor } from "./textBoxFlow.js";
16
+ import { getHeaderRowsHeight, layoutFloatingTable, layoutTable } from "./tableLayout.js";
17
+ import { tableKeepNextOpeningHeight } from "./tableRowBreak.js";
18
+ import { layoutTextBox } from "./textBoxLayout.js";
18
19
  import { DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, NOTE_SEPARATOR_RULE_THICKNESS, NOTE_SEPARATOR_WIDTH_FRACTION, floatingTextBoxReservesBand, floatingTextBoxWrapsText, getTableRowLeadingWidth, isFloatingImageRun, isFloatingTextBoxBlock, isListNumPr, isTextWrappingFloatingImageRun, resolveTableCellPadding, resolveTableWidthPx, stripFlowBlockPmAnchors, tableColumnsArePinned } from "./types.js";
19
20
  import { panic } from "better-result";
20
21
  //#region src/layout-engine/index.ts
@@ -23,92 +24,6 @@ import { panic } from "better-result";
23
24
  *
24
25
  * Converts blocks + measures into positioned fragments on pages.
25
26
  */
26
- const RENDERED_BREAK_REFLOW_TOLERANCE_LINES = 3;
27
- const DEFAULT_COLUMNS = {
28
- count: 1,
29
- gap: 0
30
- };
31
- const CONTINUE_PAGE_NUMBERING = { type: "continue" };
32
- function collectSectionConfigs(blocks, initialConfig, finalConfig) {
33
- const configs = [];
34
- const breakIndices = [];
35
- let previousConfig = initialConfig;
36
- for (let index = 0; index < blocks.length; index += 1) {
37
- const block = blocks[index];
38
- if (block?.kind !== "sectionBreak") continue;
39
- const sectionBreak = block;
40
- const config = {
41
- pageSize: sectionBreak.pageSize ?? previousConfig.pageSize,
42
- margins: sectionBreak.margins ?? previousConfig.margins,
43
- pageNumbering: sectionBreak.pageNumbering ?? CONTINUE_PAGE_NUMBERING
44
- };
45
- if (sectionBreak.columns !== void 0) config.columns = sectionBreak.columns;
46
- configs.push(config);
47
- breakIndices.push(index);
48
- previousConfig = configs.at(-1) ?? previousConfig;
49
- }
50
- configs.push(finalConfig);
51
- return {
52
- configs,
53
- breakIndices
54
- };
55
- }
56
- function balancedParagraphSectionHeight({ blocks, measures, startIndex, endIndex, incomingSpacing, columnCount, availableHeight }) {
57
- if (columnCount <= 1 || startIndex >= endIndex || availableHeight <= 0) return;
58
- const lineUnits = [];
59
- let totalHeight = 0;
60
- let tallestUnit = 0;
61
- let trailingSpacing = incomingSpacing;
62
- for (let index = startIndex; index < endIndex; index++) {
63
- const block = blocks[index];
64
- const measure = measures[index];
65
- if (block?.kind !== "paragraph" || measure?.kind !== "paragraph") return;
66
- if (block.attrs?.keepNext === true || block.attrs?.keepLines === true || block.runs.some((run) => run.kind === "text" && run.footnoteRefId !== void 0)) return;
67
- const leadingSpacing = collapseParagraphSpacing({
68
- before: getParagraphSpacingBefore(block),
69
- after: trailingSpacing
70
- });
71
- if (measure.lines.length === 0) {
72
- if (leadingSpacing > 0) {
73
- lineUnits.push(leadingSpacing);
74
- totalHeight += leadingSpacing;
75
- tallestUnit = Math.max(tallestUnit, leadingSpacing);
76
- }
77
- }
78
- for (let lineIndex = 0; lineIndex < measure.lines.length; lineIndex++) {
79
- const line = measure.lines[lineIndex];
80
- if (!line) continue;
81
- const unitHeight = measuredLineAdvance(line) + (lineIndex === 0 ? leadingSpacing : 0);
82
- lineUnits.push(unitHeight);
83
- totalHeight += unitHeight;
84
- tallestUnit = Math.max(tallestUnit, unitHeight);
85
- }
86
- if (tallestUnit > availableHeight || totalHeight > availableHeight * columnCount) return;
87
- trailingSpacing = getParagraphSpacingAfter(block);
88
- }
89
- if (totalHeight <= 0) return;
90
- const columnsNeeded = (targetHeight) => {
91
- let usedHeight = 0;
92
- let usedColumns = 1;
93
- for (const unitHeight of lineUnits) if (usedHeight > 0 && usedHeight + unitHeight > targetHeight) {
94
- usedColumns += 1;
95
- usedHeight = unitHeight;
96
- } else usedHeight += unitHeight;
97
- return usedColumns;
98
- };
99
- let lower = Math.max(tallestUnit, totalHeight / columnCount);
100
- let upper = Math.min(totalHeight, availableHeight);
101
- if (columnsNeeded(upper) > columnCount) return;
102
- for (let iteration = 0; iteration < 32; iteration++) {
103
- const middle = (lower + upper) / 2;
104
- if (columnsNeeded(middle) <= columnCount) upper = middle;
105
- else lower = middle;
106
- }
107
- return Math.ceil(upper * 1e3) / 1e3;
108
- }
109
- function hasWidowControl(block) {
110
- return block.attrs?.widowControl !== false;
111
- }
112
27
  /**
113
28
  * Apply contextual spacing suppression (OOXML §17.3.1.9).
114
29
  *
@@ -217,7 +132,7 @@ function layoutDocumentPass(blocks, measures, options) {
217
132
  if (noteAreaContinuation !== void 0) paginator.setPageTopContinuation(continuesNoteArea(options.noteAreas, block) ? noteAreaContinuation : void 0);
218
133
  const firstLine = measure.kind === "paragraph" ? measure.lines.at(0) : void 0;
219
134
  const firstLineAdvance = firstLine ? measuredLineAdvance(firstLine) : 0;
220
- const renderedBreakNeedsSnap = measure.kind === "paragraph" && (!paginator.fits(measure.totalHeight) || firstLineAdvance > 0 && paginator.getAvailableHeight() <= firstLineAdvance * RENDERED_BREAK_REFLOW_TOLERANCE_LINES);
135
+ const renderedBreakNeedsSnap = measure.kind === "paragraph" && (!paginator.fits(measure.totalHeight) || firstLineAdvance > 0 && paginator.getAvailableHeight() <= firstLineAdvance * 3);
221
136
  const hasExplicitPageBreak = block.kind === "pageBreak" || hasPageBreakBefore(block);
222
137
  const breakDecision = reconcileBreakBeforeBlock({
223
138
  state: renderedBreakState,
@@ -319,845 +234,5 @@ function layoutDocumentPass(blocks, measures, options) {
319
234
  ...options.pageGap !== void 0 ? { pageGap: options.pageGap } : {}
320
235
  };
321
236
  }
322
- /**
323
- * Footnote refs whose run sits in line `[fromRun..toRun]`, with their
324
- * pre-measured content heights. Empty when the engine isn't tracking
325
- * dynamic fn demand. Used by `layoutParagraph` to (a) reserve fn
326
- * height per line and (b) record the IDs on the host page so the
327
- * painter renders the fn on the page where the ref-bearing line
328
- * actually landed — even when the paragraph splits across pages
329
- * (fragment pmStart/pmEnd is paragraph-wide and cannot disambiguate
330
- * between split halves).
331
- */
332
- function getLineFootnoteRefs(block, fromRun, toRun, fnHeights) {
333
- if (!fnHeights) return {
334
- ids: [],
335
- height: 0
336
- };
337
- const ids = [];
338
- let height = 0;
339
- for (let r = fromRun; r <= toRun; r++) {
340
- const run = block.runs[r];
341
- if (!run || run.kind !== "text") continue;
342
- const id = run.footnoteRefId;
343
- if (id === void 0) continue;
344
- const h = fnHeights.get(id);
345
- if (h !== void 0) {
346
- ids.push(id);
347
- height += h;
348
- }
349
- }
350
- return {
351
- ids,
352
- height
353
- };
354
- }
355
- function projectedFootnoteReserveGrowth(state, additionalDemandHeight) {
356
- const projectedDemand = state.footnoteDemandHeight + additionalDemandHeight;
357
- return Math.max(state.footnoteHeightFloor, projectedDemand) - state.footnoteHeight;
358
- }
359
- function layoutParagraph({ block, measure, paginator, contentWidth, footnoteHeightById, suppressSpaceBefore }) {
360
- const lines = measure.lines;
361
- if (lines.length === 0) {
362
- const spaceBefore = suppressSpaceBefore ? 0 : getParagraphSpacingBefore(block);
363
- const spaceAfter = getParagraphSpacingAfter(block);
364
- const state = paginator.getCurrentState();
365
- const fragment = {
366
- kind: "paragraph",
367
- ...block.attrs?.suppressEmptyParagraphHeight === true ? { paginationRole: "empty-carrier" } : {},
368
- blockId: block.id,
369
- x: paginator.getColumnX(state.columnIndex),
370
- y: state.cursorY + spaceBefore,
371
- width: contentWidth,
372
- height: 0,
373
- fromLine: 0,
374
- toLine: 0,
375
- ...block.pmStart !== void 0 ? { pmStart: block.pmStart } : {},
376
- ...block.pmEnd !== void 0 ? { pmEnd: block.pmEnd } : {},
377
- ...block.sdtGroups ? { sdtGroups: block.sdtGroups } : {}
378
- };
379
- paginator.addFragment(fragment, 0, spaceBefore, spaceAfter);
380
- return;
381
- }
382
- const spaceBefore = suppressSpaceBefore ? 0 : getParagraphSpacingBefore(block);
383
- const spaceAfter = getParagraphSpacingAfter(block);
384
- let keepLinesDemand;
385
- if (lines.length > 1 && hasKeepLines(block)) {
386
- let footnoteHeight = 0;
387
- for (const line of lines) footnoteHeight += getLineFootnoteRefs(block, line.fromRun, line.toRun, footnoteHeightById).height;
388
- keepLinesDemand = {
389
- linesHeight: measuredLineRangeHeight(lines, 0, lines.length),
390
- footnoteHeight
391
- };
392
- }
393
- let keepLinesMoveAttempted = false;
394
- let currentLineIndex = 0;
395
- while (currentLineIndex < lines.length) {
396
- const state = paginator.getCurrentState();
397
- const availableHeight = paginator.getAvailableHeight();
398
- const currentLine = lines.at(currentLineIndex);
399
- const markerAlreadySatisfied = state.columnIndex === 0 && state.cursorY === state.topMargin && state.page.fragments.length === 0;
400
- if (currentLine?.renderedPageBreakBefore === true && !markerAlreadySatisfied && availableHeight <= measuredLineAdvance(currentLine) * RENDERED_BREAK_REFLOW_TOLERANCE_LINES) {
401
- paginator.forcePageBreak();
402
- continue;
403
- }
404
- if (currentLineIndex === 0 && state.trailingSpacing > 0 && state.cursorY !== state.topMargin) {
405
- const firstLine = lines[0];
406
- const firstLineRefs = getLineFootnoteRefs(block, firstLine.fromRun, firstLine.toRun, footnoteHeightById);
407
- const firstLineHeight = measuredLineAdvance(firstLine) + projectedFootnoteReserveGrowth(state, firstLineRefs.height);
408
- const collapsedLead = collapseParagraphSpacing({
409
- before: spaceBefore,
410
- after: state.trailingSpacing
411
- });
412
- const columnCapacity = state.contentBottom - state.topMargin;
413
- if (collapsedLead + firstLineHeight > availableHeight && spaceBefore + firstLineHeight <= columnCapacity) {
414
- paginator.ensureFits(collapsedLead + firstLineHeight);
415
- continue;
416
- }
417
- }
418
- if (keepLinesDemand !== void 0 && !keepLinesMoveAttempted && currentLineIndex === 0 && state.cursorY !== state.topMargin) {
419
- const wholeHeight = keepLinesDemand.linesHeight + projectedFootnoteReserveGrowth(state, keepLinesDemand.footnoteHeight);
420
- const collapsedLead = collapseParagraphSpacing({
421
- before: spaceBefore,
422
- after: state.trailingSpacing
423
- });
424
- const columnCapacity = state.contentBottom - state.topMargin;
425
- if (collapsedLead + wholeHeight > availableHeight && spaceBefore + wholeHeight <= columnCapacity) {
426
- keepLinesMoveAttempted = true;
427
- paginator.ensureFits(collapsedLead + wholeHeight);
428
- continue;
429
- }
430
- }
431
- let linesHeight = 0;
432
- let linesFnHeight = 0;
433
- const linesFnIds = [];
434
- let fittingLines = 0;
435
- let forcePageBreakAfterFragment = false;
436
- const firstFragmentSpaceBefore = currentLineIndex === 0 ? collapseParagraphSpacing({
437
- before: spaceBefore,
438
- after: state.trailingSpacing
439
- }) : 0;
440
- for (let j = currentLineIndex; j < lines.length; j++) {
441
- const line = lines[j];
442
- const lineAdvance = measuredLineAdvance(line);
443
- if (j > currentLineIndex && line.renderedPageBreakBefore === true && availableHeight - (linesHeight + firstFragmentSpaceBefore + linesFnHeight) <= lineAdvance * RENDERED_BREAK_REFLOW_TOLERANCE_LINES) {
444
- forcePageBreakAfterFragment = true;
445
- break;
446
- }
447
- const lineRefs = getLineFootnoteRefs(block, line.fromRun, line.toRun, footnoteHeightById);
448
- const totalWithLine = linesHeight + lineAdvance;
449
- const footnoteGrowth = projectedFootnoteReserveGrowth(state, linesFnHeight + lineRefs.height);
450
- if (totalWithLine + firstFragmentSpaceBefore + footnoteGrowth <= availableHeight || fittingLines === 0) {
451
- linesHeight = totalWithLine;
452
- linesFnHeight += lineRefs.height;
453
- for (const id of lineRefs.ids) linesFnIds.push(id);
454
- fittingLines++;
455
- } else break;
456
- }
457
- let forceBreakAfterFragment = false;
458
- if (hasWidowControl(block)) {
459
- const remainingAfter = lines.length - (currentLineIndex + fittingLines);
460
- if (fittingLines > 1 && remainingAfter === 1) {
461
- if (currentLineIndex === 0 && fittingLines === 2 && state.cursorY !== state.topMargin) {
462
- paginator.forceColumnBreak();
463
- continue;
464
- }
465
- fittingLines -= 1;
466
- forceBreakAfterFragment = true;
467
- linesHeight = 0;
468
- linesFnHeight = 0;
469
- linesFnIds.length = 0;
470
- for (let j = currentLineIndex; j < currentLineIndex + fittingLines; j++) {
471
- const line = lines[j];
472
- linesHeight += measuredLineAdvance(line);
473
- const lineRefs = getLineFootnoteRefs(block, line.fromRun, line.toRun, footnoteHeightById);
474
- linesFnHeight += lineRefs.height;
475
- for (const id of lineRefs.ids) linesFnIds.push(id);
476
- }
477
- }
478
- }
479
- const isFirstFragment = currentLineIndex === 0;
480
- const isLastFragment = currentLineIndex + fittingLines >= lines.length;
481
- const effectiveSpaceBefore = isFirstFragment ? spaceBefore : 0;
482
- const effectiveSpaceAfter = isLastFragment ? spaceAfter : 0;
483
- const pmRange = getParagraphFragmentPmRange(block, measure, currentLineIndex, currentLineIndex + fittingLines);
484
- const fragment = {
485
- kind: "paragraph",
486
- ...block.attrs?.suppressEmptyParagraphHeight === true ? { paginationRole: "empty-carrier" } : {},
487
- blockId: block.id,
488
- x: paginator.getColumnX(state.columnIndex),
489
- y: 0,
490
- width: contentWidth,
491
- height: linesHeight,
492
- fromLine: currentLineIndex,
493
- toLine: currentLineIndex + fittingLines,
494
- ...pmRange.pmStart !== void 0 ? { pmStart: pmRange.pmStart } : {},
495
- ...pmRange.pmEnd !== void 0 ? { pmEnd: pmRange.pmEnd } : {},
496
- ...!isFirstFragment ? { continuesFromPrev: true } : {},
497
- ...!isLastFragment ? { continuesOnNext: true } : {},
498
- ...block.sdtGroups ? { sdtGroups: block.sdtGroups } : {}
499
- };
500
- if (linesFnHeight > 0) {
501
- const stateBefore = paginator.getCurrentState();
502
- paginator.ensureFits(effectiveSpaceBefore + linesHeight + projectedFootnoteReserveGrowth(stateBefore, linesFnHeight));
503
- const stateAfter = paginator.getCurrentState();
504
- if (stateAfter.footnoteDemandHeight === 0) paginator.ensureFits(effectiveSpaceBefore + linesHeight + projectedFootnoteReserveGrowth(stateAfter, linesFnHeight + 12));
505
- }
506
- fragment.y = paginator.addFragment(fragment, linesHeight, effectiveSpaceBefore, effectiveSpaceAfter).y;
507
- if (linesFnHeight > 0) paginator.addFootnoteHeight(linesFnHeight, linesFnIds);
508
- currentLineIndex += fittingLines;
509
- if (currentLineIndex < lines.length) if (forcePageBreakAfterFragment) paginator.forcePageBreak();
510
- else if (forceBreakAfterFragment) paginator.forceColumnBreak();
511
- else paginator.ensureFits(measuredLineAdvance(lines[currentLineIndex]));
512
- }
513
- }
514
- /**
515
- * Count consecutive header rows at the start of a table.
516
- * Header rows are marked with isHeader: true in the block data.
517
- */
518
- function countHeaderRows(block) {
519
- let count = 0;
520
- for (const row of block.rows) if (row.isHeader) count++;
521
- else break;
522
- return count;
523
- }
524
- /**
525
- * Calculate total height of header rows from their measures.
526
- */
527
- function getHeaderRowsHeight(measure, headerRowCount) {
528
- let height = 0;
529
- for (let i = 0; i < headerRowCount && i < measure.rows.length; i++) height += measure.rows[i].height;
530
- return height;
531
- }
532
- const tableRowStartsWithRenderedPageBreak = (block, rowIndex) => {
533
- const row = block.rows[rowIndex];
534
- const visibleCells = row?.cells.filter((cell) => cell.blocks.some((cellBlock) => cellBlock.kind !== "paragraph" || !isEmptyParagraph(cellBlock)));
535
- if (!visibleCells || visibleCells.length === 0) return false;
536
- const startsWithRenderedPageBreak = (cell) => {
537
- const firstVisibleBlock = cell.blocks.find((cellBlock) => cellBlock.kind !== "paragraph" || !isEmptyParagraph(cellBlock));
538
- return firstVisibleBlock?.kind === "paragraph" && firstVisibleBlock.attrs?.renderedPageBreakBefore === true;
539
- };
540
- if (row?.heightRule === "exact") return startsWithRenderedPageBreak(visibleCells[0]);
541
- return visibleCells.every(startsWithRenderedPageBreak);
542
- };
543
- const getVerticallyMergedRows = (block) => {
544
- const mergedRows = /* @__PURE__ */ new Set();
545
- for (let rowIndex = 0; rowIndex < block.rows.length; rowIndex += 1) {
546
- const row = block.rows[rowIndex];
547
- if (!row) continue;
548
- for (const cell of row.cells) {
549
- const rowSpan = cell.rowSpan ?? 1;
550
- if (rowSpan <= 1) continue;
551
- for (let mergedRowIndex = rowIndex; mergedRowIndex < Math.min(block.rows.length, rowIndex + rowSpan); mergedRowIndex += 1) mergedRows.add(mergedRowIndex);
552
- }
553
- }
554
- return mergedRows;
555
- };
556
- const flowBlockHasTrackedChanges = (block) => {
557
- if (block.kind === "paragraph") return block.runs.some((run) => {
558
- if (run.kind === "lineBreak") return false;
559
- return run.isInsertion || run.isDeletion;
560
- });
561
- if (block.kind === "table") return block.rows.some((row) => row.cells.some((cell) => cell.blocks.some(flowBlockHasTrackedChanges)));
562
- if (block.kind === "textBox") return block.content.some(flowBlockHasTrackedChanges);
563
- return false;
564
- };
565
- const tableRowHasTrackedChanges = (block, rowIndex) => block.rows[rowIndex]?.cells.some((cell) => cell.blocks.some(flowBlockHasTrackedChanges)) ?? false;
566
- /**
567
- * Layout a table block onto pages.
568
- */
569
- function layoutTable(block, measure, paginator, footnoteHeightById) {
570
- const rows = measure.rows;
571
- if (rows.length === 0) return;
572
- const rowFootnoteIds = block.rows.map((row) => collectTableRowFootnoteIds(row, footnoteHeightById));
573
- const headerRowCount = countHeaderRows(block);
574
- const headerRowsHeight = getHeaderRowsHeight(measure, headerRowCount);
575
- let currentRowIndex = 0;
576
- const breakInfo = buildTableRowBreakInfo(block, measure);
577
- const verticallyMergedRows = getVerticallyMergedRows(block);
578
- const computeTableX = ({ columnIndex, rowIndex }) => {
579
- return paginator.getColumnX(columnIndex) + resolveTableInlineOffset({
580
- table: block,
581
- rowJustification: block.rows[rowIndex]?.justification,
582
- frameWidth: paginator.columnWidth,
583
- tableWidth: measure.totalWidth
584
- });
585
- };
586
- const getCurrentRowCapacity = (state = paginator.getCurrentState()) => state.rawContentBottom - state.topMargin;
587
- const hasAdjacentPriorTableRows = (rowIndex, state = paginator.getCurrentState()) => {
588
- const previous = state.page.fragments.at(-1);
589
- return previous?.kind === "table" && previous.blockId === block.id && previous.toRow === rowIndex && previous.y + previous.height === state.cursorY;
590
- };
591
- const shouldRepeatHeaderRows = (rowIndex, consumed, state = paginator.getCurrentState()) => headerRowCount > 0 && rowIndex >= headerRowCount && !(consumed === 0 && hasAdjacentPriorTableRows(rowIndex, state));
592
- const rowOpensFlowRegion = (rowIndex, state) => {
593
- if (state.cursorY === state.topMargin) return true;
594
- const previous = state.page.fragments.at(-1);
595
- return rowIndex > 0 && rowIndex <= headerRowCount && previous?.kind === "table" && previous.blockId === block.id && previous.fromRow === 0 && previous.toRow === rowIndex && previous.y === state.topMargin && previous.y + previous.height === state.cursorY;
596
- };
597
- const canSplitRow = (rowIndex, state = paginator.getCurrentState()) => {
598
- const row = rows[rowIndex];
599
- const sourceRow = block.rows[rowIndex];
600
- if (!row || !sourceRow || sourceRow.isHeader || verticallyMergedRows.has(rowIndex)) return false;
601
- if ((breakInfo.breakOffsets[rowIndex]?.length ?? 0) <= 1) return false;
602
- const freshHeaderOverhead = headerRowCount > 0 && rowIndex >= headerRowCount ? headerRowsHeight : 0;
603
- const oversized = row.height + freshHeaderOverhead > getCurrentRowCapacity(state);
604
- if (sourceRow.cantSplit) return oversized && sourceRow.heightRule !== "exact" && rowOpensFlowRegion(rowIndex, state);
605
- if (!oversized && (state.footnoteHeight > 0 || (rowFootnoteIds[rowIndex]?.length ?? 0) > 0)) return false;
606
- const currentHeaderOverhead = shouldRepeatHeaderRows(rowIndex, 0, state) ? headerRowsHeight : 0;
607
- const currentAvailableHeight = paginator.getAvailableHeight() - currentHeaderOverhead - state.trailingSpacing;
608
- return oversized || row.height > currentAvailableHeight;
609
- };
610
- while (currentRowIndex < rows.length) {
611
- const rowState = paginator.getCurrentState();
612
- const rowHeaderOverhead = shouldRepeatHeaderRows(currentRowIndex, 0, rowState) ? headerRowsHeight : 0;
613
- const rowAvailableHeight = paginator.getAvailableHeight() - rowHeaderOverhead - rowState.trailingSpacing;
614
- const rowStartsFreshPage = rowState.cursorY === rowState.topMargin && rowState.page.fragments.length === 0;
615
- if (block.rows[currentRowIndex]?.breakBefore === "page" && !rowStartsFreshPage) {
616
- paginator.forcePageBreak();
617
- continue;
618
- }
619
- if (!rowStartsFreshPage && tableRowStartsWithRenderedPageBreak(block, currentRowIndex) && !tableRowHasTrackedChanges(block, currentRowIndex) && rows[currentRowIndex].height > rowAvailableHeight) {
620
- paginator.forcePageBreak();
621
- continue;
622
- }
623
- const splittableRow = rows[currentRowIndex];
624
- if (canSplitRow(currentRowIndex)) {
625
- let consumed = 0;
626
- const renderedBreakHints = tableRowHasTrackedChanges(block, currentRowIndex) ? [] : breakInfo.renderedBreakHints[currentRowIndex] ?? [];
627
- let renderedBreakHintIndex = 0;
628
- const discardPassedRenderedBreakHints = () => {
629
- while ((renderedBreakHints[renderedBreakHintIndex]?.offset ?? Infinity) <= consumed) renderedBreakHintIndex += 1;
630
- };
631
- const advanceAfterNaturalFlowBreak = (previousPageNumber) => {
632
- discardPassedRenderedBreakHints();
633
- if (paginator.getCurrentState().page.number > previousPageNumber && renderedBreakHintIndex < renderedBreakHints.length) renderedBreakHintIndex += 1;
634
- };
635
- while (consumed < splittableRow.height) {
636
- const sliceState = paginator.getCurrentState();
637
- const repeatHeaderRows = shouldRepeatHeaderRows(currentRowIndex, consumed, sliceState);
638
- const headerOverhead = repeatHeaderRows ? headerRowsHeight : 0;
639
- const sliceAvail = paginator.getAvailableHeight() - headerOverhead - (consumed === 0 ? sliceState.trailingSpacing : 0);
640
- let slice = snapRowBreak(breakInfo, currentRowIndex, consumed, sliceAvail);
641
- let forceRenderedPageBreak = false;
642
- if (slice <= 0) {
643
- if (!(sliceState.cursorY === sliceState.topMargin && sliceState.page.fragments.length === 0)) {
644
- const previousPageNumber = sliceState.page.number;
645
- paginator.forceColumnBreak();
646
- advanceAfterNaturalFlowBreak(previousPageNumber);
647
- continue;
648
- }
649
- const from = consumed;
650
- slice = (breakInfo.breakOffsets[currentRowIndex]?.find((o) => o > from) ?? splittableRow.height) - consumed;
651
- }
652
- discardPassedRenderedBreakHints();
653
- const renderedBreakHint = renderedBreakHints[renderedBreakHintIndex];
654
- if (renderedBreakHint && renderedBreakHint.offset <= consumed + slice && sliceAvail - (renderedBreakHint.offset - consumed) <= renderedBreakHint.lineAdvance * RENDERED_BREAK_REFLOW_TOLERANCE_LINES) {
655
- slice = renderedBreakHint.offset - consumed;
656
- renderedBreakHintIndex += 1;
657
- forceRenderedPageBreak = true;
658
- }
659
- const sliceBottom = consumed + slice;
660
- const continuationSkip = sliceBottom < splittableRow.height ? getRowContinuationSkip(breakInfo, currentRowIndex, sliceBottom) : 0;
661
- const nextConsumed = Math.min(splittableRow.height, sliceBottom + continuationSkip);
662
- const moreAfter = !(nextConsumed >= splittableRow.height) || currentRowIndex + 1 < rows.length;
663
- const fragmentHeight = headerOverhead + slice;
664
- const sliceFragment = {
665
- kind: "table",
666
- blockId: block.id,
667
- x: computeTableX({
668
- columnIndex: sliceState.columnIndex,
669
- rowIndex: currentRowIndex
670
- }),
671
- y: 0,
672
- width: measure.totalWidth,
673
- height: fragmentHeight,
674
- fromRow: currentRowIndex,
675
- toRow: currentRowIndex + 1,
676
- ...block.pmStart !== void 0 ? { pmStart: block.pmStart } : {},
677
- ...block.pmEnd !== void 0 ? { pmEnd: block.pmEnd } : {},
678
- ...consumed > 0 || currentRowIndex > 0 ? { continuesFromPrev: true } : {},
679
- ...moreAfter ? { continuesOnNext: true } : {},
680
- ...repeatHeaderRows ? { headerRowCount } : {},
681
- ...consumed > 0 ? { topClip: consumed } : {},
682
- ...sliceBottom >= splittableRow.height ? {} : { bottomClip: sliceBottom },
683
- ...block.sdtGroups ? { sdtGroups: block.sdtGroups } : {}
684
- };
685
- const sliceResult = paginator.addFragment(sliceFragment, fragmentHeight, 0, 0);
686
- sliceFragment.y = sliceResult.y;
687
- sliceFragment.x = computeTableX({
688
- columnIndex: sliceResult.state.columnIndex,
689
- rowIndex: currentRowIndex
690
- });
691
- consumed = nextConsumed;
692
- if (consumed < splittableRow.height) if (forceRenderedPageBreak) paginator.forcePageBreak();
693
- else {
694
- const previousPageNumber = paginator.getCurrentState().page.number;
695
- paginator.forceColumnBreak();
696
- advanceAfterNaturalFlowBreak(previousPageNumber);
697
- }
698
- }
699
- currentRowIndex += 1;
700
- continue;
701
- }
702
- const state = paginator.getCurrentState();
703
- const rawAvailableHeight = paginator.getAvailableHeight();
704
- const isFirstFragment = currentRowIndex === 0;
705
- const bandSkip = isFirstFragment ? measure.bandSkipBefore ?? 0 : 0;
706
- const availableHeight = rawAvailableHeight - (isFirstFragment ? Math.max(bandSkip, state.trailingSpacing) : 0);
707
- const repeatHeaderRowsForNormalFragment = shouldRepeatHeaderRows(currentRowIndex, 0, state);
708
- const normalHeaderOverhead = repeatHeaderRowsForNormalFragment ? headerRowsHeight : 0;
709
- let rowsHeight = 0;
710
- let fittingRows = 0;
711
- const pageFootnoteIds = new Set(state.page.footnoteIds ?? []);
712
- const fragmentFootnoteIds = [];
713
- let fragmentFootnoteHeight = 0;
714
- let retryOnNextFlowRegion = false;
715
- const fragmentX = computeTableX({
716
- columnIndex: state.columnIndex,
717
- rowIndex: currentRowIndex
718
- });
719
- for (let j = currentRowIndex; j < rows.length; j++) {
720
- if (j > currentRowIndex && block.rows[j]?.breakBefore === "page") break;
721
- if (j > currentRowIndex && computeTableX({
722
- columnIndex: state.columnIndex,
723
- rowIndex: j
724
- }) !== fragmentX) break;
725
- const rowHeight = rows[j].height;
726
- const currentRowFootnoteIds = rowFootnoteIds[j] ?? [];
727
- const fragmentFootnoteCountBeforeRow = fragmentFootnoteIds.length;
728
- let rowFootnoteHeight = 0;
729
- for (const id of currentRowFootnoteIds) {
730
- if (pageFootnoteIds.has(id) || fragmentFootnoteIds.includes(id)) continue;
731
- fragmentFootnoteIds.push(id);
732
- rowFootnoteHeight += footnoteHeightById?.get(id) ?? 0;
733
- }
734
- const candidateFootnoteHeight = fragmentFootnoteHeight + rowFootnoteHeight;
735
- const footnoteGrowth = projectedFootnoteReserveGrowth(state, candidateFootnoteHeight + (state.footnoteDemandHeight === 0 && candidateFootnoteHeight > 0 ? 12 : 0));
736
- if (rowsHeight + rowHeight + normalHeaderOverhead + footnoteGrowth <= availableHeight) {
737
- rowsHeight += rowHeight;
738
- fragmentFootnoteHeight += rowFootnoteHeight;
739
- fittingRows++;
740
- } else if (fittingRows === 0) {
741
- if (state.cursorY === state.topMargin && state.page.fragments.length === 0) {
742
- rowsHeight += rowHeight;
743
- fragmentFootnoteHeight += rowFootnoteHeight;
744
- fittingRows++;
745
- break;
746
- }
747
- fragmentFootnoteIds.splice(fragmentFootnoteCountBeforeRow);
748
- paginator.forceColumnBreak();
749
- retryOnNextFlowRegion = true;
750
- break;
751
- } else {
752
- fragmentFootnoteIds.splice(fragmentFootnoteCountBeforeRow);
753
- break;
754
- }
755
- }
756
- if (retryOnNextFlowRegion) continue;
757
- const fragmentHeight = rowsHeight + normalHeaderOverhead;
758
- const isLastFragment = currentRowIndex + fittingRows >= rows.length;
759
- const desiredX = fragmentX;
760
- const fragment = {
761
- kind: "table",
762
- blockId: block.id,
763
- x: desiredX,
764
- y: 0,
765
- width: measure.totalWidth,
766
- height: fragmentHeight,
767
- fromRow: currentRowIndex,
768
- toRow: currentRowIndex + fittingRows,
769
- ...block.pmStart !== void 0 ? { pmStart: block.pmStart } : {},
770
- ...block.pmEnd !== void 0 ? { pmEnd: block.pmEnd } : {},
771
- ...!isFirstFragment ? { continuesFromPrev: true } : {},
772
- ...!isLastFragment ? { continuesOnNext: true } : {},
773
- ...repeatHeaderRowsForNormalFragment ? { headerRowCount } : {},
774
- ...block.sdtGroups ? { sdtGroups: block.sdtGroups } : {}
775
- };
776
- if (fragmentFootnoteHeight > 0) paginator.addFootnoteHeight(fragmentFootnoteHeight, fragmentFootnoteIds);
777
- fragment.y = paginator.addFragment(fragment, fragmentHeight, bandSkip, 0).y;
778
- fragment.x = desiredX;
779
- currentRowIndex += fittingRows;
780
- if (currentRowIndex < rows.length) {
781
- if (canSplitRow(currentRowIndex)) {
782
- const nextState = paginator.getCurrentState();
783
- const nextHeaderOverhead = shouldRepeatHeaderRows(currentRowIndex, 0, nextState) ? headerRowsHeight : 0;
784
- const nextSliceAvail = paginator.getAvailableHeight() - nextHeaderOverhead - nextState.trailingSpacing;
785
- if (snapRowBreak(breakInfo, currentRowIndex, 0, nextSliceAvail) > 0) continue;
786
- }
787
- const nextState = paginator.getCurrentState();
788
- const nextRowHeight = rows[currentRowIndex].height + (shouldRepeatHeaderRows(currentRowIndex, 0, nextState) ? headerRowsHeight : 0);
789
- paginator.ensureFits(nextRowHeight);
790
- }
791
- }
792
- }
793
- function collectTableRowFootnoteIds(row, footnoteHeightById) {
794
- if (!row || !footnoteHeightById || row.hidden) return [];
795
- const ids = [];
796
- const walk = (blocks) => {
797
- for (const block of blocks) {
798
- if (block.kind === "paragraph") {
799
- for (const run of block.runs) if (run.kind === "text" && run.footnoteRefId !== void 0 && footnoteHeightById.has(run.footnoteRefId) && !ids.includes(run.footnoteRefId)) ids.push(run.footnoteRefId);
800
- continue;
801
- }
802
- if (block.kind === "table") {
803
- for (const nestedRow of block.rows) {
804
- if (nestedRow.hidden) continue;
805
- for (const cell of nestedRow.cells) walk(cell.blocks);
806
- }
807
- continue;
808
- }
809
- if (block.kind === "textBox") walk(block.content);
810
- }
811
- };
812
- for (const cell of row.cells) walk(cell.blocks);
813
- return ids;
814
- }
815
- /**
816
- * Layout a floating table (anchored) without advancing the cursor.
817
- */
818
- function layoutFloatingTable(block, measure, paginator, contentWidth) {
819
- const state = paginator.getCurrentState();
820
- const floating = block.floating;
821
- const page = state.page;
822
- const margins = page.margins;
823
- const tableWidth = measure.totalWidth;
824
- const tableHeight = measure.totalHeight;
825
- const contentHeight = page.size.h - margins.top - margins.bottom;
826
- let baseY = margins.top;
827
- if (floating?.vertAnchor === "page") baseY = 0;
828
- else if (floating?.vertAnchor === "text") baseY = state.cursorY;
829
- let x = paginator.getColumnX(state.columnIndex);
830
- if (floating) x = resolveFloatingTablePageX({
831
- anchor: floating,
832
- justification: block.justification,
833
- tableWidth,
834
- marginWidth: contentWidth,
835
- pageWidth: page.size.w,
836
- marginLeft: margins.left,
837
- textFrameWidth: paginator.columnWidth,
838
- textFrameLeft: paginator.getColumnX(state.columnIndex)
839
- });
840
- let y = state.cursorY;
841
- let usedExplicitY = false;
842
- if (floating?.tblpY !== void 0) {
843
- y = baseY + floating.tblpY;
844
- usedExplicitY = true;
845
- } else if (floating?.tblpYSpec) {
846
- usedExplicitY = true;
847
- const spec = floating.tblpYSpec;
848
- if (spec === "top") y = baseY;
849
- else if (spec === "bottom") y = baseY + contentHeight - tableHeight;
850
- else if (spec === "center") y = baseY + (contentHeight - tableHeight) / 2;
851
- }
852
- if (!usedExplicitY) y = paginator.ensureFits(tableHeight).cursorY;
853
- const fragment = {
854
- kind: "table",
855
- blockId: block.id,
856
- x,
857
- y,
858
- width: tableWidth,
859
- height: tableHeight,
860
- fromRow: 0,
861
- toRow: block.rows.length,
862
- ...block.pmStart !== void 0 ? { pmStart: block.pmStart } : {},
863
- ...block.pmEnd !== void 0 ? { pmEnd: block.pmEnd } : {},
864
- isFloating: true,
865
- ...block.sdtGroups ? { sdtGroups: block.sdtGroups } : {}
866
- };
867
- paginator.addUnflowedFragment(fragment);
868
- }
869
- /**
870
- * Layout an image block onto pages.
871
- */
872
- function layoutImage(block, measure, paginator) {
873
- if (block.anchor?.isAnchored) {
874
- layoutAnchoredImage(block, measure, paginator);
875
- return;
876
- }
877
- const bandSkip = measure.bandSkipBefore ?? 0;
878
- const state = paginator.ensureFits(bandSkip + measure.height);
879
- const fragment = {
880
- kind: "image",
881
- blockId: block.id,
882
- x: paginator.getColumnX(state.columnIndex),
883
- y: 0,
884
- width: measure.width,
885
- height: measure.height,
886
- ...block.pmStart !== void 0 ? { pmStart: block.pmStart } : {},
887
- ...block.pmEnd !== void 0 ? { pmEnd: block.pmEnd } : {}
888
- };
889
- fragment.y = paginator.addFragment(fragment, measure.height, bandSkip, 0).y;
890
- }
891
- /**
892
- * Layout an anchored (floating) image.
893
- */
894
- function layoutAnchoredImage(block, measure, paginator) {
895
- const state = paginator.getCurrentState();
896
- const anchor = block.anchor;
897
- if (!anchor) return;
898
- const x = anchor.offsetH ?? paginator.getColumnX(state.columnIndex);
899
- const y = anchor.offsetV ?? state.cursorY;
900
- const fragment = {
901
- kind: "image",
902
- blockId: block.id,
903
- x,
904
- y,
905
- width: measure.width,
906
- height: measure.height,
907
- ...block.pmStart !== void 0 ? { pmStart: block.pmStart } : {},
908
- ...block.pmEnd !== void 0 ? { pmEnd: block.pmEnd } : {},
909
- isAnchored: true,
910
- zIndex: anchor.behindDoc ? -1 : 1
911
- };
912
- paginator.addUnflowedFragment(fragment);
913
- }
914
- const TEXT_BOX_ANCHOR_BLOCK_ID = Symbol.for("stll.textBoxAnchorBlockId");
915
- const readTextBoxAnchorBlockId = (block) => Reflect.get(block, TEXT_BOX_ANCHOR_BLOCK_ID);
916
- /**
917
- * Layout a text box block onto pages.
918
- */
919
- function layoutTextBox(block, measure, { paginator, sectionMarginTop, sectionPageHeight, sectionMarginBottom }) {
920
- if (isPagePinnedBandTextBox(block)) {
921
- const state = paginator.getCurrentState();
922
- const bandTop = bandTopContentY(block.position?.vertical, {
923
- pageHeight: sectionPageHeight,
924
- marginTop: sectionMarginTop,
925
- marginBottom: sectionMarginBottom,
926
- boxHeight: measure.height
927
- });
928
- const x = bandFragmentX(block.position?.horizontal, {
929
- pageWidth: state.page.size.w,
930
- marginLeft: state.page.margins.left,
931
- marginRight: state.page.margins.right,
932
- activeColumnLeft: paginator.getColumnX(state.columnIndex),
933
- activeColumnWidth: paginator.columnWidth,
934
- boxWidth: measure.width
935
- });
936
- const fragment = {
937
- kind: "textBox",
938
- blockId: block.id,
939
- x,
940
- y: block.position?.vertical?.relativeTo === "page" ? sectionMarginTop + bandTop : state.topMargin + bandTop,
941
- width: measure.width,
942
- height: measure.height,
943
- isPositioned: true,
944
- ...block.pmStart !== void 0 ? { pmStart: block.pmStart } : {},
945
- ...block.pmEnd !== void 0 ? { pmEnd: block.pmEnd } : {}
946
- };
947
- paginator.addUnflowedFragment(fragment);
948
- return;
949
- }
950
- if (block.position !== void 0) {
951
- const state = paginator.getCurrentState();
952
- const x = bandFragmentX(block.position.horizontal, {
953
- pageWidth: state.page.size.w,
954
- marginLeft: state.page.margins.left,
955
- marginRight: state.page.margins.right,
956
- activeColumnLeft: paginator.getColumnX(state.columnIndex),
957
- activeColumnWidth: paginator.columnWidth,
958
- boxWidth: measure.width
959
- });
960
- const vertical = block.position.vertical;
961
- const anchorBlockId = readTextBoxAnchorBlockId(block);
962
- const anchorParagraph = vertical?.relativeTo === "paragraph" && typeof anchorBlockId === "string" ? state.page.fragments.find((fragment) => fragment.kind === "paragraph" && fragment.blockId === anchorBlockId) : void 0;
963
- const bandGeometry = {
964
- pageHeight: sectionPageHeight,
965
- marginTop: sectionMarginTop,
966
- marginBottom: sectionMarginBottom,
967
- boxHeight: measure.height
968
- };
969
- let y;
970
- if (vertical?.relativeTo === "page") y = sectionMarginTop + bandTopContentY(vertical, bandGeometry);
971
- else if (isPageFrameRelativeAnchor(vertical?.relativeTo)) y = state.topMargin + bandTopContentY(vertical, {
972
- pageHeight: sectionPageHeight,
973
- marginTop: sectionMarginTop,
974
- marginBottom: sectionMarginBottom,
975
- boxHeight: measure.height
976
- });
977
- else y = (anchorParagraph?.y ?? state.cursorY) + emuToPixels(vertical?.posOffset ?? 0);
978
- const fragment = {
979
- kind: "textBox",
980
- blockId: block.id,
981
- x,
982
- y,
983
- width: measure.width,
984
- height: measure.height,
985
- isPositioned: true,
986
- ...block.pmStart !== void 0 ? { pmStart: block.pmStart } : {},
987
- ...block.pmEnd !== void 0 ? { pmEnd: block.pmEnd } : {}
988
- };
989
- paginator.addUnflowedFragment(fragment);
990
- return;
991
- }
992
- const effect = block.effectExtent;
993
- const occupiedHeight = measure.height + (effect?.top ?? 0) + (effect?.bottom ?? 0);
994
- const occupiedWidth = measure.width + (effect?.left ?? 0) + (effect?.right ?? 0);
995
- const host = block.hostParagraph;
996
- const fragment = {
997
- kind: "textBox",
998
- blockId: block.id,
999
- x: 0,
1000
- y: 0,
1001
- width: measure.width,
1002
- height: measure.height,
1003
- ...block.pmStart !== void 0 ? { pmStart: block.pmStart } : {},
1004
- ...block.pmEnd !== void 0 ? { pmEnd: block.pmEnd } : {}
1005
- };
1006
- const result = paginator.addFragment(fragment, occupiedHeight, host?.spacing?.before ?? 0, host?.spacing?.after ?? 0);
1007
- fragment.x = result.x + hostParagraphLineOffset(host, paginator.columnWidth, occupiedWidth) + (effect?.left ?? 0);
1008
- fragment.y = result.y + (effect?.top ?? 0);
1009
- }
1010
- /**
1011
- * Where a line holding only an object of `width` starts within a column:
1012
- * after the host paragraph's start and first-line indents, then aligned in
1013
- * what remains before its end indent.
1014
- */
1015
- function hostParagraphLineOffset(host, columnWidth, width) {
1016
- if (!host) return 0;
1017
- const indent = host.indent;
1018
- const start = (indent?.left ?? 0) + (indent?.firstLine ?? 0) - (indent?.hanging ?? 0);
1019
- const slack = Math.max(0, columnWidth - start - (indent?.right ?? 0) - width);
1020
- switch (host.alignment) {
1021
- case "center": return start + slack / 2;
1022
- case "right": return start + slack;
1023
- case "left":
1024
- case "justify":
1025
- case void 0: return start;
1026
- default:
1027
- host.alignment;
1028
- return start;
1029
- }
1030
- }
1031
- /**
1032
- * A topAndBottom text box whose vertical anchor pins it to the page frame
1033
- * (page/margin/margin-strip) — it floats to a fixed page position rather than
1034
- * flowing in document order. Must agree with the measure pass's band extraction
1035
- * (extractFloatingZones), which uses the same predicate. eigenpal #694.
1036
- */
1037
- function isPagePinnedBandTextBox(block) {
1038
- if (!floatingTextBoxReservesBand(block)) return false;
1039
- return isPageFrameRelativeAnchor(block.position?.vertical?.relativeTo);
1040
- }
1041
- /**
1042
- * Start the next section in the current page region, below the outgoing
1043
- * content.
1044
- *
1045
- * ECMA-376 §17.6.22: a `continuous` break normally keeps the current page
1046
- * geometry and defers the new size/margins to the next natural page break. But
1047
- * a break that changes page size or orientation cannot share a physical sheet
1048
- * with the preceding section, so Word and LibreOffice promote it to a page
1049
- * break (eigenpal/docx-editor#841).
1050
- *
1051
- * Compare against the last laid-out page without materializing one: a break
1052
- * before any content has no sheet to share, so it defers (the first content
1053
- * then opens a page with the new geometry) rather than stranding a blank
1054
- * leading page.
1055
- */
1056
- function startSectionInPlace(paginator, nextSectionConfig, nextSectionIndex) {
1057
- const currentPage = paginator.states.at(-1)?.page;
1058
- const nextSize = nextSectionConfig.pageSize;
1059
- const pageSizeChanges = currentPage != null && (Math.round(nextSize.w) !== Math.round(currentPage.size.w) || Math.round(nextSize.h) !== Math.round(currentPage.size.h));
1060
- if (nextSectionIndex !== void 0) paginator.startSection({
1061
- sectionIndex: nextSectionIndex,
1062
- pageNumbering: nextSectionConfig.pageNumbering,
1063
- placement: pageSizeChanges ? SECTION_START_PLACEMENT.NEXT_PAGE : SECTION_START_PLACEMENT.CONTINUOUS
1064
- });
1065
- if (pageSizeChanges) {
1066
- paginator.updatePageLayout(nextSize, nextSectionConfig.margins);
1067
- if (!paginator.retargetCurrentBlankPage()) paginator.forcePageBreak({ coalesceBlankPage: true });
1068
- return;
1069
- }
1070
- paginator.updatePageLayout(nextSize, nextSectionConfig.margins, false);
1071
- paginator.retargetCurrentBlankPage();
1072
- }
1073
- /**
1074
- * Whether a section opening on `target` would give two consecutive sheets page
1075
- * numbers that are both odd or both even under `w:evenAndOddHeaders` (§17.10.1). Odd and
1076
- * even page numbers alternate between right- and left-hand sheets, so a
1077
- * `w:pgNumType w:start` restart that repeats whether the previous sheet's number is odd or even
1078
- * needs a blank sheet between them.
1079
- */
1080
- function sectionStartRepeatsOddEven(paginator, target) {
1081
- if (target.page.headerFooterRefs?.evenAndOddHeaders !== true) return false;
1082
- const previous = paginator.states.at(-2)?.page;
1083
- if (previous === void 0 || previous.number !== target.page.number - 1) return false;
1084
- return previous.logicalNumber % 2 === target.page.logicalNumber % 2;
1085
- }
1086
- /**
1087
- * Keep the blank sheet the section break opened as a filler with no page
1088
- * furniture, and open the section again on the following sheet so its first
1089
- * page keeps the restarted page number.
1090
- */
1091
- function insertOddEvenFillerPage(paginator, filler, sectionIndex, sectionConfig) {
1092
- filler.page.headerFooterRefs = {};
1093
- paginator.startSection({
1094
- sectionIndex,
1095
- pageNumbering: sectionConfig.pageNumbering
1096
- });
1097
- paginator.forcePageBreak();
1098
- }
1099
- /**
1100
- * Handle a section break block.
1101
- * @param block - The section break block (current section's properties)
1102
- * @param paginator - The paginator instance
1103
- * @param nextSectionConfig - Page layout for the NEXT section
1104
- * @param nextSectionType - Break type of the NEXT section (how it starts relative to current)
1105
- */
1106
- function handleSectionBreak(_block, paginator, nextSectionConfig, nextSectionType, nextSectionIndex) {
1107
- switch (nextSectionType) {
1108
- case "nextPage": {
1109
- paginator.updatePageLayout(nextSectionConfig.pageSize, nextSectionConfig.margins);
1110
- if (nextSectionIndex !== void 0) paginator.startSection({
1111
- sectionIndex: nextSectionIndex,
1112
- pageNumbering: nextSectionConfig.pageNumbering
1113
- });
1114
- const target = paginator.forcePageBreak({ coalesceBlankPage: true });
1115
- if (nextSectionIndex !== void 0 && sectionStartRepeatsOddEven(paginator, target)) insertOddEvenFillerPage(paginator, target, nextSectionIndex, nextSectionConfig);
1116
- break;
1117
- }
1118
- case "evenPage":
1119
- if (paginator.forcePageBreak({ coalesceBlankPage: true }).page.number % 2 !== 0) paginator.forcePageBreak();
1120
- paginator.updatePageLayout(nextSectionConfig.pageSize, nextSectionConfig.margins);
1121
- if (nextSectionIndex !== void 0) paginator.startSection({
1122
- sectionIndex: nextSectionIndex,
1123
- pageNumbering: nextSectionConfig.pageNumbering
1124
- });
1125
- if (!paginator.retargetCurrentBlankPage()) panic("Even-page section target must be blank");
1126
- break;
1127
- case "oddPage":
1128
- if (paginator.forcePageBreak({ coalesceBlankPage: true }).page.number % 2 === 0) paginator.forcePageBreak();
1129
- paginator.updatePageLayout(nextSectionConfig.pageSize, nextSectionConfig.margins);
1130
- if (nextSectionIndex !== void 0) paginator.startSection({
1131
- sectionIndex: nextSectionIndex,
1132
- pageNumbering: nextSectionConfig.pageNumbering
1133
- });
1134
- if (!paginator.retargetCurrentBlankPage()) panic("Odd-page section target must be blank");
1135
- break;
1136
- case "nextColumn": {
1137
- const currentPage = paginator.states.at(-1)?.page;
1138
- if (currentPage === void 0 || !physicalColumnRegionIsShared({
1139
- pageSize: currentPage.size,
1140
- margins: currentPage.margins,
1141
- columns: paginator.columns
1142
- }, nextSectionConfig)) {
1143
- startSectionInPlace(paginator, nextSectionConfig, nextSectionIndex);
1144
- break;
1145
- }
1146
- if (nextSectionIndex !== void 0) paginator.startSection({
1147
- sectionIndex: nextSectionIndex,
1148
- pageNumbering: nextSectionConfig.pageNumbering,
1149
- placement: SECTION_START_PLACEMENT.CONTINUOUS
1150
- });
1151
- paginator.updatePageLayout(nextSectionConfig.pageSize, nextSectionConfig.margins, false);
1152
- paginator.forceColumnBreak();
1153
- return;
1154
- }
1155
- case "continuous":
1156
- startSectionInPlace(paginator, nextSectionConfig, nextSectionIndex);
1157
- break;
1158
- default:
1159
- }
1160
- paginator.updateColumns(nextSectionConfig.columns ?? DEFAULT_COLUMNS);
1161
- }
1162
237
  //#endregion
1163
238
  export { DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, NOTE_SEPARATOR_RULE_THICKNESS, NOTE_SEPARATOR_WIDTH_FRACTION, applyContextualSpacing, applyPendingToActive, assertExhaustiveFlowBlock, calculateChainHeight, collectSectionConfigs, computeKeepNextChains, createInitialSectionState, createPaginator, findPageIndexContainingPmPos, floatingTextBoxReservesBand, floatingTextBoxWrapsText, getEffectiveColumns, getEffectiveMargins, getEffectivePageSize, getHeaderRowsHeight, getMidChainIndices, getTableRowLeadingWidth, hasKeepLines, hasPageBreakBefore, isFloatingImageRun, isFloatingTextBoxBlock, isListNumPr, isTextWrappingFloatingImageRun, layoutDocument, resolveSectionHeaderFooterRefs, resolveTableCellPadding, resolveTableWidthPx, scheduleSectionBreak, stripFlowBlockPmAnchors, tableColumnsArePinned };