jsonc-effect 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/schemas.js ADDED
@@ -0,0 +1,325 @@
1
+ import { Schema } from "effect";
2
+
3
+ //#region src/schemas.ts
4
+ /**
5
+ * JSONC Schema definitions for tokens, AST nodes, and options.
6
+ *
7
+ * @packageDocumentation
8
+ */
9
+ /**
10
+ * Token types produced by the JSONC scanner.
11
+ *
12
+ * @remarks
13
+ * Uses string literals instead of numeric enums so that token kinds are
14
+ * self-documenting in debug output, log messages, and test assertions.
15
+ * This avoids the "reverse-mapping" confusion of TypeScript numeric enums
16
+ * and makes pattern-matching with `Schema.Literal` straightforward.
17
+ *
18
+ * @see {@link createScanner} — creates a scanner that emits these token types
19
+ *
20
+ * @public
21
+ */
22
+ const JsoncSyntaxKind = Schema.Literal("OpenBrace", "CloseBrace", "OpenBracket", "CloseBracket", "Comma", "Colon", "Null", "True", "False", "String", "Number", "LineComment", "BlockComment", "LineBreak", "Trivia", "Unknown", "EOF");
23
+ /**
24
+ * Scanner error codes produced by the JSONC scanner.
25
+ *
26
+ * @remarks
27
+ * `"None"` indicates a successful scan with no errors. All other values
28
+ * describe a specific lexical error encountered while tokenizing input.
29
+ *
30
+ * @see {@link JsoncScanner} — the scanner interface that reports these errors
31
+ *
32
+ * @public
33
+ */
34
+ const JsoncScanError = Schema.Literal("None", "UnexpectedEndOfComment", "UnexpectedEndOfString", "UnexpectedEndOfNumber", "InvalidUnicode", "InvalidEscapeCharacter", "InvalidCharacter", "InvalidSymbol");
35
+ /**
36
+ * A single token produced by the JSONC scanner, carrying its kind, textual
37
+ * value, position within the source, and any scan error.
38
+ *
39
+ * @remarks
40
+ * - `kind` — the {@link (JsoncSyntaxKind:type)} discriminator for this token.
41
+ * - `value` — the raw text slice from the source document.
42
+ * - `offset` — zero-based character offset from the start of the document.
43
+ * - `length` — character length of this token in the source.
44
+ * - `startLine` — zero-based line number where the token begins.
45
+ * - `startCharacter` — zero-based column within `startLine`.
46
+ * - `error` — a {@link (JsoncScanError:type)} code; `"None"` when the token is valid.
47
+ *
48
+ * @see {@link createScanner} — produces a stream of `JsoncToken` instances
49
+ *
50
+ * @example
51
+ * ```ts
52
+ * import { JsoncToken } from "jsonc-effect";
53
+ *
54
+ * const token = new JsoncToken({
55
+ * kind: "String",
56
+ * value: '"hello"',
57
+ * offset: 0,
58
+ * length: 7,
59
+ * startLine: 0,
60
+ * startCharacter: 0,
61
+ * error: "None",
62
+ * });
63
+ *
64
+ * console.log(token.kind); // "String"
65
+ * console.log(token.value); // '"hello"'
66
+ * ```
67
+ *
68
+ * @privateRemarks
69
+ * `Schema.Class` gives `JsoncToken` structural equality via `Data.Class`
70
+ * under the hood, so two tokens with identical fields are considered equal
71
+ * by `Equal.equals`. This is essential for test assertions and Effect's
72
+ * structural comparison semantics.
73
+ *
74
+ * @public
75
+ */
76
+ var JsoncToken = class extends Schema.Class("JsoncToken")({
77
+ kind: JsoncSyntaxKind,
78
+ value: Schema.String,
79
+ offset: Schema.Number,
80
+ length: Schema.Number,
81
+ startLine: Schema.Number,
82
+ startCharacter: Schema.Number,
83
+ error: JsoncScanError
84
+ }) {};
85
+ /**
86
+ * Discriminator values for JSONC AST node types.
87
+ *
88
+ * @remarks
89
+ * These correspond to the JSON value types (`"string"`, `"number"`,
90
+ * `"boolean"`, `"null"`) plus structural types (`"object"`, `"array"`)
91
+ * and the special `"property"` type representing a key-value pair inside
92
+ * an object.
93
+ *
94
+ * @see {@link JsoncNode} — the AST node that carries this discriminator
95
+ *
96
+ * @public
97
+ */
98
+ const JsoncNodeType = Schema.Literal("object", "array", "property", "string", "number", "boolean", "null");
99
+ /**
100
+ * AST node representing a parsed JSONC element, produced by {@link parseTree}.
101
+ *
102
+ * @remarks
103
+ * The `parent` field present in Microsoft's `jsonc-parser` is intentionally
104
+ * omitted here to avoid circular references, which would break structural
105
+ * equality, serialization, and Effect's `Schema.encode`/`Schema.decode`
106
+ * pipelines. Child relationships are expressed via the `children` array,
107
+ * and the recursive type is handled with `Schema.suspend`.
108
+ *
109
+ * - `type` — the {@link (JsoncNodeType:type)} discriminator.
110
+ * - `value` — the decoded JavaScript value for leaf nodes (`string`,
111
+ * `number`, `boolean`, `null`); `undefined` for structural nodes.
112
+ * - `offset` — zero-based character offset of this node in the source.
113
+ * - `length` — character length of this node in the source.
114
+ * - `colonOffset` — for `"property"` nodes, the offset of the `:` separator.
115
+ * - `children` — child nodes; present for `"object"`, `"array"`, and
116
+ * `"property"` nodes.
117
+ *
118
+ * @see {@link parseTree} — produces the root `JsoncNode`
119
+ * @see {@link findNode} — locates a descendant by path
120
+ *
121
+ * @example
122
+ * ```ts
123
+ * import { Effect } from "effect";
124
+ * import { parseTree } from "jsonc-effect";
125
+ *
126
+ * const program = parseTree('{ "key": [1, 2] }').pipe(
127
+ * Effect.map((root) => {
128
+ * // root.type === "object"
129
+ * const property = root.children?.[0]; // "property" node
130
+ * const array = property?.children?.[1]; // "array" node
131
+ * console.log(array?.children?.length); // 2
132
+ * }),
133
+ * );
134
+ * ```
135
+ *
136
+ * @privateRemarks
137
+ * Unlike the `*Base` pattern needed for `Data.TaggedError` subclasses,
138
+ * `Schema.Class` works directly for data types — api-extractor can roll up
139
+ * the generated `.d.ts` without issues. The `Schema.suspend` call for
140
+ * `children` is required to break the circular type reference at the schema
141
+ * level while still allowing recursive decode/encode.
142
+ *
143
+ * @public
144
+ */
145
+ var JsoncNode = class JsoncNode extends Schema.Class("JsoncNode")({
146
+ type: JsoncNodeType,
147
+ value: Schema.optional(Schema.Unknown),
148
+ offset: Schema.Number,
149
+ length: Schema.Number,
150
+ colonOffset: Schema.optional(Schema.Number),
151
+ children: Schema.optional(Schema.Array(Schema.suspend(() => JsoncNode)))
152
+ }) {};
153
+ /**
154
+ * A single segment of a {@link (JsoncPath:type)}: a `string` for object property
155
+ * keys or a `number` for array indices.
156
+ *
157
+ * @see {@link findNode} — resolves a path to an AST node
158
+ * @see {@link modify} — applies a value change at a path
159
+ *
160
+ * @example
161
+ * ```ts
162
+ * import type { JsoncSegment } from "jsonc-effect";
163
+ *
164
+ * const objectKey: JsoncSegment = "compilerOptions";
165
+ * const arrayIndex: JsoncSegment = 0;
166
+ * ```
167
+ *
168
+ * @public
169
+ */
170
+ const JsoncSegment = Schema.Union(Schema.String, Schema.Number);
171
+ /**
172
+ * An ordered sequence of {@link (JsoncSegment:type)} values describing a location
173
+ * within a JSONC document tree.
174
+ *
175
+ * @see {@link findNode} — resolves a `JsoncPath` to an AST node
176
+ * @see {@link modify} — applies a modification at a `JsoncPath`
177
+ *
178
+ * @example
179
+ * ```ts
180
+ * import type { JsoncPath } from "jsonc-effect";
181
+ *
182
+ * // Path to the "strict" property inside "compilerOptions"
183
+ * const path: JsoncPath = ["compilerOptions", "strict"];
184
+ *
185
+ * // Path to the second element of the "include" array
186
+ * const arrayPath: JsoncPath = ["include", 1];
187
+ * ```
188
+ *
189
+ * @public
190
+ */
191
+ const JsoncPath = Schema.Array(JsoncSegment);
192
+ /**
193
+ * A non-mutating text edit describing a replacement within a JSONC document.
194
+ *
195
+ * @remarks
196
+ * Edits use zero-based `offset` and `length` to identify the span of text
197
+ * to replace, and `content` for the replacement string. To insert without
198
+ * removing text, set `length` to `0`. To delete without inserting, set
199
+ * `content` to `""`.
200
+ *
201
+ * Edits returned by {@link format} and {@link modify} should be applied
202
+ * via {@link applyEdits}, which processes them in reverse order so that
203
+ * earlier offsets remain valid.
204
+ *
205
+ * @see {@link format} — produces edits for formatting
206
+ * @see {@link modify} — produces edits for value changes
207
+ * @see {@link applyEdits} — applies an array of edits to a source string
208
+ *
209
+ * @example
210
+ * ```ts
211
+ * import { JsoncEdit } from "jsonc-effect";
212
+ *
213
+ * // An edit that inserts ", true" at offset 10
214
+ * const edit = new JsoncEdit({ offset: 10, length: 0, content: ", true" });
215
+ *
216
+ * console.log(edit.offset); // 10
217
+ * console.log(edit.length); // 0
218
+ * console.log(edit.content); // ", true"
219
+ * ```
220
+ *
221
+ * @public
222
+ */
223
+ var JsoncEdit = class extends Schema.Class("JsoncEdit")({
224
+ offset: Schema.Number,
225
+ length: Schema.Number,
226
+ content: Schema.String
227
+ }) {};
228
+ /**
229
+ * A range within a JSONC document, expressed as a zero-based character
230
+ * offset and a length in characters.
231
+ *
232
+ * @remarks
233
+ * Both `offset` and `length` are measured in UTF-16 code units (JavaScript
234
+ * string indices). Pass a `JsoncRange` to {@link format} to restrict
235
+ * formatting to a specific region of the document rather than the whole file.
236
+ *
237
+ * @see {@link format} — accepts an optional `JsoncRange` parameter
238
+ *
239
+ * @public
240
+ */
241
+ var JsoncRange = class extends Schema.Class("JsoncRange")({
242
+ offset: Schema.Number,
243
+ length: Schema.Number
244
+ }) {};
245
+ /**
246
+ * Options controlling JSONC parse behavior.
247
+ *
248
+ * @remarks
249
+ * - `disallowComments` — when `true`, line and block comments are treated
250
+ * as parse errors. Defaults to `false`.
251
+ * - `allowTrailingComma` — when `true`, trailing commas after the last
252
+ * element in arrays and objects are permitted. Defaults to `true`, which
253
+ * differs from Microsoft's `jsonc-parser` (where the default is `false`).
254
+ * - `allowEmptyContent` — when `true`, an empty string parses as
255
+ * `undefined` rather than producing an error. Defaults to `false`.
256
+ *
257
+ * @see {@link parse} — parses JSONC text into a JavaScript value
258
+ * @see {@link parseTree} — parses JSONC text into an AST
259
+ *
260
+ * @example
261
+ * ```ts
262
+ * import { Effect } from "effect";
263
+ * import { parse, JsoncParseOptions } from "jsonc-effect";
264
+ *
265
+ * const options = new JsoncParseOptions({
266
+ * disallowComments: true,
267
+ * allowTrailingComma: false,
268
+ * });
269
+ *
270
+ * const program = parse('{ "key": "value" }', options).pipe(
271
+ * Effect.map((value) => console.log(value)),
272
+ * );
273
+ * ```
274
+ *
275
+ * @public
276
+ */
277
+ var JsoncParseOptions = class extends Schema.Class("JsoncParseOptions")({
278
+ disallowComments: Schema.optionalWith(Schema.Boolean, { default: () => false }),
279
+ allowTrailingComma: Schema.optionalWith(Schema.Boolean, { default: () => true }),
280
+ allowEmptyContent: Schema.optionalWith(Schema.Boolean, { default: () => false })
281
+ }) {};
282
+ /**
283
+ * Options controlling JSONC formatting behavior.
284
+ *
285
+ * @remarks
286
+ * - `tabSize` — number of spaces per indentation level. Defaults to `2`.
287
+ * - `insertSpaces` — when `true`, use spaces for indentation; when `false`,
288
+ * use tab characters. Defaults to `true`.
289
+ * - `eol` — the end-of-line sequence. Defaults to `"\n"`.
290
+ * - `insertFinalNewline` — when `true`, ensure the formatted output ends
291
+ * with a newline. Defaults to `false`.
292
+ * - `keepLines` — when `true`, preserve existing line breaks in the source
293
+ * rather than reflowing. Defaults to `false`.
294
+ *
295
+ * @see {@link format} — uses these options to produce formatting edits
296
+ * @see {@link formatAndApply} — formats and applies edits in one step
297
+ *
298
+ * @example
299
+ * ```ts
300
+ * import { Effect } from "effect";
301
+ * import { formatAndApply, JsoncFormattingOptions } from "jsonc-effect";
302
+ *
303
+ * const options = new JsoncFormattingOptions({
304
+ * tabSize: 4,
305
+ * insertSpaces: true,
306
+ * insertFinalNewline: true,
307
+ * });
308
+ *
309
+ * const program = formatAndApply('{"key":"value"}', options).pipe(
310
+ * Effect.map((formatted) => console.log(formatted)),
311
+ * );
312
+ * ```
313
+ *
314
+ * @public
315
+ */
316
+ var JsoncFormattingOptions = class extends Schema.Class("JsoncFormattingOptions")({
317
+ tabSize: Schema.optionalWith(Schema.Number, { default: () => 2 }),
318
+ insertSpaces: Schema.optionalWith(Schema.Boolean, { default: () => true }),
319
+ eol: Schema.optionalWith(Schema.String, { default: () => "\n" }),
320
+ insertFinalNewline: Schema.optionalWith(Schema.Boolean, { default: () => false }),
321
+ keepLines: Schema.optionalWith(Schema.Boolean, { default: () => false })
322
+ }) {};
323
+
324
+ //#endregion
325
+ export { JsoncEdit, JsoncFormattingOptions, JsoncNode, JsoncNodeType, JsoncParseOptions, JsoncRange, JsoncScanError, JsoncSegment, JsoncSyntaxKind, JsoncToken };
@@ -1,11 +1,11 @@
1
- // This file is read by tools that parse documentation comments conforming to the TSDoc standard.
2
- // It should be published with your NPM package. It should not be tracked by Git.
3
- {
4
- "tsdocVersion": "0.12",
5
- "toolPackages": [
6
- {
7
- "packageName": "@microsoft/api-extractor",
8
- "packageVersion": "7.57.7"
9
- }
10
- ]
11
- }
1
+ // This file is read by tools that parse documentation comments conforming to the TSDoc standard.
2
+ // It should be published with your NPM package. It should not be tracked by Git.
3
+ {
4
+ "tsdocVersion": "0.12",
5
+ "toolPackages": [
6
+ {
7
+ "packageName": "@microsoft/api-extractor",
8
+ "packageVersion": "7.58.9"
9
+ }
10
+ ]
11
+ }
package/visitor.js ADDED
@@ -0,0 +1,355 @@
1
+ import { createScanner } from "./scanner.js";
2
+ import { Chunk, Effect, Stream } from "effect";
3
+
4
+ //#region src/visitor.ts
5
+ /**
6
+ * SAX-style visitor API for JSONC documents.
7
+ *
8
+ * Emits typed events as an Effect Stream, enabling memory-efficient
9
+ * processing of large JSONC documents via lazy evaluation.
10
+ *
11
+ * @packageDocumentation
12
+ */
13
+ /**
14
+ * Create a lazy `Stream` of {@link JsoncVisitorEvent} from JSONC text.
15
+ *
16
+ * Events are produced on demand as the stream is consumed, not
17
+ * pre-collected into memory. This makes `visit` suitable for large
18
+ * documents and supports early termination via `Stream.take` or
19
+ * `Stream.takeWhile` without scanning the entire input.
20
+ *
21
+ * @param text - The JSONC source text to visit.
22
+ * @param options - Optional partial {@link JsoncParseOptions} controlling
23
+ * comment handling and trailing-comma tolerance.
24
+ * @returns A `Stream` of {@link JsoncVisitorEvent} objects.
25
+ *
26
+ * @remarks
27
+ * The stream is backed by a lazy generator — no work is performed until
28
+ * the stream is consumed. Because evaluation is demand-driven, combining
29
+ * `visit` with `Stream.take` allows efficient partial scans of large
30
+ * documents without allocating a full AST.
31
+ *
32
+ * @see {@link visitCollect} for a one-step filter-and-collect convenience.
33
+ * @see {@link JsoncVisitorEvent} for the event type definitions.
34
+ *
35
+ * @example Collect all events
36
+ * ```ts
37
+ * import { Chunk, Effect, Stream } from "effect";
38
+ * import { visit } from "jsonc-effect";
39
+ *
40
+ * const all = Effect.runSync(
41
+ * visit('{ "a": 1 }').pipe(
42
+ * Stream.runCollect,
43
+ * Effect.map(Chunk.toReadonlyArray),
44
+ * ),
45
+ * );
46
+ * ```
47
+ *
48
+ * @example Filter and take the first match
49
+ * ```ts
50
+ * import { Chunk, Effect, Stream } from "effect";
51
+ * import { visit } from "jsonc-effect";
52
+ *
53
+ * const firstLiteral = Effect.runSync(
54
+ * visit('{ "a": 1, "b": 2 }').pipe(
55
+ * Stream.filter((e) => e._tag === "LiteralValue"),
56
+ * Stream.take(1),
57
+ * Stream.runCollect,
58
+ * Effect.map(Chunk.toReadonlyArray),
59
+ * ),
60
+ * );
61
+ * ```
62
+ *
63
+ * @example Extract property names
64
+ * ```ts
65
+ * import { Chunk, Effect, Stream } from "effect";
66
+ * import { visit } from "jsonc-effect";
67
+ *
68
+ * const propertyNames = Effect.runSync(
69
+ * visit('{ "name": "Alice", "age": 30 }').pipe(
70
+ * Stream.filter((e) => e._tag === "ObjectProperty"),
71
+ * Stream.map((e) => (e as { property: string }).property),
72
+ * Stream.runCollect,
73
+ * Effect.map(Chunk.toReadonlyArray),
74
+ * ),
75
+ * );
76
+ * ```
77
+ *
78
+ * @privateRemarks
79
+ * Internally wraps a generator function with `Stream.fromIterable`,
80
+ * preserving laziness. The generator yields events as it encounters
81
+ * tokens from the scanner.
82
+ *
83
+ * @public
84
+ */
85
+ const visit = (text, options) => Stream.fromIterable(visitGen(text, options));
86
+ /**
87
+ * Visit JSONC text and collect all events matching a type-guard predicate.
88
+ *
89
+ * This is a convenience that composes {@link visit}, `Stream.filter`, and
90
+ * `Stream.runCollect` into a single call.
91
+ *
92
+ * @param text - The JSONC source text to visit.
93
+ * @param predicate - A type-guard function that narrows
94
+ * {@link JsoncVisitorEvent} to the desired subtype `A`.
95
+ * @param options - Optional partial {@link JsoncParseOptions}.
96
+ * @returns An `Effect` that succeeds with a read-only array of the
97
+ * matched events.
98
+ *
99
+ * @remarks
100
+ * Equivalent to:
101
+ * ```
102
+ * visit(text, options) |> Stream.filter(predicate) |> Stream.runCollect
103
+ * ```
104
+ * Use this when you need all matching events and do not require
105
+ * intermediate stream transformations.
106
+ *
107
+ * @see {@link visit} for full stream-level control.
108
+ *
109
+ * @example Collecting literal values
110
+ * ```ts
111
+ * import { Effect } from "effect";
112
+ * import type { JsoncVisitorEvent } from "jsonc-effect";
113
+ * import { visitCollect } from "jsonc-effect";
114
+ *
115
+ * const literals = Effect.runSync(
116
+ * visitCollect(
117
+ * '{ "a": 1, "b": true }',
118
+ * (e): e is Extract<JsoncVisitorEvent, { _tag: "LiteralValue" }> =>
119
+ * e._tag === "LiteralValue",
120
+ * ),
121
+ * );
122
+ * ```
123
+ *
124
+ * @public
125
+ */
126
+ const visitCollect = (text, predicate, options) => visit(text, options).pipe(Stream.filter(predicate), Stream.runCollect, Effect.map(Chunk.toReadonlyArray));
127
+ function* visitGen(text, options) {
128
+ const scanner = createScanner(text, false);
129
+ const disallowComments = options?.disallowComments ?? false;
130
+ const path = [];
131
+ function* scanNext() {
132
+ for (;;) {
133
+ const t = scanner.scan();
134
+ const scanError = scanner.getTokenError();
135
+ if (scanError !== "None") {
136
+ let code;
137
+ switch (scanError) {
138
+ case "InvalidUnicode":
139
+ code = "InvalidUnicode";
140
+ break;
141
+ case "InvalidEscapeCharacter":
142
+ code = "InvalidEscapeCharacter";
143
+ break;
144
+ case "UnexpectedEndOfNumber":
145
+ code = "InvalidNumberFormat";
146
+ break;
147
+ case "UnexpectedEndOfComment":
148
+ code = "UnexpectedEndOfComment";
149
+ break;
150
+ case "UnexpectedEndOfString":
151
+ code = "UnexpectedEndOfString";
152
+ break;
153
+ case "InvalidCharacter":
154
+ code = "InvalidCharacter";
155
+ break;
156
+ default: code = "InvalidSymbol";
157
+ }
158
+ yield {
159
+ _tag: "Error",
160
+ code,
161
+ offset: scanner.getTokenOffset(),
162
+ length: scanner.getTokenLength()
163
+ };
164
+ }
165
+ switch (t) {
166
+ case "LineComment":
167
+ case "BlockComment":
168
+ if (disallowComments) yield {
169
+ _tag: "Error",
170
+ code: "InvalidCommentToken",
171
+ offset: scanner.getTokenOffset(),
172
+ length: scanner.getTokenLength()
173
+ };
174
+ else yield {
175
+ _tag: "Comment",
176
+ offset: scanner.getTokenOffset(),
177
+ length: scanner.getTokenLength()
178
+ };
179
+ break;
180
+ case "Trivia":
181
+ case "LineBreak": break;
182
+ default: return t;
183
+ }
184
+ }
185
+ }
186
+ function getLiteralValue(kind, tokenValue) {
187
+ switch (kind) {
188
+ case "String": return tokenValue;
189
+ case "Number": return Number.parseFloat(tokenValue);
190
+ case "True": return true;
191
+ case "False": return false;
192
+ case "Null": return null;
193
+ default: return;
194
+ }
195
+ }
196
+ function* visitValue() {
197
+ const t = scanner.getToken();
198
+ switch (t) {
199
+ case "OpenBrace": return yield* visitObject();
200
+ case "OpenBracket": return yield* visitArray();
201
+ case "String":
202
+ case "Number":
203
+ case "True":
204
+ case "False":
205
+ case "Null":
206
+ yield {
207
+ _tag: "LiteralValue",
208
+ value: getLiteralValue(t, scanner.getTokenValue()),
209
+ offset: scanner.getTokenOffset(),
210
+ length: scanner.getTokenLength(),
211
+ path: [...path]
212
+ };
213
+ yield* scanNext();
214
+ return true;
215
+ default:
216
+ yield {
217
+ _tag: "Error",
218
+ code: "ValueExpected",
219
+ offset: scanner.getTokenOffset(),
220
+ length: scanner.getTokenLength()
221
+ };
222
+ return false;
223
+ }
224
+ }
225
+ function* visitObject() {
226
+ yield {
227
+ _tag: "ObjectBegin",
228
+ offset: scanner.getTokenOffset(),
229
+ length: scanner.getTokenLength(),
230
+ path: [...path]
231
+ };
232
+ yield* scanNext();
233
+ let needsComma = false;
234
+ while (scanner.getToken() !== "CloseBrace" && scanner.getToken() !== "EOF") {
235
+ if (scanner.getToken() === "Comma") {
236
+ yield {
237
+ _tag: "Separator",
238
+ character: ",",
239
+ offset: scanner.getTokenOffset(),
240
+ length: scanner.getTokenLength()
241
+ };
242
+ yield* scanNext();
243
+ if (scanner.getToken() === "CloseBrace") break;
244
+ } else if (needsComma) yield {
245
+ _tag: "Error",
246
+ code: "CommaExpected",
247
+ offset: scanner.getTokenOffset(),
248
+ length: scanner.getTokenLength()
249
+ };
250
+ if (scanner.getToken() !== "String") {
251
+ yield {
252
+ _tag: "Error",
253
+ code: "PropertyNameExpected",
254
+ offset: scanner.getTokenOffset(),
255
+ length: scanner.getTokenLength()
256
+ };
257
+ yield* scanNext();
258
+ continue;
259
+ }
260
+ const key = scanner.getTokenValue();
261
+ yield {
262
+ _tag: "ObjectProperty",
263
+ property: key,
264
+ offset: scanner.getTokenOffset(),
265
+ length: scanner.getTokenLength(),
266
+ path: [...path]
267
+ };
268
+ path.push(key);
269
+ yield* scanNext();
270
+ if (scanner.getToken() === "Colon") {
271
+ yield {
272
+ _tag: "Separator",
273
+ character: ":",
274
+ offset: scanner.getTokenOffset(),
275
+ length: scanner.getTokenLength()
276
+ };
277
+ yield* scanNext();
278
+ } else yield {
279
+ _tag: "Error",
280
+ code: "ColonExpected",
281
+ offset: scanner.getTokenOffset(),
282
+ length: scanner.getTokenLength()
283
+ };
284
+ yield* visitValue();
285
+ path.pop();
286
+ needsComma = true;
287
+ }
288
+ if (scanner.getToken() === "CloseBrace") {
289
+ yield {
290
+ _tag: "ObjectEnd",
291
+ offset: scanner.getTokenOffset(),
292
+ length: scanner.getTokenLength()
293
+ };
294
+ yield* scanNext();
295
+ } else yield {
296
+ _tag: "Error",
297
+ code: "CloseBraceExpected",
298
+ offset: scanner.getTokenOffset(),
299
+ length: scanner.getTokenLength()
300
+ };
301
+ return true;
302
+ }
303
+ function* visitArray() {
304
+ yield {
305
+ _tag: "ArrayBegin",
306
+ offset: scanner.getTokenOffset(),
307
+ length: scanner.getTokenLength(),
308
+ path: [...path]
309
+ };
310
+ yield* scanNext();
311
+ let index = 0;
312
+ let needsComma = false;
313
+ while (scanner.getToken() !== "CloseBracket" && scanner.getToken() !== "EOF") {
314
+ if (scanner.getToken() === "Comma") {
315
+ yield {
316
+ _tag: "Separator",
317
+ character: ",",
318
+ offset: scanner.getTokenOffset(),
319
+ length: scanner.getTokenLength()
320
+ };
321
+ yield* scanNext();
322
+ if (scanner.getToken() === "CloseBracket") break;
323
+ } else if (needsComma) yield {
324
+ _tag: "Error",
325
+ code: "CommaExpected",
326
+ offset: scanner.getTokenOffset(),
327
+ length: scanner.getTokenLength()
328
+ };
329
+ path.push(index);
330
+ yield* visitValue();
331
+ path.pop();
332
+ index++;
333
+ needsComma = true;
334
+ }
335
+ if (scanner.getToken() === "CloseBracket") {
336
+ yield {
337
+ _tag: "ArrayEnd",
338
+ offset: scanner.getTokenOffset(),
339
+ length: scanner.getTokenLength()
340
+ };
341
+ yield* scanNext();
342
+ } else yield {
343
+ _tag: "Error",
344
+ code: "CloseBracketExpected",
345
+ offset: scanner.getTokenOffset(),
346
+ length: scanner.getTokenLength()
347
+ };
348
+ return true;
349
+ }
350
+ yield* scanNext();
351
+ if (scanner.getToken() !== "EOF") yield* visitValue();
352
+ }
353
+
354
+ //#endregion
355
+ export { visit, visitCollect };