New

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

API Referencev1.0.2

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

Layout Engine - Main Entry Point

Converts blocks + measures into positioned fragments on pages.

Stable enough for the first-party React adapter, but the API may change in minor releases until a third-party adapter validates it. Pin a version range if you depend on this directly.

Functions(16)

Apply pending section state to active state at a page boundary. Transfers all pending values to active and clears pending.

declare function applyPendingToActive(state: SectionState): SectionState;

Exhaustiveness guard for `FlowBlock`-shaped switches. Call from the `default` arm with the still-typed value; TypeScript will refuse to compile if any variant of `FlowBlock` was missed. The thrown error names the calling site so runtime failures (e.g. an old adapter compiled against a newer core) point future debuggers at the contract.

declare function assertExhaustiveFlowBlock(block: never, site: string): never;

Calculate the total height needed to keep a chain together.

Includes all chain members plus the first line of the anchor paragraph.

declare function calculateChainHeight(chain: KeepNextChain, blocks: FlowBlock[], measures: Measure[]): number;

Pre-scan blocks to find all keepNext chains.

A keepNext chain is a sequence of consecutive paragraphs with keepNext=true, followed by an anchor paragraph (the first non-keepNext paragraph). The entire chain must stay on the same page as the anchor's first line.

Returns a map from chain start index to chain info.

declare function computeKeepNextChains(blocks: FlowBlock[]): Map<number, KeepNextChain>;

Create initial section state from default options.

declare function createInitialSectionState(margins: PageMargins, pageSize: {
    w: number;
    h: number;
}, columns?: ColumnLayout): SectionState;

Creates a paginator for managing page layout state.

declare function createPaginator(options: PaginatorOptions): {
    pages: Page[];
    states: PageState[];
    readonly columnWidth: number;
    readonly columns: {
        count: number;
        gap: number;
        equalWidth?: boolean;
        separator?: boolean;
    };
    getCurrentState: () => PageState;
    getAvailableHeight: () => number;
    getContentWidth: () => number;
    fits: (height: number) => boolean;
    ensureFits: (height: number) => PageState;
    addFragment: (fragment: Fragment, height: number, spaceBefore?: number, spaceAfter?: number) => {
        state: PageState;
        x: number;
        y: number;
    };
    forcePageBreak: () => PageState;
    forceColumnBreak: () => PageState;
    getColumnX: (columnIndex: number) => number;
    updateColumns: (newColumns: ColumnLayout) => void;
    updatePageLayout: (newPageSize?: {
        w: number;
        h: number;
    }, newMargins?: PageMargins, applyImmediately?: boolean) => void;
};

Page index (0-based) whose layout fragments cover `pmPos`, or null if none. Used when the painted DOM may not yet have `[data-pm-start]` for this position (virtualization).

Range semantics: `[pmStart, pmEnd)` — half-open, matching ProseMirror's `pos + nodeSize` convention. Boundary positions belong to the next fragment, so when a fragment ends at the same position the next one starts, the next fragment wins (avoids returning the previous page for the start of the next paragraph).

declare function findPageIndexContainingPmPos(layout: Layout, pmPos: number): number | null;

Get the effective columns for the current section state.

declare function getEffectiveColumns(state: SectionState): ColumnLayout;

Get the effective margins for the current section state. Returns active margins, or pending if scheduled.

declare function getEffectiveMargins(state: SectionState): PageMargins;

Get the effective page size for the current section state.

declare function getEffectivePageSize(state: SectionState): {
    w: number;
    h: number;
};

Calculate total height of header rows from their measures.

declare function getHeaderRowsHeight(measure: TableMeasure, headerRowCount: number): number;

Get the set of indices that are mid-chain (not chain starters). These should skip the keepNext check since their chain starter already decided.

declare function getMidChainIndices(chains: Map<number, KeepNextChain>): Set<number>;

Check if a paragraph has keepLines property (all lines must stay together).

