From 8c16efca0313d98745d1f23ba5ef5f9098f2ed3e Mon Sep 17 00:00:00 2001 From: webreflection Date: Sun, 12 Nov 2023 12:44:03 +0100 Subject: [PATCH 1/2] uhtml v4 --- .github/workflows/node.js.yml | 14 +- .gitignore | 3 +- README.md | 6 +- V0.md | 83 ---- async.d.ts | 8 - async.js | 2 - cjs/async.js | 41 -- cjs/handlers.js | 150 ------ cjs/index.js | 67 --- cjs/init.js | 629 ------------------------- cjs/json.js | 52 --- cjs/rabbit.js | 218 --------- cjs/utils.js | 40 -- cjs/x.js | 14 - es.js | 2 - esm.js | 2 - esm/async.js | 35 -- esm/create-content.js | 23 + esm/creator.js | 35 ++ esm/handler.js | 203 +++++++++ esm/handlers.js | 148 ------ esm/index.js | 67 +-- esm/init.js | 628 ------------------------- esm/json.js | 49 -- esm/keyed.js | 39 ++ esm/literals.js | 90 ++++ esm/node.js | 21 + esm/parser.js | 94 ++++ esm/persistent-fragment.js | 46 ++ esm/rabbit.js | 237 ++-------- esm/range.js | 19 + esm/render-any.js | 25 + esm/render-hole.js | 24 + esm/render-node.js | 13 + esm/utils.js | 46 +- esm/x.js | 11 - index.d.ts | 29 -- index.js | 807 +------------------------------- init.js | 2 - keyed.js | 3 + node.js | 3 + package-lock.json | 811 +++++++++++++++++++++------------ package.json | 90 ++-- pony.js | 29 -- rollup/async.config.js | 17 - rollup/babel.config.js | 15 - rollup/es.config.js | 44 +- rollup/esm.config.js | 15 - rollup/init.config.js | 15 - test/coverage.js | 10 +- test/index.js | 0 tsconfig.json | 16 + types/create-content.d.ts | 2 + types/creator.d.ts | 2 + types/handler.d.ts | 22 + types/index.d.ts | 8 + types/literals.d.ts | 82 ++++ types/parser.d.ts | 9 + types/persistent-fragment.d.ts | 10 + types/rabbit.d.ts | 14 + types/range.d.ts | 2 + types/render-hole.d.ts | 3 + types/utils.d.ts | 4 + 63 files changed, 1494 insertions(+), 3754 deletions(-) delete mode 100644 V0.md delete mode 100644 async.d.ts delete mode 100644 async.js delete mode 100644 cjs/async.js delete mode 100644 cjs/handlers.js delete mode 100644 cjs/index.js delete mode 100644 cjs/init.js delete mode 100644 cjs/json.js delete mode 100644 cjs/rabbit.js delete mode 100644 cjs/utils.js delete mode 100644 cjs/x.js delete mode 100644 es.js delete mode 100644 esm.js delete mode 100644 esm/async.js create mode 100644 esm/create-content.js create mode 100644 esm/creator.js create mode 100644 esm/handler.js delete mode 100644 esm/handlers.js delete mode 100644 esm/init.js delete mode 100644 esm/json.js create mode 100644 esm/keyed.js create mode 100644 esm/literals.js create mode 100644 esm/node.js create mode 100644 esm/parser.js create mode 100644 esm/persistent-fragment.js create mode 100644 esm/range.js create mode 100644 esm/render-any.js create mode 100644 esm/render-hole.js create mode 100644 esm/render-node.js delete mode 100644 esm/x.js delete mode 100644 index.d.ts delete mode 100644 init.js create mode 100644 keyed.js create mode 100644 node.js delete mode 100644 pony.js delete mode 100644 rollup/async.config.js delete mode 100644 rollup/babel.config.js delete mode 100644 rollup/esm.config.js delete mode 100644 rollup/init.config.js create mode 100644 test/index.js create mode 100644 tsconfig.json create mode 100644 types/create-content.d.ts create mode 100644 types/creator.d.ts create mode 100644 types/handler.d.ts create mode 100644 types/index.d.ts create mode 100644 types/literals.d.ts create mode 100644 types/parser.d.ts create mode 100644 types/persistent-fragment.d.ts create mode 100644 types/rabbit.d.ts create mode 100644 types/range.d.ts create mode 100644 types/render-hole.d.ts create mode 100644 types/utils.d.ts diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index 73cf8d6..ba93c6c 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -12,7 +12,7 @@ jobs: strategy: matrix: - node-version: [16] + node-version: [20] steps: - uses: actions/checkout@v2 @@ -23,9 +23,9 @@ jobs: cache: 'npm' - run: npm ci - run: npm run build --if-present - - run: npm test - - run: npm run coverage --if-present - - name: Coveralls - uses: coverallsapp/github-action@master - with: - github-token: ${{ secrets.GITHUB_TOKEN }} + # - run: npm test + # - run: npm run coverage --if-present + # - name: Coveralls + # uses: coverallsapp/github-action@master + # with: + # github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 8b2c3c9..d9021aa 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .nyc_output/ coverage/ node_modules/ - +cjs/* +!cjs/package.json diff --git a/README.md b/README.md index 83ea774..896180c 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,11 @@ **Social Media Photo by [Andrii Ganzevych](https://unsplash.com/@odya_kun) on [Unsplash](https://unsplash.com/)** -_micro html_ is a _~2.5K_ [lighterhtml](https://github.com/WebReflection/lighterhtml#readme) subset to build declarative and reactive UI via template literals tags. +_micro html_ is a [lighterhtml](https://github.com/WebReflection/lighterhtml#readme) subset to build declarative and reactive UI via template literals tags. -### šŸ“£ Community Announcement +### šŸ“£ uhtml v4 is out -Please ask questions in the [dedicated discussions repository](https://github.com/WebReflection/discussions), to help the community around this project grow ā™„ +Please check the [Release Notes](https://github.com/WebReflection/uhtml/pull/86) to know more about what improved, what changed or broke and what's not there yet (or anymore). --- diff --git a/V0.md b/V0.md deleted file mode 100644 index 1599028..0000000 --- a/V0.md +++ /dev/null @@ -1,83 +0,0 @@ -# uHTML v0 - -A micro HTML/SVG render - - * no diffing whatsoever, it's just a faster, and smarter `innerHTML` equivalent - * nodes with a `name="..."` attribute are collected once - * arrays in holes are joined with a space in between - * no repeated render, per each node, when same template literal is used - * a perfect tiny companion for [wickedElements](https://github.com/WebReflection/wicked-elements#readme) or [hookedElements](https://github.com/WebReflection/hooked-elements#readme) - -The key of _uhtml_ is size and simplicity: nothing is transformed, transpiled, or mapped, you have full freedom to define layouts and enrich these later on. - -```js -import {render, html, svg} from 'uhtml'; -const {render, html, svg} = require('uhtml'); -// https://unpkg.com/uhtml -``` - -## API and Features in a Nutshell - -Anything in the template gets in as is, with the exception of arrays, joined via a space, so that classes, as well as list of elements, can get in too. - -```js -import {render, html, svg} from 'uhtml'; - -const {title, kind} = render( - document.body, - html` -

Hello uHTML!

-

- Welcome to this old adventure! -

