@soundscript/soundscript 0.1.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.
Files changed (80) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +10 -0
  3. package/async.d.ts +81 -0
  4. package/async.js +214 -0
  5. package/async.js.map +1 -0
  6. package/bin/soundscript.js +54 -0
  7. package/codec.d.ts +31 -0
  8. package/codec.js +31 -0
  9. package/codec.js.map +1 -0
  10. package/compare.d.ts +28 -0
  11. package/compare.js +121 -0
  12. package/compare.js.map +1 -0
  13. package/decode.d.ts +84 -0
  14. package/decode.js +249 -0
  15. package/decode.js.map +1 -0
  16. package/encode.d.ts +98 -0
  17. package/encode.js +128 -0
  18. package/encode.js.map +1 -0
  19. package/experimental/component.d.ts +40 -0
  20. package/experimental/component.js +46 -0
  21. package/experimental/component.js.map +1 -0
  22. package/experimental/css.d.ts +16 -0
  23. package/experimental/css.js +10 -0
  24. package/experimental/css.js.map +1 -0
  25. package/experimental/debug.d.ts +2 -0
  26. package/experimental/debug.js +10 -0
  27. package/experimental/debug.js.map +1 -0
  28. package/experimental/graphql.d.ts +16 -0
  29. package/experimental/graphql.js +10 -0
  30. package/experimental/graphql.js.map +1 -0
  31. package/experimental/sql.d.ts +22 -0
  32. package/experimental/sql.js +24 -0
  33. package/experimental/sql.js.map +1 -0
  34. package/failures.d.ts +23 -0
  35. package/failures.js +42 -0
  36. package/failures.js.map +1 -0
  37. package/hash.d.ts +34 -0
  38. package/hash.js +116 -0
  39. package/hash.js.map +1 -0
  40. package/hkt.d.ts +40 -0
  41. package/hkt.js +4 -0
  42. package/hkt.js.map +1 -0
  43. package/index.d.ts +9 -0
  44. package/index.js +16 -0
  45. package/index.js.map +1 -0
  46. package/json.d.ts +98 -0
  47. package/json.js +638 -0
  48. package/json.js.map +1 -0
  49. package/match.d.ts +11 -0
  50. package/match.js +14 -0
  51. package/match.js.map +1 -0
  52. package/package.json +153 -0
  53. package/result.d.ts +52 -0
  54. package/result.js +104 -0
  55. package/result.js.map +1 -0
  56. package/soundscript/async.sts +315 -0
  57. package/soundscript/codec.sts +75 -0
  58. package/soundscript/compare.sts +159 -0
  59. package/soundscript/decode.sts +382 -0
  60. package/soundscript/encode.sts +254 -0
  61. package/soundscript/experimental/component.sts +69 -0
  62. package/soundscript/experimental/css.sts +28 -0
  63. package/soundscript/experimental/debug.sts +10 -0
  64. package/soundscript/experimental/graphql.sts +28 -0
  65. package/soundscript/experimental/sql.sts +53 -0
  66. package/soundscript/failures.sts +64 -0
  67. package/soundscript/hash.sts +196 -0
  68. package/soundscript/hkt.sts +41 -0
  69. package/soundscript/index.sts +23 -0
  70. package/soundscript/json.sts +824 -0
  71. package/soundscript/match.sts +26 -0
  72. package/soundscript/result.sts +179 -0
  73. package/soundscript/thunk.sts +15 -0
  74. package/soundscript/typeclasses.sts +167 -0
  75. package/thunk.d.ts +2 -0
  76. package/thunk.js +10 -0
  77. package/thunk.js.map +1 -0
  78. package/typeclasses.d.ts +57 -0
  79. package/typeclasses.js +78 -0
  80. package/typeclasses.js.map +1 -0
