/**
* JsEvalEngine
* Evaluate js while keeping track of it
*
* Copyright 2020, 2021 Rolf Bagge, Janus B. Kristensen, CAVI,
* Center for Advanced Visualization and Interaction, Aarhus University
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
function codeStratesEvalInContext(code, context) {
with(context) {
eval(code);
}
}
window.JsEvalEngine = class JsEvalEngine {
static async execute(code, options, fragment = null) {
options = Object.assign({}, JsEvalEngine.defaultOptions(fragment), options);
let resolver = null;
let rejector = null;
let asyncPromise = new Promise((resolve, reject)=>{
resolver = resolve;
rejector = reject;
});
let clonedConsole = Object.assign({}, console, options.customConsole);
let context = {
exports: {},
asyncResolve: resolver,
asyncReject: rejector,
console: clonedConsole,
fragmentSelfReference: fragment,
error: (e)=>{
let parsedStack = JsEvalEngine.parseErrorStack(e.name, e.stack, e);
EventSystem.triggerEvent("Codestrates.Fragment.Error", {
messages: [parsedStack],
fragment: fragment
});
let compactStack = StackWalker.compactify(parsedStack.stack);
let lineNumber = compactStack[0].lineNumber;
JsEvalEngine.doLog(console.error, fragment, lineNumber, parsedStack.extraReason, compactStack);
}
};
if(options.context != null) {
context = Object.assign({}, context, options.context);
}
let asyncCode = code;
if(options.async) {
asyncCode = JsEvalEngine.wrapInAsync(code, fragment != null ? "CS_ASYNC_" + fragment.uuid.replace("-", "_") : null);
}
try {
codeStratesEvalInContext.call(null, asyncCode, context);
if(!options.async) {
context.asyncResolve();
}
} catch(e) {
context.error(e);
throw e;
}
await asyncPromise.catch((e)=>{context.error(e); throw e});
return context[options.exportsName];
}
static wrapInAsync(code, methodName) {
if(methodName == null) {
methodName = "anonymousAsyncEval";
}
//Make code async
return `(async function ${methodName}() { ${code} \n })().then(()=>{asyncResolve();}).catch((e)=>{asyncReject(e);});`;
}
static parseErrorStack(name, stack, error) {
let parsedStackTrace = [];
let extraReason = null;
if(stack == null) {
console.warn("parseErrorStack: empty stack!");
return new StackWalker.StackTrace(
error,
[],
null
);
}
if(window.chrome) {
let stackSplit = stack.split("\n");
if(!stackSplit[0].trim().startsWith("at")) {
extraReason = stackSplit[0];
}
stackSplit.filter((line)=>{
return line.trim().startsWith("at");
}).forEach((line)=>{
let trimmedLine = line.trim();
let functionName = trimmedLine.substring(3, trimmedLine.indexOf("(")).trim();
let lineNumberAndPosition = trimmedLine.substring(trimmedLine.indexOf("),")+2).trim().split(":");
let lineNumber = parseInt(lineNumberAndPosition[1]);
if(Number.isNaN(lineNumber)) {
lineNumber = null;
}
parsedStackTrace.push({
method: functionName,
lineNumber: lineNumber,
debug: trimmedLine
});
});
} else {
console.log("Unsupported browser for parsing stack trace");
parsedStackTrace = stack;
}
return new StackWalker.StackTrace(
name,
parsedStackTrace,
extraReason
);
}
static defaultOptions(fragment = null) {
return {
context: null,
exportsName: "exports",
async: true,
customConsole: {
log: (...messages)=> {
JsEvalEngine.doLog(console.log, fragment, null, ...messages);
if(typeof EventSystem !== "undefined") {
EventSystem.triggerEvent("Codestrates.Fragment.Log", {
messages: messages,
fragment: fragment
});
}
},
warn: (...messages)=>{
JsEvalEngine.doLog(console.warn, fragment, null, ...messages);
if(typeof EventSystem !== "undefined") {
EventSystem.triggerEvent("Codestrates.Fragment.Warn", {
messages: messages,
fragment: fragment
});
}
},
error: (...messages)=>{
JsEvalEngine.doLog(console.error, fragment, null, ...messages);
if(typeof EventSystem !== "undefined") {
EventSystem.triggerEvent("Codestrates.Fragment.Error", {
messages: messages,
fragment: fragment
});
}
}
}
};
}
static doLog(logger, fragment, lineNumber, ...messages) {
if(fragment != null) {
let name = fragment.html[0].getAttribute("name");
let id = fragment.html[0].getAttribute("id");
logger("Fragment ["+(name!=null&&name.trim()!==""?name:fragment.type)+(id!=null&&id.trim()!==""?"#"+id:"")+(lineNumber!=null?":"+lineNumber:"")+"]", ...messages);
} else {
logger(...messages);
}
}
};
window.addEventListener("unhandledrejection", (evt)=>{
if(evt.reason != null) {
let parsedStack = JsEvalEngine.parseErrorStack(evt.reason.name, evt.reason.stack);
EventSystem.triggerEvent("Codestrates.Fragment.Error", {
messages: ["Uncaught rejection in promise: ", parsedStack]
});
} else {
console.warn("Did not include a reason property:", evt);
}
});
window.addEventListener("error", (evt)=>{
if(evt.error != null) {
let parsedStack = JsEvalEngine.parseErrorStack(evt.error.message, evt.error.stack);
EventSystem.triggerEvent("Codestrates.Fragment.Error", {
messages: ["Uncaught exception: ", parsedStack]
});
} else {
console.warn("Did not include an error property:", evt);
}
});
//Setup infinite stack depth
Error.stackTraceLimit = Infinity;