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

@scenesystems/effect-text

v0.4.0

Published

Effect-native text preparation, measurement, and greedy multiline layout

Readme

@scenesystems/effect-text

@scenesystems/effect-text lays out text into lines without a browser layout engine. Use it when you need line breaks, line widths, and total height for a string at a given width, and you need them repeatedly: in a canvas renderer, a virtualized list, a resize handler, or an animation that changes the available width every frame.

The package splits the work in two. Preparation is an Effect: it segments the text, measures its runs through a TextMeasurer service, consults an EngineProfile and optional hyphenation dictionary, and compiles the results into a prepared handle. Layout is a pure function of that handle and a width. Prepare once, then lay out at as many widths as you like with no measurement, no services, and no error channel.

The experimental calibration surface tunes engine profiles against measured layouts with @scenesystems/effect-search and scores them with @scenesystems/effect-math. The preparation and layout path does not depend on either.

Installation

npm install @scenesystems/effect-text effect

Effect ^3.22.1 is a required peer dependency.

Basic use

Text.prepareWithSegments prepares a string once. Text.layout returns the line count, height, and widest line for a width; Text.layoutLines returns the lines themselves.

import { Effect } from "effect"
import { Text } from "@scenesystems/effect-text"

export const program = Effect.gen(function* () {
  const prepared = yield* Text.prepareWithSegments({
    text: "Prepare once, then lay out at several widths.",
    font: { family: "Mono", size: 16 },
    whiteSpace: "normal"
  })

  return {
    compact: Text.layout(prepared, { maxWidth: 120, lineHeight: 20 }),
    wide: Text.layoutLines(prepared, { maxWidth: 240, lineHeight: 20 })
  }
}).pipe(Effect.provide(Text.TextLayoutLive))

Text.TextLayoutLive bundles the default services: an Intl-based word segmenter, a deterministic width estimator, an in-memory measurement cache, the default engine profile, and the bundled hyphenation dictionaries. It is enough for tests, servers, and any place where estimated widths are acceptable. Browser applications replace the measurer with a canvas-backed one, described below.

Preparation and layout

PrepareInput has three required fields and one optional one. text is the string. font names the family, size, and optional weight that the measurer will use. whiteSpace is normal, which collapses runs of whitespace, or pre-wrap, which preserves spaces, tabs, and hard breaks. hyphenationLocale opts a string into dictionary hyphenation.

Two preparation functions return different handles. Text.prepare returns a PreparedText that supports summaries and Text.measureNaturalWidth, the width of the widest unbroken chunk. Text.prepareWithSegments returns a PreparedTextWithSegments that also retains the logical segments needed to materialize lines, step cursors, and vary the width per line. Choose the smaller handle when you only need geometry. Text.prepareUnknown decodes untrusted input against the schema before preparing it.

Layout functions take a handle and a LayoutRequest with a positive maxWidth and lineHeight:

| Function | Handle | Returns | | ----------------------------- | ------------- | ----------------------------------------------------------------- | | Text.layout | either | lineCount, height, and maxLineWidth | | Text.layoutLines | with segments | The visual lines with their text and painted width | | Text.layoutLinesWithSummary | with segments | Lines and summary from one walk | | Text.layoutLinesWith | with segments | Lines where a resolver supplies the width for each line index | | Text.layoutNextLine | with segments | One line and the cursor for the next, or Option.none at the end | | Text.streamLines | with segments | A Stream of lines that computes only as far as it is pulled |

Text.layoutLinesWith is how text flows around obstacles or into a shaped container: return a different maxWidth for each line. Text.layoutNextLine with Text.initialCursor() and Text.streamLines serve virtualized rendering, where only the first visible lines are needed.

import { Chunk, Effect, Stream } from "effect"
import { Text } from "@scenesystems/effect-text"

