@decimalturn/toml-patch 0.6.0 → 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.
@@ -1,4 +1,4 @@
1
- //! @decimalturn/toml-patch v0.6.0 - https://github.com/DecimalTurn/toml-patch - @license: MIT
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] || 0;
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
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 match;
186
- while ((match = BY_NEW_LINE.exec(input)) != null) {
187
- indexes.push(match.index + match[0].length - 1);
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 + 1);
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
- while (!cursor.peek().done && !IS_NEW_LINE.test(cursor.peek().value)) {
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
- let quotes = multiline_char + multiline_char + multiline_char;
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
- // The reason why we need to check if there is more than three is because we have to match the last 3 quotes, not the first 3 that appears consecutively
310
- // See spec-string-basic-multiline-9.toml
311
- while (!cursor.done && (!checkThree(input, cursor.index, multiline_char) || CheckMoreThanThree(input, cursor.index, multiline_char))) {
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)
@@ -430,15 +561,6 @@ function checkThree(input, current, check) {
430
561
  const isEscaped = backslashes[0].length % 2 !== 0; // Odd number of backslashes means escaped
431
562
  return isEscaped ? false : check; // Return `check` if not escaped, otherwise `false`
432
563
  }
433
- function CheckMoreThanThree(input, current, check) {
434
- if (!check) {
435
- return false;
436
- }
437
- return (input[current] === check &&
438
- input[current + 1] === check &&
439
- input[current + 2] === check &&
440
- input[current + 3] === check);
441
- }
442
564
 
443
565
  function last(values) {
444
566
  return values[values.length - 1];
@@ -577,6 +699,40 @@ function isBackslashEscaped(source, backslashOffset) {
577
699
  return precedingBackslashes % 2 !== 0;
578
700
  }
579
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
+ }
580
736
  // TOML 1.1.0: Handle \xHH hex escapes (for codepoints < 255)
581
737
  const HEX_ESCAPE = /\\x([a-fA-F0-9]{2})/g;
582
738
  const hexEscapeSource = escaped;
@@ -825,13 +981,14 @@ class DateFormatHelper {
825
981
  // Patterns for different date/time formats
826
982
  DateFormatHelper.IS_DATE_ONLY = /^\d{4}-\d{2}-\d{2}$/;
827
983
  DateFormatHelper.IS_TIME_ONLY = /^\d{2}:\d{2}(?::\d{2})?(?:\.\d+)?$/;
828
- DateFormatHelper.IS_LOCAL_DATETIME_T = /^\d{4}-\d{2}-\d{2}T\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+)?$/;
829
985
  DateFormatHelper.IS_LOCAL_DATETIME_SPACE = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}(?::\d{2})?(?:\.\d+)?$/;
830
- DateFormatHelper.IS_OFFSET_DATETIME_T = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2})?(?:\.\d+)?(?:[Zz]|[+-]\d{2}:\d{2})$/;
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})$/;
831
987
  DateFormatHelper.IS_OFFSET_DATETIME_SPACE = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}(?::\d{2})?(?:\.\d+)?(?:[Zz]|[+-]\d{2}:\d{2})$/;
832
988
  // Legacy patterns from parse-toml.ts (for compatibility)
