@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.
- package/LICENSE +21 -0
- package/README.md +114 -0
- package/dist/toml-patch.cjs.min.js +3 -0
- package/dist/toml-patch.cjs.min.js.map +1 -0
- package/dist/toml-patch.d.ts +15 -0
- package/dist/toml-patch.es.js +2429 -0
- package/dist/toml-patch.umd.min.js +3 -0
- package/dist/toml-patch.umd.min.js.map +1 -0
- package/package.json +73 -0
|
@@ -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 };
|