All files / toml / _parser.ts

97.25% Branches 248/255
98.38% Lines 666/677
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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
 
 
 
x12
 
 
 
 
x12
x23
x23
 
x23
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
x12
x12
x849
x12
 
x12
x849
x849
 
x12
x2734
x2734
x12
x948
x948
 
 
 
 
 
x12
x143979
x143979
 
 
 
 
 
 
x12
x39
x39
 
 
 
 
x12
x30482
x30482
 
x12
x27980
x31353
x31353
 
x27980
x27988
x27988
x27988
x27988
 
x27988
x27980
 
x8376
x4182
x10417
x10417
x12725
x10417
 
x14666
x24721
x24721
x14344
x17949
x17949
x10417
x4182
 
 
 
 
x12
x34630
x34630
 
x12
x43863
x43863
 
x12
x54597
x54597
 
x12
 
 
 
x12390
x12390
x12390
x12
 
 
 
 
 
x11048
x44192
x11048
x29232
x87696
x29232
 
 
 
 
 
 
x12
x12
x2498
 
x1243
x5177
x1243
 
x1243
 
x96
x96
x96
 
x364
x364
x364
x389
x389
 
x389
x691
x364
 
x280
x280
x280
 
x280
x280
 
x280
x442
x442
x547
x561
x2805
x561
x561
x784
x4260
x852
x852
x551
x280
 
x96
x96
x96
 
x96
x96
 
x96
x396
x132
x171
x201
x219
x201
x213
x213
x213
x213
x213
x213
x213
x230
x230
x167
x885
x177
x177
x169
x96
 
x12
x12
x12
 
x562
x562
x760
x562
x830
x562
x646
x562
x562
 
 
 
 
 
 
x714
x714
 
 
 
x714
x5827
x27866
x27866
x27866
x6632
x714
x714
 
 
 
 
 
x12
x12
x12
 
x310
x310
x438
x438
x438
x557
x438
x618
x686
x618
x624
x624
x680
x680
x551
x310
x310
 
 
 
 
 
x12
x12
x12
 
x24
x24
x2256
x2256
x18393
x4481
x6443
x8701
x8380
x8386
x8386
x8695
x8695
x3899
x24
x24
 
x24
x24
x24
x24
 
x24
x24
x1960
x1960
x1960
x3325
x1960
x2001
x2001
x3282
x1960
x2093
x2093
x2093
x2093
x2093
x2093
x2093
x2993
x24
x24
 
x1147
x1147
 
x1147
x2282
x2282
x11364
x2282
x3106
x3106
x3106
x3106
x2657
x1147
x1147
 
x1825
x1825
 
x1825
x3638
x3638
x6257
x6257
x7553
x7553
x7553
x3638
x6101
x1825
x1825
 
x334
x334
x334
x334
 
x334
x334
x334
x1267
x2713
x2713
x2618
x1267
x1276
x1276
x1267
x1308
x1308
 
x1308
x1623
x334
x334
 
x978
x978
x5815
x5815
x13123
x13123
x13123
x978
x978
 
 
 
 
 
x12
x12
x2568
x2568
x2568
x4387
x4387
x2568
 
x5867
x5867
x11754
 
x11754
x11606
x11612
x11612
x5867
x5878
x5878
x5867
x5881
x5881
x5867
x5869
x5869
x5867
x5871
x5871
x5867
x11761
 
x5894
x5894
x5894
x5894
x5894
x5894
x5867
x5907
x5907
x5867
x5884
x5884
x5867
x5894
x5894
 
x5867
x5867
 
x12
x2349
x2349
x5183
x5183
x2349
x9208
x9220
x9220
x13734
x9208
x9297
x9208
x13621
x13621
x13621
x9208
x4670
x4677
x4677
 
x4677
x5140
x5140
x2349
 
x12
x1839
x1839
x3766
x3766
x1839
x4750
x4757
x4757
x5834
x5834
x5834
x3659
x3662
x3662
 
x3662
x3756
x3756
x1839
 
