@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.
@@ -7,10 +7,20 @@
7
7
  * two rows compare equal the original order is kept, so the same input gives
8
8
  * the same output every time.
9
9
  */
10
+ import { type RegexAnswers } from "@crewhaus/tool-schema";
10
11
  export type Record_ = Record<string, unknown>;
11
- /** Read a dotted path out of a record. Returns undefined for any miss. */
12
+ /**
13
+ * Read a dotted path out of a record. Returns undefined for any miss. Only
14
+ * the record's own fields are read: `constructor` on a record that has no
15
+ * such field is a miss, not `Object.prototype.constructor`.
16
+ */
12
17
  export declare function getPath(record: unknown, path: string): unknown;
13
- /** Write a dotted path into a record, creating intermediate objects. */
18
+ /**
19
+ * Write a dotted path into a record, creating intermediate objects. Every
20
+ * segment is an own field of the object it names — descent never follows an
21
+ * inherited member, so `__proto__.x` writes a field called `__proto__`, not
22
+ * into `Object.prototype`.
23
+ */
14
24
  export declare function setPath(target: Record_, path: string, value: unknown): void;
15
25
  export type Comparison = "eq" | "ne" | "lt" | "lte" | "gt" | "gte" | "contains" | "startsWith" | "endsWith" | "in" | "matches" | "exists" | "empty";
