Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion devtools/visual-testing/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions packages/layout-engine/contracts/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1810,9 +1810,14 @@ export type HeaderFooterPage = {
};

export type HeaderFooterLayout = {
/** Measurement height for pagination — excludes out-of-band fragments. */
height: number;
/** Minimum y of all rendered fragments (including out-of-band). */
minY?: number;
/** Maximum y + fragmentHeight of all rendered fragments. */
maxY?: number;
/** Full visual extent of all rendered fragments (renderMaxY - renderMinY). */
renderHeight?: number;
pages: HeaderFooterPage[];
};

Expand Down
18 changes: 8 additions & 10 deletions packages/layout-engine/layout-bridge/src/incrementalLayout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -934,6 +934,7 @@ export async function incrementalLayout(
headerMeasureCache,
HEADER_PRELAYOUT_PLACEHOLDER_PAGE_COUNT,
undefined, // No page resolver needed for height calculation
'header',
);

// Extract actual content heights from each variant
Expand All @@ -960,11 +961,8 @@ export async function incrementalLayout(
maxHeight: headerFooter.constraints.height,
};
const measures = await Promise.all(blocks.map((block) => measureFn(block, measureConstraints)));
// Layout to get actual height
const layout = layoutHeaderFooter(blocks, measures, {
width: headerFooter.constraints.width,
height: headerFooter.constraints.height,
});
// Layout to get actual height — pass full constraints for page-relative normalization
const layout = layoutHeaderFooter(blocks, measures, headerFooter.constraints, 'header');
if (layout.height > 0) {
// Store height by rId for per-page margin calculation
headerContentHeightsByRId.set(rId, layout.height);
Expand Down Expand Up @@ -1047,6 +1045,7 @@ export async function incrementalLayout(
headerMeasureCache,
FOOTER_PRELAYOUT_PLACEHOLDER_PAGE_COUNT,
undefined, // No page resolver needed for height calculation
'footer',
);

// Extract actual content heights from each variant
Expand All @@ -1073,11 +1072,8 @@ export async function incrementalLayout(
maxHeight: headerFooter.constraints.height,
};
const measures = await Promise.all(blocks.map((block) => measureFn(block, measureConstraints)));
// Layout to get actual height
const layout = layoutHeaderFooter(blocks, measures, {
width: headerFooter.constraints.width,
height: headerFooter.constraints.height,
});
// Layout to get actual height — pass full constraints for page-relative normalization
const layout = layoutHeaderFooter(blocks, measures, headerFooter.constraints, 'footer');
if (layout.height > 0) {
// Store height by rId for per-page margin calculation
footerContentHeightsByRId.set(rId, layout.height);
Expand Down Expand Up @@ -1898,6 +1894,7 @@ export async function incrementalLayout(
headerMeasureCache,
FeatureFlags.HEADER_FOOTER_PAGE_TOKENS ? undefined : numberingCtx.totalPages, // Fallback for backward compat
pageResolver, // Use page resolver for section-aware numbering
'header',
);
headers = serializeHeaderFooterResults('header', headerLayouts);
}
Expand All @@ -1909,6 +1906,7 @@ export async function incrementalLayout(
headerMeasureCache,
FeatureFlags.HEADER_FOOTER_PAGE_TOKENS ? undefined : numberingCtx.totalPages, // Fallback for backward compat
pageResolver, // Use page resolver for section-aware numbering
'footer',
);
footers = serializeHeaderFooterResults('footer', footerLayouts);
}
Expand Down
3 changes: 2 additions & 1 deletion packages/layout-engine/layout-bridge/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ export type { BoundaryRange } from './text-boundaries';
export { incrementalLayout, measureCache, normalizeMargin } from './incrementalLayout';
export type { HeaderFooterLayoutResult, IncrementalLayoutResult } from './incrementalLayout';
// Re-export computeDisplayPageNumber from layout-engine for section-aware page numbering
export { computeDisplayPageNumber, type DisplayPageInfo } from '@superdoc/layout-engine';
export { computeDisplayPageNumber } from '@superdoc/layout-engine';
export type { DisplayPageInfo, HeaderFooterConstraints } from '@superdoc/layout-engine';
export { remeasureParagraph } from './remeasure';
export { measureCharacterX } from './text-measurement';
export { clickToPositionDom, findPageElement } from './dom-mapping';
Expand Down
10 changes: 6 additions & 4 deletions packages/layout-engine/layout-bridge/src/layoutHeaderFooter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ export async function layoutHeaderFooterWithCache(
cache: HeaderFooterLayoutCache = sharedHeaderFooterCache,
totalPages?: number,
pageResolver?: PageResolver,
kind?: 'header' | 'footer',
): Promise<HeaderFooterBatchResult> {
const result: HeaderFooterBatchResult = {};

Expand All @@ -211,7 +212,7 @@ export async function layoutHeaderFooterWithCache(
resolveHeaderFooterTokens(clonedBlocks, 1, numPages);

const measures = await cache.measureBlocks(clonedBlocks, constraints, measureBlock);
const layout = layoutHeaderFooter(clonedBlocks, measures, constraints);
const layout = layoutHeaderFooter(clonedBlocks, measures, constraints, kind);

result[type] = { blocks: clonedBlocks, measures, layout };
}
Expand All @@ -231,7 +232,7 @@ export async function layoutHeaderFooterWithCache(
const hasTokens = hasPageTokens(blocks);
if (!hasTokens) {
const measures = await cache.measureBlocks(blocks, constraints, measureBlock);
const layout = layoutHeaderFooter(blocks, measures, constraints);
const layout = layoutHeaderFooter(blocks, measures, constraints, kind);
result[type] = { blocks, measures, layout };
continue;
}
Expand Down Expand Up @@ -275,7 +276,7 @@ export async function layoutHeaderFooterWithCache(

// Measure and layout
const measures = await cache.measureBlocks(clonedBlocks, constraints, measureBlock);
const pageLayout = layoutHeaderFooter(clonedBlocks, measures, constraints);
const pageLayout = layoutHeaderFooter(clonedBlocks, measures, constraints, kind);
const measuresById = new Map<string, Measure>();
for (let i = 0; i < clonedBlocks.length; i += 1) {
measuresById.set(clonedBlocks[i].id, measures[i]);
Expand Down Expand Up @@ -307,13 +308,14 @@ export async function layoutHeaderFooterWithCache(
// Construct final HeaderFooterLayout with all pages
// Use the first page's measurements for overall dimensions
const firstPageLayout = pages[0]
? layoutHeaderFooter(pages[0].blocks, pages[0].measures, constraints)
? layoutHeaderFooter(pages[0].blocks, pages[0].measures, constraints, kind)
: { height: 0, pages: [] };

const finalLayout: HeaderFooterLayout = {
height: firstPageLayout.height,
minY: firstPageLayout.minY,
maxY: firstPageLayout.maxY,
renderHeight: firstPageLayout.renderHeight,
pages: pages.map((p) => ({
number: p.number,
fragments: p.fragments,
Expand Down
22 changes: 19 additions & 3 deletions packages/layout-engine/layout-engine/src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,25 @@ export type LayoutOptions = {
export declare const SEMANTIC_PAGE_HEIGHT_PX = 1000000;
export type HeaderFooterConstraints = {
width: number;
/** Body content height used as the measurement canvas (pagination boundary). */
height: number;
/** Actual page width for page-relative anchor positioning */
/** Actual page width for page-relative anchor positioning. */
pageWidth?: number;
/** Page margins for page-relative anchor positioning */
margins?: { left: number; right: number };
/** Physical page height for vertical page-relative anchor conversion. */
pageHeight?: number;
/**
* Page margins for anchor positioning.
* `left`/`right`: horizontal page-relative conversion.
* `top`/`bottom`: vertical margin-relative conversion and footer band origin.
* `header`: header distance from page top edge (header band origin).
*/
margins?: {
left: number;
right: number;
top?: number;
bottom?: number;
header?: number;
};
/**
* Optional base height used to bound behindDoc overflow handling.
* When provided, decorative assets far outside the header/footer band
Expand All @@ -61,7 +75,9 @@ export declare function layoutHeaderFooter(
blocks: FlowBlock[],
measures: Measure[],
constraints: HeaderFooterConstraints,
kind?: 'header' | 'footer',
): HeaderFooterLayout;
export { normalizeFragmentsForRegion } from './normalize-header-footer-fragments.js';
export { buildAnchorMap, resolvePageRefTokens, getTocBlocksForRemeasurement } from './resolvePageRefs.js';
export { formatPageNumber, computeDisplayPageNumber } from './pageNumbering.js';
export type { PageNumberFormat, DisplayPageInfo } from './pageNumbering.js';
Loading
Loading