- ` -); - -// every name in the template results into an element -title.style.textDecoration = 'underline'; -kind.textContent = 'new'; -``` - -Feel free to check this **[live counter demo](https://codepen.io/WebReflection/pen/bGdpEKB)** to better understand how this works. - -## F.A.Q. - -
- - How is this any better than innerHTML ? - - - _uhtml_ never pollutes, trash, or recreate, content defined via a template literal. - - In case you didn't know, [template literals](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals) are unique per scope, so that defining some HTML or SVG content for a specific node passed as `render` argument, never replaces the content that was previously there, if the template literal is exactly the same. - - Moreover, if you use `innerHTML` for SVG content, that won't work the way you'd expect. - - _uhtml_ does indeed the minimum amount of processing to ensure your HTML or SVG content is injected once, and only if the template literal is different from the previous one. - -
- -
- - Can I use nested html or svg in the template ? - - - The _TL;DR_ answer is **no**, 'cause those utilities are there to define the kind of content you want for that specific node, instrumenting few DOM APIs to provide such content within fragments. - - This boils down to the inability, or the anti-pattern, to have lists created within a template, unless you take over such list, through a named element, in a way that allows you to update, replace, or drop, such list later on. - - The [domdiff](https://github.com/WebReflection/domdiff#readme) module, in such cases, might be a solution, otherwise you are in charge of handling inner lists changes. - -
- -
- - Should I hydrate each content manually ? - - - The `name` attribute simplifies the retrieval of elements within the template. - From that time on, you are in charge of populating, or manipulating, anything you like, and per each named node. - - Please note that a query such as `[name]` will return anything found in the template, so that name clashing is inevitable, if you use the same attribute within other elements/components defined in your template. - -
diff --git a/async.d.ts b/async.d.ts deleted file mode 100644 index a109c46..0000000 --- a/async.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { Renderable } from "./index"; - -export declare function render( - node: T, - renderer: (() => Renderable) | Renderable, -): Promise; - -export type { TemplateFunction, Tag, Renderable, html, svg, Hole } from "./index"; \ No newline at end of file diff --git a/async.js b/async.js deleted file mode 100644 index 1359ff9..0000000 --- a/async.js +++ /dev/null @@ -1,2 +0,0 @@ -self.uhtml=function(e){"use strict";class t extends Map{set(e,t){return super.set(e,t),t}}class n extends WeakMap{set(e,t){return super.set(e,t),t}}const{isArray:r}=Array,s=(e,t)=>{const n=[];for(const{length:l}=e;t{function t(t,n){return e.apply(this,[t].concat(n))}return function(e){return s(arguments,1).then(t.bind(this,e))}}; -/*! (c) Andrea Giammarchi - ISC */const o=/^(?:area|base|br|col|embed|hr|img|input|keygen|link|menuitem|meta|param|source|track|wbr)$/i,a=/<([a-z]+[a-z0-9:._-]*)([^>]*?)(\/?)>/g,c=/([^\s\\>"'=]+)\s*=\s*(['"]?)\x01/g,i=/[\x01\x02]/g;const u=(e,t)=>111===e.nodeType?1/t<0?t?(({firstChild:e,lastChild:t})=>{const n=document.createRange();return n.setStartAfter(e),n.setEndAfter(t),n.deleteContents(),e})(e):e.lastChild:t?e.valueOf():e.firstChild:e,{isArray:d}=Array,f=e=>null==e?e:e.valueOf(),p=(e,t)=>{let n,r,s=t.slice(2);return!(t in e)&&(r=t.toLowerCase())in e&&(s=r.slice(2)),t=>{const r=d(t)?t:[t,!1];n!==r[0]&&(n&&e.removeEventListener(s,n,r[1]),(n=r[0])&&e.addEventListener(s,n,r[1]))}};const{isArray:h,prototype:m}=Array,{indexOf:g}=m,{createDocumentFragment:y,createElement:b,createElementNS:v,createTextNode:w,createTreeWalker:x,importNode:N}=new Proxy({},{get:(e,t)=>document[t].bind(document)});let A;const C=(e,t)=>t?(e=>{A||(A=v("http://www.w3.org/2000/svg","svg")),A.innerHTML=e;const t=y();return t.append(...A.childNodes),t})(e):(e=>{const t=b("template");return t.innerHTML=e,t.content})(e),k=({childNodes:e},t)=>e[t],$=(e,t,n)=>((e,t,n,r,s)=>{const l=n.length;let o=t.length,a=l,c=0,i=0,u=null;for(;cs-i){const l=r(t[c],0);for(;i{switch(t[0]){case"?":return((e,t,n)=>r=>{const s=!!f(r);n!==s&&((n=s)?e.setAttribute(t,""):e.removeAttribute(t))})(e,t.slice(1),!1);case".":return((e,t)=>"dataset"===t?(({dataset:e})=>t=>{for(const n in t){const r=t[n];null==r?delete e[n]:e[n]=r}})(e):n=>{e[t]=n})(e,t.slice(1));case"@":return p(e,"on"+t.slice(1));case"o":if("n"===t[1])return p(e,t)}switch(t){case"ref":return(e=>{let t;return n=>{t!==n&&(t=n,"function"==typeof n?n(e):n.current=e)}})(e);case"aria":return(e=>t=>{for(const n in t){const r="role"===n?n:`aria-${n}`,s=t[n];null==s?e.removeAttribute(r):e.setAttribute(r,s)}})(e)}return((e,t)=>{let n,r=!0;const s=document.createAttributeNS(null,t);return t=>{const l=f(t);n!==l&&(null==(n=l)?r||(e.removeAttributeNode(s),r=!0):(s.value=l,r&&(e.setAttributeNodeNS(s),r=!1)))}})(e,t)};function O(e){const{type:t,path:n}=e,r=n.reduceRight(k,this);return"node"===t?(e=>{let t,n,r=[];const s=l=>{switch(typeof l){case"string":case"number":case"boolean":t!==l&&(t=l,n||(n=w("")),n.data=l,r=$(e,r,[n]));break;case"object":case"undefined":if(null==l){t!=l&&(t=l,r=$(e,r,[]));break}if(h(l)){t=l,0===l.length?r=$(e,r,[]):"object"==typeof l[0]?r=$(e,r,l):s(String(l));break}if(t!==l)if("ELEMENT_NODE"in l)t=l,r=$(e,r,11===l.nodeType?[...l.childNodes]:[l]);else{const e=l.valueOf();e!==l&&s(e)}break;case"function":s(l(e))}};return s})(r):"attr"===t?E(r,e.name):(e=>{let t;return n=>{const r=f(n);t!=r&&(t=r,e.textContent=null==r?"":r)}})(r)}const T=e=>{const t=[];let{parentNode:n}=e;for(;n;)t.push(g.call(n.childNodes,e)),e=n,({parentNode:n}=e);return t},S="isĀµ",L=new n,M=/^(?:textarea|script|style|title|plaintext|xmp)$/,j=(e,t)=>{const n="svg"===e,r=((e,t,n)=>{let r=0;return e.join("").trim().replace(a,((e,t,r,s)=>{let l=t+r.replace(c,"=$2$1").trimEnd();return s.length&&(l+=n||o.test(t)?" /":">"})).replace(i,(e=>""===e?"\x3c!--"+t+r+++"--\x3e":t+r++))})(t,S,n),s=C(r,n),l=x(s,129),u=[],d=t.length-1;let f=0,p=`${S}${f}`;for(;f{const{content:n,nodes:r}=L.get(t)||L.set(t,j(e,t)),s=N(n,!0);return{content:s,updates:r.map(O,s)}},P=(e,{type:t,template:n,values:r})=>{const s=D(e,r);let{entry:l}=e;l&&l.template===n&&l.type===t||(e.entry=l=((e,t)=>{const{content:n,updates:r}=B(e,t);return{type:e,template:t,content:n,updates:r,wire:null}})(t,n));const{content:o,updates:a,wire:c}=l;for(let e=0;e{const{firstChild:t,lastChild:n}=e;if(t===n)return n||e;const{childNodes:r}=e,s=[...r];return{ELEMENT_NODE:1,nodeType:111,firstChild:t,lastChild:n,valueOf:()=>(r.length!==s.length&&e.append(...s),e)}})(o))},D=({stack:e},t)=>{const{length:n}=t;for(let r=0;r{const r=new n;return Object.assign(((t,...n)=>new H(e,t,n)),{for(n,s){const l=r.get(n)||r.set(n,new t);return l.get(s)||l.set(s,(t=>(n,...r)=>P(t,{type:e,template:n,values:r}))({stack:[],entry:null,wire:null}))},node:(t,...n)=>P({stack:[],entry:null,wire:null},new H(e,t,n)).valueOf()})},z=new n,R=_("html"),W=_("svg"),{defineProperties:F}=Object,q=e=>{const t=new n;return F(l(e),{for:{value(n,r){const s=e.for(n,r);return t.get(s)||t.set(s,l(s))}},node:{value:l(e.node)}})},G=q(R),I=q(W);return e.Hole=H,e.html=G,e.render=(e,t)=>{const n="function"==typeof t?t():t;return Promise.resolve(n).then((t=>((e,t)=>{const n="function"==typeof t?t():t,r=z.get(e)||z.set(e,{stack:[],entry:null,wire:null}),s=n instanceof H?P(r,n):n;return s!==r.wire&&(r.wire=s,e.replaceChildren(s.valueOf())),e})(e,t)))},e.svg=I,e}({}); diff --git a/cjs/async.js b/cjs/async.js deleted file mode 100644 index 5128b70..0000000 --- a/cjs/async.js +++ /dev/null @@ -1,41 +0,0 @@ -'use strict'; -const {WeakMapSet} = require('@webreflection/mapset'); - -const asyncTag = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('async-tag')); - -const {render: $render, html: $html, svg: $svg} = require('./index.js'); - -const {defineProperties} = Object; - -const tag = original => { - const wrap = new WeakMapSet; - return defineProperties( - asyncTag(original), - { - for: { - value(ref, id) { - const tag = original.for(ref, id); - return wrap.get(tag) || wrap.set(tag, asyncTag(tag)); - } - }, - node: { - value: asyncTag(original.node) - } - } - ); -}; - -const html = tag($html); -exports.html = html; -const svg = tag($svg); -exports.svg = svg; - -const render = (where, what) => { - const hole = typeof what === 'function' ? what() : what; - return Promise.resolve(hole).then(what => $render(where, what)); -}; -exports.render = render; - -(m => { - exports.Hole = m.Hole; -})(require('./index.js')); diff --git a/cjs/handlers.js b/cjs/handlers.js deleted file mode 100644 index 95e222d..0000000 --- a/cjs/handlers.js +++ /dev/null @@ -1,150 +0,0 @@ -'use strict'; -const {diffable} = require('@webreflection/uwire'); - -const {aria, attribute, boolean, event, ref, setter, text} = require('uhandlers'); -const udomdiff = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('udomdiff')); - -const {isArray, createTextNode} = require('./utils.js'); - -// from a generic path, retrieves the exact targeted node -const reducePath = ({childNodes}, i) => childNodes[i]; - -// this helper avoid code bloat around handleAnything() callback -const diff = (comment, oldNodes, newNodes) => udomdiff( - comment.parentNode, - // TODO: there is a possible edge case where a node has been - // removed manually, or it was a keyed one, attached - // to a shared reference between renders. - // In this case udomdiff might fail at removing such node - // as its parent won't be the expected one. - // The best way to avoid this issue is to filter oldNodes - // in search of those not live, or not in the current parent - // anymore, but this would require both a change to uwire, - // exposing a parentNode from the firstChild, as example, - // but also a filter per each diff that should exclude nodes - // that are not in there, penalizing performance quite a lot. - // As this has been also a potential issue with domdiff, - // and both lighterhtml and hyperHTML might fail with this - // very specific edge case, I might as well document this possible - // "diffing shenanigan" and call it a day. - oldNodes, - newNodes, - diffable, - comment -); - -// if an interpolation represents a comment, the whole -// diffing will be related to such comment. -// This helper is in charge of understanding how the new -// content for such interpolation/hole should be updated -const handleAnything = comment => { - let oldValue, text, nodes = []; - const anyContent = newValue => { - switch (typeof newValue) { - // primitives are handled as text content - case 'string': - case 'number': - case 'boolean': - if (oldValue !== newValue) { - oldValue = newValue; - if (!text) - text = createTextNode(''); - text.data = newValue; - nodes = diff(comment, nodes, [text]); - } - break; - // null, and undefined are used to cleanup previous content - case 'object': - case 'undefined': - if (newValue == null) { - if (oldValue != newValue) { - oldValue = newValue; - nodes = diff(comment, nodes, []); - } - break; - } - // arrays and nodes have a special treatment - if (isArray(newValue)) { - oldValue = newValue; - // arrays can be used to cleanup, if empty - if (newValue.length === 0) - nodes = diff(comment, nodes, []); - // or diffed, if these contains nodes or "wires" - else if (typeof newValue[0] === 'object') - nodes = diff(comment, nodes, newValue); - // in all other cases the content is stringified as is - else - anyContent(String(newValue)); - break; - } - // if the new value is a DOM node, or a wire, and it's - // different from the one already live, then it's diffed. - // if the node is a fragment, it's appended once via its childNodes - // There is no `else` here, meaning if the content - // is not expected one, nothing happens, as easy as that. - if (oldValue !== newValue) { - if ('ELEMENT_NODE' in newValue) { - oldValue = newValue; - nodes = diff( - comment, - nodes, - newValue.nodeType === 11 ? - [...newValue.childNodes] : - [newValue] - ); - } - else { - const value = newValue.valueOf(); - if (value !== newValue) - anyContent(value); - } - } - break; - case 'function': - anyContent(newValue(comment)); - break; - } - }; - return anyContent; -}; - -// attributes can be: -// * ref=${...} for hooks and other purposes -// * aria=${...} for aria attributes -// * ?boolean=${...} for boolean attributes -// * .dataset=${...} for dataset related attributes -// * .setter=${...} for Custom Elements setters or nodes with setters -// such as buttons, details, options, select, etc -// * @event=${...} to explicitly handle event listeners -// * onevent=${...} to automatically handle event listeners -// * generic=${...} to handle an attribute just like an attribute -const handleAttribute = (node, name/*, svg*/) => { - switch (name[0]) { - case '?': return boolean(node, name.slice(1), false); - case '.': return setter(node, name.slice(1)); - case '@': return event(node, 'on' + name.slice(1)); - case 'o': if (name[1] === 'n') return event(node, name); - } - - switch (name) { - case 'ref': return ref(node); - case 'aria': return aria(node); - } - - return attribute(node, name/*, svg*/); -}; - -// each mapped update carries the update type and its path -// the type is either node, attribute, or text, while -// the path is how to retrieve the related node to update. -// In the attribute case, the attribute name is also carried along. -function handlers(options) { - const {type, path} = options; - const node = path.reduceRight(reducePath, this); - return type === 'node' ? - handleAnything(node) : - (type === 'attr' ? - handleAttribute(node, options.name/*, options.svg*/) : - text(node)); -} -exports.handlers = handlers; diff --git a/cjs/index.js b/cjs/index.js deleted file mode 100644 index 393cc18..0000000 --- a/cjs/index.js +++ /dev/null @@ -1,67 +0,0 @@ -'use strict'; -const {MapSet, WeakMapSet} = require('@webreflection/mapset'); - -const {Hole, createCache, unroll} = require('./rabbit.js'); - -// both `html` and `svg` template literal tags are polluted -// with a `for(ref[, id])` and a `node` tag too -const tag = type => { - // both `html` and `svg` tags have their own cache - const keyed = new WeakMapSet; - // keyed operations always re-use the same cache and unroll - // the template and its interpolations right away - const fixed = cache => (template, ...values) => unroll( - cache, - {type, template, values} - ); - return Object.assign( - // non keyed operations are recognized as instance of Hole - // during the "unroll", recursively resolved and updated - (template, ...values) => new Hole(type, template, values), - { - // keyed operations need a reference object, usually the parent node - // which is showing keyed results, and optionally a unique id per each - // related node, handy with JSON results and mutable list of objects - // that usually carry a unique identifier - for(ref, id) { - const memo = keyed.get(ref) || keyed.set(ref, new MapSet); - return memo.get(id) || memo.set(id, fixed(createCache())); - }, - // it is possible to create one-off content out of the box via node tag - // this might return the single created node, or a fragment with all - // nodes present at the root level and, of course, their child nodes - node: (template, ...values) => unroll(createCache(), new Hole(type, template, values)).valueOf() - } - ); -}; - -// each rendered node gets its own cache -const cache = new WeakMapSet; - -// rendering means understanding what `html` or `svg` tags returned -// and it relates a specific node to its own unique cache. -// Each time the content to render changes, the node is cleaned up -// and the new new content is appended, and if such content is a Hole -// then it's "unrolled" to resolve all its inner nodes. -const render = (where, what) => { - const hole = typeof what === 'function' ? what() : what; - const info = cache.get(where) || cache.set(where, createCache()); - const wire = hole instanceof Hole ? unroll(info, hole) : hole; - if (wire !== info.wire) { - info.wire = wire; - // valueOf() simply returns the node itself, but in case it was a "wire" - // it will eventually re-append all nodes to its fragment so that such - // fragment can be re-appended many times in a meaningful way - // (wires are basically persistent fragments facades with special behavior) - where.replaceChildren(wire.valueOf()); - } - return where; -}; - -const html = tag('html'); -const svg = tag('svg'); - -exports.Hole = Hole; -exports.render = render; -exports.html = html; -exports.svg = svg; diff --git a/cjs/init.js b/cjs/init.js deleted file mode 100644 index 4abda80..0000000 --- a/cjs/init.js +++ /dev/null @@ -1,629 +0,0 @@ -'use strict'; -const {WeakMapSet} = require('@webreflection/mapset'); -const instrument = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('@webreflection/uparser')); - -const udomdiff = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('udomdiff')); - -module.exports = ({document}) => { - /**start**/ -const {isArray, prototype} = Array; -const {indexOf} = prototype; - - - -const { - createDocumentFragment, - createElement, - createElementNS, - createTextNode, - createTreeWalker, - importNode -} = new Proxy({}, { - get: (_, method) => document[method].bind(document) -}); - - - -const createHTML = html => { - const template = createElement('template'); - template.innerHTML = html; - return template.content; -}; - -let xml; -const createSVG = svg => { - if (!xml) xml = createElementNS('http://www.w3.org/2000/svg', 'svg'); - xml.innerHTML = svg; - const content = createDocumentFragment(); - content.append(...xml.childNodes); - return content; -}; - -const createContent = (text, svg) => svg ? - createSVG(text) : createHTML(text); - -const ELEMENT_NODE = 1; -const nodeType = 111; - -const remove = ({firstChild, lastChild}) => { - const range = document.createRange(); - range.setStartAfter(firstChild); - range.setEndAfter(lastChild); - range.deleteContents(); - return firstChild; -}; - -const diffable = (node, operation) => node.nodeType === nodeType ? - ((1 / operation) < 0 ? - (operation ? remove(node) : node.lastChild) : - (operation ? node.valueOf() : node.firstChild)) : - node -; - -const persistent = fragment => { - const {firstChild, lastChild} = fragment; - if (firstChild === lastChild) - return lastChild || fragment; - const {childNodes} = fragment; - const nodes = [...childNodes]; - return { - ELEMENT_NODE, - nodeType, - firstChild, - lastChild, - valueOf() { - if (childNodes.length !== nodes.length) - fragment.append(...nodes); - return fragment; - } - }; -}; - - - -// flag for foreign checks (slower path, fast by default) -let useForeign = false; - -class Foreign { - constructor(handler, value) { - useForeign = true; - this._ = (...args) => handler(...args, value); - } -} - -const foreign = (handler, value) => new Foreign(handler, value); - -const aria = node => values => { - for (const key in values) { - const name = key === 'role' ? key : `aria-${key}`; - const value = values[key]; - if (value == null) - node.removeAttribute(name); - else - node.setAttribute(name, value); - } -}; - -const getValue = value => value == null ? value : value.valueOf(); - -const attribute = (node, name) => { - let oldValue, orphan = true; - const attributeNode = document.createAttributeNS(null, name); - return newValue => { - const value = useForeign && (newValue instanceof Foreign) ? - newValue._(node, name) : getValue(newValue); - if (oldValue !== value) { - if ((oldValue = value) == null) { - if (!orphan) { - node.removeAttributeNode(attributeNode); - orphan = true; - } - } - else { - attributeNode.value = value; - if (orphan) { - node.setAttributeNodeNS(attributeNode); - orphan = false; - } - } - } - }; -}; - -const boolean = (node, key, oldValue) => newValue => { - const value = !!getValue(newValue); - if (oldValue !== value) { - // when IE won't be around anymore ... - // node.toggleAttribute(key, oldValue = !!value); - if ((oldValue = value)) - node.setAttribute(key, ''); - else - node.removeAttribute(key); - } -}; - -const data = ({dataset}) => values => { - for (const key in values) { - const value = values[key]; - if (value == null) - delete dataset[key]; - else - dataset[key] = value; - } -}; - -const event = (node, name) => { - let oldValue, lower, type = name.slice(2); - if (!(name in node) && (lower = name.toLowerCase()) in node) - type = lower.slice(2); - return newValue => { - const info = isArray(newValue) ? newValue : [newValue, false]; - if (oldValue !== info[0]) { - if (oldValue) - node.removeEventListener(type, oldValue, info[1]); - if (oldValue = info[0]) - node.addEventListener(type, oldValue, info[1]); - } - }; -}; - -const ref = node => { - let oldValue; - return value => { - if (oldValue !== value) { - oldValue = value; - if (typeof value === 'function') - value(node); - else - value.current = node; - } - }; -}; - -const setter = (node, key) => key === 'dataset' ? - data(node) : - value => { - node[key] = value; - }; - -const text = node => { - let oldValue; - return newValue => { - const value = getValue(newValue); - if (oldValue != value) { - oldValue = value; - node.textContent = value == null ? '' : value; - } - }; -}; - - - - - - - - -// from a generic path, retrieves the exact targeted node -const reducePath = ({childNodes}, i) => childNodes[i]; - -// this helper avoid code bloat around handleAnything() callback -const diff = (comment, oldNodes, newNodes) => udomdiff( - comment.parentNode, - // TODO: there is a possible edge case where a node has been - // removed manually, or it was a keyed one, attached - // to a shared reference between renders. - // In this case udomdiff might fail at removing such node - // as its parent won't be the expected one. - // The best way to avoid this issue is to filter oldNodes - // in search of those not live, or not in the current parent - // anymore, but this would require both a change to uwire, - // exposing a parentNode from the firstChild, as example, - // but also a filter per each diff that should exclude nodes - // that are not in there, penalizing performance quite a lot. - // As this has been also a potential issue with domdiff, - // and both lighterhtml and hyperHTML might fail with this - // very specific edge case, I might as well document this possible - // "diffing shenanigan" and call it a day. - oldNodes, - newNodes, - diffable, - comment -); - -// if an interpolation represents a comment, the whole -// diffing will be related to such comment. -// This helper is in charge of understanding how the new -// content for such interpolation/hole should be updated -const handleAnything = comment => { - let oldValue, text, nodes = []; - const anyContent = newValue => { - switch (typeof newValue) { - // primitives are handled as text content - case 'string': - case 'number': - case 'boolean': - if (oldValue !== newValue) { - oldValue = newValue; - if (!text) - text = createTextNode(''); - text.data = newValue; - nodes = diff(comment, nodes, [text]); - } - break; - // null, and undefined are used to cleanup previous content - case 'object': - case 'undefined': - if (newValue == null) { - if (oldValue != newValue) { - oldValue = newValue; - nodes = diff(comment, nodes, []); - } - break; - } - // arrays and nodes have a special treatment - if (isArray(newValue)) { - oldValue = newValue; - // arrays can be used to cleanup, if empty - if (newValue.length === 0) - nodes = diff(comment, nodes, []); - // or diffed, if these contains nodes or "wires" - else if (typeof newValue[0] === 'object') - nodes = diff(comment, nodes, newValue); - // in all other cases the content is stringified as is - else - anyContent(String(newValue)); - break; - } - // if the new value is a DOM node, or a wire, and it's - // different from the one already live, then it's diffed. - // if the node is a fragment, it's appended once via its childNodes - // There is no `else` here, meaning if the content - // is not expected one, nothing happens, as easy as that. - if (oldValue !== newValue) { - if ('ELEMENT_NODE' in newValue) { - oldValue = newValue; - nodes = diff( - comment, - nodes, - newValue.nodeType === 11 ? - [...newValue.childNodes] : - [newValue] - ); - } - else { - const value = newValue.valueOf(); - if (value !== newValue) - anyContent(value); - } - } - break; - case 'function': - anyContent(newValue(comment)); - break; - } - }; - return anyContent; -}; - -// attributes can be: -// * ref=${...} for hooks and other purposes -// * aria=${...} for aria attributes -// * ?boolean=${...} for boolean attributes -// * .dataset=${...} for dataset related attributes -// * .setter=${...} for Custom Elements setters or nodes with setters -// such as buttons, details, options, select, etc -// * @event=${...} to explicitly handle event listeners -// * onevent=${...} to automatically handle event listeners -// * generic=${...} to handle an attribute just like an attribute -const handleAttribute = (node, name/*, svg*/) => { - switch (name[0]) { - case '?': return boolean(node, name.slice(1), false); - case '.': return setter(node, name.slice(1)); - case '@': return event(node, 'on' + name.slice(1)); - case 'o': if (name[1] === 'n') return event(node, name); - } - - switch (name) { - case 'ref': return ref(node); - case 'aria': return aria(node); - } - - return attribute(node, name/*, svg*/); -}; - -// each mapped update carries the update type and its path -// the type is either node, attribute, or text, while -// the path is how to retrieve the related node to update. -// In the attribute case, the attribute name is also carried along. -function handlers(options) { - const {type, path} = options; - const node = path.reduceRight(reducePath, this); - return type === 'node' ? - handleAnything(node) : - (type === 'attr' ? - handleAttribute(node, options.name/*, options.svg*/) : - text(node)); -}; - - - - - - - - -// from a fragment container, create an array of indexes -// related to its child nodes, so that it's possible -// to retrieve later on exact node via reducePath -const createPath = node => { - const path = []; - let {parentNode} = node; - while (parentNode) { - path.push(indexOf.call(parentNode.childNodes, node)); - node = parentNode; - ({parentNode} = node); - } - return path; -}; - -// the prefix is used to identify either comments, attributes, or nodes -// that contain the related unique id. In the attribute cases -// isĀµX="attribute-name" will be used to map current X update to that -// attribute name, while comments will be like , to map -// the update to that specific comment node, hence its parent. -// style and textarea will have text content, and are handled -// directly through text-only updates. -const prefix = 'isĀµ'; - -// Template Literals are unique per scope and static, meaning a template -// should be parsed once, and once only, as it will always represent the same -// content, within the exact same amount of updates each time. -// This cache relates each template to its unique content and updates. -const cache = new WeakMapSet; - -// a RegExp that helps checking nodes that cannot contain comments -const textOnly = /^(?:textarea|script|style|title|plaintext|xmp)$/; - -const createCache = () => ({ - stack: [], // each template gets a stack for each interpolation "hole" - - entry: null, // each entry contains details, such as: - // * the template that is representing - // * the type of node it represents (html or svg) - // * the content fragment with all nodes - // * the list of updates per each node (template holes) - // * the "wired" node or fragment that will get updates - // if the template or type are different from the previous one - // the entry gets re-created each time - - wire: null // each rendered node represent some wired content and - // this reference to the latest one. If different, the node - // will be cleaned up and the new "wire" will be appended -}); - -// the entry stored in the rendered node cache, and per each "hole" -const createEntry = (type, template) => { - const {content, updates} = mapUpdates(type, template); - return {type, template, content, updates, wire: null}; -}; - -// a template is instrumented to be able to retrieve where updates are needed. -// Each unique template becomes a fragment, cloned once per each other -// operation based on the same template, i.e. data => html`

${data}

` -const mapTemplate = (type, template) => { - const svg = type === 'svg'; - const text = instrument(template, prefix, svg); - const content = createContent(text, svg); - // once instrumented and reproduced as fragment, it's crawled - // to find out where each update is in the fragment tree - const tw = createTreeWalker(content, 1 | 128); - const nodes = []; - const length = template.length - 1; - let i = 0; - // updates are searched via unique names, linearly increased across the tree - //
- let search = `${prefix}${i}`; - while (i < length) { - const node = tw.nextNode(); - // if not all updates are bound but there's nothing else to crawl - // it means that there is something wrong with the template. - if (!node) - throw `bad template: ${text}`; - // if the current node is a comment, and it contains isĀµX - // it means the update should take care of any content - if (node.nodeType === 8) { - // The only comments to be considered are those - // which content is exactly the same as the searched one. - if (node.data === search) { - nodes.push({type: 'node', path: createPath(node)}); - search = `${prefix}${++i}`; - } - } - else { - // if the node is not a comment, loop through all its attributes - // named isĀµX and relate attribute updates to this node and the - // attribute name, retrieved through node.getAttribute("isĀµX") - // the isĀµX attribute will be removed as irrelevant for the layout - // let svg = -1; - while (node.hasAttribute(search)) { - nodes.push({ - type: 'attr', - path: createPath(node), - name: node.getAttribute(search) - }); - node.removeAttribute(search); - search = `${prefix}${++i}`; - } - // if the node was a style, textarea, or others, check its content - // and if it is then update tex-only this node - if ( - textOnly.test(node.localName) && - node.textContent.trim() === `` - ){ - node.textContent = ''; - nodes.push({type: 'text', path: createPath(node)}); - search = `${prefix}${++i}`; - } - } - } - // once all nodes to update, or their attributes, are known, the content - // will be cloned in the future to represent the template, and all updates - // related to such content retrieved right away without needing to re-crawl - // the exact same template, and its content, more than once. - return {content, nodes}; -}; - -// if a template is unknown, perform the previous mapping, otherwise grab -// its details such as the fragment with all nodes, and updates info. -const mapUpdates = (type, template) => { - const {content, nodes} = ( - cache.get(template) || - cache.set(template, mapTemplate(type, template)) - ); - // clone deeply the fragment - const fragment = importNode(content, true); - // and relate an update handler per each node that needs one - const updates = nodes.map(handlers, fragment); - // return the fragment and all updates to use within its nodes - return {content: fragment, updates}; -}; - -// as html and svg can be nested calls, but no parent node is known -// until rendered somewhere, the unroll operation is needed to -// discover what to do with each interpolation, which will result -// into an update operation. -const unroll = (info, {type, template, values}) => { - // interpolations can contain holes and arrays, so these need - // to be recursively discovered - const length = unrollValues(info, values); - let {entry} = info; - // if the cache entry is either null or different from the template - // and the type this unroll should resolve, create a new entry - // assigning a new content fragment and the list of updates. - if (!entry || (entry.template !== template || entry.type !== type)) - info.entry = (entry = createEntry(type, template)); - const {content, updates, wire} = entry; - // even if the fragment and its nodes is not live yet, - // it is already possible to update via interpolations values. - for (let i = 0; i < length; i++) - updates[i](values[i]); - // if the entry was new, or representing a different template or type, - // create a new persistent entity to use during diffing. - // This is simply a DOM node, when the template has a single container, - // as in `

`, or a "wire" in `

` and similar cases. - return wire || (entry.wire = persistent(content)); -}; - -// the stack retains, per each interpolation value, the cache -// related to each interpolation value, or null, if the render -// was conditional and the value is not special (Array or Hole) -const unrollValues = ({stack}, values) => { - const {length} = values; - for (let i = 0; i < length; i++) { - const hole = values[i]; - // each Hole gets unrolled and re-assigned as value - // so that domdiff will deal with a node/wire, not with a hole - if (hole instanceof Hole) - values[i] = unroll( - stack[i] || (stack[i] = createCache()), - hole - ); - // arrays are recursively resolved so that each entry will contain - // also a DOM node or a wire, hence it can be diffed if/when needed - else if (isArray(hole)) - unrollValues(stack[i] || (stack[i] = createCache()), hole); - // if the value is nothing special, the stack doesn't need to retain data - // this is useful also to cleanup previously retained data, if the value - // was a Hole, or an Array, but not anymore, i.e.: - // const update = content => html`
${content}
`; - // update(listOfItems); update(null); update(html`hole`) - else - stack[i] = null; - } - if (length < stack.length) - stack.splice(length); - return length; -}; - -/** - * Holds all details wrappers needed to render the content further on. - * @constructor - * @param {string} type The hole type, either `html` or `svg`. - * @param {string[]} template The template literals used to the define the content. - * @param {Array} values Zero, one, or more interpolated values to render. - */ -class Hole { - constructor(type, template, values) { - this.type = type; - this.template = template; - this.values = values; - } -}; - - - - - -// both `html` and `svg` template literal tags are polluted -// with a `for(ref[, id])` and a `node` tag too -const tag = type => { - // both `html` and `svg` tags have their own _cache - const keyed = new WeakMapSet; - // keyed operations always re-use the same _cache and unroll - // the template and its interpolations right away - const fixed = _cache => (template, ...values) => unroll( - _cache, - {type, template, values} - ); - return Object.assign( - // non keyed operations are recognized as instance of Hole - // during the "unroll", recursively resolved and updated - (template, ...values) => new Hole(type, template, values), - { - // keyed operations need a reference object, usually the parent node - // which is showing keyed results, and optionally a unique id per each - // related node, handy with JSON results and mutable list of objects - // that usually carry a unique identifier - for(ref, id) { - const memo = keyed.get(ref) || keyed.set(ref, new MapSet); - return memo.get(id) || memo.set(id, fixed(createCache())); - }, - // it is possible to create one-off content out of the box via node tag - // this might return the single created node, or a fragment with all - // nodes present at the root level and, of course, their child nodes - node: (template, ...values) => unroll(createCache(), new Hole(type, template, values)).valueOf() - } - ); -}; - -// each rendered node gets its own _cache -const _cache = new WeakMapSet; - -// rendering means understanding what `html` or `svg` tags returned -// and it relates a specific node to its own unique _cache. -// Each time the content to render changes, the node is cleaned up -// and the new new content is appended, and if such content is a Hole -// then it's "unrolled" to resolve all its inner nodes. -const render = (where, what) => { - const hole = typeof what === 'function' ? what() : what; - const info = _cache.get(where) || _cache.set(where, createCache()); - const wire = hole instanceof Hole ? unroll(info, hole) : hole; - if (wire !== info.wire) { - info.wire = wire; - // valueOf() simply returns the node itself, but in case it was a "wire" - // it will eventually re-append all nodes to its fragment so that such - // fragment can be re-appended many times in a meaningful way - // (wires are basically persistent fragments facades with special behavior) - where.replaceChildren(wire.valueOf()); - } - return where; -}; - -const html = tag('html'); -const svg = tag('svg'); - -return {Hole, render, html, svg}; - -/**end**/ -}; diff --git a/cjs/json.js b/cjs/json.js deleted file mode 100644 index 07028df..0000000 --- a/cjs/json.js +++ /dev/null @@ -1,52 +0,0 @@ -'use strict'; -const {MapSet, WeakMapSet} = require('@webreflection/mapset'); - -const {render: $render, html, svg} = require('./index.js'); - -// Sender (SW, Worker, postMessage) -const ids = new WeakMapSet; -let id = 0; - -const tag = type => (template, ...values) => ({ - id: ids.get(template) || - ids.set(template, id++), - type, - values, - template -}); - -html.json = tag('html'); -svg.json = tag('svg'); - -// Receiver (onmessage, from SW, Workers, etc) -const templates = new MapSet; - -const unroll = ({type, template, values, id}) => ( - (type === 'svg' ? svg : html).apply( - null, - [ - templates.get(id) || - templates.set(id, template) - ].concat(values.map(asJSON)) - ) -); - -const asJSON = value => isJSON(value) ? unroll(value) : value; - -const isJSON = thing => ( - typeof thing === 'object' && - thing !== null && - 'id' in thing && - 'type' in thing && - 'values' in thing && - 'template' in thing -); - -const render = (where, what) => $render( - where, - isJSON(what) ? unroll(what) : what -); - -exports.render = render; -exports.html = html; -exports.svg = svg; diff --git a/cjs/rabbit.js b/cjs/rabbit.js deleted file mode 100644 index d38cc8a..0000000 --- a/cjs/rabbit.js +++ /dev/null @@ -1,218 +0,0 @@ -'use strict'; -const {WeakMapSet} = require('@webreflection/mapset'); -const instrument = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('@webreflection/uparser')); -const {persistent} = require('@webreflection/uwire'); - -const {handlers} = require('./handlers.js'); -const {isArray, indexOf, createContent, createTreeWalker, importNode} = require('./utils.js'); - -// from a fragment container, create an array of indexes -// related to its child nodes, so that it's possible -// to retrieve later on exact node via reducePath -const createPath = node => { - const path = []; - let {parentNode} = node; - while (parentNode) { - path.push(indexOf.call(parentNode.childNodes, node)); - node = parentNode; - ({parentNode} = node); - } - return path; -}; - -// the prefix is used to identify either comments, attributes, or nodes -// that contain the related unique id. In the attribute cases -// isĀµX="attribute-name" will be used to map current X update to that -// attribute name, while comments will be like , to map -// the update to that specific comment node, hence its parent. -// style and textarea will have text content, and are handled -// directly through text-only updates. -const prefix = 'isĀµ'; - -// Template Literals are unique per scope and static, meaning a template -// should be parsed once, and once only, as it will always represent the same -// content, within the exact same amount of updates each time. -// This cache relates each template to its unique content and updates. -const cache = new WeakMapSet; - -// a RegExp that helps checking nodes that cannot contain comments -const textOnly = /^(?:textarea|script|style|title|plaintext|xmp)$/; - -const createCache = () => ({ - stack: [], // each template gets a stack for each interpolation "hole" - - entry: null, // each entry contains details, such as: - // * the template that is representing - // * the type of node it represents (html or svg) - // * the content fragment with all nodes - // * the list of updates per each node (template holes) - // * the "wired" node or fragment that will get updates - // if the template or type are different from the previous one - // the entry gets re-created each time - - wire: null // each rendered node represent some wired content and - // this reference to the latest one. If different, the node - // will be cleaned up and the new "wire" will be appended -}); -exports.createCache = createCache; - -// the entry stored in the rendered node cache, and per each "hole" -const createEntry = (type, template) => { - const {content, updates} = mapUpdates(type, template); - return {type, template, content, updates, wire: null}; -}; - -// a template is instrumented to be able to retrieve where updates are needed. -// Each unique template becomes a fragment, cloned once per each other -// operation based on the same template, i.e. data => html`

${data}

` -const mapTemplate = (type, template) => { - const svg = type === 'svg'; - const text = instrument(template, prefix, svg); - const content = createContent(text, svg); - // once instrumented and reproduced as fragment, it's crawled - // to find out where each update is in the fragment tree - const tw = createTreeWalker(content, 1 | 128); - const nodes = []; - const length = template.length - 1; - let i = 0; - // updates are searched via unique names, linearly increased across the tree - //
- let search = `${prefix}${i}`; - while (i < length) { - const node = tw.nextNode(); - // if not all updates are bound but there's nothing else to crawl - // it means that there is something wrong with the template. - if (!node) - throw `bad template: ${text}`; - // if the current node is a comment, and it contains isĀµX - // it means the update should take care of any content - if (node.nodeType === 8) { - // The only comments to be considered are those - // which content is exactly the same as the searched one. - if (node.data === search) { - nodes.push({type: 'node', path: createPath(node)}); - search = `${prefix}${++i}`; - } - } - else { - // if the node is not a comment, loop through all its attributes - // named isĀµX and relate attribute updates to this node and the - // attribute name, retrieved through node.getAttribute("isĀµX") - // the isĀµX attribute will be removed as irrelevant for the layout - // let svg = -1; - while (node.hasAttribute(search)) { - nodes.push({ - type: 'attr', - path: createPath(node), - name: node.getAttribute(search) - }); - node.removeAttribute(search); - search = `${prefix}${++i}`; - } - // if the node was a style, textarea, or others, check its content - // and if it is then update tex-only this node - if ( - textOnly.test(node.localName) && - node.textContent.trim() === `` - ){ - node.textContent = ''; - nodes.push({type: 'text', path: createPath(node)}); - search = `${prefix}${++i}`; - } - } - } - // once all nodes to update, or their attributes, are known, the content - // will be cloned in the future to represent the template, and all updates - // related to such content retrieved right away without needing to re-crawl - // the exact same template, and its content, more than once. - return {content, nodes}; -}; - -// if a template is unknown, perform the previous mapping, otherwise grab -// its details such as the fragment with all nodes, and updates info. -const mapUpdates = (type, template) => { - const {content, nodes} = ( - cache.get(template) || - cache.set(template, mapTemplate(type, template)) - ); - // clone deeply the fragment - const fragment = importNode(content, true); - // and relate an update handler per each node that needs one - const updates = nodes.map(handlers, fragment); - // return the fragment and all updates to use within its nodes - return {content: fragment, updates}; -}; - -// as html and svg can be nested calls, but no parent node is known -// until rendered somewhere, the unroll operation is needed to -// discover what to do with each interpolation, which will result -// into an update operation. -const unroll = (info, {type, template, values}) => { - // interpolations can contain holes and arrays, so these need - // to be recursively discovered - const length = unrollValues(info, values); - let {entry} = info; - // if the cache entry is either null or different from the template - // and the type this unroll should resolve, create a new entry - // assigning a new content fragment and the list of updates. - if (!entry || (entry.template !== template || entry.type !== type)) - info.entry = (entry = createEntry(type, template)); - const {content, updates, wire} = entry; - // even if the fragment and its nodes is not live yet, - // it is already possible to update via interpolations values. - for (let i = 0; i < length; i++) - updates[i](values[i]); - // if the entry was new, or representing a different template or type, - // create a new persistent entity to use during diffing. - // This is simply a DOM node, when the template has a single container, - // as in `

`, or a "wire" in `

` and similar cases. - return wire || (entry.wire = persistent(content)); -}; -exports.unroll = unroll; - -// the stack retains, per each interpolation value, the cache -// related to each interpolation value, or null, if the render -// was conditional and the value is not special (Array or Hole) -const unrollValues = ({stack}, values) => { - const {length} = values; - for (let i = 0; i < length; i++) { - const hole = values[i]; - // each Hole gets unrolled and re-assigned as value - // so that domdiff will deal with a node/wire, not with a hole - if (hole instanceof Hole) - values[i] = unroll( - stack[i] || (stack[i] = createCache()), - hole - ); - // arrays are recursively resolved so that each entry will contain - // also a DOM node or a wire, hence it can be diffed if/when needed - else if (isArray(hole)) - unrollValues(stack[i] || (stack[i] = createCache()), hole); - // if the value is nothing special, the stack doesn't need to retain data - // this is useful also to cleanup previously retained data, if the value - // was a Hole, or an Array, but not anymore, i.e.: - // const update = content => html`
${content}
`; - // update(listOfItems); update(null); update(html`hole`) - else - stack[i] = null; - } - if (length < stack.length) - stack.splice(length); - return length; -}; - -/** - * Holds all details wrappers needed to render the content further on. - * @constructor - * @param {string} type The hole type, either `html` or `svg`. - * @param {string[]} template The template literals used to the define the content. - * @param {Array} values Zero, one, or more interpolated values to render. - */ -class Hole { - constructor(type, template, values) { - this.type = type; - this.template = template; - this.values = values; - } -} -exports.Hole = Hole; diff --git a/cjs/utils.js b/cjs/utils.js deleted file mode 100644 index 61703ae..0000000 --- a/cjs/utils.js +++ /dev/null @@ -1,40 +0,0 @@ -'use strict'; -const {isArray, prototype} = Array; -const {indexOf} = prototype; - -exports.isArray = isArray; -exports.indexOf = indexOf; - -const { - createDocumentFragment, - createElement, - createElementNS, - createTextNode, - createTreeWalker, - importNode -} = new Proxy({}, { - get: (_, method) => document[method].bind(document) -}); - -exports.createTextNode = createTextNode; -exports.createTreeWalker = createTreeWalker; -exports.importNode = importNode; - -const createHTML = html => { - const template = createElement('template'); - template.innerHTML = html; - return template.content; -}; - -let xml; -const createSVG = svg => { - if (!xml) xml = createElementNS('http://www.w3.org/2000/svg', 'svg'); - xml.innerHTML = svg; - const content = createDocumentFragment(); - content.append(...xml.childNodes); - return content; -}; - -const createContent = (text, svg) => svg ? - createSVG(text) : createHTML(text); -exports.createContent = createContent; diff --git a/cjs/x.js b/cjs/x.js deleted file mode 100644 index acc4e2d..0000000 --- a/cjs/x.js +++ /dev/null @@ -1,14 +0,0 @@ -'use strict'; -const {createPragma} = require('jsx2tag'); -const {html} = require('./index.js'); - -const createElement = createPragma(html); -self.React = { - createElement, - Fragment: createElement -}; - -(m => Object.keys(m).map(k => k !== 'default' && (exports[k] = m[k]))) -(require('jsx2tag')); -(m => Object.keys(m).map(k => k !== 'default' && (exports[k] = m[k]))) -(require('./index.js')); diff --git a/es.js b/es.js deleted file mode 100644 index c9d0574..0000000 --- a/es.js +++ /dev/null @@ -1,2 +0,0 @@ -self.uhtml=function(e){"use strict";class t extends Map{set(e,t){return super.set(e,t),t}}class n extends WeakMap{set(e,t){return super.set(e,t),t}} -/*! (c) Andrea Giammarchi - ISC */const r=/^(?:area|base|br|col|embed|hr|img|input|keygen|link|menuitem|meta|param|source|track|wbr)$/i,s=/<([a-z]+[a-z0-9:._-]*)([^>]*?)(\/?)>/g,l=/([^\s\\>"'=]+)\s*=\s*(['"]?)\x01/g,o=/[\x01\x02]/g;const a=(e,t)=>111===e.nodeType?1/t<0?t?(({firstChild:e,lastChild:t})=>{const n=document.createRange();return n.setStartAfter(e),n.setEndAfter(t),n.deleteContents(),e})(e):e.lastChild:t?e.valueOf():e.firstChild:e,{isArray:i}=Array,c=e=>null==e?e:e.valueOf(),u=(e,t)=>{let n,r,s=t.slice(2);return!(t in e)&&(r=t.toLowerCase())in e&&(s=r.slice(2)),t=>{const r=i(t)?t:[t,!1];n!==r[0]&&(n&&e.removeEventListener(s,n,r[1]),(n=r[0])&&e.addEventListener(s,n,r[1]))}};const{isArray:d,prototype:p}=Array,{indexOf:f}=p,{createDocumentFragment:h,createElement:m,createElementNS:g,createTextNode:y,createTreeWalker:b,importNode:w}=new Proxy({},{get:(e,t)=>document[t].bind(document)});let v;const x=(e,t)=>t?(e=>{v||(v=g("http://www.w3.org/2000/svg","svg")),v.innerHTML=e;const t=h();return t.append(...v.childNodes),t})(e):(e=>{const t=m("template");return t.innerHTML=e,t.content})(e),N=({childNodes:e},t)=>e[t],C=(e,t,n)=>((e,t,n,r,s)=>{const l=n.length;let o=t.length,a=l,i=0,c=0,u=null;for(;is-c){const l=r(t[i],0);for(;c{switch(t[0]){case"?":return((e,t,n)=>r=>{const s=!!c(r);n!==s&&((n=s)?e.setAttribute(t,""):e.removeAttribute(t))})(e,t.slice(1),!1);case".":return((e,t)=>"dataset"===t?(({dataset:e})=>t=>{for(const n in t){const r=t[n];null==r?delete e[n]:e[n]=r}})(e):n=>{e[t]=n})(e,t.slice(1));case"@":return u(e,"on"+t.slice(1));case"o":if("n"===t[1])return u(e,t)}switch(t){case"ref":return(e=>{let t;return n=>{t!==n&&(t=n,"function"==typeof n?n(e):n.current=e)}})(e);case"aria":return(e=>t=>{for(const n in t){const r="role"===n?n:`aria-${n}`,s=t[n];null==s?e.removeAttribute(r):e.setAttribute(r,s)}})(e)}return((e,t)=>{let n,r=!0;const s=document.createAttributeNS(null,t);return t=>{const l=c(t);n!==l&&(null==(n=l)?r||(e.removeAttributeNode(s),r=!0):(s.value=l,r&&(e.setAttributeNodeNS(s),r=!1)))}})(e,t)};function k(e){const{type:t,path:n}=e,r=n.reduceRight(N,this);return"node"===t?(e=>{let t,n,r=[];const s=l=>{switch(typeof l){case"string":case"number":case"boolean":t!==l&&(t=l,n||(n=y("")),n.data=l,r=C(e,r,[n]));break;case"object":case"undefined":if(null==l){t!=l&&(t=l,r=C(e,r,[]));break}if(d(l)){t=l,0===l.length?r=C(e,r,[]):"object"==typeof l[0]?r=C(e,r,l):s(String(l));break}if(t!==l)if("ELEMENT_NODE"in l)t=l,r=C(e,r,11===l.nodeType?[...l.childNodes]:[l]);else{const e=l.valueOf();e!==l&&s(e)}break;case"function":s(l(e))}};return s})(r):"attr"===t?A(r,e.name):(e=>{let t;return n=>{const r=c(n);t!=r&&(t=r,e.textContent=null==r?"":r)}})(r)}const $=e=>{const t=[];let{parentNode:n}=e;for(;n;)t.push(f.call(n.childNodes,e)),e=n,({parentNode:n}=e);return t},E="isĀµ",O=new n,T=/^(?:textarea|script|style|title|plaintext|xmp)$/,S=(e,t)=>{const n="svg"===e,a=((e,t,n)=>{let a=0;return e.join("").trim().replace(s,((e,t,s,o)=>{let a=t+s.replace(l,"=$2$1").trimEnd();return o.length&&(a+=n||r.test(t)?" /":">"})).replace(o,(e=>""===e?"\x3c!--"+t+a+++"--\x3e":t+a++))})(t,E,n),i=x(a,n),c=b(i,129),u=[],d=t.length-1;let p=0,f=`${E}${p}`;for(;p{const{content:n,nodes:r}=O.get(t)||O.set(t,S(e,t)),s=w(n,!0);return{content:s,updates:r.map(k,s)}},M=(e,{type:t,template:n,values:r})=>{const s=j(e,r);let{entry:l}=e;l&&l.template===n&&l.type===t||(e.entry=l=((e,t)=>{const{content:n,updates:r}=L(e,t);return{type:e,template:t,content:n,updates:r,wire:null}})(t,n));const{content:o,updates:a,wire:i}=l;for(let e=0;e{const{firstChild:t,lastChild:n}=e;if(t===n)return n||e;const{childNodes:r}=e,s=[...r];return{ELEMENT_NODE:1,nodeType:111,firstChild:t,lastChild:n,valueOf:()=>(r.length!==s.length&&e.append(...s),e)}})(o))},j=({stack:e},t)=>{const{length:n}=t;for(let r=0;r{const r=new n;return Object.assign(((t,...n)=>new B(e,t,n)),{for(n,s){const l=r.get(n)||r.set(n,new t);return l.get(s)||l.set(s,(t=>(n,...r)=>M(t,{type:e,template:n,values:r}))({stack:[],entry:null,wire:null}))},node:(t,...n)=>M({stack:[],entry:null,wire:null},new B(e,t,n)).valueOf()})},H=new n,_=D("html"),z=D("svg");return e.Hole=B,e.html=_,e.render=(e,t)=>{const n="function"==typeof t?t():t,r=H.get(e)||H.set(e,{stack:[],entry:null,wire:null}),s=n instanceof B?M(r,n):n;return s!==r.wire&&(r.wire=s,e.replaceChildren(s.valueOf())),e},e.svg=z,e}({}); diff --git a/esm.js b/esm.js deleted file mode 100644 index c06dc21..0000000 --- a/esm.js +++ /dev/null @@ -1,2 +0,0 @@ -class e extends Map{set(e,t){return super.set(e,t),t}}class t extends WeakMap{set(e,t){return super.set(e,t),t}} -/*! (c) Andrea Giammarchi - ISC */const n=/^(?:area|base|br|col|embed|hr|img|input|keygen|link|menuitem|meta|param|source|track|wbr)$/i,r=/<([a-z]+[a-z0-9:._-]*)([^>]*?)(\/?)>/g,s=/([^\s\\>"'=]+)\s*=\s*(['"]?)\x01/g,l=/[\x01\x02]/g;const o=(e,t)=>111===e.nodeType?1/t<0?t?(({firstChild:e,lastChild:t})=>{const n=document.createRange();return n.setStartAfter(e),n.setEndAfter(t),n.deleteContents(),e})(e):e.lastChild:t?e.valueOf():e.firstChild:e,{isArray:a}=Array,i=e=>null==e?e:e.valueOf(),c=(e,t)=>{let n,r,s=t.slice(2);return!(t in e)&&(r=t.toLowerCase())in e&&(s=r.slice(2)),t=>{const r=a(t)?t:[t,!1];n!==r[0]&&(n&&e.removeEventListener(s,n,r[1]),(n=r[0])&&e.addEventListener(s,n,r[1]))}};const{isArray:u,prototype:d}=Array,{indexOf:p}=d,{createDocumentFragment:f,createElement:h,createElementNS:m,createTextNode:g,createTreeWalker:y,importNode:b}=new Proxy({},{get:(e,t)=>document[t].bind(document)});let w;const x=(e,t)=>t?(e=>{w||(w=m("http://www.w3.org/2000/svg","svg")),w.innerHTML=e;const t=f();return t.append(...w.childNodes),t})(e):(e=>{const t=h("template");return t.innerHTML=e,t.content})(e),v=({childNodes:e},t)=>e[t],N=(e,t,n)=>((e,t,n,r,s)=>{const l=n.length;let o=t.length,a=l,i=0,c=0,u=null;for(;is-c){const l=r(t[i],0);for(;c{switch(t[0]){case"?":return((e,t,n)=>r=>{const s=!!i(r);n!==s&&((n=s)?e.setAttribute(t,""):e.removeAttribute(t))})(e,t.slice(1),!1);case".":return((e,t)=>"dataset"===t?(({dataset:e})=>t=>{for(const n in t){const r=t[n];null==r?delete e[n]:e[n]=r}})(e):n=>{e[t]=n})(e,t.slice(1));case"@":return c(e,"on"+t.slice(1));case"o":if("n"===t[1])return c(e,t)}switch(t){case"ref":return(e=>{let t;return n=>{t!==n&&(t=n,"function"==typeof n?n(e):n.current=e)}})(e);case"aria":return(e=>t=>{for(const n in t){const r="role"===n?n:`aria-${n}`,s=t[n];null==s?e.removeAttribute(r):e.setAttribute(r,s)}})(e)}return((e,t)=>{let n,r=!0;const s=document.createAttributeNS(null,t);return t=>{const l=i(t);n!==l&&(null==(n=l)?r||(e.removeAttributeNode(s),r=!0):(s.value=l,r&&(e.setAttributeNodeNS(s),r=!1)))}})(e,t)};function A(e){const{type:t,path:n}=e,r=n.reduceRight(v,this);return"node"===t?(e=>{let t,n,r=[];const s=l=>{switch(typeof l){case"string":case"number":case"boolean":t!==l&&(t=l,n||(n=g("")),n.data=l,r=N(e,r,[n]));break;case"object":case"undefined":if(null==l){t!=l&&(t=l,r=N(e,r,[]));break}if(u(l)){t=l,0===l.length?r=N(e,r,[]):"object"==typeof l[0]?r=N(e,r,l):s(String(l));break}if(t!==l)if("ELEMENT_NODE"in l)t=l,r=N(e,r,11===l.nodeType?[...l.childNodes]:[l]);else{const e=l.valueOf();e!==l&&s(e)}break;case"function":s(l(e))}};return s})(r):"attr"===t?C(r,e.name):(e=>{let t;return n=>{const r=i(n);t!=r&&(t=r,e.textContent=null==r?"":r)}})(r)}const k=e=>{const t=[];let{parentNode:n}=e;for(;n;)t.push(p.call(n.childNodes,e)),e=n,({parentNode:n}=e);return t},$="isĀµ",E=new t,O=/^(?:textarea|script|style|title|plaintext|xmp)$/,T=(e,t)=>{const o="svg"===e,a=((e,t,o)=>{let a=0;return e.join("").trim().replace(r,((e,t,r,l)=>{let a=t+r.replace(s,"=$2$1").trimEnd();return l.length&&(a+=o||n.test(t)?" /":">"})).replace(l,(e=>""===e?"\x3c!--"+t+a+++"--\x3e":t+a++))})(t,$,o),i=x(a,o),c=y(i,129),u=[],d=t.length-1;let p=0,f=`${$}${p}`;for(;p{const{content:n,nodes:r}=E.get(t)||E.set(t,T(e,t)),s=b(n,!0);return{content:s,updates:r.map(A,s)}},L=(e,{type:t,template:n,values:r})=>{const s=M(e,r);let{entry:l}=e;l&&l.template===n&&l.type===t||(e.entry=l=((e,t)=>{const{content:n,updates:r}=S(e,t);return{type:e,template:t,content:n,updates:r,wire:null}})(t,n));const{content:o,updates:a,wire:i}=l;for(let e=0;e{const{firstChild:t,lastChild:n}=e;if(t===n)return n||e;const{childNodes:r}=e,s=[...r];return{ELEMENT_NODE:1,nodeType:111,firstChild:t,lastChild:n,valueOf:()=>(r.length!==s.length&&e.append(...s),e)}})(o))},M=({stack:e},t)=>{const{length:n}=t;for(let r=0;r{const r=new t;return Object.assign(((e,...t)=>new j(n,e,t)),{for(t,s){const l=r.get(t)||r.set(t,new e);return l.get(s)||l.set(s,(e=>(t,...r)=>L(e,{type:n,template:t,values:r}))({stack:[],entry:null,wire:null}))},node:(e,...t)=>L({stack:[],entry:null,wire:null},new j(n,e,t)).valueOf()})},D=new t,_=(e,t)=>{const n="function"==typeof t?t():t,r=D.get(e)||D.set(e,{stack:[],entry:null,wire:null}),s=n instanceof j?L(r,n):n;return s!==r.wire&&(r.wire=s,e.replaceChildren(s.valueOf())),e},z=B("html"),H=B("svg");export{j as Hole,z as html,_ as render,H as svg}; diff --git a/esm/async.js b/esm/async.js deleted file mode 100644 index 812fb92..0000000 --- a/esm/async.js +++ /dev/null @@ -1,35 +0,0 @@ -import {WeakMapSet} from '@webreflection/mapset'; - -import asyncTag from 'async-tag'; - -import {render as $render, html as $html, svg as $svg} from './index.js'; - -const {defineProperties} = Object; - -const tag = original => { - const wrap = new WeakMapSet; - return defineProperties( - asyncTag(original), - { - for: { - value(ref, id) { - const tag = original.for(ref, id); - return wrap.get(tag) || wrap.set(tag, asyncTag(tag)); - } - }, - node: { - value: asyncTag(original.node) - } - } - ); -}; - -export const html = tag($html); -export const svg = tag($svg); - -export const render = (where, what) => { - const hole = typeof what === 'function' ? what() : what; - return Promise.resolve(hole).then(what => $render(where, what)); -}; - -export {Hole} from './index.js'; diff --git a/esm/create-content.js b/esm/create-content.js new file mode 100644 index 0000000..e4d2484 --- /dev/null +++ b/esm/create-content.js @@ -0,0 +1,23 @@ +import { newRange } from './utils.js'; + +let template = document.createElement('template'), svg, range; + +/** + * @param {string} text + * @param {boolean} xml + * @returns {DocumentFragment} + */ +export default (text, xml) => { + if (xml) { + if (!svg) { + svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + range = newRange(); + range.selectNodeContents(svg); + } + return range.createContextualFragment(text); + } + template.innerHTML = text; + const { content } = template; + template = template.cloneNode(false); + return content; +}; diff --git a/esm/creator.js b/esm/creator.js new file mode 100644 index 0000000..2a1203a --- /dev/null +++ b/esm/creator.js @@ -0,0 +1,35 @@ +import { COMMENT_NODE } from 'domconstants/constants'; + +import { PersistentFragment } from './persistent-fragment.js'; +import { detail, parsed } from './literals.js'; +import { empty } from './utils.js'; + +/** + * @param {DocumentFragment} content + * @param {number[]} path + * @returns {Element} + */ +const find = (content, path) => path.reduceRight(childNodesIndex, content); +const childNodesIndex = (node, i) => node.childNodes[i]; + +/** @param {(template: TemplateStringsArray, values: any[]) => import("./parser.js").Resolved} parse */ +export default parse => ( + /** @param {(template: TemplateStringsArray, values: any[]) => import("./literals.js").Parsed} parse */ + (template, values) => { + const { c: content, e: entries, l: length } = parse(template, values); + const root = content.cloneNode(true); + // reverse loop to avoid missing paths while populating + // TODO: is it even worth to pre-populate nodes? see rabbit.js too + let current, prev, i = entries.length, details = i ? entries.slice(0) : empty; + while (i--) { + const { t: type, p: path, u: update, n: name } = entries[i]; + const node = path === prev ? current : (current = find(root, (prev = path))); + const callback = type === COMMENT_NODE ? update() : update; + details[i] = detail(callback(node, values[i], name, empty), callback, node, name); + } + return parsed( + length === 1 ? root.firstChild : new PersistentFragment(root), + details + ); + } +); diff --git a/esm/handler.js b/esm/handler.js new file mode 100644 index 0000000..e7b1cdf --- /dev/null +++ b/esm/handler.js @@ -0,0 +1,203 @@ +import udomdiff from 'udomdiff'; +import { empty, isArray, set } from './utils.js'; +import { diffFragment } from './persistent-fragment.js'; +import drop from './range.js'; + +/** + * @template T + * @param {Element} element + * @param {T} value + * @returns {T} + */ +const aria = (element, value) => { + for (const key in value) { + const $ = value[key]; + const name = key === 'role' ? key : `aria-${key}`; + if ($ == null) element.removeAttribute(name); + else element.setAttribute(name, $); + } + return value; +}; + +let listeners; + +/** + * @template T + * @param {Element} element + * @param {T} value + * @param {string} name + * @returns {T} + */ +const at = (element, value, name) => { + name = name.slice(1); + if (!listeners) listeners = new WeakMap; + const known = listeners.get(element) || set(listeners, element, {}); + let current = known[name]; + if (current && current[0]) element.removeEventListener(...current); + current = isArray(value) ? value : [value, false]; + known[name] = current; + if (current[0]) element.addEventListener(...current); + return value; +}; + +/** + * @template T + * @param {Element} element + * @param {T} value + * @returns {T} + */ +const className = (element, value) => direct(element, value, 'className'); + +/** + * @template T + * @param {Element} element + * @param {T} value + * @returns {T} + */ +const data = (element, value) => { + const { dataset } = element; + for (const key in value) { + if (value[key] == null) delete dataset[key]; + else dataset[key] = value[key]; + } + return value; +}; + +/** + * @template T + * @param {Element | CSSStyleDeclaration} ref + * @param {T} value + * @param {string} name + * @returns {T} + */ +const direct = (ref, value, name) => (ref[name] = value); + +/** + * @template T + * @param {Element} element + * @param {T} value + * @param {string} name + * @returns {T} + */ +const dot = (element, value, name) => direct(element, value, name.slice(1)); + +/** + * @template T + * @param {Element} element + * @param {T} value + * @returns {T} + */ +const ref = (element, value) => { + if (typeof value === 'function') value(element); + else value.current = element; + return value; +}; + +/** + * @template T + * @param {Element} element + * @param {T} value + * @param {string} name + * @returns {T} + */ +const regular = (element, value, name) => { + if (value == null) element.removeAttribute(name); + else element.setAttribute(name, value); + return value; +}; + +/** + * @template T + * @param {Element} element + * @param {T} value + * @returns {T} + */ +const style = (element, value) => direct(element.style, value, 'cssText'); + +/** + * @template T + * @param {Element} element + * @param {T} value + * @param {string} name + * @returns {T} + */ +const toggle = (element, value, name) => { + element.toggleAttribute(name.slice(1), value); + return value; +}; + +/** + * @template T + * @param {Node} node + * @param {T} value + * @param {string} _ + * @param {Node[]} prev + * @returns {T} + */ +export const array = (node, value, _, prev) => { + if (value.length) + return udomdiff(node.parentNode, prev, value, diffFragment, node); + else if (prev.length) + drop(prev[0], prev.at(-1), false); + return empty; +}; + +/** + * @param {Element} element + * @param {string} name + * @returns + */ +export const attribute = (element, name) => { + switch (name[0]) { + case '.': return dot; + case '?': return toggle; + case '@': return at; + default: { + switch (name) { + case 'aria': return aria; + case 'class': return className; + case 'data': return data; + case 'ref': return ref; + case 'style': return style; + default: return name in element ? direct : regular; + } + } + } +}; + +/** + * @template T + * @param {Element} element + * @param {T} value + * @returns {T} + */ +export const text = (element, value) => { + element.texContent = value == null ? '' : value; + return value; +}; + +/** + * @template T + * @this {import("./literals.js").HoleDetails} + * @param {Node} node + * @param {T} value + * @returns {T} + */ +export function hole(node, value) { + const n = this.n || (this.n = node); + switch (typeof value) { + case 'string': + case 'number': + case 'boolean': { + if (n !== node) n.replaceWith((this.n = node)); + this.n.data = value; + break; + } + case 'object': + case 'undefined': { + n.replaceWith((this.n = value == null ? node : value.valueOf())); + break; + } + } + return value; +}; diff --git a/esm/handlers.js b/esm/handlers.js deleted file mode 100644 index 2c2e82b..0000000 --- a/esm/handlers.js +++ /dev/null @@ -1,148 +0,0 @@ -import {diffable} from '@webreflection/uwire'; - -import {aria, attribute, boolean, event, ref, setter, text} from 'uhandlers'; -import udomdiff from 'udomdiff'; - -import {isArray, createTextNode} from './utils.js'; - -// from a generic path, retrieves the exact targeted node -const reducePath = ({childNodes}, i) => childNodes[i]; - -// this helper avoid code bloat around handleAnything() callback -const diff = (comment, oldNodes, newNodes) => udomdiff( - comment.parentNode, - // TODO: there is a possible edge case where a node has been - // removed manually, or it was a keyed one, attached - // to a shared reference between renders. - // In this case udomdiff might fail at removing such node - // as its parent won't be the expected one. - // The best way to avoid this issue is to filter oldNodes - // in search of those not live, or not in the current parent - // anymore, but this would require both a change to uwire, - // exposing a parentNode from the firstChild, as example, - // but also a filter per each diff that should exclude nodes - // that are not in there, penalizing performance quite a lot. - // As this has been also a potential issue with domdiff, - // and both lighterhtml and hyperHTML might fail with this - // very specific edge case, I might as well document this possible - // "diffing shenanigan" and call it a day. - oldNodes, - newNodes, - diffable, - comment -); - -// if an interpolation represents a comment, the whole -// diffing will be related to such comment. -// This helper is in charge of understanding how the new -// content for such interpolation/hole should be updated -const handleAnything = comment => { - let oldValue, text, nodes = []; - const anyContent = newValue => { - switch (typeof newValue) { - // primitives are handled as text content - case 'string': - case 'number': - case 'boolean': - if (oldValue !== newValue) { - oldValue = newValue; - if (!text) - text = createTextNode(''); - text.data = newValue; - nodes = diff(comment, nodes, [text]); - } - break; - // null, and undefined are used to cleanup previous content - case 'object': - case 'undefined': - if (newValue == null) { - if (oldValue != newValue) { - oldValue = newValue; - nodes = diff(comment, nodes, []); - } - break; - } - // arrays and nodes have a special treatment - if (isArray(newValue)) { - oldValue = newValue; - // arrays can be used to cleanup, if empty - if (newValue.length === 0) - nodes = diff(comment, nodes, []); - // or diffed, if these contains nodes or "wires" - else if (typeof newValue[0] === 'object') - nodes = diff(comment, nodes, newValue); - // in all other cases the content is stringified as is - else - anyContent(String(newValue)); - break; - } - // if the new value is a DOM node, or a wire, and it's - // different from the one already live, then it's diffed. - // if the node is a fragment, it's appended once via its childNodes - // There is no `else` here, meaning if the content - // is not expected one, nothing happens, as easy as that. - if (oldValue !== newValue) { - if ('ELEMENT_NODE' in newValue) { - oldValue = newValue; - nodes = diff( - comment, - nodes, - newValue.nodeType === 11 ? - [...newValue.childNodes] : - [newValue] - ); - } - else { - const value = newValue.valueOf(); - if (value !== newValue) - anyContent(value); - } - } - break; - case 'function': - anyContent(newValue(comment)); - break; - } - }; - return anyContent; -}; - -// attributes can be: -// * ref=${...} for hooks and other purposes -// * aria=${...} for aria attributes -// * ?boolean=${...} for boolean attributes -// * .dataset=${...} for dataset related attributes -// * .setter=${...} for Custom Elements setters or nodes with setters -// such as buttons, details, options, select, etc -// * @event=${...} to explicitly handle event listeners -// * onevent=${...} to automatically handle event listeners -// * generic=${...} to handle an attribute just like an attribute -const handleAttribute = (node, name/*, svg*/) => { - switch (name[0]) { - case '?': return boolean(node, name.slice(1), false); - case '.': return setter(node, name.slice(1)); - case '@': return event(node, 'on' + name.slice(1)); - case 'o': if (name[1] === 'n') return event(node, name); - } - - switch (name) { - case 'ref': return ref(node); - case 'aria': return aria(node); - } - - return attribute(node, name/*, svg*/); -}; - -// each mapped update carries the update type and its path -// the type is either node, attribute, or text, while -// the path is how to retrieve the related node to update. -// In the attribute case, the attribute name is also carried along. -export function handlers(options) { - const {type, path} = options; - const node = path.reduceRight(reducePath, this); - return type === 'node' ? - handleAnything(node) : - (type === 'attr' ? - handleAttribute(node, options.name/*, options.svg*/) : - text(node)); -}; diff --git a/esm/index.js b/esm/index.js index 5728fcd..8dda2c2 100644 --- a/esm/index.js +++ b/esm/index.js @@ -1,63 +1,16 @@ -import {MapSet, WeakMapSet} from '@webreflection/mapset'; +/*! (c) Andrea Giammarchi - MIT */ -import {Hole, createCache, unroll} from './rabbit.js'; +import { Hole } from './rabbit.js'; +import render from './render-hole.js'; -// both `html` and `svg` template literal tags are polluted -// with a `for(ref[, id])` and a `node` tag too -const tag = type => { - // both `html` and `svg` tags have their own cache - const keyed = new WeakMapSet; - // keyed operations always re-use the same cache and unroll - // the template and its interpolations right away - const fixed = cache => (template, ...values) => unroll( - cache, - {type, template, values} - ); - return Object.assign( - // non keyed operations are recognized as instance of Hole - // during the "unroll", recursively resolved and updated - (template, ...values) => new Hole(type, template, values), - { - // keyed operations need a reference object, usually the parent node - // which is showing keyed results, and optionally a unique id per each - // related node, handy with JSON results and mutable list of objects - // that usually carry a unique identifier - for(ref, id) { - const memo = keyed.get(ref) || keyed.set(ref, new MapSet); - return memo.get(id) || memo.set(id, fixed(createCache())); - }, - // it is possible to create one-off content out of the box via node tag - // this might return the single created node, or a fragment with all - // nodes present at the root level and, of course, their child nodes - node: (template, ...values) => unroll(createCache(), new Hole(type, template, values)).valueOf() - } - ); -}; +/** @typedef {import("./literals.js").Value} Value */ -// each rendered node gets its own cache -const cache = new WeakMapSet; +const tag = svg => (template, ...values) => new Hole(svg, template, values); -// rendering means understanding what `html` or `svg` tags returned -// and it relates a specific node to its own unique cache. -// Each time the content to render changes, the node is cleaned up -// and the new new content is appended, and if such content is a Hole -// then it's "unrolled" to resolve all its inner nodes. -const render = (where, what) => { - const hole = typeof what === 'function' ? what() : what; - const info = cache.get(where) || cache.set(where, createCache()); - const wire = hole instanceof Hole ? unroll(info, hole) : hole; - if (wire !== info.wire) { - info.wire = wire; - // valueOf() simply returns the node itself, but in case it was a "wire" - // it will eventually re-append all nodes to its fragment so that such - // fragment can be re-appended many times in a meaningful way - // (wires are basically persistent fragments facades with special behavior) - where.replaceChildren(wire.valueOf()); - } - return where; -}; +/** @type {(template: TemplateStringsArray, ...values:Value[]) => Hole} A tag to render HTML content. */ +const html = tag(false); -const html = tag('html'); -const svg = tag('svg'); +/** @type {(template: TemplateStringsArray, ...values:Value[]) => Hole} A tag to render SVG content. */ +const svg = tag(true); -export {Hole, render, html, svg}; +export { Hole, render, html, svg }; diff --git a/esm/init.js b/esm/init.js deleted file mode 100644 index c9c47e0..0000000 --- a/esm/init.js +++ /dev/null @@ -1,628 +0,0 @@ -import {WeakMapSet} from '@webreflection/mapset'; -import instrument from '@webreflection/uparser'; - -import udomdiff from 'udomdiff'; - -export default ({document}) => { - /**start**/ -const {isArray, prototype} = Array; -const {indexOf} = prototype; - - - -const { - createDocumentFragment, - createElement, - createElementNS, - createTextNode, - createTreeWalker, - importNode -} = new Proxy({}, { - get: (_, method) => document[method].bind(document) -}); - - - -const createHTML = html => { - const template = createElement('template'); - template.innerHTML = html; - return template.content; -}; - -let xml; -const createSVG = svg => { - if (!xml) xml = createElementNS('http://www.w3.org/2000/svg', 'svg'); - xml.innerHTML = svg; - const content = createDocumentFragment(); - content.append(...xml.childNodes); - return content; -}; - -const createContent = (text, svg) => svg ? - createSVG(text) : createHTML(text); - -const ELEMENT_NODE = 1; -const nodeType = 111; - -const remove = ({firstChild, lastChild}) => { - const range = document.createRange(); - range.setStartAfter(firstChild); - range.setEndAfter(lastChild); - range.deleteContents(); - return firstChild; -}; - -const diffable = (node, operation) => node.nodeType === nodeType ? - ((1 / operation) < 0 ? - (operation ? remove(node) : node.lastChild) : - (operation ? node.valueOf() : node.firstChild)) : - node -; - -const persistent = fragment => { - const {firstChild, lastChild} = fragment; - if (firstChild === lastChild) - return lastChild || fragment; - const {childNodes} = fragment; - const nodes = [...childNodes]; - return { - ELEMENT_NODE, - nodeType, - firstChild, - lastChild, - valueOf() { - if (childNodes.length !== nodes.length) - fragment.append(...nodes); - return fragment; - } - }; -}; - - - -// flag for foreign checks (slower path, fast by default) -let useForeign = false; - -class Foreign { - constructor(handler, value) { - useForeign = true; - this._ = (...args) => handler(...args, value); - } -} - -const foreign = (handler, value) => new Foreign(handler, value); - -const aria = node => values => { - for (const key in values) { - const name = key === 'role' ? key : `aria-${key}`; - const value = values[key]; - if (value == null) - node.removeAttribute(name); - else - node.setAttribute(name, value); - } -}; - -const getValue = value => value == null ? value : value.valueOf(); - -const attribute = (node, name) => { - let oldValue, orphan = true; - const attributeNode = document.createAttributeNS(null, name); - return newValue => { - const value = useForeign && (newValue instanceof Foreign) ? - newValue._(node, name) : getValue(newValue); - if (oldValue !== value) { - if ((oldValue = value) == null) { - if (!orphan) { - node.removeAttributeNode(attributeNode); - orphan = true; - } - } - else { - attributeNode.value = value; - if (orphan) { - node.setAttributeNodeNS(attributeNode); - orphan = false; - } - } - } - }; -}; - -const boolean = (node, key, oldValue) => newValue => { - const value = !!getValue(newValue); - if (oldValue !== value) { - // when IE won't be around anymore ... - // node.toggleAttribute(key, oldValue = !!value); - if ((oldValue = value)) - node.setAttribute(key, ''); - else - node.removeAttribute(key); - } -}; - -const data = ({dataset}) => values => { - for (const key in values) { - const value = values[key]; - if (value == null) - delete dataset[key]; - else - dataset[key] = value; - } -}; - -const event = (node, name) => { - let oldValue, lower, type = name.slice(2); - if (!(name in node) && (lower = name.toLowerCase()) in node) - type = lower.slice(2); - return newValue => { - const info = isArray(newValue) ? newValue : [newValue, false]; - if (oldValue !== info[0]) { - if (oldValue) - node.removeEventListener(type, oldValue, info[1]); - if (oldValue = info[0]) - node.addEventListener(type, oldValue, info[1]); - } - }; -}; - -const ref = node => { - let oldValue; - return value => { - if (oldValue !== value) { - oldValue = value; - if (typeof value === 'function') - value(node); - else - value.current = node; - } - }; -}; - -const setter = (node, key) => key === 'dataset' ? - data(node) : - value => { - node[key] = value; - }; - -const text = node => { - let oldValue; - return newValue => { - const value = getValue(newValue); - if (oldValue != value) { - oldValue = value; - node.textContent = value == null ? '' : value; - } - }; -}; - - - - - - - - -// from a generic path, retrieves the exact targeted node -const reducePath = ({childNodes}, i) => childNodes[i]; - -// this helper avoid code bloat around handleAnything() callback -const diff = (comment, oldNodes, newNodes) => udomdiff( - comment.parentNode, - // TODO: there is a possible edge case where a node has been - // removed manually, or it was a keyed one, attached - // to a shared reference between renders. - // In this case udomdiff might fail at removing such node - // as its parent won't be the expected one. - // The best way to avoid this issue is to filter oldNodes - // in search of those not live, or not in the current parent - // anymore, but this would require both a change to uwire, - // exposing a parentNode from the firstChild, as example, - // but also a filter per each diff that should exclude nodes - // that are not in there, penalizing performance quite a lot. - // As this has been also a potential issue with domdiff, - // and both lighterhtml and hyperHTML might fail with this - // very specific edge case, I might as well document this possible - // "diffing shenanigan" and call it a day. - oldNodes, - newNodes, - diffable, - comment -); - -// if an interpolation represents a comment, the whole -// diffing will be related to such comment. -// This helper is in charge of understanding how the new -// content for such interpolation/hole should be updated -const handleAnything = comment => { - let oldValue, text, nodes = []; - const anyContent = newValue => { - switch (typeof newValue) { - // primitives are handled as text content - case 'string': - case 'number': - case 'boolean': - if (oldValue !== newValue) { - oldValue = newValue; - if (!text) - text = createTextNode(''); - text.data = newValue; - nodes = diff(comment, nodes, [text]); - } - break; - // null, and undefined are used to cleanup previous content - case 'object': - case 'undefined': - if (newValue == null) { - if (oldValue != newValue) { - oldValue = newValue; - nodes = diff(comment, nodes, []); - } - break; - } - // arrays and nodes have a special treatment - if (isArray(newValue)) { - oldValue = newValue; - // arrays can be used to cleanup, if empty - if (newValue.length === 0) - nodes = diff(comment, nodes, []); - // or diffed, if these contains nodes or "wires" - else if (typeof newValue[0] === 'object') - nodes = diff(comment, nodes, newValue); - // in all other cases the content is stringified as is - else - anyContent(String(newValue)); - break; - } - // if the new value is a DOM node, or a wire, and it's - // different from the one already live, then it's diffed. - // if the node is a fragment, it's appended once via its childNodes - // There is no `else` here, meaning if the content - // is not expected one, nothing happens, as easy as that. - if (oldValue !== newValue) { - if ('ELEMENT_NODE' in newValue) { - oldValue = newValue; - nodes = diff( - comment, - nodes, - newValue.nodeType === 11 ? - [...newValue.childNodes] : - [newValue] - ); - } - else { - const value = newValue.valueOf(); - if (value !== newValue) - anyContent(value); - } - } - break; - case 'function': - anyContent(newValue(comment)); - break; - } - }; - return anyContent; -}; - -// attributes can be: -// * ref=${...} for hooks and other purposes -// * aria=${...} for aria attributes -// * ?boolean=${...} for boolean attributes -// * .dataset=${...} for dataset related attributes -// * .setter=${...} for Custom Elements setters or nodes with setters -// such as buttons, details, options, select, etc -// * @event=${...} to explicitly handle event listeners -// * onevent=${...} to automatically handle event listeners -// * generic=${...} to handle an attribute just like an attribute -const handleAttribute = (node, name/*, svg*/) => { - switch (name[0]) { - case '?': return boolean(node, name.slice(1), false); - case '.': return setter(node, name.slice(1)); - case '@': return event(node, 'on' + name.slice(1)); - case 'o': if (name[1] === 'n') return event(node, name); - } - - switch (name) { - case 'ref': return ref(node); - case 'aria': return aria(node); - } - - return attribute(node, name/*, svg*/); -}; - -// each mapped update carries the update type and its path -// the type is either node, attribute, or text, while -// the path is how to retrieve the related node to update. -// In the attribute case, the attribute name is also carried along. -function handlers(options) { - const {type, path} = options; - const node = path.reduceRight(reducePath, this); - return type === 'node' ? - handleAnything(node) : - (type === 'attr' ? - handleAttribute(node, options.name/*, options.svg*/) : - text(node)); -}; - - - - - - - - -// from a fragment container, create an array of indexes -// related to its child nodes, so that it's possible -// to retrieve later on exact node via reducePath -const createPath = node => { - const path = []; - let {parentNode} = node; - while (parentNode) { - path.push(indexOf.call(parentNode.childNodes, node)); - node = parentNode; - ({parentNode} = node); - } - return path; -}; - -// the prefix is used to identify either comments, attributes, or nodes -// that contain the related unique id. In the attribute cases -// isĀµX="attribute-name" will be used to map current X update to that -// attribute name, while comments will be like , to map -// the update to that specific comment node, hence its parent. -// style and textarea will have text content, and are handled -// directly through text-only updates. -const prefix = 'isĀµ'; - -// Template Literals are unique per scope and static, meaning a template -// should be parsed once, and once only, as it will always represent the same -// content, within the exact same amount of updates each time. -// This cache relates each template to its unique content and updates. -const cache = new WeakMapSet; - -// a RegExp that helps checking nodes that cannot contain comments -const textOnly = /^(?:textarea|script|style|title|plaintext|xmp)$/; - -const createCache = () => ({ - stack: [], // each template gets a stack for each interpolation "hole" - - entry: null, // each entry contains details, such as: - // * the template that is representing - // * the type of node it represents (html or svg) - // * the content fragment with all nodes - // * the list of updates per each node (template holes) - // * the "wired" node or fragment that will get updates - // if the template or type are different from the previous one - // the entry gets re-created each time - - wire: null // each rendered node represent some wired content and - // this reference to the latest one. If different, the node - // will be cleaned up and the new "wire" will be appended -}); - -// the entry stored in the rendered node cache, and per each "hole" -const createEntry = (type, template) => { - const {content, updates} = mapUpdates(type, template); - return {type, template, content, updates, wire: null}; -}; - -// a template is instrumented to be able to retrieve where updates are needed. -// Each unique template becomes a fragment, cloned once per each other -// operation based on the same template, i.e. data => html`

${data}

` -const mapTemplate = (type, template) => { - const svg = type === 'svg'; - const text = instrument(template, prefix, svg); - const content = createContent(text, svg); - // once instrumented and reproduced as fragment, it's crawled - // to find out where each update is in the fragment tree - const tw = createTreeWalker(content, 1 | 128); - const nodes = []; - const length = template.length - 1; - let i = 0; - // updates are searched via unique names, linearly increased across the tree - //
- let search = `${prefix}${i}`; - while (i < length) { - const node = tw.nextNode(); - // if not all updates are bound but there's nothing else to crawl - // it means that there is something wrong with the template. - if (!node) - throw `bad template: ${text}`; - // if the current node is a comment, and it contains isĀµX - // it means the update should take care of any content - if (node.nodeType === 8) { - // The only comments to be considered are those - // which content is exactly the same as the searched one. - if (node.data === search) { - nodes.push({type: 'node', path: createPath(node)}); - search = `${prefix}${++i}`; - } - } - else { - // if the node is not a comment, loop through all its attributes - // named isĀµX and relate attribute updates to this node and the - // attribute name, retrieved through node.getAttribute("isĀµX") - // the isĀµX attribute will be removed as irrelevant for the layout - // let svg = -1; - while (node.hasAttribute(search)) { - nodes.push({ - type: 'attr', - path: createPath(node), - name: node.getAttribute(search) - }); - node.removeAttribute(search); - search = `${prefix}${++i}`; - } - // if the node was a style, textarea, or others, check its content - // and if it is then update tex-only this node - if ( - textOnly.test(node.localName) && - node.textContent.trim() === `` - ){ - node.textContent = ''; - nodes.push({type: 'text', path: createPath(node)}); - search = `${prefix}${++i}`; - } - } - } - // once all nodes to update, or their attributes, are known, the content - // will be cloned in the future to represent the template, and all updates - // related to such content retrieved right away without needing to re-crawl - // the exact same template, and its content, more than once. - return {content, nodes}; -}; - -// if a template is unknown, perform the previous mapping, otherwise grab -// its details such as the fragment with all nodes, and updates info. -const mapUpdates = (type, template) => { - const {content, nodes} = ( - cache.get(template) || - cache.set(template, mapTemplate(type, template)) - ); - // clone deeply the fragment - const fragment = importNode(content, true); - // and relate an update handler per each node that needs one - const updates = nodes.map(handlers, fragment); - // return the fragment and all updates to use within its nodes - return {content: fragment, updates}; -}; - -// as html and svg can be nested calls, but no parent node is known -// until rendered somewhere, the unroll operation is needed to -// discover what to do with each interpolation, which will result -// into an update operation. -const unroll = (info, {type, template, values}) => { - // interpolations can contain holes and arrays, so these need - // to be recursively discovered - const length = unrollValues(info, values); - let {entry} = info; - // if the cache entry is either null or different from the template - // and the type this unroll should resolve, create a new entry - // assigning a new content fragment and the list of updates. - if (!entry || (entry.template !== template || entry.type !== type)) - info.entry = (entry = createEntry(type, template)); - const {content, updates, wire} = entry; - // even if the fragment and its nodes is not live yet, - // it is already possible to update via interpolations values. - for (let i = 0; i < length; i++) - updates[i](values[i]); - // if the entry was new, or representing a different template or type, - // create a new persistent entity to use during diffing. - // This is simply a DOM node, when the template has a single container, - // as in `

`, or a "wire" in `

` and similar cases. - return wire || (entry.wire = persistent(content)); -}; - -// the stack retains, per each interpolation value, the cache -// related to each interpolation value, or null, if the render -// was conditional and the value is not special (Array or Hole) -const unrollValues = ({stack}, values) => { - const {length} = values; - for (let i = 0; i < length; i++) { - const hole = values[i]; - // each Hole gets unrolled and re-assigned as value - // so that domdiff will deal with a node/wire, not with a hole - if (hole instanceof Hole) - values[i] = unroll( - stack[i] || (stack[i] = createCache()), - hole - ); - // arrays are recursively resolved so that each entry will contain - // also a DOM node or a wire, hence it can be diffed if/when needed - else if (isArray(hole)) - unrollValues(stack[i] || (stack[i] = createCache()), hole); - // if the value is nothing special, the stack doesn't need to retain data - // this is useful also to cleanup previously retained data, if the value - // was a Hole, or an Array, but not anymore, i.e.: - // const update = content => html`
${content}
`; - // update(listOfItems); update(null); update(html`hole`) - else - stack[i] = null; - } - if (length < stack.length) - stack.splice(length); - return length; -}; - -/** - * Holds all details wrappers needed to render the content further on. - * @constructor - * @param {string} type The hole type, either `html` or `svg`. - * @param {string[]} template The template literals used to the define the content. - * @param {Array} values Zero, one, or more interpolated values to render. - */ -class Hole { - constructor(type, template, values) { - this.type = type; - this.template = template; - this.values = values; - } -}; - - - - - -// both `html` and `svg` template literal tags are polluted -// with a `for(ref[, id])` and a `node` tag too -const tag = type => { - // both `html` and `svg` tags have their own _cache - const keyed = new WeakMapSet; - // keyed operations always re-use the same _cache and unroll - // the template and its interpolations right away - const fixed = _cache => (template, ...values) => unroll( - _cache, - {type, template, values} - ); - return Object.assign( - // non keyed operations are recognized as instance of Hole - // during the "unroll", recursively resolved and updated - (template, ...values) => new Hole(type, template, values), - { - // keyed operations need a reference object, usually the parent node - // which is showing keyed results, and optionally a unique id per each - // related node, handy with JSON results and mutable list of objects - // that usually carry a unique identifier - for(ref, id) { - const memo = keyed.get(ref) || keyed.set(ref, new MapSet); - return memo.get(id) || memo.set(id, fixed(createCache())); - }, - // it is possible to create one-off content out of the box via node tag - // this might return the single created node, or a fragment with all - // nodes present at the root level and, of course, their child nodes - node: (template, ...values) => unroll(createCache(), new Hole(type, template, values)).valueOf() - } - ); -}; - -// each rendered node gets its own _cache -const _cache = new WeakMapSet; - -// rendering means understanding what `html` or `svg` tags returned -// and it relates a specific node to its own unique _cache. -// Each time the content to render changes, the node is cleaned up -// and the new new content is appended, and if such content is a Hole -// then it's "unrolled" to resolve all its inner nodes. -const render = (where, what) => { - const hole = typeof what === 'function' ? what() : what; - const info = _cache.get(where) || _cache.set(where, createCache()); - const wire = hole instanceof Hole ? unroll(info, hole) : hole; - if (wire !== info.wire) { - info.wire = wire; - // valueOf() simply returns the node itself, but in case it was a "wire" - // it will eventually re-append all nodes to its fragment so that such - // fragment can be re-appended many times in a meaningful way - // (wires are basically persistent fragments facades with special behavior) - where.replaceChildren(wire.valueOf()); - } - return where; -}; - -const html = tag('html'); -const svg = tag('svg'); - -return {Hole, render, html, svg}; - -/**end**/ -}; diff --git a/esm/json.js b/esm/json.js deleted file mode 100644 index 58b033e..0000000 --- a/esm/json.js +++ /dev/null @@ -1,49 +0,0 @@ -import {MapSet, WeakMapSet} from '@webreflection/mapset'; - -import {render as $render, html, svg} from './index.js'; - -// Sender (SW, Worker, postMessage) -const ids = new WeakMapSet; -let id = 0; - -const tag = type => (template, ...values) => ({ - id: ids.get(template) || - ids.set(template, id++), - type, - values, - template -}); - -html.json = tag('html'); -svg.json = tag('svg'); - -// Receiver (onmessage, from SW, Workers, etc) -const templates = new MapSet; - -const unroll = ({type, template, values, id}) => ( - (type === 'svg' ? svg : html).apply( - null, - [ - templates.get(id) || - templates.set(id, template) - ].concat(values.map(asJSON)) - ) -); - -const asJSON = value => isJSON(value) ? unroll(value) : value; - -const isJSON = thing => ( - typeof thing === 'object' && - thing !== null && - 'id' in thing && - 'type' in thing && - 'values' in thing && - 'template' in thing -); - -const render = (where, what) => $render( - where, - isJSON(what) ? unroll(what) : what -); - -export {render, html, svg}; diff --git a/esm/keyed.js b/esm/keyed.js new file mode 100644 index 0000000..5cc5c2c --- /dev/null +++ b/esm/keyed.js @@ -0,0 +1,39 @@ +/*! (c) Andrea Giammarchi - MIT */ + +import { cache } from './literals.js'; +import { Hole, unroll } from './rabbit.js'; +import { empty, set } from './utils.js'; +import { html, svg } from './index.js'; +import render from './render-any.js'; + +/** @typedef {import("./literals.js").Cache} Cache */ +/** @typedef {import("./literals.js").Target} Target */ +/** @typedef {import("./literals.js").Value} Value */ + +/** @typedef {(ref:Object, key:string | number) => Tag} Bound */ + +/** + * @callback Tag + * @param {TemplateStringsArray} template + * @param {...Value} values + * @returns {Target} + */ + +const keyed = new WeakMap; +const createRef = svg => /** @type {Bound} */ (ref, key) => { + /** @type {Tag} */ + function tag(template, ...values) { + return unroll(this, new Hole(svg, template, values)); + } + + const memo = keyed.get(ref) || set(keyed, ref, new Map); + return memo.get(key) || set(memo, key, tag.bind(cache(empty))); +}; + +/** @type {Bound} Returns a bound tag to render HTML content. */ +const htmlFor = createRef(false); + +/** @type {Bound} Returns a bound tag to render SVG content. */ +const svgFor = createRef(true); + +export { Hole, render, html, svg, htmlFor, svgFor }; diff --git a/esm/literals.js b/esm/literals.js new file mode 100644 index 0000000..07a7d4e --- /dev/null +++ b/esm/literals.js @@ -0,0 +1,90 @@ +import { empty } from './utils.js'; + +/** @typedef {import("domconstants/constants").ATTRIBUTE_NODE} ATTRIBUTE_NODE */ +/** @typedef {import("domconstants/constants").TEXT_NODE} TEXT_NODE */ +/** @typedef {import("domconstants/constants").COMMENT_NODE} COMMENT_NODE */ +/** @typedef {ATTRIBUTE_NODE | TEXT_NODE | COMMENT_NODE} Type */ + +/** @typedef {import("./persistent-fragment.js").PersistentFragment} PersistentFragment */ +/** @typedef {import("./rabbit.js").Hole} Hole */ + +/** @typedef {Node | Element | PersistentFragment} Target */ +/** @typedef {null | undefined | string | number | boolean | Hole} Value */ +/** @typedef {null | undefined | string | number | boolean | Node | Element | PersistentFragment} DOMValue */ + +/** + * @typedef {Object} Entry + * @property {Type} type + * @property {number[]} path + * @property {function} update + * @property {string} name + */ + +/** + * @param {PersistentFragment} c content retrieved from the template + * @param {Entry[]} e entries per each hole in the template + * @param {number} l the length of content childNodes + * @returns + */ +export const cel = (c, e, l) => ({ c, e, l }); + +/** + * @typedef {Object} HoleDetails + * @property {null | Node | PersistentFragment} n the current live node, if any and not the `t` one + */ + +/** @type {() => HoleDetails} */ +export const comment = () => ({ n: null }); + +/** + * @typedef {Object} Detail + * @property {any} v the current value of the interpolation / hole + * @property {function} u the callback to update the value + * @property {Node} t the target comment node or element + * @property {string} n the name of the attribute, if any + */ + +/** + * @param {any} v the current value of the interpolation / hole + * @param {function} u the callback to update the value + * @param {Node} t the target comment node or element + * @param {string} n the name of the attribute, if any + * @returns {Detail} + */ +export const detail = (v, u, t, n) => ({ v, u, t, n }); + +/** + * @param {Type} t the operation type + * @param {number[]} p the path to retrieve the node + * @param {function} u the update function + * @param {string} n the attribute name, if any + * @returns {Entry} + */ +export const entry = (t, p, u, n = '') => ({ t, p, u, n }); + +/** + * @typedef {Object} Cache + * @property {Cache[]} s the stack of caches per each interpolation / hole + * @property {null | TemplateStringsArray} t the cached template + * @property {null | Node | PersistentFragment} n the node returned when parsing the template + * @property {Detail[]} d the list of updates to perform + */ + +/** + * @param {Cache[]} s the cache stack + * @returns {Cache} + */ +export const cache = s => ({ s, t: null, n: null, d: empty}); + +/** + * @typedef {Object} Parsed + * @property {Node | PersistentFragment} n the returned node after parsing the template + * @property {Detail[]} d the list of details to update the node + */ + +/** + * @param {Node | PersistentFragment} n the returned node after parsing the template + * @param {Detail[]} d the list of details to update the node + * @returns {Parsed} + */ +export const parsed = (n, d) => ({ n, d }); diff --git a/esm/node.js b/esm/node.js new file mode 100644 index 0000000..0d55594 --- /dev/null +++ b/esm/node.js @@ -0,0 +1,21 @@ +/*! (c) Andrea Giammarchi - MIT */ + +import create from './creator.js'; +import parser from './parser.js'; +import render from './render-node.js'; + +/** @typedef {import("./literals.js").DOMValue} DOMValue */ +/** @typedef {import("./literals.js").Target} Target */ + +const tag = svg => { + const parse = create(parser(svg)); + return (template, ...values) => parse(template, values).n; +}; + +/** @type {(template: TemplateStringsArray, ...values:DOMValue[]) => Target} A tag to render HTML content. */ +const html = tag(false); + +/** @type {(template: TemplateStringsArray, ...values:DOMValue[]) => Target} A tag to render SVG content. */ +const svg = tag(true); + +export { render, html, svg }; diff --git a/esm/parser.js b/esm/parser.js new file mode 100644 index 0000000..373dce1 --- /dev/null +++ b/esm/parser.js @@ -0,0 +1,94 @@ +import { ATTRIBUTE_NODE, TEXT_NODE, COMMENT_NODE } from 'domconstants/constants'; +import { TEXT_ELEMENTS } from 'domconstants/re'; +import parser from '@webreflection/uparser'; + +import { empty, isArray, set } from './utils.js'; +import { cel, comment, entry } from './literals.js'; + +import { array, attribute, text, hole } from './handler.js'; +import createContent from './create-content.js'; + +/** @typedef {import("./literals.js").Entry} Entry */ + +/** + * @typedef {Object} Resolved + * @property {DocumentFragment} content + * @property {Entry[]} entries + * @property {function[]} updates + * @property {number} length + */ + +/** + * @param {Element} node + * @returns {number[]} + */ +const createPath = node => { + const path = []; + let parentNode; + while ((parentNode = node.parentNode)) { + path.push(path.indexOf.call(parentNode.childNodes, node)); + node = parentNode; + } + return path; +}; + +const boundComment = () => hole.bind(comment()); +const arrayComment = () => array; + +/** + * @param {TemplateStringsArray} template + * @param {boolean} xml + * @returns {Resolved} + */ +const resolve = (template, values, xml) => { + const content = createContent(parser(template, prefix, xml), xml); + let entries = empty; + const { length } = template; + if (length > 1) { + const tw = document.createTreeWalker(content, 1 | 128); + const replace = []; + let i = 0, search = `${prefix}${i++}`; + entries = []; + while (i < length) { + const node = tw.nextNode(); + if (node.nodeType === COMMENT_NODE) { + if (node.data === search) { + let update = isArray(values[i - 1]) ? arrayComment : boundComment; + if (update === boundComment) replace.push(node); + entries.push(entry(COMMENT_NODE, createPath(node), update)); + search = `${prefix}${i++}`; + } + } + else { + let path; + while (node.hasAttribute(search)) { + if (!path) path = createPath(node); + const name = node.getAttribute(search); + entries.push(entry(ATTRIBUTE_NODE, path, attribute(node, name), name)); + node.removeAttribute(search); + search = `${prefix}${i++}`; + } + if ( + TEXT_ELEMENTS.test(node.localName) && + node.textContent.trim() === `` + ) { + entries.push(entry(TEXT_NODE, path || createPath(node), text)); + search = `${prefix}${i++}`; + } + } + } + for (i = 0; i < replace.length; i++) + replace[i].replaceWith(document.createTextNode('')); + } + return set(cache, template, cel(content, entries, content.childNodes.length)); +}; + +/** @type {WeakMap} */ +const cache = new WeakMap; +const prefix = 'isĀµ'; + +/** + * @param {boolean} xml + * @returns {(template: TemplateStringsArray, values: any[]) => Resolved} + */ +export default xml => (template, values) => cache.get(template) || resolve(template, values, xml); diff --git a/esm/persistent-fragment.js b/esm/persistent-fragment.js new file mode 100644 index 0000000..7856f8e --- /dev/null +++ b/esm/persistent-fragment.js @@ -0,0 +1,46 @@ +import { DOCUMENT_FRAGMENT_NODE } from 'domconstants/constants'; +import custom from 'custom-function/factory'; +import drop from './range.js'; + +/** + * @param {PersistentFragment} fragment + * @returns {Node | Element} + */ +const remove = ({firstChild, lastChild}) => drop(firstChild, lastChild, true); + +let checkType = false; + +/** + * @param {Node} node + * @param {1 | 0 | -0 | -1} operation + * @returns {Node} + */ +export const diffFragment = (node, operation) => ( + checkType && node.nodeType === DOCUMENT_FRAGMENT_NODE ? + ((1 / operation) < 0 ? + (operation ? remove(node) : node.lastChild) : + (operation ? node.valueOf() : node.firstChild)) : + node +); + +/** @extends {DocumentFragment} */ +export class PersistentFragment extends custom(DocumentFragment) { + #nodes; + #length; + constructor(fragment) { + super(fragment); + this.#nodes = [...fragment.childNodes]; + this.#length = this.#nodes.length; + checkType = true; + } + get firstChild() { return this.#nodes[0]; } + get lastChild() { return this.#nodes.at(-1); } + replaceWith(node) { + remove(this).replaceWith(node); + } + valueOf() { + if (this.childNodes.length !== this.#length) + this.append(...this.#nodes); + return this; + } +} diff --git a/esm/rabbit.js b/esm/rabbit.js index 4966a5b..f790952 100644 --- a/esm/rabbit.js +++ b/esm/rabbit.js @@ -1,214 +1,71 @@ -import {WeakMapSet} from '@webreflection/mapset'; -import instrument from '@webreflection/uparser'; -import {persistent} from '@webreflection/uwire'; +import { cache } from './literals.js'; +import { empty, isArray } from './utils.js'; +import create from './creator.js'; +import parser from './parser.js'; -import {handlers} from './handlers.js'; -import {isArray, indexOf, createContent, createTreeWalker, importNode} from './utils.js'; +const parseHTML = create(parser(false)); +const parseSVG = create(parser(true)); -// from a fragment container, create an array of indexes -// related to its child nodes, so that it's possible -// to retrieve later on exact node via reducePath -const createPath = node => { - const path = []; - let {parentNode} = node; - while (parentNode) { - path.push(indexOf.call(parentNode.childNodes, node)); - node = parentNode; - ({parentNode} = node); +/** + * @param {import("./literals.js").Cache} cache + * @param {Hole} hole + * @returns {Node} + */ +export const unroll = (cache, { s: stack, t: template, v: values }) => { + if (values.length && cache.s === empty) cache.s = []; + unrollValues(cache, values); + if (cache.t !== template) { + const { n: node, d: details } = (stack ? parseSVG : parseHTML)(template, values); + cache.t = template; + cache.n = node; + cache.d = details; } - return path; -}; - -// the prefix is used to identify either comments, attributes, or nodes -// that contain the related unique id. In the attribute cases -// isĀµX="attribute-name" will be used to map current X update to that -// attribute name, while comments will be like , to map -// the update to that specific comment node, hence its parent. -// style and textarea will have text content, and are handled -// directly through text-only updates. -const prefix = 'isĀµ'; - -// Template Literals are unique per scope and static, meaning a template -// should be parsed once, and once only, as it will always represent the same -// content, within the exact same amount of updates each time. -// This cache relates each template to its unique content and updates. -const cache = new WeakMapSet; - -// a RegExp that helps checking nodes that cannot contain comments -const textOnly = /^(?:textarea|script|style|title|plaintext|xmp)$/; - -export const createCache = () => ({ - stack: [], // each template gets a stack for each interpolation "hole" - - entry: null, // each entry contains details, such as: - // * the template that is representing - // * the type of node it represents (html or svg) - // * the content fragment with all nodes - // * the list of updates per each node (template holes) - // * the "wired" node or fragment that will get updates - // if the template or type are different from the previous one - // the entry gets re-created each time - - wire: null // each rendered node represent some wired content and - // this reference to the latest one. If different, the node - // will be cleaned up and the new "wire" will be appended -}); - -// the entry stored in the rendered node cache, and per each "hole" -const createEntry = (type, template) => { - const {content, updates} = mapUpdates(type, template); - return {type, template, content, updates, wire: null}; -}; - -// a template is instrumented to be able to retrieve where updates are needed. -// Each unique template becomes a fragment, cloned once per each other -// operation based on the same template, i.e. data => html`

${data}

` -const mapTemplate = (type, template) => { - const svg = type === 'svg'; - const text = instrument(template, prefix, svg); - const content = createContent(text, svg); - // once instrumented and reproduced as fragment, it's crawled - // to find out where each update is in the fragment tree - const tw = createTreeWalker(content, 1 | 128); - const nodes = []; - const length = template.length - 1; - let i = 0; - // updates are searched via unique names, linearly increased across the tree - //
- let search = `${prefix}${i}`; - while (i < length) { - const node = tw.nextNode(); - // if not all updates are bound but there's nothing else to crawl - // it means that there is something wrong with the template. - if (!node) - throw `bad template: ${text}`; - // if the current node is a comment, and it contains isĀµX - // it means the update should take care of any content - if (node.nodeType === 8) { - // The only comments to be considered are those - // which content is exactly the same as the searched one. - if (node.data === search) { - nodes.push({type: 'node', path: createPath(node)}); - search = `${prefix}${++i}`; - } - } - else { - // if the node is not a comment, loop through all its attributes - // named isĀµX and relate attribute updates to this node and the - // attribute name, retrieved through node.getAttribute("isĀµX") - // the isĀµX attribute will be removed as irrelevant for the layout - // let svg = -1; - while (node.hasAttribute(search)) { - nodes.push({ - type: 'attr', - path: createPath(node), - name: node.getAttribute(search) - }); - node.removeAttribute(search); - search = `${prefix}${++i}`; - } - // if the node was a style, textarea, or others, check its content - // and if it is then update tex-only this node - if ( - textOnly.test(node.localName) && - node.textContent.trim() === `` - ){ - node.textContent = ''; - nodes.push({type: 'text', path: createPath(node)}); - search = `${prefix}${++i}`; + else { + const { d: details } = cache; + for (let i = 0; i < details.length; i++) { + const detail = details[i]; + const value = values[i]; + const { v: previous } = detail; + if (value !== previous) { + const { u: update, t: target, n: name } = detail; + detail.v = update(target, value, name, previous); } } } - // once all nodes to update, or their attributes, are known, the content - // will be cloned in the future to represent the template, and all updates - // related to such content retrieved right away without needing to re-crawl - // the exact same template, and its content, more than once. - return {content, nodes}; -}; - -// if a template is unknown, perform the previous mapping, otherwise grab -// its details such as the fragment with all nodes, and updates info. -const mapUpdates = (type, template) => { - const {content, nodes} = ( - cache.get(template) || - cache.set(template, mapTemplate(type, template)) - ); - // clone deeply the fragment - const fragment = importNode(content, true); - // and relate an update handler per each node that needs one - const updates = nodes.map(handlers, fragment); - // return the fragment and all updates to use within its nodes - return {content: fragment, updates}; -}; - -// as html and svg can be nested calls, but no parent node is known -// until rendered somewhere, the unroll operation is needed to -// discover what to do with each interpolation, which will result -// into an update operation. -export const unroll = (info, {type, template, values}) => { - // interpolations can contain holes and arrays, so these need - // to be recursively discovered - const length = unrollValues(info, values); - let {entry} = info; - // if the cache entry is either null or different from the template - // and the type this unroll should resolve, create a new entry - // assigning a new content fragment and the list of updates. - if (!entry || (entry.template !== template || entry.type !== type)) - info.entry = (entry = createEntry(type, template)); - const {content, updates, wire} = entry; - // even if the fragment and its nodes is not live yet, - // it is already possible to update via interpolations values. - for (let i = 0; i < length; i++) - updates[i](values[i]); - // if the entry was new, or representing a different template or type, - // create a new persistent entity to use during diffing. - // This is simply a DOM node, when the template has a single container, - // as in `

`, or a "wire" in `

` and similar cases. - return wire || (entry.wire = persistent(content)); + return cache.n; }; -// the stack retains, per each interpolation value, the cache -// related to each interpolation value, or null, if the render -// was conditional and the value is not special (Array or Hole) -const unrollValues = ({stack}, values) => { - const {length} = values; +/** + * @param {Cache} cache + * @param {any[]} values + * @returns {number} + */ +const unrollValues = ({ s: stack }, values) => { + const { length } = values; for (let i = 0; i < length; i++) { const hole = values[i]; - // each Hole gets unrolled and re-assigned as value - // so that domdiff will deal with a node/wire, not with a hole if (hole instanceof Hole) - values[i] = unroll( - stack[i] || (stack[i] = createCache()), - hole - ); - // arrays are recursively resolved so that each entry will contain - // also a DOM node or a wire, hence it can be diffed if/when needed + values[i] = unroll(stack[i] || (stack[i] = cache(empty)), hole); else if (isArray(hole)) - unrollValues(stack[i] || (stack[i] = createCache()), hole); - // if the value is nothing special, the stack doesn't need to retain data - // this is useful also to cleanup previously retained data, if the value - // was a Hole, or an Array, but not anymore, i.e.: - // const update = content => html`
${content}
`; - // update(listOfItems); update(null); update(html`hole`) + unrollValues(stack[i] || (stack[i] = cache([])), hole); else stack[i] = null; } - if (length < stack.length) - stack.splice(length); + if (length < stack.length) stack.splice(length); return length; }; /** - * Holds all details wrappers needed to render the content further on. + * Holds all details needed to render the content on a render. * @constructor - * @param {string} type The hole type, either `html` or `svg`. - * @param {string[]} template The template literals used to the define the content. - * @param {Array} values Zero, one, or more interpolated values to render. + * @param {boolean} svg The content type. + * @param {TemplateStringsArray} template The template literals used to the define the content. + * @param {any[]} values Zero, one, or more interpolated values to render. */ export class Hole { - constructor(type, template, values) { - this.type = type; - this.template = template; - this.values = values; + constructor(svg, template, values) { + this.s = svg; + this.t = template; + this.v = values; } }; diff --git a/esm/range.js b/esm/range.js new file mode 100644 index 0000000..56b469c --- /dev/null +++ b/esm/range.js @@ -0,0 +1,19 @@ +import { newRange } from './utils.js'; + +let range; +/** + * @param {Node | Element} firstChild + * @param {Node | Element} lastChild + * @param {boolean} preserve + * @returns + */ +export default (firstChild, lastChild, preserve) => { + if (!range) range = newRange(); + if (preserve) + range.setStartAfter(firstChild); + else + range.setStartBefore(firstChild); + range.setEndAfter(lastChild); + range.deleteContents(); + return firstChild; +}; diff --git a/esm/render-any.js b/esm/render-any.js new file mode 100644 index 0000000..2044321 --- /dev/null +++ b/esm/render-any.js @@ -0,0 +1,25 @@ +/*! (c) Andrea Giammarchi - MIT */ + +import { cache } from './literals.js'; +import { Hole, unroll } from './rabbit.js'; +import { empty, set } from './utils.js'; + +/** @type {WeakMap} */ +const known = new WeakMap; + +/** + * Render with smart updates within a generic container. + * @template T + * @param {T} where the DOM node where to render content + * @param {() => Hole | Hole} what the hole to render + * @returns + */ +export default (where, what) => { + const info = known.get(where) || set(known, where, cache(empty)); + const hole = typeof what === 'function' ? what() : what; + const { n } = info; + const node = hole instanceof Hole ? unroll(info, hole) : hole; + if (n !== node) + where.replaceChildren((info.n = node)); + return where; +}; diff --git a/esm/render-hole.js b/esm/render-hole.js new file mode 100644 index 0000000..0a9e4d8 --- /dev/null +++ b/esm/render-hole.js @@ -0,0 +1,24 @@ +/*! (c) Andrea Giammarchi - MIT */ + +import { cache } from './literals.js'; +import { unroll } from './rabbit.js'; +import { empty, set } from './utils.js'; + +/** @typedef {import("./rabbit.js").Hole} Hole */ + +/** @type {WeakMap} */ +const known = new WeakMap; + +/** + * Render with smart updates within a generic container. + * @template T + * @param {T} where the DOM node where to render content + * @param {() => Hole | Hole} what the hole to render + * @returns + */ +export default (where, what) => { + const info = known.get(where) || set(known, where, cache(empty)); + if (info.n !== unroll(info, typeof what === 'function' ? what() : what)) + where.replaceChildren(info.n); + return where; +}; diff --git a/esm/render-node.js b/esm/render-node.js new file mode 100644 index 0000000..18a701c --- /dev/null +++ b/esm/render-node.js @@ -0,0 +1,13 @@ +/** @typedef {import("./literals.js").Target} Target */ + +/** + * Render directly within a generic container. + * @template T + * @param {T} where the DOM node where to render content + * @param {() => Target | Target} what the node to render + * @returns + */ +export default (where, what) => { + where.replaceChildren(typeof what === 'function' ? what() : what); + return where; +}; diff --git a/esm/utils.js b/esm/utils.js index c1145db..2225d38 100644 --- a/esm/utils.js +++ b/esm/utils.js @@ -1,35 +1,19 @@ -const {isArray, prototype} = Array; -const {indexOf} = prototype; +const { isArray } = Array; +export { isArray }; -export {isArray, indexOf}; +export const empty = []; -const { - createDocumentFragment, - createElement, - createElementNS, - createTextNode, - createTreeWalker, - importNode -} = new Proxy({}, { - get: (_, method) => document[method].bind(document) -}); +export const newRange = () => document.createRange(); -export {createTextNode, createTreeWalker, importNode}; - -const createHTML = html => { - const template = createElement('template'); - template.innerHTML = html; - return template.content; -}; - -let xml; -const createSVG = svg => { - if (!xml) xml = createElementNS('http://www.w3.org/2000/svg', 'svg'); - xml.innerHTML = svg; - const content = createDocumentFragment(); - content.append(...xml.childNodes); - return content; +/** + * Set the `key` `value` pair to the *Map* or *WeakMap* and returns the `value` + * @template T + * @param {Map | WeakMap} map + * @param {any} key + * @param {T} value + * @returns {T} + */ +export const set = (map, key, value) => { + map.set(key, value); + return value; }; - -export const createContent = (text, svg) => svg ? - createSVG(text) : createHTML(text); diff --git a/esm/x.js b/esm/x.js deleted file mode 100644 index fc57b43..0000000 --- a/esm/x.js +++ /dev/null @@ -1,11 +0,0 @@ -import {createPragma} from 'jsx2tag'; -import {html} from './index.js'; - -const createElement = createPragma(html); -self.React = { - createElement, - Fragment: createElement -}; - -export * from 'jsx2tag'; -export * from './index.js'; diff --git a/index.d.ts b/index.d.ts deleted file mode 100644 index eaddd47..0000000 --- a/index.d.ts +++ /dev/null @@ -1,29 +0,0 @@ -export type TemplateFunction = ( - template: TemplateStringsArray, - ...values: any[] -) => T; - -export interface Tag extends TemplateFunction { - for(object: object, id?: string): TemplateFunction; - node: TemplateFunction; -} - -export type Renderable = Hole | HTMLElement | SVGElement; - -export declare const html: Tag; -export declare const svg: Tag; -export declare function render( - node: T, - renderer: (() => Renderable) | Renderable, -): T; - -/** - * Used for internal purposes, should be created using - * the `html` or `svg` template tags. - */ -export declare class Hole { - constructor(type: string, template: TemplateStringsArray, values: any[]); - readonly type: string; - readonly template: TemplateStringsArray; - readonly values: readonly any[]; -} diff --git a/index.js b/index.js index 12a00b0..b14366b 100644 --- a/index.js +++ b/index.js @@ -1,804 +1,3 @@ -self.uhtml = (function (exports) { - 'use strict'; - - class MapSet extends Map { - set(key, value) { - super.set(key, value); - return value; - } - } - - class WeakMapSet extends WeakMap { - set(key, value) { - super.set(key, value); - return value; - } - } - - /*! (c) Andrea Giammarchi - ISC */ - const empty = /^(?:area|base|br|col|embed|hr|img|input|keygen|link|menuitem|meta|param|source|track|wbr)$/i; - const elements = /<([a-z]+[a-z0-9:._-]*)([^>]*?)(\/?)>/g; - const attributes = /([^\s\\>"'=]+)\s*=\s*(['"]?)\x01/g; - const holes = /[\x01\x02]/g; - - // \x01 Node.ELEMENT_NODE - // \x02 Node.ATTRIBUTE_NODE - - /** - * Given a template, find holes as both nodes and attributes and - * return a string with holes as either comment nodes or named attributes. - * @param {string[]} template a template literal tag array - * @param {string} prefix prefix to use per each comment/attribute - * @param {boolean} svg enforces self-closing tags - * @returns {string} X/HTML with prefixed comments or attributes - */ - var instrument = (template, prefix, svg) => { - let i = 0; - return template - .join('\x01') - .trim() - .replace( - elements, - (_, name, attrs, selfClosing) => { - let ml = name + attrs.replace(attributes, '\x02=$2$1').trimEnd(); - if (selfClosing.length) - ml += (svg || empty.test(name)) ? ' /' : ('>'; - } - ) - .replace( - holes, - hole => hole === '\x01' ? - ('') : - (prefix + i++) - ); - }; - - const ELEMENT_NODE = 1; - const nodeType = 111; - - const remove = ({firstChild, lastChild}) => { - const range = document.createRange(); - range.setStartAfter(firstChild); - range.setEndAfter(lastChild); - range.deleteContents(); - return firstChild; - }; - - const diffable = (node, operation) => node.nodeType === nodeType ? - ((1 / operation) < 0 ? - (operation ? remove(node) : node.lastChild) : - (operation ? node.valueOf() : node.firstChild)) : - node - ; - - const persistent = fragment => { - const {firstChild, lastChild} = fragment; - if (firstChild === lastChild) - return lastChild || fragment; - const {childNodes} = fragment; - const nodes = [...childNodes]; - return { - ELEMENT_NODE, - nodeType, - firstChild, - lastChild, - valueOf() { - if (childNodes.length !== nodes.length) - fragment.append(...nodes); - return fragment; - } - }; - }; - - const {isArray: isArray$1} = Array; - - const aria = node => values => { - for (const key in values) { - const name = key === 'role' ? key : `aria-${key}`; - const value = values[key]; - if (value == null) - node.removeAttribute(name); - else - node.setAttribute(name, value); - } - }; - - const getValue = value => value == null ? value : value.valueOf(); - - const attribute = (node, name) => { - let oldValue, orphan = true; - const attributeNode = document.createAttributeNS(null, name); - return newValue => { - const value = getValue(newValue); - if (oldValue !== value) { - if ((oldValue = value) == null) { - if (!orphan) { - node.removeAttributeNode(attributeNode); - orphan = true; - } - } - else { - attributeNode.value = value; - if (orphan) { - node.setAttributeNodeNS(attributeNode); - orphan = false; - } - } - } - }; - }; - - const boolean = (node, key, oldValue) => newValue => { - const value = !!getValue(newValue); - if (oldValue !== value) { - // when IE won't be around anymore ... - // node.toggleAttribute(key, oldValue = !!value); - if ((oldValue = value)) - node.setAttribute(key, ''); - else - node.removeAttribute(key); - } - }; - - const data = ({dataset}) => values => { - for (const key in values) { - const value = values[key]; - if (value == null) - delete dataset[key]; - else - dataset[key] = value; - } - }; - - const event = (node, name) => { - let oldValue, lower, type = name.slice(2); - if (!(name in node) && (lower = name.toLowerCase()) in node) - type = lower.slice(2); - return newValue => { - const info = isArray$1(newValue) ? newValue : [newValue, false]; - if (oldValue !== info[0]) { - if (oldValue) - node.removeEventListener(type, oldValue, info[1]); - if (oldValue = info[0]) - node.addEventListener(type, oldValue, info[1]); - } - }; - }; - - const ref = node => { - let oldValue; - return value => { - if (oldValue !== value) { - oldValue = value; - if (typeof value === 'function') - value(node); - else - value.current = node; - } - }; - }; - - const setter = (node, key) => key === 'dataset' ? - data(node) : - value => { - node[key] = value; - }; - - const text = node => { - let oldValue; - return newValue => { - const value = getValue(newValue); - if (oldValue != value) { - oldValue = value; - node.textContent = value == null ? '' : value; - } - }; - }; - - /** - * ISC License - * - * Copyright (c) 2020, Andrea Giammarchi, @WebReflection - * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. - * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH - * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY - * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, - * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM - * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE - * OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR - * PERFORMANCE OF THIS SOFTWARE. - */ - - /** - * @param {Node} parentNode The container where children live - * @param {Node[]} a The list of current/live children - * @param {Node[]} b The list of future children - * @param {(entry: Node, action: number) => Node} get - * The callback invoked per each entry related DOM operation. - * @param {Node} [before] The optional node used as anchor to insert before. - * @returns {Node[]} The same list of future children. - */ - var udomdiff = (parentNode, a, b, get, before) => { - const bLength = b.length; - let aEnd = a.length; - let bEnd = bLength; - let aStart = 0; - let bStart = 0; - let map = null; - while (aStart < aEnd || bStart < bEnd) { - // append head, tail, or nodes in between: fast path - if (aEnd === aStart) { - // we could be in a situation where the rest of nodes that - // need to be added are not at the end, and in such case - // the node to `insertBefore`, if the index is more than 0 - // must be retrieved, otherwise it's gonna be the first item. - const node = bEnd < bLength ? - (bStart ? - (get(b[bStart - 1], -0).nextSibling) : - get(b[bEnd - bStart], 0)) : - before; - while (bStart < bEnd) - parentNode.insertBefore(get(b[bStart++], 1), node); - } - // remove head or tail: fast path - else if (bEnd === bStart) { - while (aStart < aEnd) { - // remove the node only if it's unknown or not live - if (!map || !map.has(a[aStart])) - parentNode.removeChild(get(a[aStart], -1)); - aStart++; - } - } - // same node: fast path - else if (a[aStart] === b[bStart]) { - aStart++; - bStart++; - } - // same tail: fast path - else if (a[aEnd - 1] === b[bEnd - 1]) { - aEnd--; - bEnd--; - } - // The once here single last swap "fast path" has been removed in v1.1.0 - // https://github.com/WebReflection/udomdiff/blob/single-final-swap/esm/index.js#L69-L85 - // reverse swap: also fast path - else if ( - a[aStart] === b[bEnd - 1] && - b[bStart] === a[aEnd - 1] - ) { - // this is a "shrink" operation that could happen in these cases: - // [1, 2, 3, 4, 5] - // [1, 4, 3, 2, 5] - // or asymmetric too - // [1, 2, 3, 4, 5] - // [1, 2, 3, 5, 6, 4] - const node = get(a[--aEnd], -1).nextSibling; - parentNode.insertBefore( - get(b[bStart++], 1), - get(a[aStart++], -1).nextSibling - ); - parentNode.insertBefore(get(b[--bEnd], 1), node); - // mark the future index as identical (yeah, it's dirty, but cheap šŸ‘) - // The main reason to do this, is that when a[aEnd] will be reached, - // the loop will likely be on the fast path, as identical to b[bEnd]. - // In the best case scenario, the next loop will skip the tail, - // but in the worst one, this node will be considered as already - // processed, bailing out pretty quickly from the map index check - a[aEnd] = b[bEnd]; - } - // map based fallback, "slow" path - else { - // the map requires an O(bEnd - bStart) operation once - // to store all future nodes indexes for later purposes. - // In the worst case scenario, this is a full O(N) cost, - // and such scenario happens at least when all nodes are different, - // but also if both first and last items of the lists are different - if (!map) { - map = new Map; - let i = bStart; - while (i < bEnd) - map.set(b[i], i++); - } - // if it's a future node, hence it needs some handling - if (map.has(a[aStart])) { - // grab the index of such node, 'cause it might have been processed - const index = map.get(a[aStart]); - // if it's not already processed, look on demand for the next LCS - if (bStart < index && index < bEnd) { - let i = aStart; - // counts the amount of nodes that are the same in the future - let sequence = 1; - while (++i < aEnd && i < bEnd && map.get(a[i]) === (index + sequence)) - sequence++; - // effort decision here: if the sequence is longer than replaces - // needed to reach such sequence, which would brings again this loop - // to the fast path, prepend the difference before a sequence, - // and move only the future list index forward, so that aStart - // and bStart will be aligned again, hence on the fast path. - // An example considering aStart and bStart are both 0: - // a: [1, 2, 3, 4] - // b: [7, 1, 2, 3, 6] - // this would place 7 before 1 and, from that time on, 1, 2, and 3 - // will be processed at zero cost - if (sequence > (index - bStart)) { - const node = get(a[aStart], 0); - while (bStart < index) - parentNode.insertBefore(get(b[bStart++], 1), node); - } - // if the effort wasn't good enough, fallback to a replace, - // moving both source and target indexes forward, hoping that some - // similar node will be found later on, to go back to the fast path - else { - parentNode.replaceChild( - get(b[bStart++], 1), - get(a[aStart++], -1) - ); - } - } - // otherwise move the source forward, 'cause there's nothing to do - else - aStart++; - } - // this node has no meaning in the future list, so it's more than safe - // to remove it, and check the next live node out instead, meaning - // that only the live list index should be forwarded - else - parentNode.removeChild(get(a[aStart++], -1)); - } - } - return b; - }; - - const {isArray, prototype} = Array; - const {indexOf} = prototype; - - const { - createDocumentFragment, - createElement, - createElementNS, - createTextNode, - createTreeWalker, - importNode - } = new Proxy({}, { - get: (_, method) => document[method].bind(document) - }); - - const createHTML = html => { - const template = createElement('template'); - template.innerHTML = html; - return template.content; - }; - - let xml; - const createSVG = svg => { - if (!xml) xml = createElementNS('http://www.w3.org/2000/svg', 'svg'); - xml.innerHTML = svg; - const content = createDocumentFragment(); - content.append(...xml.childNodes); - return content; - }; - - const createContent = (text, svg) => svg ? - createSVG(text) : createHTML(text); - - // from a generic path, retrieves the exact targeted node - const reducePath = ({childNodes}, i) => childNodes[i]; - - // this helper avoid code bloat around handleAnything() callback - const diff = (comment, oldNodes, newNodes) => udomdiff( - comment.parentNode, - // TODO: there is a possible edge case where a node has been - // removed manually, or it was a keyed one, attached - // to a shared reference between renders. - // In this case udomdiff might fail at removing such node - // as its parent won't be the expected one. - // The best way to avoid this issue is to filter oldNodes - // in search of those not live, or not in the current parent - // anymore, but this would require both a change to uwire, - // exposing a parentNode from the firstChild, as example, - // but also a filter per each diff that should exclude nodes - // that are not in there, penalizing performance quite a lot. - // As this has been also a potential issue with domdiff, - // and both lighterhtml and hyperHTML might fail with this - // very specific edge case, I might as well document this possible - // "diffing shenanigan" and call it a day. - oldNodes, - newNodes, - diffable, - comment - ); - - // if an interpolation represents a comment, the whole - // diffing will be related to such comment. - // This helper is in charge of understanding how the new - // content for such interpolation/hole should be updated - const handleAnything = comment => { - let oldValue, text, nodes = []; - const anyContent = newValue => { - switch (typeof newValue) { - // primitives are handled as text content - case 'string': - case 'number': - case 'boolean': - if (oldValue !== newValue) { - oldValue = newValue; - if (!text) - text = createTextNode(''); - text.data = newValue; - nodes = diff(comment, nodes, [text]); - } - break; - // null, and undefined are used to cleanup previous content - case 'object': - case 'undefined': - if (newValue == null) { - if (oldValue != newValue) { - oldValue = newValue; - nodes = diff(comment, nodes, []); - } - break; - } - // arrays and nodes have a special treatment - if (isArray(newValue)) { - oldValue = newValue; - // arrays can be used to cleanup, if empty - if (newValue.length === 0) - nodes = diff(comment, nodes, []); - // or diffed, if these contains nodes or "wires" - else if (typeof newValue[0] === 'object') - nodes = diff(comment, nodes, newValue); - // in all other cases the content is stringified as is - else - anyContent(String(newValue)); - break; - } - // if the new value is a DOM node, or a wire, and it's - // different from the one already live, then it's diffed. - // if the node is a fragment, it's appended once via its childNodes - // There is no `else` here, meaning if the content - // is not expected one, nothing happens, as easy as that. - if (oldValue !== newValue) { - if ('ELEMENT_NODE' in newValue) { - oldValue = newValue; - nodes = diff( - comment, - nodes, - newValue.nodeType === 11 ? - [...newValue.childNodes] : - [newValue] - ); - } - else { - const value = newValue.valueOf(); - if (value !== newValue) - anyContent(value); - } - } - break; - case 'function': - anyContent(newValue(comment)); - break; - } - }; - return anyContent; - }; - - // attributes can be: - // * ref=${...} for hooks and other purposes - // * aria=${...} for aria attributes - // * ?boolean=${...} for boolean attributes - // * .dataset=${...} for dataset related attributes - // * .setter=${...} for Custom Elements setters or nodes with setters - // such as buttons, details, options, select, etc - // * @event=${...} to explicitly handle event listeners - // * onevent=${...} to automatically handle event listeners - // * generic=${...} to handle an attribute just like an attribute - const handleAttribute = (node, name/*, svg*/) => { - switch (name[0]) { - case '?': return boolean(node, name.slice(1), false); - case '.': return setter(node, name.slice(1)); - case '@': return event(node, 'on' + name.slice(1)); - case 'o': if (name[1] === 'n') return event(node, name); - } - - switch (name) { - case 'ref': return ref(node); - case 'aria': return aria(node); - } - - return attribute(node, name/*, svg*/); - }; - - // each mapped update carries the update type and its path - // the type is either node, attribute, or text, while - // the path is how to retrieve the related node to update. - // In the attribute case, the attribute name is also carried along. - function handlers(options) { - const {type, path} = options; - const node = path.reduceRight(reducePath, this); - return type === 'node' ? - handleAnything(node) : - (type === 'attr' ? - handleAttribute(node, options.name/*, options.svg*/) : - text(node)); - } - - // from a fragment container, create an array of indexes - // related to its child nodes, so that it's possible - // to retrieve later on exact node via reducePath - const createPath = node => { - const path = []; - let {parentNode} = node; - while (parentNode) { - path.push(indexOf.call(parentNode.childNodes, node)); - node = parentNode; - ({parentNode} = node); - } - return path; - }; - - // the prefix is used to identify either comments, attributes, or nodes - // that contain the related unique id. In the attribute cases - // isĀµX="attribute-name" will be used to map current X update to that - // attribute name, while comments will be like , to map - // the update to that specific comment node, hence its parent. - // style and textarea will have text content, and are handled - // directly through text-only updates. - const prefix = 'isĀµ'; - - // Template Literals are unique per scope and static, meaning a template - // should be parsed once, and once only, as it will always represent the same - // content, within the exact same amount of updates each time. - // This cache relates each template to its unique content and updates. - const cache$1 = new WeakMapSet; - - // a RegExp that helps checking nodes that cannot contain comments - const textOnly = /^(?:textarea|script|style|title|plaintext|xmp)$/; - - const createCache = () => ({ - stack: [], // each template gets a stack for each interpolation "hole" - - entry: null, // each entry contains details, such as: - // * the template that is representing - // * the type of node it represents (html or svg) - // * the content fragment with all nodes - // * the list of updates per each node (template holes) - // * the "wired" node or fragment that will get updates - // if the template or type are different from the previous one - // the entry gets re-created each time - - wire: null // each rendered node represent some wired content and - // this reference to the latest one. If different, the node - // will be cleaned up and the new "wire" will be appended - }); - - // the entry stored in the rendered node cache, and per each "hole" - const createEntry = (type, template) => { - const {content, updates} = mapUpdates(type, template); - return {type, template, content, updates, wire: null}; - }; - - // a template is instrumented to be able to retrieve where updates are needed. - // Each unique template becomes a fragment, cloned once per each other - // operation based on the same template, i.e. data => html`

${data}

` - const mapTemplate = (type, template) => { - const svg = type === 'svg'; - const text = instrument(template, prefix, svg); - const content = createContent(text, svg); - // once instrumented and reproduced as fragment, it's crawled - // to find out where each update is in the fragment tree - const tw = createTreeWalker(content, 1 | 128); - const nodes = []; - const length = template.length - 1; - let i = 0; - // updates are searched via unique names, linearly increased across the tree - //
- let search = `${prefix}${i}`; - while (i < length) { - const node = tw.nextNode(); - // if not all updates are bound but there's nothing else to crawl - // it means that there is something wrong with the template. - if (!node) - throw `bad template: ${text}`; - // if the current node is a comment, and it contains isĀµX - // it means the update should take care of any content - if (node.nodeType === 8) { - // The only comments to be considered are those - // which content is exactly the same as the searched one. - if (node.data === search) { - nodes.push({type: 'node', path: createPath(node)}); - search = `${prefix}${++i}`; - } - } - else { - // if the node is not a comment, loop through all its attributes - // named isĀµX and relate attribute updates to this node and the - // attribute name, retrieved through node.getAttribute("isĀµX") - // the isĀµX attribute will be removed as irrelevant for the layout - // let svg = -1; - while (node.hasAttribute(search)) { - nodes.push({ - type: 'attr', - path: createPath(node), - name: node.getAttribute(search) - }); - node.removeAttribute(search); - search = `${prefix}${++i}`; - } - // if the node was a style, textarea, or others, check its content - // and if it is then update tex-only this node - if ( - textOnly.test(node.localName) && - node.textContent.trim() === `` - ){ - node.textContent = ''; - nodes.push({type: 'text', path: createPath(node)}); - search = `${prefix}${++i}`; - } - } - } - // once all nodes to update, or their attributes, are known, the content - // will be cloned in the future to represent the template, and all updates - // related to such content retrieved right away without needing to re-crawl - // the exact same template, and its content, more than once. - return {content, nodes}; - }; - - // if a template is unknown, perform the previous mapping, otherwise grab - // its details such as the fragment with all nodes, and updates info. - const mapUpdates = (type, template) => { - const {content, nodes} = ( - cache$1.get(template) || - cache$1.set(template, mapTemplate(type, template)) - ); - // clone deeply the fragment - const fragment = importNode(content, true); - // and relate an update handler per each node that needs one - const updates = nodes.map(handlers, fragment); - // return the fragment and all updates to use within its nodes - return {content: fragment, updates}; - }; - - // as html and svg can be nested calls, but no parent node is known - // until rendered somewhere, the unroll operation is needed to - // discover what to do with each interpolation, which will result - // into an update operation. - const unroll = (info, {type, template, values}) => { - // interpolations can contain holes and arrays, so these need - // to be recursively discovered - const length = unrollValues(info, values); - let {entry} = info; - // if the cache entry is either null or different from the template - // and the type this unroll should resolve, create a new entry - // assigning a new content fragment and the list of updates. - if (!entry || (entry.template !== template || entry.type !== type)) - info.entry = (entry = createEntry(type, template)); - const {content, updates, wire} = entry; - // even if the fragment and its nodes is not live yet, - // it is already possible to update via interpolations values. - for (let i = 0; i < length; i++) - updates[i](values[i]); - // if the entry was new, or representing a different template or type, - // create a new persistent entity to use during diffing. - // This is simply a DOM node, when the template has a single container, - // as in `

`, or a "wire" in `

` and similar cases. - return wire || (entry.wire = persistent(content)); - }; - - // the stack retains, per each interpolation value, the cache - // related to each interpolation value, or null, if the render - // was conditional and the value is not special (Array or Hole) - const unrollValues = ({stack}, values) => { - const {length} = values; - for (let i = 0; i < length; i++) { - const hole = values[i]; - // each Hole gets unrolled and re-assigned as value - // so that domdiff will deal with a node/wire, not with a hole - if (hole instanceof Hole) - values[i] = unroll( - stack[i] || (stack[i] = createCache()), - hole - ); - // arrays are recursively resolved so that each entry will contain - // also a DOM node or a wire, hence it can be diffed if/when needed - else if (isArray(hole)) - unrollValues(stack[i] || (stack[i] = createCache()), hole); - // if the value is nothing special, the stack doesn't need to retain data - // this is useful also to cleanup previously retained data, if the value - // was a Hole, or an Array, but not anymore, i.e.: - // const update = content => html`
${content}
`; - // update(listOfItems); update(null); update(html`hole`) - else - stack[i] = null; - } - if (length < stack.length) - stack.splice(length); - return length; - }; - - /** - * Holds all details wrappers needed to render the content further on. - * @constructor - * @param {string} type The hole type, either `html` or `svg`. - * @param {string[]} template The template literals used to the define the content. - * @param {Array} values Zero, one, or more interpolated values to render. - */ - class Hole { - constructor(type, template, values) { - this.type = type; - this.template = template; - this.values = values; - } - } - - // both `html` and `svg` template literal tags are polluted - // with a `for(ref[, id])` and a `node` tag too - const tag = type => { - // both `html` and `svg` tags have their own cache - const keyed = new WeakMapSet; - // keyed operations always re-use the same cache and unroll - // the template and its interpolations right away - const fixed = cache => (template, ...values) => unroll( - cache, - {type, template, values} - ); - return Object.assign( - // non keyed operations are recognized as instance of Hole - // during the "unroll", recursively resolved and updated - (template, ...values) => new Hole(type, template, values), - { - // keyed operations need a reference object, usually the parent node - // which is showing keyed results, and optionally a unique id per each - // related node, handy with JSON results and mutable list of objects - // that usually carry a unique identifier - for(ref, id) { - const memo = keyed.get(ref) || keyed.set(ref, new MapSet); - return memo.get(id) || memo.set(id, fixed(createCache())); - }, - // it is possible to create one-off content out of the box via node tag - // this might return the single created node, or a fragment with all - // nodes present at the root level and, of course, their child nodes - node: (template, ...values) => unroll(createCache(), new Hole(type, template, values)).valueOf() - } - ); - }; - - // each rendered node gets its own cache - const cache = new WeakMapSet; - - // rendering means understanding what `html` or `svg` tags returned - // and it relates a specific node to its own unique cache. - // Each time the content to render changes, the node is cleaned up - // and the new new content is appended, and if such content is a Hole - // then it's "unrolled" to resolve all its inner nodes. - const render = (where, what) => { - const hole = typeof what === 'function' ? what() : what; - const info = cache.get(where) || cache.set(where, createCache()); - const wire = hole instanceof Hole ? unroll(info, hole) : hole; - if (wire !== info.wire) { - info.wire = wire; - // valueOf() simply returns the node itself, but in case it was a "wire" - // it will eventually re-append all nodes to its fragment so that such - // fragment can be re-appended many times in a meaningful way - // (wires are basically persistent fragments facades with special behavior) - where.replaceChildren(wire.valueOf()); - } - return where; - }; - - const html = tag('html'); - const svg = tag('svg'); - - exports.Hole = Hole; - exports.html = html; - exports.render = render; - exports.svg = svg; - - return exports; - -})({}); +const{isArray:e}=Array,t=[],n=()=>document.createRange(),r=(e,t,n)=>(e.set(t,n),n),s=(e,t,n,r="")=>({t:e,p:t,u:n,n:r}),l=e=>({s:e,t:null,n:null,d:t}),{setPrototypeOf:o}=Object;let c;var i=(e,t,r)=>(c||(c=n()),r?c.setStartAfter(e):c.setStartBefore(e),c.setEndAfter(t),c.deleteContents(),e);const a=({firstChild:e,lastChild:t})=>i(e,t,!0);let u=!1;const h=(e,t)=>u&&11===e.nodeType?1/t<0?t?a(e):e.lastChild:t?e.valueOf():e.firstChild:e;class d extends((e=>{function t(e){return o(e,new.target.prototype)}return t.prototype=e.prototype,t})(DocumentFragment)){#e;#t;constructor(e){super(e),this.#e=[...e.childNodes],this.#t=this.#e.length,u=!0}get firstChild(){return this.#e[0]}get lastChild(){return this.#e.at(-1)}replaceWith(e){a(this).replaceWith(e)}valueOf(){return this.childNodes.length!==this.#t&&this.append(...this.#e),this}}const f=(e,t)=>t.reduceRight(p,e),p=(e,t)=>e.childNodes[t];var g=e=>(n,r)=>{const{c:s,e:l,l:o}=e(n,r),c=s.cloneNode(!0);let i,a,u=l.length,h=u?l.slice(0):t;for(;u--;){const{t:e,p:n,u:s,n:o}=l[u],d=n===a?i:i=f(c,a=n),p=8===e?s():s;h[u]={v:p(d,r[u],o,t),u:p,t:d,n:o}}return((e,t)=>({n:e,d:t}))(1===o?c.firstChild:new d(c),h)};const m=/^(?:plaintext|script|style|textarea|title|xmp)$/i,v=/^(?:area|base|br|col|embed|hr|img|input|keygen|link|menuitem|meta|param|source|track|wbr)$/i,x=/<([a-zA-Z0-9]+[a-zA-Z0-9:._-]*)([^>]*?)(\/?)>/g,b=/([^\s\\>"'=]+)\s*=\s*(['"]?)\x01/g,$=/[\x01\x02]/g;const C=(e,t)=>{for(const n in t){const r=t[n],s="role"===n?n:`aria-${n}`;null==r?e.removeAttribute(s):e.setAttribute(s,r)}return t};let w;const y=(t,n,s)=>{s=s.slice(1),w||(w=new WeakMap);const l=w.get(t)||r(w,t,{});let o=l[s];return o&&o[0]&&t.removeEventListener(...o),o=e(n)?n:[n,!1],l[s]=o,o[0]&&t.addEventListener(...o),n},N=(e,t)=>W(e,t,"className"),A=(e,t)=>{const{dataset:n}=e;for(const e in t)null==t[e]?delete n[e]:n[e]=t[e];return t},W=(e,t,n)=>e[n]=t,k=(e,t,n)=>W(e,t,n.slice(1)),E=(e,t)=>("function"==typeof t?t(e):t.current=e,t),O=(e,t,n)=>(null==t?e.removeAttribute(n):e.setAttribute(n,t),t),S=(e,t)=>W(e.style,t,"cssText"),T=(e,t,n)=>(e.toggleAttribute(n.slice(1),t),t),B=(e,n,r,s)=>n.length?((e,t,n,r,s)=>{const l=n.length;let o=t.length,c=l,i=0,a=0,u=null;for(;is-a){const l=r(t[i],0);for(;a{switch(t[0]){case".":return k;case"?":return T;case"@":return y;default:switch(t){case"aria":return C;case"class":return N;case"data":return A;case"ref":return E;case"style":return S;default:return t in e?W:O}}},j=(e,t)=>(e.texContent=null==t?"":t,t);function L(e,t){const n=this.n||(this.n=e);switch(typeof t){case"string":case"number":case"boolean":n!==e&&n.replaceWith(this.n=e),this.n.data=t;break;case"object":case"undefined":n.replaceWith(this.n=null==t?e:t.valueOf())}return t}let z,F,R=document.createElement("template");var Z=(e,t)=>{if(t)return z||(z=document.createElementNS("http://www.w3.org/2000/svg","svg"),F=n(),F.selectNodeContents(z)),F.createContextualFragment(e);R.innerHTML=e;const{content:r}=R;return R=R.cloneNode(!1),r};const D=e=>{const t=[];let n;for(;n=e.parentNode;)t.push(t.indexOf.call(n.childNodes,e)),e=n;return t},H=()=>L.bind({n:null}),P=()=>B,_=(n,l,o)=>{const c=Z(((e,t,n)=>{let r=0;return e.join("").trim().replace(x,((e,t,r,s)=>`<${t}${r.replace(b,"=$2$1").trimEnd()}${s?n||v.test(t)?" /":`>`)).replace($,(e=>""===e?`\x3c!--${t+r++}--\x3e`:t+r++))})(n,G,o),o);let i=t;const{length:a}=n;if(a>1){const t=document.createTreeWalker(c,129),n=[];let r=0,o=`${G}${r++}`;for(i=[];r(t,n)=>q.get(t)||_(t,n,e);const J=g(I(!1)),K=g(I(!0)),Q=(e,{s:n,t:r,v:s})=>{if(s.length&&e.s===t&&(e.s=[]),U(e,s),e.t!==r){const{n:t,d:l}=(n?K:J)(r,s);e.t=r,e.n=t,e.d=l}else{const{d:t}=e;for(let e=0;e{const{length:s}=r;for(let o=0;o{const s=X.get(e)||r(X,e,l(t));return s.n!==Q(s,"function"==typeof n?n():n)&&e.replaceChildren(s.n),e}; +/*! (c) Andrea Giammarchi - MIT */const ee=e=>(t,...n)=>new V(e,t,n),te=ee(!1),ne=ee(!0);export{V as Hole,te as html,Y as render,ne as svg}; diff --git a/init.js b/init.js deleted file mode 100644 index 1869b3c..0000000 --- a/init.js +++ /dev/null @@ -1,2 +0,0 @@ -class e extends WeakMap{set(e,t){return super.set(e,t),t}} -/*! (c) Andrea Giammarchi - ISC */const t=/^(?:area|base|br|col|embed|hr|img|input|keygen|link|menuitem|meta|param|source|track|wbr)$/i,n=/<([a-z]+[a-z0-9:._-]*)([^>]*?)(\/?)>/g,r=/([^\s\\>"'=]+)\s*=\s*(['"]?)\x01/g,s=/[\x01\x02]/g;var l=({document:l})=>{const{isArray:o,prototype:a}=Array,{indexOf:i}=a,{createDocumentFragment:c,createElement:u,createElementNS:d,createTextNode:p,createTreeWalker:f,importNode:h}=new Proxy({},{get:(e,t)=>l[t].bind(l)});let g;const m=(e,t)=>t?(e=>{g||(g=d("http://www.w3.org/2000/svg","svg")),g.innerHTML=e;const t=c();return t.append(...g.childNodes),t})(e):(e=>{const t=u("template");return t.innerHTML=e,t.content})(e),y=(e,t)=>111===e.nodeType?1/t<0?t?(({firstChild:e,lastChild:t})=>{const n=l.createRange();return n.setStartAfter(e),n.setEndAfter(t),n.deleteContents(),e})(e):e.lastChild:t?e.valueOf():e.firstChild:e,b=e=>null==e?e:e.valueOf(),w=(e,t)=>{let n,r,s=t.slice(2);return!(t in e)&&(r=t.toLowerCase())in e&&(s=r.slice(2)),t=>{const r=o(t)?t:[t,!1];n!==r[0]&&(n&&e.removeEventListener(s,n,r[1]),(n=r[0])&&e.addEventListener(s,n,r[1]))}},v=({childNodes:e},t)=>e[t],x=(e,t,n)=>((e,t,n,r,s)=>{const l=n.length;let o=t.length,a=l,i=0,c=0,u=null;for(;is-c){const l=r(t[i],0);for(;c{switch(t[0]){case"?":return((e,t,n)=>r=>{const s=!!b(r);n!==s&&((n=s)?e.setAttribute(t,""):e.removeAttribute(t))})(e,t.slice(1),!1);case".":return((e,t)=>"dataset"===t?(({dataset:e})=>t=>{for(const n in t){const r=t[n];null==r?delete e[n]:e[n]=r}})(e):n=>{e[t]=n})(e,t.slice(1));case"@":return w(e,"on"+t.slice(1));case"o":if("n"===t[1])return w(e,t)}switch(t){case"ref":return(e=>{let t;return n=>{t!==n&&(t=n,"function"==typeof n?n(e):n.current=e)}})(e);case"aria":return(e=>t=>{for(const n in t){const r="role"===n?n:`aria-${n}`,s=t[n];null==s?e.removeAttribute(r):e.setAttribute(r,s)}})(e)}return((e,t)=>{let n,r=!0;const s=l.createAttributeNS(null,t);return t=>{const l=b(t);n!==l&&(null==(n=l)?r||(e.removeAttributeNode(s),r=!0):(s.value=l,r&&(e.setAttributeNodeNS(s),r=!1)))}})(e,t)};function C(e){const{type:t,path:n}=e,r=n.reduceRight(v,this);return"node"===t?(e=>{let t,n,r=[];const s=l=>{switch(typeof l){case"string":case"number":case"boolean":t!==l&&(t=l,n||(n=p("")),n.data=l,r=x(e,r,[n]));break;case"object":case"undefined":if(null==l){t!=l&&(t=l,r=x(e,r,[]));break}if(o(l)){t=l,0===l.length?r=x(e,r,[]):"object"==typeof l[0]?r=x(e,r,l):s(String(l));break}if(t!==l)if("ELEMENT_NODE"in l)t=l,r=x(e,r,11===l.nodeType?[...l.childNodes]:[l]);else{const e=l.valueOf();e!==l&&s(e)}break;case"function":s(l(e))}};return s})(r):"attr"===t?N(r,e.name):(e=>{let t;return n=>{const r=b(n);t!=r&&(t=r,e.textContent=null==r?"":r)}})(r)}const k=e=>{const t=[];let{parentNode:n}=e;for(;n;)t.push(i.call(n.childNodes,e)),e=n,({parentNode:n}=e);return t},$="isĀµ",A=new e,E=/^(?:textarea|script|style|title|plaintext|xmp)$/,O=(e,l)=>{const o="svg"===e,a=((e,l,o)=>{let a=0;return e.join("").trim().replace(n,((e,n,s,l)=>{let a=n+s.replace(r,"=$2$1").trimEnd();return l.length&&(a+=o||t.test(n)?" /":">"})).replace(s,(e=>""===e?"\x3c!--"+l+a+++"--\x3e":l+a++))})(l,$,o),i=m(a,o),c=f(i,129),u=[],d=l.length-1;let p=0,h=`${$}${p}`;for(;p{const{content:n,nodes:r}=A.get(t)||A.set(t,O(e,t)),s=h(n,!0);return{content:s,updates:r.map(C,s)}},S=(e,{type:t,template:n,values:r})=>{const s=L(e,r);let{entry:l}=e;l&&l.template===n&&l.type===t||(e.entry=l=((e,t)=>{const{content:n,updates:r}=T(e,t);return{type:e,template:t,content:n,updates:r,wire:null}})(t,n));const{content:o,updates:a,wire:i}=l;for(let e=0;e{const{firstChild:t,lastChild:n}=e;if(t===n)return n||e;const{childNodes:r}=e,s=[...r];return{ELEMENT_NODE:1,nodeType:111,firstChild:t,lastChild:n,valueOf:()=>(r.length!==s.length&&e.append(...s),e)}})(o))},L=({stack:e},t)=>{const{length:n}=t;for(let r=0;r{const n=new e;return Object.assign(((e,...n)=>new M(t,e,n)),{for(e,r){const s=n.get(e)||n.set(e,new MapSet);return s.get(r)||s.set(r,(e=>(n,...r)=>S(e,{type:t,template:n,values:r}))({stack:[],entry:null,wire:null}))},node:(e,...n)=>S({stack:[],entry:null,wire:null},new M(t,e,n)).valueOf()})},B=new e,D=j("html"),H=j("svg");return{Hole:M,render:(e,t)=>{const n="function"==typeof t?t():t,r=B.get(e)||B.set(e,{stack:[],entry:null,wire:null}),s=n instanceof M?S(r,n):n;return s!==r.wire&&(r.wire=s,e.replaceChildren(s.valueOf())),e},html:D,svg:H}};export{l as default}; diff --git a/keyed.js b/keyed.js new file mode 100644 index 0000000..1e3b0da --- /dev/null +++ b/keyed.js @@ -0,0 +1,3 @@ +const{isArray:e}=Array,t=[],n=()=>document.createRange(),r=(e,t,n)=>(e.set(t,n),n),s=(e,t,n,r="")=>({t:e,p:t,u:n,n:r}),l=e=>({s:e,t:null,n:null,d:t}),{setPrototypeOf:o}=Object;let i;var c=(e,t,r)=>(i||(i=n()),r?i.setStartAfter(e):i.setStartBefore(e),i.setEndAfter(t),i.deleteContents(),e);const a=({firstChild:e,lastChild:t})=>c(e,t,!0);let u=!1;const h=(e,t)=>u&&11===e.nodeType?1/t<0?t?a(e):e.lastChild:t?e.valueOf():e.firstChild:e;class d extends((e=>{function t(e){return o(e,new.target.prototype)}return t.prototype=e.prototype,t})(DocumentFragment)){#e;#t;constructor(e){super(e),this.#e=[...e.childNodes],this.#t=this.#e.length,u=!0}get firstChild(){return this.#e[0]}get lastChild(){return this.#e.at(-1)}replaceWith(e){a(this).replaceWith(e)}valueOf(){return this.childNodes.length!==this.#t&&this.append(...this.#e),this}}const f=(e,t)=>t.reduceRight(p,e),p=(e,t)=>e.childNodes[t];var g=e=>(n,r)=>{const{c:s,e:l,l:o}=e(n,r),i=s.cloneNode(!0);let c,a,u=l.length,h=u?l.slice(0):t;for(;u--;){const{t:e,p:n,u:s,n:o}=l[u],d=n===a?c:c=f(i,a=n),p=8===e?s():s;h[u]={v:p(d,r[u],o,t),u:p,t:d,n:o}}return((e,t)=>({n:e,d:t}))(1===o?i.firstChild:new d(i),h)};const m=/^(?:plaintext|script|style|textarea|title|xmp)$/i,v=/^(?:area|base|br|col|embed|hr|img|input|keygen|link|menuitem|meta|param|source|track|wbr)$/i,b=/<([a-zA-Z0-9]+[a-zA-Z0-9:._-]*)([^>]*?)(\/?)>/g,x=/([^\s\\>"'=]+)\s*=\s*(['"]?)\x01/g,$=/[\x01\x02]/g;const w=(e,t)=>{for(const n in t){const r=t[n],s="role"===n?n:`aria-${n}`;null==r?e.removeAttribute(s):e.setAttribute(s,r)}return t};let C;const y=(t,n,s)=>{s=s.slice(1),C||(C=new WeakMap);const l=C.get(t)||r(C,t,{});let o=l[s];return o&&o[0]&&t.removeEventListener(...o),o=e(n)?n:[n,!1],l[s]=o,o[0]&&t.addEventListener(...o),n},N=(e,t)=>W(e,t,"className"),A=(e,t)=>{const{dataset:n}=e;for(const e in t)null==t[e]?delete n[e]:n[e]=t[e];return t},W=(e,t,n)=>e[n]=t,k=(e,t,n)=>W(e,t,n.slice(1)),M=(e,t)=>("function"==typeof t?t(e):t.current=e,t),E=(e,t,n)=>(null==t?e.removeAttribute(n):e.setAttribute(n,t),t),O=(e,t)=>W(e.style,t,"cssText"),S=(e,t,n)=>(e.toggleAttribute(n.slice(1),t),t),T=(e,n,r,s)=>n.length?((e,t,n,r,s)=>{const l=n.length;let o=t.length,i=l,c=0,a=0,u=null;for(;cs-a){const l=r(t[c],0);for(;a{switch(t[0]){case".":return k;case"?":return S;case"@":return y;default:switch(t){case"aria":return w;case"class":return N;case"data":return A;case"ref":return M;case"style":return O;default:return t in e?W:E}}},j=(e,t)=>(e.texContent=null==t?"":t,t);function L(e,t){const n=this.n||(this.n=e);switch(typeof t){case"string":case"number":case"boolean":n!==e&&n.replaceWith(this.n=e),this.n.data=t;break;case"object":case"undefined":n.replaceWith(this.n=null==t?e:t.valueOf())}return t}let z,F,R=document.createElement("template");var Z=(e,t)=>{if(t)return z||(z=document.createElementNS("http://www.w3.org/2000/svg","svg"),F=n(),F.selectNodeContents(z)),F.createContextualFragment(e);R.innerHTML=e;const{content:r}=R;return R=R.cloneNode(!1),r};const D=e=>{const t=[];let n;for(;n=e.parentNode;)t.push(t.indexOf.call(n.childNodes,e)),e=n;return t},H=()=>L.bind({n:null}),P=()=>T,_=(n,l,o)=>{const i=Z(((e,t,n)=>{let r=0;return e.join("").trim().replace(b,((e,t,r,s)=>`<${t}${r.replace(x,"=$2$1").trimEnd()}${s?n||v.test(t)?" /":`>`)).replace($,(e=>""===e?`\x3c!--${t+r++}--\x3e`:t+r++))})(n,G,o),o);let c=t;const{length:a}=n;if(a>1){const t=document.createTreeWalker(i,129),n=[];let r=0,o=`${G}${r++}`;for(c=[];r(t,n)=>q.get(t)||_(t,n,e);const J=g(I(!1)),K=g(I(!0)),Q=(e,{s:n,t:r,v:s})=>{if(s.length&&e.s===t&&(e.s=[]),U(e,s),e.t!==r){const{n:t,d:l}=(n?K:J)(r,s);e.t=r,e.n=t,e.d=l}else{const{d:t}=e;for(let e=0;e{const{length:s}=r;for(let o=0;o(t,...n)=>new V(e,t,n),Y=X(!1),ee=X(!0),te=new WeakMap;var ne=(e,n)=>{const s=te.get(e)||r(te,e,l(t)),o="function"==typeof n?n():n,{n:i}=s,c=o instanceof V?Q(s,o):o;return i!==c&&e.replaceChildren(s.n=c),e}; +/*! (c) Andrea Giammarchi - MIT */const re=new WeakMap,se=e=>(n,s)=>{const o=re.get(n)||r(re,n,new Map);return o.get(s)||r(o,s,function(t,...n){return Q(this,new V(e,t,n))}.bind(l(t)))},le=se(!1),oe=se(!0);export{V as Hole,Y as html,le as htmlFor,ne as render,ee as svg,oe as svgFor}; diff --git a/node.js b/node.js new file mode 100644 index 0000000..169fc97 --- /dev/null +++ b/node.js @@ -0,0 +1,3 @@ +const{setPrototypeOf:e}=Object;const{isArray:t}=Array,n=[],r=()=>document.createRange(),s=(e,t,n)=>(e.set(t,n),n);let l;var o=(e,t,n)=>(l||(l=r()),n?l.setStartAfter(e):l.setStartBefore(e),l.setEndAfter(t),l.deleteContents(),e);const i=({firstChild:e,lastChild:t})=>o(e,t,!0);let c=!1;const a=(e,t)=>c&&11===e.nodeType?1/t<0?t?i(e):e.lastChild:t?e.valueOf():e.firstChild:e;class u extends((t=>{function n(t){return e(t,new.target.prototype)}return n.prototype=t.prototype,n})(DocumentFragment)){#e;#t;constructor(e){super(e),this.#e=[...e.childNodes],this.#t=this.#e.length,c=!0}get firstChild(){return this.#e[0]}get lastChild(){return this.#e.at(-1)}replaceWith(e){i(this).replaceWith(e)}valueOf(){return this.childNodes.length!==this.#t&&this.append(...this.#e),this}}const h=(e,t,n,r="")=>({t:e,p:t,u:n,n:r}),d=(e,t)=>t.reduceRight(f,e),f=(e,t)=>e.childNodes[t];const p=/^(?:plaintext|script|style|textarea|title|xmp)$/i,g=/^(?:area|base|br|col|embed|hr|img|input|keygen|link|menuitem|meta|param|source|track|wbr)$/i,m=/<([a-zA-Z0-9]+[a-zA-Z0-9:._-]*)([^>]*?)(\/?)>/g,x=/([^\s\\>"'=]+)\s*=\s*(['"]?)\x01/g,b=/[\x01\x02]/g;const v=(e,t)=>{for(const n in t){const r=t[n],s="role"===n?n:`aria-${n}`;null==r?e.removeAttribute(s):e.setAttribute(s,r)}return t};let $;const C=(e,n,r)=>{r=r.slice(1),$||($=new WeakMap);const l=$.get(e)||s($,e,{});let o=l[r];return o&&o[0]&&e.removeEventListener(...o),o=t(n)?n:[n,!1],l[r]=o,o[0]&&e.addEventListener(...o),n},y=(e,t)=>A(e,t,"className"),N=(e,t)=>{const{dataset:n}=e;for(const e in t)null==t[e]?delete n[e]:n[e]=t[e];return t},A=(e,t,n)=>e[n]=t,w=(e,t,n)=>A(e,t,n.slice(1)),W=(e,t)=>("function"==typeof t?t(e):t.current=e,t),k=(e,t,n)=>(null==t?e.removeAttribute(n):e.setAttribute(n,t),t),E=(e,t)=>A(e.style,t,"cssText"),O=(e,t,n)=>(e.toggleAttribute(n.slice(1),t),t),S=(e,t,r,s)=>t.length?((e,t,n,r,s)=>{const l=n.length;let o=t.length,i=l,c=0,a=0,u=null;for(;cs-a){const l=r(t[c],0);for(;a{switch(t[0]){case".":return w;case"?":return O;case"@":return C;default:switch(t){case"aria":return v;case"class":return y;case"data":return N;case"ref":return W;case"style":return E;default:return t in e?A:k}}},B=(e,t)=>(e.texContent=null==t?"":t,t);function M(e,t){const n=this.n||(this.n=e);switch(typeof t){case"string":case"number":case"boolean":n!==e&&n.replaceWith(this.n=e),this.n.data=t;break;case"object":case"undefined":n.replaceWith(this.n=null==t?e:t.valueOf())}return t}let j,L,z=document.createElement("template");var F=(e,t)=>{if(t)return j||(j=document.createElementNS("http://www.w3.org/2000/svg","svg"),L=r(),L.selectNodeContents(j)),L.createContextualFragment(e);z.innerHTML=e;const{content:n}=z;return z=z.cloneNode(!1),n};const R=e=>{const t=[];let n;for(;n=e.parentNode;)t.push(t.indexOf.call(n.childNodes,e)),e=n;return t},Z=()=>M.bind({n:null}),D=()=>S,H=(e,r,l)=>{const o=F(((e,t,n)=>{let r=0;return e.join("").trim().replace(m,((e,t,r,s)=>`<${t}${r.replace(x,"=$2$1").trimEnd()}${s?n||g.test(t)?" /":`>`)).replace(b,(e=>""===e?`\x3c!--${t+r++}--\x3e`:t+r++))})(e,_,l),l);let i=n;const{length:c}=e;if(c>1){const e=document.createTreeWalker(o,129),n=[];let s=0,l=`${_}${s++}`;for(i=[];s(e.replaceChildren("function"==typeof t?t():t),e); +/*! (c) Andrea Giammarchi - MIT */ +const G=e=>{const t=(e=>(t,r)=>{const{c:s,e:l,l:o}=e(t,r),i=s.cloneNode(!0);let c,a,h=l.length,f=h?l.slice(0):n;for(;h--;){const{t:e,p:t,u:s,n:o}=l[h],u=t===a?c:c=d(i,a=t),p=8===e?s():s;f[h]={v:p(u,r[h],o,n),u:p,t:u,n:o}}return{n:1===o?i.firstChild:new u(i),d:f}})((r=e,(e,t)=>P.get(e)||H(e,t,r)));var r;return(e,...n)=>t(e,n).n},I=G(!1),J=G(!0);export{I as html,q as render,J as svg}; diff --git a/package-lock.json b/package-lock.json index 9553bfe..fdd13be 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,27 +7,21 @@ "": { "name": "uhtml", "version": "3.2.2", - "license": "ISC", + "license": "MIT", "dependencies": { - "@webreflection/mapset": "^1.0.1", - "@webreflection/uparser": "^0.2.4", - "@webreflection/uwire": "^1.2.1", - "async-tag": "^0.2.0", - "jsx2tag": "^0.3.1", - "udomdiff": "^1.1.0", - "uhandlers": "^0.7.0" + "@webreflection/uparser": "^0.3.3", + "custom-function": "^1.0.6", + "domconstants": "^1.1.6", + "udomdiff": "^1.1.0" }, "devDependencies": { - "@rollup/plugin-node-resolve": "^15.1.0", - "@rollup/plugin-terser": "^0.4.3", - "@ungap/degap": "^0.2.8", - "ascjs": "^5.0.1", - "c8": "^8.0.0", - "drop-babel-typeof": "^1.0.3", - "linkedom": "^0.14.26", - "rollup": "^3.25.1", - "rollup-plugin-includepaths": "^0.2.4", - "terser": "^5.18.1" + "@rollup/plugin-node-resolve": "^15.2.3", + "@rollup/plugin-terser": "^0.4.4", + "ascjs": "^6.0.3", + "c8": "^8.0.1", + "linkedom": "^0.16.1", + "rollup": "^4.4.0", + "typescript": "^5.2.2" } }, "node_modules/@babel/parser": { @@ -122,9 +116,9 @@ "dev": true }, "node_modules/@rollup/plugin-node-resolve": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.1.0.tgz", - "integrity": "sha512-xeZHCgsiZ9pzYVgAo9580eCGqwh/XCEUM9q6iQfGNocjgkufHAqC3exA+45URvhiYV8sBF9RlBai650eNs7AsA==", + "version": "15.2.3", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.2.3.tgz", + "integrity": "sha512-j/lym8nf5E21LwBT4Df1VD6hRO2L2iwUeUmP7litikRsVp1H6NWx20NEp0Y7su+7XGc476GnXXc4kFeZNGmaSQ==", "dev": true, "dependencies": { "@rollup/pluginutils": "^5.0.1", @@ -138,7 +132,7 @@ "node": ">=14.0.0" }, "peerDependencies": { - "rollup": "^2.78.0||^3.0.0" + "rollup": "^2.78.0||^3.0.0||^4.0.0" }, "peerDependenciesMeta": { "rollup": { @@ -147,9 +141,9 @@ } }, "node_modules/@rollup/plugin-terser": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.3.tgz", - "integrity": "sha512-EF0oejTMtkyhrkwCdg0HJ0IpkcaVg1MMSf2olHb2Jp+1mnLM04OhjpJWGma4HobiDTF0WCyViWuvadyE9ch2XA==", + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz", + "integrity": "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==", "dev": true, "dependencies": { "serialize-javascript": "^6.0.1", @@ -160,7 +154,7 @@ "node": ">=14.0.0" }, "peerDependencies": { - "rollup": "^2.x || ^3.x" + "rollup": "^2.0.0||^3.0.0||^4.0.0" }, "peerDependenciesMeta": { "rollup": { @@ -169,9 +163,9 @@ } }, "node_modules/@rollup/pluginutils": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.2.tgz", - "integrity": "sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.5.tgz", + "integrity": "sha512-6aEYR910NyP73oHiJglti74iRyOwgFU4x3meH/H8OJx6Ry0j6cOVZ5X/wTvub7G7Ao6qaHBEaNsV3GLJkSsF+Q==", "dev": true, "dependencies": { "@types/estree": "^1.0.0", @@ -182,7 +176,7 @@ "node": ">=14.0.0" }, "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0" + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "peerDependenciesMeta": { "rollup": { @@ -190,16 +184,172 @@ } } }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.4.0.tgz", + "integrity": "sha512-AD30wtT58hZZsXIeiksytR6Gm2gofUxn5KqrDBdyzekgxXB9bXN9dqWIEcPfYo9lA9MVRm0lC42LuYGsscRxiA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.4.0.tgz", + "integrity": "sha512-PlqvhzFxy5FRTB3wLSsGgPhiakv9jrgfu8tjSojLJFP0CdhfZSRDOFvQ2emWLUEBOSCnjpL63XSuFVMwg59ZtA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.4.0.tgz", + "integrity": "sha512-BYmhn1Hebmkmdyn5mBFy7HptowyjtMALyTpywNSNZYigWwyv4L8WQVr0XvOQE7eE6WoKrupSVxtIcGZW8MgZUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.4.0.tgz", + "integrity": "sha512-7GXsMiX/giTDBMs/gL3rePLBRC6gV7DT7JQ0lNqoNDe5hm+Gm4NEWky9fwEmer64fIUbOsTiLUsyQ5fDXUbXPA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.4.0.tgz", + "integrity": "sha512-kavnkaV50Gu6vESlOAwUad92wYY9mUrcaPmhzOQZKlNFnzWAUYyD/uhHmWvY7Z2chtwhWlng0LvCRBF5QiPO7w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.4.0.tgz", + "integrity": "sha512-2hBHEtCjnBTeuLvDAlHRCqsuFQSyAhTQs9vbZEVBTV8ap35pDI1ukPbIVFFCWNvL/KE7xRor5YZFvfyGCfvLnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.4.0.tgz", + "integrity": "sha512-u7zy0Ygzl7O5Gvr9TSNSQj+DBzvMJC7rXfyQNgZ13KwkhgJ8z0z+gt2AO4RPd01rZioMQ2/TA24XGGg4xqhd0Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.4.0.tgz", + "integrity": "sha512-VvpAdh5SgewmWo8sa5QPYG8aSKH9hU2Kr5+3of0GzBI/8n8PBqhLyvF0DbO+zDW8j5IM8NDebv82MpHrZaD0Cw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.4.0.tgz", + "integrity": "sha512-3g6jaXxXVFaDnFoMn2+E3ludGcXFfEr6lDn+S1lh9Qe0JcL9sPt1wGh0g2cKIlb6OakNOFopZqJ5Yub9F7gQlA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.4.0.tgz", + "integrity": "sha512-jnoDRkg5Ve6Y1qx2m1+ehouOLQ4ddc15/iQSfFjcDUL6bqLdJJ5c4CKfUy/C6W1oCU4la+hMkveE9GG7ECN7dg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.4.0.tgz", + "integrity": "sha512-SoLQmJanozFow8o50ul2a3R+J7nk4pEhrp83PzTSXs5OzOmIZbPSp5kihtQ3f6ypo4MCbmh0V8Ev0bJIEp4Azw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.4.0.tgz", + "integrity": "sha512-Zaz6itfQ5sQF5Cia49YDW1ZTr+YfIKzTSb9npLyvQn346n7ulRDOv2J7GnL0zcOJ3cqW7HzG/ZisyO6fH43J9g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@types/estree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz", - "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", "dev": true }, "node_modules/@types/istanbul-lib-coverage": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", - "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==", + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", "dev": true }, "node_modules/@types/resolve": { @@ -208,26 +358,13 @@ "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", "dev": true }, - "node_modules/@ungap/degap": { - "version": "0.2.8", - "resolved": "https://registry.npmjs.org/@ungap/degap/-/degap-0.2.8.tgz", - "integrity": "sha512-avav4FVH0W/eyMCAVNHw19Oi7Hv2Ebv5xIhUhagOAuZIQEj6S+C9rVCjXGf8sxdIbwK13woArhsfzeZlYgjCpg==", - "dev": true - }, - "node_modules/@webreflection/mapset": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@webreflection/mapset/-/mapset-1.0.1.tgz", - "integrity": "sha512-cfHPwoviBs7Y/sewLQqE6Ic3XJfUr+LbNEYtR2uW4Od41y5Mg8TTQ8hUb3zBp3cepZTPpwhI6YMnjWk+olqO2w==" - }, "node_modules/@webreflection/uparser": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/@webreflection/uparser/-/uparser-0.2.4.tgz", - "integrity": "sha512-4cYSODHAbjsIlvlTLffaN+QiFcNSLTYkRLOrDqpK+m6Bzqyjudq/xHTiSl4/LxeijcQE48nJQuaBnJcnizXxrA==" - }, - "node_modules/@webreflection/uwire": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@webreflection/uwire/-/uwire-1.2.1.tgz", - "integrity": "sha512-3FIqIFzqij5NPWKWCQKJhfcRQpfS8RHAcceFosSDDiD6WrMHRAp3QoBqYt7dfrPhJ8Fg2b6T6Ea8ttClzVkZHA==" + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@webreflection/uparser/-/uparser-0.3.3.tgz", + "integrity": "sha512-XxGfo8jr2eVuvP5lrmwjgMAM7QjtZ0ngFD+dd9Fd3GStcEb4QhLlTiqZYF5O3l5k4sU/V6ZiPrVCzCWXWFEmCw==", + "dependencies": { + "domconstants": "^1.1.6" + } }, "node_modules/acorn": { "version": "8.9.0", @@ -266,9 +403,9 @@ } }, "node_modules/ascjs": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ascjs/-/ascjs-5.0.1.tgz", - "integrity": "sha512-1d/QdICzpywXiP53/Zz3fMdaC0/BB1ybLf+fK+QrqY8iyXNnWUHUrpmrowueXeswo+O+meJWm43TJSg2ClP3Sg==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/ascjs/-/ascjs-6.0.3.tgz", + "integrity": "sha512-lAIyi1j7oT0OtF9yFLiRf93LEcK7xTb/gPFwmfdi2T/BQmxJi1YcIES+VnP/kgGJkxq9Oh2DEK6GrZ6l2OVhVQ==", "dev": true, "dependencies": { "@babel/parser": "^7.12.5" @@ -277,11 +414,6 @@ "ascjs": "bin.js" } }, - "node_modules/async-tag": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/async-tag/-/async-tag-0.2.0.tgz", - "integrity": "sha512-hNstPiQvxVVJdkBjfBsNb3zDEM2IUY3Xp7qaadEnhaGXq1/OFdS+TuwjEJxnIvZRm7e13KrPW74fIeDra7P5vw==" - }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -323,9 +455,9 @@ } }, "node_modules/c8": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/c8/-/c8-8.0.0.tgz", - "integrity": "sha512-XHA5vSfCLglAc0Xt8eLBZMv19lgiBSjnb1FLAQgnwkuhJYEonpilhEB4Ea3jPAbm0FhD6VVJrc0z73jPe7JyGQ==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/c8/-/c8-8.0.1.tgz", + "integrity": "sha512-EINpopxZNH1mETuI0DzRA4MZpAUH+IFiRhnmFD3vFr3vdrgxqi3VfE3KL0AIL+zDq8rC9bZqwM/VDmmoe04y7w==", "dev": true, "dependencies": { "@bcoe/v8-coverage": "^0.2.3", @@ -333,13 +465,13 @@ "find-up": "^5.0.0", "foreground-child": "^2.0.0", "istanbul-lib-coverage": "^3.2.0", - "istanbul-lib-report": "^3.0.0", - "istanbul-reports": "^3.1.4", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.1.6", "rimraf": "^3.0.2", "test-exclude": "^6.0.0", "v8-to-istanbul": "^9.0.0", - "yargs": "^16.2.0", - "yargs-parser": "^20.2.9" + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1" }, "bin": { "c8": "bin/c8.js" @@ -349,14 +481,17 @@ } }, "node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "dev": true, "dependencies": { "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", + "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" } }, "node_modules/color-convert": { @@ -390,9 +525,9 @@ "dev": true }, "node_modules/convert-source-map": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true }, "node_modules/cross-spawn": { @@ -443,6 +578,11 @@ "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==", "dev": true }, + "node_modules/custom-function": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/custom-function/-/custom-function-1.0.6.tgz", + "integrity": "sha512-styyvwOki/EYr+VBe7/m9xAjq6uKx87SpDKIpFRdTQnofBDSZpBEFc9qJLmaJihjjTeEpAIJ+nz+9fUXj+BPNQ==" + }, "node_modules/deepmerge": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", @@ -466,6 +606,11 @@ "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" } }, + "node_modules/domconstants": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/domconstants/-/domconstants-1.1.6.tgz", + "integrity": "sha512-CuaDrThJ4VM+LyZ4ax8n52k0KbLJZtffyGkuj1WhpTRRcSfcy/9DfOBa68jenhX96oNUTunblSJEUNC4baFdmQ==" + }, "node_modules/domelementtype": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", @@ -507,15 +652,6 @@ "url": "https://github.com/fb55/domutils?sponsor=1" } }, - "node_modules/drop-babel-typeof": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/drop-babel-typeof/-/drop-babel-typeof-1.0.3.tgz", - "integrity": "sha512-nmhRIvZrHSzEv8sc6kqh+2pG7ZAg9ZRjFyY4YavbIzvF/6SYdsH4SDFJAYLWTAh1XoVfdbe3Kmy44h/KB7gv+Q==", - "dev": true, - "bin": { - "drop-babel-typeof": "index.js" - } - }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -661,9 +797,9 @@ "dev": true }, "node_modules/htmlparser2": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", - "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.0.0.tgz", + "integrity": "sha512-uxbSI98wmFT/G4P2zXx4OVx04qWUmyFPrD2/CNepa2Zo3GPNaCaaxElDgwUrwYWkK1nr9fft0Ya8dws8coDLLQ==", "dev": true, "funding": [ "https://github.com/fb55/htmlparser2?sponsor=1", @@ -675,8 +811,8 @@ "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", - "domutils": "^3.0.1", - "entities": "^4.4.0" + "domutils": "^3.1.0", + "entities": "^4.5.0" } }, "node_modules/inflight": { @@ -744,32 +880,32 @@ "dev": true }, "node_modules/istanbul-lib-coverage": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", - "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", "dev": true, "engines": { "node": ">=8" } }, "node_modules/istanbul-lib-report": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", - "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", "dev": true, "dependencies": { "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^3.0.0", + "make-dir": "^4.0.0", "supports-color": "^7.1.0" }, "engines": { - "node": ">=8" + "node": ">=10" } }, "node_modules/istanbul-reports": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.5.tgz", - "integrity": "sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w==", + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.6.tgz", + "integrity": "sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==", "dev": true, "dependencies": { "html-escaper": "^2.0.0", @@ -779,21 +915,16 @@ "node": ">=8" } }, - "node_modules/jsx2tag": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/jsx2tag/-/jsx2tag-0.3.1.tgz", - "integrity": "sha512-S1ACW3N4yDiE49x9y9f2d+utB3Kci3A3Bx7wb2bK+5sl0B60lZM60pZ8L/A+FA55DYuAy2fgHu1i47yt+xgL9Q==" - }, "node_modules/linkedom": { - "version": "0.14.26", - "resolved": "https://registry.npmjs.org/linkedom/-/linkedom-0.14.26.tgz", - "integrity": "sha512-mK6TrydfFA7phrnp+1j57ycBwFI5bGSW6YXlw9acHoqF+mP/y+FooEYYyniOt5Ot57FSKB3iwmnuQ1UUyNLm5A==", + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/linkedom/-/linkedom-0.16.1.tgz", + "integrity": "sha512-kbK4txFSjGstS8aHkYo3sRSD7viQbE1TAlYRqiqU13oihzzQFp23D1OwW3VdAQJuzqzBB+1qo9Qvp0xOeoVKig==", "dev": true, "dependencies": { "css-select": "^5.1.0", "cssom": "^0.5.0", "html-escaper": "^3.0.3", - "htmlparser2": "^8.0.1", + "htmlparser2": "^9.0.0", "uhyphen": "^0.2.0" } }, @@ -818,16 +949,28 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", "dev": true, "dependencies": { - "semver": "^6.0.0" + "semver": "^7.5.3" }, "engines": { - "node": ">=8" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -992,27 +1135,33 @@ } }, "node_modules/rollup": { - "version": "3.25.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.25.1.tgz", - "integrity": "sha512-tywOR+rwIt5m2ZAWSe5AIJcTat8vGlnPFAv15ycCrw33t6iFsXZ6mzHVFh2psSjxQPmI+xgzMZZizUAukBI4aQ==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.4.0.tgz", + "integrity": "sha512-3L67ubCc1Qm49wUodsQ72FM6JmJ9M37d63rGPjxbcKrzNJrwFipl+lDNHeWd6BId09S6Tb9KiBgYKbWhIuqVyg==", "dev": true, "bin": { "rollup": "dist/bin/rollup" }, "engines": { - "node": ">=14.18.0", + "node": ">=18.0.0", "npm": ">=8.0.0" }, "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.4.0", + "@rollup/rollup-android-arm64": "4.4.0", + "@rollup/rollup-darwin-arm64": "4.4.0", + "@rollup/rollup-darwin-x64": "4.4.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.4.0", + "@rollup/rollup-linux-arm64-gnu": "4.4.0", + "@rollup/rollup-linux-arm64-musl": "4.4.0", + "@rollup/rollup-linux-x64-gnu": "4.4.0", + "@rollup/rollup-linux-x64-musl": "4.4.0", + "@rollup/rollup-win32-arm64-msvc": "4.4.0", + "@rollup/rollup-win32-ia32-msvc": "4.4.0", + "@rollup/rollup-win32-x64-msvc": "4.4.0", "fsevents": "~2.3.2" } }, - "node_modules/rollup-plugin-includepaths": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/rollup-plugin-includepaths/-/rollup-plugin-includepaths-0.2.4.tgz", - "integrity": "sha512-iZen+XKVExeCzk7jeSZPJKL7B67slZNr8GXSC5ROBXtDGXDBH8wdjMfdNW5hf9kPt+tHyIvWh3wlE9bPrZL24g==", - "dev": true - }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -1034,12 +1183,18 @@ ] }, "node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, "bin": { "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" } }, "node_modules/serialize-javascript": { @@ -1185,24 +1340,24 @@ "node": ">=8" } }, - "node_modules/uarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/uarray/-/uarray-1.0.0.tgz", - "integrity": "sha512-LHmiAd5QuAv7pU2vbh+Zq9YOnqVK0H764p2Ozinpfy9ka58OID4IsGLiXsitqH7n0NAIDxvax1A/kDXpii/Ckg==" + "node_modules/typescript": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", + "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } }, "node_modules/udomdiff": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/udomdiff/-/udomdiff-1.1.0.tgz", "integrity": "sha512-aqjTs5x/wsShZBkVagdafJkP8S3UMGhkHKszsu1cszjjZ7iOp86+Qb3QOFYh01oWjPMy5ZTuxD6hw5uTKxd+VA==" }, - "node_modules/uhandlers": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/uhandlers/-/uhandlers-0.7.0.tgz", - "integrity": "sha512-MG6Q6Dc+xIfyFnHU8APpR916XWhnb+m30qVjPXxAmHUaVFmzUAcd6BCWws5LedyG43onCk1RRPNq0N02wcn4NA==", - "dependencies": { - "uarray": "^1.0.0" - } - }, "node_modules/uhyphen": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/uhyphen/-/uhyphen-0.2.0.tgz", @@ -1210,14 +1365,14 @@ "dev": true }, "node_modules/v8-to-istanbul": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz", - "integrity": "sha512-6z3GW9x8G1gd+JIIgQQQxXuiJtCXeAjp6RaPEPLv62mH3iPHPxV6W3robxtCzNErRo6ZwTmzWhsbNvjyEBKzKA==", + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.3.tgz", + "integrity": "sha512-9lDD+EVI2fjFsMWXc6dy5JJzBsVTcQ2fVkfBvncZ6xJWG9wtBhOldG+mHkSL0+V1K/xgZz0JDO5UT5hFwHUghg==", "dev": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.12", "@types/istanbul-lib-coverage": "^2.0.1", - "convert-source-map": "^1.6.0" + "convert-source-map": "^2.0.0" }, "engines": { "node": ">=10.12.0" @@ -1270,31 +1425,37 @@ "node": ">=10" } }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, "node_modules/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "dev": true, "dependencies": { - "cliui": "^7.0.2", + "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", - "string-width": "^4.2.0", + "string-width": "^4.2.3", "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" + "yargs-parser": "^21.1.1" }, "engines": { - "node": ">=10" + "node": ">=12" } }, "node_modules/yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "dev": true, "engines": { - "node": ">=10" + "node": ">=12" } }, "node_modules/yocto-queue": { @@ -1387,9 +1548,9 @@ } }, "@rollup/plugin-node-resolve": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.1.0.tgz", - "integrity": "sha512-xeZHCgsiZ9pzYVgAo9580eCGqwh/XCEUM9q6iQfGNocjgkufHAqC3exA+45URvhiYV8sBF9RlBai650eNs7AsA==", + "version": "15.2.3", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.2.3.tgz", + "integrity": "sha512-j/lym8nf5E21LwBT4Df1VD6hRO2L2iwUeUmP7litikRsVp1H6NWx20NEp0Y7su+7XGc476GnXXc4kFeZNGmaSQ==", "dev": true, "requires": { "@rollup/pluginutils": "^5.0.1", @@ -1401,9 +1562,9 @@ } }, "@rollup/plugin-terser": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.3.tgz", - "integrity": "sha512-EF0oejTMtkyhrkwCdg0HJ0IpkcaVg1MMSf2olHb2Jp+1mnLM04OhjpJWGma4HobiDTF0WCyViWuvadyE9ch2XA==", + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz", + "integrity": "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==", "dev": true, "requires": { "serialize-javascript": "^6.0.1", @@ -1412,9 +1573,9 @@ } }, "@rollup/pluginutils": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.2.tgz", - "integrity": "sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.5.tgz", + "integrity": "sha512-6aEYR910NyP73oHiJglti74iRyOwgFU4x3meH/H8OJx6Ry0j6cOVZ5X/wTvub7G7Ao6qaHBEaNsV3GLJkSsF+Q==", "dev": true, "requires": { "@types/estree": "^1.0.0", @@ -1422,16 +1583,100 @@ "picomatch": "^2.3.1" } }, + "@rollup/rollup-android-arm-eabi": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.4.0.tgz", + "integrity": "sha512-AD30wtT58hZZsXIeiksytR6Gm2gofUxn5KqrDBdyzekgxXB9bXN9dqWIEcPfYo9lA9MVRm0lC42LuYGsscRxiA==", + "dev": true, + "optional": true + }, + "@rollup/rollup-android-arm64": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.4.0.tgz", + "integrity": "sha512-PlqvhzFxy5FRTB3wLSsGgPhiakv9jrgfu8tjSojLJFP0CdhfZSRDOFvQ2emWLUEBOSCnjpL63XSuFVMwg59ZtA==", + "dev": true, + "optional": true + }, + "@rollup/rollup-darwin-arm64": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.4.0.tgz", + "integrity": "sha512-BYmhn1Hebmkmdyn5mBFy7HptowyjtMALyTpywNSNZYigWwyv4L8WQVr0XvOQE7eE6WoKrupSVxtIcGZW8MgZUA==", + "dev": true, + "optional": true + }, + "@rollup/rollup-darwin-x64": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.4.0.tgz", + "integrity": "sha512-7GXsMiX/giTDBMs/gL3rePLBRC6gV7DT7JQ0lNqoNDe5hm+Gm4NEWky9fwEmer64fIUbOsTiLUsyQ5fDXUbXPA==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.4.0.tgz", + "integrity": "sha512-kavnkaV50Gu6vESlOAwUad92wYY9mUrcaPmhzOQZKlNFnzWAUYyD/uhHmWvY7Z2chtwhWlng0LvCRBF5QiPO7w==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-arm64-gnu": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.4.0.tgz", + "integrity": "sha512-2hBHEtCjnBTeuLvDAlHRCqsuFQSyAhTQs9vbZEVBTV8ap35pDI1ukPbIVFFCWNvL/KE7xRor5YZFvfyGCfvLnA==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-arm64-musl": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.4.0.tgz", + "integrity": "sha512-u7zy0Ygzl7O5Gvr9TSNSQj+DBzvMJC7rXfyQNgZ13KwkhgJ8z0z+gt2AO4RPd01rZioMQ2/TA24XGGg4xqhd0Q==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-x64-gnu": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.4.0.tgz", + "integrity": "sha512-VvpAdh5SgewmWo8sa5QPYG8aSKH9hU2Kr5+3of0GzBI/8n8PBqhLyvF0DbO+zDW8j5IM8NDebv82MpHrZaD0Cw==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-x64-musl": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.4.0.tgz", + "integrity": "sha512-3g6jaXxXVFaDnFoMn2+E3ludGcXFfEr6lDn+S1lh9Qe0JcL9sPt1wGh0g2cKIlb6OakNOFopZqJ5Yub9F7gQlA==", + "dev": true, + "optional": true + }, + "@rollup/rollup-win32-arm64-msvc": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.4.0.tgz", + "integrity": "sha512-jnoDRkg5Ve6Y1qx2m1+ehouOLQ4ddc15/iQSfFjcDUL6bqLdJJ5c4CKfUy/C6W1oCU4la+hMkveE9GG7ECN7dg==", + "dev": true, + "optional": true + }, + "@rollup/rollup-win32-ia32-msvc": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.4.0.tgz", + "integrity": "sha512-SoLQmJanozFow8o50ul2a3R+J7nk4pEhrp83PzTSXs5OzOmIZbPSp5kihtQ3f6ypo4MCbmh0V8Ev0bJIEp4Azw==", + "dev": true, + "optional": true + }, + "@rollup/rollup-win32-x64-msvc": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.4.0.tgz", + "integrity": "sha512-Zaz6itfQ5sQF5Cia49YDW1ZTr+YfIKzTSb9npLyvQn346n7ulRDOv2J7GnL0zcOJ3cqW7HzG/ZisyO6fH43J9g==", + "dev": true, + "optional": true + }, "@types/estree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz", - "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", "dev": true }, "@types/istanbul-lib-coverage": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", - "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==", + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", "dev": true }, "@types/resolve": { @@ -1440,26 +1685,13 @@ "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", "dev": true }, - "@ungap/degap": { - "version": "0.2.8", - "resolved": "https://registry.npmjs.org/@ungap/degap/-/degap-0.2.8.tgz", - "integrity": "sha512-avav4FVH0W/eyMCAVNHw19Oi7Hv2Ebv5xIhUhagOAuZIQEj6S+C9rVCjXGf8sxdIbwK13woArhsfzeZlYgjCpg==", - "dev": true - }, - "@webreflection/mapset": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@webreflection/mapset/-/mapset-1.0.1.tgz", - "integrity": "sha512-cfHPwoviBs7Y/sewLQqE6Ic3XJfUr+LbNEYtR2uW4Od41y5Mg8TTQ8hUb3zBp3cepZTPpwhI6YMnjWk+olqO2w==" - }, "@webreflection/uparser": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/@webreflection/uparser/-/uparser-0.2.4.tgz", - "integrity": "sha512-4cYSODHAbjsIlvlTLffaN+QiFcNSLTYkRLOrDqpK+m6Bzqyjudq/xHTiSl4/LxeijcQE48nJQuaBnJcnizXxrA==" - }, - "@webreflection/uwire": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@webreflection/uwire/-/uwire-1.2.1.tgz", - "integrity": "sha512-3FIqIFzqij5NPWKWCQKJhfcRQpfS8RHAcceFosSDDiD6WrMHRAp3QoBqYt7dfrPhJ8Fg2b6T6Ea8ttClzVkZHA==" + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@webreflection/uparser/-/uparser-0.3.3.tgz", + "integrity": "sha512-XxGfo8jr2eVuvP5lrmwjgMAM7QjtZ0ngFD+dd9Fd3GStcEb4QhLlTiqZYF5O3l5k4sU/V6ZiPrVCzCWXWFEmCw==", + "requires": { + "domconstants": "^1.1.6" + } }, "acorn": { "version": "8.9.0", @@ -1483,19 +1715,14 @@ } }, "ascjs": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ascjs/-/ascjs-5.0.1.tgz", - "integrity": "sha512-1d/QdICzpywXiP53/Zz3fMdaC0/BB1ybLf+fK+QrqY8iyXNnWUHUrpmrowueXeswo+O+meJWm43TJSg2ClP3Sg==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/ascjs/-/ascjs-6.0.3.tgz", + "integrity": "sha512-lAIyi1j7oT0OtF9yFLiRf93LEcK7xTb/gPFwmfdi2T/BQmxJi1YcIES+VnP/kgGJkxq9Oh2DEK6GrZ6l2OVhVQ==", "dev": true, "requires": { "@babel/parser": "^7.12.5" } }, - "async-tag": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/async-tag/-/async-tag-0.2.0.tgz", - "integrity": "sha512-hNstPiQvxVVJdkBjfBsNb3zDEM2IUY3Xp7qaadEnhaGXq1/OFdS+TuwjEJxnIvZRm7e13KrPW74fIeDra7P5vw==" - }, "balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1531,9 +1758,9 @@ "dev": true }, "c8": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/c8/-/c8-8.0.0.tgz", - "integrity": "sha512-XHA5vSfCLglAc0Xt8eLBZMv19lgiBSjnb1FLAQgnwkuhJYEonpilhEB4Ea3jPAbm0FhD6VVJrc0z73jPe7JyGQ==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/c8/-/c8-8.0.1.tgz", + "integrity": "sha512-EINpopxZNH1mETuI0DzRA4MZpAUH+IFiRhnmFD3vFr3vdrgxqi3VfE3KL0AIL+zDq8rC9bZqwM/VDmmoe04y7w==", "dev": true, "requires": { "@bcoe/v8-coverage": "^0.2.3", @@ -1541,23 +1768,23 @@ "find-up": "^5.0.0", "foreground-child": "^2.0.0", "istanbul-lib-coverage": "^3.2.0", - "istanbul-lib-report": "^3.0.0", - "istanbul-reports": "^3.1.4", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.1.6", "rimraf": "^3.0.2", "test-exclude": "^6.0.0", "v8-to-istanbul": "^9.0.0", - "yargs": "^16.2.0", - "yargs-parser": "^20.2.9" + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1" } }, "cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "dev": true, "requires": { "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", + "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, @@ -1589,9 +1816,9 @@ "dev": true }, "convert-source-map": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true }, "cross-spawn": { @@ -1630,6 +1857,11 @@ "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==", "dev": true }, + "custom-function": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/custom-function/-/custom-function-1.0.6.tgz", + "integrity": "sha512-styyvwOki/EYr+VBe7/m9xAjq6uKx87SpDKIpFRdTQnofBDSZpBEFc9qJLmaJihjjTeEpAIJ+nz+9fUXj+BPNQ==" + }, "deepmerge": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", @@ -1647,6 +1879,11 @@ "entities": "^4.2.0" } }, + "domconstants": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/domconstants/-/domconstants-1.1.6.tgz", + "integrity": "sha512-CuaDrThJ4VM+LyZ4ax8n52k0KbLJZtffyGkuj1WhpTRRcSfcy/9DfOBa68jenhX96oNUTunblSJEUNC4baFdmQ==" + }, "domelementtype": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", @@ -1673,12 +1910,6 @@ "domhandler": "^5.0.3" } }, - "drop-babel-typeof": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/drop-babel-typeof/-/drop-babel-typeof-1.0.3.tgz", - "integrity": "sha512-nmhRIvZrHSzEv8sc6kqh+2pG7ZAg9ZRjFyY4YavbIzvF/6SYdsH4SDFJAYLWTAh1XoVfdbe3Kmy44h/KB7gv+Q==", - "dev": true - }, "emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -1784,15 +2015,15 @@ "dev": true }, "htmlparser2": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", - "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.0.0.tgz", + "integrity": "sha512-uxbSI98wmFT/G4P2zXx4OVx04qWUmyFPrD2/CNepa2Zo3GPNaCaaxElDgwUrwYWkK1nr9fft0Ya8dws8coDLLQ==", "dev": true, "requires": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", - "domutils": "^3.0.1", - "entities": "^4.4.0" + "domutils": "^3.1.0", + "entities": "^4.5.0" } }, "inflight": { @@ -1848,47 +2079,42 @@ "dev": true }, "istanbul-lib-coverage": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", - "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", "dev": true }, "istanbul-lib-report": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", - "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", "dev": true, "requires": { "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^3.0.0", + "make-dir": "^4.0.0", "supports-color": "^7.1.0" } }, "istanbul-reports": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.5.tgz", - "integrity": "sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w==", + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.6.tgz", + "integrity": "sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==", "dev": true, "requires": { "html-escaper": "^2.0.0", "istanbul-lib-report": "^3.0.0" } }, - "jsx2tag": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/jsx2tag/-/jsx2tag-0.3.1.tgz", - "integrity": "sha512-S1ACW3N4yDiE49x9y9f2d+utB3Kci3A3Bx7wb2bK+5sl0B60lZM60pZ8L/A+FA55DYuAy2fgHu1i47yt+xgL9Q==" - }, "linkedom": { - "version": "0.14.26", - "resolved": "https://registry.npmjs.org/linkedom/-/linkedom-0.14.26.tgz", - "integrity": "sha512-mK6TrydfFA7phrnp+1j57ycBwFI5bGSW6YXlw9acHoqF+mP/y+FooEYYyniOt5Ot57FSKB3iwmnuQ1UUyNLm5A==", + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/linkedom/-/linkedom-0.16.1.tgz", + "integrity": "sha512-kbK4txFSjGstS8aHkYo3sRSD7viQbE1TAlYRqiqU13oihzzQFp23D1OwW3VdAQJuzqzBB+1qo9Qvp0xOeoVKig==", "dev": true, "requires": { "css-select": "^5.1.0", "cssom": "^0.5.0", "html-escaper": "^3.0.3", - "htmlparser2": "^8.0.1", + "htmlparser2": "^9.0.0", "uhyphen": "^0.2.0" }, "dependencies": { @@ -1909,13 +2135,22 @@ "p-locate": "^5.0.0" } }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, "make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", "dev": true, "requires": { - "semver": "^6.0.0" + "semver": "^7.5.3" } }, "minimatch": { @@ -2029,20 +2264,26 @@ } }, "rollup": { - "version": "3.25.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.25.1.tgz", - "integrity": "sha512-tywOR+rwIt5m2ZAWSe5AIJcTat8vGlnPFAv15ycCrw33t6iFsXZ6mzHVFh2psSjxQPmI+xgzMZZizUAukBI4aQ==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.4.0.tgz", + "integrity": "sha512-3L67ubCc1Qm49wUodsQ72FM6JmJ9M37d63rGPjxbcKrzNJrwFipl+lDNHeWd6BId09S6Tb9KiBgYKbWhIuqVyg==", "dev": true, "requires": { + "@rollup/rollup-android-arm-eabi": "4.4.0", + "@rollup/rollup-android-arm64": "4.4.0", + "@rollup/rollup-darwin-arm64": "4.4.0", + "@rollup/rollup-darwin-x64": "4.4.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.4.0", + "@rollup/rollup-linux-arm64-gnu": "4.4.0", + "@rollup/rollup-linux-arm64-musl": "4.4.0", + "@rollup/rollup-linux-x64-gnu": "4.4.0", + "@rollup/rollup-linux-x64-musl": "4.4.0", + "@rollup/rollup-win32-arm64-msvc": "4.4.0", + "@rollup/rollup-win32-ia32-msvc": "4.4.0", + "@rollup/rollup-win32-x64-msvc": "4.4.0", "fsevents": "~2.3.2" } }, - "rollup-plugin-includepaths": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/rollup-plugin-includepaths/-/rollup-plugin-includepaths-0.2.4.tgz", - "integrity": "sha512-iZen+XKVExeCzk7jeSZPJKL7B67slZNr8GXSC5ROBXtDGXDBH8wdjMfdNW5hf9kPt+tHyIvWh3wlE9bPrZL24g==", - "dev": true - }, "safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -2050,10 +2291,13 @@ "dev": true }, "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } }, "serialize-javascript": { "version": "6.0.1", @@ -2165,24 +2409,17 @@ "minimatch": "^3.0.4" } }, - "uarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/uarray/-/uarray-1.0.0.tgz", - "integrity": "sha512-LHmiAd5QuAv7pU2vbh+Zq9YOnqVK0H764p2Ozinpfy9ka58OID4IsGLiXsitqH7n0NAIDxvax1A/kDXpii/Ckg==" + "typescript": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", + "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "dev": true }, "udomdiff": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/udomdiff/-/udomdiff-1.1.0.tgz", "integrity": "sha512-aqjTs5x/wsShZBkVagdafJkP8S3UMGhkHKszsu1cszjjZ7iOp86+Qb3QOFYh01oWjPMy5ZTuxD6hw5uTKxd+VA==" }, - "uhandlers": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/uhandlers/-/uhandlers-0.7.0.tgz", - "integrity": "sha512-MG6Q6Dc+xIfyFnHU8APpR916XWhnb+m30qVjPXxAmHUaVFmzUAcd6BCWws5LedyG43onCk1RRPNq0N02wcn4NA==", - "requires": { - "uarray": "^1.0.0" - } - }, "uhyphen": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/uhyphen/-/uhyphen-0.2.0.tgz", @@ -2190,14 +2427,14 @@ "dev": true }, "v8-to-istanbul": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz", - "integrity": "sha512-6z3GW9x8G1gd+JIIgQQQxXuiJtCXeAjp6RaPEPLv62mH3iPHPxV6W3robxtCzNErRo6ZwTmzWhsbNvjyEBKzKA==", + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.3.tgz", + "integrity": "sha512-9lDD+EVI2fjFsMWXc6dy5JJzBsVTcQ2fVkfBvncZ6xJWG9wtBhOldG+mHkSL0+V1K/xgZz0JDO5UT5hFwHUghg==", "dev": true, "requires": { "@jridgewell/trace-mapping": "^0.3.12", "@types/istanbul-lib-coverage": "^2.0.1", - "convert-source-map": "^1.6.0" + "convert-source-map": "^2.0.0" } }, "which": { @@ -2232,25 +2469,31 @@ "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "dev": true }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, "yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "dev": true, "requires": { - "cliui": "^7.0.2", + "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", - "string-width": "^4.2.0", + "string-width": "^4.2.3", "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" + "yargs-parser": "^21.1.1" } }, "yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "dev": true }, "yocto-queue": { diff --git a/package.json b/package.json index c147ed7..b049aed 100644 --- a/package.json +++ b/package.json @@ -2,19 +2,15 @@ "name": "uhtml", "version": "3.2.2", "description": "A micro HTML/SVG render", - "main": "cjs/index.js", - "types": "index.d.ts", + "main": "./cjs/index.js", "scripts": { - "build": "node pony.js && npm run cjs && npm run rollup:async && npm run rollup:es && npm run rollup:esm && npm run rollup:babel && npm run rollup:init && drop-babel-typeof ./index.js && npm run test && npm run size", - "cjs": "rm cjs/*.js && ascjs --no-default esm cjs", - "rollup:async": "rollup --config rollup/async.config.js && sed -i.bck 's/^var /self./' async.js && rm -rf async.js.bck", - "rollup:es": "rollup --config rollup/es.config.js && sed -i.bck 's/^var /self./' es.js && rm -rf es.js.bck", - "rollup:esm": "rollup --config rollup/esm.config.js", - "rollup:babel": "rollup --config rollup/babel.config.js && sed -i.bck 's/^var /self./' index.js && rm -rf index.js.bck", - "rollup:init": "rollup --config rollup/init.config.js && sed -i.bck 's/^var /self./' es.js && rm -rf es.js.bck", - "size": "cat es.js | brotli | wc -c && cat esm.js | brotli | wc -c && cat init.js | brotli | wc -c", + "build": "rm -rf cjs/* && npm run cjs && npm run rollup:es && rm -rf types && npm run ts && npm run size", + "cjs": "ascjs esm cjs", + "rollup:es": "rollup --config rollup/es.config.js", + "server": "npx static-handler .", + "size": "echo \"index $(cat index.js | brotli | wc -c)\";echo \"keyed $(cat keyed.js | brotli | wc -c)\";echo \"node $(cat node.js | brotli | wc -c)\";", "test": "node test/coverage.init.js && c8 node test/coverage.js", - "coverage": "mkdir -p ./coverage; c8 report --reporter=text-lcov > ./coverage/lcov.info" + "ts": "tsc -p ." }, "keywords": [ "micro", @@ -22,65 +18,41 @@ "render" ], "author": "Andrea Giammarchi", - "license": "ISC", + "license": "MIT", "devDependencies": { - "@rollup/plugin-node-resolve": "^15.1.0", - "@rollup/plugin-terser": "^0.4.3", - "@ungap/degap": "^0.2.8", - "ascjs": "^5.0.1", - "c8": "^8.0.0", - "drop-babel-typeof": "^1.0.3", - "linkedom": "^0.14.26", - "rollup": "^3.25.1", - "rollup-plugin-includepaths": "^0.2.4", - "terser": "^5.18.1" - }, - "dependencies": { - "@webreflection/mapset": "^1.0.1", - "@webreflection/uparser": "^0.2.4", - "@webreflection/uwire": "^1.2.1", - "async-tag": "^0.2.0", - "jsx2tag": "^0.3.1", - "udomdiff": "^1.1.0", - "uhandlers": "^0.7.0" + "@rollup/plugin-node-resolve": "^15.2.3", + "@rollup/plugin-terser": "^0.4.4", + "ascjs": "^6.0.3", + "c8": "^8.0.1", + "linkedom": "^0.16.1", + "rollup": "^4.4.0", + "typescript": "^5.2.2" }, "module": "./esm/index.js", "type": "module", "exports": { ".": { - "types": "./index.d.ts", + "types": "./types/index.d.ts", "import": "./esm/index.js", "default": "./cjs/index.js" }, - "./async": { - "types": "./async.d.ts", - "import": "./esm/async.js", - "default": "./cjs/async.js" - }, - "./init": { - "types": "./index.d.ts", - "import": "./esm/init.js", - "default": "./cjs/init.js" - }, - "./json": { - "types": "./index.d.ts", - "import": "./esm/json.js", - "default": "./cjs/json.js" + "./keyed": { + "types": "./types/keyed.d.ts", + "import": "./esm/keyed.js", + "default": "./cjs/keyed.js" }, - "./jsx": { - "types": "./index.d.ts", - "import": "./esm/x.js", - "default": "./cjs/x.js" + "./node": { + "types": "./types/node.d.ts", + "import": "./esm/node.js", + "default": "./cjs/node.js" }, "./package.json": "./package.json" }, - "unpkg": "es.js", - "repository": { - "type": "git", - "url": "git+https://github.com/WebReflection/uhtml.git" - }, - "bugs": { - "url": "https://github.com/WebReflection/uhtml/issues" - }, - "homepage": "https://github.com/WebReflection/uhtml#readme" + "unpkg": "./keyed.js", + "dependencies": { + "@webreflection/uparser": "^0.3.3", + "custom-function": "^1.0.6", + "domconstants": "^1.1.6", + "udomdiff": "^1.1.0" + } } diff --git a/pony.js b/pony.js deleted file mode 100644 index c9d2013..0000000 --- a/pony.js +++ /dev/null @@ -1,29 +0,0 @@ -import {readFileSync, writeFileSync} from 'fs'; - -const dropIE = s => s.replace(/^import\s+.+/mg, '').replace(/^export\s+/mg, ''); - -const utils = readFileSync('./esm/utils.js').toString(); -const uwire = readFileSync('./node_modules/@webreflection/uwire/esm/index.js').toString(); -const uhandlers = readFileSync('./node_modules/uhandlers/esm/index.js').toString(); - -const init = readFileSync('./esm/init.js').toString(); -const handlers = readFileSync('./esm/handlers.js').toString(); -const rabbit = readFileSync('./esm/rabbit.js').toString(); -const index = readFileSync('./esm/index.js').toString(); - -const outcome = [ - dropIE(utils).replace(/^\{.+\};$/gm, ''), - dropIE(uwire), - dropIE(uhandlers), - dropIE(handlers), - dropIE(rabbit), - dropIE(index).replace(/\bcache\b/g, '_cache').replace(/^\{/m, 'return {') -]; - -writeFileSync( - './esm/init.js', - init.replace( - /\/\*\*start\*\*\/[\s\S]*?\/\*\*end\*\*\//, - `/**start**/\n${outcome.join('\n')}\n/**end**/` - ) -); diff --git a/rollup/async.config.js b/rollup/async.config.js deleted file mode 100644 index c272a12..0000000 --- a/rollup/async.config.js +++ /dev/null @@ -1,17 +0,0 @@ -import {nodeResolve} from '@rollup/plugin-node-resolve'; -import terser from '@rollup/plugin-terser'; - -export default { - input: './esm/async.js', - plugins: [ - nodeResolve(), - terser() - ], - output: { - esModule: false, - exports: 'named', - file: './async.js', - format: 'iife', - name: 'uhtml' - } -}; diff --git a/rollup/babel.config.js b/rollup/babel.config.js deleted file mode 100644 index 83bb631..0000000 --- a/rollup/babel.config.js +++ /dev/null @@ -1,15 +0,0 @@ -import {nodeResolve} from '@rollup/plugin-node-resolve'; - -export default { - input: './esm/index.js', - plugins: [ - nodeResolve() - ], - output: { - esModule: false, - exports: 'named', - file: './index.js', - format: 'iife', - name: 'uhtml' - } -}; diff --git a/rollup/es.config.js b/rollup/es.config.js index 22a2cd3..cbeeaf5 100644 --- a/rollup/es.config.js +++ b/rollup/es.config.js @@ -1,17 +1,35 @@ import {nodeResolve} from '@rollup/plugin-node-resolve'; import terser from '@rollup/plugin-terser'; -export default { - input: './esm/index.js', - plugins: [ - nodeResolve(), - terser() - ], - output: { - esModule: false, - exports: 'named', - file: './es.js', - format: 'iife', - name: 'uhtml' +const plugins = [ + nodeResolve(), +].concat( + process.env.NO_MIN ? [] : [terser()] +); + +export default [ + { + plugins, + input: './esm/index.js', + output: { + esModule: true, + file: './index.js', + }, + }, + { + plugins, + input: './esm/keyed.js', + output: { + esModule: true, + file: './keyed.js', + }, + }, + { + plugins, + input: './esm/node.js', + output: { + esModule: true, + file: './node.js', + }, } -}; +]; diff --git a/rollup/esm.config.js b/rollup/esm.config.js deleted file mode 100644 index 4320c39..0000000 --- a/rollup/esm.config.js +++ /dev/null @@ -1,15 +0,0 @@ -import {nodeResolve} from '@rollup/plugin-node-resolve'; -import terser from '@rollup/plugin-terser'; - -export default { - input: './esm/index.js', - plugins: [ - nodeResolve(), - terser() - ], - output: { - esModule: false, - file: './esm.js', - format: 'module' - } -}; diff --git a/rollup/init.config.js b/rollup/init.config.js deleted file mode 100644 index 1b36549..0000000 --- a/rollup/init.config.js +++ /dev/null @@ -1,15 +0,0 @@ -import {nodeResolve} from '@rollup/plugin-node-resolve'; -import terser from '@rollup/plugin-terser'; - -export default { - input: './esm/init.js', - plugins: [ - nodeResolve(), - terser() - ], - output: { - esModule: false, - file: './init.js', - format: 'module' - } -}; diff --git a/test/coverage.js b/test/coverage.js index 6f4db77..4760079 100644 --- a/test/coverage.js +++ b/test/coverage.js @@ -3,15 +3,17 @@ const {DOMParser, HTMLElement} = require('linkedom'); const document = (new DOMParser).parseFromString('', 'text/html'); globalThis.document = document; +globalThis.DocumentFragment = document.createDocumentFragment().constructor; -const {render, html, svg} = require('../cjs'); +const {html: htmlNode} = require('../cjs/node.js'); +const {render, html, svg} = require('../cjs/index'); const {Event} = document.defaultView; const {body} = document; -const elementA = html.node`
foo
`; -const elementB = html.node` +const elementA = htmlNode`
foo
`; +const elementB = htmlNode`
bar
`; @@ -42,7 +44,7 @@ render(body, html`this is a ${1} ${2} ${3}`); render(body, html`this is a ${1}`); let div = document.createElement('div'); -render(div, html.node`this is a test`); +render(div, htmlNode`this is a test`); render(div, html.for(body)`this is a test`); render(div, html.for(body, 1)`this is a test`); render(div, () => html.for(body)`this is a test`); diff --git a/test/index.js b/test/index.js new file mode 100644 index 0000000..e69de29 diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..04a45d4 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "module": "NodeNext", + "target": "esnext", + "moduleResolution": "nodenext", + "allowJs": true, + "declaration": true, + "emitDeclarationOnly": true, + "declarationDir": "types" + }, + "include": [ + "esm/index.js", + "esm/exports.js", + "esm/xworker.js" + ] +} diff --git a/types/create-content.d.ts b/types/create-content.d.ts new file mode 100644 index 0000000..35674ec --- /dev/null +++ b/types/create-content.d.ts @@ -0,0 +1,2 @@ +declare function _default(text: string, xml: boolean): DocumentFragment; +export default _default; diff --git a/types/creator.d.ts b/types/creator.d.ts new file mode 100644 index 0000000..9c659d2 --- /dev/null +++ b/types/creator.d.ts @@ -0,0 +1,2 @@ +declare function _default(parse: (template: TemplateStringsArray, values: any[]) => import("./parser.js").Resolved): (template: any, values: any) => import("./literals.js").Parsed; +export default _default; diff --git a/types/handler.d.ts b/types/handler.d.ts new file mode 100644 index 0000000..daf5a68 --- /dev/null +++ b/types/handler.d.ts @@ -0,0 +1,22 @@ +/** + * @template T + * @this {import("./literals.js").HoleDetails} + * @param {Node} node + * @param {T} value + * @returns {T} + */ +export function hole(this: import("./literals.js").HoleDetails, node: Node, value: T): T; +export class hole { + /** + * @template T + * @this {import("./literals.js").HoleDetails} + * @param {Node} node + * @param {T} value + * @returns {T} + */ + constructor(this: import("./literals.js").HoleDetails, node: Node, value: T); + n: Object; +} +export function array(node: Node, value: T, _: string, prev: Node[]): T; +export function attribute(element: Element, name: string): (element: Element, value: T, name: string) => T; +export function text(element: Element, value: T): T; diff --git a/types/index.d.ts b/types/index.d.ts new file mode 100644 index 0000000..67c7fe3 --- /dev/null +++ b/types/index.d.ts @@ -0,0 +1,8 @@ +export type Value = import("./literals.js").Value; +import { Hole } from './rabbit.js'; +import render from './render-hole.js'; +/** @type {(template: TemplateStringsArray, ...values:Value[]) => Hole} A tag to render HTML content. */ +export const html: (template: TemplateStringsArray, ...values: Value[]) => Hole; +/** @type {(template: TemplateStringsArray, ...values:Value[]) => Hole} A tag to render SVG content. */ +export const svg: (template: TemplateStringsArray, ...values: Value[]) => Hole; +export { Hole, render }; diff --git a/types/literals.d.ts b/types/literals.d.ts new file mode 100644 index 0000000..814d9da --- /dev/null +++ b/types/literals.d.ts @@ -0,0 +1,82 @@ +export function cel(c: PersistentFragment, e: Entry[], l: number): { + c: import("./persistent-fragment.js").PersistentFragment; + e: Entry[]; + l: number; +}; +/** + * @typedef {Object} HoleDetails + * @property {null | Node | PersistentFragment} n the current live node, if any and not the `t` one + */ +/** @type {() => HoleDetails} */ +export const comment: () => HoleDetails; +export function detail(v: any, u: Function, t: Node, n: string): Detail; +export function entry(t: Type, p: number[], u: Function, n?: string): Entry; +export function cache(s: Cache[]): Cache; +export function parsed(n: Node | PersistentFragment, d: Detail[]): Parsed; +export type ATTRIBUTE_NODE = 2; +export type TEXT_NODE = 3; +export type COMMENT_NODE = 8; +export type Type = ATTRIBUTE_NODE | TEXT_NODE | COMMENT_NODE; +export type PersistentFragment = import("./persistent-fragment.js").PersistentFragment; +export type Hole = import("./rabbit.js").Hole; +export type Target = Node | Element | PersistentFragment; +export type Value = null | undefined | string | number | boolean | Hole; +export type DOMValue = null | undefined | string | number | boolean | Node | Element | PersistentFragment; +export type Entry = { + type: Type; + path: number[]; + update: Function; + name: string; +}; +export type HoleDetails = { + /** + * the current live node, if any and not the `t` one + */ + n: null | Node | PersistentFragment; +}; +export type Detail = { + /** + * the current value of the interpolation / hole + */ + v: any; + /** + * the callback to update the value + */ + u: Function; + /** + * the target comment node or element + */ + t: Node; + /** + * the name of the attribute, if any + */ + n: string; +}; +export type Cache = { + /** + * the stack of caches per each interpolation / hole + */ + s: Cache[]; + /** + * the cached template + */ + t: null | TemplateStringsArray; + /** + * the node returned when parsing the template + */ + n: null | Node | PersistentFragment; + /** + * the list of updates to perform + */ + d: Detail[]; +}; +export type Parsed = { + /** + * the returned node after parsing the template + */ + n: Node | PersistentFragment; + /** + * the list of details to update the node + */ + d: Detail[]; +}; diff --git a/types/parser.d.ts b/types/parser.d.ts new file mode 100644 index 0000000..74431ce --- /dev/null +++ b/types/parser.d.ts @@ -0,0 +1,9 @@ +declare function _default(xml: boolean): (template: TemplateStringsArray, values: any[]) => Resolved; +export default _default; +export type Entry = import("./literals.js").Entry; +export type Resolved = { + content: DocumentFragment; + entries: Entry[]; + updates: Function[]; + length: number; +}; diff --git a/types/persistent-fragment.d.ts b/types/persistent-fragment.d.ts new file mode 100644 index 0000000..3b410d8 --- /dev/null +++ b/types/persistent-fragment.d.ts @@ -0,0 +1,10 @@ +export function diffFragment(node: Node, operation: 1 | 0 | -0 | -1): Node; +/** @extends {DocumentFragment} */ +export class PersistentFragment extends DocumentFragment { + constructor(fragment: any); + get firstChild(): any; + get lastChild(): any; + replaceWith(node: any): void; + valueOf(): this; + #private; +} diff --git a/types/rabbit.d.ts b/types/rabbit.d.ts new file mode 100644 index 0000000..252e6ce --- /dev/null +++ b/types/rabbit.d.ts @@ -0,0 +1,14 @@ +export function unroll(cache: import("./literals.js").Cache, { s: stack, t: template, v: values }: Hole): Node; +/** + * Holds all details needed to render the content on a render. + * @constructor + * @param {boolean} svg The content type. + * @param {TemplateStringsArray} template The template literals used to the define the content. + * @param {any[]} values Zero, one, or more interpolated values to render. + */ +export class Hole { + constructor(svg: any, template: any, values: any); + s: any; + t: any; + v: any; +} diff --git a/types/range.d.ts b/types/range.d.ts new file mode 100644 index 0000000..d02347f --- /dev/null +++ b/types/range.d.ts @@ -0,0 +1,2 @@ +declare function _default(firstChild: Node | Element, lastChild: Node | Element, preserve: boolean): Element | Node; +export default _default; diff --git a/types/render-hole.d.ts b/types/render-hole.d.ts new file mode 100644 index 0000000..b1e45c0 --- /dev/null +++ b/types/render-hole.d.ts @@ -0,0 +1,3 @@ +declare function _default(where: T, what: () => Hole | Hole): T; +export default _default; +export type Hole = import("./rabbit.js").Hole; diff --git a/types/utils.d.ts b/types/utils.d.ts new file mode 100644 index 0000000..3abe2a5 --- /dev/null +++ b/types/utils.d.ts @@ -0,0 +1,4 @@ +export const empty: any[]; +export function newRange(): Range; +export function set(map: Map | WeakMap, key: any, value: T): T; +export const isArray: (arg: any) => arg is any[]; From 8d589bf4c4257a133ccf43129bbafe144429c708 Mon Sep 17 00:00:00 2001 From: webreflection Date: Sun, 12 Nov 2023 14:43:58 +0100 Subject: [PATCH 2/2] 4.0.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index fdd13be..efbb492 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "uhtml", - "version": "3.2.2", + "version": "4.0.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "uhtml", - "version": "3.2.2", + "version": "4.0.0", "license": "MIT", "dependencies": { "@webreflection/uparser": "^0.3.3", diff --git a/package.json b/package.json index b049aed..f5066d6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "uhtml", - "version": "3.2.2", + "version": "4.0.0", "description": "A micro HTML/SVG render", "main": "./cjs/index.js", "scripts": {