@neocomp/core 0.1.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.
@@ -0,0 +1,23 @@
1
+ {
2
+ "src/enable_chunk_parsing.ts": {
3
+ "file": "enable_chunk_parsing.js",
4
+ "name": "enable_chunk_parsing",
5
+ "src": "src/enable_chunk_parsing.ts",
6
+ "isEntry": true
7
+ },
8
+ "src/html-parser.ts": {
9
+ "file": "chunks/html-parser-DTAS5OlA.js",
10
+ "name": "html-parser",
11
+ "src": "src/html-parser.ts",
12
+ "isDynamicEntry": true
13
+ },
14
+ "src/index.ts": {
15
+ "file": "index.js",
16
+ "name": "index",
17
+ "src": "src/index.ts",
18
+ "isEntry": true,
19
+ "dynamicImports": [
20
+ "src/html-parser.ts"
21
+ ]
22
+ }
23
+ }
@@ -0,0 +1 @@
1
+ const g=/^[^<>'"`=/\s]+/,w=new Set(["area","base","br","col","embed","hr","img","input","link","meta","param","source","track","wbr"]);function c(l,n){return n+l.slice(n).match(/^\s*/)[0].length}function f(l,n){throw l==null?new SyntaxError(`unexpected end of input, expected "${n}"`):new SyntaxError(`unexpected token "${l}", expected "${n}"`)}function u(l,n,r){let o=l.slice(n).match(g)?.[0];return o||f(l[n],r),[o,n+o.length]}function x(l){let n=[{tag:"html",attrs:[],children:[]}],r="in_content";for(let[o,t]of l.entries()){let e=0;e:for(;t.length>e;){let i=n.at(-1);if(r==="in_do")e=c(t,e),t[e]==="/"&&(e+=1),t[e]!==">"&&f(t[e],">"),e+=1,r="in_content";else if(r==="in_attr_quoted")t[e]!=='"'&&t[e]!=="'"&&f(t[e],`'"'`),e+=1,r="in_attr";else if(r==="in_attr"){for(e=c(t,e);e<t.length&&t[e]!==">"&&t[e]!=="/";){let h,s;if([h,e]=u(t,e,"attribute name"),e=c(t,e),t[e]==="="){if(e+=1,e=c(t,e),e===t.length){i.attrs.push({attr:h,value:o});break e}if((t[e]==='"'||t[e]==="'")&&t.length===e+1){i.attrs.push({attr:h,value:o}),r="in_attr_quoted";break e}if(t[e]==='"'||t[e]==="'"){let d=t[e],p=t.indexOf(d,e+1);if(p===-1)throw new SyntaxError("unended string");s=t.slice(e+1,p),e=p+1}else[s,e]=u(t,e,"attribute value");let a=e;if(e=c(t,e),a===e&&e<t.length&&t[e]!==">"&&t[e]!=="/")throw new SyntaxError("expected whitespace")}else s="";i.attrs.push({attr:h,value:s})}t[e]==="/"?(e+=1,e=c(t,e),n.pop()):w.has(i.tag.toLowerCase())&&n.pop(),t[e]!==">"&&f(t[e],'">"'),e+=1,r="in_content"}else{let h=t.indexOf("<",e),s=t.slice(e,h===-1?t.length:h);if(s.length>0&&(typeof i.children.at(-1)=="string"?i.children[i.children.length-1]=i.children.at(-1)+s:i.children.push(s)),h===-1)break;if(e=h+1,t.length===e)i.children.push({type:"do",arg:o}),r="in_do";else if(t.slice(e,e+3)==="!--"){let a=t.indexOf("-->",e);if(a===-1)throw new SyntaxError("unended comment");e=a+3}else if(t[e]==="/"){e+=1;let a;if([a,e]=u(t,e,"tag"),n.length===1)throw new SyntaxError(`unexpected end tag "${a}" at root`);if(a!==i.tag)throw new SyntaxError(`expected end tag "${i.tag}" but got "${a}"`);e=c(t,e),t[e]!==">"&&f(t[e],'">"'),e+=1,n.pop()}else{let a;[a,e]=u(t,e,"tag");let d={tag:a,attrs:[],children:[]};i.children.push(d),n.push(d),r="in_attr"}}}o!==l.length-1&&r==="in_content"&&n.at(-1).children.push(o)}if(r!=="in_content")throw new SyntaxError('unexpected end of input, expected ">"');if(n.length!==1)throw new SyntaxError("end of input with unclosed tags");return n[0]}export{x as parse};
@@ -0,0 +1 @@
1
+ globalThis.__neocomp_enable_chunk_parsing=!0;
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ const $="modulepreload",B=function(i){return"/"+i},S={},I=function(e,t,s){let n=Promise.resolve();if(t&&t.length>0){let a=function(h){return Promise.all(h.map(_=>Promise.resolve(_).then(d=>({status:"fulfilled",value:d}),d=>({status:"rejected",reason:d}))))};document.getElementsByTagName("link");const l=document.querySelector("meta[property=csp-nonce]"),o=l?.nonce||l?.getAttribute("nonce");n=a(t.map(h=>{if(h=B(h),h in S)return;S[h]=!0;const _=h.endsWith(".css"),d=_?'[rel="stylesheet"]':"";if(document.querySelector(`link[href="${h}"]${d}`))return;const c=document.createElement("link");if(c.rel=_?"stylesheet":$,_||(c.as="script"),c.crossOrigin="",c.href=h,o&&c.setAttribute("nonce",o),document.head.appendChild(c),_)return new Promise((m,g)=>{c.addEventListener("load",m),c.addEventListener("error",()=>g(new Error(`Unable to preload CSS for ${h}`)))})}))}function r(l){const o=new Event("vite:preloadError",{cancelable:!0});if(o.payload=l,window.dispatchEvent(o),!o.defaultPrevented)throw l}return n.then(l=>{for(const o of l||[])o.status==="rejected"&&r(o.reason);return e().catch(r)})};class U{#e=new Map;#h=0;#s=new Map;#o=[];#c=0;#i=!1;#r=new Set;#l=new Map;#f=0;#n=new Map;#t=null;new_slab(){let e=this.#c;return this.#s.set(e,{props:[],effects:[],cleaners:[]}),this.#c+=1,e}has_slab(e){return this.#s.has(e)&&!this.#o.includes(e)}remove_slab(e){if(!this.#s.has(e))throw new Error(`slab ${e} does not exist`);this.#i?this.#o.push(e):this.#u(e)}#u(e){let t=this.#s.get(e);for(let s of t.cleaners)s();for(let s of t.props)this.#e.delete(s),this.#n.delete(s);for(let s of t.effects){for(let n of this.#l.get(s).read){let r=this.#n.get(n);r&&r.splice(r.indexOf(s),1)}this.#l.delete(s)}this.#s.delete(e)}add_cleaner(e,t){this.#s.get(e)?.cleaners.push(t)}prop(e,t=void 0){let s=this.#h;return this.#e.set(s,e),this.#h+=1,t!=null&&this.#s.get(t)?.props.push(s),s}has(e){return this.#e.has(e)}get(e){return this.#p(e),this.#e.get(e)}peek(e){return this.#e.get(e)}set(e,t){this.#d(e),this.#_(e),this.#e.set(e,t)}update(e,t){this.#d(e),this.#_(e),this.#e.set(e,t(this.#e.get(e)))}signal(e,t=void 0){return new b(this,this.prop(e,t))}signal_for(e){return new b(this,e)}effect(e,t=void 0){this.start_track(),e();let{read:s,write:n}=this.end_track();this.#a(e,s,n,t)}effect_manual(e,t,s,n=void 0,r=!0){r&&s(),this.#a(s,e,t,n)}#a(e,t,s,n=void 0){let r=this.#f;this.#l.set(r,{fun:e,read:t,write:s}),this.#f+=1,n!=null&&this.#s.get(n)?.effects.push(r);for(let l of t)this.#n.has(l)?this.#n.get(l)?.push(r):this.#n.set(l,[r]);return r}computed(e,t=void 0){this.start_track();let s=e(),{read:n,write:r}=this.end_track();if(r.length!=0)throw new Error("computed properties cannot set other properties");let l=this.prop(s,t);return this.#a(()=>this.set(l,e()),n,[l],t),new y(this,l)}get tracking(){return this.#t!=null}start_track(){if(this.#t!=null)throw new Error("already tracking");this.#t={read:new Set,write:new Set}}end_track(){if(this.#t==null)throw new Error("not tracking");let e=this.#t;return this.#t=null,{read:Array.from(e.read),write:Array.from(e.write)}}#p(e){this.#t!=null&&this.#t.read.add(e)}#d(e){this.#t!=null&&this.#t.write.add(e)}get updating(){return this.#i}#_(e){this.#i||this.#r.add(e)}force_update(e){this.#r.add(e)}flush_updates(){if(this.#i)return;for(this.#i=!0;this.#r.size>0;){let t=[],s=new Set,n=[];for(let r of this.#r)e(this,r,t,s,n);this.#r.clear();for(let r=t.length-1;r>=0;r-=1)try{this.#l.get(t[r]).fun()}catch(l){console.error(l)}}this.#i=!1;for(let t of this.#o)this.#s.has(t)&&this.#u(t);this.#o=[];function e(t,s,n,r,l){let o=t.#n.get(s);if(o!=null)for(let a of o){if(l.includes(a))throw new Error("detected circular dependency in an update");if(!r.has(a)){l.push(a);for(let h of t.#l.get(a).write)e(t,h,n,r,l);l.pop(),n.push(a),r.add(a)}}}}}class T{id;store;constructor(e,t){this.id=t,this.store=e}}class b extends T{get value(){return this.store.get(this.id)}set value(e){this.store.set(this.id,e)}peek(){return this.store.peek(this.id)}update(e){this.store.update(this.id,e)}as_ro(){return new y(this.store,this.id)}}class y extends T{get value(){return this.store.get(this.id)}peek(){return this.store.peek(this.id)}}class q{ctx=void 0;slab=void 0;init(e,t=void 0){this.ctx=e,this.slab=t}get store(){return this.ctx.store}signal(e){return this.store.signal(e,this.slab)}effect(e){return this.store.effect(e,this.slab)}computed(e){return this.store.computed(e,this.slab)}}const V=globalThis.__neocomp_enable_chunk_parsing?(await I(async()=>{const{parse:i}=await import("./chunks/html-parser-DTAS5OlA.js");return{parse:i}},[])).parse:()=>{throw new Error("chunk parsing not enabled")},x=new Map,A="class:",P="style:",C="prop:",M="on:";function E(i,e,t){e.startsWith(A)?i.classList.toggle(e.slice(A.length),t):e.startsWith(P)?i.style.setProperty(e.slice(P.length),t):e.startsWith(C)?i[e.slice(C.length)]=t:typeof t=="boolean"?i.toggleAttribute(e,t):t==null?i.removeAttribute(e):i.setAttribute(e,String(t))}function R(i){return i instanceof Node?i:i==null?document.createTextNode(""):document.createTextNode(String(i))}function L(i,e,t){let s=R(t());e.appendChild(s),s instanceof Text?i.effect(()=>{let n=t();n==null?s.nodeValue="":s.nodeValue=String(n)}):i.effect(()=>{let n=t();e.replaceChild(n,s),s=n})}function W(i,e,t,s){if(typeof t?.tag=="string"){let n=z(i,t,s);e.append(n)}else if(t?.type==="do")i.__el_stack.push(e),s[t.arg](i,e),i.__el_stack.pop();else{let n=typeof t=="number"?s[t]:t;n instanceof k?(e.append(n.base_el),n instanceof j&&i.slab!==void 0&&i.store.add_cleaner(i.slab,()=>n.remove())):n instanceof b||n instanceof y?L(i,e,()=>n.value):typeof n=="function"?L(i,e,n):e.append(R(n))}}function z(i,e,t){let s=document.createElement(e.tag);for(let{attr:n,value:r}of e.attrs){let l=typeof r=="string"?r:t[r];n.startsWith(M)?s.addEventListener(n.slice(M.length),o=>{l(o),i.store.flush_updates()}):l instanceof b||l instanceof y?i.effect(()=>E(s,n,l.value)):typeof l=="function"?i.effect(()=>E(s,n,l())):E(s,n,l)}for(let n of e.children)W(i,s,n,t);return s}class k extends q{base_el;__el_stack;constructor(e,t,s=void 0){super(),super.init(e,s),this.base_el=t,this.__el_stack=[t],this.html=this.html.bind(this),this.html.__add=this.__add.bind(this),this.signal=this.signal.bind(this),this.effect=this.effect.bind(this),this.computed=this.computed.bind(this)}get cur_el(){return this.__el_stack.at(-1)}html(e,...t){let s=e;if(x.has(s))return this.__add(x.get(s),t);let n=V(s);x.set(s,n),this.__add(n,t)}__add(e,t){for(let s of e.children)W(this,this.cur_el,s,t)}}class j extends k{remove(){this.base_el.remove(),this.slab!=null&&this.store.remove_slab(this.slab)}}function H(i){return(e,t)=>{i==!1?t.style.display="none":i instanceof b||i instanceof y?e.effect(()=>t.style.display=i.value?"":"none"):typeof i=="function"&&e.effect(()=>t.style.display=i()?"":"none")}}function J(i,e,t,s){return n=>O(n,i,e,!1,t,(r,l,o)=>s(r,l))}function K(i,e,t,s){return n=>O(n,i,e,!0,t,(r,l,o)=>s(r,l,o))}function O(i,e,t,s,n,r){let l=t??(c=>c),o=e.value,a=new Array(o.length).fill(null),h=new Array(o.length);for(let[c,m]of o.entries()){h[c]=l(m);let g=N(i.ctx,n,m,s?c:null,r);i.cur_el.append(g.build.base_el),a[c]=g}let _=i.cur_el,d=()=>{let c=e.value,m=c.map(l),g=new Array(c.length).fill(null),f=new D(_,a,g,u=>N(i.ctx,n,c[u],s?u:null,r));F(h,m,f),a=g,h=m};i.store.effect_manual([e.id],[],d,i.slab,!1),i.slab!==void 0&&i.store.add_cleaner(i.slab,()=>{for(let c of a)c?.build.remove()})}function N(i,e,t,s,n){let r=i.removable_chunk(e),l=s!==null?r.signal(s):void 0;return n(r,t,l??null),{index:l,build:r}}class D{parent;old_items;new_items;item_builder;constructor(e,t,s,n){this.parent=e,this.old_items=t,this.new_items=s,this.item_builder=n}insert(e,t){let s=this.item_builder(e);t!==null?this.new_items[t].build.base_el.before(s.build.base_el):this.parent.append(s.build.base_el),this.new_items[e]=s}move(e,t){let s=this.new_items[e].build.base_el;t!==null?this.new_items[t].build.base_el.before(s):this.parent.append(s)}remove(e){this.old_items[e].build.remove()}set_index(e,t){let s=this.old_items[e];s.index!==void 0&&e!==t&&(s.index.value=t,s.index.store.force_update(s.index.id)),this.new_items[t]=s}}function F(i,e,t){const s=i.length,n=e.length;let r=0;for(;r<s&&r<n&&i[r]===e[r];)t.set_index(r,r),r++;let l=s,o=n;for(;l>r&&o>r&&i[l-1]===e[o-1];)l--,o--,t.set_index(l,o);if(r===l)for(let a=r;a<o;a++){const h=o<n?o:null;t.insert(a,h)}else if(r===o)for(let a=r;a<l;a++)t.remove(a);else{const a=o-r,h=new Map,_=new Array(a).fill(null);for(let f=o-1;f>=r;f--){const u=e[f];h.has(u)&&(_[f-r]=h.get(u)),h.set(u,f)}const d=new Int32Array(a);let c=!1,m=0,g=0;for(let f=r;f<l;f++){if(g>=a){t.remove(f);continue}const u=i[f];if(h.has(u)){const p=h.get(u);h.delete(u);const w=p-r,v=_[w];v!==null&&h.set(e[v],v),d[w]=f+1,t.set_index(f,p),p>=m?m=p:c=!0,g++;continue}t.remove(f)}if(c){const f=G(d);let u=f.length-1;for(let p=a-1;p>=0;p--){const w=r+p,v=w+1<n?w+1:null;d[p]===0?t.insert(w,v):u<0||p!==f[u]?t.move(w,v):u--}}else for(let f=a-1;f>=0;f--)if(d[f]===0){const u=r+f,p=u+1<n?u+1:null;t.insert(u,p)}}}function G(i){const e=new Int32Array(i.length),t=[];for(let l=0;l<i.length;l++){if(i[l]===0)continue;const o=t.length;if(o===0||i[t[o-1]]<i[l]){o>0&&(e[l]=t[o-1]),t.push(l);continue}let a=0,h=t.length;for(;a<h;){const d=a+h>>1;i[t[d]]<i[l]?a=d+1:h=d}const _=a;_>0&&(e[l]=t[_-1]),t[_]=l}let s=t.length;const n=new Array(s);let r=s>0?t[s-1]:0;for(;s>0;)s--,n[s]=r,r=e[r];return n}class Q extends q{#e=new U;root_el;constructor(e){super(),this.root_el=e,super.init(this,void 0)}get store(){return this.#e}new_chunk(e){return new k(this,e)}root_chunk(){return new k(this,this.root_el)}removable_chunk(e){return new j(this,document.createElement(e),this.store.new_slab())}}export{k as ChunkBuild,Q as Context,y as ROSignal,j as RemovableChunk,b as Signal,U as Store,q as StoreProv,J as render_list,K as render_list_enumerated,H as show_if};
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@neocomp/core",
3
+ "version": "0.1.0",
4
+ "description": "new generation of reactivity and expressivity",
5
+ "author": "aliibrahim123",
6
+ "scripts": {
7
+ "start": "vite .",
8
+ "build": "vite build"
9
+ },
10
+ "type": "module",
11
+ "main": "./src/index.js",
12
+ "exports": {
13
+ ".": "./src/index.ts",
14
+ "./html-parser": "./src/html-parser.ts",
15
+ "./enable_chunk_parsing": "./src/enable_chunk_parsing.ts",
16
+ "./js/": "./dist/index.js",
17
+ "./js/enable_chunk_parsing": "./src/enable_chunk_parsing.js"
18
+ },
19
+ "homepage": "https://aliibrahim123.github.io/neocomp.js/",
20
+ "bugs": {
21
+ "url": "https://github.com/aliibrahim123/neocomp.js/issues"
22
+ },
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "https://github.com/aliibrahim123/neocomp.js.git"
26
+ },
27
+ "license": "MIT",
28
+ "devDependencies": {
29
+ "vite": "^8.0.16"
30
+ }
31
+ }
package/readme.md ADDED
@@ -0,0 +1,64 @@
1
+ # neocomp
2
+
3
+ bored of frameworks magic, want a lightweight solution for building scalable web apps and sites.
4
+
5
+ introducing **neocomp**, a lightweight, fast, and modern javascript web framework simplifying web
6
+ develepment without sacrificing maintainability or language identity.
7
+
8
+ it achieves this goal through fine grained reactivity, driven by imperative construction, with the
9
+ use of chunked templating.
10
+
11
+ ```typescript
12
+ class Counter extends Component {
13
+ constructor() {
14
+ super();
15
+ const { html } = this.createTop();
16
+
17
+ let count = this.signal(0);
18
+ html` <button on:click="${() => count.value++}">count: ${count}</button> `;
19
+
20
+ this.fireInit();
21
+ }
22
+ }
23
+ ```
24
+
25
+ # quick start
26
+
27
+ - [first steps](./docs/quick-guide/first-steps.md)
28
+ - [basic state management](./docs/quick-guide/basic-state.md)
29
+ - [templates](./docs/quick-guide/templates.md)
30
+
31
+ # docs
32
+
33
+ this is the index of the documentation.
34
+
35
+ - [rawdom](./docs/rawdom.md)
36
+ - **litedom:**
37
+ - [core](./docs/litedom/core.md)
38
+ - [parse](./docs/litedom/parse.md)
39
+ - [zro router](./docs/zro-router.md)
40
+ - **`comp-base` module:**
41
+ - **core:**
42
+ - [`Component`](./docs/comp-base.core/component.md)
43
+ - [registry](./docs/comp-base.core/registry.md)
44
+ - [general utilities](./docs/comp-base.core/utilities.md)
45
+ - **state:**
46
+ - [fundamentals](./docs/comp-base.state/fundamentals.md)
47
+ - [`Store`](./docs/comp-base.state/store.md)
48
+ - [state utilities](./docs/comp-base.state/utilities.md)
49
+ - **view:**
50
+ - [`View`](./docs/comp-base.view/view.md)
51
+ - [chunk reference](./docs/comp-base.view/chunk.md)
52
+ - [vite plugin](./docs/plugin.md)
53
+ - [examples and patterns](./docs/examples.md)
54
+
55
+ # blogpost
56
+
57
+ check out the latest blogposts about neocomp at
58
+ [recomputed](https://aliibrahim123.github.io/recomputed/web-dev).
59
+
60
+ ---
61
+
62
+ thanks for selecting neocomp as your base framework, if you noticed issues or have ideas dont be shy
63
+ to share.\
64
+ `request(new Time() |> filter(!bug))`.
package/src/chunk.ts ADDED
@@ -0,0 +1,144 @@
1
+ import type { Element as LiteElement } from './html-parser.ts';
2
+ import type { Context } from './index.ts';
3
+ import { ROSignal, Signal, type SlabID, StoreProv } from './reactive.ts';
4
+
5
+ const parse = (globalThis as any).__neocomp_enable_chunk_parsing
6
+ ? (await import('./html-parser.ts')).parse
7
+ : () => {
8
+ throw new Error('chunk parsing not enabled');
9
+ };
10
+
11
+ const chunk_registery = new Map<string[], LiteElement>();
12
+
13
+ const class_prefix = 'class:';
14
+ const style_prefix = 'style:';
15
+ const prop_prefix = 'prop:';
16
+ const on_prefix = 'on:';
17
+ function apply_attr(el: Element, attr: string, value: any) {
18
+ if (attr.startsWith(class_prefix)) el.classList.toggle(attr.slice(class_prefix.length), value);
19
+ else if (attr.startsWith(style_prefix)) {
20
+ (el as HTMLElement).style.setProperty(attr.slice(style_prefix.length), value);
21
+ } else if (attr.startsWith(prop_prefix)) (el as any)[attr.slice(prop_prefix.length)] = value;
22
+ else if (typeof value === 'boolean') el.toggleAttribute(attr, value);
23
+ else if (value === null || value === undefined) el.removeAttribute(attr);
24
+ else el.setAttribute(attr, String(value));
25
+ }
26
+
27
+ function into_node(value: any): Node {
28
+ if (value instanceof Node) return value;
29
+ if (value === null || value === undefined) return document.createTextNode('');
30
+ return document.createTextNode(String(value));
31
+ }
32
+ function dynamic_node(build: ChunkBuild, el: Element, getter: () => any) {
33
+ let last_node = into_node(getter());
34
+ el.appendChild(last_node);
35
+ if (last_node instanceof Text) {
36
+ build.effect(() => {
37
+ let value = getter();
38
+ if (value === undefined || value === null) last_node.nodeValue = '';
39
+ else last_node.nodeValue = String(value);
40
+ });
41
+ } else {
42
+ build.effect(() => {
43
+ let node = getter();
44
+ el.replaceChild(node, last_node);
45
+ last_node = node;
46
+ });
47
+ }
48
+ }
49
+
50
+ function construct_child(
51
+ build: ChunkBuild,
52
+ el: Element,
53
+ child: LiteElement['children'][number],
54
+ args: any[],
55
+ ) {
56
+ if (typeof (child as any)?.tag === 'string') {
57
+ let _child = construct_el(build, child as any, args);
58
+ el.append(_child);
59
+ } else if ((child as any)?.type === 'do') {
60
+ build.__el_stack.push(el);
61
+ args[(child as any).arg](build, el);
62
+ build.__el_stack.pop();
63
+ } else {
64
+ let arg = typeof child === 'number' ? args[child] : child;
65
+ if (arg instanceof ChunkBuild) {
66
+ el.append(arg.base_el);
67
+ if (arg instanceof RemovableChunk && build.slab !== undefined) {
68
+ build.store.add_cleaner(build.slab, () => arg.remove());
69
+ }
70
+ } else if (arg instanceof Signal || arg instanceof ROSignal) {
71
+ dynamic_node(build, el, () => arg.value);
72
+ } else if (typeof arg === 'function') dynamic_node(build, el, arg);
73
+ else el.append(into_node(arg));
74
+ }
75
+ }
76
+
77
+ function construct_el(build: ChunkBuild, lite: LiteElement, args: any[]): Element {
78
+ let el = document.createElement(lite.tag);
79
+ for (let { attr, value } of lite.attrs) {
80
+ let arg = typeof value === 'string' ? value : args[value];
81
+ if (attr.startsWith(on_prefix)) {
82
+ el.addEventListener(attr.slice(on_prefix.length), (event) => {
83
+ arg(event);
84
+ build.store.flush_updates();
85
+ });
86
+ } else if (arg instanceof Signal || arg instanceof ROSignal) {
87
+ build.effect(() => apply_attr(el, attr, arg.value));
88
+ } else if (typeof arg === 'function') {
89
+ build.effect(() => apply_attr(el, attr, arg()));
90
+ } else apply_attr(el, attr, arg);
91
+ }
92
+ for (let child of lite.children) construct_child(build, el, child, args);
93
+ return el;
94
+ }
95
+
96
+ export class ChunkBuild extends StoreProv {
97
+ base_el: Element;
98
+ __el_stack: Element[];
99
+ constructor(ctx: Context, base_el: Element, slab: SlabID | undefined = undefined) {
100
+ super();
101
+ super.init(ctx, slab);
102
+ this.base_el = base_el;
103
+ this.__el_stack = [base_el];
104
+
105
+ this.html = this.html.bind(this);
106
+ (this.html as any).__add = this.__add.bind(this);
107
+ this.signal = this.signal.bind(this);
108
+ this.effect = this.effect.bind(this);
109
+ this.computed = this.computed.bind(this);
110
+ }
111
+
112
+ get cur_el(): Element {
113
+ return this.__el_stack.at(-1)!;
114
+ }
115
+
116
+ html(parts: TemplateStringsArray, ...args: any[]) {
117
+ let _parts = parts as any as string[];
118
+ if (chunk_registery.has(_parts)) return this.__add(chunk_registery.get(_parts)!, args);
119
+ let lite = parse(_parts);
120
+ chunk_registery.set(_parts, lite);
121
+ this.__add(lite, args);
122
+ }
123
+ __add(lite: LiteElement, args: any[]) {
124
+ for (let child of lite.children) construct_child(this, this.cur_el, child, args);
125
+ }
126
+ }
127
+
128
+ export class RemovableChunk extends ChunkBuild {
129
+ remove() {
130
+ this.base_el.remove();
131
+ if (this.slab != undefined) this.store.remove_slab(this.slab);
132
+ }
133
+ }
134
+
135
+ export function show_if(value: boolean | Signal<boolean> | ROSignal<boolean> | (() => boolean)) {
136
+ return (build: ChunkBuild, el: HTMLElement) => {
137
+ if (value == false) el.style.display = 'none';
138
+ else if (value instanceof Signal || value instanceof ROSignal) {
139
+ build.effect(() => (el.style.display = value.value ? '' : 'none'));
140
+ } else if (typeof value === 'function') {
141
+ build.effect(() => (el.style.display = value() ? '' : 'none'));
142
+ }
143
+ };
144
+ }
@@ -0,0 +1 @@
1
+ (globalThis as any).__neocomp_enable_chunk_parsing = true;
@@ -0,0 +1,178 @@
1
+ export interface Element {
2
+ tag: string;
3
+ attrs: { attr: string; value: string | number }[];
4
+ children: (Element | string | number | { type: 'do'; arg: number })[];
5
+ }
6
+
7
+ const name_regex = /^[^<>'"`=/\s]+/;
8
+
9
+ // deno-fmt-ignore
10
+ const void_tags = new Set([
11
+ 'area',
12
+ 'base',
13
+ 'br',
14
+ 'col',
15
+ 'embed',
16
+ 'hr',
17
+ 'img',
18
+ 'input',
19
+ 'link',
20
+ 'meta',
21
+ 'param',
22
+ 'source',
23
+ 'track',
24
+ 'wbr',
25
+ ]);
26
+
27
+ function eat_ws(part: string, ind: number): number {
28
+ return ind + part.slice(ind).match(/^\s*/)![0].length;
29
+ }
30
+
31
+ function unexpected_token(found: string | undefined, expected: string): never {
32
+ if (found == undefined) {
33
+ throw new SyntaxError(`unexpected end of input, expected "${expected}"`);
34
+ }
35
+ throw new SyntaxError(`unexpected token "${found}", expected "${expected}"`);
36
+ }
37
+
38
+ function eat_name(part: string, ind: number, expected: string): [string, number] {
39
+ let name = part.slice(ind).match(name_regex)?.[0];
40
+ if (!name) unexpected_token(part[ind], expected);
41
+ return [name, ind + name.length];
42
+ }
43
+
44
+ export function parse(parts: string[]): Element {
45
+ let el_stack: Element[] = [
46
+ {
47
+ tag: 'html',
48
+ attrs: [],
49
+ children: [],
50
+ },
51
+ ];
52
+
53
+ let state: 'in_content' | 'in_attr' | 'in_do' | 'in_attr_quoted' = 'in_content';
54
+
55
+ for (let [part_ind, part] of parts.entries()) {
56
+ let ind = 0;
57
+ while_part: while (part.length > ind) {
58
+ let cur_el = el_stack.at(-1)!;
59
+
60
+ if (state === 'in_do') {
61
+ ind = eat_ws(part, ind);
62
+ if (part[ind] === '/') ind += 1;
63
+ if (part[ind] !== '>') unexpected_token(part[ind], '>');
64
+ ind += 1;
65
+ state = 'in_content';
66
+ } else if (state === 'in_attr_quoted') {
67
+ if (part[ind] !== '"' && part[ind] !== "'") unexpected_token(part[ind], "'\"'");
68
+ ind += 1;
69
+ state = 'in_attr';
70
+ } else if (state === 'in_attr') {
71
+ ind = eat_ws(part, ind);
72
+
73
+ while (ind < part.length && part[ind] !== '>' && part[ind] !== '/') {
74
+ let attr, value;
75
+ [attr, ind] = eat_name(part, ind, 'attribute name');
76
+ ind = eat_ws(part, ind);
77
+
78
+ if (part[ind] === '=') {
79
+ ind += 1;
80
+ ind = eat_ws(part, ind);
81
+
82
+ if (ind === part.length) {
83
+ cur_el.attrs.push({ attr, value: part_ind });
84
+ break while_part;
85
+ }
86
+ if ((part[ind] === '"' || part[ind] === "'") && part.length === ind + 1) {
87
+ cur_el.attrs.push({ attr, value: part_ind });
88
+ state = 'in_attr_quoted';
89
+ break while_part;
90
+ }
91
+ if (part[ind] === '"' || part[ind] === "'") {
92
+ let quote = part[ind];
93
+ let end = part.indexOf(quote, ind + 1);
94
+ if (end === -1) throw new SyntaxError('unended string');
95
+ value = part.slice(ind + 1, end);
96
+ ind = end + 1;
97
+ } else [value, ind] = eat_name(part, ind, 'attribute value');
98
+
99
+ let last_ind = ind;
100
+ ind = eat_ws(part, ind);
101
+ if (
102
+ last_ind === ind &&
103
+ ind < part.length &&
104
+ part[ind] !== '>' &&
105
+ part[ind] !== '/'
106
+ ) {
107
+ throw new SyntaxError('expected whitespace');
108
+ }
109
+ } else {
110
+ value = '';
111
+ }
112
+
113
+ cur_el.attrs.push({ attr, value });
114
+ }
115
+
116
+ if (part[ind] === '/') {
117
+ ind += 1;
118
+ ind = eat_ws(part, ind);
119
+ el_stack.pop();
120
+ } else if (void_tags.has(cur_el.tag.toLowerCase())) el_stack.pop();
121
+
122
+ if (part[ind] !== '>') unexpected_token(part[ind], '">"');
123
+ ind += 1;
124
+ state = 'in_content';
125
+ } else {
126
+ let next_bracket = part.indexOf('<', ind);
127
+ let text = part.slice(ind, next_bracket === -1 ? part.length : next_bracket);
128
+ if (text.length > 0) {
129
+ if (typeof cur_el.children.at(-1) === 'string') {
130
+ cur_el.children[cur_el.children.length - 1] = cur_el.children.at(-1) + text;
131
+ } else cur_el.children.push(text);
132
+ }
133
+
134
+ if (next_bracket === -1) break;
135
+
136
+ ind = next_bracket + 1;
137
+
138
+ if (part.length === ind) {
139
+ cur_el.children.push({ type: 'do', arg: part_ind });
140
+ state = 'in_do';
141
+ } else if (part.slice(ind, ind + 3) === '!--') {
142
+ let end = part.indexOf('-->', ind);
143
+ if (end === -1) throw new SyntaxError('unended comment');
144
+ ind = end + 3;
145
+ } else if (part[ind] === '/') {
146
+ ind += 1;
147
+ let tag;
148
+ [tag, ind] = eat_name(part, ind, 'tag');
149
+ if (el_stack.length === 1) {
150
+ throw new SyntaxError(`unexpected end tag "${tag}" at root`);
151
+ }
152
+ if (tag !== cur_el.tag) {
153
+ throw new SyntaxError(`expected end tag "${cur_el.tag}" but got "${tag}"`);
154
+ }
155
+ ind = eat_ws(part, ind);
156
+ if (part[ind] !== '>') unexpected_token(part[ind], '">"');
157
+ ind += 1;
158
+ el_stack.pop();
159
+ } else {
160
+ let tag;
161
+ [tag, ind] = eat_name(part, ind, 'tag');
162
+ let el: Element = { tag, attrs: [], children: [] };
163
+ cur_el.children.push(el);
164
+ el_stack.push(el);
165
+ state = 'in_attr';
166
+ }
167
+ }
168
+ }
169
+ if (part_ind !== parts.length - 1 && state === 'in_content') {
170
+ el_stack.at(-1)!.children.push(part_ind);
171
+ }
172
+ }
173
+
174
+ if (state !== 'in_content') throw new SyntaxError('unexpected end of input, expected ">"');
175
+ if (el_stack.length !== 1) throw new SyntaxError('end of input with unclosed tags');
176
+
177
+ return el_stack[0];
178
+ }
package/src/index.ts ADDED
@@ -0,0 +1,38 @@
1
+ import { ChunkBuild, RemovableChunk } from './chunk.ts';
2
+ import { Store, StoreProv } from './reactive.ts';
3
+
4
+ export class Context extends StoreProv {
5
+ #store = new Store();
6
+ root_el: Element;
7
+ constructor(root_el: Element) {
8
+ super();
9
+ this.root_el = root_el;
10
+ super.init(this, undefined);
11
+ }
12
+
13
+ override get store(): Store {
14
+ return this.#store;
15
+ }
16
+
17
+ new_chunk(base_el: Element) {
18
+ return new ChunkBuild(this, base_el);
19
+ }
20
+ root_chunk() {
21
+ return new ChunkBuild(this, this.root_el);
22
+ }
23
+ removable_chunk(tag: string) {
24
+ return new RemovableChunk(this, document.createElement(tag), this.store.new_slab());
25
+ }
26
+ }
27
+
28
+ export { ChunkBuild, RemovableChunk, show_if } from './chunk.ts';
29
+ export {
30
+ type PropId,
31
+ ROSignal,
32
+ Signal,
33
+ type SlabID,
34
+ Store,
35
+ StoreProv,
36
+ type ReadSignal,
37
+ } from './reactive.ts';
38
+ export { render_list, render_list_enumerated } from './render_list.ts';
@@ -0,0 +1,275 @@
1
+ import type { Context } from './index.ts';
2
+
3
+ export type PropId<T> = number & { __type: T };
4
+ export type SlabID = number & { __type: 'slab' };
5
+
6
+ export class Store {
7
+ #props = new Map<number, any>();
8
+ #cur_prop = 0;
9
+ #slabs = new Map<number, { props: number[]; effects: number[]; cleaners: (() => void)[] }>();
10
+ #slabs_to_remove: SlabID[] = [];
11
+ #cur_slab = 0;
12
+ #is_updating = false;
13
+ #dirty_props: Set<number> = new Set();
14
+ #effects = new Map<number, { fun: () => void; read: number[]; write: number[] }>();
15
+ #cur_effect = 0;
16
+ #read_effect_map = new Map<number, number[]>();
17
+ #track_result: { read: Set<number>; write: Set<number> } | null = null;
18
+
19
+ new_slab(): SlabID {
20
+ let id = this.#cur_slab;
21
+ this.#slabs.set(id, { props: [], effects: [], cleaners: [] });
22
+ this.#cur_slab += 1;
23
+ return id as SlabID;
24
+ }
25
+ has_slab(id: SlabID): boolean {
26
+ return this.#slabs.has(id) && !this.#slabs_to_remove.includes(id);
27
+ }
28
+ remove_slab(id: SlabID) {
29
+ if (!this.#slabs.has(id)) throw new Error(`slab ${id} does not exist`);
30
+ if (this.#is_updating) this.#slabs_to_remove.push(id);
31
+ else this.#remove_slab(id);
32
+ }
33
+ #remove_slab(id: SlabID) {
34
+ let slab = this.#slabs.get(id)!;
35
+ for (let cleaner of slab.cleaners) cleaner();
36
+ for (let prop of slab.props) {
37
+ this.#props.delete(prop);
38
+ this.#read_effect_map.delete(prop);
39
+ }
40
+ for (let effect of slab.effects) {
41
+ for (let read of this.#effects.get(effect)!.read) {
42
+ let map = this.#read_effect_map.get(read);
43
+ if (map) map.splice(map.indexOf(effect), 1);
44
+ }
45
+ this.#effects.delete(effect);
46
+ }
47
+ this.#slabs.delete(id);
48
+ }
49
+ add_cleaner(slab: SlabID, cleaner: () => void) {
50
+ this.#slabs.get(slab)?.cleaners.push(cleaner);
51
+ }
52
+
53
+ prop<T>(value: T, slab: SlabID | undefined = undefined): PropId<T> {
54
+ let id = this.#cur_prop;
55
+ this.#props.set(id, value);
56
+ this.#cur_prop += 1;
57
+ if (slab != undefined) this.#slabs.get(slab)?.props.push(id);
58
+ return id as PropId<T>;
59
+ }
60
+ has(id: PropId<any>): boolean {
61
+ return this.#props.has(id);
62
+ }
63
+
64
+ get<T>(id: PropId<T>): T {
65
+ this.#track_read(id);
66
+ return this.#props.get(id) as T;
67
+ }
68
+ peek<T>(id: PropId<T>): T {
69
+ return this.#props.get(id) as T;
70
+ }
71
+ set<T>(id: PropId<T>, value: T) {
72
+ this.#track_write(id);
73
+ this.#mark_dirty(id);
74
+ this.#props.set(id, value);
75
+ }
76
+ update<T>(id: PropId<T>, updater: (value: T) => T) {
77
+ this.#track_write(id);
78
+ this.#mark_dirty(id);
79
+ this.#props.set(id, updater(this.#props.get(id) as T));
80
+ }
81
+
82
+ signal<T>(value: T, slab: SlabID | undefined = undefined): Signal<T> {
83
+ return new Signal(this, this.prop(value, slab));
84
+ }
85
+
86
+ signal_for<T>(id: PropId<T>): Signal<T> {
87
+ return new Signal(this, id);
88
+ }
89
+
90
+ effect(fun: () => void, slab: SlabID | undefined = undefined) {
91
+ this.start_track();
92
+ fun();
93
+ let { read, write } = this.end_track();
94
+ this.#add_effect(fun, read, write, slab);
95
+ }
96
+ effect_manual(
97
+ read: number[],
98
+ write: number[],
99
+ fun: () => void,
100
+ slab: SlabID | undefined = undefined,
101
+ init_run = true,
102
+ ) {
103
+ if (init_run) fun();
104
+ this.#add_effect(fun, read, write, slab);
105
+ }
106
+ #add_effect(
107
+ fun: () => void,
108
+ read: number[],
109
+ write: number[],
110
+ slab: SlabID | undefined = undefined,
111
+ ) {
112
+ let id = this.#cur_effect;
113
+ this.#effects.set(id, { fun, read, write });
114
+ this.#cur_effect += 1;
115
+ if (slab != undefined) this.#slabs.get(slab)?.effects.push(id);
116
+ for (let prop of read) {
117
+ if (!this.#read_effect_map.has(prop)) this.#read_effect_map.set(prop, [id]);
118
+ else this.#read_effect_map.get(prop)?.push(id);
119
+ }
120
+ return id;
121
+ }
122
+ computed<T>(fun: () => T, slab: SlabID | undefined = undefined): ROSignal<T> {
123
+ this.start_track();
124
+ let value = fun();
125
+ let { read, write } = this.end_track();
126
+ if (write.length != 0) throw new Error('computed properties cannot set other properties');
127
+ let prop = this.prop(value, slab);
128
+ this.#add_effect(() => this.set(prop, fun()), read, [prop], slab);
129
+ return new ROSignal(this, prop);
130
+ }
131
+
132
+ get tracking(): boolean {
133
+ return this.#track_result != null;
134
+ }
135
+ start_track() {
136
+ if (this.#track_result != null) throw new Error('already tracking');
137
+ this.#track_result = { read: new Set(), write: new Set() };
138
+ }
139
+ end_track(): { read: number[]; write: number[] } {
140
+ if (this.#track_result == null) throw new Error('not tracking');
141
+ let result = this.#track_result;
142
+ this.#track_result = null;
143
+ return { read: Array.from(result.read), write: Array.from(result.write) };
144
+ }
145
+ #track_read(id: number) {
146
+ if (this.#track_result == null) return;
147
+ this.#track_result.read.add(id);
148
+ }
149
+ #track_write(id: number) {
150
+ if (this.#track_result == null) return;
151
+ this.#track_result.write.add(id);
152
+ }
153
+
154
+ get updating(): boolean {
155
+ return this.#is_updating;
156
+ }
157
+ #mark_dirty(id: number) {
158
+ if (this.#is_updating) return;
159
+ this.#dirty_props.add(id);
160
+ }
161
+ force_update(id: PropId<any>) {
162
+ this.#dirty_props.add(id);
163
+ }
164
+ flush_updates() {
165
+ if (this.#is_updating) return;
166
+
167
+ this.#is_updating = true;
168
+ while (this.#dirty_props.size > 0) {
169
+ let to_run: number[] = [],
170
+ visited = new Set<number>(),
171
+ visiting: number[] = [];
172
+ for (let prop of this.#dirty_props) {
173
+ visit(this, prop, to_run, visited, visiting);
174
+ }
175
+ this.#dirty_props.clear();
176
+ for (let i = to_run.length - 1; i >= 0; i -= 1) {
177
+ try {
178
+ this.#effects.get(to_run[i])!.fun();
179
+ } catch (e) {
180
+ console.error(e);
181
+ }
182
+ }
183
+ }
184
+ this.#is_updating = false;
185
+
186
+ for (let slab of this.#slabs_to_remove) {
187
+ if (this.#slabs.has(slab)) this.#remove_slab(slab);
188
+ }
189
+ this.#slabs_to_remove = [];
190
+
191
+ function visit(
192
+ store: Store,
193
+ prop: number,
194
+ to_run: number[],
195
+ visited: Set<number>,
196
+ visiting: number[],
197
+ ) {
198
+ let effects = store.#read_effect_map.get(prop);
199
+ if (effects == undefined) return;
200
+ for (let effect of effects) {
201
+ if (visiting.includes(effect)) {
202
+ throw new Error('detected circular dependency in an update');
203
+ }
204
+ if (visited.has(effect)) continue;
205
+ visiting.push(effect);
206
+ for (let prop of store.#effects.get(effect)!.write) {
207
+ visit(store, prop, to_run, visited, visiting);
208
+ }
209
+ visiting.pop();
210
+ to_run.push(effect);
211
+ visited.add(effect);
212
+ }
213
+ }
214
+ }
215
+ }
216
+
217
+ class SignalBase<T> {
218
+ id: PropId<T>;
219
+ store: Store;
220
+ constructor(store: Store, prop: PropId<T>) {
221
+ this.id = prop;
222
+ this.store = store;
223
+ }
224
+ }
225
+
226
+ export class Signal<T> extends SignalBase<T> {
227
+ get value(): T {
228
+ return this.store.get(this.id);
229
+ }
230
+ set value(value: T) {
231
+ this.store.set(this.id, value);
232
+ }
233
+ peek(): T {
234
+ return this.store.peek(this.id);
235
+ }
236
+ update(updater: (value: T) => T) {
237
+ this.store.update(this.id, updater);
238
+ }
239
+ as_ro(): ROSignal<T> {
240
+ return new ROSignal(this.store, this.id);
241
+ }
242
+ }
243
+ export class ROSignal<T> extends SignalBase<T> {
244
+ get value(): T {
245
+ return this.store.get(this.id);
246
+ }
247
+ peek(): T {
248
+ return this.store.peek(this.id);
249
+ }
250
+ }
251
+ export interface ReadSignal<T> extends SignalBase<T> {
252
+ value: T;
253
+ peek(): T;
254
+ }
255
+
256
+ export class StoreProv {
257
+ ctx: Context = undefined as any;
258
+ slab: SlabID | undefined = undefined;
259
+ init(ctx: Context, slab: SlabID | undefined = undefined) {
260
+ this.ctx = ctx;
261
+ this.slab = slab;
262
+ }
263
+ get store(): Store {
264
+ return this.ctx.store;
265
+ }
266
+ signal<T>(value: T): Signal<T> {
267
+ return this.store.signal(value, this.slab);
268
+ }
269
+ effect(fun: () => void) {
270
+ return this.store.effect(fun, this.slab);
271
+ }
272
+ computed<T>(fun: () => T): ROSignal<T> {
273
+ return this.store.computed(fun, this.slab);
274
+ }
275
+ }
@@ -0,0 +1,317 @@
1
+ import type { ChunkBuild, RemovableChunk } from './chunk.ts';
2
+ import type { Context } from './index.ts';
3
+ import type { ReadSignal, Signal } from './reactive.ts';
4
+
5
+ export function render_list<T, K>(
6
+ prop: ReadSignal<T[]>,
7
+ key_fn: ((item: T) => K) | null,
8
+ tag: string,
9
+ item_chunk: (build: ChunkBuild, item: T) => void,
10
+ ) {
11
+ return (build: ChunkBuild) =>
12
+ render_list_core(build, prop, key_fn, false, tag, (build, item, _) =>
13
+ item_chunk(build, item),
14
+ );
15
+ }
16
+
17
+ export function render_list_enumerated<T, K>(
18
+ prop: ReadSignal<T[]>,
19
+ key_fn: ((item: T) => K) | null,
20
+ tag: string,
21
+ item_chunk: (build: ChunkBuild, item: T, index: Signal<number>) => void,
22
+ ) {
23
+ return (build: ChunkBuild) =>
24
+ render_list_core(build, prop, key_fn, true, tag, (build, item, index) =>
25
+ item_chunk(build, item, index!),
26
+ );
27
+ }
28
+
29
+ function render_list_core<T, K>(
30
+ build: ChunkBuild,
31
+ prop: ReadSignal<T[]>,
32
+ _key_fn: ((item: T) => K) | null,
33
+ enumerate: boolean,
34
+ tag: string,
35
+ item_chunk: (build: ChunkBuild, item: T, index: Signal<number> | null) => void,
36
+ ): void {
37
+ let key_fn = _key_fn ?? ((item: T) => item as any as K);
38
+ let list = prop.value;
39
+
40
+ let items: (Item | null)[] = new Array(list.length).fill(null);
41
+ let keys: K[] = new Array(list.length);
42
+
43
+ for (let [ind, item_value] of list.entries()) {
44
+ keys[ind] = key_fn(item_value);
45
+
46
+ let item = build_item(build.ctx, tag, item_value, enumerate ? ind : null, item_chunk);
47
+ build.cur_el.append(item.build.base_el);
48
+
49
+ items[ind] = item;
50
+ }
51
+
52
+ let parent = build.cur_el;
53
+ let patcher = () => {
54
+ let list = prop.value;
55
+ let new_keys = list.map(key_fn);
56
+ let new_items: (Item | null)[] = new Array(list.length).fill(null);
57
+
58
+ let patcher = new Patcher(parent, items, new_items, (ind) => {
59
+ return build_item(build.ctx, tag, list[ind], enumerate ? ind : null, item_chunk);
60
+ });
61
+
62
+ diff(keys, new_keys, patcher);
63
+
64
+ items = new_items;
65
+ keys = new_keys;
66
+ };
67
+
68
+ build.store.effect_manual([prop.id], [], patcher, build.slab, false);
69
+
70
+ if (build.slab !== undefined)
71
+ build.store.add_cleaner(build.slab, () => {
72
+ for (let item of items) item?.build.remove();
73
+ });
74
+ }
75
+
76
+ interface Item {
77
+ build: RemovableChunk;
78
+ index?: Signal<number>;
79
+ }
80
+
81
+ function build_item<T>(
82
+ ctx: Context,
83
+ tag: string,
84
+ item: T,
85
+ ind: number | null,
86
+ item_chunk: (build: ChunkBuild, item: T, index: Signal<number> | null) => void,
87
+ ): Item {
88
+ let build = ctx.removable_chunk(tag);
89
+ let index = ind !== null ? build.signal(ind) : undefined;
90
+ item_chunk(build, item, index ?? null);
91
+ return { index, build };
92
+ }
93
+
94
+ export interface ReconcileOps {
95
+ insert(new_ind: number, reference: number | null): void;
96
+ move(new_ind: number, reference: number | null): void;
97
+ remove(old_ind: number): void;
98
+ set_index(old_ind: number, new_ind: number): void;
99
+ }
100
+
101
+ class Patcher implements ReconcileOps {
102
+ parent: Element;
103
+ old_items: (Item | null)[];
104
+ new_items: (Item | null)[];
105
+ item_builder: (ind: number) => Item;
106
+
107
+ constructor(
108
+ parent: Element,
109
+ old_items: (Item | null)[],
110
+ new_items: (Item | null)[],
111
+ item_builder: (ind: number) => Item,
112
+ ) {
113
+ this.parent = parent;
114
+ this.old_items = old_items;
115
+ this.new_items = new_items;
116
+ this.item_builder = item_builder;
117
+ }
118
+
119
+ insert(new_ind: number, reference: number | null): void {
120
+ let item = this.item_builder(new_ind);
121
+
122
+ if (reference !== null) {
123
+ this.new_items[reference]!.build.base_el.before(item.build.base_el);
124
+ } else this.parent.append(item.build.base_el);
125
+
126
+ this.new_items[new_ind] = item;
127
+ }
128
+
129
+ move(new_ind: number, reference: number | null): void {
130
+ let el = this.new_items[new_ind]!.build.base_el;
131
+ if (reference !== null) {
132
+ this.new_items[reference]!.build.base_el.before(el);
133
+ } else this.parent.append(el);
134
+ }
135
+
136
+ remove(old_ind: number): void {
137
+ this.old_items[old_ind]!.build.remove();
138
+ }
139
+
140
+ set_index(old_ind: number, new_ind: number): void {
141
+ let item = this.old_items[old_ind]!;
142
+ if (item.index !== undefined && old_ind !== new_ind) {
143
+ item.index.value = new_ind;
144
+ item.index.store.force_update(item.index.id);
145
+ }
146
+ this.new_items[new_ind] = item;
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Computes the diff between two lists and applies operations.
152
+ */
153
+ export function diff<K>(old_keys: K[], new_keys: K[], ops: ReconcileOps): void {
154
+ const old_len = old_keys.length;
155
+ const new_len = new_keys.length;
156
+
157
+ // 1. Common Prefix: Skip matching items at the start.
158
+ let start = 0;
159
+ while (start < old_len && start < new_len && old_keys[start] === new_keys[start]) {
160
+ ops.set_index(start, start);
161
+ start++;
162
+ }
163
+
164
+ // 2. Common Suffix: Skip matching items at the end.
165
+ let old_end = old_len;
166
+ let new_end = new_len;
167
+ while (old_end > start && new_end > start && old_keys[old_end - 1] === new_keys[new_end - 1]) {
168
+ old_end--;
169
+ new_end--;
170
+ ops.set_index(old_end, new_end);
171
+ }
172
+
173
+ // 3. Fast Paths: If we only have insertions or removals left.
174
+ if (start === old_end) {
175
+ // Only insertions remain.
176
+ for (let ind = start; ind < new_end; ind++) {
177
+ const ref_ind = new_end < new_len ? new_end : null;
178
+ ops.insert(ind, ref_ind);
179
+ }
180
+ } else if (start === new_end) {
181
+ // Only removals remain.
182
+ for (let ind = start; ind < old_end; ind++) {
183
+ ops.remove(ind);
184
+ }
185
+ } else {
186
+ // 4. Map Phase: Build a map of the remaining new items.
187
+ const new_left = new_end - start;
188
+ const new_ind_map = new Map<K, number>();
189
+ const next_duplicate = new Array<number | null>(new_left).fill(null);
190
+
191
+ for (let ind = new_end - 1; ind >= start; ind--) {
192
+ const key = new_keys[ind];
193
+ if (new_ind_map.has(key)) {
194
+ next_duplicate[ind - start] = new_ind_map.get(key)!;
195
+ }
196
+ new_ind_map.set(key, ind);
197
+ }
198
+
199
+ // Tracks where new items came from. `0` means it's a brand new item.
200
+ const sources = new Int32Array(new_left);
201
+ let some_moved = false;
202
+ let pos = 0;
203
+ let items_patched = 0;
204
+
205
+ // Find which old items are kept, moved, or removed.
206
+ for (let ind = start; ind < old_end; ind++) {
207
+ if (items_patched >= new_left) {
208
+ ops.remove(ind);
209
+ continue;
210
+ }
211
+
212
+ const key = old_keys[ind];
213
+ if (new_ind_map.has(key)) {
214
+ const new_ind = new_ind_map.get(key)!;
215
+ new_ind_map.delete(key);
216
+
217
+ const source_ind = new_ind - start;
218
+
219
+ const next_ind = next_duplicate[source_ind];
220
+ if (next_ind !== null) {
221
+ new_ind_map.set(new_keys[next_ind], next_ind);
222
+ }
223
+
224
+ // Item is kept. Record its old index (+1 to reserve 0 for "new").
225
+ sources[source_ind] = ind + 1;
226
+ ops.set_index(ind, new_ind);
227
+
228
+ // If a new index is smaller than a previous one, items crossed paths (moved).
229
+ if (new_ind >= pos) pos = new_ind;
230
+ else some_moved = true;
231
+
232
+ items_patched++;
233
+ continue;
234
+ }
235
+
236
+ // Item doesn't exist in the new array
237
+ ops.remove(ind);
238
+ }
239
+
240
+ // 5. Patch Phase: Apply DOM mutations backwards.
241
+ if (some_moved) {
242
+ // Find the longest sequence of items that don't need to move.
243
+ const seq = longest_increasing_subsequence(sources);
244
+ let j = seq.length - 1;
245
+
246
+ // in reverse to make `reference` always in its final position
247
+ for (let ind = new_left - 1; ind >= 0; ind--) {
248
+ const new_ind = start + ind;
249
+ const ref_ind = new_ind + 1 < new_len ? new_ind + 1 : null;
250
+
251
+ // Brand new item.
252
+ if (sources[ind] === 0) ops.insert(new_ind, ref_ind);
253
+ // Item exists, but isn't an anchor. MOVE it.
254
+ else if (j < 0 || ind !== seq[j]) ops.move(new_ind, ref_ind);
255
+ // Item is an anchor. Leave it exactly where it is.
256
+ else j--;
257
+ }
258
+ } else {
259
+ // Optimization: Nothing moved, just insert the new items in the gaps.
260
+ for (let ind = new_left - 1; ind >= 0; ind--) {
261
+ if (sources[ind] === 0) {
262
+ const new_ind = start + ind;
263
+ const ref_ind = new_ind + 1 < new_len ? new_ind + 1 : null;
264
+ ops.insert(new_ind, ref_ind);
265
+ }
266
+ }
267
+ }
268
+ }
269
+ }
270
+
271
+ /**
272
+ * Calculates the Longest Increasing Subsequence in O(N log N) using Patience Sorting.
273
+ * Returns the indices of the elements that form the sequence.
274
+ */
275
+ function longest_increasing_subsequence(a: Int32Array): number[] {
276
+ const pred = new Int32Array(a.length); // Predecessor tracking for backtracking
277
+ const result: number[] = []; // Stores indices of the smallest tails seen so far
278
+
279
+ for (let ind = 0; ind < a.length; ind++) {
280
+ if (a[ind] === 0) continue; // Ignore completely new nodes
281
+
282
+ const j = result.length;
283
+ if (j === 0 || a[result[j - 1]] < a[ind]) {
284
+ // Found a larger item. Extend the subsequence.
285
+ if (j > 0) pred[ind] = result[j - 1];
286
+ result.push(ind);
287
+ continue;
288
+ }
289
+
290
+ // Binary search to find partition point
291
+ let left = 0;
292
+ let right = result.length;
293
+ while (left < right) {
294
+ const mid = (left + right) >> 1;
295
+ if (a[result[mid]] < a[ind]) left = mid + 1;
296
+ else right = mid;
297
+ }
298
+ const pos = left;
299
+
300
+ if (pos > 0) pred[ind] = result[pos - 1];
301
+
302
+ result[pos] = ind;
303
+ }
304
+
305
+ // Backtrack through predecessors to build the exact anchor sequence
306
+ let u = result.length;
307
+ const seq = new Array<number>(u);
308
+ let v = u > 0 ? result[u - 1] : 0;
309
+
310
+ while (u > 0) {
311
+ u--;
312
+ seq[u] = v;
313
+ v = pred[v];
314
+ }
315
+
316
+ return seq;
317
+ }