declare function hasKeepLines(block: FlowBlock): boolean;

Check if a paragraph should start on a new page (pageBreakBefore).

declare function hasPageBreakBefore(block: FlowBlock): boolean;

Layout a document: convert blocks + measures into pages with positioned fragments.

Algorithm: 1. Walk blocks in order with their corresponding measures 2. For each block, create appropriate fragment(s) 3. Use paginator to manage page/column state 4. Handle page breaks, section breaks, and keepNext chains

declare function layoutDocument(blocks: FlowBlock[], measures: Measure[], options?: LayoutOptions): Layout;

Schedule section break effects by analyzing the break type and updating state.

This determines what layout changes should occur (page break, column changes) and schedules the new section properties to be applied at the appropriate boundary.

declare function scheduleSectionBreak(block: SectionBreakBlock, state: SectionState, _baseMargins: PageMargins): {
    decision: BreakDecision;
    state: SectionState;
};

Type aliases(67)

Unique identifier for a block in the document. Format: typically `${index}-${type}` or just the block index.

type BlockId = string | number;

Border specification for paragraphs.

type BorderStyle = {
    style?: string;
    width?: number;
    color?: string;
    space?: number;
};

Decision about what happens at a section break.

type BreakDecision = {
    forcePageBreak: boolean;
    forceMidPageRegion: boolean;
    requiredParity?: 'even' | 'odd';
};

Cell borders (all four sides).

type CellBorders = {
    top?: CellBorderSpec;
    bottom?: CellBorderSpec;
    left?: CellBorderSpec;
    right?: CellBorderSpec;
};

Cell border specification for rendering.

type CellBorderSpec = {
    width?: number;
    color?: string;
    style?: string;
};

Column break block.

type ColumnBreakBlock = {
    kind: 'columnBreak';
    id: BlockId;
    pmStart?: number;
    pmEnd?: number;
};

Measurement result for column break (no visual size).

type ColumnBreakMeasure = {
    kind: 'columnBreak';
};

Column layout configuration.

type ColumnLayout = {
    count: number;
    gap: number;
    equalWidth?: boolean;
    separator?: boolean;
};

Position within the document model.

type DocumentPosition = {
    blockIndex: number;
    runIndex?: number;
    charOffset?: number;
    pmPos?: number;
};

A field run (PAGE, NUMPAGES, etc.) that gets substituted at render time.

type FieldRun = RunFormatting & {
    kind: 'field';
    fieldType: 'PAGE' | 'NUMPAGES' | 'DATE' | 'TIME' | 'OTHER';
    fallback?: string;
    pmStart?: number;
    pmEnd?: number;
};

Floating table positioning info (pixel values).

type FloatingTablePosition = {
    horzAnchor?: 'margin' | 'page' | 'text';
    vertAnchor?: 'margin' | 'page' | 'text';
    tblpX?: number;
    tblpXSpec?: 'left' | 'center' | 'right' | 'inside' | 'outside';
    tblpY?: number;
    tblpYSpec?: 'top' | 'center' | 'bottom' | 'inside' | 'outside' | 'inline';
    topFromText?: number;
    bottomFromText?: number;
    leftFromText?: number;
    rightFromText?: number;
};

Union of every block kind the layout engine knows about.

Three switches over `block.kind` must stay in sync with this type: - `runLayoutPipeline` in `layout-engine/index.ts` (this package) - `measureBlock` in `packages/react/src/paged-editor/PagedEditor.tsx` - `measureBlock` in `packages/vue/src/composables/useDocxEditor.ts`

All three end in `assertExhaustiveFlowBlock(block, '<site>')` so adding a new variant here without updating every site is a typecheck error.

type FlowBlock = ParagraphBlock | TableBlock | ImageBlock | TextBoxBlock | SectionBreakBlock | PageBreakBlock | ColumnBreakBlock;

Pre-calculated footnote content for layout and rendering.

