@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/yaml.js
ADDED
|
@@ -0,0 +1,598 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A hand-written YAML reader and writer for the subset that configuration
|
|
3
|
+
* files actually use. There is no YAML dependency here, so the subset below
|
|
4
|
+
* is exactly what is supported — read it as a contract, not a summary.
|
|
5
|
+
*
|
|
6
|
+
* ## Read: supported
|
|
7
|
+
*
|
|
8
|
+
* - Block mappings (`key: value`), nested by space indentation.
|
|
9
|
+
* - Block sequences (`- item`), including a compact map on the dash line
|
|
10
|
+
* (`- name: a`) and nested sequences.
|
|
11
|
+
* - Plain, single-quoted and double-quoted scalars. Double-quoted strings
|
|
12
|
+
* honour `\n \t \r \" \\ \/ \0` and `\uXXXX`.
|
|
13
|
+
* - Core-schema typing of plain scalars: `null`, `~` and an empty value are
|
|
14
|
+
* null; `true`/`false` are booleans; integers, decimals, exponents, `0x`
|
|
15
|
+
* and `0o` literals are numbers; everything else is a string.
|
|
16
|
+
* - Flow collections: `[1, two, {a: b}]` and `{a: 1, b: [2]}`, nested.
|
|
17
|
+
* - Block scalars `|` and `>`, with the `-` and `+` chomping indicators.
|
|
18
|
+
* - `#` comments, on their own line or after a value.
|
|
19
|
+
* - A leading `---` and a trailing `...`.
|
|
20
|
+
* - Quoted keys, and keys containing a colon when quoted.
|
|
21
|
+
*
|
|
22
|
+
* `.inf`, `-.inf` and `.nan` read as the JavaScript infinities and NaN, and
|
|
23
|
+
* write back out as `.inf` / `.nan`. JSON has no way to spell any of them,
|
|
24
|
+
* so a YAML-to-JSON conversion renders them `null` — round-tripping a
|
|
25
|
+
* document that uses them has to stay within YAML or TOML.
|
|
26
|
+
*
|
|
27
|
+
* ## Read: rejected with an explicit error, never guessed at
|
|
28
|
+
*
|
|
29
|
+
* Anchors and aliases (`&a`, `*a`), merge keys (`<<:`), explicit tags
|
|
30
|
+
* (`!!str`), multiple documents in one stream, complex keys (`? `), tab
|
|
31
|
+
* characters in indentation, and dates — a date stays a string, because
|
|
32
|
+
* silently producing a `Date` would make the round trip lossy.
|
|
33
|
+
*
|
|
34
|
+
* ## Write
|
|
35
|
+
*
|
|
36
|
+
* Block style only, two-space indent, keys in their existing order. Strings
|
|
37
|
+
* are quoted whenever leaving them bare would change how they read back
|
|
38
|
+
* (empty, padded, or looking like a number, boolean or null). Multi-line
|
|
39
|
+
* strings are written double-quoted with `\n` escapes rather than as block
|
|
40
|
+
* scalars, so the output is unambiguous.
|
|
41
|
+
*/
|
|
42
|
+
import { isPlainObject } from "./json";
|
|
43
|
+
export class YamlError extends Error {
|
|
44
|
+
line;
|
|
45
|
+
constructor(message, line) {
|
|
46
|
+
super(message);
|
|
47
|
+
this.line = line;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const MAX_DEPTH = 64;
|
|
51
|
+
/** Parse a YAML document into JSON values. Throws `YamlError` with a line number. */
|
|
52
|
+
export function parseYaml(text) {
|
|
53
|
+
const lines = tokenize(text);
|
|
54
|
+
if (lines.length === 0)
|
|
55
|
+
return null;
|
|
56
|
+
const state = { lines, i: 0 };
|
|
57
|
+
const value = parseNode(state, lines[0]?.indent ?? 0, 0);
|
|
58
|
+
if (state.i < lines.length) {
|
|
59
|
+
const rest = lines[state.i];
|
|
60
|
+
throw new YamlError(`unexpected content ${JSON.stringify(rest.content)}`, rest.lineNo);
|
|
61
|
+
}
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
64
|
+
function tokenize(text) {
|
|
65
|
+
const out = [];
|
|
66
|
+
const raw = text.replace(/\r\n?/g, "\n").split("\n");
|
|
67
|
+
raw.forEach((original, idx) => {
|
|
68
|
+
const lineNo = idx + 1;
|
|
69
|
+
if (original.trim() === "")
|
|
70
|
+
return;
|
|
71
|
+
const indentMatch = /^[ \t]*/.exec(original);
|
|
72
|
+
if (indentMatch[0].includes("\t")) {
|
|
73
|
+
throw new YamlError("tab characters cannot be used for indentation", lineNo);
|
|
74
|
+
}
|
|
75
|
+
const indent = indentMatch[0].length;
|
|
76
|
+
const content = original.slice(indent).replace(/\s+$/, "");
|
|
77
|
+
if (content.startsWith("#"))
|
|
78
|
+
return;
|
|
79
|
+
if (content === "---")
|
|
80
|
+
return;
|
|
81
|
+
if (content === "...")
|
|
82
|
+
return;
|
|
83
|
+
if (content.startsWith("--- ")) {
|
|
84
|
+
throw new YamlError("a document header with content on the same line is not supported", lineNo);
|
|
85
|
+
}
|
|
86
|
+
if (content.startsWith("? ")) {
|
|
87
|
+
throw new YamlError("complex mapping keys ('? ') are not supported", lineNo);
|
|
88
|
+
}
|
|
89
|
+
if (content.startsWith("<<:")) {
|
|
90
|
+
throw new YamlError("merge keys ('<<:') are not supported", lineNo);
|
|
91
|
+
}
|
|
92
|
+
out.push({ indent, content, lineNo });
|
|
93
|
+
});
|
|
94
|
+
// Block scalars are re-read from the raw text, so keep it reachable.
|
|
95
|
+
rawSource.set(out, raw);
|
|
96
|
+
return out;
|
|
97
|
+
}
|
|
98
|
+
/** The original lines behind a token list, needed to read block scalars verbatim. */
|
|
99
|
+
const rawSource = new WeakMap();
|
|
100
|
+
function parseNode(state, indent, depth) {
|
|
101
|
+
if (depth > MAX_DEPTH) {
|
|
102
|
+
const line = state.lines[state.i];
|
|
103
|
+
throw new YamlError(`nesting deeper than ${MAX_DEPTH} levels`, line?.lineNo ?? 0);
|
|
104
|
+
}
|
|
105
|
+
const line = state.lines[state.i];
|
|
106
|
+
if (line === undefined)
|
|
107
|
+
return null;
|
|
108
|
+
if (isSequenceItem(line.content))
|
|
109
|
+
return parseSequence(state, indent, depth);
|
|
110
|
+
if (findKeyColon(line.content) >= 0)
|
|
111
|
+
return parseMapping(state, indent, depth);
|
|
112
|
+
// A bare scalar document.
|
|
113
|
+
state.i += 1;
|
|
114
|
+
return parseScalarValue(line.content, line.lineNo);
|
|
115
|
+
}
|
|
116
|
+
function isSequenceItem(content) {
|
|
117
|
+
return content === "-" || content.startsWith("- ");
|
|
118
|
+
}
|
|
119
|
+
function parseSequence(state, indent, depth) {
|
|
120
|
+
const out = [];
|
|
121
|
+
while (state.i < state.lines.length) {
|
|
122
|
+
const line = state.lines[state.i];
|
|
123
|
+
if (line.indent < indent)
|
|
124
|
+
break;
|
|
125
|
+
if (line.indent > indent) {
|
|
126
|
+
throw new YamlError("unexpected indentation inside a sequence", line.lineNo);
|
|
127
|
+
}
|
|
128
|
+
if (!isSequenceItem(line.content))
|
|
129
|
+
break;
|
|
130
|
+
const rest = line.content === "-" ? "" : line.content.slice(2).trimStart();
|
|
131
|
+
if (rest === "") {
|
|
132
|
+
state.i += 1;
|
|
133
|
+
out.push(parseChildBlock(state, indent, depth));
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
const offset = line.content.length - rest.length;
|
|
137
|
+
// Rewrite the dash line as if `rest` began its own line, so a compact
|
|
138
|
+
// map (`- a: 1`) or a nested sequence (`- - x`) parses by the same rules.
|
|
139
|
+
state.lines[state.i] = { indent: indent + offset, content: rest, lineNo: line.lineNo };
|
|
140
|
+
out.push(parseNode(state, indent + offset, depth + 1));
|
|
141
|
+
}
|
|
142
|
+
return out;
|
|
143
|
+
}
|
|
144
|
+
function parseMapping(state, indent, depth) {
|
|
145
|
+
const out = {};
|
|
146
|
+
while (state.i < state.lines.length) {
|
|
147
|
+
const line = state.lines[state.i];
|
|
148
|
+
if (line.indent < indent)
|
|
149
|
+
break;
|
|
150
|
+
if (line.indent > indent) {
|
|
151
|
+
throw new YamlError("unexpected indentation inside a mapping", line.lineNo);
|
|
152
|
+
}
|
|
153
|
+
if (isSequenceItem(line.content))
|
|
154
|
+
break;
|
|
155
|
+
const colon = findKeyColon(line.content);
|
|
156
|
+
if (colon < 0) {
|
|
157
|
+
throw new YamlError(`expected 'key: value', got ${JSON.stringify(line.content)}`, line.lineNo);
|
|
158
|
+
}
|
|
159
|
+
const keyText = line.content.slice(0, colon).trim();
|
|
160
|
+
const key = readKey(keyText, line.lineNo);
|
|
161
|
+
const valueText = stripComment(line.content.slice(colon + 1).trim());
|
|
162
|
+
state.i += 1;
|
|
163
|
+
if (valueText === "") {
|
|
164
|
+
out[key] = parseChildBlock(state, indent, depth);
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
const block = blockScalarHeader(valueText);
|
|
168
|
+
if (block !== null) {
|
|
169
|
+
out[key] = readBlockScalar(state, indent, block, line.lineNo);
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
out[key] = parseScalarValue(valueText, line.lineNo);
|
|
173
|
+
}
|
|
174
|
+
return out;
|
|
175
|
+
}
|
|
176
|
+
/** Parse whatever is indented under the current line, or null when nothing is. */
|
|
177
|
+
function parseChildBlock(state, indent, depth) {
|
|
178
|
+
const next = state.lines[state.i];
|
|
179
|
+
if (next === undefined || next.indent <= indent) {
|
|
180
|
+
// A sequence may be written at the same indentation as its key.
|
|
181
|
+
if (next !== undefined && next.indent === indent && isSequenceItem(next.content)) {
|
|
182
|
+
return parseSequence(state, indent, depth + 1);
|
|
183
|
+
}
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
return parseNode(state, next.indent, depth + 1);
|
|
187
|
+
}
|
|
188
|
+
/** Find the `:` that separates a key from its value, ignoring quotes and flow markers. */
|
|
189
|
+
export function findKeyColon(content) {
|
|
190
|
+
let quote = null;
|
|
191
|
+
let flow = 0;
|
|
192
|
+
for (let i = 0; i < content.length; i++) {
|
|
193
|
+
const c = content[i];
|
|
194
|
+
if (quote !== null) {
|
|
195
|
+
if (quote === '"' && c === "\\") {
|
|
196
|
+
i += 1;
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
if (c === quote)
|
|
200
|
+
quote = null;
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
if (c === "'" || c === '"') {
|
|
204
|
+
quote = c;
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
if (c === "[" || c === "{")
|
|
208
|
+
flow += 1;
|
|
209
|
+
else if (c === "]" || c === "}")
|
|
210
|
+
flow -= 1;
|
|
211
|
+
else if (c === "#" && i > 0 && content[i - 1] === " ")
|
|
212
|
+
return -1;
|
|
213
|
+
else if (c === ":" && flow === 0) {
|
|
214
|
+
const next = content[i + 1];
|
|
215
|
+
if (next === undefined || next === " ")
|
|
216
|
+
return i;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return -1;
|
|
220
|
+
}
|
|
221
|
+
function readKey(text, lineNo) {
|
|
222
|
+
if (text === "")
|
|
223
|
+
throw new YamlError("empty mapping key", lineNo);
|
|
224
|
+
if (text.startsWith("&") || text.startsWith("*")) {
|
|
225
|
+
throw new YamlError("anchors and aliases are not supported", lineNo);
|
|
226
|
+
}
|
|
227
|
+
if (text.startsWith("!"))
|
|
228
|
+
throw new YamlError("explicit tags are not supported", lineNo);
|
|
229
|
+
if ((text.startsWith('"') && text.endsWith('"')) ||
|
|
230
|
+
(text.startsWith("'") && text.endsWith("'"))) {
|
|
231
|
+
return readQuoted(text, lineNo);
|
|
232
|
+
}
|
|
233
|
+
return text;
|
|
234
|
+
}
|
|
235
|
+
function blockScalarHeader(text) {
|
|
236
|
+
const m = /^([|>])([-+]?)$/.exec(text);
|
|
237
|
+
if (m === null)
|
|
238
|
+
return null;
|
|
239
|
+
const chomp = m[2] === "-" ? "strip" : m[2] === "+" ? "keep" : "clip";
|
|
240
|
+
return { fold: m[1] === ">", chomp };
|
|
241
|
+
}
|
|
242
|
+
function readBlockScalar(state, parentIndent, header, lineNo) {
|
|
243
|
+
const raw = rawSource.get(state.lines);
|
|
244
|
+
if (raw === undefined)
|
|
245
|
+
throw new YamlError("internal: lost the source text", lineNo);
|
|
246
|
+
// Collect raw lines after the header until the indentation drops back.
|
|
247
|
+
const body = [];
|
|
248
|
+
let blockIndent = -1;
|
|
249
|
+
let cursor = lineNo; // raw index of the line after the header (0-based == lineNo)
|
|
250
|
+
for (; cursor < raw.length; cursor++) {
|
|
251
|
+
const text = raw[cursor];
|
|
252
|
+
if (text.trim() === "") {
|
|
253
|
+
body.push("");
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
const ind = /^ */.exec(text)[0].length;
|
|
257
|
+
if (ind <= parentIndent)
|
|
258
|
+
break;
|
|
259
|
+
if (blockIndent < 0)
|
|
260
|
+
blockIndent = ind;
|
|
261
|
+
if (ind < blockIndent)
|
|
262
|
+
break;
|
|
263
|
+
body.push(text.slice(blockIndent));
|
|
264
|
+
}
|
|
265
|
+
// Advance the token cursor past every token that came from those raw lines.
|
|
266
|
+
while (state.i < state.lines.length && state.lines[state.i].lineNo <= cursor) {
|
|
267
|
+
state.i += 1;
|
|
268
|
+
}
|
|
269
|
+
while (body.length > 0 && body[body.length - 1] === "")
|
|
270
|
+
body.pop();
|
|
271
|
+
let out;
|
|
272
|
+
if (header.fold) {
|
|
273
|
+
const folded = [];
|
|
274
|
+
for (const l of body) {
|
|
275
|
+
if (l === "") {
|
|
276
|
+
folded.push("\n");
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
if (folded.length > 0 && !folded[folded.length - 1]?.endsWith("\n"))
|
|
280
|
+
folded.push(" ");
|
|
281
|
+
folded.push(l);
|
|
282
|
+
}
|
|
283
|
+
out = folded.join("");
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
out = body.join("\n");
|
|
287
|
+
}
|
|
288
|
+
if (header.chomp === "strip")
|
|
289
|
+
return out.replace(/\n+$/, "");
|
|
290
|
+
return `${out}\n`;
|
|
291
|
+
}
|
|
292
|
+
/** Remove a trailing ` # comment`, respecting quotes. */
|
|
293
|
+
export function stripComment(text) {
|
|
294
|
+
let quote = null;
|
|
295
|
+
for (let i = 0; i < text.length; i++) {
|
|
296
|
+
const c = text[i];
|
|
297
|
+
if (quote !== null) {
|
|
298
|
+
if (quote === '"' && c === "\\") {
|
|
299
|
+
i += 1;
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
if (c === quote)
|
|
303
|
+
quote = null;
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
if (c === "'" || c === '"') {
|
|
307
|
+
quote = c;
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
if (c === "#" && (i === 0 || text[i - 1] === " "))
|
|
311
|
+
return text.slice(0, i).trimEnd();
|
|
312
|
+
}
|
|
313
|
+
return text;
|
|
314
|
+
}
|
|
315
|
+
/** Parse a single value: flow collection, quoted string, or plain scalar. */
|
|
316
|
+
export function parseScalarValue(text, lineNo) {
|
|
317
|
+
const t = text.trim();
|
|
318
|
+
if (t.startsWith("&") || (t.startsWith("*") && t.length > 1 && !t.includes(" "))) {
|
|
319
|
+
throw new YamlError("anchors and aliases are not supported", lineNo);
|
|
320
|
+
}
|
|
321
|
+
if (t.startsWith("!"))
|
|
322
|
+
throw new YamlError("explicit tags are not supported", lineNo);
|
|
323
|
+
if (t.startsWith("[") || t.startsWith("{")) {
|
|
324
|
+
const parsed = parseFlow(t, lineNo);
|
|
325
|
+
return parsed;
|
|
326
|
+
}
|
|
327
|
+
if ((t.startsWith('"') && t.endsWith('"') && t.length > 1) ||
|
|
328
|
+
(t.startsWith("'") && t.endsWith("'") && t.length > 1)) {
|
|
329
|
+
return readQuoted(t, lineNo);
|
|
330
|
+
}
|
|
331
|
+
return plainScalar(t);
|
|
332
|
+
}
|
|
333
|
+
/** Core-schema typing for an unquoted scalar. */
|
|
334
|
+
export function plainScalar(t) {
|
|
335
|
+
if (t === "" || t === "~" || t === "null" || t === "Null" || t === "NULL")
|
|
336
|
+
return null;
|
|
337
|
+
if (t === "true" || t === "True" || t === "TRUE")
|
|
338
|
+
return true;
|
|
339
|
+
if (t === "false" || t === "False" || t === "FALSE")
|
|
340
|
+
return false;
|
|
341
|
+
if (/^[-+]?(0|[1-9][0-9]*)$/.test(t)) {
|
|
342
|
+
const n = Number(t);
|
|
343
|
+
if (Number.isSafeInteger(n))
|
|
344
|
+
return n;
|
|
345
|
+
return t;
|
|
346
|
+
}
|
|
347
|
+
if (/^0x[0-9a-fA-F]+$/.test(t))
|
|
348
|
+
return Number.parseInt(t.slice(2), 16);
|
|
349
|
+
if (/^0o[0-7]+$/.test(t))
|
|
350
|
+
return Number.parseInt(t.slice(2), 8);
|
|
351
|
+
if (/^[-+]?(\.[0-9]+|[0-9]+\.[0-9]*)([eE][-+]?[0-9]+)?$/.test(t))
|
|
352
|
+
return Number(t);
|
|
353
|
+
if (/^[-+]?[0-9]+[eE][-+]?[0-9]+$/.test(t))
|
|
354
|
+
return Number(t);
|
|
355
|
+
if (t === ".inf" || t === ".Inf")
|
|
356
|
+
return Number.POSITIVE_INFINITY;
|
|
357
|
+
if (t === "-.inf" || t === "-.Inf")
|
|
358
|
+
return Number.NEGATIVE_INFINITY;
|
|
359
|
+
return t;
|
|
360
|
+
}
|
|
361
|
+
function readQuoted(text, lineNo) {
|
|
362
|
+
const q = text[0];
|
|
363
|
+
const body = text.slice(1, -1);
|
|
364
|
+
if (q === "'")
|
|
365
|
+
return body.replace(/''/g, "'");
|
|
366
|
+
let out = "";
|
|
367
|
+
for (let i = 0; i < body.length; i++) {
|
|
368
|
+
const c = body[i];
|
|
369
|
+
if (c !== "\\") {
|
|
370
|
+
out += c;
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
const n = body[i + 1];
|
|
374
|
+
i += 1;
|
|
375
|
+
switch (n) {
|
|
376
|
+
case "n":
|
|
377
|
+
out += "\n";
|
|
378
|
+
break;
|
|
379
|
+
case "t":
|
|
380
|
+
out += "\t";
|
|
381
|
+
break;
|
|
382
|
+
case "r":
|
|
383
|
+
out += "\r";
|
|
384
|
+
break;
|
|
385
|
+
case "0":
|
|
386
|
+
out += "\0";
|
|
387
|
+
break;
|
|
388
|
+
case "\\":
|
|
389
|
+
out += "\\";
|
|
390
|
+
break;
|
|
391
|
+
case '"':
|
|
392
|
+
out += '"';
|
|
393
|
+
break;
|
|
394
|
+
case "/":
|
|
395
|
+
out += "/";
|
|
396
|
+
break;
|
|
397
|
+
case "u": {
|
|
398
|
+
const hex = body.slice(i + 1, i + 5);
|
|
399
|
+
if (!/^[0-9a-fA-F]{4}$/.test(hex)) {
|
|
400
|
+
throw new YamlError(`bad \\u escape ${JSON.stringify(hex)}`, lineNo);
|
|
401
|
+
}
|
|
402
|
+
out += String.fromCharCode(Number.parseInt(hex, 16));
|
|
403
|
+
i += 4;
|
|
404
|
+
break;
|
|
405
|
+
}
|
|
406
|
+
default:
|
|
407
|
+
throw new YamlError(`unknown escape \\${n ?? ""}`, lineNo);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
return out;
|
|
411
|
+
}
|
|
412
|
+
/** Parse a flow collection (`[...]` or `{...}`) including nesting. */
|
|
413
|
+
export function parseFlow(text, lineNo) {
|
|
414
|
+
const state = { text, i: 0, lineNo };
|
|
415
|
+
const value = parseFlowNode(state, 0);
|
|
416
|
+
skipFlowSpace(state);
|
|
417
|
+
if (state.i < text.length) {
|
|
418
|
+
throw new YamlError(`trailing characters after a flow collection: ${text.slice(state.i)}`, lineNo);
|
|
419
|
+
}
|
|
420
|
+
return value;
|
|
421
|
+
}
|
|
422
|
+
function skipFlowSpace(s) {
|
|
423
|
+
while (s.i < s.text.length && (s.text[s.i] === " " || s.text[s.i] === "\t"))
|
|
424
|
+
s.i += 1;
|
|
425
|
+
}
|
|
426
|
+
function parseFlowNode(s, depth) {
|
|
427
|
+
if (depth > MAX_DEPTH)
|
|
428
|
+
throw new YamlError("flow collection nested too deeply", s.lineNo);
|
|
429
|
+
skipFlowSpace(s);
|
|
430
|
+
const c = s.text[s.i];
|
|
431
|
+
if (c === "[") {
|
|
432
|
+
s.i += 1;
|
|
433
|
+
const out = [];
|
|
434
|
+
skipFlowSpace(s);
|
|
435
|
+
if (s.text[s.i] === "]") {
|
|
436
|
+
s.i += 1;
|
|
437
|
+
return out;
|
|
438
|
+
}
|
|
439
|
+
for (;;) {
|
|
440
|
+
out.push(parseFlowNode(s, depth + 1));
|
|
441
|
+
skipFlowSpace(s);
|
|
442
|
+
const d = s.text[s.i];
|
|
443
|
+
if (d === ",") {
|
|
444
|
+
s.i += 1;
|
|
445
|
+
skipFlowSpace(s);
|
|
446
|
+
if (s.text[s.i] === "]") {
|
|
447
|
+
s.i += 1;
|
|
448
|
+
return out;
|
|
449
|
+
}
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
if (d === "]") {
|
|
453
|
+
s.i += 1;
|
|
454
|
+
return out;
|
|
455
|
+
}
|
|
456
|
+
throw new YamlError("expected ',' or ']' in a flow sequence", s.lineNo);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
if (c === "{") {
|
|
460
|
+
s.i += 1;
|
|
461
|
+
const out = {};
|
|
462
|
+
skipFlowSpace(s);
|
|
463
|
+
if (s.text[s.i] === "}") {
|
|
464
|
+
s.i += 1;
|
|
465
|
+
return out;
|
|
466
|
+
}
|
|
467
|
+
for (;;) {
|
|
468
|
+
skipFlowSpace(s);
|
|
469
|
+
const key = parseFlowScalarText(s);
|
|
470
|
+
skipFlowSpace(s);
|
|
471
|
+
if (s.text[s.i] !== ":")
|
|
472
|
+
throw new YamlError("expected ':' in a flow mapping", s.lineNo);
|
|
473
|
+
s.i += 1;
|
|
474
|
+
out[String(coerceFlowKey(key, s.lineNo))] = parseFlowNode(s, depth + 1);
|
|
475
|
+
skipFlowSpace(s);
|
|
476
|
+
const d = s.text[s.i];
|
|
477
|
+
if (d === ",") {
|
|
478
|
+
s.i += 1;
|
|
479
|
+
skipFlowSpace(s);
|
|
480
|
+
if (s.text[s.i] === "}") {
|
|
481
|
+
s.i += 1;
|
|
482
|
+
return out;
|
|
483
|
+
}
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
if (d === "}") {
|
|
487
|
+
s.i += 1;
|
|
488
|
+
return out;
|
|
489
|
+
}
|
|
490
|
+
throw new YamlError("expected ',' or '}' in a flow mapping", s.lineNo);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
const raw = parseFlowScalarText(s);
|
|
494
|
+
if (raw.quoted)
|
|
495
|
+
return readQuoted(raw.text, s.lineNo);
|
|
496
|
+
return plainScalar(raw.text.trim());
|
|
497
|
+
}
|
|
498
|
+
function coerceFlowKey(key, lineNo) {
|
|
499
|
+
return key.quoted ? readQuoted(key.text, lineNo) : key.text.trim();
|
|
500
|
+
}
|
|
501
|
+
function parseFlowScalarText(s) {
|
|
502
|
+
skipFlowSpace(s);
|
|
503
|
+
const c = s.text[s.i];
|
|
504
|
+
if (c === '"' || c === "'") {
|
|
505
|
+
const start = s.i;
|
|
506
|
+
s.i += 1;
|
|
507
|
+
while (s.i < s.text.length) {
|
|
508
|
+
const d = s.text[s.i];
|
|
509
|
+
if (c === '"' && d === "\\") {
|
|
510
|
+
s.i += 2;
|
|
511
|
+
continue;
|
|
512
|
+
}
|
|
513
|
+
if (d === c) {
|
|
514
|
+
s.i += 1;
|
|
515
|
+
return { text: s.text.slice(start, s.i), quoted: true };
|
|
516
|
+
}
|
|
517
|
+
s.i += 1;
|
|
518
|
+
}
|
|
519
|
+
throw new YamlError("unterminated quoted string in a flow collection", s.lineNo);
|
|
520
|
+
}
|
|
521
|
+
const start = s.i;
|
|
522
|
+
while (s.i < s.text.length && !",:]}".includes(s.text[s.i]))
|
|
523
|
+
s.i += 1;
|
|
524
|
+
return { text: s.text.slice(start, s.i), quoted: false };
|
|
525
|
+
}
|
|
526
|
+
// ---------------------------------------------------------------------------
|
|
527
|
+
// Writing
|
|
528
|
+
const PLAIN_SAFE = /^[A-Za-z_][A-Za-z0-9_./ -]*$/;
|
|
529
|
+
/** True when a string can be written bare without changing meaning on re-read. */
|
|
530
|
+
export function canWritePlain(s) {
|
|
531
|
+
if (s === "")
|
|
532
|
+
return false;
|
|
533
|
+
if (s !== s.trim())
|
|
534
|
+
return false;
|
|
535
|
+
if (!PLAIN_SAFE.test(s))
|
|
536
|
+
return false;
|
|
537
|
+
if (s.includes(" #"))
|
|
538
|
+
return false;
|
|
539
|
+
// Anything that would read back as a non-string must be quoted.
|
|
540
|
+
return typeof plainScalar(s) === "string";
|
|
541
|
+
}
|
|
542
|
+
function writeScalar(value) {
|
|
543
|
+
if (value === null || value === undefined)
|
|
544
|
+
return "null";
|
|
545
|
+
if (typeof value === "boolean")
|
|
546
|
+
return String(value);
|
|
547
|
+
if (typeof value === "number") {
|
|
548
|
+
if (!Number.isFinite(value))
|
|
549
|
+
return value > 0 ? ".inf" : Number.isNaN(value) ? ".nan" : "-.inf";
|
|
550
|
+
return String(value);
|
|
551
|
+
}
|
|
552
|
+
const s = String(value);
|
|
553
|
+
return canWritePlain(s) ? s : JSON.stringify(s);
|
|
554
|
+
}
|
|
555
|
+
/** Serialize a JSON value as block-style YAML with a two-space indent. */
|
|
556
|
+
export function stringifyYaml(value, indentLevel = 0) {
|
|
557
|
+
const pad = " ".repeat(indentLevel);
|
|
558
|
+
if (Array.isArray(value)) {
|
|
559
|
+
if (value.length === 0)
|
|
560
|
+
return `${pad}[]`;
|
|
561
|
+
return value
|
|
562
|
+
.map((el) => {
|
|
563
|
+
if (isPlainObject(el) && Object.keys(el).length > 0) {
|
|
564
|
+
const body = stringifyYaml(el, indentLevel + 1);
|
|
565
|
+
return `${pad}-${body.slice(pad.length + 1)}`;
|
|
566
|
+
}
|
|
567
|
+
if (Array.isArray(el) && el.length > 0) {
|
|
568
|
+
const body = stringifyYaml(el, indentLevel + 1);
|
|
569
|
+
return `${pad}-${body.slice(pad.length + 1)}`;
|
|
570
|
+
}
|
|
571
|
+
return `${pad}- ${writeScalar(el)}`;
|
|
572
|
+
})
|
|
573
|
+
.join("\n");
|
|
574
|
+
}
|
|
575
|
+
if (isPlainObject(value)) {
|
|
576
|
+
const keys = Object.keys(value);
|
|
577
|
+
if (keys.length === 0)
|
|
578
|
+
return `${pad}{}`;
|
|
579
|
+
return keys
|
|
580
|
+
.map((k) => {
|
|
581
|
+
const v = value[k];
|
|
582
|
+
const key = canWritePlain(k) ? k : JSON.stringify(k);
|
|
583
|
+
if (Array.isArray(v)) {
|
|
584
|
+
if (v.length === 0)
|
|
585
|
+
return `${pad}${key}: []`;
|
|
586
|
+
return `${pad}${key}:\n${stringifyYaml(v, indentLevel + 1)}`;
|
|
587
|
+
}
|
|
588
|
+
if (isPlainObject(v)) {
|
|
589
|
+
if (Object.keys(v).length === 0)
|
|
590
|
+
return `${pad}${key}: {}`;
|
|
591
|
+
return `${pad}${key}:\n${stringifyYaml(v, indentLevel + 1)}`;
|
|
592
|
+
}
|
|
593
|
+
return `${pad}${key}: ${writeScalar(v)}`;
|
|
594
|
+
})
|
|
595
|
+
.join("\n");
|
|
596
|
+
}
|
|
597
|
+
return `${pad}${writeScalar(value)}`;
|
|
598
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@crewhaus/tool-data",
|
|
3
|
+
"version": "0.7.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Deterministic structured-data tools: JSON/YAML/TOML/CSV/XML parsing, querying, conversion, diffing and reshaping",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"test": "bun test src"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@crewhaus/errors": "0.7.0",
|
|
19
|
+
"@crewhaus/tool-builder": "0.7.0",
|
|
20
|
+
"@crewhaus/tool-catalog": "0.7.0",
|
|
21
|
+
"zod": "^3.23.8"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@crewhaus/tool-executor": "workspace:*"
|
|
25
|
+
},
|
|
26
|
+
"license": "Apache-2.0",
|
|
27
|
+
"author": {
|
|
28
|
+
"name": "Max Meier",
|
|
29
|
+
"email": "max@crewhaus.ai",
|
|
30
|
+
"url": "https://crewhaus.ai"
|
|
31
|
+
},
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/crewhaus/factory.git",
|
|
35
|
+
"directory": "packages/tool-data"
|
|
36
|
+
},
|
|
37
|
+
"homepage": "https://github.com/crewhaus/factory/tree/main/packages/tool-data#readme",
|
|
38
|
+
"bugs": {
|
|
39
|
+
"url": "https://github.com/crewhaus/factory/issues"
|
|
40
|
+
},
|
|
41
|
+
"publishConfig": {
|
|
42
|
+
"access": "public"
|
|
43
|
+
},
|
|
44
|
+
"files": ["dist", "README.md", "LICENSE", "NOTICE"]
|
|
45
|
+
}
|