Comparing version 0.0.12 to 0.0.13
1669
index.js
@@ -20,3 +20,5 @@ const events = require('events'); | ||
const windowSymbol = Symbol(); | ||
const setWindowSymbol = Symbol(); | ||
const htmlTagsSymbol = Symbol(); | ||
const htmlElementsSymbol = Symbol(); | ||
const optionsSymbol = Symbol(); | ||
@@ -366,754 +368,876 @@ let id = 0; | ||
const exokit = (s = '', options = {}) => { | ||
options.url = options.url || 'http://127.0.0.1'; | ||
options.dataPath = options.dataPath || __dirname; | ||
class Node extends EventEmitter { | ||
constructor(nodeName = null) { | ||
super(); | ||
const baseUrl = options.url; | ||
const _normalizeUrl = src => new URL(src, baseUrl).href; | ||
const _parseAttributes = attrs => { | ||
const result = {}; | ||
for (let i = 0; i < attrs.length; i++) { | ||
const attr = attrs[i]; | ||
result[attr.name] = attr.value; | ||
} | ||
return result; | ||
}; | ||
const _formatAttributes = attributes => { | ||
const result = []; | ||
for (const name in attributes) { | ||
const value = attributes[name]; | ||
result.push({ | ||
name, | ||
value, | ||
}); | ||
} | ||
return result; | ||
}; | ||
const _parseStyle = styleString => { | ||
const style = {}; | ||
const split = styleString.split(/;\s*/); | ||
for (let i = 0; i < split.length; i++) { | ||
const split2 = split[i].split(/:\s*/); | ||
if (split2.length === 2) { | ||
style[split2[0]] = split2[1]; | ||
} | ||
} | ||
return style; | ||
}; | ||
const _formatStyle = style => { | ||
let styleString = ''; | ||
for (const k in style) { | ||
styleString += (styleString.length > 0 ? ' ' : '') + k + ': ' + style[k] + ';'; | ||
} | ||
return styleString; | ||
}; | ||
const _hash = s => { | ||
let result = 0; | ||
for (let i = 0; i < s.length; i++) { | ||
result += s.codePointAt(i); | ||
} | ||
return result; | ||
}; | ||
this.nodeName = nodeName; | ||
this.parentNode = null; | ||
} | ||
} | ||
class HTMLElement extends Node { | ||
constructor(tagName = 'div', attributes = {}, value = '') { | ||
super(null); | ||
class Node extends EventEmitter { | ||
constructor(nodeName = null) { | ||
super(); | ||
this.tagName = tagName; | ||
this.attributes = attributes; | ||
this.value = value; | ||
this.childNodes = []; | ||
this.nodeName = nodeName; | ||
this.parentNode = null; | ||
this._innerHTML = ''; | ||
} | ||
this[windowSymbol] = null; | ||
} | ||
get attrs() { | ||
return _formatAttributes(this.attributes); | ||
} | ||
set attrs(attrs) { | ||
this.attributes = _parseAttributes(attrs); | ||
} | ||
[setWindowSymbol](window) { | ||
this[windowSymbol] = window; | ||
get children() { | ||
return this.childNodes; | ||
} | ||
set children(children) { | ||
this.childNodes = children; | ||
} | ||
this.emit('window', window); | ||
} | ||
getAttribute(name) { | ||
return this.attributes[name]; | ||
} | ||
Node.fromAST = (node, window, parentNode = null) => { | ||
if (node.nodeName === '#text') { | ||
const textNode = new TextNode(node.value); | ||
textNode.parentNode = parentNode; | ||
textNode[setWindowSymbol](window); | ||
return textNode; | ||
} else if (node.nodeName === '#comment') { | ||
const commentNode = new CommentNode(node.value); | ||
commentNode.parentNode = parentNode; | ||
commentNode[setWindowSymbol](window); | ||
return commentNode; | ||
} else { | ||
const {tagName, value} = node; | ||
const attributes = node.attrs && _parseAttributes(node.attrs); | ||
const HTMLElementTemplate = HTML_ELEMENTS[tagName]; | ||
const element = HTMLElementTemplate ? | ||
new HTMLElementTemplate( | ||
attributes, | ||
value | ||
) | ||
: | ||
new HTMLElement( | ||
tagName, | ||
attributes, | ||
value | ||
); | ||
element.parentNode = parentNode; | ||
element[setWindowSymbol](window); | ||
if (node.childNodes) { | ||
element.childNodes = node.childNodes.map(childNode => Node.fromAST(childNode, window, element)); | ||
} | ||
return element; | ||
} | ||
}; | ||
class HTMLElement extends Node { | ||
constructor(tagName = 'div', attributes = {}, value = '') { | ||
super(null); | ||
setAttribute(name, value) { | ||
this.attributes[name] = value; | ||
this.tagName = tagName; | ||
this.attributes = attributes; | ||
this.value = value; | ||
this.childNodes = []; | ||
this.emit('attribute', name, value); | ||
} | ||
this._innerHTML = ''; | ||
appendChild(childNode) { | ||
this.childNodes.push(childNode); | ||
childNode.parentNode = this; | ||
} | ||
removeChild(childNode) { | ||
const index = this.childNodes.indexOf(childNode); | ||
if (index !== -1) { | ||
this.childNodes.splice(index, 1); | ||
childNode.parentNode = null; | ||
} | ||
get attrs() { | ||
return _formatAttributes(this.attributes); | ||
} | ||
insertBefore(childNode, nextSibling) { | ||
const index = this.childNodes.indexOf(nextSibling); | ||
if (index !== -1) { | ||
this.childNodes.splice(index, 0, childNode); | ||
childNode.parentNode = this; | ||
} | ||
set attrs(attrs) { | ||
this.attributes = _parseAttributes(attrs); | ||
} | ||
insertAfter(childNode, nextSibling) { | ||
const index = this.childNodes.indexOf(nextSibling); | ||
if (index !== -1) { | ||
this.childNodes.splice(index + 1, 0, childNode); | ||
childNode.parentNode = this; | ||
} | ||
} | ||
get children() { | ||
return this.childNodes; | ||
} | ||
set children(children) { | ||
this.childNodes = children; | ||
} | ||
getAttribute(name) { | ||
return this.attributes[name]; | ||
} | ||
setAttribute(name, value) { | ||
this.attributes[name] = value; | ||
this.emit('attribute', name, value); | ||
} | ||
appendChild(childNode) { | ||
this.childNodes.push(childNode); | ||
childNode.parentNode = this; | ||
} | ||
removeChild(childNode) { | ||
const index = this.childNodes.indexOf(childNode); | ||
if (index !== -1) { | ||
this.childNodes.splice(index, 1); | ||
childNode.parentNode = null; | ||
getElementById(id) { | ||
return this.traverse(node => { | ||
if ( | ||
(node.getAttribute && node.getAttribute('id') === id) || | ||
(node.attrs && node.attrs.some(attr => attr.name === 'id' && attr.value === id)) | ||
) { | ||
return node; | ||
} | ||
} | ||
insertBefore(childNode, nextSibling) { | ||
const index = this.childNodes.indexOf(nextSibling); | ||
if (index !== -1) { | ||
this.childNodes.splice(index, 0, childNode); | ||
childNode.parentNode = this; | ||
}); | ||
} | ||
getElementByClassName(className) { | ||
return this.traverse(node => { | ||
if ( | ||
(node.getAttribute && node.getAttribute('class') === className) || | ||
(node.attrs && node.attrs.some(attr => attr.name === 'class' && attr.value === className)) | ||
) { | ||
return node; | ||
} | ||
} | ||
insertAfter(childNode, nextSibling) { | ||
const index = this.childNodes.indexOf(nextSibling); | ||
if (index !== -1) { | ||
this.childNodes.splice(index + 1, 0, childNode); | ||
childNode.parentNode = this; | ||
}); | ||
} | ||
getElementByTagName(tagName) { | ||
return this.traverse(node => { | ||
if (node.tagName === tagName) { | ||
return node; | ||
} | ||
}); | ||
} | ||
querySelector(selector) { | ||
let match; | ||
if (match = selector.match(/^#(.+)$/)) { | ||
return this.getElementById(match[1]); | ||
} else if (match = selector.match(/^\.(.+)$/)) { | ||
return this.getElementByClassName(match[1]); | ||
} else { | ||
return this.getElementByTagName(selector); | ||
} | ||
getElementById(id) { | ||
return this.traverse(node => { | ||
if ( | ||
(node.getAttribute && node.getAttribute('id') === id) || | ||
(node.attrs && node.attrs.some(attr => attr.name === 'id' && attr.value === id)) | ||
) { | ||
return node; | ||
} | ||
}); | ||
} | ||
getElementByClassName(className) { | ||
return this.traverse(node => { | ||
if ( | ||
(node.getAttribute && node.getAttribute('class') === className) || | ||
(node.attrs && node.attrs.some(attr => attr.name === 'class' && attr.value === className)) | ||
) { | ||
return node; | ||
} | ||
}); | ||
} | ||
getElementByTagName(tagName) { | ||
return this.traverse(node => { | ||
if (node.tagName === tagName) { | ||
return node; | ||
} | ||
}); | ||
} | ||
querySelector(selector) { | ||
let match; | ||
if (match = selector.match(/^#(.+)$/)) { | ||
return this.getElementById(match[1]); | ||
} else if (match = selector.match(/^\.(.+)$/)) { | ||
return this.getElementByClassName(match[1]); | ||
} else { | ||
return this.getElementByTagName(selector); | ||
} | ||
getElementsById(id) { | ||
const result = []; | ||
this.traverse(node => { | ||
if ( | ||
(node.getAttribute && node.getAttribute('id') === id) || | ||
(node.attrs && node.attrs.some(attr => attr.name === 'id' && attr.value === id)) | ||
) { | ||
result.push(node); | ||
} | ||
}); | ||
return result; | ||
} | ||
getElementsByClassName(className) { | ||
const result = []; | ||
this.traverse(node => { | ||
if ( | ||
(node.getAttribute && node.getAttribute('class') === className) || | ||
(node.attrs && node.attrs.some(attr => attr.name === 'class' && attr.value === className)) | ||
) { | ||
result.push(node); | ||
} | ||
}); | ||
return result; | ||
} | ||
getElementsByTagName(tagName) { | ||
const result = []; | ||
this.traverse(node => { | ||
if (node.tagName === tagName) { | ||
result.push(node); | ||
} | ||
}); | ||
return result; | ||
} | ||
querySelectorAll(selector) { | ||
let match; | ||
if (match = selector.match(/^#(.+)$/)) { | ||
return this.getElementsById(match[1]); | ||
} else if (match = selector.match(/^\.(.+)$/)) { | ||
return this.getElementsByClassName(match[1]); | ||
} else { | ||
return this.getElementsByTagName(selector); | ||
} | ||
getElementsById(id) { | ||
const result = []; | ||
this.traverse(node => { | ||
if ( | ||
(node.getAttribute && node.getAttribute('id') === id) || | ||
(node.attrs && node.attrs.some(attr => attr.name === 'id' && attr.value === id)) | ||
) { | ||
result.push(node); | ||
} | ||
}); | ||
return result; | ||
} | ||
getElementsByClassName(className) { | ||
const result = []; | ||
this.traverse(node => { | ||
if ( | ||
(node.getAttribute && node.getAttribute('class') === className) || | ||
(node.attrs && node.attrs.some(attr => attr.name === 'class' && attr.value === className)) | ||
) { | ||
result.push(node); | ||
} | ||
}); | ||
return result; | ||
} | ||
getElementsByTagName(tagName) { | ||
const result = []; | ||
this.traverse(node => { | ||
if (node.tagName === tagName) { | ||
result.push(node); | ||
} | ||
}); | ||
return result; | ||
} | ||
querySelectorAll(selector) { | ||
let match; | ||
if (match = selector.match(/^#(.+)$/)) { | ||
return this.getElementsById(match[1]); | ||
} else if (match = selector.match(/^\.(.+)$/)) { | ||
return this.getElementsByClassName(match[1]); | ||
} | ||
traverse(fn) { | ||
const _recurse = node => { | ||
const result = fn(node); | ||
if (result !== undefined) { | ||
return result; | ||
} else { | ||
return this.getElementsByTagName(selector); | ||
} | ||
} | ||
traverse(fn) { | ||
const _recurse = node => { | ||
const result = fn(node); | ||
if (result !== undefined) { | ||
return result; | ||
} else { | ||
if (node.childNodes) { | ||
for (let i = 0; i < node.childNodes.length; i++) { | ||
const result = _recurse(node.childNodes[i]); | ||
if (result !== undefined) { | ||
return result; | ||
} | ||
if (node.childNodes) { | ||
for (let i = 0; i < node.childNodes.length; i++) { | ||
const result = _recurse(node.childNodes[i]); | ||
if (result !== undefined) { | ||
return result; | ||
} | ||
} | ||
} | ||
}; | ||
return _recurse(this); | ||
} | ||
} | ||
}; | ||
return _recurse(this); | ||
} | ||
addEventListener() { | ||
this.on.apply(this, arguments); | ||
} | ||
removeEventListener() { | ||
this.removeListener.apply(this, arguments); | ||
} | ||
addEventListener() { | ||
this.on.apply(this, arguments); | ||
} | ||
removeEventListener() { | ||
this.removeListener.apply(this, arguments); | ||
} | ||
get offsetWidth() { | ||
const style = _parseStyle(this.attributes['style'] || ''); | ||
const fontFamily = style['font-family']; | ||
if (fontFamily) { | ||
return _hash(fontFamily) * _hash(this.innerHTML); | ||
} else { | ||
return 0; | ||
} | ||
} | ||
set offsetWidth(offsetWidth) {} | ||
get offsetHeight() { | ||
get offsetWidth() { | ||
const style = _parseStyle(this.attributes['style'] || ''); | ||
const fontFamily = style['font-family']; | ||
if (fontFamily) { | ||
return _hash(fontFamily) * _hash(this.innerHTML); | ||
} else { | ||
return 0; | ||
} | ||
set offsetHeight(offsetHeight) {} | ||
} | ||
set offsetWidth(offsetWidth) {} | ||
get offsetHeight() { | ||
return 0; | ||
} | ||
set offsetHeight(offsetHeight) {} | ||
get className() { | ||
return this.attributes['class'] || ''; | ||
} | ||
set className(className) { | ||
this.attributes['class'] = className; | ||
} | ||
get className() { | ||
return this.attributes['class'] || ''; | ||
} | ||
set className(className) { | ||
this.attributes['class'] = className; | ||
} | ||
get style() { | ||
const style = _parseStyle(this.attributes['style'] || ''); | ||
Object.defineProperty(style, 'cssText', { | ||
get: () => this.attributes['style'], | ||
set: cssText => { | ||
this.attributes['style'] = cssText; | ||
}, | ||
}); | ||
return style; | ||
} | ||
set style(style) { | ||
this.attributes['style'] = _formatStyle(style); | ||
} | ||
get style() { | ||
const style = _parseStyle(this.attributes['style'] || ''); | ||
Object.defineProperty(style, 'cssText', { | ||
get: () => this.attributes['style'], | ||
set: cssText => { | ||
this.attributes['style'] = cssText; | ||
}, | ||
}); | ||
return style; | ||
} | ||
set style(style) { | ||
this.attributes['style'] = _formatStyle(style); | ||
} | ||
get innerHTML() { | ||
return parse5.serialize(this); | ||
} | ||
set innerHTML(innerHTML) { | ||
const childNodes = parse5.parseFragment(innerHTML).childNodes.map(childNode => Node.fromAST(childNode, this[windowSymbol], this)); | ||
this.childNodes = childNodes; | ||
get innerHTML() { | ||
return parse5.serialize(this); | ||
} | ||
set innerHTML(innerHTML) { | ||
const childNodes = parse5.parseFragment(innerHTML).childNodes.map(childNode => _fromAST(childNode, this[windowSymbol], this)); | ||
this.childNodes = childNodes; | ||
_promiseSerial(childNodes.map(childNode => () => _runHtml(childNode, this[windowSymbol]))) | ||
.catch(err => { | ||
console.warn(err); | ||
}); | ||
_promiseSerial(childNodes.map(childNode => () => _runHtml(childNode, this[windowSymbol]))) | ||
.catch(err => { | ||
console.warn(err); | ||
}); | ||
this.emit('innerHTML', innerHTML); | ||
} | ||
this.emit('innerHTML', innerHTML); | ||
} | ||
class HTMLAnchorElement extends HTMLElement { | ||
constructor(attributes = {}, value = '') { | ||
super('a', attributes, value); | ||
} | ||
} | ||
class HTMLAnchorElement extends HTMLElement { | ||
constructor(attributes = {}, value = '') { | ||
super('a', attributes, value); | ||
} | ||
get href() { | ||
return this.getAttribute('href') || ''; | ||
} | ||
set href(value) { | ||
this.setAttribute('href', value); | ||
} | ||
get href() { | ||
return this.getAttribute('href') || ''; | ||
} | ||
class HTMLLoadableElement extends HTMLElement { | ||
constructor(tagName, attributes = {}, value = '') { | ||
super(tagName, attributes, value); | ||
} | ||
set href(value) { | ||
this.setAttribute('href', value); | ||
} | ||
} | ||
class HTMLLoadableElement extends HTMLElement { | ||
constructor(tagName, attributes = {}, value = '') { | ||
super(tagName, attributes, value); | ||
} | ||
get onload() { | ||
return this.listeners('load')[0]; | ||
} | ||
set onload(onload) { | ||
if (typeof onload === 'function') { | ||
this.addEventListener('load', onload); | ||
} else { | ||
const listeners = this.listeners('load'); | ||
for (let i = 0; i < listeners.length; i++) { | ||
this.removeEventListener('load', listeners[i]); | ||
} | ||
get onload() { | ||
return this.listeners('load')[0]; | ||
} | ||
set onload(onload) { | ||
if (typeof onload === 'function') { | ||
this.addEventListener('load', onload); | ||
} else { | ||
const listeners = this.listeners('load'); | ||
for (let i = 0; i < listeners.length; i++) { | ||
this.removeEventListener('load', listeners[i]); | ||
} | ||
} | ||
} | ||
get onerror() { | ||
return this.listeners('error')[0]; | ||
} | ||
set onerror(onerror) { | ||
if (typeof onerror === 'function') { | ||
this.addEventListener('error', onerror); | ||
} else { | ||
const listeners = this.listeners('error'); | ||
for (let i = 0; i < listeners.length; i++) { | ||
this.removeEventListener('error', listeners[i]); | ||
} | ||
get onerror() { | ||
return this.listeners('error')[0]; | ||
} | ||
set onerror(onerror) { | ||
if (typeof onerror === 'function') { | ||
this.addEventListener('error', onerror); | ||
} else { | ||
const listeners = this.listeners('error'); | ||
for (let i = 0; i < listeners.length; i++) { | ||
this.removeEventListener('error', listeners[i]); | ||
} | ||
} | ||
} | ||
class HTMLWindowElement extends HTMLLoadableElement { | ||
constructor() { | ||
super('window'); | ||
} | ||
} | ||
class HTMLWindowElement extends HTMLLoadableElement { | ||
constructor() { | ||
super('window'); | ||
} | ||
postMessage(data) { | ||
this.emit('message', new MessageEvent(data)); | ||
} | ||
postMessage(data) { | ||
this.emit('message', new MessageEvent(data)); | ||
} | ||
get onmessage() { | ||
return this.listeners('load')[0]; | ||
} | ||
set onmessage(onmessage) { | ||
if (typeof onmessage === 'function') { | ||
this.addEventListener('message', onmessage); | ||
} else { | ||
const listeners = this.listeners('message'); | ||
for (let i = 0; i < listeners.length; i++) { | ||
this.removeEventListener('message', listeners[i]); | ||
} | ||
get onmessage() { | ||
return this.listeners('load')[0]; | ||
} | ||
set onmessage(onmessage) { | ||
if (typeof onmessage === 'function') { | ||
this.addEventListener('message', onmessage); | ||
} else { | ||
const listeners = this.listeners('message'); | ||
for (let i = 0; i < listeners.length; i++) { | ||
this.removeEventListener('message', listeners[i]); | ||
} | ||
} | ||
} | ||
class HTMLScriptElement extends HTMLLoadableElement { | ||
constructor(attributes = {}, value = '') { | ||
super('script', attributes, value); | ||
} | ||
class HTMLScriptElement extends HTMLLoadableElement { | ||
constructor(attributes = {}, value = '') { | ||
super('script', attributes, value); | ||
this.readyState = null; | ||
this.readyState = null; | ||
this.on('attribute', (name, value) => { | ||
if (name === 'src') { | ||
this.readyState = null; | ||
this.on('attribute', (name, value) => { | ||
if (name === 'src') { | ||
this.readyState = null; | ||
const url = _normalizeUrl(value); | ||
const url = value; | ||
this[windowSymbol].fetch(url) | ||
.then(res => { | ||
if (res.status >= 200 && res.status < 300) { | ||
return res.text(); | ||
} else { | ||
return Promise.reject(new Error('script src got invalid status code: ' + res.status + ' : ' + url)); | ||
} | ||
}) | ||
.then(jsString => { | ||
_runJavascript(jsString, this[windowSymbol], url); | ||
fetch(url) | ||
.then(res => { | ||
if (res.status >= 200 && res.status < 300) { | ||
return res.text(); | ||
} else { | ||
return Promise.reject(new Error('script src got invalid status code: ' + res.status + ' : ' + url)); | ||
} | ||
}) | ||
.then(jsString => { | ||
_runJavascript(jsString, this[windowSymbol], url); | ||
this.readyState = 'complete'; | ||
this.readyState = 'complete'; | ||
this.emit('load'); | ||
}) | ||
.catch(err => { | ||
this.readyState = 'complete'; | ||
this.emit('load'); | ||
}) | ||
.catch(err => { | ||
this.readyState = 'complete'; | ||
this.emit('error', err); | ||
}); | ||
} | ||
}); | ||
this.on('innerHTML', innerHTML => { | ||
_runJavascript(innerHTML, this[windowSymbol]); | ||
this.emit('error', err); | ||
}); | ||
} | ||
this.readyState = 'complete'; | ||
process.nextTick(() => { | ||
this.emit('load'); | ||
}); | ||
this.on('innerHTML', innerHTML => { | ||
_runJavascript(innerHTML, this[windowSymbol]); | ||
}); | ||
} | ||
this.readyState = 'complete'; | ||
get src() { | ||
return this.getAttribute('src') || ''; | ||
} | ||
set src(value) { | ||
this.setAttribute('src', value); | ||
} | ||
process.nextTick(() => { | ||
this.emit('load'); | ||
}); | ||
}); | ||
} | ||
set innerHTML(innerHTML) { | ||
this.emit('innerHTML', innerHTML); | ||
} | ||
get src() { | ||
return this.getAttribute('src') || ''; | ||
run() { | ||
let running = false; | ||
if (this.attributes.src) { | ||
this.src = this.attributes.src; | ||
running = true; | ||
} | ||
set src(value) { | ||
this.setAttribute('src', value); | ||
if (this.childNodes.length > 0) { | ||
this.innerHTML = this.childNodes[0].value; | ||
running = true; | ||
} | ||
return running; | ||
} | ||
} | ||
class HTMLMediaElement extends HTMLLoadableElement { | ||
constructor(tagName = null, attributes = {}, value = '') { | ||
super(tagName, attributes, value); | ||
} | ||
set innerHTML(innerHTML) { | ||
this.emit('innerHTML', innerHTML); | ||
get src() { | ||
this.getAttribute('src'); | ||
} | ||
set src(value) { | ||
this.setAttribute('src', value); | ||
} | ||
run() { | ||
if (this.attributes.src) { | ||
this.src = this.attributes.src; | ||
return true; | ||
} else { | ||
return false; | ||
} | ||
} | ||
} | ||
const HTMLImageElement = (() => { | ||
if (typeof nativeImage !== 'undefined') { | ||
return class HTMLImageElement extends nativeImage { | ||
constructor(attributes = {}, value = '') { | ||
super(); | ||
EventEmitter.call(this); | ||
this.tagName = 'image' | ||
this.attributes = attributes; | ||
this.value = value; | ||
run() { | ||
let running = false; | ||
if (this.attributes.src) { | ||
this.src = this.attributes.src; | ||
running = true; | ||
this._src = ''; | ||
} | ||
if (this.childNodes.length > 0) { | ||
this.innerHTML = this.childNodes[0].value; | ||
running = true; | ||
emit(event, data) { | ||
return EventEmitter.prototype.emit.call(this, event, data); | ||
} | ||
return running; | ||
} | ||
} | ||
class HTMLMediaElement extends HTMLLoadableElement { | ||
constructor(tagName = null, attributes = {}, value = '') { | ||
super(tagName, attributes, value); | ||
} | ||
on(event, cb) { | ||
return EventEmitter.prototype.on.call(this, event, cb); | ||
} | ||
removeListener(event, cb) { | ||
return EventEmitter.prototype.removeListener.call(this, event, cb); | ||
} | ||
get src() { | ||
this.getAttribute('src'); | ||
} | ||
set src(value) { | ||
this.setAttribute('src', value); | ||
} | ||
addEventListener(event, cb) { | ||
return HTMLElement.prototype.addEventListener.call(this, event, cb); | ||
} | ||
removeEventListener(event, cb) { | ||
return HTMLElement.prototype.removeEventListener.call(this, event, cb); | ||
} | ||
run() { | ||
if (this.attributes.src) { | ||
this.src = this.attributes.src; | ||
return true; | ||
} else { | ||
return false; | ||
get src() { | ||
return this._src; | ||
} | ||
} | ||
} | ||
const HTMLImageElement = (() => { | ||
if (typeof nativeImage !== 'undefined') { | ||
return class HTMLImageElement extends nativeImage { | ||
constructor(attributes = {}, value = '') { | ||
super(); | ||
EventEmitter.call(this); | ||
this.tagName = 'image' | ||
this.attributes = attributes; | ||
this.value = value; | ||
set src(src) { | ||
this._src = src; | ||
this._src = ''; | ||
} | ||
const srcError = new Error(); | ||
emit(event, data) { | ||
return EventEmitter.prototype.emit.call(this, event, data); | ||
} | ||
on(event, cb) { | ||
return EventEmitter.prototype.on.call(this, event, cb); | ||
} | ||
removeListener(event, cb) { | ||
return EventEmitter.prototype.removeListener.call(this, event, cb); | ||
} | ||
this[windowSymbol].fetch(src) | ||
.then(res => { | ||
if (res.status >= 200 && res.status < 300) { | ||
return res.arrayBuffer(); | ||
} else { | ||
return Promise.reject(new Error('img src got invalid status code: ' + res.status + ' : ' + url)); | ||
} | ||
}) | ||
.then(arrayBuffer => { | ||
if (this.load(arrayBuffer)) { | ||
return Promise.resolve(); | ||
} else { | ||
console.warn('failed to decode image src', srcError.stack); | ||
return Promise.reject(new Error('failed to decode image')); | ||
} | ||
}) | ||
.then(() => { | ||
this.emit('load'); | ||
}) | ||
.catch(err => { | ||
this.emit('error', err); | ||
}); | ||
} | ||
addEventListener(event, cb) { | ||
return HTMLElement.prototype.addEventListener.call(this, event, cb); | ||
get onload() { | ||
return this.listeners('load')[0]; | ||
} | ||
set onload(onload) { | ||
if (typeof onload === 'function') { | ||
this.addEventListener('load', onload); | ||
} else { | ||
const listeners = this.listeners('load'); | ||
for (let i = 0; i < listeners.length; i++) { | ||
this.removeEventListener('load', listeners[i]); | ||
} | ||
} | ||
removeEventListener(event, cb) { | ||
return HTMLElement.prototype.removeEventListener.call(this, event, cb); | ||
} | ||
} | ||
get src() { | ||
return this._src; | ||
get onerror() { | ||
return this.listeners('error')[0]; | ||
} | ||
set onerror(onerror) { | ||
if (typeof onerror === 'function') { | ||
this.addEventListener('error', onerror); | ||
} else { | ||
const listeners = this.listeners('error'); | ||
for (let i = 0; i < listeners.length; i++) { | ||
this.removeEventListener('error', listeners[i]); | ||
} | ||
} | ||
set src(src) { | ||
this._src = src; | ||
} | ||
}; | ||
} else { | ||
return class HTMLImageElement extends HTMLMediaElement { | ||
constructor() { | ||
super('image'); | ||
fetch(src) | ||
.then(res => { | ||
if (res.status >= 200 && res.status < 300) { | ||
return res.arrayBuffer(); | ||
} else { | ||
return Promise.reject(new Error('img src got invalid status code: ' + res.status + ' : ' + url)); | ||
} | ||
}) | ||
.then(arrayBuffer => { | ||
if (this.load(arrayBuffer)) { | ||
return Promise.resolve(); | ||
} else { | ||
return Promise.reject(new Error('failed to decode image')); | ||
} | ||
}) | ||
.then(() => { | ||
this.on('attribute', (name, value) => { | ||
if (name === 'src') { | ||
process.nextTick(() => { // XXX | ||
this.emit('load'); | ||
}) | ||
.catch(err => { | ||
this.emit('error', err); | ||
}); | ||
} | ||
get onload() { | ||
return this.listeners('load')[0]; | ||
} | ||
set onload(onload) { | ||
if (typeof onload === 'function') { | ||
this.addEventListener('load', onload); | ||
} else { | ||
const listeners = this.listeners('load'); | ||
for (let i = 0; i < listeners.length; i++) { | ||
this.removeEventListener('load', listeners[i]); | ||
} | ||
} | ||
} | ||
}); | ||
} | ||
get onerror() { | ||
return this.listeners('error')[0]; | ||
} | ||
set onerror(onerror) { | ||
if (typeof onerror === 'function') { | ||
this.addEventListener('error', onerror); | ||
} else { | ||
const listeners = this.listeners('error'); | ||
for (let i = 0; i < listeners.length; i++) { | ||
this.removeEventListener('error', listeners[i]); | ||
} | ||
} | ||
} | ||
}; | ||
} else { | ||
return class HTMLImageElement extends HTMLMediaElement { | ||
constructor() { | ||
super('image'); | ||
get width() { | ||
return 0; // XXX | ||
} | ||
set width(width) {} | ||
get height() { | ||
return 0; // XXX | ||
} | ||
set height(height) {} | ||
}; | ||
} | ||
})(); | ||
class HTMLAudioElement extends HTMLMediaElement { | ||
constructor(attributes = {}, value = '') { | ||
super('audio', attributes, value); | ||
this.on('attribute', (name, value) => { | ||
if (name === 'src') { | ||
process.nextTick(() => { // XXX | ||
this.emit('load'); | ||
}); | ||
} | ||
}); | ||
} | ||
this.on('attribute', (name, value) => { | ||
if (name === 'src') { | ||
process.nextTick(() => { // XXX | ||
this.emit('load'); | ||
this.emit('canplay'); | ||
}); | ||
} | ||
}); | ||
} | ||
get width() { | ||
return 0; // XXX | ||
} | ||
set width(width) {} | ||
get height() { | ||
return 0; // XXX | ||
} | ||
set height(height) {} | ||
}; | ||
get oncanplay() { | ||
return this.listeners('canplay')[0]; | ||
} | ||
set oncanplay(oncanplay) { | ||
if (typeof oncanplay === 'function') { | ||
this.addEventListener('canplay', oncanplay); | ||
} else { | ||
const listeners = this.listeners('canplay'); | ||
for (let i = 0; i < listeners.length; i++) { | ||
this.removeEventListener('canplay', listeners[i]); | ||
} | ||
} | ||
})(); | ||
class HTMLAudioElement extends HTMLMediaElement { | ||
constructor(attributes = {}, value = '') { | ||
super('audio', attributes, value); | ||
} | ||
this.on('attribute', (name, value) => { | ||
if (name === 'src') { | ||
process.nextTick(() => { // XXX | ||
this.emit('load'); | ||
this.emit('canplay'); | ||
}); | ||
} | ||
}); | ||
} | ||
get oncanplay() { | ||
return this.listeners('canplay')[0]; | ||
} | ||
set oncanplay(oncanplay) { | ||
if (typeof oncanplay === 'function') { | ||
this.addEventListener('canplay', oncanplay); | ||
} else { | ||
const listeners = this.listeners('canplay'); | ||
for (let i = 0; i < listeners.length; i++) { | ||
this.removeEventListener('canplay', listeners[i]); | ||
} | ||
get oncanplaythrough() { | ||
return this.listeners('canplaythrough')[0]; | ||
} | ||
set oncanplaythrough(oncanplaythrough) { | ||
if (typeof oncanplaythrough === 'function') { | ||
this.addEventListener('canplaythrough', oncanplaythrough); | ||
} else { | ||
const listeners = this.listeners('canplaythrough'); | ||
for (let i = 0; i < listeners.length; i++) { | ||
this.removeEventListener('canplaythrough', listeners[i]); | ||
} | ||
} | ||
} | ||
} | ||
class HTMLVideoElement extends HTMLMediaElement { | ||
constructor(attributes = {}, value = '') { | ||
super('video', attributes, value); | ||
get oncanplaythrough() { | ||
return this.listeners('canplaythrough')[0]; | ||
} | ||
set oncanplaythrough(oncanplaythrough) { | ||
if (typeof oncanplaythrough === 'function') { | ||
this.addEventListener('canplaythrough', oncanplaythrough); | ||
} else { | ||
const listeners = this.listeners('canplaythrough'); | ||
for (let i = 0; i < listeners.length; i++) { | ||
this.removeEventListener('canplaythrough', listeners[i]); | ||
} | ||
this.on('attribute', (name, value) => { | ||
if (name === 'src') { | ||
process.nextTick(() => { // XXX | ||
this.emit('load'); | ||
}); | ||
} | ||
} | ||
}); | ||
} | ||
class HTMLVideoElement extends HTMLMediaElement { | ||
constructor(attributes = {}, value = '') { | ||
super('video', attributes, value); | ||
} | ||
class HTMLIframeElement extends HTMLMediaElement { | ||
constructor(attributes = {}, value = '') { | ||
super('iframe', attributes, value); | ||
this.on('attribute', (name, value) => { | ||
if (name === 'src') { | ||
process.nextTick(() => { // XXX | ||
this.emit('load'); | ||
const parentWindow = this[windowSymbol]; | ||
this.contentWindow = _parseWindow('', parentWindow[optionsSymbol], parentWindow, parentWindow.top); | ||
this.contentDocument = this.contentWindow.document; | ||
this.on('attribute', (name, value) => { | ||
if (name === 'src') { | ||
const url = value; | ||
this[windowSymbol].fetch(url) | ||
.then(res => { | ||
if (res.status >= 200 && res.status < 300) { | ||
return res.text(); | ||
} else { | ||
return Promise.reject(new Error('iframe src got invalid status code: ' + res.status + ' : ' + url)); | ||
} | ||
}) | ||
.then(htmlString => { | ||
const contentDocument = _parseDocument(htmlString, this.contentWindow[optionsSymbol], this.contentWindow); | ||
this.contentDocument = contentDocument; | ||
contentDocument.once('readystatechange', () => { | ||
this.emit('load'); | ||
}); | ||
}) | ||
.catch(err => { | ||
this.emit('error', err); | ||
}); | ||
} | ||
}); | ||
} | ||
} | ||
}); | ||
} | ||
class HTMLIframeElement extends HTMLMediaElement { | ||
constructor(attributes = {}, value = '') { | ||
super('iframe', attributes, value); | ||
} | ||
class HTMLCanvasElement extends HTMLElement { | ||
constructor(attributes = {}, value = '') { | ||
super('canvas', attributes, value); | ||
this.contentWindow = null; | ||
this.contentDocument = null; | ||
this._context = null; | ||
this.on('window', window => { | ||
const contentWindow = _parseWindow('', this[windowSymbol], this[windowSymbol].top); | ||
this.contentWindow = contentWindow; | ||
this.on('attribute', (name, value) => { | ||
if (name === 'width') { | ||
// XXX | ||
} else if (name === 'height') { | ||
// XXX | ||
} | ||
}); | ||
} | ||
const {document: contentDocument} = contentWindow; | ||
this.contentDocument = contentDocument; | ||
}); | ||
this.on('attribute', (name, value) => { | ||
if (name === 'src') { | ||
const url = _normalizeUrl(value); | ||
get width() { | ||
this.getAttribute('width'); | ||
} | ||
set width(value) { | ||
this.setAttribute('width', value); | ||
} | ||
fetch(url) | ||
.then(res => { | ||
if (res.status >= 200 && res.status < 300) { | ||
return res.text(); | ||
} else { | ||
return Promise.reject(new Error('iframe src got invalid status code: ' + res.status + ' : ' + url)); | ||
} | ||
}) | ||
.then(htmlString => { | ||
const contentDocument = _parseDocument(htmlString, this.contentWindow); | ||
this.contentDocument = contentDocument; | ||
get height() { | ||
this.getAttribute('height'); | ||
} | ||
set height(value) { | ||
this.setAttribute('height', value); | ||
} | ||
contentDocument.once('readystatechange', () => { | ||
this.emit('load'); | ||
}); | ||
}) | ||
.catch(err => { | ||
this.emit('error', err); | ||
}); | ||
} | ||
}); | ||
getContext(contextType) { | ||
if (this._context === null) { | ||
if (contextType === '2d') { | ||
this._context = new CanvasRenderingContext2D(this.width, this.height); | ||
} else if (contextType === 'webgl') { | ||
this._context = new WebGLContext(); | ||
} | ||
} | ||
return this._context; | ||
} | ||
class HTMLCanvasElement extends HTMLElement { | ||
constructor(attributes = {}, value = '') { | ||
super('canvas', attributes, value); | ||
} | ||
class TextNode extends Node { | ||
constructor(value) { | ||
super('#text'); | ||
this._context = null; | ||
this.value = value; | ||
} | ||
} | ||
class CommentNode extends Node { | ||
constructor(value) { | ||
super('#comment'); | ||
this.on('attribute', (name, value) => { | ||
if (name === 'width') { | ||
// XXX | ||
} else if (name === 'height') { | ||
// XXX | ||
} | ||
}); | ||
} | ||
this.value = value; | ||
} | ||
} | ||
get width() { | ||
this.getAttribute('width'); | ||
const _fromAST = (node, window, parentNode = null) => { | ||
if (node.nodeName === '#text') { | ||
const textNode = new window[htmlElementsSymbol].TextNode(node.value); | ||
textNode.parentNode = parentNode; | ||
return textNode; | ||
} else if (node.nodeName === '#comment') { | ||
const commentNode = new window[htmlElementsSymbol].CommentNode(node.value); | ||
commentNode.parentNode = parentNode; | ||
return commentNode; | ||
} else { | ||
const {tagName, value} = node; | ||
const attributes = node.attrs && _parseAttributes(node.attrs); | ||
const HTMLElementTemplate = window[htmlTagsSymbol][tagName]; | ||
const element = HTMLElementTemplate ? | ||
new HTMLElementTemplate( | ||
attributes, | ||
value | ||
) | ||
: | ||
new window[htmlElementsSymbol].HTMLElement( | ||
tagName, | ||
attributes, | ||
value | ||
); | ||
element.parentNode = parentNode; | ||
if (node.childNodes) { | ||
element.childNodes = node.childNodes.map(childNode => _fromAST(childNode, window, element)); | ||
} | ||
set width(value) { | ||
this.setAttribute('width', value); | ||
return element; | ||
} | ||
}; | ||
const _parseAttributes = attrs => { | ||
const result = {}; | ||
for (let i = 0; i < attrs.length; i++) { | ||
const attr = attrs[i]; | ||
result[attr.name] = attr.value; | ||
} | ||
return result; | ||
}; | ||
const _formatAttributes = attributes => { | ||
const result = []; | ||
for (const name in attributes) { | ||
const value = attributes[name]; | ||
result.push({ | ||
name, | ||
value, | ||
}); | ||
} | ||
return result; | ||
}; | ||
const _parseStyle = styleString => { | ||
const style = {}; | ||
const split = styleString.split(/;\s*/); | ||
for (let i = 0; i < split.length; i++) { | ||
const split2 = split[i].split(/:\s*/); | ||
if (split2.length === 2) { | ||
style[split2[0]] = split2[1]; | ||
} | ||
} | ||
return style; | ||
}; | ||
const _formatStyle = style => { | ||
let styleString = ''; | ||
for (const k in style) { | ||
styleString += (styleString.length > 0 ? ' ' : '') + k + ': ' + style[k] + ';'; | ||
} | ||
return styleString; | ||
}; | ||
const _hash = s => { | ||
let result = 0; | ||
for (let i = 0; i < s.length; i++) { | ||
result += s.codePointAt(i); | ||
} | ||
return result; | ||
}; | ||
const _promiseSerial = async promiseFns => { | ||
for (let i = 0; i < promiseFns.length; i++) { | ||
await promiseFns[i](); | ||
} | ||
}; | ||
const _loadPromise = el => new Promise((accept, reject) => { | ||
el.on('load', () => { | ||
accept(); | ||
}); | ||
el.on('error', err => { | ||
reject(err); | ||
}); | ||
}); | ||
const _runHtml = async (element, window) => { | ||
if (element instanceof HTMLElement) { | ||
const scripts = element.querySelectorAll('script'); | ||
for (let i = 0; i < scripts.length; i++) { | ||
const script = scripts[i]; | ||
if (script.run()) { | ||
if (script.attributes.async) { | ||
_loadPromise(script) | ||
.catch(err => { | ||
console.warn(err); | ||
}); | ||
} else { | ||
try { | ||
await _loadPromise(script); | ||
} catch(err) { | ||
console.warn(err); | ||
} | ||
} | ||
} | ||
} | ||
get height() { | ||
this.getAttribute('height'); | ||
const images = element.querySelectorAll('image'); | ||
for (let i = 0; i < images.length; i++) { | ||
const image = images[i]; | ||
if (image.run()) { | ||
await _loadPromise(image); | ||
} | ||
} | ||
set height(value) { | ||
this.setAttribute('height', value); | ||
} | ||
getContext(contextType) { | ||
if (this._context === null) { | ||
if (contextType === '2d') { | ||
this._context = new CanvasRenderingContext2D(this.width, this.height); | ||
} else if (contextType === 'webgl') { | ||
this._context = new WebGLContext(); | ||
} | ||
const audios = element.querySelectorAll('audio'); | ||
for (let i = 0; i < audios.length; i++) { | ||
const audio = audios[i]; | ||
if (audio.run()) { | ||
await _loadPromise(audioEl); | ||
} | ||
return this._context; | ||
} | ||
} | ||
class TextNode extends Node { | ||
constructor(value) { | ||
super('#text'); | ||
this.value = value; | ||
const videos = element.querySelectorAll('video'); | ||
for (let i = 0; i < videos.length; i++) { | ||
const video = videos[i]; | ||
if (video.run()) { | ||
await _loadPromise(videoEl); | ||
} | ||
} | ||
} | ||
class CommentNode extends Node { | ||
constructor(value) { | ||
super('#comment'); | ||
}; | ||
const _runJavascript = (jsString, window, filename = 'script') => { | ||
try { | ||
vm.runInContext(jsString, window, { | ||
filename, | ||
}); | ||
} catch (err) { | ||
console.warn(err); | ||
} | ||
}; | ||
const _makeWindow = (options = {}, parent = null, top = null) => { | ||
const _normalizeUrl = src => new URL(src, options.baseUrl).href; | ||
this.value = value; | ||
const window = new HTMLWindowElement(); | ||
window.window = window; | ||
window.self = window; | ||
window.parent = parent || window; | ||
window.top = top || window; | ||
window.innerWidth = 1280; | ||
window.innerHeight = 1024; | ||
window.console = console; | ||
window.setTimeout = setTimeout; | ||
window.clearTimeout = clearTimeout; | ||
window.setInterval = setInterval; | ||
window.clearInterval = clearInterval; | ||
window.Date = Date; | ||
window.performance = performance; | ||
window.location = url.parse(options.baseUrl); | ||
let vrDisplays = []; | ||
window.navigator = { | ||
userAgent: 'exokit', | ||
setVRMode: vrMode => { | ||
for (let i = 0; i < vrDisplays.length; i++) { | ||
vrDisplays[i].destroy(); | ||
} | ||
if (vrMode === 'vr') { | ||
vrDisplays = [new VRDisplay(window)]; | ||
} else if (vrMode === 'ar') { | ||
vrDisplays = [new ARDisplay(window)]; | ||
} | ||
}, | ||
getVRDisplays: () => vrDisplays, | ||
}; | ||
window.localStorage = new LocalStorage(path.join(options.dataPath, '.localStorage')); | ||
window.document = null; | ||
window.URL = URL; | ||
window[htmlElementsSymbol] = { | ||
Node: (Old => class Node extends Old { constructor() { super(...arguments); this[windowSymbol] = window; } })(Node), | ||
HTMLElement: (Old => class HTMLElement extends Old { constructor() { super(...arguments); this[windowSymbol] = window; } })(HTMLElement), | ||
HTMLAnchorElement: (Old => class HTMLAnchorElement extends Old { constructor() { super(...arguments); this[windowSymbol] = window; } })(HTMLAnchorElement), | ||
HTMLScriptElement: (Old => class HTMLScriptElement extends Old { constructor() { super(...arguments); this[windowSymbol] = window; } })(HTMLScriptElement), | ||
HTMLImageElement: (Old => class HTMLImageElement extends Old { constructor() { super(...arguments); this[windowSymbol] = window; } })(HTMLImageElement), | ||
HTMLAudioElement: (Old => class HTMLAudioElement extends Old { constructor() { super(...arguments); this[windowSymbol] = window; } })(HTMLAudioElement), | ||
HTMLVideoElement: (Old => class HTMLVideoElement extends Old { constructor() { super(...arguments); this[windowSymbol] = window; } })(HTMLVideoElement), | ||
HTMLIframeElement: (Old => class HTMLIframeElement extends Old { constructor() { super(...arguments); this[windowSymbol] = window; } })(HTMLIframeElement), | ||
HTMLCanvasElement: (Old => class HTMLCanvasElement extends Old { constructor() { super(...arguments); this[windowSymbol] = window; } })(HTMLCanvasElement), | ||
TextNode: (Old => class TextNode extends Old { constructor() { super(...arguments); this[windowSymbol] = window; } })(TextNode), | ||
CommentNode: (Old => class CommentNode extends Old { constructor() { super(...arguments); this[windowSymbol] = window; } })(CommentNode), | ||
}; | ||
window[htmlTagsSymbol] = { | ||
a: window[htmlElementsSymbol].HTMLAnchorElement, | ||
script: window[htmlElementsSymbol].HTMLScriptElement, | ||
img: window[htmlElementsSymbol].HTMLImageElement, | ||
audio: window[htmlElementsSymbol].HTMLAudioElement, | ||
video: window[htmlElementsSymbol].HTMLVideoElement, | ||
iframe: window[htmlElementsSymbol].HTMLIframeElement, | ||
canvas: window[htmlElementsSymbol].HTMLCanvasElement, | ||
}; | ||
window[optionsSymbol] = options; | ||
window.Image = window[htmlElementsSymbol].HTMLImageElement; | ||
window.HTMLElement = window[htmlElementsSymbol].HTMLElement; | ||
window.HTMLAnchorElement = window[htmlElementsSymbol].HTMLAnchorElement; | ||
window.HTMLScriptElement = window[htmlElementsSymbol].HTMLScriptElement; | ||
window.HTMLImageElement = window[htmlElementsSymbol].HTMLImageElement; | ||
window.HTMLAudioElement = window[htmlElementsSymbol].HTMLAudioElement; | ||
window.HTMLVideoElement = window[htmlElementsSymbol].HTMLVideoElement; | ||
window.HTMLIframeElement = window[htmlElementsSymbol].HTMLIframeElement; | ||
window.HTMLCanvasElement = window[htmlElementsSymbol].HTMLCanvasElement; | ||
window.ImageData = ImageData; | ||
window.ImageBitmap = ImageBitmap; | ||
window.Path2D = Path2D; | ||
window.CanvasRenderingContext2D = CanvasRenderingContext2D; | ||
window.VRFrameData = VRFrameData; | ||
window.btoa = s => new Buffer(s, 'binary').toString('base64'); | ||
window.atob = s => new Buffer(s, 'base64').toString('binary'); | ||
window.fetch = (url, options) => { | ||
const blob = urls.get(url); | ||
if (blob) { | ||
return Promise.resolve(new Response(blob)); | ||
} else { | ||
return fetch(_normalizeUrl(url), options); | ||
} | ||
} | ||
const HTML_ELEMENTS = { | ||
a: HTMLAnchorElement, | ||
script: HTMLScriptElement, | ||
img: HTMLImageElement, | ||
audio: HTMLAudioElement, | ||
video: HTMLVideoElement, | ||
iframe: HTMLIframeElement, | ||
canvas: HTMLCanvasElement, | ||
}; | ||
class Worker extends WindowWorker { | ||
window.XMLHttpRequest = XMLHttpRequest; | ||
window.WebSocket = WebSocket; | ||
window.Worker = class Worker extends WindowWorker { | ||
constructor(src, options = {}) { | ||
options.baseUrl = options.baseUrl || baseUrl; | ||
options.baseUrl = baseUrl; | ||
@@ -1126,217 +1250,84 @@ if (src instanceof Blob) { | ||
} | ||
} | ||
}; | ||
window.Blob = Blob; | ||
window.AudioContext = AudioContext; | ||
window.Path2D = Path2D; | ||
window.createImageBitmap = image => Promise.resolve(ImageBitmap.createImageBitmap(image)); | ||
const rafCbs = []; | ||
const _makeWindow = (parent, top) => { | ||
const window = new HTMLWindowElement(); | ||
window.window = window; | ||
window.self = window; | ||
window.parent = parent || window; | ||
window.top = top || window; | ||
window.innerWidth = 1280; | ||
window.innerHeight = 1024; | ||
window.console = console; | ||
window.setTimeout = setTimeout; | ||
window.clearTimeout = clearTimeout; | ||
window.setInterval = setInterval; | ||
window.clearInterval = clearInterval; | ||
window.Date = Date; | ||
window.performance = performance; | ||
window.location = url.parse(baseUrl); | ||
let vrDisplays = []; | ||
window.navigator = { | ||
userAgent: 'exokit', | ||
setVRMode: vrMode => { | ||
for (let i = 0; i < vrDisplays.length; i++) { | ||
vrDisplays[i].destroy(); | ||
} | ||
if (vrMode === 'vr') { | ||
vrDisplays = [new VRDisplay(window)]; | ||
} else if (vrMode === 'ar') { | ||
vrDisplays = [new ARDisplay(window)]; | ||
} | ||
}, | ||
getVRDisplays: () => vrDisplays, | ||
}; | ||
window.localStorage = new LocalStorage(path.join(options.dataPath, '.localStorage')); | ||
window.document = null; | ||
window.URL = URL; | ||
window.Image = HTMLImageElement; | ||
window.HTMLScriptElement = HTMLScriptElement; | ||
window.HTMLImageElement = HTMLImageElement; | ||
window.HTMLAudioElement = HTMLAudioElement; | ||
window.HTMLVideoElement = HTMLVideoElement; | ||
window.HTMLIframeElement = HTMLIframeElement; | ||
window.HTMLCanvasElement = HTMLCanvasElement; | ||
window.ImageData = ImageData; | ||
window.ImageBitmap = ImageBitmap; | ||
window.Path2D = Path2D; | ||
window.CanvasRenderingContext2D = CanvasRenderingContext2D; | ||
window.VRFrameData = VRFrameData; | ||
window.btoa = s => new Buffer(s, 'binary').toString('base64'); | ||
window.atob = s => new Buffer(s, 'base64').toString('binary'); | ||
window.fetch = (url, options) => { | ||
const blob = urls.get(url); | ||
if (blob) { | ||
return Promise.resolve(new Response(blob)); | ||
} else { | ||
return fetch(_normalizeUrl(url), options); | ||
} | ||
}; | ||
window.XMLHttpRequest = XMLHttpRequest; | ||
window.WebSocket = WebSocket; | ||
window.Worker = Worker; | ||
window.Blob = Blob; | ||
window.AudioContext = AudioContext; | ||
window.Path2D = Path2D; | ||
window.createImageBitmap = image => Promise.resolve(ImageBitmap.createImageBitmap(image)); | ||
window.requestAnimationFrame = fn => { | ||
rafCbs.push(fn); | ||
}; | ||
window.clearAnimationFrame = fn => { | ||
const index = rafCbs.indexOf(fn); | ||
if (index !== -1) { | ||
rafCbs.splice(index, 1); | ||
} | ||
}; | ||
window.tickAnimationFrame = () => { | ||
const localRafCbs = rafCbs.slice(); | ||
rafCbs.length = 0; | ||
for (let i = 0; i < localRafCbs.length; i++) { | ||
localRafCbs[i](); | ||
} | ||
}; | ||
window.alignFrame = (viewMatrix, projectionMatrix) => { | ||
window.emit('alignframe', viewMatrix, projectionMatrix); | ||
}; | ||
vm.createContext(window); | ||
return window; | ||
window.requestAnimationFrame = fn => { | ||
rafCbs.push(fn); | ||
}; | ||
const _parseDocument = (s, window) => { | ||
const document = Node.fromAST(parse5.parse(s), window); | ||
const html = document.childNodes.find(element => element.tagName === 'html'); | ||
const head = html.childNodes.find(element => element.tagName === 'head'); | ||
const body = html.childNodes.find(element => element.tagName === 'body'); | ||
document.documentElement = document; | ||
document.readyState = null; | ||
document.head = head; | ||
document.body = body; | ||
document.location = url.parse(baseUrl); | ||
document.createElement = tagName => { | ||
const HTMLElementTemplate = HTML_ELEMENTS[tagName]; | ||
const el = HTMLElementTemplate ? new HTMLElementTemplate() : new HTMLElement(tagName); | ||
el[setWindowSymbol](window); | ||
return el; | ||
}; | ||
document.createElementNS = (namespace, tagName) => document.createElement(tagName); | ||
document.createDocumentFragment = () => document.createElement(); | ||
document.createTextNode = text => new TextNode(text); | ||
document.createComment = comment => new CommentNode(comment); | ||
document.styleSheets = []; | ||
document.write = htmlString => { | ||
const childNodes = parse5.parseFragment(htmlString).childNodes.map(childNode => Node.fromAST(childNode, window, this)); | ||
for (let i = 0; i < childNodes.length; i++) { | ||
document.body.appendChild(childNodes[i]); | ||
} | ||
}; | ||
window.document = document; | ||
process.nextTick(async () => { | ||
document.readyState = 'complete'; | ||
try { | ||
await _runHtml(document, window); | ||
} catch(err) { | ||
console.warn(err); | ||
} | ||
document.emit('readystatechange'); | ||
}); | ||
return document; | ||
window.clearAnimationFrame = fn => { | ||
const index = rafCbs.indexOf(fn); | ||
if (index !== -1) { | ||
rafCbs.splice(index, 1); | ||
} | ||
}; | ||
const _parseWindow = (s, parent, top) => { | ||
const window = _makeWindow(parent, top); | ||
const document = _parseDocument(s, window); | ||
window.document = document; | ||
window.tickAnimationFrame = () => { | ||
const localRafCbs = rafCbs.slice(); | ||
rafCbs.length = 0; | ||
for (let i = 0; i < localRafCbs.length; i++) { | ||
localRafCbs[i](); | ||
} | ||
}; | ||
window.alignFrame = (viewMatrix, projectionMatrix) => { | ||
window.emit('alignframe', viewMatrix, projectionMatrix); | ||
}; | ||
vm.createContext(window); | ||
return window; | ||
}; | ||
const _parseDocument = (s, options, window) => { | ||
const document = _fromAST(parse5.parse(s), window); | ||
const html = document.childNodes.find(element => element.tagName === 'html'); | ||
const head = html.childNodes.find(element => element.tagName === 'head'); | ||
const body = html.childNodes.find(element => element.tagName === 'body'); | ||
return window; | ||
document.documentElement = document; | ||
document.readyState = null; | ||
document.head = head; | ||
document.body = body; | ||
document.location = url.parse(options.baseUrl); | ||
document.createElement = tagName => { | ||
const HTMLElementTemplate = window[htmlTagsSymbol][tagName]; | ||
return HTMLElementTemplate ? new HTMLElementTemplate() : new window[htmlElementsSymbol].HTMLElement(tagName); | ||
}; | ||
const window = _parseWindow(s); | ||
const {document} = window; | ||
const _promiseSerial = async promiseFns => { | ||
for (let i = 0; i < promiseFns.length; i++) { | ||
await promiseFns[i](); | ||
document.createElementNS = (namespace, tagName) => document.createElement(tagName); | ||
document.createDocumentFragment = () => document.createElement(); | ||
document.createTextNode = text => new TextNode(text); | ||
document.createComment = comment => new CommentNode(comment); | ||
document.styleSheets = []; | ||
document.write = htmlString => { | ||
const childNodes = parse5.parseFragment(htmlString).childNodes.map(childNode => _fromAST(childNode, window, this)); | ||
for (let i = 0; i < childNodes.length; i++) { | ||
document.body.appendChild(childNodes[i]); | ||
} | ||
}; | ||
const _loadPromise = el => new Promise((accept, reject) => { | ||
el.on('load', () => { | ||
accept(); | ||
}); | ||
el.on('error', err => { | ||
reject(err); | ||
}); | ||
}); | ||
const _runHtml = async (element, window) => { | ||
if (element instanceof HTMLElement) { | ||
const scripts = element.querySelectorAll('script'); | ||
for (let i = 0; i < scripts.length; i++) { | ||
const script = scripts[i]; | ||
if (script.run()) { | ||
if (script.attributes.async) { | ||
_loadPromise(script) | ||
.catch(err => { | ||
console.warn(err); | ||
}); | ||
} else { | ||
try { | ||
await _loadPromise(script); | ||
} catch(err) { | ||
console.warn(err); | ||
} | ||
} | ||
} | ||
} | ||
window.document = document; | ||
const images = element.querySelectorAll('image'); | ||
for (let i = 0; i < images.length; i++) { | ||
const image = images[i]; | ||
if (image.run()) { | ||
await _loadPromise(image); | ||
} | ||
} | ||
process.nextTick(async () => { | ||
document.readyState = 'complete'; | ||
const audios = element.querySelectorAll('audio'); | ||
for (let i = 0; i < audios.length; i++) { | ||
const audio = audios[i]; | ||
if (audio.run()) { | ||
await _loadPromise(audioEl); | ||
} | ||
} | ||
const videos = element.querySelectorAll('video'); | ||
for (let i = 0; i < videos.length; i++) { | ||
const video = videos[i]; | ||
if (video.run()) { | ||
await _loadPromise(videoEl); | ||
} | ||
} | ||
} | ||
}; | ||
const _runJavascript = (jsString, window, filename = 'script') => { | ||
try { | ||
vm.runInContext(jsString, window, { | ||
filename, | ||
}); | ||
} catch (err) { | ||
await _runHtml(document, window); | ||
} catch(err) { | ||
console.warn(err); | ||
} | ||
}; | ||
document.emit('readystatechange'); | ||
}); | ||
return document; | ||
}; | ||
const _parseWindow = (s, options, parent, top) => { | ||
const window = _makeWindow(options, parent, top); | ||
const document = _parseDocument(s, options, window); | ||
window.document = document; | ||
return window; | ||
}; | ||
const exokit = (s = '', options = {}) => { | ||
options.baseUrl = options.baseUrl || 'http://127.0.0.1'; | ||
options.dataPath = options.dataPath || __dirname; | ||
return _parseWindow(s, options); | ||
}; | ||
exokit.fetch = src => fetch(src) | ||
@@ -1353,3 +1344,3 @@ .then(res => { | ||
return exokit(htmlString, { | ||
url: (parsedUrl.protocol || 'http:') + '//' + (parsedUrl.host || '127.0.0.1'), | ||
baseUrl: (parsedUrl.protocol || 'http:') + '//' + (parsedUrl.host || '127.0.0.1'), | ||
}); | ||
@@ -1356,0 +1347,0 @@ }); |
{ | ||
"name": "exokit", | ||
"version": "0.0.12", | ||
"version": "0.0.13", | ||
"main": "index.js", | ||
@@ -5,0 +5,0 @@ "dependencies": { |
85355
2745
3