@jarenjs/josl 0.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/machine.js ADDED
@@ -0,0 +1,1252 @@
1
+ //#region JOSL incremental reader
2
+ // JOSL is a superset of TOML 1.0; this machine is both the full parser and
3
+ // the streaming reader — `parse()` is literally `feed(text)` + `end()`, so
4
+ // there is a single grammar code path.
5
+ //
6
+ // The design exploits TOML's line orientation. A *logical line* is a
7
+ // physical line extended across the newlines that TOML permits inside a
8
+ // value (multi-line strings and multi-line arrays). The machine cuts the
9
+ // incoming chunk stream into logical lines with a tiny cutter FSM that
10
+ // tracks just enough state (string context, escape, bracket depth) to know
11
+ // which newlines terminate a line — the cutter never allocates and can
12
+ // stop mid-token at any chunk boundary, which is what makes token-by-token
13
+ // LLM output feedable. Each completed logical line is then parsed by an
14
+ // ordinary recursive-descent value parser, and events are emitted in
15
+ // document order (unlike `JSON.parse`'s bottom-up reviver).
16
+ //
17
+ // JOSL extensions over TOML 1.0 (rejected in `mode: 'toml'`):
18
+ // null key = null
19
+ // bigint key = 123n / 0xffn (unsafe plain ints auto-promote)
20
+ // regexp key = /pattern/flags (JS literal syntax)
21
+ // root array [[]] starts/appends an element of a root-level array;
22
+ // later [table] / [[array]] headers are scoped to the
23
+ // current root element.
24
+
25
+ import {
26
+ CC_TAB,
27
+ CC_LF,
28
+ CC_CR,
29
+ CC_SPACE,
30
+ CC_DQUOTE,
31
+ CC_HASH,
32
+ CC_SQUOTE,
33
+ CC_COMMA,
34
+ CC_MINUS,
35
+ CC_DOT,
36
+ CC_SLASH,
37
+ CC_COLON,
38
+ CC_EQ,
39
+ CC_LBRACKET,
40
+ CC_BACKSLASH,
41
+ CC_RBRACKET,
42
+ CC_UNDERSCORE,
43
+ CC_0,
44
+ CC_LOWER_B,
45
+ CC_LOWER_O,
46
+ CC_LOWER_X,
47
+ CC_PLUS,
48
+ CC_LBRACE,
49
+ CC_RBRACE,
50
+ CC_DEL,
51
+ isDigitCode,
52
+ isAsciiLetterCode,
53
+ } from '@jarenjs/core/scan';
54
+
55
+ import { JoslSyntaxError } from './errors.js';
56
+ import {
57
+ LocalDate,
58
+ LocalTime,
59
+ LocalDateTime,
60
+ isValidDateParts,
61
+ isValidTimeParts,
62
+ } from './values.js';
63
+ import {
64
+ getOwn, columnOf, feedMachine, beginParseAll, stickyExec, RE_DATETIME, RE_TIMEONLY,
65
+ } from './util.js';
66
+ import { setObjectMember } from '@jarenjs/core/object';
67
+ import { countCharCode } from '@jarenjs/core/string';
68
+
69
+ function isBareKeyCode(c) {
70
+ return isAsciiLetterCode(c)
71
+ || isDigitCode(c)
72
+ || c === CC_MINUS
73
+ || c === CC_UNDERSCORE;
74
+ }
75
+
76
+ // Cutter states: which context the chunk scanner is inside.
77
+ const S_NONE = 0;
78
+ const S_BASIC = 1; // "..." (single line)
79
+ const S_LITERAL = 2; // '...' (single line)
80
+ const S_ML_BASIC = 3; // """..."""
81
+ const S_ML_LITERAL = 4; // '''...'''
82
+ const S_COMMENT = 5;
83
+
84
+ // Runs of characters that cannot end a logical line or change the cutter's
85
+ // state. The cutter skips them with the regex engine rather than stepping
86
+ // per character, which is what keeps its pass cheap next to the parser's.
87
+ // `\n` stays in every stop set so the cutter can count the physical lines a
88
+ // logical line spans without a separate walk over it.
89
+ // The basic/literal classes serve both the single- and multi-line states:
90
+ // a `'` is inert inside a basic string and a `"` inside a literal one.
91
+ const RUN_NONE = /[^\n"'#[\]]*/y;
92
+ const RUN_BASIC = /[^\n"\\]*/y;
93
+ const RUN_LITERAL = /[^\n']*/y;
94
+
95
+ // Advance past a run of inert characters. `test` on a `*` pattern always
96
+ // matches (possibly empty) and, unlike `exec`, allocates no match object.
97
+ function skipRun(re, buf, pos) {
98
+ re.lastIndex = pos;
99
+ re.test(buf);
100
+ return re.lastIndex;
101
+ }
102
+
103
+ // Number token patterns; the date-time pair is shared with the JSONX
104
+ // scalar reader (util.js). Sticky (y) so they match in place.
105
+ const RE_HEX = /0x[0-9a-fA-F](?:_?[0-9a-fA-F])*(n?)/y;
106
+ const RE_OCT = /0o[0-7](?:_?[0-7])*(n?)/y;
107
+ const RE_BIN = /0b[01](?:_?[01])*(n?)/y;
108
+ const RE_NUM = /[+-]?(?:0|[1-9](?:_?[0-9])*)(?:\.[0-9](?:_?[0-9])*)?(?:[eE][+-]?[0-9](?:_?[0-9])*)?(n?)/y;
109
+
110
+ // Whether `pos` is a position a value may legally end at. Separate from
111
+ // the machine's throwing `checkValueEnd` so the number scanner can test a
112
+ // candidate token without committing to it.
113
+ function atValueEnd(line, pos) {
114
+ if (pos >= line.length)
115
+ return true;
116
+ const c = line.charCodeAt(pos);
117
+ return c === CC_SPACE || c === CC_TAB || c === CC_LF || c === CC_CR
118
+ || c === CC_COMMA || c === CC_RBRACKET || c === CC_RBRACE || c === CC_HASH;
119
+ }
120
+
121
+ const INT64_MIN = -(2n ** 63n);
122
+ const INT64_MAX = 2n ** 63n - 1n;
123
+ const SAFE_MIN = BigInt(Number.MIN_SAFE_INTEGER);
124
+ const SAFE_MAX = BigInt(Number.MAX_SAFE_INTEGER);
125
+
126
+ export class JoslMachine {
127
+ /**
128
+ * @param {object} [options] - Reader options
129
+ * @param {'josl'|'toml'} [options.mode] - 'toml' rejects JOSL extensions
130
+ * @param {(event: object) => void} [options.onEvent] - Document-order
131
+ * event sink: {type:'table'|'table-array'|'root-item'|'pair', path, ...}
132
+ */
133
+ constructor(options = {}) {
134
+ this.mode = options.mode === 'toml' ? 'toml' : 'josl';
135
+ this.onEvent = options.onEvent ?? null;
136
+ // Logical-line sink used by the CST layer: reports each line's source
137
+ // span, and the span of a pair's value inside it, so a rewriter can
138
+ // replace a value without disturbing the bytes around it.
139
+ this.onLine = options.onLine ?? null;
140
+ this.lineValueStart = -1;
141
+ this.lineValueEnd = -1;
142
+ // chunk cutter state
143
+ this.buf = '';
144
+ this.scanPos = 0;
145
+ this.scanState = S_NONE;
146
+ this.scanDepth = 0;
147
+ this.scanNl = 0; // newlines seen inside the logical line being cut
148
+ this.startLine = 1; // physical line where the current logical line begins
149
+ // Absolute physical line of `this.line`'s index 0, so error positions
150
+ // read the same whether `this.line` is one cut line or the whole source.
151
+ this.lineOrigin = 1;
152
+ this.started = false;
153
+ this.ended = false;
154
+ // document state
155
+ this.rootValue = undefined;
156
+ this.rootIsArray = false;
157
+ this.current = null; // current [table] target
158
+ this.currentPath = []; // absolute path of `current` (indices for [[..]])
159
+ this.meta = new WeakMap(); // container flags, see assign/open methods
160
+ // per-logical-line parse context (for error positions)
161
+ this.line = '';
162
+ }
163
+
164
+ //#region public surface
165
+
166
+ /**
167
+ * Feed the next chunk of source text; chunks may split any token.
168
+ * @param {string} chunk - Next piece of the document
169
+ * @returns {this} The machine, for chaining
170
+ */
171
+ feed(chunk) {
172
+ return feedMachine(this, chunk);
173
+ }
174
+
175
+ /**
176
+ * Finish the document, flushing any pending logical line.
177
+ * @returns {*} The completed root value
178
+ */
179
+ end() {
180
+ if (this.ended)
181
+ return this.root();
182
+ this.ended = true;
183
+ this.scan();
184
+ if (this.buf.length !== 0) {
185
+ // no trailing-\r strip here: a \r not followed by \n is a bare
186
+ // carriage return, which the grammar forbids (consumeLine handles
187
+ // the \r\n case)
188
+ const line = this.buf;
189
+ this.buf = '';
190
+ this.scanPos = 0;
191
+ this.lineOrigin = this.startLine;
192
+ this.parseLine(line);
193
+ }
194
+ return this.root();
195
+ }
196
+
197
+ /**
198
+ * Parse a complete document in one pass. Every value parser already stops
199
+ * at the newlines TOML forbids a construct from crossing, so with the
200
+ * whole text in hand the parser finds each logical line's end itself and
201
+ * the cutter's separate pass over the source is not needed. `feed`/`end`
202
+ * keep the cutter because a chunk can stop mid-token, where only a
203
+ * side-effect-free pre-pass can decide whether a line is complete.
204
+ * @param {string} text - The entire document
205
+ * @returns {*} The completed root value
206
+ */
207
+ parseAll(text) {
208
+ text = beginParseAll(this, text);
209
+ // positions are offsets into the whole source, which starts at line 1
210
+ this.lineOrigin = 1;
211
+ const tracking = this.onEvent !== null;
212
+ const len = text.length;
213
+ let pos = 0;
214
+ while (pos < len) {
215
+ this.lineValueStart = -1;
216
+ this.lineValueEnd = -1;
217
+ const end = this.parseLine(text, pos);
218
+ const next = end < len && text.charCodeAt(end) === CC_LF ? end + 1 : end;
219
+ if (this.onLine !== null)
220
+ this.onLine(pos, next, this.lineValueStart, this.lineValueEnd);
221
+ if (tracking) {
222
+ // only events need the logical line's own number; errors derive
223
+ // theirs from lineOrigin and the offset
224
+ let n = this.startLine;
225
+ for (let i = pos; i < next; ++i)
226
+ if (text.charCodeAt(i) === CC_LF)
227
+ n++;
228
+ this.startLine = n;
229
+ }
230
+ pos = next;
231
+ }
232
+ return this.root();
233
+ }
234
+
235
+ /**
236
+ * The (possibly still growing) root value: `{}`-rooted for documents,
237
+ * `[]`-rooted after a `[[]]` header. Undefined content yields `{}`.
238
+ * @returns {*} Current root value
239
+ */
240
+ root() {
241
+ if (this.rootValue === undefined)
242
+ this.rootValue = {};
243
+ return this.rootValue;
244
+ }
245
+
246
+ //#endregion
247
+
248
+ //#region chunk cutter
249
+
250
+ // Scan the buffered text for the newlines that terminate logical
251
+ // lines; each completed line goes to parseLine(). Stalls (saves
252
+ // position and exits) when a decision needs lookahead that has not
253
+ // arrived yet — e.g. a quote that may open a triple delimiter. The
254
+ // consumed prefix is compacted once per call, not per line, so whole-
255
+ // document parses stay linear in document size.
256
+ scan() {
257
+ const buf = this.buf;
258
+ let pos = this.scanPos;
259
+ let lineStart = 0;
260
+ let state = this.scanState;
261
+ let depth = this.scanDepth;
262
+ let nl = this.scanNl;
263
+ const ended = this.ended;
264
+ outer:
265
+ while (pos < buf.length) {
266
+ switch (state) {
267
+ case S_NONE: {
268
+ pos = skipRun(RUN_NONE, buf, pos);
269
+ if (pos >= buf.length)
270
+ break outer;
271
+ const c = buf.charCodeAt(pos);
272
+ if (c === CC_LF) {
273
+ if (depth === 0) {
274
+ this.cutLine(buf, lineStart, pos, nl);
275
+ lineStart = pos + 1;
276
+ nl = 0;
277
+ }
278
+ else
279
+ nl++;
280
+ pos++;
281
+ break;
282
+ }
283
+ if (c === CC_DQUOTE || c === CC_SQUOTE) {
284
+ if (pos + 2 >= buf.length && !ended)
285
+ break outer; // may be a triple delimiter split across chunks
286
+ if (buf.charCodeAt(pos + 1) === c && buf.charCodeAt(pos + 2) === c) {
287
+ state = c === CC_DQUOTE ? S_ML_BASIC : S_ML_LITERAL;
288
+ pos += 3;
289
+ }
290
+ else {
291
+ state = c === CC_DQUOTE ? S_BASIC : S_LITERAL;
292
+ pos++;
293
+ }
294
+ break;
295
+ }
296
+ if (c === CC_HASH) {
297
+ state = S_COMMENT;
298
+ pos++;
299
+ break;
300
+ }
301
+ if (c === CC_LBRACKET)
302
+ depth++;
303
+ else if (c === CC_RBRACKET && depth > 0)
304
+ depth--;
305
+ pos++;
306
+ break;
307
+ }
308
+ case S_COMMENT: {
309
+ // nothing but the newline can end a comment, so jump straight to it
310
+ const at = buf.indexOf('\n', pos);
311
+ if (at < 0) {
312
+ pos = buf.length;
313
+ break outer;
314
+ }
315
+ if (depth === 0) {
316
+ this.cutLine(buf, lineStart, at, nl);
317
+ lineStart = at + 1;
318
+ nl = 0;
319
+ }
320
+ else
321
+ nl++;
322
+ state = S_NONE;
323
+ pos = at + 1;
324
+ break;
325
+ }
326
+ case S_BASIC:
327
+ case S_LITERAL: {
328
+ const basic = state === S_BASIC;
329
+ pos = skipRun(basic ? RUN_BASIC : RUN_LITERAL, buf, pos);
330
+ if (pos >= buf.length)
331
+ break outer;
332
+ const c = buf.charCodeAt(pos);
333
+ if (c === CC_LF) {
334
+ // unterminated single-line string: the line parser reports it
335
+ state = S_NONE;
336
+ if (depth === 0) {
337
+ this.cutLine(buf, lineStart, pos, nl);
338
+ lineStart = pos + 1;
339
+ nl = 0;
340
+ }
341
+ else
342
+ nl++;
343
+ pos++;
344
+ break;
345
+ }
346
+ if (basic && c === CC_BACKSLASH) {
347
+ if (pos + 1 >= buf.length && !ended)
348
+ break outer;
349
+ pos += 2;
350
+ break;
351
+ }
352
+ state = S_NONE; // the run only stops on the closing quote
353
+ pos++;
354
+ break;
355
+ }
356
+ case S_ML_BASIC:
357
+ case S_ML_LITERAL: {
358
+ const basic = state === S_ML_BASIC;
359
+ pos = skipRun(basic ? RUN_BASIC : RUN_LITERAL, buf, pos);
360
+ if (pos >= buf.length)
361
+ break outer;
362
+ const c = buf.charCodeAt(pos);
363
+ if (c === CC_LF) {
364
+ nl++; // multi-line strings carry newlines inside the logical line
365
+ pos++;
366
+ break;
367
+ }
368
+ if (basic && c === CC_BACKSLASH) {
369
+ if (pos + 1 >= buf.length && !ended)
370
+ break outer;
371
+ pos += 2;
372
+ break;
373
+ }
374
+ const q = basic ? CC_DQUOTE : CC_SQUOTE;
375
+ let run = pos;
376
+ while (run < buf.length && buf.charCodeAt(run) === q)
377
+ run++;
378
+ if (run === buf.length && run - pos < 3 && !ended)
379
+ break outer; // quote run may continue in the next chunk
380
+ if (run - pos >= 3)
381
+ state = S_NONE;
382
+ pos = run;
383
+ break;
384
+ }
385
+ }
386
+ }
387
+ if (lineStart !== 0) {
388
+ this.buf = buf.slice(lineStart);
389
+ this.scanPos = pos - lineStart;
390
+ }
391
+ else
392
+ this.scanPos = pos;
393
+ this.scanState = state;
394
+ this.scanDepth = depth;
395
+ this.scanNl = nl;
396
+ }
397
+
398
+ // `innerNl` is how many newlines the cutter already counted inside this
399
+ // logical line, so the physical-line bookkeeping costs no extra walk.
400
+ cutLine(buf, start, nlPos, innerNl) {
401
+ const end = nlPos > start && buf.charCodeAt(nlPos - 1) === CC_CR
402
+ ? nlPos - 1
403
+ : nlPos;
404
+ if (end > start) {
405
+ this.lineOrigin = this.startLine;
406
+ this.parseLine(buf.slice(start, end));
407
+ }
408
+ this.startLine += innerNl + 1;
409
+ }
410
+
411
+ //#endregion
412
+
413
+ //#region errors & events
414
+
415
+ err(pos, message, hint) {
416
+ const line = this.line;
417
+ throw new JoslSyntaxError(
418
+ message,
419
+ this.lineOrigin + countCharCode(line, 0x0A, 0, Math.min(pos, line.length)),
420
+ columnOf(line, Math.min(pos, line.length)),
421
+ hint);
422
+ }
423
+
424
+ emit(event) {
425
+ if (this.onEvent !== null)
426
+ this.onEvent(event);
427
+ }
428
+
429
+ //#endregion
430
+
431
+ //#region logical line parser
432
+
433
+ // Parses one logical line and returns the offset it ended at: the index
434
+ // of the terminating newline, or the end of the text. A cut line carries
435
+ // no terminator, so the newline branches only fire for the whole-document
436
+ // driver, which parses straight out of the source.
437
+ parseLine(line, pos = 0) {
438
+ this.line = line;
439
+ pos = this.skipWs(line, pos);
440
+ if (pos >= line.length)
441
+ return pos;
442
+ const c = line.charCodeAt(pos);
443
+ if (c === CC_LF)
444
+ return pos;
445
+ if (c === CC_CR && line.charCodeAt(pos + 1) === CC_LF)
446
+ return pos + 1;
447
+ if (c === CC_HASH)
448
+ return this.checkComment(line, pos);
449
+ if (c === CC_LBRACKET)
450
+ return this.parseHeader(line, pos);
451
+ return this.parsePair(line, pos);
452
+ }
453
+
454
+ skipWs(line, pos) {
455
+ while (pos < line.length) {
456
+ const c = line.charCodeAt(pos);
457
+ if (c !== CC_SPACE && c !== CC_TAB)
458
+ break;
459
+ pos++;
460
+ }
461
+ return pos;
462
+ }
463
+
464
+ // whitespace, newlines and comments — legal between array elements
465
+ skipWsNlComment(line, pos) {
466
+ while (pos < line.length) {
467
+ const c = line.charCodeAt(pos);
468
+ if (c === CC_SPACE || c === CC_TAB || c === CC_LF || c === CC_CR) {
469
+ pos++;
470
+ continue;
471
+ }
472
+ if (c === CC_HASH) {
473
+ pos = this.checkComment(line, pos);
474
+ continue;
475
+ }
476
+ break;
477
+ }
478
+ return pos;
479
+ }
480
+
481
+ // Consumes the rest of the logical line and returns the offset it ended
482
+ // at, so the whole-document driver knows where the next one begins.
483
+ expectLineEnd(line, pos) {
484
+ pos = this.skipWs(line, pos);
485
+ if (pos >= line.length)
486
+ return pos;
487
+ const c = line.charCodeAt(pos);
488
+ if (c === CC_LF)
489
+ return pos;
490
+ if (c === CC_CR && line.charCodeAt(pos + 1) === CC_LF)
491
+ return pos + 1;
492
+ if (c !== CC_HASH)
493
+ this.err(pos, 'unexpected content after expression');
494
+ return this.checkComment(line, pos);
495
+ }
496
+
497
+ // pos sits on '#'; validates comment content and returns the position
498
+ // of the terminating newline (or end of line)
499
+ checkComment(line, pos) {
500
+ pos++;
501
+ while (pos < line.length) {
502
+ const c = line.charCodeAt(pos);
503
+ if (c === CC_LF)
504
+ return pos;
505
+ if (c === CC_CR && line.charCodeAt(pos + 1) === CC_LF)
506
+ return pos + 1;
507
+ if ((c < 0x20 && c !== CC_TAB) || c === CC_DEL)
508
+ this.err(pos, 'control characters are not allowed in comments');
509
+ pos++;
510
+ }
511
+ return pos;
512
+ }
513
+
514
+ //#endregion
515
+
516
+ //#region headers
517
+
518
+ parseHeader(line, pos) {
519
+ pos++; // consume '['
520
+ if (pos < line.length && line.charCodeAt(pos) === CC_LBRACKET) {
521
+ pos++; // consume second '['
522
+ let p = this.skipWs(line, pos);
523
+ if (p < line.length && line.charCodeAt(p) === CC_RBRACKET
524
+ && p + 1 < line.length && line.charCodeAt(p + 1) === CC_RBRACKET) {
525
+ // [[]] — JOSL root array element
526
+ this.openRootItem(p);
527
+ return this.expectLineEnd(line, p + 2);
528
+ }
529
+ const [keys, after] = this.parseKeys(line, pos);
530
+ p = after;
531
+ if (p + 1 >= line.length
532
+ || line.charCodeAt(p) !== CC_RBRACKET
533
+ || line.charCodeAt(p + 1) !== CC_RBRACKET)
534
+ this.err(p, "expected ']]' to close array-of-tables header");
535
+ this.openArrayTable(keys, p);
536
+ return this.expectLineEnd(line, p + 2);
537
+ }
538
+ const [keys, after] = this.parseKeys(line, pos);
539
+ if (after >= line.length || line.charCodeAt(after) !== CC_RBRACKET)
540
+ this.err(after, "expected ']' to close table header");
541
+ this.openTable(keys, after);
542
+ return this.expectLineEnd(line, after + 1);
543
+ }
544
+
545
+ headerBase() {
546
+ if (this.rootIsArray)
547
+ return [this.rootValue[this.rootValue.length - 1], [this.rootValue.length - 1]];
548
+ if (this.rootValue === undefined)
549
+ this.rootValue = {};
550
+ return [this.rootValue, []];
551
+ }
552
+
553
+ // walk the intermediate keys of a header, creating implicit tables and
554
+ // descending into the last element of arrays-of-tables
555
+ navigate(keys, pos) {
556
+ let [t, path] = this.headerBase();
557
+ for (let i = 0; i < keys.length - 1; ++i) {
558
+ const k = keys[i];
559
+ const ex = getOwn(t, k);
560
+ if (ex === undefined) {
561
+ const nt = {};
562
+ this.meta.set(nt, { implicit: true });
563
+ setObjectMember(t, k, nt);
564
+ t = nt;
565
+ path = path.concat(k);
566
+ continue;
567
+ }
568
+ if (Array.isArray(ex)) {
569
+ const m = this.meta.get(ex);
570
+ if (m === undefined || m.aot !== true)
571
+ this.err(pos, `cannot use static array '${k}' as a table`);
572
+ t = ex[ex.length - 1];
573
+ path = path.concat(k, ex.length - 1);
574
+ continue;
575
+ }
576
+ if (ex !== null && typeof ex === 'object') {
577
+ const m = this.meta.get(ex);
578
+ if (m !== undefined && m.inline === true)
579
+ this.err(pos, `cannot extend inline table '${k}'`);
580
+ // dotted-defined tables may be traversed as intermediates; only
581
+ // opening one as a header's final key is forbidden (spec 1.0)
582
+ t = ex;
583
+ path = path.concat(k);
584
+ continue;
585
+ }
586
+ this.err(pos, `key '${k}' conflicts with an existing value`);
587
+ }
588
+ return [t, path];
589
+ }
590
+
591
+ openTable(keys, pos) {
592
+ const [t, path] = this.navigate(keys, pos);
593
+ const k = keys[keys.length - 1];
594
+ const ex = getOwn(t, k);
595
+ if (ex === undefined) {
596
+ const nt = {};
597
+ this.meta.set(nt, { explicit: true });
598
+ setObjectMember(t, k, nt);
599
+ this.current = nt;
600
+ }
601
+ else if (ex !== null && typeof ex === 'object' && !Array.isArray(ex)) {
602
+ const m = this.meta.get(ex);
603
+ if (m === undefined || m.explicit === true || m.inline === true || m.dotted === true)
604
+ this.err(pos, `table '${keys.join('.')}' is already defined`);
605
+ m.explicit = true;
606
+ this.current = ex;
607
+ }
608
+ else
609
+ this.err(pos, `key '${keys.join('.')}' conflicts with an existing value`);
610
+ this.currentPath = path.concat(k);
611
+ this.emit({ type: 'table', path: this.currentPath, line: this.startLine });
612
+ }
613
+
614
+ openArrayTable(keys, pos) {
615
+ const [t, path] = this.navigate(keys, pos);
616
+ const k = keys[keys.length - 1];
617
+ let arr = getOwn(t, k);
618
+ if (arr === undefined) {
619
+ arr = [];
620
+ this.meta.set(arr, { aot: true });
621
+ setObjectMember(t, k, arr);
622
+ }
623
+ else if (!Array.isArray(arr) || this.meta.get(arr)?.aot !== true)
624
+ this.err(pos, `key '${keys.join('.')}' is not an array of tables`);
625
+ const el = {};
626
+ arr.push(el);
627
+ this.current = el;
628
+ this.currentPath = path.concat(k, arr.length - 1);
629
+ this.emit({ type: 'table-array', path: this.currentPath, line: this.startLine });
630
+ }
631
+
632
+ openRootItem(pos) {
633
+ if (this.mode === 'toml')
634
+ this.err(pos, 'root arrays ([[]]) are a JOSL extension',
635
+ 'name the array, e.g. [[items]], for TOML compatibility');
636
+ if (this.rootValue === undefined) {
637
+ this.rootValue = [];
638
+ this.rootIsArray = true;
639
+ this.meta.set(this.rootValue, { aot: true });
640
+ }
641
+ else if (!this.rootIsArray)
642
+ this.err(pos, 'cannot mix a root table and a root array',
643
+ 'a document that starts with key-value pairs has an object root');
644
+ const el = {};
645
+ this.rootValue.push(el);
646
+ this.current = el;
647
+ this.currentPath = [this.rootValue.length - 1];
648
+ this.emit({
649
+ type: 'root-item',
650
+ path: this.currentPath,
651
+ index: this.rootValue.length - 1,
652
+ line: this.startLine,
653
+ });
654
+ }
655
+
656
+ //#endregion
657
+
658
+ //#region key-value pairs
659
+
660
+ parsePair(line, pos) {
661
+ if (this.current === null) {
662
+ if (this.rootIsArray)
663
+ this.err(pos, 'expected [[]] before key-value pairs in a root array');
664
+ if (this.rootValue === undefined)
665
+ this.rootValue = {};
666
+ this.current = this.rootValue;
667
+ this.currentPath = [];
668
+ }
669
+ const [keys, afterKeys] = this.parseKeys(line, pos);
670
+ let p = afterKeys;
671
+ if (p >= line.length || line.charCodeAt(p) !== CC_EQ)
672
+ this.err(p, "expected '=' after key", 'a key-value pair looks like: key = value');
673
+ p = this.skipWs(line, p + 1);
674
+ const [value, afterValue] = this.parseValue(line, p);
675
+ if (this.onLine !== null) {
676
+ this.lineValueStart = p;
677
+ this.lineValueEnd = afterValue;
678
+ }
679
+ this.assignPair(keys, value, pos);
680
+ if (this.onEvent !== null)
681
+ this.emit({
682
+ type: 'pair',
683
+ path: this.currentPath.concat(keys),
684
+ key: keys[keys.length - 1],
685
+ value,
686
+ line: this.startLine,
687
+ });
688
+ return this.expectLineEnd(line, afterValue);
689
+ }
690
+
691
+ assignPair(keys, value, pos) {
692
+ let t = this.current;
693
+ for (let i = 0; i < keys.length - 1; ++i) {
694
+ const k = keys[i];
695
+ const ex = getOwn(t, k);
696
+ if (ex === undefined) {
697
+ const nt = {};
698
+ this.meta.set(nt, { dotted: true });
699
+ setObjectMember(t, k, nt);
700
+ t = nt;
701
+ continue;
702
+ }
703
+ if (ex !== null && typeof ex === 'object' && !Array.isArray(ex)
704
+ && this.meta.get(ex)?.dotted === true) {
705
+ t = ex;
706
+ continue;
707
+ }
708
+ this.err(pos, `dotted key '${k}' cannot extend a table defined elsewhere`);
709
+ }
710
+ const k = keys[keys.length - 1];
711
+ if (Object.hasOwn(t, k))
712
+ this.err(pos, `duplicate key '${k}'`);
713
+ setObjectMember(t, k, value);
714
+ }
715
+
716
+ parseKeys(line, pos) {
717
+ const keys = [];
718
+ for (;;) {
719
+ pos = this.skipWs(line, pos);
720
+ if (pos >= line.length)
721
+ this.err(pos, 'expected a key');
722
+ const c = line.charCodeAt(pos);
723
+ if (c === CC_DQUOTE) {
724
+ const [s, p] = this.parseBasicString(line, pos);
725
+ keys.push(s);
726
+ pos = p;
727
+ }
728
+ else if (c === CC_SQUOTE) {
729
+ const [s, p] = this.parseLiteralString(line, pos);
730
+ keys.push(s);
731
+ pos = p;
732
+ }
733
+ else {
734
+ const start = pos;
735
+ while (pos < line.length && isBareKeyCode(line.charCodeAt(pos)))
736
+ pos++;
737
+ if (pos === start)
738
+ this.err(pos, 'expected a key');
739
+ keys.push(line.slice(start, pos));
740
+ }
741
+ pos = this.skipWs(line, pos);
742
+ if (pos < line.length && line.charCodeAt(pos) === CC_DOT) {
743
+ pos++;
744
+ continue;
745
+ }
746
+ return [keys, pos];
747
+ }
748
+ }
749
+
750
+ //#endregion
751
+
752
+ //#region values
753
+
754
+ parseValue(line, pos) {
755
+ if (pos >= line.length)
756
+ this.err(pos, 'expected a value');
757
+ const c = line.charCodeAt(pos);
758
+ if (c === CC_DQUOTE) {
759
+ if (line.startsWith('"""', pos))
760
+ return this.parseMlBasicString(line, pos);
761
+ return this.parseBasicString(line, pos);
762
+ }
763
+ if (c === CC_SQUOTE) {
764
+ if (line.startsWith("'''", pos))
765
+ return this.parseMlLiteralString(line, pos);
766
+ return this.parseLiteralString(line, pos);
767
+ }
768
+ if (c === CC_LBRACKET)
769
+ return this.parseArray(line, pos);
770
+ if (c === CC_LBRACE)
771
+ return this.parseInlineTable(line, pos);
772
+ if (c === CC_SLASH)
773
+ return this.parseRegExp(line, pos);
774
+ if (c === CC_PLUS || c === CC_MINUS) {
775
+ const d = pos + 1 < line.length ? line.charCodeAt(pos + 1) : -1;
776
+ if (isDigitCode(d))
777
+ return this.parseNumber(line, pos);
778
+ if (isAsciiLetterCode(d))
779
+ return this.parseSignedWord(line, pos);
780
+ this.err(pos, 'expected a number after sign');
781
+ }
782
+ if (isDigitCode(c))
783
+ return this.parseDateTimeOrNumber(line, pos);
784
+ if (isAsciiLetterCode(c))
785
+ return this.parseWord(line, pos);
786
+ this.err(pos, 'invalid value');
787
+ }
788
+
789
+ checkValueEnd(line, pos) {
790
+ if (atValueEnd(line, pos))
791
+ return pos;
792
+ this.err(pos, 'unexpected character after value');
793
+ }
794
+
795
+ parseWord(line, pos) {
796
+ const start = pos;
797
+ while (pos < line.length && isAsciiLetterCode(line.charCodeAt(pos)))
798
+ pos++;
799
+ const word = line.slice(start, pos);
800
+ switch (word) {
801
+ case 'true': return [true, this.checkValueEnd(line, pos)];
802
+ case 'false': return [false, this.checkValueEnd(line, pos)];
803
+ case 'inf': return [Infinity, this.checkValueEnd(line, pos)];
804
+ case 'nan': return [NaN, this.checkValueEnd(line, pos)];
805
+ case 'null':
806
+ if (this.mode === 'toml')
807
+ this.err(start, 'null is a JOSL extension',
808
+ 'TOML has no null; omit the key instead');
809
+ return [null, this.checkValueEnd(line, pos)];
810
+ default:
811
+ this.err(start, `invalid value '${word}'`,
812
+ "strings must be quoted, e.g. key = \"value\"");
813
+ }
814
+ }
815
+
816
+ parseSignedWord(line, pos) {
817
+ const neg = line.charCodeAt(pos) === CC_MINUS;
818
+ let p = pos + 1;
819
+ const start = p;
820
+ while (p < line.length && isAsciiLetterCode(line.charCodeAt(p)))
821
+ p++;
822
+ const word = line.slice(start, p);
823
+ if (word === 'inf')
824
+ return [neg ? -Infinity : Infinity, this.checkValueEnd(line, p)];
825
+ if (word === 'nan')
826
+ return [NaN, this.checkValueEnd(line, p)];
827
+ this.err(pos, `invalid value '${line.slice(pos, p)}'`);
828
+ }
829
+
830
+ //#endregion
831
+
832
+ //#region strings
833
+
834
+ decodeEscape(line, pos) {
835
+ // pos sits on the backslash; returns [decoded, nextPos]
836
+ if (pos + 1 >= line.length)
837
+ this.err(pos, 'unterminated escape sequence');
838
+ const c = line.charCodeAt(pos + 1);
839
+ switch (c) {
840
+ case 0x62: return ['\b', pos + 2];
841
+ case 0x74: return ['\t', pos + 2];
842
+ case 0x6E: return ['\n', pos + 2];
843
+ case 0x66: return ['\f', pos + 2];
844
+ case 0x72: return ['\r', pos + 2];
845
+ case CC_DQUOTE: return ['"', pos + 2];
846
+ case CC_BACKSLASH: return ['\\', pos + 2];
847
+ case 0x75: return this.decodeUnicodeEscape(line, pos, 4);
848
+ case 0x55: return this.decodeUnicodeEscape(line, pos, 8);
849
+ default:
850
+ this.err(pos, `invalid escape '\\${line[pos + 1]}'`,
851
+ 'valid escapes are \\b \\t \\n \\f \\r \\" \\\\ \\uXXXX \\UXXXXXXXX');
852
+ }
853
+ }
854
+
855
+ decodeUnicodeEscape(line, pos, width) {
856
+ const hex = line.slice(pos + 2, pos + 2 + width);
857
+ if (hex.length !== width || !/^[0-9a-fA-F]+$/.test(hex))
858
+ this.err(pos, `expected ${width} hex digits after '\\${line[pos + 1]}'`);
859
+ const cp = parseInt(hex, 16);
860
+ if (cp > 0x10FFFF || (cp >= 0xD800 && cp <= 0xDFFF))
861
+ this.err(pos, `invalid unicode code point '\\${line[pos + 1]}${hex}'`);
862
+ return [String.fromCodePoint(cp), pos + 2 + width];
863
+ }
864
+
865
+ checkStringChar(line, pos, multiline) {
866
+ const c = line.charCodeAt(pos);
867
+ if (c === CC_TAB)
868
+ return;
869
+ if (multiline && c === CC_LF)
870
+ return;
871
+ if (multiline && c === CC_CR && line.charCodeAt(pos + 1) === CC_LF)
872
+ return;
873
+ if (c < 0x20 || c === CC_DEL)
874
+ this.err(pos, 'control characters must be escaped in strings');
875
+ }
876
+
877
+ parseBasicString(line, pos) {
878
+ pos++; // consume '"'
879
+ let out = '';
880
+ let chunk = pos;
881
+ while (pos < line.length) {
882
+ const c = line.charCodeAt(pos);
883
+ if (c === CC_DQUOTE)
884
+ return [out + line.slice(chunk, pos), pos + 1];
885
+ if (c === CC_BACKSLASH) {
886
+ out += line.slice(chunk, pos);
887
+ const [dec, p] = this.decodeEscape(line, pos);
888
+ out += dec;
889
+ pos = p;
890
+ chunk = pos;
891
+ continue;
892
+ }
893
+ if (c === CC_LF)
894
+ break;
895
+ this.checkStringChar(line, pos, false);
896
+ pos++;
897
+ }
898
+ this.err(pos, 'unterminated string', "close the string with '\"'");
899
+ }
900
+
901
+ parseLiteralString(line, pos) {
902
+ pos++; // consume "'"
903
+ const start = pos;
904
+ while (pos < line.length) {
905
+ const c = line.charCodeAt(pos);
906
+ if (c === CC_SQUOTE)
907
+ return [line.slice(start, pos), pos + 1];
908
+ if (c === CC_LF)
909
+ break;
910
+ this.checkStringChar(line, pos, false);
911
+ pos++;
912
+ }
913
+ this.err(pos, 'unterminated string', "close the string with \"'\"");
914
+ }
915
+
916
+ parseMlBasicString(line, pos) {
917
+ pos += 3; // consume '"""'
918
+ if (line.charCodeAt(pos) === CC_CR && line.charCodeAt(pos + 1) === CC_LF)
919
+ pos += 2;
920
+ else if (line.charCodeAt(pos) === CC_LF)
921
+ pos++;
922
+ let out = '';
923
+ let chunk = pos;
924
+ while (pos < line.length) {
925
+ const c = line.charCodeAt(pos);
926
+ if (c === CC_DQUOTE) {
927
+ let run = pos;
928
+ while (run < line.length && line.charCodeAt(run) === CC_DQUOTE)
929
+ run++;
930
+ const n = run - pos;
931
+ if (n >= 3) {
932
+ if (n > 5)
933
+ this.err(pos, 'too many quotes closing a multi-line string');
934
+ return [out + line.slice(chunk, pos) + '"'.repeat(n - 3), run];
935
+ }
936
+ pos = run;
937
+ continue;
938
+ }
939
+ if (c === CC_BACKSLASH) {
940
+ // line-ending backslash: trim whitespace up to and beyond the newline
941
+ let p = pos + 1;
942
+ while (p < line.length) {
943
+ const w = line.charCodeAt(p);
944
+ if (w === CC_SPACE || w === CC_TAB || w === CC_CR) {
945
+ p++;
946
+ continue;
947
+ }
948
+ break;
949
+ }
950
+ if (p < line.length && line.charCodeAt(p) === CC_LF) {
951
+ out += line.slice(chunk, pos);
952
+ while (p < line.length) {
953
+ const w = line.charCodeAt(p);
954
+ if (w === CC_SPACE || w === CC_TAB || w === CC_CR || w === CC_LF) {
955
+ p++;
956
+ continue;
957
+ }
958
+ break;
959
+ }
960
+ pos = p;
961
+ chunk = pos;
962
+ continue;
963
+ }
964
+ out += line.slice(chunk, pos);
965
+ const [dec, np] = this.decodeEscape(line, pos);
966
+ out += dec;
967
+ pos = np;
968
+ chunk = pos;
969
+ continue;
970
+ }
971
+ this.checkStringChar(line, pos, true);
972
+ pos++;
973
+ }
974
+ this.err(pos, 'unterminated multi-line string', "close the string with '\"\"\"'");
975
+ }
976
+
977
+ parseMlLiteralString(line, pos) {
978
+ pos += 3; // consume "'''"
979
+ if (line.charCodeAt(pos) === CC_CR && line.charCodeAt(pos + 1) === CC_LF)
980
+ pos += 2;
981
+ else if (line.charCodeAt(pos) === CC_LF)
982
+ pos++;
983
+ const start = pos;
984
+ while (pos < line.length) {
985
+ const c = line.charCodeAt(pos);
986
+ if (c === CC_SQUOTE) {
987
+ let run = pos;
988
+ while (run < line.length && line.charCodeAt(run) === CC_SQUOTE)
989
+ run++;
990
+ const n = run - pos;
991
+ if (n >= 3) {
992
+ if (n > 5)
993
+ this.err(pos, 'too many quotes closing a multi-line string');
994
+ return [line.slice(start, pos) + "'".repeat(n - 3), run];
995
+ }
996
+ pos = run;
997
+ continue;
998
+ }
999
+ this.checkStringChar(line, pos, true);
1000
+ pos++;
1001
+ }
1002
+ this.err(pos, 'unterminated multi-line string', "close the string with \"'''\"");
1003
+ }
1004
+
1005
+ //#endregion
1006
+
1007
+ //#region containers
1008
+
1009
+ parseArray(line, pos) {
1010
+ pos++; // consume '['
1011
+ const arr = [];
1012
+ this.meta.set(arr, { aot: false });
1013
+ for (;;) {
1014
+ pos = this.skipWsNlComment(line, pos);
1015
+ if (pos >= line.length)
1016
+ this.err(pos, 'unterminated array', "close the array with ']'");
1017
+ if (line.charCodeAt(pos) === CC_RBRACKET)
1018
+ return [arr, pos + 1];
1019
+ const [v, p] = this.parseValue(line, pos);
1020
+ arr.push(v);
1021
+ pos = this.skipWsNlComment(line, p);
1022
+ if (pos >= line.length)
1023
+ this.err(pos, 'unterminated array', "close the array with ']'");
1024
+ const c = line.charCodeAt(pos);
1025
+ if (c === CC_COMMA) {
1026
+ pos++;
1027
+ continue;
1028
+ }
1029
+ if (c === CC_RBRACKET)
1030
+ return [arr, pos + 1];
1031
+ this.err(pos, "expected ',' or ']' in array");
1032
+ }
1033
+ }
1034
+
1035
+ parseInlineTable(line, pos) {
1036
+ pos++; // consume '{'
1037
+ const obj = {};
1038
+ this.meta.set(obj, { inline: true });
1039
+ pos = this.skipWs(line, pos);
1040
+ if (pos < line.length && line.charCodeAt(pos) === CC_RBRACE)
1041
+ return [obj, pos + 1];
1042
+ for (;;) {
1043
+ if (pos < line.length && line.charCodeAt(pos) === CC_LF)
1044
+ this.err(pos, 'newlines are not allowed inside inline tables',
1045
+ 'use a [table] section for multi-line tables');
1046
+ const [keys, afterKeys] = this.parseKeys(line, pos);
1047
+ let p = afterKeys;
1048
+ if (p >= line.length || line.charCodeAt(p) !== CC_EQ)
1049
+ this.err(p, "expected '=' after key in inline table");
1050
+ p = this.skipWs(line, p + 1);
1051
+ const [v, afterValue] = this.parseValue(line, p);
1052
+ this.assignInline(obj, keys, v, pos);
1053
+ pos = this.skipWs(line, afterValue);
1054
+ if (pos >= line.length)
1055
+ this.err(pos, 'unterminated inline table', "close the table with '}'");
1056
+ const c = line.charCodeAt(pos);
1057
+ if (c === CC_RBRACE)
1058
+ return [obj, pos + 1];
1059
+ if (c === CC_COMMA) {
1060
+ pos = this.skipWs(line, pos + 1);
1061
+ continue;
1062
+ }
1063
+ this.err(pos, "expected ',' or '}' in inline table");
1064
+ }
1065
+ }
1066
+
1067
+ assignInline(obj, keys, value, pos) {
1068
+ let t = obj;
1069
+ for (let i = 0; i < keys.length - 1; ++i) {
1070
+ const k = keys[i];
1071
+ const ex = getOwn(t, k);
1072
+ if (ex === undefined) {
1073
+ const nt = {};
1074
+ this.meta.set(nt, { inline: true, dotted: true });
1075
+ setObjectMember(t, k, nt);
1076
+ t = nt;
1077
+ continue;
1078
+ }
1079
+ if (ex !== null && typeof ex === 'object' && !Array.isArray(ex)
1080
+ && this.meta.get(ex)?.dotted === true) {
1081
+ t = ex;
1082
+ continue;
1083
+ }
1084
+ this.err(pos, `dotted key '${k}' conflicts with an existing value`);
1085
+ }
1086
+ const k = keys[keys.length - 1];
1087
+ if (Object.hasOwn(t, k))
1088
+ this.err(pos, `duplicate key '${k}'`);
1089
+ setObjectMember(t, k, value);
1090
+ }
1091
+
1092
+ //#endregion
1093
+
1094
+ //#region regexp, datetimes & numbers
1095
+
1096
+ parseRegExp(line, pos) {
1097
+ if (this.mode === 'toml')
1098
+ this.err(pos, 'regexp literals are a JOSL extension',
1099
+ 'quote the pattern as a string for TOML compatibility');
1100
+ const start = pos;
1101
+ pos++; // consume '/'
1102
+ let inClass = false;
1103
+ for (;;) {
1104
+ if (pos >= line.length || line.charCodeAt(pos) === CC_LF)
1105
+ this.err(start, 'unterminated regexp literal', "close the regexp with '/'");
1106
+ const c = line.charCodeAt(pos);
1107
+ if (c === CC_BACKSLASH) {
1108
+ pos += 2;
1109
+ continue;
1110
+ }
1111
+ if (c === CC_LBRACKET)
1112
+ inClass = true;
1113
+ else if (c === CC_RBRACKET)
1114
+ inClass = false;
1115
+ else if (c === CC_SLASH && !inClass)
1116
+ break;
1117
+ pos++;
1118
+ }
1119
+ const body = line.slice(start + 1, pos);
1120
+ pos++; // consume '/'
1121
+ const flagStart = pos;
1122
+ while (pos < line.length && isAsciiLetterCode(line.charCodeAt(pos)))
1123
+ pos++;
1124
+ const flags = line.slice(flagStart, pos);
1125
+ try {
1126
+ return [new RegExp(body, flags), this.checkValueEnd(line, pos)];
1127
+ }
1128
+ catch (e) {
1129
+ this.err(start, `invalid regexp literal: ${e.message}`);
1130
+ }
1131
+ }
1132
+
1133
+ parseDateTimeOrNumber(line, pos) {
1134
+ // a datetime needs ':' at pos+2 (time) or '-' at pos+4 (date);
1135
+ // everything else goes straight to the number path
1136
+ if (line.charCodeAt(pos + 2) !== CC_COLON && line.charCodeAt(pos + 4) !== CC_MINUS)
1137
+ return this.parseNumber(line, pos);
1138
+ let m = stickyExec(RE_DATETIME, line, pos);
1139
+ if (m !== null) {
1140
+ const year = Number(m[1]);
1141
+ const month = Number(m[2]);
1142
+ const day = Number(m[3]);
1143
+ if (!isValidDateParts(year, month, day))
1144
+ this.err(pos, `invalid date '${m[0]}'`);
1145
+ const date = new LocalDate(year, month, day);
1146
+ if (m[4] === undefined)
1147
+ return [date, this.checkValueEnd(line, pos + m[0].length)];
1148
+ const hour = Number(m[4]);
1149
+ const minute = Number(m[5]);
1150
+ const second = Number(m[6]);
1151
+ if (!isValidTimeParts(hour, minute, second))
1152
+ this.err(pos, `invalid time '${m[0]}'`);
1153
+ const time = new LocalTime(hour, minute, second, m[7] ?? '');
1154
+ const end = this.checkValueEnd(line, pos + m[0].length);
1155
+ if (m[8] === undefined)
1156
+ return [new LocalDateTime(date, time), end];
1157
+ const offset = m[8] === 'z' || m[8] === 'Z' ? 'Z' : m[8];
1158
+ const instant = new Date(`${date.toString()}T${time.toString()}${offset}`);
1159
+ if (Number.isNaN(instant.getTime()))
1160
+ this.err(pos, `invalid date-time '${m[0]}'`);
1161
+ return [instant, end];
1162
+ }
1163
+ m = stickyExec(RE_TIMEONLY, line, pos);
1164
+ if (m !== null) {
1165
+ const hour = Number(m[1]);
1166
+ const minute = Number(m[2]);
1167
+ const second = Number(m[3]);
1168
+ if (!isValidTimeParts(hour, minute, second))
1169
+ this.err(pos, `invalid time '${m[0]}'`);
1170
+ return [
1171
+ new LocalTime(hour, minute, second, m[4] ?? ''),
1172
+ this.checkValueEnd(line, pos + m[0].length),
1173
+ ];
1174
+ }
1175
+ return this.parseNumber(line, pos);
1176
+ }
1177
+
1178
+ bigIntCheck(pos, suffix) {
1179
+ if (suffix === 'n' && this.mode === 'toml')
1180
+ this.err(pos, 'bigint literals are a JOSL extension',
1181
+ 'drop the n suffix for TOML compatibility');
1182
+ return suffix === 'n';
1183
+ }
1184
+
1185
+ // Integers parse exactly via BigInt, then downgrade to Number when safe.
1186
+ // Strict TOML mode enforces the spec's signed 64-bit range ("should be
1187
+ // accepted and handled losslessly"); JOSL mode has no range limit.
1188
+ // Tokens of 15 digits or fewer are always safe, so the common case
1189
+ // never touches BigInt.
1190
+ intValue(pos, source, big, m0) {
1191
+ if (!big && source.length <= (source.charCodeAt(0) === CC_MINUS ? 16 : 15))
1192
+ return Number(source);
1193
+ const value = BigInt(source);
1194
+ if (this.mode === 'toml' && (value < INT64_MIN || value > INT64_MAX))
1195
+ this.err(pos, `integer '${m0}' exceeds the TOML 64-bit integer range`);
1196
+ if (big)
1197
+ return value;
1198
+ return value >= SAFE_MIN && value <= SAFE_MAX ? Number(value) : value;
1199
+ }
1200
+
1201
+ parseNumber(line, pos) {
1202
+ const c0 = line.charCodeAt(pos);
1203
+ // Plain decimal integers dominate real documents. A digit run that ends
1204
+ // the value cannot hold a radix prefix, an underscore, a fraction or the
1205
+ // bigint suffix, so it needs none of the token regexes below. Anything
1206
+ // else — including a leading zero, which TOML forbids — falls through so
1207
+ // the regexes keep producing the established value and error positions.
1208
+ if (isDigitCode(c0)) {
1209
+ let p = pos + 1;
1210
+ while (p < line.length && isDigitCode(line.charCodeAt(p)))
1211
+ p++;
1212
+ if ((p - pos === 1 || c0 !== CC_0) && atValueEnd(line, p)) {
1213
+ const source = line.slice(pos, p);
1214
+ return [this.intValue(pos, source, false, source), p];
1215
+ }
1216
+ }
1217
+ // Radix prefixes are the only tokens those three patterns can match.
1218
+ let m = null;
1219
+ if (c0 === CC_0) {
1220
+ const c1 = line.charCodeAt(pos + 1);
1221
+ if (c1 === CC_LOWER_X)
1222
+ m = stickyExec(RE_HEX, line, pos);
1223
+ else if (c1 === CC_LOWER_O)
1224
+ m = stickyExec(RE_OCT, line, pos);
1225
+ else if (c1 === CC_LOWER_B)
1226
+ m = stickyExec(RE_BIN, line, pos);
1227
+ }
1228
+ if (m !== null) {
1229
+ const big = this.bigIntCheck(pos, m[1]);
1230
+ const stripped = (big ? m[0].slice(0, -1) : m[0]).replace(/_/g, '');
1231
+ const end = this.checkValueEnd(line, pos + m[0].length);
1232
+ return [this.intValue(pos, stripped, big, m[0]), end];
1233
+ }
1234
+ m = stickyExec(RE_NUM, line, pos);
1235
+ if (m === null)
1236
+ this.err(pos, 'invalid number');
1237
+ const big = this.bigIntCheck(pos, m[1]);
1238
+ const token = big ? m[0].slice(0, -1) : m[0];
1239
+ const isFloat = /[.eE]/.test(token);
1240
+ const end = this.checkValueEnd(line, pos + m[0].length);
1241
+ if (isFloat) {
1242
+ if (big)
1243
+ this.err(pos, 'bigint literals cannot have a fraction or exponent');
1244
+ return [Number(token.replace(/_/g, '')), end];
1245
+ }
1246
+ return [this.intValue(pos, token.replace(/[_+]/g, ''), big, m[0]), end];
1247
+ }
1248
+
1249
+ //#endregion
1250
+ }
1251
+
1252
+ //#endregion