Comparing version 1.0.1 to 1.1.0
@@ -52,2 +52,4 @@ /*! | ||
date = date.toUpperCase(); | ||
if (!offset) | ||
date += 'Z'; | ||
} | ||
@@ -54,0 +56,0 @@ } |
@@ -28,4 +28,5 @@ /*! | ||
*/ | ||
import { type TomlPrimitive } from './util.js'; | ||
export { default as TomlError } from './error.js'; | ||
export { default as TomlDate } from './date.js'; | ||
export declare function parse(toml: string): Record<string, TomlPrimitive>; | ||
export { parse } from './parse.js'; | ||
export { stringify } from './stringify.js'; |
@@ -28,123 +28,5 @@ /*! | ||
*/ | ||
import { parseKey } from './struct.js'; | ||
import { extractValue } from './parse.js'; | ||
import { skipVoid } from './util.js'; | ||
import TomlError from './error.js'; | ||
export { default as TomlError } from './error.js'; | ||
export { default as TomlDate } from './date.js'; | ||
function peekTable(key, table, meta, type) { | ||
let t = table; | ||
let m = meta; | ||
let k; | ||
let hasOwn = false; | ||
let state; | ||
for (let i = 0; i < key.length; i++) { | ||
if (i) { | ||
t = hasOwn ? t[k] : (t[k] = {}); | ||
m = (state = m[k]).c; | ||
if (type === 0 /* Type.DOTTED */ && state.t === 1 /* Type.EXPLICIT */) { | ||
return null; | ||
} | ||
if (state.t === 2 /* Type.ARRAY */) { | ||
let l = t.length - 1; | ||
t = t[l]; | ||
m = m[l].c; | ||
} | ||
} | ||
k = key[i]; | ||
if ((hasOwn = Object.hasOwn(t, k)) && m[k]?.t === 0 /* Type.DOTTED */ && m[k]?.d) { | ||
return null; | ||
} | ||
if (!hasOwn) { | ||
if (k === '__proto__') { | ||
Object.defineProperty(t, k, { enumerable: true, configurable: true, writable: true }); | ||
Object.defineProperty(m, k, { enumerable: true, configurable: true, writable: true }); | ||
} | ||
m[k] = { | ||
t: i < key.length - 1 && type === 2 /* Type.ARRAY */ | ||
? 0 /* Type.DOTTED */ | ||
: type, | ||
d: false, | ||
i: 0, | ||
c: {}, | ||
}; | ||
} | ||
} | ||
state = m[k]; | ||
if (state.t !== type) { | ||
// Bad key type! | ||
return null; | ||
} | ||
if (type === 2 /* Type.ARRAY */) { | ||
if (!state.d) { | ||
state.d = true; | ||
t[k] = []; | ||
} | ||
t[k].push(t = {}); | ||
state.c[state.i++] = (state = { t: 1 /* Type.EXPLICIT */, d: false, i: 0, c: {} }); | ||
} | ||
if (state.d) { | ||
// Redefining a table! | ||
return null; | ||
} | ||
state.d = true; | ||
if (type === 1 /* Type.EXPLICIT */) { | ||
t = hasOwn ? t[k] : (t[k] = {}); | ||
} | ||
else if (type === 0 /* Type.DOTTED */ && hasOwn) { | ||
return null; | ||
} | ||
return [k, t, state.c]; | ||
} | ||
export function parse(toml) { | ||
let res = {}; | ||
let meta = {}; | ||
let tbl = res; | ||
let m = meta; | ||
for (let ptr = skipVoid(toml, 0); ptr < toml.length;) { | ||
if (toml[ptr] === '[') { | ||
let isTableArray = toml[++ptr] === '['; | ||
let k = parseKey(toml, ptr += +isTableArray, ']'); | ||
if (isTableArray) { | ||
if (toml[k[1] - 1] !== ']') { | ||
throw new TomlError('expected end of table declaration', { | ||
toml: toml, | ||
ptr: k[1] - 1, | ||
}); | ||
} | ||
k[1]++; | ||
} | ||
let p = peekTable(k[0], res, meta, isTableArray ? 2 /* Type.ARRAY */ : 1 /* Type.EXPLICIT */); | ||
if (!p) { | ||
throw new TomlError('trying to redefine an already defined table or value', { | ||
toml: toml, | ||
ptr: ptr, | ||
}); | ||
} | ||
m = p[2]; | ||
tbl = p[1]; | ||
ptr = k[1]; | ||
} | ||
else { | ||
let k = parseKey(toml, ptr); | ||
let p = peekTable(k[0], tbl, m, 0 /* Type.DOTTED */); | ||
if (!p) { | ||
throw new TomlError('trying to redefine an already defined table or value', { | ||
toml: toml, | ||
ptr: ptr, | ||
}); | ||
} | ||
let v = extractValue(toml, k[1]); | ||
p[1][p[0]] = v[0]; | ||
ptr = v[1]; | ||
} | ||
ptr = skipVoid(toml, ptr, true); | ||
if (toml[ptr] && toml[ptr] !== '\n' && toml[ptr] !== '\r') { | ||
throw new TomlError('each key-value declaration must be followed by an end-of-line', { | ||
toml: toml, | ||
ptr: ptr | ||
}); | ||
} | ||
ptr = skipVoid(toml, ptr); | ||
} | ||
return res; | ||
} | ||
export { parse } from './parse.js'; | ||
export { stringify } from './stringify.js'; |
@@ -29,2 +29,2 @@ /*! | ||
import { type TomlPrimitive } from './util.js'; | ||
export declare function extractValue(str: string, ptr: number, end?: string): [TomlPrimitive, number]; | ||
export declare function parse(toml: string): Record<string, TomlPrimitive>; |
@@ -28,66 +28,122 @@ /*! | ||
*/ | ||
import { parseString, parseValue } from './primitive.js'; | ||
import { parseArray, parseInlineTable } from './struct.js'; | ||
import { indexOfNewline, skipVoid, skipUntil, skipComment, getStringEnd } from './util.js'; | ||
import { parseKey } from './struct.js'; | ||
import { extractValue } from './extract.js'; | ||
import { skipVoid } from './util.js'; | ||
import TomlError from './error.js'; | ||
function sliceAndTrimEndOf(str, startPtr, endPtr, allowNewLines) { | ||
let value = str.slice(startPtr, endPtr); | ||
let commentIdx = value.indexOf('#'); | ||
if (commentIdx > -1) { | ||
// The call to skipComment allows to "validate" the comment | ||
// (absence of control characters) | ||
skipComment(str, commentIdx); | ||
value = value.slice(0, commentIdx); | ||
function peekTable(key, table, meta, type) { | ||
let t = table; | ||
let m = meta; | ||
let k; | ||
let hasOwn = false; | ||
let state; | ||
for (let i = 0; i < key.length; i++) { | ||
if (i) { | ||
t = hasOwn ? t[k] : (t[k] = {}); | ||
m = (state = m[k]).c; | ||
if (type === 0 /* Type.DOTTED */ && state.t === 1 /* Type.EXPLICIT */) { | ||
return null; | ||
} | ||
if (state.t === 2 /* Type.ARRAY */) { | ||
let l = t.length - 1; | ||
t = t[l]; | ||
m = m[l].c; | ||
} | ||
} | ||
k = key[i]; | ||
if ((hasOwn = Object.hasOwn(t, k)) && m[k]?.t === 0 /* Type.DOTTED */ && m[k]?.d) { | ||
return null; | ||
} | ||
if (!hasOwn) { | ||
if (k === '__proto__') { | ||
Object.defineProperty(t, k, { enumerable: true, configurable: true, writable: true }); | ||
Object.defineProperty(m, k, { enumerable: true, configurable: true, writable: true }); | ||
} | ||
m[k] = { | ||
t: i < key.length - 1 && type === 2 /* Type.ARRAY */ | ||
? 0 /* Type.DOTTED */ | ||
: type, | ||
d: false, | ||
i: 0, | ||
c: {}, | ||
}; | ||
} | ||
} | ||
let trimmed = value.trimEnd(); | ||
if (!allowNewLines) { | ||
let newlineIdx = value.indexOf('\n', trimmed.length); | ||
if (newlineIdx > -1) { | ||
throw new TomlError('newlines are not allowed in inline tables', { | ||
toml: str, | ||
ptr: startPtr + newlineIdx | ||
}); | ||
state = m[k]; | ||
if (state.t !== type) { | ||
// Bad key type! | ||
return null; | ||
} | ||
if (type === 2 /* Type.ARRAY */) { | ||
if (!state.d) { | ||
state.d = true; | ||
t[k] = []; | ||
} | ||
t[k].push(t = {}); | ||
state.c[state.i++] = (state = { t: 1 /* Type.EXPLICIT */, d: false, i: 0, c: {} }); | ||
} | ||
return [trimmed, commentIdx]; | ||
if (state.d) { | ||
// Redefining a table! | ||
return null; | ||
} | ||
state.d = true; | ||
if (type === 1 /* Type.EXPLICIT */) { | ||
t = hasOwn ? t[k] : (t[k] = {}); | ||
} | ||
else if (type === 0 /* Type.DOTTED */ && hasOwn) { | ||
return null; | ||
} | ||
return [k, t, state.c]; | ||
} | ||
export function extractValue(str, ptr, end) { | ||
let c = str[ptr]; | ||
if (c === '[' || c === '{') { | ||
let [value, endPtr] = c === '[' | ||
? parseArray(str, ptr) | ||
: parseInlineTable(str, ptr); | ||
let newPtr = skipUntil(str, endPtr, ',', end); | ||
if (end === '}') { | ||
let nextNewLine = indexOfNewline(str, endPtr, newPtr); | ||
if (nextNewLine > -1) { | ||
throw new TomlError('newlines are not allowed in inline tables', { | ||
toml: str, | ||
ptr: nextNewLine | ||
export function parse(toml) { | ||
let res = {}; | ||
let meta = {}; | ||
let tbl = res; | ||
let m = meta; | ||
for (let ptr = skipVoid(toml, 0); ptr < toml.length;) { | ||
if (toml[ptr] === '[') { | ||
let isTableArray = toml[++ptr] === '['; | ||
let k = parseKey(toml, ptr += +isTableArray, ']'); | ||
if (isTableArray) { | ||
if (toml[k[1] - 1] !== ']') { | ||
throw new TomlError('expected end of table declaration', { | ||
toml: toml, | ||
ptr: k[1] - 1, | ||
}); | ||
} | ||
k[1]++; | ||
} | ||
let p = peekTable(k[0], res, meta, isTableArray ? 2 /* Type.ARRAY */ : 1 /* Type.EXPLICIT */); | ||
if (!p) { | ||
throw new TomlError('trying to redefine an already defined table or value', { | ||
toml: toml, | ||
ptr: ptr, | ||
}); | ||
} | ||
m = p[2]; | ||
tbl = p[1]; | ||
ptr = k[1]; | ||
} | ||
return [value, newPtr]; | ||
else { | ||
let k = parseKey(toml, ptr); | ||
let p = peekTable(k[0], tbl, m, 0 /* Type.DOTTED */); | ||
if (!p) { | ||
throw new TomlError('trying to redefine an already defined table or value', { | ||
toml: toml, | ||
ptr: ptr, | ||
}); | ||
} | ||
let v = extractValue(toml, k[1]); | ||
p[1][p[0]] = v[0]; | ||
ptr = v[1]; | ||
} | ||
ptr = skipVoid(toml, ptr, true); | ||
if (toml[ptr] && toml[ptr] !== '\n' && toml[ptr] !== '\r') { | ||
throw new TomlError('each key-value declaration must be followed by an end-of-line', { | ||
toml: toml, | ||
ptr: ptr | ||
}); | ||
} | ||
ptr = skipVoid(toml, ptr); | ||
} | ||
let endPtr; | ||
if (c === '"' || c === "'") { | ||
endPtr = getStringEnd(str, ptr); | ||
return [parseString(str, ptr, endPtr), endPtr + +(!!end && str[endPtr] === ',')]; | ||
} | ||
endPtr = skipUntil(str, ptr, ',', end); | ||
let slice = sliceAndTrimEndOf(str, ptr, endPtr - (+(str[endPtr - 1] === ',')), end === ']'); | ||
if (!slice[0]) { | ||
throw new TomlError('incomplete key-value declaration: no value specified', { | ||
toml: str, | ||
ptr: ptr | ||
}); | ||
} | ||
if (end && slice[1] > -1) { | ||
endPtr = skipVoid(str, ptr + slice[1]); | ||
endPtr += +(str[endPtr] === ','); | ||
} | ||
return [ | ||
parseValue(slice[0], str, ptr), | ||
endPtr, | ||
]; | ||
return res; | ||
} |
@@ -138,6 +138,6 @@ /*! | ||
return NaN; | ||
if (value === '-0') | ||
return 0; // Avoid FP representation of -0 | ||
// Numbers | ||
let isInt; | ||
if (value === '-0') | ||
return 0; // Avoid FP representation of -0 | ||
if ((isInt = INT_REGEX.test(value)) || FLOAT_REGEX.test(value)) { | ||
@@ -144,0 +144,0 @@ if (LEADING_ZERO.test(value)) { |
@@ -29,3 +29,3 @@ /*! | ||
import { parseString } from './primitive.js'; | ||
import { extractValue } from './parse.js'; | ||
import { extractValue } from './extract.js'; | ||
import { skipComment, indexOfNewline, getStringEnd, skipVoid } from './util.js'; | ||
@@ -32,0 +32,0 @@ import TomlError from './error.js'; |
{ | ||
"name": "smol-toml", | ||
"version": "1.0.1", | ||
"version": "1.1.0", | ||
"keywords": [ | ||
"toml", | ||
"parser" | ||
"parser", | ||
"serializer" | ||
], | ||
"description": "A small, fast, and correct TOML parser", | ||
"description": "A small, fast, and correct TOML parser/serializer", | ||
"repository": "git@github.com:squirrelchat/smol-toml.git", | ||
@@ -10,0 +11,0 @@ "author": "Cynthia <cyyynthia@borkenware.com>", |
@@ -6,3 +6,3 @@ # smol-toml | ||
A small, fast, and correct TOML parser. smol-toml is fully(ish) spec-compliant with TOML v1.0.0. | ||
A small, fast, and correct TOML parser and serializer. smol-toml is fully(ish) spec-compliant with TOML v1.0.0. | ||
@@ -27,2 +27,3 @@ Why yet another TOML parser? Well, the ecosystem of TOML parsers in JavaScript is quite underwhelming, most likely due | ||
<summary>List of failed `toml-test` cases</summary> | ||
These tests were done by modifying `primitive.ts` and make the implementation return bigints for integers. This allows | ||
@@ -33,3 +34,3 @@ verifying the parser correctly intents a number to be an integer or a float. | ||
The following tests are failing: | ||
The following parse tests are failing: | ||
- invalid/encoding/bad-utf8-in-comment | ||
@@ -50,3 +51,3 @@ - invalid/encoding/bad-utf8-in-multiline-literal | ||
```js | ||
import { parse } from 'smol-toml' | ||
import { parse, stringify } from 'smol-toml' | ||
@@ -56,4 +57,58 @@ const doc = '...' | ||
console.log(parsed) | ||
const toml = stringify(parsed) | ||
console.log(toml) | ||
``` | ||
A few notes on the `stringify` function: | ||
- `undefined` and `null` values on objects are ignored (does not produce a key/value). | ||
- `undefined` and `null` values in arrays are **rejected**. | ||
- Functions, classes and symbols are **rejected**. | ||
- floats will be serialized as integers if they don't have a decimal part. | ||
- `stringify(parse('a = 1.0')) === 'a = 1'` | ||
- JS `Date` will be serialized as Offset Date Time | ||
- Use the [`TomlDate` object](#dates) for representing other types. | ||
### Dates | ||
`smol-toml` uses an extended `Date` object to represent all types of TOML Dates. In the future, `smol-toml` will use | ||
objects from the Temporal proposal, but for now we're stuck with the legacy Date object. | ||
```js | ||
import { TomlDate } from 'smol-toml' | ||
// Offset Date Time | ||
const date = new TomlDate('1979-05-27T07:32:00.000-08:00') | ||
console.log(date.isDateTime(), date.isDate(), date.isTime(), date.isLocal()) // ~> true, false, false, false | ||
console.log(date.toISOString()) // ~> 1979-05-27T07:32:00.000-08:00 | ||
// Local Date Time | ||
const date = new TomlDate('1979-05-27T07:32:00.000') | ||
console.log(date.isDateTime(), date.isDate(), date.isTime(), date.isLocal()) // ~> true, false, false, true | ||
console.log(date.toISOString()) // ~> 1979-05-27T07:32:00.000 | ||
// Local Date | ||
const date = new TomlDate('1979-05-27') | ||
console.log(date.isDateTime(), date.isDate(), date.isTime(), date.isLocal()) // ~> false, true, false, true | ||
console.log(date.toISOString()) // ~> 1979-05-27 | ||
// Local Time | ||
const date = new TomlDate('07:32:00') | ||
console.log(date.isDateTime(), date.isDate(), date.isTime(), date.isLocal()) // ~> false, false, true, true | ||
console.log(date.toISOString()) // ~> 07:32:00.000 | ||
``` | ||
You can also wrap a native `Date` object and specify using different methods depending on the type of date you wish | ||
to represent: | ||
```js | ||
import { TomlDate } from 'smol-toml' | ||
const jsDate = new Date() | ||
const offsetDateTime = TomlDate.wrapAsOffsetDateTime(jsDate) | ||
const localDateTime = TomlDate.wrapAsLocalDateTime(jsDate) | ||
const localDate = TomlDate.wrapAsLocalDate(jsDate) | ||
const localTime = TomlDate.wrapAsLocalTime(jsDate) | ||
``` | ||
## Performance | ||
@@ -69,11 +124,16 @@ A note on these performance numbers: in some highly synthetic tests, other parsers such as `fast-toml` greatly | ||
| | smol-toml | @iarna/toml@3.0.0 | @ltd/j-toml | fast-toml | | ||
|----------------|---------------------|-------------------|----------------|----------------| | ||
| Spec example | **71,356.51 op/s** | 33,629.31 op/s | 16,433.86 op/s | 29,421.60 op/s | | ||
| ~5MB test file | **3.8091 op/s** | *DNF* | 2.4369 op/s | 2.6078 op/s | | ||
| **Parse** | smol-toml | @iarna/toml@3.0.0 | @ltd/j-toml | fast-toml | | ||
|----------------|---------------------|-------------------|-----------------|-----------------| | ||
| Spec example | **71,356.51 op/s** | 33,629.31 op/s | 16,433.86 op/s | 29,421.60 op/s | | ||
| ~5MB test file | **3.8091 op/s** | *DNF* | 2.4369 op/s | 2.6078 op/s | | ||
| **Stringify** | smol-toml | @iarna/toml@3.0.0 | @ltd/j-toml | | ||
|----------------|----------------------|-------------------|----------------| | ||
| Spec example | **195,191.99 op/s** | 46,583.07 op/s | 5,670.12 op/s | | ||
| ~5MB test file | **14.6709 op/s** | 3.5941 op/s | 0.7856 op/s | | ||
<details> | ||
<summary>Detailed benchmark data</summary> | ||
Tests ran using Vitest v0.31.0 on commit 04d233e351f9ae719222154ee2217aea8b95dbab | ||
Tests ran using Vitest v0.31.0 on commit f58cb6152e667e9cea09f31c93d90652e3b82bf5 | ||
@@ -96,2 +156,12 @@ CPU: Intel Core i7 7700K (4.2GHz) | ||
· fast-toml 2.6078 373.88 412.79 383.47 388.62 412.79 412.79 412.79 ±2.72% 10 | ||
✓ bench/stringifySpecExample.bench.ts (3) 1886ms | ||
name hz min max mean p75 p99 p995 p999 rme samples | ||
· smol-toml 195,191.99 0.0047 0.2704 0.0051 0.0050 0.0099 0.0110 0.0152 ±0.41% 97596 fastest | ||
· @iarna/toml 46,583.07 0.0197 0.2808 0.0215 0.0208 0.0448 0.0470 0.1704 ±0.47% 23292 | ||
· @ltd/j-toml 5,670.12 0.1613 0.5768 0.1764 0.1726 0.3036 0.3129 0.4324 ±0.56% 2836 slowest | ||
✓ bench/stringifyLargeMixed.bench.ts (3) 24057ms | ||
name hz min max mean p75 p99 p995 p999 rme samples | ||
· smol-toml 14.6709 65.1071 79.2199 68.1623 67.1088 79.2199 79.2199 79.2199 ±5.25% 10 fastest | ||
· @iarna/toml 3.5941 266.48 295.24 278.24 290.10 295.24 295.24 295.24 ±2.83% 10 | ||
· @ltd/j-toml 0.7856 1,254.33 1,322.05 1,272.87 1,286.82 1,322.05 1,322.05 1,322.05 ±1.37% 10 slowest | ||
@@ -109,2 +179,10 @@ | ||
4.34x faster than @ltd/j-toml | ||
smol-toml - bench/stringifyLargeMixed.bench.ts > | ||
4.00x faster than @iarna/toml | ||
18.33x faster than @ltd/j-toml | ||
smol-toml - bench/stringifySpecExample.bench.ts > | ||
4.19x faster than @iarna/toml | ||
34.42x faster than @ltd/j-toml | ||
``` | ||
@@ -122,5 +200,5 @@ | ||
For the reference anyways, `toml-nodejs` (with proper imports) is ~8x slower on both benchmark with: | ||
For the reference anyways, `toml-nodejs` (with proper imports) is ~8x slower on both parse benchmark with: | ||
- spec example: 7,543.47 op/s | ||
- 5mb mixed: 0.7006 op/s | ||
</details> |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
70529
21
1374
196