Operand

do no harem.

gram: docs

> ./packages/core/src/layout-bridge/headerFooterMargins.ts

/**
* Header/footer body-margin extension.
*
* Word grows the header (or footer) band when its in-flow content is taller
* than the authored top (or bottom) margin minus the header/footer distance,
* pushing the body text down (or up). This module owns that computation so the
* React and Vue adapters share one implementation instead of byte-identical
* inline copies (the layout pipelines were drifting candidates — see
* `docx-editor` engine-unification work, issue #696).
*
* Two correctness rules live here:
*
* 1. The band height is driven by `HeaderFooterContent.flowHeight` (in-flow
* content only), NOT `height` / `visualBottom`. A page/margin-anchored
* float — e.g. a full-page letterhead anchored in a header — is positioned
* on the page and does not push the body in Word. Counting it inflated the
* effective top margin past the page on real-world templates, so the
* paginator hard-threw "page size and margins yield no content area" and
* the document rendered blank (issue #705).
*
* 2. A clamp guarantees `top + bottom` never consumes the whole page, so a
* pathological in-flow header degrades to a thin content band with a
* warning instead of aborting pagination.
*/
import type { FlowBlock, PageMargins, SectionBreakBlock } from '../layout-engine/types';
import type { HeaderFooterContent } from '../layout-painter/renderPage';
/** Word's default `w:header` / `w:footer` distance (0.5in = 48px). */
const DEFAULT_HF_DISTANCE_PX = 48;
/**
* Floor on the body content area. Even when header/footer content is absurdly
* tall, leave at least this much height so pagination produces a page instead
* of throwing. ~one line at the default body font.
*/
const MIN_CONTENT_HEIGHT_PX = 24;
/** In-flow band height for one HF variant (falls back to total height). */
function bandHeight(hf: HeaderFooterContent | undefined): number {
if (!hf) return 0;
return hf.flowHeight ?? hf.height;
}
/** @public */
export interface ExtendMarginsForHeaderFooterInput {
pageSize: { w: number; h: number };
/** Body fallback margins. */
margins: PageMargins;
/** Final-section margins (last `sectPr`). */
finalMargins: PageMargins;
/**
* Body flow blocks. Each `sectionBreak` block's `margins` is extended IN
* PLACE so multi-section documents paginate with the same band growth (the
* layout engine prefers `sectionBreak.margins` over the body fallback).
*/
bodyBlocks?: FlowBlock[];
/** Header variants in play this layout (e.g. default + first-page). */
headers?: Array<HeaderFooterContent | undefined>;
/** Footer variants in play this layout. */
footers?: Array<HeaderFooterContent | undefined>;
/** Optional diagnostic sink for the clamp (adapters pass `console.warn`). */
warn?: (message: string) => void;
}
/** @public */
export interface ExtendMarginsForHeaderFooterResult {
margins: PageMargins;
finalMargins: PageMargins;
}
/**
* Extend body margins so the body clears the header/footer bands, mirroring
* Word. Returns new `margins` / `finalMargins`; mutates `sectionBreak.margins`
* in place. When no extension is needed the original objects are returned
* unchanged.
*
* @public
*/
export function extendMarginsForHeaderFooter(
input: ExtendMarginsForHeaderFooterInput
): ExtendMarginsForHeaderFooterResult {
const { pageSize, margins, finalMargins, bodyBlocks, headers, footers, warn } = input;
const headerDistance = margins.header ?? DEFAULT_HF_DISTANCE_PX;
const footerDistance = margins.footer ?? DEFAULT_HF_DISTANCE_PX;
const availableHeaderSpace = margins.top - headerDistance;
const availableFooterSpace = margins.bottom - footerDistance;
const headerContentHeight = Math.max(0, ...(headers ?? []).map(bandHeight));
const footerContentHeight = Math.max(0, ...(footers ?? []).map(bandHeight));
const extendHeader = headerContentHeight > availableHeaderSpace;
const extendFooter = footerContentHeight > availableFooterSpace;
if (!extendHeader && !extendFooter) {
return { margins, finalMargins };
}
const maxMargins = Math.max(0, pageSize.h - MIN_CONTENT_HEIGHT_PX);
let clamped = false;
const extend = (m: PageMargins): PageMargins => {
const out = { ...m };
if (extendHeader) out.top = Math.max(m.top, headerDistance + headerContentHeight);
if (extendFooter) out.bottom = Math.max(m.bottom, footerDistance + footerContentHeight);
// Safety net: never let header + footer consume the whole page. Clamp the
// footer band first (it sits at the page bottom), then the header band if
// it alone still overflows, so the body keeps a positive content area.
if (out.top + out.bottom > maxMargins) {
clamped = true;
out.bottom = Math.max(0, Math.min(out.bottom, maxMargins - out.top));
if (out.top + out.bottom > maxMargins) {
out.top = Math.max(0, maxMargins - out.bottom);
}
}
return out;
};
const extendedMargins = extend(margins);
const extendedFinal = extend(finalMargins);
if (bodyBlocks) {
for (const block of bodyBlocks) {
if (block.kind !== 'sectionBreak') continue;
const sb = block as SectionBreakBlock;
if (sb.margins) sb.margins = extend(sb.margins);
}
}
if (clamped && warn) {
warn(
'[layout] header/footer content exceeds page height; clamping margins to ' +
`preserve a content area. pageHeight=${Math.round(pageSize.h)} ` +
`headerBand=${Math.round(headerContentHeight)} footerBand=${Math.round(footerContentHeight)} ` +
`top=${Math.round(extendedMargins.top)} bottom=${Math.round(extendedMargins.bottom)}`
);
}
return { margins: extendedMargins, finalMargins: extendedFinal };
}