sprae 1.0.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/sprae.min.js CHANGED
@@ -1 +1 @@
1
- Symbol.observable||=Symbol("observable");var e=new FinalizationRegistry((e=>e.call?.())),t=(t,r,n,l,s,i)=>{return t&&(o=(t[Symbol.observable]?.()||t).subscribe?.(r,n,l),i=o&&(()=>o.unsubscribe?.())||t.set&&t.call?.(s,r)||(t.then?.((e=>(!s&&r(e),l?.())),n)||(async e=>{try{for await(e of t){if(s)return;r(e)}l?.()}catch(e){n?.(e)}})())&&(e=>s=1),e.register(t,i),i);var o};function r(e,r){r||={};let l=[],i=!1;const o=e=>{l.forEach((t=>t(e)))},a=e=>{let r={};for(let l in e){let c=e[l];(n=c=e[l])&&(n[Symbol.observable]||n[Symbol.asyncIterator]||n.call&&n.set||n.subscribe||n.then)?(r[l]=null,s.register(c,t(c,(e=>(r[l]=e,i&&o(r)))))):r[l]=c?.constructor===Object?a(c):c}var n;return r},c=a(r);i=!0;for(let t in n){const r=`[${t.replace(":","\\:")}]`,s=n[t],i=[...e.querySelectorAll(r)];let o;e.matches?.(r)&&i.unshift(e);for(let e of i)(o=s(e,c))&&l.push(o)}o(c),c[Symbol.iterator]=function*(){yield u,yield e=>o(Object.assign(c,e))};const u=new Proxy(c,{set:(e,t,r)=>(c[t]=r,o(c),1),deleteProperty:(e,t)=>(c[t]=void 0,o(c),1)});return u}var n={},l=(e,t)=>{const r=e.replace(":","∴");return n[e]=(n,l)=>{if(n.classList.contains(r))return;n.classList.add(r);let s=n.getAttribute(e);return n.removeAttribute(e),t(n,s,l)}},s=new FinalizationRegistry((e=>e?.call?.())),i={};function o(e){if(i[e])return i[e];let t=/^[\n\s]*if.*\(.*\)/.test(e)||/^(let|const)\s/.test(e)?`(() => { ${e} })()`:e;return i[e]=(()=>{try{return new Function(["scope"],`let result; with (scope) { result = ${t} }; return result;`)}catch(t){return a(t,e)}})()}function a(e,t){return Object.assign(e,{expression:t}),console.warn(`∴ ${e.message}\n\nundefined=${t?`"${t}"\n\n`:""}`,void 0),setTimeout((()=>{throw e}),0),Promise.resolve()}l(":with",((e,t,n)=>{let l=o(t),s=Object.create(n);Object.assign(s,l(n));let[i,a]=r(e,s);return e=>{let t=l(e);a(t)}})),l(":each",((e,t)=>{let n=function(e){let t=/,([^,\}\]]*)(?:,([^,\}\]]*))?$/,r=e.match(/([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/);if(!r)return;let n={};n.items=r[2].trim();let l=r[1].replace(/^\s*\(|\)\s*$/g,"").trim(),s=l.match(t);return s?(n.item=l.replace(t,"").trim(),n.index=s[1].trim()):n.item=l,n}(t);if(!n)return a(new Error,t);const l=o(n.items),s=new Text;e.replaceWith(s);let i=[];return t=>{i.forEach((e=>e.remove())),i=[];let o=l(t);"number"==typeof o&&(o=Array.from({length:o},((e,t)=>t+1))),o?.forEach(((l,o)=>{const a={...t};a[n.item]=l,n.index&&(a[n.index]=o);let c=e.cloneNode(!0);i.push(c),s.before(c),r(c,a)}))}})),l(":text",((e,t)=>{let r=o(t);return t=>{let n=r(t);e.textContent=null==n?"":n}})),l(":if",((e,t)=>{let r=e,n=[e],l=[o(t)],s=new Text;for(;r=e.nextElementSibling;){if(!(t=r.getAttribute(":else-if"))){r.hasAttribute(":else")&&(r.removeAttribute(":else"),r.classList.add("∴else"),r.remove(),n.push(r),l.push((()=>1)));break}r.removeAttribute(":else-if"),r.classList.add("∴else-if"),r.remove(),n.push(r),l.push(o(t))}return r=n[0],e=>{let t=l.findIndex((t=>t(e)));t>=0?r.replaceWith(r=n[t]):r.replaceWith(r=s)}}));var c=(e,t,r)=>{t.startsWith("on")&&(t=t.toLowerCase()),e[t]!==r&&(e[t]=r),!1===r||null==r?e.removeAttribute(t):"function"!=typeof r&&e.setAttribute(f(t),!0===r?"":"number"==typeof r||"string"==typeof r?r:"class"===t?(Array.isArray(r)?r:Object.entries(r).map((([e,t])=>t?e:""))).filter(Boolean).join(" "):"style"===t?Object.entries(r).map((([e,t])=>`${e}: ${t}`)).join(";"):"")},u=document.createElement("div"),f=e=>{u.dataset[e]="";let t=u.attributes[0].name.slice(5);return delete u.dataset[e],t};function b(e){l(":"+e,((t,r)=>{let n=o(r);return r=>{let l=n(r);c(t,e,l)}}))}b("id"),b("name"),b("for"),b("type"),b("hidden"),b("disabled"),b("href"),b("src"),b("style"),b("class"),l(":prop",((e,t)=>{let r=o(t);return t=>{let n=r(t);for(let t in n)c(e,t,n[t])}})),l(":data",((e,t)=>{let r=o(t);return t=>{let n=r(t);for(let t in n)e.dataset[t]=n[t]}})),l(":aria",((e,t)=>{let r=o(t);return t=>{let n=r(t);for(let t in n)c(e,"aria"+t[0].toUpperCase()+t.slice(1),n[t])}})),l(":value",((e,t)=>{let r,n=o(t),[l,s]=(e=>["checkbox"===e.type?()=>e.checked:()=>e.value,"text"===e.type||""===e.type?t=>e.value=null==t?"":t:"checkbox"===e.type?t=>(e.value=t?"on":"",c(e,"checked",t)):"select-one"===e.type?t=>([...e.options].map((e=>e.removeAttribute("selected"))),e.value=t,e.selectedOptions[0]?.setAttribute("selected","")):t=>e.value=t])(e),i=function(e){return d[e]?d[e]:d[e]=new Function(["scope","value"],`with (scope) { ${e} = value };`)}(t),a=t=>i(r,l(e));return e.addEventListener("input",a),e.addEventListener("change",a),t=>{let l=n(r=t);c(e,"value",l),s(l)}}));var d={};l(":on",((e,t)=>{let r=o(t),n={};return t=>{for(let t in n)e.removeEventListener(t,n[t]);n=r(t);for(let t in n)e.addEventListener(t,n[t])}}));var m=r;export{m as default,l as directive,a as exprError,o as parseExpr};
1
+ function t(){throw new Error("Cycle detected")}function e(){if(o>1)o--;else{for(var t,e=!1;void 0!==n;){var i=n;for(n=void 0,s++;void 0!==i;){var r=i.o;if(i.o=void 0,i.f&=-3,!(8&i.f)&&h(i))try{i.c()}catch(i){e||(t=i,e=!0)}i=r}}if(s=0,o--,e)throw t}}var r=void 0,n=void 0,o=0,s=0,f=0;function a(t){if(void 0!==r){var e=t.n;if(void 0===e||e.t!==r)return r.s=e={i:0,S:t,p:void 0,n:r.s,t:r,e:void 0,x:void 0,r:e},t.n=e,32&r.f&&t.S(e),e;if(-1===e.i)return e.i=0,void 0!==e.p&&(e.p.n=e.n,void 0!==e.n&&(e.n.p=e.p),e.p=void 0,e.n=r.s,r.s.p=e,r.s=e),e}}function u(t){this.v=t,this.i=0,this.n=void 0,this.t=void 0}function l(t){return new u(t)}function h(t){for(var 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 v(t){for(var e=t.s;void 0!==e;e=e.n){var i=e.S.n;void 0!==i&&(e.r=i),e.S.n=e,e.i=-1}}function c(t){for(var e=t.s,i=void 0;void 0!==e;){var r=e.n;-1===e.i?(e.S.U(e),e.n=void 0):(void 0!==i&&(i.p=e),e.p=void 0,e.n=i,i=e),e.S.n=e.r,void 0!==e.r&&(e.r=void 0),e=r}t.s=i}function d(t){u.call(this,void 0),this.x=t,this.s=void 0,this.g=f-1,this.f=4}function p(t){return new d(t)}function y(t){var i=t.u;if(t.u=void 0,"function"==typeof i){o++;var n=r;r=void 0;try{i()}catch(e){throw t.f&=-2,t.f|=8,b(t),e}finally{r=n,e()}}}function b(t){for(var e=t.s;void 0!==e;e=e.n)e.S.U(e);t.x=void 0,t.s=void 0,y(t)}function m(t){if(r!==this)throw new Error("Out-of-order effect");c(this),r=t,this.f&=-2,8&this.f&&b(this),e()}function g(t){this.x=t,this.u=void 0,this.s=void 0,this.o=void 0,this.f=32}function S(t){var e=new g(t);return e.c(),e.d.bind(e)}u.prototype.h=function(){return!0},u.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)},u.prototype.U=function(t){var 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)},u.prototype.subscribe=function(t){var e=this;return S((function(){var i=e.value,r=32&this.f;this.f&=-33;try{t(i)}finally{this.f|=r}}))},u.prototype.valueOf=function(){return this.value},u.prototype.toString=function(){return this.value+""},u.prototype.peek=function(){return this.v},Object.defineProperty(u.prototype,"value",{get:function(){var t=a(this);return void 0!==t&&(t.i=this.i),this.v},set:function(i){if(i!==this.v){s>100&&t(),this.v=i,this.i++,f++,o++;try{for(var r=this.t;void 0!==r;r=r.x)r.t.N()}finally{e()}}}}),(d.prototype=new u).h=function(){if(this.f&=-3,1&this.f)return!1;if(32==(36&this.f))return!0;if(this.f&=-5,this.g===f)return!0;if(this.g=f,this.f|=1,this.i>0&&!h(this))return this.f&=-2,!0;var t=r;try{v(this),r=this;var e=this.x();(16&this.f||this.v!==e||0===this.i)&&(this.v=e,this.f&=-17,this.i++)}catch(t){this.v=t,this.f|=16,this.i++}return r=t,c(this),this.f&=-2,!0},d.prototype.S=function(t){if(void 0===this.t){this.f|=36;for(var e=this.s;void 0!==e;e=e.n)e.S.S(e)}u.prototype.S.call(this,t)},d.prototype.U=function(t){if(u.prototype.U.call(this,t),void 0===this.t){this.f&=-33;for(var e=this.s;void 0!==e;e=e.n)e.S.U(e)}},d.prototype.N=function(){if(!(2&this.f)){this.f|=6;for(var t=this.t;void 0!==t;t=t.x)t.t.N()}},d.prototype.peek=function(){if(this.h()||t(),16&this.f)throw this.v;return this.v},Object.defineProperty(d.prototype,"value",{get:function(){1&this.f&&t();var e=a(this);if(this.h(),void 0!==e&&(e.i=this.i),16&this.f)throw this.v;return this.v}}),g.prototype.c=function(){var t=this.S();try{8&this.f||void 0===this.x||(this.u=this.x())}finally{t()}},g.prototype.S=function(){1&this.f&&t(),this.f|=1,this.f&=-9,y(this),v(this),o++;var e=r;return r=this,m.bind(this,e)},g.prototype.N=function(){2&this.f||(this.f|=2,this.o=n,n=this)},g.prototype.d=function(){this.f|=8,1&this.f||b(this)},Symbol.observable||=Symbol("observable");var x=new FinalizationRegistry((t=>t.call?.())),w=t=>t&&t.peek,O=Symbol("signal-struct");function j(t){if(t[O])return t;let e,i;if(!k(t))throw Error("Only array or object states are supported");e={},i={};for(let r in t)i[r]=A(e,r,t[r]);return Object.defineProperty(e,O,{configurable:!1,enumerable:!1,value:!0}),Object.seal(e),e}function A(t,e,i){let r,n=w(i)?i:k(i)?l(j(i)):l((r=(o=i)&&!!(o[Symbol.observable]||o[Symbol.asyncIterator]||o.call&&o.set||o.subscribe||o.then))?void 0:i);var o,s,f,a,u,h,v;return r&&(f=t=>n.value=t,(s=i)&&(v=(s[Symbol.observable]?.()||s).subscribe?.(f,a,undefined),h=v&&(()=>v.unsubscribe?.())||s.set&&s.call?.(u,f)||(s.then?.((t=>{!u&&f(t)}),a)||(async t=>{try{for await(t of s){if(u)return;f(t)}}catch(t){}})())&&(t=>u=1),x.register(s,h))),Object.defineProperty(t,e,{get:()=>n.value,set:w(i)?t=>n.value=t:k(i)?t=>t?Object.assign(n.value,t):n.value=t:t=>n.value=t,enumerable:!0,configurable:!1}),n}function k(t){return t&&t.constructor===Object}var N=new WeakMap;function $(t,e){if(!t.children)return;if(N.has(t))return N.get(t);e||={};const i=j(e),r=t=>{let e,n;if(t.attributes)for(let r=0;r<t.attributes.length;){let o=t.attributes[r];if(":"===o.name[0]){if(e=E[o.name]||E.default,t.removeAttribute(o.name),n=!1===e(t,o.value,i,o.name.slice(1)))break}else r++}if(!n)for(let e of t.children)r(e)};return r(t),N.set(t,i),i}var E={},U=(t,e,i)=>{e.startsWith("on")&&(e=e.toLowerCase()),t[e]!==i&&(t[e]=i),!1===i||null==i?t.removeAttribute(e):"function"!=typeof i&&t.setAttribute(C(e),!0===i?"":"number"==typeof i||"string"==typeof i?i:"class"===e?(Array.isArray(i)?i:Object.entries(i).map((([t,e])=>e?t:""))).filter(Boolean).join(" "):"style"===e?Object.entries(i).map((([t,e])=>`${t}: ${e}`)).join(";"):"")},W=document.createElement("div"),C=t=>{W.dataset[t]="";let e=W.attributes[0].name.slice(5);return delete W.dataset[t],e},B=(t,e,i,r=null)=>{let n,o,s,f=0,a=i.length,u=e.length,{remove:l,same:h,insert:v,replace:c}=B;for(;f<a&&f<u&&h(e[f],i[f]);)f++;for(;f<a&&f<u&&h(i[a-1],e[u-1]);)r=i[(--u,--a)];if(f==u)for(;f<a;)v(r,i[f++],t);else{for(n=e[f];f<a;)s=i[f++],o=n?n.nextSibling:r,h(n,s)?n=o:f<a&&h(i[f],o)?(c(n,s,t),n=o):v(n,s,t);for(;!h(n,r);)o=n.nextSibling,l(n,t),n=o}return i};B.same=(t,e)=>t==e,B.replace=(t,e,i)=>i.replaceChild(e,t),B.insert=(t,e,i)=>i.insertBefore(e,t),B.remove=(t,e)=>e.removeChild(t);var P=B,L={},M={},T={},F=t=>null===t?M:void 0===t?T:"number"==typeof t||t instanceof Number?L[t]||(L[t]=new Number(t)):"string"==typeof t||t instanceof String?L[t]||(L[t]=new String(t)):"boolean"==typeof t||t instanceof Boolean?L[t]||(L[t]=new Boolean(t)):t;E[":with"]=(t,i,r)=>{let n=R(i,"with",r);const s=p((()=>Object.assign({},r,n(r))));let f=$(t,s.value);S(((t=s.value)=>function(t){if(o>0)return t();o++;try{return t()}finally{e()}}((()=>Object.assign(f,t)))))},E[":if"]=(t,e,i)=>{let r=document.createTextNode(""),n=[R(e,":if",i)],o=[t],s=t;for(;(s=t.nextElementSibling)&&s.hasAttribute(":else");)s.removeAttribute(":else"),(e=s.getAttribute(":if"))?(s.removeAttribute(":if"),s.remove(),o.push(s),n.push(R(e,":else :if",i))):(s.remove(),o.push(s),n.push((()=>1)));t.replaceWith(s=r);let f=p((()=>n.findIndex((t=>t(i)))));return S(((t=f.value)=>o[t]!=s&&((s[I]||s).replaceWith(s=o[t]||r),$(s,i)))),!1};var I=Symbol(":each");E[":each"]=(t,e,r)=>{let n=function(t){let e=/,([^,\}\]]*)(?:,([^,\}\]]*))?$/,i=t.match(/([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/);if(!i)return;let r={};r.items=i[2].trim();let n=i[1].replace(/^\s*\(|\)\s*$/g,"").trim(),o=n.match(e);return o?(r.item=n.replace(e,"").trim(),r.index=o[1].trim()):r.item=n,r}(e);if(!n)return q(new Error,e);const o=R(n.items,":each",r),s=t[I]=document.createTextNode("");t.replaceWith(s);const f=p((()=>{let t=o(r);return"number"==typeof t?Array.from({length:t},((t,e)=>e+1)):t})),a=new WeakMap,u=new WeakMap;let l=[];return S(((e=f.value)=>{e||(e=[]);let o=[],h=[];for(let s of e){let e=F(s),f=u.get(e);if(f||(f=t.cloneNode(!0),u.set(e,f)),o.push(f),!a.has(e)){let t=Object.create(r);t[n.item]=s,n.index&&(t[n.index]=i),a.set(e,t)}h.push(a.get(e))}P(s.parentNode,l,o,s),l=o;for(let t=0;t<o.length;t++)$(o[t],h[t])})),!1},E.default=(t,e,i,r)=>{let n=R(e,":"+r,i);S((()=>{return e=n(i),U(t,r,e);var e}))},E[":aria"]=(t,e,i)=>{let r=R(e,":aria",i);S((()=>(e=>{for(let i in e)U(t,"aria"+i[0].toUpperCase()+i.slice(1),null==e[i]?null:e[i]+"")})(r(i))))},E[":data"]=(t,e,i)=>{let r=R(e,":data",i);const n=p((()=>r(i)));S(((e=n.value)=>{for(let i in e)t.dataset[i]=e[i]}))},E[":on"]=(t,e,i)=>{let r,n=R(e,":on",i),o=p((()=>n(i)));S(((e=o.value)=>{for(let e in r)t.removeEventListener(e,r[e]);r=e;for(let e in r)t.addEventListener(e,r[e])}))},E[":"]=(t,e,i)=>{let r=R(e,":",i);S((()=>(e=>{if(e)for(let i in e)U(t,i,e[i])})(r(i))))},E[":text"]=(t,e,i)=>{let r=R(e,":text",i);S((()=>{return e=r(i),void(t.textContent=null==e?"":e);var e}))},E[":value"]=(t,e,i)=>{let r=R(e,":in",i),[n,o]=(t=>["checkbox"===t.type?()=>t.checked:()=>t.value,"text"===t.type||""===t.type?e=>t.value=null==e?"":e:"checkbox"===t.type?e=>(t.value=e?"on":"",U(t,"checked",e)):"select-one"===t.type?e=>([...t.options].map((t=>t.removeAttribute("selected"))),t.value=e,t.selectedOptions[0]?.setAttribute("selected","")):e=>t.value=e])(t);S((()=>{return e=r(i),U(t,"value",e),void o(e);var e}))};var z={};function R(t,e,i){if(z[t])return z[t];let r,n=/^[\n\s]*if.*\(.*\)/.test(t)||/^(let|const)\s/.test(t)?`(() => { ${t} })()`:t;try{r=new Function(["scope"],`let result; with (scope) { result = ${n} }; return result;`)}catch(r){return q(r,t,e,i)}return z[t]=n=>{let o;try{o=r(n)}catch(r){return q(r,t,e,i)}return o}}function q(t,e,i,r){Object.assign(t,{expression:e}),console.warn(`∴sprae: ${t.message}\n\n${i}=${e?`"${e}"\n\n`:""}`,r),setTimeout((()=>{throw t}),0)}var D=$;export{D as default,E as directives};
package/src/core.js CHANGED
@@ -1,110 +1,38 @@
1
- import sube, { observable } from 'sube';
1
+ import signalStruct from 'signal-struct';
2
2
 
