// The global list of "references", as captured by the proxy in stateful let __reference_stack = []; // We add some extra properties into various objects throughout, better to use symbols and not interfere let ALICEJS_REFERENCES_MAPPING = Symbol(); let ALICEJS_REFERENCES_MARKER = Symbol(); let ALICEJS_STATEFUL_LISTENERS = Symbol(); // Say you have some code like //// let state = stateful({ //// a: 1 //// }) //// let elm =

{window.use(state.a)}

// // According to the standard, the order of events is as follows: // - the getter for window.use gets called, setting __reference_stack to an empty list // - the proxy for state.a is triggered, pushing { state, "a", Proxy(state) } onto __reference_stack // - the function that the getter returns is called, popping everything off the stack // - the JSX factory h() is now passed the *reference* of state.a, not the value Object.defineProperty(window, "use", { get: () => { __reference_stack = []; return (_sink, mapping) => { let references = __reference_stack; __reference_stack = []; references[ALICEJS_REFERENCES_MARKER] = true; if (mapping) references[ALICEJS_REFERENCES_MAPPING] = mapping; return references; }; } }); Object.assign(window, { h, html, stateful, handle, useValue, css, styled: { new: css } }); // This wraps the target in a proxy, doing 2 things: // - whenever a property is accessed, update the reference stack // - whenever a property is set, notify the subscribed listeners // This is what makes our "pass-by-reference" magic work export function stateful(target) { target[ALICEJS_STATEFUL_LISTENERS] = []; const proxy = new Proxy(target, { get(target, property, proxy) { __reference_stack.push({ target, property, proxy }); return Reflect.get(target, property, proxy); }, set(target, property, val) { for (const listener of target[ALICEJS_STATEFUL_LISTENERS]) { listener(target, property, val); } return Reflect.set(target, property, val); }, }); return proxy; } function isAJSReferences(arr) { return arr instanceof Array && ALICEJS_REFERENCES_MARKER in arr } // This lets you subscribe to a stateful object export function handle(references, callback) { if (!isAJSReferences(references)) throw new Error("Not an AliceJS reference set!"); if (ALICEJS_REFERENCES_MAPPING in references) { const mapping = references[ALICEJS_REFERENCES_MAPPING]; const used_props = []; const used_targets = []; const values = new Map(); const pairs = []; const partial_update = (target, prop, val) => { if (used_props.includes(prop) && used_targets.includes(target)) { values.get(target)[prop] = val; } }; const full_update = () => { const flattened_values = pairs.map( (pair) => values.get(pair[0])[pair[1]], ); const value = mapping(...flattened_values.reverse()); callback(value); }; for (const p of references) { const target = p.target; const prop = p.property; used_props.push(prop); used_targets.push(target); pairs.push([target, prop]); if (!values.has(target)) { values.set(target, {}); } partial_update(target, prop, target[prop]); target[ALICEJS_STATEFUL_LISTENERS].push((t, p, v) => { partial_update(t, p, v); full_update(); }); } full_update(); } else { const reference = references[references.length - 1]; const subscription = (target, prop, val) => { if (prop === reference.property && target === reference.target) { callback(val); } }; reference.target[ALICEJS_STATEFUL_LISTENERS].push(subscription); subscription(reference.target, reference.property, reference.target[reference.property]); } } export function useValue(references) { let reference = references[references.length - 1]; return reference.proxy[reference.property]; } // Actual JSX factory. Responsible for creating the HTML elements and all of the *reactive* syntactic sugar export function h(type, props, ...children) { if (typeof type === "function") { let newthis = stateful(Object.create(type.prototype)); for (const name in props) { const references = props[name]; if (isAJSReferences(references) && name.startsWith("bind:")) { let reference = references[references.length - 1]; const propname = name.substring(5); if (propname == "this") { reference.proxy[reference.property] = newthis; } else { // component two way data binding!! (exact same behavior as svelte:bind) let isRecursive = false; handle(references, value => { if (isRecursive) { isRecursive = false; return; } isRecursive = true; newthis[propname] = value }); handle(use(newthis[propname]), value => { if (isRecursive) { isRecursive = false; return; } isRecursive = true; reference.proxy[reference.property] = value; }); } delete props[name]; } } Object.assign(newthis, props); let slot = []; for (const child of children) { JSXAddChild(child, slot.push.bind(slot)); } let elm = type.apply(newthis, [slot]); elm.$ = newthis; newthis.root = elm; return elm; } const elm = document.createElement(type); for (const child of children) { JSXAddChild(child, elm.appendChild.bind(elm)); } if (!props) return elm; function useProp(name, callback) { if (!(name in props)) return; let prop = props[name]; callback(prop); delete props[name]; } // insert an element at the start useProp("before", callback => { JSXAddChild(callback()); }) // if/then/else syntax useProp("if", condition => { let thenblock = props["then"]; let elseblock = props["else"]; if (isAJSReferences(condition)) { if (thenblock) elm.appendChild(thenblock); if (elseblock) elm.appendChild(elseblock); handle(condition, val => { if (thenblock) { if (val) { thenblock.style.display = ""; if (elseblock) elseblock.style.display = "none"; } else { thenblock.style.display = "none"; if (elseblock) elseblock.style.display = ""; } } else { if (val) { elm.style.display = ""; } else { elm.style.display = "none"; } } }); } else { if (thenblock) { if (condition) { elm.appendChild(thenblock); } else if (elseblock) { elm.appendChild(elseblock); } } else { if (condition) { elm.appendChild(thenblock); } else if (elseblock) { elm.appendChild(elseblock); } else { elm.style.display = "none"; return document.createTextNode(""); } } } delete props["then"]; delete props["else"]; }); if ("for" in props && "do" in props) { const predicate = props["for"]; const closure = props["do"]; if (isAJSReferences(predicate)) { const __elms = []; let lastpredicate = []; handle(predicate, val => { if ( Object.keys(val).length && Object.keys(val).length == lastpredicate.length ) { let i = 0; for (const index in val) { if ( deepEqual(val[index], lastpredicate[index]) ) { continue; } const part = closure(val[index], index, val); elm.replaceChild(part, __elms[i]); __elms[i] = part; i += 1; } lastpredicate = Object.keys( JSON.parse(JSON.stringify(val)), ); } else { for (const part of __elms) { part.remove(); } for (const index in val) { const value = val[index]; const part = closure(value, index, val); if (part instanceof HTMLElement) { __elms.push(part); elm.appendChild(part); } } lastpredicate = []; } }); } else { for (const index in predicate) { const value = predicate[index]; const part = closure(value, index, predicate); if (part instanceof Node) elm.appendChild(part); } } delete props["for"]; delete props["do"]; } // insert an element at the end useProp("after", callback => { JSXAddChild(callback()); }) for (const name in props) { const references = props[name]; if (isAJSReferences(references) && name.startsWith("bind:")) { let reference = references[references.length - 1]; const propname = name.substring(5); if (propname == "this") { reference.proxy[reference.property] = elm; } else if (propname == "value") { handle(references, value => elm.value = value); elm.addEventListener("change", () => { reference.proxy[reference.property] = elm.value; }) } else if (propname == "checked") { handle(references, value => elm.checked = value); elm.addEventListener("click", () => { reference.proxy[reference.property] = elm.checked; }) } delete props[name]; } } // apply the non-reactive properties for (const name in props) { const prop = props[name]; if (isAJSReferences(prop)) { handle(prop, (val) => { JSXAddAttributes(elm, name, val); }); } else { JSXAddAttributes(elm, name, prop); } } useProp("css", classname => { elm.classList.add(classname); elm.classList.add("self"); }); return elm; } // glue for nested children function JSXAddChild(child, cb) { if (isAJSReferences(child)) { let appended = []; handle(child, (val) => { if (appended.length > 1) { // this is why we don't encourage arrays (jank) appended.forEach(n => n.remove()); appended = JSXAddChild(val, cb); } else if (appended.length > 0) { let old = appended[0]; appended = JSXAddChild(val, cb); if (appended[0]) { old.replaceWith(appended[0]) } else { old.remove(); } } else { appended = JSXAddChild(val, cb); } }); } else if (child instanceof Node) { cb(child); return [child]; } else if (child instanceof Array) { let elms = []; for (const childchild of child) { elms = elms.concat(JSXAddChild(childchild, cb)); } return elms; } else { let node = document.createTextNode(child); cb(node); return [node]; } } // Where properties are assigned to elements, and where the *non-reactive* syntax sugar goes function JSXAddAttributes(elm, name, prop) { if (name === "class") { elm.className = prop; return; } if (typeof prop === "function" && name === "mount") { prop(elm); return; } if (typeof prop === "function" && name.startsWith("on:")) { const names = name.substring(3); for (const name of names.split("$")) { elm.addEventListener(name, (...args) => { window.$el = elm; prop(...args); }); } return; } if (typeof prop === "function" && name.startsWith("observe")) { const observerclass = window[`${name.substring(8)}Observer`]; if (!observerclass) { console.error(`Observer ${name} does not exist`); return; } const observer = new observerclass(entries => { for (const entry of entries) { window.$el = elm; prop(entry); } }); observer.observe(elm); return; } elm.setAttribute(name, prop); } function parse_css(uid, css) { const virtualDoc = document.implementation.createHTMLDocument(""); const virtualStyleElement = document.createElement("style"); virtualDoc.body.appendChild(virtualStyleElement); let cssParsed = ""; virtualStyleElement.textContent = css; //@ts-ignore for (const rule of virtualStyleElement.sheet.cssRules) { rule.selectorText = rule.selectorText.includes("self") ? `.${uid}.self${rule.selectorText.replace("self", "")}` : `.${uid} ${rule.selectorText}`; cssParsed += `${rule.cssText}\n`; } return cssParsed; } export function css(strings, ...values) { const uid = `alicecss-${Array(16) .fill(0) .map(() => { return Math.floor(Math.random() * 16).toString(16); }) .join("")}`; const styleElement = document.createElement("style"); document.head.appendChild(styleElement); const flattened_template = []; for (const i in strings) { flattened_template.push(strings[i]); if (values[i]) { const prop = values[i]; if (isAJSReferences(prop)) { const current_i = flattened_template.length; let oldparsed; handle(prop, (val) => { flattened_template[current_i] = String(val); let parsed = flattened_template.join(""); if (parsed != oldparsed) styleElement.textContent = parse_css( uid, parsed, ); oldparsed = parsed; }); } else { flattened_template.push(String(prop)); } } } styleElement.textContent = parse_css( uid, flattened_template.join(""), ); return uid; } export function html(strings, ...values) { let flattened = ""; let markers = {}; for (const i in strings) { let string = strings[i]; let value = values[i]; flattened += string; if (i < values.length) { let dupe = Object.values(markers).findIndex(v => v == value); if (dupe !== -1) { flattened += Object.keys(markers)[dupe]; } else { let marker = "m" + Array(16).fill(0).map(() => Math.floor(Math.random() * 16).toString(16)).join(""); markers[marker] = value; flattened += marker; } } } let dom = new DOMParser().parseFromString(flattened, "text/html"); if (dom.body.children.length !== 1) throw "html builder needs exactly one child"; function wraph(elm) { let nodename = elm.nodeName.toLowerCase(); if (nodename === "#text") return elm.textContent; if (nodename in markers) nodename = markers[nodename]; let children = [...elm.childNodes].map(wraph); for (let i = 0; i < children.length; i++) { let text = children[i]; if (typeof text !== "string") continue; for (const [marker, value] of Object.entries(markers)) { if (!text) break; if (!text.includes(marker)) continue; let before; [before, text] = text.split(marker); children = [ ...children.slice(0, i), before, value, text, ...children.slice(i + 1) ]; i += 2; } } let attributes = {}; for (const attr of [...elm.attributes]) { let val = attr.nodeValue; if (val in markers) val = markers[val]; attributes[attr.name] = val; } return h(nodename, attributes, children); } return wraph(dom.body.children[0]); } function deepEqual(object1, object2) { const keys1 = Object.keys(object1); const keys2 = Object.keys(object2); if (keys1.length !== keys2.length) { return false; } for (const key of keys1) { const val1 = object1[key]; const val2 = object2[key]; const areObjects = isObject(val1) && isObject(val2); if ( (areObjects && !deepEqual(val1, val2)) || (!areObjects && val1 !== val2) ) { return false; } } return true; } function isObject(object) { return object != null && typeof object === "object"; }