diff options
Diffstat (limited to 'devtools/shared/node-properties/node-properties.js')
-rw-r--r-- | devtools/shared/node-properties/node-properties.js | 776 |
1 files changed, 776 insertions, 0 deletions
diff --git a/devtools/shared/node-properties/node-properties.js b/devtools/shared/node-properties/node-properties.js new file mode 100644 index 0000000000..05feba857b --- /dev/null +++ b/devtools/shared/node-properties/node-properties.js @@ -0,0 +1,776 @@ +/** + * 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); + } +}; |