зеркало из https://github.com/mozilla/gecko-dev.git
777 строки
19 KiB
JavaScript
777 строки
19 KiB
JavaScript
/**
|
|
* The MIT License (MIT)
|
|
*
|
|
* Copyright (c) 2014 Gabriel Llamas
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
* of this software and associated documentation files (the "Software"), to deal
|
|
* in the Software without restriction, including without limitation the rights
|
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
* copies of the Software, and to permit persons to whom the Software is
|
|
* furnished to do so, subject to the following conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be included in
|
|
* all copies or substantial portions of the Software.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
* THE SOFTWARE.
|
|
*
|
|
*/
|
|
|
|
"use strict";
|
|
|
|
var hex = function (c){
|
|
switch (c){
|
|
case "0": return 0;
|
|
case "1": return 1;
|
|
case "2": return 2;
|
|
case "3": return 3;
|
|
case "4": return 4;
|
|
case "5": return 5;
|
|
case "6": return 6;
|
|
case "7": return 7;
|
|
case "8": return 8;
|
|
case "9": return 9;
|
|
case "a": case "A": return 10;
|
|
case "b": case "B": return 11;
|
|
case "c": case "C": return 12;
|
|
case "d": case "D": return 13;
|
|
case "e": case "E": return 14;
|
|
case "f": case "F": return 15;
|
|
}
|
|
};
|
|
|
|
var parse = function (data, options, handlers, control){
|
|
var c;
|
|
var code;
|
|
var escape;
|
|
var skipSpace = true;
|
|
var isCommentLine;
|
|
var isSectionLine;
|
|
var newLine = true;
|
|
var multiLine;
|
|
var isKey = true;
|
|
var key = "";
|
|
var value = "";
|
|
var section;
|
|
var unicode;
|
|
var unicodeRemaining;
|
|
var escapingUnicode;
|
|
var keySpace;
|
|
var sep;
|
|
var ignoreLine;
|
|
|
|
var line = function (){
|
|
if (key || value || sep){
|
|
handlers.line (key, value);
|
|
key = "";
|
|
value = "";
|
|
sep = false;
|
|
}
|
|
};
|
|
|
|
var escapeString = function (key, c, code){
|
|
if (escapingUnicode && unicodeRemaining){
|
|
unicode = (unicode << 4) + hex (c);
|
|
if (--unicodeRemaining) return key;
|
|
escape = false;
|
|
escapingUnicode = false;
|
|
return key + String.fromCharCode (unicode);
|
|
}
|
|
|
|
//code 117: u
|
|
if (code === 117){
|
|
unicode = 0;
|
|
escapingUnicode = true;
|
|
unicodeRemaining = 4;
|
|
return key;
|
|
}
|
|
|
|
escape = false;
|
|
|
|
//code 116: t
|
|
//code 114: r
|
|
//code 110: n
|
|
//code 102: f
|
|
if (code === 116) return key + "\t";
|
|
else if (code === 114) return key + "\r";
|
|
else if (code === 110) return key + "\n";
|
|
else if (code === 102) return key + "\f";
|
|
|
|
return key + c;
|
|
};
|
|
|
|
var isComment;
|
|
var isSeparator;
|
|
|
|
if (options._strict){
|
|
isComment = function (c, code, options){
|
|
return options._comments[c];
|
|
};
|
|
|
|
isSeparator = function (c, code, options){
|
|
return options._separators[c];
|
|
};
|
|
}else{
|
|
isComment = function (c, code, options){
|
|
//code 35: #
|
|
//code 33: !
|
|
return code === 35 || code === 33 || options._comments[c];
|
|
};
|
|
|
|
isSeparator = function (c, code, options){
|
|
//code 61: =
|
|
//code 58: :
|
|
return code === 61 || code === 58 || options._separators[c];
|
|
};
|
|
}
|
|
|
|
for (var i=~~control.resume; i<data.length; i++){
|
|
if (control.abort) return;
|
|
if (control.pause){
|
|
//The next index is always the start of a new line, it's a like a fresh
|
|
//start, there's no need to save the current state
|
|
control.resume = i;
|
|
return;
|
|
}
|
|
|
|
c = data[i];
|
|
code = data.charCodeAt (i);
|
|
|
|
//code 13: \r
|
|
if (code === 13) continue;
|
|
|
|
if (isCommentLine){
|
|
//code 10: \n
|
|
if (code === 10){
|
|
isCommentLine = false;
|
|
newLine = true;
|
|
skipSpace = true;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
//code 93: ]
|
|
if (isSectionLine && code === 93){
|
|
handlers.section (section);
|
|
//Ignore chars after the section in the same line
|
|
ignoreLine = true;
|
|
continue;
|
|
}
|
|
|
|
if (skipSpace){
|
|
//code 32: " " (space)
|
|
//code 9: \t
|
|
//code 12: \f
|
|
if (code === 32 || code === 9 || code === 12){
|
|
continue;
|
|
}
|
|
//code 10: \n
|
|
if (!multiLine && code === 10){
|
|
//Empty line or key w/ separator and w/o value
|
|
isKey = true;
|
|
keySpace = false;
|
|
newLine = true;
|
|
line ();
|
|
continue;
|
|
}
|
|
skipSpace = false;
|
|
multiLine = false;
|
|
}
|
|
|
|
if (newLine){
|
|
newLine = false;
|
|
if (isComment (c, code, options)){
|
|
isCommentLine = true;
|
|
continue;
|
|
}
|
|
//code 91: [
|
|
if (options.sections && code === 91){
|
|
section = "";
|
|
isSectionLine = true;
|
|
control.skipSection = false;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
//code 10: \n
|
|
if (code !== 10){
|
|
if (control.skipSection || ignoreLine) continue;
|
|
|
|
if (!isSectionLine){
|
|
if (!escape && isKey && isSeparator (c, code, options)){
|
|
//sep is needed to detect empty key and empty value with a
|
|
//non-whitespace separator
|
|
sep = true;
|
|
isKey = false;
|
|
keySpace = false;
|
|
//Skip whitespace between separator and value
|
|
skipSpace = true;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
//code 92: "\" (backslash)
|
|
if (code === 92){
|
|
if (escape){
|
|
if (escapingUnicode) continue;
|
|
|
|
if (keySpace){
|
|
//Line with whitespace separator
|
|
keySpace = false;
|
|
isKey = false;
|
|
}
|
|
|
|
if (isSectionLine) section += "\\";
|
|
else if (isKey) key += "\\";
|
|
else value += "\\";
|
|
}
|
|
escape = !escape;
|
|
}else{
|
|
if (keySpace){
|
|
//Line with whitespace separator
|
|
keySpace = false;
|
|
isKey = false;
|
|
}
|
|
|
|
if (isSectionLine){
|
|
if (escape) section = escapeString (section, c, code);
|
|
else section += c;
|
|
}else if (isKey){
|
|
if (escape){
|
|
key = escapeString (key, c, code);
|
|
}else{
|
|
//code 32: " " (space)
|
|
//code 9: \t
|
|
//code 12: \f
|
|
if (code === 32 || code === 9 || code === 12){
|
|
keySpace = true;
|
|
//Skip whitespace between key and separator
|
|
skipSpace = true;
|
|
continue;
|
|
}
|
|
key += c;
|
|
}
|
|
}else{
|
|
if (escape) value = escapeString (value, c, code);
|
|
else value += c;
|
|
}
|
|
}
|
|
}else{
|
|
if (escape){
|
|
if (!escapingUnicode){
|
|
escape = false;
|
|
}
|
|
skipSpace = true;
|
|
multiLine = true;
|
|
}else{
|
|
if (isSectionLine){
|
|
isSectionLine = false;
|
|
if (!ignoreLine){
|
|
//The section doesn't end with ], it's a key
|
|
control.error = new Error ("The section line \"" + section +
|
|
"\" must end with \"]\"");
|
|
return;
|
|
}
|
|
ignoreLine = false;
|
|
}
|
|
newLine = true;
|
|
skipSpace = true;
|
|
isKey = true;
|
|
|
|
line ();
|
|
}
|
|
}
|
|
}
|
|
|
|
control.parsed = true;
|
|
|
|
if (isSectionLine && !ignoreLine){
|
|
//The section doesn't end with ], it's a key
|
|
control.error = new Error ("The section line \"" + section + "\" must end" +
|
|
"with \"]\"");
|
|
return;
|
|
}
|
|
line ();
|
|
};
|
|
|
|
var INCLUDE_KEY = "include";
|
|
var INDEX_FILE = "index.properties";
|
|
|
|
var cast = function (value){
|
|
if (value === null || value === "null") return null;
|
|
if (value === "undefined") return undefined;
|
|
if (value === "true") return true;
|
|
if (value === "false") return false;
|
|
var v = Number (value);
|
|
return isNaN (v) ? value : v;
|
|
};
|
|
|
|
var expand = function (o, str, options, cb){
|
|
if (!options.variables || !str) return cb (null, str);
|
|
|
|
var stack = [];
|
|
var c;
|
|
var cp;
|
|
var key = "";
|
|
var section = null;
|
|
var v;
|
|
var holder;
|
|
var t;
|
|
var n;
|
|
|
|
for (var i=0; i<str.length; i++){
|
|
c = str[i];
|
|
|
|
if (cp === "$" && c === "{"){
|
|
key = key.substring (0, key.length - 1);
|
|
stack.push ({
|
|
key: key,
|
|
section: section
|
|
});
|
|
key = "";
|
|
section = null;
|
|
continue;
|
|
}else if (stack.length){
|
|
if (options.sections && c === "|"){
|
|
section = key;
|
|
key = "";
|
|
continue;
|
|
}else if (c === "}"){
|
|
holder = section !== null ? searchValue (o, section, true) : o;
|
|
if (!holder){
|
|
return cb (new Error ("The section \"" + section + "\" does not " +
|
|
"exist"));
|
|
}
|
|
|
|
v = options.namespaces ? searchValue (holder, key) : holder[key];
|
|
if (v === undefined){
|
|
//Read the external vars
|
|
v = options.namespaces
|
|
? searchValue (options._vars, key)
|
|
: options._vars[key]
|
|
|
|
if (v === undefined){
|
|
return cb (new Error ("The property \"" + key + "\" does not " +
|
|
"exist"));
|
|
}
|
|
}
|
|
|
|
t = stack.pop ();
|
|
section = t.section;
|
|
key = t.key + (v === null ? "" : v);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
cp = c;
|
|
key += c;
|
|
}
|
|
|
|
if (stack.length !== 0){
|
|
return cb (new Error ("Malformed variable: " + str));
|
|
}
|
|
|
|
cb (null, key);
|
|
};
|
|
|
|
var searchValue = function (o, chain, section){
|
|
var n = chain.split (".");
|
|
var str;
|
|
|
|
for (var i=0; i<n.length-1; i++){
|
|
str = n[i];
|
|
if (o[str] === undefined) return;
|
|
o = o[str];
|
|
}
|
|
|
|
var v = o[n[n.length - 1]];
|
|
if (section){
|
|
if (typeof v !== "object") return;
|
|
return v;
|
|
}else{
|
|
if (typeof v === "object") return;
|
|
return v;
|
|
}
|
|
};
|
|
|
|
var namespaceKey = function (o, key, value){
|
|
var n = key.split (".");
|
|
var str;
|
|
|
|
for (var i=0; i<n.length-1; i++){
|
|
str = n[i];
|
|
if (o[str] === undefined){
|
|
o[str] = {};
|
|
}else if (typeof o[str] !== "object"){
|
|
throw new Error ("Invalid namespace chain in the property name '" +
|
|
key + "' ('" + str + "' has already a value)");
|
|
}
|
|
o = o[str];
|
|
}
|
|
|
|
o[n[n.length - 1]] = value;
|
|
};
|
|
|
|
var namespaceSection = function (o, section){
|
|
var n = section.split (".");
|
|
var str;
|
|
|
|
for (var i=0; i<n.length; i++){
|
|
str = n[i];
|
|
if (o[str] === undefined){
|
|
o[str] = {};
|
|
}else if (typeof o[str] !== "object"){
|
|
throw new Error ("Invalid namespace chain in the section name '" +
|
|
section + "' ('" + str + "' has already a value)");
|
|
}
|
|
o = o[str];
|
|
}
|
|
|
|
return o;
|
|
};
|
|
|
|
var merge = function (o1, o2){
|
|
for (var p in o2){
|
|
try{
|
|
if (o1[p].constructor === Object){
|
|
o1[p] = merge (o1[p], o2[p]);
|
|
}else{
|
|
o1[p] = o2[p];
|
|
}
|
|
}catch (e){
|
|
o1[p] = o2[p];
|
|
}
|
|
}
|
|
return o1;
|
|
}
|
|
|
|
var build = function (data, options, dirname, cb){
|
|
var o = {};
|
|
|
|
if (options.namespaces){
|
|
var n = {};
|
|
}
|
|
|
|
var control = {
|
|
abort: false,
|
|
skipSection: false
|
|
};
|
|
|
|
if (options.include){
|
|
var remainingIncluded = 0;
|
|
|
|
var include = function (value){
|
|
if (currentSection !== null){
|
|
return abort (new Error ("Cannot include files from inside a " +
|
|
"section: " + currentSection));
|
|
}
|
|
|
|
var p = path.resolve (dirname, value);
|
|
if (options._included[p]) return;
|
|
|
|
options._included[p] = true;
|
|
remainingIncluded++;
|
|
control.pause = true;
|
|
|
|
read (p, options, function (error, included){
|
|
if (error) return abort (error);
|
|
|
|
remainingIncluded--;
|
|
merge (options.namespaces ? n : o, included);
|
|
control.pause = false;
|
|
|
|
if (!control.parsed){
|
|
parse (data, options, handlers, control);
|
|
if (control.error) return abort (control.error);
|
|
}
|
|
|
|
if (!remainingIncluded) cb (null, options.namespaces ? n : o);
|
|
});
|
|
};
|
|
}
|
|
|
|
if (!data){
|
|
if (cb) return cb (null, o);
|
|
return o;
|
|
}
|
|
|
|
var currentSection = null;
|
|
var currentSectionStr = null;
|
|
|
|
var abort = function (error){
|
|
control.abort = true;
|
|
if (cb) return cb (error);
|
|
throw error;
|
|
};
|
|
|
|
var handlers = {};
|
|
var reviver = {
|
|
assert: function (){
|
|
return this.isProperty ? reviverLine.value : true;
|
|
}
|
|
};
|
|
var reviverLine = {};
|
|
|
|
//Line handler
|
|
//For speed reasons, if "namespaces" is enabled, the old object is still
|
|
//populated, e.g.: ${a.b} reads the "a.b" property from { "a.b": 1 }, instead
|
|
//of having a unique object { a: { b: 1 } } which is slower to search for
|
|
//the "a.b" value
|
|
//If "a.b" is not found, then the external vars are read. If "namespaces" is
|
|
//enabled, the var "a.b" is split and it searches the a.b value. If it is not
|
|
//enabled, then the var "a.b" searches the "a.b" value
|
|
|
|
var line;
|
|
var error;
|
|
|
|
if (options.reviver){
|
|
if (options.sections){
|
|
line = function (key, value){
|
|
if (options.include && key === INCLUDE_KEY) return include (value);
|
|
|
|
reviverLine.value = value;
|
|
reviver.isProperty = true;
|
|
reviver.isSection = false;
|
|
|
|
value = options.reviver.call (reviver, key, value, currentSectionStr);
|
|
if (value !== undefined){
|
|
if (options.namespaces){
|
|
try{
|
|
namespaceKey (currentSection === null ? n : currentSection,
|
|
key, value);
|
|
}catch (error){
|
|
abort (error);
|
|
}
|
|
}else{
|
|
if (currentSection === null) o[key] = value;
|
|
else currentSection[key] = value;
|
|
}
|
|
}
|
|
};
|
|
}else{
|
|
line = function (key, value){
|
|
if (options.include && key === INCLUDE_KEY) return include (value);
|
|
|
|
reviverLine.value = value;
|
|
reviver.isProperty = true;
|
|
reviver.isSection = false;
|
|
|
|
value = options.reviver.call (reviver, key, value);
|
|
if (value !== undefined){
|
|
if (options.namespaces){
|
|
try{
|
|
namespaceKey (n, key, value);
|
|
}catch (error){
|
|
abort (error);
|
|
}
|
|
}else{
|
|
o[key] = value;
|
|
}
|
|
}
|
|
};
|
|
}
|
|
}else{
|
|
if (options.sections){
|
|
line = function (key, value){
|
|
if (options.include && key === INCLUDE_KEY) return include (value);
|
|
|
|
if (options.namespaces){
|
|
try{
|
|
namespaceKey (currentSection === null ? n : currentSection, key,
|
|
value);
|
|
}catch (error){
|
|
abort (error);
|
|
}
|
|
}else{
|
|
if (currentSection === null) o[key] = value;
|
|
else currentSection[key] = value;
|
|
}
|
|
};
|
|
}else{
|
|
line = function (key, value){
|
|
if (options.include && key === INCLUDE_KEY) return include (value);
|
|
|
|
if (options.namespaces){
|
|
try{
|
|
namespaceKey (n, key, value);
|
|
}catch (error){
|
|
abort (error);
|
|
}
|
|
}else{
|
|
o[key] = value;
|
|
}
|
|
};
|
|
}
|
|
}
|
|
|
|
//Section handler
|
|
var section;
|
|
if (options.sections){
|
|
if (options.reviver){
|
|
section = function (section){
|
|
currentSectionStr = section;
|
|
reviverLine.section = section;
|
|
reviver.isProperty = false;
|
|
reviver.isSection = true;
|
|
|
|
var add = options.reviver.call (reviver, null, null, section);
|
|
if (add){
|
|
if (options.namespaces){
|
|
try{
|
|
currentSection = namespaceSection (n, section);
|
|
}catch (error){
|
|
abort (error);
|
|
}
|
|
}else{
|
|
currentSection = o[section] = {};
|
|
}
|
|
}else{
|
|
control.skipSection = true;
|
|
}
|
|
};
|
|
}else{
|
|
section = function (section){
|
|
currentSectionStr = section;
|
|
if (options.namespaces){
|
|
try{
|
|
currentSection = namespaceSection (n, section);
|
|
}catch (error){
|
|
abort (error);
|
|
}
|
|
}else{
|
|
currentSection = o[section] = {};
|
|
}
|
|
};
|
|
}
|
|
}
|
|
|
|
//Variables
|
|
if (options.variables){
|
|
handlers.line = function (key, value){
|
|
expand (options.namespaces ? n : o, key, options, function (error, key){
|
|
if (error) return abort (error);
|
|
|
|
expand (options.namespaces ? n : o, value, options,
|
|
function (error, value){
|
|
if (error) return abort (error);
|
|
|
|
line (key, cast (value || null));
|
|
});
|
|
});
|
|
};
|
|
|
|
if (options.sections){
|
|
handlers.section = function (s){
|
|
expand (options.namespaces ? n : o, s, options, function (error, s){
|
|
if (error) return abort (error);
|
|
|
|
section (s);
|
|
});
|
|
};
|
|
}
|
|
}else{
|
|
handlers.line = function (key, value){
|
|
line (key, cast (value || null));
|
|
};
|
|
|
|
if (options.sections){
|
|
handlers.section = section;
|
|
}
|
|
}
|
|
|
|
parse (data, options, handlers, control);
|
|
if (control.error) return abort (control.error);
|
|
|
|
if (control.abort || control.pause) return;
|
|
|
|
if (cb) return cb (null, options.namespaces ? n : o);
|
|
return options.namespaces ? n : o;
|
|
};
|
|
|
|
var read = function (f, options, cb){
|
|
fs.stat (f, function (error, stats){
|
|
if (error) return cb (error);
|
|
|
|
var dirname;
|
|
|
|
if (stats.isDirectory ()){
|
|
dirname = f;
|
|
f = path.join (f, INDEX_FILE);
|
|
}else{
|
|
dirname = path.dirname (f);
|
|
}
|
|
|
|
fs.readFile (f, { encoding: "utf8" }, function (error, data){
|
|
if (error) return cb (error);
|
|
build (data, options, dirname, cb);
|
|
});
|
|
});
|
|
};
|
|
|
|
module.exports = function (data, options, cb){
|
|
if (typeof options === "function"){
|
|
cb = options;
|
|
options = {};
|
|
}
|
|
|
|
options = options || {};
|
|
var code;
|
|
|
|
if (options.include){
|
|
if (!cb) throw new Error ("A callback must be passed if the 'include' " +
|
|
"option is enabled");
|
|
options._included = {};
|
|
}
|
|
|
|
options = options || {};
|
|
options._strict = options.strict && (options.comments || options.separators);
|
|
options._vars = options.vars || {};
|
|
|
|
var comments = options.comments || [];
|
|
if (!Array.isArray (comments)) comments = [comments];
|
|
var c = {};
|
|
comments.forEach (function (comment){
|
|
code = comment.charCodeAt (0);
|
|
if (comment.length > 1 || code < 33 || code > 126){
|
|
throw new Error ("The comment token must be a single printable ASCII " +
|
|
"character");
|
|
}
|
|
c[comment] = true;
|
|
});
|
|
options._comments = c;
|
|
|
|
var separators = options.separators || [];
|
|
if (!Array.isArray (separators)) separators = [separators];
|
|
var s = {};
|
|
separators.forEach (function (separator){
|
|
code = separator.charCodeAt (0);
|
|
if (separator.length > 1 || code < 33 || code > 126){
|
|
throw new Error ("The separator token must be a single printable ASCII " +
|
|
"character");
|
|
}
|
|
s[separator] = true;
|
|
});
|
|
options._separators = s;
|
|
|
|
if (options.path){
|
|
if (!cb) throw new Error ("A callback must be passed if the 'path' " +
|
|
"option is enabled");
|
|
if (options.include){
|
|
read (data, options, cb);
|
|
}else{
|
|
fs.readFile (data, { encoding: "utf8" }, function (error, data){
|
|
if (error) return cb (error);
|
|
build (data, options, ".", cb);
|
|
});
|
|
}
|
|
}else{
|
|
return build (data, options, ".", cb);
|
|
}
|
|
};
|