Computational, RTL-aware grid and layout mathematics. Zero runtime dependencies.
Built from an exhaustive survey of every open-source and academic approach to computational
grid/layout design (154 items across 12 search angles — see docs/RESEARCH.md). The survey's
central finding: no existing project anywhere reasons about grid geometry in bidi-aware
inline/block axes. Every masonry engine, bento generator, and grid-template tool outputs
physical CSS properties (left/right, literal column numbers) with zero direction-awareness
baked in. This library exists to fill that gap, alongside the classical/well-established pieces
(margin canons, modular type scales, baseline rhythm, font-metric-driven trims) that do have
solid prior art — ported and cited rather than reinvented.
| Module | What it does | Status |
|---|---|---|
canons.mjs |
Classical margin canons (Van de Graaf, Progressive, Villard de Honnecourt, Ateliers, Rosarivo) as pure f(W,H,params) → margins |
Well-established, ported from documented sources |
type-scale.mjs |
Modular type scale (size(n) = base × ratio^n), multi-strand normalization, named ratio table |
Well-established |
font-metrics.mjs |
Zero-dependency sfnt (TrueType/OpenType) table parsing — unitsPerEm, ascent/descent, capHeight/xHeight, fsType — plus Capsize-style baseline trims |
Well-established math, hand-rolled parser |
baseline-rhythm.mjs |
Line-height snapping, brute-force baseline-unit derivation, largest-remainder apportionment | Well-established |
grid-modules.mjs |
Row/column track computation composing the above | Well-established |
axis-resolver.mjs |
The novel piece. Resolves logical (inline-start/end) margins, column order, and nested grid-grammar trees into physical left/right coordinates given a reading direction | New — this is the confirmed gap the research found |
line-breaker.mjs |
Knuth-Plass paragraph line-breaking, extended with a kashida (Arabic elongation) glue primitive | New — the single most important invention the research identified; the kashida-priority distribution logic is this project's own, not validated against professional Arabic typesetting standards yet |
grid-grammar.mjs |
Seeded stochastic split-grammar for procedural grid variation (Gerstner's "design programme" idea, made concrete) — logical axes only, direction-agnostic by design | New — inspired by comparing WFC/cellular-automata/split-grammar approaches; a split-grammar can't fail to tile (no backtracking class), unlike WFC's documented "locally plausible, globally incoherent" failure mode |
index.mjs |
Composes stages 1–7 of the full pipeline into one buildGrid() call |
— |
87 tests, all passing, npm test. Every "well-established" module is tested against
hand-verified, independently-computed values, not just "doesn't crash." The novel modules
(axis-resolver, line-breaker, grid-grammar) are the ones to scrutinize hardest before
relying on them in production — read their source comments for the honest scope/caveat notes
before extending.
import { buildGrid } from 'gridmath';
import { readFileSync } from 'node:fs';
const grid = buildGrid({
page: { width: 595, height: 842, unit: 'pt' }, // A4 in points
direction: 'rtl', // or 'ltr'
scripts: {
primary: { file: readFileSync('Vazirmatn-Regular.ttf') },
secondary: { file: readFileSync('Roboto-Regular.ttf') }, // optional, for bilingual composition
},
columns: 6,
rows: 8,
marginCanon: 'vdg', // 'vdg' | 'progressive' | 'baseline' | 'vdh' | 'ateliers' | 'rosarivo' | 'grid' | literal {top,start,end,bottom}
gutterUnits: 1,
typeScale: { ratio: 1.333, baseSizePt: 12 },
typeSteps: { body: 0, h2: 2, caption: -1 },
});
// grid.margins, grid.columns, grid.rows are all physical (already RTL-resolved) coordinates.
// grid.typeStyles[name] = { fontSize, lineHeight, trims: { primary: {...}, secondary?: {...} } }For per-paragraph justified-text line-breaking (a separate concern from the page grid — see
docs/RESEARCH.md §4 for why these are split):
import { breakParagraph, makeBox, makeInterwordGlue, makeKashidaGlue, distributeStretch } from 'gridmath/line-breaker';
const items = [
makeBox(40, 'word1'), makeInterwordGlue(8, 4, 2),
makeBox(40, 'word2'), makeKashidaGlue(15), // an Arabic joining point that can elongate
makeBox(40, 'word3'),
];
const { lines } = breakParagraph(items, 120); // 120pt column widthFor procedural grid variation — a seeded split-grammar producing many valid layouts from one designer-authored rule-set, then resolved to physical coordinates for a given direction:
import { generateVariations } from 'gridmath/grid-grammar';
import { resolveGrammarTree, flattenPhysicalCells } from 'gridmath/axis-resolver';
const ruleSet = {
axiom: 'PAGE',
rules: {
PAGE: [
{ weight: 2, split: { axis: 'inline', tracks: [{ size: '1fr', symbol: 'COL' }, { size: '1fr', symbol: 'COL' }] } },
{ weight: 1, split: { axis: 'inline', tracks: [{ size: 60, symbol: 'SIDEBAR' }, { size: '2fr', symbol: 'COL' }] } },
],
COL: [{ weight: 1, terminal: 'module' }],
SIDEBAR: [{ weight: 1, terminal: 'module' }],
},
maxDepth: 4,
};
const frame = { iStart: 0, iSize: 595, bStart: 0, bSize: 842 };
const variants = generateVariations(ruleSet, frame, { count: 6, seed: 7, gutter: 8 });
// each variant.tree is in logical (inline/block) coordinates — resolve for a direction:
const physicalCells = flattenPhysicalCells(resolveGrammarTree(variants[0].tree, 'rtl'));Deliberately decoupled from any one product (it started as research for an RTL-native design tool, Tarrah, but the grid math itself is generally useful — modular scales and margin canons have nothing product-specific about them). Zero-dep, MIT-licensed, human-editable: matches the "Tool Independence" principle that a library like this should outlive any one consumer.
docs/RESEARCH.md— the full synthesis: taxonomy of 10 technique families with precise math and best reference implementations, an ADOPT/ADAPT/REJECT call per technique, the 7 explicit gaps this project had to invent (no prior art existed), and the 9-stage architecture this code implements.docs/REFERENCES.md— the full 154-item raw reference list (name/URL/language/license/ maturity/algorithm) across all 12 search angles.
line-breaker.mjs's kashida-priority distribution is this project's own invention, not a port of a validated reference implementation — none exists anywhere in the 154-item survey. It encodes the documented convention (kashida preferred over widened word-spacing) but the actual per-joining-point elongation limits are caller-supplied, not derived from real font/ script data (Gap #3 in the research: no validated baseline-trim or kashida-length data exists for Arabic fonts yet).villardDeHonnecourt()'s exact historical ratio has source disagreement — see the honest caveat insrc/canons.mjs's docstring. Implemented as one documented, self-consistent interpretation, not a verified historical fact.resolvePageMargins()requires an explicitbindingSiderather than deriving spine position from direction + page number — that derivation is a book-pagination convention, not a pure-geometry fact, and encoding it wrong silently would be worse than requiring the caller to state it (see the module's scope note).- No hyphenation-point flagged-pair demerit or fitness-class jump demerit in the line- breaker — both are real refinements from the original Knuth-Plass algorithm, left out of v1 to keep the first implementation reviewable; add once the core is validated further.
grid-grammar.mjsscores size/aspect-ratio validity only — it has no notion of whitespace-ratio or cross-cell alignment scoring (the kind of objective GRIDS' MILP solver or O'Donovan et al.'s energy function computes). Also,solveRepeatonly tiles fixed-size tracks; route baseline-integer-constrained repeats throughgrid-modules.mjsinstead — this module has no opinion about baseline snapping by design (see the module's header comment).