Documentation

The interface, in full.

This page is generated from types/index.d.ts, the declaration file the package ships. Every function, option and figure field below is the one the compiler checks, so this reference and the code agree by construction. Doc comments carry the page citations. For worked examples, read the books; for the rules behind the options, the canon; for the whole account, the manual.

type & measurement · paper, margins, grid · flow · the document layer · rendering · helpers · hyphenation · randomness · the programme layer (Gerstner)

Type & measurement

export interface Style {
  size: number;
  leading: number;
  /** size + leading : the vertical unit everything registers on */
  lineHeight: number;
  font: string;
  weight: 'normal' | 'bold' | 'semibold' | number;
  italic?: boolean;
  /** optional per-role ink (used when the style is a typeSystem role) */
  color?: string;
  /** optional per-role setting; 'center' is the centred axis (p.79) */
  align?: 'left' | 'center' | 'justify';
}

/** A text style. Leading is the space ADDED to the size (9/3 → 12pt line). */
export function style(size: number, leading: number, opts?: {
  font?: string; weight?: Style['weight']; italic?: boolean; color?: string;
}): Style;

/**
 * Enforces the cross-size register (pp.59, 66): every role's line height must
 * divide the module or be a whole multiple of it. Throws with the nearest
 * legal settings named when a role breaks it.
 */
export function typeSystem<R extends Record<string, Style>>(spec: { module: number; roles: R }): {
  module: number; roles: R;
};

/** Width of a string in a style, from the registered metrics or the seam. */
export function measure(text: string, st: Style): number;
export function wordsPerLine(width: number, st: Style): number;

/**
 * The largest face (min..max) whose measure holds `words` per line : the
 * column decides the face (p.57); 8–12pt is the book floor (p.18).
 */
export function sizeForMeasure(width: number, opts?: {
  words?: number; font?: string; min?: number; max?: number;
  leadingRatio?: number; weight?: Style['weight'];
}): { size: number; leading: number; lineHeight: number; wordsPerLine: number; meets: boolean };

export interface BreakOptions {
  /** Knuth–Plass when true (default); greedy when false */
  optimal?: boolean;
  mode?: 'left' | 'justify';
}
/** Break a paragraph at a fixed measure. */
export function breakLines(text: string, width: number, st: Style, opts?: BreakOptions): string[];
/** The same breaker where the measure may change per line (drop caps, intrusions). */
export function breakVariable(text: string, widthFor: (line: number) => number, st: Style, opts?: BreakOptions): string[];
export function breakIndented(text: string, width: number, st: Style, indentLines: number, indentWidth: number, opts?: BreakOptions): string[];

export interface FontMetrics {
  family: string; capHeight: number; xHeight: number; descent: number;
  fallbackWidth: number; widths: Record<string, number>;
  dataUri?: string; format?: string;
}
export function registerFont(name: string, metrics: FontMetrics): void;
export function fontMetrics(name: string): FontMetrics;
/** @font-face CSS for every registered font that carries a dataUri. */
export function fontFaceCSS(): string;

/** Replace the measurement backend (the browser adapter uses this). */
export function setMeasurer(fn: ((text: string, st: Style) => number) | null): void;
/**
 * Optional fast path for GREEDY breaking (never Knuth–Plass). Must agree with
 * the built-in breaker line for line; return null to decline.
 */
export function setGreedyBreaker(fn: ((text: string, width: number, st: Style) => string[] | null) | null): void;

/** A dropped initial (p.36): size derived from N body lines of cap height. */
export function dropCap(letter: string, opts: {
  bodyStyle: Style; lines?: number; font?: string; weight?: Style['weight'];
  italic?: boolean; color?: string; snap?: boolean;
}): { letter: string; lines: number; width: number; style: Style; color?: string; offsetY?: number };

Paper, margins, grid

export interface Rect { x: number; y: number; w: number; h: number }
export interface Margins { inner: number; head: number; outer: number; tail: number }

export function marginsFromRatio(ratio: 'classical' | 'golden' | Margins, scale: number): Margins;
/**
 * Scale a margin canon so the type area is exactly `fraction` of the page
 * area (p.51 diagrams 1–2: the 1:2 relation).
 */
export function marginsForAreaFraction(page: { w: number; h: number }, ratio: 'classical' | 'golden' | Margins, fraction?: number): Margins;
export function typeArea(page: { w: number; h: number }, margins: Margins, side?: 'left' | 'right'): Rect;

export interface GridGeometry {
  page: { w: number; h: number };
  margins: Margins;
  typeArea: Rect;
  columns: Rect[];
  rows: Rect[];
  fields: Rect[];
  line: number;
  columnGutter: number;
  rowGutter: number;
  linesPerField: number;
  side: 'left' | 'right';
  /** rect spanning fields [col..col+cols) × [row..row+rows) */
  field(col: number, row: number, cols?: number, rows?: number): Rect;
  baselines(): number[];
}

