@decimalturn/toml-patch 0.5.2 → 0.6.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.5.2 - https://github.com/DecimalTurn/toml-patch - @license: MIT
1
+ //! @decimalturn/toml-patch v0.6.0 - https://github.com/DecimalTurn/toml-patch - @license: MIT
2
2
  interface Location {
3
3
  start: Position;
4
4
  end: Position;
@@ -222,7 +222,17 @@ declare class TomlFormat {
222
222
  *
223
223
  */
224
224
  truncateZeroTimeInDates?: boolean;
225
- constructor(newLine?: string, trailingNewline?: number, trailingComma?: boolean, bracketSpacing?: boolean, inlineTableStart?: number, truncateZeroTimeInDates?: boolean);
225
+ /**
226
+ * Whether to use tabs instead of spaces for indentation/padding.
227
+ * When enabled, lines that need to be indented will use tabs.
228
+ *
229
+ * @example
230
+ * - true: Uses tabs for indentation
231
+ * - false: Uses spaces for indentation (default)
232
+ *
233
+ */
234
+ useTabsForIndentation?: boolean;
235
+ constructor(newLine?: string, trailingNewline?: number, trailingComma?: boolean, bracketSpacing?: boolean, inlineTableStart?: number, truncateZeroTimeInDates?: boolean, useTabsForIndentation?: boolean);
226
236
  /**
227
237
  * Creates a new TomlFormat instance with default formatting preferences.
228
238
  *
@@ -1,4 +1,4 @@
1
- //! @decimalturn/toml-patch v0.5.2 - https://github.com/DecimalTurn/toml-patch - @license: MIT
1
+ //! @decimalturn/toml-patch v0.6.0 - https://github.com/DecimalTurn/toml-patch - @license: MIT
2
2
  var NodeType;
3
3
  (function (NodeType) {
4
4
  NodeType["Document"] = "Document";
@@ -176,7 +176,7 @@ function getLine$1(input, position) {
176
176
  const lines = findLines(input);
177
177
  const start = lines[position.line - 2] || 0;
178
178
  const end = lines[position.line - 1] || input.length;
179
- return input.substr(start, end - start);
179
+ return input.substring(start, end);
180
180
  }
181
181
  function findLines(input) {
182
182
  // exec is stateful, so create new regexp each time
@@ -416,6 +416,11 @@ function checkThree(input, current, check) {
416
416
  if (!has3) {
417
417
  return false;
418
418
  }
419
+ // Only check for escaping in basic strings (double quotes)
420
+ // Literal strings (single quotes) don't support escape sequences
421
+ if (check === SINGLE_QUOTE) {
422
+ return check; // No escaping in literal strings
423
+ }
419
424
  // Check if the sequence is escaped
420
425
  const precedingText = input.slice(0, current); // Get the text before the current position
421
426
  const backslashes = precedingText.match(/\\+$/); // Match trailing backslashes
@@ -564,11 +569,41 @@ function escapeDoubleQuotes(value) {
564
569
  }
565
570
  return result;
566
571
  }
572
+ function isBackslashEscaped(source, backslashOffset) {
573
+ let precedingBackslashes = 0;
574
+ for (let i = backslashOffset - 1; i >= 0 && source[i] === '\\'; i--) {
575
+ precedingBackslashes++;
576
+ }
577
+ return precedingBackslashes % 2 !== 0;
578
+ }
567
579
  function unescapeLargeUnicode(escaped) {
580
+ // TOML 1.1.0: Handle \xHH hex escapes (for codepoints < 255)
581
+ const HEX_ESCAPE = /\\x([a-fA-F0-9]{2})/g;
582
+ const hexEscapeSource = escaped;
583
+ let withHexEscapes = hexEscapeSource.replace(HEX_ESCAPE, (match, hex, offset) => {
584
+ if (isBackslashEscaped(hexEscapeSource, offset)) {
585
+ return match;
586
+ }
587
+ const codePoint = parseInt(hex, 16);
588
+ const asString = String.fromCharCode(codePoint);
589
+ // Escape for JSON if needed
590
+ if (codePoint < 0x20 || codePoint === 0x22 || codePoint === 0x5C) {
591
+ return trim(JSON.stringify(asString), 1);
592
+ }
593
+ return asString;
594
+ });
595
+ // TOML 1.1.0: Handle \e escape character (ESC = 0x1B)
596
+ const eEscapeSource = withHexEscapes;
597
+ withHexEscapes = eEscapeSource.replace(/\\e/g, (match, offset) => {
598
+ if (isBackslashEscaped(eEscapeSource, offset)) {
599
+ return match;
600
+ }
601
+ return '\\u001b';
602
+ });
568
603
  // JSON.parse handles everything except \UXXXXXXXX
569
604
  // replace those instances with code point, escape that, and then parse
570
605
  const LARGE_UNICODE = /\\U[a-fA-F0-9]{8}/g;
571
- const json_escaped = escaped.replace(LARGE_UNICODE, value => {
606
+ const json_escaped = withHexEscapes.replace(LARGE_UNICODE, value => {
572
607
  const code_point = parseInt(value.replace('\\U', ''), 16);
573
608
  const as_string = String.fromCodePoint(code_point);
574
609
  return trim(JSON.stringify(as_string), 1);
@@ -789,14 +824,14 @@ class DateFormatHelper {
789
824
  }
790
825
  // Patterns for different date/time formats
791
826
  DateFormatHelper.IS_DATE_ONLY = /^\d{4}-\d{2}-\d{2}$/;
792
- DateFormatHelper.IS_TIME_ONLY = /^\d{2}:\d{2}:\d{2}(?:\.\d+)?$/;
793
- DateFormatHelper.IS_LOCAL_DATETIME_T = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?$/;
794
- DateFormatHelper.IS_LOCAL_DATETIME_SPACE = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?:\.\d+)?$/;
795
- DateFormatHelper.IS_OFFSET_DATETIME_T = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:[Zz]|[+-]\d{2}:\d{2})$/;
796
- DateFormatHelper.IS_OFFSET_DATETIME_SPACE = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?:\.\d+)?(?:[Zz]|[+-]\d{2}:\d{2})$/;
827
+ 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+)?$/;
829
+ 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})$/;
831
+ DateFormatHelper.IS_OFFSET_DATETIME_SPACE = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}(?::\d{2})?(?:\.\d+)?(?:[Zz]|[+-]\d{2}:\d{2})$/;
797
832
  // Legacy patterns from parse-toml.ts (for compatibility)
798
833
  DateFormatHelper.IS_FULL_DATE = /(\d{4})-(\d{2})-(\d{2})/;
799
- DateFormatHelper.IS_FULL_TIME = /(\d{2}):(\d{2}):(\d{2})/;
834
+ DateFormatHelper.IS_FULL_TIME = /(\d{2}):(\d{2})(?::(\d{2}))?/;
800
835
  /**
801
836
  * Custom Date class for local dates (date-only).
802
837
  * Format: 1979-05-27
@@ -818,9 +853,15 @@ class LocalDate extends Date {
818
853
  */
819
854
  class LocalTime extends Date {
820
855
  constructor(value, originalFormat) {
821
- // For local time, use a fixed date (1970-01-01) and the provided time
856
+ // Normalize time to include seconds if missing (TOML 1.1.0 allows optional seconds)
857
+ let normalizedValue = value;
858
+ if (!/:\d{2}:\d{2}/.test(value)) {
859
+ // No seconds present, add :00
860
+ normalizedValue = value + ':00';
861
+ }
862
+ // For local time, use year 0000 as the base (TOML spec compliance)
822
863
  // Add 'Z' to ensure it's parsed as UTC regardless of system timezone
823
- super(`1970-01-01T${value}Z`);
864
+ super(`0000-01-01T${normalizedValue}Z`);
824
865
  this.originalFormat = originalFormat;
825
866
  }
826
867
  toISOString() {
@@ -852,8 +893,13 @@ class LocalTime extends Date {
852
893
  */
853
894
  class LocalDateTime extends Date {
854
895
  constructor(value, useSpaceSeparator = false, originalFormat) {
896
+ // Normalize time part to include seconds if missing (TOML 1.1.0 allows optional seconds)
897
+ let normalizedValue = value;
898
+ if (!/\d{2}:\d{2}:\d{2}/.test(value)) {
899
+ normalizedValue = value.replace(/(\d{2}:\d{2})([\s\-+TZ]|$)/, '$1:00$2');
900
+ }
855
901
  // Convert space to T for Date parsing, but remember the original format
856
- super(value.replace(' ', 'T') + 'Z');
902
+ super(normalizedValue.replace(' ', 'T') + 'Z');
857
903
  this.useSpaceSeparator = false;
858
904
  this.useSpaceSeparator = useSpaceSeparator;
859
905
  this.originalFormat = originalFormat || value;
@@ -892,7 +938,12 @@ class LocalDateTime extends Date {
892
938
  */
893
939
  class OffsetDateTime extends Date {
894
940
  constructor(value, useSpaceSeparator = false) {
895
- super(value.replace(' ', 'T'));
941
+ // Normalize time part to include seconds if missing (TOML 1.1.0 allows optional seconds)
942
+ let normalizedValue = value;
943
+ if (!/\d{2}:\d{2}:\d{2}/.test(value)) {
944
+ normalizedValue = value.replace(/(\d{2}:\d{2})([\s\-+TZ]|$)/, '$1:00$2');
945
+ }
946
+ super(normalizedValue.replace(' ', 'T'));
896
947
  this.useSpaceSeparator = false;
897
948
  this.useSpaceSeparator = useSpaceSeparator;
898
949
  this.originalFormat = value;
@@ -1018,7 +1069,9 @@ function* walkValue$1(cursor, input) {
1018
1069
  }
1019
1070
  }
1020
1071
  else if (cursor.value.type === TokenType.Curly) {
1021
- yield inlineTable(cursor, input);
1072
+ const [inline_table, comments] = inlineTable(cursor, input);
1073
+ yield inline_table;
1074
+ yield* comments;
1022
1075
  }
1023
1076
  else if (cursor.value.type === TokenType.Bracket) {
1024
1077
  const [inline_array, comments] = inlineArray(cursor, input);
@@ -1347,9 +1400,16 @@ function inlineTable(cursor, input) {
1347
1400
  loc: cloneLocation(cursor.value.loc),
1348
1401
  items: []
1349
1402
  };
1403
+ const comments = [];
1350
1404
  cursor.next();
1351
1405
  while (!cursor.done &&
1352
1406
  !(cursor.value.type === TokenType.Curly && cursor.value.raw === '}')) {
1407
+ // TOML 1.1.0: Handle comments in inline tables
1408
+ if (cursor.value.type === TokenType.Comment) {
1409
+ comments.push(comment(cursor));
1410
+ cursor.next();
1411
+ continue;
1412
+ }
1353
1413
  if (cursor.value.type === TokenType.Comma) {
1354
1414
  const previous = value.items[value.items.length - 1];
1355
1415
  if (!previous) {
@@ -1360,7 +1420,7 @@ function inlineTable(cursor, input) {
1360
1420
  cursor.next();
1361
1421
  continue;
1362
1422
  }
1363
- const [item] = walkBlock(cursor, input);
1423
+ const [item, ...additional_comments] = walkBlock(cursor, input);
1364
1424
  if (item.type !== NodeType.KeyValue) {
1365
1425
  throw new ParseError(input, cursor.value.loc.start, `Only key-values are supported in inline tables, found ${item.type}`);
1366
1426
  }
@@ -1371,6 +1431,7 @@ function inlineTable(cursor, input) {
1371
1431
  comma: false
1372
1432
  };
1373
1433
  value.items.push(inline_item);
1434
+ merge(comments, additional_comments);
1374
1435
  cursor.next();
1375
1436
  }
1376
1437
  if (cursor.done ||
@@ -1379,7 +1440,7 @@ function inlineTable(cursor, input) {
1379
1440
  throw new ParseError(input, cursor.done ? value.loc.start : cursor.value.loc.start, `Expected "}", found ${cursor.done ? 'end of file' : cursor.value.raw}`);
1380
1441
  }
1381
1442
  value.loc.end = cursor.value.loc.end;
1382
- return value;
1443
+ return [value, comments];
1383
1444
  }
1384
1445
  function inlineArray(cursor, input) {
1385
1446
  // 7. InlineArray
@@ -2272,6 +2333,7 @@ const DEFAULT_TRAILING_COMMA = false;
2272
2333
  const DEFAULT_BRACKET_SPACING = true;
2273
2334
  const DEFAULT_INLINE_TABLE_START = 1;
2274
2335
  const DEFAULT_TRUNCATE_ZERO_TIME_IN_DATES = false;
2336
+ const DEFAULT_USE_TABS_FOR_INDENTATION = false;
2275
2337
  // Detects if trailing commas are used in the existing TOML by examining the AST
2276
2338
  // Returns true if trailing commas are used, false if not or comma-separated structures found (ie. default to false)
2277
2339
  function detectTrailingComma(ast) {
@@ -2465,6 +2527,30 @@ function countTrailingNewlines(str, newlineChar) {
2465
2527
  }
2466
2528
  return count;
2467
2529
  }
2530
+ // Detects if tabs are used for indentation by checking the first few indented lines
2531
+ function detectTabsForIndentation(str) {
2532
+ const lines = str.split(/\r?\n/);
2533
+ let tabCount = 0;
2534
+ let spaceCount = 0;
2535
+ for (const line of lines) {
2536
+ // Skip empty lines
2537
+ if (line.length === 0)
2538
+ continue;
2539
+ // Check the first character of non-empty lines
2540
+ if (line[0] === '\t') {
2541
+ tabCount++;
2542
+ }
2543
+ else if (line[0] === ' ') {
2544
+ spaceCount++;
2545
+ }
2546
+ // If we've seen enough evidence, make a decision
2547
+ if (tabCount + spaceCount >= 5) {
2548
+ break;
2549
+ }
2550
+ }
2551
+ // Prefer tabs if we see more tabs than spaces
2552
+ return tabCount > spaceCount;
2553
+ }
2468
2554
  /**
2469
2555
  * Validates a format object and warns about unsupported properties.
2470
2556
  * Throws errors for supported properties with invalid types.
@@ -2475,61 +2561,62 @@ function validateFormatObject(format) {
2475
2561
  if (!format || typeof format !== 'object') {
2476
2562
  return {};
2477
2563
  }
2478
- const supportedProperties = new Set(['newLine', 'trailingNewline', 'trailingComma', 'bracketSpacing', 'inlineTableStart', 'truncateZeroTimeInDates']);
2564
+ const supportedProperties = new Set(['newLine', 'trailingNewline', 'trailingComma', 'bracketSpacing', 'inlineTableStart', 'truncateZeroTimeInDates', 'useTabsForIndentation']);
2479
2565
  const validatedFormat = {};
2480
2566
  const unsupportedProperties = [];
2481
2567
  const invalidTypeProperties = [];
2482
- // Check all enumerable properties of the format object
2568
+ // Check all enumerable properties of the format object, including properties
2569
+ // provided via the prototype chain (common in JS Object.create(...) patterns).
2483
2570
  for (const key in format) {
2484
- if (Object.prototype.hasOwnProperty.call(format, key)) {
2485
- if (supportedProperties.has(key)) {
2486
- const value = format[key];
2487
- // Type validation for each property
2488
- switch (key) {
2489
- case 'newLine':
2490
- if (typeof value === 'string') {
2491
- validatedFormat.newLine = value;
2492
- }
2493
- else {
2494
- invalidTypeProperties.push(`${key} (expected string, got ${typeof value})`);
2495
- }
2496
- break;
2497
- case 'trailingNewline':
2498
- if (typeof value === 'boolean' || typeof value === 'number') {
2499
- validatedFormat.trailingNewline = value;
2500
- }
2501
- else {
2502
- invalidTypeProperties.push(`${key} (expected boolean or number, got ${typeof value})`);
2503
- }
2504
- break;
2505
- case 'trailingComma':
2506
- case 'bracketSpacing':
2507
- case 'truncateZeroTimeInDates':
2508
- if (typeof value === 'boolean') {
2509
- validatedFormat[key] = value;
2510
- }
2511
- else {
2512
- invalidTypeProperties.push(`${key} (expected boolean, got ${typeof value})`);
2513
- }
2514
- break;
2515
- case 'inlineTableStart':
2516
- if (typeof value === 'number' && Number.isInteger(value) && value >= 0) {
2517
- validatedFormat.inlineTableStart = value;
2518
- }
2519
- else if (value === undefined || value === null) {
2520
- // Allow undefined/null to use default
2521
- validatedFormat.inlineTableStart = value;
2522
- }
2523
- else {
2524
- invalidTypeProperties.push(`${key} (expected non-negative integer or undefined, got ${typeof value})`);
2525
- }
2526
- break;
2527
- }
2528
- }
2529
- else {
2530
- unsupportedProperties.push(key);
2571
+ const isOwnEnumerable = Object.prototype.hasOwnProperty.call(format, key);
2572
+ if (supportedProperties.has(key)) {
2573
+ const value = format[key];
2574
+ // Type validation for each property
2575
+ switch (key) {
2576
+ case 'newLine':
2577
+ if (typeof value === 'string') {
2578
+ validatedFormat.newLine = value;
2579
+ }
2580
+ else {
2581
+ invalidTypeProperties.push(`${key} (expected string, got ${typeof value})`);
2582
+ }
2583
+ break;
2584
+ case 'trailingNewline':
2585
+ if (typeof value === 'boolean' || typeof value === 'number') {
2586
+ validatedFormat.trailingNewline = value;
2587
+ }
2588
+ else {
2589
+ invalidTypeProperties.push(`${key} (expected boolean or number, got ${typeof value})`);
2590
+ }
2591
+ break;
2592
+ case 'trailingComma':
2593
+ case 'bracketSpacing':
2594
+ case 'truncateZeroTimeInDates':
2595
+ case 'useTabsForIndentation':
2596
+ if (typeof value === 'boolean') {
2597
+ validatedFormat[key] = value;
2598
+ }
2599
+ else {
2600
+ invalidTypeProperties.push(`${key} (expected boolean, got ${typeof value})`);
2601
+ }
2602
+ break;
2603
+ case 'inlineTableStart':
2604
+ if (typeof value === 'number' && Number.isInteger(value) && value >= 0) {
2605
+ validatedFormat.inlineTableStart = value;
2606
+ }
2607
+ else if (value === undefined || value === null) {
2608
+ // Allow undefined/null to use default
2609
+ validatedFormat.inlineTableStart = value;
2610
+ }
2611
+ else {
2612
+ invalidTypeProperties.push(`${key} (expected non-negative integer or undefined, got ${typeof value})`);
2613
+ }
2614
+ break;
2531
2615
  }
2532
2616
  }
2617
+ else if (isOwnEnumerable) {
2618
+ unsupportedProperties.push(key);
2619
+ }
2533
2620
  }
2534
2621
  // Warn about unsupported properties
2535
2622
  if (unsupportedProperties.length > 0) {
@@ -2550,7 +2637,7 @@ function validateFormatObject(format) {
2550
2637
  * @returns A resolved TomlFormat instance
2551
2638
  */
2552
2639
  function resolveTomlFormat(format, fallbackFormat) {
2553
- var _a, _b, _c, _d, _e;
2640
+ var _a, _b, _c, _d, _e, _f;
2554
2641
  if (format) {
2555
2642
  // If format is provided, validate and merge it with fallback
2556
2643
  if (format instanceof TomlFormat) {
@@ -2560,7 +2647,7 @@ function resolveTomlFormat(format, fallbackFormat) {
2560
2647
  // Validate the format object and warn about unsupported properties
2561
2648
  const validatedFormat = validateFormatObject(format);
2562
2649
  // 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);
2650
+ 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
2651
  }
2565
2652
  }
2566
2653
  else {
@@ -2572,8 +2659,7 @@ class TomlFormat {
2572
2659
  // These options were part of the original TimHall's version and are not yet implemented
2573
2660
  //printWidth?: number;
2574
2661
  //tabWidth?: number;
2575
- //useTabs?: boolean;
2576
- constructor(newLine, trailingNewline, trailingComma, bracketSpacing, inlineTableStart, truncateZeroTimeInDates) {
2662
+ constructor(newLine, trailingNewline, trailingComma, bracketSpacing, inlineTableStart, truncateZeroTimeInDates, useTabsForIndentation) {
2577
2663
  // Use provided values or fall back to defaults
2578
2664
  this.newLine = newLine !== null && newLine !== void 0 ? newLine : DEFAULT_NEWLINE;
2579
2665
  this.trailingNewline = trailingNewline !== null && trailingNewline !== void 0 ? trailingNewline : DEFAULT_TRAILING_NEWLINE;
@@ -2581,6 +2667,7 @@ class TomlFormat {
2581
2667
  this.bracketSpacing = bracketSpacing !== null && bracketSpacing !== void 0 ? bracketSpacing : DEFAULT_BRACKET_SPACING;
2582
2668
  this.inlineTableStart = inlineTableStart !== null && inlineTableStart !== void 0 ? inlineTableStart : DEFAULT_INLINE_TABLE_START;
2583
2669
  this.truncateZeroTimeInDates = truncateZeroTimeInDates !== null && truncateZeroTimeInDates !== void 0 ? truncateZeroTimeInDates : DEFAULT_TRUNCATE_ZERO_TIME_IN_DATES;
2670
+ this.useTabsForIndentation = useTabsForIndentation !== null && useTabsForIndentation !== void 0 ? useTabsForIndentation : DEFAULT_USE_TABS_FOR_INDENTATION;
2584
2671
  }
2585
2672
  /**
2586
2673
  * Creates a new TomlFormat instance with default formatting preferences.
@@ -2594,7 +2681,7 @@ class TomlFormat {
2594
2681
  * - truncateZeroTimeInDates: false
2595
2682
  */
2596
2683
  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);
2684
+ 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
2685
  }
2599
2686
  /**
2600
2687
  * Auto-detects formatting preferences from an existing TOML string.
@@ -2634,6 +2721,8 @@ class TomlFormat {
2634
2721
  format.trailingComma = DEFAULT_TRAILING_COMMA;
2635
2722
  format.bracketSpacing = DEFAULT_BRACKET_SPACING;
2636
2723
  }
2724
+ // Detect if tabs are used for indentation
2725
+ format.useTabsForIndentation = detectTabsForIndentation(tomlString);
2637
2726
  // inlineTableStart uses default value since auto-detection would require
2638
2727
  // complex analysis of nested table formatting preferences
2639
2728
  format.inlineTableStart = DEFAULT_INLINE_TABLE_START;
@@ -2963,71 +3052,85 @@ const BY_NEW_LINE = /(\r\n|\n)/g;
2963
3052
  * It preserves the original formatting, spacing, and structure of the TOML file.
2964
3053
  *
2965
3054
  * @param ast - The Abstract Syntax Tree representing the parsed TOML document
2966
- * @param newline - The newline character(s) to use (\n by default)
2967
- * @param options - Optional configuration object
2968
- * @param options.trailingNewline - Number of trailing newlines to add (1 by default)
3055
+ * @param format - The formatting options to use for the output
2969
3056
  * @returns The reconstructed TOML document as a string
2970
3057
  *
2971
3058
  * @example
2972
3059
  * ```typescript
2973
- * const tomlString = toTOML(ast, '\n', { trailingNewline: 1 });
3060
+ * const tomlString = toTOML(ast, TomlFormat.default());
2974
3061
  * ```
2975
3062
  */
2976
3063
  function toTOML(ast, format) {
2977
3064
  const lines = [];
3065
+ const paddingChar = format.useTabsForIndentation ? '\t' : SPACE;
2978
3066
  traverse(ast, {
2979
3067
  [NodeType.TableKey](node) {
2980
3068
  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 }, ']');
3069
+ write(lines, { start, end: { line: start.line, column: start.column + 1 } }, '[', paddingChar);
3070
+ write(lines, { start: { line: end.line, column: end.column - 1 }, end }, ']', paddingChar);
2983
3071
  },
2984
3072
  [NodeType.TableArrayKey](node) {
2985
3073
  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 }, ']]');
3074
+ write(lines, { start, end: { line: start.line, column: start.column + 2 } }, '[[', paddingChar);
3075
+ write(lines, { start: { line: end.line, column: end.column - 2 }, end }, ']]', paddingChar);
2988
3076
  },
2989
3077
  [NodeType.KeyValue](node) {
2990
3078
  const { start: { line } } = node.loc;
2991
- write(lines, { start: { line, column: node.equals }, end: { line, column: node.equals + 1 } }, '=');
3079
+ write(lines, { start: { line, column: node.equals }, end: { line, column: node.equals + 1 } }, '=', paddingChar);
2992
3080
  },
2993
3081
  [NodeType.Key](node) {
2994
- write(lines, node.loc, node.raw);
3082
+ write(lines, node.loc, node.raw, paddingChar);
2995
3083
  },
2996
3084
  [NodeType.String](node) {
2997
- write(lines, node.loc, node.raw);
3085
+ write(lines, node.loc, node.raw, paddingChar);
2998
3086
  },
2999
3087
  [NodeType.Integer](node) {
3000
- write(lines, node.loc, node.raw);
3088
+ write(lines, node.loc, node.raw, paddingChar);
3001
3089
  },
3002
3090
  [NodeType.Float](node) {
3003
- write(lines, node.loc, node.raw);
3091
+ write(lines, node.loc, node.raw, paddingChar);
3004
3092
  },
3005
3093
  [NodeType.Boolean](node) {
3006
- write(lines, node.loc, node.value.toString());
3094
+ write(lines, node.loc, node.value.toString(), paddingChar);
3007
3095
  },
3008
3096
  [NodeType.DateTime](node) {
3009
- write(lines, node.loc, node.raw);
3097
+ write(lines, node.loc, node.raw, paddingChar);
3010
3098
  },
3011
3099
  [NodeType.InlineArray](node) {
3012
3100
  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 }, ']');
3101
+ write(lines, { start, end: { line: start.line, column: start.column + 1 } }, '[', paddingChar);
3102
+ write(lines, { start: { line: end.line, column: end.column - 1 }, end }, ']', paddingChar);
3015
3103
  },
3016
3104
  [NodeType.InlineTable](node) {
3017
3105
  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 }, '}');
3106
+ write(lines, { start, end: { line: start.line, column: start.column + 1 } }, '{', paddingChar);
3107
+ write(lines, { start: { line: end.line, column: end.column - 1 }, end }, '}', paddingChar);
3020
3108
  },
3021
3109
  [NodeType.InlineItem](node) {
3022
3110
  if (!node.comma)
3023
3111
  return;
3024
3112
  const start = node.loc.end;
3025
- write(lines, { start, end: { line: start.line, column: start.column + 1 } }, ',');
3113
+ write(lines, { start, end: { line: start.line, column: start.column + 1 } }, ',', paddingChar);
3026
3114
  },
3027
3115
  [NodeType.Comment](node) {
3028
- write(lines, node.loc, node.raw);
3116
+ write(lines, node.loc, node.raw, paddingChar);
3029
3117
  }
3030
3118
  });
3119
+ // Post-process: convert leading spaces to tabs if useTabsForIndentation is enabled
3120
+ if (format.useTabsForIndentation) {
3121
+ for (let i = 0; i < lines.length; i++) {
3122
+ const line = lines[i];
3123
+ // Find the leading whitespace
3124
+ const match = line.match(/^( +)/);
3125
+ if (match) {
3126
+ const leadingSpaces = match[1];
3127
+ // Replace entire leading space sequence with equivalent tabs
3128
+ // Each space becomes a tab (preserving the visual width)
3129
+ const leadingTabs = '\t'.repeat(leadingSpaces.length);
3130
+ lines[i] = leadingTabs + line.substring(leadingSpaces.length);
3131
+ }
3132
+ }
3133
+ }
3031
3134
  return lines.join(format.newLine) + format.newLine.repeat(format.trailingNewline);
3032
3135
  }
3033
3136
  /**
@@ -3044,6 +3147,7 @@ function toTOML(ast, format) {
3044
3147
  * - end: { line: number, column: number } - Ending position (1-indexed line, 0-indexed column)
3045
3148
  * @param raw - The raw string content to write at the specified location.
3046
3149
  * Can contain multiple lines separated by \n or \r\n.
3150
+ * @param paddingChar - The character to use for padding (space or tab)
3047
3151
  *
3048
3152
  * @throws {Error} When there's a mismatch between location span and raw string line count
3049
3153
  * @throws {Error} When attempting to write to an uninitialized line
@@ -3052,11 +3156,11 @@ function toTOML(ast, format) {
3052
3156
  * ```typescript
3053
3157
  * const lines = ['', ''];
3054
3158
  * const location = { start: { line: 1, column: 0 }, end: { line: 1, column: 3 } };
3055
- * write(lines, location, 'key');
3159
+ * write(lines, location, 'key', ' ');
3056
3160
  * // Result: lines[0] becomes 'key'
3057
3161
  * ```
3058
3162
  */
3059
- function write(lines, loc, raw) {
3163
+ function write(lines, loc, raw, paddingChar = SPACE) {
3060
3164
  const raw_lines = raw.split(BY_NEW_LINE).filter(line => line !== '\n' && line !== '\r\n');
3061
3165
  const expected_lines = loc.end.line - loc.start.line + 1;
3062
3166
  if (raw_lines.length !== expected_lines) {
@@ -3070,10 +3174,19 @@ function write(lines, loc, raw) {
3070
3174
  }
3071
3175
  const is_start_line = i === loc.start.line;
3072
3176
  const is_end_line = i === loc.end.line;
3073
- const before = is_start_line
3074
- ? line.substr(0, loc.start.column).padEnd(loc.start.column, SPACE)
3075
- : '';
3076
- const after = is_end_line ? line.substr(loc.end.column) : '';
3177
+ let before = '';
3178
+ if (is_start_line) {
3179
+ const existingBefore = line.substring(0, loc.start.column);
3180
+ if (existingBefore.length < loc.start.column) {
3181
+ // Need to pad - always use spaces during write phase
3182
+ // Tab conversion happens in post-processing for leading indentation only
3183
+ before = existingBefore.padEnd(loc.start.column, SPACE);
3184
+ }
3185
+ else {
3186
+ before = existingBefore;
3187
+ }
3188
+ }
3189
+ const after = is_end_line ? line.substring(loc.end.column) : '';
3077
3190
  lines[i - 1] = before + raw_lines[i - loc.start.line] + after;
3078
3191
  }
3079
3192
  }
@@ -3191,11 +3304,15 @@ function toValue(node) {
3191
3304
  return result;
3192
3305
  case NodeType.InlineArray:
3193
3306
  return node.items.map(item => toValue(item.item));
3307
+ case NodeType.DateTime:
3308
+ // Preserve TOML date/time custom classes so format is retained when
3309
+ // round-tripping through stringify() (e.g. date-only, time-only, local vs offset).
3310
+ // These classes extend Date, so JS users can still treat them as Dates.
3311
+ return node.value;
3194
3312
  case NodeType.String:
3195
3313
  case NodeType.Integer:
3196
3314
  case NodeType.Float:
3197
3315
  case NodeType.Boolean:
3198
- case NodeType.DateTime:
3199
3316
  return node.value;
3200
3317
  default:
3201
3318
  throw new Error(`Unrecognized value type "${node.type}"`);
@@ -3435,13 +3552,29 @@ function findByPath(node, path) {
3435
3552
  key = key.concat(array_index);
3436
3553
  }
3437
3554
  else if (isInlineItem(item) && isKeyValue(item.item)) {
3555
+ // For InlineItems wrapping KeyValues, extract the key
3438
3556
  key = item.item.key.value;
3439
3557
  }
3440
3558
  else if (isInlineItem(item)) {
3441
3559
  key = [index];
3442
3560
  }
3443
3561
  if (key.length && arraysEqual(key, path.slice(0, key.length))) {
3444
- found = findByPath(item, path.slice(key.length));
3562
+ // For InlineItems containing KeyValues, we need to search within the value
3563
+ // but still return the InlineItem or its contents appropriately
3564
+ if (isInlineItem(item) && isKeyValue(item.item)) {
3565
+ if (path.length === key.length) {
3566
+ // If we've matched the full path, return the InlineItem itself
3567
+ // so it can be found and replaced in the parent's items array
3568
+ found = item;
3569
+ }
3570
+ else {
3571
+ // Continue searching within the KeyValue's value
3572
+ found = findByPath(item.item.value, path.slice(key.length));
3573
+ }
3574
+ }
3575
+ else {
3576
+ found = findByPath(item, path.slice(key.length));
3577
+ }
3445
3578
  return true;
3446
3579
  }
3447
3580
  else {
@@ -3728,7 +3861,13 @@ function applyChanges(original, updated, changes, format) {
3728
3861
  insert(original, parent, child, undefined, true);
3729
3862
  }
3730
3863
  else {
3731
- insert(original, parent, child);
3864
+ // Unwrap InlineItem if we're adding to a Table (not InlineTable)
3865
+ // InlineItems should only exist within InlineTables or InlineArrays
3866
+ let childToInsert = child;
3867
+ if (isInlineItem(child) && (isTable(parent) || isDocument(parent))) {
3868
+ childToInsert = child.item;
3869
+ }
3870
+ insert(original, parent, childToInsert);
3732
3871
  }
3733
3872
  }
3734
3873
  }
@@ -3757,6 +3896,14 @@ function applyChanges(original, updated, changes, format) {
3757
3896
  parent = existing;
3758
3897
  existing = existing.item;
3759
3898
  }
3899
+ else if (isInlineItem(existing) && isInlineItem(replacement) && isKeyValue(existing.item) && isKeyValue(replacement.item)) {
3900
+ // Both are InlineItems wrapping KeyValues (nested inline table edits)
3901
+ // Preserve formatting and edit the value within
3902
+ preserveFormatting(existing.item.value, replacement.item.value);
3903
+ parent = existing.item;
3904
+ existing = existing.item.value;
3905
+ replacement = replacement.item.value;
3906
+ }
3760
3907
  else {
3761
3908
  parent = findParent(original, change.path);
3762
3909
  // Special handling for array element edits