@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.
- package/LICENSE +2 -1
- package/README.md +468 -17
- package/dist/toml-patch.cjs.min.js +2 -2
- package/dist/toml-patch.cjs.min.js.map +1 -1
- package/dist/toml-patch.d.ts +291 -10
- package/dist/toml-patch.es.js +1664 -105
- package/dist/toml-patch.umd.min.js +2 -2
- package/dist/toml-patch.umd.min.js.map +1 -1
- package/package.json +22 -12
package/dist/toml-patch.es.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
//! @decimalturn/toml-patch v0.
|
|
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 =
|
|
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
|
-
//
|
|
837
|
-
|
|
838
|
-
|
|
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
|
-
|
|
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
|
|
1626
|
+
const isSquareBracketsStructure = isTable(child) || isTableArray(child);
|
|
1180
1627
|
let leading_lines = 0;
|
|
1181
1628
|
if (use_first_line) ;
|
|
1182
|
-
else if (
|
|
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
|
-
*
|
|
1203
|
-
* This function handles positioning
|
|
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
|
|
1206
|
-
* @param child - The
|
|
1207
|
-
* @param index - The index
|
|
1208
|
-
* @
|
|
1209
|
-
*
|
|
1210
|
-
*
|
|
1211
|
-
* @
|
|
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
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
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:
|
|
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 (
|
|
1683
|
+
if (useNewLine) {
|
|
1248
1684
|
leading_lines = 1;
|
|
1249
1685
|
}
|
|
1250
1686
|
else {
|
|
1251
|
-
|
|
1252
|
-
const
|
|
1253
|
-
|
|
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 +
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1740
|
-
|
|
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
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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 (
|
|
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
|
-
|
|
3086
|
+
skip_depth++;
|
|
1996
3087
|
},
|
|
1997
3088
|
exit() {
|
|
1998
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
2475
|
-
|
|
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 };
|