smol-toml 1.0.0 → 1.0.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/README.md CHANGED
@@ -1,8 +1,9 @@
1
1
  # smol-toml
2
+ [![TOML 1.0.0](https://img.shields.io/badge/TOML-1.0.0-9c4221?style=flat-square)](https://toml.io/en/v1.0.0)
2
3
  [![License](https://img.shields.io/github/license/squirrelchat/smol-toml.svg?style=flat-square)](https://github.com/squirrelchat/smol-toml/blob/mistress/LICENSE)
3
4
  [![npm](https://img.shields.io/npm/v/smol-toml?style=flat-square)](https://npm.im/smol-toml)
4
5
 
5
- A small, fast, and correct TOML parser. smol-toml is fully spec-compliant with TOML v1.0.0.
6
+ A small, fast, and correct TOML parser. smol-toml is fully(ish) spec-compliant with TOML v1.0.0.
6
7
 
7
8
  Why yet another TOML parser? Well, the ecosystem of TOML parsers in JavaScript is quite underwhelming, most likely due
8
9
  to a lack of interest. With most parsers being outdated, unmaintained, non-compliant, or a combination of these, a new
@@ -10,7 +11,31 @@ parser didn't feel too out of place.
10
11
 
11
12
  *[insert xkcd 927]*
12
13
 
13
- smol-toml produces valid results (or errors) for all the test TOML files in https://github.com/iarna/toml-spec-tests.
14
+ smol-toml passes most of the tests from [BurntSushi's `toml-test` suite](https://github.com/BurntSushi/toml-test).
15
+ However, due to the nature of JavaScript and the limits of the language, it doesn't pass certain tests, namely:
16
+ - Invalid UTF-8 strings are not rejected
17
+ - Certain invalid UTF-8 codepoints are not rejected
18
+ - smol-toml doesn't preserve type information between integers and floats (in JS, everything is a float)
19
+ - smol-toml doesn't support the whole 64-bit range for integers (but does throw an appropriate error)
20
+ - As all numbers are floats in JS, the safe range is `2**53 - 1` <=> `-(2**53 - 1)`.
21
+
22
+ smol-toml also passes all of the tests in https://github.com/iarna/toml-spec-tests.
23
+
24
+ <details>
25
+ <summary>List of failed `toml-test` cases</summary>
26
+ These tests were done by modifying `primitive.ts` and make the implementation return bigints for integers. This allows
27
+ verifying the parser correctly intents a number to be an integer or a float.
28
+
29
+ *Ideally, this becomes an option of the library, but for now...*
30
+
31
+ The following tests are failing:
32
+ - invalid/encoding/bad-utf8-in-comment
33
+ - invalid/encoding/bad-utf8-in-multiline-literal
34
+ - invalid/encoding/bad-utf8-in-multiline
35
+ - invalid/encoding/bad-utf8-in-string-literal
36
+ - invalid/encoding/bad-utf8-in-string
37
+ - invalid/string/bad-codepoint
38
+ </details>
14
39
 
15
40
  ## Installation
16
41
  ```
@@ -38,41 +63,55 @@ The large TOML generator can be found [here](https://gist.github.com/cyyynthia/e
38
63
 
39
64
  | | smol-toml | @iarna/toml@3.0.0 | @ltd/j-toml | fast-toml |
40
65
  |----------------|---------------------|-------------------|----------------|----------------|
41
- | Spec example | **60,733.91 op/s** | 32,565.20 op/s | 16,781.03 op/s | 31,336.67 op/s |
42
- | ~5MB test file | **4.2567 op/s** | *DNF* | 2.4873 op/s | 2.5790 op/s |
66
+ | Spec example | **71,356.51 op/s** | 33,629.31 op/s | 16,433.86 op/s | 29,421.60 op/s |
67
+ | ~5MB test file | **3.8091 op/s** | *DNF* | 2.4369 op/s | 2.6078 op/s |
43
68
 
44
69
  <details>
45
70
  <summary>Detailed benchmark data</summary>
46
71
 
47
- Tests ran using Vitest v0.31.0 on commit 361089f3dbc30d994494bf6ec1e8e2f135531247
72
+ Tests ran using Vitest v0.31.0 on commit 04d233e351f9ae719222154ee2217aea8b95dbab
48
73
 
49
74
  CPU: Intel Core i7 7700K (4.2GHz)
50
75
 
51
76
  ```
52
77
  RUN v0.31.0
53
78
 
54
- ✓ bench/parseSpecExample.bench.ts (4) 2466ms
79
+ ✓ bench/parseSpecExample.bench.ts (4) 2462ms
55
80
  name hz min max mean p75 p99 p995 p999 rme samples
56
- · smol-toml 60,733.91 0.0145 0.2580 0.0165 0.0152 0.0319 0.0345 0.1383 ±0.46% 30367 fastest
57
- · @iarna/toml 32,565.20 0.0268 0.3208 0.0307 0.0284 0.0580 0.0619 0.1699 ±0.54% 16283
58
- · @ltd/j-toml 16,781.03 0.0505 1.0392 0.0596 0.0540 0.1147 0.1360 0.7657 ±1.52% 8391 slowest
59
- · fast-toml 31,336.67 0.0298 0.3357 0.0319 0.0305 0.0578 0.0622 0.1580 ±0.41% 15669
60
- ✓ bench/parseLargeMixed.bench.ts (3) 15752ms
81
+ · smol-toml 71,356.51 0.0132 0.2633 0.0140 0.0137 0.0219 0.0266 0.1135 ±0.37% 35679 fastest
82
+ · @iarna/toml 33,629.31 0.0272 0.2629 0.0297 0.0287 0.0571 0.0650 0.1593 ±0.45% 16815
83
+ · @ltd/j-toml 16,433.86 0.0523 1.3088 0.0608 0.0550 0.1140 0.1525 0.7348 ±1.47% 8217 slowest
84
+ · fast-toml 29,421.60 0.0305 0.2995 0.0340 0.0312 0.0618 0.0640 0.1553 ±0.47% 14711
85
+ ✓ bench/parseLargeMixed.bench.ts (3) 16062ms
61
86
  name hz min max mean p75 p99 p995 p999 rme samples
62
- · smol-toml 4.2567 225.85 257.42 234.92 242.74 257.42 257.42 257.42 ±3.35% 10 fastest
63
- · @ltd/j-toml 2.4873 382.66 441.12 402.05 416.25 441.12 441.12 441.12 ±3.40% 10 slowest
64
- · fast-toml 2.5790 377.86 409.32 387.75 392.90 409.32 409.32 409.32 ±2.07% 10
87
+ · smol-toml 3.8091 239.60 287.30 262.53 274.17 287.30 287.30 287.30 ±3.66% 10 fastest
88
+ · @ltd/j-toml 2.4369 376.73 493.49 410.35 442.58 493.49 493.49 493.49 ±7.08% 10 slowest
89
+ · fast-toml 2.6078 373.88 412.79 383.47 388.62 412.79 412.79 412.79 ±2.72% 10
65
90
 
66
91
 
67
92
  BENCH Summary
68
93
 
69
94
  smol-toml - bench/parseLargeMixed.bench.ts >
70
- 1.65x faster than fast-toml
71
- 1.71x faster than @ltd/j-toml
95
+ 1.46x faster than fast-toml
96
+ 1.56x faster than @ltd/j-toml
72
97
 
73
98
  smol-toml - bench/parseSpecExample.bench.ts >
74
- 1.86x faster than @iarna/toml
75
- 1.94x faster than fast-toml
76
- 3.62x faster than @ltd/j-toml
99
+ 2.12x faster than @iarna/toml
100
+ 2.43x faster than fast-toml
101
+ 4.34x faster than @ltd/j-toml
77
102
  ```
103
+
104
+ ---
105
+ Additional notes:
106
+
107
+ I initially tried to benchmark `toml-nodejs`, but the 0.3.0 package is broken.
108
+ I initially reported this to the library author, but the author decided to
109
+ - a) advise to use a custom loader (via *experimental* flag) to circumvent the invalid imports.
110
+ - Said flag, `--experimental-specifier-resolution`, has been removed in Node v20.
111
+ - b) [delete the issue](https://github.com/huan231/toml-nodejs/issues/12) when pointed out links to the NodeJS
112
+ documentation about the flag removal and standard resolution algorithm.
113
+
114
+ For the reference anyways, `toml-nodejs` (with proper imports) is ~8x slower on both benchmark with:
115
+ - spec example: 7,543.47 op/s
116
+ - 5mb mixed: 0.7006 op/s
78
117
  </details>
package/dist/date.d.ts CHANGED
@@ -25,7 +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
- export declare let DATE_TIME_RE: RegExp;
29
28
  export default class TomlDate extends Date {
30
29
  #private;
31
30
  constructor(date: string | Date);
@@ -33,6 +32,7 @@ export default class TomlDate extends Date {
33
32
  isLocal(): boolean;
34
33
  isDate(): boolean;
35
34
  isTime(): boolean;
35
+ isValid(): boolean;
36
36
  toISOString(): string;
37
37
  static wrapAsOffsetDateTime(jsDate: Date, offset?: string): TomlDate;
38
38
  static wrapAsLocalDateTime(jsDate: Date): TomlDate;
package/dist/date.js CHANGED
@@ -25,11 +25,11 @@
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 let DATE_TIME_RE = /^(\d{4}-\d{2}-\d{2})?[Tt ]?(\d{2}:\d{2}:\d{2}(?:\.\d{3,})?)?(Z|[-+]\d{2}:\d{2})?$/;
28
+ let DATE_TIME_RE = /^(\d{4}-\d{2}-\d{2})?[T ]?(?:(\d{2}):\d{2}:\d{2}(?:\.\d+)?)?(Z|[-+]\d{2}:\d{2})?$/i;
29
29
  export default class TomlDate extends Date {
30
- #hasDate;
31
- #hasTime;
32
- #offset;
30
+ #hasDate = false;
31
+ #hasTime = false;
32
+ #offset = null;
33
33
  constructor(date) {
34
34
  let hasDate = true;
35
35
  let hasTime = true;
@@ -42,16 +42,21 @@ export default class TomlDate extends Date {
42
42
  date = `0000-01-01T${date}`;
43
43
  }
44
44
  hasTime = !!match[2];
45
- offset = match[3] || null;
45
+ // Do not allow rollover hours
46
+ if (match[2] && +match[2] > 23) {
47
+ date = '';
48
+ }
49
+ else {
50
+ offset = match[3] || null;
51
+ date = date.toUpperCase();
52
+ }
53
+ }
54
+ else {
55
+ date = '';
46
56
  }
47
57
  }
48
58
  super(date);
49
- if (isNaN(this.getTime())) {
50
- this.#hasDate = false;
51
- this.#hasTime = false;
52
- this.#offset = null;
53
- }
54
- else {
59
+ if (!isNaN(this.getTime())) {
55
60
  this.#hasDate = hasDate;
56
61
  this.#hasTime = hasTime;
57
62
  this.#offset = offset;
@@ -61,7 +66,7 @@ export default class TomlDate extends Date {
61
66
  return this.#hasDate && this.#hasTime;
62
67
  }
63
68
  isLocal() {
64
- return !this.#hasTime || !this.#hasTime || !this.#offset;
69
+ return !this.#hasDate || !this.#hasTime || !this.#offset;
65
70
  }
66
71
  isDate() {
67
72
  return this.#hasDate && !this.#hasTime;
@@ -69,6 +74,9 @@ export default class TomlDate extends Date {
69
74
  isTime() {
70
75
  return this.#hasTime && !this.#hasDate;
71
76
  }
77
+ isValid() {
78
+ return this.#hasDate || this.#hasTime;
79
+ }
72
80
  toISOString() {
73
81
  let iso = super.toISOString();
74
82
  // Local Date
package/dist/index.js CHANGED
@@ -26,56 +26,115 @@
26
26
  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
27
  */
28
28
  import { parseKey } from './struct.js';
29
- import { extractKeyValue } from './parse.js';
30
- import { skipVoid, peekTable } from './util.js';
29
+ import { extractValue } from './parse.js';
30
+ import { skipVoid } from './util.js';
31
31
  import TomlError from './error.js';
32
32
  export { default as TomlDate } from './date.js';
33
+ function peekTable(key, table, meta, type) {
34
+ let t = table;
35
+ let m = meta;
36
+ let k;
37
+ let hasOwn = false;
38
+ let state;
39
+ for (let i = 0; i < key.length; i++) {
40
+ if (i) {
41
+ t = hasOwn ? t[k] : (t[k] = {});
42
+ m = (state = m[k]).c;
43
+ if (type === 0 /* Type.DOTTED */ && state.t === 1 /* Type.EXPLICIT */) {
44
+ return null;
45
+ }
46
+ if (state.t === 2 /* Type.ARRAY */) {
47
+ let l = t.length - 1;
48
+ t = t[l];
49
+ m = m[l].c;
50
+ }
51
+ }
52
+ k = key[i];
53
+ if ((hasOwn = Object.hasOwn(t, k)) && m[k]?.t === 0 /* Type.DOTTED */ && m[k]?.d) {
54
+ return null;
55
+ }
56
+ if (!hasOwn) {
57
+ if (k === '__proto__') {
58
+ Object.defineProperty(t, k, { enumerable: true, configurable: true, writable: true });
59
+ Object.defineProperty(m, k, { enumerable: true, configurable: true, writable: true });
60
+ }
61
+ m[k] = {
62
+ t: i < key.length - 1 && type === 2 /* Type.ARRAY */
63
+ ? 0 /* Type.DOTTED */
64
+ : type,
65
+ d: false,
66
+ i: 0,
67
+ c: {},
68
+ };
69
+ }
70
+ }
71
+ state = m[k];
72
+ if (state.t !== type) {
73
+ // Bad key type!
74
+ return null;
75
+ }
76
+ if (type === 2 /* Type.ARRAY */) {
77
+ if (!state.d) {
78
+ state.d = true;
79
+ t[k] = [];
80
+ }
81
+ t[k].push(t = {});
82
+ state.c[state.i++] = (state = { t: 1 /* Type.EXPLICIT */, d: false, i: 0, c: {} });
83
+ }
84
+ if (state.d) {
85
+ // Redefining a table!
86
+ return null;
87
+ }
88
+ state.d = true;
89
+ if (type === 1 /* Type.EXPLICIT */) {
90
+ t = hasOwn ? t[k] : (t[k] = {});
91
+ }
92
+ else if (type === 0 /* Type.DOTTED */ && hasOwn) {
93
+ return null;
94
+ }
95
+ return [k, t, state.c];
96
+ }
33
97
  export function parse(toml) {
34
98
  let res = {};
99
+ let meta = {};
35
100
  let tbl = res;
36
- let seenTables = new Set();
37
- let seenValues = new Set();
101
+ let m = meta;
38
102
  for (let ptr = skipVoid(toml, 0); ptr < toml.length;) {
39
103
  if (toml[ptr] === '[') {
40
- let isTableArray = toml[ptr + 1] === '[';
41
- let end = toml.indexOf(']', ptr);
42
- if (end === -1)
43
- throw new TomlError('unfinished table encountered', {
44
- toml: toml,
45
- ptr: ptr
46
- });
47
- let k = parseKey(toml, ptr += +isTableArray + 1, end++);
48
- let strKey = k.join('"."');
49
- if (!isTableArray && seenTables.has(strKey))
50
- throw new TomlError('trying to redefine an already defined table', {
51
- toml: toml,
52
- ptr: ptr - 1
53
- });
54
- seenTables.add(strKey);
55
- let r = peekTable(res, k, seenValues, true);
56
- if (!r) {
57
- throw new TomlError('trying to redefine an already defined value', {
58
- toml: toml,
59
- ptr: ptr - 1
60
- });
61
- }
62
- let v = r[1][r[0]];
63
- if (!v) {
64
- r[1][r[0]] = (v = isTableArray ? [] : {});
104
+ let isTableArray = toml[++ptr] === '[';
105
+ let k = parseKey(toml, ptr += +isTableArray, ']');
106
+ if (isTableArray) {
107
+ if (toml[k[1] - 1] !== ']') {
108
+ throw new TomlError('expected end of table declaration', {
109
+ toml: toml,
110
+ ptr: k[1] - 1,
111
+ });
112
+ }
113
+ k[1]++;
65
114
  }
66
- else if (isTableArray && !Array.isArray(v)) {
67
- throw new TomlError('trying to define an array of tables, but a table already exists for this identifier', {
115
+ let p = peekTable(k[0], res, meta, isTableArray ? 2 /* Type.ARRAY */ : 1 /* Type.EXPLICIT */);
116
+ if (!p) {
117
+ throw new TomlError('trying to redefine an already defined table or value', {
68
118
  toml: toml,
69
- ptr: ptr - 2
119
+ ptr: ptr,
70
120
  });
71
121
  }
72
- tbl = v;
73
- if (isTableArray)
74
- v.push(tbl = {});
75
- ptr = end + +isTableArray;
122
+ m = p[2];
123
+ tbl = p[1];
124
+ ptr = k[1];
76
125
  }
77
126
  else {
78
- ptr = extractKeyValue(toml, ptr, tbl, seenValues);
127
+ let k = parseKey(toml, ptr);
128
+ let p = peekTable(k[0], tbl, m, 0 /* Type.DOTTED */);
129
+ if (!p) {
130
+ throw new TomlError('trying to redefine an already defined table or value', {
131
+ toml: toml,
132
+ ptr: ptr,
133
+ });
134
+ }
135
+ let v = extractValue(toml, k[1]);
136
+ p[1][p[0]] = v[0];
137
+ ptr = v[1];
79
138
  }
80
139
  ptr = skipVoid(toml, ptr, true);
81
140
  if (toml[ptr] && toml[ptr] !== '\n' && toml[ptr] !== '\r') {
package/dist/parse.d.ts CHANGED
@@ -27,4 +27,3 @@
27
27
  */
28
28
  import { type TomlPrimitive } from './util.js';
29
29
  export declare function extractValue(str: string, ptr: number, end?: string): [TomlPrimitive, number];
30
- export declare function extractKeyValue(str: string, ptr: number, table: Record<string, TomlPrimitive>, seen: Set<any>, isInline?: boolean): number;
package/dist/parse.js CHANGED
@@ -26,49 +26,29 @@
26
26
  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
27
  */
28
28
  import { parseString, parseValue } from './primitive.js';
29
- import { parseKey, parseArray, parseInlineTable } from './struct.js';
30
- import { peekTable, indexOfNewline, skipUntil, skipComment, skipVoid } from './util.js';
29
+ import { parseArray, parseInlineTable } from './struct.js';
30
+ import { indexOfNewline, skipVoid, skipUntil, skipComment, getStringEnd } from './util.js';
31
31
  import TomlError from './error.js';
32
- function getStringEnd(str, seek) {
33
- let first = str[seek];
34
- let target = first === str[seek + 1] && str[seek + 1] === str[seek + 2]
35
- ? str.slice(seek, seek + 3)
36
- : first;
37
- seek += target.length - 1;
38
- do
39
- seek = str.indexOf(target, ++seek);
40
- while (seek > -1 && first !== "'" && str[seek - 1] === '\\' && str[seek - 2] !== '\\');
41
- seek += target.length;
42
- if (target.length > 1) {
43
- if (str[seek] === first)
44
- seek++;
45
- if (str[seek] === first)
46
- seek++;
47
- }
48
- return seek;
49
- }
50
32
  function sliceAndTrimEndOf(str, startPtr, endPtr, allowNewLines) {
51
33
  let value = str.slice(startPtr, endPtr);
52
- let newlineIdx;
53
34
  let commentIdx = value.indexOf('#');
54
35
  if (commentIdx > -1) {
36
+ // The call to skipComment allows to "validate" the comment
37
+ // (absence of control characters)
55
38
  skipComment(str, commentIdx);
56
39
  value = value.slice(0, commentIdx);
57
40
  }
58
41
  let trimmed = value.trimEnd();
59
42
  if (!allowNewLines) {
60
- let s = '\n';
61
- newlineIdx = value.lastIndexOf('\n');
62
- if (newlineIdx < 0)
63
- newlineIdx = value.lastIndexOf(s = '\r');
64
- if (trimmed.lastIndexOf(s) !== newlineIdx) {
43
+ let newlineIdx = value.indexOf('\n', trimmed.length);
44
+ if (newlineIdx > -1) {
65
45
  throw new TomlError('newlines are not allowed in inline tables', {
66
46
  toml: str,
67
47
  ptr: startPtr + newlineIdx
68
48
  });
69
49
  }
70
50
  }
71
- return trimmed;
51
+ return [trimmed, commentIdx];
72
52
  }
73
53
  export function extractValue(str, ptr, end) {
74
54
  let c = str[ptr];
@@ -76,7 +56,7 @@ export function extractValue(str, ptr, end) {
76
56
  let [value, endPtr] = c === '['
77
57
  ? parseArray(str, ptr)
78
58
  : parseInlineTable(str, ptr);
79
- let newPtr = skipUntil(str, endPtr, end);
59
+ let newPtr = skipUntil(str, endPtr, ',', end);
80
60
  if (end === '}') {
81
61
  let nextNewLine = indexOfNewline(str, endPtr, newPtr);
82
62
  if (nextNewLine > -1) {
@@ -93,45 +73,20 @@ export function extractValue(str, ptr, end) {
93
73
  endPtr = getStringEnd(str, ptr);
94
74
  return [parseString(str, ptr, endPtr), endPtr + +(!!end && str[endPtr] === ',')];
95
75
  }
96
- endPtr = skipUntil(str, ptr, end);
97
- let valStr = sliceAndTrimEndOf(str, ptr, endPtr - (+(str[endPtr - 1] === ',')), end === ']');
98
- if (!valStr) {
76
+ endPtr = skipUntil(str, ptr, ',', end);
77
+ let slice = sliceAndTrimEndOf(str, ptr, endPtr - (+(str[endPtr - 1] === ',')), end === ']');
78
+ if (!slice[0]) {
99
79
  throw new TomlError('incomplete key-value declaration: no value specified', {
100
80
  toml: str,
101
81
  ptr: ptr
102
82
  });
103
83
  }
84
+ if (end && slice[1] > -1) {
85
+ endPtr = skipVoid(str, ptr + slice[1]);
86
+ endPtr += +(str[endPtr] === ',');
87
+ }
104
88
  return [
105
- parseValue(valStr, str, ptr),
106
- endPtr
89
+ parseValue(slice[0], str, ptr),
90
+ endPtr,
107
91
  ];
108
92
  }
109
- export function extractKeyValue(str, ptr, table, seen, isInline) {
110
- let equalIdx = str.indexOf('=', ptr);
111
- if (equalIdx < 0) {
112
- throw new TomlError('incomplete key-value declaration: no equals sign after the key', {
113
- toml: str,
114
- ptr: ptr
115
- });
116
- }
117
- // KEY
118
- let t = peekTable(table, parseKey(str, ptr, equalIdx), seen);
119
- if (!t) {
120
- throw new TomlError('trying to redefine an already defined value', {
121
- toml: str,
122
- ptr: ptr
123
- });
124
- }
125
- // VALUE
126
- ptr = skipVoid(str, equalIdx + 1, true, true);
127
- if (str[ptr] === '\n' || str[ptr] === '\r') {
128
- throw new TomlError('newlines are not allowed in key-value declarations', {
129
- toml: str,
130
- ptr: ptr
131
- });
132
- }
133
- let e = extractValue(str, ptr, isInline ? '}' : void 0);
134
- t[1][t[0]] = e[0];
135
- seen.add(e[0]);
136
- return e[1];
137
- }
package/dist/primitive.js CHANGED
@@ -26,11 +26,11 @@
26
26
  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
27
  */
28
28
  import { skipVoid } from './util.js';
29
- import TomlDate, { DATE_TIME_RE } from './date.js';
29
+ import TomlDate from './date.js';
30
30
  import TomlError from './error.js';
31
- let NUM_REGEX = /^(0x|0b|0o|[+-])?[0-9a-f_]+(\.[0-9a-f_]+)?([e][+-]?[0-9a-f_]+)?$/i;
32
- let INT_REGEX = /^(0x|0b|0o|[+-])?[0-9a-f]+$/;
33
- let LEADING_ZERO = /^[+-]?0\d/;
31
+ let INT_REGEX = /^((0x[0-9a-fA-F](_?[0-9a-fA-F])*)|(([+-]|0[ob])?\d(_?\d)*))$/;
32
+ let FLOAT_REGEX = /^[+-]?\d(_?\d)*(\.\d(_?\d)*)?([eE][+-]?\d(_?\d)*)?$/;
33
+ let LEADING_ZERO = /^[+-]?0[0-9_]/;
34
34
  let ESCAPE_REGEX = /^[0-9a-f]{4,8}$/i;
35
35
  let ESC_MAP = {
36
36
  b: '\b',
@@ -41,17 +41,6 @@ let ESC_MAP = {
41
41
  '"': '"',
42
42
  '\\': '\\',
43
43
  };
44
- let VALUES_MAP = {
45
- true: true,
46
- false: false,
47
- inf: Infinity,
48
- '+inf': Infinity,
49
- '-inf': -Infinity,
50
- nan: NaN,
51
- '+nan': NaN,
52
- '-nan': NaN,
53
- '-0': 0,
54
- };
55
44
  export function parseString(str, ptr = 0, endPtr = str.length) {
56
45
  let isLiteral = str[ptr] === "'";
57
46
  let isMultiline = str[ptr++] === str[ptr] && str[ptr] === str[ptr + 1];
@@ -68,13 +57,15 @@ export function parseString(str, ptr = 0, endPtr = str.length) {
68
57
  let sliceStart = ptr;
69
58
  while (ptr < endPtr - 1) {
70
59
  let c = str[ptr++];
71
- if (!isMultiline && (c === '\n' || c === '\r')) {
72
- throw new TomlError('newlines are not allowed in strings', {
73
- toml: str,
74
- ptr: ptr - 1
75
- });
60
+ if (c === '\n' || (c === '\r' && str[ptr] === '\n')) {
61
+ if (!isMultiline) {
62
+ throw new TomlError('newlines are not allowed in strings', {
63
+ toml: str,
64
+ ptr: ptr - 1
65
+ });
66
+ }
76
67
  }
77
- if (c < '\t' || c === '\x0b' || c === '\x0c' || c === '\x7f' || (c > '\x0d' && c < '\x20')) {
68
+ else if ((c < '\x20' && c !== '\t') || c === '\x7f') {
78
69
  throw new TomlError('control characters are not allowed in strings', {
79
70
  toml: str,
80
71
  ptr: ptr - 1
@@ -133,39 +124,49 @@ export function parseString(str, ptr = 0, endPtr = str.length) {
133
124
  return parsed + str.slice(sliceStart, endPtr - 1);
134
125
  }
135
126
  export function parseValue(value, toml, ptr) {
136
- if (Object.hasOwn(VALUES_MAP, value))
137
- return VALUES_MAP[value];
127
+ // Constant values
128
+ if (value === 'true')
129
+ return true;
130
+ if (value === 'false')
131
+ return false;
132
+ if (value === '-inf')
133
+ return -Infinity;
134
+ if (value === 'inf' || value === '+inf')
135
+ return Infinity;
136
+ if (value === 'nan' || value === '+nan' || value === '-nan')
137
+ return NaN;
138
138
  // Numbers
139
- if (NUM_REGEX.test(value)) {
139
+ let isInt;
140
+ if (value === '-0')
141
+ return 0; // Avoid FP representation of -0
142
+ if ((isInt = INT_REGEX.test(value)) || FLOAT_REGEX.test(value)) {
140
143
  if (LEADING_ZERO.test(value)) {
141
144
  throw new TomlError('leading zeroes are not allowed', {
142
145
  toml: toml,
143
146
  ptr: ptr
144
147
  });
145
148
  }
146
- value = value.replace(/_/g, '');
147
- let numeric = +value;
148
- if (INT_REGEX.test(value) && !Number.isSafeInteger(numeric)) {
149
- throw new TomlError('integer value cannot be represented losslessly', {
149
+ let numeric = +(value.replace(/_/g, ''));
150
+ if (isNaN(numeric)) {
151
+ throw new TomlError('invalid number', {
150
152
  toml: toml,
151
153
  ptr: ptr
152
154
  });
153
155
  }
154
- return numeric;
155
- }
156
- // Date
157
- if (DATE_TIME_RE.test(value)) {
158
- let date = new TomlDate(value);
159
- if (isNaN(date.getTime())) {
160
- throw new TomlError('invalid date', {
156
+ if (isInt && !Number.isSafeInteger(numeric)) {
157
+ throw new TomlError('integer value cannot be represented losslessly', {
161
158
  toml: toml,
162
159
  ptr: ptr
163
160
  });
164
161
  }
165
- return date;
162
+ return numeric;
163
+ }
164
+ let date = new TomlDate(value);
165
+ if (!date.isValid()) {
166
+ throw new TomlError('invalid value', {
167
+ toml: toml,
168
+ ptr: ptr
169
+ });
166
170
  }
167
- throw new TomlError('invalid value', {
168
- toml: toml,
169
- ptr: ptr
170
- });
171
+ return date;
171
172
  }
package/dist/struct.d.ts CHANGED
@@ -26,6 +26,6 @@
26
26
  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
27
  */
28
28
  import { type TomlPrimitive } from './util.js';
29
- export declare function parseKey(str: string, startPtr?: number, endPtr?: number): string[];
29
+ export declare function parseKey(str: string, ptr: number, end?: string): [string[], number];
30
30
  export declare function parseInlineTable(str: string, ptr: number): [Record<string, TomlPrimitive>, number];
31
31
  export declare function parseArray(str: string, ptr: number): [TomlPrimitive[], number];
package/dist/struct.js CHANGED
@@ -26,61 +26,81 @@
26
26
  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
27
  */
28
28
  import { parseString } from './primitive.js';
29
- import { extractValue, extractKeyValue } from './parse.js';
30
- import { indexOfNewline, skipComment } from './util.js';
29
+ import { extractValue } from './parse.js';
30
+ import { skipComment, indexOfNewline, getStringEnd, skipVoid } from './util.js';
31
31
  import TomlError from './error.js';
32
- let KEY_PART_RE = /^[a-zA-Z0-9-_ \t]+$/;
33
- export function parseKey(str, startPtr = 0, endPtr = str.length) {
34
- let ptr;
35
- let dot = -1;
32
+ let KEY_PART_RE = /^[a-zA-Z0-9-_]+[ \t]*$/;
33
+ export function parseKey(str, ptr, end = '=') {
34
+ let dot = ptr - 1;
36
35
  let parsed = [];
37
- let key = str.slice(startPtr, endPtr);
36
+ let endPtr = str.indexOf(end, ptr);
37
+ if (endPtr < 0) {
38
+ throw new TomlError('incomplete key-value: cannot find end of key', {
39
+ toml: str,
40
+ ptr: ptr
41
+ });
42
+ }
38
43
  do {
39
- let c = key[ptr = ++dot];
44
+ let c = str[ptr = ++dot];
40
45
  // If it's whitespace, ignore
41
46
  if (c !== ' ' && c !== '\t') {
42
47
  // If it's a string
43
48
  if (c === '"' || c === "'") {
44
- let eos = key.indexOf(c, ptr + 1) + 1;
45
- if (!eos) {
49
+ if (c === str[ptr + 1] && c === str[ptr + 2]) {
50
+ throw new TomlError('multiline strings are not allowed in keys', {
51
+ toml: str,
52
+ ptr: ptr,
53
+ });
54
+ }
55
+ let eos = getStringEnd(str, ptr);
56
+ if (eos < 0) {
46
57
  throw new TomlError('unfinished string encountered', {
47
58
  toml: str,
48
- ptr: ptr
59
+ ptr: ptr,
49
60
  });
50
61
  }
51
- dot = key.indexOf('.', eos);
52
- let end = key.slice(eos, dot < 0 ? void 0 : dot);
53
- let newLine = indexOfNewline(end);
62
+ dot = str.indexOf('.', eos);
63
+ let strEnd = str.slice(eos, dot < 0 || dot > endPtr ? endPtr : dot);
64
+ let newLine = indexOfNewline(strEnd);
54
65
  if (newLine > -1) {
55
66
  throw new TomlError('newlines are not allowed in keys', {
56
67
  toml: str,
57
- ptr: ptr + dot + newLine
68
+ ptr: ptr + dot + newLine,
58
69
  });
59
70
  }
60
- if (end.trimStart()) {
71
+ if (strEnd.trimStart()) {
61
72
  throw new TomlError('found extra tokens after the string part', {
62
73
  toml: str,
63
- ptr: eos
74
+ ptr: eos,
64
75
  });
65
76
  }
66
- parsed.push(parseString(key, ptr, eos));
77
+ if (endPtr < eos) {
78
+ endPtr = str.indexOf(end, eos);
79
+ if (endPtr < 0) {
80
+ throw new TomlError('incomplete key-value: cannot find end of key', {
81
+ toml: str,
82
+ ptr: ptr,
83
+ });
84
+ }
85
+ }
86
+ parsed.push(parseString(str, ptr, eos));
67
87
  }
68
88
  else {
69
89
  // Normal raw key part consumption and validation
70
- dot = key.indexOf('.', ptr);
71
- let part = key.slice(ptr, dot < 0 ? void 0 : dot);
90
+ dot = str.indexOf('.', ptr);
91
+ let part = str.slice(ptr, dot < 0 || dot > endPtr ? endPtr : dot);
72
92
  if (!KEY_PART_RE.test(part)) {
73
93
  throw new TomlError('only letter, numbers, dashes and underscores are allowed in keys', {
74
94
  toml: str,
75
- ptr: ptr
95
+ ptr: ptr,
76
96
  });
77
97
  }
78
98
  parsed.push(part.trimEnd());
79
99
  }
80
100
  }
81
101
  // Until there's no more dot
82
- } while (dot + 1);
83
- return parsed;
102
+ } while (dot + 1 && dot < endPtr);
103
+ return [parsed, skipVoid(str, endPtr + 1, true, true)];
84
104
  }
85
105
  export function parseInlineTable(str, ptr) {
86
106
  let res = {};
@@ -89,7 +109,7 @@ export function parseInlineTable(str, ptr) {
89
109
  let comma = 0;
90
110
  ptr++;
91
111
  while ((c = str[ptr++]) !== '}' && c) {
92
- if (c === '\n' || c === '\r') {
112
+ if (c === '\n') {
93
113
  throw new TomlError('newlines are not allowed in inline tables', {
94
114
  toml: str,
95
115
  ptr: ptr - 1
@@ -108,7 +128,34 @@ export function parseInlineTable(str, ptr) {
108
128
  });
109
129
  }
110
130
  else if (c !== ' ' && c !== '\t') {
111
- ptr = extractKeyValue(str, ptr - 1, res, seen, true);
131
+ let k;
132
+ let t = res;
133
+ let hasOwn = false;
134
+ let [key, keyEndPtr] = parseKey(str, ptr - 1);
135
+ for (let i = 0; i < key.length; i++) {
136
+ if (i)
137
+ t = hasOwn ? t[k] : (t[k] = {});
138
+ k = key[i];
139
+ if ((hasOwn = Object.hasOwn(t, k)) && (typeof t[k] !== 'object' || seen.has(t[k]))) {
140
+ throw new TomlError('trying to redefine an already defined value', {
141
+ toml: str,
142
+ ptr: ptr
143
+ });
144
+ }
145
+ if (!hasOwn && k === '__proto__') {
146
+ Object.defineProperty(t, k, { enumerable: true, configurable: true, writable: true });
147
+ }
148
+ }
149
+ if (hasOwn) {
150
+ throw new TomlError('trying to redefine an already defined value', {
151
+ toml: str,
152
+ ptr: ptr
153
+ });
154
+ }
155
+ let [value, valueEndPtr] = extractValue(str, keyEndPtr, '}');
156
+ seen.add(value);
157
+ t[k] = value;
158
+ ptr = valueEndPtr;
112
159
  comma = str[ptr - 1] === ',' ? ptr - 1 : 0;
113
160
  }
114
161
  }
package/dist/util.d.ts CHANGED
@@ -32,5 +32,5 @@ export type TomlPrimitive = string | number | boolean | TomlDate | {
32
32
  export declare function indexOfNewline(str: string, start?: number, end?: number): number;
33
33
  export declare function skipComment(str: string, ptr: number): number;
34
34
  export declare function skipVoid(str: string, ptr: number, banNewLines?: boolean, banComments?: boolean): number;
35
- export declare function skipUntil(str: string, ptr: number, end?: string): number;
36
- export declare function peekTable(table: Record<string, any>, key: string[], seen: Set<any>, allowSuper?: boolean): [string, Record<string, any>] | null;
35
+ export declare function skipUntil(str: string, ptr: number, sep: string, end?: string): number;
36
+ export declare function getStringEnd(str: string, seek: number): number;
package/dist/util.js CHANGED
@@ -25,82 +25,76 @@
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 TomlDate from './date.js';
29
28
  import TomlError from './error.js';
30
29
  export function indexOfNewline(str, start = 0, end = str.length) {
31
- for (let i = start; i < end; i++) {
32
- if (str[i] === '\n' || str[i] === '\r')
33
- return i;
34
- }
35
- return -1;
30
+ let idx = str.indexOf('\n', start);
31
+ if (str[idx - 1] === '\r')
32
+ idx--;
33
+ return idx <= end ? idx : -1;
36
34
  }
37
35
  export function skipComment(str, ptr) {
38
36
  for (let i = ptr; i < str.length; i++) {
39
37
  let c = str[i];
40
- if (c < '\t' || c === '\x0b' || c === '\x0c' || c === '\x7f' || (c > '\x0d' && c < '\x20')) {
41
- throw new TomlError('control characters are not allowed in commebts', {
38
+ if (c === '\n')
39
+ return i;
40
+ if (c === '\r' && str[i + 1] === '\n')
41
+ return i + 1;
42
+ if ((c < '\x20' && c !== '\t') || c === '\x7f') {
43
+ throw new TomlError('control characters are not allowed in comments', {
42
44
  toml: str,
43
45
  ptr: ptr,
44
46
  });
45
47
  }
46
- if (c === '\n' || c === '\r')
47
- return i;
48
48
  }
49
- return -1;
49
+ return str.length;
50
50
  }
51
51
  export function skipVoid(str, ptr, banNewLines, banComments) {
52
52
  let c;
53
- while ((c = str[ptr]) === ' ' || c === '\t' || (!banNewLines && (c === '\n' || c === '\r')))
53
+ while ((c = str[ptr]) === ' ' || c === '\t' || (!banNewLines && (c === '\n' || c === '\r' && str[ptr + 1] === '\n')))
54
54
  ptr++;
55
- if (banComments || c !== '#')
56
- return ptr;
57
- ptr = skipComment(str, ptr);
58
- return ptr < 0 ? str.length : skipVoid(str, ptr, banNewLines);
55
+ return banComments || c !== '#'
56
+ ? ptr
57
+ : skipVoid(str, skipComment(str, ptr), banNewLines);
59
58
  }
60
- export function skipUntil(str, ptr, end) {
59
+ export function skipUntil(str, ptr, sep, end) {
61
60
  if (!end) {
62
61
  ptr = indexOfNewline(str, ptr);
63
62
  return ptr < 0 ? str.length : ptr;
64
63
  }
65
- let nextEnd = str.indexOf(end, ptr);
66
- if (nextEnd < 0) {
67
- // TODO: point to start of structure instead?
68
- throw new TomlError('cannot find end of structure', {
69
- toml: str,
70
- ptr: ptr
71
- });
72
- }
73
- let nextSep = str.indexOf(',', ptr) + 1;
74
- return !nextSep || nextEnd < nextSep ? nextEnd : nextSep;
75
- }
76
- let DESCRIPTOR = { enumerable: true, configurable: true, writable: true };
77
- export function peekTable(table, key, seen, allowSuper) {
78
- let k = '';
79
- let v;
80
- let hasOwn;
81
- let hadOwn;
82
- for (let i = 0; i < key.length; i++) {
83
- if (i) {
84
- if (!(hadOwn = hasOwn)) {
85
- if (k === '__proto__')
86
- Object.defineProperty(table, k, DESCRIPTOR);
87
- table[k] = {};
88
- }
89
- table = table[k];
90
- if (Array.isArray(table))
91
- table = table[table.length - 1];
64
+ for (let i = ptr; i < str.length; i++) {
65
+ let c = str[i];
66
+ if (c === '#') {
67
+ i = indexOfNewline(str, i);
68
+ }
69
+ else if (c === sep) {
70
+ return i + 1;
92
71
  }
93
- k = key[i];
94
- hasOwn = Object.hasOwn(table, k);
95
- v = hasOwn ? table[k] : void 0;
96
- if (v !== void 0 && (typeof v !== 'object' || seen.has(v))) {
97
- return null;
72
+ else if (c === end) {
73
+ return i;
98
74
  }
99
75
  }
100
- if (hasOwn && (!allowSuper || (hadOwn && !Array.isArray(v)))) {
101
- return null;
76
+ throw new TomlError('cannot find end of structure', {
77
+ toml: str,
78
+ ptr: ptr
79
+ });
80
+ }
81
+ export function getStringEnd(str, seek) {
82
+ let first = str[seek];
83
+ let target = first === str[seek + 1] && str[seek + 1] === str[seek + 2]
84
+ ? str.slice(seek, seek + 3)
85
+ : first;
86
+ seek += target.length - 1;
87
+ do
88
+ seek = str.indexOf(target, ++seek);
89
+ while (seek > -1 && first !== "'" && str[seek - 1] === '\\' && str[seek - 2] !== '\\');
90
+ if (seek > -1) {
91
+ seek += target.length;
92
+ if (target.length > 1) {
93
+ if (str[seek] === first)
94
+ seek++;
95
+ if (str[seek] === first)
96
+ seek++;
97
+ }
102
98
  }
103
- if (!hasOwn && k === '__proto__')
104
- Object.defineProperty(table, k, DESCRIPTOR);
105
- return [k, table];
99
+ return seek;
106
100
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "smol-toml",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "keywords": [
5
5
  "toml",
6
6
  "parser"
@@ -12,7 +12,7 @@
12
12
  "type": "module",
13
13
  "engines": {
14
14
  "node": ">= 18",
15
- "pnpm": ">= 7.24"
15
+ "pnpm": ">= 8"
16
16
  },
17
17
  "scripts": {
18
18
  "build": "tsc",