Comparing version 0.1.0 to 0.1.1
{ | ||
"name": "js-toml", | ||
"version": "0.1.0", | ||
"version": "0.1.1", | ||
"description": "A TOML parser for JavaScript/TypeScript, targeting TOML 1.0.0 Spec", | ||
@@ -31,2 +31,10 @@ "keywords": [ | ||
}, | ||
"scripts": { | ||
"build": "tsup src/index.ts --format cjs,esm --dts", | ||
"test": "jest", | ||
"test:cov": "jest --coverage", | ||
"diagram": "esr script/generateDiagram.ts", | ||
"lint": "prettier --check . && eslint .", | ||
"lint:fix": "prettier --write . && eslint . --fix" | ||
}, | ||
"devDependencies": { | ||
@@ -41,2 +49,3 @@ "@types/jest": "^29.2.1", | ||
"eslint-config-prettier": "^8.5.0", | ||
"glob": "^8.0.3", | ||
"jest": "^29.2.2", | ||
@@ -52,11 +61,3 @@ "prettier": "^2.7.1", | ||
}, | ||
"packageManager": "pnpm@7.15.0", | ||
"scripts": { | ||
"build": "tsup src/index.ts --format cjs,esm --dts", | ||
"test": "jest", | ||
"test:cov": "jest --coverage", | ||
"diagram": "esr script/generateDiagram.ts", | ||
"lint": "prettier --check . && eslint .", | ||
"lint:fix": "prettier --write . && eslint . --fix" | ||
} | ||
} | ||
"packageManager": "pnpm@7.15.0" | ||
} |
@@ -6,1 +6,53 @@ # js-toml | ||
[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT) | ||
[![npm version](https://badge.fury.io/js/js-toml.svg)](https://badge.fury.io/js/js-toml) | ||
A TOML parser for JavaScript and TypeScript. Fully tested and 100% compatible with the TOML v1.0.0 spec. | ||
## Installation | ||
```bash | ||
npm install js-toml | ||
``` | ||
or with yarn | ||
```bash | ||
yarn add js-toml | ||
``` | ||
or with pnpm | ||
```bash | ||
pnpm add js-toml | ||
``` | ||
## Usage | ||
```typescript | ||
import {load} from 'js-toml'; | ||
const toml = ` | ||
title = "TOML Example" | ||
[owner] | ||
name = "Tom Preston-Werner" | ||
dob = 1979-05-27T07:32:00-08:00 # First class dates | ||
`; | ||
const data = load(toml); | ||
console.log(data); | ||
``` | ||
## API | ||
### load(toml: string): object | ||
Parses a TOML string and returns a JavaScript object. | ||
### dump(object: object): string | ||
Under development. | ||
## License | ||
MIT |
@@ -1,1 +0,7 @@ | ||
export { load, SyntaxParseError } from './load'; | ||
export { SyntaxParseError } from './load'; | ||
import { load } from './load'; | ||
const jstoml = { load }; | ||
export default jstoml; | ||
export { load }; |
@@ -28,3 +28,5 @@ import { parser } from './parser'; | ||
const tableDeclared = Symbol('tableDeclared'); | ||
const explicitlyDeclared = Symbol('explicitlyDeclared'); | ||
const implicitlyDeclared = Symbol('implicitlyDeclared'); | ||
const notEditable = Symbol('notEditable'); | ||
@@ -178,7 +180,7 @@ | ||
private assignPrimitiveValue(key, value, object) { | ||
if (object[key]) { | ||
if (key in object) { | ||
throw new DuplicateKeyError(); | ||
} | ||
if (isPlainObject(value)) { | ||
value[tableDeclared] = true; | ||
value[explicitlyDeclared] = true; | ||
} | ||
@@ -190,7 +192,14 @@ | ||
private tryCreatingObject(key, object, declareTable, ignoreDeclared) { | ||
private tryCreatingObject( | ||
key, | ||
object, | ||
declareSymbol, | ||
ignoreImplicitDeclared, | ||
ignoreExplicitDeclared | ||
) { | ||
if (object[key]) { | ||
if ( | ||
!isPlainObject(object[key]) || | ||
(!ignoreDeclared && object[key][tableDeclared]) || | ||
(!ignoreExplicitDeclared && object[key][explicitlyDeclared]) || | ||
(!ignoreImplicitDeclared && object[key][implicitlyDeclared]) || | ||
object[key][notEditable] | ||
@@ -202,4 +211,4 @@ ) { | ||
object[key] = {}; | ||
if (declareTable) { | ||
object[key][tableDeclared] = true; | ||
if (declareSymbol) { | ||
object[key][declareSymbol] = true; | ||
} | ||
@@ -214,3 +223,3 @@ } | ||
if (rest.length > 0) { | ||
this.tryCreatingObject(first, object, true, true); | ||
this.tryCreatingObject(first, object, implicitlyDeclared, true, false); | ||
return this.assignValue(rest, value, object[first]); | ||
@@ -226,9 +235,18 @@ } | ||
if (Array.isArray(object[first])) { | ||
if (object[first][notEditable]) { | ||
throw new DuplicateKeyError(); | ||
} | ||
const toAdd = object[first][object[first].length - 1]; | ||
return this.createTable(rest, toAdd); | ||
} | ||
this.tryCreatingObject(first, object, false, true); | ||
this.tryCreatingObject(first, object, null, true, true); | ||
return this.createTable(rest, object[first]); | ||
} | ||
return this.tryCreatingObject(first, object, true, false); | ||
return this.tryCreatingObject( | ||
first, | ||
object, | ||
explicitlyDeclared, | ||
false, | ||
false | ||
); | ||
} | ||
@@ -243,3 +261,3 @@ | ||
} | ||
this.tryCreatingObject(first, object, false, true); | ||
this.tryCreatingObject(first, object, null, true, true); | ||
return this.getOrCreateArray(rest, object[first]); | ||
@@ -246,0 +264,0 @@ } |
@@ -24,2 +24,3 @@ import { CstParser } from 'chevrotain'; | ||
import { ArrayTableOpen } from './tokens/ArrayTableOpen'; | ||
import { ExpressionNewLine } from './tokens/ExpressionNewLine'; | ||
@@ -40,2 +41,7 @@ class Parser extends CstParser { | ||
}); | ||
private keyValue = this.RULE('keyValue', () => { | ||
this.SUBRULE(this.key); | ||
this.CONSUME(KeyValueSeparator); | ||
this.SUBRULE(this.value); | ||
}); | ||
private inlineTableKeyValues = this.RULE('inlineTableKeyValues', () => { | ||
@@ -52,2 +58,16 @@ this.MANY_SEP({ | ||
}); | ||
private arrayValues = this.RULE('arrayValues', () => { | ||
this.SUBRULE(this.value); | ||
let havingMore = true; | ||
this.MANY({ | ||
GATE: () => havingMore, | ||
DEF: () => { | ||
this.CONSUME(ArraySep); | ||
const found = this.OPTION(() => this.SUBRULE1(this.value)); | ||
if (!found) { | ||
havingMore = false; | ||
} | ||
}, | ||
}); | ||
}); | ||
private array = this.RULE('array', () => { | ||
@@ -69,21 +89,2 @@ this.CONSUME(ArrayOpen); | ||
}); | ||
private keyValue = this.RULE('keyValue', () => { | ||
this.SUBRULE(this.key); | ||
this.CONSUME(KeyValueSeparator); | ||
this.SUBRULE(this.value); | ||
}); | ||
private arrayValues = this.RULE('arrayValues', () => { | ||
this.SUBRULE(this.value); | ||
let havingMore = true; | ||
this.MANY({ | ||
GATE: () => havingMore, | ||
DEF: () => { | ||
this.CONSUME(ArraySep); | ||
const found = this.OPTION(() => this.SUBRULE1(this.value)); | ||
if (!found) { | ||
havingMore = false; | ||
} | ||
}, | ||
}); | ||
}); | ||
private stdTable = this.RULE('stdTable', () => { | ||
@@ -112,8 +113,10 @@ this.CONSUME(StdTableOpen); | ||
toml = this.RULE('toml', () => { | ||
this.OPTION(() => this.SUBRULE(this.expression)); | ||
this.MANY(() => { | ||
this.CONSUME(Newline); | ||
this.MANY(() => this.CONSUME(ExpressionNewLine)); | ||
this.MANY1(() => { | ||
this.SUBRULE1(this.expression); | ||
this.OPTION2(() => { | ||
this.CONSUME1(Newline); | ||
this.MANY3(() => this.CONSUME2(ExpressionNewLine)); | ||
}); | ||
}); | ||
this.OPTION1(() => this.CONSUME1(Newline)); | ||
}); | ||
@@ -120,0 +123,0 @@ |
@@ -23,3 +23,3 @@ import { WhiteSpace } from './WhiteSpace'; | ||
import { ArrayClose } from './ArrayClose'; | ||
import { IgnoredNewline } from './IgnoredNewline'; | ||
import { ArrayNewline } from './ArrayNewline'; | ||
import { InlineTableSep } from './InlineTableSep'; | ||
@@ -32,2 +32,3 @@ import { SimpleKey } from './SimpleKey'; | ||
import { ArrayTableClose } from './ArrayTableClose'; | ||
import { ExpressionNewLine } from './ExpressionNewLine'; | ||
@@ -64,3 +65,3 @@ const keyTokens = [ | ||
Comment, | ||
IgnoredNewline, | ||
ExpressionNewLine, | ||
KeyValueSeparator, | ||
@@ -74,3 +75,3 @@ ArrayTableOpen, | ||
[Mode.Value]: [...valueTokens, Newline, InlineTableSep, InlineTableClose], | ||
[Mode.Array]: [...valueTokens, IgnoredNewline, ArraySep, ArrayClose], | ||
[Mode.Array]: [...valueTokens, ArrayNewline, ArraySep, ArrayClose], | ||
[Mode.InlineTable]: [...keyTokens, InlineTableKeyValSep, InlineTableClose], | ||
@@ -77,0 +78,0 @@ }, |
import { createToken } from 'chevrotain'; | ||
import { nonAscii } from './patterns'; | ||
import XRegExp = require('xregexp'); | ||
import { nonAscii } from './patterns'; | ||
const commentStartChar = /#/; | ||
const nonEol = XRegExp.build('\t|[\x20-\x7F]|{{nonAscii}}', { nonAscii }); | ||
const nonEol = XRegExp.build('\t|[\x20-\x7E]|{{nonAscii}}', { nonAscii }); | ||
const comment = XRegExp.build('{{commentStartChar}}{{nonEol}}*', { | ||
@@ -8,0 +8,0 @@ commentStartChar, |
@@ -5,2 +5,3 @@ import XRegExp = require('xregexp'); | ||
import { registerTokenInterpreter } from './tokenInterpreters'; | ||
import { SyntaxParseError } from '../exception'; | ||
@@ -70,3 +71,60 @@ const dateFullYear = XRegExp.build('{{digit}}{4}', { digit }); | ||
const isValidDate = (value: string) => { | ||
const datePattern = XRegExp.build( | ||
'({{dateFullYear}})-({{dateMonth}})-({{dateMDay}})', | ||
{ | ||
dateFullYear, | ||
dateMonth, | ||
dateMDay, | ||
} | ||
); | ||
const date = XRegExp.exec(value, datePattern); | ||
if (date) { | ||
const year = Number(date[1]); | ||
const month = Number(date[2]); | ||
const day = Number(date[3]); | ||
const dateObject = new Date(year, month - 1, day); | ||
return ( | ||
dateObject.getFullYear() === year && | ||
dateObject.getMonth() + 1 === month && | ||
dateObject.getDate() === day | ||
); | ||
} | ||
return true; | ||
}; | ||
const isValidTime = (value: string) => { | ||
const timePattern = XRegExp.build( | ||
'({{timeHour}}):({{timeMinute}}):({{timeSecond}})', | ||
{ | ||
timeHour, | ||
timeMinute, | ||
timeSecond, | ||
} | ||
); | ||
const time = XRegExp.exec(value, timePattern); | ||
if (time) { | ||
const hour = Number(time[1]); | ||
const minute = Number(time[2]); | ||
const second = Number(time[3]); | ||
const dateObject = new Date(0, 0, 0, hour, minute, second); | ||
return ( | ||
dateObject.getHours() === hour && | ||
dateObject.getMinutes() === minute && | ||
dateObject.getSeconds() === second | ||
); | ||
} | ||
return true; | ||
}; | ||
const isValidDateTime = (value: string) => | ||
isValidDate(value) && isValidTime(value); | ||
registerTokenInterpreter(DateTime, (raw) => { | ||
if (!isValidDateTime(raw)) { | ||
throw new SyntaxParseError(`Invalid date time: ${raw}`); | ||
} | ||
const onlyTime = raw.match(localTime)?.index === 0; | ||
@@ -73,0 +131,0 @@ if (onlyTime) { |
@@ -80,9 +80,5 @@ import { createToken } from 'chevrotain'; | ||
const skipWhitespaceIfFindBackslash = (string) => | ||
string.replace(/\\[ \t]*(\r\n|\n)+[ \t]*/g, ''); | ||
registerTokenInterpreter(MultiLineBasicString, (raw) => { | ||
const content = getMultiLineContent(raw); | ||
const result = skipWhitespaceIfFindBackslash(content); | ||
return unescapeString(result); | ||
return unescapeString(content); | ||
}); |
@@ -0,1 +1,18 @@ | ||
import XRegExp = require('xregexp'); | ||
import { newline, whiteSpaceChar } from './patterns'; | ||
import { SyntaxParseError } from '../exception'; | ||
const escapingWhitespaces = XRegExp.build( | ||
'^{{whiteSpaceChar}}*{{newline}}(?:{{whiteSpaceChar}}|{{newline}})*', | ||
{ | ||
whiteSpaceChar, | ||
newline, | ||
} | ||
); | ||
const getWhitespaceAndNewline = (str: string) => { | ||
const match = XRegExp.exec(str, escapingWhitespaces); | ||
return match ? match[0].length : 0; | ||
}; | ||
export const unescapeString = (string) => { | ||
@@ -7,2 +24,9 @@ let result = ''; | ||
i++; | ||
const whitespaceAndNewline = getWhitespaceAndNewline(string.slice(i)); | ||
if (whitespaceAndNewline > 0) { | ||
i += whitespaceAndNewline - 1; | ||
continue; | ||
} | ||
switch (string[i]) { | ||
@@ -30,14 +54,23 @@ case 'b': | ||
break; | ||
case 'u': | ||
result += String.fromCharCode( | ||
parseInt(string.substring(i + 1, i + 5), 16) | ||
); | ||
case 'u': { | ||
const hex = string.slice(i + 1, i + 5); | ||
const codePoint = parseInt(hex, 16); | ||
if (codePoint > 0xd7ff && codePoint < 0xe000) { | ||
throw new SyntaxParseError(`Invalid Unicode code point: \\u${hex}`); | ||
} | ||
result += String.fromCodePoint(codePoint); | ||
i += 4; | ||
break; | ||
case 'U': | ||
result += String.fromCodePoint( | ||
parseInt(string.substring(i + 1, i + 9), 16) | ||
); | ||
} | ||
case 'U': { | ||
const hex = string.slice(i + 1, i + 9); | ||
const codePoint = parseInt(hex, 16); | ||
if (codePoint > 0x10ffff) { | ||
throw new SyntaxParseError(`Invalid Unicode code point: \\U${hex}`); | ||
} | ||
result += String.fromCodePoint(codePoint); | ||
i += 8; | ||
break; | ||
} | ||
case string.match(/^[0-7]{1,3}$/): | ||
} | ||
@@ -44,0 +77,0 @@ } else { |
105604
54
3490
58
14