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

@clamly/anchor

v0.2.0

Published

Dependency-free text fixation and DOM processing for Clamly Anchor.

Readme

@clamly/anchor

Zero-dependency TypeScript engine for saccadic text fixation and reading analytics.

@clamly/anchor creates artificial visual fixation points on words — inspired by saccadic eye movement research — without altering source text, screen-reader semantics, or page layouts. It is the core engine powering the Clamly Anchor Chrome extension and web Reader Studio.


Installation

npm install @clamly/anchor
# or
pnpm add @clamly/anchor
# or
yarn add @clamly/anchor

API

splitText(text, options?)

Splits a plain-text string into TextSegment[] — each segment has a value and a bold flag indicating whether it should be rendered as a fixation anchor.

import { splitText } from "@clamly/anchor";

const segments = splitText("Reading dense material asks a lot of our attention.", {
  fixationStrength: 45,  // % of word used as anchor (default: 45)
  minimumWordLength: 1,  // min chars to be eligible (default: 1)
  cadence: "saccade",   // "all" | "alternating" | "saccade" (default: "all")
  skipWords: ["API", "OAuth"], // custom case-insensitive dictionary
  shouldAnchorWord: ({ index }) => index < 100 // optional per-word policy
});

// Render segments yourself:
segments.forEach(({ value, bold }) => {
  const el = document.createElement(bold ? "b" : "span");
  el.textContent = value;
  container.appendChild(el);
});

calculateReadingMetrics(text, options?)

Returns live reading statistics for a given block of text.

import { calculateReadingMetrics } from "@clamly/anchor";

const metrics = calculateReadingMetrics(text, { cadence: "saccade" });

console.log(metrics.wordCount);                  // total words
console.log(metrics.fixationCount);              // words that got anchors
console.log(metrics.fixationDensityPercentage);  // 0–100 %
console.log(metrics.estimatedWordsPerMinute);    // baseline WPM
console.log(metrics.estimatedSecondsSaved);      // estimated time saved

processText(text, options?)

Creates safe, ready-to-render HTML without accessing document, making it useful in Node.js, static-site generators, and server-rendered templates. Input text is HTML-escaped; only generated fixation prefixes use <b class="clamly-anchor-bold">.

import { processText } from "@clamly/anchor";

const html = processText("Read <safely>", { cadence: "saccade" });
// '<b class="clamly-anchor-bold">Re</b>ad &lt;...'

processElement(element, options?)

Transforms all eligible text nodes inside a DOM element in-place, wrapping fixation prefixes in presentational <b> tags marked with Clamly Anchor's generated-data attributes.

import { processElement } from "@clamly/anchor";

const article = document.querySelector("article")!;
processElement(article, { fixationStrength: 45, cadence: "saccade" });

Use skipTags and skipRoles to add protected regions. They are case-insensitive and supplement—not replace—the built-in safety list.

processElement(article, {
  skipTags: ["aside", "figure"],
  skipRoles: ["status"],
  onNodeProcessed: ({ originalText, wrapper, fixationCount }) => {
    console.log(`Anchored ${fixationCount} words in: ${originalText}`);
    wrapper.dataset.processedBy = "my-reader";
  }
});

Screen-reader safe: Uses <b> (presentational) not <strong> (semantic), so assistive technologies are not affected.


restoreElement(element)

Fully reverses a processElement call — removes all injected wrappers and rejoins split text nodes, leaving zero orphaned attributes.

import { restoreElement } from "@clamly/anchor";

restoreElement(article);

Options

| Option | Type | Default | Description | |---|---|---|---| | fixationStrength | number | 45 | Percentage of each word used as the visual anchor (40–50 is optimal) | | minimumWordLength | number | 1 | Words shorter than this are left untouched | | cadence | ReadingCadence | "all" | Fixation rhythm: "all", "alternating", or "saccade" | | skipWords | readonly string[] | [] | Extra words to leave unanchored; matched case-insensitively | | shouldAnchorWord | (context: WordAnchorContext) => boolean | — | Return false to skip an otherwise eligible word |

processElement also accepts skipTags, skipRoles, and onNodeProcessed. The callback receives originalText, the generated wrapper, and fixationCount.

Options are validated at runtime as well as by TypeScript: fixationStrength must be a finite number from 0 through 100, minimumWordLength a positive integer, and cadence one of the listed values. Invalid values throw TypeError; they are not silently coerced. For untyped configuration, use isAnchorOptions(value) or assertValidAnchorOptions(value) before processing.

shouldAnchorWord receives the original word, a lowercase normalizedWord, and a zero-based index. It is called only for words that pass the built-in length, cadence, and dictionary checks; return false to apply your own additional exclusion rule.

Cadence modes

| Mode | Description | |---|---| | "all" | Every eligible word gets an anchor — maximum fixation density | | "alternating" | Every other word gets an anchor — airy, rhythm-driven flow | | "saccade" | Content-rich words anchored; short stop words (the, in, of) left soft — mimics natural saccadic eye movement |


TypeScript

Full type definitions are included. Key exported types:

import type {
  AnchorOptions,
  ProcessElementOptions,
  ProcessedTextNode,
  ReadingCadence,
  ReadingMetrics,
  TextSegment,
  WordAnchorContext,
  WordParts,
} from "@clamly/anchor";

Design principles

  • Zero dependencies — no runtime dependencies whatsoever
  • Screen-reader safe — never uses <strong>; uses presentational <b> with aria-hidden
  • Layout safe — inline wrappers preserve flex/grid word flow
  • IdempotentprocessElementrestoreElement leaves the DOM exactly as it was
  • Skips interactive regions — ignores <code>, <pre>, <input>, <textarea>, editable regions, and ARIA navigation/menu roles

License

MIT © Clamly