UNPKG

sinuous

Version:

🧬 Small, fast, reactive render engine

255 lines (221 loc) • 6.55 kB
import { h, hs, api } from './index.js'; import htm from './htm.js'; export const d = context(); export const ds = context(true); // `export const html = htm.bind(h)` is not tree-shakeable! export function dhtml() { return htm.apply(d, arguments); } // `export const svg = htm.bind(hs)` is not tree-shakeable! export function dsvg() { return htm.apply(ds, arguments); } export const _ = {}; let isHydrated; /** * Create a sinuous `treeify` function. * @param {boolean} isSvg * @return {Function} */ export function context(isSvg) { return function () { if (isHydrated) { // Hydrate on first pass, create on the rest. return (isSvg ? hs : h).apply(null, arguments); } let vnode; function item(arg) { if (arg == null); else if (arg === _ || typeof arg === 'function') { // Components can only be the first argument. if (vnode) { addChild(vnode, arg); } else { vnode = { type: arg, _children: [] }; } } else if (Array.isArray(arg)) { vnode = vnode || { _children: [] }; arg.forEach(item); } else if (typeof arg === 'object') { if (arg._children) { addChild(vnode, arg); } else { vnode._props = arg; } } else { // The rest is made into a string. if (vnode) { addChild(vnode, { type: null, _props: arg }); } else { vnode = { type: arg, _children: [] }; } } if (isSvg) vnode._isSvg = isSvg; } function addChild(parent, child) { parent._children.push(child); child._parent = parent; } Array.from(arguments).forEach(item); return vnode; }; } /** * Hydrates the root node with a passed delta tree structure. * * `delta` looks like: * { * type: 'div', * _props: { class: '' }, * _children: [] * } * * @param {object} delta * @param {Node} [root] * @return {Node} Returns the `root`. */ export function hydrate(delta, root) { if (!delta) { return; } if (typeof delta.type === 'function') { // Support Components delta = delta.type.apply( null, [delta._props].concat(delta._children.map((c) => c())) ); } let isFragment = delta.type === undefined; let isRootFragment; let el; if (!root) { root = document.querySelector(findRootSelector(delta)); } function findRootSelector(delta) { let selector = ''; let prop; if (delta._props && (prop = delta._props.id)) { selector = '#'; } else if (delta._props && (prop = delta._props.class)) { selector = '.'; } else if ((prop = delta.type)) { // delta.type is truthy } else { isRootFragment = true; return delta._children && findRootSelector(delta._children[0]()); } return ( selector + (typeof prop === 'function' ? prop() : prop) .split(' ') // Escape CSS selector https://bit.ly/36h9I83 .map((sel) => sel.replace(/([^\x80-\uFFFF\w-])/g, '\\$1')) .join('.') ); } function item(arg) { if (arg instanceof Node) { el = arg; // Keep a child pointer for multiple hydrate calls per element. el._index = el._index || 0; } else if (Array.isArray(arg)) { arg.forEach(item); } else if (el) { let target = filterChildNodes(el)[el._index]; let current; let prefix; const updateText = (text) => { el._index++; // Leave whitespace alone. if (target.data.trim() !== text.trim()) { if (arg._parent._children.length !== filterChildNodes(el).length) { // If the parent's virtual children length don't match the DOM's, // it's probably adjacent text nodes stuck together. Split them. target.splitText(target.data.indexOf(text) + text.length); if (current) { // Leave prefix whitespace intact. prefix = current.match(/^\s*/)[0]; } } // Leave whitespace alone. if (target.data.trim() !== text.trim()) { target.data = text; } } }; if (target) { // Skip placeholder underscore. if (arg === _) { el._index++; } else if (typeof arg === 'object') { if (arg.type === null && target.nodeType === 3) { // This is a text vnode, add noskip so spaces don't get skipped. target._noskip = true; updateText(arg._props); } else if (arg.type) { hydrate(arg, target); el._index++; } } } if (typeof arg === 'function') { current = target ? target.data : undefined; prefix = ''; let hydrated; let marker; let startNode; api.subscribe(() => { isHydrated = hydrated; let result = arg(); if (result && result._children) { result = result.type ? result : result._children; } const isStringable = typeof result === 'string' || typeof result === 'number'; result = isStringable ? prefix + result : result; if (hydrated || (!target && !isFragment)) { current = api.insert(el, result, marker, current, startNode); } else { if (isStringable) { updateText(result); } else { if (Array.isArray(result)) { startNode = target; target = el; } if (isRootFragment) { target = el; } hydrate(result, target); current = []; } if (!isRootFragment && target) { marker = api.add(el, '', filterChildNodes(el)[el._index]); } else { marker = api.add(el.parentNode, '', el.nextSibling); } } isHydrated = false; hydrated = true; }); } else if (typeof arg === 'object') { if (!arg._children) { api.property(el, arg, null, delta._isSvg); } } } } [root, delta._props, delta._children || delta].forEach(item); return el; } /** * Filter out whitespace text nodes unless it has a noskip indicator. * * @param {Node} parent * @return {Array} */ function filterChildNodes(parent) { return Array.from(parent.childNodes).filter( (el) => el.nodeType !== 3 || el.data.trim() || el._noskip ); }