/**
 * The modular grid (pp.57–70): field heights are whole lines, gutters are
 * whole empty lines. There is no way to express an off-grid position.
 */
export function grid(spec: {
  page: { w: number; h: number } | 'A4' | 'A5' | 'A3';
  margins: Margins;
  columns: number; columnGutter: number;
  line: number; rows: number; rowGutterLines?: number;
  side?: 'left' | 'right';
}): GridGeometry;

/** The book's own dimensioned A4 reference grids (pp.73, 77, 88). */
export const presets: {
  mb8(side?: 'left' | 'right'): GridGeometry;
  mb20(side?: 'left' | 'right'): GridGeometry;
  mb32(side?: 'left' | 'right'): GridGeometry;
};

Flow

export interface Paragraph {
  text: string;
  /** a heading keeps `keep` lines of what follows with it */
  keepWithNext?: boolean;
  /** whole empty lines before (number), or false; default 1 ('line' device) */
  spaceBefore?: number | boolean;
  /** extra whole empty lines after : the rest zone (p.69) */
  spaceAfter?: number;
  /** set in styles[role]: its own face, whole slots, never split (pp.46-47, 87) */
  role?: string;
  /** hairline under the titled line (placement example 8, p.47) */
  rule?: boolean;
  /** a dropped initial, from dropCap() */
  initial?: ReturnType<typeof dropCap>;
  /** open a new page before this paragraph (a chapter, a concert) */
  pageBreak?: boolean;
}

export interface FlowBox {
  rect: Rect; lines: string[]; indents: number[]; flush: boolean[];
  roles: (string | undefined)[]; slots: number[]; ruleAfter: boolean[];
}
export function flowText(text: string, rects: Rect[], st: Style): {
  boxes: { rect: Rect; lines: string[] }[]; overflow: string | null; overflowWords: number; spareLines: number;
};
export function flowParagraphs(paragraphs: (Paragraph | string)[], rects: Rect[], st: Style, opts?: {
  keep?: number; widowControl?: boolean; minLines?: number;
  mode?: 'left' | 'justify'; optimal?: boolean;
  styles?: Record<string, Style> | null;
  /** paragraph device (p.34): 'line' (default) or 'indent' */
  device?: 'line' | 'indent';
  indentWidth?: number | null;
}): {
  boxes: FlowBox[];
  initials: { box: number; line: number; slot: number; letter: string; initial: ReturnType<typeof dropCap> }[];
  overflow: string | null; overflowWords: number;
  overflowParagraphs: Paragraph[]; spareLines: number;
};

The document layer

export interface Figure {
  id?: string;
  /** substring of the story this figure belongs beside ("(Plate 3)") */
  anchor?: string;
  /** size vocabulary in grid columns × field rows : few graded sizes (p.11) */
  sizes?: { cols: number; rows: number }[];
  cols?: number; rows?: number;
  href?: string;
  /**
   * preferred w/h. The beam scores placements against it, and a placed picture
   * is NEVER cropped: when the chosen fields miss the aspect, the picture keeps
   * its proportions inside them (full width hung from the top, or full height
   * on the reading edge) and the caption follows the picture's edge.
   */
  aspect?: number;
  aspectWeight?: number;
  caption?: string;
  captionColor?: string;
  /** one ink (feColorMatrix ramp in SVG) */
  duotone?: boolean | { shadow: string; highlight: string };
  /** 'cover' opts back in to cropping, for photographs that can afford it */
  fit?: 'meet' | 'cover';
  /** run off the paper on these sides */
  bleed?: boolean | ('left' | 'right' | 'top' | 'bottom')[];
  /** take the whole leaf, caption in the margin */
  fullPage?: boolean;
  captionBg?: string;
  /** ── the cut-out apparatus (p.98) ── */
  /** colour area beneath the cut-out, sized to its fields */
  tint?: string;
  /** line border with the field's dimensions */
  border?: boolean; borderWidth?: number; borderColor?: string;
  /** rules at the field's top and bottom limits */
  bracket?: boolean; bracketWidth?: number; bracketColor?: string;
  /** colour printed over the photograph : dominant */
  overprint?: string;
  /** a pull quote occupying fields, sized to fill them */
  quote?: string; quoteStyle?: Style; quoteColor?: string;
  /** a statistic occupying fields; bars are whole text lines (pp.14, 58-59) */
  chart?: {
    data: { label?: string; value: number }[];
    max?: number; barColor?: string; color?: string;
    format?: (v: number) => string;
  };
  seed?: string;
}

export interface PageItem {
  kind: 'text' | 'image' | 'block' | 'rule' | 'frame' | 'chart';
  rect: Rect;
  [key: string]: unknown;
}
export interface ComposedPage {
  number: number; grid: GridGeometry; items: PageItem[];
  side: 'left' | 'right'; plate?: boolean;
}

/**
 * The point of the library: story + figures + grid + seed → pages.
 * Deterministic: the same seed returns the same book, forever.
 */
