@wrnrlr/prelude 0.0.1

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,237 @@
1
+ import {sample} from './reactive.ts'
2
+ import {Properties,BooleanAttributes,DelegatedEvents,DOMElements, type Mountable} from './constants.ts'
3
+ import type {Runtime,$RUNTIME} from './runtime.ts'
4
+
5
+ const ELEMENT: unique symbol = Symbol(), {isArray} = Array
6
+
7
+ export type PropsKeys = typeof Properties extends Set<infer K> ? K : never;
8
+ export type BooleanProps = typeof BooleanAttributes extends Set<infer K> ? K : never;
9
+ export type HandlerProps = typeof DelegatedEvents extends Set<infer K> ? K : never;
10
+ export type ChildProps = {children?:any[]}
11
+
12
+ export type ElementProps = BooleanProps & HandlerProps & ChildProps & {class: string}
13
+ export type ComponentProps = { children?:Child }
14
+
15
+ /**
16
+ * @group Hyperscript
17
+ */
18
+ export type Props = {}
19
+
20
+ type AllProps = ElementProps | ComponentProps
21
+
22
+ type EmptyProps = Record<string,never>
23
+
24
+ /**
25
+ @group Hyperscript
26
+ */
27
+ export type HyperScript = {
28
+ // (children:Child[]): View
29
+
30
+ (element:Tag, props:ElementProps, children:Child): View
31
+ (element:Tag, props:ElementProps): View
32
+ (element:Tag, children:Child): View
33
+ (element:Tag): View
34
+
35
+ <T,K>(element:Component<T & {children:K}>, props:T, children:K): View
36
+ <T>(element:Component<T>, props:T): View
37
+ <K>(element:Component<{children:K}>, children:K): View
38
+ (element:Component<undefined>): View
39
+ }
40
+
41
+ /**
42
+ * @group Hyperscript
43
+ */
44
+ export type Child = { call:any } | Child[] | string | number | symbol | bigint | boolean | Record<string,unknown> | {():Child, [ELEMENT]:boolean}
45
+ /**
46
+ * @group Hyperscript
47
+ */
48
+ export type View = {():void, [ELEMENT]?:boolean}
49
+ /**
50
+ * @group Hyperscript
51
+ */
52
+ export type Component<T> = {(props:T): Mountable, [ELEMENT]?: Runtime}
53
+ /**
54
+ * @group Hyperscript
55
+ */
56
+ export type Tag = typeof DOMElements extends Set<infer K> ? K : never;
57
+
58
+ const Fragment:Component<Props> = <T extends Props>(props:T):Mountable => (props as any).children
59
+
60
+ /**
61
+
62
+ @param r
63
+ @param patch
64
+ @group Hyperscript
65
+ */
66
+ export function hyperscript(r:Runtime, patch?:any):HyperScript {
67
+
68
+ function item<T extends Props>(e: Element, c: Child, m?: null) {
69
+ if (c===null) return
70
+ if (isArray(c))
71
+ for (const child of c)
72
+ item(e, child, m)
73
+ else if (typeof c === 'object' && r.isChild(c))
74
+ r.insert(e, c, m)
75
+ else if (typeof c==='string')
76
+ (e as Element).appendChild(r.text(c))
77
+ else if ((c as any).call) {
78
+ while ((c as any)[ELEMENT]?.call) c = (c as any)()
79
+ r.insert(e, c, m)
80
+ } else (e as Element).appendChild(r.text(c.toString()))
81
+ }
82
+
83
+ return function h<T,K=unknown>(
84
+ element:Component<T&{children:K}>|Tag, // , Child[]
85
+ second?:T|K|Child,
86
+ third?:K|Child
87
+ ): View {
88
+ let props: T
89
+ let children: Child
90
+
91
+ if (typeof second === 'object' && !isArray(second)) {
92
+ children = third || [];
93
+ props = ((second ?? {}) as T&{children:K})
94
+ } else {
95
+ children = (second as Child) || []
96
+ props = {} as T&{children:K}
97
+ }
98
+
99
+ let ret:View
100
+
101
+ if ((element as Component<T>).call) {
102
+ let e:any
103
+ const d = Object.getOwnPropertyDescriptors(props)
104
+ if (children) (props as any).children = children
105
+ for (const k in d) {
106
+ if (isArray(d[k].value)) {
107
+ const list:any[] = d[k].value;
108
+ (props as any)[k] = () => {
109
+ for (let i = 0; i < list.length; i++)
110
+ while (list[i][ELEMENT]) list[i] = list[i]()
111
+ return list
112
+ }
113
+ dynamicProperty(props as any, k)
114
+ } else if (d[k].value?.call && !d[k].value.length) { // A function with zero arguments
115
+ dynamicProperty(props as any, k)
116
+ }
117
+ }
118
+ e = sample(()=>(element as Component<T&{children:K}>)(props as T&{children:K}))
119
+ ret = () => e
120
+ } else {
121
+ const tag = parseTag(element as Tag)
122
+ const multiExpression = detectMultiExpression(children) ? null : undefined
123
+ const e = r.element(tag.name)
124
+ const props2 = props as T
125
+ if (tag.id) e.setAttribute('id',tag.id)
126
+ if (tag.classes?.length) {
127
+ const cd = Object.getOwnPropertyDescriptor(props2,'class') ?? ({value:'',writable:true,enumerable:true} as any);
128
+ (props2 as any).class = (!cd.value.call) ?
129
+ [...tag.classes,...cd.value.split(' ')].filter(c=>c).join(' ') :
130
+ () => [...tag.classes,...cd.value().split(' ')].filter(c=>c).join(' ')
131
+ }
132
+ if (patch) patch(props2)
133
+ let dynamic = false
134
+ const d = Object.getOwnPropertyDescriptors(props2)
135
+ for (const k in d) {
136
+ if (k !== "ref" && !k.startsWith('on') && d[k].value?.call) {
137
+ dynamicProperty(props2 as any, k)
138
+ dynamic = true
139
+ } else if (d[k].get) dynamic = true
140
+ }
141
+ (dynamic ? r.spread : r.assign) (e, props2, !!(children as Child[])?.length)
142
+ item(e,children as any,multiExpression)
143
+ ret = () => e
144
+ }
145
+ ret[ELEMENT] = true
146
+ return ret
147
+ }
148
+ }
149
+
150
+ function detectMultiExpression(list:any):boolean {
151
+ if (list.call) return true
152
+ else if (!isArray(list)) return false
153
+ for (const i of list) {
154
+ if (i.call) return true
155
+ else if (isArray(i)) return detectMultiExpression(i)
156
+ }
157
+ return false
158
+ }
159
+
160
+ // ^([a-zA-Z]\w*)?(#[a-zA-Z][-\w]*)?(.[a-zA-Z][-\w]*)*
161
+ function parseTag(s:string):{name:string,id?:string,classes:string[]} {
162
+ const classes:string[] = [];
163
+ let name:string, id = undefined, i:number
164
+
165
+ i = s.indexOf('#')
166
+ if (i===-1) i = s.indexOf('.')
167
+ if (i===-1) i = s.length
168
+ name = s.slice(0, i) || 'div'
169
+ s = s.slice(i)
170
+
171
+ if (s[0]==='#') {
172
+ i = s.indexOf('.')
173
+ if (i===-1) i = s.length
174
+ id = s.slice(1, i)
175
+ s = s.slice(i)
176
+ }
177
+
178
+ while(s[0]==='.') {
179
+ i = s.indexOf('.',1)
180
+ if (i===-1) i = s.length
181
+ classes.push(s.slice(1, i))
182
+ s = s.slice(i)
183
+ }
184
+ return {name:name as string,classes,id:id}
185
+ }
186
+
187
+ function dynamicProperty(props:Record<string,any>, key:string):Record<string,any> {
188
+ const src = props[key]
189
+ Object.defineProperty(props, key, {get() {return src()},enumerable:true})
190
+ return props
191
+ }
192
+
193
+ // function tw(rules) {
194
+ // const classes = (classes) => classes.filter(c=>!rules.some(r=>c.match(r[0]))).join(' ')
195
+ // const styles = (classes) => classes.reduce((acc,c) => {
196
+ // for (const r of rules) {
197
+ // const m = c.match(r[0])
198
+ // if (m) acc.push(r[1](...m.splice(1)))
199
+ // }
200
+ // return acc
201
+ // },[]).join(';')
202
+ // return props => {
203
+ // if (!props.class) return
204
+ // const cd = Object.getOwnPropertyDescriptor(props,'class'), cf = typeof cd.value === 'function'
205
+ // props.class = cf ? ()=>classes(cd.value().split(' ')) : classes(cd.value.split(' '))
206
+ // if (!props.style) props.style = cf ? ()=>styles(cd.value().split(' ')) : styles(cd.value.split(' '))
207
+ // else {
208
+ // const sd = Object.getOwnPropertyDescriptor(props,'style'), sf = typeof sd.value === 'function'
209
+ // if (cf) props.style = sf ? ()=>styles(cd.value().split(' ')) + ';' + sd.value() : ()=>styles(cd.value().split(' ')) + ';' + sd.value
210
+ // else {
211
+ // const ca = styles(cd.value.split(' '))
212
+ // props.style = sf ? () => ca + ';' + sd.value() : ca + ';' + sd.value
213
+ // }
214
+ // }
215
+ // }
216
+ // }
217
+
218
+ // const spacing = {p:'padding', m:'margin'}
219
+ // const toDirection = {b:'bottom', l:'left', r:'right', t:'top'}
220
+ // const toSurface = {bg:'background',text:'text',border:'border',outline:'outline'}
221
+ // const alignContent = {start:'flex-start',center:'center',end:'flex-end',between:'space-between',around:'space-around',evenly:'space-evenly'}
222
+ // const toColor = (name,weight) => 'red'
223
+
224
+ // const rules = [
225
+ // // padding & margin
226
+ // [/([pm])-(\d+)/, (pm,size)=>`${spacing[pm]}:${size/4}rem`],
227
+ // // border
228
+ // // [/b-%d/, (width)=>({'border-size':width})]
229
+ // // bg & text & border & outline
230
+ // // [/(bg|text|border|outline)-\W+-\d+/, (style,color,weight)=>({[toSurface[style]]:toColor(color,weight)})],
231
+ // // display
232
+ // // [/(block|inline|inline-block|flex|inline-flex|none)/, (display)=>({display})],
233
+ // // [/items-(start|center|end|stretch|baseline)/, (pos)=>({'align-items':pos})],
234
+ // // [/justify-(start|center|end|stretch|baseline)/, (pos)=>({'justify-content':pos})],
235
+ // // [/content-(start|center|end|between|around|evenly)/, (pos)=>({'align-content':alignContent[pos]})],
236
+ // // [/self-(auto|start|center|end|stretch|baseline)/, (pos)=>({'aligh-self':pos})],
237
+ // ]
package/src/mod.ts ADDED
@@ -0,0 +1,45 @@
1
+ export type {Getter,Setter,Fn,EqualsFn,ErrorFn,RootFn,UpdateFn} from './reactive.ts'
2
+ export {signal,effect,sample,batch,memo,root,onMount} from './reactive.ts'
3
+ export {nbsp} from './constants.ts'
4
+ export {wrap,Show,List} from './controlflow.js'
5
+ export {runtime, type Runtime} from './runtime.ts'
6
+ import {runtime, type Runtime} from './runtime.ts'
7
+ export {hyperscript,type HyperScript,type Child,type Props,type Tag,type View,type Component} from './hyperscript.ts'
8
+ import {type HyperScript, hyperscript} from './hyperscript.ts'
9
+ export {Input,Table} from './components.js'
10
+ export * from './canvas.js'
11
+
12
+ const r:Runtime = runtime(window as any)
13
+
14
+ /** h
15
+ @example Element with a single child
16
+ ```js
17
+ h('h1','Hello World!')
18
+ ```
19
+ @example Element with multiple children
20
+ ```js
21
+ h('p',['Hello ',h('em','World!')])
22
+ ```
23
+ @example Component with event handler
24
+ ```js
25
+ h(Input,{onInput:e => {}})
26
+ ```
27
+ @group Hyperscript
28
+ */
29
+ const h:HyperScript = hyperscript(r)
30
+
31
+ const render = r.render
32
+
33
+ import {signal} from './reactive.ts'
34
+ import {wrap} from './controlflow.js'
35
+
36
+ /**
37
+ @group Utils
38
+ */
39
+ export function $(a:any,b:any):any {
40
+ const t = typeof a
41
+ if (t==='function') return wrap(a,b)
42
+ else return signal(a,b)
43
+ }
44
+
45
+ export {h,render}
@@ -0,0 +1,359 @@
1
+
2
+ export interface EffectOptions {
3
+ name?: string;
4
+ }
5
+
6
+ export interface MemoOptions<T> extends EffectOptions {
7
+ equals?: false | ((prev: T, next: T) => boolean);
8
+ }
9
+
10
+ export type EffectFunction<Prev, Next extends Prev = Prev> = (v: Prev) => Next;
11
+
12
+ /** @internal */
13
+ export type Fn<T = void> = () => T
14
+ export type EqualsFn<T> = (value: T, valueNext: T) => boolean
15
+ export type ErrorFn = (error: unknown) => void
16
+ export type RootFn<T> = (dispose: () => void) => T
17
+ export type UpdateFn<T> = (value: T) => T
18
+
19
+ /**
20
+ Get value of type `T`
21
+ @see {@link Setter} {@link signal} {@link memo}
22
+ */
23
+ export type Getter<T> = {
24
+ (): T
25
+ }
26
+
27
+ /**
28
+ Set value of type `T`
29
+ @see {@link Getter} {@link signal}
30
+ */
31
+ export type Setter<T> = {
32
+ (update: UpdateFn<T>): T,
33
+ (value: T): T
34
+ }
35
+
36
+ export type Context<T> = {
37
+ id: symbol,
38
+ defaultValue: T,
39
+ get(): T,
40
+ set(value: T): void
41
+ }
42
+
43
+ export type Options<T> = {
44
+ equals?: false | EqualsFn<T>
45
+ }
46
+
47
+ export const $TRACK = Symbol("track")
48
+
49
+ let BATCH: Map<Signal<any>, any> | undefined
50
+ let OBSERVER: Observer | undefined
51
+ let TRACKING = false
52
+ const SYMBOL_ERRORS = Symbol()
53
+
54
+ // interface Node<T> {
55
+ // parent: Node<T> | undefined
56
+ // get():T
57
+ // set(value:T):T
58
+ // }
59
+ // Root node
60
+ // class RNode extends Node {}
61
+ //
62
+ // // Computed value node
63
+ // class CNode {}
64
+ //
65
+ // // Value node
66
+ // class VNode {}
67
+
68
+ export class Signal<T = unknown> {
69
+ public parent: Computation<T> | undefined
70
+ public value: T
71
+ private readonly equals: EqualsFn<T>
72
+ public readonly observers: Set<Computation> = new Set ()
73
+
74
+ constructor(value: T, {equals}: Options<T> = {}) {
75
+ this.value = value
76
+ this.equals = (equals === false) ? () => false : equals || Object.is
77
+ }
78
+
79
+ get = (): T => {
80
+ if (TRACKING && OBSERVER instanceof Computation) {
81
+ this.observers.add(OBSERVER)
82
+ OBSERVER.signals.add(this as Signal)
83
+ }
84
+ if (this.parent?.waiting)
85
+ this.parent.update()
86
+ return this.value
87
+ }
88
+
89
+ set = (value: UpdateFn<T> | T): T => {
90
+ const valueNext = (value instanceof Function) ? value(this.value) : value
91
+ if (!this.equals(this.value, valueNext )) {
92
+ if (BATCH) {
93
+ BATCH.set(this, valueNext)
94
+ } else {
95
+ this.value = valueNext
96
+ this.stale(1, true)
97
+ this.stale(-1, true)
98
+ }
99
+ }
100
+ return this.value;
101
+ }
102
+
103
+ stale = (change: 1 | -1, fresh: boolean): void => {
104
+ for (const observer of this.observers)
105
+ observer.stale(change, fresh)
106
+ }
107
+ }
108
+
109
+ abstract class Observer {
110
+ public parent: Observer | undefined = OBSERVER
111
+ public cleanups: Fn[] = []
112
+ public contexts: Record<symbol, any> = {}
113
+ public observers: Set<Observer> = new Set()
114
+ public signals: Set<Signal> = new Set()
115
+
116
+ protected dispose = ():void => {
117
+ for (const observer of this.observers)
118
+ observer.dispose()
119
+
120
+ for (const signal of this.signals)
121
+ signal.observers.delete(this as unknown as Computation)
122
+
123
+ for (const cleanup of this.cleanups)
124
+ cleanup()
125
+
126
+ this.cleanups = []
127
+ this.contexts = {}
128
+ this.observers = new Set()
129
+ this.signals = new Set()
130
+ this.parent?.observers.delete(this)
131
+ }
132
+
133
+ get = <T> (id: symbol): T | undefined => {
134
+ if (id in this.contexts) {
135
+ return this.contexts[id]
136
+ } else {
137
+ return this.parent?.get<T>(id)
138
+ }
139
+ }
140
+
141
+ set = <T>(id: symbol, value: T): void => {
142
+ this.contexts[id] = value
143
+ }
144
+ }
145
+
146
+ class Root extends Observer {
147
+ wrap<T>(fn: RootFn<T>): T {
148
+ return wrap(() => fn(this.dispose), this, false)!
149
+ }
150
+ }
151
+
152
+ class Computation<T = unknown> extends Observer {
153
+ private readonly fn: Fn<T>
154
+ private fresh: boolean = false
155
+ public signal: Signal<T>
156
+ public waiting: number = 0
157
+
158
+ constructor(fn: Fn<T>, options?: Options<T>) {
159
+ super()
160
+ this.fn = fn
161
+ this.signal = new Signal<T>(this.run(), options)
162
+ this.signal.parent = this
163
+ }
164
+
165
+ private run = (): T => {
166
+ this.dispose()
167
+ this.parent?.observers.add(this)
168
+ return wrap(this.fn, this, true)!
169
+ }
170
+
171
+ update = (): void => {
172
+ this.waiting = 0
173
+ this.fresh = false
174
+ this.signal.set(this.run)
175
+ }
176
+
177
+ stale = (change: 1 | -1, fresh: boolean): void => {
178
+ if (!this.waiting && change < 0) return
179
+ if (!this.waiting && change > 0) this.signal.stale(1, false)
180
+
181
+ this.waiting += change
182
+ this.fresh ||= fresh
183
+
184
+ if (!this.waiting) {
185
+ this.waiting = 0
186
+ if (this.fresh) this.update()
187
+ this.signal.stale(-1, false)
188
+ }
189
+ }
190
+ }
191
+
192
+ /**
193
+ Create a {@link Signal} of type `T` with initial `value`
194
+ @param value Initial value
195
+ @param options Signal options
196
+ @example Create new Signal
197
+ ```js
198
+ const n = signal(1)
199
+ ```
200
+ @example Get current value
201
+ ```js
202
+ n()
203
+ ```
204
+ @example Set value
205
+ ```js
206
+ n(3)
207
+ ```
208
+ @group Reactive Primitive
209
+ */
210
+ export function signal<T>(value:T, options?:Options<T>): Getter<T> & Setter<T> {
211
+ const s = new Signal<T>(value,options)
212
+ const f = Object.assign((...args:T[]) => args.length ? s.set(args[0]) as T : s.get() as T, s)
213
+ return f as unknown as Getter<T> & Setter<T>
214
+ }
215
+
216
+ /**
217
+ @group Reactive Primitive
218
+ */
219
+ export function effect(fn: Fn): void {
220
+ new Computation(fn)
221
+ }
222
+
223
+ /**
224
+ Memo creates a readonly reactive value equal to the return value of the given function
225
+ and makes sure that function only gets executed when its dependencies change
226
+
227
+ ```js
228
+ const a = signal(1), b = signal(2)
229
+ const sum = memo((prev) => a()*b() + prev, 0)
230
+ sum()
231
+ ```
232
+
233
+ The memo function should not update other signals.
234
+
235
+ @param fn
236
+ @param options
237
+ @group Reactive Primitive
238
+ */
239
+ // export function memo<T extends K, K = T>(
240
+ // fn: EffectFunction<undefined | NoInfer<K>, T>
241
+ // ): Getter<T>;
242
+ // export function memo<T extends K, Init = T, K = T>(
243
+ // fn: EffectFunction<Init | K, T>,
244
+ // value: Init,
245
+ // options?: MemoOptions<T>
246
+ // ): Getter<T>;
247
+ export function memo<T extends K, Init = T, K = T>(
248
+ fn: Fn<T>,
249
+ value?: Init,
250
+ options?: Options<T>
251
+ ): Getter<T> {
252
+ return new Computation(fn, options).signal.get
253
+ }
254
+
255
+ /**
256
+
257
+ @param fn
258
+ @group Reactive Primitive
259
+ */
260
+ export function root<T>(fn: RootFn<T>): T {
261
+ return new Root().wrap(fn)
262
+ }
263
+
264
+ export function context<T>(): Context<T | undefined>;
265
+ export function context<T>(defaultValue: T): Context<T>;
266
+ export function context<T>(defaultValue?: T) {
267
+ const id = Symbol()
268
+ const get = (): T | undefined => OBSERVER?.get ( id ) ?? defaultValue
269
+ const set = ( value: T ): void => OBSERVER?.set ( id, value )
270
+ const s = {id, defaultValue, get, set}
271
+ const f = Object.assign((props:any) => {
272
+ set(props.value)
273
+ return () => props.children.call ? props.children() : props.children
274
+ }, s)
275
+ return f as unknown as Context<T>
276
+ }
277
+
278
+ export function useContext<T>(context: Context<T>): T {
279
+ return context.get()
280
+ }
281
+
282
+ export function getOwner(): Observer | undefined {
283
+ return OBSERVER
284
+ }
285
+
286
+ export function runWithOwner<T>(observer: Observer|undefined, fn: ()=>T):T {
287
+ const tracking = observer instanceof Computation
288
+ return wrap(fn, observer, tracking)!
289
+ }
290
+
291
+ /**
292
+ Execute the function `fn` only once. Implemented as an {@link effect} wrapping a {@link sample}.
293
+ @group Reactive Primitive
294
+ */
295
+ export function onMount(fn: () => void) {
296
+ effect(() => sample(fn));
297
+ }
298
+
299
+ export function onCleanup(fn: Fn):void {
300
+ OBSERVER?.cleanups.push(fn)
301
+ }
302
+
303
+ export function onError(fn: ErrorFn):void {
304
+ if ( !OBSERVER ) return
305
+ OBSERVER.contexts[SYMBOL_ERRORS] ||= []
306
+ OBSERVER.contexts[SYMBOL_ERRORS].push (fn)
307
+ }
308
+
309
+ /**
310
+ *
311
+ * @group Reactive Primitive
312
+ */
313
+ export function batch<T>(fn: Fn<T>):T {
314
+ if (BATCH) return fn ()
315
+ const batch = BATCH = new Map<Signal, any> ();
316
+ try {
317
+ return fn()
318
+ } finally {
319
+ BATCH = undefined;
320
+ // Mark all signals as stale
321
+ for (const signal of batch.keys()) signal.stale(1,false)
322
+ // Updating values
323
+ for (const [signal,value] of batch.entries()) signal.set(()=>value)
324
+ // Mark all those signals as not stale, allowing observers to finally update themselves
325
+ for (const signal of batch.keys()) signal.stale(-1,false)
326
+ }
327
+ }
328
+
329
+ /**
330
+
331
+ @param fn
332
+ @group Reactive Primitive
333
+ */
334
+ export function sample<T>(fn: Fn<T>):T {
335
+ return wrap(fn, OBSERVER, false)!
336
+ }
337
+
338
+ /**
339
+
340
+ @param fn
341
+ @group Reactive Primitive
342
+ */
343
+ function wrap<T>(fn: Fn<T>, observer: Observer | undefined, tracking: boolean ): T|undefined {
344
+ const OBSERVER_PREV = OBSERVER;
345
+ const TRACKING_PREV = TRACKING;
346
+ OBSERVER = observer;
347
+ TRACKING = tracking;
348
+ try {
349
+ return fn();
350
+ } catch (error: unknown) {
351
+ const fns = observer?.get<ErrorFn[]>(SYMBOL_ERRORS)
352
+ if (fns)
353
+ for (const fn of fns) fn(error)
354
+ else throw error
355
+ } finally {
356
+ OBSERVER = OBSERVER_PREV;
357
+ TRACKING = TRACKING_PREV;
358
+ }
359
+ }