├── .github └── workflows │ └── node.js.yml ├── .gitignore ├── .npmignore ├── .npmrc ├── LICENSE ├── LOGO.txt ├── README.md ├── cjs └── package.json ├── docs ├── README.md └── uhtml-head.jpg ├── esm ├── create-content.js ├── creator.js ├── dom │ ├── array.js │ ├── attribute.js │ ├── character-data.js │ ├── comment.js │ ├── document-fragment.js │ ├── document-type.js │ ├── document.js │ ├── dom-parser.js │ ├── element.js │ ├── event.js │ ├── index.js │ ├── named-node-map.js │ ├── node.js │ ├── parent.js │ ├── range.js │ ├── string-map.js │ ├── string-parser.js │ ├── svg-element.js │ ├── symbols.js │ ├── text.js │ ├── token-list.js │ ├── tree-walker.js │ └── utils.js ├── handler.js ├── index.js ├── keyed.js ├── literals.js ├── node.js ├── parser.js ├── persistent-fragment.js ├── rabbit.js ├── range.js ├── reactive.js ├── reactive │ ├── preact.js │ └── signal.js ├── render │ ├── hole.js │ ├── keyed.js │ ├── reactive.js │ └── shared.js ├── ssr.js └── utils.js ├── package-lock.json ├── package.json ├── rollup ├── es.config.js ├── exports.cjs ├── init.cjs ├── init.config.js ├── ssr.cjs ├── ssr.config.js └── ts.fix.js ├── test ├── async.html ├── benchmark │ ├── content.js │ ├── dom.html │ ├── linkedom-cached.js │ ├── linkedom.js │ └── w3c.html ├── blank-template.html ├── coverage.js ├── csp.html ├── custom-element.html ├── dataset.html ├── dbmonster.css ├── dbmonster.html ├── dbmonster.js ├── diffing.js ├── dom │ ├── attribute.js │ ├── comment.js │ ├── document-fragment.js │ ├── document-type.js │ ├── document.js │ ├── dom-parser.js │ ├── element.js │ ├── event.js │ ├── named-node-map.js │ ├── node.js │ ├── package.json │ ├── parent.js │ ├── range.js │ ├── text.js │ └── utils.js ├── empty.html ├── fragment.html ├── fw-bench │ ├── css │ │ ├── bootstrap │ │ │ └── dist │ │ │ │ ├── css │ │ │ │ ├── bootstrap.min.css │ │ │ │ └── bootstrap.min.css.map │ │ │ │ └── fonts │ │ │ │ ├── glyphicons-halflings-regular.ttf │ │ │ │ ├── glyphicons-halflings-regular.woff │ │ │ │ └── glyphicons-halflings-regular.woff2 │ │ ├── currentStyle.css │ │ └── main.css │ ├── dist │ │ └── index.js │ ├── index.html │ ├── package-lock.json │ ├── package.json │ ├── rollup.config.js │ └── src │ │ ├── index.js │ │ ├── jumbotron.js │ │ ├── table-delegate.js │ │ ├── table-tr.js │ │ └── table.js ├── index.html ├── index.js ├── issue-102 │ └── index.html ├── issue-103 │ ├── data.js │ └── index.html ├── issue-91 │ └── index.html ├── issue-96.html ├── issue-98 │ └── index.html ├── json.html ├── modern.html ├── modern.mjs ├── mondrian.css ├── mondrian.html ├── mondrian.js ├── node.html ├── object.html ├── object.native.html ├── package-lock.json ├── package.json ├── paranoia.html ├── preactive.html ├── ref.html ├── render-roots.html ├── repeat.html ├── select.html ├── semi-direct.html ├── shadow-root.html ├── shenanigans.html ├── shuffled.html ├── signal.html ├── ssr.mjs ├── svg.html ├── svg.mjs ├── test.html ├── textarea.html └── usignal.html └── tsconfig.json /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: build 5 | 6 | on: [push, pull_request] 7 | 8 | jobs: 9 | build: 10 | 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | node-version: [20] 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Use Node.js ${{ matrix.node-version }} 20 | uses: actions/setup-node@v4 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | cache: 'npm' 24 | cache-dependency-path: '**/package-lock.json' 25 | - run: npm ci 26 | - run: npm run build --if-present 27 | - run: npm run coverage --if-present 28 | - name: Coveralls 29 | uses: coverallsapp/github-action@master 30 | with: 31 | github-token: ${{ secrets.GITHUB_TOKEN }} 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .nyc_output/ 2 | coverage/ 3 | node_modules/ 4 | types/ 5 | cjs/* 6 | !cjs/package.json 7 | dom.js 8 | esm/init*.js 9 | init*.js 10 | worker.js 11 | keyed.js 12 | !esm/keyed.js 13 | !esm/dom/keyed.js 14 | index.js 15 | !esm/index.js 16 | !esm/dom/index.js 17 | node.js 18 | !esm/node.js 19 | !esm/dom/node.js 20 | !test/dom/node.js 21 | reactive.js 22 | !esm/reactive.js 23 | !esm/render/reactive.js 24 | signal.js 25 | !esm/signal.js 26 | !esm/render/signal.js 27 | preactive.js 28 | !test/preactive.js -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .github/ 2 | .nyc_output/ 3 | coverage/ 4 | docs/ 5 | node_modules/ 6 | rollup/ 7 | test/ 8 | package-lock.json 9 | uhtml-head.jpg 10 | LOGO.txt 11 | .travis.yml 12 | DOCUMENTATION.md 13 | V0.md 14 | tsconfig.json 15 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=true 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright © 2020-today, Andrea Giammarchi, @WebReflection 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the “Software”), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included 13 | in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 21 | IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /LOGO.txt: -------------------------------------------------------------------------------- 1 | _ _ _ 2 | _ _| |_| |_ _____| | 3 | | | | | _| | | 4 | |___|_|_|_| |_|_|_|_| 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # µhtml 2 | 3 | [![Downloads](https://img.shields.io/npm/dm/uhtml.svg)](https://www.npmjs.com/package/uhtml) [![build status](https://github.com/WebReflection/uhtml/actions/workflows/node.js.yml/badge.svg)](https://github.com/WebReflection/uhtml/actions) [![Coverage Status](https://coveralls.io/repos/github/WebReflection/uhtml/badge.svg?branch=main)](https://coveralls.io/github/WebReflection/uhtml?branch=main) [![CSP strict](https://webreflection.github.io/csp/strict.svg)](https://webreflection.github.io/csp/#-csp-strict) 4 | 5 | ![snow flake](./docs/uhtml-head.jpg) 6 | 7 | **Social Media Photo by [Andrii Ganzevych](https://unsplash.com/@odya_kun) on [Unsplash](https://unsplash.com/)** 8 | 9 | *uhtml* (micro *µ* html) is one of the smallest, fastest, memory consumption friendly, yet zero-tools based, library to safely help creating or manipulating DOM content. 10 | 11 | ### 📣 uhtml v4 is out 12 | 13 | **[Documentation](https://webreflection.github.io/uhtml/)** 14 | 15 | **[Release Notes](https://github.com/WebReflection/uhtml/pull/86)** 16 | 17 | - - - 18 | 19 | ### Exports 20 | 21 | * **[uhtml](https://cdn.jsdelivr.net/npm/uhtml/index.js)** as default `{ Hole, render, html, svg, attr }` with smart auto-keyed nodes - read [keyed or not ?](https://webreflection.github.io/uhtml/#keyed-or-not-) paragraph to know more 22 | * **[uhtml/keyed](https://cdn.jsdelivr.net/npm/uhtml/keyed.js)** with extras `{ Hole, render, html, svg, htmlFor, svgFor, attr }`, providing keyed utilities - read [keyed or not ?](https://webreflection.github.io/uhtml/#keyed-or-not-) paragraph to know more 23 | * **[uhtml/node](https://cdn.jsdelivr.net/npm/uhtml/node.js)** with *same default* exports but it's for *one-off* nodes creation only so that no cache or updates are available and it's just an easy way to hook *uhtml* into your existing project for DOM creation (not manipulation!) 24 | * **[uhtml/init](https://cdn.jsdelivr.net/npm/uhtml/init.js)** which returns a `document => uhtml/keyed` utility that can be bootstrapped with `uhtml/dom`, [LinkeDOM](https://github.com/WebReflection/linkedom), [JSDOM](https://github.com/jsdom/jsdom) for either *SSR* or *Workers* support 25 | * **uhtml/ssr** which exports an utility that both SSR or Workers can use to parse and serve documents. This export provides same keyed utilities except the keyed feature is implicitly disabled as that's usually not desirable at all for SSR or rendering use cases, actually just an overhead. This might change in the future but for now I want to benchmark and see how competitive is `uhtml/ssr` out there. The `uhtml/dom` is also embedded in this export because the `Comment` class needs an override to produce a super clean output (at least until hydro story is up and running). 26 | * **[uhtml/dom](https://cdn.jsdelivr.net/npm/uhtml/dom.js)** which returns a specialized *uhtml* compliant DOM environment that can be passed to the `uhtml/init` export to have 100% same-thing running on both client or Web Worker / Server. This entry exports `{ Document, DOMParser }` where the former can be used to create a new *document* while the latter one can parse well formed HTML or SVG content and return the document out of the box. 27 | * **[uhtml/reactive](https://cdn.jsdelivr.net/npm/uhtml/reactive.js)** which allows usage of symbols within the optionally *keyed* render function. The only difference with other exports, beside exporting a `reactive` field instead of `render`, so that `const render = reactive(effect)` creates a reactive render per each library, is that the `render(where, () => what)`, with a function as second argument is mandatory when the rendered stuff has signals in it, otherwise these can't side-effect properly. 28 | * **[uhtml/signal](https://cdn.jsdelivr.net/npm/uhtml/signal.js)** is an already bundled `uhtml/reactive` with `@webreflection/signal` in it, so that its `render` exported function is already reactive. This is the smallest possible bundle as it's ~3.3Kb but it's not nearly as complete, in terms of features, as *preact* signals are. 29 | * **[uhtml/preactive](https://cdn.jsdelivr.net/npm/uhtml/preactive.js)** is an already bundled `uhtml/reactive` with `@preact/signals-core` in it, so that its `render` exported function, among all other *preact* related exports, is already working. This is a *drop-in* replacement with extra *Preact signals* goodness in it so you can start small with *uhtml/signal* and switch any time to this more popular solution. 30 | 31 | ### uhtml/init example 32 | 33 | ```js 34 | import init from 'uhtml/init'; 35 | import { Document } from 'uhtml/dom'; 36 | 37 | const document = new Document; 38 | 39 | const { 40 | Hole, 41 | render, 42 | html, svg, 43 | htmlFor, svgFor, 44 | attr 45 | } = init(document); 46 | ``` 47 | 48 | ### uhtml/preactive example 49 | 50 | ```js 51 | import { render, html, signal, detach } from 'uhtml/preactive'; 52 | 53 | const count = signal(0); 54 | 55 | render(document.body, () => html` 56 | 59 | `); 60 | 61 | // stop reacting to signals in the future 62 | setTimeout(() => { 63 | detach(document.body); 64 | }, 10000); 65 | ``` 66 | -------------------------------------------------------------------------------- /cjs/package.json: -------------------------------------------------------------------------------- 1 | {"type":"commonjs"} -------------------------------------------------------------------------------- /docs/uhtml-head.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WebReflection/uhtml/fbae9184611abc730cebe325542dc355a8317d72/docs/uhtml-head.jpg -------------------------------------------------------------------------------- /esm/create-content.js: -------------------------------------------------------------------------------- 1 | import { SVG_NAMESPACE, newRange } from './utils.js'; 2 | 3 | let template = document.createElement('template'), svg, range; 4 | 5 | /** 6 | * @param {string} text 7 | * @param {boolean} xml 8 | * @returns {DocumentFragment} 9 | */ 10 | export default (text, xml) => { 11 | if (xml) { 12 | if (!svg) { 13 | svg = document.createElementNS(SVG_NAMESPACE, 'svg'); 14 | range = newRange(); 15 | range.selectNodeContents(svg); 16 | } 17 | return range.createContextualFragment(text); 18 | } 19 | template.innerHTML = text; 20 | const { content } = template; 21 | template = template.cloneNode(false); 22 | return content; 23 | }; 24 | -------------------------------------------------------------------------------- /esm/creator.js: -------------------------------------------------------------------------------- 1 | import { PersistentFragment } from './persistent-fragment.js'; 2 | import { bc, detail } from './literals.js'; 3 | import { array, hole } from './handler.js'; 4 | import { empty, find } from './utils.js'; 5 | import { cache } from './literals.js'; 6 | 7 | /** @param {(template: TemplateStringsArray, values: any[]) => import("./parser.js").Resolved} parse */ 8 | export default parse => ( 9 | /** 10 | * @param {TemplateStringsArray} template 11 | * @param {any[]} values 12 | * @returns {import("./literals.js").Cache} 13 | */ 14 | (template, values) => { 15 | const { a: fragment, b: entries, c: direct } = parse(template, values); 16 | const root = document.importNode(fragment, true); 17 | /** @type {import("./literals.js").Detail[]} */ 18 | let details = empty; 19 | if (entries !== empty) { 20 | details = []; 21 | for (let current, prev, i = 0; i < entries.length; i++) { 22 | const { a: path, b: update, c: name } = entries[i]; 23 | const node = path === prev ? current : (current = find(root, (prev = path))); 24 | details[i] = detail( 25 | update, 26 | node, 27 | name, 28 | update === array ? [] : (update === hole ? cache() : null) 29 | ); 30 | } 31 | } 32 | return bc( 33 | direct ? root.firstChild : new PersistentFragment(root), 34 | details, 35 | ); 36 | } 37 | ); 38 | -------------------------------------------------------------------------------- /esm/dom/array.js: -------------------------------------------------------------------------------- 1 | import { DOCUMENT_FRAGMENT_NODE } from 'domconstants/constants'; 2 | 3 | import { nodeType, childNodes } from './symbols.js'; 4 | 5 | export const push = (array, nodes) => { 6 | array.push(...nodes.flatMap(withoutFragments)); 7 | }; 8 | 9 | export const splice = (array, start, drop, nodes) => { 10 | array.splice(start, drop, ...nodes.flatMap(withoutFragments)); 11 | }; 12 | 13 | export const unshift = (array, nodes) => { 14 | array.unshift(...nodes.flatMap(withoutFragments)); 15 | }; 16 | 17 | const withoutFragments = node => ( 18 | node[nodeType] === DOCUMENT_FRAGMENT_NODE ? 19 | node[childNodes].splice(0) : node 20 | ); 21 | -------------------------------------------------------------------------------- /esm/dom/attribute.js: -------------------------------------------------------------------------------- 1 | import { ATTRIBUTE_NODE } from 'domconstants/constants'; 2 | 3 | import { escape } from 'html-escaper'; 4 | 5 | import Node from './node.js'; 6 | 7 | import { name, value, ownerElement, ownerDocument } from './symbols.js'; 8 | 9 | /** @typedef {Attribute} Attribute */ 10 | 11 | export default class Attribute extends Node { 12 | constructor(nodeName, nodeValue = '', owner = null) { 13 | super(ATTRIBUTE_NODE, owner?.[ownerDocument]); 14 | this[ownerElement] = owner; 15 | this[name] = nodeName; 16 | this.value = nodeValue; 17 | } 18 | 19 | /** @type {import("./element.js").default?} */ 20 | get ownerElement() { 21 | return this[ownerElement]; 22 | } 23 | 24 | /** @type {string} */ 25 | get name() { 26 | return this[name]; 27 | } 28 | 29 | /** @type {string} */ 30 | get localName() { 31 | return this[name]; 32 | } 33 | 34 | /** @type {string} */ 35 | get nodeName() { 36 | return this[name]; 37 | } 38 | 39 | /** @type {string} */ 40 | get value() { 41 | return this[value]; 42 | } 43 | set value(any) { 44 | this[value] = String(any); 45 | } 46 | 47 | /** @type {string} */ 48 | get nodeValue() { 49 | return this[value]; 50 | } 51 | 52 | toString() { 53 | const { [name]: key, [value]: val } = this; 54 | return val === '' ? key : `${key}="${escape(val)}"`; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /esm/dom/character-data.js: -------------------------------------------------------------------------------- 1 | import Node from './node.js'; 2 | import { nodeName, value } from './symbols.js'; 3 | 4 | export default class CharacterData extends Node { 5 | constructor(type, name, data, owner) { 6 | super(type, owner)[nodeName] = name; 7 | this.data = data; 8 | } 9 | 10 | get data() { 11 | return this[value]; 12 | } 13 | set data(any) { 14 | this[value] = String(any); 15 | } 16 | 17 | get nodeName() { 18 | return this[nodeName]; 19 | } 20 | 21 | get textContent() { 22 | return this.data; 23 | } 24 | 25 | set textContent(data) { 26 | this.data = data; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /esm/dom/comment.js: -------------------------------------------------------------------------------- 1 | import { COMMENT_NODE } from 'domconstants/constants'; 2 | 3 | import CharacterData from './character-data.js'; 4 | import { ownerDocument, value } from './symbols.js'; 5 | 6 | export default class Comment extends CharacterData { 7 | constructor(data = '', owner = null) { 8 | super(COMMENT_NODE, '#comment', data, owner); 9 | } 10 | 11 | cloneNode() { 12 | return new Comment(this[value], this[ownerDocument]); 13 | } 14 | 15 | toString() { 16 | return ``; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /esm/dom/document-fragment.js: -------------------------------------------------------------------------------- 1 | import { DOCUMENT_FRAGMENT_NODE } from 'domconstants/constants'; 2 | 3 | import Parent from './parent.js'; 4 | 5 | import { cloned } from './utils.js'; 6 | import { childNodes, nodeName, ownerDocument } from './symbols.js'; 7 | 8 | export default class DocumentFragment extends Parent { 9 | constructor(owner = null) { 10 | super(DOCUMENT_FRAGMENT_NODE, owner)[nodeName] = '#document-fragment'; 11 | } 12 | 13 | get nodeName() { 14 | return this[nodeName]; 15 | } 16 | 17 | cloneNode(deep = false) { 18 | const fragment = new DocumentFragment(this[ownerDocument]); 19 | const { [childNodes]: nodes } = this; 20 | if (deep && nodes.length) 21 | fragment[childNodes] = nodes.map(cloned, fragment); 22 | return fragment; 23 | } 24 | 25 | toString() { 26 | return this[childNodes].join(''); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /esm/dom/document-type.js: -------------------------------------------------------------------------------- 1 | import { DOCUMENT_TYPE_NODE } from 'domconstants/constants'; 2 | 3 | import Node from './node.js'; 4 | import { nodeName } from './symbols.js'; 5 | 6 | export default class DocumentType extends Node { 7 | constructor(name, owner = null) { 8 | super(DOCUMENT_TYPE_NODE, owner)[nodeName] = name; 9 | } 10 | 11 | get nodeName() { 12 | return this[nodeName]; 13 | } 14 | 15 | get name() { 16 | return this[nodeName]; 17 | } 18 | 19 | toString() { 20 | const { [nodeName]: value } = this; 21 | return value ? `` : ''; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /esm/dom/document.js: -------------------------------------------------------------------------------- 1 | import { DOCUMENT_NODE } from 'domconstants/constants'; 2 | 3 | import { setParentNode } from './utils.js'; 4 | 5 | import { childNodes, documentElement, nodeName, ownerDocument } from './symbols.js'; 6 | 7 | import Attribute from './attribute.js'; 8 | import Comment from './comment.js'; 9 | import DocumentFragment from './document-fragment.js'; 10 | import DocumentType from './document-type.js'; 11 | import Element from './element.js'; 12 | import Event from './event.js'; 13 | import SVGElement from './svg-element.js'; 14 | import Parent from './parent.js'; 15 | import Range from './range.js'; 16 | import Text from './text.js'; 17 | import TreeWalker from './tree-walker.js'; 18 | 19 | const doctype = Symbol('doctype'); 20 | const head = Symbol('head'); 21 | const body = Symbol('body'); 22 | 23 | const defaultView = Object.create(globalThis, { 24 | Event: { value: Event }, 25 | }); 26 | 27 | /** @typedef {import("./attribute.js").Attribute} Attribute */ 28 | 29 | export default class Document extends Parent { 30 | constructor(type = 'html') { 31 | super(DOCUMENT_NODE, null)[nodeName] = '#document'; 32 | this[documentElement] = null; 33 | this[doctype] = null; 34 | this[head] = null; 35 | this[body] = null; 36 | if (type === 'html') { 37 | const html = (this[documentElement] = new Element(type, this)); 38 | this[childNodes] = [ 39 | (this[doctype] = new DocumentType(type, this)), 40 | html 41 | ].map(setParentNode, this) 42 | html[childNodes] = [ 43 | (this[head] = new Element('head', this)), 44 | (this[body] = new Element('body', this)), 45 | ].map(setParentNode, html); 46 | } 47 | } 48 | 49 | /** @type {globalThis} */ 50 | get defaultView() { 51 | return defaultView; 52 | } 53 | 54 | /** @type {import("./document-type.js").default?} */ 55 | get doctype() { 56 | return this[doctype]; 57 | } 58 | 59 | /** @type {import("./element.js").default?} */ 60 | get documentElement() { 61 | return this[documentElement]; 62 | } 63 | 64 | /** @type {import("./element.js").default?} */ 65 | get head() { 66 | return this[head]; 67 | } 68 | 69 | /** @type {import("./element.js").default?} */ 70 | get body() { 71 | return this[body]; 72 | } 73 | 74 | /** @type {Attribute} */ 75 | createAttribute(name) { 76 | const attribute = new Attribute(name); 77 | attribute[ownerDocument] = this; 78 | return attribute; 79 | } 80 | 81 | /** @type {import("./comment.js").default} */ 82 | createComment(data) { 83 | return new Comment(data, this); 84 | } 85 | 86 | /** @type {import("./document-fragment.js").default} */ 87 | createDocumentFragment() { 88 | return new DocumentFragment(this); 89 | } 90 | 91 | /** @type {import("./element.js").default} */ 92 | createElement(name, options = null) { 93 | const element = new Element(name, this); 94 | if (options?.is) element.setAttribute('is', options.is); 95 | return element; 96 | } 97 | 98 | /** @type {import("./svg-element.js").default} */ 99 | createElementNS(_, name) { 100 | return new SVGElement(name, this); 101 | } 102 | 103 | /** @type {globalThis.Range} */ 104 | createRange() { 105 | return new Range; 106 | } 107 | 108 | /** @type {import("./text.js").default} */ 109 | createTextNode(data) { 110 | return new Text(data, this); 111 | } 112 | 113 | /** @type {globalThis.TreeWalker} */ 114 | createTreeWalker(parent, accept) { 115 | return new TreeWalker(parent, accept); 116 | } 117 | 118 | /** 119 | * Same as `document.importNode` 120 | * @template T 121 | * @param {T} externalNode 122 | * @param {boolean} deep 123 | * @returns {T} 124 | */ 125 | importNode(externalNode, deep = false) { 126 | return externalNode.cloneNode(deep); 127 | } 128 | 129 | toString() { 130 | return this[childNodes].join(''); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /esm/dom/dom-parser.js: -------------------------------------------------------------------------------- 1 | import Document from './document.js'; 2 | import Element from './element.js'; 3 | import SVGElement from './element.js'; 4 | import { Node, parse, setAttributes, setChild } from './string-parser.js'; 5 | import { documentElement, nodeName, ownerDocument } from './symbols.js'; 6 | 7 | import { isSVG } from './utils.js'; 8 | 9 | class Root extends Node { 10 | constructor(svg) { 11 | const document = new Document(svg ? '' : 'html'); 12 | const node = svg ? new SVGElement('svg', document) : document.body; 13 | if (svg) document[documentElement] = node; 14 | super(node, svg); 15 | } 16 | 17 | onprocessinginstruction(name, data) { 18 | const { D: document } = this; 19 | switch (name) { 20 | case '!doctype': 21 | case '!DOCTYPE': 22 | document.doctype[nodeName] = data.slice(name.length).trim(); 23 | break; 24 | } 25 | } 26 | 27 | onopentag(name, attributes) { 28 | const { D: document, n: node, s: svg } = this; 29 | let child; 30 | if (svg || isSVG(name)) { 31 | switch (name) { 32 | case 'svg': 33 | case 'SVG': 34 | child = document.documentElement; 35 | break; 36 | default: 37 | child = setChild(node, new SVGElement('svg', document)); 38 | break; 39 | } 40 | this.s = true; 41 | } 42 | else { 43 | switch (name) { 44 | case 'html': 45 | case 'HTML': 46 | child = document.documentElement; 47 | break; 48 | case 'head': 49 | case 'HEAD': 50 | child = document.head; 51 | break; 52 | case 'body': 53 | case 'BODY': 54 | child = document.body; 55 | break; 56 | default: 57 | child = setChild(node, new Element(name, document)); 58 | break; 59 | } 60 | } 61 | setAttributes(this.n = child, attributes); 62 | } 63 | } 64 | 65 | const parseDocument = (xmlMode, text) => { 66 | const handler = new Root(xmlMode); 67 | const { D: document } = handler; 68 | parse(handler, xmlMode, text); 69 | if (xmlMode) document[ownerDocument] = null; 70 | return document; 71 | }; 72 | 73 | export default class DOMParser { 74 | parseFromString(text, mimeType = 'text/html') { 75 | const html = mimeType === 'text/html'; 76 | if (html && text === '...') text = ''; 77 | return parseDocument(!html, text); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /esm/dom/element.js: -------------------------------------------------------------------------------- 1 | import { ELEMENT_NODE, TEXT_NODE } from 'domconstants/constants'; 2 | import { VOID_ELEMENTS } from 'domconstants/re'; 3 | 4 | import Attribute from './attribute.js'; 5 | import DocumentFragment from './document-fragment.js'; 6 | import Parent from './parent.js'; 7 | 8 | import namedNodeMap from './named-node-map.js'; 9 | import stringMap from './string-map.js'; 10 | import tokenList from './token-list.js'; 11 | 12 | import { parseString } from './string-parser.js'; 13 | import { cloned, setParentNode, withNewParent } from './utils.js'; 14 | 15 | import { attributes, name, value, localName, childNodes, nodeType, ownerDocument, ownerElement, parentNode } from './symbols.js'; 16 | 17 | const getAttributes = element => ( 18 | element[attributes] || (element[attributes] = new Map) 19 | ); 20 | 21 | /** @typedef {import("./attribute.js").Attribute} Attribute */ 22 | 23 | export default class Element extends Parent { 24 | constructor(name, owner = null) { 25 | super(ELEMENT_NODE, owner)[localName] = name; 26 | this[attributes] = null; 27 | } 28 | 29 | /** @type {globalThis.NamedNodeMap} */ 30 | get attributes() { 31 | return namedNodeMap(getAttributes(this)); 32 | } 33 | 34 | /** @type {globalThis.DOMStringMap} */ 35 | get dataset() { 36 | return stringMap(this); 37 | } 38 | 39 | /** @type {globalThis.DOMTokenList} */ 40 | get classList() { 41 | return tokenList(this); 42 | } 43 | 44 | /** @type {import("./document-fragment.js").default} */ 45 | get content() { 46 | const fragment = new DocumentFragment(this[ownerDocument]); 47 | const { [childNodes]: nodes } = this; 48 | if (nodes.length) 49 | fragment[childNodes] = nodes.map(cloned, fragment); 50 | return fragment; 51 | } 52 | 53 | /** @type {string} */ 54 | get localName() { 55 | return this[localName]; 56 | } 57 | 58 | /** @type {string} */ 59 | get nodeName() { 60 | return this[localName].toUpperCase(); 61 | } 62 | 63 | /** @type {string} */ 64 | get tagName() { 65 | return this[localName].toUpperCase(); 66 | } 67 | 68 | /** @type {string} */ 69 | get outerHTML() { 70 | return this.toString(); 71 | } 72 | 73 | // TODO: this is way too simple but it should work for uhtml 74 | /** @type {{cssText: string}} */ 75 | get style() { 76 | const self = this; 77 | return { 78 | get cssText() { 79 | return self.getAttribute('style') || ''; 80 | }, 81 | set cssText(value) { 82 | self.setAttribute('style', value); 83 | } 84 | }; 85 | } 86 | 87 | /** @type {string} */ 88 | get innerHTML() { 89 | return this[childNodes].join(''); 90 | } 91 | set innerHTML(text) { 92 | const fragment = parseString( 93 | this[ownerDocument].createDocumentFragment(), 94 | 'ownerSVGElement' in this, 95 | text 96 | ); 97 | this[childNodes] = fragment[childNodes].map(setParentNode, this); 98 | } 99 | 100 | /** @type {string} */ 101 | get textContent() { 102 | const data = []; 103 | for (const node of this[childNodes]) { 104 | switch (node[nodeType]) { 105 | case TEXT_NODE: 106 | data.push(node.data); 107 | break; 108 | case ELEMENT_NODE: 109 | data.push(node.textContent); 110 | break; 111 | } 112 | } 113 | return data.join(''); 114 | } 115 | set textContent(data) { 116 | this[childNodes].forEach(setParentNode, null); 117 | const text = this[ownerDocument].createTextNode(data); 118 | this[childNodes] = [setParentNode.call(this, text)]; 119 | } 120 | 121 | /** @type {string} */ 122 | get id() { 123 | return this.getAttribute('id') || ''; 124 | } 125 | set id(value) { 126 | this.setAttribute('id', value); 127 | } 128 | 129 | /** @type {string} */ 130 | get className() { 131 | return this.getAttribute('class') || ''; 132 | } 133 | set className(value) { 134 | this.setAttribute('class', value); 135 | } 136 | 137 | cloneNode(deep = false) { 138 | const element = new Element(this[localName], this[ownerDocument]); 139 | const { [attributes]: attrs, [childNodes]: nodes } = this; 140 | if (attrs) { 141 | const map = (element[attributes] = new Map); 142 | for (const { [name]: key, [value]: val } of this[attributes].values()) 143 | map.set(key, new Attribute(key, val, element)); 144 | } 145 | if (deep && nodes.length) 146 | element[childNodes] = nodes.map(cloned, element); 147 | return element; 148 | } 149 | 150 | /** 151 | * @param {string} name 152 | * @returns {string?} 153 | */ 154 | getAttribute(name) { 155 | const attribute = this[attributes]?.get(name); 156 | return attribute ? attribute.value : null; 157 | } 158 | 159 | /** 160 | * @param {string} name 161 | * @returns {Attribute?} 162 | */ 163 | getAttributeNode(name) { 164 | return this[attributes]?.get(name) || null 165 | } 166 | 167 | /** 168 | * @returns {string[]} 169 | */ 170 | getAttributeNames() { 171 | const { [attributes]: attrs } = this; 172 | return attrs ? [...attrs.keys()] : []; 173 | } 174 | 175 | /** 176 | * @param {string} name 177 | * @returns {boolean} 178 | */ 179 | hasAttribute(name) { 180 | return !!this[attributes]?.has(name); 181 | } 182 | 183 | /** 184 | * @returns {boolean} 185 | */ 186 | hasAttributes() { 187 | return !!this[attributes]?.size; 188 | } 189 | 190 | /** 191 | * @param {string} name 192 | */ 193 | removeAttribute(name) { 194 | const attribute = this[attributes]?.get(name); 195 | if (attribute) { 196 | attribute[ownerElement] = null; 197 | this[attributes].delete(name); 198 | } 199 | } 200 | 201 | /** 202 | * @param {Attribute} attribute 203 | */ 204 | removeAttributeNode(attribute) { 205 | this[attributes]?.delete(attribute[name]); 206 | attribute[ownerElement] = null; 207 | } 208 | 209 | /** 210 | * @param {string} name 211 | * @param {string} value 212 | */ 213 | setAttribute(name, value) { 214 | const attributes = getAttributes(this); 215 | const attribute = attributes.get(name); 216 | if (attribute) 217 | attribute.value = value; 218 | else { 219 | const attribute = new Attribute(name, value, this); 220 | attributes.set(name, attribute); 221 | } 222 | } 223 | 224 | /** 225 | * @param {Attribute} attribute 226 | */ 227 | setAttributeNode(attribute) { 228 | attribute[ownerElement]?.removeAttributeNode(attribute); 229 | attribute[ownerElement] = this; 230 | getAttributes(this).set(attribute[name], attribute); 231 | } 232 | 233 | /** 234 | * @param {string} name 235 | * @param {boolean?} force 236 | * @returns {boolean} 237 | */ 238 | toggleAttribute(name, ...rest) { 239 | if (this.hasAttribute(name)) { 240 | if (!rest.at(0)) { 241 | this.removeAttribute(name); 242 | return false; 243 | } 244 | return true; 245 | } 246 | else if (rest.length < 1 || rest.at(0)) { 247 | this.setAttribute(name, ''); 248 | return true; 249 | } 250 | return false; 251 | } 252 | 253 | toString() { 254 | const { [localName]: name, [childNodes]: nodes, [attributes]: attrs } = this; 255 | const html = ['<', name]; 256 | if (attrs?.size) { 257 | for (const attribute of attrs.values()) 258 | html.push(' ', attribute); 259 | } 260 | html.push('>', ...nodes); 261 | if (!VOID_ELEMENTS.test(name)) 262 | html.push(''); 263 | return html.join(''); 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /esm/dom/event.js: -------------------------------------------------------------------------------- 1 | const _type = Symbol('type'); 2 | const _bubbles = Symbol('bubbles'); 3 | const _cancelable = Symbol('cancelable'); 4 | const _defaultPrevented = Symbol('defaultPrevented'); 5 | 6 | export const _target = Symbol('target'); 7 | export const _currentTarget = Symbol('currentTarget'); 8 | export const _stoppedPropagation = Symbol('stoppedPropagation'); 9 | export const _stoppedImmediatePropagation = Symbol('stoppedImmediatePropagation'); 10 | 11 | export default class Event { 12 | constructor(type, { bubbles = false, cancelable = false } = {}) { 13 | this[_type] = type; 14 | this[_bubbles] = bubbles; 15 | this[_cancelable] = cancelable; 16 | this[_target] = null; 17 | this[_currentTarget] = null; 18 | this[_defaultPrevented] = false; 19 | this[_stoppedPropagation] = false; 20 | this[_stoppedImmediatePropagation] = false; 21 | } 22 | get type() { return this[_type]; } 23 | get bubbles() { return this[_bubbles]; } 24 | get cancelable() { return this[_cancelable]; } 25 | get target() { return this[_target]; } 26 | get currentTarget() { return this[_currentTarget]; } 27 | get defaultPrevented() { return this[_defaultPrevented]; } 28 | 29 | preventDefault() { 30 | if (this[_cancelable]) 31 | this[_defaultPrevented] = true; 32 | } 33 | 34 | stopPropagation() { 35 | this[_stoppedPropagation] = true; 36 | } 37 | 38 | stopImmediatePropagation() { 39 | this.stopPropagation(); 40 | this[_stoppedImmediatePropagation] = true; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /esm/dom/index.js: -------------------------------------------------------------------------------- 1 | /*! (c) Andrea Giammarchi - MIT */ 2 | import Document from './document.js'; 3 | import DOMParser from './dom-parser.js'; 4 | 5 | export { Document, DOMParser }; 6 | -------------------------------------------------------------------------------- /esm/dom/named-node-map.js: -------------------------------------------------------------------------------- 1 | const { from } = Array; 2 | const { iterator } = Symbol; 3 | 4 | const asString = (_, i) => String(i); 5 | const isIndex = ({ size }, name) => /^\d+$/.test(name) && name < size; 6 | 7 | const namedNodeMapHandler = { 8 | get: (map, name) => { 9 | if (name === 'length') return map.size; 10 | if (name === iterator) return yieldAttributes.bind(map.values()); 11 | return map.get(name) || ( 12 | isIndex(map, name) ? 13 | [...map.values()][name] : 14 | void 0 15 | ); 16 | }, 17 | 18 | has: (map, name) => ( 19 | name === 'length' || 20 | name === iterator || 21 | map.has(name) || 22 | isIndex(map, name) 23 | ), 24 | 25 | ownKeys: map => [ 26 | ...from({ length: map.size }, asString), 27 | ...map.keys(), 28 | ], 29 | }; 30 | 31 | function* yieldAttributes() { 32 | for (const attribute of this) 33 | yield attribute; 34 | } 35 | 36 | export default attributes => new Proxy(attributes, namedNodeMapHandler); 37 | -------------------------------------------------------------------------------- /esm/dom/node.js: -------------------------------------------------------------------------------- 1 | import { 2 | ELEMENT_NODE, 3 | ATTRIBUTE_NODE, 4 | TEXT_NODE, 5 | COMMENT_NODE, 6 | DOCUMENT_NODE, 7 | DOCUMENT_FRAGMENT_NODE, 8 | } from 'domconstants/constants'; 9 | 10 | import { childNodes, nodeType, ownerDocument, parentNode } from './symbols.js'; 11 | import { changeParentNode, withNewParent } from './utils.js'; 12 | import { push, splice, unshift } from './array.js'; 13 | 14 | /** @typedef {string | Node} Child */ 15 | 16 | const map = (values, parent) => values.map(withNewParent, parent); 17 | 18 | export default class Node { 19 | static { 20 | this.ELEMENT_NODE = ELEMENT_NODE; 21 | this.ATTRIBUTE_NODE = ATTRIBUTE_NODE; 22 | this.TEXT_NODE = TEXT_NODE; 23 | this.COMMENT_NODE = COMMENT_NODE; 24 | this.DOCUMENT_NODE = DOCUMENT_NODE; 25 | this.DOCUMENT_FRAGMENT_NODE = DOCUMENT_FRAGMENT_NODE; 26 | } 27 | 28 | constructor(type, owner) { 29 | this[parentNode] = null; 30 | this[nodeType] = type; 31 | this[ownerDocument] = owner; 32 | } 33 | 34 | /** @type {import("./parent.js").default?} */ 35 | get parentNode() { 36 | return this[parentNode]; 37 | } 38 | 39 | /** @type {ELEMENT_NODE | ATTRIBUTE_NODE | TEXT_NODE | COMMENT_NODE | DOCUMENT_NODE | DOCUMENT_FRAGMENT_NODE} */ 40 | get nodeType() { 41 | return this[nodeType]; 42 | } 43 | 44 | /** @type {import("./document.js").default?} */ 45 | get ownerDocument() { 46 | return this[ownerDocument]; 47 | } 48 | 49 | /** @type {boolean} */ 50 | get isConnected() { 51 | let { [parentNode]: parent, [ownerDocument]: owner } = this; 52 | while (parent && parent !== owner) 53 | parent = parent[parentNode]; 54 | return parent === owner; 55 | } 56 | 57 | /** @type {import("./element.js").default?} */ 58 | get parentElement() { 59 | const { [parentNode]: parent } = this; 60 | return parent?.[nodeType] === ELEMENT_NODE ? parent : null; 61 | } 62 | 63 | /** @type {Node?} */ 64 | get previousSibling() { 65 | const nodes = this[parentNode]?.[childNodes]; 66 | if (nodes) { 67 | const i = nodes.indexOf(this); 68 | if (i > 0) return nodes[i - 1]; 69 | } 70 | return null; 71 | } 72 | 73 | /** @type {import("./element.js").default?} */ 74 | get previousElementSibling() { 75 | const nodes = this[parentNode]?.[childNodes]; 76 | if (nodes) { 77 | let i = nodes.indexOf(this); 78 | while (i-- && nodes[i][nodeType] !== ELEMENT_NODE); 79 | return i < 0 ? null : nodes[i]; 80 | } 81 | return null; 82 | } 83 | 84 | /** @type {Node?} */ 85 | get nextSibling() { 86 | const nodes = this[parentNode]?.[childNodes]; 87 | return nodes && nodes.at(nodes.indexOf(this) + 1) || null; 88 | } 89 | 90 | /** @type {import("./element.js").default?} */ 91 | get nextElementSibling() { 92 | const nodes = this[parentNode]?.[childNodes]; 93 | if (nodes) { 94 | let i = nodes.indexOf(this); 95 | while (++i < nodes.length && nodes[i][nodeType] !== ELEMENT_NODE); 96 | return i < nodes.length ? nodes[i] : null; 97 | } 98 | return null; 99 | } 100 | 101 | /** @type {Node[]} */ 102 | get childNodes() { 103 | return []; 104 | } 105 | 106 | /** 107 | * @param {...import("./node.js").Child} values 108 | */ 109 | after(...values) { 110 | const { [parentNode]: parent } = this; 111 | const { [childNodes]: nodes } = parent; 112 | const i = nodes.indexOf(this) + 1; 113 | if (i === nodes.length) push(nodes, map(values, parent)); 114 | else if (i) splice(nodes, i - 1, 0, map(values, parent)); 115 | } 116 | 117 | /** 118 | * @param {...import("./node.js").Child} values 119 | */ 120 | before(...values) { 121 | const { [parentNode]: parent } = this; 122 | const { [childNodes]: nodes } = parent; 123 | const i = nodes.indexOf(this); 124 | if (!i) unshift(nodes, map(values, parent)); 125 | else if (i > 0) splice(nodes, i, 0, map(values, parent)); 126 | } 127 | 128 | remove() { 129 | changeParentNode(this, null); 130 | } 131 | 132 | /** 133 | * @param {...Child} values 134 | */ 135 | replaceWith(...values) { 136 | const { [parentNode]: parent } = this; 137 | if (parent) { 138 | const { [childNodes]: nodes } = parent; 139 | splice(nodes, nodes.indexOf(this), 1, values.map(withNewParent, parent)); 140 | this[parentNode] = null; 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /esm/dom/range.js: -------------------------------------------------------------------------------- 1 | import { childNodes, ownerDocument, ownerElement, parentNode } from './symbols.js'; 2 | import { setParentNode } from './utils.js'; 3 | import { parseString } from './string-parser.js'; 4 | 5 | const start = Symbol('start'); 6 | const end = Symbol('end'); 7 | 8 | export default class Range { 9 | constructor() { 10 | this[ownerElement] = null; 11 | this[start] = null; 12 | this[end] = null; 13 | } 14 | 15 | setStartAfter(node) { 16 | this[start] = node.nextSibling; 17 | } 18 | 19 | setStartBefore(node) { 20 | this[start] = node; 21 | } 22 | 23 | setEndAfter(node) { 24 | this[end] = node; 25 | } 26 | 27 | deleteContents() { 28 | const { [start]: s, [end]: e } = this; 29 | const { [childNodes]: nodes } = s[parentNode]; 30 | const si = nodes.indexOf(s); 31 | this[start] = null; 32 | this[end] = null; 33 | nodes.splice(si, nodes.indexOf(e) + 1 - si).forEach(setParentNode, null); 34 | } 35 | 36 | selectNodeContents(node) { 37 | this[ownerElement] = node; 38 | } 39 | 40 | createContextualFragment(text) { 41 | const { [ownerElement]: context } = this; 42 | return parseString( 43 | context[ownerDocument].createDocumentFragment(), 44 | 'ownerSVGElement' in context, 45 | text 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /esm/dom/string-map.js: -------------------------------------------------------------------------------- 1 | const key = name => `data-${name.replace(/[A-Z]/g, U => `-${U.toLowerCase()}`)}`; 2 | const prop = name => name.slice(5).replace(/-([a-z])/g, (_, $1) => $1.toUpperCase()); 3 | const byData = name => name.startsWith('data-'); 4 | 5 | const stringMapHandler = { 6 | deleteProperty(element, name) { 7 | name = key(name); 8 | if (element.hasAttribute(name)) 9 | element.removeAttribute(name); 10 | return true; 11 | }, 12 | 13 | get(element, name) { 14 | return element.getAttribute(key(name)); 15 | }, 16 | 17 | has(element, name) { 18 | return element.hasAttribute(key(name)); 19 | }, 20 | 21 | ownKeys(element) { 22 | return element.getAttributeNames().filter(byData).map(prop); 23 | }, 24 | 25 | set(element, name, value) { 26 | element.setAttribute(key(name), value); 27 | return true; 28 | }, 29 | }; 30 | 31 | export default element => new Proxy(element, stringMapHandler); 32 | -------------------------------------------------------------------------------- /esm/dom/string-parser.js: -------------------------------------------------------------------------------- 1 | import * as HTMLParser2 from 'htmlparser2'; 2 | 3 | import { SVG_NAMESPACE } from '../utils.js'; 4 | 5 | import Comment from './comment.js'; 6 | import Text from './text.js'; 7 | 8 | import { localName, ownerDocument, parentNode } from './symbols.js'; 9 | import { getNodes, isSVG } from './utils.js'; 10 | 11 | const { Parser } = HTMLParser2; 12 | const { entries } = Object; 13 | 14 | export const setAttributes = (child, attributes) => { 15 | for (const [name, value] of entries(attributes)) 16 | child.setAttribute(name, value); 17 | }; 18 | 19 | export const setChild = (parent, child) => { 20 | child[parentNode] = parent; 21 | getNodes(parent).push(child); 22 | return child; 23 | }; 24 | 25 | export class Node { 26 | constructor(node, svg) { 27 | this.D = node[ownerDocument]; 28 | this.n = node; 29 | this.s = svg; 30 | this.d = true; 31 | } 32 | 33 | onopentag(name, attributes) { 34 | const { D: document, n: node, s: svg } = this; 35 | const asSVG = svg || isSVG(name); 36 | this.n = setChild( 37 | node, 38 | asSVG ? 39 | document.createElementNS(SVG_NAMESPACE, name) : 40 | document.createElement(name) 41 | ); 42 | if (asSVG) this.s = true; 43 | setAttributes(this.n, attributes); 44 | } 45 | 46 | onclosetag() { 47 | const { n: node, s: svg } = this; 48 | this.n = node[parentNode]; 49 | if (svg && isSVG(this.n[localName])) 50 | this.s = false; 51 | } 52 | 53 | oncomment(text) { 54 | const { D: document, n: node } = this; 55 | node.appendChild(new Comment(text, document)); 56 | } 57 | 58 | ontext(text) { 59 | const { D: document, n: node, d: data } = this; 60 | if (data) node.appendChild(new Text(text, document)); 61 | } 62 | 63 | oncdatastart() { this.d = false } 64 | oncdataend() { this.d = true } 65 | } 66 | 67 | export const parse = (handler, xmlMode, text) => { 68 | const content = new Parser(handler, { 69 | lowerCaseAttributeNames: false, 70 | decodeEntities: true, 71 | recognizeCDATA: true, 72 | xmlMode 73 | }); 74 | content.write(text); 75 | content.end(); 76 | }; 77 | 78 | export const parseString = (node, xmlMode, text) => { 79 | parse(new Node(node, xmlMode), xmlMode, text); 80 | return node; 81 | }; 82 | -------------------------------------------------------------------------------- /esm/dom/svg-element.js: -------------------------------------------------------------------------------- 1 | import { ELEMENT_NODE } from 'domconstants/constants'; 2 | import { escape } from 'html-escaper'; 3 | 4 | import Attribute from './attribute.js'; 5 | import Element from './element.js'; 6 | 7 | import { cloned, isSVG } from './utils.js'; 8 | 9 | import { 10 | attributes, 11 | name, value, 12 | localName, 13 | childNodes, 14 | ownerDocument, 15 | parentNode, 16 | } from './symbols.js'; 17 | 18 | export default class SVGElement extends Element { 19 | constructor(name, owner = null) { 20 | super(ELEMENT_NODE, owner)[localName] = name; 21 | } 22 | 23 | get ownerSVGElement() { 24 | let { [parentNode]: parent } = this; 25 | while (parent && !isSVG(parent[localName])) 26 | parent = parent[parentNode]; 27 | return parent; 28 | } 29 | 30 | cloneNode(deep = false) { 31 | const svg = new SVGElement(this[localName], this[ownerDocument]); 32 | const { [attributes]: attrs, [childNodes]: nodes } = this; 33 | if (attrs) { 34 | const map = (svg[attributes] = new Map); 35 | for (const { [name]: key, [value]: val } of this[attributes].values()) 36 | map.set(key, new Attribute(key, val, svg)); 37 | } 38 | if (deep && nodes.length) 39 | svg[childNodes] = nodes.map(cloned, svg); 40 | return svg; 41 | } 42 | 43 | toString() { 44 | const { [localName]: name, [childNodes]: nodes, [attributes]: attrs } = this; 45 | const svg = ['<', name]; 46 | if (attrs?.size) { 47 | for (const { name, value } of attrs.values()) 48 | svg.push(' ', name, '="', escape(value), '"'); 49 | } 50 | if (nodes.length || isSVG(name)) 51 | svg.push('>', ...nodes, ''); 52 | else 53 | svg.push(' />'); 54 | return svg.join(''); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /esm/dom/symbols.js: -------------------------------------------------------------------------------- 1 | export const localName = Symbol('localName'); 2 | export const nodeName = Symbol('nodeName'); 3 | export const nodeType = Symbol('nodeType'); 4 | export const documentElement = Symbol('documentElement'); 5 | export const ownerDocument = Symbol('ownerDocument'); 6 | export const ownerElement = Symbol('ownerElement'); 7 | export const childNodes = Symbol('childNodes'); 8 | export const parentNode = Symbol('parentNode'); 9 | export const attributes = Symbol('attributes'); 10 | export const name = Symbol('name'); 11 | export const value = Symbol('value'); 12 | -------------------------------------------------------------------------------- /esm/dom/text.js: -------------------------------------------------------------------------------- 1 | import { TEXT_NODE } from 'domconstants/constants'; 2 | import { TEXT_ELEMENTS } from 'domconstants/re'; 3 | import { escape } from 'html-escaper'; 4 | 5 | import CharacterData from './character-data.js'; 6 | import { parentNode, localName, ownerDocument, value } from './symbols.js'; 7 | 8 | export default class Text extends CharacterData { 9 | constructor(data = '', owner = null) { 10 | super(TEXT_NODE, '#text', data, owner); 11 | } 12 | 13 | cloneNode() { 14 | return new Text(this[value], this[ownerDocument]); 15 | } 16 | 17 | toString() { 18 | const { [parentNode]: parent, [value]: data } = this; 19 | return parent && TEXT_ELEMENTS.test(parent[localName]) ? 20 | data : escape(data); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /esm/dom/token-list.js: -------------------------------------------------------------------------------- 1 | import { empty } from '../utils.js'; 2 | 3 | const { entries, keys, values } = Object; 4 | const { forEach } = empty; 5 | 6 | const classes = element => { 7 | const { className } = element; 8 | return className ? className.split(/\s+/) : []; 9 | }; 10 | 11 | const update = (element, tokens) => { 12 | element.className = [...tokens].join(' '); 13 | }; 14 | 15 | const tokenListHandler = { 16 | get(element, name) { 17 | switch(name) { 18 | case 'length': return classes(element).length; 19 | case 'value': return element.className; 20 | case 'add': return add.bind(element); 21 | case 'contains': return contains.bind(element); 22 | case 'entries': return entries.bind(null, classes(element)); 23 | case 'forEach': return forEach.bind(classes(element)); 24 | case 'keys': return keys.bind(null, classes(element)); 25 | case 'remove': return remove.bind(element); 26 | case 'replace': return replace.bind(element); 27 | case 'toggle': return toggle.bind(element); 28 | case 'values': return values.bind(null, classes(element)); 29 | } 30 | } 31 | }; 32 | 33 | export default element => new Proxy(element, tokenListHandler); 34 | 35 | function add(...tokens) { 36 | update(this, new Set(classes(this).concat(tokens))); 37 | } 38 | 39 | function contains(token) { 40 | return classes(this).includes(token); 41 | } 42 | 43 | function remove(...tokens) { 44 | const previous = new Set(classes(this)); 45 | for (const token of tokens) previous.delete(token); 46 | update(this, previous); 47 | } 48 | 49 | function replace(oldToken, newToken) { 50 | const tokens = new Set(classes(this)); 51 | if (tokens.has(oldToken)) { 52 | tokens.delete(oldToken); 53 | tokens.add(newToken); 54 | return !update(this, tokens); 55 | } 56 | return false; 57 | } 58 | 59 | function toggle(token, force) { 60 | const tokens = new Set(classes(this)); 61 | if (tokens.has(token)) { 62 | if (force) return true; 63 | tokens.delete(token); 64 | update(this, tokens); 65 | } 66 | else if (force || arguments.length === 1) { 67 | tokens.add(token); 68 | return !update(this, tokens); 69 | } 70 | return false; 71 | } 72 | -------------------------------------------------------------------------------- /esm/dom/tree-walker.js: -------------------------------------------------------------------------------- 1 | import { COMMENT_NODE, ELEMENT_NODE } from 'domconstants/constants'; 2 | 3 | import { childNodes, nodeType } from './symbols.js'; 4 | 5 | const asType = (accept, type) => ( 6 | (type === ELEMENT_NODE && (accept & 0x1)) ? 7 | ELEMENT_NODE : 8 | (type === COMMENT_NODE && (accept & 0x80)) ? 9 | COMMENT_NODE : 0 10 | ); 11 | 12 | export default class TreeWalker { 13 | constructor(parent, accept) { 14 | this[childNodes] = walk(parent, accept); 15 | } 16 | nextNode() { 17 | const { value, done } = this[childNodes].next(); 18 | return done ? null : value; 19 | } 20 | } 21 | 22 | function* walk(parent, accept) { 23 | for (const node of parent[childNodes]) { 24 | switch (asType(accept, node[nodeType])) { 25 | case ELEMENT_NODE: 26 | yield node; 27 | yield* walk(node, accept); 28 | break; 29 | case COMMENT_NODE: 30 | yield node; 31 | break; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /esm/dom/utils.js: -------------------------------------------------------------------------------- 1 | import { DOCUMENT_FRAGMENT_NODE, ELEMENT_NODE } from 'domconstants/constants'; 2 | 3 | import { childNodes, nodeType, parentNode } from './symbols.js'; 4 | 5 | import { empty } from '../utils.js'; 6 | 7 | 8 | /** 9 | * @param {import("./element.js").default} element 10 | * @returns {boolean} 11 | */ 12 | export const asElement = ({ [nodeType]: type }) => type === ELEMENT_NODE; 13 | 14 | export const changeParentNode = (node, parent) => { 15 | if (node[nodeType] === DOCUMENT_FRAGMENT_NODE) 16 | node[childNodes].forEach(setParentNode, parent); 17 | else { 18 | if (node[parentNode]) { 19 | const { [childNodes]: nodes } = node[parentNode]; 20 | nodes.splice(nodes.indexOf(node), 1); 21 | } 22 | node[parentNode] = parent; 23 | } 24 | return node; 25 | }; 26 | 27 | export const getNodes = element => ( 28 | element[childNodes] === empty ? 29 | (element[childNodes] = []) : 30 | element[childNodes] 31 | ); 32 | 33 | export const isSVG = name => (name === 'svg' || name === 'SVG'); 34 | 35 | export function cloned(node) { 36 | return setParentNode.call(this, node.cloneNode(true)); 37 | } 38 | 39 | export function setParentNode(node) { 40 | 'use strict'; 41 | node[parentNode] = this; 42 | return node; 43 | }; 44 | 45 | export function withNewParent(node) { 46 | return changeParentNode(node, this); 47 | } 48 | -------------------------------------------------------------------------------- /esm/handler.js: -------------------------------------------------------------------------------- 1 | import udomdiff from 'udomdiff'; 2 | import { empty, gPD, isArray, set } from './utils.js'; 3 | import { diffFragment } from './persistent-fragment.js'; 4 | import drop from './range.js'; 5 | 6 | const setAttribute = (element, name, value) => 7 | element.setAttribute(name, value); 8 | 9 | /** 10 | * @param {Element} element 11 | * @param {string} name 12 | * @returns {void} 13 | */ 14 | export const removeAttribute = (element, name) => 15 | element.removeAttribute(name); 16 | 17 | /** 18 | * @template T 19 | * @param {Element} element 20 | * @param {T} value 21 | * @returns {T} 22 | */ 23 | export const aria = (element, value) => { 24 | for (const key in value) { 25 | const $ = value[key]; 26 | const name = key === 'role' ? key : `aria-${key}`; 27 | if ($ == null) removeAttribute(element, name); 28 | else setAttribute(element, name, $); 29 | } 30 | return value; 31 | }; 32 | 33 | let listeners; 34 | 35 | /** 36 | * @template T 37 | * @param {Element} element 38 | * @param {T} value 39 | * @param {string} name 40 | * @returns {T} 41 | */ 42 | export const at = (element, value, name) => { 43 | name = name.slice(1); 44 | if (!listeners) listeners = new WeakMap; 45 | const known = listeners.get(element) || set(listeners, element, {}); 46 | let current = known[name]; 47 | if (current && current[0]) element.removeEventListener(name, ...current); 48 | current = isArray(value) ? value : [value, false]; 49 | known[name] = current; 50 | if (current[0]) element.addEventListener(name, ...current); 51 | return value; 52 | }; 53 | 54 | /** @type {WeakMap} */ 55 | const holes = new WeakMap; 56 | 57 | /** 58 | * @template T 59 | * @param {import("./literals.js").Detail} detail 60 | * @param {T} value 61 | * @returns {T} 62 | */ 63 | export const hole = (detail, value) => { 64 | const { t: node, n: hole } = detail; 65 | let nullish = false; 66 | switch (typeof value) { 67 | case 'object': 68 | if (value !== null) { 69 | (hole || node).replaceWith((detail.n = value.valueOf())); 70 | break; 71 | } 72 | case 'undefined': 73 | nullish = true; 74 | default: 75 | node.data = nullish ? '' : value; 76 | if (hole) { 77 | detail.n = null; 78 | hole.replaceWith(node); 79 | } 80 | break; 81 | } 82 | return value; 83 | }; 84 | 85 | /** 86 | * @template T 87 | * @param {Element} element 88 | * @param {T} value 89 | * @returns {T} 90 | */ 91 | export const className = (element, value) => maybeDirect( 92 | element, value, value == null ? 'class' : 'className' 93 | ); 94 | 95 | /** 96 | * @template T 97 | * @param {Element} element 98 | * @param {T} value 99 | * @returns {T} 100 | */ 101 | export const data = (element, value) => { 102 | const { dataset } = element; 103 | for (const key in value) { 104 | if (value[key] == null) delete dataset[key]; 105 | else dataset[key] = value[key]; 106 | } 107 | return value; 108 | }; 109 | 110 | /** 111 | * @template T 112 | * @param {Element | CSSStyleDeclaration} ref 113 | * @param {T} value 114 | * @param {string} name 115 | * @returns {T} 116 | */ 117 | export const direct = (ref, value, name) => (ref[name] = value); 118 | 119 | /** 120 | * @template T 121 | * @param {Element} element 122 | * @param {T} value 123 | * @param {string} name 124 | * @returns {T} 125 | */ 126 | export const dot = (element, value, name) => direct(element, value, name.slice(1)); 127 | 128 | /** 129 | * @template T 130 | * @param {Element} element 131 | * @param {T} value 132 | * @param {string} name 133 | * @returns {T} 134 | */ 135 | export const maybeDirect = (element, value, name) => ( 136 | value == null ? 137 | (removeAttribute(element, name), value) : 138 | direct(element, value, name) 139 | ); 140 | 141 | /** 142 | * @template T 143 | * @param {Element} element 144 | * @param {T} value 145 | * @returns {T} 146 | */ 147 | export const ref = (element, value) => ( 148 | (typeof value === 'function' ? 149 | value(element) : (value.current = element)), 150 | value 151 | ); 152 | 153 | /** 154 | * @template T 155 | * @param {Element} element 156 | * @param {T} value 157 | * @param {string} name 158 | * @returns {T} 159 | */ 160 | const regular = (element, value, name) => ( 161 | (value == null ? 162 | removeAttribute(element, name) : 163 | setAttribute(element, name, value)), 164 | value 165 | ); 166 | 167 | /** 168 | * @template T 169 | * @param {Element} element 170 | * @param {T} value 171 | * @returns {T} 172 | */ 173 | export const style = (element, value) => ( 174 | value == null ? 175 | maybeDirect(element, value, 'style') : 176 | direct(element.style, value, 'cssText') 177 | ); 178 | 179 | /** 180 | * @template T 181 | * @param {Element} element 182 | * @param {T} value 183 | * @param {string} name 184 | * @returns {T} 185 | */ 186 | export const toggle = (element, value, name) => ( 187 | element.toggleAttribute(name.slice(1), value), 188 | value 189 | ); 190 | 191 | /** 192 | * @param {Node} node 193 | * @param {Node[]} value 194 | * @param {string} _ 195 | * @param {Node[]} prev 196 | * @returns {Node[]} 197 | */ 198 | export const array = (node, value, prev) => { 199 | // normal diff 200 | const { length } = value; 201 | node.data = `[${length}]`; 202 | if (length) 203 | return udomdiff(node.parentNode, prev, value, diffFragment, node); 204 | /* c8 ignore start */ 205 | switch (prev.length) { 206 | case 1: 207 | prev[0].remove(); 208 | case 0: 209 | break; 210 | default: 211 | drop( 212 | diffFragment(prev[0], 0), 213 | diffFragment(prev.at(-1), -0), 214 | false 215 | ); 216 | break; 217 | } 218 | /* c8 ignore stop */ 219 | return empty; 220 | }; 221 | 222 | export const attr = new Map([ 223 | ['aria', aria], 224 | ['class', className], 225 | ['data', data], 226 | ['ref', ref], 227 | ['style', style], 228 | ]); 229 | 230 | /** 231 | * @param {HTMLElement | SVGElement} element 232 | * @param {string} name 233 | * @param {boolean} svg 234 | * @returns 235 | */ 236 | export const attribute = (element, name, svg) => { 237 | switch (name[0]) { 238 | case '.': return dot; 239 | case '?': return toggle; 240 | case '@': return at; 241 | default: return ( 242 | svg || ('ownerSVGElement' in element) ? 243 | (name === 'ref' ? ref : regular) : 244 | (attr.get(name) || ( 245 | name in element ? 246 | (name.startsWith('on') ? 247 | direct : 248 | (gPD(element, name)?.set ? maybeDirect : regular) 249 | ) : 250 | regular 251 | ) 252 | ) 253 | ); 254 | } 255 | }; 256 | 257 | /** 258 | * @template T 259 | * @param {Element} element 260 | * @param {T} value 261 | * @returns {T} 262 | */ 263 | export const text = (element, value) => ( 264 | (element.textContent = value == null ? '' : value), 265 | value 266 | ); 267 | -------------------------------------------------------------------------------- /esm/index.js: -------------------------------------------------------------------------------- 1 | /*! (c) Andrea Giammarchi - MIT */ 2 | import { Hole } from './rabbit.js'; 3 | import { attr } from './handler.js'; 4 | 5 | import render from './render/hole.js'; 6 | 7 | /** @typedef {import("./literals.js").Value} Value */ 8 | 9 | const tag = svg => (template, ...values) => new Hole(svg, template, values); 10 | 11 | /** @type {(template: TemplateStringsArray, ...values:Value[]) => Hole} A tag to render HTML content. */ 12 | export const html = tag(false); 13 | 14 | /** @type {(template: TemplateStringsArray, ...values:Value[]) => Hole} A tag to render SVG content. */ 15 | export const svg = tag(true); 16 | 17 | export { Hole, render, attr }; 18 | -------------------------------------------------------------------------------- /esm/keyed.js: -------------------------------------------------------------------------------- 1 | /*! (c) Andrea Giammarchi - MIT */ 2 | import { Hole } from './rabbit.js'; 3 | import { attr } from './handler.js'; 4 | import { cache } from './literals.js'; 5 | import { set } from './utils.js'; 6 | import { html, svg } from './index.js'; 7 | 8 | import render from './render/keyed.js'; 9 | 10 | /** @typedef {import("./literals.js").Cache} Cache */ 11 | /** @typedef {import("./literals.js").Target} Target */ 12 | /** @typedef {import("./literals.js").Value} Value */ 13 | 14 | /** @typedef {(ref:Object, key:string | number) => Tag} Bound */ 15 | 16 | /** 17 | * @callback Tag 18 | * @param {TemplateStringsArray} template 19 | * @param {...Value} values 20 | * @returns {Target} 21 | */ 22 | 23 | const keyed = new WeakMap; 24 | const createRef = svg => /** @type {Bound} */ (ref, key) => { 25 | /** @type {Tag} */ 26 | function tag(template, ...values) { 27 | return new Hole(svg, template, values).toDOM(this); 28 | } 29 | 30 | const memo = keyed.get(ref) || set(keyed, ref, new Map); 31 | return memo.get(key) || set(memo, key, tag.bind(cache())); 32 | }; 33 | 34 | /** @type {Bound} Returns a bound tag to render HTML content. */ 35 | export const htmlFor = createRef(false); 36 | 37 | /** @type {Bound} Returns a bound tag to render SVG content. */ 38 | export const svgFor = createRef(true); 39 | 40 | export { Hole, render, html, svg, attr }; 41 | -------------------------------------------------------------------------------- /esm/literals.js: -------------------------------------------------------------------------------- 1 | import { empty } from './utils.js'; 2 | 3 | /** @typedef {import("./persistent-fragment.js").PersistentFragment} PersistentFragment */ 4 | /** @typedef {import("./rabbit.js").Hole} Hole */ 5 | 6 | /** @typedef {unknown} Value */ 7 | /** @typedef {Node | Element | PersistentFragment} Target */ 8 | /** @typedef {null | undefined | string | number | boolean | Node | Element | PersistentFragment} DOMValue */ 9 | /** @typedef {Hole | Node} ArrayValue */ 10 | 11 | export const abc = (a, b, c) => ({ a, b, c }); 12 | 13 | export const bc = (b, c) => ({ b, c }); 14 | 15 | /** 16 | * @typedef {Object} Detail 17 | * @property {any} v the current value of the interpolation / hole 18 | * @property {function} u the callback to update the value 19 | * @property {Node} t the target comment node or element 20 | * @property {string | null | Node} n the attribute name, if any, or `null` 21 | * @property {Cache | ArrayValue[] | null} c the cache value for this detail 22 | */ 23 | 24 | /** 25 | * @returns {Detail} 26 | */ 27 | export const detail = (u, t, n, c) => ({ v: empty, u, t, n, c }); 28 | 29 | /** 30 | * @typedef {Object} Entry 31 | * @property {number[]} a the path to retrieve the node 32 | * @property {function} b the update function 33 | * @property {string | null} c the attribute name, if any, or `null` 34 | */ 35 | 36 | /** 37 | * @typedef {Object} Cache 38 | * @property {null | TemplateStringsArray} a the cached template 39 | * @property {null | Node | PersistentFragment} b the node returned when parsing the template 40 | * @property {Detail[]} c the list of updates to perform 41 | */ 42 | 43 | /** 44 | * @returns {Cache} 45 | */ 46 | export const cache = () => abc(null, null, empty); 47 | -------------------------------------------------------------------------------- /esm/node.js: -------------------------------------------------------------------------------- 1 | /*! (c) Andrea Giammarchi - MIT */ 2 | import { Hole } from './rabbit.js'; 3 | import { attr } from './handler.js'; 4 | 5 | /** @typedef {import("./literals.js").DOMValue} DOMValue */ 6 | /** @typedef {import("./literals.js").Target} Target */ 7 | 8 | const tag = svg => (template, ...values) => new Hole(svg, template, values).toDOM().valueOf(); 9 | 10 | /** @type {(template: TemplateStringsArray, ...values:DOMValue[]) => Target} A tag to render HTML content. */ 11 | export const html = tag(false); 12 | 13 | /** @type {(template: TemplateStringsArray, ...values:DOMValue[]) => Target} A tag to render SVG content. */ 14 | export const svg = tag(true); 15 | 16 | /** 17 | * Render directly within a generic container. 18 | * @template T 19 | * @param {T} where the DOM node where to render content 20 | * @param {(() => Target) | Target} what the node to render 21 | * @returns 22 | */ 23 | export const render = (where, what) => { 24 | where.replaceChildren(typeof what === 'function' ? what() : what); 25 | return where; 26 | }; 27 | 28 | export { attr }; 29 | -------------------------------------------------------------------------------- /esm/parser.js: -------------------------------------------------------------------------------- 1 | import { COMMENT_NODE, ELEMENT_NODE } from 'domconstants/constants'; 2 | import { TEXT_ELEMENTS } from 'domconstants/re'; 3 | import parser from '@webreflection/uparser'; 4 | 5 | import { empty, isArray, set } from './utils.js'; 6 | import { abc } from './literals.js'; 7 | 8 | import { array, attribute, hole, text, removeAttribute } from './handler.js'; 9 | import createContent from './create-content.js'; 10 | 11 | /** @typedef {import("./literals.js").Entry} Entry */ 12 | 13 | /** 14 | * @typedef {Object} Resolved 15 | * @param {DocumentFragment} f content retrieved from the template 16 | * @param {Entry[]} e entries per each hole in the template 17 | * @param {boolean} d direct node to handle 18 | */ 19 | 20 | /** 21 | * @param {Element} node 22 | * @returns {number[]} 23 | */ 24 | const createPath = node => { 25 | const path = []; 26 | let parentNode; 27 | while ((parentNode = node.parentNode)) { 28 | path.push(path.indexOf.call(parentNode.childNodes, node)); 29 | node = parentNode; 30 | } 31 | return path; 32 | }; 33 | 34 | const textNode = () => document.createTextNode(''); 35 | 36 | /** 37 | * @param {TemplateStringsArray} template 38 | * @param {boolean} xml 39 | * @returns {Resolved} 40 | */ 41 | const resolve = (template, values, xml) => { 42 | const content = createContent(parser(template, prefix, xml), xml); 43 | const { length } = template; 44 | let entries = empty; 45 | if (length > 1) { 46 | const replace = []; 47 | const tw = document.createTreeWalker(content, 1 | 128); 48 | let i = 0, search = `${prefix}${i++}`; 49 | entries = []; 50 | while (i < length) { 51 | const node = tw.nextNode(); 52 | // these are holes or arrays 53 | if (node.nodeType === COMMENT_NODE) { 54 | if (node.data === search) { 55 | // ⚠️ once array, always array! 56 | const update = isArray(values[i - 1]) ? array : hole; 57 | if (update === hole) replace.push(node); 58 | entries.push(abc(createPath(node), update, null)); 59 | search = `${prefix}${i++}`; 60 | } 61 | } 62 | else { 63 | let path; 64 | // these are attributes 65 | while (node.hasAttribute(search)) { 66 | if (!path) path = createPath(node); 67 | const name = node.getAttribute(search); 68 | entries.push(abc(path, attribute(node, name, xml), name)); 69 | removeAttribute(node, search); 70 | search = `${prefix}${i++}`; 71 | } 72 | // these are special text-only nodes 73 | if ( 74 | !xml && 75 | TEXT_ELEMENTS.test(node.localName) && 76 | node.textContent.trim() === `` 77 | ) { 78 | entries.push(abc(path || createPath(node), text, null)); 79 | search = `${prefix}${i++}`; 80 | } 81 | } 82 | } 83 | // can't replace holes on the fly or the tree walker fails 84 | for (i = 0; i < replace.length; i++) 85 | replace[i].replaceWith(textNode()); 86 | } 87 | 88 | // need to decide if there should be a persistent fragment 89 | const { childNodes } = content; 90 | let { length: len } = childNodes; 91 | 92 | // html`` or svg`` to signal an empty content 93 | // these nodes can be passed directly as never mutated 94 | if (len < 1) { 95 | len = 1; 96 | content.appendChild(textNode()); 97 | } 98 | // html`${'b'}` or svg`${[]}` cases 99 | else if ( 100 | len === 1 && 101 | // ignore html`static` or svg`static` because 102 | // these nodes can be passed directly as never mutated 103 | length !== 1 && 104 | childNodes[0].nodeType !== ELEMENT_NODE 105 | ) { 106 | // use a persistent fragment for these cases too 107 | len = 0; 108 | } 109 | 110 | return set(cache, template, abc(content, entries, len === 1)); 111 | }; 112 | 113 | /** @type {WeakMap} */ 114 | const cache = new WeakMap; 115 | const prefix = 'isµ'; 116 | 117 | /** 118 | * @param {boolean} xml 119 | * @returns {(template: TemplateStringsArray, values: any[]) => Resolved} 120 | */ 121 | export default xml => (template, values) => cache.get(template) || resolve(template, values, xml); 122 | -------------------------------------------------------------------------------- /esm/persistent-fragment.js: -------------------------------------------------------------------------------- 1 | import { DOCUMENT_FRAGMENT_NODE } from 'domconstants/constants'; 2 | import custom from 'custom-function/factory'; 3 | import drop from './range.js'; 4 | import { empty } from './utils.js'; 5 | 6 | /** 7 | * @param {PersistentFragment} fragment 8 | * @returns {Node | Element} 9 | */ 10 | const remove = ({firstChild, lastChild}, preserve) => drop(firstChild, lastChild, preserve); 11 | 12 | let checkType = false; 13 | 14 | /** 15 | * @param {Node} node 16 | * @param {1 | 0 | -0 | -1} operation 17 | * @returns {Node} 18 | */ 19 | export const diffFragment = (node, operation) => ( 20 | checkType && node.nodeType === DOCUMENT_FRAGMENT_NODE ? 21 | ((1 / operation) < 0 ? 22 | (operation ? remove(node, true) : node.lastChild) : 23 | (operation ? node.valueOf() : node.firstChild)) : 24 | node 25 | ); 26 | 27 | const comment = value => document.createComment(value); 28 | 29 | /** @extends {DocumentFragment} */ 30 | export class PersistentFragment extends custom(DocumentFragment) { 31 | #firstChild = comment('<>'); 32 | #lastChild = comment(''); 33 | #nodes = empty; 34 | constructor(fragment) { 35 | super(fragment); 36 | this.replaceChildren(...[ 37 | this.#firstChild, 38 | ...fragment.childNodes, 39 | this.#lastChild, 40 | ]); 41 | checkType = true; 42 | } 43 | get firstChild() { return this.#firstChild; } 44 | get lastChild() { return this.#lastChild; } 45 | get parentNode() { return this.#firstChild.parentNode; } 46 | remove() { 47 | remove(this, false); 48 | } 49 | replaceWith(node) { 50 | remove(this, true).replaceWith(node); 51 | } 52 | valueOf() { 53 | const { parentNode } = this; 54 | if (parentNode === this) { 55 | if (this.#nodes === empty) 56 | this.#nodes = [...this.childNodes]; 57 | } 58 | else { 59 | /* c8 ignore start */ 60 | // there are cases where a fragment might be just appended 61 | // out of the box without valueOf() invoke (first render). 62 | // When these are moved around and lose their parent and, 63 | // such parent is not the fragment itself, it's possible there 64 | // where changes or mutations in there to take care about. 65 | // This is a render-only specific issue but it's tested and 66 | // it's worth fixing to me to have more consistent fragments. 67 | if (parentNode) { 68 | let { firstChild, lastChild } = this; 69 | this.#nodes = [firstChild]; 70 | while (firstChild !== lastChild) 71 | this.#nodes.push((firstChild = firstChild.nextSibling)); 72 | } 73 | /* c8 ignore stop */ 74 | this.replaceChildren(...this.#nodes); 75 | } 76 | return this; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /esm/rabbit.js: -------------------------------------------------------------------------------- 1 | import { array, hole } from './handler.js'; 2 | import { cache } from './literals.js'; 3 | import create from './creator.js'; 4 | import parser from './parser.js'; 5 | 6 | const createHTML = create(parser(false)); 7 | const createSVG = create(parser(true)); 8 | 9 | /** 10 | * @param {import("./literals.js").Cache} info 11 | * @param {Hole} hole 12 | * @returns {Node} 13 | */ 14 | const unroll = (info, { s, t, v }) => { 15 | if (info.a !== t) { 16 | const { b, c } = (s ? createSVG : createHTML)(t, v); 17 | info.a = t; 18 | info.b = b; 19 | info.c = c; 20 | } 21 | for (let { c } = info, i = 0; i < c.length; i++) { 22 | const value = v[i]; 23 | const detail = c[i]; 24 | switch (detail.u) { 25 | case array: 26 | detail.v = array( 27 | detail.t, 28 | unrollValues(detail.c, value), 29 | detail.v 30 | ); 31 | break; 32 | case hole: 33 | const current = value instanceof Hole ? 34 | unroll(detail.c || (detail.c = cache()), value) : 35 | (detail.c = null, value) 36 | ; 37 | if (current !== detail.v) 38 | detail.v = hole(detail, current); 39 | break; 40 | default: 41 | if (value !== detail.v) 42 | detail.v = detail.u(detail.t, value, detail.n, detail.v); 43 | break; 44 | } 45 | } 46 | return info.b; 47 | }; 48 | 49 | /** 50 | * @param {Cache} cache 51 | * @param {any[]} values 52 | * @returns {number} 53 | */ 54 | const unrollValues = (stack, values) => { 55 | let i = 0, { length } = values; 56 | if (length < stack.length) stack.splice(length); 57 | for (; i < length; i++) { 58 | const value = values[i]; 59 | if (value instanceof Hole) 60 | values[i] = unroll(stack[i] || (stack[i] = cache()), value); 61 | else stack[i] = null; 62 | } 63 | return values; 64 | }; 65 | 66 | /** 67 | * Holds all details needed to render the content on a render. 68 | * @constructor 69 | * @param {boolean} svg The content type. 70 | * @param {TemplateStringsArray} template The template literals used to the define the content. 71 | * @param {any[]} values Zero, one, or more interpolated values to render. 72 | */ 73 | export class Hole { 74 | constructor(svg, template, values) { 75 | this.s = svg; 76 | this.t = template; 77 | this.v = values; 78 | } 79 | toDOM(info = cache()) { 80 | return unroll(info, this); 81 | } 82 | }; 83 | -------------------------------------------------------------------------------- /esm/range.js: -------------------------------------------------------------------------------- 1 | import { newRange } from './utils.js'; 2 | 3 | let range; 4 | /** 5 | * @param {Node | Element} firstChild 6 | * @param {Node | Element} lastChild 7 | * @param {boolean} preserve 8 | * @returns 9 | */ 10 | export default (firstChild, lastChild, preserve) => { 11 | if (!range) range = newRange(); 12 | /* c8 ignore start */ 13 | if (preserve) 14 | range.setStartAfter(firstChild); 15 | else 16 | range.setStartBefore(firstChild); 17 | /* c8 ignore stop */ 18 | range.setEndAfter(lastChild); 19 | range.deleteContents(); 20 | return firstChild; 21 | }; 22 | -------------------------------------------------------------------------------- /esm/reactive.js: -------------------------------------------------------------------------------- 1 | import { Hole, html, svg, htmlFor, svgFor, attr } from './keyed.js'; 2 | 3 | import { attach, detach } from './render/reactive.js'; 4 | 5 | export { Hole, attach, detach, html, svg, htmlFor, svgFor, attr }; 6 | 7 | // TODO: mostly backward compatibility ... should this change? 8 | export { attach as reactive }; 9 | -------------------------------------------------------------------------------- /esm/reactive/preact.js: -------------------------------------------------------------------------------- 1 | export * from '@preact/signals-core'; 2 | export { Hole, html, svg, htmlFor, svgFor, detach, attr } from '../reactive.js'; 3 | 4 | import { effect } from '@preact/signals-core'; 5 | import { attach } from '../reactive.js'; 6 | export const render = attach(effect); 7 | -------------------------------------------------------------------------------- /esm/reactive/signal.js: -------------------------------------------------------------------------------- 1 | export * from '@webreflection/signal'; 2 | export { Hole, html, svg, htmlFor, svgFor, detach, attr } from '../reactive.js'; 3 | 4 | import { effect } from '@webreflection/signal'; 5 | import { attach } from '../reactive.js'; 6 | export const render = attach(effect); 7 | -------------------------------------------------------------------------------- /esm/render/hole.js: -------------------------------------------------------------------------------- 1 | import { cache } from '../literals.js'; 2 | import { set } from '../utils.js'; 3 | 4 | /** @typedef {import("../rabbit.js").Hole} Hole */ 5 | 6 | /** @type {WeakMap} */ 7 | const known = new WeakMap; 8 | 9 | /** 10 | * Render with smart updates within a generic container. 11 | * @template T 12 | * @param {T} where the DOM node where to render content 13 | * @param {(() => Hole) | Hole} what the hole to render 14 | * @returns 15 | */ 16 | export default (where, what) => { 17 | const info = known.get(where) || set(known, where, cache()); 18 | const { b } = info; 19 | if (b !== (typeof what === 'function' ? what() : what).toDOM(info)) 20 | where.replaceChildren(info.b.valueOf()); 21 | return where; 22 | }; 23 | -------------------------------------------------------------------------------- /esm/render/keyed.js: -------------------------------------------------------------------------------- 1 | import render from './shared.js'; 2 | 3 | /** @typedef {import("../rabbit.js").Hole} Hole */ 4 | 5 | /** 6 | * Render with smart updates within a generic container. 7 | * @template T 8 | * @param {T} where the DOM node where to render content 9 | * @param {(() => Hole) | Hole} what the hole to render 10 | * @returns 11 | */ 12 | export default (where, what) => render(where, what, true); 13 | -------------------------------------------------------------------------------- /esm/render/reactive.js: -------------------------------------------------------------------------------- 1 | import { create, drop } from 'gc-hook'; 2 | 3 | import render from './shared.js'; 4 | 5 | /** @typedef {import("../rabbit.js").Hole} Hole */ 6 | 7 | /** @type {WeakMap} */ 8 | const effects = new WeakMap; 9 | 10 | /** 11 | * @param {Function} dispose 12 | * @returns {void} 13 | */ 14 | const onGC = dispose => dispose(); 15 | 16 | let remove = true; 17 | 18 | /** 19 | * @param {Function} effect the reactive `effect` callback provided by a 3rd party library. 20 | * @returns 21 | */ 22 | export const attach = effect => { 23 | /** 24 | * Render with smart updates within a generic container. 25 | * If the `what` is a function, it automatically create 26 | * an effect for the render function. 27 | * @template T 28 | * @param {T} where the DOM node where to render content 29 | * @param {(() => Hole) | Hole} what the hole to render 30 | * @returns {T} 31 | */ 32 | return (where, what) => { 33 | remove = typeof what !== 'function'; 34 | detach(where); 35 | 36 | if (remove) return render(where, what, false); 37 | remove = true; 38 | 39 | const wr = new WeakRef(where); 40 | const dispose = effect(() => { render(wr.deref(), what(), false) }); 41 | effects.set(where, dispose); 42 | return create(dispose, onGC, { return: where }); 43 | }; 44 | }; 45 | 46 | /** 47 | * Allow manual cleanup of subscribed signals. 48 | * @param {Element} where a reference container previously used to render signals. 49 | */ 50 | export const detach = where => { 51 | const dispose = effects.get(where); 52 | if (dispose) { 53 | if (remove) effects.delete(where); 54 | drop(dispose); 55 | dispose(); 56 | } 57 | }; 58 | -------------------------------------------------------------------------------- /esm/render/shared.js: -------------------------------------------------------------------------------- 1 | import { Hole } from '../rabbit.js'; 2 | import { cache } from '../literals.js'; 3 | import { set } from '../utils.js'; 4 | 5 | /** @type {WeakMap} */ 6 | const known = new WeakMap; 7 | 8 | /** 9 | * Render with smart updates within a generic container. 10 | * @template T 11 | * @param {T} where the DOM node where to render content 12 | * @param {(() => Hole) | Hole} what the hole to render 13 | * @param {boolean} check does a `typeof` check (internal usage). 14 | * @returns 15 | */ 16 | export default (where, what, check) => { 17 | const info = known.get(where) || set(known, where, cache()); 18 | const { b } = info; 19 | const hole = (check && typeof what === 'function') ? what() : what; 20 | const node = hole instanceof Hole ? hole.toDOM(info) : hole; 21 | if (b !== node) 22 | where.replaceChildren((info.b = node).valueOf()); 23 | return where; 24 | }; 25 | -------------------------------------------------------------------------------- /esm/ssr.js: -------------------------------------------------------------------------------- 1 | import { Hole, render, html, svg, attr } from './index.js'; 2 | 3 | export const htmlFor = () => html; 4 | export const svgFor = () => svg; 5 | 6 | export { Hole, render, html, svg, attr }; 7 | -------------------------------------------------------------------------------- /esm/utils.js: -------------------------------------------------------------------------------- 1 | const { isArray } = Array; 2 | const { getPrototypeOf, getOwnPropertyDescriptor } = Object; 3 | 4 | export { isArray }; 5 | 6 | export const SVG_NAMESPACE = 'http://www.w3.org/2000/svg'; 7 | 8 | export const empty = []; 9 | 10 | export const newRange = () => document.createRange(); 11 | 12 | /** 13 | * Set the `key` `value` pair to the *Map* or *WeakMap* and returns the `value` 14 | * @template T 15 | * @param {Map | WeakMap} map 16 | * @param {any} key 17 | * @param {T} value 18 | * @returns {T} 19 | */ 20 | export const set = (map, key, value) => { 21 | map.set(key, value); 22 | return value; 23 | }; 24 | 25 | /** 26 | * Return a descriptor, if any, for the referenced *Element* 27 | * @param {Element} ref 28 | * @param {string} prop 29 | * @returns 30 | */ 31 | export const gPD = (ref, prop) => { 32 | let desc; 33 | do { desc = getOwnPropertyDescriptor(ref, prop); } 34 | while(!desc && (ref = getPrototypeOf(ref))); 35 | return desc; 36 | }; 37 | 38 | /* c8 ignore start */ 39 | /** 40 | * @param {DocumentFragment} content 41 | * @param {number[]} path 42 | * @returns {Element} 43 | */ 44 | export const find = (content, path) => path.reduceRight(childNodesIndex, content); 45 | const childNodesIndex = (node, i) => node.childNodes[i]; 46 | /* c8 ignore stop */ 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "uhtml", 3 | "version": "4.7.1", 4 | "description": "A micro HTML/SVG render", 5 | "main": "./cjs/index.js", 6 | "types": "./types/index.d.ts", 7 | "scripts": { 8 | "benchmark:w3c": "node test/benchmark/linkedom.js --w3c; node test/benchmark/linkedom-cached.js --w3c; node test/benchmark/dom.js --w3c", 9 | "benchmark:dom": "node test/benchmark/linkedom.js --dom; node test/benchmark/linkedom-cached.js --dom; node test/benchmark/dom.js --dom", 10 | "build": "npm run rollup:es && node rollup/ssr.cjs && node rollup/init.cjs && npm run rollup:init && npm run rollup:ssr && rm -rf cjs/* && npm run cjs && npm run build:types && npm run test && npm run size && npm run publint", 11 | "cjs": "ascjs --no-default esm cjs", 12 | "rollup:es": "rollup --config rollup/es.config.js", 13 | "rollup:init": "rollup --config rollup/init.config.js", 14 | "rollup:ssr": "rollup --config rollup/ssr.config.js && rm esm/init-ssr.js_ && terser --module esm/init-ssr.js -mc -o ./worker.js", 15 | "server": "npx static-handler .", 16 | "size": "echo \"index $(cat index.js | brotli | wc -c)\";echo \"keyed $(cat keyed.js | brotli | wc -c)\";echo \"reactive $(cat reactive.js | brotli | wc -c)\";echo \"preactive $(cat preactive.js | brotli | wc -c)\";echo \"signal $(cat signal.js | brotli | wc -c)\";echo \"node $(cat node.js | brotli | wc -c)\";echo \"worker $(cat worker.js | brotli | wc -c)\";", 17 | "test": "c8 node test/coverage.js && node test/modern.mjs && node test/svg.mjs", 18 | "coverage": "mkdir -p ./coverage; c8 report --reporter=text-lcov > ./coverage/lcov.info", 19 | "clean": "rm -rf coverage ./*.js cjs/**/*.js cjs/*.js types", 20 | "check:types": "npx attw $(npm pack) --profile esm-only", 21 | "build:types": "rm -rf types && npx tsc -p tsconfig.json && node rollup/ts.fix.js", 22 | "publint": "npx publint ." 23 | }, 24 | "keywords": [ 25 | "micro", 26 | "HTML", 27 | "render" 28 | ], 29 | "author": "Andrea Giammarchi", 30 | "license": "MIT", 31 | "devDependencies": { 32 | "@arethetypeswrong/cli": "~0.17.4", 33 | "publint": "~0.3.12", 34 | "@rollup/plugin-node-resolve": "^15.3.0", 35 | "@rollup/plugin-terser": "^0.4.4", 36 | "@types/estree": "^1.0.6", 37 | "@types/istanbul-lib-coverage": "^2.0.6", 38 | "@types/resolve": "^1.20.6", 39 | "ascjs": "^6.0.3", 40 | "c8": "^10.1.2", 41 | "fast-glob": "^3.3.2", 42 | "rollup": "^4.27.4", 43 | "terser": "^5.36.0", 44 | "typescript": "^5.7.2" 45 | }, 46 | "module": "./esm/index.js", 47 | "type": "module", 48 | "exports": { 49 | ".": { 50 | "types": { 51 | "require": "./types/index.d.cts", 52 | "default": "./types/index.d.mts" 53 | }, 54 | "import": "./esm/index.js", 55 | "default": "./cjs/index.js" 56 | }, 57 | "./dom": { 58 | "types": { 59 | "require": "./types/dom/index.d.cts", 60 | "default": "./types/dom/index.d.mts" 61 | }, 62 | "import": "./esm/dom/index.js", 63 | "default": "./cjs/dom/index.js" 64 | }, 65 | "./init": { 66 | "types": { 67 | "require": "./types/init.d.cts", 68 | "default": "./types/init.d.mts" 69 | }, 70 | "import": "./esm/init.js", 71 | "default": "./cjs/init.js" 72 | }, 73 | "./keyed": { 74 | "types": { 75 | "require": "./types/keyed.d.cts", 76 | "default": "./types/keyed.d.mts" 77 | }, 78 | "import": "./esm/keyed.js", 79 | "default": "./cjs/keyed.js" 80 | }, 81 | "./node": { 82 | "types": { 83 | "require": "./types/node.d.cts", 84 | "default": "./types/node.d.mts" 85 | }, 86 | "import": "./esm/node.js", 87 | "default": "./cjs/node.js" 88 | }, 89 | "./reactive": { 90 | "types": { 91 | "require": "./types/reactive.d.cts", 92 | "default": "./types/reactive.d.mts" 93 | }, 94 | "import": "./esm/reactive.js", 95 | "default": "./cjs/reactive.js" 96 | }, 97 | "./preactive": { 98 | "types": { 99 | "require": "./types/reactive/preact.d.cts", 100 | "default": "./types/reactive/preact.d.mts" 101 | }, 102 | "import": "./esm/reactive/preact.js", 103 | "default": "./cjs/reactive/preact.js" 104 | }, 105 | "./signal": { 106 | "types": { 107 | "require": "./types/reactive/signal.d.cts", 108 | "default": "./types/reactive/signal.d.mts" 109 | }, 110 | "import": "./esm/reactive/signal.js", 111 | "default": "./cjs/reactive/signal.js" 112 | }, 113 | "./ssr": { 114 | "types": { 115 | "require": "./types/init-ssr.d.cts", 116 | "default": "./types/init-ssr.d.mts" 117 | }, 118 | "import": "./esm/init-ssr.js", 119 | "default": "./cjs/init-ssr.js" 120 | }, 121 | "./worker": { 122 | "types": { 123 | "require": "./types/init-ssr.d.cts", 124 | "default": "./types/init-ssr.d.mts" 125 | }, 126 | "import": "./worker.js" 127 | }, 128 | "./package.json": "./package.json" 129 | }, 130 | "unpkg": "./keyed.js", 131 | "dependencies": { 132 | "@webreflection/uparser": "^0.4.0", 133 | "custom-function": "^2.0.0", 134 | "domconstants": "^1.1.6", 135 | "gc-hook": "^0.4.1", 136 | "html-escaper": "^3.0.3", 137 | "htmlparser2": "^9.1.0", 138 | "udomdiff": "^1.1.2" 139 | }, 140 | "repository": { 141 | "type": "git", 142 | "url": "git+https://github.com/WebReflection/uhtml.git" 143 | }, 144 | "bugs": { 145 | "url": "https://github.com/WebReflection/uhtml/issues" 146 | }, 147 | "homepage": "https://github.com/WebReflection/uhtml#readme", 148 | "optionalDependencies": { 149 | "@preact/signals-core": "^1.8.0", 150 | "@webreflection/signal": "^2.1.2" 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /rollup/es.config.js: -------------------------------------------------------------------------------- 1 | import {nodeResolve} from '@rollup/plugin-node-resolve'; 2 | import terser from '@rollup/plugin-terser'; 3 | 4 | const plugins = [ 5 | nodeResolve(), 6 | ].concat( 7 | process.env.NO_MIN ? [] : [terser()] 8 | ); 9 | 10 | export default [ 11 | { 12 | plugins: [nodeResolve()], 13 | input: './esm/keyed.js', 14 | output: { 15 | esModule: false, 16 | file: './esm/init.js', 17 | format: 'iife', 18 | name: 'uhtml', 19 | }, 20 | }, 21 | { 22 | plugins: [nodeResolve()], 23 | input: './esm/ssr.js', 24 | output: { 25 | esModule: false, 26 | file: './esm/init-ssr.js', 27 | format: 'iife', 28 | name: 'uhtml', 29 | }, 30 | }, 31 | { 32 | plugins, 33 | input: './esm/index.js', 34 | output: { 35 | esModule: true, 36 | file: './index.js', 37 | }, 38 | }, 39 | { 40 | plugins, 41 | input: './esm/keyed.js', 42 | output: { 43 | esModule: true, 44 | file: './keyed.js', 45 | }, 46 | }, 47 | { 48 | plugins, 49 | input: './esm/node.js', 50 | output: { 51 | esModule: true, 52 | file: './node.js', 53 | }, 54 | }, 55 | { 56 | plugins, 57 | input: './esm/reactive.js', 58 | output: { 59 | esModule: true, 60 | file: './reactive.js', 61 | }, 62 | }, 63 | { 64 | plugins, 65 | input: './esm/reactive/preact.js', 66 | output: { 67 | esModule: true, 68 | file: './preactive.js', 69 | }, 70 | }, 71 | { 72 | plugins, 73 | input: './esm/reactive/signal.js', 74 | output: { 75 | esModule: true, 76 | file: './signal.js', 77 | }, 78 | }, 79 | { 80 | plugins, 81 | input: './esm/dom/index.js', 82 | output: { 83 | esModule: true, 84 | file: './dom.js', 85 | }, 86 | }, 87 | ]; 88 | -------------------------------------------------------------------------------- /rollup/exports.cjs: -------------------------------------------------------------------------------- 1 | module.exports = code => { 2 | const out = []; 3 | code = code.replace( 4 | /^\s+exports\.(\S+)\s*=\s*([^;]+);/gm, 5 | (_, name, exported) => { 6 | out.push(name === exported ? name : `${name}: ${exported}`); 7 | return ''; 8 | } 9 | ); 10 | return code 11 | .replace(/^\s+return exports;/m, `\n return { ${out.join(', ')} };`) 12 | .replace('function (exports) {', 'function () {') 13 | .replace(/\}\)\(\{\}\);(\s*)$/, '})();$1'); 14 | }; 15 | -------------------------------------------------------------------------------- /rollup/init.cjs: -------------------------------------------------------------------------------- 1 | const { readFileSync, writeFileSync } = require('fs'); 2 | const { join } = require('path'); 3 | const fixExports = require('./exports.cjs'); 4 | 5 | const init = join(__dirname, '..', 'esm', 'init.js'); 6 | const uhtml = readFileSync(init).toString(); 7 | 8 | writeFileSync(init, ` 9 | // ⚠️ WARNING - THIS FILE IS AN ARTIFACT - DO NOT EDIT 10 | 11 | /** 12 | * @param {Document} document 13 | * @returns {import("./keyed.js")} 14 | */ 15 | export default document => ${ 16 | // tested via integration 17 | fixExports( 18 | uhtml 19 | .replace(`svg || ('ownerSVGElement' in element)`, `/* c8 ignore start */ svg || ('ownerSVGElement' in element) /* c8 ignore stop */`) 20 | .replace(/diffFragment = \(([\S\s]+?)return /, 'diffFragment = /* c8 ignore start */($1/* c8 ignore stop */return ') 21 | .replace(/udomdiff = \(([\S\s]+?)return /, 'udomdiff = /* c8 ignore start */($1/* c8 ignore stop */return ') 22 | .replace(/^(\s+)replaceWith\(([^}]+?)\}/m, '$1/* c8 ignore start */\n$1replaceWith($2}\n$1/* c8 ignore stop */') 23 | .replace(/^(\s+)(["'])use strict\2;/m, '$1$2use strict$2;\n\n$1const { constructor: DocumentFragment } = document.createDocumentFragment();') 24 | .replace(/^[^(]+/, '') 25 | ) 26 | }`); 27 | -------------------------------------------------------------------------------- /rollup/init.config.js: -------------------------------------------------------------------------------- 1 | import {nodeResolve} from '@rollup/plugin-node-resolve'; 2 | import terser from '@rollup/plugin-terser'; 3 | 4 | const plugins = [ 5 | nodeResolve(), 6 | ].concat( 7 | process.env.NO_MIN ? [] : [terser()] 8 | ); 9 | 10 | export default { 11 | plugins, 12 | input: './esm/init.js', 13 | output: { 14 | esModule: true, 15 | file: './init.js', 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /rollup/ssr.cjs: -------------------------------------------------------------------------------- 1 | const { readFileSync, writeFileSync } = require('fs'); 2 | const { join } = require('path'); 3 | const fixExports = require('./exports.cjs'); 4 | 5 | const init = join(__dirname, '..', 'esm', 'init-ssr.js'); 6 | const uhtml = readFileSync(init).toString(); 7 | 8 | const content = [ 9 | 'const document = content ? new DOMParser().parseFromString(content, mimeType || \'text/html\') : new Document;', 10 | 'const { constructor: DocumentFragment } = document.createDocumentFragment();', 11 | ]; 12 | 13 | writeFileSync(init + '_', ` 14 | // ⚠️ WARNING - THIS FILE IS AN ARTIFACT - DO NOT EDIT 15 | 16 | import Document from './dom/document.js'; 17 | import DOMParser from './dom/dom-parser.js'; 18 | 19 | import { value } from './dom/symbols.js'; 20 | import Comment from './dom/comment.js'; 21 | Comment.prototype.toString = function toString() { 22 | const content = this[value]; 23 | switch (content) { 24 | case '<>': 25 | case '': 26 | return ''; 27 | default: 28 | return /^\\[\\d+\\]$/.test(content) ? '' : \`\`; 29 | } 30 | }; 31 | 32 | /** @type { (content?: string, mimeType?: string) => import("./keyed.js") & { document: Document } } */ 33 | export default (content, mimeType) => ${ 34 | // tested via integration 35 | fixExports( 36 | uhtml 37 | .replace(/const create(HTML|SVG) = create\(parse\((false|true), false\)\)/g, 'const create$1 = create(parse($2, true))') 38 | .replace(`svg || ('ownerSVGElement' in element)`, `/* c8 ignore start */ svg || ('ownerSVGElement' in element) /* c8 ignore stop */`) 39 | .replace(/diffFragment = \(([\S\s]+?)return /, 'diffFragment = /* c8 ignore start */($1/* c8 ignore stop */return ') 40 | .replace(/udomdiff = \(([\S\s]+?)return /, 'udomdiff = /* c8 ignore start */($1/* c8 ignore stop */return ') 41 | .replace(/^(\s+)replaceWith\(([^}]+?)\}/m, '$1/* c8 ignore start */\n$1replaceWith($2}\n$1/* c8 ignore stop */') 42 | .replace(/^(\s+)(["'])use strict\2;/m, (_, tab, quote) => `${tab}${quote}use strict${quote};\n\n${tab}${content.join(`\n${tab}`)}`) 43 | .replace(/^(\s+)(return exports;)/m, '$1exports.document = document;\n$1$2') 44 | .replace(/^[^(]+/, '') 45 | ) 46 | }`); 47 | -------------------------------------------------------------------------------- /rollup/ssr.config.js: -------------------------------------------------------------------------------- 1 | import {nodeResolve} from '@rollup/plugin-node-resolve'; 2 | 3 | export default [ 4 | { 5 | plugins: [nodeResolve()], 6 | input: './esm/init-ssr.js_', 7 | output: { 8 | esModule: true, 9 | file: './esm/init-ssr.js', 10 | }, 11 | }, 12 | ]; 13 | -------------------------------------------------------------------------------- /rollup/ts.fix.js: -------------------------------------------------------------------------------- 1 | import { copyFileSync } from 'node:fs'; 2 | import { resolve } from 'node:path'; 3 | import { fileURLToPath } from 'node:url'; 4 | 5 | import fg from 'fast-glob'; 6 | 7 | const dir = fileURLToPath(new URL('..', import.meta.url)); 8 | 9 | const dts = await fg(resolve(dir, 'types/**/*.d.ts')); 10 | 11 | // Create MTS and CTS files 12 | for (let f of dts) { 13 | copyFileSync(f, f.replace(/\.d\.ts$/, '.d.mts')); 14 | copyFileSync(f, f.replace(/\.d\.ts$/, '.d.cts')); 15 | } 16 | 17 | console.log(`Copied \x1b[1m${dts.length} files\x1b[0m from \`*.d.ts\` to \`*.d.[cm]ts\``); 18 | -------------------------------------------------------------------------------- /test/async.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /test/benchmark/content.js: -------------------------------------------------------------------------------- 1 | // Please note: you can literally copy and paste this file 2 | // into any browser console and see it running 3 | // in there too, so it's easy to compare with NodeJS. 4 | 5 | let browser = false; 6 | 7 | const bench = (name, count, times) => { 8 | let total = 0; 9 | for (let i = 0; i < times; i++) { 10 | const timeName = `${name} \x1b[2m${i < 1 ? 'cold' : ' hot'}\x1b[0m`; 11 | console.time(clean(timeName)); 12 | total = count(); 13 | console.timeEnd(clean(timeName)); 14 | } 15 | return total; 16 | }; 17 | 18 | const clean = str => browser ? str.replace(/\x1b\[\dm/g, '') : str; 19 | 20 | const crawl = (element, kind) => { 21 | const nodes = element[kind]; 22 | const {length} = nodes; 23 | let count = length; 24 | for (let i = 0; i < length; i++) 25 | count += crawl(nodes[i], kind); 26 | return count; 27 | }; 28 | 29 | const sleep = ms => new Promise($ => setTimeout($, ms)); 30 | 31 | const onContent = async (createDocument, html, times, logHeap = () => {}, cloneBench = true, customElements = false, mutationObserver = false) => { 32 | 33 | console.time(clean('\x1b[1mtotal benchmark time\x1b[0m')); 34 | 35 | logHeap('initial heap'); 36 | console.log(); 37 | 38 | let document; 39 | try { 40 | console.time(clean(' parsing \x1b[2mcold\x1b[0m')); 41 | document = createDocument(html.toString()); 42 | console.timeEnd(clean(' parsing \x1b[2mcold\x1b[0m')); 43 | console.log(); 44 | logHeap('document heap'); 45 | } 46 | catch (o_O) { 47 | console.error(o_O); 48 | console.warn(clean(`⚠ \x1b[1merror\x1b[0m - unable to parse the document: ${o_O.message}`)); 49 | process.exit(1); 50 | } 51 | console.log(); 52 | 53 | if (customElements) { 54 | let {constructor} = document.documentElement; 55 | while (constructor.name !== 'HTMLElement') 56 | constructor = Object.getPrototypeOf(constructor); 57 | (document.defaultView.customElements || global.customElements).define( 58 | 'custom-element', 59 | class extends constructor { 60 | static get observedAttributes() { return ['nothing', 'really']; } 61 | attributeChangedCallback() {} 62 | connectedCallback() {} 63 | disconnectedCallback() {} 64 | } 65 | ); 66 | console.log(clean('\x1b[1mCustom Elements\x1b[0m enabled via ' + constructor.name)); 67 | console.log(); 68 | } 69 | 70 | let observed = 0; 71 | if (mutationObserver) { 72 | const {MutationObserver} = document.defaultView; 73 | const mo = new MutationObserver(() => { 74 | observed++; 75 | }); 76 | mo.observe(document, { 77 | childList: true, 78 | subtree: true, 79 | attributes: true 80 | }); 81 | console.log(clean('\x1b[1mMutationObserver\x1b[0m enabled')); 82 | console.log(); 83 | } 84 | 85 | try { 86 | bench(' html.normalize()', () => { document.documentElement.normalize(); }, 1); 87 | } 88 | catch (o_O) { 89 | console.warn(clean(`⚠ \x1b[1merror\x1b[0m - unable to normalize html: ${o_O.message}`)); 90 | } 91 | console.log(); 92 | 93 | try { 94 | console.log(' total childNodes', bench(' crawling childNodes', () => crawl(document.documentElement, 'childNodes'), times)); 95 | } 96 | catch (o_O) { 97 | console.warn(clean(`⚠ \x1b[1merror\x1b[0m - unable to crawl childNodes: ${o_O.message}`)); 98 | } 99 | console.log(); 100 | 101 | await sleep(100); 102 | 103 | try { 104 | console.log(' total children', bench(' crawling children', () => crawl(document.documentElement, 'children'), times)); 105 | } 106 | catch (o_O) { 107 | console.warn(clean(`⚠ \x1b[1merror\x1b[0m - unable to crawl children: ${o_O.message}`)); 108 | } 109 | console.log(); 110 | 111 | logHeap('after crawling heap'); 112 | console.log(); 113 | 114 | if (cloneBench) { 115 | await sleep(100); 116 | 117 | try { 118 | const html = bench(' html.cloneNode(true)', () => document.documentElement.cloneNode(true), 1); 119 | console.log(' cloning: OK'); 120 | 121 | const {outerHTML: cloned} = html; 122 | const {outerHTML: original} = document.documentElement; 123 | console.log(' outerHTML: OK'); 124 | 125 | if (cloned.length !== original.length) 126 | throw new Error('invalid output'); 127 | console.log(' outcome: OK'); 128 | } 129 | catch (o_O) { 130 | console.log(); 131 | console.warn(clean(`⚠ \x1b[1merror\x1b[0m - unable to clone html: ${o_O.message}`)); 132 | } 133 | console.log(); 134 | 135 | logHeap('after cloning heap'); 136 | console.log(); 137 | } 138 | 139 | await sleep(100); 140 | 141 | // try { 142 | // console.log(' total div', bench(' querySelectorAll("div")', () => document.documentElement.querySelectorAll('div').length, times)); 143 | // } 144 | // catch (o_O) { 145 | // console.warn(clean(`⚠ \x1b[1merror\x1b[0m - unable to querySelectorAll("div"): ${o_O.message}`)); 146 | // } 147 | // console.log(); 148 | 149 | await sleep(100); 150 | 151 | try { 152 | console.log(' total p', bench(' getElementsByTagName("p")', () => document.documentElement.getElementsByTagName('p').length, times)); 153 | } 154 | catch (o_O) { 155 | console.warn(clean(`⚠ \x1b[1merror\x1b[0m - unable to getElementsByTagName("p"): ${o_O.message}`)); 156 | } 157 | console.log(); 158 | 159 | logHeap('after querying heap'); 160 | console.log(); 161 | 162 | await sleep(100); 163 | 164 | try { 165 | const divs = document.documentElement.getElementsByTagName('div'); 166 | console.time(' removing divs'); 167 | divs.forEach(div => { 168 | div.remove(); 169 | }); 170 | console.timeEnd(' removing divs'); 171 | } 172 | catch (o_O) { 173 | console.warn(clean(`⚠ \x1b[1merror\x1b[0m - unable to div.remove() them all: ${o_O.message}`)); 174 | } 175 | console.log(); 176 | 177 | await sleep(100); 178 | 179 | try { 180 | console.log(' total div', bench(' div count', () => document.documentElement.getElementsByTagName('div').length, 1)); 181 | } 182 | catch (o_O) { 183 | console.warn(clean(`⚠ \x1b[1merror\x1b[0m - unable to getElementsByTagName("div"): ${o_O.message}`)); 184 | } 185 | console.log(); 186 | 187 | await sleep(100); 188 | 189 | try { 190 | console.log(' total p', bench(' p count', () => document.documentElement.getElementsByTagName('p').length, 1)); 191 | } 192 | catch (o_O) { 193 | console.warn(clean(`⚠ \x1b[1merror\x1b[0m - unable to getElementsByTagName("p"): ${o_O.message}`)); 194 | } 195 | console.log(); 196 | 197 | await sleep(100); 198 | 199 | logHeap('after removing divs heap'); 200 | console.log(); 201 | 202 | if (cloneBench) { 203 | try { 204 | const html = bench(' html.cloneNode(true)', () => document.documentElement.cloneNode(true), 1); 205 | console.log(' cloneNode: OK'); 206 | console.log(); 207 | 208 | await sleep(100); 209 | const outerHTML = bench(' html.outerHTML', () => html.outerHTML, times); 210 | 211 | if (outerHTML.length !== document.documentElement.outerHTML.length) 212 | throw new Error('invalid output'); 213 | console.log(' outcome: OK'); 214 | } 215 | catch (o_O) { 216 | console.warn(clean(`⚠ \x1b[1merror\x1b[0m - unable to clone html: ${o_O.message}`)); 217 | } 218 | console.log(); 219 | } 220 | 221 | if (cloneBench) { 222 | await sleep(100); 223 | console.time(' html.innerHTML'); 224 | document.documentElement.innerHTML = document.documentElement.innerHTML; 225 | console.timeEnd(' html.innerHTML'); 226 | } 227 | 228 | if (mutationObserver) { 229 | console.log(); 230 | console.log(clean('\x1b[1mobserved mutations: \x1b[0m' + observed)); 231 | } 232 | 233 | console.log(); 234 | console.timeEnd(clean('\x1b[1mtotal benchmark time\x1b[0m')); 235 | }; 236 | 237 | try { 238 | module.exports = onContent; 239 | } 240 | catch (o_O) { 241 | browser = true; 242 | onContent( 243 | html => { 244 | return (new DOMParser).parseFromString(html, 'text/html'); 245 | }, 246 | `${document.documentElement.outerHTML}`, 247 | 2 248 | ); 249 | } 250 | -------------------------------------------------------------------------------- /test/benchmark/linkedom-cached.js: -------------------------------------------------------------------------------- 1 | const benchmark = require('./index.js'); 2 | const { DOMParser } = require('linkedom/cached'); 3 | const dp = new DOMParser; 4 | 5 | benchmark('linkedom cached', html => dp.parseFromString(html, 'text/html')); 6 | -------------------------------------------------------------------------------- /test/benchmark/linkedom.js: -------------------------------------------------------------------------------- 1 | const benchmark = require('./index.js'); 2 | const { DOMParser } = require('linkedom'); 3 | const dp = new DOMParser; 4 | 5 | benchmark('linkedom', html => dp.parseFromString(html, 'text/html')); 6 | -------------------------------------------------------------------------------- /test/blank-template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 92 | 93 | -------------------------------------------------------------------------------- /test/coverage.js: -------------------------------------------------------------------------------- 1 | import('../esm/dom/index.js').then(({ DOMParser }) => { 2 | 3 | const document = (new DOMParser).parseFromString('...', 'text/html'); 4 | const { constructor: HTMLElement } = document.createElement('e'); 5 | Object.defineProperty( 6 | HTMLElement.prototype, 7 | 'getOnly', 8 | { get() { return 'OK' }, 9 | }); 10 | 11 | const { render, html, svg, htmlFor } = require('../cjs/init.js')(document); 12 | 13 | const htmlNode = (template, ...values) => htmlFor({})(template, ...values); 14 | 15 | const {Event} = document.defaultView; 16 | 17 | const {body} = document; 18 | 19 | const elementA = htmlNode`
foo
`; 20 | const elementB = htmlNode` 21 |
bar
22 | `; 23 | 24 | console.assert(elementA instanceof HTMLElement, 'elementA not instanceof HTMLElement'); 25 | console.assert(elementB instanceof HTMLElement, 'elementB not instanceof HTMLElement'); 26 | 27 | const fragment = () => html`

1

2

`; 28 | const variousContent = content => html`${content}`; 29 | 30 | render(body, html`this is a test`); 31 | render(body, html`this is a ${ 32 | [1, 2].map(n => html`${n}`) 33 | } test`); 34 | render(body, html`this is a ${ 35 | [1, 2].map(n => svg`${n}`) 36 | } test`); 37 | 38 | (function twice(i) { 39 | render(body, html`this is a ${ 40 | (i ? [1, 2, 3] : [1, 2]).map(n => svg`${n}`) 41 | } test`); 42 | if (i--) twice(i); 43 | }(1)); 44 | 45 | render(body, html`this is a ${'test'}`); 46 | render(body, html`this is a ${true}`); 47 | render(body, html`this is a ${1} ${2} ${3}`); 48 | render(body, html`this is a ${1}`); 49 | 50 | let div = document.createElement('div'); 51 | render(div, htmlNode`this is a test`); 52 | render(div, htmlFor(body)`this is a test`); 53 | render(div, htmlFor(body, 1)`this is a test`); 54 | render(div, () => htmlFor(body)`this is a test`); 55 | render(div, () => htmlFor(body, 1)`this is a test`); 56 | (function twice(i) { 57 | render(div, () => htmlFor(body)`this is a test`); 58 | render(div, () => htmlFor(body, 1)`this is a test`); 59 | if (i--) twice(i); 60 | }(1)); 61 | 62 | let clicked = false; 63 | render(div, html`
{ clicked = true; }} .disabled=${true} .contentEditable=${false} null=${null} />`); 64 | div.firstElementChild.dispatchEvent(new Event('click')); 65 | console.assert(clicked, '@click worked'); 66 | 67 | clicked = false; 68 | render(div, html`
{ clicked = true; }} .disabled=${true} .contentEditable=${false} null=${null} />`); 69 | div.firstElementChild.dispatchEvent(new Event('click')); 70 | console.assert(clicked, 'onclick worked'); 71 | 72 | const textArea = content => html``; 73 | const style = content => html``; 74 | render(document.createElement('div'), textArea('test')); 75 | render(document.createElement('div'), textArea(null)); 76 | render(document.createElement('div'), style('test')); 77 | render(document.createElement('div'), style(void 0)); 78 | 79 | const sameWire = content => html`
${content}
`; 80 | render(div, sameWire([fragment()])); 81 | render(div, sameWire([])); 82 | render(div, sameWire([fragment()])); 83 | 84 | render(div, html``); 85 | render(div, html`
`); 86 | console.assert(div.firstChild.getAttribute('getOnly') === 'yup'); 87 | 88 | render(div, variousContent([ 89 | html`

`, 90 | html`

` 91 | ])); 92 | render(div, variousContent([ 93 | html`

`, 94 | html`

`, 95 | html`

` 96 | ])); 97 | render(div, variousContent([ 98 | html`

` 99 | ])); 100 | 101 | render(div, html``); 102 | 103 | const oneHoleContent = content => html`${content}`; 104 | render(div, oneHoleContent(html`OK`)); 105 | render(div, oneHoleContent('text')); 106 | console.assert(div.textContent === 'text'); 107 | render(div, oneHoleContent(null)); 108 | console.assert(div.textContent === ''); 109 | render(div, oneHoleContent(void 0)); 110 | console.assert(div.textContent === ''); 111 | 112 | const reference = {}; 113 | render(div, html`

test
`); 114 | console.assert(reference.hasOwnProperty('current')); 115 | 116 | const fnReference = node => { fnReference.node = node; }; 117 | render(div, html`
test
`); 118 | console.assert(fnReference.node === div.firstElementChild); 119 | 120 | const withHandler = handler => html`
`; 121 | render(div, withHandler(Object)); 122 | render(div, withHandler(Object)); 123 | render(div, withHandler(String)); 124 | render(div, withHandler(null)); 125 | render(div, withHandler([Object, false])); 126 | 127 | const withAttribute = value => html`
`; 128 | render(div, withAttribute(null)); 129 | render(div, withAttribute('test')); 130 | render(div, withAttribute('test')); 131 | render(div, withAttribute(null)); 132 | render(div, withAttribute('test')); 133 | 134 | const withText = value => html``; 135 | render(div, withText('test')); 136 | render(div, withText('test')); 137 | render(div, withText(null)); 138 | render(div, withText('test')); 139 | 140 | render(div, html`${document.createDocumentFragment()}`); 141 | 142 | const wire1 = html`

`; 143 | const wire2 = html`

`; 144 | const wire = what => html`${what}`; 145 | render(div, wire([wire1, fragment(), wire2])); 146 | render(div, wire([wire2, fragment(), wire1])); 147 | 148 | render(div, html``); 149 | render(div, html``); 150 | 151 | try { 152 | render(div, html`

`); 153 | console.assert(false, 'broken template is not breaking'); 154 | } catch (OK) {} 155 | 156 | const otherWire = (className, text, content) => html`
${content}
`; 157 | render(div, otherWire('some', 'border:1px solid black', 'test')); 158 | console.assert(div.firstElementChild.className === 'some', 'semiDirect set'); 159 | render(div, otherWire(null, null, 'test')); 160 | console.assert(!div.firstElementChild.hasAttribute('class'), 'semiDirect null'); 161 | render(div, otherWire('other', '', 'test')); 162 | console.assert(div.firstElementChild.className === 'other', 'semiDirect set again'); 163 | render(div, otherWire(document.createElement('p'))); 164 | 165 | const sameAttribute = value => html`
`; 166 | render(body, sameAttribute(1)); 167 | render(body, sameAttribute(null)); 168 | render(body, sameAttribute(null)); 169 | render(body, sameAttribute(2)); 170 | render(body, sameAttribute(3)); 171 | 172 | render(body, html`

${'hole'}

`); 173 | render(body, html`

${{no: "op"}}

`); 174 | render(body, html`

test

`); 175 | render(body, html`

test

test

`); 176 | render(body, html`${fragment()}`); 177 | render(body, html`${fragment()}`); 178 | render(body, html`${[fragment()]}`); 179 | render(body, html`

${'content'}

`); 180 | render(body, html`
{}} .disabled=${true} .contentEditable=${false} null=${null} />`); 181 | render(body, variousContent([ 182 | html`

`, 183 | html`

` 184 | ])); 185 | render(body, variousContent([ 186 | html`

`, 187 | html`

`, 188 | html`

` 189 | ])); 190 | render(body, variousContent([ 191 | html`

` 192 | ])); 193 | 194 | render(body, html`

`); 195 | console.assert(body.firstElementChild.getAttribute('role') === 'button', 'aria=${role}'); 196 | console.assert(body.firstElementChild.getAttribute('aria-labelledBy') === 'id', 'aria=${labelledBy}'); 197 | render(body, html`
`); 198 | 199 | render(body, html`
`); 200 | console.assert(body.firstElementChild.dataset.labelledBy === 'id', '.dataset=${...}'); 201 | render(body, html`
`); 202 | 203 | render(body, html`
`); 204 | console.assert(body.firstElementChild.getAttribute('thing') === '', '?thing=${truthy}'); 205 | 206 | render(body, html`
`); 207 | console.assert(!body.firstElementChild.hasAttribute('thing'), '?thing=${falsy}'); 208 | 209 | const handler = () => {}; 210 | const withComplexHandler = handler => html`
`; 211 | render(body, withComplexHandler(handler)); 212 | render(body, withComplexHandler(() => {})); 213 | render(body, withComplexHandler(null)); 214 | render(body, withComplexHandler(void 0)); 215 | render(body, withComplexHandler([handler, { once: true }])); 216 | render(body, withComplexHandler([() => {}, { once: true }])); 217 | render(body, withComplexHandler([null, { once: true }])); 218 | render(body, withComplexHandler([void 0, { once: true }])); 219 | 220 | const uhtml = require('../cjs/init.js')(document); 221 | uhtml.render(body, uhtml.html`${456}`); 222 | 223 | for (const file of require('fs').readdirSync(`${__dirname}/dom`)) { 224 | if (file.endsWith('.js')) import(`${__dirname}/dom/${file}`); 225 | } 226 | 227 | }); 228 | -------------------------------------------------------------------------------- /test/csp.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /test/custom-element.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | uhtml 7 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /test/dataset.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 26 | 27 | -------------------------------------------------------------------------------- /test/dbmonster.css: -------------------------------------------------------------------------------- 1 | body { 2 | color: #333; 3 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 4 | font-size: 14px; 5 | line-height: 1.42857143; 6 | margin: 0; 7 | } 8 | 9 | label { 10 | display: inline-block; 11 | font-weight: 700; 12 | margin-bottom: 5px; 13 | } 14 | 15 | #range { 16 | display: flex; 17 | } 18 | 19 | input[type=range] { 20 | display: block; 21 | width: 100%; 22 | margin-bottom: 10px; 23 | margin-top: 5px; 24 | } 25 | 26 | table { 27 | border-collapse: collapse; 28 | border-spacing: 0; 29 | } 30 | 31 | :before, 32 | :after { 33 | box-sizing: border-box; 34 | } 35 | 36 | .table > thead > tr > th, 37 | .table > tbody > tr > th, 38 | .table > tfoot > tr > th, 39 | .table > thead > tr > td, 40 | .table > tbody > tr > td, 41 | .table > tfoot > tr > td { 42 | border-top: 1px solid #ddd; 43 | line-height: 1.42857143; 44 | padding: 8px; 45 | vertical-align: top; 46 | } 47 | 48 | .table { 49 | width: 100%; 50 | } 51 | 52 | .table-striped > tbody > tr:nth-child(odd) > td, 53 | .table-striped > tbody > tr:nth-child(odd) > th { 54 | background: #f9f9f9; 55 | } 56 | 57 | .label { 58 | border-radius: .25em; 59 | color: #fff; 60 | display: inline; 61 | font-size: 75%; 62 | font-weight: 700; 63 | line-height: 1; 64 | padding: .2em .6em .3em; 65 | text-align: center; 66 | vertical-align: baseline; 67 | white-space: nowrap; 68 | } 69 | 70 | .label-success { 71 | background-color: #5cb85c; 72 | } 73 | 74 | .label-warning { 75 | background-color: #f0ad4e; 76 | } 77 | 78 | .popover { 79 | background-color: #fff; 80 | background-clip: padding-box; 81 | border: 1px solid #ccc; 82 | border: 1px solid rgba(0, 0, 0, .2); 83 | border-radius: 6px; 84 | box-shadow: 0 5px 10px rgba(0, 0, 0, .2); 85 | display: none; 86 | left: 0; 87 | max-width: 276px; 88 | padding: 1px; 89 | position: absolute; 90 | text-align: left; 91 | top: 0; 92 | white-space: normal; 93 | z-index: 1010; 94 | } 95 | 96 | .popover>.arrow:after { 97 | border-width: 10px; 98 | content: ""; 99 | } 100 | 101 | .popover.left { 102 | margin-left: -10px; 103 | } 104 | 105 | .popover.left > .arrow { 106 | border-right-width: 0; 107 | border-left-color: rgba(0, 0, 0, .25); 108 | margin-top: -11px; 109 | right: -11px; 110 | top: 50%; 111 | } 112 | 113 | .popover.left > .arrow:after { 114 | border-left-color: #fff; 115 | border-right-width: 0; 116 | bottom: -10px; 117 | content: " "; 118 | right: 1px; 119 | } 120 | 121 | .popover > .arrow { 122 | border-width: 11px; 123 | } 124 | 125 | .popover > .arrow, 126 | .popover>.arrow:after { 127 | border-color: transparent; 128 | border-style: solid; 129 | display: block; 130 | height: 0; 131 | position: absolute; 132 | width: 0; 133 | } 134 | 135 | .popover-content { 136 | padding: 9px 14px; 137 | } 138 | 139 | .Query { 140 | position: relative; 141 | } 142 | 143 | .Query:hover .popover { 144 | display: block; 145 | left: -100%; 146 | width: 100%; 147 | } -------------------------------------------------------------------------------- /test/dbmonster.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | uhtml dbmonster 5 | 6 | 7 | 8 | 72 | 73 | 74 |
75 |
76 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /test/dbmonster.js: -------------------------------------------------------------------------------- 1 | window.ENV = function () {'use strict'; 2 | var counter = 0; 3 | var data; 4 | var _base; 5 | if (!(_base = String.prototype).lpad) 6 | _base.lpad = function (padding, toLength) { 7 | return padding.repeat((toLength - this.length) / padding.length).concat(this); 8 | }; 9 | 10 | function formatElapsed(value) { 11 | var str = parseFloat(value).toFixed(2); 12 | if (value > 60) { 13 | var minutes = Math.floor(value / 60); 14 | var comps = (value % 60).toFixed(2).split('.'); 15 | var seconds = comps[0].lpad('0', 2); 16 | var ms = comps[1]; 17 | str = minutes + ":" + seconds + "." + ms; 18 | } 19 | return str; 20 | } 21 | 22 | function getElapsedClassName(elapsed) { 23 | var className = 'Query elapsed'; 24 | if (elapsed >= 10.0) { 25 | className += ' warn_long'; 26 | } 27 | else if (elapsed >= 1.0) { 28 | className += ' warn'; 29 | } 30 | else { 31 | className += ' short'; 32 | } 33 | return className; 34 | } 35 | 36 | function countClassName(queries) { 37 | var countClassName = "label"; 38 | if (queries >= 20) { 39 | countClassName += " label-important"; 40 | } 41 | else if (queries >= 10) { 42 | countClassName += " label-warning"; 43 | } 44 | else { 45 | countClassName += " label-success"; 46 | } 47 | return countClassName; 48 | } 49 | 50 | function updateQuery(object) { 51 | if (!object) { 52 | object = {}; 53 | } 54 | var elapsed = Math.random() * 15; 55 | object.elapsed = elapsed; 56 | object.formatElapsed = formatElapsed(elapsed); 57 | object.elapsedClassName = getElapsedClassName(elapsed); 58 | object.query = "SELECT blah FROM something"; 59 | object.waiting = Math.random() < 0.5; 60 | if (Math.random() < 0.2) { 61 | object.query = " in transaction"; 62 | } 63 | if (Math.random() < 0.1) { 64 | object.query = "vacuum"; 65 | } 66 | return object; 67 | } 68 | 69 | function cleanQuery(value) { 70 | if (value) { 71 | value.formatElapsed = ""; 72 | value.elapsedClassName = ""; 73 | value.query = ""; 74 | value.elapsed = null; 75 | value.waiting = null; 76 | } else { 77 | return { 78 | query: "***", 79 | formatElapsed: "", 80 | elapsedClassName: "" 81 | }; 82 | } 83 | } 84 | 85 | function generateRow(object, keepIdentity, counter) { 86 | var nbQueries = Math.floor((Math.random() * 10) + 1); 87 | if (!object) { 88 | object = {}; 89 | } 90 | object.lastMutationId = counter; 91 | object.nbQueries = nbQueries; 92 | if (!object.lastSample) { 93 | object.lastSample = {}; 94 | } 95 | if (!object.lastSample.topFiveQueries) { 96 | object.lastSample.topFiveQueries = []; 97 | } 98 | if (keepIdentity) { 99 | // for Angular optimization 100 | if (!object.lastSample.queries) { 101 | object.lastSample.queries = []; 102 | for (var l = 0; l < 12; l++) { 103 | object.lastSample.queries[l] = cleanQuery(); 104 | } 105 | } 106 | for (var j in object.lastSample.queries) { 107 | var value = object.lastSample.queries[j]; 108 | if (j <= nbQueries) { 109 | updateQuery(value); 110 | } else { 111 | cleanQuery(value); 112 | } 113 | } 114 | } else { 115 | object.lastSample.queries = []; 116 | for (var j = 0; j < 12; j++) { 117 | if (j < nbQueries) { 118 | var value = updateQuery(cleanQuery()); 119 | object.lastSample.queries.push(value); 120 | } else { 121 | object.lastSample.queries.push(cleanQuery()); 122 | } 123 | } 124 | } 125 | for (var i = 0; i < 5; i++) { 126 | var source = object.lastSample.queries[i]; 127 | object.lastSample.topFiveQueries[i] = source; 128 | } 129 | object.lastSample.nbQueries = nbQueries; 130 | object.lastSample.countClassName = countClassName(nbQueries); 131 | return object; 132 | } 133 | 134 | function getData(keepIdentity) { 135 | var oldData = data; 136 | if (!keepIdentity) { // reset for each tick when !keepIdentity 137 | data = []; 138 | for (var i = 1; i <= ENV.rows; i++) { 139 | data.push({ dbname: 'cluster' + i, query: "", formatElapsed: "", elapsedClassName: "" }); 140 | data.push({ dbname: 'cluster' + i + ' slave', query: "", formatElapsed: "", elapsedClassName: "" }); 141 | } 142 | } 143 | if (!data) { // first init when keepIdentity 144 | data = []; 145 | for (var i = 1; i <= ENV.rows; i++) { 146 | data.push({ dbname: 'cluster' + i }); 147 | data.push({ dbname: 'cluster' + i + ' slave' }); 148 | } 149 | oldData = data; 150 | } 151 | for (var i in data) { 152 | var row = data[i]; 153 | if (!keepIdentity && oldData && oldData[i]) { 154 | row.lastSample = oldData[i].lastSample; 155 | } 156 | if (!row.lastSample || Math.random() < ENV.mutations()) { 157 | counter = counter + 1; 158 | if (!keepIdentity) { 159 | row.lastSample = null; 160 | } 161 | generateRow(row, keepIdentity, counter); 162 | } else { 163 | data[i] = oldData[i]; 164 | } 165 | } 166 | return { 167 | toArray: function () { 168 | return data; 169 | } 170 | }; 171 | } 172 | 173 | var mutationsValue = 0.5; 174 | 175 | function mutations(value) { 176 | if (value) { 177 | mutationsValue = value; 178 | return mutationsValue; 179 | } else { 180 | return mutationsValue; 181 | } 182 | } 183 | 184 | var body = document.querySelector('body'); 185 | var theFirstChild = body.firstChild; 186 | 187 | var sliderContainer = document.createElement('div'); 188 | sliderContainer.style.cssText = "display: flex"; 189 | var slider = document.createElement('input'); 190 | var text = document.createElement('label'); 191 | text.innerHTML = 'mutations : ' + (mutationsValue * 100).toFixed(0) + '%'; 192 | text.id = "ratioval"; 193 | slider.setAttribute("type", "range"); 194 | slider.style.cssText = 'margin-bottom: 10px; margin-top: 5px'; 195 | slider.addEventListener('change', function (e) { 196 | ENV.mutations(e.target.value / 100); 197 | document.querySelector('#ratioval').innerHTML = 'mutations : ' + (ENV.mutations() * 100).toFixed(0) + '%'; 198 | }); 199 | sliderContainer.appendChild(text); 200 | sliderContainer.appendChild(slider); 201 | body.insertBefore(sliderContainer, theFirstChild); 202 | 203 | return { 204 | generateData: getData, 205 | rows: 50, 206 | timeout: 0, 207 | mutations: mutations 208 | }; 209 | }(); -------------------------------------------------------------------------------- /test/diffing.js: -------------------------------------------------------------------------------- 1 | import {render, html} from './instrumented/index.js'; 2 | 3 | const {body} = document; 4 | 5 | const createList = (...args) => html`
${args}
`; 6 | const testDiff = (a, b, c, d, e, f, g, h, i, j, k) => { 7 | render(body, createList()); 8 | render(body, createList(b, c, d)); 9 | render(body, createList(a, b, c, d)); 10 | render(body, createList(d, c, b, a)); 11 | render(body, createList(a, b, c, d)); 12 | render(body, createList(a, b, c, d, e, f)); 13 | render(body, createList(a, b, c, g, h, i, d, e, f)); 14 | render(body, createList(a, b, c, g, h, i, d, e)); 15 | render(body, createList(c, g, h, i, d, e)); 16 | render(body, createList(c, g, d, e)); 17 | render(body, createList()); 18 | render(body, createList(a, b, c, d, e, f)); 19 | render(body, createList(a, b, g, i, d, e, f)); 20 | render(body, createList(a, b, c, d, e, f)); 21 | render(body, createList(j, g, a, b, c, d, e, f, h, i)); 22 | render(body, createList(a, b, c, d, e, f)); 23 | render(body, createList(a, g, c, d, h, i)); 24 | render(body, createList(i, g, a, d, h, c)); 25 | render(body, createList(c, h, d, a, g, i)); 26 | render(body, createList(d, f, g)); 27 | render(body, createList(a, b, c, d, f, g)); 28 | render(body, createList(a, b, c, d, e, f, g)); 29 | render(body, createList(g, f, e, d, c, b, a)); 30 | render(body, createList(f, d, b, a, e, g)); 31 | render(body, createList(a, b, c, d, e, f)); 32 | render(body, createList(a, b, c, d, e, f, h, i, j)); 33 | render(body, createList(a, b, c, d, e, h, f, i, j)); 34 | render(body, createList(a, b, i, d, e, h, f, c, j)); 35 | render(body, createList(a, b, c, d, e, f, h, i, j)); 36 | render(body, createList(a, b, c, d, e, f, g, h, i, j, k)); 37 | render(body, createList(g, h, i)); 38 | render(body, createList(a, b, c, d)); 39 | render(body, createList(b, c, a, d)); 40 | render(body, createList(a, b, c, d, e)); 41 | render(body, createList(d, a, b, c, f)); 42 | render(body, createList(a, d, e)); 43 | render(body, createList(d, f)); 44 | render(body, createList(b, d, c, k)); 45 | render(body, createList(c, k, b, d)); 46 | render(body, createList()); 47 | render(body, createList(a, b, c, d)); 48 | render(body, createList(a, b, d, e, c)); 49 | render(body, createList(a, b, c)); 50 | render(body, createList(c, a, b)); 51 | render(body, createList()); 52 | }; 53 | 54 | testDiff( 55 | html`

a

`, 56 | html`

b

`, 57 | html`

c

`, 58 | html`

d

`, 59 | html`

e

`, 60 | html`

f

`, 61 | html`

g

`, 62 | html`

h

`, 63 | html`

i

`, 64 | html`

j

`, 65 | html`

k

` 66 | ); 67 | 68 | testDiff( 69 | html`

a

a

`, 70 | html`

b

b

`, 71 | html`

c

c

`, 72 | html`

d

d

`, 73 | html`

e

e

`, 74 | html`

f

f

`, 75 | html`

g

g

`, 76 | html`

h

h

`, 77 | html`

i

i

`, 78 | html`

j

j

`, 79 | html`

k

k

` 80 | ); 81 | -------------------------------------------------------------------------------- /test/dom/attribute.js: -------------------------------------------------------------------------------- 1 | import Attribute from '../../esm/dom/attribute.js'; 2 | import Document from '../../esm/dom/document.js'; 3 | 4 | const document = new Document; 5 | 6 | const a = new Attribute('a'); 7 | const b = new Attribute('b', 2); 8 | 9 | console.assert(a.name === 'a'); 10 | console.assert(a.localName === 'a'); 11 | console.assert(a.nodeName === 'a'); 12 | console.assert(a.value === ''); 13 | console.assert(a.nodeValue === ''); 14 | console.assert(a.toString() === 'a'); 15 | a.value = 1; 16 | console.assert(a.value === '1'); 17 | console.assert(a.nodeValue === '1'); 18 | console.assert(a.toString() === 'a="1"'); 19 | 20 | console.assert(b.value === '2'); 21 | 22 | const element = document.createElement('test'); 23 | element.setAttribute('c', 3); 24 | console.assert(element.getAttributeNode('c').ownerElement === element); 25 | 26 | 27 | console.assert(a.ownerElement == null); 28 | element.setAttributeNode(a); 29 | console.assert(a.ownerElement == element); 30 | -------------------------------------------------------------------------------- /test/dom/comment.js: -------------------------------------------------------------------------------- 1 | import Document from '../../esm/dom/document.js'; 2 | 3 | const document = new Document; 4 | 5 | const comment = document.createComment('test'); 6 | console.assert(!comment.childNodes.length); 7 | console.assert(comment.nodeName === '#comment'); 8 | console.assert(comment.toString() === ''); 9 | console.assert(comment.textContent === 'test'); 10 | comment.textContent = 'ok'; 11 | console.assert(comment.textContent === 'ok'); 12 | 13 | const clone = comment.cloneNode(); 14 | console.assert(clone.nodeName === '#comment'); 15 | console.assert(clone.toString() === ''); -------------------------------------------------------------------------------- /test/dom/document-fragment.js: -------------------------------------------------------------------------------- 1 | import Document from '../../esm/dom/document.js'; 2 | 3 | const document = new Document; 4 | 5 | const f1 = document.createDocumentFragment(); 6 | const f2 = document.createDocumentFragment(); 7 | 8 | console.assert(f1.nodeType === 11, 'nodeType'); 9 | console.assert(f1.nodeName === '#document-fragment', 'nodeName'); 10 | 11 | f1.append('b', 'c'); 12 | console.assert(f1.childNodes.length === 2, 'childNodes'); 13 | console.assert(f1.firstChild.parentNode === f1, 'parentNode'); 14 | 15 | f2.append(f1, 'd'); 16 | console.assert(f1.childNodes.length === 0, 'childNodes'); 17 | console.assert(f2.childNodes.length === 3, 'childNodes'); 18 | console.assert(f2.firstChild.parentNode === f2, 'parentNode'); 19 | 20 | f2.prepend('a'); 21 | 22 | document.body.append(f2); 23 | console.assert(f2.childNodes.length === 0, 'childNodes'); 24 | console.assert(document.body.firstChild.parentNode === document.body, 'parentNode'); 25 | 26 | console.assert(document.body.toString() === 'abcd'); 27 | 28 | f1.append(...document.body.childNodes); 29 | console.assert(f1.firstChild.parentNode === f1, 'parentNode'); 30 | console.assert(document.body.childNodes.length === 0, 'childNodes'); 31 | 32 | console.assert(f1.toString() === 'abcd'); 33 | 34 | const f3 = f1.cloneNode(); 35 | console.assert(f3.toString() === ''); 36 | const f4 = f1.cloneNode(true); 37 | console.assert(f4.firstChild !== f1.firstChild); 38 | console.assert(f4.toString() === f1.toString()); 39 | console.assert(f4.firstChild.parentNode === f4); 40 | console.assert(f1.firstChild.parentNode === f1); 41 | -------------------------------------------------------------------------------- /test/dom/document-type.js: -------------------------------------------------------------------------------- 1 | import Document from '../../esm/dom/document.js'; 2 | import DocumentType from '../../esm/dom/document-type.js'; 3 | 4 | const document = new Document; 5 | 6 | let { doctype } = document; 7 | console.assert(doctype.toString() === ''); 8 | console.assert(doctype.nodeName === 'html'); 9 | console.assert(doctype.name === 'html'); 10 | console.assert(doctype.ownerDocument === document); 11 | 12 | doctype = new DocumentType(''); 13 | console.assert(doctype.ownerDocument === null); 14 | console.assert(doctype.toString() === ''); -------------------------------------------------------------------------------- /test/dom/document.js: -------------------------------------------------------------------------------- 1 | import Document from '../../esm/dom/document.js'; 2 | 3 | const document = new Document; 4 | 5 | const { documentElement, body, head } = document; 6 | 7 | const attribute = document.createAttribute('lang'); 8 | attribute.value = 'en'; 9 | documentElement.setAttributeNode(attribute); 10 | 11 | const ce = document.createElement('a', { is: 'a-link' }); 12 | body.appendChild(ce); 13 | 14 | console.assert(document.toString() === ''); 15 | console.assert(head === documentElement.firstElementChild); 16 | console.assert(document.getElementsByTagName('html').length === 1); 17 | 18 | const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); 19 | console.assert('ownerSVGElement' in svg); 20 | console.assert('ownerSVGElement' in svg.cloneNode()); 21 | console.assert(svg.ownerSVGElement === null); 22 | console.assert(svg.toString() === ''); 23 | 24 | const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); 25 | svg.append(rect); 26 | console.assert(rect.ownerSVGElement === svg); 27 | console.assert(svg.toString() === ''); 28 | rect.setAttribute('x', 1); 29 | console.assert(svg.toString() === ''); 30 | console.assert('ownerSVGElement' in svg.cloneNode(true)); 31 | 32 | const inner = rect.appendChild(document.createElementNS('http://www.w3.org/2000/svg', 'rect')); 33 | console.assert(inner.ownerSVGElement === svg); 34 | 35 | const tree = document.createElement('div'); 36 | let tree0, tree1, tree2; 37 | tree.append( 38 | tree0 = document.createElement('div'), 39 | tree2 = document.createComment('2'), 40 | document.createTextNode('') 41 | ); 42 | tree0.append( 43 | tree1 = document.createComment('1') 44 | ); 45 | 46 | const tw = document.createTreeWalker(tree, 1 | 128); 47 | 48 | console.assert(tw.nextNode() === tree0); 49 | console.assert(tw.nextNode() === tree1); 50 | console.assert(tw.nextNode() === tree2); 51 | console.assert(tw.nextNode() == null); 52 | -------------------------------------------------------------------------------- /test/dom/dom-parser.js: -------------------------------------------------------------------------------- 1 | import DOMParser from '../../esm/dom/dom-parser.js'; 2 | 3 | const dp = new DOMParser; 4 | 5 | let html = dp.parseFromString('...'); 6 | 7 | console.assert(html.toString() === ''); 8 | 9 | html = dp.parseFromString('

'); 10 | 11 | console.assert(html.toString() === '

'); 12 | 13 | let svg = dp.parseFromString('', 'svg'); 14 | 15 | console.log(svg.toString()); 16 | -------------------------------------------------------------------------------- /test/dom/element.js: -------------------------------------------------------------------------------- 1 | import Document from '../../esm/dom/document.js'; 2 | 3 | const document = new Document; 4 | 5 | const element = document.createElement('element'); 6 | const text = document.createTextNode('text'); 7 | 8 | console.assert(element.localName === 'element'); 9 | console.assert(element.nodeName === 'ELEMENT'); 10 | console.assert(element.tagName === 'ELEMENT'); 11 | console.assert(element.innerHTML === ''); 12 | console.assert(element.outerHTML === ''); 13 | console.assert(element.previousSibling == null); 14 | console.assert(element.previousElementSibling == null); 15 | console.assert(element.nextSibling == null); 16 | console.assert(element.nextElementSibling == null); 17 | 18 | console.assert(element.getAttributeNames().length === 0); 19 | console.assert(!element.hasAttribute('style')); 20 | console.assert(element.style.cssText === ''); 21 | element.style.cssText = 'margin: 0;'; 22 | console.assert(element.hasAttribute('style')); 23 | console.assert(element.style.cssText === 'margin: 0;'); 24 | element.removeAttribute('style'); 25 | 26 | const br = document.createElement('br'); 27 | console.assert(br.outerHTML === '
'); 28 | 29 | document.body.append(element, text, br); 30 | console.assert(element.previousSibling == null); 31 | console.assert(br.previousSibling == text); 32 | console.assert(br.previousElementSibling == element); 33 | console.assert(element.nextElementSibling == br); 34 | console.assert(element.nextSibling == text); 35 | console.assert(br.nextSibling == null); 36 | console.assert(JSON.stringify(element.getAttributeNames()) === '[]'); 37 | console.assert(!element.hasAttributes()); 38 | console.assert(!element.hasAttribute('id')); 39 | console.assert(element.id === ''); 40 | element.id = 'element'; 41 | console.assert(element.hasAttributes()); 42 | console.assert(element.hasAttribute('id')); 43 | console.assert(element.id === 'element'); 44 | console.assert(element.className === ''); 45 | element.className = 'element'; 46 | console.assert(element.className === 'element'); 47 | console.assert(document.getElementsByClassName('element').length === 1); 48 | console.assert(element.toString() === ''); 49 | console.assert(document.getElementById('element') === element); 50 | console.assert(JSON.stringify(element.getAttributeNames()) === '["id","class"]'); 51 | br.id = 'br'; 52 | console.assert(br.toString() === '
'); 53 | 54 | const cloneBR = br.cloneNode(); 55 | console.assert(cloneBR.toString() === '
'); 56 | 57 | element.replaceWith(text); 58 | console.assert(document.body.toString() === 'text
'); 59 | console.assert(br.previousElementSibling === null); 60 | br.after(element); 61 | console.assert(document.body.toString() === 'text
'); 62 | br.after(text); 63 | console.assert(document.body.toString() === '
text'); 64 | 65 | console.assert(document.body.cloneNode(true).toString() === document.body.toString()); 66 | 67 | br.before(element); 68 | console.assert(document.body.toString() === '
text'); 69 | console.assert(br.nextElementSibling === null); 70 | br.before(text); 71 | console.assert(document.body.toString() === 'text
'); 72 | 73 | br.removeAttribute('class'); 74 | br.removeAttribute('id'); 75 | console.assert(br.outerHTML === '
'); 76 | 77 | br.toggleAttribute('hidden'); 78 | console.assert(br.outerHTML === ''); 79 | const hidden = br.getAttributeNode('hidden'); 80 | br.setAttribute('hidden', ''); 81 | console.assert(br.getAttributeNode('hidden') === hidden); 82 | br.setAttributeNode(hidden); 83 | console.assert(br.getAttributeNode('hidden') === hidden); 84 | br.toggleAttribute('hidden'); 85 | console.assert(br.outerHTML === '
'); 86 | br.toggleAttribute('hidden', false); 87 | console.assert(br.outerHTML === '
'); 88 | br.toggleAttribute('hidden', true); 89 | console.assert(br.outerHTML === ''); 90 | br.toggleAttribute('hidden', true); 91 | console.assert(br.outerHTML === ''); 92 | br.removeAttributeNode(br.attributes.hidden); 93 | console.assert(br.outerHTML === '
'); 94 | console.assert(br.getAttributeNode('hidden') === null); 95 | 96 | const { dataset } = br; 97 | delete dataset.testMe; 98 | console.assert(!('testMe' in dataset)); 99 | dataset.testMe = 1; 100 | console.assert('testMe' in dataset); 101 | console.assert(dataset.testMe === '1'); 102 | console.assert(br.outerHTML === '
'); 103 | console.assert(Reflect.ownKeys(dataset).join('') === 'testMe'); 104 | delete dataset.testMe; 105 | console.assert(br.outerHTML === '
'); 106 | 107 | const { classList } = br; 108 | console.assert(classList.length === 0); 109 | console.assert(classList.value === ''); 110 | console.assert(!classList.contains('a')); 111 | classList.add('a'); 112 | console.assert(classList.length === 1); 113 | console.assert(classList.value === 'a'); 114 | console.assert(classList.contains('a')); 115 | classList.add('b'); 116 | console.assert(classList.length === 2); 117 | console.assert(classList.value === 'a b'); 118 | classList.remove('a'); 119 | console.assert(classList.length === 1); 120 | console.assert(classList.value === 'b'); 121 | console.assert(!classList.contains('a')); 122 | console.assert(classList.contains('b')); 123 | classList.replace('b', 'a'); 124 | console.assert(classList.length === 1); 125 | console.assert(classList.value === 'a'); 126 | console.assert(!classList.contains('b')); 127 | console.assert(classList.contains('a')); 128 | classList.replace('c', 'b'); 129 | console.assert(classList.length === 1); 130 | console.assert(classList.value === 'a'); 131 | console.assert(classList.contains('a')); 132 | classList.toggle('b'); 133 | console.assert(classList.length === 2); 134 | console.assert(classList.value === 'a b'); 135 | console.assert(br.outerHTML === '
'); 136 | classList.toggle('b'); 137 | console.assert(classList.length === 1); 138 | console.assert(classList.value === 'a'); 139 | classList.toggle('a', true); 140 | console.assert(classList.length === 1); 141 | console.assert(classList.value === 'a'); 142 | console.assert(br.outerHTML === '
'); 143 | 144 | console.assert([...classList.keys()].join(',') === '0'); 145 | console.assert([...classList.values()].join(',') === 'a'); 146 | console.assert([...classList.entries()].join(',') === '0,a'); 147 | let each = []; 148 | classList.forEach(value => { 149 | each.push(value); 150 | }); 151 | console.assert(each.join(',') === 'a'); 152 | 153 | const template = document.createElement('template'); 154 | template.append('a', 'b'); 155 | const { content } = template; 156 | console.assert(content.childNodes.length === 2); 157 | console.assert(content.childNodes[0] !== template.childNodes[0]); 158 | 159 | const div = document.createElement('div'); 160 | div.innerHTML = ` 161 | Some text 162 | 163 |

some node

164 | & ]]> 165 | 166 | `; 167 | console.assert(div.outerHTML === ` 168 |
169 | Some text 170 | 171 |

some node

172 | 173 | 174 |
175 | `.trim()); 176 | console.assert(div.childNodes.every(node => node.parentNode === div)); 177 | console.assert(div.textContent.trim() === `Some text 178 | 179 | some node`); 180 | 181 | div.textContent = 'OK'; 182 | console.assert(div.outerHTML === `
OK
`); 183 | 184 | div.append('', '!', document.createElement('br')); 185 | console.assert(div.childNodes.length === 4); 186 | div.normalize(); 187 | console.assert(div.childNodes.length === 2); 188 | console.assert(div.outerHTML === `
OK!
`); 189 | -------------------------------------------------------------------------------- /test/dom/event.js: -------------------------------------------------------------------------------- 1 | import Document from '../../esm/dom/document.js'; 2 | 3 | const document = new Document; 4 | 5 | const { Event } = document.defaultView; 6 | 7 | const div = document.createElement('div'); 8 | const p = div.appendChild(document.createElement('p')); 9 | 10 | let invoked = false; 11 | 12 | const listener = event => { 13 | event.preventDefault(); 14 | console.assert(event.defaultPrevented, 'defaultPrevented'); 15 | 16 | event.stopPropagation(); 17 | event.stopImmediatePropagation(); 18 | console.assert(event.target === p, 'target'); 19 | console.assert(event.currentTarget === div, 'currentTarget'); 20 | }; 21 | 22 | div.addEventListener('click', listener); 23 | 24 | p.addEventListener('click', { 25 | handleEvent(event) { 26 | console.assert(event.bubbles, 'bubbles once'); 27 | console.assert(event.cancelable, 'cancelable once'); 28 | console.assert(!event.defaultPrevented); 29 | } 30 | }, { once: true }); 31 | 32 | p.addEventListener('click', event => { 33 | console.assert(event.bubbles, 'bubbles'); 34 | console.assert(event.cancelable, 'cancelable'); 35 | invoked = true; 36 | }); 37 | 38 | p.dispatchEvent(new Event('click', { bubbles: true, cancelable: true })); 39 | 40 | console.assert(invoked, 'invoked'); 41 | 42 | p.removeEventListener('click', {}); 43 | div.removeEventListener('click', listener); 44 | p.dispatchEvent(new Event('click', { bubbles: true, cancelable: true })); 45 | -------------------------------------------------------------------------------- /test/dom/named-node-map.js: -------------------------------------------------------------------------------- 1 | import Document from '../../esm/dom/document.js'; 2 | 3 | const document = new Document; 4 | 5 | const element = document.createElement('p'); 6 | const { attributes } = element; 7 | 8 | console.assert(attributes.length === 0); 9 | 10 | element.setAttribute('a', 1); 11 | console.assert(attributes.length === 1); 12 | console.assert(attributes[0] === element.getAttributeNode('a')); 13 | console.assert(attributes.a === element.getAttributeNode('a')); 14 | 15 | const attrs = [...attributes]; 16 | console.assert(attrs.length === 1); 17 | console.assert(attrs[0] === element.getAttributeNode('a')); 18 | 19 | console.assert(JSON.stringify(Reflect.ownKeys(attributes)) === '["0","a"]'); 20 | console.assert(attributes.b === void 0); 21 | console.assert(attributes[1] === void 0); 22 | 23 | console.assert('a' in attributes); 24 | console.assert(!('b' in attributes)); 25 | console.assert(!(1 in attributes)); 26 | -------------------------------------------------------------------------------- /test/dom/node.js: -------------------------------------------------------------------------------- 1 | import Document from '../../esm/dom/document.js'; 2 | 3 | const document = new Document; 4 | 5 | const [a, b] = [ 6 | document.createElement('a'), 7 | document.createElement('b'), 8 | ]; 9 | 10 | a.append(b); 11 | 12 | console.assert(!b.isConnected); 13 | document.body.append(a); 14 | console.assert(b.isConnected); 15 | 16 | console.assert(b.parentElement === a); 17 | console.assert(a.parentElement === document.body); 18 | console.assert(document.body.parentElement === document.documentElement); 19 | console.assert(document.documentElement.parentElement === null); 20 | -------------------------------------------------------------------------------- /test/dom/package.json: -------------------------------------------------------------------------------- 1 | {"type":"module"} -------------------------------------------------------------------------------- /test/dom/parent.js: -------------------------------------------------------------------------------- 1 | import Document from '../../esm/dom/document.js'; 2 | 3 | const document = new Document; 4 | 5 | const fragment = document.createDocumentFragment(); 6 | const [a, c] = [ 7 | document.createElement('a'), 8 | document.createElement('c'), 9 | ]; 10 | 11 | console.assert(fragment.childNodes.length === 0); 12 | console.assert(fragment.firstChild === null); 13 | console.assert(fragment.firstElementChild === null); 14 | console.assert(fragment.lastChild === null); 15 | console.assert(fragment.lastElementChild === null); 16 | 17 | fragment.append(a, 'b', c); 18 | 19 | console.assert(fragment.children.length === 2); 20 | console.assert(fragment.firstChild === a); 21 | console.assert(fragment.firstElementChild === a); 22 | console.assert(fragment.lastChild === c); 23 | console.assert(fragment.lastElementChild === c); 24 | 25 | console.assert(fragment.childElementCount === 2); 26 | 27 | fragment.prepend(c); 28 | console.assert(fragment.firstChild === c); 29 | fragment.replaceChildren(a); 30 | console.assert(fragment.contains(a)); 31 | console.assert(a.parentNode === fragment); 32 | console.assert(!fragment.contains(c)); 33 | fragment.insertBefore(c, a); 34 | console.assert(a.parentNode === fragment); 35 | console.assert(c.parentNode === fragment); 36 | console.assert(fragment.firstChild === c); 37 | fragment.removeChild(c); 38 | console.assert(a.parentNode === fragment); 39 | fragment.insertBefore(c); 40 | console.assert(fragment.lastChild === c); 41 | fragment.removeChild(c); 42 | console.assert(a.parentNode === fragment); 43 | console.assert(c.parentNode === null); 44 | console.assert(fragment.replaceChild(c, a) === a); 45 | console.assert(a.parentNode === null); 46 | console.assert(c.parentNode === fragment); 47 | console.assert(fragment.toString() === ''); 48 | c.append(a); 49 | console.assert(fragment.toString() === ''); 50 | console.assert(fragment.contains(a)); 51 | -------------------------------------------------------------------------------- /test/dom/range.js: -------------------------------------------------------------------------------- 1 | import Document from '../../esm/dom/document.js'; 2 | 3 | const document = new Document; 4 | 5 | const p = document.createElement('p'); 6 | const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); 7 | const range = document.createRange(); 8 | 9 | p.append('a', 'b', 'c', 'd', 'e'); 10 | 11 | let node = p.childNodes[1]; 12 | 13 | range.setStartBefore(node); 14 | range.setEndAfter(node); 15 | range.deleteContents(); 16 | 17 | console.assert(p.toString() === '

acde

'); 18 | console.assert(node.parentNode === null); 19 | 20 | range.setStartAfter(p.childNodes[0]); 21 | range.setEndAfter(p.childNodes[2]); 22 | range.deleteContents(); 23 | console.assert(p.toString() === '

ae

'); 24 | 25 | range.selectNodeContents(svg); 26 | 27 | const fragment = range.createContextualFragment( 28 | '' 29 | ); 30 | 31 | console.assert(fragment.toString() === ''); 32 | -------------------------------------------------------------------------------- /test/dom/text.js: -------------------------------------------------------------------------------- 1 | import Document from '../../esm/dom/document.js'; 2 | 3 | const document = new Document; 4 | 5 | const text = document.createTextNode('&'); 6 | console.assert(text.nodeName === '#text'); 7 | console.assert(text.toString() === '&'); 8 | console.assert(text.textContent === '&'); 9 | document.createElement('textarea').appendChild(text); 10 | text.textContent = 'ok'; 11 | console.assert(text.textContent === 'ok'); 12 | text.textContent = '&'; 13 | console.assert(text.textContent === '&'); 14 | console.assert(text.toString() === '&'); 15 | -------------------------------------------------------------------------------- /test/dom/utils.js: -------------------------------------------------------------------------------- 1 | import { gPD, set, newRange } from '../../esm/utils.js'; 2 | import Document from '../../esm/dom/document.js'; 3 | 4 | globalThis.document = new Document; 5 | 6 | const map = new Map; 7 | console.assert(set(map, 'key', 'value') === 'value'); 8 | 9 | console.assert(JSON.stringify(gPD({}, 'hasOwnProperty')) === '{"writable":true,"enumerable":false,"configurable":true}'); 10 | 11 | // TODO 12 | newRange(); 13 | -------------------------------------------------------------------------------- /test/empty.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | uhtml 7 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /test/fragment.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /test/fw-bench/css/bootstrap/dist/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WebReflection/uhtml/fbae9184611abc730cebe325542dc355a8317d72/test/fw-bench/css/bootstrap/dist/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /test/fw-bench/css/bootstrap/dist/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WebReflection/uhtml/fbae9184611abc730cebe325542dc355a8317d72/test/fw-bench/css/bootstrap/dist/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /test/fw-bench/css/bootstrap/dist/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WebReflection/uhtml/fbae9184611abc730cebe325542dc355a8317d72/test/fw-bench/css/bootstrap/dist/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /test/fw-bench/css/currentStyle.css: -------------------------------------------------------------------------------- 1 | @import url("/css/bootstrap/dist/css/bootstrap.min.css"); 2 | @import url("/css/main.css"); -------------------------------------------------------------------------------- /test/fw-bench/css/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 10px 0 0 0; 3 | margin: 0; 4 | overflow-y: scroll; 5 | } 6 | #duration { 7 | padding-top: 0px; 8 | } 9 | .jumbotron { 10 | padding-top:10px; 11 | padding-bottom:10px; 12 | } 13 | .test-data a { 14 | display: block; 15 | } 16 | .preloadicon { 17 | position: absolute; 18 | top:-20px; 19 | left:-20px; 20 | } 21 | .col-sm-6.smallpad { 22 | padding: 5px; 23 | } 24 | .jumbotron .row h1 { 25 | font-size: 40px; 26 | } -------------------------------------------------------------------------------- /test/fw-bench/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | µhtml keyed 4 | 5 |
6 | -------------------------------------------------------------------------------- /test/fw-bench/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-framework-benchmark-uhtml", 3 | "version": "1.0.0", 4 | "description": "uhtml demo", 5 | "main": "index.js", 6 | "js-framework-benchmark": { 7 | "frameworkVersionFromPackage": "uhtml", 8 | "issues": [ 9 | 772 10 | ] 11 | }, 12 | "scripts": { 13 | "build-dev": "rollup -c -w", 14 | "build-prod": "rollup -c" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/krausest/js-framework-benchmark.git" 19 | }, 20 | "keywords": [ 21 | "uhtml" 22 | ], 23 | "author": "Mathis Zeiher", 24 | "license": "Apache-2.0", 25 | "bugs": { 26 | "url": "https://github.com/krausest/js-framework-benchmark/issues" 27 | }, 28 | "homepage": "https://github.com/krausest/js-framework-benchmark#readme", 29 | "dependencies": { 30 | "js-framework-benchmark-utils": "^0.3.2" 31 | }, 32 | "devDependencies": { 33 | "@rollup/plugin-node-resolve": "^13.0.0", 34 | "rollup": "^3.29.5", 35 | "rollup-plugin-includepaths": "^0.2.4", 36 | "rollup-plugin-minify-html-literals": "^1.2.6", 37 | "rollup-plugin-terser": "^7.0.2" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /test/fw-bench/rollup.config.js: -------------------------------------------------------------------------------- 1 | import minifyHTML from 'rollup-plugin-minify-html-literals'; 2 | import {nodeResolve} from '@rollup/plugin-node-resolve'; 3 | import {terser} from 'rollup-plugin-terser'; 4 | import includePaths from 'rollup-plugin-includepaths'; 5 | 6 | export default { 7 | input: 'src/index.js', 8 | plugins: [ 9 | minifyHTML({ 10 | options: { 11 | minifyOptions: { 12 | keepClosingSlash: true 13 | } 14 | } 15 | }), 16 | includePaths({ 17 | include: { 18 | 'uhtml': '../../esm.js' 19 | }, 20 | }), 21 | nodeResolve(), 22 | terser() 23 | ], 24 | output: { 25 | esModule: false, 26 | file: 'dist/index.js', 27 | exports: 'named', 28 | format: 'iife', 29 | name: 'app' 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /test/fw-bench/src/index.js: -------------------------------------------------------------------------------- 1 | import {State} from 'js-framework-benchmark-utils'; 2 | import {html, htmlFor, render} from '../../../keyed.js'; 3 | 4 | import Jumbotron from './jumbotron.js'; 5 | import Table from './table.js'; 6 | 7 | const state = State(Table, false, htmlFor); 8 | 9 | render(document.getElementById('container'), html` 10 |
11 | ${Jumbotron(state)} 12 | ${Table(state)} 13 |
15 | `); 16 | -------------------------------------------------------------------------------- /test/fw-bench/src/jumbotron.js: -------------------------------------------------------------------------------- 1 | import {html} from '../../../keyed.js'; 2 | 3 | export default ({run, runLots, add, update, clear, swapRows}) => html` 4 |
5 |
6 |
7 |

µhtml keyed

8 |
9 |
10 |
11 |
12 | 14 |
15 |
16 | 18 |
19 |
20 | 22 |
23 |
24 | 26 |
27 |
28 | 30 |
31 |
32 | 34 |
35 |
36 |
37 |
38 |
39 | `; 40 | -------------------------------------------------------------------------------- /test/fw-bench/src/table-delegate.js: -------------------------------------------------------------------------------- 1 | import {htmlFor} from '../../../keyed.js'; 2 | 3 | const handler = ({currentTarget, target}) => { 4 | const a = target.closest('a'); 5 | const {action} = a.dataset; 6 | currentTarget.state[action](+a.closest('tr').id); 7 | }; 8 | 9 | export default (state) => { 10 | const {data, selected} = state; 11 | return htmlFor(state)` 12 | 14 | ${ 15 | data.map(item => { 16 | const {id, label} = item; 17 | return htmlFor(data, id)` 18 | 19 | 20 | 23 | 28 | `; 30 | }) 31 | } 32 |
${id} 21 | ${label} 22 | 24 | 25 | 27 | 29 |
33 | `; 34 | }; 35 | -------------------------------------------------------------------------------- /test/fw-bench/src/table-tr.js: -------------------------------------------------------------------------------- 1 | import {htmlFor} from '../../../keyed.js'; 2 | 3 | const stateHandler = new WeakMap; 4 | 5 | export default (state) => { 6 | if (!stateHandler.has(state)) 7 | stateHandler.set(state, [ 8 | ({currentTarget, target}) => { 9 | const a = target.closest('a'); 10 | const {action} = a.dataset; 11 | state[action](+currentTarget.id); 12 | }, 13 | false 14 | ]); 15 | 16 | const handler = stateHandler.get(state); 17 | const {data, selected} = state; 18 | return htmlFor(state)` 19 | 20 | ${ 21 | data.map(item => { 22 | const {id, label} = item; 23 | return htmlFor(data, id)` 24 | 25 | 26 | 29 | 34 | `; 36 | }) 37 | } 38 |
${id} 27 | ${label} 28 | 30 | 31 | 33 | 35 |
39 | `; 40 | }; 41 | -------------------------------------------------------------------------------- /test/fw-bench/src/table.js: -------------------------------------------------------------------------------- 1 | import {htmlFor} from '../../../keyed.js'; 2 | 3 | export default (state) => { 4 | const {data, selected, selectRow, removeRow} = state; 5 | return htmlFor(state)` 6 | 7 | ${ 8 | data.map(({id, label, html}) => html` 9 | 10 | 11 | 14 | 19 | ` 21 | )} 22 |
${id} 12 | ${label} 13 | 15 | 16 | 18 | 20 |
23 | `; 24 | }; 25 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | uhtml 7 | 45 | 46 | 47 |
48 |
49 | 50 | 51 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WebReflection/uhtml/fbae9184611abc730cebe325542dc355a8317d72/test/index.js -------------------------------------------------------------------------------- /test/issue-102/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 70 | 71 | 72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 | 80 | 81 | -------------------------------------------------------------------------------- /test/issue-103/data.js: -------------------------------------------------------------------------------- 1 | export const data = [ 2 | { 3 | // 9 4 | className: "Stack", 5 | children: [ 6 | { 7 | className: "Stack", 8 | children: [ 9 | { 10 | className: "Stack", 11 | children: [], 12 | id: "TreeBase-117", 13 | }, 14 | { 15 | className: "Stack", 16 | children: [], 17 | id: "TreeBase-118", 18 | }, 19 | { 20 | className: "Stack", 21 | children: [ 22 | { 23 | className: "Stack", 24 | children: [], 25 | id: "TreeBase-122", 26 | }, 27 | ], 28 | id: "TreeBase-121", 29 | }, 30 | ], 31 | id: "TreeBase-5", 32 | }, 33 | ], 34 | id: "TreeBase-4", 35 | }, 36 | { 37 | // 8 38 | className: "Stack", 39 | children: [ 40 | { 41 | className: "Stack", 42 | children: [ 43 | { 44 | className: "Stack", 45 | children: [], 46 | id: "TreeBase-117", 47 | }, 48 | { 49 | className: "Stack", 50 | children: [], 51 | id: "TreeBase-118", 52 | }, 53 | { 54 | className: "Stack", 55 | children: [], 56 | id: "TreeBase-121", 57 | }, 58 | ], 59 | id: "TreeBase-5", 60 | }, 61 | ], 62 | id: "TreeBase-4", 63 | }, 64 | { 65 | // 7 66 | className: "Stack", 67 | children: [ 68 | { 69 | className: "Stack", 70 | children: [ 71 | { 72 | className: "Stack", 73 | children: [ 74 | { 75 | className: "Gap", 76 | children: [], 77 | id: "TreeBase-120", 78 | }, 79 | ], 80 | id: "TreeBase-117", 81 | }, 82 | { 83 | className: "Stack", 84 | children: [], 85 | id: "TreeBase-118", 86 | }, 87 | { 88 | className: "Stack", 89 | children: [], 90 | id: "TreeBase-121", 91 | }, 92 | ], 93 | id: "TreeBase-5", 94 | }, 95 | ], 96 | id: "TreeBase-4", 97 | }, 98 | { 99 | // 6 100 | className: "Stack", 101 | children: [ 102 | { 103 | className: "Stack", 104 | children: [ 105 | { 106 | className: "Stack", 107 | children: [ 108 | { 109 | className: "Gap", 110 | children: [], 111 | id: "TreeBase-120", 112 | }, 113 | ], 114 | id: "TreeBase-117", 115 | }, 116 | { 117 | className: "Stack", 118 | children: [], 119 | id: "TreeBase-118", 120 | }, 121 | { 122 | className: "Gap", 123 | children: [], 124 | id: "TreeBase-119", 125 | }, 126 | { 127 | className: "Stack", 128 | children: [], 129 | id: "TreeBase-121", 130 | }, 131 | ], 132 | id: "TreeBase-5", 133 | }, 134 | ], 135 | id: "TreeBase-4", 136 | }, 137 | ]; 138 | -------------------------------------------------------------------------------- /test/issue-103/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /test/issue-91/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /test/issue-96.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 28 | 29 | -------------------------------------------------------------------------------- /test/issue-98/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /test/json.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | uhtml/json 8 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /test/modern.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /test/modern.mjs: -------------------------------------------------------------------------------- 1 | import { DOMParser } from '../dom.js'; 2 | import init from '../init.js'; 3 | 4 | const assert = (real, expected, message = `expected ${expected} got ${real}`) => { 5 | if (!Object.is(real, expected)) 6 | throw new Error(message); 7 | }; 8 | 9 | if (typeof document === 'undefined') { 10 | globalThis.document = (new DOMParser).parseFromString('...', 'text/html'); 11 | globalThis.HTMLElement = document.createElement('e').constructor; 12 | globalThis.DocumentFragment = document.createDocumentFragment().constructor; 13 | } 14 | 15 | Object.defineProperty( 16 | HTMLElement.prototype, 17 | 'getOnly', 18 | { get() { return 'OK' }, 19 | }); 20 | 21 | const node = new Proxy(new Map, { 22 | get(map, content) { 23 | let node = map.get(content); 24 | if (!node) map.set(content, (node = document.createTextNode(content))); 25 | return node; 26 | } 27 | }); 28 | 29 | const { render, html, htmlFor } = init(document); 30 | 31 | const empty = () => html``; 32 | 33 | const fragment = () => html`

br


`; 34 | 35 | const holeyFragment = b => html`a${b}c`; 36 | 37 | const sameFragment = b => htmlFor(document)`a${b}c`; 38 | 39 | render(document.body, empty()); 40 | assert( 41 | document.body.childNodes.length, 42 | 1 43 | ); 44 | 45 | render(document.body, html`${empty()}`); 46 | assert( 47 | document.body.childNodes.length, 48 | 3 49 | ); 50 | 51 | render(document.body, html`a`); 52 | assert( 53 | document.body.childNodes.length, 54 | 1 55 | ); 56 | assert( 57 | document.body.outerHTML, 58 | 'a' 59 | ); 60 | 61 | render(document.body, html``); 62 | assert( 63 | document.body.childNodes.length, 64 | 1 65 | ); 66 | assert( 67 | document.body.outerHTML, 68 | '' 69 | ); 70 | 71 | render(document.body, html`${'a'}`); 72 | assert( 73 | document.body.childNodes.length, 74 | 3 75 | ); 76 | assert( 77 | document.body.outerHTML, 78 | 'a' 79 | ); 80 | 81 | render(document.body, fragment()); 82 | assert( 83 | document.body.childNodes.length, 84 | 4 85 | ); 86 | assert( 87 | document.body.outerHTML, 88 | '

br


' 89 | ); 90 | 91 | render(document.body, holeyFragment('b')); 92 | assert( 93 | document.body.childNodes.length, 94 | 5 95 | ); 96 | assert( 97 | document.body.outerHTML, 98 | 'abc' 99 | ); 100 | 101 | render(document.body, holeyFragment('z')); 102 | assert( 103 | document.body.childNodes.length, 104 | 5 105 | ); 106 | assert( 107 | document.body.outerHTML, 108 | 'azc' 109 | ); 110 | 111 | render(document.body, sameFragment('b')); 112 | assert( 113 | document.body.childNodes.length, 114 | 5 115 | ); 116 | assert( 117 | document.body.outerHTML, 118 | 'abc' 119 | ); 120 | 121 | const div = document.body.appendChild(document.createElement('div')); 122 | 123 | render(div, sameFragment('z')); 124 | assert( 125 | document.body.childNodes.length, 126 | 1 127 | ); 128 | assert( 129 | document.body.outerHTML, 130 | '
azc
' 131 | ); 132 | 133 | 134 | render(document.body, html`ab`); 135 | assert( 136 | document.body.childNodes.length, 137 | 4 138 | ); 139 | assert( 140 | document.body.outerHTML, 141 | 'ab' 142 | ); 143 | 144 | const spans = (a, b) => html`${a}${b}`; 145 | 146 | render(document.body, spans('a', 'c')); 147 | assert( 148 | document.body.childNodes.length, 149 | 4 150 | ); 151 | assert( 152 | document.body.outerHTML, 153 | 'ac' 154 | ); 155 | 156 | render(document.body, spans('a', 'b')); 157 | assert( 158 | document.body.childNodes.length, 159 | 4 160 | ); 161 | assert( 162 | document.body.outerHTML, 163 | 'ab' 164 | ); 165 | 166 | const holeArray = list => html`${list}`; 167 | 168 | render(document.body, holeArray([])); 169 | assert( 170 | document.body.childNodes.length, 171 | 3 172 | ); 173 | assert( 174 | document.body.outerHTML, 175 | '' 176 | ); 177 | 178 | render(document.body, holeArray([node.a])); 179 | assert( 180 | document.body.childNodes.length, 181 | 4 182 | ); 183 | assert( 184 | document.body.outerHTML, 185 | 'a' 186 | ); 187 | 188 | render(document.body, holeArray([node.a, node.b, node.c, node.d])); 189 | assert( 190 | document.body.childNodes.length, 191 | 7 192 | ); 193 | assert( 194 | document.body.outerHTML, 195 | 'abcd' 196 | ); 197 | 198 | render(document.body, holeArray([node.a, node.b])); 199 | assert( 200 | document.body.childNodes.length, 201 | 5 202 | ); 203 | assert( 204 | document.body.outerHTML, 205 | 'ab' 206 | ); 207 | 208 | import('../node.js').then(({ render, html }) => { 209 | render(document.head, html``); 210 | assert( 211 | document.head.outerHTML, 212 | '' 213 | ); 214 | }); 215 | -------------------------------------------------------------------------------- /test/mondrian.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css?family=Lato:300,900"); 2 | 3 | * { 4 | box-sizing: border-box; 5 | animation: fadeIn 0.5s; 6 | } 7 | 8 | body, 9 | html { 10 | background-color: #fff; 11 | display: grid; 12 | align-items: center; 13 | justify-content: center; 14 | font-family: 'Lato', sans-serif; 15 | text-align: center; 16 | padding: 20px; 17 | min-height: 100vh; 18 | width: 100vw; 19 | } 20 | 21 | .mondrian { 22 | background-color: #070908; 23 | border: 10px solid #070908; 24 | box-shadow: 5px 10px 10px #aaa; 25 | display: grid; 26 | grid-auto-columns: 50px; 27 | grid-auto-flow: dense; 28 | grid-auto-rows: 50px; 29 | grid-gap: 10px; 30 | grid-template-columns: repeat(auto-fit, 50px); 31 | grid-template-rows: repeat(auto-fit, 50px); 32 | height: 300px; 33 | overflow: hidden; 34 | width: 250px; 35 | } 36 | 37 | .mondrian__block { 38 | animation: scaleIn 0.25s ease 0s; 39 | background-color: #f2f5f1; 40 | } 41 | 42 | .mondrian__block:nth-child(1) { 43 | animation-delay: 0.15s; 44 | background-color: $color; 45 | transform: scale(0); 46 | animation-fill-mode: forwards; 47 | } 48 | 49 | .mondrian__block:nth-child(2) { 50 | animation-delay: 0.3s; 51 | background-color: $color; 52 | transform: scale(0); 53 | animation-fill-mode: forwards; 54 | } 55 | 56 | .mondrian__block:nth-child(3) { 57 | animation-delay: 0.45s; 58 | background-color: $color; 59 | transform: scale(0); 60 | animation-fill-mode: forwards; 61 | } 62 | 63 | .mondrian__block:nth-child(4) { 64 | animation-delay: 0.6s; 65 | background-color: $color; 66 | transform: scale(0); 67 | animation-fill-mode: forwards; 68 | } 69 | 70 | .mondrian__block:nth-child(5) { 71 | animation-delay: 0.75s; 72 | background-color: $color; 73 | transform: scale(0); 74 | animation-fill-mode: forwards; 75 | } 76 | 77 | .mondrian__block:nth-child(6) { 78 | animation-delay: 0.9s; 79 | background-color: $color; 80 | transform: scale(0); 81 | animation-fill-mode: forwards; 82 | } 83 | 84 | .mondrian__block:nth-child(7) { 85 | animation-delay: 1.05s; 86 | background-color: $color; 87 | transform: scale(0); 88 | animation-fill-mode: forwards; 89 | } 90 | 91 | .mondrian__block:nth-child(8) { 92 | animation-delay: 1.2s; 93 | background-color: $color; 94 | transform: scale(0); 95 | animation-fill-mode: forwards; 96 | } 97 | 98 | .mondrian__block:nth-child(9) { 99 | animation-delay: 1.35s; 100 | background-color: $color; 101 | transform: scale(0); 102 | animation-fill-mode: forwards; 103 | } 104 | 105 | .mondrian__block:nth-child(10) { 106 | animation-delay: 1.5s; 107 | background-color: $color; 108 | transform: scale(0); 109 | animation-fill-mode: forwards; 110 | } 111 | 112 | .mondrian__block[data-row-span="1"] { 113 | grid-row: span 1; 114 | } 115 | 116 | .mondrian__block[data-col-span="1"] { 117 | grid-column: span 1; 118 | } 119 | 120 | .mondrian__block[data-row-span="2"] { 121 | grid-row: span 2; 122 | } 123 | 124 | .mondrian__block[data-col-span="2"] { 125 | grid-column: span 2; 126 | } 127 | 128 | .mondrian__block[data-row-span="3"] { 129 | grid-row: span 3; 130 | } 131 | 132 | .mondrian__block[data-col-span="3"] { 133 | grid-column: span 3; 134 | } 135 | 136 | .mondrian__block[data-color-index="1"] { 137 | background-color: #f2f5f1; 138 | } 139 | 140 | .mondrian__block[data-color-index="2"] { 141 | background-color: #f8d92d; 142 | } 143 | 144 | .mondrian__block[data-color-index="3"] { 145 | background-color: #f2f5f1; 146 | } 147 | 148 | .mondrian__block[data-color-index="4"] { 149 | background-color: #0b54a4; 150 | } 151 | 152 | .mondrian__block[data-color-index="5"] { 153 | background-color: #f2f5f1; 154 | } 155 | 156 | .mondrian__block[data-color-index="6"] { 157 | background-color: #d60014; 158 | } 159 | 160 | .generate-button { 161 | cursor: pointer; 162 | padding: 4px 12px; 163 | border-radius: 4px; 164 | font-family: 'Lato', sans-serif; 165 | margin-top: 30px; 166 | background-color: #fff; 167 | } 168 | 169 | .generate-button:hover { 170 | background-color: #e6e6e6; 171 | } 172 | 173 | .generate-button:active { 174 | background-color: #ccc; 175 | } 176 | 177 | @-moz-keyframes scaleIn { 178 | from { 179 | transform: scale(0); 180 | } 181 | 182 | to { 183 | transform: scale(1); 184 | } 185 | } 186 | 187 | @-webkit-keyframes scaleIn { 188 | from { 189 | transform: scale(0); 190 | } 191 | 192 | to { 193 | transform: scale(1); 194 | } 195 | } 196 | 197 | @-o-keyframes scaleIn { 198 | from { 199 | transform: scale(0); 200 | } 201 | 202 | to { 203 | transform: scale(1); 204 | } 205 | } 206 | 207 | @keyframes scaleIn { 208 | from { 209 | transform: scale(0); 210 | } 211 | 212 | to { 213 | transform: scale(1); 214 | } 215 | } 216 | 217 | @-moz-keyframes fadeIn { 218 | from { 219 | opacity: 0; 220 | } 221 | 222 | to { 223 | opacity: 1; 224 | } 225 | } 226 | 227 | @-webkit-keyframes fadeIn { 228 | from { 229 | opacity: 0; 230 | } 231 | 232 | to { 233 | opacity: 1; 234 | } 235 | } 236 | 237 | @-o-keyframes fadeIn { 238 | from { 239 | opacity: 0; 240 | } 241 | 242 | to { 243 | opacity: 1; 244 | } 245 | } 246 | 247 | @keyframes fadeIn { 248 | from { 249 | opacity: 0; 250 | } 251 | 252 | to { 253 | opacity: 1; 254 | } 255 | } -------------------------------------------------------------------------------- /test/mondrian.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Randomly generate Mondrian Art with CSS Grid + uhtml 🎨 7 | 8 | 9 | 10 | 11 |
12 |
13 | 16 |
17 | 18 | 19 | -------------------------------------------------------------------------------- /test/mondrian.js: -------------------------------------------------------------------------------- 1 | const require = (name, es = '../index.js') => import( 2 | /^(?:localhost|[0-9.]+)$/.test(location.hostname) ? 3 | es : `https://unpkg.com/${name}` 4 | ); 5 | 6 | Promise.all([ 7 | import('https://unpkg.com/wicked-elements?module'), 8 | require('uhtml') 9 | ]).then(([{define}, {render, html}]) => { 10 | 11 | define('#app .mondrian', { 12 | connected() { this.render(); }, 13 | render(blocks = this.generateBlocks()) { 14 | render(this.element, html`${blocks.map( 15 | ({colSpan, rowSpan, colorIndex}) => html` 16 |
` 20 | )}`); 21 | }, 22 | generateBlocks() { 23 | const blocks = []; 24 | for (let i = 0; i < 10; i++) { 25 | blocks.push({ 26 | colSpan: Math.floor(Math.random() * 3 + 1), 27 | rowSpan: Math.floor(Math.random() * 3 + 1), 28 | colorIndex: Math.floor(Math.random() * 6 + 1), 29 | }); 30 | } 31 | return blocks; 32 | }, 33 | onGenerate() { 34 | this.render([]); 35 | setTimeout(() => this.render()); 36 | } 37 | }); 38 | 39 | define('#app .generate-button', { 40 | onclick() { 41 | const mondrian = document.querySelector('.mondrian'); 42 | mondrian.dispatchEvent(new Event('generate')); 43 | } 44 | }); 45 | 46 | }); 47 | -------------------------------------------------------------------------------- /test/node.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | uhtml 7 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /test/object.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 43 | 44 | -------------------------------------------------------------------------------- /test/object.native.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 30 | 31 | -------------------------------------------------------------------------------- /test/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test", 3 | "lockfileVersion": 3, 4 | "requires": true, 5 | "packages": { 6 | "": { 7 | "devDependencies": { 8 | "linkedom": "^0.16.6" 9 | } 10 | }, 11 | "node_modules/boolbase": { 12 | "version": "1.0.0", 13 | "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", 14 | "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", 15 | "dev": true 16 | }, 17 | "node_modules/css-select": { 18 | "version": "5.1.0", 19 | "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", 20 | "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", 21 | "dev": true, 22 | "dependencies": { 23 | "boolbase": "^1.0.0", 24 | "css-what": "^6.1.0", 25 | "domhandler": "^5.0.2", 26 | "domutils": "^3.0.1", 27 | "nth-check": "^2.0.1" 28 | }, 29 | "funding": { 30 | "url": "https://github.com/sponsors/fb55" 31 | } 32 | }, 33 | "node_modules/css-what": { 34 | "version": "6.1.0", 35 | "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", 36 | "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", 37 | "dev": true, 38 | "engines": { 39 | "node": ">= 6" 40 | }, 41 | "funding": { 42 | "url": "https://github.com/sponsors/fb55" 43 | } 44 | }, 45 | "node_modules/cssom": { 46 | "version": "0.5.0", 47 | "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", 48 | "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==", 49 | "dev": true 50 | }, 51 | "node_modules/dom-serializer": { 52 | "version": "2.0.0", 53 | "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", 54 | "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", 55 | "dev": true, 56 | "dependencies": { 57 | "domelementtype": "^2.3.0", 58 | "domhandler": "^5.0.2", 59 | "entities": "^4.2.0" 60 | }, 61 | "funding": { 62 | "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" 63 | } 64 | }, 65 | "node_modules/domelementtype": { 66 | "version": "2.3.0", 67 | "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", 68 | "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", 69 | "dev": true, 70 | "funding": [ 71 | { 72 | "type": "github", 73 | "url": "https://github.com/sponsors/fb55" 74 | } 75 | ] 76 | }, 77 | "node_modules/domhandler": { 78 | "version": "5.0.3", 79 | "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", 80 | "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", 81 | "dev": true, 82 | "dependencies": { 83 | "domelementtype": "^2.3.0" 84 | }, 85 | "engines": { 86 | "node": ">= 4" 87 | }, 88 | "funding": { 89 | "url": "https://github.com/fb55/domhandler?sponsor=1" 90 | } 91 | }, 92 | "node_modules/domutils": { 93 | "version": "3.1.0", 94 | "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", 95 | "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", 96 | "dev": true, 97 | "dependencies": { 98 | "dom-serializer": "^2.0.0", 99 | "domelementtype": "^2.3.0", 100 | "domhandler": "^5.0.3" 101 | }, 102 | "funding": { 103 | "url": "https://github.com/fb55/domutils?sponsor=1" 104 | } 105 | }, 106 | "node_modules/entities": { 107 | "version": "4.5.0", 108 | "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", 109 | "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", 110 | "dev": true, 111 | "engines": { 112 | "node": ">=0.12" 113 | }, 114 | "funding": { 115 | "url": "https://github.com/fb55/entities?sponsor=1" 116 | } 117 | }, 118 | "node_modules/html-escaper": { 119 | "version": "3.0.3", 120 | "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz", 121 | "integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==", 122 | "dev": true 123 | }, 124 | "node_modules/htmlparser2": { 125 | "version": "9.0.0", 126 | "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.0.0.tgz", 127 | "integrity": "sha512-uxbSI98wmFT/G4P2zXx4OVx04qWUmyFPrD2/CNepa2Zo3GPNaCaaxElDgwUrwYWkK1nr9fft0Ya8dws8coDLLQ==", 128 | "dev": true, 129 | "funding": [ 130 | "https://github.com/fb55/htmlparser2?sponsor=1", 131 | { 132 | "type": "github", 133 | "url": "https://github.com/sponsors/fb55" 134 | } 135 | ], 136 | "dependencies": { 137 | "domelementtype": "^2.3.0", 138 | "domhandler": "^5.0.3", 139 | "domutils": "^3.1.0", 140 | "entities": "^4.5.0" 141 | } 142 | }, 143 | "node_modules/linkedom": { 144 | "version": "0.16.6", 145 | "resolved": "https://registry.npmjs.org/linkedom/-/linkedom-0.16.6.tgz", 146 | "integrity": "sha512-vJ8oadtJe3DM4FNW15Fj5NLIJlJk+AOypoRxzq9prLx+gAKvHYcTfV98pzOoRkwx4ZvvYzqT1bcDKluHH72apg==", 147 | "dev": true, 148 | "dependencies": { 149 | "css-select": "^5.1.0", 150 | "cssom": "^0.5.0", 151 | "html-escaper": "^3.0.3", 152 | "htmlparser2": "^9.0.0", 153 | "uhyphen": "^0.2.0" 154 | } 155 | }, 156 | "node_modules/nth-check": { 157 | "version": "2.1.1", 158 | "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", 159 | "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", 160 | "dev": true, 161 | "dependencies": { 162 | "boolbase": "^1.0.0" 163 | }, 164 | "funding": { 165 | "url": "https://github.com/fb55/nth-check?sponsor=1" 166 | } 167 | }, 168 | "node_modules/uhyphen": { 169 | "version": "0.2.0", 170 | "resolved": "https://registry.npmjs.org/uhyphen/-/uhyphen-0.2.0.tgz", 171 | "integrity": "sha512-qz3o9CHXmJJPGBdqzab7qAYuW8kQGKNEuoHFYrBwV6hWIMcpAmxDLXojcHfFr9US1Pe6zUswEIJIbLI610fuqA==", 172 | "dev": true 173 | } 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /test/package.json: -------------------------------------------------------------------------------- 1 | {"type":"commonjs","devDependencies":{"linkedom":"^0.16.6"}} -------------------------------------------------------------------------------- /test/paranoia.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /test/preactive.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | uhtml/preactive 7 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /test/ref.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | uHTML 7 | 8 | 9 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /test/render-roots.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | -------------------------------------------------------------------------------- /test/repeat.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 21 | -------------------------------------------------------------------------------- /test/select.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17 | -------------------------------------------------------------------------------- /test/semi-direct.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /test/shadow-root.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | uhtml Custom Element example 7 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /test/shenanigans.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /test/shuffled.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 44 | 45 | 46 |
47 | 48 | -------------------------------------------------------------------------------- /test/signal.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | uhtml/signal 7 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /test/ssr.mjs: -------------------------------------------------------------------------------- 1 | import init from '../esm/init-ssr.js'; 2 | 3 | const { document, render, html } = init(` 4 | 5 | 6 | ${'Hello SSR'} 7 | 8 |
9 | `.trim() 10 | ); 11 | 12 | render(document.getElementById('test'), html` 13 |

14 | !!! ${[html``, html``, html`e`]} !!! 15 |

16 | `); 17 | 18 | console.log(document.toString()); 19 | -------------------------------------------------------------------------------- /test/svg.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /test/svg.mjs: -------------------------------------------------------------------------------- 1 | import init from '../esm/init-ssr.js'; 2 | 3 | const { document, render, svg } = init(); 4 | 5 | render(document.body, svg``); 6 | 7 | if (document.body.innerHTML !== '') 8 | throw new Error('Invalid SVG expectations'); 9 | -------------------------------------------------------------------------------- /test/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | uHTML 7 | 8 | 9 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /test/textarea.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/usignal.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 28 | 29 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "module": "NodeNext", 5 | "target": "esnext", 6 | "moduleResolution": "nodenext", 7 | "allowJs": true, 8 | "declaration": true, 9 | "emitDeclarationOnly": true, 10 | "declarationDir": "types", 11 | "forceConsistentCasingInFileNames": true, 12 | "checkJs": false, 13 | "strict": true 14 | }, 15 | "include": [ 16 | "esm/**/*.js" 17 | ] 18 | } 19 | --------------------------------------------------------------------------------