├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── basichtml.js ├── logo ├── basichtml.inkscape.svg ├── basichtml.png └── basichtml.svg ├── package.json ├── src ├── Attr.js ├── CSSStyleDeclaration.js ├── CharacterData.js ├── ChildNode.js ├── Comment.js ├── CustomElementRegistry.js ├── CustomEvent.js ├── DOMStringMap.js ├── DOMTokenList.js ├── Document.js ├── DocumentFragment.js ├── DocumentType.js ├── Element.js ├── Event.js ├── EventTarget.js ├── HTMLCanvasElement.js ├── HTMLElement.js ├── HTMLHtmlElement.js ├── HTMLImageElement.js ├── HTMLNoComments.js ├── HTMLStyleElement.js ├── HTMLTemplateElement.js ├── HTMLTextAreaElement.js ├── HTMLUnknownElement.js ├── Image.js ├── ImageFactory.js ├── ImagePrototype.js ├── NamedNodeMap.js ├── Node.js ├── NodeFilter.js ├── ParentNode.js ├── Range.js ├── Text.js ├── TreeWalker.js └── utils.js ├── test ├── all.js ├── custom-element.js ├── html.js ├── hyperhtml.js ├── issue-56.js ├── many-rows.js ├── sizzle.js ├── style.js ├── textarea.js ├── void.js └── xmlish.js └── web.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | coverage/ 3 | .nyc_output/ 4 | package-lock.json 5 | 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .nyc_output/* 2 | coverage/* 3 | logo/* 4 | node_modules/* 5 | test/* 6 | .DS_Store 7 | .gitignore 8 | .travis.yml 9 | package-lock.json 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 13 4 | git: 5 | depth: 1 6 | branches: 7 | only: 8 | - master 9 | - /^greenkeeper/.*$/ 10 | after_success: 11 | - "npm run coveralls" 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2017-2020, Andrea Giammarchi, @WebReflection 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 10 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 11 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 12 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 13 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE 14 | OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 15 | PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # basicHTML 2 | 3 | [![Coverage Status](https://coveralls.io/repos/github/WebReflection/basicHTML/badge.svg?branch=master)](https://coveralls.io/github/WebReflection/basicHTML?branch=master) 4 | [![Build Status](https://travis-ci.org/WebReflection/basicHTML.svg?branch=master)](https://travis-ci.org/WebReflection/basicHTML) 5 | [![License: ISC](https://img.shields.io/badge/License-ISC-yellow.svg)](https://opensource.org/licenses/ISC) 6 | 7 | 8 | ## 📣 Announcement 9 | 10 | Be aware there is a shiny new module called **[LinkeDOM](https://github.com/WebReflection/linkedom#readme)** which is completely different, but better than *basicHTML*, at pretty much everything. 11 | 12 | All modules of mine are going to use *linkedom* instead, and *basicHTML* will be soon deprecated or put in maintainance mode. 13 | 14 | Feel free to read the [related post](https://webreflection.medium.com/linkedom-a-jsdom-alternative-53dd8f699311) to know more about this decision. 15 | 16 | - - - 17 | 18 | A NodeJS based, standard oriented, HTML implementation. 19 | 20 | viperHTML logo 21 | 22 | 23 | ### Breaking V2 Changes 24 | 25 | As the `canvas` module brought in ~100MB of dependency, and as it's not even a common use case, I've decided to move the `canvas` package into `devDependencies`, so that you need to explicitly include it when you use _basicHTML_. 26 | 27 | ```js 28 | npm i basichtml canvas 29 | ``` 30 | 31 | By default, no `canvas` module will be installed at all. 32 | 33 | 34 | ### New in v1 35 | 36 | Introduced optional [node-canvas](https://www.npmjs.com/package/canvas) dependency behind the `` and `` scene 🦄 37 | 38 | * automatic fallback if the `canvas` module doesn't build 39 | * provide canvas 2d API, with the ability to create real images 40 | * provide the `node-canvas` Image ability to react on `load` and `error` events 41 | 42 | ```js 43 | const {Image, document} = require('basichtml').init({}); 44 | 45 | const canvas = document.createElement('canvas'); 46 | canvas.width = 320; 47 | canvas.height = 200; 48 | 49 | const ctx = canvas.getContext('2d'); 50 | ctx.moveTo(0, 0); 51 | ctx.lineTo(320, 200); 52 | ctx.stroke(); 53 | 54 | const img = new Image(); 55 | img.onload = () => { 56 | console.log(img.outerHTML); 57 | }; 58 | img.src = canvas.toDataURL(); 59 | ``` 60 | 61 | 62 | ### New in 0.23 63 | 64 | Custom Elements built-in extends are finally supported 🎉 65 | 66 | ```js 67 | customElements.define('my-special-thing', MySpecialThing, {extends: 'div'}); 68 | document.createElement('div', {is: 'my-special-thing'}); 69 | ``` 70 | 71 | ### New `init(...)` in 0.13 72 | ```js 73 | // easy way, introduced in 0.13 74 | // pollutes by default the global with: 75 | // - window 76 | // - document 77 | // - customElements 78 | // - HTMLElement 79 | // if a non global window is provided 80 | // it will use it as defaultView 81 | require('basichtml').init({ 82 | // all properties are optional 83 | window: global, 84 | // in case you'd like to share a predefined 85 | // registry of Custom Elements 86 | customElements, 87 | // specify a different selector 88 | selector: { 89 | // use the module sizzle, it will be required 90 | // automatically 91 | name: 'sizzle', 92 | // or alternatively, use a module function 93 | module() { 94 | return require('sizzle'); 95 | }, 96 | // how to retrieve results => querySelectorAll 97 | $(Sizzle, element, css) { 98 | return Sizzle(css, element); 99 | } 100 | } 101 | }); 102 | // returns the window itself 103 | ``` 104 | 105 | 106 | #### Good old way to init with basic selectors 107 | ```js 108 | const {Document} = require('basichtml'); 109 | 110 | const document = new Document(); 111 | 112 | // attributes 113 | document.documentElement.setAttribute('lang', 'en'); 114 | 115 | // common accessors 116 | document.documentElement.innerHTML = ` 117 | 118 | 119 | `; 120 | document.body.textContent = 'Hello basicHTML'; 121 | 122 | // basic querySelector / querySelectorAll 123 | document.querySelector('head').appendChild( 124 | document.createElement('title') 125 | ).textContent = 'HTML on NodeJS'; 126 | 127 | // toString() necessary to read, it's a Buffer 128 | console.log(document.toString()); 129 | ``` 130 | 131 | Above log will produce an output like the following one. 132 | ```html 133 | 134 | 135 | HTML on NodeJS 136 | Hello basicHTML 137 | 138 | ``` 139 | 140 | 141 | ### Features 142 | 143 | * create any amount of documents 144 | * document fragments, comments, text nodes, and elements 145 | * elements have classList and dataset too 146 | * Event and CustomEvent through add/removeEventListener and dispatchEvent 147 | * DOM Level 0 compatible events 148 | * Attributes compatible with Custom Elements reactions 149 | * arbitrary Custom Elements creation 150 | * customizable selector engine 151 | 152 | 153 | #### Current caveats / exceptions 154 | 155 | * since `v0.2`, the property `nodeName` is **case-sensitive** to make _basicHTML_ compatible with _XML_ projects too 156 | * `el.querySelectorAll(css)` works with `tagName`, `#id`, or `.className`. You can use more complex selectors including 3rd party libraries such [Sizzle](https://github.com/jquery/sizzle), as shown in this [test example](https://github.com/WebReflection/basicHTML/blob/master/test/sizzle.js). 157 | * `el.querySelector(css)` is not optimized and will return just index `0` of the whole collection. However, selecting a lot is not the goal of this library. 158 | * `el.getElementsByTagName` as well as `el.getElementsByClassName` and `el.getElementsById` are all available. The latter is the fastest one of the trio. 159 | * all collections are basically just arrays. You should use official DOM methods to mutate them. As example, do not ever `childNodes.push(new Node)` 'cause that's not what you could do on the DOM. The whole point here is to provide a Web like env, not to write defensive code for NodeJS or other non strictly Web environments. 160 | * most historical properties and standards are most likely not implemented 161 | 162 | 163 | ### License 164 | ``` 165 | ISC License 166 | 167 | Copyright (c) 2017, Andrea Giammarchi, @WebReflection 168 | 169 | Permission to use, copy, modify, and/or distribute this software for any 170 | purpose with or without fee is hereby granted, provided that the above 171 | copyright notice and this permission notice appear in all copies. 172 | 173 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 174 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 175 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 176 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 177 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE 178 | OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 179 | PERFORMANCE OF THIS SOFTWARE. 180 | ``` 181 | 182 | 183 | #### What is this about? 184 | 185 | This is an essential implementation of most common HTML operations without the necessary bloat brought in by the entire HTML specification. 186 | 187 | The ideal scenario is together with [hyperHTML](https://github.com/WebReflection/hyperHTML) to be able to create DOM trees and objects capable of being updated, refreshed, related to any native component. 188 | 189 | The perfect scenario would be to drive [NativeScript](https://www.nativescript.org/) components using a CustomElementRegistry like you would do on the Web for Custom Elements. 190 | 191 | Please bear in mind this project is not aiming to become a fully standard compliant implementation of the whole WebIDL based specifications, there are other projects for that. 192 | -------------------------------------------------------------------------------- /basichtml.js: -------------------------------------------------------------------------------- 1 | const utils = require('./src/utils'); 2 | const CustomElementRegistry = require('./src/CustomElementRegistry'); 3 | const Document = require('./src/Document'); 4 | const EventTarget = require('./src/EventTarget'); 5 | const HTMLElement = require('./src/HTMLElement'); 6 | const HTMLUnknownElement = require('./src/HTMLUnknownElement'); 7 | const CustomEvent = require('./src/CustomEvent'); 8 | const Image = require('./src/ImageFactory'); 9 | const NodeFilter = require('./src/NodeFilter'); 10 | const TreeWalker = require('./src/TreeWalker'); 11 | 12 | module.exports = { 13 | Attr: require('./src/Attr'), 14 | CharacterData: require('./src/CharacterData'), 15 | Comment: require('./src/Comment'), 16 | CustomElementRegistry: CustomElementRegistry, 17 | CustomEvent: CustomEvent, 18 | Document: Document, 19 | DocumentFragment: require('./src/DocumentFragment'), 20 | DocumentType: require('./src/DocumentType'), 21 | DOMStringMap: require('./src/DOMStringMap'), 22 | DOMTokenList: require('./src/DOMTokenList'), 23 | Element: require('./src/Element'), 24 | Event: require('./src/Event'), 25 | EventTarget: EventTarget, 26 | HTMLElement: HTMLElement, 27 | HTMLUnknownElement: HTMLUnknownElement, 28 | HTMLHtmlElement: require('./src/HTMLHtmlElement'), 29 | HTMLTemplateElement: require('./src/HTMLTemplateElement'), 30 | Image: Image, 31 | Node: require('./src/Node'), 32 | Text: require('./src/Text'), 33 | NodeFilter: NodeFilter, 34 | TreeWalker: TreeWalker, 35 | init: (options) => { 36 | if (!options) options = {}; 37 | const window = options.window || 38 | (typeof self === 'undefined' ? global : self); 39 | window.customElements = options.customElements || 40 | new CustomElementRegistry(); 41 | window.document = new Document(window.customElements); 42 | window.window = window; 43 | window.HTMLElement = HTMLElement; 44 | window.HTMLUnknownElement = HTMLUnknownElement; 45 | window.CustomEvent = CustomEvent; 46 | window.Image = function (...args) { 47 | return Image(window.document, ...args); 48 | }; 49 | EventTarget.init(window); 50 | if (options.selector) { 51 | const $ = options.selector.$; 52 | const selector = options.selector.module ? 53 | options.selector.module(window) : 54 | require(options.selector.name); 55 | utils.querySelectorAll = function querySelectorAll(css) { 56 | return $(selector, this, css); 57 | }; 58 | } 59 | return window; 60 | } 61 | }; 62 | -------------------------------------------------------------------------------- /logo/basichtml.inkscape.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | 24 | 30 | 33 | 38 | 39 | 42 | 47 | 52 | 57 | 58 | 59 | 62 | 67 | 72 | 77 | 78 | 79 | 80 | 81 | 100 | 102 | 103 | 105 | image/svg+xml 106 | 108 | 109 | 110 | 111 | 112 | 117 | 119 | 126 | 129 | 134 | 137 | 140 | 145 | 150 | 151 | 152 | 153 | 157 | 162 | 163 | 167 | 172 | 173 | 177 | 182 | 183 | 189 | 195 | 198 | 199 | 200 | 201 | 202 | -------------------------------------------------------------------------------- /logo/basichtml.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WebReflection/basicHTML/a957c7843d4c1e73a866774cd4a8cbbdb5f4a3ed/logo/basichtml.png -------------------------------------------------------------------------------- /logo/basichtml.svg: -------------------------------------------------------------------------------- 1 | 2 | 13 | 15 | 21 | 24 | 28 | 29 | 32 | 36 | 41 | 45 | 46 | 47 | 50 | 54 | 59 | 63 | 64 | 65 | 66 | 67 | 69 | 70 | 72 | image/svg+xml 73 | 75 | 76 | 77 | 78 | 79 | 82 | 84 | 91 | 94 | 99 | 102 | 105 | 109 | 113 | 114 | 115 | 116 | 120 | 124 | 125 | 129 | 133 | 134 | 138 | 142 | 143 | 147 | 151 | 154 | 155 | 156 | 157 | 158 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "basichtml", 3 | "type": "commonjs", 4 | "version": "2.4.9", 5 | "description": "A NodeJS based, standard oriented, HTML implementation.", 6 | "main": "basichtml.js", 7 | "unpkg": "web.js", 8 | "scripts": { 9 | "build": "npm test && npm run web", 10 | "test": "nyc node test/all.js", 11 | "coveralls": "nyc report --reporter=text-lcov | coveralls", 12 | "web": "echo \"$(cat basichtml.js)global.basicHTML=module.exports;\" | browserify - | uglifyjs --comments=/^!/ --compress --mangle -o web.js && npm run less-ag-licenses", 13 | "less-ag-licenses": "node -e 'var fs=require(\"fs\");fs.writeFile(\"web.js\",fs.readFileSync(\"web.js\").toString().replace(/\\s*Permission[\\S\\s]+?SOFTWARE\\.\\s*/,\"\"),Object)'" 14 | }, 15 | "author": "Andrea Giammarchi", 16 | "license": "ISC", 17 | "dependencies": { 18 | "@webreflection/interface": "^0.1.1", 19 | "broadcast": "^3.0.0", 20 | "html-escaper": "^3.0.0", 21 | "htmlparser2": "^4.1.0" 22 | }, 23 | "devDependencies": { 24 | "browserify": "^16.5.2", 25 | "canvas": "^2.6.1", 26 | "coveralls": "^3.1.0", 27 | "hyperhtml": "^2.33.0", 28 | "nyc": "^15.1.0", 29 | "sizzle": "^2.3.5", 30 | "tressa": "^0.3.1", 31 | "uglify-es": "^3.3.9" 32 | }, 33 | "repository": { 34 | "type": "git", 35 | "url": "git+https://github.com/WebReflection/basicHTML.git" 36 | }, 37 | "bugs": { 38 | "url": "https://github.com/WebReflection/basicHTML/issues" 39 | }, 40 | "homepage": "https://github.com/WebReflection/basicHTML#readme" 41 | } 42 | -------------------------------------------------------------------------------- /src/Attr.js: -------------------------------------------------------------------------------- 1 | const utils = require('./utils'); 2 | const Node = require('./Node'); 3 | 4 | // interface Attr // https://dom.spec.whatwg.org/#attr 5 | module.exports = class Attr extends Node { 6 | 7 | constructor( 8 | ownerElement, 9 | name, 10 | /* istanbul ignore next */ 11 | value = null 12 | ) { 13 | super(ownerElement.ownerDocument); 14 | this.ownerElement = ownerElement; 15 | this.name = name; 16 | this.nodeType = Node.ATTRIBUTE_NODE; 17 | this.nodeName = name; 18 | this._value = value; 19 | } 20 | 21 | get value() { 22 | return this.name === 'style' ? this._value.cssText : this._value; 23 | } 24 | 25 | set value(_value) { 26 | const oldValue = this._value; 27 | switch (this.name) { 28 | case 'style': 29 | this._value.cssText = _value; 30 | break; 31 | case 'class': 32 | if (this.ownerElement) { 33 | const cl = this.ownerElement.classList; 34 | if (_value == null) { 35 | this._value = _value; 36 | cl.splice(0, cl.length); 37 | } else { 38 | this._value = String(_value); 39 | if (oldValue !== this._value) { 40 | cl.value = this._value; 41 | } 42 | } 43 | break; 44 | } 45 | default: 46 | this._value = _value; 47 | break; 48 | } 49 | if (this.ownerElement && oldValue !== this._value) { 50 | utils.notifyAttributeChanged( 51 | this.ownerElement, 52 | this.name, oldValue, this._value 53 | ); 54 | } 55 | } 56 | 57 | }; 58 | -------------------------------------------------------------------------------- /src/CSSStyleDeclaration.js: -------------------------------------------------------------------------------- 1 | const handler = { 2 | has(target, property) { 3 | switch (property) { 4 | case 'cssText': 5 | return true; 6 | } 7 | return _.get(target).props.hasOwnProperty(property); 8 | }, 9 | get(target, property, receiver) { 10 | switch (property) { 11 | case 'cssText': return target.toString(); 12 | case 'getPropertyValue': return target.getPropertyValue.bind(_.get(target).props); 13 | case 'setProperty': return target.setProperty.bind(_.get(target).props); 14 | } 15 | return _.get(target).props[property]; 16 | }, 17 | set(target, property, value) { 18 | const rel = _.get(target); 19 | const {props} = rel; 20 | rel.value = ('' + value).trim(); 21 | switch (property) { 22 | case 'cssText': 23 | for (const key in props) delete props[key]; 24 | (value || '').split(';').forEach(pair => { 25 | const kv = pair.split(':'); 26 | if (kv.length < 2) 27 | return; 28 | const key = toProperty((kv[0] || '').trim()); 29 | if (key) { 30 | const value = kv[1].trim(); 31 | props[key] = (key === '_hyper' ? ' ' : '') + value; 32 | } 33 | }); 34 | break; 35 | default: 36 | props[property] = value; 37 | break; 38 | } 39 | return true; 40 | } 41 | }; 42 | 43 | const _ = new WeakMap; 44 | 45 | module.exports = class CSSStyleDeclaration { 46 | constructor() { 47 | _.set(this, {props: {}, value: ''}); 48 | return new Proxy(this, handler); 49 | } 50 | getPropertyValue(key) { 51 | return this[key]; 52 | } 53 | setProperty(key, value) { 54 | this[key] = value; 55 | } 56 | toString() { 57 | const {props, value} = _.get(this); 58 | return Object.keys(props).reduce( 59 | (css, key) => css + toStyle(key) + ':' + props[key] + ';', 60 | '' 61 | ) || value; 62 | } 63 | }; 64 | 65 | function toProperty(key) { 66 | return key.replace(/(.?)-([^-])/g, ($0, $1, $2) => ($1 === '-' ? $0 : ($1 + $2.toUpperCase()))); 67 | } 68 | 69 | function toStyle(key) { 70 | return key.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase(); 71 | } 72 | -------------------------------------------------------------------------------- /src/CharacterData.js: -------------------------------------------------------------------------------- 1 | const Node = require('./Node'); 2 | const ChildNode = require('./ChildNode'); 3 | 4 | // interface CharacterData // https://dom.spec.whatwg.org/#characterdata 5 | module.exports = class CharacterData extends Node.implements(ChildNode) { 6 | constructor(ownerDocument, data) { 7 | super(ownerDocument); 8 | this.data = data; 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /src/ChildNode.js: -------------------------------------------------------------------------------- 1 | require('@webreflection/interface'); 2 | 3 | // interface ChildNode @ https://dom.spec.whatwg.org/#interface-childnode 4 | module.exports = Object.interface({ 5 | before(node) { 6 | const {parentNode} = this; 7 | if (parentNode) 8 | parentNode.insertBefore(node, this); 9 | }, 10 | after(node) { 11 | const {parentNode} = this; 12 | if (parentNode) 13 | parentNode.insertBefore(node, this.nextSibling); 14 | }, 15 | replaceWith(node) { 16 | const {parentNode} = this; 17 | if (parentNode) 18 | parentNode.replaceChild(node, this); 19 | }, 20 | remove() { 21 | const {parentNode} = this; 22 | if (parentNode) 23 | parentNode.removeChild(this); 24 | } 25 | }); 26 | -------------------------------------------------------------------------------- /src/Comment.js: -------------------------------------------------------------------------------- 1 | const Node = require('./Node'); 2 | const CharacterData = require('./CharacterData'); 3 | 4 | // interface Comment // https://dom.spec.whatwg.org/#comment 5 | module.exports = class Comment extends CharacterData { 6 | constructor(ownerDocument, comment) { 7 | super(ownerDocument, comment); 8 | this.nodeType = Node.COMMENT_NODE; 9 | this.nodeName = '#comment'; 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /src/CustomElementRegistry.js: -------------------------------------------------------------------------------- 1 | // CAVEAT: Custom Elements MUST be defined upfront 2 | // There is no instance upgrade since it's 3 | // not possible to reproduce it procedurally. 4 | 5 | const broadcast = require('broadcast'); 6 | 7 | module.exports = class CustomElementRegistry { 8 | 9 | constructor() { 10 | this._registry = Object.create(null); 11 | } 12 | 13 | define(name, constructor, options) { 14 | if (name in this._registry) 15 | throw new Error(name + ' already defined'); 16 | this._registry[name] = constructor; 17 | broadcast.that(name, constructor); 18 | } 19 | 20 | get(name) { 21 | return this._registry[name] || null; 22 | } 23 | 24 | whenDefined(name) { 25 | return broadcast.when(name); 26 | } 27 | 28 | }; 29 | -------------------------------------------------------------------------------- /src/CustomEvent.js: -------------------------------------------------------------------------------- 1 | const Event = require('./Event'); 2 | 3 | // interface CustomEvent // https://dom.spec.whatwg.org/#customevent 4 | module.exports = class CustomEvent extends Event { 5 | constructor(type, eventInitDict = { 6 | bubbles: false, 7 | cancelable: false, 8 | composed: false, 9 | detail: null 10 | }) { 11 | super(type, eventInitDict); 12 | this.detail = eventInitDict.detail; 13 | } 14 | 15 | initCustomEvent(type, bubbles, cancelable, detail) { 16 | this.initEvent(type, bubbles, cancelable); 17 | this.detail = detail; 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /src/DOMStringMap.js: -------------------------------------------------------------------------------- 1 | const HYPHENIZE = /(^|[a-z])([A-Z]+)/g; 2 | 3 | const hyphen = k => 'data-' + String(k).replace(HYPHENIZE, '$1-$2').toLowerCase(); 4 | 5 | const DOMStringMapHandler = { 6 | 7 | has(target, property) { 8 | return target._ownerElement.hasAttribute(hyphen(property)); 9 | }, 10 | 11 | get(target, property) { 12 | return target._ownerElement.getAttribute(hyphen(property)); 13 | }, 14 | 15 | set(target, property, value) { 16 | target._ownerElement.setAttribute(hyphen(property), value); 17 | return true; 18 | }, 19 | 20 | deleteProperty(target, property) { 21 | target._ownerElement.removeAttribute(hyphen(property)); 22 | return true; 23 | } 24 | 25 | }; 26 | 27 | // interface DOMStringMap // https://html.spec.whatwg.org/multipage/dom.html#domstringmap 28 | module.exports = function DOMStringMap(ownerElement) {'use strict'; 29 | this._ownerElement = ownerElement; 30 | return new Proxy(this, DOMStringMapHandler); 31 | }; 32 | -------------------------------------------------------------------------------- /src/DOMTokenList.js: -------------------------------------------------------------------------------- 1 | const afterChanges = dtl => { 2 | const el = dtl._ownerElement; 3 | const attr = el.getAttributeNode('class'); 4 | if (attr) { 5 | if (attr.value !== dtl.value) { 6 | attr.value = dtl.value; 7 | } 8 | } else if (dtl.value) { 9 | el.setAttribute('class', dtl.value); 10 | } 11 | }; 12 | 13 | // interface DOMTokenList // https://dom.spec.whatwg.org/#interface-domtokenlist 14 | module.exports = class DOMTokenList extends Array { 15 | 16 | constructor(ownerElement) { 17 | super(); 18 | this._ownerElement = ownerElement; 19 | } 20 | 21 | item(i) { 22 | return this[i]; 23 | } 24 | 25 | contains(token) { 26 | return this.includes(token); 27 | } 28 | 29 | add(...tokens) { 30 | this.splice(0, this.length, ...new Set(this.concat(tokens))); 31 | afterChanges(this); 32 | } 33 | 34 | remove(...tokens) { 35 | this.push(...this.splice(0, this.length) 36 | .filter(token => !tokens.includes(token))); 37 | afterChanges(this); 38 | } 39 | 40 | replace(token, newToken) { 41 | const i = this.indexOf(token); 42 | if (i < 0) this.add(newToken); 43 | else this[i] = newToken; 44 | afterChanges(this); 45 | } 46 | 47 | toggle(token, force) { 48 | let result = false; 49 | if (this.contains(token)) { 50 | if (force) result = true; 51 | else this.remove(token); 52 | } else { 53 | if (arguments.length < 2 || force) { 54 | result = true; 55 | this.add(token); 56 | } 57 | } 58 | return result; 59 | } 60 | 61 | get value() { 62 | return this.join(' '); 63 | } 64 | 65 | set value(className) { 66 | this.splice(0, this.length); 67 | this.add(...String(className || '').trim().split(/\s+/)); 68 | afterChanges(this); 69 | } 70 | 71 | }; 72 | -------------------------------------------------------------------------------- /src/Document.js: -------------------------------------------------------------------------------- 1 | const CustomElementRegistry = require('./CustomElementRegistry'); 2 | const Event = require('./Event'); 3 | const CustomEvent = require('./CustomEvent'); 4 | const Node = require('./Node'); 5 | const DocumentType = require('./DocumentType'); 6 | const Attr = require('./Attr'); 7 | const CSSStyleDeclaration = require('./CSSStyleDeclaration'); 8 | const Comment = require('./Comment'); 9 | const DocumentFragment = require('./DocumentFragment'); 10 | const HTMLElement = require('./HTMLElement'); 11 | const HTMLHtmlElement = require('./HTMLHtmlElement'); 12 | const HTMLStyleElement = require('./HTMLStyleElement'); 13 | const HTMLTemplateElement = require('./HTMLTemplateElement'); 14 | const HTMLTextAreaElement = require('./HTMLTextAreaElement'); 15 | const Range = require('./Range'); 16 | const Text = require('./Text'); 17 | const TreeWalker = require('./TreeWalker'); 18 | 19 | const headTag = el => el.nodeName === 'head'; 20 | const bodyTag = el => el.nodeName === 'body'; 21 | 22 | const createElement = (self, name, is) => { 23 | const Class = self.customElements.get(is) || HTMLElement; 24 | return new Class(self, name); 25 | }; 26 | 27 | const getFoundOrNull = result => { 28 | if (result) { 29 | const el = findById.found; 30 | findById.found = null; 31 | return el; 32 | } else { 33 | return null; 34 | } 35 | }; 36 | 37 | function findById(child) {'use strict'; 38 | return child.id === this ? 39 | !!(findById.found = child) : 40 | child.children.some(findById, this); 41 | } 42 | 43 | // interface Document // https://dom.spec.whatwg.org/#document 44 | module.exports = class Document extends Node { 45 | 46 | constructor(customElements = new CustomElementRegistry()) { 47 | super(null); 48 | this.nodeType = Node.DOCUMENT_NODE; 49 | this.nodeName = '#document'; 50 | this.appendChild(new DocumentType()); 51 | this.documentElement = new HTMLHtmlElement(this, 'html'); 52 | this.appendChild(this.documentElement); 53 | this.customElements = customElements; 54 | Object.freeze(this.childNodes); 55 | } 56 | 57 | createAttribute(name) { 58 | const attr = new Attr( 59 | {ownerDocument: this}, 60 | name, 61 | name === 'style' ? 62 | new CSSStyleDeclaration() : 63 | null 64 | ); 65 | attr.ownerElement = null; 66 | return attr; 67 | } 68 | 69 | createAttributeNS(_, name) { 70 | return this.createAttribute(name); 71 | } 72 | 73 | createComment(comment) { 74 | return new Comment(this, comment); 75 | } 76 | 77 | createDocumentFragment() { 78 | return new DocumentFragment(this); 79 | } 80 | 81 | createElement(name, options) { 82 | switch (name) { 83 | case 'style': 84 | return new HTMLStyleElement(this, name); 85 | case 'template': 86 | return new HTMLTemplateElement(this, name); 87 | case 'textarea': 88 | return new HTMLTextAreaElement(this, name); 89 | case 'canvas': 90 | case 'img': 91 | try { 92 | const file = name === 'img' ? './HTMLImageElement' : './HTMLCanvasElement'; 93 | const Constructor = require(file); 94 | return new Constructor(this); 95 | } 96 | catch (o_O) {} 97 | default: 98 | const extending = 1 < arguments.length && 'is' in options; 99 | const el = createElement(this, name, extending ? options.is : name); 100 | if (extending) 101 | el.setAttribute('is', options.is); 102 | return el; 103 | } 104 | } 105 | 106 | createElementNS(ns, name) { 107 | if (ns === 'http://www.w3.org/1999/xhtml') { 108 | return this.createElement(name); 109 | } 110 | return new HTMLElement(this, name + ':' + ns); 111 | } 112 | 113 | createEvent(name) { 114 | switch (name) { 115 | case 'Event': 116 | return new Event(); 117 | case 'CustomEvent': 118 | return new CustomEvent(); 119 | default: 120 | throw new Error(name + ' not implemented'); 121 | } 122 | } 123 | 124 | createRange() { 125 | return new Range; 126 | } 127 | 128 | createTextNode(text) { 129 | return new Text(this, text); 130 | } 131 | 132 | createTreeWalker(root, whatToShow) { 133 | return new TreeWalker(root, whatToShow); 134 | } 135 | 136 | getElementsByTagName(name) { 137 | const html = this.documentElement; 138 | return /html/i.test(name) ? 139 | [html] : 140 | (name === '*' ? [html] : []).concat(html.getElementsByTagName(name)); 141 | } 142 | 143 | getElementsByClassName(name) { 144 | const html = this.documentElement; 145 | return (html.classList.contains(name) ? [html] : []) 146 | .concat(html.getElementsByClassName(name)); 147 | } 148 | 149 | importNode(node) { 150 | return node.cloneNode(!!arguments[1]); 151 | } 152 | 153 | toString() { 154 | return this.childNodes[0] + this.documentElement.outerHTML; 155 | } 156 | 157 | get defaultView() { 158 | return global; 159 | } 160 | 161 | get head() { 162 | const html = this.documentElement; 163 | return this.documentElement.childNodes.find(headTag) || 164 | html.insertBefore(this.createElement('head'), this.body); 165 | } 166 | 167 | get body() { 168 | const html = this.documentElement; 169 | return html.childNodes.find(bodyTag) || 170 | html.appendChild(this.createElement('body')); 171 | } 172 | 173 | // interface NonElementParentNode // https://dom.spec.whatwg.org/#nonelementparentnode 174 | getElementById(id) { 175 | const html = this.documentElement; 176 | return html.id === id ? html : getFoundOrNull(html.children.some(findById, id)); 177 | } 178 | 179 | // interface ParentNode @ https://dom.spec.whatwg.org/#parentnode 180 | get children() { 181 | return [this.documentElement]; 182 | } 183 | 184 | get firstElementChild() { 185 | return this.documentElement; 186 | } 187 | 188 | get lastElementChild() { 189 | return this.documentElement; 190 | } 191 | 192 | get childElementCount() { 193 | return 1; 194 | } 195 | 196 | prepend() { throw new Error('Only one element on document allowed.'); } 197 | append() { this.prepend(); } 198 | 199 | querySelector(css) { 200 | return this.documentElement.querySelector(css); 201 | } 202 | 203 | querySelectorAll(css) { 204 | return this.documentElement.querySelectorAll(css); 205 | } 206 | 207 | }; 208 | -------------------------------------------------------------------------------- /src/DocumentFragment.js: -------------------------------------------------------------------------------- 1 | require('@webreflection/interface'); 2 | 3 | const Node = require('./Node'); 4 | const ParentNode = require('./ParentNode'); 5 | const ChildNode = require('./ChildNode'); 6 | const Extend = Node.implements(ParentNode, ChildNode); 7 | 8 | // interface DocumentFragment // https://dom.spec.whatwg.org/#documentfragment 9 | module.exports = class DocumentFragment extends Extend { 10 | 11 | constructor(ownerDocument) { 12 | super(ownerDocument); 13 | this.nodeType = Node.DOCUMENT_FRAGMENT_NODE; 14 | this.nodeName = '#document-fragment'; 15 | } 16 | 17 | }; 18 | -------------------------------------------------------------------------------- /src/DocumentType.js: -------------------------------------------------------------------------------- 1 | const Node = require('./Node'); 2 | 3 | // interface DocumentType // https://dom.spec.whatwg.org/#documenttype 4 | module.exports = class DocumentType extends Node { 5 | constructor(ownerDocument) { 6 | super(ownerDocument); 7 | this.nodeType = Node.DOCUMENT_TYPE_NODE; 8 | this.name = 'html'; 9 | } 10 | 11 | toString() { 12 | return ''; 13 | } 14 | 15 | }; 16 | -------------------------------------------------------------------------------- /src/Element.js: -------------------------------------------------------------------------------- 1 | require('@webreflection/interface'); 2 | 3 | const CSS_SPLITTER = /\s*,\s*/; 4 | const AVOID_ESCAPING = /^(?:script|style)$/i; 5 | const {VOID_ELEMENT, voidSanitizer} = require('./utils'); 6 | 7 | const escape = require('html-escaper').escape; 8 | const Parser = require('htmlparser2').Parser; 9 | const findName = (Class, registry) => { 10 | for (let key in registry) 11 | if (registry[key] === Class) 12 | return key; 13 | }; 14 | const parseInto = (node, html) => { 15 | const stack = []; 16 | const document = node.ownerDocument; 17 | const content = new Parser({ 18 | onopentagname(name) { 19 | switch (name) { 20 | /* TODO this actually breaks heresy-ssr 21 | case 'html': 22 | node = document.documentElement; 23 | node.childNodes = []; 24 | break; 25 | case 'head': 26 | case 'body': 27 | node.replaceChild(document.createElement(name), document[name]); 28 | node = document[name]; 29 | break; 30 | */ 31 | default: 32 | const child = document.createElement(name); 33 | if (child.isCustomElement) { 34 | stack.push(node, child); 35 | node = child; 36 | } 37 | else 38 | node = node.appendChild(child); 39 | break; 40 | } 41 | }, 42 | onattribute(name, value) { 43 | node.setAttribute(name, value); 44 | }, 45 | oncomment(data) { 46 | node.appendChild(document.createComment(data)); 47 | }, 48 | ontext(text) { 49 | node.appendChild(document.createTextNode(text)); 50 | }, 51 | onclosetag(name) { 52 | switch (name) { 53 | default: 54 | while (stack.length) 55 | stack.shift().appendChild(stack.shift()); 56 | /* istanbul ignore else */ 57 | if (node.nodeName === name) 58 | node = node.parentNode; 59 | break; 60 | } 61 | } 62 | }, { 63 | decodeEntities: true, 64 | xmlMode: true 65 | }); 66 | content.write(voidSanitizer(html)); 67 | content.end(); 68 | }; 69 | 70 | const utils = require('./utils'); 71 | const ParentNode = require('./ParentNode'); 72 | const ChildNode = require('./ChildNode'); 73 | const NamedNodeMap = require('./NamedNodeMap'); 74 | const Node = require('./Node'); 75 | const DOMTokenList = require('./DOMTokenList'); 76 | 77 | function matchesBySelector(css) { 78 | switch (css[0]) { 79 | case '#': return this.id === css.slice(1); 80 | case '.': return this.classList.contains(css.slice(1)); 81 | default: return css === this.nodeName; 82 | } 83 | } 84 | 85 | const specialAttribute = (owner, attr) => { 86 | switch (attr.name) { 87 | case 'class': 88 | owner.classList.value = attr.value; 89 | return true; 90 | } 91 | return false; 92 | }; 93 | 94 | const stringifiedNode = el => { 95 | switch (el.nodeType) { 96 | case Node.ELEMENT_NODE: 97 | return ('<' + el.nodeName).concat( 98 | el.attributes.map(stringifiedNode).join(''), 99 | VOID_ELEMENT.test(el.nodeName) ? 100 | ' />' : 101 | ('>' + ( 102 | AVOID_ESCAPING.test(el.nodeName) ? 103 | el.textContent : 104 | el.childNodes.map(stringifiedNode).join('') 105 | ) + '') 106 | ); 107 | case Node.ATTRIBUTE_NODE: 108 | return el.name === 'style' && !el.value ? '' : ( 109 | typeof el.value === 'boolean' || el.value == null ? 110 | (el.value ? (' ' + el.name) : '') : 111 | (' ' + el.name + '="' + escape(el.value) + '"') 112 | ); 113 | case Node.TEXT_NODE: 114 | return escape(el.data); 115 | case Node.COMMENT_NODE: 116 | return ''; 117 | } 118 | }; 119 | 120 | // interface Element // https://dom.spec.whatwg.org/#interface-element 121 | class Element extends Node.implements(ParentNode, ChildNode) { 122 | constructor(ownerDocument, name) { 123 | super(ownerDocument); 124 | this.attributes = new NamedNodeMap(this); 125 | this.nodeType = Node.ELEMENT_NODE; 126 | this.nodeName = name || findName( 127 | this.constructor, 128 | this.ownerDocument.customElements._registry 129 | ); 130 | this.classList = new DOMTokenList(this); 131 | } 132 | 133 | // it doesn't actually really work as expected 134 | // it simply provides shadowRoot as the element itself 135 | attachShadow(init) { 136 | switch (init.mode) { 137 | case 'open': return (this.shadowRoot = this); 138 | case 'closed': return this; 139 | } 140 | throw new Error('element.attachShadow({mode: "open" | "closed"})'); 141 | } 142 | 143 | getAttribute(name) { 144 | const attr = this.getAttributeNode(name); 145 | return attr && attr.value; 146 | } 147 | 148 | getAttributeNames() { 149 | return this.attributes.map(attr => attr.name); 150 | } 151 | 152 | getAttributeNode(name) { 153 | return this.attributes.find(attr => attr.name === name) || null; 154 | } 155 | 156 | getElementsByClassName(name) { 157 | const list = []; 158 | for (let i = 0; i < this.children.length; i++) { 159 | let el = this.children[i]; 160 | if (el.classList.contains(name)) list.push(el); 161 | list.push(...el.getElementsByClassName(name)); 162 | } 163 | return list; 164 | } 165 | 166 | getElementsByTagName(name) { 167 | const list = []; 168 | for (let i = 0; i < this.children.length; i++) { 169 | let el = this.children[i]; 170 | if (name === '*' || el.nodeName === name) list.push(el); 171 | list.push(...el.getElementsByTagName(name)); 172 | } 173 | return list; 174 | } 175 | 176 | hasAttribute(name) { 177 | return this.attributes.some(attr => attr.name === name); 178 | } 179 | 180 | hasAttributes() { 181 | return 0 < this.attributes.length; 182 | } 183 | 184 | closest(css) { 185 | let el = this; 186 | do { 187 | if (el.matches(css)) return el; 188 | } while ((el = el.parentNode) && el.nodeType === Node.ELEMENT_NODE); 189 | return null; 190 | } 191 | 192 | matches(css) { 193 | return css.split(CSS_SPLITTER).some(matchesBySelector, this); 194 | } 195 | 196 | removeAttribute(name) { 197 | const attr = this.getAttributeNode(name); 198 | if (attr) this.removeAttributeNode(attr); 199 | } 200 | 201 | setAttribute(name, value) { 202 | const attr = this.getAttributeNode(name); 203 | if (attr) { 204 | attr.value = value; 205 | } else { 206 | const attr = this.ownerDocument.createAttribute(name); 207 | attr.ownerElement = this; 208 | this.attributes.push(attr); 209 | this.attributes[name] = attr; 210 | attr.value = value; 211 | } 212 | } 213 | 214 | removeAttributeNode(attr) { 215 | const i = this.attributes.indexOf(attr); 216 | if (i < 0) throw new Error('unable to remove ' + attr); 217 | this.attributes.splice(i, 1); 218 | attr.value = null; 219 | delete this.attributes[attr.name]; 220 | specialAttribute(this, attr); 221 | } 222 | 223 | setAttributeNode(attr) { 224 | const name = attr.name; 225 | const old = this.getAttributeNode(name); 226 | if (old === attr) return attr; 227 | else { 228 | if (attr.ownerElement) { 229 | if (attr.ownerElement !== this) { 230 | throw new Error('The attribute is already used in other nodes.'); 231 | } 232 | } 233 | else attr.ownerElement = this; 234 | this.attributes[name] = attr; 235 | if (old) { 236 | this.attributes.splice(this.attributes.indexOf(old), 1, attr); 237 | if (!specialAttribute(this, attr)) 238 | utils.notifyAttributeChanged(this, name, old.value, attr.value); 239 | return old; 240 | } else { 241 | this.attributes.push(attr); 242 | if (!specialAttribute(this, attr)) 243 | utils.notifyAttributeChanged(this, name, null, attr.value); 244 | return null; 245 | } 246 | } 247 | } 248 | 249 | setAttributeNodeNS(attr) { 250 | return this.setAttributeNode(attr); 251 | } 252 | 253 | get id() { 254 | return this.getAttribute('id') || ''; 255 | } 256 | 257 | set id(value) { 258 | this.setAttribute('id', value); 259 | } 260 | 261 | get className() { 262 | return this.classList.value; 263 | } 264 | 265 | set className(value) { 266 | this.classList.value = value; 267 | } 268 | 269 | get innerHTML() { 270 | return this.childNodes.map(stringifiedNode).join(''); 271 | } 272 | 273 | set innerHTML(html) { 274 | this.textContent = ''; 275 | parseInto(this, html); 276 | } 277 | 278 | get nextElementSibling() { 279 | const children = this.parentNode.children; 280 | let i = children.indexOf(this); 281 | return ++i < children.length ? children[i] : null; 282 | } 283 | 284 | get previousElementSibling() { 285 | const children = this.parentNode.children; 286 | let i = children.indexOf(this); 287 | return --i < 0 ? null : children[i]; 288 | } 289 | 290 | get outerHTML() { 291 | return stringifiedNode(this); 292 | } 293 | 294 | get tagName() { 295 | return this.nodeName 296 | } 297 | 298 | }; 299 | 300 | module.exports = Element; 301 | -------------------------------------------------------------------------------- /src/Event.js: -------------------------------------------------------------------------------- 1 | const getTime = () => { 2 | const time = process.hrtime(); 3 | return time[0] * 1000000 + time[1] / 1000; 4 | }; 5 | 6 | // interface Event // https://dom.spec.whatwg.org/#event 7 | class Event { 8 | 9 | constructor(type, eventInitDict = { 10 | bubbles: false, 11 | cancelable: false, 12 | composed: false 13 | }) { 14 | if (type) this.initEvent( 15 | type, 16 | eventInitDict.bubbles, 17 | eventInitDict.cancelable 18 | ); 19 | this.composed = eventInitDict.composed; 20 | this.isTrusted = false; 21 | this.defaultPrevented = false; 22 | this.cancelBubble = false; 23 | this.cancelImmediateBubble = false; 24 | this.eventPhase = Event.NONE; 25 | this.timeStamp = getTime(); 26 | } 27 | 28 | initEvent(type, bubbles, cancelable) { 29 | this.type = type; 30 | this.bubbles = bubbles; 31 | this.cancelable = cancelable; 32 | } 33 | 34 | stopPropagation() { 35 | this.cancelBubble = true; 36 | } 37 | 38 | stopImmediatePropagation() { 39 | this.cancelBubble = true; 40 | this.cancelImmediateBubble = true; 41 | } 42 | 43 | preventDefault() { 44 | this.defaultPrevented = true; 45 | } 46 | 47 | } 48 | 49 | Event.NONE = 0; 50 | Event.CAPTURING_PHASE = 1; 51 | Event.AT_TARGET = 2; 52 | Event.BUBBLING_PHASE = 3; 53 | 54 | module.exports = Event; 55 | -------------------------------------------------------------------------------- /src/EventTarget.js: -------------------------------------------------------------------------------- 1 | const Event = require('./Event'); 2 | 3 | const {defineProperty} = Object; 4 | 5 | const crawlUp = node => 6 | node.parentNode || 7 | (node.nodeType === node.DOCUMENT_NODE ? node.defaultView : null); 8 | 9 | const getHandler = (self, handler) => 10 | handler.handleEvent ? 11 | e => handler.handleEvent(e) : 12 | e => handler.call(self, e); 13 | 14 | const getOnce = (self, type, handler, options) => 15 | e => { 16 | self.removeEventListener(type, handler, options); 17 | getHandler(self, handler)(e); 18 | }; 19 | 20 | // interface EventTarget // https://dom.spec.whatwg.org/#eventtarget 21 | module.exports = class EventTarget { 22 | 23 | static init(self) { 24 | self._eventTarget = Object.create(null); 25 | if (self instanceof EventTarget) 26 | return; 27 | const et = EventTarget.prototype; 28 | self.addEventListener = et.addEventListener; 29 | self.removeEventListener = et.removeEventListener; 30 | self.dispatchEvent = et.dispatchEvent; 31 | } 32 | 33 | constructor() { 34 | EventTarget.init(this); 35 | } 36 | 37 | addEventListener(type, handler, options) { 38 | const listener = this._eventTarget[type] || (this._eventTarget[type] = { 39 | handlers: [], 40 | callbacks: [] 41 | }); 42 | const i = listener.handlers.indexOf(handler); 43 | if (i < 0) { 44 | listener.callbacks[listener.handlers.push(handler) - 1] = 45 | options && options.once ? 46 | getOnce(this, type, handler, options) : 47 | getHandler(this, handler); 48 | } 49 | } 50 | 51 | removeEventListener(type, handler, options) { 52 | const listener = this._eventTarget[type]; 53 | if (listener) { 54 | const i = listener.handlers.indexOf(handler); 55 | if (-1 < i) { 56 | listener.handlers.splice(i, 1); 57 | listener.callbacks.splice(i, 1); 58 | if (listener.handlers.length < 1) { 59 | delete this._eventTarget[type]; 60 | } 61 | } 62 | } 63 | } 64 | 65 | dispatchEvent(event) { 66 | const type = event.type; 67 | let node = this; 68 | /* istanbul ignore next */ 69 | if (!event.target) defineProperty(event, 'target', {get: () => node}); 70 | /* istanbul ignore next */ 71 | if (!event.currentTarget) defineProperty(event, 'currentTarget', {get: () => node}); 72 | /* istanbul ignore next */ 73 | if (!event.eventPhase) defineProperty(event, 'eventPhase', {configurable: true, value: Event.AT_TARGET}); 74 | do { 75 | if (type in node._eventTarget) { 76 | [...node._eventTarget[type].callbacks].some( 77 | cb => (cb(event), event.cancelImmediateBubble) 78 | ); 79 | } 80 | defineProperty(event, 'eventPhase', {configurable: true, value: Event.BUBBLING_PHASE}); 81 | } while (event.bubbles && !event.cancelBubble && (node = crawlUp(node))); 82 | return !event.defaultPrevented; 83 | } 84 | 85 | }; 86 | -------------------------------------------------------------------------------- /src/HTMLCanvasElement.js: -------------------------------------------------------------------------------- 1 | const HTMLElement = require('./HTMLElement'); 2 | const {createCanvas} = require('canvas'); 3 | 4 | const canvas = new WeakMap; 5 | 6 | class HTMLCanvasElement extends HTMLElement { 7 | constructor(ownerDocument) { 8 | super(ownerDocument, 'canvas'); 9 | canvas.set(this, createCanvas(300, 150)); 10 | } 11 | get width() { 12 | return canvas.get(this).width; 13 | } 14 | set width(value) { 15 | this.setAttribute('width', value); 16 | canvas.get(this).width = value; 17 | } 18 | get height() { 19 | return canvas.get(this).height; 20 | } 21 | set height(value) { 22 | this.setAttribute('height', value); 23 | canvas.get(this).height = value; 24 | } 25 | getContext(type) { 26 | return canvas.get(this).getContext(type); 27 | } 28 | toDataURL(...args) { 29 | return canvas.get(this).toDataURL(...args); 30 | } 31 | } 32 | 33 | module.exports = HTMLCanvasElement; 34 | -------------------------------------------------------------------------------- /src/HTMLElement.js: -------------------------------------------------------------------------------- 1 | const escape = require('html-escaper').escape; 2 | 3 | const Attr = require('./Attr'); 4 | const Element = require('./Element'); 5 | const DOMStringMap = require('./DOMStringMap'); 6 | const CSSStyleDeclaration = require('./CSSStyleDeclaration'); 7 | 8 | const {setPrototypeOf} = Object; 9 | 10 | // interface HTMLElement // https://html.spec.whatwg.org/multipage/dom.html#htmlelement 11 | class HTMLElement extends Element { 12 | constructor(ownerDocument, name) { 13 | super(ownerDocument, name); 14 | this.dataset = new DOMStringMap(this); 15 | this.style = new CSSStyleDeclaration(); 16 | const style = new Attr(this, 'style', this.style); 17 | this.attributes.push(style); 18 | this.attributes.style = style; 19 | this.__isCE = -1; 20 | } 21 | get isCustomElement() { 22 | if (this.__isCE < 0) { 23 | this.__isCE = 0; 24 | const is = this.getAttribute('is') || this.nodeName; 25 | const ceName = -1 < is.indexOf('-'); 26 | if (ceName) { 27 | const Class = this.ownerDocument.customElements.get(is); 28 | if (Class) { 29 | this.__isCE = 1; 30 | setPrototypeOf(this, Class.prototype); 31 | } 32 | } 33 | } 34 | return this.__isCE === 1; 35 | } 36 | } 37 | 38 | [ 39 | 'click', 40 | 'focus', 41 | 'blur' 42 | ].forEach(type => { 43 | Object.defineProperty(HTMLElement.prototype, type, { 44 | configurable: true, 45 | value: function () { 46 | const {ownerDocument} = this; 47 | ownerDocument.activeElement = type === 'blur' ? null : this; 48 | const event = ownerDocument.createEvent('Event'); 49 | event.initEvent(type, true, true); 50 | this.dispatchEvent(event); 51 | } 52 | }); 53 | }); 54 | 55 | [ 56 | 'title', 57 | 'lang', 58 | 'translate', 59 | 'dir', 60 | 'hidden', 61 | 'tabIndex', 62 | 'accessKey', 63 | 'draggable', 64 | 'spellcheck', 65 | 'contentEditable' 66 | ].forEach(name => { 67 | const lowName = name; 68 | Object.defineProperty(HTMLElement.prototype, name, { 69 | configurable: true, 70 | get() { return this.getAttribute(lowName); }, 71 | set(value) { this.setAttribute(lowName, value); } 72 | }); 73 | }); 74 | 75 | // HTMLElement implements GlobalEventHandlers; 76 | // HTMLElement implements DocumentAndElementEventHandlers; 77 | 78 | [ 79 | 'onabort', 80 | 'onblur', 81 | 'oncancel', 82 | 'oncanplay', 83 | 'oncanplaythrough', 84 | 'onchange', 85 | 'onclick', 86 | 'onclose', 87 | 'oncontextmenu', 88 | 'oncuechange', 89 | 'ondblclick', 90 | 'ondrag', 91 | 'ondragend', 92 | 'ondragenter', 93 | 'ondragleave', 94 | 'ondragover', 95 | 'ondragstart', 96 | 'ondrop', 97 | 'ondurationchange', 98 | 'onemptied', 99 | 'onended', 100 | 'onerror', 101 | 'onfocus', 102 | 'oninput', 103 | 'oninvalid', 104 | 'onkeydown', 105 | 'onkeypress', 106 | 'onkeyup', 107 | 'onload', 108 | 'onloadeddata', 109 | 'onloadedmetadata', 110 | 'onloadstart', 111 | 'onmousedown', 112 | 'onmouseenter', 113 | 'onmouseleave', 114 | 'onmousemove', 115 | 'onmouseout', 116 | 'onmouseover', 117 | 'onmouseup', 118 | 'onmousewheel', 119 | 'onpause', 120 | 'onplay', 121 | 'onplaying', 122 | 'onprogress', 123 | 'onratechange', 124 | 'onreset', 125 | 'onresize', 126 | 'onscroll', 127 | 'onseeked', 128 | 'onseeking', 129 | 'onselect', 130 | 'onshow', 131 | 'onstalled', 132 | 'onsubmit', 133 | 'onsuspend', 134 | 'ontimeupdate', 135 | 'ontoggle', 136 | 'onvolumechange', 137 | 'onwaiting', 138 | 'onauxclick', 139 | 'ongotpointercapture', 140 | 'onlostpointercapture', 141 | 'onpointercancel', 142 | 'onpointerdown', 143 | 'onpointerenter', 144 | 'onpointerleave', 145 | 'onpointermove', 146 | 'onpointerout', 147 | 'onpointerover', 148 | 'onpointerup' 149 | ].forEach(ontype => { 150 | let _value = null; 151 | const type = ontype.slice(2); 152 | Object.defineProperty(HTMLElement.prototype, ontype, { 153 | configurable: true, 154 | get() { 155 | return _value; 156 | }, 157 | set(value) { 158 | if (!value) { 159 | if (_value) { 160 | value = _value; 161 | _value = null; 162 | this.removeEventListener(type, value); 163 | } 164 | this.removeAttribute(ontype); 165 | } else { 166 | _value = value; 167 | this.addEventListener(type, value); 168 | this.setAttribute(ontype, 'return (' + escape( 169 | JS_SHORTCUT.test(value) && !JS_FUNCTION.test(value) ? 170 | ('function ' + value) : 171 | ('' + value) 172 | ) + ').call(this, event)'); 173 | } 174 | } 175 | }); 176 | }); 177 | 178 | // helpers 179 | const JS_SHORTCUT = /^[a-z$_]\S*?\(/; 180 | const JS_FUNCTION = /^function\S*?\(/; 181 | 182 | module.exports = HTMLElement; 183 | -------------------------------------------------------------------------------- /src/HTMLHtmlElement.js: -------------------------------------------------------------------------------- 1 | const HTMLElement = require('./HTMLElement'); 2 | 3 | // interface HTMLHtmlElement // https://html.spec.whatwg.org/multipage/semantics.html#htmlhtmlelement 4 | module.exports = class HTMLHtmlElement extends HTMLElement { 5 | 6 | get innerHTML() { 7 | const document = this.ownerDocument; 8 | return document.head.outerHTML + document.body.outerHTML; 9 | } 10 | 11 | set innerHTML(html) { 12 | super.innerHTML = html; 13 | } 14 | 15 | }; 16 | -------------------------------------------------------------------------------- /src/HTMLImageElement.js: -------------------------------------------------------------------------------- 1 | const {Image} = require('canvas'); 2 | 3 | const HTMLElement = require('./HTMLElement'); 4 | const {image, descriptors} = require('./ImagePrototype'); 5 | 6 | class HTMLImageElement extends HTMLElement { 7 | constructor(ownerDocument) { 8 | super(ownerDocument, 'img'); 9 | image.set(this, new Image()); 10 | } 11 | get onload() { 12 | return image.get(this).onload; 13 | } 14 | set onload(callback) { 15 | image.get(this).onload = callback; 16 | } 17 | get onerror() { 18 | return image.get(this).onerror; 19 | } 20 | set onerror(callback) { 21 | image.get(this).onerror = callback; 22 | } 23 | } 24 | 25 | Object.defineProperties(HTMLImageElement.prototype, descriptors); 26 | 27 | module.exports = HTMLImageElement; 28 | -------------------------------------------------------------------------------- /src/HTMLNoComments.js: -------------------------------------------------------------------------------- 1 | const HTMLElement = require('./HTMLElement'); 2 | const {COMMENT_NODE} = HTMLElement; 3 | 4 | module.exports = class HTMLNoComments extends HTMLElement { 5 | 6 | appendChild(node) { 7 | super.appendChild( 8 | node.nodeType === COMMENT_NODE ? 9 | this.ownerDocument.createTextNode(``) : 10 | node 11 | ); 12 | } 13 | 14 | }; 15 | -------------------------------------------------------------------------------- /src/HTMLStyleElement.js: -------------------------------------------------------------------------------- 1 | const HTMLNoComments = require('./HTMLNoComments'); 2 | module.exports = class HTMLStyleElement extends HTMLNoComments {}; 3 | -------------------------------------------------------------------------------- /src/HTMLTemplateElement.js: -------------------------------------------------------------------------------- 1 | const HTMLElement = require('./HTMLElement'); 2 | 3 | // interface HTMLTemplateElement // https://html.spec.whatwg.org/multipage/scripting.html#htmltemplateelement 4 | module.exports = class HTMLTemplateElement extends HTMLElement { 5 | 6 | get content() { 7 | const fragment = this.ownerDocument.createDocumentFragment(); 8 | fragment.childNodes = this.childNodes; 9 | return fragment; 10 | } 11 | 12 | }; 13 | -------------------------------------------------------------------------------- /src/HTMLTextAreaElement.js: -------------------------------------------------------------------------------- 1 | const HTMLNoComments = require('./HTMLNoComments'); 2 | module.exports = class HTMLTextAreaElement extends HTMLNoComments {}; 3 | -------------------------------------------------------------------------------- /src/HTMLUnknownElement.js: -------------------------------------------------------------------------------- 1 | const HTMLElement = require('./HTMLElement'); 2 | 3 | // interface HTMLElement // https://developer.mozilla.org/en-US/docs/Web/API/HTMLUnknownElement 4 | class HTMLUnknowElement extends HTMLElement { 5 | 6 | } 7 | 8 | module.exports = HTMLUnknowElement; 9 | -------------------------------------------------------------------------------- /src/Image.js: -------------------------------------------------------------------------------- 1 | const HTMLElement = require('./HTMLElement'); 2 | const {image, descriptors} = require('./ImagePrototype'); 3 | 4 | class Image extends HTMLElement { 5 | constructor(ownerDocument) { 6 | super(ownerDocument, 'img'); 7 | image.set(this, {}); 8 | } 9 | } 10 | 11 | Object.defineProperties(Image.prototype, descriptors); 12 | 13 | module.exports = Image; 14 | -------------------------------------------------------------------------------- /src/ImageFactory.js: -------------------------------------------------------------------------------- 1 | const HTMLElement = require('./HTMLElement'); 2 | const Image = require('./Image'); 3 | 4 | module.exports = function ImageFactory(document, ...size) { 5 | const [width, height] = size; 6 | let img = document.createElement('img'); 7 | if (img.constructor === HTMLElement) 8 | img = new Image(document); 9 | switch (size.length) { 10 | case 1: 11 | img.width = width; 12 | img.height = width; 13 | return img; 14 | case 2: 15 | img.width = width; 16 | img.height = height; 17 | return img; 18 | } 19 | return img; 20 | }; 21 | -------------------------------------------------------------------------------- /src/ImagePrototype.js: -------------------------------------------------------------------------------- 1 | const image = new WeakMap; 2 | 3 | module.exports = { 4 | image, 5 | descriptors: [ 6 | 'alt', 7 | 'height', 8 | 'src', 9 | 'title', 10 | 'width' 11 | ].reduce((descriptors, key) => { 12 | descriptors[key] = { 13 | configurable: true, 14 | get() { return image.get(this)[key]; }, 15 | set(value) { 16 | this.setAttribute(key, value); 17 | image.get(this)[key] = value; 18 | } 19 | }; 20 | return descriptors; 21 | }, {}) 22 | }; 23 | -------------------------------------------------------------------------------- /src/NamedNodeMap.js: -------------------------------------------------------------------------------- 1 | module.exports = class NamedNodeMap extends Array { 2 | constructor(ownerElement) { 3 | super(); 4 | this.ownerElement = ownerElement; 5 | } 6 | getNamedItem(name) { 7 | return this.ownerElement.getAttributeNode(name); 8 | } 9 | setNamedItem(attr) { 10 | return this.ownerElement.setAttributeNode(attr); 11 | } 12 | removeNamedItem(name) { 13 | return this.ownerElement.removeAttribute(name); 14 | } 15 | item(index) { 16 | return this[index] || null; 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /src/Node.js: -------------------------------------------------------------------------------- 1 | const utils = require('./utils'); 2 | const EventTarget = require('./EventTarget'); 3 | 4 | const nullParent = node => resetParent(null, node); 5 | 6 | const removeFromParent = (parentNode, child) => { 7 | const cn = parentNode.childNodes; 8 | cn.splice(cn.indexOf(child), 1); 9 | utils.disconnect(parentNode, child); 10 | }; 11 | 12 | const resetParent = (parentNode, child) => { 13 | if (child.parentNode) { 14 | removeFromParent(child.parentNode, child); 15 | } 16 | if ((child.parentNode = parentNode)) { 17 | utils.connect(parentNode, child); 18 | } 19 | }; 20 | 21 | const stringifiedContent = el => { 22 | switch(el.nodeType) { 23 | case Node.ELEMENT_NODE: 24 | case Node.DOCUMENT_FRAGMENT_NODE: return el.textContent; 25 | case Node.TEXT_NODE: return el.data; 26 | default: return ''; 27 | } 28 | }; 29 | 30 | // interface Node : EventTarget // https://dom.spec.whatwg.org/#node 31 | class Node extends EventTarget { 32 | 33 | constructor(ownerDocument) { 34 | super(); 35 | this.ownerDocument = ownerDocument || global.document; 36 | this.childNodes = []; 37 | } 38 | 39 | appendChild(node) { 40 | if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE) { 41 | node.childNodes.splice(0).forEach(this.appendChild, this); 42 | } else { 43 | const i = this.childNodes.indexOf(node); 44 | if (-1 < i) this.childNodes.splice(i, 1); 45 | this.childNodes.push(node); 46 | if (i < 0) resetParent(this, node); 47 | } 48 | return node; 49 | } 50 | 51 | cloneNode(deep) { 52 | let node; 53 | const document = this.ownerDocument; 54 | switch (this.nodeType) { 55 | case Node.ATTRIBUTE_NODE: 56 | node = document.createAttribute(this.name); 57 | node.value = this.value; 58 | return node; 59 | case Node.TEXT_NODE: 60 | return document.createTextNode(this.data); 61 | case Node.COMMENT_NODE: 62 | return document.createComment(this.data); 63 | case Node.ELEMENT_NODE: 64 | node = document.createElement(this.nodeName); 65 | // if populated during constructor discard all content 66 | if (this.nodeName in document.customElements._registry) { 67 | node.childNodes.forEach(removeChild, node); 68 | node.attributes.forEach(removeAttribute, node); 69 | } 70 | this.attributes.forEach(a => node.setAttribute(a.name, a.value)); 71 | case Node.DOCUMENT_FRAGMENT_NODE: 72 | if (!node) node = document.createDocumentFragment(); 73 | if (deep) 74 | this.childNodes.forEach(c => node.appendChild(c.cloneNode(deep))); 75 | return node; 76 | } 77 | } 78 | 79 | hasChildNodes() { 80 | return 0 < this.childNodes.length; 81 | } 82 | 83 | insertBefore(node, child) { 84 | if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE) { 85 | node.childNodes.splice(0).forEach(node => this.insertBefore(node, child)); 86 | } else if (node !== child) { 87 | const index = this.childNodes.indexOf(node); 88 | const swapping = -1 < index; 89 | if (swapping) this.childNodes.splice(index, 1); 90 | if (child) { 91 | this.childNodes.splice(this.childNodes.indexOf(child), 0, node); 92 | } else { 93 | this.childNodes.push(node); 94 | } 95 | if (!swapping) resetParent(this, node); 96 | } 97 | return node; 98 | } 99 | 100 | /* istanbul ignore next */ 101 | normalize() { 102 | for (let {childNodes} = this, i = 0; i < childNodes.length; i++) { 103 | const node = childNodes[i]; 104 | if (node.nodeType === 3) { 105 | if (!node.textContent.trim()) 106 | childNodes.splice(i--, 1); 107 | else { 108 | const {previousSibling} = node; 109 | if (previousSibling && previousSibling.nodeType === 3) { 110 | previousSibling.textContent += node.textContent; 111 | childNodes.splice(i--, 1); 112 | } 113 | } 114 | } 115 | else if (node.nodeType === 1) 116 | node.normalize(); 117 | } 118 | } 119 | 120 | removeChild(child) { 121 | nullParent(child); 122 | return child; 123 | } 124 | 125 | replaceChild(node, child) { 126 | if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE) { 127 | this.insertBefore(node, child); 128 | this.removeChild(child); 129 | } else if (node !== child) { 130 | const i = this.childNodes.indexOf(child); 131 | this.childNodes.splice(i, 0, node); 132 | nullParent(child); 133 | resetParent(this, node); 134 | } 135 | return child; 136 | } 137 | 138 | get firstChild() { 139 | return this.childNodes[0]; 140 | } 141 | 142 | get lastChild() { 143 | return this.childNodes[this.childNodes.length - 1]; 144 | } 145 | 146 | get nextSibling() { 147 | if (this.parentNode) { 148 | const cn = this.parentNode.childNodes; 149 | return cn[cn.indexOf(this) + 1] || null; 150 | } 151 | return null; 152 | } 153 | 154 | get parentElement() { 155 | const {parentNode} = this; 156 | return parentNode && parentNode.nodeType === 1 ? parentNode : null; 157 | } 158 | 159 | get previousSibling() { 160 | if (this.parentNode) { 161 | const cn = this.parentNode.childNodes; 162 | return cn[cn.indexOf(this) - 1] || null; 163 | } 164 | return null; 165 | } 166 | 167 | get textContent() { 168 | switch (this.nodeType) { 169 | case Node.ELEMENT_NODE: 170 | case Node.DOCUMENT_FRAGMENT_NODE: 171 | return this.childNodes.map(stringifiedContent).join(''); 172 | case Node.ATTRIBUTE_NODE: 173 | return this.value; 174 | case Node.TEXT_NODE: 175 | case Node.COMMENT_NODE: 176 | return this.data; 177 | default: return null; 178 | } 179 | } 180 | 181 | set textContent(text) { 182 | switch (this.nodeType) { 183 | case Node.ELEMENT_NODE: 184 | case Node.DOCUMENT_FRAGMENT_NODE: 185 | this.childNodes.splice(0).forEach(nullParent); 186 | if (text) { 187 | const node = this.ownerDocument.createTextNode(text); 188 | node.parentNode = this; 189 | this.childNodes.push(node); 190 | } 191 | break; 192 | case Node.ATTRIBUTE_NODE: 193 | this.value = text; 194 | break; 195 | case Node.TEXT_NODE: 196 | case Node.COMMENT_NODE: 197 | this.data = text; 198 | break; 199 | } 200 | } 201 | 202 | }; 203 | 204 | Object.keys(utils.types).forEach(type => { 205 | Node[type] = (Node.prototype[type] = utils.types[type]); 206 | }); 207 | 208 | module.exports = Node; 209 | 210 | function removeAttribute(attr) { 211 | if (attr.name === 'style') 212 | this.style.cssText = ''; 213 | else 214 | this.removeAttributeNode(attr); 215 | } 216 | 217 | function removeChild(node) { 218 | this.removeChild(node); 219 | } 220 | -------------------------------------------------------------------------------- /src/NodeFilter.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | SHOW_ALL: -1, 3 | SHOW_COMMENT: 128, 4 | SHOW_ELEMENT: 1 5 | }; 6 | -------------------------------------------------------------------------------- /src/ParentNode.js: -------------------------------------------------------------------------------- 1 | require('@webreflection/interface'); 2 | 3 | const Node = require('./Node'); 4 | const utils = require('./utils'); 5 | 6 | const childrenType = node => node.nodeType === Node.ELEMENT_NODE; 7 | 8 | function asNode(node) { 9 | return typeof node === 'object' ? 10 | node : 11 | this.createTextNode(node); 12 | } 13 | 14 | // interface ParentNode @ https://dom.spec.whatwg.org/#parentnode 15 | module.exports = Object.interface({ 16 | 17 | get children() { 18 | return this.childNodes.filter(childrenType); 19 | }, 20 | 21 | get firstElementChild() { 22 | for (let i = 0, length = this.childNodes.length; i < length; i++) { 23 | let child = this.childNodes[i]; 24 | if (child.nodeType === Node.ELEMENT_NODE) return child; 25 | } 26 | return null; 27 | }, 28 | 29 | get lastElementChild() { 30 | for (let i = this.childNodes.length; i--;) { 31 | let child = this.childNodes[i]; 32 | if (child.nodeType === Node.ELEMENT_NODE) return child; 33 | } 34 | return null; 35 | }, 36 | 37 | get childElementCount() { 38 | return this.children.length; 39 | }, 40 | 41 | prepend(...nodes) { 42 | const fragment = this.ownerDocument.createDocumentFragment(); 43 | fragment.childNodes.push(...nodes.map(asNode, this.ownerDocument)); 44 | if (this.childNodes.length) { 45 | this.insertBefore(fragment, this.firstChild); 46 | } else { 47 | this.appendChild(fragment); 48 | } 49 | }, 50 | 51 | append(...nodes) { 52 | const fragment = this.ownerDocument.createDocumentFragment(); 53 | fragment.childNodes.push(...nodes.map(asNode, this.ownerDocument)); 54 | this.appendChild(fragment); 55 | }, 56 | 57 | querySelector(css) { 58 | return this.querySelectorAll(css)[0] || null; 59 | }, 60 | 61 | querySelectorAll(css) { 62 | return utils.querySelectorAll.call(this, css); 63 | } 64 | 65 | }); 66 | -------------------------------------------------------------------------------- /src/Range.js: -------------------------------------------------------------------------------- 1 | // WARNING: this class is incomplete and 2 | // it doesn't fully reflect the whole client side API 3 | 4 | const DocumentFragment = require('./DocumentFragment'); 5 | 6 | function append(node) { 7 | this.appendChild(node); 8 | } 9 | 10 | function clone(node) { 11 | return node.cloneNode(true); 12 | } 13 | 14 | function remove(node) { 15 | this.removeChild(node); 16 | } 17 | 18 | function getContents(start, end) { 19 | const nodes = [start]; 20 | while (start !== end) { 21 | nodes.push(start = start.nextSibling); 22 | } 23 | return nodes; 24 | } 25 | 26 | // interface Text // https://dom.spec.whatwg.org/#text 27 | module.exports = class Range { 28 | 29 | cloneContents() { 30 | const fragment = new DocumentFragment(this._start.ownerDocument); 31 | getContents(this._start, this._end).map(clone).forEach(append, fragment); 32 | return fragment; 33 | } 34 | 35 | deleteContents() { 36 | getContents(this._start, this._end) 37 | .forEach(remove, this._start.parentNode); 38 | } 39 | 40 | extractContents() { 41 | const fragment = new DocumentFragment(this._start.ownerDocument); 42 | const nodes = getContents(this._start, this._end); 43 | nodes.forEach(remove, this._start.parentNode); 44 | nodes.forEach(append, fragment); 45 | return fragment; 46 | } 47 | 48 | cloneRange() { 49 | const range = new Range; 50 | range._start = this._start; 51 | range._end = this._end; 52 | return range; 53 | } 54 | 55 | setStartAfter(node) { 56 | this._start = node.nextSibling; 57 | } 58 | 59 | setStartBefore(node) { 60 | this._start = node; 61 | } 62 | 63 | setEndAfter(node) { 64 | this._end = node; 65 | } 66 | 67 | setEndBefore(node) { 68 | this._end = node.previousSibling; 69 | } 70 | 71 | }; 72 | -------------------------------------------------------------------------------- /src/Text.js: -------------------------------------------------------------------------------- 1 | const Node = require('./Node'); 2 | const CharacterData = require('./CharacterData'); 3 | 4 | // interface Text // https://dom.spec.whatwg.org/#text 5 | module.exports = class Text extends CharacterData { 6 | constructor(ownerDocument, text) { 7 | super(ownerDocument, text); 8 | this.nodeType = Node.TEXT_NODE; 9 | this.nodeName = '#text'; 10 | } 11 | 12 | get wholeText() { 13 | let text = this.textContent; 14 | let prev = this.previousSibling; 15 | while (prev && prev.nodeType === 3) { 16 | text = prev.textContent + text; 17 | prev = prev.previousSibling; 18 | } 19 | let next = this.nextSibling; 20 | while (next && next.nodeType === 3) { 21 | text = text + next.textContent; 22 | next = next.nextSibling; 23 | } 24 | return text; 25 | } 26 | 27 | set wholeText(val) {} 28 | }; 29 | -------------------------------------------------------------------------------- /src/TreeWalker.js: -------------------------------------------------------------------------------- 1 | const {SHOW_ALL, SHOW_COMMENT, SHOW_ELEMENT} = require('./NodeFilter.js'); 2 | 3 | const flat = (parentNode, list) => { 4 | const root = !list; 5 | if (root) 6 | list = new Set; 7 | else 8 | list.add(parentNode); 9 | const {childNodes, nextSibling} = parentNode; 10 | for (let i = 0, {length} = childNodes; i < length; i++) 11 | flat(childNodes[i], list); 12 | if (!root && nextSibling) 13 | flat(nextSibling, list); 14 | if (root) 15 | return [...list]; 16 | }; 17 | 18 | // this is dumb, but it works for uhtml 😎 19 | const isOK = ({nodeType}, mask) => { 20 | if (mask === SHOW_ALL) 21 | return true; 22 | const OTHERS = SHOW_ELEMENT | SHOW_COMMENT; 23 | switch (nodeType) { 24 | case 1: 25 | return mask === SHOW_ELEMENT || mask === OTHERS; 26 | case 8: 27 | return mask === SHOW_COMMENT || mask === OTHERS; 28 | } 29 | return false; 30 | }; 31 | 32 | module.exports = class TreeWalker { 33 | constructor(root, whatToShow = SHOW_ALL) { 34 | this.root = root; 35 | this.currentNode = null; 36 | this.whatToShow = whatToShow; 37 | this._list = flat(root); 38 | this._index = 0; 39 | this._length = this._list.length; 40 | } 41 | nextNode() { 42 | while (this._index < this._length) { 43 | const currentNode = this._list[this._index++]; 44 | if (isOK(currentNode, this.whatToShow)) 45 | return (this.currentNode = currentNode); 46 | } 47 | return (this.currentNode = null); 48 | } 49 | }; 50 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | // Used as Node.TYPE 2 | const ELEMENT_NODE = 1; 3 | const types = { 4 | ELEMENT_NODE, 5 | ATTRIBUTE_NODE: 2, 6 | TEXT_NODE: 3, 7 | COMMENT_NODE: 8, 8 | DOCUMENT_NODE: 9, 9 | DOCUMENT_TYPE_NODE: 10, 10 | DOCUMENT_FRAGMENT_NODE: 11 11 | }; 12 | 13 | // void cases 14 | const VOID_ELEMENT = /^area|base|br|col|embed|hr|img|input|keygen|link|menuitem|meta|param|source|track|wbr$/i; 15 | const VOID_ELEMENTS = new RegExp(`<(${VOID_ELEMENT.source})([^>]*?)>`, 'gi'); 16 | const VOID_SANITIZER = (_, $1, $2) => `<${$1}${$2}${/\/$/.test($2) ? '' : ' /'}>`; 17 | 18 | // shared constants 19 | const connect = (parentNode, child) => { 20 | if ( 21 | child.nodeType === ELEMENT_NODE && 22 | child.isCustomElement && 'connectedCallback' in child && 23 | parentNode && 24 | parentNode.nodeType !== types.DOCUMENT_FRAGMENT_NODE && 25 | parentNode.nodeName !== 'template' 26 | ) { 27 | child.connectedCallback(); 28 | } 29 | }; 30 | 31 | const disconnect = (parentNode, child) => { 32 | if ( 33 | child.nodeType === ELEMENT_NODE && 34 | child.isCustomElement && 'disconnectedCallback' in child && 35 | parentNode && parentNode.nodeType !== types.DOCUMENT_FRAGMENT_NODE 36 | ) { 37 | child.disconnectedCallback(); 38 | } 39 | }; 40 | 41 | const notifyAttributeChanged = (el, name, oldValue, newValue) => { 42 | if ( 43 | el.isCustomElement && 44 | 'attributeChangedCallback' in el && 45 | (el.constructor.observedAttributes || []).includes(name) 46 | ) { 47 | el.attributeChangedCallback(name, oldValue, newValue); 48 | } 49 | }; 50 | 51 | const CSS_SPLITTER = /\s*,\s*/; 52 | function findBySelector(css) { 53 | switch (css[0]) { 54 | case '#': 55 | return this.ownerDocument.getElementById(css.slice(1)); 56 | case '.': 57 | return this.getElementsByClassName(css.slice(1)); 58 | case '[': 59 | return findBAttributeName(this, css.replace(/[\[\]]/g, ''), []); 60 | default: 61 | return this.getElementsByTagName(css); 62 | } 63 | } 64 | 65 | module.exports = { 66 | VOID_ELEMENT, 67 | connect, 68 | disconnect, 69 | notifyAttributeChanged, 70 | types, 71 | querySelectorAll(css) { 72 | return [].concat(...('' + css).split(CSS_SPLITTER).map(findBySelector, this)); 73 | }, 74 | voidSanitizer: html => html.replace(VOID_ELEMENTS, VOID_SANITIZER) 75 | }; 76 | 77 | function findBAttributeName(node, name, all) { 78 | for (let children = node.children, {length} = children, i = 0; i < length; i++) { 79 | const child = children[i]; 80 | if (child.hasAttribute(name)) 81 | all.push(child); 82 | findBAttributeName(child, name, all); 83 | } 84 | return all; 85 | } 86 | -------------------------------------------------------------------------------- /test/all.js: -------------------------------------------------------------------------------- 1 | const {title, assert, async, log} = require('tressa'); 2 | const { 3 | CustomElementRegistry, CustomEvent, 4 | Document, 5 | Event, EventTarget, 6 | HTMLElement, HTMLTemplateElement, HTMLUnknownElement, 7 | Image 8 | } = require('../basichtml.js'); 9 | 10 | title('basicHTML'); 11 | assert( 12 | typeof CustomElementRegistry === 'function' && 13 | typeof CustomEvent === 'function' && 14 | typeof Document === 'function' && 15 | typeof Event === 'function' && 16 | typeof HTMLElement === 'function' && 17 | typeof HTMLUnknownElement === 'function', 18 | 'classes exported' 19 | ); 20 | 21 | let customElements = new CustomElementRegistry(); 22 | let document = new Document(customElements); 23 | 24 | assert( 25 | document.customElements === customElements, 26 | 'a document can share customElements with others' 27 | ); 28 | assert( 29 | document.defaultView === global, 30 | 'a document has a defaultView too' 31 | ); 32 | 33 | document = new Document(); 34 | assert( 35 | document.customElements !== customElements, 36 | 'a document creates by default its own customElements reference' 37 | ); 38 | customElements = document.customElements; 39 | 40 | log('## basic element'); 41 | let any = document.createElement('any'); 42 | assert( 43 | any instanceof HTMLElement, 44 | 'you can create any element' 45 | ); 46 | 47 | log('## basic attr'); 48 | let attribute = document.createAttribute('test-attribute'); 49 | attribute.value = 'some content'; 50 | assert( 51 | attribute.ownerElement === null, 52 | 'attributes do not need an owner element' 53 | ); 54 | 55 | log('## basic attr + element'); 56 | any.setAttributeNode(attribute); 57 | assert( 58 | attribute.ownerElement === any, 59 | 'but once set these will reflect the owner' 60 | ); 61 | assert( 62 | any.getAttribute('test-attribute') === attribute.value, 63 | 'get and setAttribute works' 64 | ); 65 | attribute.value = 'something else'; 66 | assert( 67 | any.getAttribute('test-attribute') === 'something else', 68 | 'attributes direct changes are reflected' 69 | ); 70 | 71 | log('## canvas'); 72 | let canvas = document.createElement('canvas'); 73 | assert( 74 | canvas.width === 300 && canvas.height === 150, 75 | 'a canvas has default size' 76 | ); 77 | canvas.width = canvas.height = 1; 78 | assert( 79 | canvas.width === 1 && canvas.height === 1, 80 | 'a canvas can change size' 81 | ); 82 | assert( 83 | canvas.toDataURL() === 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAABmJLR0QA/wD/AP+gvaeTAAAAC0lEQVQImWNgAAIAAAUAAWJVMogAAAAASUVORK5CYII=', 84 | 'a canvas produces an image' 85 | ); 86 | assert( 87 | typeof canvas.getContext('2d') === 'object', 88 | 'a canvas has a context' 89 | ); 90 | 91 | log('## image'); 92 | let img = Image(document, 1, 1); 93 | assert( 94 | img.outerHTML === '', 95 | 'img rendered correctly with two args' 96 | ); 97 | img = Image(document, 2); 98 | assert( 99 | img.outerHTML === '', 100 | 'img rendered correctly with one arg' 101 | ); 102 | img.width = canvas.width; 103 | img.height = canvas.height; 104 | img.onload = img.onerror = Object; 105 | img.src = canvas.toDataURL(); 106 | assert( 107 | img.onload && img.onerror && img.width && img.height && 108 | img.src === canvas.toDataURL(), 109 | 'same properties' 110 | ); 111 | 112 | document.createElement = (function (createElement) { 113 | return function (name) { 114 | return name === 'img' ? 115 | new HTMLElement(document, name) : 116 | createElement.apply(this, arguments); 117 | }; 118 | }(document.createElement)); 119 | img = Image(document); 120 | assert(img.tagName === 'img', 'Simple image works too'); 121 | 122 | 123 | log('## class attr'); 124 | attribute = document.createAttribute('class'); 125 | attribute.value = 'some class'; 126 | let nodeWithoutClass = document.createElement('div'); 127 | nodeWithoutClass.setAttributeNode(attribute); 128 | assert( 129 | nodeWithoutClass.outerHTML === '
', 130 | 'class attribute works even without owner element' 131 | ); 132 | 133 | log('## setAttributeNode'); 134 | let targetA = document.createElement('yo'); 135 | let targetB = document.createElement('yo'); 136 | let an = document.createAttribute('a'); 137 | let bn = document.createAttribute('a'); 138 | targetA.setAttributeNode(an); 139 | assert( 140 | targetA.setAttributeNode(an) === an, 141 | 'same attribute does not effect the node' 142 | ); 143 | try { 144 | targetB.setAttributeNode(an); 145 | assert(false, 'attributes cannot be in two nodes'); 146 | } catch(e) { 147 | assert(true, e.message); 148 | } 149 | 150 | assert(targetA.setAttributeNode(bn) === an); 151 | assert(targetA.setAttributeNode(an) === bn); 152 | 153 | an = document.createAttribute('class'); 154 | assert(targetA.setAttributeNode(an) === null); 155 | 156 | log('## DocumentType'); 157 | const DocumentType = require('../src/DocumentType.js'); 158 | let dt = new DocumentType(); 159 | assert( 160 | dt.textContent === null, 161 | 'DocumentType has no content' 162 | ); 163 | 164 | log('## DOMTokenList'); 165 | any.setAttribute('class', 'a b c'); 166 | assert( 167 | any.classList.value === any.getAttribute('class'), 168 | 'class attribute is reflected through classList' 169 | ); 170 | 171 | any.classList.toggle('b'); 172 | assert( 173 | any.classList.value === 'a c', 174 | 'toggle works too' 175 | ); 176 | 177 | any.classList.replace('c', 'd'); 178 | assert( 179 | any.classList.value === 'a d', 180 | 'replace works too' 181 | ); 182 | 183 | any.classList.toggle('b'); 184 | any.classList.toggle('b', true); 185 | assert( 186 | any.classList.value === 'a d b', 187 | 'toggle can add too' 188 | ); 189 | 190 | any.classList.toggle('x'); 191 | any.classList.toggle('y', false); 192 | any.classList.replace('z', 'w'); 193 | any.classList.remove('b', 'x', 'y', 'w'); 194 | 195 | assert( 196 | any.classList.item(0) === 'a', 197 | 'classes preserve the order' 198 | ); 199 | 200 | let nope = document.createElement('nope'); 201 | nope.classList.value = 'OK'; 202 | assert( 203 | nope.getAttribute('class') === 'OK', 204 | 'of classList.value is set the class attribute is created' 205 | ); 206 | 207 | any.textContent = 'hello'; 208 | any.appendChild(document.createElement('br')); 209 | any.setAttribute('hidden', true); 210 | any.setAttribute('wut', ''); 211 | assert( 212 | any.outerHTML === '', 213 | 'nodes can have a text content' 214 | ); 215 | 216 | any.attributes['test-attribute'].value = null; 217 | assert( 218 | any.outerHTML === '', 219 | 'attribute can have a null value' 220 | ); 221 | 222 | any.setAttribute('hidden', false); 223 | any.removeAttribute('wut'); 224 | 225 | any.innerHTML = '

