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/package.json
CHANGED
|
@@ -1,58 +1,52 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
"README.md",
|
|
53
|
-
"index.d.ts",
|
|
54
|
-
"index.js",
|
|
55
|
-
"package.json",
|
|
56
|
-
"tsdoc-metadata.json"
|
|
57
|
-
]
|
|
2
|
+
"name": "jsonc-effect",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "Pure Effect-TS implementation of a JSONC (JSON with Comments) parser",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"jsonc",
|
|
8
|
+
"json",
|
|
9
|
+
"json-with-comments",
|
|
10
|
+
"parser",
|
|
11
|
+
"scanner",
|
|
12
|
+
"lexer",
|
|
13
|
+
"ast",
|
|
14
|
+
"effect",
|
|
15
|
+
"effect-ts",
|
|
16
|
+
"schema",
|
|
17
|
+
"typed-errors",
|
|
18
|
+
"config",
|
|
19
|
+
"tsconfig",
|
|
20
|
+
"formatting",
|
|
21
|
+
"visitor"
|
|
22
|
+
],
|
|
23
|
+
"homepage": "https://github.com/spencerbeggs/jsonc-effect#readme",
|
|
24
|
+
"bugs": {
|
|
25
|
+
"url": "https://github.com/spencerbeggs/jsonc-effect/issues"
|
|
26
|
+
},
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/spencerbeggs/jsonc-effect.git"
|
|
30
|
+
},
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"author": {
|
|
33
|
+
"name": "C. Spencer Beggs",
|
|
34
|
+
"email": "spencer@beggs.codes",
|
|
35
|
+
"url": "https://spencerbeg.gs"
|
|
36
|
+
},
|
|
37
|
+
"sideEffects": false,
|
|
38
|
+
"type": "module",
|
|
39
|
+
"exports": {
|
|
40
|
+
".": {
|
|
41
|
+
"types": "./index.d.ts",
|
|
42
|
+
"import": "./index.js"
|
|
43
|
+
},
|
|
44
|
+
"./package.json": "./package.json"
|
|
45
|
+
},
|
|
46
|
+
"peerDependencies": {
|
|
47
|
+
"effect": "^3.21.0"
|
|
48
|
+
},
|
|
49
|
+
"engines": {
|
|
50
|
+
"node": ">=24.11.0"
|
|
51
|
+
}
|
|
58
52
|
}
|
package/parse.js
ADDED
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
import { JsoncParseError, JsoncParseErrorDetail } from "./errors.js";
|
|
2
|
+
import { createScanner } from "./scanner.js";
|
|
3
|
+
import { Effect, Option } from "effect";
|
|
4
|
+
|
|
5
|
+
//#region src/parse.ts
|
|
6
|
+
/**
|
|
7
|
+
* JSONC Parser — converts token stream into JavaScript values or AST nodes.
|
|
8
|
+
*
|
|
9
|
+
* Pure Effect implementation using recursive descent parsing.
|
|
10
|
+
* Reference: Microsoft's jsonc-parser parser design (MIT).
|
|
11
|
+
*
|
|
12
|
+
* @packageDocumentation
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Parse a JSONC string into a JavaScript value.
|
|
16
|
+
*
|
|
17
|
+
* @param text - JSONC string to parse
|
|
18
|
+
* @param options - Optional {@link JsoncParseOptions} controlling comment and
|
|
19
|
+
* trailing-comma handling.
|
|
20
|
+
* @returns `Effect<unknown, JsoncParseError>` — succeeds with the parsed value
|
|
21
|
+
* or fails with a {@link JsoncParseError} containing every error encountered.
|
|
22
|
+
*
|
|
23
|
+
* @remarks
|
|
24
|
+
* The return type is `unknown` (not `any`) so consumers are forced to narrow
|
|
25
|
+
* the result, which is safer in Effect pipelines. By default
|
|
26
|
+
* `allowTrailingComma` is `true`, matching common JSONC conventions used in
|
|
27
|
+
* VS Code settings and `tsconfig.json`.
|
|
28
|
+
*
|
|
29
|
+
* @see {@link parseTree} — parse into an AST instead of a plain value
|
|
30
|
+
* @see {@link JsoncParseOptions} — available parse options
|
|
31
|
+
* @see {@link JsoncParseError} — the tagged error type on the failure channel
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* Basic parsing:
|
|
35
|
+
* ```ts
|
|
36
|
+
* import { Effect } from "effect";
|
|
37
|
+
* import { parse } from "jsonc-effect";
|
|
38
|
+
*
|
|
39
|
+
* const value = Effect.runSync(parse('{ "key": 42 }'));
|
|
40
|
+
* console.log(value); // { key: 42 }
|
|
41
|
+
* ```
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* Parsing with options:
|
|
45
|
+
* ```ts
|
|
46
|
+
* import { Effect } from "effect";
|
|
47
|
+
* import { parse } from "jsonc-effect";
|
|
48
|
+
*
|
|
49
|
+
* const value = Effect.runSync(
|
|
50
|
+
* parse('{ "key": 42 }', { disallowComments: true }),
|
|
51
|
+
* );
|
|
52
|
+
* ```
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* Error handling with `catchTag`:
|
|
56
|
+
* ```ts
|
|
57
|
+
* import { Effect } from "effect";
|
|
58
|
+
* import { parse } from "jsonc-effect";
|
|
59
|
+
*
|
|
60
|
+
* const program = parse("{ bad }").pipe(
|
|
61
|
+
* Effect.catchTag("JsoncParseError", (err) =>
|
|
62
|
+
* Effect.succeed({ fallback: true, errors: err.errors }),
|
|
63
|
+
* ),
|
|
64
|
+
* );
|
|
65
|
+
*
|
|
66
|
+
* const result = Effect.runSync(program);
|
|
67
|
+
* console.log(result);
|
|
68
|
+
* ```
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* Using `Effect.gen`:
|
|
72
|
+
* ```ts
|
|
73
|
+
* import { Effect } from "effect";
|
|
74
|
+
* import { parse } from "jsonc-effect";
|
|
75
|
+
*
|
|
76
|
+
* const program = Effect.gen(function* () {
|
|
77
|
+
* const config = yield* parse('{ "port": 3000 }');
|
|
78
|
+
* return config;
|
|
79
|
+
* });
|
|
80
|
+
*
|
|
81
|
+
* const result = Effect.runSync(program);
|
|
82
|
+
* console.log(result); // { port: 3000 }
|
|
83
|
+
* ```
|
|
84
|
+
*
|
|
85
|
+
* @privateRemarks
|
|
86
|
+
* Uses {@link createScanner} internally with a recursive descent parser.
|
|
87
|
+
* The scanner is created with `ignoreTrivia = false` so the parser can
|
|
88
|
+
* report comment-related errors when `disallowComments` is set.
|
|
89
|
+
*
|
|
90
|
+
* @public
|
|
91
|
+
*/
|
|
92
|
+
const parse = (text, options) => Effect.sync(() => parseInternal(text, options ?? {}, false)).pipe(Effect.flatMap(({ value, errors }) => {
|
|
93
|
+
if (errors.length > 0) return Effect.fail(new JsoncParseError({
|
|
94
|
+
errors,
|
|
95
|
+
text,
|
|
96
|
+
...options !== void 0 ? { options } : {}
|
|
97
|
+
}));
|
|
98
|
+
return Effect.succeed(value);
|
|
99
|
+
}));
|
|
100
|
+
/**
|
|
101
|
+
* Parse a JSONC string into an immutable AST.
|
|
102
|
+
*
|
|
103
|
+
* @param text - JSONC string to parse
|
|
104
|
+
* @param options - Optional {@link JsoncParseOptions} controlling comment and
|
|
105
|
+
* trailing-comma handling.
|
|
106
|
+
* @returns `Effect<Option<JsoncNode>, JsoncParseError>` — succeeds with
|
|
107
|
+
* `Option.some(root)` for non-empty documents or `Option.none()` when the
|
|
108
|
+
* input is empty (and `allowEmptyContent` is set).
|
|
109
|
+
*
|
|
110
|
+
* @remarks
|
|
111
|
+
* The returned AST is immutable and does **not** contain parent pointers, which
|
|
112
|
+
* keeps nodes safe to share across fibers. Use the AST navigation helpers
|
|
113
|
+
* ({@link findNode}, {@link getNodeValue}) to traverse and extract values from
|
|
114
|
+
* the tree.
|
|
115
|
+
*
|
|
116
|
+
* `Option.none()` is returned only when the document contains no value tokens
|
|
117
|
+
* and `allowEmptyContent` is enabled; otherwise an empty document produces a
|
|
118
|
+
* {@link JsoncParseError}.
|
|
119
|
+
*
|
|
120
|
+
* @see {@link parse} — parse into a plain JavaScript value instead of an AST
|
|
121
|
+
* @see {@link findNode} — locate a node by JSON path segments
|
|
122
|
+
* @see {@link getNodeValue} — extract the JavaScript value from a subtree
|
|
123
|
+
* @see {@link JsoncNode} — the AST node type
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* Parsing a JSONC string and navigating the tree:
|
|
127
|
+
* ```ts
|
|
128
|
+
* import { Effect, Option } from "effect";
|
|
129
|
+
* import { parseTree } from "jsonc-effect";
|
|
130
|
+
*
|
|
131
|
+
* const program = Effect.gen(function* () {
|
|
132
|
+
* const maybeRoot = yield* parseTree('{ "a": [1, 2, 3] }');
|
|
133
|
+
* if (Option.isSome(maybeRoot)) {
|
|
134
|
+
* const root = maybeRoot.value;
|
|
135
|
+
* console.log(root.type); // "object"
|
|
136
|
+
* console.log(root.children?.length); // 1
|
|
137
|
+
* }
|
|
138
|
+
* });
|
|
139
|
+
*
|
|
140
|
+
* Effect.runSync(program);
|
|
141
|
+
* ```
|
|
142
|
+
*
|
|
143
|
+
* @privateRemarks
|
|
144
|
+
* Internally the parser builds a mutable tree using `MutableJsoncNode` and
|
|
145
|
+
* casts to the readonly `JsoncNode` on output.
|
|
146
|
+
*
|
|
147
|
+
* @public
|
|
148
|
+
*/
|
|
149
|
+
const parseTree = (text, options) => Effect.sync(() => parseInternal(text, options ?? {}, true)).pipe(Effect.flatMap(({ root, errors }) => {
|
|
150
|
+
if (errors.length > 0) return Effect.fail(new JsoncParseError({
|
|
151
|
+
errors,
|
|
152
|
+
text,
|
|
153
|
+
...options !== void 0 ? { options } : {}
|
|
154
|
+
}));
|
|
155
|
+
return Effect.succeed(root ? Option.some(root) : Option.none());
|
|
156
|
+
}));
|
|
157
|
+
/**
|
|
158
|
+
* Remove all comments from JSONC text, producing valid JSON.
|
|
159
|
+
*
|
|
160
|
+
* @param text - JSONC string to strip comments from
|
|
161
|
+
* @param replaceCh - Optional single character used to replace each character of
|
|
162
|
+
* every comment. When provided, the output has the **same length** as the
|
|
163
|
+
* input so that all offsets are preserved (line breaks inside block comments
|
|
164
|
+
* are kept as-is).
|
|
165
|
+
* @returns `Effect<string>` — the text with all comments removed (or replaced).
|
|
166
|
+
*
|
|
167
|
+
* @remarks
|
|
168
|
+
* When `replaceCh` is omitted the comment text is simply deleted, which means
|
|
169
|
+
* character offsets in the output no longer match the original document. Pass a
|
|
170
|
+
* space (`" "`) as `replaceCh` to keep offsets stable — this is useful when you
|
|
171
|
+
* need to correlate positions between the original JSONC and the stripped JSON.
|
|
172
|
+
*
|
|
173
|
+
* @see {@link parse} — parse JSONC directly without a stripping step
|
|
174
|
+
*
|
|
175
|
+
* @example
|
|
176
|
+
* Basic comment stripping:
|
|
177
|
+
* ```ts
|
|
178
|
+
* import { Effect } from "effect";
|
|
179
|
+
* import { stripComments } from "jsonc-effect";
|
|
180
|
+
*
|
|
181
|
+
* const json = Effect.runSync(
|
|
182
|
+
* stripComments('{ "a": 1 // comment\n}'),
|
|
183
|
+
* );
|
|
184
|
+
* console.log(json); // '{ "a": 1 \n}'
|
|
185
|
+
* ```
|
|
186
|
+
*
|
|
187
|
+
* @example
|
|
188
|
+
* Using a replacement character to preserve offsets:
|
|
189
|
+
* ```ts
|
|
190
|
+
* import { Effect } from "effect";
|
|
191
|
+
* import { stripComments } from "jsonc-effect";
|
|
192
|
+
*
|
|
193
|
+
* const json = Effect.runSync(
|
|
194
|
+
* stripComments('{ "a": 1 // comment\n}', " "),
|
|
195
|
+
* );
|
|
196
|
+
* console.log(json.length === '{ "a": 1 // comment\n}'.length); // true
|
|
197
|
+
* ```
|
|
198
|
+
*
|
|
199
|
+
* @public
|
|
200
|
+
*/
|
|
201
|
+
const stripComments = (text, replaceCh) => Effect.sync(() => {
|
|
202
|
+
const scanner = createScanner(text);
|
|
203
|
+
const parts = [];
|
|
204
|
+
let lastOffset = 0;
|
|
205
|
+
let kind;
|
|
206
|
+
do {
|
|
207
|
+
kind = scanner.scan();
|
|
208
|
+
const offset = scanner.getTokenOffset();
|
|
209
|
+
const length = scanner.getTokenLength();
|
|
210
|
+
if (kind === "LineComment" || kind === "BlockComment") {
|
|
211
|
+
if (lastOffset < offset) parts.push(text.substring(lastOffset, offset));
|
|
212
|
+
if (replaceCh !== void 0) for (let i = 0; i < length; i++) {
|
|
213
|
+
const ch = text.charCodeAt(offset + i);
|
|
214
|
+
parts.push(ch === 10 || ch === 13 ? text[offset + i] : replaceCh);
|
|
215
|
+
}
|
|
216
|
+
lastOffset = offset + length;
|
|
217
|
+
}
|
|
218
|
+
} while (kind !== "EOF");
|
|
219
|
+
if (lastOffset < text.length) parts.push(text.substring(lastOffset));
|
|
220
|
+
return parts.join("");
|
|
221
|
+
});
|
|
222
|
+
function parseInternal(text, options, buildTree) {
|
|
223
|
+
const scanner = createScanner(text, false);
|
|
224
|
+
const errors = [];
|
|
225
|
+
const disallowComments = options.disallowComments ?? false;
|
|
226
|
+
const allowTrailingComma = options.allowTrailingComma ?? true;
|
|
227
|
+
const allowEmptyContent = options.allowEmptyContent ?? false;
|
|
228
|
+
let currentToken = "Unknown";
|
|
229
|
+
function token() {
|
|
230
|
+
return currentToken;
|
|
231
|
+
}
|
|
232
|
+
function scanNext() {
|
|
233
|
+
for (;;) {
|
|
234
|
+
currentToken = scanner.scan();
|
|
235
|
+
switch (scanner.getTokenError()) {
|
|
236
|
+
case "InvalidUnicode":
|
|
237
|
+
handleError("InvalidUnicode");
|
|
238
|
+
break;
|
|
239
|
+
case "InvalidEscapeCharacter":
|
|
240
|
+
handleError("InvalidEscapeCharacter");
|
|
241
|
+
break;
|
|
242
|
+
case "UnexpectedEndOfNumber":
|
|
243
|
+
handleError("InvalidNumberFormat");
|
|
244
|
+
break;
|
|
245
|
+
case "UnexpectedEndOfComment":
|
|
246
|
+
handleError("UnexpectedEndOfComment");
|
|
247
|
+
break;
|
|
248
|
+
case "UnexpectedEndOfString":
|
|
249
|
+
handleError("UnexpectedEndOfString");
|
|
250
|
+
break;
|
|
251
|
+
case "InvalidCharacter":
|
|
252
|
+
handleError("InvalidCharacter");
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
switch (currentToken) {
|
|
256
|
+
case "LineComment":
|
|
257
|
+
case "BlockComment":
|
|
258
|
+
if (disallowComments) handleError("InvalidCommentToken");
|
|
259
|
+
break;
|
|
260
|
+
case "Trivia":
|
|
261
|
+
case "LineBreak": break;
|
|
262
|
+
default: return currentToken;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
function handleError(code, skipUntilAfter = [], skipUntil = []) {
|
|
267
|
+
errors.push(new JsoncParseErrorDetail({
|
|
268
|
+
code,
|
|
269
|
+
message: formatError(code, scanner.getTokenOffset()),
|
|
270
|
+
offset: scanner.getTokenOffset(),
|
|
271
|
+
length: scanner.getTokenLength(),
|
|
272
|
+
startLine: scanner.getTokenStartLine(),
|
|
273
|
+
startCharacter: scanner.getTokenStartCharacter()
|
|
274
|
+
}));
|
|
275
|
+
if (skipUntilAfter.length > 0 || skipUntil.length > 0) {
|
|
276
|
+
let t = token();
|
|
277
|
+
while (t !== "EOF") {
|
|
278
|
+
if (skipUntilAfter.includes(t)) {
|
|
279
|
+
scanNext();
|
|
280
|
+
break;
|
|
281
|
+
}
|
|
282
|
+
if (skipUntil.includes(t)) break;
|
|
283
|
+
t = scanNext();
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
function parseValue() {
|
|
288
|
+
switch (token()) {
|
|
289
|
+
case "OpenBracket": return parseArray();
|
|
290
|
+
case "OpenBrace": return parseObject();
|
|
291
|
+
case "String": return parseString();
|
|
292
|
+
case "Number": return parseNumber();
|
|
293
|
+
case "True":
|
|
294
|
+
scanNext();
|
|
295
|
+
return true;
|
|
296
|
+
case "False":
|
|
297
|
+
scanNext();
|
|
298
|
+
return false;
|
|
299
|
+
case "Null":
|
|
300
|
+
scanNext();
|
|
301
|
+
return null;
|
|
302
|
+
default: return;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
function parseString() {
|
|
306
|
+
const value = scanner.getTokenValue();
|
|
307
|
+
scanNext();
|
|
308
|
+
return value;
|
|
309
|
+
}
|
|
310
|
+
function parseNumber() {
|
|
311
|
+
const value = Number.parseFloat(scanner.getTokenValue());
|
|
312
|
+
scanNext();
|
|
313
|
+
return value;
|
|
314
|
+
}
|
|
315
|
+
function parseArray() {
|
|
316
|
+
scanNext();
|
|
317
|
+
const arr = [];
|
|
318
|
+
let needsComma = false;
|
|
319
|
+
while (token() !== "CloseBracket" && token() !== "EOF") {
|
|
320
|
+
if (token() === "Comma") {
|
|
321
|
+
if (!needsComma) handleError("ValueExpected");
|
|
322
|
+
scanNext();
|
|
323
|
+
if (token() === "CloseBracket" && allowTrailingComma) break;
|
|
324
|
+
} else if (needsComma) handleError("CommaExpected");
|
|
325
|
+
const value = parseValue();
|
|
326
|
+
if (value === void 0) handleError("ValueExpected", [], ["CloseBracket", "Comma"]);
|
|
327
|
+
else arr.push(value);
|
|
328
|
+
needsComma = true;
|
|
329
|
+
}
|
|
330
|
+
if (token() !== "CloseBracket") handleError("CloseBracketExpected");
|
|
331
|
+
else scanNext();
|
|
332
|
+
return arr;
|
|
333
|
+
}
|
|
334
|
+
function parseObject() {
|
|
335
|
+
scanNext();
|
|
336
|
+
const obj = {};
|
|
337
|
+
let needsComma = false;
|
|
338
|
+
while (token() !== "CloseBrace" && token() !== "EOF") {
|
|
339
|
+
if (token() === "Comma") {
|
|
340
|
+
if (!needsComma) handleError("PropertyNameExpected");
|
|
341
|
+
scanNext();
|
|
342
|
+
if (token() === "CloseBrace" && allowTrailingComma) break;
|
|
343
|
+
} else if (needsComma) handleError("CommaExpected");
|
|
344
|
+
if (token() !== "String") {
|
|
345
|
+
handleError("PropertyNameExpected", [], ["CloseBrace", "Comma"]);
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
const key = scanner.getTokenValue();
|
|
349
|
+
scanNext();
|
|
350
|
+
if (token() !== "Colon") {
|
|
351
|
+
handleError("ColonExpected", [], ["CloseBrace", "Comma"]);
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
scanNext();
|
|
355
|
+
const value = parseValue();
|
|
356
|
+
if (value === void 0) handleError("ValueExpected", [], ["CloseBrace", "Comma"]);
|
|
357
|
+
else obj[key] = value;
|
|
358
|
+
needsComma = true;
|
|
359
|
+
}
|
|
360
|
+
if (token() !== "CloseBrace") handleError("CloseBraceExpected");
|
|
361
|
+
else scanNext();
|
|
362
|
+
return obj;
|
|
363
|
+
}
|
|
364
|
+
function parseValueTree() {
|
|
365
|
+
switch (token()) {
|
|
366
|
+
case "OpenBracket": return parseArrayTree();
|
|
367
|
+
case "OpenBrace": return parseObjectTree();
|
|
368
|
+
case "String": {
|
|
369
|
+
const node = {
|
|
370
|
+
type: "string",
|
|
371
|
+
offset: scanner.getTokenOffset(),
|
|
372
|
+
length: 0,
|
|
373
|
+
value: scanner.getTokenValue()
|
|
374
|
+
};
|
|
375
|
+
scanNext();
|
|
376
|
+
node.length = scanner.getTokenOffset() - node.offset;
|
|
377
|
+
return node;
|
|
378
|
+
}
|
|
379
|
+
case "Number": {
|
|
380
|
+
const node = {
|
|
381
|
+
type: "number",
|
|
382
|
+
offset: scanner.getTokenOffset(),
|
|
383
|
+
length: 0,
|
|
384
|
+
value: Number.parseFloat(scanner.getTokenValue())
|
|
385
|
+
};
|
|
386
|
+
scanNext();
|
|
387
|
+
node.length = scanner.getTokenOffset() - node.offset;
|
|
388
|
+
return node;
|
|
389
|
+
}
|
|
390
|
+
case "True": {
|
|
391
|
+
const node = {
|
|
392
|
+
type: "boolean",
|
|
393
|
+
offset: scanner.getTokenOffset(),
|
|
394
|
+
length: 0,
|
|
395
|
+
value: true
|
|
396
|
+
};
|
|
397
|
+
scanNext();
|
|
398
|
+
node.length = scanner.getTokenOffset() - node.offset;
|
|
399
|
+
return node;
|
|
400
|
+
}
|
|
401
|
+
case "False": {
|
|
402
|
+
const node = {
|
|
403
|
+
type: "boolean",
|
|
404
|
+
offset: scanner.getTokenOffset(),
|
|
405
|
+
length: 0,
|
|
406
|
+
value: false
|
|
407
|
+
};
|
|
408
|
+
scanNext();
|
|
409
|
+
node.length = scanner.getTokenOffset() - node.offset;
|
|
410
|
+
return node;
|
|
411
|
+
}
|
|
412
|
+
case "Null": {
|
|
413
|
+
const node = {
|
|
414
|
+
type: "null",
|
|
415
|
+
offset: scanner.getTokenOffset(),
|
|
416
|
+
length: 0,
|
|
417
|
+
value: null
|
|
418
|
+
};
|
|
419
|
+
scanNext();
|
|
420
|
+
node.length = scanner.getTokenOffset() - node.offset;
|
|
421
|
+
return node;
|
|
422
|
+
}
|
|
423
|
+
default: return;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
function parseArrayTree() {
|
|
427
|
+
const node = {
|
|
428
|
+
type: "array",
|
|
429
|
+
offset: scanner.getTokenOffset(),
|
|
430
|
+
length: 0,
|
|
431
|
+
children: []
|
|
432
|
+
};
|
|
433
|
+
scanNext();
|
|
434
|
+
let needsComma = false;
|
|
435
|
+
while (token() !== "CloseBracket" && token() !== "EOF") {
|
|
436
|
+
if (token() === "Comma") {
|
|
437
|
+
if (!needsComma) handleError("ValueExpected");
|
|
438
|
+
scanNext();
|
|
439
|
+
if (token() === "CloseBracket" && allowTrailingComma) break;
|
|
440
|
+
} else if (needsComma) handleError("CommaExpected");
|
|
441
|
+
const child = parseValueTree();
|
|
442
|
+
if (child) node.children.push(child);
|
|
443
|
+
else handleError("ValueExpected", [], ["CloseBracket", "Comma"]);
|
|
444
|
+
needsComma = true;
|
|
445
|
+
}
|
|
446
|
+
if (token() !== "CloseBracket") handleError("CloseBracketExpected");
|
|
447
|
+
else scanNext();
|
|
448
|
+
node.length = scanner.getTokenOffset() - node.offset;
|
|
449
|
+
return node;
|
|
450
|
+
}
|
|
451
|
+
function parseObjectTree() {
|
|
452
|
+
const node = {
|
|
453
|
+
type: "object",
|
|
454
|
+
offset: scanner.getTokenOffset(),
|
|
455
|
+
length: 0,
|
|
456
|
+
children: []
|
|
457
|
+
};
|
|
458
|
+
scanNext();
|
|
459
|
+
let needsComma = false;
|
|
460
|
+
while (token() !== "CloseBrace" && token() !== "EOF") {
|
|
461
|
+
if (token() === "Comma") {
|
|
462
|
+
if (!needsComma) handleError("PropertyNameExpected");
|
|
463
|
+
scanNext();
|
|
464
|
+
if (token() === "CloseBrace" && allowTrailingComma) break;
|
|
465
|
+
} else if (needsComma) handleError("CommaExpected");
|
|
466
|
+
if (token() !== "String") {
|
|
467
|
+
handleError("PropertyNameExpected", [], ["CloseBrace", "Comma"]);
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
const property = {
|
|
471
|
+
type: "property",
|
|
472
|
+
offset: scanner.getTokenOffset(),
|
|
473
|
+
length: 0,
|
|
474
|
+
children: []
|
|
475
|
+
};
|
|
476
|
+
const keyNode = {
|
|
477
|
+
type: "string",
|
|
478
|
+
offset: scanner.getTokenOffset(),
|
|
479
|
+
length: 0,
|
|
480
|
+
value: scanner.getTokenValue()
|
|
481
|
+
};
|
|
482
|
+
scanNext();
|
|
483
|
+
keyNode.length = scanner.getTokenOffset() - keyNode.offset;
|
|
484
|
+
property.children.push(keyNode);
|
|
485
|
+
if (token() !== "Colon") {
|
|
486
|
+
handleError("ColonExpected", [], ["CloseBrace", "Comma"]);
|
|
487
|
+
property.length = scanner.getTokenOffset() - property.offset;
|
|
488
|
+
node.children.push(property);
|
|
489
|
+
continue;
|
|
490
|
+
}
|
|
491
|
+
property.colonOffset = scanner.getTokenOffset();
|
|
492
|
+
scanNext();
|
|
493
|
+
const valueNode = parseValueTree();
|
|
494
|
+
if (valueNode) property.children.push(valueNode);
|
|
495
|
+
else handleError("ValueExpected", [], ["CloseBrace", "Comma"]);
|
|
496
|
+
property.length = scanner.getTokenOffset() - property.offset;
|
|
497
|
+
node.children.push(property);
|
|
498
|
+
needsComma = true;
|
|
499
|
+
}
|
|
500
|
+
if (token() !== "CloseBrace") handleError("CloseBraceExpected");
|
|
501
|
+
else scanNext();
|
|
502
|
+
node.length = scanner.getTokenOffset() - node.offset;
|
|
503
|
+
return node;
|
|
504
|
+
}
|
|
505
|
+
scanNext();
|
|
506
|
+
if (buildTree) {
|
|
507
|
+
const root = parseValueTree();
|
|
508
|
+
if (token() !== "EOF") handleError("EndOfFileExpected");
|
|
509
|
+
if (!root && !allowEmptyContent) handleError("ValueExpected");
|
|
510
|
+
return {
|
|
511
|
+
value: void 0,
|
|
512
|
+
root,
|
|
513
|
+
errors
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
const value = parseValue();
|
|
517
|
+
if (token() !== "EOF") handleError("EndOfFileExpected");
|
|
518
|
+
if (value === void 0 && !allowEmptyContent) handleError("ValueExpected");
|
|
519
|
+
return {
|
|
520
|
+
value,
|
|
521
|
+
root: void 0,
|
|
522
|
+
errors
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
function formatError(code, offset) {
|
|
526
|
+
switch (code) {
|
|
527
|
+
case "InvalidSymbol": return `Invalid symbol at offset ${offset}`;
|
|
528
|
+
case "InvalidNumberFormat": return `Invalid number format at offset ${offset}`;
|
|
529
|
+
case "PropertyNameExpected": return `Property name expected at offset ${offset}`;
|
|
530
|
+
case "ValueExpected": return `Value expected at offset ${offset}`;
|
|
531
|
+
case "ColonExpected": return `Colon expected at offset ${offset}`;
|
|
532
|
+
case "CommaExpected": return `Comma expected at offset ${offset}`;
|
|
533
|
+
case "CloseBraceExpected": return `Close brace expected at offset ${offset}`;
|
|
534
|
+
case "CloseBracketExpected": return `Close bracket expected at offset ${offset}`;
|
|
535
|
+
case "EndOfFileExpected": return `End of file expected at offset ${offset}`;
|
|
536
|
+
case "InvalidCommentToken": return `Comments not allowed at offset ${offset}`;
|
|
537
|
+
case "UnexpectedEndOfComment": return `Unexpected end of comment at offset ${offset}`;
|
|
538
|
+
case "UnexpectedEndOfString": return `Unexpected end of string at offset ${offset}`;
|
|
539
|
+
case "UnexpectedEndOfNumber": return `Unexpected end of number at offset ${offset}`;
|
|
540
|
+
case "InvalidUnicode": return `Invalid unicode escape at offset ${offset}`;
|
|
541
|
+
case "InvalidEscapeCharacter": return `Invalid escape character at offset ${offset}`;
|
|
542
|
+
case "InvalidCharacter": return `Invalid character at offset ${offset}`;
|
|
543
|
+
default: return `Parse error at offset ${offset}`;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
//#endregion
|
|
548
|
+
export { parse, parseTree, stripComments };
|