@crewhaus/tool-data 0.7.0 → 0.8.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/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
- const defined = new Set();
58
- const dotted = new Set();
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
- if (defined.has(key)) {
76
- throw new TomlError(`table [${key}] is defined twice`, cur.line);
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(key)) {
79
- throw new TomlError(`table [${key}] was already created by a dotted key, so it cannot be defined again`, cur.line);
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(key);
82
- table = descend(root, path, cur);
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[seg];
180
+ const existing = getOwn(node, seg);
167
181
  if (existing === undefined) {
168
182
  const fresh = {};
169
- node[seg] = fresh;
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[key];
205
+ const existing = getOwn(parent, key);
192
206
  const fresh = {};
193
207
  if (existing === undefined) {
194
- parent[key] = [fresh];
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
- 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];
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
- const fresh = {};
226
- node[seg] = fresh;
227
- node = fresh;
228
- return;
234
+ next = {};
235
+ setOwn(node, seg, next);
229
236
  }
230
- if (!isPlainObject(existing)) {
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
- node = existing;
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[key] = value;
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 skipped = [];
485
- const lines = [];
486
- emitTable(value, [], lines, skipped);
487
- return { text: lines.join("\n"), skipped };
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, lines, skipped, headerAlreadyWritten = false) {
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
- skipped.push([...path, key].join("."));
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
- lines.push("");
515
- lines.push(`[${path.map(writeKey).join(".")}]`);
540
+ if (out.lines.length > 0)
541
+ emitLine(out, "");
542
+ emitLine(out, `[${path.map(writeKey).join(".")}]`);
516
543
  }
517
- lines.push(...scalars);
544
+ for (const line of scalars)
545
+ emitLine(out, line);
518
546
  for (const [key, sub] of subTables) {
519
- emitTable(sub, [...path, key], lines, skipped);
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
- lines.push("");
526
- lines.push(`[[${childPath.map(writeKey).join(".")}]]`);
527
- emitTable(element, childPath, lines, skipped, true);
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[attrName] = "";
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[attrName] = decodeEntities(src.slice(i + 1, end), line);
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[attrName] = decodeEntities(src.slice(valueStart, i), line);
235
+ setOwn(attributes, attrName, decodeEntities(src.slice(valueStart, i), line));
235
236
  }
236
237
  }
237
- const NAMED_ENTITIES = {
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
- const existing = out[node.name];
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[node.name] = value;
319
+ setOwn(out, node.name, value);
311
320
  else if (Array.isArray(existing))
312
321
  existing.push(value);
313
322
  else
314
- out[node.name] = [existing, value];
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(out, inner);
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. */
@@ -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
- /** Serialize a JSON value as block-style YAML with a two-space indent. */
61
- export declare function stringifyYaml(value: unknown, indentLevel?: number): string;
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
- const content = original.slice(indent).replace(/\s+$/, "");
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[key] = parseChildBlock(state, indent, depth);
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[key] = readBlockScalar(state, indent, block, line.lineNo);
173
+ setOwn(out, key, readBlockScalar(state, indent, block, line.lineNo));
170
174
  continue;
171
175
  }
172
- out[key] = parseScalarValue(valueText, line.lineNo);
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.replace(/\n+$/, "");
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[String(coerceFlowKey(key, s.lineNo))] = parseFlowNode(s, depth + 1);
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
- /** 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);
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
- 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");
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
- return `${pad}{}`;
579
- return keys
580
- .map((k) => {
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
- if (v.length === 0)
585
- return `${pad}${key}: []`;
586
- return `${pad}${key}:\n${stringifyYaml(v, indentLevel + 1)}`;
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
- 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");
638
+ else
639
+ pushLine(out, `${prefix()}${key}: ${writeScalar(v)}`);
640
+ }
641
+ return;
596
642
  }
597
- return `${pad}${writeScalar(value)}`;
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.7.0",
3
+ "version": "0.8.0",
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.7.0",
19
- "@crewhaus/tool-builder": "0.7.0",
20
- "@crewhaus/tool-catalog": "0.7.0",
19
+ "@crewhaus/errors": "0.8.0",
20
+ "@crewhaus/tool-builder": "0.8.0",
21
+ "@crewhaus/tool-catalog": "0.8.0",
22
+ "@crewhaus/tool-schema": "0.8.0",
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
  }