x12
x12
 
x1695
x1695
x3411
x3353
 
x3371
x3353
 
x3394
x3394
x3411
x1695
 
x4707
x4727
x14181
x4727
x4707
x6046
x18138
x6046
x6046
x6036
x4707
x4731
x4707
x6001
x6001
x6001
x4707
 
x3353
x3361
x3361
 
x3361
 
x3353
x3356
x3356
x3356
x3392
x3392
x1695
 
x12
x12
 
x1640
x1640
x3271
x3243
 
x3255
x3243
 
x3260
x3260
x3271
x1640
x4015
x4015
x4015
x3243
x3248
x3248
 
x3248
 
x3243
x3248
x3248
x3248
x3266
x3266
x1640
 
x12
x12
x1159
x1159
x1159
x1210
x1210
x1210
x1210
x1159
 
x12
x48
x48
x36
x12
x12
x12
x1113
x1113
x1113
x1130
x1130
x1130
x1130
x1113
 
x12
x12
x1100
x1100
x1100
x1117
x1117
x1117
x1117
x1100
 
x60
 
x12
x12
x999
x999
x999
x1990
x1990
x1990
 
x999
 
x12
x12
x993
x993
x993
x1982
x1982
x1982
 
x993
 
x12
x12
x984
x984
x984
x1968
x1968
x1968
 
x984
 
x12
x12
x512
x512
x512
x517
x517
x517
x517
x512
 
x12
x12
x12
x987
x987
x987
x1465
x1465
x1465
 
x1465
x987
 
x12
x12
x12
x1096
x1096
x1096
x2268
x2268
x2268
 
x2175
x2191
x2191
x2210
x2210
x2216
x2191
x2196
x2196
x2191
x2258
 
x2175
x2206
x2206
x2227
x1096
 
x12
x12
x1004
 
x1004
x1004
x1016
x1016
x1004
 
x12
x505
 
x505
x690
 
x690
x505
x854
x854
x854
x1144
x1144
 
x854
x1365
x1365
x668
 
x505
x630
 
x630
x505
 
x12
x12
 
x325
x325
x340
x1020
x340
x623
x325
x1287
x325
x488
x488
x429
x325
 
x12
x12
x12
x12
x12
x12
x12
x12
x12
x12
x12
x12
x12
x12
x12
x12
x12
x12
 
x12
 
x12
x12
 
x1147
x1147
x9106
x2697
x1147
 
x12
 
x12
x300
x300
x300
x494
x494
x494
x494
x494
x1008
x300
x300
 
x12
 
x12
x12
 
x359
x359
x359
x763
x763
x763
x763
x763
x1113
x359
x359
 
x12
x12
 
x3450
x2118
x2898
x966
x690
 
x665
x665
x665
x665
 
x665
x665
 
x12
x713
x1549
x1549
x1549
x1549
x2487
x2487
x2375
x2916
x2916
x2916
 
 
 
x713
x713

































































































































I


























































































































































































































































































































































































































































































































I










I










I






















I






























































































































































I














I




// Copyright 2018-2025 the Deno authors. MIT license.
// This module is browser compatible.

import { deepMerge } from "@std/collections/deep-merge";

/**
 * Copy of `import { isLeap } from "@std/datetime";` because it cannot be impoted as long as it is unstable.
 */
function isLeap(yearNumber: number): boolean {
  return (
    (yearNumber % 4 === 0 && yearNumber % 100 !== 0) || yearNumber % 400 === 0
  );
}

// ---------------------------
// Interfaces and base classes
// ---------------------------

interface Success<T> {
  ok: true;
  body: T;
}
interface Failure {
  ok: false;
}
type ParseResult<T> = Success<T> | Failure;

type ParserComponent<T = unknown> = (scanner: Scanner) => ParseResult<T>;

type Block = {
  type: "Block";
  value: Record<string, unknown>;
};
type Table = {
  type: "Table";
  keys: string[];
  value: Record<string, unknown>;
};
type TableArray = {
  type: "TableArray";
  keys: string[];
  value: Record<string, unknown>;
};

