@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.
@@ -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 };