@ikdao/hyp 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.
- package/README.md +119 -0
- package/doc.md +223 -0
- package/example/counter/index.html +84 -0
- package/example/hello-world/index.html +37 -0
- package/helper/file-loader.js +19 -0
- package/index.js +12 -0
- package/license.md +4 -0
- package/package.json +31 -0
- package/src/a.js +26 -0
- package/src/dA.js +12 -0
- package/src/e.js +255 -0
- package/src/h.js +56 -0
- package/src/hyp.js +671 -0
- package/src/i.js +20 -0
- package/src/n.js +190 -0
- package/src/o.js +75 -0
- package/src/r.js +16 -0
- package/src/s.js +39 -0
- package/src/sA.js +52 -0
package/src/hyp.js
ADDED
|
@@ -0,0 +1,671 @@
|
|
|
1
|
+
// HYP UI Framework
|
|
2
|
+
|
|
3
|
+
// Zero One One License - 011sl
|
|
4
|
+
// https://legal/ikdao.org/license/011sl
|
|
5
|
+
// Hyp UI Framework - [Hemang Tewari]
|
|
6
|
+
|
|
7
|
+
// HYP Organ Factory h()
|
|
8
|
+
// --- 1. Hyperscript h() ---
|
|
9
|
+
|
|
10
|
+
export const h = (ty, prp, ...chd) => {
|
|
11
|
+
// Normalize props & children
|
|
12
|
+
if (prp == null || typeof prp !== "object" || Array.isArray(prp)) {
|
|
13
|
+
chd.unshift(prp);
|
|
14
|
+
prp = {};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const stack = [...chd];
|
|
18
|
+
const flatChildren = [];
|
|
19
|
+
|
|
20
|
+
while (stack.length > 0) {
|
|
21
|
+
const item = stack.pop();
|
|
22
|
+
if (item == null || item === false) continue;
|
|
23
|
+
|
|
24
|
+
if (Array.isArray(item)) {
|
|
25
|
+
// Push in reverse to preserve order
|
|
26
|
+
for (let i = item.length - 1; i >= 0; i--) {
|
|
27
|
+
stack.push(item[i]);
|
|
28
|
+
}
|
|
29
|
+
} else if (item instanceof Actor) {
|
|
30
|
+
flatChildren.push(item);
|
|
31
|
+
} else if (typeof item === "string" || typeof item === "number" || typeof item === "boolean") {
|
|
32
|
+
flatChildren.push(String(item));
|
|
33
|
+
} else if (typeof item === "object" && item.ty) {
|
|
34
|
+
flatChildren.push(item);
|
|
35
|
+
} else if (typeof item === "function") {
|
|
36
|
+
flatChildren.push(item());
|
|
37
|
+
} else {
|
|
38
|
+
flatChildren.push(String(item));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
flatChildren.reverse(); // because we popped in reverse
|
|
43
|
+
|
|
44
|
+
// Handle component (function as type)
|
|
45
|
+
if (typeof ty === "function") {
|
|
46
|
+
return ty({ ...prp, children: flatChildren });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Handle element
|
|
50
|
+
return {
|
|
51
|
+
ty,
|
|
52
|
+
prp,
|
|
53
|
+
chd: flatChildren,
|
|
54
|
+
key: prp.key ?? null,
|
|
55
|
+
ref: prp.ref ?? null,
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// HYP Triad Architectural Pattern
|
|
60
|
+
// spatial/temporal/execution
|
|
61
|
+
|
|
62
|
+
// SCHEDULER (s)
|
|
63
|
+
// Temporal Layer — queues & runs tasks efficiently
|
|
64
|
+
|
|
65
|
+
export const s = (function () {
|
|
66
|
+
const left = new Set();
|
|
67
|
+
let flushing = false;
|
|
68
|
+
|
|
69
|
+
function flush() {
|
|
70
|
+
flushing = false;
|
|
71
|
+
const tasks = Array.from(left);
|
|
72
|
+
left.clear();
|
|
73
|
+
for (const task of tasks) {
|
|
74
|
+
try { task.fn(); }
|
|
75
|
+
catch (err) { console.error("Scheduler task error:", err); }
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
add(fn, ei) {
|
|
81
|
+
if (ei && !o.isAlive(ei)) return;
|
|
82
|
+
left.add({ fn, ei });
|
|
83
|
+
if (!flushing) {
|
|
84
|
+
queueMicrotask(flush);
|
|
85
|
+
flushing = true;
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
flush() { flush(); },
|
|
90
|
+
|
|
91
|
+
clear(ei) {
|
|
92
|
+
for (const task of [...left]) {
|
|
93
|
+
if (task.ei === ei) left.delete(task);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
})();
|
|
98
|
+
|
|
99
|
+
// ORGANISER (o)
|
|
100
|
+
// Structural Layer — Organise Organs, keep identities and map
|
|
101
|
+
|
|
102
|
+
export const o = (function () {
|
|
103
|
+
const organs = new Map();
|
|
104
|
+
let nextEi = 1;
|
|
105
|
+
|
|
106
|
+
function newEi() { return "ei_" + nextEi++; }
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
create(hi, body) {
|
|
110
|
+
const ei = newEi();
|
|
111
|
+
organs.set(ei, {
|
|
112
|
+
hi,
|
|
113
|
+
body,
|
|
114
|
+
ctx: new Map(),
|
|
115
|
+
mounted: true,
|
|
116
|
+
lifecycles: {
|
|
117
|
+
willMount: [], didMount: [],
|
|
118
|
+
willUpdate: [], didUpdate: [],
|
|
119
|
+
willUnmount: [], didUnmount: []
|
|
120
|
+
},
|
|
121
|
+
effects: new Set()
|
|
122
|
+
});
|
|
123
|
+
return ei;
|
|
124
|
+
},
|
|
125
|
+
addLifecycle(ei, phase, fn) {
|
|
126
|
+
const inst = organs.get(ei);
|
|
127
|
+
if (inst) inst.lifecycles[phase].push(fn);
|
|
128
|
+
},
|
|
129
|
+
runLifecycle(ei, phase, bodyRef) {
|
|
130
|
+
const inst = organs.get(ei);
|
|
131
|
+
if (!inst) return;
|
|
132
|
+
const list = inst.lifecycles[phase];
|
|
133
|
+
if (!list) return;
|
|
134
|
+
for (const fn of list)
|
|
135
|
+
s.add(() => fn(bodyRef), ei);
|
|
136
|
+
},
|
|
137
|
+
addEffect(ei, clear) {
|
|
138
|
+
const inst = organs.get(ei);
|
|
139
|
+
if (inst) inst.effects.add({ clear });
|
|
140
|
+
},
|
|
141
|
+
destroy(ei, { runLifecycle = true } = {}) {
|
|
142
|
+
const inst = organs.get(ei);
|
|
143
|
+
if (!inst) return;
|
|
144
|
+
|
|
145
|
+
inst.mounted = false;
|
|
146
|
+
|
|
147
|
+
if (runLifecycle) {
|
|
148
|
+
this.runLifecycle(ei, "willUnmount");
|
|
149
|
+
s.add(() => this.runLifecycle(ei, "didUnmount"), ei);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (inst.effects) {
|
|
153
|
+
for (const ef of inst.effects)
|
|
154
|
+
if (typeof ef.clear === "function") {
|
|
155
|
+
try { ef.clear(); }
|
|
156
|
+
catch (err) { console.error("Effect clear error:", err); }
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
organs.delete(ei);
|
|
160
|
+
s.clear(ei);
|
|
161
|
+
},
|
|
162
|
+
get(ei) { return organs.get(ei); },
|
|
163
|
+
has(ei) { return organs.has(ei); },
|
|
164
|
+
isAlive(ei) {
|
|
165
|
+
const inst = organs.get(ei);
|
|
166
|
+
return inst ? inst.mounted : false;
|
|
167
|
+
},
|
|
168
|
+
all() { return organs; }
|
|
169
|
+
};
|
|
170
|
+
})();
|
|
171
|
+
|
|
172
|
+
// executor e()
|
|
173
|
+
// EI execution identity/instance
|
|
174
|
+
// render/update/unmount
|
|
175
|
+
|
|
176
|
+
export const e = (function () {
|
|
177
|
+
const execStack = [];
|
|
178
|
+
|
|
179
|
+
function pushEI(ei) { execStack.push(ei); }
|
|
180
|
+
function popEI() { execStack.pop(); }
|
|
181
|
+
function currentEI() { return execStack[execStack.length - 1] || null; }
|
|
182
|
+
|
|
183
|
+
function render(vnode, body) {
|
|
184
|
+
const hi = vnode?.ty?.name || vnode?.ty || "anonymous";
|
|
185
|
+
const ei = o.create(hi, body);
|
|
186
|
+
pushEI(ei);
|
|
187
|
+
o.runLifecycle(ei, "willMount");
|
|
188
|
+
const dom = createDom(vnode, ei);
|
|
189
|
+
if (body) body.appendChild(dom);
|
|
190
|
+
s.add(() => o.runLifecycle(ei, "didMount"), ei);
|
|
191
|
+
popEI();
|
|
192
|
+
return ei;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function patch(dom, oldVNode, newVNode, ei) {
|
|
196
|
+
// Guard: invalid DOM or dead instance
|
|
197
|
+
if (!dom || !o.isAlive(ei)) return dom;
|
|
198
|
+
|
|
199
|
+
if (oldVNode == null) {
|
|
200
|
+
const newDom = createDom(newVNode, ei);
|
|
201
|
+
dom.replaceWith(newDom);
|
|
202
|
+
s.add(() => o.runLifecycle(ei, "didUpdate"), ei);
|
|
203
|
+
return newDom;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (newVNode == null) {
|
|
207
|
+
dom.remove();
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
pushEI(ei);
|
|
212
|
+
o.runLifecycle(ei, "willUpdate");
|
|
213
|
+
|
|
214
|
+
// Type or key changed → full replace
|
|
215
|
+
if (oldVNode.ty !== newVNode.ty || oldVNode.key !== newVNode.key) {
|
|
216
|
+
const newDom = createDom(newVNode, ei);
|
|
217
|
+
dom.replaceWith(newDom);
|
|
218
|
+
s.add(() => o.runLifecycle(ei, "didUpdate"), ei);
|
|
219
|
+
popEI();
|
|
220
|
+
return newDom;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Handle reactive text nodes (Actor)
|
|
224
|
+
if (oldVNode instanceof Actor && newVNode instanceof Actor) {
|
|
225
|
+
dom.data = newVNode.get();
|
|
226
|
+
s.add(() => o.runLifecycle(ei, "didUpdate"), ei);
|
|
227
|
+
popEI();
|
|
228
|
+
return dom;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Handle primitive text nodes
|
|
232
|
+
if ((typeof oldVNode === "string" || typeof oldVNode === "number") &&
|
|
233
|
+
(typeof newVNode === "string" || typeof newVNode === "number")) {
|
|
234
|
+
const newVal = String(newVNode);
|
|
235
|
+
if (dom.data !== newVal) dom.data = newVal;
|
|
236
|
+
s.add(() => o.runLifecycle(ei, "didUpdate"), ei);
|
|
237
|
+
popEI();
|
|
238
|
+
return dom;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Update props and children
|
|
242
|
+
updateprps(dom, oldVNode.prp || {}, newVNode.prp || {});
|
|
243
|
+
patchChildren(dom, oldVNode.chd || [], newVNode.chd || [], ei);
|
|
244
|
+
|
|
245
|
+
// Handle ref
|
|
246
|
+
if (newVNode.ref) newVNode.ref(dom);
|
|
247
|
+
|
|
248
|
+
s.add(() => o.runLifecycle(ei, "didUpdate"), ei);
|
|
249
|
+
popEI();
|
|
250
|
+
|
|
251
|
+
return dom;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function unmount(vnode = null, ei) {
|
|
255
|
+
const inst = o.get(ei);
|
|
256
|
+
if (!inst) return;
|
|
257
|
+
pushEI(ei);
|
|
258
|
+
const bodyRef = inst.body;
|
|
259
|
+
|
|
260
|
+
o.runLifecycle(ei, "willUnmount");
|
|
261
|
+
if (bodyRef?.parentNode)
|
|
262
|
+
bodyRef.parentNode.removeChild(bodyRef);
|
|
263
|
+
|
|
264
|
+
s.add(() => o.runLifecycle(ei, "didUnmount", bodyRef), ei);
|
|
265
|
+
|
|
266
|
+
o.destroy(ei, { runLifecycle: false });
|
|
267
|
+
popEI();
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
function createDom(v, ei) {
|
|
272
|
+
// null or primitive → text node
|
|
273
|
+
if (v == null) return document.createTextNode("");
|
|
274
|
+
if (typeof v === "string" || typeof v === "number")
|
|
275
|
+
return document.createTextNode(String(v));
|
|
276
|
+
|
|
277
|
+
// Reactive text node (Actor or dA)
|
|
278
|
+
if (v instanceof Actor) {
|
|
279
|
+
const textNode = document.createTextNode(v.get());
|
|
280
|
+
const update = () => { textNode.data = v.get(); };
|
|
281
|
+
const unsub = v.subscribe(update);
|
|
282
|
+
// tie cleanup to organiser (o)
|
|
283
|
+
if (ei) o.addEffect(ei, unsub);
|
|
284
|
+
return textNode;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const el = document.createElement(v.ty);
|
|
288
|
+
|
|
289
|
+
for (const [k, val] of Object.entries(v.prp || {})) {
|
|
290
|
+
if (k.startsWith("on") && typeof val === "function") {
|
|
291
|
+
el.addEventListener(k.slice(2).toLowerCase(), val);
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
if (k === "style" && typeof val === "object") {
|
|
295
|
+
for (const [sk, sv] of Object.entries(val)) {
|
|
296
|
+
if (sv instanceof Actor) {
|
|
297
|
+
const updateStyle = () => { el.style[sk] = sv.get(); };
|
|
298
|
+
updateStyle();
|
|
299
|
+
const unsub = sv.subscribe(updateStyle);
|
|
300
|
+
if (ei) o.addEffect(ei, unsub);
|
|
301
|
+
} else {
|
|
302
|
+
el.style[sk] = sv;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
if (val instanceof Actor) {
|
|
308
|
+
const updateAttr = () => {
|
|
309
|
+
const next = val.get();
|
|
310
|
+
if (k in el) el[k] = next;
|
|
311
|
+
else el.setAttribute(k, next);
|
|
312
|
+
};
|
|
313
|
+
updateAttr();
|
|
314
|
+
const unsub = val.subscribe(updateAttr);
|
|
315
|
+
if (ei) o.addEffect(ei, unsub);
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (k in el) el[k] = val;
|
|
320
|
+
else el.setAttribute(k, val);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
(v.chd || []).forEach(ch => {
|
|
324
|
+
el.appendChild(createDom(ch, ei));
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
if (v.ref) v.ref(el);
|
|
328
|
+
|
|
329
|
+
return el;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function updateprps(dom, oldprps, newprps) {
|
|
333
|
+
for (const k in oldprps) {
|
|
334
|
+
if (!(k in newprps)) {
|
|
335
|
+
if (k.startsWith("on") && typeof oldprps[k] === "function")
|
|
336
|
+
dom.removeEventListener(k.slice(2).toLowerCase(), oldprps[k]);
|
|
337
|
+
else
|
|
338
|
+
dom.removeAttribute(k);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
for (const [k, v] of Object.entries(newprps)) {
|
|
343
|
+
if (oldprps[k] !== v) {
|
|
344
|
+
if (k.startsWith("on") && typeof v === "function") {
|
|
345
|
+
if (oldprps[k]) dom.removeEventListener(k.slice(2).toLowerCase(), oldprps[k]);
|
|
346
|
+
dom.addEventListener(k.slice(2).toLowerCase(), v);
|
|
347
|
+
} else {
|
|
348
|
+
dom.setAttribute(k, v);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function patchChildren(dom, oldCh, newCh, ei) {
|
|
355
|
+
const oldKeyed = new Map();
|
|
356
|
+
const usedKeys = new Set();
|
|
357
|
+
|
|
358
|
+
// Index old children by key (skip non-keyed)
|
|
359
|
+
oldCh.forEach((c, i) => {
|
|
360
|
+
if (c && c.key != null) {
|
|
361
|
+
oldKeyed.set(c.key, { vnode: c, dom: dom.childNodes[i], index: i });
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
const newDoms = [];
|
|
366
|
+
|
|
367
|
+
// Process each new child
|
|
368
|
+
for (let i = 0; i < newCh.length; i++) {
|
|
369
|
+
const newV = newCh[i];
|
|
370
|
+
let newDom;
|
|
371
|
+
|
|
372
|
+
if (newV && newV.key != null) {
|
|
373
|
+
// Keyed node: try to reuse
|
|
374
|
+
const oldEntry = oldKeyed.get(newV.key);
|
|
375
|
+
if (oldEntry && oldEntry.dom) {
|
|
376
|
+
newDom = patch(oldEntry.dom, oldEntry.vnode, newV, ei);
|
|
377
|
+
usedKeys.add(newV.key);
|
|
378
|
+
} else {
|
|
379
|
+
// Create new
|
|
380
|
+
newDom = createDom(newV, ei);
|
|
381
|
+
}
|
|
382
|
+
} else {
|
|
383
|
+
// Non-keyed: patch by index if possible
|
|
384
|
+
const oldV = oldCh[i];
|
|
385
|
+
const oldDom = dom.childNodes[i];
|
|
386
|
+
if (oldDom && oldV != null) {
|
|
387
|
+
newDom = patch(oldDom, oldV, newV, ei);
|
|
388
|
+
} else if (oldDom) {
|
|
389
|
+
// Replace with new content
|
|
390
|
+
newDom = createDom(newV, ei);
|
|
391
|
+
oldDom.replaceWith(newDom);
|
|
392
|
+
} else {
|
|
393
|
+
// Append new
|
|
394
|
+
newDom = createDom(newV, ei);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
newDoms.push(newDom);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Update DOM order to match newDoms
|
|
402
|
+
for (let i = 0; i < newDoms.length; i++) {
|
|
403
|
+
const nextDom = dom.childNodes[i];
|
|
404
|
+
if (nextDom !== newDoms[i]) {
|
|
405
|
+
dom.insertBefore(newDoms[i], nextDom || null);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Remove unused keyed nodes
|
|
410
|
+
for (const [key, entry] of oldKeyed) {
|
|
411
|
+
if (!usedKeys.has(key) && entry.dom) {
|
|
412
|
+
entry.dom.remove();
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Remove extra non-keyed nodes at the end
|
|
417
|
+
while (dom.childNodes.length > newCh.length) {
|
|
418
|
+
dom.lastChild.remove();
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
return { render, patch, unmount, pushEI, popEI, currentEI };
|
|
423
|
+
})();
|
|
424
|
+
|
|
425
|
+
// Active/Reactive/Interactive Parts
|
|
426
|
+
|
|
427
|
+
// Actor a()
|
|
428
|
+
let tr = null;
|
|
429
|
+
export class Actor {
|
|
430
|
+
constructor(initial) {
|
|
431
|
+
this.value = initial;
|
|
432
|
+
this.subs = new Set();
|
|
433
|
+
}
|
|
434
|
+
get() {
|
|
435
|
+
if (tr) this.subs.add(tr);
|
|
436
|
+
return this.value;
|
|
437
|
+
}
|
|
438
|
+
set(next) {
|
|
439
|
+
if (next === this.value) return;
|
|
440
|
+
this.value = next;
|
|
441
|
+
this.subs.forEach(fn => s.add(fn));
|
|
442
|
+
}
|
|
443
|
+
subscribe(fn) {
|
|
444
|
+
this.subs.add(fn);
|
|
445
|
+
return () => this.subs.delete(fn);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
export const a = (initial) => new Actor(initial);
|
|
449
|
+
|
|
450
|
+
// Reactor r()/Derived Act dA()
|
|
451
|
+
export const r = (compute) => {
|
|
452
|
+
const sig = a();
|
|
453
|
+
const recompute = () => {
|
|
454
|
+
tr = recompute;
|
|
455
|
+
const val = compute();
|
|
456
|
+
tr = null;
|
|
457
|
+
sig.set(val);
|
|
458
|
+
};
|
|
459
|
+
recompute();
|
|
460
|
+
return sig;
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
// Interactor i()/Side Act sA()
|
|
464
|
+
export const i = (effect, explicitEI = null) => {
|
|
465
|
+
const ei = explicitEI ?? e.currentEI();
|
|
466
|
+
if (!ei) return;
|
|
467
|
+
let cleanup; // Track previous effect cleanup
|
|
468
|
+
const run = () => {
|
|
469
|
+
if (cleanup) {
|
|
470
|
+
try { cleanup(); } catch (err) { console.error("sA cleanup error:", err); }
|
|
471
|
+
}
|
|
472
|
+
tr = run;
|
|
473
|
+
cleanup = effect();
|
|
474
|
+
tr = null;
|
|
475
|
+
if (cleanup) o.addEffect(ei, cleanup);
|
|
476
|
+
};
|
|
477
|
+
run();
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
/* ---- Utilities -------------------------------- */
|
|
481
|
+
|
|
482
|
+
function parseQuery(search) {
|
|
483
|
+
return Object.fromEntries(new URLSearchParams(search || ""));
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function compileMatcher(pattern) {
|
|
487
|
+
const keys = [];
|
|
488
|
+
const regex = new RegExp(
|
|
489
|
+
"^" +
|
|
490
|
+
pattern
|
|
491
|
+
.replace(/\/+$/, "")
|
|
492
|
+
.replace(/:([^/]+)/g, (_, k) => {
|
|
493
|
+
keys.push(k);
|
|
494
|
+
return "([^/]+)";
|
|
495
|
+
}) +
|
|
496
|
+
"$"
|
|
497
|
+
);
|
|
498
|
+
|
|
499
|
+
return (path) => {
|
|
500
|
+
const m = path.replace(/\/+$/, "").match(regex);
|
|
501
|
+
if (!m) return null;
|
|
502
|
+
const params = {};
|
|
503
|
+
keys.forEach((k, i) => (params[k] = decodeURIComponent(m[i + 1])));
|
|
504
|
+
return params;
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/* ---- Navigator factory -------------- */
|
|
509
|
+
|
|
510
|
+
export function createNavigator({
|
|
511
|
+
url = null,
|
|
512
|
+
historyEnabled = true
|
|
513
|
+
} = {}) {
|
|
514
|
+
const initialURL = url
|
|
515
|
+
? new URL(url, "http://localhost")
|
|
516
|
+
: typeof window !== "undefined"
|
|
517
|
+
? window.location
|
|
518
|
+
: { pathname: "/", search: "" };
|
|
519
|
+
|
|
520
|
+
/* path is ALWAYS a string */
|
|
521
|
+
const path = a(String(initialURL.pathname));
|
|
522
|
+
const query = a(parseQuery(initialURL.search));
|
|
523
|
+
const params = a({});
|
|
524
|
+
|
|
525
|
+
/* async state */
|
|
526
|
+
const loading = a(false);
|
|
527
|
+
const error = a(null);
|
|
528
|
+
|
|
529
|
+
const routes = [];
|
|
530
|
+
let beforeEach = null;
|
|
531
|
+
|
|
532
|
+
/* canonical path setter */
|
|
533
|
+
function setPath(next) {
|
|
534
|
+
if (typeof next !== "string") {
|
|
535
|
+
console.warn("[navigator] path must be string, got:", next);
|
|
536
|
+
next = String(next);
|
|
537
|
+
}
|
|
538
|
+
path.set(next);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function syncFromLocation() {
|
|
542
|
+
if (typeof window === "undefined") return;
|
|
543
|
+
setPath(window.location.pathname);
|
|
544
|
+
query.set(parseQuery(window.location.search));
|
|
545
|
+
matchRoutes();
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function matchRoutes() {
|
|
549
|
+
const p = path.get();
|
|
550
|
+
for (const r of routes) {
|
|
551
|
+
const res = r.match(p);
|
|
552
|
+
if (res) {
|
|
553
|
+
params.set(res);
|
|
554
|
+
return r;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
params.set({});
|
|
558
|
+
return null;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
async function navigate(to, replace = false) {
|
|
562
|
+
const from = path.get();
|
|
563
|
+
const target = String(to);
|
|
564
|
+
|
|
565
|
+
/* Guards */
|
|
566
|
+
if (beforeEach) {
|
|
567
|
+
const result = await beforeEach(target, from);
|
|
568
|
+
if (result === false) return;
|
|
569
|
+
if (typeof result === "string") {
|
|
570
|
+
return navigate(result, true);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
if (typeof window !== "undefined" && historyEnabled) {
|
|
575
|
+
if (replace) history.replaceState(null, "", target);
|
|
576
|
+
else history.pushState(null, "", target);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
setPath(target);
|
|
580
|
+
matchRoutes();
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
if (typeof window !== "undefined" && historyEnabled) {
|
|
584
|
+
window.addEventListener("popstate", syncFromLocation);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/* -------- 4: async resolve ---------------- */
|
|
588
|
+
|
|
589
|
+
async function resolveAsync() {
|
|
590
|
+
loading.set(true);
|
|
591
|
+
error.set(null);
|
|
592
|
+
|
|
593
|
+
try {
|
|
594
|
+
const r = matchRoutes();
|
|
595
|
+
if (!r) {
|
|
596
|
+
loading.set(false);
|
|
597
|
+
return null;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
let view = r.view;
|
|
601
|
+
|
|
602
|
+
// Allow lazy / async route views
|
|
603
|
+
if (typeof view === "function") {
|
|
604
|
+
const res = view();
|
|
605
|
+
view = res instanceof Promise ? await res : res;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
loading.set(false);
|
|
609
|
+
return view;
|
|
610
|
+
} catch (err) {
|
|
611
|
+
error.set(err);
|
|
612
|
+
loading.set(false);
|
|
613
|
+
throw err;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
return {
|
|
618
|
+
/* Reactive state */
|
|
619
|
+
path,
|
|
620
|
+
query,
|
|
621
|
+
params,
|
|
622
|
+
loading,
|
|
623
|
+
error,
|
|
624
|
+
|
|
625
|
+
/* Routing table */
|
|
626
|
+
route(pattern, view) {
|
|
627
|
+
routes.push({
|
|
628
|
+
pattern,
|
|
629
|
+
view,
|
|
630
|
+
match: compileMatcher(pattern)
|
|
631
|
+
});
|
|
632
|
+
},
|
|
633
|
+
|
|
634
|
+
/* Navigation */
|
|
635
|
+
go(to) {
|
|
636
|
+
return navigate(to, false);
|
|
637
|
+
},
|
|
638
|
+
|
|
639
|
+
replace(to) {
|
|
640
|
+
return navigate(to, true);
|
|
641
|
+
},
|
|
642
|
+
|
|
643
|
+
back() {
|
|
644
|
+
if (typeof window !== "undefined") history.back();
|
|
645
|
+
},
|
|
646
|
+
|
|
647
|
+
forward() {
|
|
648
|
+
if (typeof window !== "undefined") history.forward();
|
|
649
|
+
},
|
|
650
|
+
|
|
651
|
+
/* Guards */
|
|
652
|
+
beforeEach(fn) {
|
|
653
|
+
beforeEach = fn;
|
|
654
|
+
},
|
|
655
|
+
|
|
656
|
+
/* Sync resolve (unchanged) */
|
|
657
|
+
resolve() {
|
|
658
|
+
const r = matchRoutes();
|
|
659
|
+
return r ? r.view : null;
|
|
660
|
+
},
|
|
661
|
+
|
|
662
|
+
/* Async resolve (NEW) */
|
|
663
|
+
resolveAsync
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
export const n = createNavigator();
|
|
668
|
+
|
|
669
|
+
const HYP = { h, e, o, s, a, r, i, n };
|
|
670
|
+
window.HYP = HYP;
|
|
671
|
+
export default HYP;
|
package/src/i.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Interactor i()/Side Act sA()
|
|
2
|
+
// Self License - 01SL
|
|
3
|
+
// HYP UI Framework
|
|
4
|
+
// Author: Hemang Tewari
|
|
5
|
+
|
|
6
|
+
export const i = (effect, explicitEI = null) => {
|
|
7
|
+
const ei = explicitEI ?? e.currentEI();
|
|
8
|
+
if (!ei) return;
|
|
9
|
+
let cleanup; // Track previous effect cleanup
|
|
10
|
+
const run = () => {
|
|
11
|
+
if (cleanup) {
|
|
12
|
+
try { cleanup(); } catch (err) { console.error("i cleanup error:", err); }
|
|
13
|
+
}
|
|
14
|
+
tr = run;
|
|
15
|
+
cleanup = effect();
|
|
16
|
+
tr = null;
|
|
17
|
+
if (cleanup) o.addEffect(ei, cleanup);
|
|
18
|
+
};
|
|
19
|
+
run();
|
|
20
|
+
};
|