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

modern-ahocorasick

v3.3.0

Published

Unicode-aware Aho–Corasick multi-pattern matching with UTF-16 ranges, counting, lazy iteration, and replacement.

Readme

modern-ahocorasick

Native extensions add dynamic dictionaries, Unicode folding streams, character boundaries, tokenization/replacement streams, Node/Web adapters, filters, previews and a double-array backend. See the extension guide. No runtime dependencies are added.

Match many keywords in Unicode text with Aho–Corasick. Compile a dictionary once, then find, count or replace matches using ranges that work with JavaScript's slice().

npm CI MIT license

English · 简体中文

Documentation · Interactive workbench · API reference

Install and find matches

npm install modern-ahocorasick
# or: pnpm add modern-ahocorasick
import AhoCorasick from 'modern-ahocorasick'

const matcher = new AhoCorasick(['cat', '猫'])
const text = '😀cat和猫'

const matches = matcher.search(text)
// [
//   { pattern: 'cat', patternIndex: 0, start: 2, end: 5, data: undefined },
//   { pattern: '猫', patternIndex: 1, start: 6, end: 7, data: undefined },
// ]

matches.map(({ start, end }) => text.slice(start, end))
// ['cat', '猫']

Matching respects complete Unicode grapheme clusters, including emoji and combining characters. Results use original-text UTF-16 offsets: start is inclusive and end is exclusive. Reuse the matcher across texts; construct a new one to change its dictionary.

Choose a method

| You need | Method | Result | | ----------------------------------- | ----------------------------------------------- | ----------------------------------------------------- | | Check whether any keyword occurs | match(text) | Boolean; stops at the first hit | | Count every occurrence | count(text) | Number; includes overlaps and duplicate entries | | Count each dictionary entry | countByPattern(text) | Counts in input order; includes zeroes and duplicates | | Match incoming chunks | createStream(options?) | Stateful writes, EOF flush and cancellation | | Return one match without collecting | findFirst() / findAt() | Low-allocation first-hit queries | | Save or load a dictionary | serialize() / AhoCorasick.deserialize() | Versioned, validated compiled data | | Distribute a compiled artifact | serializeArtifact() / deserializeArtifact() | Profile-validated Worker/Serverless payload | | Collect matched ranges | search(text, options?) | Array of independent match objects | | Read matches as needed | iterate(text, options?) | Lazy iterator over match objects | | Replace non-overlapping matches | replace(text, replacement, options?) | New string |

count() avoids creating match objects. iterate() avoids collecting a result array and stops scanning when you stop consuming it. It accepts a complete string, not a stream of chunks; the iterator retains the text and dictionary.

import AhoCorasick from 'modern-ahocorasick'

const matcher = new AhoCorasick(['he', 'she', 'hers'])

matcher.match('ushers') // true
matcher.count('ushers') // 3 — 'she', 'he' and 'hers' overlap
matcher.countByPattern('ushers') // [1, 1, 1]

for (const hit of matcher.iterate('ushers')) {
  console.log(hit.pattern) // 'she'
  if (hit.pattern === 'she') {
    break
  }
}

For full signatures and validation behavior, see the API reference.

Additional matching tools

countByPattern(), whole-word options, streams, compiled persistence and the optional text adapter require v3.1.0 or later.

matcher.countByPattern('ushers') // [1, 1, 1]
new AhoCorasick(['cat']).match('concatenate', { wholeWord: true, locale: 'en' }) // false
const restored = AhoCorasick.deserialize(matcher.serialize())
const stream = restored.createStream()
const hits = [...stream.write('ush'), ...stream.write('ers'), ...stream.finish()]

For normalization or full Unicode case folding, import the separate TextMatcher constructor from modern-ahocorasick/text. It maps results back to original UTF-16 ranges, including expansions such as ß → ss. It also provides transformed createStream(), createTokenStream(), createReplaceStream(), serialize() and deserialize(). The default entry keeps exact matching and does not load the folding table.

See the API guide for word boundary rules, metadata codecs, stream buffering/cancellation and conversion costs. Stream tails are bounded explicitly and never silently truncated.

Select overlaps deliberately

search() and iterate() default to all. Use a leftmost strategy when ranges must not overlap, for example before highlighting text.

| Strategy | Selection | Order | | ------------------ | ---------------------------------------------------------------------- | --------------------------------------------------------------- | | all | Every occurrence, including overlaps and duplicate entries | End ascending; then pattern length descending; then input order | | leftmost-first | Earliest start; input order breaks same-start ties | Start ascending, non-overlapping | | leftmost-longest | Earliest start; longest pattern wins same-start ties, then input order | Start ascending, non-overlapping |

import AhoCorasick from 'modern-ahocorasick'

const matcher = new AhoCorasick(['a', 'ab', 'bc'])

matcher.search('abc', { strategy: 'leftmost-first' }).map(hit => hit.pattern)
// ['a', 'bc']
matcher.search('abc', { strategy: 'leftmost-longest' }).map(hit => hit.pattern)
// ['ab']

“Longest” breaks ties at the same start; it does not choose the longest match anywhere in the text. Adjacent matches are retained. With all, the first result is the earliest-ending match, which is not necessarily the leftmost one. Non-overlapping iteration may look ahead by the longest keyword's grapheme length.

Attach metadata and replace text

Dictionary entries can be strings or { pattern, data } records. Each input entry keeps its patternIndex, so duplicate keywords remain distinct. Metadata is caller-owned and retained by reference, not deep-cloned. Changing input records or returned match objects does not reconfigure the matcher.

