leaner 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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (C) 2024 Michał Męciński
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,5 @@
1
+ # Leaner.js
2
+
3
+ **Leaner.js** is a thin wrapper over the HTML DOM API. Although it contains many features of modern JavaScript frameworks (reactive state, components, etc.), it doesn’t require any special compilers or build tools, and it doesn’t use a virtual DOM so it's very lightweight and efficient.
4
+
5
+ **Note:** the current version is a proof of concept.
package/dist/core.js ADDED
@@ -0,0 +1,562 @@
1
+ function isPlainObject( value ) {
2
+ if ( typeof value == 'object' && value != null ) {
3
+ const prototype = Object.getPrototypeOf( value );
4
+ if ( prototype == null || prototype == Object.prototype )
5
+ return true;
6
+ }
7
+ return false;
8
+ }
9
+
10
+ function isPlainObjectOrArray( value ) {
11
+ return isPlainObject( value ) || Array.isArray( value );
12
+ }
13
+
14
+ let deps = null;
15
+
16
+ function track( record ) {
17
+ if ( deps != null )
18
+ deps.add( record );
19
+ }
20
+
21
+ function react( record ) {
22
+ if ( record.watches != null ) {
23
+ for ( const watch of record.watches.values() )
24
+ watch();
25
+ }
26
+ }
27
+
28
+ function withDeps( callback ) {
29
+ const oldDeps = deps;
30
+ deps = new Set();
31
+
32
+ try {
33
+ const result = callback();
34
+
35
+ return [ result, deps ];
36
+ } finally {
37
+ deps = oldDeps;
38
+ }
39
+ }
40
+
41
+ function addDeps( newDeps ) {
42
+ if ( deps != null && newDeps != null ) {
43
+ for ( const dep of newDeps.values() )
44
+ deps.add( dep );
45
+ }
46
+ }
47
+
48
+ function updateDeps( newDeps, deps, callback ) {
49
+ for ( const dep of newDeps ) {
50
+ if ( dep.watches == null )
51
+ dep.watches = new Set();
52
+ dep.watches.add( callback );
53
+ if ( deps != null )
54
+ deps.delete( dep );
55
+ }
56
+
57
+ if ( deps != null ) {
58
+ for ( const dep of deps )
59
+ dep.watches.delete( callback );
60
+ }
61
+
62
+ return newDeps;
63
+ }
64
+
65
+ const stateGetterHandler = {
66
+ get: stateGetterGet,
67
+ apply: stateGetterApply,
68
+ set() {
69
+ throw new TypeError( 'Mutating a state getter is not allowed' );
70
+ },
71
+ deleteProperty() {
72
+ throw new TypeError( 'Mutating a state getter is not allowed' );
73
+ },
74
+ };
75
+
76
+ const stateGetterRecords = new WeakMap();
77
+
78
+ function stateGetterGet( target, prop ) {
79
+ if ( prop === 'get' ) {
80
+ return arg => {
81
+ if ( typeof arg == 'function' )
82
+ return createDynamicStateRecord( target, arg ).proxy;
83
+ else
84
+ return createChildStateRecord( target, arg ).proxy;
85
+ };
86
+ }
87
+
88
+ if ( prop === Symbol.toPrimitive )
89
+ throw new Error( 'Using a state getter as a primitive value is not allowed' );
90
+
91
+ return createChildStateRecord( target, prop ).proxy;
92
+ }
93
+
94
+ function stateGetterApply( target ) {
95
+ const value = target();
96
+
97
+ if ( isPlainObjectOrArray( value ) ) {
98
+ const record = stateGetterRecords.get( target );
99
+ return createReaderRecord( record, value ).proxy;
100
+ }
101
+
102
+ return value;
103
+ }
104
+
105
+ function createRecord( prop, getter ) {
106
+ return {
107
+ prop,
108
+ target: getter,
109
+ proxy: new Proxy( getter, stateGetterHandler ),
110
+ children: null,
111
+ reader: null,
112
+ watches: null,
113
+ };
114
+ }
115
+
116
+ function createRootStateRecord( getter ) {
117
+ const record = createRecord( '(root)', getter );
118
+
119
+ stateGetterRecords.set( getter, record );
120
+
121
+ return record;
122
+ }
123
+
124
+ function createChildStateRecord( target, prop ) {
125
+ const record = stateGetterRecords.get( target );
126
+
127
+ if ( record.children != null && record.children[ prop ] != null )
128
+ return record.children[ prop ];
129
+
130
+ if ( record.children == null )
131
+ record.children = Object.create( null );
132
+
133
+ const childRecord = createRecord( prop, getter );
134
+
135
+ function getter() {
136
+ const value = target();
137
+ track( childRecord );
138
+ return value[ prop ];
139
+ }
140
+ stateGetterRecords.set( getter, childRecord );
141
+
142
+ record.children[ prop ] = childRecord;
143
+
144
+ return childRecord;
145
+ }
146
+
147
+ function createDynamicStateRecord( target, arg ) {
148
+ const dynamicRecord = {
149
+ prop: '(dynamic)',
150
+ target: getter,
151
+ proxy: new Proxy( getter, stateGetterHandler ),
152
+ };
153
+
154
+ function getter() {
155
+ const value = target();
156
+ const index = arg();
157
+ track( createChildStateRecord( target, index ) );
158
+ return value[ index ];
159
+ }
160
+
161
+ stateGetterRecords.set( getter, dynamicRecord );
162
+
163
+ return dynamicRecord;
164
+ }
165
+
166
+ const stateReaderHandler = {
167
+ get: stateReaderGet,
168
+ set() {
169
+ throw new TypeError( 'Mutating a reactive value is not allowed' );
170
+ },
171
+ deleteProperty() {
172
+ throw new TypeError( 'Mutating a reactive value is not allowed' );
173
+ },
174
+ };
175
+
176
+ const stateReaderRecords = new WeakMap();
177
+
178
+ const stateReaderProxies = new WeakSet();
179
+
180
+ function stateReaderGet( target, prop ) {
181
+ const record = stateReaderRecords.get( target );
182
+
183
+ const childRecord = createChildStateRecord( record.target, prop );
184
+
185
+ track( childRecord );
186
+
187
+ const value = target[ prop ];
188
+
189
+ if ( isPlainObjectOrArray( value ) )
190
+ return createReaderRecord( childRecord, value ).proxy;
191
+
192
+ return value;
193
+ }
194
+
195
+ function createReaderRecord( record, value ) {
196
+ if ( record.reader != null && record.reader.value === value )
197
+ return record.reader;
198
+
199
+ // reuse an existing record; this happens e.g. if an array was rearranged using splice/unshift
200
+ const existingRecord = stateReaderRecords.get( value );
201
+ if ( existingRecord != null ) {
202
+ record.reader = existingRecord;
203
+ existingRecord.target = record.target;
204
+ return existingRecord;
205
+ }
206
+
207
+ record.reader = {
208
+ value,
209
+ target: record.target,
210
+ proxy: new Proxy( value, stateReaderHandler ),
211
+ };
212
+
213
+ stateReaderRecords.set( value, record.reader );
214
+ stateReaderProxies.add( record.reader.proxy );
215
+
216
+ return record.reader;
217
+ }
218
+
219
+ function isStateReader( value ) {
220
+ if ( stateReaderProxies.has( value ) )
221
+ return true;
222
+ return false;
223
+ }
224
+
225
+ const computedGetterHandler = {
226
+ get: computedGetterGet,
227
+ apply: computedGetterApply,
228
+ set() {
229
+ throw new TypeError( 'Mutating a computed getter is not allowed' );
230
+ },
231
+ deleteProperty() {
232
+ throw new TypeError( 'Mutating a computed getter is not allowed' );
233
+ },
234
+ };
235
+
236
+ function computedGetterGet( target, prop ) {
237
+ if ( prop === 'get' ) {
238
+ return arg => {
239
+ if ( typeof arg == 'function' )
240
+ return createDynamicComputedProxy( target, arg );
241
+ else
242
+ return createChildComputedProxy( target, arg );
243
+ };
244
+ }
245
+
246
+ if ( prop == Symbol.toPrimitive )
247
+ throw new Error( 'Cannot use a computed getter as array index; use the get() method instead' );
248
+
249
+ return createChildComputedProxy( target, prop );
250
+ }
251
+
252
+ function createRootComputedProxy( getter ) {
253
+ return new Proxy( getter, computedGetterHandler );
254
+ }
255
+
256
+ function createChildComputedProxy( target, prop ) {
257
+ return new Proxy( getter, computedGetterHandler );
258
+
259
+ function getter() {
260
+ return target()[ prop ];
261
+ }
262
+ }
263
+
264
+ function createDynamicComputedProxy( target, arg ) {
265
+ return new Proxy( getter, computedGetterHandler );
266
+
267
+ function getter() {
268
+ return target()[ arg() ];
269
+ }
270
+ }
271
+
272
+ function computedGetterApply( target ) {
273
+ const value = target();
274
+
275
+ if ( isPlainObjectOrArray( value ) && !isStateReader( value ) )
276
+ return new Proxy( value, computedReaderHandler );
277
+
278
+ return value;
279
+ }
280
+
281
+ const computedReaderHandler = {
282
+ get: computedReaderGet,
283
+ set() {
284
+ throw new TypeError( 'Mutating a computed value is not allowed' );
285
+ },
286
+ deleteProperty() {
287
+ throw new TypeError( 'Mutating a computed value is not allowed' );
288
+ },
289
+ };
290
+
291
+ function computedReaderGet( target, prop ) {
292
+ const value = target[ prop ];
293
+
294
+ if ( isPlainObjectOrArray( value ) && !isStateReader( value ) )
295
+ return new Proxy( value, computedReaderHandler );
296
+
297
+ return value;
298
+ }
299
+
300
+ class Mutator {
301
+ constructor( callback ) {
302
+ this.callback = callback;
303
+ }
304
+ }
305
+
306
+ function applyMutator( record, value, callback ) {
307
+ mutableValues = new WeakMap();
308
+ mutableValues.set( value, record );
309
+
310
+ const proxy = new Proxy( value, stateMutatorHandler );
311
+
312
+ stateMutatorTargets = new WeakMap();
313
+ stateMutatorTargets.set( proxy, value );
314
+
315
+ mutated = new Set();
316
+
317
+ try {
318
+ callback( proxy );
319
+
320
+ return mutated;
321
+ } finally {
322
+ mutableValues = null;
323
+ stateMutatorTargets = null;
324
+ mutated = null;
325
+ }
326
+ }
327
+
328
+ const stateMutatorHandler = {
329
+ get: stateMutatorGet,
330
+ set: stateMutatorSet,
331
+ deleteProperty: stateMutatorDelete,
332
+ };
333
+
334
+ let mutableValues = null;
335
+ let stateMutatorTargets = null;
336
+
337
+ let mutated = null;
338
+
339
+ function stateMutatorGet( target, prop ) {
340
+ const value = target[ prop ];
341
+
342
+ if ( isPlainObjectOrArray( value ) ) {
343
+ const record = mutableValues.get( target );
344
+
345
+ const childRecord = createChildStateRecord( record.target, prop );
346
+ mutableValues.set( value, childRecord );
347
+
348
+ const proxy = new Proxy( value, stateMutatorHandler );
349
+ stateMutatorTargets.set( proxy, value );
350
+
351
+ return proxy;
352
+ }
353
+
354
+ return value;
355
+ }
356
+
357
+ function stateMutatorSet( target, prop, value ) {
358
+ const record = mutableValues.get( target );
359
+
360
+ if ( Array.isArray( target ) && isIntegerKey( prop ) && Number( prop ) >= target.length ) {
361
+ // the length of the array is changed implicitly when a new element is added
362
+ const lengthRecord = createChildStateRecord( record.target, 'length' );
363
+ mutated.add( lengthRecord );
364
+ }
365
+
366
+ if ( isPlainObjectOrArray( value ) ) {
367
+ const valueTarget = stateMutatorTargets.get( value );
368
+ if ( valueTarget != null )
369
+ value = valueTarget;
370
+ }
371
+
372
+ if ( target[ prop ] === value )
373
+ return true;
374
+
375
+ target[ prop ] = value;
376
+
377
+ const childRecord = createChildStateRecord( record.target, prop );
378
+ mutated.add( childRecord );
379
+
380
+ return true;
381
+ }
382
+
383
+ function stateMutatorDelete( target, prop ) {
384
+ if ( !( prop in target ) )
385
+ return true;
386
+
387
+ delete target[ prop ];
388
+
389
+ const record = mutableValues.get( target );
390
+
391
+ const childRecord = createChildStateRecord( record.target, prop );
392
+ mutated.add( childRecord );
393
+
394
+ return true;
395
+ }
396
+
397
+ function isIntegerKey( key ) {
398
+ return typeof key == 'string' && key != 'NaN' && key[ 0 ] != '-' && '' + parseInt( key, 10 ) == key;
399
+ }
400
+
401
+ let scheduled = null;
402
+
403
+ function schedule( callback ) {
404
+ if ( scheduled == null ) {
405
+ scheduled = new Set();
406
+ Promise.resolve().then( () => {
407
+ const watches = scheduled.values();
408
+ scheduled = null;
409
+ for ( const watch of watches )
410
+ watch();
411
+ } );
412
+ }
413
+
414
+ scheduled.add( callback );
415
+ }
416
+
417
+ function useState( initial ) {
418
+ let current = initial;
419
+
420
+ const record = createRootStateRecord( getter );
421
+
422
+ return [ record.proxy, setter ];
423
+
424
+ function getter() {
425
+ track( record );
426
+ return current;
427
+ }
428
+ function setter( value ) {
429
+ if ( value instanceof Mutator ) {
430
+ const mutated = applyMutator( record, current, value.callback );
431
+
432
+ for ( const record of mutated )
433
+ react( record );
434
+ } else {
435
+ if ( typeof value == 'function' ) {
436
+ const readOnlyValue = stateGetterApply( getter );
437
+ value = value( readOnlyValue );
438
+ }
439
+
440
+ if ( current !== value ) {
441
+ current = value;
442
+ react( record );
443
+ }
444
+ }
445
+ }
446
+ }
447
+
448
+ function useWatchEffect( callback ) {
449
+ let deps = null;
450
+
451
+ update();
452
+
453
+ function update() {
454
+ const [ _, newDeps ] = withDeps( callback );
455
+
456
+ deps = updateDeps( newDeps, deps, update );
457
+ }
458
+ }
459
+
460
+ function useWatch( getter, callback ) {
461
+ let [ value, deps ] = withDeps( getter );
462
+
463
+ updateDeps( deps, null, update );
464
+
465
+ function update() {
466
+ const [ newValue, newDeps ] = withDeps( getter );
467
+
468
+ deps = updateDeps( newDeps, deps, update );
469
+
470
+ if ( newValue !== value ) {
471
+ callback( newValue, value );
472
+ value = newValue;
473
+ }
474
+ }
475
+ }
476
+
477
+ function useEffect( callback ) {
478
+ let deps = null;
479
+
480
+ schedule( update );
481
+
482
+ function update() {
483
+ const [ _, newDeps ] = withDeps( callback );
484
+
485
+ deps = updateDeps( newDeps, deps, () => schedule( update ) );
486
+ }
487
+ }
488
+
489
+ function useReactive( callback ) {
490
+ let deps = null;
491
+
492
+ update();
493
+
494
+ function update() {
495
+ const [ _, newDeps ] = withDeps( callback );
496
+
497
+ deps = updateDeps( newDeps, deps, () => schedule( update ) );
498
+ }
499
+ }
500
+
501
+ function useReactiveWatch( getter, callback ) {
502
+ let value = null, deps = null;
503
+
504
+ update( true );
505
+
506
+ function update( force ) {
507
+ const [ newValue, newDeps ] = withDeps( getter );
508
+
509
+ deps = updateDeps( newDeps, deps, () => schedule( update ) );
510
+
511
+ if ( force || newValue !== value ) {
512
+ callback( newValue, value );
513
+ value = newValue;
514
+ }
515
+ }
516
+ }
517
+
518
+ function useComputed( callback ) {
519
+ let deps = null, value = null, valid = false;
520
+
521
+ return createRootComputedProxy( execute );
522
+
523
+ function execute() {
524
+ if ( !valid ) {
525
+ const [ newValue, newDeps ] = withDeps( callback );
526
+
527
+ deps = updateDeps( newDeps, deps, invalidate );
528
+
529
+ value = newValue;
530
+ valid = true;
531
+ }
532
+
533
+ addDeps( deps );
534
+
535
+ return value;
536
+ }
537
+
538
+ function invalidate() {
539
+ value = null;
540
+ valid = false;
541
+ }
542
+ }
543
+
544
+ function useConstant( value ) {
545
+ return createRootComputedProxy( getter );
546
+
547
+ function getter() {
548
+ return value;
549
+ }
550
+ }
551
+
552
+ function mutate( callback ) {
553
+ return new Mutator( callback );
554
+ }
555
+
556
+ function current( value ) {
557
+ if ( typeof value == 'function' )
558
+ return value();
559
+ return value;
560
+ }
561
+
562
+ export { current, mutate, schedule, useComputed, useConstant, useEffect, useReactive, useReactiveWatch, useState, useWatch, useWatchEffect };
package/dist/web.js ADDED
@@ -0,0 +1,638 @@
1
+ import { useReactive, useReactiveWatch, useConstant } from 'leaner';
2
+
3
+ function appendToArray( item, target ) {
4
+ if ( Array.isArray( item ) )
5
+ target.push( ...item );
6
+ else
7
+ target.push( item );
8
+ }
9
+
10
+ function isPlainObject( value ) {
11
+ if ( typeof value == 'object' && value != null ) {
12
+ const prototype = Object.getPrototypeOf( value );
13
+ if ( prototype == null || prototype == Object.prototype )
14
+ return true;
15
+ }
16
+ return false;
17
+ }
18
+
19
+ function isPlainObjectOrArray( value ) {
20
+ return isPlainObject( value ) || Array.isArray( value );
21
+ }
22
+
23
+ let current = null;
24
+
25
+ function createComponent( template ) {
26
+ const component = template[ 0 ];
27
+
28
+ let props, i = 1;
29
+ if ( i < template.length && isPlainObject( template[ i ] ) )
30
+ props = template[ i++ ];
31
+ else
32
+ props = {};
33
+
34
+ const children = template.slice( i );
35
+
36
+ return component( props, children );
37
+ }
38
+
39
+ function createContext( parent ) {
40
+ return {
41
+ parent,
42
+ children: null,
43
+ mount: null,
44
+ destroy: null,
45
+ };
46
+ }
47
+
48
+ function createChildContext() {
49
+ const context = createContext( current );
50
+
51
+ if ( current != null ) {
52
+ if ( current.children == null )
53
+ current.children = [];
54
+ current.children.push( context );
55
+ }
56
+
57
+ return context;
58
+ }
59
+
60
+ function withContext( context, callback ) {
61
+ const last = current;
62
+ current = context;
63
+ try {
64
+ return callback();
65
+ } finally {
66
+ current = last;
67
+ }
68
+ }
69
+
70
+ function withChildContext( callback ) {
71
+ current = createChildContext();
72
+ try {
73
+ return callback();
74
+ } finally {
75
+ current = current.parent;
76
+ }
77
+ }
78
+
79
+ function onMount( callback ) {
80
+ if ( current == null )
81
+ throw new Error( 'onMount() cannot be called outside of a component' );
82
+ if ( current.mount == null )
83
+ current.mount = [];
84
+ current.mount.push( callback );
85
+ }
86
+
87
+ function onDestroy( callback ) {
88
+ if ( current == null )
89
+ throw new Error( 'onDestroy() cannot be called outside of a component' );
90
+ if ( current.destroy == null )
91
+ current.destroy = [];
92
+ current.destroy.push( callback );
93
+ }
94
+
95
+ function mountContext( context ) {
96
+ if ( context.children != null ) {
97
+ for ( const child of context.children )
98
+ mountContext( child );
99
+ }
100
+
101
+ if ( context.mount != null ) {
102
+ for ( const callback of context.mount )
103
+ callback();
104
+ }
105
+ }
106
+
107
+ function destroyContext( context ) {
108
+ if ( context.destroy != null ) {
109
+ for ( const callback of context.destroy )
110
+ callback();
111
+ }
112
+
113
+ if ( context.children != null ) {
114
+ for ( const child of context.children )
115
+ destroyContext( child );
116
+
117
+ context.children = null;
118
+ }
119
+ }
120
+
121
+ function setClasses( element, classes ) {
122
+ if ( typeof classes == 'function' ) {
123
+ useReactive( () => {
124
+ const value = classes();
125
+ if ( Array.isArray( value ) ) {
126
+ element.className = '';
127
+ element.classList.add( ...value );
128
+ } else {
129
+ element.className = value;
130
+ }
131
+ } );
132
+ } else if ( isPlainObject( classes ) ) {
133
+ setClassesObject( element, classes );
134
+ } else if ( Array.isArray( classes ) ) {
135
+ setClassesArray( element, classes );
136
+ } else {
137
+ element.className = classes;
138
+ }
139
+ }
140
+
141
+ function setClassesObject( element, classes ) {
142
+ for ( const [ key, value ] of Object.entries( classes ) ) {
143
+ if ( typeof value == 'function' ) {
144
+ useReactive( () => {
145
+ if ( value() )
146
+ element.classList.add( key );
147
+ else
148
+ element.classList.remove( key );
149
+ } );
150
+ } else if ( value ) {
151
+ element.classList.add( key );
152
+ }
153
+ }
154
+ }
155
+
156
+ function setClassesArray( element, classes ) {
157
+ for ( const item of classes ) {
158
+ if ( typeof item == 'function' ) {
159
+ let value = '';
160
+ useReactive( () => {
161
+ const newValue = item();
162
+ if ( newValue === value )
163
+ return;
164
+ if ( value === '' )
165
+ element.classList.add( newValue );
166
+ else if ( newValue === '' )
167
+ element.classList.remove( value );
168
+ else
169
+ element.classList.replace( value, newValue );
170
+ value = newValue;
171
+ } );
172
+ } else if ( isPlainObject( item ) ) {
173
+ setClassesObject( element, item );
174
+ } else {
175
+ element.classList.add( item );
176
+ }
177
+ }
178
+ }
179
+
180
+ class DynamicNode {
181
+ constructor( content ) {
182
+ this.content = content;
183
+ }
184
+ }
185
+
186
+ function appendNode( node, target ) {
187
+ if ( Array.isArray( node ) ) {
188
+ if ( node.some( item => item instanceof DynamicNode ) ) {
189
+ for ( const item of node ) {
190
+ if ( item instanceof DynamicNode )
191
+ appendNode( item.content, target );
192
+ else
193
+ target.append( item );
194
+ }
195
+ } else {
196
+ target.append( ...node );
197
+ }
198
+ } else if ( node instanceof DynamicNode ) {
199
+ appendNode( node.content, target );
200
+ } else {
201
+ target.append( node );
202
+ }
203
+ }
204
+
205
+ function insertBefore( node, parent, before ) {
206
+ if ( Array.isArray( node ) ) {
207
+ for ( const item of node ) {
208
+ if ( item instanceof DynamicNode )
209
+ insertBefore( item.content, parent, before );
210
+ else
211
+ parent.insertBefore( item, before );
212
+ }
213
+ } else if ( node instanceof DynamicNode ) {
214
+ insertBefore( node.content, before );
215
+ } else {
216
+ parent.insertBefore( node, before );
217
+ }
218
+ }
219
+
220
+ function removeNode( node ) {
221
+ if ( Array.isArray( node ) ) {
222
+ let parent = null, before = null;
223
+ for ( const item of node ) {
224
+ if ( item instanceof DynamicNode ) {
225
+ [ parent, before ] = removeNode( item.content );
226
+ } else {
227
+ parent = item.parentNode;
228
+ before = item.nextSibling;
229
+ parent.removeChild( item );
230
+ }
231
+ }
232
+ return [ parent, before ];
233
+ }
234
+
235
+ if ( node instanceof DynamicNode )
236
+ return removeNode( node.content );
237
+
238
+ const parent = node.parentNode;
239
+ const before = node.nextSibling;
240
+
241
+ parent.removeChild( node );
242
+
243
+ return [ parent, before ];
244
+ }
245
+
246
+ function replaceNode( newNode, node ) {
247
+ if ( newNode instanceof Node && node instanceof Node ) {
248
+ node.parentNode.replaceChild( newNode, node );
249
+ } else {
250
+ const [ parent, before ] = removeNode( node );
251
+
252
+ insertBefore( newNode, parent, before );
253
+ }
254
+ }
255
+
256
+ function ensureNotEmpty( node ) {
257
+ if ( isEmpty( node ) ) {
258
+ const comment = document.createComment( '' );
259
+ if ( Array.isArray( node ) )
260
+ node.push( comment );
261
+ else
262
+ return comment;
263
+ }
264
+ return node;
265
+ }
266
+
267
+ function isEmpty( node ) {
268
+ if ( Array.isArray( node ) ) {
269
+ for ( const item of node ) {
270
+ if ( !isEmpty( item ) )
271
+ return false;
272
+ }
273
+ return true;
274
+ }
275
+
276
+ return node == null;
277
+ }
278
+
279
+ function updateDynamicNode( node, context, template ) {
280
+ let content = null;
281
+
282
+ if ( node.content != null )
283
+ destroyContext( context );
284
+
285
+ if ( template != null )
286
+ content = withContext( context, () => make( template ) );
287
+
288
+ content = ensureNotEmpty( content );
289
+
290
+ if ( node.content != null ) {
291
+ replaceNode( content, node.content );
292
+ mountContext( context );
293
+ }
294
+
295
+ node.content = content;
296
+ }
297
+
298
+ function createDynamicDirective( template ) {
299
+ if ( template.length < 2 || typeof template[ 1 ] != 'function' )
300
+ throw new TypeError( 'Invalid dynamic template' );
301
+
302
+ const [ , type, ...args ] = template;
303
+
304
+ const result = new DynamicNode( null );
305
+
306
+ const context = createChildContext();
307
+
308
+ useReactiveWatch( type, update );
309
+
310
+ return result;
311
+
312
+ function update( type ) {
313
+ updateDynamicNode( result, context, [ type, ...args ] );
314
+ }
315
+ }
316
+
317
+ function createForDirective( template ) {
318
+ if ( template.length != 3 || typeof template[ 1 ] != 'function' || typeof template[ 2 ] != 'function' )
319
+ throw new TypeError( 'Invalid for template' );
320
+
321
+ let length = 0;
322
+ let items = null;
323
+
324
+ let watchingContent = false;
325
+
326
+ const result = new DynamicNode( null );
327
+
328
+ const context = createChildContext();
329
+
330
+ useReactiveWatch( watchLength, updateLength );
331
+
332
+ return result;
333
+
334
+ function watchLength() {
335
+ if ( !watchingContent )
336
+ return template[ 1 ].length();
337
+ }
338
+
339
+ function updateLength( newLength ) {
340
+ if ( !watchingContent ) {
341
+ update( template[ 1 ]() );
342
+ length = newLength;
343
+ }
344
+ }
345
+
346
+ function watchContent() {
347
+ // clone the array to watch all elements
348
+ return [ ...template[ 1 ]() ];
349
+ }
350
+
351
+ function updateContent( newItems ) {
352
+ if ( watchingContent )
353
+ update( newItems );
354
+ items = newItems;
355
+ length = newItems.length;
356
+ }
357
+
358
+ function update( newItems ) {
359
+ const nodes = [];
360
+ const itemContexts = [];
361
+
362
+ const oldItemContexts = context.children != null ? new Set( context.children ) : null;
363
+ let newItemContexts = null;
364
+
365
+ let containsMatchedByValue = false;
366
+
367
+ for ( let index = 0; index < newItems.length; index++ ) {
368
+ const value = newItems[ index ];
369
+
370
+ const isMatchedByValue = isPlainObjectOrArray( value );
371
+
372
+ if ( isMatchedByValue )
373
+ containsMatchedByValue = true;
374
+
375
+ let oldIndex = -1;
376
+
377
+ // reuse existing items - objects and arrays are matched by value, other items by index
378
+ if ( result.content != null ) {
379
+ if ( isMatchedByValue ) {
380
+ if ( items != null )
381
+ oldIndex = items.indexOf( value );
382
+ } else if ( index < length ) {
383
+ if ( items == null || !isPlainObjectOrArray( items[ index ] ) )
384
+ oldIndex = index;
385
+ }
386
+ }
387
+
388
+ if ( oldIndex >= 0 ) {
389
+ nodes.push( result.content[ oldIndex ] );
390
+
391
+ const itemContext = context.children[ oldIndex ];
392
+ itemContexts.push( itemContext );
393
+
394
+ if ( oldItemContexts != null )
395
+ oldItemContexts.delete( itemContext );
396
+ } else {
397
+ const itemContext = createContext( context );
398
+ itemContexts.push( itemContext );
399
+
400
+ if ( newItemContexts == null )
401
+ newItemContexts = [];
402
+ newItemContexts.push( itemContext );
403
+
404
+ const item = isMatchedByValue ? useConstant( value ) : template[ 1 ][ index ];
405
+
406
+ const itemTemplate = template[ 2 ]( item, index );
407
+
408
+ const node = withContext( itemContext, () => make( itemTemplate ) );
409
+
410
+ if ( Array.isArray( node ) )
411
+ nodes.push( new DynamicNode( node ) );
412
+ else
413
+ nodes.push( node );
414
+ }
415
+ }
416
+
417
+ ensureNotEmpty( nodes );
418
+
419
+ if ( result.content != null ) {
420
+ if ( oldItemContexts != null ) {
421
+ for ( const itemContext of oldItemContexts )
422
+ destroyContext( itemContext );
423
+ }
424
+
425
+ replaceNode( nodes, result.content );
426
+
427
+ if ( newItemContexts != null ) {
428
+ for ( const itemContext of newItemContexts )
429
+ mountContext( itemContext );
430
+ }
431
+ }
432
+
433
+ result.content = nodes;
434
+ if ( itemContexts.length > 0 )
435
+ context.children = itemContexts;
436
+ else
437
+ context.children = null;
438
+
439
+ if ( containsMatchedByValue && !watchingContent ) {
440
+ useReactiveWatch( watchContent, updateContent );
441
+ watchingContent = true;
442
+ }
443
+ }
444
+ }
445
+
446
+ function createIfDirective( template ) {
447
+ let numberOfConditions = 0;
448
+ let hasElse = false;
449
+
450
+ for ( let i = 1; i < template.length; i += 2 ) {
451
+ if ( i == template.length - 1 )
452
+ hasElse = true;
453
+ else if ( typeof template[ i ] == 'function' )
454
+ numberOfConditions++;
455
+ else
456
+ throw new TypeError( 'Invalid if template' );
457
+ }
458
+
459
+ if ( numberOfConditions == 0 )
460
+ throw new TypeError( 'Invalid if template' );
461
+
462
+ const result = new DynamicNode( null );
463
+
464
+ const context = createChildContext();
465
+
466
+ useReactiveWatch( watchState, update );
467
+
468
+ return result;
469
+
470
+ function watchState() {
471
+ let state = null;
472
+
473
+ for ( let i = 0; i < numberOfConditions; i++ ) {
474
+ if ( template[ 2 * i + 1 ]() ) {
475
+ state = 2 * i + 2;
476
+ break;
477
+ }
478
+ }
479
+
480
+ if ( state == null && hasElse )
481
+ state = template.length - 1;
482
+
483
+ return state;
484
+ }
485
+
486
+ function update( state ) {
487
+ updateDynamicNode( result, context, state != null ? template[ state ] : null );
488
+ }
489
+ }
490
+
491
+ function setStyles( element, styles ) {
492
+ if ( typeof styles == 'function' ) {
493
+ useReactive( () => {
494
+ const value = styles();
495
+ if ( isPlainObject( value ) ) {
496
+ element.style = '';
497
+ for ( const [ key, value ] of Object.entries( value ) )
498
+ setStyleProperty( element, key, value );
499
+ } else {
500
+ element.style = value;
501
+ }
502
+ } );
503
+ } else if ( isPlainObject( styles ) ) {
504
+ setStylesObject( element, styles );
505
+ } else {
506
+ element.style = styles;
507
+ }
508
+ }
509
+
510
+ function setStylesObject( element, styles ) {
511
+ for ( const [ key, value ] of Object.entries( styles ) ) {
512
+ if ( typeof value == 'function' )
513
+ useReactive( () => element.style[ key ] = value() );
514
+ else
515
+ setStyleProperty( element, key, value );
516
+ }
517
+ }
518
+
519
+ function setStyleProperty( element, key, value ) {
520
+ if ( key.startsWith( '--' ) )
521
+ element.style.setProperty( key, value );
522
+ else
523
+ element.style[ key ] = value;
524
+ }
525
+
526
+ const directives = {
527
+ if: createIfDirective,
528
+ for: createForDirective,
529
+ dynamic: createDynamicDirective,
530
+ };
531
+
532
+ function make( template ) {
533
+ if ( template == null )
534
+ return [];
535
+
536
+ if ( Array.isArray( template ) ) {
537
+ if ( template.length == 0 )
538
+ return [];
539
+
540
+ if ( template.length == 1 && Array.isArray( template[ 0 ] ) ) {
541
+ const result = [];
542
+ for ( const item of template[ 0 ] )
543
+ appendToArray( make( item ), result );
544
+ return result;
545
+ }
546
+
547
+ if ( typeof template[ 0 ] == 'string' ) {
548
+ const tag = template[ 0 ];
549
+
550
+ if ( directives.hasOwnProperty( tag ) )
551
+ return directives[ tag ]( template );
552
+
553
+ const element = document.createElement( tag );
554
+
555
+ let i = 1;
556
+ if ( i < template.length && isPlainObject( template[ i ] ) )
557
+ setElementProperties( element, template[ i++ ] );
558
+
559
+ for ( ; i < template.length; i++ )
560
+ appendNode( make( template[ i ] ), element );
561
+
562
+ return element;
563
+ }
564
+
565
+ if ( typeof template[ 0 ] == 'function' )
566
+ return withChildContext( () => make( createComponent( template ) ) );
567
+
568
+ throw new TypeError( 'Unexpected value passed to make()' );
569
+ }
570
+
571
+ if ( typeof template == 'function' ) {
572
+ const textNode = document.createTextNode( '' );
573
+ useReactive( () => textNode.textContent = template() );
574
+ return textNode;
575
+ }
576
+
577
+ if ( template instanceof Node || template instanceof DynamicNode )
578
+ return template;
579
+
580
+ return document.createTextNode( template );
581
+ }
582
+
583
+ function setElementProperties( element, properties ) {
584
+ for ( const [ key, value ] of Object.entries( properties ) ) {
585
+ switch ( key ) {
586
+ case 'class':
587
+ setClasses( element, value );
588
+ break;
589
+ case 'style':
590
+ setStyles( element, value );
591
+ break;
592
+ case 'ref':
593
+ value( element );
594
+ break;
595
+ default:
596
+ setElementProperty( element, key, value );
597
+ break;
598
+ }
599
+ }
600
+ }
601
+
602
+ function setElementProperty( element, key, value ) {
603
+ if ( key in element ) {
604
+ if ( typeof value == 'function' && !key.startsWith( 'on' ) )
605
+ useReactive( () => element[ key ] = value() );
606
+ else
607
+ element[ key ] = value;
608
+ } else {
609
+ if ( typeof value == 'function' )
610
+ useReactive( () => element.setAttribute( key, value() ) );
611
+ else
612
+ element.setAttribute( key, value );
613
+ }
614
+ }
615
+
616
+ function mount( component, target ) {
617
+ const context = {
618
+ parent: null,
619
+ children: [],
620
+ };
621
+
622
+ const root = withContext( context, () => make( component( {}, [] ) ) );
623
+
624
+ appendNode( root, target );
625
+
626
+ mountContext( context );
627
+
628
+ return {
629
+ destroy,
630
+ };
631
+
632
+ function destroy() {
633
+ destroyContext( context );
634
+ removeNode( root );
635
+ }
636
+ }
637
+
638
+ export { mount, onDestroy, onMount };
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "leaner",
3
+ "version": "0.0.1",
4
+ "description": "Leaner.js",
5
+ "type": "module",
6
+ "files": [
7
+ "dist"
8
+ ],
9
+ "exports": {
10
+ ".": "./dist/core.js",
11
+ "./web": "./dist/web.js",
12
+ "./dist/*": "./dist/*",
13
+ "./package.json": "./package.json"
14
+ },
15
+ "scripts": {
16
+ "build": "rollup -c"
17
+ },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/mimecorg/leaner.git"
21
+ },
22
+ "keywords": [
23
+ "leaner",
24
+ "javascript",
25
+ "framework"
26
+ ],
27
+ "author": "Michał Męciński",
28
+ "license": "MIT",
29
+ "bugs": {
30
+ "url": "https://github.com/mimecorg/leaner/issues"
31
+ },
32
+ "homepage": "https://github.com/mimecorg/leaner",
33
+ "devDependencies": {
34
+ "rollup": "^4.17.2"
35
+ }
36
+ }