mrkeyoor.com_
Sat 08 Aug 20:58 UTC
npmUtilsupdated 08 Aug 2026

character-parser

character-parser is a zero-dependency scanner for finding balanced JavaScript-like sections inside templates. It reads UTF-16 code units, tracks parentheses, braces, brackets, quoted strings, template literals, regular-expression literals, and comments, then helps you stop at a delimiter that occurs outside those contexts. It is useful inside template and preprocessor tooling. The README explicitly says it is not a JavaScript validator, and its regex-versus-division decision is heuristic rather than a full grammar parse.

Verdict

character-parser is a good narrow tool for template delimiters when a full parser would be unnecessary at the scanning stage. Do not mistake it for JavaScript validation, and work around the ignored parseUntil end option and heuristic slash handling.

API stability4/5The version 4 surface is small: parse, parseUntil, parseChar, State, defaultState, token constants, and two lexical helpers. The README clearly records the large version 1 to 2 transition, while current exports support ESM and CommonJS. Stability is tempered by a declaration and implementation mismatch where parseUntil advertises end but ignores it, plus behavior tied to heuristic source scanning.
Docs4/5The README gives concrete EJS-style and Jade-style delimiter examples, documents continuation state, every public function, token types, error codes, and migration from version 1. It is unusually candid that the library is not a validator. It misses import examples for the current dual-module package and does not mention the unused parseUntil end option, RegExp lastIndex risk, or retained source history.
Maintenance3/5The repository is not archived, its workflow and source were pushed in August 2024, and GitHub's combined issue and pull-request counter is 0. The published 4.0.0 release is older, from December 2021. A small scanner can be feature-complete, but the ignored option and evolving JavaScript lexical grammar would benefit from a newer release and explicit compatibility statement.
Ecosystem3/5character-parser recorded 4,178,039 downloads for the measured week, which reflects its use underneath established template tooling. The repository itself has 16 stars and no plugin ecosystem. It integrates easily through substring ranges and has no runtime dependencies, but users still need a separate parser for AST work or syntactic validation.

Use it if

  • You are building a template language and need to find a closing marker without stopping inside JavaScript strings, comments, or nested brackets
  • Input arrives in chunks and you need to carry bracket, quote, comment, and escape state into the next scan
  • A compiler utility needs a tiny ESM or CommonJS dependency with bundled TypeScript declarations and no runtime dependencies
  • You only need structural scanning and will send the extracted JavaScript to a real parser or compiler afterward
Skip it if

Setup reality

npm install character-parser adds no dependencies, native build, peers, credentials, or config. Version 4 exports both ESM and CommonJS targets and includes declarations. In ESM, import the default parse function or named exports such as parseUntil and parseChar. In CommonJS, use require('character-parser').parse or destructuring; require('character-parser') returns an exports object, not the parse function itself. The main choice is between parse and parseUntil. parse updates a State and can continue across chunks, but it only reports structural state. parseUntil always creates a fresh state, starts at options.start, and returns a half-open start/end range excluding the delimiter. It throws if no usable delimiter is found. Delimiters inside strings and comments are skipped, and bracket delimiters also respect nesting unless ignoreNesting is true. ignoreLineComment changes whether a delimiter inside a line comment can count. A RegExp delimiter is tested repeatedly against each remaining substring; avoid global or sticky flags because RegExp.lastIndex can make repeated test calls stateful. All syntax failures carry a code beginning CHARACTER_PARSER:, while parse and parseUntil also attach the failing index. The state stores the entire accumulated src plus reversed history, so feeding a very large stream one character at a time retains input and is not a constant-memory tokenizer. Extracted code still needs Acorn, Babel, TypeScript, or another real parser before you trust it as JavaScript.

Patterns

Inspect unclosed nestingparse-nesting-state

import parse from 'character-parser';

const state = parse('call(arg, {items: [1, 2]');
console.log(state.stack);

The stack contains token-type constants, not literal closing characters, despite older examples showing bracket characters.

