@nextgensoftwares/folio-layout
DOM-free layout engine: document + theme + measurer → positioned pages. Guides: Layout pipeline, Incremental layout, Themes.
layoutDocument
function layoutDocument(doc: FolioDocument, options: LayoutOptions): DocumentLayout;
interface LayoutOptions {
measurer: TextMeasurer;
theme?: Theme; // default: defaultTheme
math?: MathMeasurer;
mediaSize?: MediaSizer;
renderers?: Record<string, BlockRenderer>; // win over built-in types
headerFooter?: HeaderFooterConfig;
variables?: Record<string, string>; // {{name}} in headers/footers
firstPageNumber?: number; // number printed on the first page
totalPages?: number; // overrides {{total}}
hints?: ReadonlyMap<number, BlockHint>; // by top-level block index
cache?: LayoutCache; // re-measure only changed blocks
previous?: DocumentLayout; // incremental pagination + page reuse
}
interface DocumentLayout { pages: PageLayout[]; warnings: LayoutWarning[]; continuous?: boolean } // continuous: laid out as reflow screens
interface PageLayout {
index: number; // 0-based
number: number; // printed number
width: number; height: number;
content: { x: number; y: number; width: number; height: number };
fragments: Fragment[]; // lazy, non-enumerable; paths from the doc root
chrome: Fragment[]; // header/footer; lazy, non-enumerable
blocks: { first: number; last: number } | null; // top-level blocks with content here
forcedCut: boolean; // content overflowed this page
}
interface BlockHint { pageBreakBefore?: boolean; keepWithNext?: boolean }
interface LayoutWarning { message: string; path?: number[] }| Export | Description |
|---|---|
class LayoutCache | block-measure cache keyed by node identity (get(node, key), set(node, key, flow)); one per (theme, measurer) |
toPageBoundaries(layout) | { pageNumber, startNodeIndex, endNodeIndex, estimatedHeight }[] per non-empty page (inclusive top-level block indices) |
Measurement contracts
interface FontSpec {
family: string; size: number; weight: number; style: 'normal' | 'italic';
features?: string; // OpenType features ('smcp', 'smcp,c2sc'): real small caps
letterSpacing?: number; // px after every grapheme cluster
}
interface TextMeasurer {
width(text: string, font: FontSpec): number;
metrics?(font: FontSpec): { ascent: number; descent: number }; // fractions of size; default 0.8 / 0.2
positions?(text: string, font: FontSpec): number[]; // px at every UTF-16 boundary
hasFeature?(font: FontSpec, tag: string): boolean; // primary face has 'smcp'/'c2sc' (else synthesized)
}
interface BoxSize { width: number; height: number; depth: number }
interface MathMeasurer { measure(latex: string, opts: { display: boolean; font: FontSpec }): BoxSize }
type MediaSizer = (attrs: Attrs) => { width: number; height: number } | undefined;Fragments
interface FragmentBase {
x: number; y: number; width: number; height: number; path: number[]; repeated?: boolean;
layer?: 'float' | 'behind' | 'front'; // out-of-flow objects (pages list behind first, front last)
pageY?: number; // behind/front anchored to the page margin
pageOrigin?: 'edge'; pageX?: number; // positioned from the page's top-left edge (pageX/pageY used as-is, also in headers/footers)
z?: number; // paint order inside the behind / in-front stack (Word's relativeHeight)
}
interface LineFragment extends FragmentBase {
kind: 'line'; baseline: number; dir: 'ltr' | 'rtl'; items: (TextItem | AtomItem)[];
from: number; to: number; code?: boolean; decorative?: boolean;
}
interface TextItem {
kind: 'text'; x: number; width: number; text: string; font: FontSpec; marks: Mark[]; color?: string;
from: number; to: number; rtl: boolean; wordSpacing?: number; shift?: number;
}
interface AtomItem { kind: 'atom'; type: string; attrs: Attrs; x: number; width: number; height: number; depth: number; from: number; to: number }
interface RectFragment extends FragmentBase {
kind: 'rect'; role: 'quote-border' | 'code-box' | 'code-header' | 'cell' | 'rule' | (string & {});
fill?: string; stroke?: string; strokeWidth?: number; radius?: number;
}
interface MediaFragment extends FragmentBase { kind: 'media'; attrs: Attrs }
interface MathFragment extends FragmentBase { kind: 'math'; latex: string; depth: number }
interface MarkerFragment extends FragmentBase { kind: 'marker'; text: string; font: FontSpec; baseline: number; dir: 'ltr' | 'rtl' }
interface CustomFragment extends FragmentBase { kind: 'custom'; type: string; attrs: Attrs }
type Fragment = LineFragment | RectFragment | MediaFragment | MathFragment | MarkerFragment | CustomFragment;TextItem.x is relative to the line's x; from/to are ProseMirror-compatible offsets inside the textblock (UTF-16, atoms count 1). shift is a baseline shift (positive = up). repeated marks copies on continuation pages (table headers).
Flows and custom renderers
interface Flow {
height: number; fragments: Fragment[]; breaks: Break[];
marginTop: number; marginBottom: number; keepWithNext?: boolean; pageBreakBefore?: boolean;
pageParity?: 'odd' | 'even'; // with pageBreakBefore: an odd/even page start (a blank page fills the gap)
keepMarginAtBreak?: boolean; // keep marginTop at the document start and after a forced break (explicit spaceBefore)
noBreakAfter?: boolean; // boundary after it is inside a float's span (penalty FLOAT_SPAN)
float?: Exclusion; // a square-wrapped float (height-0 flow); top level only
}
interface Break { end: number; resume: number; penalty: number; forced?: boolean; carry?: Carry; cap?: Carry } // cap: drawn at the bottom of the page ending here (a table border repeated at the cut)
interface Carry { height: number; fragments: Fragment[] }
type BlockRenderer = (node: FolioNode, ctx: MeasureCtx) => Flow;
interface MeasureCtx {
env: LayoutEnv; width: number; dir: 'ltr' | 'rtl'; listDepth: number; inList: boolean; inHeaderCell: boolean;
exclusions?: readonly Exclusion[]; // floats over this block (y from its top, x in this box)
floats?: boolean; // top level: square objects may float
area?: { x: number; width: number; pageWidth: number; marginLeft: number }; // the block's column in its page's text area (multi-column sections)
}
interface Exclusion {
side: 'left' | 'right'; top: number; bottom: number; left: number; right: number;
poly?: readonly number[]; // tight wrap: closed outline (x,y pairs); lines avoid only its extent within their own span
pad?: readonly [number, number, number, number]; // distance from text around `poly` (top, right, bottom, left)
both?: boolean; // text takes the larger free side per line (Word's "largest")
}
interface LayoutEnv {
theme: Theme; measurer: TextMeasurer; math?: MathMeasurer; mediaSize?: MediaSizer;
renderers?: Record<string, BlockRenderer>; pageContentHeight: number;
warn(message: string, path?: number[]): void;
}
interface StackOptions { dx?: number; indexPaths?: boolean; contain?: boolean }| Export | Signature | Description |
|---|---|---|
measureBlock | (node, ctx) => Flow | measure one block (host renderers first, then built-ins, then generic fallbacks) |
measureCode | (node, ctx) => Flow | the core codeBlock measurement (box, language header, mono lines); what code blocks look like without a code plugin (@nextgensoftwares/folio-plugin-code lays out its own richer box) |
measureChildren | (children, ctx, opts?: StackOptions) => Flow | measure and stack child blocks |
stack | (children: readonly Flow[], opts?: StackOptions) => Flow | vertical stacking with CSS margin collapsing and a break at each boundary |
lineBreaks | (bottoms, widows, orphans, keepTogether?) => Break[] | line breaks with widow/orphan control |
emptyFlow | () => Flow | a zero-height flow |
shiftFragment | (f, dx, dy, pathPrefix?) => F | moved copy, optionally prefixing its path |
shiftBreak | (b, dy) => Break | moved copy of a break |
FLOAT_SPAN | 2 | penalty of breaks inside a float's span: taken only when no penalty-0/1 break fits |
Placement (floats and text wrap)
type WrapMode = 'inline' | 'square' | 'topBottom' | 'behind' | 'front';
interface Placement {
wrap: WrapMode;
align: 'left' | 'center' | 'right'; // square: the side
dist: { top: number; bottom: number; left: number; right: number }; // px
offset: { x: number; y: number }; // behind/front
anchor: 'paragraph' | 'margin';
}| Export | Signature | Description |
|---|---|---|
placementOf | (attrs, marginY = 8) => Placement | sanitized placement from wrap, float (legacy side), alignment, distT/B/L/R, offsetX/Y, anchor |
placeBox | (box: Fragment, p: Placement, ctx) => Flow | lay out an object box: aligned block (inline/topBottom), float flow (square, top level), or height-0 overlay (behind/front). Host renderers use it so their objects wrap like media |
layoutEnv | (layout: DocumentLayout) => LayoutEnv | undefined | the measuring environment (theme, measurer, renderers) a layout was made with; wrapLayout plugins use it to lay out content outside the flow (page-level objects) exactly like the body |
isFloating | (p) => boolean | square, behind or front |
DEFAULT_DIST | { square: 12, overlay: 0 } | Word's default distance from text (px) |
polygonSpan | (poly, y0, y1) => [number, number] | null | horizontal extent of a closed polygon within a band (tight wrap, per line) |
polygonBounds | (poly) => { left, top, right, bottom } | bounding box of a polygon |
convexHull | (points) => number[] | convex hull of x,y pairs (the wrap outline of a group) |
shiftPolygon | (poly, dx, dy) => number[] | a polygon moved |
Placement, WrapMode, Exclusion | see above |
Page objects (text wraps around objects placed on pages)
interface PageObjectInfo {
index: number; width: number; height: number;
content: { x: number; y: number; width: number; height: number }; // after header/footer bands
blocks: { first: number; last: number } | null; blank: boolean;
}
interface PageObjects {
key: unknown; // identity changes when the objects change (page reuse)
zones(page: PageObjectInfo): readonly Exclusion[]; // page coordinates; text wraps around them
fragments?(page: PageObjectInfo): Fragment[]; // painted in the body's behind / in-front stacks by z
}
type PageObjectsProvider = (doc: FolioDocument, env: LayoutEnv) => PageObjects | null;LayoutOptions.pageObjects (or a renderer carrying renderer.pageObjects, how @nextgensoftwares/folio-plugin-layers provides its masters and page objects) adds objects anchored to pages. Pagination decides which text lands on a page and the text wraps around that page's objects, so layout iterates to a fixed point (at most MAX_ZONE_PASSES); the same mechanism lays out unequal columns at their own widths and places objects positioned from the margin in their column.
| Export | Signature | Description |
|---|---|---|
pageObjectsOf | (doc, options, env) => PageObjects | null | the layout's page objects (option, else renderer-carried providers, combined) |
MAX_ZONE_PASSES | 8 | bound of the page-object / column fixed point (past it the last pass is kept, with a warning) |
mergeStacks | (frags, objects) => Fragment[] | page fragments with page objects merged into the behind / in-front stacks by z (page objects first on ties) |
byZ | (stack) => Fragment[] | a stack in paint order (z, then document order) |
onPage | (f, x, y) => F | a fragment placed on a page whose content box starts at (x, y); edge-anchored objects keep their page coordinates |
hasParity | (a: Arranged) => boolean | whether any flow is an odd/even page start |
PageObjects, PageObjectInfo, PageObjectsProvider | see above |
Pagination internals
Exposed for tools and advanced hosts; layoutDocument uses them.
interface Arranged { flows: readonly Flow[]; offsets: number[]; total: number }
interface PageSlice { start: number; end: number; carry?: Carry; forcedCut?: boolean; blank?: boolean; cap?: { block: number; fragments: Fragment[] } }
interface PageSource { first: number; flows: readonly Flow[]; offsets: readonly number[]; height: number; owned: number; carry: Carry | undefined }| Export | Signature | Description |
|---|---|---|
arrange | (flows) => Arranged | lay top-level flows end to end with collapsed margins |
paginate | (a: Arranged, capacity: (pageIndex) => number, opts?) => PageSlice[] | choose page ends (first forced, else furthest penalty 0, else penalty 1, else overflow); opts.resume / opts.converge drive incremental layout |
blocksOnPage | (a, slices, p) => { first; last } | null | top-level blocks that can put fragments on page p |
pageSource | (a, slices, p, range) => PageSource | the page-local inputs for building fragments |
pageFragments | (src: PageSource) => Fragment[] | build a page's fragments (rects clipped at page edges) |
Reflow and progressive layout
See Responsive & mobile.
| Export | Signature | Description |
|---|---|---|
reflowTheme | (theme, o: ReflowThemeOptions) => Theme | screen-wide virtual page: no margins/bands/pagination rules, text scaled to a readable size |
ReflowThemeOptions, REFLOW_DEFAULTS | { width; padding?; sheetHeight?; minBodySize?; maxScale?; narrow? } | defaults: padding 0, sheet 1600, body ≥ 16px, ×1.5 max, narrow below 480 |
reflowScale | (theme, minBodySize?, maxScale?) => number | the text scale reflow applies |
LayoutOptions.continuous | boolean | sheets cut anywhere on one continuous column (no gaps, no forced breaks); renderers see MeasureCtx.reflow and the layout is marked continuous |
LayoutOptions.floats | boolean | false: square floats laid out as aligned blocks |
LayoutOptions.lazy | (index, node) => boolean | may this uncached block be estimated |
estimable | (node) => boolean | the default lazy policy (text blocks; never headings, media, floats) |
estimateFlow | (node, ctx, key) => Flow | the height guess (with a skeleton rect) |
isEstimate | (flow) => boolean | is this a placeholder flow |
estimatedBlocks | (layout) => readonly number[] | blocks a layout only estimated |
warmLayout | (doc, options, indices, stop?) => number | measure blocks into options.cache with the next layout's keys |
countUncached | (doc, options, limit?) => number | how many blocks the cache lacks |
Theme
interface Theme {
page: { width: number; height: number; margin: { top: number; right: number; bottom: number; left: number }; headerHeight: number; footerHeight: number };
direction: 'ltr' | 'rtl';
fonts: { body: string; mono: string };
body: { size: number; lineHeight: number; color: string; paragraphSpacing: number };
headings: Record<1 | 2 | 3 | 4 | 5 | 6, HeadingStyle>;
headingsKeepWithNext: boolean;
widows: number; orphans: number;
list: { indent: number; marginY: number; markerGap: number };
blockquote: { indent: number; borderWidth: number; borderColor: string; marginY: number };
code: { size: number; lineHeight: number; padding: number; marginY: number; radius: number; background: string; border: string; color: string; headerHeight: number; headerBackground: string };
table: { cellPadX: number; cellPadY: number; border: string; headerBackground: string; marginY: number };
rule: { marginY: number; color: string; thickness: number };
math: { marginY: number }; media: { marginY: number };
chrome: { size: number; color: string };
}
interface HeadingStyle { size: number; weight: number; lineHeight: number; marginTop: number; marginBottom: number; color?: string; family?: string; italic?: boolean; keepWithNext?: boolean }
type DeepPartial<T> = { [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K] };| Export | Signature | Description |
|---|---|---|
defaultTheme | Theme | A4 at 96 DPI, 48 px margins, Geist + Noto Sans Arabic 11 pt / 1.6 |
mergeTheme | (overrides?: DeepPartial<Theme>, base?: Theme) => Theme | deep merge |
pt, mm | (v: number) => number | points / millimetres → px |
parseLength | (value: unknown, em: number, percentOf?: number) => number | undefined | CSS-ish lengths: px, pt, em, rem, %, unitless = × em |
Theme.tabInterval | number? | default tab stop interval (px); missing = DEFAULT_TAB_INTERVAL (48 = 0.5in) |
Theme.leading | 'split' | 'below' | where a line's extra height goes: 'split' (default, CSS half-leading) or 'below' (Word/LibreOffice: line gap above the text, extra below; paragraphs' lineRule: 'atLeast' / px heights follow Word's at-least / exactly rules). DOCX imports set 'below' |
Tab stops
| Export | Signature | Description |
|---|---|---|
TabStop, TabAlign, TabLeaderKind | { pos; align?: 'left' | 'center' | 'right' | 'decimal'; leader?: 'none' | 'dot' | 'hyphen' | 'underscore' | 'middleDot' } | a paragraph's tabStops entry; pos px from the paragraph box's start edge |
tabStopsOf | (value: unknown) => TabStop[] | a tabStops attr validated and sorted (empty for none) |
DEFAULT_TAB_INTERVAL | 48 | Word's default stop interval (0.5in) |
TabItemInfo, TabLeader | { align; leader?: { text; x; width } } | TextItem.tab: the item is a tab (text \t, width its advance); the leader text is drawn at x from the item |
drawnItem | (item: TextItem) => TextItem | null | what a painter draws: the item, a tab's leader as plain text, or null for a blank tab |
MeasureCtx.margin | { left; right }? | room to the paper edges for negative indents (top level: the page margins) |
Headers and footers
interface HeaderFooterZone {
enabled?: boolean; leftContent?: string; centerContent?: string; rightContent?: string;
showPageNumber?: boolean; pageNumberPosition?: 'left' | 'center' | 'right'; differentFirstPage?: boolean;
}
interface HeaderFooterConfig { header?: HeaderFooterZone; footer?: HeaderFooterZone }
interface ChromeContext { pageIndex: number; pageNumber: number; totalPages: number; variables: Record<string, string> }| Export | Signature | Description |
|---|---|---|
zoneActive | (zone, pageIndex) => boolean | enabled and not suppressed on the first page |
substitute | (template, c: ChromeContext) => string | fill {{name}}, {{page}}, {{total}} |
zoneFragments | (zone, band, c, theme, measurer) => LineFragment[] | lay out one band's left/center/right slots (decorative lines) |
Document headers and footers
Rich headers/footers stored in the document (doc.attrs.headerFooter); used instead of HeaderFooterConfig zones when present and headerFooter is passed (fromDocument: false opts out; reflow drops it). Editing UI: @nextgensoftwares/folio-plugin-header-footer.
| Export | Signature | Description |
|---|---|---|
HF_ATTR, SECTION_ATTR, SECTION_BREAK_ATTR, CHAPTERS_ATTR, FIELD_TYPE | string | 'headerFooter', 'hfSection', 'sectionBreak', 'chapters', 'hfField' |
type HeaderFooterSettings | enabled, differentOddEven, title, variables, sections, plus the first section's SectionSettings | |
type SectionSettings | differentFirstPage?, pageNumbers?, header?, footer?, page?, columns?, headerDistance?, footerDistance? (px from the page edge; missing = linked to previous, the first section's default is the page margin) | |
type SectionPage | a section's own width?, height?, margin?: { top?, right?, bottom?, left? } (px; missing fields inherit) | |
type SectionColumns | { count, gap?, separator?, custom?: { width, gap? }[] } (Word's w:cols; { count: 1 } ends inherited columns) | |
type HFParts, type HFContent, type HFZone, type VariantKind | { default?, first?, even? }, FolioNode[], 'header' | 'footer', 'default' | 'first' | 'even' | |
type PageNumbering, type PageNumberFormat | { format?, start? }, '1' | 'i' | 'I' | 'a' | 'A' | |
type ChapterSettings | { enabled?, level? } (doc.attrs.chapters; missing = on, level 1) | |
type FieldKind, type FieldAttrs, type FieldValues | hfField attrs (field, format, name, level) and one page's values | |
type VariantRef | which stored part a page shows: zone, kind, owner (section key), section, linked | |
formatPageNumber | (n, format?) => string | 1, i, I, a (aa…), A |
formatDate | (ms, pattern?) => string | Word date pictures: yyyy, yy, MMMM, MMM, MM, M, dddd, ddd, dd, d |
isPageNumberFormat, PAGE_NUMBER_FORMATS | format guard and list | |
chapterSettings | (doc) => { enabled, level } | chapters with defaults |
isChapter | (node, chapters?) => boolean | a heading of the chapter level |
scanSections | (content, settings, chapters?) => SectionInfo[] | document start, chapters, section breaks (O(blocks)) |
resolveSections | (sections) => ResolvedSection[] | link-to-previous resolution of every part, first page flag and format |
type SectionInfo, type ResolvedSection, type ResolvedPart | kind, start, block, id, own; resolved parts with owner/linked | |
sectionAt, sectionKey, plainText | section of a block, its settings key, node text | |
KINDS, ZONES | variant kinds, zones | |
pageRanges | (arranged, slices) => { first, last }[] | blocks per page (without one merely ending at the page top) |
planPages | (input: PlanInput) => PagePlan[] | per page: section, kind, number, label, format, section pages |
type PagePlan, type PlanInput | ||
HeadingIndex | class | running headings per level (on(level, range), Word's STYLEREF search), firstHeading() |
fieldText | (attrs, values) => string | a field's text on a page |
prepare | (content, values) => Prepared | fields replaced by text, with their offsets (type Prepared, type FieldSpan) |
layoutBand | (content, values, env, edit?) => BandLayout | content laid out in a band; edit keeps PM offsets with fields as atoms |
bandHeight, bandEnv, chromeTheme | content height; measuring context; body styles at the theme's chrome size/colour | |
type BandEnv, type BandLayout, type Band | ||
pageChrome | (page) => PageChromeInfo | undefined | a laid-out page's plan, values, section, bands and zones (ZoneChrome: ref, content, box, layout(content, edit?)) |
type PageChromeInfo, type ZoneChrome | ||
documentHeaderFooter | (doc, options) => HeaderFooterSettings | null | the settings a layout uses |
PageLayout.label is the printed page number when document settings number pages (roman, restarts); LayoutOptions.now fixes the DATE field's clock.
Sections: page setup and columns
Sections (scanSections) may carry their own page size, margins and newspaper columns (SectionSettings.page / .columns, also on the document-level settings for the first section). Pages then differ in size (PageLayout.width/height/content) and list their column boxes in PageLayout.columns. Documents without any keep the uniform fast path (docGeometry returns null and nothing else runs). See the layout pipeline guide for how frames are paginated.
| Export | Signature | Description |
|---|---|---|
docGeometry | (doc, theme) => DocGeometry | null | every section's resolved page box and columns (null when all sections use the theme's page in one column); cached per document |
geometrySettings | (doc) => HeaderFooterSettings | null | the settings, when any section has page or columns |
resolvePage | (prev: PageBox, own?: SectionPage) => PageBox | a section's page from the previous one (missing fields inherit, clamped) |
columnBoxes | (columns?, width, rtl?) => ColumnBox[] | column x/width across a text width (custom widths scaled to fit; right to left in RTL) |
DEFAULT_COLUMN_GAP | 48 | Word's 0.5in |
type DocGeometry | sections, geoms, sectionOf(block), key | |
type SectionGeometry | page, columns, separator, measureWidth (the column width blocks are measured at), key | |
type PageBox, type ColumnBox | { width, height, margin }, { x, width } | |
type PageColumn | a column on a laid-out page (page coordinates): x, y, width, height, section |
Column breaks are the paragraph attr columnBreakBefore (a page break in one-column text); a continuous section start (sectionBreak without pageBreakBefore) continues below the previous section on the same page, balancing its columns. Flow.breakBefore ('column' / 'section') marks those forced breaks; the capacity callback of paginate gets a third soFar argument ({ slices, carry }) and converge a fourth (slices).
Tables
measureTable uses these; exporters call the same functions so Word and PDF match the page.
| Export | Signature | Description |
|---|---|---|
placeCells, GridCell | (rows) => { cells, cols, at } | grid placement with rowspan occupancy; at[r][c] = index into cells |
tableWidth | (attrs, cells, cols, available) => number | the table's width: width px, "NN%", "auto" (colwidth sum when every column has one), else the full area |
tableOffset | (attrs, width, available, dir) => number | x of the table in the area from align (left/center/right/start/end) and indent |
tableColumnWidths | (cells, cols, width) => number[] | colwidths where given, the rest share what's left (≥ 40px), scaled to width |
hasBorderInfo | (attrs, cells) => boolean | table borders, a tableStyle or any cell borders: collapsed borders instead of Folio's grid |
cellLooks, CellLook | (attrs, cells, at, rows, cols, theme, dir) => CellLook[] | per cell: resolved visual borders (each shared edge once), logical sides before collapsing, conditional fill, strong (bold text) |
tableEdges | (attrs, theme) => Record<TableEdge, BorderLine | null> | the table's own edges (attrs, else style, else the default grid's 1px lines) |
parseBorder | (spec, color) => BorderLine | null | undefined | a stored BorderSpec → a drawn line, null (none) or undefined (inherit) |
pickBorder | (a, b) => BorderLine | null | undefined | collapsed-border conflict: visible beats none, then wider, then heavier style (double > single > dashed > dotted) |
TABLE_STYLES, TableStylePreset, presetOf | Record<name, (theme) => TableStylePreset> | built-in styles: grid, plain, lines, banded, accent, boxed (borders, header/band/first-column fills, header and last-row borders) |
ACCENT_KINDS, AccentKind | readonly ['light', 'medium', 'dark'], its element type | the theme-accent table style families; each is a style name on its own (accent 1) or with -1…-6 (medium-3) |
themeAccents | (theme: Theme) => string[] | the six table accent colours: theme.table.accents, else the blockquote border colour first and a fixed palette after |
parseAccentStyle | (name: string) => { kind: AccentKind; accent: number } | null | medium-3 → { kind: 'medium', accent: 2 } (0-based accent); null for other names |
accentPreset | (name: string, theme: Theme) => TableStylePreset | undefined | the preset of a light / medium / dark accent style (optionally -1…-6), else undefined |
mixColor | (hex: string, amount: number) => string | mix a #rgb / #rrggbb colour toward white (amount > 0) or black (< 0), amount clamped to ±1; unparseable colours pass through |
TABLE_STYLE_GALLERY | readonly { id: string; label: string }[] | every built-in table style for a gallery, in display order: the six base styles then the 18 accent styles |
inlineImageBox | (attrs: Attrs | undefined, font: FontSpec) => BoxSize | an inline image's box: its px width/height (capped at 4000), depth 0 so the bottom sits on the baseline and a tall picture raises its line; a missing size falls back to the other, else the font size |
lookOf, DEFAULT_LOOK, TableLook | (attrs) => Required<TableLook> | which conditional parts apply (headerRow, firstColumn, lastRow, lastColumn, bandedRows, bandedColumns); default header row + banded rows |
paddingOf, paddingOfCell | (value, fallback), (tableAttrs, cellAttrs, fallback) | cell padding {top, bottom, start, end}: the cell's padding, the table's cellPadding, the fallback |
BorderSpec, TableEdge, CellEdge | types | stored border (style incl. none, width px, color); table edges (top, bottom, start, end, insideH, insideV); cell sides |
BorderLine, BorderStyle, BoxBorders | types | a resolved line (width, color, style: single/double/dashed/dotted) and a rect's per-side borders (RectFragment.borders, visual sides, drawn inside the box) |
borderStrips, BorderStrip | (box, borders) => BorderStrip[] | the filled rectangles that draw a box's borders (double = two strips a third wide, dashed/dotted = dashes); canvas and PDF paint these |
dashPattern | (style, width) => [dash, gap] | null | dash and gap lengths of a dashed/dotted line |
Lists and bidi helpers
| Export | Signature | Description |
|---|---|---|
resolveListStyle | (ordered, listStyleType, type, depth) => string | CSS list-style-type for a list, browser defaults for nesting |
markerText | (style, n) => string | marker for item n (bullets, decimal, alpha, roman, greek…) |
formatCounter | (n, format) => string | a counter in a list format without punctuation (CSS styles plus lower-letter/upper-letter, ordinal, arabic-indic; '' for bullets) |
fillTemplate | (text, chain: ListCounter[]) => string | fills %1…%9 from the enclosing counters (ListCounter = { value, format }, outermost first) |
listMarkerSpec | (listNode, depth, theme) => ListMarkerSpec | what a list draws: resolved style, counter format, its level, text(value, chain), marker font/color/image, indent, hanging, align (from listLevel, else listStyleType) |
markerX | (spec, width, gap, dir, left, right) => number | marker x relative to the item's text band (hanging indent, or gap before the text) |
embeddingLevels | (text, dir) => Uint8Array | null | UAX #9 embedding levels per UTF-16 code unit; null for an LTR paragraph with no RTL characters (fast path) |
SMALL_CAPS_SCALE, capsOf(marks), CapsMode | synthesized small capitals are drawn at this fraction of the size (0.8) when the font has no smcp; capsOf reads a run's uppercase / small-caps / all-small-caps mode from its textStyle (see caps and letter spacing) | |
visualOrder | (levels: readonly number[]) => number[] | run indices in left-to-right display order (UAX #9 rule L2), given each run's level |