@stll/folio-core 0.29.0 → 0.30.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 (99) hide show
  1. package/dist/controller/folioEditor.d.ts +28 -1
  2. package/dist/controller/folioEditor.js +37 -26
  3. package/dist/controller/fontReadiness.js +28 -24
  4. package/dist/controller/hiddenEditorManager.d.ts +2 -7
  5. package/dist/controller/layoutPipeline.js +33 -3
  6. package/dist/controller/noteEditorManager.d.ts +3 -6
  7. package/dist/docx/metafileSvg.d.ts +12 -0
  8. package/dist/docx/metafileSvg.js +462 -0
  9. package/dist/docx/paragraphTextBoxEnrichment.js +55 -4
  10. package/dist/docx/parser.d.ts +2 -2
  11. package/dist/docx/parser.js +26 -0
  12. package/dist/docx/runParser.js +2 -1
  13. package/dist/docx/serializer/runSerializer.js +14 -2
  14. package/dist/docx/styleParser.js +2 -1
  15. package/dist/docx/textBoxParser.js +14 -0
  16. package/dist/docx/vmlImageParser.js +72 -152
  17. package/dist/docx/vmlPreview.d.ts +31 -0
  18. package/dist/docx/vmlPreview.js +535 -0
  19. package/dist/docx/wrapTypes.d.ts +2 -36
  20. package/dist/docx/wrapTypes.js +1 -39
  21. package/dist/fonts/fontAlternates.d.ts +8 -0
  22. package/dist/fonts/fontAlternates.js +16 -0
  23. package/dist/layout-bridge/convert/footnoteLayout.d.ts +1 -0
  24. package/dist/layout-bridge/convert/footnoteLayout.js +34 -3
  25. package/dist/layout-bridge/convert/headerFooterLayout.d.ts +1 -0
  26. package/dist/layout-bridge/convert/headerFooterLayout.js +5 -0
  27. package/dist/layout-bridge/convert/toFlowBlocks.d.ts +3 -0
  28. package/dist/layout-bridge/convert/toFlowBlocks.js +78 -28
  29. package/dist/layout-bridge/dom/noteStoryDom.d.ts +1 -1
  30. package/dist/layout-bridge/engine/selectionRects.js +3 -1
  31. package/dist/layout-engine/index.js +65 -32
  32. package/dist/layout-engine/justifiedLineFit.d.ts +7 -0
  33. package/dist/layout-engine/justifiedLineFit.js +6 -0
  34. package/dist/layout-engine/keep-together.js +15 -10
  35. package/dist/layout-engine/measure/complexScriptFormatting.d.ts +6 -1
  36. package/dist/layout-engine/measure/complexScriptFormatting.js +11 -2
  37. package/dist/layout-engine/measure/floatingTablePosition.d.ts +16 -1
  38. package/dist/layout-engine/measure/floatingTablePosition.js +29 -6
  39. package/dist/layout-engine/measure/lineBreakProvider.js +67 -8
  40. package/dist/layout-engine/measure/listMarkerWidth.d.ts +1 -0
  41. package/dist/layout-engine/measure/listMarkerWidth.js +23 -10
  42. package/dist/layout-engine/measure/measureBlocks.d.ts +15 -0
  43. package/dist/layout-engine/measure/measureBlocks.js +197 -30
  44. package/dist/layout-engine/measure/measureContainer.js +20 -24
  45. package/dist/layout-engine/measure/measureHelpers.d.ts +1 -1
  46. package/dist/layout-engine/measure/measureHelpers.js +14 -8
  47. package/dist/layout-engine/measure/measureParagraph.js +111 -37
  48. package/dist/layout-engine/measure/measureTypes.d.ts +3 -0
  49. package/dist/layout-engine/measure/tabCalculator.d.ts +123 -0
  50. package/dist/layout-engine/measure/tabCalculator.js +144 -0
  51. package/dist/layout-engine/measure/tableCellFlow.d.ts +21 -1
  52. package/dist/layout-engine/measure/tableCellFlow.js +45 -8
  53. package/dist/layout-engine/measure/tableInlinePlacement.d.ts +10 -2
  54. package/dist/layout-engine/measure/tableInlinePlacement.js +17 -15
  55. package/dist/layout-engine/paginator.d.ts +26 -2
  56. package/dist/layout-engine/paginator.js +76 -14
  57. package/dist/layout-engine/paragraphSpacing.d.ts +3 -1
  58. package/dist/layout-engine/paragraphSpacing.js +4 -3
  59. package/dist/layout-engine/renderedBreakReconciliation.d.ts +14 -3
  60. package/dist/layout-engine/renderedBreakReconciliation.js +45 -14
  61. package/dist/layout-engine/section-breaks.d.ts +3 -1
  62. package/dist/layout-engine/section-breaks.js +6 -2
  63. package/dist/layout-engine/types.d.ts +18 -1
  64. package/dist/layout-engine/types.js +1 -1
  65. package/dist/layout-painter/imageLayout.d.ts +1 -1
  66. package/dist/layout-painter/renderPage.js +6 -1
  67. package/dist/layout-painter/renderParagraph.js +169 -82
  68. package/dist/layout-painter/renderTable.d.ts +1 -1
  69. package/dist/layout-painter/renderTable.js +18 -7
  70. package/dist/layout-painter/renderTextBox.d.ts +1 -1
  71. package/dist/layout-painter/renderTextBox.js +6 -1
  72. package/dist/model.d.ts +3 -3
  73. package/dist/model.js +2 -2
  74. package/dist/paged-layout/sectionBlockWidths.d.ts +14 -0
  75. package/dist/paged-layout/sectionBlockWidths.js +103 -14
  76. package/dist/prosemirror/attrs/index.js +12 -1
  77. package/dist/prosemirror/conversion/fromProseDoc.js +6 -2
  78. package/dist/prosemirror/conversion/toProseDoc.js +10 -3
  79. package/dist/prosemirror/extensions/marks/CharacterSpacingExtension.js +9 -5
  80. package/dist/prosemirror/extensions/nodes/TableExtension.js +2 -0
  81. package/dist/prosemirror/extensions/nodes/TextBoxExtension.js +6 -2
  82. package/dist/prosemirror/schema/nodes.d.ts +4 -0
  83. package/dist/prosemirror/utils/tabCalculator.d.ts +2 -123
  84. package/dist/prosemirror/utils/tabCalculator.js +1 -140
  85. package/dist/render-dom/RemoteSelectionOverlay.d.ts +4 -4
  86. package/dist/types/documentEnumValues.d.ts +4 -1
  87. package/dist/types/documentEnumValues.js +12 -1
  88. package/dist/types/editor-story.d.ts +8 -0
  89. package/dist/types/editor-story.js +0 -0
  90. package/dist/types/remote-selection.d.ts +11 -0
  91. package/dist/types/remote-selection.js +0 -0
  92. package/dist/types/wrap.d.ts +36 -0
  93. package/dist/types/wrap.js +40 -0
  94. package/dist/utils/fontResolver.d.ts +1 -1
  95. package/dist/utils/fontResolver.js +42 -2
  96. package/dist/utils/formatToStyle.js +4 -2
  97. package/dist/utils/horizontalScale.d.ts +28 -0
  98. package/dist/utils/horizontalScale.js +42 -0
  99. package/package.json +2 -2
