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

@slnknrr/str-im

v1.0.0

Published

Zero-allocation string engine: unique-character iterators, UTF-8 buffer inspection without decoding, run detection, Unicode classification. Numbers in the core, strings only at the boundary.

Readme

str-im

A zero-allocation string engine. Numbers in the core, strings only at the boundary.

str-im adds exactly the string operations JavaScript is missing — and nothing it already has. It never re-implements for..of, indexOf, or slice. It implements the things you keep hand-writing (with bugs): unique-character iterators, utf-8 buffer inspection without decoding, run detection, grapheme/width segmentation, mixed-script and homoglyph detection, bounded edit distance, single-pass multi-pattern search, lazy split, and entropy.

The core runs on code units and code points as numbers. A number below 2³¹ is a V8 Smi — it lives inside the pointer, so it costs zero allocations. Strings are created in exactly one place: the public boundary, when you ask for characters back. If you only ask a question ("how many distinct?", "is it mixed-script?", "does it fit in 120 bytes?"), no string is ever built.

import s from '@slnknrr/str-im';

s.ulen('mississippi');            // 4  — distinct chars, no strings allocated
[...s.char('mississippi')];       // ['m','i','s','p'] — unique, first-seen order
s.blen('héllo 👾');               // 11 — utf-8 byte length, computed WITHOUT encoding
s.bcut('hi 👾!', 7);              // 'hi 👾' — cut to 7 utf-8 bytes; keeps the whole emoji, never half
s.mix('раypal');                  // 1  — Latin + Cyrillic look-alikes: spoofing detected
s.near('kitten', 'sitting', 2);   // Infinity — "not within 2 edits", answered without the full matrix

What it is

  • A precision tool for text the standard library fumbles. utf-8 bytes, grapheme clusters, display width, scripts, confusables, edit distance, run-length structure, n-grams, entropy.
  • Lazy and bounded by default. Iterators stop when you stop pulling. max lets you ask "is there more than N?" and leave. near answers "≤ max edits?" in O(n·max) time and O(max) space — never the full O(n·m).
  • Source-agnostic. Every method works on a string, a utf-8 Uint8Array/Buffer, or an Array<number> of code points — through a one-line adapter you can extend to your own type.
  • Regex-free classification. Not one character class is tested with a regex (/\p{L}/u.test(c) forces a string allocation per character). Unicode properties are sorted Int32Array ranges, generated offline, queried by binary search, and materialized lazily on first use.
  • Pure ESM, dependency-free, sync. ~1900 lines, one file, immutable tables. Node ≥ 20.

What it is NOT

  • Not a faster String/RegExp. It does not try to out-run V8's compiled regex or Set at eager, single-pattern, whole-string work — that is a C++ engine and it wins, deservedly. str-im wins where the native idiom is forced to do work you didn't ask for. See Performance for the exact line.
  • Not a normalizer or transliterator. deburr/skel are for search and matching, not display: ö → o destroys meaning in German. Normalization is String.prototype.normalize; str-im calls it, it doesn't replace it.
  • Not a full TR39 confusables database. skel is NFKD + a built-in cross-script homoglyph minimum + whatever you add via strim.confuse(). It catches the classic а/a, о/o, fullwidth, and ligature spoofs; it is not the entire Unicode confusables file.
  • Not async, not streaming I/O. It operates on in-memory sources. There are no promises anywhere.
  • Not mutating. Inputs are never modified. You get a new string, a lazy iterator, or a number.

Install

npm install @slnknrr/str-im
import s from '@slnknrr/str-im';                       // ready-to-use singleton — no `new`
import { strim, _strim, NotFound, UNICODE } from '@slnknrr/str-im';

Requirements: Node ≥ 20, ESM only ("type": "module" or import). Tables track Unicode UNICODE (currently 17.0).


Core conventions

These are load-bearing. Learn them once; they apply everywhere.

Predicates return 0 | 1 | -1, not a boolean. 0 = no, 1 = yes, -1 = matched in full. -1 is truthy, so if (s.contains(a, b)) reads naturally — but "contains" and "equals" stay distinguishable without a second call.

s.starts('hello', 'he'); // 1
s.starts('hello', 'hello'); // -1  (equal, still truthy)
s.starts('hello', 'x'); // 0

Validation is synchronous. A generator's body doesn't run until the first next(), so a naive lazy API would throw later, in the consumer's stack. str-im primes every iterator: all argument checks happen at the call site. Bad input throws when you call, exactly like an ordinary function.

s.char('x', 0);          // throws RangeError NOW, not on first iteration

Errors are typed. TypeError = wrong type. RangeError = index/length out of range. NotFound = a lookup target genuinely absent (its own class, so a missing substring is catchable apart from a real bug).

Saturating counts return Infinity. When a count is capped (consc(str, max)), reaching the cap yields Infinity — an honest "at least this many", never confused with a real result that happens to equal the cap.