export class Scanner {
  #whitespace = /[ \t]/;
  #position = 0;
  #source: string;

  constructor(source: string) {
    this.#source = source;
  }

  get position() {
    return this.#position;
  }
  get source() {
    return this.#source;
  }

  /**
   * Get current character
   * @param index - relative index from current position
   */
  char(index = 0) {
    return this.#source[this.#position + index] ?? "";
  }

  /**
   * Get sliced string
   * @param start - start position relative from current position
   * @param end - end position relative from current position
   */
  slice(start: number, end: number): string {
    return this.#source.slice(this.#position + start, this.#position + end);
  }

  /**
   * Move position to next
   */
  next(count: number = 1) {
    this.#position += count;
  }

  skipWhitespaces() {
    while (this.#whitespace.test(this.char()) && !this.eof()) {
      this.next();
    }
    // Invalid if current char is other kinds of whitespace
    if (!this.isCurrentCharEOL() && /\s/.test(this.char())) {
      const escaped = "\\u" + this.char().charCodeAt(0).toString(16);
      const position = this.#position;
      throw new SyntaxError(
        `Cannot parse the TOML: It contains invalid whitespace at position '${position}': \`${escaped}\``,
      );
    }
  }

  nextUntilChar(options: { skipComments?: boolean } = { skipComments: true }) {
    while (!this.eof()) {
      const char = this.char();
      if (this.#whitespace.test(char) || this.isCurrentCharEOL()) {
        this.next();
      } else if (options.skipComments && this.char() === "#") {
        // entering comment
        while (!this.isCurrentCharEOL() && !this.eof()) {
          this.next();
        }
      } else {
        break;
      }
    }
  }

  /**
   * Position reached EOF or not
   */
  eof() {
    return this.#position >= this.#source.length;
  }

  isCurrentCharEOL() {
    return this.char() === "\n" || this.startsWith("\r\n");
  }

  startsWith(searchString: string) {
    return this.#source.startsWith(searchString, this.#position);
  }

  match(regExp: RegExp) {
    if (!regExp.sticky) {
      throw new Error(`RegExp ${regExp} does not have a sticky 'y' flag`);
    }
    regExp.lastIndex = this.#position;
    return this.#source.match(regExp);
  }
}

// -----------------------
// Utilities
// -----------------------

function success<T>(body: T): Success<T> {
  return { ok: true, body };
}
function failure(): Failure {
  return { ok: false };
}

/**
 * Creates a nested object from the keys and values.
 *
 * e.g. `unflat(["a", "b", "c"], 1)` returns `{ a: { b: { c: 1 } } }`
 */
export function unflat(
  keys: string[],
  values: unknown = { __proto__: null },
): Record<string, unknown> {
  return keys.reduceRight(
    (acc, key) => ({ [key]: acc }),
    values,
  ) as Record<string, unknown>;
}

function isObject(value: unknown): value is Record<string, unknown> {
  return typeof value === "object" && value !== null;
}

function getTargetValue(target: Record<string, unknown>, keys: string[]) {
  const key = keys[0];
  if (!key) {
    throw new Error(
      "Cannot parse the TOML: key length is not a positive number",
    );
  }
  return target[key];
}

function deepAssignTable(
  target: Record<string, unknown>,
  table: Table,
) {
  const { keys, type, value } = table;
  const currentValue = getTargetValue(target, keys);

  if (currentValue === undefined) {
    return Object.assign(target, unflat(keys, value));
  }
  if (Array.isArray(currentValue)) {
    const last = currentValue.at(-1);
    deepAssign(last, { type, keys: keys.slice(1), value });
    return target;
  }
  if (isObject(currentValue)) {
    deepAssign(currentValue, { type, keys: keys.slice(1), value });
    return target;
  }
  throw new Error("Unexpected assign");
}

