@decimalturn/toml-patch 0.5.2 → 0.7.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/README.md +6 -12
- package/dist/toml-patch.cjs.min.js +2 -2
- package/dist/toml-patch.cjs.min.js.map +1 -1
- package/dist/toml-patch.d.ts +12 -2
- package/dist/toml-patch.es.js +1355 -276
- package/dist/toml-patch.umd.min.js +2 -2
- package/dist/toml-patch.umd.min.js.map +1 -1
- package/package.json +3 -3
package/dist/toml-patch.es.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//! @decimalturn/toml-patch v0.
|
|
1
|
+
//! @decimalturn/toml-patch v0.7.0 - https://github.com/DecimalTurn/toml-patch - @license: MIT
|
|
2
2
|
var NodeType;
|
|
3
3
|
(function (NodeType) {
|
|
4
4
|
NodeType["Document"] = "Document";
|
|
@@ -82,6 +82,9 @@ function isBlock(node) {
|
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
function iterator(value) {
|
|
85
|
+
if (typeof value === 'string') {
|
|
86
|
+
return utf16Iterator(value);
|
|
87
|
+
}
|
|
85
88
|
return value[Symbol.iterator]();
|
|
86
89
|
}
|
|
87
90
|
/**
|
|
@@ -139,6 +142,18 @@ class Cursor {
|
|
|
139
142
|
function done() {
|
|
140
143
|
return { value: undefined, done: true };
|
|
141
144
|
}
|
|
145
|
+
/**
|
|
146
|
+
* Creates a UTF-16 code unit iterator for a string.
|
|
147
|
+
* This is necessary because cursor.index is used to access string positions
|
|
148
|
+
* via input[cursor.index] and input.slice() throughout the tokenizer.
|
|
149
|
+
* While Symbol.iterator yields code points (better for humans), we need
|
|
150
|
+
* UTF-16 indices for correct string access in JavaScript.
|
|
151
|
+
*/
|
|
152
|
+
function* utf16Iterator(str) {
|
|
153
|
+
for (let i = 0; i < str.length; i++) {
|
|
154
|
+
yield str[i];
|
|
155
|
+
}
|
|
156
|
+
}
|
|
142
157
|
|
|
143
158
|
function getSpan(location) {
|
|
144
159
|
return {
|
|
@@ -174,19 +189,29 @@ function findPosition(input, index) {
|
|
|
174
189
|
}
|
|
175
190
|
function getLine$1(input, position) {
|
|
176
191
|
const lines = findLines(input);
|
|
177
|
-
const start = lines[position.line - 2]
|
|
192
|
+
const start = lines[position.line - 2] !== undefined ? lines[position.line - 2] + 1 : 0;
|
|
178
193
|
const end = lines[position.line - 1] || input.length;
|
|
179
|
-
return input.
|
|
194
|
+
return input.substring(start, end);
|
|
180
195
|
}
|
|
181
196
|
function findLines(input) {
|
|
182
|
-
// exec is stateful, so create new regexp each time
|
|
183
|
-
const BY_NEW_LINE = /\r\n|\n/g;
|
|
184
197
|
const indexes = [];
|
|
185
|
-
let
|
|
186
|
-
|
|
187
|
-
|
|
198
|
+
for (let i = 0; i < input.length; i++) {
|
|
199
|
+
const char = input[i];
|
|
200
|
+
if (char === '\n') {
|
|
201
|
+
indexes.push(i);
|
|
202
|
+
}
|
|
203
|
+
else if (char === '\r') {
|
|
204
|
+
// Handle \r\n as a single line break
|
|
205
|
+
if (input[i + 1] === '\n') {
|
|
206
|
+
indexes.push(i + 1); // Position after \r\n
|
|
207
|
+
i++; // Skip the \n
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
indexes.push(i);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
188
213
|
}
|
|
189
|
-
indexes.push(input.length
|
|
214
|
+
indexes.push(input.length);
|
|
190
215
|
return indexes;
|
|
191
216
|
}
|
|
192
217
|
function clonePosition(position) {
|
|
@@ -235,7 +260,6 @@ var TokenType;
|
|
|
235
260
|
TokenType["Literal"] = "Literal";
|
|
236
261
|
})(TokenType || (TokenType = {}));
|
|
237
262
|
const IS_WHITESPACE = /\s/;
|
|
238
|
-
const IS_NEW_LINE = /(\r\n|\n)/;
|
|
239
263
|
const DOUBLE_QUOTE = `"`;
|
|
240
264
|
const SINGLE_QUOTE = `'`;
|
|
241
265
|
const SPACE = ' ';
|
|
@@ -246,6 +270,19 @@ function* tokenize(input) {
|
|
|
246
270
|
cursor.next();
|
|
247
271
|
const locate = createLocate(input);
|
|
248
272
|
while (!cursor.done) {
|
|
273
|
+
const code = cursor.value.charCodeAt(0);
|
|
274
|
+
// TOML does not allow ASCII control characters other than HT (TAB), LF, and CR.
|
|
275
|
+
// CR is only allowed as part of CRLF and is validated below.
|
|
276
|
+
if ((code <= 0x1f || code === 0x7f) && code !== 0x09 && code !== 0x0d && code !== 0x0a) {
|
|
277
|
+
throw new ParseError(input, findPosition(input, cursor.index), `Control character 0x${code.toString(16).toUpperCase().padStart(2, '0')} is not allowed in TOML`);
|
|
278
|
+
}
|
|
279
|
+
// CR (0x0D) is only allowed as part of CRLF
|
|
280
|
+
if (cursor.value === '\r') {
|
|
281
|
+
const next = cursor.peek();
|
|
282
|
+
if (next.done || next.value !== '\n') {
|
|
283
|
+
throw new ParseError(input, findPosition(input, cursor.index), 'Invalid standalone CR (\\r); CR must be part of a CRLF sequence');
|
|
284
|
+
}
|
|
285
|
+
}
|
|
249
286
|
if (IS_WHITESPACE.test(cursor.value)) ;
|
|
250
287
|
else if (cursor.value === '[' || cursor.value === ']') {
|
|
251
288
|
// Handle special characters: [, ], {, }, =, comma
|
|
@@ -265,7 +302,7 @@ function* tokenize(input) {
|
|
|
265
302
|
}
|
|
266
303
|
else if (cursor.value === '#') {
|
|
267
304
|
// Handle comments = # -> EOL
|
|
268
|
-
yield comment$1(cursor, locate);
|
|
305
|
+
yield comment$1(cursor, locate, input);
|
|
269
306
|
}
|
|
270
307
|
else {
|
|
271
308
|
const multiline_char = checkThree(input, cursor.index, SINGLE_QUOTE) ||
|
|
@@ -284,11 +321,19 @@ function* tokenize(input) {
|
|
|
284
321
|
function specialCharacter(cursor, locate, type) {
|
|
285
322
|
return { type, raw: cursor.value, loc: locate(cursor.index, cursor.index + 1) };
|
|
286
323
|
}
|
|
287
|
-
function comment$1(cursor, locate) {
|
|
324
|
+
function comment$1(cursor, locate, input) {
|
|
288
325
|
const start = cursor.index;
|
|
289
326
|
let raw = cursor.value;
|
|
290
|
-
|
|
327
|
+
// TOML comment ends at CR or LF.
|
|
328
|
+
while (!cursor.peek().done &&
|
|
329
|
+
cursor.peek().value !== '\n' &&
|
|
330
|
+
cursor.peek().value !== '\r') {
|
|
291
331
|
cursor.next();
|
|
332
|
+
const code = cursor.value.charCodeAt(0);
|
|
333
|
+
// Disallow ASCII control characters in comments (except HT / TAB).
|
|
334
|
+
if ((code <= 0x1f || code === 0x7f) && code !== 0x09) {
|
|
335
|
+
throw new ParseError(input, findPosition(input, cursor.index), `Control character 0x${code.toString(16).toUpperCase().padStart(2, '0')} is not allowed in TOML`);
|
|
336
|
+
}
|
|
292
337
|
raw += cursor.value;
|
|
293
338
|
}
|
|
294
339
|
// Early exit is ok for comment, no closing conditions
|
|
@@ -300,19 +345,76 @@ function comment$1(cursor, locate) {
|
|
|
300
345
|
}
|
|
301
346
|
function multiline(cursor, locate, multiline_char, input) {
|
|
302
347
|
const start = cursor.index;
|
|
303
|
-
|
|
348
|
+
const quotes = multiline_char + multiline_char + multiline_char;
|
|
304
349
|
let raw = quotes;
|
|
305
|
-
// Skip over quotes
|
|
350
|
+
// Skip over opening quotes
|
|
306
351
|
cursor.next();
|
|
307
352
|
cursor.next();
|
|
308
353
|
cursor.next();
|
|
309
|
-
//
|
|
310
|
-
//
|
|
311
|
-
|
|
354
|
+
// Multiline strings close on the first unescaped """ / '''.
|
|
355
|
+
// A run of 4 or 5 quote characters at the end is allowed to include 1 or 2 quotes
|
|
356
|
+
// immediately before the closing delimiter, but 6+ consecutive quotes is invalid.
|
|
357
|
+
while (!cursor.done) {
|
|
358
|
+
const found = checkThree(input, cursor.index, multiline_char);
|
|
359
|
+
if (found) {
|
|
360
|
+
let runLength = 3;
|
|
361
|
+
while (input[cursor.index + runLength] === multiline_char) {
|
|
362
|
+
runLength++;
|
|
363
|
+
}
|
|
364
|
+
if (runLength >= 6) {
|
|
365
|
+
throw new ParseError(input, findPosition(input, cursor.index), `Invalid multiline string: ${runLength} consecutive ${multiline_char} characters`);
|
|
366
|
+
}
|
|
367
|
+
if (runLength === 3) {
|
|
368
|
+
break;
|
|
369
|
+
}
|
|
370
|
+
// runLength is 4 or 5: keep the leading 1 or 2 quote chars as content,
|
|
371
|
+
// and close on the last 3.
|
|
372
|
+
raw += multiline_char.repeat(runLength - 3);
|
|
373
|
+
for (let i = 0; i < runLength - 3; i++) {
|
|
374
|
+
cursor.next();
|
|
375
|
+
}
|
|
376
|
+
break;
|
|
377
|
+
}
|
|
378
|
+
if (cursor.value === '\r') {
|
|
379
|
+
const next = cursor.peek();
|
|
380
|
+
if (next.done || next.value !== '\n') {
|
|
381
|
+
throw new ParseError(input, findPosition(input, cursor.index), 'Invalid standalone CR (\\r) in multiline string (must be part of CRLF sequence)');
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
// Validate control characters in multiline strings
|
|
385
|
+
const code = cursor.value.charCodeAt(0);
|
|
386
|
+
// In multiline strings, control characters are not allowed except tab (0x09), LF (0x0A), and CR (0x0D as part of CRLF)
|
|
387
|
+
// DEL (0x7F) is also not allowed
|
|
388
|
+
if ((code <= 0x1f || code === 0x7f) && code !== 0x09 && code !== 0x0a && code !== 0x0d) {
|
|
389
|
+
const stringType = multiline_char === DOUBLE_QUOTE ? 'multiline basic strings' : 'multiline literal strings';
|
|
390
|
+
const hexCode = `0x${code.toString(16).toUpperCase().padStart(2, '0')}`;
|
|
391
|
+
// Provide friendly names for common control characters
|
|
392
|
+
let charName = '';
|
|
393
|
+
if (code === 0x00) {
|
|
394
|
+
charName = 'Null';
|
|
395
|
+
}
|
|
396
|
+
else if (code === 0x7f) {
|
|
397
|
+
charName = 'DEL';
|
|
398
|
+
}
|
|
399
|
+
const message = charName
|
|
400
|
+
? `${charName} (control character ${hexCode}) is not allowed in ${stringType}`
|
|
401
|
+
: `Control character ${hexCode} is not allowed in ${stringType}`;
|
|
402
|
+
throw new ParseError(input, findPosition(input, cursor.index), message);
|
|
403
|
+
}
|
|
312
404
|
raw += cursor.value;
|
|
313
405
|
cursor.next();
|
|
314
406
|
}
|
|
315
407
|
if (cursor.done) {
|
|
408
|
+
// Check if the issue might be caused by escape sequences preventing proper closure
|
|
409
|
+
// For multiline basic strings ("""), check if there are backslashes near the end that might be escaping quotes
|
|
410
|
+
if (multiline_char === DOUBLE_QUOTE) {
|
|
411
|
+
// Check the last few characters before EOF for patterns like \""" or \"
|
|
412
|
+
const precedingText = input.slice(0, cursor.index);
|
|
413
|
+
const hasEscapedQuotes = /\\"+$/.test(precedingText);
|
|
414
|
+
if (hasEscapedQuotes) {
|
|
415
|
+
throw new ParseError(input, findPosition(input, cursor.index), `Expected close of multiline string with ${quotes}, reached end of file. Check for escape sequences (\\) that may be preventing proper string closure`);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
316
418
|
throw new ParseError(input, findPosition(input, cursor.index), `Expected close of multiline string with ${quotes}, reached end of file`);
|
|
317
419
|
}
|
|
318
420
|
raw += quotes;
|
|
@@ -367,6 +469,35 @@ function string$1(cursor, locate, input) {
|
|
|
367
469
|
};
|
|
368
470
|
while (!cursor.done && !isFinished(cursor)) {
|
|
369
471
|
cursor.next();
|
|
472
|
+
// Validate control characters in quoted strings
|
|
473
|
+
if (double_quoted || single_quoted) {
|
|
474
|
+
const code = cursor.value.charCodeAt(0);
|
|
475
|
+
// In basic strings (double-quoted) and literal strings (single-quoted),
|
|
476
|
+
// control characters are not allowed except tab (0x09)
|
|
477
|
+
// DEL (0x7F) is also not allowed
|
|
478
|
+
if ((code <= 0x1f || code === 0x7f) && code !== 0x09) {
|
|
479
|
+
const stringType = double_quoted ? 'basic strings' : 'literal strings';
|
|
480
|
+
const hexCode = `0x${code.toString(16).toUpperCase().padStart(2, '0')}`;
|
|
481
|
+
// Provide friendly names for common control characters
|
|
482
|
+
let charName = '';
|
|
483
|
+
if (code === 0x0a) {
|
|
484
|
+
charName = 'Newline';
|
|
485
|
+
}
|
|
486
|
+
else if (code === 0x0d) {
|
|
487
|
+
charName = 'Carriage return';
|
|
488
|
+
}
|
|
489
|
+
else if (code === 0x00) {
|
|
490
|
+
charName = 'Null';
|
|
491
|
+
}
|
|
492
|
+
else if (code === 0x7f) {
|
|
493
|
+
charName = 'DEL';
|
|
494
|
+
}
|
|
495
|
+
const message = charName
|
|
496
|
+
? `${charName} (control character ${hexCode}) is not allowed in ${stringType}`
|
|
497
|
+
: `Control character ${hexCode} is not allowed in ${stringType}`;
|
|
498
|
+
throw new ParseError(input, findPosition(input, cursor.index), message);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
370
501
|
if (cursor.value === DOUBLE_QUOTE)
|
|
371
502
|
double_quoted = !double_quoted;
|
|
372
503
|
if (cursor.value === SINGLE_QUOTE && !double_quoted)
|
|
@@ -416,6 +547,11 @@ function checkThree(input, current, check) {
|
|
|
416
547
|
if (!has3) {
|
|
417
548
|
return false;
|
|
418
549
|
}
|
|
550
|
+
// Only check for escaping in basic strings (double quotes)
|
|
551
|
+
// Literal strings (single quotes) don't support escape sequences
|
|
552
|
+
if (check === SINGLE_QUOTE) {
|
|
553
|
+
return check; // No escaping in literal strings
|
|
554
|
+
}
|
|
419
555
|
// Check if the sequence is escaped
|
|
420
556
|
const precedingText = input.slice(0, current); // Get the text before the current position
|
|
421
557
|
const backslashes = precedingText.match(/\\+$/); // Match trailing backslashes
|
|
@@ -425,15 +561,6 @@ function checkThree(input, current, check) {
|
|
|
425
561
|
const isEscaped = backslashes[0].length % 2 !== 0; // Odd number of backslashes means escaped
|
|
426
562
|
return isEscaped ? false : check; // Return `check` if not escaped, otherwise `false`
|
|
427
563
|
}
|
|
428
|
-
function CheckMoreThanThree(input, current, check) {
|
|
429
|
-
if (!check) {
|
|
430
|
-
return false;
|
|
431
|
-
}
|
|
432
|
-
return (input[current] === check &&
|
|
433
|
-
input[current + 1] === check &&
|
|
434
|
-
input[current + 2] === check &&
|
|
435
|
-
input[current + 3] === check);
|
|
436
|
-
}
|
|
437
564
|
|
|
438
565
|
function last(values) {
|
|
439
566
|
return values[values.length - 1];
|
|
@@ -564,11 +691,75 @@ function escapeDoubleQuotes(value) {
|
|
|
564
691
|
}
|
|
565
692
|
return result;
|
|
566
693
|
}
|
|
694
|
+
function isBackslashEscaped(source, backslashOffset) {
|
|
695
|
+
let precedingBackslashes = 0;
|
|
696
|
+
for (let i = backslashOffset - 1; i >= 0 && source[i] === '\\'; i--) {
|
|
697
|
+
precedingBackslashes++;
|
|
698
|
+
}
|
|
699
|
+
return precedingBackslashes % 2 !== 0;
|
|
700
|
+
}
|
|
567
701
|
function unescapeLargeUnicode(escaped) {
|
|
702
|
+
// First, validate all escape sequences are valid TOML escapes
|
|
703
|
+
// Valid TOML escape sequences: \b \t \n \f \r \" \\ \uXXXX \UXXXXXXXX \xHH (1.1.0) \e (1.1.0)
|
|
704
|
+
const ESCAPE_VALIDATION = /\\(.)/g;
|
|
705
|
+
let match;
|
|
706
|
+
while ((match = ESCAPE_VALIDATION.exec(escaped)) !== null) {
|
|
707
|
+
const offset = match.index;
|
|
708
|
+
if (isBackslashEscaped(escaped, offset)) {
|
|
709
|
+
continue; // This backslash is itself escaped, so skip
|
|
710
|
+
}
|
|
711
|
+
const escapeChar = match[1];
|
|
712
|
+
// Valid single-char escapes: b, t, n, f, r, ", \, e
|
|
713
|
+
// Valid multi-char escapes: u (followed by 4 hex), U (followed by 8 hex), x (followed by 2 hex)
|
|
714
|
+
const validEscapes = ['b', 't', 'n', 'f', 'r', '"', '\\', 'e', 'u', 'U', 'x'];
|
|
715
|
+
if (!validEscapes.includes(escapeChar)) {
|
|
716
|
+
throw new Error(`Invalid escape sequence: \\${escapeChar}`);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
// Validate \uXXXX sequences don't use surrogate codepoints (0xD800-0xDFFF)
|
|
720
|
+
const SMALL_UNICODE = /\\u([a-fA-F0-9]{4})/g;
|
|
721
|
+
const smallUnicodeSource = escaped;
|
|
722
|
+
while ((match = SMALL_UNICODE.exec(smallUnicodeSource)) !== null) {
|
|
723
|
+
const offset = match.index;
|
|
724
|
+
if (isBackslashEscaped(smallUnicodeSource, offset)) {
|
|
725
|
+
continue;
|
|
726
|
+
}
|
|
727
|
+
const hex = match[1];
|
|
728
|
+
const codePoint = parseInt(hex, 16);
|
|
729
|
+
// Surrogate pair range: 0xD800-0xDFFF
|
|
730
|
+
// High surrogates: 0xD800-0xDBFF
|
|
731
|
+
// Low surrogates: 0xDC00-0xDFFF
|
|
732
|
+
if (codePoint >= 0xD800 && codePoint <= 0xDFFF) {
|
|
733
|
+
throw new Error(`Invalid Unicode escape: \\u${hex} (surrogate codepoints are not allowed)`);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
// TOML 1.1.0: Handle \xHH hex escapes (for codepoints < 255)
|
|
737
|
+
const HEX_ESCAPE = /\\x([a-fA-F0-9]{2})/g;
|
|
738
|
+
const hexEscapeSource = escaped;
|
|
739
|
+
let withHexEscapes = hexEscapeSource.replace(HEX_ESCAPE, (match, hex, offset) => {
|
|
740
|
+
if (isBackslashEscaped(hexEscapeSource, offset)) {
|
|
741
|
+
return match;
|
|
742
|
+
}
|
|
743
|
+
const codePoint = parseInt(hex, 16);
|
|
744
|
+
const asString = String.fromCharCode(codePoint);
|
|
745
|
+
// Escape for JSON if needed
|
|
746
|
+
if (codePoint < 0x20 || codePoint === 0x22 || codePoint === 0x5C) {
|
|
747
|
+
return trim(JSON.stringify(asString), 1);
|
|
748
|
+
}
|
|
749
|
+
return asString;
|
|
750
|
+
});
|
|
751
|
+
// TOML 1.1.0: Handle \e escape character (ESC = 0x1B)
|
|
752
|
+
const eEscapeSource = withHexEscapes;
|
|
753
|
+
withHexEscapes = eEscapeSource.replace(/\\e/g, (match, offset) => {
|
|
754
|
+
if (isBackslashEscaped(eEscapeSource, offset)) {
|
|
755
|
+
return match;
|
|
756
|
+
}
|
|
757
|
+
return '\\u001b';
|
|
758
|
+
});
|
|
568
759
|
// JSON.parse handles everything except \UXXXXXXXX
|
|
569
760
|
// replace those instances with code point, escape that, and then parse
|
|
570
761
|
const LARGE_UNICODE = /\\U[a-fA-F0-9]{8}/g;
|
|
571
|
-
const json_escaped =
|
|
762
|
+
const json_escaped = withHexEscapes.replace(LARGE_UNICODE, value => {
|
|
572
763
|
const code_point = parseInt(value.replace('\\U', ''), 16);
|
|
573
764
|
const as_string = String.fromCodePoint(code_point);
|
|
574
765
|
return trim(JSON.stringify(as_string), 1);
|
|
@@ -789,14 +980,15 @@ class DateFormatHelper {
|
|
|
789
980
|
}
|
|
790
981
|
// Patterns for different date/time formats
|
|
791
982
|
DateFormatHelper.IS_DATE_ONLY = /^\d{4}-\d{2}-\d{2}$/;
|
|
792
|
-
DateFormatHelper.IS_TIME_ONLY = /^\d{2}:\d{2}
|
|
793
|
-
DateFormatHelper.IS_LOCAL_DATETIME_T = /^\d{4}-\d{2}-\d{2}
|
|
794
|
-
DateFormatHelper.IS_LOCAL_DATETIME_SPACE = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}
|
|
795
|
-
DateFormatHelper.IS_OFFSET_DATETIME_T = /^\d{4}-\d{2}-\d{2}
|
|
796
|
-
DateFormatHelper.IS_OFFSET_DATETIME_SPACE = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}
|
|
983
|
+
DateFormatHelper.IS_TIME_ONLY = /^\d{2}:\d{2}(?::\d{2})?(?:\.\d+)?$/;
|
|
984
|
+
DateFormatHelper.IS_LOCAL_DATETIME_T = /^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}(?::\d{2})?(?:\.\d+)?$/;
|
|
985
|
+
DateFormatHelper.IS_LOCAL_DATETIME_SPACE = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}(?::\d{2})?(?:\.\d+)?$/;
|
|
986
|
+
DateFormatHelper.IS_OFFSET_DATETIME_T = /^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}(?::\d{2})?(?:\.\d+)?(?:[Zz]|[+-]\d{2}:\d{2})$/;
|
|
987
|
+
DateFormatHelper.IS_OFFSET_DATETIME_SPACE = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}(?::\d{2})?(?:\.\d+)?(?:[Zz]|[+-]\d{2}:\d{2})$/;
|
|
797
988
|
// Legacy patterns from parse-toml.ts (for compatibility)
|
|
798
|
-
|
|
799
|
-
DateFormatHelper.
|
|
989
|
+
// Made more permissive to catch malformed dates (e.g., 1987-7-05) for validation
|
|
990
|
+
DateFormatHelper.IS_FULL_DATE = /(\d{4})-(\d+)-(\d+)/;
|
|
991
|
+
DateFormatHelper.IS_FULL_TIME = /(\d+):(\d+)(?::(\d+))?/;
|
|
800
992
|
/**
|
|
801
993
|
* Custom Date class for local dates (date-only).
|
|
802
994
|
* Format: 1979-05-27
|
|
@@ -818,9 +1010,15 @@ class LocalDate extends Date {
|
|
|
818
1010
|
*/
|
|
819
1011
|
class LocalTime extends Date {
|
|
820
1012
|
constructor(value, originalFormat) {
|
|
821
|
-
//
|
|
1013
|
+
// Normalize time to include seconds if missing (TOML 1.1.0 allows optional seconds)
|
|
1014
|
+
let normalizedValue = value;
|
|
1015
|
+
if (!/:\d{2}:\d{2}/.test(value)) {
|
|
1016
|
+
// No seconds present, add :00
|
|
1017
|
+
normalizedValue = value + ':00';
|
|
1018
|
+
}
|
|
1019
|
+
// For local time, use year 0000 as the base (TOML spec compliance)
|
|
822
1020
|
// Add 'Z' to ensure it's parsed as UTC regardless of system timezone
|
|
823
|
-
super(`
|
|
1021
|
+
super(`0000-01-01T${normalizedValue}Z`);
|
|
824
1022
|
this.originalFormat = originalFormat;
|
|
825
1023
|
}
|
|
826
1024
|
toISOString() {
|
|
@@ -852,8 +1050,13 @@ class LocalTime extends Date {
|
|
|
852
1050
|
*/
|
|
853
1051
|
class LocalDateTime extends Date {
|
|
854
1052
|
constructor(value, useSpaceSeparator = false, originalFormat) {
|
|
1053
|
+
// Normalize time part to include seconds if missing (TOML 1.1.0 allows optional seconds)
|
|
1054
|
+
let normalizedValue = value;
|
|
1055
|
+
if (!/\d{2}:\d{2}:\d{2}/.test(value)) {
|
|
1056
|
+
normalizedValue = value.replace(/(\d{2}:\d{2})([\s\-+TZ]|$)/, '$1:00$2');
|
|
1057
|
+
}
|
|
855
1058
|
// Convert space to T for Date parsing, but remember the original format
|
|
856
|
-
super(
|
|
1059
|
+
super(normalizedValue.replace(' ', 'T') + 'Z');
|
|
857
1060
|
this.useSpaceSeparator = false;
|
|
858
1061
|
this.useSpaceSeparator = useSpaceSeparator;
|
|
859
1062
|
this.originalFormat = originalFormat || value;
|
|
@@ -892,7 +1095,12 @@ class LocalDateTime extends Date {
|
|
|
892
1095
|
*/
|
|
893
1096
|
class OffsetDateTime extends Date {
|
|
894
1097
|
constructor(value, useSpaceSeparator = false) {
|
|
895
|
-
|
|
1098
|
+
// Normalize time part to include seconds if missing (TOML 1.1.0 allows optional seconds)
|
|
1099
|
+
let normalizedValue = value;
|
|
1100
|
+
if (!/\d{2}:\d{2}:\d{2}/.test(value)) {
|
|
1101
|
+
normalizedValue = value.replace(/(\d{2}:\d{2})([\s\-+TZ]|$)/, '$1:00$2');
|
|
1102
|
+
}
|
|
1103
|
+
super(normalizedValue.replace(' ', 'T'));
|
|
896
1104
|
this.useSpaceSeparator = false;
|
|
897
1105
|
this.useSpaceSeparator = useSpaceSeparator;
|
|
898
1106
|
this.originalFormat = value;
|
|
@@ -953,16 +1161,19 @@ const TRUE = 'true';
|
|
|
953
1161
|
const FALSE = 'false';
|
|
954
1162
|
const HAS_E = /e/i;
|
|
955
1163
|
const IS_DIVIDER = /\_/g;
|
|
956
|
-
const IS_INF =
|
|
957
|
-
const IS_NAN =
|
|
958
|
-
const IS_HEX = /^0x
|
|
959
|
-
const IS_OCTAL = /^0o
|
|
960
|
-
const IS_BINARY = /^0b
|
|
1164
|
+
const IS_INF = /^[+\-]?inf$/;
|
|
1165
|
+
const IS_NAN = /^[+\-]?nan$/;
|
|
1166
|
+
const IS_HEX = /^[+\-]?0x/i;
|
|
1167
|
+
const IS_OCTAL = /^[+\-]?0o/i;
|
|
1168
|
+
const IS_BINARY = /^[+\-]?0b/i;
|
|
961
1169
|
function* parseTOML(input) {
|
|
962
|
-
|
|
963
|
-
const cursor = new Cursor(
|
|
1170
|
+
// Use non-generator parsing to avoid stack overflow on deeply nested structures
|
|
1171
|
+
const cursor = new Cursor(tokenize(input));
|
|
964
1172
|
while (!cursor.next().done) {
|
|
965
|
-
|
|
1173
|
+
const blocks = walkBlock(cursor, input);
|
|
1174
|
+
for (const block of blocks) {
|
|
1175
|
+
yield block;
|
|
1176
|
+
}
|
|
966
1177
|
}
|
|
967
1178
|
}
|
|
968
1179
|
/**
|
|
@@ -977,57 +1188,14 @@ function* continueParsingTOML(existingAst, remainingString) {
|
|
|
977
1188
|
for (const item of existingAst) {
|
|
978
1189
|
yield item;
|
|
979
1190
|
}
|
|
980
|
-
// Parse and yield all items from the remaining string
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
if (cursor.value.type === TokenType.Comment) {
|
|
987
|
-
yield comment(cursor);
|
|
988
|
-
}
|
|
989
|
-
else if (cursor.value.type === TokenType.Bracket) {
|
|
990
|
-
yield table(cursor, input);
|
|
991
|
-
}
|
|
992
|
-
else if (cursor.value.type === TokenType.Literal) {
|
|
993
|
-
yield* keyValue(cursor, input);
|
|
994
|
-
}
|
|
995
|
-
else {
|
|
996
|
-
throw new ParseError(input, cursor.value.loc.start, `Unexpected token "${cursor.value.type}". Expected Comment, Bracket, or String`);
|
|
997
|
-
}
|
|
998
|
-
}
|
|
999
|
-
function* walkValue$1(cursor, input) {
|
|
1000
|
-
if (cursor.value.type === TokenType.Literal) {
|
|
1001
|
-
if (cursor.value.raw[0] === DOUBLE_QUOTE || cursor.value.raw[0] === SINGLE_QUOTE) {
|
|
1002
|
-
yield string(cursor);
|
|
1003
|
-
}
|
|
1004
|
-
else if (cursor.value.raw === TRUE || cursor.value.raw === FALSE) {
|
|
1005
|
-
yield boolean(cursor);
|
|
1006
|
-
}
|
|
1007
|
-
else if (dateFormatHelper.IS_FULL_DATE.test(cursor.value.raw) || dateFormatHelper.IS_FULL_TIME.test(cursor.value.raw)) {
|
|
1008
|
-
yield datetime(cursor, input);
|
|
1009
|
-
}
|
|
1010
|
-
else if ((!cursor.peek().done && cursor.peek().value.type === TokenType.Dot) ||
|
|
1011
|
-
IS_INF.test(cursor.value.raw) ||
|
|
1012
|
-
IS_NAN.test(cursor.value.raw) ||
|
|
1013
|
-
(HAS_E.test(cursor.value.raw) && !IS_HEX.test(cursor.value.raw))) {
|
|
1014
|
-
yield float(cursor, input);
|
|
1015
|
-
}
|
|
1016
|
-
else {
|
|
1017
|
-
yield integer(cursor);
|
|
1191
|
+
// Parse and yield all items from the remaining string using non-generator path
|
|
1192
|
+
const cursor = new Cursor(tokenize(remainingString));
|
|
1193
|
+
while (!cursor.next().done) {
|
|
1194
|
+
const blocks = walkBlock(cursor, remainingString);
|
|
1195
|
+
for (const block of blocks) {
|
|
1196
|
+
yield block;
|
|
1018
1197
|
}
|
|
1019
1198
|
}
|
|
1020
|
-
else if (cursor.value.type === TokenType.Curly) {
|
|
1021
|
-
yield inlineTable(cursor, input);
|
|
1022
|
-
}
|
|
1023
|
-
else if (cursor.value.type === TokenType.Bracket) {
|
|
1024
|
-
const [inline_array, comments] = inlineArray(cursor, input);
|
|
1025
|
-
yield inline_array;
|
|
1026
|
-
yield* comments;
|
|
1027
|
-
}
|
|
1028
|
-
else {
|
|
1029
|
-
throw new ParseError(input, cursor.value.loc.start, `Unrecognized token type "${cursor.value.type}". Expected String, Curly, or Bracket`);
|
|
1030
|
-
}
|
|
1031
1199
|
}
|
|
1032
1200
|
function comment(cursor) {
|
|
1033
1201
|
// # line comment
|
|
@@ -1061,8 +1229,22 @@ function table(cursor, input) {
|
|
|
1061
1229
|
if (is_table && cursor.value.raw !== '[') {
|
|
1062
1230
|
throw new ParseError(input, cursor.value.loc.start, `Expected table opening "[", found ${cursor.value.raw}`);
|
|
1063
1231
|
}
|
|
1064
|
-
if (!is_table
|
|
1065
|
-
|
|
1232
|
+
if (!is_table) {
|
|
1233
|
+
const next = cursor.peek();
|
|
1234
|
+
if (next.done) {
|
|
1235
|
+
throw new ParseError(input, cursor.value.loc.start, 'Expected second "[" for array of tables opening, found end of input');
|
|
1236
|
+
}
|
|
1237
|
+
if (cursor.value.raw !== '[' || next.value.raw !== '[') {
|
|
1238
|
+
throw new ParseError(input, cursor.value.loc.start, `Expected array of tables opening "[[", found ${cursor.value.raw + next.value.raw}`);
|
|
1239
|
+
}
|
|
1240
|
+
// Validate that table array brackets are immediately adjacent (no whitespace)
|
|
1241
|
+
const firstBracket = cursor.value;
|
|
1242
|
+
const secondBracket = next.value;
|
|
1243
|
+
// Check if brackets are on the same line and adjacent columns
|
|
1244
|
+
if (firstBracket.loc.end.line !== secondBracket.loc.start.line ||
|
|
1245
|
+
firstBracket.loc.end.column !== secondBracket.loc.start.column) {
|
|
1246
|
+
throw new ParseError(input, firstBracket.loc.start, 'Array of tables opening brackets must be immediately adjacent with no whitespace: [[table]]');
|
|
1247
|
+
}
|
|
1066
1248
|
}
|
|
1067
1249
|
// Set start location from opening tag
|
|
1068
1250
|
const key = is_table
|
|
@@ -1081,23 +1263,84 @@ function table(cursor, input) {
|
|
|
1081
1263
|
if (cursor.done) {
|
|
1082
1264
|
throw new ParseError(input, key.loc.start, `Expected table key, reached end of file`);
|
|
1083
1265
|
}
|
|
1266
|
+
// Check if the table/array name is empty (e.g., [[]] or [])
|
|
1267
|
+
if (cursor.value.type === TokenType.Bracket && cursor.value.raw === ']') {
|
|
1268
|
+
throw new ParseError(input, cursor.value.loc.start, type === NodeType.TableArray
|
|
1269
|
+
? 'Array of tables header [[]] requires a table name'
|
|
1270
|
+
: 'Table header [] requires a table name');
|
|
1271
|
+
}
|
|
1272
|
+
// Validate that multiline strings are not used as table keys
|
|
1273
|
+
const raw = cursor.value.raw;
|
|
1274
|
+
{
|
|
1275
|
+
if (raw.startsWith('"""') || raw.startsWith("'''")) {
|
|
1276
|
+
throw new ParseError(input, cursor.value.loc.start, 'Multiline strings (""" or \'\'\') cannot be used as keys');
|
|
1277
|
+
}
|
|
1278
|
+
// Validate bare key characters (TOML 1.1.0: A-Za-z0-9_- only)
|
|
1279
|
+
const isQuoted = raw.startsWith('"') || raw.startsWith("'");
|
|
1280
|
+
if (!isQuoted) {
|
|
1281
|
+
for (let i = 0; i < raw.length; i++) {
|
|
1282
|
+
const char = raw[i];
|
|
1283
|
+
if (!/[A-Za-z0-9_-]/.test(char)) {
|
|
1284
|
+
throw new ParseError(input, { line: cursor.value.loc.start.line, column: cursor.value.loc.start.column + i }, `Invalid character '${char}' in bare key. Bare keys can only contain A-Z, a-z, 0-9, _, and -`);
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
let keyValue;
|
|
1290
|
+
try {
|
|
1291
|
+
keyValue = [parseString(cursor.value.raw)];
|
|
1292
|
+
}
|
|
1293
|
+
catch (err) {
|
|
1294
|
+
const e = err;
|
|
1295
|
+
throw new ParseError(input, cursor.value.loc.start, e.message);
|
|
1296
|
+
}
|
|
1084
1297
|
key.item = {
|
|
1085
1298
|
type: NodeType.Key,
|
|
1086
1299
|
loc: cloneLocation(cursor.value.loc),
|
|
1087
1300
|
raw: cursor.value.raw,
|
|
1088
|
-
value:
|
|
1301
|
+
value: keyValue
|
|
1089
1302
|
};
|
|
1090
1303
|
while (!cursor.peek().done && cursor.peek().value.type === TokenType.Dot) {
|
|
1091
1304
|
cursor.next();
|
|
1092
1305
|
const dot = cursor.value;
|
|
1093
1306
|
cursor.next();
|
|
1307
|
+
// Validate each part of a dotted table key
|
|
1308
|
+
const partRaw = cursor.value.raw;
|
|
1309
|
+
const partIsQuoted = partRaw.startsWith('"') || partRaw.startsWith("'");
|
|
1310
|
+
if (!partIsQuoted) {
|
|
1311
|
+
for (let i = 0; i < partRaw.length; i++) {
|
|
1312
|
+
const char = partRaw[i];
|
|
1313
|
+
if (!/[A-Za-z0-9_-]/.test(char)) {
|
|
1314
|
+
throw new ParseError(input, { line: cursor.value.loc.start.line, column: cursor.value.loc.start.column + i }, `Invalid character '${char}' in bare key. Bare keys can only contain A-Z, a-z, 0-9, _, and -`);
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1094
1318
|
const before = ' '.repeat(dot.loc.start.column - key.item.loc.end.column);
|
|
1095
1319
|
const after = ' '.repeat(cursor.value.loc.start.column - dot.loc.end.column);
|
|
1096
1320
|
key.item.loc.end = cursor.value.loc.end;
|
|
1097
1321
|
key.item.raw += `${before}.${after}${cursor.value.raw}`;
|
|
1098
|
-
|
|
1322
|
+
try {
|
|
1323
|
+
key.item.value.push(parseString(cursor.value.raw));
|
|
1324
|
+
}
|
|
1325
|
+
catch (err) {
|
|
1326
|
+
const e = err;
|
|
1327
|
+
throw new ParseError(input, cursor.value.loc.start, e.message);
|
|
1328
|
+
}
|
|
1099
1329
|
}
|
|
1100
1330
|
cursor.next();
|
|
1331
|
+
// Table headers must not contain newlines - all parts must be on the same line
|
|
1332
|
+
// Example invalid TOML (table/newline-01): [tbl\n]
|
|
1333
|
+
// Example invalid TOML (table/newline-03): ["tbl"\n]
|
|
1334
|
+
if (!cursor.done) {
|
|
1335
|
+
const headerStartLine = is_table
|
|
1336
|
+
? key.loc.start.line
|
|
1337
|
+
: key.loc.start.line; // Both use the opening bracket line
|
|
1338
|
+
if (cursor.value.loc.start.line !== headerStartLine) {
|
|
1339
|
+
throw new ParseError(input, cursor.value.loc.start, is_table
|
|
1340
|
+
? `Table header must not contain newlines. Expected closing ']' on line ${headerStartLine}, found on line ${cursor.value.loc.start.line}`
|
|
1341
|
+
: `Unclosed array of tables header: expected closing ']]' on line ${headerStartLine}, found newline`);
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1101
1344
|
if (is_table && (cursor.done || cursor.value.raw !== ']')) {
|
|
1102
1345
|
throw new ParseError(input, cursor.done ? key.item.loc.end : cursor.value.loc.start, `Expected table closing "]", found ${cursor.done ? 'end of file' : cursor.value.raw}`);
|
|
1103
1346
|
}
|
|
@@ -1110,15 +1353,37 @@ function table(cursor, input) {
|
|
|
1110
1353
|
? 'end of file'
|
|
1111
1354
|
: cursor.value.raw + cursor.peek().value.raw}`);
|
|
1112
1355
|
}
|
|
1356
|
+
// Validate that table array closing brackets are immediately adjacent (no whitespace)
|
|
1357
|
+
if (!is_table) {
|
|
1358
|
+
const firstBracket = cursor.value;
|
|
1359
|
+
const secondBracket = cursor.peek().value;
|
|
1360
|
+
// Check if brackets are on the same line and adjacent columns
|
|
1361
|
+
if (firstBracket.loc.end.line !== secondBracket.loc.start.line ||
|
|
1362
|
+
firstBracket.loc.end.column !== secondBracket.loc.start.column) {
|
|
1363
|
+
throw new ParseError(input, firstBracket.loc.start, 'Array of tables closing brackets must be immediately adjacent with no whitespace: ]]');
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1113
1366
|
// Set end location from closing tag
|
|
1114
1367
|
if (!is_table)
|
|
1115
1368
|
cursor.next();
|
|
1116
1369
|
key.loc.end = cursor.value.loc.end;
|
|
1370
|
+
// Table/array headers must be alone on their line - nothing can follow the closing bracket(s)
|
|
1371
|
+
// Example invalid TOML (key/after-table): [error] this = "should not be here"
|
|
1372
|
+
// Example invalid TOML (key/after-array): [[agencies]] owner = "S Cjelli"
|
|
1373
|
+
if (!cursor.peek().done) {
|
|
1374
|
+
const nextToken = cursor.peek().value;
|
|
1375
|
+
// Check if there's content on the same line after the closing bracket
|
|
1376
|
+
// Comments are the only thing allowed on the same line
|
|
1377
|
+
if (nextToken.loc.start.line === key.loc.end.line &&
|
|
1378
|
+
nextToken.type !== TokenType.Comment) {
|
|
1379
|
+
throw new ParseError(input, nextToken.loc.start, `Unexpected content after ${is_table ? 'table' : 'array of tables'} header`);
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1117
1382
|
// Add child items
|
|
1118
1383
|
let items = [];
|
|
1119
1384
|
while (!cursor.peek().done && cursor.peek().value.type !== TokenType.Bracket) {
|
|
1120
1385
|
cursor.next();
|
|
1121
|
-
merge(items,
|
|
1386
|
+
merge(items, walkBlock(cursor, input));
|
|
1122
1387
|
}
|
|
1123
1388
|
return {
|
|
1124
1389
|
type: is_table ? NodeType.Table : NodeType.TableArray,
|
|
@@ -1132,56 +1397,20 @@ function table(cursor, input) {
|
|
|
1132
1397
|
items
|
|
1133
1398
|
};
|
|
1134
1399
|
}
|
|
1135
|
-
function
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
// ^-^ key
|
|
1140
|
-
// ^ equals
|
|
1141
|
-
// ^---^ value
|
|
1142
|
-
const key = {
|
|
1143
|
-
type: NodeType.Key,
|
|
1144
|
-
loc: cloneLocation(cursor.value.loc),
|
|
1145
|
-
raw: cursor.value.raw,
|
|
1146
|
-
value: [parseString(cursor.value.raw)]
|
|
1147
|
-
};
|
|
1148
|
-
while (!cursor.peek().done && cursor.peek().value.type === TokenType.Dot) {
|
|
1149
|
-
cursor.next();
|
|
1150
|
-
cursor.next();
|
|
1151
|
-
key.loc.end = cursor.value.loc.end;
|
|
1152
|
-
key.raw += `.${cursor.value.raw}`;
|
|
1153
|
-
key.value.push(parseString(cursor.value.raw));
|
|
1154
|
-
}
|
|
1155
|
-
cursor.next();
|
|
1156
|
-
if (cursor.done || cursor.value.type !== TokenType.Equal) {
|
|
1157
|
-
throw new ParseError(input, cursor.done ? key.loc.end : cursor.value.loc.start, `Expected "=" for key-value, found ${cursor.done ? 'end of file' : cursor.value.raw}`);
|
|
1400
|
+
function string(cursor, input) {
|
|
1401
|
+
let value;
|
|
1402
|
+
try {
|
|
1403
|
+
value = parseString(cursor.value.raw);
|
|
1158
1404
|
}
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
throw new ParseError(input, key.loc.start, `Expected value for key-value, reached end of file`);
|
|
1405
|
+
catch (err) {
|
|
1406
|
+
const e = err;
|
|
1407
|
+
throw new ParseError(input, cursor.value.loc.start, e.message);
|
|
1163
1408
|
}
|
|
1164
|
-
const [value, ...comments] = walkValue$1(cursor, input);
|
|
1165
|
-
return [
|
|
1166
|
-
{
|
|
1167
|
-
type: NodeType.KeyValue,
|
|
1168
|
-
key,
|
|
1169
|
-
value: value,
|
|
1170
|
-
loc: {
|
|
1171
|
-
start: clonePosition(key.loc.start),
|
|
1172
|
-
end: clonePosition(value.loc.end)
|
|
1173
|
-
},
|
|
1174
|
-
equals
|
|
1175
|
-
},
|
|
1176
|
-
...comments
|
|
1177
|
-
];
|
|
1178
|
-
}
|
|
1179
|
-
function string(cursor) {
|
|
1180
1409
|
return {
|
|
1181
1410
|
type: NodeType.String,
|
|
1182
1411
|
loc: cursor.value.loc,
|
|
1183
1412
|
raw: cursor.value.raw,
|
|
1184
|
-
value
|
|
1413
|
+
value
|
|
1185
1414
|
};
|
|
1186
1415
|
}
|
|
1187
1416
|
function boolean(cursor) {
|
|
@@ -1234,6 +1463,10 @@ function datetime(cursor, input) {
|
|
|
1234
1463
|
loc = { start, end: cursor.value.loc.end };
|
|
1235
1464
|
raw += `.${cursor.value.raw}`;
|
|
1236
1465
|
}
|
|
1466
|
+
// Validate datetime format
|
|
1467
|
+
{
|
|
1468
|
+
validateDateTimeFormat(raw, input, loc.start);
|
|
1469
|
+
}
|
|
1237
1470
|
if (!dateFormatHelper.IS_FULL_DATE.test(raw)) {
|
|
1238
1471
|
// Local time only (e.g., "07:32:00" or "07:32:00.999")
|
|
1239
1472
|
if (dateFormatHelper.IS_TIME_ONLY.test(raw)) {
|
|
@@ -1276,18 +1509,227 @@ function datetime(cursor, input) {
|
|
|
1276
1509
|
value
|
|
1277
1510
|
};
|
|
1278
1511
|
}
|
|
1512
|
+
// Helper function to calculate days in a month for any year (including 0-99)
|
|
1513
|
+
// JavaScript's Date constructor treats years 0-99 as 1900-1999, so we need manual calculation
|
|
1514
|
+
function getDaysInMonth(year, month) {
|
|
1515
|
+
// Month is 1-12
|
|
1516
|
+
const daysPerMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
1517
|
+
if (month === 2) {
|
|
1518
|
+
// Check if it's a leap year
|
|
1519
|
+
// Leap year rules: divisible by 4, except century years (divisible by 100) unless also divisible by 400
|
|
1520
|
+
const isLeapYear = (year % 4 === 0 && year % 100 !== 0) || (year % 400 === 0);
|
|
1521
|
+
return isLeapYear ? 29 : 28;
|
|
1522
|
+
}
|
|
1523
|
+
return daysPerMonth[month - 1];
|
|
1524
|
+
}
|
|
1525
|
+
// Helper function to validate datetime format
|
|
1526
|
+
function validateDateTimeFormat(raw, input, loc) {
|
|
1527
|
+
// Group 9: fractional seconds and timezone offset validation.
|
|
1528
|
+
// Reject fractional seconds with no digits after the dot (e.g. "...:09.Z" or "...:09.+01:00").
|
|
1529
|
+
if (/\.([Zz]|[+-])/.test(raw)) {
|
|
1530
|
+
throw new ParseError(input, loc, `Invalid datetime "${raw}": fractional seconds must have at least one digit after decimal point`);
|
|
1531
|
+
}
|
|
1532
|
+
// Reject trailing +/- without hour/minute (e.g., "...+" or "...-")
|
|
1533
|
+
if (/[+-]$/.test(raw)) {
|
|
1534
|
+
throw new ParseError(input, loc, `Invalid datetime "${raw}": timezone offset requires hour and minute components`);
|
|
1535
|
+
}
|
|
1536
|
+
// If an offset is present, it must be [+-]HH:MM and only after a time component.
|
|
1537
|
+
// (Avoid accidentally matching date hyphens by requiring a time first.)
|
|
1538
|
+
const hasTime = /\d{2}:\d{2}/.test(raw);
|
|
1539
|
+
const offsetMatch = hasTime ? raw.match(/([+-])(\d+)(:?)(\d*)$/) : null;
|
|
1540
|
+
if (offsetMatch) {
|
|
1541
|
+
const fullOffset = offsetMatch[0];
|
|
1542
|
+
const hours = offsetMatch[2];
|
|
1543
|
+
const colon = offsetMatch[3];
|
|
1544
|
+
const minutes = offsetMatch[4];
|
|
1545
|
+
if (colon !== ':') {
|
|
1546
|
+
throw new ParseError(input, loc, `Invalid timezone offset "${fullOffset}": must use colon separator (e.g., +09:09)`);
|
|
1547
|
+
}
|
|
1548
|
+
if (hours.length !== 2) {
|
|
1549
|
+
throw new ParseError(input, loc, `Invalid timezone offset "${fullOffset}": hour must be exactly 2 digits`);
|
|
1550
|
+
}
|
|
1551
|
+
if (!minutes || minutes.length === 0) {
|
|
1552
|
+
throw new ParseError(input, loc, `Invalid timezone offset "${fullOffset}": minute component is required`);
|
|
1553
|
+
}
|
|
1554
|
+
if (minutes.length !== 2) {
|
|
1555
|
+
throw new ParseError(input, loc, `Invalid timezone offset "${fullOffset}": minute must be exactly 2 digits`);
|
|
1556
|
+
}
|
|
1557
|
+
const hourNum = parseInt(hours, 10);
|
|
1558
|
+
if (hourNum < 0 || hourNum > 23) {
|
|
1559
|
+
throw new ParseError(input, loc, `Invalid timezone offset "${fullOffset}": hour must be between 00 and 23, found ${hours}`);
|
|
1560
|
+
}
|
|
1561
|
+
const minuteNum = parseInt(minutes, 10);
|
|
1562
|
+
if (minuteNum < 0 || minuteNum > 59) {
|
|
1563
|
+
throw new ParseError(input, loc, `Invalid timezone offset "${fullOffset}": minute must be between 00 and 59, found ${minutes}`);
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
// First, ensure the overall shape is valid (anchors matter).
|
|
1567
|
+
// This catches cases where regexes below might partially match a prefix and ignore trailing junk.
|
|
1568
|
+
const validDateTimePattern = /^\d{4}-\d{2}-\d{2}(?:[Tt ]\d{2}:\d{2}(?::\d{2})?(?:\.\d+)?(?:[Zz]|[+-]\d{2}:\d{2})?)?$/;
|
|
1569
|
+
const validTimePattern = /^\d{2}:\d{2}(?::\d{2})?(?:\.\d+)?$/;
|
|
1570
|
+
if (!validDateTimePattern.test(raw) && !validTimePattern.test(raw)) {
|
|
1571
|
+
// Date cannot end with trailing T without a time component
|
|
1572
|
+
if (/^\d{4}-\d{2}-\d{2}T$/.test(raw)) {
|
|
1573
|
+
throw new ParseError(input, loc, `Invalid date "${raw}": date cannot end with 'T' without a time component`);
|
|
1574
|
+
}
|
|
1575
|
+
// Any unexpected character immediately after a date-only value.
|
|
1576
|
+
// Exclude T/t (date-time separators) from this check.
|
|
1577
|
+
if (/^\d{4}-\d{2}-\d{2}[a-su-zA-SU-Z]/.test(raw)) {
|
|
1578
|
+
throw new ParseError(input, loc, `Invalid date "${raw}": unexpected character after date`);
|
|
1579
|
+
}
|
|
1580
|
+
// Missing separator between date and time
|
|
1581
|
+
if (/^\d{4}-\d{2}-\d{2}\d{2}:\d{2}/.test(raw)) {
|
|
1582
|
+
throw new ParseError(input, loc, `Invalid datetime "${raw}": missing separator 'T' or space between date and time`);
|
|
1583
|
+
}
|
|
1584
|
+
throw new ParseError(input, loc, `Invalid datetime "${raw}"`);
|
|
1585
|
+
}
|
|
1586
|
+
// Check for year with wrong number of digits (must be exactly 4)
|
|
1587
|
+
const yearMatch = raw.match(/^(\d+)-/);
|
|
1588
|
+
if (yearMatch && yearMatch[1].length !== 4) {
|
|
1589
|
+
throw new ParseError(input, loc, `Invalid date "${raw}": year must be exactly 4 digits, found ${yearMatch[1].length}`);
|
|
1590
|
+
}
|
|
1591
|
+
// Check for date with wrong number of digits for month/day BEFORE extracting components
|
|
1592
|
+
// Pattern should be YYYY-MM-DD (exactly 4, 2, 2 digits)
|
|
1593
|
+
const datePattern = /^(\d+)-(\d+)-(\d+)/;
|
|
1594
|
+
const dateMatch = raw.match(datePattern);
|
|
1595
|
+
if (dateMatch) {
|
|
1596
|
+
const [, , month, day] = dateMatch;
|
|
1597
|
+
if (month.length !== 2) {
|
|
1598
|
+
throw new ParseError(input, loc, `Invalid date "${raw}": month must be exactly 2 digits, found ${month.length}`);
|
|
1599
|
+
}
|
|
1600
|
+
if (day.length !== 2) {
|
|
1601
|
+
throw new ParseError(input, loc, `Invalid date "${raw}": day must be exactly 2 digits, found ${day.length}`);
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
// Check for time with wrong number of digits for hour/minute/second
|
|
1605
|
+
const timePattern = /[T ](\d+):(\d+)(?::(\d+))?/;
|
|
1606
|
+
const timeMatch = raw.match(timePattern);
|
|
1607
|
+
if (timeMatch) {
|
|
1608
|
+
const [, hour, minute, second] = timeMatch;
|
|
1609
|
+
if (hour.length !== 2) {
|
|
1610
|
+
throw new ParseError(input, loc, `Invalid time "${raw}": hour must be exactly 2 digits, found ${hour.length}`);
|
|
1611
|
+
}
|
|
1612
|
+
if (minute.length !== 2) {
|
|
1613
|
+
throw new ParseError(input, loc, `Invalid time "${raw}": minute must be exactly 2 digits, found ${minute.length}`);
|
|
1614
|
+
}
|
|
1615
|
+
if (second && second.length !== 2) {
|
|
1616
|
+
throw new ParseError(input, loc, `Invalid time "${raw}": second must be exactly 2 digits, found ${second.length}`);
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
// Check for standalone time (no date prefix)
|
|
1620
|
+
const timeOnlyPattern = /^(\d+):(\d+)(?::(\d+))?/;
|
|
1621
|
+
const timeOnlyMatch = raw.match(timeOnlyPattern);
|
|
1622
|
+
if (timeOnlyMatch && !dateMatch) {
|
|
1623
|
+
const [, hour, minute, second] = timeOnlyMatch;
|
|
1624
|
+
if (hour.length !== 2) {
|
|
1625
|
+
throw new ParseError(input, loc, `Invalid time "${raw}": hour must be exactly 2 digits, found ${hour.length}`);
|
|
1626
|
+
}
|
|
1627
|
+
if (minute.length !== 2) {
|
|
1628
|
+
throw new ParseError(input, loc, `Invalid time "${raw}": minute must be exactly 2 digits, found ${minute.length}`);
|
|
1629
|
+
}
|
|
1630
|
+
if (second && second.length !== 2) {
|
|
1631
|
+
throw new ParseError(input, loc, `Invalid time "${raw}": second must be exactly 2 digits, found ${second.length}`);
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
// Extract components for range validation (now we know they have the right length)
|
|
1635
|
+
const dateTimeMatch = raw.match(/^(\d{4})-(\d{2})-(\d{2})(?:[T ](\d{2}):(\d{2})(?::(\d{2}))?)?/);
|
|
1636
|
+
const timeOnlyMatchExact = raw.match(/^(\d{2}):(\d{2})(?::(\d{2}))?/);
|
|
1637
|
+
if (dateTimeMatch) {
|
|
1638
|
+
const [, year, month, day, hour, minute, second] = dateTimeMatch;
|
|
1639
|
+
// Validate month range (01-12)
|
|
1640
|
+
const monthNum = parseInt(month, 10);
|
|
1641
|
+
if (monthNum < 1 || monthNum > 12) {
|
|
1642
|
+
throw new ParseError(input, loc, `Invalid date "${raw}": month must be between 01 and 12`);
|
|
1643
|
+
}
|
|
1644
|
+
// Validate day range (01-31 depending on month)
|
|
1645
|
+
const dayNum = parseInt(day, 10);
|
|
1646
|
+
if (dayNum < 1 || dayNum > 31) {
|
|
1647
|
+
throw new ParseError(input, loc, `Invalid date "${raw}": day must be between 01 and 31`);
|
|
1648
|
+
}
|
|
1649
|
+
// Check if day is valid for the specific month
|
|
1650
|
+
const yearNum = parseInt(year, 10);
|
|
1651
|
+
const daysInMonth = getDaysInMonth(yearNum, monthNum);
|
|
1652
|
+
if (dayNum > daysInMonth) {
|
|
1653
|
+
throw new ParseError(input, loc, `Invalid date "${raw}": day ${day} is invalid for month ${month} in year ${year}`);
|
|
1654
|
+
}
|
|
1655
|
+
// Validate time component ranges if present
|
|
1656
|
+
if (hour !== undefined) {
|
|
1657
|
+
const hourNum = parseInt(hour, 10);
|
|
1658
|
+
if (hourNum < 0 || hourNum > 23) {
|
|
1659
|
+
throw new ParseError(input, loc, `Invalid time "${raw}": hour must be between 00 and 23`);
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
if (minute !== undefined) {
|
|
1663
|
+
const minuteNum = parseInt(minute, 10);
|
|
1664
|
+
if (minuteNum < 0 || minuteNum > 59) {
|
|
1665
|
+
throw new ParseError(input, loc, `Invalid time "${raw}": minute must be between 00 and 59`);
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
if (second !== undefined) {
|
|
1669
|
+
const secondNum = parseInt(second, 10);
|
|
1670
|
+
if (secondNum < 0 || secondNum > 60) {
|
|
1671
|
+
throw new ParseError(input, loc, `Invalid time "${raw}": second must be between 00 and 60`);
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
else if (timeOnlyMatchExact) {
|
|
1676
|
+
const [, hour, minute, second] = timeOnlyMatchExact;
|
|
1677
|
+
const hourNum = parseInt(hour, 10);
|
|
1678
|
+
if (hourNum < 0 || hourNum > 23) {
|
|
1679
|
+
throw new ParseError(input, loc, `Invalid time "${raw}": hour must be between 00 and 23`);
|
|
1680
|
+
}
|
|
1681
|
+
const minuteNum = parseInt(minute, 10);
|
|
1682
|
+
if (minuteNum < 0 || minuteNum > 59) {
|
|
1683
|
+
throw new ParseError(input, loc, `Invalid time "${raw}": minute must be between 00 and 59`);
|
|
1684
|
+
}
|
|
1685
|
+
if (second !== undefined) {
|
|
1686
|
+
const secondNum = parseInt(second, 10);
|
|
1687
|
+
if (secondNum < 0 || secondNum > 60) {
|
|
1688
|
+
throw new ParseError(input, loc, `Invalid time "${raw}": second must be between 00 and 60`);
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1279
1693
|
function float(cursor, input) {
|
|
1280
1694
|
let loc = cursor.value.loc;
|
|
1281
1695
|
let raw = cursor.value.raw;
|
|
1282
1696
|
let value;
|
|
1283
1697
|
if (IS_INF.test(raw)) {
|
|
1284
|
-
value = raw
|
|
1698
|
+
value = raw.startsWith('-') ? -Infinity : Infinity;
|
|
1285
1699
|
}
|
|
1286
1700
|
else if (IS_NAN.test(raw)) {
|
|
1287
|
-
value =
|
|
1701
|
+
value = NaN;
|
|
1288
1702
|
}
|
|
1289
1703
|
else if (!cursor.peek().done && cursor.peek().value.type === TokenType.Dot) {
|
|
1290
1704
|
const start = loc.start;
|
|
1705
|
+
{
|
|
1706
|
+
// Validate that we don't already have an exponent (e.g., 1e2 cannot have a fractional part after it)
|
|
1707
|
+
if (HAS_E.test(raw) && !IS_HEX.test(raw)) {
|
|
1708
|
+
throw new ParseError(input, loc.start, `Invalid float "${raw}": cannot have decimal point after exponent`);
|
|
1709
|
+
}
|
|
1710
|
+
// Validate integer part before decimal point
|
|
1711
|
+
const intPart = raw;
|
|
1712
|
+
// Validate no leading zeros in integer part (after optional sign)
|
|
1713
|
+
// e.g. 00.1, +00.1, 0_0.1 are invalid
|
|
1714
|
+
const withoutUnderscores = intPart.replace(IS_DIVIDER, '');
|
|
1715
|
+
if (/^[+\-]?0\d/.test(withoutUnderscores) && !IS_HEX.test(intPart) && !IS_OCTAL.test(intPart) && !IS_BINARY.test(intPart)) {
|
|
1716
|
+
throw new ParseError(input, loc.start, 'Leading zeros are not allowed in the integer part of a float');
|
|
1717
|
+
}
|
|
1718
|
+
// Validate no leading dot (must have at least one digit before the dot)
|
|
1719
|
+
const withoutSign = intPart.replace(/^[+\-]/, '');
|
|
1720
|
+
if (withoutSign === '' || withoutSign === '_') {
|
|
1721
|
+
throw new ParseError(input, loc.start, `Invalid float: decimal point must be preceded by at least one digit`);
|
|
1722
|
+
}
|
|
1723
|
+
if (/_$/.test(intPart)) {
|
|
1724
|
+
throw new ParseError(input, loc.start, 'Underscore before decimal point is not allowed');
|
|
1725
|
+
}
|
|
1726
|
+
if (/^[+\-]?_/.test(intPart)) {
|
|
1727
|
+
throw new ParseError(input, loc.start, 'Leading underscore is not allowed');
|
|
1728
|
+
}
|
|
1729
|
+
if (/__/.test(intPart)) {
|
|
1730
|
+
throw new ParseError(input, loc.start, 'Consecutive underscores are not allowed');
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1291
1733
|
// From spec:
|
|
1292
1734
|
// | A fractional part is a decimal point followed by one or more digits.
|
|
1293
1735
|
//
|
|
@@ -1299,47 +1741,425 @@ function float(cursor, input) {
|
|
|
1299
1741
|
cursor.next();
|
|
1300
1742
|
raw += `.${cursor.value.raw}`;
|
|
1301
1743
|
loc = { start, end: cursor.value.loc.end };
|
|
1744
|
+
{
|
|
1745
|
+
// Validate underscore placement in fractional part
|
|
1746
|
+
const fracPart = cursor.value.raw;
|
|
1747
|
+
// Validate that fractional part starts with a digit (not 'e')
|
|
1748
|
+
if (!/^\d/.test(fracPart)) {
|
|
1749
|
+
throw new ParseError(input, cursor.value.loc.start, `Invalid float: fractional part must start with a digit, found "${fracPart}"`);
|
|
1750
|
+
}
|
|
1751
|
+
if (/^_/.test(fracPart)) {
|
|
1752
|
+
throw new ParseError(input, cursor.value.loc.start, 'Underscore after decimal point is not allowed');
|
|
1753
|
+
}
|
|
1754
|
+
if (/_$/.test(fracPart)) {
|
|
1755
|
+
throw new ParseError(input, cursor.value.loc.start, 'Trailing underscore in fractional part is not allowed');
|
|
1756
|
+
}
|
|
1757
|
+
// Validate underscore before exponent in fractional part
|
|
1758
|
+
if (/_[eE]/.test(fracPart)) {
|
|
1759
|
+
throw new ParseError(input, cursor.value.loc.start, 'Underscore before exponent is not allowed');
|
|
1760
|
+
}
|
|
1761
|
+
// Validate underscore at start of exponent in fractional part
|
|
1762
|
+
if (/[eE][+\-]?_/.test(fracPart)) {
|
|
1763
|
+
throw new ParseError(input, cursor.value.loc.start, 'Underscore at start of exponent is not allowed');
|
|
1764
|
+
}
|
|
1765
|
+
// Validate incomplete exponent in fractional part (just E with nothing or just sign after)
|
|
1766
|
+
if (/[eE][+\-]?$/.test(fracPart)) {
|
|
1767
|
+
throw new ParseError(input, cursor.value.loc.start, `Invalid float "${raw}": incomplete exponent`);
|
|
1768
|
+
}
|
|
1769
|
+
// Validate no decimal point in exponent in fractional part
|
|
1770
|
+
if (/[eE][+\-]?.*\./.test(fracPart)) {
|
|
1771
|
+
throw new ParseError(input, cursor.value.loc.start, `Invalid float "${raw}": decimal point not allowed in exponent`);
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1302
1774
|
value = Number(raw.replace(IS_DIVIDER, ''));
|
|
1303
1775
|
}
|
|
1304
1776
|
else {
|
|
1777
|
+
// Validate underscore placement in integer part (exponent-only floats like 1e5)
|
|
1778
|
+
if (/_$/.test(raw)) {
|
|
1779
|
+
throw new ParseError(input, loc.start, 'Underscore before decimal point is not allowed');
|
|
1780
|
+
}
|
|
1781
|
+
if (/^[+\-]?_/.test(raw)) {
|
|
1782
|
+
throw new ParseError(input, loc.start, 'Leading underscore is not allowed');
|
|
1783
|
+
}
|
|
1784
|
+
if (/__/.test(raw)) {
|
|
1785
|
+
throw new ParseError(input, loc.start, 'Consecutive underscores are not allowed');
|
|
1786
|
+
}
|
|
1787
|
+
// Validate incomplete exponent (just E with nothing or just sign after)
|
|
1788
|
+
if (/[eE][+\-]?$/.test(raw)) {
|
|
1789
|
+
throw new ParseError(input, loc.start, `Invalid float "${raw}": incomplete exponent`);
|
|
1790
|
+
}
|
|
1791
|
+
// Validate no decimal point in exponent
|
|
1792
|
+
if (/[eE][+\-]?.*\./.test(raw)) {
|
|
1793
|
+
throw new ParseError(input, loc.start, `Invalid float "${raw}": decimal point not allowed in exponent`);
|
|
1794
|
+
}
|
|
1795
|
+
// Validate underscore before exponent
|
|
1796
|
+
if (/_[eE]/.test(raw)) {
|
|
1797
|
+
throw new ParseError(input, loc.start, 'Underscore before exponent is not allowed');
|
|
1798
|
+
}
|
|
1799
|
+
// Validate underscore at start of exponent
|
|
1800
|
+
if (/[eE][+\-]?_/.test(raw)) {
|
|
1801
|
+
throw new ParseError(input, loc.start, 'Underscore at start of exponent is not allowed');
|
|
1802
|
+
}
|
|
1803
|
+
// Validate no dot after exponent (e.g., 1e2.3 is invalid)
|
|
1804
|
+
if (!cursor.peek().done && cursor.peek().value.type === TokenType.Dot) {
|
|
1805
|
+
throw new ParseError(input, cursor.peek().value.loc.start, `Invalid float "${raw}.": cannot have decimal point after exponent`);
|
|
1806
|
+
}
|
|
1807
|
+
// Validate no leading zeros in integer part (after optional sign)
|
|
1808
|
+
// e.g. 00e1, +00e1, 0_0e1 are invalid
|
|
1809
|
+
const withoutUnderscores = raw.replace(IS_DIVIDER, '');
|
|
1810
|
+
if (/^[+\-]?0\d/.test(withoutUnderscores) && !IS_HEX.test(raw) && !IS_OCTAL.test(raw) && !IS_BINARY.test(raw)) {
|
|
1811
|
+
throw new ParseError(input, loc.start, 'Leading zeros are not allowed in the integer part of a float');
|
|
1812
|
+
}
|
|
1305
1813
|
value = Number(raw.replace(IS_DIVIDER, ''));
|
|
1306
1814
|
}
|
|
1815
|
+
// Reject non-special floats that parse to NaN (e.g. "Inf", "NaN", "1ee2")
|
|
1816
|
+
if (Number.isNaN(value) && !IS_NAN.test(raw)) {
|
|
1817
|
+
throw new ParseError(input, loc.start, `Invalid float "${raw}"`);
|
|
1818
|
+
}
|
|
1307
1819
|
return { type: NodeType.Float, loc, raw, value };
|
|
1308
1820
|
}
|
|
1309
|
-
function integer(cursor) {
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
};
|
|
1821
|
+
function integer(cursor, input) {
|
|
1822
|
+
const raw = cursor.value.raw;
|
|
1823
|
+
const loc = cursor.value.loc;
|
|
1824
|
+
// Guard: values that look like dates/times must never be parsed as integers.
|
|
1825
|
+
// (Prevents parseInt() from accepting prefixes like "199-09-09" -> 199.)
|
|
1826
|
+
if (/^\d{1,}-\d{1,}/.test(raw) ||
|
|
1827
|
+
/^\d{1,}:\d{1,}/.test(raw) ||
|
|
1828
|
+
/^\d{6}-\d{2}$/.test(raw)) {
|
|
1829
|
+
throw new ParseError(input, loc.start, `Invalid integer "${raw}"`);
|
|
1830
|
+
}
|
|
1831
|
+
{
|
|
1832
|
+
// > Integer values -0 and +0 are valid and identical to an unprefixed zero
|
|
1833
|
+
if (raw === '-0' || raw === '+0') {
|
|
1834
|
+
return {
|
|
1835
|
+
type: NodeType.Integer,
|
|
1836
|
+
loc: loc,
|
|
1837
|
+
raw: raw,
|
|
1838
|
+
value: 0
|
|
1839
|
+
};
|
|
1840
|
+
}
|
|
1841
|
+
// Validation: No double signs (++99, --99)
|
|
1842
|
+
if (/^[+\-]{2,}/.test(raw)) {
|
|
1843
|
+
throw new ParseError(input, loc.start, 'Double sign is not allowed in integers');
|
|
1844
|
+
}
|
|
1845
|
+
// Validation: No leading zeros (except for hex/octal/binary with prefixes)
|
|
1846
|
+
// Check after removing underscores to catch cases like 0_0 which is equivalent to 00
|
|
1847
|
+
const withoutUnderscores = raw.replace(/_/g, '');
|
|
1848
|
+
if (/^[+\-]?0\d/.test(withoutUnderscores) && !IS_HEX.test(raw) && !IS_OCTAL.test(raw) && !IS_BINARY.test(raw)) {
|
|
1849
|
+
throw new ParseError(input, loc.start, 'Leading zeros are not allowed in decimal integers');
|
|
1850
|
+
}
|
|
1851
|
+
// Validation: No trailing underscores
|
|
1852
|
+
if (/_$/.test(raw)) {
|
|
1853
|
+
throw new ParseError(input, loc.start, 'Underscores in numbers must be surrounded by digits');
|
|
1854
|
+
}
|
|
1855
|
+
// Validation: No leading underscores (after optional sign)
|
|
1856
|
+
if (/^[+\-]?_/.test(raw)) {
|
|
1857
|
+
throw new ParseError(input, loc.start, 'Underscores in numbers must be surrounded by digits');
|
|
1858
|
+
}
|
|
1859
|
+
// Validation: No consecutive underscores
|
|
1860
|
+
if (/__/.test(raw)) {
|
|
1861
|
+
throw new ParseError(input, loc.start, 'Consecutive underscores in numbers are not allowed');
|
|
1862
|
+
}
|
|
1318
1863
|
}
|
|
1319
1864
|
let radix = 10;
|
|
1320
|
-
|
|
1865
|
+
let numericPart;
|
|
1866
|
+
// Hexadecimal validation
|
|
1867
|
+
if (IS_HEX.test(raw)) {
|
|
1321
1868
|
radix = 16;
|
|
1869
|
+
// Validation: Capital prefix not allowed
|
|
1870
|
+
if (/^[+\-]?0X/.test(raw)) {
|
|
1871
|
+
throw new ParseError(input, loc.start, 'Hexadecimal prefix must be lowercase "0x"');
|
|
1872
|
+
}
|
|
1873
|
+
// Validation: Underscore after prefix
|
|
1874
|
+
if (/^[+\-]?0x_/.test(raw)) {
|
|
1875
|
+
throw new ParseError(input, loc.start, 'Underscores in numbers must be surrounded by digits');
|
|
1876
|
+
}
|
|
1877
|
+
numericPart = raw.replace(/^[+\-]?0x/i, '');
|
|
1878
|
+
// Validation: Incomplete hexadecimal
|
|
1879
|
+
if (!numericPart || numericPart === '_' || /^_/.test(numericPart)) {
|
|
1880
|
+
throw new ParseError(input, loc.start, 'Incomplete hexadecimal number');
|
|
1881
|
+
}
|
|
1882
|
+
// Validation: Invalid hexadecimal digits
|
|
1883
|
+
const hexDigits = numericPart.replace(/_/g, '');
|
|
1884
|
+
if (!/^[0-9a-fA-F]+$/.test(hexDigits)) {
|
|
1885
|
+
throw new ParseError(input, loc.start, 'Invalid hexadecimal digits');
|
|
1886
|
+
}
|
|
1887
|
+
// Validation: Signed non-decimal numbers not allowed
|
|
1888
|
+
if (/^[+\-]/.test(raw)) {
|
|
1889
|
+
throw new ParseError(input, loc.start, 'Hexadecimal numbers cannot have a sign prefix');
|
|
1890
|
+
}
|
|
1322
1891
|
}
|
|
1323
|
-
|
|
1892
|
+
// Octal validation
|
|
1893
|
+
else if (IS_OCTAL.test(raw)) {
|
|
1324
1894
|
radix = 8;
|
|
1895
|
+
// Validation: Capital prefix not allowed
|
|
1896
|
+
if (/^[+\-]?0O/.test(raw)) {
|
|
1897
|
+
throw new ParseError(input, loc.start, 'Octal prefix must be lowercase "0o"');
|
|
1898
|
+
}
|
|
1899
|
+
// Validation: Underscore after prefix
|
|
1900
|
+
if (/^[+\-]?0o_/.test(raw)) {
|
|
1901
|
+
throw new ParseError(input, loc.start, 'Underscores in numbers must be surrounded by digits');
|
|
1902
|
+
}
|
|
1903
|
+
numericPart = raw.replace(/^[+\-]?0o/i, '');
|
|
1904
|
+
// Validation: Incomplete octal
|
|
1905
|
+
if (!numericPart || numericPart === '_' || /^_/.test(numericPart)) {
|
|
1906
|
+
throw new ParseError(input, loc.start, 'Incomplete octal number');
|
|
1907
|
+
}
|
|
1908
|
+
// Validation: Invalid octal digits
|
|
1909
|
+
const octalDigits = numericPart.replace(/_/g, '');
|
|
1910
|
+
if (!/^[0-7]+$/.test(octalDigits)) {
|
|
1911
|
+
throw new ParseError(input, loc.start, 'Invalid octal digits (must be 0-7)');
|
|
1912
|
+
}
|
|
1913
|
+
// Validation: Signed non-decimal numbers not allowed
|
|
1914
|
+
if (/^[+\-]/.test(raw)) {
|
|
1915
|
+
throw new ParseError(input, loc.start, 'Octal numbers cannot have a sign prefix');
|
|
1916
|
+
}
|
|
1325
1917
|
}
|
|
1326
|
-
|
|
1918
|
+
// Binary validation
|
|
1919
|
+
else if (IS_BINARY.test(raw)) {
|
|
1327
1920
|
radix = 2;
|
|
1921
|
+
// Validation: Capital prefix not allowed
|
|
1922
|
+
if (/^[+\-]?0B/.test(raw)) {
|
|
1923
|
+
throw new ParseError(input, loc.start, 'Binary prefix must be lowercase "0b"');
|
|
1924
|
+
}
|
|
1925
|
+
// Validation: Underscore after prefix
|
|
1926
|
+
if (/^[+\-]?0b_/.test(raw)) {
|
|
1927
|
+
throw new ParseError(input, loc.start, 'Underscores in numbers must be surrounded by digits');
|
|
1928
|
+
}
|
|
1929
|
+
numericPart = raw.replace(/^[+\-]?0b/i, '');
|
|
1930
|
+
// Validation: Incomplete binary
|
|
1931
|
+
if (!numericPart || numericPart === '_' || /^_/.test(numericPart)) {
|
|
1932
|
+
throw new ParseError(input, loc.start, 'Incomplete binary number');
|
|
1933
|
+
}
|
|
1934
|
+
// Validation: Invalid binary digits
|
|
1935
|
+
const binaryDigits = numericPart.replace(/_/g, '');
|
|
1936
|
+
if (!/^[01]+$/.test(binaryDigits)) {
|
|
1937
|
+
throw new ParseError(input, loc.start, 'Invalid binary digits (must be 0 or 1)');
|
|
1938
|
+
}
|
|
1939
|
+
// Validation: Signed non-decimal numbers not allowed
|
|
1940
|
+
if (/^[+\-]/.test(raw)) {
|
|
1941
|
+
throw new ParseError(input, loc.start, 'Binary numbers cannot have a sign prefix');
|
|
1942
|
+
}
|
|
1328
1943
|
}
|
|
1329
|
-
const value = parseInt(
|
|
1330
|
-
.
|
|
1944
|
+
const value = parseInt(raw
|
|
1945
|
+
.replace(IS_DIVIDER, '')
|
|
1331
1946
|
.replace(IS_OCTAL, '')
|
|
1332
1947
|
.replace(IS_BINARY, ''), radix);
|
|
1948
|
+
if (Number.isNaN(value)) {
|
|
1949
|
+
throw new ParseError(input, loc.start, `Invalid integer "${raw}"`);
|
|
1950
|
+
}
|
|
1333
1951
|
return {
|
|
1334
1952
|
type: NodeType.Integer,
|
|
1335
|
-
loc:
|
|
1336
|
-
raw:
|
|
1953
|
+
loc: loc,
|
|
1954
|
+
raw: raw,
|
|
1337
1955
|
value
|
|
1338
1956
|
};
|
|
1339
1957
|
}
|
|
1958
|
+
/**
|
|
1959
|
+
* Walk a Block (Comment, Table, or KeyValue)
|
|
1960
|
+
* This new version avoids recursion for key-value pairs to improve performance on large files.
|
|
1961
|
+
* @param cursor Cursor<Token>
|
|
1962
|
+
* @param input string
|
|
1963
|
+
* @returns Block[]
|
|
1964
|
+
*/
|
|
1965
|
+
function walkBlock(cursor, input) {
|
|
1966
|
+
if (cursor.value.type === TokenType.Comment) {
|
|
1967
|
+
return [comment(cursor)];
|
|
1968
|
+
}
|
|
1969
|
+
else if (cursor.value.type === TokenType.Bracket) {
|
|
1970
|
+
// For tables, we can't easily avoid recursion, so just use the existing function
|
|
1971
|
+
// In practice, top-level tables aren't deeply nested
|
|
1972
|
+
return [table(cursor, input)];
|
|
1973
|
+
}
|
|
1974
|
+
else if (cursor.value.type === TokenType.Literal) {
|
|
1975
|
+
return keyValue(cursor, input);
|
|
1976
|
+
}
|
|
1977
|
+
else if (cursor.value.type === TokenType.Equal) {
|
|
1978
|
+
throw new ParseError(input, cursor.value.loc.start, `Missing key before '='`);
|
|
1979
|
+
}
|
|
1980
|
+
else {
|
|
1981
|
+
throw new ParseError(input, cursor.value.loc.start, `Unexpected token "${cursor.value.type}". Expected Comment, Bracket, or String`);
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
/**
|
|
1985
|
+
* Walk a KeyValue pair or Comment
|
|
1986
|
+
* This new version avoids recursion for key-value pairs to improve performance on large files.
|
|
1987
|
+
* @param cursor Cursor<Token>
|
|
1988
|
+
* @param input string
|
|
1989
|
+
* @returns Array<KeyValue | Comment>
|
|
1990
|
+
*/
|
|
1991
|
+
function keyValue(cursor, input) {
|
|
1992
|
+
// 3. KeyValue
|
|
1993
|
+
//
|
|
1994
|
+
// key = value
|
|
1995
|
+
// ^-^ key
|
|
1996
|
+
// ^ equals
|
|
1997
|
+
// ^---^ value
|
|
1998
|
+
// Match the more helpful diagnostic when users write `key: value`.
|
|
1999
|
+
// Depending on tokenization, the ':' may be attached to the key token (e.g. 'name:').
|
|
2000
|
+
const rawKeyToken = cursor.value.raw;
|
|
2001
|
+
if (rawKeyToken.endsWith(':')) {
|
|
2002
|
+
throw new ParseError(input, { line: cursor.value.loc.start.line, column: cursor.value.loc.start.column + [...rawKeyToken].length - 1 }, `Use '=' to separate keys and values, not ':'`);
|
|
2003
|
+
}
|
|
2004
|
+
// Validate that multiline strings are not used as keys
|
|
2005
|
+
if (rawKeyToken.startsWith('"""') || rawKeyToken.startsWith("'''")) {
|
|
2006
|
+
throw new ParseError(input, cursor.value.loc.start, 'Multiline strings (""" or \'\'\') cannot be used as keys');
|
|
2007
|
+
}
|
|
2008
|
+
// Validate bare key characters (TOML 1.1.0: A-Za-z0-9_- only)
|
|
2009
|
+
const isQuotedKey = rawKeyToken.startsWith('"') || rawKeyToken.startsWith("'");
|
|
2010
|
+
if (!isQuotedKey) {
|
|
2011
|
+
for (let i = 0; i < rawKeyToken.length; i++) {
|
|
2012
|
+
const char = rawKeyToken[i];
|
|
2013
|
+
if (!/[A-Za-z0-9_-]/.test(char)) {
|
|
2014
|
+
throw new ParseError(input, { line: cursor.value.loc.start.line, column: cursor.value.loc.start.column + i }, `Invalid character '${char}' in bare key. Bare keys can only contain A-Z, a-z, 0-9, _, and -`);
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
let keyValue2;
|
|
2019
|
+
try {
|
|
2020
|
+
keyValue2 = [parseString(cursor.value.raw)];
|
|
2021
|
+
}
|
|
2022
|
+
catch (err) {
|
|
2023
|
+
const e = err;
|
|
2024
|
+
throw new ParseError(input, cursor.value.loc.start, e.message);
|
|
2025
|
+
}
|
|
2026
|
+
const key = {
|
|
2027
|
+
type: NodeType.Key,
|
|
2028
|
+
loc: cloneLocation(cursor.value.loc),
|
|
2029
|
+
raw: cursor.value.raw,
|
|
2030
|
+
value: keyValue2
|
|
2031
|
+
};
|
|
2032
|
+
while (!cursor.peek().done && cursor.peek().value.type === TokenType.Dot) {
|
|
2033
|
+
cursor.next();
|
|
2034
|
+
cursor.next();
|
|
2035
|
+
// Validate each part of a dotted key
|
|
2036
|
+
const partRaw = cursor.value.raw;
|
|
2037
|
+
if (partRaw.startsWith('"""') || partRaw.startsWith("'''")) {
|
|
2038
|
+
throw new ParseError(input, cursor.value.loc.start, 'Multiline strings (""" or \'\'\') cannot be used as keys');
|
|
2039
|
+
}
|
|
2040
|
+
const partIsQuoted = partRaw.startsWith('"') || partRaw.startsWith("'");
|
|
2041
|
+
if (!partIsQuoted) {
|
|
2042
|
+
for (let i = 0; i < partRaw.length; i++) {
|
|
2043
|
+
const char = partRaw[i];
|
|
2044
|
+
if (!/[A-Za-z0-9_-]/.test(char)) {
|
|
2045
|
+
throw new ParseError(input, { line: cursor.value.loc.start.line, column: cursor.value.loc.start.column + i }, `Invalid character '${char}' in bare key. Bare keys can only contain A-Z, a-z, 0-9, _, and -`);
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
key.loc.end = cursor.value.loc.end;
|
|
2050
|
+
key.raw += `.${cursor.value.raw}`;
|
|
2051
|
+
try {
|
|
2052
|
+
key.value.push(parseString(cursor.value.raw));
|
|
2053
|
+
}
|
|
2054
|
+
catch (err) {
|
|
2055
|
+
const e = err;
|
|
2056
|
+
throw new ParseError(input, cursor.value.loc.start, e.message);
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2059
|
+
cursor.next();
|
|
2060
|
+
// TOML key/value pairs must include '=' on the same line as the key.
|
|
2061
|
+
// Example invalid TOML (spec: bare-key-2):
|
|
2062
|
+
// barekey\n = 123
|
|
2063
|
+
if (!cursor.done && cursor.value.loc.start.line !== key.loc.end.line) {
|
|
2064
|
+
throw new ParseError(input, cursor.value.loc.start, `Expected "=" for key-value on the same line as the key`);
|
|
2065
|
+
}
|
|
2066
|
+
if (cursor.done || cursor.value.type !== TokenType.Equal) {
|
|
2067
|
+
if (!cursor.done && cursor.value.raw === ':') {
|
|
2068
|
+
throw new ParseError(input, cursor.value.loc.start, `Use '=' to separate keys and values, not ':'`);
|
|
2069
|
+
}
|
|
2070
|
+
throw new ParseError(input, cursor.done ? key.loc.end : cursor.value.loc.start, `Expected "=" for key-value`);
|
|
2071
|
+
}
|
|
2072
|
+
const equals = cursor.value.loc.start.column;
|
|
2073
|
+
const equalsLine = cursor.value.loc.start.line;
|
|
2074
|
+
cursor.next();
|
|
2075
|
+
if (cursor.done) {
|
|
2076
|
+
throw new ParseError(input, key.loc.start, `Expected value for key-value, reached end of file`);
|
|
2077
|
+
}
|
|
2078
|
+
// TOML values must be on the same line as the '=' sign.
|
|
2079
|
+
// Example invalid TOML (key/newline-06):
|
|
2080
|
+
// key =\n1
|
|
2081
|
+
if (cursor.value.loc.start.line !== equalsLine) {
|
|
2082
|
+
throw new ParseError(input, cursor.value.loc.start, `Expected value on the same line as the '=' sign`);
|
|
2083
|
+
}
|
|
2084
|
+
if (cursor.done) {
|
|
2085
|
+
throw new ParseError(input, key.loc.start, `Expected value for key-value`);
|
|
2086
|
+
}
|
|
2087
|
+
const results = walkValue$1(cursor, input);
|
|
2088
|
+
const value = results[0];
|
|
2089
|
+
const comments = results.slice(1);
|
|
2090
|
+
// Key/value pairs must be separated by a newline (or EOF). Whitespace alone isn't enough.
|
|
2091
|
+
// Example invalid TOML: first = "Tom" last = "Preston-Werner"
|
|
2092
|
+
//
|
|
2093
|
+
// Note: don't reject valid inline-tables like { a = 1, b = 2 } where tokens like ',' or '}'
|
|
2094
|
+
// legitimately follow a value on the same line.
|
|
2095
|
+
if (!cursor.peek().done) {
|
|
2096
|
+
const nextToken = cursor.peek().value;
|
|
2097
|
+
// Check for Dot token after a numeric value (likely multiple decimal points)
|
|
2098
|
+
if (nextToken.type === TokenType.Dot &&
|
|
2099
|
+
nextToken.loc.start.line === value.loc.end.line &&
|
|
2100
|
+
(value.type === NodeType.Float || value.type === NodeType.Integer)) {
|
|
2101
|
+
throw new ParseError(input, nextToken.loc.start, 'Invalid number: multiple decimal points not allowed');
|
|
2102
|
+
}
|
|
2103
|
+
const startsNewStatement = nextToken.type === TokenType.Literal ||
|
|
2104
|
+
nextToken.type === TokenType.Bracket;
|
|
2105
|
+
if (startsNewStatement && nextToken.loc.start.line === value.loc.end.line) {
|
|
2106
|
+
throw new ParseError(input, nextToken.loc.start, 'Key/value pairs must be separated by a newline');
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
2109
|
+
return [
|
|
2110
|
+
{
|
|
2111
|
+
type: NodeType.KeyValue,
|
|
2112
|
+
key,
|
|
2113
|
+
value: value,
|
|
2114
|
+
loc: { start: clonePosition(key.loc.start), end: clonePosition(value.loc.end) },
|
|
2115
|
+
equals
|
|
2116
|
+
},
|
|
2117
|
+
...comments
|
|
2118
|
+
];
|
|
2119
|
+
}
|
|
2120
|
+
function walkValue$1(cursor, input) {
|
|
2121
|
+
if (cursor.value.type === TokenType.Literal) {
|
|
2122
|
+
const raw = cursor.value.raw;
|
|
2123
|
+
if (raw[0] === DOUBLE_QUOTE || raw[0] === SINGLE_QUOTE) {
|
|
2124
|
+
return [string(cursor, input)];
|
|
2125
|
+
}
|
|
2126
|
+
else if (raw === TRUE || raw === FALSE) {
|
|
2127
|
+
return [boolean(cursor)];
|
|
2128
|
+
// Route anything that looks like a date or time through datetime() so invalid formats throw,
|
|
2129
|
+
// instead of being mis-parsed as integers (e.g., "199-09-09" -> 199).
|
|
2130
|
+
}
|
|
2131
|
+
else if (/^\d/.test(raw) &&
|
|
2132
|
+
(/^\d{1,}-\d{1,}/.test(raw) || /^\d{1,}:\d{1,}/.test(raw))) {
|
|
2133
|
+
return [datetime(cursor, input)];
|
|
2134
|
+
}
|
|
2135
|
+
else if ((!cursor.peek().done && cursor.peek().value.type === TokenType.Dot) ||
|
|
2136
|
+
IS_INF.test(raw) ||
|
|
2137
|
+
IS_NAN.test(raw) ||
|
|
2138
|
+
(HAS_E.test(raw) && !IS_HEX.test(raw))) {
|
|
2139
|
+
return [float(cursor, input)];
|
|
2140
|
+
}
|
|
2141
|
+
else {
|
|
2142
|
+
return [integer(cursor, input)];
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
else if (cursor.value.type === TokenType.Curly) {
|
|
2146
|
+
const [inline_table, comments] = inlineTable(cursor, input);
|
|
2147
|
+
return [inline_table, ...comments];
|
|
2148
|
+
}
|
|
2149
|
+
else if (cursor.value.type === TokenType.Bracket) {
|
|
2150
|
+
const [inline_array, comments] = inlineArray(cursor, input);
|
|
2151
|
+
return [inline_array, ...comments];
|
|
2152
|
+
}
|
|
2153
|
+
else if (cursor.value.type === TokenType.Dot) {
|
|
2154
|
+
throw new ParseError(input, cursor.value.loc.start, `Invalid number: cannot start with a dot. Numbers must start with a digit`);
|
|
2155
|
+
}
|
|
2156
|
+
else {
|
|
2157
|
+
throw new ParseError(input, cursor.value.loc.start, `Unrecognized token type`);
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
1340
2160
|
function inlineTable(cursor, input) {
|
|
1341
2161
|
if (cursor.value.raw !== '{') {
|
|
1342
|
-
throw new ParseError(input, cursor.value.loc.start, `Expected "{" for inline table
|
|
2162
|
+
throw new ParseError(input, cursor.value.loc.start, `Expected "{" for inline table`);
|
|
1343
2163
|
}
|
|
1344
2164
|
// 6. InlineTable
|
|
1345
2165
|
const value = {
|
|
@@ -1347,51 +2167,66 @@ function inlineTable(cursor, input) {
|
|
|
1347
2167
|
loc: cloneLocation(cursor.value.loc),
|
|
1348
2168
|
items: []
|
|
1349
2169
|
};
|
|
2170
|
+
const comments = [];
|
|
1350
2171
|
cursor.next();
|
|
1351
2172
|
while (!cursor.done &&
|
|
1352
2173
|
!(cursor.value.type === TokenType.Curly && cursor.value.raw === '}')) {
|
|
2174
|
+
if (cursor.value.type === TokenType.Comment) {
|
|
2175
|
+
comments.push(comment(cursor));
|
|
2176
|
+
cursor.next();
|
|
2177
|
+
continue;
|
|
2178
|
+
}
|
|
1353
2179
|
if (cursor.value.type === TokenType.Comma) {
|
|
1354
2180
|
const previous = value.items[value.items.length - 1];
|
|
1355
2181
|
if (!previous) {
|
|
1356
2182
|
throw new ParseError(input, cursor.value.loc.start, 'Found "," without previous value in inline table');
|
|
1357
2183
|
}
|
|
2184
|
+
if (previous.comma) {
|
|
2185
|
+
throw new ParseError(input, cursor.value.loc.start, 'Found consecutive commas in inline table (double comma is not allowed)');
|
|
2186
|
+
}
|
|
1358
2187
|
previous.comma = true;
|
|
1359
2188
|
previous.loc.end = cursor.value.loc.start;
|
|
1360
2189
|
cursor.next();
|
|
1361
2190
|
continue;
|
|
1362
2191
|
}
|
|
1363
|
-
const
|
|
1364
|
-
if (
|
|
1365
|
-
throw new ParseError(input, cursor.value.loc.start,
|
|
2192
|
+
const previous = value.items[value.items.length - 1];
|
|
2193
|
+
if (previous && !previous.comma) {
|
|
2194
|
+
throw new ParseError(input, cursor.value.loc.start, 'Missing comma between inline table items');
|
|
2195
|
+
}
|
|
2196
|
+
// Recursively parse the key-value, but without generators
|
|
2197
|
+
const blocks = walkBlock(cursor, input);
|
|
2198
|
+
const item = blocks[0];
|
|
2199
|
+
const additional_comments = blocks.slice(1);
|
|
2200
|
+
if (item.type === NodeType.KeyValue) {
|
|
2201
|
+
value.items.push({
|
|
2202
|
+
type: NodeType.InlineItem,
|
|
2203
|
+
loc: cloneLocation(item.loc),
|
|
2204
|
+
item: item,
|
|
2205
|
+
comma: false
|
|
2206
|
+
});
|
|
2207
|
+
merge(comments, additional_comments);
|
|
1366
2208
|
}
|
|
1367
|
-
const inline_item = {
|
|
1368
|
-
type: NodeType.InlineItem,
|
|
1369
|
-
loc: cloneLocation(item.loc),
|
|
1370
|
-
item,
|
|
1371
|
-
comma: false
|
|
1372
|
-
};
|
|
1373
|
-
value.items.push(inline_item);
|
|
1374
2209
|
cursor.next();
|
|
1375
2210
|
}
|
|
1376
2211
|
if (cursor.done ||
|
|
1377
2212
|
cursor.value.type !== TokenType.Curly ||
|
|
1378
2213
|
cursor.value.raw !== '}') {
|
|
1379
|
-
throw new ParseError(input, cursor.done ? value.loc.start : cursor.value.loc.start, `Expected "}"
|
|
2214
|
+
throw new ParseError(input, cursor.done ? value.loc.start : cursor.value.loc.start, `Expected "}"`);
|
|
1380
2215
|
}
|
|
1381
2216
|
value.loc.end = cursor.value.loc.end;
|
|
1382
|
-
return value;
|
|
2217
|
+
return [value, comments];
|
|
1383
2218
|
}
|
|
1384
2219
|
function inlineArray(cursor, input) {
|
|
1385
2220
|
// 7. InlineArray
|
|
1386
2221
|
if (cursor.value.raw !== '[') {
|
|
1387
|
-
throw new ParseError(input, cursor.value.loc.start, `Expected "[" for inline array
|
|
2222
|
+
throw new ParseError(input, cursor.value.loc.start, `Expected "[" for inline array`);
|
|
1388
2223
|
}
|
|
1389
2224
|
const value = {
|
|
1390
2225
|
type: NodeType.InlineArray,
|
|
1391
2226
|
loc: cloneLocation(cursor.value.loc),
|
|
1392
2227
|
items: []
|
|
1393
2228
|
};
|
|
1394
|
-
|
|
2229
|
+
const comments = [];
|
|
1395
2230
|
cursor.next();
|
|
1396
2231
|
while (!cursor.done &&
|
|
1397
2232
|
!(cursor.value.type === TokenType.Bracket && cursor.value.raw === ']')) {
|
|
@@ -1400,6 +2235,9 @@ function inlineArray(cursor, input) {
|
|
|
1400
2235
|
if (!previous) {
|
|
1401
2236
|
throw new ParseError(input, cursor.value.loc.start, 'Found "," without previous value for inline array');
|
|
1402
2237
|
}
|
|
2238
|
+
if (previous.comma) {
|
|
2239
|
+
throw new ParseError(input, cursor.value.loc.start, 'Found consecutive commas in array (double comma is not allowed)');
|
|
2240
|
+
}
|
|
1403
2241
|
previous.comma = true;
|
|
1404
2242
|
previous.loc.end = cursor.value.loc.start;
|
|
1405
2243
|
}
|
|
@@ -1407,14 +2245,19 @@ function inlineArray(cursor, input) {
|
|
|
1407
2245
|
comments.push(comment(cursor));
|
|
1408
2246
|
}
|
|
1409
2247
|
else {
|
|
1410
|
-
const [
|
|
1411
|
-
|
|
2248
|
+
const previous = value.items[value.items.length - 1];
|
|
2249
|
+
if (previous && !previous.comma) {
|
|
2250
|
+
throw new ParseError(input, cursor.value.loc.start, 'Missing comma between array elements');
|
|
2251
|
+
}
|
|
2252
|
+
const results = walkValue$1(cursor, input);
|
|
2253
|
+
const item = results[0];
|
|
2254
|
+
const additional_comments = results.slice(1);
|
|
2255
|
+
value.items.push({
|
|
1412
2256
|
type: NodeType.InlineItem,
|
|
1413
2257
|
loc: cloneLocation(item.loc),
|
|
1414
2258
|
item,
|
|
1415
2259
|
comma: false
|
|
1416
|
-
};
|
|
1417
|
-
value.items.push(inline_item);
|
|
2260
|
+
});
|
|
1418
2261
|
merge(comments, additional_comments);
|
|
1419
2262
|
}
|
|
1420
2263
|
cursor.next();
|
|
@@ -1422,7 +2265,7 @@ function inlineArray(cursor, input) {
|
|
|
1422
2265
|
if (cursor.done ||
|
|
1423
2266
|
cursor.value.type !== TokenType.Bracket ||
|
|
1424
2267
|
cursor.value.raw !== ']') {
|
|
1425
|
-
throw new ParseError(input, cursor.done ? value.loc.start : cursor.value.loc.start, `Expected "]"
|
|
2268
|
+
throw new ParseError(input, cursor.done ? value.loc.start : cursor.value.loc.start, `Expected "]"`);
|
|
1426
2269
|
}
|
|
1427
2270
|
value.loc.end = cursor.value.loc.end;
|
|
1428
2271
|
return [value, comments];
|
|
@@ -2272,6 +3115,7 @@ const DEFAULT_TRAILING_COMMA = false;
|
|
|
2272
3115
|
const DEFAULT_BRACKET_SPACING = true;
|
|
2273
3116
|
const DEFAULT_INLINE_TABLE_START = 1;
|
|
2274
3117
|
const DEFAULT_TRUNCATE_ZERO_TIME_IN_DATES = false;
|
|
3118
|
+
const DEFAULT_USE_TABS_FOR_INDENTATION = false;
|
|
2275
3119
|
// Detects if trailing commas are used in the existing TOML by examining the AST
|
|
2276
3120
|
// Returns true if trailing commas are used, false if not or comma-separated structures found (ie. default to false)
|
|
2277
3121
|
function detectTrailingComma(ast) {
|
|
@@ -2465,6 +3309,30 @@ function countTrailingNewlines(str, newlineChar) {
|
|
|
2465
3309
|
}
|
|
2466
3310
|
return count;
|
|
2467
3311
|
}
|
|
3312
|
+
// Detects if tabs are used for indentation by checking the first few indented lines
|
|
3313
|
+
function detectTabsForIndentation(str) {
|
|
3314
|
+
const lines = str.split(/\r?\n/);
|
|
3315
|
+
let tabCount = 0;
|
|
3316
|
+
let spaceCount = 0;
|
|
3317
|
+
for (const line of lines) {
|
|
3318
|
+
// Skip empty lines
|
|
3319
|
+
if (line.length === 0)
|
|
3320
|
+
continue;
|
|
3321
|
+
// Check the first character of non-empty lines
|
|
3322
|
+
if (line[0] === '\t') {
|
|
3323
|
+
tabCount++;
|
|
3324
|
+
}
|
|
3325
|
+
else if (line[0] === ' ') {
|
|
3326
|
+
spaceCount++;
|
|
3327
|
+
}
|
|
3328
|
+
// If we've seen enough evidence, make a decision
|
|
3329
|
+
if (tabCount + spaceCount >= 5) {
|
|
3330
|
+
break;
|
|
3331
|
+
}
|
|
3332
|
+
}
|
|
3333
|
+
// Prefer tabs if we see more tabs than spaces
|
|
3334
|
+
return tabCount > spaceCount;
|
|
3335
|
+
}
|
|
2468
3336
|
/**
|
|
2469
3337
|
* Validates a format object and warns about unsupported properties.
|
|
2470
3338
|
* Throws errors for supported properties with invalid types.
|
|
@@ -2475,61 +3343,62 @@ function validateFormatObject(format) {
|
|
|
2475
3343
|
if (!format || typeof format !== 'object') {
|
|
2476
3344
|
return {};
|
|
2477
3345
|
}
|
|
2478
|
-
const supportedProperties = new Set(['newLine', 'trailingNewline', 'trailingComma', 'bracketSpacing', 'inlineTableStart', 'truncateZeroTimeInDates']);
|
|
3346
|
+
const supportedProperties = new Set(['newLine', 'trailingNewline', 'trailingComma', 'bracketSpacing', 'inlineTableStart', 'truncateZeroTimeInDates', 'useTabsForIndentation']);
|
|
2479
3347
|
const validatedFormat = {};
|
|
2480
3348
|
const unsupportedProperties = [];
|
|
2481
3349
|
const invalidTypeProperties = [];
|
|
2482
|
-
// Check all enumerable properties of the format object
|
|
3350
|
+
// Check all enumerable properties of the format object, including properties
|
|
3351
|
+
// provided via the prototype chain (common in JS Object.create(...) patterns).
|
|
2483
3352
|
for (const key in format) {
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
}
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
}
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
}
|
|
2529
|
-
else {
|
|
2530
|
-
unsupportedProperties.push(key);
|
|
3353
|
+
const isOwnEnumerable = Object.prototype.hasOwnProperty.call(format, key);
|
|
3354
|
+
if (supportedProperties.has(key)) {
|
|
3355
|
+
const value = format[key];
|
|
3356
|
+
// Type validation for each property
|
|
3357
|
+
switch (key) {
|
|
3358
|
+
case 'newLine':
|
|
3359
|
+
if (typeof value === 'string') {
|
|
3360
|
+
validatedFormat.newLine = value;
|
|
3361
|
+
}
|
|
3362
|
+
else {
|
|
3363
|
+
invalidTypeProperties.push(`${key} (expected string, got ${typeof value})`);
|
|
3364
|
+
}
|
|
3365
|
+
break;
|
|
3366
|
+
case 'trailingNewline':
|
|
3367
|
+
if (typeof value === 'boolean' || typeof value === 'number') {
|
|
3368
|
+
validatedFormat.trailingNewline = value;
|
|
3369
|
+
}
|
|
3370
|
+
else {
|
|
3371
|
+
invalidTypeProperties.push(`${key} (expected boolean or number, got ${typeof value})`);
|
|
3372
|
+
}
|
|
3373
|
+
break;
|
|
3374
|
+
case 'trailingComma':
|
|
3375
|
+
case 'bracketSpacing':
|
|
3376
|
+
case 'truncateZeroTimeInDates':
|
|
3377
|
+
case 'useTabsForIndentation':
|
|
3378
|
+
if (typeof value === 'boolean') {
|
|
3379
|
+
validatedFormat[key] = value;
|
|
3380
|
+
}
|
|
3381
|
+
else {
|
|
3382
|
+
invalidTypeProperties.push(`${key} (expected boolean, got ${typeof value})`);
|
|
3383
|
+
}
|
|
3384
|
+
break;
|
|
3385
|
+
case 'inlineTableStart':
|
|
3386
|
+
if (typeof value === 'number' && Number.isInteger(value) && value >= 0) {
|
|
3387
|
+
validatedFormat.inlineTableStart = value;
|
|
3388
|
+
}
|
|
3389
|
+
else if (value === undefined || value === null) {
|
|
3390
|
+
// Allow undefined/null to use default
|
|
3391
|
+
validatedFormat.inlineTableStart = value;
|
|
3392
|
+
}
|
|
3393
|
+
else {
|
|
3394
|
+
invalidTypeProperties.push(`${key} (expected non-negative integer or undefined, got ${typeof value})`);
|
|
3395
|
+
}
|
|
3396
|
+
break;
|
|
2531
3397
|
}
|
|
2532
3398
|
}
|
|
3399
|
+
else if (isOwnEnumerable) {
|
|
3400
|
+
unsupportedProperties.push(key);
|
|
3401
|
+
}
|
|
2533
3402
|
}
|
|
2534
3403
|
// Warn about unsupported properties
|
|
2535
3404
|
if (unsupportedProperties.length > 0) {
|
|
@@ -2550,7 +3419,7 @@ function validateFormatObject(format) {
|
|
|
2550
3419
|
* @returns A resolved TomlFormat instance
|
|
2551
3420
|
*/
|
|
2552
3421
|
function resolveTomlFormat(format, fallbackFormat) {
|
|
2553
|
-
var _a, _b, _c, _d, _e;
|
|
3422
|
+
var _a, _b, _c, _d, _e, _f;
|
|
2554
3423
|
if (format) {
|
|
2555
3424
|
// If format is provided, validate and merge it with fallback
|
|
2556
3425
|
if (format instanceof TomlFormat) {
|
|
@@ -2560,7 +3429,7 @@ function resolveTomlFormat(format, fallbackFormat) {
|
|
|
2560
3429
|
// Validate the format object and warn about unsupported properties
|
|
2561
3430
|
const validatedFormat = validateFormatObject(format);
|
|
2562
3431
|
// Create a new TomlFormat instance with validated properties
|
|
2563
|
-
return new TomlFormat((_a = validatedFormat.newLine) !== null && _a !== void 0 ? _a : fallbackFormat.newLine, (_b = validatedFormat.trailingNewline) !== null && _b !== void 0 ? _b : fallbackFormat.trailingNewline, (_c = validatedFormat.trailingComma) !== null && _c !== void 0 ? _c : fallbackFormat.trailingComma, (_d = validatedFormat.bracketSpacing) !== null && _d !== void 0 ? _d : fallbackFormat.bracketSpacing, validatedFormat.inlineTableStart !== undefined ? validatedFormat.inlineTableStart : fallbackFormat.inlineTableStart, (_e = validatedFormat.truncateZeroTimeInDates) !== null && _e !== void 0 ? _e : fallbackFormat.truncateZeroTimeInDates);
|
|
3432
|
+
return new TomlFormat((_a = validatedFormat.newLine) !== null && _a !== void 0 ? _a : fallbackFormat.newLine, (_b = validatedFormat.trailingNewline) !== null && _b !== void 0 ? _b : fallbackFormat.trailingNewline, (_c = validatedFormat.trailingComma) !== null && _c !== void 0 ? _c : fallbackFormat.trailingComma, (_d = validatedFormat.bracketSpacing) !== null && _d !== void 0 ? _d : fallbackFormat.bracketSpacing, validatedFormat.inlineTableStart !== undefined ? validatedFormat.inlineTableStart : fallbackFormat.inlineTableStart, (_e = validatedFormat.truncateZeroTimeInDates) !== null && _e !== void 0 ? _e : fallbackFormat.truncateZeroTimeInDates, (_f = validatedFormat.useTabsForIndentation) !== null && _f !== void 0 ? _f : fallbackFormat.useTabsForIndentation);
|
|
2564
3433
|
}
|
|
2565
3434
|
}
|
|
2566
3435
|
else {
|
|
@@ -2572,8 +3441,7 @@ class TomlFormat {
|
|
|
2572
3441
|
// These options were part of the original TimHall's version and are not yet implemented
|
|
2573
3442
|
//printWidth?: number;
|
|
2574
3443
|
//tabWidth?: number;
|
|
2575
|
-
|
|
2576
|
-
constructor(newLine, trailingNewline, trailingComma, bracketSpacing, inlineTableStart, truncateZeroTimeInDates) {
|
|
3444
|
+
constructor(newLine, trailingNewline, trailingComma, bracketSpacing, inlineTableStart, truncateZeroTimeInDates, useTabsForIndentation) {
|
|
2577
3445
|
// Use provided values or fall back to defaults
|
|
2578
3446
|
this.newLine = newLine !== null && newLine !== void 0 ? newLine : DEFAULT_NEWLINE;
|
|
2579
3447
|
this.trailingNewline = trailingNewline !== null && trailingNewline !== void 0 ? trailingNewline : DEFAULT_TRAILING_NEWLINE;
|
|
@@ -2581,6 +3449,7 @@ class TomlFormat {
|
|
|
2581
3449
|
this.bracketSpacing = bracketSpacing !== null && bracketSpacing !== void 0 ? bracketSpacing : DEFAULT_BRACKET_SPACING;
|
|
2582
3450
|
this.inlineTableStart = inlineTableStart !== null && inlineTableStart !== void 0 ? inlineTableStart : DEFAULT_INLINE_TABLE_START;
|
|
2583
3451
|
this.truncateZeroTimeInDates = truncateZeroTimeInDates !== null && truncateZeroTimeInDates !== void 0 ? truncateZeroTimeInDates : DEFAULT_TRUNCATE_ZERO_TIME_IN_DATES;
|
|
3452
|
+
this.useTabsForIndentation = useTabsForIndentation !== null && useTabsForIndentation !== void 0 ? useTabsForIndentation : DEFAULT_USE_TABS_FOR_INDENTATION;
|
|
2584
3453
|
}
|
|
2585
3454
|
/**
|
|
2586
3455
|
* Creates a new TomlFormat instance with default formatting preferences.
|
|
@@ -2594,7 +3463,7 @@ class TomlFormat {
|
|
|
2594
3463
|
* - truncateZeroTimeInDates: false
|
|
2595
3464
|
*/
|
|
2596
3465
|
static default() {
|
|
2597
|
-
return new TomlFormat(DEFAULT_NEWLINE, DEFAULT_TRAILING_NEWLINE, DEFAULT_TRAILING_COMMA, DEFAULT_BRACKET_SPACING, DEFAULT_INLINE_TABLE_START, DEFAULT_TRUNCATE_ZERO_TIME_IN_DATES);
|
|
3466
|
+
return new TomlFormat(DEFAULT_NEWLINE, DEFAULT_TRAILING_NEWLINE, DEFAULT_TRAILING_COMMA, DEFAULT_BRACKET_SPACING, DEFAULT_INLINE_TABLE_START, DEFAULT_TRUNCATE_ZERO_TIME_IN_DATES, DEFAULT_USE_TABS_FOR_INDENTATION);
|
|
2598
3467
|
}
|
|
2599
3468
|
/**
|
|
2600
3469
|
* Auto-detects formatting preferences from an existing TOML string.
|
|
@@ -2634,6 +3503,8 @@ class TomlFormat {
|
|
|
2634
3503
|
format.trailingComma = DEFAULT_TRAILING_COMMA;
|
|
2635
3504
|
format.bracketSpacing = DEFAULT_BRACKET_SPACING;
|
|
2636
3505
|
}
|
|
3506
|
+
// Detect if tabs are used for indentation
|
|
3507
|
+
format.useTabsForIndentation = detectTabsForIndentation(tomlString);
|
|
2637
3508
|
// inlineTableStart uses default value since auto-detection would require
|
|
2638
3509
|
// complex analysis of nested table formatting preferences
|
|
2639
3510
|
format.inlineTableStart = DEFAULT_INLINE_TABLE_START;
|
|
@@ -2963,71 +3834,85 @@ const BY_NEW_LINE = /(\r\n|\n)/g;
|
|
|
2963
3834
|
* It preserves the original formatting, spacing, and structure of the TOML file.
|
|
2964
3835
|
*
|
|
2965
3836
|
* @param ast - The Abstract Syntax Tree representing the parsed TOML document
|
|
2966
|
-
* @param
|
|
2967
|
-
* @param options - Optional configuration object
|
|
2968
|
-
* @param options.trailingNewline - Number of trailing newlines to add (1 by default)
|
|
3837
|
+
* @param format - The formatting options to use for the output
|
|
2969
3838
|
* @returns The reconstructed TOML document as a string
|
|
2970
3839
|
*
|
|
2971
3840
|
* @example
|
|
2972
3841
|
* ```typescript
|
|
2973
|
-
* const tomlString = toTOML(ast,
|
|
3842
|
+
* const tomlString = toTOML(ast, TomlFormat.default());
|
|
2974
3843
|
* ```
|
|
2975
3844
|
*/
|
|
2976
3845
|
function toTOML(ast, format) {
|
|
2977
3846
|
const lines = [];
|
|
3847
|
+
const paddingChar = format.useTabsForIndentation ? '\t' : SPACE;
|
|
2978
3848
|
traverse(ast, {
|
|
2979
3849
|
[NodeType.TableKey](node) {
|
|
2980
3850
|
const { start, end } = node.loc;
|
|
2981
|
-
write(lines, { start, end: { line: start.line, column: start.column + 1 } }, '[');
|
|
2982
|
-
write(lines, { start: { line: end.line, column: end.column - 1 }, end }, ']');
|
|
3851
|
+
write(lines, { start, end: { line: start.line, column: start.column + 1 } }, '[', paddingChar);
|
|
3852
|
+
write(lines, { start: { line: end.line, column: end.column - 1 }, end }, ']', paddingChar);
|
|
2983
3853
|
},
|
|
2984
3854
|
[NodeType.TableArrayKey](node) {
|
|
2985
3855
|
const { start, end } = node.loc;
|
|
2986
|
-
write(lines, { start, end: { line: start.line, column: start.column + 2 } }, '[[');
|
|
2987
|
-
write(lines, { start: { line: end.line, column: end.column - 2 }, end }, ']]');
|
|
3856
|
+
write(lines, { start, end: { line: start.line, column: start.column + 2 } }, '[[', paddingChar);
|
|
3857
|
+
write(lines, { start: { line: end.line, column: end.column - 2 }, end }, ']]', paddingChar);
|
|
2988
3858
|
},
|
|
2989
3859
|
[NodeType.KeyValue](node) {
|
|
2990
3860
|
const { start: { line } } = node.loc;
|
|
2991
|
-
write(lines, { start: { line, column: node.equals }, end: { line, column: node.equals + 1 } }, '=');
|
|
3861
|
+
write(lines, { start: { line, column: node.equals }, end: { line, column: node.equals + 1 } }, '=', paddingChar);
|
|
2992
3862
|
},
|
|
2993
3863
|
[NodeType.Key](node) {
|
|
2994
|
-
write(lines, node.loc, node.raw);
|
|
3864
|
+
write(lines, node.loc, node.raw, paddingChar);
|
|
2995
3865
|
},
|
|
2996
3866
|
[NodeType.String](node) {
|
|
2997
|
-
write(lines, node.loc, node.raw);
|
|
3867
|
+
write(lines, node.loc, node.raw, paddingChar);
|
|
2998
3868
|
},
|
|
2999
3869
|
[NodeType.Integer](node) {
|
|
3000
|
-
write(lines, node.loc, node.raw);
|
|
3870
|
+
write(lines, node.loc, node.raw, paddingChar);
|
|
3001
3871
|
},
|
|
3002
3872
|
[NodeType.Float](node) {
|
|
3003
|
-
write(lines, node.loc, node.raw);
|
|
3873
|
+
write(lines, node.loc, node.raw, paddingChar);
|
|
3004
3874
|
},
|
|
3005
3875
|
[NodeType.Boolean](node) {
|
|
3006
|
-
write(lines, node.loc, node.value.toString());
|
|
3876
|
+
write(lines, node.loc, node.value.toString(), paddingChar);
|
|
3007
3877
|
},
|
|
3008
3878
|
[NodeType.DateTime](node) {
|
|
3009
|
-
write(lines, node.loc, node.raw);
|
|
3879
|
+
write(lines, node.loc, node.raw, paddingChar);
|
|
3010
3880
|
},
|
|
3011
3881
|
[NodeType.InlineArray](node) {
|
|
3012
3882
|
const { start, end } = node.loc;
|
|
3013
|
-
write(lines, { start, end: { line: start.line, column: start.column + 1 } }, '[');
|
|
3014
|
-
write(lines, { start: { line: end.line, column: end.column - 1 }, end }, ']');
|
|
3883
|
+
write(lines, { start, end: { line: start.line, column: start.column + 1 } }, '[', paddingChar);
|
|
3884
|
+
write(lines, { start: { line: end.line, column: end.column - 1 }, end }, ']', paddingChar);
|
|
3015
3885
|
},
|
|
3016
3886
|
[NodeType.InlineTable](node) {
|
|
3017
3887
|
const { start, end } = node.loc;
|
|
3018
|
-
write(lines, { start, end: { line: start.line, column: start.column + 1 } }, '{');
|
|
3019
|
-
write(lines, { start: { line: end.line, column: end.column - 1 }, end }, '}');
|
|
3888
|
+
write(lines, { start, end: { line: start.line, column: start.column + 1 } }, '{', paddingChar);
|
|
3889
|
+
write(lines, { start: { line: end.line, column: end.column - 1 }, end }, '}', paddingChar);
|
|
3020
3890
|
},
|
|
3021
3891
|
[NodeType.InlineItem](node) {
|
|
3022
3892
|
if (!node.comma)
|
|
3023
3893
|
return;
|
|
3024
3894
|
const start = node.loc.end;
|
|
3025
|
-
write(lines, { start, end: { line: start.line, column: start.column + 1 } }, ',');
|
|
3895
|
+
write(lines, { start, end: { line: start.line, column: start.column + 1 } }, ',', paddingChar);
|
|
3026
3896
|
},
|
|
3027
3897
|
[NodeType.Comment](node) {
|
|
3028
|
-
write(lines, node.loc, node.raw);
|
|
3898
|
+
write(lines, node.loc, node.raw, paddingChar);
|
|
3029
3899
|
}
|
|
3030
3900
|
});
|
|
3901
|
+
// Post-process: convert leading spaces to tabs if useTabsForIndentation is enabled
|
|
3902
|
+
if (format.useTabsForIndentation) {
|
|
3903
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3904
|
+
const line = lines[i];
|
|
3905
|
+
// Find the leading whitespace
|
|
3906
|
+
const match = line.match(/^( +)/);
|
|
3907
|
+
if (match) {
|
|
3908
|
+
const leadingSpaces = match[1];
|
|
3909
|
+
// Replace entire leading space sequence with equivalent tabs
|
|
3910
|
+
// Each space becomes a tab (preserving the visual width)
|
|
3911
|
+
const leadingTabs = '\t'.repeat(leadingSpaces.length);
|
|
3912
|
+
lines[i] = leadingTabs + line.substring(leadingSpaces.length);
|
|
3913
|
+
}
|
|
3914
|
+
}
|
|
3915
|
+
}
|
|
3031
3916
|
return lines.join(format.newLine) + format.newLine.repeat(format.trailingNewline);
|
|
3032
3917
|
}
|
|
3033
3918
|
/**
|
|
@@ -3044,6 +3929,7 @@ function toTOML(ast, format) {
|
|
|
3044
3929
|
* - end: { line: number, column: number } - Ending position (1-indexed line, 0-indexed column)
|
|
3045
3930
|
* @param raw - The raw string content to write at the specified location.
|
|
3046
3931
|
* Can contain multiple lines separated by \n or \r\n.
|
|
3932
|
+
* @param paddingChar - The character to use for padding (space or tab)
|
|
3047
3933
|
*
|
|
3048
3934
|
* @throws {Error} When there's a mismatch between location span and raw string line count
|
|
3049
3935
|
* @throws {Error} When attempting to write to an uninitialized line
|
|
@@ -3052,11 +3938,11 @@ function toTOML(ast, format) {
|
|
|
3052
3938
|
* ```typescript
|
|
3053
3939
|
* const lines = ['', ''];
|
|
3054
3940
|
* const location = { start: { line: 1, column: 0 }, end: { line: 1, column: 3 } };
|
|
3055
|
-
* write(lines, location, 'key');
|
|
3941
|
+
* write(lines, location, 'key', ' ');
|
|
3056
3942
|
* // Result: lines[0] becomes 'key'
|
|
3057
3943
|
* ```
|
|
3058
3944
|
*/
|
|
3059
|
-
function write(lines, loc, raw) {
|
|
3945
|
+
function write(lines, loc, raw, paddingChar = SPACE) {
|
|
3060
3946
|
const raw_lines = raw.split(BY_NEW_LINE).filter(line => line !== '\n' && line !== '\r\n');
|
|
3061
3947
|
const expected_lines = loc.end.line - loc.start.line + 1;
|
|
3062
3948
|
if (raw_lines.length !== expected_lines) {
|
|
@@ -3070,10 +3956,19 @@ function write(lines, loc, raw) {
|
|
|
3070
3956
|
}
|
|
3071
3957
|
const is_start_line = i === loc.start.line;
|
|
3072
3958
|
const is_end_line = i === loc.end.line;
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3959
|
+
let before = '';
|
|
3960
|
+
if (is_start_line) {
|
|
3961
|
+
const existingBefore = line.substring(0, loc.start.column);
|
|
3962
|
+
if (existingBefore.length < loc.start.column) {
|
|
3963
|
+
// Need to pad - always use spaces during write phase
|
|
3964
|
+
// Tab conversion happens in post-processing for leading indentation only
|
|
3965
|
+
before = existingBefore.padEnd(loc.start.column, SPACE);
|
|
3966
|
+
}
|
|
3967
|
+
else {
|
|
3968
|
+
before = existingBefore;
|
|
3969
|
+
}
|
|
3970
|
+
}
|
|
3971
|
+
const after = is_end_line ? line.substring(loc.end.column) : '';
|
|
3077
3972
|
lines[i - 1] = before + raw_lines[i - loc.start.line] + after;
|
|
3078
3973
|
}
|
|
3079
3974
|
}
|
|
@@ -3105,6 +4000,21 @@ function getLine(lines, index) {
|
|
|
3105
4000
|
return lines[index - 1];
|
|
3106
4001
|
}
|
|
3107
4002
|
|
|
4003
|
+
/**
|
|
4004
|
+
* Recursively tracks all nested inline tables within an inline table.
|
|
4005
|
+
* This ensures that nested inline tables like { nest = {} } are also tracked as immutable.
|
|
4006
|
+
*/
|
|
4007
|
+
function trackNestedInlineTables(inlineTable, basePath, inlineTables) {
|
|
4008
|
+
for (const item of inlineTable.items) {
|
|
4009
|
+
const keyValue = item.item;
|
|
4010
|
+
const fullPath = basePath.concat(keyValue.key.value);
|
|
4011
|
+
if (keyValue.value.type === NodeType.InlineTable) {
|
|
4012
|
+
inlineTables.add(joinKey(fullPath));
|
|
4013
|
+
// Recursively track nested inline tables
|
|
4014
|
+
trackNestedInlineTables(keyValue.value, fullPath, inlineTables);
|
|
4015
|
+
}
|
|
4016
|
+
}
|
|
4017
|
+
}
|
|
3108
4018
|
/**
|
|
3109
4019
|
* Converts the given AST to a JavaScript object.
|
|
3110
4020
|
*
|
|
@@ -3117,13 +4027,16 @@ function toJS(ast, input = '') {
|
|
|
3117
4027
|
const tables = new Set();
|
|
3118
4028
|
const table_arrays = new Set();
|
|
3119
4029
|
const defined = new Set();
|
|
4030
|
+
const implicit_tables = new Set();
|
|
4031
|
+
const inline_tables = new Set();
|
|
3120
4032
|
let active = result;
|
|
3121
4033
|
let skip_depth = 0;
|
|
4034
|
+
let active_path = [];
|
|
3122
4035
|
traverse(ast, {
|
|
3123
4036
|
[NodeType.Table](node) {
|
|
3124
4037
|
const key = node.key.item.value;
|
|
3125
4038
|
try {
|
|
3126
|
-
validateKey(result, key, node.type, { tables, table_arrays, defined });
|
|
4039
|
+
validateKey(result, [], key, node.type, { tables, table_arrays, defined, implicit_tables, inline_tables });
|
|
3127
4040
|
}
|
|
3128
4041
|
catch (err) {
|
|
3129
4042
|
const e = err;
|
|
@@ -3133,11 +4046,12 @@ function toJS(ast, input = '') {
|
|
|
3133
4046
|
tables.add(joined_key);
|
|
3134
4047
|
defined.add(joined_key);
|
|
3135
4048
|
active = ensureTable(result, key);
|
|
4049
|
+
active_path = key;
|
|
3136
4050
|
},
|
|
3137
4051
|
[NodeType.TableArray](node) {
|
|
3138
4052
|
const key = node.key.item.value;
|
|
3139
4053
|
try {
|
|
3140
|
-
validateKey(result, key, node.type, { tables, table_arrays, defined });
|
|
4054
|
+
validateKey(result, [], key, node.type, { tables, table_arrays, defined, implicit_tables, inline_tables });
|
|
3141
4055
|
}
|
|
3142
4056
|
catch (err) {
|
|
3143
4057
|
const e = err;
|
|
@@ -3147,6 +4061,7 @@ function toJS(ast, input = '') {
|
|
|
3147
4061
|
table_arrays.add(joined_key);
|
|
3148
4062
|
defined.add(joined_key);
|
|
3149
4063
|
active = ensureTableArray(result, key);
|
|
4064
|
+
active_path = key;
|
|
3150
4065
|
},
|
|
3151
4066
|
[NodeType.KeyValue]: {
|
|
3152
4067
|
enter(node) {
|
|
@@ -3154,16 +4069,48 @@ function toJS(ast, input = '') {
|
|
|
3154
4069
|
return;
|
|
3155
4070
|
const key = node.key.value;
|
|
3156
4071
|
try {
|
|
3157
|
-
validateKey(active, key, node.type, {
|
|
4072
|
+
validateKey(active, active_path, key, node.type, {
|
|
4073
|
+
tables,
|
|
4074
|
+
table_arrays,
|
|
4075
|
+
defined,
|
|
4076
|
+
implicit_tables,
|
|
4077
|
+
inline_tables
|
|
4078
|
+
});
|
|
3158
4079
|
}
|
|
3159
4080
|
catch (err) {
|
|
3160
4081
|
const e = err;
|
|
3161
4082
|
throw new ParseError(input, node.key.loc.start, e.message);
|
|
3162
4083
|
}
|
|
3163
|
-
|
|
4084
|
+
// Track implicit tables created by dotted keys.
|
|
4085
|
+
// Example: within [fruit], `apple.color = ...` implicitly defines tables `fruit.apple`.
|
|
4086
|
+
// Example: `type.name = ...` implicitly defines table `product.type`.
|
|
4087
|
+
if (key.length > 1) {
|
|
4088
|
+
for (let i = 1; i < key.length; i++) {
|
|
4089
|
+
const implicit = joinKey(active_path.concat(key.slice(0, i)));
|
|
4090
|
+
implicit_tables.add(implicit);
|
|
4091
|
+
defined.add(implicit);
|
|
4092
|
+
}
|
|
4093
|
+
}
|
|
4094
|
+
let value;
|
|
4095
|
+
try {
|
|
4096
|
+
value = toValue(node.value);
|
|
4097
|
+
}
|
|
4098
|
+
catch (err) {
|
|
4099
|
+
// Convert plain Errors from toValue() to ParseErrors with location info
|
|
4100
|
+
const e = err;
|
|
4101
|
+
throw new ParseError(input, node.value.loc.start, e.message);
|
|
4102
|
+
}
|
|
4103
|
+
// Inline tables are immutable in TOML: once defined, they cannot be extended.
|
|
4104
|
+
// Track their key path so later dotted keys can be rejected.
|
|
4105
|
+
if (node.value.type === NodeType.InlineTable) {
|
|
4106
|
+
const base_path = active_path.concat(key);
|
|
4107
|
+
inline_tables.add(joinKey(base_path));
|
|
4108
|
+
// Also track nested inline tables within this inline table
|
|
4109
|
+
trackNestedInlineTables(node.value, base_path, inline_tables);
|
|
4110
|
+
}
|
|
3164
4111
|
const target = key.length > 1 ? ensureTable(active, key.slice(0, -1)) : active;
|
|
3165
4112
|
target[last(key)] = value;
|
|
3166
|
-
defined.add(joinKey(key));
|
|
4113
|
+
defined.add(joinKey(active_path.concat(key)));
|
|
3167
4114
|
}
|
|
3168
4115
|
},
|
|
3169
4116
|
[NodeType.InlineTable]: {
|
|
@@ -3182,26 +4129,120 @@ function toValue(node) {
|
|
|
3182
4129
|
switch (node.type) {
|
|
3183
4130
|
case NodeType.InlineTable:
|
|
3184
4131
|
const result = blank();
|
|
4132
|
+
const defined_keys = new Set();
|
|
4133
|
+
const defined_prefixes = new Map(); // prefix -> one of the full keys that uses it
|
|
3185
4134
|
node.items.forEach(({ item }) => {
|
|
3186
4135
|
const key = item.key.value;
|
|
3187
4136
|
const value = toValue(item.value);
|
|
4137
|
+
// Check for duplicate keys and conflicting key paths
|
|
4138
|
+
const full_key = joinKey(key);
|
|
4139
|
+
// Check if this exact key was already defined
|
|
4140
|
+
if (defined_keys.has(full_key)) {
|
|
4141
|
+
throw new Error(`Duplicate key "${full_key}" in inline table`);
|
|
4142
|
+
}
|
|
4143
|
+
// Check if any prefix of this key conflicts with an existing key
|
|
4144
|
+
// e.g., if "a.b" is defined, we can't later define "a.b.c" (would overwrite the value)
|
|
4145
|
+
for (let i = 1; i < key.length; i++) {
|
|
4146
|
+
const prefix = joinKey(key.slice(0, i));
|
|
4147
|
+
if (defined_keys.has(prefix)) {
|
|
4148
|
+
throw new Error(`Key "${full_key}" conflicts with already defined key "${prefix}" in inline table`);
|
|
4149
|
+
}
|
|
4150
|
+
}
|
|
4151
|
+
// Check if this key is a prefix of an already defined key
|
|
4152
|
+
// e.g., if "a.b.c" is defined, we can't later define "a.b" (would overwrite the table)
|
|
4153
|
+
if (defined_prefixes.has(full_key)) {
|
|
4154
|
+
const existing = defined_prefixes.get(full_key);
|
|
4155
|
+
throw new Error(`Key "${full_key}" conflicts with already defined key "${existing}" in inline table`);
|
|
4156
|
+
}
|
|
4157
|
+
defined_keys.add(full_key);
|
|
4158
|
+
// Track all prefixes of this key
|
|
4159
|
+
for (let i = 1; i < key.length; i++) {
|
|
4160
|
+
const prefix = joinKey(key.slice(0, i));
|
|
4161
|
+
if (!defined_prefixes.has(prefix)) {
|
|
4162
|
+
defined_prefixes.set(prefix, full_key);
|
|
4163
|
+
}
|
|
4164
|
+
}
|
|
3188
4165
|
const target = key.length > 1 ? ensureTable(result, key.slice(0, -1)) : result;
|
|
3189
4166
|
target[last(key)] = value;
|
|
3190
4167
|
});
|
|
3191
4168
|
return result;
|
|
3192
4169
|
case NodeType.InlineArray:
|
|
3193
4170
|
return node.items.map(item => toValue(item.item));
|
|
4171
|
+
case NodeType.DateTime:
|
|
4172
|
+
// Preserve TOML date/time custom classes so format is retained when
|
|
4173
|
+
// round-tripping through stringify() (e.g. date-only, time-only, local vs offset).
|
|
4174
|
+
// These classes extend Date, so JS users can still treat them as Dates.
|
|
4175
|
+
return node.value;
|
|
3194
4176
|
case NodeType.String:
|
|
3195
4177
|
case NodeType.Integer:
|
|
3196
4178
|
case NodeType.Float:
|
|
3197
4179
|
case NodeType.Boolean:
|
|
3198
|
-
case NodeType.DateTime:
|
|
3199
4180
|
return node.value;
|
|
3200
4181
|
default:
|
|
3201
4182
|
throw new Error(`Unrecognized value type "${node.type}"`);
|
|
3202
4183
|
}
|
|
3203
4184
|
}
|
|
3204
|
-
function validateKey(object, key, type, state) {
|
|
4185
|
+
function validateKey(object, prefix, key, type, state) {
|
|
4186
|
+
const full_key = prefix.concat(key);
|
|
4187
|
+
const joined_full_key = joinKey(full_key);
|
|
4188
|
+
// 0. Inline tables are immutable.
|
|
4189
|
+
// Once a key is assigned an inline table, it cannot be extended by dotted keys or table headers.
|
|
4190
|
+
// (toml-test invalid: spec-1.1.0/common-49-0, inline-table/overwrite-02, inline-table/overwrite-05)
|
|
4191
|
+
if (type === NodeType.KeyValue && key.length > 1) {
|
|
4192
|
+
for (let i = 1; i < key.length; i++) {
|
|
4193
|
+
const candidate = joinKey(prefix.concat(key.slice(0, i)));
|
|
4194
|
+
if (state.inline_tables.has(candidate)) {
|
|
4195
|
+
throw new Error(`Invalid key, cannot extend an inline table at ${candidate}`);
|
|
4196
|
+
}
|
|
4197
|
+
}
|
|
4198
|
+
}
|
|
4199
|
+
// Also check if a table header tries to extend an inline table
|
|
4200
|
+
if ((type === NodeType.Table || type === NodeType.TableArray) && state.inline_tables.has(joined_full_key)) {
|
|
4201
|
+
throw new Error(`Invalid key, cannot extend an inline table at ${joined_full_key}`);
|
|
4202
|
+
}
|
|
4203
|
+
// Check if table header path contains an inline table
|
|
4204
|
+
if (type === NodeType.Table || type === NodeType.TableArray) {
|
|
4205
|
+
for (let i = 1; i < key.length; i++) {
|
|
4206
|
+
const candidate = joinKey(prefix.concat(key.slice(0, i)));
|
|
4207
|
+
if (state.inline_tables.has(candidate)) {
|
|
4208
|
+
throw new Error(`Invalid key, cannot extend an inline table at ${candidate}`);
|
|
4209
|
+
}
|
|
4210
|
+
}
|
|
4211
|
+
}
|
|
4212
|
+
// 0a. Dotted key-value assignments cannot traverse into an array-of-tables.
|
|
4213
|
+
// This would be ambiguous (which element?) and is rejected by toml-test's
|
|
4214
|
+
// append-with-dotted-keys fixtures.
|
|
4215
|
+
if (type === NodeType.KeyValue && key.length > 1) {
|
|
4216
|
+
for (let i = 1; i < key.length; i++) {
|
|
4217
|
+
const candidate = joinKey(prefix.concat(key.slice(0, i)));
|
|
4218
|
+
if (state.table_arrays.has(candidate)) {
|
|
4219
|
+
throw new Error(`Invalid key, cannot traverse into an array of tables at ${candidate}`);
|
|
4220
|
+
}
|
|
4221
|
+
}
|
|
4222
|
+
}
|
|
4223
|
+
// 0b. Tables created implicitly by dotted keys cannot be re-opened via table headers.
|
|
4224
|
+
// (toml-test invalid: spec-1.1.0/common-46-0 and common-46-1)
|
|
4225
|
+
if ((type === NodeType.Table || type === NodeType.TableArray) && state.implicit_tables.has(joined_full_key)) {
|
|
4226
|
+
throw new Error(`Invalid key, a table has already been defined implicitly named ${joined_full_key}`);
|
|
4227
|
+
}
|
|
4228
|
+
// 0c. A table path cannot later be re-assigned as a value.
|
|
4229
|
+
// Example: `type.name = "Nail"` then `type = { edible = false }` is invalid.
|
|
4230
|
+
// Example: `a.b.c = 1` then `a.b = 2` is invalid (a.b was implicitly created as a table).
|
|
4231
|
+
// (toml-test invalid: spec-1.1.0/common-50-0, table/append-with-dotted-keys-05)
|
|
4232
|
+
if (type === NodeType.KeyValue && state.implicit_tables.has(joined_full_key)) {
|
|
4233
|
+
throw new Error(`Invalid key, a table has already been defined named ${joined_full_key}`);
|
|
4234
|
+
}
|
|
4235
|
+
// 0d. Dotted keys cannot extend tables that were explicitly defined earlier.
|
|
4236
|
+
// Example: `[a.b.c]` followed by `[a]` then `b.c.t = "value"` is invalid.
|
|
4237
|
+
// (toml-test invalid: table/append-with-dotted-keys-01, table/append-with-dotted-keys-02)
|
|
4238
|
+
if (type === NodeType.KeyValue && key.length > 1) {
|
|
4239
|
+
for (let i = 1; i <= key.length; i++) {
|
|
4240
|
+
const candidate = joinKey(prefix.concat(key.slice(0, i)));
|
|
4241
|
+
if (state.tables.has(candidate)) {
|
|
4242
|
+
throw new Error(`Invalid key, cannot add to an explicitly defined table ${candidate} using dotted keys`);
|
|
4243
|
+
}
|
|
4244
|
+
}
|
|
4245
|
+
}
|
|
3205
4246
|
// 1. Cannot override primitive value
|
|
3206
4247
|
let parts = [];
|
|
3207
4248
|
let index = 0;
|
|
@@ -3210,20 +4251,28 @@ function validateKey(object, key, type, state) {
|
|
|
3210
4251
|
if (!has(object, part))
|
|
3211
4252
|
return;
|
|
3212
4253
|
if (isPrimitive(object[part])) {
|
|
3213
|
-
|
|
4254
|
+
const fullKey = joinKey(prefix.concat(parts));
|
|
4255
|
+
throw new Error(`Invalid key, a value has already been defined for ${fullKey}`);
|
|
3214
4256
|
}
|
|
3215
|
-
const joined_parts = joinKey(parts);
|
|
4257
|
+
const joined_parts = joinKey(prefix.concat(parts));
|
|
3216
4258
|
if (Array.isArray(object[part]) && !state.table_arrays.has(joined_parts)) {
|
|
3217
4259
|
throw new Error(`Invalid key, cannot add to a static array at ${joined_parts}`);
|
|
3218
4260
|
}
|
|
3219
4261
|
const next_is_last = index++ < key.length - 1;
|
|
3220
4262
|
object = Array.isArray(object[part]) && next_is_last ? last(object[part]) : object[part];
|
|
3221
4263
|
}
|
|
3222
|
-
const joined_key =
|
|
4264
|
+
const joined_key = joined_full_key;
|
|
3223
4265
|
// 2. Cannot override table
|
|
3224
4266
|
if (object && type === NodeType.Table && state.defined.has(joined_key)) {
|
|
3225
4267
|
throw new Error(`Invalid key, a table has already been defined named ${joined_key}`);
|
|
3226
4268
|
}
|
|
4269
|
+
// 2b. Cannot assign a value to a path that is already a table (explicit or implicit).
|
|
4270
|
+
if (object && type === NodeType.KeyValue && key.length === 1 && state.defined.has(joined_key)) {
|
|
4271
|
+
// If the path exists as a structured value, overriding it is invalid.
|
|
4272
|
+
if (!isPrimitive(object)) {
|
|
4273
|
+
throw new Error(`Invalid key, a table has already been defined named ${joined_key}`);
|
|
4274
|
+
}
|
|
4275
|
+
}
|
|
3227
4276
|
// 3. Cannot add table array to static array or table
|
|
3228
4277
|
if (object && type === NodeType.TableArray && !state.table_arrays.has(joined_key)) {
|
|
3229
4278
|
throw new Error(`Invalid key, cannot add an array of tables to a table at ${joined_key}`);
|
|
@@ -3435,13 +4484,29 @@ function findByPath(node, path) {
|
|
|
3435
4484
|
key = key.concat(array_index);
|
|
3436
4485
|
}
|
|
3437
4486
|
else if (isInlineItem(item) && isKeyValue(item.item)) {
|
|
4487
|
+
// For InlineItems wrapping KeyValues, extract the key
|
|
3438
4488
|
key = item.item.key.value;
|
|
3439
4489
|
}
|
|
3440
4490
|
else if (isInlineItem(item)) {
|
|
3441
4491
|
key = [index];
|
|
3442
4492
|
}
|
|
3443
4493
|
if (key.length && arraysEqual(key, path.slice(0, key.length))) {
|
|
3444
|
-
|
|
4494
|
+
// For InlineItems containing KeyValues, we need to search within the value
|
|
4495
|
+
// but still return the InlineItem or its contents appropriately
|
|
4496
|
+
if (isInlineItem(item) && isKeyValue(item.item)) {
|
|
4497
|
+
if (path.length === key.length) {
|
|
4498
|
+
// If we've matched the full path, return the InlineItem itself
|
|
4499
|
+
// so it can be found and replaced in the parent's items array
|
|
4500
|
+
found = item;
|
|
4501
|
+
}
|
|
4502
|
+
else {
|
|
4503
|
+
// Continue searching within the KeyValue's value
|
|
4504
|
+
found = findByPath(item.item.value, path.slice(key.length));
|
|
4505
|
+
}
|
|
4506
|
+
}
|
|
4507
|
+
else {
|
|
4508
|
+
found = findByPath(item, path.slice(key.length));
|
|
4509
|
+
}
|
|
3445
4510
|
return true;
|
|
3446
4511
|
}
|
|
3447
4512
|
else {
|
|
@@ -3728,7 +4793,13 @@ function applyChanges(original, updated, changes, format) {
|
|
|
3728
4793
|
insert(original, parent, child, undefined, true);
|
|
3729
4794
|
}
|
|
3730
4795
|
else {
|
|
3731
|
-
|
|
4796
|
+
// Unwrap InlineItem if we're adding to a Table (not InlineTable)
|
|
4797
|
+
// InlineItems should only exist within InlineTables or InlineArrays
|
|
4798
|
+
let childToInsert = child;
|
|
4799
|
+
if (isInlineItem(child) && (isTable(parent) || isDocument(parent))) {
|
|
4800
|
+
childToInsert = child.item;
|
|
4801
|
+
}
|
|
4802
|
+
insert(original, parent, childToInsert);
|
|
3732
4803
|
}
|
|
3733
4804
|
}
|
|
3734
4805
|
}
|
|
@@ -3757,6 +4828,14 @@ function applyChanges(original, updated, changes, format) {
|
|
|
3757
4828
|
parent = existing;
|
|
3758
4829
|
existing = existing.item;
|
|
3759
4830
|
}
|
|
4831
|
+
else if (isInlineItem(existing) && isInlineItem(replacement) && isKeyValue(existing.item) && isKeyValue(replacement.item)) {
|
|
4832
|
+
// Both are InlineItems wrapping KeyValues (nested inline table edits)
|
|
4833
|
+
// Preserve formatting and edit the value within
|
|
4834
|
+
preserveFormatting(existing.item.value, replacement.item.value);
|
|
4835
|
+
parent = existing.item;
|
|
4836
|
+
existing = existing.item.value;
|
|
4837
|
+
replacement = replacement.item.value;
|
|
4838
|
+
}
|
|
3760
4839
|
else {
|
|
3761
4840
|
parent = findParent(original, change.path);
|
|
3762
4841
|
// Special handling for array element edits
|