All files / internal / engine / renderer.ts

99.55% Branches 220/221
100.00% Lines 491/491
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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
 
 
 
x40
x40
x40
x40
x40
x40
 
 
 
 
 
 
x40
 
x40
x721
x721
x721
x721
x721
x2884
x721
x721
 
 
 
 
 
 
x40
 
 
x721
 
 
x721
x2108
x2108
 
 
x721
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
x721
x12369
x13625
x13625
x12369
x12369
 
 
 
 
 
 
x721
 
 
x721
x6948
x2316
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
x721
x5010
x1670
x1670
x1670
x15152
x15154
x15154
x15154
x15152
x15216
x15216
x15152
x15155
x15155
x15155
x15152
x28564
x28564
x2616
x2616
x1670
 
 
 
 
 
 
x721
 
 
x761
x9120
x9120
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
x761
x1130
x1130
 
 
 
 
 
 
x761
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
x761
x3827
x3828
x3828
x3827
x3827
x4374
x4374
x4374
x4374
 
x4374
x6892
x48244
x6892
x3827
 
 
x721
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
x721
x1208
 
x1208
x1217
x1217
 
x4104
x1208
 
x14295
x1208
x1208
x1213
x1213
x1213
x1217
x1217
x1213
 
x1208
x1208
x1222
x1222
x1222
x1677
x1208
 
 
 
 
 
 
x721
 
x4284
x9266
x9266
x9821
x29463
 
x4284
x4311
x4311
x4313
x4313
x4311
 
x9891
x7832
x341268
x56468
x56491
x56491
x56468
x56540
x56540
x56468
x56472
x56472
x56468
 
 
x4284
 
 
x4284
x4284
 
x51831
x51831
x93746
x93746
 
x51831
x51835
x51835
x51831
x51832
x51832
 
x63466
x426955
x51831
x51898
x51900
x51900
x51900
x51900
x51900
x51898
x51898
x51831
x51907
x51907
x51831
x51843
x51844
x51844
x51844
x51844
x51843
x51843
x51831
x51925
x51925
x51831
 
x11401
x11428
x11428
x7846
x86814
x17527
x7846
x7873
x7873
x4284
x4298
x4298
x4284
 
x8866
x348146
x57895
 
x8866
x8893
x44465
x8893
x8866
x4284
 
 
x721
 
 
x721
x778
x788
x788
x778
x4794
x799
x778
x778
x808
x4520
x808
x808
x778
x778
 
 
x721
x802
x802
x802
x802
x802
 
 
x721
x748
x751
x751
x797
x797
x797
x773
x789
x3264
x816
x835
x4175
x835
x789
x789
x789
x748
 
 
 
 
 
 
 
 
 
x721
x2960
x740
x763
x763
x810
x813
x813
x854
x854
x763
x766
x766
x740
x740
x740
 
 
x721
x1402
x1402
x1402
x1402
x1402
x1421
x1452
x1452
x1445
x1445
x8670
x1439
x1441
x1441
x1445
x1445
x1445
x1454
x5844
x1461
x1445
 
x1445
x1445
x1445
x1445
x1445
x1447
x1447
x1447
x1445
x1402
x721
 
 
x721
 
 
x721
x723
x723
x2892
x723
 
 
x721
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
x721
x1104
x1104
x1104
x1104
x1104
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
x721
x736
x736
x761
x761
x761
x736
x736
 
 
 
 
 
 
x721
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
x721
x814
x814
x2442
x814
x814
x814
x814
x814
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
x721
x738
x739
x739
x754
x738
x738
x738
x738
x738
 
 
 
 
 
 
 
 
 
 
 
 
 
x721
x807
x807
 
 
 
 
 
 
 
 
 
 
 
 
 
 
x721
x815
x815
x815
 
 
 
 
 
 
x721
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
x721
x899
x899
x899
x899
x3114
x1038
x1038
x899
x899
x899
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
x721
x1516
x3708
x2192
x2192
x2192
x2199
x2199
x2192
x2192
x3151
x1635
x1635
x1635
x1836
x1836
x1836
x1789
x1789
x1789
x1516
x1516
 
 
x721
x721
x721
x721
x721
x721
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
x721
x85764
x85764
x170803
x526242
x175414
x170803
x214576
x214576
x221444
x221444
x221603
x221603
x221444
x214576
x170803
x85764
x85764
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
x721
 
x6883
x7450
x37250
x7450
x7850
x7850
x8253
x8253
x34276
x8569
x8340
x7850
 
x7850
x10001
x10001
x7850
x7450
x7450
x6883
 
x6883
x6883
x6883
x7226
x7226
x6883
x7101
x7101
x6883
x6883
 
 
 
x721
 
 
x721
x9034
x10788
x10788
x22823
x9034
x18438
x9404
x9404
x9404
x9404
x9407
x9407
x9771
x9771
x18088
x9054
x9054
x9054
x9054
x9058
x9058
x9054
x9056
x9056
x9054
x9056
x9056
x9054
x9059
x9059
x9069
x9069
x18087
x9053
x9053
x9053
x36212
x9053
x9059
x9059
x9066
x9066
x24218
x15184
x15184
x15184
x15184
x15187
x15187
x15187
x21331
x21331
x9034
x9051
x9051
x9034
 
 
 
 
 
 
 
 
 
 
 
 
 
 
x721
x88483
x88483
 
 
 
 
 
 
 
 
 
 
 
 
 
 
x721
x3618
x3618
 
 
x721
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
x721
x784
x788
x788
x788
x784
x821
x821
x784
 
 
x40
 
 
 
 
 
 
 
 
 
 
 
 
x40
x66
x66
x40
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 











































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































I

























































































































































































































































// Imports
import type { Arg, Arrayable, callback, DeepReadonly, NonVoid, Nullable, Optional } from "@libs/typing/types"
import type { Cache, Directive } from "./directive.ts"
import { escape } from "@std/regexp"
import { AsyncFunction } from "@libs/typing/func"
import { Context } from "@libs/reactive"
import { Phase } from "./phase.ts"
import { delay } from "@std/async"
export { Context, Phase }
export type { Arg, Arrayable, Cache, callback, Directive, NonVoid, Nullable, Optional }
export type * from "./directive.ts"

/**
 * Mizu directive renderer.
 */
export class Renderer {
  /** {@linkcode Renderer} constructor. */
  constructor(window: Window, { warn, debug, directives = [] } = {} as RendererOptions) {
    this.window = window as Renderer["window"]
    this.cache("*", new WeakMap())
    this.#warn = warn
    this.#debug = debug
    this.#directives = [] as Directive[]
    this.#flush = { request: Promise.withResolvers<true>(), response: Promise.withResolvers<void>() }
    this.ready = this.load(directives)
  }

  /**
   * Whether the {@linkcode Renderer} is ready to be used.
   *
   * This promise resolves once the initial {@linkcode Renderer.load()} call is completed.
   */
  readonly ready: Promise<this>

  /** Linked {@linkcode https://developer.mozilla.org/docs/Web/API/Window | Window}. */
  readonly window: VirtualWindow

  /** Linked {@linkcode https://developer.mozilla.org/docs/Web/API/Document | Document}. */
  get document(): Document {
    return this.window.document
  }

  /** Internal cache registries. */
  readonly #cache = new Map<Directive["name"], unknown>()

