Skip to content

Commit c0299dc

Browse files
committed
Reworked how comments are handled in JSONC.
1 parent 2f2d35b commit c0299dc

19 files changed

Lines changed: 390 additions & 124 deletions

AGENTS.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ stream-json/
109109
- **Keep `.js` and `.d.ts` files in sync** for all modules under `src/`.
110110
- **Token-based architecture.** The parser produces a stream of `{name, value}` tokens. All filters, streamers, and utilities operate on this token protocol.
111111
- **Backpressure must be handled correctly.** All stream components rely on Node.js stream infrastructure via `stream-chain`.
112-
- **Intended input is data the user owns or trusts** (dumps, exports, logs). The library is not designed for hostile input; docs say so, and code changes are not hardened against adversarial JSON beyond `JSON.parse` parity (`__proto__` becomes an own property) and the filters' `maxDepth` guard.
112+
- **Intended input is data the user owns or trusts** (dumps, exports, logs). The library is not designed for hostile input; docs say so, and code changes are not hardened against adversarial JSON or JSONC beyond `JSON.parse` parity (`__proto__` becomes an own property) and the filters' `maxDepth` guard.
113113

114114
## Architecture
115115

@@ -136,7 +136,7 @@ stream-json/
136136
- `withParser(fn, options)` creates a `gen(parser(options), fn(options))` pipeline — the most common pattern.
137137
- Most components export `.withParser(options)` and `.withParserAsStream(options)` static methods.
138138
- **JSONL** (**deprecated — slated for removal in a future major**): `jsonl/parser.js` and `jsonl/stringer.js` are thin re-exports of stream-chain's JSONL (the parser API was absorbed into stream-chain). The Node/Web wrappers delegate `.asStream`/`.asWebStream` to stream-chain's bundled `stream-chain/node/jsonl/*` and `stream-chain/web/jsonl/*` factories. Use stream-chain's JSONL directly. Rationale: stream-json is a JSON _token_ library; JSONL yields whole objects per line and belongs in stream-chain with the other substrate components.
139-
- **JSONC**: `jsonc/parser.js`, `jsonc/stringer.js`, and `jsonc/verifier.js` for JSON with Comments. Extend the standard parser/stringer/verifier (same `charCodeAt` tokenizer/validator) with `whitespace`/`comment`/`comma` tokens, trailing comma support, and `streamWhitespace`/`streamComments`/`streamCommas` (parser) plus `useCommas` (stringer) options. `streamCommas` + `useCommas` give byte-faithful comma round-trips (incl. trailing commas) for streaming edits; both default off. Raw inner exports: `jsoncParser`, `jsoncVerifier`.
139+
- **JSONC**: `jsonc/parser.js`, `jsonc/stringer.js`, and `jsonc/verifier.js` for JSON with Comments. Extend the standard parser/stringer/verifier (same `charCodeAt` tokenizer/validator) with `whitespace` / comment / `comma` tokens, trailing comma support, and `streamWhitespace`/`streamComments`/`packComments`/`streamCommas` (parser) plus `useCommentValues`/`useCommas` (stringer) options. Comments mirror strings (`startComment` / `commentChunk` / `endComment`, packed `commentValue`) and their scan resumes across chunks — never rescan an accumulated comment from its start. `streamCommas` + `useCommas` give byte-faithful comma round-trips (incl. trailing commas) for streaming edits; both default off. Raw inner exports: `jsoncParser`, `jsoncVerifier`.
140140
- **File I/O (Node-only)** (`src/file/`, since 3.3.0): `parseFile()` is an input-edge stage that turns a file path into a token stream (`gen(asyncBlockReader, jsonParser)`); `stringerToFile(path)` is the symmetric output-edge sink (`gen(stringer, asyncBlockWriter)`); `verifyFile(path)` is a standalone async validator returning `Promise<void>`. JSONC variants under `src/file/jsonc/`. NOT mirrored in `core/` or `web/` because they use `node:fs/promises`. Compose with `pipe(...)` (one-shot driver with auto-flush — `gen(...)` alone doesn't flush, so `stringerToFile` wouldn't close the file) and `drain(asyncGen)` (returns the last yielded value or `undefined`). Both helpers live in `core/utils/` (web-safe, no Node deps).
141141
- **Substrate split**: `src/core/` holds the pure substrate-agnostic factories (no Node-stream imports — checked by `tests/node/test-browser-safe.js` which scans `.d.ts` for `node:*` imports and `extends DuplexOptions`). `src/` (Node entry) attaches `.asStream` (Node Duplex) and `.asWebStream` (Web `{readable, writable}` pair). `src/web/` attaches only `.asWebStream` and `.withParserAsWebStream`, with no Node-stream imports — safe for browser bundles. The `chain` from `stream-chain` (Node) or `stream-chain/web` (Web) auto-wraps the pure flushables on both substrates, so user-facing pipeline code is identical.
142142