import AhoCorasick from 'modern-ahocorasick'

const matcher = new AhoCorasick([
  { pattern: 'cat', data: { replacement: '猫' } },
  { pattern: 'dog', data: { replacement: '狗' } },
])

matcher.replace('cat and dog', hit => hit.data.replacement)
// '猫 and 狗'
matcher.replace('cat', '$&') // '$&' — string replacements are literal

replace() defaults to leftmost-longest and also accepts leftmost-first. It rejects all. A callback receives (match, originalSubstring) and must return a string. Replacement runs once over the original ranges; inserted text is not searched again. The library produces strings and ranges, not HTML; escape text according to your rendering framework when highlighting.

Unicode and compatibility

Matching is case-sensitive and does not normalize Unicode. é and e\u0301 are different patterns, and e does not match inside e\u0301. Grapheme boundaries follow the runtime's ICU/Unicode version. Read Unicode and indices for examples and coordinate conventions.

| Environment | Requirement | | ------------------ | -------------------------------------------------------- | | JavaScript runtime | ES2022 support and Intl.Segmenter; no bundled polyfill | | Modules | ESM default import or direct CommonJS require() | | TypeScript | 5.3+ for the bundled declarations |

const AhoCorasick = require('modern-ahocorasick')

const matcher = new AhoCorasick(['cat'])
matcher.match('cat') // true
import type { Match, PatternInput } from 'modern-ahocorasick'
import AhoCorasick from 'modern-ahocorasick'

const patterns: PatternInput<{ id: string }>[] = [
  { pattern: 'cat', data: { id: 'animal-cat' } },
]
const matcher = new AhoCorasick(patterns)
const matches: Match<{ id: string }>[] = matcher.search('cat')

An empty dictionary is valid. Empty keywords throw RangeError; invalid runtime arguments throw TypeError. Counts above Number.MAX_SAFE_INTEGER throw RangeError. Development Node.js requirements are separate from consumer runtime requirements.

Upgrade from v2

v3 is available on npm. search() now returns individual match objects with UTF-16 ranges instead of grouped tuples. The default constructor export, direct CommonJS require() and match(text): boolean remain supported.

Follow the v2 → v3 migration guide and consult the changelog.

Performance

Aho–Corasick compiles a reusable dictionary. For g input graphemes and z occurrences, all-match search takes O(g log(d + 1) + z) time, excluding runtime segmentation costs; d is the maximum transition degree. count() uses aggregate counts in O(g log(d + 1)) scan time; match() can stop early. Search retains its result array, while non-overlapping selection uses a candidate window bounded by the longest keyword. Replacement still allocates output text.

Performance depends on the dictionary, input, output density and runtime. See the reproducible benchmarks for workload-specific results, memory measurements and comparison limitations.

Contribute

See the contributing guide for local development, validation, releases and documentation deployment. Report a bug with a small reproduction and your runtime version.

License and credits

MIT. Originally forked from BrunoRB/ahocorasick, based on Aho and Corasick's “Efficient string matching: an aid to bibliographic search”. Modern library maintained by SonOfMagic.

The optional text adapter includes Unicode 17 case-folding data under the Unicode License V3.

Ranges, anchoring and compile statistics

Every whole-text query accepts start, end and anchored. Offsets are original UTF-16 positions in a half-open range; defaults are 0, text.length and false. Coordinates must be safe integers within the input, ordered and at original grapheme boundaries, otherwise a RangeError is thrown. A non-boolean anchored throws TypeError. An empty range has no matches. Anchoring accepts only hits beginning exactly at start.

import AhoCorasick from 'modern-ahocorasick'

const matcher = new AhoCorasick(['abc', 'bc'])
matcher.search('!abc!', { start: 2, end: 4, anchored: true })
// [{ pattern: 'bc', patternIndex: 1, start: 2, end: 4, data: undefined }]
matcher.replace('!abc!', 'X', { start: 2, end: 4 }) // '!aX!'
matcher.getStats() // frozen, cached scalar diagnostics

search, iterate, match, count, countByPattern, replace and tokenize share the contract, including /unicode, /fast, /unicode-fast and /text. Range filtering happens before overlap selection; whole-word and constructor boundary rules still inspect the complete input. Replacement and tokens retain text outside the range. Folding/normalization never changes the coordinate system. This is a semantic range filter; it does not promise work proportional only to the selected span. Streams reject these offline options (even anchored: false).

getStats() on the default and derived compiled constructors returns backend (compact or double-array), patternCount (duplicates included), stateCount (root included, unused DAT slots excluded), transitionCount (trie edges), alphabetSize, maxPatternUnits, unit (grapheme or folded-codepoint) and typedArrayBytes. Folded units are codepoints after Unicode folding, so ß has two units there. Byte counts include retained typed scan/auxiliary arrays, including unused allocated slots; they exclude strings, Maps, JS objects, temporary build allocations, and native ICU. They are not total heap usage. Values are compiled once; reads do not traverse tables. Compact deserialization recreates identical statistics; serializing /fast uses the existing portable compact format, so restored backend/storage statistics describe compact storage. /text is a mapping adapter, not a compiled-constructor subclass, and does not expose getStats().

The cross-language source assessment and measurements live in the repository's docs/research/ directory. This batch borrows range/diagnostic and contiguous-output design ideas; the default backend remains compact and no native runtime dependency is added.