@@ -2,12 +2,33 @@ import { measuredLineRangeHeight } from "../lineFlow.js";
2
2
  import { collapseParagraphSpacing, getParagraphSpacingAfter, getParagraphSpacingBefore, isEmptyParagraph } from "../paragraphSpacing.js";
3
3
  import { isFloatingTextBoxBlock } from "../types.js";
4
4
  //#region src/layout-engine/measure/tableCellFlow.ts
5
+ const TABLE_CELL_FLOW_POSITION = {
6
+ start: "start",
7
+ interior: "interior"
8
+ };
9
+ const TABLE_CELL_TRAILING_SPACING_TYPE = {
10
+ none: "none",
11
+ authored: "authored",
12
+ automatic: "automatic"
13
+ };
5
14
  const createTableCellFlowState = () => ({
6
15
  height: 0,
16
+ position: TABLE_CELL_FLOW_POSITION.start,
7
17
  previousParagraphWasEmpty: false,
8
- trailingSpacing: 0
18
+ trailingSpacing: { type: TABLE_CELL_TRAILING_SPACING_TYPE.none }
9
19
  });
10
- const finishTableCellFlow = (state) => state.height + state.trailingSpacing;
20
+ const trailingSpacingValue = (spacing) => {
21
+ switch (spacing.type) {
22
+ case TABLE_CELL_TRAILING_SPACING_TYPE.none: return 0;
23
+ case TABLE_CELL_TRAILING_SPACING_TYPE.authored:
24
+ case TABLE_CELL_TRAILING_SPACING_TYPE.automatic: return spacing.value;
25
+ default: return spacing;
26
+ }
27
+ };
28
+ const finishTableCellFlow = (state) => {
29
+ if (state.trailingSpacing.type === TABLE_CELL_TRAILING_SPACING_TYPE.automatic) return state.height;
30
+ return state.height + trailingSpacingValue(state.trailingSpacing);
31
+ };
11
32
  const isSuppressedParagraphMeasure = (measure) => measure.totalHeight === 0 && measure.lines.every(({ lineHeight }) => lineHeight === 0);