ARCHITECTURE.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -194,8 +194,8 @@ The item shape is the exported `KeyedValue<K, T>` type (`core/streamers/stream-b
194194

195195
### JSONC support
196196

197-
- `jsonc/parser.js` — the `charCodeAt` tokenizer of `parser.js` extended with `//` and `/* */` comments, trailing commas, and optional `whitespace`/`comment`/`comma` tokens (raw inner export `jsoncParser`). Options: `streamWhitespace` (default: true), `streamComments` (default: true), `streamCommas` (default: false — emit a valueless `comma` token at every comma's position; the comma byte is already buffered, so emission needs no lookahead and is fully resumable). All standard parser options are supported.
198-
- `jsonc/stringer.js` — fork of `stringer.js` that passes `whitespace` and `comment` tokens through verbatim. Option `useCommas` (default: false) renders streamed `comma` tokens as `,` (a separator is still auto-inserted before a value when no `comma` token preceded it, so output stays valid even if commas were dropped upstream) — `streamCommas` + `useCommas` give byte-faithful comma round-trips, incl. trailing commas. All standard stringer options are supported.
197+
- `jsonc/parser.js` — the `charCodeAt` tokenizer of `parser.js` extended with `//` and `/* */` comments, trailing commas, and optional `whitespace` / comment / `comma` tokens (raw inner export `jsoncParser`). Comments mirror strings — streamed `startComment` / `commentChunk` / `endComment` and packed `commentValue`; the comment scan resumes across input chunks, so a comment of any length costs linear time (GHSA-hqr4-qq8f-hg3x). Options: `streamWhitespace` (default: true), `streamComments` (default: true), `packComments` (default: true), `streamCommas` (default: false — emit a valueless `comma` token at every comma's position; the comma byte is already buffered, so emission needs no lookahead and is fully resumable). All standard parser options are supported.
198+
- `jsonc/stringer.js` — fork of `stringer.js` that passes `whitespace` and comment tokens through verbatim (`useCommentValues` selects the packed form). Option `useCommas` (default: false) renders streamed `comma` tokens as `,` (a separator is still auto-inserted before a value when no `comma` token preceded it, so output stays valid even if commas were dropped upstream) — `streamCommas` + `useCommas` give byte-faithful comma round-trips, incl. trailing commas. All standard stringer options are supported.
199199
- `jsonc/verifier.js` — the `charCodeAt` validator of `utils/verifier.js` extended to accept comments and trailing commas (raw inner export `jsoncVerifier`). Reports error offset, line, and position for invalid JSONC.
200200
- Downstream compatibility: all existing filters, streamers, and utilities ignore unknown token types, so they work with JSONC parser output unmodified.
201201

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ Why it might be for you:
1616

1717
## Intended input
1818

19-
`stream-json` is built for data you own or trust &mdash; database dumps, exports, logs, and files produced by your own systems. It is not designed for hostile input: do not feed it JSON from the open internet or from untrusted users. Untrusted JSON needs validation of its own before it reaches a pipeline.
19+
`stream-json` is built for data you own or trust &mdash; database dumps, exports, logs, and files produced by your own systems. It is not designed for hostile input: do not feed it JSON or JSONC from the open internet or from untrusted users. Untrusted JSON needs validation of its own before it reaches a pipeline.
2020

2121
## Example
2222

bench/parser-jsonc-comments.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Scaling meter for JSONC comment scanning (GHSA-hqr4-qq8f-hg3x): one block
2+
// comment fed in 16 KB chunks; time must grow linearly with size — a quadratic
3+
// curve (4x per doubling) means the scan restarted from the comment's start.
4+
// Run via `npx nano-bench-io bench/parser-jsonc-comments.js` and read the ratios.
5+
import jsoncParser from '../src/jsonc/parser.js';
6+
7+
const feed = doc =>
8+
new Promise((resolve, reject) => {
9+
const stream = jsoncParser.asStream();
10+
stream.on('data', () => {});
11+
stream.on('error', reject);
12+
stream.on('end', resolve);
13+
for (let i = 0; i < doc.length; i += 16384) stream.write(doc.slice(i, i + 16384));
14+
stream.end();
15+
});
16+
17+
const K = 1024;
18+
const docs = {
19+
'comment 256K': '/*' + 'a'.repeat(256 * K) + '*/1',
20+
'comment 512K': '/*' + 'a'.repeat(512 * K) + '*/1',
21+
'comment 1M': '/*' + 'a'.repeat(1024 * K) + '*/1',
22+
'string 1M': '"' + 'a'.repeat(1024 * K) + '"'
23+
};
24+
25+
export default Object.fromEntries(
26+
Object.entries(docs).map(([name, doc]) => [
27+
name,
28+
async n => {
29+
for (let i = 0; i < n; ++i) await feed(doc);
30+
}
31+
])
32+
);

llms-full.txt

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
- Proper backpressure handling via Node.js stream infrastructure
1515
- Works with `stream-chain` for pipeline composition
1616

17-
**Intended input:** data you own or trust — database dumps, exports, logs, files produced by your own systems. `stream-json` is not designed for hostile input: do not feed it JSON from the open internet or from untrusted users; untrusted JSON needs validation of its own before it reaches a pipeline.
17+
**Intended input:** data you own or trust — database dumps, exports, logs, files produced by your own systems. `stream-json` is not designed for hostile input: do not feed it JSON or JSONC from the open internet or from untrusted users; untrusted JSON needs validation of its own before it reaches a pipeline.
1818

1919
## Quick start
2020

@@ -810,7 +810,7 @@ for await (const chunk of ts.readable) console.log(chunk);
810810

811811
### jsonc/Parser
812812

813-
Streaming JSONC (JSON with Comments) parser. Uses the same `charCodeAt` tokenizer as the standard parser, extended with `//` and `/* */` comments, trailing commas, and optional `whitespace`/`comment`/`comma` tokens. Has `asStream` (Node Duplex) and `asWebStream` (Web pair). Browser-safe Web-only entry: `stream-json/web/jsonc/parser.js`.
813+
Streaming JSONC (JSON with Comments) parser. Uses the same `charCodeAt` tokenizer as the standard parser, extended with `//` and `/* */` comments, trailing commas, and optional `whitespace` / comment / `comma` tokens. Comments mirror strings: streamed as `startComment` / `commentChunk` / `endComment` (resumable across input chunks — linear time, constant memory without packing) and packed as `commentValue`. Has `asStream` (Node Duplex) and `asWebStream` (Web pair). Browser-safe Web-only entry: `stream-json/web/jsonc/parser.js`.
814814

815815
Static methods:
816816
- `jsoncParser(options)` — factory function returning a composable function for `chain()`.
@@ -820,13 +820,15 @@ Static methods:
820820

821821
Options (in addition to all standard parser options):
822822
- `streamWhitespace` (boolean, default: true) — emit `whitespace` tokens.
823-
- `streamComments` (boolean, default: true) — emit `comment` tokens.
823+
- `streamComments` (boolean, default: true) — emit `startComment` / `commentChunk` / `endComment` tokens.
824+
- `packComments` (boolean, default: true) — emit `commentValue` tokens holding the whole comment. Both off: comments are consumed silently.
824825
- `streamCommas` (boolean, default: false) — emit a valueless `comma` token at the position of every comma (separator or trailing). For faithful round-trip editing: pair with the stringer's `useCommas` to reproduce comma placement (incl. trailing commas) exactly. No lookahead — the comma is already buffered when seen, so emission is fully resumable.
825826

826827
Additional tokens:
827828
- `{name: 'whitespace', value: ' \n'}` — contiguous whitespace between tokens.
828-
- `{name: 'comment', value: '// ...\n'}` — single-line comment (includes EOL).
829-
- `{name: 'comment', value: '/* ... */'}` — multi-line comment (includes delimiters).
829+
- `{name: 'startComment'}`, `{name: 'commentChunk', value: '...'}`, `{name: 'endComment'}` — a comment in chunks (delimiters included; a chunk may end anywhere).
830+
- `{name: 'commentValue', value: '// ...\n'}` — single-line comment (includes EOL).
831+
- `{name: 'commentValue', value: '/* ... */'}` — multi-line comment (includes delimiters).
830832
- `{name: 'comma'}` — a `,` (separator or trailing), valueless; only with `streamCommas`.
831833

832834
```js
@@ -846,15 +848,15 @@ chain([
846848
// Suppress whitespace/comment tokens
847849
chain([
848850
fs.createReadStream('settings.jsonc'),
849-
jsoncParser({streamWhitespace: false, streamComments: false}),
851+
jsoncParser({streamWhitespace: false, streamComments: false, packComments: false}),
850852
streamArray(),
851853
({value}) => console.log(value)
852854
]);
853855
```
854856

855857
### jsonc/Stringer
856858

857-
JSONC stringer that passes `whitespace` and `comment` tokens through verbatim. All other tokens are handled identically to the standard stringer. Has `asStream` (Node Duplex) and `asWebStream` (Web pair). Browser-safe Web-only entry: `stream-json/web/jsonc/stringer.js`.
859+
JSONC stringer that passes `whitespace` and comment tokens through verbatim — comments from `commentChunk`s by default, or from `commentValue` with `useCommentValues` (default: false). All other tokens are handled identically to the standard stringer. Has `asStream` (Node Duplex) and `asWebStream` (Web pair). Browser-safe Web-only entry: `stream-json/web/jsonc/stringer.js`.
858860

859861
Static methods:
860862
- `jsoncStringer(options)` — factory function returning a flushable function for `chain()`.

llms.txt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
> Micro-library of Node.js stream components for creating custom JSON processing pipelines with a minimal memory footprint. Parse JSON files far exceeding available memory using a SAX-inspired streaming token API. One dependency: `stream-chain`.
44

5-
**Intended input:** data you own or trust (database dumps, exports, logs, your own systems' files). Not designed for hostile input — do not feed it JSON from the open internet or from untrusted users; validate untrusted JSON before it reaches a pipeline.
5+
**Intended input:** data you own or trust (database dumps, exports, logs, your own systems' files). Not designed for hostile input — do not feed it JSON or JSONC from the open internet or from untrusted users; validate untrusted JSON before it reaches a pipeline.
66

77
## Install
88

@@ -190,10 +190,10 @@ chain([fs.createReadStream('data.jsonl'), parser(), ({value}) => transform(value
190190

191191
## JSONC support
192192

193-
- **`jsonc/parser(options)`** — JSONC parser (JSON with Comments). Same `charCodeAt` tokenizer as the standard parser, extended with `//` and `/* */` comments, trailing commas, and `whitespace` / `comment` / `comma` tokens.
194-
- Extra options: `streamWhitespace` (default: true), `streamComments` (default: true), `streamCommas` (default: false — emit a valueless `comma` token at every comma, separator or trailing, for faithful round-trip editing).
193+
- **`jsonc/parser(options)`** — JSONC parser (JSON with Comments). Same `charCodeAt` tokenizer as the standard parser, extended with `//` and `/* */` comments, trailing commas, and `whitespace` / comment / `comma` tokens. Comments mirror strings: streamed as `startComment` / `commentChunk` / `endComment` (resumable across input chunks, linear time) and packed as `commentValue`.
194+
- Extra options: `streamWhitespace` (default: true), `streamComments` (default: true — the streamed form), `packComments` (default: true — `commentValue`; both off consumes comments silently), `streamCommas` (default: false — emit a valueless `comma` token at every comma, separator or trailing, for faithful round-trip editing).
195195
- All standard parser options are supported.
196-
- **`jsonc/stringer(options)`** — JSONC stringer. Passes `whitespace` and `comment` tokens through verbatim. Extra option: `useCommas` (default: false — render streamed `comma` tokens as `,`, auto-inserting a separator only when no comma token arrived, so output stays valid even if commas were dropped upstream).
196+
- **`jsonc/stringer(options)`** — JSONC stringer. Passes `whitespace` and comment tokens through verbatim (comments from `commentChunk`s, or from `commentValue` with `useCommentValues`). Extra options: `useCommentValues` (default: false), `useCommas` (default: false — render streamed `comma` tokens as `,`, auto-inserting a separator only when no comma token arrived, so output stays valid even if commas were dropped upstream).
197197
- **`jsonc/verifier(options)`** — JSONC validator. Same `charCodeAt` validator as `Verifier`, accepting comments and trailing commas. Reports exact error position.
198198

199199
```js

src/core/jsonc/parser.d.ts

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,14 @@ declare function parser(options?: parser.JsoncParserOptions): Flushable<string,
2121
declare namespace parser {
2222
/**
2323
* A single token emitted by the JSONC parser. Extends the base JSON `Token`
24-
* with `comment` — single-line (`//`) and block (`/* ... *​/`) comments
25-
* surfaced when `streamComments` is set — and `comma`, a valueless marker
26-
* emitted at every comma's position when `streamCommas` is set.
24+
* with comments — single-line (`//`) and block (`/* ... *​/`), delimiters
25+
* included — in the same two forms as strings: streamed as
26+
* `startComment` / `commentChunk` / `endComment` when `streamComments` is set,
27+
* packed as `commentValue` when `packComments` is set — and `comma`, a
28+
* valueless marker emitted at every comma's position when `streamCommas` is set.
2729
*/
28-
export type Token = BaseToken | {name: 'comment'; value: string} | {name: 'comma'};
30+
export type Token =
31+
BaseToken | {name: 'startComment'} | {name: 'commentChunk'; value: string} | {name: 'endComment'} | {name: 'commentValue'; value: string} | {name: 'comma'};
2932
/** Alias of `Token` — disambiguates when both JSON and JSONC tokens are imported. */
3033
export type JsoncToken = Token;
3134

@@ -56,8 +59,19 @@ declare namespace parser {
5659
jsonStreaming?: boolean;
5760
/** Emit `whitespace` tokens. Default: `true`. */
5861
streamWhitespace?: boolean;
59-
/** Emit `comment` tokens. Default: `true`. */
62+
/**
63+
* Emit `startComment`/`endComment`/`commentChunk` tokens. Default: `true`.
64+
* A comment may arrive in several chunks; the scan resumes across input
65+
* chunks, so a long comment costs linear time and, without `packComments`,
66+
* constant memory.
67+
*/
6068
streamComments?: boolean;
69+
/**
70+
* Pack comments into `commentValue` tokens (the whole comment, delimiters
71+
* included). Default: `true`. With both `streamComments` and `packComments`
72+
* off, comments are consumed silently.
73+
*/
74+
packComments?: boolean;
6175
/**
6276
* Emit a valueless `comma` token at the position of every comma (separator
6377
* or trailing), so a parse → stringify round-trip can reproduce comma

0 commit comments

Comments
 (0)