@a5i/eregex
v0.1.5
Published
Node.js bindings for eregex, an advanced regular expression engine inspired by mrab-regex
Downloads
186
Maintainers
Readme
@a5i/eregex (Node.js bindings)
Native Node.js bindings for eregex —
an advanced regular expression engine for Rust inspired by mrab-regex (the
Python regex module).
This package exposes eregex's full API to JavaScript / TypeScript via napi-rs. All matching logic runs in compiled Rust; the JavaScript layer is a thin adapter.
Features
- Named groups, duplicate group names, repeated captures
- Greedy / lazy / possessive quantifiers, atomic groups
(?>...) - Variable-length lookbehind / lookahead
- Inline scoped flags
(?i),(?i-m:...) - Backreferences
\1,\g<name>,(?P=name) - Partial / end-anchored matching (
findPartial) find,matchAtStart(Pythonre.match),fullMatch(re.fullmatch)replace,replaceAllwith$1/${name}/$$templatessplit,escape, and more
Build
The native addon is built locally from the Rust core:
cd crates/eregex-node
npm install
npm run build # release build → index.js + eregex.<platform>.node
# or: npm run build:debugindex.js, index.d.ts and the .node binary are generated by the build;
they are not checked in.
Quick start
const { Regex, IGNORECASE, parseFlags } = require('@a5i/eregex');
const re = new Regex(String.raw`(\w+)\s+(\w+)`);
const m = re.find('hello world');
console.log(m.matched); // 'hello world'
console.log(m.group(1)); // 'hello'
console.log(m.group(2)); // 'world'
// Flags: pass a bitset of the exported constants, or parse a string.
new Regex('hello', IGNORECASE).isMatch('HELLO'); // true
new Regex('hello', parseFlags('i')).isMatch('HELLO'); // true
// Repeated captures (signature mrab-regex feature).
new Regex(String.raw`(\w)+`).find('abc').captures(1); // ['a', 'b', 'c']
// Replace with named groups.
new Regex(String.raw`(?P<a>\d)(?P<b>\d)`).replaceAll('12 34', '${b}${a}'); // '21 43'Regex
class Regex {
constructor(pattern: string, flags?: number)
get pattern(): string
get flags(): number // resolved flags (defaults UNICODE + VERSION1 are added)
get captureCount(): number // capturing groups (group 0 excluded)
groupNames(): string[]
groupIndex(name: string): number | null
isMatch(haystack: string): boolean
find(haystack: string): Match | null
findAt(haystack: string, start: number): Match | null
matchAtStart(haystack: string): Match | null // like re.match
fullMatch(haystack: string): Match | null // like re.fullmatch
findAll(haystack: string): Match[]
findPartial(haystack: string): PartialMatch | null
replace(haystack: string, repl: string): string
replaceAll(haystack: string, repl: string): string
split(haystack: string): string[]
dump(): string // parsed AST (debug aid)
}flags is a bitwise OR of the exported constants: IGNORECASE, MULTILINE,
DOTALL, UNICODE, ASCII, VERBOSE, FULLCASE, WORD, LOCALE,
VERSION0, VERSION1. parseFlags("ims") parses a flag string for
RegExp-familiar ergonomics.
Match
class Match {
get matched(): string // whole match (group 0)
get input(): string // original haystack
get start(): number // byte offset
get end(): number
get span(): { start: number; end: number }
get captureCount(): number
get groups(): (string | null)[] // current text, group 0 first
get namedGroups(): Record<string, string>
get allCaptures(): (string | null)[][] // repeated-capture history
get capturesDict(): Record<string, (string | null)[]>
group(index: number): string | null
namedGroup(name: string): string | null
captures(index: number): (string | null)[]
capturesByName(name: string): (string | null)[]
spanOf(index: number): { start: number; end: number } | null
}All offsets are byte offsets (UTF-8), matching Python's re and the Rust
core. null is returned for groups that did not participate.
Partial matching
findPartial is an end-anchored search: it asks whether the haystack,
taken up to its end, could be the start of a full match. Use it when
validating input as the user types, parsing an incomplete stream, or asking
"could more input turn this into a match?"
It returns one of three outcomes:
| result | meaning |
| ------------------------ | ------------------------------------------------------------- |
| PartialMatch (partial) | a valid prefix so far — more input could complete it |
| PartialMatch (full) | the input already fully matches (and consumes it to its end) |
| null | a hard mismatch: no possible continuation could match |
Each capturing group in a partial match is itself in one of three states,
reported by groupState(i): 'matched' (fully matched), 'partial' (entered
but not yet completed), or 'none' (never participated — group(i) is null).
class PartialMatch {
get status(): 'full' | 'partial'
get isFull(): boolean
get isPartial(): boolean
get matched(): string
get start(): number // byte offset where the match starts
get end(): number // byte offset of the input end (always haystack.length)
get captureCount(): number
group(index: number): string | null
namedGroup(name: string): string | null
groupState(index: number): 'matched' | 'partial' | 'none'
}Incremental typing graduates partial → full → null:
const re = new Regex(String.raw`abc`);
re.findPartial(''); // null (nothing started yet)
re.findPartial('a'); // partial .status === 'partial'
re.findPartial('ab'); // partial
re.findPartial('abc'); // full .isFull === true
re.findPartial('abcd'); // null ('d' rules out any continuation)Group states as a match fills in. With token=([a-z]+)([0-9]+)([A-Z]+):
const re = new Regex(String.raw`token=([a-z]+)([0-9]+)([A-Z]+)`);
const p = re.findPartial('x token=abc');
p.isPartial; // true
p.matched; // 'token=abc'
p.start; // 2 (byte offset of the match)
p.end; // 11 (end of input — always, since end-anchored)
p.captureCount; // 3
p.group(1); // 'abc' p.groupState(1); // 'matched'
p.group(2); // '' p.groupState(2); // 'partial' (entered, empty so far)
p.group(3); // null p.groupState(3); // 'none' (never entered)
re.findPartial('token=abc123XYZ'); // group 3 -> 'matched', status 'full'
re.findPartial('x token=abc!'); // null ('!' rules out any continuation)Named groups work the same way:
const re = new Regex(String.raw`token=(?P<word>[a-z]+)(?P<num>[0-9]+)`);
const p = re.findPartial('token=ab');
p.namedGroup('word'); // 'ab' (matched)
p.namedGroup('num'); // '' (partial — empty so far)Module-level helpers
escape(s: string): string
escapeSpecialOnly(s: string): string
escapeLiteralSpaces(s: string): string
isMatch(pattern: string, haystack: string): boolean // compiles pattern once
parseFlags(flagStr: string): numberTesting
npm test # runs test/smoke.jsLayout
This is one half of eregex's binding story. The same Rust core (eregex)
also ships Python bindings via pyo3 + maturin. See the project root for
the core crate and its feature matrix.
License
Apache-2.0, matching the upstream mrab-regex project.