type FootnoteContent = {
    id: number;
    displayNumber: number;
    blocks: FlowBlock[];
    measures: Measure[];
    height: number;
};

Union of all fragment types.

type Fragment = ParagraphFragment | TableFragment | ImageFragment | TextBoxFragment;

Base fragment properties common to all fragment types.

type FragmentBase = {
    blockId: BlockId;
    x: number;
    y: number;
    width: number;
    pmStart?: number;
    pmEnd?: number;
};

Header/footer content heights by variant type.

type HeaderFooterContentHeights = Partial<Record<'default' | 'first' | 'even' | 'odd', number>>;

Header/footer layout for a specific type.

type HeaderFooterLayout = {
    height: number;
    fragments: Fragment[];
};

Result of hit-testing a click position.

type HitTestResult = {
    pageIndex: number;
    fragment?: Fragment;
    localX?: number;
    localY?: number;
};

Hyperlink information for a run.

type HyperlinkInfo = {
    href: string;
    tooltip?: string;
    noDefaultStyle?: boolean;
};

An anchored/floating image block.

type ImageBlock = {
    kind: 'image';
    id: BlockId;
    src: string;
    width: number;
    height: number;
    alt?: string;
    transform?: string;
    anchor?: {
        isAnchored?: boolean;
        offsetH?: number;
        offsetV?: number;
        behindDoc?: boolean;
    };
    hlinkHref?: string;
    pmStart?: number;
    pmEnd?: number;
};

An image fragment positioned on a page.

type ImageFragment = FragmentBase & {
    kind: 'image';
    height: number;
    isAnchored?: boolean;
    zIndex?: number;
};

Measurement result for an image block.

type ImageMeasure = {
    kind: 'image';
    width: number;
    height: number;
};

An inline image run.

type ImageRun = {
    kind: 'image';
    src: string;
    width: number;
    height: number;
    alt?: string;
    transform?: string;
    position?: ImageRunPosition;
    wrapType?: string;
    displayMode?: 'inline' | 'block' | 'float';
    cssFloat?: 'left' | 'right' | 'none';
    distTop?: number;
    distBottom?: number;
    distLeft?: number;
    distRight?: number;
    cropTop?: number;
    cropRight?: number;
    cropBottom?: number;
    cropLeft?: number;
    opacity?: number;
    pmStart?: number;
    pmEnd?: number;
};

Position data for floating/anchored images.

type ImageRunPosition = {
    horizontal?: {
        relativeTo?: string;
        posOffset?: number;
        align?: string;
    };
    vertical?: {
        relativeTo?: string;
        posOffset?: number;
        align?: string;
    };
};

A chain of consecutive keepNext paragraphs.

type KeepNextChain = {
    startIndex: number;
    endIndex: number;
    memberIndices: number[];
    anchorIndex: number;
};

Final layout output ready for rendering/painting.

type Layout = {
    pageSize: {
        w: number;
        h: number;
    };
    pages: Page[];
    columns?: ColumnLayout;
    headers?: Record<string, HeaderFooterLayout>;
    footers?: Record<string, HeaderFooterLayout>;
    pageGap?: number;
};

Options for the layout engine.

type LayoutOptions = {
    pageSize: {
        w: number;
        h: number;
    };
    margins: PageMargins;
    finalPageSize?: {
        w: number;
        h: number;
    };
    finalMargins?: PageMargins;
    columns?: ColumnLayout;
    pageGap?: number;
    defaultLineHeight?: number;
    headerContentHeights?: HeaderFooterContentHeights;
    footerContentHeights?: HeaderFooterContentHeights;
    titlePage?: boolean;
    evenAndOddHeaders?: boolean;
    footnoteReservedHeights?: Map<number, number>;
    bodyBreakType?: 'continuous' | 'nextPage' | 'evenPage' | 'oddPage';
};

A line break run.

type LineBreakRun = {
    kind: 'lineBreak';
    pmStart?: number;
    pmEnd?: number;
};

