ajo 0.1.30 → 0.1.31
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/LLMs.md +329 -0
- package/context.js +18 -0
- package/dist/html.cjs +1 -1
- package/dist/html.js +25 -24
- package/dist/index.cjs +1 -1
- package/dist/index.js +28 -27
- package/html.js +142 -0
- package/index.js +286 -0
- package/license +1 -1
- package/package.json +58 -54
- package/readme.md +22 -6
- package/types.ts +2 -1
package/LLMs.md
ADDED
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
# Ajo LLM Instructions
|
|
2
|
+
|
|
3
|
+
Ajo is a micro UI library using JSX and generators. **No React imports**: JSX compiles to Ajo via build config.
|
|
4
|
+
|
|
5
|
+
## Stateless Component
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
import type { Stateless, WithChildren } from 'ajo'
|
|
9
|
+
import clsx from 'clsx' // optional, for conditional classes
|
|
10
|
+
|
|
11
|
+
type Args = WithChildren<{ title: string; active?: boolean }>
|
|
12
|
+
|
|
13
|
+
const Card: Stateless<Args> = ({ title, active, children }) => ( // can destructure args here
|
|
14
|
+
<div class={clsx('card', { active })}>
|
|
15
|
+
<h3>{title}</h3>
|
|
16
|
+
{children}
|
|
17
|
+
</div>
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
// Example usage - everything goes to args:
|
|
21
|
+
<Card title="Hello" active>
|
|
22
|
+
<p>This is a card.</p>
|
|
23
|
+
</Card>
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Stateful Component
|
|
27
|
+
|
|
28
|
+
```tsx
|
|
29
|
+
import type { Stateful } from 'ajo'
|
|
30
|
+
|
|
31
|
+
type Args = { initial: number; step?: number }
|
|
32
|
+
|
|
33
|
+
const Counter: Stateful<Args, 'section'> = function* (args) { // do NOT destructure args here
|
|
34
|
+
|
|
35
|
+
// before loop: persistent state & handlers
|
|
36
|
+
|
|
37
|
+
let count = args.initial
|
|
38
|
+
let inputRef: HTMLInputElement | null = null
|
|
39
|
+
|
|
40
|
+
const inc = () => this.next(({ step = 1 }) => count += step)
|
|
41
|
+
const dec = () => this.next(() => count--)
|
|
42
|
+
|
|
43
|
+
const handleKeydown = (e: KeyboardEvent) => {
|
|
44
|
+
if (e.key === 'ArrowUp') inc()
|
|
45
|
+
else if (e.key === 'ArrowDown' && count > 0) dec()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
this.addEventListener('keydown', handleKeydown, { signal: this.signal }) // auto-cleanup on unmount
|
|
49
|
+
|
|
50
|
+
while (true) { // main render loop
|
|
51
|
+
|
|
52
|
+
try { // optional: error boundary
|
|
53
|
+
|
|
54
|
+
// fresh destructure each render
|
|
55
|
+
const { step = 1 } = args
|
|
56
|
+
|
|
57
|
+
// derived values
|
|
58
|
+
const isEven = count % 2 === 0
|
|
59
|
+
|
|
60
|
+
yield (
|
|
61
|
+
<>
|
|
62
|
+
<input
|
|
63
|
+
ref={el => inputRef = el}
|
|
64
|
+
value={count}
|
|
65
|
+
set:oninput={e => this.next(() => count = +(e.target as HTMLInputElement).value)}
|
|
66
|
+
/>
|
|
67
|
+
<button set:onclick={inc}>+{step}</button>
|
|
68
|
+
<button set:onclick={dec} disabled={count <= 0}>-</button>
|
|
69
|
+
<p memo={isEven}>Even: {isEven ? 'yes' : 'no'}</p>
|
|
70
|
+
<footer memo>Static content - rendered once</footer>
|
|
71
|
+
<div skip>{/* third-party managed DOM here */}</div>
|
|
72
|
+
</>
|
|
73
|
+
)
|
|
74
|
+
} catch (err: unknown) {
|
|
75
|
+
yield <p class="error">{err instanceof Error ? err.message : String(err)}</p>
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
Counter.is = 'section' // wrapper element (default: div)
|
|
81
|
+
Counter.attrs = { class: 'counter-wrap' } // default wrapper attributes
|
|
82
|
+
Counter.args = { step: 1 } // default args
|
|
83
|
+
|
|
84
|
+
// Example usage - special attrs apply to wrapper, rest goes to args:
|
|
85
|
+
|
|
86
|
+
let ref: ThisParameterType<typeof Counter> | null = null
|
|
87
|
+
|
|
88
|
+
<Counter
|
|
89
|
+
initial={0} step={5} // → args
|
|
90
|
+
attr:id="main" attr:class="my-counter" // → wrapper attributes (HTML attrs)
|
|
91
|
+
set:onclick={fn} // → wrapper properties (DOM props)
|
|
92
|
+
key={id} // → wrapper key
|
|
93
|
+
memo={[id]} // → wrapper memo (array)
|
|
94
|
+
memo={id} // → wrapper memo (single value)
|
|
95
|
+
memo // → wrapper memo (render once)
|
|
96
|
+
ref={el => ref = el} // → wrapper ref (el is <section> + .next()/.throw()/.return())
|
|
97
|
+
/>
|
|
98
|
+
|
|
99
|
+
ref?.next() // trigger re-render from outside
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Rules
|
|
103
|
+
|
|
104
|
+
| Topic | Rule |
|
|
105
|
+
|-------|------|
|
|
106
|
+
| **Elements** | Everything becomes HTML attributes. `set:prop` assigns DOM properties instead (`node[prop] = value`) |
|
|
107
|
+
| **Stateless** | Everything goes to `args`. Special attrs like `memo` must be applied to elements inside |
|
|
108
|
+
| **Stateful** | `key`, `memo`, `skip`, `ref`, `set:*` apply to implicit wrapper element. `attr:*` sets wrapper attributes. Rest goes to `args` |
|
|
109
|
+
| **Events** | `set:onclick`, `set:oninput`, etc. Never `onClick` |
|
|
110
|
+
| **Classes** | `class`, never `className`. Must be string, no object/array syntax. Use `clsx()` or template literals |
|
|
111
|
+
| **Styles** | `style` must be string (`style="color: red"`), not object. No special handling |
|
|
112
|
+
| **Args** | Never destructure in generator signature. Use `args` param |
|
|
113
|
+
| **Root JSX** | Use `<>...</>` in stateful to avoid double wrapper |
|
|
114
|
+
| **Re-render** | `this.next(fn?)` — returns `fn`'s result. Use `this.throw(e)` for explicit error routing |
|
|
115
|
+
| **Context** | `context<T>(fallback)` creates context. Stateless: read only. Stateful: read/write inside `while` loop |
|
|
116
|
+
| **Lists** | Always provide unique `key` on elements |
|
|
117
|
+
| **Refs** | `ref={el => ...}` on elements. Receives `null` on unmount. Stateful ref type: `ThisParameterType<typeof Component>` |
|
|
118
|
+
| **Memo** | `memo={[deps]}` array, `memo={value}` single, or just `memo` (never re-render). Skips subtree if unchanged |
|
|
119
|
+
| **Skip** | `skip` excludes children from reconciliation. Use for `set:textContent`/`set:innerHTML` or third-party maneged DOM |
|
|
120
|
+
| **Custom wrapper** | Set `.is = 'tagname'` AND TypeScript generic `Stateful<Args, 'tagname'>` for stateful components. Default is `div` (no need to set) |
|
|
121
|
+
| **Default attrs** | `.attrs = { class: '...' }` on stateful component generator function |
|
|
122
|
+
| **Default args** | `.args = { prop: value }` on stateful component generator function |
|
|
123
|
+
| **Cleanup** | `this.signal` for APIs that accept AbortSignal (fetch, addEventListener). `try/finally` for the rest |
|
|
124
|
+
| **Error recovery** | `try { ... } catch { yield error UI }` inside loop |
|
|
125
|
+
| **this** | Stateful wrapper element with `.signal`, `.next(fn?)` (returns `fn`'s result), `.throw()`, `.return()`. Type: `ThisParameterType<typeof Component>` |
|
|
126
|
+
|
|
127
|
+
## Common Patterns
|
|
128
|
+
|
|
129
|
+
```tsx
|
|
130
|
+
import type { Stateful, Stateless, WithChildren } from 'ajo'
|
|
131
|
+
import { context } from 'ajo/context'
|
|
132
|
+
|
|
133
|
+
// Async data loading
|
|
134
|
+
type LoaderArgs = { url: string }
|
|
135
|
+
|
|
136
|
+
const DataLoader: Stateful<LoaderArgs> = function* (args) {
|
|
137
|
+
|
|
138
|
+
let data: unknown = null
|
|
139
|
+
let error: Error | null = null
|
|
140
|
+
|
|
141
|
+
fetch(args.url, { signal: this.signal })
|
|
142
|
+
.then(r => r.json())
|
|
143
|
+
.then(d => this.next(() => data = d))
|
|
144
|
+
.catch(e => this.next(() => error = e))
|
|
145
|
+
|
|
146
|
+
while (true) yield (
|
|
147
|
+
<>
|
|
148
|
+
{error ? <p>Error: {error.message}</p>
|
|
149
|
+
: data ? <div>{JSON.stringify(data)}</div>
|
|
150
|
+
: <p>Loading...</p>}
|
|
151
|
+
</>
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// List with keys
|
|
156
|
+
type Item = { id: string; text: string }
|
|
157
|
+
|
|
158
|
+
const List: Stateless<{ items: Item[] }> = ({ items }) => (
|
|
159
|
+
<ul>
|
|
160
|
+
{items.map(item => <li key={item.id}>{item.text}</li>)}
|
|
161
|
+
</ul>
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
// Conditional rendering
|
|
165
|
+
type ShowArgs = WithChildren<{ when: boolean }>
|
|
166
|
+
|
|
167
|
+
const Show: Stateless<ShowArgs> = ({ when, children }) => when ? children : null
|
|
168
|
+
|
|
169
|
+
// Context - create with fallback value
|
|
170
|
+
const ThemeContext = context<'light' | 'dark'>('light')
|
|
171
|
+
const UserContext = context<{ name: string } | null>(null)
|
|
172
|
+
|
|
173
|
+
// Stateless - read only (call without args)
|
|
174
|
+
const ThemedCard: Stateless<{ title: string }> = ({ title }) => {
|
|
175
|
+
const theme = ThemeContext() // reads current value
|
|
176
|
+
return <div class={`card theme-${theme}`}>{title}</div>
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Stateful - read/write inside while loop
|
|
180
|
+
const ThemeProvider: Stateful<WithChildren> = function* (args) {
|
|
181
|
+
|
|
182
|
+
let theme: 'light' | 'dark' = 'light'
|
|
183
|
+
|
|
184
|
+
const toggle = () => this.next(() => theme = theme === 'light' ? 'dark' : 'light')
|
|
185
|
+
|
|
186
|
+
while (true) {
|
|
187
|
+
|
|
188
|
+
ThemeContext(theme) // write: sets value for descendants
|
|
189
|
+
const user = UserContext() // read: gets value from ancestor
|
|
190
|
+
|
|
191
|
+
yield (
|
|
192
|
+
<>
|
|
193
|
+
<button set:onclick={toggle}>Theme: {theme}</button>
|
|
194
|
+
{user && <span>User: {user.name}</span>}
|
|
195
|
+
{args.children}
|
|
196
|
+
</>
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Ref typing for stateful components
|
|
202
|
+
let counterRef: ThisParameterType<typeof Counter> | null = null
|
|
203
|
+
<Counter ref={el => counterRef = el} initial={0} />
|
|
204
|
+
// counterRef is <section> element + .next(), .throw(), .return() methods
|
|
205
|
+
counterRef?.next() // trigger re-render from outside
|
|
206
|
+
|
|
207
|
+
// memo variations
|
|
208
|
+
<div memo={[a, b]}>{/* re-render when a or b changes */}</div>
|
|
209
|
+
<div memo={count}>{/* re-render when count changes (single value) */}</div>
|
|
210
|
+
<div memo>{/* render once, never update - good for static content */}</div>
|
|
211
|
+
|
|
212
|
+
// Boolean attributes
|
|
213
|
+
<input type="checkbox" checked disabled /> // checked="" disabled=""
|
|
214
|
+
<button disabled={false} /> // removes disabled attr
|
|
215
|
+
|
|
216
|
+
// Attributes vs Properties (HTML-first)
|
|
217
|
+
<input value="text" /> // HTML attribute: initial value only
|
|
218
|
+
<input set:value={text} /> // DOM property: syncs with state
|
|
219
|
+
<input type="checkbox" checked /> // HTML attribute: initial state
|
|
220
|
+
<input type="checkbox" set:checked={bool} /> // DOM property: syncs with state
|
|
221
|
+
<video set:currentTime={0} set:muted /> // DOM properties
|
|
222
|
+
<div set:textContent={str} skip /> // DOM property + skip (required!)
|
|
223
|
+
<div set:innerHTML={html} skip /> // DOM property + skip (required!)
|
|
224
|
+
|
|
225
|
+
// Post-render work (DOM is updated by the time the microtask runs)
|
|
226
|
+
const ScrollList: Stateful<{ items: Item[] }> = function* (args) {
|
|
227
|
+
let container: HTMLUListElement | null = null
|
|
228
|
+
while (true) {
|
|
229
|
+
queueMicrotask(() => container!.scrollTop = container!.scrollHeight)
|
|
230
|
+
yield (
|
|
231
|
+
<ul ref={el => container = el}>
|
|
232
|
+
{args.items.map(item => <li key={item.id}>{item.text}</li>)}
|
|
233
|
+
</ul>
|
|
234
|
+
)
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Third-party managed DOM
|
|
239
|
+
let map: MapLibrary | null = null
|
|
240
|
+
<div skip ref={el => el ? (map ??= new MapLibrary(el)) : map?.destroy()} /> // skip lets library control children
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
## Anti-patterns
|
|
244
|
+
|
|
245
|
+
```tsx
|
|
246
|
+
// ❌ React patterns: NEVER use
|
|
247
|
+
import React from 'react'
|
|
248
|
+
className="..."
|
|
249
|
+
onClick={...}
|
|
250
|
+
useState, useEffect, useCallback
|
|
251
|
+
|
|
252
|
+
// ❌ class/style as object or array: no special handling in Ajo
|
|
253
|
+
<div class={{ active: isActive }} /> // won't work
|
|
254
|
+
<div class={['btn', 'primary']} /> // won't work
|
|
255
|
+
<div style={{ color: 'red' }} /> // won't work
|
|
256
|
+
|
|
257
|
+
// ✅ class/style must be strings
|
|
258
|
+
<div class={`btn ${isActive ? 'active' : ''}`} />
|
|
259
|
+
<div class={clsx('btn', { active: isActive })} /> // use clsx library
|
|
260
|
+
<div style="color: red; font-size: 14px" />
|
|
261
|
+
<div style={`color: ${color}`} />
|
|
262
|
+
|
|
263
|
+
// ❌ memo in args but not applied to element - does nothing
|
|
264
|
+
const Bad: Stateless<{ memo: unknown }> = ({ memo }) => (
|
|
265
|
+
<div>content</div> // memo arg ignored, no memoization
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
// ✅ Pass deps in args, apply to root element
|
|
269
|
+
const Good: Stateless<{ deps?: unknown }> = ({ deps }) => (
|
|
270
|
+
<div memo={deps}>content</div> // memoized when deps provided
|
|
271
|
+
)
|
|
272
|
+
// <Good deps={[id]} /> or <Good deps={data} />
|
|
273
|
+
|
|
274
|
+
// ❌ Destructure in signature - locks to initial values
|
|
275
|
+
function* Bad({ count }) { ... }
|
|
276
|
+
|
|
277
|
+
// ❌ Context read/write outside loop in stateful - stale values
|
|
278
|
+
function* Bad(args) {
|
|
279
|
+
const theme = ThemeContext() // frozen at mount
|
|
280
|
+
ThemeContext('dark') // only set once, not updated
|
|
281
|
+
while (true) yield ...
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ✅ Context read/write inside loop in stateful
|
|
285
|
+
function* Good(args) {
|
|
286
|
+
let theme = 'light'
|
|
287
|
+
while (true) {
|
|
288
|
+
ThemeContext(theme) // write: updated each render
|
|
289
|
+
const user = UserContext() // read: fresh value each render
|
|
290
|
+
yield ...
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// ❌ Missing key in lists
|
|
295
|
+
{items.map(item => <li>{item}</li>)}
|
|
296
|
+
|
|
297
|
+
// ❌ Direct state mutation without next()
|
|
298
|
+
const inc = () => count++ // won't re-render
|
|
299
|
+
|
|
300
|
+
// ❌ set:textContent/innerHTML without skip: content gets cleared
|
|
301
|
+
<div set:innerHTML={html} />
|
|
302
|
+
|
|
303
|
+
// ✅ Correct
|
|
304
|
+
const inc = () => this.next(() => count++)
|
|
305
|
+
<div set:innerHTML={html} skip />
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
## Setup
|
|
309
|
+
|
|
310
|
+
```bash
|
|
311
|
+
npm install ajo
|
|
312
|
+
pnpm add ajo
|
|
313
|
+
yarn add ajo
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
Configure JSX factory (Vite example):
|
|
317
|
+
|
|
318
|
+
```ts
|
|
319
|
+
// vite.config.ts
|
|
320
|
+
export default defineConfig({
|
|
321
|
+
esbuild: {
|
|
322
|
+
jsxFactory: 'h',
|
|
323
|
+
jsxFragment: 'Fragment',
|
|
324
|
+
jsxInject: `import { h, Fragment } from 'ajo'`,
|
|
325
|
+
},
|
|
326
|
+
})
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
For other build systems: `jsxFactory: 'h'`, `jsxFragment: 'Fragment'`, auto-import `{ h, Fragment }` from `'ajo'`.
|
package/context.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export const Context = Symbol.for('ajo.context')
|
|
2
|
+
|
|
3
|
+
export const context = (fallback, key = Symbol()) => function (...args) {
|
|
4
|
+
|
|
5
|
+
const self = this ?? component
|
|
6
|
+
|
|
7
|
+
return self
|
|
8
|
+
? args.length
|
|
9
|
+
? self[Context][key] = args[0]
|
|
10
|
+
: key in self[Context]
|
|
11
|
+
? self[Context][key]
|
|
12
|
+
: fallback
|
|
13
|
+
: fallback
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let component = null
|
|
17
|
+
|
|
18
|
+
export const current = (...args) => args.length ? (component = args[0]) : component
|
package/dist/html.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const s=require("./context.cjs"),m=new Set(["area","base","br","col","command","embed","hr","img","input","keygen","link","meta","param","source","track","wbr"]),
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const s=require("./context.cjs"),m=new Set(["area","base","br","col","command","embed","hr","img","input","keygen","link","meta","param","source","track","wbr"]),p=Symbol.for("ajo.args"),d=e=>e.replace(/[&<>"']/g,r=>`&#${r.charCodeAt(0)};`),y=()=>{},k=e=>[...u(e)].join(""),u=function*(e){for(e of c(e))typeof e=="string"?yield d(e):yield*$(e)},$=function*({nodeName:e,children:r,...i}){let o="";for(const n in i)n.startsWith("set:")||i[n]==null||i[n]===!1||(i[n]===!0?o+=` ${n}`:o+=` ${n}="${d(String(i[n]))}"`);m.has(e)?yield`<${e}${o}>`:(yield`<${e}${o}>`,r!=null&&(yield*u(r)),yield`</${e}>`)},c=function*(e){if(e==null)return;const r=typeof e;if(r!="boolean")if(r=="string")yield e;else if(r=="number"||r=="bigint")yield String(e);else if(Symbol.iterator in e)for(e of e)yield*c(e);else"nodeName"in e?typeof e.nodeName=="function"?yield*S(e):yield g(e):yield String(e)},S=function*({nodeName:e,fallback:r=e.fallback,...i}){e.constructor.name=="GeneratorFunction"?yield w(e,i):yield*c(e(i))},w=(e,r)=>{const i={...e.attrs},o={...e.args};for(const t in r)t.startsWith("attr:")?i[t.slice(5)]=r[t]:t=="key"||t=="skip"||t=="memo"||t=="ref"||t.startsWith("set:")?i[t]=r[t]:o[t]=r[t];const n=new AbortController,l={[s.Context]:Object.create(s.current()?.[s.Context]??null),[p]:o,signal:n.signal,next:y,return:y,throw:t=>{throw t}},a=e.call(l,o),b=s.current();s.current(l);const f=t=>({...i,nodeName:e.is??"div",...g({children:t})});try{return f(a.next().value)}catch(t){return f(a.throw(t).value)}finally{a.return(),n.abort(),s.current(b)}},g=({key:e,skip:r,memo:i,ref:o,...n})=>{if("children"in n){const l=[...c(n.children)];l.length?n.children=l.length==1?l[0]:l:delete n.children}return n};exports.html=u;exports.render=k;
|
package/dist/html.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { Context as
|
|
2
|
-
const p = /* @__PURE__ */ new Set(["area", "base", "br", "col", "command", "embed", "hr", "img", "input", "keygen", "link", "meta", "param", "source", "track", "wbr"]),
|
|
3
|
-
},
|
|
1
|
+
import { Context as u, current as s } from "./context.js";
|
|
2
|
+
const p = /* @__PURE__ */ new Set(["area", "base", "br", "col", "command", "embed", "hr", "img", "input", "keygen", "link", "meta", "param", "source", "track", "wbr"]), k = /* @__PURE__ */ Symbol.for("ajo.args"), d = (e) => e.replace(/[&<>"']/g, (n) => `&#${n.charCodeAt(0)};`), y = () => {
|
|
3
|
+
}, x = (e) => [...g(e)].join(""), g = function* (e) {
|
|
4
4
|
for (e of c(e))
|
|
5
|
-
typeof e == "string" ? yield
|
|
6
|
-
},
|
|
5
|
+
typeof e == "string" ? yield d(e) : yield* $(e);
|
|
6
|
+
}, $ = function* ({ nodeName: e, children: n, ...i }) {
|
|
7
7
|
let o = "";
|
|
8
8
|
for (const r in i)
|
|
9
|
-
r.startsWith("set:") || i[r] == null || i[r] === !1 || (i[r] === !0 ? o += ` ${r}` : o += ` ${r}="${
|
|
10
|
-
p.has(e) ? yield `<${e}${o}>` : (yield `<${e}${o}>`, n != null && (yield*
|
|
9
|
+
r.startsWith("set:") || i[r] == null || i[r] === !1 || (i[r] === !0 ? o += ` ${r}` : o += ` ${r}="${d(String(i[r]))}"`);
|
|
10
|
+
p.has(e) ? yield `<${e}${o}>` : (yield `<${e}${o}>`, n != null && (yield* g(n)), yield `</${e}>`);
|
|
11
11
|
}, c = function* (e) {
|
|
12
12
|
if (e == null) return;
|
|
13
13
|
const n = typeof e;
|
|
@@ -15,30 +15,31 @@ const p = /* @__PURE__ */ new Set(["area", "base", "br", "col", "command", "embe
|
|
|
15
15
|
if (n == "string") yield e;
|
|
16
16
|
else if (n == "number" || n == "bigint") yield String(e);
|
|
17
17
|
else if (Symbol.iterator in e) for (e of e) yield* c(e);
|
|
18
|
-
else "nodeName" in e ? typeof e.nodeName == "function" ? yield*
|
|
19
|
-
},
|
|
20
|
-
e.constructor.name == "GeneratorFunction" ? yield
|
|
21
|
-
},
|
|
18
|
+
else "nodeName" in e ? typeof e.nodeName == "function" ? yield* w(e) : yield m(e) : yield String(e);
|
|
19
|
+
}, w = function* ({ nodeName: e, fallback: n = e.fallback, ...i }) {
|
|
20
|
+
e.constructor.name == "GeneratorFunction" ? yield S(e, i) : yield* c(e(i));
|
|
21
|
+
}, S = (e, n) => {
|
|
22
22
|
const i = { ...e.attrs }, o = { ...e.args };
|
|
23
23
|
for (const t in n)
|
|
24
24
|
t.startsWith("attr:") ? i[t.slice(5)] = n[t] : t == "key" || t == "skip" || t == "memo" || t == "ref" || t.startsWith("set:") ? i[t] = n[t] : o[t] = n[t];
|
|
25
|
-
const r = {
|
|
26
|
-
[
|
|
27
|
-
[
|
|
28
|
-
|
|
29
|
-
|
|
25
|
+
const r = new AbortController(), l = {
|
|
26
|
+
[u]: Object.create(s()?.[u] ?? null),
|
|
27
|
+
[k]: o,
|
|
28
|
+
signal: r.signal,
|
|
29
|
+
next: y,
|
|
30
|
+
return: y,
|
|
30
31
|
throw: (t) => {
|
|
31
32
|
throw t;
|
|
32
33
|
}
|
|
33
|
-
},
|
|
34
|
-
s(
|
|
35
|
-
const
|
|
34
|
+
}, a = e.call(l, o), b = s();
|
|
35
|
+
s(l);
|
|
36
|
+
const f = (t) => ({ ...i, nodeName: e.is ?? "div", ...m({ children: t }) });
|
|
36
37
|
try {
|
|
37
|
-
return a
|
|
38
|
+
return f(a.next().value);
|
|
38
39
|
} catch (t) {
|
|
39
|
-
return a
|
|
40
|
+
return f(a.throw(t).value);
|
|
40
41
|
} finally {
|
|
41
|
-
|
|
42
|
+
a.return(), r.abort(), s(b);
|
|
42
43
|
}
|
|
43
44
|
}, m = ({ key: e, skip: n, memo: i, ref: o, ...r }) => {
|
|
44
45
|
if ("children" in r) {
|
|
@@ -48,6 +49,6 @@ const p = /* @__PURE__ */ new Set(["area", "base", "br", "col", "command", "embe
|
|
|
48
49
|
return r;
|
|
49
50
|
};
|
|
50
51
|
export {
|
|
51
|
-
|
|
52
|
-
|
|
52
|
+
g as html,
|
|
53
|
+
x as render
|
|
53
54
|
};
|
package/dist/index.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const a=require("./context.cjs"),y=Symbol.for("ajo.key"),j=Symbol.for("ajo.memo"),g=Symbol.for("ajo.ref"),A=Symbol.for("ajo.cache"),c=Symbol.for("ajo.generator"),o=Symbol.for("ajo.iterator"),b=Symbol.for("ajo.render"),l=Symbol.for("ajo.args"),p=Symbol.for("ajo.controller"),N=e=>e.children,k=(e,t,...r)=>((t??={}).nodeName=e,!("children"in t)&&r.length&&(t.children=r.length==1?r[0]:r),t),f=(e,t,r=t.firstChild,s=null)=>{for(e of S(e)){const n=E(e,t,r);r==null?m(t,n,s):n==r?r=n.nextSibling:n==r.nextSibling?(m(t,r,s),r=n.nextSibling):m(t,n,r)}for(;r!=s;){const n=r.nextSibling;r.nodeType==1&&C(r),t.removeChild(r),r=n}},S=function*(e){if(e==null)return;const t=typeof e;if(t!="boolean")if(t=="string")yield e;else if(t=="number"||t=="bigint")yield String(e);else if(Symbol.iterator in e)for(e of e)yield*S(e);else"nodeName"in e?typeof e.nodeName=="function"?yield*O(e):yield e:yield String(e)},O=function*({nodeName:e,...t}){e.constructor.name=="GeneratorFunction"?yield T(e,t):yield*S(e(t))},T=(e,t)=>{const r={...e.attrs},s={...e.args};for(const n in t)n.startsWith("attr:")?r[n.slice(5)]=t[n]:n=="key"||n=="skip"||n=="memo"||n=="ref"||n.startsWith("set:")?r[n]=t[n]:s[n]=t[n];return{...r,nodeName:e.is??"div",[c]:e,[l]:s}},E=(e,t,r)=>typeof e=="string"?F(e,r):G(e,t,r),F=(e,t)=>{for(;t&&t.nodeType!=3;)t=t.nextSibling;return t?t.data!=e&&(t.data=e):t=document.createTextNode(e),t},G=({nodeName:e,children:t,key:r,skip:s,memo:n,ref:x,[c]:u,[l]:h,...w},v,i)=>{for(;i&&(i.localName!=e||i[y]!=null&&i[y]!=r||i[c]&&i[c]!=u);)i=i.nextSibling;return i??=document.createElementNS(w.xmlns??v.namespaceURI,e),r!=null&&(i[y]=r),(n==null||W(i[j],i[j]=n))&&(R(i[A]??B(i),i[A]=w,i),s||(u?I(u,h,i):f(t,i)),typeof x=="function"&&(i[g]=x)(i)),i},R=(e,t,r)=>{for(const s in{...e,...t})e[s]!==t[s]&&(s.startsWith("set:")?r[s.slice(4)]=t[s]:t[s]==null||t[s]===!1?r.removeAttribute(s):r.setAttribute(s,t[s]===!0?"":t[s]))},W=(e,t)=>Array.isArray(e)&&Array.isArray(t)?e.some((r,s)=>r!==t[s]):e!==t,B=e=>Array.from(e.attributes).reduce((t,r)=>(t[r.name]=r.value,t),{}),m=(e,t,r)=>{if(t.contains(document.activeElement)){const s=t.nextSibling;for(;r&&r!=t;){const n=r.nextSibling;e.insertBefore(r,s),r=n}}else e.insertBefore(t,r)},C=e=>{for(const t of e.children)C(t);typeof e.return=="function"&&e.return(),e[g]?.(null)},I=(e,t,r)=>{r[c]??=(M(r),e),Object.assign(r[l]??={},t),r[b]()},M=e=>{Object.assign(e,q),e[a.Context]=Object.create(a.current()?.[a.Context]??null)},q={[b](){const e=a.current();a.current(this);try{this[o]||(this.signal=(this[p]=new AbortController).signal,this[o]=this[c].call(this,this[l]));const{value:t,done:r}=this[o].next();f(t,this),this[g]?.(this),r&&this.return()}catch(t){this.throw(t)}finally{a.current(e)}},next(e,t){try{typeof e=="function"&&(t=e.call(this,this[l]))}catch(r){return this.throw(r)}return a.current()?.contains(this)||this[b](),t},throw(e){for(let t=this;t;t=t.parentNode)if(t[o]?.throw)try{return f(t[o].throw(e).value,t)}catch(r){e=new Error(r?.message??r,{cause:e})}throw e},return(){try{this[o]?.return()}catch(e){this.throw(e)}finally{this[o]=null,this[p]?.abort()}}};exports.Fragment=N;exports.h=k;exports.render=f;
|
package/dist/index.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { Context as j, current as
|
|
2
|
-
const u = /* @__PURE__ */ Symbol.for("ajo.key"), p = /* @__PURE__ */ Symbol.for("ajo.memo"), g = /* @__PURE__ */ Symbol.for("ajo.ref"), A = /* @__PURE__ */ Symbol.for("ajo.cache"),
|
|
1
|
+
import { Context as j, current as l } from "./context.js";
|
|
2
|
+
const u = /* @__PURE__ */ Symbol.for("ajo.key"), p = /* @__PURE__ */ Symbol.for("ajo.memo"), g = /* @__PURE__ */ Symbol.for("ajo.ref"), A = /* @__PURE__ */ Symbol.for("ajo.cache"), a = /* @__PURE__ */ Symbol.for("ajo.generator"), o = /* @__PURE__ */ Symbol.for("ajo.iterator"), m = /* @__PURE__ */ Symbol.for("ajo.render"), c = /* @__PURE__ */ Symbol.for("ajo.args"), C = /* @__PURE__ */ Symbol.for("ajo.controller"), U = (e) => e.children, q = (e, t, ...r) => ((t ??= {}).nodeName = e, !("children" in t) && r.length && (t.children = r.length == 1 ? r[0] : r), t), b = (e, t, r = t.firstChild, s = null) => {
|
|
3
3
|
for (e of S(e)) {
|
|
4
4
|
const n = G(e, t, r);
|
|
5
5
|
r == null ? y(t, n, s) : n == r ? r = n.nextSibling : n == r.nextSibling ? (y(t, r, s), r = n.nextSibling) : y(t, n, r);
|
|
6
6
|
}
|
|
7
7
|
for (; r != s; ) {
|
|
8
8
|
const n = r.nextSibling;
|
|
9
|
-
r.nodeType == 1 &&
|
|
9
|
+
r.nodeType == 1 && h(r), t.removeChild(r), r = n;
|
|
10
10
|
}
|
|
11
11
|
}, S = function* (e) {
|
|
12
12
|
if (e == null) return;
|
|
@@ -15,20 +15,20 @@ const u = /* @__PURE__ */ Symbol.for("ajo.key"), p = /* @__PURE__ */ Symbol.for(
|
|
|
15
15
|
if (t == "string") yield e;
|
|
16
16
|
else if (t == "number" || t == "bigint") yield String(e);
|
|
17
17
|
else if (Symbol.iterator in e) for (e of e) yield* S(e);
|
|
18
|
-
else "nodeName" in e ? typeof e.nodeName == "function" ? yield*
|
|
19
|
-
},
|
|
18
|
+
else "nodeName" in e ? typeof e.nodeName == "function" ? yield* v(e) : yield e : yield String(e);
|
|
19
|
+
}, v = function* ({ nodeName: e, ...t }) {
|
|
20
20
|
e.constructor.name == "GeneratorFunction" ? yield E(e, t) : yield* S(e(t));
|
|
21
21
|
}, E = (e, t) => {
|
|
22
22
|
const r = { ...e.attrs }, s = { ...e.args };
|
|
23
23
|
for (const n in t)
|
|
24
24
|
n.startsWith("attr:") ? r[n.slice(5)] = t[n] : n == "key" || n == "skip" || n == "memo" || n == "ref" || n.startsWith("set:") ? r[n] = t[n] : s[n] = t[n];
|
|
25
|
-
return { ...r, nodeName: e.is ?? "div", [
|
|
25
|
+
return { ...r, nodeName: e.is ?? "div", [a]: e, [c]: s };
|
|
26
26
|
}, G = (e, t, r) => typeof e == "string" ? O(e, r) : R(e, t, r), O = (e, t) => {
|
|
27
27
|
for (; t && t.nodeType != 3; ) t = t.nextSibling;
|
|
28
28
|
return t ? t.data != e && (t.data = e) : t = document.createTextNode(e), t;
|
|
29
|
-
}, R = ({ nodeName: e, children: t, key: r, skip: s, memo: n, ref: x, [
|
|
30
|
-
for (; i && (i.localName != e || i[u] != null && i[u] != r || i[
|
|
31
|
-
return i ??= document.createElementNS(w.xmlns ??
|
|
29
|
+
}, R = ({ nodeName: e, children: t, key: r, skip: s, memo: n, ref: x, [a]: f, [c]: N, ...w }, k, i) => {
|
|
30
|
+
for (; i && (i.localName != e || i[u] != null && i[u] != r || i[a] && i[a] != f); ) i = i.nextSibling;
|
|
31
|
+
return i ??= document.createElementNS(w.xmlns ?? k.namespaceURI, e), r != null && (i[u] = r), (n == null || W(i[p], i[p] = n)) && (T(i[A] ?? B(i), i[A] = w, i), s || (f ? F(f, N, i) : b(t, i)), typeof x == "function" && (i[g] = x)(i)), i;
|
|
32
32
|
}, T = (e, t, r) => {
|
|
33
33
|
for (const s in { ...e, ...t })
|
|
34
34
|
e[s] !== t[s] && (s.startsWith("set:") ? r[s.slice(4)] = t[s] : t[s] == null || t[s] === !1 ? r.removeAttribute(s) : r.setAttribute(s, t[s] === !0 ? "" : t[s]));
|
|
@@ -40,37 +40,38 @@ const u = /* @__PURE__ */ Symbol.for("ajo.key"), p = /* @__PURE__ */ Symbol.for(
|
|
|
40
40
|
e.insertBefore(r, s), r = n;
|
|
41
41
|
}
|
|
42
42
|
} else e.insertBefore(t, r);
|
|
43
|
-
},
|
|
44
|
-
for (const t of e.children)
|
|
43
|
+
}, h = (e) => {
|
|
44
|
+
for (const t of e.children) h(t);
|
|
45
45
|
typeof e.return == "function" && e.return(), e[g]?.(null);
|
|
46
46
|
}, F = (e, t, r) => {
|
|
47
|
-
r[
|
|
47
|
+
r[a] ??= (I(r), e), Object.assign(r[c] ??= {}, t), r[m]();
|
|
48
48
|
}, I = (e) => {
|
|
49
|
-
Object.assign(e, K), e[j] = Object.create(
|
|
49
|
+
Object.assign(e, K), e[j] = Object.create(l()?.[j] ?? null);
|
|
50
50
|
}, K = {
|
|
51
51
|
[m]() {
|
|
52
|
-
const e =
|
|
53
|
-
|
|
52
|
+
const e = l();
|
|
53
|
+
l(this);
|
|
54
54
|
try {
|
|
55
|
-
|
|
55
|
+
this[o] || (this.signal = (this[C] = new AbortController()).signal, this[o] = this[a].call(this, this[c]));
|
|
56
|
+
const { value: t, done: r } = this[o].next();
|
|
56
57
|
b(t, this), this[g]?.(this), r && this.return();
|
|
57
58
|
} catch (t) {
|
|
58
59
|
this.throw(t);
|
|
59
60
|
} finally {
|
|
60
|
-
|
|
61
|
+
l(e);
|
|
61
62
|
}
|
|
62
63
|
},
|
|
63
|
-
next(e) {
|
|
64
|
+
next(e, t) {
|
|
64
65
|
try {
|
|
65
|
-
e
|
|
66
|
-
} catch (
|
|
67
|
-
return this.throw(
|
|
66
|
+
typeof e == "function" && (t = e.call(this, this[c]));
|
|
67
|
+
} catch (r) {
|
|
68
|
+
return this.throw(r);
|
|
68
69
|
}
|
|
69
|
-
|
|
70
|
+
return l()?.contains(this) || this[m](), t;
|
|
70
71
|
},
|
|
71
72
|
throw(e) {
|
|
72
|
-
for (let t = this; t; t = t.parentNode) if (t[
|
|
73
|
-
return b(t[
|
|
73
|
+
for (let t = this; t; t = t.parentNode) if (t[o]?.throw) try {
|
|
74
|
+
return b(t[o].throw(e).value, t);
|
|
74
75
|
} catch (r) {
|
|
75
76
|
e = new Error(r?.message ?? r, { cause: e });
|
|
76
77
|
}
|
|
@@ -78,16 +79,16 @@ const u = /* @__PURE__ */ Symbol.for("ajo.key"), p = /* @__PURE__ */ Symbol.for(
|
|
|
78
79
|
},
|
|
79
80
|
return() {
|
|
80
81
|
try {
|
|
81
|
-
this[
|
|
82
|
+
this[o]?.return();
|
|
82
83
|
} catch (e) {
|
|
83
84
|
this.throw(e);
|
|
84
85
|
} finally {
|
|
85
|
-
this[
|
|
86
|
+
this[o] = null, this[C]?.abort();
|
|
86
87
|
}
|
|
87
88
|
}
|
|
88
89
|
};
|
|
89
90
|
export {
|
|
90
91
|
U as Fragment,
|
|
91
|
-
h,
|
|
92
|
+
q as h,
|
|
92
93
|
b as render
|
|
93
94
|
};
|
package/html.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { Context, current } from 'ajo/context'
|
|
2
|
+
|
|
3
|
+
const Void = new Set(['area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr'])
|
|
4
|
+
|
|
5
|
+
const Args = Symbol.for('ajo.args')
|
|
6
|
+
|
|
7
|
+
const escape = s => s.replace(/[&<>"']/g, c => `&#${c.charCodeAt(0)};`)
|
|
8
|
+
|
|
9
|
+
const noop = () => { }
|
|
10
|
+
|
|
11
|
+
export const render = h => [...html(h)].join('')
|
|
12
|
+
|
|
13
|
+
export const html = function* (h) {
|
|
14
|
+
|
|
15
|
+
for (h of walk(h)) {
|
|
16
|
+
|
|
17
|
+
if (typeof h == 'string') yield escape(h)
|
|
18
|
+
|
|
19
|
+
else yield* element(h)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const element = function* ({ nodeName, children, ...h }) {
|
|
24
|
+
|
|
25
|
+
let attrs = ''
|
|
26
|
+
|
|
27
|
+
for (const key in h) {
|
|
28
|
+
|
|
29
|
+
if (key.startsWith('set:') || h[key] == null || h[key] === false) continue
|
|
30
|
+
|
|
31
|
+
if (h[key] === true) attrs += ` ${key}`
|
|
32
|
+
|
|
33
|
+
else attrs += ` ${key}="${escape(String(h[key]))}"`
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (Void.has(nodeName)) yield `<${nodeName}${attrs}>`
|
|
37
|
+
|
|
38
|
+
else {
|
|
39
|
+
|
|
40
|
+
yield `<${nodeName}${attrs}>`
|
|
41
|
+
|
|
42
|
+
if (children != null) yield* html(children)
|
|
43
|
+
|
|
44
|
+
yield `</${nodeName}>`
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const walk = function* (h) {
|
|
49
|
+
|
|
50
|
+
if (h == null) return
|
|
51
|
+
|
|
52
|
+
const type = typeof h
|
|
53
|
+
|
|
54
|
+
if (type == 'boolean') return
|
|
55
|
+
|
|
56
|
+
if (type == 'string') yield h
|
|
57
|
+
|
|
58
|
+
else if (type == 'number' || type == 'bigint') yield String(h)
|
|
59
|
+
|
|
60
|
+
else if (Symbol.iterator in h) for (h of h) yield* walk(h)
|
|
61
|
+
|
|
62
|
+
else if ('nodeName' in h) typeof h.nodeName == 'function' ? yield* run(h) : yield vdom(h)
|
|
63
|
+
|
|
64
|
+
else yield String(h)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const run = function* ({ nodeName, fallback = nodeName.fallback, ...h }) {
|
|
68
|
+
|
|
69
|
+
if (nodeName.constructor.name == 'GeneratorFunction') yield runGenerator(nodeName, h)
|
|
70
|
+
|
|
71
|
+
else yield* walk(nodeName(h))
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const runGenerator = (fn, h) => {
|
|
75
|
+
|
|
76
|
+
const attrs = { ...fn.attrs }, args = { ...fn.args }
|
|
77
|
+
|
|
78
|
+
for (const key in h) {
|
|
79
|
+
|
|
80
|
+
if (key.startsWith('attr:')) attrs[key.slice(5)] = h[key]
|
|
81
|
+
|
|
82
|
+
else if (key == 'key' || key == 'skip' || key == 'memo' || key == 'ref' || key.startsWith('set:')) attrs[key] = h[key]
|
|
83
|
+
|
|
84
|
+
else args[key] = h[key]
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const controller = new AbortController()
|
|
88
|
+
|
|
89
|
+
const instance = {
|
|
90
|
+
|
|
91
|
+
[Context]: Object.create(current()?.[Context] ?? null),
|
|
92
|
+
|
|
93
|
+
[Args]: args,
|
|
94
|
+
|
|
95
|
+
signal: controller.signal,
|
|
96
|
+
|
|
97
|
+
next: noop,
|
|
98
|
+
|
|
99
|
+
return: noop,
|
|
100
|
+
|
|
101
|
+
throw: value => { throw value }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const iterator = fn.call(instance, args)
|
|
105
|
+
|
|
106
|
+
const parent = current()
|
|
107
|
+
|
|
108
|
+
current(instance)
|
|
109
|
+
|
|
110
|
+
const result = children => ({ ...attrs, nodeName: fn.is ?? 'div', ...vdom({ children }) })
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
|
|
114
|
+
return result(iterator.next().value)
|
|
115
|
+
|
|
116
|
+
} catch (error) {
|
|
117
|
+
|
|
118
|
+
return result(iterator.throw(error).value)
|
|
119
|
+
|
|
120
|
+
} finally {
|
|
121
|
+
|
|
122
|
+
iterator.return()
|
|
123
|
+
|
|
124
|
+
controller.abort()
|
|
125
|
+
|
|
126
|
+
current(parent)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const vdom = ({ key, skip, memo, ref, ...h }) => {
|
|
131
|
+
|
|
132
|
+
if ('children' in h) {
|
|
133
|
+
|
|
134
|
+
const children = [...walk(h.children)]
|
|
135
|
+
|
|
136
|
+
if (children.length) h.children = children.length == 1 ? children[0] : children
|
|
137
|
+
|
|
138
|
+
else delete h.children
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return h
|
|
142
|
+
}
|
package/index.js
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { Context, current } from 'ajo/context'
|
|
2
|
+
|
|
3
|
+
const Key = Symbol.for('ajo.key')
|
|
4
|
+
const Memo = Symbol.for('ajo.memo')
|
|
5
|
+
const Ref = Symbol.for('ajo.ref')
|
|
6
|
+
const Cache = Symbol.for('ajo.cache')
|
|
7
|
+
const Generator = Symbol.for('ajo.generator')
|
|
8
|
+
const Iterator = Symbol.for('ajo.iterator')
|
|
9
|
+
const Render = Symbol.for('ajo.render')
|
|
10
|
+
const Args = Symbol.for('ajo.args')
|
|
11
|
+
const Controller = Symbol.for('ajo.controller')
|
|
12
|
+
|
|
13
|
+
export const Fragment = props => props.children
|
|
14
|
+
|
|
15
|
+
export const h = (type, props, ...children) => {
|
|
16
|
+
|
|
17
|
+
(props ??= {}).nodeName = type
|
|
18
|
+
|
|
19
|
+
if (!('children' in props) && children.length) props.children = children.length == 1 ? children[0] : children
|
|
20
|
+
|
|
21
|
+
return props
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const render = (h, el, child = el.firstChild, ref = null) => {
|
|
25
|
+
|
|
26
|
+
for (h of walk(h)) {
|
|
27
|
+
|
|
28
|
+
const node = reconcile(h, el, child)
|
|
29
|
+
|
|
30
|
+
if (child == null) {
|
|
31
|
+
|
|
32
|
+
before(el, node, ref)
|
|
33
|
+
|
|
34
|
+
} else if (node == child) {
|
|
35
|
+
|
|
36
|
+
child = node.nextSibling
|
|
37
|
+
|
|
38
|
+
} else if (node == child.nextSibling) {
|
|
39
|
+
|
|
40
|
+
before(el, child, ref)
|
|
41
|
+
|
|
42
|
+
child = node.nextSibling
|
|
43
|
+
|
|
44
|
+
} else {
|
|
45
|
+
|
|
46
|
+
before(el, node, child)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
while (child != ref) {
|
|
51
|
+
|
|
52
|
+
const node = child.nextSibling
|
|
53
|
+
|
|
54
|
+
if (child.nodeType == 1) unref(child)
|
|
55
|
+
|
|
56
|
+
el.removeChild(child)
|
|
57
|
+
|
|
58
|
+
child = node
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const walk = function* (h) {
|
|
63
|
+
|
|
64
|
+
if (h == null) return
|
|
65
|
+
|
|
66
|
+
const type = typeof h
|
|
67
|
+
|
|
68
|
+
if (type == 'boolean') return
|
|
69
|
+
|
|
70
|
+
if (type == 'string') yield h
|
|
71
|
+
|
|
72
|
+
else if (type == 'number' || type == 'bigint') yield String(h)
|
|
73
|
+
|
|
74
|
+
else if (Symbol.iterator in h) for (h of h) yield* walk(h)
|
|
75
|
+
|
|
76
|
+
else if ('nodeName' in h) typeof h.nodeName == 'function' ? yield* run(h) : yield h
|
|
77
|
+
|
|
78
|
+
else yield String(h)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const run = function* ({ nodeName, ...h }) {
|
|
82
|
+
|
|
83
|
+
if (nodeName.constructor.name == 'GeneratorFunction') yield runGenerator(nodeName, h)
|
|
84
|
+
|
|
85
|
+
else yield* walk(nodeName(h))
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const runGenerator = (fn, h) => {
|
|
89
|
+
|
|
90
|
+
const attrs = { ...fn.attrs }, args = { ...fn.args }
|
|
91
|
+
|
|
92
|
+
for (const key in h) {
|
|
93
|
+
|
|
94
|
+
if (key.startsWith('attr:')) attrs[key.slice(5)] = h[key]
|
|
95
|
+
|
|
96
|
+
else if (key == 'key' || key == 'skip' || key == 'memo' || key == 'ref' || key.startsWith('set:')) attrs[key] = h[key]
|
|
97
|
+
|
|
98
|
+
else args[key] = h[key]
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return { ...attrs, nodeName: fn.is ?? 'div', [Generator]: fn, [Args]: args }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const reconcile = (h, el, node) => typeof h == 'string' ? text(h, node) : element(h, el, node)
|
|
105
|
+
|
|
106
|
+
const text = (h, node) => {
|
|
107
|
+
|
|
108
|
+
while (node && node.nodeType != 3) node = node.nextSibling
|
|
109
|
+
|
|
110
|
+
node ? node.data != h && (node.data = h) : node = document.createTextNode(h)
|
|
111
|
+
|
|
112
|
+
return node
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const element = ({ nodeName, children, key, skip, memo, ref, [Generator]: gen, [Args]: args, ...h }, el, node) => {
|
|
116
|
+
|
|
117
|
+
while (node && (
|
|
118
|
+
|
|
119
|
+
(node.localName != nodeName) ||
|
|
120
|
+
|
|
121
|
+
(node[Key] != null && node[Key] != key) ||
|
|
122
|
+
|
|
123
|
+
(node[Generator] && node[Generator] != gen)
|
|
124
|
+
|
|
125
|
+
)) node = node.nextSibling
|
|
126
|
+
|
|
127
|
+
node ??= document.createElementNS(h.xmlns ?? el.namespaceURI, nodeName)
|
|
128
|
+
|
|
129
|
+
if (key != null) node[Key] = key
|
|
130
|
+
|
|
131
|
+
if (memo == null || some(node[Memo], node[Memo] = memo)) {
|
|
132
|
+
|
|
133
|
+
attrs(node[Cache] ?? extract(node), node[Cache] = h, node)
|
|
134
|
+
|
|
135
|
+
if (!skip) gen ? next(gen, args, node) : render(children, node)
|
|
136
|
+
|
|
137
|
+
if (typeof ref == 'function') (node[Ref] = ref)(node)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return node
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const attrs = (cache, h, node) => {
|
|
144
|
+
|
|
145
|
+
for (const key in { ...cache, ...h }) {
|
|
146
|
+
|
|
147
|
+
if (cache[key] === h[key]) continue
|
|
148
|
+
|
|
149
|
+
if (key.startsWith('set:')) node[key.slice(4)] = h[key]
|
|
150
|
+
|
|
151
|
+
else if (h[key] == null || h[key] === false) node.removeAttribute(key)
|
|
152
|
+
|
|
153
|
+
else node.setAttribute(key, h[key] === true ? '' : h[key])
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const some = (a, b) => Array.isArray(a) && Array.isArray(b) ? a.some((v, i) => v !== b[i]) : a !== b
|
|
158
|
+
|
|
159
|
+
const extract = el => Array.from(el.attributes).reduce((out, attr) => (out[attr.name] = attr.value, out), {})
|
|
160
|
+
|
|
161
|
+
const before = (el, node, child) => {
|
|
162
|
+
|
|
163
|
+
if (node.contains(document.activeElement)) {
|
|
164
|
+
|
|
165
|
+
const ref = node.nextSibling
|
|
166
|
+
|
|
167
|
+
while (child && child != node) {
|
|
168
|
+
|
|
169
|
+
const next = child.nextSibling
|
|
170
|
+
|
|
171
|
+
el.insertBefore(child, ref)
|
|
172
|
+
|
|
173
|
+
child = next
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
} else el.insertBefore(node, child)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const unref = node => {
|
|
180
|
+
|
|
181
|
+
for (const child of node.children) unref(child)
|
|
182
|
+
|
|
183
|
+
if (typeof node.return == 'function') node.return()
|
|
184
|
+
|
|
185
|
+
node[Ref]?.(null)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const next = (fn, args, el) => {
|
|
189
|
+
|
|
190
|
+
el[Generator] ??= (attach(el), fn)
|
|
191
|
+
|
|
192
|
+
Object.assign(el[Args] ??= {}, args)
|
|
193
|
+
|
|
194
|
+
el[Render]()
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const attach = el => {
|
|
198
|
+
|
|
199
|
+
Object.assign(el, methods)
|
|
200
|
+
|
|
201
|
+
el[Context] = Object.create(current()?.[Context] ?? null)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const methods = {
|
|
205
|
+
|
|
206
|
+
[Render]() {
|
|
207
|
+
|
|
208
|
+
const parent = current()
|
|
209
|
+
|
|
210
|
+
current(this)
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
|
|
214
|
+
if (!this[Iterator]) {
|
|
215
|
+
|
|
216
|
+
this.signal = (this[Controller] = new AbortController()).signal
|
|
217
|
+
|
|
218
|
+
this[Iterator] = this[Generator].call(this, this[Args])
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const { value, done } = this[Iterator].next()
|
|
222
|
+
|
|
223
|
+
render(value, this)
|
|
224
|
+
|
|
225
|
+
this[Ref]?.(this)
|
|
226
|
+
|
|
227
|
+
if (done) this.return()
|
|
228
|
+
|
|
229
|
+
} catch (e) {
|
|
230
|
+
|
|
231
|
+
this.throw(e)
|
|
232
|
+
|
|
233
|
+
} finally {
|
|
234
|
+
|
|
235
|
+
current(parent)
|
|
236
|
+
}
|
|
237
|
+
},
|
|
238
|
+
|
|
239
|
+
next(fn, result) {
|
|
240
|
+
|
|
241
|
+
try {
|
|
242
|
+
|
|
243
|
+
if (typeof fn == 'function') result = fn.call(this, this[Args])
|
|
244
|
+
|
|
245
|
+
} catch (e) {
|
|
246
|
+
|
|
247
|
+
return this.throw(e)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (!current()?.contains(this)) this[Render]()
|
|
251
|
+
|
|
252
|
+
return result
|
|
253
|
+
},
|
|
254
|
+
|
|
255
|
+
throw(value) {
|
|
256
|
+
|
|
257
|
+
for (let el = this; el; el = el.parentNode) if (el[Iterator]?.throw) try {
|
|
258
|
+
|
|
259
|
+
return render(el[Iterator].throw(value).value, el)
|
|
260
|
+
|
|
261
|
+
} catch (e) {
|
|
262
|
+
|
|
263
|
+
value = new Error(e?.message ?? e, { cause: value })
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
throw value
|
|
267
|
+
},
|
|
268
|
+
|
|
269
|
+
return() {
|
|
270
|
+
|
|
271
|
+
try {
|
|
272
|
+
|
|
273
|
+
this[Iterator]?.return()
|
|
274
|
+
|
|
275
|
+
} catch (e) {
|
|
276
|
+
|
|
277
|
+
this.throw(e)
|
|
278
|
+
|
|
279
|
+
} finally {
|
|
280
|
+
|
|
281
|
+
this[Iterator] = null
|
|
282
|
+
|
|
283
|
+
this[Controller]?.abort()
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
package/license
CHANGED
package/package.json
CHANGED
|
@@ -1,55 +1,59 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
2
|
+
"name": "ajo",
|
|
3
|
+
"version": "0.1.31",
|
|
4
|
+
"description": "ajo is a JavaScript view library for building user interfaces",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"types": "./types.ts",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"main": "./dist/index.cjs",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./types.ts",
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"require": "./dist/index.cjs"
|
|
14
|
+
},
|
|
15
|
+
"./context": {
|
|
16
|
+
"types": "./types.ts",
|
|
17
|
+
"import": "./dist/context.js",
|
|
18
|
+
"require": "./dist/context.cjs"
|
|
19
|
+
},
|
|
20
|
+
"./html": {
|
|
21
|
+
"types": "./types.ts",
|
|
22
|
+
"import": "./dist/html.js",
|
|
23
|
+
"require": "./dist/html.cjs"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist",
|
|
28
|
+
"context.js",
|
|
29
|
+
"html.js",
|
|
30
|
+
"index.js",
|
|
31
|
+
"LLMs.md",
|
|
32
|
+
"types.ts"
|
|
33
|
+
],
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/node": "25.2.3",
|
|
36
|
+
"happy-dom": "20.6.1",
|
|
37
|
+
"vite": "7.3.1",
|
|
38
|
+
"vite-tsconfig-paths": "6.1.1",
|
|
39
|
+
"vitest": "4.0.18"
|
|
40
|
+
},
|
|
41
|
+
"keywords": [
|
|
42
|
+
"ui",
|
|
43
|
+
"frontend",
|
|
44
|
+
"web",
|
|
45
|
+
"dom",
|
|
46
|
+
"jsx"
|
|
47
|
+
],
|
|
48
|
+
"repository": "cristianfalcone/ajo",
|
|
49
|
+
"author": "Cristian Falcone",
|
|
50
|
+
"license": "ISC",
|
|
51
|
+
"bugs": "https://github.com/cristianfalcone/ajo/issues",
|
|
52
|
+
"homepage": "https://github.com/cristianfalcone/ajo#readme",
|
|
53
|
+
"scripts": {
|
|
54
|
+
"test": "vitest --run",
|
|
55
|
+
"build": "vite build",
|
|
56
|
+
"bump": "pnpm version patch && git push && git push --tags",
|
|
57
|
+
"release": "pnpm test && pnpm build && pnpm bump && pnpm publish"
|
|
58
|
+
}
|
|
59
|
+
}
|
package/readme.md
CHANGED
|
@@ -125,7 +125,7 @@ function* TodoList() {
|
|
|
125
125
|
|
|
126
126
|
### Re-rendering with `this.next()`
|
|
127
127
|
|
|
128
|
-
Call `this.next()` to trigger a re-render. The optional callback receives current props
|
|
128
|
+
Call `this.next()` to trigger a re-render. The optional callback receives current props and its return value is passed through:
|
|
129
129
|
|
|
130
130
|
```javascript
|
|
131
131
|
function* Stepper(args) {
|
|
@@ -158,7 +158,22 @@ function* Good(args) {
|
|
|
158
158
|
|
|
159
159
|
### Lifecycle and Cleanup
|
|
160
160
|
|
|
161
|
-
|
|
161
|
+
Every stateful component has a `this.signal` (AbortSignal) that aborts when the component unmounts. Use it with any API that accepts a signal:
|
|
162
|
+
|
|
163
|
+
```javascript
|
|
164
|
+
function* MouseTracker() {
|
|
165
|
+
|
|
166
|
+
let pos = { x: 0, y: 0 }
|
|
167
|
+
|
|
168
|
+
document.addEventListener('mousemove', e => this.next(() => {
|
|
169
|
+
pos = { x: e.clientX, y: e.clientY }
|
|
170
|
+
}), { signal: this.signal }) // auto-removed on unmount
|
|
171
|
+
|
|
172
|
+
while (true) yield <p>{pos.x}, {pos.y}</p>
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
For APIs that don't accept a signal, use `try...finally`:
|
|
162
177
|
|
|
163
178
|
```javascript
|
|
164
179
|
function* Clock() {
|
|
@@ -329,7 +344,7 @@ function* UserProfile(args) {
|
|
|
329
344
|
|
|
330
345
|
let data = null, error = null, loading = true
|
|
331
346
|
|
|
332
|
-
fetch(`/api/users/${args.id}
|
|
347
|
+
fetch(`/api/users/${args.id}`, { signal: this.signal })
|
|
333
348
|
.then(r => r.json())
|
|
334
349
|
.then(d => this.next(() => { data = d; loading = false }))
|
|
335
350
|
.catch(e => this.next(() => { error = e; loading = false }))
|
|
@@ -401,9 +416,10 @@ let ref: ThisParameterType<typeof Counter> | null = null
|
|
|
401
416
|
| `render(children)` | Render to HTML string |
|
|
402
417
|
|
|
403
418
|
### Stateful `this`
|
|
404
|
-
|
|
|
405
|
-
|
|
406
|
-
| `this.
|
|
419
|
+
| Property | Description |
|
|
420
|
+
|----------|-------------|
|
|
421
|
+
| `this.signal` | AbortSignal that aborts on unmount. Pass to `fetch()`, `addEventListener()`, etc. |
|
|
422
|
+
| `this.next(fn?)` | Re-render. Callback receives current args. Returns callback's result. |
|
|
407
423
|
| `this.throw(error)` | Throw to parent boundary |
|
|
408
424
|
| `this.return()` | Terminate generator |
|
|
409
425
|
|
package/types.ts
CHANGED
|
@@ -53,7 +53,8 @@ declare module 'ajo' {
|
|
|
53
53
|
TArguments
|
|
54
54
|
|
|
55
55
|
type StatefulElement<TArguments, TTag> = ElementType<TTag> & {
|
|
56
|
-
|
|
56
|
+
signal: AbortSignal,
|
|
57
|
+
next: <R>(fn?: (this: StatefulElement<TArguments, TTag>, args: StatefulArgs<TArguments, TTag>) => R) => R,
|
|
57
58
|
throw: (value?: unknown) => void,
|
|
58
59
|
return: () => void,
|
|
59
60
|
}
|