-
Notifications
You must be signed in to change notification settings - Fork 8
/
main.js
307 lines (258 loc) · 12.7 KB
/
main.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
// a simple parser for Valve's KeyValue format
// https://developer.valvesoftware.com/wiki/KeyValues
//
// authors:
// Rossen Popov, 2014-2016
// p0358, 2019-2021
const TYPEEX = {
INT: /^\-?\d+$/,
FLOAT: /^\-?\d+\.\d+$/,
BOOLEAN: /^(true|false)$/i,
}
function parse(text, options) {
if (typeof text !== "string") {
throw new TypeError("VDF.parse: Expecting parameter to be a string");
}
options = {
types:
(typeof options === "boolean") ? options // backward compatibility with the old boolean param
: ((typeof options === "object" && "types" in options) ? options.types : true),
arrayify: (typeof options === "object" && "arrayify" in options) ? options.arrayify : true,
conditionals: (typeof options === "object" && "conditionals" in options) ? options.conditionals : undefined
};
if (options.conditionals && !Array.isArray(options.conditionals) && typeof options.conditionals === "string") options.conditionals = [options.conditionals];
var lines = text.split("\n");
var obj = {};
var stack = [obj];
var expect_bracket = false;
var odd = false;
var re_kv = new RegExp(
'^[ \\t]*' +
'("((?:\\\\.|[^\\\\"])+)"|([a-zA-Z0-9\\-\\_]+))' + // qkey, key
'([ \\t]*(' +
'"((?:\\\\.|[^\\\\"])*)(")?' + // qval, vq_end
'|([a-zA-Z0-9\\-\\_.]+)' + // val
'))?' +
'(?:[ \\t]*\\[(\\!?\\$[A-Z0-9]+(?:(?:[\\|]{2}|[\\&]{2})\\!?\\$[A-Z0-9]+)*)\\])?' // conditionals
);
var i = -1, j = lines.length, line, sublines;
var getNextLine = function() {
if (sublines && sublines.length)
{
var _subline = sublines.shift();
if (!odd) _subline = _subline.trim(); // we need to trim the line if outside of quoted value
return _subline;
}
var _line = lines[++i];
// skip empty and comment lines
// but only if we are not inside of a quote value
while ( !odd && _line !== undefined && (_line = _line.trim()) && (_line == "" || _line[0] == '/') )
_line = lines[++i];
if (_line === undefined)
return false; // this is the end
// make sure brackets are in separate lines, as this code assumes
// done separately to retain correct line numbers in errors
// skip tricky comments + add newlines around brackets, while making sure that slashes are not part of some key/value (inside quotes)
//var odd = false; // odd number of quotes encountered means we are inside of a quote value
var comment_slash_pos = -1;
sanitization: for (var l = 0; l < _line.length; l++) {
switch (_line.charAt(l)) {
case '"': if (_line.charAt(l-1) != '\\') odd = !odd; break;
case '/': if (!odd) { comment_slash_pos = l; break sanitization; } break;
case '{': if (!odd) { _line = _line.slice(0, l) + "\n{\n" + _line.slice(l+1); l+=2; } break;
case '}': if (!odd) { _line = _line.slice(0, l) + "\n}\n" + _line.slice(l+1); l+=2; } break;
}
}
if (comment_slash_pos > -1) _line = _line.substr(0, comment_slash_pos);
//if (!odd) _line = _line.trim(); // isn't that redundant?
sublines = _line.split("\n"); // no trim here
return getNextLine();
}
while ((line = getNextLine()) !== false) {
// skip empty and comment lines, again
if ( line == "" || line[0] == '/') { continue; }
// one level deeper
if ( line[0] == "{" ) {
expect_bracket = false;
continue;
}
if (expect_bracket) {
throw new SyntaxError("VDF.parse: invalid syntax on line " + (i+1) + " (expected opening bracket, empty unquoted values are not allowed):\n" + line);
}
// one level back
if ( line[0] == "}" ) {
if (Array.isArray(stack[stack.length-2])) stack.pop(); // if the element above is an array, we need to pop twice
stack.pop();
continue;
}
// parse keyvalue pairs
while (true) {
var m = re_kv.exec(line);
if (m === null) {
throw new SyntaxError("VDF.parse: invalid syntax on line " + (i+1) + ":\n" + line);
}
// qkey = 2
// key = 3
// qval = 6
// vq_end = 7
// val = 8
var key = (m[2] !== undefined) ? m[2] : m[3];
var val = (m[6] !== undefined) ? m[6] : m[8];
if (val === undefined) {
// parent key
// does not exist at all yet
if (stack[stack.length-1][key] === undefined /*|| typeof stack[stack.length-1][key] !== 'object'*/) {
stack[stack.length-1][key] = {};
stack.push(stack[stack.length-1][key]);
}
// exists already, is an object, but not an array
else if (stack[stack.length-1][key] !== undefined && !Array.isArray(stack[stack.length-1][key])) {
if (options.arrayify) {
// we turn it into an array to push the next object there
stack[stack.length-1][key] = [stack[stack.length-1][key], {}]; // turn current object into array with the object and new empty object
stack.push(stack[stack.length-1][key]); // push our array to stack
stack.push(stack[stack.length-1][1]); // push our newly created (2nd) object to stack
} else {
// push it on stack and let it get patched with new values
stack.push(stack[stack.length-1][key]);
}
}
// exists already, is an array of objects
else if (stack[stack.length-1][key] !== undefined && Array.isArray(stack[stack.length-1][key])) {
if (!options.arrayify)
throw new Error("VDF.parse: this code block should never be reached with arrayify set to false! [1]");
stack.push(stack[stack.length-1][key]); // push current array on stack
stack[stack.length-1].push({}); // append new object to that array
stack.push(stack[stack.length-1][ (stack[stack.length-1]).length-1 ]); // push that new (last) object on stack
}
expect_bracket = true;
}
else {
// value key
if (m[7] === undefined && m[8] === undefined) {
if (i + 1 >= j) {
throw new SyntaxError("VDF.parse: un-closed quotes at end of file");
}
line += "\n" + getNextLine();
continue;
}
if (options.conditionals !== undefined && Array.isArray(options.conditionals) && m[9]) {
var conditionals = m[9];
var single_cond_regex = new RegExp('^(\\|\\||&&)?(!)?\\$([A-Z0-9]+)');
var ok = false;
while (conditionals) {
var d = single_cond_regex.exec(conditionals);
if (d === null || !d[3])
throw new SyntaxError("VDF.parse: encountered an incorrect conditional: " + conditionals);
conditionals = conditionals.replace(d[0], '').trim(); // erase parsed fragment from the list
var op = d[1];
var not = d[2] && d[2] === '!';
var cond = d[3];
var includes = options.conditionals.indexOf(cond) !== -1;
var _ok = not ? !includes : includes;
if (!op || op === '||')
ok = ok || _ok;
else // &&
ok = ok && _ok;
}
//console.log('cond', key, val, _ok);
if (!ok) {
// conditions are not met
// continue processing the line (code duplicated from the bottom of our while loop)
line = line.replace(m[0], "");
if (!line || line[0] == '/') break; // break if there is nothing else (of interest) left in this line
continue;
}
}
if (options.types) {
if (TYPEEX.INT.test(val)) {
val = parseInt(val);
} else if (TYPEEX.FLOAT.test(val)) {
val = parseFloat(val);
} else if (TYPEEX.BOOLEAN.test(val)) {
val = val.toLowerCase() == "true";
}
}
// does not exist at all yet
if (stack[stack.length-1][key] === undefined) {
stack[stack.length-1][key] = val;
}
// exists already, but is not an array
else if (stack[stack.length-1][key] !== undefined && !Array.isArray(stack[stack.length-1][key])) {
if (options.arrayify) {
// we turn it into an array and push the next object there
stack[stack.length-1][key] = [stack[stack.length-1][key], val]; // turn current object into array with the old object and the new object
} else {
// replace it with the new value
stack[stack.length-1][key] = val;
}
}
// exists already, is an array
else if (stack[stack.length-1][key] !== undefined && Array.isArray(stack[stack.length-1][key])) {
if (!options.arrayify)
throw new Error("VDF.parse: this code block should never be reached with arrayify set to false! [2]");
stack[stack.length-1][key].push(val);
}
}
if (expect_bracket) break; // there was just key, no value, the next line should contain bracket (to go one level deeper)
line = line.replace(m[0], "").trim();
if (!line || line[0] == '/') break; // break if there is nothing else (of interest) left in this line
line = line.replace(/^\s*\[\!?\$[A-Z0-9]+(?:(?:[\|]{2}|[\&]{2})\!?\$[A-Z0-9]+)*\]/, "").trim(); // ignore conditionals
if (!line || line[0] == '/') break; // again; if there's nothing left after skipping the conditional
}
}
if (stack.length != 1) throw new SyntaxError("VDF.parse: open parentheses somewhere");
return obj;
}
function stringify(obj, options) {
if (typeof obj !== "object") {
throw new TypeError("VDF.stringify: First input parameter is not an object");
}
options = {
pretty:
(typeof options === "boolean") ? options // backward compatibility with the old boolean param
: ((typeof options === "object" && "pretty" in options) ? options.pretty : false),
indent: (typeof options === "object" && "indent" in options) ? options.indent : "\t"
};
return _dump(obj, options, 0);
}
function _dump(obj, options, level) {
if (typeof obj !== "object") {
throw new TypeError("VDF.stringify: a key has value of type other than string or object: " + typeof obj);
}
var indent = options.indent; // "\t"
var buf = "";
var line_indent = "";
if (options.pretty) {
for (var i = 0; i < level; i++ ) {
line_indent += indent;
}
}
for (var key in obj) {
if (typeof obj[key] === "object") {
if (Array.isArray(obj[key])) {
obj[key].forEach(function (element) {
if (typeof element !== "object") {
// de-arrayifying a non-object (strings etc)
// fake an object to write:
_element = {};
_element[key] = element;
buf += _dump(_element,options,level);
}
else {
// de-arrayifying an object
buf += [line_indent, '"', key, '"\n', line_indent, '{\n', _dump(element,options,level+1), line_indent, "}\n"].join('');
}
});
}
else
buf += [line_indent, '"', key, '"\n', line_indent, '{\n', _dump(obj[key],options,level+1), line_indent, "}\n"].join('');
}
else {
buf += [line_indent, '"', key, '" "', String(obj[key]), '"\n'].join('');
}
}
return buf;
}
exports.parse = parse;
exports.stringify = stringify;