summaryrefslogtreecommitdiff
path: root/std/encoding/toml.ts
diff options
context:
space:
mode:
Diffstat (limited to 'std/encoding/toml.ts')
-rw-r--r--std/encoding/toml.ts565
1 files changed, 565 insertions, 0 deletions
diff --git a/std/encoding/toml.ts b/std/encoding/toml.ts
new file mode 100644
index 000000000..0cbd51ba0
--- /dev/null
+++ b/std/encoding/toml.ts
@@ -0,0 +1,565 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { deepAssign } from "../util/deep_assign.ts";
+import { pad } from "../strings/pad.ts";
+
+class KeyValuePair {
+ constructor(public key: string, public value: unknown) {}
+}
+
+class ParserGroup {
+ arrValues: unknown[] = [];
+ objValues: Record<string, unknown> = {};
+
+ constructor(public type: string, public name: string) {}
+}
+
+class ParserContext {
+ currentGroup?: ParserGroup;
+ output: Record<string, unknown> = {};
+}
+
+class Parser {
+ tomlLines: string[];
+ context: ParserContext;
+ constructor(tomlString: string) {
+ this.tomlLines = this._split(tomlString);
+ this.context = new ParserContext();
+ }
+ _sanitize(): void {
+ const out: string[] = [];
+ for (let i = 0; i < this.tomlLines.length; i++) {
+ const s = this.tomlLines[i];
+ const trimmed = s.trim();
+ if (trimmed !== "" && trimmed[0] !== "#") {
+ 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;
+ }
+
+ const merged = [];
+ let 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 {
+ const out: Record<string, unknown> = {};
+ if (keys.length === 0) {
+ return cObj;
+ } else {
+ if (Object.keys(cObj).length === 0) {
+ cObj = values;
+ }
+ const key: string | undefined = keys.pop();
+ if (key) {
+ 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[] {
+ const out = [];
+ out.push(...str.split("\n"));
+ return out;
+ }
+ _isGroup(line: string): boolean {
+ const t = line.trim();
+ return t[0] === "[" && /\[(.*)\]/.exec(t) ? true : false;
+ }
+ _isDeclaration(line: string): boolean {
+ return line.split("=").length > 1;
+ }
+ _createGroup(line: string): void {
+ const captureReg = /\[(.*)\]/;
+ if (this.context.currentGroup) {
+ this._groupToOutput();
+ }
+
+ let type;
+ let name = line.match(captureReg)![1];
+ if (name.match(/\[.*\]/)) {
+ type = "array";
+ name = name.match(captureReg)![1];
+ } else {
+ type = "object";
+ }
+ this.context.currentGroup = new ParserGroup(type, name);
+ }
+ _processDeclaration(line: string): KeyValuePair {
+ const idx = line.indexOf("=");
+ const key = line.substring(0, idx).trim();
+ const value = this._parseData(line.slice(idx + 1));
+ return new KeyValuePair(key, value);
+ }
+ // TODO (zekth) Need refactor using ACC
+ _parseData(dataString: string): unknown {
+ dataString = dataString.trim();
+
+ if (this._isDate(dataString)) {
+ return new Date(dataString.split("#")[0].trim());
+ }
+
+ if (this._isLocalTime(dataString)) {
+ return eval(`"${dataString.split("#")[0].trim()}"`);
+ }
+
+ const cut3 = dataString.substring(0, 3).toLowerCase();
+ const cut4 = dataString.substring(0, 4).toLowerCase();
+ if (cut3 === "inf" || cut4 === "+inf") {
+ return Infinity;
+ }
+ if (cut4 === "-inf") {
+ return -Infinity;
+ }
+
+ if (cut3 === "nan" || cut4 === "+nan" || cut4 === "-nan") {
+ return NaN;
+ }
+
+ // If binary / octal / hex
+ const hex = /(0(?:x|o|b)[0-9a-f_]*)[^#]/gi.exec(dataString);
+ if (hex && hex[0]) {
+ return hex[0].trim();
+ }
+
+ const testNumber = this._isParsableNumber(dataString);
+ if (testNumber && !isNaN(testNumber as number)) {
+ return testNumber;
+ }
+
+ const invalidArr = /,\]/g.exec(dataString);
+ if (invalidArr) {
+ dataString = dataString.replace(/,]/g, "]");
+ }
+ const m = /(?:\'|\[|{|\").*(?:\'|\]|\"|})\s*[^#]/g.exec(dataString);
+ if (m) {
+ dataString = m[0].trim();
+ }
+ if (dataString[0] === "{" && dataString[dataString.length - 1] === "}") {
+ const reg = /([a-zA-Z0-9-_\.]*) (=)/gi;
+ let result;
+ while ((result = reg.exec(dataString))) {
+ const ogVal = result[0];
+ const newVal = ogVal
+ .replace(result[1], `"${result[1]}"`)
+ .replace(result[2], ":");
+ dataString = dataString.replace(ogVal, newVal);
+ }
+ return JSON.parse(dataString);
+ }
+
+ // 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'`, `'`);
+ }
+ return eval(dataString);
+ }
+ _isLocalTime(str: string): boolean {
+ const reg = /(\d{2}):(\d{2}):(\d{2})/;
+ return reg.test(str);
+ }
+ _isParsableNumber(dataString: string): number | boolean {
+ const m = /((?:\+|-|)[0-9_\.e+\-]*)[^#]/i.exec(dataString.trim());
+ if (!m) {
+ return false;
+ } else {
+ return parseFloat(m[0].replace(/_/g, ""));
+ }
+ }
+ _isDate(dateStr: string): boolean {
+ const reg = /\d{4}-\d{2}-\d{2}/;
+ return reg.test(dateStr);
+ }
+ _parseDeclarationName(declaration: string): string[] {
+ const out = [];
+ let acc = [];
+ let inLiteral = false;
+ for (let i = 0; i < declaration.length; i++) {
+ const c = declaration[i];
+ switch (c) {
+ case ".":
+ if (!inLiteral) {
+ out.push(acc.join(""));
+ acc = [];
+ } else {
+ acc.push(c);
+ }
+ break;
+ case `"`:
+ if (inLiteral) {
+ inLiteral = false;
+ } else {
+ inLiteral = true;
+ }
+ break;
+ default:
+ acc.push(c);
+ break;
+ }
+ }
+ if (acc.length !== 0) {
+ out.push(acc.join(""));
+ }
+ return out;
+ }
+ _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)) {
+ const kv = this._processDeclaration(line);
+ const key = kv.key;
+ const value = kv.value;
+ if (!this.context.currentGroup) {
+ this.context.output[key] = value;
+ } else {
+ this.context.currentGroup.objValues[key] = value;
+ }
+ }
+ }
+ if (this.context.currentGroup) {
+ if (this.context.currentGroup.type === "array") {
+ this.context.currentGroup.arrValues.push(
+ this.context.currentGroup.objValues
+ );
+ }
+ this._groupToOutput();
+ }
+ }
+ _cleanOutput(): void {
+ this._propertyClean(this.context.output);
+ }
+ _propertyClean(obj: Record<string, unknown>): void {
+ const keys = Object.keys(obj);
+ for (let i = 0; i < keys.length; i++) {
+ let k = keys[i];
+ if (k) {
+ let v = obj[k];
+ const pathDeclaration = this._parseDeclarationName(k);
+ delete obj[k];
+ if (pathDeclaration.length > 1) {
+ const shift = pathDeclaration.shift();
+ if (shift) {
+ k = shift.replace(/"/g, "");
+ v = this._unflat(pathDeclaration, v as object);
+ }
+ } else {
+ k = k.replace(/"/g, "");
+ }
+ obj[k] = v;
+ if (v instanceof Object) {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ this._propertyClean(v as any);
+ }
+ }
+ }
+ }
+ parse(): object {
+ this._sanitize();
+ this._parseLines();
+ this._cleanOutput();
+ return this.context.output;
+ }
+}
+
+// Bare keys may only contain ASCII letters,
+// ASCII digits, underscores, and dashes (A-Za-z0-9_-).
+function joinKeys(keys: string[]): string {
+ // Dotted keys are a sequence of bare or quoted keys joined with a dot.
+ // This allows for grouping similar properties together:
+ return keys
+ .map((str: string): string => {
+ return str.match(/[^A-Za-z0-9_-]/) ? `"${str}"` : str;
+ })
+ .join(".");
+}
+
+class Dumper {
+ maxPad = 0;
+ srcObject: object;
+ output: string[] = [];
+ constructor(srcObjc: object) {
+ this.srcObject = srcObjc;
+ }
+ dump(): string[] {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ this.output = this._parse(this.srcObject as any);
+ this.output = this._format();
+ return this.output;
+ }
+ _parse(obj: Record<string, unknown>, keys: string[] = []): string[] {
+ const out = [];
+ const props = Object.keys(obj);
+ const propObj = props.filter((e: string): boolean => {
+ if (obj[e] instanceof Array) {
+ const d: unknown[] = obj[e] as unknown[];
+ return !this._isSimplySerializable(d[0]);
+ }
+ return !this._isSimplySerializable(obj[e]);
+ });
+ const propPrim = props.filter((e: string): boolean => {
+ if (obj[e] instanceof Array) {
+ const d: unknown[] = obj[e] as unknown[];
+ return this._isSimplySerializable(d[0]);
+ }
+ return this._isSimplySerializable(obj[e]);
+ });
+ const k = propPrim.concat(propObj);
+ for (let i = 0; i < k.length; i++) {
+ const prop = k[i];
+ const value = obj[prop];
+ if (value instanceof Date) {
+ out.push(this._dateDeclaration([prop], value));
+ } else if (typeof value === "string" || value instanceof RegExp) {
+ out.push(this._strDeclaration([prop], value.toString()));
+ } else if (typeof value === "number") {
+ out.push(this._numberDeclaration([prop], value));
+ } else if (
+ value instanceof Array &&
+ this._isSimplySerializable(value[0])
+ ) {
+ // only if primitives types in the array
+ out.push(this._arrayDeclaration([prop], value));
+ } else if (
+ value instanceof Array &&
+ !this._isSimplySerializable(value[0])
+ ) {
+ // array of objects
+ for (let i = 0; i < value.length; i++) {
+ out.push("");
+ out.push(this._headerGroup([...keys, prop]));
+ out.push(...this._parse(value[i], [...keys, prop]));
+ }
+ } else if (typeof value === "object") {
+ out.push("");
+ out.push(this._header([...keys, prop]));
+ if (value) {
+ const toParse = value as Record<string, unknown>;
+ out.push(...this._parse(toParse, [...keys, prop]));
+ }
+ // out.push(...this._parse(value, `${path}${prop}.`));
+ }
+ }
+ out.push("");
+ return out;
+ }
+ _isSimplySerializable(value: unknown): boolean {
+ return (
+ typeof value === "string" ||
+ typeof value === "number" ||
+ value instanceof RegExp ||
+ value instanceof Date ||
+ value instanceof Array
+ );
+ }
+ _header(keys: string[]): string {
+ return `[${joinKeys(keys)}]`;
+ }
+ _headerGroup(keys: string[]): string {
+ return `[[${joinKeys(keys)}]]`;
+ }
+ _declaration(keys: string[]): string {
+ const title = joinKeys(keys);
+ if (title.length > this.maxPad) {
+ this.maxPad = title.length;
+ }
+ return `${title} = `;
+ }
+ _arrayDeclaration(keys: string[], value: unknown[]): string {
+ return `${this._declaration(keys)}${JSON.stringify(value)}`;
+ }
+ _strDeclaration(keys: string[], value: string): string {
+ return `${this._declaration(keys)}"${value}"`;
+ }
+ _numberDeclaration(keys: string[], value: number): string {
+ switch (value) {
+ case Infinity:
+ return `${this._declaration(keys)}inf`;
+ case -Infinity:
+ return `${this._declaration(keys)}-inf`;
+ default:
+ return `${this._declaration(keys)}${value}`;
+ }
+ }
+ _dateDeclaration(keys: string[], value: Date): string {
+ function dtPad(v: string, lPad = 2): string {
+ return pad(v, lPad, { char: "0" });
+ }
+ const m = dtPad((value.getUTCMonth() + 1).toString());
+ const d = dtPad(value.getUTCDate().toString());
+ const h = dtPad(value.getUTCHours().toString());
+ const min = dtPad(value.getUTCMinutes().toString());
+ const s = dtPad(value.getUTCSeconds().toString());
+ const ms = dtPad(value.getUTCMilliseconds().toString(), 3);
+ // formated date
+ const fData = `${value.getUTCFullYear()}-${m}-${d}T${h}:${min}:${s}.${ms}`;
+ return `${this._declaration(keys)}${fData}`;
+ }
+ _format(): string[] {
+ const rDeclaration = /(.*)\s=/;
+ const out = [];
+ for (let i = 0; i < this.output.length; i++) {
+ const l = this.output[i];
+ // we keep empty entry for array of objects
+ if (l[0] === "[" && l[1] !== "[") {
+ // empty object
+ if (this.output[i + 1] === "") {
+ i += 1;
+ continue;
+ }
+ out.push(l);
+ } else {
+ const m = rDeclaration.exec(l);
+ if (m) {
+ out.push(l.replace(m[1], pad(m[1], this.maxPad, { side: "right" })));
+ } else {
+ out.push(l);
+ }
+ }
+ }
+ // Cleaning multiple spaces
+ const cleanedOutput = [];
+ for (let i = 0; i < out.length; i++) {
+ const l = out[i];
+ if (!(l === "" && out[i + 1] === "")) {
+ cleanedOutput.push(l);
+ }
+ }
+ return cleanedOutput;
+ }
+}
+
+export function stringify(srcObj: object): string {
+ return new Dumper(srcObj).dump().join("\n");
+}
+
+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();
+}