diff --git a/lib/common/yaml.ts b/lib/common/yaml.ts new file mode 100644 index 0000000..549812c --- /dev/null +++ b/lib/common/yaml.ts @@ -0,0 +1,198 @@ +const LINE_WIDTH = 80; +const INDENTATION = " "; + +const STRING_RESERVED = new Set([ + "true", + "True", + "TRUE", + "false", + "False", + "FALSE", + "null", + "Null", + "NULL", +]); + +function isPrintable(str: string): boolean { + return !/[^\t\n\x20-\x7e\x85\u{a0}-\u{d7ff}\u{e000}-\u{fffd}\u{10000}-\u{10ffff}]/u.test( + str + ); +} + +function stringifyKey(str: string): string { + if (!str || !isPrintable(str)) return JSON.stringify(str); + if (/^[\s-?:,[\]{}#&$!|>'"%@`]|: | #|[\n,[\]{}]|\s$/.test(str)) + return JSON.stringify(str); + return str; +} + +function foldString(str: string): string[] { + if (str.length <= LINE_WIDTH) return [str]; + if (str.startsWith(" ")) return [str]; + const words = str.split(/(?<=[^ ]) (?=[^ ])/); + const lines: string[] = []; + + let idx = 0; + let len = 0; + for (const [i, word] of words.entries()) { + len += word.length + 1; + if (len >= LINE_WIDTH && len !== word.length) { + lines.push(words.slice(idx, i).join(" ")); + idx = i; + len = 0; + } + } + + if (idx < words.length) lines.push(words.slice(idx).join(" ")); + + return lines; +} + +function stringifyString(str: string, res: string[], prefix1, prefix2): void { + if (/^\s*$/.test(str) || STRING_RESERVED.has(str) || !isPrintable(str)) { + res.push(prefix1 + JSON.stringify(str)); + return; + } + + if (!prefix2) prefix2 = INDENTATION; + + const lines = str.split("\n"); + if (lines.length > 1) { + let idt = ""; + let chmp = "-"; + if ((lines.find((l) => l) || "").startsWith(" ")) + idt = `${INDENTATION.length}`; + + if (!lines[lines.length - 1]) { + lines.pop(); + if (lines[lines.length - 1]) chmp = ""; + else chmp = "+"; + } + + if (/^\s+$/.test(lines[lines.length - 1])) { + res.push(prefix1 + JSON.stringify(str)); + return; + } + + let isFolded = false; + const folded = lines.map((l) => { + const ls = foldString(l); + if (ls.length > 1) isFolded = true; + return ls; + }); + + if (!isFolded) { + res.push( + `${prefix1}|${idt}${chmp}`, + ...lines.map((l) => (l ? prefix2 + l : l)) + ); + return; + } + + res.push(`${prefix1}>${idt}${chmp}`); + res.push(...folded[0].map((f) => prefix2 + f)); + for (let i = 1; i < folded.length; ++i) { + const prevLine = folded[i - 1][0]; + if (prevLine && !folded[i - 1][0].startsWith(" ")) res.push(""); + res.push(...folded[i].map((f) => prefix2 + f)); + } + return; + } + + if ( + /^[\s-?:,[\]{}#&$!|>'"%@`]|: | #|\s$/.test(str) || + parseFloat(str) === +str + ) { + res.push(prefix1 + JSON.stringify(str)); + return; + } + + res.push(prefix1 + str); +} + +function stringifyAny( + obj: unknown, + res: string[], + prefix1 = "", + prefix2 = "" +): void { + if (obj == null || typeof obj === "number" || typeof obj === "boolean") { + res.push(`${prefix1}${obj}`); + return; + } + if (obj instanceof Date) { + res.push(`${prefix1}${obj.toJSON()}`); + return; + } + + if (typeof obj === "string") { + stringifyString(obj, res, prefix1, prefix2); + return; + } + + if (Array.isArray(obj)) { + if (!obj.length) { + res.push(prefix1 + "[]"); + return; + } + + if (!prefix1 || prefix1.endsWith("- ")) { + stringifyAny(obj[0], res, prefix1 + "- ", prefix2 + INDENTATION); + prefix1 = prefix2 + "- "; + prefix2 = prefix2 + INDENTATION; + for (let i = 1; i < obj.length; ++i) + stringifyAny(obj[i], res, prefix1, prefix2); + } else { + res.push(prefix1); + prefix1 = prefix2 + "- "; + prefix2 = prefix2 + INDENTATION; + for (let i = 0; i < obj.length; ++i) + stringifyAny(obj[i], res, prefix1, prefix2); + } + return; + } + + const entries = Object.entries(obj); + + if (!entries.length) { + res.push(prefix1 + "{}"); + return; + } + + if (!prefix1 || prefix1.endsWith("- ")) { + stringifyAny( + entries[0][1], + res, + prefix1 + `${stringifyKey(entries[0][0])}: `, + prefix2 + INDENTATION + ); + prefix1 = prefix2; + prefix2 = prefix2 + INDENTATION; + for (let i = 1; i < entries.length; ++i) { + stringifyAny( + entries[i][1], + res, + prefix1 + `${stringifyKey(entries[i][0])}: `, + prefix2 + ); + } + } else { + res.push(prefix1); + prefix1 = prefix2; + prefix2 = prefix2 + INDENTATION; + for (let i = 0; i < entries.length; ++i) { + stringifyAny( + entries[i][1], + res, + prefix1 + `${stringifyKey(entries[i][0])}: `, + prefix2 + ); + } + } +} + +export function stringify(obj: unknown): string { + const lines: string[] = []; + stringifyAny(obj, lines); + return lines.join("\n") + "\n"; +} diff --git a/lib/debug.ts b/lib/debug.ts index 1801000..d2a8de1 100644 --- a/lib/debug.ts +++ b/lib/debug.ts @@ -25,7 +25,7 @@ import { } from "http"; import { Socket } from "net"; import { appendFileSync } from "fs"; -import * as yaml from "yaml"; +import { stringify } from "./common/yaml"; import * as config from "./config"; const DEBUG_FILE = "" + config.get("DEBUG_FILE"); @@ -64,7 +64,7 @@ export function incomingHttpRequest( }; if (DEBUG_FORMAT === "yaml") - appendFileSync(DEBUG_FILE, "---\n" + yaml.stringify(msg)); + appendFileSync(DEBUG_FILE, "---\n" + stringify(msg)); else if (DEBUG_FORMAT === "json") appendFileSync(DEBUG_FILE, JSON.stringify(msg) + "\n"); else throw new Error(`Unrecognized DEBUG_FORMAT option`); @@ -90,7 +90,7 @@ export function outgoingHttpResponse( }; if (DEBUG_FORMAT === "yaml") - appendFileSync(DEBUG_FILE, "---\n" + yaml.stringify(msg)); + appendFileSync(DEBUG_FILE, "---\n" + stringify(msg)); else if (DEBUG_FORMAT === "json") appendFileSync(DEBUG_FILE, JSON.stringify(msg) + "\n"); else throw new Error(`Unrecognized DEBUG_FORMAT option`); @@ -119,7 +119,7 @@ export function outgoingHttpRequest( }; if (DEBUG_FORMAT === "yaml") - appendFileSync(DEBUG_FILE, "---\n" + yaml.stringify(msg)); + appendFileSync(DEBUG_FILE, "---\n" + stringify(msg)); else if (DEBUG_FORMAT === "json") appendFileSync(DEBUG_FILE, JSON.stringify(msg) + "\n"); else throw new Error(`Unrecognized DEBUG_FORMAT option`); @@ -147,7 +147,7 @@ export function outgoingHttpRequestError( }; if (DEBUG_FORMAT === "yaml") - appendFileSync(DEBUG_FILE, "---\n" + yaml.stringify(msg)); + appendFileSync(DEBUG_FILE, "---\n" + stringify(msg)); else if (DEBUG_FORMAT === "json") appendFileSync(DEBUG_FILE, JSON.stringify(msg) + "\n"); else throw new Error(`Unrecognized DEBUG_FORMAT option`); @@ -173,7 +173,7 @@ export function incomingHttpResponse( }; if (DEBUG_FORMAT === "yaml") - appendFileSync(DEBUG_FILE, "---\n" + yaml.stringify(msg)); + appendFileSync(DEBUG_FILE, "---\n" + stringify(msg)); else if (DEBUG_FORMAT === "json") appendFileSync(DEBUG_FILE, JSON.stringify(msg) + "\n"); else throw new Error(`Unrecognized DEBUG_FORMAT option`); @@ -197,7 +197,7 @@ export function outgoingUdpMessage( }; if (DEBUG_FORMAT === "yaml") - appendFileSync(DEBUG_FILE, "---\n" + yaml.stringify(msg)); + appendFileSync(DEBUG_FILE, "---\n" + stringify(msg)); else if (DEBUG_FORMAT === "json") appendFileSync(DEBUG_FILE, JSON.stringify(msg) + "\n"); else throw new Error(`Unrecognized DEBUG_FORMAT option`); diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index e16738d..4cfe31a 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -8167,7 +8167,8 @@ "yaml": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.0.tgz", - "integrity": "sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg==" + "integrity": "sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg==", + "dev": true }, "yargs": { "version": "16.0.3", diff --git a/package.json b/package.json index c8fa757..3aca7d4 100644 --- a/package.json +++ b/package.json @@ -33,8 +33,7 @@ "later": "^1.2.0", "mongodb": "^3.6.2", "parsimmon": "^1.16.0", - "seedrandom": "^3.0.5", - "yaml": "^1.10.0" + "seedrandom": "^3.0.5" }, "devDependencies": { "@rollup/plugin-commonjs": "^16.0.0", @@ -72,7 +71,8 @@ "svgo": "^1.3.2", "ts-node": "^9.0.0", "typescript": "^4.0.5", - "webpack": "^5.2.0" + "webpack": "^5.2.0", + "yaml": "^1.10.0" }, "engines": { "node": ">=12.13.0" diff --git a/test/yaml-tests.json b/test/yaml-tests.json new file mode 100644 index 0000000..d4f97ab --- /dev/null +++ b/test/yaml-tests.json @@ -0,0 +1,861 @@ +[ + [ + { + "name": "Mark McGwire", + "hr": 65, + "avg": 0.278 + }, + { + "name": "Sammy Sosa", + "hr": 63, + "avg": 0.288 + } + ], + { + "top1": { + "key1": "scalar1" + }, + "top2": { + "key2": "scalar2" + }, + "top3": { + "scalar1": "scalar3" + }, + "top4": { + "scalar2": "scalar4" + }, + "top5": "scalar5", + "top6": { + "key6": "scalar6" + } + }, + "text", + ["a", "b", 42, "d"], + { + "a!\"#$%&'()*+,-./09:;<=>?@AZ[\\]^_`az{|}~": "safe", + "?foo": "safe question mark", + ":foo": "safe colon", + "-foo": "safe dash", + "this is#not": "a comment" + }, + { + "": "a" + }, + "foo", + { + "key": "value", + "foo": "key" + }, + [1, -2, 33], + { + "plain": "a b\nc" + }, + [["s1_i1", "s1_i2"], "s2"], + { + "First occurrence": "Foo", + "Second occurrence": "Foo", + "Override anchor": "Bar", + "Reuse anchor": "Bar" + }, + "k:#foo &a !t s", + ["a"], + { + "escaped slash": "a/b" + }, + { + "plain": "This unquoted scalar spans many lines.", + "quoted": "So does this quoted scalar.\n" + }, + "here's to \"quotes\"", + { + "foo": "bar" + }, + "ab cd\nef\n\ngh\n", + ["detected\n", "\n\n# detected\n", " explicit\n", "detected\n"], + "foo: bar\": baz", + "plain\\value\\with\\backslashes", + { + "plain": "text lines", + "quoted": "text lines", + "block": "text\n \tlines\n" + }, + "a", + { + "foo": "you", + "bar": "far" + }, + { + "sequence": ["entry", ["nested"]], + "mapping": { + "foo": "bar" + } + }, + { + "literal": "some\ntext\n", + "folded": "some text\n" + }, + [ + { + "one": "two", + "three": "four" + }, + { + "five": "six", + "seven": "eight" + } + ], + { + "Folding": "Empty line\nas a line feed", + "Chomping": "Clipped empty lines\n" + }, + [ + ["one", "two"], + ["three", "four"] + ], + { + "foo": "bar" + }, + { + "key": "value" + }, + { + "explicit key": null, + "block key\n": ["one", "two"] + }, + ["foo"], + [ + { + "foo": "bar" + }, + ["baz", "baz"] + ], + "ab\n\n \n", + { + "foo: bar\\": "baz'" + }, + { + "Not indented": { + "By one space": "By four\n spaces\n", + "Flow style": ["By two", "Also by two", "Still by two"] + } + }, + "\\//||\\/||\n// || ||__\n", + { + "foo": [ + "a", + { + "key": "value" + } + ] + }, + { + "a": null, + "b": null + }, + "foo", + { + "a": "b", + "": "a" + }, + { + "foo\nbar:baz\tx \\$%^&*()x": 23, + "x\\ny:z\\tx $%^&*()x": 24 + }, + "Sammy Sosa completed another fine season with great stats.\n\n 63 Home Runs\n 0.288 Batting Average\n\nWhat a year!\n", + " foo\nbar\nbaz ", + { "matches %": 20 }, + [ + "flow in block", + "Block scalar\n", + { + "foo": "bar" + } + ], + { + "a": "b", + "c": 42, + "e": "f", + "g": "h", + "23": false + }, + "ab", + " 1st non-empty\n2nd non-empty 3rd non-empty ", + { + "top1": { + "key1": "one" + }, + "top2": { + "key2": "two" + }, + "top3": { + "key3": "three" + }, + "top4": { + "key4": "four" + }, + "top5": { + "key5": "five" + }, + "top6": "six", + "top7": "seven" + }, + { + "hr": ["Mark McGwire", "Sammy Sosa"], + "rbi": ["Sammy Sosa", "Ken Griffey"] + }, + "\nfolded line\nnext line\n * bullet\n\n * list\n * lines\n\nlast line\n", + ["word1", "word2"], + { + "a": null, + "b": null, + "c": null + }, + { + "nested sequences": [[[[]]], [[{}]]], + "key1": [], + "key2": {} + }, + "---word1 word2", + { + "implicit block key": [ + { + "implicit flow key": "value" + } + ] + }, + { + "key ends with two colons::": "value" + }, + [ + { + "single line": null, + "a": "b" + }, + { + "multi line": null, + "a": "b" + } + ], + "a", + { + "key": ["item1", "item2"] + }, + [ + "double quoted", + "single quoted", + "plain text", + ["nested"], + { + "single": "pair" + } + ], + ["unicode anchor"], + [ + { + "key": "value", + "key2": "value2" + }, + { + "key3": "value3" + } + ], + "trimmed\n\n\nas space", + "Mark McGwire's year was crippled by a knee injury.\n", + [ + { + "single line": null, + "a": "b" + }, + { + "multi line": null, + "a": "b" + } + ], + { + "a": { + "b": { + "c": "d" + }, + "e": { + "f": "g" + } + }, + "h": "i" + }, + { + "foo": { + "bar": "baz" + } + }, + [ + { + "single line": "value" + }, + { + "multi line": "value" + } + ], + { + "single": "text", + "double": "text" + }, + " 1st non-empty\n2nd non-empty 3rd non-empty ", + [ + { + "item": "Super Hoop", + "quantity": 1 + }, + { + "item": "Basketball", + "quantity": 4 + }, + { + "item": "Big Shoes", + "quantity": 1 + } + ], + "a b c d\ne", + { + "a": ["b", ["c", "d"]] + }, + { + "strip": "text", + "clip": "text\n", + "keep": "text\n" + }, + { + "a": "b c", + "d": "e f" + }, + ["single multiline - sequence entry"], + { + "1": [2, 3], + "4": 5 + }, + [ + { + "bla\"keks": "foo" + }, + { + "bla]keks": "foo" + } + ], + "folded text\n", + "foo", + { + "key": { + "a": "b" + } + }, + { + "adjacent": "value", + "readable": "value", + "empty": null + }, + [ + { + "a": "b" + }, + { + "c": "d" + }, + { + "e": "f" + }, + { + "g": "h" + } + ], + { + "tab": "\tstring" + }, + [ + { + "foo bar": "baz" + } + ], + { + "anchored": "value", + "alias": "value" + }, + ["explicit indent and chomp", "chomp and explicit indent"], + { + "a": ["b", "c"] + }, + { + "foo": "bar" + }, + [ + "::vector", + ": - ()", + "Up, up, and away!", + -123, + "http://example.com/foo#bar", + [ + "::vector", + ": - ()", + "Up, up and away!", + -123, + "http://example.com/foo#bar" + ] + ], + { + "a": "b", + "seq": ["a"], + "c": "d" + }, + ["foo", "bar", 42], + "line1 # no comment line3\n", + "\n\nliteral\n \n\ntext\n", + { + "a": "b" + }, + { + "k": ["a", "b"] + }, + "a b c d\ne", + "---word1 word2", + ["a", 2, 4, "d"], + { + "a": [ + "b", + "c", + { + "d": ["e", "f"] + } + ] + }, + { + "a": " more indented\nregular\n", + "b": "\n\n more indented\nregular\n" + }, + { + "strip": "# text", + "clip": "# text\n", + "keep": "# text\n\n" + }, + { + "safe": "a!\"#$%&'()*+,-./09:;<=>?@AZ[\\]^_`az{|}~ !\"#$%&'()*+,-./09:;<=>?@AZ[\\]^_`az{|}~", + "safe question mark": "?foo", + "safe colon": ":foo", + "safe dash": "-foo" + }, + "line1 line2 line3\n", + ["Mark McGwire", "Sammy Sosa", "Ken Griffey"], + ["a"], + ["a", ["b", "c"]], + { + "unicode": "Sosa did fine.☺", + "control": "\b1998\t1999\t2000\n", + "hex esc": "\r\n is \r\n", + "single": "\"Howdy!\" he cried.", + "quoted": " # Not a 'comment'.", + "tie-fighter": "|\\-*-/|" + }, + [["-", "-"]], + "folded text\n", + { + "a": 13, + "1.5": "d" + }, + { + "foo": 1, + "bar": 2, + "text": "a\n \nb\n\nc\n\nd\n" + }, + { + "wanted": "love ♥ and peace ☮" + }, + { + "name": "Mark McGwire", + "accomplishment": "Mark set a major league home run record in 1998.\n", + "stats": "65 Home Runs\n0.278 Batting Average\n" + }, + { + "foo": "bar", + "baz": "foo" + }, + "1st non-empty\n2nd non-empty 3rd non-empty", + { + "quoted": "Quoted \t", + "block": "void main() {\n\tprintf(\"Hello, world!\\n\");\n}\n" + }, + { + "foo": "blue", + "bar": "arrr", + "baz": "jazz" + }, + [ + { + "Mark McGwire": 65 + }, + { + "Sammy Sosa": 63 + }, + { + "Ken Griffy": 58 + } + ], + { + "1": 2, + "3": 4 + }, + { + "hr": ["Mark McGwire", "Sammy Sosa"], + "rbi": ["Sammy Sosa", "Ken Griffey"] + }, + "k:#foo &a !t s", + { + "block sequence": [ + "one", + { + "two": "three" + } + ] + }, + { + "First occurrence": "Value", + "Second occurrence": "Value" + }, + { + "a true": "null d", + "e 42": null + }, + { + "foo": "bar" + }, + ["foo", "bar", 42], + "trimmed\n\n\nas space", + "scalar", + { + "strip": "", + "clip": "", + "keep": "\n" + }, + { + "foo": { + "bar": 1 + }, + "baz": 2 + }, + { + "a": 47, + "c": "d" + }, + { + "implicit block key": [ + { + "implicit flow key": "value" + } + ] + }, + ["a", "b", "c", "c", ""], + [ + ["a", "b", "c"], + { + "a": "b", + "c": "d", + "e": "f" + }, + [] + ], + { + "implicit block key": [ + { + "implicit flow key": "value" + } + ] + }, + { + "a": "ab\n\ncd\nef\n" + }, + { + "literal": "value\n", + "folded": "value\n" + }, + { + "a": [ + "b", + "c", + { + "d": ["e", "f"] + } + ] + }, + "literal\n\ttext\n", + "foo \n\n\t bar\n\nbaz\n", + [ + { + "a": "b" + } + ], + "ab", + ["plain", "double quoted", "single quoted", "block\n", "plain again"], + { + "a": " ", + "b": " ", + "c": " ", + "d": " ", + "e": "\n", + "f": "\n", + "g": "\n\n", + "h": "\n\n" + }, + { + "key": "value with\ntabs" + }, + { + "": null + }, + [ + { + "single line": "value" + }, + { + "multi line": "value" + } + ], + "folded to a space,\nto a line feed, or \t \tnon-content", + ["literal\n", " folded\n", "keep\n\n", " strip"], + { + "key": "value" + }, + { + "american": ["Boston Red Sox", "Detroit Tigers", "New York Yankees"], + "national": ["New York Mets", "Chicago Cubs", "Atlanta Braves"] + }, + " 1st non-empty\n2nd non-empty 3rd non-empty ", + {}, + [ + ["a", "b"], + { + "a": "b" + }, + "a", + "b", + "c" + ], + "folded to a space,\nto a line feed, or \t \tnon-content", + [ + { + "foo": "bar" + } + ], + ["detected\n", "\n\n# detected\n", " explicit\n", "\t\ndetected\n"], + { + "top1": [ + "item1", + { + "key2": "value2" + }, + "item3" + ], + "top2": "value2" + }, + { + "foo": [42], + "bar": [44] + }, + { + "23": "d", + "a": 4.2 + }, + "Document", + { + "Date": "2001-11-23 15:03:17 -5", + "User": "ed", + "Fatal": "Unknown variable \"bar\"", + "Stack": [ + { + "file": "TopClass.py", + "line": 23, + "code": "x = MoreObject(\"345\\n\")\n" + }, + { "file": "MoreClass.py", "line": 58, "code": "foo = bar" } + ] + }, + { + "plain key": "in-line value", + "": null, + "quoted key": ["entry"] + }, + ["12", 12, "12"], + { + "aaa": "bbb" + }, + [":,"], + { + "sequence": ["one", "two"], + "mapping": { + "sky": "blue", + "sea": "green" + } + }, + { + "seq": ["a", "b"] + }, + "here's to \"quotes\"", + { + "hr": 65, + "avg": 0.278, + "rbi": 147 + }, + "\n\nliteral\n \n\ntext\n", + " 1st non-empty\n2nd non-empty 3rd non-empty ", + "literal\n\ttext\n", + { + "block mapping": { + "key": "value" + } + }, + " foo\nbar\nbaz ", + "ab cd\nef\n\ngh\n", + "foo", + { + "top1": { + "key1": "one" + }, + "top2": { + "key2": "two" + }, + "top3": { + "key3": "three" + }, + "top4": { + "key4": "four" + }, + "top5": { + "key5": "five" + }, + "top6": "six", + "top7": "seven" + }, + [ + { + "url": "http://example.org" + } + ], + { + "sequence": ["one", "two"], + "mapping": { + "sky": "blue", + "sea": "green" + } + }, + { + "invoice": 34843, + "date": "2001-01-23", + "bill-to": { + "given": "Chris", + "family": "Dumars", + "address": { + "lines": "458 Walkman Dr.\nSuite #292\n", + "city": "Royal Oak", + "state": "MI", + "postal": 48046 + } + }, + "ship-to": { + "given": "Chris", + "family": "Dumars", + "address": { + "lines": "458 Walkman Dr.\nSuite #292\n", + "city": "Royal Oak", + "state": "MI", + "postal": 48046 + } + }, + "product": [ + { + "sku": "BL394D", + "quantity": 4, + "description": "Basketball", + "price": 450 + }, + { + "sku": "BL4438H", + "quantity": 1, + "description": "Super Hoop", + "price": 2392 + } + ], + "tax": 251.42, + "total": 4443.52, + "comments": "Late afternoon is best. Backup contact is Nancy Billsmer @ 338-4338." + }, + ["a", "b", "a", "b"], + [ + null, + "block node\n", + ["one", "two"], + { + "one": "two" + } + ], + { + "a": "scalar a", + "b": "scalar a" + }, + { + "foo": "", + "": "bar" + }, + { + "key": "value" + }, + "scalar %YAML 1.2", + { + "Folding": "Empty line\nas a line feed", + "Chomping": "Clipped empty lines\n" + }, + { + "key": "value" + }, + [ + ["name", "hr", "avg"], + ["Mark McGwire", 65, 0.278], + ["Sammy Sosa", 63, 0.288] + ], + { + "literal": "value\n", + "folded": "value\n" + }, + { + "Mark McGwire": { + "hr": 65, + "avg": 0.278 + }, + "Sammy Sosa": { + "hr": 63, + "avg": 0.288 + } + }, + { + "a": "b", + "c": "d" + }, + { + "key": [[["value"]]] + }, + { + "a": 1, + "b": null, + "c": 3 + }, + { + "event": "outgoing HTTP response", + "timestamp": "2021-01-21T18:44:57.116Z", + "remoteAddress": "127.0.0.1", + "deviceId": "202BC1-BM632w-000000", + "connection": "2021-01-21T18:44:57.105Z", + "statusCode": 200, + "headers": { + "content-length": 528, + "server": "GenieACS/1.2.3+20210121000910", + "soapserver": "GenieACS/1.2.3+20210121000910", + "content-type": "text/xml; charset=\"utf-8\"", + "set-cookie": "session=ea31d97ccf6832fb" + }, + "body": "\nwdsx50vq1" + } +] diff --git a/test/yaml.ts b/test/yaml.ts new file mode 100644 index 0000000..6983efe --- /dev/null +++ b/test/yaml.ts @@ -0,0 +1,14 @@ +import ava from "ava"; +import * as yaml from "yaml"; +import { stringify } from "../lib/common/yaml"; + +import testCases from "./yaml-tests.json"; + +ava("stringify", (t) => { + for (const testCase of testCases) { + let str = stringify(testCase); + if (str.startsWith(">2")) str = ">3" + str.slice(2); + if (str.startsWith("|2")) str = "|3" + str.slice(2); + t.deepEqual(yaml.parse(yaml.stringify(testCase)), yaml.parse(str)); + } +}); diff --git a/ui/components/device-faults.ts b/ui/components/device-faults.ts index b38abf2..6acb5e0 100644 --- a/ui/components/device-faults.ts +++ b/ui/components/device-faults.ts @@ -22,6 +22,7 @@ import { m } from "../components"; import * as store from "../store"; import * as notifications from "../notifications"; import { getIcon } from "../icons"; +import { stringify } from "../../lib/common/yaml"; const component: ClosureComponent = (): Component => { return { @@ -38,6 +39,7 @@ const component: ClosureComponent = (): Component => { "Channel", "Code", "Message", + "Detail", "Retries", "Timestamp", ].map((l) => m("th", l)); @@ -48,7 +50,9 @@ const component: ClosureComponent = (): Component => { rows.push([ m("td", f["channel"]), m("td", f["code"]), - m("td", f["message"]), + m("td", m("long-text", { text: f["message"] })), + m("td", m("long-text", { text: stringify(f["detail"]) })), + m("td", f["retries"]), m("td", new Date(f["timestamp"]).toLocaleString()), m( diff --git a/ui/faults-page.ts b/ui/faults-page.ts index 4d46d4b..c2bd803 100644 --- a/ui/faults-page.ts +++ b/ui/faults-page.ts @@ -27,7 +27,7 @@ import * as notifications from "./notifications"; import memoize from "../lib/common/memoize"; import * as smartQuery from "./smart-query"; import { map, parse, stringify } from "../lib/common/expression-parser"; -import { loadYaml, yaml } from "./dynamic-loader"; +import { stringify as yamlStringify } from "../lib/common/yaml"; const PAGE_SIZE = config.ui.pageSize || 10; @@ -76,13 +76,7 @@ export function init( const sort = args.hasOwnProperty("sort") ? "" + args["sort"] : ""; const filter = args.hasOwnProperty("filter") ? "" + args["filter"] : ""; - return new Promise((resolve, reject) => { - loadYaml() - .then(() => { - resolve({ filter, sort }); - }) - .catch(reject); - }); + return Promise.resolve({ filter, sort }); } export const component: ClosureComponent = (): Component => { @@ -147,8 +141,11 @@ export const component: ClosureComponent = (): Component => { return m("a", { href: deviceHref }, fault["device"]); } + if (attr.id === "message") + return m("long-text", { text: fault["message"] }); + if (attr.id === "detail") - return m("long-text", { text: yaml.stringify(fault["detail"]) }); + return m("long-text", { text: yamlStringify(fault["detail"]) }); if (attr.id === "timestamp") return new Date(fault["timestamp"]).toLocaleString();