(function() {
"use strict"
function parseStack(stack) {
let found = [], m
let re = /([\w$]*)@(.*?):(\d+)|\bat (?:([^\s(]+) \()?(.*?):(\d+)/g
while (m = re.exec(stack)) {
let fn = m[1] || m[4] || null
let file = m[2] || m[5] || null
if (fn && /sandbox/i.test(fn) || file && /sandbox/i.test(file)) break
found.push({fn, file, line: m[3] || m[6]})
}
return found
}
function frameString(frame) {
return "line " + frame.line + (frame.fn ? " in function " + frame.fn : "")
}
let SandBox = window.SandBox = class {
static create(options) {
return new Promise(done => {
let frame = document.createElement("iframe")
frame.addEventListener("load", loaded)
frame.src = "empty.html"
if (options.place) {
options.place(frame)
} else {
frame.style.display = "none"
document.body.appendChild(frame)
}
function loaded() {
frame.removeEventListener("load", loaded)
let box = new SandBox(frame)
promiseChain((options.loadFiles || []).map(file => () => {
let script = box.win.document.createElement("script")
script.src = file
box.win.document.body.appendChild(script)
return new Promise(done => script.addEventListener("load", done))
})).then(() => {
if (box.notify.onLoad) box.notify.onLoad()
done(box)
})
}
})
}
constructor(frame) {
this.startedAt = null
this.extraSecs = 2
this.output = null
this.handleDeps = true
this.notify = {}
this.callbacks = {}
// Used to cancel existing events when new code is loaded
this.timeouts = []; this.intervals = []; this.frames = []
this.framePos = 0
// Loaded CommonJS modules
this.loaded = window.loadedModules = new Cached(name => resolved.compute(name).then(({name, code}) => this.evalModule(name, code)))
this.frame = frame
this.win = frame.contentWindow
this.setupEnv()
const resize = () => {
if (this.frame.style.display != "none") this.resizeFrame()
}
this.frame.addEventListener("load", resize)
let resizeTimeout = null
const scheduleResize = () => {
this.win.clearTimeout(resizeTimeout)
this.win.__setTimeout(resize, 200)
}
this.win.addEventListener("keydown", scheduleResize)
this.win.addEventListener("mousedown", scheduleResize)
}
run(code, output, meta) {
if (output) this.output = output
this.startedAt = Date.now()
this.extraSecs = typeof code == "string" && /promtDirection/.test(code) ? 0.1 : 2
this.win.__c = 0
this.prepare(code)
.then(code => {
if (code instanceof Function) return code()
let value = this.win.eval(code)
if (this.notify.onRun) this.notify.onRun(code, meta)
return value
})
.catch(err => this.error(err))
}
prepare(text) {
let {code, dependencies} = preprocess(text, this)
return (this.handleDeps ? Promise.all(dependencies.map(dep => this.loaded.compute(dep))) : Promise.resolve([])).then(() => code)
}
evalModule(name, code) {
if (/\.json$/.test(name))
return this.loaded.store(name, {exports: JSON.parse(code)})
let work = findDeps(code).map(dep => this.loaded.compute(resolveRelative(name, dep)))
return Promise.all(work).then(() => {
let f = new this.win.Function("require, exports, module, __dirname, __filename",
code + "\n//# sourceURL=code" + randomID())
let module = this.loaded.store(name, {exports: {}})
f(dep => this.require(resolveRelative(name, dep)), module.exports, module, name, name)
return module
})
}
require(name) {
let found = resolved.get(name)
if (!found) throw new Error(`Could not load module '${name}'`)
return this.loaded.get(found.name).exports
}
setHTML(code, output, callback) {
this.clearEvents()
let loc = String(this.win.document.location)
if (loc != String(document.location) && !/\/empty\.html$/.test(loc)) {
this.frame.src = "empty.html"
let loaded = () => {
this.frame.removeEventListener("load", loaded)
this.setupEnv()
this.setHTML(code, output, callback)
}
this.frame.addEventListener("load", loaded)
return
}
let scriptTags = [], sandbox = this, doc = this.win.document
this.frame.style.display = "block"
doc.documentElement.innerHTML = code.replace(/<script\b[^>]*?(?:\bsrc\s*=\s*('[^']+'|"[^"]+"|[^\s>]+)[^>]*)?>([\s\S]*?)<\/script>/g, function(_, src, content) {
let tag = doc.createElement("script")
if (src) {
if (/["']/.test(src.charAt(0))) src = src.slice(1, src.length - 1)
tag.src = src
} else {
tag.text = preprocess(content, sandbox).code
}
scriptTags.push(tag)
return ""
})
this.frame.style.height = "80px"
this.resizeFrame()
if (output) this.output = output
function loadScript(i) {
if (i == scriptTags.length) {
if (i) setTimeout(() => {sandbox.resizeFrame()}, 50)
callback && callback()
return
}
sandbox.startedAt = Date.now()
sandbox.extraSecs = 2
sandbox.win.__c = 0
let tag = scriptTags[i]
if (tag.src) {
tag.addEventListener("load", () => { loadScript(i + 1) })
} else {
let id = randomID()
sandbox.callbacks[id] = () => { delete sandbox.callbacks[id]; loadScript(i + 1) }
tag.text += "\n;__sandbox.callbacks['" + id + "']();"
}
doc.body.appendChild(tag)
}
loadScript(0)
}
setupEnv() {
let win = this.win
win.__sandbox = this
win.onerror = (e, _file, line) => {
if (!this.output) return
this.output.out("error", [e + (line != null ? " (line " + line + ")" : "")])
return true
}
win.console = {
log: (...args) => this.out("log", args),
error: (...args) => this.out("error", args),
warn: (...args) => this.out("warn", args),
info: (...args) => this.out("log", args)
}
win.setInterval(() => {
this.startedAt = null
}, 1000)
win.__setTimeout = win.setTimeout
win.__setInterval = win.setInterval
win.setTimeout = (code, time, ...args) => {
if (args.length && typeof code != "string") {
let f = code
code = () => f(...args)
}
let val = win.__setTimeout(() => this.run(code), time)
this.timeouts.push(val)
return val
}
win.setInterval = (code, time, ...args) => {
if (args.length && typeof code != "string") {
let f = code
code = () => f(...args)
}
let val = win.__setInterval(() => this.run(code), time)
this.intervals.push(val)
return val
}
let reqAnimFrame = win.requestAnimationFrame
if (!reqAnimFrame) ["webkit", "moz", "ms", "o"].forEach(prefix => {
let val = win[prefix + "RequestAnimationFrame"]
if (val) {
reqAnimFrame = val
win.cancelAnimationFrame = prefix + "CancelAnimationFrame"
}
})
if (!reqAnimFrame) {
reqAnimFrame = f => this.__setTimeout(f, 50)
win.cancelAnimationFrame = win.clearTimeout
}
win.requestAnimationFrame = f => {
let val = reqAnimFrame.call(win, f)
if (this.frames.length > 50)
this.frames[this.framePos++ % 50] = val
else
this.frames.push(val)
return val
}
win.addEventListener("unhandledrejection", e => this.error(e.reason))
win.require = name => this.require(name)
win.require.preload = (name, code) => resolved.store(name, {name, code})
win.module = {exports: {}}
win.exports = win.module.exports
}
resizeFrame() {
this.frame.style.height = Math.max(80, Math.min(this.win.document.documentElement.offsetHeight + 10, 500)) + "px"
let box = this.frame.getBoundingClientRect()
if (box.bottom > box.top && box.top >= 0 && box.top < window.innerHeight && box.bottom > window.innerHeight)
window.scrollBy(0, Math.min(box.top, box.bottom - window.innerHeight))
}
tick() {
let now = Date.now()
if (this.startedAt == null) this.startedAt = now
if (now < this.startedAt + this.extraSecs * 1000) return
let bail = confirm("This code has been running for " + this.extraSecs +
" seconds. Abort it?")
this.startedAt += Date.now() - now
this.extraSecs += Math.max(this.extraSecs, 8)
if (bail) throw new Error("Aborted")
}
out(type, args) {
if (this.output) this.output.out(type, args)
else console[type].apply(console, args)
}
error(exception) {
if (!this.output) throw exception
let stack = parseStack(exception.stack)
this.output.out("error", [String(exception) + (stack.length ? " (" + frameString(stack[0]) + ")" : "")])
if (stack.length > 1) {
this.output.div.lastChild.appendChild(document.createTextNode(" "))
let mark = this.output.div.lastChild.appendChild(document.createElement("span"))
mark.innerHTML = "â¦"
mark.className = "sandbox-output-etc"
mark.addEventListener("click", () => {
mark.className = ""
mark.innerHTML = "\n called from " + stack.slice(1).map(frameString).join("\n called from ")
})
}
}
clearEvents() {
while (this.timeouts.length) this.win.clearTimeout(this.timeouts.pop())
while (this.intervals.length) this.win.clearInterval(this.intervals.pop())
while (this.frames.length) this.win.cancelAnimationFrame(this.frames.pop())
this.timeouts.length = this.intervals.length = this.frames.length = this.framePos = 0
}
}
function preprocess(code, sandbox) {
if (typeof code != "string") {
if (code.apply) {
let orig = code
code = (...args) => {
try { return orig.apply(null, args) }
catch(e) { sandbox.error(e) }
}
}
return {code, dependencies: []}
}
if (!/\n$/.test(code)) code += "\n"
let strict = /^(\s|\/\/.*)*["']use strict['"]/.test(code), ast
try { ast = acorn.parse(code, {sourceType: detectSourceType(code)}) }
catch(e) { return {code, dependencies: []} }
let patches = []
let backJump = ";if (++__c % 1000 === 0) __sandbox.tick();"
function loop(node) {
if (node.body.type == "BlockStatement") {
patches.push({from: node.body.end - 1, text: backJump})
} else {
patches.push({from: node.body.start, text: "{"},
{from: node.body.end, text: backJump + "}"})
}
}
let dependencies = []
acorn.walk.simple(ast, {
ForStatement: loop,
ForInStatement: loop,
WhileStatement: loop,
DoWhileStatement: loop,
CallExpression(node) {
if (node.callee.type == "Identifier" && node.callee.name == "require" &&
node.arguments.length == 1 && node.arguments[0].type == "Literal" &&
typeof node.arguments[0].value == "string" && !dependencies.includes(node.arguments[0].value))
dependencies.push(node.arguments[0].value)
},
ImportDeclaration(node) {
dependencies.push(node.source.value)
let req = "require(" + node.source.raw + ")", text
if (node.specifiers.length == 0) {
text = req
} else if (node.specifiers.length > 1 || node.specifiers[0].type == "ImportDefaultSpecifier") {
let name = "m_" + node.source.value.replace(/\W+/g, "_") + "__"
text = "var " + name + " = " + req
node.specifiers.forEach(spec => {
if (spec.type == "ImportDefaultSpecifier")
text += ", " + spec.local.name + " = " + name + ".default || " + name
else if (name != null)
text += ", " + spec.local.name + " = " + name + "." + spec.imported.name
})
} else {
text = "var "
node.specifiers.forEach(spec => {
if (spec.type == "ImportNamespaceSpecifier")
text += spec.local.name + " = " + req
else
text += spec.local.name + " = " + req + "." + spec.imported.name
})
}
patches.push({from: node.start, to: node.end, text: text + ";"})
},
ExportNamedDeclaration(node) {
if (node.source || !node.declaration)
patches.push({from: node.start, to: node.end, text: ""})
else
patches.push({from: node.start, to: node.declaration.start, text: ""})
},
ExportDefaultDeclaration(node) {
if (/Declaration/.test(node.declaration.type)) {
patches.push({from: node.start, to: node.declaration.start, text: ""})
} else {
patches.push({from: node.start, to: node.declaration.start, text: ";("},
{from: node.declaration.end, text: ")"})
}
},
ExportAllDeclaration: function(node) {
patches.push({from: node.start, to: node.end, text: ""})
}
})
let tryPos = 0, catchPos = ast.end
for (let i = strict ? 1 : 0; i < ast.body.length; i++) {
let stat = ast.body[i]
if (stat.type != "FunctionDeclaration") {
if (tryPos == 0) tryPos = stat.start
catchPos = stat.end
}
if (stat.type == "VariableDeclaration" && stat.kind != "var") {
if (stat.kind == "const") {
let found = findAssignmentsTo(stat.declarations, ast)
if (found) patches.push({from: 0, text: `throw new TypeError("invalid assignment to const '${found}'");`})
}
patches.push({from: stat.start, to: stat.start + stat.kind.length, text: "var"})
}
if (stat.type == "ClassDeclaration")
patches.push({from: stat.start, text: "var " + stat.id.name + " = "})
}
patches.push({from: tryPos, text: "try{", priority: 10})
patches.push({from: catchPos, text: "}catch(e){__sandbox.error(e);}", priority: -10})
patches.sort((a, b) => a.from - b.from || (a.to || a.from) - (b.to || b.from) || (b.priority || 0) - (a.priority || 0))
let out = "", pos = 0
for (let i = 0; i < patches.length; ++i) {
let patch = patches[i]
out += code.slice(pos, patch.from) + patch.text
pos = patch.to || patch.from
}
out += code.slice(pos, code.length)
out += "\n//# sourceURL=code" + randomID()
return {code: (strict ? '"use strict";' : "") + out, dependencies}
}
function findAssignmentsTo(decls, ast) {
let found = null
for (let i = 0; i < decls.length; i++)
acorn.walk.simple(decls[i].id, {
VariablePattern(node) { if (findAssignments(node.name, ast)) found = node.name }
}, null, null, "Pattern")
return found
}
function findAssignments(name, ast) {
let found = false
acorn.walk.ancestor(ast, {
AssignmentExpression(node, ancestors) {
if (node.left.type != "Identifier" || node.left.name != name) return
for (let i = 2; i < ancestors.length; i++) if (/Statement|Declaration/.test(ancestors[i].type)) return
found = true
}
})
return found
}
function detectSourceType(code) {
return /(^|\n)\s*(im|ex)port\b/.test(code) ? "module" : "script"
}
function randomID() {
return Math.floor(Math.random() * 0xffffffff).toString(16)
}
function findDeps(code) {
let deps = [], ast
try { ast = acorn.parse(code) }
catch(e) { return deps }
acorn.walk.simple(ast, {
CallExpression(node) {
if (node.callee.type == "Identifier" && node.callee.name == "require" &&
node.arguments.length == 1 && node.arguments[0].type == "Literal" &&
typeof node.arguments[0].value == "string" && !deps.includes(node.arguments[0].value))
deps.push(node.arguments[0].value)
}
})
return deps
}
function resolveRelative(base, path) {
if (!/\.\.?\//.test(path)) return path
base = base.replace(/[^\/]+$/, "")
let m
while (m = /^\.(\.)?\//.exec(path)) {
if (m[1]) base = base.replace(/\/[^\/]+\/$/, "/")
path = path.slice(m[0].length)
}
return base + path
}
class Cached {
constructor(mapping) {
this.mapping = mapping
this.work = Object.create(null)
this.done = Object.create(null)
}
compute(value) {
return this.work[value] || (this.work[value] = this.mapping(value).then(result => this.done[value] = result))
}
store(value, result) {
this.work[value] = Promise.resolve(result)
return this.done[value] = result
}
get(value) {
return this.done[value]
}
}
// Cache for loaded code and resolved unpkg redirects
const resolved = new Cached(name => fetch("https://unpkg.com/" + name.replace(/\/$/, "")).then(resp => {
if (resp.status >= 400) throw new Error(`Failed to resolve package '${name}'`)
let found = resp.url.replace(/.*unpkg\.com\//, "")
let known = resolved.get(found)
return known || resp.text().then(code => resolved.store(found, {name: found, code}))
}))
SandBox.Output = class {
constructor(div) { this.div = div }
clear() {
let clone = this.div.cloneNode(false)
this.div.parentNode.replaceChild(clone, this.div)
this.div = clone
}
out(type, args) {
let wrap = document.createElement("pre")
wrap.className = "sandbox-output-" + type
for (let i = 0; i < args.length; ++i) {
let arg = args[i]
if (i) wrap.appendChild(document.createTextNode(" "))
if (typeof arg == "string")
wrap.appendChild(document.createTextNode(arg))
else
wrap.appendChild(represent(arg, 58))
}
this.div.appendChild(wrap)
}
}
function span(type, text) {
let sp = document.createElement("span")
sp.className = "sandbox-output-" + type
sp.appendChild(document.createTextNode(text))
return sp
}
function eltSize(elt) {
return elt.textContent.length
}
function represent(val, space) {
if (typeof val == "boolean") return span("bool", String(val))
if (typeof val == "number") return span("number", String(val))
if (typeof val == "string") return span("string", JSON.stringify(val))
if (typeof val == "symbol") return span("symbol", String(val))
if (val == null) return span("null", String(val))
if (Array.isArray(val)) return representArray(val, space)
else return representObj(val, space)
}
function representArray(val, space) {
space -= 2
let wrap = document.createElement("span")
wrap.appendChild(document.createTextNode("["))
for (let i = 0; i < val.length; ++i) {
if (i) {
wrap.appendChild(document.createTextNode(", "))
space -= 2
}
let next = space > 0 && represent(val[i], space)
let nextSize = next ? eltSize(next) : 0
if (space - nextSize <= 0) {
wrap.appendChild(span("etc", "â¦")).addEventListener("click", () => expandObj(wrap, "array", val))
break
}
space -= nextSize
wrap.appendChild(next)
}
wrap.appendChild(document.createTextNode("]"))
return wrap
}
function representObj(val, space) {
let string = typeof val.toString == "function" && val.toString(), m
if (!string || /^\[object .*\]$/.test(string))
return representSimpleObj(val, space)
if (val.call && (m = string.match(/^\s*(function[^(]*\([^)]*\))/)))
return span("fun", m[1] + "{â¦}")
let elt = span("etc", string)
elt.addEventListener("click", () => expandObj(elt, "obj", val))
return elt
}
function constructorName(obj) {
if (!obj.constructor) return null
let m = String(obj.constructor).match(/^function\s*([^\s(]+)/)
if (m && m[1] != "Object") return m[1]
}
function hop(obj, prop) {
return Object.prototype.hasOwnProperty.call(obj, prop)
}
function representSimpleObj(val, space) {
space -= 2
let wrap = document.createElement("span")
let name = constructorName(val)
if (name) {
space -= name.length
wrap.appendChild(document.createTextNode(name))
}
wrap.appendChild(document.createTextNode("{"))
try {
let first = true
for (let prop in val) if (hop(val, prop)) {
if (first) {
first = false
} else {
space -= 2
wrap.appendChild(document.createTextNode(", "))
}
let next = space > 0 && represent(val[prop], space)
let nextSize = next ? prop.length + 2 + eltSize(next) : 0
if (space - nextSize <= 0) {
wrap.appendChild(span("etc", "â¦")).addEventListener("click", () => expandObj(wrap, "obj", val))
break
}
space -= nextSize
wrap.appendChild(span("prop", prop + ": "))
wrap.appendChild(next)
}
} catch (e) {
wrap.appendChild(document.createTextNode("â¦"))
}
wrap.appendChild(document.createTextNode("}"))
return wrap
}
function expandObj(node, type, val) {
let wrap = document.createElement("span")
let opening = type == "array" ? "[" : "{", cname
if (opening == "{" && (cname = constructorName(val))) opening = cname + " {"
wrap.appendChild(document.createTextNode(opening))
let block = wrap.appendChild(document.createElement("div"))
block.className = "sandbox-output-etc-block"
let table = block.appendChild(document.createElement("table"))
function addProp(name) {
let row = table.appendChild(document.createElement("tr"))
row.appendChild(document.createElement("td")).appendChild(span("prop", name + ":"))
row.appendChild(document.createElement("td")).appendChild(represent(val[name], 40))
}
if (type == "array") {
for (let i = 0; i < val.length; ++i) addProp(i)
} else {
for (let prop in val) if (hop(val, prop)) addProp(prop)
}
wrap.appendChild(document.createTextNode(type == "array" ? "]" : "}"))
node.parentNode.replaceChild(wrap, node)
}
function promiseChain(thunks) {
function proceed(i, value) {
if (i == thunks.length) return Promise.resolve(value)
else return thunks[i](value).then(value => proceed(i + 1, value))
}
return proceed(0, null)
}
})()