New

docx-editor 1.x has shipped. Vue support, i18n, agents. Read the migration guide →

API Referencev1.3.3

@eigenpal/docx-editor-core/layout-bridge

Layout Bridge — measure, hit-test, and map between PM positions and pixels.

Internal layer between the layout engine and rendering. The named exports below are the public contract for adapter authors, but the API is still evolving and may change in minor releases until a third-party adapter validates it.

Functions(103)

Apply the `.layout-table-cell-selected` class to painted layout cells matching a CellSelection in the PM state. Clears the class everywhere (within scope) first so toggling off (or moving to a TextSelection) erases prior highlights.

Duck-types CellSelection via `$anchorCell` / `forEachCell` rather than `instanceof` to dodge bundling issues across `prosemirror-tables` copies (the same trick used inline in the selection-overlay update).

declare function applyCellSelectionHighlight(pagesContainer: HTMLElement, state: EditorState, options?: {
    scope?: 'body' | 'header' | 'footer';
}): void;

Footnote-specific block normalization. Mirrors the spirit of `normalizeHeaderFooterMeasureBlocks`: post-process the body-pipeline output for a single footnote so it carries the correct visual prefix (its display number, rendered as a superscript) and a default 8pt font for any run that didn't specify a size.

The displayNumber is prepended onto the FIRST paragraph as a fresh superscript text run — visually matches Word's footnote numbering without disturbing the authored runs.

Exported for callers that want to compose their own conversion pipeline; `convertFootnoteToContent` calls it as part of its flow.

declare function applyFootnotePresentation(blocks: FlowBlock[], displayNumber: number): FlowBlock[];

Build a CSS font string from styling properties

Font sizes are in points and need to be converted to pixels for canvas. 1pt = 96/72 px ≈ 1.333px at standard web DPI.

Uses the font resolver to get category-appropriate fallback stacks (serif fonts get serif fallbacks, sans-serif get sans-serif, etc.) matching the same stacks used in rendering for consistent measurements.

declare function buildFontString(style: FontStyle): string;
buildFontString( fontFamily: "Arial", fontSize: 12, bold: true ) // Returns: "bold 16px Arial, Arimo, Helvetica, sans-serif" (12pt = 16px)

Build footnote content for all footnotes referenced in the document. Display numbers are assigned by first-appearance order (the same way Word renders them).

declare function buildFootnoteContentMap(footnotes: Footnote[], footnoteRefs: Array<{
    footnoteId: number;
}>, contentWidth: number, options: ConvertFootnoteOptions): Map<number, FootnoteContent>;

Turn the page→footnote-id map into the per-page render payload that `renderPages` consumes via `footnotesByPage`. Skips non-`normal` notes (separators, continuation notices), reads the display number out of the content map, and pulls plain text via `getFootnoteText`.

Lives in core (not in either adapter) so React + Vue both call the same helper — same rule as the rest of this module.

declare function buildFootnoteRenderItems(pageFootnoteMap: Map<number, number[]>, footnoteContentMap: Map<number, FootnoteContent>, doc: Document | null): Map<number, FootnoteRenderItem[]>;

Calculate per-page footnote reserved heights. Returns MappageNumber, reservedHeight.

declare function calculateFootnoteReservedHeights(pageFootnoteMap: Map<number, number[]>, footnoteContentMap: Map<number, {
    height: number;
}>): Map<number, number>;
declare function calculateHeaderFooterVisualBounds(blocks: FlowBlock[], measures: Measure[], flowHeight: number, metrics: HeaderFooterMetrics): {
    visualTop: number;
    visualBottom: number;
};

When a float's wrap margins consume the entire content width (or more), there is no horizontal strip beside it for body text. Word renders the following lines at full content width instead of squeezing them into a 1-pixel column. Unchecked margins from near-full-width tables/images can exceed contentWidth and collapse every line to ~1 glyph (the "single character per line after a wide floating table" bug).

Returned margins are zeroed when: - either side alone is = contentWidth (no strip on that side at all), or - their sum is = contentWidth (no strip exists between the two sides).

declare function clampFloatingWrapMargins(leftMargin: number, rightMargin: number, contentWidth: number): {
    leftMargin: number;
    rightMargin: number;
};

Clear all measurement caches Call when fonts change, page width changes, or for testing

declare function clearAllCaches(): void;

Clear the font metrics cache

declare function clearFontMetricsCache(): void;

Clear the paragraph measure cache

declare function clearParagraphMeasureCache(): void;