export const program = Effect.gen(function* () {
  const prepared = yield* Text.prepareWithSegments({
    text: "Text can flow into a shape when each line asks for its own width.",
    font: { family: "Mono", size: 14 },
    whiteSpace: "normal"
  })
  const request = { maxWidth: 160, lineHeight: 18 }

  const shaped = Text.layoutLinesWith(prepared, request, (lineIndex) => 160 - lineIndex * 20)
  const firstThree = yield* Text.streamLines(prepared, request).pipe(Stream.take(3), Stream.runCollect)

  return { shaped, firstThree: Chunk.toReadonlyArray(firstThree) }
}).pipe(Effect.provide(Text.TextLayoutLive))

Line breaking prefers hard breaks, then soft hyphens, then dictionary hyphens, then explicit break opportunities, and falls back to breaking between graphemes only when a single grapheme exceeds the width. Tabs align to four-column stops.

Measurement services

Preparation requires five services, all declared in Contracts: WordSegmenter, TextMeasurer, MeasurementCache, EngineProfile, and HyphenationDictionary. Text.TextLayoutLive provides all of them. To replace one, compose the individual layers instead.

import { Effect, Layer } from "effect"
import { Contracts, Text } from "@scenesystems/effect-text"

const services = Layer.mergeAll(
  Text.WordSegmenterLive,
  Text.NoHyphenationDictionaryLive,
  Layer.succeed(Contracts.EngineProfile, {
    lineFitEpsilon: 0.01,
    tabWidth: 8,
    defaultDirection: "ltr",
    preferEarlySoftHyphenBreak: true,
    preferPrefixWidthsForBreakableRuns: true
  }),
  Text.MeasurementCacheLive.pipe(Layer.provide(Text.TextMeasurerLive))
)

export const program = Text.prepare({
  text: "soft\u00adhyphen and\tcustom tabs",
  font: { family: "Mono", size: 12 },
  whiteSpace: "pre-wrap"
}).pipe(
  Effect.map((prepared) => Text.layout(prepared, { maxWidth: 72, lineHeight: 16 })),
  Effect.provide(services)
)

In a browser, measure with the real font. Browser.CanvasTextMeasurerLive wraps a 2D canvas context, serializes access to it, and optionally corrects under-reported emoji advances. Browser.BrowserMeasurementCacheLive keys its cache by a support-profile id and a font-readiness revision, so measurements taken before a web font loaded are discarded once you bump the revision. Browser.browserSupportProfile returns the engine profile tuned for canvas-monospace or canvas-system-ui.

import { Effect, Layer } from "effect"
import { Browser, Contracts, Text } from "@scenesystems/effect-text"

type CanvasContext = Parameters<typeof Browser.CanvasTextMeasurerLive>[0]["context"]

export const layoutOnCanvas = (context: CanvasContext, text: string, maxWidth: number) => {
  const profile = Browser.browserSupportProfile("canvas-system-ui")
  const services = Layer.mergeAll(
    Text.WordSegmenterLive,
    Text.NoHyphenationDictionaryLive,
    Layer.succeed(Contracts.EngineProfile, profile.engineProfile),
    Browser.BrowserMeasurementCacheLive({
      profileId: profile.id,
      fontReadinessRevision: Browser.initialFontReadinessRevision()
    }).pipe(Layer.provide(Browser.CanvasTextMeasurerLive({ context, textBaseline: "alphabetic" })))
  )

  return Text.prepareWithSegments({ text, font: { family: "system-ui", size: 16 }, whiteSpace: "normal" }).pipe(
    Effect.map((prepared) => Text.layoutLinesWithSummary(prepared, { maxWidth, lineHeight: 22 })),
    Effect.provide(services)
  )
}

Widths are in the measurer's units, which for canvas measurement are CSS pixels. Validate against your target browsers and fonts; the package guarantees consistent breaking for a given set of measurements, not equivalence with a browser's own layout.

Hyphenation

Set hyphenationLocale on the prepare input to break words at dictionary hyphenation points. The default layer bundles en-us, en-gb, de, fr, and es, and falls back from an exact tag to its base language. Text.HyphenationSupport lists the bundled locales.

