@decimalturn/toml-patch 0.3.7 → 0.4.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,9 +1,13 @@
1
- //! @decimalturn/toml-patch v0.3.7 - https://github.com/DecimalTurn/toml-patch - @license: MIT
1
+ //! @decimalturn/toml-patch v0.4.0 - https://github.com/DecimalTurn/toml-patch - @license: MIT
2
2
  var NodeType;
3
3
  (function (NodeType) {
4
4
  NodeType["Document"] = "Document";
5
5
  NodeType["Table"] = "Table";
6
6
  NodeType["TableKey"] = "TableKey";
7
+ /**
8
+ * Array of Tables node
9
+ * More info: https://toml.io/en/latest#array-of-tables
10
+ */
7
11
  NodeType["TableArray"] = "TableArray";
8
12
  NodeType["TableArrayKey"] = "TableArrayKey";
9
13
  NodeType["KeyValue"] = "KeyValue";
@@ -16,6 +20,10 @@ var NodeType;
16
20
  NodeType["InlineArray"] = "InlineArray";
17
21
  NodeType["InlineItem"] = "InlineItem";
18
22
  NodeType["InlineTable"] = "InlineTable";
23
+ /**
24
+ * Comment node
25
+ * More info: https://toml.io/en/latest#comment
26
+ */
19
27
  NodeType["Comment"] = "Comment";
20
28
  })(NodeType || (NodeType = {}));