Clear the text width cache

declare function clearTextWidthCache(): void;

Main entry point: Map a click to a PM position.

This function takes the result of hit testing and returns the PM position.

declare function clickToPosition(fragmentHit: FragmentHit | null, tableCellHit?: TableCellHit | null): number | null;

Find ProseMirror position from a click using DOM-based detection.

declare function clickToPositionDom(container: HTMLElement, clientX: number, clientY: number, zoom?: number): number | null;

Map a click within a paragraph fragment to a PM position.

declare function clickToPositionInParagraph(fragmentHit: FragmentHit): PositionResult | null;

Map a click within a table cell to a PM position.

declare function clickToPositionInTableCell(tableCellHit: TableCellHit): number | null;

Vertically clip a client rect to the enclosing page-fragment table's visible box. A table that breaks across a page sits in a `.layout-table` with `overflow:hidden` and `height = visibleHeight`; getClientRects() still reports the geometry of lines that are visually clipped (off the page / in the inter-page gap), so selection highlights must be clipped to the same box.

Uses `.layout-table:not(.layout-nested-table)` because only the page-fragment table carries the window clip — the inner nested table has no overflow:hidden. Clips vertically only (the gap bug is vertical; horizontal clipping could trim change bars on tracked-change tables that set overflow-x: visible).

Returns null when the rect is fully outside the window.

declare function clipRectToTableWindow(spanEl: Element, rect: {
    readonly left: number;
    readonly top: number;
    readonly right: number;
    readonly bottom: number;
}): {
    left: number;
    top: number;
    right: number;
    bottom: number;
} | null;

Scan FlowBlocks for runs with footnoteRefId set. Returns a list of footnoteId, pmPos in document order.

Recurses into container blocks (table cells, text boxes) so footnote references authored anywhere in the body reach the page-reservation pass. Without this, a `footnoteRefId` nested inside a table cell never gets mapped to a page and the per-page `.layout-footnote-area` silently drops that entry even though the body still renders the in-line ref marker.

declare function collectFootnoteRefs(blocks: FlowBlock[]): Array<{
    footnoteId: number;
    pmPos: number;
}>;
declare function columnWidthForSection(config: SectionLayoutConfig): number;

TODO(unify-hf-editing follow-up): this function duplicates the span-walking + Range/TreeWalker logic in `packages/react/src/components/DocxEditor/internals/domSelection.ts:getCaretFromDom` (body). The body's helper is scoped to `.layout-page-content` via `findBodyPmSpans`; we walk the same shape scoped to `.layout-page-header / .layout-page-footer` here. Unification path: 1. Add `findHfPmSpans` / `findHfEmptyRuns` mirrors next to the body ones in `packages/core/src/layout-bridge/findBodyPmSpans.ts`. 2. Add `scope: 'body' | 'hf'` param to `getCaretFromDom` + `computeSelectionRectsFromDom`; switch the helper internally. 3. Move the (now scope-aware) helpers into core so React + Vue both call them. 4. Delete this function and `computeHfSelectionRectsFromView` — `DocxEditorPagedArea` calls `getCaretFromDom(scope: 'hf', ...)`. Reviewer estimate: ~30 LOC net deletion + body↔HF parity for free (lineHeight from `.layout-line` ancestor, empty-paragraph fallback via `findBodyEmptyRuns`, etc.). Deferred because it's a multi-file shape change that doesn't affect observable behavior.

declare function computeHfCaretRectFromView(view: EditorView, section: 'header' | 'footer', doc?: globalThis.Document): {
    top: number;
    left: number;
    height: number;
} | null;

Selection-rect set for a non-empty HF selection, projected against the painted HF spans. Mirror of `computeSelectionRectsFromDom` but scoped to `.layout-page-header` / `.layout-page-footer` instead of the body. Used so the painter draws a visible highlight when the user drag-selects inside a header/footer in edit mode.

Returns viewport-relative `{top, left, width, height}` rects. Empty array when selection is collapsed or no painted spans overlap the range.

declare function computeHfSelectionRectsFromView(view: EditorView, section: 'header' | 'footer', doc?: globalThis.Document): Array<{
    top: number;
    left: number;
    width: number;
    height: number;
}>;

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.

declare function computePerBlockWidths(blocks: FlowBlock[], initialConfig: SectionLayoutConfig, finalConfig: SectionLayoutConfig): number[];

Whether a header/footer block participates in the in-flow band height that pushes the body margin.

