jsonh-ts 1.0.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 (56) hide show
  1. package/build/index.d.ts +22 -0
  2. package/build/index.d.ts.map +1 -0
  3. package/build/index.js +22 -0
  4. package/build/index.js.map +1 -0
  5. package/build/json-token-type.d.ts +77 -0
  6. package/build/json-token-type.d.ts.map +1 -0
  7. package/build/json-token-type.js +79 -0
  8. package/build/json-token-type.js.map +1 -0
  9. package/build/jsonh-number-parser.d.ts +21 -0
  10. package/build/jsonh-number-parser.d.ts.map +1 -0
  11. package/build/jsonh-number-parser.js +174 -0
  12. package/build/jsonh-number-parser.js.map +1 -0
  13. package/build/jsonh-reader-options.d.ts +25 -0
  14. package/build/jsonh-reader-options.d.ts.map +1 -0
  15. package/build/jsonh-reader-options.js +26 -0
  16. package/build/jsonh-reader-options.js.map +1 -0
  17. package/build/jsonh-reader.d.ts +68 -0
  18. package/build/jsonh-reader.d.ts.map +1 -0
  19. package/build/jsonh-reader.js +1218 -0
  20. package/build/jsonh-reader.js.map +1 -0
  21. package/build/jsonh-token.d.ts +21 -0
  22. package/build/jsonh-token.d.ts.map +1 -0
  23. package/build/jsonh-token.js +36 -0
  24. package/build/jsonh-token.js.map +1 -0
  25. package/build/jsonh-version.d.ts +15 -0
  26. package/build/jsonh-version.d.ts.map +1 -0
  27. package/build/jsonh-version.js +17 -0
  28. package/build/jsonh-version.js.map +1 -0
  29. package/build/result-helpers.d.ts +6 -0
  30. package/build/result-helpers.d.ts.map +1 -0
  31. package/build/result-helpers.js +11 -0
  32. package/build/result-helpers.js.map +1 -0
  33. package/build/result.d.ts +48 -0
  34. package/build/result.d.ts.map +1 -0
  35. package/build/result.js +95 -0
  36. package/build/result.js.map +1 -0
  37. package/build/string-text-reader.d.ts +11 -0
  38. package/build/string-text-reader.d.ts.map +1 -0
  39. package/build/string-text-reader.js +33 -0
  40. package/build/string-text-reader.js.map +1 -0
  41. package/build/text-reader.d.ts +20 -0
  42. package/build/text-reader.d.ts.map +1 -0
  43. package/build/text-reader.js +19 -0
  44. package/build/text-reader.js.map +1 -0
  45. package/index.ts +21 -0
  46. package/json-token-type.ts +77 -0
  47. package/jsonh-number-parser.ts +191 -0
  48. package/jsonh-reader-options.ts +26 -0
  49. package/jsonh-reader.ts +1317 -0
  50. package/jsonh-token.ts +37 -0
  51. package/jsonh-version.ts +15 -0
  52. package/package.json +30 -0
  53. package/result.ts +97 -0
  54. package/string-text-reader.ts +35 -0
  55. package/text-reader.ts +30 -0
  56. package/tsconfig.json +38 -0
