cfg-reader
Version:
alt-config (alt:V configuration) file parser
487 lines (478 loc) • 17.6 kB
JavaScript
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;
;