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

@smooth-reading/core

v0.3.0

Published

Zero-dependency guided fixation reading: emphasise the first part of every word so the eye gets an artificial fixation point. Unicode-aware, streaming, ESM + CJS.

Readme

@smooth-reading/core

Guided fixation reading for modern JavaScript and TypeScript: the leading letters of every word are emphasised so the eye lands on an artificial fixation point and the brain completes the rest of the word.

Similar to commercial fixation-reading products, this package is an independent, clean-room, zero-dependency, Apache-2.0 library implemented strictly against the project specification.

  • Zero runtime dependencies: Pure TypeScript, ESM and CJS outputs, sideEffects: false, fully tree-shakeable.
  • Universal compatibility: Runs in all modern browsers, Node.js 18+, Deno, Bun, and React Native (Hermes).
  • Unicode & script accurate: Uses Intl.Segmenter for real dictionary word breaking (Chinese, Japanese, Thai, Lao, Khmer, Burmese) and grapheme cluster counting, with a spec-compliant regular expression fallback.
  • Four primary entry points:
    • toHtml: Fast, safe HTML string transformer.
    • tokenize: Pure token array for building UI components without innerHTML.
    • applyToElement: Non-destructive DOM walker with a clean, idempotent restore() callback.
    • createTransformStream: Web standard streaming transform for LLM output and chunked text.

Installation

npm install @smooth-reading/core
# pnpm add @smooth-reading/core  ·  yarn add @smooth-reading/core  ·  bun add @smooth-reading/core

Usage

1. toHtml — String in, HTML out

import { toHtml } from '@smooth-reading/core';

toHtml('Smooth reading works.');
// '<b>Smo</b>oth <b>read</b>ing <b>wor</b>ks.'

toHtml('Smooth reading works.', { fixation: 5, saccade: 2 });
// '<b>Smoot</b>h reading <b>work</b>s.'

By default, the input is treated as HTML (ignoreHtmlTags: true):

  • HTML tags and valid character references (&amp;, &#x27;) pass through untouched.
  • Special HTML characters (&, <, >, ") in regular text are safely escaped.
  • Text within elements listed in skipTags (such as <code> or <pre>), as well as text already inside emphasis tags, is left untouched.
toHtml('Try <code>toHtml(x)</code> now.');
// '<b>Tr</b>y <code>toHtml(x)</code> <b>no</b>w.'

toHtml('Tom & Jerry &amp; 5 < 6');
// '<b>To</b>m &amp; <b>Jer</b>ry &amp; 5 &lt; 6'

Pass ignoreHtmlTags: false to treat the entire input as plain text and escape all HTML characters.


2. tokenize — Build custom UI components

Framework components can render native elements instead of serializing to HTML strings. tokenize returns an array of structured tokens:

import { tokenize } from '@smooth-reading/core';

export function SmoothText({ text }: { text: string }) {
  const tokens = tokenize(text);

  return (
    <>
      {tokens.map((token, index) =>
        token.type === 'word' && token.fixation > 0 ? (
          <span key={index}>
            <b>{token.fixationText}</b>
            {token.restText}
          </span>
        ) : (
          <span key={index}>{token.text}</span>
        ),
      )}
    </>
  );
}
  • Concatenating token.text across all tokens reconstructs the original string byte-for-byte.
  • For every word token: token.fixationText + token.restText === token.text.

For ready-to-use framework adapters, see docs/recipes for React, Vue, Svelte, Angular, React Native, and Web Components.


3. applyToElement — In-place DOM enhancement with restoration

Use applyToElement to enhance existing server-rendered HTML or third-party markup in the browser:

import { applyToElement } from '@smooth-reading/core';

const article = document.querySelector('article')!;
const restore = applyToElement(article, {
  fixation: 4,
  skipSelector: '.no-smooth, [data-verbatim]',
});

// Later: restore original DOM text nodes byte-for-byte
restore();
  • Safe text node manipulation: Processes each text node individually. Words split across inline elements (smo<i>oth</i>) are handled without restructuring the DOM. Text is inserted as text nodes, never via innerHTML.
  • Idempotent: Tracks generated nodes and avoids double-wrapping.
  • Resilient cleanup: restore() tolerates DOM mutations made while active. Removed wrappers are ignored, added content remains untouched, and re-calling restore() is a safe no-op.

4. Streaming with createTransformStream

Transform chunked text or streaming LLM completions in real time:

import { createTransformStream } from '@smooth-reading/core';

await response.body!
  .pipeThrough(new TextDecoderStream())
  .pipeThrough(createTransformStream({ fixation: 3 }))
  .pipeTo(writableDestination);
  • Only the tail segment after the last safe boundary is buffered.
  • Words, grapheme clusters, HTML tags, and character references are never split across chunks.
  • saccade state is maintained seamlessly across chunks, and flush emits any remaining buffered text.
  • Concatenated stream output is identical to calling toHtml on the full input.

Configuration options

Algorithm options

Shared across tokenize, toHtml, applyToElement, and createTransformStream:

| Option | Type | Default | Description | | --- | --- | --- | --- | | fixation | 1 \| 2 \| 3 \| 4 \| 5 | 3 | Fixation strength. Ratios: 0.20 / 0.35 / 0.50 / 0.65 / 0.80. Non-integers are truncated toward zero and values outside 1…5 are clamped (4.94, 995); non-finite values fall back to 3. | | saccade | number | 1 | Emphasises every n-th word. Every word token consumes an index (including numbers and short words). Non-integers are truncated and values < 1 are clamped to 1. | | minWordLength | number | 1 | Minimum grapheme clusters required for a word to receive fixation. | | emphasizeNumbers | boolean | false | Whether to emphasise words composed entirely of decimal digits. | | locale | string | undefined | BCP-47 language tag passed to Intl.Segmenter. Falls back to runtime default if unspecified or invalid. | | fixationLength | FixationLengthFn | undefined | Custom function (word, graphemes, options) => number to override the fixation length algorithm. Output is clamped to 0…graphemes. |

HTML & Stream options

Additional options accepted by toHtml and createTransformStream:

| Option | Type | Default | Description | | --- | --- | --- | --- | | tag | string | "b" | Tag name wrapping the fixation prefix. | | className | string | undefined | Optional class attribute for the fixation element. | | restTag | string | undefined | Optional tag name wrapping the remainder of the word. Omit to leave as plain text. | | restClassName | string | undefined | Optional class attribute for the rest element. | | ignoreHtmlTags | boolean | true | When true, preserves existing HTML tags and entities. When false, treats input as plain text and escapes all HTML characters. | | skipTags | string[] | defaultSkipTags | Tag names whose contents are never modified. Defaults to ["code","pre","script","style","kbd","samp","textarea"]. |

DOM options

applyToElement accepts all options above plus:

  • skipSelector: CSS selector matching elements (and their subtrees) to skip entirely.

Exports

| Export | Description | | --- | --- | | tokenize(text, options?) | Returns structured tokens (words and separators) with computed fixation splits. | | toHtml(text, options?) | Transforms input string to formatted HTML. | | applyToElement(root, options?) | In-place DOM transformation; returns cleanup restore() function. | | createTransformStream(options?) | Web standard TransformStream<string, string> with toHtml semantics. | | fixationLength(word, graphemes, options) | Standalone fixation calculation helper for custom overrides. | | defaults | Frozen resolved default options (ResolvedSmoothOptions). | | defaultSkipTags | Default list of skipped HTML tag names. | | usesIntlSegmenter() | Returns true if the current runtime environment supports Intl.Segmenter. | | styles.css | Optional stylesheet for styling semantic <span> tags. |


Styling with CSS

The default output (<b>) works out of the box without extra stylesheets. If you prefer custom styling (e.g. semibold, custom color, or opacity), render semantic <span> tags and import styles.css:

import '@smooth-reading/core/styles.css';

toHtml(text, {
  tag: 'span',
  className: 'sr-fixation',
  restTag: 'span',
  restClassName: 'sr-rest',
});
.sr-fixation {
  font-weight: 700;
  color: var(--sr-fixation-color, inherit);
}

.sr-rest {
  opacity: var(--sr-rest-opacity, 1);
}

Many readers prefer a softer emphasis, such as font-weight: 600 combined with --sr-rest-opacity: 0.8.


Intl.Segmenter & Non-Latin scripts

When supported by the JavaScript runtime (modern browsers, Node.js 16+, Deno, Bun, and recent Hermes), Intl.Segmenter provides ICU dictionary-based word breaking and extended grapheme cluster counting. This enables accurate word boundary detection for scripts written without spaces, such as Chinese, Japanese, and Thai:

toHtml('我喜欢阅读', { locale: 'zh' }); // '<b>我</b><b>喜</b>欢<b>阅</b>读'
toHtml('สวัสดีครับ', { locale: 'th' }); // '<b>สวั</b>สดี<b>ครั</b>บ'

Regular expression fallback

In runtimes lacking Intl.Segmenter, the library falls back to the specification's regular expression scanner: [\p{L}\p{N}][\p{L}\p{N}\p{M}]*(?:['’][\p{L}\p{N}\p{M}]+)*

