Signal drop!
Relay (operand.online) is unreachable.
Usually, a dropped signal means an upgrade is happening. Hold on!
Sorry, no connección.
Hang in there while we get back on track
gram: docs
> ./packages/core/src/layout-bridge/sectionGeometry.ts
/**
* Section-properties → page geometry helpers.
*
* Both adapters need the same translation from `SectionProperties` (twips
* + DOCX field names) into the layout engine's pixel-shaped `PageSize`
* and `PageMargins`. Living in core eliminates the subtle React/Vue
* drift that grows when each adapter ships its own twipsToPixels math.
*/
import { collectSectionConfigs, type SectionLayoutConfig } from '../layout-engine';
import type { ColumnLayout, FlowBlock, PageMargins } from '../layout-engine/types';
import type { Document, SectionProperties } from '../types/document';
import type { HeaderFooter } from '../types/content';
/** US Letter at 96 DPI — Word's default page size. */
export const DEFAULT_PAGE_WIDTH_PX = 816;
/** US Letter at 96 DPI. */
export const DEFAULT_PAGE_HEIGHT_PX = 1056;
/** 1 inch at 96 DPI — Word's default body margins. */
export const DEFAULT_BODY_MARGIN_PX = 96;
/** Word's default `headerDistance` / `footerDistance` (0.5in = 48px). */
export const DEFAULT_HF_DISTANCE_PX = 48;
/** Convert twips to pixels (1 twip = 1/20 point, 96 pixels per inch). */
export function twipsToPixels(twips: number): number {
return Math.round((twips / 1440) * 96);
}
/**
* Convert an OFFSET-like twip dimension (page margin, header/footer distance)
* to px, falling back to `fallbackPx` ONLY when the value is absent.
*
* Use this — not `value ? twipsToPixels(value) : fallback` — for any dimension
* where `0` is a meaningful, explicit value distinct from "not set". A truthy
* test treats `0` as absent and substitutes the default, which silently breaks
* documents that pin a margin/header/footer to 0 (e.g. full-bleed layouts, or
* `w:header="0"` — issue #740). `parseNumericAttribute` already returns
* `undefined` for a missing attribute and `0` for `="0"`, so a nullish guard is
* the correct discriminator.
*
* NOTE: this is for OFFSETS, not SIZES. A size (page width/height, image/shape
* extent) treats `0` as malformed/missing and SHOULD fall back — honoring a
* literal `0` there would render a zero-area element. Keep the truthy guard for
* sizes; use this helper for offsets.
*/
export function twipsToPxOr(twips: number | null | undefined, fallbackPx: number): number {
return twips != null ? twipsToPixels(twips) : fallbackPx;
}
/**
* Convert SectionProperties page size (twips) → pixel `{ w, h }`.
*
* Page size is a SIZE: a literal `0` (malformed `w:pgSz`) defaults to Letter
* rather than rendering a zero-area page — so the truthy guard is intentional
* here (contrast `getMargins`, where `0` is honored). See `twipsToPxOr`.
*/
export function getPageSize(sp: SectionProperties | null | undefined): {
w: number;
h: number;
} {
return {
w: sp?.pageWidth ? twipsToPixels(sp.pageWidth) : DEFAULT_PAGE_WIDTH_PX,
h: sp?.pageHeight ? twipsToPixels(sp.pageHeight) : DEFAULT_PAGE_HEIGHT_PX,
};
}
/**
* Convert SectionProperties margins (twips) → pixel `PageMargins`.
*
* Every distance is an OFFSET, so an explicit `0` is honored (full-bleed body
* margins; a header/footer pinned to the page edge — issue #740). Only an
* ABSENT distance falls back to Word's default. `header`/`footer` default to
* 48px (Word's 0.5in) so the HF margin-extension math needn't special-case
* undefined.
*/
export function getMargins(sp: SectionProperties | null | undefined): PageMargins {
return {
top: twipsToPxOr(sp?.marginTop, DEFAULT_BODY_MARGIN_PX),
right: twipsToPxOr(sp?.marginRight, DEFAULT_BODY_MARGIN_PX),
bottom: twipsToPxOr(sp?.marginBottom, DEFAULT_BODY_MARGIN_PX),
left: twipsToPxOr(sp?.marginLeft, DEFAULT_BODY_MARGIN_PX),
header: twipsToPxOr(sp?.headerDistance, DEFAULT_HF_DISTANCE_PX),
footer: twipsToPxOr(sp?.footerDistance, DEFAULT_HF_DISTANCE_PX),
};
}
/**
* Resolve the HeaderFooter pair (default + first-page) for a section.
*
* Mirrors React's lookup in DocxEditor.tsx: read `pkg.headers`/`footers`
* (Maps keyed by rId), resolve through `sp.headerReferences` /
* `footerReferences`. When `titlePg` is unset and only `first` HFs
* exist, they serve as the default — same Word fallback both adapters
* have shipped.
*/
export function resolveHeaderFooter(
doc: Document | null,
sp: SectionProperties | null | undefined
): {
header: HeaderFooter | null;
footer: HeaderFooter | null;
firstHeader: HeaderFooter | null;
firstFooter: HeaderFooter | null;
} {
const empty = { header: null, footer: null, firstHeader: null, firstFooter: null };
if (!doc?.package) return empty;
const headers = doc.package.headers;
const footers = doc.package.footers;
let header: HeaderFooter | null = null;
let footer: HeaderFooter | null = null;
let firstHeader: HeaderFooter | null = null;
let firstFooter: HeaderFooter | null = null;
if (headers && sp?.headerReferences) {
const def = sp.headerReferences.find((r) => r.type === 'default');
if (def?.rId) header = headers.get(def.rId) ?? null;
const first = sp.headerReferences.find((r) => r.type === 'first');
if (first?.rId) firstHeader = headers.get(first.rId) ?? null;
}
if (footers && sp?.footerReferences) {
const def = sp.footerReferences.find((r) => r.type === 'default');
if (def?.rId) footer = footers.get(def.rId) ?? null;
const first = sp.footerReferences.find((r) => r.type === 'first');
if (first?.rId) firstFooter = footers.get(first.rId) ?? null;
}
if (!sp?.titlePg) {
if (!header && firstHeader) header = firstHeader;
if (!footer && firstFooter) footer = firstFooter;
}
return { header, footer, firstHeader, firstFooter };
}
/**
* Extract column layout from section properties.
* Returns undefined for single-column (default) to avoid unnecessary paginator overhead.
*/
export function getColumns(
sectionProps: SectionProperties | null | undefined
): ColumnLayout | undefined {
const count = sectionProps?.columnCount ?? 1;
if (count <= 1) return undefined;
// Default column spacing: 720 twips (0.5 inch) per OOXML spec
const gap = twipsToPixels(sectionProps?.columnSpace ?? 720);
return {
count,
gap,
equalWidth: sectionProps?.equalWidth ?? true,
separator: sectionProps?.separator,
};
}
export function columnWidthForSection(config: SectionLayoutConfig): number {
const contentWidth = config.pageSize.w - config.margins.left - config.margins.right;
const cols = config.columns;
if (!cols || cols.count <= 1) return contentWidth;
return Math.floor((contentWidth - (cols.count - 1) * cols.gap) / cols.count);
}
/**
* Compute per-block measurement widths by scanning for section breaks.
* Blocks must be measured with the page width/margins/columns of their own
* section so that the layout engine can paginate them against the right
* geometry without remeasuring.
*/
export function computePerBlockWidths(
blocks: FlowBlock[],
initialConfig: SectionLayoutConfig,
finalConfig: SectionLayoutConfig
): number[] {
const { configs: sectionConfigs, breakIndices } = collectSectionConfigs(
blocks,
initialConfig,
finalConfig
);
let sectionIdx = 0;
const widths: number[] = [];
for (let i = 0; i < blocks.length; i++) {
widths.push(columnWidthForSection(sectionConfigs[sectionIdx] ?? initialConfig));
if (sectionIdx < breakIndices.length && i === breakIndices[sectionIdx]) {
sectionIdx++;
}
}
return widths;
}