assetgraph
Version:
An auto discovery dependency graph based optimization framework for web pages and applications
123 lines (113 loc) • 6.03 kB
JavaScript
// Javascript object literal parser
// Splits an object literal string into a set of top-level key-value pairs
// (c) Michael Best (https://github.com/mbest)
// License: MIT (http://www.opensource.org/licenses/mit-license.php)
// Version 2.1.0
// https://github.com/umdjs/umd
// Support AMD, Node.js, and globals
;(function (root, factory) {
if (typeof exports === 'object') {
// Node. Does not work with strict CommonJS, but
// only CommonJS-like enviroments that support module.exports,
// like Node.
module.exports = factory();
} else if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(factory);
} else {
// Browser globals (root is window)
root.parseObjectLiteral = factory();
}
}(this, function(undefined) {
// This parser is inspired by json-sans-eval by Mike Samuel (http://code.google.com/p/json-sans-eval/)
// These two match strings, either with double quotes or single quotes
const stringDouble = '"(?:[^"\\\\]|\\\\.)*"',
stringSingle = "'(?:[^'\\\\]|\\\\.)*'",
// Matches a regular expression (text enclosed by slashes), but will also match sets of divisions
// as a regular expression (this is handled by the parsing loop below).
stringRegexp = '/(?:[^/\\\\]|\\\\.)*/\w*',
// These characters have special meaning to the parser and must not appear in the middle of a
// token, except as part of a string.
specials = ',"\'{}()/:[\\]',
// Match text (at least two characters) that does not contain any of the above special characters,
// although some of the special characters are allowed to start it (all but the colon and comma).
// The text can contain spaces, but leading or trailing spaces are skipped.
everyThingElse = '[^\\s:,/][^' + specials + ']*[^\\s' + specials + ']',
// Match any non-space character not matched already. This will match colons and commas, since they're
// not matched by "everyThingElse", but will also match any other single character that wasn't already
// matched (for example: in "a: 1, b: 2", each of the non-space characters will be matched by oneNotSpace).
oneNotSpace = '[^\\s]',
// Create the actual regular expression by or-ing the above strings. The order is important.
token = RegExp(stringDouble + '|' + stringSingle + '|' + stringRegexp + '|' + everyThingElse + '|' + oneNotSpace, 'g'),
// Match end of previous token to determine whether a slash is a division or regex.
divisionLookBehind = /[\])"'A-Za-z0-9_$]+$/,
keywordRegexLookBehind = {'in':1,'return':1,'typeof':1};
function trim(string) {
return string == null ? '' :
string.trim ?
string.trim() :
string.toString().replace(/^[\s\xa0]+|[\s\xa0]+$/g, '');
}
return function(objectLiteralString) {
// Trim leading and trailing spaces from the string
let str = trim(objectLiteralString);
// Trim braces '{' surrounding the whole object literal
if (str.charCodeAt(0) === 123)
str = str.slice(1, -1);
// Split into tokens
const result = [];
let toks = str.match(token);
let key;
let values = [];
let depth = 0;
if (toks) {
// Append a comma so that we don't need a separate code block to deal with the last item
toks.push(',');
for (let i = 0, tok; tok = toks[i]; ++i) {
const c = tok.charCodeAt(0);
// A comma signals the end of a key/value pair if depth is zero
if (c === 44) { // ","
if (depth <= 0) {
if (!key && values.length === 1) {
key = values.pop();
}
result.push([key, values.length ? values.join('') : undefined]);
key = undefined;
values = [];
depth = 0;
continue;
}
// Simply skip the colon that separates the name and value
} else if (c === 58) { // ":"
if (!depth && !key && values.length === 1) {
key = values.pop();
continue;
}
// A set of slashes is initially matched as a regular expression, but could be division
} else if (c === 47 && i && tok.length > 1) { // "/"
// Look at the end of the previous token to determine if the slash is actually division
const match = toks[i-1].match(divisionLookBehind);
if (match && !keywordRegexLookBehind[match[0]]) {
// The slash is actually a division punctuator; re-parse the remainder of the string (not including the slash)
str = str.substr(str.indexOf(tok) + 1);
toks = str.match(token);
toks.push(',');
i = -1;
// Continue with just the slash
tok = '/';
}
// Increment depth for parentheses, braces, and brackets so that interior commas are ignored
} else if (c === 40 || c === 123 || c === 91) { // '(', '{', '['
++depth;
} else if (c === 41 || c === 125 || c === 93) { // ')', '}', ']'
--depth;
// The key will be the first token; if it's a string, trim the quotes
} else if (!key && !values.length && (c === 34 || c === 39)) { // '"', "'"
tok = tok.slice(1, -1);
}
values.push(tok);
}
}
return result;
};
}));