function deepAssignTableArray(
  target: Record<string, unknown>,
  table: TableArray,
) {
  const { type, keys, value } = table;
  const currentValue = getTargetValue(target, keys);

  if (currentValue === undefined) {
    return Object.assign(target, unflat(keys, [value]));
  }
  if (Array.isArray(currentValue)) {
    if (table.keys.length === 1) {
      currentValue.push(value);
    } else {
      const last = currentValue.at(-1);
      deepAssign(last, {
        type: table.type,
        keys: table.keys.slice(1),
        value: table.value,
      });
    }
    return target;
  }
  if (isObject(currentValue)) {
    deepAssign(currentValue, { type, keys: keys.slice(1), value });
    return target;
  }
  throw new Error("Unexpected assign");
}

export function deepAssign(
  target: Record<string, unknown>,
  body: Block | Table | TableArray,
) {
  switch (body.type) {
    case "Block":
      return deepMerge(target, body.value);
    case "Table":
      return deepAssignTable(target, body);
    case "TableArray":
      return deepAssignTableArray(target, body);
  }
}

// ---------------------------------
// Parser combinators and generators
// ---------------------------------

// deno-lint-ignore no-explicit-any
function or<T extends readonly ParserComponent<any>[]>(
  parsers: T,
): ParserComponent<
  ReturnType<T[number]> extends ParseResult<infer R> ? R : Failure
> {
  return (scanner: Scanner) => {
    for (const parse of parsers) {
      const result = parse(scanner);
      if (result.ok) return result;
    }
    return failure();
  };
}

/** Join the parse results of the given parser into an array.
 *
 * If the parser fails at the first attempt, it will return an empty array.
 */
function join<T>(
  parser: ParserComponent<T>,
  separator: string,
): ParserComponent<T[]> {
  const Separator = character(separator);
  return (scanner: Scanner): ParseResult<T[]> => {
    const out: T[] = [];
    const first = parser(scanner);
    if (!first.ok) return success(out);
    out.push(first.body);
    while (!scanner.eof()) {
      if (!Separator(scanner).ok) break;
      const result = parser(scanner);
      if (!result.ok) {
        throw new SyntaxError(`Invalid token after "${separator}"`);
      }
      out.push(result.body);
    }
    return success(out);
  };
}

/** Join the parse results of the given parser into an array.
 *
 * This requires the parser to succeed at least once.
 */
function join1<T>(
  parser: ParserComponent<T>,
  separator: string,
): ParserComponent<T[]> {
  const Separator = character(separator);
  return (scanner: Scanner): ParseResult<T[]> => {
    const first = parser(scanner);
    if (!first.ok) return failure();
    const out: T[] = [first.body];
    while (!scanner.eof()) {
      if (!Separator(scanner).ok) break;
      const result = parser(scanner);
      if (!result.ok) {
        throw new SyntaxError(`Invalid token after "${separator}"`);
      }
      out.push(result.body);
    }
    return success(out);
  };
}

function kv<T>(
  keyParser: ParserComponent<string[]>,
  separator: string,
  valueParser: ParserComponent<T>,
): ParserComponent<{ [key: string]: unknown }> {
  const Separator = character(separator);
  return (scanner: Scanner): ParseResult<{ [key: string]: unknown }> => {
    const position = scanner.position;
    const key = keyParser(scanner);
    if (!key.ok) return failure();
    const sep = Separator(scanner);
    if (!sep.ok) {
      throw new SyntaxError(`key/value pair doesn't have "${separator}"`);
    }
    const value = valueParser(scanner);
    if (!value.ok) {
      const lineEndIndex = scanner.source.indexOf("\n", scanner.position);
      const endPosition = lineEndIndex > 0
        ? lineEndIndex
        : scanner.source.length;
      const line = scanner.source.slice(position, endPosition);
      throw new SyntaxError(`Cannot parse value on line '${line}'`);
    }
    return success(unflat(key.body, value.body));
  };
}