  /**
   * Retrieve {@linkcode Directive}-specific cache registry.
   *
   * Directive-specific caches can be used to store related data.
   * These are automatically exposed by {@linkcode Renderer.render()} during {@linkcode Directive.setup()}, {@linkcode Directive.execute()} and {@linkcode Directive.cleanup()} executions.
   *
   * ```ts
   * import { Window } from "@mizu/internal/vdom"
   *
   * const directive = {
   *   name: "*foo",
   *   phase: Phase.TESTING,
   *   init(renderer) {
   *     if (!renderer.cache(this.name)) {
   *       renderer.cache<Cache<typeof directive>>(this.name, new WeakSet())
   *     }
   *   },
   *   setup(renderer, element, { cache }) {
   *      console.assert(cache instanceof WeakSet)
   *      console.assert(renderer.cache(directive.name) instanceof WeakSet)
   *      cache.add(element)
   *   }
   * } as Directive<WeakSet<HTMLElement | Comment>> & { name: string }
   *
   * const renderer = await new Renderer(new Window(), { directives: [directive] }).ready
   * const element = renderer.createElement("div", { attributes: { "*foo": "" } })
   * await renderer.render(element)
   * console.assert(renderer.cache<Cache<typeof directive>>(directive.name).has(element))
   * ```
   */
  cache<T>(directive: Directive["name"]): T
  /**
   * Set {@linkcode Directive}-specific cache registry.
   *
   * These are expected to be initialized by {@linkcode Renderer.load()} during {@linkcode Directive.init()} execution if a cache is needed.
   *
   * ```ts
   * import { Window } from "@mizu/internal/vdom"
   *
   * const directive = {
   *   name: "*foo",
   *   phase: Phase.TESTING,
   *   init(renderer) {
   *     renderer.cache(this.name, new WeakSet())
   *   },
   * } as Directive<WeakSet<HTMLElement | Comment>> & { name: string }
   *
   * const renderer = await new Renderer(new Window(), { directives: [directive] }).ready
   * console.assert(renderer.cache(directive.name) instanceof WeakSet)
   * ```
   */
  cache<T>(directive: Directive["name"], cache: T): T
  /**
   * Retrieve generic cache registry.
   *
   * This cache is automatically created upon {@linkcode Renderer} instantiation.
   * It is shared between all {@linkcode Renderer.directives} and is mostly used to check whether a node was already processed,
   * and to map back {@linkcode https://developer.mozilla.org/docs/Web/API/Comment | Comment} nodes to their original {@linkcode https://developer.mozilla.org/docs/Web/API/HTMLElement | HTMLElement} if they were replaced by {@linkcode Renderer.comment()}.
   *
   * This cache should not be used to store {@linkcode Directive}-specific data.
   *
   * ```ts
   * import { Window } from "@mizu/internal/vdom"
   * const renderer = await new Renderer(new Window()).ready
   *
   * console.assert(renderer.cache("*") instanceof WeakMap)
   * ```
   */
  cache<T>(directive: "*"): WeakMap<HTMLElement | Comment, HTMLElement>
  cache<T>(directive: Directive["name"], cache?: T): Nullable<T> {
    if (cache && (!this.#cache.has(directive))) {
      this.#cache.set(directive, cache)
    }
    return this.#cache.get(directive) as Optional<T> ?? null
  }

  /**
   * {@linkcode Directive} list.
   *
   * It contains any `Directive` that was registered during {@linkcode Renderer} instantiation.
   */
  #directives

  /** {@linkcode Directive} list. */
  get directives(): Readonly<Directive[]> {
    return [...this.#directives]
  }

  /**
   * Load additional {@linkcode Directive}s.
   *
   * A `Directive` needs to have both a valid {@linkcode Directive.phase} and {@linkcode Directive.name} to be valid.
   * If a `Directive` with the same name already exists, it is ignored.
   *
   * It is possible to dynamically {@linkcode https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import | import()} a `Directive` by passing a `string` instead.
   * Note that in this case the resolved module must have an {@linkcode https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/export#using_the_default_export | export default} statement.
   *
   * ```ts
   * import { Window } from "@mizu/internal/vdom"
   * const renderer = await new Renderer(new Window()).ready
   *
   * const directive = {
   *   name: "*foo",
   *   phase: Phase.TESTING
   * } as Directive & { name: string }
   *
   * await renderer.load([directive, import.meta.resolve("@mizu/test")])
   * console.assert(renderer.directives.includes(directive))
   * ```
   */
  async load(directives: Arrayable<Arrayable<Partial<Directive> | string>>): Promise<this> {
    const loaded = (await Promise.all<Arrayable<Directive>>(([directives].flat(Infinity) as Array<Directive | string>)
      .map(async (directive) => typeof directive === "string" ? (await import(directive)).default : directive)))
      .flat(Infinity) as Array<Directive>
    for (const directive of loaded) {
      if ((!directive?.name) || (!Number.isFinite(directive?.phase)) || (Number(directive?.phase) < 0)) {
        const object = JSON.stringify(directive, (_, value) => (value instanceof Function) ? `[[Function]]` : value)
        throw new SyntaxError(`Failed to load directive: Malformed directive: ${object}`)
      }
      if (directive.phase === Phase.META) {
        continue
      }
      if (this.#directives.some((existing) => `${existing.name}` === `${directive.name}`)) {
        this.warn(`Directive [${directive.name}] is already loaded, skipping`)
        continue
      }
      await directive.init?.(this)
      this.#directives.push(directive as Directive)
    }
    this.#directives.sort((a, b) => a.phase - b.phase)
    return this
  }

  /**
   * Internal identifier prefix.
   *
   * This is used to avoid conflicts with user-defined variables in expressions.
   */
  static readonly internal = "__mizu_internal" as const

  /** Alias to {@linkcode Renderer.internal}. */
  get #internal() {
    return (this.constructor as typeof Renderer).internal
  }

  /**
   * Generate an internal identifier for specified name by prefixing it with {@linkcode Renderer.internal}.
   *
   * When creating internal variables or functions in expressions, this method should always be used to name them.
   * It ensures that they won't collide with end-user-defined variables or functions.
   *
   * ```ts
   * import { Window } from "@mizu/internal/vdom"
   * const renderer = await new Renderer(new Window()).ready
   *
   * console.assert(renderer.internal("foo").startsWith(`${Renderer.internal}_`))
   * ```
   */
  internal(name: string): `${typeof Renderer.internal}_${string}`
  /**
   * Retrieve {@linkcode Renderer.internal} prefix.
   *
   * ```ts
   * import { Window } from "@mizu/internal/vdom"
   * const renderer = await new Renderer(new Window()).ready
   *
   * console.assert(renderer.internal() === Renderer.internal)
   * ```
   */
  internal(): typeof Renderer.internal
  internal(name = "") {
    return `${this.#internal}${name ? `_${name}` : ""}`
  }

  /**
   * Internal expressions cache.
   *
   * This is used to store compiled expressions for faster evaluation.
   */
  readonly #expressions = new WeakMap<HTMLElement | Comment | Renderer, Record<PropertyKey, ReturnType<typeof AsyncFunction>>>()

  /**
   * Evaluate an expression with given {@linkcode https://developer.mozilla.org/docs/Web/API/HTMLElement | HTMLElement} (or {@linkcode https://developer.mozilla.org/docs/Web/API/Comment | Comment}), {@linkcode Context}, {@linkcode State} and arguments.
   *
   * Passed `HTMLElement` or `Comment` can be accessed through the `this` keyword in the expression.
   *
   * Both `context` and `state` are exposed through {@linkcode https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/with | with} statements,
   * meaning that their properties can be accessed directly in the expression without prefixing them.
   * The difference between both is that the latter is not reactive and is intended to be used for specific stateful data added by a {@linkcode Directive}.
   *
   * > [!NOTE]
   * > The root {@linkcode Renderer.internal} prefix is used internally to manage evaluation state, and thus cannot be used as a variable name.
   *
   * ```ts
   * import { Window } from "@mizu/internal/vdom"
   * const renderer = await new Renderer(new Window()).ready
   *
   * console.assert(await renderer.evaluate(null, "1 + 1") === 2)
   * console.assert(await renderer.evaluate(null, "foo => foo", { args: ["bar"] }) === "bar")
   * console.assert(await renderer.evaluate(null, "$foo", { state: { $foo: "bar" } }) === "bar")
   * ```
   */
  async evaluate(that: Nullable<HTMLElement | Comment>, expression: string, { context = new Context(), state = {}, args } = {} as RendererEvaluateOptions): Promise<unknown> {
    if (this.#internal in context.target) {
      throw new TypeError(`"${this.#internal}" is a reserved variable name`)
    }
    const these = that ?? this
    if (!this.#expressions.get(these)?.[expression]) {
      const cache = (!this.#expressions.has(these) ? this.#expressions.set(these, {}) : this.#expressions).get(these)!
      cache[expression] = new AsyncFunction(
        this.#internal,
        `with(${this.#internal}.state){with(${this.#internal}.context){${this.#internal}.result=${expression};if(${this.#internal}.args)${this.#internal}.result=${this.#internal}.result?.call?.(this,...${this.#internal}.args)}}return ${this.#internal}.result`,
      )
    }
    const compiled = this.#expressions.get(these)![expression]
    const internal = { this: that, context: context.target, state, args, result: undefined }
    return await compiled.call(that, internal)
  }

  /** Explicit rendering attribute name. */
  static readonly #explicit = "*mizu"

  /**
   * Render {@linkcode https://developer.mozilla.org/docs/Web/API/Element | Element} and its subtree with specified {@linkcode Context} and {@linkcode State} against {@linkcode Renderer.directives}.
   *
   * ```ts
   * import { Window } from "@mizu/internal/vdom"
   * import _test from "@mizu/test"
   * const renderer = await new Renderer(new Window(), { directives: [ _test ] }).ready
   * const element = renderer.createElement("div", { attributes: { "~test.text": "foo" } })
   *
   * const result = await renderer.render(element, { context: new Context({ foo: "bar" }) })
   * console.assert(result.textContent === "bar")
   * ```
   */
  async render<T extends Element>(element: T, options?: Omit<RendererRenderOptions, "select" | "stringify"> & { stringify?: false }): Promise<T>
  /**
   * Render {@linkcode https://developer.mozilla.org/docs/Web/API/Element | Element} and its subtree with specified {@linkcode Context} and {@linkcode State} against {@linkcode Renderer.directives} and {@link https://developer.mozilla.org/docs/Web/API/Document/querySelector | query select} the return using a {@link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_selectors | CSS selector}.
   *
   * ```ts
   * import { Window } from "@mizu/internal/vdom"
   * import _test from "@mizu/test"
   * const renderer = await new Renderer(new Window(), { directives: [ _test ] }).ready
   * const element = renderer.createElement("div", { innerHTML: renderer.createElement("span", { attributes: { "~test.text": "foo" } }).outerHTML })
   *
   * const result = await renderer.render(element, { context: new Context({ foo: "bar" }), select: "span" })
   * console.assert(result?.tagName === "SPAN")
   * console.assert(result?.textContent === "bar")
   * ```
   */
  async render<T extends Element>(element: HTMLElement, options?: Omit<RendererRenderOptions, "select" | "stringify"> & Required<Pick<RendererRenderOptions, "select">> & { stringify?: false }): Promise<Nullable<T>>
  /**
   * Render {@linkcode https://developer.mozilla.org/docs/Web/API/Element | Element} and its subtree with specified {@linkcode Context} and {@linkcode State} against {@linkcode Renderer.directives} and returns it as an HTML string.
   *
   * ```ts
   * import { Window } from "@mizu/internal/vdom"
   * import _test from "@mizu/test"
   * const renderer = await new Renderer(new Window(), { directives: [ _test ] }).ready
   * const element = renderer.createElement("div", { attributes: { "~test.text": "foo" } })
   *
   * const result = await renderer.render(element, { context: new Context({ foo: "bar" }), stringify: true })
   * console.assert(result.startsWith("<!DOCTYPE html>"))
   * ```
   */
  async render(element: HTMLElement, options?: Omit<RendererRenderOptions, "reactive" | "stringify"> & { stringify: true }): Promise<string>
  async render<T extends Element>(element = this.document.documentElement, { context = new Context(), state = {}, implicit = true, reactive = false, select = "", stringify = false, throw: _throw = false } = {} as RendererRenderOptions) {
    await this.ready
    // Create a new sub-context when reactivity is enabled to avoid polluting the given root context
    if (reactive) {
      context = context.with({})
    }
    // Search for all elements with the mizu attribute, and filter out all elements that have an ancestor with the mizu attribute (since they will be rendered anyway)
    let subtrees = implicit || (element.hasAttribute(Renderer.#explicit)) ? [element] : Array.from(element.querySelectorAll<HTMLElement>(`[${escape(Renderer.#explicit)}]`))
    subtrees = subtrees.filter((element) => subtrees.every((ancestor) => (ancestor === element) || (!ancestor.contains(element))))
    // Render subtrees
    const rendered = await Promise.allSettled(subtrees.map((element) => this.#render(element, { context, state, reactive, root: { context, state } })))
    const rejected = rendered.filter((render) => render.status === "rejected")
    if (rejected.length) {
      const error = new AggregateError(rejected.map((render) => render.reason))
      this.warn(error.message)
      if (_throw) {
        throw error
      }
    }
    // Process result
    const result = select ? element.querySelector<T>(select) : element
    if (stringify) {
      const html = result?.outerHTML ?? ""
      return select ? html : `<!DOCTYPE html>${html}`
    }
    return result as T
  }

  /**
   * Used by {@linkcode Renderer.render()} to recursively render an {@linkcode https://developer.mozilla.org/docs/Web/API/Element | Element} and its subtree.
   *
   * For more information, see the {@link https://mizu.sh/#concept-rendering | mizu.sh documentation}.
   */
  async #render(element: HTMLElement | Comment, { context, state, reactive, root }: { context: Context; state: State; reactive: boolean; root: InitialContextState }) {
    // 1. Ignore non-element nodes unless they were processed before and put into cache
    if ((element.nodeType !== this.window.Node.ELEMENT_NODE) && (!this.cache("*").has(element))) {
      return
    }
    try {
      state = { ...state }
      // R1. Watch context and restore queued context state
      if (reactive) {
        this.#watch(context, element)
        if (this.#queued.get(element)?.entrypoint === false) {
          Object.assign(state, this.#queued.get(element)!.state)
        }
      }
      // 2. Setup directives
      const forced = new WeakMap<Directive, boolean>()
      for (const directive of this.#directives) {
        const changes = await directive.setup?.(this, element, { cache: this.cache(directive.name), context, state, root })
        if (changes === false) {
          return
        }
        if (changes?.state) {
          Object.assign(state, changes.state)
        }
        if (typeof changes?.execute === "boolean") {
          forced.set(directive, changes.execute)
        }
      }

      // 3. Retrieve source element
      const source = this.cache("*").get(element) ?? element

      // 4. Execute directives
      const phases = new Map<Phase, Directive["name"]>()
      for (const directive of this.#directives) {
        // 4.1 Check eligibility
        const attributes = this.getAttributes(source, directive.name)
        if ((forced.get(directive) === false) || ((!forced.has(directive)) && (!attributes.length))) {
          continue
        }
        // 4.2 Notify misuses
        if (phases.has(directive.phase)) {
          this.warn(`Using [${directive.name}] and [${phases.get(directive.phase)}] directives together might result in unexpected behaviour`, element)
        }
        if ((attributes.length > 1) && (!directive.multiple)) {
          this.warn(`Using multiple [${directive.name}] directives might result in unexpected behaviour`, element)
        }
        // 4.3 Execute directive
        phases.set(directive.phase, directive.name)
        const changes = await directive.execute?.(this, element, { cache: this.cache(directive.name), context, state, attributes, root })
        if (changes?.element) {
          if (reactive && (this.#watched.get(context)?.has(element))) {
            this.#unwatch(context, element)
            this.#watch(context, changes.element)
            this.#watched.get(context)!.get(element)!.properties.forEach((property) => this.#watched.get(context)!.get(changes.element!)!.properties.add(property))
            this.#watched.get(context)!.delete(element)
          }
          element = changes.element
        }
        if (changes?.final) {
          return
        }
        if (changes?.context) {
          if (reactive && (this.#watched.get(context)?.has(element))) {
            this.#unwatch(context, element)
            this.#watch(changes.context, element)
            this.#watched.get(context)!.get(element)!.properties.forEach((property) => this.#watched.get(changes.context!)!.get(element)!.properties.add(property))
          }
          context = changes.context
        }
        if (changes?.state) {
          Object.assign(state, changes.state)
        }
      }
      // 5. Recurse on child nodes
      if (reactive) {
        this.#unwatch(context, element)
      }
      for (const child of Array.from(element.childNodes) as Array<HTMLElement | Comment>) {
        await this.#render(child, { context, state, reactive, root })
      }
      if (reactive) {
        this.#watch(context, element)
      }
    } catch (error) {
      this.warn(error.message, element)
      throw error
    } finally {
      // 6. Cleanup directives
      for (const directive of this.#directives) {
        await directive.cleanup?.(this, element, { cache: this.cache(directive.name), context, state, root })
      }
      // R2. Unwatch context and start reacting
      if (reactive) {
        this.#unwatch(context, element)
        this.#react(element, { context, state, root })
      }
    }
  }

  /** Watched {@linkcode Context}s. */
  readonly #watched = new WeakMap<Context, WeakMap<HTMLElement | Comment, { properties: Set<string>; _get: Nullable<callback>; _set: Nullable<callback>; _call: Nullable<callback> }>>()

  /** Start watching a {@linkcode Context} for properties read operations. */
  #watch(context: Context, element: HTMLElement | Comment) {
    if (!this.#watched.has(context)) {
      this.#watched.set(context, new WeakMap())
    }
    if (!this.#watched.get(context)!.has(element)) {
      this.#watched.get(context)!.set(element, { properties: new Set(), _get: null, _set: null, _call: null })
    }
    const watched = this.#watched.get(context)!.get(element)!
    if (!watched._get) {
      watched._get = ({ detail: { path, property } }: CustomEvent) => {
        watched.properties.add([...path, property].join("."))
      }
    }
    context.addEventListener("get", watched._get as EventListener)
  }

  /** Stop watching a {@linkcode Context} for properties read operations. */
  #unwatch(context: Context, element: HTMLElement | Comment) {
    if (this.#watched.get(context)?.has(element)) {
      const watched = this.#watched.get(context)!.get(element)!
      context.removeEventListener("get", watched._get as EventListener)
    }
  }

  /** Start reacting to any {@linkcode Context} properties changes. */
  #react(element: HTMLElement | Comment, { context, state, root }: { context: Context; state: State; root: InitialContextState }) {
    if (!this.#watched.get(context)?.get(element)?.properties.size) {
      return
    }
    this.#unwatch(context, element)
    const watched = this.#watched.get(context)!.get(element)!
    watched._get = null
    if (!watched._set) {
      watched._set = ({ detail: { path, property } }: CustomEvent) => {
        const key = [...path, property].join(".")
        if (watched.properties.has(key)) {
          this.debug(`"${key}" has been modified, queuing reactive render request`, element)
          this.#queueReactiveRender(element, { context, state, root })
        }
      }
      context.addEventListener("set", watched._set as EventListener)
    }
  }

  /**
   * Queue a {@linkcode Renderer.render()} request emitted by a reactive change.
   *
   * This method automatically discards render requests that would be already covered by a parent element,
   * and removes any queued render request that could be covered by the current element.
   *
   * The actual rendering call is throttled to prevent over-rendering.
   */
  #queueReactiveRender(element: HTMLElement | Comment, options: { context: Context; state: State; root: InitialContextState }) {
    this.#queued.set(element, { ...options, entrypoint: true })
    this.#queued.forEach((_, element) => {
      let ancestor = element.parentElement
      while (ancestor) {
        if (this.#queued.has(ancestor)) {
          break
        }
        ancestor = ancestor.parentElement
      }
      if (ancestor) {
        this.#queued.get(element)!.entrypoint = false
      }
    })
    this.#reactiveRender()
  }

  /** Throttled {@linkcode Renderer.render()} call. */
  #reactiveRender = ((throttle = 50, grace = 25) => {
    const controller = new AbortController()
    let t = NaN
    let active = false
    let flushed = false
    return async () => {
      if (active || (!this.#queued.size) || (Date.now() - t <= throttle)) {
        return
      }
      try {
        active = true
        flushed = await Promise.race([delay(grace, { signal: controller.signal }), this.#flush.request.promise]) as boolean
        if (flushed) {
          controller.abort()
        }
        this.debug("processing queued reactive render requests")
        await Promise.all(
          Array.from(this.#queued.entries()).map(([element, { entrypoint, ...options }]) => {
            if (entrypoint && (element.isConnected)) {
              return this.#render(element, { reactive: true, ...options })
            }
          }),
        )
      } finally {
        t = Date.now()
        this.#queued.clear()
        active = false
        if (flushed) {
          flushed = false
          this.#flush.response.resolve()
        }
      }
    }
  })()

  /** Track reactive render queue flush requests and responses. */
  #flush

  /** Flush the reactive render queue. */
  async flushReactiveRenderQueue(): Promise<void> {
    this.#flush.request.resolve(true)
    await this.#flush.response.promise
    this.#flush = { request: Promise.withResolvers<true>(), response: Promise.withResolvers<void>() }
  }

  /** Queued reactive render requests. */
  #queued = new Map<HTMLElement | Comment, { context: Context; state: State; root: InitialContextState; entrypoint: boolean }>()

  /**
   * Create a new {@linkcode https://developer.mozilla.org/docs/Web/API/HTMLElement | HTMLElement} within {@linkcode Renderer.document}.
   *
   * It is possible to specify additional properties that will be assigned to the element.
   * The `attributes` property is handled by {@linkcode Renderer.setAttribute()} which allows to set attributes with non-standard characters.
   *
   * ```ts
   * import { Window } from "@mizu/internal/vdom"
   * const renderer = await new Renderer(new Window()).ready
   *
   * const element = renderer.createElement("div", { innerHTML: "foo", attributes: { "*foo": "bar" } })
   * console.assert(element.tagName === "DIV")
   * console.assert(element.innerHTML === "foo")
   * console.assert(element.getAttribute("*foo") === "bar")
   * ```
   */
  createElement<T extends HTMLElement>(tagname: string, properties = {} as Record<PropertyKey, unknown>): T {
    const { attributes = {}, ...rest } = properties
    const element = Object.assign(this.document.createElement(tagname), rest)
    Object.entries(attributes as Record<PropertyKey, unknown>).forEach(([name, value]) => this.setAttribute(element, name, `${value}`))
    return element as unknown as T
  }

  /**
   * Replace a {@linkcode https://developer.mozilla.org/docs/Web/API/HTMLElement | HTMLElement} with another {@linkcode https://developer.mozilla.org/fr/docs/Web/API/Node/childNodes | HTMLElement.childNodes}.
   *
   * Note that the `HTMLElement` is entirely replaced, meaning that is is actually removed from the DOM.
   *
   * ```ts
   * import { Window } from "@mizu/internal/vdom"
   * const renderer = await new Renderer(new Window()).ready
   * const parent = renderer.createElement("div")
   * const slot = parent.appendChild(renderer.createElement("slot")) as HTMLSlotElement
   * const content = renderer.createElement("div", { innerHTML: "<span>foo</span><span>bar</span>" })
   *
   * renderer.replaceElementWithChildNodes(slot, content)
   * console.assert(parent.innerHTML === "<span>foo</span><span>bar</span>")
   * ```
   */
  replaceElementWithChildNodes(a: HTMLElement, b: HTMLElement) {
    let position = a as HTMLElement
    for (const child of Array.from(b.cloneNode(true).childNodes) as HTMLElement[]) {
      a.parentNode?.insertBefore(child, position.nextSibling)
      position = child
    }
    a.remove()
  }

  /**
   * Internal {@linkcode https://developer.mozilla.org/en-US/docs/Web/API/Comment | Comment} registry.
   *
   * It is used to map {@linkcode https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement | HTMLElement} to their `Comment` replacement.
   */
  readonly #comments = new WeakMap<HTMLElement, Comment>()

  /**
   * Replace a {@linkcode https://developer.mozilla.org/docs/Web/API/HTMLElement | HTMLElement} by a {@linkcode https://developer.mozilla.org/en-US/docs/Web/API/Comment | Comment}.
   *
   * Specified `directive` and `expression` are used to set {@linkcode https://developer.mozilla.org/docs/Web/API/Node/nodeValue | Comment.nodeValue} and help identify which {@linkcode Directive} performed the replacement.
   *
   * Use {@linkcode Renderer.uncomment()} to restore the original `HTMLElement`.
   * Original `HTMLElement` can be retrieved through the generic {@linkcode Renderer.cache()}.
   * If you hold a reference to a replaced `HTMLElement`, use {@linkcode Renderer.getComment()} to retrieve the replacement `Comment`.
   *
   * ```ts
   * import { Window } from "@mizu/internal/vdom"
   * const renderer = await new Renderer(new Window()).ready
   * const parent = renderer.createElement("div")
   * const element = parent.appendChild(renderer.createElement("div"))
   *
   * const comment = renderer.comment(element, { directive: "foo", expression: "bar" })
   * console.assert(!parent.contains(element))
   * console.assert(parent.contains(comment))
   * console.assert(renderer.cache("*").get(comment) === element)
   * ```
   */
  comment(element: HTMLElement, { directive, expression }: { directive: string; expression: string }): Comment {
    const attributes = this.createNamedNodeMap()
    attributes.setNamedItem(this.createAttribute(directive, expression))
    const comment = Object.assign(this.document.createComment(`[${directive}="${expression}"]`), { attributes })
    this.cache("*").set(comment, element)
    this.#comments.set(element, comment)
    element.parentNode?.replaceChild(comment, element)
    return comment
  }

  /**
   * Replace {@linkcode https://developer.mozilla.org/en-US/docs/Web/API/Comment | Comment} by restoring its original {@linkcode https://developer.mozilla.org/docs/Web/API/HTMLElement | HTMLElement}.
   *
   * Calling this method on a `Comment`that was not created by {@linkcode Renderer.comment()} will throw a {@linkcode https://developer.mozilla.org/docs/Web/API/ReferenceError | ReferenceError}.
   *
   * ```ts
   * import { Window } from "@mizu/internal/vdom"
   * const renderer = await new Renderer(new Window()).ready
   * const parent = renderer.createElement("div")
   * const element = parent.appendChild(renderer.createElement("div"))
   * const comment = renderer.comment(element, { directive: "foo", expression: "bar" })
   *
   * renderer.uncomment(comment)
   * console.assert(!parent.contains(comment))
   * console.assert(parent.contains(element))
   * console.assert(!renderer.cache("*").has(comment))
   * ```
   */
  uncomment(comment: Comment): HTMLElement {
    if (!this.cache("*").has(comment)) {
      throw new ReferenceError(`Tried to uncomment an element that is not present in cache`)
    }
    const element = this.cache("*").get(comment)!
    comment.parentNode?.replaceChild(element, comment)
    this.cache("*").delete(comment)
    this.#comments.delete(element)
    return element
  }

  /**
   * Retrieve the {@linkcode https://developer.mozilla.org/docs/Web/API/Comment | Comment} associated with an {@linkcode https://developer.mozilla.org/docs/Web/API/HTMLElement | HTMLElement} replaced by {@linkcode Renderer.comment()}.
   *
   * ```ts
   * import { Window } from "@mizu/internal/vdom"
   * const renderer = await new Renderer(new Window()).ready
   *
   * const element = renderer.document.documentElement.appendChild(renderer.createElement("div"))
   * const comment = renderer.comment(element, { directive: "foo", expression: "bar" })
   * console.assert(renderer.getComment(element) === comment)
   * ```
   */
  getComment(element: HTMLElement): Nullable<Comment> {
    return this.#comments.get(element) ?? null
  }

  /**
   * Create a new {@linkcode https://developer.mozilla.org/docs/Web/API/NamedNodeMap | NamedNodeMap}.
   *
   * This bypasses the illegal constructor check.
   *
   * ```ts
   * import { Window } from "@mizu/internal/vdom"
   * const renderer = await new Renderer(new Window()).ready
   *
   * const nodemap = renderer.createNamedNodeMap()
   * console.assert(nodemap.constructor.name.includes("NamedNodeMap"))
   * ```
   */
  createNamedNodeMap(): NamedNodeMap {
    const div = this.createElement("div")
    return div.attributes
  }

  /**
   * Internal {@linkcode https://developer.mozilla.org/docs/Web/API/Attr | Attr} cache.
   *
   * It is used to store `Attr` instances so they can be duplicated with {@linkcode https://developer.mozilla.org/docs/Web/API/Node/cloneNode | Attr.cloneNode()} without having to create a new one each time.
   */
  readonly #attributes = {} as Record<PropertyKey, Attr>

  /**
   * Create a new {@linkcode https://developer.mozilla.org/docs/Web/API/Attr | Attr}.
   *
   * This bypasses the attribute name validation check.
   *
   * ```ts
   * import { Window } from "@mizu/internal/vdom"
   * const renderer = await new Renderer(new Window()).ready
   *
   * const attribute = renderer.createAttribute("*foo", "bar")
   * console.assert(attribute.name === "*foo")
   * console.assert(attribute.value === "bar")
   * ```
   */
  createAttribute(name: string, value = ""): Attr {
    let attribute = this.#attributes[name]?.cloneNode() as Attr
    try {
      attribute ??= this.document.createAttribute(name)
    } catch {
      this.#attributes[name] ??= (this.createElement("div", { innerHTML: `<div ${name}=""></div>` }).firstChild! as HTMLElement).attributes[0]
      attribute = this.#attributes[name].cloneNode() as Attr
    }
    attribute.value = value
    return attribute
  }

  /**
   * Set an {@linkcode https://developer.mozilla.org/docs/Web/API/Attr | Attr} on a {@linkcode https://developer.mozilla.org/docs/Web/API/HTMLElement | HTMLElement}
   * or updates a {@linkcode https://developer.mozilla.org/docs/Web/API/Node/nodeValue | Comment.nodeValue}.
   *
   * ```ts
   * import { Window } from "@mizu/internal/vdom"
   * const renderer = await new Renderer(new Window()).ready
   *
   * const element = renderer.createElement("div")
   * renderer.setAttribute(element, "*foo", "bar")
   * console.assert(element.getAttribute("*foo") === "bar")
   *
   * const comment = renderer.comment(element, { directive: "foo", expression: "bar" })
   * renderer.setAttribute(comment, "*foo", "bar")
   * console.assert(comment.nodeValue?.includes(`[*foo="bar"]`))
   * ```
   */
  setAttribute(element: HTMLElement | Comment, name: string, value = "") {
    switch (element.nodeType) {
      case this.window.Node.COMMENT_NODE: {
        element = element as Comment
        const tag = `[${name}="${value.replaceAll('"', "&quot;")}"]`
        if (!element.nodeValue!.includes(tag)) {
          element.nodeValue += ` ${tag}`
        }
        break
      }
      case this.window.Node.ELEMENT_NODE: {
        element = element as HTMLElement
        const attribute = Array.from(element.attributes).find((attribute) => attribute.name === name)
        if (!attribute) {
          element.attributes.setNamedItem(this.createAttribute(name, value))
          break
        }
        attribute.value = value
        break
      }
    }
  }

  /** A collection of {@linkcode https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp | RegExp} used by {@linkcode Renderer.getAttributes()} and {@linkcode Renderer.parseAttribute()}. */
  readonly #extractor = {
    attribute: /^(?:(?:(?<a>\S*?)\{(?<b>\S+?)\})|(?<name>[^{}]\S*?))(?:\[(?<tag>\S+?)\])?(?:\.(?<modifiers>\S+))?$/,
    modifier: /^(?<key>\S*?)(?:\[(?<value>\S*)\])?$/,
    boolean: /^(?<truthy>yes|on|true)|(?<falsy>no|off|false)$/,
    duration: /^(?<delay>(?:\d+)|(?:\d*\.\d+))(?<unit>(?:ms|s|m)?)$/,
  } as const

  /**
   * Retrieve all matching {@linkcode https://developer.mozilla.org/docs/Web/API/Attr | Attr} from an {@linkcode https://developer.mozilla.org/docs/Web/API/HTMLElement | HTMLElement}.
   *
   * It is designed to handle attributes that follows the syntax described in {@linkcode Renderer.parseAttribute()}.
   *
   * ```ts
   * import { Window } from "@mizu/internal/vdom"
   * const renderer = await new Renderer(new Window()).ready
   *
   * const element = renderer.createElement("div", { attributes: { "*foo.modifier[value]": "bar" } })
   * console.assert(renderer.getAttributes(element, "*foo").length === 1)
   * ```
   */
  getAttributes(element: Optional<HTMLElement | Comment>, names: Arrayable<string> | RegExp, options?: { first: false }): Attr[]
  /**
   * Retrieve the first matching {@linkcode https://developer.mozilla.org/docs/Web/API/Attr | Attr} from an {@linkcode https://developer.mozilla.org/docs/Web/API/HTMLElement | HTMLElement}.
   *
   * It is designed to handle attributes that follows the syntax described in {@linkcode Renderer.parseAttribute()}.
   * If no matching `Attr` is found, `null` is returned.
   *
   * ```ts
   * import { Window } from "@mizu/internal/vdom"
   * const renderer = await new Renderer(new Window()).ready
   *
   * const element = renderer.createElement("div", { attributes: { "*foo.modifier[value]": "bar" } })
   * console.assert(renderer.getAttributes(element, "*foo", { first: true })?.value === "bar")
   * console.assert(renderer.getAttributes(element, "*bar", { first: true }) === null)
   * ```
   */
  getAttributes(element: Optional<HTMLElement | Comment>, names: Arrayable<string> | RegExp, options: { first: true }): Nullable<Attr>
  getAttributes(element: Optional<HTMLElement | Comment>, names: Arrayable<string> | RegExp, { first = false } = {}): Attr[] | Nullable<Attr> {
    const attributes = []
    if (element && (this.isHtmlElement(element))) {
      if (!(names instanceof RegExp)) {
        names = [names].flat()
      }
      for (const attribute of Array.from(element.attributes)) {
        const { a: _a, b: _b, name: name = `${_a}${_b}` } = attribute.name.match(this.#extractor.attribute)!.groups!
        if (((names as string[]).includes?.(name)) || ((names as RegExp).test?.(name))) {
          attributes.push(attribute)
          if (first) {
            break
          }
        }
      }
    }
    return first ? attributes[0] ?? null : attributes
  }

  /**
   * Parse an {@linkcode https://developer.mozilla.org/docs/Web/API/Attr | Attr} from an {@linkcode https://developer.mozilla.org/docs/Web/API/HTMLElement | HTMLElement}.
   *
   * Prefixes can automatically be stripped from the attribute name by setting the `prefix` option.
   * It is especially useful when `Attr` were extracted with a `RegExp` matching.
   *
   * The `typings` descriptor is passed to enforce types and validate values on {@linkcode https://developer.mozilla.org/en-US/docs/Web/API/Attr/value | Attr.value} and modifiers.
   * The following {@linkcode AttrTypings} are supported:
   * - {@linkcode AttrBoolean}, matching `BOOLEAN` token described below.
   *   - {@linkcode AttrBoolean.default} is `true`
   * - {@linkcode AttrNumber}, matching `NUMBER` token described below.
   *   - {@linkcode AttrNumber.default} is `0`.
   *   - {@linkcode AttrNumber.integer} will round the value to the nearest integer.
   *   - {@linkcode AttrNumber.min} will clamp the value to a minimum value.
   *   - {@linkcode AttrNumber.max} will clamp the value to a maximum value.
   * - {@linkcode AttrDuration}, matching `DURATION` described below.
   *   - {@linkcode AttrDuration.default} is `0`.
   *   - Value is normalized to milliseconds.
   *   - Value is clamped to a minimum of 0, and is rounded to the nearest integer.
   * - {@linkcode AttrString} (the default), matching `STRING` token described below.
   *   - {@linkcode AttrString.default} is `""`, or first {@linkcode AttrString.allowed} value if set.
   *   - {@linkcode AttrString.allowed} will restrict the value to a specific set of strings, in a similar fashion to an enum.
   *
   * > [!IMPORTANT]
   * > A {@linkcode AttrAny.default} is only applied when a key has been explicitly defined but its value was not.
   * > Use {@linkcode AttrAny.enforce} to force the default value to be applied event if the key was not defined.
   * >
   * > ```ts ignore
   * > import { Window } from "@mizu/internal/vdom"
   * > const renderer = await new Renderer(new Window()).ready
   * > const modifier = (attribute: Attr, typing: AttrBoolean) => renderer.parseAttribute(attribute, { modifiers: { value: typing } }, { modifiers: true }).modifiers
   * > const [a, b] = Array.from(renderer.createElement("div", { attributes: { "*a.value": "", "*b": "" } }).attributes)
   * >
   * > // `a.value === true` because it was defined, and the default for `AttrBoolean` is `true`
   * > console.assert(modifier(a, { type: Boolean }).value === true)
   * > // `a.value === false` because it was defined, and the default was set to `false`
   * > console.assert(modifier(a, { type: Boolean, default: false }).value === false)
   * >
   * > // `b.value === undefined` because it was not explicitly defined, despite the default for `AttrBoolean` being `true`
   * > console.assert(modifier(b, { type: Boolean }).value === undefined)
   * > // `b.value === true` because it was not explicitly defined, but the default was enforced
   * > console.assert(modifier(b, { type: Boolean, enforce: true }).value === true)
   * > // `b.value === false` because it was not explicitly defined, and the default was set to `false` and was enforced
   * > console.assert(modifier(b, { type: Boolean, default: false, enforce: true }).value === false)
   * > ```
   * >
   *
   * > [!NOTE]
   * > Modifiers parsing must be explicitly enabled with `modifiers: true`.
   * > This is to prevent unnecessary parsing when modifiers are not needed.
   *
   * Supported syntax is described below.
   * Please note that this syntax is still ruled by {@link https://html.spec.whatwg.org/#attributes-2 | HTML standards} and might not be fully accurate or subject to limitations.
   * It is advised to refrain from using especially complex {@linkcode https://developer.mozilla.org/en-US/docs/Web/API/Attr/name | Attr.name} that contain specials characters or structures that may prove both confusing and challenging to parse.
   *
   * > ```
   * > ┌────────┬──────┬─────┬───────────┬─────┬───────┐
   * > │ PREFIX │ NAME │ TAG │ MODIFIERS │ '=' │ VALUE │
   * > └────────┴──────┴─────┴───────────┴─────┴───────┘
   * >
   * > PREFIX
   * >   └─[   PREFIX]── STRING
   * >
   * > NAME
   * >   ├─[  ESCAPED]── STRING '{' STRING '}'
   * >   └─[ UNDOTTED]── [^.]
   * >
   * > TAG
   * >   ├─[      TAG]── '[' STRING ']'
   * >   └─[     NONE]── ∅
   * >
   * > MODIFIERS
   * >   ├─[ MODIFIER]── '.' MODIFIER MODIFIERS
   * >   └─[     NONE]── ∅
   * >
   * > MODIFIER
   * >   ├─[      KEY]── STRING
   * >   └─[KEY+VALUE]── STRING '[' MODIFIER_VALUE ']'
   * >
   * > MODIFIER_VALUE
   * >   └─[    VALUE]── BOOLEAN | NUMBER | DURATION | STRING
   * >
   * > BOOLEAN
   * >  ├─[      TRUE]── 'yes' | 'on'  | 'true'
   * >  └─[     FALSE]── 'no'  | 'off' | 'false'
   * >
   * > NUMBER
   * >  ├─[EXPL_NEG_N]── '-' POSITIVE_NUMBER
   * >  ├─[EXPL_POS_N]── '+' POSITIVE_NUMBER
   * >  └─[IMPL_POS_N]── POSITIVE_NUMBER
   * >
   * > POSITIVE_NUMBER
   * >  ├─[   INTEGER]── [0-9]
   * >  ├─[EXPL_FLOAT]── [0-9] '.' [0-9]
   * >  └─[IMPL_FLOAT]── '.' [0-9]
   * >
   * > DURATION
   * >  └─[  DURATION]── POSITIVE_NUMBER DURATION_UNIT
   * >
   * > DURATION_UNIT
   * >  ├─[   MINUTES]── 'm'
   * >  ├─[   SECONDS]── 's'
   * >  ├─[EXPL_MILLI]── 'ms'
   * >  └─[IMPL_MILLI]── ∅
   * >
   * > STRING
   * >   └─[      ANY]── [*]
   * > ```
   *
   * ```ts
   * import { Window } from "@mizu/internal/vdom"
   * const renderer = await new Renderer(new Window()).ready
   * const element = renderer.createElement("div", { attributes: { "*foo.bar[baz]": "true" } })
   * const [attribute] = Array.from(element.attributes)
   *
   * let parsed
   * const typings = { type: Boolean, modifiers: { bar: { type: String } } }
   *
   * parsed = renderer.parseAttribute(attribute, typings, { modifiers: true })
   * console.assert(parsed.name === "*foo")
   * console.assert(parsed.value === true)
   * console.assert(parsed.modifiers.bar === "baz")
   *
   * parsed = renderer.parseAttribute(attribute, typings, { modifiers: false, prefix: "*" })
   * console.assert(parsed.name === "foo")
   * console.assert(parsed.value === true)
   * console.assert(!("modifiers" in parsed))
   * ```
   *
   * ```ts
   * const typedef = {
   *   // "yes", "on", "true", "no", "off", "false"
   *   boolean: { type: Boolean, default: false },
   *   // "0", "3.1415", ".42", "69", etc.
   *   number: { type: Number, default: 0, min: -Infinity, max: Infinity, integer: false },
   *   // "10", "10ms", "10s", "10m", etc.
   *   duration: { type: Date, default: "0ms" },
   *   // "foo", "bar", "foobar", etc.
   *   string: { type: String, default: "" },
   *   // "foo", "bar"
   *   enum: { type: String, get default() { return this.allowed[0] } , allowed: ["foo", "bar"] },
   * }
   * ```
   */
  parseAttribute<T extends AttrTypings>(attribute: Attr, typings?: Nullable<T>, options?: Omit<RendererParseAttributeOptions, "modifiers"> & { modifiers: true }): InferAttrTypings<T>
  /**
   * Same as {@linkcode Renderer.parseAttribute()} but without modifiers.
   */
  parseAttribute<T extends AttrTypings>(attribute: Attr, typings?: Nullable<T>, options?: Omit<RendererParseAttributeOptions, "modifiers"> & { modifiers?: false }): Omit<InferAttrTypings<T>, "modifiers">
  parseAttribute<T extends AttrTypings>(attribute: Attr, typings?: Nullable<T>, { modifiers = false, prefix = "" } = {} as RendererParseAttributeOptions) {
    // Parse attribute name
    if (!this.#parsed.has(attribute)) {
      const { a: _a, b: _b, name = `${_a}${_b}`, tag = "", modifiers: _modifiers = "" } = attribute.name.match(this.#extractor.attribute)!.groups!
      const cached = { name, tag, modifiers: {} as Record<PropertyKey, unknown> }
      if (modifiers && (typings?.modifiers)) {
        const modifiers = Object.fromEntries(
          _modifiers.split(".").map((modifier) => {
            const { key, value } = modifier.match(this.#extractor.modifier)!.groups!
            if (key in typings.modifiers!) {
              return [key, value ?? ""]
            }
            return null
          }).filter((modifier): modifier is [string, string] => modifier !== null),
        )
        for (const key in typings.modifiers) {
          cached.modifiers[key] = this.#parseAttributeValue(attribute.parentElement, name, key, modifiers[key], typings.modifiers[key])
        }
      }
      this.#parsed.set(attribute, cached)
    }
    const parsed = structuredClone(this.#parsed.get(attribute)) as InferAttrTypings<T>
    // Update values that might have changed since the last parsing or options
    parsed.value = this.#parseAttributeValue(attribute.parentElement, parsed.name, "value", attribute.value, typings as AttrAny) as typeof parsed.value
    parsed.attribute = attribute
    if (prefix && (parsed.name.startsWith(prefix))) {
      parsed.name = parsed.name.slice(prefix.length)
    }
    if (!modifiers) {
      delete (parsed as Record<PropertyKey, unknown>).modifiers
    }
    return parsed
  }

  /** Internal cache used to store parsed already parsed {@linkcode https://developer.mozilla.org/en-US/docs/Web/API/Attr/name | Attr.name}. */
  // deno-lint-ignore ban-types
  readonly #parsed = new WeakMap<Attr, Pick<InferAttrTypings<{}>, "name" | "tag" | "modifiers">>()

  /** Used by {@linkcode Renderer.parseAttribute()} to parse a single {@linkcode https://developer.mozilla.org/en-US/docs/Web/API/Attr/value | Attr.value} according to specified {@linkcode AttrAny} typing. */
  #parseAttributeValue<T extends AttrAny>(element: Nullable<HTMLElement>, name: string, key: string, value: Optional<string>, typings: T): Optional<boolean | number | string> {
    if ((value === undefined) && (!typings?.enforce)) {
      return undefined
    }
    let fallback
    switch (typings?.type) {
      case Boolean: {
        const typing = typings as AttrBoolean
        fallback = typing?.default ?? true
        const { truthy, falsy } = `${value || fallback}`.match(this.#extractor.boolean)?.groups ?? {}
        if ((!truthy) && (!falsy)) {
          break
        }
        return Boolean(truthy)
      }
      case Number: {
        const typing = typings as AttrNumber
        fallback = typing?.default ?? 0
        let number = Number.parseFloat(`${value || fallback}`)
        if (typing?.integer) {
          number = Math.round(number)
        }
        if (typeof typing?.min === "number") {
          number = Math.max(typing.min, number)
        }
        if (typeof typing?.max === "number") {
          number = Math.min(typing.max, number)
        }
        if (!Number.isFinite(number)) {
          break
        }
        return number
      }
      case Date: {
        const typing = typings as AttrDuration
        fallback = typing?.default ?? 0
        const { delay, unit } = `${value || fallback}`.match(this.#extractor.duration)?.groups ?? {}
        const duration = Math.round(Number.parseFloat(delay) * ({ ms: 1, s: 1000, m: 60000 }[unit || "ms"] ?? NaN))
        if ((!Number.isFinite(duration)) || (duration < 0)) {
          break
        }
        return duration
      }
      default: {
        const typing = typings as AttrString
        fallback = typing?.default ?? ""
        const string = `${value || fallback}`
        if ((typing?.allowed?.length) && (!typing.allowed.includes(string))) {
          fallback = typing.allowed.includes(fallback) ? fallback : typing.allowed[0]
          break
        }
        return string
      }
    }
    this.warn(`Invalid value "${value}" for "${name}.${key}", fallbacking to to "${fallback}"`, element)
    return fallback
  }

  /**
   * Type guard for {@linkcode https://developer.mozilla.org/docs/Web/API/HTMLElement | HTMLElement}.
   *
   * ```ts
   * import { Window } from "@mizu/internal/vdom"
   * const renderer = await new Renderer(new Window()).ready
   * const element = renderer.createElement("div")
   * const comment = renderer.comment(element, { directive: "foo", expression: "bar" })
   *
   * console.assert(renderer.isHtmlElement(element))
   * console.assert(!renderer.isHtmlElement(comment))
   * ```
   */
  isHtmlElement(element: HTMLElement | Comment): element is HTMLElement {
    return element.nodeType === this.window.Node.ELEMENT_NODE
  }

  /**
   * Type guard for {@linkcode https://developer.mozilla.org/docs/Web/API/Comment | Comment}.
   *
   * ```ts
   * import { Window } from "@mizu/internal/vdom"
   * const renderer = await new Renderer(new Window()).ready
   * const element = renderer.createElement("div")
   * const comment = renderer.comment(element, { directive: "foo", expression: "bar" })
   *
   * console.assert(!renderer.isComment(element))
   * console.assert(renderer.isComment(comment))
   * ```
   */
  isComment(element: HTMLElement | Comment): element is Comment {
    return element.nodeType === this.window.Node.COMMENT_NODE
  }

  /** Warnings callback. */
  readonly #warn

  /**
   * Generate a warning message.
   *
   * If no warnings callback was provided, the warning message is applied with {@linkcode Renderer.setAttribute()} with the name `*warn`
   * to the `target` {@linkcode https://developer.mozilla.org/docs/Web/API/HTMLElement | HTMLElement} or {@linkcode https://developer.mozilla.org/docs/Web/API/Comment | Comment} if there is one.
   *
   * ```ts
   * import { Window } from "@mizu/internal/vdom"
   * const renderer = await new Renderer(new Window()).ready
   *
   * const element = renderer.createElement("div")
   * renderer.warn("foo", element)
   * console.assert(element.getAttribute("*warn") === "foo")
   *
   * const comment = renderer.comment(element, { directive: "foo", expression: "bar" })
   * renderer.warn("foo", comment)
   * console.assert(comment.nodeValue?.includes(`[*warn="foo"]`))
   * ```
   */
  warn(message: string, target?: Nullable<HTMLElement | Comment>): void {
    if (this.#warn) {
      this.#warn(message, target)
      return
    }
    if (target && ((this.isHtmlElement(target)) || (this.isComment(target)))) {
      return this.setAttribute(target, "*warn", message)
    }
  }

  /** Debug callback. */
  readonly #debug

  /**
   * Generate a debug message.
   *
   * ```ts
   * import { Window } from "@mizu/internal/vdom"
   * const renderer = await new Renderer(new Window()).ready
   *
   * const element = renderer.createElement("div")
   * renderer.debug("foo", element)
   * ```
   */
  debug(message: string, target?: Nullable<HTMLElement | Comment>): void {
    this.#debug?.(message, target)
  }
}

/** {@linkcode Renderer} options. */
export type RendererOptions = {
  /** Initial {@linkcode Directive}s. */
  directives?: Arg<Renderer["load"]>
  /** Warnings callback. */
  warn?: (message: string, target?: Nullable<HTMLElement | Comment>) => unknown
  /** Debug callback. */
  debug?: (message: string, target?: Nullable<HTMLElement | Comment>) => unknown
}

/** {@linkcode Renderer.evaluate()} options. */
export type RendererEvaluateOptions = {
  /** {@linkcode Context} to use. */
  context?: Context
  /** {@linkcode State} to use. */
  state?: State
  /** It the evaluated expression is {@link https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Function/call | callable}, it will be called with these arguments and its result is returned instead. */
  args?: unknown[]
}

/** {@linkcode Renderer.render()} options. */
export type RendererRenderOptions = {
  /** {@linkcode Context} to use. */
  context?: Context
  /** {@linkcode State} to use. */
  state?: State
  /** Whether to render subtrees that do not possess the explicit rendering attribute. */
  implicit?: boolean
  /** Whether to enable reactivity. */
  reactive?: boolean
  /** {@link https://developer.mozilla.org/docs/Web/API/Document/querySelector | CSS selector} to query select the return. */
  select?: string
  /** Whether to return the result as an HTML string. */
  stringify?: boolean
  /** Whether to throw on errors. */
  throw?: boolean
}

/** {@linkcode Renderer.parseAttribute()} options. */
export type RendererParseAttributeOptions = {
  /** Whether to parse modifiers. */
  modifiers?: boolean
  /** Attribute name prefix to strip. */
  prefix?: string
}

/** {@linkcode Renderer.render()} initial {@linkcode Context} and {@linkcode State}. */
export type InitialContextState = Readonly<{
  /** Initial {@linkcode Context}. */
  context: Context
  /** Initial {@linkcode State}. */
  state: DeepReadonly<State>
}>

/** Current {@linkcode Renderer.render()} state. */
export type State = Record<`$${string}` | `${typeof Renderer.internal}_${string}`, unknown>

/** Boolean type definition. */
export type AttrBoolean = {
  /** Type. */
  type: typeof Boolean
  /** Default value. */
  default?: boolean
  /** Enforce value. */
  enforce?: true
}

/** Duration type definition. */
export type AttrDuration = {
  /** Type. */
  type: typeof Date
  /** Default value. */
  default?: number | string
  /** Enforce value. */
  enforce?: true
}

/** Number type definition. */
export type AttrNumber = {
  /** Type. */
  type: typeof Number
  /** Default value. */
  default?: number
  /** Round to nearest integer. */
  integer?: boolean
  /** Minimum value. */
  min?: number
  /** Maximum value. */
  max?: number
  /** Enforce value. */
  enforce?: true
}

/** String type definition. */
export type AttrString = {
  /** Type. */
  type?: typeof String
  /** Default value. */
  default?: string
  /** Allowed values. */
  allowed?: string[]
  /** Enforce value. */
  enforce?: true
}

/** Generic type definition. */
export type AttrAny = {
  /** Type. */
  type?: typeof Boolean | typeof Number | typeof Date | typeof String
  /** Default value. */
  default?: unknown
  /** Enforce value. */
  enforce?: true
}

/** Infer value from {@linkcode AttrAny} type definition. */
export type InferAttrAny<T> = T extends AttrBoolean ? boolean : T extends (AttrDuration | AttrNumber) ? number : string

/** Type definition for {@linkcode https://developer.mozilla.org/docs/Web/API/Attr | Attr} compatible with {@linkcode Renderer.parseAttribute()}. */
export type AttrTypings = Omit<AttrAny & { modifiers?: Record<PropertyKey, AttrAny> }, "enforce">

/** Infer value from {@linkcode AttrTypings} type definition. */
export type InferAttrTypings<T extends AttrTypings> = {
  /** Parsed {@linkcode https://developer.mozilla.org/docs/Web/API/Attr | Attr} reference. */
  attribute: Attr
  /** Parsed {@linkcode https://developer.mozilla.org/docs/Web/API/Attr/name | Attr.name}. */
  name: string
  /** Parsed {@linkcode https://developer.mozilla.org/docs/Web/API/Attr/value | Attr.value}. */
  value: InferAttrAny<T>
  /** Parsed {@linkcode https://developer.mozilla.org/docs/Web/API/Attr | Attr} tag. */
  tag: string
  /** Parsed {@linkcode https://developer.mozilla.org/docs/Web/API/Attr | Attr} modifiers. */
  modifiers: { [P in keyof T["modifiers"]]: T["modifiers"][P] extends { enforce: true } ? InferAttrAny<T["modifiers"][P]> : Optional<InferAttrAny<T["modifiers"][P]>> }
}

/** Additional typings for {@linkcode https://developer.mozilla.org/en-US/docs/Web/API/Window | Window} when using a {@link https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model | virtual DOM implementation}. */
export type VirtualWindow = Window & {
  Node: typeof Node
  HTMLElement: typeof HTMLElement
  Event: typeof Event
  NodeFilter: typeof NodeFilter
  KeyboardEvent: typeof KeyboardEvent
  MouseEvent: typeof MouseEvent
  [Symbol.asyncDispose]: () => Promise<void>
}