mrkeyoor.com_
Wed 23 Sept 02:50 UTC
npmUtilsupdated 21 Sept 2026

character-parser review

Character-parser 4.0.0 is a 4.6 KB minified scanner for locating the end of JavaScript-like expressions embedded in templates. It walks UTF-16 code units and tracks brackets, quoted strings, template literals, regular-expression literals, escapes, and comments, allowing a delimiter such as %> to be ignored while it appears inside those contexts. The package can preserve scanner state across chunks and returns substring boundaries rather than an AST. Version 4 added separate CommonJS and ESM entries through an exports map and ships TypeScript declarations. Its README states that it is not a JavaScript validator, and the code still decides whether / begins a regex with a lexical heuristic rather than a full grammar.

Verdict

Character-parser 4.0.0 installed in 0.7 seconds, left 1 MB on our box, and produced a 1.8 KB gzipped browser bundle with no audit findings. It is a good delimiter scanner for template tooling, but choose a real parser if any decision depends on JavaScript being syntactically valid.

We installed it

Lab card: what happened when we installed character-parserScreenshot of character-parser documentation
Install✓ · 0.7s1 package on disk · 1 MB
ImportESM import works · require() works · CommonJS package with exports map
Browser1.8 KBgzipped (4.6 KB minified), bundled with esbuild
TypesTypeScript types bundled
Known vulns00 critical · 0 high · 0 moderate · 0 low (npm audit)

Answers from our run

Does character-parser install cleanly?

Yes. In a fresh container with an empty cache, npm install character-parser finished in 0.7s, leaving 1 package and 1 MB on disk. npm audit reported no known vulnerabilities.

How much does character-parser add to a browser bundle?

1.8 KB gzipped (4.6 KB minified) when the whole package is bundled for the browser with esbuild. Importing only part of it is usually smaller.

Does character-parser work with both ESM and CommonJS?

Yes. Both import 'character-parser' and require('character-parser') worked in Node 22 in our run. The package is published as CommonJS with an exports map.

Does character-parser include TypeScript types?

Yes, type declarations ship inside the package, so no @types install is needed.

character-parser or balanced-match: which should you use?

balanced-match: Use it for balanced delimiters when JavaScript strings, comments, and regex literals do not need special treatment. Character-parser 4.0.0 installed in 0.7 seconds, left 1 MB on our box, and produced a 1.8 KB gzipped browser bundle with no audit findings.

When should you not use character-parser?

You need JavaScript validation or an AST. The README limits character-parser to scanning, and its slash handling cannot replace a grammar parser such as Acorn.

API stability4/5Version 4.0.0 exposes a short list: parse, parseUntil, parseChar, defaultState, State methods, token constants, and two lexical helpers. Its exports map provides both CommonJS and ESM entry points, and declarations describe the same main calls. The documented transition from v1 to v2 records the last broad API reshaping. One concrete mismatch remains: TypeScript lists an end option for parseUntil, while the implementation does not use it.
Docs4/5The README gives EJS-style and Jade-style delimiter examples, explains state reuse, documents every public function and State flag, lists coded errors, and says plainly that the scanner is not a validator. It also records the nesting behavior changed after v1. Readers still have to inspect the exports or declarations for current import forms, and the page does not warn about the ignored parseUntil.end option, stateful RegExp flags, or retained source history.
Maintenance3/5The GitHub repository is not archived, has 16 stars, reports 0 open issues and pull requests, and was last pushed on 2024-08-16. npm published 4.0.0 on 2021-12-23. A narrow scanner can remain useful without frequent releases, but JavaScript lexical rules continue to change, and the unresolved declaration mismatch gives maintainers a specific reason to cut another release rather than treating age alone as the concern.
Ecosystem3/5The npm endpoint counted 3,998,344 downloads in the latest completed week, far more reach than the repository's 16 stars suggest. Its zero-dependency install, dual module entry points, and plain range result make it easy for template compilers to embed. There is no plugin layer or syntax-extension system, and any consumer that needs validation, tokens, JSX, TypeScript, or an AST must pair it with a separate parser.