OK

'; 226 | assert( 227 | any.innerHTML === '

OK

' && 228 | any.outerHTML === '' + any.innerHTML + '', 229 | 'but also html' 230 | ); 231 | 232 | log('## document as string'); 233 | assert( 234 | document.toString() === '', 235 | 'document can be represented as a string' 236 | ); 237 | 238 | log('## document.querySelector'); 239 | assert( 240 | document.querySelector('any') === null, 241 | 'of not appended, a node canot be queried' 242 | ); 243 | 244 | document.body.appendChild(any); 245 | assert( 246 | document.querySelector('any') === any, 247 | 'but once in there, everything is fine' 248 | ); 249 | 250 | log('## text node'); 251 | let text = document.createTextNode('hellO'); 252 | 253 | assert(text.nextSibling === null); 254 | assert(text.previousSibling === null); 255 | assert(text.textContent === 'hellO'); 256 | text.textContent = 'Hello'; 257 | 258 | document.body.appendChild(text); 259 | assert( 260 | document.body.lastChild === text, 261 | 'also text node can be appended' 262 | ); 263 | 264 | assert( 265 | document.body.lastElementChild === any, 266 | 'but not retrieved as element child' 267 | ); 268 | let text1 = document.body.insertBefore(document.createTextNode(''), text); 269 | let text2 = document.body.insertBefore(document.createTextNode('basicHTML: '), text); 270 | let text3 = document.body.appendChild(document.createTextNode(' world!')); 271 | let text4 = document.body.appendChild(document.createTextNode('')); 272 | assert( 273 | text.wholeText === 'basicHTML: Hello world!' 274 | ); 275 | assert( 276 | text2.wholeText === 'basicHTML: Hello world!' 277 | ); 278 | assert( 279 | text3.wholeText === 'basicHTML: Hello world!' 280 | ); 281 | assert( 282 | text.wholeText = 'Does nothing' 283 | ); 284 | document.body.removeChild(text1); 285 | document.body.removeChild(text2); 286 | document.body.removeChild(text3); 287 | document.body.removeChild(text4); 288 | 289 | log('## node siblings'); 290 | assert( 291 | any.nextSibling === text, 292 | 'nextSibling works as expected' 293 | ); 294 | assert( 295 | text.nextSibling === null, 296 | 'if not present, returns null' 297 | ); 298 | assert( 299 | text.previousSibling === any, 300 | 'previousSibling works as expected' 301 | ); 302 | assert( 303 | document.head.previousSibling === null, 304 | 'if not present, returns null' 305 | ); 306 | 307 | log('## elements features'); 308 | assert( 309 | document.documentElement.childElementCount === 2, 310 | 'childElementCount works as expected' 311 | ); 312 | assert( 313 | document.documentElement.firstElementChild === document.head, 314 | 'firstElementChild works as expected' 315 | ); 316 | 317 | document.head.append('text'); 318 | assert( 319 | document.head.firstElementChild === null, 320 | 'and it can be null' 321 | ); 322 | assert( 323 | document.documentElement.lastElementChild === document.body, 324 | 'lastElementChild works as expected' 325 | ); 326 | assert( 327 | document.head.lastElementChild === null, 328 | 'and it can be null' 329 | ); 330 | 331 | log('## EventTarget'); 332 | 333 | let globalEventTarget = false; 334 | EventTarget.init(document.defaultView); 335 | document.defaultView.addEventListener('global-event-target', () => { globalEventTarget = true; }); 336 | document.body.dispatchEvent(new Event('global-event-target', { bubbles: true })); 337 | assert( 338 | globalEventTarget, 339 | 'window is reached while dispatching events' 340 | ); 341 | 342 | let first = document.createElement('first'); 343 | let second = document.createElement('second'); 344 | let third = document.createElement('third'); 345 | first.appendChild(second).append(third); 346 | third.once = 0; 347 | third.addEventListener('once', e => third.once++, {once: true}); 348 | third.twice = 0; 349 | let twice = e => { 350 | third.twice++; 351 | e.preventDefault(); 352 | }; 353 | third.addEventListener('twice', twice); 354 | third.addEventListener('twice', twice); 355 | let e = new Event('once'); 356 | e.target = third; 357 | e.currentTarget = third; 358 | third.dispatchEvent(e); 359 | third.dispatchEvent(new Event('once')); 360 | third.dispatchEvent(new Event('twice')); 361 | third.dispatchEvent(new Event('twice')); 362 | 363 | assert( 364 | third.once === 1, 365 | 'using {once: true} via addEventListener works' 366 | ); 367 | assert( 368 | third.twice === 2, 369 | 'setting twice same listener does not add them twice' 370 | ); 371 | first.addEventListener('twice', e => first.called = true); 372 | second.addEventListener('twice', e => { 373 | second.called = true; 374 | e.stopPropagation(); 375 | }, {once: true}); 376 | try { 377 | document.createEvent('MouseEvent'); 378 | assert(false, 'MouseEvent should not be allowed'); 379 | } catch(e) { 380 | assert(true, 'document.createEvent(...) can be used with "Event" only'); 381 | } 382 | var createdEvent = document.createEvent('Event'); 383 | createdEvent.initEvent('twice', true, true); 384 | third.dispatchEvent(createdEvent); 385 | assert( 386 | second.called === true, 387 | 'events bubble up' 388 | ); 389 | assert( 390 | first.called !== true, 391 | 'and you can stopPropagation' 392 | ); 393 | second.addEventListener('twice', e => { 394 | e.stopImmediatePropagation(); 395 | }, {once: true}); 396 | second.addEventListener('twice', e => { 397 | second.nope = true; 398 | }, {once: true}); 399 | third.dispatchEvent(new Event('twice', {bubbles: true})); 400 | assert( 401 | second.nope !== true, 402 | 'you can also stopImmediatePropagation' 403 | ); 404 | third.dispatchEvent(new Event('twice', {bubbles: true})); 405 | assert( 406 | first.called === true, 407 | 'bubbles up to the top' 408 | ); 409 | third.addEventListener('click', {handleEvent(e) { 410 | assert( 411 | this !== third && e.target === third, 412 | 'even {handleEvent(){}} works' 413 | ); 414 | }}, {once: true}); 415 | third.click(); 416 | assert(document.activeElement === third, 'activeElement'); 417 | third.blur(); 418 | assert(document.activeElement == null, 'activeElement as null'); 419 | third.removeEventListener('twice', {}); 420 | third.removeEventListener('nope', {}); 421 | 422 | first.addEventListener('custom', e => (first.detail = e.detail)); 423 | first.dispatchEvent(new CustomEvent('custom', {detail: second})); 424 | assert( 425 | first.detail === second, 426 | 'CustomEvent also works as expected' 427 | ); 428 | 429 | var createdCustomEvent = document.createEvent('CustomEvent'); 430 | createdCustomEvent.initCustomEvent('custom', true, true, 'detail'); 431 | assert( 432 | createdCustomEvent.detail === 'detail', 433 | 'CustomEvent can be created procedurally' 434 | ); 435 | 436 | log('## Comments'); 437 | let comment = document.createComment('Here a comment'); 438 | assert(comment.textContent === 'Here a comment'); 439 | comment.textContent = 'here a comment'; 440 | 441 | third.appendChild(comment); 442 | assert( 443 | third.innerHTML === '', 444 | 'comments also works as expected' 445 | ); 446 | assert( 447 | third.textContent === '', 448 | 'and do not interfere as textContent' 449 | ); 450 | 451 | third.textContent = '
'; 452 | assert( 453 | third.innerHTML === '<br/>', 454 | 'text nodes are sanitized' 455 | ); 456 | 457 | log('## DOMStringMap'); 458 | first.dataset.testName = 'test value'; 459 | assert( 460 | first.hasAttribute('data-test-name'), 461 | 'dataset also works as expected' 462 | ); 463 | assert( 464 | 'testName' in first.dataset, 465 | 'the attribute name is normalized' 466 | ); 467 | assert( 468 | first.dataset.testName === 'test value', 469 | 'the value is the expected' 470 | ); 471 | 472 | delete first.dataset.testName; 473 | assert( 474 | !first.hasAttribute('data-test-name'), 475 | 'properties can be deleted' 476 | ); 477 | 478 | let id = String(Math.random()); 479 | document.body.appendChild(document.createElement('by-id')).id = id; 480 | document.body.lastChild.setAttribute('ref', ''); 481 | assert( 482 | document.querySelector('#' + id) === document.body.lastChild && 483 | document.getElementById(id) === document.body.lastChild && 484 | document.querySelector('[ref]') === document.body.lastChild && 485 | document.body.lastChild.matches('#' + id), 486 | 'elements can be found by ID' 487 | ); 488 | 489 | document.documentElement.id = 'whatever'; 490 | assert( 491 | document.getElementById('whatever') === document.documentElement, 492 | 'even the HTML one' 493 | ); 494 | assert(document.getElementById('nope') === null); 495 | 496 | assert( 497 | document.querySelector('by-id') === document.body.lastChild && 498 | document.getElementsByTagName('by-id')[0] === document.body.lastChild, 499 | 'elements can be found also by tag name' 500 | ); 501 | 502 | assert( 503 | document.getElementsByTagName('html')[0] === document.documentElement && 504 | document.children[0] === document.documentElement && 505 | document.firstElementChild === document.documentElement && 506 | document.lastElementChild === document.documentElement && 507 | document.documentElement.matches('html') && 508 | document.childElementCount === 1, 509 | 'even getting the HTML one' 510 | ); 511 | 512 | 513 | document.body.classList.value = 'test'; 514 | assert( 515 | document.getElementsByClassName('test')[0] === document.body && 516 | document.querySelector('.test') === document.body && 517 | document.body.matches('.test'), 518 | 'getElementsByClassName works too' 519 | ); 520 | 521 | document.documentElement.classList.value = 'the-html'; 522 | assert( 523 | document.getElementsByClassName('the-html')[0] === document.documentElement, 524 | 'even with HTML tag' 525 | ); 526 | 527 | assert( 528 | document.querySelectorAll('head,body') 529 | .every((el, i) => i ? el === document.body : el === document.head), 530 | 'and querySelectorAll works too' 531 | ); 532 | 533 | try { document.append('banana'); } catch(e) { 534 | assert(true, 'append and prepend are not allowed on the document'); 535 | } 536 | 537 | assert( 538 | document.createElementNS('svg', 'test').nodeName === 'test:svg', 539 | 'createElementNS simply puts tags and namespace together' 540 | ); 541 | 542 | assert(document.createElementNS('http://www.w3.org/1999/xhtml', 'template') instanceof HTMLTemplateElement, 'createElementNS uses createElement for HTML namespace'); 543 | 544 | assert( 545 | document.createElement('something').tagName == 'something', 546 | 'elements have tagNames' 547 | ) 548 | 549 | log('## documentElement'); 550 | document.documentElement.title = 'some title'; 551 | assert( 552 | document.documentElement.getAttribute('title') === 'some title' && 553 | document.documentElement.title === 'some title', 554 | 'some attribute is special' 555 | ); 556 | document.documentElement.onclick = e => { 557 | assert( 558 | e.type === 'click' && e.target === document.documentElement, 559 | 'and can be dispatched like others' 560 | ); 561 | }; 562 | assert( 563 | typeof document.documentElement.onclick === 'function', 564 | 'even DOM Level 0 events are supported' 565 | ); 566 | document.documentElement.dispatchEvent(new Event('click')); 567 | document.documentElement.onclick = null; 568 | document.documentElement.dispatchEvent(new Event('click')); 569 | document.documentElement.onclick = null; 570 | document.documentElement.onclick = {method(){}}.method; 571 | 572 | document.documentElement.innerHTML = ` 573 | 574 | 575 | 576 | 577 | 578 |
579 | 580 | 581 | `; 582 | assert( 583 | document.documentElement.hasChildNodes() && 584 | true || document.getElementsByTagName('*').length === 5, 585 | 'documentElement can inject whole documents' 586 | ); 587 | 588 | assert( 589 | document.getElementsByTagName('style')[0].outerHTML === 590 | '', 591 | 'style preserves HTML entities' 592 | ); 593 | 594 | document.documentElement.innerHTML = ''; 595 | assert( 596 | document.documentElement.hasChildNodes() && 597 | document.getElementsByTagName('*').length === 3, 598 | 'but partial head and body can also be assigned' 599 | ); 600 | 601 | document.documentElement.removeChild(document.documentElement.firstChild); 602 | document.documentElement.removeChild(document.documentElement.firstChild); 603 | assert( 604 | !document.documentElement.hasChildNodes() && 605 | document.getElementsByTagName('*').length === 1, 606 | 'or eventually removed too' 607 | ); 608 | 609 | document.documentElement.innerHTML = ` 610 | 611 | 612 | 613 | `; 614 | assert( 615 | document.documentElement.innerHTML === 616 | "\n \n \n ", 617 | 'multi void elements are supported' 618 | ); 619 | 620 | let voidInDiv = document.createElement('div'); 621 | voidInDiv.innerHTML = ` 622 |
623 | 624 | 625 |
626 | `; 627 | 628 | assert( 629 | voidInDiv.innerHTML === 630 | "\n
\n \n \n
\n", 631 | 'also inside elements' 632 | ); 633 | 634 | log('## playing with childNodes'); 635 | let one = document.body.appendChild(document.createElement('one')); 636 | let two = document.createElement('two'); 637 | document.body.appendChild(one); 638 | let fragment = document.createDocumentFragment(); 639 | fragment.appendChild(two).textContent = '2'; 640 | assert(fragment.textContent === '2'); 641 | document.body.replaceChild(fragment, one); 642 | assert(document.body.lastChild === two, 'you can replace fragments'); 643 | document.body.replaceChild(one, two); 644 | assert(document.body.lastChild === one, 'or simple nodes too'); 645 | document.body.insertBefore(one, one); 646 | document.body.replaceChild(one, one); 647 | assert(document.body.childNodes.length === 1); 648 | 649 | let before = document.createElement('before'); 650 | let after = document.createElement('after'); 651 | document.body.append(before, after); 652 | assert(document.body.lastElementChild === after, 'after is the last element'); 653 | assert(document.body.lastElementChild.previousElementSibling === before, 'before is the previous one'); 654 | document.body.insertBefore(after, before); 655 | assert(document.body.lastElementChild === before, 'before is now after'); 656 | assert(document.body.lastElementChild.previousElementSibling === after, 'after is now before'); 657 | 658 | log('## attachShadow'); 659 | try { 660 | document.body.attachShadow(); 661 | } catch(e) { 662 | assert(true, 'attachShadow needs one argument'); 663 | } 664 | try { 665 | document.body.attachShadow({}); 666 | } catch(e) { 667 | assert(true, 'attachShadow needs one argument with a mode'); 668 | } 669 | assert( 670 | document.body.attachShadow({mode: 'closed'}) === document.body && 671 | document.body.shadowRoot === undefined, 672 | 'attachShadow({mode: "closed"})' 673 | ); 674 | assert( 675 | document.body.attachShadow({mode: 'open'}) === document.body && 676 | document.body.shadowRoot === document.body, 677 | 'attachShadow({mode: "open"})' 678 | ); 679 | 680 | log('## style'); 681 | 682 | assert( 683 | document.createAttribute('style').value === '', 684 | 'it can be created as attribute' 685 | ); 686 | 687 | assert( 688 | document.body.style && document.body.style === document.body.style, 689 | 'style available per each element' 690 | ); 691 | 692 | assert( 693 | 'cssText' in document.body.style, 694 | 'cssText available per each style' 695 | ); 696 | 697 | document.body.style.cssText = '-00.0123456%'; 698 | assert( 699 | document.body.style.cssText === '-00.0123456%', 700 | 'meaningless styles are not ignored' 701 | ); 702 | 703 | document.body.style.cssText = ':-00.0123456%'; 704 | assert( 705 | document.body.style.cssText === ':-00.0123456%', 706 | 'empty keys are not ignored either' 707 | ); 708 | 709 | document.body.style.cssText = '_hyper:123'; 710 | assert( 711 | document.body.style.cssText === '_hyper: 123;', 712 | '_hyper style has no issues' 713 | ); 714 | 715 | document.body.style.setProperty('--custom', 456); 716 | assert( 717 | document.body.style.getPropertyValue('--custom') == 456, 718 | 'getPropertyValue works as expected' 719 | ); 720 | assert( 721 | document.body.style.cssText === '_hyper: 123;--custom:456;', 722 | 'custom style has no issues' 723 | ); 724 | 725 | document.body.style.cssText = ''; 726 | 727 | 728 | document.body.style.fontFamily = 'sans-serif'; 729 | assert( 730 | document.body.style.cssText === 'font-family:sans-serif;', 731 | 'style text can be retrieved' 732 | ); 733 | 734 | document.body.style.cssText = 'font-family:monospace;'; 735 | assert( 736 | document.body.style.cssText === 'font-family:monospace;', 737 | 'style text can be set' 738 | ); 739 | 740 | assert( 741 | 'fontFamily' in document.body.style && 742 | document.body.style.fontFamily === 'monospace', 743 | 'style as a property' 744 | ); 745 | 746 | log('## className'); 747 | document.body.className = 'a b'; 748 | assert( 749 | document.body.className === 'a b' && 750 | document.body.classList.value === 'a b', 751 | 'it can be set and retrieved back' 752 | ); 753 | 754 | log('## extras'); 755 | document.body.textContent = ''; 756 | assert(document.body.getAttributeNames().join(',') === 'style,class', 'getAttributeNames works'); 757 | assert(document.body.hasAttributes(), 'hasAttributes too'); 758 | document.body.prepend('a', 'b', 'c'); 759 | document.body.prepend('d'); 760 | assert(document.body.textContent === 'dabc', 'and so does .prepend(...)'); 761 | assert( 762 | document.body.closest('body') === document.body && 763 | document.body.closest('html') === document.documentElement && 764 | document.body.closest('shenanigans') === null, 765 | 'same goes for closest' 766 | ); 767 | 768 | document.body.innerHTML = '

1

'; 769 | document.body.firstChild.replaceWith(document.createElement('p')); 770 | assert(document.body.firstChild.textContent === ''); 771 | document.body.firstChild.after(document.createElement('div')); 772 | document.body.lastChild.before(document.createElement('div')); 773 | let tmpChild = document.body.firstChild; 774 | tmpChild.remove(); 775 | tmpChild.remove(); 776 | tmpChild.before(); 777 | tmpChild.after(); 778 | tmpChild.replaceWith(); 779 | 780 | document.body.innerHTML = '

some

'; 781 | assert(document.body.innerHTML === '

some

'); 782 | let flexibility = document.createElement('_'); 783 | let value = {thing: Math.random()}; 784 | flexibility.setAttribute('any', value); 785 | assert(value === flexibility.getAttribute('any')); 786 | 787 | log('## Node.normalize()'); 788 | tmpChild = document.createElement('ul'); 789 | tmpChild.innerHTML = ` 790 |
  • a
  • 791 |
  • b
  • 792 | `; 793 | tmpChild.normalize(); 794 | assert(tmpChild.innerHTML === '
  • a
  • b
  • '); 795 | 796 | log('## TreeWalker'); 797 | let twFragment = document.createDocumentFragment(); 798 | let twNode = twFragment.appendChild(document.createElement('p')); 799 | let twComment = twFragment.appendChild(document.createComment('comment')); 800 | let twText = twFragment.appendChild(document.createTextNode('node')); 801 | let tw = document.createTreeWalker(twFragment, 1); 802 | let currentNode = tw.nextNode(); 803 | assert(currentNode === twNode, 'TreeWalker node'); 804 | currentNode = tw.nextNode(); 805 | assert(currentNode === null, 'TreeWalker one node only'); 806 | tw = document.createTreeWalker(twFragment, 128); 807 | currentNode = tw.nextNode(); 808 | assert(currentNode === twComment, 'TreeWalker comment'); 809 | currentNode = tw.nextNode(); 810 | assert(currentNode === null, 'TreeWalker one comment only'); 811 | tw = document.createTreeWalker(twFragment); 812 | currentNode = tw.nextNode(); 813 | assert(currentNode === twNode, 'TreeWalker node'); 814 | currentNode = tw.nextNode(); 815 | assert(currentNode === twComment, 'TreeWalker comment'); 816 | currentNode = tw.nextNode(); 817 | assert(currentNode === twText, 'TreeWalker text'); 818 | currentNode = tw.nextNode(); 819 | assert(currentNode === null, 'TreeWalker all children parsed'); 820 | 821 | log('## document.importNode()'); 822 | let toBeImported = document.createDocumentFragment(); 823 | assert(document.importNode(toBeImported) !== toBeImported, 'import node'); 824 | 825 | log('## Node.cloneNode()'); 826 | let toBeCloned = document.createDocumentFragment(); 827 | let toBeClonedP = toBeCloned.appendChild(document.createElement('p')); 828 | toBeClonedP.setAttribute('one', 'two'); 829 | toBeClonedP.appendChild(document.createTextNode('three')); 830 | toBeClonedP.appendChild(document.createComment('four')); 831 | assert(toBeClonedP.cloneNode().outerHTML === '

    ', 'clone Element'); 832 | assert(toBeClonedP.cloneNode(true).outerHTML === toBeClonedP.outerHTML, 'clone Element deep'); 833 | assert(toBeCloned.cloneNode(true).firstChild.outerHTML === toBeClonedP.outerHTML, 'clone #document-fragment'); 834 | assert(toBeClonedP.getAttributeNode('one') === toBeClonedP.attributes.one, 'attributes by name'); 835 | let toBeClonedAttr = toBeClonedP.getAttributeNode('one').cloneNode(); 836 | assert(toBeClonedAttr.name === 'one' && toBeClonedAttr.value === 'two', 'clone attributes'); 837 | 838 | toBeClonedP.removeAttributeNode(toBeClonedP.attributes.one); 839 | assert(toBeClonedP.attributes.one == null, 'attributes can be removed'); 840 | try { 841 | toBeClonedP.removeAttributeNode(toBeClonedP.attributes.one); 842 | assert(false, 'attributes must be valid to be removed'); 843 | } catch(e) { 844 | assert(true, 'attributes must be valid to be removed'); 845 | } 846 | 847 | log('## siblings'); 848 | let ol = document.createElement('ol'); 849 | let li = document.createElement('li'); 850 | ol.appendChild(li); 851 | assert(li.parentElement === ol, 'parentElement'); 852 | assert(ol.parentElement === null, 'parentElement'); 853 | assert(li.previousElementSibling === null, 'null previousElementSibling'); 854 | assert(li.nextElementSibling === null, 'null nextElementSibling'); 855 | ol.insertBefore(document.createElement('li'), li); 856 | ol.appendChild(document.createElement('li')); 857 | assert(li.previousElementSibling === ol.childNodes[0], 'previousElementSibling'); 858 | assert(li.nextElementSibling === ol.childNodes[2], 'nextElementSibling'); 859 | ol.insertBefore(document.createElement('li')); 860 | assert(ol.childNodes.length === 4); 861 | 862 | log('## HTMLTemplateElement'); 863 | assert(document.createElement('template').content.nodeType === 11, 'has a fragment content'); 864 | 865 | log('## Range'); 866 | let range = document.createRange(); 867 | let clone = range.cloneRange(); 868 | range.setStartAfter(ol.childNodes[0]); 869 | range.setEndBefore(ol.childNodes[3]); 870 | range.deleteContents(); 871 | assert(ol.childNodes.length === 2, 'range removed two nodes'); 872 | range = document.createRange(); 873 | range.setStartBefore(ol.firstChild); 874 | range.setEndAfter(ol.lastChild); 875 | range.deleteContents(); 876 | assert(ol.childNodes.length === 0, 'range removed two other nodes'); 877 | ol.appendChild(document.createElement('li')); 878 | ol.appendChild(document.createElement('li')); 879 | ol.appendChild(document.createElement('li')); 880 | range = document.createRange(); 881 | range.setStartBefore(ol.firstChild); 882 | range.setEndAfter(ol.lastChild); 883 | let olLive = ol.childNodes.slice(0); 884 | let olExtracted = range.extractContents(); 885 | assert(olLive.every((li, i) => li === olExtracted.childNodes[i]), 'extractContents works'); 886 | ol.appendChild(document.createElement('li')).textContent = '0'; 887 | ol.appendChild(document.createElement('li')).textContent = '1'; 888 | ol.appendChild(document.createElement('li')).textContent = '2'; 889 | range = document.createRange(); 890 | range.setStartBefore(ol.firstChild); 891 | range.setEndAfter(ol.lastChild); 892 | olExtracted = range.cloneContents(); 893 | assert(olExtracted.childNodes.every( 894 | (li, i) => li.textContent == i && 895 | li.textContent === ol.childNodes[i].textContent 896 | ), 'cloneContents works'); 897 | 898 | log('## NamedNodeMap'); 899 | let withAttrs = document.createElement('div'); 900 | withAttrs.setAttribute('test', 'value'); 901 | let attr = withAttrs.getAttributeNode('test'); 902 | let attrs = withAttrs.attributes; 903 | assert(attrs.length, 'NamedNodeMap#length'); 904 | let last = attrs.length - 1; 905 | assert(attrs.item(last) === attr, 'NamedNodeMap#item'); 906 | assert(attrs.getNamedItem('test') === attr, 'NamedNodeMap#getNamedItem'); 907 | attrs.removeNamedItem('test'); 908 | assert(!withAttrs.hasAttribute('test'), 'NamedNodeMap#removeNamedItem'); 909 | assert(attrs.item(last) === null, 'NamedNodeMap#item(...) as null'); 910 | attrs.setNamedItem(attr); 911 | assert(withAttrs.hasAttribute('test'), 'NamedNodeMap#setNamedItem'); 912 | 913 | log('## Custom Element'); 914 | async(done => { 915 | customElements.whenDefined('test-node').then(() => { 916 | document.body.innerHTML = ``; 917 | const test = document.body.firstChild; 918 | test.setAttribute('nope', 123); 919 | test.setAttribute('test', 123); 920 | test.setAttribute('test', 123); 921 | var attr = test.getAttributeNode('test'); 922 | attr.value = 456; 923 | attr = document.createAttributeNS(null, 'test'); 924 | test.setAttributeNodeNS(attr); 925 | attr.value = 345; 926 | assert(attr.textContent == attr.value); 927 | attr.textContent = 345; 928 | assert( 929 | actions.splice(0).join(',') === 930 | [ 931 | 'created', 932 | 'connected', 933 | 'attributeChanged', 934 | 'attributeChanged', 935 | 'attributeChanged', 936 | 'attributeChanged' 937 | ].join(','), 938 | 'expected actions' 939 | ); 940 | test.setAttribute('class', 123); 941 | test.setAttribute('class', 123); 942 | attr = test.getAttributeNode('class'); 943 | attr.value = 456; 944 | attr = document.createAttribute('class'); 945 | test.setAttributeNode(attr); 946 | attr.value = 345; 947 | test.removeAttribute('class'); 948 | document.body.textContent = ''; 949 | assert( 950 | actions.splice(0, actions.length).join(',') === 951 | [ 952 | 'attributeChanged', 953 | 'attributeChanged', 954 | 'attributeChanged', 955 | 'attributeChanged', 956 | 'attributeChanged', 957 | 'disconnected' 958 | ].join(','), 959 | 'expected actions with class too' 960 | ); 961 | document.body.innerHTML = ``; 962 | assert( 963 | actions.splice(0, actions.length).join(',') === 964 | [ 965 | 'created', 'attributeChanged', 'connected' 966 | ].join(','), 967 | 'attributes are notified if already there' 968 | ); 969 | const TestNode = customElements.get('test-node'); 970 | global.document = document; 971 | const tn = new TestNode(); 972 | delete global.document; 973 | assert(tn.nodeName === 'test-node', 'custom elements can be initialized via new'); 974 | 975 | customElements.whenDefined('test-node-v0').then(() => { 976 | document.body.innerHTML = ``; 977 | document.body.firstChild.setAttribute('test', '123'); 978 | assert( 979 | document.body.firstChild.getAttribute('test') === '123', 980 | 'attribute sets without throwing' 981 | ); 982 | customElements.whenDefined('test-clone-outer').then(() => { 983 | global.document = document; 984 | document.body.innerHTML = ''; 985 | document.body.firstElementChild.removeAttribute('test'); 986 | const value = document.body.innerHTML; 987 | assert(value === 'wut', 'nested content is OK'); 988 | document.body.appendChild(document.body.firstElementChild.cloneNode(true)); 989 | assert(document.body.innerHTML === (value + value), 'cloned content is not repeated'); 990 | delete global.document; 991 | customElements.whenDefined('built-in').then(() => { 992 | const div = document.createElement('div', {is: 'built-in'}); 993 | assert(div.outerHTML === '
    ', 'built-in extends work too'); 994 | done(); 995 | }); 996 | }); 997 | }); 998 | }); 999 | }) 1000 | .then(() => { 1001 | try { 1002 | customElements.define('test-node', class extends HTMLElement {}); 1003 | assert(false, 'this should not happen'); 1004 | } catch(e) { 1005 | assert(true, 'you cannot define same element twice'); 1006 | } 1007 | }).then(() => { 1008 | require('./sizzle.js'); 1009 | }); 1010 | 1011 | const actions = []; 1012 | customElements.define('test-empty', class extends HTMLElement {}); 1013 | customElements.define('test-node-v0', class extends HTMLElement { 1014 | attributeChangedCallback() { 1015 | throw 'this should not be called'; 1016 | } 1017 | }); 1018 | customElements.define('test-node', class extends HTMLElement { 1019 | static get observedAttributes() { 1020 | return ['class', 'test']; 1021 | } 1022 | constructor(...args) { 1023 | super(...args); 1024 | actions.push('created'); 1025 | } 1026 | connectedCallback() { 1027 | actions.push('connected'); 1028 | } 1029 | disconnectedCallback() { 1030 | actions.push('disconnected'); 1031 | } 1032 | attributeChangedCallback() { 1033 | actions.push('attributeChanged'); 1034 | } 1035 | }); 1036 | customElements.define('test-clone-outer', class extends HTMLElement { 1037 | constructor() { 1038 | super(); 1039 | this.innerHTML = ''; 1040 | this.setAttribute('test', 'value'); 1041 | } 1042 | }); 1043 | customElements.define('test-clone-inner', class extends HTMLElement { 1044 | constructor() { 1045 | super().textContent = 'wut'; 1046 | } 1047 | }); 1048 | customElements.define('built-in', class extends HTMLElement {}, {extends: 'div'}); 1049 | //*/ 1050 | 1051 | require('./textarea'); 1052 | require('./style'); 1053 | require('./void'); 1054 | 1055 | require('./issue-56'); 1056 | -------------------------------------------------------------------------------- /test/custom-element.js: -------------------------------------------------------------------------------- 1 | const {Document, HTMLElement} = require('../basichtml.js'); 2 | 3 | const document = new Document(); 4 | const customElements = document.customElements; 5 | 6 | customElements.define('test-component', class extends HTMLElement { 7 | connectedCallback() { 8 | this.innerHTML = `Hello ${this.getAttribute('greet')}!`; 9 | } 10 | }); 11 | 12 | document.body.innerHTML = ''; 13 | 14 | console.log(document.toString()); 15 | 16 | customElements.define('test-node', class extends HTMLElement { 17 | 18 | static get observedAttributes() { 19 | return ['class', 'test']; 20 | } 21 | 22 | constructor(...args) { 23 | super(...args); 24 | console.log('created'); 25 | } 26 | 27 | connectedCallback() { 28 | console.log('connected'); 29 | } 30 | 31 | disconnectedCallback() { 32 | console.log('disconnected'); 33 | } 34 | 35 | attributeChangedCallback() { 36 | console.log('attributeChanged', arguments); 37 | } 38 | 39 | }); 40 | 41 | document.body.innerHTML = ``; 42 | 43 | const test = document.body.firstChild; 44 | test.setAttribute('nope', 123); 45 | test.setAttribute('test', 123); 46 | test.setAttribute('test', 123); 47 | var attr = test.getAttributeNode('test'); 48 | attr.value = 456; 49 | attr = document.createAttribute('test'); 50 | test.setAttributeNode(attr); 51 | attr.value = 345; 52 | console.log(''); 53 | test.setAttribute('class', 123); 54 | test.setAttribute('class', 123); 55 | attr = test.getAttributeNode('class'); 56 | attr.value = 456; 57 | attr = document.createAttribute('class'); 58 | test.setAttributeNode(attr); 59 | attr.value = 345; 60 | test.removeAttribute('class'); 61 | console.log(test.outerHTML); 62 | document.body.textContent = ''; 63 | -------------------------------------------------------------------------------- /test/html.js: -------------------------------------------------------------------------------- 1 | const basicHTML = require('../basichtml.js'); 2 | const {Document} = basicHTML; 3 | const {document} = basicHTML.init(); 4 | 5 | const html = "

    Hello

    "; 6 | 7 | const tmp = document.createElement('div'); 8 | tmp.innerHTML = html; 9 | const {attributes, children} = tmp.firstElementChild; 10 | 11 | document.documentElement.textContent = ''; 12 | document.documentElement.attributes = attributes; 13 | document.documentElement.append(...children); 14 | console.assert(document.toString() === createDocumentFromHTML(html).toString()); 15 | 16 | // const {Document} = require('../basichtml.js'); 17 | function createDocumentFromHTML(html, customElements) { 18 | const document = new Document(customElements); 19 | const tmp = document.createElement('div'); 20 | tmp.innerHTML = html; 21 | const {attributes, children} = tmp.firstElementChild; 22 | document.documentElement.attributes = attributes; 23 | document.documentElement.append(...children); 24 | return document; 25 | } 26 | -------------------------------------------------------------------------------- /test/hyperhtml.js: -------------------------------------------------------------------------------- 1 | const {Document} = require('../basichtml'); 2 | const document = new Document(); 3 | 4 | // hyperHTML needs at least a global document 5 | // to perform an initial feature detection 6 | global.document = document; 7 | const hyperHTML = require('hyperhtml'); 8 | 9 | // most basic hyperHTML examples in node 10 | function tick(render) { 11 | console.log(render` 12 |
    13 |

    Hello, world!

    14 |

    It is ${new Date().toLocaleTimeString()}.

    15 |
    16 | `.innerHTML); 17 | } 18 | 19 | // start ticking 20 | setInterval(tick, 1000, hyperHTML.bind(document.body)); 21 | -------------------------------------------------------------------------------- /test/issue-56.js: -------------------------------------------------------------------------------- 1 | const { Document } = require("../"); 2 | 3 | const document = new Document(); 4 | 5 | // attributes 6 | document.documentElement.setAttribute("lang", "en"); 7 | 8 | // common accessors 9 | document.documentElement.innerHTML = ` 10 | 11 | 12 | `; 13 | document.body.textContent = "Hello basicHTML"; 14 | 15 | // basic querySelector / querySelectorAll 16 | document 17 | .querySelector("head") 18 | .appendChild(document.createElement("title")).textContent = "HTML on NodeJS"; 19 | 20 | // toString() necessary to read, it's a Buffer 21 | console.log(document.toString()); -------------------------------------------------------------------------------- /test/many-rows.js: -------------------------------------------------------------------------------- 1 | const {Document} = require('../basichtml.js'); 2 | 3 | global.document = new Document; 4 | const hyperHTML = require('hyperhtml'); 5 | 6 | document.documentElement.innerHTML = ` 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |

    Boot speed:

    15 |
    16 | 17 | 18 | `; 19 | 20 | const renderer = hyperHTML.bind(document.querySelector('#example')); 21 | 22 | const state = { 23 | start: null, 24 | items: [] 25 | }; 26 | 27 | function onClick() { 28 | console.time('items.reverse()'); 29 | state.items.reverse(); 30 | render(); 31 | console.timeEnd('items.reverse()'); 32 | } 33 | 34 | function render() { 35 | renderer` 36 |
    37 | 40 |

    ${ 41 | state.items.map((item, i) => hyperHTML.wire(item)` 42 |

    43 | ${ item.name } 44 |

    ` 45 | )}
    46 | ` 47 | } 48 | 49 | let amount = 1000; 50 | while(amount--) { 51 | state.items.push({ 52 | name: `item-${ amount }` 53 | }); 54 | } 55 | 56 | setTimeout(onClick, 500); // around 100ms 57 | setTimeout(onClick, 1000);// around 25ms 58 | -------------------------------------------------------------------------------- /test/sizzle.js: -------------------------------------------------------------------------------- 1 | const {title, assert, async, log} = require('tressa'); 2 | const HTML = require('../basichtml.js'); 3 | 4 | HTML.init({ 5 | selector: { 6 | name: 'sizzle', // will be required 7 | $(Sizzle, element, css) { 8 | return Sizzle(css, element); 9 | } 10 | } 11 | }); 12 | 13 | title('basicHTML & Sizzle'); 14 | document.documentElement.innerHTML = 15 | `sizzle 16 |

    content

    `; 17 | 18 | assert( 19 | document.body.querySelector('[attr="value"]') === 20 | document.body.firstElementChild, 21 | 'it can retrieve via core unsupported selectors' 22 | ); 23 | 24 | // for code coverage sake 25 | global.self = global; 26 | HTML.init(); 27 | 28 | const {Image} = HTML.init({ 29 | selector: { 30 | module: () => require('sizzle'), 31 | $(Sizzle, element, css) { 32 | return Sizzle(css, element); 33 | } 34 | } 35 | }); 36 | 37 | assert(new Image(3).width === 3); 38 | -------------------------------------------------------------------------------- /test/style.js: -------------------------------------------------------------------------------- 1 | const {document} = require('../basichtml.js').init(); 2 | 3 | document.documentElement.innerHTML = ` 4 | 5 | 6 | 7 | `; 8 | 9 | console.assert(document.querySelector('style').childNodes[0].nodeType === 3); 10 | -------------------------------------------------------------------------------- /test/textarea.js: -------------------------------------------------------------------------------- 1 | const {document} = require('../basichtml.js').init(); 2 | 3 | document.documentElement.innerHTML = ` 4 | 5 | 6 | 7 | `; 8 | 9 | console.assert(document.querySelector('textarea').childNodes[0].nodeType === 3); 10 | -------------------------------------------------------------------------------- /test/void.js: -------------------------------------------------------------------------------- 1 | const {document} = require('../basichtml.js').init(); 2 | 3 | document.body.innerHTML = `a
    b
    c
    d
    e`; 4 | 5 | console.assert(document.body.innerHTML === 'a
    b
    c
    d
    e', 'void elements work as expected'); 6 | -------------------------------------------------------------------------------- /test/xmlish.js: -------------------------------------------------------------------------------- 1 | const {Document} = require('../basichtml.js'); 2 | 3 | const document = new Document(); 4 | 5 | document.documentElement.innerHTML = ''; 6 | console.log(document.toString()); --------------------------------------------------------------------------------