"use strict" module.exports = function() { function isModernEvent(type) { return type === "transitionstart" || type === "transitionend" || type === "animationstart" || type === "animationend" } function appendChild(child) { var ancestor = this while (ancestor !== child && ancestor !== null) ancestor = ancestor.parentNode if (ancestor === child) throw new Error("Node cannot be inserted at the specified point in the hierarchy") if (child.nodeType == null) throw new Error("Argument is not a DOM element") var index = this.childNodes.indexOf(child) if (index > -1) this.childNodes.splice(index, 1) if (child.nodeType === 11) { while (child.firstChild != null) this.appendChild(child.firstChild) child.childNodes = [] } else { this.childNodes.push(child) if (child.parentNode != null && child.parentNode !== this) child.parentNode.removeChild(child) child.parentNode = this } } function removeChild(child) { var index = this.childNodes.indexOf(child) if (index > -1) { this.childNodes.splice(index, 1) child.parentNode = null } else throw new TypeError("Failed to execute 'removeChild'") } function insertBefore(child, reference) { var ancestor = this while (ancestor !== child && ancestor !== null) ancestor = ancestor.parentNode if (ancestor === child) throw new Error("Node cannot be inserted at the specified point in the hierarchy") if (child.nodeType == null) throw new Error("Argument is not a DOM element") var refIndex = this.childNodes.indexOf(reference) var index = this.childNodes.indexOf(child) if (reference !== null && refIndex < 0) throw new TypeError("Invalid argument") if (index > -1) this.childNodes.splice(index, 1) if (reference === null) this.appendChild(child) else { if (child.nodeType === 11) { this.childNodes.splice.apply(this.childNodes, [refIndex, 0].concat(child.childNodes)) while (child.firstChild) { var subchild = child.firstChild child.removeChild(subchild) subchild.parentNode = this } child.childNodes = [] } else { this.childNodes.splice(refIndex, 0, child) if (child.parentNode != null && child.parentNode !== this) child.parentNode.removeChild(child) child.parentNode = this } } } function getAttribute(name) { if (this.attributes[name] == null) return null return this.attributes[name].nodeValue } function setAttribute(name, value) { var nodeValue = String(value) this.attributes[name] = { namespaceURI: null, get nodeValue() {return nodeValue}, set nodeValue(value) {nodeValue = String(value)}, } } function setAttributeNS(ns, name, value) { this.setAttribute(name, value) this.attributes[name].namespaceURI = ns } function removeAttribute(name) { delete this.attributes[name] } var declListTokenizer = /;|"(?:\\.|[^"\n])*"|'(?:\\.|[^'\n])*'/g /** * This will split a semicolon-separated CSS declaration list into an array of * individual declarations, ignoring semicolons in strings. * * Comments are also stripped. * * @param {string} declList * @return {string[]} */ function splitDeclList(declList) { var indices = [], res = [], match // remove comments, preserving comments in strings. declList = declList.replace( /("(?:\\.|[^"\n])*"|'(?:\\.|[^'\n])*')|\/\*[\s\S]*?\*\//g, function(m, str){ return str || '' } ) /*eslint-disable no-cond-assign*/ while (match = declListTokenizer.exec(declList)) { if (match[0] === ";") indices.push(match.index) } /*eslint-enable no-cond-assign*/ for (var i = indices.length; i--;){ res.unshift(declList.slice(indices[i] + 1)) declList = declList.slice(0, indices[i]) } res.unshift(declList) return res } var activeElement var $window = { document: { createElement: function(tag, is) { var cssText = "" var style = {} Object.defineProperty(style, "cssText", { get: function() {return cssText}, set: function (value) { var buf = [] if (typeof value === "string") { for (var key in style) style[key] = "" var rules = splitDeclList(value) for (var i = 0; i < rules.length; i++) { var rule = rules[i] var colonIndex = rule.indexOf(":") if (colonIndex > -1) { var rawKey = rule.slice(0, colonIndex).trim() var key = rawKey.replace(/-\D/g, function(match) {return match[1].toUpperCase()}) var value = rule.slice(colonIndex + 1).trim() if (key !== "cssText") { style[key] = value buf.push(rawKey + ": " + value + ";") } } } cssText = buf.join(" ") } } }) var events = {} var element = { nodeType: 1, nodeName: tag.toUpperCase(), namespaceURI: "http://www.w3.org/1999/xhtml", appendChild: appendChild, removeChild: removeChild, insertBefore: insertBefore, getAttribute: getAttribute, setAttribute: setAttribute, setAttributeNS: setAttributeNS, removeAttribute: removeAttribute, parentNode: null, childNodes: [], attributes: {}, get firstChild() { return this.childNodes[0] || null }, get nextSibling() { if (this.parentNode == null) return null var index = this.parentNode.childNodes.indexOf(this) if (index < 0) throw new TypeError("Parent's childNodes is out of sync") return this.parentNode.childNodes[index + 1] || null }, set textContent(value) { this.childNodes = [] if (value !== "") this.appendChild($window.document.createTextNode(value)) }, set innerHTML(value) { while (this.firstChild) this.removeChild(this.firstChild) var stack = [this], depth = 0, voidElements = ["area", "base", "br", "col", "command", "embed", "hr", "img", "input", "keygen", "link", "meta", "param", "source", "track", "wbr"] value.replace(/<([a-z0-9\-]+?)((?:\s+?[^=]+?=(?:"[^"]*?"|'[^']*?'|[^\s>]*))*?)(\s*\/)?>|<\/([a-z0-9\-]+?)>|([^<]+)/g, function(match, startTag, attrs, selfClosed, endTag, text) { if (startTag) { var element = $window.document.createElement(startTag) attrs.replace(/\s+?([^=]+?)=(?:"([^"]*?)"|'([^']*?)'|([^\s>]*))/g, function(match, key, doubleQuoted, singleQuoted, unquoted) { var keyParts = key.split(":") var name = keyParts.pop() var ns = keyParts[0] var value = doubleQuoted || singleQuoted || unquoted || "" if (ns != null) element.setAttributeNS(ns, name, value) else element.setAttribute(name, value) }) stack[depth].appendChild(element) if (!selfClosed && voidElements.indexOf(startTag.toLowerCase()) < 0) stack[++depth] = element } else if (endTag) { depth-- } else if (text) { stack[depth].appendChild($window.document.createTextNode(text)) // FIXME handle html entities } }) }, get style() { return style }, set style(_){ // https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/style#Setting_style throw new Error("setting element.style is not portable") }, get className() { return this.attributes["class"] ? this.attributes["class"].nodeValue : "" }, set className(value) { if (this.namespaceURI === "http://www.w3.org/2000/svg") throw new Error("Cannot set property className of SVGElement") else this.setAttribute("class", value) }, focus: function() {activeElement = this}, addEventListener: function(type, callback, useCapture) { if (events[type] == null) events[type] = [callback] else events[type].push(callback) }, removeEventListener: function(type, callback, useCapture) { if (events[type] != null) { var index = events[type].indexOf(callback) if (index > -1) events[type].splice(index, 1) } }, dispatchEvent: function(e) { if (this.nodeName === "INPUT" && this.attributes["type"] != null && this.attributes["type"].nodeValue === "checkbox" && e.type === "click") { this.checked = !this.checked } e.target = this if (events[e.type] != null) { for (var i = 0; i < events[e.type].length; i++) { events[e.type][i].call(this, e) } } e.preventDefault = function() { // TODO: should this do something? } if (typeof this["on" + e.type] === "function" && !isModernEvent(e.type)) this["on" + e.type](e) }, onclick: null, } if (element.nodeName === "A") { var href Object.defineProperty(element, "href", { get: function() {return this.attributes["href"] === undefined ? "" : "[FIXME implement]"}, set: function(value) {this.setAttribute("href", value)}, enumerable: true, }) } if (element.nodeName === "INPUT") { var checked Object.defineProperty(element, "checked", { get: function() {return checked === undefined ? this.attributes["checked"] !== undefined : checked}, set: function(value) {checked = Boolean(value)}, enumerable: true, }) element.value = "" } if (element.nodeName === "TEXTAREA") { var value Object.defineProperty(element, "value", { get: function() { return value != null ? value : this.firstChild ? this.firstChild.nodeValue : "" }, set: function(v) {value = v}, enumerable: true, }) } if (element.nodeName === "CANVAS") { Object.defineProperty(element, "width", { get: function() {return this.attributes["width"] ? Math.floor(parseInt(this.attributes["width"].nodeValue) || 0) : 300}, set: function(value) {this.setAttribute("width", Math.floor(Number(value) || 0).toString())}, }) Object.defineProperty(element, "height", { get: function() {return this.attributes["height"] ? Math.floor(parseInt(this.attributes["height"].nodeValue) || 0) : 300}, set: function(value) {this.setAttribute("height", Math.floor(Number(value) || 0).toString())}, }) } function getOptions(element) { var options = [] for (var i = 0; i < element.childNodes.length; i++) { if (element.childNodes[i].nodeName === "OPTION") options.push(element.childNodes[i]) else if (element.childNodes[i].nodeName === "OPTGROUP") options = options.concat(getOptions(element.childNodes[i])) } return options } function getOptionValue(element) { return element.attributes["value"] != null ? element.attributes["value"].nodeValue : element.firstChild != null ? element.firstChild.nodeValue : "" } if (element.nodeName === "SELECT") { var selectedValue, selectedIndex = 0 Object.defineProperty(element, "selectedIndex", { get: function() {return getOptions(this).length > 0 ? selectedIndex : -1}, set: function(value) { var options = getOptions(this) if (value >= 0 && value < options.length) { selectedValue = getOptionValue(options[selectedIndex]) selectedIndex = value } else { selectedValue = "" selectedIndex = -1 } }, enumerable: true, }) Object.defineProperty(element, "value", { get: function() { if (this.selectedIndex > -1) return getOptionValue(getOptions(this)[this.selectedIndex]) return "" }, set: function(value) { var options = getOptions(this) var stringValue = String(value) for (var i = 0; i < options.length; i++) { if (getOptionValue(options[i]) === stringValue) { selectedValue = stringValue selectedIndex = i return } } selectedValue = stringValue selectedIndex = -1 }, enumerable: true, }) } if (element.nodeName === "OPTION") { Object.defineProperty(element, "value", { get: function() {return getOptionValue(this)}, set: function(value) { this.setAttribute("value", value) }, enumerable: true, }) Object.defineProperty(element, "selected", { get: function() { var options = getOptions(this.parentNode) var index = options.indexOf(this) return index === this.parentNode.selectedIndex }, set: function(value) { if (value) { var options = getOptions(this.parentNode) var index = options.indexOf(this) if (index > -1) this.parentNode.selectedIndex = index } else this.parentNode.selectedIndex = 0 }, enumerable: true, }) } return element }, createElementNS: function(ns, tag, is) { var element = this.createElement(tag, is) element.nodeName = tag element.namespaceURI = ns return element }, createTextNode: function(text) { var nodeValue = String(text) return { nodeType: 3, nodeName: "#text", parentNode: null, get nodeValue() {return nodeValue}, set nodeValue(value) {nodeValue = String(value)}, } }, createDocumentFragment: function() { return { nodeType: 11, nodeName: "#document-fragment", appendChild: appendChild, insertBefore: insertBefore, removeChild: removeChild, parentNode: null, childNodes: [], get firstChild() { return this.childNodes[0] || null }, } }, createEvent: function() { return { initEvent: function(type) {this.type = type}, } }, get activeElement() {return activeElement}, }, } $window.document.documentElement = $window.document.createElement("html") $window.document.documentElement.appendChild($window.document.createElement("head")) $window.document.body = $window.document.createElement("body") $window.document.documentElement.appendChild($window.document.body) activeElement = $window.document.body return $window }