├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── babel.config.json ├── cjs ├── diff.js ├── dist │ ├── preact.js │ ├── signal.js │ ├── solid.js │ └── usignal.js ├── html-escaper.js ├── index.js ├── package.json └── ssr.js ├── es.js ├── esm ├── diff.js ├── dist │ ├── preact.js │ ├── signal.js │ ├── solid.js │ └── usignal.js ├── html-escaper.js ├── index.js └── ssr.js ├── index.js ├── package-lock.json ├── package.json ├── preact.js ├── rollup ├── babel.config.js ├── dist.js ├── es.config.js ├── preact.config.js ├── signal.config.js ├── solid.config.js └── usignal.config.js ├── signal.js ├── solid.js ├── test ├── array.html ├── array.js ├── array.jsx ├── base.js ├── base.jsx ├── children.html ├── children.js ├── children.jsx ├── counter.html ├── counter.js ├── counter.jsx ├── dist │ ├── preact.html │ ├── preact.js │ ├── preact.jsx │ ├── signal.html │ ├── signal.js │ ├── signal.jsx │ ├── solid.html │ ├── solid.js │ ├── solid.jsx │ ├── usignal.html │ ├── usignal.js │ └── usignal.jsx ├── esx-babel.js ├── esx.html ├── esx.js ├── esx.jsx ├── index.html ├── index.js ├── index.jsx ├── ssr.js ├── ssr.jsx ├── uhooks.html ├── uhooks.js └── uhooks.jsx └── usignal.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .nyc_output 3 | coverage/ 4 | node_modules/ 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .nyc_output 3 | .eslintrc.json 4 | .travis.yml 5 | .github 6 | babel.config.json 7 | coverage/ 8 | node_modules/ 9 | rollup/ 10 | test/ 11 | tsconfig.json 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2023, Andrea Giammarchi, @WebReflection 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 10 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 11 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 12 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 13 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE 14 | OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 15 | PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # udomsay 2 | 3 | **EXPERIMENTAL** ⚠ **Social Media Image from [Know Your Meme](https://knowyourmeme.com/memes/you-dont-say--3)** 4 | 5 | A stricter, signals driven, **ESX** based library. 6 | 7 | ## What 8 | 9 | This library includes, in about *2.2Kb*, logic to parse [a specialized form of JSX](https://github.com/ungap/babel-plugin-transform-esx#readme), or its [template literal based variant](https://github.com/ungap/esx#reade), and use signals from various authors, handling rendering automatically and avoiding side effects when used as *SSR*. 10 | 11 | ## How 12 | 13 | Given the following `counter.jsx` file: 14 | ```js 15 | // grab signals from various libaries, here the simplest I know 16 | import {Signal, signal, effect} from 'https://unpkg.com/@webreflection/signal'; 17 | 18 | // import the `createRender` utility 19 | import createRender from 'https://unpkg.com/udomsay'; 20 | const render = createRender({Signal, effect}); 21 | 22 | // Counter Component example 23 | function Counter({clicks}) { 24 | return ( 25 |
26 | 27 | {clicks} 28 | 29 |
30 | ); 31 | } 32 | 33 | render( 34 | , 35 | document.body 36 | ); 37 | ``` 38 | 39 | Providing the following `babel.config.json` transformer: 40 | ```json 41 | { 42 | "plugins": [ 43 | ["@ungap/babel-plugin-transform-esx"] 44 | ] 45 | } 46 | ``` 47 | 48 | The result can be **[tested in CodePen.io](https://codepen.io/WebReflection/pen/vYrYxKY)**. 49 | 50 | ## Custom Signals Library 51 | 52 | Bringing in your favorite signals libraries is almost a no brainer with *udomsay*: check the fews already tested within this project! 53 | 54 | * **[preact](https://www.npmjs.com/package/@preact/signals-core)**, implemented through [this file](./esm/dist/preact.js) and [live tested here](https://webreflection.github.io/udomsay/test/dist/preact.html). Try `import {createRender, signal} from "udomsay/preact"` yourself! 55 | * **[@webreflection/signal](https://www.npmjs.com/package/@webreflection/signal)**, implemented through [this file](./esm/dist/signal.js) and [live tested here](https://webreflection.github.io/udomsay/test/dist/signal.html). Try `import {createRender, signal} from "udomsay/signal"` yourself! 56 | * **[solid-js](https://www.npmjs.com/package/solid-js)**, implemented through [this file](./esm/dist/solid.js) and [live tested here](https://webreflection.github.io/udomsay/test/dist/solid.html). Try `import {createRender, createSignal} from "udomsay/solid"` yourself! 57 | 58 | ### Current udomsay ESX Interpolations Rules 59 | 60 | Following the current set of stricter rules around *JSX* usage and how to avoid/prevent issues: 61 | 62 | * if an interpolation contains a *primitive* value (e.g. a string, a number, a boolean or undefined) or a *signal* which value is primitive, every future update of such interpolation will *expect a primitive* value or *signal* carrying a primitive value. Conditional primitives values or signals are fine, but `{condition ? "string" : }` is **not supported**. 63 | * if a *signal* is used as interpolation and its value is *primitive*, an *effect* is used to update its value on the target text node *only if the signal changes or its value did*. This allows to fine-tune and confine updates per each component or even regular element node, without needing to re-trigger the outer component logic. 64 | * if a *signal* is used as interpolation and its value is *not primitive*, every future update of such interpolation will *expect a signal*. Conditional signals are fine, but `{condition ? signal : ( || "string")}` is **not supported**. 65 | * if an interpolation contains an *array* of items, every future update of such interpolation will *expect an array*. Conditional arrays are fine, but `{condition ? [..items] : ( || "string")}` is **not supported**. 66 | 67 | ### Library Goals 68 | 69 | The goal of this library is: 70 | 71 | * explore if [a better instrumented JSX](https://webreflection.medium.com/jsx-is-inefficient-by-default-but-d1122c992399) can actually help performance and memory consumption 72 | * avoid the need of *vDOM*, still [diffing](https://github.com/WebReflection/udomdiff#readme) when necessary through *arrays* in interpolations 73 | * create once and map on the fly (JIT) templates for both nodes, fragments, and components 74 | * fine-tune operations per each interpolation, such as spread properties VS known static properties, conditional holes or signals and, last but not least, arrays of items 75 | -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | ["@ungap/transform-esx", { "polyfill": "inline" }] 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /cjs/diff.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* (c) Andrea Giammarchi - ISC */ 3 | // @see https://github.com/WebReflection/udomdiff 4 | const diff = (a, b, before) => { 5 | const {parentNode} = before; 6 | const bLength = b.length; 7 | let aEnd = a.length; 8 | let bEnd = bLength; 9 | let aStart = 0; 10 | let bStart = 0; 11 | let map = null; 12 | while (aStart < aEnd || bStart < bEnd) { 13 | // append head, tail, or nodes in between: fast path 14 | if (aEnd === aStart) { 15 | // we could be in a situation where the rest of nodes that 16 | // need to be added are not at the end, and in such case 17 | // the node to `insertBefore`, if the index is more than 0 18 | // must be retrieved, otherwise it's gonna be the first item. 19 | const node = bEnd < bLength ? 20 | (bStart ? 21 | b[bStart - 1].nextSibling : 22 | b[bEnd - bStart]) : 23 | before; 24 | while (bStart < bEnd) 25 | parentNode.insertBefore(b[bStart++], node); 26 | } 27 | // remove head or tail: fast path 28 | else if (bEnd === bStart) { 29 | while (aStart < aEnd) { 30 | // remove the node only if it's unknown or not live 31 | if (!map || !map.has(a[aStart])) 32 | a[aStart].remove(); 33 | aStart++; 34 | } 35 | } 36 | // same node: fast path 37 | else if (a[aStart] === b[bStart]) { 38 | aStart++; 39 | bStart++; 40 | } 41 | // same tail: fast path 42 | else if (a[aEnd - 1] === b[bEnd - 1]) { 43 | aEnd--; 44 | bEnd--; 45 | } 46 | // The once here single last swap "fast path" has been removed in v1.1.0 47 | // https://github.com/WebReflection/udomdiff/blob/single-final-swap/esm/index.js#L69-L85 48 | // reverse swap: also fast path 49 | else if ( 50 | a[aStart] === b[bEnd - 1] && 51 | b[bStart] === a[aEnd - 1] 52 | ) { 53 | // this is a "shrink" operation that could happen in these cases: 54 | // [1, 2, 3, 4, 5] 55 | // [1, 4, 3, 2, 5] 56 | // or asymmetric too 57 | // [1, 2, 3, 4, 5] 58 | // [1, 2, 3, 5, 6, 4] 59 | const node = a[--aEnd].nextSibling; 60 | parentNode.insertBefore( 61 | b[bStart++], 62 | a[aStart++].nextSibling 63 | ); 64 | parentNode.insertBefore(b[--bEnd], node); 65 | // mark the future index as identical (yeah, it's dirty, but cheap 👍) 66 | // The main reason to do this, is that when a[aEnd] will be reached, 67 | // the loop will likely be on the fast path, as identical to b[bEnd]. 68 | // In the best case scenario, the next loop will skip the tail, 69 | // but in the worst one, this node will be considered as already 70 | // processed, bailing out pretty quickly from the map index check 71 | a[aEnd] = b[bEnd]; 72 | } 73 | // map based fallback, "slow" path 74 | else { 75 | // the map requires an O(bEnd - bStart) operation once 76 | // to store all future nodes indexes for later purposes. 77 | // In the worst case scenario, this is a full O(N) cost, 78 | // and such scenario happens at least when all nodes are different, 79 | // but also if both first and last items of the lists are different 80 | if (!map) { 81 | map = new Map; 82 | let i = bStart; 83 | while (i < bEnd) 84 | map.set(b[i], i++); 85 | } 86 | // if it's a future node, hence it needs some handling 87 | if (map.has(a[aStart])) { 88 | // grab the index of such node, 'cause it might have been processed 89 | const index = map.get(a[aStart]); 90 | // if it's not already processed, look on demand for the next LCS 91 | if (bStart < index && index < bEnd) { 92 | let i = aStart; 93 | // counts the amount of nodes that are the same in the future 94 | let sequence = 1; 95 | while (++i < aEnd && i < bEnd && map.get(a[i]) === (index + sequence)) 96 | sequence++; 97 | // effort decision here: if the sequence is longer than replaces 98 | // needed to reach such sequence, which would brings again this loop 99 | // to the fast path, prepend the difference before a sequence, 100 | // and move only the future list index forward, so that aStart 101 | // and bStart will be aligned again, hence on the fast path. 102 | // An example considering aStart and bStart are both 0: 103 | // a: [1, 2, 3, 4] 104 | // b: [7, 1, 2, 3, 6] 105 | // this would place 7 before 1 and, from that time on, 1, 2, and 3 106 | // will be processed at zero cost 107 | if (sequence > (index - bStart)) { 108 | const node = a[aStart]; 109 | while (bStart < index) 110 | parentNode.insertBefore(b[bStart++], node); 111 | } 112 | // if the effort wasn't good enough, fallback to a replace, 113 | // moving both source and target indexes forward, hoping that some 114 | // similar node will be found later on, to go back to the fast path 115 | else { 116 | parentNode.replaceChild( 117 | b[bStart++], 118 | a[aStart++] 119 | ); 120 | } 121 | } 122 | // otherwise move the source forward, 'cause there's nothing to do 123 | else 124 | aStart++; 125 | } 126 | // this node has no meaning in the future list, so it's more than safe 127 | // to remove it, and check the next live node out instead, meaning 128 | // that only the live list index should be forwarded 129 | else 130 | a[aStart++].remove(); 131 | } 132 | } 133 | return b; 134 | }; 135 | exports.diff = diff; 136 | -------------------------------------------------------------------------------- /cjs/dist/preact.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const {Signal, effect} = require('@preact/signals-core'); 3 | (m => Object.keys(m).map(k => k !== 'default' && (exports[k] = m[k]))) 4 | (require('@preact/signals-core')); 5 | 6 | const $ = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('../index.js')); 7 | const createRender = (options = {}) => 8 | $({...options, Signal, effect, isSignal: void 0}); 9 | exports.createRender = createRender; 10 | -------------------------------------------------------------------------------- /cjs/dist/signal.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const {Signal, effect} = require('@webreflection/signal'); 3 | (m => Object.keys(m).map(k => k !== 'default' && (exports[k] = m[k]))) 4 | (require('@webreflection/signal')); 5 | 6 | const $ = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('../index.js')); 7 | const createRender = (options = {}) => 8 | $({...options, Signal, effect, isSignal: void 0}); 9 | exports.createRender = createRender; 10 | -------------------------------------------------------------------------------- /cjs/dist/solid.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const {createSignal: $, createEffect, untrack} = require('solid-js'); 3 | (m => Object.keys(m).map(k => k !== 'default' && (exports[k] = m[k]))) 4 | (require('solid-js')); 5 | 6 | const _ = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('../index.js')); 7 | 8 | const signals = new WeakSet; 9 | const noop = () => {}; 10 | 11 | const createSignal = (value, ...rest) => { 12 | const [signal, update] = $(value, ...rest); 13 | signals.add(signal); 14 | return [signal, update]; 15 | }; 16 | exports.createSignal = createSignal; 17 | 18 | const createRender = (options = {}) => _({ 19 | ...options, 20 | effect: (...args) => (createEffect(...args), noop), 21 | getPeek: s => untrack(s), 22 | getValue: s => s(), 23 | isSignal: s => signals.has(s) 24 | }); 25 | exports.createRender = createRender; 26 | -------------------------------------------------------------------------------- /cjs/dist/usignal.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const {Signal, effect} = require('usignal'); 3 | (m => Object.keys(m).map(k => k !== 'default' && (exports[k] = m[k]))) 4 | (require('usignal')); 5 | 6 | const $ = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('../index.js')); 7 | const createRender = (options = {}) => 8 | $({...options, Signal, effect, isSignal: void 0}); 9 | exports.createRender = createRender; 10 | -------------------------------------------------------------------------------- /cjs/html-escaper.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | // @see https://github.com/WebReflection/html-escaper#readme 3 | const es = /[&<>'"]/g; 4 | const cape = pe => esca[pe]; 5 | const esca = { 6 | '&': '&', 7 | '<': '<', 8 | '>': '>', 9 | "'": ''', 10 | '"': '"' 11 | }; 12 | 13 | const {replace} = ''; 14 | 15 | const escape = value => replace.call(value, es, cape); 16 | exports.escape = escape; 17 | -------------------------------------------------------------------------------- /cjs/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /*! (c) Andrea Giammarchi - ISC */ 3 | 4 | const EMPTY = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('@webreflection/empty/array')); 5 | const noop = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('@webreflection/empty/function')); 6 | 7 | const {Token} = require('@ungap/esx'); 8 | const {diff} = require('./diff.js'); 9 | 10 | const {isArray} = Array; 11 | const {entries, getPrototypeOf, prototype: {isPrototypeOf}} = Object; 12 | 13 | const { 14 | COMPONENT, 15 | ELEMENT, 16 | FRAGMENT, 17 | INTERPOLATION, 18 | STATIC 19 | } = Token; 20 | 21 | const UDOMSAY = '🙊'; 22 | 23 | // generic utils 24 | const asChildNodes = ({childNodes}) => ({childNodes: [...childNodes]}); 25 | const getChild = ({childNodes}, i) => childNodes[i]; 26 | const getToken = ({children}, i) => children[i]; 27 | const invoke = ({value, properties, children}) => value(properties, ...children); 28 | const isKey = ({name}) => name === 'key'; 29 | const reachChild = (c, {content}) => c.reduce(getChild, content); 30 | const reachToken = (c, token) => c.reduce(getToken, token); 31 | const setData = (node, value) => { 32 | const data = value == null ? '' : String(value); 33 | if (data !== node.data) 34 | node.data = data; 35 | }; 36 | 37 | /** 38 | * @typedef {Object} RenderOptions utilities to use while rendering. 39 | * @prop {Document} [document] the default document to use. By default it's the global one. 40 | * @prop {[string, (node:Element, current:any, previous:any) => void][]} [plugins] a list of plugins to deal with, 41 | * used with attributes, example: `["stuff", (node, curr, prev) => { ... }]` 42 | * @prop {(fn:function) => function} [effect] an utility to create effects on components. 43 | * It must return a dispose utility to drop previous effect. 44 | * @prop {(s:any) => any} [getPeek] an utility to retrieve a Signal value without side-effects. 45 | * @prop {(s:any) => any} [getValue] an utility to retrieve a Signal value. 46 | * @prop {(s:any) => boolean} [isSignal] an utility to know if a value is a Signal. 47 | * @prop {function} [Signal] an optional signal constructor used to trap-check. 48 | * @prop {function} [diff] an optional function to diff nodes, not available in SSR. 49 | * `isSignal(ref)` utility whenever the `isSignal` field has not been provided. 50 | */ 51 | 52 | /** 53 | * Return a `render(what, where)` utility able to deal with provided options. 54 | * @param {RenderOptions} options 55 | */ 56 | module.exports = (options = {}) => { 57 | const document = options.document || globalThis.document; 58 | const plugins = new Map(options.plugins || []); 59 | const considerPlugins = !!plugins.size; 60 | const differ = options.diff || diff; 61 | const effect = options.effect || (fn => (fn(), noop)); 62 | const getPeek = options.getPeek || (s => s.peek()); 63 | const getValue = options.getValue || (s => s.value); 64 | const isSignal = options.isSignal || ( 65 | options.Signal ? 66 | isPrototypeOf.bind(options.Signal.prototype) : 67 | () => false 68 | ); 69 | 70 | const text = value => document.createTextNode(value); 71 | const comment = () => document.createComment(UDOMSAY); 72 | 73 | const getChildrenID = (node, i) => { 74 | let IDs = views.get(node); 75 | if (!IDs) views.set(node, IDs = {}); 76 | return IDs[i] || (IDs[i] = {}); 77 | }; 78 | const getComponentView = (view, component) => { 79 | view.dispose(); 80 | const dispose = effect(() => { 81 | const token = invoke(component); 82 | if (token.id !== view.id) 83 | view = getView(view, token, false); 84 | else 85 | view.update(token); 86 | }); 87 | view.dispose = dispose; 88 | return view; 89 | }; 90 | const getNewView = (view, token, createEffect) => { 91 | view.dispose(); 92 | view = new View(token); 93 | if (createEffect) 94 | view.dispose = effect(() => view.update(token)); 95 | else 96 | view.update(token); 97 | return view; 98 | }; 99 | const getView = (view, token, createEffect) => token.type === COMPONENT ? 100 | getComponentView(view, token) : 101 | getNewView(view, token, createEffect) 102 | ; 103 | 104 | class View { 105 | constructor(token, shouldParse = true) { 106 | const [updates, content] = shouldParse ? parse(token) : [EMPTY, null]; 107 | this._ = shouldParse && token.type === FRAGMENT; 108 | this.id = token.id; 109 | this.updates = updates; 110 | this.content = content; 111 | this.dispose = noop; 112 | } 113 | get $() { 114 | const {content, _} = this; 115 | if (_) { 116 | this._ = !_; 117 | return (this.content = asChildNodes(content)).childNodes; 118 | } 119 | return [content]; 120 | } 121 | update(token) { 122 | for (const update of this.updates) 123 | update.call(this, token); 124 | } 125 | } 126 | 127 | const defaultView = new View({id: null}, false); 128 | const defaultEntry = {id: null, view: defaultView}; 129 | 130 | const views = new WeakMap; 131 | let isToken; 132 | 133 | const tokens = new WeakMap; 134 | const parse = token => { 135 | let info = tokens.get(token.id); 136 | if (!info) { 137 | const updates = []; 138 | const content = mapToken(token, updates, [], EMPTY, false); 139 | tokens.set(token.id, info = [updates, content]); 140 | } 141 | const [updates, content] = info; 142 | return [updates.slice(), content.cloneNode(true)]; 143 | }; 144 | 145 | const setAttribute = (node, key, value, set) => { 146 | if (set) 147 | node.setAttribute(key, value); 148 | else 149 | node[key] = value; 150 | }; 151 | 152 | const asAttribute = (node, key, value, prev, set) => { 153 | if (isSignal(value)) { 154 | const dispose = UDOMSAY + key; 155 | if (dispose in prev) 156 | prev[dispose](); 157 | prev[dispose] = effect(() => { 158 | setAttribute(node, key, getValue(value), set); 159 | }); 160 | } 161 | else 162 | setAttribute(node, key, value, set); 163 | }; 164 | 165 | const setProperty = (node, key, value, prev) => { 166 | if (considerPlugins && plugins.has(key)) 167 | plugins.get(key)(node, value, prev); 168 | else if (prev[key] !== value) { 169 | prev[key] = value; 170 | switch (key) { 171 | case 'class': 172 | key += 'Name'; 173 | case 'className': 174 | case 'textContent': 175 | asAttribute(node, key, value, prev, false); 176 | break; 177 | case 'ref': 178 | value.current = node; 179 | break; 180 | default: 181 | if (key.startsWith('on')) 182 | node[key.toLowerCase()] = value; 183 | else if (key in node) 184 | asAttribute(node, key, value, prev, false); 185 | else { 186 | if (value == null) 187 | node.removeAttribute(key); 188 | else 189 | asAttribute(node, key, value, prev, true); 190 | } 191 | break; 192 | } 193 | } 194 | }; 195 | 196 | const handleAttributes = (a, c, i) => function (token) { 197 | const prev = {}; 198 | const node = reachChild(c, this); 199 | (this.updates[i] = token => { 200 | const {attributes} = reachToken(c, token); 201 | for (const index of a) { 202 | const entry = attributes[index]; 203 | const {type, value} = entry; 204 | if (type < 2) 205 | setProperty(node, entry.name, value, prev); 206 | else { 207 | for (const [name, v] of entries(value)) 208 | setProperty(node, name, v, prev); 209 | } 210 | } 211 | })(token); 212 | }; 213 | 214 | const handleArray = (c, i) => function (token) { 215 | let diffed = EMPTY, findIndex = true, index = -1; 216 | const node = reachChild(c, this); 217 | const keys = new Map; 218 | (this.updates[i] = token => { 219 | const {value} = reachToken(c, token); 220 | const diffing = []; 221 | for (let i = 0; i < value.length; i++) { 222 | const token = value[i]; 223 | if (findIndex) { 224 | findIndex = !findIndex; 225 | index = token.attributes.findIndex(isKey); 226 | } 227 | const key = index < 0 ? i : token.attributes[index].value; 228 | let {id, view} = keys.get(key) || defaultEntry; 229 | if (id !== (token.id || (token.id = getChildrenID(node, i)))) { 230 | view = getView(view, token, false); 231 | keys.set(key, {id: token.id, view}); 232 | } 233 | else 234 | view.update(token); 235 | diffing.push(...view.$); 236 | } 237 | if (diffing.length) 238 | diffed = differ(diffed, diffing, node); 239 | else if(diffed !== EMPTY) { 240 | const range = document.createRange(); 241 | range.setStartBefore(diffed[0]); 242 | range.setEndAfter(diffed[diffed.length - 1]); 243 | range.deleteContents(); 244 | keys.clear(); 245 | diffed = EMPTY; 246 | findIndex = true; 247 | } 248 | })(token); 249 | }; 250 | 251 | const handleComponent = (c, i) => function (token) { 252 | let diffed = EMPTY, view = defaultView; 253 | const node = reachChild(c, this); 254 | (this.updates[i] = token => { 255 | view = getComponentView(view, reachToken(c, token)); 256 | diffed = differ(diffed, view.$, node); 257 | })(token); 258 | }; 259 | 260 | const handleContent = (c, i) => function (token) { 261 | const node = reachChild(c, this); 262 | (this.updates[i] = token => { 263 | setData(node, reachToken(c, token).value); 264 | })(token); 265 | }; 266 | 267 | const handleSignal = (c, i) => function (token) { 268 | let dispose = noop, signal, fx; 269 | const node = reachChild(c, this); 270 | const {value} = reachToken(c, token); 271 | const update = value => { 272 | if (signal !== value) { 273 | dispose(); 274 | signal = value; 275 | dispose = effect(fx); 276 | } 277 | }; 278 | if (isToken(getPeek(value))) { 279 | let diffed = EMPTY, view = defaultView; 280 | fx = () => { 281 | const token = getValue(signal); 282 | view = getView(view, token, false); 283 | diffed = differ(diffed, view.$, node); 284 | }; 285 | } 286 | else { 287 | const t = text(''); 288 | node.replaceWith(t); 289 | fx = () => { setData(t, getValue(signal)) }; 290 | } 291 | this.updates[i] = token => update(reachToken(c, token).value); 292 | update(value); 293 | }; 294 | 295 | const handleToken = (c, i) => function (token) { 296 | let diffed = EMPTY, view = defaultView, id = null; 297 | const node = reachChild(c, this); 298 | (this.updates[i] = token => { 299 | token = reachToken(c, token).value; 300 | if (id !== token.id) { 301 | id = token.id; 302 | // TODO: should this effect instead? 303 | view = getView(view, token, false); 304 | diffed = differ(diffed, view.$, node); 305 | } 306 | else if (token.type === COMPONENT) 307 | view.update(invoke(token)); 308 | else 309 | view.update(token); 310 | })(token); 311 | }; 312 | 313 | const addChildren = ({children}, updates, content, c, svg) => { 314 | for (let i = 0; i < children.length; i++) { 315 | const child = children[i]; 316 | switch (child.type) { 317 | case STATIC: 318 | content.appendChild(text(child.value)); 319 | break; 320 | default: 321 | content.appendChild( 322 | mapToken(children[i], updates, [], c.concat(i), svg) 323 | ); 324 | break; 325 | } 326 | } 327 | }; 328 | 329 | const mapToken = (token, updates, a, c, svg) => { 330 | let callback, content; 331 | const {length} = updates; 332 | type: switch (token.type) { 333 | case INTERPOLATION: { 334 | const {value} = token; 335 | switch (true) { 336 | case isToken(value): 337 | callback = handleToken; 338 | break; 339 | case isArray(value): 340 | callback = handleArray; 341 | break; 342 | case isSignal(value): 343 | callback = handleSignal; 344 | break; 345 | default: { 346 | content = text(''); 347 | updates.push(handleContent(c, length)); 348 | break type; 349 | } 350 | } 351 | } 352 | case COMPONENT: { 353 | content = comment(); 354 | updates.push((callback || handleComponent)(c, length)); 355 | break; 356 | } 357 | case ELEMENT: { 358 | const {attributes, name} = token; 359 | const args = [name]; 360 | const attrs = []; 361 | for (let i = 0; i < attributes.length; i++) { 362 | const entry = attributes[i]; 363 | if (entry.type === INTERPOLATION || entry.dynamic) { 364 | if (!isKey(entry)) 365 | a.push(i); 366 | } 367 | else { 368 | if (entry.name === 'is') 369 | args.push({is: entry.value}); 370 | attrs.push(entry); 371 | } 372 | } 373 | if (a.length) 374 | updates.push(handleAttributes(a, c, length)); 375 | content = svg || (svg = name === 'svg') ? 376 | document.createElementNS('http://www.w3.org/2000/svg', ...args) : 377 | document.createElement(...args); 378 | for (const {name, value} of attrs) 379 | setAttribute(content, name, value, true); 380 | addChildren(token, updates, content, c, svg); 381 | break; 382 | } 383 | case FRAGMENT: { 384 | content = document.createDocumentFragment(); 385 | addChildren(token, updates, content, c, svg); 386 | break; 387 | } 388 | } 389 | return content; 390 | }; 391 | 392 | /** 393 | * Reveal some `token` content into a `DOM` element. 394 | * @param {() => Token | Token} what the token to render 395 | * @param {Element} where the DOM node to render such token 396 | */ 397 | return (what, where) => { 398 | /** @type {Token} */ 399 | const token = typeof what === 'function' ? what() : what; 400 | if (!isToken) isToken = isPrototypeOf.bind(getPrototypeOf(token)); 401 | const view = getView(views.get(where) || defaultView, token, true); 402 | views.set(where, view); 403 | where.replaceChildren(...view.$); 404 | }; 405 | }; 406 | -------------------------------------------------------------------------------- /cjs/package.json: -------------------------------------------------------------------------------- 1 | {"type":"commonjs"} -------------------------------------------------------------------------------- /cjs/ssr.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /*! (c) Andrea Giammarchi - ISC */ 3 | 4 | const {TEXT_ELEMENTS, VOID_ELEMENTS} = require('domconstants'); 5 | const {escape} = require('./html-escaper.js'); 6 | 7 | const EMPTY = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('@webreflection/empty/array')); 8 | const noop = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('@webreflection/empty/function')); 9 | 10 | const createRender = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('./index.js')); 11 | 12 | const cloneAttribute = ({name, value}) => ({name, value}); 13 | 14 | const cloneNode = (current, newNode) => { 15 | for (const node of current) { 16 | switch (node.nodeType) { 17 | case 1: 18 | const element = new Element(node.name); 19 | if (node.attributes !== EMPTY) 20 | element.attributes = node.attributes.map(cloneAttribute); 21 | cloneNode(node, element); 22 | newNode.appendChild(element); 23 | break; 24 | case 3: 25 | newNode.appendChild(new Text(node.data)); 26 | break; 27 | case 11: 28 | for (const inner of node) 29 | cloneNode(inner, newNode); 30 | break; 31 | } 32 | } 33 | return newNode; 34 | }; 35 | 36 | class Text { 37 | constructor(data) { 38 | this.nodeType = 3; 39 | this.parentNode = null; 40 | this.data = data; 41 | } 42 | replaceWith(node) { 43 | const {parentNode} = this; 44 | parentNode.splice( 45 | parentNode.indexOf(this), 46 | 1, 47 | node 48 | ); 49 | } 50 | remove() { 51 | Element.prototype.remove.call(this); 52 | } 53 | toString() { 54 | return TEXT_ELEMENTS.test(this.parentNode?.name) ? 55 | this.data : escape(this.data); 56 | } 57 | } 58 | 59 | class Node extends Array { 60 | get childNodes() { 61 | return this; 62 | } 63 | appendChild(node) { 64 | switch (node.nodeType) { 65 | case 1: 66 | case 3: 67 | node.parentNode = this; 68 | this.push(node); 69 | break; 70 | case 11: 71 | for (const inner of node) 72 | this.appendChild(inner); 73 | break; 74 | } 75 | return node; 76 | } 77 | replaceChild(newChild, oldChild) { 78 | this[this.indexOf(oldChild)] = newChild; 79 | newChild.parentNode = this; 80 | return oldChild; 81 | } 82 | } 83 | 84 | class Fragment extends Node { 85 | constructor() { 86 | super(); 87 | this.nodeType = 11; 88 | this.parentNode = null; 89 | } 90 | cloneNode() { 91 | return cloneNode(this, new Fragment); 92 | } 93 | toString() { 94 | return this.join(''); 95 | } 96 | } 97 | 98 | class Element extends Node { 99 | constructor(name) { 100 | super(); 101 | this.nodeType = 1; 102 | this.name = name; 103 | this.attributes = EMPTY; 104 | } 105 | set className(value) { 106 | this.setAttribute('class', value); 107 | } 108 | set textContent(value) { 109 | for (const node of this.splice(0)) 110 | node.parentNode = null; 111 | this.appendChild(new Text(value)); 112 | } 113 | cloneNode() { 114 | return cloneNode(this, new Element(this.name)); 115 | } 116 | remove() { 117 | const {parentNode} = this; 118 | if (parentNode) { 119 | this.parentNode = null; 120 | parentNode.splice(parentNode.indexOf(this), 1); 121 | } 122 | } 123 | removeAttribute() {} 124 | setAttribute(name, value) { 125 | if (this.attributes === EMPTY) 126 | this.attributes = []; 127 | this.attributes.push({name, value}); 128 | } 129 | toString() { 130 | const {length, name, attributes} = this; 131 | const html = ['<', name]; 132 | for (const {name, value} of attributes) { 133 | if (typeof value === 'boolean') { 134 | if (value) 135 | html.push(` ${name}`); 136 | } 137 | else if (value != null) 138 | html.push(` ${name}="${escape(value)}"`); 139 | } 140 | if (!length && VOID_ELEMENTS.test(name)) 141 | html.push(' />'); 142 | else 143 | html.push('>', ...this, ``); 144 | return html.join(''); 145 | } 146 | } 147 | 148 | class Range { 149 | constructor() { 150 | this.before = null; 151 | this.after = null; 152 | } 153 | setStartBefore(node) { 154 | this.before = node; 155 | } 156 | setEndAfter(node) { 157 | this.after = node; 158 | } 159 | deleteContents() { 160 | const {parentNode} = this.before; 161 | const i = parentNode.indexOf(this.before); 162 | for (const node of parentNode.splice(i, parentNode.indexOf(this.after) - i + 1)) 163 | node.parentNode = null; 164 | } 165 | } 166 | 167 | const document = { 168 | createTextNode: data => new Text(data), 169 | createComment: () => new Text(''), 170 | createDocumentFragment: () => new Fragment, 171 | createElementNS: name => new Element(name), 172 | createElement: (name, options) => { 173 | const element = new Element(name); 174 | if (options && options.is) 175 | element.setAttribute('is', options.is); 176 | return element; 177 | }, 178 | createRange: () => new Range 179 | }; 180 | 181 | const diff = (_, nodes, before) => { 182 | const {parentNode} = before; 183 | const i = parentNode.indexOf(before); 184 | for (const node of nodes) 185 | node.parentNode = parentNode; 186 | parentNode.splice(i, 0, ...nodes); 187 | }; 188 | 189 | /** 190 | * @typedef {Object} RenderOptions utilities to use while rendering. 191 | * @prop {Document} [document] the default document to use. By default it's the global one. 192 | * @prop {[string, (node:Element, current:any, previous:any) => void][]} [plugins] a list of plugins to deal with, 193 | * used with attributes, example: `["stuff", (node, curr, prev) => { ... }]` 194 | * @prop {(fn:function) => function} [effect] an utility to create effects on components. 195 | * It must return a dispose utility to drop previous effect. 196 | * @prop {(s:any) => any} [getPeek] an utility to retrieve a Signal value without side-effects. 197 | * @prop {(s:any) => any} [getValue] an utility to retrieve a Signal value. 198 | * @prop {(s:any) => boolean} [isSignal] an utility to know if a value is a Signal. 199 | * @prop {function} [Signal] an optional signal constructor used to trap-check. 200 | * `isSignal(ref)` utility whenever the `isSignal` field has not been provided. 201 | */ 202 | 203 | /** 204 | * Return a `render(what, where)` utility able to deal with provided options. 205 | * @param {RenderOptions} options 206 | */ 207 | module.exports = (options = {}) => { 208 | const getPeek = options.getPeek || (s => s.peek()); 209 | const render = createRender({ 210 | document, diff, getPeek, 211 | plugins: options.plugins || EMPTY, 212 | effect: (fn => (fn(), noop)), 213 | getValue: getPeek, 214 | isSignal: options.isSignal, 215 | Signal: options.Signal 216 | }); 217 | return (what, where) => { 218 | render(what, { 219 | replaceChildren(...nodes) { 220 | if (typeof where === 'function') { 221 | let html = nodes.join(''); 222 | if (/^(|]+?>)/.test(html)) 223 | html = '' + html; 224 | where(html); 225 | } 226 | else { 227 | for (const node of nodes) 228 | where.write(node.toString()); 229 | } 230 | } 231 | }); 232 | }; 233 | }; 234 | -------------------------------------------------------------------------------- /es.js: -------------------------------------------------------------------------------- 1 | var e=(0,Object.freeze)([]),t=()=>{};const s=(e,t,s)=>{const{parentNode:n}=s,i=t.length;let o=e.length,a=i,c=0,l=0,r=null;for(;cs-l){const i=e[c];for(;le[t],p=({children:e},t)=>e[t],h=({value:e,properties:t,children:s})=>e(t,...s),g=({name:e})=>"key"===e,v=(e,{content:t})=>e.reduce(f,t),b=(e,t)=>e.reduce(p,t),N=(e,t)=>{const s=null==t?"":String(t);s!==e.data&&(e.data=s)}; 2 | /*! (c) Andrea Giammarchi - ISC */var m=(f={})=>{const p=f.document||globalThis.document,m=new Map(f.plugins||[]),T=!!m.size,w=f.diff||s,y=f.effect||(e=>(e(),t)),E=f.getPeek||(e=>e.peek()),k=f.getValue||(e=>e.value),O=f.isSignal||(f.Signal?a.bind(f.Signal.prototype):()=>!1),A=e=>p.createTextNode(e),C=(e,t)=>{let s=B.get(e);return s||B.set(e,s={}),s[t]||(s[t]={})},M=(e,t)=>{e.dispose();const s=y((()=>{const s=h(t);s.id!==e.id?e=S(e,s,!1):e.update(s)}));return e.dispose=s,e},S=(e,t,s)=>t.type===c?M(e,t):((e,t,s)=>(e.dispose(),e=new I(t),s?e.dispose=y((()=>e.update(t))):e.update(t),e))(e,t,s);class I{constructor(s,n=!0){const[i,o]=n?L(s):[e,null];this._=n&&s.type===r,this.id=s.id,this.updates=i,this.content=o,this.dispose=t}get $(){const{content:e,_:t}=this;return t?(this._=!t,(this.content=(({childNodes:e})=>({childNodes:[...e]}))(e)).childNodes):[e]}update(e){for(const t of this.updates)t.call(this,e)}}const x=new I({id:null},!1),P={id:null,view:x},B=new WeakMap;let R;const $=new WeakMap,L=t=>{let s=$.get(t.id);if(!s){const n=[],i=H(t,n,[],e,!1);$.set(t.id,s=[n,i])}const[n,i]=s;return[n.slice(),i.cloneNode(!0)]},W=(e,t,s,n)=>{n?e.setAttribute(t,s):e[t]=s},j=(e,t,s,n,i)=>{if(O(s)){const o="🙊"+t;o in n&&n[o](),n[o]=y((()=>{W(e,t,k(s),i)}))}else W(e,t,s,i)},F=(e,t,s,n)=>{if(T&&m.has(t))m.get(t)(e,s,n);else if(n[t]!==s)switch(n[t]=s,t){case"class":t+="Name";case"className":case"textContent":j(e,t,s,n,!1);break;case"ref":s.current=e;break;default:t.startsWith("on")?e[t.toLowerCase()]=s:t in e?j(e,t,s,n,!1):null==s?e.removeAttribute(t):j(e,t,s,n,!0)}},_=(e,t,s)=>function(n){const o={},a=v(t,this);(this.updates[s]=s=>{const{attributes:n}=b(t,s);for(const t of e){const e=n[t],{type:s,value:c}=e;if(s<2)F(a,e.name,c,o);else for(const[e,t]of i(c))F(a,e,t,o)}})(n)},z=(t,s)=>function(n){let i=e,o=!0,a=-1;const c=v(t,this),l=new Map;(this.updates[s]=s=>{const{value:n}=b(t,s),r=[];for(let e=0;efunction(n){let i=e,o=x;const a=v(t,this);(this.updates[s]=e=>{o=M(o,b(t,e)),i=w(i,o.$,a)})(n)},D=(e,t)=>function(s){const n=v(e,this);(this.updates[t]=t=>{N(n,b(e,t).value)})(s)},U=(s,n)=>function(i){let o,a,c=t;const l=v(s,this),{value:r}=b(s,i),u=e=>{o!==e&&(c(),o=e,c=y(a))};if(R(E(r))){let t=e,s=x;a=()=>{const e=k(o);s=S(s,e,!1),t=w(t,s.$,l)}}else{const e=A("");l.replaceWith(e),a=()=>{N(e,k(o))}}this.updates[n]=e=>u(b(s,e).value),u(r)},V=(t,s)=>function(n){let i=e,o=x,a=null;const l=v(t,this);(this.updates[s]=e=>{e=b(t,e).value,a!==e.id?(a=e.id,o=S(o,e,!1),i=w(i,o.$,l)):e.type===c?o.update(h(e)):o.update(e)})(n)},q=({children:e},t,s,n,i)=>{for(let o=0;o{let a,d;const{length:f}=t;e:switch(e.type){case u:{const{value:s}=e;switch(!0){case R(s):a=V;break;case n(s):a=z;break;case O(s):a=U;break;default:d=A(""),t.push(D(i,f));break e}}case c:d=p.createComment("🙊"),t.push((a||G)(i,f));break;case l:{const{attributes:n,name:a}=e,c=[a],l=[];for(let e=0;e{const s="function"==typeof e?e():e;R||(R=a.bind(o(s)));const n=S(B.get(t)||x,s,!0);B.set(t,n),t.replaceChildren(...n.$)}};export{m as default}; 3 | -------------------------------------------------------------------------------- /esm/diff.js: -------------------------------------------------------------------------------- 1 | /* (c) Andrea Giammarchi - ISC */ 2 | // @see https://github.com/WebReflection/udomdiff 3 | export const diff = (a, b, before) => { 4 | const {parentNode} = before; 5 | const bLength = b.length; 6 | let aEnd = a.length; 7 | let bEnd = bLength; 8 | let aStart = 0; 9 | let bStart = 0; 10 | let map = null; 11 | while (aStart < aEnd || bStart < bEnd) { 12 | // append head, tail, or nodes in between: fast path 13 | if (aEnd === aStart) { 14 | // we could be in a situation where the rest of nodes that 15 | // need to be added are not at the end, and in such case 16 | // the node to `insertBefore`, if the index is more than 0 17 | // must be retrieved, otherwise it's gonna be the first item. 18 | const node = bEnd < bLength ? 19 | (bStart ? 20 | b[bStart - 1].nextSibling : 21 | b[bEnd - bStart]) : 22 | before; 23 | while (bStart < bEnd) 24 | parentNode.insertBefore(b[bStart++], node); 25 | } 26 | // remove head or tail: fast path 27 | else if (bEnd === bStart) { 28 | while (aStart < aEnd) { 29 | // remove the node only if it's unknown or not live 30 | if (!map || !map.has(a[aStart])) 31 | a[aStart].remove(); 32 | aStart++; 33 | } 34 | } 35 | // same node: fast path 36 | else if (a[aStart] === b[bStart]) { 37 | aStart++; 38 | bStart++; 39 | } 40 | // same tail: fast path 41 | else if (a[aEnd - 1] === b[bEnd - 1]) { 42 | aEnd--; 43 | bEnd--; 44 | } 45 | // The once here single last swap "fast path" has been removed in v1.1.0 46 | // https://github.com/WebReflection/udomdiff/blob/single-final-swap/esm/index.js#L69-L85 47 | // reverse swap: also fast path 48 | else if ( 49 | a[aStart] === b[bEnd - 1] && 50 | b[bStart] === a[aEnd - 1] 51 | ) { 52 | // this is a "shrink" operation that could happen in these cases: 53 | // [1, 2, 3, 4, 5] 54 | // [1, 4, 3, 2, 5] 55 | // or asymmetric too 56 | // [1, 2, 3, 4, 5] 57 | // [1, 2, 3, 5, 6, 4] 58 | const node = a[--aEnd].nextSibling; 59 | parentNode.insertBefore( 60 | b[bStart++], 61 | a[aStart++].nextSibling 62 | ); 63 | parentNode.insertBefore(b[--bEnd], node); 64 | // mark the future index as identical (yeah, it's dirty, but cheap 👍) 65 | // The main reason to do this, is that when a[aEnd] will be reached, 66 | // the loop will likely be on the fast path, as identical to b[bEnd]. 67 | // In the best case scenario, the next loop will skip the tail, 68 | // but in the worst one, this node will be considered as already 69 | // processed, bailing out pretty quickly from the map index check 70 | a[aEnd] = b[bEnd]; 71 | } 72 | // map based fallback, "slow" path 73 | else { 74 | // the map requires an O(bEnd - bStart) operation once 75 | // to store all future nodes indexes for later purposes. 76 | // In the worst case scenario, this is a full O(N) cost, 77 | // and such scenario happens at least when all nodes are different, 78 | // but also if both first and last items of the lists are different 79 | if (!map) { 80 | map = new Map; 81 | let i = bStart; 82 | while (i < bEnd) 83 | map.set(b[i], i++); 84 | } 85 | // if it's a future node, hence it needs some handling 86 | if (map.has(a[aStart])) { 87 | // grab the index of such node, 'cause it might have been processed 88 | const index = map.get(a[aStart]); 89 | // if it's not already processed, look on demand for the next LCS 90 | if (bStart < index && index < bEnd) { 91 | let i = aStart; 92 | // counts the amount of nodes that are the same in the future 93 | let sequence = 1; 94 | while (++i < aEnd && i < bEnd && map.get(a[i]) === (index + sequence)) 95 | sequence++; 96 | // effort decision here: if the sequence is longer than replaces 97 | // needed to reach such sequence, which would brings again this loop 98 | // to the fast path, prepend the difference before a sequence, 99 | // and move only the future list index forward, so that aStart 100 | // and bStart will be aligned again, hence on the fast path. 101 | // An example considering aStart and bStart are both 0: 102 | // a: [1, 2, 3, 4] 103 | // b: [7, 1, 2, 3, 6] 104 | // this would place 7 before 1 and, from that time on, 1, 2, and 3 105 | // will be processed at zero cost 106 | if (sequence > (index - bStart)) { 107 | const node = a[aStart]; 108 | while (bStart < index) 109 | parentNode.insertBefore(b[bStart++], node); 110 | } 111 | // if the effort wasn't good enough, fallback to a replace, 112 | // moving both source and target indexes forward, hoping that some 113 | // similar node will be found later on, to go back to the fast path 114 | else { 115 | parentNode.replaceChild( 116 | b[bStart++], 117 | a[aStart++] 118 | ); 119 | } 120 | } 121 | // otherwise move the source forward, 'cause there's nothing to do 122 | else 123 | aStart++; 124 | } 125 | // this node has no meaning in the future list, so it's more than safe 126 | // to remove it, and check the next live node out instead, meaning 127 | // that only the live list index should be forwarded 128 | else 129 | a[aStart++].remove(); 130 | } 131 | } 132 | return b; 133 | }; 134 | -------------------------------------------------------------------------------- /esm/dist/preact.js: -------------------------------------------------------------------------------- 1 | import {Signal, effect} from '@preact/signals-core'; 2 | export * from '@preact/signals-core'; 3 | 4 | import $ from '../index.js'; 5 | export const createRender = (options = {}) => 6 | $({...options, Signal, effect, isSignal: void 0}); 7 | -------------------------------------------------------------------------------- /esm/dist/signal.js: -------------------------------------------------------------------------------- 1 | import {Signal, effect} from '@webreflection/signal'; 2 | export * from '@webreflection/signal'; 3 | 4 | import $ from '../index.js'; 5 | export const createRender = (options = {}) => 6 | $({...options, Signal, effect, isSignal: void 0}); 7 | -------------------------------------------------------------------------------- /esm/dist/solid.js: -------------------------------------------------------------------------------- 1 | import {createSignal as $, createEffect, untrack} from 'solid-js'; 2 | export * from 'solid-js'; 3 | 4 | import _ from '../index.js'; 5 | 6 | const signals = new WeakSet; 7 | const noop = () => {}; 8 | 9 | export const createSignal = (value, ...rest) => { 10 | const [signal, update] = $(value, ...rest); 11 | signals.add(signal); 12 | return [signal, update]; 13 | }; 14 | 15 | export const createRender = (options = {}) => _({ 16 | ...options, 17 | effect: (...args) => (createEffect(...args), noop), 18 | getPeek: s => untrack(s), 19 | getValue: s => s(), 20 | isSignal: s => signals.has(s) 21 | }); 22 | -------------------------------------------------------------------------------- /esm/dist/usignal.js: -------------------------------------------------------------------------------- 1 | import {Signal, effect} from 'usignal'; 2 | export * from 'usignal'; 3 | 4 | import $ from '../index.js'; 5 | export const createRender = (options = {}) => 6 | $({...options, Signal, effect, isSignal: void 0}); 7 | -------------------------------------------------------------------------------- /esm/html-escaper.js: -------------------------------------------------------------------------------- 1 | // @see https://github.com/WebReflection/html-escaper#readme 2 | const es = /[&<>'"]/g; 3 | const cape = pe => esca[pe]; 4 | const esca = { 5 | '&': '&', 6 | '<': '<', 7 | '>': '>', 8 | "'": ''', 9 | '"': '"' 10 | }; 11 | 12 | const {replace} = ''; 13 | 14 | export const escape = value => replace.call(value, es, cape); 15 | -------------------------------------------------------------------------------- /esm/index.js: -------------------------------------------------------------------------------- 1 | /*! (c) Andrea Giammarchi - ISC */ 2 | 3 | import EMPTY from '@webreflection/empty/array'; 4 | import noop from '@webreflection/empty/function'; 5 | 6 | import {Token} from '@ungap/esx'; 7 | import {diff} from './diff.js'; 8 | 9 | const {isArray} = Array; 10 | const {entries, getPrototypeOf, prototype: {isPrototypeOf}} = Object; 11 | 12 | const { 13 | COMPONENT, 14 | ELEMENT, 15 | FRAGMENT, 16 | INTERPOLATION, 17 | STATIC 18 | } = Token; 19 | 20 | const UDOMSAY = '🙊'; 21 | 22 | // generic utils 23 | const asChildNodes = ({childNodes}) => ({childNodes: [...childNodes]}); 24 | const getChild = ({childNodes}, i) => childNodes[i]; 25 | const getToken = ({children}, i) => children[i]; 26 | const invoke = ({value, properties, children}) => value(properties, ...children); 27 | const isKey = ({name}) => name === 'key'; 28 | const reachChild = (c, {content}) => c.reduce(getChild, content); 29 | const reachToken = (c, token) => c.reduce(getToken, token); 30 | const setData = (node, value) => { 31 | const data = value == null ? '' : String(value); 32 | if (data !== node.data) 33 | node.data = data; 34 | }; 35 | 36 | /** 37 | * @typedef {Object} RenderOptions utilities to use while rendering. 38 | * @prop {Document} [document] the default document to use. By default it's the global one. 39 | * @prop {[string, (node:Element, current:any, previous:any) => void][]} [plugins] a list of plugins to deal with, 40 | * used with attributes, example: `["stuff", (node, curr, prev) => { ... }]` 41 | * @prop {(fn:function) => function} [effect] an utility to create effects on components. 42 | * It must return a dispose utility to drop previous effect. 43 | * @prop {(s:any) => any} [getPeek] an utility to retrieve a Signal value without side-effects. 44 | * @prop {(s:any) => any} [getValue] an utility to retrieve a Signal value. 45 | * @prop {(s:any) => boolean} [isSignal] an utility to know if a value is a Signal. 46 | * @prop {function} [Signal] an optional signal constructor used to trap-check. 47 | * @prop {function} [diff] an optional function to diff nodes, not available in SSR. 48 | * `isSignal(ref)` utility whenever the `isSignal` field has not been provided. 49 | */ 50 | 51 | /** 52 | * Return a `render(what, where)` utility able to deal with provided options. 53 | * @param {RenderOptions} options 54 | */ 55 | export default (options = {}) => { 56 | const document = options.document || globalThis.document; 57 | const plugins = new Map(options.plugins || []); 58 | const considerPlugins = !!plugins.size; 59 | const differ = options.diff || diff; 60 | const effect = options.effect || (fn => (fn(), noop)); 61 | const getPeek = options.getPeek || (s => s.peek()); 62 | const getValue = options.getValue || (s => s.value); 63 | const isSignal = options.isSignal || ( 64 | options.Signal ? 65 | isPrototypeOf.bind(options.Signal.prototype) : 66 | () => false 67 | ); 68 | 69 | const text = value => document.createTextNode(value); 70 | const comment = () => document.createComment(UDOMSAY); 71 | 72 | const getChildrenID = (node, i) => { 73 | let IDs = views.get(node); 74 | if (!IDs) views.set(node, IDs = {}); 75 | return IDs[i] || (IDs[i] = {}); 76 | }; 77 | const getComponentView = (view, component) => { 78 | view.dispose(); 79 | const dispose = effect(() => { 80 | const token = invoke(component); 81 | if (token.id !== view.id) 82 | view = getView(view, token, false); 83 | else 84 | view.update(token); 85 | }); 86 | view.dispose = dispose; 87 | return view; 88 | }; 89 | const getNewView = (view, token, createEffect) => { 90 | view.dispose(); 91 | view = new View(token); 92 | if (createEffect) 93 | view.dispose = effect(() => view.update(token)); 94 | else 95 | view.update(token); 96 | return view; 97 | }; 98 | const getView = (view, token, createEffect) => token.type === COMPONENT ? 99 | getComponentView(view, token) : 100 | getNewView(view, token, createEffect) 101 | ; 102 | 103 | class View { 104 | constructor(token, shouldParse = true) { 105 | const [updates, content] = shouldParse ? parse(token) : [EMPTY, null]; 106 | this._ = shouldParse && token.type === FRAGMENT; 107 | this.id = token.id; 108 | this.updates = updates; 109 | this.content = content; 110 | this.dispose = noop; 111 | } 112 | get $() { 113 | const {content, _} = this; 114 | if (_) { 115 | this._ = !_; 116 | return (this.content = asChildNodes(content)).childNodes; 117 | } 118 | return [content]; 119 | } 120 | update(token) { 121 | for (const update of this.updates) 122 | update.call(this, token); 123 | } 124 | } 125 | 126 | const defaultView = new View({id: null}, false); 127 | const defaultEntry = {id: null, view: defaultView}; 128 | 129 | const views = new WeakMap; 130 | let isToken; 131 | 132 | const tokens = new WeakMap; 133 | const parse = token => { 134 | let info = tokens.get(token.id); 135 | if (!info) { 136 | const updates = []; 137 | const content = mapToken(token, updates, [], EMPTY, false); 138 | tokens.set(token.id, info = [updates, content]); 139 | } 140 | const [updates, content] = info; 141 | return [updates.slice(), content.cloneNode(true)]; 142 | }; 143 | 144 | const setAttribute = (node, key, value, set) => { 145 | if (set) 146 | node.setAttribute(key, value); 147 | else 148 | node[key] = value; 149 | }; 150 | 151 | const asAttribute = (node, key, value, prev, set) => { 152 | if (isSignal(value)) { 153 | const dispose = UDOMSAY + key; 154 | if (dispose in prev) 155 | prev[dispose](); 156 | prev[dispose] = effect(() => { 157 | setAttribute(node, key, getValue(value), set); 158 | }); 159 | } 160 | else 161 | setAttribute(node, key, value, set); 162 | }; 163 | 164 | const setProperty = (node, key, value, prev) => { 165 | if (considerPlugins && plugins.has(key)) 166 | plugins.get(key)(node, value, prev); 167 | else if (prev[key] !== value) { 168 | prev[key] = value; 169 | switch (key) { 170 | case 'class': 171 | key += 'Name'; 172 | case 'className': 173 | case 'textContent': 174 | asAttribute(node, key, value, prev, false); 175 | break; 176 | case 'ref': 177 | value.current = node; 178 | break; 179 | default: 180 | if (key.startsWith('on')) 181 | node[key.toLowerCase()] = value; 182 | else if (key in node) 183 | asAttribute(node, key, value, prev, false); 184 | else { 185 | if (value == null) 186 | node.removeAttribute(key); 187 | else 188 | asAttribute(node, key, value, prev, true); 189 | } 190 | break; 191 | } 192 | } 193 | }; 194 | 195 | const handleAttributes = (a, c, i) => function (token) { 196 | const prev = {}; 197 | const node = reachChild(c, this); 198 | (this.updates[i] = token => { 199 | const {attributes} = reachToken(c, token); 200 | for (const index of a) { 201 | const entry = attributes[index]; 202 | const {type, value} = entry; 203 | if (type < 2) 204 | setProperty(node, entry.name, value, prev); 205 | else { 206 | for (const [name, v] of entries(value)) 207 | setProperty(node, name, v, prev); 208 | } 209 | } 210 | })(token); 211 | }; 212 | 213 | const handleArray = (c, i) => function (token) { 214 | let diffed = EMPTY, findIndex = true, index = -1; 215 | const node = reachChild(c, this); 216 | const keys = new Map; 217 | (this.updates[i] = token => { 218 | const {value} = reachToken(c, token); 219 | const diffing = []; 220 | for (let i = 0; i < value.length; i++) { 221 | const token = value[i]; 222 | if (findIndex) { 223 | findIndex = !findIndex; 224 | index = token.attributes.findIndex(isKey); 225 | } 226 | const key = index < 0 ? i : token.attributes[index].value; 227 | let {id, view} = keys.get(key) || defaultEntry; 228 | if (id !== (token.id || (token.id = getChildrenID(node, i)))) { 229 | view = getView(view, token, false); 230 | keys.set(key, {id: token.id, view}); 231 | } 232 | else 233 | view.update(token); 234 | diffing.push(...view.$); 235 | } 236 | if (diffing.length) 237 | diffed = differ(diffed, diffing, node); 238 | else if(diffed !== EMPTY) { 239 | const range = document.createRange(); 240 | range.setStartBefore(diffed[0]); 241 | range.setEndAfter(diffed[diffed.length - 1]); 242 | range.deleteContents(); 243 | keys.clear(); 244 | diffed = EMPTY; 245 | findIndex = true; 246 | } 247 | })(token); 248 | }; 249 | 250 | const handleComponent = (c, i) => function (token) { 251 | let diffed = EMPTY, view = defaultView; 252 | const node = reachChild(c, this); 253 | (this.updates[i] = token => { 254 | view = getComponentView(view, reachToken(c, token)); 255 | diffed = differ(diffed, view.$, node); 256 | })(token); 257 | }; 258 | 259 | const handleContent = (c, i) => function (token) { 260 | const node = reachChild(c, this); 261 | (this.updates[i] = token => { 262 | setData(node, reachToken(c, token).value); 263 | })(token); 264 | }; 265 | 266 | const handleSignal = (c, i) => function (token) { 267 | let dispose = noop, signal, fx; 268 | const node = reachChild(c, this); 269 | const {value} = reachToken(c, token); 270 | const update = value => { 271 | if (signal !== value) { 272 | dispose(); 273 | signal = value; 274 | dispose = effect(fx); 275 | } 276 | }; 277 | if (isToken(getPeek(value))) { 278 | let diffed = EMPTY, view = defaultView; 279 | fx = () => { 280 | const token = getValue(signal); 281 | view = getView(view, token, false); 282 | diffed = differ(diffed, view.$, node); 283 | }; 284 | } 285 | else { 286 | const t = text(''); 287 | node.replaceWith(t); 288 | fx = () => { setData(t, getValue(signal)) }; 289 | } 290 | this.updates[i] = token => update(reachToken(c, token).value); 291 | update(value); 292 | }; 293 | 294 | const handleToken = (c, i) => function (token) { 295 | let diffed = EMPTY, view = defaultView, id = null; 296 | const node = reachChild(c, this); 297 | (this.updates[i] = token => { 298 | token = reachToken(c, token).value; 299 | if (id !== token.id) { 300 | id = token.id; 301 | // TODO: should this effect instead? 302 | view = getView(view, token, false); 303 | diffed = differ(diffed, view.$, node); 304 | } 305 | else if (token.type === COMPONENT) 306 | view.update(invoke(token)); 307 | else 308 | view.update(token); 309 | })(token); 310 | }; 311 | 312 | const addChildren = ({children}, updates, content, c, svg) => { 313 | for (let i = 0; i < children.length; i++) { 314 | const child = children[i]; 315 | switch (child.type) { 316 | case STATIC: 317 | content.appendChild(text(child.value)); 318 | break; 319 | default: 320 | content.appendChild( 321 | mapToken(children[i], updates, [], c.concat(i), svg) 322 | ); 323 | break; 324 | } 325 | } 326 | }; 327 | 328 | const mapToken = (token, updates, a, c, svg) => { 329 | let callback, content; 330 | const {length} = updates; 331 | type: switch (token.type) { 332 | case INTERPOLATION: { 333 | const {value} = token; 334 | switch (true) { 335 | case isToken(value): 336 | callback = handleToken; 337 | break; 338 | case isArray(value): 339 | callback = handleArray; 340 | break; 341 | case isSignal(value): 342 | callback = handleSignal; 343 | break; 344 | default: { 345 | content = text(''); 346 | updates.push(handleContent(c, length)); 347 | break type; 348 | } 349 | } 350 | } 351 | case COMPONENT: { 352 | content = comment(); 353 | updates.push((callback || handleComponent)(c, length)); 354 | break; 355 | } 356 | case ELEMENT: { 357 | const {attributes, name} = token; 358 | const args = [name]; 359 | const attrs = []; 360 | for (let i = 0; i < attributes.length; i++) { 361 | const entry = attributes[i]; 362 | if (entry.type === INTERPOLATION || entry.dynamic) { 363 | if (!isKey(entry)) 364 | a.push(i); 365 | } 366 | else { 367 | if (entry.name === 'is') 368 | args.push({is: entry.value}); 369 | attrs.push(entry); 370 | } 371 | } 372 | if (a.length) 373 | updates.push(handleAttributes(a, c, length)); 374 | content = svg || (svg = name === 'svg') ? 375 | document.createElementNS('http://www.w3.org/2000/svg', ...args) : 376 | document.createElement(...args); 377 | for (const {name, value} of attrs) 378 | setAttribute(content, name, value, true); 379 | addChildren(token, updates, content, c, svg); 380 | break; 381 | } 382 | case FRAGMENT: { 383 | content = document.createDocumentFragment(); 384 | addChildren(token, updates, content, c, svg); 385 | break; 386 | } 387 | } 388 | return content; 389 | }; 390 | 391 | /** 392 | * Reveal some `token` content into a `DOM` element. 393 | * @param {() => Token | Token} what the token to render 394 | * @param {Element} where the DOM node to render such token 395 | */ 396 | return (what, where) => { 397 | /** @type {Token} */ 398 | const token = typeof what === 'function' ? what() : what; 399 | if (!isToken) isToken = isPrototypeOf.bind(getPrototypeOf(token)); 400 | const view = getView(views.get(where) || defaultView, token, true); 401 | views.set(where, view); 402 | where.replaceChildren(...view.$); 403 | }; 404 | }; 405 | -------------------------------------------------------------------------------- /esm/ssr.js: -------------------------------------------------------------------------------- 1 | /*! (c) Andrea Giammarchi - ISC */ 2 | 3 | import {TEXT_ELEMENTS, VOID_ELEMENTS} from 'domconstants'; 4 | import {escape} from './html-escaper.js'; 5 | 6 | import EMPTY from '@webreflection/empty/array'; 7 | import noop from '@webreflection/empty/function'; 8 | 9 | import createRender from './index.js'; 10 | 11 | const cloneAttribute = ({name, value}) => ({name, value}); 12 | 13 | const cloneNode = (current, newNode) => { 14 | for (const node of current) { 15 | switch (node.nodeType) { 16 | case 1: 17 | const element = new Element(node.name); 18 | if (node.attributes !== EMPTY) 19 | element.attributes = node.attributes.map(cloneAttribute); 20 | cloneNode(node, element); 21 | newNode.appendChild(element); 22 | break; 23 | case 3: 24 | newNode.appendChild(new Text(node.data)); 25 | break; 26 | case 11: 27 | for (const inner of node) 28 | cloneNode(inner, newNode); 29 | break; 30 | } 31 | } 32 | return newNode; 33 | }; 34 | 35 | class Text { 36 | constructor(data) { 37 | this.nodeType = 3; 38 | this.parentNode = null; 39 | this.data = data; 40 | } 41 | replaceWith(node) { 42 | const {parentNode} = this; 43 | parentNode.splice( 44 | parentNode.indexOf(this), 45 | 1, 46 | node 47 | ); 48 | } 49 | remove() { 50 | Element.prototype.remove.call(this); 51 | } 52 | toString() { 53 | return TEXT_ELEMENTS.test(this.parentNode?.name) ? 54 | this.data : escape(this.data); 55 | } 56 | } 57 | 58 | class Node extends Array { 59 | get childNodes() { 60 | return this; 61 | } 62 | appendChild(node) { 63 | switch (node.nodeType) { 64 | case 1: 65 | case 3: 66 | node.parentNode = this; 67 | this.push(node); 68 | break; 69 | case 11: 70 | for (const inner of node) 71 | this.appendChild(inner); 72 | break; 73 | } 74 | return node; 75 | } 76 | replaceChild(newChild, oldChild) { 77 | this[this.indexOf(oldChild)] = newChild; 78 | newChild.parentNode = this; 79 | return oldChild; 80 | } 81 | } 82 | 83 | class Fragment extends Node { 84 | constructor() { 85 | super(); 86 | this.nodeType = 11; 87 | this.parentNode = null; 88 | } 89 | cloneNode() { 90 | return cloneNode(this, new Fragment); 91 | } 92 | toString() { 93 | return this.join(''); 94 | } 95 | } 96 | 97 | class Element extends Node { 98 | constructor(name) { 99 | super(); 100 | this.nodeType = 1; 101 | this.name = name; 102 | this.attributes = EMPTY; 103 | } 104 | set className(value) { 105 | this.setAttribute('class', value); 106 | } 107 | set textContent(value) { 108 | for (const node of this.splice(0)) 109 | node.parentNode = null; 110 | this.appendChild(new Text(value)); 111 | } 112 | cloneNode() { 113 | return cloneNode(this, new Element(this.name)); 114 | } 115 | remove() { 116 | const {parentNode} = this; 117 | if (parentNode) { 118 | this.parentNode = null; 119 | parentNode.splice(parentNode.indexOf(this), 1); 120 | } 121 | } 122 | removeAttribute() {} 123 | setAttribute(name, value) { 124 | if (this.attributes === EMPTY) 125 | this.attributes = []; 126 | this.attributes.push({name, value}); 127 | } 128 | toString() { 129 | const {length, name, attributes} = this; 130 | const html = ['<', name]; 131 | for (const {name, value} of attributes) { 132 | if (typeof value === 'boolean') { 133 | if (value) 134 | html.push(` ${name}`); 135 | } 136 | else if (value != null) 137 | html.push(` ${name}="${escape(value)}"`); 138 | } 139 | if (!length && VOID_ELEMENTS.test(name)) 140 | html.push(' />'); 141 | else 142 | html.push('>', ...this, ``); 143 | return html.join(''); 144 | } 145 | } 146 | 147 | class Range { 148 | constructor() { 149 | this.before = null; 150 | this.after = null; 151 | } 152 | setStartBefore(node) { 153 | this.before = node; 154 | } 155 | setEndAfter(node) { 156 | this.after = node; 157 | } 158 | deleteContents() { 159 | const {parentNode} = this.before; 160 | const i = parentNode.indexOf(this.before); 161 | for (const node of parentNode.splice(i, parentNode.indexOf(this.after) - i + 1)) 162 | node.parentNode = null; 163 | } 164 | } 165 | 166 | const document = { 167 | createTextNode: data => new Text(data), 168 | createComment: () => new Text(''), 169 | createDocumentFragment: () => new Fragment, 170 | createElementNS: name => new Element(name), 171 | createElement: (name, options) => { 172 | const element = new Element(name); 173 | if (options && options.is) 174 | element.setAttribute('is', options.is); 175 | return element; 176 | }, 177 | createRange: () => new Range 178 | }; 179 | 180 | const diff = (_, nodes, before) => { 181 | const {parentNode} = before; 182 | const i = parentNode.indexOf(before); 183 | for (const node of nodes) 184 | node.parentNode = parentNode; 185 | parentNode.splice(i, 0, ...nodes); 186 | }; 187 | 188 | /** 189 | * @typedef {Object} RenderOptions utilities to use while rendering. 190 | * @prop {Document} [document] the default document to use. By default it's the global one. 191 | * @prop {[string, (node:Element, current:any, previous:any) => void][]} [plugins] a list of plugins to deal with, 192 | * used with attributes, example: `["stuff", (node, curr, prev) => { ... }]` 193 | * @prop {(fn:function) => function} [effect] an utility to create effects on components. 194 | * It must return a dispose utility to drop previous effect. 195 | * @prop {(s:any) => any} [getPeek] an utility to retrieve a Signal value without side-effects. 196 | * @prop {(s:any) => any} [getValue] an utility to retrieve a Signal value. 197 | * @prop {(s:any) => boolean} [isSignal] an utility to know if a value is a Signal. 198 | * @prop {function} [Signal] an optional signal constructor used to trap-check. 199 | * `isSignal(ref)` utility whenever the `isSignal` field has not been provided. 200 | */ 201 | 202 | /** 203 | * Return a `render(what, where)` utility able to deal with provided options. 204 | * @param {RenderOptions} options 205 | */ 206 | export default (options = {}) => { 207 | const getPeek = options.getPeek || (s => s.peek()); 208 | const render = createRender({ 209 | document, diff, getPeek, 210 | plugins: options.plugins || EMPTY, 211 | effect: (fn => (fn(), noop)), 212 | getValue: getPeek, 213 | isSignal: options.isSignal, 214 | Signal: options.Signal 215 | }); 216 | return (what, where) => { 217 | render(what, { 218 | replaceChildren(...nodes) { 219 | if (typeof where === 'function') { 220 | let html = nodes.join(''); 221 | if (/^(|]+?>)/.test(html)) 222 | html = '' + html; 223 | where(html); 224 | } 225 | else { 226 | for (const node of nodes) 227 | where.write(node.toString()); 228 | } 229 | } 230 | }); 231 | }; 232 | }; 233 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var freeze = Object.freeze; 2 | 3 | var EMPTY = freeze([]); 4 | 5 | var noop = () => {}; 6 | 7 | /** (c) Andrea Giammarchi - ISC */ 8 | 9 | class Token { 10 | static ATTRIBUTE = 1; 11 | static COMPONENT = 2; 12 | static ELEMENT = 3; 13 | static FRAGMENT = 4; 14 | static INTERPOLATION = 5; 15 | static STATIC = 6; 16 | get properties() { 17 | const {attributes} = this; 18 | if (attributes.length) { 19 | const properties = {}; 20 | for (const entry of attributes) { 21 | if (entry.type < 2) 22 | properties[entry.name] = entry.value; 23 | else 24 | Object.assign(properties, entry.value); 25 | } 26 | return properties; 27 | } 28 | return null; 29 | } 30 | } 31 | 32 | /* (c) Andrea Giammarchi - ISC */ 33 | // @see https://github.com/WebReflection/udomdiff 34 | const diff = (a, b, before) => { 35 | const {parentNode} = before; 36 | const bLength = b.length; 37 | let aEnd = a.length; 38 | let bEnd = bLength; 39 | let aStart = 0; 40 | let bStart = 0; 41 | let map = null; 42 | while (aStart < aEnd || bStart < bEnd) { 43 | // append head, tail, or nodes in between: fast path 44 | if (aEnd === aStart) { 45 | // we could be in a situation where the rest of nodes that 46 | // need to be added are not at the end, and in such case 47 | // the node to `insertBefore`, if the index is more than 0 48 | // must be retrieved, otherwise it's gonna be the first item. 49 | const node = bEnd < bLength ? 50 | (bStart ? 51 | b[bStart - 1].nextSibling : 52 | b[bEnd - bStart]) : 53 | before; 54 | while (bStart < bEnd) 55 | parentNode.insertBefore(b[bStart++], node); 56 | } 57 | // remove head or tail: fast path 58 | else if (bEnd === bStart) { 59 | while (aStart < aEnd) { 60 | // remove the node only if it's unknown or not live 61 | if (!map || !map.has(a[aStart])) 62 | a[aStart].remove(); 63 | aStart++; 64 | } 65 | } 66 | // same node: fast path 67 | else if (a[aStart] === b[bStart]) { 68 | aStart++; 69 | bStart++; 70 | } 71 | // same tail: fast path 72 | else if (a[aEnd - 1] === b[bEnd - 1]) { 73 | aEnd--; 74 | bEnd--; 75 | } 76 | // The once here single last swap "fast path" has been removed in v1.1.0 77 | // https://github.com/WebReflection/udomdiff/blob/single-final-swap/esm/index.js#L69-L85 78 | // reverse swap: also fast path 79 | else if ( 80 | a[aStart] === b[bEnd - 1] && 81 | b[bStart] === a[aEnd - 1] 82 | ) { 83 | // this is a "shrink" operation that could happen in these cases: 84 | // [1, 2, 3, 4, 5] 85 | // [1, 4, 3, 2, 5] 86 | // or asymmetric too 87 | // [1, 2, 3, 4, 5] 88 | // [1, 2, 3, 5, 6, 4] 89 | const node = a[--aEnd].nextSibling; 90 | parentNode.insertBefore( 91 | b[bStart++], 92 | a[aStart++].nextSibling 93 | ); 94 | parentNode.insertBefore(b[--bEnd], node); 95 | // mark the future index as identical (yeah, it's dirty, but cheap 👍) 96 | // The main reason to do this, is that when a[aEnd] will be reached, 97 | // the loop will likely be on the fast path, as identical to b[bEnd]. 98 | // In the best case scenario, the next loop will skip the tail, 99 | // but in the worst one, this node will be considered as already 100 | // processed, bailing out pretty quickly from the map index check 101 | a[aEnd] = b[bEnd]; 102 | } 103 | // map based fallback, "slow" path 104 | else { 105 | // the map requires an O(bEnd - bStart) operation once 106 | // to store all future nodes indexes for later purposes. 107 | // In the worst case scenario, this is a full O(N) cost, 108 | // and such scenario happens at least when all nodes are different, 109 | // but also if both first and last items of the lists are different 110 | if (!map) { 111 | map = new Map; 112 | let i = bStart; 113 | while (i < bEnd) 114 | map.set(b[i], i++); 115 | } 116 | // if it's a future node, hence it needs some handling 117 | if (map.has(a[aStart])) { 118 | // grab the index of such node, 'cause it might have been processed 119 | const index = map.get(a[aStart]); 120 | // if it's not already processed, look on demand for the next LCS 121 | if (bStart < index && index < bEnd) { 122 | let i = aStart; 123 | // counts the amount of nodes that are the same in the future 124 | let sequence = 1; 125 | while (++i < aEnd && i < bEnd && map.get(a[i]) === (index + sequence)) 126 | sequence++; 127 | // effort decision here: if the sequence is longer than replaces 128 | // needed to reach such sequence, which would brings again this loop 129 | // to the fast path, prepend the difference before a sequence, 130 | // and move only the future list index forward, so that aStart 131 | // and bStart will be aligned again, hence on the fast path. 132 | // An example considering aStart and bStart are both 0: 133 | // a: [1, 2, 3, 4] 134 | // b: [7, 1, 2, 3, 6] 135 | // this would place 7 before 1 and, from that time on, 1, 2, and 3 136 | // will be processed at zero cost 137 | if (sequence > (index - bStart)) { 138 | const node = a[aStart]; 139 | while (bStart < index) 140 | parentNode.insertBefore(b[bStart++], node); 141 | } 142 | // if the effort wasn't good enough, fallback to a replace, 143 | // moving both source and target indexes forward, hoping that some 144 | // similar node will be found later on, to go back to the fast path 145 | else { 146 | parentNode.replaceChild( 147 | b[bStart++], 148 | a[aStart++] 149 | ); 150 | } 151 | } 152 | // otherwise move the source forward, 'cause there's nothing to do 153 | else 154 | aStart++; 155 | } 156 | // this node has no meaning in the future list, so it's more than safe 157 | // to remove it, and check the next live node out instead, meaning 158 | // that only the live list index should be forwarded 159 | else 160 | a[aStart++].remove(); 161 | } 162 | } 163 | return b; 164 | }; 165 | 166 | /*! (c) Andrea Giammarchi - ISC */ 167 | 168 | const {isArray} = Array; 169 | const {entries, getPrototypeOf, prototype: {isPrototypeOf}} = Object; 170 | 171 | const { 172 | COMPONENT, 173 | ELEMENT, 174 | FRAGMENT, 175 | INTERPOLATION, 176 | STATIC 177 | } = Token; 178 | 179 | const UDOMSAY = '🙊'; 180 | 181 | // generic utils 182 | const asChildNodes = ({childNodes}) => ({childNodes: [...childNodes]}); 183 | const getChild = ({childNodes}, i) => childNodes[i]; 184 | const getToken = ({children}, i) => children[i]; 185 | const invoke = ({value, properties, children}) => value(properties, ...children); 186 | const isKey = ({name}) => name === 'key'; 187 | const reachChild = (c, {content}) => c.reduce(getChild, content); 188 | const reachToken = (c, token) => c.reduce(getToken, token); 189 | const setData = (node, value) => { 190 | const data = value == null ? '' : String(value); 191 | if (data !== node.data) 192 | node.data = data; 193 | }; 194 | 195 | /** 196 | * @typedef {Object} RenderOptions utilities to use while rendering. 197 | * @prop {Document} [document] the default document to use. By default it's the global one. 198 | * @prop {[string, (node:Element, current:any, previous:any) => void][]} [plugins] a list of plugins to deal with, 199 | * used with attributes, example: `["stuff", (node, curr, prev) => { ... }]` 200 | * @prop {(fn:function) => function} [effect] an utility to create effects on components. 201 | * It must return a dispose utility to drop previous effect. 202 | * @prop {(s:any) => any} [getPeek] an utility to retrieve a Signal value without side-effects. 203 | * @prop {(s:any) => any} [getValue] an utility to retrieve a Signal value. 204 | * @prop {(s:any) => boolean} [isSignal] an utility to know if a value is a Signal. 205 | * @prop {function} [Signal] an optional signal constructor used to trap-check. 206 | * @prop {function} [diff] an optional function to diff nodes, not available in SSR. 207 | * `isSignal(ref)` utility whenever the `isSignal` field has not been provided. 208 | */ 209 | 210 | /** 211 | * Return a `render(what, where)` utility able to deal with provided options. 212 | * @param {RenderOptions} options 213 | */ 214 | var index = (options = {}) => { 215 | const document = options.document || globalThis.document; 216 | const plugins = new Map(options.plugins || []); 217 | const considerPlugins = !!plugins.size; 218 | const differ = options.diff || diff; 219 | const effect = options.effect || (fn => (fn(), noop)); 220 | const getPeek = options.getPeek || (s => s.peek()); 221 | const getValue = options.getValue || (s => s.value); 222 | const isSignal = options.isSignal || ( 223 | options.Signal ? 224 | isPrototypeOf.bind(options.Signal.prototype) : 225 | () => false 226 | ); 227 | 228 | const text = value => document.createTextNode(value); 229 | const comment = () => document.createComment(UDOMSAY); 230 | 231 | const getChildrenID = (node, i) => { 232 | let IDs = views.get(node); 233 | if (!IDs) views.set(node, IDs = {}); 234 | return IDs[i] || (IDs[i] = {}); 235 | }; 236 | const getComponentView = (view, component) => { 237 | view.dispose(); 238 | const dispose = effect(() => { 239 | const token = invoke(component); 240 | if (token.id !== view.id) 241 | view = getView(view, token, false); 242 | else 243 | view.update(token); 244 | }); 245 | view.dispose = dispose; 246 | return view; 247 | }; 248 | const getNewView = (view, token, createEffect) => { 249 | view.dispose(); 250 | view = new View(token); 251 | if (createEffect) 252 | view.dispose = effect(() => view.update(token)); 253 | else 254 | view.update(token); 255 | return view; 256 | }; 257 | const getView = (view, token, createEffect) => token.type === COMPONENT ? 258 | getComponentView(view, token) : 259 | getNewView(view, token, createEffect) 260 | ; 261 | 262 | class View { 263 | constructor(token, shouldParse = true) { 264 | const [updates, content] = shouldParse ? parse(token) : [EMPTY, null]; 265 | this._ = shouldParse && token.type === FRAGMENT; 266 | this.id = token.id; 267 | this.updates = updates; 268 | this.content = content; 269 | this.dispose = noop; 270 | } 271 | get $() { 272 | const {content, _} = this; 273 | if (_) { 274 | this._ = !_; 275 | return (this.content = asChildNodes(content)).childNodes; 276 | } 277 | return [content]; 278 | } 279 | update(token) { 280 | for (const update of this.updates) 281 | update.call(this, token); 282 | } 283 | } 284 | 285 | const defaultView = new View({id: null}, false); 286 | const defaultEntry = {id: null, view: defaultView}; 287 | 288 | const views = new WeakMap; 289 | let isToken; 290 | 291 | const tokens = new WeakMap; 292 | const parse = token => { 293 | let info = tokens.get(token.id); 294 | if (!info) { 295 | const updates = []; 296 | const content = mapToken(token, updates, [], EMPTY, false); 297 | tokens.set(token.id, info = [updates, content]); 298 | } 299 | const [updates, content] = info; 300 | return [updates.slice(), content.cloneNode(true)]; 301 | }; 302 | 303 | const setAttribute = (node, key, value, set) => { 304 | if (set) 305 | node.setAttribute(key, value); 306 | else 307 | node[key] = value; 308 | }; 309 | 310 | const asAttribute = (node, key, value, prev, set) => { 311 | if (isSignal(value)) { 312 | const dispose = UDOMSAY + key; 313 | if (dispose in prev) 314 | prev[dispose](); 315 | prev[dispose] = effect(() => { 316 | setAttribute(node, key, getValue(value), set); 317 | }); 318 | } 319 | else 320 | setAttribute(node, key, value, set); 321 | }; 322 | 323 | const setProperty = (node, key, value, prev) => { 324 | if (considerPlugins && plugins.has(key)) 325 | plugins.get(key)(node, value, prev); 326 | else if (prev[key] !== value) { 327 | prev[key] = value; 328 | switch (key) { 329 | case 'class': 330 | key += 'Name'; 331 | case 'className': 332 | case 'textContent': 333 | asAttribute(node, key, value, prev, false); 334 | break; 335 | case 'ref': 336 | value.current = node; 337 | break; 338 | default: 339 | if (key.startsWith('on')) 340 | node[key.toLowerCase()] = value; 341 | else if (key in node) 342 | asAttribute(node, key, value, prev, false); 343 | else { 344 | if (value == null) 345 | node.removeAttribute(key); 346 | else 347 | asAttribute(node, key, value, prev, true); 348 | } 349 | break; 350 | } 351 | } 352 | }; 353 | 354 | const handleAttributes = (a, c, i) => function (token) { 355 | const prev = {}; 356 | const node = reachChild(c, this); 357 | (this.updates[i] = token => { 358 | const {attributes} = reachToken(c, token); 359 | for (const index of a) { 360 | const entry = attributes[index]; 361 | const {type, value} = entry; 362 | if (type < 2) 363 | setProperty(node, entry.name, value, prev); 364 | else { 365 | for (const [name, v] of entries(value)) 366 | setProperty(node, name, v, prev); 367 | } 368 | } 369 | })(token); 370 | }; 371 | 372 | const handleArray = (c, i) => function (token) { 373 | let diffed = EMPTY, findIndex = true, index = -1; 374 | const node = reachChild(c, this); 375 | const keys = new Map; 376 | (this.updates[i] = token => { 377 | const {value} = reachToken(c, token); 378 | const diffing = []; 379 | for (let i = 0; i < value.length; i++) { 380 | const token = value[i]; 381 | if (findIndex) { 382 | findIndex = !findIndex; 383 | index = token.attributes.findIndex(isKey); 384 | } 385 | const key = index < 0 ? i : token.attributes[index].value; 386 | let {id, view} = keys.get(key) || defaultEntry; 387 | if (id !== (token.id || (token.id = getChildrenID(node, i)))) { 388 | view = getView(view, token, false); 389 | keys.set(key, {id: token.id, view}); 390 | } 391 | else 392 | view.update(token); 393 | diffing.push(...view.$); 394 | } 395 | if (diffing.length) 396 | diffed = differ(diffed, diffing, node); 397 | else if(diffed !== EMPTY) { 398 | const range = document.createRange(); 399 | range.setStartBefore(diffed[0]); 400 | range.setEndAfter(diffed[diffed.length - 1]); 401 | range.deleteContents(); 402 | keys.clear(); 403 | diffed = EMPTY; 404 | findIndex = true; 405 | } 406 | })(token); 407 | }; 408 | 409 | const handleComponent = (c, i) => function (token) { 410 | let diffed = EMPTY, view = defaultView; 411 | const node = reachChild(c, this); 412 | (this.updates[i] = token => { 413 | view = getComponentView(view, reachToken(c, token)); 414 | diffed = differ(diffed, view.$, node); 415 | })(token); 416 | }; 417 | 418 | const handleContent = (c, i) => function (token) { 419 | const node = reachChild(c, this); 420 | (this.updates[i] = token => { 421 | setData(node, reachToken(c, token).value); 422 | })(token); 423 | }; 424 | 425 | const handleSignal = (c, i) => function (token) { 426 | let dispose = noop, signal, fx; 427 | const node = reachChild(c, this); 428 | const {value} = reachToken(c, token); 429 | const update = value => { 430 | if (signal !== value) { 431 | dispose(); 432 | signal = value; 433 | dispose = effect(fx); 434 | } 435 | }; 436 | if (isToken(getPeek(value))) { 437 | let diffed = EMPTY, view = defaultView; 438 | fx = () => { 439 | const token = getValue(signal); 440 | view = getView(view, token, false); 441 | diffed = differ(diffed, view.$, node); 442 | }; 443 | } 444 | else { 445 | const t = text(''); 446 | node.replaceWith(t); 447 | fx = () => { setData(t, getValue(signal)); }; 448 | } 449 | this.updates[i] = token => update(reachToken(c, token).value); 450 | update(value); 451 | }; 452 | 453 | const handleToken = (c, i) => function (token) { 454 | let diffed = EMPTY, view = defaultView, id = null; 455 | const node = reachChild(c, this); 456 | (this.updates[i] = token => { 457 | token = reachToken(c, token).value; 458 | if (id !== token.id) { 459 | id = token.id; 460 | // TODO: should this effect instead? 461 | view = getView(view, token, false); 462 | diffed = differ(diffed, view.$, node); 463 | } 464 | else if (token.type === COMPONENT) 465 | view.update(invoke(token)); 466 | else 467 | view.update(token); 468 | })(token); 469 | }; 470 | 471 | const addChildren = ({children}, updates, content, c, svg) => { 472 | for (let i = 0; i < children.length; i++) { 473 | const child = children[i]; 474 | switch (child.type) { 475 | case STATIC: 476 | content.appendChild(text(child.value)); 477 | break; 478 | default: 479 | content.appendChild( 480 | mapToken(children[i], updates, [], c.concat(i), svg) 481 | ); 482 | break; 483 | } 484 | } 485 | }; 486 | 487 | const mapToken = (token, updates, a, c, svg) => { 488 | let callback, content; 489 | const {length} = updates; 490 | type: switch (token.type) { 491 | case INTERPOLATION: { 492 | const {value} = token; 493 | switch (true) { 494 | case isToken(value): 495 | callback = handleToken; 496 | break; 497 | case isArray(value): 498 | callback = handleArray; 499 | break; 500 | case isSignal(value): 501 | callback = handleSignal; 502 | break; 503 | default: { 504 | content = text(''); 505 | updates.push(handleContent(c, length)); 506 | break type; 507 | } 508 | } 509 | } 510 | case COMPONENT: { 511 | content = comment(); 512 | updates.push((callback || handleComponent)(c, length)); 513 | break; 514 | } 515 | case ELEMENT: { 516 | const {attributes, name} = token; 517 | const args = [name]; 518 | const attrs = []; 519 | for (let i = 0; i < attributes.length; i++) { 520 | const entry = attributes[i]; 521 | if (entry.type === INTERPOLATION || entry.dynamic) { 522 | if (!isKey(entry)) 523 | a.push(i); 524 | } 525 | else { 526 | if (entry.name === 'is') 527 | args.push({is: entry.value}); 528 | attrs.push(entry); 529 | } 530 | } 531 | if (a.length) 532 | updates.push(handleAttributes(a, c, length)); 533 | content = svg || (svg = name === 'svg') ? 534 | document.createElementNS('http://www.w3.org/2000/svg', ...args) : 535 | document.createElement(...args); 536 | for (const {name, value} of attrs) 537 | setAttribute(content, name, value, true); 538 | addChildren(token, updates, content, c, svg); 539 | break; 540 | } 541 | case FRAGMENT: { 542 | content = document.createDocumentFragment(); 543 | addChildren(token, updates, content, c, svg); 544 | break; 545 | } 546 | } 547 | return content; 548 | }; 549 | 550 | /** 551 | * Reveal some `token` content into a `DOM` element. 552 | * @param {() => Token | Token} what the token to render 553 | * @param {Element} where the DOM node to render such token 554 | */ 555 | return (what, where) => { 556 | /** @type {Token} */ 557 | const token = typeof what === 'function' ? what() : what; 558 | if (!isToken) isToken = isPrototypeOf.bind(getPrototypeOf(token)); 559 | const view = getView(views.get(where) || defaultView, token, true); 560 | views.set(where, view); 561 | where.replaceChildren(...view.$); 562 | }; 563 | }; 564 | 565 | export { index as default }; 566 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "udomsay", 3 | "version": "0.4.19", 4 | "description": "A stricter, signals driven, ESX based library", 5 | "main": "./cjs/index.js", 6 | "scripts": { 7 | "build": "npm run cjs && npm run rollup:es && npm run rollup:babel && npm run build:dist && npm run size && npm run build:size", 8 | "build:array": "babel test/array.jsx -o test/array.js", 9 | "build:dist": "for lib in 'preact' 'signal' 'solid' 'usignal'; do npm run rollup:$lib; done; npm run build:dist:test", 10 | "build:dist:test": "for lib in 'preact' 'signal' 'solid' 'usignal'; do echo \"$lib\" > test/dist/$lib.html && babel test/dist/$lib.jsx -o test/dist/$lib.js; done", 11 | "build:children": "babel test/children.jsx -o test/children.js", 12 | "build:counter": "babel test/counter.jsx -o test/counter.js", 13 | "build:index": "babel test/index.jsx -o test/index.js", 14 | "build:size": "for lib in 'preact' 'signal' 'solid' 'usignal'; do echo -e \"\\x1b[1m$lib\\x1b[0m: $(cat $lib.js | brotli | wc -c)\"; done", 15 | "build:ssr": "babel test/ssr.jsx -o test/ssr.js", 16 | "build:esx": "babel test/esx.jsx -o test/esx-babel.js", 17 | "build:uhooks": "babel test/uhooks.jsx -o test/uhooks.js", 18 | "cjs": "ascjs --no-default esm cjs", 19 | "rollup:es": "rollup --config rollup/es.config.js", 20 | "rollup:babel": "rollup --config rollup/babel.config.js", 21 | "rollup:preact": "rollup --config rollup/preact.config.js", 22 | "rollup:signal": "rollup --config rollup/signal.config.js", 23 | "rollup:solid": "rollup --config rollup/solid.config.js", 24 | "rollup:usignal": "rollup --config rollup/usignal.config.js", 25 | "coveralls": "c8 report --reporter=text-lcov | coveralls", 26 | "size": "echo -e \"\\x1b[1mudomsay\\x1b[0m: $(cat es.js | brotli | wc -c)\"", 27 | "test": "c8 node test/index.js" 28 | }, 29 | "keywords": [ 30 | "ESX", 31 | "JSX", 32 | "signals", 33 | "library" 34 | ], 35 | "author": "Andrea Giammarchi", 36 | "license": "ISC", 37 | "devDependencies": { 38 | "@babel/cli": "^7.20.7", 39 | "@babel/core": "^7.20.12", 40 | "@preact/signals-core": "^1.2.3", 41 | "@rollup/plugin-node-resolve": "^15.0.1", 42 | "@rollup/plugin-terser": "^0.4.0", 43 | "@ungap/babel-plugin-transform-esx": "^0.4.3", 44 | "@webreflection/signal": "^1.0.1", 45 | "ascjs": "^5.0.1", 46 | "c8": "^7.12.0", 47 | "rollup": "^3.13.0", 48 | "solid-js": "^1.6.10", 49 | "usignal": "^0.8.10" 50 | }, 51 | "module": "./esm/index.js", 52 | "type": "module", 53 | "exports": { 54 | ".": { 55 | "import": "./esm/index.js", 56 | "default": "./cjs/index.js" 57 | }, 58 | "./preact": { 59 | "import": "./preact.js" 60 | }, 61 | "./signal": { 62 | "import": "./signal.js" 63 | }, 64 | "./solid": { 65 | "import": "./solid.js" 66 | }, 67 | "./usignal": { 68 | "import": "./usignal.js" 69 | }, 70 | "./ssr": { 71 | "import": "./esm/ssr.js", 72 | "default": "./cjs/ssr.js" 73 | }, 74 | "./package.json": "./package.json" 75 | }, 76 | "unpkg": "es.js", 77 | "dependencies": { 78 | "@ungap/esx": "^0.3.1", 79 | "@webreflection/empty": "^0.2.1", 80 | "domconstants": "^1.0.0" 81 | }, 82 | "repository": { 83 | "type": "git", 84 | "url": "git+https://github.com/WebReflection/udomsay.git" 85 | }, 86 | "bugs": { 87 | "url": "https://github.com/WebReflection/udomsay/issues" 88 | }, 89 | "homepage": "https://github.com/WebReflection/udomsay#readme" 90 | } 91 | -------------------------------------------------------------------------------- /preact.js: -------------------------------------------------------------------------------- 1 | function t(){throw new Error("Cycle detected")}function e(){if(o>1)return void o--;let t,e=!1;for(;void 0!==n;){let i=n;for(n=void 0,r++;void 0!==i;){const s=i.o;if(i.o=void 0,i.f&=-3,!(8&i.f)&&a(i))try{i.c()}catch(i){e||(t=i,e=!0)}i=s}}if(r=0,o--,e)throw t}function i(t){if(o>0)return t();o++;try{return t()}finally{e()}}let s,n,o=0,r=0,c=0;function h(t){if(void 0===s)return;let e=t.n;return void 0===e||e.t!==s?(e={i:0,S:t,p:s.s,n:void 0,t:s,e:void 0,x:void 0,r:e},void 0!==s.s&&(s.s.n=e),s.s=e,t.n=e,32&s.f&&t.S(e),e):-1===e.i?(e.i=0,void 0!==e.n&&(e.n.p=e.p,void 0!==e.p&&(e.p.n=e.n),e.p=s.s,e.n=void 0,s.s.n=e,s.s=e),e):void 0}function f(t){this.v=t,this.i=0,this.n=void 0,this.t=void 0}function u(t){return new f(t)}function a(t){for(let e=t.s;void 0!==e;e=e.n)if(e.S.i!==e.i||!e.S.h()||e.S.i!==e.i)return!0;return!1}function l(t){for(let e=t.s;void 0!==e;e=e.n){const i=e.S.n;if(void 0!==i&&(e.r=i),e.S.n=e,e.i=-1,void 0===e.n){t.s=e;break}}}function d(t){let e,i=t.s;for(;void 0!==i;){const t=i.p;-1===i.i?(i.S.U(i),void 0!==t&&(t.n=i.n),void 0!==i.n&&(i.n.p=t)):e=i,i.S.n=i.r,void 0!==i.r&&(i.r=void 0),i=t}t.s=e}function p(t){f.call(this,void 0),this.x=t,this.s=void 0,this.g=c-1,this.f=4}function v(t){return new p(t)}function y(t){const i=t.u;if(t.u=void 0,"function"==typeof i){o++;const n=s;s=void 0;try{i()}catch(e){throw t.f&=-2,t.f|=8,g(t),e}finally{s=n,e()}}}function g(t){for(let e=t.s;void 0!==e;e=e.n)e.S.U(e);t.x=void 0,t.s=void 0,y(t)}function S(t){if(s!==this)throw new Error("Out-of-order effect");d(this),s=t,this.f&=-2,8&this.f&&g(this),e()}function b(t){this.x=t,this.u=void 0,this.s=void 0,this.o=void 0,this.f=32}function w(t){const e=new b(t);try{e.c()}catch(t){throw e.d(),t}return e.d.bind(e)}f.prototype.h=function(){return!0},f.prototype.S=function(t){this.t!==t&&void 0===t.e&&(t.x=this.t,void 0!==this.t&&(this.t.e=t),this.t=t)},f.prototype.U=function(t){if(void 0!==this.t){const e=t.e,i=t.x;void 0!==e&&(e.x=i,t.e=void 0),void 0!==i&&(i.e=e,t.x=void 0),t===this.t&&(this.t=i)}},f.prototype.subscribe=function(t){const e=this;return w((function(){const i=e.value,s=32&this.f;this.f&=-33;try{t(i)}finally{this.f|=s}}))},f.prototype.valueOf=function(){return this.value},f.prototype.toString=function(){return this.value+""},f.prototype.peek=function(){return this.v},Object.defineProperty(f.prototype,"value",{get(){const t=h(this);return void 0!==t&&(t.i=this.i),this.v},set(i){if(i!==this.v){r>100&&t(),this.v=i,this.i++,c++,o++;try{for(let t=this.t;void 0!==t;t=t.x)t.t.N()}finally{e()}}}}),(p.prototype=new f).h=function(){if(this.f&=-3,1&this.f)return!1;if(32==(36&this.f))return!0;if(this.f&=-5,this.g===c)return!0;if(this.g=c,this.f|=1,this.i>0&&!a(this))return this.f&=-2,!0;const t=s;try{l(this),s=this;const t=this.x();(16&this.f||this.v!==t||0===this.i)&&(this.v=t,this.f&=-17,this.i++)}catch(t){this.v=t,this.f|=16,this.i++}return s=t,d(this),this.f&=-2,!0},p.prototype.S=function(t){if(void 0===this.t){this.f|=36;for(let t=this.s;void 0!==t;t=t.n)t.S.S(t)}f.prototype.S.call(this,t)},p.prototype.U=function(t){if(void 0!==this.t&&(f.prototype.U.call(this,t),void 0===this.t)){this.f&=-33;for(let t=this.s;void 0!==t;t=t.n)t.S.U(t)}},p.prototype.N=function(){if(!(2&this.f)){this.f|=6;for(let t=this.t;void 0!==t;t=t.x)t.t.N()}},p.prototype.peek=function(){if(this.h()||t(),16&this.f)throw this.v;return this.v},Object.defineProperty(p.prototype,"value",{get(){1&this.f&&t();const e=h(this);if(this.h(),void 0!==e&&(e.i=this.i),16&this.f)throw this.v;return this.v}}),b.prototype.c=function(){const t=this.S();try{8&this.f||void 0===this.x||(this.u=this.x())}finally{t()}},b.prototype.S=function(){1&this.f&&t(),this.f|=1,this.f&=-9,y(this),l(this),o++;const e=s;return s=this,S.bind(this,e)},b.prototype.N=function(){2&this.f||(this.f|=2,this.o=n,n=this)},b.prototype.d=function(){this.f|=8,1&this.f||g(this)};var N=(0,Object.freeze)([]),m=()=>{};const x=(t,e,i)=>{const{parentNode:s}=i,n=e.length;let o=t.length,r=n,c=0,h=0,f=null;for(;ci-h){const n=t[c];for(;ht[e],B=({children:t},e)=>t[e],R=({value:t,properties:e,children:i})=>t(e,...i),$=({name:t})=>"key"===t,j=(t,{content:e})=>t.reduce(U,e),L=(t,e)=>t.reduce(B,e),W=(t,e)=>{const i=null==e?"":String(e);i!==t.data&&(t.data=i)}; 2 | /*! (c) Andrea Giammarchi - ISC */var F=(t={})=>{const e=t.document||globalThis.document,i=new Map(t.plugins||[]),s=!!i.size,n=t.diff||x,o=t.effect||(t=>(t(),m)),r=t.getPeek||(t=>t.peek()),c=t.getValue||(t=>t.value),h=t.isSignal||(t.Signal?k.bind(t.Signal.prototype):()=>!1),f=t=>e.createTextNode(t),u=(t,e)=>{let i=y.get(t);return i||y.set(t,i={}),i[e]||(i[e]={})},a=(t,e)=>{t.dispose();const i=o((()=>{const i=R(e);i.id!==t.id?t=l(t,i,!1):t.update(i)}));return t.dispose=i,t},l=(t,e,i)=>e.type===C?a(t,e):((t,e,i)=>(t.dispose(),t=new d(e),i?t.dispose=o((()=>t.update(e))):t.update(e),t))(t,e,i);class d{constructor(t,e=!0){const[i,s]=e?b(t):[N,null];this._=e&&t.type===M,this.id=t.id,this.updates=i,this.content=s,this.dispose=m}get $(){const{content:t,_:e}=this;return e?(this._=!e,(this.content=(({childNodes:t})=>({childNodes:[...t]}))(t)).childNodes):[t]}update(t){for(const e of this.updates)e.call(this,t)}}const p=new d({id:null},!1),v={id:null,view:p},y=new WeakMap;let g;const S=new WeakMap,b=t=>{let e=S.get(t.id);if(!e){const i=[],s=H(t,i,[],N,!1);S.set(t.id,e=[i,s])}const[i,s]=e;return[i.slice(),s.cloneNode(!0)]},w=(t,e,i,s)=>{s?t.setAttribute(e,i):t[e]=i},U=(t,e,i,s,n)=>{if(h(i)){const r="🙊"+e;r in s&&s[r](),s[r]=o((()=>{w(t,e,c(i),n)}))}else w(t,e,i,n)},B=(t,e,n,o)=>{if(s&&i.has(e))i.get(e)(t,n,o);else if(o[e]!==n)switch(o[e]=n,e){case"class":e+="Name";case"className":case"textContent":U(t,e,n,o,!1);break;case"ref":n.current=t;break;default:e.startsWith("on")?t[e.toLowerCase()]=n:e in t?U(t,e,n,o,!1):null==n?t.removeAttribute(e):U(t,e,n,o,!0)}},F=(t,e,i)=>function(s){const n={},o=j(e,this);(this.updates[i]=i=>{const{attributes:s}=L(e,i);for(const e of t){const t=s[e],{type:i,value:r}=t;if(i<2)B(o,t.name,r,n);else for(const[t,e]of E(r))B(o,t,e,n)}})(s)},_=(t,i)=>function(s){let o=N,r=!0,c=-1;const h=j(t,this),f=new Map;(this.updates[i]=i=>{const{value:s}=L(t,i),a=[];for(let t=0;tfunction(i){let s=N,o=p;const r=j(t,this);(this.updates[e]=e=>{o=a(o,L(t,e)),s=n(s,o.$,r)})(i)},G=(t,e)=>function(i){const s=j(t,this);(this.updates[e]=e=>{W(s,L(t,e).value)})(i)},D=(t,e)=>function(i){let s,h,u=m;const a=j(t,this),{value:d}=L(t,i),v=t=>{s!==t&&(u(),s=t,u=o(h))};if(g(r(d))){let t=N,e=p;h=()=>{const i=c(s);e=l(e,i,!1),t=n(t,e.$,a)}}else{const t=f("");a.replaceWith(t),h=()=>{W(t,c(s))}}this.updates[e]=e=>v(L(t,e).value),v(d)},V=(t,e)=>function(i){let s=N,o=p,r=null;const c=j(t,this);(this.updates[e]=e=>{e=L(t,e).value,r!==e.id?(r=e.id,o=l(o,e,!1),s=n(s,o.$,c)):e.type===C?o.update(R(e)):o.update(e)})(i)},q=({children:t},e,i,s,n)=>{for(let o=0;o{let r,c;const{length:u}=i;t:switch(t.type){case P:{const{value:e}=t;switch(!0){case g(e):r=V;break;case T(e):r=_;break;case h(e):r=D;break;default:c=f(""),i.push(G(n,u));break t}}case C:c=e.createComment("🙊"),i.push((r||z)(n,u));break;case A:{const{attributes:r,name:h}=t,f=[h],a=[];for(let t=0;t{const i="function"==typeof t?t():t;g||(g=k.bind(O(i)));const s=l(y.get(e)||p,i,!0);y.set(e,s),e.replaceChildren(...s.$)}};const _=(t={})=>F({...t,Signal:f,effect:w,isSignal:void 0});export{f as Signal,i as batch,v as computed,_ as createRender,w as effect,u as signal}; 3 | -------------------------------------------------------------------------------- /rollup/babel.config.js: -------------------------------------------------------------------------------- 1 | import {nodeResolve} from '@rollup/plugin-node-resolve'; 2 | 3 | export default { 4 | input: './esm/index.js', 5 | plugins: [ 6 | nodeResolve() 7 | ], 8 | output: { 9 | file: './index.js', 10 | format: 'module' 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /rollup/dist.js: -------------------------------------------------------------------------------- 1 | import {nodeResolve} from '@rollup/plugin-node-resolve'; 2 | import terser from '@rollup/plugin-terser'; 3 | 4 | export default name => ({ 5 | input: `./esm/dist/${name}.js`, 6 | plugins: [ 7 | nodeResolve(), 8 | terser() 9 | ], 10 | output: { 11 | file: `./${name}.js`, 12 | format: 'module' 13 | } 14 | }); 15 | -------------------------------------------------------------------------------- /rollup/es.config.js: -------------------------------------------------------------------------------- 1 | import {nodeResolve} from '@rollup/plugin-node-resolve'; 2 | import terser from '@rollup/plugin-terser'; 3 | 4 | export default { 5 | input: './esm/index.js', 6 | plugins: [ 7 | nodeResolve(), 8 | terser() 9 | ], 10 | output: { 11 | file: './es.js', 12 | format: 'module' 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /rollup/preact.config.js: -------------------------------------------------------------------------------- 1 | import dist from './dist.js'; 2 | 3 | export default dist('preact'); 4 | -------------------------------------------------------------------------------- /rollup/signal.config.js: -------------------------------------------------------------------------------- 1 | import dist from './dist.js'; 2 | 3 | export default dist('signal'); 4 | -------------------------------------------------------------------------------- /rollup/solid.config.js: -------------------------------------------------------------------------------- 1 | import dist from './dist.js'; 2 | 3 | export default dist('solid'); 4 | -------------------------------------------------------------------------------- /rollup/usignal.config.js: -------------------------------------------------------------------------------- 1 | import dist from './dist.js'; 2 | 3 | export default dist('usignal'); 4 | -------------------------------------------------------------------------------- /signal.js: -------------------------------------------------------------------------------- 1 | /*! (c) Andrea Giammarchi */ 2 | let e=null;const t=t=>{let s=e;s||(e=new Set);try{t()}finally{if(!s){[e,s]=[null,e];for(const e of s)e._()}}},s=e=>{const t=[...e];return e.clear(),t};class n extends Set{constructor(e){super()._=e}dispose(){for(const e of s(this))e.delete(this),e.dispose?.()}}let i=null;const o=e=>{const t=new n((()=>{const s=i;i=t;try{e()}finally{i=s}}));return t},r=(e,t)=>{const s=o((()=>{t=e(t)}));return i&&i.add(s),s._(),()=>s.dispose()};class a extends Set{constructor(e){super()._=e}get value(){return i&&i.add(this.add(i)),this._}set value(t){if(this._!==t){this._=t;const n=!e;for(const t of s(this))n?t._():e.add(t)}}peek(){return this._}then(e){e(this.value)}toJSON(){return this.value}valueOf(){return this.value}toString(){return String(this.value)}}const l=e=>new a(e);class c extends a{constructor(e,t){super(t).f=e,this.e=null}get value(){return this.e||(this.e=o((()=>{super.value=this.f(this._)})))._(),super.value}set value(e){throw new Error("computed is read-only")}}const u=(e,t)=>new c(e,t);var d=(0,Object.freeze)([]),h=()=>{};const f=(e,t,s)=>{const{parentNode:n}=s,i=t.length;let o=e.length,r=i,a=0,l=0,c=null;for(;as-l){const i=e[a];for(;le[t],E=({children:e},t)=>e[t],O=({value:e,properties:t,children:s})=>e(t,...s),k=({name:e})=>"key"===e,_=(e,{content:t})=>e.reduce(T,t),A=(e,t)=>e.reduce(E,t),C=(e,t)=>{const s=null==t?"":String(t);s!==e.data&&(e.data=s)}; 3 | /*! (c) Andrea Giammarchi - ISC */var M=(e={})=>{const t=e.document||globalThis.document,s=new Map(e.plugins||[]),n=!!s.size,i=e.diff||f,o=e.effect||(e=>(e(),h)),r=e.getPeek||(e=>e.peek()),a=e.getValue||(e=>e.value),l=e.isSignal||(e.Signal?b.bind(e.Signal.prototype):()=>!1),c=e=>t.createTextNode(e),u=(e,t)=>{let s=P.get(e);return s||P.set(e,s={}),s[t]||(s[t]={})},T=(e,t)=>{e.dispose();const s=o((()=>{const s=O(t);s.id!==e.id?e=E(e,s,!1):e.update(s)}));return e.dispose=s,e},E=(e,t,s)=>t.type===w?T(e,t):((e,t,s)=>(e.dispose(),e=new M(t),s?e.dispose=o((()=>e.update(t))):e.update(t),e))(e,t,s);class M{constructor(e,t=!0){const[s,n]=t?$(e):[d,null];this._=t&&e.type===y,this.id=e.id,this.updates=s,this.content=n,this.dispose=h}get $(){const{content:e,_:t}=this;return t?(this._=!t,(this.content=(({childNodes:e})=>({childNodes:[...e]}))(e)).childNodes):[e]}update(e){for(const t of this.updates)t.call(this,e)}}const x=new M({id:null},!1),I={id:null,view:x},P=new WeakMap;let B;const R=new WeakMap,$=e=>{let t=R.get(e.id);if(!t){const s=[],n=q(e,s,[],d,!1);R.set(e.id,t=[s,n])}const[s,n]=t;return[s.slice(),n.cloneNode(!0)]},L=(e,t,s,n)=>{n?e.setAttribute(t,s):e[t]=s},W=(e,t,s,n,i)=>{if(l(s)){const r="🙊"+t;r in n&&n[r](),n[r]=o((()=>{L(e,t,a(s),i)}))}else L(e,t,s,i)},j=(e,t,i,o)=>{if(n&&s.has(t))s.get(t)(e,i,o);else if(o[t]!==i)switch(o[t]=i,t){case"class":t+="Name";case"className":case"textContent":W(e,t,i,o,!1);break;case"ref":i.current=e;break;default:t.startsWith("on")?e[t.toLowerCase()]=i:t in e?W(e,t,i,o,!1):null==i?e.removeAttribute(t):W(e,t,i,o,!0)}},F=(e,t,s)=>function(n){const i={},o=_(t,this);(this.updates[s]=s=>{const{attributes:n}=A(t,s);for(const t of e){const e=n[t],{type:s,value:r}=e;if(s<2)j(o,e.name,r,i);else for(const[e,t]of g(r))j(o,e,t,i)}})(n)},z=(e,s)=>function(n){let o=d,r=!0,a=-1;const l=_(e,this),c=new Map;(this.updates[s]=s=>{const{value:n}=A(e,s),h=[];for(let e=0;efunction(s){let n=d,o=x;const r=_(e,this);(this.updates[t]=t=>{o=T(o,A(e,t)),n=i(n,o.$,r)})(s)},D=(e,t)=>function(s){const n=_(e,this);(this.updates[t]=t=>{C(n,A(e,t).value)})(s)},J=(e,t)=>function(s){let n,l,u=h;const f=_(e,this),{value:p}=A(e,s),g=e=>{n!==e&&(u(),n=e,u=o(l))};if(B(r(p))){let e=d,t=x;l=()=>{const s=a(n);t=E(t,s,!1),e=i(e,t.$,f)}}else{const e=c("");f.replaceWith(e),l=()=>{C(e,a(n))}}this.updates[t]=t=>g(A(e,t).value),g(p)},U=(e,t)=>function(s){let n=d,o=x,r=null;const a=_(e,this);(this.updates[t]=t=>{t=A(e,t).value,r!==t.id?(r=t.id,o=E(o,t,!1),n=i(n,o.$,a)):t.type===w?o.update(O(t)):o.update(t)})(s)},V=({children:e},t,s,n,i)=>{for(let o=0;o{let r,a;const{length:u}=s;e:switch(e.type){case m:{const{value:t}=e;switch(!0){case B(t):r=U;break;case p(t):r=z;break;case l(t):r=J;break;default:a=c(""),s.push(D(i,u));break e}}case w:a=t.createComment("🙊"),s.push((r||G)(i,u));break;case N:{const{attributes:r,name:l}=e,c=[l],d=[];for(let e=0;e{const s="function"==typeof e?e():e;B||(B=b.bind(v(s)));const n=E(P.get(t)||x,s,!0);P.set(t,n),t.replaceChildren(...n.$)}};const x=(e={})=>M({...e,Signal:a,effect:r,isSignal:void 0});export{c as Computed,a as Signal,t as batch,u as computed,x as createRender,r as effect,l as signal}; 4 | -------------------------------------------------------------------------------- /solid.js: -------------------------------------------------------------------------------- 1 | let e=1,t=!1,n=!1,o=[],r=null,s=null,l=5,i=0,u=300,c=null,a=null;const f=1073741823;function d(r,d){c||function(){const e=new MessageChannel,t=e.port2;if(c=()=>t.postMessage(null),e.port1.onmessage=()=>{if(null!==a){const e=performance.now();i=e+l;const n=!0;try{a(n,e)?t.postMessage(null):a=null}catch(e){throw t.postMessage(null),e}}},navigator&&navigator.scheduling&&navigator.scheduling.isInputPending){const e=navigator.scheduling;s=()=>{const t=performance.now();return t>=i&&(!!e.isInputPending()||t>=u)}}else s=()=>performance.now()>=i}();let h=performance.now(),g=f;d&&d.timeout&&(g=d.timeout);const v={id:e++,fn:r,startTime:h,expirationTime:h+g};return function(e,t){e.splice(function(){let n=0,o=e.length-1;for(;n<=o;){const r=o+n>>1,s=t.expirationTime-e[r].expirationTime;if(s>0)n=r+1;else{if(!(s<0))return r;o=r-1}}return n}(),0,t)}(o,v),t||n||(t=!0,a=p,c()),v}function h(e){e.fn=null}function p(e,l){t=!1,n=!0;try{return function(e,t){let n=t;r=o[0]||null;for(;null!==r&&(!(r.expirationTime>n)||e&&!s());){const e=r.fn;if(null!==e){r.fn=null;e(r.expirationTime<=n),n=performance.now(),r===o[0]&&o.shift()}else o.shift();r=o[0]||null}return null!==r}(e,l)}finally{r=null,n=!1}}const g={};function v(e){g.context=e}const b=(e,t)=>e===t,w=Symbol("solid-proxy"),y=Symbol("solid-track"),m=Symbol("solid-dev-component"),k={equals:b};let S=null,x=ye;const O=1,A=2,P={owned:null,cleanups:null,context:null,owner:null},T={};var E=null;let M=null,N=null,F=null,C=null,j=null,$=null,V=0;const[q,I]=L(!1);function B(e,t){const n=C,o=E,r=0===e.length,s=r?P:{owned:null,cleanups:null,context:null,owner:t||o},l=r?e:()=>e((()=>J((()=>xe(s)))));E=s,C=null;try{return we(l,!0)}finally{C=n,E=o}}function L(e,t){const n={value:e,observers:null,observerSlots:null,comparator:(t=t?Object.assign({},k,t):k).equals||void 0};return[de.bind(n),e=>("function"==typeof e&&(e=M&&M.running&&M.sources.has(n)?e(n.tValue):e(n.value)),he(n,e))]}function R(e,t,n){const o=ve(e,t,!0,O);N&&M&&M.running?j.push(o):pe(o)}function z(e,t,n){const o=ve(e,t,!1,O);N&&M&&M.running?j.push(o):pe(o)}function W(e,t,n){x=me;const o=ve(e,t,!1,O),r=ce&&Te(E,ce.id);r&&(o.suspense=r),o.user=!0,$?$.push(o):pe(o)}function D(e,t){let n;const o=ve((()=>{n?n():J(e),n=void 0}),void 0,!1,0),r=ce&&Te(E,ce.id);return r&&(o.suspense=r),o.user=!0,e=>{n=e,pe(o)}}function _(e,t,n){n=n?Object.assign({},k,n):k;const o=ve(e,t,!0,0);return o.observers=null,o.observerSlots=null,o.comparator=n.equals||void 0,N&&M&&M.running?(o.tState=O,j.push(o)):pe(o),de.bind(o)}function G(e,t,n){let o,r,s;2===arguments.length&&"object"==typeof t||1===arguments.length?(o=!0,r=e,s=t||{}):(o=e,r=t,s=n||{});let l=null,i=T,u=null,c=!1,a=!1,f="initialValue"in s,d="function"==typeof o&&_(o);const h=new Set,[p,v]=(s.storage||L)(s.initialValue),[b,w]=L(void 0),[y,m]=L(void 0,{equals:!1}),[k,S]=L(f?"ready":"unresolved");if(g.context){let e;u=`${g.context.id}${g.context.count++}`,"initial"===s.ssrLoadFrom?i=s.initialValue:g.load&&(e=g.load(u))&&(i=e[0])}function x(e,t,n,o){return l===e&&(l=null,f=!0,e!==i&&t!==i||!s.onHydrated||queueMicrotask((()=>s.onHydrated(o,{value:t}))),i=T,M&&e&&c?(M.promises.delete(e),c=!1,we((()=>{M.running=!0,O(t,n)}),!1)):O(t,n)),t}function O(e,t){we((()=>{t||v((()=>e)),S(t?"errored":"ready"),w(t);for(const e of h.keys())e.decrement();h.clear()}),!1)}function A(){const e=ce&&Te(E,ce.id),t=p(),n=b();if(n&&!l)throw n;return C&&!C.user&&e&&R((()=>{y(),l&&(e.resolved&&M&&c?M.promises.add(l):h.has(e)||(e.increment(),h.add(e)))})),t}function P(e=!0){if(!1!==e&&a)return;a=!1;const t=d?d():o;if(c=M&&M.running,null==t||!1===t)return void x(l,J(p));M&&l&&M.promises.delete(l);const n=i!==T?i:J((()=>r(t,{value:p(),refetching:e})));return"object"==typeof n&&n&&"then"in n?(l=n,a=!0,queueMicrotask((()=>a=!1)),we((()=>{S(f?"refreshing":"pending"),m()}),!1),n.then((e=>x(n,e,void 0,t)),(e=>x(n,void 0,Ae(e),t)))):(x(l,n,void 0,t),n)}return Object.defineProperties(A,{state:{get:()=>k()},error:{get:()=>b()},loading:{get(){const e=k();return"pending"===e||"refreshing"===e}},latest:{get(){if(!f)return A();const e=b();if(e&&!l)throw e;return p()}}}),d?R((()=>P(!1))):P(!1),[A,{refetch:P,mutate:v}]}function H(e,t){let n,o=t?t.timeoutMs:void 0;const r=ve((()=>(n&&n.fn||(n=d((()=>l((()=>r.value))),void 0!==o?{timeout:o}:void 0)),e())),void 0,!0),[s,l]=L(r.value,t);return pe(r),l((()=>r.value)),s}function U(e,t=b,n){const o=new Map,r=ve((n=>{const r=e();for(const[e,s]of o.entries())if(t(e,r)!==t(e,n))for(const e of s.values())e.state=O,e.pure?j.push(e):$.push(e);return r}),void 0,!0,O);return pe(r),e=>{const n=C;if(n){let t;(t=o.get(e))?t.add(n):o.set(e,t=new Set([n])),Y((()=>{t.delete(n),!t.size&&o.delete(e)}))}return t(e,M&&M.running&&M.sources.has(r)?r.tValue:r.value)}}function K(e){return we(e,!1)}function J(e){const t=C;C=null;try{return e()}finally{C=t}}function Q(e,t,n){const o=Array.isArray(e);let r,s=n&&n.defer;return n=>{let l;if(o){l=Array(e.length);for(let t=0;tt(l,r,n)));return r=l,i}}function X(e){W((()=>J(e)))}function Y(e){return null===E||(null===E.cleanups?E.cleanups=[e]:E.cleanups.push(e)),e}function Z(e){S||(S=Symbol("error")),null===E||(null===E.context?E.context={[S]:[e]}:E.context[S]?E.context[S].push(e):E.context[S]=[e])}function ee(){return C}function te(){return E}function ne(e,t){const n=E,o=C;E=e,C=null;try{return we(t,!0)}catch(e){Pe(e)}finally{E=n,C=o}}function oe(e=d){N=e}function re(e){if(M&&M.running)return e(),M.done;const t=C,n=E;return Promise.resolve().then((()=>{let o;return C=t,E=n,(N||ce)&&(o=M||(M={sources:new Set,effects:[],promises:new Set,disposed:new Set,queue:new Set,running:!0}),o.done||(o.done=new Promise((e=>o.resolve=e))),o.running=!0),we(e,!1),C=E=null,o?o.done:void 0}))}function se(){return[q,re]}function le(e,t){const n=Symbol("context");return{id:n,Provider:Me(n),defaultValue:e}}function ie(e){let t;return void 0!==(t=Te(E,e.id))?t:e.defaultValue}function ue(e){const t=_(e),n=_((()=>Ee(t())));return n.toArray=()=>{const e=n();return Array.isArray(e)?e:null!=e?[e]:[]},n}let ce;function ae(){return ce||(ce=le({}))}function fe(e){if(F){const t=F;F=(n,o)=>{const r=t(n,o),s=e((e=>r.track(e)),o);return{track:e=>s.track(e),dispose(){s.dispose(),r.dispose()}}}}else F=e}function de(){const e=M&&M.running;if(this.sources&&(!e&&this.state||e&&this.tState))if(!e&&this.state===O||e&&this.tState===O)pe(this);else{const e=j;j=null,we((()=>ke(this)),!1),j=e}if(C){const e=this.observers?this.observers.length:0;C.sources?(C.sources.push(this),C.sourceSlots.push(e)):(C.sources=[this],C.sourceSlots=[e]),this.observers?(this.observers.push(C),this.observerSlots.push(C.sources.length-1)):(this.observers=[C],this.observerSlots=[C.sources.length-1])}return e&&M.sources.has(this)?this.tValue:this.value}function he(e,t,n){let o=M&&M.running&&M.sources.has(e)?e.tValue:e.value;if(!e.comparator||!e.comparator(o,t)){if(M){const o=M.running;(o||!n&&M.sources.has(e))&&(M.sources.add(e),e.tValue=t),o||(e.value=t)}else e.value=t;e.observers&&e.observers.length&&we((()=>{for(let t=0;t1e6)throw j=[],new Error}),!1)}return t}function pe(e){if(!e.fn)return;xe(e);const t=E,n=C,o=V;C=E=e,ge(e,M&&M.running&&M.sources.has(e)?e.tValue:e.value,o),M&&!M.running&&M.sources.has(e)&&queueMicrotask((()=>{we((()=>{M&&(M.running=!0),C=E=e,ge(e,e.tValue,o),C=E=null}),!1)})),C=n,E=t}function ge(e,t,n){let o;try{o=e.fn(t)}catch(t){e.pure&&(M&&M.running?(e.tState=O,e.tOwned&&e.tOwned.forEach(xe),e.tOwned=void 0):(e.state=O,e.owned&&e.owned.forEach(xe),e.owned=null)),Pe(t)}(!e.updatedAt||e.updatedAt<=n)&&(null!=e.updatedAt&&"observers"in e?he(e,o,!0):M&&M.running&&e.pure?(M.sources.add(e),e.tValue=o):e.value=o,e.updatedAt=n)}function ve(e,t,n,o=O,r){const s={fn:e,state:o,updatedAt:null,owned:null,sources:null,sourceSlots:null,cleanups:null,value:t,owner:E,context:null,pure:n};if(M&&M.running&&(s.state=0,s.tState=o),null===E||E!==P&&(M&&M.running&&E.pure?E.tOwned?E.tOwned.push(s):E.tOwned=[s]:E.owned?E.owned.push(s):E.owned=[s]),F){const[e,t]=L(void 0,{equals:!1}),n=F(s.fn,t);Y((()=>n.dispose()));const o=()=>re(t).then((()=>r.dispose())),r=F(s.fn,o);s.fn=t=>(e(),M&&M.running?r.track(t):n.track(t))}return s}function be(e){const t=M&&M.running;if(!t&&0===e.state||t&&0===e.tState)return;if(!t&&e.state===A||t&&e.tState===A)return ke(e);if(e.suspense&&J(e.suspense.inFallback))return e.suspense.effects.push(e);const n=[e];for(;(e=e.owner)&&(!e.updatedAt||e.updatedAt=0;o--){if(e=n[o],t){let t=e,r=n[o+1];for(;(t=t.owner)&&t!==r;)if(M.disposed.has(t))return}if(!t&&e.state===O||t&&e.tState===O)pe(e);else if(!t&&e.state===A||t&&e.tState===A){const t=j;j=null,we((()=>ke(e,n[0])),!1),j=t}}}function we(e,t){if(j)return e();let n=!1;t||(j=[]),$?n=!0:$=[],V++;try{const t=e();return function(e){j&&(N&&M&&M.running?function(e){for(let t=0;t{o.delete(n),we((()=>{M.running=!0,be(n)}),!1),M&&(M.running=!1)})))}}(j):ye(j),j=null);if(e)return;let t;if(M)if(M.promises.size||M.queue.size){if(M.running)return M.running=!1,M.effects.push.apply(M.effects,$),$=null,void I(!0)}else{const e=M.sources,n=M.disposed;$.push.apply($,M.effects),t=M.resolve;for(const e of $)"tState"in e&&(e.state=e.tState),delete e.tState;M=null,we((()=>{for(const e of n)xe(e);for(const t of e){if(t.value=t.tValue,t.owned)for(let e=0,n=t.owned.length;ex(n)),!1);t&&t()}(n),t}catch(e){j||($=null),j=null,Pe(e)}}function ye(e){for(let t=0;tn=J((()=>(E.context={[e]:t.value},ue((()=>t.children)))))),void 0),n}}function Ne(e){return{subscribe(t){if(!(t instanceof Object)||null==t)throw new TypeError("Expected the observer to be an object.");const n="function"==typeof t?t:t.next&&t.next.bind(t);if(!n)return{unsubscribe(){}};const o=B((t=>(W((()=>{const t=e();J((()=>n(t)))})),t)));return te()&&Y(o),{unsubscribe(){o()}}},[Symbol.observable||"@@observable"](){return this}}}function Fe(e){const[t,n]=L(void 0,{equals:!1});if("subscribe"in e){const t=e.subscribe((e=>n((()=>e))));Y((()=>"unsubscribe"in t?t.unsubscribe():t()))}else{Y(e(n))}return t}const Ce=Symbol("fallback");function je(e){for(let t=0;t1?[]:null;return Y((()=>je(s))),()=>{let u,c,a=e()||[];return a[y],J((()=>{let e,t,d,h,p,g,v,b,w,y=a.length;if(0===y)0!==l&&(je(s),s=[],o=[],r=[],l=0,i&&(i=[])),n.fallback&&(o=[Ce],r[0]=B((e=>(s[0]=e,n.fallback()))),l=1);else if(0===l){for(r=new Array(y),c=0;c=g&&b>=g&&o[v]===a[b];v--,b--)d[b]=r[v],h[b]=s[v],i&&(p[b]=i[v]);for(e=new Map,t=new Array(b+1),c=b;c>=g;c--)w=a[c],u=e.get(w),t[c]=void 0===u?-1:u,e.set(w,c);for(u=g;u<=v;u++)w=o[u],c=e.get(w),void 0!==c&&-1!==c?(d[c]=r[u],h[c]=s[u],i&&(p[c]=i[u]),c=t[c],e.set(w,c)):s[u]();for(c=g;cje(l))),()=>{const c=e()||[];return c[y],J((()=>{if(0===c.length)return 0!==u&&(je(l),l=[],r=[],s=[],u=0,i=[]),n.fallback&&(r=[Ce],s[0]=B((e=>(l[0]=e,n.fallback()))),u=1),s;for(r[0]===Ce&&(l[0](),l=[],r=[],s=[],u=0),o=0;oc[o])):o>=r.length&&(s[o]=B(a));for(;oe(t||{})));return v(n),o}return J((()=>e(t||{})))}function Le(){return!0}const Re={get:(e,t,n)=>t===w?n:e.get(t),has:(e,t)=>t===w||e.has(t),set:Le,deleteProperty:Le,getOwnPropertyDescriptor:(e,t)=>({configurable:!0,enumerable:!0,get:()=>e.get(t),set:Le,deleteProperty:Le}),ownKeys:e=>e.keys()};function ze(e){return(e="function"==typeof e?e():e)?e:{}}function We(...e){let t=!1;for(let n=0;n=0;n--){const o=ze(e[n])[t];if(void 0!==o)return o}},has(t){for(let n=e.length-1;n>=0;n--)if(t in ze(e[n]))return!0;return!1},keys(){const t=[];for(let n=0;n=0;t--)if(e[t]){const o=Object.getOwnPropertyDescriptors(e[t]);for(const t in o)t in n||Object.defineProperty(n,t,{enumerable:!0,get(){for(let n=e.length-1;n>=0;n--){const o=(e[n]||{})[t];if(void 0!==o)return o}}})}return n}function De(e,...t){const n=new Set(t.flat());if(w in e){const o=t.map((t=>new Proxy({get:n=>t.includes(n)?e[n]:void 0,has:n=>t.includes(n)&&n in e,keys:()=>t.filter((t=>t in e))},Re)));return o.push(new Proxy({get:t=>n.has(t)?void 0:e[t],has:t=>!n.has(t)&&t in e,keys:()=>Object.keys(e).filter((e=>!n.has(e)))},Re)),o}const o=Object.getOwnPropertyDescriptors(e);return t.push(Object.keys(o).filter((e=>!n.has(e)))),t.map((t=>{const n={};for(let r=0;re[s],set:()=>!0,enumerable:!0})}return n}))}function _e(e){let t,n;const o=o=>{const r=g.context;if(r){const[o,s]=L();(n||(n=e())).then((e=>{v(r),s((()=>e.default)),v()})),t=o}else if(!t){const[o]=G((()=>(n||(n=e())).then((e=>e.default))));t=o}let s;return _((()=>(s=t())&&J((()=>{if(!r)return s(o);const e=g.context;v(r);const t=s(o);return v(e),t}))))};return o.preload=()=>n||((n=e()).then((e=>t=()=>e.default)),n),o}let Ge,He=0;function Ue(){const e=g.context;return e?`${e.id}${e.count++}`:"cl-"+He++}function Ke(e){const t="fallback"in e&&{fallback:()=>e.fallback};return _($e((()=>e.each),e.children,t||void 0))}function Je(e){const t="fallback"in e&&{fallback:()=>e.fallback};return _(Ve((()=>e.each),e.children,t||void 0))}function Qe(e){let t=!1;const n=e.keyed,o=_((()=>e.when),void 0,{equals:(e,n)=>t?e===n:!e==!n});return _((()=>{const r=o();if(r){const o=e.children,s="function"==typeof o&&o.length>0;return t=n||s,s?J((()=>o(r))):o}return e.fallback}),void 0,void 0)}function Xe(e){let t=!1,n=!1;const o=ue((()=>e.children)),r=_((()=>{let e=o();Array.isArray(e)||(e=[e]);for(let t=0;te[0]===n[0]&&(t?e[1]===n[1]:!e[1]==!n[1])&&e[2]===n[2]});return _((()=>{const[o,s,l]=r();if(o<0)return e.fallback;const i=l.children,u="function"==typeof i&&i.length>0;return t=n||u,u?J((()=>i(s))):i}),void 0,void 0)}function Ye(e){return e}function Ze(){Ge&&[...Ge].forEach((e=>e()))}function et(e){let t,n;g.context&&g.load&&(n=g.load(g.context.id+g.context.count))&&(t=n[0]);const[o,r]=L(t,void 0);return Ge||(Ge=new Set),Ge.add(r),Y((()=>Ge.delete(r))),_((()=>{let t;if(t=o()){const n=e.fallback,o="function"==typeof n&&n.length?J((()=>n(t,(()=>r())))):n;return Z(r),o}return Z(r),e.children}),void 0,void 0)}const tt=(e,t)=>e.showContent===t.showContent&&e.showFallback===t.showFallback,nt=le();function ot(e){let t,[n,o]=L((()=>({inFallback:!1})));const r=ie(nt),[s,l]=L([]);r&&(t=r.register(_((()=>n()().inFallback))));const i=_((n=>{const o=e.revealOrder,r=e.tail,{showContent:l=!0,showFallback:i=!0}=t?t():{},u=s(),c="backwards"===o;if("together"===o){const e=u.every((e=>!e())),t=u.map((()=>({showContent:e&&l,showFallback:i})));return t.inFallback=!e,t}let a=!1,f=n.inFallback;const d=[];for(let e=0,t=u.length;ei)),Be(nt.Provider,{value:{register:e=>{let t;return l((n=>(t=n.length,[...n,e]))),_((()=>i()[t]),void 0,{equals:tt})}},get children(){return e.children}})}function rt(e){let t,n,o,r,s,l=0;const[i,u]=L(!1),c=ae(),a={increment:()=>{1==++l&&u(!0)},decrement:()=>{0==--l&&u(!1)},inFallback:i,effects:[],resolved:!1},f=te();if(g.context&&g.load){const e=g.context.id+g.context.count;let t=g.load(e);if(t&&(o=t[0])&&"$$f"!==o){"object"==typeof o&&"then"in o||(o=Promise.resolve(o));const[t,l]=L(void 0,{equals:!1});r=t,o.then((t=>{if(t||g.done)return t&&(s=t),l();g.gather(e),v(n),l(),v()}))}}const d=ie(nt);let h;return d&&(t=d.register(a.inFallback)),Y((()=>h&&h())),Be(c.Provider,{value:a,get children(){return _((()=>{if(s)throw s;if(n=g.context,r)return r(),r=void 0;n&&"$$f"===o&&v();const l=_((()=>e.children));return _((r=>{const s=a.inFallback(),{showContent:i=!0,showFallback:u=!0}=t?t():{};return(!s||o&&"$$f"!==o)&&i?(a.resolved=!0,h&&h(),h=n=o=void 0,c=a.effects,$.push.apply($,c),c.length=0,l()):u?h?r:B((t=>(h=t,n&&(v({id:n.id+"f",count:0}),n=void 0),e.fallback)),f):void 0;var c}))}))}})}let st;var lt=(0,Object.freeze)([]),it=()=>{};const ut=(e,t,n)=>{const{parentNode:o}=n,r=t.length;let s=e.length,l=r,i=0,u=0,c=null;for(;in-u){const r=e[i];for(;ue[t],yt=({children:e},t)=>e[t],mt=({value:e,properties:t,children:n})=>e(t,...n),kt=({name:e})=>"key"===e,St=(e,{content:t})=>e.reduce(wt,t),xt=(e,t)=>e.reduce(yt,t),Ot=(e,t)=>{const n=null==t?"":String(t);n!==e.data&&(e.data=n)}; 2 | /*! (c) Andrea Giammarchi - ISC */var At=(e={})=>{const t=e.document||globalThis.document,n=new Map(e.plugins||[]),o=!!n.size,r=e.diff||ut,s=e.effect||(e=>(e(),it)),l=e.getPeek||(e=>e.peek()),i=e.getValue||(e=>e.value),u=e.isSignal||(e.Signal?dt.bind(e.Signal.prototype):()=>!1),c=e=>t.createTextNode(e),a=(e,t)=>{let n=v.get(e);return n||v.set(e,n={}),n[t]||(n[t]={})},f=(e,t)=>{e.dispose();const n=s((()=>{const n=mt(t);n.id!==e.id?e=d(e,n,!1):e.update(n)}));return e.dispose=n,e},d=(e,t,n)=>t.type===ht?f(e,t):((e,t,n)=>(e.dispose(),e=new h(t),n?e.dispose=s((()=>e.update(t))):e.update(t),e))(e,t,n);class h{constructor(e,t=!0){const[n,o]=t?y(e):[lt,null];this._=t&&e.type===gt,this.id=e.id,this.updates=n,this.content=o,this.dispose=it}get $(){const{content:e,_:t}=this;return t?(this._=!t,(this.content=(({childNodes:e})=>({childNodes:[...e]}))(e)).childNodes):[e]}update(e){for(const t of this.updates)t.call(this,e)}}const p=new h({id:null},!1),g={id:null,view:p},v=new WeakMap;let b;const w=new WeakMap,y=e=>{let t=w.get(e.id);if(!t){const n=[],o=N(e,n,[],lt,!1);w.set(e.id,t=[n,o])}const[n,o]=t;return[n.slice(),o.cloneNode(!0)]},m=(e,t,n,o)=>{o?e.setAttribute(t,n):e[t]=n},k=(e,t,n,o,r)=>{if(u(n)){const l="🙊"+t;l in o&&o[l](),o[l]=s((()=>{m(e,t,i(n),r)}))}else m(e,t,n,r)},S=(e,t,r,s)=>{if(o&&n.has(t))n.get(t)(e,r,s);else if(s[t]!==r)switch(s[t]=r,t){case"class":t+="Name";case"className":case"textContent":k(e,t,r,s,!1);break;case"ref":r.current=e;break;default:t.startsWith("on")?e[t.toLowerCase()]=r:t in e?k(e,t,r,s,!1):null==r?e.removeAttribute(t):k(e,t,r,s,!0)}},x=(e,t,n)=>function(o){const r={},s=St(t,this);(this.updates[n]=n=>{const{attributes:o}=xt(t,n);for(const t of e){const e=o[t],{type:n,value:l}=e;if(n<2)S(s,e.name,l,r);else for(const[e,t]of at(l))S(s,e,t,r)}})(o)},O=(e,n)=>function(o){let s=lt,l=!0,i=-1;const u=St(e,this),c=new Map;(this.updates[n]=n=>{const{value:o}=xt(e,n),f=[];for(let e=0;efunction(n){let o=lt,s=p;const l=St(e,this);(this.updates[t]=t=>{s=f(s,xt(e,t)),o=r(o,s.$,l)})(n)},P=(e,t)=>function(n){const o=St(e,this);(this.updates[t]=t=>{Ot(o,xt(e,t).value)})(n)},T=(e,t)=>function(n){let o,u,a=it;const f=St(e,this),{value:h}=xt(e,n),g=e=>{o!==e&&(a(),o=e,a=s(u))};if(b(l(h))){let e=lt,t=p;u=()=>{const n=i(o);t=d(t,n,!1),e=r(e,t.$,f)}}else{const e=c("");f.replaceWith(e),u=()=>{Ot(e,i(o))}}this.updates[t]=t=>g(xt(e,t).value),g(h)},E=(e,t)=>function(n){let o=lt,s=p,l=null;const i=St(e,this);(this.updates[t]=t=>{t=xt(e,t).value,l!==t.id?(l=t.id,s=d(s,t,!1),o=r(o,s.$,i)):t.type===ht?s.update(mt(t)):s.update(t)})(n)},M=({children:e},t,n,o,r)=>{for(let s=0;s{let l,i;const{length:a}=n;e:switch(e.type){case vt:{const{value:t}=e;switch(!0){case b(t):l=E;break;case ct(t):l=O;break;case u(t):l=T;break;default:i=c(""),n.push(P(r,a));break e}}case ht:i=t.createComment("🙊"),n.push((l||A)(r,a));break;case pt:{const{attributes:l,name:u}=e,c=[u],f=[];for(let e=0;e{const n="function"==typeof e?e():e;b||(b=dt.bind(ft(n)));const o=d(v.get(t)||p,n,!0);v.set(t,o),t.replaceChildren(...o.$)}};const Pt=new WeakSet,Tt=()=>{},Et=(e,...t)=>{const[n,o]=L(e,...t);return Pt.add(n),[n,o]},Mt=(e={})=>At({...e,effect:(...e)=>(W(...e),Tt),getPeek:e=>J(e),getValue:e=>e(),isSignal:e=>Pt.has(e)});export{m as $DEVCOMP,w as $PROXY,y as $TRACK,st as DEV,et as ErrorBoundary,Ke as For,Je as Index,Ye as Match,Qe as Show,rt as Suspense,ot as SuspenseList,Xe as Switch,K as batch,h as cancelCallback,ue as children,Be as createComponent,R as createComputed,le as createContext,H as createDeferred,W as createEffect,_ as createMemo,D as createReaction,Mt as createRender,z as createRenderEffect,G as createResource,B as createRoot,U as createSelector,Et as createSignal,Ue as createUniqueId,fe as enableExternalSource,Ie as enableHydration,oe as enableScheduling,b as equalFn,Fe as from,ee as getListener,te as getOwner,Ve as indexArray,_e as lazy,$e as mapArray,We as mergeProps,Ne as observable,Q as on,Y as onCleanup,Z as onError,X as onMount,d as requestCallback,Ze as resetErrorBoundaries,ne as runWithOwner,g as sharedConfig,De as splitProps,re as startTransition,J as untrack,ie as useContext,se as useTransition}; 3 | -------------------------------------------------------------------------------- /test/array.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | udomsay 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /test/array.js: -------------------------------------------------------------------------------- 1 | var _templateReference = {}, 2 | _templateReference2 = {}, 3 | _templateReference3 = {}, 4 | _templateReference4 = {}; 5 | globalThis.ESXToken || (globalThis.ESXToken = class ESXToken { static ATTRIBUTE = 1; static COMPONENT = 2; static ELEMENT = 3; static FRAGMENT = 4; static INTERPOLATION = 5; static STATIC = 6; static _ = Object.freeze([]); static a = (dynamic, name, value) => ({ type: 1, dynamic, name, value }); static b = (type, value) => ({ type, value }); constructor(id, type, attributes, children, name, value) { this.id = id; this.type = type; this.attributes = attributes; this.children = children; this.name = name; this.value = value; } get properties() { const { attributes } = this; if (attributes.length) { const properties = {}; for (const entry of attributes) { if (entry.type < 2) properties[entry.name] = entry.value;else Object.assign(properties, entry.value); } return properties; } return null; } }); 6 | import createRender from '../index.js'; 7 | import { Signal, signal, effect } from 'https://unpkg.com/@webreflection/signal'; 8 | const render = createRender({ 9 | Signal, 10 | effect 11 | }); 12 | function Counter({ 13 | signal 14 | }) { 15 | console.log('Counter'); 16 | return new ESXToken(_templateReference, 3, ESXToken._, [new ESXToken(null, 3, [ESXToken.a(true, "onclick", () => { 17 | signal.value--; 18 | })], [ESXToken.b(6, "-")], "button", "button"), new ESXToken(null, 3, ESXToken._, [ESXToken.b(5, signal.value)], "span", "span"), new ESXToken(null, 3, [ESXToken.a(true, "onclick", () => { 19 | signal.value++; 20 | })], [ESXToken.b(6, "+")], "button", "button")], "div", "div"); 21 | } 22 | const Counters = ({ 23 | many 24 | }) => { 25 | console.log('Counters'); 26 | const data = []; 27 | for (let i = 0; i < many; i++) data[i] = new ESXToken(_templateReference2, 2, [ESXToken.a(true, "signal", signal(0))], ESXToken._, "Counter", Counter); 28 | return new ESXToken(_templateReference3, 3, ESXToken._, [ESXToken.b(5, data)], "div", "div"); 29 | }; 30 | render(new ESXToken(_templateReference4, 2, [ESXToken.a(true, "many", 3)], ESXToken._, "Counters", Counters), document.body); 31 | -------------------------------------------------------------------------------- /test/array.jsx: -------------------------------------------------------------------------------- 1 | import createRender from '../index.js'; 2 | import {Signal, signal, effect} from 'https://unpkg.com/@webreflection/signal'; 3 | const render = createRender({Signal, effect}); 4 | 5 | function Counter({signal}) { 6 | console.log('Counter'); 7 | return ( 8 |
9 | 10 | { signal.value } 11 | 12 |
13 | ); 14 | } 15 | 16 | const Counters = ({many}) => { 17 | console.log('Counters'); 18 | const data = []; 19 | for (let i = 0; i < many; i++) 20 | data[i] = ; 21 | return ( 22 |
23 | {data} 24 |
25 | ); 26 | }; 27 | 28 | render(, document.body); 29 | -------------------------------------------------------------------------------- /test/base.js: -------------------------------------------------------------------------------- 1 | var _templateReference = {}; 2 | globalThis.ESXToken || (globalThis.ESXToken = class ESXToken { static ATTRIBUTE = 1; static COMPONENT = 2; static ELEMENT = 3; static FRAGMENT = 4; static INTERPOLATION = 5; static STATIC = 6; static _ = Object.freeze([]); static a = (dynamic, name, value) => ({ type: 1, dynamic, name, value }); static b = (type, value) => ({ type, value }); constructor(id, type, attributes, children, name, value) { this.id = id; this.type = type; this.attributes = attributes; this.children = children; this.name = name; this.value = value; } get properties() { const { attributes } = this; if (attributes.length) { const properties = {}; for (const entry of attributes) { if (entry.type < 2) properties[entry.name] = entry.value;else Object.assign(properties, entry.value); } return properties; } return null; } }); 3 | const div = new ESXToken(_templateReference, 3, [ESXToken.a(true, "a", 1), ESXToken.a(false, "b", "2")], ESXToken._, "div", "div"); 4 | -------------------------------------------------------------------------------- /test/base.jsx: -------------------------------------------------------------------------------- 1 | const div =
; 2 | -------------------------------------------------------------------------------- /test/children.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | udomsay 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /test/children.js: -------------------------------------------------------------------------------- 1 | var _templateReference = {}, 2 | _templateReference2 = {}; 3 | globalThis.ESXToken || (globalThis.ESXToken = class ESXToken { static ATTRIBUTE = 1; static COMPONENT = 2; static ELEMENT = 3; static FRAGMENT = 4; static INTERPOLATION = 5; static STATIC = 6; static _ = Object.freeze([]); static a = (dynamic, name, value) => ({ type: 1, dynamic, name, value }); static b = (type, value) => ({ type, value }); constructor(id, type, attributes, children, name, value) { this.id = id; this.type = type; this.attributes = attributes; this.children = children; this.name = name; this.value = value; } get properties() { const { attributes } = this; if (attributes.length) { const properties = {}; for (const entry of attributes) { if (entry.type < 2) properties[entry.name] = entry.value;else Object.assign(properties, entry.value); } return properties; } return null; } }); 4 | // grab signals from various libaries, here the simplest I know 5 | import { createRender, signal } from '../signal.js'; 6 | const render = createRender(); 7 | 8 | // Counter Cmponent example 9 | function Outer(_, ...children) { 10 | return new ESXToken(_templateReference, 3, ESXToken._, [ESXToken.b(5, children)], "ul", "ul"); 11 | } 12 | render(new ESXToken(_templateReference2, 2, ESXToken._, [new ESXToken(null, 3, ESXToken._, [ESXToken.b(6, "A")], "li", "li"), new ESXToken(null, 3, ESXToken._, [ESXToken.b(5, 'B')], "li", "li")], "Outer", Outer), document.body); 13 | -------------------------------------------------------------------------------- /test/children.jsx: -------------------------------------------------------------------------------- 1 | // grab signals from various libaries, here the simplest I know 2 | import {createRender, signal} from '../signal.js'; 3 | 4 | const render = createRender(); 5 | 6 | // Counter Cmponent example 7 | function Outer(_, ...children) { 8 | return ( 9 |
    {children}
10 | ); 11 | } 12 | 13 | render( 14 | 15 |
  • A
  • 16 |
  • {'B'}
  • 17 |
    , 18 | document.body 19 | ); 20 | -------------------------------------------------------------------------------- /test/counter.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | udomsay 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /test/counter.js: -------------------------------------------------------------------------------- 1 | var _templateReference = {}, 2 | _templateReference2 = {}; 3 | globalThis.ESXToken || (globalThis.ESXToken = class ESXToken { static ATTRIBUTE = 1; static COMPONENT = 2; static ELEMENT = 3; static FRAGMENT = 4; static INTERPOLATION = 5; static STATIC = 6; static _ = Object.freeze([]); static a = (dynamic, name, value) => ({ type: 1, dynamic, name, value }); static b = (type, value) => ({ type, value }); constructor(id, type, attributes, children, name, value) { this.id = id; this.type = type; this.attributes = attributes; this.children = children; this.name = name; this.value = value; } get properties() { const { attributes } = this; if (attributes.length) { const properties = {}; for (const entry of attributes) { if (entry.type < 2) properties[entry.name] = entry.value;else Object.assign(properties, entry.value); } return properties; } return null; } }); 4 | // grab signals from various libaries, here the simplest I know 5 | import { Signal, signal, effect } from 'https://unpkg.com/@webreflection/signal'; 6 | 7 | // import the `createRender` utility 8 | import createRender from '../index.js'; 9 | const render = createRender({ 10 | Signal, 11 | effect 12 | }); 13 | 14 | // Counter Cmponent example 15 | function Counter({ 16 | clicks 17 | }) { 18 | const attrs = { 19 | a: 1 20 | }; 21 | return new ESXToken(_templateReference, 3, [ESXToken.b(5, attrs)], [new ESXToken(null, 3, [ESXToken.a(true, "onclick", () => { 22 | clicks.value--; 23 | })], [ESXToken.b(6, "-")], "button", "button"), new ESXToken(null, 3, ESXToken._, [ESXToken.b(5, clicks)], "span", "span"), new ESXToken(null, 3, [ESXToken.a(true, "onclick", () => { 24 | clicks.value++; 25 | })], [ESXToken.b(6, "+")], "button", "button")], "div", "div"); 26 | } 27 | render(new ESXToken(_templateReference2, 2, [ESXToken.a(true, "clicks", signal(0))], ESXToken._, "Counter", Counter), document.body); 28 | -------------------------------------------------------------------------------- /test/counter.jsx: -------------------------------------------------------------------------------- 1 | // grab signals from various libaries, here the simplest I know 2 | import {Signal, signal, effect} from 'https://unpkg.com/@webreflection/signal'; 3 | 4 | // import the `createRender` utility 5 | import createRender from 'https://unpkg.com/udomsay'; 6 | const render = createRender({Signal, effect}); 7 | 8 | // Counter Cmponent example 9 | function Counter({clicks}) { 10 | const attrs = {a: 1}; 11 | return ( 12 |
    13 | 14 | {clicks} 15 | 16 |
    17 | ); 18 | } 19 | 20 | render( 21 | , 22 | document.body 23 | ); 24 | -------------------------------------------------------------------------------- /test/dist/preact.html: -------------------------------------------------------------------------------- 1 | preact 2 | -------------------------------------------------------------------------------- /test/dist/preact.js: -------------------------------------------------------------------------------- 1 | var _templateReference = {}, 2 | _templateReference2 = {}; 3 | globalThis.ESXToken || (globalThis.ESXToken = class ESXToken { static ATTRIBUTE = 1; static COMPONENT = 2; static ELEMENT = 3; static FRAGMENT = 4; static INTERPOLATION = 5; static STATIC = 6; static _ = Object.freeze([]); static a = (dynamic, name, value) => ({ type: 1, dynamic, name, value }); static b = (type, value) => ({ type, value }); constructor(id, type, attributes, children, name, value) { this.id = id; this.type = type; this.attributes = attributes; this.children = children; this.name = name; this.value = value; } get properties() { const { attributes } = this; if (attributes.length) { const properties = {}; for (const entry of attributes) { if (entry.type < 2) properties[entry.name] = entry.value;else Object.assign(properties, entry.value); } return properties; } return null; } }); 4 | import { createRender, signal } from '../../preact.js'; 5 | const render = createRender(); 6 | render(new ESXToken(_templateReference, 2, [ESXToken.a(true, "clicks", signal(0))], ESXToken._, "Counter", Counter), document.body); 7 | function Counter({ 8 | clicks 9 | }) { 10 | return new ESXToken(_templateReference2, 3, ESXToken._, [new ESXToken(null, 3, [ESXToken.a(true, "onclick", () => { 11 | clicks.value--; 12 | })], [ESXToken.b(6, "-")], "button", "button"), new ESXToken(null, 3, ESXToken._, [ESXToken.b(5, clicks)], "span", "span"), new ESXToken(null, 3, [ESXToken.a(true, "onclick", () => { 13 | clicks.value++; 14 | })], [ESXToken.b(6, "+")], "button", "button")], "div", "div"); 15 | } 16 | -------------------------------------------------------------------------------- /test/dist/preact.jsx: -------------------------------------------------------------------------------- 1 | import {createRender, signal} from '../../preact.js'; 2 | const render = createRender(); 3 | 4 | render( 5 | , 6 | document.body 7 | ); 8 | 9 | function Counter({clicks}) { 10 | return ( 11 |
    12 | 13 | {clicks} 14 | 15 |
    16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /test/dist/signal.html: -------------------------------------------------------------------------------- 1 | signal 2 | -------------------------------------------------------------------------------- /test/dist/signal.js: -------------------------------------------------------------------------------- 1 | var _templateReference = {}, 2 | _templateReference2 = {}; 3 | globalThis.ESXToken || (globalThis.ESXToken = class ESXToken { static ATTRIBUTE = 1; static COMPONENT = 2; static ELEMENT = 3; static FRAGMENT = 4; static INTERPOLATION = 5; static STATIC = 6; static _ = Object.freeze([]); static a = (dynamic, name, value) => ({ type: 1, dynamic, name, value }); static b = (type, value) => ({ type, value }); constructor(id, type, attributes, children, name, value) { this.id = id; this.type = type; this.attributes = attributes; this.children = children; this.name = name; this.value = value; } get properties() { const { attributes } = this; if (attributes.length) { const properties = {}; for (const entry of attributes) { if (entry.type < 2) properties[entry.name] = entry.value;else Object.assign(properties, entry.value); } return properties; } return null; } }); 4 | import { createRender, signal } from '../../signal.js'; 5 | const render = createRender(); 6 | render(new ESXToken(_templateReference, 2, [ESXToken.a(true, "clicks", signal(0))], ESXToken._, "Counter", Counter), document.body); 7 | function Counter({ 8 | clicks 9 | }) { 10 | return new ESXToken(_templateReference2, 3, ESXToken._, [new ESXToken(null, 3, [ESXToken.a(true, "onclick", () => { 11 | clicks.value--; 12 | })], [ESXToken.b(6, "-")], "button", "button"), new ESXToken(null, 3, ESXToken._, [ESXToken.b(5, clicks)], "span", "span"), new ESXToken(null, 3, [ESXToken.a(true, "onclick", () => { 13 | clicks.value++; 14 | })], [ESXToken.b(6, "+")], "button", "button")], "div", "div"); 15 | } 16 | -------------------------------------------------------------------------------- /test/dist/signal.jsx: -------------------------------------------------------------------------------- 1 | import {createRender, signal} from '../../signal.js'; 2 | const render = createRender(); 3 | 4 | render( 5 | , 6 | document.body 7 | ); 8 | 9 | function Counter({clicks}) { 10 | return ( 11 |
    12 | 13 | {clicks} 14 | 15 |
    16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /test/dist/solid.html: -------------------------------------------------------------------------------- 1 | solid 2 | -------------------------------------------------------------------------------- /test/dist/solid.js: -------------------------------------------------------------------------------- 1 | var _templateReference = {}, 2 | _templateReference2 = {}; 3 | globalThis.ESXToken || (globalThis.ESXToken = class ESXToken { static ATTRIBUTE = 1; static COMPONENT = 2; static ELEMENT = 3; static FRAGMENT = 4; static INTERPOLATION = 5; static STATIC = 6; static _ = Object.freeze([]); static a = (dynamic, name, value) => ({ type: 1, dynamic, name, value }); static b = (type, value) => ({ type, value }); constructor(id, type, attributes, children, name, value) { this.id = id; this.type = type; this.attributes = attributes; this.children = children; this.name = name; this.value = value; } get properties() { const { attributes } = this; if (attributes.length) { const properties = {}; for (const entry of attributes) { if (entry.type < 2) properties[entry.name] = entry.value;else Object.assign(properties, entry.value); } return properties; } return null; } }); 4 | import { createRender, createSignal } from '../../solid.js'; 5 | const render = createRender(); 6 | render(new ESXToken(_templateReference, 2, [ESXToken.a(true, "clicks", createSignal(0))], ESXToken._, "Counter", Counter), document.body); 7 | function Counter({ 8 | clicks 9 | }) { 10 | const [signal, update] = clicks; 11 | const value = signal(); 12 | return new ESXToken(_templateReference2, 3, ESXToken._, [new ESXToken(null, 3, [ESXToken.a(true, "onclick", () => { 13 | update(value - 1); 14 | })], [ESXToken.b(6, "-")], "button", "button"), new ESXToken(null, 3, ESXToken._, [ESXToken.b(5, value)], "span", "span"), new ESXToken(null, 3, [ESXToken.a(true, "onclick", () => { 15 | update(value + 1); 16 | })], [ESXToken.b(6, "+")], "button", "button")], "div", "div"); 17 | } 18 | -------------------------------------------------------------------------------- /test/dist/solid.jsx: -------------------------------------------------------------------------------- 1 | import {createRender, createSignal} from '../../solid.js'; 2 | const render = createRender(); 3 | 4 | render( 5 | , 6 | document.body 7 | ); 8 | 9 | function Counter({clicks}) { 10 | const [signal, update] = clicks; 11 | const value = signal(); 12 | return ( 13 |
    14 | 15 | {value} 16 | 17 |
    18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /test/dist/usignal.html: -------------------------------------------------------------------------------- 1 | usignal 2 | -------------------------------------------------------------------------------- /test/dist/usignal.js: -------------------------------------------------------------------------------- 1 | var _templateReference = {}, 2 | _templateReference2 = {}; 3 | globalThis.ESXToken || (globalThis.ESXToken = class ESXToken { static ATTRIBUTE = 1; static COMPONENT = 2; static ELEMENT = 3; static FRAGMENT = 4; static INTERPOLATION = 5; static STATIC = 6; static _ = Object.freeze([]); static a = (dynamic, name, value) => ({ type: 1, dynamic, name, value }); static b = (type, value) => ({ type, value }); constructor(id, type, attributes, children, name, value) { this.id = id; this.type = type; this.attributes = attributes; this.children = children; this.name = name; this.value = value; } get properties() { const { attributes } = this; if (attributes.length) { const properties = {}; for (const entry of attributes) { if (entry.type < 2) properties[entry.name] = entry.value;else Object.assign(properties, entry.value); } return properties; } return null; } }); 4 | import { createRender, signal } from '../../usignal.js'; 5 | const render = createRender(); 6 | render(new ESXToken(_templateReference, 2, [ESXToken.a(true, "clicks", signal(0))], ESXToken._, "Counter", Counter), document.body); 7 | function Counter({ 8 | clicks 9 | }) { 10 | return new ESXToken(_templateReference2, 3, ESXToken._, [new ESXToken(null, 3, [ESXToken.a(true, "onclick", () => { 11 | clicks.value--; 12 | })], [ESXToken.b(6, "-")], "button", "button"), new ESXToken(null, 3, ESXToken._, [ESXToken.b(5, clicks)], "span", "span"), new ESXToken(null, 3, [ESXToken.a(true, "onclick", () => { 13 | clicks.value++; 14 | })], [ESXToken.b(6, "+")], "button", "button")], "div", "div"); 15 | } 16 | -------------------------------------------------------------------------------- /test/dist/usignal.jsx: -------------------------------------------------------------------------------- 1 | import {createRender, signal} from '../../usignal.js'; 2 | const render = createRender(); 3 | 4 | render( 5 | , 6 | document.body 7 | ); 8 | 9 | function Counter({clicks}) { 10 | return ( 11 |
    12 | 13 | {clicks} 14 | 15 |
    16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /test/esx-babel.js: -------------------------------------------------------------------------------- 1 | var _templateReference = {}, 2 | _templateReference2 = {}, 3 | _templateReference3 = {}, 4 | _templateReference4 = {}, 5 | _templateReference5 = {}, 6 | _templateReference6 = {}, 7 | _templateReference7 = {}; 8 | globalThis.ESXToken || (globalThis.ESXToken = class ESXToken { static ATTRIBUTE = 1; static COMPONENT = 2; static ELEMENT = 3; static FRAGMENT = 4; static INTERPOLATION = 5; static STATIC = 6; static _ = Object.freeze([]); static a = (dynamic, name, value) => ({ type: 1, dynamic, name, value }); static b = (type, value) => ({ type, value }); constructor(id, type, attributes, children, name, value) { this.id = id; this.type = type; this.attributes = attributes; this.children = children; this.name = name; this.value = value; } get properties() { const { attributes } = this; if (attributes.length) { const properties = {}; for (const entry of attributes) { if (entry.type < 2) properties[entry.name] = entry.value;else Object.assign(properties, entry.value); } return properties; } return null; } }); 9 | import { render, signal } from '../index.js'; 10 | const s = signal('test'); 11 | const c = signal(new ESXToken(_templateReference, 3, ESXToken._, [ESXToken.b(6, "deep")], "strong", "strong")); 12 | function P({ 13 | test 14 | }) { 15 | return new ESXToken(_templateReference2, 3, ESXToken._, [ESXToken.b(6, "\n Another "), ESXToken.b(5, c), ESXToken.b(6, " / "), ESXToken.b(5, test)], "p", "p"); 16 | } 17 | [1, 2, 3].map(num => new ESXToken(_templateReference3, 3, ESXToken._, [ESXToken.b(5, num)], "span", "span")); 18 | document.body.innerHTML = '
    '; 19 | const { 20 | firstElementChild, 21 | lastElementChild 22 | } = document.body; 23 | const main = test => new ESXToken(_templateReference4, 3, [ESXToken.a(true, "data-test", test)], [ESXToken.b(6, "\n Hello World "), ESXToken.b(5, test), ESXToken.b(6, " "), ESXToken.b(5, s), ESXToken.b(5, [1, 2, 3].map(num => new ESXToken(_templateReference5, 2, [ESXToken.a(true, "num", num)], ESXToken._, "Span", Span))), new ESXToken(null, 2, [ESXToken.a(true, "test", test)], ESXToken._, "P", P)], "div", "div"); 24 | render(main('test'), firstElementChild); 25 | render(main('test'), lastElementChild); 26 | setTimeout(() => { 27 | render(main('OK'), firstElementChild); 28 | setTimeout(() => { 29 | s.value = 'signal'; 30 | setTimeout(() => { 31 | c.value = new ESXToken(_templateReference6, 3, ESXToken._, [ESXToken.b(6, "deep")], "i", "i"); 32 | }, 1000); 33 | }, 1000); 34 | }, 2000); 35 | function Span({ 36 | num 37 | }) { 38 | return new ESXToken(_templateReference7, 3, ESXToken._, [ESXToken.b(5, num)], "span", "span"); 39 | } 40 | -------------------------------------------------------------------------------- /test/esx.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | udomsay 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /test/esx.js: -------------------------------------------------------------------------------- 1 | import {ESX, Token} from '../node_modules/@ungap/esx/esm/index.js'; 2 | 3 | import {render, signal} from '../index.js'; 4 | 5 | const esx = ESX({P}); 6 | const s = signal('test'); 7 | const c = signal(esx`deep`); 8 | 9 | function P({test}) { 10 | return esx` 11 |

    12 | Another ${c} / ${test} 13 |

    14 | `; 15 | } 16 | 17 | document.body.innerHTML = '
    '; 18 | const {firstElementChild, lastElementChild} = document.body; 19 | 20 | const main = (test) => esx` 21 |
    22 | Hello World ${test} ${s} 23 | ${[1, 2, 3].map(num => esx`${num}`)} 24 |

    25 |

    26 | `; 27 | 28 | render( 29 | main('test'), 30 | firstElementChild 31 | ); 32 | 33 | render( 34 | main('test'), 35 | lastElementChild 36 | ); 37 | 38 | setTimeout( 39 | () => { 40 | render( 41 | main('OK'), 42 | firstElementChild 43 | ); 44 | setTimeout(() => { 45 | s.value = 'signal'; 46 | setTimeout(() => { 47 | c.value = esx`deep`; 48 | }, 1000); 49 | }, 1000); 50 | }, 51 | 2000 52 | ); 53 | 54 | -------------------------------------------------------------------------------- /test/esx.jsx: -------------------------------------------------------------------------------- 1 | import {render, signal} from '../index.js'; 2 | 3 | const s = signal('test'); 4 | const c = signal(deep); 5 | 6 | function P({test}) { 7 | return ( 8 |

    9 | Another {c} / {test} 10 |

    11 | ); 12 | } 13 | 14 | [1, 2, 3].map(num => {num}); 15 | 16 | document.body.innerHTML = '
    '; 17 | const {firstElementChild, lastElementChild} = document.body; 18 | 19 | const main = (test) => ( 20 |
    21 | Hello World {test} {s} 22 | {[1, 2, 3].map(num => )} 23 |

    24 |

    25 | ); 26 | 27 | render( 28 | main('test'), 29 | firstElementChild 30 | ); 31 | 32 | render( 33 | main('test'), 34 | lastElementChild 35 | ); 36 | 37 | setTimeout( 38 | () => { 39 | render( 40 | main('OK'), 41 | firstElementChild 42 | ); 43 | setTimeout(() => { 44 | s.value = 'signal'; 45 | setTimeout(() => { 46 | c.value = deep; 47 | }, 1000); 48 | }, 1000); 49 | }, 50 | 2000 51 | ); 52 | 53 | function Span({num}) { 54 | return {num}; 55 | } -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | udomsay 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | var _templateReference = {}, 2 | _templateReference2 = {}, 3 | _templateReference3 = {}, 4 | _templateReference4 = {}, 5 | _templateReference5 = {}; 6 | globalThis.ESXToken || (globalThis.ESXToken = class ESXToken { static ATTRIBUTE = 1; static COMPONENT = 2; static ELEMENT = 3; static FRAGMENT = 4; static INTERPOLATION = 5; static STATIC = 6; static _ = Object.freeze([]); static a = (dynamic, name, value) => ({ type: 1, dynamic, name, value }); static b = (type, value) => ({ type, value }); constructor(id, type, attributes, children, name, value) { this.id = id; this.type = type; this.attributes = attributes; this.children = children; this.name = name; this.value = value; } get properties() { const { attributes } = this; if (attributes.length) { const properties = {}; for (const entry of attributes) { if (entry.type < 2) properties[entry.name] = entry.value;else Object.assign(properties, entry.value); } return properties; } return null; } }); 7 | // import './esm/document.js'; 8 | import createRender from '../index.js'; 9 | import { Signal, signal, effect } from 'https://unpkg.com/@webreflection/signal'; 10 | const A = signal('A'); 11 | const B = signal('B'); 12 | const C = signal('C'); 13 | const p = signal(new ESXToken(_templateReference, 3, [ESXToken.a(false, "is", "built-in-p")], [ESXToken.b(6, "first")], "p", "p")); 14 | customElements.define('built-in-p', class BuiltInP extends HTMLParagraphElement { 15 | connectedCallback() { 16 | console.log('connected', this); 17 | } 18 | }, { 19 | extends: 'p' 20 | }); 21 | const div = (A, B) => new ESXToken(_templateReference2, 3, [ESXToken.a(true, "test", A), ESXToken.a(false, "static", "value")], [ESXToken.b(5, B), new ESXToken(null, 2, [ESXToken.a(true, "test", A)], ESXToken._, "Component", Component)], "div", "div"); 22 | function Component({ 23 | test 24 | }) { 25 | return new ESXToken(_templateReference3, 3, ESXToken._, [new ESXToken(null, 3, [ESXToken.a(true, "test", test)], [ESXToken.b(6, "OK "), ESXToken.b(5, test)], "p", "p"), ESXToken.b(5, p)], "div", "div"); 26 | } 27 | function CompA({ 28 | name 29 | }) { 30 | return new ESXToken(_templateReference4, 3, ESXToken._, [ESXToken.b(6, "Comp "), ESXToken.b(5, name)], "strong", "strong"); 31 | } 32 | function CompB({ 33 | name 34 | }) { 35 | return new ESXToken(_templateReference5, 3, ESXToken._, [ESXToken.b(6, "Comp "), ESXToken.b(5, name)], "em", "em"); 36 | } 37 | const render = createRender({ 38 | Signal, 39 | effect 40 | }); 41 | 42 | // render( 43 | // 44 | // 45 | // , 46 | // document.body 47 | // ); 48 | 49 | // setInterval(() => { render(<>{Math.random() < .5 ? : }, document.body) }, 1000); 50 | 51 | // render(, document.body); 52 | // setTimeout(() => render(, document.body), 2000); 53 | // setTimeout(() => render(, document.body), 4000); 54 | // setTimeout(() => render(, document.body), 6000); 55 | 56 | // render(div(A, B), document.body.appendChild(document.createElement('div'))); 57 | // render(div(A, C), document.body.appendChild(document.createElement('div'))); 58 | // setTimeout(() => A.value = 'B', 2000); 59 | // setTimeout(() => B.value = 'C', 2000); 60 | // setTimeout(() => B.value = 'D', 4000); 61 | // setTimeout(() => p.value =

    last

    , 7000); 62 | // setTimeout(() => { 63 | // render(div(B, C), document.body.firstElementChild); 64 | // render(div(C, A), document.body.lastElementChild); 65 | // }, 10000); 66 | -------------------------------------------------------------------------------- /test/index.jsx: -------------------------------------------------------------------------------- 1 | // import './esm/document.js'; 2 | import createRender from '../index.js'; 3 | import {Signal, signal, effect} from 'https://unpkg.com/@webreflection/signal'; 4 | const render = createRender({Signal, effect}); 5 | 6 | const A = signal('A'); 7 | const B = signal('B'); 8 | const C = signal('C'); 9 | const p = signal(

    first

    ); 10 | 11 | customElements.define( 12 | 'built-in-p', 13 | class BuiltInP extends HTMLParagraphElement { 14 | connectedCallback() { 15 | console.log('connected', this); 16 | } 17 | }, 18 | {extends: 'p'} 19 | ); 20 | 21 | const div = (A, B) => ( 22 |
    23 | {B} 24 | 25 |
    26 | ); 27 | 28 | function Component({test}) { 29 | return ( 30 |
    31 |

    OK {test}

    32 | {p} 33 |
    34 | ); 35 | } 36 | 37 | function CompA({name}) { 38 | return Comp {name}; 39 | } 40 | 41 | function CompB({name}) { 42 | return Comp {name}; 43 | } 44 | 45 | // render( 46 | // 47 | // 48 | // , 49 | // document.body 50 | // ); 51 | 52 | // setInterval(() => { render(<>{Math.random() < .5 ? : }, document.body) }, 1000); 53 | 54 | // render(, document.body); 55 | // setTimeout(() => render(, document.body), 2000); 56 | // setTimeout(() => render(, document.body), 4000); 57 | // setTimeout(() => render(, document.body), 6000); 58 | 59 | // render(div(A, B), document.body.appendChild(document.createElement('div'))); 60 | // render(div(A, C), document.body.appendChild(document.createElement('div'))); 61 | // setTimeout(() => A.value = 'B', 2000); 62 | // setTimeout(() => B.value = 'C', 2000); 63 | // setTimeout(() => B.value = 'D', 4000); 64 | // setTimeout(() => p.value =

    last

    , 7000); 65 | // setTimeout(() => { 66 | // render(div(B, C), document.body.firstElementChild); 67 | // render(div(C, A), document.body.lastElementChild); 68 | // }, 10000); 69 | -------------------------------------------------------------------------------- /test/ssr.js: -------------------------------------------------------------------------------- 1 | var _templateReference = {}, 2 | _templateReference2 = {}, 3 | _templateReference3 = {}; 4 | globalThis.ESXToken || (globalThis.ESXToken = class ESXToken { static ATTRIBUTE = 1; static COMPONENT = 2; static ELEMENT = 3; static FRAGMENT = 4; static INTERPOLATION = 5; static STATIC = 6; static _ = Object.freeze([]); static a = (dynamic, name, value) => ({ type: 1, dynamic, name, value }); static b = (type, value) => ({ type, value }); constructor(id, type, attributes, children, name, value) { this.id = id; this.type = type; this.attributes = attributes; this.children = children; this.name = name; this.value = value; } get properties() { const { attributes } = this; if (attributes.length) { const properties = {}; for (const entry of attributes) { if (entry.type < 2) properties[entry.name] = entry.value;else Object.assign(properties, entry.value); } return properties; } return null; } }); 5 | import createRender from '../esm/ssr.js'; 6 | import { Signal, signal } from '@webreflection/signal'; 7 | const render = createRender({ 8 | Signal 9 | }); 10 | function Counter({ 11 | clicks 12 | }) { 13 | return new ESXToken(_templateReference, 3, ESXToken._, [new ESXToken(null, 3, [ESXToken.a(true, "onclick", () => { 14 | clicks.value--; 15 | })], [ESXToken.b(6, "-")], "button", "button"), new ESXToken(null, 3, ESXToken._, [ESXToken.b(5, clicks)], "span", "span"), new ESXToken(null, 3, [ESXToken.a(true, "onclick", () => { 16 | clicks.value++; 17 | })], [ESXToken.b(6, "+")], "button", "button")], "div", "div"); 18 | } 19 | const counters = new Array(2).fill(); 20 | function test() { 21 | console.time('render'); 22 | render(new ESXToken(_templateReference2, 3, [ESXToken.a(false, "lang", "en")], [new ESXToken(null, 3, ESXToken._, [ESXToken.b(6, "not &scaped")], "title", "title"), new ESXToken(null, 3, ESXToken._, [new ESXToken(null, 3, [ESXToken.a(false, "test", "attribute")], [ESXToken.b(5, counters.map((_, i) => new ESXToken(_templateReference3, 3, ESXToken._, [new ESXToken(null, 2, [ESXToken.a(true, "clicks", signal(i))], ESXToken._, "Counter", Counter)], "li", "li")))], "ul", "ul")], "body", "body")], "html", "html"), console.log.bind(console)); 23 | console.timeEnd('render'); 24 | } 25 | for (let i = 0; i < 5; i++) test(); 26 | -------------------------------------------------------------------------------- /test/ssr.jsx: -------------------------------------------------------------------------------- 1 | import createRender from '../esm/ssr.js'; 2 | import {Signal, signal} from '@webreflection/signal'; 3 | const render = createRender({Signal}); 4 | 5 | function Counter({clicks}) { 6 | return ( 7 |
    8 | 9 | {clicks} 10 | 11 |
    12 | ); 13 | } 14 | 15 | const counters = new Array(2).fill(); 16 | 17 | function test() { 18 | console.time('render'); 19 | render( 20 | ( 21 | 22 | not &scaped 23 | 24 |
      25 | {counters.map((_, i) =>
    • )} 26 |
    27 | 28 | 29 | ), 30 | console.log.bind(console) 31 | ); 32 | console.timeEnd('render'); 33 | } 34 | 35 | for (let i = 0; i < 5; i++) test(); 36 | -------------------------------------------------------------------------------- /test/uhooks.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ESX + uhooks 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /test/uhooks.js: -------------------------------------------------------------------------------- 1 | var _templateReference = {}, 2 | _templateReference2 = {}, 3 | _templateReference3 = {}, 4 | _templateReference4 = {}, 5 | _templateReference5 = {}; 6 | globalThis.ESXToken || (globalThis.ESXToken = class ESXToken { static ATTRIBUTE = 1; static COMPONENT = 2; static ELEMENT = 3; static FRAGMENT = 4; static INTERPOLATION = 5; static STATIC = 6; static _ = Object.freeze([]); static a = (dynamic, name, value) => ({ type: 1, dynamic, name, value }); static b = (type, value) => ({ type, value }); constructor(id, type, attributes, children, name, value) { this.id = id; this.type = type; this.attributes = attributes; this.children = children; this.name = name; this.value = value; } get properties() { const { attributes } = this; if (attributes.length) { const properties = {}; for (const entry of attributes) { if (entry.type < 2) properties[entry.name] = entry.value;else Object.assign(properties, entry.value); } return properties; } return null; } }); 7 | import { hooked, useState } from 'https://unpkg.com/uhooks?module'; 8 | const transform = token => { 9 | switch (token.type) { 10 | case ESXToken.COMPONENT: 11 | { 12 | let id, node; 13 | hooked(() => { 14 | console.time('update'); 15 | const result = token.value(); 16 | if (id !== result.id) { 17 | id = result.id; 18 | const replace = transform(result); 19 | if (!node) node = replace;else { 20 | node.replaceWith(replace); 21 | node = replace; 22 | } 23 | } 24 | node.update(result); 25 | console.timeEnd('update'); 26 | })(); 27 | return node; 28 | } 29 | case ESXToken.ELEMENT: 30 | { 31 | const node = document.createElement(token.name); 32 | node.update = ({ 33 | attributes, 34 | children 35 | }) => { 36 | node.textContent = children[0].value; 37 | if (attributes.length) node.onclick = attributes[0].value; 38 | }; 39 | return node; 40 | } 41 | } 42 | }; 43 | const same = () => transform(new ESXToken(_templateReference, 2, ESXToken._, ESXToken._, "Counter", Counter)); 44 | console.time('render'); 45 | document.body.append(transform(new ESXToken(_templateReference2, 2, ESXToken._, ESXToken._, "Counter", Counter)), same(), same(), same(), transform(new ESXToken(_templateReference3, 2, ESXToken._, ESXToken._, "Counter", Counter))); 46 | console.timeEnd('render'); 47 | if (location.search === '?auto') { 48 | requestAnimationFrame(function click() { 49 | const button = document.querySelector('button'); 50 | if (button) { 51 | button.click(); 52 | requestAnimationFrame(click); 53 | } 54 | }); 55 | } 56 | function Counter() { 57 | const [count, update] = useState(0); 58 | return count < 10 ? new ESXToken(_templateReference4, 3, [ESXToken.a(true, "onClick", () => update(count + 1))], [ESXToken.b(5, count)], "button", "button") : new ESXToken(_templateReference5, 3, ESXToken._, [ESXToken.b(6, "You reached 10 \uD83E\uDD73")], "div", "div"); 59 | } 60 | -------------------------------------------------------------------------------- /test/uhooks.jsx: -------------------------------------------------------------------------------- 1 | import {hooked, useState} from 'https://unpkg.com/uhooks?module'; 2 | 3 | const transform = token => { 4 | switch (token.type) { 5 | case ESXToken.COMPONENT: { 6 | let id, node; 7 | hooked(() => { 8 | const result = token.value(); 9 | if (id !== result.id) { 10 | id = result.id; 11 | const replace = transform(result); 12 | if (!node) 13 | node = replace; 14 | else { 15 | node.replaceWith(replace); 16 | node = replace; 17 | } 18 | } 19 | node.update(result); 20 | })(); 21 | return node; 22 | } 23 | case ESXToken.ELEMENT: { 24 | const node = document.createElement(token.name); 25 | node.update = ({attributes, children}) => { 26 | node.textContent = children[0].value; 27 | if (attributes.length) 28 | node.onclick = attributes[0].value; 29 | }; 30 | return node; 31 | } 32 | } 33 | }; 34 | 35 | const same = () => transform(); 36 | 37 | console.time('render'); 38 | document.body.append( 39 | transform(), 40 | same(), 41 | same(), 42 | same(), 43 | transform() 44 | ); 45 | console.timeEnd('render'); 46 | 47 | if (location.search === '?auto') { 48 | requestAnimationFrame(function click() { 49 | const button = document.querySelector('button'); 50 | if (button) { 51 | button.click(); 52 | requestAnimationFrame(click); 53 | } 54 | }); 55 | } 56 | 57 | function Counter() { 58 | const [count, update] = useState(0); 59 | return count < 10 ? 60 | : 61 |
    You reached 10 🥳
    62 | ; 63 | } 64 | -------------------------------------------------------------------------------- /usignal.js: -------------------------------------------------------------------------------- 1 | /*! (c) Andrea Giammarchi */ 2 | const{is:t}=Object;let e;const s=t=>{const s=e;e=s||[];try{if(t(),!s)for(const{value:t}of e);}finally{e=s}};class n{constructor(t){this._=t}then(t){t(this.value)}toJSON(){return this.value}toString(){return this.value}valueOf(){return this.value}}let i;class o extends n{constructor(t,e,s,n){super(t),this.f=n,this.$=!0,this.r=new Set,this.s=new g(e,s)}get value(){if(this.$){const t=i;i=this;try{this.s.value=this._(this.s._)}finally{this.$=!1,i=t}}return this.s.value}}const c={async:!1,equals:!0},r=(t,e,s=c)=>new o(t,e,s,!1);let a;const l=[],u=()=>{},h=({s:t})=>{"function"==typeof t._&&(t._=t._())};class f extends o{constructor(t,e,s){super(t,e,s,!0),this.e=l}run(){return this.$=!0,this.value,this}stop(){this._=u,this.r.clear(),this.s.c.clear()}}class p extends f{constructor(t,e,s){super(t,e,s),this.i=0,this.a=!!s.async,this.m=!0,this.e=[]}get value(){this.a?this.async():this.sync()}async(){this.m&&(this.m=!1,queueMicrotask((()=>{this.m=!0,this.sync()})))}sync(){const t=a;(a=this).i=0,h(this),super.value,a=t}stop(){super.stop(),h(this);for(const t of this.e.splice(0))t.stop()}}const d=()=>!1;class g extends n{constructor(e,{equals:s}){super(e),this.c=new Set,this.s=!0===s?t:s||d}peek(){return this._}get value(){return i&&(this.c.add(i),i.r.add(this)),this._}set value(t){const s=this._;if(!this.s(this._=t,s)&&this.c.size){const t=[],s=[this];for(const e of s)for(const n of e.c)if(!n.$&&n.r.has(e))if(n.r.clear(),n.$=!0,n.f){t.push(n);const e=[n];for(const t of e)for(const s of t.e)s.r.clear(),s.$=!0,e.push(s)}else s.push(n.s);for(const s of t)e?e.push(s):s.value}}}const v=(t,e=c)=>new g(t,e),y={async:!1},b=(t,e)=>((t,e,s=c)=>{let n;if(a){const{i:i,e:o}=a,c=i===o.length;(c||o[i]._!==t)&&(c||o[i].stop(),o[i]=new p(t,e,s).run()),n=o[i],a.i++}else n=new p(t,e,s).run();return()=>{n.stop()}})(t,e,y);var w=(0,Object.freeze)([]),m=()=>{};const N=(t,e,s)=>{const{parentNode:n}=s,i=e.length;let o=t.length,c=i,r=0,a=0,l=null;for(;rs-a){const i=t[r];for(;at[e],x=({children:t},e)=>t[e],I=({value:t,properties:e,children:s})=>t(e,...s),P=({name:t})=>"key"===t,B=(t,{content:e})=>t.reduce(M,e),R=(t,e)=>t.reduce(x,e),L=(t,e)=>{const s=null==e?"":String(e);s!==t.data&&(t.data=s)}; 3 | /*! (c) Andrea Giammarchi - ISC */var j=(t={})=>{const e=t.document||globalThis.document,s=new Map(t.plugins||[]),n=!!s.size,i=t.diff||N,o=t.effect||(t=>(t(),m)),c=t.getPeek||(t=>t.peek()),r=t.getValue||(t=>t.value),a=t.isSignal||(t.Signal?O.bind(t.Signal.prototype):()=>!1),l=t=>e.createTextNode(t),u=(t,e)=>{let s=v.get(t);return s||v.set(t,s={}),s[e]||(s[e]={})},h=(t,e)=>{t.dispose();const s=o((()=>{const s=I(e);s.id!==t.id?t=f(t,s,!1):t.update(s)}));return t.dispose=s,t},f=(t,e,s)=>e.type===k?h(t,e):((t,e,s)=>(t.dispose(),t=new p(e),s?t.dispose=o((()=>t.update(e))):t.update(e),t))(t,e,s);class p{constructor(t,e=!0){const[s,n]=e?M(t):[w,null];this._=e&&t.type===$,this.id=t.id,this.updates=s,this.content=n,this.dispose=m}get $(){const{content:t,_:e}=this;return e?(this._=!e,(this.content=(({childNodes:t})=>({childNodes:[...t]}))(t)).childNodes):[t]}update(t){for(const e of this.updates)e.call(this,t)}}const d=new p({id:null},!1),g={id:null,view:d},v=new WeakMap;let y;const b=new WeakMap,M=t=>{let e=b.get(t.id);if(!e){const s=[],n=V(t,s,[],w,!1);b.set(t.id,e=[s,n])}const[s,n]=e;return[s.slice(),n.cloneNode(!0)]},x=(t,e,s,n)=>{n?t.setAttribute(e,s):t[e]=s},j=(t,e,s,n,i)=>{if(a(s)){const c="🙊"+e;c in n&&n[c](),n[c]=o((()=>{x(t,e,r(s),i)}))}else x(t,e,s,i)},W=(t,e,i,o)=>{if(n&&s.has(e))s.get(e)(t,i,o);else if(o[e]!==i)switch(o[e]=i,e){case"class":e+="Name";case"className":case"textContent":j(t,e,i,o,!1);break;case"ref":i.current=t;break;default:e.startsWith("on")?t[e.toLowerCase()]=i:e in t?j(t,e,i,o,!1):null==i?t.removeAttribute(e):j(t,e,i,o,!0)}},q=(t,e,s)=>function(n){const i={},o=B(e,this);(this.updates[s]=s=>{const{attributes:n}=R(e,s);for(const e of t){const t=n[e],{type:s,value:c}=t;if(s<2)W(o,t.name,c,i);else for(const[t,e]of S(c))W(o,t,e,i)}})(n)},z=(t,s)=>function(n){let o=w,c=!0,r=-1;const a=B(t,this),l=new Map;(this.updates[s]=s=>{const{value:n}=R(t,s),h=[];for(let t=0;tfunction(s){let n=w,o=d;const c=B(t,this);(this.updates[e]=e=>{o=h(o,R(t,e)),n=i(n,o.$,c)})(s)},G=(t,e)=>function(s){const n=B(t,this);(this.updates[e]=e=>{L(n,R(t,e).value)})(s)},D=(t,e)=>function(s){let n,a,u=m;const h=B(t,this),{value:p}=R(t,s),g=t=>{n!==t&&(u(),n=t,u=o(a))};if(y(c(p))){let t=w,e=d;a=()=>{const s=r(n);e=f(e,s,!1),t=i(t,e.$,h)}}else{const t=l("");h.replaceWith(t),a=()=>{L(t,r(n))}}this.updates[e]=e=>g(R(t,e).value),g(p)},J=(t,e)=>function(s){let n=w,o=d,c=null;const r=B(t,this);(this.updates[e]=e=>{e=R(t,e).value,c!==e.id?(c=e.id,o=f(o,e,!1),n=i(n,o.$,r)):e.type===k?o.update(I(e)):o.update(e)})(s)},U=({children:t},e,s,n,i)=>{for(let o=0;o{let c,r;const{length:u}=s;t:switch(t.type){case A:{const{value:e}=t;switch(!0){case y(e):c=J;break;case T(e):c=z;break;case a(e):c=D;break;default:r=l(""),s.push(G(i,u));break t}}case k:r=e.createComment("🙊"),s.push((c||F)(i,u));break;case _:{const{attributes:c,name:a}=t,l=[a],h=[];for(let t=0;t{const s="function"==typeof t?t():t;y||(y=O.bind(E(s)));const n=f(v.get(e)||d,s,!0);v.set(e,n),e.replaceChildren(...n.$)}};const W=(t={})=>j({...t,Signal:n,effect:b,isSignal:void 0});export{p as Effect,f as FX,n as Signal,s as batch,r as computed,W as createRender,b as effect,v as signal}; 4 | --------------------------------------------------------------------------------