function merge(
  parser: ParserComponent<unknown[]>,
): ParserComponent<Record<string, unknown>> {
  return (scanner: Scanner): ParseResult<Record<string, unknown>> => {
    const result = parser(scanner);
    if (!result.ok) return failure();
    let body = { __proto__: null };
    for (const record of result.body) {
      if (typeof record === "object" && record !== null) {
        body = deepMerge(body, record);
      }
    }
    return success(body);
  };
}

function repeat<T>(
  parser: ParserComponent<T>,
): ParserComponent<T[]> {
  return (scanner: Scanner) => {
    const body: T[] = [];
    while (!scanner.eof()) {
      const result = parser(scanner);
      if (!result.ok) break;
      body.push(result.body);
      scanner.nextUntilChar();
    }
    if (body.length === 0) return failure();
    return success(body);
  };
}

function surround<T>(
  left: string,
  parser: ParserComponent<T>,
  right: string,
): ParserComponent<T> {
  const Left = character(left);
  const Right = character(right);
  return (scanner: Scanner) => {
    if (!Left(scanner).ok) {
      return failure();
    }
    const result = parser(scanner);
    if (!result.ok) {
      throw new SyntaxError(`Invalid token after "${left}"`);
    }
    if (!Right(scanner).ok) {
      throw new SyntaxError(
        `Not closed by "${right}" after started with "${left}"`,
      );
    }
    return success(result.body);
  };
}

function character(str: string) {
  return (scanner: Scanner): ParseResult<void> => {
    scanner.skipWhitespaces();
    if (!scanner.startsWith(str)) return failure();
    scanner.next(str.length);
    scanner.skipWhitespaces();
    return success(undefined);
  };
}

// -----------------------
// Parser components
// -----------------------

const BARE_KEY_REGEXP = /[A-Za-z0-9_-]+/y;
export function bareKey(scanner: Scanner): ParseResult<string> {
  scanner.skipWhitespaces();
  const key = scanner.match(BARE_KEY_REGEXP)?.[0];
  if (!key) return failure();
  scanner.next(key.length);
  return success(key);
}

function escapeSequence(scanner: Scanner): ParseResult<string> {
  if (scanner.char() !== "\\") return failure();
  scanner.next();
  // See https://toml.io/en/v1.0.0-rc.3#string
  switch (scanner.char()) {
    case "b":
      scanner.next();
      return success("\b");
    case "t":
      scanner.next();
      return success("\t");
    case "n":
      scanner.next();
      return success("\n");
    case "f":
      scanner.next();
      return success("\f");
    case "r":
      scanner.next();
      return success("\r");
    case "u":
    case "U": {
      // Unicode character
      const codePointLen = scanner.char() === "u" ? 4 : 6;
      const codePoint = parseInt("0x" + scanner.slice(1, 1 + codePointLen), 16);
      const str = String.fromCodePoint(codePoint);
      scanner.next(codePointLen + 1);
      return success(str);
    }
    case '"':
      scanner.next();
      return success('"');
    case "\\":
      scanner.next();
      return success("\\");
    default:
      throw new SyntaxError(
        `Invalid escape sequence: \\${scanner.char()}`,
      );
  }
}

export function basicString(scanner: Scanner): ParseResult<string> {
  scanner.skipWhitespaces();
  if (scanner.char() !== '"') return failure();
  scanner.next();
  const acc = [];
  while (scanner.char() !== '"' && !scanner.eof()) {
    if (scanner.char() === "\n") {
      throw new SyntaxError("Single-line string cannot contain EOL");
    }
    const escapedChar = escapeSequence(scanner);
    if (escapedChar.ok) {
      acc.push(escapedChar.body);
    } else {
      acc.push(scanner.char());
      scanner.next();
    }
  }
  if (scanner.eof()) {
    throw new SyntaxError(
      `Single-line string is not closed:\n${acc.join("")}`,
    );
  }
  scanner.next(); // skip last '""
  return success(acc.join(""));
}

