@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)
applyPendingToActive
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;assertExhaustiveFlowBlock
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;calculateChainHeight
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;computeKeepNextChains
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>;createInitialSectionState
Create initial section state from default options.
declare function createInitialSectionState(margins: PageMargins, pageSize: {
w: number;
h: number;
}, columns?: ColumnLayout): SectionState;createPaginator
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;
};findPageIndexContainingPmPos
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;getEffectiveColumns
Get the effective columns for the current section state.
declare function getEffectiveColumns(state: SectionState): ColumnLayout;getEffectiveMargins
Get the effective margins for the current section state. Returns active margins, or pending if scheduled.
declare function getEffectiveMargins(state: SectionState): PageMargins;getEffectivePageSize
Get the effective page size for the current section state.
declare function getEffectivePageSize(state: SectionState): {
w: number;
h: number;
};getHeaderRowsHeight
Calculate total height of header rows from their measures.
declare function getHeaderRowsHeight(measure: TableMeasure, headerRowCount: number): number;getMidChainIndices
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>;hasKeepLines
Check if a paragraph has keepLines property (all lines must stay together).
declare function hasKeepLines(block: FlowBlock): boolean;hasPageBreakBefore
Check if a paragraph should start on a new page (pageBreakBefore).
declare function hasPageBreakBefore(block: FlowBlock): boolean;layoutDocument
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;scheduleSectionBreak
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)
BlockId
Unique identifier for a block in the document. Format: typically `${index}-${type}` or just the block index.
type BlockId = string | number;BorderStyle
Border specification for paragraphs.
type BorderStyle = {
style?: string;
width?: number;
color?: string;
space?: number;
};BreakDecision
Decision about what happens at a section break.
type BreakDecision = {
forcePageBreak: boolean;
forceMidPageRegion: boolean;
requiredParity?: 'even' | 'odd';
};CellBorders
Cell borders (all four sides).
type CellBorders = {
top?: CellBorderSpec;
bottom?: CellBorderSpec;
left?: CellBorderSpec;
right?: CellBorderSpec;
};CellBorderSpec
Cell border specification for rendering.
type CellBorderSpec = {
width?: number;
color?: string;
style?: string;
};ColumnBreakBlock
Column break block.
type ColumnBreakBlock = {
kind: 'columnBreak';
id: BlockId;
pmStart?: number;
pmEnd?: number;
};ColumnBreakMeasure
Measurement result for column break (no visual size).
type ColumnBreakMeasure = {
kind: 'columnBreak';
};ColumnLayout
Column layout configuration.
type ColumnLayout = {
count: number;
gap: number;
equalWidth?: boolean;
separator?: boolean;
};DocumentPosition
Position within the document model.
type DocumentPosition = {
blockIndex: number;
runIndex?: number;
charOffset?: number;
pmPos?: number;
};FieldRun
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;
};FloatingTablePosition
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;
};FlowBlock
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;FootnoteContent
Pre-calculated footnote content for layout and rendering.
type FootnoteContent = {
id: number;
displayNumber: number;
blocks: FlowBlock[];
measures: Measure[];
height: number;
};Fragment
Union of all fragment types.
type Fragment = ParagraphFragment | TableFragment | ImageFragment | TextBoxFragment;FragmentBase
Base fragment properties common to all fragment types.
type FragmentBase = {
blockId: BlockId;
x: number;
y: number;
width: number;
pmStart?: number;
pmEnd?: number;
};HitTestResult
Result of hit-testing a click position.
type HitTestResult = {
pageIndex: number;
fragment?: Fragment;
localX?: number;
localY?: number;
};HyperlinkInfo
Hyperlink information for a run.
type HyperlinkInfo = {
href: string;
tooltip?: string;
noDefaultStyle?: boolean;
};ImageBlock
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;
};ImageFragment
An image fragment positioned on a page.
type ImageFragment = FragmentBase & {
kind: 'image';
height: number;
isAnchored?: boolean;
zIndex?: number;
};ImageMeasure
Measurement result for an image block.
type ImageMeasure = {
kind: 'image';
width: number;
height: number;
};ImageRun
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;
};ImageRunPosition
Position data for floating/anchored images.
type ImageRunPosition = {
horizontal?: {
relativeTo?: string;
posOffset?: number;
align?: string;
};
vertical?: {
relativeTo?: string;
posOffset?: number;
align?: string;
};
};KeepNextChain
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;
};LayoutOptions
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';
};LineBreakRun
A line break run.
type LineBreakRun = {
kind: 'lineBreak';
pmStart?: number;
pmEnd?: number;
};ListNumPr
List numbering properties for a paragraph.
type ListNumPr = {
numId?: number;
ilvl?: number;
};Measure
Union of all measurement types.
type Measure = ParagraphMeasure | ImageMeasure | TableMeasure | TextBoxMeasure | SectionBreakMeasure | PageBreakMeasure | ColumnBreakMeasure;MeasuredLine
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[];
};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;
};PageBreakBlock
Explicit page break block.
type PageBreakBlock = {
kind: 'pageBreak';
id: BlockId;
pmStart?: number;
pmEnd?: number;
};PageBreakMeasure
Measurement result for page break (no visual size).
type PageBreakMeasure = {
kind: 'pageBreak';
};PageMargins
Page margin configuration.
type PageMargins = {
top: number;
right: number;
bottom: number;
left: number;
header?: number;
footer?: number;
};PageState
Current state of a page being laid out.
type PageState = {
page: Page;
cursorY: number;
columnIndex: number;
topMargin: number;
contentBottom: number;
trailingSpacing: number;
};Paginator
type Paginator = ReturnType<typeof createPaginator>;PaginatorOptions
Options for creating a paginator.
type PaginatorOptions = {
pageSize: {
w: number;
h: number;
};
margins: PageMargins;
columns?: ColumnLayout;
footnoteReservedHeights?: Map<number, number>;
onNewPage?: (state: PageState) => void;
};ParagraphAttrs
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;
};ParagraphBlock
A paragraph block containing runs.
type ParagraphBlock = {
kind: 'paragraph';
id: BlockId;
runs: Run[];
attrs?: ParagraphAttrs;
pmStart?: number;
pmEnd?: number;
};ParagraphBorders
Paragraph borders.
type ParagraphBorders = {
top?: BorderStyle;
bottom?: BorderStyle;
left?: BorderStyle;
right?: BorderStyle;
between?: BorderStyle;
bar?: BorderStyle;
};ParagraphFragment
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;
};ParagraphIndent
Paragraph indentation configuration.
type ParagraphIndent = {
left?: number;
right?: number;
firstLine?: number;
hanging?: number;
};ParagraphMeasure
Measurement result for a paragraph block.
type ParagraphMeasure = {
kind: 'paragraph';
lines: MeasuredLine[];
totalHeight: number;
};ParagraphSpacing
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;RunFormatting
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;
};SectionBreakBlock
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;
};SectionBreakMeasure
Measurement result for section break (no visual size).
type SectionBreakMeasure = {
kind: 'sectionBreak';
};SectionLayoutConfig
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;
};SectionState
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;
};TabAlignment
Tab stop alignment types
type TabAlignment = 'start' | 'end' | 'center' | 'decimal' | 'bar' | 'clear';TableBlock
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;
};TableCell
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;
};TableCellMeasure
Measurement result for a table cell.
type TableCellMeasure = {
blocks: Measure[];
width: number;
height: number;
colSpan?: number;
rowSpan?: number;
};TableFragment
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;
};TableMeasure
Measurement result for a table block.
type TableMeasure = {
kind: 'table';
rows: TableRowMeasure[];
columnWidths: number[];
totalWidth: number;
totalHeight: number;
};TableRow
A table row containing cells.
type TableRow = {
id: BlockId;
cells: TableCell[];
height?: number;
heightRule?: 'auto' | 'atLeast' | 'exact';
isHeader?: boolean;
};TableRowMeasure
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;
};TabStop
Tab stop definition
type TabStop = {
val: TabAlignment;
pos: number;
leader?: 'none' | 'dot' | 'hyphen' | 'underscore' | 'heavy' | 'middleDot';
};TextBoxBlock
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;
};TextBoxFragment
A text box fragment positioned on a page.
type TextBoxFragment = FragmentBase & {
kind: 'textBox';
height: number;
isFloating?: boolean;
zIndex?: number;
};TextBoxMeasure
Measurement result for a text box block.
type TextBoxMeasure = {
kind: 'textBox';
width: number;
height: number;
innerMeasures: ParagraphMeasure[];
};TextRun
A text run within a paragraph.
type TextRun = RunFormatting & {
kind: 'text';
text: string;
hyperlink?: HyperlinkInfo;
pmStart?: number;
pmEnd?: number;
};WrapTextDirection
type WrapTextDirection = 'bothSides' | 'left' | 'right' | 'largest';Variables(2)
DEFAULT_TEXTBOX_MARGINS
Default internal margins for text boxes (OOXML defaults in pixels)
DEFAULT_TEXTBOX_MARGINS: {
top: number;
bottom: number;
left: number;
right: number;
}DEFAULT_TEXTBOX_WIDTH
Default text box width in pixels when no width is specified
DEFAULT_TEXTBOX_WIDTH = 200