@crewhaus/tool-data 0.7.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/README.md +193 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +988 -0
- package/dist/lib/csv.d.ts +80 -0
- package/dist/lib/csv.js +240 -0
- package/dist/lib/diff.d.ts +44 -0
- package/dist/lib/diff.js +166 -0
- package/dist/lib/json.d.ts +65 -0
- package/dist/lib/json.js +133 -0
- package/dist/lib/jsonl.d.ts +39 -0
- package/dist/lib/jsonl.js +63 -0
- package/dist/lib/jsonpath.d.ts +91 -0
- package/dist/lib/jsonpath.js +430 -0
- package/dist/lib/patch.d.ts +72 -0
- package/dist/lib/patch.js +272 -0
- package/dist/lib/table.d.ts +156 -0
- package/dist/lib/table.js +571 -0
- package/dist/lib/toml.d.ts +59 -0
- package/dist/lib/toml.js +530 -0
- package/dist/lib/xml.d.ts +72 -0
- package/dist/lib/xml.js +344 -0
- package/dist/lib/yaml.d.ts +61 -0
- package/dist/lib/yaml.js +598 -0
- package/package.json +45 -0
package/dist/lib/toml.js
ADDED
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A hand-written TOML reader and writer. As with the YAML module, there is
|
|
3
|
+
* no dependency behind it, so the supported subset below is the contract.
|
|
4
|
+
*
|
|
5
|
+
* ## Read: supported
|
|
6
|
+
*
|
|
7
|
+
* - `key = value` pairs, bare, quoted and dotted keys (`a.b.c = 1`).
|
|
8
|
+
* - Tables `[a.b]` and arrays of tables `[[a.b]]`, at any depth.
|
|
9
|
+
* - Basic strings `"..."` with the standard escapes including `\uXXXX` and
|
|
10
|
+
* `\UXXXXXXXX`, literal strings `'...'`, and the multi-line `"""..."""`
|
|
11
|
+
* and `'''...'''` forms, with the leading-newline trim and the backslash
|
|
12
|
+
* line continuation.
|
|
13
|
+
* - Integers, including `_` separators and `0x` / `0o` / `0b` literals.
|
|
14
|
+
* - Floats, including exponents, `inf` and `nan`.
|
|
15
|
+
* - Booleans, arrays (nested, multi-line, with trailing commas) and inline
|
|
16
|
+
* tables `{ a = 1, b = "x" }`.
|
|
17
|
+
* - `#` comments.
|
|
18
|
+
*
|
|
19
|
+
* ## Read: deliberate departures, each documented rather than silent
|
|
20
|
+
*
|
|
21
|
+
* - **Dates and times stay strings.** TOML has first-class offset date-time,
|
|
22
|
+
* local date-time, date and time types. JSON has none of them, so
|
|
23
|
+
* converting to a `Date` would make a round trip lossy. `2026-01-01` reads
|
|
24
|
+
* back as the string `"2026-01-01"`.
|
|
25
|
+
* - Redefining a table or key is an error, as the spec requires — including
|
|
26
|
+
* both halves of the rule: a `[header]` may not reopen a table a dotted
|
|
27
|
+
* key created, and a dotted key may not reach into a table a `[header]`
|
|
28
|
+
* already defined.
|
|
29
|
+
* - `inf` and `nan` read as the JavaScript infinities and NaN and write back
|
|
30
|
+
* out as `inf` / `nan`, but JSON cannot spell them, so converting to JSON
|
|
31
|
+
* renders them `null`.
|
|
32
|
+
* - Integers outside the JavaScript safe range are kept as strings rather
|
|
33
|
+
* than silently rounded.
|
|
34
|
+
*
|
|
35
|
+
* ## Write
|
|
36
|
+
*
|
|
37
|
+
* Scalars and scalar arrays first, then sub-tables, then arrays of tables —
|
|
38
|
+
* which is the order that reads back identically. A value TOML cannot hold
|
|
39
|
+
* (`null`) is skipped, and the writer reports which keys it dropped rather
|
|
40
|
+
* than pretending the output is complete.
|
|
41
|
+
*/
|
|
42
|
+
import { isPlainObject } from "./json";
|
|
43
|
+
export class TomlError extends Error {
|
|
44
|
+
line;
|
|
45
|
+
constructor(message, line) {
|
|
46
|
+
super(message);
|
|
47
|
+
this.line = line;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/** Parse a TOML document into JSON values. Throws `TomlError` with a line number. */
|
|
51
|
+
export function parseToml(text) {
|
|
52
|
+
const root = {};
|
|
53
|
+
const cur = { text: text.replace(/\r\n?/g, "\n"), i: 0, line: 1 };
|
|
54
|
+
// Tables created by a `[header]`, and tables brought into being implicitly
|
|
55
|
+
// by a dotted key (`a.b = 1` creates `a`). The spec forbids each from
|
|
56
|
+
// redefining the other, so both have to be remembered.
|
|
57
|
+
const defined = new Set();
|
|
58
|
+
const dotted = new Set();
|
|
59
|
+
let table = root;
|
|
60
|
+
let tablePath = [];
|
|
61
|
+
for (;;) {
|
|
62
|
+
skipWhitespaceAndComments(cur);
|
|
63
|
+
if (cur.i >= cur.text.length)
|
|
64
|
+
break;
|
|
65
|
+
if (cur.text[cur.i] === "[") {
|
|
66
|
+
const isArray = cur.text[cur.i + 1] === "[";
|
|
67
|
+
cur.i += isArray ? 2 : 1;
|
|
68
|
+
const path = readKeyPath(cur, isArray ? "]]" : "]");
|
|
69
|
+
const key = path.join(".");
|
|
70
|
+
if (isArray) {
|
|
71
|
+
table = pushArrayTable(root, path, cur);
|
|
72
|
+
tablePath = path;
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
if (defined.has(key)) {
|
|
76
|
+
throw new TomlError(`table [${key}] is defined twice`, cur.line);
|
|
77
|
+
}
|
|
78
|
+
if (dotted.has(key)) {
|
|
79
|
+
throw new TomlError(`table [${key}] was already created by a dotted key, so it cannot be defined again`, cur.line);
|
|
80
|
+
}
|
|
81
|
+
defined.add(key);
|
|
82
|
+
table = descend(root, path, cur);
|
|
83
|
+
tablePath = path;
|
|
84
|
+
}
|
|
85
|
+
expectLineEnd(cur);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
const path = readKeyPath(cur, "=");
|
|
89
|
+
const value = readValue(cur);
|
|
90
|
+
assign(table, path, value, cur, tablePath, { defined, dotted });
|
|
91
|
+
expectLineEnd(cur);
|
|
92
|
+
}
|
|
93
|
+
return root;
|
|
94
|
+
}
|
|
95
|
+
function skipWhitespaceAndComments(c) {
|
|
96
|
+
for (;;) {
|
|
97
|
+
const ch = c.text[c.i];
|
|
98
|
+
if (ch === " " || ch === "\t") {
|
|
99
|
+
c.i += 1;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (ch === "\n") {
|
|
103
|
+
c.i += 1;
|
|
104
|
+
c.line += 1;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (ch === "#") {
|
|
108
|
+
while (c.i < c.text.length && c.text[c.i] !== "\n")
|
|
109
|
+
c.i += 1;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function skipInlineSpace(c) {
|
|
116
|
+
while (c.text[c.i] === " " || c.text[c.i] === "\t")
|
|
117
|
+
c.i += 1;
|
|
118
|
+
}
|
|
119
|
+
function expectLineEnd(c) {
|
|
120
|
+
skipInlineSpace(c);
|
|
121
|
+
if (c.text[c.i] === "#") {
|
|
122
|
+
while (c.i < c.text.length && c.text[c.i] !== "\n")
|
|
123
|
+
c.i += 1;
|
|
124
|
+
}
|
|
125
|
+
const ch = c.text[c.i];
|
|
126
|
+
if (ch === undefined)
|
|
127
|
+
return;
|
|
128
|
+
if (ch !== "\n") {
|
|
129
|
+
throw new TomlError(`unexpected ${JSON.stringify(ch)} after a value`, c.line);
|
|
130
|
+
}
|
|
131
|
+
c.i += 1;
|
|
132
|
+
c.line += 1;
|
|
133
|
+
}
|
|
134
|
+
const BARE_KEY = /[A-Za-z0-9_-]/;
|
|
135
|
+
function readKeyPath(c, terminator) {
|
|
136
|
+
const path = [];
|
|
137
|
+
for (;;) {
|
|
138
|
+
skipInlineSpace(c);
|
|
139
|
+
const ch = c.text[c.i];
|
|
140
|
+
if (ch === '"' || ch === "'") {
|
|
141
|
+
path.push(readString(c));
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
const start = c.i;
|
|
145
|
+
while (c.i < c.text.length && BARE_KEY.test(c.text[c.i]))
|
|
146
|
+
c.i += 1;
|
|
147
|
+
if (c.i === start)
|
|
148
|
+
throw new TomlError("expected a key", c.line);
|
|
149
|
+
path.push(c.text.slice(start, c.i));
|
|
150
|
+
}
|
|
151
|
+
skipInlineSpace(c);
|
|
152
|
+
if (c.text[c.i] === ".") {
|
|
153
|
+
c.i += 1;
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (c.text.startsWith(terminator, c.i)) {
|
|
157
|
+
c.i += terminator.length;
|
|
158
|
+
return path;
|
|
159
|
+
}
|
|
160
|
+
throw new TomlError(`expected ${JSON.stringify(terminator)} after the key, got ${JSON.stringify(c.text[c.i] ?? "end of file")}`, c.line);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function descend(root, path, c) {
|
|
164
|
+
let node = root;
|
|
165
|
+
path.forEach((seg, idx) => {
|
|
166
|
+
const existing = node[seg];
|
|
167
|
+
if (existing === undefined) {
|
|
168
|
+
const fresh = {};
|
|
169
|
+
node[seg] = fresh;
|
|
170
|
+
node = fresh;
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (Array.isArray(existing)) {
|
|
174
|
+
const last = existing[existing.length - 1];
|
|
175
|
+
if (!isPlainObject(last)) {
|
|
176
|
+
throw new TomlError(`cannot descend into "${path.slice(0, idx + 1).join(".")}"`, c.line);
|
|
177
|
+
}
|
|
178
|
+
node = last;
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (!isPlainObject(existing)) {
|
|
182
|
+
throw new TomlError(`"${path.slice(0, idx + 1).join(".")}" is already a value, not a table`, c.line);
|
|
183
|
+
}
|
|
184
|
+
node = existing;
|
|
185
|
+
});
|
|
186
|
+
return node;
|
|
187
|
+
}
|
|
188
|
+
function pushArrayTable(root, path, c) {
|
|
189
|
+
const parent = descend(root, path.slice(0, -1), c);
|
|
190
|
+
const key = path[path.length - 1];
|
|
191
|
+
const existing = parent[key];
|
|
192
|
+
const fresh = {};
|
|
193
|
+
if (existing === undefined) {
|
|
194
|
+
parent[key] = [fresh];
|
|
195
|
+
return fresh;
|
|
196
|
+
}
|
|
197
|
+
if (!Array.isArray(existing)) {
|
|
198
|
+
throw new TomlError(`"${path.join(".")}" is already a table, not an array of tables`, c.line);
|
|
199
|
+
}
|
|
200
|
+
existing.push(fresh);
|
|
201
|
+
return fresh;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Assign a (possibly dotted) key inside the current table.
|
|
205
|
+
*
|
|
206
|
+
* `tables` is passed for a document-level assignment and omitted inside an
|
|
207
|
+
* inline table, whose keys are scoped to that table and cannot redefine
|
|
208
|
+
* anything outside it. When it is passed, every table the dotted key brings
|
|
209
|
+
* into being is recorded, and a dotted key that reaches into a table already
|
|
210
|
+
* written as a `[header]` is refused — the two halves of the spec's "a table
|
|
211
|
+
* may not be defined twice".
|
|
212
|
+
*/
|
|
213
|
+
function assign(table, path, value, c, tablePath, tables) {
|
|
214
|
+
let node = table;
|
|
215
|
+
path.slice(0, -1).forEach((seg, idx) => {
|
|
216
|
+
const absolute = [...tablePath, ...path.slice(0, idx + 1)].join(".");
|
|
217
|
+
if (tables !== undefined) {
|
|
218
|
+
if (tables.defined.has(absolute)) {
|
|
219
|
+
throw new TomlError(`"${absolute}" is already a table defined by [${absolute}], so a dotted key cannot extend it`, c.line);
|
|
220
|
+
}
|
|
221
|
+
tables.dotted.add(absolute);
|
|
222
|
+
}
|
|
223
|
+
const existing = node[seg];
|
|
224
|
+
if (existing === undefined) {
|
|
225
|
+
const fresh = {};
|
|
226
|
+
node[seg] = fresh;
|
|
227
|
+
node = fresh;
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
if (!isPlainObject(existing)) {
|
|
231
|
+
throw new TomlError(`"${[...tablePath, ...path].join(".")}" collides with a value`, c.line);
|
|
232
|
+
}
|
|
233
|
+
node = existing;
|
|
234
|
+
});
|
|
235
|
+
const key = path[path.length - 1];
|
|
236
|
+
if (Object.hasOwn(node, key)) {
|
|
237
|
+
throw new TomlError(`key "${[...tablePath, ...path].join(".")}" is defined twice`, c.line);
|
|
238
|
+
}
|
|
239
|
+
node[key] = value;
|
|
240
|
+
}
|
|
241
|
+
function readString(c) {
|
|
242
|
+
const q = c.text[c.i];
|
|
243
|
+
if (q === '"' && c.text.startsWith('"""', c.i))
|
|
244
|
+
return readMultiline(c, '"""', true);
|
|
245
|
+
if (q === "'" && c.text.startsWith("'''", c.i))
|
|
246
|
+
return readMultiline(c, "'''", false);
|
|
247
|
+
if (q === "'") {
|
|
248
|
+
c.i += 1;
|
|
249
|
+
const start = c.i;
|
|
250
|
+
while (c.i < c.text.length && c.text[c.i] !== "'") {
|
|
251
|
+
if (c.text[c.i] === "\n")
|
|
252
|
+
throw new TomlError("unterminated literal string", c.line);
|
|
253
|
+
c.i += 1;
|
|
254
|
+
}
|
|
255
|
+
if (c.i >= c.text.length)
|
|
256
|
+
throw new TomlError("unterminated literal string", c.line);
|
|
257
|
+
const out = c.text.slice(start, c.i);
|
|
258
|
+
c.i += 1;
|
|
259
|
+
return out;
|
|
260
|
+
}
|
|
261
|
+
if (q !== '"')
|
|
262
|
+
throw new TomlError("expected a string", c.line);
|
|
263
|
+
c.i += 1;
|
|
264
|
+
let out = "";
|
|
265
|
+
for (;;) {
|
|
266
|
+
const ch = c.text[c.i];
|
|
267
|
+
if (ch === undefined || ch === "\n")
|
|
268
|
+
throw new TomlError("unterminated string", c.line);
|
|
269
|
+
if (ch === '"') {
|
|
270
|
+
c.i += 1;
|
|
271
|
+
return out;
|
|
272
|
+
}
|
|
273
|
+
if (ch === "\\") {
|
|
274
|
+
out += readEscape(c);
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
out += ch;
|
|
278
|
+
c.i += 1;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
function readMultiline(c, delim, basic) {
|
|
282
|
+
c.i += 3;
|
|
283
|
+
if (c.text[c.i] === "\n") {
|
|
284
|
+
c.i += 1;
|
|
285
|
+
c.line += 1;
|
|
286
|
+
}
|
|
287
|
+
let out = "";
|
|
288
|
+
for (;;) {
|
|
289
|
+
if (c.i >= c.text.length)
|
|
290
|
+
throw new TomlError("unterminated multi-line string", c.line);
|
|
291
|
+
if (c.text.startsWith(delim, c.i)) {
|
|
292
|
+
c.i += 3;
|
|
293
|
+
return out;
|
|
294
|
+
}
|
|
295
|
+
const ch = c.text[c.i];
|
|
296
|
+
if (basic && ch === "\\") {
|
|
297
|
+
if (/^\\[ \t]*\n/.test(c.text.slice(c.i))) {
|
|
298
|
+
// Line continuation: swallow the newline and the following whitespace.
|
|
299
|
+
c.i += 1;
|
|
300
|
+
while (c.i < c.text.length && /[ \t\n]/.test(c.text[c.i])) {
|
|
301
|
+
if (c.text[c.i] === "\n")
|
|
302
|
+
c.line += 1;
|
|
303
|
+
c.i += 1;
|
|
304
|
+
}
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
out += readEscape(c);
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
if (ch === "\n")
|
|
311
|
+
c.line += 1;
|
|
312
|
+
out += ch;
|
|
313
|
+
c.i += 1;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
function readEscape(c) {
|
|
317
|
+
c.i += 1;
|
|
318
|
+
const ch = c.text[c.i];
|
|
319
|
+
c.i += 1;
|
|
320
|
+
switch (ch) {
|
|
321
|
+
case "n":
|
|
322
|
+
return "\n";
|
|
323
|
+
case "t":
|
|
324
|
+
return "\t";
|
|
325
|
+
case "r":
|
|
326
|
+
return "\r";
|
|
327
|
+
case "b":
|
|
328
|
+
return "\b";
|
|
329
|
+
case "f":
|
|
330
|
+
return "\f";
|
|
331
|
+
case '"':
|
|
332
|
+
return '"';
|
|
333
|
+
case "\\":
|
|
334
|
+
return "\\";
|
|
335
|
+
case "u":
|
|
336
|
+
case "U": {
|
|
337
|
+
const width = ch === "u" ? 4 : 8;
|
|
338
|
+
const hex = c.text.slice(c.i, c.i + width);
|
|
339
|
+
if (!new RegExp(`^[0-9a-fA-F]{${width}}$`).test(hex)) {
|
|
340
|
+
throw new TomlError(`bad escape ${JSON.stringify(hex)}`, c.line);
|
|
341
|
+
}
|
|
342
|
+
c.i += width;
|
|
343
|
+
return String.fromCodePoint(Number.parseInt(hex, 16));
|
|
344
|
+
}
|
|
345
|
+
default:
|
|
346
|
+
throw new TomlError(`unknown escape for ${JSON.stringify(ch ?? "")}`, c.line);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
const DATE_LIKE = /^\d{4}-\d{2}-\d{2}([Tt ]\d{2}:\d{2}:\d{2}(\.\d+)?([Zz]|[+-]\d{2}:\d{2})?)?$|^\d{2}:\d{2}:\d{2}(\.\d+)?$/;
|
|
350
|
+
function readValue(c) {
|
|
351
|
+
skipInlineSpace(c);
|
|
352
|
+
const ch = c.text[c.i];
|
|
353
|
+
if (ch === undefined)
|
|
354
|
+
throw new TomlError("expected a value", c.line);
|
|
355
|
+
if (ch === '"' || ch === "'")
|
|
356
|
+
return readString(c);
|
|
357
|
+
if (ch === "[")
|
|
358
|
+
return readArray(c);
|
|
359
|
+
if (ch === "{")
|
|
360
|
+
return readInlineTable(c);
|
|
361
|
+
const start = c.i;
|
|
362
|
+
while (c.i < c.text.length && !"\n,]}#".includes(c.text[c.i]))
|
|
363
|
+
c.i += 1;
|
|
364
|
+
const raw = c.text.slice(start, c.i).trim();
|
|
365
|
+
if (raw === "")
|
|
366
|
+
throw new TomlError("expected a value", c.line);
|
|
367
|
+
return scalarFromToken(raw, c.line);
|
|
368
|
+
}
|
|
369
|
+
/** Type a bare TOML token. Exported so the tests can pin the exact rules. */
|
|
370
|
+
export function scalarFromToken(raw, line) {
|
|
371
|
+
if (raw === "true")
|
|
372
|
+
return true;
|
|
373
|
+
if (raw === "false")
|
|
374
|
+
return false;
|
|
375
|
+
if (DATE_LIKE.test(raw))
|
|
376
|
+
return raw; // dates stay strings — see the module note
|
|
377
|
+
if (/^[+-]?inf$/.test(raw)) {
|
|
378
|
+
return raw.startsWith("-") ? Number.NEGATIVE_INFINITY : Number.POSITIVE_INFINITY;
|
|
379
|
+
}
|
|
380
|
+
if (/^[+-]?nan$/.test(raw))
|
|
381
|
+
return Number.NaN;
|
|
382
|
+
const cleaned = raw.replace(/_/g, "");
|
|
383
|
+
if (/^[+-]?(0|[1-9][0-9]*)$/.test(cleaned)) {
|
|
384
|
+
const n = Number(cleaned);
|
|
385
|
+
return Number.isSafeInteger(n) ? n : raw;
|
|
386
|
+
}
|
|
387
|
+
if (/^0x[0-9a-fA-F]+$/.test(cleaned))
|
|
388
|
+
return Number.parseInt(cleaned.slice(2), 16);
|
|
389
|
+
if (/^0o[0-7]+$/.test(cleaned))
|
|
390
|
+
return Number.parseInt(cleaned.slice(2), 8);
|
|
391
|
+
if (/^0b[01]+$/.test(cleaned))
|
|
392
|
+
return Number.parseInt(cleaned.slice(2), 2);
|
|
393
|
+
if (/^[+-]?([0-9]+(\.[0-9]+)?)([eE][+-]?[0-9]+)?$/.test(cleaned) && /[.eE]/.test(cleaned)) {
|
|
394
|
+
return Number(cleaned);
|
|
395
|
+
}
|
|
396
|
+
throw new TomlError(`cannot read ${JSON.stringify(raw)} as a value`, line);
|
|
397
|
+
}
|
|
398
|
+
function readArray(c) {
|
|
399
|
+
c.i += 1;
|
|
400
|
+
const out = [];
|
|
401
|
+
for (;;) {
|
|
402
|
+
skipWhitespaceAndComments(c);
|
|
403
|
+
if (c.text[c.i] === "]") {
|
|
404
|
+
c.i += 1;
|
|
405
|
+
return out;
|
|
406
|
+
}
|
|
407
|
+
if (c.i >= c.text.length)
|
|
408
|
+
throw new TomlError("unterminated array", c.line);
|
|
409
|
+
out.push(readValue(c));
|
|
410
|
+
skipWhitespaceAndComments(c);
|
|
411
|
+
if (c.text[c.i] === ",") {
|
|
412
|
+
c.i += 1;
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
if (c.text[c.i] === "]") {
|
|
416
|
+
c.i += 1;
|
|
417
|
+
return out;
|
|
418
|
+
}
|
|
419
|
+
throw new TomlError("expected ',' or ']' in an array", c.line);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
function readInlineTable(c) {
|
|
423
|
+
c.i += 1;
|
|
424
|
+
const out = {};
|
|
425
|
+
skipInlineSpace(c);
|
|
426
|
+
if (c.text[c.i] === "}") {
|
|
427
|
+
c.i += 1;
|
|
428
|
+
return out;
|
|
429
|
+
}
|
|
430
|
+
for (;;) {
|
|
431
|
+
const path = readKeyPath(c, "=");
|
|
432
|
+
const value = readValue(c);
|
|
433
|
+
assign(out, path, value, c, []);
|
|
434
|
+
skipInlineSpace(c);
|
|
435
|
+
if (c.text[c.i] === ",") {
|
|
436
|
+
c.i += 1;
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
if (c.text[c.i] === "}") {
|
|
440
|
+
c.i += 1;
|
|
441
|
+
return out;
|
|
442
|
+
}
|
|
443
|
+
throw new TomlError("expected ',' or '}' in an inline table", c.line);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
// ---------------------------------------------------------------------------
|
|
447
|
+
// Writing
|
|
448
|
+
const BARE_KEY_FULL = /^[A-Za-z0-9_-]+$/;
|
|
449
|
+
function writeKey(key) {
|
|
450
|
+
return BARE_KEY_FULL.test(key) ? key : JSON.stringify(key);
|
|
451
|
+
}
|
|
452
|
+
function writeTomlScalar(value) {
|
|
453
|
+
if (typeof value === "boolean")
|
|
454
|
+
return String(value);
|
|
455
|
+
if (typeof value === "number") {
|
|
456
|
+
if (Number.isNaN(value))
|
|
457
|
+
return "nan";
|
|
458
|
+
if (!Number.isFinite(value))
|
|
459
|
+
return value > 0 ? "inf" : "-inf";
|
|
460
|
+
return String(value);
|
|
461
|
+
}
|
|
462
|
+
if (Array.isArray(value))
|
|
463
|
+
return `[${value.map(writeTomlScalar).join(", ")}]`;
|
|
464
|
+
if (isPlainObject(value)) {
|
|
465
|
+
const inner = Object.keys(value)
|
|
466
|
+
.filter((k) => value[k] !== null && value[k] !== undefined)
|
|
467
|
+
.map((k) => `${writeKey(k)} = ${writeTomlScalar(value[k])}`);
|
|
468
|
+
return `{ ${inner.join(", ")} }`;
|
|
469
|
+
}
|
|
470
|
+
return JSON.stringify(String(value));
|
|
471
|
+
}
|
|
472
|
+
function isTableArray(value) {
|
|
473
|
+
return Array.isArray(value) && value.length > 0 && value.every(isPlainObject);
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Serialize an object as TOML. Returns the text plus the dotted paths of any
|
|
477
|
+
* keys that were dropped because TOML has no representation for them
|
|
478
|
+
* (`null` and `undefined`).
|
|
479
|
+
*/
|
|
480
|
+
export function stringifyToml(value) {
|
|
481
|
+
if (!isPlainObject(value)) {
|
|
482
|
+
throw new TomlError("TOML documents must be objects at the top level", 0);
|
|
483
|
+
}
|
|
484
|
+
const skipped = [];
|
|
485
|
+
const lines = [];
|
|
486
|
+
emitTable(value, [], lines, skipped);
|
|
487
|
+
return { text: lines.join("\n"), skipped };
|
|
488
|
+
}
|
|
489
|
+
function emitTable(table, path, lines, skipped, headerAlreadyWritten = false) {
|
|
490
|
+
const scalars = [];
|
|
491
|
+
const subTables = [];
|
|
492
|
+
const tableArrays = [];
|
|
493
|
+
for (const key of Object.keys(table)) {
|
|
494
|
+
const v = table[key];
|
|
495
|
+
if (v === null || v === undefined) {
|
|
496
|
+
skipped.push([...path, key].join("."));
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
if (isTableArray(v)) {
|
|
500
|
+
tableArrays.push([key, v]);
|
|
501
|
+
continue;
|
|
502
|
+
}
|
|
503
|
+
if (isPlainObject(v)) {
|
|
504
|
+
subTables.push([key, v]);
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
507
|
+
scalars.push(`${writeKey(key)} = ${writeTomlScalar(v)}`);
|
|
508
|
+
}
|
|
509
|
+
const needsHeader = !headerAlreadyWritten &&
|
|
510
|
+
path.length > 0 &&
|
|
511
|
+
(scalars.length > 0 || (subTables.length === 0 && tableArrays.length === 0));
|
|
512
|
+
if (needsHeader) {
|
|
513
|
+
if (lines.length > 0)
|
|
514
|
+
lines.push("");
|
|
515
|
+
lines.push(`[${path.map(writeKey).join(".")}]`);
|
|
516
|
+
}
|
|
517
|
+
lines.push(...scalars);
|
|
518
|
+
for (const [key, sub] of subTables) {
|
|
519
|
+
emitTable(sub, [...path, key], lines, skipped);
|
|
520
|
+
}
|
|
521
|
+
for (const [key, arr] of tableArrays) {
|
|
522
|
+
const childPath = [...path, key];
|
|
523
|
+
for (const element of arr) {
|
|
524
|
+
if (lines.length > 0)
|
|
525
|
+
lines.push("");
|
|
526
|
+
lines.push(`[[${childPath.map(writeKey).join(".")}]]`);
|
|
527
|
+
emitTable(element, childPath, lines, skipped, true);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A hand-written XML / HTML-fragment reader.
|
|
3
|
+
*
|
|
4
|
+
* ## What it is
|
|
5
|
+
*
|
|
6
|
+
* A well-formedness-checking parser for the XML that shows up in API
|
|
7
|
+
* responses, RSS feeds, SVG, and configuration files. It is not a conformant
|
|
8
|
+
* XML processor and does not claim to be one.
|
|
9
|
+
*
|
|
10
|
+
* ## Supported
|
|
11
|
+
*
|
|
12
|
+
* - Elements, nesting, attributes (single- or double-quoted), self-closing
|
|
13
|
+
* tags, text content, and CDATA sections.
|
|
14
|
+
* - The five predefined entities (`& < > " '`) and
|
|
15
|
+
* numeric character references (`A`, `A`). A reference outside
|
|
16
|
+
* the Unicode range, or naming a surrogate, is an error rather than a
|
|
17
|
+
* crash.
|
|
18
|
+
* - Comments, processing instructions (`<?xml ... ?>`) and `<!DOCTYPE ...>`
|
|
19
|
+
* are skipped, not returned.
|
|
20
|
+
* - Namespace prefixes are kept verbatim as part of the name (`ns:tag`);
|
|
21
|
+
* they are not resolved to URIs.
|
|
22
|
+
* - In `html` mode, the void elements (`br`, `img`, `input`, `meta`, `link`,
|
|
23
|
+
* `hr`, `area`, `base`, `col`, `embed`, `param`, `source`, `track`, `wbr`)
|
|
24
|
+
* are treated as self-closing, unquoted attribute values are accepted, and
|
|
25
|
+
* tag names are compared case-insensitively.
|
|
26
|
+
*
|
|
27
|
+
* ## Not supported — each raises an error rather than guessing
|
|
28
|
+
*
|
|
29
|
+
* - DTD-declared entities, parameter entities and external entity references
|
|
30
|
+
* (so this parser cannot be made to fetch anything: there is no resolver).
|
|
31
|
+
* - Validation against a DTD or schema, `xml:space`, and attribute-value
|
|
32
|
+
* normalization beyond entity expansion.
|
|
33
|
+
* - Mismatched or unclosed tags are an error in `xml` mode. In `html` mode a
|
|
34
|
+
* mismatched close tag that matches an open ancestor closes the
|
|
35
|
+
* intervening elements, which is what browsers do; anything else is an
|
|
36
|
+
* error.
|
|
37
|
+
* - Raw-text elements (`<script>`, `<style>`) are parsed as ordinary
|
|
38
|
+
* elements, so `<` inside them must be escaped.
|
|
39
|
+
*/
|
|
40
|
+
export declare class XmlError extends Error {
|
|
41
|
+
readonly line: number;
|
|
42
|
+
constructor(message: string, line: number);
|
|
43
|
+
}
|
|
44
|
+
export type XmlElement = {
|
|
45
|
+
name: string;
|
|
46
|
+
attributes: Record<string, string>;
|
|
47
|
+
children: XmlNode[];
|
|
48
|
+
};
|
|
49
|
+
export type XmlText = {
|
|
50
|
+
text: string;
|
|
51
|
+
};
|
|
52
|
+
export type XmlNode = XmlElement | XmlText;
|
|
53
|
+
export declare function isElement(node: XmlNode): node is XmlElement;
|
|
54
|
+
export type XmlParseOptions = {
|
|
55
|
+
mode: "xml" | "html";
|
|
56
|
+
/** Drop text nodes that are only whitespace, which is nearly always formatting. */
|
|
57
|
+
trimWhitespace: boolean;
|
|
58
|
+
maxDepth: number;
|
|
59
|
+
};
|
|
60
|
+
/** Parse a document or fragment into a list of top-level nodes. */
|
|
61
|
+
export declare function parseXml(text: string, options: XmlParseOptions): XmlNode[];
|
|
62
|
+
/** Expand the predefined entities and numeric references; anything else is an error. */
|
|
63
|
+
export declare function decodeEntities(text: string, line: number): string;
|
|
64
|
+
/**
|
|
65
|
+
* Collapse the explicit tree into the compact shape most callers want:
|
|
66
|
+
* attributes under `@name`, text under `#text`, and repeated child elements
|
|
67
|
+
* as arrays. An element with one text child and no attributes becomes that
|
|
68
|
+
* string, which is what makes the result pleasant to query.
|
|
69
|
+
*/
|
|
70
|
+
export declare function toCompact(nodes: ReadonlyArray<XmlNode>): unknown;
|
|
71
|
+
/** All the text under a node, concatenated — the XPath `string()` of it. */
|
|
72
|
+
export declare function textContent(node: XmlNode): string;
|