export function literalString(scanner: Scanner): ParseResult<string> {
  scanner.skipWhitespaces();
  if (scanner.char() !== "'") return failure();
  scanner.next();
  const acc: string[] = [];
  while (scanner.char() !== "'" && !scanner.eof()) {
    if (scanner.char() === "\n") {
      throw new SyntaxError("Single-line string cannot contain EOL");
    }
    acc.push(scanner.char());
    scanner.next();
  }
  if (scanner.eof()) {
    throw new SyntaxError(
      `Single-line string is not closed:\n${acc.join("")}`,
    );
  }
  scanner.next(); // skip last "'"
  return success(acc.join(""));
}

export function multilineBasicString(
  scanner: Scanner,
): ParseResult<string> {
  scanner.skipWhitespaces();
  if (!scanner.startsWith('"""')) return failure();
  scanner.next(3);
  if (scanner.char() === "\n") {
    // The first newline (LF) is trimmed
    scanner.next();
  } else if (scanner.startsWith("\r\n")) {
    // The first newline (CRLF) is trimmed
    scanner.next(2);
  }
  const acc: string[] = [];
  while (!scanner.startsWith('"""') && !scanner.eof()) {
    // line ending backslash
    if (scanner.startsWith("\\\n")) {
      scanner.next();
      scanner.nextUntilChar({ skipComments: false });
      continue;
    } else if (scanner.startsWith("\\\r\n")) {
      scanner.next();
      scanner.nextUntilChar({ skipComments: false });
      continue;
    }
    const escapedChar = escapeSequence(scanner);
    if (escapedChar.ok) {
      acc.push(escapedChar.body);
    } else {
      acc.push(scanner.char());
      scanner.next();
    }
  }

  if (scanner.eof()) {
    throw new SyntaxError(
      `Multi-line string is not closed:\n${acc.join("")}`,
    );
  }
  // if ends with 4 `"`, push the fist `"` to string
  if (scanner.char(3) === '"') {
    acc.push('"');
    scanner.next();
  }
  scanner.next(3); // skip last '""""
  return success(acc.join(""));
}

export function multilineLiteralString(
  scanner: Scanner,
): ParseResult<string> {
  scanner.skipWhitespaces();
  if (!scanner.startsWith("'''")) return failure();
  scanner.next(3);
  if (scanner.char() === "\n") {
    // The first newline (LF) is trimmed
    scanner.next();
  } else if (scanner.startsWith("\r\n")) {
    // The first newline (CRLF) is trimmed
    scanner.next(2);
  }
  const acc: string[] = [];
  while (!scanner.startsWith("'''") && !scanner.eof()) {
    acc.push(scanner.char());
    scanner.next();
  }
  if (scanner.eof()) {
    throw new SyntaxError(
      `Multi-line string is not closed:\n${acc.join("")}`,
    );
  }
  // if ends with 4 `'`, push the fist `'` to string
  if (scanner.char(3) === "'") {
    acc.push("'");
    scanner.next();
  }
  scanner.next(3); // skip last "'''"
  return success(acc.join(""));
}

const BOOLEAN_REGEXP = /(?:true|false)\b/y;
export function boolean(scanner: Scanner): ParseResult<boolean> {
  scanner.skipWhitespaces();
  const match = scanner.match(BOOLEAN_REGEXP);
  if (!match) return failure();
  const string = match[0];
  scanner.next(string.length);
  const value = string === "true";
  return success(value);
}

const INFINITY_MAP = new Map<string, number>([
  ["inf", Infinity],
  ["+inf", Infinity],
  ["-inf", -Infinity],
]);
const INFINITY_REGEXP = /[+-]?inf\b/y;
export function infinity(scanner: Scanner): ParseResult<number> {
  scanner.skipWhitespaces();
  const match = scanner.match(INFINITY_REGEXP);
  if (!match) return failure();
  const string = match[0];
  scanner.next(string.length);
  const value = INFINITY_MAP.get(string)!;
  return success(value);
}

const NAN_REGEXP = /[+-]?nan\b/y;
export function nan(scanner: Scanner): ParseResult<number> {
  scanner.skipWhitespaces();
  const match = scanner.match(NAN_REGEXP);
  if (!match) return failure();
  const string = match[0];
  scanner.next(string.length);
  const value = NaN;
  return success(value);
}

