/** Protrusion codes in thousandths of the glyph's own advance (pdfTeX). * `l`/`r` are LOGICAL line-start/line-end sides: the renderer maps them to * physical margins via inline-start/end, so in an RTL paragraph an `l` * code hangs at the right edge and an `r` code at the left. */ interface ProtrusionCodes { l?: number; r?: number; } type ProtrusionTable = Readonly>; interface ExpansionOptions { /** Max glyph stretch as a fraction (0.02 → font-stretch up to 102%). */ max: number; /** Max glyph shrink as a fraction (0.02 → down to 98%). */ shrink: number; /** Quantization step as a fraction (0.005 → 0.5% increments). */ step: number; } interface TrackingOptions { /** Max letterfit opening as a fraction of the line's set width. */ max: number; /** Max letterfit closing as a fraction of the line's set width. */ shrink: number; } /** * Full-hanging policy layered over the selected protrusion model. Each name * says which line edges hang fully, in increasing order. * * `"first-line"` and `"all-lines"` are the original spellings of * `"first-line-and-line-ends"` and `"all-line-edges"`, and remain supported. */ type HangingPunctuationMode = false | "none" | "line-end-only" | "first-line-and-line-ends" | "all-line-edges" | "first-line" | "all-lines"; /** * The public option surface, and what it resolves to. * * Two audiences meet here. Outward, this is the documented API — every knob a * caller can turn, with the typographic reasoning behind its default, because * a justification setting whose consequence is not explained is a setting * nobody can choose between. Inward, `resolveOptions` turns that into the * flat, fully-defaulted shape the layout actually runs on, once per * controller. * * Pure by construction: nothing here touches the DOM, so what a controller * will do with a given option object can be worked out without a document. * * The split between the public defaults and the CORE defaults in * ./core/types.js is deliberate and shows up repeatedly below. The core is * classic TeX; the public API is Bringhurst. `lastLineMinWidth` and * `tracking` are both core-off, public-on for that reason. */ interface JustifyOptions { /** Word splitter, e.g. `hyphenateEnUS` from "justif/hyphenate/en-us". * Never called for RTL paragraphs (Arabic joining makes fragment * measurement invalid; Hebrew convention breaks without hyphens). */ hyphenate?: (word: string) => readonly string[]; tolerance?: number; pretolerance?: number; hyphenPenalty?: number; exHyphenPenalty?: number; linePenalty?: number; adjDemerits?: number; doubleHyphenDemerits?: number; finalHyphenDemerits?: number; emergencyStretch?: number | "auto"; /** * Keep paragraph endings and lines terminated by `
` at least this * fraction of the measure wide (0.33 ≈ Bringhurst's "at least a third"). * Two mechanisms compose. * The breaker prefers arrangements whose endings reach the threshold * naturally — cost pressure that escalates into hyphenation when * needed, and prices endings by exactly what will render, so it steers * into arrangements the render floor can finish. An ending that still * falls short is then RENDERED with its word spaces widened to the * threshold — within a willingness that scales with the setting: * rectangles (`1`) work the spaces up to TeX's underfull-reporting * standard (≈ 2× natural at the default `spacing`), a gentle `0.33` * floor barely opens them. An ending that would need more keeps fully * natural spacing instead: all or nothing, never stretched AND still * short. The same principle holds for the whole paragraph: a threshold * ending is never bought with a worse-than-tolerance body line, and the * option never renders a shorter ending than it would produce switched * off (the breaker compares and keeps the better solution). The top of * the range can still be non-monotone per paragraph — one may satisfy * `0.5` yet revert to its natural ending at `1`. At `1` every paragraph * that can afford it sets as a perfect rectangle, including a one-line * paragraph. Values below `1` apply only to multi-line paragraphs: a * naturally one-line element stays in native layout because it has no * short ending to repair. Defaults to `0.33` (Bringhurst); pass `0` to * disable. */ lastLineMinWidth?: number; /** * Character protrusion model. `true` (the default) measures each font's * glyph-specific optical alignment by rasterizing its glyphs. `false` * disables character protrusion — and only that: `hangingPunctuation` is an * independent setting, so `false` with hanging left on sets ordinary glyphs * exactly flush while the eligible marks still hang. * * An object selects the fixed table-backed model and supplies * per-character overrides, in thousandths of the character's own advance. * Overrides are merged over the generic Latin table and any matching * hand-tuned per-font table. Values cap at 1000 — a whole advance is as far * out as a glyph goes — while negatives are honoured and pull it inward. A * value here sets how far a character protrudes and never makes it hang; * membership is `hangingPunctuation`'s to decide. * * Built-in tables (the generic Latin list plus microtype's per-font configs) * remain as the FALLBACK, used per font wherever the measurement cannot run * — a canvas that will not rasterize or read back, or one the browser will * not shape a run's font-variant in — and for the characters the raster pass * has no candidate for, such as the Arabic and Hebrew stops. They are not * separately selected when `true`; passing an object (including `{}`) * bypasses measurement and uses them directly. */ protrusion?: boolean | ProtrusionTable; /** * Full-hanging policy, independent of `protrusion`. `"line-end-only"` (the * default) fully hangs eligible punctuation at line ends while line starts * retain optical alignment. `"first-line-and-line-ends"` adds the CSS `first` * model on top of that: the paragraph's opening quote hangs fully and later * line starts set those marks flush. `"all-line-edges"` fully hangs at every * line edge; `"none"` applies only the selected protrusion model. * * Hanging is composed as a protrusion overlay, but the two settings switch * separately: with `protrusion: false` the overlay composes over an empty * base, and with `"none"` the protrusion model applies alone. * * An object additionally chooses WHICH characters are marginal — see * `HangingPunctuationOptions`. * * Compatibility: `true` selects the default; `false` selects `"none"`; * `"first-line"` aliases `"first-line-and-line-ends"`; and `"all-lines"` * aliases `"all-line-edges"`. */ hangingPunctuation?: true | HangingPunctuationMode | HangingPunctuationOptions; /** Glyph expansion limits via the wdth axis; false disables. Fields left out * take their default, like `spacing` and `tracking`. */ expansion?: Partial | false; /** * Inter-word glue flexibility as fractions of the space width. `pull` * (0–1, default 0.7) is the downward pressure on secondary-font spaces * wider than the paragraph base font's: 0 keeps each font's natural * space, 1 converges them fully to the base (risks dissolving word * boundaries in loose-fitting fonts like monospace). `boundaryShrink` * (0–1, default 0) multiplies the shrink of spaces at font-FAMILY * boundaries: chips and pills (inline code, ) live there, their * insets occupy part of the adjacent gap, and native CSS justification * never shrinks a space — so by default those gaps stretch but hold * their natural width. 1 restores TeX semantics. */ spacing?: Partial<{ stretch: number; shrink: number; pull: number; boundaryShrink: number; }>; /** * Letterfit tracking: lets inter-character space open or close each * line's set width, participating in break decisions like expansion. * `true` (the DEFAULT) allows ±3% — Bringhurst's tolerance for * letterspacing variation in justified text (The Elements of Typographic * Style); `false` disables. Word space and glyph expansion remain the * primary flexes (tracking saturates at its budget), and the last line * always keeps its natural letterfit. Beyond TeX: microtype's * letterspacing is static styling, never a per-line justification * variable. Always off for RTL paragraphs (letterspacing cursive Arabic * is typographically wrong and renders inconsistently across engines). */ tracking?: boolean | Partial; /** * Last-line color matching (eTeX's \lastlinefit): the paragraph ending's * spaces are set at this fraction (0–1) of the paragraph's average * looseness, instead of always natural width — a connoisseur's * refinement mainstream DTP tools only approximate with a static * "desired spacing" value. Lines terminated by `
` contribute their * justified body lines to the average but do not receive last-line fitting * themselves. 0 (default) = off. */ lastLineFit?: number; /** * Clean library-introduced characters out of copied text (default true). * Wrap determinism renders mid-line run-boundary spaces as NBSP and rare * dash junctions carry a U+2060 word joiner — plumbing that shouldn't * survive into the clipboard. Word joiners are always removed; NBSPs are * normalized back to spaces only when the selection's paragraphs * contained no author NBSPs (author intent like `Fig. 7` wins over * cleanup). `false` restores raw copies. */ cleanClipboard?: boolean; /** * Re-layout managed paragraphs when their content width changes * (default true). With `false`, width changes after enhancement are * not tracked — including ones caused by OTHER elements' late-loading * fonts resizing a shared shrink-to-fit container; call `refresh()` * after such changes. */ observeResize?: boolean; /** * Called after a paragraph's rendered layout changes — initial * enhancement, resize re-layout, promotion from a native one-line state, * restoration when it fits on one line again, refresh, and re-measures * triggered by fonts finishing to load. Use it to keep overlays or * annotations positioned over the text in sync. NOT fired for the deferred * wrap-guarantee corrections: those reconcile sub-pixel painted-edge drift * with small spacing changes but do not alter chosen breaks or paragraph * structure. */ onRelayout?: (paragraph: HTMLElement) => void; /** * Called once per paragraph that justif declines to manage, with a short * human-readable reason ("inline has a horizontal margin", * "font-variation-settings on a run", "threw while rendering: …"). * Declines are otherwise silent by design — the paragraph keeps its * native CSS rendering — which makes "skipped" indistinguishable from * "broken" while integrating; this is the diagnosis channel. */ onSkip?: (paragraph: HTMLElement, reason: string) => void; } /** * Hanging punctuation as its two independent parts: which characters are * marginal, and where that classification applies. * * Membership carries no depth. A character is either outside the measure or * it is not — how far a mark sits from the margin when it is NOT hung is the * `protrusion` model's business, and writing a depth here would merge two * features that answer different questions. */ interface HangingPunctuationOptions { /** Which line edges the classification applies to, and on which lines. * Defaults to the same policy the string form selects. */ edges?: HangingPunctuationMode; /** * Which characters are marginal. Each side REPLACES the built-in set for * that side; a side left out keeps its default, so naming one edge never * silently empties the other. Compose from the exported `hangingCharacters` * to extend rather than replace: * * ```js * characters: { start: hangingCharacters.start + "([{" } // + CSS brackets * characters: { end: "" } // starts only * ``` * * Replacing `end` wholesale drops the CJK stops that make burasage work, so * mixed Japanese and Latin text wants `hangingCharacters.end + "…"` rather * than a bare list. Nothing is validated: a letter here will hang. */ characters?: { start?: string; end?: string; }; } /** * The layout settings a live reconfiguration can replace. Everything else a * controller was built with — the hyphenator, callbacks, breaker penalties, * clipboard cleanup, resize observation — is fixed for its lifetime. */ type LayoutOptions = Pick; /** * justif — text justification for perfectionists. * * `justify(document.querySelectorAll("p"))` re-lays-out existing paragraphs * with Knuth-Plass optimal line breaking, character protrusion (optical * margin alignment, per-font via measured glyph geometry), font expansion * on variable fonts with a wdth axis, and optional letterfit tracking * (Bringhurst's ±3%). Resize re-runs arithmetic only; `destroy()` restores * the original DOM. */ interface JustifyController { /** * Resolves once the content's font faces have settled (loaded or * failed) and the layout converged on them. The text is enhanced * earlier than this — justify() commits synchronously against * whatever fonts are rendering at call time, so a still-loading * webfont shows its fallback justified until the faces settle. */ readonly ready: Promise; /** * Re-measure with the currently loaded font files and re-layout (also runs * automatically when webfonts finish loading). The original scan is reused, so * CSS changes need `rescan()` and content changes a fresh controller. */ refresh(): void; /** * Re-read author CSS and re-enhance wherever it now reads differently: what to * call after changing the styling of managed paragraphs — `hyphens`, the font, * `letter-spacing`, `white-space`, `line-height`, `text-indent` — from a * stylesheet, a class, a theme toggle, or the devtools inspector. * * Returns the paragraphs it re-read. Ones whose styling is unchanged are left * strictly alone, so calling this on every suspicion is cheap: the check is one * computed-style read each. Paragraphs previously DECLINED are retried on the * same terms, since a style change is exactly what can make one eligible. * * `targets` narrows the work to some of this controller's paragraphs; omitted, * it considers all of them. Paragraphs released by `unjustify()` stay released. * * A re-read paragraph is restored to its author DOM and enhanced again, so — * unlike `refresh()` — a selection or caret inside one does not survive. Text * `content` changes are still out of scope: what gets re-read is the CSS. */ rescan(targets?: Iterable): readonly HTMLElement[]; /** * Replace this controller's layout settings and re-lay out its paragraphs, * reusing the existing scan. Cheaper than `destroy()` + `justify()`, and it * keeps observers, clipboard registration, and paragraph identity. * * `config` is COMPLETE, not a patch: a field left out takes the library * default, which is how a caller restores one. Anything outside * `LayoutOptions` is untouched — notably `hyphenate` (with its memoized * cache), `onSkip`, and `onRelayout`. `cleanClipboard` and `observeResize` * are deliberately not reconfigurable: the first registers a shared copy * handler once, and the second attaches observers once, so changing either * needs a fresh controller. */ applyLayoutOptions(config: LayoutOptions): void; /** Restore the original DOM and disconnect observers. */ destroy(): void; readonly paragraphs: readonly HTMLElement[]; /** * The subset of `paragraphs` this controller still manages. Absent are ones * it declined, ones released by `destroy()` or `unjustify()`, and ones whose * enhancement was removed from the DOM from outside. * * Paragraphs sitting in native one-line layout ARE managed: they carry no * `data-justif` attribute, but the controller still holds their measurements * and watches for a measure narrow enough to make line breaking useful. Test * this rather than the attribute to ask "is this enhancement still live?". * * The attribute answers the OTHER question, "is justif's rendering on the * page right now?": every enhancement sets `data-justif` and every restore * removes it, so a managed paragraph without the attribute is one the * controller is watching but currently renders natively — a paragraph * short enough for one line, or one whose leading float leaves no room to * set beside it. Both signals are supported; they are two questions. */ readonly managed: readonly HTMLElement[]; } declare function justify(targets: Element | Iterable, options?: JustifyOptions): JustifyController; /** Restore paragraphs enhanced by any controller to their original DOM. */ declare function unjustify(targets: Element | Iterable): void; /** * Drop-in auto-enhancement — the zero-config entry: * * * * The page's own CSS decides what gets enhanced: every candidate element * whose computed `text-align` is `justify` (the fallback rendering justif * recommends anyway) is upgraded in place; nothing else is touched, so * adding the script never changes the alignment of anything. * * Hyphenation follows the page's declared languages: en-US is inlined * (English or unlabeled content costs no extra request), and content whose * nearest `lang` attribute matches one of the other bundled languages * loads that pattern module on demand — a sibling file on the same CDN, * one small request per distinct language. Languages we don't bundle (and * failed loads, e.g. when only this one file was copied to a static host) * justify with spacing only: wrong-language hyphenation is worse than * none. For full control use the API: `import { justify } from "justif"`. * * Typography is configured with `--justif-*` custom properties on the * paragraphs, because that is where element-scoped settings belong: the cascade * decides precedence, so there is none to invent here. Paragraphs group by * language AND resolved configuration, so configuring nothing still costs * exactly one controller per language. auto-options.ts owns that surface, and * auto-watch.ts is what makes changes to it apply on their own where the * engine allows. Which patterns a language wants, and what it costs to get * them, is auto-languages.ts. * * What is left here is the orchestration: grouping the page, starting a * controller per group, and reconciling that against what the CSS now says. * * The script tag carries only what is not element-scoped: * data-justif-selector="article p" candidate elements (default below) * data-justif-defer read the page after the page's own * DOMContentLoaded work, not before it * * Controllers are exposed at `window.justif.controllers` (with `justify` * and `unjustify`) as an escape hatch for debugging or teardown, and * `window.justif.booted` settles once every group's fonts settled and its * layout converged. FULL teardown must await `booted` first: language * groups whose patterns load on demand may not have pushed their final * controller yet (tearing down an interim-committed group by any route — * destroy(), unjustify(), a manual restore — cancels its pending * upgrade, but a group that committed no interim has nothing to tear * down until its pattern module lands). */ declare global { interface Window { justif?: { justify: typeof justify; unjustify: typeof unjustify; controllers: ReturnType[]; /** Settles once every group's fonts have settled and its layout * converged. The text is justified earlier — as soon as every * group has committed against the fonts rendering at that moment. */ booted: Promise; /** * Re-read the `--justif-*` configuration and bring controllers into line * with it. Configuration changes normally apply on their own; this is the * explicit path for the cases the watcher cannot see — an engine without * `@property`, or a paragraph carrying the author's own `transition` * declaration, which is never replaced. * * Resolves once the affected controllers have settled. `booted` keeps its * original meaning and does not re-arm: on a page whose configuration * changes periodically it would never settle. */ reconfigure: () => Promise; }; } }