diff options
| author | Vincent LE GOFF <g_n_s@hotmail.fr> | 2019-03-28 17:31:15 +0100 |
|---|---|---|
| committer | Ryan Dahl <ry@tinyclouds.org> | 2019-03-28 12:31:15 -0400 |
| commit | 13aeee460a62dc6b49de3a7d5d6305ea51ab5e4c (patch) | |
| tree | 0605d5e3e4b5e7536aa1ea2f7218fb73edded514 /toml/parser.ts | |
| parent | 8f0407efad81ea8c255daf03c0e19b6bae3b88b6 (diff) | |
Add TOML module (denoland/deno_std#300)
Original: https://github.com/denoland/deno_std/commit/fa1664e6ccaad9ad98a131f03fdd600c5fa24100
Diffstat (limited to 'toml/parser.ts')
| -rw-r--r-- | toml/parser.ts | 327 |
1 files changed, 327 insertions, 0 deletions
diff --git a/toml/parser.ts b/toml/parser.ts new file mode 100644 index 000000000..9263f0111 --- /dev/null +++ b/toml/parser.ts @@ -0,0 +1,327 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. +import { existsSync } from "../fs/exists.ts"; +import { deepAssign } from "../util/deep_assign.ts"; + +class KeyValuePair { + key: string; + value: unknown; +} + +class ParserGroup { + type: string; + name: string; + arrValues: unknown[] = []; + objValues: object = {}; +} + +class ParserContext { + currentGroup?: ParserGroup; + output: object = {}; +} + +class Parser { + tomlLines: string[]; + context: ParserContext; + constructor(tomlString: string) { + this.tomlLines = this._split(tomlString); + this.context = new ParserContext(); + } + _sanitize(): void { + const out = []; + for (let i = 0; i < this.tomlLines.length; i++) { + const s = this.tomlLines[i].split("#")[0]; + if (s !== "") { + out.push(s); + } + } + this.tomlLines = out; + this._mergeMultilines(); + } + + _mergeMultilines(): void { + function arrayStart(line: string): boolean { + const reg = /.*=\s*\[/g; + return reg.test(line) && !(line[line.length - 1] === "]"); + } + + function arrayEnd(line: string): boolean { + return line[line.length - 1] === "]"; + } + + function stringStart(line: string): boolean { + const m = line.match(/.*=\s*(?:\"\"\"|''')/); + if (!m) { + return false; + } + return !line.endsWith(`"""`) || !line.endsWith(`'''`); + } + + function stringEnd(line: string): boolean { + return line.endsWith(`'''`) || line.endsWith(`"""`); + } + + function isLiteralString(line: string): boolean { + return line.match(/'''/) ? true : false; + } + + let merged = [], + acc = [], + isLiteral = false, + capture = false, + captureType = "", + merge = false; + + for (let i = 0; i < this.tomlLines.length; i++) { + const line = this.tomlLines[i]; + const trimmed = line.trim(); + if (!capture && arrayStart(trimmed)) { + capture = true; + captureType = "array"; + } else if (!capture && stringStart(trimmed)) { + isLiteral = isLiteralString(trimmed); + capture = true; + captureType = "string"; + } else if (capture && arrayEnd(trimmed)) { + merge = true; + } else if (capture && stringEnd(trimmed)) { + merge = true; + } + + if (capture) { + if (isLiteral) { + acc.push(line); + } else { + acc.push(trimmed); + } + } else { + if (isLiteral) { + merged.push(line); + } else { + merged.push(trimmed); + } + } + + if (merge) { + capture = false; + merge = false; + if (captureType === "string") { + merged.push( + acc + .join("\n") + .replace(/"""/g, '"') + .replace(/'''/g, `'`) + .replace(/\n/g, "\\n") + ); + isLiteral = false; + } else { + merged.push(acc.join("")); + } + captureType = ""; + acc = []; + } + } + this.tomlLines = merged; + } + _unflat(keys: string[], values: object = {}, cObj: object = {}): object { + let out = {}; + if (keys.length === 0) { + return cObj; + } else { + if (Object.keys(cObj).length === 0) { + cObj = values; + } + let key = keys.pop(); + out[key] = cObj; + return this._unflat(keys, values, out); + } + } + _groupToOutput(): void { + const arrProperty = this.context.currentGroup.name + .replace(/"/g, "") + .replace(/'/g, "") + .split("."); + let u = {}; + if (this.context.currentGroup.type === "array") { + u = this._unflat(arrProperty, this.context.currentGroup.arrValues); + } else { + u = this._unflat(arrProperty, this.context.currentGroup.objValues); + } + deepAssign(this.context.output, u); + delete this.context.currentGroup; + } + _split(str: string): string[] { + let out = []; + out.push(...str.split("\n")); + return out; + } + _isGroup(line: string): boolean { + const t = line.trim(); + return t[0] === "[" && t[t.length - 1] === "]"; + } + _isDeclaration(line: string): boolean { + return line.split("=").length > 1; + } + _createGroup(line: string): void { + const captureReg = /\[(.*)\]/; + if (this.context.currentGroup) { + this._groupToOutput(); + } + let g = new ParserGroup(); + g.name = line.match(captureReg)[1]; + if (g.name.match(/\[.*\]/)) { + g.type = "array"; + g.name = g.name.match(captureReg)[1]; + } else { + g.type = "object"; + } + this.context.currentGroup = g; + } + _processDeclaration(line: string): KeyValuePair { + let kv = new KeyValuePair(); + const idx = line.indexOf("="); + kv.key = line.substring(0, idx).trim(); + kv.value = this._parseData(line.slice(idx + 1)); + return kv; + } + _parseData(dataString: string): unknown { + dataString = dataString.trim(); + if (this._isDate(dataString)) { + return new Date(dataString); + } + if (this._isLocalTime(dataString)) { + return eval(`"${dataString}"`); + } + if (dataString === "inf" || dataString === "+inf") { + return Infinity; + } + if (dataString === "-inf") { + return -Infinity; + } + if ( + dataString === "nan" || + dataString === "+nan" || + dataString === "-nan" + ) { + return NaN; + } + // inline table + if (dataString[0] === "{" && dataString[dataString.length - 1] === "}") { + const reg = /([a-zA-Z0-9-_\.]*) (=)/gi; + let result; + while ((result = reg.exec(dataString))) { + let ogVal = result[0]; + let newVal = ogVal + .replace(result[1], `"${result[1]}"`) + .replace(result[2], ":"); + dataString = dataString.replace(ogVal, newVal); + } + // TODO : unflat if necessary + return JSON.parse(dataString); + } + // If binary / octal / hex + if ( + dataString[0] === "0" && + (dataString[1] === "b" || dataString[1] === "o" || dataString[1] === "x") + ) { + return dataString; + } + + if (this._isParsableNumber(dataString)) { + return eval(dataString.replace(/_/g, "")); + } + + // Handle First and last EOL for multiline strings + if (dataString.startsWith(`"\\n`)) { + dataString = dataString.replace(`"\\n`, `"`); + } else if (dataString.startsWith(`'\\n`)) { + dataString = dataString.replace(`'\\n`, `'`); + } + if (dataString.endsWith(`\\n"`)) { + dataString = dataString.replace(`\\n"`, `"`); + } else if (dataString.endsWith(`\\n'`)) { + dataString = dataString.replace(`\\n'`, `'`); + } + + // dataString = dataString.replace(/\\/, "\\\\"); + + return eval(dataString); + } + _isLocalTime(str: string): boolean { + const reg = /(\d{2}):(\d{2}):(\d{2})/; + return reg.test(str); + } + _isParsableNumber(dataString: string): boolean { + let d = dataString.replace(/_/g, ""); + return !isNaN(parseFloat(d)); + } + _isDate(dateStr: string): boolean { + const reg = /\d{4}-\d{2}-\d{2}/; + return reg.test(dateStr); + } + _parseLines(): void { + for (let i = 0; i < this.tomlLines.length; i++) { + const line = this.tomlLines[i]; + + // TODO (zekth) Handle unflat of array of tables + if (this._isGroup(line)) { + // if the current group is an array we push the + // parsed objects in it. + if ( + this.context.currentGroup && + this.context.currentGroup.type === "array" + ) { + this.context.currentGroup.arrValues.push( + this.context.currentGroup.objValues + ); + this.context.currentGroup.objValues = {}; + } + // If we need to create a group or to change group + if ( + !this.context.currentGroup || + (this.context.currentGroup && + this.context.currentGroup.name !== + line.replace(/\[/g, "").replace(/\]/g, "")) + ) { + this._createGroup(line); + continue; + } + } + if (this._isDeclaration(line)) { + let kv = this._processDeclaration(line); + if (!this.context.currentGroup) { + this.context.output[kv.key] = kv.value; + } else { + this.context.currentGroup.objValues[kv.key] = kv.value; + } + } + } + if (this.context.currentGroup) { + if (this.context.currentGroup.type === "array") { + this.context.currentGroup.arrValues.push( + this.context.currentGroup.objValues + ); + } + this._groupToOutput(); + } + } + parse(): object { + this._sanitize(); + this._parseLines(); + return this.context.output; + } +} + +export function parse(tomlString: string): object { + // File is potentially using EOL CRLF + tomlString = tomlString.replace(/\r\n/g, "\n").replace(/\\\n/g, "\n"); + return new Parser(tomlString).parse(); +} + +export function parseFile(filePath: string): object { + if (!existsSync(filePath)) { + throw new Error("File not found"); + } + const decoder = new TextDecoder(); + const strFile = decoder.decode(Deno.readFileSync(filePath)); + return parse(strFile); +} |
