@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 }).
(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.
| Name | Type | Default |
|---|---|---|
options | { showPagination?: boolean; } |
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().
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.
headerFooterBindingPlugin
Main → header/footer sub-editor sync. The inner → main direction is
useHeaderFooterEditor (pagination-react).
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.
Commands
togglePagination
Always toggle pagination through this transaction: the plugin flips its state and mirrors it to the show-pagination store.
| Name | Type | Default |
|---|---|---|
state | EditorState | |
dispatch | (tr: Transaction) => void |
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.
| Name | Type | Default |
|---|---|---|
idOrState | string | EditorState |
getSectionPaginationCache
A section’s current layout: measurements, pagination items and breaks. Read it imperatively; it isn’t meant to be subscribed to.
| Name | Type | Default |
|---|---|---|
idOrState | string | EditorState | |
sectionId | string |
PaginationCache
One section’s layout, from getSectionPaginationCache: measurements, the
engine’s pagination items and the breaks it published.
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.
| Name | Type | Default |
|---|---|---|
idOrState | string | EditorState | |
ids | Iterable<string> |
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.
| Name | Type | Default |
|---|---|---|
h | PaginationFailureHandler |
PaginationFailureHandler
Receives every failure report; register it with addPaginationFailureHandler.
| Name | Type | Default |
|---|---|---|
report | PaginationFailureReport |
PaginationFailureReport
Raised by @stesura/pagination; defined here so EditorDiagnostic can carry it.
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.
| Name | Type | Default |
|---|---|---|
idOrState | string | EditorState |
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.
| Name | Type | Default |
|---|---|---|
state | EditorState |
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.
| Name | Type | Default |
|---|---|---|
idOrState | string | EditorState |
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.
| Name | Type | Default |
|---|---|---|
idOrState | string | EditorState |
PaginationActivity
An editor’s engine counters, from getPaginationActivity. Tests use them to wait for layout.
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).
| Name | Type | Default |
|---|---|---|
pagesLayout | PageLayout[] | |
pageIdx | number | |
pageHeight | number | |
collapsedEmbeds | boolean |
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.
| Name | Type | Default |
|---|---|---|
pageIdx | number | |
variants | Set<"even" | "first"> | |
pageOffset | number |
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.
| Name | Type | Default |
|---|---|---|
idOrState | string | EditorState |
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.
| Name | Type | Default |
|---|---|---|
idOrState | string | EditorState |
getDirtyFootnotes
The footnoteRefs whose bodies still need re-measuring. Edits inside a footnote
body land here, not in getDirtyNodes.
| Name | Type | Default |
|---|---|---|
idOrState | string | EditorState |
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.
| Name | Type | Default |
|---|---|---|
idOrState | string | EditorState |
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.
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.