12
33
  const paragraphSpacing = (block, measure) => {
13
34
  if (isSuppressedParagraphMeasure(measure)) return {
@@ -42,12 +63,19 @@ const placeTableCellBlock = (state, block, measure) => {
42
63
  contentHeight: 0,
43
64
  leadingSpacing: 0
44
65
  };
66
+ if (block.kind === "textBox" && isFloatingTextBoxBlock(block)) return {
67
+ top,
68
+ contentTop: top,
69
+ contentHeight: 0,
70
+ leadingSpacing: 0
71
+ };
45
72
  if (block.kind !== "paragraph" || measure.kind !== "paragraph") {
46
- const leadingSpacing = state.trailingSpacing;
73
+ const leadingSpacing = trailingSpacingValue(state.trailingSpacing);
47
74
  const contentTop = top + leadingSpacing;
48
75
  state.height = contentTop + contentHeight;
76
+ state.position = TABLE_CELL_FLOW_POSITION.interior;
49
77
  state.previousParagraphWasEmpty = false;
50
- state.trailingSpacing = 0;
78
+ state.trailingSpacing = { type: TABLE_CELL_TRAILING_SPACING_TYPE.none };
51
79
  return {
52
80
  top,
53
81
  contentTop,
@@ -57,14 +85,23 @@ const placeTableCellBlock = (state, block, measure) => {
57
85
  }
58
86
  const spacing = paragraphSpacing(block, measure);
59
87
  const empty = isEmptyParagraph(block);
60
- const leadingSpacing = empty && state.previousParagraphWasEmpty ? spacing.before + state.trailingSpacing : collapseParagraphSpacing({
61
- before: spacing.before,
62
- after: state.trailingSpacing
88
+ const before = state.position === TABLE_CELL_FLOW_POSITION.start && block.attrs?.automaticSpacing?.before === true ? 0 : spacing.before;
89
+ const trailingSpacing = trailingSpacingValue(state.trailingSpacing);
90
+ const leadingSpacing = empty && state.previousParagraphWasEmpty ? before + trailingSpacing : collapseParagraphSpacing({
91
+ before,
92
+ after: trailingSpacing
63
93
  });
64
94
  const contentTop = top + leadingSpacing;
65
95
  state.height = contentTop + contentHeight;
96
+ state.position = TABLE_CELL_FLOW_POSITION.interior;
66
97
  state.previousParagraphWasEmpty = empty;
67
- state.trailingSpacing = spacing.after;
98
+ state.trailingSpacing = block.attrs?.automaticSpacing?.after === true ? {
99
+ type: TABLE_CELL_TRAILING_SPACING_TYPE.automatic,
100
+ value: spacing.after
101
+ } : {
102
+ type: TABLE_CELL_TRAILING_SPACING_TYPE.authored,
103
+ value: spacing.after
104
+ };
68
105
  return {
69
106
  top,
70
107
  contentTop,
@@ -7,6 +7,14 @@ type TableInlinePlacement = {
7
7
  offset: number;
8
8
  };
9
9
  /** Resolve an inline table's horizontal anchor without losing RTL leading-edge semantics. */
10
- declare const resolveTableInlinePlacement: (table: Pick<TableBlock, "bidi" | "indent" | "justification" | "rows">) => TableInlinePlacement;
10
+ declare const resolveTableInlinePlacement: (table: Pick<TableBlock, "bidi" | "indent" | "justification">, rowJustification?: TableBlock["justification"]) => TableInlinePlacement;
11
+ type ResolveTableInlineOffsetOptions = {
12
+ table: Pick<TableBlock, "bidi" | "indent" | "justification">;
13
+ rowJustification?: TableBlock["justification"];
14
+ frameWidth: number;
15
+ tableWidth: number;
16
+ };
17
+ /** Resolve a table's physical inline offset within its current content frame. */
18
+ declare const resolveTableInlineOffset: ({ table, rowJustification, frameWidth, tableWidth }: ResolveTableInlineOffsetOptions) => number;
11
19
  //#endregion
12
- export { resolveTableInlinePlacement };
20
+ export { resolveTableInlineOffset, resolveTableInlinePlacement };
@@ -1,22 +1,24 @@
1
- import { resolveTableCellPadding } from "../types.js";
2
1
  //#region src/layout-engine/measure/tableInlinePlacement.ts
3
2
  /** Resolve an inline table's horizontal anchor without losing RTL leading-edge semantics. */
4
- const resolveTableInlinePlacement = (table) => {
5
- if (table.justification === "center") return { alignment: "center" };
6
- if (table.justification === "right") return {
7
- alignment: "right",
8
- offset: 0
9
- };
10
- const firstCell = table.rows.at(0)?.cells.at(0);
11
- const firstCellPadding = firstCell ? resolveTableCellPadding(firstCell) : void 0;
12
- if (table.justification === "left" || table.bidi !== true) return {
13
- alignment: "left",
14
- offset: table.indent ?? -(firstCellPadding?.left ?? 0)
3
+ const resolveTableInlinePlacement = (table, rowJustification) => {
4
+ const logicalJustification = rowJustification ?? table.justification ?? "left";
5
+ if (logicalJustification === "center") return { alignment: "center" };
6
+ const offset = logicalJustification === "left" ? table.indent ?? 0 : 0;
7
+ if (table.bidi !== true) return {
8
+ alignment: logicalJustification,
9
+ offset
15
10
  };
16
11
  return {
17
- alignment: "right",
18
- offset: table.indent ?? -(firstCellPadding?.right ?? 0)
12
+ alignment: logicalJustification === "left" ? "right" : "left",
13
+ offset
19
14
  };
20
15
  };
16
+ /** Resolve a table's physical inline offset within its current content frame. */
17
+ const resolveTableInlineOffset = ({ table, rowJustification, frameWidth, tableWidth }) => {
18
+ const placement = resolveTableInlinePlacement(table, rowJustification);
19
+ if (placement.alignment === "center") return (frameWidth - tableWidth) / 2;
20
+ if (placement.alignment === "right") return frameWidth - tableWidth - placement.offset;
21
+ return placement.offset;
22
+ };
21
23
  //#endregion
22
- export { resolveTableInlinePlacement };
24
+ export { resolveTableInlineOffset, resolveTableInlinePlacement };
@@ -65,6 +65,28 @@ type PaginatorOptions = {
65
65
  type ForcePageBreakOptions = {
66
66
  coalesceBlankPage?: boolean;
67
67
  };
68
+ declare const SECTION_START_PLACEMENT: {
69
+ readonly CONTINUOUS: "continuous";
70
+ readonly NEXT_PAGE: "nextPage";
71
+ };
72
+ type SectionStartPlacement = (typeof SECTION_START_PLACEMENT)[keyof typeof SECTION_START_PLACEMENT];
73
+ type StartSectionOptions = {
74
+ sectionIndex: number;
75
+ pageNumbering?: SectionPageNumbering;
76
+ placement?: SectionStartPlacement;
77
+ };
78
+ /** Calculate active column widths, preferring authored unequal widths. */
79
+ declare function calculateColumnWidths(pageWidth: number, leftMargin: number, rightMargin: number, columns: ColumnLayout): number[];
80
+ /** Resolve every column's absolute page X in one pass. */
81
+ declare function calculateColumnLefts(leftMargin: number, columnWidths: readonly number[], columns: ColumnLayout): number[];
82
+ type ResolveColumnLeftOptions = {
83
+ leftMargin: number;
84
+ columnWidths: readonly number[];
85
+ columns: ColumnLayout;
86
+ columnIndex: number;
87
+ };
88
+ /** Resolve an active column's absolute page X from the shared width/gap model. */
89
+ declare function resolveColumnLeft({ leftMargin, columnWidths, columns, columnIndex }: ResolveColumnLeftOptions): number;
68
90
  /**
69
91
  * Creates a paginator for managing page layout state.
70
92
  */
@@ -108,6 +130,8 @@ declare function createPaginator(options: PaginatorOptions): {
108
130
  addFootnoteHeight: (additionalHeight: number, footnoteIds?: number[]) => void;
109
131
  /** Force a page break. */
110
132
  forcePageBreak: (breakOptions?: ForcePageBreakOptions) => PageState;
133
+ /** Reuse the current sheet while advancing authored page ordinals. */
134
+ coalescePageBreak: () => PageState;
111
135
  /** Reuse an already blank current page for the active section/layout. */
112
136
  retargetCurrentBlankPage: () => boolean;
113
137
  /** Force a column break. */
@@ -122,8 +146,8 @@ declare function createPaginator(options: PaginatorOptions): {
122
146
  h: number;
123
147
  }, newMargins?: PageMargins, applyImmediately?: boolean) => void;
124
148
  /** Mark the next created page as the first page of a new section. */
125
- startSection: (sectionIndex: number, pageNumbering?: SectionPageNumbering) => void;
149
+ startSection: ({ sectionIndex, pageNumbering, placement }: StartSectionOptions) => void;
126
150
  };
127
151
  type Paginator = ReturnType<typeof createPaginator>;
128
152
  //#endregion
129
- export { FOOTNOTE_SEPARATOR_HEIGHT, PageState, Paginator, PaginatorOptions, createPaginator };
153
+ export { FOOTNOTE_SEPARATOR_HEIGHT, PageState, Paginator, PaginatorOptions, SECTION_START_PLACEMENT, calculateColumnLefts, calculateColumnWidths, createPaginator, resolveColumnLeft };
@@ -8,6 +8,10 @@ import { panic } from "better-result";
8
8
  * Tracks the current page, cursor position, and available space.
9
9
  * Creates new pages when content doesn't fit.
10
10
  */
11
+ const SECTION_START_PLACEMENT = {
12
+ CONTINUOUS: "continuous",
13
+ NEXT_PAGE: "nextPage"
14
+ };
11
15
  /** Calculate active column widths, preferring authored unequal widths. */
12
16
  function calculateColumnWidths(pageWidth, leftMargin, rightMargin, columns) {
13
17
  if (columns.widths?.length === columns.count && columns.widths.every((width) => Number.isFinite(width) && width > 0)) return [...columns.widths];
@@ -15,6 +19,22 @@ function calculateColumnWidths(pageWidth, leftMargin, rightMargin, columns) {
15
19
  return Array.from({ length: columns.count }, () => equalWidth);
16
20
  }
17
21
  const gapAfterColumn = (columns, columnIndex) => columns.gaps?.[columnIndex] ?? columns.gap;
22
+ /** Resolve every column's absolute page X in one pass. */
23
+ function calculateColumnLefts(leftMargin, columnWidths, columns) {
24
+ const lefts = [];
25
+ let x = leftMargin;
26
+ for (let index = 0; index < columnWidths.length; index++) {
27
+ lefts.push(x);
28
+ x += (columnWidths[index] ?? 0) + gapAfterColumn(columns, index);
29
+ }
30
+ return lefts;
31
+ }
32
+ /** Resolve an active column's absolute page X from the shared width/gap model. */
33
+ function resolveColumnLeft({ leftMargin, columnWidths, columns, columnIndex }) {
34
+ let x = leftMargin;
35
+ for (let index = 0; index < columnIndex; index++) x += (columnWidths[index] ?? columnWidths[0] ?? 0) + gapAfterColumn(columns, index);
36
+ return x;
37
+ }
18
38
  function arePageSizesEqual(left, right) {
19
39
  return left.w === right.w && left.h === right.h;
20
40
  }
@@ -37,7 +57,10 @@ function createPaginator(options) {
37
57
  let currentSectionPageNumber = 0;
38
58
  let currentPageNumbering = options.pageNumbering ?? { type: "continue" };
39
59
  let nextLogicalPageNumber = currentPageNumbering.type === "restart" ? currentPageNumbering.start : 1;
40
- let sectionStartPending = true;
60
+ let sectionStart = {
61
+ type: "pending",
62
+ placement: "nextPage"
63
+ };
41
64
  const pages = [];
42
65
  const states = [];
43
66
  function getContentBottom() {
@@ -82,9 +105,12 @@ function createPaginator(options) {
82
105
  * Get X position for a given column index.
83
106
  */
84
107
  function getColumnX(columnIndex) {
85
- let x = states.at(-1)?.page.margins.left ?? getPageMargins(1, nextLogicalPageNumber).left;
86
- for (let index = 0; index < columnIndex; index++) x += (columnWidths[index] ?? columnWidths[0] ?? 0) + gapAfterColumn(columns, index);
87
- return x;
108
+ return resolveColumnLeft({
109
+ leftMargin: states.at(-1)?.page.margins.left ?? getPageMargins(1, nextLogicalPageNumber).left,
110
+ columnWidths,
111
+ columns,
112
+ columnIndex
113
+ });
88
114
  }
89
115
  /**
90
116
  * Create a new page and add it to the list.
@@ -94,7 +120,7 @@ function createPaginator(options) {
94
120
  const pageNumber = pages.length + 1;
95
121
  const logicalNumber = nextLogicalPageNumber;
96
122
  nextLogicalPageNumber += 1;
97
- sectionStartPending = false;
123
+ sectionStart = { type: "placed" };
98
124
  currentSectionPageNumber += 1;
99
125
  const pageMargins = getPageMargins(pageNumber, logicalNumber);
100
126
  const topMargin = pageMargins.top;
@@ -208,11 +234,14 @@ function createPaginator(options) {
208
234
  y
209
235
  };
210
236
  }
237
+ function consumeSharedSectionPage(state) {
238
+ if (sectionStart.type !== "pending" || sectionStart.placement !== "continuous" || state.page.sectionIndex === currentSectionIndex) return;
239
+ if (currentPageNumbering.type === "restart") nextLogicalPageNumber = currentPageNumbering.start + 1;
240
+ currentSectionPageNumber = 1;
241
+ sectionStart = { type: "placed" };
242
+ }
211
243
  function commitFragment(state, fragment) {
212
- if (sectionStartPending && state.page.sectionIndex !== currentSectionIndex) {
213
- if (currentPageNumbering.type === "restart") nextLogicalPageNumber = currentPageNumbering.start + 1;
214
- sectionStartPending = false;
215
- }
244
+ consumeSharedSectionPage(state);
216
245
  state.page.fragments.push(fragment);
217
246
  }
218
247
  function addUnflowedFragment(fragment) {
@@ -259,6 +288,7 @@ function createPaginator(options) {
259
288
  */
260
289
  function forcePageBreak(breakOptions = {}) {
261
290
  const current = states.at(-1);
291
+ if (current?.page.fragments.length) consumeSharedSectionPage(current);
262
292
  if (breakOptions.coalesceBlankPage && current && current.page.fragments.length === 0 && current.cursorY === current.topMargin) {
263
293
  if (current.page.sectionIndex !== currentSectionIndex) {
264
294
  retargetCurrentBlankPage();
@@ -268,6 +298,33 @@ function createPaginator(options) {
268
298
  }
269
299
  return createNewPage();
270
300
  }
301
+ function coalescePageBreak() {
302
+ const state = getCurrentState();
303
+ consumeSharedSectionPage(state);
304
+ const previousMargins = state.page.margins;
305
+ const logicalNumber = nextLogicalPageNumber;
306
+ nextLogicalPageNumber += 1;
307
+ currentSectionPageNumber += 1;
308
+ state.page.logicalNumber = logicalNumber;
309
+ if (currentPageNumbering.format === void 0) delete state.page.logicalNumberFormat;
310
+ else state.page.logicalNumberFormat = currentPageNumbering.format;
311
+ applySectionMetadata(state.page);
312
+ const pageMargins = getPageMargins(state.page.number, logicalNumber);
313
+ const xDelta = pageMargins.left - previousMargins.left;
314
+ const yDelta = pageMargins.top - previousMargins.top;
315
+ for (const fragment of state.page.fragments) {
316
+ fragment.x += xDelta;
317
+ fragment.y += yDelta;
318
+ }
319
+ state.page.margins = pageMargins;
320
+ state.topMargin = pageMargins.top;
321
+ state.cursorY += yDelta;
322
+ state.rawContentBottom = pageSize.h - pageMargins.bottom;
323
+ state.contentBottom = state.rawContentBottom - state.footnoteHeight;
324
+ columnRegionTop += yDelta;
325
+ columnRegionMaxBottom += yDelta;
326
+ return state;
327
+ }
271
328
  function retargetCurrentBlankPage() {
272
329
  const current = states.at(-1);
273
330
  if (!current || current.page.fragments.length > 0 || current.cursorY !== current.topMargin) return false;
@@ -334,20 +391,23 @@ function createPaginator(options) {
334
391
  pendingPageSize = void 0;
335
392
  pendingMargins = void 0;
336
393
  }
337
- function startSection(sectionIndex, pageNumbering = { type: "continue" }) {
394
+ function startSection({ sectionIndex, pageNumbering = { type: "continue" }, placement = SECTION_START_PLACEMENT.NEXT_PAGE }) {
338
395
  currentSectionIndex = sectionIndex;
339
396
  currentSectionPageNumber = 0;
340
397
  currentPageNumbering = pageNumbering;
341
- sectionStartPending = true;
398
+ sectionStart = {
399
+ type: "pending",
400
+ placement
401
+ };
342
402
  if (pageNumbering.type === "restart") nextLogicalPageNumber = pageNumbering.start;
343
403
  }
344
404
  function retargetSectionMetadata(page) {
345
405
  currentSectionPageNumber = 1;
346
- if (sectionStartPending && currentPageNumbering.type === "restart") {
406
+ if (sectionStart.type === "pending" && currentPageNumbering.type === "restart") {
347
407
  page.logicalNumber = currentPageNumbering.start;
348
408
  nextLogicalPageNumber = currentPageNumbering.start + 1;
349
409
  }
350
- sectionStartPending = false;
410
+ sectionStart = { type: "placed" };
351
411
  if (currentPageNumbering.format === void 0) delete page.logicalNumberFormat;
352
412
  else page.logicalNumberFormat = currentPageNumbering.format;
353
413
  applySectionMetadata(page);
@@ -393,6 +453,8 @@ function createPaginator(options) {
393
453
  addFootnoteHeight,
394
454
  /** Force a page break. */
395
455
  forcePageBreak,
456
+ /** Reuse the current sheet while advancing authored page ordinals. */
457
+ coalescePageBreak,
396
458
  /** Reuse an already blank current page for the active section/layout. */
397
459
  retargetCurrentBlankPage,
398
460
  /** Force a column break. */
@@ -408,4 +470,4 @@ function createPaginator(options) {
408
470
  };
409
471
  }
410
472
  //#endregion
411
- export { FOOTNOTE_SEPARATOR_HEIGHT, createPaginator };
473
+ export { FOOTNOTE_SEPARATOR_HEIGHT, SECTION_START_PLACEMENT, calculateColumnLefts, calculateColumnWidths, createPaginator, resolveColumnLeft };
@@ -12,6 +12,8 @@ type CollapseParagraphSpacingOptions = {
12
12
  declare const collapseParagraphSpacing: ({ before, after }: CollapseParagraphSpacingOptions) => number;
13
13
  /** Whether a paragraph has no visible run content. */
14
14
  declare function isEmptyParagraph(block: ParagraphBlock): boolean;
15
+ /** Whether a visually empty paragraph carries authored formatting semantics. */
16
+ declare const isAuthoredEmptyParagraph: (block: ParagraphBlock) => boolean;
15
17
  /**
16
18
  * Resolve leading paragraph spacing after applying the empty-paragraph
17
19
  * inherited-spacing collapse rule.
@@ -34,4 +36,4 @@ declare const resolveEffectiveParagraphSpacing: (block: ParagraphBlock) => Effec
34
36
  declare const resolveEffectiveParagraphSpacingTree: (blocks: FlowBlock[]) => FlowBlock[];
35
37
  declare const paragraphsShareStyle: (previous: ParagraphBlock, current: ParagraphBlock) => boolean;
36
38
  //#endregion
37
- export { collapseParagraphSpacing, getParagraphSpacingAfter, getParagraphSpacingBefore, isEmptyParagraph, paragraphsShareStyle, resolveEffectiveParagraphSpacing, resolveEffectiveParagraphSpacingTree };
39
+ export { collapseParagraphSpacing, getParagraphSpacingAfter, getParagraphSpacingBefore, isAuthoredEmptyParagraph, isEmptyParagraph, paragraphsShareStyle, resolveEffectiveParagraphSpacing, resolveEffectiveParagraphSpacingTree };
@@ -9,6 +9,8 @@ function isEmptyParagraph(block) {
9
9
  const run = block.runs.at(0);
10
10
  return run?.kind === "text" && run.text === "";
11
11
  }
12
+ /** Whether a visually empty paragraph carries authored formatting semantics. */
13
+ const isAuthoredEmptyParagraph = (block) => isEmptyParagraph(block) && (block.attrs?.styleId !== void 0 || block.attrs?.hasDirectParagraphFormatting === true || block.attrs?.hasDirectParagraphMarkFormatting === true);
12
14
  /**
13
15
  * Resolve leading paragraph spacing after applying the empty-paragraph
14
16
  * inherited-spacing collapse rule.
@@ -23,7 +25,7 @@ const getParagraphSpacingAfter = (block) => resolveEffectiveParagraphSpacing(blo
23
25
  const resolveEffectiveParagraphSpacing = (block) => {
24
26
  const before = block.attrs?.spacing?.before ?? 0;
25
27
  const after = block.attrs?.spacing?.after ?? 0;
26
- if (!isEmptyParagraph(block) || preservesInheritedEmptySpacing(block)) return {
28
+ if (!isEmptyParagraph(block) || isAuthoredEmptyParagraph(block)) return {
27
29
  before,
28
30
  after
29
31
  };
@@ -49,7 +51,6 @@ const resolveEffectiveParagraphSpacingTree = (blocks) => {
49
51
  return changed ? resolved : blocks;
50
52
  };
51
53
  const paragraphsShareStyle = (previous, current) => previous.attrs?.styleId === current.attrs?.styleId;
52
- const preservesInheritedEmptySpacing = (block) => Boolean(block.attrs?.styleId || block.attrs?.hasDirectParagraphFormatting || block.attrs?.hasDirectParagraphMarkFormatting);
53
54
  const resolveBlockSpacing = (blocks, index) => {
54
55
  const block = blocks[index];
55
56
  if (block.kind === "paragraph") return resolveParagraphSequenceSpacing(blocks, index, block);
@@ -121,4 +122,4 @@ const resolveTextBoxSpacing = (block) => {
121
122
  };
122
123
  };
123
124
  //#endregion
124
- export { collapseParagraphSpacing, getParagraphSpacingAfter, getParagraphSpacingBefore, isEmptyParagraph, paragraphsShareStyle, resolveEffectiveParagraphSpacing, resolveEffectiveParagraphSpacingTree };
125
+ export { collapseParagraphSpacing, getParagraphSpacingAfter, getParagraphSpacingBefore, isAuthoredEmptyParagraph, isEmptyParagraph, paragraphsShareStyle, resolveEffectiveParagraphSpacing, resolveEffectiveParagraphSpacingTree };
@@ -8,7 +8,12 @@ type RenderedBreakState = {
8
8
  } | {
9
9
  type: "pageAdvance";
10
10
  reason: "authoredBoundary";
11
- boundary: "hardBreak" | "sectionBreak";
11
+ boundary: "hardBreak";
12
+ } | {
13
+ type: "pageAdvance";
14
+ reason: "authoredBoundary";
15
+ boundary: "sectionBreak";
16
+ nextHardBreak: "coalesce" | "advance";
12
17
  };
13
18
  declare const INITIAL_RENDERED_BREAK_STATE: RenderedBreakState;
14
19
  type ReconcileBeforeBlockOptions = {
@@ -20,8 +25,14 @@ type ReconcileBeforeBlockOptions = {
20
25
  hasExplicitPageBreak: boolean;
21
26
  renderedBreakNeedsSnap: boolean;
22
27
  };
28
+ declare const PAGE_ADVANCE: {
29
+ readonly COALESCED: "coalesced";
30
+ readonly NONE: "none";
31
+ readonly PHYSICAL: "physical";
32
+ };
33
+ type PageAdvance = (typeof PAGE_ADVANCE)[keyof typeof PAGE_ADVANCE];
23
34
  type BreakDecision = {
24
- forcePageBreak: boolean;
35
+ pageAdvance: PageAdvance;
25
36
  suppressSpaceBefore: boolean;
26
37
  state: RenderedBreakState;
27
38
  };
@@ -45,4 +56,4 @@ type ReconcileAfterBlockOptions = {
45
56
  /** Record page movement and carry authored boundaries across pagination-empty paragraphs. */
46
57
  declare const reconcileAfterBlock: ({ state, block, pageNumberBefore, pageNumberAfter, previousPage }: ReconcileAfterBlockOptions) => RenderedBreakState;
47
58
  //#endregion
48
- export { INITIAL_RENDERED_BREAK_STATE, RenderedBreakState, reconcileAfterBlock, reconcileBreakBeforeBlock, recordReflowBoundary };
59
+ export { INITIAL_RENDERED_BREAK_STATE, PAGE_ADVANCE, RenderedBreakState, reconcileAfterBlock, reconcileBreakBeforeBlock, recordReflowBoundary };
@@ -1,6 +1,11 @@
1
1
  import { continuesNumberedSequence, continuesTabbedParagraphSequence } from "./paragraphSequence.js";
2
2
  //#region src/layout-engine/renderedBreakReconciliation.ts
3
3
  const INITIAL_RENDERED_BREAK_STATE = { type: "noPageAdvance" };
4
+ const PAGE_ADVANCE = {
5
+ COALESCED: "coalesced",
6
+ NONE: "none",
7
+ PHYSICAL: "physical"
8
+ };
4
9
  /**
5
10
  * Reconcile an authored or cached break before laying out one block.
6
11
  *
@@ -10,13 +15,21 @@ const INITIAL_RENDERED_BREAK_STATE = { type: "noPageAdvance" };
10
15
  * independent decisions.
11
16
  */
12
17
  const reconcileBreakBeforeBlock = ({ state, block, previousBlock, page, blocksById, hasExplicitPageBreak, renderedBreakNeedsSnap }) => {
13
- if (hasExplicitPageBreak) return {
14
- forcePageBreak: !(state.type === "pageAdvance" && state.reason === "authoredBoundary" && state.boundary === "sectionBreak" && previousBlock?.kind === "sectionBreak" && previousBlock.type !== "continuous"),
15
- suppressSpaceBefore: false,
16
- state: INITIAL_RENDERED_BREAK_STATE
17
- };
18
+ if (hasExplicitPageBreak) {
19
+ const sectionBoundaryAlreadyOpenedPage = state.type === "pageAdvance" && state.reason === "authoredBoundary" && state.boundary === "sectionBreak" && state.nextHardBreak === "coalesce" && !pageHasVisibleBodyContent(page, blocksById);
20
+ return {
21
+ pageAdvance: sectionBoundaryAlreadyOpenedPage ? PAGE_ADVANCE.COALESCED : PAGE_ADVANCE.PHYSICAL,
22
+ suppressSpaceBefore: false,
23
+ state: sectionBoundaryAlreadyOpenedPage ? {
24
+ type: "pageAdvance",
25
+ reason: "authoredBoundary",
26
+ boundary: "sectionBreak",
27
+ nextHardBreak: "advance"
28
+ } : INITIAL_RENDERED_BREAK_STATE
29
+ };
30
+ }
18
31
  if (block.kind !== "paragraph" || block.attrs?.renderedPageBreakBefore !== true) return {
19
- forcePageBreak: false,
32
+ pageAdvance: PAGE_ADVANCE.NONE,
20
33
  suppressSpaceBefore: false,
21
34
  state
22
35
  };
@@ -27,9 +40,10 @@ const reconcileBreakBeforeBlock = ({ state, block, previousBlock, page, blocksBy
27
40
  const followsPageBreakingSection = followsSectionBreak && previousBlock.type !== "continuous";
28
41
  const followsCarriedSectionBoundary = state.type === "pageAdvance" && state.reason === "authoredBoundary" && state.boundary === "sectionBreak";
29
42
  const preserveSectionLeadingSpacing = (followsSectionBreak || followsCarriedSectionBoundary) && block.attrs?.spacingExplicit?.before === true;
43
+ const markerAccountsForBoundary = forcePageBreak || markerAlreadySatisfied || followsAuthoredPageBreak || followsPageBreakingSection;
30
44
  return {
31
- forcePageBreak,
32
- suppressSpaceBefore: (forcePageBreak || markerAlreadySatisfied || followsAuthoredPageBreak || followsPageBreakingSection) && !preserveSectionLeadingSpacing,
45
+ pageAdvance: forcePageBreak ? PAGE_ADVANCE.PHYSICAL : PAGE_ADVANCE.NONE,
46
+ suppressSpaceBefore: markerAccountsForBoundary && !preserveSectionLeadingSpacing,
33
47
  state: INITIAL_RENDERED_BREAK_STATE
34
48
  };
35
49
  };
@@ -41,16 +55,33 @@ const recordReflowBoundary = (state, crossedPageBoundary) => crossedPageBoundary
41
55
  const reconcileAfterBlock = ({ state, block, pageNumberBefore, pageNumberAfter, previousPage }) => {
42
56
  if (isStructuralBreak(block)) {
43
57
  if (pageNumberAfter <= pageNumberBefore) {
44
- if (state.type === "pageAdvance" && state.reason === "authoredBoundary") return block.kind === "sectionBreak" ? {
45
- ...state,
46
- boundary: "sectionBreak"
47
- } : state;
58
+ if (state.type === "pageAdvance" && state.reason === "authoredBoundary") {
59
+ if (block.kind !== "sectionBreak") return state.boundary === "sectionBreak" ? {
60
+ type: "pageAdvance",
61
+ reason: "authoredBoundary",
62
+ boundary: "sectionBreak",
63
+ nextHardBreak: "advance"
64
+ } : state;
65
+ if (state.boundary === "sectionBreak") return state;
66
+ return {
67
+ type: "pageAdvance",
68
+ reason: "authoredBoundary",
69
+ boundary: "sectionBreak",
70
+ nextHardBreak: "advance"
71
+ };
72
+ }
48
73
  return INITIAL_RENDERED_BREAK_STATE;
49
74
  }
75
+ if (block.kind === "sectionBreak") return {
76
+ type: "pageAdvance",
77
+ reason: "authoredBoundary",
78
+ boundary: "sectionBreak",
79
+ nextHardBreak: "coalesce"
80
+ };
50
81
  return {
51
82
  type: "pageAdvance",
52
83
  reason: "authoredBoundary",
53
- boundary: block.kind === "sectionBreak" ? "sectionBreak" : "hardBreak"
84
+ boundary: "hardBreak"
54
85
  };
55
86
  }
56
87
  if (block.kind === "paragraph" && isPaginationEmptyParagraph(block)) return state;
@@ -83,4 +114,4 @@ const pageStartsWithPreviousParagraphContinuation = (page, previousBlock) => {
83
114
  };
84
115
  const isStructuralBreak = (block) => block.kind === "pageBreak" || block.kind === "columnBreak" || block.kind === "sectionBreak";
85
116
  //#endregion
86
- export { INITIAL_RENDERED_BREAK_STATE, reconcileAfterBlock, reconcileBreakBeforeBlock, recordReflowBoundary };
117
+ export { INITIAL_RENDERED_BREAK_STATE, PAGE_ADVANCE, reconcileAfterBlock, reconcileBreakBeforeBlock, recordReflowBoundary };
@@ -53,6 +53,8 @@ type BreakDecision = {
53
53
  /** Required page parity (even or odd). */
54
54
  requiredParity?: "even" | "odd";
55
55
  };
56
+ declare const DEFAULT_SECTION_BREAK_TYPE = "nextPage";
57
+ declare function normalizeSectionBreakType(type: SectionBreakBlock["type"]): "continuous" | "nextPage" | "evenPage" | "oddPage";
56
58
  /**
57
59
  * Create initial section state from default options.
58
60
  */
@@ -92,4 +94,4 @@ declare function getEffectivePageSize(state: SectionState): {
92
94
  */
93
95
  declare function getEffectiveColumns(state: SectionState): ColumnLayout;
94
96
  //#endregion
95
- export { BreakDecision, SectionState, applyPendingToActive, createInitialSectionState, getEffectiveColumns, getEffectiveMargins, getEffectivePageSize, scheduleSectionBreak };
97
+ export { BreakDecision, DEFAULT_SECTION_BREAK_TYPE, SectionState, applyPendingToActive, createInitialSectionState, getEffectiveColumns, getEffectiveMargins, getEffectivePageSize, normalizeSectionBreakType, scheduleSectionBreak };
@@ -6,6 +6,10 @@ const DEFAULT_COLUMNS = {
6
6
  count: 1,
7
7
  gap: 0
8
8
  };
9
+ const DEFAULT_SECTION_BREAK_TYPE = "nextPage";
10
+ function normalizeSectionBreakType(type) {
11
+ return type ?? "nextPage";
12
+ }
9
13
  /**
10
14
  * Create initial section state from default options.
11
15
  */
@@ -52,7 +56,7 @@ function getColumnConfig(columns) {
52
56
  */
53
57
  function scheduleSectionBreak(block, state, _baseMargins) {
54
58
  const next = { ...state };
55
- const sectionType = block.type ?? "continuous";
59
+ const sectionType = normalizeSectionBreakType(block.type);
56
60
  const sectionMargins = block.margins;
57
61
  const sectionPageSize = block.pageSize;
58
62
  const sectionOrientation = block.orientation;
@@ -179,4 +183,4 @@ function getEffectiveColumns(state) {
179
183
  return state.pendingColumns ?? state.activeColumns;
180
184
  }
181
185
  //#endregion
182
- export { applyPendingToActive, createInitialSectionState, getEffectiveColumns, getEffectiveMargins, getEffectivePageSize, scheduleSectionBreak };
186
+ export { DEFAULT_SECTION_BREAK_TYPE, applyPendingToActive, createInitialSectionState, getEffectiveColumns, getEffectiveMargins, getEffectivePageSize, normalizeSectionBreakType, scheduleSectionBreak };