@stll/folio-core 0.6.1 → 0.8.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/blockRange.d.ts +13 -2
  2. package/dist/ai-edits/blockRange.js +23 -2
  3. package/dist/ai-edits/index.d.ts +4 -2
  4. package/dist/ai-edits/index.js +3 -1
  5. package/dist/ai-edits/scoped-reading.d.ts +9 -0
  6. package/dist/ai-edits/scoped-reading.js +60 -0
  7. package/dist/ai-edits/snapshot.js +13 -4
  8. package/dist/ai-edits/types.d.ts +44 -2
  9. package/dist/compat/eigenpal.d.ts +5 -2
  10. package/dist/compat/eigenpal.js +4 -1
  11. package/dist/controller/fontReadiness.d.ts +29 -0
  12. package/dist/controller/fontReadiness.js +139 -0
  13. package/dist/controller/layoutPipeline.js +86 -11
  14. package/dist/docx/blockContentParser.js +51 -8
  15. package/dist/docx/capabilities.d.ts +41 -0
  16. package/dist/docx/capabilities.js +322 -0
  17. package/dist/docx/compatibility.d.ts +34 -2
  18. package/dist/docx/compatibility.js +128 -38
  19. package/dist/docx/conformance.d.ts +6 -0
  20. package/dist/docx/conformance.js +18 -0
  21. package/dist/docx/ensureParaIds.d.ts +29 -0
  22. package/dist/docx/ensureParaIds.js +423 -0
  23. package/dist/docx/groupDrawingParser.d.ts +1 -1
  24. package/dist/docx/groupDrawingParser.js +49 -8
  25. package/dist/docx/numberingParser.js +1 -0
  26. package/dist/docx/paragraphParser.js +9 -0
  27. package/dist/docx/paragraphTextBoxEnrichment.js +141 -2
  28. package/dist/docx/parser.js +2 -0
  29. package/dist/docx/runParser.js +11 -2
  30. package/dist/docx/selectiveXmlPatch.d.ts +10 -1
  31. package/dist/docx/selectiveXmlPatch.js +1 -1
  32. package/dist/docx/serializer/paragraphSerializer.js +4 -0
  33. package/dist/docx/serializer/runSerializer.js +7 -2
  34. package/dist/docx/serializer/tableSerializer.js +4 -0
  35. package/dist/docx/server/boundedArchive.d.ts +24 -0
  36. package/dist/docx/server/boundedArchive.js +106 -0
  37. package/dist/docx/server/extractDocxText.d.ts +23 -0
  38. package/dist/docx/server/extractDocxText.js +154 -0
  39. package/dist/docx/tableParser.js +8 -0
  40. package/dist/docx/textBoxParser.js +6 -1
  41. package/dist/docx/vmlImageParser.js +145 -1
  42. package/dist/index.d.ts +5 -2
  43. package/dist/index.js +4 -1
  44. package/dist/layout-bridge/convert/headerFooterLayout.js +16 -2
  45. package/dist/layout-bridge/convert/paragraphFrames.d.ts +7 -0
  46. package/dist/layout-bridge/convert/paragraphFrames.js +136 -0
  47. package/dist/layout-bridge/convert/toFlowBlocks.js +106 -13
  48. package/dist/layout-bridge/engine/hitTest.js +2 -1
  49. package/dist/layout-bridge/engine/selectionRects.js +2 -1
  50. package/dist/layout-engine/headerFooterRefs.d.ts +7 -0
  51. package/dist/layout-engine/headerFooterRefs.js +40 -0
  52. package/dist/layout-engine/index.d.ts +3 -2
  53. package/dist/layout-engine/index.js +125 -72
  54. package/dist/layout-engine/keep-together.d.ts +7 -5
  55. package/dist/layout-engine/keep-together.js +20 -4
  56. package/dist/layout-engine/measure/cache.js +2 -0
  57. package/dist/layout-engine/measure/listMarkerWidth.d.ts +3 -1
  58. package/dist/layout-engine/measure/listMarkerWidth.js +23 -4
  59. package/dist/layout-engine/measure/measureBlocks.d.ts +3 -0
  60. package/dist/layout-engine/measure/measureBlocks.js +133 -51
  61. package/dist/layout-engine/measure/measureParagraph.js +42 -15
  62. package/dist/layout-engine/measure/tableCellGrid.d.ts +15 -0
  63. package/dist/layout-engine/measure/tableCellGrid.js +62 -0
  64. package/dist/layout-engine/measure/textBoxParagraphLayout.d.ts +18 -0
  65. package/dist/layout-engine/measure/textBoxParagraphLayout.js +32 -0
  66. package/dist/layout-engine/paginator.d.ts +2 -1
  67. package/dist/layout-engine/paginator.js +17 -8
  68. package/dist/layout-engine/paragraphFrame.d.ts +22 -0
  69. package/dist/layout-engine/paragraphFrame.js +17 -0
  70. package/dist/layout-engine/paragraphSequence.d.ts +7 -0
  71. package/dist/layout-engine/paragraphSequence.js +25 -0
  72. package/dist/layout-engine/paragraphSpacing.d.ts +17 -0
  73. package/dist/layout-engine/paragraphSpacing.js +30 -0
  74. package/dist/layout-engine/renderedBreakReconciliation.d.ts +58 -0
  75. package/dist/layout-engine/renderedBreakReconciliation.js +63 -0
  76. package/dist/layout-engine/types.d.ts +31 -10
  77. package/dist/layout-engine/types.js +15 -2
  78. package/dist/layout-painter/renderPage.js +2 -2
  79. package/dist/layout-painter/renderParagraph.js +94 -10
  80. package/dist/layout-painter/renderTable.js +42 -12
  81. package/dist/layout-painter/renderTextBox.js +17 -7
  82. package/dist/managers/DocumentLoaderManager.js +1 -1
  83. package/dist/paged-layout/sectionBlockWidths.d.ts +3 -0
  84. package/dist/paged-layout/sectionBlockWidths.js +9 -0
  85. package/dist/paged-layout/sectionGeometry.js +1 -1
  86. package/dist/prosemirror/attrs/index.js +11 -0
  87. package/dist/prosemirror/conversion/fromProseDoc.js +2 -0
  88. package/dist/prosemirror/conversion/toProseDoc.js +29 -23
  89. package/dist/prosemirror/extensions/core/ParagraphExtension.js +1 -0
  90. package/dist/prosemirror/extensions/features/ListExtension.js +1 -0
  91. package/dist/prosemirror/extensions/features/ParaIdAllocatorExtension.js +68 -16
  92. package/dist/prosemirror/extensions/nodes/TableExtension.js +1 -0
  93. package/dist/prosemirror/extensions/nodes/TextBoxExtension.d.ts +2 -1
  94. package/dist/prosemirror/extensions/nodes/TextBoxExtension.js +7 -0
  95. package/dist/prosemirror/schema/nodes.d.ts +6 -3
  96. package/dist/prosemirror/styles/resolvedStyleAttrs.js +1 -0
  97. package/dist/prosemirror/utils/tabCalculator.js +1 -1
  98. package/dist/server.d.ts +6 -2
  99. package/dist/server.js +5 -1
  100. package/dist/types/index.d.ts +2 -1
  101. package/dist/utils/createDocument.js +26 -18
  102. package/dist/utils/formatToStyle.js +3 -3
  103. package/dist/utils/hexId.d.ts +8 -1
  104. package/dist/utils/hexId.js +11 -3
  105. package/dist/utils/units.d.ts +6 -6
  106. package/dist/utils/units.js +8 -8
  107. package/package.json +1 -1