The *char family yields each character at most once, in order of first appearance. That is the entire point: iterating all characters is what for..of and str[i] already do.


Sources & adapters

Every reading method resolves an adapter once per call, then stays in a tight numeric loop. Three sources work out of the box:

| Source | Decoding | Example | |---|---|---| | string | utf-16 code units | s.len('café') | | Uint8Array / Buffer | utf-8 bytes | s.len(Buffer.from('café')) | | Array<number> | ready code points | s.len([99, 97, 102, 233]) |

Register your own once, process-wide, and the entire method family works with it:

strim.use(MyRope, {
  size: (r) => r.length,        // number of units
  at:   (r, i) => r.codeUnitAt(i), // unit at i, as a number
  dec:  0,                       // 0 = utf-16, 1 = utf-8 bytes, 2 = code points
});

Performance

str-im is fast where laziness, bounds, single-pass multi-pattern, and not materializing matter. It does not compete with compiled regex on regex's turf, and says so out loud.

Representative run (npm run bench, Node 24.16, one machine, median of 3 — your numbers will differ; the shape won't):

| Case | str-im | the native way | speedup | |---|--:|---|--:| | "more than 8 distinct chars?" | 1.2M/s | [...new Set(s)] | ~1200× | | first line of a big blob | 1.2M/s | s.split('\n')[0] | ~305× | | truncate to 120 utf-8 bytes | 240k/s | encode + slice + repair | ~27× | | find any of 400 needles (1 pass) | 850/s | 400× indexOf | ~2.4× | | "edit distance ≤ 2?" | 510k/s | full DP matrix | ~1.9× | | grapheme count | 4k/s | Intl.Segmenter | ~1.0× (par) |

Why these win, structurally: the early-exit iterator stops at 8 instead of scanning the string; lazy split yields the first piece instead of allocating the whole array; bcut streams the first ~120 bytes instead of encoding all 44 KB; Aho-Corasick scans once with cost flat in the needle count while indexOf pays one full scan per needle; banded near fills a diagonal stripe instead of the whole matrix. None of these is a micro-optimization — each is the native idiom being asked to do work you didn't request.

Run it yourself: npm run bench.


API

73 methods across 13 groups. str accepts any registered source. A target is a string to locate or a number treated as a position. max/n default sensibly. Iterator methods return lazy IterableIterators.

1 · Slices relative to a target — l = last, b = back

| Method | Returns | |---|---| | substr(str, target, offset=∞) | slice after first target, length offset | | lsubstr(str, target, offset=∞) | slice after last target | | substrb(str, target, offset=∞) | slice before first target | | lsubstrb(str, target, offset=∞) | slice before last target |

2 · Predicates — 0 / 1 / -1

| Method | Meaning | |---|---| | starts(str, target) | starts-with (numeric target ⇒ length compare) | | ends(str, target) | ends-with | | contains(str, target) | includes |

3 · Counters — no strings built

| Method | Counts | |---|---| | len(buf) | characters (code points) | | wlen(buf) | wide chars (≥ U+10000 / surrogate pairs) | | ulen(buf) | distinct characters | | blen(buf) | utf-8 byte length, without encoding | | glen(buf) | grapheme clusters (UAX #29) | | dlen(buf) | display width in columns |

4 · Dropping characters

| Method | Returns | |---|---| | omit(str, omits) | lazy chars of str, skipping omits | | omitn(str, omits) | indices of the dropped chars | | clear(str, omits) | new string without omits (same ref if nothing matched) |

5 · Unique-character iterators — each char once, first-seen order

| Method | Yields the unique… | |---|---| | char / wchar / rchar | code units / code points / code units from the end | | ichar | code points, case-folded (Aa) | | vchar / cchar | vowels / consonants (active alphabet) | | uchar / lchar | uppercase / lowercase letters | | nchar / pchar / schar | digits \p{Nd} / punctuation \p{P} / symbols \p{S} | | mchar / zchar | combining marks \p{M} / invisibles \p{Cc}∪\p{Cf} | | gchar | grapheme clusters |

All take (str, max=∞).

6 · Consecutive runs — aaabb structure

| Method | Returns | |---|---| | consc / rconsc | length of the first run (from start / end) | | cons / rcons | the character of each run | | consg / rconsg | the length of each run | | ucons / urcons | run characters, skipping already-seen | | flat(str, chars?, keep=1) | collapse repeats ('привееет' → 'привет') |

7 · Access by target

| Method | Returns | |---|---| | has(str, target) | char at index ±n (code units) or the char if present, else undefined | | whas(str, target) | same, indexed in code points |

8 · Bytes & boundaries — utf-8 without materializing a string

| Method | Returns | |---|---| | badn(buf) | index of the first invalid utf-8 byte, or -1 | | bom(buf) | byte length of a leading BOM, or 0 | | cut(src, max, ell?) | truncate to max code points, never splitting a pair | | bcut(src, max, ell?) | truncate to max utf-8 bytes | | wcut(src, max, ell?) | truncate to max grapheme clusters |

cut/bcut/wcut return the source's own type; ell (ellipsis) is charged against the same budget.

9 · Search & compare

| Method | Returns | |---|---| | find(str, target, {all}?) | lazy match indices; target = string or many (one Aho-Corasick pass) | | lfind / ifind | right-to-left / case-insensitive | | near(a, b, max=∞, {damerau}?) | edit distance, or Infinity if it exceeds max | | com(a, b) / comb(a, b) | length of common prefix / suffix (b = one string or many) |

10 · Segmentation & parsing

| Method | Returns | |---|---| | split(str, sep?, opts?) | lazy pieces; sep absent ⇒ newlines (\r\n\|\n\|\r as one) | | splitn(str, sep?, opts?) | piece boundaries, allocating nothing | | pair(str, i, opts?) / pairb | matching bracket index, nesting- and quote-aware | | ind(str) | { char, size, min, mixed } indentation analysis | | dedent(str) | strip the common indent | | esc(str, chars?, opts?) / unesc | escape / unescape a character set | | trim(str, chars?) / rtrim | trim a custom set (default \p{White_Space}) |

opts: { quote, quotes, esc, nest, limit, pairs } — quote/escape-aware splitting and matching (CSV, shell).

11 · Unicode analysis — security-grade

| Method | Returns | |---|---| | scr(str, max=∞) | unique script names | | mix(str) | mixed-script check: 0 one / 1 mixed / -1 none (ignores Common/Inherited) | | skel(str) | TR39-style skeleton — visually-equal strings collapse | | deburr(str) | strip diacritics (search/sort, not display) |

12 · Metrics

| Method | Returns | |---|---| | ent(str, base=2, n=1) | Shannon entropy (bits by default; n-grams for n>1) | | gram(str, n=2) / wgram | n-gram window start indices (code units / code points) | | hash(str, n=2) | rolling Rabin–Karp hash of each n-gram window |

13 · Second tier

| Method | Returns | |---|---| | glob(str, pattern, {nocase}?) | * ? [abc] [a-z] [!neg] match, O(1) memory, no compile | | style(str) | naming style: camel/pascal/snake/screaming/kebab/dot/mixed/… | | wrap(str, width, opts?) | width-aware line wrap (display columns, ANSI-safe) |

Statics & factory

| Symbol | Purpose | |---|---| | strim.use(ctor, adapter) | register a source type | | strim.alphabet(name, vowels?, consonants?) | register / fetch a vowel-consonant alphabet | | strim.confuse(map) | add confusable code points for skel | | _strim(overrides?) | build a configured instance (see below) | | UNICODE | Unicode version of the tables | | NotFound | error class for absent targets |


Extending & configuring

The default export is a singleton — you never write new. To specialize behavior (a custom terminal width, Turkish-i folding, tracing), pass overrides to _strim. An override may call super to wrap the original:

import { _strim } from '@slnknrr/str-im';

const traced = _strim({
  find(str, target, opts) {
    console.count('find');
    return super.find(str, target, opts);
  },
});

Every internal call dispatches through this, so replacing one method replaces it everywhere it is used — no registry, no wiring.

⚠️ Keep configurations few and long-lived

This is not a style note; it is a performance contract. Internal call sites are monomorphic and V8 inlines them to zero cost — as long as few distinct str-im classes exist in the process. Create your configuration once, at module load, and reuse it. _strim caches by the overrides object (a WeakMap), so the same object always yields the same class.

Spawn more than ~4 distinct configurations and internal call sites go megamorphic: the inline cache degrades to a hash lookup and every allocation guarantee in this library evaporates. If you find yourself calling _strim in a hot path or a loop, you are holding it wrong.


Design notes

  • Priming. Public "generators" are ordinary methods returning an already-primed generator: they validate, cross one dummy yield barrier, and hand the generator back positioned at the first real value. One idle yield per call, zero per element — the price of synchronous errors.
  • Indices, not tuples. Internal generators yield a single boundary number, never a [value, length] pair — the value is the element at that index, the length is the next boundary minus this one. No per-element allocation, ever. The n-suffixed public methods (omitn, splitn) expose those raw boundaries.
  • Tables, offline. \p{…} is invoked ~30 million times once, at build time (npm run tables), and compiled to base64-VLQ delta-coded ranges. At runtime only a binary search over a lazily-materialized Int32Array remains. Ask about punctuation and only the punctuation table comes into existence.

Scripts

| Command | Does | |---|---| | npm test | behavioral suite (node --test, zero dependencies) | | npm run types | type-check the shipped declarations (tsc --noEmit) | | npm run bench | the benchmarks above | | npm run tables | regenerate src/tables.js from the engine's own ICU |


License

MIT + restrictions. The MIT License, with added limits — chiefly no use in AI/ML training and mandatory source-attribution headers. Full terms: Slinkin Restricted License 1.0. © 2026 Yury Slinkin.