smol-toml 1.3.4 → 1.4.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 CHANGED
@@ -16,6 +16,9 @@ parser didn't feel too out of place.
16
16
 
17
17
  *[insert xkcd 927]*
18
18
 
19
+ Nowadays, smol-toml is the most downloaded TOML parser on npm thanks to its quality. From frameworks to tooling, it
20
+ has been battle-tested and is actively used in production systems.
21
+
19
22
  smol-toml passes most of the tests from the [`toml-test` suite](https://github.com/toml-lang/toml-test); use the
20
23
  `run-toml-test.bash` script to run the tests. Due to the nature of JavaScript and the limits of the language,
21
24
  it doesn't pass certain tests, namely:
@@ -24,7 +27,9 @@ it doesn't pass certain tests, namely:
24
27
  - Certain invalid dates are not rejected
25
28
  - For instance, `2023-02-30` would be accepted and parsed as `2023-03-02`. While additional checks could be performed
26
29
  to reject these, they've not been added for performance reasons.
27
- - smol-toml doesn't preserve type information between integers and floats (in JS, everything is a float)
30
+
31
+ Please also note that by default, the behavior regarding integers doesn't preserve type information, nor does it allow
32
+ deserializing integers larger than 53 bits. See [Integers](#integers).
28
33
 
29
34
  You can see a list of all tests smol-toml fails (and the reason why it fails these) in the list of skipped tests in
30
35
  `run-toml-test.bash`. Note that some failures are *not* specification violations per-se. For instance, the TOML spec
@@ -59,11 +64,42 @@ A few notes on the `stringify` function:
59
64
  - `undefined` and `null` values on objects are ignored (does not produce a key/value).
60
65
  - `undefined` and `null` values in arrays are **rejected**.
61
66
  - Functions, classes and symbols are **rejected**.
62
- - floats will be serialized as integers if they don't have a decimal part.
67
+ - By default, floats will be serialized as integers if they don't have a decimal part. See [Integers](#integers)
63
68
  - `stringify(parse('a = 1.0')) === 'a = 1'`
64
69
  - JS `Date` will be serialized as Offset Date Time
65
70
  - Use the [`TomlDate` object](#dates) for representing other types.
66
71
 
72
+ ### Integers
73
+ When parsing, both integers and floats are read as plain JavaScript numbers, which essentially are floats. This means
74
+ loss of type information, and makes it impossible to safely represent integers beyond 53 bits.
75
+
76
+ When serializing, numbers without a decimal part are serialized as integers. This allows in most cases to preserve
77
+ whether a number is an integer or not, but fails to preserve type information for numbers like `1.0`.
78
+
79
+ #### Enabling BigInt support and type preservation
80
+ To parse integers beyond 53 bits, it's possible to tell the parser to return all integers as BigInt. This will
81
+ therefore preserve the type information at the cost of using a slightly more expensive container.
82
+
83
+ ```js
84
+ import { parse } from 'smol-toml'
85
+
86
+ const doc = '...'
87
+ const parsed = parse(doc, { integersAsBigInt: true })
88
+ ```
89
+
90
+ If you want to keep numbers for integers that can safely be represented as a JavaScript number, you can pass
91
+ `"asNeeded"` instead.
92
+
93
+ To get end-to-end type preservation, you can tell the serializer to always treat numbers as floating point numbers.
94
+ Then, only BigInts will be serialized as integers and numbers without a decimal part will still be serialized as float.
95
+
96
+ ```js
97
+ import { stringify } from 'smol-toml'
98
+
99
+ const obj = { ... }
100
+ const toml = stringify(obj, { numbersAsFloat: true })
101
+ ```
102
+
67
103
  ### Dates
68
104
  `smol-toml` uses an extended `Date` object to represent all types of TOML Dates. In the future, `smol-toml` will use
69
105
  objects from the Temporal proposal, but for now we're stuck with the legacy Date object.
@@ -107,6 +143,10 @@ const localTime = TomlDate.wrapAsLocalTime(jsDate)
107
143
  ```
108
144
 
109
145
  ## Performance
146
+ > [!NOTE]
147
+ > These benchmarks are starting to get a bit old. They will be updated in the (hopefully near) future to better
148
+ > reflect numbers of the latest version of smol-toml on the latest version of Node.js.
149
+
110
150
  A note on these performance numbers: in some highly synthetic tests, other parsers such as `fast-toml` greatly
111
151
  outperform other parsers, mostly due to their lack of compliance with the spec. For example, to parse a string,
112
152
  `fast-toml` skips the entire string while `smol-toml` does validate the string, costing a fair share of performance.
@@ -187,7 +227,7 @@ I initially tried to benchmark `toml-nodejs`, but the 0.3.0 package is broken.
187
227
  I initially reported this to the library author, but the author decided to
188
228
  - a) advise to use a custom loader (via *experimental* flag) to circumvent the invalid imports.
189
229
  - Said flag, `--experimental-specifier-resolution`, has been removed in Node v20.
190
- - b) [delete the issue](https://github.com/huan231/toml-nodejs/issues/12) when pointed out links to the NodeJS
230
+ - b) [delete the issue](https://github.com/huan231/toml-nodejs/issues/12) when pointed out links to the Node.js
191
231
  documentation about the flag removal and standard resolution algorithm.
192
232
 
193
233
  For the reference anyway, `toml-nodejs` (with proper imports) is ~8x slower on both parse benchmark with:
package/dist/extract.d.ts CHANGED
@@ -25,5 +25,6 @@
25
25
  * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
26
  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
27
  */
28
- import { type TomlPrimitive } from './util.js';
29
- export declare function extractValue(str: string, ptr: number, end?: string | undefined, depth?: number): [TomlPrimitive, number];
28
+ import { type IntegersAsBigInt } from './primitive.js';
29
+ import { type TomlValue } from './util.js';
30
+ export declare function extractValue(str: string, ptr: number, end: string | undefined, depth: number, integersAsBigInt: IntegersAsBigInt): [TomlValue, number];
package/dist/extract.js CHANGED
@@ -50,7 +50,7 @@ function sliceAndTrimEndOf(str, startPtr, endPtr, allowNewLines) {
50
50
  }
51
51
  return [trimmed, commentIdx];
52
52
  }
53
- export function extractValue(str, ptr, end, depth = -1) {
53
+ export function extractValue(str, ptr, end, depth, integersAsBigInt) {
54
54
  if (depth === 0) {
55
55
  throw new TomlError('document contains excessively nested structures. aborting.', {
56
56
  toml: str,
@@ -60,8 +60,8 @@ export function extractValue(str, ptr, end, depth = -1) {
60
60
  let c = str[ptr];
61
61
  if (c === '[' || c === '{') {
62
62
  let [value, endPtr] = c === '['
63
- ? parseArray(str, ptr, depth)
64
- : parseInlineTable(str, ptr, depth);
63
+ ? parseArray(str, ptr, depth, integersAsBigInt)
64
+ : parseInlineTable(str, ptr, depth, integersAsBigInt);
65
65
  let newPtr = end ? skipUntil(str, endPtr, ',', end) : endPtr;
66
66
  if (endPtr - newPtr && end === '}') {
67
67
  let nextNewLine = indexOfNewline(str, endPtr, newPtr);
@@ -103,7 +103,7 @@ export function extractValue(str, ptr, end, depth = -1) {
103
103
  endPtr += +(str[endPtr] === ',');
104
104
  }
105
105
  return [
106
- parseValue(slice[0], str, ptr),
106
+ parseValue(slice[0], str, ptr, integersAsBigInt),
107
107
  endPtr,
108
108
  ];
109
109
  }
package/dist/index.cjs CHANGED
@@ -315,7 +315,7 @@ function parseString(str, ptr = 0, endPtr = str.length) {
315
315
  }
316
316
  return parsed + str.slice(sliceStart, endPtr - 1);
317
317
  }
318
- function parseValue(value, toml, ptr) {
318
+ function parseValue(value, toml, ptr, integersAsBigInt) {
319
319
  if (value === "true")
320
320
  return true;
321
321
  if (value === "false")
@@ -327,31 +327,36 @@ function parseValue(value, toml, ptr) {
327
327
  if (value === "nan" || value === "+nan" || value === "-nan")
328
328
  return NaN;
329
329
  if (value === "-0")
330
- return 0;
331
- let isInt;
332
- if ((isInt = INT_REGEX.test(value)) || FLOAT_REGEX.test(value)) {
330
+ return integersAsBigInt ? 0n : 0;
331
+ let isInt = INT_REGEX.test(value);
332
+ if (isInt || FLOAT_REGEX.test(value)) {
333
333
  if (LEADING_ZERO.test(value)) {
334
334
  throw new TomlError("leading zeroes are not allowed", {
335
335
  toml,
336
336
  ptr
337
337
  });
338
338
  }
339
- let numeric = +value.replace(/_/g, "");
339
+ value = value.replace(/_/g, "");
340
+ let numeric = +value;
340
341
  if (isNaN(numeric)) {
341
342
  throw new TomlError("invalid number", {
342
343
  toml,
343
344
  ptr
344
345
  });
345
346
  }
346
- if (isInt && !Number.isSafeInteger(numeric)) {
347
- throw new TomlError("integer value cannot be represented losslessly", {
348
- toml,
349
- ptr
350
- });
347
+ if (isInt) {
348
+ if ((isInt = !Number.isSafeInteger(numeric)) && !integersAsBigInt) {
349
+ throw new TomlError("integer value cannot be represented losslessly", {
350
+ toml,
351
+ ptr
352
+ });
353
+ }
354
+ if (isInt || integersAsBigInt)
355
+ numeric = BigInt(value);
351
356
  }
352
357
  return numeric;
353
358
  }
354
- let date = new TomlDate(value);
359
+ const date = new TomlDate(value);
355
360
  if (!date.isValid()) {
356
361
  throw new TomlError("invalid value", {
357
362
  toml,
@@ -381,7 +386,7 @@ function sliceAndTrimEndOf(str, startPtr, endPtr, allowNewLines) {
381
386
  }
382
387
  return [trimmed, commentIdx];
383
388
  }
384
- function extractValue(str, ptr, end, depth = -1) {
389
+ function extractValue(str, ptr, end, depth, integersAsBigInt) {
385
390
  if (depth === 0) {
386
391
  throw new TomlError("document contains excessively nested structures. aborting.", {
387
392
  toml: str,
@@ -390,7 +395,7 @@ function extractValue(str, ptr, end, depth = -1) {
390
395
  }
391
396
  let c = str[ptr];
392
397
  if (c === "[" || c === "{") {
393
- let [value, endPtr2] = c === "[" ? parseArray(str, ptr, depth) : parseInlineTable(str, ptr, depth);
398
+ let [value, endPtr2] = c === "[" ? parseArray(str, ptr, depth, integersAsBigInt) : parseInlineTable(str, ptr, depth, integersAsBigInt);
394
399
  let newPtr = end ? skipUntil(str, endPtr2, ",", end) : endPtr2;
395
400
  if (endPtr2 - newPtr && end === "}") {
396
401
  let nextNewLine = indexOfNewline(str, endPtr2, newPtr);
@@ -432,7 +437,7 @@ function extractValue(str, ptr, end, depth = -1) {
432
437
  endPtr += +(str[endPtr] === ",");
433
438
  }
434
439
  return [
435
- parseValue(slice[0], str, ptr),
440
+ parseValue(slice[0], str, ptr, integersAsBigInt),
436
441
  endPtr
437
442
  ];
438
443
  }
@@ -506,28 +511,20 @@ function parseKey(str, ptr, end = "=") {
506
511
  } while (dot + 1 && dot < endPtr);
507
512
  return [parsed, skipVoid(str, endPtr + 1, true, true)];
508
513
  }
509
- function parseInlineTable(str, ptr, depth = -1) {
514
+ function parseInlineTable(str, ptr, depth, integersAsBigInt) {
510
515
  let res = {};
511
516
  let seen = /* @__PURE__ */ new Set();
512
517
  let c;
513
518
  let comma = 0;
514
519
  ptr++;
515
520
  while ((c = str[ptr++]) !== "}" && c) {
521
+ let err = { toml: str, ptr: ptr - 1 };
516
522
  if (c === "\n") {
517
- throw new TomlError("newlines are not allowed in inline tables", {
518
- toml: str,
519
- ptr: ptr - 1
520
- });
523
+ throw new TomlError("newlines are not allowed in inline tables", err);
521
524
  } else if (c === "#") {
522
- throw new TomlError("inline tables cannot contain comments", {
523
- toml: str,
524
- ptr: ptr - 1
525
- });
525
+ throw new TomlError("inline tables cannot contain comments", err);
526
526
  } else if (c === ",") {
527
- throw new TomlError("expected key-value, found comma", {
528
- toml: str,
529
- ptr: ptr - 1
530
- });
527
+ throw new TomlError("expected key-value, found comma", err);
531
528
  } else if (c !== " " && c !== " ") {
532
529
  let k;
533
530
  let t = res;
@@ -553,7 +550,7 @@ function parseInlineTable(str, ptr, depth = -1) {
553
550
  ptr
554
551
  });
555
552
  }
556
- let [value, valueEndPtr] = extractValue(str, keyEndPtr, "}", depth - 1);
553
+ let [value, valueEndPtr] = extractValue(str, keyEndPtr, "}", depth - 1, integersAsBigInt);
557
554
  seen.add(value);
558
555
  t[k] = value;
559
556
  ptr = valueEndPtr;
@@ -574,7 +571,7 @@ function parseInlineTable(str, ptr, depth = -1) {
574
571
  }
575
572
  return [res, ptr];
576
573
  }
577
- function parseArray(str, ptr, depth = -1) {
574
+ function parseArray(str, ptr, depth, integersAsBigInt) {
578
575
  let res = [];
579
576
  let c;
580
577
  ptr++;
@@ -587,7 +584,7 @@ function parseArray(str, ptr, depth = -1) {
587
584
  } else if (c === "#")
588
585
  ptr = skipComment(str, ptr);
589
586
  else if (c !== " " && c !== " " && c !== "\n" && c !== "\r") {
590
- let e = extractValue(str, ptr - 1, "]", depth - 1);
587
+ let e = extractValue(str, ptr - 1, "]", depth - 1, integersAsBigInt);
591
588
  res.push(e[0]);
592
589
  ptr = e[1];
593
590
  }
@@ -661,8 +658,7 @@ function peekTable(key, table, meta, type) {
661
658
  }
662
659
  return [k, t, state.c];
663
660
  }
664
- function parse(toml, opts) {
665
- let maxDepth = opts?.maxDepth ?? 1e3;
661
+ function parse(toml, { maxDepth = 1e3, integersAsBigInt } = {}) {
666
662
  let res = {};
667
663
  let meta = {};
668
664
  let tbl = res;
@@ -711,7 +707,7 @@ function parse(toml, opts) {
711
707
  ptr
712
708
  });
713
709
  }
714
- let v = extractValue(toml, k[1], void 0, maxDepth);
710
+ let v = extractValue(toml, k[1], void 0, maxDepth, integersAsBigInt);
715
711
  p[1][p[0]] = v[0];
716
712
  ptr = v[1];
717
713
  }
@@ -749,7 +745,7 @@ function isArrayOfTables(obj) {
749
745
  function formatString(s) {
750
746
  return JSON.stringify(s).replace(/\x7f/g, "\\u007f");
751
747
  }
752
- function stringifyValue(val, type, depth) {
748
+ function stringifyValue(val, type, depth, numberAsFloat) {
753
749
  if (depth === 0) {
754
750
  throw new Error("Could not stringify the object: maximum object depth exceeded");
755
751
  }
@@ -760,6 +756,8 @@ function stringifyValue(val, type, depth) {
760
756
  return "inf";
761
757
  if (val === -Infinity)
762
758
  return "-inf";
759
+ if (numberAsFloat && Number.isInteger(val))
760
+ return val.toFixed(1);
763
761
  return val.toString();
764
762
  }
765
763
  if (type === "bigint" || type === "boolean") {
@@ -775,13 +773,13 @@ function stringifyValue(val, type, depth) {
775
773
  return val.toISOString();
776
774
  }
777
775
  if (type === "object") {
778
- return stringifyInlineTable(val, depth);
776
+ return stringifyInlineTable(val, depth, numberAsFloat);
779
777
  }
780
778
  if (type === "array") {
781
- return stringifyArray(val, depth);
779
+ return stringifyArray(val, depth, numberAsFloat);
782
780
  }
783
781
  }
784
- function stringifyInlineTable(obj, depth) {
782
+ function stringifyInlineTable(obj, depth, numberAsFloat) {
785
783
  let keys = Object.keys(obj);
786
784
  if (keys.length === 0)
787
785
  return "{}";
@@ -792,11 +790,11 @@ function stringifyInlineTable(obj, depth) {
792
790
  res += ", ";
793
791
  res += BARE_KEY.test(k) ? k : formatString(k);
794
792
  res += " = ";
795
- res += stringifyValue(obj[k], extendedTypeOf(obj[k]), depth - 1);
793
+ res += stringifyValue(obj[k], extendedTypeOf(obj[k]), depth - 1, numberAsFloat);
796
794
  }
797
795
  return res + " }";
798
796
  }
799
- function stringifyArray(array, depth) {
797
+ function stringifyArray(array, depth, numberAsFloat) {
800
798
  if (array.length === 0)
801
799
  return "[]";
802
800
  let res = "[ ";
@@ -806,11 +804,11 @@ function stringifyArray(array, depth) {
806
804
  if (array[i] === null || array[i] === void 0) {
807
805
  throw new TypeError("arrays cannot contain null or undefined values");
808
806
  }
809
- res += stringifyValue(array[i], extendedTypeOf(array[i]), depth - 1);
807
+ res += stringifyValue(array[i], extendedTypeOf(array[i]), depth - 1, numberAsFloat);
810
808
  }
811
809
  return res + " ]";
812
810
  }
813
- function stringifyArrayTable(array, key, depth) {
811
+ function stringifyArrayTable(array, key, depth, numberAsFloat) {
814
812
  if (depth === 0) {
815
813
  throw new Error("Could not stringify the object: maximum object depth exceeded");
816
814
  }
@@ -818,12 +816,12 @@ function stringifyArrayTable(array, key, depth) {
818
816
  for (let i = 0; i < array.length; i++) {
819
817
  res += `[[${key}]]
820
818
  `;
821
- res += stringifyTable(array[i], key, depth);
819
+ res += stringifyTable(array[i], key, depth, numberAsFloat);
822
820
  res += "\n\n";
823
821
  }
824
822
  return res;
825
823
  }
826
- function stringifyTable(obj, prefix, depth) {
824
+ function stringifyTable(obj, prefix, depth, numberAsFloat) {
827
825
  if (depth === 0) {
828
826
  throw new Error("Could not stringify the object: maximum object depth exceeded");
829
827
  }
@@ -839,17 +837,17 @@ function stringifyTable(obj, prefix, depth) {
839
837
  }
840
838
  let key = BARE_KEY.test(k) ? k : formatString(k);
841
839
  if (type === "array" && isArrayOfTables(obj[k])) {
842
- tables += stringifyArrayTable(obj[k], prefix ? `${prefix}.${key}` : key, depth - 1);
840
+ tables += stringifyArrayTable(obj[k], prefix ? `${prefix}.${key}` : key, depth - 1, numberAsFloat);
843
841
  } else if (type === "object") {
844
842
  let tblKey = prefix ? `${prefix}.${key}` : key;
845
843
  tables += `[${tblKey}]
846
844
  `;
847
- tables += stringifyTable(obj[k], tblKey, depth - 1);
845
+ tables += stringifyTable(obj[k], tblKey, depth - 1, numberAsFloat);
848
846
  tables += "\n\n";
849
847
  } else {
850
848
  preamble += key;
851
849
  preamble += " = ";
852
- preamble += stringifyValue(obj[k], type, depth);
850
+ preamble += stringifyValue(obj[k], type, depth, numberAsFloat);
853
851
  preamble += "\n";
854
852
  }
855
853
  }
@@ -857,12 +855,11 @@ function stringifyTable(obj, prefix, depth) {
857
855
  return `${preamble}
858
856
  ${tables}`.trim();
859
857
  }
860
- function stringify(obj, opts) {
858
+ function stringify(obj, { maxDepth = 1e3, numbersAsFloat = false } = {}) {
861
859
  if (extendedTypeOf(obj) !== "object") {
862
860
  throw new TypeError("stringify can only be called with an object");
863
861
  }
864
- let maxDepth = opts?.maxDepth ?? 1e3;
865
- return stringifyTable(obj, "", maxDepth);
862
+ return stringifyTable(obj, "", maxDepth, numbersAsFloat);
866
863
  }
867
864
 
868
865
  // dist/index.js
package/dist/index.d.ts CHANGED
@@ -29,7 +29,7 @@ import { parse } from './parse.js';
29
29
  import { stringify } from './stringify.js';
30
30
  import { TomlDate } from './date.js';
31
31
  import { TomlError } from './error.js';
32
- export type { TomlPrimitive } from './util.js';
32
+ export type { TomlValue, TomlTable, TomlValueWithoutBigInt, TomlTableWithoutBigInt } from './util.js';
33
33
  declare const _default: {
34
34
  parse: typeof parse;
35
35
  stringify: typeof stringify;
@@ -38,3 +38,6 @@ declare const _default: {
38
38
  };
39
39
  export default _default;
40
40
  export { parse, stringify, TomlDate, TomlError };
41
+ export type {
42
+ /** @deprecated use TomlValue instead */
43
+ TomlValue as TomlPrimitive } from './util.js';
package/dist/parse.d.ts CHANGED
@@ -25,7 +25,13 @@
25
25
  * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
26
  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
27
  */
28
- import { type TomlPrimitive } from './util.js';
29
- export declare function parse(toml: string, opts?: {
30
- maxDepth: number;
31
- }): Record<string, TomlPrimitive>;
28
+ import type { IntegersAsBigInt } from './primitive.js';
29
+ import { type TomlTable, type TomlTableWithoutBigInt } from './util.js';
30
+ export interface ParseOptions {
31
+ maxDepth?: number;
32
+ integersAsBigInt?: IntegersAsBigInt;
33
+ }
34
+ export declare function parse(toml: string, options?: ParseOptions & {
35
+ integersAsBigInt: Exclude<IntegersAsBigInt, undefined | false>;
36
+ }): TomlTable;
37
+ export declare function parse(toml: string, options?: ParseOptions): TomlTableWithoutBigInt;
package/dist/parse.js CHANGED
@@ -93,8 +93,7 @@ function peekTable(key, table, meta, type) {
93
93
  }
94
94
  return [k, t, state.c];
95
95
  }
96
- export function parse(toml, opts) {
97
- let maxDepth = opts?.maxDepth ?? 1000;
96
+ export function parse(toml, { maxDepth = 1000, integersAsBigInt } = {}) {
98
97
  let res = {};
99
98
  let meta = {};
100
99
  let tbl = res;
@@ -132,7 +131,7 @@ export function parse(toml, opts) {
132
131
  ptr: ptr,
133
132
  });
134
133
  }
135
- let v = extractValue(toml, k[1], void 0, maxDepth);
134
+ let v = extractValue(toml, k[1], void 0, maxDepth, integersAsBigInt);
136
135
  p[1][p[0]] = v[0];
137
136
  ptr = v[1];
138
137
  }
@@ -27,4 +27,5 @@
27
27
  */
28
28
  import { TomlDate } from './date.js';
29
29
  export declare function parseString(str: string, ptr?: number, endPtr?: number): string;
30
- export declare function parseValue(value: string, toml: string, ptr: number): boolean | number | TomlDate;
30
+ export type IntegersAsBigInt = undefined | boolean | 'asNeeded';
31
+ export declare function parseValue(value: string, toml: string, ptr: number, integersAsBigInt: IntegersAsBigInt): boolean | number | bigint | TomlDate;
package/dist/primitive.js CHANGED
@@ -42,7 +42,7 @@ let ESC_MAP = {
42
42
  '\\': '\\',
43
43
  };
44
44
  export function parseString(str, ptr = 0, endPtr = str.length) {
45
- let isLiteral = str[ptr] === "'";
45
+ let isLiteral = str[ptr] === '\'';
46
46
  let isMultiline = str[ptr++] === str[ptr] && str[ptr] === str[ptr + 1];
47
47
  if (isMultiline) {
48
48
  endPtr -= 2;
@@ -61,14 +61,14 @@ export function parseString(str, ptr = 0, endPtr = str.length) {
61
61
  if (!isMultiline) {
62
62
  throw new TomlError('newlines are not allowed in strings', {
63
63
  toml: str,
64
- ptr: ptr - 1
64
+ ptr: ptr - 1,
65
65
  });
66
66
  }
67
67
  }
68
68
  else if ((c < '\x20' && c !== '\t') || c === '\x7f') {
69
69
  throw new TomlError('control characters are not allowed in strings', {
70
70
  toml: str,
71
- ptr: ptr - 1
71
+ ptr: ptr - 1,
72
72
  });
73
73
  }
74
74
  if (isEscape) {
@@ -79,7 +79,7 @@ export function parseString(str, ptr = 0, endPtr = str.length) {
79
79
  if (!ESCAPE_REGEX.test(code)) {
80
80
  throw new TomlError('invalid unicode escape', {
81
81
  toml: str,
82
- ptr: tmp
82
+ ptr: tmp,
83
83
  });
84
84
  }
85
85
  try {
@@ -88,7 +88,7 @@ export function parseString(str, ptr = 0, endPtr = str.length) {
88
88
  catch {
89
89
  throw new TomlError('invalid unicode escape', {
90
90
  toml: str,
91
- ptr: tmp
91
+ ptr: tmp,
92
92
  });
93
93
  }
94
94
  }
@@ -98,7 +98,7 @@ export function parseString(str, ptr = 0, endPtr = str.length) {
98
98
  if (str[ptr] !== '\n' && str[ptr] !== '\r') {
99
99
  throw new TomlError('invalid escape: only line-ending whitespace may be escaped', {
100
100
  toml: str,
101
- ptr: tmp
101
+ ptr: tmp,
102
102
  });
103
103
  }
104
104
  ptr = skipVoid(str, ptr);
@@ -110,7 +110,7 @@ export function parseString(str, ptr = 0, endPtr = str.length) {
110
110
  else {
111
111
  throw new TomlError('unrecognized escape sequence', {
112
112
  toml: str,
113
- ptr: tmp
113
+ ptr: tmp,
114
114
  });
115
115
  }
116
116
  sliceStart = ptr;
@@ -123,7 +123,7 @@ export function parseString(str, ptr = 0, endPtr = str.length) {
123
123
  }
124
124
  return parsed + str.slice(sliceStart, endPtr - 1);
125
125
  }
126
- export function parseValue(value, toml, ptr) {
126
+ export function parseValue(value, toml, ptr, integersAsBigInt) {
127
127
  // Constant values
128
128
  if (value === 'true')
129
129
  return true;
@@ -135,37 +135,43 @@ export function parseValue(value, toml, ptr) {
135
135
  return Infinity;
136
136
  if (value === 'nan' || value === '+nan' || value === '-nan')
137
137
  return NaN;
138
+ // Avoid FP representation of -0
138
139
  if (value === '-0')
139
- return 0; // Avoid FP representation of -0
140
+ return integersAsBigInt ? 0n : 0;
140
141
  // Numbers
141
- let isInt;
142
- if ((isInt = INT_REGEX.test(value)) || FLOAT_REGEX.test(value)) {
142
+ let isInt = INT_REGEX.test(value);
143
+ if (isInt || FLOAT_REGEX.test(value)) {
143
144
  if (LEADING_ZERO.test(value)) {
144
145
  throw new TomlError('leading zeroes are not allowed', {
145
146
  toml: toml,
146
- ptr: ptr
147
+ ptr: ptr,
147
148
  });
148
149
  }
149
- let numeric = +(value.replace(/_/g, ''));
150
+ value = value.replace(/_/g, '');
151
+ let numeric = +value;
150
152
  if (isNaN(numeric)) {
151
153
  throw new TomlError('invalid number', {
152
154
  toml: toml,
153
- ptr: ptr
155
+ ptr: ptr,
154
156
  });
155
157
  }
156
- if (isInt && !Number.isSafeInteger(numeric)) {
157
- throw new TomlError('integer value cannot be represented losslessly', {
158
- toml: toml,
159
- ptr: ptr
160
- });
158
+ if (isInt) {
159
+ if ((isInt = !Number.isSafeInteger(numeric)) && !integersAsBigInt) {
160
+ throw new TomlError('integer value cannot be represented losslessly', {
161
+ toml: toml,
162
+ ptr: ptr,
163
+ });
164
+ }
165
+ if (isInt || integersAsBigInt)
166
+ numeric = BigInt(value);
161
167
  }
162
168
  return numeric;
163
169
  }
164
- let date = new TomlDate(value);
170
+ const date = new TomlDate(value);
165
171
  if (!date.isValid()) {
166
172
  throw new TomlError('invalid value', {
167
173
  toml: toml,
168
- ptr: ptr
174
+ ptr: ptr,
169
175
  });
170
176
  }
171
177
  return date;
@@ -25,6 +25,7 @@
25
25
  * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
26
  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
27
  */
28
- export declare function stringify(obj: any, opts?: {
28
+ export declare function stringify(obj: any, { maxDepth, numbersAsFloat }?: {
29
29
  maxDepth?: number;
30
+ numbersAsFloat?: boolean;
30
31
  }): string;
package/dist/stringify.js CHANGED
@@ -46,9 +46,9 @@ function isArrayOfTables(obj) {
46
46
  function formatString(s) {
47
47
  return JSON.stringify(s).replace(/\x7f/g, '\\u007f');
48
48
  }
49
- function stringifyValue(val, type, depth) {
49
+ function stringifyValue(val, type, depth, numberAsFloat) {
50
50
  if (depth === 0) {
51
- throw new Error("Could not stringify the object: maximum object depth exceeded");
51
+ throw new Error('Could not stringify the object: maximum object depth exceeded');
52
52
  }
53
53
  if (type === 'number') {
54
54
  if (isNaN(val))
@@ -57,6 +57,8 @@ function stringifyValue(val, type, depth) {
57
57
  return 'inf';
58
58
  if (val === -Infinity)
59
59
  return '-inf';
60
+ if (numberAsFloat && Number.isInteger(val))
61
+ return val.toFixed(1);
60
62
  return val.toString();
61
63
  }
62
64
  if (type === 'bigint' || type === 'boolean') {
@@ -72,13 +74,13 @@ function stringifyValue(val, type, depth) {
72
74
  return val.toISOString();
73
75
  }
74
76
  if (type === 'object') {
75
- return stringifyInlineTable(val, depth);
77
+ return stringifyInlineTable(val, depth, numberAsFloat);
76
78
  }
77
79
  if (type === 'array') {
78
- return stringifyArray(val, depth);
80
+ return stringifyArray(val, depth, numberAsFloat);
79
81
  }
80
82
  }
81
- function stringifyInlineTable(obj, depth) {
83
+ function stringifyInlineTable(obj, depth, numberAsFloat) {
82
84
  let keys = Object.keys(obj);
83
85
  if (keys.length === 0)
84
86
  return '{}';
@@ -89,11 +91,11 @@ function stringifyInlineTable(obj, depth) {
89
91
  res += ', ';
90
92
  res += BARE_KEY.test(k) ? k : formatString(k);
91
93
  res += ' = ';
92
- res += stringifyValue(obj[k], extendedTypeOf(obj[k]), depth - 1);
94
+ res += stringifyValue(obj[k], extendedTypeOf(obj[k]), depth - 1, numberAsFloat);
93
95
  }
94
96
  return res + ' }';
95
97
  }
96
- function stringifyArray(array, depth) {
98
+ function stringifyArray(array, depth, numberAsFloat) {
97
99
  if (array.length === 0)
98
100
  return '[]';
99
101
  let res = '[ ';
@@ -103,25 +105,25 @@ function stringifyArray(array, depth) {
103
105
  if (array[i] === null || array[i] === void 0) {
104
106
  throw new TypeError('arrays cannot contain null or undefined values');
105
107
  }
106
- res += stringifyValue(array[i], extendedTypeOf(array[i]), depth - 1);
108
+ res += stringifyValue(array[i], extendedTypeOf(array[i]), depth - 1, numberAsFloat);
107
109
  }
108
110
  return res + ' ]';
109
111
  }
110
- function stringifyArrayTable(array, key, depth) {
112
+ function stringifyArrayTable(array, key, depth, numberAsFloat) {
111
113
  if (depth === 0) {
112
- throw new Error("Could not stringify the object: maximum object depth exceeded");
114
+ throw new Error('Could not stringify the object: maximum object depth exceeded');
113
115
  }
114
116
  let res = '';
115
117
  for (let i = 0; i < array.length; i++) {
116
118
  res += `[[${key}]]\n`;
117
- res += stringifyTable(array[i], key, depth);
119
+ res += stringifyTable(array[i], key, depth, numberAsFloat);
118
120
  res += '\n\n';
119
121
  }
120
122
  return res;
121
123
  }
122
- function stringifyTable(obj, prefix, depth) {
124
+ function stringifyTable(obj, prefix, depth, numberAsFloat) {
123
125
  if (depth === 0) {
124
- throw new Error("Could not stringify the object: maximum object depth exceeded");
126
+ throw new Error('Could not stringify the object: maximum object depth exceeded');
125
127
  }
126
128
  let preamble = '';
127
129
  let tables = '';
@@ -135,28 +137,27 @@ function stringifyTable(obj, prefix, depth) {
135
137
  }
136
138
  let key = BARE_KEY.test(k) ? k : formatString(k);
137
139
  if (type === 'array' && isArrayOfTables(obj[k])) {
138
- tables += stringifyArrayTable(obj[k], prefix ? `${prefix}.${key}` : key, depth - 1);
140
+ tables += stringifyArrayTable(obj[k], prefix ? `${prefix}.${key}` : key, depth - 1, numberAsFloat);
139
141
  }
140
142
  else if (type === 'object') {
141
143
  let tblKey = prefix ? `${prefix}.${key}` : key;
142
144
  tables += `[${tblKey}]\n`;
143
- tables += stringifyTable(obj[k], tblKey, depth - 1);
145
+ tables += stringifyTable(obj[k], tblKey, depth - 1, numberAsFloat);
144
146
  tables += '\n\n';
145
147
  }
146
148
  else {
147
149
  preamble += key;
148
150
  preamble += ' = ';
149
- preamble += stringifyValue(obj[k], type, depth);
151
+ preamble += stringifyValue(obj[k], type, depth, numberAsFloat);
150
152
  preamble += '\n';
151
153
  }
152
154
  }
153
155
  }
154
156
  return `${preamble}\n${tables}`.trim();
155
157
  }
156
- export function stringify(obj, opts) {
158
+ export function stringify(obj, { maxDepth = 1000, numbersAsFloat = false } = {}) {
157
159
  if (extendedTypeOf(obj) !== 'object') {
158
160
  throw new TypeError('stringify can only be called with an object');
159
161
  }
160
- let maxDepth = opts?.maxDepth ?? 1000;
161
- return stringifyTable(obj, '', maxDepth);
162
+ return stringifyTable(obj, '', maxDepth, numbersAsFloat);
162
163
  }
package/dist/struct.d.ts CHANGED
@@ -25,7 +25,8 @@
25
25
  * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
26
  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
27
  */
28
- import { type TomlPrimitive } from './util.js';
28
+ import { type IntegersAsBigInt } from './primitive.js';
29
+ import { type TomlTable, type TomlValue } from './util.js';
29
30
  export declare function parseKey(str: string, ptr: number, end?: string): [string[], number];
30
- export declare function parseInlineTable(str: string, ptr: number, depth?: number): [Record<string, TomlPrimitive>, number];
31
- export declare function parseArray(str: string, ptr: number, depth?: number): [TomlPrimitive[], number];
31
+ export declare function parseInlineTable(str: string, ptr: number, depth: number, integersAsBigInt: IntegersAsBigInt): [TomlTable, number];
32
+ export declare function parseArray(str: string, ptr: number, depth: number, integersAsBigInt: IntegersAsBigInt): [TomlValue[], number];
package/dist/struct.js CHANGED
@@ -27,7 +27,7 @@
27
27
  */
28
28
  import { parseString } from './primitive.js';
29
29
  import { extractValue } from './extract.js';
30
- import { skipComment, indexOfNewline, getStringEnd, skipVoid } from './util.js';
30
+ import { getStringEnd, indexOfNewline, skipComment, skipVoid } from './util.js';
31
31
  import { TomlError } from './error.js';
32
32
  let KEY_PART_RE = /^[a-zA-Z0-9-_]+[ \t]*$/;
33
33
  export function parseKey(str, ptr, end = '=') {
@@ -37,7 +37,7 @@ export function parseKey(str, ptr, end = '=') {
37
37
  if (endPtr < 0) {
38
38
  throw new TomlError('incomplete key-value: cannot find end of key', {
39
39
  toml: str,
40
- ptr: ptr
40
+ ptr: ptr,
41
41
  });
42
42
  }
43
43
  do {
@@ -45,7 +45,7 @@ export function parseKey(str, ptr, end = '=') {
45
45
  // If it's whitespace, ignore
46
46
  if (c !== ' ' && c !== '\t') {
47
47
  // If it's a string
48
- if (c === '"' || c === "'") {
48
+ if (c === '"' || c === '\'') {
49
49
  if (c === str[ptr + 1] && c === str[ptr + 2]) {
50
50
  throw new TomlError('multiline strings are not allowed in keys', {
51
51
  toml: str,
@@ -102,30 +102,22 @@ export function parseKey(str, ptr, end = '=') {
102
102
  } while (dot + 1 && dot < endPtr);
103
103
  return [parsed, skipVoid(str, endPtr + 1, true, true)];
104
104
  }
105
- export function parseInlineTable(str, ptr, depth = -1) {
105
+ export function parseInlineTable(str, ptr, depth, integersAsBigInt) {
106
106
  let res = {};
107
107
  let seen = new Set();
108
108
  let c;
109
109
  let comma = 0;
110
110
  ptr++;
111
111
  while ((c = str[ptr++]) !== '}' && c) {
112
+ let err = { toml: str, ptr: ptr - 1 };
112
113
  if (c === '\n') {
113
- throw new TomlError('newlines are not allowed in inline tables', {
114
- toml: str,
115
- ptr: ptr - 1
116
- });
114
+ throw new TomlError('newlines are not allowed in inline tables', err);
117
115
  }
118
116
  else if (c === '#') {
119
- throw new TomlError('inline tables cannot contain comments', {
120
- toml: str,
121
- ptr: ptr - 1
122
- });
117
+ throw new TomlError('inline tables cannot contain comments', err);
123
118
  }
124
119
  else if (c === ',') {
125
- throw new TomlError('expected key-value, found comma', {
126
- toml: str,
127
- ptr: ptr - 1
128
- });
120
+ throw new TomlError('expected key-value, found comma', err);
129
121
  }
130
122
  else if (c !== ' ' && c !== '\t') {
131
123
  let k;
@@ -139,7 +131,7 @@ export function parseInlineTable(str, ptr, depth = -1) {
139
131
  if ((hasOwn = Object.hasOwn(t, k)) && (typeof t[k] !== 'object' || seen.has(t[k]))) {
140
132
  throw new TomlError('trying to redefine an already defined value', {
141
133
  toml: str,
142
- ptr: ptr
134
+ ptr: ptr,
143
135
  });
144
136
  }
145
137
  if (!hasOwn && k === '__proto__') {
@@ -149,10 +141,10 @@ export function parseInlineTable(str, ptr, depth = -1) {
149
141
  if (hasOwn) {
150
142
  throw new TomlError('trying to redefine an already defined value', {
151
143
  toml: str,
152
- ptr: ptr
144
+ ptr: ptr,
153
145
  });
154
146
  }
155
- let [value, valueEndPtr] = extractValue(str, keyEndPtr, '}', depth - 1);
147
+ let [value, valueEndPtr] = extractValue(str, keyEndPtr, '}', depth - 1, integersAsBigInt);
156
148
  seen.add(value);
157
149
  t[k] = value;
158
150
  ptr = valueEndPtr;
@@ -162,18 +154,18 @@ export function parseInlineTable(str, ptr, depth = -1) {
162
154
  if (comma) {
163
155
  throw new TomlError('trailing commas are not allowed in inline tables', {
164
156
  toml: str,
165
- ptr: comma
157
+ ptr: comma,
166
158
  });
167
159
  }
168
160
  if (!c) {
169
161
  throw new TomlError('unfinished table encountered', {
170
162
  toml: str,
171
- ptr: ptr
163
+ ptr: ptr,
172
164
  });
173
165
  }
174
166
  return [res, ptr];
175
167
  }
176
- export function parseArray(str, ptr, depth = -1) {
168
+ export function parseArray(str, ptr, depth, integersAsBigInt) {
177
169
  let res = [];
178
170
  let c;
179
171
  ptr++;
@@ -181,13 +173,13 @@ export function parseArray(str, ptr, depth = -1) {
181
173
  if (c === ',') {
182
174
  throw new TomlError('expected value, found comma', {
183
175
  toml: str,
184
- ptr: ptr - 1
176
+ ptr: ptr - 1,
185
177
  });
186
178
  }
187
179
  else if (c === '#')
188
180
  ptr = skipComment(str, ptr);
189
181
  else if (c !== ' ' && c !== '\t' && c !== '\n' && c !== '\r') {
190
- let e = extractValue(str, ptr - 1, ']', depth - 1);
182
+ let e = extractValue(str, ptr - 1, ']', depth - 1, integersAsBigInt);
191
183
  res.push(e[0]);
192
184
  ptr = e[1];
193
185
  }
@@ -195,7 +187,7 @@ export function parseArray(str, ptr, depth = -1) {
195
187
  if (!c) {
196
188
  throw new TomlError('unfinished array encountered', {
197
189
  toml: str,
198
- ptr: ptr
190
+ ptr: ptr,
199
191
  });
200
192
  }
201
193
  return [res, ptr];
package/dist/util.d.ts CHANGED
@@ -26,9 +26,15 @@
26
26
  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
27
  */
28
28
  import type { TomlDate } from './date.js';
29
- export type TomlPrimitive = string | number | boolean | TomlDate | {
30
- [key: string]: TomlPrimitive;
31
- } | TomlPrimitive[];
29
+ export type TomlPrimitive = string | number | bigint | boolean | TomlDate;
30
+ export type TomlTable = {
31
+ [key: string]: TomlValue;
32
+ };
33
+ export type TomlValue = TomlPrimitive | TomlValue[] | TomlTable;
34
+ export type TomlTableWithoutBigInt = {
35
+ [key: string]: TomlValueWithoutBigInt;
36
+ };
37
+ export type TomlValueWithoutBigInt = Exclude<TomlPrimitive, bigint> | TomlValueWithoutBigInt[] | TomlTableWithoutBigInt;
32
38
  export declare function indexOfNewline(str: string, start?: number, end?: number): number;
33
39
  export declare function skipComment(str: string, ptr: number): number;
34
40
  export declare function skipVoid(str: string, ptr: number, banNewLines?: boolean, banComments?: boolean): number;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "smol-toml",
3
3
  "license": "BSD-3-Clause",
4
- "version": "1.3.4",
4
+ "version": "1.4.0",
5
5
  "description": "A small, fast, and correct TOML parser/serializer",
6
6
  "author": "Cynthia <cyyynthia@borkenware.com>",
7
7
  "repository": "github:squirrelchat/smol-toml",
@@ -19,15 +19,15 @@
19
19
  "devDependencies": {
20
20
  "@iarna/toml": "3.0.0",
21
21
  "@ltd/j-toml": "^1.38.0",
22
- "@tsconfig/node-lts": "^22.0.1",
22
+ "@tsconfig/node-lts": "^22.0.2",
23
23
  "@tsconfig/strictest": "^2.0.5",
24
- "@types/node": "^22.14.1",
25
- "@vitest/ui": "^3.1.2",
26
- "esbuild": "^0.25.3",
24
+ "@types/node": "^24.0.7",
25
+ "@vitest/ui": "^3.2.4",
26
+ "esbuild": "^0.25.5",
27
27
  "fast-toml": "^0.5.4",
28
- "pin-github-action": "^3.1.2",
28
+ "pin-github-action": "^3.4.0",
29
29
  "typescript": "^5.8.3",
30
- "vitest": "^3.1.2"
30
+ "vitest": "^3.2.4"
31
31
  },
32
32
  "main": "./dist/index.cjs",
33
33
  "module": "./dist/index.js",