mork-parser
Version:
Mork file format parser
319 lines (257 loc) • 8.72 kB
JavaScript
/** Mozilla Mork (.mab, .msf) content parser **/
//Constructor
function MorkParser() {
var self = this;
this.key_table = null;
this.val_table = null;
this.row_hash = null;
this.skipped = 0;
this.total = 0;
//Returns an array of hashes
function mork_parse_content(body) {
//Reset global variables
self.key_table = { };
self.val_table = { };
self.row_hash = { };
self.skipped = 0;
self.total = 0;
//Local variables
var section_end_re = null;
var section = "top level";
//Windows Mozilla uses \r\n
body = body.replace(/\r\n/g, "\n");
//Presumably Mac Mozilla is similarly dumb
body = body.replace(/\r/g, "\n");
//Sometimes backslash is quoted with a backslash; convert to hex.
body = body.replace(/\\\\/g, "$5C");
//Close-paren is quoted with a backslash; convert to hex.
body = body.replace(/\\\)/g, "$29");
//Backslash at end of line is continuation.
body = body.replace(/\\\n/g, "");
//Figure out what we're looking at, and parse it.
while (body.trim().length > 0) {
var m;
//Comment
m = /^\s*\/\/.*?\n/g.exec(body);
if (m) {
var captured = m[1];
body = body.substring(m[0].length);
//no-op
continue;
}
//Key table <(a=c)>
m = /^\s*<\s*<\(a=c\)>[\S\s]+?(([^>]*))>\s*/g.exec(body);
if (m) {
var captured = m[1];
body = body.replace(m[0], "");
mork_parse_key_table(section, captured);
continue;
}
//Values <...>
m = /^\s*<([\S\s]*?\))>\s*/g.exec(body);
if (m) {
var captured = m[1];
body = body.replace(m[0], "");
mork_parse_value_table(section, captured);
continue;
}
//Table {...}
m = /^\s*\{-?[\dA-F]+:[\S\s]*?\{(([\S\s]*?\})([\S\s]*?\}))\s*/gi.exec(body);
if (m) {
var captured = m[1];
body = body.replace(m[0], "");
mork_parse_table(section, captured);
continue;
}
//Rows (-> table) [...]
m = /^\s*((\[[\S\s]*?\]\s*)+)/g.exec(body);
if (m) {
var captured = m[1];
body = body.replace(m[0], "");
mork_parse_table(section, captured);
continue;
}
//Section end
if (section_end_re) {
m = section_end_re.exec(body);
if (m) {
var captured = m[1];
body = body.replace(m[0], "");
section_end_re = null;
section = "top level";
continue;
}
}
//Section begin
m = /\@\$\$\{([\dA-F]+)\{\@\s*/gi.exec(body);
if (m) {
var captured = m[1];
section = captured;
body = body.replace(m[0], "");
section_end_re = new RegExp("^\\s*\\@\\$\\$\\}" + section + "\\}\\@\\s*", "g");
continue;
}
//Unknown segment
var segment = body.substring(0, 255 < body.length ? 255 : body.length);
console.error(section + ": Cannot parse");
console.error(segment);
return [ { error: "Cannot parse!", section: section, segment: segment } ];
}
if (section_end_re) {
console.error("Unterminated section " + section);
}
//Convert dictionary to array
var list = [ ];
var keys = Object.keys(self.row_hash);
for (let key of keys) {
list.push(self.row_hash[key]);
}
return list;
}
//Parse a row and column table
function mork_parse_table(section, table_part) {
//Assumption: no relevant spaces in values in this section
table_part = table_part.replace(/\s+/g, "");
//Grab each complete [...] block
var regex = /[^[]*\[([\S\s]+?)\]/g;
while ((m = regex.exec(table_part)) != null) {
var hash = { };
//break up the table - each line cosists of a $id and the rest are records
var parts = m[1].split(/[()]+/);
var id = parts[0];
var cells = parts;
//A long way of saying skip the line if there are no records in the cells array
if (cells.length < 1) {
continue;
}
//Trim junk
id = id.replace(/^-/g, "");
id = id.replace(/:[\S\s]*/g, "");
//Check that the id number we've been given corresponds to one we pulled out from the key_table index
if (self.row_hash[id]) {
hash = self.row_hash[id];
}
for (var i = 1; i < cells.length; i++) {
var cell = cells[i];
//Skip empty record
if (!cell || cell.trim().length == 0) {
continue;
}
//Extract key and value
var m = /^\^([-\dA-F]+)([\^=])([\S\s]*)$/gi.exec(cell);
if (!m) {
continue;
}
var keyi = m[1];
var which = m[2];
var vali = m[3];
//Empty value
if (!vali || vali.trim().length == 0) {
//console.error("Unparsable cell: " + cell);
}
//Ignore the key if it isn't in the key table
var key = self.key_table[keyi];
if (!key) {
continue;
}
var val = (which == '=' ? vali : self.val_table[vali]);
//Fix character encoding
val = mork_fix_encoding(val);
hash[key] = val;
}
self.total++;
self.row_hash[id] = hash;
}
}
//Parse a values table
function mork_parse_value_table(section, val_part) {
if (!val_part) {
return { };
}
//Extract pairs (key=value)
var pairs = val_part.split(/\(([^\)]+)\)/g);
for (let pair of pairs) {
//Skip empty line
if (pair.trim().length == 0) {
continue;
}
var m = /([\dA-F]*)[\t\n ]*=[\t\n ]*([\S\s]*)/gi.exec(pair);
if (!m) {
continue;
}
key = m[1];
val = m[2];
if (!val || val.trim().length == 0) {
//console.error(section + ": unparsable value: " + pair);
//continue;
}
//Approximate wchar_t -> ASCII and remove NULs
//val = mork_fix_encoding(val);
self.val_table[key] = val;
}
return self.val_table;
}
//Parse a key table
function mork_parse_key_table(section, key_part) {
//Remove comments (starting with "//" until the end of the line)
key_part = key_part.replace(/\s*\/\/.*$/gm, "");
//Extract pairs (key=value)
var pairs = key_part.split(/\(([^\)]+)\)/g);
//Convert to dictionary object
for (let pair of pairs) {
//Skip empty line
if (pair.trim().length == 0) {
continue;
}
//Parse key-value pairs
var m = /([\dA-F]+)\s*=\s*([\S\s]*)/gi.exec(pair);
if (m) {
key = m[1];
val = m[2];
self.key_table[key] = val;
}
}
return self.key_table;
}
//Fix character encoding, e.g. remove $00 but keep \$ff (escaped with slash)
function mork_fix_encoding(value) {
if (value && value.indexOf("$") >= 0) {
function fixASCII(m, m0, m1) {
var n1 = parseInt(m1, 16);
var ch = String.fromCharCode(n1); //Convert byte to ASCII
return m0 + ch;
}
function fixUTF8(m, m0, m1, m2) {
var n1 = parseInt(m1, 16);
var n2 = parseInt(m2, 16);
var ch = "?";
//Convert bytes to ASCII
//var ch = String.fromCharCode(n1, n2);
//Convert bytes to UTF-8
if (typeof Buffer != "undefined" && typeof Buffer.from == "function") {
ch = Buffer.from([ n1, n2 ]).toString("utf-8"); //Works with node.js
}
else if (typeof TextDecoder != "undefined") {
ch = new TextDecoder("utf-8").decode([ n1, n2 ]); //Works inside a web browser
}
else {
console.warn("Cannot decode character " + m.substring(1));
}
return m0 + ch;
}
return value
.replace(/([^\\])\$00/g, "$1")
.replace(/([^\\])\$([0-9A-Z][0-9A-Z])\$([0-9A-Z][0-9A-Z])/gi, fixUTF8) //Replace non-escaped $xx$yy but ignore \$xx$yy
.replace(/^()\$([0-9A-Z][0-9A-Z])\$([0-9A-Z][0-9A-Z])/gi, fixUTF8) //Replace value starting with $xx$yy
.replace(/([^\\])\$([0-9A-Z][0-9A-Z])/gi, fixASCII) //Replace non-escaped $xx but ignore \$xx
.replace(/^()\$([0-9A-Z][0-9A-Z])/gi, fixASCII); //Replace value starting with $xx
}
return value;
}
this._mork_parse_content = mork_parse_content;
}
//Parse mork content and return an array of objects
MorkParser.prototype.parse = function(content) {
return this._mork_parse_content(content);
};
module.exports = MorkParser;