OOXML semantics: Word grows the header/footer band — and shifts body text — based only on the story's in-flow content. A floating/anchored object (`wp:anchor` DrawingML or an absolutely-positioned VML shape, e.g. a full-page letterhead anchored to the page in a header) is removed from the text flow and positioned on the page; it does NOT grow the band or push the body. So only inline-flow blocks count here. Anchored image *runs* inside a paragraph are likewise out of flow, but they don't contribute to the paragraph's measured line height, so paragraphs need no special handling.

declare function contributesToHeaderFooterFlowHeight(block: FlowBlock): boolean;

Convert an OOXML BorderSpec to a layout-engine BorderStyle. Shared by paragraph borders, cell borders, and header/footer borders.

declare function convertBorderSpecToLayout(border: {
    style?: string;
    size?: number;
    space?: number;
    color?: {
        rgb?: string;
        themeColor?: string;
        themeTint?: string;
        themeShade?: string;
    };
}, theme?: Theme | null): BorderStyle | undefined;

Convert a Footnote to renderable FootnoteContent via the body pipeline: `footnoteToProseDoc → toFlowBlocks → applyFootnotePresentation → measureBlocks`. Pre-PR (#378) this lived in a hand-rolled shadow stack that silently dropped non-paragraph content; routing through the body pipeline gives footnotes full block-kind support — paragraph + table + image + textBox + fields.

declare function convertFootnoteToContent(footnote: Footnote, displayNumber: number, contentWidth: number, options: ConvertFootnoteOptions): FootnoteContent;

Same pipeline as [convertHeaderFooterToContent](convertHeaderFooterToContent), but starts from an already-built ProseMirror document instead of `HeaderFooter.content`.

The unified HF editing model (see `openspec/changes/unify-hf-editing/`) maintains one persistent hidden PM EditorView per HF `rId`. The painter reads from that EditorView's current `state.doc` rather than re-parsing the Document-model `HeaderFooter` every layout pass — this is what actually makes the painter and the editor stay in lockstep.

`headerFooterToProseDoc` is still the right entry point when there is no mounted PM for the slot (cold load, or rId not yet projected).

declare function convertHeaderFooterPmDocToContent(pmDoc: Node, contentWidth: number, metrics: HeaderFooterMetrics, options: ConvertHeaderFooterOptions): HeaderFooterContent | undefined;

Convert HeaderFooter (document type) to HeaderFooterContent (render type).

Routes through the same pipeline as the body: HF.content → headerFooterToProseDoc → toFlowBlocks → measureBlocks. The inline editor uses the same conversion chain, so block support (paragraph, table, image, textBox, fields) and the inline editor's content stay in lockstep.

declare function convertHeaderFooterToContent(headerFooter: HeaderFooter | null | undefined, contentWidth: number, metrics: HeaderFooterMetrics, options: ConvertHeaderFooterOptions): HeaderFooterContent | undefined;

Total grid columns, derived from the widest row's accumulated colSpans.

declare function countTableColumns(tableBlock: TableBlock): number;

Detect whether a mousemove should surface a row/column insert "+" button.

Returns the button anchor + target cell PM position, or `null` if the mouse isn't near a row's left edge or a column's top edge — or if the relevant table belongs to an inactive HF/body region.

declare function detectTableInsertHover(input: TableInsertHoverInput): TableInsertHoverHit | null;

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.

declare function extendMarginsForHeaderFooter(input: ExtendMarginsForHeaderFooterInput): ExtendMarginsForHeaderFooterResult;

All body-tree empty-paragraph runs. Used as a caret fallback when a paragraph has no painted text spans.

declare function findBodyEmptyRuns(container: ParentNode): HTMLElement[];

First body-tree element whose `data-pm-start` exactly matches `pmStart`.

Used for scroll anchors and image `NodeSelection` resolution where the caller already knows the exact PM position it wants to find. Returns `null` for non-finite inputs so callers don't need to guard.

declare function findBodyPmAnchor(container: ParentNode, pmStart: number): HTMLElement | null;

All body-tree elements carrying a `data-pm-start` attribute.

Includes paragraph elements as well as run spans, which is what scroll- anchor recovery needs. Distinct from [findBodyPmSpans](findBodyPmSpans), which filters down to run spans only.

declare function findBodyPmAnchors(container: ParentNode): HTMLElement[];

All body-tree run spans carrying both `data-pm-start` and `data-pm-end`.

This is the workhorse for caret resolution and selection-rect painting.

declare function findBodyPmSpans(container: ParentNode): HTMLElement[];

Find the character offset at a given X position within a text run

declare function findCharacterAtX(x: number, charWidths: number[]): number;

Compare two per-page footnote reservation maps. Used by the React + Vue adapters to detect when the multi-pass loop has converged.

declare function footnoteReservedHeightsEqual(a: Map<number, number>, b: Map<number, number>): boolean;

Get cached font metrics or return undefined

declare function getCachedFontMetrics(fontFamily: string, fontSize: number, bold?: boolean, italic?: boolean): FontMetricsEntry | undefined;

Get cached paragraph measurement or return undefined

declare function getCachedParagraphMeasure(block: ParagraphBlock, maxWidth: number): ParagraphMeasure | undefined;

Get cached text width or return undefined

declare function getCachedTextWidth(text: string, font: string, letterSpacing?: number): number | undefined;

Get or create a canvas 2D context for text measurement

declare function getCanvasContext(): CanvasRenderingContext2D;

Get caret position for a collapsed selection.

declare function getCaretPosition(layout: Layout, blocks: FlowBlock[], measures: Measure[], pmPosition: number): CaretPosition | null;
declare function getCaretPositionFromDom(container: HTMLElement, pmPos: number, overlayRect: DOMRect): DomCaretPosition | null;

Extract column layout from section properties. Returns undefined for single-column (default) to avoid unnecessary paginator overhead.

declare function getColumns(sectionProps: SectionProperties | null | undefined): ColumnLayout | undefined;
declare function getFloatingMargins(lineY: number, lineHeight: number, zones: FloatingImageZone[] | undefined, paragraphYOffset: number): FloatingLineMargins;

Get current font metrics cache size

declare function getFontCacheSize(): number;

Get typography metrics for a given font size and family

Uses Canvas TextMetrics API when available for precise metrics, falls back to ratio-based approximations.

declare function getFontMetrics(style: FontStyle): FontMetrics;

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.

declare function getMargins(sp: SectionProperties | null | undefined): PageMargins;

Get page bounds (top and bottom Y coordinates).

declare function getPageBounds(layout: Layout, pageIndex: number): {
    top: number;
    bottom: number;
} | null;

Get the page index at a specific Y coordinate. Returns null if the coordinate is outside all pages.

declare function getPageIndexAtY(layout: Layout, y: number): number | null;

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`.

declare function getPageSize(sp: SectionProperties | null | undefined): {
    w: number;
    h: number;
};

Calculate the Y offset of a page from the top of the document.

declare function getPageTop(layout: Layout, pageIndex: number): number;

Get current paragraph measure cache size

declare function getParagraphCacheSize(): number;

Get the bounding rect for a PM position (for caret rendering).

declare function getPositionRect(block: ParagraphBlock, measure: ParagraphMeasure, pmPosition: number, fragmentX: number, fragmentY: number, fragmentWidth: number, fromLine: number): {
    x: number;
    y: number;
    height: number;
} | null;

Get per-character widths for a text run (for click positioning)

declare function getRunCharWidths(run: TextRun): number[];

Get Y coordinate to scroll to for a specific page.

declare function getScrollYForPage(layout: Layout, pageIndex: number): number;
declare function getSelectionRectsFromDom(container: HTMLElement, from: number, to: number, overlayRect: DOMRect): DomSelectionRect[];

Get current text width cache size

declare function getTextCacheSize(): number;

Get total size of all caches

declare function getTotalCacheSize(): number;

Get the total document height (all pages + gaps).

declare function getTotalDocumentHeight(layout: Layout): number;

Get the X position of a character offset within a text run

declare function getXForCharacter(offset: number, charWidths: number[]): number;

Get selection rectangles grouped by page.

declare function groupRectsByPage(rects: SelectionRect[]): Map<number, SelectionRect[]>;

Convert OOXML half-points to pixels OOXML font sizes are in half-points (24 = 12pt)

declare function halfPtToPx(halfPt: number): number;

Generate a simple hash for a paragraph block Used as cache key to identify identical content

declare function hashParagraphBlock(block: ParagraphBlock): string;

Perform complete hit testing from document coordinates to the most specific element.

declare function hitTest(layout: Layout, blocks: FlowBlock[], measures: Measure[], point: Point): HitTestResult;

Hit-test fragments on a page to find which fragment contains a point.

Fragments are checked in visual order (sorted by Y then X). Only considers paragraph and table fragments (not images for now).

declare function hitTestFragment(pageHit: PageHit, blocks: FlowBlock[], measures: Measure[], pagePoint: Point): FragmentHit | null;

Hit-test to find image fragments (they may overlap other content).

declare function hitTestImageFragment(pageHit: PageHit, blocks: FlowBlock[], measures: Measure[], pagePoint: Point): FragmentHit | null;

Hit-test pages to find which page contains a given Y coordinate.

Pages are stacked vertically with optional gaps between them. If the point is in a gap, returns the nearest page.

declare function hitTestPage(layout: Layout, point: Point): PageHit | null;

Hit-test within a table fragment to find the specific cell.

declare function hitTestTableCell(pageHit: PageHit, blocks: FlowBlock[], measures: Measure[], pagePoint: Point): TableCellHit | null;

Drop the cached HF host + span lists. Hosts/painters call this after a repaint (or HF mode toggle) so the next caret / selection compute re-walks the DOM. Public so adapters can call it from their painter commit signal.

declare function invalidateHfDomCache(): void;

Check if a selection spans multiple pages.

declare function isMultiPageSelection(rects: SelectionRect[]): boolean;

After layout, determine which footnotes appear on which pages. Checks each page's fragments to see if any footnoteRef PM positions fall within.

Returns MappageNumber, footnoteId[] in document order.

declare function mapFootnotesToPages(pages: Page[], footnoteRefs: Array<{
    footnoteId: number;
    pmPos: number;
}>): Map<number, number[]>;

Walk `blocks` and produce one `Measure` per block. Before measuring, this extracts floating exclusion zones (images / floating tables / floating textboxes), groups overlapping co-located floats, and threads the active zones plus cumulative Y into each `measureBlock` call.

Pass `pageGeometry` whenever the document may contain page/margin-anchored `topAndBottom` text boxes (e.g. a title banner pinned to the page top): without it their reserved band falls back to flow-relative Y and the band won't line up with where the painter places the box. Build it with the shared `pageGeometryFromPage` helper.

declare function measureBlocksWithFloats(blocks: FlowBlock[], contentWidth: number | number[], measureBlock: MeasureBlockFn, pageGeometry?: FloatPageGeometry): Measure[];

Measure a paragraph block and compute line breaks

declare function measureParagraph(block: ParagraphBlock, maxWidth: number, options?: MeasureParagraphOptions): ParagraphMeasure;

Measure multiple paragraph blocks

declare function measureParagraphs(blocks: ParagraphBlock[], maxWidth: number): ParagraphMeasure[];

Measure a run of text and return per-character widths for click positioning

declare function measureRun(text: string, style: FontStyle): RunMeasurement;

Measure a `TableBlock` against a content-width budget.

`measureBlock` is the per-cell-content measurement callback the adapter uses for everything inside a cell. The adapter passes its own `measureBlock` so block coverage stays per-renderer.

declare function measureTableBlock(tableBlock: TableBlock, contentWidth: number, measureBlock: (block: FlowBlock, contentWidth: number) => Measure): TableMeasure;
fn

measureTableCellBlockVisualHeight

packages/core/src/layout-bridge/index.ts:147

Visual height of a single block inside a table cell.

A one-line paragraph that contains only image runs is laid out at the image's intrinsic height (plus the paragraph's explicit spacing), not a full text line — matching Word's per-cell layout. Everything else uses the measured `totalHeight` / `height`.

declare function measureTableCellBlockVisualHeight(block: FlowBlock, blockMeasure: Measure): number;

Measure text and return full metrics

declare function measureText(text: string, style: FontStyle): TextMeasurement;

Measure the width of a text string with specific styling

declare function measureTextWidth(text: string, style: FontStyle): number;
declare function normalizeHeaderFooterMeasureBlocks(blocks: FlowBlock[]): FlowBlock[];

Make `columnWidths` exactly `colCount` long with every entry positive. Missing trailing columns inherit the average of existing positives; zero or negative entries split the leftover `targetWidth` evenly. Callers scale down totals that exceed the target — this helper only fills gaps.

declare function normalizeTableColumnWidths(columnWidths: number[], colCount: number, targetWidth: number): number[];

Map a PM position to X coordinates within a line (for caret positioning).

declare function positionToX(block: ParagraphBlock, measure: ParagraphMeasure, pmPosition: number, _fragmentWidth: number): {
    x: number;
    lineIndex: number;
} | null;

Convert points to pixels

declare function ptToPx(pt: number): number;

Convert pixels to OOXML half-points

declare function pxToHalfPt(px: number): number;

Convert pixels to points

declare function pxToPt(px: number): number;

Convert pixels to twips

declare function pxToTwips(px: number): number;
declare function rectsToFloatingZones(rects: FloatingExclusionRect[], contentWidth: number): FloatingImageZone[];

Reset the block ID counter (useful for testing).

declare function resetBlockIdCounter(): void;

Reset the canvas context (useful for testing)

declare function resetCanvasContext(): void;

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.

declare function resolveHeaderFooter(doc: Document | null, sp: SectionProperties | null | undefined): {
    header: HeaderFooter | null;
    footer: HeaderFooter | null;
    firstHeader: HeaderFooter | null;
    firstFooter: HeaderFooter | null;
};
declare function resolveHeaderFooterVisualTop(run: ImageRun, paragraphY: number, flowHeight: number, metrics: HeaderFooterMetrics): number;

Resolve an OOXML lvlText template like "%1.%2." against the counter stack and per-level numFmt list (ECMA-376 §17.9.11).

When a referenced counter has no value yet (e.g. "%2" referenced from a level-0 paragraph), the placeholder AND the punctuation immediately following it are dropped — matches Word's behavior so "%1.%2." renders "1." rather than "1..".

Exported for unit testing.

declare function resolveListTemplate(template: string, counters: number[], levelNumFmts: NumberFormat[] | undefined): string;

Convert a ProseMirror selection range to screen rectangles.

declare function selectionToRects(layout: Layout, blocks: FlowBlock[], measures: Measure[], from: number, to: number): SelectionRect[];

Store font metrics in cache

declare function setCachedFontMetrics(fontFamily: string, fontSize: number, bold: boolean, italic: boolean, metrics: FontMetricsEntry): void;

Store paragraph measurement in cache

declare function setCachedParagraphMeasure(block: ParagraphBlock, maxWidth: number, measure: ParagraphMeasure): void;

Store text width in cache

declare function setCachedTextWidth(text: string, font: string, letterSpacing: number, width: number): void;

Set the maximum size of the font metrics cache

declare function setFontCacheSize(size: number): void;

Set the maximum size of the paragraph measure cache

declare function setParagraphCacheSize(size: number): void;

Set the maximum size of the text width cache

declare function setTextCacheSize(size: number): void;

Run the multi-pass footnote layout loop. Reserving footnote space on a page can move a reference to another page, which changes the reservation, which can move references again. Iterate until the page→height contract is the same one used by the latest layout, or `MAX_FOOTNOTE_LAYOUT_PASSES` passes have run.

Lives in core so the React + Vue adapters call the same loop and stay in lockstep on convergence behaviour. Writes `page.footnoteIds` onto each page in the returned layout so renderers can paint footnote areas.

declare function stabilizeFootnoteLayout(args: StabilizeFootnoteLayoutArgs): StabilizeFootnoteLayoutResult;

Convert a ProseMirror document to FlowBlock array.

Walks the document tree, converting each node to the appropriate block type. Tracks pmStart/pmEnd positions for each block for click-to-position mapping.

declare function toFlowBlocks(doc: Node, options?: ToFlowBlocksOptions): FlowBlock[];

Convert twips to pixels (1 twip = 1/20 point, 96 pixels per inch).

declare function twipsToPixels(twips: number): number;

Convert twips to pixels

declare function twipsToPx(twips: number): number;

Interfaces(15)

Get caret position from DOM for a PM position.

interface DomCaretPosition
MemberTypeSummary
heightnumber
pageIndexnumber
xnumber
ynumber

Get selection rectangles for a PM range using DOM-based detection.

interface DomSelectionRect
MemberTypeSummary
heightnumber
pageIndexnumber
widthnumber
xnumber
ynumber
interface

ExtendMarginsForHeaderFooterInput

packages/core/src/layout-bridge/headerFooterMargins.ts:46
interface ExtendMarginsForHeaderFooterInput
MemberTypeSummary
bodyBlocks?FlowBlock[]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).
finalMarginsPageMarginsFinal-section margins (last `sectPr`).
footers?Array<HeaderFooterContent | undefined>Footer variants in play this layout.
headers?Array<HeaderFooterContent | undefined>Header variants in play this layout (e.g. default + first-page).
marginsPageMarginsBody fallback margins.
pageSize{ w: number; h: number; }
warn?(message: string) => voidOptional diagnostic sink for the clamp (adapters pass `console.warn`).
interface

ExtendMarginsForHeaderFooterResult

packages/core/src/layout-bridge/headerFooterMargins.ts:67
interface ExtendMarginsForHeaderFooterResult
MemberTypeSummary
finalMarginsPageMargins
marginsPageMargins
interface FloatingExclusionRect
MemberTypeSummary
distBottomnumber
distLeftnumber
distRightnumber
distTopnumber
heightnumber
side'left' | 'right'Which side the object is on for simple one-sided wrapping.
widthnumber
wrapText?WrapTextDirection
wrapType?string
xnumberX position relative to the content area.
ynumberY position relative to the content area.
interface FloatingImageZone
MemberTypeSummary
bottomYnumber
fullWidthBlock?booleanFull-width vertical band (OOXML `topAndBottom` wrap): no text fits beside it, so any line overlapping `[topY, bottomY]` is pushed below the band.
leftMarginnumber
rightMarginnumber
segments?FloatingLineSegmentZone[]
topYnumber
interface FloatingLineSegmentZone
MemberTypeSummary
availableWidthnumber
leftOffsetnumber

Typography metrics for a font

interface FontMetrics
MemberTypeSummary
ascentnumber
descentnumber
fontFamilystring
fontSizenumber
lineHeightnumber
singleLineRationumberOS/2 single-line ratio for OOXML line spacing calculation

Font styling properties for measurement

interface FontStyle
MemberTypeSummary
bold?boolean
fontFamily?string
fontSize?number
italic?boolean
letterSpacing?number

Options for paragraph measurement

interface MeasureParagraphOptions
MemberTypeSummary
floatingZones?FloatingImageZone[]Floating image exclusion zones that affect line widths
paragraphYOffset?numberY offset of this paragraph relative to the exclusion zones (default: 0)

A cell with its resolved grid position (column index honoring spans).

interface ResolvedGridCell
MemberTypeSummary
cellIndexnumber
colSpannumber
columnIndexnumber
rowIndexnumber
rowSpannumber

Result of measuring a run of text

interface RunMeasurement
MemberTypeSummary
charWidthsnumber[]
metricsFontMetrics
widthnumber
interface StabilizeFootnoteLayoutArgs
MemberTypeSummary
blocksFlowBlock[]
footnoteContentMapMap<number, FootnoteContent>
footnoteRefsArray<{ footnoteId: number; pmPos: number; }>
initialLayoutLayoutFirst-pass layout already computed by the caller without reserved heights.
layoutOptsLayoutOptions
measuresMeasure[]
interface

StabilizeFootnoteLayoutResult

packages/core/src/layout-bridge/footnoteLayout.ts:407
interface StabilizeFootnoteLayoutResult
MemberTypeSummary
convergedbooleanTrue if the loop converged before hitting MAX_FOOTNOTE_LAYOUT_PASSES.
layoutLayout
pageFootnoteMapMap<number, number[]>

Result of measuring a text string

interface TextMeasurement
MemberTypeSummary
ascentnumber
descentnumber
heightnumber
widthnumber

Type aliases(17)

type

CaretPosition_2

Caret position for collapsed selection.

type CaretPosition = {
    x: number;
    y: number;
    height: number;
    pageIndex: number;
};

Options for [convertFootnoteToContent](convertFootnoteToContent).

type ConvertFootnoteOptions = {
    styles?: StyleDefinitions | null;
    theme?: Theme | null;
    measureBlocks: MeasureBlocksFn;
    defaultTabStopTwips?: number | null;
};
type ConvertHeaderFooterOptions = {
    styles?: StyleDefinitions | null;
    theme?: Theme | null;
    measureBlocks: MeasureBlocksFn;
    defaultTabStopTwips?: number | null;
};

Page geometry (CSS px) used to resolve page/margin-relative anchored objects into content-area coordinates — currently the vertical anchor of a top-pinned `topAndBottom` band. Same shape the painter uses (see `pageGeometryFromPage`), so both paths resolve to identical positions.

type FloatPageGeometry = PageGeometry;

Result of hit-testing to find which fragment contains a point.

type FragmentHit = {
    fragment: Fragment;
    block: FlowBlock;
    measure: Measure;
    pageIndex: number;
    localY: number;
    localX: number;
};

Header / Footer Layout Utilities

The header/footer rendering pipeline lives here so any rendering adapter (React, Vue, etc.) can share the conversion logic and just supply its platform-specific [MeasureBlocksFn](MeasureBlocksFn). Mirrors the footnote pipeline in `footnoteLayout.ts`.

Pipeline: HF.content → headerFooterToProseDoc → toFlowBlocks → measureBlocks (caller-supplied, Canvas-aware) → HeaderFooterContent (blocks, measures, height, visualTop/Bottom)

The render side uses the normalized block list so paint and measurement stay in lockstep. Visual-bounds calculation still inspects the original block list because floating images can paint above/below the nominal flow box even when they do not contribute to flow height.

type HeaderFooterMetrics = {
    section: 'header' | 'footer';
    pageSize: {
        w: number;
        h: number;
    };
    margins: PageMargins;
};

Combined result of all hit testing.

type HitTestResult = {
    pageHit: PageHit | null;
    fragmentHit: FragmentHit | null;
    tableCellHit: TableCellHit | null;
};

Block-measurement callback shape passed to [measureBlocksWithFloats](measureBlocksWithFloats). Adapters (React, Vue) supply this so they can decide platform-specific concerns (e.g. paragraph-measure caching, per-section width) while sharing the floating-zone orchestration. This is adapter-author API, not end-consumer API.

type MeasureBlockFn = (block: FlowBlock, contentWidth: number, floatingZones?: FloatingImageZone[], cumulativeY?: number) => Measure;

Adapter-supplied block measurement function. The caller (React / Vue / etc.) supplies its platform's measure routine — at minimum paragraph + table + image + textBox — so this core helper stays Canvas-free.

type MeasureBlocksFn = (blocks: FlowBlock[], contentWidth: number) => Measure[];

Result of hit-testing to find which page contains a point.

type PageHit = {
    pageIndex: number;
    page: Page;
    pageY: number;
};

A 2D point in page coordinate space.

type Point = {
    x: number;
    y: number;
};

Result of click-to-position mapping.

type PositionResult = {
    pmPosition: number;
    charOffset: number;
    lineIndex: number;
};

A rectangle representing part of a selection.

type SelectionRect = {
    x: number;
    y: number;
    width: number;
    height: number;
    pageIndex: number;
};

Result of hit-testing a table to find the cell.

type TableCellHit = {
    fragment: TableFragment;
    block: TableBlock;
    measure: TableMeasure;
    pageIndex: number;
    rowIndex: number;
    colIndex: number;
    cellBlock?: ParagraphBlock;
    cellMeasure?: ParagraphMeasure;
    cellLocalX: number;
    cellLocalY: number;
};
type TableInsertHoverHit = {
    type: 'row' | 'column';
    clientX: number;
    clientY: number;
    cellPmPos: number;
};
type TableInsertHoverInput = {
    mouseX: number;
    mouseY: number;
    pagesContainer: HTMLElement;
    target: HTMLElement;
    hfEditMode: 'header' | 'footer' | null;
    edgeProximity?: number;
};

Options for the conversion.

type ToFlowBlocksOptions = {
    defaultFont?: string;
    defaultSize?: number;
    theme?: Theme | null;
    pageContentHeight?: number;
    defaultTabStopTwips?: number;
    listCounters?: Map<number, number[]>;
    listSeenNumIds?: Set<string>;
};

Variables(8)

1 inch at 96 DPI — Word's default body margins.

DEFAULT_BODY_MARGIN_PX = 96

Word's default `headerDistance` / `footerDistance` (0.5in = 48px).

DEFAULT_HF_DISTANCE_PX = 48

US Letter at 96 DPI.

DEFAULT_PAGE_HEIGHT_PX = 1056

US Letter at 96 DPI — Word's default page size.

DEFAULT_PAGE_WIDTH_PX = 816

Separator line height + vertical padding in pixels.

FOOTNOTE_SEPARATOR_HEIGHT = 12

Hard cap on the multi-pass footnote layout loop. Reserving footnote space can move a reference to another page, so adapters keep remapping until the page→height contract is stable. Dense layouts converge in 2–3 passes in practice; 6 is a safe ceiling.

MAX_FOOTNOTE_LAYOUT_PASSES = 6

Table-insert "+" hover hit-test.

Pure DOM logic for the floating row/column insert button that shows when the mouse is near the left or top edge of a layout table. Lives in core so React + Vue + any future adapter can share the hit-test and just wire up their own UI rendering of the button.

The function is gated by `hfEditMode`: tables in inactive HF/body regions don't surface the affordance. A header table only matches when the user is editing the header; a body table only matches when not in any HF edit mode.

TABLE_INSERT_EDGE_PROXIMITY = 30
TABLE_INSERT_HIDE_DELAY_MS = 200