export const dottedKey = join1(or([bareKey, basicString, literalString]), ".");

const BINARY_REGEXP = /0b[01]+(?:_[01]+)*\b/y;
export function binary(scanner: Scanner): ParseResult<number | string> {
  scanner.skipWhitespaces();
  const match = scanner.match(BINARY_REGEXP)?.[0];
  if (!match) return failure();
  scanner.next(match.length);
  const value = match.slice(2).replaceAll("_", "");
  const number = parseInt(value, 2);
  return isNaN(number) ? failure() : success(number);
}

const OCTAL_REGEXP = /0o[0-7]+(?:_[0-7]+)*\b/y;
export function octal(scanner: Scanner): ParseResult<number | string> {
  scanner.skipWhitespaces();
  const match = scanner.match(OCTAL_REGEXP)?.[0];
  if (!match) return failure();
  scanner.next(match.length);
  const value = match.slice(2).replaceAll("_", "");
  const number = parseInt(value, 8);
  return isNaN(number) ? failure() : success(number);
}

const HEX_REGEXP = /0x[0-9a-f]+(?:_[0-9a-f]+)*\b/yi;
export function hex(scanner: Scanner): ParseResult<number | string> {
  scanner.skipWhitespaces();
  const match = scanner.match(HEX_REGEXP)?.[0];
  if (!match) return failure();
  scanner.next(match.length);
  const value = match.slice(2).replaceAll("_", "");
  const number = parseInt(value, 16);
  return isNaN(number) ? failure() : success(number);
}

const INTEGER_REGEXP = /[+-]?(?:0|[1-9][0-9]*(?:_[0-9]+)*)\b/y;
export function integer(scanner: Scanner): ParseResult<number | string> {
  scanner.skipWhitespaces();
  const match = scanner.match(INTEGER_REGEXP)?.[0];
  if (!match) return failure();
  scanner.next(match.length);
  const value = match.replaceAll("_", "");
  const int = parseInt(value, 10);
  return success(int);
}

const FLOAT_REGEXP =
  /[+-]?(?:0|[1-9][0-9]*(?:_[0-9]+)*)(?:\.[0-9]+(?:_[0-9]+)*)?(?:e[+-]?[0-9]+(?:_[0-9]+)*)?\b/yi;
export function float(scanner: Scanner): ParseResult<number> {
  scanner.skipWhitespaces();
  const match = scanner.match(FLOAT_REGEXP)?.[0];
  if (!match) return failure();
  scanner.next(match.length);
  const value = match.replaceAll("_", "");
  const float = parseFloat(value);
  if (isNaN(float)) return failure();
  return success(float);
}

const DATE_TIME_REGEXP =
  /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})(?:[ 0-9TZ.:+-]+)?\b/y;
export function dateTime(scanner: Scanner): ParseResult<Date> {
  scanner.skipWhitespaces();
  const match = scanner.match(DATE_TIME_REGEXP);
  if (!match) return failure();
  const string = match[0];
  scanner.next(string.length);
  const groups = match.groups as { year: string; month: string; day: string };
  // special case if month is February
  if (groups.month == "02") {
    const days = parseInt(groups.day);
    if (days > 29) {
      throw new SyntaxError(`Invalid date string "${match}"`);
    }
    const year = parseInt(groups.year);
    if (days > 28 && !isLeap(year)) {
      throw new SyntaxError(`Invalid date string "${match}"`);
    }
  }
  const date = new Date(string.trim());
  // invalid date
  if (isNaN(date.getTime())) {
    throw new SyntaxError(`Invalid date string "${match}"`);
  }
  return success(date);
}

const LOCAL_TIME_REGEXP = /(\d{2}):(\d{2}):(\d{2})(?:\.[0-9]+)?\b/y;
export function localTime(scanner: Scanner): ParseResult<string> {
  scanner.skipWhitespaces();

  const match = scanner.match(LOCAL_TIME_REGEXP)?.[0];
  if (!match) return failure();
  scanner.next(match.length);
  return success(match);
}

