diff --git a/strings/pad.ts b/strings/pad.ts index 1c18687695..3c5084da2b 100644 --- a/strings/pad.ts +++ b/strings/pad.ts @@ -3,9 +3,9 @@ /** FillOption Object */ export interface FillOption { /** Char to fill in */ - char: string; + char?: string; /** Side to fill in */ - side: "left" | "right"; + side?: "left" | "right"; /** If strict, output string can't be greater than strLen*/ strict?: boolean; /** char/string used to specify the string has been truncated */ @@ -54,7 +54,7 @@ export function pad( let out = input; const outL = out.length; if (outL < strLen) { - if (opts.side === "left") { + if (!opts.side || opts.side === "left") { out = out.padStart(strLen, opts.char); } else { out = out.padEnd(strLen, opts.char); diff --git a/toml/README.md b/toml/README.md index b19974b2eb..f4ae258578 100644 --- a/toml/README.md +++ b/toml/README.md @@ -91,6 +91,8 @@ will output: ## Usage +### Parse + ```ts import { parseFile, parse } from "./parser.ts"; @@ -99,3 +101,17 @@ const tomlObject = parseFile("file.toml"); const tomlString = 'foo.bar = "Deno"'; const tomlObject22 = parse(tomlString); ``` + +### Stringify + +```ts +import { stringify } from "./parser.ts"; +const obj = { + bin: [ + { name: "deno", path: "cli/main.rs" }, + { name: "deno_core", path: "src/foo.rs" } + ], + nib: [{ name: "node", path: "not_found" }] +}; +const tomlString = stringify(obj); +``` diff --git a/toml/parser.ts b/toml/parser.ts index 09203775a4..a7dd977509 100644 --- a/toml/parser.ts +++ b/toml/parser.ts @@ -1,6 +1,7 @@ // Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. import { existsSync } from "../fs/exists.ts"; import { deepAssign } from "../util/deep_assign.ts"; +import { pad } from "../strings/pad.ts"; class KeyValuePair { key: string; @@ -381,6 +382,156 @@ class Parser { } } +class Dumper { + maxPad: number = 0; + srcObject: object; + output: string[] = []; + constructor(srcObjc: object) { + this.srcObject = srcObjc; + } + dump(): string[] { + this.output = this._parse(this.srcObject); + this.output = this._format(); + return this.output; + } + _parse(obj: object, path: string = ""): string[] { + const out = []; + const props = Object.keys(obj); + const propObj = props.filter( + e => + (obj[e] instanceof Array && !this._isSimplySerializable(obj[e][0])) || + !this._isSimplySerializable(obj[e]) + ); + const propPrim = props.filter( + e => + !(obj[e] instanceof Array && !this._isSimplySerializable(obj[e][0])) && + 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(path + prop)); + out.push(...this._parse(value[i], `${path}${prop}.`)); + } + } else if (typeof value === "object") { + out.push(""); + out.push(this._header(path + 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(title: string): string { + return `[${title}]`; + } + _headerGroup(title: string): string { + return `[[${title}]]`; + } + _declaration(title: string): string { + if (title.length > this.maxPad) { + this.maxPad = title.length; + } + return `${title} = `; + } + _arrayDeclaration(title: string, value: unknown[]): string { + return `${this._declaration(title)}${JSON.stringify(value)}`; + } + _strDeclaration(title: string, value: string): string { + return `${this._declaration(title)}"${value}"`; + } + _numberDeclaration(title: string, value: number): string { + switch (value) { + case Infinity: + return `${this._declaration(title)}inf`; + case -Infinity: + return `${this._declaration(title)}-inf`; + default: + return `${this._declaration(title)}${value}`; + } + } + _dateDeclaration(title: string, value: Date): string { + function dtPad(v: string, lPad: number = 2): string { + return pad(v, lPad, { char: "0" }); + } + let m = dtPad((value.getUTCMonth() + 1).toString()); + let 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); + const fmtDate = `${value.getUTCFullYear()}-${m}-${d}T${h}:${min}:${s}.${ms}`; + return `${this._declaration(title)}${fmtDate}`; + } + _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 { + let out: string[] = []; + out = new Dumper(srcObj).dump(); + return out.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"); diff --git a/toml/parser_test.ts b/toml/parser_test.ts index 77bf9dc97d..6cafb4b0d2 100644 --- a/toml/parser_test.ts +++ b/toml/parser_test.ts @@ -1,7 +1,7 @@ // Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. import { test } from "../testing/mod.ts"; import { assertEquals } from "../testing/asserts.ts"; -import { parseFile } from "./parser.ts"; +import { parseFile, stringify } from "./parser.ts"; import * as path from "../fs/path/mod.ts"; const testFilesDir = path.resolve("toml", "testdata"); @@ -282,3 +282,97 @@ test({ assertEquals(actual, expected); } }); + +test({ + name: "[TOML] Stringify", + fn() { + const src = { + foo: { bar: "deno" }, + this: { is: { nested: "denonono" } }, + arrayObjects: [{ stuff: "in" }, {}, { the: "array" }], + deno: "is", + not: "[node]", + regex: "", + NANI: "何?!", + comment: "Comment inside # the comment", + int1: 99, + int2: 42, + int3: 0, + int4: -17, + int5: 1000, + int6: 5349221, + int7: 12345, + flt1: 1.0, + flt2: 3.1415, + flt3: -0.01, + flt4: 5e22, + flt5: 1e6, + flt6: -2e-2, + flt7: 6.626e-34, + odt1: new Date("1979-05-01T07:32:00Z"), + odt2: new Date("1979-05-27T00:32:00-07:00"), + odt3: new Date("1979-05-27T00:32:00.999999-07:00"), + odt4: new Date("1979-05-27 07:32:00Z"), + ld1: new Date("1979-05-27"), + reg: /foo[bar]/, + sf1: Infinity, + sf2: Infinity, + sf3: -Infinity, + sf4: NaN, + sf5: NaN, + sf6: NaN, + data: [["gamma", "delta"], [1, 2]], + hosts: ["alpha", "omega"] + }; + const expected = `deno = "is" +not = "[node]" +regex = "" +NANI = "何?!" +comment = "Comment inside # the comment" +int1 = 99 +int2 = 42 +int3 = 0 +int4 = -17 +int5 = 1000 +int6 = 5349221 +int7 = 12345 +flt1 = 1 +flt2 = 3.1415 +flt3 = -0.01 +flt4 = 5e+22 +flt5 = 1000000 +flt6 = -0.02 +flt7 = 6.626e-34 +odt1 = 1979-05-01T07:32:00.000 +odt2 = 1979-05-27T07:32:00.000 +odt3 = 1979-05-27T07:32:00.999 +odt4 = 1979-05-27T07:32:00.000 +ld1 = 1979-05-27T00:00:00.000 +reg = "/foo[bar]/" +sf1 = inf +sf2 = inf +sf3 = -inf +sf4 = NaN +sf5 = NaN +sf6 = NaN +data = [["gamma","delta"],[1,2]] +hosts = ["alpha","omega"] + +[foo] +bar = "deno" + +[this.is] +nested = "denonono" + +[[arrayObjects]] +stuff = "in" + +[[arrayObjects]] + +[[arrayObjects]] +the = "array" +`; + const actual = stringify(src); + assertEquals(actual, expected); + } +});