js-awe
Version:
Awesome js utils including - plan: An Asynchronous control flow with a functional taste - Chrono: record and visualize timelines in the console
1,738 lines (1,619 loc) • 608 kB
JavaScript
let myGlobal = typeof globalThis !== undefined ? globalThis : typeof window !== undefined ? window : typeof global !== undefined ? global : typeof self !== undefined ? self : typeof this !== undefined ? this : {}
let performance = myGlobal.performance
if(performance === undefined) performance = {}
import util from 'node:util';
const dict = {
upperLetters: ['A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z'],
lowerLetters: ['a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z'],
numbers: [0,1,2,3,4,5,6,7,8,9]
};
function anonymize(toAnonymize)
{
let changes = 0;
const result = toAnonymize
.split('')
.map(char => {
let toReturn;
const type = getTypeDict(char);
if(type)
toReturn = type[getRandomInt(type.length -1)];
else toReturn = char;
if(toReturn !== char) changes++;
return toReturn
})
.join('');
if(changes < toAnonymize.length/2) return Array.from({length: result.length}).fill('*').join('')
return result
}
function getRandomInt(max) {
return Math.floor(Math.random() * Math.floor(max));
}
function getTypeDict(char)
{
const type = char.charCodeAt();
if( type > 47 && type < 58 ) return dict.numbers
if( type > 64 && type < 91 ) return dict.upperLetters
if( type > 96 && type < 123 ) return dict.lowerLetters
return undefined
}
var collectionClone = clone$1;
/*
Deep clones all properties except functions
var arr = [1, 2, 3];
var subObj = {aa: 1};
var obj = {a: 3, b: 5, c: arr, d: subObj};
var objClone = clone(obj);
arr.push(4);
subObj.bb = 2;
obj; // {a: 3, b: 5, c: [1, 2, 3, 4], d: {aa: 1}}
objClone; // {a: 3, b: 5, c: [1, 2, 3], d: {aa: 1, bb: 2}}
*/
function clone$1(obj) {
let result = obj;
var type = {}.toString.call(obj).slice(8, -1);
if (type == 'Set') {
return new Set([...obj].map(value => clone$1(value)));
}
if (type == 'Map') {
return new Map([...obj].map(kv => [clone$1(kv[0]), clone$1(kv[1])]));
}
if (type == 'Date') {
return new Date(obj.getTime());
}
if (type == 'RegExp') {
return RegExp(obj.source, getRegExpFlags(obj));
}
if (type == 'Array' || type == 'Object') {
result = Array.isArray(obj) ? [] : {};
for (var key in obj) {
// include prototype properties
result[key] = clone$1(obj[key]);
}
}
// primitives and non-supported objects (e.g. functions) land here
return result;
}
function getRegExpFlags(regExp) {
if (typeof regExp.source.flags == 'string') {
return regExp.source.flags;
} else {
var flags = [];
regExp.global && flags.push('g');
regExp.ignoreCase && flags.push('i');
regExp.multiline && flags.push('m');
regExp.sticky && flags.push('y');
regExp.unicode && flags.push('u');
return flags.join('');
}
}
var global$1 = (typeof global !== "undefined" ? global :
typeof self !== "undefined" ? self :
typeof window !== "undefined" ? window : {});
/*
from https://github.com/substack/vm-browserify/blob/bfd7c5f59edec856dc7efe0b77a4f6b2fa20f226/index.js
MIT license no Copyright holder mentioned
*/
function Object_keys(obj) {
if (Object.keys) return Object.keys(obj)
else {
var res = [];
for (var key in obj) res.push(key);
return res;
}
}
function forEach$1(xs, fn) {
if (xs.forEach) return xs.forEach(fn)
else
for (var i = 0; i < xs.length; i++) {
fn(xs[i], i, xs);
}
}
var _defineProp;
function defineProp(obj, name, value) {
if (typeof _defineProp !== 'function') {
_defineProp = createDefineProp;
}
_defineProp(obj, name, value);
}
function createDefineProp() {
try {
Object.defineProperty({}, '_', {});
return function(obj, name, value) {
Object.defineProperty(obj, name, {
writable: true,
enumerable: false,
configurable: true,
value: value
});
};
} catch (e) {
return function(obj, name, value) {
obj[name] = value;
};
}
}
var globals = ['Array', 'Boolean', 'Date', 'Error', 'EvalError', 'Function',
'Infinity', 'JSON', 'Math', 'NaN', 'Number', 'Object', 'RangeError',
'ReferenceError', 'RegExp', 'String', 'SyntaxError', 'TypeError', 'URIError',
'decodeURI', 'decodeURIComponent', 'encodeURI', 'encodeURIComponent', 'escape',
'eval', 'isFinite', 'isNaN', 'parseFloat', 'parseInt', 'undefined', 'unescape'
];
function Context() {}
Context.prototype = {};
function Script(code) {
if (!(this instanceof Script)) return new Script(code);
this.code = code;
}
function otherRunInContext(code, context) {
var args = Object_keys(global$1);
args.push('with (this.__ctx__){return eval(this.__code__)}');
var fn = Function.apply(null, args);
return fn.apply({
__code__: code,
__ctx__: context
});
}
Script.prototype.runInContext = function(context) {
if (!(context instanceof Context)) {
throw new TypeError('needs a \'context\' argument.');
}
if (global$1.document) {
var iframe = global$1.document.createElement('iframe');
if (!iframe.style) iframe.style = {};
iframe.style.display = 'none';
global$1.document.body.appendChild(iframe);
var win = iframe.contentWindow;
var wEval = win.eval,
wExecScript = win.execScript;
if (!wEval && wExecScript) {
// win.eval() magically appears when this is called in IE:
wExecScript.call(win, 'null');
wEval = win.eval;
}
forEach$1(Object_keys(context), function(key) {
win[key] = context[key];
});
forEach$1(globals, function(key) {
if (context[key]) {
win[key] = context[key];
}
});
var winKeys = Object_keys(win);
var res = wEval.call(win, this.code);
forEach$1(Object_keys(win), function(key) {
// Avoid copying circular objects like `top` and `window` by only
// updating existing context properties or new properties in the `win`
// that was only introduced after the eval.
if (key in context || indexOf$1(winKeys, key) === -1) {
context[key] = win[key];
}
});
forEach$1(globals, function(key) {
if (!(key in context)) {
defineProp(context, key, win[key]);
}
});
global$1.document.body.removeChild(iframe);
return res;
}
return otherRunInContext(this.code, context);
};
Script.prototype.runInThisContext = function() {
var fn = new Function('code', 'return eval(code);');
return fn.call(global$1, this.code); // maybe...
};
Script.prototype.runInNewContext = function(context) {
var ctx = createContext(context);
var res = this.runInContext(ctx);
if (context) {
forEach$1(Object_keys(ctx), function(key) {
context[key] = ctx[key];
});
}
return res;
};
function createScript(code) {
return new Script(code);
}
function createContext(context) {
if (isContext(context)) {
return context;
}
var copy = new Context();
if (typeof context === 'object') {
forEach$1(Object_keys(context), function(key) {
copy[key] = context[key];
});
}
return copy;
}
function runInContext(code, contextifiedSandbox, options) {
var script = new Script(code);
return script.runInContext(contextifiedSandbox, options);
}
function runInThisContext(code, options) {
var script = new Script(code);
return script.runInThisContext(options);
}
function isContext(context) {
return context instanceof Context;
}
function runInNewContext(code, sandbox, options) {
var script = new Script(code);
return script.runInNewContext(sandbox, options);
}
var vm = {
runInContext: runInContext,
isContext: isContext,
createContext: createContext,
createScript: createScript,
Script: Script,
runInThisContext: runInThisContext,
runInNewContext: runInNewContext
};
/*
from indexOf
@ author tjholowaychuk
@ license MIT
*/
var _indexOf$1 = [].indexOf;
function indexOf$1(arr, obj){
if (_indexOf$1) return arr.indexOf(obj);
for (var i = 0; i < arr.length; ++i) {
if (arr[i] === obj) return i;
}
return -1;
}
/**
* @implements {IHooks}
*/
class Hooks {
/**
* @callback HookCallback
* @this {*|Jsep} this
* @param {Jsep} env
* @returns: void
*/
/**
* Adds the given callback to the list of callbacks for the given hook.
*
* The callback will be invoked when the hook it is registered for is run.
*
* One callback function can be registered to multiple hooks and the same hook multiple times.
*
* @param {string|object} name The name of the hook, or an object of callbacks keyed by name
* @param {HookCallback|boolean} callback The callback function which is given environment variables.
* @param {?boolean} [first=false] Will add the hook to the top of the list (defaults to the bottom)
* @public
*/
add(name, callback, first) {
if (typeof arguments[0] != 'string') {
// Multiple hook callbacks, keyed by name
for (let name in arguments[0]) {
this.add(name, arguments[0][name], arguments[1]);
}
} else {
(Array.isArray(name) ? name : [name]).forEach(function (name) {
this[name] = this[name] || [];
if (callback) {
this[name][first ? 'unshift' : 'push'](callback);
}
}, this);
}
}
/**
* Runs a hook invoking all registered callbacks with the given environment variables.
*
* Callbacks will be invoked synchronously and in the order in which they were registered.
*
* @param {string} name The name of the hook.
* @param {Object<string, any>} env The environment variables of the hook passed to all callbacks registered.
* @public
*/
run(name, env) {
this[name] = this[name] || [];
this[name].forEach(function (callback) {
callback.call(env && env.context ? env.context : env, env);
});
}
}
/**
* @implements {IPlugins}
*/
class Plugins {
constructor(jsep) {
this.jsep = jsep;
this.registered = {};
}
/**
* @callback PluginSetup
* @this {Jsep} jsep
* @returns: void
*/
/**
* Adds the given plugin(s) to the registry
*
* @param {object} plugins
* @param {string} plugins.name The name of the plugin
* @param {PluginSetup} plugins.init The init function
* @public
*/
register(...plugins) {
plugins.forEach(plugin => {
if (typeof plugin !== 'object' || !plugin.name || !plugin.init) {
throw new Error('Invalid JSEP plugin format');
}
if (this.registered[plugin.name]) {
// already registered. Ignore.
return;
}
plugin.init(this.jsep);
this.registered[plugin.name] = plugin;
});
}
}
// JavaScript Expression Parser (JSEP) 1.4.0
class Jsep {
/**
* @returns {string}
*/
static get version() {
// To be filled in by the template
return '1.4.0';
}
/**
* @returns {string}
*/
static toString() {
return 'JavaScript Expression Parser (JSEP) v' + Jsep.version;
}
// ==================== CONFIG ================================
/**
* @method addUnaryOp
* @param {string} op_name The name of the unary op to add
* @returns {Jsep}
*/
static addUnaryOp(op_name) {
Jsep.max_unop_len = Math.max(op_name.length, Jsep.max_unop_len);
Jsep.unary_ops[op_name] = 1;
return Jsep;
}
/**
* @method jsep.addBinaryOp
* @param {string} op_name The name of the binary op to add
* @param {number} precedence The precedence of the binary op (can be a float). Higher number = higher precedence
* @param {boolean} [isRightAssociative=false] whether operator is right-associative
* @returns {Jsep}
*/
static addBinaryOp(op_name, precedence, isRightAssociative) {
Jsep.max_binop_len = Math.max(op_name.length, Jsep.max_binop_len);
Jsep.binary_ops[op_name] = precedence;
if (isRightAssociative) {
Jsep.right_associative.add(op_name);
} else {
Jsep.right_associative.delete(op_name);
}
return Jsep;
}
/**
* @method addIdentifierChar
* @param {string} char The additional character to treat as a valid part of an identifier
* @returns {Jsep}
*/
static addIdentifierChar(char) {
Jsep.additional_identifier_chars.add(char);
return Jsep;
}
/**
* @method addLiteral
* @param {string} literal_name The name of the literal to add
* @param {*} literal_value The value of the literal
* @returns {Jsep}
*/
static addLiteral(literal_name, literal_value) {
Jsep.literals[literal_name] = literal_value;
return Jsep;
}
/**
* @method removeUnaryOp
* @param {string} op_name The name of the unary op to remove
* @returns {Jsep}
*/
static removeUnaryOp(op_name) {
delete Jsep.unary_ops[op_name];
if (op_name.length === Jsep.max_unop_len) {
Jsep.max_unop_len = Jsep.getMaxKeyLen(Jsep.unary_ops);
}
return Jsep;
}
/**
* @method removeAllUnaryOps
* @returns {Jsep}
*/
static removeAllUnaryOps() {
Jsep.unary_ops = {};
Jsep.max_unop_len = 0;
return Jsep;
}
/**
* @method removeIdentifierChar
* @param {string} char The additional character to stop treating as a valid part of an identifier
* @returns {Jsep}
*/
static removeIdentifierChar(char) {
Jsep.additional_identifier_chars.delete(char);
return Jsep;
}
/**
* @method removeBinaryOp
* @param {string} op_name The name of the binary op to remove
* @returns {Jsep}
*/
static removeBinaryOp(op_name) {
delete Jsep.binary_ops[op_name];
if (op_name.length === Jsep.max_binop_len) {
Jsep.max_binop_len = Jsep.getMaxKeyLen(Jsep.binary_ops);
}
Jsep.right_associative.delete(op_name);
return Jsep;
}
/**
* @method removeAllBinaryOps
* @returns {Jsep}
*/
static removeAllBinaryOps() {
Jsep.binary_ops = {};
Jsep.max_binop_len = 0;
return Jsep;
}
/**
* @method removeLiteral
* @param {string} literal_name The name of the literal to remove
* @returns {Jsep}
*/
static removeLiteral(literal_name) {
delete Jsep.literals[literal_name];
return Jsep;
}
/**
* @method removeAllLiterals
* @returns {Jsep}
*/
static removeAllLiterals() {
Jsep.literals = {};
return Jsep;
}
// ==================== END CONFIG ============================
/**
* @returns {string}
*/
get char() {
return this.expr.charAt(this.index);
}
/**
* @returns {number}
*/
get code() {
return this.expr.charCodeAt(this.index);
}
/**
* @param {string} expr a string with the passed in express
* @returns Jsep
*/
constructor(expr) {
// `index` stores the character number we are currently at
// All of the gobbles below will modify `index` as we move along
this.expr = expr;
this.index = 0;
}
/**
* static top-level parser
* @returns {jsep.Expression}
*/
static parse(expr) {
return new Jsep(expr).parse();
}
/**
* Get the longest key length of any object
* @param {object} obj
* @returns {number}
*/
static getMaxKeyLen(obj) {
return Math.max(0, ...Object.keys(obj).map(k => k.length));
}
/**
* `ch` is a character code in the next three functions
* @param {number} ch
* @returns {boolean}
*/
static isDecimalDigit(ch) {
return ch >= 48 && ch <= 57; // 0...9
}
/**
* Returns the precedence of a binary operator or `0` if it isn't a binary operator. Can be float.
* @param {string} op_val
* @returns {number}
*/
static binaryPrecedence(op_val) {
return Jsep.binary_ops[op_val] || 0;
}
/**
* Looks for start of identifier
* @param {number} ch
* @returns {boolean}
*/
static isIdentifierStart(ch) {
return ch >= 65 && ch <= 90 ||
// A...Z
ch >= 97 && ch <= 122 ||
// a...z
ch >= 128 && !Jsep.binary_ops[String.fromCharCode(ch)] ||
// any non-ASCII that is not an operator
Jsep.additional_identifier_chars.has(String.fromCharCode(ch)); // additional characters
}
/**
* @param {number} ch
* @returns {boolean}
*/
static isIdentifierPart(ch) {
return Jsep.isIdentifierStart(ch) || Jsep.isDecimalDigit(ch);
}
/**
* throw error at index of the expression
* @param {string} message
* @throws
*/
throwError(message) {
const error = new Error(message + ' at character ' + this.index);
error.index = this.index;
error.description = message;
throw error;
}
/**
* Run a given hook
* @param {string} name
* @param {jsep.Expression|false} [node]
* @returns {?jsep.Expression}
*/
runHook(name, node) {
if (Jsep.hooks[name]) {
const env = {
context: this,
node
};
Jsep.hooks.run(name, env);
return env.node;
}
return node;
}
/**
* Runs a given hook until one returns a node
* @param {string} name
* @returns {?jsep.Expression}
*/
searchHook(name) {
if (Jsep.hooks[name]) {
const env = {
context: this
};
Jsep.hooks[name].find(function (callback) {
callback.call(env.context, env);
return env.node;
});
return env.node;
}
}
/**
* Push `index` up to the next non-space character
*/
gobbleSpaces() {
let ch = this.code;
// Whitespace
while (ch === Jsep.SPACE_CODE || ch === Jsep.TAB_CODE || ch === Jsep.LF_CODE || ch === Jsep.CR_CODE) {
ch = this.expr.charCodeAt(++this.index);
}
this.runHook('gobble-spaces');
}
/**
* Top-level method to parse all expressions and returns compound or single node
* @returns {jsep.Expression}
*/
parse() {
this.runHook('before-all');
const nodes = this.gobbleExpressions();
// If there's only one expression just try returning the expression
const node = nodes.length === 1 ? nodes[0] : {
type: Jsep.COMPOUND,
body: nodes
};
return this.runHook('after-all', node);
}
/**
* top-level parser (but can be reused within as well)
* @param {number} [untilICode]
* @returns {jsep.Expression[]}
*/
gobbleExpressions(untilICode) {
let nodes = [],
ch_i,
node;
while (this.index < this.expr.length) {
ch_i = this.code;
// Expressions can be separated by semicolons, commas, or just inferred without any
// separators
if (ch_i === Jsep.SEMCOL_CODE || ch_i === Jsep.COMMA_CODE) {
this.index++; // ignore separators
} else {
// Try to gobble each expression individually
if (node = this.gobbleExpression()) {
nodes.push(node);
// If we weren't able to find a binary expression and are out of room, then
// the expression passed in probably has too much
} else if (this.index < this.expr.length) {
if (ch_i === untilICode) {
break;
}
this.throwError('Unexpected "' + this.char + '"');
}
}
}
return nodes;
}
/**
* The main parsing function.
* @returns {?jsep.Expression}
*/
gobbleExpression() {
const node = this.searchHook('gobble-expression') || this.gobbleBinaryExpression();
this.gobbleSpaces();
return this.runHook('after-expression', node);
}
/**
* Search for the operation portion of the string (e.g. `+`, `===`)
* Start by taking the longest possible binary operations (3 characters: `===`, `!==`, `>>>`)
* and move down from 3 to 2 to 1 character until a matching binary operation is found
* then, return that binary operation
* @returns {string|boolean}
*/
gobbleBinaryOp() {
this.gobbleSpaces();
let to_check = this.expr.substr(this.index, Jsep.max_binop_len);
let tc_len = to_check.length;
while (tc_len > 0) {
// Don't accept a binary op when it is an identifier.
// Binary ops that start with a identifier-valid character must be followed
// by a non identifier-part valid character
if (Jsep.binary_ops.hasOwnProperty(to_check) && (!Jsep.isIdentifierStart(this.code) || this.index + to_check.length < this.expr.length && !Jsep.isIdentifierPart(this.expr.charCodeAt(this.index + to_check.length)))) {
this.index += tc_len;
return to_check;
}
to_check = to_check.substr(0, --tc_len);
}
return false;
}
/**
* This function is responsible for gobbling an individual expression,
* e.g. `1`, `1+2`, `a+(b*2)-Math.sqrt(2)`
* @returns {?jsep.BinaryExpression}
*/
gobbleBinaryExpression() {
let node, biop, prec, stack, biop_info, left, right, i, cur_biop;
// First, try to get the leftmost thing
// Then, check to see if there's a binary operator operating on that leftmost thing
// Don't gobbleBinaryOp without a left-hand-side
left = this.gobbleToken();
if (!left) {
return left;
}
biop = this.gobbleBinaryOp();
// If there wasn't a binary operator, just return the leftmost node
if (!biop) {
return left;
}
// Otherwise, we need to start a stack to properly place the binary operations in their
// precedence structure
biop_info = {
value: biop,
prec: Jsep.binaryPrecedence(biop),
right_a: Jsep.right_associative.has(biop)
};
right = this.gobbleToken();
if (!right) {
this.throwError("Expected expression after " + biop);
}
stack = [left, biop_info, right];
// Properly deal with precedence using [recursive descent](http://www.engr.mun.ca/~theo/Misc/exp_parsing.htm)
while (biop = this.gobbleBinaryOp()) {
prec = Jsep.binaryPrecedence(biop);
if (prec === 0) {
this.index -= biop.length;
break;
}
biop_info = {
value: biop,
prec,
right_a: Jsep.right_associative.has(biop)
};
cur_biop = biop;
// Reduce: make a binary expression from the three topmost entries.
const comparePrev = prev => biop_info.right_a && prev.right_a ? prec > prev.prec : prec <= prev.prec;
while (stack.length > 2 && comparePrev(stack[stack.length - 2])) {
right = stack.pop();
biop = stack.pop().value;
left = stack.pop();
node = {
type: Jsep.BINARY_EXP,
operator: biop,
left,
right
};
stack.push(node);
}
node = this.gobbleToken();
if (!node) {
this.throwError("Expected expression after " + cur_biop);
}
stack.push(biop_info, node);
}
i = stack.length - 1;
node = stack[i];
while (i > 1) {
node = {
type: Jsep.BINARY_EXP,
operator: stack[i - 1].value,
left: stack[i - 2],
right: node
};
i -= 2;
}
return node;
}
/**
* An individual part of a binary expression:
* e.g. `foo.bar(baz)`, `1`, `"abc"`, `(a % 2)` (because it's in parenthesis)
* @returns {boolean|jsep.Expression}
*/
gobbleToken() {
let ch, to_check, tc_len, node;
this.gobbleSpaces();
node = this.searchHook('gobble-token');
if (node) {
return this.runHook('after-token', node);
}
ch = this.code;
if (Jsep.isDecimalDigit(ch) || ch === Jsep.PERIOD_CODE) {
// Char code 46 is a dot `.` which can start off a numeric literal
return this.gobbleNumericLiteral();
}
if (ch === Jsep.SQUOTE_CODE || ch === Jsep.DQUOTE_CODE) {
// Single or double quotes
node = this.gobbleStringLiteral();
} else if (ch === Jsep.OBRACK_CODE) {
node = this.gobbleArray();
} else {
to_check = this.expr.substr(this.index, Jsep.max_unop_len);
tc_len = to_check.length;
while (tc_len > 0) {
// Don't accept an unary op when it is an identifier.
// Unary ops that start with a identifier-valid character must be followed
// by a non identifier-part valid character
if (Jsep.unary_ops.hasOwnProperty(to_check) && (!Jsep.isIdentifierStart(this.code) || this.index + to_check.length < this.expr.length && !Jsep.isIdentifierPart(this.expr.charCodeAt(this.index + to_check.length)))) {
this.index += tc_len;
const argument = this.gobbleToken();
if (!argument) {
this.throwError('missing unaryOp argument');
}
return this.runHook('after-token', {
type: Jsep.UNARY_EXP,
operator: to_check,
argument,
prefix: true
});
}
to_check = to_check.substr(0, --tc_len);
}
if (Jsep.isIdentifierStart(ch)) {
node = this.gobbleIdentifier();
if (Jsep.literals.hasOwnProperty(node.name)) {
node = {
type: Jsep.LITERAL,
value: Jsep.literals[node.name],
raw: node.name
};
} else if (node.name === Jsep.this_str) {
node = {
type: Jsep.THIS_EXP
};
}
} else if (ch === Jsep.OPAREN_CODE) {
// open parenthesis
node = this.gobbleGroup();
}
}
if (!node) {
return this.runHook('after-token', false);
}
node = this.gobbleTokenProperty(node);
return this.runHook('after-token', node);
}
/**
* Gobble properties of of identifiers/strings/arrays/groups.
* e.g. `foo`, `bar.baz`, `foo['bar'].baz`
* It also gobbles function calls:
* e.g. `Math.acos(obj.angle)`
* @param {jsep.Expression} node
* @returns {jsep.Expression}
*/
gobbleTokenProperty(node) {
this.gobbleSpaces();
let ch = this.code;
while (ch === Jsep.PERIOD_CODE || ch === Jsep.OBRACK_CODE || ch === Jsep.OPAREN_CODE || ch === Jsep.QUMARK_CODE) {
let optional;
if (ch === Jsep.QUMARK_CODE) {
if (this.expr.charCodeAt(this.index + 1) !== Jsep.PERIOD_CODE) {
break;
}
optional = true;
this.index += 2;
this.gobbleSpaces();
ch = this.code;
}
this.index++;
if (ch === Jsep.OBRACK_CODE) {
node = {
type: Jsep.MEMBER_EXP,
computed: true,
object: node,
property: this.gobbleExpression()
};
if (!node.property) {
this.throwError('Unexpected "' + this.char + '"');
}
this.gobbleSpaces();
ch = this.code;
if (ch !== Jsep.CBRACK_CODE) {
this.throwError('Unclosed [');
}
this.index++;
} else if (ch === Jsep.OPAREN_CODE) {
// A function call is being made; gobble all the arguments
node = {
type: Jsep.CALL_EXP,
'arguments': this.gobbleArguments(Jsep.CPAREN_CODE),
callee: node
};
} else if (ch === Jsep.PERIOD_CODE || optional) {
if (optional) {
this.index--;
}
this.gobbleSpaces();
node = {
type: Jsep.MEMBER_EXP,
computed: false,
object: node,
property: this.gobbleIdentifier()
};
}
if (optional) {
node.optional = true;
} // else leave undefined for compatibility with esprima
this.gobbleSpaces();
ch = this.code;
}
return node;
}
/**
* Parse simple numeric literals: `12`, `3.4`, `.5`. Do this by using a string to
* keep track of everything in the numeric literal and then calling `parseFloat` on that string
* @returns {jsep.Literal}
*/
gobbleNumericLiteral() {
let number = '',
ch,
chCode;
while (Jsep.isDecimalDigit(this.code)) {
number += this.expr.charAt(this.index++);
}
if (this.code === Jsep.PERIOD_CODE) {
// can start with a decimal marker
number += this.expr.charAt(this.index++);
while (Jsep.isDecimalDigit(this.code)) {
number += this.expr.charAt(this.index++);
}
}
ch = this.char;
if (ch === 'e' || ch === 'E') {
// exponent marker
number += this.expr.charAt(this.index++);
ch = this.char;
if (ch === '+' || ch === '-') {
// exponent sign
number += this.expr.charAt(this.index++);
}
while (Jsep.isDecimalDigit(this.code)) {
// exponent itself
number += this.expr.charAt(this.index++);
}
if (!Jsep.isDecimalDigit(this.expr.charCodeAt(this.index - 1))) {
this.throwError('Expected exponent (' + number + this.char + ')');
}
}
chCode = this.code;
// Check to make sure this isn't a variable name that start with a number (123abc)
if (Jsep.isIdentifierStart(chCode)) {
this.throwError('Variable names cannot start with a number (' + number + this.char + ')');
} else if (chCode === Jsep.PERIOD_CODE || number.length === 1 && number.charCodeAt(0) === Jsep.PERIOD_CODE) {
this.throwError('Unexpected period');
}
return {
type: Jsep.LITERAL,
value: parseFloat(number),
raw: number
};
}
/**
* Parses a string literal, staring with single or double quotes with basic support for escape codes
* e.g. `"hello world"`, `'this is\nJSEP'`
* @returns {jsep.Literal}
*/
gobbleStringLiteral() {
let str = '';
const startIndex = this.index;
const quote = this.expr.charAt(this.index++);
let closed = false;
while (this.index < this.expr.length) {
let ch = this.expr.charAt(this.index++);
if (ch === quote) {
closed = true;
break;
} else if (ch === '\\') {
// Check for all of the common escape codes
ch = this.expr.charAt(this.index++);
switch (ch) {
case 'n':
str += '\n';
break;
case 'r':
str += '\r';
break;
case 't':
str += '\t';
break;
case 'b':
str += '\b';
break;
case 'f':
str += '\f';
break;
case 'v':
str += '\x0B';
break;
default:
str += ch;
}
} else {
str += ch;
}
}
if (!closed) {
this.throwError('Unclosed quote after "' + str + '"');
}
return {
type: Jsep.LITERAL,
value: str,
raw: this.expr.substring(startIndex, this.index)
};
}
/**
* Gobbles only identifiers
* e.g.: `foo`, `_value`, `$x1`
* Also, this function checks if that identifier is a literal:
* (e.g. `true`, `false`, `null`) or `this`
* @returns {jsep.Identifier}
*/
gobbleIdentifier() {
let ch = this.code,
start = this.index;
if (Jsep.isIdentifierStart(ch)) {
this.index++;
} else {
this.throwError('Unexpected ' + this.char);
}
while (this.index < this.expr.length) {
ch = this.code;
if (Jsep.isIdentifierPart(ch)) {
this.index++;
} else {
break;
}
}
return {
type: Jsep.IDENTIFIER,
name: this.expr.slice(start, this.index)
};
}
/**
* Gobbles a list of arguments within the context of a function call
* or array literal. This function also assumes that the opening character
* `(` or `[` has already been gobbled, and gobbles expressions and commas
* until the terminator character `)` or `]` is encountered.
* e.g. `foo(bar, baz)`, `my_func()`, or `[bar, baz]`
* @param {number} termination
* @returns {jsep.Expression[]}
*/
gobbleArguments(termination) {
const args = [];
let closed = false;
let separator_count = 0;
while (this.index < this.expr.length) {
this.gobbleSpaces();
let ch_i = this.code;
if (ch_i === termination) {
// done parsing
closed = true;
this.index++;
if (termination === Jsep.CPAREN_CODE && separator_count && separator_count >= args.length) {
this.throwError('Unexpected token ' + String.fromCharCode(termination));
}
break;
} else if (ch_i === Jsep.COMMA_CODE) {
// between expressions
this.index++;
separator_count++;
if (separator_count !== args.length) {
// missing argument
if (termination === Jsep.CPAREN_CODE) {
this.throwError('Unexpected token ,');
} else if (termination === Jsep.CBRACK_CODE) {
for (let arg = args.length; arg < separator_count; arg++) {
args.push(null);
}
}
}
} else if (args.length !== separator_count && separator_count !== 0) {
// NOTE: `&& separator_count !== 0` allows for either all commas, or all spaces as arguments
this.throwError('Expected comma');
} else {
const node = this.gobbleExpression();
if (!node || node.type === Jsep.COMPOUND) {
this.throwError('Expected comma');
}
args.push(node);
}
}
if (!closed) {
this.throwError('Expected ' + String.fromCharCode(termination));
}
return args;
}
/**
* Responsible for parsing a group of things within parentheses `()`
* that have no identifier in front (so not a function call)
* This function assumes that it needs to gobble the opening parenthesis
* and then tries to gobble everything within that parenthesis, assuming
* that the next thing it should see is the close parenthesis. If not,
* then the expression probably doesn't have a `)`
* @returns {boolean|jsep.Expression}
*/
gobbleGroup() {
this.index++;
let nodes = this.gobbleExpressions(Jsep.CPAREN_CODE);
if (this.code === Jsep.CPAREN_CODE) {
this.index++;
if (nodes.length === 1) {
return nodes[0];
} else if (!nodes.length) {
return false;
} else {
return {
type: Jsep.SEQUENCE_EXP,
expressions: nodes
};
}
} else {
this.throwError('Unclosed (');
}
}
/**
* Responsible for parsing Array literals `[1, 2, 3]`
* This function assumes that it needs to gobble the opening bracket
* and then tries to gobble the expressions as arguments.
* @returns {jsep.ArrayExpression}
*/
gobbleArray() {
this.index++;
return {
type: Jsep.ARRAY_EXP,
elements: this.gobbleArguments(Jsep.CBRACK_CODE)
};
}
}
// Static fields:
const hooks = new Hooks();
Object.assign(Jsep, {
hooks,
plugins: new Plugins(Jsep),
// Node Types
// ----------
// This is the full set of types that any JSEP node can be.
// Store them here to save space when minified
COMPOUND: 'Compound',
SEQUENCE_EXP: 'SequenceExpression',
IDENTIFIER: 'Identifier',
MEMBER_EXP: 'MemberExpression',
LITERAL: 'Literal',
THIS_EXP: 'ThisExpression',
CALL_EXP: 'CallExpression',
UNARY_EXP: 'UnaryExpression',
BINARY_EXP: 'BinaryExpression',
ARRAY_EXP: 'ArrayExpression',
TAB_CODE: 9,
LF_CODE: 10,
CR_CODE: 13,
SPACE_CODE: 32,
PERIOD_CODE: 46,
// '.'
COMMA_CODE: 44,
// ','
SQUOTE_CODE: 39,
// single quote
DQUOTE_CODE: 34,
// double quotes
OPAREN_CODE: 40,
// (
CPAREN_CODE: 41,
// )
OBRACK_CODE: 91,
// [
CBRACK_CODE: 93,
// ]
QUMARK_CODE: 63,
// ?
SEMCOL_CODE: 59,
// ;
COLON_CODE: 58,
// :
// Operations
// ----------
// Use a quickly-accessible map to store all of the unary operators
// Values are set to `1` (it really doesn't matter)
unary_ops: {
'-': 1,
'!': 1,
'~': 1,
'+': 1
},
// Also use a map for the binary operations but set their values to their
// binary precedence for quick reference (higher number = higher precedence)
// see [Order of operations](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_Precedence)
binary_ops: {
'||': 1,
'??': 1,
'&&': 2,
'|': 3,
'^': 4,
'&': 5,
'==': 6,
'!=': 6,
'===': 6,
'!==': 6,
'<': 7,
'>': 7,
'<=': 7,
'>=': 7,
'<<': 8,
'>>': 8,
'>>>': 8,
'+': 9,
'-': 9,
'*': 10,
'/': 10,
'%': 10,
'**': 11
},
// sets specific binary_ops as right-associative
right_associative: new Set(['**']),
// Additional valid identifier chars, apart from a-z, A-Z and 0-9 (except on the starting char)
additional_identifier_chars: new Set(['$', '_']),
// Literals
// ----------
// Store the values to return for the various literals we may encounter
literals: {
'true': true,
'false': false,
'null': null
},
// Except for `this`, which is special. This could be changed to something like `'self'` as well
this_str: 'this'
});
Jsep.max_unop_len = Jsep.getMaxKeyLen(Jsep.unary_ops);
Jsep.max_binop_len = Jsep.getMaxKeyLen(Jsep.binary_ops);
// Backward Compatibility:
const jsep = expr => new Jsep(expr).parse();
const stdClassProps = Object.getOwnPropertyNames(class Test {});
Object.getOwnPropertyNames(Jsep).filter(prop => !stdClassProps.includes(prop) && jsep[prop] === undefined).forEach(m => {
jsep[m] = Jsep[m];
});
jsep.Jsep = Jsep; // allows for const { Jsep } = require('jsep');
const CONDITIONAL_EXP = 'ConditionalExpression';
var ternary = {
name: 'ternary',
init(jsep) {
// Ternary expression: test ? consequent : alternate
jsep.hooks.add('after-expression', function gobbleTernary(env) {
if (env.node && this.code === jsep.QUMARK_CODE) {
this.index++;
const test = env.node;
const consequent = this.gobbleExpression();
if (!consequent) {
this.throwError('Expected expression');
}
this.gobbleSpaces();
if (this.code === jsep.COLON_CODE) {
this.index++;
const alternate = this.gobbleExpression();
if (!alternate) {
this.throwError('Expected expression');
}
env.node = {
type: CONDITIONAL_EXP,
test,
consequent,
alternate
};
// check for operators of higher priority than ternary (i.e. assignment)
// jsep sets || at 1, and assignment at 0.9, and conditional should be between them
if (test.operator && jsep.binary_ops[test.operator] <= 0.9) {
let newTest = test;
while (newTest.right.operator && jsep.binary_ops[newTest.right.operator] <= 0.9) {
newTest = newTest.right;
}
env.node.test = newTest.right;
newTest.right = env.node;
env.node = test;
}
} else {
this.throwError('Expected :');
}
}
});
}
};
// Add default plugins:
jsep.plugins.register(ternary);
const FSLASH_CODE = 47; // '/'
const BSLASH_CODE = 92; // '\\'
var index$1 = {
name: 'regex',
init(jsep) {
// Regex literal: /abc123/ig
jsep.hooks.add('gobble-token', function gobbleRegexLiteral(env) {
if (this.code === FSLASH_CODE) {
const patternIndex = ++this.index;
let inCharSet = false;
while (this.index < this.expr.length) {
if (this.code === FSLASH_CODE && !inCharSet) {
const pattern = this.expr.slice(patternIndex, this.index);
let flags = '';
while (++this.index < this.expr.length) {
const code = this.code;
if (code >= 97 && code <= 122 // a...z
|| code >= 65 && code <= 90 // A...Z
|| code >= 48 && code <= 57) {
// 0-9
flags += this.char;
} else {
break;
}
}
let value;
try {
value = new RegExp(pattern, flags);
} catch (e) {
this.throwError(e.message);
}
env.node = {
type: jsep.LITERAL,
value,
raw: this.expr.slice(patternIndex - 1, this.index)
};
// allow . [] and () after regex: /regex/.test(a)
env.node = this.gobbleTokenProperty(env.node);
return env.node;
}
if (this.code === jsep.OBRACK_CODE) {
inCharSet = true;
} else if (inCharSet && this.code === jsep.CBRACK_CODE) {
inCharSet = false;
}
this.index += this.code === BSLASH_CODE ? 2 : 1;
}
this.throwError('Unclosed Regex');
}
});
}
};
const PLUS_CODE = 43; // +
const MINUS_CODE = 45; // -
const plugin = {
name: 'assignment',
assignmentOperators: new Set(['=', '*=', '**=', '/=', '%=', '+=', '-=', '<<=', '>>=', '>>>=', '&=', '^=', '|=', '||=', '&&=', '??=']),
updateOperators: [PLUS_CODE, MINUS_CODE],
assignmentPrecedence: 0.9,
init(jsep) {
const updateNodeTypes = [jsep.IDENTIFIER, jsep.MEMBER_EXP];
plugin.assignmentOperators.forEach(op => jsep.addBinaryOp(op, plugin.assignmentPrecedence, true));
jsep.hooks.add('gobble-token', function gobbleUpdatePrefix(env) {
const code = this.code;
if (plugin.updateOperators.some(c => c === code && c === this.expr.charCodeAt(this.index + 1))) {
this.index += 2;
env.node = {
type: 'UpdateExpression',
operator: code === PLUS_CODE ? '++' : '--',
argument: this.gobbleTokenProperty(this.gobbleIdentifier()),
prefix: true
};
if (!env.node.argument || !updateNodeTypes.includes(env.node.argument.type)) {
this.throwError(`Unexpected ${env.node.operator}`);
}
}
});
jsep.hooks.add('after-token', function gobbleUpdatePostfix(env) {
if (env.node) {
const code = this.code;
if (plugin.updateOperators.some(c => c === code && c === this.expr.charCodeAt(this.index + 1))) {
if (!updateNodeTypes.includes(env.node.type)) {
this.throwError(`Unexpected ${env.node.operator}`);
}
this.index += 2;
env.node = {
type: 'UpdateExpression',
operator: code === PLUS_CODE ? '++' : '--',
argument: env.node,
prefix: false
};
}
}
});
jsep.hooks.add('after-expression', function gobbleAssignment(env) {
if (env.node) {
// Note: Binaries can be chained in a single expression to respect
// operator precedence (i.e. a = b = 1 + 2 + 3)
// Update all binary assignment nodes in the tree
updateBinariesToAssignments(env.node);
}
});
function updateBinariesToAssignments(node) {
if (plugin.assignmentOperators.has(node.operator)) {
node.type = 'AssignmentExpression';
updateBinariesToAssignments(node.left);
updateBinariesToAssignments(node.right);
} else if (!node.operator) {
Object.values(node).forEach(val => {
if (val && typeof val === 'object') {
updateBinariesToAssignments(val);
}
});
}
}
}
};
/* eslint-disable no-bitwise -- Convenient */
// register plugins
jsep.plugins.register(index$1, plugin);
jsep.addUnaryOp('typeof');
jsep.addLiteral('null', null);
jsep.addLiteral('undefined', undefined);
const BLOCKED_PROTO_PROPERTIES = new Set(['constructor', '__proto__', '__defineGetter__', '__defineSetter__']);
const SafeEval = {
/**
* @param {jsep.Expression} ast
* @param {Record<string, any>} subs
*/
evalAst(ast, subs) {
switch (ast.type) {
case 'BinaryExpression':
case 'LogicalExpression':
return SafeEval.evalBinaryExpression(ast, subs);
case 'Compound':
return SafeEval.evalCompound(ast, subs);
case 'ConditionalExpression':
return SafeEval.evalConditionalExpression(ast, subs);
case 'Identifier':
return SafeEval.evalIdentifier(ast, subs);
case 'Literal':
return SafeEval.evalLiteral(ast, subs);
case 'MemberExpression':
return SafeEval.evalMemberExpression(ast, subs);
case 'UnaryExpression':
return SafeEval.evalUnaryExpression(ast, subs);
case 'ArrayExpression':
return SafeEval.evalArrayExpression(ast, subs);
case 'CallExpression':
return SafeEval.evalCallExpression(ast, subs);
case 'AssignmentExpression':
return SafeEval.evalAssignmentExpression(ast, subs);
default:
throw SyntaxError('Unexpected expression', ast);
}
},
evalBinaryExpression(ast, subs) {
const result = {
'||': (a, b) => a || b(),
'&&': (a, b) => a && b(),
'|': (a, b) => a | b(),
'^': (a, b) => a ^ b(),
'&': (a, b) => a & b(),
// eslint-disable-next-line eqeqeq -- API
'==': (a, b) => a == b(),
// eslint-disable-next-line eqeqeq -- API
'!=': (a, b) => a != b(),
'===': (a, b) => a === b(),
'!==': (a, b) => a !== b(),
'<': (a, b) => a < b(),
'>': (a, b) => a > b(),
'<=': (a, b) => a <= b(),
'>=': (a, b) => a >= b(),
'<<': (a, b) => a << b(),
'>>': (a, b) => a >> b(),
'>>>': (a, b) => a >>> b(),
'+': (a, b) => a + b(),
'-': (a, b) => a - b(),
'*': (a, b) => a * b(),
'/': (a, b) => a / b(),
'%': (a, b) => a % b()
}[ast.operator](SafeEval.evalAst(ast.left, subs), () => SafeEval.evalAst(ast.right, subs));
return result;
},
evalCompound(ast, subs) {
let last;
for (let i = 0; i < ast.body.length; i++) {
if (ast.body[i].type === 'Identifier' && ['var', 'let', 'const'].includes(ast.body[i].name) && ast.body[i + 1] && ast.body[i + 1].type === 'AssignmentExpression') {
// var x=2; is detected as
// [{Identifier var}, {AssignmentExpression x=2}]
// eslint-disable-next-line @stylistic/max-len -- Long
// eslint-disable-next-line sonarjs/updated-loop-counter -- Convenient
i += 1;
}
const expr = ast.body[i];
last = SafeEval.evalAst(expr, subs);
}
return last;
},
evalConditionalExpression(ast, subs) {
if (SafeEval.evalAst(ast.test, subs)) {
return SafeEval.evalAst(ast.consequent, subs);
}
return SafeEval.evalAst(ast.alternate, subs);
},
evalIdentifier(ast, subs) {
if (Object.hasOwn(subs, ast.name)) {
return subs[ast.name];
}
throw ReferenceError(`${ast.name} is not defined`);
},
evalLiteral(ast) {
return ast.value;
},
evalMemberExpression(ast, subs) {
const prop = String(
// NOTE: `String(value)` throws error when
// value has overwritten the toString method to return non-string
// i.e. `value = {toString: () => []}`
ast.computed ? SafeEval.evalAst(ast.property) // `object[property]`
: ast.property.name // `object.property` property is Identifier
);
const obj = SafeEval.evalAst(ast.object, subs);
if (obj === undefined || obj === null) {
throw TypeError(`Cannot read properties of ${obj} (reading '${prop}')`);
}
if (!Object.hasOwn(obj, prop) && BLOCKED_PROTO_PROPERTIES.has(prop)) {
throw TypeError(`Cannot read properties of ${obj} (reading '${prop}')`);
}
const result = obj[prop];
if (typeof result === 'function') {
return result.bind(obj); // arrow functions aren't affected by bind.
}
return result;
},
evalUnaryExpression(ast, subs) {
const result = {
'-': a => -SafeEval.evalAst(a, subs),
'!': a => !SafeEval.evalAst(a, subs),
'~': a => ~SafeEval.evalAst(a, subs),
// eslint-disable-next-line no-implicit-coercion -- API
'+': a => +SafeEval.evalAst(a, subs),
typeof: a => typeof SafeEval.evalAst(a, subs)
}[ast.operator](ast.argument);
return result;
},
evalArrayExpression(ast, subs) {
return ast.elements.map(el => SafeEval.evalAst(el, subs));
},
evalCallExpression(ast, subs) {
const args = ast.arguments.map(arg => SafeEval.evalAst(arg, subs));
const func = SafeEval.evalAst(ast.callee, subs);
// if (func === Function) {
// throw new Error('Function constructor is disabled');
// }
return func(...args);
},
evalAssignmentExpression(ast, subs) {
if (ast.left.type !== 'Identifier') {
throw SyntaxError('Invalid left-hand side in assignment');
}
const id = ast.left.name;
const value = SafeEval.evalAst(ast.right, subs);
subs[id] = value;
return subs[id];
}
};
/**
* A replacement for NodeJS' VM.Script which is also {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP | Content Security Policy} friendly.
*/
class SafeScript {
/**
* @param {string} expr Expression to evaluate
*/
constructor(expr) {
this.code = expr;
this.ast = jsep(this.code);
}
/**
* @param {object} context Object whose items will be added
* to evaluation
* @returns {EvaluatedResult} Result of evaluated code
*/
runInNewContext(context) {
// `Object.create(null)` creates a prototypeless object
const keyMap = Object.assign(Object.create(null), context);
return SafeEval.evalAst(this.ast, keyMap);
}
}
/* eslint-disable camelcase -- Convenient for escaping */
/**
* @typedef {null|boolean|number|string|object|GenericArray} JSONObject
*/
/**
* @typedef {any} AnyItem
*/
/**
* @typedef {any} AnyResult
*/
/**
* Copies array and then pushes item into it.
* @param {GenericArray} arr Array to copy and into which to push
* @param {AnyItem} item Array item to add (to end)
* @returns {GenericArray} Copy of the original array
*/
function push(arr, item) {
arr = arr.slice();
arr.push(item);
return arr;
}
/**
* Copies array and then unshifts item into it.
* @param {AnyItem} item Array item to add (to beginning)
* @param {GenericArray} arr Array to copy and into which to unshift
* @returns {GenericArray} Copy of the original array
*/
function unshift(item, arr) {
arr = arr.slice();
arr.unshift(item);
return arr;
}
/**
* Caught when JSONPath is used without `new` but rethrown if with `new`
* @extends Error
*/
class NewError extends Error {
/**
* @param {AnyResult} value The evaluated scalar value
*/
constructor(value) {
super('JSONPath should not be called with "new" (it prevents return ' + 'of (unwrapped) scalar values)');
this.avoidNew = true;
this.value = value;
this.name = 'NewError';
}
}