@decimalturn/toml-patch 0.6.0 → 1.0.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/CHANGELOG.md +145 -0
- package/README.md +10 -22
- package/dist/toml-patch.d.ts +1 -1
- package/dist/toml-patch.js +2 -0
- package/package.json +18 -21
- package/dist/toml-patch.cjs.min.js +0 -3
- package/dist/toml-patch.cjs.min.js.map +0 -1
- package/dist/toml-patch.es.js +0 -4300
- package/dist/toml-patch.umd.min.js +0 -3
- package/dist/toml-patch.umd.min.js.map +0 -1
package/dist/toml-patch.es.js
DELETED
|
@@ -1,4300 +0,0 @@
|
|
|
1
|
-
//! @decimalturn/toml-patch v0.6.0 - https://github.com/DecimalTurn/toml-patch - @license: MIT
|
|
2
|
-
var NodeType;
|
|
3
|
-
(function (NodeType) {
|
|
4
|
-
NodeType["Document"] = "Document";
|
|
5
|
-
NodeType["Table"] = "Table";
|
|
6
|
-
NodeType["TableKey"] = "TableKey";
|
|
7
|
-
/**
|
|
8
|
-
* Array of Tables node
|
|
9
|
-
* More info: https://toml.io/en/latest#array-of-tables
|
|
10
|
-
*/
|
|
11
|
-
NodeType["TableArray"] = "TableArray";
|
|
12
|
-
NodeType["TableArrayKey"] = "TableArrayKey";
|
|
13
|
-
NodeType["KeyValue"] = "KeyValue";
|
|
14
|
-
NodeType["Key"] = "Key";
|
|
15
|
-
NodeType["String"] = "String";
|
|
16
|
-
NodeType["Integer"] = "Integer";
|
|
17
|
-
NodeType["Float"] = "Float";
|
|
18
|
-
NodeType["Boolean"] = "Boolean";
|
|
19
|
-
NodeType["DateTime"] = "DateTime";
|
|
20
|
-
NodeType["InlineArray"] = "InlineArray";
|
|
21
|
-
NodeType["InlineItem"] = "InlineItem";
|
|
22
|
-
NodeType["InlineTable"] = "InlineTable";
|
|
23
|
-
/**
|
|
24
|
-
* Comment node
|
|
25
|
-
* More info: https://toml.io/en/latest#comment
|
|
26
|
-
*/
|
|
27
|
-
NodeType["Comment"] = "Comment";
|
|
28
|
-
})(NodeType || (NodeType = {}));
|
|
29
|
-
function isDocument(node) {
|
|
30
|
-
return node.type === NodeType.Document;
|
|
31
|
-
}
|
|
32
|
-
function isTable(node) {
|
|
33
|
-
return node.type === NodeType.Table;
|
|
34
|
-
}
|
|
35
|
-
function isTableKey(node) {
|
|
36
|
-
return node.type === NodeType.TableKey;
|
|
37
|
-
}
|
|
38
|
-
/**
|
|
39
|
-
* Is a TableArray (aka array of tables)
|
|
40
|
-
* @param node
|
|
41
|
-
* @returns
|
|
42
|
-
*/
|
|
43
|
-
function isTableArray(node) {
|
|
44
|
-
return node.type === NodeType.TableArray;
|
|
45
|
-
}
|
|
46
|
-
function isTableArrayKey(node) {
|
|
47
|
-
return node.type === NodeType.TableArrayKey;
|
|
48
|
-
}
|
|
49
|
-
function isKeyValue(node) {
|
|
50
|
-
return node.type === NodeType.KeyValue;
|
|
51
|
-
}
|
|
52
|
-
function isString$1(node) {
|
|
53
|
-
return node.type === NodeType.String;
|
|
54
|
-
}
|
|
55
|
-
function isDateTime(node) {
|
|
56
|
-
return node.type === NodeType.DateTime;
|
|
57
|
-
}
|
|
58
|
-
function isInlineArray(node) {
|
|
59
|
-
return node.type === NodeType.InlineArray;
|
|
60
|
-
}
|
|
61
|
-
function isInlineItem(node) {
|
|
62
|
-
return node.type === NodeType.InlineItem;
|
|
63
|
-
}
|
|
64
|
-
function isInlineTable(node) {
|
|
65
|
-
return node.type === NodeType.InlineTable;
|
|
66
|
-
}
|
|
67
|
-
function isComment(node) {
|
|
68
|
-
return node.type === NodeType.Comment;
|
|
69
|
-
}
|
|
70
|
-
function hasItems(node) {
|
|
71
|
-
return (isDocument(node) ||
|
|
72
|
-
isTable(node) ||
|
|
73
|
-
isTableArray(node) ||
|
|
74
|
-
isInlineTable(node) ||
|
|
75
|
-
isInlineArray(node));
|
|
76
|
-
}
|
|
77
|
-
function hasItem(node) {
|
|
78
|
-
return isTableKey(node) || isTableArrayKey(node) || isInlineItem(node);
|
|
79
|
-
}
|
|
80
|
-
function isBlock(node) {
|
|
81
|
-
return isKeyValue(node) || isTable(node) || isTableArray(node) || isComment(node);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function iterator(value) {
|
|
85
|
-
return value[Symbol.iterator]();
|
|
86
|
-
}
|
|
87
|
-
/**
|
|
88
|
-
* Cursor<T>
|
|
89
|
-
*
|
|
90
|
-
* A utility class that wraps an iterator and provides additional functionality
|
|
91
|
-
* such as peeking at the next value without advancing the iterator, tracking
|
|
92
|
-
* the current index, and iterating over the values.
|
|
93
|
-
*
|
|
94
|
-
* @template T - The type of elements in the iterator.
|
|
95
|
-
*
|
|
96
|
-
* Properties:
|
|
97
|
-
* - `iterator`: The underlying iterator being wrapped.
|
|
98
|
-
* - `index`: The current index of the iterator (starts at -1).
|
|
99
|
-
* - `value`: The current value of the iterator.
|
|
100
|
-
* - `done`: A boolean indicating whether the iterator is complete.
|
|
101
|
-
* - `peeked`: The result of peeking at the next value without advancing.
|
|
102
|
-
*
|
|
103
|
-
* Methods:
|
|
104
|
-
* - `next()`: Advances the iterator and returns the next value.
|
|
105
|
-
* - `peek()`: Returns the next value without advancing the iterator.
|
|
106
|
-
* - `[Symbol.iterator]`: Makes the Cursor itself iterable.
|
|
107
|
-
*/
|
|
108
|
-
class Cursor {
|
|
109
|
-
constructor(iterator) {
|
|
110
|
-
this.iterator = iterator;
|
|
111
|
-
this.index = -1;
|
|
112
|
-
this.value = undefined;
|
|
113
|
-
this.done = false;
|
|
114
|
-
this.peeked = null;
|
|
115
|
-
}
|
|
116
|
-
next() {
|
|
117
|
-
var _a;
|
|
118
|
-
if (this.done)
|
|
119
|
-
return done();
|
|
120
|
-
const result = this.peeked || this.iterator.next();
|
|
121
|
-
this.index += 1;
|
|
122
|
-
this.value = result.value;
|
|
123
|
-
this.done = (_a = result.done) !== null && _a !== void 0 ? _a : false;
|
|
124
|
-
this.peeked = null;
|
|
125
|
-
return result;
|
|
126
|
-
}
|
|
127
|
-
peek() {
|
|
128
|
-
if (this.done)
|
|
129
|
-
return done();
|
|
130
|
-
if (this.peeked)
|
|
131
|
-
return this.peeked;
|
|
132
|
-
this.peeked = this.iterator.next();
|
|
133
|
-
return this.peeked;
|
|
134
|
-
}
|
|
135
|
-
[Symbol.iterator]() {
|
|
136
|
-
return this;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
function done() {
|
|
140
|
-
return { value: undefined, done: true };
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function getSpan(location) {
|
|
144
|
-
return {
|
|
145
|
-
lines: location.end.line - location.start.line + 1,
|
|
146
|
-
columns: location.end.column - location.start.column
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
|
-
function createLocate(input) {
|
|
150
|
-
const lines = findLines(input);
|
|
151
|
-
return (start, end) => {
|
|
152
|
-
return {
|
|
153
|
-
start: findPosition(lines, start),
|
|
154
|
-
end: findPosition(lines, end)
|
|
155
|
-
};
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
function findPosition(input, index) {
|
|
159
|
-
// abc\ndef\ng
|
|
160
|
-
// 0123 4567 8
|
|
161
|
-
// 012
|
|
162
|
-
// 0
|
|
163
|
-
//
|
|
164
|
-
// lines = [3, 7, 9]
|
|
165
|
-
//
|
|
166
|
-
// c = 2: 0 -> 1, 2 - (undefined + 1 || 0) = 2
|
|
167
|
-
// 3: 0 -> 1, 3 - (undefined + 1 || 0) = 3
|
|
168
|
-
// e = 5: 1 -> 2, 5 - (3 + 1 || 0) = 1
|
|
169
|
-
// g = 8: 2 -> 3, 8 - (7 + 1 || 0) = 0
|
|
170
|
-
const lines = Array.isArray(input) ? input : findLines(input);
|
|
171
|
-
const line = lines.findIndex(line_index => line_index >= index) + 1;
|
|
172
|
-
const column = index - (lines[line - 2] + 1 || 0);
|
|
173
|
-
return { line, column };
|
|
174
|
-
}
|
|
175
|
-
function getLine$1(input, position) {
|
|
176
|
-
const lines = findLines(input);
|
|
177
|
-
const start = lines[position.line - 2] || 0;
|
|
178
|
-
const end = lines[position.line - 1] || input.length;
|
|
179
|
-
return input.substring(start, end);
|
|
180
|
-
}
|
|
181
|
-
function findLines(input) {
|
|
182
|
-
// exec is stateful, so create new regexp each time
|
|
183
|
-
const BY_NEW_LINE = /\r\n|\n/g;
|
|
184
|
-
const indexes = [];
|
|
185
|
-
let match;
|
|
186
|
-
while ((match = BY_NEW_LINE.exec(input)) != null) {
|
|
187
|
-
indexes.push(match.index + match[0].length - 1);
|
|
188
|
-
}
|
|
189
|
-
indexes.push(input.length + 1);
|
|
190
|
-
return indexes;
|
|
191
|
-
}
|
|
192
|
-
function clonePosition(position) {
|
|
193
|
-
return { line: position.line, column: position.column };
|
|
194
|
-
}
|
|
195
|
-
function cloneLocation(location) {
|
|
196
|
-
return { start: clonePosition(location.start), end: clonePosition(location.end) };
|
|
197
|
-
}
|
|
198
|
-
/**
|
|
199
|
-
* Returns a Position at line 1, column 0.
|
|
200
|
-
* This means that lines are 1-indexed and columns are 0-indexed.
|
|
201
|
-
*
|
|
202
|
-
* @returns A Position at line 1, column 0
|
|
203
|
-
*/
|
|
204
|
-
function zero() {
|
|
205
|
-
return { line: 1, column: 0 };
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
class ParseError extends Error {
|
|
209
|
-
constructor(input, position, message) {
|
|
210
|
-
let error_message = `Error parsing TOML (${position.line}, ${position.column + 1}):\n`;
|
|
211
|
-
if (input) {
|
|
212
|
-
const line = getLine$1(input, position);
|
|
213
|
-
const pointer = `${whitespace(position.column)}^`;
|
|
214
|
-
if (line)
|
|
215
|
-
error_message += `${line}\n${pointer}\n`;
|
|
216
|
-
}
|
|
217
|
-
error_message += message;
|
|
218
|
-
super(error_message);
|
|
219
|
-
this.line = position.line;
|
|
220
|
-
this.column = position.column;
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
function whitespace(count, character = ' ') {
|
|
224
|
-
return character.repeat(count);
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
var TokenType;
|
|
228
|
-
(function (TokenType) {
|
|
229
|
-
TokenType["Bracket"] = "Bracket";
|
|
230
|
-
TokenType["Curly"] = "Curly";
|
|
231
|
-
TokenType["Equal"] = "Equal";
|
|
232
|
-
TokenType["Comma"] = "Comma";
|
|
233
|
-
TokenType["Dot"] = "Dot";
|
|
234
|
-
TokenType["Comment"] = "Comment";
|
|
235
|
-
TokenType["Literal"] = "Literal";
|
|
236
|
-
})(TokenType || (TokenType = {}));
|
|
237
|
-
const IS_WHITESPACE = /\s/;
|
|
238
|
-
const IS_NEW_LINE = /(\r\n|\n)/;
|
|
239
|
-
const DOUBLE_QUOTE = `"`;
|
|
240
|
-
const SINGLE_QUOTE = `'`;
|
|
241
|
-
const SPACE = ' ';
|
|
242
|
-
const ESCAPE = '\\';
|
|
243
|
-
const IS_VALID_LEADING_CHARACTER = /[\w,\d,\",\',\+,\-,\_]/;
|
|
244
|
-
function* tokenize(input) {
|
|
245
|
-
const cursor = new Cursor(iterator(input));
|
|
246
|
-
cursor.next();
|
|
247
|
-
const locate = createLocate(input);
|
|
248
|
-
while (!cursor.done) {
|
|
249
|
-
if (IS_WHITESPACE.test(cursor.value)) ;
|
|
250
|
-
else if (cursor.value === '[' || cursor.value === ']') {
|
|
251
|
-
// Handle special characters: [, ], {, }, =, comma
|
|
252
|
-
yield specialCharacter(cursor, locate, TokenType.Bracket);
|
|
253
|
-
}
|
|
254
|
-
else if (cursor.value === '{' || cursor.value === '}') {
|
|
255
|
-
yield specialCharacter(cursor, locate, TokenType.Curly);
|
|
256
|
-
}
|
|
257
|
-
else if (cursor.value === '=') {
|
|
258
|
-
yield specialCharacter(cursor, locate, TokenType.Equal);
|
|
259
|
-
}
|
|
260
|
-
else if (cursor.value === ',') {
|
|
261
|
-
yield specialCharacter(cursor, locate, TokenType.Comma);
|
|
262
|
-
}
|
|
263
|
-
else if (cursor.value === '.') {
|
|
264
|
-
yield specialCharacter(cursor, locate, TokenType.Dot);
|
|
265
|
-
}
|
|
266
|
-
else if (cursor.value === '#') {
|
|
267
|
-
// Handle comments = # -> EOL
|
|
268
|
-
yield comment$1(cursor, locate);
|
|
269
|
-
}
|
|
270
|
-
else {
|
|
271
|
-
const multiline_char = checkThree(input, cursor.index, SINGLE_QUOTE) ||
|
|
272
|
-
checkThree(input, cursor.index, DOUBLE_QUOTE);
|
|
273
|
-
if (multiline_char) {
|
|
274
|
-
// Multi-line literals or strings = no escaping
|
|
275
|
-
yield multiline(cursor, locate, multiline_char, input);
|
|
276
|
-
}
|
|
277
|
-
else {
|
|
278
|
-
yield string$1(cursor, locate, input);
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
cursor.next();
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
function specialCharacter(cursor, locate, type) {
|
|
285
|
-
return { type, raw: cursor.value, loc: locate(cursor.index, cursor.index + 1) };
|
|
286
|
-
}
|
|
287
|
-
function comment$1(cursor, locate) {
|
|
288
|
-
const start = cursor.index;
|
|
289
|
-
let raw = cursor.value;
|
|
290
|
-
while (!cursor.peek().done && !IS_NEW_LINE.test(cursor.peek().value)) {
|
|
291
|
-
cursor.next();
|
|
292
|
-
raw += cursor.value;
|
|
293
|
-
}
|
|
294
|
-
// Early exit is ok for comment, no closing conditions
|
|
295
|
-
return {
|
|
296
|
-
type: TokenType.Comment,
|
|
297
|
-
raw,
|
|
298
|
-
loc: locate(start, cursor.index + 1)
|
|
299
|
-
};
|
|
300
|
-
}
|
|
301
|
-
function multiline(cursor, locate, multiline_char, input) {
|
|
302
|
-
const start = cursor.index;
|
|
303
|
-
let quotes = multiline_char + multiline_char + multiline_char;
|
|
304
|
-
let raw = quotes;
|
|
305
|
-
// Skip over quotes
|
|
306
|
-
cursor.next();
|
|
307
|
-
cursor.next();
|
|
308
|
-
cursor.next();
|
|
309
|
-
// The reason why we need to check if there is more than three is because we have to match the last 3 quotes, not the first 3 that appears consecutively
|
|
310
|
-
// See spec-string-basic-multiline-9.toml
|
|
311
|
-
while (!cursor.done && (!checkThree(input, cursor.index, multiline_char) || CheckMoreThanThree(input, cursor.index, multiline_char))) {
|
|
312
|
-
raw += cursor.value;
|
|
313
|
-
cursor.next();
|
|
314
|
-
}
|
|
315
|
-
if (cursor.done) {
|
|
316
|
-
throw new ParseError(input, findPosition(input, cursor.index), `Expected close of multiline string with ${quotes}, reached end of file`);
|
|
317
|
-
}
|
|
318
|
-
raw += quotes;
|
|
319
|
-
cursor.next();
|
|
320
|
-
cursor.next();
|
|
321
|
-
return {
|
|
322
|
-
type: TokenType.Literal,
|
|
323
|
-
raw,
|
|
324
|
-
loc: locate(start, cursor.index + 1)
|
|
325
|
-
};
|
|
326
|
-
}
|
|
327
|
-
function string$1(cursor, locate, input) {
|
|
328
|
-
// Remaining possibilities: keys, strings, literals, integer, float, boolean
|
|
329
|
-
//
|
|
330
|
-
// Special cases:
|
|
331
|
-
// "..." -> quoted
|
|
332
|
-
// '...' -> quoted
|
|
333
|
-
// "...".'...' -> bare
|
|
334
|
-
// 0000-00-00 00:00:00 -> bare
|
|
335
|
-
//
|
|
336
|
-
// See https://github.com/toml-lang/toml#offset-date-time
|
|
337
|
-
//
|
|
338
|
-
// | For the sake of readability, you may replace the T delimiter between date and time with a space (as permitted by RFC 3339 section 5.6).
|
|
339
|
-
// | `odt4 = 1979-05-27 07:32:00Z`
|
|
340
|
-
//
|
|
341
|
-
// From RFC 3339:
|
|
342
|
-
//
|
|
343
|
-
// | NOTE: ISO 8601 defines date and time separated by "T".
|
|
344
|
-
// | Applications using this syntax may choose, for the sake of
|
|
345
|
-
// | readability, to specify a full-date and full-time separated by
|
|
346
|
-
// | (say) a space character.
|
|
347
|
-
// First, check for invalid characters
|
|
348
|
-
if (!IS_VALID_LEADING_CHARACTER.test(cursor.value)) {
|
|
349
|
-
throw new ParseError(input, findPosition(input, cursor.index), `Unsupported character "${cursor.value}". Expected ALPHANUMERIC, ", ', +, -, or _`);
|
|
350
|
-
}
|
|
351
|
-
const start = cursor.index;
|
|
352
|
-
let raw = cursor.value;
|
|
353
|
-
let double_quoted = cursor.value === DOUBLE_QUOTE;
|
|
354
|
-
let single_quoted = cursor.value === SINGLE_QUOTE;
|
|
355
|
-
const isFinished = (cursor) => {
|
|
356
|
-
if (cursor.peek().done)
|
|
357
|
-
return true;
|
|
358
|
-
const next_item = cursor.peek().value;
|
|
359
|
-
return (!(double_quoted || single_quoted) &&
|
|
360
|
-
(IS_WHITESPACE.test(next_item) ||
|
|
361
|
-
next_item === ',' ||
|
|
362
|
-
next_item === '.' ||
|
|
363
|
-
next_item === ']' ||
|
|
364
|
-
next_item === '}' ||
|
|
365
|
-
next_item === '=' ||
|
|
366
|
-
next_item === '#'));
|
|
367
|
-
};
|
|
368
|
-
while (!cursor.done && !isFinished(cursor)) {
|
|
369
|
-
cursor.next();
|
|
370
|
-
if (cursor.value === DOUBLE_QUOTE)
|
|
371
|
-
double_quoted = !double_quoted;
|
|
372
|
-
if (cursor.value === SINGLE_QUOTE && !double_quoted)
|
|
373
|
-
single_quoted = !single_quoted;
|
|
374
|
-
raw += cursor.value;
|
|
375
|
-
if (cursor.peek().done)
|
|
376
|
-
break;
|
|
377
|
-
let next_item = cursor.peek().value;
|
|
378
|
-
// If next character is escape and currently double-quoted,
|
|
379
|
-
// check for escaped quote
|
|
380
|
-
if (double_quoted && cursor.value === ESCAPE) {
|
|
381
|
-
if (next_item === DOUBLE_QUOTE) {
|
|
382
|
-
raw += DOUBLE_QUOTE;
|
|
383
|
-
cursor.next();
|
|
384
|
-
}
|
|
385
|
-
else if (next_item === ESCAPE) {
|
|
386
|
-
raw += ESCAPE;
|
|
387
|
-
cursor.next();
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
if (double_quoted || single_quoted) {
|
|
392
|
-
throw new ParseError(input, findPosition(input, start), `Expected close of string with ${double_quoted ? DOUBLE_QUOTE : SINGLE_QUOTE}`);
|
|
393
|
-
}
|
|
394
|
-
return {
|
|
395
|
-
type: TokenType.Literal,
|
|
396
|
-
raw,
|
|
397
|
-
loc: locate(start, cursor.index + 1)
|
|
398
|
-
};
|
|
399
|
-
}
|
|
400
|
-
/**
|
|
401
|
-
* Check if the current character and the next two characters are the same
|
|
402
|
-
* and not escaped.
|
|
403
|
-
*
|
|
404
|
-
* @param input - The input string.
|
|
405
|
-
* @param current - The current index in the input string.
|
|
406
|
-
* @param check - The character to check for.
|
|
407
|
-
* @returns ⚠️The character if found, otherwise false.
|
|
408
|
-
*/
|
|
409
|
-
function checkThree(input, current, check) {
|
|
410
|
-
if (!check) {
|
|
411
|
-
return false;
|
|
412
|
-
}
|
|
413
|
-
const has3 = input[current] === check &&
|
|
414
|
-
input[current + 1] === check &&
|
|
415
|
-
input[current + 2] === check;
|
|
416
|
-
if (!has3) {
|
|
417
|
-
return false;
|
|
418
|
-
}
|
|
419
|
-
// Only check for escaping in basic strings (double quotes)
|
|
420
|
-
// Literal strings (single quotes) don't support escape sequences
|
|
421
|
-
if (check === SINGLE_QUOTE) {
|
|
422
|
-
return check; // No escaping in literal strings
|
|
423
|
-
}
|
|
424
|
-
// Check if the sequence is escaped
|
|
425
|
-
const precedingText = input.slice(0, current); // Get the text before the current position
|
|
426
|
-
const backslashes = precedingText.match(/\\+$/); // Match trailing backslashes
|
|
427
|
-
if (!backslashes) {
|
|
428
|
-
return check; // No backslashes means not escaped
|
|
429
|
-
}
|
|
430
|
-
const isEscaped = backslashes[0].length % 2 !== 0; // Odd number of backslashes means escaped
|
|
431
|
-
return isEscaped ? false : check; // Return `check` if not escaped, otherwise `false`
|
|
432
|
-
}
|
|
433
|
-
function CheckMoreThanThree(input, current, check) {
|
|
434
|
-
if (!check) {
|
|
435
|
-
return false;
|
|
436
|
-
}
|
|
437
|
-
return (input[current] === check &&
|
|
438
|
-
input[current + 1] === check &&
|
|
439
|
-
input[current + 2] === check &&
|
|
440
|
-
input[current + 3] === check);
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
function last(values) {
|
|
444
|
-
return values[values.length - 1];
|
|
445
|
-
}
|
|
446
|
-
function blank() {
|
|
447
|
-
return Object.create(null);
|
|
448
|
-
}
|
|
449
|
-
function isString(value) {
|
|
450
|
-
return typeof value === 'string';
|
|
451
|
-
}
|
|
452
|
-
function isInteger(value) {
|
|
453
|
-
return typeof value === 'number' && value % 1 === 0 && isFinite(value) && !Object.is(value, -0);
|
|
454
|
-
}
|
|
455
|
-
function isFloat(value) {
|
|
456
|
-
return typeof value === 'number' && (!isInteger(value) || !isFinite(value) || Object.is(value, -0));
|
|
457
|
-
}
|
|
458
|
-
function isBoolean(value) {
|
|
459
|
-
return typeof value === 'boolean';
|
|
460
|
-
}
|
|
461
|
-
function isDate(value) {
|
|
462
|
-
return Object.prototype.toString.call(value) === '[object Date]';
|
|
463
|
-
}
|
|
464
|
-
function isObject(value) {
|
|
465
|
-
return value && typeof value === 'object' && !isDate(value) && !Array.isArray(value);
|
|
466
|
-
}
|
|
467
|
-
function isIterable(value) {
|
|
468
|
-
return value != null && typeof value[Symbol.iterator] === 'function';
|
|
469
|
-
}
|
|
470
|
-
function has(object, key) {
|
|
471
|
-
return Object.prototype.hasOwnProperty.call(object, key);
|
|
472
|
-
}
|
|
473
|
-
function arraysEqual(a, b) {
|
|
474
|
-
if (a.length !== b.length)
|
|
475
|
-
return false;
|
|
476
|
-
for (let i = 0; i < a.length; i++) {
|
|
477
|
-
if (a[i] !== b[i])
|
|
478
|
-
return false;
|
|
479
|
-
}
|
|
480
|
-
return true;
|
|
481
|
-
}
|
|
482
|
-
function datesEqual(a, b) {
|
|
483
|
-
return isDate(a) && isDate(b) && a.toISOString() === b.toISOString();
|
|
484
|
-
}
|
|
485
|
-
function pipe(value, ...fns) {
|
|
486
|
-
return fns.reduce((value, fn) => fn(value), value);
|
|
487
|
-
}
|
|
488
|
-
function stableStringify(object) {
|
|
489
|
-
if (isObject(object)) {
|
|
490
|
-
const key_values = Object.keys(object)
|
|
491
|
-
.sort()
|
|
492
|
-
.map(key => `${JSON.stringify(key)}:${stableStringify(object[key])}`);
|
|
493
|
-
return `{${key_values.join(',')}}`;
|
|
494
|
-
}
|
|
495
|
-
else if (Array.isArray(object)) {
|
|
496
|
-
return `[${object.map(stableStringify).join(',')}]`;
|
|
497
|
-
}
|
|
498
|
-
else {
|
|
499
|
-
return JSON.stringify(object);
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
function merge(target, values) {
|
|
503
|
-
// __mutating__: merge values into target
|
|
504
|
-
// Reference: https://dev.to/uilicious/javascript-array-push-is-945x-faster-than-array-concat-1oki
|
|
505
|
-
const original_length = target.length;
|
|
506
|
-
const added_length = values.length;
|
|
507
|
-
target.length = original_length + added_length;
|
|
508
|
-
for (let i = 0; i < added_length; i++) {
|
|
509
|
-
target[original_length + i] = values[i];
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
/**
|
|
513
|
-
* Checks if a string is a multiline string (starts with """ or ''')
|
|
514
|
-
* @param raw The raw string value including quotes
|
|
515
|
-
* @returns true if the string is multiline, false otherwise
|
|
516
|
-
*/
|
|
517
|
-
function isMultilineString(raw) {
|
|
518
|
-
return raw.startsWith('"""') || raw.startsWith("'''");
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
const TRIPLE_DOUBLE_QUOTE = `"""`;
|
|
522
|
-
const TRIPLE_SINGLE_QUOTE = `'''`;
|
|
523
|
-
const LF = '\\n';
|
|
524
|
-
const CRLF = '\\r\\n';
|
|
525
|
-
const IS_CRLF = /\r\n/g;
|
|
526
|
-
const IS_LF = /\n/g;
|
|
527
|
-
const IS_LEADING_NEW_LINE = /^(\r\n|\n)/;
|
|
528
|
-
// This regex is used to match an odd number of backslashes followed by a line ending
|
|
529
|
-
// It uses a negative lookbehind to ensure that the backslash is not preceded by another backslash.
|
|
530
|
-
// We need an odd number of backslashes so that the last one is not escaped.
|
|
531
|
-
const IS_LINE_ENDING_BACKSLASH = /(?<!\\)(?:\\\\)*(\\\s*[\n\r\n]\s*)/g;
|
|
532
|
-
function parseString(raw) {
|
|
533
|
-
if (raw.startsWith(TRIPLE_SINGLE_QUOTE)) {
|
|
534
|
-
return pipe(trim(raw, 3), trimLeadingWhitespace);
|
|
535
|
-
}
|
|
536
|
-
else if (raw.startsWith(SINGLE_QUOTE)) {
|
|
537
|
-
return trim(raw, 1);
|
|
538
|
-
}
|
|
539
|
-
else if (raw.startsWith(TRIPLE_DOUBLE_QUOTE)) {
|
|
540
|
-
return pipe(trim(raw, 3), trimLeadingWhitespace, lineEndingBackslash, escapeNewLines, escapeDoubleQuotes, unescapeLargeUnicode);
|
|
541
|
-
}
|
|
542
|
-
else if (raw.startsWith(DOUBLE_QUOTE)) {
|
|
543
|
-
return pipe(trim(raw, 1), unescapeLargeUnicode);
|
|
544
|
-
}
|
|
545
|
-
else {
|
|
546
|
-
return raw;
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
function escapeDoubleQuotes(value) {
|
|
550
|
-
let result = '';
|
|
551
|
-
let precedingBackslashes = 0;
|
|
552
|
-
for (let i = 0; i < value.length; i++) {
|
|
553
|
-
const char = value[i];
|
|
554
|
-
if (char === '"' && precedingBackslashes % 2 === 0) {
|
|
555
|
-
// If the current character is a quote and it is not escaped, escape it
|
|
556
|
-
result += '\\"';
|
|
557
|
-
}
|
|
558
|
-
else {
|
|
559
|
-
// Otherwise, add the character as is
|
|
560
|
-
result += char;
|
|
561
|
-
}
|
|
562
|
-
// Update the count of consecutive backslashes
|
|
563
|
-
if (char === '\\') {
|
|
564
|
-
precedingBackslashes++;
|
|
565
|
-
}
|
|
566
|
-
else {
|
|
567
|
-
precedingBackslashes = 0; // Reset if the character is not a backslash
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
return result;
|
|
571
|
-
}
|
|
572
|
-
function isBackslashEscaped(source, backslashOffset) {
|
|
573
|
-
let precedingBackslashes = 0;
|
|
574
|
-
for (let i = backslashOffset - 1; i >= 0 && source[i] === '\\'; i--) {
|
|
575
|
-
precedingBackslashes++;
|
|
576
|
-
}
|
|
577
|
-
return precedingBackslashes % 2 !== 0;
|
|
578
|
-
}
|
|
579
|
-
function unescapeLargeUnicode(escaped) {
|
|
580
|
-
// TOML 1.1.0: Handle \xHH hex escapes (for codepoints < 255)
|
|
581
|
-
const HEX_ESCAPE = /\\x([a-fA-F0-9]{2})/g;
|
|
582
|
-
const hexEscapeSource = escaped;
|
|
583
|
-
let withHexEscapes = hexEscapeSource.replace(HEX_ESCAPE, (match, hex, offset) => {
|
|
584
|
-
if (isBackslashEscaped(hexEscapeSource, offset)) {
|
|
585
|
-
return match;
|
|
586
|
-
}
|
|
587
|
-
const codePoint = parseInt(hex, 16);
|
|
588
|
-
const asString = String.fromCharCode(codePoint);
|
|
589
|
-
// Escape for JSON if needed
|
|
590
|
-
if (codePoint < 0x20 || codePoint === 0x22 || codePoint === 0x5C) {
|
|
591
|
-
return trim(JSON.stringify(asString), 1);
|
|
592
|
-
}
|
|
593
|
-
return asString;
|
|
594
|
-
});
|
|
595
|
-
// TOML 1.1.0: Handle \e escape character (ESC = 0x1B)
|
|
596
|
-
const eEscapeSource = withHexEscapes;
|
|
597
|
-
withHexEscapes = eEscapeSource.replace(/\\e/g, (match, offset) => {
|
|
598
|
-
if (isBackslashEscaped(eEscapeSource, offset)) {
|
|
599
|
-
return match;
|
|
600
|
-
}
|
|
601
|
-
return '\\u001b';
|
|
602
|
-
});
|
|
603
|
-
// JSON.parse handles everything except \UXXXXXXXX
|
|
604
|
-
// replace those instances with code point, escape that, and then parse
|
|
605
|
-
const LARGE_UNICODE = /\\U[a-fA-F0-9]{8}/g;
|
|
606
|
-
const json_escaped = withHexEscapes.replace(LARGE_UNICODE, value => {
|
|
607
|
-
const code_point = parseInt(value.replace('\\U', ''), 16);
|
|
608
|
-
const as_string = String.fromCodePoint(code_point);
|
|
609
|
-
return trim(JSON.stringify(as_string), 1);
|
|
610
|
-
});
|
|
611
|
-
const fixed_json_escaped = escapeTabsForJSON(json_escaped);
|
|
612
|
-
// Parse the properly escaped JSON string
|
|
613
|
-
const parsed = JSON.parse(`"${fixed_json_escaped}"`);
|
|
614
|
-
return parsed;
|
|
615
|
-
}
|
|
616
|
-
function escapeTabsForJSON(value) {
|
|
617
|
-
return value
|
|
618
|
-
.replace(/\t/g, '\\t');
|
|
619
|
-
}
|
|
620
|
-
function trim(value, count) {
|
|
621
|
-
return value.slice(count, value.length - count);
|
|
622
|
-
}
|
|
623
|
-
function trimLeadingWhitespace(value) {
|
|
624
|
-
return value.replace(IS_LEADING_NEW_LINE, '');
|
|
625
|
-
}
|
|
626
|
-
function escapeNewLines(value) {
|
|
627
|
-
return value.replace(IS_CRLF, CRLF).replace(IS_LF, LF);
|
|
628
|
-
}
|
|
629
|
-
function lineEndingBackslash(value) {
|
|
630
|
-
return value.replace(IS_LINE_ENDING_BACKSLASH, (match, group) => match.replace(group, ''));
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
/**
|
|
634
|
-
* Central module for TOML date/time format handling and custom date classes.
|
|
635
|
-
* This module provides all the patterns, classes, and utilities needed to work
|
|
636
|
-
* with the different date/time formats supported by the TOML specification.
|
|
637
|
-
*/
|
|
638
|
-
/**
|
|
639
|
-
* Helper class containing all date format patterns and utilities for TOML date/time handling
|
|
640
|
-
*/
|
|
641
|
-
class DateFormatHelper {
|
|
642
|
-
/**
|
|
643
|
-
* Creates a custom date/time object that preserves the original TOML date/time format.
|
|
644
|
-
*
|
|
645
|
-
* This method detects the TOML date/time format from the raw string and returns an appropriate
|
|
646
|
-
* custom date/time instance (e.g., LocalDate, LocalTime, LocalDateTime, OffsetDateTime) or a Date,
|
|
647
|
-
* using the provided new JavaScript Date value.
|
|
648
|
-
*
|
|
649
|
-
* @param {Date} newJSDate - The new JavaScript Date object representing the updated
|
|
650
|
-
* date/time value. This is used as the source for constructing the custom date/time object.
|
|
651
|
-
* In some cases, this may be a custom date/time object (e.g., LocalTime) instead of a native Date.
|
|
652
|
-
* @param {string} originalRaw - The original TOML date/time string as it appeared in the input.
|
|
653
|
-
* Used to detect the specific TOML date/time format and to extract formatting details (e.g., separator, offset).
|
|
654
|
-
*
|
|
655
|
-
* @returns {Date | LocalDate | LocalTime | LocalDateTime | OffsetDateTime}
|
|
656
|
-
* Returns a custom date/time object that matches the original TOML format:
|
|
657
|
-
* - LocalDate for date-only values (e.g., "2024-01-15")
|
|
658
|
-
* - LocalTime for time-only values (e.g., "10:30:00")
|
|
659
|
-
* - LocalDateTime for local datetimes (e.g., "2024-01-15T10:30:00" or "2024-01-15 10:30:00")
|
|
660
|
-
* - OffsetDateTime for datetimes with offsets (e.g., "2024-01-15T10:30:00+02:00")
|
|
661
|
-
* - Date (native JS Date) as a fallback if the format is unrecognized
|
|
662
|
-
*
|
|
663
|
-
* Format-specific behavior:
|
|
664
|
-
* - Date-only: Returns a LocalDate constructed from the date part of newJSDate.
|
|
665
|
-
* - Time-only: Returns a LocalTime, either from newJSDate (if already LocalTime) or constructed from its time part.
|
|
666
|
-
* - Local datetime: Returns a LocalDateTime, preserving the separator (T or space).
|
|
667
|
-
* - Offset datetime: Returns an OffsetDateTime, reconstructing the date/time with the original offset and separator.
|
|
668
|
-
* - Fallback: Returns newJSDate as-is.
|
|
669
|
-
*/
|
|
670
|
-
static createDateWithOriginalFormat(newJSDate, originalRaw) {
|
|
671
|
-
if (DateFormatHelper.IS_DATE_ONLY.test(originalRaw)) {
|
|
672
|
-
// Local date (date-only) - format: 2024-01-15
|
|
673
|
-
// Check if newJSDate has time components - if so, upgrade appropriately
|
|
674
|
-
if (newJSDate.getUTCHours() !== 0 ||
|
|
675
|
-
newJSDate.getUTCMinutes() !== 0 ||
|
|
676
|
-
newJSDate.getUTCSeconds() !== 0 ||
|
|
677
|
-
newJSDate.getUTCMilliseconds() !== 0) {
|
|
678
|
-
// Check if the new value is an OffsetDateTime (has offset information)
|
|
679
|
-
if (newJSDate instanceof OffsetDateTime) {
|
|
680
|
-
// Upgrade to OffsetDateTime - it already has the right format
|
|
681
|
-
return newJSDate;
|
|
682
|
-
}
|
|
683
|
-
// Upgrade from date-only to local datetime with time components
|
|
684
|
-
// Use T separator as it's the more common format
|
|
685
|
-
let isoString = newJSDate.toISOString().replace('Z', '');
|
|
686
|
-
// Strip .000 milliseconds if present (don't show unnecessary precision)
|
|
687
|
-
isoString = isoString.replace(/\.000$/, '');
|
|
688
|
-
return new LocalDateTime(isoString, false);
|
|
689
|
-
}
|
|
690
|
-
const dateStr = newJSDate.toISOString().split('T')[0];
|
|
691
|
-
return new LocalDate(dateStr);
|
|
692
|
-
}
|
|
693
|
-
else if (DateFormatHelper.IS_TIME_ONLY.test(originalRaw)) {
|
|
694
|
-
// Local time (time-only) - format: 10:30:00
|
|
695
|
-
// For time-only values, we need to handle this more carefully
|
|
696
|
-
// The newJSDate might be a LocalTime object itself
|
|
697
|
-
if (newJSDate instanceof LocalTime) {
|
|
698
|
-
// If the new date is already a LocalTime, use its toISOString
|
|
699
|
-
return newJSDate;
|
|
700
|
-
}
|
|
701
|
-
else {
|
|
702
|
-
// Determine if originalRaw had milliseconds and how many digits
|
|
703
|
-
const msMatch = originalRaw.match(/\.(\d+)\s*$/);
|
|
704
|
-
// Extract time from a regular Date object
|
|
705
|
-
const isoString = newJSDate.toISOString();
|
|
706
|
-
if (isoString && isoString.includes('T')) {
|
|
707
|
-
let newTime = isoString.split('T')[1].split('Z')[0];
|
|
708
|
-
if (msMatch) {
|
|
709
|
-
// Original had milliseconds, preserve the number of digits
|
|
710
|
-
const msDigits = msMatch[1].length;
|
|
711
|
-
const [h, m, sMs] = newTime.split(':');
|
|
712
|
-
const [s] = sMs.split('.');
|
|
713
|
-
const ms = String(newJSDate.getUTCMilliseconds()).padStart(3, '0').slice(0, msDigits);
|
|
714
|
-
newTime = `${h}:${m}:${s}.${ms}`;
|
|
715
|
-
}
|
|
716
|
-
// If original had no milliseconds, keep newTime as-is (with milliseconds if present)
|
|
717
|
-
return new LocalTime(newTime, originalRaw);
|
|
718
|
-
}
|
|
719
|
-
else {
|
|
720
|
-
// Fallback: construct time from the Date object directly
|
|
721
|
-
const hours = String(newJSDate.getUTCHours()).padStart(2, '0');
|
|
722
|
-
const minutes = String(newJSDate.getUTCMinutes()).padStart(2, '0');
|
|
723
|
-
const seconds = String(newJSDate.getUTCSeconds()).padStart(2, '0');
|
|
724
|
-
const milliseconds = newJSDate.getUTCMilliseconds();
|
|
725
|
-
let timeStr;
|
|
726
|
-
if (msMatch) {
|
|
727
|
-
const msDigits = msMatch[1].length;
|
|
728
|
-
let ms = String(milliseconds).padStart(3, '0').slice(0, msDigits);
|
|
729
|
-
timeStr = `${hours}:${minutes}:${seconds}.${ms}`;
|
|
730
|
-
}
|
|
731
|
-
else if (milliseconds > 0) {
|
|
732
|
-
// No original milliseconds, but new value has them - include them
|
|
733
|
-
// Note: milliseconds > 0 ensures we don't format ".0" for zero milliseconds
|
|
734
|
-
const ms = String(milliseconds).padStart(3, '0').replace(/0+$/, '');
|
|
735
|
-
timeStr = `${hours}:${minutes}:${seconds}.${ms}`;
|
|
736
|
-
}
|
|
737
|
-
else {
|
|
738
|
-
timeStr = `${hours}:${minutes}:${seconds}`;
|
|
739
|
-
}
|
|
740
|
-
return new LocalTime(timeStr, originalRaw);
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
|
-
else if (DateFormatHelper.IS_LOCAL_DATETIME_T.test(originalRaw)) {
|
|
745
|
-
// Local datetime with T separator - format: 2024-01-15T10:30:00
|
|
746
|
-
// Determine if originalRaw had milliseconds and how many digits
|
|
747
|
-
const msMatch = originalRaw.match(/\.(\d+)\s*$/);
|
|
748
|
-
let isoString = newJSDate.toISOString().replace('Z', '');
|
|
749
|
-
if (msMatch) {
|
|
750
|
-
// Original had milliseconds, preserve the number of digits
|
|
751
|
-
const msDigits = msMatch[1].length;
|
|
752
|
-
// isoString is like "2024-01-15T10:30:00.123"
|
|
753
|
-
const [datePart, timePart] = isoString.split('T');
|
|
754
|
-
const [h, m, sMs] = timePart.split(':');
|
|
755
|
-
const [s] = sMs.split('.');
|
|
756
|
-
const ms = String(newJSDate.getUTCMilliseconds()).padStart(3, '0').slice(0, msDigits);
|
|
757
|
-
isoString = `${datePart}T${h}:${m}:${s}.${ms}`;
|
|
758
|
-
}
|
|
759
|
-
// If original had no milliseconds, keep isoString as-is (with milliseconds if present)
|
|
760
|
-
return new LocalDateTime(isoString, false, originalRaw);
|
|
761
|
-
}
|
|
762
|
-
else if (DateFormatHelper.IS_LOCAL_DATETIME_SPACE.test(originalRaw)) {
|
|
763
|
-
// Local datetime with space separator - format: 2024-01-15 10:30:00
|
|
764
|
-
const msMatch = originalRaw.match(/\.(\d+)\s*$/);
|
|
765
|
-
let isoString = newJSDate.toISOString().replace('Z', '').replace('T', ' ');
|
|
766
|
-
if (msMatch) {
|
|
767
|
-
const msDigits = msMatch[1].length;
|
|
768
|
-
// isoString is like "2024-01-15 10:30:00.123"
|
|
769
|
-
const [datePart, timePart] = isoString.split(' ');
|
|
770
|
-
const [h, m, sMs] = timePart.split(':');
|
|
771
|
-
const [s] = sMs.split('.');
|
|
772
|
-
const ms = String(newJSDate.getUTCMilliseconds()).padStart(3, '0').slice(0, msDigits);
|
|
773
|
-
isoString = `${datePart} ${h}:${m}:${s}.${ms}`;
|
|
774
|
-
}
|
|
775
|
-
// If original had no milliseconds, keep isoString as-is (with milliseconds if present)
|
|
776
|
-
return new LocalDateTime(isoString, true, originalRaw);
|
|
777
|
-
}
|
|
778
|
-
else if (DateFormatHelper.IS_OFFSET_DATETIME_T.test(originalRaw) || DateFormatHelper.IS_OFFSET_DATETIME_SPACE.test(originalRaw)) {
|
|
779
|
-
// Offset datetime - preserve the original timezone offset and separator
|
|
780
|
-
const offsetMatch = originalRaw.match(/([+-]\d{2}:\d{2}|[Zz])$/);
|
|
781
|
-
const originalOffset = offsetMatch ? (offsetMatch[1] === 'z' ? 'Z' : offsetMatch[1]) : 'Z';
|
|
782
|
-
const useSpaceSeparator = DateFormatHelper.IS_OFFSET_DATETIME_SPACE.test(originalRaw);
|
|
783
|
-
// Check if original had milliseconds and preserve precision
|
|
784
|
-
const msMatch = originalRaw.match(/\.(\d+)(?:[Zz]|[+-]\d{2}:\d{2})\s*$/);
|
|
785
|
-
// Convert UTC time to local time in the original timezone
|
|
786
|
-
const utcTime = newJSDate.getTime();
|
|
787
|
-
let offsetMinutes = 0;
|
|
788
|
-
if (originalOffset !== 'Z') {
|
|
789
|
-
const sign = originalOffset[0] === '+' ? 1 : -1;
|
|
790
|
-
const [hours, minutes] = originalOffset.slice(1).split(':');
|
|
791
|
-
offsetMinutes = sign * (parseInt(hours) * 60 + parseInt(minutes));
|
|
792
|
-
}
|
|
793
|
-
// Create local time by applying the offset to UTC
|
|
794
|
-
const localTime = new Date(utcTime + offsetMinutes * 60000);
|
|
795
|
-
// Format the local time components
|
|
796
|
-
const year = localTime.getUTCFullYear();
|
|
797
|
-
const month = String(localTime.getUTCMonth() + 1).padStart(2, '0');
|
|
798
|
-
const day = String(localTime.getUTCDate()).padStart(2, '0');
|
|
799
|
-
const hours = String(localTime.getUTCHours()).padStart(2, '0');
|
|
800
|
-
const minutes = String(localTime.getUTCMinutes()).padStart(2, '0');
|
|
801
|
-
const seconds = String(localTime.getUTCSeconds()).padStart(2, '0');
|
|
802
|
-
const milliseconds = localTime.getUTCMilliseconds();
|
|
803
|
-
const separator = useSpaceSeparator ? ' ' : 'T';
|
|
804
|
-
let timePart = `${hours}:${minutes}:${seconds}`;
|
|
805
|
-
// Handle millisecond precision
|
|
806
|
-
if (msMatch) {
|
|
807
|
-
const msDigits = msMatch[1].length;
|
|
808
|
-
const ms = String(milliseconds).padStart(3, '0').slice(0, msDigits);
|
|
809
|
-
timePart += `.${ms}`;
|
|
810
|
-
}
|
|
811
|
-
else if (milliseconds > 0) {
|
|
812
|
-
// Original had no milliseconds, but new value has them
|
|
813
|
-
const ms = String(milliseconds).padStart(3, '0').replace(/0+$/, '');
|
|
814
|
-
timePart += `.${ms}`;
|
|
815
|
-
}
|
|
816
|
-
const newDateTimeString = `${year}-${month}-${day}${separator}${timePart}${originalOffset}`;
|
|
817
|
-
return new OffsetDateTime(newDateTimeString, useSpaceSeparator);
|
|
818
|
-
}
|
|
819
|
-
else {
|
|
820
|
-
// Fallback to regular Date
|
|
821
|
-
return newJSDate;
|
|
822
|
-
}
|
|
823
|
-
}
|
|
824
|
-
}
|
|
825
|
-
// Patterns for different date/time formats
|
|
826
|
-
DateFormatHelper.IS_DATE_ONLY = /^\d{4}-\d{2}-\d{2}$/;
|
|
827
|
-
DateFormatHelper.IS_TIME_ONLY = /^\d{2}:\d{2}(?::\d{2})?(?:\.\d+)?$/;
|
|
828
|
-
DateFormatHelper.IS_LOCAL_DATETIME_T = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2})?(?:\.\d+)?$/;
|
|
829
|
-
DateFormatHelper.IS_LOCAL_DATETIME_SPACE = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}(?::\d{2})?(?:\.\d+)?$/;
|
|
830
|
-
DateFormatHelper.IS_OFFSET_DATETIME_T = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2})?(?:\.\d+)?(?:[Zz]|[+-]\d{2}:\d{2})$/;
|
|
831
|
-
DateFormatHelper.IS_OFFSET_DATETIME_SPACE = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}(?::\d{2})?(?:\.\d+)?(?:[Zz]|[+-]\d{2}:\d{2})$/;
|
|
832
|
-
// Legacy patterns from parse-toml.ts (for compatibility)
|
|
833
|
-
DateFormatHelper.IS_FULL_DATE = /(\d{4})-(\d{2})-(\d{2})/;
|
|
834
|
-
DateFormatHelper.IS_FULL_TIME = /(\d{2}):(\d{2})(?::(\d{2}))?/;
|
|
835
|
-
/**
|
|
836
|
-
* Custom Date class for local dates (date-only).
|
|
837
|
-
* Format: 1979-05-27
|
|
838
|
-
*/
|
|
839
|
-
class LocalDate extends Date {
|
|
840
|
-
constructor(value) {
|
|
841
|
-
super(value);
|
|
842
|
-
}
|
|
843
|
-
toISOString() {
|
|
844
|
-
const year = this.getUTCFullYear();
|
|
845
|
-
const month = String(this.getUTCMonth() + 1).padStart(2, '0');
|
|
846
|
-
const day = String(this.getUTCDate()).padStart(2, '0');
|
|
847
|
-
return `${year}-${month}-${day}`;
|
|
848
|
-
}
|
|
849
|
-
}
|
|
850
|
-
/**
|
|
851
|
-
* Custom Date class for local times (time-only)
|
|
852
|
-
* Format: 07:32:00 or 07:32:00.999
|
|
853
|
-
*/
|
|
854
|
-
class LocalTime extends Date {
|
|
855
|
-
constructor(value, originalFormat) {
|
|
856
|
-
// Normalize time to include seconds if missing (TOML 1.1.0 allows optional seconds)
|
|
857
|
-
let normalizedValue = value;
|
|
858
|
-
if (!/:\d{2}:\d{2}/.test(value)) {
|
|
859
|
-
// No seconds present, add :00
|
|
860
|
-
normalizedValue = value + ':00';
|
|
861
|
-
}
|
|
862
|
-
// For local time, use year 0000 as the base (TOML spec compliance)
|
|
863
|
-
// Add 'Z' to ensure it's parsed as UTC regardless of system timezone
|
|
864
|
-
super(`0000-01-01T${normalizedValue}Z`);
|
|
865
|
-
this.originalFormat = originalFormat;
|
|
866
|
-
}
|
|
867
|
-
toISOString() {
|
|
868
|
-
const hours = String(this.getUTCHours()).padStart(2, '0');
|
|
869
|
-
const minutes = String(this.getUTCMinutes()).padStart(2, '0');
|
|
870
|
-
const seconds = String(this.getUTCSeconds()).padStart(2, '0');
|
|
871
|
-
const milliseconds = this.getUTCMilliseconds();
|
|
872
|
-
// Check if the original format had milliseconds
|
|
873
|
-
const originalHadMs = this.originalFormat && this.originalFormat.includes('.');
|
|
874
|
-
if (originalHadMs) {
|
|
875
|
-
// Determine the number of millisecond digits from the original format
|
|
876
|
-
const msMatch = this.originalFormat.match(/\.(\d+)\s*$/);
|
|
877
|
-
const msDigits = msMatch ? msMatch[1].length : 3;
|
|
878
|
-
const ms = String(milliseconds).padStart(3, '0').slice(0, msDigits);
|
|
879
|
-
return `${hours}:${minutes}:${seconds}.${ms}`;
|
|
880
|
-
}
|
|
881
|
-
else if (milliseconds > 0) {
|
|
882
|
-
// Original had no milliseconds, but current has non-zero milliseconds
|
|
883
|
-
// Show them with trailing zeros removed
|
|
884
|
-
const ms = String(milliseconds).padStart(3, '0').replace(/0+$/, '');
|
|
885
|
-
return `${hours}:${minutes}:${seconds}.${ms}`;
|
|
886
|
-
}
|
|
887
|
-
return `${hours}:${minutes}:${seconds}`;
|
|
888
|
-
}
|
|
889
|
-
}
|
|
890
|
-
/**
|
|
891
|
-
* Custom Date class for local datetime (no timezone)
|
|
892
|
-
* Format: 1979-05-27T07:32:00 or 1979-05-27 07:32:00
|
|
893
|
-
*/
|
|
894
|
-
class LocalDateTime extends Date {
|
|
895
|
-
constructor(value, useSpaceSeparator = false, originalFormat) {
|
|
896
|
-
// Normalize time part to include seconds if missing (TOML 1.1.0 allows optional seconds)
|
|
897
|
-
let normalizedValue = value;
|
|
898
|
-
if (!/\d{2}:\d{2}:\d{2}/.test(value)) {
|
|
899
|
-
normalizedValue = value.replace(/(\d{2}:\d{2})([\s\-+TZ]|$)/, '$1:00$2');
|
|
900
|
-
}
|
|
901
|
-
// Convert space to T for Date parsing, but remember the original format
|
|
902
|
-
super(normalizedValue.replace(' ', 'T') + 'Z');
|
|
903
|
-
this.useSpaceSeparator = false;
|
|
904
|
-
this.useSpaceSeparator = useSpaceSeparator;
|
|
905
|
-
this.originalFormat = originalFormat || value;
|
|
906
|
-
}
|
|
907
|
-
toISOString() {
|
|
908
|
-
const year = this.getUTCFullYear();
|
|
909
|
-
const month = String(this.getUTCMonth() + 1).padStart(2, '0');
|
|
910
|
-
const day = String(this.getUTCDate()).padStart(2, '0');
|
|
911
|
-
const hours = String(this.getUTCHours()).padStart(2, '0');
|
|
912
|
-
const minutes = String(this.getUTCMinutes()).padStart(2, '0');
|
|
913
|
-
const seconds = String(this.getUTCSeconds()).padStart(2, '0');
|
|
914
|
-
const milliseconds = this.getUTCMilliseconds();
|
|
915
|
-
const datePart = `${year}-${month}-${day}`;
|
|
916
|
-
const separator = this.useSpaceSeparator ? ' ' : 'T';
|
|
917
|
-
// Check if the original format had milliseconds
|
|
918
|
-
const originalHadMs = this.originalFormat && this.originalFormat.includes('.');
|
|
919
|
-
if (originalHadMs) {
|
|
920
|
-
// Determine the number of millisecond digits from the original format
|
|
921
|
-
const msMatch = this.originalFormat.match(/\.(\d+)\s*$/);
|
|
922
|
-
const msDigits = msMatch ? msMatch[1].length : 3;
|
|
923
|
-
const ms = String(milliseconds).padStart(3, '0').slice(0, msDigits);
|
|
924
|
-
return `${datePart}${separator}${hours}:${minutes}:${seconds}.${ms}`;
|
|
925
|
-
}
|
|
926
|
-
else if (milliseconds > 0) {
|
|
927
|
-
// Original had no milliseconds, but current has non-zero milliseconds
|
|
928
|
-
// Show them with trailing zeros removed
|
|
929
|
-
const ms = String(milliseconds).padStart(3, '0').replace(/0+$/, '');
|
|
930
|
-
return `${datePart}${separator}${hours}:${minutes}:${seconds}.${ms}`;
|
|
931
|
-
}
|
|
932
|
-
return `${datePart}${separator}${hours}:${minutes}:${seconds}`;
|
|
933
|
-
}
|
|
934
|
-
}
|
|
935
|
-
/**
|
|
936
|
-
* Custom Date class for offset datetime that preserves space separator
|
|
937
|
-
* Format: 1979-05-27T07:32:00Z or 1979-05-27 07:32:00-07:00
|
|
938
|
-
*/
|
|
939
|
-
class OffsetDateTime extends Date {
|
|
940
|
-
constructor(value, useSpaceSeparator = false) {
|
|
941
|
-
// Normalize time part to include seconds if missing (TOML 1.1.0 allows optional seconds)
|
|
942
|
-
let normalizedValue = value;
|
|
943
|
-
if (!/\d{2}:\d{2}:\d{2}/.test(value)) {
|
|
944
|
-
normalizedValue = value.replace(/(\d{2}:\d{2})([\s\-+TZ]|$)/, '$1:00$2');
|
|
945
|
-
}
|
|
946
|
-
super(normalizedValue.replace(' ', 'T'));
|
|
947
|
-
this.useSpaceSeparator = false;
|
|
948
|
-
this.useSpaceSeparator = useSpaceSeparator;
|
|
949
|
-
this.originalFormat = value;
|
|
950
|
-
// Extract and preserve the original offset
|
|
951
|
-
const offsetMatch = value.match(/([+-]\d{2}:\d{2}|[Zz])$/);
|
|
952
|
-
if (offsetMatch) {
|
|
953
|
-
this.originalOffset = offsetMatch[1] === 'z' ? 'Z' : offsetMatch[1];
|
|
954
|
-
}
|
|
955
|
-
}
|
|
956
|
-
toISOString() {
|
|
957
|
-
if (this.originalOffset) {
|
|
958
|
-
// Calculate the local time in the original timezone
|
|
959
|
-
const utcTime = this.getTime();
|
|
960
|
-
let offsetMinutes = 0;
|
|
961
|
-
if (this.originalOffset !== 'Z') {
|
|
962
|
-
const sign = this.originalOffset[0] === '+' ? 1 : -1;
|
|
963
|
-
const [hours, minutes] = this.originalOffset.slice(1).split(':');
|
|
964
|
-
offsetMinutes = sign * (parseInt(hours) * 60 + parseInt(minutes));
|
|
965
|
-
}
|
|
966
|
-
const localTime = new Date(utcTime + offsetMinutes * 60000);
|
|
967
|
-
const year = localTime.getUTCFullYear();
|
|
968
|
-
const month = String(localTime.getUTCMonth() + 1).padStart(2, '0');
|
|
969
|
-
const day = String(localTime.getUTCDate()).padStart(2, '0');
|
|
970
|
-
const hours = String(localTime.getUTCHours()).padStart(2, '0');
|
|
971
|
-
const minutes = String(localTime.getUTCMinutes()).padStart(2, '0');
|
|
972
|
-
const seconds = String(localTime.getUTCSeconds()).padStart(2, '0');
|
|
973
|
-
const milliseconds = localTime.getUTCMilliseconds();
|
|
974
|
-
const datePart = `${year}-${month}-${day}`;
|
|
975
|
-
const separator = this.useSpaceSeparator ? ' ' : 'T';
|
|
976
|
-
// Check if the original format had milliseconds
|
|
977
|
-
const originalHadMs = this.originalFormat && this.originalFormat.includes('.');
|
|
978
|
-
if (originalHadMs) {
|
|
979
|
-
// Determine the number of millisecond digits from the original format
|
|
980
|
-
const msMatch = this.originalFormat.match(/\.(\d+)(?:[Zz]|[+-]\d{2}:\d{2})\s*$/);
|
|
981
|
-
const msDigits = msMatch ? msMatch[1].length : 3;
|
|
982
|
-
const ms = String(milliseconds).padStart(3, '0').slice(0, msDigits);
|
|
983
|
-
return `${datePart}${separator}${hours}:${minutes}:${seconds}.${ms}${this.originalOffset}`;
|
|
984
|
-
}
|
|
985
|
-
else if (milliseconds > 0) {
|
|
986
|
-
// Original had no milliseconds, but current has non-zero milliseconds
|
|
987
|
-
// Show them with trailing zeros removed
|
|
988
|
-
const ms = String(milliseconds).padStart(3, '0').replace(/0+$/, '');
|
|
989
|
-
return `${datePart}${separator}${hours}:${minutes}:${seconds}.${ms}${this.originalOffset}`;
|
|
990
|
-
}
|
|
991
|
-
return `${datePart}${separator}${hours}:${minutes}:${seconds}${this.originalOffset}`;
|
|
992
|
-
}
|
|
993
|
-
const isoString = super.toISOString();
|
|
994
|
-
if (this.useSpaceSeparator) {
|
|
995
|
-
return isoString.replace('T', ' ');
|
|
996
|
-
}
|
|
997
|
-
return isoString;
|
|
998
|
-
}
|
|
999
|
-
}
|
|
1000
|
-
|
|
1001
|
-
// Create a shorter alias for convenience
|
|
1002
|
-
const dateFormatHelper = DateFormatHelper;
|
|
1003
|
-
const TRUE = 'true';
|
|
1004
|
-
const FALSE = 'false';
|
|
1005
|
-
const HAS_E = /e/i;
|
|
1006
|
-
const IS_DIVIDER = /\_/g;
|
|
1007
|
-
const IS_INF = /inf/;
|
|
1008
|
-
const IS_NAN = /nan/;
|
|
1009
|
-
const IS_HEX = /^0x/;
|
|
1010
|
-
const IS_OCTAL = /^0o/;
|
|
1011
|
-
const IS_BINARY = /^0b/;
|
|
1012
|
-
function* parseTOML(input) {
|
|
1013
|
-
const tokens = tokenize(input);
|
|
1014
|
-
const cursor = new Cursor(tokens);
|
|
1015
|
-
while (!cursor.next().done) {
|
|
1016
|
-
yield* walkBlock(cursor, input);
|
|
1017
|
-
}
|
|
1018
|
-
}
|
|
1019
|
-
/**
|
|
1020
|
-
* Continues parsing TOML from a remaining string and appends the results to an existing AST.
|
|
1021
|
-
*
|
|
1022
|
-
* @param existingAst - The existing AST to append to
|
|
1023
|
-
* @param remainingString - The remaining TOML string to parse
|
|
1024
|
-
* @returns A new complete AST with both the existing and newly parsed items
|
|
1025
|
-
*/
|
|
1026
|
-
function* continueParsingTOML(existingAst, remainingString) {
|
|
1027
|
-
// Yield all items from the existing AST
|
|
1028
|
-
for (const item of existingAst) {
|
|
1029
|
-
yield item;
|
|
1030
|
-
}
|
|
1031
|
-
// Parse and yield all items from the remaining string
|
|
1032
|
-
for (const item of parseTOML(remainingString)) {
|
|
1033
|
-
yield item;
|
|
1034
|
-
}
|
|
1035
|
-
}
|
|
1036
|
-
function* walkBlock(cursor, input) {
|
|
1037
|
-
if (cursor.value.type === TokenType.Comment) {
|
|
1038
|
-
yield comment(cursor);
|
|
1039
|
-
}
|
|
1040
|
-
else if (cursor.value.type === TokenType.Bracket) {
|
|
1041
|
-
yield table(cursor, input);
|
|
1042
|
-
}
|
|
1043
|
-
else if (cursor.value.type === TokenType.Literal) {
|
|
1044
|
-
yield* keyValue(cursor, input);
|
|
1045
|
-
}
|
|
1046
|
-
else {
|
|
1047
|
-
throw new ParseError(input, cursor.value.loc.start, `Unexpected token "${cursor.value.type}". Expected Comment, Bracket, or String`);
|
|
1048
|
-
}
|
|
1049
|
-
}
|
|
1050
|
-
function* walkValue$1(cursor, input) {
|
|
1051
|
-
if (cursor.value.type === TokenType.Literal) {
|
|
1052
|
-
if (cursor.value.raw[0] === DOUBLE_QUOTE || cursor.value.raw[0] === SINGLE_QUOTE) {
|
|
1053
|
-
yield string(cursor);
|
|
1054
|
-
}
|
|
1055
|
-
else if (cursor.value.raw === TRUE || cursor.value.raw === FALSE) {
|
|
1056
|
-
yield boolean(cursor);
|
|
1057
|
-
}
|
|
1058
|
-
else if (dateFormatHelper.IS_FULL_DATE.test(cursor.value.raw) || dateFormatHelper.IS_FULL_TIME.test(cursor.value.raw)) {
|
|
1059
|
-
yield datetime(cursor, input);
|
|
1060
|
-
}
|
|
1061
|
-
else if ((!cursor.peek().done && cursor.peek().value.type === TokenType.Dot) ||
|
|
1062
|
-
IS_INF.test(cursor.value.raw) ||
|
|
1063
|
-
IS_NAN.test(cursor.value.raw) ||
|
|
1064
|
-
(HAS_E.test(cursor.value.raw) && !IS_HEX.test(cursor.value.raw))) {
|
|
1065
|
-
yield float(cursor, input);
|
|
1066
|
-
}
|
|
1067
|
-
else {
|
|
1068
|
-
yield integer(cursor);
|
|
1069
|
-
}
|
|
1070
|
-
}
|
|
1071
|
-
else if (cursor.value.type === TokenType.Curly) {
|
|
1072
|
-
const [inline_table, comments] = inlineTable(cursor, input);
|
|
1073
|
-
yield inline_table;
|
|
1074
|
-
yield* comments;
|
|
1075
|
-
}
|
|
1076
|
-
else if (cursor.value.type === TokenType.Bracket) {
|
|
1077
|
-
const [inline_array, comments] = inlineArray(cursor, input);
|
|
1078
|
-
yield inline_array;
|
|
1079
|
-
yield* comments;
|
|
1080
|
-
}
|
|
1081
|
-
else {
|
|
1082
|
-
throw new ParseError(input, cursor.value.loc.start, `Unrecognized token type "${cursor.value.type}". Expected String, Curly, or Bracket`);
|
|
1083
|
-
}
|
|
1084
|
-
}
|
|
1085
|
-
function comment(cursor) {
|
|
1086
|
-
// # line comment
|
|
1087
|
-
// ^------------^ Comment
|
|
1088
|
-
return {
|
|
1089
|
-
type: NodeType.Comment,
|
|
1090
|
-
loc: cursor.value.loc,
|
|
1091
|
-
raw: cursor.value.raw
|
|
1092
|
-
};
|
|
1093
|
-
}
|
|
1094
|
-
function table(cursor, input) {
|
|
1095
|
-
// Table or TableArray
|
|
1096
|
-
//
|
|
1097
|
-
// [ key ]
|
|
1098
|
-
// ^-----^ TableKey
|
|
1099
|
-
// ^-^ Key
|
|
1100
|
-
//
|
|
1101
|
-
// [[ key ]]
|
|
1102
|
-
// ^ ------^ TableArrayKey
|
|
1103
|
-
// ^-^ Key
|
|
1104
|
-
//
|
|
1105
|
-
// a = "b" < Items
|
|
1106
|
-
// # c |
|
|
1107
|
-
// d = "f" <
|
|
1108
|
-
//
|
|
1109
|
-
// ...
|
|
1110
|
-
const type = !cursor.peek().done && cursor.peek().value.type === TokenType.Bracket
|
|
1111
|
-
? NodeType.TableArray
|
|
1112
|
-
: NodeType.Table;
|
|
1113
|
-
const is_table = type === NodeType.Table;
|
|
1114
|
-
if (is_table && cursor.value.raw !== '[') {
|
|
1115
|
-
throw new ParseError(input, cursor.value.loc.start, `Expected table opening "[", found ${cursor.value.raw}`);
|
|
1116
|
-
}
|
|
1117
|
-
if (!is_table && (cursor.value.raw !== '[' || cursor.peek().value.raw !== '[')) {
|
|
1118
|
-
throw new ParseError(input, cursor.value.loc.start, `Expected array of tables opening "[[", found ${cursor.value.raw + cursor.peek().value.raw}`);
|
|
1119
|
-
}
|
|
1120
|
-
// Set start location from opening tag
|
|
1121
|
-
const key = is_table
|
|
1122
|
-
? {
|
|
1123
|
-
type: NodeType.TableKey,
|
|
1124
|
-
loc: cursor.value.loc
|
|
1125
|
-
}
|
|
1126
|
-
: {
|
|
1127
|
-
type: NodeType.TableArrayKey,
|
|
1128
|
-
loc: cursor.value.loc
|
|
1129
|
-
};
|
|
1130
|
-
// Skip to cursor.value for key value
|
|
1131
|
-
cursor.next();
|
|
1132
|
-
if (type === NodeType.TableArray)
|
|
1133
|
-
cursor.next();
|
|
1134
|
-
if (cursor.done) {
|
|
1135
|
-
throw new ParseError(input, key.loc.start, `Expected table key, reached end of file`);
|
|
1136
|
-
}
|
|
1137
|
-
key.item = {
|
|
1138
|
-
type: NodeType.Key,
|
|
1139
|
-
loc: cloneLocation(cursor.value.loc),
|
|
1140
|
-
raw: cursor.value.raw,
|
|
1141
|
-
value: [parseString(cursor.value.raw)]
|
|
1142
|
-
};
|
|
1143
|
-
while (!cursor.peek().done && cursor.peek().value.type === TokenType.Dot) {
|
|
1144
|
-
cursor.next();
|
|
1145
|
-
const dot = cursor.value;
|
|
1146
|
-
cursor.next();
|
|
1147
|
-
const before = ' '.repeat(dot.loc.start.column - key.item.loc.end.column);
|
|
1148
|
-
const after = ' '.repeat(cursor.value.loc.start.column - dot.loc.end.column);
|
|
1149
|
-
key.item.loc.end = cursor.value.loc.end;
|
|
1150
|
-
key.item.raw += `${before}.${after}${cursor.value.raw}`;
|
|
1151
|
-
key.item.value.push(parseString(cursor.value.raw));
|
|
1152
|
-
}
|
|
1153
|
-
cursor.next();
|
|
1154
|
-
if (is_table && (cursor.done || cursor.value.raw !== ']')) {
|
|
1155
|
-
throw new ParseError(input, cursor.done ? key.item.loc.end : cursor.value.loc.start, `Expected table closing "]", found ${cursor.done ? 'end of file' : cursor.value.raw}`);
|
|
1156
|
-
}
|
|
1157
|
-
if (!is_table &&
|
|
1158
|
-
(cursor.done ||
|
|
1159
|
-
cursor.peek().done ||
|
|
1160
|
-
cursor.value.raw !== ']' ||
|
|
1161
|
-
cursor.peek().value.raw !== ']')) {
|
|
1162
|
-
throw new ParseError(input, cursor.done || cursor.peek().done ? key.item.loc.end : cursor.value.loc.start, `Expected array of tables closing "]]", found ${cursor.done || cursor.peek().done
|
|
1163
|
-
? 'end of file'
|
|
1164
|
-
: cursor.value.raw + cursor.peek().value.raw}`);
|
|
1165
|
-
}
|
|
1166
|
-
// Set end location from closing tag
|
|
1167
|
-
if (!is_table)
|
|
1168
|
-
cursor.next();
|
|
1169
|
-
key.loc.end = cursor.value.loc.end;
|
|
1170
|
-
// Add child items
|
|
1171
|
-
let items = [];
|
|
1172
|
-
while (!cursor.peek().done && cursor.peek().value.type !== TokenType.Bracket) {
|
|
1173
|
-
cursor.next();
|
|
1174
|
-
merge(items, [...walkBlock(cursor, input)]);
|
|
1175
|
-
}
|
|
1176
|
-
return {
|
|
1177
|
-
type: is_table ? NodeType.Table : NodeType.TableArray,
|
|
1178
|
-
loc: {
|
|
1179
|
-
start: clonePosition(key.loc.start),
|
|
1180
|
-
end: items.length
|
|
1181
|
-
? clonePosition(items[items.length - 1].loc.end)
|
|
1182
|
-
: clonePosition(key.loc.end)
|
|
1183
|
-
},
|
|
1184
|
-
key: key,
|
|
1185
|
-
items
|
|
1186
|
-
};
|
|
1187
|
-
}
|
|
1188
|
-
function keyValue(cursor, input) {
|
|
1189
|
-
// 3. KeyValue
|
|
1190
|
-
//
|
|
1191
|
-
// key = value
|
|
1192
|
-
// ^-^ key
|
|
1193
|
-
// ^ equals
|
|
1194
|
-
// ^---^ value
|
|
1195
|
-
const key = {
|
|
1196
|
-
type: NodeType.Key,
|
|
1197
|
-
loc: cloneLocation(cursor.value.loc),
|
|
1198
|
-
raw: cursor.value.raw,
|
|
1199
|
-
value: [parseString(cursor.value.raw)]
|
|
1200
|
-
};
|
|
1201
|
-
while (!cursor.peek().done && cursor.peek().value.type === TokenType.Dot) {
|
|
1202
|
-
cursor.next();
|
|
1203
|
-
cursor.next();
|
|
1204
|
-
key.loc.end = cursor.value.loc.end;
|
|
1205
|
-
key.raw += `.${cursor.value.raw}`;
|
|
1206
|
-
key.value.push(parseString(cursor.value.raw));
|
|
1207
|
-
}
|
|
1208
|
-
cursor.next();
|
|
1209
|
-
if (cursor.done || cursor.value.type !== TokenType.Equal) {
|
|
1210
|
-
throw new ParseError(input, cursor.done ? key.loc.end : cursor.value.loc.start, `Expected "=" for key-value, found ${cursor.done ? 'end of file' : cursor.value.raw}`);
|
|
1211
|
-
}
|
|
1212
|
-
const equals = cursor.value.loc.start.column;
|
|
1213
|
-
cursor.next();
|
|
1214
|
-
if (cursor.done) {
|
|
1215
|
-
throw new ParseError(input, key.loc.start, `Expected value for key-value, reached end of file`);
|
|
1216
|
-
}
|
|
1217
|
-
const [value, ...comments] = walkValue$1(cursor, input);
|
|
1218
|
-
return [
|
|
1219
|
-
{
|
|
1220
|
-
type: NodeType.KeyValue,
|
|
1221
|
-
key,
|
|
1222
|
-
value: value,
|
|
1223
|
-
loc: {
|
|
1224
|
-
start: clonePosition(key.loc.start),
|
|
1225
|
-
end: clonePosition(value.loc.end)
|
|
1226
|
-
},
|
|
1227
|
-
equals
|
|
1228
|
-
},
|
|
1229
|
-
...comments
|
|
1230
|
-
];
|
|
1231
|
-
}
|
|
1232
|
-
function string(cursor) {
|
|
1233
|
-
return {
|
|
1234
|
-
type: NodeType.String,
|
|
1235
|
-
loc: cursor.value.loc,
|
|
1236
|
-
raw: cursor.value.raw,
|
|
1237
|
-
value: parseString(cursor.value.raw)
|
|
1238
|
-
};
|
|
1239
|
-
}
|
|
1240
|
-
function boolean(cursor) {
|
|
1241
|
-
return {
|
|
1242
|
-
type: NodeType.Boolean,
|
|
1243
|
-
loc: cursor.value.loc,
|
|
1244
|
-
value: cursor.value.raw === TRUE
|
|
1245
|
-
};
|
|
1246
|
-
}
|
|
1247
|
-
function datetime(cursor, input) {
|
|
1248
|
-
// Possible values:
|
|
1249
|
-
//
|
|
1250
|
-
// Offset Date-Time
|
|
1251
|
-
// | odt1 = 1979-05-27T07:32:00Z
|
|
1252
|
-
// | odt2 = 1979-05-27T00:32:00-07:00
|
|
1253
|
-
// | odt3 = 1979-05-27T00:32:00.999999-07:00
|
|
1254
|
-
// | odt4 = 1979-05-27 07:32:00Z
|
|
1255
|
-
//
|
|
1256
|
-
// Local Date-Time
|
|
1257
|
-
// | ldt1 = 1979-05-27T07:32:00
|
|
1258
|
-
// | ldt2 = 1979-05-27T00:32:00.999999
|
|
1259
|
-
//
|
|
1260
|
-
// Local Date
|
|
1261
|
-
// | ld1 = 1979-05-27
|
|
1262
|
-
//
|
|
1263
|
-
// Local Time
|
|
1264
|
-
// | lt1 = 07:32:00
|
|
1265
|
-
// | lt2 = 00:32:00.999999
|
|
1266
|
-
let loc = cursor.value.loc;
|
|
1267
|
-
let raw = cursor.value.raw;
|
|
1268
|
-
let value;
|
|
1269
|
-
// If next token is string,
|
|
1270
|
-
// check if raw is full date and following is full time
|
|
1271
|
-
if (!cursor.peek().done &&
|
|
1272
|
-
cursor.peek().value.type === TokenType.Literal &&
|
|
1273
|
-
dateFormatHelper.IS_FULL_DATE.test(raw) &&
|
|
1274
|
-
dateFormatHelper.IS_FULL_TIME.test(cursor.peek().value.raw)) {
|
|
1275
|
-
const start = loc.start;
|
|
1276
|
-
cursor.next();
|
|
1277
|
-
loc = { start, end: cursor.value.loc.end };
|
|
1278
|
-
raw += ` ${cursor.value.raw}`;
|
|
1279
|
-
}
|
|
1280
|
-
if (!cursor.peek().done && cursor.peek().value.type === TokenType.Dot) {
|
|
1281
|
-
const start = loc.start;
|
|
1282
|
-
cursor.next();
|
|
1283
|
-
if (cursor.peek().done || cursor.peek().value.type !== TokenType.Literal) {
|
|
1284
|
-
throw new ParseError(input, cursor.value.loc.end, `Expected fractional value for DateTime`);
|
|
1285
|
-
}
|
|
1286
|
-
cursor.next();
|
|
1287
|
-
loc = { start, end: cursor.value.loc.end };
|
|
1288
|
-
raw += `.${cursor.value.raw}`;
|
|
1289
|
-
}
|
|
1290
|
-
if (!dateFormatHelper.IS_FULL_DATE.test(raw)) {
|
|
1291
|
-
// Local time only (e.g., "07:32:00" or "07:32:00.999")
|
|
1292
|
-
if (dateFormatHelper.IS_TIME_ONLY.test(raw)) {
|
|
1293
|
-
value = new LocalTime(raw, raw);
|
|
1294
|
-
}
|
|
1295
|
-
else {
|
|
1296
|
-
// For other time formats, use local ISO date
|
|
1297
|
-
const [local_date] = new Date().toISOString().split('T');
|
|
1298
|
-
value = new Date(`${local_date}T${raw}`);
|
|
1299
|
-
}
|
|
1300
|
-
}
|
|
1301
|
-
else if (dateFormatHelper.IS_DATE_ONLY.test(raw)) {
|
|
1302
|
-
// Local date only (e.g., "1979-05-27")
|
|
1303
|
-
value = new LocalDate(raw);
|
|
1304
|
-
}
|
|
1305
|
-
else if (dateFormatHelper.IS_LOCAL_DATETIME_T.test(raw)) {
|
|
1306
|
-
// Local datetime with T separator (e.g., "1979-05-27T07:32:00")
|
|
1307
|
-
value = new LocalDateTime(raw, false);
|
|
1308
|
-
}
|
|
1309
|
-
else if (dateFormatHelper.IS_LOCAL_DATETIME_SPACE.test(raw)) {
|
|
1310
|
-
// Local datetime with space separator (e.g., "1979-05-27 07:32:00")
|
|
1311
|
-
value = new LocalDateTime(raw, true);
|
|
1312
|
-
}
|
|
1313
|
-
else if (dateFormatHelper.IS_OFFSET_DATETIME_T.test(raw)) {
|
|
1314
|
-
// Offset datetime with T separator (e.g., "1979-05-27T07:32:00Z" or "1979-05-27T07:32:00-07:00")
|
|
1315
|
-
value = new OffsetDateTime(raw, false);
|
|
1316
|
-
}
|
|
1317
|
-
else if (dateFormatHelper.IS_OFFSET_DATETIME_SPACE.test(raw)) {
|
|
1318
|
-
// Offset datetime with space separator (e.g., "1979-05-27 07:32:00Z")
|
|
1319
|
-
value = new OffsetDateTime(raw, true);
|
|
1320
|
-
}
|
|
1321
|
-
else {
|
|
1322
|
-
// Default: offset datetime with T separator or any other format
|
|
1323
|
-
value = new Date(raw.replace(' ', 'T'));
|
|
1324
|
-
}
|
|
1325
|
-
return {
|
|
1326
|
-
type: NodeType.DateTime,
|
|
1327
|
-
loc,
|
|
1328
|
-
raw,
|
|
1329
|
-
value
|
|
1330
|
-
};
|
|
1331
|
-
}
|
|
1332
|
-
function float(cursor, input) {
|
|
1333
|
-
let loc = cursor.value.loc;
|
|
1334
|
-
let raw = cursor.value.raw;
|
|
1335
|
-
let value;
|
|
1336
|
-
if (IS_INF.test(raw)) {
|
|
1337
|
-
value = raw === '-inf' ? -Infinity : Infinity;
|
|
1338
|
-
}
|
|
1339
|
-
else if (IS_NAN.test(raw)) {
|
|
1340
|
-
value = raw === '-nan' ? -NaN : NaN;
|
|
1341
|
-
}
|
|
1342
|
-
else if (!cursor.peek().done && cursor.peek().value.type === TokenType.Dot) {
|
|
1343
|
-
const start = loc.start;
|
|
1344
|
-
// From spec:
|
|
1345
|
-
// | A fractional part is a decimal point followed by one or more digits.
|
|
1346
|
-
//
|
|
1347
|
-
// -> Don't have to handle "4." (i.e. nothing behind decimal place)
|
|
1348
|
-
cursor.next();
|
|
1349
|
-
if (cursor.peek().done || cursor.peek().value.type !== TokenType.Literal) {
|
|
1350
|
-
throw new ParseError(input, cursor.value.loc.end, `Expected fraction value for Float`);
|
|
1351
|
-
}
|
|
1352
|
-
cursor.next();
|
|
1353
|
-
raw += `.${cursor.value.raw}`;
|
|
1354
|
-
loc = { start, end: cursor.value.loc.end };
|
|
1355
|
-
value = Number(raw.replace(IS_DIVIDER, ''));
|
|
1356
|
-
}
|
|
1357
|
-
else {
|
|
1358
|
-
value = Number(raw.replace(IS_DIVIDER, ''));
|
|
1359
|
-
}
|
|
1360
|
-
return { type: NodeType.Float, loc, raw, value };
|
|
1361
|
-
}
|
|
1362
|
-
function integer(cursor) {
|
|
1363
|
-
// > Integer values -0 and +0 are valid and identical to an unprefixed zero
|
|
1364
|
-
if (cursor.value.raw === '-0' || cursor.value.raw === '+0') {
|
|
1365
|
-
return {
|
|
1366
|
-
type: NodeType.Integer,
|
|
1367
|
-
loc: cursor.value.loc,
|
|
1368
|
-
raw: cursor.value.raw,
|
|
1369
|
-
value: 0
|
|
1370
|
-
};
|
|
1371
|
-
}
|
|
1372
|
-
let radix = 10;
|
|
1373
|
-
if (IS_HEX.test(cursor.value.raw)) {
|
|
1374
|
-
radix = 16;
|
|
1375
|
-
}
|
|
1376
|
-
else if (IS_OCTAL.test(cursor.value.raw)) {
|
|
1377
|
-
radix = 8;
|
|
1378
|
-
}
|
|
1379
|
-
else if (IS_BINARY.test(cursor.value.raw)) {
|
|
1380
|
-
radix = 2;
|
|
1381
|
-
}
|
|
1382
|
-
const value = parseInt(cursor
|
|
1383
|
-
.value.raw.replace(IS_DIVIDER, '')
|
|
1384
|
-
.replace(IS_OCTAL, '')
|
|
1385
|
-
.replace(IS_BINARY, ''), radix);
|
|
1386
|
-
return {
|
|
1387
|
-
type: NodeType.Integer,
|
|
1388
|
-
loc: cursor.value.loc,
|
|
1389
|
-
raw: cursor.value.raw,
|
|
1390
|
-
value
|
|
1391
|
-
};
|
|
1392
|
-
}
|
|
1393
|
-
function inlineTable(cursor, input) {
|
|
1394
|
-
if (cursor.value.raw !== '{') {
|
|
1395
|
-
throw new ParseError(input, cursor.value.loc.start, `Expected "{" for inline table, found ${cursor.value.raw}`);
|
|
1396
|
-
}
|
|
1397
|
-
// 6. InlineTable
|
|
1398
|
-
const value = {
|
|
1399
|
-
type: NodeType.InlineTable,
|
|
1400
|
-
loc: cloneLocation(cursor.value.loc),
|
|
1401
|
-
items: []
|
|
1402
|
-
};
|
|
1403
|
-
const comments = [];
|
|
1404
|
-
cursor.next();
|
|
1405
|
-
while (!cursor.done &&
|
|
1406
|
-
!(cursor.value.type === TokenType.Curly && cursor.value.raw === '}')) {
|
|
1407
|
-
// TOML 1.1.0: Handle comments in inline tables
|
|
1408
|
-
if (cursor.value.type === TokenType.Comment) {
|
|
1409
|
-
comments.push(comment(cursor));
|
|
1410
|
-
cursor.next();
|
|
1411
|
-
continue;
|
|
1412
|
-
}
|
|
1413
|
-
if (cursor.value.type === TokenType.Comma) {
|
|
1414
|
-
const previous = value.items[value.items.length - 1];
|
|
1415
|
-
if (!previous) {
|
|
1416
|
-
throw new ParseError(input, cursor.value.loc.start, 'Found "," without previous value in inline table');
|
|
1417
|
-
}
|
|
1418
|
-
previous.comma = true;
|
|
1419
|
-
previous.loc.end = cursor.value.loc.start;
|
|
1420
|
-
cursor.next();
|
|
1421
|
-
continue;
|
|
1422
|
-
}
|
|
1423
|
-
const [item, ...additional_comments] = walkBlock(cursor, input);
|
|
1424
|
-
if (item.type !== NodeType.KeyValue) {
|
|
1425
|
-
throw new ParseError(input, cursor.value.loc.start, `Only key-values are supported in inline tables, found ${item.type}`);
|
|
1426
|
-
}
|
|
1427
|
-
const inline_item = {
|
|
1428
|
-
type: NodeType.InlineItem,
|
|
1429
|
-
loc: cloneLocation(item.loc),
|
|
1430
|
-
item,
|
|
1431
|
-
comma: false
|
|
1432
|
-
};
|
|
1433
|
-
value.items.push(inline_item);
|
|
1434
|
-
merge(comments, additional_comments);
|
|
1435
|
-
cursor.next();
|
|
1436
|
-
}
|
|
1437
|
-
if (cursor.done ||
|
|
1438
|
-
cursor.value.type !== TokenType.Curly ||
|
|
1439
|
-
cursor.value.raw !== '}') {
|
|
1440
|
-
throw new ParseError(input, cursor.done ? value.loc.start : cursor.value.loc.start, `Expected "}", found ${cursor.done ? 'end of file' : cursor.value.raw}`);
|
|
1441
|
-
}
|
|
1442
|
-
value.loc.end = cursor.value.loc.end;
|
|
1443
|
-
return [value, comments];
|
|
1444
|
-
}
|
|
1445
|
-
function inlineArray(cursor, input) {
|
|
1446
|
-
// 7. InlineArray
|
|
1447
|
-
if (cursor.value.raw !== '[') {
|
|
1448
|
-
throw new ParseError(input, cursor.value.loc.start, `Expected "[" for inline array, found ${cursor.value.raw}`);
|
|
1449
|
-
}
|
|
1450
|
-
const value = {
|
|
1451
|
-
type: NodeType.InlineArray,
|
|
1452
|
-
loc: cloneLocation(cursor.value.loc),
|
|
1453
|
-
items: []
|
|
1454
|
-
};
|
|
1455
|
-
let comments = [];
|
|
1456
|
-
cursor.next();
|
|
1457
|
-
while (!cursor.done &&
|
|
1458
|
-
!(cursor.value.type === TokenType.Bracket && cursor.value.raw === ']')) {
|
|
1459
|
-
if (cursor.value.type === TokenType.Comma) {
|
|
1460
|
-
const previous = value.items[value.items.length - 1];
|
|
1461
|
-
if (!previous) {
|
|
1462
|
-
throw new ParseError(input, cursor.value.loc.start, 'Found "," without previous value for inline array');
|
|
1463
|
-
}
|
|
1464
|
-
previous.comma = true;
|
|
1465
|
-
previous.loc.end = cursor.value.loc.start;
|
|
1466
|
-
}
|
|
1467
|
-
else if (cursor.value.type === TokenType.Comment) {
|
|
1468
|
-
comments.push(comment(cursor));
|
|
1469
|
-
}
|
|
1470
|
-
else {
|
|
1471
|
-
const [item, ...additional_comments] = walkValue$1(cursor, input);
|
|
1472
|
-
const inline_item = {
|
|
1473
|
-
type: NodeType.InlineItem,
|
|
1474
|
-
loc: cloneLocation(item.loc),
|
|
1475
|
-
item,
|
|
1476
|
-
comma: false
|
|
1477
|
-
};
|
|
1478
|
-
value.items.push(inline_item);
|
|
1479
|
-
merge(comments, additional_comments);
|
|
1480
|
-
}
|
|
1481
|
-
cursor.next();
|
|
1482
|
-
}
|
|
1483
|
-
if (cursor.done ||
|
|
1484
|
-
cursor.value.type !== TokenType.Bracket ||
|
|
1485
|
-
cursor.value.raw !== ']') {
|
|
1486
|
-
throw new ParseError(input, cursor.done ? value.loc.start : cursor.value.loc.start, `Expected "]", found ${cursor.done ? 'end of file' : cursor.value.raw}`);
|
|
1487
|
-
}
|
|
1488
|
-
value.loc.end = cursor.value.loc.end;
|
|
1489
|
-
return [value, comments];
|
|
1490
|
-
}
|
|
1491
|
-
|
|
1492
|
-
////////////////////////////////////////////////////////////////////////////////
|
|
1493
|
-
// The traverse function is used to walk the AST and call the visitor functions
|
|
1494
|
-
////////////////////////////////////////////////////////////////////////////////
|
|
1495
|
-
function traverse(ast, visitor) {
|
|
1496
|
-
if (isIterable(ast)) {
|
|
1497
|
-
traverseArray(ast, null);
|
|
1498
|
-
}
|
|
1499
|
-
else {
|
|
1500
|
-
traverseNode(ast, null);
|
|
1501
|
-
}
|
|
1502
|
-
function traverseArray(array, parent) {
|
|
1503
|
-
for (const node of array) {
|
|
1504
|
-
traverseNode(node, parent);
|
|
1505
|
-
}
|
|
1506
|
-
}
|
|
1507
|
-
function traverseNode(node, parent) {
|
|
1508
|
-
const visit = visitor[node.type];
|
|
1509
|
-
if (visit && typeof visit === 'function') {
|
|
1510
|
-
visit(node, parent);
|
|
1511
|
-
}
|
|
1512
|
-
if (visit && visit.enter) {
|
|
1513
|
-
visit.enter(node, parent);
|
|
1514
|
-
}
|
|
1515
|
-
switch (node.type) {
|
|
1516
|
-
case NodeType.Document:
|
|
1517
|
-
traverseArray(node.items, node);
|
|
1518
|
-
break;
|
|
1519
|
-
case NodeType.Table:
|
|
1520
|
-
traverseNode(node.key, node);
|
|
1521
|
-
traverseArray(node.items, node);
|
|
1522
|
-
break;
|
|
1523
|
-
case NodeType.TableKey:
|
|
1524
|
-
traverseNode(node.item, node);
|
|
1525
|
-
break;
|
|
1526
|
-
case NodeType.TableArray:
|
|
1527
|
-
traverseNode(node.key, node);
|
|
1528
|
-
traverseArray(node.items, node);
|
|
1529
|
-
break;
|
|
1530
|
-
case NodeType.TableArrayKey:
|
|
1531
|
-
traverseNode(node.item, node);
|
|
1532
|
-
break;
|
|
1533
|
-
case NodeType.KeyValue:
|
|
1534
|
-
traverseNode(node.key, node);
|
|
1535
|
-
traverseNode(node.value, node);
|
|
1536
|
-
break;
|
|
1537
|
-
case NodeType.InlineArray:
|
|
1538
|
-
traverseArray(node.items, node);
|
|
1539
|
-
break;
|
|
1540
|
-
case NodeType.InlineItem:
|
|
1541
|
-
traverseNode(node.item, node);
|
|
1542
|
-
break;
|
|
1543
|
-
case NodeType.InlineTable:
|
|
1544
|
-
traverseArray(node.items, node);
|
|
1545
|
-
break;
|
|
1546
|
-
case NodeType.Key:
|
|
1547
|
-
case NodeType.String:
|
|
1548
|
-
case NodeType.Integer:
|
|
1549
|
-
case NodeType.Float:
|
|
1550
|
-
case NodeType.Boolean:
|
|
1551
|
-
case NodeType.DateTime:
|
|
1552
|
-
case NodeType.Comment:
|
|
1553
|
-
break;
|
|
1554
|
-
default:
|
|
1555
|
-
throw new Error(`Unrecognized node type "${node.type}"`);
|
|
1556
|
-
}
|
|
1557
|
-
if (visit && visit.exit) {
|
|
1558
|
-
visit.exit(node, parent);
|
|
1559
|
-
}
|
|
1560
|
-
}
|
|
1561
|
-
}
|
|
1562
|
-
|
|
1563
|
-
const enter_offsets = new WeakMap();
|
|
1564
|
-
const getEnterOffsets = (root) => {
|
|
1565
|
-
if (!enter_offsets.has(root)) {
|
|
1566
|
-
enter_offsets.set(root, new WeakMap());
|
|
1567
|
-
}
|
|
1568
|
-
return enter_offsets.get(root);
|
|
1569
|
-
};
|
|
1570
|
-
const exit_offsets = new WeakMap();
|
|
1571
|
-
const getExitOffsets = (root) => {
|
|
1572
|
-
if (!exit_offsets.has(root)) {
|
|
1573
|
-
exit_offsets.set(root, new WeakMap());
|
|
1574
|
-
}
|
|
1575
|
-
return exit_offsets.get(root);
|
|
1576
|
-
};
|
|
1577
|
-
//TODO: Add getOffsets function to get all offsets contained in the tree
|
|
1578
|
-
function replace(root, parent, existing, replacement) {
|
|
1579
|
-
// First, replace existing node
|
|
1580
|
-
// (by index for items, item, or key/value)
|
|
1581
|
-
if (hasItems(parent)) {
|
|
1582
|
-
const index = parent.items.indexOf(existing);
|
|
1583
|
-
if (index < 0) {
|
|
1584
|
-
throw new Error(`Could not find existing item in parent node for replace`);
|
|
1585
|
-
}
|
|
1586
|
-
parent.items.splice(index, 1, replacement);
|
|
1587
|
-
// This next case is a special case for Inline-Table item
|
|
1588
|
-
// however due to the fact that both replacement of the whole Inline-Table and Inline-Table element will have the same parent,
|
|
1589
|
-
// we need to make sure it's not an Inline-Table
|
|
1590
|
-
}
|
|
1591
|
-
else if (isKeyValue(parent) && isInlineTable(parent.value) && !isInlineTable(existing)) {
|
|
1592
|
-
const index = parent.value.items.indexOf(existing);
|
|
1593
|
-
if (index < 0) {
|
|
1594
|
-
throw new Error(`Could not find existing item in parent node for replace`);
|
|
1595
|
-
}
|
|
1596
|
-
parent.value.items.splice(index, 1, replacement);
|
|
1597
|
-
}
|
|
1598
|
-
else if (hasItem(parent)) {
|
|
1599
|
-
parent.item = replacement;
|
|
1600
|
-
}
|
|
1601
|
-
else if (isKeyValue(parent)) {
|
|
1602
|
-
if (parent.key === existing) {
|
|
1603
|
-
parent.key = replacement;
|
|
1604
|
-
}
|
|
1605
|
-
else {
|
|
1606
|
-
parent.value = replacement;
|
|
1607
|
-
}
|
|
1608
|
-
}
|
|
1609
|
-
else {
|
|
1610
|
-
throw new Error(`Unsupported parent type "${parent.type}" for replace`);
|
|
1611
|
-
}
|
|
1612
|
-
// Shift the replacement node into the same start position as existing
|
|
1613
|
-
const shift = {
|
|
1614
|
-
lines: existing.loc.start.line - replacement.loc.start.line,
|
|
1615
|
-
columns: existing.loc.start.column - replacement.loc.start.column
|
|
1616
|
-
};
|
|
1617
|
-
shiftNode(replacement, shift);
|
|
1618
|
-
// Apply offsets after replacement node
|
|
1619
|
-
const existing_span = getSpan(existing.loc);
|
|
1620
|
-
const replacement_span = getSpan(replacement.loc);
|
|
1621
|
-
const offset = {
|
|
1622
|
-
lines: replacement_span.lines - existing_span.lines,
|
|
1623
|
-
columns: replacement_span.columns - existing_span.columns
|
|
1624
|
-
};
|
|
1625
|
-
addOffset(offset, getExitOffsets(root), replacement, existing);
|
|
1626
|
-
}
|
|
1627
|
-
/**
|
|
1628
|
-
* Inserts a child node into the AST.
|
|
1629
|
-
*
|
|
1630
|
-
* @param root - The root node of the AST
|
|
1631
|
-
* @param parent - The parent node to insert the child into
|
|
1632
|
-
* @param child - The child node to insert
|
|
1633
|
-
* @param index - The index at which to insert the child (optional)
|
|
1634
|
-
* @param forceInline - Whether to force inline positioning even for document-level insertions (optional)
|
|
1635
|
-
*/
|
|
1636
|
-
function insert(root, parent, child, index, forceInline) {
|
|
1637
|
-
if (!hasItems(parent)) {
|
|
1638
|
-
throw new Error(`Unsupported parent type "${parent.type}" for insert`);
|
|
1639
|
-
}
|
|
1640
|
-
index = (index != null && typeof index === 'number') ? index : parent.items.length;
|
|
1641
|
-
let shift;
|
|
1642
|
-
let offset;
|
|
1643
|
-
if (isInlineArray(parent) || isInlineTable(parent)) {
|
|
1644
|
-
({ shift, offset } = insertInline(parent, child, index));
|
|
1645
|
-
}
|
|
1646
|
-
else if (forceInline && isDocument(parent)) {
|
|
1647
|
-
({ shift, offset } = insertInlineAtRoot(parent, child, index));
|
|
1648
|
-
}
|
|
1649
|
-
else {
|
|
1650
|
-
({ shift, offset } = insertOnNewLine(parent, child, index));
|
|
1651
|
-
}
|
|
1652
|
-
shiftNode(child, shift);
|
|
1653
|
-
// The child element is placed relative to the previous element,
|
|
1654
|
-
// if the previous element has an offset, need to position relative to that
|
|
1655
|
-
// -> Move previous offset to child's offset
|
|
1656
|
-
const previous = parent.items[index - 1];
|
|
1657
|
-
const previous_offset = previous && getExitOffsets(root).get(previous);
|
|
1658
|
-
if (previous_offset) {
|
|
1659
|
-
offset.lines += previous_offset.lines;
|
|
1660
|
-
offset.columns += previous_offset.columns;
|
|
1661
|
-
getExitOffsets(root).delete(previous);
|
|
1662
|
-
}
|
|
1663
|
-
const offsets = getExitOffsets(root);
|
|
1664
|
-
offsets.set(child, offset);
|
|
1665
|
-
}
|
|
1666
|
-
function insertOnNewLine(parent, child, index) {
|
|
1667
|
-
if (!isBlock(child)) {
|
|
1668
|
-
throw new Error(`Incompatible child type "${child.type}"`);
|
|
1669
|
-
}
|
|
1670
|
-
const previous = parent.items[index - 1];
|
|
1671
|
-
const use_first_line = isDocument(parent) && !parent.items.length;
|
|
1672
|
-
parent.items.splice(index, 0, child);
|
|
1673
|
-
// Set start location from previous item or start of array
|
|
1674
|
-
// (previous is undefined for empty array or inserting at first item)
|
|
1675
|
-
const start = previous
|
|
1676
|
-
? {
|
|
1677
|
-
line: previous.loc.end.line,
|
|
1678
|
-
column: !isComment(previous) ? previous.loc.start.column : parent.loc.start.column
|
|
1679
|
-
}
|
|
1680
|
-
: clonePosition(parent.loc.start);
|
|
1681
|
-
const isSquareBracketsStructure = isTable(child) || isTableArray(child);
|
|
1682
|
-
let leading_lines = 0;
|
|
1683
|
-
if (use_first_line) ;
|
|
1684
|
-
else if (isSquareBracketsStructure) {
|
|
1685
|
-
leading_lines = 2;
|
|
1686
|
-
}
|
|
1687
|
-
else {
|
|
1688
|
-
leading_lines = 1;
|
|
1689
|
-
}
|
|
1690
|
-
start.line += leading_lines;
|
|
1691
|
-
const shift = {
|
|
1692
|
-
lines: start.line - child.loc.start.line,
|
|
1693
|
-
columns: start.column - child.loc.start.column
|
|
1694
|
-
};
|
|
1695
|
-
// Apply offsets after child node
|
|
1696
|
-
const child_span = getSpan(child.loc);
|
|
1697
|
-
const offset = {
|
|
1698
|
-
lines: child_span.lines + (leading_lines - 1),
|
|
1699
|
-
columns: child_span.columns
|
|
1700
|
-
};
|
|
1701
|
-
return { shift, offset };
|
|
1702
|
-
}
|
|
1703
|
-
/**
|
|
1704
|
-
* Calculates positioning (shift and offset) for inserting a child into a parent container.
|
|
1705
|
-
* This function handles the core positioning logic used to insert an inline item inside a table (or at the document root level).
|
|
1706
|
-
*
|
|
1707
|
-
* @param parent - The parent container (Document, InlineArray or InlineTable)
|
|
1708
|
-
* @param child - The child node to be inserted
|
|
1709
|
-
* @param index - The insertion index within the parent's items
|
|
1710
|
-
* @param options - Configuration options for positioning calculation
|
|
1711
|
-
* @param options.useNewLine - Whether to place the child on a new line
|
|
1712
|
-
* @param options.skipCommaSpace - Number of columns to skip for comma + space (default: 2)
|
|
1713
|
-
* @param options.skipBracketSpace - Number of columns to skip for bracket/space (default: 1)
|
|
1714
|
-
* @param options.hasCommaHandling - Whether comma handling logic should be applied
|
|
1715
|
-
* @param options.isLastElement - Whether this is the last element in the container
|
|
1716
|
-
* @param options.hasSeparatingCommaBefore - Whether a comma should precede this element
|
|
1717
|
-
* @param options.hasSeparatingCommaAfter - Whether a comma should follow this element
|
|
1718
|
-
* @param options.hasTrailingComma - Whether the element has a trailing comma
|
|
1719
|
-
* @returns Object containing shift (positioning adjustment for the child) and offset (adjustment for following elements)
|
|
1720
|
-
*/
|
|
1721
|
-
function calculateInlinePositioning(parent, child, index, options = {}) {
|
|
1722
|
-
// Configuration options with default values
|
|
1723
|
-
const { useNewLine = false, skipCommaSpace = 2, skipBracketSpace = 1, hasCommaHandling = false, isLastElement = false, hasSeparatingCommaBefore = false, hasSeparatingCommaAfter = false, hasTrailingComma = false } = options;
|
|
1724
|
-
// Store preceding node
|
|
1725
|
-
const previous = index > 0 ? parent.items[index - 1] : undefined;
|
|
1726
|
-
// Set start location from previous item or start of parent
|
|
1727
|
-
const start = previous
|
|
1728
|
-
? {
|
|
1729
|
-
line: previous.loc.end.line,
|
|
1730
|
-
column: useNewLine
|
|
1731
|
-
? !isComment(previous)
|
|
1732
|
-
? previous.loc.start.column
|
|
1733
|
-
: parent.loc.start.column
|
|
1734
|
-
: previous.loc.end.column
|
|
1735
|
-
}
|
|
1736
|
-
: clonePosition(parent.loc.start);
|
|
1737
|
-
let leading_lines = 0;
|
|
1738
|
-
if (useNewLine) {
|
|
1739
|
-
leading_lines = 1;
|
|
1740
|
-
}
|
|
1741
|
-
else {
|
|
1742
|
-
// Add spacing for inline positioning
|
|
1743
|
-
const hasSpacing = hasSeparatingCommaBefore || (!hasCommaHandling && !!previous);
|
|
1744
|
-
if (hasSpacing && hasCommaHandling) {
|
|
1745
|
-
start.column += skipCommaSpace;
|
|
1746
|
-
}
|
|
1747
|
-
else if (hasSpacing || (hasCommaHandling && !previous)) {
|
|
1748
|
-
start.column += skipBracketSpace;
|
|
1749
|
-
}
|
|
1750
|
-
}
|
|
1751
|
-
start.line += leading_lines;
|
|
1752
|
-
const shift = {
|
|
1753
|
-
lines: start.line - child.loc.start.line,
|
|
1754
|
-
columns: start.column - child.loc.start.column
|
|
1755
|
-
};
|
|
1756
|
-
// Apply offsets after child node
|
|
1757
|
-
const child_span = getSpan(child.loc);
|
|
1758
|
-
if (!hasCommaHandling) {
|
|
1759
|
-
// For documents or contexts without comma handling, simpler offset calculation
|
|
1760
|
-
const offset = {
|
|
1761
|
-
lines: child_span.lines + (leading_lines - 1),
|
|
1762
|
-
columns: child_span.columns
|
|
1763
|
-
};
|
|
1764
|
-
return { shift, offset };
|
|
1765
|
-
}
|
|
1766
|
-
// Special case: Fix trailing comma spacing issue for arrays that have trailing commas
|
|
1767
|
-
const has_trailing_comma_spacing_bug = hasSeparatingCommaBefore &&
|
|
1768
|
-
hasTrailingComma &&
|
|
1769
|
-
!hasSeparatingCommaAfter &&
|
|
1770
|
-
isLastElement;
|
|
1771
|
-
let trailing_comma_offset_adjustment = 0;
|
|
1772
|
-
if (has_trailing_comma_spacing_bug) {
|
|
1773
|
-
trailing_comma_offset_adjustment = -1;
|
|
1774
|
-
}
|
|
1775
|
-
const offset = {
|
|
1776
|
-
lines: child_span.lines + (leading_lines - 1),
|
|
1777
|
-
columns: child_span.columns +
|
|
1778
|
-
(hasSeparatingCommaBefore || hasSeparatingCommaAfter ? skipCommaSpace : 0) +
|
|
1779
|
-
(hasTrailingComma ? 1 + trailing_comma_offset_adjustment : 0)
|
|
1780
|
-
};
|
|
1781
|
-
return { shift, offset };
|
|
1782
|
-
}
|
|
1783
|
-
function insertInline(parent, child, index) {
|
|
1784
|
-
if (!isInlineItem(child)) {
|
|
1785
|
-
throw new Error(`Incompatible child type "${child.type}"`);
|
|
1786
|
-
}
|
|
1787
|
-
// Store preceding node and insert
|
|
1788
|
-
const previous = index != null ? parent.items[index - 1] : last(parent.items);
|
|
1789
|
-
const is_last = index == null || index === parent.items.length;
|
|
1790
|
-
parent.items.splice(index, 0, child);
|
|
1791
|
-
// Add commas as-needed
|
|
1792
|
-
const has_separating_comma_before = !!previous;
|
|
1793
|
-
const has_separating_comma_after = !is_last;
|
|
1794
|
-
if (has_separating_comma_before) {
|
|
1795
|
-
previous.comma = true;
|
|
1796
|
-
}
|
|
1797
|
-
if (has_separating_comma_after) {
|
|
1798
|
-
child.comma = true;
|
|
1799
|
-
}
|
|
1800
|
-
// Use new line for inline arrays that span multiple lines
|
|
1801
|
-
const use_new_line = isInlineArray(parent) && perLine(parent);
|
|
1802
|
-
const has_trailing_comma = is_last && child.comma === true;
|
|
1803
|
-
return calculateInlinePositioning(parent, child, index, {
|
|
1804
|
-
useNewLine: use_new_line,
|
|
1805
|
-
hasCommaHandling: true,
|
|
1806
|
-
isLastElement: is_last,
|
|
1807
|
-
hasSeparatingCommaBefore: has_separating_comma_before,
|
|
1808
|
-
hasSeparatingCommaAfter: has_separating_comma_after,
|
|
1809
|
-
hasTrailingComma: has_trailing_comma
|
|
1810
|
-
});
|
|
1811
|
-
}
|
|
1812
|
-
/**
|
|
1813
|
-
* Inserts a child into a Document with inline positioning behavior.
|
|
1814
|
-
* This provides inline-style spacing while maintaining Document's Block item types.
|
|
1815
|
-
*/
|
|
1816
|
-
function insertInlineAtRoot(parent, child, index) {
|
|
1817
|
-
// Calculate positioning as if inserting into an inline context
|
|
1818
|
-
const result = calculateInlinePositioning(parent, child, index, {
|
|
1819
|
-
useNewLine: false,
|
|
1820
|
-
hasCommaHandling: false
|
|
1821
|
-
});
|
|
1822
|
-
// Insert the child directly into the Document (as a Block item)
|
|
1823
|
-
parent.items.splice(index, 0, child);
|
|
1824
|
-
return result;
|
|
1825
|
-
}
|
|
1826
|
-
function remove(root, parent, node) {
|
|
1827
|
-
// Remove an element from the parent's items
|
|
1828
|
-
// (supports Document, Table, TableArray, InlineTable, and InlineArray
|
|
1829
|
-
//
|
|
1830
|
-
// X
|
|
1831
|
-
// [ 1, 2, 3 ]
|
|
1832
|
-
// ^-^
|
|
1833
|
-
// -> Remove element 2 and apply 0,-3 offset to 1
|
|
1834
|
-
//
|
|
1835
|
-
// [table]
|
|
1836
|
-
// a = 1
|
|
1837
|
-
// b = 2 # X
|
|
1838
|
-
// c = 3
|
|
1839
|
-
// -> Remove element 2 and apply -1,0 offset to 1
|
|
1840
|
-
if (!hasItems(parent)) {
|
|
1841
|
-
throw new Error(`Unsupported parent type "${parent.type}" for remove`);
|
|
1842
|
-
}
|
|
1843
|
-
let index = parent.items.indexOf(node);
|
|
1844
|
-
if (index < 0) {
|
|
1845
|
-
// Try again, looking at child items for nodes like InlineArrayItem
|
|
1846
|
-
index = parent.items.findIndex(item => hasItem(item) && item.item === node);
|
|
1847
|
-
if (index < 0) {
|
|
1848
|
-
throw new Error('Could not find node in parent for removal');
|
|
1849
|
-
}
|
|
1850
|
-
node = parent.items[index];
|
|
1851
|
-
}
|
|
1852
|
-
const previous = parent.items[index - 1];
|
|
1853
|
-
let next = parent.items[index + 1];
|
|
1854
|
-
// Remove node
|
|
1855
|
-
parent.items.splice(index, 1);
|
|
1856
|
-
let removed_span = getSpan(node.loc);
|
|
1857
|
-
// Remove an associated comment that appears on the same line
|
|
1858
|
-
//
|
|
1859
|
-
// [table]
|
|
1860
|
-
// a = 1
|
|
1861
|
-
// b = 2 # remove this too
|
|
1862
|
-
// c = 3
|
|
1863
|
-
//
|
|
1864
|
-
// TODO InlineTable - this only applies to comments in Table/TableArray
|
|
1865
|
-
if (next && isComment(next) && next.loc.start.line === node.loc.end.line) {
|
|
1866
|
-
// Add comment to removed
|
|
1867
|
-
removed_span = getSpan({ start: node.loc.start, end: next.loc.end });
|
|
1868
|
-
// Shift to next item
|
|
1869
|
-
// (use same index since node has already been removed)
|
|
1870
|
-
next = parent.items[index + 1];
|
|
1871
|
-
// Remove comment
|
|
1872
|
-
parent.items.splice(index, 1);
|
|
1873
|
-
}
|
|
1874
|
-
// For inline tables and arrays, check whether the line should be kept
|
|
1875
|
-
const is_inline = previous && isInlineItem(previous) || next && isInlineItem(next);
|
|
1876
|
-
const previous_on_same_line = previous && previous.loc.end.line === node.loc.start.line;
|
|
1877
|
-
const next_on_sameLine = next && next.loc.start.line === node.loc.end.line;
|
|
1878
|
-
const keep_line = is_inline && (previous_on_same_line || next_on_sameLine);
|
|
1879
|
-
const offset = {
|
|
1880
|
-
lines: -(removed_span.lines - (keep_line ? 1 : 0)),
|
|
1881
|
-
columns: -removed_span.columns
|
|
1882
|
-
};
|
|
1883
|
-
// If there is nothing left, don't perform any offsets
|
|
1884
|
-
if (previous === undefined && next === undefined) {
|
|
1885
|
-
offset.lines = 0;
|
|
1886
|
-
offset.columns = 0;
|
|
1887
|
-
}
|
|
1888
|
-
// Offset for comma and remove comma that appear in front of the element (if-needed)
|
|
1889
|
-
if (is_inline && previous_on_same_line) {
|
|
1890
|
-
offset.columns -= 2;
|
|
1891
|
-
}
|
|
1892
|
-
// If first element in array/inline-table, remove space for comma and space after element
|
|
1893
|
-
if (is_inline && !previous && next) {
|
|
1894
|
-
offset.columns -= 2;
|
|
1895
|
-
}
|
|
1896
|
-
if (is_inline && previous && !next) {
|
|
1897
|
-
// When removing the last element, preserve trailing comma preference
|
|
1898
|
-
// If the removed element had a trailing comma, transfer it to the new last element
|
|
1899
|
-
const removedHadTrailingComma = node.comma;
|
|
1900
|
-
if (removedHadTrailingComma) {
|
|
1901
|
-
previous.comma = true;
|
|
1902
|
-
}
|
|
1903
|
-
else {
|
|
1904
|
-
previous.comma = false;
|
|
1905
|
-
}
|
|
1906
|
-
}
|
|
1907
|
-
// Apply offsets after preceding node or before children of parent node
|
|
1908
|
-
const target = previous || parent;
|
|
1909
|
-
const target_offsets = previous ? getExitOffsets(root) : getEnterOffsets(root);
|
|
1910
|
-
const node_offsets = getExitOffsets(root);
|
|
1911
|
-
const previous_offset = target_offsets.get(target);
|
|
1912
|
-
if (previous_offset) {
|
|
1913
|
-
offset.lines += previous_offset.lines;
|
|
1914
|
-
offset.columns += previous_offset.columns;
|
|
1915
|
-
}
|
|
1916
|
-
const removed_offset = node_offsets.get(node);
|
|
1917
|
-
if (removed_offset) {
|
|
1918
|
-
offset.lines += removed_offset.lines;
|
|
1919
|
-
offset.columns += removed_offset.columns;
|
|
1920
|
-
}
|
|
1921
|
-
target_offsets.set(target, offset);
|
|
1922
|
-
}
|
|
1923
|
-
function applyBracketSpacing(root, node, bracket_spacing = true) {
|
|
1924
|
-
// Can only add bracket spacing currently
|
|
1925
|
-
if (!bracket_spacing)
|
|
1926
|
-
return;
|
|
1927
|
-
if (!node.items.length)
|
|
1928
|
-
return;
|
|
1929
|
-
// Apply enter to node so that items are affected
|
|
1930
|
-
addOffset({ lines: 0, columns: 1 }, getEnterOffsets(root), node);
|
|
1931
|
-
// Apply exit to last node in items
|
|
1932
|
-
const last_item = last(node.items);
|
|
1933
|
-
addOffset({ lines: 0, columns: 1 }, getExitOffsets(root), last_item);
|
|
1934
|
-
}
|
|
1935
|
-
function applyTrailingComma(root, node, trailing_commas = false) {
|
|
1936
|
-
// Can only add trailing comma currently
|
|
1937
|
-
if (!trailing_commas)
|
|
1938
|
-
return;
|
|
1939
|
-
if (!node.items.length)
|
|
1940
|
-
return;
|
|
1941
|
-
const last_item = last(node.items);
|
|
1942
|
-
last_item.comma = true;
|
|
1943
|
-
addOffset({ lines: 0, columns: 1 }, getExitOffsets(root), last_item);
|
|
1944
|
-
}
|
|
1945
|
-
/**
|
|
1946
|
-
* Applies all accumulated write offsets (enter and exit) to the given AST node.
|
|
1947
|
-
* This function adjusts the start and end locations of each node in the tree based on
|
|
1948
|
-
* the offsets stored in the `enter` and `exit` maps. It ensures that the tree's location
|
|
1949
|
-
* data is consistent after modifications.
|
|
1950
|
-
*
|
|
1951
|
-
* @param root - The root node of the AST tree to which the write offsets will be applied.
|
|
1952
|
-
*/
|
|
1953
|
-
function applyWrites(root) {
|
|
1954
|
-
const enter = getEnterOffsets(root);
|
|
1955
|
-
const exit = getExitOffsets(root);
|
|
1956
|
-
const offset = {
|
|
1957
|
-
lines: 0,
|
|
1958
|
-
columns: {}
|
|
1959
|
-
};
|
|
1960
|
-
function shiftStart(node) {
|
|
1961
|
-
const lineOffset = offset.lines;
|
|
1962
|
-
node.loc.start.line += lineOffset;
|
|
1963
|
-
const columnOffset = offset.columns[node.loc.start.line] || 0;
|
|
1964
|
-
node.loc.start.column += columnOffset;
|
|
1965
|
-
const entering = enter.get(node);
|
|
1966
|
-
if (entering) {
|
|
1967
|
-
offset.lines += entering.lines;
|
|
1968
|
-
offset.columns[node.loc.start.line] =
|
|
1969
|
-
(offset.columns[node.loc.start.line] || 0) + entering.columns;
|
|
1970
|
-
}
|
|
1971
|
-
}
|
|
1972
|
-
function shiftEnd(node) {
|
|
1973
|
-
const lineOffset = offset.lines;
|
|
1974
|
-
node.loc.end.line += lineOffset;
|
|
1975
|
-
const columnOffset = offset.columns[node.loc.end.line] || 0;
|
|
1976
|
-
node.loc.end.column += columnOffset;
|
|
1977
|
-
const exiting = exit.get(node);
|
|
1978
|
-
if (exiting) {
|
|
1979
|
-
offset.lines += exiting.lines;
|
|
1980
|
-
offset.columns[node.loc.end.line] =
|
|
1981
|
-
(offset.columns[node.loc.end.line] || 0) + exiting.columns;
|
|
1982
|
-
}
|
|
1983
|
-
}
|
|
1984
|
-
const shiftLocation = {
|
|
1985
|
-
enter: shiftStart,
|
|
1986
|
-
exit: shiftEnd
|
|
1987
|
-
};
|
|
1988
|
-
traverse(root, {
|
|
1989
|
-
[NodeType.Document]: shiftLocation,
|
|
1990
|
-
[NodeType.Table]: shiftLocation,
|
|
1991
|
-
[NodeType.TableArray]: shiftLocation,
|
|
1992
|
-
[NodeType.InlineTable]: shiftLocation,
|
|
1993
|
-
[NodeType.InlineArray]: shiftLocation,
|
|
1994
|
-
[NodeType.InlineItem]: shiftLocation,
|
|
1995
|
-
[NodeType.TableKey]: shiftLocation,
|
|
1996
|
-
[NodeType.TableArrayKey]: shiftLocation,
|
|
1997
|
-
[NodeType.KeyValue]: {
|
|
1998
|
-
enter(node) {
|
|
1999
|
-
const start_line = node.loc.start.line + offset.lines;
|
|
2000
|
-
const key_offset = exit.get(node.key);
|
|
2001
|
-
node.equals += (offset.columns[start_line] || 0) + (key_offset ? key_offset.columns : 0);
|
|
2002
|
-
shiftStart(node);
|
|
2003
|
-
},
|
|
2004
|
-
exit: shiftEnd
|
|
2005
|
-
},
|
|
2006
|
-
[NodeType.Key]: shiftLocation,
|
|
2007
|
-
[NodeType.String]: shiftLocation,
|
|
2008
|
-
[NodeType.Integer]: shiftLocation,
|
|
2009
|
-
[NodeType.Float]: shiftLocation,
|
|
2010
|
-
[NodeType.Boolean]: shiftLocation,
|
|
2011
|
-
[NodeType.DateTime]: shiftLocation,
|
|
2012
|
-
[NodeType.Comment]: shiftLocation
|
|
2013
|
-
});
|
|
2014
|
-
enter_offsets.delete(root);
|
|
2015
|
-
exit_offsets.delete(root);
|
|
2016
|
-
}
|
|
2017
|
-
function shiftNode(node, span, options = {}) {
|
|
2018
|
-
const { first_line_only = false } = options;
|
|
2019
|
-
const start_line = node.loc.start.line;
|
|
2020
|
-
const { lines, columns } = span;
|
|
2021
|
-
const move = (node) => {
|
|
2022
|
-
if (!first_line_only || node.loc.start.line === start_line) {
|
|
2023
|
-
node.loc.start.column += columns;
|
|
2024
|
-
node.loc.end.column += columns;
|
|
2025
|
-
}
|
|
2026
|
-
node.loc.start.line += lines;
|
|
2027
|
-
node.loc.end.line += lines;
|
|
2028
|
-
};
|
|
2029
|
-
traverse(node, {
|
|
2030
|
-
[NodeType.Table]: move,
|
|
2031
|
-
[NodeType.TableKey]: move,
|
|
2032
|
-
[NodeType.TableArray]: move,
|
|
2033
|
-
[NodeType.TableArrayKey]: move,
|
|
2034
|
-
[NodeType.KeyValue](node) {
|
|
2035
|
-
move(node);
|
|
2036
|
-
node.equals += columns;
|
|
2037
|
-
},
|
|
2038
|
-
[NodeType.Key]: move,
|
|
2039
|
-
[NodeType.String]: move,
|
|
2040
|
-
[NodeType.Integer]: move,
|
|
2041
|
-
[NodeType.Float]: move,
|
|
2042
|
-
[NodeType.Boolean]: move,
|
|
2043
|
-
[NodeType.DateTime]: move,
|
|
2044
|
-
[NodeType.InlineArray]: move,
|
|
2045
|
-
[NodeType.InlineItem]: move,
|
|
2046
|
-
[NodeType.InlineTable]: move,
|
|
2047
|
-
[NodeType.Comment]: move
|
|
2048
|
-
});
|
|
2049
|
-
return node;
|
|
2050
|
-
}
|
|
2051
|
-
function perLine(array) {
|
|
2052
|
-
if (!array.items.length)
|
|
2053
|
-
return false;
|
|
2054
|
-
const span = getSpan(array.loc);
|
|
2055
|
-
return span.lines > array.items.length;
|
|
2056
|
-
}
|
|
2057
|
-
function addOffset(offset, offsets, node, from) {
|
|
2058
|
-
const previous_offset = offsets.get(from || node);
|
|
2059
|
-
if (previous_offset) {
|
|
2060
|
-
offset.lines += previous_offset.lines;
|
|
2061
|
-
offset.columns += previous_offset.columns;
|
|
2062
|
-
}
|
|
2063
|
-
offsets.set(node, offset);
|
|
2064
|
-
}
|
|
2065
|
-
|
|
2066
|
-
/**
|
|
2067
|
-
* Generates a new TOML document node.
|
|
2068
|
-
*
|
|
2069
|
-
* @returns A new Document node.
|
|
2070
|
-
*/
|
|
2071
|
-
function generateDocument() {
|
|
2072
|
-
return {
|
|
2073
|
-
type: NodeType.Document,
|
|
2074
|
-
loc: { start: zero(), end: zero() },
|
|
2075
|
-
items: []
|
|
2076
|
-
};
|
|
2077
|
-
}
|
|
2078
|
-
function generateTable(key) {
|
|
2079
|
-
const table_key = generateTableKey(key);
|
|
2080
|
-
return {
|
|
2081
|
-
type: NodeType.Table,
|
|
2082
|
-
loc: cloneLocation(table_key.loc),
|
|
2083
|
-
key: table_key,
|
|
2084
|
-
items: []
|
|
2085
|
-
};
|
|
2086
|
-
}
|
|
2087
|
-
function generateTableKey(key) {
|
|
2088
|
-
const raw = keyValueToRaw(key);
|
|
2089
|
-
return {
|
|
2090
|
-
type: NodeType.TableKey,
|
|
2091
|
-
loc: {
|
|
2092
|
-
start: zero(),
|
|
2093
|
-
end: { line: 1, column: raw.length + 2 }
|
|
2094
|
-
},
|
|
2095
|
-
item: {
|
|
2096
|
-
type: NodeType.Key,
|
|
2097
|
-
loc: {
|
|
2098
|
-
start: { line: 1, column: 1 },
|
|
2099
|
-
end: { line: 1, column: raw.length + 1 }
|
|
2100
|
-
},
|
|
2101
|
-
value: key,
|
|
2102
|
-
raw
|
|
2103
|
-
}
|
|
2104
|
-
};
|
|
2105
|
-
}
|
|
2106
|
-
function generateTableArray(key) {
|
|
2107
|
-
const table_array_key = generateTableArrayKey(key);
|
|
2108
|
-
return {
|
|
2109
|
-
type: NodeType.TableArray,
|
|
2110
|
-
loc: cloneLocation(table_array_key.loc),
|
|
2111
|
-
key: table_array_key,
|
|
2112
|
-
items: []
|
|
2113
|
-
};
|
|
2114
|
-
}
|
|
2115
|
-
function generateTableArrayKey(key) {
|
|
2116
|
-
const raw = keyValueToRaw(key);
|
|
2117
|
-
return {
|
|
2118
|
-
type: NodeType.TableArrayKey,
|
|
2119
|
-
loc: {
|
|
2120
|
-
start: zero(),
|
|
2121
|
-
end: { line: 1, column: raw.length + 4 }
|
|
2122
|
-
},
|
|
2123
|
-
item: {
|
|
2124
|
-
type: NodeType.Key,
|
|
2125
|
-
loc: {
|
|
2126
|
-
start: { line: 1, column: 2 },
|
|
2127
|
-
end: { line: 1, column: raw.length + 2 }
|
|
2128
|
-
},
|
|
2129
|
-
value: key,
|
|
2130
|
-
raw
|
|
2131
|
-
}
|
|
2132
|
-
};
|
|
2133
|
-
}
|
|
2134
|
-
function generateKeyValue(key, value) {
|
|
2135
|
-
const key_node = generateKey(key);
|
|
2136
|
-
const { column } = key_node.loc.end;
|
|
2137
|
-
const equals = column + 1;
|
|
2138
|
-
shiftNode(value, { lines: 0, columns: column + 3 - value.loc.start.column }, { first_line_only: true });
|
|
2139
|
-
return {
|
|
2140
|
-
type: NodeType.KeyValue,
|
|
2141
|
-
loc: {
|
|
2142
|
-
start: clonePosition(key_node.loc.start),
|
|
2143
|
-
end: clonePosition(value.loc.end)
|
|
2144
|
-
},
|
|
2145
|
-
key: key_node,
|
|
2146
|
-
equals,
|
|
2147
|
-
value
|
|
2148
|
-
};
|
|
2149
|
-
}
|
|
2150
|
-
const IS_BARE_KEY = /^[\w-]+$/;
|
|
2151
|
-
function keyValueToRaw(value) {
|
|
2152
|
-
return value.map(part => (IS_BARE_KEY.test(part) ? part : JSON.stringify(part))).join('.');
|
|
2153
|
-
}
|
|
2154
|
-
function generateKey(value) {
|
|
2155
|
-
const raw = keyValueToRaw(value);
|
|
2156
|
-
return {
|
|
2157
|
-
type: NodeType.Key,
|
|
2158
|
-
loc: { start: zero(), end: { line: 1, column: raw.length } },
|
|
2159
|
-
raw,
|
|
2160
|
-
value
|
|
2161
|
-
};
|
|
2162
|
-
}
|
|
2163
|
-
/**
|
|
2164
|
-
* Generates a new String node, preserving multiline format if existingRaw is provided.
|
|
2165
|
-
*
|
|
2166
|
-
* @param value - The string value.
|
|
2167
|
-
* @param existingRaw - The existing raw string to determine multiline format (optional).
|
|
2168
|
-
* @returns A new String node.
|
|
2169
|
-
*/
|
|
2170
|
-
function generateString(value, existingRaw) {
|
|
2171
|
-
let raw;
|
|
2172
|
-
if (existingRaw && isMultilineString(existingRaw)) {
|
|
2173
|
-
// Preserve multiline format
|
|
2174
|
-
let isLiteral = existingRaw.startsWith("'''");
|
|
2175
|
-
// Literal strings cannot contain ''' - convert to basic string if needed
|
|
2176
|
-
if (isLiteral && value.includes("'''")) {
|
|
2177
|
-
isLiteral = false;
|
|
2178
|
-
}
|
|
2179
|
-
const delimiter = isLiteral ? "'''" : '"""';
|
|
2180
|
-
// Detect newline character from existing raw
|
|
2181
|
-
const newlineChar = existingRaw.includes('\r\n') ? '\r\n' : '\n';
|
|
2182
|
-
const hasLeadingNewline = existingRaw.startsWith(`${delimiter}${newlineChar}`) ||
|
|
2183
|
-
((existingRaw.startsWith("'''\n") || existingRaw.startsWith("'''\r\n")) && !isLiteral);
|
|
2184
|
-
let escaped;
|
|
2185
|
-
if (isLiteral) {
|
|
2186
|
-
// Literal strings: no escaping needed (we already checked for ''' above)
|
|
2187
|
-
escaped = value;
|
|
2188
|
-
}
|
|
2189
|
-
else {
|
|
2190
|
-
// Basic multiline strings: escape backslashes, control characters, and triple quotes
|
|
2191
|
-
escaped = value
|
|
2192
|
-
.replace(/\\/g, '\\\\') // Escape backslashes first
|
|
2193
|
-
.replace(/\x08/g, '\\b') // Backspace (U+0008)
|
|
2194
|
-
.replace(/\f/g, '\\f') // Form feed (U+000C)
|
|
2195
|
-
.replace(/\t/g, '\\t') // Tab (U+0009)
|
|
2196
|
-
.replace(/[\x00-\x07\x0B\x0E-\x1F\x7F]/g, (char) => {
|
|
2197
|
-
// Escape other control characters
|
|
2198
|
-
const code = char.charCodeAt(0);
|
|
2199
|
-
return '\\u' + code.toString(16).padStart(4, '0').toUpperCase();
|
|
2200
|
-
})
|
|
2201
|
-
// Escape triple quotes safely: two literal quotes + escaped quote
|
|
2202
|
-
.replace(/"""/g, '""\\\"');
|
|
2203
|
-
}
|
|
2204
|
-
// Format with or without leading newline based on original
|
|
2205
|
-
if (hasLeadingNewline) {
|
|
2206
|
-
raw = `${delimiter}${newlineChar}${escaped}${delimiter}`;
|
|
2207
|
-
}
|
|
2208
|
-
else {
|
|
2209
|
-
raw = `${delimiter}${escaped}${delimiter}`;
|
|
2210
|
-
}
|
|
2211
|
-
}
|
|
2212
|
-
else {
|
|
2213
|
-
raw = JSON.stringify(value);
|
|
2214
|
-
}
|
|
2215
|
-
// Calculate proper end location for multiline strings
|
|
2216
|
-
let endLocation;
|
|
2217
|
-
if (raw.includes('\r\n') || (raw.includes('\n') && !raw.includes('\r\n'))) {
|
|
2218
|
-
const newlineChar = raw.includes('\r\n') ? '\r\n' : '\n';
|
|
2219
|
-
const lineCount = (raw.match(new RegExp(newlineChar === '\r\n' ? '\\r\\n' : '\\n', 'g')) || []).length;
|
|
2220
|
-
if (lineCount > 0) {
|
|
2221
|
-
endLocation = {
|
|
2222
|
-
line: 1 + lineCount,
|
|
2223
|
-
column: 3 // length of delimiter (""" or ''')
|
|
2224
|
-
};
|
|
2225
|
-
}
|
|
2226
|
-
else {
|
|
2227
|
-
endLocation = { line: 1, column: raw.length };
|
|
2228
|
-
}
|
|
2229
|
-
}
|
|
2230
|
-
else {
|
|
2231
|
-
endLocation = { line: 1, column: raw.length };
|
|
2232
|
-
}
|
|
2233
|
-
return {
|
|
2234
|
-
type: NodeType.String,
|
|
2235
|
-
loc: { start: zero(), end: endLocation },
|
|
2236
|
-
raw,
|
|
2237
|
-
value
|
|
2238
|
-
};
|
|
2239
|
-
}
|
|
2240
|
-
/**
|
|
2241
|
-
* Generates a new Integer node.
|
|
2242
|
-
*
|
|
2243
|
-
* @param value - The integer value.
|
|
2244
|
-
* @returns A new Integer node.
|
|
2245
|
-
*/
|
|
2246
|
-
function generateInteger(value) {
|
|
2247
|
-
const raw = value.toString();
|
|
2248
|
-
return {
|
|
2249
|
-
type: NodeType.Integer,
|
|
2250
|
-
loc: { start: zero(), end: { line: 1, column: raw.length } },
|
|
2251
|
-
raw,
|
|
2252
|
-
value
|
|
2253
|
-
};
|
|
2254
|
-
}
|
|
2255
|
-
function generateFloat(value) {
|
|
2256
|
-
let raw;
|
|
2257
|
-
if (value === Infinity) {
|
|
2258
|
-
raw = 'inf';
|
|
2259
|
-
}
|
|
2260
|
-
else if (value === -Infinity) {
|
|
2261
|
-
raw = '-inf';
|
|
2262
|
-
}
|
|
2263
|
-
else if (Number.isNaN(value)) {
|
|
2264
|
-
raw = 'nan';
|
|
2265
|
-
}
|
|
2266
|
-
else if (Object.is(value, -0)) {
|
|
2267
|
-
raw = '-0.0';
|
|
2268
|
-
}
|
|
2269
|
-
else {
|
|
2270
|
-
raw = value.toString();
|
|
2271
|
-
}
|
|
2272
|
-
return {
|
|
2273
|
-
type: NodeType.Float,
|
|
2274
|
-
loc: { start: zero(), end: { line: 1, column: raw.length } },
|
|
2275
|
-
raw,
|
|
2276
|
-
value
|
|
2277
|
-
};
|
|
2278
|
-
}
|
|
2279
|
-
function generateBoolean(value) {
|
|
2280
|
-
return {
|
|
2281
|
-
type: NodeType.Boolean,
|
|
2282
|
-
loc: { start: zero(), end: { line: 1, column: value ? 4 : 5 } },
|
|
2283
|
-
value
|
|
2284
|
-
};
|
|
2285
|
-
}
|
|
2286
|
-
function generateDateTime(value, format) {
|
|
2287
|
-
// Convert Date objects with zero time components to LocalDate
|
|
2288
|
-
// so they are serialized as date-only in TOML
|
|
2289
|
-
if (format.truncateZeroTimeInDates &&
|
|
2290
|
-
value.getUTCHours() === 0 &&
|
|
2291
|
-
value.getUTCMinutes() === 0 &&
|
|
2292
|
-
value.getUTCSeconds() === 0 &&
|
|
2293
|
-
value.getUTCMilliseconds() === 0) {
|
|
2294
|
-
value = new LocalDate(value.toISOString().split('T')[0]);
|
|
2295
|
-
}
|
|
2296
|
-
// Custom date classes have their own toISOString() implementations
|
|
2297
|
-
// that return the properly formatted strings for each TOML date/time type
|
|
2298
|
-
const raw = value.toISOString();
|
|
2299
|
-
return {
|
|
2300
|
-
type: NodeType.DateTime,
|
|
2301
|
-
loc: { start: zero(), end: { line: 1, column: raw.length } },
|
|
2302
|
-
raw,
|
|
2303
|
-
value
|
|
2304
|
-
};
|
|
2305
|
-
}
|
|
2306
|
-
function generateInlineArray() {
|
|
2307
|
-
return {
|
|
2308
|
-
type: NodeType.InlineArray,
|
|
2309
|
-
loc: { start: zero(), end: { line: 1, column: 2 } },
|
|
2310
|
-
items: []
|
|
2311
|
-
};
|
|
2312
|
-
}
|
|
2313
|
-
function generateInlineItem(item) {
|
|
2314
|
-
return {
|
|
2315
|
-
type: NodeType.InlineItem,
|
|
2316
|
-
loc: cloneLocation(item.loc),
|
|
2317
|
-
item,
|
|
2318
|
-
comma: false
|
|
2319
|
-
};
|
|
2320
|
-
}
|
|
2321
|
-
function generateInlineTable() {
|
|
2322
|
-
return {
|
|
2323
|
-
type: NodeType.InlineTable,
|
|
2324
|
-
loc: { start: zero(), end: { line: 1, column: 2 } },
|
|
2325
|
-
items: []
|
|
2326
|
-
};
|
|
2327
|
-
}
|
|
2328
|
-
|
|
2329
|
-
// Default formatting values
|
|
2330
|
-
const DEFAULT_NEWLINE = '\n';
|
|
2331
|
-
const DEFAULT_TRAILING_NEWLINE = 1;
|
|
2332
|
-
const DEFAULT_TRAILING_COMMA = false;
|
|
2333
|
-
const DEFAULT_BRACKET_SPACING = true;
|
|
2334
|
-
const DEFAULT_INLINE_TABLE_START = 1;
|
|
2335
|
-
const DEFAULT_TRUNCATE_ZERO_TIME_IN_DATES = false;
|
|
2336
|
-
const DEFAULT_USE_TABS_FOR_INDENTATION = false;
|
|
2337
|
-
// Detects if trailing commas are used in the existing TOML by examining the AST
|
|
2338
|
-
// Returns true if trailing commas are used, false if not or comma-separated structures found (ie. default to false)
|
|
2339
|
-
function detectTrailingComma(ast) {
|
|
2340
|
-
// Convert iterable to array and look for the first inline array or inline table to determine trailing comma preference
|
|
2341
|
-
const items = Array.from(ast);
|
|
2342
|
-
for (const item of items) {
|
|
2343
|
-
const result = findTrailingCommaInNode(item);
|
|
2344
|
-
if (result !== null) {
|
|
2345
|
-
return result;
|
|
2346
|
-
}
|
|
2347
|
-
}
|
|
2348
|
-
// Return default if no comma-separated structures are found
|
|
2349
|
-
return DEFAULT_TRAILING_COMMA;
|
|
2350
|
-
}
|
|
2351
|
-
// Detects if bracket spacing is used in inline arrays and tables by examining the raw string
|
|
2352
|
-
// Returns true if bracket spacing is found, false if not or no bracket structures found (default to true)
|
|
2353
|
-
function detectBracketSpacing(tomlString, ast) {
|
|
2354
|
-
// Convert iterable to array and look for inline arrays and tables
|
|
2355
|
-
const items = Array.from(ast);
|
|
2356
|
-
for (const item of items) {
|
|
2357
|
-
const result = findBracketSpacingInNode(item, tomlString);
|
|
2358
|
-
if (result !== null) {
|
|
2359
|
-
return result;
|
|
2360
|
-
}
|
|
2361
|
-
}
|
|
2362
|
-
// Return default if no bracket structures are found
|
|
2363
|
-
return DEFAULT_BRACKET_SPACING;
|
|
2364
|
-
}
|
|
2365
|
-
// Helper function to recursively search for bracket spacing in a node
|
|
2366
|
-
function findBracketSpacingInNode(node, tomlString) {
|
|
2367
|
-
if (!node || typeof node !== 'object') {
|
|
2368
|
-
return null;
|
|
2369
|
-
}
|
|
2370
|
-
// Check if this is an InlineArray or InlineTable
|
|
2371
|
-
if ((node.type === 'InlineArray' || node.type === 'InlineTable') && node.loc) {
|
|
2372
|
-
const bracketSpacing = checkBracketSpacingInLocation(node.loc, tomlString);
|
|
2373
|
-
if (bracketSpacing !== null) {
|
|
2374
|
-
return bracketSpacing;
|
|
2375
|
-
}
|
|
2376
|
-
}
|
|
2377
|
-
// Recursively check nested structures
|
|
2378
|
-
if (node.items && Array.isArray(node.items)) {
|
|
2379
|
-
for (const child of node.items) {
|
|
2380
|
-
const result = findBracketSpacingInNode(child, tomlString);
|
|
2381
|
-
if (result !== null) {
|
|
2382
|
-
return result;
|
|
2383
|
-
}
|
|
2384
|
-
// Also check nested item if it exists
|
|
2385
|
-
if (child.item) {
|
|
2386
|
-
const nestedResult = findBracketSpacingInNode(child.item, tomlString);
|
|
2387
|
-
if (nestedResult !== null) {
|
|
2388
|
-
return nestedResult;
|
|
2389
|
-
}
|
|
2390
|
-
}
|
|
2391
|
-
}
|
|
2392
|
-
}
|
|
2393
|
-
// Check other properties that might contain nodes
|
|
2394
|
-
for (const prop of ['value', 'key', 'item']) {
|
|
2395
|
-
if (node[prop]) {
|
|
2396
|
-
const result = findBracketSpacingInNode(node[prop], tomlString);
|
|
2397
|
-
if (result !== null) {
|
|
2398
|
-
return result;
|
|
2399
|
-
}
|
|
2400
|
-
}
|
|
2401
|
-
}
|
|
2402
|
-
return null;
|
|
2403
|
-
}
|
|
2404
|
-
// Helper function to check bracket spacing in a specific location
|
|
2405
|
-
function checkBracketSpacingInLocation(loc, tomlString) {
|
|
2406
|
-
var _a;
|
|
2407
|
-
if (!loc || !loc.start || !loc.end) {
|
|
2408
|
-
return null;
|
|
2409
|
-
}
|
|
2410
|
-
// Extract the raw text for this location
|
|
2411
|
-
const lines = tomlString.split(/\r?\n/);
|
|
2412
|
-
const startLine = loc.start.line - 1; // Convert to 0-based
|
|
2413
|
-
const endLine = loc.end.line - 1;
|
|
2414
|
-
const startCol = loc.start.column;
|
|
2415
|
-
const endCol = loc.end.column;
|
|
2416
|
-
let rawText = '';
|
|
2417
|
-
if (startLine === endLine) {
|
|
2418
|
-
rawText = ((_a = lines[startLine]) === null || _a === void 0 ? void 0 : _a.substring(startCol, endCol + 1)) || '';
|
|
2419
|
-
}
|
|
2420
|
-
else {
|
|
2421
|
-
// Multi-line case
|
|
2422
|
-
if (lines[startLine]) {
|
|
2423
|
-
rawText += lines[startLine].substring(startCol);
|
|
2424
|
-
}
|
|
2425
|
-
for (let i = startLine + 1; i < endLine; i++) {
|
|
2426
|
-
rawText += '\n' + (lines[i] || '');
|
|
2427
|
-
}
|
|
2428
|
-
if (lines[endLine]) {
|
|
2429
|
-
rawText += '\n' + lines[endLine].substring(0, endCol + 1);
|
|
2430
|
-
}
|
|
2431
|
-
}
|
|
2432
|
-
// Check for bracket spacing patterns
|
|
2433
|
-
// For arrays: [ elements ] vs [elements]
|
|
2434
|
-
// For tables: { elements } vs {elements}
|
|
2435
|
-
const arrayMatch = rawText.match(/^\[(\s*)/);
|
|
2436
|
-
const tableMatch = rawText.match(/^\{(\s*)/);
|
|
2437
|
-
if (arrayMatch) {
|
|
2438
|
-
// Check if there's a space after the opening bracket
|
|
2439
|
-
return arrayMatch[1].length > 0;
|
|
2440
|
-
}
|
|
2441
|
-
if (tableMatch) {
|
|
2442
|
-
// Check if there's a space after the opening brace
|
|
2443
|
-
return tableMatch[1].length > 0;
|
|
2444
|
-
}
|
|
2445
|
-
return null;
|
|
2446
|
-
}
|
|
2447
|
-
// Helper function to recursively search for comma usage in a node
|
|
2448
|
-
function findTrailingCommaInNode(node) {
|
|
2449
|
-
if (!node || typeof node !== 'object') {
|
|
2450
|
-
return null;
|
|
2451
|
-
}
|
|
2452
|
-
// Check if this is an InlineArray
|
|
2453
|
-
if (node.type === 'InlineArray' && node.items && Array.isArray(node.items)) {
|
|
2454
|
-
return checkTrailingCommaInItems(node.items);
|
|
2455
|
-
}
|
|
2456
|
-
// Check if this is an InlineTable
|
|
2457
|
-
if (node.type === 'InlineTable' && node.items && Array.isArray(node.items)) {
|
|
2458
|
-
return checkTrailingCommaInItems(node.items);
|
|
2459
|
-
}
|
|
2460
|
-
// Check if this is a KeyValue with a value that might contain arrays/tables
|
|
2461
|
-
if (node.type === 'KeyValue' && node.value) {
|
|
2462
|
-
return findTrailingCommaInNode(node.value);
|
|
2463
|
-
}
|
|
2464
|
-
// For other node types, recursively check any items array
|
|
2465
|
-
if (node.items && Array.isArray(node.items)) {
|
|
2466
|
-
for (const item of node.items) {
|
|
2467
|
-
const result = findTrailingCommaInNode(item);
|
|
2468
|
-
if (result !== null) {
|
|
2469
|
-
return result;
|
|
2470
|
-
}
|
|
2471
|
-
}
|
|
2472
|
-
}
|
|
2473
|
-
return null;
|
|
2474
|
-
}
|
|
2475
|
-
// Check trailing comma usage in an array of inline items
|
|
2476
|
-
function checkTrailingCommaInItems(items) {
|
|
2477
|
-
if (items.length === 0) {
|
|
2478
|
-
return null;
|
|
2479
|
-
}
|
|
2480
|
-
// Check the last item to see if it has a trailing comma
|
|
2481
|
-
const lastItem = items[items.length - 1];
|
|
2482
|
-
if (lastItem && typeof lastItem === 'object' && 'comma' in lastItem) {
|
|
2483
|
-
return lastItem.comma === true;
|
|
2484
|
-
}
|
|
2485
|
-
return false;
|
|
2486
|
-
}
|
|
2487
|
-
// Helper function to detect if an InlineArray originally had trailing commas
|
|
2488
|
-
function arrayHadTrailingCommas(node) {
|
|
2489
|
-
if (!isInlineArray(node))
|
|
2490
|
-
return false;
|
|
2491
|
-
if (node.items.length === 0)
|
|
2492
|
-
return false;
|
|
2493
|
-
// Check if the last item has a trailing comma
|
|
2494
|
-
const lastItem = node.items[node.items.length - 1];
|
|
2495
|
-
return lastItem.comma === true;
|
|
2496
|
-
}
|
|
2497
|
-
// Helper function to detect if an InlineTable originally had trailing commas
|
|
2498
|
-
function tableHadTrailingCommas(node) {
|
|
2499
|
-
if (!isInlineTable(node))
|
|
2500
|
-
return false;
|
|
2501
|
-
if (node.items.length === 0)
|
|
2502
|
-
return false;
|
|
2503
|
-
// Check if the last item has a trailing comma
|
|
2504
|
-
const lastItem = node.items[node.items.length - 1];
|
|
2505
|
-
return lastItem.comma === true;
|
|
2506
|
-
}
|
|
2507
|
-
// Returns the detected newline (\n or \r\n) from a string, defaulting to \n
|
|
2508
|
-
function detectNewline(str) {
|
|
2509
|
-
const lfIndex = str.indexOf('\n');
|
|
2510
|
-
if (lfIndex > 0 && str.substring(lfIndex - 1, lfIndex) === '\r') {
|
|
2511
|
-
return '\r\n';
|
|
2512
|
-
}
|
|
2513
|
-
return '\n';
|
|
2514
|
-
}
|
|
2515
|
-
// Counts consecutive trailing newlines at the end of a string
|
|
2516
|
-
function countTrailingNewlines(str, newlineChar) {
|
|
2517
|
-
let count = 0;
|
|
2518
|
-
let pos = str.length;
|
|
2519
|
-
while (pos >= newlineChar.length) {
|
|
2520
|
-
if (str.substring(pos - newlineChar.length, pos) === newlineChar) {
|
|
2521
|
-
count++;
|
|
2522
|
-
pos -= newlineChar.length;
|
|
2523
|
-
}
|
|
2524
|
-
else {
|
|
2525
|
-
break;
|
|
2526
|
-
}
|
|
2527
|
-
}
|
|
2528
|
-
return count;
|
|
2529
|
-
}
|
|
2530
|
-
// Detects if tabs are used for indentation by checking the first few indented lines
|
|
2531
|
-
function detectTabsForIndentation(str) {
|
|
2532
|
-
const lines = str.split(/\r?\n/);
|
|
2533
|
-
let tabCount = 0;
|
|
2534
|
-
let spaceCount = 0;
|
|
2535
|
-
for (const line of lines) {
|
|
2536
|
-
// Skip empty lines
|
|
2537
|
-
if (line.length === 0)
|
|
2538
|
-
continue;
|
|
2539
|
-
// Check the first character of non-empty lines
|
|
2540
|
-
if (line[0] === '\t') {
|
|
2541
|
-
tabCount++;
|
|
2542
|
-
}
|
|
2543
|
-
else if (line[0] === ' ') {
|
|
2544
|
-
spaceCount++;
|
|
2545
|
-
}
|
|
2546
|
-
// If we've seen enough evidence, make a decision
|
|
2547
|
-
if (tabCount + spaceCount >= 5) {
|
|
2548
|
-
break;
|
|
2549
|
-
}
|
|
2550
|
-
}
|
|
2551
|
-
// Prefer tabs if we see more tabs than spaces
|
|
2552
|
-
return tabCount > spaceCount;
|
|
2553
|
-
}
|
|
2554
|
-
/**
|
|
2555
|
-
* Validates a format object and warns about unsupported properties.
|
|
2556
|
-
* Throws errors for supported properties with invalid types.
|
|
2557
|
-
* @param format - The format object to validate
|
|
2558
|
-
* @returns The validated format object with only supported properties and correct types
|
|
2559
|
-
*/
|
|
2560
|
-
function validateFormatObject(format) {
|
|
2561
|
-
if (!format || typeof format !== 'object') {
|
|
2562
|
-
return {};
|
|
2563
|
-
}
|
|
2564
|
-
const supportedProperties = new Set(['newLine', 'trailingNewline', 'trailingComma', 'bracketSpacing', 'inlineTableStart', 'truncateZeroTimeInDates', 'useTabsForIndentation']);
|
|
2565
|
-
const validatedFormat = {};
|
|
2566
|
-
const unsupportedProperties = [];
|
|
2567
|
-
const invalidTypeProperties = [];
|
|
2568
|
-
// Check all enumerable properties of the format object, including properties
|
|
2569
|
-
// provided via the prototype chain (common in JS Object.create(...) patterns).
|
|
2570
|
-
for (const key in format) {
|
|
2571
|
-
const isOwnEnumerable = Object.prototype.hasOwnProperty.call(format, key);
|
|
2572
|
-
if (supportedProperties.has(key)) {
|
|
2573
|
-
const value = format[key];
|
|
2574
|
-
// Type validation for each property
|
|
2575
|
-
switch (key) {
|
|
2576
|
-
case 'newLine':
|
|
2577
|
-
if (typeof value === 'string') {
|
|
2578
|
-
validatedFormat.newLine = value;
|
|
2579
|
-
}
|
|
2580
|
-
else {
|
|
2581
|
-
invalidTypeProperties.push(`${key} (expected string, got ${typeof value})`);
|
|
2582
|
-
}
|
|
2583
|
-
break;
|
|
2584
|
-
case 'trailingNewline':
|
|
2585
|
-
if (typeof value === 'boolean' || typeof value === 'number') {
|
|
2586
|
-
validatedFormat.trailingNewline = value;
|
|
2587
|
-
}
|
|
2588
|
-
else {
|
|
2589
|
-
invalidTypeProperties.push(`${key} (expected boolean or number, got ${typeof value})`);
|
|
2590
|
-
}
|
|
2591
|
-
break;
|
|
2592
|
-
case 'trailingComma':
|
|
2593
|
-
case 'bracketSpacing':
|
|
2594
|
-
case 'truncateZeroTimeInDates':
|
|
2595
|
-
case 'useTabsForIndentation':
|
|
2596
|
-
if (typeof value === 'boolean') {
|
|
2597
|
-
validatedFormat[key] = value;
|
|
2598
|
-
}
|
|
2599
|
-
else {
|
|
2600
|
-
invalidTypeProperties.push(`${key} (expected boolean, got ${typeof value})`);
|
|
2601
|
-
}
|
|
2602
|
-
break;
|
|
2603
|
-
case 'inlineTableStart':
|
|
2604
|
-
if (typeof value === 'number' && Number.isInteger(value) && value >= 0) {
|
|
2605
|
-
validatedFormat.inlineTableStart = value;
|
|
2606
|
-
}
|
|
2607
|
-
else if (value === undefined || value === null) {
|
|
2608
|
-
// Allow undefined/null to use default
|
|
2609
|
-
validatedFormat.inlineTableStart = value;
|
|
2610
|
-
}
|
|
2611
|
-
else {
|
|
2612
|
-
invalidTypeProperties.push(`${key} (expected non-negative integer or undefined, got ${typeof value})`);
|
|
2613
|
-
}
|
|
2614
|
-
break;
|
|
2615
|
-
}
|
|
2616
|
-
}
|
|
2617
|
-
else if (isOwnEnumerable) {
|
|
2618
|
-
unsupportedProperties.push(key);
|
|
2619
|
-
}
|
|
2620
|
-
}
|
|
2621
|
-
// Warn about unsupported properties
|
|
2622
|
-
if (unsupportedProperties.length > 0) {
|
|
2623
|
-
console.warn(`toml-patch: Ignoring unsupported format properties: ${unsupportedProperties.join(', ')}. Supported properties are: ${Array.from(supportedProperties).join(', ')}`);
|
|
2624
|
-
}
|
|
2625
|
-
// Throw error for invalid types
|
|
2626
|
-
if (invalidTypeProperties.length > 0) {
|
|
2627
|
-
throw new TypeError(`Invalid types for format properties: ${invalidTypeProperties.join(', ')}`);
|
|
2628
|
-
}
|
|
2629
|
-
return validatedFormat;
|
|
2630
|
-
}
|
|
2631
|
-
/**
|
|
2632
|
-
* Resolves a format parameter to a TomlFormat instance.
|
|
2633
|
-
* Handles TomlFormat instances and partial TomlFormat objects as well as undefined.
|
|
2634
|
-
*
|
|
2635
|
-
* @param format - The format parameter to resolve (TomlFormat instance, partial format object, or undefined)
|
|
2636
|
-
* @param fallbackFormat - The fallback TomlFormat to use when no format is provided
|
|
2637
|
-
* @returns A resolved TomlFormat instance
|
|
2638
|
-
*/
|
|
2639
|
-
function resolveTomlFormat(format, fallbackFormat) {
|
|
2640
|
-
var _a, _b, _c, _d, _e, _f;
|
|
2641
|
-
if (format) {
|
|
2642
|
-
// If format is provided, validate and merge it with fallback
|
|
2643
|
-
if (format instanceof TomlFormat) {
|
|
2644
|
-
return format;
|
|
2645
|
-
}
|
|
2646
|
-
else {
|
|
2647
|
-
// Validate the format object and warn about unsupported properties
|
|
2648
|
-
const validatedFormat = validateFormatObject(format);
|
|
2649
|
-
// Create a new TomlFormat instance with validated properties
|
|
2650
|
-
return new TomlFormat((_a = validatedFormat.newLine) !== null && _a !== void 0 ? _a : fallbackFormat.newLine, (_b = validatedFormat.trailingNewline) !== null && _b !== void 0 ? _b : fallbackFormat.trailingNewline, (_c = validatedFormat.trailingComma) !== null && _c !== void 0 ? _c : fallbackFormat.trailingComma, (_d = validatedFormat.bracketSpacing) !== null && _d !== void 0 ? _d : fallbackFormat.bracketSpacing, validatedFormat.inlineTableStart !== undefined ? validatedFormat.inlineTableStart : fallbackFormat.inlineTableStart, (_e = validatedFormat.truncateZeroTimeInDates) !== null && _e !== void 0 ? _e : fallbackFormat.truncateZeroTimeInDates, (_f = validatedFormat.useTabsForIndentation) !== null && _f !== void 0 ? _f : fallbackFormat.useTabsForIndentation);
|
|
2651
|
-
}
|
|
2652
|
-
}
|
|
2653
|
-
else {
|
|
2654
|
-
// Use fallback format when no format is provided
|
|
2655
|
-
return fallbackFormat;
|
|
2656
|
-
}
|
|
2657
|
-
}
|
|
2658
|
-
class TomlFormat {
|
|
2659
|
-
// These options were part of the original TimHall's version and are not yet implemented
|
|
2660
|
-
//printWidth?: number;
|
|
2661
|
-
//tabWidth?: number;
|
|
2662
|
-
constructor(newLine, trailingNewline, trailingComma, bracketSpacing, inlineTableStart, truncateZeroTimeInDates, useTabsForIndentation) {
|
|
2663
|
-
// Use provided values or fall back to defaults
|
|
2664
|
-
this.newLine = newLine !== null && newLine !== void 0 ? newLine : DEFAULT_NEWLINE;
|
|
2665
|
-
this.trailingNewline = trailingNewline !== null && trailingNewline !== void 0 ? trailingNewline : DEFAULT_TRAILING_NEWLINE;
|
|
2666
|
-
this.trailingComma = trailingComma !== null && trailingComma !== void 0 ? trailingComma : DEFAULT_TRAILING_COMMA;
|
|
2667
|
-
this.bracketSpacing = bracketSpacing !== null && bracketSpacing !== void 0 ? bracketSpacing : DEFAULT_BRACKET_SPACING;
|
|
2668
|
-
this.inlineTableStart = inlineTableStart !== null && inlineTableStart !== void 0 ? inlineTableStart : DEFAULT_INLINE_TABLE_START;
|
|
2669
|
-
this.truncateZeroTimeInDates = truncateZeroTimeInDates !== null && truncateZeroTimeInDates !== void 0 ? truncateZeroTimeInDates : DEFAULT_TRUNCATE_ZERO_TIME_IN_DATES;
|
|
2670
|
-
this.useTabsForIndentation = useTabsForIndentation !== null && useTabsForIndentation !== void 0 ? useTabsForIndentation : DEFAULT_USE_TABS_FOR_INDENTATION;
|
|
2671
|
-
}
|
|
2672
|
-
/**
|
|
2673
|
-
* Creates a new TomlFormat instance with default formatting preferences.
|
|
2674
|
-
*
|
|
2675
|
-
* @returns A new TomlFormat instance with default values:
|
|
2676
|
-
* - newLine: '\n'
|
|
2677
|
-
* - trailingNewline: 1
|
|
2678
|
-
* - trailingComma: false
|
|
2679
|
-
* - bracketSpacing: true
|
|
2680
|
-
* - inlineTableStart: 1
|
|
2681
|
-
* - truncateZeroTimeInDates: false
|
|
2682
|
-
*/
|
|
2683
|
-
static default() {
|
|
2684
|
-
return new TomlFormat(DEFAULT_NEWLINE, DEFAULT_TRAILING_NEWLINE, DEFAULT_TRAILING_COMMA, DEFAULT_BRACKET_SPACING, DEFAULT_INLINE_TABLE_START, DEFAULT_TRUNCATE_ZERO_TIME_IN_DATES, DEFAULT_USE_TABS_FOR_INDENTATION);
|
|
2685
|
-
}
|
|
2686
|
-
/**
|
|
2687
|
-
* Auto-detects formatting preferences from an existing TOML string.
|
|
2688
|
-
*
|
|
2689
|
-
* This method analyzes the provided TOML string to determine formatting
|
|
2690
|
-
* preferences such as line endings, trailing newlines, and comma usage.
|
|
2691
|
-
*
|
|
2692
|
-
* @param tomlString - The TOML string to analyze for formatting patterns
|
|
2693
|
-
* @returns A new TomlFormat instance with detected formatting preferences
|
|
2694
|
-
*
|
|
2695
|
-
* @example
|
|
2696
|
-
* ```typescript
|
|
2697
|
-
* const toml = 'array = ["a", "b", "c",]\ntable = { x = 1, y = 2, }';
|
|
2698
|
-
* const format = TomlFormat.autoDetectFormat(toml);
|
|
2699
|
-
* // format.trailingComma will be true
|
|
2700
|
-
* // format.newLine will be '\n'
|
|
2701
|
-
* // format.trailingNewline will be 0 (no trailing newline)
|
|
2702
|
-
* ```
|
|
2703
|
-
*/
|
|
2704
|
-
static autoDetectFormat(tomlString) {
|
|
2705
|
-
const format = TomlFormat.default();
|
|
2706
|
-
// Detect line ending style
|
|
2707
|
-
format.newLine = detectNewline(tomlString);
|
|
2708
|
-
// Detect trailing newline count
|
|
2709
|
-
format.trailingNewline = countTrailingNewlines(tomlString, format.newLine);
|
|
2710
|
-
// Parse the TOML to detect comma and bracket spacing usage patterns
|
|
2711
|
-
try {
|
|
2712
|
-
const ast = parseTOML(tomlString);
|
|
2713
|
-
// Convert to array once to avoid consuming the iterator multiple times
|
|
2714
|
-
const astArray = Array.from(ast);
|
|
2715
|
-
format.trailingComma = detectTrailingComma(astArray);
|
|
2716
|
-
format.bracketSpacing = detectBracketSpacing(tomlString, astArray);
|
|
2717
|
-
}
|
|
2718
|
-
catch (error) {
|
|
2719
|
-
// If parsing fails, fall back to defaults
|
|
2720
|
-
// This ensures the method is robust against malformed TOML
|
|
2721
|
-
format.trailingComma = DEFAULT_TRAILING_COMMA;
|
|
2722
|
-
format.bracketSpacing = DEFAULT_BRACKET_SPACING;
|
|
2723
|
-
}
|
|
2724
|
-
// Detect if tabs are used for indentation
|
|
2725
|
-
format.useTabsForIndentation = detectTabsForIndentation(tomlString);
|
|
2726
|
-
// inlineTableStart uses default value since auto-detection would require
|
|
2727
|
-
// complex analysis of nested table formatting preferences
|
|
2728
|
-
format.inlineTableStart = DEFAULT_INLINE_TABLE_START;
|
|
2729
|
-
// truncateZeroTimeInDates uses default value as well
|
|
2730
|
-
// We could always implement detection logic if needed
|
|
2731
|
-
// That would imply checking if all dates have no time component in the TOML.
|
|
2732
|
-
// However, it's not because all dates have no time component that a new key-value can't be introduced where the time component corresponds to midnight.
|
|
2733
|
-
format.truncateZeroTimeInDates = DEFAULT_TRUNCATE_ZERO_TIME_IN_DATES;
|
|
2734
|
-
return format;
|
|
2735
|
-
}
|
|
2736
|
-
}
|
|
2737
|
-
function formatTopLevel(document, format) {
|
|
2738
|
-
// If inlineTableStart is 0, convert all top-level tables to inline tables
|
|
2739
|
-
if (format.inlineTableStart === 0) {
|
|
2740
|
-
return document;
|
|
2741
|
-
}
|
|
2742
|
-
const move_to_top_level = document.items.filter(item => {
|
|
2743
|
-
if (!isKeyValue(item))
|
|
2744
|
-
return false;
|
|
2745
|
-
const is_inline_table = isInlineTable(item.value);
|
|
2746
|
-
const is_inline_array = isInlineArray(item.value) &&
|
|
2747
|
-
item.value.items.length &&
|
|
2748
|
-
isInlineTable(item.value.items[0].item);
|
|
2749
|
-
// Only move to top level if the depth is less than inlineTableStart
|
|
2750
|
-
if (is_inline_table || is_inline_array) {
|
|
2751
|
-
const depth = calculateTableDepth(item.key.value);
|
|
2752
|
-
return format.inlineTableStart === undefined || depth < format.inlineTableStart;
|
|
2753
|
-
}
|
|
2754
|
-
return false;
|
|
2755
|
-
});
|
|
2756
|
-
move_to_top_level.forEach(node => {
|
|
2757
|
-
remove(document, document, node);
|
|
2758
|
-
if (isInlineTable(node.value)) {
|
|
2759
|
-
insert(document, document, formatTable(node));
|
|
2760
|
-
}
|
|
2761
|
-
else {
|
|
2762
|
-
formatTableArray(node).forEach(table_array => {
|
|
2763
|
-
insert(document, document, table_array);
|
|
2764
|
-
});
|
|
2765
|
-
}
|
|
2766
|
-
});
|
|
2767
|
-
applyWrites(document);
|
|
2768
|
-
return document;
|
|
2769
|
-
}
|
|
2770
|
-
function formatTable(key_value) {
|
|
2771
|
-
const table = generateTable(key_value.key.value);
|
|
2772
|
-
for (const item of key_value.value.items) {
|
|
2773
|
-
insert(table, table, item.item);
|
|
2774
|
-
}
|
|
2775
|
-
applyWrites(table);
|
|
2776
|
-
return table;
|
|
2777
|
-
}
|
|
2778
|
-
function formatTableArray(key_value) {
|
|
2779
|
-
const root = generateDocument();
|
|
2780
|
-
for (const inline_array_item of key_value.value.items) {
|
|
2781
|
-
const table_array = generateTableArray(key_value.key.value);
|
|
2782
|
-
insert(root, root, table_array);
|
|
2783
|
-
for (const inline_table_item of inline_array_item.item.items) {
|
|
2784
|
-
insert(root, table_array, inline_table_item.item);
|
|
2785
|
-
}
|
|
2786
|
-
}
|
|
2787
|
-
applyWrites(root);
|
|
2788
|
-
return root.items;
|
|
2789
|
-
}
|
|
2790
|
-
/**
|
|
2791
|
-
* Updates a table's location end position after removing inline table items.
|
|
2792
|
-
* When inline table content is removed from a parent table, the parent table's
|
|
2793
|
-
* end position needs to be adjusted to reflect where the content actually ends.
|
|
2794
|
-
*
|
|
2795
|
-
* @param table - The table whose end position should be updated
|
|
2796
|
-
*/
|
|
2797
|
-
function postInlineItemRemovalAdjustment(table) {
|
|
2798
|
-
if (table.items.length > 0) {
|
|
2799
|
-
const lastItem = table.items[table.items.length - 1];
|
|
2800
|
-
table.loc.end.line = lastItem.loc.end.line;
|
|
2801
|
-
table.loc.end.column = lastItem.loc.end.column;
|
|
2802
|
-
}
|
|
2803
|
-
else {
|
|
2804
|
-
// If no items left, table ends at the header line
|
|
2805
|
-
table.loc.end.line = table.key.loc.end.line;
|
|
2806
|
-
table.loc.end.column = table.key.loc.end.column;
|
|
2807
|
-
}
|
|
2808
|
-
}
|
|
2809
|
-
/**
|
|
2810
|
-
* Calculates the nesting depth of a table based on its key path.
|
|
2811
|
-
* Root level tables (e.g., [table]) have depth 0.
|
|
2812
|
-
* First level nested tables (e.g., [table.nested]) have depth 1.
|
|
2813
|
-
*
|
|
2814
|
-
* @param keyPath - Array representing the table key path (e.g., ['table', 'nested'])
|
|
2815
|
-
* @returns The nesting depth (0 for root level, 1+ for nested levels)
|
|
2816
|
-
*/
|
|
2817
|
-
function calculateTableDepth(keyPath) {
|
|
2818
|
-
return Math.max(0, keyPath.length - 1);
|
|
2819
|
-
}
|
|
2820
|
-
/**
|
|
2821
|
-
* Converts nested inline tables to separate table sections based on the inlineTableStart depth setting.
|
|
2822
|
-
* This function recursively processes all tables in the document and extracts inline tables that are
|
|
2823
|
-
* at a depth less than the inlineTableStart threshold.
|
|
2824
|
-
*/
|
|
2825
|
-
function formatNestedTablesMultiline(document, format) {
|
|
2826
|
-
// If inlineTableStart is undefined, use the default behavior (no conversion)
|
|
2827
|
-
// If inlineTableStart is 0, all should be inline (no conversion)
|
|
2828
|
-
if (format.inlineTableStart === undefined || format.inlineTableStart === 0) {
|
|
2829
|
-
return document;
|
|
2830
|
-
}
|
|
2831
|
-
const additionalTables = [];
|
|
2832
|
-
// Process all existing tables for nested inline tables
|
|
2833
|
-
for (const item of document.items) {
|
|
2834
|
-
if (isKeyValue(item) && isInlineTable(item.value)) {
|
|
2835
|
-
// This is a top-level inline table (depth 0)
|
|
2836
|
-
const depth = calculateTableDepth(item.key.value);
|
|
2837
|
-
if (depth < format.inlineTableStart) {
|
|
2838
|
-
// Convert to a separate table
|
|
2839
|
-
const table = formatTable(item);
|
|
2840
|
-
// Remove the original inline table item
|
|
2841
|
-
remove(document, document, item);
|
|
2842
|
-
// Add the new table
|
|
2843
|
-
insert(document, document, table);
|
|
2844
|
-
// Process this table for further nested inlines
|
|
2845
|
-
processTableForNestedInlines(table, additionalTables, format);
|
|
2846
|
-
}
|
|
2847
|
-
}
|
|
2848
|
-
else if (item.type === 'Table') {
|
|
2849
|
-
// Process existing table for nested inline tables
|
|
2850
|
-
processTableForNestedInlines(item, additionalTables, format);
|
|
2851
|
-
}
|
|
2852
|
-
}
|
|
2853
|
-
// Add all the additional tables to the document
|
|
2854
|
-
for (const table of additionalTables) {
|
|
2855
|
-
insert(document, document, table);
|
|
2856
|
-
}
|
|
2857
|
-
applyWrites(document);
|
|
2858
|
-
return document;
|
|
2859
|
-
}
|
|
2860
|
-
/**
|
|
2861
|
-
* Recursively processes a table for nested inline tables and extracts them as separate tables
|
|
2862
|
-
* when they are at a depth less than the inlineTableStart threshold.
|
|
2863
|
-
*/
|
|
2864
|
-
function processTableForNestedInlines(table, additionalTables, format) {
|
|
2865
|
-
var _a;
|
|
2866
|
-
// Process from end to beginning to avoid index issues when removing items
|
|
2867
|
-
for (let i = table.items.length - 1; i >= 0; i--) {
|
|
2868
|
-
const item = table.items[i];
|
|
2869
|
-
if (isKeyValue(item) && isInlineTable(item.value)) {
|
|
2870
|
-
// Calculate the depth of this nested table
|
|
2871
|
-
const nestedTableKey = [...table.key.item.value, ...item.key.value];
|
|
2872
|
-
const depth = calculateTableDepth(nestedTableKey);
|
|
2873
|
-
// Only convert to separate table if depth is less than inlineTableStart
|
|
2874
|
-
if (depth < ((_a = format.inlineTableStart) !== null && _a !== void 0 ? _a : 1)) {
|
|
2875
|
-
// Convert this inline table to a separate table section
|
|
2876
|
-
const separateTable = generateTable(nestedTableKey);
|
|
2877
|
-
// Move all items from the inline table to the separate table
|
|
2878
|
-
for (const inlineItem of item.value.items) {
|
|
2879
|
-
insert(separateTable, separateTable, inlineItem.item);
|
|
2880
|
-
}
|
|
2881
|
-
// Remove this item from the original table
|
|
2882
|
-
remove(table, table, item);
|
|
2883
|
-
// Update the parent table's end position after removal
|
|
2884
|
-
postInlineItemRemovalAdjustment(table);
|
|
2885
|
-
// Add this table to be inserted into the document
|
|
2886
|
-
additionalTables.push(separateTable);
|
|
2887
|
-
// Recursively process the new table for further nested inlines
|
|
2888
|
-
processTableForNestedInlines(separateTable, additionalTables, format);
|
|
2889
|
-
}
|
|
2890
|
-
}
|
|
2891
|
-
}
|
|
2892
|
-
}
|
|
2893
|
-
function formatPrintWidth(document, format) {
|
|
2894
|
-
// TODO
|
|
2895
|
-
return document;
|
|
2896
|
-
}
|
|
2897
|
-
function formatEmptyLines(document) {
|
|
2898
|
-
let shift = 0;
|
|
2899
|
-
let previous = 0;
|
|
2900
|
-
for (const item of document.items) {
|
|
2901
|
-
if (previous === 0 && item.loc.start.line > 1) {
|
|
2902
|
-
// Remove leading newlines
|
|
2903
|
-
shift = 1 - item.loc.start.line;
|
|
2904
|
-
}
|
|
2905
|
-
else if (item.loc.start.line + shift > previous + 2) {
|
|
2906
|
-
shift += previous + 2 - (item.loc.start.line + shift);
|
|
2907
|
-
}
|
|
2908
|
-
shiftNode(item, {
|
|
2909
|
-
lines: shift,
|
|
2910
|
-
columns: 0
|
|
2911
|
-
});
|
|
2912
|
-
previous = item.loc.end.line;
|
|
2913
|
-
}
|
|
2914
|
-
return document;
|
|
2915
|
-
}
|
|
2916
|
-
|
|
2917
|
-
function parseJS(value, format = TomlFormat.default()) {
|
|
2918
|
-
value = toJSON(value);
|
|
2919
|
-
// Reorder the elements in the object
|
|
2920
|
-
value = reorderElements(value);
|
|
2921
|
-
const document = generateDocument();
|
|
2922
|
-
for (const item of walkObject(value, format)) {
|
|
2923
|
-
insert(document, document, item);
|
|
2924
|
-
}
|
|
2925
|
-
applyWrites(document);
|
|
2926
|
-
// Heuristics:
|
|
2927
|
-
// 1. Top-level objects/arrays should be tables/table arrays
|
|
2928
|
-
// 2. Convert objects/arrays to tables/table arrays based on print width
|
|
2929
|
-
// 3. Convert nested inline tables to separate tables based on preferNestedTablesMultiline
|
|
2930
|
-
const formatted = pipe(document, document => formatTopLevel(document, format), document => formatNestedTablesMultiline(document, format), document => formatPrintWidth(document));
|
|
2931
|
-
// Apply formatEmptyLines only once at the end
|
|
2932
|
-
return formatEmptyLines(formatted);
|
|
2933
|
-
}
|
|
2934
|
-
/**
|
|
2935
|
-
This function makes sure that properties that are simple values (not objects or arrays) are ordered first,
|
|
2936
|
-
and that objects and arrays are ordered last. This makes parseJS more reliable and easier to test.
|
|
2937
|
-
*/
|
|
2938
|
-
function reorderElements(value) {
|
|
2939
|
-
// Pre-sort keys to avoid multiple iterations
|
|
2940
|
-
const simpleKeys = [];
|
|
2941
|
-
const complexKeys = [];
|
|
2942
|
-
// Separate keys in a single pass
|
|
2943
|
-
for (const key in value) {
|
|
2944
|
-
if (isObject(value[key]) || Array.isArray(value[key])) {
|
|
2945
|
-
complexKeys.push(key);
|
|
2946
|
-
}
|
|
2947
|
-
else {
|
|
2948
|
-
simpleKeys.push(key);
|
|
2949
|
-
}
|
|
2950
|
-
}
|
|
2951
|
-
// Create result with the correct order
|
|
2952
|
-
const result = {};
|
|
2953
|
-
// Add simple values first
|
|
2954
|
-
for (let i = 0; i < simpleKeys.length; i++) {
|
|
2955
|
-
const key = simpleKeys[i];
|
|
2956
|
-
result[key] = value[key];
|
|
2957
|
-
}
|
|
2958
|
-
// Then add complex values
|
|
2959
|
-
for (let i = 0; i < complexKeys.length; i++) {
|
|
2960
|
-
const key = complexKeys[i];
|
|
2961
|
-
result[key] = value[key];
|
|
2962
|
-
}
|
|
2963
|
-
return result;
|
|
2964
|
-
}
|
|
2965
|
-
function* walkObject(object, format) {
|
|
2966
|
-
for (const key of Object.keys(object)) {
|
|
2967
|
-
yield generateKeyValue([key], walkValue(object[key], format));
|
|
2968
|
-
}
|
|
2969
|
-
}
|
|
2970
|
-
function walkValue(value, format) {
|
|
2971
|
-
if (value == null) {
|
|
2972
|
-
throw new Error('"null" and "undefined" values are not supported');
|
|
2973
|
-
}
|
|
2974
|
-
if (isString(value)) {
|
|
2975
|
-
return generateString(value);
|
|
2976
|
-
}
|
|
2977
|
-
else if (isInteger(value)) {
|
|
2978
|
-
return generateInteger(value);
|
|
2979
|
-
}
|
|
2980
|
-
else if (isFloat(value)) {
|
|
2981
|
-
return generateFloat(value);
|
|
2982
|
-
}
|
|
2983
|
-
else if (isBoolean(value)) {
|
|
2984
|
-
return generateBoolean(value);
|
|
2985
|
-
}
|
|
2986
|
-
else if (isDate(value)) {
|
|
2987
|
-
return generateDateTime(value, format);
|
|
2988
|
-
}
|
|
2989
|
-
else if (Array.isArray(value)) {
|
|
2990
|
-
return walkInlineArray(value, format);
|
|
2991
|
-
}
|
|
2992
|
-
else {
|
|
2993
|
-
return walkInlineTable(value, format);
|
|
2994
|
-
}
|
|
2995
|
-
}
|
|
2996
|
-
function walkInlineArray(value, format) {
|
|
2997
|
-
const inline_array = generateInlineArray();
|
|
2998
|
-
for (const element of value) {
|
|
2999
|
-
const item = walkValue(element, format);
|
|
3000
|
-
const inline_array_item = generateInlineItem(item);
|
|
3001
|
-
insert(inline_array, inline_array, inline_array_item);
|
|
3002
|
-
}
|
|
3003
|
-
applyBracketSpacing(inline_array, inline_array, format.bracketSpacing);
|
|
3004
|
-
applyTrailingComma(inline_array, inline_array, format.trailingComma);
|
|
3005
|
-
applyWrites(inline_array);
|
|
3006
|
-
return inline_array;
|
|
3007
|
-
}
|
|
3008
|
-
function walkInlineTable(value, format) {
|
|
3009
|
-
value = toJSON(value);
|
|
3010
|
-
if (!isObject(value))
|
|
3011
|
-
return walkValue(value, format);
|
|
3012
|
-
const inline_table = generateInlineTable();
|
|
3013
|
-
const items = [...walkObject(value, format)];
|
|
3014
|
-
for (const item of items) {
|
|
3015
|
-
const inline_table_item = generateInlineItem(item);
|
|
3016
|
-
insert(inline_table, inline_table, inline_table_item);
|
|
3017
|
-
}
|
|
3018
|
-
applyBracketSpacing(inline_table, inline_table, format.bracketSpacing);
|
|
3019
|
-
applyTrailingComma(inline_table, inline_table, format.trailingComma);
|
|
3020
|
-
applyWrites(inline_table);
|
|
3021
|
-
return inline_table;
|
|
3022
|
-
}
|
|
3023
|
-
/**
|
|
3024
|
-
* Handles custom object serialization by checking for and using toJSON methods
|
|
3025
|
-
*
|
|
3026
|
-
* @param value - The value to potentially convert
|
|
3027
|
-
* @returns The result of value.toJSON() if available, otherwise the original value
|
|
3028
|
-
*/
|
|
3029
|
-
function toJSON(value) {
|
|
3030
|
-
// Skip null/undefined values
|
|
3031
|
-
if (!value) {
|
|
3032
|
-
return value;
|
|
3033
|
-
}
|
|
3034
|
-
// Skip Date objects (they have special handling)
|
|
3035
|
-
if (isDate(value)) {
|
|
3036
|
-
return value;
|
|
3037
|
-
}
|
|
3038
|
-
// Use object's custom toJSON method if available
|
|
3039
|
-
if (typeof value.toJSON === 'function') {
|
|
3040
|
-
return value.toJSON();
|
|
3041
|
-
}
|
|
3042
|
-
// Otherwise return unmodified
|
|
3043
|
-
return value;
|
|
3044
|
-
}
|
|
3045
|
-
|
|
3046
|
-
const BY_NEW_LINE = /(\r\n|\n)/g;
|
|
3047
|
-
/**
|
|
3048
|
-
* Converts an Abstract Syntax Tree (AST) back to TOML format string.
|
|
3049
|
-
*
|
|
3050
|
-
* This function traverses the AST and reconstructs the original TOML document
|
|
3051
|
-
* by writing each node's raw content to the appropriate location coordinates.
|
|
3052
|
-
* It preserves the original formatting, spacing, and structure of the TOML file.
|
|
3053
|
-
*
|
|
3054
|
-
* @param ast - The Abstract Syntax Tree representing the parsed TOML document
|
|
3055
|
-
* @param format - The formatting options to use for the output
|
|
3056
|
-
* @returns The reconstructed TOML document as a string
|
|
3057
|
-
*
|
|
3058
|
-
* @example
|
|
3059
|
-
* ```typescript
|
|
3060
|
-
* const tomlString = toTOML(ast, TomlFormat.default());
|
|
3061
|
-
* ```
|
|
3062
|
-
*/
|
|
3063
|
-
function toTOML(ast, format) {
|
|
3064
|
-
const lines = [];
|
|
3065
|
-
const paddingChar = format.useTabsForIndentation ? '\t' : SPACE;
|
|
3066
|
-
traverse(ast, {
|
|
3067
|
-
[NodeType.TableKey](node) {
|
|
3068
|
-
const { start, end } = node.loc;
|
|
3069
|
-
write(lines, { start, end: { line: start.line, column: start.column + 1 } }, '[', paddingChar);
|
|
3070
|
-
write(lines, { start: { line: end.line, column: end.column - 1 }, end }, ']', paddingChar);
|
|
3071
|
-
},
|
|
3072
|
-
[NodeType.TableArrayKey](node) {
|
|
3073
|
-
const { start, end } = node.loc;
|
|
3074
|
-
write(lines, { start, end: { line: start.line, column: start.column + 2 } }, '[[', paddingChar);
|
|
3075
|
-
write(lines, { start: { line: end.line, column: end.column - 2 }, end }, ']]', paddingChar);
|
|
3076
|
-
},
|
|
3077
|
-
[NodeType.KeyValue](node) {
|
|
3078
|
-
const { start: { line } } = node.loc;
|
|
3079
|
-
write(lines, { start: { line, column: node.equals }, end: { line, column: node.equals + 1 } }, '=', paddingChar);
|
|
3080
|
-
},
|
|
3081
|
-
[NodeType.Key](node) {
|
|
3082
|
-
write(lines, node.loc, node.raw, paddingChar);
|
|
3083
|
-
},
|
|
3084
|
-
[NodeType.String](node) {
|
|
3085
|
-
write(lines, node.loc, node.raw, paddingChar);
|
|
3086
|
-
},
|
|
3087
|
-
[NodeType.Integer](node) {
|
|
3088
|
-
write(lines, node.loc, node.raw, paddingChar);
|
|
3089
|
-
},
|
|
3090
|
-
[NodeType.Float](node) {
|
|
3091
|
-
write(lines, node.loc, node.raw, paddingChar);
|
|
3092
|
-
},
|
|
3093
|
-
[NodeType.Boolean](node) {
|
|
3094
|
-
write(lines, node.loc, node.value.toString(), paddingChar);
|
|
3095
|
-
},
|
|
3096
|
-
[NodeType.DateTime](node) {
|
|
3097
|
-
write(lines, node.loc, node.raw, paddingChar);
|
|
3098
|
-
},
|
|
3099
|
-
[NodeType.InlineArray](node) {
|
|
3100
|
-
const { start, end } = node.loc;
|
|
3101
|
-
write(lines, { start, end: { line: start.line, column: start.column + 1 } }, '[', paddingChar);
|
|
3102
|
-
write(lines, { start: { line: end.line, column: end.column - 1 }, end }, ']', paddingChar);
|
|
3103
|
-
},
|
|
3104
|
-
[NodeType.InlineTable](node) {
|
|
3105
|
-
const { start, end } = node.loc;
|
|
3106
|
-
write(lines, { start, end: { line: start.line, column: start.column + 1 } }, '{', paddingChar);
|
|
3107
|
-
write(lines, { start: { line: end.line, column: end.column - 1 }, end }, '}', paddingChar);
|
|
3108
|
-
},
|
|
3109
|
-
[NodeType.InlineItem](node) {
|
|
3110
|
-
if (!node.comma)
|
|
3111
|
-
return;
|
|
3112
|
-
const start = node.loc.end;
|
|
3113
|
-
write(lines, { start, end: { line: start.line, column: start.column + 1 } }, ',', paddingChar);
|
|
3114
|
-
},
|
|
3115
|
-
[NodeType.Comment](node) {
|
|
3116
|
-
write(lines, node.loc, node.raw, paddingChar);
|
|
3117
|
-
}
|
|
3118
|
-
});
|
|
3119
|
-
// Post-process: convert leading spaces to tabs if useTabsForIndentation is enabled
|
|
3120
|
-
if (format.useTabsForIndentation) {
|
|
3121
|
-
for (let i = 0; i < lines.length; i++) {
|
|
3122
|
-
const line = lines[i];
|
|
3123
|
-
// Find the leading whitespace
|
|
3124
|
-
const match = line.match(/^( +)/);
|
|
3125
|
-
if (match) {
|
|
3126
|
-
const leadingSpaces = match[1];
|
|
3127
|
-
// Replace entire leading space sequence with equivalent tabs
|
|
3128
|
-
// Each space becomes a tab (preserving the visual width)
|
|
3129
|
-
const leadingTabs = '\t'.repeat(leadingSpaces.length);
|
|
3130
|
-
lines[i] = leadingTabs + line.substring(leadingSpaces.length);
|
|
3131
|
-
}
|
|
3132
|
-
}
|
|
3133
|
-
}
|
|
3134
|
-
return lines.join(format.newLine) + format.newLine.repeat(format.trailingNewline);
|
|
3135
|
-
}
|
|
3136
|
-
/**
|
|
3137
|
-
* Writes raw string content to specific location coordinates within a lines array.
|
|
3138
|
-
*
|
|
3139
|
-
* This function is responsible for placing TOML content at precise positions within
|
|
3140
|
-
* the output lines, handling multi-line content and preserving existing content
|
|
3141
|
-
* around the target location.
|
|
3142
|
-
*
|
|
3143
|
-
* @param lines - Array of string lines representing the TOML document being built.
|
|
3144
|
-
* Lines are 1-indexed but stored in 0-indexed array.
|
|
3145
|
-
* @param loc - Location object specifying where to write the content, containing:
|
|
3146
|
-
* - start: { line: number, column: number } - Starting position (1-indexed line, 0-indexed column)
|
|
3147
|
-
* - end: { line: number, column: number } - Ending position (1-indexed line, 0-indexed column)
|
|
3148
|
-
* @param raw - The raw string content to write at the specified location.
|
|
3149
|
-
* Can contain multiple lines separated by \n or \r\n.
|
|
3150
|
-
* @param paddingChar - The character to use for padding (space or tab)
|
|
3151
|
-
*
|
|
3152
|
-
* @throws {Error} When there's a mismatch between location span and raw string line count
|
|
3153
|
-
* @throws {Error} When attempting to write to an uninitialized line
|
|
3154
|
-
*
|
|
3155
|
-
* @example
|
|
3156
|
-
* ```typescript
|
|
3157
|
-
* const lines = ['', ''];
|
|
3158
|
-
* const location = { start: { line: 1, column: 0 }, end: { line: 1, column: 3 } };
|
|
3159
|
-
* write(lines, location, 'key', ' ');
|
|
3160
|
-
* // Result: lines[0] becomes 'key'
|
|
3161
|
-
* ```
|
|
3162
|
-
*/
|
|
3163
|
-
function write(lines, loc, raw, paddingChar = SPACE) {
|
|
3164
|
-
const raw_lines = raw.split(BY_NEW_LINE).filter(line => line !== '\n' && line !== '\r\n');
|
|
3165
|
-
const expected_lines = loc.end.line - loc.start.line + 1;
|
|
3166
|
-
if (raw_lines.length !== expected_lines) {
|
|
3167
|
-
throw new Error(`Mismatch between location and raw string, expected ${expected_lines} lines for "${raw}"`);
|
|
3168
|
-
}
|
|
3169
|
-
for (let i = loc.start.line; i <= loc.end.line; i++) {
|
|
3170
|
-
const line = getLine(lines, i);
|
|
3171
|
-
//Throw if line is uninitialized
|
|
3172
|
-
if (line === undefined) {
|
|
3173
|
-
throw new Error(`Line ${i} is uninitialized when writing "${raw}" at ${loc.start.line}:${loc.start.column} to ${loc.end.line}:${loc.end.column}`);
|
|
3174
|
-
}
|
|
3175
|
-
const is_start_line = i === loc.start.line;
|
|
3176
|
-
const is_end_line = i === loc.end.line;
|
|
3177
|
-
let before = '';
|
|
3178
|
-
if (is_start_line) {
|
|
3179
|
-
const existingBefore = line.substring(0, loc.start.column);
|
|
3180
|
-
if (existingBefore.length < loc.start.column) {
|
|
3181
|
-
// Need to pad - always use spaces during write phase
|
|
3182
|
-
// Tab conversion happens in post-processing for leading indentation only
|
|
3183
|
-
before = existingBefore.padEnd(loc.start.column, SPACE);
|
|
3184
|
-
}
|
|
3185
|
-
else {
|
|
3186
|
-
before = existingBefore;
|
|
3187
|
-
}
|
|
3188
|
-
}
|
|
3189
|
-
const after = is_end_line ? line.substring(loc.end.column) : '';
|
|
3190
|
-
lines[i - 1] = before + raw_lines[i - loc.start.line] + after;
|
|
3191
|
-
}
|
|
3192
|
-
}
|
|
3193
|
-
/**
|
|
3194
|
-
* Safely retrieves a line from the lines array, initializing empty lines as needed.
|
|
3195
|
-
*
|
|
3196
|
-
* This helper function handles the conversion between 1-indexed line numbers (used in locations)
|
|
3197
|
-
* and 0-indexed array positions. It ensures that accessing a line that doesn't exist yet
|
|
3198
|
-
* will initialize all preceding lines with empty strings.
|
|
3199
|
-
*
|
|
3200
|
-
* @param lines - Array of string lines representing the document
|
|
3201
|
-
* @param index - 1-indexed line number to retrieve
|
|
3202
|
-
* @returns The line content as a string, or empty string for new lines
|
|
3203
|
-
*
|
|
3204
|
-
* @example
|
|
3205
|
-
* ```typescript
|
|
3206
|
-
* const lines = ['first line'];
|
|
3207
|
-
* const line = getLine(lines, 3); // Initializes lines[1] and lines[2] as empty strings
|
|
3208
|
-
* // lines becomes ['first line', '', '']
|
|
3209
|
-
* ```
|
|
3210
|
-
*/
|
|
3211
|
-
function getLine(lines, index) {
|
|
3212
|
-
if (!lines[index - 1]) {
|
|
3213
|
-
for (let i = 0; i < index; i++) {
|
|
3214
|
-
if (!lines[i])
|
|
3215
|
-
lines[i] = '';
|
|
3216
|
-
}
|
|
3217
|
-
}
|
|
3218
|
-
return lines[index - 1];
|
|
3219
|
-
}
|
|
3220
|
-
|
|
3221
|
-
/**
|
|
3222
|
-
* Converts the given AST to a JavaScript object.
|
|
3223
|
-
*
|
|
3224
|
-
* @param ast The abstract syntax tree to convert.
|
|
3225
|
-
* @param input The original input string (used for error reporting).
|
|
3226
|
-
* @returns The JavaScript object representation of the AST.
|
|
3227
|
-
*/
|
|
3228
|
-
function toJS(ast, input = '') {
|
|
3229
|
-
const result = blank();
|
|
3230
|
-
const tables = new Set();
|
|
3231
|
-
const table_arrays = new Set();
|
|
3232
|
-
const defined = new Set();
|
|
3233
|
-
let active = result;
|
|
3234
|
-
let skip_depth = 0;
|
|
3235
|
-
traverse(ast, {
|
|
3236
|
-
[NodeType.Table](node) {
|
|
3237
|
-
const key = node.key.item.value;
|
|
3238
|
-
try {
|
|
3239
|
-
validateKey(result, key, node.type, { tables, table_arrays, defined });
|
|
3240
|
-
}
|
|
3241
|
-
catch (err) {
|
|
3242
|
-
const e = err;
|
|
3243
|
-
throw new ParseError(input, node.key.loc.start, e.message);
|
|
3244
|
-
}
|
|
3245
|
-
const joined_key = joinKey(key);
|
|
3246
|
-
tables.add(joined_key);
|
|
3247
|
-
defined.add(joined_key);
|
|
3248
|
-
active = ensureTable(result, key);
|
|
3249
|
-
},
|
|
3250
|
-
[NodeType.TableArray](node) {
|
|
3251
|
-
const key = node.key.item.value;
|
|
3252
|
-
try {
|
|
3253
|
-
validateKey(result, key, node.type, { tables, table_arrays, defined });
|
|
3254
|
-
}
|
|
3255
|
-
catch (err) {
|
|
3256
|
-
const e = err;
|
|
3257
|
-
throw new ParseError(input, node.key.loc.start, e.message);
|
|
3258
|
-
}
|
|
3259
|
-
const joined_key = joinKey(key);
|
|
3260
|
-
table_arrays.add(joined_key);
|
|
3261
|
-
defined.add(joined_key);
|
|
3262
|
-
active = ensureTableArray(result, key);
|
|
3263
|
-
},
|
|
3264
|
-
[NodeType.KeyValue]: {
|
|
3265
|
-
enter(node) {
|
|
3266
|
-
if (skip_depth > 0)
|
|
3267
|
-
return;
|
|
3268
|
-
const key = node.key.value;
|
|
3269
|
-
try {
|
|
3270
|
-
validateKey(active, key, node.type, { tables, table_arrays, defined });
|
|
3271
|
-
}
|
|
3272
|
-
catch (err) {
|
|
3273
|
-
const e = err;
|
|
3274
|
-
throw new ParseError(input, node.key.loc.start, e.message);
|
|
3275
|
-
}
|
|
3276
|
-
const value = toValue(node.value);
|
|
3277
|
-
const target = key.length > 1 ? ensureTable(active, key.slice(0, -1)) : active;
|
|
3278
|
-
target[last(key)] = value;
|
|
3279
|
-
defined.add(joinKey(key));
|
|
3280
|
-
}
|
|
3281
|
-
},
|
|
3282
|
-
[NodeType.InlineTable]: {
|
|
3283
|
-
enter() {
|
|
3284
|
-
// Handled by toValue
|
|
3285
|
-
skip_depth++;
|
|
3286
|
-
},
|
|
3287
|
-
exit() {
|
|
3288
|
-
skip_depth--;
|
|
3289
|
-
}
|
|
3290
|
-
}
|
|
3291
|
-
});
|
|
3292
|
-
return result;
|
|
3293
|
-
}
|
|
3294
|
-
function toValue(node) {
|
|
3295
|
-
switch (node.type) {
|
|
3296
|
-
case NodeType.InlineTable:
|
|
3297
|
-
const result = blank();
|
|
3298
|
-
node.items.forEach(({ item }) => {
|
|
3299
|
-
const key = item.key.value;
|
|
3300
|
-
const value = toValue(item.value);
|
|
3301
|
-
const target = key.length > 1 ? ensureTable(result, key.slice(0, -1)) : result;
|
|
3302
|
-
target[last(key)] = value;
|
|
3303
|
-
});
|
|
3304
|
-
return result;
|
|
3305
|
-
case NodeType.InlineArray:
|
|
3306
|
-
return node.items.map(item => toValue(item.item));
|
|
3307
|
-
case NodeType.DateTime:
|
|
3308
|
-
// Preserve TOML date/time custom classes so format is retained when
|
|
3309
|
-
// round-tripping through stringify() (e.g. date-only, time-only, local vs offset).
|
|
3310
|
-
// These classes extend Date, so JS users can still treat them as Dates.
|
|
3311
|
-
return node.value;
|
|
3312
|
-
case NodeType.String:
|
|
3313
|
-
case NodeType.Integer:
|
|
3314
|
-
case NodeType.Float:
|
|
3315
|
-
case NodeType.Boolean:
|
|
3316
|
-
return node.value;
|
|
3317
|
-
default:
|
|
3318
|
-
throw new Error(`Unrecognized value type "${node.type}"`);
|
|
3319
|
-
}
|
|
3320
|
-
}
|
|
3321
|
-
function validateKey(object, key, type, state) {
|
|
3322
|
-
// 1. Cannot override primitive value
|
|
3323
|
-
let parts = [];
|
|
3324
|
-
let index = 0;
|
|
3325
|
-
for (const part of key) {
|
|
3326
|
-
parts.push(part);
|
|
3327
|
-
if (!has(object, part))
|
|
3328
|
-
return;
|
|
3329
|
-
if (isPrimitive(object[part])) {
|
|
3330
|
-
throw new Error(`Invalid key, a value has already been defined for ${parts.join('.')}`);
|
|
3331
|
-
}
|
|
3332
|
-
const joined_parts = joinKey(parts);
|
|
3333
|
-
if (Array.isArray(object[part]) && !state.table_arrays.has(joined_parts)) {
|
|
3334
|
-
throw new Error(`Invalid key, cannot add to a static array at ${joined_parts}`);
|
|
3335
|
-
}
|
|
3336
|
-
const next_is_last = index++ < key.length - 1;
|
|
3337
|
-
object = Array.isArray(object[part]) && next_is_last ? last(object[part]) : object[part];
|
|
3338
|
-
}
|
|
3339
|
-
const joined_key = joinKey(key);
|
|
3340
|
-
// 2. Cannot override table
|
|
3341
|
-
if (object && type === NodeType.Table && state.defined.has(joined_key)) {
|
|
3342
|
-
throw new Error(`Invalid key, a table has already been defined named ${joined_key}`);
|
|
3343
|
-
}
|
|
3344
|
-
// 3. Cannot add table array to static array or table
|
|
3345
|
-
if (object && type === NodeType.TableArray && !state.table_arrays.has(joined_key)) {
|
|
3346
|
-
throw new Error(`Invalid key, cannot add an array of tables to a table at ${joined_key}`);
|
|
3347
|
-
}
|
|
3348
|
-
}
|
|
3349
|
-
function ensureTable(object, key) {
|
|
3350
|
-
const target = ensure(object, key.slice(0, -1));
|
|
3351
|
-
const last_key = last(key);
|
|
3352
|
-
if (!target[last_key]) {
|
|
3353
|
-
target[last_key] = blank();
|
|
3354
|
-
}
|
|
3355
|
-
return target[last_key];
|
|
3356
|
-
}
|
|
3357
|
-
function ensureTableArray(object, key) {
|
|
3358
|
-
const target = ensure(object, key.slice(0, -1));
|
|
3359
|
-
const last_key = last(key);
|
|
3360
|
-
if (!target[last_key]) {
|
|
3361
|
-
target[last_key] = [];
|
|
3362
|
-
}
|
|
3363
|
-
const next = blank();
|
|
3364
|
-
target[last(key)].push(next);
|
|
3365
|
-
return next;
|
|
3366
|
-
}
|
|
3367
|
-
function ensure(object, keys) {
|
|
3368
|
-
return keys.reduce((active, subkey) => {
|
|
3369
|
-
if (!active[subkey]) {
|
|
3370
|
-
active[subkey] = blank();
|
|
3371
|
-
}
|
|
3372
|
-
return Array.isArray(active[subkey]) ? last(active[subkey]) : active[subkey];
|
|
3373
|
-
}, object);
|
|
3374
|
-
}
|
|
3375
|
-
function isPrimitive(value) {
|
|
3376
|
-
return typeof value !== 'object' && !isDate(value);
|
|
3377
|
-
}
|
|
3378
|
-
function joinKey(key) {
|
|
3379
|
-
return key.join('.');
|
|
3380
|
-
}
|
|
3381
|
-
|
|
3382
|
-
var ChangeType;
|
|
3383
|
-
(function (ChangeType) {
|
|
3384
|
-
ChangeType["Add"] = "Add";
|
|
3385
|
-
ChangeType["Edit"] = "Edit";
|
|
3386
|
-
ChangeType["Remove"] = "Remove";
|
|
3387
|
-
ChangeType["Move"] = "Move";
|
|
3388
|
-
ChangeType["Rename"] = "Rename";
|
|
3389
|
-
})(ChangeType || (ChangeType = {}));
|
|
3390
|
-
function isAdd(change) {
|
|
3391
|
-
return change.type === ChangeType.Add;
|
|
3392
|
-
}
|
|
3393
|
-
function isEdit(change) {
|
|
3394
|
-
return change.type === ChangeType.Edit;
|
|
3395
|
-
}
|
|
3396
|
-
function isRemove(change) {
|
|
3397
|
-
return change.type === ChangeType.Remove;
|
|
3398
|
-
}
|
|
3399
|
-
function isMove(change) {
|
|
3400
|
-
return change.type === ChangeType.Move;
|
|
3401
|
-
}
|
|
3402
|
-
function isRename(change) {
|
|
3403
|
-
return change.type === ChangeType.Rename;
|
|
3404
|
-
}
|
|
3405
|
-
function diff(before, after, path = []) {
|
|
3406
|
-
if (before === after || datesEqual(before, after)) {
|
|
3407
|
-
return [];
|
|
3408
|
-
}
|
|
3409
|
-
if (Array.isArray(before) && Array.isArray(after)) {
|
|
3410
|
-
return compareArrays(before, after, path);
|
|
3411
|
-
}
|
|
3412
|
-
else if (isObject(before) && isObject(after)) {
|
|
3413
|
-
return compareObjects(before, after, path);
|
|
3414
|
-
}
|
|
3415
|
-
else {
|
|
3416
|
-
return [
|
|
3417
|
-
{
|
|
3418
|
-
type: ChangeType.Edit,
|
|
3419
|
-
path
|
|
3420
|
-
}
|
|
3421
|
-
];
|
|
3422
|
-
}
|
|
3423
|
-
}
|
|
3424
|
-
function compareObjects(before, after, path = []) {
|
|
3425
|
-
let changes = [];
|
|
3426
|
-
// 1. Get keys and stable values
|
|
3427
|
-
const before_keys = Object.keys(before);
|
|
3428
|
-
const before_stable = before_keys.map(key => stableStringify(before[key]));
|
|
3429
|
-
const after_keys = Object.keys(after);
|
|
3430
|
-
const after_stable = after_keys.map(key => stableStringify(after[key]));
|
|
3431
|
-
// Check for rename by seeing if object is in both before and after
|
|
3432
|
-
// and that key is no longer used in after
|
|
3433
|
-
const isRename = (stable, search) => {
|
|
3434
|
-
const index = search.indexOf(stable);
|
|
3435
|
-
if (index < 0)
|
|
3436
|
-
return false;
|
|
3437
|
-
const before_key = before_keys[before_stable.indexOf(stable)];
|
|
3438
|
-
return !after_keys.includes(before_key);
|
|
3439
|
-
};
|
|
3440
|
-
// 2. Check for changes, rename, and removed
|
|
3441
|
-
before_keys.forEach((key, index) => {
|
|
3442
|
-
const sub_path = path.concat(key);
|
|
3443
|
-
if (after_keys.includes(key)) {
|
|
3444
|
-
merge(changes, diff(before[key], after[key], sub_path));
|
|
3445
|
-
}
|
|
3446
|
-
else if (isRename(before_stable[index], after_stable)) {
|
|
3447
|
-
const to = after_keys[after_stable.indexOf(before_stable[index])];
|
|
3448
|
-
changes.push({
|
|
3449
|
-
type: ChangeType.Rename,
|
|
3450
|
-
path,
|
|
3451
|
-
from: key,
|
|
3452
|
-
to
|
|
3453
|
-
});
|
|
3454
|
-
}
|
|
3455
|
-
else {
|
|
3456
|
-
changes.push({
|
|
3457
|
-
type: ChangeType.Remove,
|
|
3458
|
-
path: sub_path
|
|
3459
|
-
});
|
|
3460
|
-
}
|
|
3461
|
-
});
|
|
3462
|
-
// 3. Check for additions
|
|
3463
|
-
after_keys.forEach((key, index) => {
|
|
3464
|
-
if (!before_keys.includes(key) && !isRename(after_stable[index], before_stable)) {
|
|
3465
|
-
changes.push({
|
|
3466
|
-
type: ChangeType.Add,
|
|
3467
|
-
path: path.concat(key)
|
|
3468
|
-
});
|
|
3469
|
-
}
|
|
3470
|
-
});
|
|
3471
|
-
return changes;
|
|
3472
|
-
}
|
|
3473
|
-
function compareArrays(before, after, path = []) {
|
|
3474
|
-
let changes = [];
|
|
3475
|
-
// 1. Convert arrays to stable objects
|
|
3476
|
-
const before_stable = before.map(stableStringify);
|
|
3477
|
-
const after_stable = after.map(stableStringify);
|
|
3478
|
-
// 2. Step through after array making changes to before array as-needed
|
|
3479
|
-
after_stable.forEach((value, index) => {
|
|
3480
|
-
const overflow = index >= before_stable.length;
|
|
3481
|
-
// Check if items are the same
|
|
3482
|
-
if (!overflow && before_stable[index] === value) {
|
|
3483
|
-
return;
|
|
3484
|
-
}
|
|
3485
|
-
// Check if item has been moved -> shift into place
|
|
3486
|
-
const from = before_stable.indexOf(value, index + 1);
|
|
3487
|
-
if (!overflow && from > -1) {
|
|
3488
|
-
changes.push({
|
|
3489
|
-
type: ChangeType.Move,
|
|
3490
|
-
path,
|
|
3491
|
-
from,
|
|
3492
|
-
to: index
|
|
3493
|
-
});
|
|
3494
|
-
const move = before_stable.splice(from, 1);
|
|
3495
|
-
before_stable.splice(index, 0, ...move);
|
|
3496
|
-
return;
|
|
3497
|
-
}
|
|
3498
|
-
// Check if item is removed -> assume it's been edited and replace
|
|
3499
|
-
const removed = !after_stable.includes(before_stable[index]);
|
|
3500
|
-
if (!overflow && removed) {
|
|
3501
|
-
merge(changes, diff(before[index], after[index], path.concat(index)));
|
|
3502
|
-
before_stable[index] = value;
|
|
3503
|
-
return;
|
|
3504
|
-
}
|
|
3505
|
-
// Add as new item and shift existing
|
|
3506
|
-
changes.push({
|
|
3507
|
-
type: ChangeType.Add,
|
|
3508
|
-
path: path.concat(index)
|
|
3509
|
-
});
|
|
3510
|
-
before_stable.splice(index, 0, value);
|
|
3511
|
-
});
|
|
3512
|
-
// 3. Remove any remaining overflow items
|
|
3513
|
-
for (let i = after_stable.length; i < before_stable.length; i++) {
|
|
3514
|
-
changes.push({
|
|
3515
|
-
type: ChangeType.Remove,
|
|
3516
|
-
path: path.concat(i)
|
|
3517
|
-
});
|
|
3518
|
-
}
|
|
3519
|
-
return changes;
|
|
3520
|
-
}
|
|
3521
|
-
|
|
3522
|
-
function findByPath(node, path) {
|
|
3523
|
-
if (!path.length) {
|
|
3524
|
-
// If this is an InlineItem containing a KeyValue, return the KeyValue
|
|
3525
|
-
if (isInlineItem(node) && isKeyValue(node.item)) {
|
|
3526
|
-
return node.item;
|
|
3527
|
-
}
|
|
3528
|
-
return node;
|
|
3529
|
-
}
|
|
3530
|
-
if (isKeyValue(node)) {
|
|
3531
|
-
return findByPath(node.value, path);
|
|
3532
|
-
}
|
|
3533
|
-
const indexes = {};
|
|
3534
|
-
let found;
|
|
3535
|
-
if (hasItems(node)) {
|
|
3536
|
-
node.items.some((item, index) => {
|
|
3537
|
-
try {
|
|
3538
|
-
let key = [];
|
|
3539
|
-
if (isKeyValue(item)) {
|
|
3540
|
-
key = item.key.value;
|
|
3541
|
-
}
|
|
3542
|
-
else if (isTable(item)) {
|
|
3543
|
-
key = item.key.item.value;
|
|
3544
|
-
}
|
|
3545
|
-
else if (isTableArray(item)) {
|
|
3546
|
-
key = item.key.item.value;
|
|
3547
|
-
const key_string = stableStringify(key);
|
|
3548
|
-
if (!indexes[key_string]) {
|
|
3549
|
-
indexes[key_string] = 0;
|
|
3550
|
-
}
|
|
3551
|
-
const array_index = indexes[key_string]++;
|
|
3552
|
-
key = key.concat(array_index);
|
|
3553
|
-
}
|
|
3554
|
-
else if (isInlineItem(item) && isKeyValue(item.item)) {
|
|
3555
|
-
// For InlineItems wrapping KeyValues, extract the key
|
|
3556
|
-
key = item.item.key.value;
|
|
3557
|
-
}
|
|
3558
|
-
else if (isInlineItem(item)) {
|
|
3559
|
-
key = [index];
|
|
3560
|
-
}
|
|
3561
|
-
if (key.length && arraysEqual(key, path.slice(0, key.length))) {
|
|
3562
|
-
// For InlineItems containing KeyValues, we need to search within the value
|
|
3563
|
-
// but still return the InlineItem or its contents appropriately
|
|
3564
|
-
if (isInlineItem(item) && isKeyValue(item.item)) {
|
|
3565
|
-
if (path.length === key.length) {
|
|
3566
|
-
// If we've matched the full path, return the InlineItem itself
|
|
3567
|
-
// so it can be found and replaced in the parent's items array
|
|
3568
|
-
found = item;
|
|
3569
|
-
}
|
|
3570
|
-
else {
|
|
3571
|
-
// Continue searching within the KeyValue's value
|
|
3572
|
-
found = findByPath(item.item.value, path.slice(key.length));
|
|
3573
|
-
}
|
|
3574
|
-
}
|
|
3575
|
-
else {
|
|
3576
|
-
found = findByPath(item, path.slice(key.length));
|
|
3577
|
-
}
|
|
3578
|
-
return true;
|
|
3579
|
-
}
|
|
3580
|
-
else {
|
|
3581
|
-
return false;
|
|
3582
|
-
}
|
|
3583
|
-
}
|
|
3584
|
-
catch (err) {
|
|
3585
|
-
return false;
|
|
3586
|
-
}
|
|
3587
|
-
});
|
|
3588
|
-
}
|
|
3589
|
-
if (!found) {
|
|
3590
|
-
throw new Error(`Could not find node at path ${path.join('.')}`);
|
|
3591
|
-
}
|
|
3592
|
-
return found;
|
|
3593
|
-
}
|
|
3594
|
-
function tryFindByPath(node, path) {
|
|
3595
|
-
try {
|
|
3596
|
-
return findByPath(node, path);
|
|
3597
|
-
}
|
|
3598
|
-
catch (err) { }
|
|
3599
|
-
}
|
|
3600
|
-
function findParent(node, path) {
|
|
3601
|
-
let parent_path = path;
|
|
3602
|
-
let parent;
|
|
3603
|
-
while (parent_path.length && !parent) {
|
|
3604
|
-
parent_path = parent_path.slice(0, -1);
|
|
3605
|
-
parent = tryFindByPath(node, parent_path);
|
|
3606
|
-
}
|
|
3607
|
-
if (!parent) {
|
|
3608
|
-
throw new Error(`Count not find parent node for path ${path.join('.')}`);
|
|
3609
|
-
}
|
|
3610
|
-
return parent;
|
|
3611
|
-
}
|
|
3612
|
-
|
|
3613
|
-
/**
|
|
3614
|
-
* Applies modifications to a TOML document by comparing an existing TOML string with updated JavaScript data.
|
|
3615
|
-
*
|
|
3616
|
-
* This function preserves formatting and comments from the existing TOML document while
|
|
3617
|
-
* applying changes from the updated data structure. It performs a diff between the existing
|
|
3618
|
-
* and updated data, then strategically applies only the necessary changes to maintain the
|
|
3619
|
-
* original document structure as much as possible.
|
|
3620
|
-
*
|
|
3621
|
-
* @param existing - The original TOML document as a string
|
|
3622
|
-
* @param updated - The updated JavaScript object with desired changes
|
|
3623
|
-
* @param format - Optional formatting options to apply to new or modified sections
|
|
3624
|
-
* @returns A new TOML string with the changes applied
|
|
3625
|
-
*/
|
|
3626
|
-
function patch(existing, updated, format) {
|
|
3627
|
-
const existing_ast = parseTOML(existing);
|
|
3628
|
-
// Auto-detect formatting preferences from the existing TOML string for fallback
|
|
3629
|
-
const autoDetectedFormat = TomlFormat.autoDetectFormat(existing);
|
|
3630
|
-
const fmt = resolveTomlFormat(format, autoDetectedFormat);
|
|
3631
|
-
return patchAst(existing_ast, updated, fmt).tomlString;
|
|
3632
|
-
}
|
|
3633
|
-
function patchAst(existing_ast, updated, format) {
|
|
3634
|
-
const items = [...existing_ast];
|
|
3635
|
-
const existing_js = toJS(items);
|
|
3636
|
-
const existing_document = {
|
|
3637
|
-
type: NodeType.Document,
|
|
3638
|
-
loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 0 } },
|
|
3639
|
-
items
|
|
3640
|
-
};
|
|
3641
|
-
// Certain formatting options should not be applied to the updated document during patching, because it would
|
|
3642
|
-
// override the existing formatting too aggressively. For example, preferNestedTablesMultiline would
|
|
3643
|
-
// convert all nested tables to multiline, which is not be desired during patching.
|
|
3644
|
-
// Therefore, we create a modified format for generating the updated document used for diffing.
|
|
3645
|
-
const diffing_fmt = resolveTomlFormat(Object.assign(Object.assign({}, format), { inlineTableStart: undefined }), format);
|
|
3646
|
-
const updated_document = parseJS(updated, diffing_fmt);
|
|
3647
|
-
const changes = reorder(diff(existing_js, updated));
|
|
3648
|
-
if (changes.length === 0) {
|
|
3649
|
-
return {
|
|
3650
|
-
tomlString: toTOML(items, format),
|
|
3651
|
-
document: existing_document
|
|
3652
|
-
};
|
|
3653
|
-
}
|
|
3654
|
-
const patched_document = applyChanges(existing_document, updated_document, changes, format);
|
|
3655
|
-
// Validate the patched_document
|
|
3656
|
-
// This would prevent overlapping element positions in the AST, but since those are handled at stringification time, we can skip this for now
|
|
3657
|
-
//validate(patched_document);
|
|
3658
|
-
return {
|
|
3659
|
-
tomlString: toTOML(patched_document.items, format),
|
|
3660
|
-
document: patched_document
|
|
3661
|
-
};
|
|
3662
|
-
}
|
|
3663
|
-
function reorder(changes) {
|
|
3664
|
-
for (let i = 0; i < changes.length; i++) {
|
|
3665
|
-
const change = changes[i];
|
|
3666
|
-
if (isRemove(change)) {
|
|
3667
|
-
let j = i + 1;
|
|
3668
|
-
while (j < changes.length) {
|
|
3669
|
-
const next_change = changes[j];
|
|
3670
|
-
if (isRemove(next_change) && next_change.path[0] === change.path[0] &&
|
|
3671
|
-
next_change.path[1] > change.path[1]) {
|
|
3672
|
-
changes.splice(j, 1);
|
|
3673
|
-
changes.splice(i, 0, next_change);
|
|
3674
|
-
// We reset i to the beginning of the loop to avoid skipping any changes
|
|
3675
|
-
i = 0;
|
|
3676
|
-
break;
|
|
3677
|
-
}
|
|
3678
|
-
j++;
|
|
3679
|
-
}
|
|
3680
|
-
}
|
|
3681
|
-
}
|
|
3682
|
-
return changes;
|
|
3683
|
-
}
|
|
3684
|
-
/**
|
|
3685
|
-
* Preserves formatting from the existing node when applying it to the replacement node.
|
|
3686
|
-
* This includes multiline string formats, trailing commas, DateTime formats, etc.
|
|
3687
|
-
*
|
|
3688
|
-
* @param existing - The existing node with formatting to preserve
|
|
3689
|
-
* @param replacement - The replacement node to apply formatting to
|
|
3690
|
-
*/
|
|
3691
|
-
function preserveFormatting(existing, replacement) {
|
|
3692
|
-
// Preserve multiline string format
|
|
3693
|
-
if (isString$1(existing) && isString$1(replacement) && isMultilineString(existing.raw)) {
|
|
3694
|
-
// Generate new string node with preserved multiline format
|
|
3695
|
-
const newString = generateString(replacement.value, existing.raw);
|
|
3696
|
-
replacement.raw = newString.raw;
|
|
3697
|
-
replacement.loc = newString.loc;
|
|
3698
|
-
}
|
|
3699
|
-
// Preserve DateTime format
|
|
3700
|
-
if (isDateTime(existing) && isDateTime(replacement)) {
|
|
3701
|
-
// Analyze the original raw format and create a properly formatted replacement
|
|
3702
|
-
const originalRaw = existing.raw;
|
|
3703
|
-
const newValue = replacement.value;
|
|
3704
|
-
// Create a new date with the original format preserved
|
|
3705
|
-
const formattedDate = DateFormatHelper.createDateWithOriginalFormat(newValue, originalRaw);
|
|
3706
|
-
// Update the replacement with the properly formatted date
|
|
3707
|
-
replacement.value = formattedDate;
|
|
3708
|
-
replacement.raw = formattedDate.toISOString();
|
|
3709
|
-
// Adjust the location information to match the new raw length
|
|
3710
|
-
const lengthDiff = replacement.raw.length - originalRaw.length;
|
|
3711
|
-
if (lengthDiff !== 0) {
|
|
3712
|
-
replacement.loc.end.column = replacement.loc.start.column + replacement.raw.length;
|
|
3713
|
-
}
|
|
3714
|
-
}
|
|
3715
|
-
// Preserve array trailing comma format
|
|
3716
|
-
if (isInlineArray(existing) && isInlineArray(replacement)) {
|
|
3717
|
-
const originalHadTrailingCommas = arrayHadTrailingCommas(existing);
|
|
3718
|
-
if (replacement.items.length > 0) {
|
|
3719
|
-
const lastItem = replacement.items[replacement.items.length - 1];
|
|
3720
|
-
lastItem.comma = originalHadTrailingCommas;
|
|
3721
|
-
}
|
|
3722
|
-
}
|
|
3723
|
-
// Preserve inline table trailing comma format
|
|
3724
|
-
if (isInlineTable(existing) && isInlineTable(replacement)) {
|
|
3725
|
-
const originalHadTrailingCommas = tableHadTrailingCommas(existing);
|
|
3726
|
-
if (replacement.items.length > 0) {
|
|
3727
|
-
const lastItem = replacement.items[replacement.items.length - 1];
|
|
3728
|
-
lastItem.comma = originalHadTrailingCommas;
|
|
3729
|
-
}
|
|
3730
|
-
}
|
|
3731
|
-
}
|
|
3732
|
-
/**
|
|
3733
|
-
* Applies a list of changes to the original TOML document AST while preserving formatting and structure.
|
|
3734
|
-
*
|
|
3735
|
-
* This function processes different types of changes (Add, Edit, Remove, Move, Rename) and applies them
|
|
3736
|
-
* to the original document in a way that maintains the existing formatting preferences, comments, and
|
|
3737
|
-
* structural elements as much as possible. Special handling is provided for different node types like
|
|
3738
|
-
* inline tables, arrays, and table arrays to ensure proper formatting consistency.
|
|
3739
|
-
*
|
|
3740
|
-
* @param original - The original TOML document AST to be modified
|
|
3741
|
-
* @param updated - The updated document AST containing new values for changes
|
|
3742
|
-
* @param changes - Array of change objects describing what modifications to apply
|
|
3743
|
-
* @param format - Formatting preferences to use for newly added elements
|
|
3744
|
-
* @returns The modified original document with all changes applied
|
|
3745
|
-
*
|
|
3746
|
-
* @example
|
|
3747
|
-
* ```typescript
|
|
3748
|
-
* const changes = [
|
|
3749
|
-
* { type: 'add', path: ['newKey'], value: 'newValue' },
|
|
3750
|
-
* { type: 'edit', path: ['existingKey'], value: 'updatedValue' }
|
|
3751
|
-
* ];
|
|
3752
|
-
* const result = applyChanges(originalDoc, updatedDoc, changes, format);
|
|
3753
|
-
* ```
|
|
3754
|
-
*/
|
|
3755
|
-
function applyChanges(original, updated, changes, format) {
|
|
3756
|
-
// Potential Changes:
|
|
3757
|
-
//
|
|
3758
|
-
// Add: Add key-value to object, add item to array
|
|
3759
|
-
// Edit: Change in value
|
|
3760
|
-
// Remove: Remove key-value from object, remove item from array
|
|
3761
|
-
// Move: Move item in array
|
|
3762
|
-
// Rename: Rename key in key-value
|
|
3763
|
-
//
|
|
3764
|
-
// Special consideration, inline comments need to move as-needed
|
|
3765
|
-
changes.forEach(change => {
|
|
3766
|
-
if (isAdd(change)) {
|
|
3767
|
-
const child = findByPath(updated, change.path);
|
|
3768
|
-
const parent_path = change.path.slice(0, -1);
|
|
3769
|
-
let index = last(change.path);
|
|
3770
|
-
let is_table_array = isTableArray(child);
|
|
3771
|
-
if (isInteger(index) && !parent_path.some(isInteger)) {
|
|
3772
|
-
const sibling = tryFindByPath(original, parent_path.concat(0));
|
|
3773
|
-
if (sibling && isTableArray(sibling)) {
|
|
3774
|
-
is_table_array = true;
|
|
3775
|
-
}
|
|
3776
|
-
}
|
|
3777
|
-
// Determine the parent node where the new child will be inserted
|
|
3778
|
-
let parent;
|
|
3779
|
-
if (isTable(child)) {
|
|
3780
|
-
parent = original;
|
|
3781
|
-
}
|
|
3782
|
-
else if (is_table_array) {
|
|
3783
|
-
parent = original;
|
|
3784
|
-
// The index needs to be updated to top-level items
|
|
3785
|
-
// to properly account for other items, comments, and nesting
|
|
3786
|
-
const document = original;
|
|
3787
|
-
const before = tryFindByPath(document, parent_path.concat(index - 1));
|
|
3788
|
-
const after = tryFindByPath(document, parent_path.concat(index));
|
|
3789
|
-
if (after) {
|
|
3790
|
-
index = document.items.indexOf(after);
|
|
3791
|
-
}
|
|
3792
|
-
else if (before) {
|
|
3793
|
-
index = document.items.indexOf(before) + 1;
|
|
3794
|
-
}
|
|
3795
|
-
else {
|
|
3796
|
-
index = document.items.length;
|
|
3797
|
-
}
|
|
3798
|
-
}
|
|
3799
|
-
else {
|
|
3800
|
-
parent = findParent(original, change.path);
|
|
3801
|
-
if (isKeyValue(parent)) {
|
|
3802
|
-
parent = parent.value;
|
|
3803
|
-
}
|
|
3804
|
-
}
|
|
3805
|
-
if (isTableArray(parent) || isInlineArray(parent) || isDocument(parent)) {
|
|
3806
|
-
// Special handling for InlineArray: preserve original trailing comma format
|
|
3807
|
-
if (isInlineArray(parent)) {
|
|
3808
|
-
const originalHadTrailingCommas = arrayHadTrailingCommas(parent);
|
|
3809
|
-
// If this is an InlineItem being added to an array, check its comma setting
|
|
3810
|
-
if (isInlineItem(child)) {
|
|
3811
|
-
// The child comes from the updated document with global format applied
|
|
3812
|
-
// Override with the original array's format
|
|
3813
|
-
child.comma = originalHadTrailingCommas;
|
|
3814
|
-
}
|
|
3815
|
-
}
|
|
3816
|
-
// Check if we should convert nested inline tables to multiline tables
|
|
3817
|
-
if (format.inlineTableStart !== undefined && format.inlineTableStart > 0 && isDocument(parent) && isTable(child)) {
|
|
3818
|
-
const additionalTables = convertNestedInlineTablesToMultiline(child, original, format);
|
|
3819
|
-
// Insert the main table first
|
|
3820
|
-
insert(original, parent, child, index);
|
|
3821
|
-
// Then insert all the additional tables
|
|
3822
|
-
for (const table of additionalTables) {
|
|
3823
|
-
insert(original, original, table, undefined);
|
|
3824
|
-
}
|
|
3825
|
-
}
|
|
3826
|
-
else {
|
|
3827
|
-
insert(original, parent, child, index);
|
|
3828
|
-
}
|
|
3829
|
-
}
|
|
3830
|
-
else if (isInlineTable(parent)) {
|
|
3831
|
-
// Special handling for adding KeyValue to InlineTable
|
|
3832
|
-
// Preserve original trailing comma format
|
|
3833
|
-
const originalHadTrailingCommas = tableHadTrailingCommas(parent);
|
|
3834
|
-
// InlineTable items must be wrapped in InlineItem
|
|
3835
|
-
if (isKeyValue(child)) {
|
|
3836
|
-
const inlineItem = generateInlineItem(child);
|
|
3837
|
-
// Override with the original table's format
|
|
3838
|
-
inlineItem.comma = originalHadTrailingCommas;
|
|
3839
|
-
insert(original, parent, inlineItem);
|
|
3840
|
-
}
|
|
3841
|
-
else {
|
|
3842
|
-
insert(original, parent, child);
|
|
3843
|
-
}
|
|
3844
|
-
}
|
|
3845
|
-
else {
|
|
3846
|
-
// Check if we should convert inline tables to multiline tables when adding to existing tables
|
|
3847
|
-
if (format.inlineTableStart !== undefined && format.inlineTableStart > 0 && isKeyValue(child) && isInlineTable(child.value) && isTable(parent)) {
|
|
3848
|
-
// Calculate the depth of the inline table that would be created
|
|
3849
|
-
const baseTableKey = parent.key.item.value;
|
|
3850
|
-
const nestedTableKey = [...baseTableKey, ...child.key.value];
|
|
3851
|
-
const depth = calculateTableDepth(nestedTableKey);
|
|
3852
|
-
// Convert to separate section only if depth is less than inlineTableStart
|
|
3853
|
-
if (depth < format.inlineTableStart) {
|
|
3854
|
-
convertInlineTableToSeparateSection(child, parent, original, format);
|
|
3855
|
-
}
|
|
3856
|
-
else {
|
|
3857
|
-
insert(original, parent, child);
|
|
3858
|
-
}
|
|
3859
|
-
}
|
|
3860
|
-
else if (format.inlineTableStart === 0 && isKeyValue(child) && isInlineTable(child.value) && isDocument(parent)) {
|
|
3861
|
-
insert(original, parent, child, undefined, true);
|
|
3862
|
-
}
|
|
3863
|
-
else {
|
|
3864
|
-
// Unwrap InlineItem if we're adding to a Table (not InlineTable)
|
|
3865
|
-
// InlineItems should only exist within InlineTables or InlineArrays
|
|
3866
|
-
let childToInsert = child;
|
|
3867
|
-
if (isInlineItem(child) && (isTable(parent) || isDocument(parent))) {
|
|
3868
|
-
childToInsert = child.item;
|
|
3869
|
-
}
|
|
3870
|
-
insert(original, parent, childToInsert);
|
|
3871
|
-
}
|
|
3872
|
-
}
|
|
3873
|
-
}
|
|
3874
|
-
else if (isEdit(change)) {
|
|
3875
|
-
let existing = findByPath(original, change.path);
|
|
3876
|
-
let replacement = findByPath(updated, change.path);
|
|
3877
|
-
let parent;
|
|
3878
|
-
if (isKeyValue(existing) && isKeyValue(replacement)) {
|
|
3879
|
-
// Edit for key-value means value changes
|
|
3880
|
-
// Preserve formatting from existing value in replacement value
|
|
3881
|
-
preserveFormatting(existing.value, replacement.value);
|
|
3882
|
-
parent = existing;
|
|
3883
|
-
existing = existing.value;
|
|
3884
|
-
replacement = replacement.value;
|
|
3885
|
-
}
|
|
3886
|
-
else if (isKeyValue(existing) && isInlineItem(replacement) && isKeyValue(replacement.item)) {
|
|
3887
|
-
// Sometimes, the replacement looks like it could be an inline item, but the original is a key-value
|
|
3888
|
-
// In this case, we convert the replacement to a key-value to match the original
|
|
3889
|
-
parent = existing;
|
|
3890
|
-
existing = existing.value;
|
|
3891
|
-
replacement = replacement.item.value;
|
|
3892
|
-
}
|
|
3893
|
-
else if (isInlineItem(existing) && isKeyValue(replacement)) {
|
|
3894
|
-
// Editing inline table item: existing is InlineItem, replacement is KeyValue
|
|
3895
|
-
// We need to replace the KeyValue inside the InlineItem, preserving the InlineItem wrapper
|
|
3896
|
-
parent = existing;
|
|
3897
|
-
existing = existing.item;
|
|
3898
|
-
}
|
|
3899
|
-
else if (isInlineItem(existing) && isInlineItem(replacement) && isKeyValue(existing.item) && isKeyValue(replacement.item)) {
|
|
3900
|
-
// Both are InlineItems wrapping KeyValues (nested inline table edits)
|
|
3901
|
-
// Preserve formatting and edit the value within
|
|
3902
|
-
preserveFormatting(existing.item.value, replacement.item.value);
|
|
3903
|
-
parent = existing.item;
|
|
3904
|
-
existing = existing.item.value;
|
|
3905
|
-
replacement = replacement.item.value;
|
|
3906
|
-
}
|
|
3907
|
-
else {
|
|
3908
|
-
parent = findParent(original, change.path);
|
|
3909
|
-
// Special handling for array element edits
|
|
3910
|
-
if (isKeyValue(parent)) {
|
|
3911
|
-
// Check if we're actually editing an array element
|
|
3912
|
-
const parentPath = change.path.slice(0, -1);
|
|
3913
|
-
const arrayNode = findByPath(original, parentPath);
|
|
3914
|
-
if (isKeyValue(arrayNode) && isInlineArray(arrayNode.value)) {
|
|
3915
|
-
parent = arrayNode.value;
|
|
3916
|
-
}
|
|
3917
|
-
}
|
|
3918
|
-
}
|
|
3919
|
-
replace(original, parent, existing, replacement);
|
|
3920
|
-
}
|
|
3921
|
-
else if (isRemove(change)) {
|
|
3922
|
-
let parent = findParent(original, change.path);
|
|
3923
|
-
if (isKeyValue(parent))
|
|
3924
|
-
parent = parent.value;
|
|
3925
|
-
const node = findByPath(original, change.path);
|
|
3926
|
-
remove(original, parent, node);
|
|
3927
|
-
}
|
|
3928
|
-
else if (isMove(change)) {
|
|
3929
|
-
let parent = findByPath(original, change.path);
|
|
3930
|
-
if (hasItem(parent))
|
|
3931
|
-
parent = parent.item;
|
|
3932
|
-
if (isKeyValue(parent))
|
|
3933
|
-
parent = parent.value;
|
|
3934
|
-
const node = parent.items[change.from];
|
|
3935
|
-
remove(original, parent, node);
|
|
3936
|
-
insert(original, parent, node, change.to);
|
|
3937
|
-
}
|
|
3938
|
-
else if (isRename(change)) {
|
|
3939
|
-
let parent = findByPath(original, change.path.concat(change.from));
|
|
3940
|
-
let replacement = findByPath(updated, change.path.concat(change.to));
|
|
3941
|
-
if (hasItem(parent))
|
|
3942
|
-
parent = parent.item;
|
|
3943
|
-
if (hasItem(replacement))
|
|
3944
|
-
replacement = replacement.item;
|
|
3945
|
-
replace(original, parent, parent.key, replacement.key);
|
|
3946
|
-
}
|
|
3947
|
-
});
|
|
3948
|
-
applyWrites(original);
|
|
3949
|
-
return original;
|
|
3950
|
-
}
|
|
3951
|
-
/**
|
|
3952
|
-
* Converts nested inline tables to separate table sections based on the inlineTableStart depth setting.
|
|
3953
|
-
* This function recursively processes a table and extracts any inline tables within it,
|
|
3954
|
-
* creating separate table sections with properly nested keys.
|
|
3955
|
-
*
|
|
3956
|
-
* @param table - The table to process for nested inline tables
|
|
3957
|
-
* @param original - The original document for inserting new items
|
|
3958
|
-
* @param format - The formatting options
|
|
3959
|
-
* @returns Array of additional tables that should be added to the document
|
|
3960
|
-
*/
|
|
3961
|
-
function convertNestedInlineTablesToMultiline(table, original, format) {
|
|
3962
|
-
const additionalTables = [];
|
|
3963
|
-
const processTableForNestedInlines = (currentTable, tablesToAdd) => {
|
|
3964
|
-
var _a;
|
|
3965
|
-
for (let i = currentTable.items.length - 1; i >= 0; i--) {
|
|
3966
|
-
const item = currentTable.items[i];
|
|
3967
|
-
if (isKeyValue(item) && isInlineTable(item.value)) {
|
|
3968
|
-
// Calculate the depth of this nested table
|
|
3969
|
-
const nestedTableKey = [...currentTable.key.item.value, ...item.key.value];
|
|
3970
|
-
const depth = calculateTableDepth(nestedTableKey);
|
|
3971
|
-
// Only convert to separate table if depth is less than inlineTableStart
|
|
3972
|
-
if (depth < ((_a = format.inlineTableStart) !== null && _a !== void 0 ? _a : 1) && format.inlineTableStart !== 0) {
|
|
3973
|
-
// Convert this inline table to a separate table section
|
|
3974
|
-
const separateTable = generateTable(nestedTableKey);
|
|
3975
|
-
// Move all items from the inline table to the separate table
|
|
3976
|
-
for (const inlineItem of item.value.items) {
|
|
3977
|
-
if (isInlineItem(inlineItem) && isKeyValue(inlineItem.item)) {
|
|
3978
|
-
insert(original, separateTable, inlineItem.item, undefined);
|
|
3979
|
-
}
|
|
3980
|
-
}
|
|
3981
|
-
// Remove this item from the original table
|
|
3982
|
-
currentTable.items.splice(i, 1);
|
|
3983
|
-
// Update the parent table's end position after removal
|
|
3984
|
-
postInlineItemRemovalAdjustment(currentTable);
|
|
3985
|
-
// Queue this table to be added to the document
|
|
3986
|
-
tablesToAdd.push(separateTable);
|
|
3987
|
-
// Recursively process the new table for further nested inlines
|
|
3988
|
-
processTableForNestedInlines(separateTable, tablesToAdd);
|
|
3989
|
-
}
|
|
3990
|
-
}
|
|
3991
|
-
}
|
|
3992
|
-
};
|
|
3993
|
-
processTableForNestedInlines(table, additionalTables);
|
|
3994
|
-
return additionalTables;
|
|
3995
|
-
}
|
|
3996
|
-
/**
|
|
3997
|
-
* Converts an inline table to a separate table section when adding to an existing table.
|
|
3998
|
-
* This function creates a new table section with the combined key path and moves all
|
|
3999
|
-
* properties from the inline table to the separate table section.
|
|
4000
|
-
*
|
|
4001
|
-
* @param child - The KeyValue node with an InlineTable as its value
|
|
4002
|
-
* @param parent - The parent table where the KeyValue would be added
|
|
4003
|
-
* @param original - The original document for inserting new items
|
|
4004
|
-
* @param format - The formatting options
|
|
4005
|
-
*/
|
|
4006
|
-
function convertInlineTableToSeparateSection(child, parent, original, format) {
|
|
4007
|
-
// Convert the inline table to a separate table section
|
|
4008
|
-
const baseTableKey = parent.key.item.value; // Get the parent table's key path
|
|
4009
|
-
const nestedTableKey = [...baseTableKey, ...child.key.value]; // Combine with the new key
|
|
4010
|
-
const separateTable = generateTable(nestedTableKey);
|
|
4011
|
-
// We know child.value is an InlineTable from the calling context
|
|
4012
|
-
if (isInlineTable(child.value)) {
|
|
4013
|
-
// Move all items from the inline table to the separate table
|
|
4014
|
-
for (const inlineItem of child.value.items) {
|
|
4015
|
-
if (isInlineItem(inlineItem) && isKeyValue(inlineItem.item)) {
|
|
4016
|
-
insert(original, separateTable, inlineItem.item, undefined);
|
|
4017
|
-
}
|
|
4018
|
-
}
|
|
4019
|
-
}
|
|
4020
|
-
// Add the separate table to the document
|
|
4021
|
-
insert(original, original, separateTable, undefined);
|
|
4022
|
-
// Update the parent table's end position since we're not adding the inline table to it
|
|
4023
|
-
postInlineItemRemovalAdjustment(parent);
|
|
4024
|
-
// Also handle any nested inline tables within the new table
|
|
4025
|
-
const additionalTables = convertNestedInlineTablesToMultiline(separateTable, original, format);
|
|
4026
|
-
for (const table of additionalTables) {
|
|
4027
|
-
insert(original, original, table, undefined);
|
|
4028
|
-
}
|
|
4029
|
-
}
|
|
4030
|
-
|
|
4031
|
-
/******************************************************************************
|
|
4032
|
-
Copyright (c) Microsoft Corporation.
|
|
4033
|
-
|
|
4034
|
-
Permission to use, copy, modify, and/or distribute this software for any
|
|
4035
|
-
purpose with or without fee is hereby granted.
|
|
4036
|
-
|
|
4037
|
-
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
4038
|
-
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
4039
|
-
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
4040
|
-
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
4041
|
-
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
4042
|
-
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
4043
|
-
PERFORMANCE OF THIS SOFTWARE.
|
|
4044
|
-
***************************************************************************** */
|
|
4045
|
-
/* global Reflect, Promise, SuppressedError, Symbol, Iterator */
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
function __classPrivateFieldGet(receiver, state, kind, f) {
|
|
4049
|
-
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
|
|
4050
|
-
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");
|
|
4051
|
-
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
4052
|
-
}
|
|
4053
|
-
|
|
4054
|
-
function __classPrivateFieldSet(receiver, state, value, kind, f) {
|
|
4055
|
-
if (kind === "m") throw new TypeError("Private method is not writable");
|
|
4056
|
-
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
|
|
4057
|
-
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");
|
|
4058
|
-
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
|
|
4059
|
-
}
|
|
4060
|
-
|
|
4061
|
-
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
|
|
4062
|
-
var e = new Error(message);
|
|
4063
|
-
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
4064
|
-
};
|
|
4065
|
-
|
|
4066
|
-
/**
|
|
4067
|
-
* Compares two positions to determine their ordering.
|
|
4068
|
-
* @param pos1 - First position
|
|
4069
|
-
* @param pos2 - Second position
|
|
4070
|
-
* @returns Negative if pos1 < pos2, 0 if equal, positive if pos1 > pos2
|
|
4071
|
-
*/
|
|
4072
|
-
function comparePositions(pos1, pos2) {
|
|
4073
|
-
if (pos1.line !== pos2.line) {
|
|
4074
|
-
return pos1.line - pos2.line;
|
|
4075
|
-
}
|
|
4076
|
-
return pos1.column - pos2.column;
|
|
4077
|
-
}
|
|
4078
|
-
/**
|
|
4079
|
-
* Truncates an AST based on a position (line, column) in the source string.
|
|
4080
|
-
*
|
|
4081
|
-
* This function filters the AST to include only the nodes that end before
|
|
4082
|
-
* the specified position. This ensures that blocks containing changes are
|
|
4083
|
-
* excluded and can be reparsed. This is useful for incremental parsing scenarios
|
|
4084
|
-
* where you want to keep only the unchanged portion of the AST.
|
|
4085
|
-
*
|
|
4086
|
-
* Special handling: If the truncation point falls within a Table or TableArray
|
|
4087
|
-
* (e.g., in a comment inside the table), the entire table is excluded to ensure
|
|
4088
|
-
* proper reparsing.
|
|
4089
|
-
*
|
|
4090
|
-
* @param ast - The AST to truncate
|
|
4091
|
-
* @param line - The line number (1-indexed) at which to truncate
|
|
4092
|
-
* @param column - The column number (0-indexed) at which to truncate
|
|
4093
|
-
* @returns An object containing the truncated AST and the end position of the last included node
|
|
4094
|
-
*
|
|
4095
|
-
* @example
|
|
4096
|
-
* ```typescript
|
|
4097
|
-
* const ast = parseTOML(tomlString);
|
|
4098
|
-
* // Get AST up to line 5, column 10 (only nodes that end before this position)
|
|
4099
|
-
* const { truncatedAst, lastEndPosition } = truncateAst(ast, 5, 10);
|
|
4100
|
-
* for (const node of truncatedAst) {
|
|
4101
|
-
* // process node
|
|
4102
|
-
* }
|
|
4103
|
-
* ```
|
|
4104
|
-
*/
|
|
4105
|
-
function truncateAst(ast, line, column) {
|
|
4106
|
-
const limit = { line, column };
|
|
4107
|
-
const nodes = [];
|
|
4108
|
-
let lastEndPosition = null;
|
|
4109
|
-
for (const node of ast) {
|
|
4110
|
-
const nodeEndsBeforeLimit = comparePositions(node.loc.end, limit) < 0;
|
|
4111
|
-
const nodeStartsBeforeLimit = comparePositions(node.loc.start, limit) < 0;
|
|
4112
|
-
if (nodeEndsBeforeLimit) {
|
|
4113
|
-
// Node completely ends before the limit - include it
|
|
4114
|
-
nodes.push(node);
|
|
4115
|
-
lastEndPosition = node.loc.end;
|
|
4116
|
-
}
|
|
4117
|
-
else if (nodeStartsBeforeLimit && !nodeEndsBeforeLimit) {
|
|
4118
|
-
// Node starts before the limit but ends at or after it
|
|
4119
|
-
// This means the truncation point is within this node
|
|
4120
|
-
// For Table/TableArray nodes, don't include them if the change is inside
|
|
4121
|
-
// This ensures the entire table gets reparsed
|
|
4122
|
-
break;
|
|
4123
|
-
}
|
|
4124
|
-
else {
|
|
4125
|
-
// Node starts at or after the limit - stop
|
|
4126
|
-
break;
|
|
4127
|
-
}
|
|
4128
|
-
}
|
|
4129
|
-
return {
|
|
4130
|
-
truncatedAst: nodes,
|
|
4131
|
-
lastEndPosition
|
|
4132
|
-
};
|
|
4133
|
-
}
|
|
4134
|
-
|
|
4135
|
-
var _TomlDocument_ast, _TomlDocument_currentTomlString, _TomlDocument_Format;
|
|
4136
|
-
/**
|
|
4137
|
-
* TomlDocument encapsulates a TOML AST and provides methods to interact with it.
|
|
4138
|
-
*/
|
|
4139
|
-
class TomlDocument {
|
|
4140
|
-
/**
|
|
4141
|
-
* Initializes the TomlDocument with a TOML string, parsing it into an AST.
|
|
4142
|
-
* @param tomlString - The TOML string to parse
|
|
4143
|
-
*/
|
|
4144
|
-
constructor(tomlString) {
|
|
4145
|
-
_TomlDocument_ast.set(this, void 0);
|
|
4146
|
-
_TomlDocument_currentTomlString.set(this, void 0);
|
|
4147
|
-
_TomlDocument_Format.set(this, void 0);
|
|
4148
|
-
__classPrivateFieldSet(this, _TomlDocument_currentTomlString, tomlString, "f");
|
|
4149
|
-
__classPrivateFieldSet(this, _TomlDocument_ast, Array.from(parseTOML(tomlString)), "f");
|
|
4150
|
-
// Auto-detect formatting preferences from the original TOML string
|
|
4151
|
-
__classPrivateFieldSet(this, _TomlDocument_Format, TomlFormat.autoDetectFormat(tomlString), "f");
|
|
4152
|
-
}
|
|
4153
|
-
get toTomlString() {
|
|
4154
|
-
return __classPrivateFieldGet(this, _TomlDocument_currentTomlString, "f");
|
|
4155
|
-
}
|
|
4156
|
-
/**
|
|
4157
|
-
* Returns the JavaScript object representation of the TOML document.
|
|
4158
|
-
*/
|
|
4159
|
-
get toJsObject() {
|
|
4160
|
-
const jsObject = toJS(__classPrivateFieldGet(this, _TomlDocument_ast, "f"));
|
|
4161
|
-
// Convert custom date classes to regular JavaScript Date objects
|
|
4162
|
-
return convertCustomDateClasses(jsObject);
|
|
4163
|
-
}
|
|
4164
|
-
/**
|
|
4165
|
-
* Returns the internal AST (for testing purposes).
|
|
4166
|
-
* @internal
|
|
4167
|
-
*/
|
|
4168
|
-
get ast() {
|
|
4169
|
-
return __classPrivateFieldGet(this, _TomlDocument_ast, "f");
|
|
4170
|
-
}
|
|
4171
|
-
/**
|
|
4172
|
-
* Applies a patch to the current AST using a modified JS object.
|
|
4173
|
-
* Updates the internal AST. Use toTomlString getter to retrieve the updated TOML string.
|
|
4174
|
-
* @param updatedObject - The modified JS object to patch with
|
|
4175
|
-
* @param format - Optional formatting options
|
|
4176
|
-
*/
|
|
4177
|
-
patch(updatedObject, format) {
|
|
4178
|
-
const fmt = resolveTomlFormat(format, __classPrivateFieldGet(this, _TomlDocument_Format, "f"));
|
|
4179
|
-
const { tomlString, document } = patchAst(__classPrivateFieldGet(this, _TomlDocument_ast, "f"), updatedObject, fmt);
|
|
4180
|
-
__classPrivateFieldSet(this, _TomlDocument_ast, document.items, "f");
|
|
4181
|
-
__classPrivateFieldSet(this, _TomlDocument_currentTomlString, tomlString, "f");
|
|
4182
|
-
}
|
|
4183
|
-
/**
|
|
4184
|
-
* Updates the internal document by supplying a modified tomlString.
|
|
4185
|
-
* Use toJsObject getter to retrieve the updated JS object representation.
|
|
4186
|
-
* @param tomlString - The modified TOML string to update with
|
|
4187
|
-
*/
|
|
4188
|
-
update(tomlString) {
|
|
4189
|
-
if (tomlString === this.toTomlString) {
|
|
4190
|
-
return;
|
|
4191
|
-
}
|
|
4192
|
-
// Now, let's check where the first difference is
|
|
4193
|
-
const existingLines = this.toTomlString.split(__classPrivateFieldGet(this, _TomlDocument_Format, "f").newLine);
|
|
4194
|
-
const newLineChar = detectNewline(tomlString);
|
|
4195
|
-
const newTextLines = tomlString.split(newLineChar);
|
|
4196
|
-
let firstDiffLineIndex = 0;
|
|
4197
|
-
while (firstDiffLineIndex < existingLines.length &&
|
|
4198
|
-
firstDiffLineIndex < newTextLines.length &&
|
|
4199
|
-
existingLines[firstDiffLineIndex] === newTextLines[firstDiffLineIndex]) {
|
|
4200
|
-
firstDiffLineIndex++;
|
|
4201
|
-
}
|
|
4202
|
-
// Calculate the 1-based line number and 0-based column where the first difference occurs
|
|
4203
|
-
let firstDiffColumn = 0;
|
|
4204
|
-
// If we're within the bounds of both arrays, find the column where they differ
|
|
4205
|
-
if (firstDiffLineIndex < existingLines.length && firstDiffLineIndex < newTextLines.length) {
|
|
4206
|
-
const existingLine = existingLines[firstDiffLineIndex];
|
|
4207
|
-
const newLine = newTextLines[firstDiffLineIndex];
|
|
4208
|
-
// Find the first character position where the lines differ
|
|
4209
|
-
for (let i = 0; i < Math.max(existingLine.length, newLine.length); i++) {
|
|
4210
|
-
if (existingLine[i] !== newLine[i]) {
|
|
4211
|
-
firstDiffColumn = i;
|
|
4212
|
-
break;
|
|
4213
|
-
}
|
|
4214
|
-
}
|
|
4215
|
-
}
|
|
4216
|
-
let firstDiffLine = firstDiffLineIndex + 1; // Convert to 1-based
|
|
4217
|
-
const { truncatedAst, lastEndPosition } = truncateAst(__classPrivateFieldGet(this, _TomlDocument_ast, "f"), firstDiffLine, firstDiffColumn);
|
|
4218
|
-
// Determine where to continue parsing from in the new string
|
|
4219
|
-
// If lastEndPosition exists, continue from there; otherwise from the start of the document
|
|
4220
|
-
const continueFromLine = lastEndPosition ? lastEndPosition.line : 1;
|
|
4221
|
-
const continueFromColumn = lastEndPosition ? lastEndPosition.column + 1 : 0;
|
|
4222
|
-
// Based on the first difference, we can re-parse only the affected part
|
|
4223
|
-
// We will need to supply the remaining string after where the AST was truncated
|
|
4224
|
-
const remainingLines = newTextLines.slice(continueFromLine - 1);
|
|
4225
|
-
// If there's a partial line match, we need to extract only the part after the continuation column
|
|
4226
|
-
if (remainingLines.length > 0 && continueFromColumn > 0) {
|
|
4227
|
-
remainingLines[0] = remainingLines[0].substring(continueFromColumn);
|
|
4228
|
-
}
|
|
4229
|
-
const remainingToml = remainingLines.join(__classPrivateFieldGet(this, _TomlDocument_Format, "f").newLine);
|
|
4230
|
-
__classPrivateFieldSet(this, _TomlDocument_ast, Array.from(continueParsingTOML(truncatedAst, remainingToml)), "f");
|
|
4231
|
-
__classPrivateFieldSet(this, _TomlDocument_currentTomlString, tomlString, "f");
|
|
4232
|
-
// Update the auto-detected format with the new string's characteristics
|
|
4233
|
-
__classPrivateFieldSet(this, _TomlDocument_Format, TomlFormat.autoDetectFormat(tomlString), "f");
|
|
4234
|
-
}
|
|
4235
|
-
/**
|
|
4236
|
-
* Overwrites the internal AST by fully re-parsing the supplied tomlString.
|
|
4237
|
-
* This is simpler but slower than update() which uses incremental parsing.
|
|
4238
|
-
* @param tomlString - The TOML string to overwrite with
|
|
4239
|
-
*/
|
|
4240
|
-
overwrite(tomlString) {
|
|
4241
|
-
if (tomlString === this.toTomlString) {
|
|
4242
|
-
return;
|
|
4243
|
-
}
|
|
4244
|
-
// Re-parse the entire document
|
|
4245
|
-
__classPrivateFieldSet(this, _TomlDocument_ast, Array.from(parseTOML(tomlString)), "f");
|
|
4246
|
-
__classPrivateFieldSet(this, _TomlDocument_currentTomlString, tomlString, "f");
|
|
4247
|
-
// Update the auto-detected format with the new string's characteristics
|
|
4248
|
-
__classPrivateFieldSet(this, _TomlDocument_Format, TomlFormat.autoDetectFormat(tomlString), "f");
|
|
4249
|
-
}
|
|
4250
|
-
}
|
|
4251
|
-
_TomlDocument_ast = new WeakMap(), _TomlDocument_currentTomlString = new WeakMap(), _TomlDocument_Format = new WeakMap();
|
|
4252
|
-
/**
|
|
4253
|
-
* Recursively converts custom date classes to regular JavaScript Date objects.
|
|
4254
|
-
* This ensures that the toJsObject property returns standard Date objects
|
|
4255
|
-
* while preserving the custom classes internally for TOML formatting.
|
|
4256
|
-
*/
|
|
4257
|
-
function convertCustomDateClasses(obj) {
|
|
4258
|
-
if (obj instanceof Date) {
|
|
4259
|
-
// Convert custom date classes to regular Date objects
|
|
4260
|
-
return new Date(obj.getTime());
|
|
4261
|
-
}
|
|
4262
|
-
else if (Array.isArray(obj)) {
|
|
4263
|
-
return obj.map(convertCustomDateClasses);
|
|
4264
|
-
}
|
|
4265
|
-
else if (obj && typeof obj === 'object') {
|
|
4266
|
-
const result = {};
|
|
4267
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
4268
|
-
result[key] = convertCustomDateClasses(value);
|
|
4269
|
-
}
|
|
4270
|
-
return result;
|
|
4271
|
-
}
|
|
4272
|
-
return obj;
|
|
4273
|
-
}
|
|
4274
|
-
|
|
4275
|
-
/**
|
|
4276
|
-
* Parses a TOML string into a JavaScript object.
|
|
4277
|
-
* The function converts TOML syntax to its JavaScript equivalent.
|
|
4278
|
-
* This proceeds in two steps: first, it parses the TOML string into an AST,
|
|
4279
|
-
* and then it converts the AST into a JavaScript object.
|
|
4280
|
-
*
|
|
4281
|
-
* @param value - The TOML string to parse
|
|
4282
|
-
* @returns The parsed JavaScript object
|
|
4283
|
-
*/
|
|
4284
|
-
function parse(value) {
|
|
4285
|
-
return toJS(parseTOML(value), value);
|
|
4286
|
-
}
|
|
4287
|
-
/**
|
|
4288
|
-
* Converts a JavaScript object to a TOML string.
|
|
4289
|
-
*
|
|
4290
|
-
* @param value - The JavaScript object to stringify
|
|
4291
|
-
* @param format - Optional formatting options for the resulting TOML
|
|
4292
|
-
* @returns The stringified TOML representation
|
|
4293
|
-
*/
|
|
4294
|
-
function stringify(value, format) {
|
|
4295
|
-
const fmt = resolveTomlFormat(format, TomlFormat.default());
|
|
4296
|
-
const document = parseJS(value, fmt);
|
|
4297
|
-
return toTOML(document.items, fmt);
|
|
4298
|
-
}
|
|
4299
|
-
|
|
4300
|
-
export { TomlDocument, TomlFormat, parse, patch, stringify };
|