@decimalturn/toml-patch 0.3.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.
@@ -0,0 +1,2429 @@
1
+ //! @decimalturn/toml-patch v0.3.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
+ NodeType["TableArray"] = "TableArray";
8
+ NodeType["TableArrayKey"] = "TableArrayKey";
9
+ NodeType["KeyValue"] = "KeyValue";
10
+ NodeType["Key"] = "Key";
11
+ NodeType["String"] = "String";
12
+ NodeType["Integer"] = "Integer";
13
+ NodeType["Float"] = "Float";
14
+ NodeType["Boolean"] = "Boolean";
15
+ NodeType["DateTime"] = "DateTime";
16
+ NodeType["InlineArray"] = "InlineArray";
17
+ NodeType["InlineItem"] = "InlineItem";
18
+ NodeType["InlineTable"] = "InlineTable";
19
+ NodeType["Comment"] = "Comment";
20
+ })(NodeType || (NodeType = {}));
21
+ function isDocument(node) {
22
+ return node.type === NodeType.Document;
23
+ }
24
+ function isTable(node) {
25
+ return node.type === NodeType.Table;
26
+ }
27
+ function isTableKey(node) {
28
+ return node.type === NodeType.TableKey;
29
+ }
30
+ function isTableArray(node) {
31
+ return node.type === NodeType.TableArray;
32
+ }
33
+ function isTableArrayKey(node) {
34
+ return node.type === NodeType.TableArrayKey;
35
+ }
36
+ function isKeyValue(node) {
37
+ return node.type === NodeType.KeyValue;
38
+ }
39
+ function isInlineArray(node) {
40
+ return node.type === NodeType.InlineArray;
41
+ }
42
+ function isInlineItem(node) {
43
+ return node.type === NodeType.InlineItem;
44
+ }
45
+ function isInlineTable(node) {
46
+ return node.type === NodeType.InlineTable;
47
+ }
48
+ function isComment(node) {
49
+ return node.type === NodeType.Comment;
50
+ }
51
+ function hasItems(node) {
52
+ return (isDocument(node) ||
53
+ isTable(node) ||
54
+ isTableArray(node) ||
55
+ isInlineTable(node) ||
56
+ isInlineArray(node));
57
+ }
58
+ function hasItem(node) {
59
+ return isTableKey(node) || isTableArrayKey(node) || isInlineItem(node);
60
+ }
61
+ function isBlock(node) {
62
+ return isKeyValue(node) || isTable(node) || isTableArray(node) || isComment(node);
63
+ }
64
+
65
+ function iterator(value) {
66
+ return value[Symbol.iterator]();
67
+ }
68
+ /**
69
+ * Cursor<T>
70
+ *
71
+ * A utility class that wraps an iterator and provides additional functionality
72
+ * such as peeking at the next value without advancing the iterator, tracking
73
+ * the current index, and iterating over the values.
74
+ *
75
+ * @template T - The type of elements in the iterator.
76
+ *
77
+ * Properties:
78
+ * - `iterator`: The underlying iterator being wrapped.
79
+ * - `index`: The current index of the iterator (starts at -1).
80
+ * - `value`: The current value of the iterator.
81
+ * - `done`: A boolean indicating whether the iterator is complete.
82
+ * - `peeked`: The result of peeking at the next value without advancing.
83
+ *
84
+ * Methods:
85
+ * - `next()`: Advances the iterator and returns the next value.
86
+ * - `peek()`: Returns the next value without advancing the iterator.
87
+ * - `[Symbol.iterator]`: Makes the Cursor itself iterable.
88
+ */
89
+ class Cursor {
90
+ constructor(iterator) {
91
+ this.iterator = iterator;
92
+ this.index = -1;
93
+ this.value = undefined;
94
+ this.done = false;
95
+ this.peeked = null;
96
+ }
97
+ next() {
98
+ var _a;
99
+ if (this.done)
100
+ return done();
101
+ const result = this.peeked || this.iterator.next();
102
+ this.index += 1;
103
+ this.value = result.value;
104
+ this.done = (_a = result.done) !== null && _a !== void 0 ? _a : false;
105
+ this.peeked = null;
106
+ return result;
107
+ }
108
+ peek() {
109
+ if (this.done)
110
+ return done();
111
+ if (this.peeked)
112
+ return this.peeked;
113
+ this.peeked = this.iterator.next();
114
+ return this.peeked;
115
+ }
116
+ [Symbol.iterator]() {
117
+ return this;
118
+ }
119
+ }
120
+ function done() {
121
+ return { value: undefined, done: true };
122
+ }
123
+
124
+ function getSpan(location) {
125
+ return {
126
+ lines: location.end.line - location.start.line + 1,
127
+ columns: location.end.column - location.start.column
128
+ };
129
+ }
130
+ function createLocate(input) {
131
+ const lines = findLines(input);
132
+ return (start, end) => {
133
+ return {
134
+ start: findPosition(lines, start),
135
+ end: findPosition(lines, end)
136
+ };
137
+ };
138
+ }
139
+ function findPosition(input, index) {
140
+ // abc\ndef\ng
141
+ // 0123 4567 8
142
+ // 012
143
+ // 0
144
+ //
145
+ // lines = [3, 7, 9]
146
+ //
147
+ // c = 2: 0 -> 1, 2 - (undefined + 1 || 0) = 2
148
+ // 3: 0 -> 1, 3 - (undefined + 1 || 0) = 3
149
+ // e = 5: 1 -> 2, 5 - (3 + 1 || 0) = 1
150
+ // g = 8: 2 -> 3, 8 - (7 + 1 || 0) = 0
151
+ const lines = Array.isArray(input) ? input : findLines(input);
152
+ const line = lines.findIndex(line_index => line_index >= index) + 1;
153
+ const column = index - (lines[line - 2] + 1 || 0);
154
+ return { line, column };
155
+ }
156
+ function getLine$1(input, position) {
157
+ const lines = findLines(input);
158
+ const start = lines[position.line - 2] || 0;
159
+ const end = lines[position.line - 1] || input.length;
160
+ return input.substr(start, end - start);
161
+ }
162
+ function findLines(input) {
163
+ // exec is stateful, so create new regexp each time
164
+ const BY_NEW_LINE = /[\r\n|\n]/g;
165
+ const indexes = [];
166
+ let match;
167
+ while ((match = BY_NEW_LINE.exec(input)) != null) {
168
+ indexes.push(match.index);
169
+ }
170
+ indexes.push(input.length + 1);
171
+ return indexes;
172
+ }
173
+ function clonePosition(position) {
174
+ return { line: position.line, column: position.column };
175
+ }
176
+ function cloneLocation(location) {
177
+ return { start: clonePosition(location.start), end: clonePosition(location.end) };
178
+ }
179
+ function zero() {
180
+ return { line: 1, column: 0 };
181
+ }
182
+
183
+ class ParseError extends Error {
184
+ constructor(input, position, message) {
185
+ let error_message = `Error parsing TOML (${position.line}, ${position.column + 1}):\n`;
186
+ if (input) {
187
+ const line = getLine$1(input, position);
188
+ const pointer = `${whitespace(position.column)}^`;
189
+ if (line)
190
+ error_message += `${line}\n${pointer}\n`;
191
+ }
192
+ error_message += message;
193
+ super(error_message);
194
+ this.line = position.line;
195
+ this.column = position.column;
196
+ }
197
+ }
198
+ function whitespace(count, character = ' ') {
199
+ return character.repeat(count);
200
+ }
201
+
202
+ var TokenType;
203
+ (function (TokenType) {
204
+ TokenType["Bracket"] = "Bracket";
205
+ TokenType["Curly"] = "Curly";
206
+ TokenType["Equal"] = "Equal";
207
+ TokenType["Comma"] = "Comma";
208
+ TokenType["Dot"] = "Dot";
209
+ TokenType["Comment"] = "Comment";
210
+ TokenType["Literal"] = "Literal";
211
+ })(TokenType || (TokenType = {}));
212
+ const IS_WHITESPACE = /\s/;
213
+ const IS_NEW_LINE = /(\r\n|\n)/;
214
+ const DOUBLE_QUOTE = `"`;
215
+ const SINGLE_QUOTE = `'`;
216
+ const SPACE = ' ';
217
+ const ESCAPE = '\\';
218
+ const IS_VALID_LEADING_CHARACTER = /[\w,\d,\",\',\+,\-,\_]/;
219
+ function* tokenize(input) {
220
+ const cursor = new Cursor(iterator(input));
221
+ cursor.next();
222
+ const locate = createLocate(input);
223
+ while (!cursor.done) {
224
+ if (IS_WHITESPACE.test(cursor.value)) ;
225
+ else if (cursor.value === '[' || cursor.value === ']') {
226
+ // Handle special characters: [, ], {, }, =, comma
227
+ yield specialCharacter(cursor, locate, TokenType.Bracket);
228
+ }
229
+ else if (cursor.value === '{' || cursor.value === '}') {
230
+ yield specialCharacter(cursor, locate, TokenType.Curly);
231
+ }
232
+ else if (cursor.value === '=') {
233
+ yield specialCharacter(cursor, locate, TokenType.Equal);
234
+ }
235
+ else if (cursor.value === ',') {
236
+ yield specialCharacter(cursor, locate, TokenType.Comma);
237
+ }
238
+ else if (cursor.value === '.') {
239
+ yield specialCharacter(cursor, locate, TokenType.Dot);
240
+ }
241
+ else if (cursor.value === '#') {
242
+ // Handle comments = # -> EOL
243
+ yield comment$1(cursor, locate);
244
+ }
245
+ else {
246
+ const multiline_char = checkThree(input, cursor.index, SINGLE_QUOTE) ||
247
+ checkThree(input, cursor.index, DOUBLE_QUOTE);
248
+ if (multiline_char) {
249
+ // Multi-line literals or strings = no escaping
250
+ yield multiline(cursor, locate, multiline_char, input);
251
+ }
252
+ else {
253
+ yield string$1(cursor, locate, input);
254
+ }
255
+ }
256
+ cursor.next();
257
+ }
258
+ }
259
+ function specialCharacter(cursor, locate, type) {
260
+ return { type, raw: cursor.value, loc: locate(cursor.index, cursor.index + 1) };
261
+ }
262
+ function comment$1(cursor, locate) {
263
+ const start = cursor.index;
264
+ let raw = cursor.value;
265
+ while (!cursor.peek().done && !IS_NEW_LINE.test(cursor.peek().value)) {
266
+ cursor.next();
267
+ raw += cursor.value;
268
+ }
269
+ // Early exit is ok for comment, no closing conditions
270
+ return {
271
+ type: TokenType.Comment,
272
+ raw,
273
+ loc: locate(start, cursor.index + 1)
274
+ };
275
+ }
276
+ function multiline(cursor, locate, multiline_char, input) {
277
+ const start = cursor.index;
278
+ let quotes = multiline_char + multiline_char + multiline_char;
279
+ let raw = quotes;
280
+ // Skip over quotes
281
+ cursor.next();
282
+ cursor.next();
283
+ cursor.next();
284
+ // 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
285
+ // See spec-string-basic-multiline-9.toml
286
+ while (!cursor.done && (!checkThree(input, cursor.index, multiline_char) || CheckMoreThanThree(input, cursor.index, multiline_char))) {
287
+ raw += cursor.value;
288
+ cursor.next();
289
+ }
290
+ if (cursor.done) {
291
+ throw new ParseError(input, findPosition(input, cursor.index), `Expected close of multiline string with ${quotes}, reached end of file`);
292
+ }
293
+ raw += quotes;
294
+ cursor.next();
295
+ cursor.next();
296
+ return {
297
+ type: TokenType.Literal,
298
+ raw,
299
+ loc: locate(start, cursor.index + 1)
300
+ };
301
+ }
302
+ function string$1(cursor, locate, input) {
303
+ // Remaining possibilities: keys, strings, literals, integer, float, boolean
304
+ //
305
+ // Special cases:
306
+ // "..." -> quoted
307
+ // '...' -> quoted
308
+ // "...".'...' -> bare
309
+ // 0000-00-00 00:00:00 -> bare
310
+ //
311
+ // See https://github.com/toml-lang/toml#offset-date-time
312
+ //
313
+ // | 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).
314
+ // | `odt4 = 1979-05-27 07:32:00Z`
315
+ //
316
+ // From RFC 3339:
317
+ //
318
+ // | NOTE: ISO 8601 defines date and time separated by "T".
319
+ // | Applications using this syntax may choose, for the sake of
320
+ // | readability, to specify a full-date and full-time separated by
321
+ // | (say) a space character.
322
+ // First, check for invalid characters
323
+ if (!IS_VALID_LEADING_CHARACTER.test(cursor.value)) {
324
+ throw new ParseError(input, findPosition(input, cursor.index), `Unsupported character "${cursor.value}". Expected ALPHANUMERIC, ", ', +, -, or _`);
325
+ }
326
+ const start = cursor.index;
327
+ let raw = cursor.value;
328
+ let double_quoted = cursor.value === DOUBLE_QUOTE;
329
+ let single_quoted = cursor.value === SINGLE_QUOTE;
330
+ const isFinished = (cursor) => {
331
+ if (cursor.peek().done)
332
+ return true;
333
+ const next_item = cursor.peek().value;
334
+ return (!(double_quoted || single_quoted) &&
335
+ (IS_WHITESPACE.test(next_item) ||
336
+ next_item === ',' ||
337
+ next_item === '.' ||
338
+ next_item === ']' ||
339
+ next_item === '}' ||
340
+ next_item === '=' ||
341
+ next_item === '#'));
342
+ };
343
+ while (!cursor.done && !isFinished(cursor)) {
344
+ cursor.next();
345
+ if (cursor.value === DOUBLE_QUOTE)
346
+ double_quoted = !double_quoted;
347
+ if (cursor.value === SINGLE_QUOTE && !double_quoted)
348
+ single_quoted = !single_quoted;
349
+ raw += cursor.value;
350
+ if (cursor.peek().done)
351
+ break;
352
+ let next_item = cursor.peek().value;
353
+ // If next character is escape and currently double-quoted,
354
+ // check for escaped quote
355
+ if (double_quoted && cursor.value === ESCAPE) {
356
+ if (next_item === DOUBLE_QUOTE) {
357
+ raw += DOUBLE_QUOTE;
358
+ cursor.next();
359
+ }
360
+ else if (next_item === ESCAPE) {
361
+ raw += ESCAPE;
362
+ cursor.next();
363
+ }
364
+ }
365
+ }
366
+ if (double_quoted || single_quoted) {
367
+ throw new ParseError(input, findPosition(input, start), `Expected close of string with ${double_quoted ? DOUBLE_QUOTE : SINGLE_QUOTE}`);
368
+ }
369
+ return {
370
+ type: TokenType.Literal,
371
+ raw,
372
+ loc: locate(start, cursor.index + 1)
373
+ };
374
+ }
375
+ /**
376
+ * Check if the current character and the next two characters are the same
377
+ * and not escaped.
378
+ *
379
+ * @param input - The input string.
380
+ * @param current - The current index in the input string.
381
+ * @param check - The character to check for.
382
+ * @returns ⚠️The character if found, otherwise false.
383
+ */
384
+ function checkThree(input, current, check) {
385
+ if (!check) {
386
+ return false;
387
+ }
388
+ const has3 = input[current] === check &&
389
+ input[current + 1] === check &&
390
+ input[current + 2] === check;
391
+ if (!has3) {
392
+ return false;
393
+ }
394
+ // Check if the sequence is escaped
395
+ const precedingText = input.slice(0, current); // Get the text before the current position
396
+ const backslashes = precedingText.match(/\\+$/); // Match trailing backslashes
397
+ if (!backslashes) {
398
+ return check; // No backslashes means not escaped
399
+ }
400
+ const isEscaped = backslashes[0].length % 2 !== 0; // Odd number of backslashes means escaped
401
+ return isEscaped ? false : check; // Return `check` if not escaped, otherwise `false`
402
+ }
403
+ function CheckMoreThanThree(input, current, check) {
404
+ if (!check) {
405
+ return false;
406
+ }
407
+ return (input[current] === check &&
408
+ input[current + 1] === check &&
409
+ input[current + 2] === check &&
410
+ input[current + 3] === check);
411
+ }
412
+
413
+ function last(values) {
414
+ return values[values.length - 1];
415
+ }
416
+ function blank() {
417
+ return Object.create(null);
418
+ }
419
+ function isString(value) {
420
+ return typeof value === 'string';
421
+ }
422
+ function isInteger(value) {
423
+ return typeof value === 'number' && value % 1 === 0;
424
+ }
425
+ function isFloat(value) {
426
+ return typeof value === 'number' && !isInteger(value);
427
+ }
428
+ function isBoolean(value) {
429
+ return typeof value === 'boolean';
430
+ }
431
+ function isDate(value) {
432
+ return Object.prototype.toString.call(value) === '[object Date]';
433
+ }
434
+ function isObject(value) {
435
+ return value && typeof value === 'object' && !isDate(value) && !Array.isArray(value);
436
+ }
437
+ function isIterable(value) {
438
+ return value != null && typeof value[Symbol.iterator] === 'function';
439
+ }
440
+ function has(object, key) {
441
+ return Object.prototype.hasOwnProperty.call(object, key);
442
+ }
443
+ function arraysEqual(a, b) {
444
+ if (a.length !== b.length)
445
+ return false;
446
+ for (let i = 0; i < a.length; i++) {
447
+ if (a[i] !== b[i])
448
+ return false;
449
+ }
450
+ return true;
451
+ }
452
+ function datesEqual(a, b) {
453
+ return isDate(a) && isDate(b) && a.toISOString() === b.toISOString();
454
+ }
455
+ function pipe(value, ...fns) {
456
+ return fns.reduce((value, fn) => fn(value), value);
457
+ }
458
+ function stableStringify(object) {
459
+ if (isObject(object)) {
460
+ const key_values = Object.keys(object)
461
+ .sort()
462
+ .map(key => `${JSON.stringify(key)}:${stableStringify(object[key])}`);
463
+ return `{${key_values.join(',')}}`;
464
+ }
465
+ else if (Array.isArray(object)) {
466
+ return `[${object.map(stableStringify).join(',')}]`;
467
+ }
468
+ else {
469
+ return JSON.stringify(object);
470
+ }
471
+ }
472
+ function merge(target, values) {
473
+ // __mutating__: merge values into target
474
+ // Reference: https://dev.to/uilicious/javascript-array-push-is-945x-faster-than-array-concat-1oki
475
+ const original_length = target.length;
476
+ const added_length = values.length;
477
+ target.length = original_length + added_length;
478
+ for (let i = 0; i < added_length; i++) {
479
+ target[original_length + i] = values[i];
480
+ }
481
+ }
482
+
483
+ const TRIPLE_DOUBLE_QUOTE = `"""`;
484
+ const TRIPLE_SINGLE_QUOTE = `'''`;
485
+ const LF = '\\n';
486
+ const CRLF = '\\r\\n';
487
+ const IS_CRLF = /\r\n/g;
488
+ const IS_LF = /\n/g;
489
+ const IS_LEADING_NEW_LINE = /^(\r\n|\n)/;
490
+ // This regex is used to match an odd number of backslashes followed by a line ending
491
+ // It uses a negative lookbehind to ensure that the backslash is not preceded by another backslash.
492
+ // We need an odd number of backslashes so that the last one is not escaped.
493
+ const IS_LINE_ENDING_BACKSLASH = /(?<!\\)(?:\\\\)*(\\\s*[\n\r\n]\s*)/g;
494
+ function parseString(raw) {
495
+ if (raw.startsWith(TRIPLE_SINGLE_QUOTE)) {
496
+ return pipe(trim(raw, 3), trimLeadingWhitespace);
497
+ }
498
+ else if (raw.startsWith(SINGLE_QUOTE)) {
499
+ return trim(raw, 1);
500
+ }
501
+ else if (raw.startsWith(TRIPLE_DOUBLE_QUOTE)) {
502
+ return pipe(trim(raw, 3), trimLeadingWhitespace, lineEndingBackslash, escapeNewLines, escapeDoubleQuotes, unescapeLargeUnicode);
503
+ }
504
+ else if (raw.startsWith(DOUBLE_QUOTE)) {
505
+ return pipe(trim(raw, 1), unescapeLargeUnicode);
506
+ }
507
+ else {
508
+ return raw;
509
+ }
510
+ }
511
+ function escapeDoubleQuotes(value) {
512
+ let result = '';
513
+ let precedingBackslashes = 0;
514
+ for (let i = 0; i < value.length; i++) {
515
+ const char = value[i];
516
+ if (char === '"' && precedingBackslashes % 2 === 0) {
517
+ // If the current character is a quote and it is not escaped, escape it
518
+ result += '\\"';
519
+ }
520
+ else {
521
+ // Otherwise, add the character as is
522
+ result += char;
523
+ }
524
+ // Update the count of consecutive backslashes
525
+ if (char === '\\') {
526
+ precedingBackslashes++;
527
+ }
528
+ else {
529
+ precedingBackslashes = 0; // Reset if the character is not a backslash
530
+ }
531
+ }
532
+ return result;
533
+ }
534
+ function unescapeLargeUnicode(escaped) {
535
+ // JSON.parse handles everything except \UXXXXXXXX
536
+ // replace those instances with code point, escape that, and then parse
537
+ const LARGE_UNICODE = /\\U[a-fA-F0-9]{8}/g;
538
+ const json_escaped = escaped.replace(LARGE_UNICODE, value => {
539
+ const code_point = parseInt(value.replace('\\U', ''), 16);
540
+ const as_string = String.fromCodePoint(code_point);
541
+ return trim(JSON.stringify(as_string), 1);
542
+ });
543
+ const fixed_json_escaped = escapeTabsForJSON(json_escaped);
544
+ // Parse the properly escaped JSON string
545
+ const parsed = JSON.parse(`"${fixed_json_escaped}"`);
546
+ return parsed;
547
+ }
548
+ function escapeTabsForJSON(value) {
549
+ return value
550
+ .replace(/\t/g, '\\t');
551
+ }
552
+ function trim(value, count) {
553
+ return value.slice(count, value.length - count);
554
+ }
555
+ function trimLeadingWhitespace(value) {
556
+ return value.replace(IS_LEADING_NEW_LINE, '');
557
+ }
558
+ function escapeNewLines(value) {
559
+ return value.replace(IS_CRLF, CRLF).replace(IS_LF, LF);
560
+ }
561
+ function lineEndingBackslash(value) {
562
+ return value.replace(IS_LINE_ENDING_BACKSLASH, (match, group) => match.replace(group, ''));
563
+ }
564
+
565
+ const TRUE = 'true';
566
+ const FALSE = 'false';
567
+ const HAS_E = /e/i;
568
+ const IS_DIVIDER = /\_/g;
569
+ const IS_INF = /inf/;
570
+ const IS_NAN = /nan/;
571
+ const IS_HEX = /^0x/;
572
+ const IS_OCTAL = /^0o/;
573
+ const IS_BINARY = /^0b/;
574
+ const IS_FULL_DATE = /(\d{4})-(\d{2})-(\d{2})/;
575
+ const IS_FULL_TIME = /(\d{2}):(\d{2}):(\d{2})/;
576
+ function* parseTOML(input) {
577
+ const tokens = tokenize(input);
578
+ const cursor = new Cursor(tokens);
579
+ while (!cursor.next().done) {
580
+ yield* walkBlock(cursor, input);
581
+ }
582
+ }
583
+ function* walkBlock(cursor, input) {
584
+ if (cursor.value.type === TokenType.Comment) {
585
+ yield comment(cursor);
586
+ }
587
+ else if (cursor.value.type === TokenType.Bracket) {
588
+ yield table(cursor, input);
589
+ }
590
+ else if (cursor.value.type === TokenType.Literal) {
591
+ yield* keyValue(cursor, input);
592
+ }
593
+ else {
594
+ throw new ParseError(input, cursor.value.loc.start, `Unexpected token "${cursor.value.type}". Expected Comment, Bracket, or String`);
595
+ }
596
+ }
597
+ function* walkValue$1(cursor, input) {
598
+ if (cursor.value.type === TokenType.Literal) {
599
+ if (cursor.value.raw[0] === DOUBLE_QUOTE || cursor.value.raw[0] === SINGLE_QUOTE) {
600
+ yield string(cursor);
601
+ }
602
+ else if (cursor.value.raw === TRUE || cursor.value.raw === FALSE) {
603
+ yield boolean(cursor);
604
+ }
605
+ else if (IS_FULL_DATE.test(cursor.value.raw) || IS_FULL_TIME.test(cursor.value.raw)) {
606
+ yield datetime(cursor, input);
607
+ }
608
+ else if ((!cursor.peek().done && cursor.peek().value.type === TokenType.Dot) ||
609
+ IS_INF.test(cursor.value.raw) ||
610
+ IS_NAN.test(cursor.value.raw) ||
611
+ (HAS_E.test(cursor.value.raw) && !IS_HEX.test(cursor.value.raw))) {
612
+ yield float(cursor, input);
613
+ }
614
+ else {
615
+ yield integer(cursor);
616
+ }
617
+ }
618
+ else if (cursor.value.type === TokenType.Curly) {
619
+ yield inlineTable(cursor, input);
620
+ }
621
+ else if (cursor.value.type === TokenType.Bracket) {
622
+ const [inline_array, comments] = inlineArray(cursor, input);
623
+ yield inline_array;
624
+ yield* comments;
625
+ }
626
+ else {
627
+ throw new ParseError(input, cursor.value.loc.start, `Unrecognized token type "${cursor.value.type}". Expected String, Curly, or Bracket`);
628
+ }
629
+ }
630
+ function comment(cursor) {
631
+ // # line comment
632
+ // ^------------^ Comment
633
+ return {
634
+ type: NodeType.Comment,
635
+ loc: cursor.value.loc,
636
+ raw: cursor.value.raw
637
+ };
638
+ }
639
+ function table(cursor, input) {
640
+ // Table or TableArray
641
+ //
642
+ // [ key ]
643
+ // ^-----^ TableKey
644
+ // ^-^ Key
645
+ //
646
+ // [[ key ]]
647
+ // ^ ------^ TableArrayKey
648
+ // ^-^ Key
649
+ //
650
+ // a = "b" < Items
651
+ // # c |
652
+ // d = "f" <
653
+ //
654
+ // ...
655
+ const type = !cursor.peek().done && cursor.peek().value.type === TokenType.Bracket
656
+ ? NodeType.TableArray
657
+ : NodeType.Table;
658
+ const is_table = type === NodeType.Table;
659
+ if (is_table && cursor.value.raw !== '[') {
660
+ throw new ParseError(input, cursor.value.loc.start, `Expected table opening "[", found ${cursor.value.raw}`);
661
+ }
662
+ if (!is_table && (cursor.value.raw !== '[' || cursor.peek().value.raw !== '[')) {
663
+ throw new ParseError(input, cursor.value.loc.start, `Expected array of tables opening "[[", found ${cursor.value.raw + cursor.peek().value.raw}`);
664
+ }
665
+ // Set start location from opening tag
666
+ const key = is_table
667
+ ? {
668
+ type: NodeType.TableKey,
669
+ loc: cursor.value.loc
670
+ }
671
+ : {
672
+ type: NodeType.TableArrayKey,
673
+ loc: cursor.value.loc
674
+ };
675
+ // Skip to cursor.value for key value
676
+ cursor.next();
677
+ if (type === NodeType.TableArray)
678
+ cursor.next();
679
+ if (cursor.done) {
680
+ throw new ParseError(input, key.loc.start, `Expected table key, reached end of file`);
681
+ }
682
+ key.item = {
683
+ type: NodeType.Key,
684
+ loc: cloneLocation(cursor.value.loc),
685
+ raw: cursor.value.raw,
686
+ value: [parseString(cursor.value.raw)]
687
+ };
688
+ while (!cursor.peek().done && cursor.peek().value.type === TokenType.Dot) {
689
+ cursor.next();
690
+ const dot = cursor.value;
691
+ cursor.next();
692
+ const before = ' '.repeat(dot.loc.start.column - key.item.loc.end.column);
693
+ const after = ' '.repeat(cursor.value.loc.start.column - dot.loc.end.column);
694
+ key.item.loc.end = cursor.value.loc.end;
695
+ key.item.raw += `${before}.${after}${cursor.value.raw}`;
696
+ key.item.value.push(parseString(cursor.value.raw));
697
+ }
698
+ cursor.next();
699
+ if (is_table && (cursor.done || cursor.value.raw !== ']')) {
700
+ 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}`);
701
+ }
702
+ if (!is_table &&
703
+ (cursor.done ||
704
+ cursor.peek().done ||
705
+ cursor.value.raw !== ']' ||
706
+ cursor.peek().value.raw !== ']')) {
707
+ 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
708
+ ? 'end of file'
709
+ : cursor.value.raw + cursor.peek().value.raw}`);
710
+ }
711
+ // Set end location from closing tag
712
+ if (!is_table)
713
+ cursor.next();
714
+ key.loc.end = cursor.value.loc.end;
715
+ // Add child items
716
+ let items = [];
717
+ while (!cursor.peek().done && cursor.peek().value.type !== TokenType.Bracket) {
718
+ cursor.next();
719
+ merge(items, [...walkBlock(cursor, input)]);
720
+ }
721
+ return {
722
+ type: is_table ? NodeType.Table : NodeType.TableArray,
723
+ loc: {
724
+ start: clonePosition(key.loc.start),
725
+ end: items.length
726
+ ? clonePosition(items[items.length - 1].loc.end)
727
+ : clonePosition(key.loc.end)
728
+ },
729
+ key: key,
730
+ items
731
+ };
732
+ }
733
+ function keyValue(cursor, input) {
734
+ // 3. KeyValue
735
+ //
736
+ // key = value
737
+ // ^-^ key
738
+ // ^ equals
739
+ // ^---^ value
740
+ const key = {
741
+ type: NodeType.Key,
742
+ loc: cloneLocation(cursor.value.loc),
743
+ raw: cursor.value.raw,
744
+ value: [parseString(cursor.value.raw)]
745
+ };
746
+ while (!cursor.peek().done && cursor.peek().value.type === TokenType.Dot) {
747
+ cursor.next();
748
+ cursor.next();
749
+ key.loc.end = cursor.value.loc.end;
750
+ key.raw += `.${cursor.value.raw}`;
751
+ key.value.push(parseString(cursor.value.raw));
752
+ }
753
+ cursor.next();
754
+ if (cursor.done || cursor.value.type !== TokenType.Equal) {
755
+ 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}`);
756
+ }
757
+ const equals = cursor.value.loc.start.column;
758
+ cursor.next();
759
+ if (cursor.done) {
760
+ throw new ParseError(input, key.loc.start, `Expected value for key-value, reached end of file`);
761
+ }
762
+ const [value, ...comments] = walkValue$1(cursor, input);
763
+ return [
764
+ {
765
+ type: NodeType.KeyValue,
766
+ key,
767
+ value: value,
768
+ loc: {
769
+ start: clonePosition(key.loc.start),
770
+ end: clonePosition(value.loc.end)
771
+ },
772
+ equals
773
+ },
774
+ ...comments
775
+ ];
776
+ }
777
+ function string(cursor) {
778
+ return {
779
+ type: NodeType.String,
780
+ loc: cursor.value.loc,
781
+ raw: cursor.value.raw,
782
+ value: parseString(cursor.value.raw)
783
+ };
784
+ }
785
+ function boolean(cursor) {
786
+ return {
787
+ type: NodeType.Boolean,
788
+ loc: cursor.value.loc,
789
+ value: cursor.value.raw === TRUE
790
+ };
791
+ }
792
+ function datetime(cursor, input) {
793
+ // Possible values:
794
+ //
795
+ // Offset Date-Time
796
+ // | odt1 = 1979-05-27T07:32:00Z
797
+ // | odt2 = 1979-05-27T00:32:00-07:00
798
+ // | odt3 = 1979-05-27T00:32:00.999999-07:00
799
+ // | odt4 = 1979-05-27 07:32:00Z
800
+ //
801
+ // Local Date-Time
802
+ // | ldt1 = 1979-05-27T07:32:00
803
+ // | ldt2 = 1979-05-27T00:32:00.999999
804
+ //
805
+ // Local Date
806
+ // | ld1 = 1979-05-27
807
+ //
808
+ // Local Time
809
+ // | lt1 = 07:32:00
810
+ // | lt2 = 00:32:00.999999
811
+ let loc = cursor.value.loc;
812
+ let raw = cursor.value.raw;
813
+ let value;
814
+ // If next token is string,
815
+ // check if raw is full date and following is full time
816
+ if (!cursor.peek().done &&
817
+ cursor.peek().value.type === TokenType.Literal &&
818
+ IS_FULL_DATE.test(raw) &&
819
+ IS_FULL_TIME.test(cursor.peek().value.raw)) {
820
+ const start = loc.start;
821
+ cursor.next();
822
+ loc = { start, end: cursor.value.loc.end };
823
+ raw += ` ${cursor.value.raw}`;
824
+ }
825
+ if (!cursor.peek().done && cursor.peek().value.type === TokenType.Dot) {
826
+ const start = loc.start;
827
+ cursor.next();
828
+ if (cursor.peek().done || cursor.peek().value.type !== TokenType.Literal) {
829
+ throw new ParseError(input, cursor.value.loc.end, `Expected fractional value for DateTime`);
830
+ }
831
+ cursor.next();
832
+ loc = { start, end: cursor.value.loc.end };
833
+ raw += `.${cursor.value.raw}`;
834
+ }
835
+ if (!IS_FULL_DATE.test(raw)) {
836
+ // For local time, use local ISO date
837
+ const [local_date] = new Date().toISOString().split('T');
838
+ value = new Date(`${local_date}T${raw}`);
839
+ }
840
+ else {
841
+ value = new Date(raw.replace(' ', 'T'));
842
+ }
843
+ return {
844
+ type: NodeType.DateTime,
845
+ loc,
846
+ raw,
847
+ value
848
+ };
849
+ }
850
+ function float(cursor, input) {
851
+ let loc = cursor.value.loc;
852
+ let raw = cursor.value.raw;
853
+ let value;
854
+ if (IS_INF.test(raw)) {
855
+ value = raw === '-inf' ? -Infinity : Infinity;
856
+ }
857
+ else if (IS_NAN.test(raw)) {
858
+ value = raw === '-nan' ? -NaN : NaN;
859
+ }
860
+ else if (!cursor.peek().done && cursor.peek().value.type === TokenType.Dot) {
861
+ const start = loc.start;
862
+ // From spec:
863
+ // | A fractional part is a decimal point followed by one or more digits.
864
+ //
865
+ // -> Don't have to handle "4." (i.e. nothing behind decimal place)
866
+ cursor.next();
867
+ if (cursor.peek().done || cursor.peek().value.type !== TokenType.Literal) {
868
+ throw new ParseError(input, cursor.value.loc.end, `Expected fraction value for Float`);
869
+ }
870
+ cursor.next();
871
+ raw += `.${cursor.value.raw}`;
872
+ loc = { start, end: cursor.value.loc.end };
873
+ value = Number(raw.replace(IS_DIVIDER, ''));
874
+ }
875
+ else {
876
+ value = Number(raw.replace(IS_DIVIDER, ''));
877
+ }
878
+ return { type: NodeType.Float, loc, raw, value };
879
+ }
880
+ function integer(cursor) {
881
+ // > Integer values -0 and +0 are valid and identical to an unprefixed zero
882
+ if (cursor.value.raw === '-0' || cursor.value.raw === '+0') {
883
+ return {
884
+ type: NodeType.Integer,
885
+ loc: cursor.value.loc,
886
+ raw: cursor.value.raw,
887
+ value: 0
888
+ };
889
+ }
890
+ let radix = 10;
891
+ if (IS_HEX.test(cursor.value.raw)) {
892
+ radix = 16;
893
+ }
894
+ else if (IS_OCTAL.test(cursor.value.raw)) {
895
+ radix = 8;
896
+ }
897
+ else if (IS_BINARY.test(cursor.value.raw)) {
898
+ radix = 2;
899
+ }
900
+ const value = parseInt(cursor
901
+ .value.raw.replace(IS_DIVIDER, '')
902
+ .replace(IS_OCTAL, '')
903
+ .replace(IS_BINARY, ''), radix);
904
+ return {
905
+ type: NodeType.Integer,
906
+ loc: cursor.value.loc,
907
+ raw: cursor.value.raw,
908
+ value
909
+ };
910
+ }
911
+ function inlineTable(cursor, input) {
912
+ if (cursor.value.raw !== '{') {
913
+ throw new ParseError(input, cursor.value.loc.start, `Expected "{" for inline table, found ${cursor.value.raw}`);
914
+ }
915
+ // 6. InlineTable
916
+ const value = {
917
+ type: NodeType.InlineTable,
918
+ loc: cloneLocation(cursor.value.loc),
919
+ items: []
920
+ };
921
+ cursor.next();
922
+ while (!cursor.done &&
923
+ !(cursor.value.type === TokenType.Curly && cursor.value.raw === '}')) {
924
+ if (cursor.value.type === TokenType.Comma) {
925
+ const previous = value.items[value.items.length - 1];
926
+ if (!previous) {
927
+ throw new ParseError(input, cursor.value.loc.start, 'Found "," without previous value in inline table');
928
+ }
929
+ previous.comma = true;
930
+ previous.loc.end = cursor.value.loc.start;
931
+ cursor.next();
932
+ continue;
933
+ }
934
+ const [item] = walkBlock(cursor, input);
935
+ if (item.type !== NodeType.KeyValue) {
936
+ throw new ParseError(input, cursor.value.loc.start, `Only key-values are supported in inline tables, found ${item.type}`);
937
+ }
938
+ const inline_item = {
939
+ type: NodeType.InlineItem,
940
+ loc: cloneLocation(item.loc),
941
+ item,
942
+ comma: false
943
+ };
944
+ value.items.push(inline_item);
945
+ cursor.next();
946
+ }
947
+ if (cursor.done ||
948
+ cursor.value.type !== TokenType.Curly ||
949
+ cursor.value.raw !== '}') {
950
+ throw new ParseError(input, cursor.done ? value.loc.start : cursor.value.loc.start, `Expected "}", found ${cursor.done ? 'end of file' : cursor.value.raw}`);
951
+ }
952
+ value.loc.end = cursor.value.loc.end;
953
+ return value;
954
+ }
955
+ function inlineArray(cursor, input) {
956
+ // 7. InlineArray
957
+ if (cursor.value.raw !== '[') {
958
+ throw new ParseError(input, cursor.value.loc.start, `Expected "[" for inline array, found ${cursor.value.raw}`);
959
+ }
960
+ const value = {
961
+ type: NodeType.InlineArray,
962
+ loc: cloneLocation(cursor.value.loc),
963
+ items: []
964
+ };
965
+ let comments = [];
966
+ cursor.next();
967
+ while (!cursor.done &&
968
+ !(cursor.value.type === TokenType.Bracket && cursor.value.raw === ']')) {
969
+ if (cursor.value.type === TokenType.Comma) {
970
+ const previous = value.items[value.items.length - 1];
971
+ if (!previous) {
972
+ throw new ParseError(input, cursor.value.loc.start, 'Found "," without previous value for inline array');
973
+ }
974
+ previous.comma = true;
975
+ previous.loc.end = cursor.value.loc.start;
976
+ }
977
+ else if (cursor.value.type === TokenType.Comment) {
978
+ comments.push(comment(cursor));
979
+ }
980
+ else {
981
+ const [item, ...additional_comments] = walkValue$1(cursor, input);
982
+ const inline_item = {
983
+ type: NodeType.InlineItem,
984
+ loc: cloneLocation(item.loc),
985
+ item,
986
+ comma: false
987
+ };
988
+ value.items.push(inline_item);
989
+ merge(comments, additional_comments);
990
+ }
991
+ cursor.next();
992
+ }
993
+ if (cursor.done ||
994
+ cursor.value.type !== TokenType.Bracket ||
995
+ cursor.value.raw !== ']') {
996
+ throw new ParseError(input, cursor.done ? value.loc.start : cursor.value.loc.start, `Expected "]", found ${cursor.done ? 'end of file' : cursor.value.raw}`);
997
+ }
998
+ value.loc.end = cursor.value.loc.end;
999
+ return [value, comments];
1000
+ }
1001
+
1002
+ ////////////////////////////////////////////////////////////////////////////////
1003
+ // The traverse function is used to walk the AST and call the visitor functions
1004
+ ////////////////////////////////////////////////////////////////////////////////
1005
+ function traverse(ast, visitor) {
1006
+ if (isIterable(ast)) {
1007
+ traverseArray(ast, null);
1008
+ }
1009
+ else {
1010
+ traverseNode(ast, null);
1011
+ }
1012
+ function traverseArray(array, parent) {
1013
+ for (const node of array) {
1014
+ traverseNode(node, parent);
1015
+ }
1016
+ }
1017
+ function traverseNode(node, parent) {
1018
+ const visit = visitor[node.type];
1019
+ if (visit && typeof visit === 'function') {
1020
+ visit(node, parent);
1021
+ }
1022
+ if (visit && visit.enter) {
1023
+ visit.enter(node, parent);
1024
+ }
1025
+ switch (node.type) {
1026
+ case NodeType.Document:
1027
+ traverseArray(node.items, node);
1028
+ break;
1029
+ case NodeType.Table:
1030
+ traverseNode(node.key, node);
1031
+ traverseArray(node.items, node);
1032
+ break;
1033
+ case NodeType.TableKey:
1034
+ traverseNode(node.item, node);
1035
+ break;
1036
+ case NodeType.TableArray:
1037
+ traverseNode(node.key, node);
1038
+ traverseArray(node.items, node);
1039
+ break;
1040
+ case NodeType.TableArrayKey:
1041
+ traverseNode(node.item, node);
1042
+ break;
1043
+ case NodeType.KeyValue:
1044
+ traverseNode(node.key, node);
1045
+ traverseNode(node.value, node);
1046
+ break;
1047
+ case NodeType.InlineArray:
1048
+ traverseArray(node.items, node);
1049
+ break;
1050
+ case NodeType.InlineItem:
1051
+ traverseNode(node.item, node);
1052
+ break;
1053
+ case NodeType.InlineTable:
1054
+ traverseArray(node.items, node);
1055
+ break;
1056
+ case NodeType.Key:
1057
+ case NodeType.String:
1058
+ case NodeType.Integer:
1059
+ case NodeType.Float:
1060
+ case NodeType.Boolean:
1061
+ case NodeType.DateTime:
1062
+ case NodeType.Comment:
1063
+ break;
1064
+ default:
1065
+ throw new Error(`Unrecognized node type "${node.type}"`);
1066
+ }
1067
+ if (visit && visit.exit) {
1068
+ visit.exit(node, parent);
1069
+ }
1070
+ }
1071
+ }
1072
+
1073
+ const enter_offsets = new WeakMap();
1074
+ const getEnterOffsets = (root) => {
1075
+ if (!enter_offsets.has(root)) {
1076
+ enter_offsets.set(root, new WeakMap());
1077
+ }
1078
+ return enter_offsets.get(root);
1079
+ };
1080
+ const exit_offsets = new WeakMap();
1081
+ const getExitOffsets = (root) => {
1082
+ if (!exit_offsets.has(root)) {
1083
+ exit_offsets.set(root, new WeakMap());
1084
+ }
1085
+ return exit_offsets.get(root);
1086
+ };
1087
+ //TODO: Add getOffsets function to get all offsets contained in the tree
1088
+ function replace(root, parent, existing, replacement) {
1089
+ // First, replace existing node
1090
+ // (by index for items, item, or key/value)
1091
+ if (hasItems(parent)) {
1092
+ const index = parent.items.indexOf(existing);
1093
+ if (index < 0)
1094
+ throw new Error(`Could not find existing item in parent node for replace`);
1095
+ parent.items.splice(index, 1, replacement);
1096
+ }
1097
+ else if (hasItem(parent)) {
1098
+ parent.item = replacement;
1099
+ }
1100
+ else if (isKeyValue(parent)) {
1101
+ if (parent.key === existing) {
1102
+ parent.key = replacement;
1103
+ }
1104
+ else {
1105
+ parent.value = replacement;
1106
+ }
1107
+ }
1108
+ else {
1109
+ throw new Error(`Unsupported parent type "${parent.type}" for replace`);
1110
+ }
1111
+ // Shift the replacement node into the same start position as existing
1112
+ const shift = {
1113
+ lines: existing.loc.start.line - replacement.loc.start.line,
1114
+ columns: existing.loc.start.column - replacement.loc.start.column
1115
+ };
1116
+ shiftNode(replacement, shift);
1117
+ // Apply offsets after replacement node
1118
+ const existing_span = getSpan(existing.loc);
1119
+ const replacement_span = getSpan(replacement.loc);
1120
+ const offset = {
1121
+ lines: replacement_span.lines - existing_span.lines,
1122
+ columns: replacement_span.columns - existing_span.columns
1123
+ };
1124
+ addOffset(offset, getExitOffsets(root), replacement, existing);
1125
+ }
1126
+ function insert(root, parent, child, index) {
1127
+ if (!hasItems(parent)) {
1128
+ throw new Error(`Unsupported parent type "${parent.type}" for insert`);
1129
+ }
1130
+ index = (index != null && typeof index === 'number') ? index : parent.items.length;
1131
+ let shift;
1132
+ let offset;
1133
+ if (isInlineArray(parent) || isInlineTable(parent)) {
1134
+ ({ shift, offset } = insertInline(parent, child, index));
1135
+ }
1136
+ else {
1137
+ ({ shift, offset } = insertOnNewLine(parent, child, index));
1138
+ }
1139
+ shiftNode(child, shift);
1140
+ // The child element is placed relative to the previous element,
1141
+ // if the previous element has an offset, need to position relative to that
1142
+ // -> Move previous offset to child's offset
1143
+ const previous = parent.items[index - 1];
1144
+ const previous_offset = previous && getExitOffsets(root).get(previous);
1145
+ if (previous_offset) {
1146
+ offset.lines += previous_offset.lines;
1147
+ offset.columns += previous_offset.columns;
1148
+ getExitOffsets(root).delete(previous);
1149
+ }
1150
+ const offsets = getExitOffsets(root);
1151
+ offsets.set(child, offset);
1152
+ }
1153
+ function insertOnNewLine(parent, child, index) {
1154
+ if (!isBlock(child)) {
1155
+ throw new Error(`Incompatible child type "${child.type}"`);
1156
+ }
1157
+ const previous = parent.items[index - 1];
1158
+ const use_first_line = isDocument(parent) && !parent.items.length;
1159
+ parent.items.splice(index, 0, child);
1160
+ // Set start location from previous item or start of array
1161
+ // (previous is undefined for empty array or inserting at first item)
1162
+ const start = previous
1163
+ ? {
1164
+ line: previous.loc.end.line,
1165
+ column: !isComment(previous) ? previous.loc.start.column : parent.loc.start.column
1166
+ }
1167
+ : clonePosition(parent.loc.start);
1168
+ const is_block = isTable(child) || isTableArray(child);
1169
+ let leading_lines = 0;
1170
+ if (use_first_line) ;
1171
+ else if (is_block) {
1172
+ leading_lines = 2;
1173
+ }
1174
+ else {
1175
+ leading_lines = 1;
1176
+ }
1177
+ start.line += leading_lines;
1178
+ const shift = {
1179
+ lines: start.line - child.loc.start.line,
1180
+ columns: start.column - child.loc.start.column
1181
+ };
1182
+ // Apply offsets after child node
1183
+ const child_span = getSpan(child.loc);
1184
+ const offset = {
1185
+ lines: child_span.lines + (leading_lines - 1),
1186
+ columns: child_span.columns
1187
+ };
1188
+ return { shift, offset };
1189
+ }
1190
+ /**
1191
+ * Inserts an inline element into an inline array or table at the specified index.
1192
+ * This function handles positioning, comma management, and offset calculation for the inserted item.
1193
+ *
1194
+ * @param parent - The inline array or table where the child will be inserted
1195
+ * @param child - The inline item to insert
1196
+ * @param index - The index position where to insert the child
1197
+ * @returns An object containing shift and offset spans:
1198
+ * - shift: Adjustments needed to position the child correctly
1199
+ * - offset: Adjustments needed for elements that follow the insertion
1200
+ * @throws Error if the child is not a compatible inline item type
1201
+ */
1202
+ function insertInline(parent, child, index) {
1203
+ if (!isInlineItem(child)) {
1204
+ throw new Error(`Incompatible child type "${child.type}"`);
1205
+ }
1206
+ // Store preceding node and insert
1207
+ const previous = index != null ? parent.items[index - 1] : last(parent.items);
1208
+ const is_last = index == null || index === parent.items.length;
1209
+ parent.items.splice(index, 0, child);
1210
+ // Add commas as-needed
1211
+ const has_seperating_comma_before = !!previous;
1212
+ const has_seperating_comma_after = !is_last;
1213
+ const has_trailing_comma = is_last && child.comma === true;
1214
+ if (has_seperating_comma_before) {
1215
+ previous.comma = true;
1216
+ }
1217
+ if (has_seperating_comma_after) {
1218
+ child.comma = true;
1219
+ }
1220
+ // Use a new line for documents, children of Table/TableArray,
1221
+ // and if an inline table is using new lines
1222
+ const use_new_line = isInlineArray(parent) && perLine(parent);
1223
+ // Set start location from previous item or start of array
1224
+ // (previous is undefined for empty array or inserting at first item)
1225
+ const start = previous
1226
+ ? {
1227
+ line: previous.loc.end.line,
1228
+ column: use_new_line
1229
+ ? !isComment(previous)
1230
+ ? previous.loc.start.column
1231
+ : parent.loc.start.column
1232
+ : previous.loc.end.column
1233
+ }
1234
+ : clonePosition(parent.loc.start);
1235
+ let leading_lines = 0;
1236
+ if (use_new_line) {
1237
+ leading_lines = 1;
1238
+ }
1239
+ else {
1240
+ const skip_comma = 2;
1241
+ const skip_bracket = 1;
1242
+ start.column += has_seperating_comma_before ? skip_comma : skip_bracket;
1243
+ }
1244
+ start.line += leading_lines;
1245
+ const shift = {
1246
+ lines: start.line - child.loc.start.line,
1247
+ columns: start.column - child.loc.start.column
1248
+ };
1249
+ // Apply offsets after child node
1250
+ const child_span = getSpan(child.loc);
1251
+ const offset = {
1252
+ lines: child_span.lines + (leading_lines - 1),
1253
+ columns: child_span.columns + (has_seperating_comma_before || has_seperating_comma_after ? 2 : 0) + (has_trailing_comma ? 1 : 0)
1254
+ };
1255
+ return { shift, offset };
1256
+ }
1257
+ function remove(root, parent, node) {
1258
+ // Remove an element from the parent's items
1259
+ // (supports Document, Table, TableArray, InlineTable, and InlineArray
1260
+ //
1261
+ // X
1262
+ // [ 1, 2, 3 ]
1263
+ // ^-^
1264
+ // -> Remove element 2 and apply 0,-3 offset to 1
1265
+ //
1266
+ // [table]
1267
+ // a = 1
1268
+ // b = 2 # X
1269
+ // c = 3
1270
+ // -> Remove element 2 and apply -1,0 offset to 1
1271
+ if (!hasItems(parent)) {
1272
+ throw new Error(`Unsupported parent type "${parent.type}" for remove`);
1273
+ }
1274
+ let index = parent.items.indexOf(node);
1275
+ if (index < 0) {
1276
+ // Try again, looking at child items for nodes like InlineArrayItem
1277
+ index = parent.items.findIndex(item => hasItem(item) && item.item === node);
1278
+ if (index < 0) {
1279
+ throw new Error('Could not find node in parent for removal');
1280
+ }
1281
+ node = parent.items[index];
1282
+ }
1283
+ const previous = parent.items[index - 1];
1284
+ let next = parent.items[index + 1];
1285
+ // Remove node
1286
+ parent.items.splice(index, 1);
1287
+ let removed_span = getSpan(node.loc);
1288
+ // Remove an associated comment that appears on the same line
1289
+ //
1290
+ // [table]
1291
+ // a = 1
1292
+ // b = 2 # remove this too
1293
+ // c = 3
1294
+ //
1295
+ // TODO InlineTable - this only applies to comments in Table/TableArray
1296
+ if (next && isComment(next) && next.loc.start.line === node.loc.end.line) {
1297
+ // Add comment to removed
1298
+ removed_span = getSpan({ start: node.loc.start, end: next.loc.end });
1299
+ // Shift to next item
1300
+ // (use same index since node has already been removed)
1301
+ next = parent.items[index + 1];
1302
+ // Remove comment
1303
+ parent.items.splice(index, 1);
1304
+ }
1305
+ // For inline tables and arrays, check whether the line should be kept
1306
+ const is_inline = previous && isInlineItem(previous);
1307
+ const previous_on_same_line = previous && previous.loc.end.line === node.loc.start.line;
1308
+ const next_on_sameLine = next && next.loc.start.line === node.loc.end.line;
1309
+ const keep_line = is_inline && (previous_on_same_line || next_on_sameLine);
1310
+ const offset = {
1311
+ lines: -(removed_span.lines - (keep_line ? 1 : 0)),
1312
+ columns: -removed_span.columns
1313
+ };
1314
+ // Offset for comma and remove comma that appear in front of the element (if-needed)
1315
+ if (is_inline && previous_on_same_line) {
1316
+ offset.columns -= 2;
1317
+ }
1318
+ if (is_inline && previous && !next) {
1319
+ previous.comma = false;
1320
+ }
1321
+ // Apply offsets after preceding node or before children of parent node
1322
+ const target = previous || parent;
1323
+ const target_offsets = previous ? getExitOffsets(root) : getEnterOffsets(root);
1324
+ const node_offsets = getExitOffsets(root);
1325
+ const previous_offset = target_offsets.get(target);
1326
+ if (previous_offset) {
1327
+ offset.lines += previous_offset.lines;
1328
+ offset.columns += previous_offset.columns;
1329
+ }
1330
+ const removed_offset = node_offsets.get(node);
1331
+ if (removed_offset) {
1332
+ offset.lines += removed_offset.lines;
1333
+ offset.columns += removed_offset.columns;
1334
+ }
1335
+ target_offsets.set(target, offset);
1336
+ }
1337
+ function applyBracketSpacing(root, node, bracket_spacing = true) {
1338
+ // Can only add bracket spacing currently
1339
+ if (!bracket_spacing)
1340
+ return;
1341
+ if (!node.items.length)
1342
+ return;
1343
+ // Apply enter to node so that items are affected
1344
+ addOffset({ lines: 0, columns: 1 }, getEnterOffsets(root), node);
1345
+ // Apply exit to last node in items
1346
+ const last_item = last(node.items);
1347
+ addOffset({ lines: 0, columns: 1 }, getExitOffsets(root), last_item);
1348
+ }
1349
+ function applyTrailingComma(root, node, trailing_commas = false) {
1350
+ // Can only add trailing comma currently
1351
+ if (!trailing_commas)
1352
+ return;
1353
+ if (!node.items.length)
1354
+ return;
1355
+ const last_item = last(node.items);
1356
+ last_item.comma = true;
1357
+ addOffset({ lines: 0, columns: 1 }, getExitOffsets(root), last_item);
1358
+ }
1359
+ /**
1360
+ * Applies all accumulated write offsets (enter and exit) to the given AST node.
1361
+ * This function adjusts the start and end locations of each node in the tree based on
1362
+ * the offsets stored in the `enter` and `exit` maps. It ensures that the tree's location
1363
+ * data is consistent after modifications.
1364
+ *
1365
+ * @param root - The root node of the AST tree to which the write offsets will be applied.
1366
+ */
1367
+ function applyWrites(root) {
1368
+ const enter = getEnterOffsets(root);
1369
+ const exit = getExitOffsets(root);
1370
+ const offset = {
1371
+ lines: 0,
1372
+ columns: {}
1373
+ };
1374
+ function shiftStart(node) {
1375
+ const lineOffset = offset.lines;
1376
+ node.loc.start.line += lineOffset;
1377
+ const columnOffset = offset.columns[node.loc.start.line] || 0;
1378
+ node.loc.start.column += columnOffset;
1379
+ const entering = enter.get(node);
1380
+ if (entering) {
1381
+ offset.lines += entering.lines;
1382
+ offset.columns[node.loc.start.line] =
1383
+ (offset.columns[node.loc.start.line] || 0) + entering.columns;
1384
+ }
1385
+ }
1386
+ function shiftEnd(node) {
1387
+ const lineOffset = offset.lines;
1388
+ node.loc.end.line += lineOffset;
1389
+ const columnOffset = offset.columns[node.loc.end.line] || 0;
1390
+ node.loc.end.column += columnOffset;
1391
+ const exiting = exit.get(node);
1392
+ if (exiting) {
1393
+ offset.lines += exiting.lines;
1394
+ offset.columns[node.loc.end.line] =
1395
+ (offset.columns[node.loc.end.line] || 0) + exiting.columns;
1396
+ }
1397
+ }
1398
+ const shiftLocation = {
1399
+ enter: shiftStart,
1400
+ exit: shiftEnd
1401
+ };
1402
+ traverse(root, {
1403
+ [NodeType.Document]: shiftLocation,
1404
+ [NodeType.Table]: shiftLocation,
1405
+ [NodeType.TableArray]: shiftLocation,
1406
+ [NodeType.InlineTable]: shiftLocation,
1407
+ [NodeType.InlineArray]: shiftLocation,
1408
+ [NodeType.InlineItem]: shiftLocation,
1409
+ [NodeType.TableKey]: shiftLocation,
1410
+ [NodeType.TableArrayKey]: shiftLocation,
1411
+ [NodeType.KeyValue]: {
1412
+ enter(node) {
1413
+ const start_line = node.loc.start.line + offset.lines;
1414
+ const key_offset = exit.get(node.key);
1415
+ node.equals += (offset.columns[start_line] || 0) + (key_offset ? key_offset.columns : 0);
1416
+ shiftStart(node);
1417
+ },
1418
+ exit: shiftEnd
1419
+ },
1420
+ [NodeType.Key]: shiftLocation,
1421
+ [NodeType.String]: shiftLocation,
1422
+ [NodeType.Integer]: shiftLocation,
1423
+ [NodeType.Float]: shiftLocation,
1424
+ [NodeType.Boolean]: shiftLocation,
1425
+ [NodeType.DateTime]: shiftLocation,
1426
+ [NodeType.Comment]: shiftLocation
1427
+ });
1428
+ enter_offsets.delete(root);
1429
+ exit_offsets.delete(root);
1430
+ }
1431
+ function shiftNode(node, span, options = {}) {
1432
+ const { first_line_only = false } = options;
1433
+ const start_line = node.loc.start.line;
1434
+ const { lines, columns } = span;
1435
+ const move = (node) => {
1436
+ if (!first_line_only || node.loc.start.line === start_line) {
1437
+ node.loc.start.column += columns;
1438
+ node.loc.end.column += columns;
1439
+ }
1440
+ node.loc.start.line += lines;
1441
+ node.loc.end.line += lines;
1442
+ };
1443
+ traverse(node, {
1444
+ [NodeType.Table]: move,
1445
+ [NodeType.TableKey]: move,
1446
+ [NodeType.TableArray]: move,
1447
+ [NodeType.TableArrayKey]: move,
1448
+ [NodeType.KeyValue](node) {
1449
+ move(node);
1450
+ node.equals += columns;
1451
+ },
1452
+ [NodeType.Key]: move,
1453
+ [NodeType.String]: move,
1454
+ [NodeType.Integer]: move,
1455
+ [NodeType.Float]: move,
1456
+ [NodeType.Boolean]: move,
1457
+ [NodeType.DateTime]: move,
1458
+ [NodeType.InlineArray]: move,
1459
+ [NodeType.InlineItem]: move,
1460
+ [NodeType.InlineTable]: move,
1461
+ [NodeType.Comment]: move
1462
+ });
1463
+ return node;
1464
+ }
1465
+ function perLine(array) {
1466
+ if (!array.items.length)
1467
+ return false;
1468
+ const span = getSpan(array.loc);
1469
+ return span.lines > array.items.length;
1470
+ }
1471
+ function addOffset(offset, offsets, node, from) {
1472
+ const previous_offset = offsets.get(from || node);
1473
+ if (previous_offset) {
1474
+ offset.lines += previous_offset.lines;
1475
+ offset.columns += previous_offset.columns;
1476
+ }
1477
+ offsets.set(node, offset);
1478
+ }
1479
+
1480
+ function generateDocument() {
1481
+ return {
1482
+ type: NodeType.Document,
1483
+ loc: { start: zero(), end: zero() },
1484
+ items: []
1485
+ };
1486
+ }
1487
+ function generateTable(key) {
1488
+ const table_key = generateTableKey(key);
1489
+ return {
1490
+ type: NodeType.Table,
1491
+ loc: cloneLocation(table_key.loc),
1492
+ key: table_key,
1493
+ items: []
1494
+ };
1495
+ }
1496
+ function generateTableKey(key) {
1497
+ const raw = keyValueToRaw(key);
1498
+ return {
1499
+ type: NodeType.TableKey,
1500
+ loc: {
1501
+ start: zero(),
1502
+ end: { line: 1, column: raw.length + 2 }
1503
+ },
1504
+ item: {
1505
+ type: NodeType.Key,
1506
+ loc: {
1507
+ start: { line: 1, column: 1 },
1508
+ end: { line: 1, column: raw.length + 1 }
1509
+ },
1510
+ value: key,
1511
+ raw
1512
+ }
1513
+ };
1514
+ }
1515
+ function generateTableArray(key) {
1516
+ const table_array_key = generateTableArrayKey(key);
1517
+ return {
1518
+ type: NodeType.TableArray,
1519
+ loc: cloneLocation(table_array_key.loc),
1520
+ key: table_array_key,
1521
+ items: []
1522
+ };
1523
+ }
1524
+ function generateTableArrayKey(key) {
1525
+ const raw = keyValueToRaw(key);
1526
+ return {
1527
+ type: NodeType.TableArrayKey,
1528
+ loc: {
1529
+ start: zero(),
1530
+ end: { line: 1, column: raw.length + 4 }
1531
+ },
1532
+ item: {
1533
+ type: NodeType.Key,
1534
+ loc: {
1535
+ start: { line: 1, column: 2 },
1536
+ end: { line: 1, column: raw.length + 2 }
1537
+ },
1538
+ value: key,
1539
+ raw
1540
+ }
1541
+ };
1542
+ }
1543
+ function generateKeyValue(key, value) {
1544
+ const key_node = generateKey(key);
1545
+ const { column } = key_node.loc.end;
1546
+ const equals = column + 1;
1547
+ shiftNode(value, { lines: 0, columns: column + 3 - value.loc.start.column }, { first_line_only: true });
1548
+ return {
1549
+ type: NodeType.KeyValue,
1550
+ loc: {
1551
+ start: clonePosition(key_node.loc.start),
1552
+ end: clonePosition(value.loc.end)
1553
+ },
1554
+ key: key_node,
1555
+ equals,
1556
+ value
1557
+ };
1558
+ }
1559
+ const IS_BARE_KEY = /[\w,\d,\_,\-]+/;
1560
+ function keyValueToRaw(value) {
1561
+ return value.map(part => (IS_BARE_KEY.test(part) ? part : JSON.stringify(part))).join('.');
1562
+ }
1563
+ function generateKey(value) {
1564
+ const raw = keyValueToRaw(value);
1565
+ return {
1566
+ type: NodeType.Key,
1567
+ loc: { start: zero(), end: { line: 1, column: raw.length } },
1568
+ raw,
1569
+ value
1570
+ };
1571
+ }
1572
+ function generateString(value) {
1573
+ const raw = JSON.stringify(value);
1574
+ return {
1575
+ type: NodeType.String,
1576
+ loc: { start: zero(), end: { line: 1, column: raw.length } },
1577
+ raw,
1578
+ value
1579
+ };
1580
+ }
1581
+ function generateInteger(value) {
1582
+ const raw = value.toString();
1583
+ return {
1584
+ type: NodeType.Integer,
1585
+ loc: { start: zero(), end: { line: 1, column: raw.length } },
1586
+ raw,
1587
+ value
1588
+ };
1589
+ }
1590
+ function generateFloat(value) {
1591
+ const raw = value.toString();
1592
+ return {
1593
+ type: NodeType.Float,
1594
+ loc: { start: zero(), end: { line: 1, column: raw.length } },
1595
+ raw,
1596
+ value
1597
+ };
1598
+ }
1599
+ function generateBoolean(value) {
1600
+ return {
1601
+ type: NodeType.Boolean,
1602
+ loc: { start: zero(), end: { line: 1, column: value ? 4 : 5 } },
1603
+ value
1604
+ };
1605
+ }
1606
+ function generateDateTime(value) {
1607
+ const raw = value.toISOString();
1608
+ return {
1609
+ type: NodeType.DateTime,
1610
+ loc: { start: zero(), end: { line: 1, column: raw.length } },
1611
+ raw,
1612
+ value
1613
+ };
1614
+ }
1615
+ function generateInlineArray() {
1616
+ return {
1617
+ type: NodeType.InlineArray,
1618
+ loc: { start: zero(), end: { line: 1, column: 2 } },
1619
+ items: []
1620
+ };
1621
+ }
1622
+ function generateInlineItem(item) {
1623
+ return {
1624
+ type: NodeType.InlineItem,
1625
+ loc: cloneLocation(item.loc),
1626
+ item,
1627
+ comma: false
1628
+ };
1629
+ }
1630
+ function generateInlineTable() {
1631
+ return {
1632
+ type: NodeType.InlineTable,
1633
+ loc: { start: zero(), end: { line: 1, column: 2 } },
1634
+ items: []
1635
+ };
1636
+ }
1637
+
1638
+ function formatTopLevel(document) {
1639
+ const move_to_top_level = document.items.filter(item => {
1640
+ if (!isKeyValue(item))
1641
+ return false;
1642
+ const is_inline_table = isInlineTable(item.value);
1643
+ const is_inline_array = isInlineArray(item.value) &&
1644
+ item.value.items.length &&
1645
+ isInlineTable(item.value.items[0].item);
1646
+ return is_inline_table || is_inline_array;
1647
+ });
1648
+ move_to_top_level.forEach(node => {
1649
+ remove(document, document, node);
1650
+ if (isInlineTable(node.value)) {
1651
+ insert(document, document, formatTable(node));
1652
+ }
1653
+ else {
1654
+ formatTableArray(node).forEach(table_array => {
1655
+ insert(document, document, table_array);
1656
+ });
1657
+ }
1658
+ });
1659
+ applyWrites(document);
1660
+ return document;
1661
+ }
1662
+ function formatTable(key_value) {
1663
+ const table = generateTable(key_value.key.value);
1664
+ for (const item of key_value.value.items) {
1665
+ insert(table, table, item.item);
1666
+ }
1667
+ applyWrites(table);
1668
+ return table;
1669
+ }
1670
+ function formatTableArray(key_value) {
1671
+ const root = generateDocument();
1672
+ for (const inline_array_item of key_value.value.items) {
1673
+ const table_array = generateTableArray(key_value.key.value);
1674
+ insert(root, root, table_array);
1675
+ for (const inline_table_item of inline_array_item.item.items) {
1676
+ insert(root, table_array, inline_table_item.item);
1677
+ }
1678
+ }
1679
+ applyWrites(root);
1680
+ return root.items;
1681
+ }
1682
+ function formatPrintWidth(document, format) {
1683
+ // TODO
1684
+ return document;
1685
+ }
1686
+ function formatEmptyLines(document) {
1687
+ let shift = 0;
1688
+ let previous = 0;
1689
+ for (const item of document.items) {
1690
+ if (previous === 0 && item.loc.start.line > 1) {
1691
+ // Remove leading newlines
1692
+ shift = 1 - item.loc.start.line;
1693
+ }
1694
+ else if (item.loc.start.line + shift > previous + 2) {
1695
+ shift += previous + 2 - (item.loc.start.line + shift);
1696
+ }
1697
+ shiftNode(item, {
1698
+ lines: shift,
1699
+ columns: 0
1700
+ });
1701
+ previous = item.loc.end.line;
1702
+ }
1703
+ return document;
1704
+ }
1705
+
1706
+ const default_format = {
1707
+ printWidth: 80,
1708
+ trailingComma: false,
1709
+ bracketSpacing: true
1710
+ };
1711
+ function parseJS(value, format = {}) {
1712
+ format = Object.assign({}, default_format, format);
1713
+ value = toJSON(value);
1714
+ // Reorder the elements in the object
1715
+ value = reorderElements(value);
1716
+ const document = generateDocument();
1717
+ for (const item of walkObject(value, format)) {
1718
+ insert(document, document, item);
1719
+ }
1720
+ applyWrites(document);
1721
+ // Heuristics:
1722
+ // 1. Top-level objects/arrays should be tables/table arrays
1723
+ // 2. Convert objects/arrays to tables/table arrays based on print width
1724
+ const formatted = pipe(document, formatTopLevel, document => formatPrintWidth(document), formatEmptyLines);
1725
+ return formatted;
1726
+ }
1727
+ /**
1728
+ This function makes sure that properties that are simple values (not objects or arrays) are ordered first,
1729
+ and that objects and arrays are ordered last. This makes parseJS more reliable and easier to test.
1730
+ */
1731
+ function reorderElements(value) {
1732
+ let result = {};
1733
+ // First add all simple values
1734
+ for (const key in value) {
1735
+ if (!isObject(value[key]) && !Array.isArray(value[key])) {
1736
+ result[key] = value[key];
1737
+ }
1738
+ }
1739
+ // Then add all objects and arrays
1740
+ for (const key in value) {
1741
+ if (isObject(value[key]) || Array.isArray(value[key])) {
1742
+ result[key] = value[key];
1743
+ }
1744
+ }
1745
+ return result;
1746
+ }
1747
+ function* walkObject(object, format) {
1748
+ for (const key of Object.keys(object)) {
1749
+ yield generateKeyValue([key], walkValue(object[key], format));
1750
+ }
1751
+ }
1752
+ function walkValue(value, format) {
1753
+ if (value == null) {
1754
+ throw new Error('"null" and "undefined" values are not supported');
1755
+ }
1756
+ if (isString(value)) {
1757
+ return generateString(value);
1758
+ }
1759
+ else if (isInteger(value)) {
1760
+ return generateInteger(value);
1761
+ }
1762
+ else if (isFloat(value)) {
1763
+ return generateFloat(value);
1764
+ }
1765
+ else if (isBoolean(value)) {
1766
+ return generateBoolean(value);
1767
+ }
1768
+ else if (isDate(value)) {
1769
+ return generateDateTime(value);
1770
+ }
1771
+ else if (Array.isArray(value)) {
1772
+ return walkInlineArray(value, format);
1773
+ }
1774
+ else {
1775
+ return walkInlineTable(value, format);
1776
+ }
1777
+ }
1778
+ function walkInlineArray(value, format) {
1779
+ const inline_array = generateInlineArray();
1780
+ for (const element of value) {
1781
+ const item = walkValue(element, format);
1782
+ const inline_array_item = generateInlineItem(item);
1783
+ insert(inline_array, inline_array, inline_array_item);
1784
+ }
1785
+ applyBracketSpacing(inline_array, inline_array, format.bracketSpacing);
1786
+ applyTrailingComma(inline_array, inline_array, format.trailingComma);
1787
+ applyWrites(inline_array);
1788
+ return inline_array;
1789
+ }
1790
+ function walkInlineTable(value, format) {
1791
+ value = toJSON(value);
1792
+ if (!isObject(value))
1793
+ return walkValue(value, format);
1794
+ const inline_table = generateInlineTable();
1795
+ const items = [...walkObject(value, format)];
1796
+ for (const item of items) {
1797
+ const inline_table_item = generateInlineItem(item);
1798
+ insert(inline_table, inline_table, inline_table_item);
1799
+ }
1800
+ applyBracketSpacing(inline_table, inline_table, format.bracketSpacing);
1801
+ applyTrailingComma(inline_table, inline_table, format.trailingComma);
1802
+ applyWrites(inline_table);
1803
+ return inline_table;
1804
+ }
1805
+ /**
1806
+ * Handles custom object serialization by checking for and using toJSON methods
1807
+ *
1808
+ * @param value - The value to potentially convert
1809
+ * @returns The result of value.toJSON() if available, otherwise the original value
1810
+ */
1811
+ function toJSON(value) {
1812
+ // Skip null/undefined values
1813
+ if (!value) {
1814
+ return value;
1815
+ }
1816
+ // Skip Date objects (they have special handling)
1817
+ if (isDate(value)) {
1818
+ return value;
1819
+ }
1820
+ // Use object's custom toJSON method if available
1821
+ if (typeof value.toJSON === 'function') {
1822
+ return value.toJSON();
1823
+ }
1824
+ // Otherwise return unmodified
1825
+ return value;
1826
+ }
1827
+
1828
+ const BY_NEW_LINE = /(\r\n|\n)/g;
1829
+ function toTOML(ast, newline = '\n') {
1830
+ const lines = [];
1831
+ traverse(ast, {
1832
+ [NodeType.TableKey](node) {
1833
+ const { start, end } = node.loc;
1834
+ write(lines, { start, end: { line: start.line, column: start.column + 1 } }, '[');
1835
+ write(lines, { start: { line: end.line, column: end.column - 1 }, end }, ']');
1836
+ },
1837
+ [NodeType.TableArrayKey](node) {
1838
+ const { start, end } = node.loc;
1839
+ write(lines, { start, end: { line: start.line, column: start.column + 2 } }, '[[');
1840
+ write(lines, { start: { line: end.line, column: end.column - 2 }, end }, ']]');
1841
+ },
1842
+ [NodeType.KeyValue](node) {
1843
+ const { start: { line } } = node.loc;
1844
+ write(lines, { start: { line, column: node.equals }, end: { line, column: node.equals + 1 } }, '=');
1845
+ },
1846
+ [NodeType.Key](node) {
1847
+ write(lines, node.loc, node.raw);
1848
+ },
1849
+ [NodeType.String](node) {
1850
+ write(lines, node.loc, node.raw);
1851
+ },
1852
+ [NodeType.Integer](node) {
1853
+ write(lines, node.loc, node.raw);
1854
+ },
1855
+ [NodeType.Float](node) {
1856
+ write(lines, node.loc, node.raw);
1857
+ },
1858
+ [NodeType.Boolean](node) {
1859
+ write(lines, node.loc, node.value.toString());
1860
+ },
1861
+ [NodeType.DateTime](node) {
1862
+ write(lines, node.loc, node.raw);
1863
+ },
1864
+ [NodeType.InlineArray](node) {
1865
+ const { start, end } = node.loc;
1866
+ write(lines, { start, end: { line: start.line, column: start.column + 1 } }, '[');
1867
+ write(lines, { start: { line: end.line, column: end.column - 1 }, end }, ']');
1868
+ },
1869
+ [NodeType.InlineTable](node) {
1870
+ const { start, end } = node.loc;
1871
+ write(lines, { start, end: { line: start.line, column: start.column + 1 } }, '{');
1872
+ write(lines, { start: { line: end.line, column: end.column - 1 }, end }, '}');
1873
+ },
1874
+ [NodeType.InlineItem](node) {
1875
+ if (!node.comma)
1876
+ return;
1877
+ const start = node.loc.end;
1878
+ write(lines, { start, end: { line: start.line, column: start.column + 1 } }, ',');
1879
+ },
1880
+ [NodeType.Comment](node) {
1881
+ write(lines, node.loc, node.raw);
1882
+ }
1883
+ });
1884
+ return lines.join(newline) + newline;
1885
+ }
1886
+ function write(lines, loc, raw) {
1887
+ const raw_lines = raw.split(BY_NEW_LINE).filter(line => line !== '\n' && line !== '\r\n');
1888
+ const expected_lines = loc.end.line - loc.start.line + 1;
1889
+ if (raw_lines.length !== expected_lines) {
1890
+ throw new Error(`Mismatch between location and raw string, expected ${expected_lines} lines for "${raw}"`);
1891
+ }
1892
+ for (let i = loc.start.line; i <= loc.end.line; i++) {
1893
+ const line = getLine(lines, i);
1894
+ const is_start_line = i === loc.start.line;
1895
+ const is_end_line = i === loc.end.line;
1896
+ const before = is_start_line
1897
+ ? line.substr(0, loc.start.column).padEnd(loc.start.column, SPACE)
1898
+ : '';
1899
+ const after = is_end_line ? line.substr(loc.end.column) : '';
1900
+ lines[i - 1] = before + raw_lines[i - loc.start.line] + after;
1901
+ }
1902
+ }
1903
+ function getLine(lines, index) {
1904
+ if (!lines[index - 1]) {
1905
+ for (let i = 0; i < index; i++) {
1906
+ if (!lines[i])
1907
+ lines[i] = '';
1908
+ }
1909
+ }
1910
+ return lines[index - 1];
1911
+ }
1912
+
1913
+ function toJS(ast, input = '') {
1914
+ const result = blank();
1915
+ const tables = new Set();
1916
+ const table_arrays = new Set();
1917
+ const defined = new Set();
1918
+ let active = result;
1919
+ let previous_active;
1920
+ let skip = false;
1921
+ traverse(ast, {
1922
+ [NodeType.Table](node) {
1923
+ const key = node.key.item.value;
1924
+ try {
1925
+ validateKey(result, key, node.type, { tables, table_arrays, defined });
1926
+ }
1927
+ catch (err) {
1928
+ const e = err;
1929
+ throw new ParseError(input, node.key.loc.start, e.message);
1930
+ }
1931
+ const joined_key = joinKey(key);
1932
+ tables.add(joined_key);
1933
+ defined.add(joined_key);
1934
+ active = ensureTable(result, key);
1935
+ },
1936
+ [NodeType.TableArray](node) {
1937
+ const key = node.key.item.value;
1938
+ try {
1939
+ validateKey(result, key, node.type, { tables, table_arrays, defined });
1940
+ }
1941
+ catch (err) {
1942
+ const e = err;
1943
+ throw new ParseError(input, node.key.loc.start, e.message);
1944
+ }
1945
+ const joined_key = joinKey(key);
1946
+ table_arrays.add(joined_key);
1947
+ defined.add(joined_key);
1948
+ active = ensureTableArray(result, key);
1949
+ },
1950
+ [NodeType.KeyValue]: {
1951
+ enter(node) {
1952
+ if (skip)
1953
+ return;
1954
+ const key = node.key.value;
1955
+ try {
1956
+ validateKey(active, key, node.type, { tables, table_arrays, defined });
1957
+ }
1958
+ catch (err) {
1959
+ const e = err;
1960
+ throw new ParseError(input, node.key.loc.start, e.message);
1961
+ }
1962
+ const value = toValue(node.value);
1963
+ const target = key.length > 1 ? ensureTable(active, key.slice(0, -1)) : active;
1964
+ target[last(key)] = value;
1965
+ defined.add(joinKey(key));
1966
+ if (isInlineTable(node.value)) {
1967
+ previous_active = active;
1968
+ active = value;
1969
+ }
1970
+ },
1971
+ exit(node) {
1972
+ if (isInlineTable(node.value)) {
1973
+ active = previous_active;
1974
+ }
1975
+ }
1976
+ },
1977
+ [NodeType.InlineTable]: {
1978
+ enter() {
1979
+ // Handled by toValue
1980
+ skip = true;
1981
+ },
1982
+ exit() {
1983
+ skip = false;
1984
+ }
1985
+ }
1986
+ });
1987
+ return result;
1988
+ }
1989
+ function toValue(node) {
1990
+ switch (node.type) {
1991
+ case NodeType.InlineTable:
1992
+ const result = blank();
1993
+ node.items.forEach(({ item }) => {
1994
+ const key = item.key.value;
1995
+ const value = toValue(item.value);
1996
+ const target = key.length > 1 ? ensureTable(result, key.slice(0, -1)) : result;
1997
+ target[last(key)] = value;
1998
+ });
1999
+ return result;
2000
+ case NodeType.InlineArray:
2001
+ return node.items.map(item => toValue(item.item));
2002
+ case NodeType.String:
2003
+ case NodeType.Integer:
2004
+ case NodeType.Float:
2005
+ case NodeType.Boolean:
2006
+ case NodeType.DateTime:
2007
+ return node.value;
2008
+ default:
2009
+ throw new Error(`Unrecognized value type "${node.type}"`);
2010
+ }
2011
+ }
2012
+ function validateKey(object, key, type, state) {
2013
+ // 1. Cannot override primitive value
2014
+ let parts = [];
2015
+ let index = 0;
2016
+ for (const part of key) {
2017
+ parts.push(part);
2018
+ if (!has(object, part))
2019
+ return;
2020
+ if (isPrimitive(object[part])) {
2021
+ throw new Error(`Invalid key, a value has already been defined for ${parts.join('.')}`);
2022
+ }
2023
+ const joined_parts = joinKey(parts);
2024
+ if (Array.isArray(object[part]) && !state.table_arrays.has(joined_parts)) {
2025
+ throw new Error(`Invalid key, cannot add to a static array at ${joined_parts}`);
2026
+ }
2027
+ const next_is_last = index++ < key.length - 1;
2028
+ object = Array.isArray(object[part]) && next_is_last ? last(object[part]) : object[part];
2029
+ }
2030
+ const joined_key = joinKey(key);
2031
+ // 2. Cannot override table
2032
+ if (object && type === NodeType.Table && state.defined.has(joined_key)) {
2033
+ throw new Error(`Invalid key, a table has already been defined named ${joined_key}`);
2034
+ }
2035
+ // 3. Cannot add table array to static array or table
2036
+ if (object && type === NodeType.TableArray && !state.table_arrays.has(joined_key)) {
2037
+ throw new Error(`Invalid key, cannot add an array of tables to a table at ${joined_key}`);
2038
+ }
2039
+ }
2040
+ function ensureTable(object, key) {
2041
+ const target = ensure(object, key.slice(0, -1));
2042
+ const last_key = last(key);
2043
+ if (!target[last_key]) {
2044
+ target[last_key] = blank();
2045
+ }
2046
+ return target[last_key];
2047
+ }
2048
+ function ensureTableArray(object, key) {
2049
+ const target = ensure(object, key.slice(0, -1));
2050
+ const last_key = last(key);
2051
+ if (!target[last_key]) {
2052
+ target[last_key] = [];
2053
+ }
2054
+ const next = blank();
2055
+ target[last(key)].push(next);
2056
+ return next;
2057
+ }
2058
+ function ensure(object, keys) {
2059
+ return keys.reduce((active, subkey) => {
2060
+ if (!active[subkey]) {
2061
+ active[subkey] = blank();
2062
+ }
2063
+ return Array.isArray(active[subkey]) ? last(active[subkey]) : active[subkey];
2064
+ }, object);
2065
+ }
2066
+ function isPrimitive(value) {
2067
+ return typeof value !== 'object' && !isDate(value);
2068
+ }
2069
+ function joinKey(key) {
2070
+ return key.join('.');
2071
+ }
2072
+
2073
+ var ChangeType;
2074
+ (function (ChangeType) {
2075
+ ChangeType["Add"] = "Add";
2076
+ ChangeType["Edit"] = "Edit";
2077
+ ChangeType["Remove"] = "Remove";
2078
+ ChangeType["Move"] = "Move";
2079
+ ChangeType["Rename"] = "Rename";
2080
+ })(ChangeType || (ChangeType = {}));
2081
+ function isAdd(change) {
2082
+ return change.type === ChangeType.Add;
2083
+ }
2084
+ function isEdit(change) {
2085
+ return change.type === ChangeType.Edit;
2086
+ }
2087
+ function isRemove(change) {
2088
+ return change.type === ChangeType.Remove;
2089
+ }
2090
+ function isMove(change) {
2091
+ return change.type === ChangeType.Move;
2092
+ }
2093
+ function isRename(change) {
2094
+ return change.type === ChangeType.Rename;
2095
+ }
2096
+ function diff(before, after, path = []) {
2097
+ if (before === after || datesEqual(before, after)) {
2098
+ return [];
2099
+ }
2100
+ if (Array.isArray(before) && Array.isArray(after)) {
2101
+ return compareArrays(before, after, path);
2102
+ }
2103
+ else if (isObject(before) && isObject(after)) {
2104
+ return compareObjects(before, after, path);
2105
+ }
2106
+ else {
2107
+ return [
2108
+ {
2109
+ type: ChangeType.Edit,
2110
+ path
2111
+ }
2112
+ ];
2113
+ }
2114
+ }
2115
+ function compareObjects(before, after, path = []) {
2116
+ let changes = [];
2117
+ // 1. Get keys and stable values
2118
+ const before_keys = Object.keys(before);
2119
+ const before_stable = before_keys.map(key => stableStringify(before[key]));
2120
+ const after_keys = Object.keys(after);
2121
+ const after_stable = after_keys.map(key => stableStringify(after[key]));
2122
+ // Check for rename by seeing if object is in both before and after
2123
+ // and that key is no longer used in after
2124
+ const isRename = (stable, search) => {
2125
+ const index = search.indexOf(stable);
2126
+ if (index < 0)
2127
+ return false;
2128
+ const before_key = before_keys[before_stable.indexOf(stable)];
2129
+ return !after_keys.includes(before_key);
2130
+ };
2131
+ // 2. Check for changes, rename, and removed
2132
+ before_keys.forEach((key, index) => {
2133
+ const sub_path = path.concat(key);
2134
+ if (after_keys.includes(key)) {
2135
+ merge(changes, diff(before[key], after[key], sub_path));
2136
+ }
2137
+ else if (isRename(before_stable[index], after_stable)) {
2138
+ const to = after_keys[after_stable.indexOf(before_stable[index])];
2139
+ changes.push({
2140
+ type: ChangeType.Rename,
2141
+ path,
2142
+ from: key,
2143
+ to
2144
+ });
2145
+ }
2146
+ else {
2147
+ changes.push({
2148
+ type: ChangeType.Remove,
2149
+ path: sub_path
2150
+ });
2151
+ }
2152
+ });
2153
+ // 3. Check for additions
2154
+ after_keys.forEach((key, index) => {
2155
+ if (!before_keys.includes(key) && !isRename(after_stable[index], before_stable)) {
2156
+ changes.push({
2157
+ type: ChangeType.Add,
2158
+ path: path.concat(key)
2159
+ });
2160
+ }
2161
+ });
2162
+ return changes;
2163
+ }
2164
+ function compareArrays(before, after, path = []) {
2165
+ let changes = [];
2166
+ // 1. Convert arrays to stable objects
2167
+ const before_stable = before.map(stableStringify);
2168
+ const after_stable = after.map(stableStringify);
2169
+ // 2. Step through after array making changes to before array as-needed
2170
+ after_stable.forEach((value, index) => {
2171
+ const overflow = index >= before_stable.length;
2172
+ // Check if items are the same
2173
+ if (!overflow && before_stable[index] === value) {
2174
+ return;
2175
+ }
2176
+ // Check if item has been moved -> shift into place
2177
+ const from = before_stable.indexOf(value, index + 1);
2178
+ if (!overflow && from > -1) {
2179
+ changes.push({
2180
+ type: ChangeType.Move,
2181
+ path,
2182
+ from,
2183
+ to: index
2184
+ });
2185
+ const move = before_stable.splice(from, 1);
2186
+ before_stable.splice(index, 0, ...move);
2187
+ return;
2188
+ }
2189
+ // Check if item is removed -> assume it's been edited and replace
2190
+ const removed = !after_stable.includes(before_stable[index]);
2191
+ if (!overflow && removed) {
2192
+ merge(changes, diff(before[index], after[index], path.concat(index)));
2193
+ before_stable[index] = value;
2194
+ return;
2195
+ }
2196
+ // Add as new item and shift existing
2197
+ changes.push({
2198
+ type: ChangeType.Add,
2199
+ path: path.concat(index)
2200
+ });
2201
+ before_stable.splice(index, 0, value);
2202
+ });
2203
+ // 3. Remove any remaining overflow items
2204
+ for (let i = after_stable.length; i < before_stable.length; i++) {
2205
+ changes.push({
2206
+ type: ChangeType.Remove,
2207
+ path: path.concat(i)
2208
+ });
2209
+ }
2210
+ return changes;
2211
+ }
2212
+
2213
+ function findByPath(node, path) {
2214
+ if (!path.length)
2215
+ return node;
2216
+ if (isKeyValue(node)) {
2217
+ return findByPath(node.value, path);
2218
+ }
2219
+ const indexes = {};
2220
+ let found;
2221
+ if (hasItems(node)) {
2222
+ node.items.some((item, index) => {
2223
+ try {
2224
+ let key = [];
2225
+ if (isKeyValue(item)) {
2226
+ key = item.key.value;
2227
+ }
2228
+ else if (isTable(item)) {
2229
+ key = item.key.item.value;
2230
+ }
2231
+ else if (isTableArray(item)) {
2232
+ key = item.key.item.value;
2233
+ const key_string = stableStringify(key);
2234
+ if (!indexes[key_string]) {
2235
+ indexes[key_string] = 0;
2236
+ }
2237
+ const array_index = indexes[key_string]++;
2238
+ key = key.concat(array_index);
2239
+ }
2240
+ else if (isInlineItem(item) && isKeyValue(item.item)) {
2241
+ key = item.item.key.value;
2242
+ }
2243
+ else if (isInlineItem(item)) {
2244
+ key = [index];
2245
+ }
2246
+ if (key.length && arraysEqual(key, path.slice(0, key.length))) {
2247
+ found = findByPath(item, path.slice(key.length));
2248
+ return true;
2249
+ }
2250
+ else {
2251
+ return false;
2252
+ }
2253
+ }
2254
+ catch (err) {
2255
+ return false;
2256
+ }
2257
+ });
2258
+ }
2259
+ if (!found) {
2260
+ throw new Error(`Could not find node at path ${path.join('.')}`);
2261
+ }
2262
+ return found;
2263
+ }
2264
+ function tryFindByPath(node, path) {
2265
+ try {
2266
+ return findByPath(node, path);
2267
+ }
2268
+ catch (err) { }
2269
+ }
2270
+ function findParent(node, path) {
2271
+ let parent_path = path;
2272
+ let parent;
2273
+ while (parent_path.length && !parent) {
2274
+ parent_path = parent_path.slice(0, -1);
2275
+ parent = tryFindByPath(node, parent_path);
2276
+ }
2277
+ if (!parent) {
2278
+ throw new Error(`Count not find parent node for path ${path.join('.')}`);
2279
+ }
2280
+ return parent;
2281
+ }
2282
+
2283
+ function patch(existing, updated, format) {
2284
+ const existing_ast = parseTOML(existing);
2285
+ const items = [...existing_ast];
2286
+ const existing_js = toJS(items);
2287
+ const existing_document = {
2288
+ type: NodeType.Document,
2289
+ loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 0 } },
2290
+ items
2291
+ };
2292
+ const updated_document = parseJS(updated, format);
2293
+ const changes = reorder(diff(existing_js, updated));
2294
+ const patched_document = applyChanges(existing_document, updated_document, changes);
2295
+ // Validate the patched_document
2296
+ //validate(patched_document);
2297
+ return toTOML(patched_document.items);
2298
+ }
2299
+ function reorder(changes) {
2300
+ for (let i = 0; i < changes.length; i++) {
2301
+ const change = changes[i];
2302
+ if (isRemove(change)) {
2303
+ let j = i + 1;
2304
+ while (j < changes.length) {
2305
+ const next_change = changes[j];
2306
+ if (isRemove(next_change) && next_change.path[0] === change.path[0] &&
2307
+ next_change.path[1] > change.path[1]) {
2308
+ changes.splice(j, 1);
2309
+ changes.splice(i, 0, next_change);
2310
+ // We reset i to the beginning of the loop to avoid skipping any changes
2311
+ i = 0;
2312
+ break;
2313
+ }
2314
+ j++;
2315
+ }
2316
+ }
2317
+ }
2318
+ return changes;
2319
+ }
2320
+ function applyChanges(original, updated, changes) {
2321
+ // Potential Changes:
2322
+ //
2323
+ // Add: Add key-value to object, add item to array
2324
+ // Edit: Change in value
2325
+ // Remove: Remove key-value from object, remove item from array
2326
+ // Move: Move item in array
2327
+ // Rename: Rename key in key-value
2328
+ //
2329
+ // Special consideration, inline comments need to move as-needed
2330
+ changes.forEach(change => {
2331
+ if (isAdd(change)) {
2332
+ const child = findByPath(updated, change.path);
2333
+ const parent_path = change.path.slice(0, -1);
2334
+ let index = last(change.path);
2335
+ let is_table_array = isTableArray(child);
2336
+ if (isInteger(index) && !parent_path.some(isInteger)) {
2337
+ const sibling = tryFindByPath(original, parent_path.concat(0));
2338
+ if (sibling && isTableArray(sibling)) {
2339
+ is_table_array = true;
2340
+ }
2341
+ }
2342
+ let parent;
2343
+ if (isTable(child)) {
2344
+ parent = original;
2345
+ }
2346
+ else if (is_table_array) {
2347
+ parent = original;
2348
+ // The index needs to be updated to top-level items
2349
+ // to properly account for other items, comments, and nesting
2350
+ const document = original;
2351
+ const before = tryFindByPath(document, parent_path.concat(index - 1));
2352
+ const after = tryFindByPath(document, parent_path.concat(index));
2353
+ if (after) {
2354
+ index = document.items.indexOf(after);
2355
+ }
2356
+ else if (before) {
2357
+ index = document.items.indexOf(before) + 1;
2358
+ }
2359
+ else {
2360
+ index = document.items.length;
2361
+ }
2362
+ }
2363
+ else {
2364
+ parent = findParent(original, change.path);
2365
+ if (isKeyValue(parent))
2366
+ parent = parent.value;
2367
+ }
2368
+ if (isTableArray(parent) || isInlineArray(parent) || isDocument(parent)) {
2369
+ insert(original, parent, child, index);
2370
+ }
2371
+ else {
2372
+ insert(original, parent, child);
2373
+ }
2374
+ }
2375
+ else if (isEdit(change)) {
2376
+ let existing = findByPath(original, change.path);
2377
+ let replacement = findByPath(updated, change.path);
2378
+ let parent;
2379
+ if (isKeyValue(existing) && isKeyValue(replacement)) {
2380
+ // Edit for key-value means value changes
2381
+ parent = existing;
2382
+ existing = existing.value;
2383
+ replacement = replacement.value;
2384
+ }
2385
+ else {
2386
+ parent = findParent(original, change.path);
2387
+ }
2388
+ replace(original, parent, existing, replacement);
2389
+ }
2390
+ else if (isRemove(change)) {
2391
+ let parent = findParent(original, change.path);
2392
+ if (isKeyValue(parent))
2393
+ parent = parent.value;
2394
+ const node = findByPath(original, change.path);
2395
+ remove(original, parent, node);
2396
+ }
2397
+ else if (isMove(change)) {
2398
+ let parent = findByPath(original, change.path);
2399
+ if (hasItem(parent))
2400
+ parent = parent.item;
2401
+ if (isKeyValue(parent))
2402
+ parent = parent.value;
2403
+ const node = parent.items[change.from];
2404
+ remove(original, parent, node);
2405
+ insert(original, parent, node, change.to);
2406
+ }
2407
+ else if (isRename(change)) {
2408
+ let parent = findByPath(original, change.path.concat(change.from));
2409
+ let replacement = findByPath(updated, change.path.concat(change.to));
2410
+ if (hasItem(parent))
2411
+ parent = parent.item;
2412
+ if (hasItem(replacement))
2413
+ replacement = replacement.item;
2414
+ replace(original, parent, parent.key, replacement.key);
2415
+ }
2416
+ });
2417
+ applyWrites(original);
2418
+ return original;
2419
+ }
2420
+
2421
+ function parse(value) {
2422
+ return toJS(parseTOML(value), value);
2423
+ }
2424
+ function stringify(value, format) {
2425
+ const document = parseJS(value, format);
2426
+ return toTOML(document.items);
2427
+ }
2428
+
2429
+ export { parse, patch, stringify };