@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/anchorAPI
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 savedprocessText(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 <...'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>witharia-hidden - Layout safe — inline wrappers preserve flex/grid word flow
- Idempotent —
processElement→restoreElementleaves the DOM exactly as it was - Skips interactive regions — ignores
<code>,<pre>,<input>,<textarea>, editable regions, and ARIA navigation/menu roles
License
MIT © Clamly
