diff --git a/test.ts b/test.ts index 7e3af53136..9760b0f234 100755 --- a/test.ts +++ b/test.ts @@ -15,6 +15,7 @@ import "./prettier/test.ts"; import "./strings/test.ts"; import "./testing/test.ts"; import "./textproto/test.ts"; +import "./toml/test.ts"; import "./ws/test.ts"; import "./testing/main.ts"; diff --git a/toml/README.md b/toml/README.md new file mode 100644 index 0000000000..a1acd7466d --- /dev/null +++ b/toml/README.md @@ -0,0 +1,95 @@ +# TOML + +This module parse TOML files. It follows as much as possible the +[TOML specs](https://github.com/toml-lang/toml). Be sure to read the supported +types as not every specs is supported at the moment and the handling in +TypeScript side is a bit different. + +## Supported types and handling + +- :heavy_check_mark: [Keys](https://github.com/toml-lang/toml#string) +- :exclamation: [String](https://github.com/toml-lang/toml#string) +- :heavy_check_mark: + [Multiline String](https://github.com/toml-lang/toml#string) +- :heavy_check_mark: [Literal String](https://github.com/toml-lang/toml#string) +- :exclamation: [Integer](https://github.com/toml-lang/toml#integer) +- :heavy_check_mark: [Float](https://github.com/toml-lang/toml#float) +- :heavy_check_mark: [Boolean](https://github.com/toml-lang/toml#boolean) +- :heavy_check_mark: + [Offset Date-time](https://github.com/toml-lang/toml#offset-date-time) +- :heavy_check_mark: + [Local Date-time](https://github.com/toml-lang/toml#local-date-time) +- :heavy_check_mark: [Local Date](https://github.com/toml-lang/toml#local-date) +- :exclamation: [Local Time](https://github.com/toml-lang/toml#local-time) +- :heavy_check_mark: [Table](https://github.com/toml-lang/toml#table) +- :exclamation: [Inline Table](https://github.com/toml-lang/toml#inline-table) +- :exclamation: [Array of Tables](https://github.com/toml-lang/toml#array-of-tables) + +:exclamation: _Supported with warnings see [Warning](#Warning)._ + +### :warning: Warning + +#### String + +- Regex : Due to the spec, there is no flag to detect regex properly + in a TOML declaration. So the regex is stored as string. + +#### Integer + +For **Binary** / **Octal** / **Hexadecimal** numbers, +they are stored as string to be not interpreted as Decimal. + +#### Local Time + +Because local time does not exist in JavaScript, the local time is stored as a string. + +#### Inline Table + +Inline tables are supported but nested inline property name are **not**. See below: + +```toml +animal = { type = { name = "pug" } } # Supported +animal = { type.name = "pug" } +# not supported. Will output { "animal" : {"type.name":"pug"} } +``` + +#### Array of Tables + +At the moment only simple declarations like below are supported: + +```toml +[[bin]] +name = "deno" +path = "cli/main.rs" + +[[bin]] +name = "deno_core" +path = "src/foo.rs" + +[[nib]] +name = "node" +path = "not_found" +``` + +will output: + +```json +{ + "bin": [ + { "name": "deno", "path": "cli/main.rs" }, + { "name": "deno_core", "path": "src/foo.rs" } + ], + "nib": [{ "name": "node", "path": "not_found" }] +} +``` + +## Usage + +```ts +import { parseFile, parse } from "./parser.ts"; + +const tomlObject = parseFile("file.toml"); + +const tomlString = 'foo.bar = "Deno"'; +const tomlObject22 = parse(tomlString); +``` diff --git a/toml/parser.ts b/toml/parser.ts new file mode 100644 index 0000000000..9263f01112 --- /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); +} diff --git a/toml/parser_test.ts b/toml/parser_test.ts new file mode 100644 index 0000000000..cc66752960 --- /dev/null +++ b/toml/parser_test.ts @@ -0,0 +1,263 @@ +// 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 * as path from "../fs/path/mod.ts"; +const testFilesDir = path.resolve("toml", "testdata"); + +test({ + name: "[TOML] Strings", + fn() { + const expected = { + strings: { + str0: "deno", + str1: "Roses are not Deno\nViolets are not Deno either", + str2: "Roses are not Deno\nViolets are not Deno either", + str3: "Roses are not Deno\r\nViolets are not Deno either", + str4: 'this is a "quote"', + str5: "The quick brown\nfox jumps over\nthe lazy dog.", + str6: "The quick brown\nfox jumps over\nthe lazy dog.", + lines: + "The first newline is\ntrimmed in raw strings.\n All other whitespace\n is preserved." + } + }; + const actual = parseFile(path.join(testFilesDir, "string.toml")); + assertEquals(actual, expected); + } +}); + +test({ + name: "[TOML] CRLF", + fn() { + const expected = { boolean: { bool1: true, bool2: false } }; + const actual = parseFile(path.join(testFilesDir, "CRLF.toml")); + assertEquals(actual, expected); + } +}); + +test({ + name: "[TOML] Boolean", + fn() { + const expected = { boolean: { bool1: true, bool2: false } }; + const actual = parseFile(path.join(testFilesDir, "boolean.toml")); + assertEquals(actual, expected); + } +}); + +test({ + name: "[TOML] Integer", + fn() { + const expected = { + integer: { + int1: 99, + int2: 42, + int3: 0, + int4: -17, + int5: 1000, + int6: 5349221, + int7: 12345, + hex1: "0xDEADBEEF", + hex2: "0xdeadbeef", + hex3: "0xdead_beef", + oct1: "0o01234567", + oct2: "0o755", + bin1: "0b11010110" + } + }; + const actual = parseFile(path.join(testFilesDir, "integer.toml")); + assertEquals(actual, expected); + } +}); + +test({ + name: "[TOML] Float", + fn() { + const expected = { + float: { + flt1: 1.0, + flt2: 3.1415, + flt3: -0.01, + flt4: 5e22, + flt5: 1e6, + flt6: -2e-2, + flt7: 6.626e-34, + flt8: 224_617.445_991_228, + sf1: Infinity, + sf2: Infinity, + sf3: -Infinity, + sf4: NaN, + sf5: NaN, + sf6: NaN + } + }; + const actual = parseFile(path.join(testFilesDir, "float.toml")); + assertEquals(actual, expected); + } +}); + +test({ + name: "[TOML] Arrays", + fn() { + const expected = { + arrays: { + data: [["gamma", "delta"], [1, 2]], + hosts: ["alpha", "omega"] + } + }; + const actual = parseFile(path.join(testFilesDir, "arrays.toml")); + assertEquals(actual, expected); + } +}); + +test({ + name: "[TOML] Table", + fn() { + const expected = { + deeply: { + nested: { + object: { + in: { + the: { + toml: { + name: "Tom Preston-Werner" + } + } + } + } + } + }, + servers: { + alpha: { + ip: "10.0.0.1", + dc: "eqdc10" + }, + beta: { + ip: "10.0.0.2", + dc: "eqdc20" + } + } + }; + const actual = parseFile(path.join(testFilesDir, "table.toml")); + assertEquals(actual, expected); + } +}); + +test({ + name: "[TOML] Simple", + fn() { + const expected = { + deno: "is", + not: "[node]", + regex: "", + NANI: "何?!" + }; + const actual = parseFile(path.join(testFilesDir, "simple.toml")); + assertEquals(actual, expected); + } +}); + +test({ + name: "[TOML] Datetime", + fn() { + const expected = { + datetime: { + odt1: new Date("1979-05-27T07: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"), + lt1: "07:32:00", + lt2: "00:32:00.999999" + } + }; + const actual = parseFile(path.join(testFilesDir, "datetime.toml")); + assertEquals(actual, expected); + } +}); + +test({ + name: "[TOML] Inline Table", + fn() { + const expected = { + inlinetable: { + name: { + first: "Tom", + last: "Preston-Werner" + }, + point: { + x: 1, + y: 2 + }, + animal: { + type: { + name: "pug" + } + } + } + }; + const actual = parseFile(path.join(testFilesDir, "inlineTable.toml")); + assertEquals(actual, expected); + } +}); + +test({ + name: "[TOML] Array of Tables", + fn() { + const expected = { + bin: [ + { name: "deno", path: "cli/main.rs" }, + { name: "deno_core", path: "src/foo.rs" } + ], + nib: [{ name: "node", path: "not_found" }] + }; + const actual = parseFile(path.join(testFilesDir, "arrayTable.toml")); + assertEquals(actual, expected); + } +}); + +test({ + name: "[TOML] Cargo", + fn() { + /* eslint-disable @typescript-eslint/camelcase */ + const expected = { + workspace: { members: ["./", "core"] }, + bin: [{ name: "deno", path: "cli/main.rs" }], + package: { name: "deno", version: "0.3.4", edition: "2018" }, + dependencies: { + deno_core: { path: "./core" }, + ansi_term: "0.11.0", + atty: "0.2.11", + dirs: "1.0.5", + flatbuffers: "0.5.0", + futures: "0.1.25", + getopts: "0.2.18", + http: "0.1.16", + hyper: "0.12.24", + "hyper-rustls": "0.16.0", + "integer-atomics": "1.0.2", + lazy_static: "1.3.0", + libc: "0.2.49", + log: "0.4.6", + rand: "0.6.5", + regex: "1.1.0", + remove_dir_all: "0.5.1", + ring: "0.14.6", + rustyline: "3.0.0", + serde_json: "1.0.38", + "source-map-mappings": "0.5.0", + tempfile: "3.0.7", + tokio: "0.1.15", + "tokio-executor": "0.1.6", + "tokio-fs": "0.1.5", + "tokio-io": "0.1.11", + "tokio-process": "0.2.3", + "tokio-threadpool": "0.1.11", + url: "1.7.2" + }, + target: { "cfg(windows)": { dependencies: { winapi: "0.3.6" } } } + }; + /* eslint-enable @typescript-eslint/camelcase */ + const actual = parseFile(path.join(testFilesDir, "cargo.toml")); + assertEquals(actual, expected); + } +}); diff --git a/toml/test.ts b/toml/test.ts new file mode 100644 index 0000000000..16be28c8d2 --- /dev/null +++ b/toml/test.ts @@ -0,0 +1,2 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. +import "./parser_test.ts"; diff --git a/toml/testdata/CRLF.toml b/toml/testdata/CRLF.toml new file mode 100644 index 0000000000..92264888a2 --- /dev/null +++ b/toml/testdata/CRLF.toml @@ -0,0 +1,3 @@ +[boolean] +bool1 = true +bool2 = false \ No newline at end of file diff --git a/toml/testdata/arrayTable.toml b/toml/testdata/arrayTable.toml new file mode 100644 index 0000000000..3788b7e7c4 --- /dev/null +++ b/toml/testdata/arrayTable.toml @@ -0,0 +1,12 @@ + +[[bin]] +name = "deno" +path = "cli/main.rs" + +[[bin]] +name = "deno_core" +path = "src/foo.rs" + +[[nib]] +name = "node" +path = "not_found" \ No newline at end of file diff --git a/toml/testdata/arrays.toml b/toml/testdata/arrays.toml new file mode 100644 index 0000000000..5d5913d0c2 --- /dev/null +++ b/toml/testdata/arrays.toml @@ -0,0 +1,8 @@ +[arrays] +data = [ ["gamma", "delta"], [1, 2] ] + +# Line breaks are OK when inside arrays +hosts = [ + "alpha", + "omega" +] diff --git a/toml/testdata/boolean.toml b/toml/testdata/boolean.toml new file mode 100644 index 0000000000..92264888a2 --- /dev/null +++ b/toml/testdata/boolean.toml @@ -0,0 +1,3 @@ +[boolean] +bool1 = true +bool2 = false \ No newline at end of file diff --git a/toml/testdata/cargo.toml b/toml/testdata/cargo.toml new file mode 100644 index 0000000000..5402d10a4c --- /dev/null +++ b/toml/testdata/cargo.toml @@ -0,0 +1,56 @@ +# Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. +# Dummy package info required by `cargo fetch`. +# Use tools/sync_third_party.py to install deps after editing this file. +# Deno does not build with cargo. Deno uses a build system called gn. +# See build_extra/rust/BUILD.gn for the manually built configuration of rust +# crates. + +[workspace] +members = [ + "./", + "core", +] + +[[bin]] +name = "deno" +path = "cli/main.rs" + +[package] +name = "deno" +version = "0.3.4" +edition = "2018" + +[dependencies] +deno_core = { path = "./core" } + +ansi_term = "0.11.0" +atty = "0.2.11" +dirs = "1.0.5" +flatbuffers = "0.5.0" +futures = "0.1.25" +getopts = "0.2.18" +http = "0.1.16" +hyper = "0.12.24" +hyper-rustls = "0.16.0" +integer-atomics = "1.0.2" +lazy_static = "1.3.0" +libc = "0.2.49" +log = "0.4.6" +rand = "0.6.5" +regex = "1.1.0" +remove_dir_all = "0.5.1" +ring = "0.14.6" +rustyline = "3.0.0" +serde_json = "1.0.38" +source-map-mappings = "0.5.0" +tempfile = "3.0.7" +tokio = "0.1.15" +tokio-executor = "0.1.6" +tokio-fs = "0.1.5" +tokio-io = "0.1.11" +tokio-process = "0.2.3" +tokio-threadpool = "0.1.11" +url = "1.7.2" + +[target.'cfg(windows)'.dependencies] +winapi = "0.3.6" \ No newline at end of file diff --git a/toml/testdata/cargoTest.toml b/toml/testdata/cargoTest.toml new file mode 100644 index 0000000000..47e7f6e4d3 --- /dev/null +++ b/toml/testdata/cargoTest.toml @@ -0,0 +1,147 @@ +# This is a TOML document. + +title = "TOML Example" + +[deeply.nested.object.in.the.toml] +name = "Tom Preston-Werner" +dob = 2009-05-27T07:32:00 + +[database] +server = "192.168.1.1" +ports = [ 8001, 8001, 8002 ] +connection_max = 5000 +enabled = true + +[servers] + + # Indentation (tabs and/or spaces) is allowed but not required + [servers.alpha] + ip = "10.0.0.1" + dc = "eqdc10" + + [servers.beta] + ip = "10.0.0.2" + dc = "eqdc10" + +[clients] +data = [ ["gamma", "delta"], [1, 2] ] + +# Line breaks are OK when inside arrays +hosts = [ + "alpha", + "omega" +] + +[strings] +str0 = "deno" +str1 = """ +Roses are red + Violets are blue""" +# On a Unix system, the above multi-line string will most likely be the same as: +str2 = "Roses are red\nViolets are blue" + +# On a Windows system, it will most likely be equivalent to: +str3 = "Roses are red\r\nViolets are blue" +str4 = "The quick brown fox jumps over the lazy dog." +str5 = "this is a \"quote\"" + +str5 = """ +The quick brown \ + + + fox jumps over \ + the lazy dog.""" + +str6 = """\ + The quick brown \ + fox jumps over \ + the lazy dog.\ + """ +lines = ''' +The first newline is +trimmed in raw strings. + All other whitespace + is preserved. +''' + +[Integer] +int1 = +99 +int2 = 42 +int3 = 0 +int4 = -17 +int5 = 1_000 +int6 = 5_349_221 +int7 = 1_2_3_4_5 # VALID but discouraged + +# hexadecimal with prefix `0x` +hex1 = 0xDEADBEEF +hex2 = 0xdeadbeef +hex3 = 0xdead_beef + +# octal with prefix `0o` +oct1 = 0o01234567 +oct2 = 0o755 # useful for Unix file permissions + +# binary with prefix `0b` +bin1 = 0b11010110 + +[Date-Time] +odt1 = 1979-05-27T07:32:00Z +odt2 = 1979-05-27T00:32:00-07:00 +odt3 = 1979-05-27T00:32:00.999999-07:00 +odt4 = 1979-05-27 07:32:00Z +ld1 = 1979-05-27 +lt1 = 07:32:00 #buggy +lt2 = 00:32:00.999999 #buggy + +[boolean] +bool1 = true +bool2 = false + +[float] +# fractional +flt1 = +1.0 +flt2 = 3.1415 +flt3 = -0.01 + +# exponent +flt4 = 5e+22 +flt5 = 1e6 +flt6 = -2E-2 + +# both +flt7 = 6.626e-34 +flt8 = 224_617.445_991_228 +# infinity +sf1 = inf # positive infinity +sf2 = +inf # positive infinity +sf3 = -inf # negative infinity + +# not a number +sf4 = nan # actual sNaN/qNaN encoding is implementation specific +sf5 = +nan # same as `nan` +sf6 = -nan # valid, actual encoding is implementation specific + +[Table] +name = { first = "Tom", last = "Preston-Werner" } +point = { x = 1, y = 2 } +animal = { type.name = "pug" } + +[[fruit]] + name = "apple" + + [fruit.physical] + color = "red" + shape = "round" + + [[fruit.variety]] + name = "red delicious" + + [[fruit.variety]] + name = "granny smith" + +[[fruit]] + name = "banana" + + [[fruit.variety]] + name = "plantain" diff --git a/toml/testdata/datetime.toml b/toml/testdata/datetime.toml new file mode 100644 index 0000000000..b21924793c --- /dev/null +++ b/toml/testdata/datetime.toml @@ -0,0 +1,8 @@ +[datetime] +odt1 = 1979-05-27T07:32:00Z +odt2 = 1979-05-27T00:32:00-07:00 +odt3 = 1979-05-27T00:32:00.999999-07:00 +odt4 = 1979-05-27 07:32:00Z +ld1 = 1979-05-27 +lt1 = 07:32:00 +lt2 = 00:32:00.999999 diff --git a/toml/testdata/float.toml b/toml/testdata/float.toml new file mode 100644 index 0000000000..92f0b6d17b --- /dev/null +++ b/toml/testdata/float.toml @@ -0,0 +1,23 @@ +[float] +# fractional +flt1 = +1.0 +flt2 = 3.1415 +flt3 = -0.01 + +# exponent +flt4 = 5e+22 +flt5 = 1e6 +flt6 = -2E-2 + +# both +flt7 = 6.626e-34 +flt8 = 224_617.445_991_228 +# infinity +sf1 = inf # positive infinity +sf2 = +inf # positive infinity +sf3 = -inf # negative infinity + +# not a number +sf4 = nan # actual sNaN/qNaN encoding is implementation specific +sf5 = +nan # same as `nan` +sf6 = -nan # valid, actual encoding is implementation specific \ No newline at end of file diff --git a/toml/testdata/inlineTable.toml b/toml/testdata/inlineTable.toml new file mode 100644 index 0000000000..1a7e55c526 --- /dev/null +++ b/toml/testdata/inlineTable.toml @@ -0,0 +1,4 @@ +[inlinetable] +name = { first = "Tom", last = "Preston-Werner" } +point = { x = 1, y = 2 } +animal = { type = { name = "pug" } } \ No newline at end of file diff --git a/toml/testdata/integer.toml b/toml/testdata/integer.toml new file mode 100644 index 0000000000..3bd781e8f5 --- /dev/null +++ b/toml/testdata/integer.toml @@ -0,0 +1,20 @@ +[integer] +int1 = +99 +int2 = 42 +int3 = 0 +int4 = -17 +int5 = 1_000 +int6 = 5_349_221 +int7 = 1_2_3_4_5 # VALID but discouraged + +# hexadecimal with prefix `0x` +hex1 = 0xDEADBEEF +hex2 = 0xdeadbeef +hex3 = 0xdead_beef + +# octal with prefix `0o` +oct1 = 0o01234567 +oct2 = 0o755 # useful for Unix file permissions + +# binary with prefix `0b` +bin1 = 0b11010110 \ No newline at end of file diff --git a/toml/testdata/simple.toml b/toml/testdata/simple.toml new file mode 100644 index 0000000000..aac44714e9 --- /dev/null +++ b/toml/testdata/simple.toml @@ -0,0 +1,4 @@ +deno = "is" +not = "[node]" +regex = '<\i\c*\s*>' +NANI = '何?!' \ No newline at end of file diff --git a/toml/testdata/string.toml b/toml/testdata/string.toml new file mode 100644 index 0000000000..f811824ebe --- /dev/null +++ b/toml/testdata/string.toml @@ -0,0 +1,30 @@ +[strings] +str0 = "deno" +str1 = """ +Roses are not Deno + Violets are not Deno either""" +# On a Unix system, the above multi-line string will most likely be the same as: +str2 = "Roses are not Deno\nViolets are not Deno either" + +# On a Windows system, it will most likely be equivalent to: +str3 = "Roses are not Deno\r\nViolets are not Deno either" +str4 = "this is a \"quote\"" + +str5 = """ +The quick brown \ + + + fox jumps over \ + the lazy dog.""" + +str6 = """\ + The quick brown \ + fox jumps over \ + the lazy dog.\ + """ +lines = ''' +The first newline is +trimmed in raw strings. + All other whitespace + is preserved. +''' \ No newline at end of file diff --git a/toml/testdata/table.toml b/toml/testdata/table.toml new file mode 100644 index 0000000000..7008e6fb01 --- /dev/null +++ b/toml/testdata/table.toml @@ -0,0 +1,13 @@ +[deeply.nested.object.in.the.toml] +name = "Tom Preston-Werner" + +[servers] + + # Indentation (tabs and/or spaces) is allowed but not required + [servers.alpha] + ip = "10.0.0.1" + dc = "eqdc10" + + [servers.beta] + ip = "10.0.0.2" + dc = "eqdc20" \ No newline at end of file diff --git a/util/deep_assign.ts b/util/deep_assign.ts new file mode 100644 index 0000000000..4857e18b53 --- /dev/null +++ b/util/deep_assign.ts @@ -0,0 +1,28 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. +export function deepAssign(target: object, ...sources: object[]): object { + for (let i = 0; i < sources.length; i++) { + const source = sources[i]; + if (!source || typeof source !== `object`) { + return; + } + Object.entries(source).forEach(([key, value]) => { + if (value instanceof Date) { + target[key] = new Date(value); + return; + } + if (!value || typeof value !== `object`) { + target[key] = value; + return; + } + if (Array.isArray(value)) { + target[key] = []; + } + // value is an Object + if (typeof target[key] !== `object` || !target[key]) { + target[key] = {}; + } + deepAssign(target[key], value); + }); + } + return target; +} diff --git a/util/deep_assign_test.ts b/util/deep_assign_test.ts new file mode 100644 index 0000000000..36be979d8d --- /dev/null +++ b/util/deep_assign_test.ts @@ -0,0 +1,24 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. +import { test } from "../testing/mod.ts"; +import { assertEquals, assert } from "../testing/asserts.ts"; +import { deepAssign } from "./deep_assign.ts"; + +test(function deepAssignTest() { + const date = new Date("1979-05-27T07:32:00Z"); + const reg = RegExp(/DENOWOWO/); + const obj1 = { deno: { bar: { deno: ["is", "not", "node"] } } }; + const obj2 = { foo: { deno: date } }; + const obj3 = { foo: { bar: "deno" }, reg: reg }; + const actual = deepAssign(obj1, obj2, obj3); + const expected = { + foo: { + deno: new Date("1979-05-27T07:32:00Z"), + bar: "deno" + }, + deno: { bar: { deno: ["is", "not", "node"] } }, + reg: RegExp(/DENOWOWO/) + }; + assert(date !== expected.foo.deno); + assert(reg !== expected.reg); + assertEquals(actual, expected); +}); diff --git a/util/test.ts b/util/test.ts new file mode 100644 index 0000000000..a617c10ab3 --- /dev/null +++ b/util/test.ts @@ -0,0 +1 @@ +import "./deep_assign_test.ts";