Text.HyphenationDictionaryLive({ dictionaries }) adds or overrides entries; each word maps to the indexes where a hyphen may be inserted. Text.NoHyphenationDictionaryLive disables dictionary hyphenation while keeping soft hyphens (U+00AD) in the text as break opportunities.

import { Effect, Layer } from "effect"
import { Text } from "@scenesystems/effect-text"

const services = Layer.mergeAll(
  Text.WordSegmenterLive,
  Text.EngineProfileLive,
  Text.MeasurementCacheLive.pipe(Layer.provide(Text.TextMeasurerLive)),
  Text.HyphenationDictionaryLive({ dictionaries: { "en-gb": { colouration: [3, 6] } } })
)

export const program = Text.prepareWithSegments({
  text: "colouration",
  font: { family: "Mono", size: 10 },
  hyphenationLocale: "en-gb",
  whiteSpace: "normal"
}).pipe(
  Effect.map((prepared) => Text.layoutLines(prepared, { maxWidth: 35, lineHeight: 12 })),
  Effect.provide(services)
)

React integration

The React module contains no components or hooks. It provides the two pieces that a React integration needs and that are easy to get wrong: a stable cache identity for prepared handles and a pure projection for render time.

React.prepareIdentityFor combines the prepare input, engine profile, support-profile id, and font-readiness revision into a PrepareIdentity, a structural Data.Class whose equality and hash follow its fields, so it is directly usable as a HashMap key or an Atom.family argument. Two inputs with equal identities produce the same prepared handle, so the application can run preparation once per identity and keep the handle in state; React.prepareInputFromIdentity recovers the prepare input on a cache miss. React.projectPreparedLayout is Text.layoutLinesWithSummary under a name that signals it is safe to call during render: it measures nothing and touches no services.

The application owns the rest: running preparation effects, storing handles, bumping the font-readiness revision when document.fonts changes, and calling the projection in render or resize work.

Public surface

Every module is available as a namespace from the package root and as a subpath such as @scenesystems/effect-text/Text.

| Module | Scope | | --------------------------------------------- | ------------------------------------------------------------------------------------------------- | | Text | Prepare inputs, prepared handles, layout functions, cursors, streams, and default layers | | Contracts | WordSegmenter, TextMeasurer, MeasurementCache, EngineProfile, and HyphenationDictionary | | Browser | Canvas measurement, browser measurement cache, font-readiness revisions, and support profiles | | React | Prepare identities and pure layout projection | | Errors | MeasurementFailed, TextLayoutDecodeError, and the PrepareError union | | Experimental | Search-backed engine-profile calibration; may change outside semver guarantees |

Contracts and Errors are stable within the current release line. Text, Browser, and React are provisional and may change in minor releases. Paths under internal are not exported.

Errors and boundaries

Text.prepare and Text.prepareWithSegments fail with MeasurementFailed when the measurer cannot measure a run. Text.prepareUnknown also fails with TextLayoutDecodeError for input that does not match PrepareInput; PrepareError is the union of the two. Layout functions have no error channel: once a handle exists, every width produces a result.

This is a bounded manual layout engine. It resolves bidirectional levels for mixed-direction text and mirrors paired punctuation, but it declines unsupported bidi control characters at preparation time and performs no font shaping. Full CSS layout equivalence is out of scope.

Examples

The examples directory contains one runnable program per capability. Start with the quick start, then follow the topic you need: cursors and streams, explicit services, canvas measurement, dictionary hyphenation, and experimental calibration.

Status

This package is pre-1.0. Minor releases may change public APIs; pin a compatible version and review the changelog when upgrading. The Experimental module may change or be removed with less migration support than the other modules.

Contributing and support

Read the repository contributing guide before opening a pull request. Report defects and request changes through GitHub issues. For security concerns, follow the security policy.

Attribution

The split between effectful preparation and pure layout is inspired by pretext.

License

MIT. Copyright 2026 Scene Systems.