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

knuth-plass-wrap

v1.1.0

Published

TeX-quality Knuth-Plass line breaking for the web, powered by HarfBuzz WASM

Readme

knuth-plass-wrap

TeX-quality Knuth-Plass line breaking for the web, powered by HarfBuzz WASM.

Produces optimally justified paragraphs by considering the entire text at once — the same algorithm TeX has used since 1981 — instead of the greedy line-at-a-time approach browsers use. Word measurement runs through HarfBuzz (via the harfrust Rust port) compiled to WebAssembly, so glyph widths match the browser's rendering with sub-pixel accuracy.

Features

  • Optimal line breaking — Knuth-Plass dynamic programming minimises total paragraph demerits
  • HarfBuzz-accurate measurement — glyph shaping in WASM matches browser rendering
  • Hyphenation — automatic hyphenation (17 languages) via the hypher crate
  • Hz justification — micro-adjusts the font's wdth axis per-line for tighter composition
  • Variable font supportwght, opsz, ital/slnt, wdth axes
  • React 19use() + Suspense for zero-effect async loading
  • Headless hookuseKnuthPlassWrap returns lines for custom rendering
  • Vanilla JSinit() + layoutParagraph() works without React
  • Tree-shakeable — separate knuth-plass-wrap/core and knuth-plass-wrap/react entry points
  • Small — ~80 KB WASM binary (gzipped)

Installation

npm install knuth-plass-wrap
# or
pnpm add knuth-plass-wrap

Quick Start

React Component

import { Suspense } from "react";
import { KnuthPlassWrap } from "knuth-plass-wrap/react";

function Article() {
  return (
    <Suspense fallback={<p>Loading…</p>}>
      <KnuthPlassWrap
        text="The problem of breaking a paragraph into lines of approximately equal length has been important since the invention of movable type."
        fontUrl="/fonts/Literata[opsz,wght].ttf"
        fontSize={17}
        lineWidth={400}
      />
    </Suspense>
  );
}

The component suspends while the WASM module and font binary load, then renders justified lines as plain <div> elements with text-align: justify.

Headless Hook

Use useKnuthPlassWrap when you need custom rendering (canvas, SVG, etc.):

import { useKnuthPlassWrap } from "knuth-plass-wrap/react";

function CustomRenderer({ fontData }: { fontData: ArrayBuffer }) {
  const { lines } = useKnuthPlassWrap({
    text: "Your paragraph text here…",
    fontData,
    fontSize: 16,
    lineWidth: 500,
  });

  return (
    <div>
      {lines.map((line, i) => (
        <div key={i}>{line.words.join(" ")}</div>
      ))}
    </div>
  );
}

Vanilla JS (No React)

import { init, layoutParagraph } from "knuth-plass-wrap/core";

await init();

const fontData = await fetch("/fonts/Inter[opsz,wght].ttf").then(r => r.arrayBuffer());
const lines = layoutParagraph(fontData, {
  text: "Your paragraph text…",
  fontSize: 16,
  lineWidth: 400,
});

for (const line of lines) {
  console.log(line.words.join(" "));
}

API Reference

init(input?): Promise<void>

Initialise the WASM module. Must be called before layoutParagraph or measureWord. The component and hook call this automatically.

// Default — loads .wasm from the package directory
await init();

// Custom URL (self-hosted, CDN, etc.)
await init("https://cdn.example.com/kp_break_wasm_bg.wasm");

// Fetch response
await init(fetch("/my-path/kp_break_wasm_bg.wasm"));

layoutParagraph(fontData, options): Line[] | HzLine[]

Lay out a paragraph. The entire pipeline — measurement, tokenisation, hyphenation, optimal breaking, and line construction — runs in a single WASM call.

| Option | Type | Default | Description | |--------|------|---------|-------------| | text | string | required | Paragraph text | | fontSize | number | required | Font size in CSS px | | lineWidth | number | required | Target line width in CSS px | | fontWeight | number | 400 | Font weight (variable font axis) | | liga | boolean | true | Standard ligatures | | opsz | number | fontSize | Optical sizing axis value (0 = disabled) | | ital | number | 0 | Italic/slant axis (e.g. 12 for 12° slant) | | hyphenate | boolean | false | Automatic English hyphenation | | similarityDemerits | number | 2000 | Penalty for adjacent lines with different tightness (0 = disabled) | | hz | { min, max } | — | Hz justification wdth axis range |

measureWord(fontData, fontSize, word, ...): number

Measure the advance width of a single word using HarfBuzz shaping. Useful for debugging or building custom layout logic.

