Skip to Content
API Referencepagination

@stesura/pagination

Lays the document out as Word-style pages: page and section breaks, headers and footers, footnotes, columns and line numbers. Framework-free; the node views and panel are in @stesura/pagination-react. Setup and behaviour are in the Pagination guide.

Plugins

Most hosts mount paginationPlugins(). Mount the five individually, in the same order, only to pass options.

paginationPlugins

The five pagination plugins, in the order they must be mounted: paginationPlugin(), columnLayoutPlugin(), columnSpanPlugin(), headerFooterBindingPlugin(), headerFooterSyncPlugin(). Mount them yourself, in this order, to pass options such as paginationPlugin({ showPagination: false }).

Parameters:
This function does not accept any parameters.
Returns:
(Plugin<any> | Plugin<DecorationSet> | Plugin<PaginationPluginState> | Plugin<ColumnLayoutPluginState>)[]

paginationPlugin

Tracks what needs re-measuring, syncs showPagination to its store, and owns the pagination-active class on the editor DOM.

Parameters:
NameTypeDefault
options{ showPagination?: boolean; }
Returns:
Plugin<PaginationPluginState>

columnLayoutPlugin

Lays out column sections: holds the engine’s per-page column layout and renders the column spacers and line reservations from it. Part of paginationPlugins().

Parameters:
This function does not accept any parameters.
Returns:
Plugin<ColumnLayoutPluginState>

columnSpanPlugin

Marks the blocks spanning a column section’s columns (column-span: all, pagination.css): the ones the engine places above the first page’s columns. Derived from the doc rather than the engine’s output, so the shadow measures them at full width from the first pass; except a spanner the engine found taller than the page (getTallColumnSpanners), which is rebuilt out.

Parameters:
This function does not accept any parameters.
Returns:
Plugin<DecorationSet>

headerFooterBindingPlugin

Main → header/footer sub-editor sync. The inner → main direction is useHeaderFooterEditor (pagination-react).

Parameters:
This function does not accept any parameters.
Returns:
Plugin<any>

headerFooterSyncPlugin

Keeps the serialized header/footer content cache (rendered on every page by the Header/Footer components) in sync with the doc: rebuilt at mount and, debounced, after any step that touches a header/footer. Edit blocking lives in core’s headerFooterGuard plugin, not here.

Parameters:
This function does not accept any parameters.
Returns:
Plugin<any>

Commands

togglePagination

Always toggle pagination through this transaction: the plugin flips its state and mirrors it to the show-pagination store.

Parameters:
NameTypeDefault
stateEditorState
dispatch(tr: Transaction) => void
Returns:
boolean

Layout state

Read-only views of the per-editor stores. Inside React, prefer the hooks in @stesura/pagination-react.

getShowPagination

Whether pagination is on for an editor, outside React. Change it with togglePagination; in React, read it with useShowPagination.

Parameters:
NameTypeDefault
idOrStatestring | EditorState
Returns:
boolean

getSectionPaginationCache

A section’s current layout: measurements, pagination items and breaks. Read it imperatively; it isn’t meant to be subscribed to.

Parameters:
NameTypeDefault
idOrStatestring | EditorState
sectionIdstring
Returns:
PaginationCache

PaginationCache

One section’s layout, from getSectionPaginationCache: measurements, the engine’s pagination items and the breaks it published.

NameTypeDefault
measurementsMap<string, CachedMeasurement>
footnotesMap<string, CachedFootnote>

Keyed by footnoteRef. Built incrementally by buildFootnotesCache.

paginationItemsPaginationArrayItem[]
prevPaginationItemsPaginationArrayItem[]
tableCellBreaksMap<string, CellBreaks>

Keyed by cell node ID. Populated by processTable.

paragraphBreaksMap<string, ParagraphBreak[]>

Keyed by textblock node ID; see ParagraphBreak.

blockBreaksMap<string, BlockBreaks>

Keyed by placed block node ID (tables by the table): the in-block model’s breaks. The node views read them per block (blockBreaksFamily).

blockBreakSpacersBlockBreakSpacer[]
blockBreakCheckstring[]

What the in-block equivalence check found (dev builds and localStorage.paginationVerifyBlockBreaks): empty when it passed, absent when it didn’t run.

prevFirstNodesOnPagesstring[]

First-on-page node IDs (page > 0), diffed to toggle [data-invalidate-margin-top].

nodePageMapMap<string, number>

Placed block nodeId → 0-based start page. Also read by E2E tests.

nodeBottomMapMap<string, NodeBottom>

Expected bottom of every placed block on the page it ends on, from that page’s content start. The verify hook reconciles it against the DOM.

oversizedNodesMap<string, { actualHeight: number; maxHeight: number; }>

Keyed by node ID: atomic nodes taller than the usable page, clipped to maxHeight. actualHeight is diagnostic; only maxHeight is published.

rowHeightOverridesMap<string, number | null>

Keyed by table_row node ID, when the engine can’t honor a row’s height attr verbatim (Word fixed-row-height behaviour):

  • number: render at this height with overflow: hidden (forced height taller than the page, clamped).
  • null: release the forced height; the row splits at its natural height (content taller than a page).
  • absent: render the attr height.
