@crewhaus/tool-data 0.7.0 → 0.8.1
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 +201 -0
- package/NOTICE +9 -0
- package/README.md +38 -1
- package/dist/index.js +280 -63
- package/dist/lib/csv.d.ts +27 -2
- package/dist/lib/csv.js +77 -14
- package/dist/lib/diff.js +3 -3
- package/dist/lib/json.d.ts +51 -0
- package/dist/lib/json.js +174 -2
- package/dist/lib/jsonl.d.ts +1 -10
- package/dist/lib/jsonl.js +15 -1
- package/dist/lib/jsonpath.d.ts +38 -2
- package/dist/lib/jsonpath.js +166 -53
- package/dist/lib/own-record.d.ts +22 -0
- package/dist/lib/own-record.js +66 -0
- package/dist/lib/patch.d.ts +18 -2
- package/dist/lib/patch.js +72 -21
- package/dist/lib/table.d.ts +45 -8
- package/dist/lib/table.js +164 -59
- package/dist/lib/toml.d.ts +8 -1
- package/dist/lib/toml.js +77 -48
- package/dist/lib/xml.js +23 -10
- package/dist/lib/yaml.d.ts +19 -2
- package/dist/lib/yaml.js +88 -42
- package/package.json +11 -6
package/dist/lib/toml.js
CHANGED
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
* (`null`) is skipped, and the writer reports which keys it dropped rather
|
|
40
40
|
* than pretending the output is complete.
|
|
41
41
|
*/
|
|
42
|
-
import { isPlainObject } from "./json";
|
|
42
|
+
import { MAX_NESTING_DEPTH, OutputLimitError, getOwn, isPlainObject, setOwn } from "./json";
|
|
43
43
|
export class TomlError extends Error {
|
|
44
44
|
line;
|
|
45
45
|
constructor(message, line) {
|
|
@@ -47,15 +47,26 @@ export class TomlError extends Error {
|
|
|
47
47
|
this.line = line;
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* A key or table path with more parts than MAX_NESTING_DEPTH, refused as it
|
|
52
|
+
* is read: the value it builds would nest past the cap and be refused
|
|
53
|
+
* anyway, and building it first cost the square of the path's length.
|
|
54
|
+
*/
|
|
55
|
+
export class TomlTooDeepError extends TomlError {
|
|
56
|
+
}
|
|
50
57
|
/** Parse a TOML document into JSON values. Throws `TomlError` with a line number. */
|
|
51
58
|
export function parseToml(text) {
|
|
52
59
|
const root = {};
|
|
53
60
|
const cur = { text: text.replace(/\r\n?/g, "\n"), i: 0, line: 1 };
|
|
54
61
|
// Tables created by a `[header]`, and tables brought into being implicitly
|
|
55
62
|
// 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
|
-
|
|
58
|
-
|
|
63
|
+
// redefining the other, so both have to be remembered. They are the table
|
|
64
|
+
// objects themselves, not their dotted names: a name per prefix of every
|
|
65
|
+
// dotted key cost the square of the key's length (80 KB took 3.6 GB), and
|
|
66
|
+
// the joined name confused a quoted "a.b" with a.b, and each element's
|
|
67
|
+
// sub-table in an array of tables with the first's.
|
|
68
|
+
const defined = new WeakSet();
|
|
69
|
+
const dotted = new WeakSet();
|
|
59
70
|
let table = root;
|
|
60
71
|
let tablePath = [];
|
|
61
72
|
for (;;) {
|
|
@@ -66,20 +77,20 @@ export function parseToml(text) {
|
|
|
66
77
|
const isArray = cur.text[cur.i + 1] === "[";
|
|
67
78
|
cur.i += isArray ? 2 : 1;
|
|
68
79
|
const path = readKeyPath(cur, isArray ? "]]" : "]");
|
|
69
|
-
const key = path.join(".");
|
|
70
80
|
if (isArray) {
|
|
71
81
|
table = pushArrayTable(root, path, cur);
|
|
72
82
|
tablePath = path;
|
|
73
83
|
}
|
|
74
84
|
else {
|
|
75
|
-
|
|
76
|
-
|
|
85
|
+
const target = descend(root, path, cur);
|
|
86
|
+
if (defined.has(target)) {
|
|
87
|
+
throw new TomlError(`table [${path.join(".")}] is defined twice`, cur.line);
|
|
77
88
|
}
|
|
78
|
-
if (dotted.has(
|
|
79
|
-
throw new TomlError(`table [${
|
|
89
|
+
if (dotted.has(target)) {
|
|
90
|
+
throw new TomlError(`table [${path.join(".")}] was already created by a dotted key, so it cannot be defined again`, cur.line);
|
|
80
91
|
}
|
|
81
|
-
defined.add(
|
|
82
|
-
table =
|
|
92
|
+
defined.add(target);
|
|
93
|
+
table = target;
|
|
83
94
|
tablePath = path;
|
|
84
95
|
}
|
|
85
96
|
expectLineEnd(cur);
|
|
@@ -135,6 +146,9 @@ const BARE_KEY = /[A-Za-z0-9_-]/;
|
|
|
135
146
|
function readKeyPath(c, terminator) {
|
|
136
147
|
const path = [];
|
|
137
148
|
for (;;) {
|
|
149
|
+
if (path.length >= MAX_NESTING_DEPTH) {
|
|
150
|
+
throw new TomlTooDeepError(`a key with more than ${MAX_NESTING_DEPTH} parts nests deeper than ${MAX_NESTING_DEPTH} levels`, c.line);
|
|
151
|
+
}
|
|
138
152
|
skipInlineSpace(c);
|
|
139
153
|
const ch = c.text[c.i];
|
|
140
154
|
if (ch === '"' || ch === "'") {
|
|
@@ -163,10 +177,10 @@ function readKeyPath(c, terminator) {
|
|
|
163
177
|
function descend(root, path, c) {
|
|
164
178
|
let node = root;
|
|
165
179
|
path.forEach((seg, idx) => {
|
|
166
|
-
const existing = node
|
|
180
|
+
const existing = getOwn(node, seg);
|
|
167
181
|
if (existing === undefined) {
|
|
168
182
|
const fresh = {};
|
|
169
|
-
node
|
|
183
|
+
setOwn(node, seg, fresh);
|
|
170
184
|
node = fresh;
|
|
171
185
|
return;
|
|
172
186
|
}
|
|
@@ -188,10 +202,10 @@ function descend(root, path, c) {
|
|
|
188
202
|
function pushArrayTable(root, path, c) {
|
|
189
203
|
const parent = descend(root, path.slice(0, -1), c);
|
|
190
204
|
const key = path[path.length - 1];
|
|
191
|
-
const existing = parent
|
|
205
|
+
const existing = getOwn(parent, key);
|
|
192
206
|
const fresh = {};
|
|
193
207
|
if (existing === undefined) {
|
|
194
|
-
parent
|
|
208
|
+
setOwn(parent, key, [fresh]);
|
|
195
209
|
return fresh;
|
|
196
210
|
}
|
|
197
211
|
if (!Array.isArray(existing)) {
|
|
@@ -212,31 +226,34 @@ function pushArrayTable(root, path, c) {
|
|
|
212
226
|
*/
|
|
213
227
|
function assign(table, path, value, c, tablePath, tables) {
|
|
214
228
|
let node = table;
|
|
215
|
-
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
|
|
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];
|
|
229
|
+
for (let idx = 0; idx < path.length - 1; idx++) {
|
|
230
|
+
const seg = path[idx];
|
|
231
|
+
const existing = getOwn(node, seg);
|
|
232
|
+
let next;
|
|
224
233
|
if (existing === undefined) {
|
|
225
|
-
|
|
226
|
-
node
|
|
227
|
-
node = fresh;
|
|
228
|
-
return;
|
|
234
|
+
next = {};
|
|
235
|
+
setOwn(node, seg, next);
|
|
229
236
|
}
|
|
230
|
-
if (
|
|
237
|
+
else if (isPlainObject(existing)) {
|
|
238
|
+
next = existing;
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
231
241
|
throw new TomlError(`"${[...tablePath, ...path].join(".")}" collides with a value`, c.line);
|
|
232
242
|
}
|
|
233
|
-
|
|
234
|
-
|
|
243
|
+
if (tables !== undefined) {
|
|
244
|
+
if (tables.defined.has(next)) {
|
|
245
|
+
const absolute = [...tablePath, ...path.slice(0, idx + 1)].join(".");
|
|
246
|
+
throw new TomlError(`"${absolute}" is already a table defined by [${absolute}], so a dotted key cannot extend it`, c.line);
|
|
247
|
+
}
|
|
248
|
+
tables.dotted.add(next);
|
|
249
|
+
}
|
|
250
|
+
node = next;
|
|
251
|
+
}
|
|
235
252
|
const key = path[path.length - 1];
|
|
236
253
|
if (Object.hasOwn(node, key)) {
|
|
237
254
|
throw new TomlError(`key "${[...tablePath, ...path].join(".")}" is defined twice`, c.line);
|
|
238
255
|
}
|
|
239
|
-
node
|
|
256
|
+
setOwn(node, key, value);
|
|
240
257
|
}
|
|
241
258
|
function readString(c) {
|
|
242
259
|
const q = c.text[c.i];
|
|
@@ -477,23 +494,33 @@ function isTableArray(value) {
|
|
|
477
494
|
* keys that were dropped because TOML has no representation for them
|
|
478
495
|
* (`null` and `undefined`).
|
|
479
496
|
*/
|
|
480
|
-
export function stringifyToml(value) {
|
|
497
|
+
export function stringifyToml(value, maxChars = Number.POSITIVE_INFINITY) {
|
|
481
498
|
if (!isPlainObject(value)) {
|
|
482
499
|
throw new TomlError("TOML documents must be objects at the top level", 0);
|
|
483
500
|
}
|
|
484
|
-
const
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
501
|
+
const out = { lines: [], skipped: [], chars: -1, max: maxChars };
|
|
502
|
+
emitTable(value, [], out);
|
|
503
|
+
return { text: out.lines.join("\n"), skipped: out.skipped };
|
|
504
|
+
}
|
|
505
|
+
function charge(out, length) {
|
|
506
|
+
out.chars += length;
|
|
507
|
+
if (out.chars > out.max)
|
|
508
|
+
throw new OutputLimitError(out.max, "the TOML");
|
|
509
|
+
}
|
|
510
|
+
function emitLine(out, line) {
|
|
511
|
+
charge(out, line.length + 1);
|
|
512
|
+
out.lines.push(line);
|
|
488
513
|
}
|
|
489
|
-
function emitTable(table, path,
|
|
514
|
+
function emitTable(table, path, out, headerAlreadyWritten = false) {
|
|
490
515
|
const scalars = [];
|
|
491
516
|
const subTables = [];
|
|
492
517
|
const tableArrays = [];
|
|
493
518
|
for (const key of Object.keys(table)) {
|
|
494
519
|
const v = table[key];
|
|
495
520
|
if (v === null || v === undefined) {
|
|
496
|
-
|
|
521
|
+
const dropped = [...path, key].join(".");
|
|
522
|
+
charge(out, dropped.length + 2);
|
|
523
|
+
out.skipped.push(dropped);
|
|
497
524
|
continue;
|
|
498
525
|
}
|
|
499
526
|
if (isTableArray(v)) {
|
|
@@ -510,21 +537,23 @@ function emitTable(table, path, lines, skipped, headerAlreadyWritten = false) {
|
|
|
510
537
|
path.length > 0 &&
|
|
511
538
|
(scalars.length > 0 || (subTables.length === 0 && tableArrays.length === 0));
|
|
512
539
|
if (needsHeader) {
|
|
513
|
-
if (lines.length > 0)
|
|
514
|
-
|
|
515
|
-
|
|
540
|
+
if (out.lines.length > 0)
|
|
541
|
+
emitLine(out, "");
|
|
542
|
+
emitLine(out, `[${path.map(writeKey).join(".")}]`);
|
|
516
543
|
}
|
|
517
|
-
|
|
544
|
+
for (const line of scalars)
|
|
545
|
+
emitLine(out, line);
|
|
518
546
|
for (const [key, sub] of subTables) {
|
|
519
|
-
emitTable(sub, [...path, key],
|
|
547
|
+
emitTable(sub, [...path, key], out);
|
|
520
548
|
}
|
|
521
549
|
for (const [key, arr] of tableArrays) {
|
|
522
550
|
const childPath = [...path, key];
|
|
551
|
+
const header = `[[${childPath.map(writeKey).join(".")}]]`;
|
|
523
552
|
for (const element of arr) {
|
|
524
|
-
if (lines.length > 0)
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
emitTable(element, childPath,
|
|
553
|
+
if (out.lines.length > 0)
|
|
554
|
+
emitLine(out, "");
|
|
555
|
+
emitLine(out, header);
|
|
556
|
+
emitTable(element, childPath, out, true);
|
|
528
557
|
}
|
|
529
558
|
}
|
|
530
559
|
}
|
package/dist/lib/xml.js
CHANGED
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
* - Raw-text elements (`<script>`, `<style>`) are parsed as ordinary
|
|
38
38
|
* elements, so `<` inside them must be escaped.
|
|
39
39
|
*/
|
|
40
|
+
import { getOwn, setOwn } from "./json";
|
|
40
41
|
export class XmlError extends Error {
|
|
41
42
|
line;
|
|
42
43
|
constructor(message, line) {
|
|
@@ -211,7 +212,7 @@ function readTag(src, start, line, mode) {
|
|
|
211
212
|
// A bare attribute is HTML's `<input disabled>`; XML requires a value.
|
|
212
213
|
if (mode === "xml")
|
|
213
214
|
throw new XmlError(`attribute "${attrRaw}" has no value`, line);
|
|
214
|
-
attributes
|
|
215
|
+
setOwn(attributes, attrName, "");
|
|
215
216
|
continue;
|
|
216
217
|
}
|
|
217
218
|
i += 1;
|
|
@@ -222,7 +223,7 @@ function readTag(src, start, line, mode) {
|
|
|
222
223
|
const end = src.indexOf(quote, i + 1);
|
|
223
224
|
if (end < 0)
|
|
224
225
|
throw new XmlError(`unterminated value for "${attrRaw}"`, line);
|
|
225
|
-
attributes
|
|
226
|
+
setOwn(attributes, attrName, decodeEntities(src.slice(i + 1, end), line));
|
|
226
227
|
i = end + 1;
|
|
227
228
|
continue;
|
|
228
229
|
}
|
|
@@ -231,16 +232,22 @@ function readTag(src, start, line, mode) {
|
|
|
231
232
|
const valueStart = i;
|
|
232
233
|
while (i < src.length && !/[\s>]/.test(src[i]))
|
|
233
234
|
i += 1;
|
|
234
|
-
attributes
|
|
235
|
+
setOwn(attributes, attrName, decodeEntities(src.slice(valueStart, i), line));
|
|
235
236
|
}
|
|
236
237
|
}
|
|
237
|
-
|
|
238
|
+
/**
|
|
239
|
+
* A null prototype, and read with `Object.hasOwn`: the key is whatever name
|
|
240
|
+
* the document wrote between `&` and `;`, and on an object literal
|
|
241
|
+
* `&constructor;` decoded to "function Object() { [native code] }" and
|
|
242
|
+
* `&__proto__;` to "[object Object]" instead of being refused as unknown.
|
|
243
|
+
*/
|
|
244
|
+
const NAMED_ENTITIES = Object.freeze(Object.assign(Object.create(null), {
|
|
238
245
|
amp: "&",
|
|
239
246
|
lt: "<",
|
|
240
247
|
gt: ">",
|
|
241
248
|
quot: '"',
|
|
242
249
|
apos: "'",
|
|
243
|
-
};
|
|
250
|
+
}));
|
|
244
251
|
/** Expand the predefined entities and numeric references; anything else is an error. */
|
|
245
252
|
export function decodeEntities(text, line) {
|
|
246
253
|
if (!text.includes("&"))
|
|
@@ -266,7 +273,7 @@ export function decodeEntities(text, line) {
|
|
|
266
273
|
out += codePoint(body, Number.parseInt(body.slice(1), 10), line);
|
|
267
274
|
continue;
|
|
268
275
|
}
|
|
269
|
-
const named = NAMED_ENTITIES[body];
|
|
276
|
+
const named = Object.hasOwn(NAMED_ENTITIES, body) ? NAMED_ENTITIES[body] : undefined;
|
|
270
277
|
if (named === undefined) {
|
|
271
278
|
throw new XmlError(`unknown entity &${body}; — only the five predefined ones are known`, line);
|
|
272
279
|
}
|
|
@@ -305,13 +312,15 @@ export function toCompact(nodes) {
|
|
|
305
312
|
continue;
|
|
306
313
|
}
|
|
307
314
|
const value = compactElement(node);
|
|
308
|
-
|
|
315
|
+
// Own fields only: an element named `constructor` or `__proto__` is
|
|
316
|
+
// data like any other, not a reach into Object.prototype.
|
|
317
|
+
const existing = getOwn(out, node.name);
|
|
309
318
|
if (existing === undefined)
|
|
310
|
-
out
|
|
319
|
+
setOwn(out, node.name, value);
|
|
311
320
|
else if (Array.isArray(existing))
|
|
312
321
|
existing.push(value);
|
|
313
322
|
else
|
|
314
|
-
out
|
|
323
|
+
setOwn(out, node.name, [existing, value]);
|
|
315
324
|
}
|
|
316
325
|
const text = texts.join("").trim();
|
|
317
326
|
if (Object.keys(out).length === 0)
|
|
@@ -333,7 +342,11 @@ function compactElement(el) {
|
|
|
333
342
|
out["#text"] = inner;
|
|
334
343
|
return out;
|
|
335
344
|
}
|
|
336
|
-
Object.assign
|
|
345
|
+
// Not Object.assign: it would call the `__proto__` setter for a child
|
|
346
|
+
// element of that name, instead of copying it.
|
|
347
|
+
for (const k of Object.keys(inner)) {
|
|
348
|
+
setOwn(out, k, inner[k]);
|
|
349
|
+
}
|
|
337
350
|
return out;
|
|
338
351
|
}
|
|
339
352
|
/** All the text under a node, concatenated — the XPath `string()` of it. */
|
package/dist/lib/yaml.d.ts
CHANGED
|
@@ -47,6 +47,13 @@ export declare class YamlError extends Error {
|
|
|
47
47
|
export declare function parseYaml(text: string): unknown;
|
|
48
48
|
/** Find the `:` that separates a key from its value, ignoring quotes and flow markers. */
|
|
49
49
|
export declare function findKeyColon(content: string): number;
|
|
50
|
+
/**
|
|
51
|
+
* `text` without its trailing newlines: what `text.replace(/\n+$/, "")`
|
|
52
|
+
* returned, in one pass from the end. That pattern tries every start inside
|
|
53
|
+
* a run of newlines that is not at the end (blank lines inside a block
|
|
54
|
+
* scalar) and rescans the rest of the run from each, quadratic in its length.
|
|
55
|
+
*/
|
|
56
|
+
export declare function withoutTrailingNewlines(text: string): string;
|
|
50
57
|
/** Remove a trailing ` # comment`, respecting quotes. */
|
|
51
58
|
export declare function stripComment(text: string): string;
|
|
52
59
|
/** Parse a single value: flow collection, quoted string, or plain scalar. */
|
|
@@ -57,5 +64,15 @@ export declare function plainScalar(t: string): unknown;
|
|
|
57
64
|
export declare function parseFlow(text: string, lineNo: number): unknown;
|
|
58
65
|
/** True when a string can be written bare without changing meaning on re-read. */
|
|
59
66
|
export declare function canWritePlain(s: string): boolean;
|
|
60
|
-
/**
|
|
61
|
-
|
|
67
|
+
/**
|
|
68
|
+
* Serialize a JSON value as block-style YAML with a two-space indent.
|
|
69
|
+
*
|
|
70
|
+
* Lines are emitted once, into one list: the 0.7.0 writer returned each
|
|
71
|
+
* nested block as a string and re-sliced it at every level to fold a
|
|
72
|
+
* sequence item's first line onto its dash (`- - x`), which copied the
|
|
73
|
+
* whole block once per level. The folding is now a prefix handed down
|
|
74
|
+
* instead. With `maxChars`, it throws `OutputLimitError` as soon as the text
|
|
75
|
+
* would pass that many characters, before building the rest: indentation
|
|
76
|
+
* makes YAML's size depth x width, not the input's size.
|
|
77
|
+
*/
|
|
78
|
+
export declare function stringifyYaml(value: unknown, indentLevel?: number, maxChars?: number): string;
|
package/dist/lib/yaml.js
CHANGED
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
* strings are written double-quoted with `\n` escapes rather than as block
|
|
40
40
|
* scalars, so the output is unambiguous.
|
|
41
41
|
*/
|
|
42
|
-
import { isPlainObject } from "./json";
|
|
42
|
+
import { OutputLimitError, isPlainObject, setOwn } from "./json";
|
|
43
43
|
export class YamlError extends Error {
|
|
44
44
|
line;
|
|
45
45
|
constructor(message, line) {
|
|
@@ -73,7 +73,11 @@ function tokenize(text) {
|
|
|
73
73
|
throw new YamlError("tab characters cannot be used for indentation", lineNo);
|
|
74
74
|
}
|
|
75
75
|
const indent = indentMatch[0].length;
|
|
76
|
-
|
|
76
|
+
// trimEnd drops exactly what `.replace(/\s+$/, "")` did. That call was
|
|
77
|
+
// linear only because JSC turns that exact spelling into a trim; the same
|
|
78
|
+
// pattern run as a pattern (exec, test, a /g flag) rescans a run of spaces
|
|
79
|
+
// inside the line from each start in it, quadratic in the run's length.
|
|
80
|
+
const content = original.slice(indent).trimEnd();
|
|
77
81
|
if (content.startsWith("#"))
|
|
78
82
|
return;
|
|
79
83
|
if (content === "---")
|
|
@@ -161,15 +165,15 @@ function parseMapping(state, indent, depth) {
|
|
|
161
165
|
const valueText = stripComment(line.content.slice(colon + 1).trim());
|
|
162
166
|
state.i += 1;
|
|
163
167
|
if (valueText === "") {
|
|
164
|
-
out
|
|
168
|
+
setOwn(out, key, parseChildBlock(state, indent, depth));
|
|
165
169
|
continue;
|
|
166
170
|
}
|
|
167
171
|
const block = blockScalarHeader(valueText);
|
|
168
172
|
if (block !== null) {
|
|
169
|
-
out
|
|
173
|
+
setOwn(out, key, readBlockScalar(state, indent, block, line.lineNo));
|
|
170
174
|
continue;
|
|
171
175
|
}
|
|
172
|
-
out
|
|
176
|
+
setOwn(out, key, parseScalarValue(valueText, line.lineNo));
|
|
173
177
|
}
|
|
174
178
|
return out;
|
|
175
179
|
}
|
|
@@ -286,9 +290,21 @@ function readBlockScalar(state, parentIndent, header, lineNo) {
|
|
|
286
290
|
out = body.join("\n");
|
|
287
291
|
}
|
|
288
292
|
if (header.chomp === "strip")
|
|
289
|
-
return out
|
|
293
|
+
return withoutTrailingNewlines(out);
|
|
290
294
|
return `${out}\n`;
|
|
291
295
|
}
|
|
296
|
+
/**
|
|
297
|
+
* `text` without its trailing newlines: what `text.replace(/\n+$/, "")`
|
|
298
|
+
* returned, in one pass from the end. That pattern tries every start inside
|
|
299
|
+
* a run of newlines that is not at the end (blank lines inside a block
|
|
300
|
+
* scalar) and rescans the rest of the run from each, quadratic in its length.
|
|
301
|
+
*/
|
|
302
|
+
export function withoutTrailingNewlines(text) {
|
|
303
|
+
let end = text.length;
|
|
304
|
+
while (end > 0 && text.charCodeAt(end - 1) === 0x0a)
|
|
305
|
+
end--;
|
|
306
|
+
return end === text.length ? text : text.slice(0, end);
|
|
307
|
+
}
|
|
292
308
|
/** Remove a trailing ` # comment`, respecting quotes. */
|
|
293
309
|
export function stripComment(text) {
|
|
294
310
|
let quote = null;
|
|
@@ -471,7 +487,7 @@ function parseFlowNode(s, depth) {
|
|
|
471
487
|
if (s.text[s.i] !== ":")
|
|
472
488
|
throw new YamlError("expected ':' in a flow mapping", s.lineNo);
|
|
473
489
|
s.i += 1;
|
|
474
|
-
out
|
|
490
|
+
setOwn(out, String(coerceFlowKey(key, s.lineNo)), parseFlowNode(s, depth + 1));
|
|
475
491
|
skipFlowSpace(s);
|
|
476
492
|
const d = s.text[s.i];
|
|
477
493
|
if (d === ",") {
|
|
@@ -552,47 +568,77 @@ function writeScalar(value) {
|
|
|
552
568
|
const s = String(value);
|
|
553
569
|
return canWritePlain(s) ? s : JSON.stringify(s);
|
|
554
570
|
}
|
|
555
|
-
/**
|
|
556
|
-
|
|
557
|
-
|
|
571
|
+
/**
|
|
572
|
+
* Serialize a JSON value as block-style YAML with a two-space indent.
|
|
573
|
+
*
|
|
574
|
+
* Lines are emitted once, into one list: the 0.7.0 writer returned each
|
|
575
|
+
* nested block as a string and re-sliced it at every level to fold a
|
|
576
|
+
* sequence item's first line onto its dash (`- - x`), which copied the
|
|
577
|
+
* whole block once per level. The folding is now a prefix handed down
|
|
578
|
+
* instead. With `maxChars`, it throws `OutputLimitError` as soon as the text
|
|
579
|
+
* would pass that many characters, before building the rest: indentation
|
|
580
|
+
* makes YAML's size depth x width, not the input's size.
|
|
581
|
+
*/
|
|
582
|
+
export function stringifyYaml(value, indentLevel = 0, maxChars = Number.POSITIVE_INFINITY) {
|
|
583
|
+
const out = { lines: [], chars: -1, max: maxChars };
|
|
584
|
+
emitYaml(value, indentLevel, null, out);
|
|
585
|
+
return out.lines.join("\n");
|
|
586
|
+
}
|
|
587
|
+
function pushLine(out, line) {
|
|
588
|
+
out.chars += line.length + 1;
|
|
589
|
+
if (out.chars > out.max)
|
|
590
|
+
throw new OutputLimitError(out.max, "the YAML");
|
|
591
|
+
out.lines.push(line);
|
|
592
|
+
}
|
|
593
|
+
const nonEmptyContainer = (v) => (isPlainObject(v) && Object.keys(v).length > 0) || (Array.isArray(v) && v.length > 0);
|
|
594
|
+
/**
|
|
595
|
+
* Emit `value` at `level`. `lead`, when given, replaces the indentation of
|
|
596
|
+
* the FIRST line only: it is how a sequence item's dash lands on the same
|
|
597
|
+
* line as the item's first key or first element. It always has the same
|
|
598
|
+
* length as the indentation it replaces.
|
|
599
|
+
*/
|
|
600
|
+
function emitYaml(value, level, lead, out) {
|
|
601
|
+
const pad = " ".repeat(level);
|
|
602
|
+
let first = true;
|
|
603
|
+
const prefix = () => {
|
|
604
|
+
const p = first && lead !== null ? lead : pad;
|
|
605
|
+
first = false;
|
|
606
|
+
return p;
|
|
607
|
+
};
|
|
558
608
|
if (Array.isArray(value)) {
|
|
559
|
-
if (value.length === 0)
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
}
|
|
571
|
-
return `${pad}- ${writeScalar(el)}`;
|
|
572
|
-
})
|
|
573
|
-
.join("\n");
|
|
609
|
+
if (value.length === 0) {
|
|
610
|
+
pushLine(out, `${prefix()}[]`);
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
for (const el of value) {
|
|
614
|
+
if (nonEmptyContainer(el))
|
|
615
|
+
emitYaml(el, level + 1, `${prefix()}- `, out);
|
|
616
|
+
else
|
|
617
|
+
pushLine(out, `${prefix()}- ${writeScalar(el)}`);
|
|
618
|
+
}
|
|
619
|
+
return;
|
|
574
620
|
}
|
|
575
621
|
if (isPlainObject(value)) {
|
|
576
622
|
const keys = Object.keys(value);
|
|
577
|
-
if (keys.length === 0)
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
623
|
+
if (keys.length === 0) {
|
|
624
|
+
pushLine(out, `${prefix()}{}`);
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
for (const k of keys) {
|
|
581
628
|
const v = value[k];
|
|
582
629
|
const key = canWritePlain(k) ? k : JSON.stringify(k);
|
|
583
|
-
if (Array.isArray(v))
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
630
|
+
if (Array.isArray(v) && v.length === 0)
|
|
631
|
+
pushLine(out, `${prefix()}${key}: []`);
|
|
632
|
+
else if (isPlainObject(v) && Object.keys(v).length === 0)
|
|
633
|
+
pushLine(out, `${prefix()}${key}: {}`);
|
|
634
|
+
else if (Array.isArray(v) || isPlainObject(v)) {
|
|
635
|
+
pushLine(out, `${prefix()}${key}:`);
|
|
636
|
+
emitYaml(v, level + 1, null, out);
|
|
587
637
|
}
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
}
|
|
593
|
-
return `${pad}${key}: ${writeScalar(v)}`;
|
|
594
|
-
})
|
|
595
|
-
.join("\n");
|
|
638
|
+
else
|
|
639
|
+
pushLine(out, `${prefix()}${key}: ${writeScalar(v)}`);
|
|
640
|
+
}
|
|
641
|
+
return;
|
|
596
642
|
}
|
|
597
|
-
|
|
643
|
+
pushLine(out, `${prefix()}${writeScalar(value)}`);
|
|
598
644
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crewhaus/tool-data",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Deterministic structured-data tools: JSON/YAML/TOML/CSV/XML parsing, querying, conversion, diffing and reshaping",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -8,16 +8,18 @@
|
|
|
8
8
|
"exports": {
|
|
9
9
|
".": {
|
|
10
10
|
"types": "./dist/index.d.ts",
|
|
11
|
-
"import": "./dist/index.js"
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"default": "./dist/index.js"
|
|
12
13
|
}
|
|
13
14
|
},
|
|
14
15
|
"scripts": {
|
|
15
16
|
"test": "bun test src"
|
|
16
17
|
},
|
|
17
18
|
"dependencies": {
|
|
18
|
-
"@crewhaus/errors": "0.
|
|
19
|
-
"@crewhaus/tool-builder": "0.
|
|
20
|
-
"@crewhaus/tool-catalog": "0.
|
|
19
|
+
"@crewhaus/errors": "0.8.1",
|
|
20
|
+
"@crewhaus/tool-builder": "0.8.1",
|
|
21
|
+
"@crewhaus/tool-catalog": "0.8.1",
|
|
22
|
+
"@crewhaus/tool-schema": "0.8.1",
|
|
21
23
|
"zod": "^3.23.8"
|
|
22
24
|
},
|
|
23
25
|
"devDependencies": {
|
|
@@ -41,5 +43,8 @@
|
|
|
41
43
|
"publishConfig": {
|
|
42
44
|
"access": "public"
|
|
43
45
|
},
|
|
44
|
-
"files": ["dist", "README.md", "LICENSE", "NOTICE"]
|
|
46
|
+
"files": ["dist", "README.md", "LICENSE", "NOTICE"],
|
|
47
|
+
"engines": {
|
|
48
|
+
"bun": ">=1.3.14"
|
|
49
|
+
}
|
|
45
50
|
}
|