Carry parser state across chunkscontinue-across-chunks

import { defaultState, parse } from 'character-parser';

let state = defaultState();
state = parse('render({title: "hel', state);
state = parse('lo"})', state);
console.log(state.isNesting()); // false

Reuse the returned State. It accumulates src and reversed history, so long-running streams retain all scanned content.

Scan only part of a source stringparse-source-range

import { parse } from 'character-parser';

const src = 'prefix fn({x: 1}) suffix';
const state = parse(src, undefined, { start: 7, end: 17 });

The end index is exclusive. Because the implementation uses options.end || src.length, an end value of 0 cannot request an empty scan.

Extract code before a template delimiterextract-until-delimiter

import { parseUntil } from 'character-parser';

const section = parseUntil('user.name.replace("%>", "") %> rest', '%>');
console.log(section.src);
console.log(section.start, section.end);

The delimiter inside the quoted string is ignored. end points to the first delimiter character and the delimiter is excluded from src.

Start after an opening template markerextract-from-offset

const input = '<% profile({name: "%>"}) %> tail';
const section = parseUntil(input, '%>', { start: 2 });
console.log(section.src);

parseUntil creates fresh scanner state at start. Do not start in the middle of an already-open string, comment, regex, or bracket.

Stop at one of several delimiter spellingsmatch-regex-delimiter

const section = parseUntil('value + 1 -%> tail', /-?%>/);

Avoid g and y flags. parseUntil repeatedly calls RegExp.test, and stateful lastIndex behavior can make global or sticky delimiters skip matches.

Stop without respecting nested bracketsignore-bracket-nesting

const section = parseUntil('#[p= [1, 2][i]]', ']', {
  start: 2,
  ignoreNesting: true,
});
console.log(section.src); // p= [1, 2

This restores the simpler version 1 behavior and can cut out syntactically incomplete code.

Let a delimiter inside a line comment countallow-line-comment-delimiter

const section = parseUntil('value // close %> ignored', '%>', {
  ignoreLineComment: true,
});

By default a line comment counts as nesting until newline, so its delimiter is ignored. Block comments are still ignored.

Drive the scanner one character at a timeparse-single-character

import { defaultState, parseChar } from 'character-parser';

const state = defaultState();
for (const codeUnit of 'fn([1])'.split('')) {
  parseChar(codeUnit, state);
}

parseChar requires string length 1 in UTF-16 units. Iterating an astral symbol with for...of produces a length-2 string and triggers CHAR_LENGTH_NOT_ONE.

Check whether scanning is inside text or commentsinspect-current-context

const state = parse('fn({message: `hello ${name}`');

console.log(state.current());
console.log(state.isString());
console.log(state.isComment());
console.log(state.isNesting());

Template interpolation adds a curly-bracket frame inside the template-quote frame, so current() reflects only the innermost context.

Handle coded syntax and delimiter failureshandle-parser-errors

try {
  parseUntil('fn([1, 2)', '%>');
} catch (err) {
  if (err.code?.startsWith('CHARACTER_PARSER:')) {
    console.error(err.code, err.index);
  } else {
    throw err;
  }
}

parse and parseUntil attach an index. parseChar errors have a code but no source index unless you add one.

Use the exported keyword and punctuation checksinspect-lexical-helpers

import { isKeyword, isPunctuator } from 'character-parser';

console.log(isKeyword('return')); // true
console.log(isPunctuator('/'));    // true

These helpers support the package's regex-versus-division heuristic; they are not a complete ECMAScript tokenizer.

Alternatives

PackageRegistryPick it when
balanced-matchnpmYou only need the range between balanced delimiters and do not need JavaScript string or comment awareness
acornnpmYou need a standards-focused JavaScript parser, tokens, or an AST rather than substring scanning
esprimanpmYou want a familiar ESTree parser and tokenizer for standard JavaScript source
@babel/parsernpmThe embedded language can include JSX, TypeScript, decorators, or other Babel syntax plugins