123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777 |
- /**
- * 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);
- }
- };
|