UNPKG

cfg-reader

Version:

alt-config (alt:V configuration) file parser

487 lines (478 loc) 17.6 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var fs = require('fs'); var util = require('util'); class Detail { static Unescape(str) { let res = ""; for(let i = 0; i < str.length; i++){ let char = str[i]; if (char == "\\" && i != str.length - 1) { char = str[i + 1]; switch(char){ case "n": case "\n": res += "\n"; break; case "r": res += "\r"; break; case "'": case "\"": case "\\": res += char; break; default: res += "\\"; res += char; break; } continue; } res += char; } return res.trim(); } static Escape(str) { let res = ""; for(let i = 0; i < str.length; i++){ const char = str[i]; switch(char){ case "\n": res += "\\n"; break; case "\r": res += "\\r"; break; case "'": case "\"": case "\\": res += "\\"; res += char; break; default: res += char; break; } } return res; } } var NodeType; (function(NodeType) { NodeType[NodeType["None"] = 0] = "None"; NodeType[NodeType["Scalar"] = 1] = "Scalar"; NodeType[NodeType["List"] = 2] = "List"; NodeType[NodeType["Dict"] = 3] = "Dict"; })(NodeType || (NodeType = {})); class Node { constructor(type, val){ this.type = type; this.value = val; } } class Emitter { containsSpecials(value) { return /[:,'"\[\]\{\}]/gm.test(value); } emitNode(node, os, indent = 0, isLast = true) { const _indent = ' '.repeat(indent * 2); if (node.type === NodeType.Scalar) { os.write(`'${Detail.Escape(node.value)}',\n`); } else if (node.type === NodeType.List) { os.write('[\n'); const list = node.value; for(let i = 0; i < list.length; i++){ const it = list[i]; os.write(_indent); this.emitNode(it, os, indent + 1, i == list.length - 1); } os.write(`${' '.repeat((indent - 1) * 2)}${isLast ? ']\n' : '],\n'}`); } else if (node.type == NodeType.Dict) { if (indent > 0) os.write('{\n'); const dict = node.value; const keys = Object.keys(dict); for(let i = 0; i < keys.length; i++){ const key = keys[i]; if (dict[key].type == NodeType.None) continue; os.write(_indent + key + ':'); this.emitNode(dict[key], os, indent + 1, i == keys.length - 1); } if (indent > 0) os.write(`${' '.repeat((indent - 1) * 2)}${isLast ? '}\n' : '},\n'}`); } } emitConfigValue(value, indent = 0, isLast = true, commas = true, apostrophes = true) { const _indent = ' '.repeat(indent * 2); if (value instanceof Array) { //os.write('[\n'); this.stream += '[\n'; for(let i = 0; i < value.length; i++){ //os.write(_indent); this.stream += _indent; this.emitConfigValue(value[i], indent + 1, i == value.length - 1); } //os.write(_indent.repeat(indent - 1) + `${isLast || !commas ? ']\n' : '],\n'}`); this.stream += _indent.repeat(indent - 1) + `${isLast || !commas ? ']\n' : '],\n'}`; } else if (value instanceof Object) { if (indent > 0) //os.write('{\n'); this.stream += '{\n'; const keys = Object.keys(value); for(let i = 0; i < keys.length; i++){ const key = keys[i]; const _value = value[key]; if (_value == null) continue; //os.write(_indent + key + ':'); this.stream += _indent + key + ':'; this.emitConfigValue(_value, indent + 1, i == keys.length - 1); } if (indent > 0) //os.write(_indent.repeat(indent - 1) + `${isLast || !commas ? '}\n' : '},\n'}`); this.stream += _indent.repeat(indent - 1) + `${isLast || !commas ? '}\n' : '},\n'}`; } else { let escaped; if (typeof value === "boolean") { escaped = Detail.Escape(String(value)); } else if (typeof value === "string") { escaped = Detail.Escape(value); } else if (typeof value === "number") { escaped = Detail.Escape(value.toString()); } if (escaped === undefined) { throw new Error(`[CFG-READER] can not emit value of type: ${typeof value}. (you passed an invalid data type)`); } const useApostrophes = apostrophes || this.containsSpecials(escaped); this.stream += (useApostrophes ? "'" : '') + escaped + (useApostrophes ? "'" : '') + (commas ? ',' : '') + '\n'; } } constructor(){ this.stream = ""; } } var TokenType; (function(TokenType) { TokenType[TokenType["ArrayStart"] = 0] = "ArrayStart"; TokenType[TokenType["ArrayEnd"] = 1] = "ArrayEnd"; TokenType[TokenType["DictStart"] = 2] = "DictStart"; TokenType[TokenType["DictEnd"] = 3] = "DictEnd"; TokenType[TokenType["Key"] = 4] = "Key"; TokenType[TokenType["Scalar"] = 5] = "Scalar"; })(TokenType || (TokenType = {})); var ErrorType; (function(ErrorType) { ErrorType[ErrorType["KeyExpected"] = 0] = "KeyExpected"; ErrorType[ErrorType["InvalidToken"] = 1] = "InvalidToken"; ErrorType[ErrorType["UnexpectedEOF"] = 2] = "UnexpectedEOF"; })(ErrorType || (ErrorType = {})); let Token = class Token { constructor(_type, _value = "", _pos = 0, _line = 0, _col = 0){ this.type = _type; this.value = _value; this.pos = _pos; this.line = _line; this.col = _col; } }; class Parser { parse() { this.tokenize(); return this.parseToken(); } unread() { return this.buffer.length - this.readPos; } peek(offset = 0) { const idx = this.readPos + offset; return this.buffer[idx]; } get() { this.column++; if (this.peek() == '\n') { this.line++; this.column = 0; } //const currPos = this.readPos; //this.readPos++; //return this.buffer[currPos]; return this.buffer[this.readPos++]; } skip(n = 1) { for(let i = 0; i < n; i++){ this.column++; if (this.peek(i) == '\n') { this.line++; this.column = 0; } } this.readPos += n; } skipNextToken() { while(this.unread() > 0){ if (this.peek() == ' ' || this.peek() == '\n' || this.peek() == '\r' || this.peek() == '\t' || this.peek() == ',') { this.skip(); } else if (this.peek() == '#') { this.skip(); while(this.unread() > 0 && this.peek() != '\n' && this.peek() != '#'){ this.skip(); } if (this.unread() > 0) { this.skip(); } } else { break; } } } tokenize() { this.tokens.push(new Token(TokenType.DictStart)); while(this.unread() > 0){ this.skipNextToken(); if (this.unread() == 0) { break; } if (this.peek() == '[') { this.skip(); this.tokens.push(new Token(TokenType.ArrayStart, "", this.readPos, this.line, this.column)); } else if (this.peek() == ']') { this.skip(); this.tokens.push(new Token(TokenType.ArrayEnd, "", this.readPos, this.line, this.column)); } else if (this.peek() == '{') { this.skip(); this.tokens.push(new Token(TokenType.DictStart, "", this.readPos, this.line, this.column)); } else if (this.peek() == '}') { this.skip(); this.tokens.push(new Token(TokenType.DictEnd, "", this.readPos, this.line, this.column)); } else { let val = ""; if (this.peek() == '\'' || this.peek() == '"') { const start = this.get(); if (this.peek() != start) { while(this.unread() > 1 && (this.peek() == '\\' || this.peek(1) != start)){ if (this.peek() == '\n' || this.peek() == '\r') { if (this.get() == '\r' || this.peek() == '\n') { this.skip(); } val += "\n"; continue; } val += this.get(); } if (this.unread() > 0) { val += this.get(); } if (this.unread() == 0) { throw new Error(this.createParseError(ErrorType.UnexpectedEOF, this.line, this.column)); } } this.skip(); } else { while(this.unread() > 0 && this.peek() != '\n' && this.peek() != ':' && this.peek() != ',' && this.peek() != ']' && this.peek() != '}' && this.peek() != '#'){ val += this.get(); } } val = Detail.Unescape(val); if (this.unread() > 0 && this.peek() == ':') { this.tokens.push(new Token(TokenType.Key, val, this.readPos, this.line, this.column)); } else { this.tokens.push(new Token(TokenType.Scalar, val, this.readPos, this.line, this.column)); } if (this.unread() > 0 && (this.peek() == ':' || this.peek() == ',')) { this.skip(); } } } this.tokens.push(new Token(TokenType.DictEnd)); return; // end } createParseError(type, token, col) { let line; if (token instanceof Token) { col = token.col; line = token.line; } else line = token; line++; col++; const place = this.filePath ? `${this.filePath}:${line}:${col}` : `${line}:${col}`; const base = `[CFG-READER] error at line ${place} -> `; switch(type){ case ErrorType.KeyExpected: return base + `key expected`; case ErrorType.InvalidToken: return base + `invalid token`; case ErrorType.UnexpectedEOF: return base + `unexpected end of file`; } } parseToken() { const token = this.tokens[this.tokIdx]; switch(token.type){ case TokenType.Scalar: return new Node(NodeType.Scalar, token.value); case TokenType.ArrayStart: const list = new Node(NodeType.List, []); while(this.tokIdx < this.tokens.length - 1 && this.tokens[this.tokIdx + 1].type != TokenType.ArrayEnd){ this.tokIdx++; const node = this.parseToken(); list.value.push(node); } this.tokIdx++; return list; case TokenType.DictStart: const dict = new Node(NodeType.Dict, {}); while(this.tokIdx < this.tokens.length - 1 && this.tokens[this.tokIdx + 1].type != TokenType.DictEnd){ this.tokIdx++; const nextTok = this.tokens[this.tokIdx]; if (nextTok.type != TokenType.Key) { throw new Error(this.createParseError(ErrorType.KeyExpected, nextTok)); } const key = nextTok.value; this.tokIdx++; const node = this.parseToken(); dict.value[key] = node; } this.tokIdx++; return dict; } throw new Error(this.createParseError(ErrorType.InvalidToken, token)); } constructor(content, filePath){ this.tokens = []; this.readPos = 0; this.line = 0; this.column = 0; this.tokIdx = 0; this.buffer = content; this.filePath = filePath; } } class Config { existsFile(path) { return fs.existsSync(path); } createFile(path) { fs.writeFileSync(path, "", { encoding: "utf8" }); } loadFile(path) { this.content = fs.readFileSync(path, { encoding: "utf8" }); } // returns false when value is a float isInt(value) { return /^-?\d+$/.test(value); } isFloat(value) { const x = value.split("."); return x.length == 2 && x.every(this.isInt); } parseNode(node) { if (node.type == NodeType.Dict) { const dict = {}; for(const key in node.value){ const valueNode = node.value[key]; const value = this.parseNode(valueNode); dict[key] = value; } return dict; } else if (node.type == NodeType.List) { const length = node.value.length; const list = new Array(length); for(let i = 0; i < length; i++){ const valueNode = node.value[i]; const value = this.parseNode(valueNode); list[i] = value; } return list; } else if (node.type == NodeType.Scalar) { const value = node.value; if (value === "true" || value === "false" || value === "yes" || value === "no") { return value === "true" || value === "yes"; } else if (this.isInt(value) || this.isFloat(value)) { return parseFloat(value); } else { return value; } } return null; } parse() { if (this.content == null) { throw new Error(`[CFG-READER]: no file loaded (internal)`); } this.parser = new Parser(this.content, this.fileName); const node = this.parser.parse(); const config = this.parseNode(node); this.config = Object.assign(this.config, config); } /** * Get a config value with unknown type, slower than GetOfType * @param {string} key * @returns {ConfigValue} */ get(key) { return this.config[key]; } /** * Set a config value * @param {string} key * @param {ConfigValue} value */ set(key, value) { this.config[key] = value; } /** * Save the current changes to the opened file * @param {boolean} useCommas [default: true] * @param {boolean} useApostrophe [default: true] * * @returns {Promise<void>} */ async save(useCommas, useApostrophe) { if (!this.existsFile(this.fileName)) this.createFile(this.fileName); this.emitter = new Emitter(); this.emitter.emitConfigValue(this.config, 0, true, useCommas, useApostrophe); await util.promisify(fs.writeFile)(this.fileName, this.emitter.stream, { encoding: "utf8" }); } /** * Get a config value with known type, faster than normal Get * @param {string} key * @param {ValueType} type * @returns {ReturnValueType} */ getOfType(key) { return this.config[key]; } /** * Serialize config * @param {boolean} useCommas [default: true] * @param {boolean} useApostrophe [default: true] * @returns {string} */ serialize(useCommas, useApostrophe) { this.emitter = new Emitter(); this.emitter.emitConfigValue(this.config, 0, true, useCommas, useApostrophe); return this.emitter.stream; } /** * * @param {string} fileName * @param {Object} predefinedValues [optional] */ constructor(fileName, preDefines){ this.config = {}; if (typeof fileName !== "string") { throw new Error("[CFG-READER]: invalid constructor call, fileName must be type string"); } this.fileName = fileName; if (!(preDefines instanceof Object) && preDefines != null) { throw new Error("[CFG-READER]: invalid constructor call, preDefines must be null or Object"); } if (preDefines == null && this.existsFile(fileName)) { this.loadFile(fileName); this.parse(); } else if (preDefines instanceof Object) { this.config = preDefines; if (this.existsFile(fileName)) { this.loadFile(fileName); this.parse(); } } } } exports.Config = Config;