UNPKG

bbpro

Version:

BrowserBox - remote browser isolation tool

1,431 lines (1,276 loc) 78.3 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : typeof define === 'function' && define.amd ? define(['exports'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory((global.vv = global.vv || {}, global.vv.bang = {}))); })(this, (function (exports) { // eslint directives /* eslint-disable no-empty */ // vanillaview.js // imports const CODE = Math.random().toFixed(18); const IMMEDIATE = Symbol.for(`[[IMMEDIATE]]`); // backwards compatible alias const skip = markup; const attrskip = attrmarkup; const NULLFUNC = () => void 0; /* eslint-disable no-useless-escape */ const KEYMATCH = /(?:<!\-\-)?(key0.\d+)(?:\-\->)?/gm; /* eslint-enable no-useless-escape */ const ATTRMATCH = /\w+=/; const JOINER = '<link rel=join>'; const KEYLEN = 20; const XSS = () => `Possible XSS / object forgery attack detected. ` + `Object code could not be verified.`; const OBJ = x => ({message:`Object values not allowed here.`, x}); const UNSET = () => `Unset values not allowed here.`; const INSERT = () => `Error inserting template into DOM. ` + `Position must be one of: ` + `replace, beforebegin, afterbegin, beforeend, innerhtml, afterend`; const NOTFOUND = loc => `Error inserting template into DOM. ` + `Location ${loc} was not found in the document.`; const MOVE = new class { beforeend (frag,elem) { elem.appendChild(frag); } beforebegin (frag,elem) { elem.parentNode.insertBefore(frag,elem); } afterend (frag,elem) { elem.parentNode.insertBefore(frag,elem.nextSibling); } replace (frag,elem) { elem.parentNode.replaceChild(frag,elem); } afterbegin (frag,elem) { elem.insertBefore(frag,elem.firstChild); } innerhtml (frag,elem) { elem.replaceChildren(); elem.appendChild(frag); } insert (frag,node) { node.replaceChildren(frag); } }; const REMOVE_MAP = new Map(); const DIV = document.createElement('div'); const POS = 'beforeend'; const EMPTY = ''; const {stringify:_STR} = JSON; const JS = o => _STR(o, null, EMPTY); const isVV = x => x?.code === CODE && Array.isArray(x.nodes); const NextFunc = () => `f${FuncCounter++}` + (Math.random()*10).toString(36).replace('.', '_'); // logging globalThis.onerror = (...v) => (console.log(v, v[0]+EMPTY, v[4] && v[4].message, v[4] && v[4].stack), true); // type functions const isKey = v => !!v && (typeof v.key === 'string' || typeof v.key === 'number') && Object.getOwnPropertyNames(v).length <= 2; // state let FuncCounter = 10; // cache const cache = {}; // deux // main exports Object.assign(s,{attrskip,skip,attrmarkup,markup,guardEmptyHandlers,die}); Object.assign(globalThis, {vanillaview: {c, s}}); async function s(p,...v) { const that = this; let SystemCall = false; let state; if ( p[0].length === 0 && v[0].state ) { // by convention (see how we construct the template that we tag with FUNC) // the first value is the state object when our system calls it SystemCall = true; } if ( SystemCall ) { ({state} = v.shift()); p.shift(); v = await Promise.all(v.map(val => process(that, val, state))); const xyz = vanillaview(p,v); return xyz; } else { const laterFunc = async state => { v = await Promise.all(v.map(val => process(that, val, state))); const xyz = vanillaview(p,v); //xyz[Symbol.for('BANG-VV')] = true; return xyz; }; laterFunc[IMMEDIATE] = true; //laterFunc[Symbol.for('BANG-VV')] = true; return laterFunc; } } function c(p,...v) { return vanillaview(p,v, {useCache:false}); } // main function (TODO: should we refactor?) function vanillaview(p,v,{useCache:useCache=true}={}) { const retVal = {}; let instance, cacheKey; v = v.map(guardAndTransformVal); if ( useCache ) { (instance = (v.find(isKey) || {})); cacheKey = p.join(JOINER); const {cached,firstCall} = isCached(cacheKey,v,instance); if ( ! firstCall ) { cached.update(v); return cached; } else { retVal.oldVals = Array.from(v); } } else { retVal.oldVals = Array.from(v); } // compile the template into an updater p = [...p]; const vmap = {}; const V = v.map(replaceValWithKeyAndOmitInstanceKey(vmap)); const externals = []; let str = EMPTY; while( p.length > 1 ) str += p.shift() + V.shift(); str += p.shift(); const frag = toDOM(str); // FIXME: do we need to walk through shadows here? const walker = document.createTreeWalker(frag, NodeFilter.SHOW_ALL); do { makeUpdaters({walker,vmap,externals}); } while(walker.nextNode()) Object.assign(retVal, { externals, v:Object.values(vmap), cacheKey, instance, to, update, code:CODE, nodes:Array.from(frag.childNodes) }); if ( useCache ) { if ( instance.key !== undefined ) { cache[cacheKey].instances[instance.key] = retVal; } else { cache[cacheKey] = retVal; } retVal.nodes.forEach(node => { REMOVE_MAP.set(node, {ck:cacheKey, ik: instance.key+EMPTY}); }); } return retVal; } // bang integration functions (modified from bang versions) async function process(that, x, state) { if ( typeof x === 'string' ) return x; else if ( typeof x === 'number' ) return x+EMPTY; else if ( typeof x === 'boolean' ) return x+EMPTY; else if ( x instanceof Date ) return x+EMPTY; else if ( isUnset(x) ) { if ( CONFIG.allowUnset ) return CONFIG.unsetPlaceholder || EMPTY; else { throw new TypeError(`Value cannot be unset, was: ${x}`); } } else if ( x instanceof Promise ) return await process(that, await x.catch(err => err+EMPTY), state); else if ( x instanceof Element ) return x.outerHTML; else if ( x instanceof Node ) return x.textContent; else if ( isVV(x) ) { return {code:CODE, externals: x.externals, nodes: x.nodes}; } const isArray = Array.isArray(x); const isVVArray = isArray && (x.length === 0 || isVV(x[0])); if ( isIterable(x) ) { if ( isVVArray ) { return join(x); // is a func array ? } else if ( (x[0] instanceof Function) && ! x[0][IMMEDIATE] ) { const randomName = NextFunc(); if ( ! state._funcs ) state._funcs = {}; if ( ! state._tasks ) state._tasks = []; const func = ( function(ev) { for( const fun of x ) { try { fun(ev); } catch(e) { console.warn(`Handler in func array failed`, {fun, e, ev, x}); } } } ); state._funcs[randomName] = func; state._tasks.push(component => (component[randomName] = func, randomName)); // return "console.log(event)"; return `${randomName}(event)`; } else if ( x[0] instanceof Element || x[0] instanceof Node ) { return {code:CODE, externals: [], nodes: x}; } else { // if an Array or iterable is given then // its values are recursively processed via this same function return process(that, await Promise.all( ( await Promise.all(Array.from(x)).catch(e => e+EMPTY) ).map(v => process(that, v, state)) ), state); } } const isVVK = isKey(x); const isMAO = x.code === CODE && typeof x.str === "string"; if ( isVVK || isMAO || isVV(x) ) { return x; // let vanillaview guardAndTransformVal handle } else if ( x[IMMEDIATE] && Object.getPrototypeOf(x).constructor.name === 'AsyncFunction' ) { return await process(that, await x(state), state); } else if ( x[IMMEDIATE] && (x instanceof Function) ) return x(state); else // it's an object, of some type if ( x instanceof Function ) { const name = NextFunc(); if ( ! state._funcs ) state._funcs = {}; if ( ! state._tasks ) state._tasks = []; state._funcs[name] = x; state._tasks.push(component => (component[name] = x, name)); //console.log({name, x:x+''}, state._tasks); //return "console.log(event)"; return `${name}(event)`; } else { // State store /* so we assume an object is state and save it */ /* to the global state store */ /* which is two-sides so we can find a key */ /* given an object. This avoid duplicates */ let stateKey; // own keys // an object can specify it's own state key // to provide a single logical identity for a piece of state that may // be represented by many objects if ( Object.prototype.hasOwnProperty.call(x, CONFIG.bangKey) ) { stateKey = new that.StateKey(x[CONFIG.bangKey])+EMPTY; // in that case, replace the previously saved object with the same logical identity const oldX = that.STATE.get(stateKey); that.STATE.delete(oldX); that.STATE.set(stateKey, x); that.STATE.set(x, stateKey); } else /* or the system can come up with a state key */ { const jsx = JS(x); if ( that.STATE.has(x) || that.STATE.has(jsx) ) { stateKey = (that.STATE.get(x) || that.STATE.get(jsx)).replace(/.json.last$/,''); const lastXJSON = that.STATE.get(stateKey+'.json.last'); if ( jsx !== lastXJSON ) { that.STATE.delete(lastXJSON); if ( stateKey.startsWith('system-key') ) { that.STATE.delete(stateKey); const oKey = stateKey; stateKey = new that.StateKey()+EMPTY; console.log({oKey, stateKey}); } that.STATE.set(stateKey, x); that.STATE.set(x, stateKey); } } else { const oKey = stateKey; stateKey = new that.StateKey()+EMPTY; console.log({oKey, stateKey, block2:true, jsx}); that.STATE.set(stateKey, x); that.STATE.set(x, stateKey); } that.STATE.set(jsx, stateKey+'.json.last'); that.STATE.set(stateKey+'.json.last', jsx); } stateKey += EMPTY; return stateKey; } } function isIterable(y) { if ( y === null ) return false; return y[Symbol.iterator] instanceof Function; } function isUnset(x) { return x === undefined || x === null; } // to function function to(location, options) { const position = (options || 'replace').toLocaleLowerCase(); const frag = document.createDocumentFragment(); this.nodes.forEach(n => frag.appendChild(n)); const isNode = location instanceof Node; const elem = isNode ? location : document.querySelector(location); try { MOVE[position](frag,elem); } catch(e) { switch(e.constructor && e.constructor.name) { case "DOMException": die({error: INSERT()}); break; case "TypeError": die({error: NOTFOUND(location)}); break; default: throw e; } } while(this.externals.length) { this.externals.shift()(); } } // update functions function makeUpdaters({walker,vmap,externals}) { const node = walker.currentNode; if ( node.shadowRoot instanceof ShadowRoot ) { throw new TypeError(`Shadow not supported here currently`); } switch( node.nodeType ) { case Node.ELEMENT_NODE: handleElement({node,vmap,externals}); break; case Node.COMMENT_NODE: case Node.TEXT_NODE: handleNode({node,vmap,externals}); break; } } // debug stuff const Replacers = new Map(); // end debug stuff function handleNode({node,vmap,externals}) { const lengths = []; const text = node.nodeValue; let result = KEYMATCH.exec(text); while ( result ) { const {index} = result; const key = result[1]; const val = vmap[key]; const replacer = makeNodeUpdater({node,index,lengths,val}); const wrappedReplacer = () => { try { replacer(val.val); } catch(error) { console.warn(`Error in replacer for key ${key}`, {val, error}); } Replacers.delete(key); }; externals.push(wrappedReplacer); Replacers.set(key, wrappedReplacer); val.replacers.push( replacer ); result = KEYMATCH.exec(text); } } // node functions function makeNodeUpdater(nodeState) { const {node} = nodeState; const scope = Object.assign({}, nodeState, { oldVal: {length: KEYLEN}, oldNodes: [node], lastAnchor: node, }); return (newVal) => { if ( scope.oldVal == newVal ) return; scope.val.val = newVal; switch(getType(newVal)) { case "vanillaviewobject": handleMarkupInNode(newVal, scope); break; default: handleTextInNode(newVal, scope); break; } }; } function handleMarkupInNode(newVal, state) { let {oldNodes,lastAnchor} = state; if ( newVal.nodes.length ) { if ( sameOrder(oldNodes,newVal.nodes) ) ; else { { const insertable = []; Array.from(newVal.nodes).forEach(node => { const inserted = document.contains(node.ownerDocument); if ( ! inserted ) { insertable.push(node); } else { while( insertable.length ) { const insertee = insertable.shift(); node.parentNode.insertBefore(insertee, node); } } }); while ( insertable.length ) { const insertee = insertable.shift(); lastAnchor.parentNode.insertBefore(insertee,lastAnchor); } state.lastAnchor = newVal.nodes[newVal.nodes.length-1]; } } } else { const placeholderNode = summonPlaceholder(lastAnchor); lastAnchor.parentNode.insertBefore(placeholderNode,lastAnchor.nextSibling); state.lastAnchor = placeholderNode; } // MARK: Unbond event might be relevant here. // if nodes are not included we can just remove them const dn = diffNodes(oldNodes,newVal.nodes); if ( dn.size ) { const f = document.createDocumentFragment(); const killSet = new Set(); dn.forEach(n => { f.appendChild(n); if ( n.linkedCustomElement ) { f.appendChild(n.linkedCustomElement); } if ( n.nodeType === Node.COMMENT_NODE && n.textContent.match(/key\d+/) ) return; const kill = REMOVE_MAP.get(n); if ( kill ) { killSet.add(JS(kill)); } else { console.warn(`No kill signature for`, n, REMOVE_MAP); } }); killSet.forEach(kill => { const {ck: cacheKey, ik: instanceKey} = JSON.parse(kill); try { if ( cacheKey && instanceKey && instanceKey !== "undefined" ) { cache[cacheKey].instances[instanceKey] = null; } else if ( cacheKey ) { cache[cacheKey] = null; } } catch(e) { console.warn(`Error in kill for`, {kill, cacheKey, instanceKey}); } }); } state.oldNodes = newVal.nodes || [lastAnchor]; while ( newVal.externals.length ) { const func = newVal.externals.shift(); func(); } } function sameOrder(nodesA, nodesB) { if ( nodesA.length != nodesB.length ) return false; return Array.from(nodesA).every((an,i) => an == nodesB[i]); } function handleTextInNode(newVal, state) { let {oldVal, index, val, lengths, node} = state; const valIndex = val.vi; const originalLengthBefore = Object.keys(lengths.slice(0,valIndex)).length*KEYLEN; const lengthBefore = lengths.slice(0,valIndex).reduce((sum,x) => sum + x, 0); const value = node.nodeValue; lengths[valIndex] = newVal.length; const correction = lengthBefore-originalLengthBefore; const before = value.slice(0,index+correction); const after = value.slice(index+correction+oldVal.length); const newValue = before + newVal + after; node.nodeValue = newValue; if ( node.linkedCustomElement && newValue !== oldVal ) { updateLinkedCustomElement(node); } state.oldVal = newVal; } // element attribute functions function updateLinkedCustomElement(node) { const lce = node.linkedCustomElement; const span = toDOM(`<span ${node.textContent}></span>`).firstChild; //FIXME: may have to look at this for the combination of vv and bang, may not need to remove these const toRemove = new Set( getAttributes(lce) .filter(({name}) => !name.startsWith('on')) .map(({name}) => name) ); getAttributes(span).forEach(({name, value}) => { if ( name === lce.localName ) return; // i.e., it's the bang tag name if ( name.startsWith('on') ) return; // we don't handle event handlers here, that's in bang lce.setAttribute(name, value); toRemove.delete(name); }); toRemove.forEach(name => lce.removeAttribute(name)); } function handleElement({node,vmap,externals}) { getAttributes(node).forEach(({name,value} = {}) => { const attrState = {node, vmap, externals, name, lengths: []}; KEYMATCH.lastIndex = 0; let result = KEYMATCH.exec(name); while( result ) { prepareAttributeUpdater(result, attrState, {updateName:true}); result = KEYMATCH.exec(name); } KEYMATCH.lastIndex = 0; result = KEYMATCH.exec(value); while( result ) { prepareAttributeUpdater(result, attrState, {updateName:false}); result = KEYMATCH.exec(value); } }); } function prepareAttributeUpdater(result, attrState, {updateName}) { const {index, input} = result; const scope = Object.assign({}, attrState, { index, input, updateName, val: attrState.vmap[result[1]], oldVal: {length: KEYLEN}, oldName: attrState.name, }); let replacer; if ( updateName ) { replacer = makeAttributeNameUpdater(scope); } else { replacer = makeAttributeValueUpdater(scope); } scope.externals.push(() => replacer(scope.val.val)); scope.val.replacers.push( replacer ); } // FIXME: needs to support multiple replacements just like value // QUESTION: why is the variable oldName so required here, why can't we call it oldVal? // if we do it breaks, WHY? function makeAttributeNameUpdater(scope) { let {oldName,node,val} = scope; return (newVal) => { if ( oldName == newVal ) return; val.val = newVal; const attr = node.hasAttribute(oldName) ? oldName : EMPTY; if ( attr !== newVal ) { if ( attr ) { node.removeAttribute(oldName); node[oldName] = undefined; } if ( newVal ) { newVal = newVal.trim(); let name = newVal, value = undefined; if( ATTRMATCH.test(newVal) ) { const assignmentIndex = newVal.indexOf('='); ([name,value] = [newVal.slice(0,assignmentIndex), newVal.slice(assignmentIndex+1)]); } reliablySetAttribute(node, name, value); } oldName = newVal; } }; } function makeAttributeValueUpdater(scope) { const updater = (newVal) => { if ( scope.oldVal == newVal ) return; scope.val.val = newVal; switch(getType(newVal)) { case "funcarray": updateAttrWithFuncarrayValue(newVal, scope); break; case "function": updateAttrWithFunctionValue(newVal, scope); break; case "handlers": updateAttrWithHandlersValue(newVal, scope); break; case "vanillaviewobject": newVal = nodesToStr(newVal.nodes); updateAttrWithTextValue(newVal, scope); break; /* eslint-disable no-fallthrough */ case "markupattrobject": // deliberate fall through newVal = newVal.str; default: updateAttrWithTextValue(newVal, scope); break; /* eslint-enable no-fallthrough */ } }; // call it the first time so it loads well // and we elide out the key placeholders here updater(scope.val.val); return updater; } // helpers function getAttributes(node) { if ( ! node.hasAttribute ) return []; // for parity with classList.add (which trims whitespace) // otherwise once the classList manipulation happens // our indexes for replacement will be off if ( node.hasAttribute('class') ) { node.setAttribute('class', formatClassListValue(node.getAttribute('class'))); } return Array.from(node.attributes); /* if ( !! node.attributes && Number.isInteger(node.attributes.length) ) return Array.from(node.attributes); const attrs = []; for ( const name of node ) { if ( node.hasAttribute(name) ) { attrs.push({name, value:node.getAttribute(name)}); } } return attrs; */ } function updateAttrWithFunctionValue(newVal, scope) { let {oldVal,node,name,externals} = scope; if ( name !== 'bond' ) { let flags = {}; if ( name.includes(':') ) { ([name, ...flags] = name.split(':')); flags = flags.reduce((O,f) => { O[f] = true; return O; }, {}); } if ( oldVal ) { node.removeEventListener(name, oldVal, flags); } node.addEventListener(name, newVal, flags); reliablySetAttribute(node, name, EMPTY); } else { if ( oldVal ) { const index = externals.indexOf(oldVal); if ( index >= 0 ) { externals.splice(index,1); } } externals.push(() => newVal(node)); } scope.oldVal = newVal; } function updateAttrWithFuncarrayValue(newVal, scope) { let {oldVal,node,name,externals} = scope; if ( oldVal && ! Array.isArray(oldVal) ) { oldVal = [oldVal]; } if ( name !== 'bond' ) { let flags = {}; if ( name.includes(':') ) { ([name, ...flags] = name.split(':')); flags = flags.reduce((O,f) => { O[f] = true; return O; }, {}); } if ( oldVal ) { oldVal.forEach(of => node.removeEventListener(name, of, flags)); } newVal.forEach(f => node.addEventListener(name, f, flags)); } else { if ( oldVal ) { oldVal.forEach(of => { const index = externals.indexOf(of); if ( index >= 0 ) { externals.splice(index,1); } }); } newVal.forEach(f => externals.push(() => f(node))); } scope.oldVal = newVal; } function updateAttrWithHandlersValue(newVal, scope) { let {oldVal,node,externals,} = scope; if ( !!oldVal && typeof oldVal === 'object' ) { Object.entries(oldVal).forEach(([eventName,funcVal]) => { if ( eventName !== 'bond' ) { let flags = {}; if ( eventName.includes(':') ) { ([eventName, ...flags] = eventName.split(':')); flags = flags.reduce((O,f) => { O[f] = true; return O; }, {}); } console.log(eventName, funcVal, flags); node.removeEventListener(eventName, funcVal, flags); } else { const index = externals.indexOf(funcVal); if ( index >= 0 ) { externals.splice(index,1); } } }); } Object.entries(newVal).forEach(([eventName,funcVal]) => { if ( eventName !== 'bond' ) { let flags = {}; if ( eventName.includes(':') ) { ([eventName, ...flags] = eventName.split(':')); flags = flags.reduce((O,f) => { O[f] = true; return O; }, {}); } node.addEventListener(eventName, funcVal, flags); } else { externals.push(() => funcVal(node)); } }); scope.oldVal = newVal; } function updateAttrWithTextValue(newVal, scope) { let {oldVal,node,index,name,val,lengths,oldAttrVal} = scope; let attr = node.getAttribute(name); let newAttrValue; if ( oldAttrVal === oldVal ) { // if we are setting old val to be the whole attribute value // then we can just splice it in by setting it easily newAttrValue = newVal; } else { // otherwise we need to carefully calculate everything let zeroWidthCorrection = 0; const valIndex = val.vi; const originalLengthBefore = Object.keys(lengths.slice(0,valIndex)).length*KEYLEN; // we need to trim newVal to have parity with classlist add // the reason we have zeroWidthCorrection = -1 // is because the classList is a set of non-zero width tokens // separated by spaces // when we have a zero width token, we have two adjacent spaces // which, by virtue of our other requirement, gets replaced by a single space // effectively elliding out our replacement location // in order to keep our replacement location in tact // we need to compensate for the loss of a token slot (effectively a token + a space) // and having a -1 correction effectively does this. if ( name == "class" ) { newVal = newVal.trim(); if ( newVal.length == 0 ) { zeroWidthCorrection = -1; } scope.val.val = newVal; } lengths[valIndex] = newVal.length + zeroWidthCorrection; const lengthBefore = lengths.slice(0,valIndex).reduce((sum,x) => sum + x, 0); const correction = lengthBefore-originalLengthBefore; const before = attr.slice(0,index+correction); const after = attr.slice(index+correction+oldVal.length); if ( name == "class" ) { const spacer = oldVal.length == 0 ? ' ' : EMPTY; newAttrValue = before + spacer + newVal + spacer + after; } else { newAttrValue = before + newVal + after; } } if ( attr !== newAttrValue ) { reliablySetAttribute(node, name, newAttrValue); } scope.oldVal = newVal; scope.oldAttrVal = newAttrValue; } function reliablySetAttribute(node, name, value, /*{funcValue} = {}*/) { if ( name == "class" ) { value = formatClassListValue(value); } const oName = name; let modifiers; if ( name.includes(':') ) { ([name, ...modifiers] = name.split(':')); } if ( modifiers ) { modifiers = modifiers.map(m => ([m, true])); console.warn("not handling modifiers currently", {node, name, value, modifiers}); //node.addEventListener(name, funcValue, Object.fromEntries(modifiers)); } if ( CONFIG.EVENTS.includes('on'+name) ) { node.removeAttribute(oName); name = 'on'+name; const existingValue = node.getAttribute(name); if ( existingValue?.startsWith('this.') ) { return; } } try { node.setAttribute(name,isUnset(value) ? name : value); } catch(e) { } // if you set style like this is fucks it up if ( name in node && name !== 'style' ) { try { node[name] = isUnset(value) ? true : value; } catch(e) { } } } function getType(val) { const to = typeof val; const type = to === 'function' ? 'function' : val.code === CODE && Array.isArray(val.nodes) ? 'vanillaviewobject' : val.code === CODE && typeof val.str === 'string' ? 'markupattrobject' : Array.isArray(val) && (val.length === 0 || ( val[0].code === CODE && Array.isArray(val[0].nodes) )) ? 'vanillaviewarray' : Array.isArray(val) && (val.length === 0 || ( typeof val[0] === 'function' )) ? 'funcarray' : to === 'object' ? 'handlers' : 'default' ; return type; } function summonPlaceholder(sibling) { let ph = [...sibling.parentNode.childNodes].find( node => node.isConnected && node.nodeType == Node.COMMENT_NODE && node.nodeValue == 'vanillaview_placeholder' ); if ( ! ph ) { ph = toDOM(`<!--vanillaview_placeholder-->`).firstChild; } return ph; } // cache helpers // FIXME: function needs refactor function isCached(cacheKey,v,instance) { let firstCall; let cached = cache[cacheKey]; if ( cached == undefined ) { cached = cache[cacheKey] = {}; if ( instance.key !== undefined ) { cached.instances = {}; cached = cached.instances[instance.key] = {}; } firstCall = true; } else { if ( instance.key !== undefined ) { if ( ! cached.instances ) { cached.instances = {}; firstCall = true; } else { cached = cached.instances[instance.key]; if ( ! cached ) { firstCall = true; } else { if ( instance.kill === true ) { cached = cache[cacheKey]; if ( cached && cached.instances ) { cached.instances[instance.key] = null; } cached = null; firstCall = true; } else { firstCall = false; } } } } else { firstCall = false; } } //console.log({cached,firstCall,instance}); return {cached,firstCall}; } // Markup helpers // Returns an object that VanillaView treats as markup, // even tho it is NOT a VanillaView Object (defined with R/X/$) // And even tho it is in the location of a template value replacement // Which would normally be the treated as String function markup(str) { str = isUnset(str) ? EMPTY : str; const frag = toDOM(str); const retVal = { type: 'MarkupObject', code:CODE, nodes:Array.from(frag.childNodes), externals: [] }; return retVal; } // Returns an object that VanillaView treats, again, as markup // But this time markup that is OKAY to have within a quoted attribute function attrmarkup(str) { str = isUnset(str) ? EMPTY : str; str = str.replace(/"/g,'&quot;'); const retVal = { type: 'MarkupAttrObject', code: CODE, str }; return retVal; } function guardEmptyHandlers(val) { if ( Array.isArray(val) ) { if ( val.length == 0 ) { return [NULLFUNC] } return val; } else { if ( isUnset(val) ) { return NULLFUNC; } } } // other helpers function formatClassListValue(value) { value = value.trim(); value = value.replace(/\s+/g, ' '); return value; } function replaceValWithKeyAndOmitInstanceKey(vmap) { return (val,vi) => { // omit instance key if ( isKey(val) ) { return EMPTY; } const key = 'key'+Math.random().toFixed(15); let k = key; if ( val.code === CODE && Array.isArray(val.nodes) ) { k = `<!--${k}-->`; } vmap[key] = {vi,val,replacers:[]}; return k; }; } function toDOM(str) { DIV.replaceChildren(); DIV.insertAdjacentHTML(POS, `<template>${str}</template>`); return DIV.firstElementChild.content; } function guardAndTransformVal(v) { const isVVArray = Array.isArray(v) && (v.length === 0 || isVV(v[0])); const isNotSet = isUnset(v); const isForgery = v.code !== CODE && Array.isArray(v.nodes); const isObject = typeof v === 'object'; if ( isVVArray ) return join(v); if ( isKey(v) ) return v; if ( v.code === CODE ) return v; if ( isNotSet ) die({error: UNSET()}); if ( isForgery ) die({error: XSS()}); if ( Array.isArray(v) && v[0] instanceof Node ) { return {code:CODE, nodes: v, externals: []}; } if ( Array.isArray(v) && v[0] instanceof Function ) { return v; } if ( isObject ) die({error: OBJ(v)}); return v+EMPTY; } function join(os) { const externals = []; const bigNodes = []; const v = []; const oldVals = []; for( const o of os ) { //v.push(...o.v); //oldVals.push(...o.oldVals); externals.push(...o.externals); bigNodes.push(...o.nodes); } const retVal = {v,code:CODE,oldVals,nodes:bigNodes,to,update,externals}; return retVal; } function nodesToStr(nodes) { const frag = document.createDocumentFragment(); nodes.forEach(n => frag.appendChild(n.cloneNode(true))); const container = document.createElement('body'); container.appendChild(frag); return container.innerHTML; } function diffNodes(last,next) { last = new Set(last); next = new Set(next); return new Set([...last].filter(n => !next.has(n))); } function update(newVals) { const updateable = this.v.filter(({vi}) => didChange(newVals[vi], this.oldVals[vi])); updateable.forEach(({vi,replacers}) => replacers.forEach(f => f(newVals[vi]))); this.oldVals = Array.from(newVals); } function didChange(oldVal, newVal) { const [oldType, newType] = [oldVal, newVal].map(getType); let ret; if ( oldType != newType ) { ret = true; } else { switch(oldType) { case "vanillaviewobject": // the vanillaview object is returned by a view function // which has already called its updaters and checked its slot values // to determine and show changes // except in the case of a list of nodes ret = true; break; /* eslint-disable no-fallthrough */ case "funcarray": case "function": // hard to equate even if same str value as scope could be diff ret = true; break; case "vanillaviewarray": // need to do array dif so don't do here ret = true; break; case "markupattrobject": // need to check multiple things ret = true; break; default: ret = JS(oldVal) !== JS(newVal); break; /* eslint-enable no-fallthrough */ } } return ret; } // reporting and error helpers function die(msg,err) { msg.stack = (new Error()).stack.split(/\s*\n\s*/g); throw msg; } exports.c = c; exports.s = s; Object.defineProperty(exports, '__esModule', { value: true }); })); /* eslint-disable no-setter-return, no-with, no-constant-condition, no-async-promise-executor */ (function () { // constants, classes, config and state const DEBUG = false; const IMMEDIATE = Symbol.for(`[[IMMEDIATE]]`); const NAMESPACE = 'b'; const PIPELINE_REQUESTS = true; const RANDOM_SLEEP_ON_FIRST_PRINT = true; const RESPONSIVE_MEDIATION = true; const USE_XPATH = true; const X_NS_ATTRS = `.//@*[starts-with(name(), '${NAMESPACE}:')]`; const X_NEWLISTENING = document.createExpression(X_NS_ATTRS); const XON_EVENT_ATTRS = `.//@*[starts-with(local-name(), 'on')]`; const X_LISTENING = document.createExpression(XON_EVENT_ATTRS); const OPTIMIZE = true; const GET_ONLY = true; const MOBILE = isMobile(); const GC_TIMEOUT = 10000; //const GENERATOR = (function*(){yield}()).constructor; const EMPTY = ''; const {stringify:_STR} = JSON; const JS = o => _STR(o, Replacer, EMPTY); const LIGHTHOUSE = navigator.userAgent.includes("Chrome-Lighthouse"); const DOUBLE_BARREL = /^\w+-(?:\w+-?)*$/; // note that this matches triple- and higher barrels, too const POS = 'beforeend'; const LOCAL_PATH = 'this.'; const PARENT_PATH = 'this.getRootNode().host.'; const ONE_HIGHER = 'getRootNode().host.'; const CALL_WITH_EVENT = '(event)'; const F = _FUNC; const FUNC_CALL = /\);?$/; const MirrorNode = Symbol.for('[[MN]]'); const DIV = document.createElement('div'); const path = location.pathname; const CONFIG = { htmlFile: 'markup.html', scriptFile: 'script.js', styleFile: 'style.css', bangKey: '_bang_key', componentsPath: `${path}${path.endsWith('/') ? EMPTY : '/'}../components`, allowUnset: true, unsetPlaceholder: EMPTY, EVENTS: `bond error load click pointerdown pointerup pointermove mousedown mouseup submit mousemove touchstart touchend touchmove touchcancel dblclick dragstart dragend dragmove drag mouseover mouseout focus blur focusin focusout scroll input change compositionstart compositionend text paste beforepaste select cut copy keydown keyup keypress compositionupdate contextmenu wheel `.split(/\s+/g).filter(s => s.length).map(e => `[on${e}]`).join(','), delayFirstPaintUntilLoaded: false, capBangRatioAtUnity: false, noHandlerPassthrough: false } const History = []; const STATE = new Map(); const CACHE = new Map(); const Waiters = new Map(); const Started = new Set(); const TRANSFORMING = new WeakSet(); const Dependents = new Map(); const MAX_CONCURRENT_REQUESTS = 5; const RequestPipeLine = new Map(); const RequestWaiting = []; class Counter { started = 0; finished = 0; constructor(root) { root.counts = this; this.root = root; } check() { const {root} = this; const isTopLevel = root === document; let loaded = false; if ( isTopLevel ) { const noSwiftDescendents = root.querySelectorAll('.bang-el:not([lazy])').length === 0; loaded = noSwiftDescendents; } else { const nonZeroCheck = this.started > 0; const finishedCheck = this.finished >= this.started; loaded = nonZeroCheck && finishedCheck; } return loaded; } start() { if ( this.root == document ) say('log', 'Counting start'); this.started++; } finish() { if ( this.root == document ) say('log', 'Counting finished'); this.finished++; } } const SHADOW_OPTS = {mode:'open', delegatesFocus: true}; const OBSERVE_OPTS = {subtree: true, childList: true, characterData: true}; const INSERT = 'insert'; const ALL_DEPS = {allDependents: true}; let LoadChecker; let RequestId = 0; let hindex = 0; let observer; // global mutation observer let systemKeys = 1; let _c$; const BangBase = (name) => class Base extends HTMLElement { static #activeAttrs = ['state']; // we listen for changes to these attributes only static get observedAttributes() { return Array.from(Base.#activeAttrs); } #name = name; #dependents = []; constructor() { super(); new Counter(this); this.cookMarkup = async (markup, state) => { const cooked = await cook.call(this, markup, state); if ( !this.shadowRoot ) { const shadow = this.attachShadow(SHADOW_OPTS); state._tasks && state._tasks.forEach(t => { const funcName = t(this); DEBUG && console.log(`Applied automatic event handler function ${funcName} to component ${this}`); }); observer.observe(shadow, OBSERVE_OPTS); await cooked.to(shadow, INSERT); cookListeners(shadow); // add dependents const deps = await findBangs(transformBang, shadow, ALL_DEPS); this.#dependents = deps.map(node => node.untilVisible()); } } this.markLoaded = async () => { this.alreadyPrinted = true; if ( ! this.loaded ) { this.counts.finish(); const loaded = await this.untilLoaded(); if ( loaded ) { this.loaded = loaded; this.setVisible(); if ( ! this.isLazy ) { setTimeout(() => document.counts.finish(), 0); } } else { console.warn('Not loaded', this); // right now this never happens } } } this.loadCheck = () => this.counts.check(); this.visibleCheck = () => this.classList?.contains('bang-styled'); this.loadKey = Math.random().toString(36); this.visibleLoadKey = Math.random().toString(36); } get name() { return this.#name; } // BANG! API methods async print() { if ( !this.alreadyPrinted ) { this.prepareVisibility(); } const state = this.handleAttrs(this.attributes); if ( OPTIMIZE ) { const nextState = JS(state); if ( this.alreadyPrinted && this.lastState === nextState ) { return; } this.lastState = nextState; } return this.printShadow(state) } update() { if ( this.fastUpdate ) { return this.fastUpdate(); } else { return this.print(); } } prepareVisibility() { this.classList.add('bang-el'); this.counts.start(); if ( !this.isLazy ) { document.counts.start(); } this.classList.remove('bang-styled'); // we prefetch the style fetchStyle(name).catch(err => { say('warn', err); }); } async untilLoaded() { const myDependentsLoaded = (await Promise.all(this.#dependents)).every(visible => visible); const myContentLoaded = await becomesTrue(this.loadCheck, this.loadKey); DEBUG && console.log(new Date - self.Start); return myContentLoaded && myDependentsLoaded; } async untilVisible() { if ( this.isLazy ) return true; return await becomesTrue(this.visibleCheck, this.visibleLoadKey); } get deps() { return this.#dependents; } updateIfChanged(state) { const {didChange} = stateChanged(state); if ( didChange ) { const views = getViews(state); const newKey = updateState(state); views.forEach(view => view.setAttribute('state', newKey)); } } setVisible() { this.classList.add('bang-styled'); } get state() { const key = this.getAttribute('state'); return getState(key); //(key); } set state(newValue) { const key = this.getAttribute('state'); if ( key.startsWith('system-key:') ) { return this.updateIfChanged(this.state); } return setState(key, newValue); } // Web Components methods attributeChangedCallback(name, oldValue) { if ( name === 'state' && !isUnset(oldValue) ) { this.update(); } } connectedCallback() { say('log',name, 'connected'); this.handleAttrs(this.attributes, {originals: true}); if ( this.hasAttribute('lazy') ) { this.isLazy = true; if ( this.hasAttribute('super') ) { this.superLazy = true; loaded().then(() => sleep(400*Math.random()).then(() => this.print())); } else { if ( RANDOM_SLEEP_ON_FIRST_PRINT ) { sleep(160*Math.random()).then(() => this.print()); } else { this.print(); } } } else { this.print(); } } // private methods handleAttrs(attrs, {node, originals} = {}) { const stateHolder = {}; if ( ! node ) node = this; // we can optimize this method more, we only get attrs if originals == true // otherwise we just get and process the single 'state' attr // this is a lot more performant for( const {name,value} of attrs ) { if ( isUnset(value) ) continue; handleAttribute(name, value, {node, originals, stateHolder}); } self._states.push(stateHolder.state); return stateHolder.state; } printShadow(state) { return fetchMarkup(this.#name).then(markup => this.cookMarkup(markup, state)) .catch(err => DEBUG && say('warn!',err)) .finally(this.markLoaded); } }; class StateKey extends String { constructor (keyNumber) { if ( keyNumber == undefined ) super(`system-key:${systemKeys+=2}`); else super(`client-key:${keyNumber}`); } } install(); // API async function use(name) { if ( self.customElements.get(name) ) return; console.log('using', name); let component; await fetchScript(name) .then(script => { // if there's a script that extends base, evaluate it to be component const Base = BangBase(name); const Compose = `(function () { ${Base.toString()}; return ${script}; }())`; try { component = eval(Compose); } catch(e) { say('warn!',e, Compose, component) } }).catch(err