@decimalturn/toml-patch 0.5.1 → 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.1 - 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;
@@ -948,7 +999,7 @@ class OffsetDateTime extends Date {
948
999
  }
949
1000
 
950
1001
  // Create a shorter alias for convenience
951
- const dateFormatHelper$1 = DateFormatHelper;
1002
+ const dateFormatHelper = DateFormatHelper;
952
1003
  const TRUE = 'true';
953
1004
  const FALSE = 'false';
954
1005
  const HAS_E = /e/i;
@@ -1004,7 +1055,7 @@ function* walkValue$1(cursor, input) {
1004
1055
  else if (cursor.value.raw === TRUE || cursor.value.raw === FALSE) {
1005
1056
  yield boolean(cursor);
1006
1057
  }
1007
- else if (dateFormatHelper$1.IS_FULL_DATE.test(cursor.value.raw) || dateFormatHelper$1.IS_FULL_TIME.test(cursor.value.raw)) {
1058
+ else if (dateFormatHelper.IS_FULL_DATE.test(cursor.value.raw) || dateFormatHelper.IS_FULL_TIME.test(cursor.value.raw)) {
1008
1059
  yield datetime(cursor, input);
1009
1060
  }
1010
1061
  else if ((!cursor.peek().done && cursor.peek().value.type === TokenType.Dot) ||
@@ -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);
@@ -1217,8 +1270,8 @@ function datetime(cursor, input) {
1217
1270
  // check if raw is full date and following is full time
1218
1271
  if (!cursor.peek().done &&
1219
1272
  cursor.peek().value.type === TokenType.Literal &&
1220
- dateFormatHelper$1.IS_FULL_DATE.test(raw) &&
1221
- dateFormatHelper$1.IS_FULL_TIME.test(cursor.peek().value.raw)) {
1273
+ dateFormatHelper.IS_FULL_DATE.test(raw) &&
1274
+ dateFormatHelper.IS_FULL_TIME.test(cursor.peek().value.raw)) {
1222
1275
  const start = loc.start;
1223
1276
  cursor.next();
1224
1277
  loc = { start, end: cursor.value.loc.end };
@@ -1234,9 +1287,9 @@ function datetime(cursor, input) {
1234
1287
  loc = { start, end: cursor.value.loc.end };
1235
1288
  raw += `.${cursor.value.raw}`;
1236
1289
  }
1237
- if (!dateFormatHelper$1.IS_FULL_DATE.test(raw)) {
1290
+ if (!dateFormatHelper.IS_FULL_DATE.test(raw)) {
1238
1291
  // Local time only (e.g., "07:32:00" or "07:32:00.999")
1239
- if (dateFormatHelper$1.IS_TIME_ONLY.test(raw)) {
1292
+ if (dateFormatHelper.IS_TIME_ONLY.test(raw)) {
1240
1293
  value = new LocalTime(raw, raw);
1241
1294
  }
1242
1295
  else {
@@ -1245,23 +1298,23 @@ function datetime(cursor, input) {
1245
1298
  value = new Date(`${local_date}T${raw}`);
1246
1299
  }
1247
1300
  }
1248
- else if (dateFormatHelper$1.IS_DATE_ONLY.test(raw)) {
1301
+ else if (dateFormatHelper.IS_DATE_ONLY.test(raw)) {
1249
1302
  // Local date only (e.g., "1979-05-27")
1250
1303
  value = new LocalDate(raw);
1251
1304
  }
1252
- else if (dateFormatHelper$1.IS_LOCAL_DATETIME_T.test(raw)) {
1305
+ else if (dateFormatHelper.IS_LOCAL_DATETIME_T.test(raw)) {
1253
1306
  // Local datetime with T separator (e.g., "1979-05-27T07:32:00")
1254
1307
  value = new LocalDateTime(raw, false);
1255
1308
  }
1256
- else if (dateFormatHelper$1.IS_LOCAL_DATETIME_SPACE.test(raw)) {
1309
+ else if (dateFormatHelper.IS_LOCAL_DATETIME_SPACE.test(raw)) {
1257
1310
  // Local datetime with space separator (e.g., "1979-05-27 07:32:00")
1258
1311
  value = new LocalDateTime(raw, true);
1259
1312
  }
1260
- else if (dateFormatHelper$1.IS_OFFSET_DATETIME_T.test(raw)) {
1313
+ else if (dateFormatHelper.IS_OFFSET_DATETIME_T.test(raw)) {
1261
1314
  // Offset datetime with T separator (e.g., "1979-05-27T07:32:00Z" or "1979-05-27T07:32:00-07:00")
1262
1315
  value = new OffsetDateTime(raw, false);
1263
1316
  }
1264
- else if (dateFormatHelper$1.IS_OFFSET_DATETIME_SPACE.test(raw)) {
1317
+ else if (dateFormatHelper.IS_OFFSET_DATETIME_SPACE.test(raw)) {
1265
1318
  // Offset datetime with space separator (e.g., "1979-05-27 07:32:00Z")
1266
1319
  value = new OffsetDateTime(raw, true);
1267
1320
  }
@@ -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
@@ -1499,8 +1560,6 @@ function traverse(ast, visitor) {
1499
1560
  }
1500
1561
  }
1501
1562
 
1502
- // Create a shorter alias for convenience
1503
- const dateFormatHelper = DateFormatHelper;
1504
1563
  const enter_offsets = new WeakMap();
1505
1564
  const getEnterOffsets = (root) => {
1506
1565
  if (!enter_offsets.has(root)) {
@@ -1515,65 +1574,8 @@ const getExitOffsets = (root) => {
1515
1574
  }
1516
1575
  return exit_offsets.get(root);
1517
1576
  };
1518
- /**
1519
- * Updates location information for a replacement string to match the existing string's position.
1520
- * The actual formatting and escaping is done by generateString().
1521
- *
1522
- * @param existing - The existing string node with the original format and location
1523
- * @param replacement - The replacement string node (already fully formatted by generateString)
1524
- * @returns The replacement string node with updated location information
1525
- */
1526
- function fixStringLocation(existing, replacement) {
1527
- if (!isMultilineString(replacement.raw)) {
1528
- return replacement;
1529
- }
1530
- // Detect newline character to count lines correctly
1531
- const newlineChar = replacement.raw.includes('\r\n') ? '\r\n' : '\n';
1532
- const lineCount = (replacement.raw.match(new RegExp(newlineChar === '\r\n' ? '\\r\\n' : '\\n', 'g')) || []).length;
1533
- // Update location to match existing position and account for multiple lines
1534
- if (lineCount > 0) {
1535
- return Object.assign(Object.assign({}, replacement), { loc: {
1536
- start: existing.loc.start,
1537
- end: {
1538
- line: existing.loc.start.line + lineCount,
1539
- column: 3 // length of delimiter
1540
- }
1541
- } });
1542
- }
1543
- else {
1544
- return Object.assign(Object.assign({}, replacement), { loc: {
1545
- start: existing.loc.start,
1546
- end: {
1547
- line: existing.loc.start.line,
1548
- column: existing.loc.start.column + replacement.raw.length
1549
- }
1550
- } });
1551
- }
1552
- }
1553
1577
  //TODO: Add getOffsets function to get all offsets contained in the tree
1554
1578
  function replace(root, parent, existing, replacement) {
1555
- // Special handling for String nodes to preserve multiline format by editing replacement values
1556
- if (isString$1(existing) && isString$1(replacement)) {
1557
- // Regenerate the replacement with proper escaping based on existing format
1558
- const escapedReplacement = generateString(replacement.value, existing.raw);
1559
- replacement = fixStringLocation(existing, escapedReplacement);
1560
- }
1561
- // Special handling for DateTime nodes to preserve original format by editing replacement values
1562
- if (isDateTime(existing) && isDateTime(replacement)) {
1563
- // Analyze the original raw format and create a properly formatted replacement
1564
- const originalRaw = existing.raw;
1565
- const newValue = replacement.value;
1566
- // Create a new date with the original format preserved
1567
- const formattedDate = dateFormatHelper.createDateWithOriginalFormat(newValue, originalRaw);
1568
- // Update the replacement with the properly formatted date
1569
- replacement.value = formattedDate;
1570
- replacement.raw = formattedDate.toISOString();
1571
- // Adjust the location information to match the new raw length
1572
- const lengthDiff = replacement.raw.length - originalRaw.length;
1573
- if (lengthDiff !== 0) {
1574
- replacement.loc.end.column = replacement.loc.start.column + replacement.raw.length;
1575
- }
1576
- }
1577
1579
  // First, replace existing node
1578
1580
  // (by index for items, item, or key/value)
1579
1581
  if (hasItems(parent)) {
@@ -2210,13 +2212,37 @@ function generateString(value, existingRaw) {
2210
2212
  else {
2211
2213
  raw = JSON.stringify(value);
2212
2214
  }
2215
+ // Calculate proper end location for multiline strings
2216
+ let endLocation;
2217
+ if (raw.includes('\r\n') || (raw.includes('\n') && !raw.includes('\r\n'))) {
2218
+ const newlineChar = raw.includes('\r\n') ? '\r\n' : '\n';
2219
+ const lineCount = (raw.match(new RegExp(newlineChar === '\r\n' ? '\\r\\n' : '\\n', 'g')) || []).length;
2220
+ if (lineCount > 0) {
2221
+ endLocation = {
2222
+ line: 1 + lineCount,
2223
+ column: 3 // length of delimiter (""" or ''')
2224
+ };
2225
+ }
2226
+ else {
2227
+ endLocation = { line: 1, column: raw.length };
2228
+ }
2229
+ }
2230
+ else {
2231
+ endLocation = { line: 1, column: raw.length };
2232
+ }
2213
2233
  return {
2214
2234
  type: NodeType.String,
2215
- loc: { start: zero(), end: { line: 1, column: raw.length } },
2235
+ loc: { start: zero(), end: endLocation },
2216
2236
  raw,
2217
2237
  value
2218
2238
  };
2219
2239
  }
2240
+ /**
2241
+ * Generates a new Integer node.
2242
+ *
2243
+ * @param value - The integer value.
2244
+ * @returns A new Integer node.
2245
+ */
2220
2246
  function generateInteger(value) {
2221
2247
  const raw = value.toString();
2222
2248
  return {
@@ -2307,6 +2333,7 @@ const DEFAULT_TRAILING_COMMA = false;
2307
2333
  const DEFAULT_BRACKET_SPACING = true;
2308
2334
  const DEFAULT_INLINE_TABLE_START = 1;
2309
2335
  const DEFAULT_TRUNCATE_ZERO_TIME_IN_DATES = false;
2336
+ const DEFAULT_USE_TABS_FOR_INDENTATION = false;
2310
2337
  // Detects if trailing commas are used in the existing TOML by examining the AST
2311
2338
  // Returns true if trailing commas are used, false if not or comma-separated structures found (ie. default to false)
2312
2339
  function detectTrailingComma(ast) {
@@ -2500,6 +2527,30 @@ function countTrailingNewlines(str, newlineChar) {
2500
2527
  }
2501
2528
  return count;
2502
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
+ }
2503
2554
  /**
2504
2555
  * Validates a format object and warns about unsupported properties.
2505
2556
  * Throws errors for supported properties with invalid types.
@@ -2510,61 +2561,62 @@ function validateFormatObject(format) {
2510
2561
  if (!format || typeof format !== 'object') {
2511
2562
  return {};
2512
2563
  }
2513
- const supportedProperties = new Set(['newLine', 'trailingNewline', 'trailingComma', 'bracketSpacing', 'inlineTableStart', 'truncateZeroTimeInDates']);
2564
+ const supportedProperties = new Set(['newLine', 'trailingNewline', 'trailingComma', 'bracketSpacing', 'inlineTableStart', 'truncateZeroTimeInDates', 'useTabsForIndentation']);
2514
2565
  const validatedFormat = {};
2515
2566
  const unsupportedProperties = [];
2516
2567
  const invalidTypeProperties = [];
2517
- // 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).
2518
2570
  for (const key in format) {
2519
- if (Object.prototype.hasOwnProperty.call(format, key)) {
2520
- if (supportedProperties.has(key)) {
2521
- const value = format[key];
2522
- // Type validation for each property
2523
- switch (key) {
2524
- case 'newLine':
2525
- if (typeof value === 'string') {
2526
- validatedFormat.newLine = value;
2527
- }
2528
- else {
2529
- invalidTypeProperties.push(`${key} (expected string, got ${typeof value})`);
2530
- }
2531
- break;
2532
- case 'trailingNewline':
2533
- if (typeof value === 'boolean' || typeof value === 'number') {
2534
- validatedFormat.trailingNewline = value;
2535
- }
2536
- else {
2537
- invalidTypeProperties.push(`${key} (expected boolean or number, got ${typeof value})`);
2538
- }
2539
- break;
2540
- case 'trailingComma':
2541
- case 'bracketSpacing':
2542
- case 'truncateZeroTimeInDates':
2543
- if (typeof value === 'boolean') {
2544
- validatedFormat[key] = value;
2545
- }
2546
- else {
2547
- invalidTypeProperties.push(`${key} (expected boolean, got ${typeof value})`);
2548
- }
2549
- break;
2550
- case 'inlineTableStart':
2551
- if (typeof value === 'number' && Number.isInteger(value) && value >= 0) {
2552
- validatedFormat.inlineTableStart = value;
2553
- }
2554
- else if (value === undefined || value === null) {
2555
- // Allow undefined/null to use default
2556
- validatedFormat.inlineTableStart = value;
2557
- }
2558
- else {
2559
- invalidTypeProperties.push(`${key} (expected non-negative integer or undefined, got ${typeof value})`);
2560
- }
2561
- break;
2562
- }
2563
- }
2564
- else {
2565
- 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;
2566
2615
  }
2567
2616
  }
2617
+ else if (isOwnEnumerable) {
2618
+ unsupportedProperties.push(key);
2619
+ }
2568
2620
  }
2569
2621
  // Warn about unsupported properties
2570
2622
  if (unsupportedProperties.length > 0) {
@@ -2585,7 +2637,7 @@ function validateFormatObject(format) {
2585
2637
  * @returns A resolved TomlFormat instance
2586
2638
  */
2587
2639
  function resolveTomlFormat(format, fallbackFormat) {
2588
- var _a, _b, _c, _d, _e;
2640
+ var _a, _b, _c, _d, _e, _f;
2589
2641
  if (format) {
2590
2642
  // If format is provided, validate and merge it with fallback
2591
2643
  if (format instanceof TomlFormat) {
@@ -2595,7 +2647,7 @@ function resolveTomlFormat(format, fallbackFormat) {
2595
2647
  // Validate the format object and warn about unsupported properties
2596
2648
  const validatedFormat = validateFormatObject(format);
2597
2649
  // Create a new TomlFormat instance with validated properties
2598
- 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);
2599
2651
  }
2600
2652
  }
2601
2653
  else {
@@ -2607,8 +2659,7 @@ class TomlFormat {
2607
2659
  // These options were part of the original TimHall's version and are not yet implemented
2608
2660
  //printWidth?: number;
2609
2661
  //tabWidth?: number;
2610
- //useTabs?: boolean;
2611
- constructor(newLine, trailingNewline, trailingComma, bracketSpacing, inlineTableStart, truncateZeroTimeInDates) {
2662
+ constructor(newLine, trailingNewline, trailingComma, bracketSpacing, inlineTableStart, truncateZeroTimeInDates, useTabsForIndentation) {
2612
2663
  // Use provided values or fall back to defaults
2613
2664
  this.newLine = newLine !== null && newLine !== void 0 ? newLine : DEFAULT_NEWLINE;
2614
2665
  this.trailingNewline = trailingNewline !== null && trailingNewline !== void 0 ? trailingNewline : DEFAULT_TRAILING_NEWLINE;
@@ -2616,6 +2667,7 @@ class TomlFormat {
2616
2667
  this.bracketSpacing = bracketSpacing !== null && bracketSpacing !== void 0 ? bracketSpacing : DEFAULT_BRACKET_SPACING;
2617
2668
  this.inlineTableStart = inlineTableStart !== null && inlineTableStart !== void 0 ? inlineTableStart : DEFAULT_INLINE_TABLE_START;
2618
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;
2619
2671
  }
2620
2672
  /**
2621
2673
  * Creates a new TomlFormat instance with default formatting preferences.
@@ -2629,7 +2681,7 @@ class TomlFormat {
2629
2681
  * - truncateZeroTimeInDates: false
2630
2682
  */
2631
2683
  static default() {
2632
- 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);
2633
2685
  }
2634
2686
  /**
2635
2687
  * Auto-detects formatting preferences from an existing TOML string.
@@ -2669,6 +2721,8 @@ class TomlFormat {
2669
2721
  format.trailingComma = DEFAULT_TRAILING_COMMA;
2670
2722
  format.bracketSpacing = DEFAULT_BRACKET_SPACING;
2671
2723
  }
2724
+ // Detect if tabs are used for indentation
2725
+ format.useTabsForIndentation = detectTabsForIndentation(tomlString);
2672
2726
  // inlineTableStart uses default value since auto-detection would require
2673
2727
  // complex analysis of nested table formatting preferences
2674
2728
  format.inlineTableStart = DEFAULT_INLINE_TABLE_START;
@@ -2998,71 +3052,85 @@ const BY_NEW_LINE = /(\r\n|\n)/g;
2998
3052
  * It preserves the original formatting, spacing, and structure of the TOML file.
2999
3053
  *
3000
3054
  * @param ast - The Abstract Syntax Tree representing the parsed TOML document
3001
- * @param newline - The newline character(s) to use (\n by default)
3002
- * @param options - Optional configuration object
3003
- * @param options.trailingNewline - Number of trailing newlines to add (1 by default)
3055
+ * @param format - The formatting options to use for the output
3004
3056
  * @returns The reconstructed TOML document as a string
3005
3057
  *
3006
3058
  * @example
3007
3059
  * ```typescript
3008
- * const tomlString = toTOML(ast, '\n', { trailingNewline: 1 });
3060
+ * const tomlString = toTOML(ast, TomlFormat.default());
3009
3061
  * ```
3010
3062
  */
3011
3063
  function toTOML(ast, format) {
3012
3064
  const lines = [];
3065
+ const paddingChar = format.useTabsForIndentation ? '\t' : SPACE;
3013
3066
  traverse(ast, {
3014
3067
  [NodeType.TableKey](node) {
3015
3068
  const { start, end } = node.loc;
3016
- write(lines, { start, end: { line: start.line, column: start.column + 1 } }, '[');
3017
- 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);
3018
3071
  },
3019
3072
  [NodeType.TableArrayKey](node) {
3020
3073
  const { start, end } = node.loc;
3021
- write(lines, { start, end: { line: start.line, column: start.column + 2 } }, '[[');
3022
- 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);
3023
3076
  },
3024
3077
  [NodeType.KeyValue](node) {
3025
3078
  const { start: { line } } = node.loc;
3026
- 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);
3027
3080
  },
3028
3081
  [NodeType.Key](node) {
3029
- write(lines, node.loc, node.raw);
3082
+ write(lines, node.loc, node.raw, paddingChar);
3030
3083
  },
3031
3084
  [NodeType.String](node) {
3032
- write(lines, node.loc, node.raw);
3085
+ write(lines, node.loc, node.raw, paddingChar);
3033
3086
  },
3034
3087
  [NodeType.Integer](node) {
3035
- write(lines, node.loc, node.raw);
3088
+ write(lines, node.loc, node.raw, paddingChar);
3036
3089
  },
3037
3090
  [NodeType.Float](node) {
3038
- write(lines, node.loc, node.raw);
3091
+ write(lines, node.loc, node.raw, paddingChar);
3039
3092
  },
3040
3093
  [NodeType.Boolean](node) {
3041
- write(lines, node.loc, node.value.toString());
3094
+ write(lines, node.loc, node.value.toString(), paddingChar);
3042
3095
  },
3043
3096
  [NodeType.DateTime](node) {
3044
- write(lines, node.loc, node.raw);
3097
+ write(lines, node.loc, node.raw, paddingChar);
3045
3098
  },
3046
3099
  [NodeType.InlineArray](node) {
3047
3100
  const { start, end } = node.loc;
3048
- write(lines, { start, end: { line: start.line, column: start.column + 1 } }, '[');
3049
- 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);
3050
3103
  },
3051
3104
  [NodeType.InlineTable](node) {
3052
3105
  const { start, end } = node.loc;
3053
- write(lines, { start, end: { line: start.line, column: start.column + 1 } }, '{');
3054
- 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);
3055
3108
  },
3056
3109
  [NodeType.InlineItem](node) {
3057
3110
  if (!node.comma)
3058
3111
  return;
3059
3112
  const start = node.loc.end;
3060
- write(lines, { start, end: { line: start.line, column: start.column + 1 } }, ',');
3113
+ write(lines, { start, end: { line: start.line, column: start.column + 1 } }, ',', paddingChar);
3061
3114
  },
3062
3115
  [NodeType.Comment](node) {
3063
- write(lines, node.loc, node.raw);
3116
+ write(lines, node.loc, node.raw, paddingChar);
3064
3117
  }
3065
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
+ }
3066
3134
  return lines.join(format.newLine) + format.newLine.repeat(format.trailingNewline);
3067
3135
  }
3068
3136
  /**
@@ -3079,6 +3147,7 @@ function toTOML(ast, format) {
3079
3147
  * - end: { line: number, column: number } - Ending position (1-indexed line, 0-indexed column)
3080
3148
  * @param raw - The raw string content to write at the specified location.
3081
3149
  * Can contain multiple lines separated by \n or \r\n.
3150
+ * @param paddingChar - The character to use for padding (space or tab)
3082
3151
  *
3083
3152
  * @throws {Error} When there's a mismatch between location span and raw string line count
3084
3153
  * @throws {Error} When attempting to write to an uninitialized line
@@ -3087,11 +3156,11 @@ function toTOML(ast, format) {
3087
3156
  * ```typescript
3088
3157
  * const lines = ['', ''];
3089
3158
  * const location = { start: { line: 1, column: 0 }, end: { line: 1, column: 3 } };
3090
- * write(lines, location, 'key');
3159
+ * write(lines, location, 'key', ' ');
3091
3160
  * // Result: lines[0] becomes 'key'
3092
3161
  * ```
3093
3162
  */
3094
- function write(lines, loc, raw) {
3163
+ function write(lines, loc, raw, paddingChar = SPACE) {
3095
3164
  const raw_lines = raw.split(BY_NEW_LINE).filter(line => line !== '\n' && line !== '\r\n');
3096
3165
  const expected_lines = loc.end.line - loc.start.line + 1;
3097
3166
  if (raw_lines.length !== expected_lines) {
@@ -3105,10 +3174,19 @@ function write(lines, loc, raw) {
3105
3174
  }
3106
3175
  const is_start_line = i === loc.start.line;
3107
3176
  const is_end_line = i === loc.end.line;
3108
- const before = is_start_line
3109
- ? line.substr(0, loc.start.column).padEnd(loc.start.column, SPACE)
3110
- : '';
3111
- 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) : '';
3112
3190
  lines[i - 1] = before + raw_lines[i - loc.start.line] + after;
3113
3191
  }
3114
3192
  }
@@ -3226,11 +3304,15 @@ function toValue(node) {
3226
3304
  return result;
3227
3305
  case NodeType.InlineArray:
3228
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;
3229
3312
  case NodeType.String:
3230
3313
  case NodeType.Integer:
3231
3314
  case NodeType.Float:
3232
3315
  case NodeType.Boolean:
3233
- case NodeType.DateTime:
3234
3316
  return node.value;
3235
3317
  default:
3236
3318
  throw new Error(`Unrecognized value type "${node.type}"`);
@@ -3470,13 +3552,29 @@ function findByPath(node, path) {
3470
3552
  key = key.concat(array_index);
3471
3553
  }
3472
3554
  else if (isInlineItem(item) && isKeyValue(item.item)) {
3555
+ // For InlineItems wrapping KeyValues, extract the key
3473
3556
  key = item.item.key.value;
3474
3557
  }
3475
3558
  else if (isInlineItem(item)) {
3476
3559
  key = [index];
3477
3560
  }
3478
3561
  if (key.length && arraysEqual(key, path.slice(0, key.length))) {
3479
- 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
+ }
3480
3578
  return true;
3481
3579
  }
3482
3580
  else {
@@ -3583,6 +3681,54 @@ function reorder(changes) {
3583
3681
  }
3584
3682
  return changes;
3585
3683
  }
3684
+ /**
3685
+ * Preserves formatting from the existing node when applying it to the replacement node.
3686
+ * This includes multiline string formats, trailing commas, DateTime formats, etc.
3687
+ *
3688
+ * @param existing - The existing node with formatting to preserve
3689
+ * @param replacement - The replacement node to apply formatting to
3690
+ */
3691
+ function preserveFormatting(existing, replacement) {
3692
+ // Preserve multiline string format
3693
+ if (isString$1(existing) && isString$1(replacement) && isMultilineString(existing.raw)) {
3694
+ // Generate new string node with preserved multiline format
3695
+ const newString = generateString(replacement.value, existing.raw);
3696
+ replacement.raw = newString.raw;
3697
+ replacement.loc = newString.loc;
3698
+ }
3699
+ // Preserve DateTime format
3700
+ if (isDateTime(existing) && isDateTime(replacement)) {
3701
+ // Analyze the original raw format and create a properly formatted replacement
3702
+ const originalRaw = existing.raw;
3703
+ const newValue = replacement.value;
3704
+ // Create a new date with the original format preserved
3705
+ const formattedDate = DateFormatHelper.createDateWithOriginalFormat(newValue, originalRaw);
3706
+ // Update the replacement with the properly formatted date
3707
+ replacement.value = formattedDate;
3708
+ replacement.raw = formattedDate.toISOString();
3709
+ // Adjust the location information to match the new raw length
3710
+ const lengthDiff = replacement.raw.length - originalRaw.length;
3711
+ if (lengthDiff !== 0) {
3712
+ replacement.loc.end.column = replacement.loc.start.column + replacement.raw.length;
3713
+ }
3714
+ }
3715
+ // Preserve array trailing comma format
3716
+ if (isInlineArray(existing) && isInlineArray(replacement)) {
3717
+ const originalHadTrailingCommas = arrayHadTrailingCommas(existing);
3718
+ if (replacement.items.length > 0) {
3719
+ const lastItem = replacement.items[replacement.items.length - 1];
3720
+ lastItem.comma = originalHadTrailingCommas;
3721
+ }
3722
+ }
3723
+ // Preserve inline table trailing comma format
3724
+ if (isInlineTable(existing) && isInlineTable(replacement)) {
3725
+ const originalHadTrailingCommas = tableHadTrailingCommas(existing);
3726
+ if (replacement.items.length > 0) {
3727
+ const lastItem = replacement.items[replacement.items.length - 1];
3728
+ lastItem.comma = originalHadTrailingCommas;
3729
+ }
3730
+ }
3731
+ }
3586
3732
  /**
3587
3733
  * Applies a list of changes to the original TOML document AST while preserving formatting and structure.
3588
3734
  *
@@ -3715,7 +3861,13 @@ function applyChanges(original, updated, changes, format) {
3715
3861
  insert(original, parent, child, undefined, true);
3716
3862
  }
3717
3863
  else {
3718
- 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);
3719
3871
  }
3720
3872
  }
3721
3873
  }
@@ -3725,26 +3877,8 @@ function applyChanges(original, updated, changes, format) {
3725
3877
  let parent;
3726
3878
  if (isKeyValue(existing) && isKeyValue(replacement)) {
3727
3879
  // Edit for key-value means value changes
3728
- // Special handling for arrays: preserve original trailing comma format
3729
- if (isInlineArray(existing.value) && isInlineArray(replacement.value)) {
3730
- const originalHadTrailingCommas = arrayHadTrailingCommas(existing.value);
3731
- const newArray = replacement.value;
3732
- // Apply or remove trailing comma based on original format
3733
- if (newArray.items.length > 0) {
3734
- const lastItem = newArray.items[newArray.items.length - 1];
3735
- lastItem.comma = originalHadTrailingCommas;
3736
- }
3737
- }
3738
- // Special handling for inline tables: preserve original trailing comma format
3739
- if (isInlineTable(existing.value) && isInlineTable(replacement.value)) {
3740
- const originalHadTrailingCommas = tableHadTrailingCommas(existing.value);
3741
- const newTable = replacement.value;
3742
- // Apply or remove trailing comma based on original format
3743
- if (newTable.items.length > 0) {
3744
- const lastItem = newTable.items[newTable.items.length - 1];
3745
- lastItem.comma = originalHadTrailingCommas;
3746
- }
3747
- }
3880
+ // Preserve formatting from existing value in replacement value
3881
+ preserveFormatting(existing.value, replacement.value);
3748
3882
  parent = existing;
3749
3883
  existing = existing.value;
3750
3884
  replacement = replacement.value;
@@ -3762,6 +3896,14 @@ function applyChanges(original, updated, changes, format) {
3762
3896
  parent = existing;
3763
3897
  existing = existing.item;
3764
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
+ }
3765
3907
  else {
3766
3908
  parent = findParent(original, change.path);
3767
3909
  // Special handling for array element edits