@@ -47,7 +47,7 @@ function createPaginator(options) {
47
47
  }
48
48
  function currentPageUsesActiveLayout(state) {
49
49
  if (pendingPageSize || pendingMargins) return false;
50
- return arePageSizesEqual(state.page.size, pageSize) && areMarginsEqual(state.page.margins, getPageMargins(state.page.number));
50
+ return arePageSizesEqual(state.page.size, pageSize) && areMarginsEqual(state.page.margins, getPageMargins(state.page.number, state.page.sectionPageNumber ?? currentSectionPageNumber));
51
51
  }
52
52
  if (getContentHeight() <= 0) panic("Paginator: page size and margins yield no content area");
53
53
  let columnWidths = calculateColumnWidths(pageSize.w, margins.left, margins.right, columns);
@@ -62,8 +62,9 @@ function createPaginator(options) {
62
62
  if (getContentHeight() <= 0) panic("Paginator: section page size and margins yield no content area");
63
63
  recalculateColumnWidths();
64
64
  }
65
- function getPageMargins(pageNumber) {
66
- const pageMargins = pageNumber === 1 && options.firstPageMargins ? { ...options.firstPageMargins } : { ...margins };
65
+ function getPageMargins(pageNumber, sectionPageNumber) {
66
+ const evenMargins = sectionPageNumber % 2 === 0 ? options.sectionEvenPageMargins?.[currentSectionIndex] : void 0;
67
+ const pageMargins = pageNumber === 1 && options.firstPageMargins ? { ...options.firstPageMargins } : { ...evenMargins ?? margins };
67
68
  if (options.mirrorMargins === true && pageNumber % 2 === 0) return {
68
69
  ...pageMargins,
69
70
  left: pageMargins.right,
@@ -72,11 +73,12 @@ function createPaginator(options) {
72
73
  return pageMargins;
73
74
  }
74
75
  let columnRegionTop = margins.top;
76
+ let columnRegionMaxBottom = margins.top;
75
77
  /**
76
78
  * Get X position for a given column index.
77
79
  */
78
80
  function getColumnX(columnIndex) {
79
- let x = states.at(-1)?.page.margins.left ?? getPageMargins(1).left;
81
+ let x = states.at(-1)?.page.margins.left ?? getPageMargins(1, 1).left;
80
82
  for (let index = 0; index < columnIndex; index++) x += (columnWidths[index] ?? columnWidths[0] ?? 0) + gapAfterColumn(columns, index);
81
83
  return x;
82
84
  }
@@ -87,7 +89,7 @@ function createPaginator(options) {
87
89
  if (pendingPageSize || pendingMargins) applyPendingLayout();
88
90
  const pageNumber = pages.length + 1;
89
91
  currentSectionPageNumber += 1;
90
- const pageMargins = getPageMargins(pageNumber);
92
+ const pageMargins = getPageMargins(pageNumber, currentSectionPageNumber);
91
93
  const topMargin = pageMargins.top;
92
94
  const contentBottom = pageSize.h - pageMargins.bottom;
93
95
  const footnoteHeight = options.footnoteReservedHeights?.get(pageNumber) ?? 0;
@@ -114,6 +116,7 @@ function createPaginator(options) {
114
116
  pages.push(page);
115
117
  states.push(state);
116
118
  columnRegionTop = topMargin;
119
+ columnRegionMaxBottom = topMargin;
117
120
  if (options.onNewPage) options.onNewPage(state);
118
121
  return state;
119
122
  }
@@ -143,6 +146,7 @@ function createPaginator(options) {
143
146
  */
144
147
  function advanceColumn(state) {
145
148
  if (state.columnIndex < columns.count - 1) {
149
+ columnRegionMaxBottom = Math.max(columnRegionMaxBottom, state.cursorY);
146
150
  state.columnIndex += 1;
147
151
  state.cursorY = columnRegionTop;
148
152
  state.trailingSpacing = 0;
@@ -159,7 +163,7 @@ function createPaginator(options) {
159
163
  let state = getCurrentState();
160
164
  const safeHeight = Number.isFinite(height) && height > 0 ? height : 0;
161
165
  while (!fits(safeHeight, state)) {
162
- if (safeHeight > state.contentBottom - state.topMargin) {
166
+ if (safeHeight > state.contentBottom - columnRegionTop) {
163
167
  if (state.cursorY !== state.topMargin) state = advanceColumn(state);
164
168
  return state;
165
169
  }
@@ -241,7 +245,7 @@ function createPaginator(options) {
241
245
  const current = states.at(-1);
242
246
  if (!current || current.page.fragments.length > 0 || current.cursorY !== current.topMargin) return false;
243
247
  if (pendingPageSize || pendingMargins) applyPendingLayout();
244
- const pageMargins = getPageMargins(current.page.number);
248
+ const pageMargins = getPageMargins(current.page.number, 1);
245
249
  const topMargin = pageMargins.top;
246
250
  const rawContentBottom = pageSize.h - pageMargins.bottom;
247
251
  const footnoteHeight = options.footnoteReservedHeights?.get(current.page.number) ?? 0;
@@ -276,12 +280,17 @@ function createPaginator(options) {
276
280
  * column advancement stays below existing content (for continuous breaks).
277
281
  */
278
282
  function updateColumns(newColumns) {
283
+ const previousColumnCount = columns.count;
284
+ const state = getCurrentState();
285
+ const previousRegionBottom = Math.max(columnRegionMaxBottom, state.cursorY);
279
286
  columns = newColumns;
280
287
  recalculateColumnWidths();
281
- const state = getCurrentState();
282
288
  if (columns.count > 1) state.page.columns = { ...columns };
283
289
  else delete state.page.columns;
290
+ state.contentBottom = state.rawContentBottom - state.footnoteHeight;
291
+ if (previousColumnCount > 1) state.cursorY = previousRegionBottom;
284
292
  columnRegionTop = state.cursorY;
293
+ columnRegionMaxBottom = state.cursorY;
285
294
  state.columnIndex = 0;
286
295
  }
287
296
  function updatePageLayout(newPageSize, newMargins, applyImmediately = true) {
@@ -0,0 +1,22 @@
1
+ import { ParagraphBlock, TextBoxBlock } from "./types.js";
2
+
3
+ //#region src/layout-engine/paragraphFrame.d.ts
4
+ type ParagraphFrame = {
5
+ width?: number;
6
+ height?: number;
7
+ hSpace?: number;
8
+ vSpace?: number;
9
+ hAnchor?: "text" | "margin" | "page";
10
+ vAnchor?: "text" | "margin" | "page";
11
+ x?: number;
12
+ y?: number;
13
+ xAlign?: "left" | "center" | "right" | "inside" | "outside";
14
+ yAlign?: "top" | "center" | "bottom" | "inside" | "outside" | "inline";
15
+ wrap?: "around" | "auto" | "none" | "notBeside" | "through" | "tight";
16
+ };
17
+ declare function setParagraphFrame(block: ParagraphBlock, frame: ParagraphFrame): void;
18
+ declare function getParagraphFrame(block: ParagraphBlock): ParagraphFrame | undefined;
19
+ declare function markParagraphFrameTextBox(block: TextBoxBlock): void;
20
+ declare function isParagraphFrameTextBox(block: TextBoxBlock): boolean;
21
+ //#endregion
22
+ export { ParagraphFrame, getParagraphFrame, isParagraphFrameTextBox, markParagraphFrameTextBox, setParagraphFrame };
@@ -0,0 +1,17 @@
1
+ //#region src/layout-engine/paragraphFrame.ts
2
+ const paragraphFrames = /* @__PURE__ */ new WeakMap();
3
+ const paragraphFrameTextBoxes = /* @__PURE__ */ new WeakSet();
4
+ function setParagraphFrame(block, frame) {
5
+ paragraphFrames.set(block, frame);
6
+ }
7
+ function getParagraphFrame(block) {
8
+ return paragraphFrames.get(block);
9
+ }
10
+ function markParagraphFrameTextBox(block) {
11
+ paragraphFrameTextBoxes.add(block);
12
+ }
13
+ function isParagraphFrameTextBox(block) {
14
+ return paragraphFrameTextBoxes.has(block);
15
+ }
16
+ //#endregion
17
+ export { getParagraphFrame, isParagraphFrameTextBox, markParagraphFrameTextBox, setParagraphFrame };
@@ -0,0 +1,7 @@
1
+ import { FlowBlock } from "./types.js";
2
+
3
+ //#region src/layout-engine/paragraphSequence.d.ts
4
+ declare const continuesNumberedSequence: (previous: FlowBlock | undefined, current: FlowBlock) => boolean;
5
+ declare const continuesTabbedParagraphSequence: (previous: FlowBlock | undefined, current: FlowBlock) => boolean;
6
+ //#endregion
7
+ export { continuesNumberedSequence, continuesTabbedParagraphSequence };
@@ -0,0 +1,25 @@
1
+ //#region src/layout-engine/paragraphSequence.ts
2
+ const continuesNumberedSequence = (previous, current) => {
3
+ if (previous?.kind !== "paragraph" || current.kind !== "paragraph") return false;
4
+ const previousNumPr = previous.attrs?.numPr;
5
+ const currentNumPr = current.attrs?.numPr;
6
+ if (previousNumPr?.numId === void 0 || currentNumPr?.numId === void 0) return false;
7
+ return previousNumPr.numId === currentNumPr.numId && previousNumPr.ilvl === currentNumPr.ilvl;
8
+ };
9
+ const continuesTabbedParagraphSequence = (previous, current) => {
10
+ if (previous?.kind !== "paragraph" || current.kind !== "paragraph") return false;
11
+ if (!previous.attrs?.styleId || previous.attrs.styleId !== current.attrs?.styleId) return false;
12
+ if (!previous.runs.some((run) => run.kind === "tab") || !current.runs.some((run) => run.kind === "tab")) return false;
13
+ const previousIndent = previous.attrs.indent;
14
+ const currentIndent = current.attrs.indent;
15
+ if (previousIndent?.left !== currentIndent?.left || previousIndent?.right !== currentIndent?.right || previousIndent?.firstLine !== currentIndent?.firstLine || previousIndent?.hanging !== currentIndent?.hanging) return false;
16
+ const previousTabs = previous.attrs.tabs ?? [];
17
+ const currentTabs = current.attrs.tabs ?? [];
18
+ return previousTabs.length === currentTabs.length && previousTabs.every((tab, index) => {
19
+ const currentTab = currentTabs[index];
20
+ if (!currentTab) return false;
21
+ return tab.val === currentTab.val && tab.pos === currentTab.pos && tab.leader === currentTab.leader;
22
+ });
23
+ };
24
+ //#endregion
25
+ export { continuesNumberedSequence, continuesTabbedParagraphSequence };
@@ -0,0 +1,17 @@
1
+ import { ParagraphBlock } from "./types.js";
2
+
3
+ //#region src/layout-engine/paragraphSpacing.d.ts
4
+ /** Whether a paragraph has no visible run content. */
5
+ declare function isEmptyParagraph(block: ParagraphBlock): boolean;
6
+ /**
7
+ * Resolve leading paragraph spacing after applying the empty-paragraph
8
+ * inherited-spacing collapse rule.
9
+ */
10
+ declare function getParagraphSpacingBefore(block: ParagraphBlock): number;
11
+ /**
12
+ * Resolve trailing paragraph spacing after applying the empty-paragraph
13
+ * inherited-spacing collapse rule.
14
+ */
15
+ declare function getParagraphSpacingAfter(block: ParagraphBlock): number;
16
+ //#endregion
17
+ export { getParagraphSpacingAfter, getParagraphSpacingBefore, isEmptyParagraph };
@@ -0,0 +1,30 @@
1
+ //#region src/layout-engine/paragraphSpacing.ts
2
+ /** Whether a paragraph has no visible run content. */
3
+ function isEmptyParagraph(block) {
4
+ if (block.runs.length === 0) return true;
5
+ if (block.runs.length !== 1) return false;
6
+ const run = block.runs.at(0);
7
+ return run?.kind === "text" && run.text === "";
8
+ }
9
+ /**
10
+ * Resolve leading paragraph spacing after applying the empty-paragraph
11
+ * inherited-spacing collapse rule.
12
+ */
13
+ function getParagraphSpacingBefore(block) {
14
+ const value = block.attrs?.spacing?.before ?? 0;
15
+ if (value === 0) return 0;
16
+ if (isEmptyParagraph(block) && !block.attrs?.styleId && !block.attrs?.hasDirectParagraphFormatting && !block.attrs?.hasDirectParagraphMarkFormatting && !block.attrs?.spacingExplicit?.before) return 0;
17
+ return value;
18
+ }
19
+ /**
20
+ * Resolve trailing paragraph spacing after applying the empty-paragraph
21
+ * inherited-spacing collapse rule.
22
+ */
23
+ function getParagraphSpacingAfter(block) {
24
+ const value = block.attrs?.spacing?.after ?? 0;
25
+ if (value === 0) return 0;
26
+ if (isEmptyParagraph(block) && !block.attrs?.styleId && !block.attrs?.hasDirectParagraphFormatting && !block.attrs?.hasDirectParagraphMarkFormatting && !block.attrs?.spacingExplicit?.after) return 0;
27
+ return value;
28
+ }
29
+ //#endregion
30
+ export { getParagraphSpacingAfter, getParagraphSpacingBefore, isEmptyParagraph };
@@ -0,0 +1,58 @@
1
+ import { FlowBlock, Page } from "./types.js";
2
+
3
+ //#region src/layout-engine/renderedBreakReconciliation.d.ts
4
+ type RenderedBreakState = {
5
+ type: "noPageAdvance";
6
+ } | {
7
+ type: "pageAdvance";
8
+ reason: "ordinary" | "reflowBoundary";
9
+ };
10
+ declare const INITIAL_RENDERED_BREAK_STATE: RenderedBreakState;
11
+ type ReconcileBeforeBlockOptions = {
12
+ state: RenderedBreakState;
13
+ block: FlowBlock;
14
+ previousBlock: FlowBlock | undefined;
15
+ page: Page;
16
+ blocksById: ReadonlyMap<string, FlowBlock>;
17
+ hasExplicitPageBreak: boolean;
18
+ renderedBreakNeedsSnap: boolean;
19
+ };
20
+ type BreakDecision = {
21
+ forcePageBreak: boolean;
22
+ state: RenderedBreakState;
23
+ };
24
+ /**
25
+ * Reconcile an authored or cached break before laying out one block.
26
+ *
27
+ * Cached markers describe a boundary at a paragraph, not an ordinal page.
28
+ * Natural reflow may therefore satisfy the marker before layout reaches it.
29
+ * Every marker consumes the accumulated advance state so later markers make
30
+ * independent decisions.
31
+ */
32
+ declare const reconcileBreakBeforeBlock: ({
33
+ state,
34
+ block,
35
+ previousBlock,
36
+ page,
37
+ blocksById,
38
+ hasExplicitPageBreak,
39
+ renderedBreakNeedsSnap
40
+ }: ReconcileBeforeBlockOptions) => BreakDecision;
41
+ declare const recordReflowBoundary: (state: RenderedBreakState, crossedPageBoundary: boolean) => RenderedBreakState;
42
+ type ReconcileAfterBlockOptions = {
43
+ state: RenderedBreakState;
44
+ block: FlowBlock;
45
+ pageNumberBefore: number;
46
+ pageNumberAfter: number;
47
+ previousPage: Page | undefined;
48
+ };
49
+ /** Record page movement caused by laying out a block, or reset at a structural break. */
50
+ declare const reconcileAfterBlock: ({
51
+ state,
52
+ block,
53
+ pageNumberBefore,
54
+ pageNumberAfter,
55
+ previousPage
56
+ }: ReconcileAfterBlockOptions) => RenderedBreakState;
57
+ //#endregion
58
+ export { INITIAL_RENDERED_BREAK_STATE, RenderedBreakState, reconcileAfterBlock, reconcileBreakBeforeBlock, recordReflowBoundary };
@@ -0,0 +1,63 @@
1
+ import { isEmptyParagraph } from "./paragraphSpacing.js";
2
+ import { continuesNumberedSequence, continuesTabbedParagraphSequence } from "./paragraphSequence.js";
3
+ //#region src/layout-engine/renderedBreakReconciliation.ts
4
+ const INITIAL_RENDERED_BREAK_STATE = { type: "noPageAdvance" };
5
+ /**
6
+ * Reconcile an authored or cached break before laying out one block.
7
+ *
8
+ * Cached markers describe a boundary at a paragraph, not an ordinal page.
9
+ * Natural reflow may therefore satisfy the marker before layout reaches it.
10
+ * Every marker consumes the accumulated advance state so later markers make
11
+ * independent decisions.
12
+ */
13
+ const reconcileBreakBeforeBlock = ({ state, block, previousBlock, page, blocksById, hasExplicitPageBreak, renderedBreakNeedsSnap }) => {
14
+ if (hasExplicitPageBreak) return {
15
+ forcePageBreak: true,
16
+ state: INITIAL_RENDERED_BREAK_STATE
17
+ };
18
+ if (block.kind !== "paragraph" || block.attrs?.renderedPageBreakBefore !== true) return {
19
+ forcePageBreak: false,
20
+ state
21
+ };
22
+ const markerAlreadySatisfied = pageStartsWithPreviousParagraphContinuation(page, previousBlock) || state.type === "pageAdvance" && state.reason === "reflowBoundary" || state.type === "pageAdvance" && isPaginationEmptyParagraph(block) || state.type === "pageAdvance" && (continuesNumberedSequence(previousBlock, block) || continuesTabbedParagraphSequence(previousBlock, block));
23
+ return {
24
+ forcePageBreak: renderedBreakNeedsSnap && !markerAlreadySatisfied && pageHasVisibleBodyContent(page, blocksById),
25
+ state: INITIAL_RENDERED_BREAK_STATE
26
+ };
27
+ };
28
+ const recordReflowBoundary = (state, crossedPageBoundary) => crossedPageBoundary ? {
29
+ type: "pageAdvance",
30
+ reason: "reflowBoundary"
31
+ } : state;
32
+ /** Record page movement caused by laying out a block, or reset at a structural break. */
33
+ const reconcileAfterBlock = ({ state, block, pageNumberBefore, pageNumberAfter, previousPage }) => {
34
+ if (isStructuralBreak(block)) return INITIAL_RENDERED_BREAK_STATE;
35
+ if (!isVisibleBodyBlock(block) || pageNumberAfter <= pageNumberBefore) return state;
36
+ return {
37
+ type: "pageAdvance",
38
+ reason: previousPage?.fragments.some(({ blockId }) => blockId === block.id) ? "reflowBoundary" : "ordinary"
39
+ };
40
+ };
41
+ const isPaginationEmptyParagraph = (block) => block.runs.every((run) => {
42
+ if (run.kind === "text") return run.text.trim().length === 0;
43
+ return run.kind === "tab" || run.kind === "lineBreak";
44
+ });
45
+ const pageHasVisibleBodyContent = (page, blocksById) => {
46
+ for (const fragment of page.fragments) {
47
+ if (fragment.kind !== "paragraph") return true;
48
+ const block = blocksById.get(String(fragment.blockId));
49
+ if (block?.kind !== "paragraph" || !isPaginationEmptyParagraph(block)) return true;
50
+ }
51
+ return false;
52
+ };
53
+ const pageStartsWithPreviousParagraphContinuation = (page, previousBlock) => {
54
+ if (previousBlock?.kind !== "paragraph") return false;
55
+ return page.fragments.some((fragment) => fragment.kind === "paragraph" && String(fragment.blockId) === String(previousBlock.id) && fragment.continuesFromPrev === true);
56
+ };
57
+ const isStructuralBreak = (block) => block.kind === "pageBreak" || block.kind === "columnBreak" || block.kind === "sectionBreak";
58
+ const isVisibleBodyBlock = (block) => {
59
+ if (block.kind === "paragraph") return !isEmptyParagraph(block);
60
+ return !isStructuralBreak(block);
61
+ };
62
+ //#endregion
63
+ export { INITIAL_RENDERED_BREAK_STATE, reconcileAfterBlock, reconcileBreakBeforeBlock, recordReflowBoundary };
@@ -1,9 +1,9 @@
1
1
  import { OutlineStyleAttr } from "../types/documentEnumValues.js";
2
- import { ImagePosition, ImageWrap, SdtProperties, SdtType, TableWidthType } from "@stll/docx-core/model";
2
+ import { ImagePosition, ImageWrap, SdtProperties, SdtType, ShapeTextBody, TableWidthType } from "@stll/docx-core/model";
3
3
 
4
4
  //#region src/layout-engine/types.d.ts
5
5
  declare namespace types_d_exports {
6
- export { BlockId, BorderStyle, CellBorderSpec, CellBorders, ColumnBreakBlock, ColumnBreakMeasure, ColumnLayout, DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, DocumentPosition, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, FieldRun, FloatingTablePosition, FlowBlock, FootnoteContent, Fragment, FragmentBase, HeaderFooterContent, HeaderFooterContentHeights, HeaderFooterLayout, HitTestResult, HyperlinkInfo, ImageBlock, ImageFragment, ImageMeasure, ImageRun, ImageRunPosition, Layout, LayoutOptions, LineBreakRun, ListNumPr, MathRun, Measure, MeasuredLine, Page, PageBreakBlock, PageBreakMeasure, PageHeaderFooterRefs, PageMargins, ParagraphAttrs, ParagraphBlock, ParagraphBorders, ParagraphFragment, ParagraphIndent, ParagraphMeasure, ParagraphSpacing, Run, RunFormatting, SdtGroup, SectionBreakBlock, SectionBreakMeasure, TabAlignment, TabRun, TabStop, TableBlock, TableCell, TableCellMeasure, TableFragment, TableMeasure, TableRow, TableRowMeasure, TextBoxBlock, TextBoxFlowAttrs, TextBoxFragment, TextBoxMeasure, TextRun, floatingTextBoxReservesBand, floatingTextBoxWrapsText, isFloatingImageRun, isFloatingTextBoxBlock, isTextWrappingFloatingImageRun, tableColumnsArePinned };
6
+ export { BlockId, BorderStyle, CellBorderSpec, CellBorders, ColumnBreakBlock, ColumnBreakMeasure, ColumnLayout, DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, DocumentPosition, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, FieldRun, FloatingTablePosition, FlowBlock, FootnoteContent, Fragment, FragmentBase, HeaderFooterContent, HeaderFooterContentHeights, HeaderFooterLayout, HitTestResult, HyperlinkInfo, ImageBlock, ImageFragment, ImageMeasure, ImageRun, ImageRunPosition, Layout, LayoutOptions, LineBreakRun, ListNumPr, MathRun, Measure, MeasuredLine, Page, PageBreakBlock, PageBreakMeasure, PageHeaderFooterRefs, PageMargins, ParagraphAttrs, ParagraphBlock, ParagraphBorders, ParagraphFragment, ParagraphIndent, ParagraphMeasure, ParagraphSpacing, Run, RunFormatting, SdtGroup, SectionBreakBlock, SectionBreakMeasure, TabAlignment, TabRun, TabStop, TableBlock, TableCell, TableCellMeasure, TableFragment, TableMeasure, TableRow, TableRowMeasure, TextBoxBlock, TextBoxFlowAttrs, TextBoxFragment, TextBoxMeasure, TextRun, floatingTextBoxReservesBand, floatingTextBoxWrapsText, getTableRowLeadingWidth, isFloatingImageRun, isFloatingTextBoxBlock, isTextWrappingFloatingImageRun, tableColumnsArePinned };
7
7
  }
8
8
  /**
9
9
  * Unique identifier for a block in the document.
@@ -286,8 +286,13 @@ type ListNumPr = {
286
286
  * Paragraph block attributes.
287
287
  */
288
288
  type ParagraphAttrs = {
289
- alignment?: "left" | "center" | "right" | "justify";
290
- spacing?: ParagraphSpacing;
289
+ alignment?: "left" | "center" | "right" | "justify"; /** OOXML outline level (`w:outlineLvl`), where zero is the top level. */
290
+ outlineLevel?: number;
291
+ spacing?: ParagraphSpacing; /** Spacing sides resolved from OOXML automatic paragraph spacing. */
292
+ automaticSpacing?: {
293
+ before?: boolean;
294
+ after?: boolean;
295
+ };
291
296
  /**
292
297
  * Tracks which `spacing` sides came from inline (`<w:pPr><w:spacing>`)
293
298
  * formatting versus inherited via paragraph style. Word collapses
@@ -300,6 +305,13 @@ type ParagraphAttrs = {
300
305
  before?: boolean;
301
306
  after?: boolean;
302
307
  };
308
+ /**
309
+ * Whether an empty paragraph carries direct paragraph formatting in its
310
+ * source `w:pPr`. Word treats such a blank as authored and keeps its
311
+ * inherited spacing; a bare empty paragraph still collapses that spacing.
312
+ */
313
+ hasDirectParagraphFormatting?: boolean; /** Whether an empty paragraph carries direct formatting on its paragraph mark. */
314
+ hasDirectParagraphMarkFormatting?: boolean;
303
315
  indent?: ParagraphIndent;
304
316
  keepNext?: boolean;
305
317
  keepLines?: boolean;
@@ -319,13 +331,15 @@ type ParagraphAttrs = {
319
331
  borders?: ParagraphBorders;
320
332
  shading?: string;
321
333
  tabs?: TabStop[]; /** Render structural empty paragraphs as zero-height anchors. */
322
- suppressEmptyParagraphHeight?: boolean;
334
+ suppressEmptyParagraphHeight?: boolean; /** Reserve the reference extra line advance for a story-leading empty level-0 outline paragraph. */
335
+ reserveEmptyOutlineHeight?: boolean;
323
336
  numPr?: ListNumPr;
324
337
  listMarker?: string;
325
338
  listIsBullet?: boolean;
326
339
  listMarkerHidden?: boolean;
327
340
  listMarkerFontFamily?: string;
328
- listMarkerFontSize?: number;
341
+ listMarkerFontSize?: number; /** Horizontal alignment of the marker around the paragraph's list anchor. */
342
+ listMarkerAlignment?: "left" | "center" | "right";
329
343
  /**
330
344
  * `w:suff` (§17.9.25) — what follows the marker before body text.
331
345
  * `tab` (default) grows the marker to the next tab stop; `space` adds
@@ -431,6 +445,8 @@ type TableCell = {
431
445
  type TableRow = {
432
446
  id: BlockId;
433
447
  cells: TableCell[];
448
+ gridBefore?: number;
449
+ gridAfter?: number;
434
450
  height?: number;
435
451
  heightRule?: "auto" | "atLeast" | "exact";
436
452
  isHeader?: boolean;
@@ -570,7 +586,8 @@ type TextBoxBlock = {
570
586
  kind: "textBox";
571
587
  id: BlockId; /** Width in pixels */
572
588
  width: number; /** Height in pixels (may be auto-calculated) */
573
- height?: number; /** Fill/background color */
589
+ height?: number; /** Text fitting behavior */
590
+ autoFit?: ShapeTextBody["autoFit"]; /** Fill/background color */
574
591
  fillColor?: string; /** Border width in pixels */
575
592
  outlineWidth?: number; /** Border color */
576
593
  outlineColor?: string; /** Outline dash style, or `"none"` for an explicit no-outline. */
@@ -901,7 +918,8 @@ type LayoutOptions = {
901
918
  * overflowing first-page header without forcing pages 2+ to inherit the
902
919
  * same extension.
903
920
  */
904
- firstPageMargins?: PageMargins; /** Body-level final section page size. */
921
+ firstPageMargins?: PageMargins; /** Per-section body margins used on even section pages. */
922
+ sectionEvenPageMargins?: (PageMargins | undefined)[]; /** Body-level final section page size. */
905
923
  finalPageSize?: {
906
924
  w: number;
907
925
  h: number;
@@ -1004,7 +1022,8 @@ type HeaderFooterContent = {
1004
1022
  /**
1005
1023
  * `true` when the image run is anchored at page-level coordinates instead of
1006
1024
  * participating in inline flow. Covers the OOXML floating wrap types
1007
- * (`square`, `tight`, `through`, `behind`, `inFront`) and the legacy
1025
+ * (`square`, `tight`, `through`, `behind`, `inFront`), explicitly positioned
1026
+ * `topAndBottom` artwork, and the legacy
1008
1027
  * `displayMode === "float"` escape hatch used by ProseMirror nodes that
1009
1028
  * pre-date the wrap-type round-trip.
1010
1029
  *
@@ -1055,5 +1074,7 @@ declare function floatingTextBoxReservesBand(block: TextBoxFlowAttrs): boolean;
1055
1074
  * and the painter both consult this so their line count and `white-space` agree.
1056
1075
  */
1057
1076
  declare function tableColumnsArePinned(table: TableBlock): boolean;
1077
+ /** Return the leading visual offset for a row with omitted grid columns. */
1078
+ declare const getTableRowLeadingWidth: (row: TableRow, columnWidths: readonly number[]) => number;
1058
1079
  //#endregion
1059
- export { BlockId, BorderStyle, CellBorderSpec, CellBorders, ColumnBreakBlock, ColumnBreakMeasure, ColumnLayout, DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, DocumentPosition, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, FieldRun, FloatingTablePosition, FlowBlock, FootnoteContent, Fragment, FragmentBase, HeaderFooterContent, HeaderFooterContentHeights, HeaderFooterLayout, HitTestResult, HyperlinkInfo, ImageBlock, ImageFragment, ImageMeasure, ImageRun, ImageRunPosition, Layout, LayoutOptions, LineBreakRun, ListNumPr, MathRun, Measure, MeasuredLine, Page, PageBreakBlock, PageBreakMeasure, PageHeaderFooterRefs, PageMargins, ParagraphAttrs, ParagraphBlock, ParagraphBorders, ParagraphFragment, ParagraphIndent, ParagraphMeasure, ParagraphSpacing, Run, RunFormatting, SdtGroup, SectionBreakBlock, SectionBreakMeasure, TabAlignment, TabRun, TabStop, TableBlock, TableCell, TableCellMeasure, TableFragment, TableMeasure, TableRow, TableRowMeasure, TextBoxBlock, TextBoxFlowAttrs, TextBoxFragment, TextBoxMeasure, TextRun, floatingTextBoxReservesBand, floatingTextBoxWrapsText, isFloatingImageRun, isFloatingTextBoxBlock, isTextWrappingFloatingImageRun, tableColumnsArePinned, types_d_exports };
1080
+ export { BlockId, BorderStyle, CellBorderSpec, CellBorders, ColumnBreakBlock, ColumnBreakMeasure, ColumnLayout, DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, DocumentPosition, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, FieldRun, FloatingTablePosition, FlowBlock, FootnoteContent, Fragment, FragmentBase, HeaderFooterContent, HeaderFooterContentHeights, HeaderFooterLayout, HitTestResult, HyperlinkInfo, ImageBlock, ImageFragment, ImageMeasure, ImageRun, ImageRunPosition, Layout, LayoutOptions, LineBreakRun, ListNumPr, MathRun, Measure, MeasuredLine, Page, PageBreakBlock, PageBreakMeasure, PageHeaderFooterRefs, PageMargins, ParagraphAttrs, ParagraphBlock, ParagraphBorders, ParagraphFragment, ParagraphIndent, ParagraphMeasure, ParagraphSpacing, Run, RunFormatting, SdtGroup, SectionBreakBlock, SectionBreakMeasure, TabAlignment, TabRun, TabStop, TableBlock, TableCell, TableCellMeasure, TableFragment, TableMeasure, TableRow, TableRowMeasure, TextBoxBlock, TextBoxFlowAttrs, TextBoxFragment, TextBoxMeasure, TextRun, floatingTextBoxReservesBand, floatingTextBoxWrapsText, getTableRowLeadingWidth, isFloatingImageRun, isFloatingTextBoxBlock, isTextWrappingFloatingImageRun, tableColumnsArePinned, types_d_exports };
@@ -9,6 +9,7 @@ var types_exports = /* @__PURE__ */ __exportAll({
9
9
  FOOTNOTE_SEPARATOR_HEIGHT: () => 12,
10
10
  floatingTextBoxReservesBand: () => floatingTextBoxReservesBand,
11
11
  floatingTextBoxWrapsText: () => floatingTextBoxWrapsText,
12
+ getTableRowLeadingWidth: () => getTableRowLeadingWidth,
12
13
  isFloatingImageRun: () => isFloatingImageRun,
13
14
  isFloatingTextBoxBlock: () => isFloatingTextBoxBlock,
14
15
  isTextWrappingFloatingImageRun: () => isTextWrappingFloatingImageRun,
@@ -46,7 +47,8 @@ const FOOTNOTE_FALLBACK_LINE_HEIGHT = 13;
46
47
  /**
47
48
  * `true` when the image run is anchored at page-level coordinates instead of
48
49
  * participating in inline flow. Covers the OOXML floating wrap types
49
- * (`square`, `tight`, `through`, `behind`, `inFront`) and the legacy
50
+ * (`square`, `tight`, `through`, `behind`, `inFront`), explicitly positioned
51
+ * `topAndBottom` artwork, and the legacy
50
52
  * `displayMode === "float"` escape hatch used by ProseMirror nodes that
51
53
  * pre-date the wrap-type round-trip.
52
54
  *
@@ -55,6 +57,7 @@ const FOOTNOTE_FALLBACK_LINE_HEIGHT = 13;
55
57
  */
56
58
  const isFloatingImageRun = (run) => {
57
59
  const wrapType = run.wrapType;
60
+ if (wrapType === "topAndBottom" && run.position !== void 0) return true;
58
61
  if (wrapType === "square" || wrapType === "tight" || wrapType === "through" || wrapType === "behind" || wrapType === "inFront") return true;
59
62
  return run.displayMode === "float";
60
63
  };
@@ -114,5 +117,15 @@ function tableColumnsArePinned(table) {
114
117
  if (table.layout === "fixed") return true;
115
118
  return table.widthType === "dxa" || table.widthType === "pct";
116
119
  }
120
+ const tableRowOwnsNestedGrid = (row) => {
121
+ const cell = row.cells.length === 1 ? row.cells.at(0) : void 0;
122
+ if (!cell?.blocks.some((block) => block.kind === "table")) return false;
123
+ return cell.blocks.every((block) => block.kind === "table" || block.kind === "paragraph" && block.runs.length === 0);
124
+ };
125
+ /** Return the leading visual offset for a row with omitted grid columns. */
126
+ const getTableRowLeadingWidth = (row, columnWidths) => {
127
+ if (tableRowOwnsNestedGrid(row)) return 0;
128
+ return columnWidths.slice(0, row.gridBefore ?? 0).reduce((sum, columnWidth) => sum + columnWidth, 0);
129
+ };
117
130
  //#endregion
118
- export { DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, floatingTextBoxReservesBand, floatingTextBoxWrapsText, isFloatingImageRun, isFloatingTextBoxBlock, isTextWrappingFloatingImageRun, tableColumnsArePinned, types_exports };
131
+ export { DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, floatingTextBoxReservesBand, floatingTextBoxWrapsText, getTableRowLeadingWidth, isFloatingImageRun, isFloatingTextBoxBlock, isTextWrappingFloatingImageRun, tableColumnsArePinned, types_exports };
@@ -1188,12 +1188,12 @@ function applySectionHeaderFooterOptions(page, pageOptions, options) {
1188
1188
  const useEven = refs.evenAndOddHeaders === true && sectionPageNumber % 2 === 0;
1189
1189
  const headerRId = (() => {
1190
1190
  if (useFirst) return refs.headerFirst;
1191
- if (useEven && refs.headerEven) return refs.headerEven;
1191
+ if (useEven) return refs.headerEven;
1192
1192
  return refs.headerDefault;
1193
1193
  })();
1194
1194
  const footerRId = (() => {
1195
1195
  if (useFirst) return refs.footerFirst;
1196
- if (useEven && refs.footerEven) return refs.footerEven;
1196
+ if (useEven) return refs.footerEven;
1197
1197
  return refs.footerDefault;
1198
1198
  })();
1199
1199
  if (headerRId) {