exactRowsReleasedSet<string>

table_row node IDs whose exact height is taller than the page: the row view drops the clip and the row grows and splits like an at-least row (docs/executed/table-sizing-plan.md, D6).

rowPaddingTopsMap<string, number>

Keyed by table_row node ID: page-break clearance (px) for a row starting a new page. The row view grows a forced row by it and forwards it to cells.

floatPageFramesMap<string, FloatPageFrame>

Front/behind object id → where its anchor sits on its page (L2), published to core’s floatPageFrames.

columnLayoutColumnLayoutOutput

Column sections only; see ColumnLayoutOutput.

lineNumberCountnumber

Lines counted on the last compute, regardless of restarts. A following restart: "continuous" section seeds from the sum over preceding sections. Undefined without line numbering or after a cache clear.

requestNodeRemeasure

Re-measure blocks whose rendered height changed without a transaction (e.g. AI ghost-text widget decorations). The shadow measurer clones the live DOM, so decorations are measured; this only supplies the missing dirty signal. Merges rather than replaces: ids from a pending flush may be unconsumed.

Parameters:
NameTypeDefault
idOrStatestring | EditorState
idsIterable<string>
Returns:
void

Layout failures

report.stage is detected, recovered, tolerated or unrecovered; see Layout failures.

addPaginationFailureHandler

Handlers are additive and isolated: one throwing doesn’t stop the others. Reports are console.warn’ed regardless. Returns an unsubscribe function.

Parameters:
NameTypeDefault
hPaginationFailureHandler
Returns:
() => void

PaginationFailureHandler

Receives every failure report; register it with addPaginationFailureHandler.

Parameters:
NameTypeDefault
reportPaginationFailureReport
Returns:
void

PaginationFailureReport

Raised by @stesura/pagination; defined here so EditorDiagnostic can carry it.

NameTypeDefault
editorIdstring
sectionIdstring
stagePaginationFailureStage
kindPaginationFailureKind
pageCountnumber
expectedHeightnumber

Section height implied by the computed layout (layout px).

actualHeightnumber

Section element’s actual offsetHeight (layout px).

overflownumber

actualHeight − expectedHeight at detection time.

detailstring

Occurrence specifics as a plain string (e.g. the offending node id).

attemptnumber

The clean rebuild a detected report starts (1, 2…), or the last one tried. Layout checks only.

finalboolean

No recovery follows: the layout stays as it is. Set on a detected report when recovery is off (setPaginationRecovery("failFast")). Hosts test isTerminalPaginationFailure, which also covers unrecovered.

PaginationFailureKind

export type PaginationFailureKind = /** Section content flows past the last computed page. */ | "overflow" /** The engine produced no pages for a section that has measurements. */ | "engine-empty" /** * Column sections: a column doesn't hold the block the engine placed first * in it, so the browser broke somewhere the engine didn't. */ | "column-mismatch" /** A block reached the layout solver with no entry in the measurement cache. */ | "measurement-missing" /** * A block taller than the page can ever hold, usually because the * header/footer leaves less than a line of content height. It takes its own * page and is clipped, as in Word. */ | "oversized-content" /** * Two sections share an id (or one has none), so they would share every * per-section store and one would paginate blank. The layout pass is skipped * and the ids healed. */ | "duplicate-section-id";

overflow, engine-empty and column-mismatch run detect → recover → unrecovered; measurement-missing and oversized-content are always detected (informational: their layout is the best available). duplicate-section-id is detected when the pre-flight heals the ids, unrecovered when the healed doc fails again.

Waiting for layout

For tests and print/export flows; see Waiting for layout in tests.

isPaginationIdle

No scheduled work and no pass in flight. Deliberately not gated on hasPendingPaginationWork: a dirty id that no section owns would keep it false forever. A pass that never reports back keeps this false too, which at least fails loudly (a timeout) rather than silently early.

Parameters:
NameTypeDefault
idOrStatestring | EditorState
Returns:
boolean

paginationLayoutManifest

The editor’s layout as page starts, for a PDF export to compare against (ExportLayoutManifest): per page, in document order, the first block the engine placed starting on it. Null when pagination is off or not idle: mid-pass the layout is about to change, and comparing it would report a false mismatch.

Parameters:
NameTypeDefault
stateEditorState
Returns:
ExportLayoutManifest | null

getPaginationActivity

Pagination activity counters, read by e2e tests (via the demo’s window.__editor bridge) to wait for pagination to settle instead of sleeping: isPaginationIdle plus at least one engine run. Always on: recording is one counter increment per pass.

Parameters:
NameTypeDefault
idOrStatestring | EditorState
Returns:
PaginationActivity

hasPendingPaginationWork

Blind to work scheduled but not yet marked dirty (header/footer sync debounce, rAF-coalesced full recomputes, the fonts-gated first compute): that is what isPaginationIdle tracks.

Parameters:
NameTypeDefault
idOrStatestring | EditorState
Returns:
boolean

PaginationActivity