export function composeDocument(spec: {
  grid: GridGeometry | ((ctx: { pageNo: number; side: 'left' | 'right' }) => GridGeometry);
  styles: Record<string, Style> & { body: Style };
  story: string | (Paragraph | string)[];
  title?: string | null; titleRows?: number;
  figures?: Figure[];
  seed?: string;
  folio?: { start?: number; placement?: 'second-column' | 'left' | 'outer' | 'center' } | false;
  maxPages?: number;
  ink?: string | null;
  /** grid columns per text column (the book's own page spans 2, p.87) */
  textSpan?: number;
  textSnap?: boolean | null;
  /** the taste dial, in cost units : see each key's page in the source */
  harmony?: { newSize?: number; sameBand?: number; sharedEdge?: number; stacked?: number; adrift?: number };
  /** pull toward the anchor's column: a preference, not a law */
  anchorPull?: number;
  dispositions?: { bands?: number[]; bandPull?: number; heroChance?: number; heroPull?: number };
  bodyAlign?: 'left' | 'justify' | 'center';
  minSegmentLines?: number;
  marginCaptions?: boolean;
  /** paragraph device (p.34): 'line' | 'indent' */
  paragraphDevice?: 'line' | 'indent';
  indentWidth?: number | null;
  /** elastic limits for justified spaces, in multiples of the natural space */
  justify?: { shrink?: number; stretch?: number };
  captionAir?: number;
  titleRule?: { offset?: number; width?: number };
  quoteFit?: { ceiling?: number; leading?: number };
  /** softmax temperature of the page search; 0 = deterministic top-k */
  explore?: number;
}): {
  pages: ComposedPage[];
  report: {
    fits: boolean; leftoverWords: number;
    figures: { id: string; placed: boolean; page?: number; anchorPage?: number; drift?: number }[];
  };
};

Rendering

/** One page → a self-contained SVG string. */
export function renderPage(spec: {
  grid?: GridGeometry; page?: { w: number; h: number };
  items?: PageItem[];
  showGrid?: boolean; showBaselines?: boolean;
  background?: string; scale?: number;
}): string;

/**
 * One page → live DOM: selectable text on the baseline grid, baselines
 * MEASURED from the browser (never computed from font metrics).
 */
export function renderDOM(page: { grid?: GridGeometry; page?: { w: number; h: number }; items: PageItem[] },
  container: HTMLElement,
  opts?: { scale?: number; background?: string; showGrid?: boolean; showBaselines?: boolean }): HTMLElement;
export function clearBaselineCache(): void;

/** Measure through the canvas of the very fonts about to be drawn. */
export function useBrowserFonts(opts?: { families?: Record<string, string>; scale?: number }): Promise<void>;
export function pageFromElement(el: HTMLElement): { w: number; h: number };

Helpers

export function folio(g: GridGeometry, pageNumber: number, opts?: {
  placement?: 'second-column' | 'left' | 'outer' | 'center';
  lines?: number; style: Style; side?: 'left' | 'right';
}): PageItem;
export function captionBelow(imageRect: Rect, text: string, st: Style, opts?: { lines?: number; gapLines?: number }): PageItem;

Hyphenation

export function setHyphenator(fn: ((word: string) => string[]) | null): void;
export function hyphenatorFrom(hyphenateSync: (word: string) => string): (word: string) => string[];

Randomness

/** xmur3 + sfc32: the seeded PRNG everything deterministic runs on. */
export class Rand {
  constructor(seed: string);
  dec(): number;
  int(maxExclusive: number): number;
  choice<T>(arr: T[]): T;
}
export function xmur3(seed: string): () => number;
export function sfc32(a: number, b: number, c: number, d: number): () => number;

The programme layer (Gerstner)

// Typed loosely on purpose: the solver's DSL is documented in the README and
// research/foundations.md; these types keep call sites honest without
// freezing a young API.

export function programme(spec: object): {
  solutions(opts?: { limit?: number }): object[];
  count(): number;
  layout(seed: string): object;
  reroll(seed: string, opts?: { lock?: string[] }): object;
  sample(seed: string, n: number): object[];
};
export function mobileGrid(units: number, opts?: object): object;
export function mobileProgramme(spec: object): object;
export function gerstnerNumber(divisions: number[], gutter: number): { units: number; widths: Record<number, number[]> };
export function compoundProgramme(spec: object): object;
export function morphologicalBox(axes: Record<string, unknown[]>): {
  count: number; at(index: number): Record<string, unknown>; sample(seed: string): Record<string, unknown>;
};

export function mfield(...args: unknown[]): object;
export function malign(...args: unknown[]): { left(): object; right(): object; top(): object };
export function mbelow(...args: unknown[]): object;
export function mwhitespace(...args: unknown[]): object;
export function story(...args: unknown[]): object;
export function initialIndents(...args: unknown[]): number[];