VDF в LUA (parse and stringify)

  • 20
  • 1
  • 12
Когда мне нужно было спарсить инфу о миникарте, я столкнулся с неизвестным мне типом синтаксиса, как я понял это VDF.
С помощью парсера можно переводить инфу валвовских файлов в луа таблицу
Lua:
local javaScript = [[
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;
}
return {parse: parse, stringify: stringify}
]]
local VDF = panorama.loadstring(javaScript)()
--VDF.parse, VDF.stringify
 
Сверху Снизу