Grapheme clusters are approximated as a base character plus following combining marks (including ZWJ sequences and CR LF). The fallback produces identical output to Intl.Segmenter for every case in fixtures/common/.

The continuous run rule. Text in a script written without spaces — Han, Hiragana, Katakana, Hangul, Thai, Lao, Myanmar, Khmer, per the code-point table in SPEC §3 — is one word per unbroken run, split from the letters and digits of every other script. Combining marks following a run character stay in the run:

toHtml('iPhone手机');   // '<b>iPh</b>one<b>手</b>机'   — two words
toHtml('日本語OKです'); // '<b>日本</b>語<b>O</b>K<b>で</b>す' — three words

Accepted differences. The fallback is a predictable degradation, not a match for ICU. Without dictionaries it cannot split 手机很好用 into lexical words, and without the full UAX #29 table it differs on 3.14, 1,000, snake_case, ½ and X'0. SPEC §3 lists each case; fixtures/common/ deliberately contains only inputs on which both paths agree.

Runtime capability is detected dynamically on each call, so polyfills loaded after initialization are picked up automatically. Use usesIntlSegmenter() to inspect active segmentation mode.

HTML lexing

toHtml and createTransformStream share one lexer (SPEC §4.6). A < starts markup only before an ASCII letter, /, ! or ?, and the markup ends at the next > — except <!--/<![CDATA[, which run to -->/]]> and degrade to a declaration ending at the first > when unterminated. A tag name begins at the first ASCII letter (after an optional /, which may be followed by whitespace) and runs until whitespace, / or >, so <code.x> is named code.x and is not the code skip tag; a tag ending in / before the > is self-closing and never opens a skipped element.

toHtml('<code.x>hello</code.x>');    // '<code.x><b>hel</b>lo</code.x>'
toHtml('<code>a</ code>b');          // '<code>a</ code><b>b</b>'
toHtml('<code / >hello</code>');     // '<code / ><b>hel</b>lo</code>'

License

Apache-2.0. See LICENSE.