List numbering properties for a paragraph.

type ListNumPr = {
    numId?: number;
    ilvl?: number;
};

Union of all measurement types.

type Measure = ParagraphMeasure | ImageMeasure | TableMeasure | TextBoxMeasure | SectionBreakMeasure | PageBreakMeasure | ColumnBreakMeasure;

A measured line within a paragraph.

type MeasuredLine = {
    fromRun: number;
    fromChar: number;
    toRun: number;
    toChar: number;
    width: number;
    ascent: number;
    descent: number;
    lineHeight: number;
    leftOffset?: number;
    rightOffset?: number;
    segments?: MeasuredLineSegment[];
};
type MeasuredLineSegment = {
    fromRun: number;
    fromChar: number;
    toRun: number;
    toChar: number;
    width: number;
    leftOffset: number;
    availableWidth: number;
};

A rendered page containing positioned fragments.

type Page = {
    number: number;
    fragments: Fragment[];
    margins: PageMargins;
    size: {
        w: number;
        h: number;
    };
    orientation?: 'portrait' | 'landscape';
    sectionIndex?: number;
    headerFooterRefs?: {
        headerDefault?: string;
        headerFirst?: string;
        headerEven?: string;
        footerDefault?: string;
        footerFirst?: string;
        footerEven?: string;
    };
    footnoteIds?: number[];
    footnoteReservedHeight?: number;
    columns?: ColumnLayout;
};

Explicit page break block.

type PageBreakBlock = {
    kind: 'pageBreak';
    id: BlockId;
    pmStart?: number;
    pmEnd?: number;
};

Measurement result for page break (no visual size).

type PageBreakMeasure = {
    kind: 'pageBreak';
};

Page margin configuration.

type PageMargins = {
    top: number;
    right: number;
    bottom: number;
    left: number;
    header?: number;
    footer?: number;
};

Current state of a page being laid out.

type PageState = {
    page: Page;
    cursorY: number;
    columnIndex: number;
    topMargin: number;
    contentBottom: number;
    trailingSpacing: number;
};
type Paginator = ReturnType<typeof createPaginator>;

Options for creating a paginator.

type PaginatorOptions = {
    pageSize: {
        w: number;
        h: number;
    };
    margins: PageMargins;
    columns?: ColumnLayout;
    footnoteReservedHeights?: Map<number, number>;
    onNewPage?: (state: PageState) => void;
};

Paragraph block attributes.

type ParagraphAttrs = {
    alignment?: 'left' | 'center' | 'right' | 'justify';
    spacing?: ParagraphSpacing;
    spacingExplicit?: {
        before?: boolean;
        after?: boolean;
    };
    indent?: ParagraphIndent;
    keepNext?: boolean;
    keepLines?: boolean;
    pageBreakBefore?: boolean;
    styleId?: string;
    contextualSpacing?: boolean;
    bidi?: boolean;
    borders?: ParagraphBorders;
    shading?: string;
    tabs?: TabStop[];
    numPr?: ListNumPr;
    listMarker?: string;
    listIsBullet?: boolean;
    listMarkerHidden?: boolean;
    listMarkerFontFamily?: string;
    listMarkerFontSize?: number;
    defaultFontSize?: number;
    defaultFontFamily?: string;
    suppressEmptyParagraphHeight?: boolean;
};

A paragraph block containing runs.

type ParagraphBlock = {
    kind: 'paragraph';
    id: BlockId;
    runs: Run[];
    attrs?: ParagraphAttrs;
    pmStart?: number;
    pmEnd?: number;
};

Paragraph borders.

type ParagraphBorders = {
    top?: BorderStyle;
    bottom?: BorderStyle;
    left?: BorderStyle;
    right?: BorderStyle;
    between?: BorderStyle;
    bar?: BorderStyle;
};

A paragraph fragment positioned on a page. May span only part of the paragraph's lines if split across pages.

