@amxts/config-core 0.1.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/PAWN.md +13 -0
- package/PAWN.ru.md +13 -0
- package/README.md +249 -0
- package/README.ru.md +249 -0
- package/include/universal_config.inc +355 -0
- package/package.json +67 -0
- package/prebuilt/linux/config-core.aot +0 -0
- package/prebuilt/manifest.json +2281 -0
- package/prebuilt/windows/config-core.aot +0 -0
- package/scripts/docs/src/index.ts +197 -0
- package/scripts/docs/src/types.ts +20 -0
- package/src/files.ts +27 -0
- package/src/index.ts +360 -0
- package/src/ini-tree.ts +144 -0
- package/src/ini.ts +723 -0
- package/src/internal.ts +44 -0
- package/src/json.ts +386 -0
- package/src/natives.ts +271 -0
- package/src/tree.ts +171 -0
- package/src/typed.ts +419 -0
- package/src/types.ts +22 -0
- package/src/yaml.ts +840 -0
package/src/yaml.ts
ADDED
|
@@ -0,0 +1,840 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* YAML for Config Core: the part of it configs are written in, read into the
|
|
3
|
+
* tree and written back. Block and flow mappings and lists, plain, quoted and
|
|
4
|
+
* block (| and >) text, comments, one document. Anchors, aliases, tags,
|
|
5
|
+
* complex keys, directives and several documents in a file are not read:
|
|
6
|
+
* the reader stops there and says where. Not part of the API.
|
|
7
|
+
*/
|
|
8
|
+
import { Place, TreeDocument, TreeNode } from "./internal";
|
|
9
|
+
import { jsonText } from "./json";
|
|
10
|
+
import { BACKSLASH, isControl, isDigits, isHexDigits, isScalar, lineStarts, makeNode, numberNode, placeOf, writeComments } from "./tree";
|
|
11
|
+
|
|
12
|
+
const NULLS = ["", "~", "null", "Null", "NULL"];
|
|
13
|
+
const TRUES = ["true", "True", "TRUE"];
|
|
14
|
+
const FALSES = ["false", "False", "FALSE"];
|
|
15
|
+
const INFINITIES = [".inf", ".Inf", ".INF"];
|
|
16
|
+
const NANS = [".nan", ".NaN", ".NAN"];
|
|
17
|
+
/** What a plain value cannot start with: YAML reads it as something else. */
|
|
18
|
+
const INDICATORS = "-?:,[]{}#&*!|>'\"%@`";
|
|
19
|
+
/** Escapes of double-quoted text: the letter after the backslash, and what it stands for, at the same place. */
|
|
20
|
+
const ESCAPE_LETTERS = "0abtnvfre \"/\\N";
|
|
21
|
+
const ESCAPED = "\u0000\u0007\u0008\u0009\u000A\u000B\u000C\u000D\u001B \"/\\\u0085";
|
|
22
|
+
|
|
23
|
+
function isSpace(character: string) {
|
|
24
|
+
return character == " " || character == "\t";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** A space, a line end, or the end of the text. */
|
|
28
|
+
function isBlank(character: string) {
|
|
29
|
+
return character == " " || character == "\t" || character == "\n" || character == "";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** A number as YAML's core schema reads one: 12, -3.5, 1e3, 0x1F, 0o17, .inf, .nan. */
|
|
33
|
+
function isNumber(text: string) {
|
|
34
|
+
if (NANS.includes(text)) return true;
|
|
35
|
+
if (text.startsWith("0x")) return isHexDigits(text.slice(2));
|
|
36
|
+
if (text.startsWith("0o")) return isDigits(text.slice(2)) && !text.includes("8") && !text.includes("9");
|
|
37
|
+
const body = text.startsWith("-") || text.startsWith("+") ? text.slice(1) : text;
|
|
38
|
+
if (INFINITIES.includes(body)) return true;
|
|
39
|
+
|
|
40
|
+
let mantissa = body;
|
|
41
|
+
let exponent = body.indexOf("e");
|
|
42
|
+
if (exponent < 0) exponent = body.indexOf("E");
|
|
43
|
+
|
|
44
|
+
if (exponent >= 0) {
|
|
45
|
+
mantissa = body.slice(0, exponent);
|
|
46
|
+
const power = body.slice(exponent + 1);
|
|
47
|
+
if (!isDigits(power.startsWith("-") || power.startsWith("+") ? power.slice(1) : power)) return false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const dot = mantissa.indexOf(".");
|
|
51
|
+
if (dot < 0) return isDigits(mantissa);
|
|
52
|
+
const whole = mantissa.slice(0, dot);
|
|
53
|
+
const fraction = mantissa.slice(dot + 1);
|
|
54
|
+
if (whole.length == 0 && fraction.length == 0) return false;
|
|
55
|
+
return (whole.length == 0 || isDigits(whole)) && (fraction.length == 0 || isDigits(fraction));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function numberValue(text: string) {
|
|
59
|
+
if (NANS.includes(text)) return NaN;
|
|
60
|
+
if (text.startsWith("0x")) return parseInt(text.slice(2), 16);
|
|
61
|
+
if (text.startsWith("0o")) return parseInt(text.slice(2), 8);
|
|
62
|
+
if (INFINITIES.includes(text.startsWith("-") || text.startsWith("+") ? text.slice(1) : text)) return text.startsWith("-") ? -Infinity : Infinity;
|
|
63
|
+
return parseFloat(text);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** A plain value with its type: null, a boolean, a number, or text. */
|
|
67
|
+
export function plainValue(text: string, place: Place) {
|
|
68
|
+
if (NULLS.includes(text)) return makeNode("null", "", text, place);
|
|
69
|
+
if (TRUES.includes(text)) return makeNode("boolean", "", "true", place);
|
|
70
|
+
if (FALSES.includes(text)) return makeNode("boolean", "", "false", place);
|
|
71
|
+
if (isNumber(text)) return numberNode(text, numberValue(text), place);
|
|
72
|
+
return makeNode("string", "", text, place);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Lines of a > block folded: lines of text joined by spaces, a blank line a line end, more indented lines kept as they are. */
|
|
76
|
+
function fold(lines: string[]) {
|
|
77
|
+
let folded = "";
|
|
78
|
+
let first = true;
|
|
79
|
+
let plainBefore = false;
|
|
80
|
+
let blanks = 0;
|
|
81
|
+
for (const line of lines) {
|
|
82
|
+
if (line.length == 0) {
|
|
83
|
+
blanks++;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const indented = isSpace(line.charAt(0));
|
|
88
|
+
if (first) folded += "\n".repeat(blanks);
|
|
89
|
+
else if (plainBefore && !indented) folded += blanks == 0 ? " " : "\n".repeat(blanks);
|
|
90
|
+
else folded += "\n".repeat(blanks + 1);
|
|
91
|
+
folded += line;
|
|
92
|
+
first = false;
|
|
93
|
+
plainBefore = !indented;
|
|
94
|
+
blanks = 0;
|
|
95
|
+
}
|
|
96
|
+
return folded;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Reads a YAML text into the tree; the first thing it cannot read stops it, with the place. */
|
|
100
|
+
export class YamlReader {
|
|
101
|
+
pos = 0;
|
|
102
|
+
/** What stopped the reading; "" while nothing has. */
|
|
103
|
+
error = "";
|
|
104
|
+
errorAt = 0;
|
|
105
|
+
/** Where a quoted text read last ends: just past its closing quote. */
|
|
106
|
+
after = 0;
|
|
107
|
+
/** One level of indentation as the text has it; "" until an indented line is met. */
|
|
108
|
+
indent = "";
|
|
109
|
+
/** Comment and blank lines waiting for the value they are written before. */
|
|
110
|
+
pending: string[] | null = null;
|
|
111
|
+
starts: number[];
|
|
112
|
+
|
|
113
|
+
constructor(public text: string) {
|
|
114
|
+
this.starts = lineStarts(text);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
get failed() {
|
|
118
|
+
return this.error.length > 0;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
fail(at: number, message: string) {
|
|
122
|
+
if (this.failed) return;
|
|
123
|
+
this.error = message;
|
|
124
|
+
this.errorAt = at;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
placeAt(at: number) {
|
|
128
|
+
return placeOf(this.starts, at);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** The column of a position, from 0: how far in it is. */
|
|
132
|
+
columnOf(at: number) {
|
|
133
|
+
return this.placeAt(at).column - 1;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
lineEnd(at: number) {
|
|
137
|
+
const end = this.text.indexOf("\n", at);
|
|
138
|
+
return end < 0 ? this.text.length : end;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
nextLine(at: number) {
|
|
142
|
+
const end = this.lineEnd(at);
|
|
143
|
+
return end < this.text.length ? end + 1 : this.text.length;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** The spaces a line starts with. */
|
|
147
|
+
indentAt(start: number) {
|
|
148
|
+
let at = start;
|
|
149
|
+
while (this.text.charAt(at) == " ") at++;
|
|
150
|
+
return at - start;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** The comment and blank lines kept so far, handed to whoever takes them. */
|
|
154
|
+
takeComments() {
|
|
155
|
+
const taken = this.pending;
|
|
156
|
+
this.pending = null;
|
|
157
|
+
return taken;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
keep(line: string) {
|
|
161
|
+
let list = this.pending;
|
|
162
|
+
|
|
163
|
+
if (list == null) {
|
|
164
|
+
list = [];
|
|
165
|
+
this.pending = list;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
list.push(line);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Moves to the next line with a value on it, keeping the comment and blank
|
|
173
|
+
* lines on the way. False at the end of the text, and at a "---" or "..."
|
|
174
|
+
* line, where the document ends.
|
|
175
|
+
*/
|
|
176
|
+
skipToContent() {
|
|
177
|
+
while (this.pos < this.text.length) {
|
|
178
|
+
const start = this.pos;
|
|
179
|
+
const line = this.text.slice(start, this.lineEnd(start));
|
|
180
|
+
const content = line.trim();
|
|
181
|
+
|
|
182
|
+
if (content.length == 0 || content.startsWith("#")) {
|
|
183
|
+
this.keep(content);
|
|
184
|
+
this.pos = this.nextLine(start);
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (line == "---" || line.startsWith("--- ") || line == "..." || line.startsWith("... ")) return false;
|
|
189
|
+
const indent = this.indentAt(start);
|
|
190
|
+
|
|
191
|
+
if (this.text.charAt(start + indent) == "\t") {
|
|
192
|
+
this.fail(start + indent, "a tab indents this line - YAML indents with spaces");
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (indent > 0 && this.indent.length == 0) this.indent = " ".repeat(indent);
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** The document: its value - an empty object for none - after an optional "---". */
|
|
203
|
+
read() {
|
|
204
|
+
let root = makeNode("object", "", "", { line: 1, column: 1 });
|
|
205
|
+
let marked = false;
|
|
206
|
+
let found = false;
|
|
207
|
+
let ended = false;
|
|
208
|
+
|
|
209
|
+
for (;;) {
|
|
210
|
+
const content = this.skipToContent();
|
|
211
|
+
if (this.failed || this.pos >= this.text.length) break;
|
|
212
|
+
const start = this.pos;
|
|
213
|
+
const line = this.text.slice(start, this.lineEnd(start));
|
|
214
|
+
|
|
215
|
+
if (!content && line.startsWith("---") && (marked || found)) {
|
|
216
|
+
this.fail(start, "a second document (---) - a config file holds one");
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (!content && line.startsWith("---")) {
|
|
221
|
+
marked = true;
|
|
222
|
+
if (!line.slice(3).trim().startsWith("#") && line.slice(3).trim().length > 0) this.fail(start + 4, "a value on the --- line - write it on the next line");
|
|
223
|
+
this.pos = this.nextLine(start);
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (!content) {
|
|
228
|
+
ended = true;
|
|
229
|
+
this.pos = this.nextLine(start);
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const at = start + this.indentAt(start);
|
|
234
|
+
|
|
235
|
+
if (this.text.charAt(at) == "%") {
|
|
236
|
+
this.fail(at, "directives (%YAML, %TAG) are not supported - remove the line");
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (found || ended) {
|
|
241
|
+
this.fail(at, "this line does not belong to the value above it - check its indentation");
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
root = this.valueAt(at, -1);
|
|
246
|
+
found = true;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return root;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** A value that starts at `at`: a list, a mapping, or a value on this line. */
|
|
253
|
+
valueAt(at: number, parent: number) {
|
|
254
|
+
if (this.text.charAt(at) == "-" && isBlank(this.text.charAt(at + 1))) return this.sequence(at, false);
|
|
255
|
+
if (this.keyEnd(at) >= 0) return this.mapping(at);
|
|
256
|
+
return this.inline(at, parent, false);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** The ":" that ends a key starting at `at`, on the same line; -1 when it is not a key. */
|
|
260
|
+
keyEnd(at: number) {
|
|
261
|
+
const first = this.text.charAt(at);
|
|
262
|
+
const end = this.lineEnd(at);
|
|
263
|
+
|
|
264
|
+
if (first == "\"" || first == "'") {
|
|
265
|
+
const close = this.quoteEnd(at);
|
|
266
|
+
if (close < 0) return -1;
|
|
267
|
+
let colon = close + 1;
|
|
268
|
+
while (isSpace(this.text.charAt(colon))) colon++;
|
|
269
|
+
return this.text.charAt(colon) == ":" && isBlank(this.text.charAt(colon + 1)) ? colon : -1;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (first == "[" || first == "{" || first == "#") return -1;
|
|
273
|
+
for (let i = at; i < end; i++) {
|
|
274
|
+
const character = this.text.charAt(i);
|
|
275
|
+
if (character == "#" && isSpace(this.text.charAt(i - 1))) return -1;
|
|
276
|
+
if (character == ":" && isBlank(this.text.charAt(i + 1))) return i;
|
|
277
|
+
}
|
|
278
|
+
return -1;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** Where a quoted text that starts at `at` closes on its line; -1 when it goes on to the next. */
|
|
282
|
+
quoteEnd(at: number) {
|
|
283
|
+
const quote = this.text.charAt(at);
|
|
284
|
+
const end = this.lineEnd(at);
|
|
285
|
+
let i = at + 1;
|
|
286
|
+
while (i < end) {
|
|
287
|
+
const character = this.text.charAt(i);
|
|
288
|
+
|
|
289
|
+
if (character == BACKSLASH && quote == "\"") {
|
|
290
|
+
i += 2;
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (character == quote && quote == "'" && this.text.charAt(i + 1) == "'") {
|
|
295
|
+
i += 2;
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (character == quote) return i;
|
|
300
|
+
i++;
|
|
301
|
+
}
|
|
302
|
+
return -1;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** A key's text: quoted, or plain up to its ":". */
|
|
306
|
+
keyText(at: number, colon: number) {
|
|
307
|
+
const first = this.text.charAt(at);
|
|
308
|
+
if (first == "\"" || first == "'") return this.quoted(at).text;
|
|
309
|
+
if (first == "?" && isBlank(this.text.charAt(at + 1))) this.fail(at, "complex keys (?) are not supported");
|
|
310
|
+
else if ("&*!|>%@`".includes(first)) this.fail(at, `a key cannot start with ${first} - quote it`);
|
|
311
|
+
return this.text.slice(at, colon).trim();
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
mapping(at: number) {
|
|
315
|
+
const node = makeNode("object", "", "", this.placeAt(at));
|
|
316
|
+
const indent = this.columnOf(at);
|
|
317
|
+
let start = at;
|
|
318
|
+
|
|
319
|
+
while (!this.failed) {
|
|
320
|
+
const comments = this.takeComments();
|
|
321
|
+
const colon = this.keyEnd(start);
|
|
322
|
+
|
|
323
|
+
if (colon < 0) {
|
|
324
|
+
this.fail(start, "expected a key: \"name: value\"");
|
|
325
|
+
return node;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const key = this.keyText(start, colon);
|
|
329
|
+
if (this.failed) return node;
|
|
330
|
+
const twice = node.items.find(each => each.key == key);
|
|
331
|
+
|
|
332
|
+
if (twice != null) {
|
|
333
|
+
this.fail(start, `"${key}" is here twice - first on line ${twice.line}`);
|
|
334
|
+
return node;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const value = this.memberValue(colon + 1, indent);
|
|
338
|
+
if (this.failed) return node;
|
|
339
|
+
const place = this.placeAt(start);
|
|
340
|
+
value.key = key;
|
|
341
|
+
value.line = place.line;
|
|
342
|
+
value.column = place.column;
|
|
343
|
+
value.comments = comments;
|
|
344
|
+
node.items.push(value);
|
|
345
|
+
|
|
346
|
+
if (!this.skipToContent()) return node;
|
|
347
|
+
const lineIndent = this.indentAt(this.pos);
|
|
348
|
+
if (lineIndent < indent) return node;
|
|
349
|
+
start = this.pos + lineIndent;
|
|
350
|
+
|
|
351
|
+
if (lineIndent > indent) {
|
|
352
|
+
this.fail(start, "indented deeper than the key above - a value that goes on for several lines is quoted, or written after | or >");
|
|
353
|
+
return node;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (this.text.charAt(start) == "-" && isBlank(this.text.charAt(start + 1))) {
|
|
357
|
+
this.fail(start, "a list item among keys - indent the list under its key");
|
|
358
|
+
return node;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return node;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/** A key's value: on its line, or on the lines under it. */
|
|
365
|
+
memberValue(after: number, indent: number) {
|
|
366
|
+
let at = after;
|
|
367
|
+
while (isSpace(this.text.charAt(at))) at++;
|
|
368
|
+
const first = this.text.charAt(at);
|
|
369
|
+
if (at < this.lineEnd(at) && first != "#") return this.inline(at, indent, true);
|
|
370
|
+
this.pos = this.nextLine(at);
|
|
371
|
+
return this.below(indent, true, this.placeAt(at));
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* The value on the lines under a key or a "-": more indented than it, or -
|
|
376
|
+
* under a key - a list at the key's own indentation. Null when there is none.
|
|
377
|
+
*/
|
|
378
|
+
below(indent: number, underKey: boolean, place: Place) {
|
|
379
|
+
if (!this.skipToContent()) return makeNode("null", "", "", place);
|
|
380
|
+
const lineIndent = this.indentAt(this.pos);
|
|
381
|
+
const at = this.pos + lineIndent;
|
|
382
|
+
if (lineIndent > indent) return this.valueAt(at, indent);
|
|
383
|
+
const listHere = this.text.charAt(at) == "-" && isBlank(this.text.charAt(at + 1));
|
|
384
|
+
if (underKey && lineIndent == indent && listHere) return this.sequence(at, true);
|
|
385
|
+
return makeNode("null", "", "", place);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/** A list of "- " items; `atKey` when it is written at its key's own indentation, where the next key is the key's neighbour. */
|
|
389
|
+
sequence(at: number, atKey: boolean) {
|
|
390
|
+
const node = makeNode("array", "", "", this.placeAt(at));
|
|
391
|
+
const indent = this.columnOf(at);
|
|
392
|
+
let start = at;
|
|
393
|
+
|
|
394
|
+
while (!this.failed) {
|
|
395
|
+
const comments = this.takeComments();
|
|
396
|
+
let content = start + 1;
|
|
397
|
+
while (isSpace(this.text.charAt(content))) content++;
|
|
398
|
+
const first = this.text.charAt(content);
|
|
399
|
+
let item = makeNode("null", "", "", this.placeAt(start));
|
|
400
|
+
|
|
401
|
+
if (content < this.lineEnd(start) && first != "#") {
|
|
402
|
+
item = this.valueAt(content, indent);
|
|
403
|
+
} else {
|
|
404
|
+
this.pos = this.nextLine(start);
|
|
405
|
+
item = this.below(indent, false, this.placeAt(start));
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (this.failed) return node;
|
|
409
|
+
item.comments = comments;
|
|
410
|
+
node.items.push(item);
|
|
411
|
+
|
|
412
|
+
if (!this.skipToContent()) return node;
|
|
413
|
+
const lineIndent = this.indentAt(this.pos);
|
|
414
|
+
if (lineIndent < indent) return node;
|
|
415
|
+
start = this.pos + lineIndent;
|
|
416
|
+
const another = this.text.charAt(start) == "-" && isBlank(this.text.charAt(start + 1));
|
|
417
|
+
|
|
418
|
+
if (lineIndent > indent) {
|
|
419
|
+
this.fail(start, "indented deeper than the item above - a value that goes on for several lines is quoted, or written after | or >");
|
|
420
|
+
return node;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (!another && !atKey) this.fail(start, "a key among the items of a list - an item starts with \"- \"");
|
|
424
|
+
if (!another) return node;
|
|
425
|
+
}
|
|
426
|
+
return node;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/** A value written on its line: [ ] or { }, quoted or plain text, or a | or > block that starts here. */
|
|
430
|
+
inline(at: number, parent: number, afterKey: boolean) {
|
|
431
|
+
const first = this.text.charAt(at);
|
|
432
|
+
const second = this.text.charAt(at + 1);
|
|
433
|
+
const place = this.placeAt(at);
|
|
434
|
+
if (first == "|" || first == ">") return this.block(at, parent);
|
|
435
|
+
|
|
436
|
+
if (first == "[" || first == "{") {
|
|
437
|
+
this.pos = at;
|
|
438
|
+
const node = this.flowValue();
|
|
439
|
+
this.endLine(this.pos);
|
|
440
|
+
return node;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if (first == "\"" || first == "'") {
|
|
444
|
+
const node = this.quoted(at);
|
|
445
|
+
this.endLine(this.after);
|
|
446
|
+
return node;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
if (first == "-" && isBlank(second)) this.fail(at, "a list cannot start on the line of its key - put it on the next line");
|
|
450
|
+
else if (first == "&" || first == "*") this.fail(at, "anchors and aliases (& and *) are not supported - write the value out");
|
|
451
|
+
else if (first == "!") this.fail(at, "tags (!) are not supported - write the value as it is");
|
|
452
|
+
else if (first == "?" && isBlank(second)) this.fail(at, "complex keys (?) are not supported");
|
|
453
|
+
else if ("%@`,]}".includes(first)) this.fail(at, `a value cannot start with ${first} - quote it: "${first}..."`);
|
|
454
|
+
if (this.failed) return makeNode("null", "", "", place);
|
|
455
|
+
return this.plain(at, afterKey);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/** Plain text to the end of the line or a comment, with its type. */
|
|
459
|
+
plain(at: number, afterKey: boolean) {
|
|
460
|
+
const end = this.lineEnd(at);
|
|
461
|
+
let stop = end;
|
|
462
|
+
for (let i = at; i < end; i++) {
|
|
463
|
+
const character = this.text.charAt(i);
|
|
464
|
+
|
|
465
|
+
if (character == "#" && isSpace(this.text.charAt(i - 1))) {
|
|
466
|
+
stop = i;
|
|
467
|
+
break;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
if (afterKey && character == ":" && isBlank(this.text.charAt(i + 1))) this.fail(i, "\": \" in a value - quote the value");
|
|
471
|
+
}
|
|
472
|
+
this.pos = this.nextLine(at);
|
|
473
|
+
return plainValue(this.text.slice(at, stop).trim(), this.placeAt(at));
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/** After a value on a line only spaces and a comment may follow. */
|
|
477
|
+
endLine(at: number) {
|
|
478
|
+
let i = at;
|
|
479
|
+
while (isSpace(this.text.charAt(i))) i++;
|
|
480
|
+
const next = this.text.charAt(i);
|
|
481
|
+
if (next != "" && next != "\n" && !(next == "#" && i > at)) this.fail(i, "unexpected text after the value");
|
|
482
|
+
this.pos = this.nextLine(i);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/** Text in quotes: '' is a quote in '...', escapes in "...", and a line break is a space. */
|
|
486
|
+
quoted(at: number) {
|
|
487
|
+
const quote = this.text.charAt(at);
|
|
488
|
+
let value = "";
|
|
489
|
+
let i = at + 1;
|
|
490
|
+
|
|
491
|
+
for (;;) {
|
|
492
|
+
if (i >= this.text.length) {
|
|
493
|
+
this.fail(at, "a quoted value that is never closed");
|
|
494
|
+
break;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const character = this.text.charAt(i);
|
|
498
|
+
|
|
499
|
+
if (character == quote && quote == "'" && this.text.charAt(i + 1) == "'") {
|
|
500
|
+
value += "'";
|
|
501
|
+
i += 2;
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
if (character == quote) {
|
|
506
|
+
i++;
|
|
507
|
+
break;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
if (character == BACKSLASH && quote == "\"") {
|
|
511
|
+
value += this.escape(i);
|
|
512
|
+
i = this.after;
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
if (character == "\n") {
|
|
517
|
+
value = value.trimEnd() + this.lineBreak(i);
|
|
518
|
+
i = this.after;
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
value += character;
|
|
523
|
+
i++;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
this.after = i;
|
|
527
|
+
return makeNode("string", "", value, this.placeAt(at));
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/** A line break inside quotes: a space, or a line end for each blank line after it. */
|
|
531
|
+
lineBreak(at: number) {
|
|
532
|
+
let i = at + 1;
|
|
533
|
+
let blanks = 0;
|
|
534
|
+
for (;;) {
|
|
535
|
+
while (isSpace(this.text.charAt(i))) i++;
|
|
536
|
+
if (this.text.charAt(i) != "\n") break;
|
|
537
|
+
blanks++;
|
|
538
|
+
i++;
|
|
539
|
+
}
|
|
540
|
+
this.after = i;
|
|
541
|
+
return blanks > 0 ? "\n".repeat(blanks) : " ";
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/** The escape at `at` in "...": the text it stands for. */
|
|
545
|
+
escape(at: number) {
|
|
546
|
+
const letter = this.text.charAt(at + 1);
|
|
547
|
+
this.after = at + 2;
|
|
548
|
+
|
|
549
|
+
if (letter == "\n") {
|
|
550
|
+
let i = at + 2;
|
|
551
|
+
while (isSpace(this.text.charAt(i))) i++;
|
|
552
|
+
this.after = i;
|
|
553
|
+
return "";
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
const known = ESCAPE_LETTERS.indexOf(letter);
|
|
557
|
+
if (known >= 0 && letter.length == 1) return ESCAPED.charAt(known);
|
|
558
|
+
// A no-break space, and the line and paragraph separators - which the compiler takes for line ends in a string literal.
|
|
559
|
+
if (letter == "_") return String.fromCharCode(0xA0);
|
|
560
|
+
if (letter == "L" || letter == "P") return String.fromCharCode(letter == "L" ? 0x2028 : 0x2029);
|
|
561
|
+
const digits = letter == "x" ? 2 : letter == "u" ? 4 : letter == "U" ? 8 : 0;
|
|
562
|
+
const hex = this.text.slice(at + 2, at + 2 + digits);
|
|
563
|
+
|
|
564
|
+
if (digits == 0 || hex.length < digits || !isHexDigits(hex)) {
|
|
565
|
+
this.fail(at, `an unknown escape ${BACKSLASH}${letter}`);
|
|
566
|
+
return "";
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
this.after = at + 2 + digits;
|
|
570
|
+
return String.fromCodePoint(parseInt(hex, 16));
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/** A | or > block: the lines under it that are more indented than `parent`. */
|
|
574
|
+
block(at: number, parent: number) {
|
|
575
|
+
const place = this.placeAt(at);
|
|
576
|
+
const literal = this.text.charAt(at) == "|";
|
|
577
|
+
let chomp = "clip";
|
|
578
|
+
let explicit = 0;
|
|
579
|
+
let i = at + 1;
|
|
580
|
+
|
|
581
|
+
for (let n = 0; n < 2; n++) {
|
|
582
|
+
const character = this.text.charAt(i);
|
|
583
|
+
if (character == "-") chomp = "strip";
|
|
584
|
+
else if (character == "+") chomp = "keep";
|
|
585
|
+
else if (character.length == 1 && "123456789".includes(character)) explicit = parseInt(character, 10);
|
|
586
|
+
else break;
|
|
587
|
+
i++;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
this.endLine(i);
|
|
591
|
+
const lines: string[] = [];
|
|
592
|
+
let indent = explicit > 0 ? Math.max(parent, 0) + explicit : -1;
|
|
593
|
+
|
|
594
|
+
while (this.pos < this.text.length && !this.failed) {
|
|
595
|
+
const start = this.pos;
|
|
596
|
+
const line = this.text.slice(start, this.lineEnd(start));
|
|
597
|
+
|
|
598
|
+
if (line.trim().length == 0) {
|
|
599
|
+
lines.push("");
|
|
600
|
+
this.pos = this.nextLine(start);
|
|
601
|
+
continue;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
const lineIndent = this.indentAt(start);
|
|
605
|
+
if (indent < 0 && lineIndent > parent) indent = lineIndent;
|
|
606
|
+
if (indent < 0 || lineIndent < indent) break;
|
|
607
|
+
lines.push(line.slice(indent));
|
|
608
|
+
this.pos = this.nextLine(start);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
let trailing = 0;
|
|
612
|
+
|
|
613
|
+
while (lines.length > 0 && lines[lines.length - 1].length == 0) {
|
|
614
|
+
lines.pop();
|
|
615
|
+
trailing++;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
let value = literal ? lines.join("\n") : fold(lines);
|
|
619
|
+
if (lines.length > 0 && chomp == "clip") value += "\n";
|
|
620
|
+
if (chomp == "keep") value += "\n".repeat(lines.length > 0 ? trailing + 1 : trailing);
|
|
621
|
+
return makeNode("string", "", value, place);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/** Spaces, line ends and comments inside [ ] and { }. */
|
|
625
|
+
skipFlowSpace() {
|
|
626
|
+
for (;;) {
|
|
627
|
+
const character = this.text.charAt(this.pos);
|
|
628
|
+
|
|
629
|
+
if (character == " " || character == "\t" || character == "\n") {
|
|
630
|
+
this.pos++;
|
|
631
|
+
continue;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
if (character == "#" && isBlank(this.text.charAt(this.pos - 1))) {
|
|
635
|
+
this.pos = this.lineEnd(this.pos);
|
|
636
|
+
continue;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/** A value inside [ ] or { }, or one of them. */
|
|
644
|
+
flowValue() {
|
|
645
|
+
this.skipFlowSpace();
|
|
646
|
+
const at = this.pos;
|
|
647
|
+
const first = this.text.charAt(at);
|
|
648
|
+
if (first == "[") return this.flowSequence();
|
|
649
|
+
if (first == "{") return this.flowMapping();
|
|
650
|
+
|
|
651
|
+
if (first == "\"" || first == "'") {
|
|
652
|
+
const node = this.quoted(at);
|
|
653
|
+
this.pos = this.after;
|
|
654
|
+
return node;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
if (first == "&" || first == "*") this.fail(at, "anchors and aliases (& and *) are not supported - write the value out");
|
|
658
|
+
else if (first == "!") this.fail(at, "tags (!) are not supported - write the value as it is");
|
|
659
|
+
else if (first == "|" || first == ">") this.fail(at, "| and > blocks cannot be inside [ ] or { }");
|
|
660
|
+
else if ("%@`".includes(first)) this.fail(at, `a value cannot start with ${first} - quote it: "${first}..."`);
|
|
661
|
+
return this.flowPlain(at);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/** Plain text inside [ ] or { }: up to a comma, a bracket, ": " or the end of the line. */
|
|
665
|
+
flowPlain(at: number) {
|
|
666
|
+
let i = at;
|
|
667
|
+
while (i < this.text.length) {
|
|
668
|
+
const character = this.text.charAt(i);
|
|
669
|
+
const next = this.text.charAt(i + 1);
|
|
670
|
+
if (",[]{}\n".includes(character)) break;
|
|
671
|
+
if (character == ":" && (isBlank(next) || ",[]{}".includes(next))) break;
|
|
672
|
+
if (character == "#" && isBlank(this.text.charAt(i - 1))) break;
|
|
673
|
+
i++;
|
|
674
|
+
}
|
|
675
|
+
const text = this.text.slice(at, i).trim();
|
|
676
|
+
if (text.length == 0 && !this.failed) this.fail(at, "expected a value");
|
|
677
|
+
this.pos = i;
|
|
678
|
+
return plainValue(text, this.placeAt(at));
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
flowSequence() {
|
|
682
|
+
const node = makeNode("array", "", "", this.placeAt(this.pos));
|
|
683
|
+
this.pos++;
|
|
684
|
+
|
|
685
|
+
while (!this.failed) {
|
|
686
|
+
this.skipFlowSpace();
|
|
687
|
+
|
|
688
|
+
if (this.text.charAt(this.pos) == "]") {
|
|
689
|
+
this.pos++;
|
|
690
|
+
return node;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
if (this.pos >= this.text.length) this.fail(this.pos, "the list is never closed with ]");
|
|
694
|
+
if (this.failed) return node;
|
|
695
|
+
const item = this.flowValue();
|
|
696
|
+
this.skipFlowSpace();
|
|
697
|
+
if (this.text.charAt(this.pos) == ":") this.fail(this.pos, "a key inside [ ] is not supported - write { key: value }");
|
|
698
|
+
if (this.failed) return node;
|
|
699
|
+
node.items.push(item);
|
|
700
|
+
const next = this.text.charAt(this.pos);
|
|
701
|
+
if (next == "," || next == "]") this.pos += next == "," ? 1 : 0;
|
|
702
|
+
else this.fail(this.pos, "expected , or ] after an item");
|
|
703
|
+
if (this.failed) return node;
|
|
704
|
+
}
|
|
705
|
+
return node;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
flowMapping() {
|
|
709
|
+
const node = makeNode("object", "", "", this.placeAt(this.pos));
|
|
710
|
+
this.pos++;
|
|
711
|
+
|
|
712
|
+
while (!this.failed) {
|
|
713
|
+
this.skipFlowSpace();
|
|
714
|
+
|
|
715
|
+
if (this.text.charAt(this.pos) == "}") {
|
|
716
|
+
this.pos++;
|
|
717
|
+
return node;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
if (this.pos >= this.text.length) this.fail(this.pos, "the mapping is never closed with }");
|
|
721
|
+
else if ("[{".includes(this.text.charAt(this.pos))) this.fail(this.pos, "a key is text, not a list or a mapping");
|
|
722
|
+
if (this.failed) return node;
|
|
723
|
+
const keyAt = this.pos;
|
|
724
|
+
const keyNode = this.flowValue();
|
|
725
|
+
const key = keyNode.text;
|
|
726
|
+
this.skipFlowSpace();
|
|
727
|
+
let value = makeNode("null", "", "", this.placeAt(this.pos));
|
|
728
|
+
|
|
729
|
+
if (this.text.charAt(this.pos) == ":") {
|
|
730
|
+
this.pos++;
|
|
731
|
+
this.skipFlowSpace();
|
|
732
|
+
if (!",}".includes(this.text.charAt(this.pos))) value = this.flowValue();
|
|
733
|
+
this.skipFlowSpace();
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
if (this.failed) return node;
|
|
737
|
+
const twice = node.items.find(each => each.key == key);
|
|
738
|
+
if (twice != null) this.fail(keyAt, `"${key}" is here twice - first on line ${twice.line}`);
|
|
739
|
+
if (this.failed) return node;
|
|
740
|
+
value.key = key;
|
|
741
|
+
value.line = keyNode.line;
|
|
742
|
+
value.column = keyNode.column;
|
|
743
|
+
node.items.push(value);
|
|
744
|
+
const next = this.text.charAt(this.pos);
|
|
745
|
+
if (next == "," || next == "}") this.pos += next == "," ? 1 : 0;
|
|
746
|
+
else this.fail(this.pos, "expected , or } after a value");
|
|
747
|
+
if (this.failed) return node;
|
|
748
|
+
}
|
|
749
|
+
return node;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
/** Text written plain when YAML reads it back as the same text; else in double quotes. */
|
|
754
|
+
export function yamlText(text: string) {
|
|
755
|
+
return plainSafe(text) ? text : jsonText(text);
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
function plainSafe(text: string) {
|
|
759
|
+
if (text.length == 0 || text.trim() != text) return false;
|
|
760
|
+
if (INDICATORS.includes(text.charAt(0))) return false;
|
|
761
|
+
if (text.includes(": ") || text.includes(" #") || text.endsWith(":")) return false;
|
|
762
|
+
if (text.split("").some(isControl)) return false;
|
|
763
|
+
// YAML 1.1 reads these as booleans: other tools may still.
|
|
764
|
+
if (["yes", "no", "on", "off", "y", "n"].includes(text.toLowerCase())) return false;
|
|
765
|
+
return plainValue(text, { line: 0, column: 0 }).kind == "string";
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
/** A scalar as YAML writes it. */
|
|
769
|
+
function scalar(node: TreeNode) {
|
|
770
|
+
if (node.kind == "string") return yamlText(node.text);
|
|
771
|
+
if (node.kind == "number") return node.text.length > 0 ? node.text : yamlNumber(node.value);
|
|
772
|
+
return node.text;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/** A number made at run time: .nan and .inf are YAML's words for what JavaScript prints NaN and Infinity. */
|
|
776
|
+
function yamlNumber(value: number) {
|
|
777
|
+
if (isNaN(value)) return ".nan";
|
|
778
|
+
if (value == Infinity) return ".inf";
|
|
779
|
+
if (value == -Infinity) return "-.inf";
|
|
780
|
+
return `${value}`;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
function isEmpty(node: TreeNode) {
|
|
784
|
+
return (node.kind == "object" || node.kind == "array") && node.items.length == 0;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
/** What follows a key or a "-" on its line: a scalar, {} or [], or nothing for a value on the lines under it. */
|
|
788
|
+
function inlineValue(node: TreeNode) {
|
|
789
|
+
if (isScalar(node)) return ` ${scalar(node)}`;
|
|
790
|
+
if (node.kind == "null") return node.text.length > 0 ? ` ${node.text}` : "";
|
|
791
|
+
if (isEmpty(node)) return node.kind == "object" ? " {}" : " []";
|
|
792
|
+
return "";
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
function writeMembers(lines: string[], object: TreeNode, pad: string, unit: string) {
|
|
796
|
+
for (const each of object.items) {
|
|
797
|
+
writeComments(lines, each.comments, pad);
|
|
798
|
+
lines.push(`${pad}${yamlText(each.key)}:${inlineValue(each)}`);
|
|
799
|
+
if (inlineValue(each).length == 0 && each.kind != "null") writeBlock(lines, each, pad + unit, unit);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
function writeItems(lines: string[], array: TreeNode, pad: string, unit: string) {
|
|
804
|
+
for (const item of array.items) {
|
|
805
|
+
writeComments(lines, item.comments, pad);
|
|
806
|
+
const inline = inlineValue(item);
|
|
807
|
+
|
|
808
|
+
if (inline.length > 0 || item.kind == "null") {
|
|
809
|
+
lines.push(`${pad}-${inline}`);
|
|
810
|
+
continue;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// A mapping starts on the "- " line: its first key there, the rest under it.
|
|
814
|
+
const nested: string[] = [];
|
|
815
|
+
writeBlock(nested, item, `${pad} `, unit);
|
|
816
|
+
const first = nested.findIndex(line => line.startsWith(`${pad} `) && !line.trim().startsWith("#"));
|
|
817
|
+
for (let i = 0; i < nested.length; i++) {
|
|
818
|
+
if (i < first) lines.push(nested[i].length > 0 ? `${pad}${nested[i].trim()}` : "");
|
|
819
|
+
else if (i == first) lines.push(`${pad}- ${nested[i].slice(pad.length + 2)}`);
|
|
820
|
+
else lines.push(nested[i]);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
function writeBlock(lines: string[], node: TreeNode, pad: string, unit: string) {
|
|
826
|
+
if (node.kind == "object") writeMembers(lines, node, pad, unit);
|
|
827
|
+
else if (node.kind == "array") writeItems(lines, node, pad, unit);
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
/** The document as YAML lines. */
|
|
831
|
+
export function writeYaml(document: TreeDocument) {
|
|
832
|
+
const lines: string[] = [];
|
|
833
|
+
const root = document.root;
|
|
834
|
+
const unit = document.indent.length > 0 ? document.indent : " ";
|
|
835
|
+
writeComments(lines, root.comments, "");
|
|
836
|
+
if (root.kind == "object" || root.kind == "array") writeBlock(lines, root, "", unit);
|
|
837
|
+
else lines.push(scalar(root));
|
|
838
|
+
writeComments(lines, document.tail, "");
|
|
839
|
+
return lines;
|
|
840
|
+
}
|