An editor’s engine counters, from getPaginationActivity. Tests use them to wait for layout.

NameTypeDefault
recomputesSkippednumber

Layout-effect passes that decided their section needed no recompute.

enginesSkippednumber

Passes that re-measured but skipped the engine (nothing it reads changed).

engineRunsnumber

PaginationEngine.compute() runs.

recoveriesnumber

Clean rebuilds the layout verifier started on a failed check. They run the engine with no new editor state; settle gates tell them apart from a scheduler they don’t track.

lastChangeAtnumber

performance.now() of the last counter change; 0 until the first record.

schedulednumber

Work scheduled but not yet run: the header/footer sync debounce, the rAF-coalesced full recompute, the devicePixelRatio debounce and the fonts-gated first compute. Each scheduler pairs schedulePaginationWork with completePaginationWork on every exit, cancellation included.

passPendingboolean

A version bump has woken the layout effects and no pass has reported back yet. Set in incrementPaginationVersion, cleared by the first recordPaginationActivity (every pass records on every exit).

Page geometry

For custom page chrome drawn over the flow.

PAGINATION_GAP

export const PAGINATION_GAP = 15;

Canvas gap (px) between pages.

OVERSIZED_CLEARANCE

export const OVERSIZED_CLEARANCE = 4;

Clearance (px) under a box that fills the usable page (oversized clamp, PDF embed). A BFC root needs a float-free window at least its own height; the rendered window is a hair under the usable height, and losing by ~1px pushes the box below every later float to the section’s end.

pageTopOffset

Layout px from the section top to page pageIdx, embeds included. pageIdx === pagesLayout.length gives the section bottom plus one gap. The print mount collapses embeds (collapsedEmbeds).

Parameters:
NameTypeDefault
pagesLayoutPageLayout[]
pageIdxnumber
pageHeightnumber
collapsedEmbedsboolean
Returns:
number

getVariantForPage

pageIdx is section-local; pageOffset counts document pages before the section. Odd/even is document parity (Word), but “different first page” is per section. Renderer and engine must pass the same offset, or the measured and rendered header heights disagree.

Parameters:
NameTypeDefault
pageIdxnumber
variantsSet<"even" | "first">
pageOffsetnumber
Returns:
HeaderFooterVariant

Line numbering

Line numbers are driven by the section’s lineNumbers attr and set with the core commands setLineNumbers, toggleLineNumbers and toggleSuppressLineNumbers. The engine and node views render them; there is nothing to call in this package. See Line numbering.

Diagnostics

Internal state exposed for debugging tools and E2E fixtures. Not needed to run pagination.

getPaginationStore

An editor’s per-section layout atoms. For diagnostics; prefer getSectionPaginationCache.

Parameters:
NameTypeDefault
idOrStatestring | EditorState
Returns:
SectionAtoms

getDirtyNodes

Returns the atom’s live set, not a copy. Callers that mutate it in place (the layout effect prunes it to avoid copying a per-keystroke set) must follow with setDirtyNodes: nanostores compares by reference.

Parameters:
NameTypeDefault
idOrStatestring | EditorState
Returns:
Set<string>

getDirtyFootnotes

The footnoteRefs whose bodies still need re-measuring. Edits inside a footnote body land here, not in getDirtyNodes.

Parameters:
NameTypeDefault
idOrStatestring | EditorState
Returns:
Set<string>

getDirtyHeaderFooterSectionsAtom

Section IDs whose header/footer changed, so the engine must re-run with fresh header/footer heights. headerFooterSync uses this instead of clearPaginationCache: body blocks are measured in continuous flow without page chrome, so a header edit never changes their heights.

Parameters:
NameTypeDefault
idOrStatestring | EditorState
Returns:
WritableAtom<Set<string>>

paragraphBreaksFamily

Page breaks cutting through a wrapped paragraph (floats an anchored image, renders display: flow-root). Every other block holds a permanently-null atom that never notifies.

NameTypeDefault
atomsMap<string, Map<string, PerNodeAtom<ParagraphBreak[]>>>

editorId → nodeId → atom. Exposed for teardown assertions in tests.

get(editorId: string, nodeId: string) => PerNodeAtom<ParagraphBreak[]>
publishSection(editorId: string, sectionId: string, next: Map<string, ParagraphBreak[]> | undefined, prev: Map<string, ParagraphBreak[]> | undefined) => void

Writes every node in next and nulls nodes in prev that are gone. Equal values are skipped, so a no-op compute notifies nobody.

clearSection(editorId: string, sectionId: string) => void

Null every entry owned by a section (the section left the document).

clearEditor(editorId: string) => void

Null every entry of an editor, keeping the atoms alive for subscribers.

removeEditor(editorId: string) => void

Drop an editor’s atoms entirely (the editor itself is gone).

Measuring a range

measureRangeFragments(block, range) returns one RangeFragment per visual line a DOM range occupies in a laid-out block (width, height, descent below the baseline, and how far its runs’ line boxes reached), from the engine’s own line grouping. A redacted PDF’s export page uses it to size its proportional boxes; measure in the browser that renders the export.

Last updated on