Typed parser combinators for TypeScript (Deno + Node). Build small parsers, then compose them into a grammar.
import { seq, str } from "jsr:@claudiu-ceia/combine@^0.6.0";Subpath imports are also supported:
import { recognizeAt } from "jsr:@claudiu-ceia/combine/nondeterministic";
import { createTracer } from "jsr:@claudiu-ceia/combine/perf";npm i @claudiu-ceia/combineNote: the npm package is ESM-only.
import { seq, str } from "@claudiu-ceia/combine";If you're in a CommonJS project, use a dynamic import:
(async () => {
const { seq, str } = await import("@claudiu-ceia/combine");
// Use seq and str here.
})();Parsers are plain functions: (ctx) => Result<T>. Use parseAll when the
parser must consume the complete input, or runParser for partial parsing and
custom start offsets.
import {
map,
optional,
parseAll,
regex,
seq,
space,
str,
trim,
} from "@claudiu-ceia/combine";
const name = trim(regex(/[^!]+/, "name"));
const hello = map(
seq(str("Hello,"), optional(space()), name, str("!")),
([, , who]) => who,
);
const result = parseAll(hello, "Hello, World!");
if (result.success) {
console.log(result.value); // "World"
} else {
console.error(result.expected, result.location);
}The library exports a lot of small pieces; these are the ones you'll likely reach for first:
- Parsers:
str,regex,digit,letter,int,double,space,eof - Composition:
seq,any,either,oneOf,many,many1,optional - Transform:
map,mapJoin,trim
If you like learning by examples, start with tests/.
When a parser needs to reference itself (directly or indirectly), wrap the
reference with lazy:
import { any, lazy, map, type Parser, seq, str } from "@claudiu-ceia/combine";
type Expr = { kind: "paren"; inner: Expr } | { kind: "lit"; value: string };
const lit: Parser<Expr> = map(str("x"), (value) => ({ kind: "lit", value }));
const paren: Parser<Expr> = map(
seq(str("("), lazy(() => expr), str(")")),
([, inner]) => ({ kind: "paren", inner }),
);
// A tiny recursive expression: x | (expr)
const expr: Parser<Expr> = any(lit, paren);If you're defining a larger mutually-recursive grammar, use defineLanguage
with a map of production output types. It provides fully typed sibling parsers
without making declaration order significant. See docs/guide.md.
For user-facing parsers, wrap important nodes with context(...), and commit to
branches with cut(...) (to avoid confusing backtracking). To print failures:
import { formatErrorStack } from "@claudiu-ceia/combine";
if (!result.success) console.error(formatErrorStack(result));Most combinators are deterministic: they return a single success or failure. For tokenizer-like use cases where you want multiple simultaneous matches at the same input position, use the nondeterministic/recognizer module:
import { recognizeAt } from "jsr:@claudiu-ceia/combine/nondeterministic";These combinators can return multiple successes; you must decide how (or whether) to advance the cursor.
tests/has the most coverage and real usage patternsexamples/contains small runnable snippets
If you want the deeper explanations (recursion patterns, defineLanguage, error
handling, cut vs context, and any vs furthest), see docs/guide.md.
The guide also covers the optional lexer layer (lexeme, symbol, keyword,
createLexer) for trivia/comments, plus the library's UTF-16 offset policy.
MIT © Claudiu Ceia