3
- let curEl, curDir;
4
3
  // sprae element: apply directives
5
- export default function sprae(el, initScope) {
6
- initScope ||= {};
7
-
8
- let updates=[], // all spray directive updators
9
- ready=false;
10
-
11
- const update = (values) => { updates.forEach(update => update(values)); };
12
-
13
- // hook up observables (deeply, to include item.text etc)
14
- // that's least evil compared to dlv/dset or proxies
15
- // returns dynamic values snapshot
16
- const rsube = (scope) => {
17
- let values = {}
18
- for (let k in scope) {
19
- let v = scope[k];
20
- if (observable(v = scope[k])) values[k] = null, registry.register(v, sube(v, v => (values[k] = v, ready && update(values))));
21
- // FIXME: add []
22
- else if (v?.constructor === Object) values[k] = rsube(v);
23
- else values[k] = v;
4
+ const memo = new WeakMap
5
+ export default function sprae(container, values) {
6
+ if (!container.children) return
7
+ if (memo.has(container)) return memo.get(container)
8
+
9
+ values ||= {};
10
+
11
+ const state = signalStruct(values);
12
+
13
+ // init directives on element
14
+ const init = (el) => {
15
+ let dir, stop
16
+ if (el.attributes) {
17
+ for (let i = 0; i < el.attributes.length;) {
18
+ let attr = el.attributes[i]
19
+ if (attr.name[0]===':') {
20
+ dir = directives[attr.name] || directives.default
21
+ el.removeAttribute(attr.name)
22
+ if (stop = (dir(el, attr.value, state, attr.name.slice(1))===false)) break
23
+ }
24
+ else i++
25
+ }
24
26
  }
25
- return values;
26
- };
27
- const values = rsube(initScope);
28
- ready = true;
29
-
30
- // prepare directives - need to be after subscribing to values to get init state here
31
- for (let name in directives) {
32
- // updates[dir] = directives[dir](el)
33
- const sel = `[${name.replace(':','\\:')}]`,
34
- initDirective = directives[name]
35
-
36
- // FIXME: possibly linear init of directives is better, who knows
37
- const els = [...el.querySelectorAll(sel)];
38
- if (el.matches?.(sel)) els.unshift(el);
39
-
40
- let update
41
- for (let el of els) if (update = initDirective(el, values)) updates.push(update);
42
- };
43
-
44
- update(values);
45
-
46
- // return update via destructuring of result to allow batch-update
47
- values[Symbol.iterator] = function*(){ yield proxy; yield (diff) => update(Object.assign(values, diff)); };
48
-
49
- const proxy = new Proxy(values, {
50
- set: (s, k, v) => (values[k]=v, update(values), 1),
51
- deleteProperty: (s, k) => (values[k]=undefined, update(values), 1)
52
- });
53
-
54
- return proxy
55
- }
56
-
57
- // dict of directives
58
- const directives = {}
59
-
60
- // register a directive
61
- export const directive = (name, initializer) => {
62
- const className = name.replace(':','∴')
63
-
64
- // create initializer of a directive on an element
65
- return directives[name] = (el, initValues) => {
66
- if (el.classList.contains(className)) return
67
- el.classList.add(className)
68
- let expr = el.getAttribute(name)
69
- el.removeAttribute(name)
70
- return initializer(el, expr, initValues);
27
+ if (!stop) for (let child of el.children) init(child)
71
28
  }
72
- }
73
-
74
- const registry = new FinalizationRegistry(unsub => unsub?.call?.())
75
-
76
- let evaluatorMemo = {}
77
-
78
- // borrowed from alpine: https://github.com/alpinejs/alpine/blob/main/packages/alpinejs/src/evaluator.js#L61
79
- // it seems to be more robust than subscript
80
- export function parseExpr(expression) {
81
- if (evaluatorMemo[expression]) return evaluatorMemo[expression]
29
+ init(container)
82
30
 
83
- // Some expressions that are useful in Alpine are not valid as the right side of an expression.
84
- // Here we'll detect if the expression isn't valid for an assignement and wrap it in a self-
85
- // calling function so that we don't throw an error AND a "return" statement can b e used.
86
- let rightSideSafeExpression = 0
87
- // Support expressions starting with "if" statements like: "if (...) doSomething()"
88
- || /^[\n\s]*if.*\(.*\)/.test(expression)
89
- // Support expressions starting with "let/const" like: "let foo = 'bar'"
90
- || /^(let|const)\s/.test(expression)
91
- ? `(() => { ${expression} })()`
92
- : expression
31
+ memo.set(container, state);
93
32
 
94
- const safeFunction = () => {
95
- try {
96
- return new Function(['scope'], `let result; with (scope) { result = ${rightSideSafeExpression} }; return result;`)
97
- } catch ( e ) {
98
- return exprError(e, expression)
99
- }
100
- }
101
-
102
- return evaluatorMemo[expression] = safeFunction()
33
+ return state;
103
34
  }
104
35
 
105
- export function exprError(error, expression) {
106
- Object.assign( error, { expression } )
107
- console.warn(`∴ ${error.message}\n\n${curDir}=${ expression ? `"${expression}"\n\n` : '' }`, curEl)
108
- setTimeout(() => { throw error }, 0)
109
- return Promise.resolve()
110
- }
36
+
37
+ // dict of directives
38
+ export const directives = {}
@@ -0,0 +1,235 @@
1
+ // directives & parsing
2
+ import sprae, { directives } from './core.js'
3
+ import { prop, input } from 'element-props'
4
+ // import { effect, computed, batch } from 'usignal'
5
+ import { effect, computed, batch } from '@preact/signals-core'
6
+ import swap from 'swapdom'
7
+ import p from 'primitive-pool'
8
+
9
+ directives[':with'] = (el, expr, rootState) => {
10
+ let evaluate = parseExpr(expr, 'with', rootState)
11
+
12
+ // Instead of extending signals (which is a bit hard since signal-struct internals is not uniform)
13
+ // we bind updating
14
+ const params = computed(() => Object.assign({}, rootState, evaluate(rootState)))
15
+ let state = sprae(el, params.value)
16
+ effect((values=params.value) => batch(()=>Object.assign(state, values)))
17
+ }
18
+
19
+ directives[':if'] = (el, expr, state) => {
20
+ let holder = document.createTextNode(''),
21
+ clauses = [parseExpr(expr, ':if', state)],
22
+ els = [el], cur = el
23
+
24
+ while (cur = el.nextElementSibling) {
25
+ if (cur.hasAttribute(':else')) {
26
+ cur.removeAttribute(':else');
27
+ if (expr = cur.getAttribute(':if')) {
28
+ cur.removeAttribute(':if'), cur.remove();
29
+ els.push(cur); clauses.push(parseExpr(expr, ':else :if', state));
30
+ }
31
+ else {
32
+ cur.remove(); els.push(cur); clauses.push(() => 1);
33
+ }
34
+ }
35
+ else break;
36
+ }
37
+
38
+ el.replaceWith(cur = holder)
39
+ let idx = computed(() => clauses.findIndex(f => f(state)))
40
+ // NOTE: it lazily initializes elements on insertion, it's safe to sprae multiple times
41
+ effect((i=idx.value) => (els[i] != cur && ((cur[_eachHolder]||cur).replaceWith(cur = els[i] || holder), sprae(cur, state))))
42
+
43
+ return false
44
+ }
45
+
46
+ const _eachHolder = Symbol(':each')
47
+ directives[':each'] = (tpl, expr, state) => {
48
+ let each = parseForExpression(expr);
49
+ if (!each) return exprError(new Error, expr);
50
+
51
+ const getItems = parseExpr(each.items, ':each', state);
52
+
53
+ // FIXME: make sure no memory leak here
54
+ // we need holder to be able :if replace it instead of tpl for combined case
55
+ const holder = tpl[_eachHolder] = document.createTextNode('')
56
+ tpl.replaceWith(holder)
57
+
58
+ const items = computed(()=>{
59
+ let list = getItems(state)
60
+ if (typeof list === 'number') return Array.from({length: list}, (_, i)=>i+1)
61
+ return list
62
+ })
63
+
64
+ // stores scope per data item
65
+ const scopes = new WeakMap()
66
+ // element per data item
67
+ const itemEls = new WeakMap()
68
+ let curEls = []
69
+ effect((list=items.value) => {
70
+ if (!list) list = []
71
+ // collect elements/scopes for items
72
+ let newEls = [], elScopes = []
73
+
74
+ for (let item of list) {
75
+ let key = p(item)
76
+ let el = itemEls.get(key)
77
+ if (!el) {
78
+ el = tpl.cloneNode(true)
79
+ itemEls.set(key, el)
80
+ }
81
+ newEls.push(el)
82
+
83
+ if (!scopes.has(key)) {
84
+ let scope = Object.create(state)
85
+ scope[each.item] = item
86
+ if (each.index) scope[each.index] = i;
87
+ scopes.set(key, scope)
88
+ }
89
+ elScopes.push(scopes.get(key))
90
+ }
91
+
92
+ // swap is really fast & tiny
93
+ swap(holder.parentNode, curEls, newEls, holder)
94
+ curEls = newEls
95
+
96
+ // init new elements
97
+ for (let i = 0; i < newEls.length; i++) {
98
+ sprae(newEls[i], elScopes[i])
99
+ }
100
+ })
101
+
102
+ return false
103
+ }
104
+
105
+ // This was taken AlpineJS, former VueJS 2.* core. Thanks Alpine & Vue!
106
+ function parseForExpression(expression) {
107
+ let forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/
108
+ let stripParensRE = /^\s*\(|\)\s*$/g
109
+ let forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/
110
+ let inMatch = expression.match(forAliasRE)
111
+
112
+ if (!inMatch) return
113
+
114
+ let res = {}
115
+ res.items = inMatch[2].trim()
116
+ let item = inMatch[1].replace(stripParensRE, '').trim()
117
+ let iteratorMatch = item.match(forIteratorRE)
118
+
119
+ if (iteratorMatch) {
120
+ res.item = item.replace(forIteratorRE, '').trim()
121
+ res.index = iteratorMatch[1].trim()
122
+ } else {
123
+ res.item = item
124
+ }
125
+
126
+ return res
127
+ }
128
+
129
+
130
+ // common-setter directives
131
+ directives['default'] = (el, expr, state, name) => {
132
+ let evaluate = parseExpr(expr, ':'+name, state)
133
+ const update = value => prop(el, name, value)
134
+ effect(() => update(evaluate(state)))
135
+ }
136
+
137
+ directives[':aria'] = (el, expr, state) => {
138
+ let evaluate = parseExpr(expr, ':aria', state)
139
+ const update = (value) => {
140
+ for (let key in value) prop(el, 'aria'+key[0].toUpperCase()+key.slice(1), value[key] == null ? null : value[key] + '');
141
+ }
142
+ effect(() => update(evaluate(state)))
143
+ }
144
+
145
+ directives[':data'] = (el, expr, state) => {
146
+ let evaluate = parseExpr(expr, ':data', state)
147
+ const value = computed(() => evaluate(state))
148
+ effect((v=value.value) => {
149
+ for (let key in v) el.dataset[key] = v[key];
150
+ })
151
+ }
152
+
153
+ directives[':on'] = (el, expr, state) => {
154
+ let evaluate = parseExpr(expr, ':on', state)
155
+ let listeners = computed(() => evaluate(state))
156
+ let prevListeners
157
+ effect((values=listeners.value) => {
158
+ for (let evt in prevListeners) el.removeEventListener(evt, prevListeners[evt]);
159
+ prevListeners = values;
160
+ for (let evt in prevListeners) el.addEventListener(evt, prevListeners[evt]);
161
+ })
162
+ }
163
+
164
+ directives[':'] = (el, expr, state) => {
165
+ let evaluate = parseExpr(expr, ':', state)
166
+ const update = (value) => {
167
+ if (!value) return
168
+ for (let key in value) prop(el, key, value[key]);
169
+ }
170
+ effect(()=>update(evaluate(state)))
171
+ }
172
+
173
+ directives[':text'] = (el, expr, state) => {
174
+ let evaluate = parseExpr(expr, ':text', state)
175
+
176
+ const update = (value) => {
177
+ el.textContent = value == null ? '' : value;
178
+ }
179
+
180
+ effect(()=>update(evaluate(state)))
181
+ }
182
+
183
+ // connect expr to element value
184
+ directives[':value'] = (el, expr, state) => {
185
+ let evaluate = parseExpr(expr, ':in', state)
186
+
187
+ let [get, set] = input(el);
188
+
189
+ const update = (value) => {
190
+ prop(el, 'value', value)
191
+ set(value);
192
+ }
193
+ effect(()=>update(evaluate(state)))
194
+ }
195
+
196
+ let evaluatorMemo = {}
197
+
198
+ // borrowed from alpine: https://github.com/alpinejs/alpine/blob/main/packages/alpinejs/src/evaluator.js#L61
199
+ // it seems to be more robust than subscript
200
+ function parseExpr(expression, dir, scope) {
201
+ if (evaluatorMemo[expression]) return evaluatorMemo[expression]
202
+
203
+ // Some expressions that are useful in Alpine are not valid as the right side of an expression.
204
+ // Here we'll detect if the expression isn't valid for an assignement and wrap it in a self-
205
+ // calling function so that we don't throw an error AND a "return" statement can b e used.
206
+ let rightSideSafeExpression = 0
207
+ // Support expressions starting with "if" statements like: "if (...) doSomething()"
208
+ || /^[\n\s]*if.*\(.*\)/.test(expression)
209
+ // Support expressions starting with "let/const" like: "let foo = 'bar'"
210
+ || /^(let|const)\s/.test(expression)
211
+ ? `(() => { ${expression} })()`
212
+ : expression;
213
+
214
+ // guard static-time eval errors
215
+ let evaluate
216
+ try {
217
+ evaluate = new Function(['scope'], `let result; with (scope) { result = ${rightSideSafeExpression} }; return result;`)
218
+ } catch ( e ) {
219
+ return exprError(e, expression, dir, scope)
220
+ }
221
+
222
+ // guard runtime eval errors
223
+ return evaluatorMemo[expression] = (state) => {
224
+ let result
225
+ try { result = evaluate(state) }
226
+ catch (e) { return exprError(e, expression, dir, scope) }
227
+ return result
228
+ }
229
+ }
230
+
231
+ export function exprError(error, expression, dir, scope) {
232
+ Object.assign( error, { expression } )
233
+ console.warn(`∴sprae: ${error.message}\n\n${dir}=${ expression ? `"${expression}"\n\n` : '' }`, scope)
234
+ setTimeout(() => { throw error }, 0)
235
+ }
package/src/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import sprae from './core.js';
2
- import './directives/index.js';
2
+ import './directives.js';
3
3
 
4
4
  export default sprae;
5
5
  export * from './core.js';
package/test/index.html CHANGED
@@ -12,11 +12,14 @@
12
12
  "value-ref" : "../node_modules/value-ref/value-ref.js",
13
13
  "hyperf" : "../node_modules/hyperf/hyperf.js",
14
14
  "sube" : "../node_modules/sube/sube.js",
15
+ "signal-struct" : "../node_modules/signal-struct/signal-struct.js",
15
16
  "element-props" : "../node_modules/element-props/element-props.js",
17
+ "swapdom" : "../node_modules/swapdom/swap-inflate.js",
18
+ "primitive-pool" : "../node_modules/primitive-pool/index.js",
16
19
  "preact": "../node_modules/preact/dist/preact.mjs",
17
20
  "preact/hooks": "../node_modules/preact/hooks/dist/hooks.mjs",
18
- "@preact/signals": "../node_modules/@preact/signals/dist/signals.mjs",
19
- "@preact/signals-core": "../node_modules/@preact/signals-core/dist/signals-core.mjs"
21
+ "@preact/signals-core": "../node_modules/@preact/signals-core/dist/signals-core.mjs",
22
+ "usignal": "../node_modules/usignal/esm/sync.js"
20
23
  }
21
24
  }
22
25
  </script>