21
29
  function isDocument(node) {
@@ -27,6 +35,11 @@ function isTable(node) {
27
35
  function isTableKey(node) {
28
36
  return node.type === NodeType.TableKey;
29
37
  }
38
+ /**
39
+ * Is a TableArray (aka array of tables)
40
+ * @param node
41
+ * @returns
42
+ */
30
43
  function isTableArray(node) {
31
44
  return node.type === NodeType.TableArray;
32
45
  }
@@ -36,6 +49,9 @@ function isTableArrayKey(node) {
36
49
  function isKeyValue(node) {
37
50
  return node.type === NodeType.KeyValue;
38
51
  }
52
+ function isDateTime(node) {
53
+ return node.type === NodeType.DateTime;
54
+ }
39
55
  function isInlineArray(node) {
40
56
  return node.type === NodeType.InlineArray;
41
57
  }
@@ -161,11 +177,11 @@ function getLine$1(input, position) {
161
177
  }
162
178
  function findLines(input) {
163
179
  // exec is stateful, so create new regexp each time
164
- const BY_NEW_LINE = /[\r\n|\n]/g;
180
+ const BY_NEW_LINE = /\r\n|\n/g;
165
181
  const indexes = [];
166
182
  let match;
167
183
  while ((match = BY_NEW_LINE.exec(input)) != null) {
168
- indexes.push(match.index);
184
+ indexes.push(match.index + match[0].length - 1);
169
185
  }
170
186
  indexes.push(input.length + 1);
171
187
  return indexes;
@@ -176,6 +192,12 @@ function clonePosition(position) {
176
192
  function cloneLocation(location) {
177
193
  return { start: clonePosition(location.start), end: clonePosition(location.end) };
178
194
  }
195
+ /**
196
+ * Returns a Position at line 1, column 0.
197
+ * This means that lines are 1-indexed and columns are 0-indexed.
198
+ *
199
+ * @returns A Position at line 1, column 0
200
+ */
179
201
  function zero() {
180
202
  return { line: 1, column: 0 };
181
203
  }
@@ -420,10 +442,10 @@ function isString(value) {
420
442
  return typeof value === 'string';
421
443
  }
422
444
  function isInteger(value) {
423
- return typeof value === 'number' && value % 1 === 0;
445
+ return typeof value === 'number' && value % 1 === 0 && isFinite(value) && !Object.is(value, -0);
424
446
  }
425
447
  function isFloat(value) {
426
- return typeof value === 'number' && !isInteger(value);
448
+ return typeof value === 'number' && (!isInteger(value) || !isFinite(value) || Object.is(value, -0));
427
449
  }
428
450
  function isBoolean(value) {
429
451
  return typeof value === 'boolean';
@@ -562,6 +584,359 @@ function lineEndingBackslash(value) {
562
584
  return value.replace(IS_LINE_ENDING_BACKSLASH, (match, group) => match.replace(group, ''));
563
585
  }
564
586
 
587
+ /**
588
+ * Central module for TOML date/time format handling and custom date classes.
589
+ * This module provides all the patterns, classes, and utilities needed to work
590
+ * with the different date/time formats supported by the TOML specification.
591
+ */
592
+ /**
593
+ * Helper class containing all date format patterns and utilities for TOML date/time handling
594
+ */
595
+ class DateFormatHelper {
596
+ /**
597
+ * Creates a custom date/time object that preserves the original TOML date/time format.
598
+ *
599
+ * This method detects the TOML date/time format from the raw string and returns an appropriate
600
+ * custom date/time instance (e.g., LocalDate, LocalTime, LocalDateTime, OffsetDateTime) or a Date,
601
+ * using the provided new JavaScript Date value.
602
+ *
603
+ * @param {Date} newJSDate - The new JavaScript Date object representing the updated
604
+ * date/time value. This is used as the source for constructing the custom date/time object.
605
+ * In some cases, this may be a custom date/time object (e.g., LocalTime) instead of a native Date.
606
+ * @param {string} originalRaw - The original TOML date/time string as it appeared in the input.
607
+ * Used to detect the specific TOML date/time format and to extract formatting details (e.g., separator, offset).
608
+ *
609
+ * @returns {Date | LocalDate | LocalTime | LocalDateTime | OffsetDateTime}
610
+ * Returns a custom date/time object that matches the original TOML format:
611
+ * - LocalDate for date-only values (e.g., "2024-01-15")
612
+ * - LocalTime for time-only values (e.g., "10:30:00")
613
+ * - LocalDateTime for local datetimes (e.g., "2024-01-15T10:30:00" or "2024-01-15 10:30:00")
614
+ * - OffsetDateTime for datetimes with offsets (e.g., "2024-01-15T10:30:00+02:00")
615
+ * - Date (native JS Date) as a fallback if the format is unrecognized
616
+ *
617
+ * Format-specific behavior:
618
+ * - Date-only: Returns a LocalDate constructed from the date part of newJSDate.
619
+ * - Time-only: Returns a LocalTime, either from newJSDate (if already LocalTime) or constructed from its time part.
620
+ * - Local datetime: Returns a LocalDateTime, preserving the separator (T or space).
621
+ * - Offset datetime: Returns an OffsetDateTime, reconstructing the date/time with the original offset and separator.
622
+ * - Fallback: Returns newJSDate as-is.
623
+ */
624
+ static createDateWithOriginalFormat(newJSDate, originalRaw) {
625
+ if (DateFormatHelper.IS_DATE_ONLY.test(originalRaw)) {
626
+ // Local date (date-only) - format: 2024-01-15
627
+ // Check if newJSDate has time components - if so, upgrade appropriately
628
+ if (newJSDate.getUTCHours() !== 0 ||
629
+ newJSDate.getUTCMinutes() !== 0 ||
630
+ newJSDate.getUTCSeconds() !== 0 ||
631
+ newJSDate.getUTCMilliseconds() !== 0) {
632
+ // Check if the new value is an OffsetDateTime (has offset information)
633
+ if (newJSDate instanceof OffsetDateTime) {
634
+ // Upgrade to OffsetDateTime - it already has the right format
635
+ return newJSDate;
636
+ }
637
+ // Upgrade from date-only to local datetime with time components
638
+ // Use T separator as it's the more common format
639
+ let isoString = newJSDate.toISOString().replace('Z', '');
640
+ // Strip .000 milliseconds if present (don't show unnecessary precision)
641
+ isoString = isoString.replace(/\.000$/, '');
642
+ return new LocalDateTime(isoString, false);
643
+ }
644
+ const dateStr = newJSDate.toISOString().split('T')[0];
645
+ return new LocalDate(dateStr);
646
+ }
647
+ else if (DateFormatHelper.IS_TIME_ONLY.test(originalRaw)) {
648
+ // Local time (time-only) - format: 10:30:00
649
+ // For time-only values, we need to handle this more carefully
650
+ // The newJSDate might be a LocalTime object itself
651
+ if (newJSDate instanceof LocalTime) {
652
+ // If the new date is already a LocalTime, use its toISOString
653
+ return newJSDate;
654
+ }
655
+ else {
656
+ // Determine if originalRaw had milliseconds and how many digits
657
+ const msMatch = originalRaw.match(/\.(\d+)\s*$/);
658
+ // Extract time from a regular Date object
659
+ const isoString = newJSDate.toISOString();
660
+ if (isoString && isoString.includes('T')) {
661
+ let newTime = isoString.split('T')[1].split('Z')[0];
662
+ if (msMatch) {
663
+ // Original had milliseconds, preserve the number of digits
664
+ const msDigits = msMatch[1].length;
665
+ const [h, m, sMs] = newTime.split(':');
666
+ const [s] = sMs.split('.');
667
+ const ms = String(newJSDate.getUTCMilliseconds()).padStart(3, '0').slice(0, msDigits);
668
+ newTime = `${h}:${m}:${s}.${ms}`;
669
+ }
670
+ // If original had no milliseconds, keep newTime as-is (with milliseconds if present)
671
+ return new LocalTime(newTime, originalRaw);
672
+ }
673
+ else {
674
+ // Fallback: construct time from the Date object directly
675
+ const hours = String(newJSDate.getUTCHours()).padStart(2, '0');
676
+ const minutes = String(newJSDate.getUTCMinutes()).padStart(2, '0');
677
+ const seconds = String(newJSDate.getUTCSeconds()).padStart(2, '0');
678
+ const milliseconds = newJSDate.getUTCMilliseconds();
679
+ let timeStr;
680
+ if (msMatch) {
681
+ const msDigits = msMatch[1].length;
682
+ let ms = String(milliseconds).padStart(3, '0').slice(0, msDigits);
683
+ timeStr = `${hours}:${minutes}:${seconds}.${ms}`;
684
+ }
685
+ else if (milliseconds > 0) {
686
+ // No original milliseconds, but new value has them - include them
687
+ // Note: milliseconds > 0 ensures we don't format ".0" for zero milliseconds
688
+ const ms = String(milliseconds).padStart(3, '0').replace(/0+$/, '');
689
+ timeStr = `${hours}:${minutes}:${seconds}.${ms}`;
690
+ }
691
+ else {
692
+ timeStr = `${hours}:${minutes}:${seconds}`;
693
+ }
694
+ return new LocalTime(timeStr, originalRaw);
695
+ }
696
+ }
697
+ }
698
+ else if (DateFormatHelper.IS_LOCAL_DATETIME_T.test(originalRaw)) {
699
+ // Local datetime with T separator - format: 2024-01-15T10:30:00
700
+ // Determine if originalRaw had milliseconds and how many digits
701
+ const msMatch = originalRaw.match(/\.(\d+)\s*$/);
702
+ let isoString = newJSDate.toISOString().replace('Z', '');
703
+ if (msMatch) {
704
+ // Original had milliseconds, preserve the number of digits
705
+ const msDigits = msMatch[1].length;
706
+ // isoString is like "2024-01-15T10:30:00.123"
707
+ const [datePart, timePart] = isoString.split('T');
708
+ const [h, m, sMs] = timePart.split(':');
709
+ const [s] = sMs.split('.');
710
+ const ms = String(newJSDate.getUTCMilliseconds()).padStart(3, '0').slice(0, msDigits);
711
+ isoString = `${datePart}T${h}:${m}:${s}.${ms}`;
712
+ }
713
+ // If original had no milliseconds, keep isoString as-is (with milliseconds if present)
714
+ return new LocalDateTime(isoString, false, originalRaw);
715
+ }
716
+ else if (DateFormatHelper.IS_LOCAL_DATETIME_SPACE.test(originalRaw)) {
717
+ // Local datetime with space separator - format: 2024-01-15 10:30:00
718
+ const msMatch = originalRaw.match(/\.(\d+)\s*$/);
719
+ let isoString = newJSDate.toISOString().replace('Z', '').replace('T', ' ');
720
+ if (msMatch) {
721
+ const msDigits = msMatch[1].length;
722
+ // isoString is like "2024-01-15 10:30:00.123"
723
+ const [datePart, timePart] = isoString.split(' ');
724
+ const [h, m, sMs] = timePart.split(':');
725
+ const [s] = sMs.split('.');
726
+ const ms = String(newJSDate.getUTCMilliseconds()).padStart(3, '0').slice(0, msDigits);
727
+ isoString = `${datePart} ${h}:${m}:${s}.${ms}`;
728
+ }
729
+ // If original had no milliseconds, keep isoString as-is (with milliseconds if present)
730
+ return new LocalDateTime(isoString, true, originalRaw);
731
+ }
732
+ else if (DateFormatHelper.IS_OFFSET_DATETIME_T.test(originalRaw) || DateFormatHelper.IS_OFFSET_DATETIME_SPACE.test(originalRaw)) {
733
+ // Offset datetime - preserve the original timezone offset and separator
734
+ const offsetMatch = originalRaw.match(/([+-]\d{2}:\d{2}|[Zz])$/);
735
+ const originalOffset = offsetMatch ? (offsetMatch[1] === 'z' ? 'Z' : offsetMatch[1]) : 'Z';
736
+ const useSpaceSeparator = DateFormatHelper.IS_OFFSET_DATETIME_SPACE.test(originalRaw);
737
+ // Check if original had milliseconds and preserve precision
738
+ const msMatch = originalRaw.match(/\.(\d+)(?:[Zz]|[+-]\d{2}:\d{2})\s*$/);
739
+ // Convert UTC time to local time in the original timezone
740
+ const utcTime = newJSDate.getTime();
741
+ let offsetMinutes = 0;
742
+ if (originalOffset !== 'Z') {
743
+ const sign = originalOffset[0] === '+' ? 1 : -1;
744
+ const [hours, minutes] = originalOffset.slice(1).split(':');
745
+ offsetMinutes = sign * (parseInt(hours) * 60 + parseInt(minutes));
746
+ }
747
+ // Create local time by applying the offset to UTC
748
+ const localTime = new Date(utcTime + offsetMinutes * 60000);
749
+ // Format the local time components
750
+ const year = localTime.getUTCFullYear();
751
+ const month = String(localTime.getUTCMonth() + 1).padStart(2, '0');
752
+ const day = String(localTime.getUTCDate()).padStart(2, '0');
753
+ const hours = String(localTime.getUTCHours()).padStart(2, '0');
754
+ const minutes = String(localTime.getUTCMinutes()).padStart(2, '0');
755
+ const seconds = String(localTime.getUTCSeconds()).padStart(2, '0');
756
+ const milliseconds = localTime.getUTCMilliseconds();
757
+ const separator = useSpaceSeparator ? ' ' : 'T';
758
+ let timePart = `${hours}:${minutes}:${seconds}`;
759
+ // Handle millisecond precision
760
+ if (msMatch) {
761
+ const msDigits = msMatch[1].length;
762
+ const ms = String(milliseconds).padStart(3, '0').slice(0, msDigits);
763
+ timePart += `.${ms}`;
764
+ }
765
+ else if (milliseconds > 0) {
766
+ // Original had no milliseconds, but new value has them
767
+ const ms = String(milliseconds).padStart(3, '0').replace(/0+$/, '');
768
+ timePart += `.${ms}`;
769
+ }
770
+ const newDateTimeString = `${year}-${month}-${day}${separator}${timePart}${originalOffset}`;
771
+ return new OffsetDateTime(newDateTimeString, useSpaceSeparator);
772
+ }
773
+ else {
774
+ // Fallback to regular Date
775
+ return newJSDate;
776
+ }
777
+ }
778
+ }
779
+ // Patterns for different date/time formats
780
+ DateFormatHelper.IS_DATE_ONLY = /^\d{4}-\d{2}-\d{2}$/;
781
+ DateFormatHelper.IS_TIME_ONLY = /^\d{2}:\d{2}:\d{2}(?:\.\d+)?$/;
782
+ DateFormatHelper.IS_LOCAL_DATETIME_T = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?$/;
783
+ DateFormatHelper.IS_LOCAL_DATETIME_SPACE = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?:\.\d+)?$/;
784
+ DateFormatHelper.IS_OFFSET_DATETIME_T = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:[Zz]|[+-]\d{2}:\d{2})$/;
785
+ DateFormatHelper.IS_OFFSET_DATETIME_SPACE = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?:\.\d+)?(?:[Zz]|[+-]\d{2}:\d{2})$/;
786
+ // Legacy patterns from parse-toml.ts (for compatibility)
787
+ DateFormatHelper.IS_FULL_DATE = /(\d{4})-(\d{2})-(\d{2})/;
788
+ DateFormatHelper.IS_FULL_TIME = /(\d{2}):(\d{2}):(\d{2})/;
789
+ /**
790
+ * Custom Date class for local dates (date-only).
791
+ * Format: 1979-05-27
792
+ */
793
+ class LocalDate extends Date {
794
+ constructor(value) {
795
+ super(value);
796
+ }
797
+ toISOString() {
798
+ const year = this.getUTCFullYear();
799
+ const month = String(this.getUTCMonth() + 1).padStart(2, '0');
800
+ const day = String(this.getUTCDate()).padStart(2, '0');
801
+ return `${year}-${month}-${day}`;
802
+ }
803
+ }
804
+ /**
805
+ * Custom Date class for local times (time-only)
806
+ * Format: 07:32:00 or 07:32:00.999
807
+ */
808
+ class LocalTime extends Date {
809
+ constructor(value, originalFormat) {
810
+ // For local time, use a fixed date (1970-01-01) and the provided time
811
+ super(`1970-01-01T${value}`);
812
+ this.originalFormat = originalFormat;
813
+ }
814
+ toISOString() {
815
+ const hours = String(this.getUTCHours()).padStart(2, '0');
816
+ const minutes = String(this.getUTCMinutes()).padStart(2, '0');
817
+ const seconds = String(this.getUTCSeconds()).padStart(2, '0');
818
+ const milliseconds = this.getUTCMilliseconds();
819
+ // Check if the original format had milliseconds
820
+ const originalHadMs = this.originalFormat && this.originalFormat.includes('.');
821
+ if (originalHadMs) {
822
+ // Determine the number of millisecond digits from the original format
823
+ const msMatch = this.originalFormat.match(/\.(\d+)\s*$/);
824
+ const msDigits = msMatch ? msMatch[1].length : 3;
825
+ const ms = String(milliseconds).padStart(3, '0').slice(0, msDigits);
826
+ return `${hours}:${minutes}:${seconds}.${ms}`;
827
+ }
828
+ else if (milliseconds > 0) {
829
+ // Original had no milliseconds, but current has non-zero milliseconds
830
+ // Show them with trailing zeros removed
831
+ const ms = String(milliseconds).padStart(3, '0').replace(/0+$/, '');
832
+ return `${hours}:${minutes}:${seconds}.${ms}`;
833
+ }
834
+ return `${hours}:${minutes}:${seconds}`;
835
+ }
836
+ }
837
+ /**
838
+ * Custom Date class for local datetime (no timezone)
839
+ * Format: 1979-05-27T07:32:00 or 1979-05-27 07:32:00
840
+ */
841
+ class LocalDateTime extends Date {
842
+ constructor(value, useSpaceSeparator = false, originalFormat) {
843
+ // Convert space to T for Date parsing, but remember the original format
844
+ super(value.replace(' ', 'T') + 'Z');
845
+ this.useSpaceSeparator = false;
846
+ this.useSpaceSeparator = useSpaceSeparator;
847
+ this.originalFormat = originalFormat || value;
848
+ }
849
+ toISOString() {
850
+ const year = this.getUTCFullYear();
851
+ const month = String(this.getUTCMonth() + 1).padStart(2, '0');
852
+ const day = String(this.getUTCDate()).padStart(2, '0');
853
+ const hours = String(this.getUTCHours()).padStart(2, '0');
854
+ const minutes = String(this.getUTCMinutes()).padStart(2, '0');
855
+ const seconds = String(this.getUTCSeconds()).padStart(2, '0');
856
+ const milliseconds = this.getUTCMilliseconds();
857
+ const datePart = `${year}-${month}-${day}`;
858
+ const separator = this.useSpaceSeparator ? ' ' : 'T';
859
+ // Check if the original format had milliseconds
860
+ const originalHadMs = this.originalFormat && this.originalFormat.includes('.');
861
+ if (originalHadMs) {
862
+ // Determine the number of millisecond digits from the original format
863
+ const msMatch = this.originalFormat.match(/\.(\d+)\s*$/);
864
+ const msDigits = msMatch ? msMatch[1].length : 3;
865
+ const ms = String(milliseconds).padStart(3, '0').slice(0, msDigits);
866
+ return `${datePart}${separator}${hours}:${minutes}:${seconds}.${ms}`;
867
+ }
868
+ else if (milliseconds > 0) {
869
+ // Original had no milliseconds, but current has non-zero milliseconds
870
+ // Show them with trailing zeros removed
871
+ const ms = String(milliseconds).padStart(3, '0').replace(/0+$/, '');
872
+ return `${datePart}${separator}${hours}:${minutes}:${seconds}.${ms}`;
873
+ }
874
+ return `${datePart}${separator}${hours}:${minutes}:${seconds}`;
875
+ }
876
+ }
877
+ /**
878
+ * Custom Date class for offset datetime that preserves space separator
879
+ * Format: 1979-05-27T07:32:00Z or 1979-05-27 07:32:00-07:00
880
+ */
881
+ class OffsetDateTime extends Date {
882
+ constructor(value, useSpaceSeparator = false) {
883
+ super(value.replace(' ', 'T'));
884
+ this.useSpaceSeparator = false;
885
+ this.useSpaceSeparator = useSpaceSeparator;
886
+ this.originalFormat = value;
887
+ // Extract and preserve the original offset
888
+ const offsetMatch = value.match(/([+-]\d{2}:\d{2}|[Zz])$/);
889
+ if (offsetMatch) {
890
+ this.originalOffset = offsetMatch[1] === 'z' ? 'Z' : offsetMatch[1];
891
+ }
892
+ }
893
+ toISOString() {
894
+ if (this.originalOffset) {
895
+ // Calculate the local time in the original timezone
896
+ const utcTime = this.getTime();
897
+ let offsetMinutes = 0;
898
+ if (this.originalOffset !== 'Z') {
899
+ const sign = this.originalOffset[0] === '+' ? 1 : -1;
900
+ const [hours, minutes] = this.originalOffset.slice(1).split(':');
901
+ offsetMinutes = sign * (parseInt(hours) * 60 + parseInt(minutes));
902
+ }
903
+ const localTime = new Date(utcTime + offsetMinutes * 60000);
904
+ const year = localTime.getUTCFullYear();
905
+ const month = String(localTime.getUTCMonth() + 1).padStart(2, '0');
906
+ const day = String(localTime.getUTCDate()).padStart(2, '0');
907
+ const hours = String(localTime.getUTCHours()).padStart(2, '0');
908
+ const minutes = String(localTime.getUTCMinutes()).padStart(2, '0');
909
+ const seconds = String(localTime.getUTCSeconds()).padStart(2, '0');
910
+ const milliseconds = localTime.getUTCMilliseconds();
911
+ const datePart = `${year}-${month}-${day}`;
912
+ const separator = this.useSpaceSeparator ? ' ' : 'T';
913
+ // Check if the original format had milliseconds
914
+ const originalHadMs = this.originalFormat && this.originalFormat.includes('.');
915
+ if (originalHadMs) {
916
+ // Determine the number of millisecond digits from the original format
917
+ const msMatch = this.originalFormat.match(/\.(\d+)(?:[Zz]|[+-]\d{2}:\d{2})\s*$/);
918
+ const msDigits = msMatch ? msMatch[1].length : 3;
919
+ const ms = String(milliseconds).padStart(3, '0').slice(0, msDigits);
920
+ return `${datePart}${separator}${hours}:${minutes}:${seconds}.${ms}${this.originalOffset}`;
921
+ }
922
+ else if (milliseconds > 0) {
923
+ // Original had no milliseconds, but current has non-zero milliseconds
924
+ // Show them with trailing zeros removed
925
+ const ms = String(milliseconds).padStart(3, '0').replace(/0+$/, '');
926
+ return `${datePart}${separator}${hours}:${minutes}:${seconds}.${ms}${this.originalOffset}`;
927
+ }
928
+ return `${datePart}${separator}${hours}:${minutes}:${seconds}${this.originalOffset}`;
929
+ }
930
+ const isoString = super.toISOString();
931
+ if (this.useSpaceSeparator) {
932
+ return isoString.replace('T', ' ');
933
+ }
934
+ return isoString;
935
+ }
936
+ }
937
+
938
+ // Create a shorter alias for convenience
939
+ const dateFormatHelper$1 = DateFormatHelper;
565
940
  const TRUE = 'true';
566
941
  const FALSE = 'false';
567
942
  const HAS_E = /e/i;
@@ -571,8 +946,6 @@ const IS_NAN = /nan/;
571
946
  const IS_HEX = /^0x/;
572
947
  const IS_OCTAL = /^0o/;
573
948
  const IS_BINARY = /^0b/;
574
- const IS_FULL_DATE = /(\d{4})-(\d{2})-(\d{2})/;
575
- const IS_FULL_TIME = /(\d{2}):(\d{2}):(\d{2})/;
576
949
  function* parseTOML(input) {
577
950
  const tokens = tokenize(input);
578
951
  const cursor = new Cursor(tokens);
@@ -580,6 +953,23 @@ function* parseTOML(input) {
580
953
  yield* walkBlock(cursor, input);
581
954
  }
582
955
  }
956
+ /**
957
+ * Continues parsing TOML from a remaining string and appends the results to an existing AST.
958
+ *
959
+ * @param existingAst - The existing AST to append to
960
+ * @param remainingString - The remaining TOML string to parse
961
+ * @returns A new complete AST with both the existing and newly parsed items
962
+ */
963
+ function* continueParsingTOML(existingAst, remainingString) {
964
+ // Yield all items from the existing AST
965
+ for (const item of existingAst) {
966
+ yield item;
967
+ }
968
+ // Parse and yield all items from the remaining string
969
+ for (const item of parseTOML(remainingString)) {
970
+ yield item;
971
+ }
972
+ }
583
973
  function* walkBlock(cursor, input) {
584
974
  if (cursor.value.type === TokenType.Comment) {
585
975
  yield comment(cursor);
@@ -602,7 +992,7 @@ function* walkValue$1(cursor, input) {
602
992
  else if (cursor.value.raw === TRUE || cursor.value.raw === FALSE) {
603
993
  yield boolean(cursor);
604
994
  }
605
- else if (IS_FULL_DATE.test(cursor.value.raw) || IS_FULL_TIME.test(cursor.value.raw)) {
995
+ else if (dateFormatHelper$1.IS_FULL_DATE.test(cursor.value.raw) || dateFormatHelper$1.IS_FULL_TIME.test(cursor.value.raw)) {
606
996
  yield datetime(cursor, input);
607
997
  }
608
998
  else if ((!cursor.peek().done && cursor.peek().value.type === TokenType.Dot) ||
@@ -815,8 +1205,8 @@ function datetime(cursor, input) {
815
1205
  // check if raw is full date and following is full time
816
1206
  if (!cursor.peek().done &&
817
1207
  cursor.peek().value.type === TokenType.Literal &&
818
- IS_FULL_DATE.test(raw) &&
819
- IS_FULL_TIME.test(cursor.peek().value.raw)) {
1208
+ dateFormatHelper$1.IS_FULL_DATE.test(raw) &&
1209
+ dateFormatHelper$1.IS_FULL_TIME.test(cursor.peek().value.raw)) {
820
1210
  const start = loc.start;
821
1211
  cursor.next();
822
1212
  loc = { start, end: cursor.value.loc.end };
@@ -832,12 +1222,39 @@ function datetime(cursor, input) {
832
1222
  loc = { start, end: cursor.value.loc.end };
833
1223
  raw += `.${cursor.value.raw}`;
834
1224
  }
835
- if (!IS_FULL_DATE.test(raw)) {
836
- // For local time, use local ISO date
837
- const [local_date] = new Date().toISOString().split('T');
838
- value = new Date(`${local_date}T${raw}`);
1225
+ if (!dateFormatHelper$1.IS_FULL_DATE.test(raw)) {
1226
+ // Local time only (e.g., "07:32:00" or "07:32:00.999")
1227
+ if (dateFormatHelper$1.IS_TIME_ONLY.test(raw)) {
1228
+ value = new LocalTime(raw, raw);
1229
+ }
1230
+ else {
1231
+ // For other time formats, use local ISO date
1232
+ const [local_date] = new Date().toISOString().split('T');
1233
+ value = new Date(`${local_date}T${raw}`);
1234
+ }
1235
+ }
1236
+ else if (dateFormatHelper$1.IS_DATE_ONLY.test(raw)) {
1237
+ // Local date only (e.g., "1979-05-27")
1238
+ value = new LocalDate(raw);
1239
+ }
1240
+ else if (dateFormatHelper$1.IS_LOCAL_DATETIME_T.test(raw)) {
1241
+ // Local datetime with T separator (e.g., "1979-05-27T07:32:00")
1242
+ value = new LocalDateTime(raw, false);
1243
+ }
1244
+ else if (dateFormatHelper$1.IS_LOCAL_DATETIME_SPACE.test(raw)) {
1245
+ // Local datetime with space separator (e.g., "1979-05-27 07:32:00")
1246
+ value = new LocalDateTime(raw, true);
1247
+ }
1248
+ else if (dateFormatHelper$1.IS_OFFSET_DATETIME_T.test(raw)) {
1249
+ // Offset datetime with T separator (e.g., "1979-05-27T07:32:00Z" or "1979-05-27T07:32:00-07:00")
1250
+ value = new OffsetDateTime(raw, false);
1251
+ }
1252
+ else if (dateFormatHelper$1.IS_OFFSET_DATETIME_SPACE.test(raw)) {
1253
+ // Offset datetime with space separator (e.g., "1979-05-27 07:32:00Z")
1254
+ value = new OffsetDateTime(raw, true);
839
1255
  }
840
1256
  else {
1257
+ // Default: offset datetime with T separator or any other format
841
1258
  value = new Date(raw.replace(' ', 'T'));
842
1259
  }
843
1260
  return {
@@ -1070,6 +1487,8 @@ function traverse(ast, visitor) {
1070
1487
  }
1071
1488
  }
1072
1489
 
1490
+ // Create a shorter alias for convenience
1491
+ const dateFormatHelper = DateFormatHelper;
1073
1492
  const enter_offsets = new WeakMap();
1074
1493
  const getEnterOffsets = (root) => {
1075
1494
  if (!enter_offsets.has(root)) {
@@ -1086,6 +1505,22 @@ const getExitOffsets = (root) => {
1086
1505
  };
1087
1506
  //TODO: Add getOffsets function to get all offsets contained in the tree
1088
1507
  function replace(root, parent, existing, replacement) {
1508
+ // Special handling for DateTime nodes to preserve original format
1509
+ if (isDateTime(existing) && isDateTime(replacement)) {
1510
+ // Analyze the original raw format and create a properly formatted replacement
1511
+ const originalRaw = existing.raw;
1512
+ const newValue = replacement.value;
1513
+ // Create a new date with the original format preserved
1514
+ const formattedDate = dateFormatHelper.createDateWithOriginalFormat(newValue, originalRaw);
1515
+ // Update the replacement with the properly formatted date
1516
+ replacement.value = formattedDate;
1517
+ replacement.raw = formattedDate.toISOString();
1518
+ // Adjust the location information to match the new raw length
1519
+ const lengthDiff = replacement.raw.length - originalRaw.length;
1520
+ if (lengthDiff !== 0) {
1521
+ replacement.loc.end.column = replacement.loc.start.column + replacement.raw.length;
1522
+ }
1523
+ }
1089
1524
  // First, replace existing node
1090
1525
  // (by index for items, item, or key/value)
1091
1526
  if (hasItems(parent)) {
@@ -1134,7 +1569,16 @@ function replace(root, parent, existing, replacement) {
1134
1569
  };
1135
1570
  addOffset(offset, getExitOffsets(root), replacement, existing);
1136
1571
  }
1137
- function insert(root, parent, child, index) {
1572
+ /**
1573
+ * Inserts a child node into the AST.
1574
+ *
1575
+ * @param root - The root node of the AST
1576
+ * @param parent - The parent node to insert the child into
1577
+ * @param child - The child node to insert
1578
+ * @param index - The index at which to insert the child (optional)
1579
+ * @param forceInline - Whether to force inline positioning even for document-level insertions (optional)
1580
+ */
1581
+ function insert(root, parent, child, index, forceInline) {
1138
1582
  if (!hasItems(parent)) {
1139
1583
  throw new Error(`Unsupported parent type "${parent.type}" for insert`);
1140
1584
  }
@@ -1144,6 +1588,9 @@ function insert(root, parent, child, index) {
1144
1588
  if (isInlineArray(parent) || isInlineTable(parent)) {
1145
1589
  ({ shift, offset } = insertInline(parent, child, index));
1146
1590
  }
1591
+ else if (forceInline && isDocument(parent)) {
1592
+ ({ shift, offset } = insertInlineAtRoot(parent, child, index));
1593
+ }
1147
1594
  else {
1148
1595
  ({ shift, offset } = insertOnNewLine(parent, child, index));
1149
1596
  }
@@ -1176,10 +1623,10 @@ function insertOnNewLine(parent, child, index) {
1176
1623
  column: !isComment(previous) ? previous.loc.start.column : parent.loc.start.column
1177
1624
  }
1178
1625
  : clonePosition(parent.loc.start);
1179
- const is_block = isTable(child) || isTableArray(child);
1626
+ const isSquareBracketsStructure = isTable(child) || isTableArray(child);
1180
1627
  let leading_lines = 0;
1181
1628
  if (use_first_line) ;
1182
- else if (is_block) {
1629
+ else if (isSquareBracketsStructure) {
1183
1630
  leading_lines = 2;
1184
1631
  }
1185
1632
  else {
@@ -1199,44 +1646,33 @@ function insertOnNewLine(parent, child, index) {
1199
1646
  return { shift, offset };
1200
1647
  }
1201
1648
  /**
1202
- * Inserts an inline element into an inline array or table at the specified index.
1203
- * This function handles positioning, comma management, and offset calculation for the inserted item.
1649
+ * Calculates positioning (shift and offset) for inserting a child into a parent container.
1650
+ * This function handles the core positioning logic used to insert an inline item inside a table (or at the document root level).
1204
1651
  *
1205
- * @param parent - The inline array or table where the child will be inserted
1206
- * @param child - The inline item to insert
1207
- * @param index - The index position where to insert the child
1208
- * @returns An object containing shift and offset spans:
1209
- * - shift: Adjustments needed to position the child correctly
1210
- * - offset: Adjustments needed for elements that follow the insertion
1211
- * @throws Error if the child is not a compatible inline item type
1652
+ * @param parent - The parent container (Document, InlineArray or InlineTable)
1653
+ * @param child - The child node to be inserted
1654
+ * @param index - The insertion index within the parent's items
1655
+ * @param options - Configuration options for positioning calculation
1656
+ * @param options.useNewLine - Whether to place the child on a new line
1657
+ * @param options.skipCommaSpace - Number of columns to skip for comma + space (default: 2)
1658
+ * @param options.skipBracketSpace - Number of columns to skip for bracket/space (default: 1)
1659
+ * @param options.hasCommaHandling - Whether comma handling logic should be applied
1660
+ * @param options.isLastElement - Whether this is the last element in the container
1661
+ * @param options.hasSeparatingCommaBefore - Whether a comma should precede this element
1662
+ * @param options.hasSeparatingCommaAfter - Whether a comma should follow this element
1663
+ * @param options.hasTrailingComma - Whether the element has a trailing comma
1664
+ * @returns Object containing shift (positioning adjustment for the child) and offset (adjustment for following elements)
1212
1665
  */
1213
- function insertInline(parent, child, index) {
1214
- if (!isInlineItem(child)) {
1215
- throw new Error(`Incompatible child type "${child.type}"`);
1216
- }
1217
- // Store preceding node and insert
1218
- const previous = index != null ? parent.items[index - 1] : last(parent.items);
1219
- const is_last = index == null || index === parent.items.length;
1220
- parent.items.splice(index, 0, child);
1221
- // Add commas as-needed
1222
- const has_seperating_comma_before = !!previous;
1223
- const has_seperating_comma_after = !is_last;
1224
- const has_trailing_comma = is_last && child.comma === true;
1225
- if (has_seperating_comma_before) {
1226
- previous.comma = true;
1227
- }
1228
- if (has_seperating_comma_after) {
1229
- child.comma = true;
1230
- }
1231
- // Use a new line for documents, children of Table/TableArray,
1232
- // and if an inline table is using new lines
1233
- const use_new_line = isInlineArray(parent) && perLine(parent);
1234
- // Set start location from previous item or start of array
1235
- // (previous is undefined for empty array or inserting at first item)
1666
+ function calculateInlinePositioning(parent, child, index, options = {}) {
1667
+ // Configuration options with default values
1668
+ const { useNewLine = false, skipCommaSpace = 2, skipBracketSpace = 1, hasCommaHandling = false, isLastElement = false, hasSeparatingCommaBefore = false, hasSeparatingCommaAfter = false, hasTrailingComma = false } = options;
1669
+ // Store preceding node
1670
+ const previous = index > 0 ? parent.items[index - 1] : undefined;
1671
+ // Set start location from previous item or start of parent
1236
1672
  const start = previous
1237
1673
  ? {
1238
1674
  line: previous.loc.end.line,
1239
- column: use_new_line
1675
+ column: useNewLine
1240
1676
  ? !isComment(previous)
1241
1677
  ? previous.loc.start.column
1242
1678
  : parent.loc.start.column
@@ -1244,13 +1680,18 @@ function insertInline(parent, child, index) {
1244
1680
  }
1245
1681
  : clonePosition(parent.loc.start);
1246
1682
  let leading_lines = 0;
1247
- if (use_new_line) {
1683
+ if (useNewLine) {
1248
1684
  leading_lines = 1;
1249
1685
  }
1250
1686
  else {
1251
- const skip_comma = 2;
1252
- const skip_bracket = 1;
1253
- start.column += has_seperating_comma_before ? skip_comma : skip_bracket;
1687
+ // Add spacing for inline positioning
1688
+ const hasSpacing = hasSeparatingCommaBefore || (!hasCommaHandling && !!previous);
1689
+ if (hasSpacing && hasCommaHandling) {
1690
+ start.column += skipCommaSpace;
1691
+ }
1692
+ else if (hasSpacing || (hasCommaHandling && !previous)) {
1693
+ start.column += skipBracketSpace;
1694
+ }
1254
1695
  }
1255
1696
  start.line += leading_lines;
1256
1697
  const shift = {
@@ -1259,12 +1700,74 @@ function insertInline(parent, child, index) {
1259
1700
  };
1260
1701
  // Apply offsets after child node
1261
1702
  const child_span = getSpan(child.loc);
1703
+ if (!hasCommaHandling) {
1704
+ // For documents or contexts without comma handling, simpler offset calculation
1705
+ const offset = {
1706
+ lines: child_span.lines + (leading_lines - 1),
1707
+ columns: child_span.columns
1708
+ };
1709
+ return { shift, offset };
1710
+ }
1711
+ // Special case: Fix trailing comma spacing issue for arrays that have trailing commas
1712
+ const has_trailing_comma_spacing_bug = hasSeparatingCommaBefore &&
1713
+ hasTrailingComma &&
1714
+ !hasSeparatingCommaAfter &&
1715
+ isLastElement;
1716
+ let trailing_comma_offset_adjustment = 0;
1717
+ if (has_trailing_comma_spacing_bug) {
1718
+ trailing_comma_offset_adjustment = -1;
1719
+ }
1262
1720
  const offset = {
1263
1721
  lines: child_span.lines + (leading_lines - 1),
1264
- columns: child_span.columns + (has_seperating_comma_before || has_seperating_comma_after ? 2 : 0) + (has_trailing_comma ? 1 : 0)
1722
+ columns: child_span.columns +
1723
+ (hasSeparatingCommaBefore || hasSeparatingCommaAfter ? skipCommaSpace : 0) +
1724
+ (hasTrailingComma ? 1 + trailing_comma_offset_adjustment : 0)
1265
1725
  };
1266
1726
  return { shift, offset };
1267
1727
  }
1728
+ function insertInline(parent, child, index) {
1729
+ if (!isInlineItem(child)) {
1730
+ throw new Error(`Incompatible child type "${child.type}"`);
1731
+ }
1732
+ // Store preceding node and insert
1733
+ const previous = index != null ? parent.items[index - 1] : last(parent.items);
1734
+ const is_last = index == null || index === parent.items.length;
1735
+ parent.items.splice(index, 0, child);
1736
+ // Add commas as-needed
1737
+ const has_separating_comma_before = !!previous;
1738
+ const has_separating_comma_after = !is_last;
1739
+ if (has_separating_comma_before) {
1740
+ previous.comma = true;
1741
+ }
1742
+ if (has_separating_comma_after) {
1743
+ child.comma = true;
1744
+ }
1745
+ // Use new line for inline arrays that span multiple lines
1746
+ const use_new_line = isInlineArray(parent) && perLine(parent);
1747
+ const has_trailing_comma = is_last && child.comma === true;
1748
+ return calculateInlinePositioning(parent, child, index, {
1749
+ useNewLine: use_new_line,
1750
+ hasCommaHandling: true,
1751
+ isLastElement: is_last,
1752
+ hasSeparatingCommaBefore: has_separating_comma_before,
1753
+ hasSeparatingCommaAfter: has_separating_comma_after,
1754
+ hasTrailingComma: has_trailing_comma
1755
+ });
1756
+ }
1757
+ /**
1758
+ * Inserts a child into a Document with inline positioning behavior.
1759
+ * This provides inline-style spacing while maintaining Document's Block item types.
1760
+ */
1761
+ function insertInlineAtRoot(parent, child, index) {
1762
+ // Calculate positioning as if inserting into an inline context
1763
+ const result = calculateInlinePositioning(parent, child, index, {
1764
+ useNewLine: false,
1765
+ hasCommaHandling: false
1766
+ });
1767
+ // Insert the child directly into the Document (as a Block item)
1768
+ parent.items.splice(index, 0, child);
1769
+ return result;
1770
+ }
1268
1771
  function remove(root, parent, node) {
1269
1772
  // Remove an element from the parent's items
1270
1773
  // (supports Document, Table, TableArray, InlineTable, and InlineArray
@@ -1322,6 +1825,11 @@ function remove(root, parent, node) {
1322
1825
  lines: -(removed_span.lines - (keep_line ? 1 : 0)),
1323
1826
  columns: -removed_span.columns
1324
1827
  };
1828
+ // If there is nothing left, don't perform any offsets
1829
+ if (previous === undefined && next === undefined) {
1830
+ offset.lines = 0;
1831
+ offset.columns = 0;
1832
+ }
1325
1833
  // Offset for comma and remove comma that appear in front of the element (if-needed)
1326
1834
  if (is_inline && previous_on_same_line) {
1327
1835
  offset.columns -= 2;
@@ -1331,7 +1839,15 @@ function remove(root, parent, node) {
1331
1839
  offset.columns -= 2;
1332
1840
  }
1333
1841
  if (is_inline && previous && !next) {
1334
- previous.comma = false;
1842
+ // When removing the last element, preserve trailing comma preference
1843
+ // If the removed element had a trailing comma, transfer it to the new last element
1844
+ const removedHadTrailingComma = node.comma;
1845
+ if (removedHadTrailingComma) {
1846
+ previous.comma = true;
1847
+ }
1848
+ else {
1849
+ previous.comma = false;
1850
+ }
1335
1851
  }
1336
1852
  // Apply offsets after preceding node or before children of parent node
1337
1853
  const target = previous || parent;
@@ -1492,6 +2008,11 @@ function addOffset(offset, offsets, node, from) {
1492
2008
  offsets.set(node, offset);
1493
2009
  }
1494
2010
 
2011
+ /**
2012
+ * Generates a new TOML document node.
2013
+ *
2014
+ * @returns A new Document node.
2015
+ */
1495
2016
  function generateDocument() {
1496
2017
  return {
1497
2018
  type: NodeType.Document,
@@ -1571,7 +2092,7 @@ function generateKeyValue(key, value) {
1571
2092
  value
1572
2093
  };
1573
2094
  }
1574
- const IS_BARE_KEY = /[\w,\d,\_,\-]+/;
2095
+ const IS_BARE_KEY = /^[\w-]+$/;
1575
2096
  function keyValueToRaw(value) {
1576
2097
  return value.map(part => (IS_BARE_KEY.test(part) ? part : JSON.stringify(part))).join('.');
1577
2098
  }
@@ -1603,7 +2124,22 @@ function generateInteger(value) {
1603
2124
  };
1604
2125
  }
1605
2126
  function generateFloat(value) {
1606
- const raw = value.toString();
2127
+ let raw;
2128
+ if (value === Infinity) {
2129
+ raw = 'inf';
2130
+ }
2131
+ else if (value === -Infinity) {
2132
+ raw = '-inf';
2133
+ }
2134
+ else if (Number.isNaN(value)) {
2135
+ raw = 'nan';
2136
+ }
2137
+ else if (Object.is(value, -0)) {
2138
+ raw = '-0.0';
2139
+ }
2140
+ else {
2141
+ raw = value.toString();
2142
+ }
1607
2143
  return {
1608
2144
  type: NodeType.Float,
1609
2145
  loc: { start: zero(), end: { line: 1, column: raw.length } },
@@ -1619,6 +2155,8 @@ function generateBoolean(value) {
1619
2155
  };
1620
2156
  }
1621
2157
  function generateDateTime(value) {
2158
+ // Custom date classes have their own toISOString() implementations
2159
+ // that return the properly formatted strings for each TOML date/time type
1622
2160
  const raw = value.toISOString();
1623
2161
  return {
1624
2162
  type: NodeType.DateTime,
@@ -1650,7 +2188,382 @@ function generateInlineTable() {
1650
2188
  };
1651
2189
  }
1652
2190
 
1653
- function formatTopLevel(document) {
2191
+ // Default formatting values
2192
+ const DEFAULT_NEWLINE = '\n';
2193
+ const DEFAULT_TRAILING_NEWLINE = 1;
2194
+ const DEFAULT_TRAILING_COMMA = false;
2195
+ const DEFAULT_BRACKET_SPACING = true;
2196
+ const DEFAULT_INLINE_TABLE_START = 1;
2197
+ // Detects if trailing commas are used in the existing TOML by examining the AST
2198
+ // Returns true if trailing commas are used, false if not or comma-separated structures found (ie. default to false)
2199
+ function detectTrailingComma(ast) {
2200
+ // Convert iterable to array and look for the first inline array or inline table to determine trailing comma preference
2201
+ const items = Array.from(ast);
2202
+ for (const item of items) {
2203
+ const result = findTrailingCommaInNode(item);
2204
+ if (result !== null) {
2205
+ return result;
2206
+ }
2207
+ }
2208
+ // Return default if no comma-separated structures are found
2209
+ return DEFAULT_TRAILING_COMMA;
2210
+ }
2211
+ // Detects if bracket spacing is used in inline arrays and tables by examining the raw string
2212
+ // Returns true if bracket spacing is found, false if not or no bracket structures found (default to true)
2213
+ function detectBracketSpacing(tomlString, ast) {
2214
+ // Convert iterable to array and look for inline arrays and tables
2215
+ const items = Array.from(ast);
2216
+ for (const item of items) {
2217
+ const result = findBracketSpacingInNode(item, tomlString);
2218
+ if (result !== null) {
2219
+ return result;
2220
+ }
2221
+ }
2222
+ // Return default if no bracket structures are found
2223
+ return DEFAULT_BRACKET_SPACING;
2224
+ }
2225
+ // Helper function to recursively search for bracket spacing in a node
2226
+ function findBracketSpacingInNode(node, tomlString) {
2227
+ if (!node || typeof node !== 'object') {
2228
+ return null;
2229
+ }
2230
+ // Check if this is an InlineArray or InlineTable
2231
+ if ((node.type === 'InlineArray' || node.type === 'InlineTable') && node.loc) {
2232
+ const bracketSpacing = checkBracketSpacingInLocation(node.loc, tomlString);
2233
+ if (bracketSpacing !== null) {
2234
+ return bracketSpacing;
2235
+ }
2236
+ }
2237
+ // Recursively check nested structures
2238
+ if (node.items && Array.isArray(node.items)) {
2239
+ for (const child of node.items) {
2240
+ const result = findBracketSpacingInNode(child, tomlString);
2241
+ if (result !== null) {
2242
+ return result;
2243
+ }
2244
+ // Also check nested item if it exists
2245
+ if (child.item) {
2246
+ const nestedResult = findBracketSpacingInNode(child.item, tomlString);
2247
+ if (nestedResult !== null) {
2248
+ return nestedResult;
2249
+ }
2250
+ }
2251
+ }
2252
+ }
2253
+ // Check other properties that might contain nodes
2254
+ for (const prop of ['value', 'key', 'item']) {
2255
+ if (node[prop]) {
2256
+ const result = findBracketSpacingInNode(node[prop], tomlString);
2257
+ if (result !== null) {
2258
+ return result;
2259
+ }
2260
+ }
2261
+ }
2262
+ return null;
2263
+ }
2264
+ // Helper function to check bracket spacing in a specific location
2265
+ function checkBracketSpacingInLocation(loc, tomlString) {
2266
+ var _a;
2267
+ if (!loc || !loc.start || !loc.end) {
2268
+ return null;
2269
+ }
2270
+ // Extract the raw text for this location
2271
+ const lines = tomlString.split(/\r?\n/);
2272
+ const startLine = loc.start.line - 1; // Convert to 0-based
2273
+ const endLine = loc.end.line - 1;
2274
+ const startCol = loc.start.column;
2275
+ const endCol = loc.end.column;
2276
+ let rawText = '';
2277
+ if (startLine === endLine) {
2278
+ rawText = ((_a = lines[startLine]) === null || _a === void 0 ? void 0 : _a.substring(startCol, endCol + 1)) || '';
2279
+ }
2280
+ else {
2281
+ // Multi-line case
2282
+ if (lines[startLine]) {
2283
+ rawText += lines[startLine].substring(startCol);
2284
+ }
2285
+ for (let i = startLine + 1; i < endLine; i++) {
2286
+ rawText += '\n' + (lines[i] || '');
2287
+ }
2288
+ if (lines[endLine]) {
2289
+ rawText += '\n' + lines[endLine].substring(0, endCol + 1);
2290
+ }
2291
+ }
2292
+ // Check for bracket spacing patterns
2293
+ // For arrays: [ elements ] vs [elements]
2294
+ // For tables: { elements } vs {elements}
2295
+ const arrayMatch = rawText.match(/^\[(\s*)/);
2296
+ const tableMatch = rawText.match(/^\{(\s*)/);
2297
+ if (arrayMatch) {
2298
+ // Check if there's a space after the opening bracket
2299
+ return arrayMatch[1].length > 0;
2300
+ }
2301
+ if (tableMatch) {
2302
+ // Check if there's a space after the opening brace
2303
+ return tableMatch[1].length > 0;
2304
+ }
2305
+ return null;
2306
+ }
2307
+ // Helper function to recursively search for comma usage in a node
2308
+ function findTrailingCommaInNode(node) {
2309
+ if (!node || typeof node !== 'object') {
2310
+ return null;
2311
+ }
2312
+ // Check if this is an InlineArray
2313
+ if (node.type === 'InlineArray' && node.items && Array.isArray(node.items)) {
2314
+ return checkTrailingCommaInItems(node.items);
2315
+ }
2316
+ // Check if this is an InlineTable
2317
+ if (node.type === 'InlineTable' && node.items && Array.isArray(node.items)) {
2318
+ return checkTrailingCommaInItems(node.items);
2319
+ }
2320
+ // Check if this is a KeyValue with a value that might contain arrays/tables
2321
+ if (node.type === 'KeyValue' && node.value) {
2322
+ return findTrailingCommaInNode(node.value);
2323
+ }
2324
+ // For other node types, recursively check any items array
2325
+ if (node.items && Array.isArray(node.items)) {
2326
+ for (const item of node.items) {
2327
+ const result = findTrailingCommaInNode(item);
2328
+ if (result !== null) {
2329
+ return result;
2330
+ }
2331
+ }
2332
+ }
2333
+ return null;
2334
+ }
2335
+ // Check trailing comma usage in an array of inline items
2336
+ function checkTrailingCommaInItems(items) {
2337
+ if (items.length === 0) {
2338
+ return null;
2339
+ }
2340
+ // Check the last item to see if it has a trailing comma
2341
+ const lastItem = items[items.length - 1];
2342
+ if (lastItem && typeof lastItem === 'object' && 'comma' in lastItem) {
2343
+ return lastItem.comma === true;
2344
+ }
2345
+ return false;
2346
+ }
2347
+ // Helper function to detect if an InlineArray originally had trailing commas
2348
+ function arrayHadTrailingCommas(node) {
2349
+ if (!isInlineArray(node))
2350
+ return false;
2351
+ if (node.items.length === 0)
2352
+ return false;
2353
+ // Check if the last item has a trailing comma
2354
+ const lastItem = node.items[node.items.length - 1];
2355
+ return lastItem.comma === true;
2356
+ }
2357
+ // Helper function to detect if an InlineTable originally had trailing commas
2358
+ function tableHadTrailingCommas(node) {
2359
+ if (!isInlineTable(node))
2360
+ return false;
2361
+ if (node.items.length === 0)
2362
+ return false;
2363
+ // Check if the last item has a trailing comma
2364
+ const lastItem = node.items[node.items.length - 1];
2365
+ return lastItem.comma === true;
2366
+ }
2367
+ // Returns the detected newline (\n or \r\n) from a string, defaulting to \n
2368
+ function detectNewline(str) {
2369
+ const lfIndex = str.indexOf('\n');
2370
+ if (lfIndex > 0 && str.substring(lfIndex - 1, lfIndex) === '\r') {
2371
+ return '\r\n';
2372
+ }
2373
+ return '\n';
2374
+ }
2375
+ // Counts consecutive trailing newlines at the end of a string
2376
+ function countTrailingNewlines(str, newlineChar) {
2377
+ let count = 0;
2378
+ let pos = str.length;
2379
+ while (pos >= newlineChar.length) {
2380
+ if (str.substring(pos - newlineChar.length, pos) === newlineChar) {
2381
+ count++;
2382
+ pos -= newlineChar.length;
2383
+ }
2384
+ else {
2385
+ break;
2386
+ }
2387
+ }
2388
+ return count;
2389
+ }
2390
+ /**
2391
+ * Validates a format object and warns about unsupported properties.
2392
+ * Throws errors for supported properties with invalid types.
2393
+ * @param format - The format object to validate
2394
+ * @returns The validated format object with only supported properties and correct types
2395
+ */
2396
+ function validateFormatObject(format) {
2397
+ if (!format || typeof format !== 'object') {
2398
+ return {};
2399
+ }
2400
+ const supportedProperties = new Set(['newLine', 'trailingNewline', 'trailingComma', 'bracketSpacing', 'inlineTableStart']);
2401
+ const validatedFormat = {};
2402
+ const unsupportedProperties = [];
2403
+ const invalidTypeProperties = [];
2404
+ // Check all enumerable properties of the format object
2405
+ for (const key in format) {
2406
+ if (Object.prototype.hasOwnProperty.call(format, key)) {
2407
+ if (supportedProperties.has(key)) {
2408
+ const value = format[key];
2409
+ // Type validation for each property
2410
+ switch (key) {
2411
+ case 'newLine':
2412
+ if (typeof value === 'string') {
2413
+ validatedFormat.newLine = value;
2414
+ }
2415
+ else {
2416
+ invalidTypeProperties.push(`${key} (expected string, got ${typeof value})`);
2417
+ }
2418
+ break;
2419
+ case 'trailingNewline':
2420
+ if (typeof value === 'boolean' || typeof value === 'number') {
2421
+ validatedFormat.trailingNewline = value;
2422
+ }
2423
+ else {
2424
+ invalidTypeProperties.push(`${key} (expected boolean or number, got ${typeof value})`);
2425
+ }
2426
+ break;
2427
+ case 'trailingComma':
2428
+ case 'bracketSpacing':
2429
+ if (typeof value === 'boolean') {
2430
+ validatedFormat[key] = value;
2431
+ }
2432
+ else {
2433
+ invalidTypeProperties.push(`${key} (expected boolean, got ${typeof value})`);
2434
+ }
2435
+ break;
2436
+ case 'inlineTableStart':
2437
+ if (typeof value === 'number' && Number.isInteger(value) && value >= 0) {
2438
+ validatedFormat.inlineTableStart = value;
2439
+ }
2440
+ else if (value === undefined || value === null) {
2441
+ // Allow undefined/null to use default
2442
+ validatedFormat.inlineTableStart = value;
2443
+ }
2444
+ else {
2445
+ invalidTypeProperties.push(`${key} (expected non-negative integer or undefined, got ${typeof value})`);
2446
+ }
2447
+ break;
2448
+ }
2449
+ }
2450
+ else {
2451
+ unsupportedProperties.push(key);
2452
+ }
2453
+ }
2454
+ }
2455
+ // Warn about unsupported properties
2456
+ if (unsupportedProperties.length > 0) {
2457
+ console.warn(`toml-patch: Ignoring unsupported format properties: ${unsupportedProperties.join(', ')}. Supported properties are: ${Array.from(supportedProperties).join(', ')}`);
2458
+ }
2459
+ // Throw error for invalid types
2460
+ if (invalidTypeProperties.length > 0) {
2461
+ throw new TypeError(`Invalid types for format properties: ${invalidTypeProperties.join(', ')}`);
2462
+ }
2463
+ return validatedFormat;
2464
+ }
2465
+ /**
2466
+ * Resolves a format parameter to a TomlFormat instance.
2467
+ * Handles TomlFormat instances and partial TomlFormat objects as well as undefined.
2468
+ *
2469
+ * @param format - The format parameter to resolve (TomlFormat instance, partial format object, or undefined)
2470
+ * @param fallbackFormat - The fallback TomlFormat to use when no format is provided
2471
+ * @returns A resolved TomlFormat instance
2472
+ */
2473
+ function resolveTomlFormat(format, fallbackFormat) {
2474
+ var _a, _b, _c, _d;
2475
+ if (format) {
2476
+ // If format is provided, validate and merge it with fallback
2477
+ if (format instanceof TomlFormat) {
2478
+ return format;
2479
+ }
2480
+ else {
2481
+ // Validate the format object and warn about unsupported properties
2482
+ const validatedFormat = validateFormatObject(format);
2483
+ // Create a new TomlFormat instance with validated properties
2484
+ 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);
2485
+ }
2486
+ }
2487
+ else {
2488
+ // Use fallback format when no format is provided
2489
+ return fallbackFormat;
2490
+ }
2491
+ }
2492
+ class TomlFormat {
2493
+ // These options were part of the original TimHall's version and are not yet implemented
2494
+ //printWidth?: number;
2495
+ //tabWidth?: number;
2496
+ //useTabs?: boolean;
2497
+ constructor(newLine, trailingNewline, trailingComma, bracketSpacing, inlineTableStart) {
2498
+ // Use provided values or fall back to defaults
2499
+ this.newLine = newLine !== null && newLine !== void 0 ? newLine : DEFAULT_NEWLINE;
2500
+ this.trailingNewline = trailingNewline !== null && trailingNewline !== void 0 ? trailingNewline : DEFAULT_TRAILING_NEWLINE;
2501
+ this.trailingComma = trailingComma !== null && trailingComma !== void 0 ? trailingComma : DEFAULT_TRAILING_COMMA;
2502
+ this.bracketSpacing = bracketSpacing !== null && bracketSpacing !== void 0 ? bracketSpacing : DEFAULT_BRACKET_SPACING;
2503
+ this.inlineTableStart = inlineTableStart !== null && inlineTableStart !== void 0 ? inlineTableStart : DEFAULT_INLINE_TABLE_START;
2504
+ }
2505
+ /**
2506
+ * Creates a new TomlFormat instance with default formatting preferences.
2507
+ *
2508
+ * @returns A new TomlFormat instance with default values:
2509
+ * - newLine: '\n'
2510
+ * - trailingNewline: 1
2511
+ * - trailingComma: false
2512
+ * - bracketSpacing: true
2513
+ * - inlineTableStart: 1
2514
+ */
2515
+ static default() {
2516
+ return new TomlFormat(DEFAULT_NEWLINE, DEFAULT_TRAILING_NEWLINE, DEFAULT_TRAILING_COMMA, DEFAULT_BRACKET_SPACING, DEFAULT_INLINE_TABLE_START);
2517
+ }
2518
+ /**
2519
+ * Auto-detects formatting preferences from an existing TOML string.
2520
+ *
2521
+ * This method analyzes the provided TOML string to determine formatting
2522
+ * preferences such as line endings, trailing newlines, and comma usage.
2523
+ *
2524
+ * @param tomlString - The TOML string to analyze for formatting patterns
2525
+ * @returns A new TomlFormat instance with detected formatting preferences
2526
+ *
2527
+ * @example
2528
+ * ```typescript
2529
+ * const toml = 'array = ["a", "b", "c",]\ntable = { x = 1, y = 2, }';
2530
+ * const format = TomlFormat.autoDetectFormat(toml);
2531
+ * // format.trailingComma will be true
2532
+ * // format.newLine will be '\n'
2533
+ * // format.trailingNewline will be 0 (no trailing newline)
2534
+ * ```
2535
+ */
2536
+ static autoDetectFormat(tomlString) {
2537
+ const format = TomlFormat.default();
2538
+ // Detect line ending style
2539
+ format.newLine = detectNewline(tomlString);
2540
+ // Detect trailing newline count
2541
+ format.trailingNewline = countTrailingNewlines(tomlString, format.newLine);
2542
+ // Parse the TOML to detect comma and bracket spacing usage patterns
2543
+ try {
2544
+ const ast = parseTOML(tomlString);
2545
+ // Convert to array once to avoid consuming the iterator multiple times
2546
+ const astArray = Array.from(ast);
2547
+ format.trailingComma = detectTrailingComma(astArray);
2548
+ format.bracketSpacing = detectBracketSpacing(tomlString, astArray);
2549
+ }
2550
+ catch (error) {
2551
+ // If parsing fails, fall back to defaults
2552
+ // This ensures the method is robust against malformed TOML
2553
+ format.trailingComma = DEFAULT_TRAILING_COMMA;
2554
+ format.bracketSpacing = DEFAULT_BRACKET_SPACING;
2555
+ }
2556
+ // inlineTableStart uses default value since auto-detection would require
2557
+ // complex analysis of nested table formatting preferences
2558
+ format.inlineTableStart = DEFAULT_INLINE_TABLE_START;
2559
+ return format;
2560
+ }
2561
+ }
2562
+ function formatTopLevel(document, format) {
2563
+ // If inlineTableStart is 0, convert all top-level tables to inline tables
2564
+ if (format.inlineTableStart === 0) {
2565
+ return document;
2566
+ }
1654
2567
  const move_to_top_level = document.items.filter(item => {
1655
2568
  if (!isKeyValue(item))
1656
2569
  return false;
@@ -1658,7 +2571,12 @@ function formatTopLevel(document) {
1658
2571
  const is_inline_array = isInlineArray(item.value) &&
1659
2572
  item.value.items.length &&
1660
2573
  isInlineTable(item.value.items[0].item);
1661
- return is_inline_table || is_inline_array;
2574
+ // Only move to top level if the depth is less than inlineTableStart
2575
+ if (is_inline_table || is_inline_array) {
2576
+ const depth = calculateTableDepth(item.key.value);
2577
+ return format.inlineTableStart === undefined || depth < format.inlineTableStart;
2578
+ }
2579
+ return false;
1662
2580
  });
1663
2581
  move_to_top_level.forEach(node => {
1664
2582
  remove(document, document, node);
@@ -1694,6 +2612,109 @@ function formatTableArray(key_value) {
1694
2612
  applyWrites(root);
1695
2613
  return root.items;
1696
2614
  }
2615
+ /**
2616
+ * Updates a table's location end position after removing inline table items.
2617
+ * When inline table content is removed from a parent table, the parent table's
2618
+ * end position needs to be adjusted to reflect where the content actually ends.
2619
+ *
2620
+ * @param table - The table whose end position should be updated
2621
+ */
2622
+ function postInlineItemRemovalAdjustment(table) {
2623
+ if (table.items.length > 0) {
2624
+ const lastItem = table.items[table.items.length - 1];
2625
+ table.loc.end.line = lastItem.loc.end.line;
2626
+ table.loc.end.column = lastItem.loc.end.column;
2627
+ }
2628
+ else {
2629
+ // If no items left, table ends at the header line
2630
+ table.loc.end.line = table.key.loc.end.line;
2631
+ table.loc.end.column = table.key.loc.end.column;
2632
+ }
2633
+ }
2634
+ /**
2635
+ * Calculates the nesting depth of a table based on its key path.
2636
+ * Root level tables (e.g., [table]) have depth 0.
2637
+ * First level nested tables (e.g., [table.nested]) have depth 1.
2638
+ *
2639
+ * @param keyPath - Array representing the table key path (e.g., ['table', 'nested'])
2640
+ * @returns The nesting depth (0 for root level, 1+ for nested levels)
2641
+ */
2642
+ function calculateTableDepth(keyPath) {
2643
+ return Math.max(0, keyPath.length - 1);
2644
+ }
2645
+ /**
2646
+ * Converts nested inline tables to separate table sections based on the inlineTableStart depth setting.
2647
+ * This function recursively processes all tables in the document and extracts inline tables that are
2648
+ * at a depth less than the inlineTableStart threshold.
2649
+ */
2650
+ function formatNestedTablesMultiline(document, format) {
2651
+ // If inlineTableStart is undefined, use the default behavior (no conversion)
2652
+ // If inlineTableStart is 0, all should be inline (no conversion)
2653
+ if (format.inlineTableStart === undefined || format.inlineTableStart === 0) {
2654
+ return document;
2655
+ }
2656
+ const additionalTables = [];
2657
+ // Process all existing tables for nested inline tables
2658
+ for (const item of document.items) {
2659
+ if (isKeyValue(item) && isInlineTable(item.value)) {
2660
+ // This is a top-level inline table (depth 0)
2661
+ const depth = calculateTableDepth(item.key.value);
2662
+ if (depth < format.inlineTableStart) {
2663
+ // Convert to a separate table
2664
+ const table = formatTable(item);
2665
+ // Remove the original inline table item
2666
+ remove(document, document, item);
2667
+ // Add the new table
2668
+ insert(document, document, table);
2669
+ // Process this table for further nested inlines
2670
+ processTableForNestedInlines(table, additionalTables, format);
2671
+ }
2672
+ }
2673
+ else if (item.type === 'Table') {
2674
+ // Process existing table for nested inline tables
2675
+ processTableForNestedInlines(item, additionalTables, format);
2676
+ }
2677
+ }
2678
+ // Add all the additional tables to the document
2679
+ for (const table of additionalTables) {
2680
+ insert(document, document, table);
2681
+ }
2682
+ applyWrites(document);
2683
+ return document;
2684
+ }
2685
+ /**
2686
+ * Recursively processes a table for nested inline tables and extracts them as separate tables
2687
+ * when they are at a depth less than the inlineTableStart threshold.
2688
+ */
2689
+ function processTableForNestedInlines(table, additionalTables, format) {
2690
+ var _a;
2691
+ // Process from end to beginning to avoid index issues when removing items
2692
+ for (let i = table.items.length - 1; i >= 0; i--) {
2693
+ const item = table.items[i];
2694
+ if (isKeyValue(item) && isInlineTable(item.value)) {
2695
+ // Calculate the depth of this nested table
2696
+ const nestedTableKey = [...table.key.item.value, ...item.key.value];
2697
+ const depth = calculateTableDepth(nestedTableKey);
2698
+ // Only convert to separate table if depth is less than inlineTableStart
2699
+ if (depth < ((_a = format.inlineTableStart) !== null && _a !== void 0 ? _a : 1)) {
2700
+ // Convert this inline table to a separate table section
2701
+ const separateTable = generateTable(nestedTableKey);
2702
+ // Move all items from the inline table to the separate table
2703
+ for (const inlineItem of item.value.items) {
2704
+ insert(separateTable, separateTable, inlineItem.item);
2705
+ }
2706
+ // Remove this item from the original table
2707
+ remove(table, table, item);
2708
+ // Update the parent table's end position after removal
2709
+ postInlineItemRemovalAdjustment(table);
2710
+ // Add this table to be inserted into the document
2711
+ additionalTables.push(separateTable);
2712
+ // Recursively process the new table for further nested inlines
2713
+ processTableForNestedInlines(separateTable, additionalTables, format);
2714
+ }
2715
+ }
2716
+ }
2717
+ }
1697
2718
  function formatPrintWidth(document, format) {
1698
2719
  // TODO
1699
2720
  return document;
@@ -1718,13 +2739,7 @@ function formatEmptyLines(document) {
1718
2739
  return document;
1719
2740
  }
1720
2741
 
1721
- const default_format = {
1722
- printWidth: 80,
1723
- trailingComma: false,
1724
- bracketSpacing: true
1725
- };
1726
- function parseJS(value, format = {}) {
1727
- format = Object.assign({}, default_format, format);
2742
+ function parseJS(value, format = TomlFormat.default()) {
1728
2743
  value = toJSON(value);
1729
2744
  // Reorder the elements in the object
1730
2745
  value = reorderElements(value);
@@ -1736,26 +2751,39 @@ function parseJS(value, format = {}) {
1736
2751
  // Heuristics:
1737
2752
  // 1. Top-level objects/arrays should be tables/table arrays
1738
2753
  // 2. Convert objects/arrays to tables/table arrays based on print width
1739
- const formatted = pipe(document, formatTopLevel, document => formatPrintWidth(document), formatEmptyLines);
1740
- return formatted;
2754
+ // 3. Convert nested inline tables to separate tables based on preferNestedTablesMultiline
2755
+ const formatted = pipe(document, document => formatTopLevel(document, format), document => formatNestedTablesMultiline(document, format), document => formatPrintWidth(document));
2756
+ // Apply formatEmptyLines only once at the end
2757
+ return formatEmptyLines(formatted);
1741
2758
  }
1742
2759
  /**
1743
2760
  This function makes sure that properties that are simple values (not objects or arrays) are ordered first,
1744
2761
  and that objects and arrays are ordered last. This makes parseJS more reliable and easier to test.
1745
2762
  */
1746
2763
  function reorderElements(value) {
1747
- let result = {};
1748
- // First add all simple values
1749
- for (const key in value) {
1750
- if (!isObject(value[key]) && !Array.isArray(value[key])) {
1751
- result[key] = value[key];
1752
- }
1753
- }
1754
- // Then add all objects and arrays
2764
+ // Pre-sort keys to avoid multiple iterations
2765
+ const simpleKeys = [];
2766
+ const complexKeys = [];
2767
+ // Separate keys in a single pass
1755
2768
  for (const key in value) {
1756
2769
  if (isObject(value[key]) || Array.isArray(value[key])) {
1757
- result[key] = value[key];
2770
+ complexKeys.push(key);
1758
2771
  }
2772
+ else {
2773
+ simpleKeys.push(key);
2774
+ }
2775
+ }
2776
+ // Create result with the correct order
2777
+ const result = {};
2778
+ // Add simple values first
2779
+ for (let i = 0; i < simpleKeys.length; i++) {
2780
+ const key = simpleKeys[i];
2781
+ result[key] = value[key];
2782
+ }
2783
+ // Then add complex values
2784
+ for (let i = 0; i < complexKeys.length; i++) {
2785
+ const key = complexKeys[i];
2786
+ result[key] = value[key];
1759
2787
  }
1760
2788
  return result;
1761
2789
  }
@@ -1841,7 +2869,25 @@ function toJSON(value) {
1841
2869
  }
1842
2870
 
1843
2871
  const BY_NEW_LINE = /(\r\n|\n)/g;
1844
- function toTOML(ast, newline = '\n') {
2872
+ /**
2873
+ * Converts an Abstract Syntax Tree (AST) back to TOML format string.
2874
+ *
2875
+ * This function traverses the AST and reconstructs the original TOML document
2876
+ * by writing each node's raw content to the appropriate location coordinates.
2877
+ * It preserves the original formatting, spacing, and structure of the TOML file.
2878
+ *
2879
+ * @param ast - The Abstract Syntax Tree representing the parsed TOML document
2880
+ * @param newline - The newline character(s) to use (\n by default)
2881
+ * @param options - Optional configuration object
2882
+ * @param options.trailingNewline - Number of trailing newlines to add (1 by default)
2883
+ * @returns The reconstructed TOML document as a string
2884
+ *
2885
+ * @example
2886
+ * ```typescript
2887
+ * const tomlString = toTOML(ast, '\n', { trailingNewline: 1 });
2888
+ * ```
2889
+ */
2890
+ function toTOML(ast, format) {
1845
2891
  const lines = [];
1846
2892
  traverse(ast, {
1847
2893
  [NodeType.TableKey](node) {
@@ -1896,8 +2942,34 @@ function toTOML(ast, newline = '\n') {
1896
2942
  write(lines, node.loc, node.raw);
1897
2943
  }
1898
2944
  });
1899
- return lines.join(newline) + newline;
2945
+ return lines.join(format.newLine) + format.newLine.repeat(format.trailingNewline);
1900
2946
  }
2947
+ /**
2948
+ * Writes raw string content to specific location coordinates within a lines array.
2949
+ *
2950
+ * This function is responsible for placing TOML content at precise positions within
2951
+ * the output lines, handling multi-line content and preserving existing content
2952
+ * around the target location.
2953
+ *
2954
+ * @param lines - Array of string lines representing the TOML document being built.
2955
+ * Lines are 1-indexed but stored in 0-indexed array.
2956
+ * @param loc - Location object specifying where to write the content, containing:
2957
+ * - start: { line: number, column: number } - Starting position (1-indexed line, 0-indexed column)
2958
+ * - end: { line: number, column: number } - Ending position (1-indexed line, 0-indexed column)
2959
+ * @param raw - The raw string content to write at the specified location.
2960
+ * Can contain multiple lines separated by \n or \r\n.
2961
+ *
2962
+ * @throws {Error} When there's a mismatch between location span and raw string line count
2963
+ * @throws {Error} When attempting to write to an uninitialized line
2964
+ *
2965
+ * @example
2966
+ * ```typescript
2967
+ * const lines = ['', ''];
2968
+ * const location = { start: { line: 1, column: 0 }, end: { line: 1, column: 3 } };
2969
+ * write(lines, location, 'key');
2970
+ * // Result: lines[0] becomes 'key'
2971
+ * ```
2972
+ */
1901
2973
  function write(lines, loc, raw) {
1902
2974
  const raw_lines = raw.split(BY_NEW_LINE).filter(line => line !== '\n' && line !== '\r\n');
1903
2975
  const expected_lines = loc.end.line - loc.start.line + 1;
@@ -1906,6 +2978,10 @@ function write(lines, loc, raw) {
1906
2978
  }
1907
2979
  for (let i = loc.start.line; i <= loc.end.line; i++) {
1908
2980
  const line = getLine(lines, i);
2981
+ //Throw if line is uninitialized
2982
+ if (line === undefined) {
2983
+ throw new Error(`Line ${i} is uninitialized when writing "${raw}" at ${loc.start.line}:${loc.start.column} to ${loc.end.line}:${loc.end.column}`);
2984
+ }
1909
2985
  const is_start_line = i === loc.start.line;
1910
2986
  const is_end_line = i === loc.end.line;
1911
2987
  const before = is_start_line
@@ -1915,6 +2991,24 @@ function write(lines, loc, raw) {
1915
2991
  lines[i - 1] = before + raw_lines[i - loc.start.line] + after;
1916
2992
  }
1917
2993
  }
2994
+ /**
2995
+ * Safely retrieves a line from the lines array, initializing empty lines as needed.
2996
+ *
2997
+ * This helper function handles the conversion between 1-indexed line numbers (used in locations)
2998
+ * and 0-indexed array positions. It ensures that accessing a line that doesn't exist yet
2999
+ * will initialize all preceding lines with empty strings.
3000
+ *
3001
+ * @param lines - Array of string lines representing the document
3002
+ * @param index - 1-indexed line number to retrieve
3003
+ * @returns The line content as a string, or empty string for new lines
3004
+ *
3005
+ * @example
3006
+ * ```typescript
3007
+ * const lines = ['first line'];
3008
+ * const line = getLine(lines, 3); // Initializes lines[1] and lines[2] as empty strings
3009
+ * // lines becomes ['first line', '', '']
3010
+ * ```
3011
+ */
1918
3012
  function getLine(lines, index) {
1919
3013
  if (!lines[index - 1]) {
1920
3014
  for (let i = 0; i < index; i++) {
@@ -1925,14 +3019,20 @@ function getLine(lines, index) {
1925
3019
  return lines[index - 1];
1926
3020
  }
1927
3021
 
3022
+ /**
3023
+ * Converts the given AST to a JavaScript object.
3024
+ *
3025
+ * @param ast The abstract syntax tree to convert.
3026
+ * @param input The original input string (used for error reporting).
3027
+ * @returns The JavaScript object representation of the AST.
3028
+ */
1928
3029
  function toJS(ast, input = '') {
1929
3030
  const result = blank();
1930
3031
  const tables = new Set();
1931
3032
  const table_arrays = new Set();
1932
3033
  const defined = new Set();
1933
3034
  let active = result;
1934
- let previous_active;
1935
- let skip = false;
3035
+ let skip_depth = 0;
1936
3036
  traverse(ast, {
1937
3037
  [NodeType.Table](node) {
1938
3038
  const key = node.key.item.value;
@@ -1964,7 +3064,7 @@ function toJS(ast, input = '') {
1964
3064
  },
1965
3065
  [NodeType.KeyValue]: {
1966
3066
  enter(node) {
1967
- if (skip)
3067
+ if (skip_depth > 0)
1968
3068
  return;
1969
3069
  const key = node.key.value;
1970
3070
  try {
@@ -1978,24 +3078,15 @@ function toJS(ast, input = '') {
1978
3078
  const target = key.length > 1 ? ensureTable(active, key.slice(0, -1)) : active;
1979
3079
  target[last(key)] = value;
1980
3080
  defined.add(joinKey(key));
1981
- if (isInlineTable(node.value)) {
1982
- previous_active = active;
1983
- active = value;
1984
- }
1985
- },
1986
- exit(node) {
1987
- if (isInlineTable(node.value)) {
1988
- active = previous_active;
1989
- }
1990
3081
  }
1991
3082
  },
1992
3083
  [NodeType.InlineTable]: {
1993
3084
  enter() {
1994
3085
  // Handled by toValue
1995
- skip = true;
3086
+ skip_depth++;
1996
3087
  },
1997
3088
  exit() {
1998
- skip = false;
3089
+ skip_depth--;
1999
3090
  }
2000
3091
  }
2001
3092
  });
@@ -2226,8 +3317,13 @@ function compareArrays(before, after, path = []) {
2226
3317
  }
2227
3318
 
2228
3319
  function findByPath(node, path) {
2229
- if (!path.length)
3320
+ if (!path.length) {
3321
+ // If this is an InlineItem containing a KeyValue, return the KeyValue
3322
+ if (isInlineItem(node) && isKeyValue(node.item)) {
3323
+ return node.item;
3324
+ }
2230
3325
  return node;
3326
+ }
2231
3327
  if (isKeyValue(node)) {
2232
3328
  return findByPath(node.value, path);
2233
3329
  }
@@ -2310,6 +3406,12 @@ function findParent(node, path) {
2310
3406
  */
2311
3407
  function patch(existing, updated, format) {
2312
3408
  const existing_ast = parseTOML(existing);
3409
+ // Auto-detect formatting preferences from the existing TOML string for fallback
3410
+ const autoDetectedFormat = TomlFormat.autoDetectFormat(existing);
3411
+ const fmt = resolveTomlFormat(format, autoDetectedFormat);
3412
+ return patchAst(existing_ast, updated, fmt).tomlString;
3413
+ }
3414
+ function patchAst(existing_ast, updated, format) {
2313
3415
  const items = [...existing_ast];
2314
3416
  const existing_js = toJS(items);
2315
3417
  const existing_document = {
@@ -2317,12 +3419,27 @@ function patch(existing, updated, format) {
2317
3419
  loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 0 } },
2318
3420
  items
2319
3421
  };
2320
- const updated_document = parseJS(updated, format);
3422
+ // Certain formatting options should not be applied to the updated document during patching, because it would
3423
+ // override the existing formatting too aggressively. For example, preferNestedTablesMultiline would
3424
+ // convert all nested tables to multiline, which is not be desired during patching.
3425
+ // Therefore, we create a modified format for generating the updated document used for diffing.
3426
+ const diffing_fmt = resolveTomlFormat(Object.assign(Object.assign({}, format), { inlineTableStart: undefined }), format);
3427
+ const updated_document = parseJS(updated, diffing_fmt);
2321
3428
  const changes = reorder(diff(existing_js, updated));
2322
- const patched_document = applyChanges(existing_document, updated_document, changes);
3429
+ if (changes.length === 0) {
3430
+ return {
3431
+ tomlString: toTOML(items, format),
3432
+ document: existing_document
3433
+ };
3434
+ }
3435
+ const patched_document = applyChanges(existing_document, updated_document, changes, format);
2323
3436
  // Validate the patched_document
3437
+ // This would prevent overlapping element positions in the AST, but since those are handled at stringification time, we can skip this for now
2324
3438
  //validate(patched_document);
2325
- return toTOML(patched_document.items);
3439
+ return {
3440
+ tomlString: toTOML(patched_document.items, format),
3441
+ document: patched_document
3442
+ };
2326
3443
  }
2327
3444
  function reorder(changes) {
2328
3445
  for (let i = 0; i < changes.length; i++) {
@@ -2345,7 +3462,30 @@ function reorder(changes) {
2345
3462
  }
2346
3463
  return changes;
2347
3464
  }
2348
- function applyChanges(original, updated, changes) {
3465
+ /**
3466
+ * Applies a list of changes to the original TOML document AST while preserving formatting and structure.
3467
+ *
3468
+ * This function processes different types of changes (Add, Edit, Remove, Move, Rename) and applies them
3469
+ * to the original document in a way that maintains the existing formatting preferences, comments, and
3470
+ * structural elements as much as possible. Special handling is provided for different node types like
3471
+ * inline tables, arrays, and table arrays to ensure proper formatting consistency.
3472
+ *
3473
+ * @param original - The original TOML document AST to be modified
3474
+ * @param updated - The updated document AST containing new values for changes
3475
+ * @param changes - Array of change objects describing what modifications to apply
3476
+ * @param format - Formatting preferences to use for newly added elements
3477
+ * @returns The modified original document with all changes applied
3478
+ *
3479
+ * @example
3480
+ * ```typescript
3481
+ * const changes = [
3482
+ * { type: 'add', path: ['newKey'], value: 'newValue' },
3483
+ * { type: 'edit', path: ['existingKey'], value: 'updatedValue' }
3484
+ * ];
3485
+ * const result = applyChanges(originalDoc, updatedDoc, changes, format);
3486
+ * ```
3487
+ */
3488
+ function applyChanges(original, updated, changes, format) {
2349
3489
  // Potential Changes:
2350
3490
  //
2351
3491
  // Add: Add key-value to object, add item to array
@@ -2367,6 +3507,7 @@ function applyChanges(original, updated, changes) {
2367
3507
  is_table_array = true;
2368
3508
  }
2369
3509
  }
3510
+ // Determine the parent node where the new child will be inserted
2370
3511
  let parent;
2371
3512
  if (isTable(child)) {
2372
3513
  parent = original;
@@ -2390,14 +3531,71 @@ function applyChanges(original, updated, changes) {
2390
3531
  }
2391
3532
  else {
2392
3533
  parent = findParent(original, change.path);
2393
- if (isKeyValue(parent))
3534
+ if (isKeyValue(parent)) {
2394
3535
  parent = parent.value;
3536
+ }
2395
3537
  }
2396
3538
  if (isTableArray(parent) || isInlineArray(parent) || isDocument(parent)) {
2397
- insert(original, parent, child, index);
3539
+ // Special handling for InlineArray: preserve original trailing comma format
3540
+ if (isInlineArray(parent)) {
3541
+ const originalHadTrailingCommas = arrayHadTrailingCommas(parent);
3542
+ // If this is an InlineItem being added to an array, check its comma setting
3543
+ if (isInlineItem(child)) {
3544
+ // The child comes from the updated document with global format applied
3545
+ // Override with the original array's format
3546
+ child.comma = originalHadTrailingCommas;
3547
+ }
3548
+ }
3549
+ // Check if we should convert nested inline tables to multiline tables
3550
+ if (format.inlineTableStart !== undefined && format.inlineTableStart > 0 && isDocument(parent) && isTable(child)) {
3551
+ const additionalTables = convertNestedInlineTablesToMultiline(child, original, format);
3552
+ // Insert the main table first
3553
+ insert(original, parent, child, index);
3554
+ // Then insert all the additional tables
3555
+ for (const table of additionalTables) {
3556
+ insert(original, original, table, undefined);
3557
+ }
3558
+ }
3559
+ else {
3560
+ insert(original, parent, child, index);
3561
+ }
3562
+ }
3563
+ else if (isInlineTable(parent)) {
3564
+ // Special handling for adding KeyValue to InlineTable
3565
+ // Preserve original trailing comma format
3566
+ const originalHadTrailingCommas = tableHadTrailingCommas(parent);
3567
+ // InlineTable items must be wrapped in InlineItem
3568
+ if (isKeyValue(child)) {
3569
+ const inlineItem = generateInlineItem(child);
3570
+ // Override with the original table's format
3571
+ inlineItem.comma = originalHadTrailingCommas;
3572
+ insert(original, parent, inlineItem);
3573
+ }
3574
+ else {
3575
+ insert(original, parent, child);
3576
+ }
2398
3577
  }
2399
3578
  else {
2400
- insert(original, parent, child);
3579
+ // Check if we should convert inline tables to multiline tables when adding to existing tables
3580
+ if (format.inlineTableStart !== undefined && format.inlineTableStart > 0 && isKeyValue(child) && isInlineTable(child.value) && isTable(parent)) {
3581
+ // Calculate the depth of the inline table that would be created
3582
+ const baseTableKey = parent.key.item.value;
3583
+ const nestedTableKey = [...baseTableKey, ...child.key.value];
3584
+ const depth = calculateTableDepth(nestedTableKey);
3585
+ // Convert to separate section only if depth is less than inlineTableStart
3586
+ if (depth < format.inlineTableStart) {
3587
+ convertInlineTableToSeparateSection(child, parent, original, format);
3588
+ }
3589
+ else {
3590
+ insert(original, parent, child);
3591
+ }
3592
+ }
3593
+ else if (format.inlineTableStart === 0 && isKeyValue(child) && isInlineTable(child.value) && isDocument(parent)) {
3594
+ insert(original, parent, child, undefined, true);
3595
+ }
3596
+ else {
3597
+ insert(original, parent, child);
3598
+ }
2401
3599
  }
2402
3600
  }
2403
3601
  else if (isEdit(change)) {
@@ -2406,6 +3604,26 @@ function applyChanges(original, updated, changes) {
2406
3604
  let parent;
2407
3605
  if (isKeyValue(existing) && isKeyValue(replacement)) {
2408
3606
  // Edit for key-value means value changes
3607
+ // Special handling for arrays: preserve original trailing comma format
3608
+ if (isInlineArray(existing.value) && isInlineArray(replacement.value)) {
3609
+ const originalHadTrailingCommas = arrayHadTrailingCommas(existing.value);
3610
+ const newArray = replacement.value;
3611
+ // Apply or remove trailing comma based on original format
3612
+ if (newArray.items.length > 0) {
3613
+ const lastItem = newArray.items[newArray.items.length - 1];
3614
+ lastItem.comma = originalHadTrailingCommas;
3615
+ }
3616
+ }
3617
+ // Special handling for inline tables: preserve original trailing comma format
3618
+ if (isInlineTable(existing.value) && isInlineTable(replacement.value)) {
3619
+ const originalHadTrailingCommas = tableHadTrailingCommas(existing.value);
3620
+ const newTable = replacement.value;
3621
+ // Apply or remove trailing comma based on original format
3622
+ if (newTable.items.length > 0) {
3623
+ const lastItem = newTable.items[newTable.items.length - 1];
3624
+ lastItem.comma = originalHadTrailingCommas;
3625
+ }
3626
+ }
2409
3627
  parent = existing;
2410
3628
  existing = existing.value;
2411
3629
  replacement = replacement.value;
@@ -2417,8 +3635,23 @@ function applyChanges(original, updated, changes) {
2417
3635
  existing = existing.value;
2418
3636
  replacement = replacement.item.value;
2419
3637
  }
3638
+ else if (isInlineItem(existing) && isKeyValue(replacement)) {
3639
+ // Editing inline table item: existing is InlineItem, replacement is KeyValue
3640
+ // We need to replace the KeyValue inside the InlineItem, preserving the InlineItem wrapper
3641
+ parent = existing;
3642
+ existing = existing.item;
3643
+ }
2420
3644
  else {
2421
3645
  parent = findParent(original, change.path);
3646
+ // Special handling for array element edits
3647
+ if (isKeyValue(parent)) {
3648
+ // Check if we're actually editing an array element
3649
+ const parentPath = change.path.slice(0, -1);
3650
+ const arrayNode = findByPath(original, parentPath);
3651
+ if (isKeyValue(arrayNode) && isInlineArray(arrayNode.value)) {
3652
+ parent = arrayNode.value;
3653
+ }
3654
+ }
2422
3655
  }
2423
3656
  replace(original, parent, existing, replacement);
2424
3657
  }
@@ -2452,10 +3685,335 @@ function applyChanges(original, updated, changes) {
2452
3685
  applyWrites(original);
2453
3686
  return original;
2454
3687
  }
3688
+ /**
3689
+ * Converts nested inline tables to separate table sections based on the inlineTableStart depth setting.
3690
+ * This function recursively processes a table and extracts any inline tables within it,
3691
+ * creating separate table sections with properly nested keys.
3692
+ *
3693
+ * @param table - The table to process for nested inline tables
3694
+ * @param original - The original document for inserting new items
3695
+ * @param format - The formatting options
3696
+ * @returns Array of additional tables that should be added to the document
3697
+ */
3698
+ function convertNestedInlineTablesToMultiline(table, original, format) {
3699
+ const additionalTables = [];
3700
+ const processTableForNestedInlines = (currentTable, tablesToAdd) => {
3701
+ var _a;
3702
+ for (let i = currentTable.items.length - 1; i >= 0; i--) {
3703
+ const item = currentTable.items[i];
3704
+ if (isKeyValue(item) && isInlineTable(item.value)) {
3705
+ // Calculate the depth of this nested table
3706
+ const nestedTableKey = [...currentTable.key.item.value, ...item.key.value];
3707
+ const depth = calculateTableDepth(nestedTableKey);
3708
+ // Only convert to separate table if depth is less than inlineTableStart
3709
+ if (depth < ((_a = format.inlineTableStart) !== null && _a !== void 0 ? _a : 1) && format.inlineTableStart !== 0) {
3710
+ // Convert this inline table to a separate table section
3711
+ const separateTable = generateTable(nestedTableKey);
3712
+ // Move all items from the inline table to the separate table
3713
+ for (const inlineItem of item.value.items) {
3714
+ if (isInlineItem(inlineItem) && isKeyValue(inlineItem.item)) {
3715
+ insert(original, separateTable, inlineItem.item, undefined);
3716
+ }
3717
+ }
3718
+ // Remove this item from the original table
3719
+ currentTable.items.splice(i, 1);
3720
+ // Update the parent table's end position after removal
3721
+ postInlineItemRemovalAdjustment(currentTable);
3722
+ // Queue this table to be added to the document
3723
+ tablesToAdd.push(separateTable);
3724
+ // Recursively process the new table for further nested inlines
3725
+ processTableForNestedInlines(separateTable, tablesToAdd);
3726
+ }
3727
+ }
3728
+ }
3729
+ };
3730
+ processTableForNestedInlines(table, additionalTables);
3731
+ return additionalTables;
3732
+ }
3733
+ /**
3734
+ * Converts an inline table to a separate table section when adding to an existing table.
3735
+ * This function creates a new table section with the combined key path and moves all
3736
+ * properties from the inline table to the separate table section.
3737
+ *
3738
+ * @param child - The KeyValue node with an InlineTable as its value
3739
+ * @param parent - The parent table where the KeyValue would be added
3740
+ * @param original - The original document for inserting new items
3741
+ * @param format - The formatting options
3742
+ */
3743
+ function convertInlineTableToSeparateSection(child, parent, original, format) {
3744
+ // Convert the inline table to a separate table section
3745
+ const baseTableKey = parent.key.item.value; // Get the parent table's key path
3746
+ const nestedTableKey = [...baseTableKey, ...child.key.value]; // Combine with the new key
3747
+ const separateTable = generateTable(nestedTableKey);
3748
+ // We know child.value is an InlineTable from the calling context
3749
+ if (isInlineTable(child.value)) {
3750
+ // Move all items from the inline table to the separate table
3751
+ for (const inlineItem of child.value.items) {
3752
+ if (isInlineItem(inlineItem) && isKeyValue(inlineItem.item)) {
3753
+ insert(original, separateTable, inlineItem.item, undefined);
3754
+ }
3755
+ }
3756
+ }
3757
+ // Add the separate table to the document
3758
+ insert(original, original, separateTable, undefined);
3759
+ // Update the parent table's end position since we're not adding the inline table to it
3760
+ postInlineItemRemovalAdjustment(parent);
3761
+ // Also handle any nested inline tables within the new table
3762
+ const additionalTables = convertNestedInlineTablesToMultiline(separateTable, original, format);
3763
+ for (const table of additionalTables) {
3764
+ insert(original, original, table, undefined);
3765
+ }
3766
+ }
3767
+
3768
+ /******************************************************************************
3769
+ Copyright (c) Microsoft Corporation.
3770
+
3771
+ Permission to use, copy, modify, and/or distribute this software for any
3772
+ purpose with or without fee is hereby granted.
3773
+
3774
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
3775
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
3776
+ AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
3777
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
3778
+ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
3779
+ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
3780
+ PERFORMANCE OF THIS SOFTWARE.
3781
+ ***************************************************************************** */
3782
+ /* global Reflect, Promise, SuppressedError, Symbol, Iterator */
3783
+
3784
+
3785
+ function __classPrivateFieldGet(receiver, state, kind, f) {
3786
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
3787
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
3788
+ return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
3789
+ }
3790
+
3791
+ function __classPrivateFieldSet(receiver, state, value, kind, f) {
3792
+ if (kind === "m") throw new TypeError("Private method is not writable");
3793
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
3794
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
3795
+ return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
3796
+ }
3797
+
3798
+ typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
3799
+ var e = new Error(message);
3800
+ return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
3801
+ };
3802
+
3803
+ /**
3804
+ * Compares two positions to determine their ordering.
3805
+ * @param pos1 - First position
3806
+ * @param pos2 - Second position
3807
+ * @returns Negative if pos1 < pos2, 0 if equal, positive if pos1 > pos2
3808
+ */
3809
+ function comparePositions(pos1, pos2) {
3810
+ if (pos1.line !== pos2.line) {
3811
+ return pos1.line - pos2.line;
3812
+ }
3813
+ return pos1.column - pos2.column;
3814
+ }
3815
+ /**
3816
+ * Truncates an AST based on a position (line, column) in the source string.
3817
+ *
3818
+ * This function filters the AST to include only the nodes that end before
3819
+ * the specified position. This ensures that blocks containing changes are
3820
+ * excluded and can be reparsed. This is useful for incremental parsing scenarios
3821
+ * where you want to keep only the unchanged portion of the AST.
3822
+ *
3823
+ * Special handling: If the truncation point falls within a Table or TableArray
3824
+ * (e.g., in a comment inside the table), the entire table is excluded to ensure
3825
+ * proper reparsing.
3826
+ *
3827
+ * @param ast - The AST to truncate
3828
+ * @param line - The line number (1-indexed) at which to truncate
3829
+ * @param column - The column number (0-indexed) at which to truncate
3830
+ * @returns An object containing the truncated AST and the end position of the last included node
3831
+ *
3832
+ * @example
3833
+ * ```typescript
3834
+ * const ast = parseTOML(tomlString);
3835
+ * // Get AST up to line 5, column 10 (only nodes that end before this position)
3836
+ * const { truncatedAst, lastEndPosition } = truncateAst(ast, 5, 10);
3837
+ * for (const node of truncatedAst) {
3838
+ * // process node
3839
+ * }
3840
+ * ```
3841
+ */
3842
+ function truncateAst(ast, line, column) {
3843
+ const limit = { line, column };
3844
+ const nodes = [];
3845
+ let lastEndPosition = null;
3846
+ for (const node of ast) {
3847
+ const nodeEndsBeforeLimit = comparePositions(node.loc.end, limit) < 0;
3848
+ const nodeStartsBeforeLimit = comparePositions(node.loc.start, limit) < 0;
3849
+ if (nodeEndsBeforeLimit) {
3850
+ // Node completely ends before the limit - include it
3851
+ nodes.push(node);
3852
+ lastEndPosition = node.loc.end;
3853
+ }
3854
+ else if (nodeStartsBeforeLimit && !nodeEndsBeforeLimit) {
3855
+ // Node starts before the limit but ends at or after it
3856
+ // This means the truncation point is within this node
3857
+ // For Table/TableArray nodes, don't include them if the change is inside
3858
+ // This ensures the entire table gets reparsed
3859
+ break;
3860
+ }
3861
+ else {
3862
+ // Node starts at or after the limit - stop
3863
+ break;
3864
+ }
3865
+ }
3866
+ return {
3867
+ truncatedAst: nodes,
3868
+ lastEndPosition
3869
+ };
3870
+ }
3871
+
3872
+ var _TomlDocument_ast, _TomlDocument_currentTomlString, _TomlDocument_Format;
3873
+ /**
3874
+ * TomlDocument encapsulates a TOML AST and provides methods to interact with it.
3875
+ */
3876
+ class TomlDocument {
3877
+ /**
3878
+ * Initializes the TomlDocument with a TOML string, parsing it into an AST.
3879
+ * @param tomlString - The TOML string to parse
3880
+ */
3881
+ constructor(tomlString) {
3882
+ _TomlDocument_ast.set(this, void 0);
3883
+ _TomlDocument_currentTomlString.set(this, void 0);
3884
+ _TomlDocument_Format.set(this, void 0);
3885
+ __classPrivateFieldSet(this, _TomlDocument_currentTomlString, tomlString, "f");
3886
+ __classPrivateFieldSet(this, _TomlDocument_ast, Array.from(parseTOML(tomlString)), "f");
3887
+ // Auto-detect formatting preferences from the original TOML string
3888
+ __classPrivateFieldSet(this, _TomlDocument_Format, TomlFormat.autoDetectFormat(tomlString), "f");
3889
+ }
3890
+ get toTomlString() {
3891
+ return __classPrivateFieldGet(this, _TomlDocument_currentTomlString, "f");
3892
+ }
3893
+ /**
3894
+ * Returns the JavaScript object representation of the TOML document.
3895
+ */
3896
+ get toJsObject() {
3897
+ const jsObject = toJS(__classPrivateFieldGet(this, _TomlDocument_ast, "f"));
3898
+ // Convert custom date classes to regular JavaScript Date objects
3899
+ return convertCustomDateClasses(jsObject);
3900
+ }
3901
+ /**
3902
+ * Returns the internal AST (for testing purposes).
3903
+ * @internal
3904
+ */
3905
+ get ast() {
3906
+ return __classPrivateFieldGet(this, _TomlDocument_ast, "f");
3907
+ }
3908
+ /**
3909
+ * Applies a patch to the current AST using a modified JS object.
3910
+ * Updates the internal AST. Use toTomlString getter to retrieve the updated TOML string.
3911
+ * @param updatedObject - The modified JS object to patch with
3912
+ * @param format - Optional formatting options
3913
+ */
3914
+ patch(updatedObject, format) {
3915
+ const fmt = resolveTomlFormat(format, __classPrivateFieldGet(this, _TomlDocument_Format, "f"));
3916
+ const { tomlString, document } = patchAst(__classPrivateFieldGet(this, _TomlDocument_ast, "f"), updatedObject, fmt);
3917
+ __classPrivateFieldSet(this, _TomlDocument_ast, document.items, "f");
3918
+ __classPrivateFieldSet(this, _TomlDocument_currentTomlString, tomlString, "f");
3919
+ }
3920
+ /**
3921
+ * Updates the internal document by supplying a modified tomlString.
3922
+ * Use toJsObject getter to retrieve the updated JS object representation.
3923
+ * @param tomlString - The modified TOML string to update with
3924
+ */
3925
+ update(tomlString) {
3926
+ if (tomlString === this.toTomlString) {
3927
+ return;
3928
+ }
3929
+ // Now, let's check where the first difference is
3930
+ const existingLines = this.toTomlString.split(__classPrivateFieldGet(this, _TomlDocument_Format, "f").newLine);
3931
+ const newLineChar = detectNewline(tomlString);
3932
+ const newTextLines = tomlString.split(newLineChar);
3933
+ let firstDiffLineIndex = 0;
3934
+ while (firstDiffLineIndex < existingLines.length &&
3935
+ firstDiffLineIndex < newTextLines.length &&
3936
+ existingLines[firstDiffLineIndex] === newTextLines[firstDiffLineIndex]) {
3937
+ firstDiffLineIndex++;
3938
+ }
3939
+ // Calculate the 1-based line number and 0-based column where the first difference occurs
3940
+ let firstDiffColumn = 0;
3941
+ // If we're within the bounds of both arrays, find the column where they differ
3942
+ if (firstDiffLineIndex < existingLines.length && firstDiffLineIndex < newTextLines.length) {
3943
+ const existingLine = existingLines[firstDiffLineIndex];
3944
+ const newLine = newTextLines[firstDiffLineIndex];
3945
+ // Find the first character position where the lines differ
3946
+ for (let i = 0; i < Math.max(existingLine.length, newLine.length); i++) {
3947
+ if (existingLine[i] !== newLine[i]) {
3948
+ firstDiffColumn = i;
3949
+ break;
3950
+ }
3951
+ }
3952
+ }
3953
+ let firstDiffLine = firstDiffLineIndex + 1; // Convert to 1-based
3954
+ const { truncatedAst, lastEndPosition } = truncateAst(__classPrivateFieldGet(this, _TomlDocument_ast, "f"), firstDiffLine, firstDiffColumn);
3955
+ // Determine where to continue parsing from in the new string
3956
+ // If lastEndPosition exists, continue from there; otherwise from the start of the document
3957
+ const continueFromLine = lastEndPosition ? lastEndPosition.line : 1;
3958
+ const continueFromColumn = lastEndPosition ? lastEndPosition.column + 1 : 0;
3959
+ // Based on the first difference, we can re-parse only the affected part
3960
+ // We will need to supply the remaining string after where the AST was truncated
3961
+ const remainingLines = newTextLines.slice(continueFromLine - 1);
3962
+ // If there's a partial line match, we need to extract only the part after the continuation column
3963
+ if (remainingLines.length > 0 && continueFromColumn > 0) {
3964
+ remainingLines[0] = remainingLines[0].substring(continueFromColumn);
3965
+ }
3966
+ const remainingToml = remainingLines.join(__classPrivateFieldGet(this, _TomlDocument_Format, "f").newLine);
3967
+ __classPrivateFieldSet(this, _TomlDocument_ast, Array.from(continueParsingTOML(truncatedAst, remainingToml)), "f");
3968
+ __classPrivateFieldSet(this, _TomlDocument_currentTomlString, tomlString, "f");
3969
+ // Update the auto-detected format with the new string's characteristics
3970
+ __classPrivateFieldSet(this, _TomlDocument_Format, TomlFormat.autoDetectFormat(tomlString), "f");
3971
+ }
3972
+ /**
3973
+ * Overwrites the internal AST by fully re-parsing the supplied tomlString.
3974
+ * This is simpler but slower than update() which uses incremental parsing.
3975
+ * @param tomlString - The TOML string to overwrite with
3976
+ */
3977
+ overwrite(tomlString) {
3978
+ if (tomlString === this.toTomlString) {
3979
+ return;
3980
+ }
3981
+ // Re-parse the entire document
3982
+ __classPrivateFieldSet(this, _TomlDocument_ast, Array.from(parseTOML(tomlString)), "f");
3983
+ __classPrivateFieldSet(this, _TomlDocument_currentTomlString, tomlString, "f");
3984
+ // Update the auto-detected format with the new string's characteristics
3985
+ __classPrivateFieldSet(this, _TomlDocument_Format, TomlFormat.autoDetectFormat(tomlString), "f");
3986
+ }
3987
+ }
3988
+ _TomlDocument_ast = new WeakMap(), _TomlDocument_currentTomlString = new WeakMap(), _TomlDocument_Format = new WeakMap();
3989
+ /**
3990
+ * Recursively converts custom date classes to regular JavaScript Date objects.
3991
+ * This ensures that the toJsObject property returns standard Date objects
3992
+ * while preserving the custom classes internally for TOML formatting.
3993
+ */
3994
+ function convertCustomDateClasses(obj) {
3995
+ if (obj instanceof Date) {
3996
+ // Convert custom date classes to regular Date objects
3997
+ return new Date(obj.getTime());
3998
+ }
3999
+ else if (Array.isArray(obj)) {
4000
+ return obj.map(convertCustomDateClasses);
4001
+ }
4002
+ else if (obj && typeof obj === 'object') {
4003
+ const result = {};
4004
+ for (const [key, value] of Object.entries(obj)) {
4005
+ result[key] = convertCustomDateClasses(value);
4006
+ }
4007
+ return result;
4008
+ }
4009
+ return obj;
4010
+ }
2455
4011
 
2456
4012
  /**
2457
4013
  * Parses a TOML string into a JavaScript object.
2458
4014
  * The function converts TOML syntax to its JavaScript equivalent.
4015
+ * This proceeds in two steps: first, it parses the TOML string into an AST,
4016
+ * and then it converts the AST into a JavaScript object.
2459
4017
  *
2460
4018
  * @param value - The TOML string to parse
2461
4019
  * @returns The parsed JavaScript object
@@ -2471,8 +4029,9 @@ function parse(value) {
2471
4029
  * @returns The stringified TOML representation
2472
4030
  */
2473
4031
  function stringify(value, format) {
2474
- const document = parseJS(value, format);
2475
- return toTOML(document.items);
4032
+ const fmt = resolveTomlFormat(format, TomlFormat.default());
4033
+ const document = parseJS(value, fmt);
4034
+ return toTOML(document.items, fmt);
2476
4035
  }
2477
4036
 
2478
- export { parse, patch, stringify };
4037
+ export { TomlDocument, TomlFormat, parse, patch, stringify };