parse-statements
parse-statements is a dependency-free source-text scanner that finds ordered sequences of regular-expression tokens and reports the token offsets through callbacks. You describe statement shapes and optional comment delimiters, create a parser once, then let it mutate a context object as it walks a string. It can recognize lightweight constructs across different languages, but it is not a grammar parser: it does not build an AST, understand nesting, or validate a language beyond the token sequences you supply.
A focused tool for token-sequence extraction when a full parser would be excessive. Do not mistake its language-neutral input for language understanding; nested or ambiguous syntax deserves a real grammar parser.
Use it if
- You need to extract a few predictable statement shapes from source text without installing a full language parser
- Your format has comments between meaningful tokens and you need their exact source offsets
- You want one configured scanner reused against many strings while accumulating results in a caller-owned context
- You need both ESM and CommonJS entry points plus bundled TypeScript declarations
- You need a correct JavaScript, TypeScript, SQL, or CSS syntax tree: this package matches regular-expression token sequences and does not model grammar or nesting
- Statements can overlap or a later match should supersede an earlier one; the README explicitly says statements cannot overlap and matching advances through the source
- You need best-match or backtracking behavior: each statement is an ordered token chain, so an early plausible token can lead to the incomplete-statement callback instead of a smarter parse
- Your runtime lacks ES2018 named capture groups or is Node.js older than 10, both listed as compatibility requirements
- You want a large user-facing ecosystem or extensive troubleshooting material: the repository has 12 stars, and documentation is one dense README centered on a single long example
Setup reality
There are no runtime dependencies, native builds, credentials, or config files. Install the package, import createParseFunction, and define a mutable context plus statement descriptors. Most of the setup cost is escaping and parser design. Every token is a string passed to RegExp, not a RegExp object, and the default flags are gmu; a backslash must therefore survive both JavaScript string parsing and RegExp construction, so word boundaries look like '\b' in source. The scanner only knows comments you describe with opening and closing token pairs. Set canIncludeComments on each statement that permits them, and use shouldSearchBeforeComments when a statement opener and comment opener can start at the same position. Callbacks receive numeric start and end offsets, with end exclusive, so use source.slice(start, end). An incomplete statement calls its local onError with only the tokens found so far. An unclosed comment calls the comment error handler and stops parsing. A statement callback may return a replacement end index, but if it moves past comments you must parse those comments yourself, as the README warns. Custom regexpFlags replace the defaults rather than extending them, so omitting g can break the global scanning design. TypeScript can type a fixed token count through OnParse<Context, N>, but heterogeneous descriptors often require a broader OnParse cast, as the official example demonstrates.
Patterns
Extract a simple begin-to-end statementfind-token-sequence
import { createParseFunction } from 'parse-statements';
type Context = { values: string[] };
const parse = createParseFunction<Context>({
statements: [{
tokens: ['^BEGIN\b', '\bEND$'],
onParse: ({ values }, source, first, last) => {
values.push(source.slice(first.end, last.start).trim());
},
}],
});
const context: Context = { values: [] };
parse(context, 'BEGIN useful text END');Token strings are passed to RegExp, so backslashes such as the one in \b must be escaped in JavaScript source.
Slice the complete matched statementcapture-whole-statement
const parse = createParseFunction<{ statements: string[] }>({
statements: [{
tokens: ['^include\b', ';'],
onParse: (context, source, first, last) => {
context.statements.push(source.slice(first.start, last.end));
},
}],
});start is inclusive and end is exclusive, which matches String.prototype.slice without adjustment.
Record an incomplete token sequencereport-incomplete-statement
const parse = createParseFunction<{ errors: string[] }>({
statements: [{
tokens: ['^import\b', '\bfrom\b', '$\n?'],
onError: (context, source, ...tokens) => {
const first = tokens[0]!;
const last = tokens[tokens.length - 1]!;
context.errors.push(source.slice(first.start, last.end));
},
}],
});onError receives only the statement tokens found before the sequence became incomplete.
Collect line commentsparse-line-comments
const parse = createParseFunction<{ comments: string[] }>({
comments: [{
tokens: ['\/\/', '$\n?'],
onParse: (context, source, open, close) => {
context.comments.push(source.slice(open.end, close.start));
},
}],
});The closing token is a regular expression too; the README uses $\n? to end a line comment.
Collect block comments and reject unclosed onesparse-block-comments
const parse = createParseFunction<{ comments: string[] }>({
comments: [{
tokens: ['\/\*', '\*\/'],
onParse: (context, source, open, close) => {
context.comments.push(source.slice(open.end, close.start));
},
onError: (_context, source, open) => {
throw new SyntaxError(`Unclosed comment at ${open.start}: ${source.slice(open.start)}`);
},
}],
});An unclosed comment ends the parse after the comment error callback runs.
Allow comments between statement tokensallow-comments-inside
const parse = createParseFunction<{ imports: string[] }>({
comments: [{ tokens: ['\/\*', '\*\/'] }],
statements: [{
canIncludeComments: true,
shouldSearchBeforeComments: true,
tokens: ['^import\b', '\bfrom\b', '$\n?'],
onParse: (context, source, first, _from, last) => {
context.imports.push(source.slice(first.start, last.end));
},
}],
});Comments are attached to the preceding parsed token; shouldSearchBeforeComments controls precedence when openers compete.
Read comments attached between tokensread-attached-comments
const onParse: OnParse<Context, 3> = (context, source, first, middle, last) => {
for (const [open, close] of first.comments ?? []) {
context.comments.push(source.slice(open.end, close.start));
}
context.values.push(source.slice(first.end, last.start));
};Only non-final tokens can have a comments property because comments are stored between a token and its successor.
Surface parser-internal mismatcheshandle-global-error
const parse = createParseFunction<{ errors: string[] }>({
onError: (context, _source, message, index) => {
context.errors.push(`${index}: ${message}`);
},
statements,
comments,
});This global callback is distinct from a statement's incomplete-sequence callback and a comment's missing-close callback.
Continue scanning from a custom indexoverride-statement-end
const parse = createParseFunction<Context>({
statements: [{
tokens: ['^section\b', '\{'],
onParse: (context, source, first, openBrace) => {
const close = findMatchingBrace(source, openBrace.end);
context.sections.push(source.slice(first.start, close + 1));
return close + 1;
},
}],
});If the custom range crosses comments, the callback must account for them because the scanner resumes only at the returned index.
Reuse one parser with separate contextsreuse-configured-parser
const parseDirectives = createParseFunction<Context>(options);
const first: Context = { directives: [], errors: [] };
const second: Context = { directives: [], errors: [] };
parseDirectives(first, sourceA);
parseDirectives(second, sourceB);The parser mutates the context passed to each call and does not create or return a result object for you.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| esprima | npm | Use it when the input is JavaScript and you need a standards-aware ESTree syntax tree |
| @babel/parser | npm | Use it for JavaScript or TypeScript syntax, proposals, JSX, and parser error recovery |
| meriyah | npm | Use it for a fast ECMAScript parser when a real AST matters more than a tiny generic scanner |