export function arrayValue(scanner: Scanner): ParseResult<unknown[]> {
  scanner.skipWhitespaces();

  if (scanner.char() !== "[") return failure();
  scanner.next();

  const array: unknown[] = [];
  while (!scanner.eof()) {
    scanner.nextUntilChar();
    const result = value(scanner);
    if (!result.ok) break;
    array.push(result.body);
    scanner.skipWhitespaces();
    // may have a next item, but trailing comma is allowed at array
    if (scanner.char() !== ",") break;
    scanner.next();
  }
  scanner.nextUntilChar();

  if (scanner.char() !== "]") throw new SyntaxError("Array is not closed");
  scanner.next();

  return success(array);
}

export function inlineTable(
  scanner: Scanner,
): ParseResult<Record<string, unknown>> {
  scanner.nextUntilChar();
  if (scanner.char(1) === "}") {
    scanner.next(2);
    return success({ __proto__: null });
  }
  const pairs = surround("{", join(pair, ","), "}")(scanner);
  if (!pairs.ok) return failure();
  let table = { __proto__: null } as Record<string, unknown>;
  for (const pair of pairs.body) {
    table = deepMerge(table, pair);
  }
  return success(table);
}

export const value = or([
  multilineBasicString,
  multilineLiteralString,
  basicString,
  literalString,
  boolean,
  infinity,
  nan,
  dateTime,
  localTime,
  binary,
  octal,
  hex,
  float,
  integer,
  arrayValue,
  inlineTable,
]);

export const pair = kv(dottedKey, "=", value);

export function block(
  scanner: Scanner,
): ParseResult<Block> {
  scanner.nextUntilChar();
  const result = merge(repeat(pair))(scanner);
  if (result.ok) return success({ type: "Block", value: result.body });
  return failure();
}

export const tableHeader = surround("[", dottedKey, "]");

export function table(scanner: Scanner): ParseResult<Table> {
  scanner.nextUntilChar();
  const header = tableHeader(scanner);
  if (!header.ok) return failure();
  scanner.nextUntilChar();
  const b = block(scanner);
  return success({
    type: "Table",
    keys: header.body,
    value: b.ok ? b.body.value : { __proto__: null },
  });
}

export const tableArrayHeader = surround("[[", dottedKey, "]]");

export function tableArray(
  scanner: Scanner,
): ParseResult<TableArray> {
  scanner.nextUntilChar();
  const header = tableArrayHeader(scanner);
  if (!header.ok) return failure();
  scanner.nextUntilChar();
  const b = block(scanner);
  return success({
    type: "TableArray",
    keys: header.body,
    value: b.ok ? b.body.value : { __proto__: null },
  });
}

export function toml(
  scanner: Scanner,
): ParseResult<Record<string, unknown>> {
  const blocks = repeat(or([block, tableArray, table]))(scanner);
  if (!blocks.ok) return success({ __proto__: null });
  const body = blocks.body.reduce(deepAssign, { __proto__: null });
  return success(body);
}

function createParseErrorMessage(scanner: Scanner, message: string) {
  const string = scanner.source.slice(0, scanner.position);
  const lines = string.split("\n");
  const row = lines.length;
  const column = lines.at(-1)?.length ?? 0;
  return `Parse error on line ${row}, column ${column}: ${message}`;
}

export function parserFactory<T>(parser: ParserComponent<T>) {
  return (tomlString: string): T => {
    const scanner = new Scanner(tomlString);
    try {
      const result = parser(scanner);
      if (result.ok && scanner.eof()) return result.body;
      const message = `Unexpected character: "${scanner.char()}"`;
      throw new SyntaxError(createParseErrorMessage(scanner, message));
    } catch (error) {
      if (error instanceof Error) {
        throw new SyntaxError(createParseErrorMessage(scanner, error.message));
      }
      const message = "Invalid error type caught";
      throw new SyntaxError(createParseErrorMessage(scanner, message));
    }
  };
}