833
- DateFormatHelper.IS_FULL_DATE = /(\d{4})-(\d{2})-(\d{2})/;
834
- DateFormatHelper.IS_FULL_TIME = /(\d{2}):(\d{2})(?::(\d{2}))?/;
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+))?/;
835
992
  /**
836
993
  * Custom Date class for local dates (date-only).
837
994
  * Format: 1979-05-27
@@ -1004,16 +1161,19 @@ const TRUE = 'true';
1004
1161
  const FALSE = 'false';
1005
1162
  const HAS_E = /e/i;
1006
1163
  const IS_DIVIDER = /\_/g;
1007
- const IS_INF = /inf/;
1008
- const IS_NAN = /nan/;
1009
- const IS_HEX = /^0x/;
1010
- const IS_OCTAL = /^0o/;
1011
- 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;
1012
1169
  function* parseTOML(input) {
1013
- const tokens = tokenize(input);
1014
- const cursor = new Cursor(tokens);
1170
+ // Use non-generator parsing to avoid stack overflow on deeply nested structures
1171
+ const cursor = new Cursor(tokenize(input));
1015
1172
  while (!cursor.next().done) {
1016
- yield* walkBlock(cursor, input);
1173
+ const blocks = walkBlock(cursor, input);
1174
+ for (const block of blocks) {
1175
+ yield block;
1176
+ }
1017
1177
  }
1018
1178
  }
1019
1179
  /**
@@ -1028,59 +1188,14 @@ function* continueParsingTOML(existingAst, remainingString) {
1028
1188
  for (const item of existingAst) {
1029
1189
  yield item;
1030
1190
  }
1031
- // Parse and yield all items from the remaining string
1032
- for (const item of parseTOML(remainingString)) {
1033
- yield item;
1034
- }
1035
- }
1036
- function* walkBlock(cursor, input) {
1037
- if (cursor.value.type === TokenType.Comment) {
1038
- yield comment(cursor);
1039
- }
1040
- else if (cursor.value.type === TokenType.Bracket) {
1041
- yield table(cursor, input);
1042
- }
1043
- else if (cursor.value.type === TokenType.Literal) {
1044
- yield* keyValue(cursor, input);
1045
- }
1046
- else {
1047
- throw new ParseError(input, cursor.value.loc.start, `Unexpected token "${cursor.value.type}". Expected Comment, Bracket, or String`);
1048
- }
1049
- }
1050
- function* walkValue$1(cursor, input) {
1051
- if (cursor.value.type === TokenType.Literal) {
1052
- if (cursor.value.raw[0] === DOUBLE_QUOTE || cursor.value.raw[0] === SINGLE_QUOTE) {
1053
- yield string(cursor);
1054
- }
1055
- else if (cursor.value.raw === TRUE || cursor.value.raw === FALSE) {
1056
- yield boolean(cursor);
1057
- }
1058
- else if (dateFormatHelper.IS_FULL_DATE.test(cursor.value.raw) || dateFormatHelper.IS_FULL_TIME.test(cursor.value.raw)) {
1059
- yield datetime(cursor, input);
1060
- }
1061
- else if ((!cursor.peek().done && cursor.peek().value.type === TokenType.Dot) ||
1062
- IS_INF.test(cursor.value.raw) ||
1063
- IS_NAN.test(cursor.value.raw) ||
1064
- (HAS_E.test(cursor.value.raw) && !IS_HEX.test(cursor.value.raw))) {
1065
- yield float(cursor, input);
1066
- }
1067
- else {
1068
- 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;
1069
1197
  }
1070
1198
  }
1071
- else if (cursor.value.type === TokenType.Curly) {
1072
- const [inline_table, comments] = inlineTable(cursor, input);
1073
- yield inline_table;
1074
- yield* comments;
1075
- }
1076
- else if (cursor.value.type === TokenType.Bracket) {
1077
- const [inline_array, comments] = inlineArray(cursor, input);
1078
- yield inline_array;
1079
- yield* comments;
1080
- }
1081
- else {
1082
- throw new ParseError(input, cursor.value.loc.start, `Unrecognized token type "${cursor.value.type}". Expected String, Curly, or Bracket`);
1083
- }
1084
1199
  }
1085
1200
  function comment(cursor) {
1086
1201
  // # line comment
@@ -1114,8 +1229,22 @@ function table(cursor, input) {
1114
1229
  if (is_table && cursor.value.raw !== '[') {
1115
1230
  throw new ParseError(input, cursor.value.loc.start, `Expected table opening "[", found ${cursor.value.raw}`);
1116
1231
  }
1117
- if (!is_table && (cursor.value.raw !== '[' || cursor.peek().value.raw !== '[')) {
1118
- throw new ParseError(input, cursor.value.loc.start, `Expected array of tables opening "[[", found ${cursor.value.raw + cursor.peek().value.raw}`);
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
+ }
1119
1248
  }
1120
1249
  // Set start location from opening tag
1121
1250
  const key = is_table
@@ -1134,23 +1263,84 @@ function table(cursor, input) {
1134
1263
  if (cursor.done) {
1135
1264
  throw new ParseError(input, key.loc.start, `Expected table key, reached end of file`);
1136
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
+ }
1137
1297
  key.item = {
1138
1298
  type: NodeType.Key,
1139
1299
  loc: cloneLocation(cursor.value.loc),
1140
1300
  raw: cursor.value.raw,
1141
- value: [parseString(cursor.value.raw)]
1301
+ value: keyValue
1142
1302
  };
1143
1303
  while (!cursor.peek().done && cursor.peek().value.type === TokenType.Dot) {
1144
1304
  cursor.next();
1145
1305
  const dot = cursor.value;
1146
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
+ }
1147
1318
  const before = ' '.repeat(dot.loc.start.column - key.item.loc.end.column);
1148
1319
  const after = ' '.repeat(cursor.value.loc.start.column - dot.loc.end.column);
1149
1320
  key.item.loc.end = cursor.value.loc.end;
1150
1321
  key.item.raw += `${before}.${after}${cursor.value.raw}`;
1151
- key.item.value.push(parseString(cursor.value.raw));
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
+ }
1152
1329
  }
1153
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
+ }
1154
1344
  if (is_table && (cursor.done || cursor.value.raw !== ']')) {
1155
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}`);
1156
1346
  }
@@ -1163,15 +1353,37 @@ function table(cursor, input) {
1163
1353
  ? 'end of file'
1164
1354
  : cursor.value.raw + cursor.peek().value.raw}`);
1165
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
+ }
1166
1366
  // Set end location from closing tag
1167
1367
  if (!is_table)
1168
1368
  cursor.next();
1169
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
+ }
1170
1382
  // Add child items
1171
1383
  let items = [];
1172
1384
  while (!cursor.peek().done && cursor.peek().value.type !== TokenType.Bracket) {
1173
1385
  cursor.next();
1174
- merge(items, [...walkBlock(cursor, input)]);
1386
+ merge(items, walkBlock(cursor, input));
1175
1387
  }
1176
1388
  return {
1177
1389
  type: is_table ? NodeType.Table : NodeType.TableArray,
@@ -1185,56 +1397,20 @@ function table(cursor, input) {
1185
1397
  items
1186
1398
  };
1187
1399
  }
1188
- function keyValue(cursor, input) {
1189
- // 3. KeyValue
1190
- //
1191
- // key = value
1192
- // ^-^ key
1193
- // ^ equals
1194
- // ^---^ value
1195
- const key = {
1196
- type: NodeType.Key,
1197
- loc: cloneLocation(cursor.value.loc),
1198
- raw: cursor.value.raw,
1199
- value: [parseString(cursor.value.raw)]
1200
- };
1201
- while (!cursor.peek().done && cursor.peek().value.type === TokenType.Dot) {
1202
- cursor.next();
1203
- cursor.next();
1204
- key.loc.end = cursor.value.loc.end;
1205
- key.raw += `.${cursor.value.raw}`;
1206
- key.value.push(parseString(cursor.value.raw));
1207
- }
1208
- cursor.next();
1209
- if (cursor.done || cursor.value.type !== TokenType.Equal) {
1210
- 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);
1211
1404
  }
1212
- const equals = cursor.value.loc.start.column;
1213
- cursor.next();
1214
- if (cursor.done) {
1215
- 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);
1216
1408
  }
1217
- const [value, ...comments] = walkValue$1(cursor, input);
1218
- return [
1219
- {
1220
- type: NodeType.KeyValue,
1221
- key,
1222
- value: value,
1223
- loc: {
1224
- start: clonePosition(key.loc.start),
1225
- end: clonePosition(value.loc.end)
1226
- },
1227
- equals
1228
- },
1229
- ...comments
1230
- ];
1231
- }
1232
- function string(cursor) {
1233
1409
  return {
1234
1410
  type: NodeType.String,
1235
1411
  loc: cursor.value.loc,
1236
1412
  raw: cursor.value.raw,
1237
- value: parseString(cursor.value.raw)
1413
+ value
1238
1414
  };
1239
1415
  }
1240
1416
  function boolean(cursor) {
@@ -1287,6 +1463,10 @@ function datetime(cursor, input) {
1287
1463
  loc = { start, end: cursor.value.loc.end };
1288
1464
  raw += `.${cursor.value.raw}`;
1289
1465
  }
1466
+ // Validate datetime format
1467
+ {
1468
+ validateDateTimeFormat(raw, input, loc.start);
1469
+ }
1290
1470
  if (!dateFormatHelper.IS_FULL_DATE.test(raw)) {
1291
1471
  // Local time only (e.g., "07:32:00" or "07:32:00.999")
1292
1472
  if (dateFormatHelper.IS_TIME_ONLY.test(raw)) {
@@ -1329,18 +1509,227 @@ function datetime(cursor, input) {
1329
1509
  value
1330
1510
  };
1331
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
+ }
1332
1693
  function float(cursor, input) {
1333
1694
  let loc = cursor.value.loc;
1334
1695
  let raw = cursor.value.raw;
1335
1696
  let value;
1336
1697
  if (IS_INF.test(raw)) {
1337
- value = raw === '-inf' ? -Infinity : Infinity;
1698
+ value = raw.startsWith('-') ? -Infinity : Infinity;
1338
1699
  }
1339
1700
  else if (IS_NAN.test(raw)) {
1340
- value = raw === '-nan' ? -NaN : NaN;
1701
+ value = NaN;
1341
1702
  }
1342
1703
  else if (!cursor.peek().done && cursor.peek().value.type === TokenType.Dot) {
1343
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
+ }
1344
1733
  // From spec:
1345
1734
  // | A fractional part is a decimal point followed by one or more digits.
1346
1735
  //
@@ -1352,47 +1741,425 @@ function float(cursor, input) {
1352
1741
  cursor.next();
1353
1742
  raw += `.${cursor.value.raw}`;
1354
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
+ }
1355
1774
  value = Number(raw.replace(IS_DIVIDER, ''));
1356
1775
  }
1357
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
+ }
1358
1813
  value = Number(raw.replace(IS_DIVIDER, ''));
1359
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
+ }
1360
1819
  return { type: NodeType.Float, loc, raw, value };
1361
1820
  }
1362
- function integer(cursor) {
1363
- // > Integer values -0 and +0 are valid and identical to an unprefixed zero
1364
- if (cursor.value.raw === '-0' || cursor.value.raw === '+0') {
1365
- return {
1366
- type: NodeType.Integer,
1367
- loc: cursor.value.loc,
1368
- raw: cursor.value.raw,
1369
- value: 0
1370
- };
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
+ }
1371
1863
  }
1372
1864
  let radix = 10;
1373
- if (IS_HEX.test(cursor.value.raw)) {
1865
+ let numericPart;
1866
+ // Hexadecimal validation
1867
+ if (IS_HEX.test(raw)) {
1374
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
+ }
1375
1891
  }
1376
- else if (IS_OCTAL.test(cursor.value.raw)) {
1892
+ // Octal validation
1893
+ else if (IS_OCTAL.test(raw)) {
1377
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
+ }
1378
1917
  }
1379
- else if (IS_BINARY.test(cursor.value.raw)) {
1918
+ // Binary validation
1919
+ else if (IS_BINARY.test(raw)) {
1380
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
+ }
1381
1943
  }
1382
- const value = parseInt(cursor
1383
- .value.raw.replace(IS_DIVIDER, '')
1944
+ const value = parseInt(raw
1945
+ .replace(IS_DIVIDER, '')
1384
1946
  .replace(IS_OCTAL, '')
1385
1947
  .replace(IS_BINARY, ''), radix);
1948
+ if (Number.isNaN(value)) {
1949
+ throw new ParseError(input, loc.start, `Invalid integer "${raw}"`);
1950
+ }
1386
1951
  return {
1387
1952
  type: NodeType.Integer,
1388
- loc: cursor.value.loc,
1389
- raw: cursor.value.raw,
1953
+ loc: loc,
1954
+ raw: raw,
1390
1955
  value
1391
1956
  };
1392
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
+ }
1393
2160
  function inlineTable(cursor, input) {
1394
2161
  if (cursor.value.raw !== '{') {
1395
- throw new ParseError(input, cursor.value.loc.start, `Expected "{" for inline table, found ${cursor.value.raw}`);
2162
+ throw new ParseError(input, cursor.value.loc.start, `Expected "{" for inline table`);
1396
2163
  }
1397
2164
  // 6. InlineTable
1398
2165
  const value = {
@@ -1404,7 +2171,6 @@ function inlineTable(cursor, input) {
1404
2171
  cursor.next();
1405
2172
  while (!cursor.done &&
1406
2173
  !(cursor.value.type === TokenType.Curly && cursor.value.raw === '}')) {
1407
- // TOML 1.1.0: Handle comments in inline tables
1408
2174
  if (cursor.value.type === TokenType.Comment) {
1409
2175
  comments.push(comment(cursor));
1410
2176
  cursor.next();
@@ -1415,29 +2181,37 @@ function inlineTable(cursor, input) {
1415
2181
  if (!previous) {
1416
2182
  throw new ParseError(input, cursor.value.loc.start, 'Found "," without previous value in inline table');
1417
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
+ }
1418
2187
  previous.comma = true;
1419
2188
  previous.loc.end = cursor.value.loc.start;
1420
2189
  cursor.next();
1421
2190
  continue;
1422
2191
  }
1423
- const [item, ...additional_comments] = walkBlock(cursor, input);
1424
- if (item.type !== NodeType.KeyValue) {
1425
- throw new ParseError(input, cursor.value.loc.start, `Only key-values are supported in inline tables, found ${item.type}`);
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);
1426
2208
  }
1427
- const inline_item = {
1428
- type: NodeType.InlineItem,
1429
- loc: cloneLocation(item.loc),
1430
- item,
1431
- comma: false
1432
- };
1433
- value.items.push(inline_item);
1434
- merge(comments, additional_comments);
1435
2209
  cursor.next();
1436
2210
  }
1437
2211
  if (cursor.done ||
1438
2212
  cursor.value.type !== TokenType.Curly ||
1439
2213
  cursor.value.raw !== '}') {
1440
- throw new ParseError(input, cursor.done ? value.loc.start : cursor.value.loc.start, `Expected "}", found ${cursor.done ? 'end of file' : cursor.value.raw}`);
2214
+ throw new ParseError(input, cursor.done ? value.loc.start : cursor.value.loc.start, `Expected "}"`);
1441
2215
  }
1442
2216
  value.loc.end = cursor.value.loc.end;
1443
2217
  return [value, comments];
@@ -1445,14 +2219,14 @@ function inlineTable(cursor, input) {
1445
2219
  function inlineArray(cursor, input) {
1446
2220
  // 7. InlineArray
1447
2221
  if (cursor.value.raw !== '[') {
1448
- throw new ParseError(input, cursor.value.loc.start, `Expected "[" for inline array, found ${cursor.value.raw}`);
2222
+ throw new ParseError(input, cursor.value.loc.start, `Expected "[" for inline array`);
1449
2223
  }
1450
2224
  const value = {
1451
2225
  type: NodeType.InlineArray,
1452
2226
  loc: cloneLocation(cursor.value.loc),
1453
2227
  items: []
1454
2228
  };
1455
- let comments = [];
2229
+ const comments = [];
1456
2230
  cursor.next();
1457
2231
  while (!cursor.done &&
1458
2232
  !(cursor.value.type === TokenType.Bracket && cursor.value.raw === ']')) {
@@ -1461,6 +2235,9 @@ function inlineArray(cursor, input) {
1461
2235
  if (!previous) {
1462
2236
  throw new ParseError(input, cursor.value.loc.start, 'Found "," without previous value for inline array');
1463
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
+ }
1464
2241
  previous.comma = true;
1465
2242
  previous.loc.end = cursor.value.loc.start;
1466
2243
  }
@@ -1468,14 +2245,19 @@ function inlineArray(cursor, input) {
1468
2245
  comments.push(comment(cursor));
1469
2246
  }
1470
2247
  else {
1471
- const [item, ...additional_comments] = walkValue$1(cursor, input);
1472
- const inline_item = {
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({
1473
2256
  type: NodeType.InlineItem,
1474
2257
  loc: cloneLocation(item.loc),
1475
2258
  item,
1476
2259
  comma: false
1477
- };
1478
- value.items.push(inline_item);
2260
+ });
1479
2261
  merge(comments, additional_comments);
1480
2262
  }
1481
2263
  cursor.next();
@@ -1483,7 +2265,7 @@ function inlineArray(cursor, input) {
1483
2265
  if (cursor.done ||
1484
2266
  cursor.value.type !== TokenType.Bracket ||
1485
2267
  cursor.value.raw !== ']') {
1486
- throw new ParseError(input, cursor.done ? value.loc.start : cursor.value.loc.start, `Expected "]", found ${cursor.done ? 'end of file' : cursor.value.raw}`);
2268
+ throw new ParseError(input, cursor.done ? value.loc.start : cursor.value.loc.start, `Expected "]"`);
1487
2269
  }
1488
2270
  value.loc.end = cursor.value.loc.end;
1489
2271
  return [value, comments];
@@ -3218,6 +4000,21 @@ function getLine(lines, index) {
3218
4000
  return lines[index - 1];
3219
4001
  }
3220
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
+ }
3221
4018
  /**
3222
4019
  * Converts the given AST to a JavaScript object.
3223
4020
  *
@@ -3230,13 +4027,16 @@ function toJS(ast, input = '') {
3230
4027
  const tables = new Set();
3231
4028
  const table_arrays = new Set();
3232
4029
  const defined = new Set();
4030
+ const implicit_tables = new Set();
4031
+ const inline_tables = new Set();
3233
4032
  let active = result;
3234
4033
  let skip_depth = 0;
4034
+ let active_path = [];
3235
4035
  traverse(ast, {
3236
4036
  [NodeType.Table](node) {
3237
4037
  const key = node.key.item.value;
3238
4038
  try {
3239
- validateKey(result, key, node.type, { tables, table_arrays, defined });
4039
+ validateKey(result, [], key, node.type, { tables, table_arrays, defined, implicit_tables, inline_tables });
3240
4040
  }
3241
4041
  catch (err) {
3242
4042
  const e = err;
@@ -3246,11 +4046,12 @@ function toJS(ast, input = '') {
3246
4046
  tables.add(joined_key);
3247
4047
  defined.add(joined_key);
3248
4048
  active = ensureTable(result, key);
4049
+ active_path = key;
3249
4050
  },
3250
4051
  [NodeType.TableArray](node) {
3251
4052
  const key = node.key.item.value;
3252
4053
  try {
3253
- validateKey(result, key, node.type, { tables, table_arrays, defined });
4054
+ validateKey(result, [], key, node.type, { tables, table_arrays, defined, implicit_tables, inline_tables });
3254
4055
  }
3255
4056
  catch (err) {
3256
4057
  const e = err;
@@ -3260,6 +4061,7 @@ function toJS(ast, input = '') {
3260
4061
  table_arrays.add(joined_key);
3261
4062
  defined.add(joined_key);
3262
4063
  active = ensureTableArray(result, key);
4064
+ active_path = key;
3263
4065
  },
3264
4066
  [NodeType.KeyValue]: {
3265
4067
  enter(node) {
@@ -3267,16 +4069,48 @@ function toJS(ast, input = '') {
3267
4069
  return;
3268
4070
  const key = node.key.value;
3269
4071
  try {
3270
- validateKey(active, key, node.type, { tables, table_arrays, defined });
4072
+ validateKey(active, active_path, key, node.type, {
4073
+ tables,
4074
+ table_arrays,
4075
+ defined,
4076
+ implicit_tables,
4077
+ inline_tables
4078
+ });
3271
4079
  }
3272
4080
  catch (err) {
3273
4081
  const e = err;
3274
4082
  throw new ParseError(input, node.key.loc.start, e.message);
3275
4083
  }
3276
- const value = toValue(node.value);
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
+ }
3277
4111
  const target = key.length > 1 ? ensureTable(active, key.slice(0, -1)) : active;
3278
4112
  target[last(key)] = value;
3279
- defined.add(joinKey(key));
4113
+ defined.add(joinKey(active_path.concat(key)));
3280
4114
  }
3281
4115
  },
3282
4116
  [NodeType.InlineTable]: {
@@ -3295,9 +4129,39 @@ function toValue(node) {
3295
4129
  switch (node.type) {
3296
4130
  case NodeType.InlineTable:
3297
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
3298
4134
  node.items.forEach(({ item }) => {
3299
4135
  const key = item.key.value;
3300
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
+ }
3301
4165
  const target = key.length > 1 ? ensureTable(result, key.slice(0, -1)) : result;
3302
4166
  target[last(key)] = value;
3303
4167
  });
@@ -3318,7 +4182,67 @@ function toValue(node) {
3318
4182
  throw new Error(`Unrecognized value type "${node.type}"`);
3319
4183
  }
3320
4184
  }
3321
- 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
+ }
3322
4246
  // 1. Cannot override primitive value
3323
4247
  let parts = [];
3324
4248
  let index = 0;
@@ -3327,20 +4251,28 @@ function validateKey(object, key, type, state) {
3327
4251
  if (!has(object, part))
3328
4252
  return;
3329
4253
  if (isPrimitive(object[part])) {
3330
- throw new Error(`Invalid key, a value has already been defined for ${parts.join('.')}`);
4254
+ const fullKey = joinKey(prefix.concat(parts));
4255
+ throw new Error(`Invalid key, a value has already been defined for ${fullKey}`);
3331
4256
  }
3332
- const joined_parts = joinKey(parts);
4257
+ const joined_parts = joinKey(prefix.concat(parts));
3333
4258
  if (Array.isArray(object[part]) && !state.table_arrays.has(joined_parts)) {
3334
4259
  throw new Error(`Invalid key, cannot add to a static array at ${joined_parts}`);
3335
4260
  }
3336
4261
  const next_is_last = index++ < key.length - 1;
3337
4262
  object = Array.isArray(object[part]) && next_is_last ? last(object[part]) : object[part];
3338
4263
  }
3339
- const joined_key = joinKey(key);
4264
+ const joined_key = joined_full_key;
3340
4265
  // 2. Cannot override table
3341
4266
  if (object && type === NodeType.Table && state.defined.has(joined_key)) {
3342
4267
  throw new Error(`Invalid key, a table has already been defined named ${joined_key}`);
3343
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
+ }
3344
4276
  // 3. Cannot add table array to static array or table
3345
4277
  if (object && type === NodeType.TableArray && !state.table_arrays.has(joined_key)) {
3346
4278
  throw new Error(`Invalid key, cannot add an array of tables to a table at ${joined_key}`);