@@ -0,0 +1,1317 @@
1
+ import JsonhReaderOptions = require("./jsonh-reader-options.js");
2
+ import TextReader = require("./text-reader.js");
3
+ import StringTextReader = require("./string-text-reader.js");
4
+ import JsonhToken = require("./jsonh-token.js");
5
+ import JsonTokenType = require("./json-token-type.js");
6
+ import JsonhNumberParser = require("./jsonh-number-parser.js")
7
+ import Result = require("./result.js");
8
+
9
+ /**
10
+ * A reader that reads JSONH tokens from a string.
11
+ */
12
+ class JsonhReader {
13
+ /**
14
+ * The text reader to read characters from.
15
+ */
16
+ #textReader: TextReader;
17
+ /**
18
+ * The text reader to read characters from.
19
+ */
20
+ get textReader(): TextReader {
21
+ return this.#textReader;
22
+ }
23
+ /**
24
+ * The options to use when reading JSONH.
25
+ */
26
+ #options: JsonhReaderOptions;
27
+ /**
28
+ * The options to use when reading JSONH.
29
+ */
30
+ get options(): JsonhReaderOptions {
31
+ return this.#options;
32
+ }
33
+ /**
34
+ * The number of characters read from {@link string}.
35
+ */
36
+ #charCounter: number;
37
+ /**
38
+ * The number of characters read from {@link string}.
39
+ */
40
+ get charCounter(): number {
41
+ return this.#charCounter;
42
+ }
43
+
44
+ /**
45
+ * Characters that cannot be used unescaped in quoteless strings.
46
+ */
47
+ static readonly #reservedChars: ReadonlyArray<string> = ['\\', ',', ':', '[', ']', '{', '}', '/', '#', '"', '\''];
48
+ /**
49
+ * Characters that are considered newlines.
50
+ */
51
+ static readonly #newlineChars: ReadonlyArray<string> = ['\n', '\r', '\u2028', '\u2029'];
52
+ /**
53
+ * Characters that are considered whitespace.
54
+ */
55
+ static readonly #whitespaceChars: ReadonlyArray<string> = [
56
+ '\u0020', '\u00A0', '\u1680', '\u2000', '\u2001', '\u2002', '\u2003', '\u2004', '\u2005',
57
+ '\u2006', '\u2007', '\u2008', '\u2009', '\u200A', '\u202F', '\u205F', '\u3000', '\u2028',
58
+ '\u2029', '\u0009', '\u000A', '\u000B', '\u000C', '\u000D', '\u0085',
59
+ ];
60
+
61
+ /**
62
+ * Constructs a reader that reads JSONH from a text reader.
63
+ */
64
+ private constructor(textReader: TextReader, options: JsonhReaderOptions = new JsonhReaderOptions()) {
65
+ if (typeof textReader === "string") {
66
+ throw new Error("Do not pass a string to new JsonhReader(). Use JsonhReader.fromString().");
67
+ }
68
+
69
+ this.#textReader = textReader;
70
+ this.#options = options;
71
+ this.#charCounter = 0;
72
+ }
73
+ /**
74
+ * Constructs a reader that reads JSONH from a text reader.
75
+ */
76
+ static fromTextReader(textReader: TextReader, options: JsonhReaderOptions = new JsonhReaderOptions()): JsonhReader {
77
+ return new JsonhReader(textReader, options);
78
+ }
79
+ /**
80
+ * Constructs a reader that reads JSONH from a string.
81
+ */
82
+ static fromString(string: string, options: JsonhReaderOptions = new JsonhReaderOptions()): JsonhReader {
83
+ return new JsonhReader(new StringTextReader(string), options);
84
+ }
85
+
86
+ /**
87
+ * Parses a single element from a text reader.
88
+ */
89
+ static parseElementfromTextReader<T = unknown>(textReader: TextReader): Result<T> {
90
+ return new JsonhReader(textReader).parseElement<T>();
91
+ }
92
+ /**
93
+ * Parses a single element from a string.
94
+ */
95
+ static parseElementFromString<T = unknown>(string: string): Result<T> {
96
+ return this.fromString(string).parseElement<T>();
97
+ }
98
+
99
+ /**
100
+ * Parses a single element from the reader.
101
+ */
102
+ parseElement<T = unknown>(): Result<T> {
103
+ let currentNodes: unknown[] = [];
104
+ let currentPropertyName: string | null = null;
105
+
106
+ let submitNode = function(node: unknown): boolean {
107
+ // Root value
108
+ if (currentNodes.length === 0) {
109
+ return true;
110
+ }
111
+ // Array item
112
+ if (currentPropertyName === null) {
113
+ (currentNodes.at(-1) as any[]).push(node);
114
+ return false;
115
+ }
116
+ // Object property
117
+ else {
118
+ (currentNodes.at(-1) as any)[currentPropertyName] = node;
119
+ currentPropertyName = null;
120
+ return false;
121
+ }
122
+ };
123
+ let startNode = function(node: unknown): void {
124
+ submitNode(node);
125
+ currentNodes.push(node);
126
+ };
127
+
128
+ for (let tokenResult of this.readElement()) {
129
+ // Check error
130
+ if (tokenResult.isError) {
131
+ return Result.fromError(tokenResult.error);
132
+ }
133
+
134
+ switch (tokenResult.value.jsonType) {
135
+ // Null
136
+ case JsonTokenType.Null: {
137
+ let node: null = null;
138
+ if (submitNode(node)) {
139
+ return Result.fromValue(node as T);
140
+ }
141
+ break;
142
+ }
143
+ // True
144
+ case JsonTokenType.True: {
145
+ let node: boolean = true;
146
+ if (submitNode(node)) {
147
+ return Result.fromValue(node as T);
148
+ }
149
+ break;
150
+ }
151
+ // False
152
+ case JsonTokenType.False: {
153
+ let node: boolean = false;
154
+ if (submitNode(node)) {
155
+ return Result.fromValue(node as T);
156
+ }
157
+ break;
158
+ }
159
+ // String
160
+ case JsonTokenType.String: {
161
+ let node: string = tokenResult.value.value;
162
+ if (submitNode(node)) {
163
+ return Result.fromValue(node as T);
164
+ }
165
+ break;
166
+ }
167
+ // Number
168
+ case JsonTokenType.Number: {
169
+ // TODO
170
+ let result: Result<number> = JsonhNumberParser.parse(tokenResult.value.value);
171
+ if (result.isError) {
172
+ return Result.fromError(result.error);
173
+ }
174
+ let node: number = result.value;
175
+ if (submitNode(node)) {
176
+ return Result.fromValue(node as T);
177
+ }
178
+ break;
179
+ }
180
+ // Start Object
181
+ case JsonTokenType.StartObject: {
182
+ let node: object = {};
183
+ startNode(node);
184
+ break;
185
+ }
186
+ // Start Array
187
+ case JsonTokenType.StartArray: {
188
+ let node: any[] = [];
189
+ startNode(node);
190
+ break;
191
+ }
192
+ // End Object/Array
193
+ case JsonTokenType.EndObject:
194
+ case JsonTokenType.EndArray: {
195
+ // Nested node
196
+ if (currentNodes.length > 1) {
197
+ currentNodes.pop();
198
+ }
199
+ // Root node
200
+ else {
201
+ return Result.fromValue(currentNodes.at(-1) as T);
202
+ }
203
+ break;
204
+ }
205
+ // Property Name
206
+ case JsonTokenType.PropertyName: {
207
+ currentPropertyName = tokenResult.value.value;
208
+ break;
209
+ }
210
+ // Comment
211
+ case JsonTokenType.Comment: {
212
+ break;
213
+ }
214
+ // Not Implemented
215
+ default: {
216
+ return Result.fromError(new Error("Token type not implemented"));
217
+ }
218
+ }
219
+ }
220
+
221
+ // End of input
222
+ return Result.fromError(new Error("Expected token, got end of input"));
223
+ }
224
+ /**
225
+ * Tries to find the given property name in the reader.
226
+ * For example, to find `c`:
227
+ * ```
228
+ * // Original position
229
+ * {
230
+ * "a": "1",
231
+ * "b": {
232
+ * "c": "2"
233
+ * },
234
+ * "c": // Final position
235
+ * "3"
236
+ * }
237
+ * ```
238
+ */
239
+ findPropertyValue(propertyName: string): boolean {
240
+ let currentDepth: number = 0;
241
+
242
+ for (let tokenResult of this.readElement()) {
243
+ // Check error
244
+ if (tokenResult.isError) {
245
+ return false;
246
+ }
247
+
248
+ switch (tokenResult.value.jsonType) {
249
+ // Start structure
250
+ case JsonTokenType.StartObject:
251
+ case JsonTokenType.StartArray: {
252
+ currentDepth++;
253
+ break;
254
+ }
255
+ // End structure
256
+ case JsonTokenType.EndObject:
257
+ case JsonTokenType.EndArray: {
258
+ currentDepth--;
259
+ break;
260
+ }
261
+ // Property name
262
+ case JsonTokenType.PropertyName: {
263
+ if (currentDepth === 1 && tokenResult.value.value === propertyName) {
264
+ // Path found
265
+ return true;
266
+ }
267
+ break;
268
+ }
269
+ }
270
+ }
271
+
272
+ // Path not found
273
+ return false;
274
+ }
275
+ /**
276
+ * Reads a single element from the reader.
277
+ */
278
+ *readElement(): Generator<Result<JsonhToken>> {
279
+ // Comments & whitespace
280
+ for (let token of this.#readCommentsAndWhitespace()) {
281
+ if (token.isError) {
282
+ yield token;
283
+ return;
284
+ }
285
+ yield token;
286
+ }
287
+
288
+ // Peek result
289
+ let next: string | null = this.#peek();
290
+ if (next === null) {
291
+ yield Result.fromError(new Error("Expected token, got end of input"));
292
+ return;
293
+ }
294
+
295
+ // Object
296
+ if (next === '{') {
297
+ for (let token of this.#readObject()) {
298
+ if (token.isError) {
299
+ yield token;
300
+ return;
301
+ }
302
+ yield token;
303
+ }
304
+ }
305
+ // Array
306
+ else if (next === '[') {
307
+ for (let token of this.#readArray()) {
308
+ if (token.isError) {
309
+ yield token;
310
+ return;
311
+ }
312
+ yield token;
313
+ }
314
+ }
315
+ // Primitive value (null, true, false, string, number)
316
+ else {
317
+ let token: Result<JsonhToken> = this.#readPrimitiveElement();
318
+ if (token.isError) {
319
+ yield token;
320
+ return;
321
+ }
322
+
323
+ // Detect braceless object from property name
324
+ if (token.value.jsonType === JsonTokenType.String) {
325
+ // Try read property name
326
+ let propertyNameTokens: JsonhToken[] = [];
327
+ for (let propertyNameToken of this.#readPropertyName(token.value.value)) {
328
+ // Possible braceless object
329
+ if (!propertyNameToken.isError) {
330
+ propertyNameTokens.push(propertyNameToken.value);
331
+ }
332
+ // Primitive value (error reading property name)
333
+ else {
334
+ yield token;
335
+ for (let nonPropertyNameToken of propertyNameTokens) {
336
+ yield Result.fromValue(nonPropertyNameToken);
337
+ }
338
+ return;
339
+ }
340
+ }
341
+ // Braceless object
342
+ for (let objectToken of this.#readBracelessObject(propertyNameTokens)) {
343
+ if (objectToken.isError) {
344
+ yield objectToken;
345
+ return;
346
+ }
347
+ yield objectToken;
348
+ }
349
+ }
350
+ // Primitive value
351
+ else {
352
+ yield token;
353
+ }
354
+ }
355
+ }
356
+
357
+ *#readObject(): Generator<Result<JsonhToken>> {
358
+ // Opening brace
359
+ if (!this.#readOne('{')) {
360
+ // Braceless object
361
+ for (let token of this.#readBracelessObject()) {
362
+ if (token.isError) {
363
+ yield token;
364
+ return;
365
+ }
366
+ yield token;
367
+ }
368
+ return;
369
+ }
370
+ // Start object
371
+ yield Result.fromValue(new JsonhToken(JsonTokenType.StartObject));
372
+
373
+ while (true) {
374
+ // Comments & whitespace
375
+ for (let token of this.#readCommentsAndWhitespace()) {
376
+ if (token.isError) {
377
+ yield token;
378
+ return;
379
+ }
380
+ yield token;
381
+ }
382
+
383
+ let next: string | null = this.#peek();
384
+ if (next === null) {
385
+ // End of incomplete object
386
+ if (this.#options.incompleteInputs) {
387
+ yield Result.fromValue(new JsonhToken(JsonTokenType.EndObject));
388
+ return;
389
+ }
390
+ // Missing closing brace
391
+ yield Result.fromError(new Error("Expected `}` to end object, got end of input"));
392
+ return;
393
+ }
394
+
395
+ // Closing brace
396
+ if (next === '}') {
397
+ // End of object
398
+ this.#read();
399
+ yield Result.fromValue(new JsonhToken(JsonTokenType.EndObject));
400
+ return;
401
+ }
402
+ // Property
403
+ else {
404
+ for (let token of this.#readProperty()) {
405
+ if (token.isError) {
406
+ yield token;
407
+ return;
408
+ }
409
+ yield token;
410
+ }
411
+ }
412
+ }
413
+ }
414
+ *#readBracelessObject(propertyNameTokens: Iterable<JsonhToken> | null = null): Generator<Result<JsonhToken>> {
415
+ // Start of object
416
+ yield Result.fromValue(new JsonhToken(JsonTokenType.StartObject));
417
+
418
+ // Initial tokens
419
+ if (propertyNameTokens !== null) {
420
+ for (let initialToken of this.#readProperty(propertyNameTokens)) {
421
+ if (initialToken.isError) {
422
+ yield initialToken;
423
+ return;
424
+ }
425
+ yield initialToken;
426
+ }
427
+ }
428
+
429
+ while (true) {
430
+ // Comments & whitespace
431
+ for (let token of this.#readCommentsAndWhitespace()) {
432
+ if (token.isError) {
433
+ yield token;
434
+ return;
435
+ }
436
+ yield token;
437
+ }
438
+
439
+ if (this.#peek() === null) {
440
+ // End of braceless object
441
+ yield Result.fromValue(new JsonhToken(JsonTokenType.EndObject));
442
+ return;
443
+ }
444
+
445
+ // Property
446
+ for (let token of this.#readProperty()) {
447
+ if (token.isError) {
448
+ yield token;
449
+ return;
450
+ }
451
+ yield token;
452
+ }
453
+ }
454
+ }
455
+ *#readProperty(propertyNameTokens: Iterable<JsonhToken> | null = null): Generator<Result<JsonhToken>> {
456
+ // Property name
457
+ if (propertyNameTokens !== null) {
458
+ for (let token of propertyNameTokens) {
459
+ yield Result.fromValue(token);
460
+ }
461
+ }
462
+ else {
463
+ for (let token of this.#readPropertyName()) {
464
+ if (token.isError) {
465
+ yield token;
466
+ return;
467
+ }
468
+ yield token;
469
+ }
470
+ }
471
+
472
+ // Comments & whitespace
473
+ for (let token of this.#readCommentsAndWhitespace()) {
474
+ if (token.isError) {
475
+ yield token;
476
+ return;
477
+ }
478
+ yield token;
479
+ }
480
+
481
+ // Property value
482
+ for (let token of this.readElement()) {
483
+ if (token.isError) {
484
+ yield token;
485
+ return;
486
+ }
487
+ yield token;
488
+ }
489
+
490
+ // Comments & whitespace
491
+ for (let token of this.#readCommentsAndWhitespace()) {
492
+ if (token.isError) {
493
+ yield token;
494
+ return;
495
+ }
496
+ yield token;
497
+ }
498
+
499
+ // Optional comma
500
+ this.#readOne(',');
501
+ }
502
+ *#readPropertyName(string: string | null = null): Generator<Result<JsonhToken>> {
503
+ // String
504
+ if (string === null) {
505
+ let stringToken: Result<JsonhToken> = this.#readString();
506
+ if (stringToken.isError) {
507
+ yield stringToken;
508
+ return;
509
+ }
510
+ string = stringToken.value.value;
511
+ }
512
+
513
+ // Comments & whitespace
514
+ for (let token of this.#readCommentsAndWhitespace()) {
515
+ if (token.isError) {
516
+ yield token;
517
+ return;
518
+ }
519
+ yield token;
520
+ }
521
+
522
+ // Colon
523
+ if (!this.#readOne(':')) {
524
+ yield Result.fromError(new Error("Expected `:` after property name in object"));
525
+ return;
526
+ }
527
+
528
+ // End of property name
529
+ yield Result.fromValue(new JsonhToken(JsonTokenType.PropertyName, string));
530
+ }
531
+ *#readArray(): Generator<Result<JsonhToken>> {
532
+ // Opening bracket
533
+ if (!this.#readOne('[')) {
534
+ yield Result.fromError(new Error("Expected `[` to start array"));
535
+ return;
536
+ }
537
+ // Start of array
538
+ yield Result.fromValue(new JsonhToken(JsonTokenType.StartArray));
539
+
540
+ while (true) {
541
+ // Comments & whitespace
542
+ for (let token of this.#readCommentsAndWhitespace()) {
543
+ if (token.isError) {
544
+ yield token;
545
+ return;
546
+ }
547
+ yield token;
548
+ }
549
+
550
+ let next: string | null = this.#peek();
551
+ if (next === null) {
552
+ // End of incomplete array
553
+ if (this.#options.incompleteInputs) {
554
+ yield Result.fromValue(new JsonhToken(JsonTokenType.EndArray));
555
+ return;
556
+ }
557
+ // Missing closing bracket
558
+ yield Result.fromError(new Error("Expected `]` to end array, got end of input"));
559
+ return;
560
+ }
561
+
562
+ // Closing bracket
563
+ if (next === ']') {
564
+ // End of array
565
+ this.#read();
566
+ yield Result.fromValue(new JsonhToken(JsonTokenType.EndArray));
567
+ return;
568
+ }
569
+ // Item
570
+ else {
571
+ for (let token of this.#readItem()) {
572
+ if (token.isError) {
573
+ yield token;
574
+ return;
575
+ }
576
+ yield token;
577
+ }
578
+ }
579
+ }
580
+ }
581
+ *#readItem(): Generator<Result<JsonhToken>> {
582
+ // Element
583
+ for (let token of this.readElement()) {
584
+ if (token.isError) {
585
+ yield token;
586
+ return;
587
+ }
588
+ yield token;
589
+ }
590
+
591
+ // Comments & whitespace
592
+ for (let token of this.#readCommentsAndWhitespace()) {
593
+ if (token.isError) {
594
+ yield token;
595
+ return;
596
+ }
597
+ yield token;
598
+ }
599
+
600
+ // Optional comma
601
+ this.#readOne(',');
602
+ }
603
+ #readString(): Result<JsonhToken> {
604
+ // Start quote
605
+ let startQuote: string | null = this.#readAny('"', '\'');
606
+ if (startQuote === null) {
607
+ return this.#readQuotelessString();
608
+ }
609
+
610
+ // Count multiple start quotes
611
+ let startQuoteCounter: number = 1;
612
+ while (this.#readOne(startQuote)) {
613
+ startQuoteCounter++;
614
+ }
615
+
616
+ // Empty string
617
+ if (startQuoteCounter === 2) {
618
+ return Result.fromValue(new JsonhToken(JsonTokenType.String, ""));
619
+ }
620
+
621
+ // Count multiple end quotes
622
+ let endQuoteCounter: number = 0;
623
+
624
+ // Read string
625
+ let stringBuilder = "";
626
+
627
+ while (true) {
628
+ let next: string | null = this.#read();
629
+ if (next === null) {
630
+ return Result.fromError(new Error("Expected end of string, got end of input"));
631
+ }
632
+
633
+ // Partial end quote was actually part of string
634
+ if (next !== startQuote) {
635
+ stringBuilder += startQuote.repeat(endQuoteCounter);
636
+ endQuoteCounter = 0;
637
+ }
638
+
639
+ // End quote
640
+ if (next === startQuote) {
641
+ endQuoteCounter++;
642
+ if (endQuoteCounter === startQuoteCounter) {
643
+ break;
644
+ }
645
+ }
646
+ // Escape sequence
647
+ else if (next === '\\') {
648
+ let escapeSequenceResult: Result<string> = this.#readEscapeSequence();
649
+ if (escapeSequenceResult.isError) {
650
+ return Result.fromError(escapeSequenceResult.error);
651
+ }
652
+ stringBuilder += escapeSequenceResult.value;
653
+ }
654
+ // Literal character
655
+ else {
656
+ stringBuilder += next;
657
+ }
658
+ }
659
+
660
+ // Condition: skip remaining steps unless started with multiple quotes
661
+ if (startQuoteCounter > 1) {
662
+ // Pass 1: count leading whitespace -> newline
663
+ let hasLeadingWhitespaceNewline: boolean = false;
664
+ let leadingWhitespaceNewlineCounter: number = 0;
665
+ for (let index: number = 0; index < stringBuilder.length; index++) {
666
+ let next: string = stringBuilder.at(index)!;
667
+
668
+ // Newline
669
+ if (JsonhReader.#newlineChars.includes(next)) {
670
+ // Join CR LF
671
+ if (next === '\r' && index + 1 < stringBuilder.length && stringBuilder[index + 1] === '\n') {
672
+ index++;
673
+ }
674
+
675
+ hasLeadingWhitespaceNewline = true;
676
+ leadingWhitespaceNewlineCounter = index + 1;
677
+ break;
678
+ }
679
+ // Non-whitespace
680
+ else if (!JsonhReader.#whitespaceChars.includes(next)) {
681
+ break;
682
+ }
683
+ }
684
+
685
+ // Condition: skip remaining steps if pass 1 failed
686
+ if (hasLeadingWhitespaceNewline) {
687
+ // Pass 2: count trailing newline -> whitespace
688
+ let hasTrailingNewlineWhitespace: boolean = false;
689
+ let lastNewlineIndex: number = 0;
690
+ let trailingWhitespaceCounter: number = 0;
691
+ for (let index: number = 0; index < stringBuilder.length; index++) {
692
+ let next: string = stringBuilder.at(index)!;
693
+
694
+ // Newline
695
+ if (JsonhReader.#newlineChars.includes(next)) {
696
+ hasTrailingNewlineWhitespace = true;
697
+ lastNewlineIndex = index;
698
+ trailingWhitespaceCounter = 0;
699
+
700
+ // Join CR LF
701
+ if (next === '\r' && index + 1 < stringBuilder.length && stringBuilder[index + 1] === '\n') {
702
+ index++;
703
+ }
704
+ }
705
+ // Whitespace
706
+ else if (JsonhReader.#whitespaceChars.includes(next)) {
707
+ trailingWhitespaceCounter++;
708
+ }
709
+ // Non-whitespace
710
+ else {
711
+ hasTrailingNewlineWhitespace = false;
712
+ trailingWhitespaceCounter = 0;
713
+ }
714
+ }
715
+
716
+ // Condition: skip remaining steps if pass 2 failed
717
+ if (hasTrailingNewlineWhitespace) {
718
+ // Pass 3: strip trailing newline -> whitespace
719
+ stringBuilder = JsonhReader.#removeRange(stringBuilder, lastNewlineIndex, stringBuilder.length - lastNewlineIndex);
720
+
721
+ // Pass 4: strip leading whitespace -> newline
722
+ stringBuilder = JsonhReader.#removeRange(stringBuilder, 0, leadingWhitespaceNewlineCounter);
723
+
724
+ // Condition: skip remaining steps if no trailing whitespace
725
+ if (trailingWhitespaceCounter > 0) {
726
+ // Pass 5: strip line-leading whitespace
727
+ let isLineLeadingWhitespace: boolean = true;
728
+ let lineLeadingWhitespaceCounter: number = 0;
729
+ for (let index: number = 0; index < stringBuilder.length; index++) {
730
+ let next: string = stringBuilder.at(index)!;
731
+
732
+ // Newline
733
+ if (JsonhReader.#newlineChars.includes(next)) {
734
+ isLineLeadingWhitespace = true;
735
+ lineLeadingWhitespaceCounter = 0;
736
+ }
737
+ // Whitespace
738
+ else if (JsonhReader.#whitespaceChars.includes(next)) {
739
+ if (isLineLeadingWhitespace) {
740
+ // Increment line-leading whitespace
741
+ lineLeadingWhitespaceCounter++;
742
+
743
+ // Maximum line-leading whitespace reached
744
+ if (lineLeadingWhitespaceCounter === trailingWhitespaceCounter) {
745
+ // Remove line-leading whitespace
746
+ stringBuilder = JsonhReader.#removeRange(stringBuilder, index + 1 - lineLeadingWhitespaceCounter, lineLeadingWhitespaceCounter);
747
+ index -= lineLeadingWhitespaceCounter;
748
+ // Exit line-leading whitespace
749
+ isLineLeadingWhitespace = false;
750
+ }
751
+ }
752
+ }
753
+ // Non-whitespace
754
+ else {
755
+ if (isLineLeadingWhitespace) {
756
+ // Remove partial line-leading whitespace
757
+ stringBuilder = JsonhReader.#removeRange(stringBuilder, index - lineLeadingWhitespaceCounter, lineLeadingWhitespaceCounter);
758
+ index -= lineLeadingWhitespaceCounter;
759
+ // Exit line-leading whitespace
760
+ isLineLeadingWhitespace = false;
761
+ }
762
+ }
763
+ }
764
+ }
765
+ }
766
+ }
767
+ }
768
+
769
+ // End of string
770
+ return Result.fromValue(new JsonhToken(JsonTokenType.String, stringBuilder));
771
+ }
772
+ #readQuotelessString(initialChars: string = ""): Result<JsonhToken> {
773
+ let isNamedLiteralPossible: boolean = true;
774
+
775
+ // Read quoteless string
776
+ let stringBuilder: string = initialChars;
777
+
778
+ while (true) {
779
+ // Peek char
780
+ let next: string | null = this.#peek();
781
+ if (next === null) {
782
+ break;
783
+ }
784
+
785
+ // Escape sequence
786
+ if (next === '\\') {
787
+ this.#read();
788
+ let escapeSequenceResult: Result<string> = this.#readEscapeSequence();
789
+ if (escapeSequenceResult.isError) {
790
+ return Result.fromError(escapeSequenceResult.error);
791
+ }
792
+ stringBuilder += escapeSequenceResult.value;
793
+ isNamedLiteralPossible = false;
794
+ }
795
+ // End on reserved character
796
+ else if (JsonhReader.#reservedChars.includes(next)) {
797
+ break;
798
+ }
799
+ // End on newline
800
+ else if (JsonhReader.#newlineChars.includes(next)) {
801
+ break;
802
+ }
803
+ // Literal character
804
+ else {
805
+ this.#read();
806
+ stringBuilder += next;
807
+ }
808
+ }
809
+
810
+ // Ensure not empty
811
+ if (stringBuilder.length === 0) {
812
+ return Result.fromError(new Error("Empty quoteless string"));
813
+ }
814
+
815
+ // Trim whitespace
816
+ stringBuilder = JsonhReader.#trimAny(stringBuilder, JsonhReader.#whitespaceChars);
817
+
818
+ // Match named literal
819
+ if (isNamedLiteralPossible) {
820
+ if (stringBuilder === "null") {
821
+ return Result.fromValue(new JsonhToken(JsonTokenType.Null));
822
+ }
823
+ else if (stringBuilder === "true") {
824
+ return Result.fromValue(new JsonhToken(JsonTokenType.True));
825
+ }
826
+ else if (stringBuilder === "false") {
827
+ return Result.fromValue(new JsonhToken(JsonTokenType.False));
828
+ }
829
+ }
830
+
831
+ // End of quoteless string
832
+ return Result.fromValue(new JsonhToken(JsonTokenType.String, stringBuilder));
833
+ }
834
+ #detectQuotelessString(): { foundQuotelessString: boolean, whitespaceChars: string } {
835
+ // Read whitespace
836
+ let whitespaceBuilder: string = "";
837
+
838
+ while (true) {
839
+ // Read char
840
+ let next: string | null = this.#peek();
841
+ if (next === null) {
842
+ break;
843
+ }
844
+
845
+ // Newline
846
+ if (JsonhReader.#newlineChars.includes(next)) {
847
+ // Quoteless strings cannot contain unescaped newlines
848
+ return {
849
+ foundQuotelessString: false,
850
+ whitespaceChars: whitespaceBuilder
851
+ };
852
+ }
853
+
854
+ // End of whitespace
855
+ if (!JsonhReader.#whitespaceChars.includes(next)) {
856
+ break;
857
+ }
858
+
859
+ // Whitespace
860
+ whitespaceBuilder += next;
861
+ this.#read();
862
+ }
863
+
864
+ // Found quoteless string if found backslash or non-reserved char
865
+ let nextChar: string | null = this.#peek();
866
+ return {
867
+ foundQuotelessString: nextChar !== null && (nextChar === '\\' || !JsonhReader.#reservedChars.includes(nextChar)),
868
+ whitespaceChars: whitespaceBuilder
869
+ };
870
+ }
871
+ #readNumber(): { numberToken: Result<JsonhToken>, partialCharsRead: string } {
872
+ // Read number
873
+ let numberBuilder: string = "";
874
+
875
+ // Read sign
876
+ let sign: string | null = this.#readAny('-', '+');
877
+ if (sign !== null) {
878
+ numberBuilder += sign;
879
+ }
880
+
881
+ // Read base
882
+ let baseDigits: string = "0123456789";
883
+ let hasBaseSpecifier: boolean = false;
884
+ if (this.#readOne('0')) {
885
+ numberBuilder += '0';
886
+
887
+ let hexBaseChar: string | null = this.#readAny('x', 'X');
888
+ if (hexBaseChar !== null) {
889
+ numberBuilder += hexBaseChar;
890
+ baseDigits = "0123456789abcdef";
891
+ hasBaseSpecifier = true;
892
+ }
893
+ else {
894
+ let binaryBaseChar: string | null = this.#readAny('b', 'B');
895
+ if (binaryBaseChar !== null) {
896
+ numberBuilder += binaryBaseChar;
897
+ baseDigits = "01";
898
+ hasBaseSpecifier = true;
899
+ }
900
+ else {
901
+ let octalBaseChar: string | null = this.#readAny('o', 'O');
902
+ if (octalBaseChar !== null) {
903
+ numberBuilder += octalBaseChar;
904
+ baseDigits = "01234567";
905
+ hasBaseSpecifier = true;
906
+ }
907
+ }
908
+ }
909
+ }
910
+
911
+ // Read main number
912
+ let mainResult: { result: Result, numberNoExponent: string } = this.#readNumberNoExponent(baseDigits, hasBaseSpecifier);
913
+ numberBuilder += mainResult.numberNoExponent;
914
+ if (mainResult.result.isError) {
915
+ return { numberToken: Result.fromError(mainResult.result.error), partialCharsRead: numberBuilder };
916
+ }
917
+
918
+ // Hexadecimal exponent
919
+ if (numberBuilder.at(-1) === 'e' || numberBuilder.at(-1) === 'E') {
920
+ // Read sign
921
+ let exponentSign: string | null = this.#readAny('-', '+');
922
+ if (exponentSign !== null) {
923
+ numberBuilder += exponentSign;
924
+
925
+ // Read exponent number
926
+ let exponentResult: { result: Result, numberNoExponent: string } = this.#readNumberNoExponent(baseDigits, hasBaseSpecifier);
927
+ numberBuilder += exponentResult.numberNoExponent;
928
+ if (exponentResult.result.isError) {
929
+ return { numberToken: Result.fromError(exponentResult.result.error), partialCharsRead: numberBuilder };
930
+ }
931
+ }
932
+ }
933
+ // Exponent
934
+ else {
935
+ let exponentChar: string | null = this.#readAny('e', 'E');
936
+ if (exponentChar !== null) {
937
+ numberBuilder += exponentChar;
938
+
939
+ // Read sign
940
+ let exponentSign: string | null = this.#readAny('-', '+');
941
+ if (exponentSign !== null) {
942
+ numberBuilder += exponentSign;
943
+ }
944
+
945
+ // Read exponent number
946
+ let exponentResult: { result: Result, numberNoExponent: string } = this.#readNumberNoExponent(baseDigits, hasBaseSpecifier);
947
+ numberBuilder += exponentResult.numberNoExponent;
948
+ if (exponentResult.result.isError) {
949
+ return { numberToken: Result.fromError(exponentResult.result.error), partialCharsRead: numberBuilder };
950
+ }
951
+ }
952
+ }
953
+
954
+ // End of number
955
+ return { numberToken: Result.fromValue(new JsonhToken(JsonTokenType.Number, numberBuilder)), partialCharsRead: "" };
956
+ }
957
+ #readNumberNoExponent(baseDigits: string, hasBaseSpecifier: boolean): { result: Result, numberNoExponent: string } {
958
+ let numberBuilder: string = "";
959
+
960
+ // Leading underscore
961
+ if (!hasBaseSpecifier && this.#peek() === '_') {
962
+ return { result: Result.fromError(new Error("Leading `_` in number")), numberNoExponent: numberBuilder };
963
+ }
964
+
965
+ let isFraction: boolean = false;
966
+ let isEmpty: boolean = true;
967
+
968
+ while (true) {
969
+ // Peek char
970
+ let next: string | null = this.#peek();
971
+ if (next === null) {
972
+ break;
973
+ }
974
+
975
+ // Digit
976
+ if (baseDigits.includes(next.toLowerCase())) {
977
+ this.#read();
978
+ numberBuilder += next;
979
+ isEmpty = false;
980
+ }
981
+ // Dot
982
+ else if (next === '.') {
983
+ this.#read();
984
+ numberBuilder += next;
985
+ isEmpty = false;
986
+
987
+ // Duplicate dot
988
+ if (isFraction) {
989
+ return { result: Result.fromError(new Error("Duplicate `.` in number")), numberNoExponent: numberBuilder };
990
+ }
991
+ isFraction = true;
992
+ }
993
+ // Underscore
994
+ else if (next === '_') {
995
+ this.#read();
996
+ numberBuilder += next;
997
+ isEmpty = false;
998
+ }
999
+ // Other
1000
+ else {
1001
+ break;
1002
+ }
1003
+ }
1004
+
1005
+ // Ensure not empty
1006
+ if (isEmpty) {
1007
+ return { result: Result.fromError(new Error("Empty number")), numberNoExponent: numberBuilder };
1008
+ }
1009
+
1010
+ // Ensure at least one digit
1011
+ if (!JsonhReader.#containsAnyExcept(numberBuilder, ['.', '-', '+', '_'])) {
1012
+ return { result: Result.fromError(new Error("Number must have at least one digit")), numberNoExponent: numberBuilder };
1013
+ }
1014
+
1015
+ // Trailing underscore
1016
+ if (numberBuilder.endsWith('_')) {
1017
+ return { result: Result.fromError(new Error("Trailing `_` in number")), numberNoExponent: numberBuilder };
1018
+ }
1019
+
1020
+ // End of number
1021
+ return { result: Result.fromValue(), numberNoExponent: numberBuilder };
1022
+ }
1023
+ #readNumberOrQuotelessString(): Result<JsonhToken> {
1024
+ // Read number
1025
+ let number: { numberToken: Result<JsonhToken>, partialCharsRead: string } = this.#readNumber();
1026
+ if (!number.numberToken.isError) {
1027
+ // Try read quoteless string starting with number
1028
+ let detectQuotelessStringResult: { foundQuotelessString: boolean, whitespaceChars: string } = this.#detectQuotelessString();
1029
+ if (detectQuotelessStringResult.foundQuotelessString) {
1030
+ return this.#readQuotelessString(number.numberToken.value.value + detectQuotelessStringResult.whitespaceChars);
1031
+ }
1032
+ // Otherwise, accept number
1033
+ else {
1034
+ return number.numberToken;
1035
+ }
1036
+ }
1037
+ // Read quoteless string starting with malformed number
1038
+ else {
1039
+ return this.#readQuotelessString(number.partialCharsRead);
1040
+ }
1041
+ }
1042
+ #readPrimitiveElement(): Result<JsonhToken> {
1043
+ // Peek char
1044
+ let next: string | null = this.#peek();
1045
+ if (next === null) {
1046
+ return Result.fromError(new Error("Expected primitive element, got end of input"));
1047
+ }
1048
+
1049
+ // Number
1050
+ if (next.length === 1 && ((next >= '0' && next <= '9') || (next === '-' || next === '+') || next === '.')) {
1051
+ return this.#readNumberOrQuotelessString();
1052
+ }
1053
+ // String
1054
+ else if (next === '"' || next === '\'') {
1055
+ return this.#readString();
1056
+ }
1057
+ // Quoteless string (or named literal)
1058
+ else {
1059
+ return this.#readQuotelessString();
1060
+ }
1061
+ }
1062
+ *#readCommentsAndWhitespace(): Generator<Result<JsonhToken>> {
1063
+ while (true) {
1064
+ // Whitespace
1065
+ this.#readWhitespace();
1066
+
1067
+ // Peek char
1068
+ let next: string | null = this.#peek();
1069
+ if (next === null) {
1070
+ return;
1071
+ }
1072
+
1073
+ // Comment
1074
+ if (next === '#' || next === '/') {
1075
+ yield this.#readComment();
1076
+ }
1077
+ // End of comments
1078
+ else {
1079
+ return;
1080
+ }
1081
+ }
1082
+ }
1083
+ #readComment(): Result<JsonhToken> {
1084
+ let blockComment: boolean = false;
1085
+
1086
+ // Hash-style comment
1087
+ if (this.#readOne('#')) {
1088
+ }
1089
+ else if (this.#readOne('/')) {
1090
+ // Line-style comment
1091
+ if (this.#readOne('/')) {
1092
+ }
1093
+ // Block-style comment
1094
+ else if (this.#readOne('*')) {
1095
+ blockComment = true;
1096
+ }
1097
+ else {
1098
+ return Result.fromError(new Error("Unexpected `/`"));
1099
+ }
1100
+ }
1101
+ else {
1102
+ return Result.fromError(new Error("Unexpected character"));
1103
+ }
1104
+
1105
+ // Read comment
1106
+ let commentBuilder: string = "";
1107
+
1108
+ while (true) {
1109
+ // Read char
1110
+ let next: string | null = this.#read();
1111
+
1112
+ if (blockComment) {
1113
+ // Error
1114
+ if (next === null) {
1115
+ return Result.fromError(new Error("Expected end of block comment, got end of input"));
1116
+ }
1117
+ // End of block comment
1118
+ if (next === '*' && this.#readOne('/')) {
1119
+ return Result.fromValue(new JsonhToken(JsonTokenType.Comment, commentBuilder));
1120
+ }
1121
+ }
1122
+ else {
1123
+ // End of line comment
1124
+ if (next === null || JsonhReader.#newlineChars.includes(next)) {
1125
+ return Result.fromValue(new JsonhToken(JsonTokenType.Comment, commentBuilder));
1126
+ }
1127
+ }
1128
+
1129
+ // Comment char
1130
+ commentBuilder += next;
1131
+ }
1132
+ }
1133
+ #readWhitespace(): void {
1134
+ while (true) {
1135
+ // Peek char
1136
+ let next: string | null = this.#peek();
1137
+ if (next === null) {
1138
+ return;
1139
+ }
1140
+
1141
+ // Whitespace
1142
+ if (JsonhReader.#whitespaceChars.includes(next)) {
1143
+ this.#read();
1144
+ }
1145
+ // End of whitespace
1146
+ else {
1147
+ return;
1148
+ }
1149
+ }
1150
+ }
1151
+ #readHexSequence(length: number): Result<number> {
1152
+ let hexChars: string = "";
1153
+
1154
+ for (let index: number = 0; index < length; index++) {
1155
+ let next: string | null = this.#read();
1156
+
1157
+ // Hex digit
1158
+ if (next !== null && ((next >= "0" && next <= "9") || (next >= "A" && next <= "F") || (next >= "a" && next <= "f"))) {
1159
+ hexChars += next;
1160
+ }
1161
+ // Unexpected char
1162
+ else {
1163
+ return Result.fromError(new Error("Incorrect number of hexadecimal digits in unicode escape sequence"));
1164
+ }
1165
+ }
1166
+
1167
+ // Parse unicode character from hex digits
1168
+ return Result.fromValue(Number.parseInt(hexChars, 16));
1169
+ }
1170
+ #readEscapeSequence(): Result<string> {
1171
+ let escapeChar: string | null = this.#read();
1172
+ if (escapeChar === null) {
1173
+ return Result.fromError(new Error("Expected escape sequence, got end of input"));
1174
+ }
1175
+
1176
+ // Reverse solidus
1177
+ if (escapeChar === '\\') {
1178
+ return Result.fromValue('\\');
1179
+ }
1180
+ // Backspace
1181
+ else if (escapeChar === 'b') {
1182
+ return Result.fromValue('\b');
1183
+ }
1184
+ // Form feed
1185
+ else if (escapeChar === 'f') {
1186
+ return Result.fromValue('\f');
1187
+ }
1188
+ // Newline
1189
+ else if (escapeChar === 'n') {
1190
+ return Result.fromValue('\n');
1191
+ }
1192
+ // Carriage return
1193
+ else if (escapeChar === 'r') {
1194
+ return Result.fromValue('\r');
1195
+ }
1196
+ // Tab
1197
+ else if (escapeChar === 't') {
1198
+ return Result.fromValue('\t');
1199
+ }
1200
+ // Vertical tab
1201
+ else if (escapeChar === 'v') {
1202
+ return Result.fromValue('\v');
1203
+ }
1204
+ // Null
1205
+ else if (escapeChar === '0') {
1206
+ return Result.fromValue('\0');
1207
+ }
1208
+ // Alert
1209
+ else if (escapeChar === 'a') {
1210
+ return Result.fromValue('\a');
1211
+ }
1212
+ // Escape
1213
+ else if (escapeChar === 'e') {
1214
+ return Result.fromValue('\u001b');
1215
+ }
1216
+ // Unicode hex sequence
1217
+ else if (escapeChar === 'u') {
1218
+ let hexSequence: Result<number> = this.#readHexSequence(4);
1219
+ if (hexSequence.isError) {
1220
+ return Result.fromError(hexSequence.error);
1221
+ }
1222
+ return Result.fromValue(String.fromCodePoint(hexSequence.value));
1223
+ }
1224
+ // Short unicode hex sequence
1225
+ else if (escapeChar === 'x') {
1226
+ let hexSequence: Result<number> = this.#readHexSequence(2);
1227
+ if (hexSequence.isError) {
1228
+ return Result.fromError(hexSequence.error);
1229
+ }
1230
+ return Result.fromValue(String.fromCodePoint(hexSequence.value));
1231
+ }
1232
+ // Long unicode hex sequence
1233
+ else if (escapeChar === 'U') {
1234
+ let hexSequence: Result<number> = this.#readHexSequence(8);
1235
+ if (hexSequence.isError) {
1236
+ return Result.fromError(hexSequence.error);
1237
+ }
1238
+ return Result.fromValue(String.fromCodePoint(hexSequence.value));
1239
+ }
1240
+ // Escaped newline
1241
+ else if (JsonhReader.#newlineChars.includes(escapeChar)) {
1242
+ // Join CR LF
1243
+ if (escapeChar === '\r') {
1244
+ this.#readOne('\n');
1245
+ }
1246
+ return Result.fromValue("");
1247
+ }
1248
+ // Other
1249
+ else {
1250
+ return Result.fromValue(escapeChar);
1251
+ }
1252
+ }
1253
+ #peek(): string | null {
1254
+ let next: string | null = this.#textReader.peek();
1255
+ if (next === null) {
1256
+ return null;
1257
+ }
1258
+ return next;
1259
+ }
1260
+ #read(): string | null {
1261
+ let next: string | null = this.#textReader.read();
1262
+ if (next === null) {
1263
+ return null;
1264
+ }
1265
+ this.#charCounter++;
1266
+ return next;
1267
+ }
1268
+ #readOne(option: string): boolean {
1269
+ if (this.#peek() === option) {
1270
+ this.#read();
1271
+ return true;
1272
+ }
1273
+ return false;
1274
+ }
1275
+ #readAny(...options: ReadonlyArray<string>): string | null {
1276
+ // Peek char
1277
+ let next: string | null = this.#peek();
1278
+ if (next === null) {
1279
+ return null;
1280
+ }
1281
+ // Match option
1282
+ if (!options.includes(next)) {
1283
+ return null;
1284
+ }
1285
+ // Option matched
1286
+ this.#read();
1287
+ return next;
1288
+ }
1289
+
1290
+ static #removeRange(input: string, start: number, count: number): string {
1291
+ return input.slice(0, start) + input.slice(start + count);
1292
+ }
1293
+ static #trimAny(input: string, trimChars: ReadonlyArray<string>) {
1294
+ let start: number = 0;
1295
+ let end: number = input.length;
1296
+
1297
+ while (start < end && trimChars.includes(input.at(start)!)) {
1298
+ start++;
1299
+ }
1300
+
1301
+ while (end > start && trimChars.includes(input.at(end - 1)!)) {
1302
+ end--;
1303
+ }
1304
+
1305
+ return input.slice(start, end);
1306
+ }
1307
+ static #containsAnyExcept(input: string, allowed: ReadonlyArray<string>): boolean {
1308
+ for (let char of input) {
1309
+ if (!allowed.includes(char)) {
1310
+ return true;
1311
+ }
1312
+ }
1313
+ return false;
1314
+ }
1315
+ }
1316
+
1317
+ export = JsonhReader;