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