Use it if

  • A template engine must find its closing marker without stopping on the same text inside a string, comment, regex, or nested bracket.
  • You scan an expression in chunks and need to carry quote, escape, comment, and nesting state into the next call.
  • A compiler front end needs a zero-dependency structural scanner before handing extracted JavaScript to a real parser.
  • Both CommonJS and ESM consumers need the same small API with bundled TypeScript declarations.
Skip it if

Setup reality

We installed character-parser 4.0.0 in a fresh unprivileged Node 22 Bookworm sandbox. npm completed in 0.7 seconds and left 1 package using 1 MB on disk. The tarball unpacks to 52 KB and declares 0 direct dependencies and 0 peers. npm audit reported 0 known vulnerabilities. Both require() and ESM import worked through the exports map, and the package includes TypeScript declarations. Our browser build measured 4.6 KB minified and 1.8 KB gzipped.

There are no credentials, native builds, peers, or config files. ESM callers can use the default parse export or named functions. CommonJS callers receive an exports object, so destructure parse or call require('character-parser').parse; the package object itself is not the parser function. Version 4's main packaging change is this dual entry arrangement.

parse accepts an existing State and can continue across chunks. That State keeps the accumulated source and reversed history, so a long-lived character-by-character stream retains all scanned text. parseUntil starts with fresh state at options.start, excludes the delimiter from its result, and throws when it reaches the input end without a match. Starting midway through an already-open string or comment loses the missing context.

String delimiters and RegExp delimiters are both accepted. Avoid g and y flags on a RegExp delimiter because repeated test() calls mutate lastIndex. Bracket delimiters respect nesting unless ignoreNesting is true; ignoreLineComment lets a delimiter inside a line comment count. Errors use codes beginning CHARACTER_PARSER:, and the higher-level scan functions attach the failing index. Send the extracted substring to Acorn, Babel, TypeScript, or another parser before treating it as valid code.

Patterns

Inspect unfinished brackets inspect-nesting-state

import parse from 'character-parser';

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

Version 4 stores token-type constants in stack; do not assume every entry is a literal closing bracket.

Resume a scan with saved state continue-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());

Pass the returned State into the next call. It retains the concatenated source and reversed history as chunks arrive.

Scan a bounded source range scan-source-range

import { parse } from 'character-parser';

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

parse treats end as exclusive. Its options.end || src.length check means an end value of 0 falls back to the full input.

Stop at an EJS-style marker extract-template-expression

import { parseUntil } from 'character-parser';

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

The %> inside the quoted argument is ignored. end points at the matched delimiter, which is absent from src.

Begin after an opening marker start-after-opener

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

parseUntil creates fresh state at index 2. Starting inside an existing quote, comment, regex, or nesting frame produces the wrong context.

Match either delimiter spelling match-variable-delimiter

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

Do not add global or sticky flags. RegExp test() mutates lastIndex for those flags across the scanner's repeated calls.

Stop at the first closing bracket ignore-nested-brackets

const part = parseUntil('#[p= [1, 2][i]]', ']', {
  start: 2,
  ignoreNesting: true,
});

ignoreNesting: true can return an incomplete expression such as p= [1, 2; use it only when the outer language wants that first bracket.

Accept a marker inside a line comment count-comment-delimiter

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

The default ignores delimiters inside // comments until newline. This option changes line comments only; block-comment markers stay hidden.

Feed one UTF-16 unit at a time parse-one-code-unit

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

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

parseChar checks character.length === 1. Many emoji have length 2 in JavaScript and trigger CHAR_LENGTH_NOT_ONE.

Handle scanner errors by code handle-coded-errors

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

parse and parseUntil attach a source index. Direct parseChar errors carry a code but do not add that index for you.

Alternatives

PackageRegistryPick it when
balanced-matchnpmUse it for balanced delimiters when JavaScript strings, comments, and regex literals do not need special treatment.
acornnpmUse it when standards-focused tokenization, syntax validation, or an ESTree-compatible AST is the actual requirement.
esprimanpmUse it for a small, familiar JavaScript tokenizer and ESTree parser rather than substring boundaries.
@babel/parsernpmUse it when embedded expressions can contain JSX, TypeScript, decorators, or other Babel syntax plugins.

More utils guides

lru-cache · ajv · type-fest · p-limit · find-up · js-yaml · the whole shelf →

How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.