@@ -0,0 +1,824 @@
1
+ import type { Decoder } from '@soundscript/soundscript/decode';
2
+ import type { Encoder } from '@soundscript/soundscript/encode';
3
+ import { Failure } from '@soundscript/soundscript/failures';
4
+ import { isErr, type Result, resultOf } from '@soundscript/soundscript/result';
5
+
6
+ export type JsonArray = JsonValue[];
7
+ export type JsonObject = {
8
+ [key: string]: JsonValue;
9
+ };
10
+ export type JsonValue = string | number | boolean | null | JsonObject | JsonArray;
11
+ type LosslessJsonArray = LosslessJsonValue[];
12
+ type LosslessJsonObject = {
13
+ [key: string]: LosslessJsonValue;
14
+ };
15
+ export type LosslessJsonValue =
16
+ | string
17
+ | number
18
+ | bigint
19
+ | boolean
20
+ | null
21
+ | LosslessJsonObject
22
+ | LosslessJsonArray;
23
+
24
+ export type JsonLikeArray = JsonLikeValue[];
25
+ export type JsonLikeObject = {
26
+ [key: string]: JsonLikeValue;
27
+ };
28
+ export type JsonLikeValue =
29
+ | string
30
+ | number
31
+ | boolean
32
+ | bigint
33
+ | null
34
+ | undefined
35
+ | JsonLikeObject
36
+ | JsonLikeArray;
37
+
38
+ export interface JsonParseOptions {
39
+ int64?: 'default' | 'lossless';
40
+ }
41
+
42
+ export type JsonStringifyBigintMode = 'number' | 'reject' | 'string';
43
+
44
+ export interface JsonStringifyOptions {
45
+ int64?: 'default' | 'string' | 'lossless';
46
+ readonly bigint?: JsonStringifyBigintMode;
47
+ }
48
+
49
+ const MAX_SAFE_INTEGER_BIGINT = BigInt(Number.MAX_SAFE_INTEGER);
50
+ const MIN_SAFE_INTEGER_BIGINT = BigInt(Number.MIN_SAFE_INTEGER);
51
+
52
+ export class JsonParseFailure extends Failure {
53
+ constructor(cause?: unknown) {
54
+ super('Failed to parse JSON.', { cause });
55
+ }
56
+ }
57
+
58
+ export class JsonStringifyFailure extends Failure {
59
+ constructor(cause?: unknown) {
60
+ super('Failed to stringify JSON.', { cause });
61
+ }
62
+ }
63
+
64
+ export function parseJson(text: string): Result<JsonValue, JsonParseFailure>;
65
+ export function parseJson(
66
+ text: string,
67
+ options: JsonParseOptions & { int64: 'lossless' },
68
+ ): Result<LosslessJsonValue, JsonParseFailure>;
69
+ export function parseJson(
70
+ text: string,
71
+ options: JsonParseOptions = {},
72
+ ): Result<JsonValue | LosslessJsonValue, JsonParseFailure> {
73
+ return resultOf(
74
+ () => options.int64 === 'lossless' ? parseLosslessJson(text) : JSON.parse(text),
75
+ (cause) => new JsonParseFailure(cause),
76
+ );
77
+ }
78
+
79
+ export function stringifyJson(value: JsonValue): Result<string, JsonStringifyFailure>;
80
+ export function stringifyJson(
81
+ value: LosslessJsonValue,
82
+ options: JsonStringifyOptions & { int64: 'string' | 'lossless' },
83
+ ): Result<string, JsonStringifyFailure>;
84
+ export function stringifyJson(
85
+ value: JsonValue | LosslessJsonValue,
86
+ options: JsonStringifyOptions = {},
87
+ ): Result<string, JsonStringifyFailure> {
88
+ return resultOf(
89
+ () => {
90
+ if (options.int64 === 'string') {
91
+ return stringifyJsonWithInt64Mode(value as LosslessJsonValue, 'string');
92
+ }
93
+ if (options.int64 === 'lossless') {
94
+ return stringifyJsonWithInt64Mode(value as LosslessJsonValue, 'lossless');
95
+ }
96
+
97
+ const encoded = JSON.stringify(value);
98
+ if (encoded === undefined) {
99
+ throw new TypeError(
100
+ 'JSON.stringify returned undefined for a JsonValue input.',
101
+ );
102
+ }
103
+ return encoded;
104
+ },
105
+ (cause) => new JsonStringifyFailure(cause),
106
+ );
107
+ }
108
+
109
+ export function parseAndDecode<T, E>(
110
+ text: string,
111
+ decoder: Decoder<T, E>,
112
+ ): Result<T, JsonParseFailure | E> {
113
+ const parsed = parseJson(text);
114
+ return isErr(parsed) ? parsed : decoder.decode(parsed.value);
115
+ }
116
+
117
+ export function encodeAndStringify<T, E>(
118
+ value: T,
119
+ encoder: Encoder<T, JsonValue, E>,
120
+ ): Result<string, E | JsonStringifyFailure> {
121
+ const encoded = encoder.encode(value);
122
+ return isErr(encoded) ? encoded : stringifyJson(encoded.value);
123
+ }
124
+
125
+ export function isJsonValue(value: unknown): value is JsonValue {
126
+ return isJsonValueInternal(value, new Set<object>());
127
+ }
128
+
129
+ export function parseJsonLike(text: string): Result<JsonLikeValue, JsonParseFailure> {
130
+ return resultOf(
131
+ () => {
132
+ const parser = new JsonLikeParser(text);
133
+ const value = parser.parseValue();
134
+ parser.finish();
135
+ return value;
136
+ },
137
+ (cause) => new JsonParseFailure(cause),
138
+ );
139
+ }
140
+
141
+ export function stringifyJsonLike(
142
+ value: JsonLikeValue,
143
+ options: JsonStringifyOptions = {},
144
+ ): Result<string, JsonStringifyFailure> {
145
+ return resultOf(
146
+ () => {
147
+ const encoded = stringifyJsonLikeInternal(
148
+ value,
149
+ new Set<object>(),
150
+ options.bigint ?? 'reject',
151
+ 'root',
152
+ );
153
+ if (encoded === undefined) {
154
+ throw new TypeError('JSON-like stringification produced no top-level value.');
155
+ }
156
+ return encoded;
157
+ },
158
+ (cause) => new JsonStringifyFailure(cause),
159
+ );
160
+ }
161
+
162
+ export function decodeJson<T, E>(
163
+ text: string,
164
+ decoder: Decoder<T, E>,
165
+ ): Result<T, E | JsonParseFailure> {
166
+ const parsed = parseJsonLike(text);
167
+ return isErr(parsed) ? parsed : decoder.decode(parsed.value);
168
+ }
169
+
170
+ export function encodeJson<T, E>(
171
+ value: T,
172
+ encoder: Encoder<T, JsonLikeValue, E>,
173
+ options: JsonStringifyOptions = {},
174
+ ): Result<string, E | JsonStringifyFailure> {
175
+ const encoded = encoder.encode(value);
176
+ return isErr(encoded) ? encoded : stringifyJsonLike(encoded.value, options);
177
+ }
178
+
179
+ export function isJsonLikeValue(value: unknown): value is JsonLikeValue {
180
+ return isJsonLikeValueInternal(value, new Set<object>());
181
+ }
182
+
183
+ function stringifyJsonWithInt64Mode(
184
+ value: LosslessJsonValue,
185
+ int64Mode: 'string' | 'lossless',
186
+ ): string {
187
+ return stringifyJsonWithInt64ModeInternal(value, int64Mode, new Set<object>());
188
+ }
189
+
190
+ function stringifyJsonWithInt64ModeInternal(
191
+ value: LosslessJsonValue,
192
+ int64Mode: 'string' | 'lossless',
193
+ visited: Set<object>,
194
+ ): string {
195
+ switch (typeof value) {
196
+ case 'string':
197
+ return JSON.stringify(value);
198
+ case 'number': {
199
+ const encoded = JSON.stringify(value);
200
+ if (encoded === undefined) {
201
+ throw new TypeError('JSON.stringify returned undefined for a numeric input.');
202
+ }
203
+ return encoded;
204
+ }
205
+ case 'bigint':
206
+ return int64Mode === 'string'
207
+ ? JSON.stringify(value.toString())
208
+ : value.toString();
209
+ case 'boolean':
210
+ return value ? 'true' : 'false';
211
+ case 'object':
212
+ if (value === null) {
213
+ return 'null';
214
+ }
215
+
216
+ if (visited.has(value)) {
217
+ throw new TypeError('Could not stringify cyclic JSON value.');
218
+ }
219
+
220
+ visited.add(value);
221
+ try {
222
+ if (Array.isArray(value)) {
223
+ return `[${value.map((entry) => stringifyJsonWithInt64ModeInternal(entry, int64Mode, visited)).join(',')}]`;
224
+ }
225
+
226
+ const fields = Object.keys(value).map((key) =>
227
+ `${JSON.stringify(key)}:${stringifyJsonWithInt64ModeInternal(value[key]!, int64Mode, visited)}`
228
+ );
229
+ return `{${fields.join(',')}}`;
230
+ } finally {
231
+ visited.delete(value);
232
+ }
233
+ default:
234
+ throw new TypeError(`Unsupported JSON value kind: ${typeof value}`);
235
+ }
236
+ }
237
+
238
+ function parseLosslessJson(text: string): LosslessJsonValue {
239
+ const parser = new LosslessJsonParser(text);
240
+ const value = parser.parseValue();
241
+ parser.skipWhitespace();
242
+ if (!parser.isAtEnd()) {
243
+ parser.fail('Unexpected trailing characters.');
244
+ }
245
+ return value;
246
+ }
247
+
248
+ function isJsonValueInternal(value: unknown, visited: Set<object>): value is JsonValue {
249
+ switch (typeof value) {
250
+ case 'string':
251
+ case 'number':
252
+ case 'boolean':
253
+ return true;
254
+ case 'object':
255
+ if (value === null) {
256
+ return true;
257
+ }
258
+
259
+ if (visited.has(value)) {
260
+ return true;
261
+ }
262
+
263
+ visited.add(value);
264
+ try {
265
+ if (Array.isArray(value)) {
266
+ return value.every((entry) => isJsonValueInternal(entry, visited));
267
+ }
268
+
269
+ for (const key of Object.keys(value)) {
270
+ if (!isJsonValueInternal((value as Record<string, unknown>)[key], visited)) {
271
+ return false;
272
+ }
273
+ }
274
+
275
+ return true;
276
+ } finally {
277
+ visited.delete(value);
278
+ }
279
+ default:
280
+ return false;
281
+ }
282
+ }
283
+
284
+ function isJsonLikeValueInternal(
285
+ value: unknown,
286
+ visited: Set<object>,
287
+ ): value is JsonLikeValue {
288
+ switch (typeof value) {
289
+ case 'string':
290
+ case 'number':
291
+ case 'boolean':
292
+ case 'bigint':
293
+ case 'undefined':
294
+ return true;
295
+ case 'object':
296
+ if (value === null) {
297
+ return true;
298
+ }
299
+
300
+ if (visited.has(value)) {
301
+ return true;
302
+ }
303
+
304
+ visited.add(value);
305
+ try {
306
+ if (Array.isArray(value)) {
307
+ return value.every((entry) => isJsonLikeValueInternal(entry, visited));
308
+ }
309
+
310
+ for (const key of Object.keys(value)) {
311
+ if (!isJsonLikeValueInternal((value as Record<string, unknown>)[key], visited)) {
312
+ return false;
313
+ }
314
+ }
315
+
316
+ return true;
317
+ } finally {
318
+ visited.delete(value);
319
+ }
320
+ default:
321
+ return false;
322
+ }
323
+ }
324
+
325
+ function stringifyJsonLikeInternal(
326
+ value: JsonLikeValue,
327
+ visited: Set<object>,
328
+ bigintMode: JsonStringifyBigintMode,
329
+ position: 'array' | 'object' | 'root',
330
+ ): string | undefined {
331
+ switch (typeof value) {
332
+ case 'string':
333
+ return JSON.stringify(value);
334
+ case 'number':
335
+ return Number.isFinite(value) ? JSON.stringify(value) : 'null';
336
+ case 'boolean':
337
+ return value ? 'true' : 'false';
338
+ case 'bigint':
339
+ switch (bigintMode) {
340
+ case 'string':
341
+ return JSON.stringify(value.toString());
342
+ case 'number':
343
+ return value.toString();
344
+ case 'reject':
345
+ throw new TypeError('Encountered bigint while stringifying JSON-like data.');
346
+ }
347
+ case 'undefined':
348
+ return position === 'array' ? 'null' : undefined;
349
+ case 'object':
350
+ if (value === null) {
351
+ return 'null';
352
+ }
353
+
354
+ if (visited.has(value)) {
355
+ throw new TypeError('Converting circular structure to JSON-like text.');
356
+ }
357
+
358
+ visited.add(value);
359
+ try {
360
+ if (Array.isArray(value)) {
361
+ return `[${value.map((entry) =>
362
+ stringifyJsonLikeInternal(entry, visited, bigintMode, 'array') ?? 'null'
363
+ ).join(',')}]`;
364
+ }
365
+
366
+ const encodedProperties: string[] = [];
367
+ for (const key of Object.keys(value)) {
368
+ const encodedValue = stringifyJsonLikeInternal(
369
+ (value as Record<string, JsonLikeValue>)[key],
370
+ visited,
371
+ bigintMode,
372
+ 'object',
373
+ );
374
+ if (encodedValue === undefined) {
375
+ continue;
376
+ }
377
+ encodedProperties.push(`${JSON.stringify(key)}:${encodedValue}`);
378
+ }
379
+ return `{${encodedProperties.join(',')}}`;
380
+ } finally {
381
+ visited.delete(value);
382
+ }
383
+ default:
384
+ throw new TypeError('Encountered an unsupported JSON-like value.');
385
+ }
386
+ }
387
+
388
+ class LosslessJsonParser {
389
+ private readonly text: string;
390
+ private index = 0;
391
+
392
+ constructor(text: string) {
393
+ this.text = text;
394
+ }
395
+
396
+ fail(message: string): never {
397
+ throw new SyntaxError(`${message} At character ${this.index}.`);
398
+ }
399
+
400
+ isAtEnd(): boolean {
401
+ return this.index >= this.text.length;
402
+ }
403
+
404
+ skipWhitespace(): void {
405
+ while (!this.isAtEnd() && /\s/u.test(this.text[this.index]!)) {
406
+ this.index += 1;
407
+ }
408
+ }
409
+
410
+ parseValue(): LosslessJsonValue {
411
+ this.skipWhitespace();
412
+ if (this.isAtEnd()) {
413
+ this.fail('Unexpected end of JSON input.');
414
+ }
415
+
416
+ const current = this.text[this.index]!;
417
+ switch (current) {
418
+ case '"':
419
+ return this.parseString();
420
+ case '{':
421
+ return this.parseObject();
422
+ case '[':
423
+ return this.parseArray();
424
+ case 't':
425
+ this.consumeKeyword('true');
426
+ return true;
427
+ case 'f':
428
+ this.consumeKeyword('false');
429
+ return false;
430
+ case 'n':
431
+ this.consumeKeyword('null');
432
+ return null;
433
+ default:
434
+ if (current === '-' || isAsciiDigit(current)) {
435
+ return this.parseNumber();
436
+ }
437
+ this.fail(`Unexpected token ${JSON.stringify(current)}.`);
438
+ }
439
+ }
440
+
441
+ private consumeKeyword(keyword: string): void {
442
+ if (!this.text.startsWith(keyword, this.index)) {
443
+ this.fail(`Expected ${keyword}.`);
444
+ }
445
+ this.index += keyword.length;
446
+ }
447
+
448
+ private parseString(): string {
449
+ let result = '';
450
+ this.index += 1;
451
+
452
+ while (!this.isAtEnd()) {
453
+ const current = this.text[this.index]!;
454
+ if (current === '"') {
455
+ this.index += 1;
456
+ return result;
457
+ }
458
+ if (current === '\\') {
459
+ this.index += 1;
460
+ if (this.isAtEnd()) {
461
+ this.fail('Unexpected end of escape sequence.');
462
+ }
463
+ result += this.parseEscapeSequence();
464
+ continue;
465
+ }
466
+ result += current;
467
+ this.index += 1;
468
+ }
469
+
470
+ this.fail('Unterminated string literal.');
471
+ }
472
+
473
+ private parseEscapeSequence(): string {
474
+ const current = this.text[this.index]!;
475
+ this.index += 1;
476
+ switch (current) {
477
+ case '"':
478
+ case '\\':
479
+ case '/':
480
+ return current;
481
+ case 'b':
482
+ return '\b';
483
+ case 'f':
484
+ return '\f';
485
+ case 'n':
486
+ return '\n';
487
+ case 'r':
488
+ return '\r';
489
+ case 't':
490
+ return '\t';
491
+ case 'u': {
492
+ const hex = this.text.slice(this.index, this.index + 4);
493
+ if (!/^[0-9A-Fa-f]{4}$/u.test(hex)) {
494
+ this.fail('Invalid unicode escape.');
495
+ }
496
+ this.index += 4;
497
+ return String.fromCharCode(Number.parseInt(hex, 16));
498
+ }
499
+ default:
500
+ this.fail(`Invalid escape sequence \\${current}.`);
501
+ }
502
+ }
503
+
504
+ private parseArray(): LosslessJsonArray {
505
+ const result: LosslessJsonArray = [];
506
+ this.index += 1;
507
+ this.skipWhitespace();
508
+ if (this.text[this.index] === ']') {
509
+ this.index += 1;
510
+ return result;
511
+ }
512
+
513
+ while (true) {
514
+ result.push(this.parseValue());
515
+ this.skipWhitespace();
516
+ const current = this.text[this.index];
517
+ if (current === ']') {
518
+ this.index += 1;
519
+ return result;
520
+ }
521
+ if (current !== ',') {
522
+ this.fail('Expected , or ] in array literal.');
523
+ }
524
+ this.index += 1;
525
+ }
526
+ }
527
+
528
+ private parseObject(): LosslessJsonObject {
529
+ const result: LosslessJsonObject = {};
530
+ this.index += 1;
531
+ this.skipWhitespace();
532
+ if (this.text[this.index] === '}') {
533
+ this.index += 1;
534
+ return result;
535
+ }
536
+
537
+ while (true) {
538
+ this.skipWhitespace();
539
+ if (this.text[this.index] !== '"') {
540
+ this.fail('Expected string key in object literal.');
541
+ }
542
+ const key = this.parseString();
543
+ this.skipWhitespace();
544
+ if (this.text[this.index] !== ':') {
545
+ this.fail('Expected : after object key.');
546
+ }
547
+ this.index += 1;
548
+ result[key] = this.parseValue();
549
+ this.skipWhitespace();
550
+ const current = this.text[this.index];
551
+ if (current === '}') {
552
+ this.index += 1;
553
+ return result;
554
+ }
555
+ if (current !== ',') {
556
+ this.fail('Expected , or } in object literal.');
557
+ }
558
+ this.index += 1;
559
+ }
560
+ }
561
+
562
+ private parseNumber(): number | bigint {
563
+ const start = this.index;
564
+ if (this.text[this.index] === '-') {
565
+ this.index += 1;
566
+ }
567
+
568
+ if (this.text[this.index] === '0') {
569
+ this.index += 1;
570
+ } else {
571
+ this.consumeDigits();
572
+ }
573
+
574
+ let isInteger = true;
575
+ if (this.text[this.index] === '.') {
576
+ isInteger = false;
577
+ this.index += 1;
578
+ this.consumeDigits();
579
+ }
580
+
581
+ const exponentMarker = this.text[this.index];
582
+ if (exponentMarker === 'e' || exponentMarker === 'E') {
583
+ isInteger = false;
584
+ this.index += 1;
585
+ const sign = this.text[this.index];
586
+ if (sign === '+' || sign === '-') {
587
+ this.index += 1;
588
+ }
589
+ this.consumeDigits();
590
+ }
591
+
592
+ const token = this.text.slice(start, this.index);
593
+ if (!isInteger) {
594
+ return Number(token);
595
+ }
596
+ if (token === '-0') {
597
+ return -0;
598
+ }
599
+
600
+ const bigintValue = BigInt(token);
601
+ return bigintValue <= MAX_SAFE_INTEGER_BIGINT && bigintValue >= MIN_SAFE_INTEGER_BIGINT
602
+ ? Number(token)
603
+ : bigintValue;
604
+ }
605
+
606
+ private consumeDigits(): void {
607
+ const start = this.index;
608
+ while (!this.isAtEnd() && isAsciiDigit(this.text[this.index]!)) {
609
+ this.index += 1;
610
+ }
611
+ if (start === this.index) {
612
+ this.fail('Expected digits.');
613
+ }
614
+ }
615
+ }
616
+
617
+ class JsonLikeParser {
618
+ #index = 0;
619
+ readonly #text: string;
620
+
621
+ constructor(text: string) {
622
+ this.#text = text;
623
+ }
624
+
625
+ finish(): void {
626
+ this.#skipWhitespace();
627
+ if (this.#index !== this.#text.length) {
628
+ this.#error('Unexpected trailing JSON input');
629
+ }
630
+ }
631
+
632
+ parseValue(): JsonLikeValue {
633
+ this.#skipWhitespace();
634
+ const current = this.#text[this.#index];
635
+ switch (current) {
636
+ case '{':
637
+ return this.#parseObject();
638
+ case '[':
639
+ return this.#parseArray();
640
+ case '"':
641
+ return this.#parseString();
642
+ case 't':
643
+ this.#consumeKeyword('true');
644
+ return true;
645
+ case 'f':
646
+ this.#consumeKeyword('false');
647
+ return false;
648
+ case 'n':
649
+ this.#consumeKeyword('null');
650
+ return null;
651
+ default:
652
+ if (current === '-' || this.#isDigit(current)) {
653
+ return this.#parseNumber();
654
+ }
655
+ this.#error('Unexpected token in JSON input');
656
+ }
657
+ }
658
+
659
+ #consumeKeyword(keyword: string): void {
660
+ if (this.#text.slice(this.#index, this.#index + keyword.length) !== keyword) {
661
+ this.#error(`Expected ${keyword}`);
662
+ }
663
+ this.#index += keyword.length;
664
+ }
665
+
666
+ #parseArray(): JsonLikeArray {
667
+ const values: JsonLikeValue[] = [];
668
+ this.#index += 1;
669
+ this.#skipWhitespace();
670
+ if (this.#text[this.#index] === ']') {
671
+ this.#index += 1;
672
+ return values;
673
+ }
674
+
675
+ while (true) {
676
+ values.push(this.parseValue());
677
+ this.#skipWhitespace();
678
+ const current = this.#text[this.#index];
679
+ if (current === ']') {
680
+ this.#index += 1;
681
+ return values;
682
+ }
683
+ if (current !== ',') {
684
+ this.#error('Expected , or ] in JSON array');
685
+ }
686
+ this.#index += 1;
687
+ }
688
+ }
689
+
690
+ #parseNumber(): number | bigint {
691
+ const start = this.#index;
692
+ if (this.#text[this.#index] === '-') {
693
+ this.#index += 1;
694
+ }
695
+
696
+ if (this.#text[this.#index] === '0') {
697
+ this.#index += 1;
698
+ } else {
699
+ this.#consumeDigits();
700
+ }
701
+
702
+ let isInteger = true;
703
+ if (this.#text[this.#index] === '.') {
704
+ isInteger = false;
705
+ this.#index += 1;
706
+ this.#consumeDigits();
707
+ }
708
+
709
+ const exponent = this.#text[this.#index];
710
+ if (exponent === 'e' || exponent === 'E') {
711
+ isInteger = false;
712
+ this.#index += 1;
713
+ const sign = this.#text[this.#index];
714
+ if (sign === '+' || sign === '-') {
715
+ this.#index += 1;
716
+ }
717
+ this.#consumeDigits();
718
+ }
719
+
720
+ const token = this.#text.slice(start, this.#index);
721
+ if (!isInteger) {
722
+ return Number(token);
723
+ }
724
+
725
+ const bigintValue = BigInt(token);
726
+ const numberValue = Number(token);
727
+ return Number.isSafeInteger(numberValue) && BigInt(numberValue) === bigintValue
728
+ ? numberValue
729
+ : bigintValue;
730
+ }
731
+
732
+ #parseObject(): JsonLikeObject {
733
+ const object: Record<string, JsonLikeValue> = {};
734
+ this.#index += 1;
735
+ this.#skipWhitespace();
736
+ if (this.#text[this.#index] === '}') {
737
+ this.#index += 1;
738
+ return object;
739
+ }
740
+
741
+ while (true) {
742
+ this.#skipWhitespace();
743
+ if (this.#text[this.#index] !== '"') {
744
+ this.#error('Expected string key in JSON object');
745
+ }
746
+ const key = this.#parseString();
747
+ this.#skipWhitespace();
748
+ if (this.#text[this.#index] !== ':') {
749
+ this.#error('Expected : in JSON object');
750
+ }
751
+ this.#index += 1;
752
+ object[key] = this.parseValue();
753
+ this.#skipWhitespace();
754
+ const current = this.#text[this.#index];
755
+ if (current === '}') {
756
+ this.#index += 1;
757
+ return object;
758
+ }
759
+ if (current !== ',') {
760
+ this.#error('Expected , or } in JSON object');
761
+ }
762
+ this.#index += 1;
763
+ }
764
+ }
765
+
766
+ #parseString(): string {
767
+ const start = this.#index;
768
+ this.#index += 1;
769
+ while (this.#index < this.#text.length) {
770
+ const current = this.#text[this.#index];
771
+ if (current === '"') {
772
+ this.#index += 1;
773
+ return JSON.parse(this.#text.slice(start, this.#index));
774
+ }
775
+ if (current === '\\') {
776
+ this.#index += 1;
777
+ const escaped = this.#text[this.#index];
778
+ if (escaped === undefined) {
779
+ this.#error('Unterminated string escape');
780
+ }
781
+ if (escaped === 'u') {
782
+ for (let index = 0; index < 4; index += 1) {
783
+ this.#index += 1;
784
+ if (!/[0-9A-Fa-f]/u.test(this.#text[this.#index] ?? '')) {
785
+ this.#error('Invalid unicode escape');
786
+ }
787
+ }
788
+ }
789
+ } else if (current <= '\u001F') {
790
+ this.#error('Invalid control character in string literal');
791
+ }
792
+ this.#index += 1;
793
+ }
794
+ this.#error('Unterminated string literal');
795
+ }
796
+
797
+ #consumeDigits(): void {
798
+ const start = this.#index;
799
+ while (this.#isDigit(this.#text[this.#index])) {
800
+ this.#index += 1;
801
+ }
802
+ if (start === this.#index) {
803
+ this.#error('Expected digit in JSON number');
804
+ }
805
+ }
806
+
807
+ #skipWhitespace(): void {
808
+ while (/\s/u.test(this.#text[this.#index] ?? '')) {
809
+ this.#index += 1;
810
+ }
811
+ }
812
+
813
+ #isDigit(value: string | undefined): boolean {
814
+ return value !== undefined && value >= '0' && value <= '9';
815
+ }
816
+
817
+ #error(message: string): never {
818
+ throw new SyntaxError(`${message} at position ${this.#index}.`);
819
+ }
820
+ }
821
+
822
+ function isAsciiDigit(text: string): boolean {
823
+ return text >= '0' && text <= '9';
824
+ }