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/ast.js +255 -0
- package/equality.js +166 -0
- package/errors.js +218 -0
- package/format.js +453 -0
- package/index.d.ts +1933 -1983
- package/index.js +11 -1454
- package/package.json +50 -56
- package/parse.js +548 -0
- package/scanner.js +350 -0
- package/schema-integration.js +178 -0
- package/schemas.js +325 -0
- package/tsdoc-metadata.json +11 -11
- package/visitor.js +355 -0
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 };
|
package/tsdoc-metadata.json
CHANGED
|
@@ -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.
|
|
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 };
|