npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@kitlangton/justice

v0.3.2

Published

A small, DOM-free paragraph justification engine.

Readme

@kitlangton/justice

Small, dependency-free paragraph justification engine. ESM with TypeScript declarations.

bun add @kitlangton/justice

Compose a paragraph

import { prepare, solve, lineText } from "@kitlangton/justice"

// Supply widths in pixels for the exact font and shaping configuration used to render.
const prepared = prepare(text, measureTextWidth)
const layout = solve(prepared, 620)

for (const line of layout.lines) {
  console.log(lineText(prepared, line), {
    wordSpacing: line.wordSpacing,
    letterSpacing: line.tracking,
    marginLeft: -line.opening,
  })
}

prepare measures words once and retains prefix sums. Reuse its result across widths and policy changes. solve returns line boundaries, spacing adjustments, optical allowances, residuals, costs, and a candidate count. Positive residuals are underfilled; negative residuals are overfull. defaults and the Options, Measure, Prepared, Line, and Layout types are exported.

Per-line measures

solve(prepared, [620 - 32, 620])           // first-line indent of 32px
solve(prepared, [620 - 96, 620 - 96, 620]) // a two-line drop cap

A Measure is a single width or one width per line; the final entry repeats for all remaining lines. The solver treats the line index as part of its search state, so a paragraph is still fitted as a whole. Each returned line.width records the measure it was fitted to. The renderer offsets the narrowed lines (by the indent, or by the cap's width) itself.

Words containing a hyphen between letters or digits (well-known) may break after the hyphen without adding a glyph; explicitHyphenPenalty prices that choice. Such lines report an endOffset but not hyphenated.

Optional hyphenation

import { prepare, withHyphenation, solve } from "@kitlangton/justice"

const prepared = withHyphenation(
  prepare(text, measureTextWidth),
  hyphenateWord, // e.g. "configuration" → ["con", "fig", "u", "ra", "tion"]
  measureTextWidth,
)
const layout = solve(prepared, 620, {
  hyphenPenalty: 50,
  consecutiveHyphenPenalty: 200,
  finalHyphenPenalty: 200,
  explicitHyphenPenalty: 20, // breaking after a hyphen already in the source
})

Supply your language's dictionary and minimum word/prefix/suffix rules through hyphenateWord(word, index). Return nonempty parts that concatenate exactly to the input word, or [word] to keep it whole. Breaks must respect grapheme boundaries; NBSP-containing tokens always remain indivisible. No dictionary is bundled into the numerical core. Measurement receives (fragment, wordIndex) so an adapter can use the corresponding word's font. It measures complete shaped fragments including their hyphen, rather than adding isolated glyph widths.

Whole-word and discretionary breaks compete in the same fitting pass. The optional path searches the expanded break graph without overflow pruning; paragraphs with no discretionary breaks retain the pruned whole-word path. Costs discourage hyphens, consecutive hyphenated lines, and a hyphen before the final line. Prepared fragment measurements are reused across solves.

Hyphenated layouts retain source word indices. line.startOffset and line.endOffset are UTF-16 offsets inside the first/last included word (defaults: zero and that word's length). line.hyphenated requests a visible trailing -. lineText returns this display text. To reconstruct source, omit the generated hyphen and join to the next line without a space; ordinary line breaks retain a space. DOM renderers should keep generated hyphens out of narration and clipboard text. Naturally fitting final lines still remain ragged.

Policy

solve(prepared, 620, {
  mode: "balanced",        // "strict" keeps stretch/shrink limits hard
  stretch: 0.6,
  shrink: 0.25,
  tracking: 0.3,            // maximum pixels per grapheme
  compressionPenalty: 2,
  emergencyStretch: 0.15,  // maximum fallback credit relative to column width
  hanging: 1,               // trailing punctuation allowance
  opening: 0.3,             // leading quote allowance
  protrusion: 1,            // strength of supplied font-aware optical margins
  adjacentPenalty: 100,     // discourage abrupt tight/loose neighbour transitions
  lastLine: 0.33,
  ending: "fit",           // "soft" uses a width-relative ending penalty
  widowPenalty: 300,
})

Balanced mode first tries ordinary fitting, then relaxes stretch scoring if necessary. Shrink and tracking remain bounded. Naturally fitting final lines remain ragged. Opening quotes and trailing punctuation can sit beyond the nominal margin by their measured allowances.

Emergency fitting adds scoring credit up to width * emergencyStretch, capped at one measured natural space per inter-word gap. The scoring denominator is at least one natural space, including lines with no gaps. This keeps sparse lines with enormous spaces expensive rather than treating the entire column as extra spacing capacity. The credit changes which breaks win, not the rendering limits: balanced mode can still expand spaces when no better complete layout exists.

The solver retains four line fitness classes (tight, decent, loose, very loose) at each break. A jump of more than one class adds adjacentPenalty. This cost participates in the paragraph search, including hyphenated layouts; it is not a spacing adjustment applied afterwards. Set it to zero for independent line scoring and a single fitness state. line.fitness exposes the selected class as 0–3; each returned line.cost includes its incoming adjacency cost.

Font-aware optical margins

import { withOpticalMargins } from "@kitlangton/justice"

// Apply after withHyphenation, if used. Return absolute pixel allowances for
// the actual face at each edge. Measurement is supplied by the consumer.
const optical = withOpticalMargins(prepared, (text, wordIndex) =>
  measureFontEdges(text, wordIndex))
const layout = solve(optical, 620)

The callback is called for full words, continuation suffixes, and the generated hyphen. Its nonnegative start credit replaces the quote-only allowance; its end credit combines with punctuation hanging by taking the larger value. Set protrusion: 0 to disable these supplied optical credits while keeping punctuation hanging. Plain prepare inputs retain their quote/punctuation model. Both solvers include the optical allowances when choosing breaks. Whole-word overflow pruning accounts for the largest possible leading allowance.

The core accepts numerical measurements; it does not include a browser rasterizer. A consumer can supply an ink-profile measurer for its font (for example, sampling glyph edges on a canvas) and pass the resulting allowances here.

Build-time compilation

import { compileStatic, staticSpacing } from "@kitlangton/justice/static"

const bands = compileStatic(prepared, 320, 960)   // sample every px from min to max
const shared = staticSpacing(prepared)            // CSS custom properties, once per paragraph

compileStatic solves the paragraph at each sampled measure and coalesces adjacent identical break plans into half-open bands { min, max, lines }. Within a band the breaks are fixed and the spacing is expressed as CSS calc() against the container width, so a static page needs no script at runtime: emit one container query per band, render each band's lines as words and gaps carrying the band's properties, and declare staticSpacing on the paragraph. Bind --j-t to letter-spacing, --j-gap to a gap's width, and --j-left to the first word's horizontal offset. A band whose lines is null did not fit at that measure; keep native wrapping there.

Sampling is explicit: unsampled fractional widths are not promised the same optimum as their neighbours. Measure with the fonts the page will load; a late font swap invalidates the compiled plan exactly as it would a live one.

Rendering contract

The package is the numerical engine; it does not measure browser text or manage the DOM. Render each line separately with wrapping disabled and apply its wordSpacing, tracking, and negative opening offset. Width predictions assume that letter spacing contributes once per grapheme, including ordinary spaces. Nonbreaking spaces stay inside a word and should not receive interword spacing. Measure and render with the same kerning and ligature settings so the measured words compose predictably.

Process paragraphs and explicit hard-break segments separately. Supported input is horizontal LTR, single-font, space-delimited plain text, with optional supplied hyphenation and supplied glyph-specific optical margins. Rich inline markup and bidi/CJK breaking are outside this version's scope.

The core can execute during SSR or a Cloudflare build/Worker request when supplied matching measurements and a known width. Font measurement, responsive-width selection, and SSR markup integration belong to the caller.

Requires an ECMAScript 2022 runtime with Intl.Segmenter.