KnuthPlassWrap (React Component)

Must be wrapped in a <Suspense> boundary.

| Prop | Type | Default | Description | |------|------|---------|-------------| | text | string | required | Paragraph text | | fontSize | number | required | Font size in CSS px | | lineWidth | number | required | Target line width in CSS px | | fontData | ArrayBuffer | — | Raw TTF/OTF font binary | | fontUrl | string | — | URL to font file (fetched and cached) | | fontDataMap | Record<number, ArrayBuffer> | — | Weight-keyed binaries for static font families | | fontFamily | string | — | CSS font-family (when managing @font-face yourself) | | fontWeight | number | 400 | CSS font-weight / variable font axis | | fontStyle | string | "normal" | CSS font-style | | fontStretch | string | "100%" | CSS font-stretch | | lineHeight | number | 1.6 | Line height multiplier | | color | string | "#2a2623" | Text colour | | liga | boolean | true | Standard ligatures | | opticalSizing | "auto" \| "none" \| number | "auto" | Optical sizing | | hyphenate | boolean | false | Automatic hyphenation | | similarity | boolean | true | Similarity demerits | | hz | { min, max } | — | Hz justification wdth range | | className | string | — | Container CSS class | | style | CSSProperties | — | Container inline styles | | fallback | ReactNode | — | Content shown while loading |

useKnuthPlassWrap(options): { lines, isLoading }

Headless hook for custom rendering. Same WASM engine, you control the DOM.

| Option | Type | Default | Description | |--------|------|---------|-------------| | text | string | required | Paragraph text | | fontData | ArrayBuffer \| null | required | Font binary (null while loading) | | fontSize | number | required | Font size in CSS px | | lineWidth | number | required | Target line width in CSS px | | fontWeight | number | 400 | Font weight | | liga | boolean | true | Ligatures | | opsz | number | fontSize | Optical sizing (0 = disabled) | | hyphenate | boolean | false | Hyphenation | | similarity | boolean | true | Similarity demerits | | hz | { min, max } | — | Hz justification range |

Font Registration Utilities

When you provide fontData or fontUrl without a fontFamily, the component automatically registers the font binary as a scoped @font-face so the browser renders with the exact same bytes HarfBuzz shaped.

import { registerFontBinary, registerFontBinaryMap } from "knuth-plass-wrap/core";

// Single variable font
const { name, ready } = registerFontBinary("MyFont", arrayBuffer);
await ready;

// Static font family (multiple weights)
const { name, ready } = registerFontBinaryMap("MyFont", [
  { binary: regular, weight: 400 },
  { binary: bold, weight: 700 },
]);
await ready;

Line Data

Each line returned by layoutParagraph or useKnuthPlassWrap has:

interface Line {
  words: string[];      // Words on this line
  widths: number[];     // Per-word widths in px
  boxW: number;         // Total word width (excluding spaces)
  spaceWidth: number;   // Natural inter-word space width
  last: boolean;        // True for the last line (left-aligned)
}

interface HzLine extends Line {
  wdth: number;         // CSS font-variation-settings 'wdth' value (100 = normal)
}

Hz Justification

When the font supports a wdth variation axis, Hz justification micro-adjusts the font width per-line to achieve tighter composition — the same technique used by Adobe InDesign and Hermann Zapf's Hz-program.

<KnuthPlassWrap
  text={text}
  fontUrl="/fonts/RobotoFlex-VariableFont.ttf"
  fontSize={16}
  lineWidth={400}
  hz={{ min: 95, max: 105 }}
/>

Browser Support

Requires WebAssembly, FontFace API, and fetch. Works in all modern browsers (Chrome 57+, Firefox 52+, Safari 11+, Edge 79+).

CDN Usage

<script type="module">
  import { init, layoutParagraph } from "https://esm.sh/knuth-plass-wrap/core";

  await init("https://esm.sh/knuth-plass-wrap/wasm/kp_break_wasm_bg.wasm");

  const fontData = await fetch("/fonts/MyFont.ttf").then(r => r.arrayBuffer());
  const lines = layoutParagraph(fontData, {
    text: "Your text here…",
    fontSize: 16,
    lineWidth: 400,
  });
</script>

Development

pnpm install
pnpm build:wasm    # Build WASM (requires Rust + wasm-pack)
pnpm dev           # Start Vite dev server
pnpm build         # Build library (WASM + ESM + CJS + types)
pnpm build:demo    # Build demo app
pnpm lint          # Run ESLint
pnpm test          # Run Rust tests

License

MIT