16
26
  export type Condition = {
@@ -26,8 +36,21 @@ export type Predicate = {
26
36
  };
27
37
  export declare class PredicateError extends Error {
28
38
  }
29
- /** Evaluate one condition against a record. */
30
- export declare function testCondition(record: unknown, cond: Condition): boolean;
39
+ /** A predicate's answer for one record: a verdict, or undetermined (a pattern had no answer). */
40
+ export type PredicateVerdict = boolean | "undetermined";
41
+ /**
42
+ * Ask `regex` about every `matches` condition in `predicate` for every
43
+ * record, so one resolve in the regex worker answers them all before
44
+ * {@link testPredicate} reads them.
45
+ */
46
+ export declare function askPredicatePatterns(records: ReadonlyArray<unknown>, predicate: Predicate, regex: RegexAnswers): void;
47
+ /**
48
+ * Evaluate one condition against a record. A `matches` pattern is never run
49
+ * here: its answer comes from `regex`, resolved in the worker by the tool
50
+ * (or, for a library caller with none, from the bounded fallback on this
51
+ * thread), and one with no answer is `"undetermined"`, never `false`.
52
+ */
53
+ export declare function testCondition(record: unknown, cond: Condition, regex?: RegexAnswers): PredicateVerdict;
31
54
  /**
32
55
  * Order two scalars, or null when they are not comparable. Numbers compare
33
56
  * numerically, strings by code unit, booleans false-before-true; a
@@ -35,8 +58,16 @@ export declare function testCondition(record: unknown, cond: Condition): boolean
35
58
  * quietly being true.
36
59
  */
37
60
  export declare function compareValues(a: unknown, b: unknown): number | null;
38
- /** Evaluate a whole predicate. An empty predicate matches everything. */
39
- export declare function testPredicate(record: unknown, predicate: Predicate): boolean;
61
+ /**
62
+ * Evaluate a whole predicate. An empty predicate matches everything.
63
+ *
64
+ * Three-valued, so a condition with no answer cannot decide a record either
65
+ * way on its own: `all` fails on any definite miss, `none` excludes on any
66
+ * definite hit, `any` holds on any definite hit; otherwise a condition with
67
+ * no answer leaves the record `"undetermined"`. Reading it as a miss would
68
+ * let a `none` exclusion pass the very row it was written to stop.
69
+ */
70
+ export declare function testPredicate(record: unknown, predicate: Predicate, regex?: RegexAnswers): PredicateVerdict;
40
71
  export type SortKey = {
41
72
  field: string;
42
73
  direction?: "asc" | "desc";
@@ -111,11 +142,17 @@ export type JoinOptions = {
111
142
  * how a join quietly loses rows.
112
143
  *
113
144
  * Every right-hand match is emitted, so a one-to-many join multiplies rows,
114
- * as SQL does. `maxRows` bounds that.
145
+ * as SQL does. `maxRows` bounds that — the WORK as well as the output: a row
146
+ * past the cap is counted, never built, so a join on a low-cardinality key
147
+ * (every row `status: "open"`) costs the rows it returns plus one pass over
148
+ * each side, not |left| x |right| merges thrown away. `totalRows` is the size
149
+ * the uncapped join would have had, counted arithmetically, so a caller can
150
+ * tell ten rows cut from eleven apart from ten cut from millions.
115
151
  */
116
152
  export declare function joinRecords(left: ReadonlyArray<Record_>, right: ReadonlyArray<Record_>, options: JoinOptions): {
117
153
  rows: Record_[];
118
154
  truncated: boolean;
155
+ totalRows: number;
119
156
  unmatchedLeft: number;
120
157
  unmatchedRight: number;
121
158
  };
@@ -130,7 +167,7 @@ export declare function columnsToRecords(columns: Record<string, ReadonlyArray<u
130
167
  ragged: string[];
131
168
  };
132
169
  /** Flatten nested objects into dotted keys. Arrays become `a.0`, `a.1` when expanded. */
133
- export declare function flattenObject(value: unknown, separator: string, expandArrays: boolean, maxDepth: number): Record<string, unknown>;
170
+ export declare function flattenObject(value: unknown, separator: string, expandArrays: boolean, maxDepth: number, maxKeyChars?: number): Record<string, unknown>;
134
171
  /**
135
172
  * Rebuild nested objects from dotted keys. A numeric segment becomes an
136
173
  * array index when `arraysFromNumericKeys` is on and the keys under that
package/dist/lib/table.js CHANGED
@@ -7,15 +7,20 @@
7
7
  * two rows compare equal the original order is kept, so the same input gives
8
8
  * the same output every time.
9
9
  */
10
- import { canonicalStringify, deepEqual, isPlainObject } from "./json";
11
- /** Read a dotted path out of a record. Returns undefined for any miss. */
10
+ import { testPatternSync } from "@crewhaus/tool-schema";
11
+ import { OutputLimitError, canonicalStringify, deepEqual, getOwn, isPlainObject, setOwn, } from "./json";
12
+ /**
13
+ * Read a dotted path out of a record. Returns undefined for any miss. Only
14
+ * the record's own fields are read: `constructor` on a record that has no
15
+ * such field is a miss, not `Object.prototype.constructor`.
16
+ */
12
17
  export function getPath(record, path) {
13
18
  if (path === "")
14
19
  return record;
15
20
  let cur = record;
16
21
  for (const seg of path.split(".")) {
17
22
  if (isPlainObject(cur)) {
18
- cur = cur[seg];
23
+ cur = getOwn(cur, seg);
19
24
  continue;
20
25
  }
21
26
  if (Array.isArray(cur) && /^\d+$/.test(seg)) {
@@ -26,28 +31,59 @@ export function getPath(record, path) {
26
31
  }
27
32
  return cur;
28
33
  }
29
- /** Write a dotted path into a record, creating intermediate objects. */
34
+ /**
35
+ * Write a dotted path into a record, creating intermediate objects. Every
36
+ * segment is an own field of the object it names — descent never follows an
37
+ * inherited member, so `__proto__.x` writes a field called `__proto__`, not
38
+ * into `Object.prototype`.
39
+ */
30
40
  export function setPath(target, path, value) {
31
41
  const segs = path.split(".");
32
42
  let cur = target;
33
43
  for (let i = 0; i < segs.length - 1; i++) {
34
44
  const seg = segs[i];
35
- const next = cur[seg];
45
+ const next = getOwn(cur, seg);
36
46
  if (!isPlainObject(next)) {
37
47
  const fresh = {};
38
- cur[seg] = fresh;
48
+ setOwn(cur, seg, fresh);
39
49
  cur = fresh;
40
50
  }
41
51
  else {
42
52
  cur = next;
43
53
  }
44
54
  }
45
- cur[segs[segs.length - 1]] = value;
55
+ setOwn(cur, segs[segs.length - 1], value);
46
56
  }
47
57
  export class PredicateError extends Error {
48
58
  }
49
- /** Evaluate one condition against a record. */
50
- export function testCondition(record, cond) {
59
+ /**
60
+ * Ask `regex` about every `matches` condition in `predicate` for every
61
+ * record, so one resolve in the regex worker answers them all before
62
+ * {@link testPredicate} reads them.
63
+ */
64
+ export function askPredicatePatterns(records, predicate, regex) {
65
+ const conditions = [
66
+ ...(predicate.all ?? []),
67
+ ...(predicate.any ?? []),
68
+ ...(predicate.none ?? []),
69
+ ].filter((c) => c.op === "matches" && typeof c.value === "string");
70
+ if (conditions.length === 0)
71
+ return;
72
+ for (const record of records) {
73
+ for (const cond of conditions) {
74
+ const actual = getPath(record, cond.field);
75
+ if (actual !== undefined && actual !== null)
76
+ regex.lookup(cond.value, "", String(actual));
77
+ }
78
+ }
79
+ }
80
+ /**
81
+ * Evaluate one condition against a record. A `matches` pattern is never run
82
+ * here: its answer comes from `regex`, resolved in the worker by the tool
83
+ * (or, for a library caller with none, from the bounded fallback on this
84
+ * thread), and one with no answer is `"undetermined"`, never `false`.
85
+ */
86
+ export function testCondition(record, cond, regex) {
51
87
  const actual = getPath(record, cond.field);
52
88
  const expected = cond.value;
53
89
  switch (cond.op) {
@@ -78,14 +114,25 @@ export function testCondition(record, cond) {
78
114
  if (typeof expected !== "string") {
79
115
  throw new PredicateError(`"matches" needs a regular-expression string for ${cond.field}`);
80
116
  }
81
- let re;
82
117
  try {
83
- re = new RegExp(expected);
118
+ // Compiling reads only the pattern; the match is what is not run here.
119
+ new RegExp(expected);
84
120
  }
85
121
  catch (err) {
86
122
  throw new PredicateError(`invalid regex for ${cond.field}: ${err.message}`);
87
123
  }
88
- return actual !== undefined && actual !== null && re.test(String(actual));
124
+ if (actual === undefined || actual === null)
125
+ return false;
126
+ const subject = String(actual);
127
+ const answer = regex === undefined
128
+ ? testPatternSync(expected, "", subject)
129
+ : regex.lookup(expected, "", subject);
130
+ if (typeof answer === "boolean")
131
+ return answer;
132
+ if (answer !== undefined && "refused" in answer) {
133
+ throw new PredicateError(`invalid regex for ${cond.field}: ${answer.refused}`);
134
+ }
135
+ return "undetermined";
89
136
  }
90
137
  default:
91
138
  return compareOrdered(actual, expected, cond.op);
@@ -120,20 +167,52 @@ export function compareValues(a, b) {
120
167
  return a === b ? 0 : a ? 1 : -1;
121
168
  return null;
122
169
  }
123
- /** Evaluate a whole predicate. An empty predicate matches everything. */
124
- export function testPredicate(record, predicate) {
170
+ /**
171
+ * Evaluate a whole predicate. An empty predicate matches everything.
172
+ *
173
+ * Three-valued, so a condition with no answer cannot decide a record either
174
+ * way on its own: `all` fails on any definite miss, `none` excludes on any
175
+ * definite hit, `any` holds on any definite hit; otherwise a condition with
176
+ * no answer leaves the record `"undetermined"`. Reading it as a miss would
177
+ * let a `none` exclusion pass the very row it was written to stop.
178
+ */
179
+ export function testPredicate(record, predicate, regex) {
125
180
  const all = predicate.all ?? [];
126
181
  const any = predicate.any ?? [];
127
182
  const none = predicate.none ?? [];
128
- for (const c of all)
129
- if (!testCondition(record, c))
183
+ let open = false;
184
+ for (const c of all) {
185
+ const v = testCondition(record, c, regex);
186
+ if (v === false)
130
187
  return false;
131
- for (const c of none)
132
- if (testCondition(record, c))
188
+ if (v === "undetermined")
189
+ open = true;
190
+ }
191
+ for (const c of none) {
192
+ const v = testCondition(record, c, regex);
193
+ if (v === true)
133
194
  return false;
134
- if (any.length > 0 && !any.some((c) => testCondition(record, c)))
135
- return false;
136
- return true;
195
+ if (v === "undetermined")
196
+ open = true;
197
+ }
198
+ if (any.length > 0) {
199
+ let hit = false;
200
+ let anyOpen = false;
201
+ for (const c of any) {
202
+ const v = testCondition(record, c, regex);
203
+ if (v === true) {
204
+ hit = true;
205
+ break;
206
+ }
207
+ if (v === "undetermined")
208
+ anyOpen = true;
209
+ }
210
+ if (!hit && !anyOpen)
211
+ return false;
212
+ if (!hit)
213
+ open = true;
214
+ }
215
+ return open ? "undetermined" : true;
137
216
  }
138
217
  /**
139
218
  * Sort records by one or more keys. The sort is stable and total: values of
@@ -243,10 +322,11 @@ export function omitFields(record, fields) {
243
322
  for (let i = 0; i < segs.length - 1; i++) {
244
323
  if (!isPlainObject(cur))
245
324
  break;
246
- cur = cur[segs[i]];
325
+ cur = getOwn(cur, segs[i]);
247
326
  }
248
- if (isPlainObject(cur))
249
- delete cur[segs[segs.length - 1]];
327
+ const last = segs[segs.length - 1];
328
+ if (isPlainObject(cur) && Object.hasOwn(cur, last))
329
+ delete cur[last];
250
330
  }
251
331
  return out;
252
332
  }
@@ -254,7 +334,7 @@ function structuredCloneish(value) {
254
334
  const out = {};
255
335
  for (const k of Object.keys(value)) {
256
336
  const v = value[k];
257
- out[k] = isPlainObject(v) ? structuredCloneish(v) : v;
337
+ setOwn(out, k, isPlainObject(v) ? structuredCloneish(v) : v);
258
338
  }
259
339
  return out;
260
340
  }
@@ -271,7 +351,7 @@ export function aggregate(records, groupBy, aggregations) {
271
351
  for (const rec of records) {
272
352
  const key = {};
273
353
  for (const field of groupBy)
274
- key[field] = getPath(rec, field) ?? null;
354
+ setOwn(key, field, getPath(rec, field) ?? null);
275
355
  const id = canonicalStringify(key);
276
356
  const existing = buckets.get(id);
277
357
  if (existing === undefined)
@@ -288,37 +368,37 @@ export function aggregate(records, groupBy, aggregations) {
288
368
  const values = bucket.rows.map((r) => getPath(r, field));
289
369
  switch (agg.fn) {
290
370
  case "count":
291
- out[agg.as] = field === "" ? bucket.rows.length : values.filter(isPresent).length;
371
+ setOwn(out, agg.as, field === "" ? bucket.rows.length : values.filter(isPresent).length);
292
372
  break;
293
373
  case "sum":
294
374
  case "avg": {
295
375
  const nums = values.filter((v) => typeof v === "number");
296
376
  skipped += values.filter(isPresent).length - nums.length;
297
377
  const total = nums.reduce((a, b) => a + b, 0);
298
- out[agg.as] = agg.fn === "sum" ? total : nums.length === 0 ? null : total / nums.length;
378
+ setOwn(out, agg.as, agg.fn === "sum" ? total : nums.length === 0 ? null : total / nums.length);
299
379
  break;
300
380
  }
301
381
  case "min":
302
382
  case "max": {
303
383
  const present = values.filter(isPresent);
304
384
  if (present.length === 0) {
305
- out[agg.as] = null;
385
+ setOwn(out, agg.as, null);
306
386
  break;
307
387
  }
308
- out[agg.as] = present.reduce((best, v) => {
388
+ setOwn(out, agg.as, present.reduce((best, v) => {
309
389
  const cmp = totalCompare(v, best);
310
390
  return (agg.fn === "min" ? cmp < 0 : cmp > 0) ? v : best;
311
- });
391
+ }));
312
392
  break;
313
393
  }
314
394
  case "first":
315
- out[agg.as] = values[0] ?? null;
395
+ setOwn(out, agg.as, values[0] ?? null);
316
396
  break;
317
397
  case "last":
318
- out[agg.as] = values[values.length - 1] ?? null;
398
+ setOwn(out, agg.as, values[values.length - 1] ?? null);
319
399
  break;
320
400
  case "distinct":
321
- out[agg.as] = new Set(values.filter(isPresent).map((v) => canonicalStringify(v))).size;
401
+ setOwn(out, agg.as, new Set(values.filter(isPresent).map((v) => canonicalStringify(v))).size);
322
402
  break;
323
403
  default:
324
404
  break;
@@ -337,7 +417,12 @@ function isPresent(v) {
337
417
  * how a join quietly loses rows.
338
418
  *
339
419
  * Every right-hand match is emitted, so a one-to-many join multiplies rows,
340
- * as SQL does. `maxRows` bounds that.
420
+ * as SQL does. `maxRows` bounds that — the WORK as well as the output: a row
421
+ * past the cap is counted, never built, so a join on a low-cardinality key
422
+ * (every row `status: "open"`) costs the rows it returns plus one pass over
423
+ * each side, not |left| x |right| merges thrown away. `totalRows` is the size
424
+ * the uncapped join would have had, counted arithmetically, so a caller can
425
+ * tell ten rows cut from eleven apart from ten cut from millions.
341
426
  */
342
427
  export function joinRecords(left, right, options) {
343
428
  const index = new Map();
@@ -355,13 +440,14 @@ export function joinRecords(left, right, options) {
355
440
  const rows = [];
356
441
  const matchedRight = new Set();
357
442
  let unmatchedLeft = 0;
358
- let truncated = false;
359
- const push = (row) => {
360
- if (rows.length >= options.maxRows) {
361
- truncated = true;
362
- return;
363
- }
364
- rows.push(row);
443
+ let totalRows = 0;
444
+ // The cap is checked BEFORE a row is built. Building it first and then
445
+ // discarding it (the 0.7.0 shape) made maxRows bound the answer but not
446
+ // the work: every matching pair was merged whatever the cap said.
447
+ const keep = (build) => {
448
+ totalRows += 1;
449
+ if (rows.length < options.maxRows)
450
+ rows.push(build());
365
451
  };
366
452
  for (const l of left) {
367
453
  const key = getPath(l, options.leftKey);
@@ -370,13 +456,16 @@ export function joinRecords(left, right, options) {
370
456
  if (matches === undefined || matches.length === 0) {
371
457
  unmatchedLeft += 1;
372
458
  if (options.kind === "left" || options.kind === "full")
373
- push({ ...l });
459
+ keep(() => ({ ...l }));
374
460
  continue;
375
461
  }
376
462
  if (id !== null)
377
463
  matchedRight.add(id);
378
- for (const r of matches)
379
- push(mergeRow(l, r, options.rightPrefix));
464
+ const take = Math.min(Math.max(options.maxRows - rows.length, 0), matches.length);
465
+ for (let i = 0; i < take; i++) {
466
+ rows.push(mergeRow(l, matches[i], options.rightPrefix));
467
+ }
468
+ totalRows += matches.length;
380
469
  }
381
470
  let unmatchedRight = 0;
382
471
  for (const r of right) {
@@ -385,15 +474,21 @@ export function joinRecords(left, right, options) {
385
474
  if (id === null || !matchedRight.has(id)) {
386
475
  unmatchedRight += 1;
387
476
  if (options.kind === "right" || options.kind === "full")
388
- push({ ...r });
477
+ keep(() => ({ ...r }));
389
478
  }
390
479
  }
391
- return { rows, truncated, unmatchedLeft, unmatchedRight };
480
+ return {
481
+ rows,
482
+ truncated: totalRows > rows.length,
483
+ totalRows,
484
+ unmatchedLeft,
485
+ unmatchedRight,
486
+ };
392
487
  }
393
488
  function mergeRow(left, right, prefix) {
394
489
  const out = { ...left };
395
490
  for (const k of Object.keys(right)) {
396
- out[Object.hasOwn(left, k) ? `${prefix}${k}` : k] = right[k];
491
+ setOwn(out, Object.hasOwn(left, k) ? `${prefix}${k}` : k, right[k]);
397
492
  }
398
493
  return out;
399
494
  }
@@ -413,7 +508,7 @@ export function recordsToColumns(records) {
413
508
  }
414
509
  const out = {};
415
510
  for (const k of keys)
416
- out[k] = records.map((r) => (Object.hasOwn(r, k) ? r[k] : null));
511
+ setOwn(out, k, records.map((r) => (Object.hasOwn(r, k) ? r[k] : null)));
417
512
  return out;
418
513
  }
419
514
  /**
@@ -430,25 +525,35 @@ export function columnsToRecords(columns) {
430
525
  const rec = {};
431
526
  for (const k of keys) {
432
527
  const col = columns[k] ?? [];
433
- rec[k] = i < col.length ? col[i] : null;
528
+ setOwn(rec, k, i < col.length ? col[i] : null);
434
529
  }
435
530
  records.push(rec);
436
531
  }
437
532
  return { records, ragged };
438
533
  }
439
534
  /** Flatten nested objects into dotted keys. Arrays become `a.0`, `a.1` when expanded. */
440
- export function flattenObject(value, separator, expandArrays, maxDepth) {
535
+ export function flattenObject(value, separator, expandArrays, maxDepth, maxKeyChars = Number.POSITIVE_INFINITY) {
441
536
  const out = {};
537
+ // Every flattened key spells its whole path, so the keys alone are
538
+ // depth x leaves characters; `maxKeyChars` stops the walk (throwing
539
+ // OutputLimitError) before they are all built.
540
+ let keyChars = 0;
541
+ const put = (key, v) => {
542
+ keyChars += key.length;
543
+ if (keyChars > maxKeyChars)
544
+ throw new OutputLimitError(maxKeyChars, "the flattened keys");
545
+ setOwn(out, key, v);
546
+ };
442
547
  const walk = (node, prefix, depth) => {
443
548
  if (depth >= maxDepth) {
444
- out[prefix] = node;
549
+ put(prefix, node);
445
550
  return;
446
551
  }
447
552
  if (isPlainObject(node)) {
448
553
  const keys = Object.keys(node);
449
554
  if (keys.length === 0) {
450
555
  if (prefix !== "")
451
- out[prefix] = {};
556
+ put(prefix, {});
452
557
  return;
453
558
  }
454
559
  for (const k of keys)
@@ -458,13 +563,13 @@ export function flattenObject(value, separator, expandArrays, maxDepth) {
458
563
  if (Array.isArray(node) && expandArrays) {
459
564
  if (node.length === 0) {
460
565
  if (prefix !== "")
461
- out[prefix] = [];
566
+ put(prefix, []);
462
567
  return;
463
568
  }
464
569
  node.forEach((el, i) => walk(el, prefix === "" ? String(i) : `${prefix}${separator}${i}`, depth + 1));
465
570
  return;
466
571
  }
467
- out[prefix] = node;
572
+ put(prefix, node);
468
573
  };
469
574
  walk(value, "", 0);
470
575
  return out;
@@ -482,17 +587,17 @@ export function unflattenObject(flat, separator, arraysFromNumericKeys) {
482
587
  let cur = root;
483
588
  for (let i = 0; i < segs.length - 1; i++) {
484
589
  const seg = segs[i];
485
- const next = cur[seg];
590
+ const next = getOwn(cur, seg);
486
591
  if (!isPlainObject(next)) {
487
592
  const fresh = {};
488
- cur[seg] = fresh;
593
+ setOwn(cur, seg, fresh);
489
594
  cur = fresh;
490
595
  }
491
596
  else {
492
597
  cur = next;
493
598
  }
494
599
  }
495
- cur[segs[segs.length - 1]] = flat[key];
600
+ setOwn(cur, segs[segs.length - 1], flat[key]);
496
601
  }
497
602
  return arraysFromNumericKeys ? arrayify(root) : root;
498
603
  }
@@ -502,7 +607,7 @@ function arrayify(node) {
502
607
  const keys = Object.keys(node);
503
608
  const converted = {};
504
609
  for (const k of keys)
505
- converted[k] = arrayify(node[k]);
610
+ setOwn(converted, k, arrayify(node[k]));
506
611
  if (keys.length === 0)
507
612
  return converted;
508
613
  const allNumeric = keys.every((k) => /^(0|[1-9][0-9]*)$/.test(k));
@@ -43,6 +43,13 @@ export declare class TomlError extends Error {
43
43
  readonly line: number;
44
44
  constructor(message: string, line: number);
45
45
  }
46
+ /**
47
+ * A key or table path with more parts than MAX_NESTING_DEPTH, refused as it
48
+ * is read: the value it builds would nest past the cap and be refused
49
+ * anyway, and building it first cost the square of the path's length.
50
+ */
51
+ export declare class TomlTooDeepError extends TomlError {
52
+ }
46
53
  /** Parse a TOML document into JSON values. Throws `TomlError` with a line number. */
47
54
  export declare function parseToml(text: string): Record<string, unknown>;
48
55
  /** Type a bare TOML token. Exported so the tests can pin the exact rules. */
@@ -56,4 +63,4 @@ export type TomlWriteResult = {
56
63
  * keys that were dropped because TOML has no representation for them
57
64
  * (`null` and `undefined`).
58
65
  */
59
- export declare function stringifyToml(value: unknown): TomlWriteResult;
66
+ export declare function stringifyToml(value: unknown, maxChars?: number): TomlWriteResult;