type ParagraphFragment = FragmentBase & {
    kind: 'paragraph';
    fromLine: number;
    toLine: number;
    height: number;
    continuesFromPrev?: boolean;
    continuesOnNext?: boolean;
};

Paragraph indentation configuration.

type ParagraphIndent = {
    left?: number;
    right?: number;
    firstLine?: number;
    hanging?: number;
};

Measurement result for a paragraph block.

type ParagraphMeasure = {
    kind: 'paragraph';
    lines: MeasuredLine[];
    totalHeight: number;
};

Paragraph spacing configuration.

type ParagraphSpacing = {
    before?: number;
    after?: number;
    line?: number;
    lineUnit?: 'px' | 'multiplier';
    lineRule?: 'auto' | 'exact' | 'atLeast';
};

Union of all run types.

type Run = TextRun | TabRun | ImageRun | LineBreakRun | FieldRun;

Common run formatting properties applied to text runs.

type RunFormatting = {
    bold?: boolean;
    italic?: boolean;
    underline?: boolean | {
        style?: string;
        color?: string;
    };
    strike?: boolean;
    color?: string;
    highlight?: string;
    fontFamily?: string;
    fontSize?: number;
    letterSpacing?: number;
    superscript?: boolean;
    subscript?: boolean;
    allCaps?: boolean;
    smallCaps?: boolean;
    positionPx?: number;
    horizontalScale?: number;
    kerningMinPt?: number;
    imprint?: boolean;
    emboss?: boolean;
    textShadow?: boolean;
    textOutline?: boolean;
    emphasisMark?: 'dot' | 'comma' | 'circle' | 'underDot';
    hidden?: boolean;
    rtl?: boolean;
    textEffect?: 'blinkBackground' | 'lights' | 'antsBlack' | 'antsRed' | 'shimmer' | 'sparkle';
    hyperlink?: HyperlinkInfo;
    footnoteRefId?: number;
    endnoteRefId?: number;
    commentIds?: number[];
    isInsertion?: boolean;
    isDeletion?: boolean;
    changeAuthor?: string;
    changeDate?: string;
    changeRevisionId?: number;
};

Section break block defining page layout changes.

type SectionBreakBlock = {
    kind: 'sectionBreak';
    id: BlockId;
    type?: 'continuous' | 'nextPage' | 'evenPage' | 'oddPage';
    pageSize?: {
        w: number;
        h: number;
    };
    orientation?: 'portrait' | 'landscape';
    margins?: PageMargins;
    columns?: ColumnLayout;
};

Measurement result for section break (no visual size).

type SectionBreakMeasure = {
    kind: 'sectionBreak';
};

Page-flow geometry resolved from a single section's properties. Exported so the React paged editor can reuse the same shape when measuring blocks per section width — keeping pagination and measurement consistent.

type SectionLayoutConfig = {
    pageSize: {
        w: number;
        h: number;
    };
    margins: PageMargins;
    columns?: ColumnLayout;
};

State tracking for sections during layout. Uses active/pending pattern to schedule changes at page boundaries.

type SectionState = {
    activeTopMargin: number;
    activeBottomMargin: number;
    activeLeftMargin: number;
    activeRightMargin: number;
    pendingTopMargin: number | null;
    pendingBottomMargin: number | null;
    pendingLeftMargin: number | null;
    pendingRightMargin: number | null;
    activePageSize: {
        w: number;
        h: number;
    };
    pendingPageSize: {
        w: number;
        h: number;
    } | null;
    activeColumns: ColumnLayout;
    pendingColumns: ColumnLayout | null;
    activeOrientation: 'portrait' | 'landscape' | null;
    pendingOrientation: 'portrait' | 'landscape' | null;
    hasAnyPages: boolean;
};

Tab stop alignment types

type TabAlignment = 'start' | 'end' | 'center' | 'decimal' | 'bar' | 'clear';

A table block containing rows.

