UNPKG

uhtml

Version:
878 lines (746 loc) 34.3 kB
self.uhtml = (function (exports) { 'use strict'; var umap = (function (_) { return { // About: get: _.get.bind(_) // It looks like WebKit/Safari didn't optimize bind at all, // so that using bind slows it down by 60%. // Firefox and Chrome are just fine in both cases, // so let's use the approach that works fast everywhere 👍 get: function get(key) { return _.get(key); }, set: function set(key, value) { return _.set(key, value), value; } }; }); var attr = /([^\s\\>"'=]+)\s*=\s*(['"]?)$/; var empty = /^(?:area|base|br|col|embed|hr|img|input|keygen|link|menuitem|meta|param|source|track|wbr)$/i; var node = /<[a-z][^>]+$/i; var notNode = />[^<>]*$/; var selfClosing = /<([a-z]+[a-z0-9:._-]*)([^>]*?)(\/>)/ig; var trimEnd = /\s+$/; var isNode = function isNode(template, i) { return 0 < i-- && (node.test(template[i]) || !notNode.test(template[i]) && isNode(template, i)); }; var regular = function regular(original, name, extra) { return empty.test(name) ? original : "<".concat(name).concat(extra.replace(trimEnd, ''), "></").concat(name, ">"); }; var instrument = (function (template, prefix, svg) { var text = []; var length = template.length; var _loop = function _loop(i) { var chunk = template[i - 1]; text.push(attr.test(chunk) && isNode(template, i) ? chunk.replace(attr, function (_, $1, $2) { return "".concat(prefix).concat(i - 1, "=").concat($2 || '"').concat($1).concat($2 ? '' : '"'); }) : "".concat(chunk, "<!--").concat(prefix).concat(i - 1, "-->")); }; for (var i = 1; i < length; i++) { _loop(i); } text.push(template[length - 1]); var output = text.join('').trim(); return svg ? output : output.replace(selfClosing, regular); }); var isArray = Array.isArray; var _ref = [], indexOf = _ref.indexOf, slice = _ref.slice; var ELEMENT_NODE = 1; var nodeType = 111; var remove = function remove(_ref) { var firstChild = _ref.firstChild, lastChild = _ref.lastChild; var range = document.createRange(); range.setStartAfter(firstChild); range.setEndAfter(lastChild); range.deleteContents(); return firstChild; }; var diffable = function diffable(node, operation) { return node.nodeType === nodeType ? 1 / operation < 0 ? operation ? remove(node) : node.lastChild : operation ? node.valueOf() : node.firstChild : node; }; var persistent = function persistent(fragment) { var childNodes = fragment.childNodes; var length = childNodes.length; if (length < 2) return length ? childNodes[0] : fragment; var nodes = slice.call(childNodes, 0); var firstChild = nodes[0]; var lastChild = nodes[length - 1]; return { ELEMENT_NODE: ELEMENT_NODE, nodeType: nodeType, firstChild: firstChild, lastChild: lastChild, valueOf: function valueOf() { if (childNodes.length !== length) { var i = 0; while (i < length) { fragment.appendChild(nodes[i++]); } } return fragment; } }; }; /** * ISC License * * Copyright (c) 2020, Andrea Giammarchi, @WebReflection * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE * OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR * PERFORMANCE OF THIS SOFTWARE. */ /** * @param {Node} parentNode The container where children live * @param {Node[]} a The list of current/live children * @param {Node[]} b The list of future children * @param {(entry: Node, action: number) => Node} get * The callback invoked per each entry related DOM operation. * @param {Node} [before] The optional node used as anchor to insert before. * @returns {Node[]} The same list of future children. */ var udomdiff = (function (parentNode, a, b, get, before) { var bLength = b.length; var aEnd = a.length; var bEnd = bLength; var aStart = 0; var bStart = 0; var map = null; while (aStart < aEnd || bStart < bEnd) { // append head, tail, or nodes in between: fast path if (aEnd === aStart) { // we could be in a situation where the rest of nodes that // need to be added are not at the end, and in such case // the node to `insertBefore`, if the index is more than 0 // must be retrieved, otherwise it's gonna be the first item. var node = bEnd < bLength ? bStart ? get(b[bStart - 1], -0).nextSibling : get(b[bEnd - bStart], 0) : before; while (bStart < bEnd) { parentNode.insertBefore(get(b[bStart++], 1), node); } } // remove head or tail: fast path else if (bEnd === bStart) { while (aStart < aEnd) { // remove the node only if it's unknown or not live if (!map || !map.has(a[aStart])) parentNode.removeChild(get(a[aStart], -1)); aStart++; } } // same node: fast path else if (a[aStart] === b[bStart]) { aStart++; bStart++; } // same tail: fast path else if (a[aEnd - 1] === b[bEnd - 1]) { aEnd--; bEnd--; } // The once here single last swap "fast path" has been removed in v1.1.0 // https://github.com/WebReflection/udomdiff/blob/single-final-swap/esm/index.js#L69-L85 // reverse swap: also fast path else if (a[aStart] === b[bEnd - 1] && b[bStart] === a[aEnd - 1]) { // this is a "shrink" operation that could happen in these cases: // [1, 2, 3, 4, 5] // [1, 4, 3, 2, 5] // or asymmetric too // [1, 2, 3, 4, 5] // [1, 2, 3, 5, 6, 4] var _node = get(a[--aEnd], -1).nextSibling; parentNode.insertBefore(get(b[bStart++], 1), get(a[aStart++], -1).nextSibling); parentNode.insertBefore(get(b[--bEnd], 1), _node); // mark the future index as identical (yeah, it's dirty, but cheap 👍) // The main reason to do this, is that when a[aEnd] will be reached, // the loop will likely be on the fast path, as identical to b[bEnd]. // In the best case scenario, the next loop will skip the tail, // but in the worst one, this node will be considered as already // processed, bailing out pretty quickly from the map index check a[aEnd] = b[bEnd]; } // map based fallback, "slow" path else { // the map requires an O(bEnd - bStart) operation once // to store all future nodes indexes for later purposes. // In the worst case scenario, this is a full O(N) cost, // and such scenario happens at least when all nodes are different, // but also if both first and last items of the lists are different if (!map) { map = new Map(); var i = bStart; while (i < bEnd) { map.set(b[i], i++); } } // if it's a future node, hence it needs some handling if (map.has(a[aStart])) { // grab the index of such node, 'cause it might have been processed var index = map.get(a[aStart]); // if it's not already processed, look on demand for the next LCS if (bStart < index && index < bEnd) { var _i = aStart; // counts the amount of nodes that are the same in the future var sequence = 1; while (++_i < aEnd && _i < bEnd && map.get(a[_i]) === index + sequence) { sequence++; } // effort decision here: if the sequence is longer than replaces // needed to reach such sequence, which would brings again this loop // to the fast path, prepend the difference before a sequence, // and move only the future list index forward, so that aStart // and bStart will be aligned again, hence on the fast path. // An example considering aStart and bStart are both 0: // a: [1, 2, 3, 4] // b: [7, 1, 2, 3, 6] // this would place 7 before 1 and, from that time on, 1, 2, and 3 // will be processed at zero cost if (sequence > index - bStart) { var _node2 = get(a[aStart], 0); while (bStart < index) { parentNode.insertBefore(get(b[bStart++], 1), _node2); } } // if the effort wasn't good enough, fallback to a replace, // moving both source and target indexes forward, hoping that some // similar node will be found later on, to go back to the fast path else { parentNode.replaceChild(get(b[bStart++], 1), get(a[aStart++], -1)); } } // otherwise move the source forward, 'cause there's nothing to do else aStart++; } // this node has no meaning in the future list, so it's more than safe // to remove it, and check the next live node out instead, meaning // that only the live list index should be forwarded else parentNode.removeChild(get(a[aStart++], -1)); } } return b; }); var aria = function aria(node) { return function (values) { for (var key in values) { var name = key === 'role' ? key : "aria-".concat(key); var value = values[key]; if (value == null) node.removeAttribute(name);else node.setAttribute(name, value); } }; }; var attribute = function attribute(node, name) { var oldValue, orphan = true; var attributeNode = document.createAttributeNS(null, name); return function (newValue) { if (oldValue !== newValue) { oldValue = newValue; if (oldValue == null) { if (!orphan) { node.removeAttributeNode(attributeNode); orphan = true; } } else { attributeNode.value = newValue; if (orphan) { node.setAttributeNodeNS(attributeNode); orphan = false; } } } }; }; var _boolean = function _boolean(node, key, oldValue) { return function (newValue) { if (oldValue !== !!newValue) { // when IE won't be around anymore ... // node.toggleAttribute(key, oldValue = !!newValue); if (oldValue = !!newValue) node.setAttribute(key, '');else node.removeAttribute(key); } }; }; var data = function data(_ref) { var dataset = _ref.dataset; return function (values) { for (var key in values) { var value = values[key]; if (value == null) delete dataset[key];else dataset[key] = value; } }; }; var event = function event(node, name) { var oldValue, type = name.slice(2); if (!(name in node) && name.toLowerCase() in node) type = type.toLowerCase(); return function (newValue) { var info = isArray(newValue) ? newValue : [newValue, false]; if (oldValue !== info[0]) { if (oldValue) node.removeEventListener(type, oldValue, info[1]); if (oldValue = info[0]) node.addEventListener(type, oldValue, info[1]); } }; }; var ref = function ref(node) { var oldValue; return function (value) { if (oldValue !== value) { oldValue = value; if (typeof value === 'function') value(node);else value.current = node; } }; }; var setter = function setter(node, key) { return key === 'dataset' ? data(node) : function (value) { node[key] = value; }; }; var text = function text(node) { var oldValue; return function (newValue) { if (oldValue != newValue) { oldValue = newValue; node.textContent = newValue == null ? '' : newValue; } }; }; /*! (c) Andrea Giammarchi - ISC */ var createContent = function (document) { var FRAGMENT = 'fragment'; var TEMPLATE = 'template'; var HAS_CONTENT = ('content' in create(TEMPLATE)); var createHTML = HAS_CONTENT ? function (html) { var template = create(TEMPLATE); template.innerHTML = html; return template.content; } : function (html) { var content = create(FRAGMENT); var template = create(TEMPLATE); var childNodes = null; if (/^[^\S]*?<(col(?:group)?|t(?:head|body|foot|r|d|h))/i.test(html)) { var selector = RegExp.$1; template.innerHTML = '<table>' + html + '</table>'; childNodes = template.querySelectorAll(selector); } else { template.innerHTML = html; childNodes = template.childNodes; } append(content, childNodes); return content; }; return function createContent(markup, type) { return (type === 'svg' ? createSVG : createHTML)(markup); }; function append(root, childNodes) { var length = childNodes.length; while (length--) { root.appendChild(childNodes[0]); } } function create(element) { return element === FRAGMENT ? document.createDocumentFragment() : document.createElementNS('http://www.w3.org/1999/xhtml', element); } // it could use createElementNS when hasNode is there // but this fallback is equally fast and easier to maintain // it is also battle tested already in all IE function createSVG(svg) { var content = create(FRAGMENT); var template = create('div'); template.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg">' + svg + '</svg>'; append(content, template.firstChild.childNodes); return content; } }(document); var reducePath = function reducePath(_ref, i) { var childNodes = _ref.childNodes; return childNodes[i]; }; // from a fragment container, create an array of indexes // related to its child nodes, so that it's possible // to retrieve later on exact node via reducePath var createPath = function createPath(node) { var path = []; var _node = node, parentNode = _node.parentNode; while (parentNode) { path.push(indexOf.call(parentNode.childNodes, node)); node = parentNode; parentNode = node.parentNode; } return path; }; var _document = document, createTreeWalker = _document.createTreeWalker, importNode = _document.importNode; var isImportNodeLengthWrong = importNode.length != 1; // IE11 and old Edge discard empty nodes when cloning, potentially // resulting in broken paths to find updates. The workaround here // is to import once, upfront, the fragment that will be cloned // later on, so that paths are retrieved from one already parsed, // hence without missing child nodes once re-cloned. var createFragment = isImportNodeLengthWrong ? function (text, type, normalize) { return importNode.call(document, createContent(text, type, normalize), true); } : createContent; // IE11 and old Edge have a different createTreeWalker signature that // has been deprecated in other browsers. This export is needed only // to guarantee the TreeWalker doesn't show warnings and, ultimately, works var createWalker = isImportNodeLengthWrong ? function (fragment) { return createTreeWalker.call(document, fragment, 1 | 128, null, false); } : function (fragment) { return createTreeWalker.call(document, fragment, 1 | 128); }; var diff = function diff(comment, oldNodes, newNodes) { return udomdiff(comment.parentNode, // TODO: there is a possible edge case where a node has been // removed manually, or it was a keyed one, attached // to a shared reference between renders. // In this case udomdiff might fail at removing such node // as its parent won't be the expected one. // The best way to avoid this issue is to filter oldNodes // in search of those not live, or not in the current parent // anymore, but this would require both a change to uwire, // exposing a parentNode from the firstChild, as example, // but also a filter per each diff that should exclude nodes // that are not in there, penalizing performance quite a lot. // As this has been also a potential issue with domdiff, // and both lighterhtml and hyperHTML might fail with this // very specific edge case, I might as well document this possible // "diffing shenanigan" and call it a day. oldNodes, newNodes, diffable, comment); }; // if an interpolation represents a comment, the whole // diffing will be related to such comment. // This helper is in charge of understanding how the new // content for such interpolation/hole should be updated var handleAnything = function handleAnything(comment) { var oldValue, text, nodes = []; var anyContent = function anyContent(newValue) { switch (typeof(newValue)) { // primitives are handled as text content case 'string': case 'number': case 'boolean': if (oldValue !== newValue) { oldValue = newValue; if (!text) text = document.createTextNode(''); text.data = newValue; nodes = diff(comment, nodes, [text]); } break; // null, and undefined are used to cleanup previous content case 'object': case 'undefined': if (newValue == null) { if (oldValue != newValue) { oldValue = newValue; nodes = diff(comment, nodes, []); } break; } // arrays and nodes have a special treatment if (isArray(newValue)) { oldValue = newValue; // arrays can be used to cleanup, if empty if (newValue.length === 0) nodes = diff(comment, nodes, []); // or diffed, if these contains nodes or "wires" else if (typeof(newValue[0]) === 'object') nodes = diff(comment, nodes, newValue); // in all other cases the content is stringified as is else anyContent(String(newValue)); break; } // if the new value is a DOM node, or a wire, and it's // different from the one already live, then it's diffed. // if the node is a fragment, it's appended once via its childNodes // There is no `else` here, meaning if the content // is not expected one, nothing happens, as easy as that. if (oldValue !== newValue && 'ELEMENT_NODE' in newValue) { oldValue = newValue; nodes = diff(comment, nodes, newValue.nodeType === 11 ? slice.call(newValue.childNodes) : [newValue]); } break; case 'function': anyContent(newValue(comment)); break; } }; return anyContent; }; // attributes can be: // * ref=${...} for hooks and other purposes // * aria=${...} for aria attributes // * ?boolean=${...} for boolean attributes // * .dataset=${...} for dataset related attributes // * .setter=${...} for Custom Elements setters or nodes with setters // such as buttons, details, options, select, etc // * onevent=${...} to automatically handle event listeners // * generic=${...} to handle an attribute just like an attribute var handleAttribute = function handleAttribute(node, name /*, svg*/ ) { switch (name[0]) { case '?': return _boolean(node, name.slice(1), false); case '.': return setter(node, name.slice(1)); case 'o': if (name[1] === 'n') return event(node, name); } switch (name) { case 'ref': return ref(node); case 'aria': return aria(node); } return attribute(node, name /*, svg*/ ); }; // each mapped update carries the update type and its path // the type is either node, attribute, or text, while // the path is how to retrieve the related node to update. // In the attribute case, the attribute name is also carried along. function handlers(options) { var type = options.type, path = options.path; var node = path.reduceRight(reducePath, this); return type === 'node' ? handleAnything(node) : type === 'attr' ? handleAttribute(node, options.name /*, options.svg*/ ) : text(node); } // that contain the related unique id. In the attribute cases // isµX="attribute-name" will be used to map current X update to that // attribute name, while comments will be like <!--isµX-->, to map // the update to that specific comment node, hence its parent. // style and textarea will have <!--isµX--> text content, and are handled // directly through text-only updates. var prefix = 'isµ'; // Template Literals are unique per scope and static, meaning a template // should be parsed once, and once only, as it will always represent the same // content, within the exact same amount of updates each time. // This cache relates each template to its unique content and updates. var cache$1 = umap(new WeakMap()); // a RegExp that helps checking nodes that cannot contain comments var textOnly = /^(?:plaintext|script|style|textarea|title|xmp)$/i; var createCache = function createCache() { return { stack: [], // each template gets a stack for each interpolation "hole" entry: null, // each entry contains details, such as: // * the template that is representing // * the type of node it represents (html or svg) // * the content fragment with all nodes // * the list of updates per each node (template holes) // * the "wired" node or fragment that will get updates // if the template or type are different from the previous one // the entry gets re-created each time wire: null // each rendered node represent some wired content and // this reference to the latest one. If different, the node // will be cleaned up and the new "wire" will be appended }; }; // the entry stored in the rendered node cache, and per each "hole" var createEntry = function createEntry(type, template) { var _mapUpdates = mapUpdates(type, template), content = _mapUpdates.content, updates = _mapUpdates.updates; return { type: type, template: template, content: content, updates: updates, wire: null }; }; // a template is instrumented to be able to retrieve where updates are needed. // Each unique template becomes a fragment, cloned once per each other // operation based on the same template, i.e. data => html`<p>${data}</p>` var mapTemplate = function mapTemplate(type, template) { var text = instrument(template, prefix, type === 'svg'); var content = createFragment(text, type); // once instrumented and reproduced as fragment, it's crawled // to find out where each update is in the fragment tree var tw = createWalker(content); var nodes = []; var length = template.length - 1; var i = 0; // updates are searched via unique names, linearly increased across the tree // <div isµ0="attr" isµ1="other"><!--isµ2--><style><!--isµ3--</style></div> var search = "".concat(prefix).concat(i); while (i < length) { var node = tw.nextNode(); // if not all updates are bound but there's nothing else to crawl // it means that there is something wrong with the template. if (!node) throw "bad template: ".concat(text); // if the current node is a comment, and it contains isµX // it means the update should take care of any content if (node.nodeType === 8) { // The only comments to be considered are those // which content is exactly the same as the searched one. if (node.data === search) { nodes.push({ type: 'node', path: createPath(node) }); search = "".concat(prefix).concat(++i); } } else { // if the node is not a comment, loop through all its attributes // named isµX and relate attribute updates to this node and the // attribute name, retrieved through node.getAttribute("isµX") // the isµX attribute will be removed as irrelevant for the layout // let svg = -1; while (node.hasAttribute(search)) { nodes.push({ type: 'attr', path: createPath(node), name: node.getAttribute(search) //svg: svg < 0 ? (svg = ('ownerSVGElement' in node ? 1 : 0)) : svg }); node.removeAttribute(search); search = "".concat(prefix).concat(++i); } // if the node was a style, textarea, or others, check its content // and if it is <!--isµX--> then update tex-only this node if (textOnly.test(node.tagName) && node.textContent.trim() === "<!--".concat(search, "-->")) { node.textContent = ''; nodes.push({ type: 'text', path: createPath(node) }); search = "".concat(prefix).concat(++i); } } } // once all nodes to update, or their attributes, are known, the content // will be cloned in the future to represent the template, and all updates // related to such content retrieved right away without needing to re-crawl // the exact same template, and its content, more than once. return { content: content, nodes: nodes }; }; // if a template is unknown, perform the previous mapping, otherwise grab // its details such as the fragment with all nodes, and updates info. var mapUpdates = function mapUpdates(type, template) { var _ref = cache$1.get(template) || cache$1.set(template, mapTemplate(type, template)), content = _ref.content, nodes = _ref.nodes; // clone deeply the fragment var fragment = importNode.call(document, content, true); // and relate an update handler per each node that needs one var updates = nodes.map(handlers, fragment); // return the fragment and all updates to use within its nodes return { content: fragment, updates: updates }; }; // as html and svg can be nested calls, but no parent node is known // until rendered somewhere, the unroll operation is needed to // discover what to do with each interpolation, which will result // into an update operation. var unroll = function unroll(info, _ref2) { var type = _ref2.type, template = _ref2.template, values = _ref2.values; var length = values.length; // interpolations can contain holes and arrays, so these need // to be recursively discovered unrollValues(info, values, length); var entry = info.entry; // if the cache entry is either null or different from the template // and the type this unroll should resolve, create a new entry // assigning a new content fragment and the list of updates. if (!entry || entry.template !== template || entry.type !== type) info.entry = entry = createEntry(type, template); var _entry = entry, content = _entry.content, updates = _entry.updates, wire = _entry.wire; // even if the fragment and its nodes is not live yet, // it is already possible to update via interpolations values. for (var i = 0; i < length; i++) { updates[i](values[i]); } // if the entry was new, or representing a different template or type, // create a new persistent entity to use during diffing. // This is simply a DOM node, when the template has a single container, // as in `<p></p>`, or a "wire" in `<p></p><p></p>` and similar cases. return wire || (entry.wire = persistent(content)); }; // the stack retains, per each interpolation value, the cache // related to each interpolation value, or null, if the render // was conditional and the value is not special (Array or Hole) var unrollValues = function unrollValues(_ref3, values, length) { var stack = _ref3.stack; for (var i = 0; i < length; i++) { var hole = values[i]; // each Hole gets unrolled and re-assigned as value // so that domdiff will deal with a node/wire, not with a hole if (hole instanceof Hole) values[i] = unroll(stack[i] || (stack[i] = createCache()), hole); // arrays are recursively resolved so that each entry will contain // also a DOM node or a wire, hence it can be diffed if/when needed else if (isArray(hole)) unrollValues(stack[i] || (stack[i] = createCache()), hole, hole.length); // if the value is nothing special, the stack doesn't need to retain data // this is useful also to cleanup previously retained data, if the value // was a Hole, or an Array, but not anymore, i.e.: // const update = content => html`<div>${content}</div>`; // update(listOfItems); update(null); update(html`hole`) else stack[i] = null; } if (length < stack.length) stack.splice(length); }; /** * Holds all details wrappers needed to render the content further on. * @constructor * @param {string} type The hole type, either `html` or `svg`. * @param {string[]} template The template literals used to the define the content. * @param {Array} values Zero, one, or more interpolated values to render. */ function Hole(type, template, values) { this.type = type; this.template = template; this.values = values; } var create = Object.create, defineProperties = Object.defineProperties; // both `html` and `svg` template literal tags are polluted // with a `for(ref[, id])` and a `node` tag too var tag = function tag(type) { // both `html` and `svg` tags have their own cache var keyed = umap(new WeakMap()); // keyed operations always re-use the same cache and unroll // the template and its interpolations right away var fixed = function fixed(cache) { return function (template) { for (var _len = arguments.length, values = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { values[_key - 1] = arguments[_key]; } return unroll(cache, { type: type, template: template, values: values }); }; }; return defineProperties( // non keyed operations are recognized as instance of Hole // during the "unroll", recursively resolved and updated function (template) { for (var _len2 = arguments.length, values = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) { values[_key2 - 1] = arguments[_key2]; } return new Hole(type, template, values); }, { "for": { // keyed operations need a reference object, usually the parent node // which is showing keyed results, and optionally a unique id per each // related node, handy with JSON results and mutable list of objects // that usually carry a unique identifier value: function value(ref, id) { var memo = keyed.get(ref) || keyed.set(ref, create(null)); return memo[id] || (memo[id] = fixed(createCache())); } }, node: { // it is possible to create one-off content out of the box via node tag // this might return the single created node, or a fragment with all // nodes present at the root level and, of course, their child nodes value: function value(template) { for (var _len3 = arguments.length, values = new Array(_len3 > 1 ? _len3 - 1 : 0), _key3 = 1; _key3 < _len3; _key3++) { values[_key3 - 1] = arguments[_key3]; } return unroll(createCache(), { type: type, template: template, values: values }).valueOf(); } } }); }; // each rendered node gets its own cache var cache = umap(new WeakMap()); // rendering means understanding what `html` or `svg` tags returned // and it relates a specific node to its own unique cache. // Each time the content to render changes, the node is cleaned up // and the new new content is appended, and if such content is a Hole // then it's "unrolled" to resolve all its inner nodes. var render = function render(where, what) { var hole = typeof what === 'function' ? what() : what; var info = cache.get(where) || cache.set(where, createCache()); var wire = hole instanceof Hole ? unroll(info, hole) : hole; if (wire !== info.wire) { info.wire = wire; where.textContent = ''; // valueOf() simply returns the node itself, but in case it was a "wire" // it will eventually re-append all nodes to its fragment so that such // fragment can be re-appended many times in a meaningful way // (wires are basically persistent fragments facades with special behavior) where.appendChild(wire.valueOf()); } return where; }; var html = tag('html'); var svg = tag('svg'); exports.Hole = Hole; exports.html = html; exports.render = render; exports.svg = svg; return exports; }({}));