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.2.6";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/combineimport { seq, str } from "@claudiu-ceia/combine";Parsers are plain functions: (ctx) => Result<T>. The context tracks where you
are in the input: { text, index }.
import {
eof,
map,
optional,
regex,
seq,
space,
str,
trim,
} from "@claudiu-ceia/combine";
const name = trim(regex(/[^!]+/, "name"));
const hello = map(
seq(str("Hello,"), optional(space()), name, str("!"), eof()),
([, , who]) => who,
);
const result = hello({ text: "Hello, World!", index: 0 });
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 createLanguage
(src/language.ts) to avoid worrying about declaration order.
If you want better type inference without writing an explicit language type, use
createLanguageThis (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, createLanguage, 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.
MIT © Claudiu Ceia