type TableBlock = {
    kind: 'table';
    id: BlockId;
    rows: TableRow[];
    columnWidths?: number[];
    width?: number;
    widthType?: string;
    justification?: 'left' | 'center' | 'right';
    indent?: number;
    floating?: FloatingTablePosition;
    pmStart?: number;
    pmEnd?: number;
};

A table cell with content.

type TableCell = {
    id: BlockId;
    blocks: FlowBlock[];
    colSpan?: number;
    rowSpan?: number;
    width?: number;
    widthValue?: number;
    widthType?: string;
    verticalAlign?: 'top' | 'center' | 'bottom';
    background?: string;
    borders?: CellBorders;
    padding?: {
        top: number;
        right: number;
        bottom: number;
        left: number;
    };
    noWrap?: boolean;
};

Measurement result for a table cell.

type TableCellMeasure = {
    blocks: Measure[];
    width: number;
    height: number;
    colSpan?: number;
    rowSpan?: number;
};

A table fragment positioned on a page. May span only part of the table's rows if split across pages.

type TableFragment = FragmentBase & {
    kind: 'table';
    fromRow: number;
    toRow: number;
    height: number;
    isFloating?: boolean;
    continuesFromPrev?: boolean;
    continuesOnNext?: boolean;
    headerRowCount?: number;
};

Measurement result for a table block.

type TableMeasure = {
    kind: 'table';
    rows: TableRowMeasure[];
    columnWidths: number[];
    totalWidth: number;
    totalHeight: number;
};

A table row containing cells.

type TableRow = {
    id: BlockId;
    cells: TableCell[];
    height?: number;
    heightRule?: 'auto' | 'atLeast' | 'exact';
    isHeader?: boolean;
};

Measurement result for a table row.

type TableRowMeasure = {
    cells: TableCellMeasure[];
    height: number;
};

A tab character run.

type TabRun = RunFormatting & {
    kind: 'tab';
    width?: number;
    pmStart?: number;
    pmEnd?: number;
};

Tab stop definition

type TabStop = {
    val: TabAlignment;
    pos: number;
    leader?: 'none' | 'dot' | 'hyphen' | 'underscore' | 'heavy' | 'middleDot';
};

Text box block — positioned container with paragraph content.

type TextBoxBlock = {
    kind: 'textBox';
    id: BlockId;
    width: number;
    height?: number;
    fillColor?: string;
    outlineWidth?: number;
    outlineColor?: string;
    outlineStyle?: string;
    margins?: {
        top: number;
        bottom: number;
        left: number;
        right: number;
    };
    content: ParagraphBlock[];
    displayMode?: 'inline' | 'float' | 'block';
    cssFloat?: 'left' | 'right' | 'none';
    wrapType?: string;
    wrapText?: WrapTextDirection;
    anchorTarget?: 'followingBlock';
    position?: ImageRunPosition;
    distTop?: number;
    distBottom?: number;
    distLeft?: number;
    distRight?: number;
    pmStart?: number;
    pmEnd?: number;
};

A text box fragment positioned on a page.

type TextBoxFragment = FragmentBase & {
    kind: 'textBox';
    height: number;
    isFloating?: boolean;
    zIndex?: number;
};

Measurement result for a text box block.

type TextBoxMeasure = {
    kind: 'textBox';
    width: number;
    height: number;
    innerMeasures: ParagraphMeasure[];
};

A text run within a paragraph.

type TextRun = RunFormatting & {
    kind: 'text';
    text: string;
    hyperlink?: HyperlinkInfo;
    pmStart?: number;
    pmEnd?: number;
};
type WrapTextDirection = 'bothSides' | 'left' | 'right' | 'largest';

Variables(2)

Default internal margins for text boxes (OOXML defaults in pixels)

DEFAULT_TEXTBOX_MARGINS: {
    top: number;
    bottom: number;
    left: number;
    right: number;
}

Default text box width in pixels when no width is specified

DEFAULT_TEXTBOX_WIDTH = 200