treffer
v0.5.0
Published
Tiny, bounded RFC 9485 I-Regexp matcher backed by a Thompson NFA.
Maintainers
Readme
treffer
A tiny, bounded RFC 9485 I-Regexp matcher for JavaScript. ~2KB min+gzip, one tiny runtime dependency.
Treffer is Dutch for a hit or match. It parses I-Regexp patterns into Thompson NFAs and evaluates all active states together. Matching never backtracks, so patterns such as (a+)+ have predictable runtime.
Install
npm install trefferNode.js 22 or newer, ESM only.
Usage
import { compile, match, search } from "treffer";
const isbn = compile("[0-9]{13}");
isbn.match("9780131103627"); // true
isbn.match("ISBN 9780131103627"); // false
isbn.search("ISBN 9780131103627"); // true
match("a|b", "a"); // true
search("\\p{Lu}+", "price: EUR"); // trueAPI
compile(pattern, options?)
Checks and compiles a pattern once. The returned object has two methods:
match(subject)tests the whole subject.search(subject)tests whether any substring matches.
const words = compile("[\\p{L}-]+");
words.match("naïve"); // true
words.search("42 naïve"); // truematch(pattern, subject, options?)
Compiles the pattern and tests the whole subject.
search(pattern, subject, options?)
Compiles the pattern and tests every possible start position in one forward pass.
Use compile() when a pattern will run more than once.
Syntax
Treffer is a checking RFC 9485 implementation. It supports:
- alternation, concatenation, and groups;
., character classes, ranges, and negated classes;- Unicode general categories such as
\p{Lu}and\P{N}; *,+,?, and{m,n}quantifiers.
JavaScript-only syntax such as \d, \w, lookarounds, backreferences, and lazy quantifiers is rejected. Use [0-9] instead of \d.
RFC 9485 treats ^ and $ as ordinary characters. Pass { anchors: true } to use them as subject anchors:
const line = compile("^item-[0-9]+$", { anchors: true });
line.search("item-42"); // true
line.search("x item-42"); // falseErrors and limits
Errors produced by Treffer keep their SyntaxError, TypeError, or RangeError class. Syntax and resource errors expose machine-readable properties:
code: a stable category;start: the zero-based offset into the pattern, for syntax errors;end: the exclusive offset into the pattern, for syntax errors;limit: the fixed resource limit, for resource errors;actual: the observed value, when it can be determined without weakening early rejection.
Spans are UTF-16 string offsets into the pattern you passed in, so pattern.slice(start, end) is the offending text. They cover the character that made the pattern invalid — the whole quantifier for an impossible bound like a{2,1}, the property name for an unknown \\p{...}, and an empty span at the end of the pattern when it ends early. A resource limit is not a position in the pattern, so those diagnostics carry limit and actual instead and have no span. Their message names the budget and the number it passed — group depth limit of 64 exceeded. Branch on code; the message is for a human reading a stack.
The codes are TREFFER_SYNTAX, TREFFER_MAX_PATTERN_SCALARS, TREFFER_MAX_GROUP_DEPTH, TREFFER_MAX_QUANTIFIER_DIGITS, TREFFER_MAX_REPETITIONS, TREFFER_MAX_NFA_STATES, TREFFER_MAX_SUBJECT_SCALARS, and TREFFER_MAX_TRANSITIONS. API TypeErrors have no code.
Errors thrown by caller-provided option accessors are host errors. Treffer passes them through unchanged and does not attach diagnostic fields.
Use isDiagnostic(error) when a host needs to distinguish those errors. It returns true only for errors created by the same Treffer module instance. Copying documented code, start, end, limit, and actual properties onto another error does not authenticate it. A diagnostic from another installed copy or module instance also returns false.
Relocating a diagnostic
An embedder that compiles patterns out of a larger document — a filter selector in a JSONPath query, a validation rule in a schema — reports the fault in its own coordinates, not the pattern's. relocate(diagnostic, { prefix, offset }) returns the copy to re-throw:
import { compile, isDiagnostic, relocate } from "treffer";
try {
compile(pattern);
} catch (error) {
if (!isDiagnostic(error)) throw error;
// where the pattern literal starts inside the surrounding query
throw relocate(error, { prefix: "$.a[?match(@.b, ...)]: ", offset: 16 });
}The copy keeps the original's class, prepends prefix to the message verbatim, moves the span when there is one, and carries every other field across. It is registered exactly as the original was, so it passes isDiagnostic. The original is left untouched. Relocation belongs here rather than in the embedder because authentication is by identity: a copy an embedder builds itself cannot be authenticated, and a field added to a diagnostic here would be a field the embedder's copy silently drops. Passing anything but a Treffer diagnostic throws a TypeError.
offset is right whenever the pattern was a verbatim slice of your text. It is wrong when your text was decoded first — a pattern read out of a JSON string literal, where \\d is three characters standing for two and every later offset slides. Pass span instead and name the region the pattern came from:
// The pattern reached Treffer after JSON unescaping, so no shift can reach the
// offending character. Point at the literal that carried it.
throw relocate(error, { prefix: "$.a[?match(@.b, ...)]: ", span: [16, 24] });span replaces the span outright and wins when both are given. Neither option adds a span to a diagnostic that had none.
The fixed safety limits are:
- 4,096 Unicode scalar values per pattern;
- 64 nested groups;
- 4,096 NFA states;
- 1,024 repetitions in a range quantifier;
- six digits per quantifier bound;
- one million Unicode scalar values per subject;
- one million state transitions per match.
Runtime is bounded by the subject length times the number of active NFA states. Character-class checks count toward the transition budget. Treffer validates Unicode scalar values and rejects lone surrogates.
Content Security Policy
Treffer parses patterns into data structures and closures. It generates no JavaScript source and works under a strict Content Security Policy.
Environments
Node.js 22 and newer, ESM only. Browser use is supported through a standards-based ESM bundler in environments supporting ES2024. Direct <script> globals, UMD, and CommonJS builds are not provided.
Shipping CommonJS alongside ESM would put two copies of the core in any process that mixed require and import. Each copy would have its own diagnostic identity, so isDiagnostic would return false across the seam.
Contributing
git clone https://github.com/getquario/treffer.git
cd treffer
npm install
npm run checknpm run check is the local gate. Conventions for this repo live in AGENTS.md.
License
Copyright 2026 Robin van der Vleuten
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
