mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-27 15:44:42 -05:00
Compare commits
660 Commits
2182
...
0.8.0-beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2faae43d5f | ||
|
|
6760c87e83 | ||
|
|
e19e42c650 | ||
|
|
fb4be49ebf | ||
|
|
0b4cbbc17d | ||
|
|
dbbeb7c6ef | ||
|
|
36aef2565d | ||
|
|
a2a7eb8a2a | ||
|
|
97e22e2506 | ||
|
|
8bedacb0c7 | ||
|
|
56b7b9a16a | ||
|
|
d04d4c77f9 | ||
|
|
5c75928b5b | ||
|
|
abc5631654 | ||
|
|
40e5288ac1 | ||
|
|
335934d40e | ||
|
|
6ee72f42e2 | ||
|
|
93af23a970 | ||
|
|
95e8ae84af | ||
|
|
5cfe7f6b5e | ||
|
|
0404efd5c3 | ||
|
|
93173c1400 | ||
|
|
cd2904f6a6 | ||
|
|
6b453845f9 | ||
|
|
111b84ce3b | ||
|
|
5633148047 | ||
|
|
4edb012de3 | ||
|
|
b2bea2e6b7 | ||
|
|
acbd6378a8 | ||
|
|
b7462aab10 | ||
|
|
7593540774 | ||
|
|
ed915f8e06 | ||
|
|
f65d87d566 | ||
|
|
5034539411 | ||
|
|
bc48aa4228 | ||
|
|
d2c81fe955 | ||
|
|
28eb96831a | ||
|
|
330920eae2 | ||
|
|
a94bc0a6da | ||
|
|
f85e01f4d6 | ||
|
|
599c87c88a | ||
|
|
3ca98279e1 | ||
|
|
a730bffe13 | ||
|
|
d0bf843821 | ||
|
|
7250bc312e | ||
|
|
0e8242f94c | ||
|
|
439b41f0e8 | ||
|
|
18570e970c | ||
|
|
787bf385d3 | ||
|
|
b6d2808671 | ||
|
|
2e4d94b6c6 | ||
|
|
66f9c8c999 | ||
|
|
352080d91a | ||
|
|
1e579614a5 | ||
|
|
a4e47d4086 | ||
|
|
3164721fdb | ||
|
|
fee4bccb32 | ||
|
|
4ba9f67440 | ||
|
|
2242ad1847 | ||
|
|
131b18bddb | ||
|
|
5e9d6e2dfd | ||
|
|
d76e5bb4ea | ||
|
|
f752e32ae3 | ||
|
|
a9197102a6 | ||
|
|
58f1bf95e1 | ||
|
|
d84ab6d9bf | ||
|
|
f069d4478e | ||
|
|
65b5d55d62 | ||
|
|
860ad7a221 | ||
|
|
901e038aa0 | ||
|
|
f49f0965bc | ||
|
|
bb62d08d3f | ||
|
|
bfe04593fd | ||
|
|
38a3aae28e | ||
|
|
5eaaff045f | ||
|
|
5b484eaec4 | ||
|
|
e9384e3286 | ||
|
|
5149ad54db | ||
|
|
4d9ec54ad1 | ||
|
|
a1cd7ae9a1 | ||
|
|
083f9c663f | ||
|
|
63c9549120 | ||
|
|
6232f6482a | ||
|
|
825e89f25c | ||
|
|
3dbb251853 | ||
|
|
98e00fcb3b | ||
|
|
5da4c438d9 | ||
|
|
80ed74c075 | ||
|
|
cdee2a9476 | ||
|
|
c97ab9a72c | ||
|
|
4fc8972f2b | ||
|
|
79e9340a9b | ||
|
|
41d01cedb2 | ||
|
|
b800c009c7 | ||
|
|
374a020d84 | ||
|
|
83fcf8663c | ||
|
|
e7a73595de | ||
|
|
a9a988e0e1 | ||
|
|
db10d961df | ||
|
|
fb608158cb | ||
|
|
1a472ebad1 | ||
|
|
2d1b66a5c6 | ||
|
|
c524b0aefc | ||
|
|
e4c977911c | ||
|
|
f488d4b5b7 | ||
|
|
d4cfd0e2cb | ||
|
|
a4b0d3408c | ||
|
|
2037bf12cb | ||
|
|
2747a496fc | ||
|
|
c286812116 | ||
|
|
1e0a9ef189 | ||
|
|
1363b941bc | ||
|
|
7479010f84 | ||
|
|
f7a1a2cab2 | ||
|
|
92b82688a6 | ||
|
|
9b9983af79 | ||
|
|
04e79a0dc4 | ||
|
|
f64951126e | ||
|
|
0a29071779 | ||
|
|
efcb6f6d21 | ||
|
|
42988b1bc1 | ||
|
|
6904eec207 | ||
|
|
7eb8ca702d | ||
|
|
c75397ea75 | ||
|
|
848fd724dd | ||
|
|
5bc254d49f | ||
|
|
885f4a1654 | ||
|
|
ddd243d07a | ||
|
|
362c300eac | ||
|
|
284a724e5f | ||
|
|
1a7b40b507 | ||
|
|
73dd677843 | ||
|
|
131f414fdc | ||
|
|
c3ed874d4d | ||
|
|
f003e50446 | ||
|
|
ea29685c92 | ||
|
|
6ecc681cdd | ||
|
|
49e44a2ec2 | ||
|
|
7c34b4a4a5 | ||
|
|
7157958822 | ||
|
|
37cf25fba5 | ||
|
|
f975b8d33b | ||
|
|
d37450d12f | ||
|
|
4804dac32d | ||
|
|
a9f27d6128 | ||
|
|
04cb036a7d | ||
|
|
1d3784ed7b | ||
|
|
8cc1a34c00 | ||
|
|
68f4d46c5f | ||
|
|
590728e47e | ||
|
|
e84b527743 | ||
|
|
96b125d54f | ||
|
|
16d66362f8 | ||
|
|
e27801a2c2 | ||
|
|
b81f71997b | ||
|
|
2a11325749 | ||
|
|
5604f3e979 | ||
|
|
3a9a0891a3 | ||
|
|
a39add50c0 | ||
|
|
2a2675dd36 | ||
|
|
7be6a9da86 | ||
|
|
c51e07b5a4 | ||
|
|
b17a4c92c7 | ||
|
|
03f9c6cb6d | ||
|
|
6ad300c592 | ||
|
|
b4e683d969 | ||
|
|
c2289b23a7 | ||
|
|
9e8b8886da | ||
|
|
299acd25f3 | ||
|
|
6a6b3dee15 | ||
|
|
5d71913523 | ||
|
|
706617ab0a | ||
|
|
cd64bb9d67 | ||
|
|
f881c1877d | ||
|
|
8783f6a478 | ||
|
|
2add714c65 | ||
|
|
88b1b2d882 | ||
|
|
9d3a743d33 | ||
|
|
287fc47163 | ||
|
|
8f74a6d8a0 | ||
|
|
c6de7c714e | ||
|
|
597175a54b | ||
|
|
6154199850 | ||
|
|
ede25b9e3d | ||
|
|
32be3a023a | ||
|
|
d9043e4f34 | ||
|
|
8f636e354a | ||
|
|
e3010c7f1f | ||
|
|
1dcc5838f7 | ||
|
|
7da64f22c4 | ||
|
|
0073ae7d8a | ||
|
|
1ff373dbc2 | ||
|
|
c35c42c6e3 | ||
|
|
d4cbba7c63 | ||
|
|
9cc8ee3c5a | ||
|
|
f0c5ffe55f | ||
|
|
8465716a19 | ||
|
|
0e24b2e63f | ||
|
|
c64d205984 | ||
|
|
f17cb98eb0 | ||
|
|
30f3e82664 | ||
|
|
152d5a5c92 | ||
|
|
669e1ba7fa | ||
|
|
2ad6a086f9 | ||
|
|
32e58d6b66 | ||
|
|
a107443104 | ||
|
|
c859b07901 | ||
|
|
a9868bea2b | ||
|
|
7183c2b993 | ||
|
|
7a03621db1 | ||
|
|
586c330995 | ||
|
|
72f960a026 | ||
|
|
b62ae56094 | ||
|
|
9ccefbbd8c | ||
|
|
d1108826cc | ||
|
|
d6c4cd2a81 | ||
|
|
f8acdf9168 | ||
|
|
09bbae2a72 | ||
|
|
2b589fa61f | ||
|
|
ac3352724b | ||
|
|
3413825638 | ||
|
|
22b2d8ec84 | ||
|
|
1c3e013a63 | ||
|
|
35e6f17930 | ||
|
|
d1513a4a0b | ||
|
|
aa27b9e474 | ||
|
|
cfe925d58f | ||
|
|
9d4b1bc4b7 | ||
|
|
f303ff9706 | ||
|
|
9fd2a75da4 | ||
|
|
429d1b1262 | ||
|
|
035586e5d8 | ||
|
|
fdd5671fe1 | ||
|
|
293149eeb2 | ||
|
|
a9ce608433 | ||
|
|
62196ff638 | ||
|
|
db9cabb365 | ||
|
|
5293afddb7 | ||
|
|
eeb01e2d52 | ||
|
|
7d9cd43c0e | ||
|
|
723685c370 | ||
|
|
6d19e1a921 | ||
|
|
165911b2e6 | ||
|
|
575186349c | ||
|
|
10db2f83eb | ||
|
|
357f3be393 | ||
|
|
a1f5d6f79f | ||
|
|
34ecd2d541 | ||
|
|
df38b075d8 | ||
|
|
46233e3097 | ||
|
|
46f6d9ae26 | ||
|
|
8264d478e4 | ||
|
|
9c54da304e | ||
|
|
1c002a2df8 | ||
|
|
6e4dac91bf | ||
|
|
6493b0b359 | ||
|
|
d58042b7ba | ||
|
|
7a5b27ad20 | ||
|
|
78d0dbdfa2 | ||
|
|
1c30a4c131 | ||
|
|
b91429d4f0 | ||
|
|
6663fddf76 | ||
|
|
28af33d511 | ||
|
|
5a3413d3b3 | ||
|
|
702d2e247b | ||
|
|
71f698f322 | ||
|
|
8d4776bf5f | ||
|
|
65798e430f | ||
|
|
d46d1a4fab | ||
|
|
f9533ab75b | ||
|
|
d08f8822c0 | ||
|
|
7357839efb | ||
|
|
1b8ad58114 | ||
|
|
ef72f1ce96 | ||
|
|
6a5bfe9a5d | ||
|
|
1661fe2412 | ||
|
|
2324853155 | ||
|
|
28a3859365 | ||
|
|
6b50179189 | ||
|
|
0bb825f6bd | ||
|
|
a6aa111122 | ||
|
|
753cfe8e44 | ||
|
|
bc9c3add87 | ||
|
|
d0bb45dbc4 | ||
|
|
2a4b80cf22 | ||
|
|
881734b43a | ||
|
|
68fdc8d9c4 | ||
|
|
03ed805137 | ||
|
|
5d4603e988 | ||
|
|
e65969cfcd | ||
|
|
e1e90b8595 | ||
|
|
71a136c7af | ||
|
|
49366be2a5 | ||
|
|
6f5bdcc675 | ||
|
|
726a99441f | ||
|
|
775bea46d9 | ||
|
|
2aa9827a1f | ||
|
|
be3c1811fc | ||
|
|
8df4f3c71d | ||
|
|
3028a9acd0 | ||
|
|
4b167fb809 | ||
|
|
46ae8ef9b2 | ||
|
|
21f26e7a6b | ||
|
|
204648d388 | ||
|
|
a04bca55a2 | ||
|
|
1fec678174 | ||
|
|
4dbb8d1a42 | ||
|
|
fc2c52eb04 | ||
|
|
1ae35ce5b3 | ||
|
|
8edb11f324 | ||
|
|
5a01a7f2ed | ||
|
|
f252460d02 | ||
|
|
6331b488e4 | ||
|
|
fcba8b3b17 | ||
|
|
d665dd4b89 | ||
|
|
be740b38ee | ||
|
|
d2803c938c | ||
|
|
f29224415a | ||
|
|
292772c4d6 | ||
|
|
5947aa299e | ||
|
|
6098836cf7 | ||
|
|
2dfa61ff6a | ||
|
|
4f39b0b0ef | ||
|
|
980595f1f0 | ||
|
|
14eb707e82 | ||
|
|
3de0414ed5 | ||
|
|
75cae91661 | ||
|
|
8b258b0d26 | ||
|
|
84bdd6b568 | ||
|
|
809023a2ad | ||
|
|
1477ae2cfb | ||
|
|
26995f8efd | ||
|
|
3c174b26a5 | ||
|
|
0c7e77800a | ||
|
|
fee2421047 | ||
|
|
89f26f6e9b | ||
|
|
e76b22bec8 | ||
|
|
cff277b3db | ||
|
|
0258ac6df4 | ||
|
|
36132a5823 | ||
|
|
6f35f8d197 | ||
|
|
3973c4f420 | ||
|
|
7f2237b91e | ||
|
|
aa6cd08387 | ||
|
|
15fc7dd7be | ||
|
|
eac979e309 | ||
|
|
f7ac7be32b | ||
|
|
2462a1dc92 | ||
|
|
2c6d790cdb | ||
|
|
1c26261fd7 | ||
|
|
c2b239dba2 | ||
|
|
d4044cd5a1 | ||
|
|
43912f4fd0 | ||
|
|
a5293f0b79 | ||
|
|
998eefb8c5 | ||
|
|
63f3508818 | ||
|
|
24308bb0eb | ||
|
|
f804a69f2f | ||
|
|
b596af45f0 | ||
|
|
5206755124 | ||
|
|
53a55a1ef2 | ||
|
|
0f74332d38 | ||
|
|
bb0dff6af5 | ||
|
|
d204ac6d5e | ||
|
|
b0ad85e624 | ||
|
|
0eebe9e289 | ||
|
|
2abbdb6594 | ||
|
|
8f8f3e23e4 | ||
|
|
aab952357e | ||
|
|
f1ebf77fa6 | ||
|
|
5cc2f3858d | ||
|
|
8252655959 | ||
|
|
14e47e87ba | ||
|
|
4a8cfad7c5 | ||
|
|
d9f52dad76 | ||
|
|
3a8508df6c | ||
|
|
865c6df483 | ||
|
|
c1d7f0f8d1 | ||
|
|
8c2dd73b70 | ||
|
|
d5894555cc | ||
|
|
2ef1723607 | ||
|
|
13f7387d45 | ||
|
|
0a13f7c08c | ||
|
|
7c83904aea | ||
|
|
6e13ff9787 | ||
|
|
234d138f03 | ||
|
|
97110cd5ac | ||
|
|
5acc1b1a5a | ||
|
|
f3987246cb | ||
|
|
e5149fb348 | ||
|
|
d67ff03568 | ||
|
|
1dbca3005d | ||
|
|
af61be0c72 | ||
|
|
76facf9539 | ||
|
|
0e73d18d7b | ||
|
|
d306a15f86 | ||
|
|
bf95648dc9 | ||
|
|
00edfc0e0a | ||
|
|
396327b667 | ||
|
|
a437289f81 | ||
|
|
58e7897db6 | ||
|
|
1be1f41fba | ||
|
|
7b8cd90a6e | ||
|
|
d0ef7b904d | ||
|
|
7904e0c395 | ||
|
|
7b4c470155 | ||
|
|
98eccc9eb8 | ||
|
|
70d06e2716 | ||
|
|
67c3bf2478 | ||
|
|
f3aaae857a | ||
|
|
d727e53dd6 | ||
|
|
e4543ab5df | ||
|
|
1ca0f4430c | ||
|
|
b59fa11853 | ||
|
|
e55f08e017 | ||
|
|
fa1939e5b2 | ||
|
|
8b2f0eaf44 | ||
|
|
b118d69281 | ||
|
|
ee66f6c395 | ||
|
|
eba08ad592 | ||
|
|
4833b4e287 | ||
|
|
9d1be64e4d | ||
|
|
d6e6cd3be0 | ||
|
|
70476f9277 | ||
|
|
d8ddfc26e9 | ||
|
|
c8acc3e8bd | ||
|
|
547442243b | ||
|
|
6e58266f54 | ||
|
|
f0cd0fb41d | ||
|
|
7585faf57e | ||
|
|
da7f6a34e8 | ||
|
|
4f7fa41262 | ||
|
|
4becfa39ca | ||
|
|
f8388b122d | ||
|
|
f57a57b92b | ||
|
|
f0bcbd9cfe | ||
|
|
115477ef1d | ||
|
|
832b9cb321 | ||
|
|
b0150ceeec | ||
|
|
af8df34360 | ||
|
|
b2e6185b22 | ||
|
|
d2bfb3080b | ||
|
|
72ebd17042 | ||
|
|
e2f0b4deeb | ||
|
|
57c07e9aec | ||
|
|
0835066bc0 | ||
|
|
656e83fe24 | ||
|
|
ad0252ecfd | ||
|
|
77f05c6f4e | ||
|
|
a4ea491dc0 | ||
|
|
3c89b9c930 | ||
|
|
93d7ba0d5f | ||
|
|
e188993800 | ||
|
|
c1dc8c7629 | ||
|
|
ab9de1b8c0 | ||
|
|
b39985d9b8 | ||
|
|
5e8e93001d | ||
|
|
a4ed0cbe5b | ||
|
|
422fe9f43b | ||
|
|
36df36e16c | ||
|
|
95fc79034b | ||
|
|
7403e4084f | ||
|
|
8feee5e5d7 | ||
|
|
e6da266b4f | ||
|
|
0798e0812d | ||
|
|
03514e68be | ||
|
|
4092368581 | ||
|
|
dcc7865989 | ||
|
|
896f7de8e1 | ||
|
|
29b2d3024a | ||
|
|
c47893ad60 | ||
|
|
0d4f3b51e9 | ||
|
|
2431b19cdf | ||
|
|
4801e1ec6d | ||
|
|
e206b93ba5 | ||
|
|
f0675446d8 | ||
|
|
2ac7eaad15 | ||
|
|
8a040fda69 | ||
|
|
2f5c966cf4 | ||
|
|
45e2629f0e | ||
|
|
517566d085 | ||
|
|
6df8657700 | ||
|
|
2a4063a259 | ||
|
|
013ec4a09d | ||
|
|
d10fec08e2 | ||
|
|
94f4328586 | ||
|
|
2b70961110 | ||
|
|
699c54e16c | ||
|
|
1217ef4d8e | ||
|
|
9aba3efbe4 | ||
|
|
1b48a2f8d5 | ||
|
|
31cb766206 | ||
|
|
4c3bcaa68d | ||
|
|
fe060617d2 | ||
|
|
de28a317f0 | ||
|
|
d29433b98d | ||
|
|
f2582b6ac9 | ||
|
|
67845be161 | ||
|
|
2bf1f46b88 | ||
|
|
70ae3a0abb | ||
|
|
96e2b5cba1 | ||
|
|
ef27a198d9 | ||
|
|
47299926bb | ||
|
|
bdc2285658 | ||
|
|
9d4ce6e526 | ||
|
|
3d2cdc21a1 | ||
|
|
93d939aef8 | ||
|
|
fb04750607 | ||
|
|
a080496e7e | ||
|
|
9fc1002167 | ||
|
|
bc5c766530 | ||
|
|
17821f863a | ||
|
|
1ca4f34ef3 | ||
|
|
8f0a1554b1 | ||
|
|
38d4f26d03 | ||
|
|
2b04c2710d | ||
|
|
a4937a1236 | ||
|
|
f6f2c39686 | ||
|
|
d7eacf1ab5 | ||
|
|
d1a4bbe28e | ||
|
|
412ecd6b1b | ||
|
|
9bc0152121 | ||
|
|
4b05cada8f | ||
|
|
a818862704 | ||
|
|
173487debc | ||
|
|
449d96cc9a | ||
|
|
f9bf6a95ed | ||
|
|
5bf6c94bb2 | ||
|
|
e1ce94a28d | ||
|
|
2a62dcf27e | ||
|
|
3094766c5c | ||
|
|
a52804595d | ||
|
|
e72f12d32b | ||
|
|
e70083708a | ||
|
|
cbc4caef19 | ||
|
|
fbeee4dbf5 | ||
|
|
d13f7e5438 | ||
|
|
7b543bd31c | ||
|
|
1743724420 | ||
|
|
73e0add670 | ||
|
|
4f5eb444bc | ||
|
|
7de98823fb | ||
|
|
6d930573fc | ||
|
|
3317002ff5 | ||
|
|
99403d0167 | ||
|
|
23ce022c60 | ||
|
|
96e1fd0fb8 | ||
|
|
f28dac1093 | ||
|
|
ff28544fb2 | ||
|
|
27765b417c | ||
|
|
b0d8d4ee26 | ||
|
|
c4b1176a6a | ||
|
|
fd133dd79a | ||
|
|
9c2477a4cf | ||
|
|
f3b6d1f351 | ||
|
|
5af7b54c9c | ||
|
|
ba9604101d | ||
|
|
e136c1fc44 | ||
|
|
c581b3293e | ||
|
|
cc7f861637 | ||
|
|
642d6fc72b | ||
|
|
e69c7f4ae0 | ||
|
|
9ca36d4763 | ||
|
|
8dc600ca02 | ||
|
|
b621ead607 | ||
|
|
66cf21f650 | ||
|
|
f3dcdc057d | ||
|
|
2bdacf636e | ||
|
|
fc06980c60 | ||
|
|
550a3a4e6d | ||
|
|
3310e7766b | ||
|
|
5ab865e89d | ||
|
|
f0c60f6ef6 | ||
|
|
f3f685c923 | ||
|
|
3646bf31b0 | ||
|
|
b39895fa2d | ||
|
|
1fce8931ab | ||
|
|
6166f6edbd | ||
|
|
dc9fbb0585 | ||
|
|
d7b2f9d05b | ||
|
|
69c4090d32 | ||
|
|
fff5fa3459 | ||
|
|
e92b80c71e | ||
|
|
8bb04ef248 | ||
|
|
d7881ccfb5 | ||
|
|
96a1f80daf | ||
|
|
a083b57260 | ||
|
|
4fa6660a3f | ||
|
|
43f2ad7043 | ||
|
|
2bf04072ea | ||
|
|
efc6fc017d | ||
|
|
6cb10401df | ||
|
|
346efd66f5 | ||
|
|
7c0889e873 | ||
|
|
bb40576bd5 | ||
|
|
6baf20275f | ||
|
|
5a57d48913 | ||
|
|
73f0207a7d | ||
|
|
4e4fb8ab10 | ||
|
|
b9cccc6b91 | ||
|
|
d42163d888 | ||
|
|
2db3e4f4d8 | ||
|
|
45380a258a | ||
|
|
40292d0896 | ||
|
|
e8be9e31ff | ||
|
|
3d0fdb1ab0 | ||
|
|
4dea1195e2 | ||
|
|
92ea39ddac | ||
|
|
05e08166c4 | ||
|
|
827cc0bdfa | ||
|
|
57bd343f4a | ||
|
|
4a76aead68 | ||
|
|
48c2148589 | ||
|
|
32bea69c28 | ||
|
|
f3c57f8bce | ||
|
|
000896b2f7 | ||
|
|
88004e5042 | ||
|
|
6001a93475 | ||
|
|
4784b2ddab | ||
|
|
32b4cd008f | ||
|
|
823f8b51be | ||
|
|
209743d6bc | ||
|
|
b93a88accc | ||
|
|
dc2314d5e2 | ||
|
|
33aa676854 | ||
|
|
4a3b3ffb8a | ||
|
|
ee5cbf1891 | ||
|
|
8fcf3544a8 | ||
|
|
2b8e987cb8 | ||
|
|
998165148b | ||
|
|
c80eff1098 | ||
|
|
cd8f2c2153 | ||
|
|
cb0abff2d5 | ||
|
|
3b1b2e2dcc | ||
|
|
7831e4ad05 | ||
|
|
e7bb859cd9 | ||
|
|
9fc26e609c | ||
|
|
4f1ee65e6c | ||
|
|
ceff827a77 | ||
|
|
a7db918775 | ||
|
|
be20ecd366 | ||
|
|
5790d8ad12 | ||
|
|
7dc58e248c | ||
|
|
7b03e63b23 | ||
|
|
55fd7c6421 | ||
|
|
ba1ea4c2bb | ||
|
|
6a4fc96835 | ||
|
|
58476bb98e | ||
|
|
88d4f14541 | ||
|
|
6bba233ba7 | ||
|
|
1d99764740 | ||
|
|
53cc479c14 | ||
|
|
d3707d9b88 | ||
|
|
1df4076fd2 | ||
|
|
28337bb6c9 | ||
|
|
c5dea52e69 | ||
|
|
d5096ff2e6 | ||
|
|
84734f1110 | ||
|
|
7094308287 | ||
|
|
3b88c8ccd2 |
13
.github/dependabot.yml
vendored
13
.github/dependabot.yml
vendored
@@ -1,13 +0,0 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
- package-ecosystem: "cargo"
|
||||
directories:
|
||||
- "/"
|
||||
- "/examples/*"
|
||||
- "/benchmarks"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
54
.github/workflows/autofix.yml
vendored
Normal file
54
.github/workflows/autofix.yml
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
name: autofix.ci
|
||||
on:
|
||||
pull_request:
|
||||
# Running this workflow on main branch pushes requires write permission to apply changes.
|
||||
# Leave it alone for future uses.
|
||||
# push:
|
||||
# branches: ["main"]
|
||||
permissions:
|
||||
contents: read
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
RUST_BACKTRACE: 1
|
||||
DEBIAN_FRONTEND: noninteractive
|
||||
jobs:
|
||||
autofix:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with: {toolchain: nightly, components: "rustfmt, clippy", target: "wasm32-unknown-unknown", rustflags: ""}
|
||||
- name: Install Glib
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libglib2.0-dev
|
||||
- name: Install jq
|
||||
run: sudo apt-get install jq
|
||||
- run: |
|
||||
echo "Formatting the workspace"
|
||||
cargo fmt --all
|
||||
|
||||
echo "Running Clippy against each member's features (default features included)"
|
||||
for member in $(cargo metadata --no-deps --format-version 1 | jq -r '.packages[] | .name'); do
|
||||
echo "Working on member $member":
|
||||
echo -e "\tdefault-features/no-features:"
|
||||
# this will also run on members with no features or default features
|
||||
cargo clippy --allow-dirty --fix --lib --package "$member"
|
||||
|
||||
features=$(cargo metadata --no-deps --format-version 1 | jq -r ".packages[] | select(.name == \"$member\") | .features | keys[]")
|
||||
for feature in $features; do
|
||||
if [ "$feature" = "default" ]; then
|
||||
continue
|
||||
fi
|
||||
echo -e "\tfeature $feature"
|
||||
cargo clippy --allow-dirty --fix --lib --package "$member" --features "$feature"
|
||||
done
|
||||
done
|
||||
- uses: autofix-ci/action@v1.3.1
|
||||
if: ${{ always() }}
|
||||
with:
|
||||
fail-fast: false
|
||||
9
.github/workflows/ci-changed-examples.yml
vendored
9
.github/workflows/ci-changed-examples.yml
vendored
@@ -1,23 +1,23 @@
|
||||
name: CI Changed Examples
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- leptos_0.6
|
||||
- leptos_0.8
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
- leptos_0.6
|
||||
- leptos_0.8
|
||||
jobs:
|
||||
get-example-changed:
|
||||
uses: ./.github/workflows/get-example-changed.yml
|
||||
|
||||
get-matrix:
|
||||
needs: [get-example-changed]
|
||||
uses: ./.github/workflows/get-changed-examples-matrix.yml
|
||||
with:
|
||||
example_changed: ${{ fromJSON(needs.get-example-changed.outputs.example_changed) }}
|
||||
|
||||
test:
|
||||
name: CI
|
||||
needs: [get-example-changed, get-matrix]
|
||||
@@ -28,5 +28,6 @@ jobs:
|
||||
uses: ./.github/workflows/run-cargo-make-task.yml
|
||||
with:
|
||||
directory: ${{ matrix.directory }}
|
||||
erased_mode: ${{ matrix.erased_mode }}
|
||||
cargo_make_task: "ci"
|
||||
toolchain: stable
|
||||
|
||||
7
.github/workflows/ci-examples.yml
vendored
7
.github/workflows/ci-examples.yml
vendored
@@ -1,13 +1,15 @@
|
||||
name: CI Examples
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- leptos_0.6
|
||||
- leptos_0.8
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
- leptos_0.6
|
||||
- leptos_0.8
|
||||
jobs:
|
||||
get-leptos-changed:
|
||||
uses: ./.github/workflows/get-leptos-changed.yml
|
||||
@@ -23,5 +25,6 @@ jobs:
|
||||
uses: ./.github/workflows/run-cargo-make-task.yml
|
||||
with:
|
||||
directory: ${{ matrix.directory }}
|
||||
erased_mode: ${{ matrix.erased_mode }}
|
||||
cargo_make_task: "ci"
|
||||
toolchain: stable
|
||||
|
||||
21
.github/workflows/ci-semver.yml
vendored
21
.github/workflows/ci-semver.yml
vendored
@@ -1,28 +1,33 @@
|
||||
name: CI semver
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- leptos_0.6
|
||||
- leptos_0.8
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
- leptos_0.6
|
||||
- leptos_0.8
|
||||
env:
|
||||
DEBIAN_FRONTEND: noninteractive
|
||||
jobs:
|
||||
get-leptos-changed:
|
||||
uses: ./.github/workflows/get-leptos-changed.yml
|
||||
|
||||
test:
|
||||
needs: [get-leptos-changed]
|
||||
if: github.event.pull_request.labels[0].name == 'semver' # needs.get-leptos-changed.outputs.leptos_changed == 'true' && github.event.pull_request.labels[0].name != 'breaking'
|
||||
name: Run semver check (nightly-2024-08-01)
|
||||
if: needs.get-leptos-changed.outputs.leptos_changed == 'true' && github.event.pull_request.labels[0].name != 'breaking'
|
||||
name: Run semver check (nightly-2025-03-05)
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Install Glib
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libglib2.0-dev
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Semver Checks
|
||||
uses: obi1kenobi/cargo-semver-checks-action@v2
|
||||
with:
|
||||
rust-toolchain: nightly-2024-08-01
|
||||
rust-toolchain: nightly-2025-03-05
|
||||
|
||||
44
.github/workflows/ci.yml
vendored
44
.github/workflows/ci.yml
vendored
@@ -1,52 +1,30 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- leptos_0.6
|
||||
- leptos_0.8
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
- leptos_0.6
|
||||
- leptos_0.8
|
||||
jobs:
|
||||
get-leptos-changed:
|
||||
uses: ./.github/workflows/get-leptos-changed.yml
|
||||
|
||||
get-leptos-matrix:
|
||||
uses: ./.github/workflows/get-leptos-matrix.yml
|
||||
test:
|
||||
name: CI
|
||||
needs: [get-leptos-changed]
|
||||
needs: [get-leptos-changed, get-leptos-matrix]
|
||||
if: needs.get-leptos-changed.outputs.leptos_changed == 'true'
|
||||
strategy:
|
||||
matrix:
|
||||
directory:
|
||||
[
|
||||
any_error,
|
||||
any_spawner,
|
||||
const_str_slice_concat,
|
||||
either_of,
|
||||
hydration_context,
|
||||
integrations/actix,
|
||||
integrations/axum,
|
||||
integrations/utils,
|
||||
leptos,
|
||||
leptos_config,
|
||||
leptos_dom,
|
||||
leptos_hot_reload,
|
||||
leptos_macro,
|
||||
leptos_server,
|
||||
meta,
|
||||
next_tuple,
|
||||
oco,
|
||||
or_poisoned,
|
||||
reactive_graph,
|
||||
router,
|
||||
router_macro,
|
||||
server_fn,
|
||||
server_fn/server_fn_macro_default,
|
||||
server_fn_macro,
|
||||
]
|
||||
matrix: ${{ fromJSON(needs.get-leptos-matrix.outputs.matrix) }}
|
||||
fail-fast: false
|
||||
uses: ./.github/workflows/run-cargo-make-task.yml
|
||||
with:
|
||||
directory: ${{ matrix.directory }}
|
||||
erased_mode: ${{ matrix.erased_mode }}
|
||||
cargo_make_task: "ci"
|
||||
toolchain: nightly-2024-08-01
|
||||
toolchain: nightly-2025-03-05
|
||||
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
|
||||
- name: Get example project directories that changed
|
||||
id: changed-dirs
|
||||
uses: tj-actions/changed-files@v44
|
||||
uses: tj-actions/changed-files@v45
|
||||
with:
|
||||
dir_names: true
|
||||
dir_names_max_depth: "2"
|
||||
@@ -50,5 +50,5 @@ jobs:
|
||||
echo "matrix={\"directory\":${{ steps.changed-dirs.outputs.all_changed_files }}}" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
# Create matrix with one item to prevent an empty vector error
|
||||
echo "matrix={\"directory\":[\"NO_CHANGE\"]}" >> "$GITHUB_OUTPUT"
|
||||
echo "matrix={\"directory\":[\"NO_CHANGE\"], \"erased_mode\": [false, true]}" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
8
.github/workflows/get-example-changed.yml
vendored
8
.github/workflows/get-example-changed.yml
vendored
@@ -1,12 +1,10 @@
|
||||
name: Examples Changed Call
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
outputs:
|
||||
example_changed:
|
||||
description: "Example Changed"
|
||||
value: ${{ jobs.get-example-changed.outputs.example_changed }}
|
||||
|
||||
jobs:
|
||||
get-example-changed:
|
||||
name: Get Example Changed
|
||||
@@ -18,21 +16,17 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Get example files that changed
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v44
|
||||
uses: tj-actions/changed-files@v45
|
||||
with:
|
||||
files: |
|
||||
examples/**
|
||||
!examples/cargo-make/**
|
||||
!examples/gtk/**
|
||||
!examples/Makefile.toml
|
||||
!examples/*.md
|
||||
|
||||
- name: List example files that changed
|
||||
run: echo '${{ steps.changed-files.outputs.all_changed_files }}'
|
||||
|
||||
- name: Set example_changed
|
||||
id: set-example-changed
|
||||
run: |
|
||||
|
||||
20
.github/workflows/get-examples-matrix.yml
vendored
20
.github/workflows/get-examples-matrix.yml
vendored
@@ -1,38 +1,34 @@
|
||||
name: Get Examples Matrix Call
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
outputs:
|
||||
matrix:
|
||||
description: "Matrix"
|
||||
value: ${{ jobs.create.outputs.matrix }}
|
||||
|
||||
jobs:
|
||||
create:
|
||||
name: Create Examples Matrix
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
||||
env:
|
||||
# separate examples using "|" (vertical bar) char like "a|b|c".
|
||||
# cargo-make should be excluded by default.
|
||||
EXCLUDED_EXAMPLES: cargo-make
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install jq
|
||||
run: sudo apt-get install jq
|
||||
|
||||
- name: Set Matrix
|
||||
id: set-matrix
|
||||
run: |
|
||||
examples=$(ls examples |
|
||||
awk '{print "examples/" $0}' |
|
||||
grep -v .md |
|
||||
grep -v examples/Makefile.toml |
|
||||
grep -v examples/cargo-make |
|
||||
grep -v examples/gtk |
|
||||
examples=$(ls -1d examples/*/ |
|
||||
grep -vE "($EXCLUDED_EXAMPLES)" |
|
||||
sed 's/\/$//' |
|
||||
jq -R -s -c 'split("\n")[:-1]')
|
||||
echo "Example Directories: $examples"
|
||||
echo "matrix={\"directory\":$examples}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
echo "matrix={\"directory\":$examples, \"erased_mode\": [false, true]}" >> "$GITHUB_OUTPUT"
|
||||
- name: Print Location Info
|
||||
run: |
|
||||
echo "Workspace: ${{ github.workspace }}"
|
||||
|
||||
39
.github/workflows/get-leptos-changed.yml
vendored
39
.github/workflows/get-leptos-changed.yml
vendored
@@ -1,12 +1,10 @@
|
||||
name: Get Leptos Changed Call
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
outputs:
|
||||
leptos_changed:
|
||||
description: "Leptos Changed"
|
||||
value: ${{ jobs.create.outputs.leptos_changed }}
|
||||
|
||||
jobs:
|
||||
create:
|
||||
name: Detect Source Change
|
||||
@@ -18,40 +16,19 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Get source files that changed
|
||||
id: changed-source
|
||||
uses: tj-actions/changed-files@v44
|
||||
uses: tj-actions/changed-files@v45
|
||||
with:
|
||||
files: |
|
||||
any_error/**
|
||||
any_spawner/**
|
||||
const_str_slice_concat/**
|
||||
either_of/**
|
||||
hydration_context/**
|
||||
integrations/actix/**
|
||||
integrations/axum/**
|
||||
integrations/utils/**
|
||||
leptos/**
|
||||
leptos_config/**
|
||||
leptos_dom/**
|
||||
leptos_hot_reload/**
|
||||
leptos_macro/**
|
||||
leptos_server/**
|
||||
meta/**
|
||||
next_tuple/**
|
||||
oco/**
|
||||
or_poisoned/**
|
||||
reactive_graph/**
|
||||
router/**
|
||||
router_macro/**
|
||||
server_fn/**
|
||||
server_fn/server_fn_macro_default/**
|
||||
server_fn_macro/**
|
||||
|
||||
files_ignore: |
|
||||
.*/**/*
|
||||
cargo-make/**/*
|
||||
examples/**/*
|
||||
projects/**/*
|
||||
benchmarks/**/*
|
||||
docs/**/*
|
||||
- name: List source files that changed
|
||||
run: echo '${{ steps.changed-source.outputs.all_changed_files }}'
|
||||
|
||||
- name: Set leptos_changed
|
||||
id: set-source-changed
|
||||
run: |
|
||||
|
||||
32
.github/workflows/get-leptos-matrix.yml
vendored
Normal file
32
.github/workflows/get-leptos-matrix.yml
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
name: Get Leptos Matrix Call
|
||||
on:
|
||||
workflow_call:
|
||||
outputs:
|
||||
matrix:
|
||||
description: "Matrix"
|
||||
value: ${{ jobs.create.outputs.matrix }}
|
||||
jobs:
|
||||
create:
|
||||
name: Create Leptos Matrix
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Install jq
|
||||
run: sudo apt-get install jq
|
||||
- name: Set Matrix
|
||||
id: set-matrix
|
||||
run: |
|
||||
crates=$(cargo metadata --no-deps --quiet --format-version 1 |
|
||||
jq -r '.packages[] | select(.name != "workspace") | .manifest_path| rtrimstr("/Cargo.toml")' |
|
||||
sed "s|$(pwd)/||" |
|
||||
jq -R -s -c 'split("\n")[:-1]')
|
||||
echo "Leptos Directories: $crates"
|
||||
echo "matrix={\"directory\":$crates, \"erased_mode\": [false, true]}" >> "$GITHUB_OUTPUT"
|
||||
- name: Print Location Info
|
||||
run: |
|
||||
echo "Workspace: ${{ github.workspace }}"
|
||||
pwd
|
||||
ls | sort -u
|
||||
56
.github/workflows/run-cargo-make-task.yml
vendored
56
.github/workflows/run-cargo-make-task.yml
vendored
@@ -1,81 +1,85 @@
|
||||
name: Run Task
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
directory:
|
||||
required: true
|
||||
type: string
|
||||
erased_mode:
|
||||
required: true
|
||||
type: boolean
|
||||
cargo_make_task:
|
||||
required: true
|
||||
type: string
|
||||
toolchain:
|
||||
required: true
|
||||
type: string
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
|
||||
|
||||
DEBIAN_FRONTEND: noninteractive
|
||||
RUSTFLAGS: ${{ inputs.erased_mode && '--cfg erase_components' || '' }}
|
||||
jobs:
|
||||
test:
|
||||
name: Run ${{ inputs.cargo_make_task }} (${{ inputs.toolchain }})
|
||||
name: "Run ${{ inputs.cargo_make_task }} (${{ inputs.toolchain }}) (erased_mode: ${{ inputs.erased_mode }})"
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Free Disk Space
|
||||
run: |
|
||||
echo "Disk space before cleanup:"
|
||||
df -h
|
||||
sudo rm -rf /usr/local/.ghcup
|
||||
sudo rm -rf /opt/hostedtoolcache/CodeQL
|
||||
sudo rm -rf /usr/local/lib/android/sdk/ndk
|
||||
sudo rm -rf /usr/share/dotnet
|
||||
sudo rm -rf /opt/ghc
|
||||
sudo rm -rf /usr/local/share/boost
|
||||
sudo apt-get clean
|
||||
echo "Disk space after cleanup:"
|
||||
df -h
|
||||
# Setup environment
|
||||
- name: Install Glib
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libglib2.0-dev
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: ${{ inputs.toolchain }}
|
||||
|
||||
- name: Add wasm32-unknown-unknown
|
||||
run: rustup target add wasm32-unknown-unknown
|
||||
|
||||
- name: Setup cargo-make
|
||||
uses: davidB/rust-cargo-make@v1
|
||||
|
||||
- name: Cargo generate-lockfile
|
||||
run: cargo generate-lockfile
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Install binstall
|
||||
uses: cargo-bins/cargo-binstall@main
|
||||
|
||||
- name: Install wasm-bindgen
|
||||
run: cargo binstall wasm-bindgen-cli --no-confirm
|
||||
|
||||
- name: Install cargo-leptos
|
||||
run: cargo binstall cargo-leptos --no-confirm
|
||||
|
||||
run: cargo binstall cargo-leptos --locked --no-confirm
|
||||
- name: Install Trunk
|
||||
uses: jetli/trunk-action@v0.5.0
|
||||
with:
|
||||
version: "latest"
|
||||
|
||||
- name: Print Trunk Version
|
||||
run: trunk --version
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
name: Install pnpm
|
||||
id: pnpm-install
|
||||
with:
|
||||
version: 8
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
run: |
|
||||
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: actions/cache@v4
|
||||
name: Setup pnpm cache
|
||||
with:
|
||||
@@ -83,7 +87,6 @@ jobs:
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Maybe install chromedriver
|
||||
run: |
|
||||
project_makefile=${{inputs.directory}}/Makefile.toml
|
||||
@@ -99,7 +102,6 @@ jobs:
|
||||
else
|
||||
echo chromedriver is not required
|
||||
fi
|
||||
|
||||
- name: Maybe install playwright browser dependencies
|
||||
run: |
|
||||
for pw_path in $(find ${{inputs.directory}} -name playwright.config.ts)
|
||||
@@ -113,12 +115,16 @@ jobs:
|
||||
echo Playwright is not required
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Install Deno
|
||||
uses: denoland/setup-deno@v1
|
||||
uses: denoland/setup-deno@v2
|
||||
with:
|
||||
deno-version: v1.x
|
||||
|
||||
- name: Maybe install gtk-rs dependencies
|
||||
run: |
|
||||
if [ ! -z $(echo ${{inputs.directory}} | grep gtk) ]; then
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libglib2.0-dev libgio2.0-cil-dev libgraphene-1.0-dev libcairo2-dev libpango1.0-dev libgtk-4-dev
|
||||
fi
|
||||
# Run Cargo Make Task
|
||||
- name: ${{ inputs.cargo_make_task }}
|
||||
run: |
|
||||
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -3,7 +3,9 @@ dist
|
||||
pkg
|
||||
comparisons
|
||||
blob.rs
|
||||
Cargo.lock
|
||||
**/projects/**/Cargo.lock
|
||||
**/examples/**/Cargo.lock
|
||||
**/benchmarks/**/Cargo.lock
|
||||
**/*.rs.bk
|
||||
.DS_Store
|
||||
.idea
|
||||
@@ -11,4 +13,5 @@ Cargo.lock
|
||||
.envrc
|
||||
|
||||
.vscode
|
||||
vendor
|
||||
vendor
|
||||
hash.txt
|
||||
|
||||
4563
Cargo.lock
generated
Normal file
4563
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
57
Cargo.toml
57
Cargo.toml
@@ -40,36 +40,39 @@ members = [
|
||||
exclude = ["benchmarks", "examples", "projects"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.7.0-beta2"
|
||||
version = "0.8.0-beta"
|
||||
edition = "2021"
|
||||
rust-version = "1.76"
|
||||
|
||||
[workspace.dependencies]
|
||||
throw_error = { path = "./any_error/", version = "0.2.0-beta2" }
|
||||
any_spawner = { path = "./any_spawner/", version = "0.1.0" }
|
||||
const_str_slice_concat = { path = "./const_str_slice_concat", version = "0.1.0" }
|
||||
either_of = { path = "./either_of/", version = "0.1.0" }
|
||||
hydration_context = { path = "./hydration_context", version = "0.2.0-beta2" }
|
||||
leptos = { path = "./leptos", version = "0.7.0-beta2" }
|
||||
leptos_config = { path = "./leptos_config", version = "0.7.0-beta2" }
|
||||
leptos_dom = { path = "./leptos_dom", version = "0.7.0-beta2" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.7.0-beta2" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.7.0-beta2" }
|
||||
leptos_macro = { path = "./leptos_macro", version = "0.7.0-beta2" }
|
||||
leptos_router = { path = "./router", version = "0.7.0-beta2" }
|
||||
leptos_router_macro = { path = "./router_macro", version = "0.7.0-beta2" }
|
||||
leptos_server = { path = "./leptos_server", version = "0.7.0-beta2" }
|
||||
leptos_meta = { path = "./meta", version = "0.7.0-beta2" }
|
||||
next_tuple = { path = "./next_tuple", version = "0.1.0-beta2" }
|
||||
throw_error = { path = "./any_error/", version = "0.3.0" }
|
||||
any_spawner = { path = "./any_spawner/", version = "0.2.0" }
|
||||
const_str_slice_concat = { path = "./const_str_slice_concat", version = "0.1" }
|
||||
either_of = { path = "./either_of/", version = "0.1.5" }
|
||||
hydration_context = { path = "./hydration_context", version = "0.3.0" }
|
||||
itertools = "0.14.0"
|
||||
leptos = { path = "./leptos", version = "0.8.0-beta" }
|
||||
leptos_config = { path = "./leptos_config", version = "0.8.0-beta" }
|
||||
leptos_dom = { path = "./leptos_dom", version = "0.8.0-beta" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.8.0-beta" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.8.0-beta" }
|
||||
leptos_macro = { path = "./leptos_macro", version = "0.8.0-beta" }
|
||||
leptos_router = { path = "./router", version = "0.8.0-beta" }
|
||||
leptos_router_macro = { path = "./router_macro", version = "0.8.0-beta" }
|
||||
leptos_server = { path = "./leptos_server", version = "0.8.0-beta" }
|
||||
leptos_meta = { path = "./meta", version = "0.8.0-beta" }
|
||||
next_tuple = { path = "./next_tuple", version = "0.1.0" }
|
||||
oco_ref = { path = "./oco", version = "0.2.0" }
|
||||
or_poisoned = { path = "./or_poisoned", version = "0.1.0" }
|
||||
reactive_graph = { path = "./reactive_graph", version = "0.1.0-beta2" }
|
||||
reactive_stores = { path = "./reactive_stores", version = "0.1.0-beta2" }
|
||||
reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.1.0-beta2" }
|
||||
server_fn = { path = "./server_fn", version = "0.7.0-beta2" }
|
||||
server_fn_macro = { path = "./server_fn_macro", version = "0.7.0-beta2" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.7.0-beta2" }
|
||||
tachys = { path = "./tachys", version = "0.1.0-beta2" }
|
||||
reactive_graph = { path = "./reactive_graph", version = "0.2.0-beta" }
|
||||
reactive_stores = { path = "./reactive_stores", version = "0.2.0-beta" }
|
||||
reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.2.0-beta" }
|
||||
serde_json = "1.0.0"
|
||||
server_fn = { path = "./server_fn", version = "0.8.0-beta" }
|
||||
server_fn_macro = { path = "./server_fn_macro", version = "0.8.0-beta" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.8.0-beta" }
|
||||
tachys = { path = "./tachys", version = "0.2.0-beta" }
|
||||
wasm-bindgen = { version = "0.2.100" }
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
@@ -78,3 +81,9 @@ opt-level = 'z'
|
||||
|
||||
[workspace.metadata.cargo-all-features]
|
||||
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
|
||||
|
||||
[workspace.lints.rust]
|
||||
unexpected_cfgs = { level = "warn", check-cfg = [
|
||||
'cfg(leptos_debuginfo)',
|
||||
'cfg(erase_components)',
|
||||
] }
|
||||
|
||||
15
README.md
15
README.md
@@ -5,6 +5,7 @@
|
||||
|
||||
[](https://crates.io/crates/leptos)
|
||||
[](https://docs.rs/leptos)
|
||||

|
||||
[](https://discord.gg/YdRAhS7eQB)
|
||||
[](https://matrix.to/#/#leptos:matrix.org)
|
||||
|
||||
@@ -12,8 +13,6 @@
|
||||
|
||||
You can find a list of useful libraries and example projects at [`awesome-leptos`](https://github.com/leptos-rs/awesome-leptos).
|
||||
|
||||
# The `main` branch is currently undergoing major changes in preparation for the [0.7](https://github.com/leptos-rs/leptos/milestone/4) release. For a stable version, please use the [v0.6.13 tag](https://github.com/leptos-rs/leptos/tree/v0.6.13)
|
||||
|
||||
# Leptos
|
||||
|
||||
```rust
|
||||
@@ -22,7 +21,7 @@ use leptos::*;
|
||||
#[component]
|
||||
pub fn SimpleCounter(initial_value: i32) -> impl IntoView {
|
||||
// create a reactive signal with the initial value
|
||||
let (value, set_value) = create_signal(initial_value);
|
||||
let (value, set_value) = signal(initial_value);
|
||||
|
||||
// create event handlers for our buttons
|
||||
// note that `value` and `set_value` are `Copy`, so it's super easy to move them into closures
|
||||
@@ -47,7 +46,7 @@ pub fn SimpleCounter(initial_value: i32) -> impl IntoView {
|
||||
pub fn SimpleCounterWithBuilder(initial_value: i32) -> impl IntoView {
|
||||
use leptos::html::*;
|
||||
|
||||
let (value, set_value) = create_signal(initial_value);
|
||||
let (value, set_value) = signal(initial_value);
|
||||
let clear = move |_| set_value(0);
|
||||
let decrement = move |_| set_value.update(|value| *value -= 1);
|
||||
let increment = move |_| set_value.update(|value| *value += 1);
|
||||
@@ -159,9 +158,7 @@ Sure! Obviously the `view` macro is for generating DOM nodes but you can use the
|
||||
- Use event listeners to update signals
|
||||
- Create effects to update the UI
|
||||
|
||||
I've put together a [very simple GTK example](https://github.com/leptos-rs/leptos/blob/main/examples/gtk/src/main.rs) so you can see what I mean.
|
||||
|
||||
The new rendering approach being developed for 0.7 supports “universal rendering,” i.e., it can use any rendering library that supports a small set of 6-8 functions. (This is intended as a layer over typical retained-mode, OOP-style GUI toolkits like the DOM, GTK, etc.) That future rendering work will allow creating native UI in a way that is much more similar to the declarative approach used by the web framework.
|
||||
The 0.7 update originally set out to create a "generic rendering" approach that would allow us to reuse most of the same view logic to do all of the above. Unfortunately, this has had to be shelved for now due to difficulties encountered by the Rust compiler when building larger-scale applications with the number of generics spread throughout the codebase that this required. It's an approach I'm looking forward to exploring again in the future; feel free to reach out if you're interested in this kind of work.
|
||||
|
||||
### How is this different from Yew?
|
||||
|
||||
@@ -171,14 +168,14 @@ Yew is the most-used library for Rust web UI development, but there are several
|
||||
- **Performance:** This has huge performance implications: Leptos is simply much faster at both creating and updating the UI than Yew is.
|
||||
- **Server integration:** Yew was created in an era in which browser-rendered single-page apps (SPAs) were the dominant paradigm. While Leptos supports client-side rendering, it also focuses on integrating with the server side of your application via server functions and multiple modes of serving HTML, including out-of-order streaming.
|
||||
|
||||
- ### How is this different from Dioxus?
|
||||
### How is this different from Dioxus?
|
||||
|
||||
Like Leptos, Dioxus is a framework for building UIs using web technologies. However, there are significant differences in approach and features.
|
||||
|
||||
- **VDOM vs. fine-grained:** While Dioxus has a performant virtual DOM (VDOM), it still uses coarse-grained/component-scoped reactivity: changing a stateful value reruns the component function and diffs the old UI against the new one. Leptos components use a different mental model, creating (and returning) actual DOM nodes and setting up a reactive system to update those DOM nodes.
|
||||
- **Web vs. desktop priorities:** Dioxus uses Leptos server functions in its fullstack mode, but does not have the same `<Suspense>`-based support for things like streaming HTML rendering, or share the same focus on holistic web performance. Leptos tends to prioritize holistic web performance (streaming HTML rendering, smaller WASM binary sizes, etc.), whereas Dioxus has an unparalleled experience when building desktop apps, because your application logic runs as a native Rust binary.
|
||||
|
||||
- ### How is this different from Sycamore?
|
||||
### How is this different from Sycamore?
|
||||
|
||||
Sycamore and Leptos are both heavily influenced by SolidJS. At this point, Leptos has a larger community and ecosystem and is more actively developed. Other differences:
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "throw_error"
|
||||
version = "0.2.0-beta2"
|
||||
version = "0.3.0"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
@@ -10,4 +10,4 @@ rust-version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
pin-project-lite = "0.2.14"
|
||||
pin-project-lite = "0.2.15"
|
||||
|
||||
@@ -9,7 +9,7 @@ use std::{
|
||||
error,
|
||||
fmt::{self, Display},
|
||||
future::Future,
|
||||
mem, ops,
|
||||
ops,
|
||||
pin::Pin,
|
||||
sync::Arc,
|
||||
task::{Context, Poll},
|
||||
@@ -17,11 +17,6 @@ use std::{
|
||||
|
||||
/* Wrapper Types */
|
||||
|
||||
/// This is a result type into which any error can be converted.
|
||||
///
|
||||
/// Results are stored as [`Error`].
|
||||
pub type Result<T, E = Error> = core::result::Result<T, E>;
|
||||
|
||||
/// A generic wrapper for any error.
|
||||
#[derive(Debug, Clone)]
|
||||
#[repr(transparent)]
|
||||
@@ -109,7 +104,7 @@ pub fn get_error_hook() -> Option<Arc<dyn ErrorHook>> {
|
||||
/// Sets the current thread-local error hook, which will be invoked when [`throw`] is called.
|
||||
pub fn set_error_hook(hook: Arc<dyn ErrorHook>) -> ResetErrorHookOnDrop {
|
||||
ResetErrorHookOnDrop(
|
||||
ERROR_HOOK.with_borrow_mut(|this| mem::replace(this, Some(hook))),
|
||||
ERROR_HOOK.with_borrow_mut(|this| Option::replace(this, hook)),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "any_spawner"
|
||||
version = "0.1.1"
|
||||
version = "0.2.1"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
@@ -9,22 +9,28 @@ description = "Spawn asynchronous tasks in an executor-independent way."
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
futures = "0.3.30"
|
||||
glib = { version = "0.20.0", optional = true }
|
||||
thiserror = "1.0"
|
||||
tokio = { version = "1.39", optional = true, default-features = false, features = [
|
||||
async-executor = { version = "1.13.1", optional = true }
|
||||
futures = "0.3.31"
|
||||
glib = { version = "0.20.6", optional = true }
|
||||
thiserror = "2.0"
|
||||
tokio = { version = "1.41", optional = true, default-features = false, features = [
|
||||
"rt",
|
||||
] }
|
||||
tracing = { version = "0.1.40", optional = true }
|
||||
wasm-bindgen-futures = { version = "0.4.42", optional = true }
|
||||
tracing = { version = "0.1.41", optional = true }
|
||||
wasm-bindgen-futures = { version = "0.4.50", optional = true }
|
||||
|
||||
[features]
|
||||
async-executor = ["dep:async-executor"]
|
||||
tracing = ["dep:tracing"]
|
||||
tokio = ["dep:tokio"]
|
||||
glib = ["dep:glib"]
|
||||
wasm-bindgen = ["dep:wasm-bindgen-futures"]
|
||||
futures-executor = ["futures/thread-pool", "futures/executor"]
|
||||
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["tracing"]
|
||||
|
||||
@@ -32,11 +32,14 @@
|
||||
use std::{future::Future, pin::Pin, sync::OnceLock};
|
||||
use thiserror::Error;
|
||||
|
||||
pub(crate) type PinnedFuture<T> = Pin<Box<dyn Future<Output = T> + Send>>;
|
||||
pub(crate) type PinnedLocalFuture<T> = Pin<Box<dyn Future<Output = T>>>;
|
||||
/// A future that has been pinned.
|
||||
pub type PinnedFuture<T> = Pin<Box<dyn Future<Output = T> + Send>>;
|
||||
/// A future that has been pinned.
|
||||
pub type PinnedLocalFuture<T> = Pin<Box<dyn Future<Output = T>>>;
|
||||
|
||||
static SPAWN: OnceLock<fn(PinnedFuture<()>)> = OnceLock::new();
|
||||
static SPAWN_LOCAL: OnceLock<fn(PinnedLocalFuture<()>)> = OnceLock::new();
|
||||
static POLL_LOCAL: OnceLock<fn()> = OnceLock::new();
|
||||
|
||||
/// Errors that can occur when using the executor.
|
||||
#[derive(Error, Debug)]
|
||||
@@ -115,6 +118,14 @@ impl Executor {
|
||||
});
|
||||
_ = rx.await;
|
||||
}
|
||||
|
||||
/// Polls the current async executor.
|
||||
/// Not all async executors support polling, so this function may not do anything.
|
||||
pub fn poll_local() {
|
||||
if let Some(poller) = POLL_LOCAL.get() {
|
||||
poller()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Executor {
|
||||
@@ -193,13 +204,15 @@ impl Executor {
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "futures-executor")))]
|
||||
pub fn init_futures_executor() -> Result<(), ExecutorError> {
|
||||
use futures::{
|
||||
executor::{LocalPool, ThreadPool},
|
||||
executor::{LocalPool, LocalSpawner, ThreadPool},
|
||||
task::{LocalSpawnExt, SpawnExt},
|
||||
};
|
||||
use std::cell::RefCell;
|
||||
|
||||
static THREAD_POOL: OnceLock<ThreadPool> = OnceLock::new();
|
||||
thread_local! {
|
||||
static LOCAL_POOL: LocalPool = LocalPool::new();
|
||||
static LOCAL_POOL: RefCell<LocalPool> = RefCell::new(LocalPool::new());
|
||||
static SPAWNER: LocalSpawner = LOCAL_POOL.with(|pool| pool.borrow().spawner());
|
||||
}
|
||||
|
||||
fn get_thread_pool() -> &'static ThreadPool {
|
||||
@@ -218,28 +231,131 @@ impl Executor {
|
||||
.map_err(|_| ExecutorError::AlreadySet)?;
|
||||
SPAWN_LOCAL
|
||||
.set(|fut| {
|
||||
LOCAL_POOL.with(|pool| {
|
||||
let spawner = pool.spawner();
|
||||
SPAWNER.with(|spawner| {
|
||||
spawner.spawn_local(fut).expect("failed to spawn future");
|
||||
});
|
||||
})
|
||||
.map_err(|_| ExecutorError::AlreadySet)?;
|
||||
POLL_LOCAL
|
||||
.set(|| {
|
||||
LOCAL_POOL.with(|pool| {
|
||||
if let Ok(mut pool) = pool.try_borrow_mut() {
|
||||
pool.run_until_stalled();
|
||||
}
|
||||
// If we couldn't borrow_mut, we're in a nested call to poll, so we don't need to do anything.
|
||||
});
|
||||
})
|
||||
.map_err(|_| ExecutorError::AlreadySet)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Globally sets the [`async_executor`] executor as the executor used to spawn tasks,
|
||||
/// lazily creating a thread pool to spawn tasks into.
|
||||
///
|
||||
/// Returns `Err(_)` if an executor has already been set.
|
||||
///
|
||||
/// Requires the `async-executor` feature to be activated on this crate.
|
||||
#[cfg(feature = "async-executor")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "async-executor")))]
|
||||
pub fn init_async_executor() -> Result<(), ExecutorError> {
|
||||
use async_executor::{Executor, LocalExecutor};
|
||||
|
||||
static THREAD_POOL: OnceLock<Executor> = OnceLock::new();
|
||||
thread_local! {
|
||||
static LOCAL_POOL: LocalExecutor<'static> = const { LocalExecutor::new() };
|
||||
}
|
||||
|
||||
fn get_thread_pool() -> &'static Executor<'static> {
|
||||
THREAD_POOL.get_or_init(Executor::new)
|
||||
}
|
||||
|
||||
SPAWN
|
||||
.set(|fut| {
|
||||
get_thread_pool().spawn(fut).detach();
|
||||
})
|
||||
.map_err(|_| ExecutorError::AlreadySet)?;
|
||||
SPAWN_LOCAL
|
||||
.set(|fut| {
|
||||
LOCAL_POOL.with(|pool| pool.spawn(fut).detach());
|
||||
})
|
||||
.map_err(|_| ExecutorError::AlreadySet)?;
|
||||
POLL_LOCAL
|
||||
.set(|| {
|
||||
LOCAL_POOL.with(|pool| pool.try_tick());
|
||||
})
|
||||
.map_err(|_| ExecutorError::AlreadySet)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Globally sets a custom executor as the executor used to spawn tasks.
|
||||
///
|
||||
/// Returns `Err(_)` if an executor has already been set.
|
||||
pub fn init_custom_executor(
|
||||
custom_executor: impl CustomExecutor + Send + Sync + 'static,
|
||||
) -> Result<(), ExecutorError> {
|
||||
static EXECUTOR: OnceLock<Box<dyn CustomExecutor + Send + Sync>> =
|
||||
OnceLock::new();
|
||||
EXECUTOR
|
||||
.set(Box::new(custom_executor))
|
||||
.map_err(|_| ExecutorError::AlreadySet)?;
|
||||
|
||||
SPAWN
|
||||
.set(|fut| {
|
||||
EXECUTOR.get().unwrap().spawn(fut);
|
||||
})
|
||||
.map_err(|_| ExecutorError::AlreadySet)?;
|
||||
SPAWN_LOCAL
|
||||
.set(|fut| EXECUTOR.get().unwrap().spawn_local(fut))
|
||||
.map_err(|_| ExecutorError::AlreadySet)?;
|
||||
POLL_LOCAL
|
||||
.set(|| EXECUTOR.get().unwrap().poll_local())
|
||||
.map_err(|_| ExecutorError::AlreadySet)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Locally sets a custom executor as the executor used to spawn tasks
|
||||
/// in the current thread.
|
||||
///
|
||||
/// Returns `Err(_)` if an executor has already been set.
|
||||
pub fn init_local_custom_executor(
|
||||
custom_executor: impl CustomExecutor + 'static,
|
||||
) -> Result<(), ExecutorError> {
|
||||
thread_local! {
|
||||
static EXECUTOR: OnceLock<Box<dyn CustomExecutor>> = OnceLock::new();
|
||||
}
|
||||
EXECUTOR.with(|this| {
|
||||
this.set(Box::new(custom_executor))
|
||||
.map_err(|_| ExecutorError::AlreadySet)
|
||||
})?;
|
||||
|
||||
SPAWN
|
||||
.set(|fut| {
|
||||
EXECUTOR.with(|this| this.get().unwrap().spawn(fut));
|
||||
})
|
||||
.map_err(|_| ExecutorError::AlreadySet)?;
|
||||
SPAWN_LOCAL
|
||||
.set(|fut| {
|
||||
EXECUTOR.with(|this| this.get().unwrap().spawn_local(fut));
|
||||
})
|
||||
.map_err(|_| ExecutorError::AlreadySet)?;
|
||||
POLL_LOCAL
|
||||
.set(|| {
|
||||
EXECUTOR.with(|this| this.get().unwrap().poll_local());
|
||||
})
|
||||
.map_err(|_| ExecutorError::AlreadySet)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[cfg(feature = "futures-executor")]
|
||||
#[test]
|
||||
fn can_spawn_local_future() {
|
||||
use crate::Executor;
|
||||
use std::rc::Rc;
|
||||
Executor::init_futures_executor().expect("couldn't set executor");
|
||||
let rc = Rc::new(());
|
||||
Executor::spawn_local(async {
|
||||
_ = rc;
|
||||
});
|
||||
Executor::spawn(async {});
|
||||
}
|
||||
/// A trait for custom executors.
|
||||
/// Custom executors can be used to integrate with any executor that supports spawning futures.
|
||||
///
|
||||
/// All methods can be called recursively.
|
||||
pub trait CustomExecutor {
|
||||
/// Spawns a future, usually on a thread pool.
|
||||
fn spawn(&self, fut: PinnedFuture<()>);
|
||||
/// Spawns a local future. May require calling `poll_local` to make progress.
|
||||
fn spawn_local(&self, fut: PinnedLocalFuture<()>);
|
||||
/// Polls the executor, if it supports polling.
|
||||
fn poll_local(&self);
|
||||
}
|
||||
|
||||
55
any_spawner/tests/custom_runtime.rs
Normal file
55
any_spawner/tests/custom_runtime.rs
Normal file
@@ -0,0 +1,55 @@
|
||||
#[cfg(feature = "futures-executor")]
|
||||
use any_spawner::{CustomExecutor, Executor, PinnedFuture, PinnedLocalFuture};
|
||||
#[cfg(feature = "futures-executor")]
|
||||
#[test]
|
||||
fn can_create_custom_executor() {
|
||||
use futures::{
|
||||
executor::{LocalPool, LocalSpawner},
|
||||
task::LocalSpawnExt,
|
||||
};
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
sync::{
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
Arc,
|
||||
},
|
||||
};
|
||||
|
||||
thread_local! {
|
||||
static LOCAL_POOL: RefCell<LocalPool> = RefCell::new(LocalPool::new());
|
||||
static SPAWNER: LocalSpawner = LOCAL_POOL.with(|pool| pool.borrow().spawner());
|
||||
}
|
||||
|
||||
struct CustomFutureExecutor;
|
||||
impl CustomExecutor for CustomFutureExecutor {
|
||||
fn spawn(&self, _fut: PinnedFuture<()>) {
|
||||
panic!("not supported in this test");
|
||||
}
|
||||
|
||||
fn spawn_local(&self, fut: PinnedLocalFuture<()>) {
|
||||
SPAWNER.with(|spawner| {
|
||||
spawner.spawn_local(fut).expect("failed to spawn future");
|
||||
});
|
||||
}
|
||||
|
||||
fn poll_local(&self) {
|
||||
LOCAL_POOL.with(|pool| {
|
||||
if let Ok(mut pool) = pool.try_borrow_mut() {
|
||||
pool.run_until_stalled();
|
||||
}
|
||||
// If we couldn't borrow_mut, we're in a nested call to poll, so we don't need to do anything.
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Executor::init_custom_executor(CustomFutureExecutor)
|
||||
.expect("couldn't set executor");
|
||||
|
||||
let counter = Arc::new(AtomicUsize::new(0));
|
||||
let counter_clone = Arc::clone(&counter);
|
||||
Executor::spawn_local(async move {
|
||||
counter_clone.store(1, Ordering::Release);
|
||||
});
|
||||
Executor::poll_local();
|
||||
assert_eq!(counter.load(Ordering::Acquire), 1);
|
||||
}
|
||||
38
any_spawner/tests/futures_runtime.rs
Normal file
38
any_spawner/tests/futures_runtime.rs
Normal file
@@ -0,0 +1,38 @@
|
||||
#[cfg(feature = "futures-executor")]
|
||||
use any_spawner::Executor;
|
||||
// All tests in this file use the same executor.
|
||||
|
||||
#[cfg(feature = "futures-executor")]
|
||||
#[test]
|
||||
fn can_spawn_local_future() {
|
||||
use std::rc::Rc;
|
||||
|
||||
let _ = Executor::init_futures_executor();
|
||||
let rc = Rc::new(());
|
||||
Executor::spawn_local(async {
|
||||
_ = rc;
|
||||
});
|
||||
Executor::spawn(async {});
|
||||
}
|
||||
|
||||
#[cfg(feature = "futures-executor")]
|
||||
#[test]
|
||||
fn can_make_local_progress() {
|
||||
use std::sync::{
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
Arc,
|
||||
};
|
||||
let _ = Executor::init_futures_executor();
|
||||
let counter = Arc::new(AtomicUsize::new(0));
|
||||
Executor::spawn_local({
|
||||
let counter = Arc::clone(&counter);
|
||||
async move {
|
||||
assert_eq!(counter.fetch_add(1, Ordering::AcqRel), 0);
|
||||
Executor::spawn_local(async {
|
||||
// Should not crash
|
||||
});
|
||||
}
|
||||
});
|
||||
Executor::poll_local();
|
||||
assert_eq!(counter.load(Ordering::Acquire), 1);
|
||||
}
|
||||
@@ -2,7 +2,8 @@
|
||||
name = "benchmarks"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
rust-version.workspace = true
|
||||
# std::sync::LazyLock is stabilized in Rust version 1.80.0
|
||||
rust-version = "1.80.0"
|
||||
|
||||
[dependencies]
|
||||
l0410 = { package = "leptos", version = "0.4.10", features = [
|
||||
@@ -22,7 +23,7 @@ tokio-test = "0.4.0"
|
||||
miniserde = "0.1.0"
|
||||
gloo = "0.8.0"
|
||||
uuid = { version = "1.0", features = ["serde", "v4", "wasm-bindgen"] }
|
||||
wasm-bindgen = "0.2.0"
|
||||
wasm-bindgen = "0.2.100"
|
||||
lazy_static = "1.0"
|
||||
log = "0.4.0"
|
||||
strum = "0.24.0"
|
||||
|
||||
@@ -18,7 +18,7 @@ fn leptos_ssr_bench(b: &mut Bencher) {
|
||||
}
|
||||
}
|
||||
|
||||
let rendered = view! {
|
||||
let rendered = view! {
|
||||
<main>
|
||||
<h1>"Welcome to our benchmark page."</h1>
|
||||
<p>"Here's some introductory text."</p>
|
||||
@@ -58,7 +58,7 @@ fn tachys_ssr_bench(b: &mut Bencher) {
|
||||
}
|
||||
}
|
||||
|
||||
let rendered = view! {
|
||||
let rendered = view! {
|
||||
<main>
|
||||
<h1>"Welcome to our benchmark page."</h1>
|
||||
<p>"Here's some introductory text."</p>
|
||||
@@ -92,13 +92,13 @@ fn tera_ssr_bench(b: &mut Bencher) {
|
||||
{% endfor %}
|
||||
</main>"#;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref TERA: Tera = {
|
||||
let mut tera = Tera::default();
|
||||
tera.add_raw_templates(vec![("template.html", TEMPLATE)]).unwrap();
|
||||
tera
|
||||
};
|
||||
}
|
||||
|
||||
static LazyCell<TERA>: Tera = LazyLock::new(|| {
|
||||
let mut tera = Tera::default();
|
||||
tera.add_raw_templates(vec![("template.html", TEMPLATE)]).unwrap();
|
||||
tera
|
||||
});
|
||||
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct Counter {
|
||||
|
||||
@@ -55,7 +55,7 @@ static TEMPLATE: &str = r#"<main>
|
||||
{% else %}
|
||||
<li><a href="/">All</a></li>
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% if mode_active %}
|
||||
<li><a href="/active" class="selected">Active</a></li>
|
||||
{% else %}
|
||||
@@ -91,13 +91,13 @@ fn tera_todomvc_ssr(b: &mut Bencher) {
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tera::*;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref TERA: Tera = {
|
||||
|
||||
static LazyLock<TERA>: Tera = LazyLock( || {
|
||||
let mut tera = Tera::default();
|
||||
tera.add_raw_templates(vec![("template.html", TEMPLATE)]).unwrap();
|
||||
tera
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct Todo {
|
||||
@@ -131,13 +131,13 @@ fn tera_todomvc_ssr_1000(b: &mut Bencher) {
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tera::*;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref TERA: Tera = {
|
||||
let mut tera = Tera::default();
|
||||
tera.add_raw_templates(vec![("template.html", TEMPLATE)]).unwrap();
|
||||
tera
|
||||
};
|
||||
}
|
||||
|
||||
static TERA: LazyLock<Tera> = LazyLock::new(|| {
|
||||
let mut tera = Tera::default();
|
||||
tera.add_raw_templates(vec![("template.html", TEMPLATE)]).unwrap();
|
||||
tera
|
||||
});
|
||||
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct Todo {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "either_of"
|
||||
version = "0.1.0"
|
||||
version = "0.1.5"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
@@ -10,4 +10,9 @@ rust-version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
pin-project-lite = "0.2.14"
|
||||
pin-project-lite = "0.2.16"
|
||||
paste = "1.0.15"
|
||||
|
||||
[features]
|
||||
default = ["no_std"]
|
||||
no_std = []
|
||||
|
||||
@@ -1,135 +1,907 @@
|
||||
#![no_std]
|
||||
#![cfg_attr(feature = "no_std", no_std)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
//! Utilities for working with enumerated types that contain one of `2..n` other types.
|
||||
|
||||
use core::{
|
||||
cmp::Ordering,
|
||||
fmt::Display,
|
||||
future::Future,
|
||||
iter::{Product, Sum},
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
use paste::paste;
|
||||
use pin_project_lite::pin_project;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Either<A, B> {
|
||||
Left(A),
|
||||
Right(B),
|
||||
}
|
||||
|
||||
impl<Item, A, B> Iterator for Either<A, B>
|
||||
where
|
||||
A: Iterator<Item = Item>,
|
||||
B: Iterator<Item = Item>,
|
||||
{
|
||||
type Item = Item;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
match self {
|
||||
Either::Left(i) => i.next(),
|
||||
Either::Right(i) => i.next(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pin_project! {
|
||||
#[project = EitherFutureProj]
|
||||
pub enum EitherFuture<A, B> {
|
||||
Left { #[pin] inner: A },
|
||||
Right { #[pin] inner: B },
|
||||
}
|
||||
}
|
||||
|
||||
impl<A, B> Future for EitherFuture<A, B>
|
||||
where
|
||||
A: Future,
|
||||
B: Future,
|
||||
{
|
||||
type Output = Either<A::Output, B::Output>;
|
||||
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
let this = self.project();
|
||||
match this {
|
||||
EitherFutureProj::Left { inner } => match inner.poll(cx) {
|
||||
Poll::Pending => Poll::Pending,
|
||||
Poll::Ready(inner) => Poll::Ready(Either::Left(inner)),
|
||||
},
|
||||
EitherFutureProj::Right { inner } => match inner.poll(cx) {
|
||||
Poll::Pending => Poll::Pending,
|
||||
Poll::Ready(inner) => Poll::Ready(Either::Right(inner)),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(not(feature = "no_std"))]
|
||||
use std::error::Error; // TODO: replace with core::error::Error once MSRV is >= 1.81.0
|
||||
|
||||
macro_rules! tuples {
|
||||
($name:ident + $fut_name:ident + $fut_proj:ident => $($ty:ident),*) => {
|
||||
($name:ident + $fut_name:ident + $fut_proj:ident {
|
||||
$($ty:ident => ($($rest_variant:ident),*) + <$($mapped_ty:ident),+>),+$(,)?
|
||||
}) => {
|
||||
tuples!($name + $fut_name + $fut_proj {
|
||||
$($ty($ty) => ($($rest_variant),*) + <$($mapped_ty),+>),+
|
||||
});
|
||||
};
|
||||
($name:ident + $fut_name:ident + $fut_proj:ident {
|
||||
$($variant:ident($ty:ident) => ($($rest_variant:ident),*) + <$($mapped_ty:ident),+>),+$(,)?
|
||||
}) => {
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
|
||||
pub enum $name<$($ty,)*> {
|
||||
$($ty ($ty),)*
|
||||
pub enum $name<$($ty),+> {
|
||||
$($variant ($ty),)+
|
||||
}
|
||||
|
||||
impl<$($ty,)*> Display for $name<$($ty,)*>
|
||||
impl<$($ty),+> $name<$($ty),+> {
|
||||
paste! {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn map<$([<F $ty>]),+, $([<$ty 1>]),+>(self, $([<$variant:lower>]: [<F $ty>]),+) -> $name<$([<$ty 1>]),+>
|
||||
where
|
||||
$([<F $ty>]: FnOnce($ty) -> [<$ty 1>],)+
|
||||
{
|
||||
match self {
|
||||
$($name::$variant(inner) => $name::$variant([<$variant:lower>](inner)),)+
|
||||
}
|
||||
}
|
||||
|
||||
$(
|
||||
pub fn [<map_ $variant:lower>]<Fun, [<$ty 1>]>(self, f: Fun) -> $name<$($mapped_ty),+>
|
||||
where
|
||||
Fun: FnOnce($ty) -> [<$ty 1>],
|
||||
{
|
||||
match self {
|
||||
$name::$variant(inner) => $name::$variant(f(inner)),
|
||||
$($name::$rest_variant(inner) => $name::$rest_variant(inner),)*
|
||||
}
|
||||
}
|
||||
|
||||
pub fn [<inspect_ $variant:lower>]<Fun, [<$ty 1>]>(self, f: Fun) -> Self
|
||||
where
|
||||
Fun: FnOnce(&$ty),
|
||||
{
|
||||
if let $name::$variant(inner) = &self {
|
||||
f(inner);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn [<is_ $variant:lower>](&self) -> bool {
|
||||
matches!(self, $name::$variant(_))
|
||||
}
|
||||
|
||||
pub fn [<as_ $variant:lower>](&self) -> Option<&$ty> {
|
||||
match self {
|
||||
$name::$variant(inner) => Some(inner),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn [<as_ $variant:lower _mut>](&mut self) -> Option<&mut $ty> {
|
||||
match self {
|
||||
$name::$variant(inner) => Some(inner),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn [<unwrap_ $variant:lower>](self) -> $ty {
|
||||
match self {
|
||||
$name::$variant(inner) => inner,
|
||||
_ => panic!(concat!(
|
||||
"called `unwrap_", stringify!([<$variant:lower>]), "()` on a non-`", stringify!($variant), "` variant of `", stringify!($name), "`"
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn [<into_ $variant:lower>](self) -> Result<$ty, Self> {
|
||||
match self {
|
||||
$name::$variant(inner) => Ok(inner),
|
||||
_ => Err(self),
|
||||
}
|
||||
}
|
||||
)+
|
||||
}
|
||||
}
|
||||
|
||||
impl<$($ty),+> Display for $name<$($ty),+>
|
||||
where
|
||||
$($ty: Display,)*
|
||||
$($ty: Display,)+
|
||||
{
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self {
|
||||
$($name::$ty(this) => this.fmt(f),)*
|
||||
$($name::$variant(this) => this.fmt(f),)+
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Item, $($ty,)*> Iterator for $name<$($ty,)*>
|
||||
#[cfg(not(feature = "no_std"))]
|
||||
impl<$($ty),+> Error for $name<$($ty),+>
|
||||
where
|
||||
$($ty: Iterator<Item = Item>,)*
|
||||
$($ty: Error,)+
|
||||
{
|
||||
fn source(&self) -> Option<&(dyn Error + 'static)> {
|
||||
match self {
|
||||
$($name::$variant(this) => this.source(),)+
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Item, $($ty),+> Iterator for $name<$($ty),+>
|
||||
where
|
||||
$($ty: Iterator<Item = Item>,)+
|
||||
{
|
||||
type Item = Item;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
match self {
|
||||
$($name::$ty(i) => i.next(),)*
|
||||
$($name::$variant(i) => i.next(),)+
|
||||
}
|
||||
}
|
||||
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
match self {
|
||||
$($name::$variant(i) => i.size_hint(),)+
|
||||
}
|
||||
}
|
||||
|
||||
fn count(self) -> usize
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
match self {
|
||||
$($name::$variant(i) => i.count(),)+
|
||||
}
|
||||
}
|
||||
|
||||
fn last(self) -> Option<Self::Item>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
match self {
|
||||
$($name::$variant(i) => i.last(),)+
|
||||
}
|
||||
}
|
||||
|
||||
fn nth(&mut self, n: usize) -> Option<Self::Item> {
|
||||
match self {
|
||||
$($name::$variant(i) => i.nth(n),)+
|
||||
}
|
||||
}
|
||||
|
||||
fn for_each<Fun>(self, f: Fun)
|
||||
where
|
||||
Self: Sized,
|
||||
Fun: FnMut(Self::Item),
|
||||
{
|
||||
match self {
|
||||
$($name::$variant(i) => i.for_each(f),)+
|
||||
}
|
||||
}
|
||||
|
||||
fn collect<Col: FromIterator<Self::Item>>(self) -> Col
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
match self {
|
||||
$($name::$variant(i) => i.collect(),)+
|
||||
}
|
||||
}
|
||||
|
||||
fn partition<Col, Fun>(self, f: Fun) -> (Col, Col)
|
||||
where
|
||||
Self: Sized,
|
||||
Col: Default + Extend<Self::Item>,
|
||||
Fun: FnMut(&Self::Item) -> bool,
|
||||
{
|
||||
match self {
|
||||
$($name::$variant(i) => i.partition(f),)+
|
||||
}
|
||||
}
|
||||
|
||||
fn fold<Acc, Fun>(self, init: Acc, f: Fun) -> Acc
|
||||
where
|
||||
Self: Sized,
|
||||
Fun: FnMut(Acc, Self::Item) -> Acc,
|
||||
{
|
||||
match self {
|
||||
$($name::$variant(i) => i.fold(init, f),)+
|
||||
}
|
||||
}
|
||||
|
||||
fn reduce<Fun>(self, f: Fun) -> Option<Self::Item>
|
||||
where
|
||||
Self: Sized,
|
||||
Fun: FnMut(Self::Item, Self::Item) -> Self::Item,
|
||||
{
|
||||
match self {
|
||||
$($name::$variant(i) => i.reduce(f),)+
|
||||
}
|
||||
}
|
||||
|
||||
fn all<Fun>(&mut self, f: Fun) -> bool
|
||||
where
|
||||
Self: Sized,
|
||||
Fun: FnMut(Self::Item) -> bool,
|
||||
{
|
||||
match self {
|
||||
$($name::$variant(i) => i.all(f),)+
|
||||
}
|
||||
}
|
||||
|
||||
fn any<Fun>(&mut self, f: Fun) -> bool
|
||||
where
|
||||
Self: Sized,
|
||||
Fun: FnMut(Self::Item) -> bool,
|
||||
{
|
||||
match self {
|
||||
$($name::$variant(i) => i.any(f),)+
|
||||
}
|
||||
}
|
||||
|
||||
fn find<Pre>(&mut self, predicate: Pre) -> Option<Self::Item>
|
||||
where
|
||||
Self: Sized,
|
||||
Pre: FnMut(&Self::Item) -> bool,
|
||||
{
|
||||
match self {
|
||||
$($name::$variant(i) => i.find(predicate),)+
|
||||
}
|
||||
}
|
||||
|
||||
fn find_map<Out, Fun>(&mut self, f: Fun) -> Option<Out>
|
||||
where
|
||||
Self: Sized,
|
||||
Fun: FnMut(Self::Item) -> Option<Out>,
|
||||
{
|
||||
match self {
|
||||
$($name::$variant(i) => i.find_map(f),)+
|
||||
}
|
||||
}
|
||||
|
||||
fn position<Pre>(&mut self, predicate: Pre) -> Option<usize>
|
||||
where
|
||||
Self: Sized,
|
||||
Pre: FnMut(Self::Item) -> bool,
|
||||
{
|
||||
match self {
|
||||
$($name::$variant(i) => i.position(predicate),)+
|
||||
}
|
||||
}
|
||||
|
||||
fn max(self) -> Option<Self::Item>
|
||||
where
|
||||
Self: Sized,
|
||||
Self::Item: Ord,
|
||||
{
|
||||
match self {
|
||||
$($name::$variant(i) => i.max(),)+
|
||||
}
|
||||
}
|
||||
|
||||
fn min(self) -> Option<Self::Item>
|
||||
where
|
||||
Self: Sized,
|
||||
Self::Item: Ord,
|
||||
{
|
||||
match self {
|
||||
$($name::$variant(i) => i.min(),)+
|
||||
}
|
||||
}
|
||||
|
||||
fn max_by_key<Key: Ord, Fun>(self, f: Fun) -> Option<Self::Item>
|
||||
where
|
||||
Self: Sized,
|
||||
Fun: FnMut(&Self::Item) -> Key,
|
||||
{
|
||||
match self {
|
||||
$($name::$variant(i) => i.max_by_key(f),)+
|
||||
}
|
||||
}
|
||||
|
||||
fn max_by<Cmp>(self, compare: Cmp) -> Option<Self::Item>
|
||||
where
|
||||
Self: Sized,
|
||||
Cmp: FnMut(&Self::Item, &Self::Item) -> Ordering,
|
||||
{
|
||||
match self {
|
||||
$($name::$variant(i) => i.max_by(compare),)+
|
||||
}
|
||||
}
|
||||
|
||||
fn min_by_key<Key: Ord, Fun>(self, f: Fun) -> Option<Self::Item>
|
||||
where
|
||||
Self: Sized,
|
||||
Fun: FnMut(&Self::Item) -> Key,
|
||||
{
|
||||
match self {
|
||||
$($name::$variant(i) => i.min_by_key(f),)+
|
||||
}
|
||||
}
|
||||
|
||||
fn min_by<Cmp>(self, compare: Cmp) -> Option<Self::Item>
|
||||
where
|
||||
Self: Sized,
|
||||
Cmp: FnMut(&Self::Item, &Self::Item) -> Ordering,
|
||||
{
|
||||
match self {
|
||||
$($name::$variant(i) => i.min_by(compare),)+
|
||||
}
|
||||
}
|
||||
|
||||
fn sum<Out>(self) -> Out
|
||||
where
|
||||
Self: Sized,
|
||||
Out: Sum<Self::Item>,
|
||||
{
|
||||
match self {
|
||||
$($name::$variant(i) => i.sum(),)+
|
||||
}
|
||||
}
|
||||
|
||||
fn product<Out>(self) -> Out
|
||||
where
|
||||
Self: Sized,
|
||||
Out: Product<Self::Item>,
|
||||
{
|
||||
match self {
|
||||
$($name::$variant(i) => i.product(),)+
|
||||
}
|
||||
}
|
||||
|
||||
fn cmp<Other>(self, other: Other) -> Ordering
|
||||
where
|
||||
Other: IntoIterator<Item = Self::Item>,
|
||||
Self::Item: Ord,
|
||||
Self: Sized,
|
||||
{
|
||||
match self {
|
||||
$($name::$variant(i) => i.cmp(other),)+
|
||||
}
|
||||
}
|
||||
|
||||
fn partial_cmp<Other>(self, other: Other) -> Option<Ordering>
|
||||
where
|
||||
Other: IntoIterator,
|
||||
Self::Item: PartialOrd<Other::Item>,
|
||||
Self: Sized,
|
||||
{
|
||||
match self {
|
||||
$($name::$variant(i) => i.partial_cmp(other),)+
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: uncomment once MSRV is >= 1.82.0
|
||||
// fn is_sorted(self) -> bool
|
||||
// where
|
||||
// Self: Sized,
|
||||
// Self::Item: PartialOrd,
|
||||
// {
|
||||
// match self {
|
||||
// $($name::$variant(i) => i.is_sorted(),)+
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// fn is_sorted_by<Cmp>(self, compare: Cmp) -> bool
|
||||
// where
|
||||
// Self: Sized,
|
||||
// Cmp: FnMut(&Self::Item, &Self::Item) -> bool,
|
||||
// {
|
||||
// match self {
|
||||
// $($name::$variant(i) => i.is_sorted_by(compare),)+
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// fn is_sorted_by_key<Fun, Key>(self, f: Fun) -> bool
|
||||
// where
|
||||
// Self: Sized,
|
||||
// Fun: FnMut(Self::Item) -> Key,
|
||||
// Key: PartialOrd,
|
||||
// {
|
||||
// match self {
|
||||
// $($name::$variant(i) => i.is_sorted_by_key(f),)+
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
impl<Item, $($ty),+> ExactSizeIterator for $name<$($ty),+>
|
||||
where
|
||||
$($ty: ExactSizeIterator<Item = Item>,)+
|
||||
{
|
||||
fn len(&self) -> usize {
|
||||
match self {
|
||||
$($name::$variant(i) => i.len(),)+
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Item, $($ty),+> DoubleEndedIterator for $name<$($ty),+>
|
||||
where
|
||||
$($ty: DoubleEndedIterator<Item = Item>,)+
|
||||
{
|
||||
fn next_back(&mut self) -> Option<Self::Item> {
|
||||
match self {
|
||||
$($name::$variant(i) => i.next_back(),)+
|
||||
}
|
||||
}
|
||||
|
||||
fn nth_back(&mut self, n: usize) -> Option<Self::Item> {
|
||||
match self {
|
||||
$($name::$variant(i) => i.nth_back(n),)+
|
||||
}
|
||||
}
|
||||
|
||||
fn rfind<Pre>(&mut self, predicate: Pre) -> Option<Self::Item>
|
||||
where
|
||||
Pre: FnMut(&Self::Item) -> bool,
|
||||
{
|
||||
match self {
|
||||
$($name::$variant(i) => i.rfind(predicate),)+
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pin_project! {
|
||||
#[project = $fut_proj]
|
||||
pub enum $fut_name<$($ty,)*> {
|
||||
$($ty { #[pin] inner: $ty },)*
|
||||
pub enum $fut_name<$($ty),+> {
|
||||
$($variant { #[pin] inner: $ty },)+
|
||||
}
|
||||
}
|
||||
|
||||
impl<$($ty,)*> Future for $fut_name<$($ty,)*>
|
||||
impl<$($ty),+> Future for $fut_name<$($ty),+>
|
||||
where
|
||||
$($ty: Future,)*
|
||||
$($ty: Future,)+
|
||||
{
|
||||
type Output = $name<$($ty::Output,)*>;
|
||||
type Output = $name<$($ty::Output),+>;
|
||||
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
let this = self.project();
|
||||
match this {
|
||||
$($fut_proj::$ty { inner } => match inner.poll(cx) {
|
||||
$($fut_proj::$variant { inner } => match inner.poll(cx) {
|
||||
Poll::Pending => Poll::Pending,
|
||||
Poll::Ready(inner) => Poll::Ready($name::$ty(inner)),
|
||||
},)*
|
||||
Poll::Ready(inner) => Poll::Ready($name::$variant(inner)),
|
||||
},)+
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tuples!(EitherOf3 + EitherOf3Future + EitherOf3FutureProj => A, B, C);
|
||||
tuples!(EitherOf4 + EitherOf4Future + EitherOf4FutureProj => A, B, C, D);
|
||||
tuples!(EitherOf5 + EitherOf5Future + EitherOf5FutureProj => A, B, C, D, E);
|
||||
tuples!(EitherOf6 + EitherOf6Future + EitherOf6FutureProj => A, B, C, D, E, F);
|
||||
tuples!(EitherOf7 + EitherOf7Future + EitherOf7FutureProj => A, B, C, D, E, F, G);
|
||||
tuples!(EitherOf8 + EitherOf8Future + EitherOf8FutureProj => A, B, C, D, E, F, G, H);
|
||||
tuples!(EitherOf9 + EitherOf9Future + EitherOf9FutureProj => A, B, C, D, E, F, G, H, I);
|
||||
tuples!(EitherOf10 + EitherOf10Future + EitherOf10FutureProj => A, B, C, D, E, F, G, H, I, J);
|
||||
tuples!(EitherOf11 + EitherOf11Future + EitherOf11FutureProj => A, B, C, D, E, F, G, H, I, J, K);
|
||||
tuples!(EitherOf12 + EitherOf12Future + EitherOf12FutureProj => A, B, C, D, E, F, G, H, I, J, K, L);
|
||||
tuples!(EitherOf13 + EitherOf13Future + EitherOf13FutureProj => A, B, C, D, E, F, G, H, I, J, K, L, M);
|
||||
tuples!(EitherOf14 + EitherOf14Future + EitherOf14FutureProj => A, B, C, D, E, F, G, H, I, J, K, L, M, N);
|
||||
tuples!(EitherOf15 + EitherOf15Future + EitherOf15FutureProj => A, B, C, D, E, F, G, H, I, J, K, L, M, N, O);
|
||||
tuples!(EitherOf16 + EitherOf16Future + EitherOf16FutureProj => A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P);
|
||||
tuples!(Either + EitherFuture + EitherFutureProj {
|
||||
Left(A) => (Right) + <A1, B>,
|
||||
Right(B) => (Left) + <A, B1>,
|
||||
});
|
||||
|
||||
impl<A, B> Either<A, B> {
|
||||
pub fn swap(self) -> Either<B, A> {
|
||||
match self {
|
||||
Either::Left(a) => Either::Right(a),
|
||||
Either::Right(b) => Either::Left(b),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<A, B> From<Result<A, B>> for Either<A, B> {
|
||||
fn from(value: Result<A, B>) -> Self {
|
||||
match value {
|
||||
Ok(left) => Either::Left(left),
|
||||
Err(right) => Either::Right(right),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait EitherOr {
|
||||
type Left;
|
||||
type Right;
|
||||
fn either_or<FA, A, FB, B>(self, a: FA, b: FB) -> Either<A, B>
|
||||
where
|
||||
FA: FnOnce(Self::Left) -> A,
|
||||
FB: FnOnce(Self::Right) -> B;
|
||||
}
|
||||
|
||||
impl EitherOr for bool {
|
||||
type Left = ();
|
||||
type Right = ();
|
||||
|
||||
fn either_or<FA, A, FB, B>(self, a: FA, b: FB) -> Either<A, B>
|
||||
where
|
||||
FA: FnOnce(Self::Left) -> A,
|
||||
FB: FnOnce(Self::Right) -> B,
|
||||
{
|
||||
if self {
|
||||
Either::Left(a(()))
|
||||
} else {
|
||||
Either::Right(b(()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> EitherOr for Option<T> {
|
||||
type Left = T;
|
||||
type Right = ();
|
||||
|
||||
fn either_or<FA, A, FB, B>(self, a: FA, b: FB) -> Either<A, B>
|
||||
where
|
||||
FA: FnOnce(Self::Left) -> A,
|
||||
FB: FnOnce(Self::Right) -> B,
|
||||
{
|
||||
match self {
|
||||
Some(t) => Either::Left(a(t)),
|
||||
None => Either::Right(b(())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, E> EitherOr for Result<T, E> {
|
||||
type Left = T;
|
||||
type Right = E;
|
||||
|
||||
fn either_or<FA, A, FB, B>(self, a: FA, b: FB) -> Either<A, B>
|
||||
where
|
||||
FA: FnOnce(Self::Left) -> A,
|
||||
FB: FnOnce(Self::Right) -> B,
|
||||
{
|
||||
match self {
|
||||
Ok(t) => Either::Left(a(t)),
|
||||
Err(err) => Either::Right(b(err)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<A, B> EitherOr for Either<A, B> {
|
||||
type Left = A;
|
||||
type Right = B;
|
||||
|
||||
#[inline]
|
||||
fn either_or<FA, A1, FB, B1>(self, a: FA, b: FB) -> Either<A1, B1>
|
||||
where
|
||||
FA: FnOnce(<Self as EitherOr>::Left) -> A1,
|
||||
FB: FnOnce(<Self as EitherOr>::Right) -> B1,
|
||||
{
|
||||
self.map(a, b)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_either_or() {
|
||||
let right = false.either_or(|_| 'a', |_| 12);
|
||||
assert!(matches!(right, Either::Right(12)));
|
||||
|
||||
let left = true.either_or(|_| 'a', |_| 12);
|
||||
assert!(matches!(left, Either::Left('a')));
|
||||
|
||||
let left = Some(12).either_or(|a| a, |_| 'a');
|
||||
assert!(matches!(left, Either::Left(12)));
|
||||
let right = None.either_or(|a: i32| a, |_| 'a');
|
||||
assert!(matches!(right, Either::Right('a')));
|
||||
|
||||
let result: Result<_, ()> = Ok(1.2f32);
|
||||
let left = result.either_or(|a| a * 2f32, |b| b);
|
||||
assert!(matches!(left, Either::Left(2.4f32)));
|
||||
|
||||
let result: Result<i32, _> = Err("12");
|
||||
let right = result.either_or(|a| a, |b| b.chars().next());
|
||||
assert!(matches!(right, Either::Right(Some('1'))));
|
||||
|
||||
let either = Either::<i32, char>::Left(12);
|
||||
let left = either.either_or(|a| a, |b| b);
|
||||
assert!(matches!(left, Either::Left(12)));
|
||||
|
||||
let either = Either::<i32, char>::Right('a');
|
||||
let right = either.either_or(|a| a, |b| b);
|
||||
assert!(matches!(right, Either::Right('a')));
|
||||
}
|
||||
|
||||
tuples!(EitherOf3 + EitherOf3Future + EitherOf3FutureProj {
|
||||
A => (B, C) + <A1, B, C>,
|
||||
B => (A, C) + <A, B1, C>,
|
||||
C => (A, B) + <A, B, C1>,
|
||||
});
|
||||
tuples!(EitherOf4 + EitherOf4Future + EitherOf4FutureProj {
|
||||
A => (B, C, D) + <A1, B, C, D>,
|
||||
B => (A, C, D) + <A, B1, C, D>,
|
||||
C => (A, B, D) + <A, B, C1, D>,
|
||||
D => (A, B, C) + <A, B, C, D1>,
|
||||
});
|
||||
tuples!(EitherOf5 + EitherOf5Future + EitherOf5FutureProj {
|
||||
A => (B, C, D, E) + <A1, B, C, D, E>,
|
||||
B => (A, C, D, E) + <A, B1, C, D, E>,
|
||||
C => (A, B, D, E) + <A, B, C1, D, E>,
|
||||
D => (A, B, C, E) + <A, B, C, D1, E>,
|
||||
E => (A, B, C, D) + <A, B, C, D, E1>,
|
||||
});
|
||||
tuples!(EitherOf6 + EitherOf6Future + EitherOf6FutureProj {
|
||||
A => (B, C, D, E, F) + <A1, B, C, D, E, F>,
|
||||
B => (A, C, D, E, F) + <A, B1, C, D, E, F>,
|
||||
C => (A, B, D, E, F) + <A, B, C1, D, E, F>,
|
||||
D => (A, B, C, E, F) + <A, B, C, D1, E, F>,
|
||||
E => (A, B, C, D, F) + <A, B, C, D, E1, F>,
|
||||
F => (A, B, C, D, E) + <A, B, C, D, E, F1>,
|
||||
});
|
||||
tuples!(EitherOf7 + EitherOf7Future + EitherOf7FutureProj {
|
||||
A => (B, C, D, E, F, G) + <A1, B, C, D, E, F, G>,
|
||||
B => (A, C, D, E, F, G) + <A, B1, C, D, E, F, G>,
|
||||
C => (A, B, D, E, F, G) + <A, B, C1, D, E, F, G>,
|
||||
D => (A, B, C, E, F, G) + <A, B, C, D1, E, F, G>,
|
||||
E => (A, B, C, D, F, G) + <A, B, C, D, E1, F, G>,
|
||||
F => (A, B, C, D, E, G) + <A, B, C, D, E, F1, G>,
|
||||
G => (A, B, C, D, E, F) + <A, B, C, D, E, F, G1>,
|
||||
});
|
||||
tuples!(EitherOf8 + EitherOf8Future + EitherOf8FutureProj {
|
||||
A => (B, C, D, E, F, G, H) + <A1, B, C, D, E, F, G, H>,
|
||||
B => (A, C, D, E, F, G, H) + <A, B1, C, D, E, F, G, H>,
|
||||
C => (A, B, D, E, F, G, H) + <A, B, C1, D, E, F, G, H>,
|
||||
D => (A, B, C, E, F, G, H) + <A, B, C, D1, E, F, G, H>,
|
||||
E => (A, B, C, D, F, G, H) + <A, B, C, D, E1, F, G, H>,
|
||||
F => (A, B, C, D, E, G, H) + <A, B, C, D, E, F1, G, H>,
|
||||
G => (A, B, C, D, E, F, H) + <A, B, C, D, E, F, G1, H>,
|
||||
H => (A, B, C, D, E, F, G) + <A, B, C, D, E, F, G, H1>,
|
||||
});
|
||||
tuples!(EitherOf9 + EitherOf9Future + EitherOf9FutureProj {
|
||||
A => (B, C, D, E, F, G, H, I) + <A1, B, C, D, E, F, G, H, I>,
|
||||
B => (A, C, D, E, F, G, H, I) + <A, B1, C, D, E, F, G, H, I>,
|
||||
C => (A, B, D, E, F, G, H, I) + <A, B, C1, D, E, F, G, H, I>,
|
||||
D => (A, B, C, E, F, G, H, I) + <A, B, C, D1, E, F, G, H, I>,
|
||||
E => (A, B, C, D, F, G, H, I) + <A, B, C, D, E1, F, G, H, I>,
|
||||
F => (A, B, C, D, E, G, H, I) + <A, B, C, D, E, F1, G, H, I>,
|
||||
G => (A, B, C, D, E, F, H, I) + <A, B, C, D, E, F, G1, H, I>,
|
||||
H => (A, B, C, D, E, F, G, I) + <A, B, C, D, E, F, G, H1, I>,
|
||||
I => (A, B, C, D, E, F, G, H) + <A, B, C, D, E, F, G, H, I1>,
|
||||
});
|
||||
tuples!(EitherOf10 + EitherOf10Future + EitherOf10FutureProj {
|
||||
A => (B, C, D, E, F, G, H, I, J) + <A1, B, C, D, E, F, G, H, I, J>,
|
||||
B => (A, C, D, E, F, G, H, I, J) + <A, B1, C, D, E, F, G, H, I, J>,
|
||||
C => (A, B, D, E, F, G, H, I, J) + <A, B, C1, D, E, F, G, H, I, J>,
|
||||
D => (A, B, C, E, F, G, H, I, J) + <A, B, C, D1, E, F, G, H, I, J>,
|
||||
E => (A, B, C, D, F, G, H, I, J) + <A, B, C, D, E1, F, G, H, I, J>,
|
||||
F => (A, B, C, D, E, G, H, I, J) + <A, B, C, D, E, F1, G, H, I, J>,
|
||||
G => (A, B, C, D, E, F, H, I, J) + <A, B, C, D, E, F, G1, H, I, J>,
|
||||
H => (A, B, C, D, E, F, G, I, J) + <A, B, C, D, E, F, G, H1, I, J>,
|
||||
I => (A, B, C, D, E, F, G, H, J) + <A, B, C, D, E, F, G, H, I1, J>,
|
||||
J => (A, B, C, D, E, F, G, H, I) + <A, B, C, D, E, F, G, H, I, J1>,
|
||||
});
|
||||
tuples!(EitherOf11 + EitherOf11Future + EitherOf11FutureProj {
|
||||
A => (B, C, D, E, F, G, H, I, J, K) + <A1, B, C, D, E, F, G, H, I, J, K>,
|
||||
B => (A, C, D, E, F, G, H, I, J, K) + <A, B1, C, D, E, F, G, H, I, J, K>,
|
||||
C => (A, B, D, E, F, G, H, I, J, K) + <A, B, C1, D, E, F, G, H, I, J, K>,
|
||||
D => (A, B, C, E, F, G, H, I, J, K) + <A, B, C, D1, E, F, G, H, I, J, K>,
|
||||
E => (A, B, C, D, F, G, H, I, J, K) + <A, B, C, D, E1, F, G, H, I, J, K>,
|
||||
F => (A, B, C, D, E, G, H, I, J, K) + <A, B, C, D, E, F1, G, H, I, J, K>,
|
||||
G => (A, B, C, D, E, F, H, I, J, K) + <A, B, C, D, E, F, G1, H, I, J, K>,
|
||||
H => (A, B, C, D, E, F, G, I, J, K) + <A, B, C, D, E, F, G, H1, I, J, K>,
|
||||
I => (A, B, C, D, E, F, G, H, J, K) + <A, B, C, D, E, F, G, H, I1, J, K>,
|
||||
J => (A, B, C, D, E, F, G, H, I, K) + <A, B, C, D, E, F, G, H, I, J1, K>,
|
||||
K => (A, B, C, D, E, F, G, H, I, J) + <A, B, C, D, E, F, G, H, I, J, K1>,
|
||||
});
|
||||
tuples!(EitherOf12 + EitherOf12Future + EitherOf12FutureProj {
|
||||
A => (B, C, D, E, F, G, H, I, J, K, L) + <A1, B, C, D, E, F, G, H, I, J, K, L>,
|
||||
B => (A, C, D, E, F, G, H, I, J, K, L) + <A, B1, C, D, E, F, G, H, I, J, K, L>,
|
||||
C => (A, B, D, E, F, G, H, I, J, K, L) + <A, B, C1, D, E, F, G, H, I, J, K, L>,
|
||||
D => (A, B, C, E, F, G, H, I, J, K, L) + <A, B, C, D1, E, F, G, H, I, J, K, L>,
|
||||
E => (A, B, C, D, F, G, H, I, J, K, L) + <A, B, C, D, E1, F, G, H, I, J, K, L>,
|
||||
F => (A, B, C, D, E, G, H, I, J, K, L) + <A, B, C, D, E, F1, G, H, I, J, K, L>,
|
||||
G => (A, B, C, D, E, F, H, I, J, K, L) + <A, B, C, D, E, F, G1, H, I, J, K, L>,
|
||||
H => (A, B, C, D, E, F, G, I, J, K, L) + <A, B, C, D, E, F, G, H1, I, J, K, L>,
|
||||
I => (A, B, C, D, E, F, G, H, J, K, L) + <A, B, C, D, E, F, G, H, I1, J, K, L>,
|
||||
J => (A, B, C, D, E, F, G, H, I, K, L) + <A, B, C, D, E, F, G, H, I, J1, K, L>,
|
||||
K => (A, B, C, D, E, F, G, H, I, J, L) + <A, B, C, D, E, F, G, H, I, J, K1, L>,
|
||||
L => (A, B, C, D, E, F, G, H, I, J, K) + <A, B, C, D, E, F, G, H, I, J, K, L1>,
|
||||
});
|
||||
tuples!(EitherOf13 + EitherOf13Future + EitherOf13FutureProj {
|
||||
A => (B, C, D, E, F, G, H, I, J, K, L, M) + <A1, B, C, D, E, F, G, H, I, J, K, L, M>,
|
||||
B => (A, C, D, E, F, G, H, I, J, K, L, M) + <A, B1, C, D, E, F, G, H, I, J, K, L, M>,
|
||||
C => (A, B, D, E, F, G, H, I, J, K, L, M) + <A, B, C1, D, E, F, G, H, I, J, K, L, M>,
|
||||
D => (A, B, C, E, F, G, H, I, J, K, L, M) + <A, B, C, D1, E, F, G, H, I, J, K, L, M>,
|
||||
E => (A, B, C, D, F, G, H, I, J, K, L, M) + <A, B, C, D, E1, F, G, H, I, J, K, L, M>,
|
||||
F => (A, B, C, D, E, G, H, I, J, K, L, M) + <A, B, C, D, E, F1, G, H, I, J, K, L, M>,
|
||||
G => (A, B, C, D, E, F, H, I, J, K, L, M) + <A, B, C, D, E, F, G1, H, I, J, K, L, M>,
|
||||
H => (A, B, C, D, E, F, G, I, J, K, L, M) + <A, B, C, D, E, F, G, H1, I, J, K, L, M>,
|
||||
I => (A, B, C, D, E, F, G, H, J, K, L, M) + <A, B, C, D, E, F, G, H, I1, J, K, L, M>,
|
||||
J => (A, B, C, D, E, F, G, H, I, K, L, M) + <A, B, C, D, E, F, G, H, I, J1, K, L, M>,
|
||||
K => (A, B, C, D, E, F, G, H, I, J, L, M) + <A, B, C, D, E, F, G, H, I, J, K1, L, M>,
|
||||
L => (A, B, C, D, E, F, G, H, I, J, K, M) + <A, B, C, D, E, F, G, H, I, J, K, L1, M>,
|
||||
M => (A, B, C, D, E, F, G, H, I, J, K, L) + <A, B, C, D, E, F, G, H, I, J, K, L, M1>,
|
||||
});
|
||||
tuples!(EitherOf14 + EitherOf14Future + EitherOf14FutureProj {
|
||||
A => (B, C, D, E, F, G, H, I, J, K, L, M, N) + <A1, B, C, D, E, F, G, H, I, J, K, L, M, N>,
|
||||
B => (A, C, D, E, F, G, H, I, J, K, L, M, N) + <A, B1, C, D, E, F, G, H, I, J, K, L, M, N>,
|
||||
C => (A, B, D, E, F, G, H, I, J, K, L, M, N) + <A, B, C1, D, E, F, G, H, I, J, K, L, M, N>,
|
||||
D => (A, B, C, E, F, G, H, I, J, K, L, M, N) + <A, B, C, D1, E, F, G, H, I, J, K, L, M, N>,
|
||||
E => (A, B, C, D, F, G, H, I, J, K, L, M, N) + <A, B, C, D, E1, F, G, H, I, J, K, L, M, N>,
|
||||
F => (A, B, C, D, E, G, H, I, J, K, L, M, N) + <A, B, C, D, E, F1, G, H, I, J, K, L, M, N>,
|
||||
G => (A, B, C, D, E, F, H, I, J, K, L, M, N) + <A, B, C, D, E, F, G1, H, I, J, K, L, M, N>,
|
||||
H => (A, B, C, D, E, F, G, I, J, K, L, M, N) + <A, B, C, D, E, F, G, H1, I, J, K, L, M, N>,
|
||||
I => (A, B, C, D, E, F, G, H, J, K, L, M, N) + <A, B, C, D, E, F, G, H, I1, J, K, L, M, N>,
|
||||
J => (A, B, C, D, E, F, G, H, I, K, L, M, N) + <A, B, C, D, E, F, G, H, I, J1, K, L, M, N>,
|
||||
K => (A, B, C, D, E, F, G, H, I, J, L, M, N) + <A, B, C, D, E, F, G, H, I, J, K1, L, M, N>,
|
||||
L => (A, B, C, D, E, F, G, H, I, J, K, M, N) + <A, B, C, D, E, F, G, H, I, J, K, L1, M, N>,
|
||||
M => (A, B, C, D, E, F, G, H, I, J, K, L, N) + <A, B, C, D, E, F, G, H, I, J, K, L, M1, N>,
|
||||
N => (A, B, C, D, E, F, G, H, I, J, K, L, M) + <A, B, C, D, E, F, G, H, I, J, K, L, M, N1>,
|
||||
});
|
||||
tuples!(EitherOf15 + EitherOf15Future + EitherOf15FutureProj {
|
||||
A => (B, C, D, E, F, G, H, I, J, K, L, M, N, O) + <A1, B, C, D, E, F, G, H, I, J, K, L, M, N, O>,
|
||||
B => (A, C, D, E, F, G, H, I, J, K, L, M, N, O) + <A, B1, C, D, E, F, G, H, I, J, K, L, M, N, O>,
|
||||
C => (A, B, D, E, F, G, H, I, J, K, L, M, N, O) + <A, B, C1, D, E, F, G, H, I, J, K, L, M, N, O>,
|
||||
D => (A, B, C, E, F, G, H, I, J, K, L, M, N, O) + <A, B, C, D1, E, F, G, H, I, J, K, L, M, N, O>,
|
||||
E => (A, B, C, D, F, G, H, I, J, K, L, M, N, O) + <A, B, C, D, E1, F, G, H, I, J, K, L, M, N, O>,
|
||||
F => (A, B, C, D, E, G, H, I, J, K, L, M, N, O) + <A, B, C, D, E, F1, G, H, I, J, K, L, M, N, O>,
|
||||
G => (A, B, C, D, E, F, H, I, J, K, L, M, N, O) + <A, B, C, D, E, F, G1, H, I, J, K, L, M, N, O>,
|
||||
H => (A, B, C, D, E, F, G, I, J, K, L, M, N, O) + <A, B, C, D, E, F, G, H1, I, J, K, L, M, N, O>,
|
||||
I => (A, B, C, D, E, F, G, H, J, K, L, M, N, O) + <A, B, C, D, E, F, G, H, I1, J, K, L, M, N, O>,
|
||||
J => (A, B, C, D, E, F, G, H, I, K, L, M, N, O) + <A, B, C, D, E, F, G, H, I, J1, K, L, M, N, O>,
|
||||
K => (A, B, C, D, E, F, G, H, I, J, L, M, N, O) + <A, B, C, D, E, F, G, H, I, J, K1, L, M, N, O>,
|
||||
L => (A, B, C, D, E, F, G, H, I, J, K, M, N, O) + <A, B, C, D, E, F, G, H, I, J, K, L1, M, N, O>,
|
||||
M => (A, B, C, D, E, F, G, H, I, J, K, L, N, O) + <A, B, C, D, E, F, G, H, I, J, K, L, M1, N, O>,
|
||||
N => (A, B, C, D, E, F, G, H, I, J, K, L, M, O) + <A, B, C, D, E, F, G, H, I, J, K, L, M, N1, O>,
|
||||
O => (A, B, C, D, E, F, G, H, I, J, K, L, M, N) + <A, B, C, D, E, F, G, H, I, J, K, L, M, N, O1>,
|
||||
});
|
||||
tuples!(EitherOf16 + EitherOf16Future + EitherOf16FutureProj {
|
||||
A => (B, C, D, E, F, G, H, I, J, K, L, M, N, O, P) + <A1, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P>,
|
||||
B => (A, C, D, E, F, G, H, I, J, K, L, M, N, O, P) + <A, B1, C, D, E, F, G, H, I, J, K, L, M, N, O, P>,
|
||||
C => (A, B, D, E, F, G, H, I, J, K, L, M, N, O, P) + <A, B, C1, D, E, F, G, H, I, J, K, L, M, N, O, P>,
|
||||
D => (A, B, C, E, F, G, H, I, J, K, L, M, N, O, P) + <A, B, C, D1, E, F, G, H, I, J, K, L, M, N, O, P>,
|
||||
E => (A, B, C, D, F, G, H, I, J, K, L, M, N, O, P) + <A, B, C, D, E1, F, G, H, I, J, K, L, M, N, O, P>,
|
||||
F => (A, B, C, D, E, G, H, I, J, K, L, M, N, O, P) + <A, B, C, D, E, F1, G, H, I, J, K, L, M, N, O, P>,
|
||||
G => (A, B, C, D, E, F, H, I, J, K, L, M, N, O, P) + <A, B, C, D, E, F, G1, H, I, J, K, L, M, N, O, P>,
|
||||
H => (A, B, C, D, E, F, G, I, J, K, L, M, N, O, P) + <A, B, C, D, E, F, G, H1, I, J, K, L, M, N, O, P>,
|
||||
I => (A, B, C, D, E, F, G, H, J, K, L, M, N, O, P) + <A, B, C, D, E, F, G, H, I1, J, K, L, M, N, O, P>,
|
||||
J => (A, B, C, D, E, F, G, H, I, K, L, M, N, O, P) + <A, B, C, D, E, F, G, H, I, J1, K, L, M, N, O, P>,
|
||||
K => (A, B, C, D, E, F, G, H, I, J, L, M, N, O, P) + <A, B, C, D, E, F, G, H, I, J, K1, L, M, N, O, P>,
|
||||
L => (A, B, C, D, E, F, G, H, I, J, K, M, N, O, P) + <A, B, C, D, E, F, G, H, I, J, K, L1, M, N, O, P>,
|
||||
M => (A, B, C, D, E, F, G, H, I, J, K, L, N, O, P) + <A, B, C, D, E, F, G, H, I, J, K, L, M1, N, O, P>,
|
||||
N => (A, B, C, D, E, F, G, H, I, J, K, L, M, O, P) + <A, B, C, D, E, F, G, H, I, J, K, L, M, N1, O, P>,
|
||||
O => (A, B, C, D, E, F, G, H, I, J, K, L, M, N, P) + <A, B, C, D, E, F, G, H, I, J, K, L, M, N, O1, P>,
|
||||
P => (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O) + <A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P1>,
|
||||
});
|
||||
|
||||
/// Matches over the first expression and returns an either ([`Either`], [`EitherOf3`], ... [`EitherOf8`])
|
||||
/// composed of the values returned by the match arms.
|
||||
///
|
||||
/// The pattern syntax is exactly the same as found in a match arm.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use either_of::*;
|
||||
/// let either2 = either!(Some("hello"),
|
||||
/// Some(s) => s.len(),
|
||||
/// None => 0.0,
|
||||
/// );
|
||||
/// assert!(matches!(either2, Either::<usize, f64>::Left(5)));
|
||||
///
|
||||
/// let either3 = either!(Some("admin"),
|
||||
/// Some("admin") => "hello admin",
|
||||
/// Some(_) => 'x',
|
||||
/// _ => 0,
|
||||
/// );
|
||||
/// assert!(matches!(either3, EitherOf3::<&str, char, i32>::A("hello admin")));
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! either {
|
||||
($match:expr, $left_pattern:pat => $left_expression:expr, $right_pattern:pat => $right_expression:expr,) => {
|
||||
match $match {
|
||||
$left_pattern => $crate::Either::Left($left_expression),
|
||||
$right_pattern => $crate::Either::Right($right_expression),
|
||||
}
|
||||
};
|
||||
($match:expr, $a_pattern:pat => $a_expression:expr, $b_pattern:pat => $b_expression:expr, $c_pattern:pat => $c_expression:expr,) => {
|
||||
match $match {
|
||||
$a_pattern => $crate::EitherOf3::A($a_expression),
|
||||
$b_pattern => $crate::EitherOf3::B($b_expression),
|
||||
$c_pattern => $crate::EitherOf3::C($c_expression),
|
||||
}
|
||||
};
|
||||
($match:expr, $a_pattern:pat => $a_expression:expr, $b_pattern:pat => $b_expression:expr, $c_pattern:pat => $c_expression:expr, $d_pattern:pat => $d_expression:expr,) => {
|
||||
match $match {
|
||||
$a_pattern => $crate::EitherOf4::A($a_expression),
|
||||
$b_pattern => $crate::EitherOf4::B($b_expression),
|
||||
$c_pattern => $crate::EitherOf4::C($c_expression),
|
||||
$d_pattern => $crate::EitherOf4::D($d_expression),
|
||||
}
|
||||
};
|
||||
($match:expr, $a_pattern:pat => $a_expression:expr, $b_pattern:pat => $b_expression:expr, $c_pattern:pat => $c_expression:expr, $d_pattern:pat => $d_expression:expr, $e_pattern:pat => $e_expression:expr,) => {
|
||||
match $match {
|
||||
$a_pattern => $crate::EitherOf5::A($a_expression),
|
||||
$b_pattern => $crate::EitherOf5::B($b_expression),
|
||||
$c_pattern => $crate::EitherOf5::C($c_expression),
|
||||
$d_pattern => $crate::EitherOf5::D($d_expression),
|
||||
$e_pattern => $crate::EitherOf5::E($e_expression),
|
||||
}
|
||||
};
|
||||
($match:expr, $a_pattern:pat => $a_expression:expr, $b_pattern:pat => $b_expression:expr, $c_pattern:pat => $c_expression:expr, $d_pattern:pat => $d_expression:expr, $e_pattern:pat => $e_expression:expr, $f_pattern:pat => $f_expression:expr,) => {
|
||||
match $match {
|
||||
$a_pattern => $crate::EitherOf6::A($a_expression),
|
||||
$b_pattern => $crate::EitherOf6::B($b_expression),
|
||||
$c_pattern => $crate::EitherOf6::C($c_expression),
|
||||
$d_pattern => $crate::EitherOf6::D($d_expression),
|
||||
$e_pattern => $crate::EitherOf6::E($e_expression),
|
||||
$f_pattern => $crate::EitherOf6::F($f_expression),
|
||||
}
|
||||
};
|
||||
($match:expr, $a_pattern:pat => $a_expression:expr, $b_pattern:pat => $b_expression:expr, $c_pattern:pat => $c_expression:expr, $d_pattern:pat => $d_expression:expr, $e_pattern:pat => $e_expression:expr, $f_pattern:pat => $f_expression:expr, $g_pattern:pat => $g_expression:expr,) => {
|
||||
match $match {
|
||||
$a_pattern => $crate::EitherOf7::A($a_expression),
|
||||
$b_pattern => $crate::EitherOf7::B($b_expression),
|
||||
$c_pattern => $crate::EitherOf7::C($c_expression),
|
||||
$d_pattern => $crate::EitherOf7::D($d_expression),
|
||||
$e_pattern => $crate::EitherOf7::E($e_expression),
|
||||
$f_pattern => $crate::EitherOf7::F($f_expression),
|
||||
$g_pattern => $crate::EitherOf7::G($g_expression),
|
||||
}
|
||||
};
|
||||
($match:expr, $a_pattern:pat => $a_expression:expr, $b_pattern:pat => $b_expression:expr, $c_pattern:pat => $c_expression:expr, $d_pattern:pat => $d_expression:expr, $e_pattern:pat => $e_expression:expr, $f_pattern:pat => $f_expression:expr, $g_pattern:pat => $g_expression:expr, $h_pattern:pat => $h_expression:expr,) => {
|
||||
match $match {
|
||||
$a_pattern => $crate::EitherOf8::A($a_expression),
|
||||
$b_pattern => $crate::EitherOf8::B($b_expression),
|
||||
$c_pattern => $crate::EitherOf8::C($c_expression),
|
||||
$d_pattern => $crate::EitherOf8::D($d_expression),
|
||||
$e_pattern => $crate::EitherOf8::E($e_expression),
|
||||
$f_pattern => $crate::EitherOf8::F($f_expression),
|
||||
$g_pattern => $crate::EitherOf8::G($g_expression),
|
||||
$h_pattern => $crate::EitherOf8::H($h_expression),
|
||||
}
|
||||
}; // if you need more eithers feel free to open a PR ;-)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// compile time test
|
||||
#[test]
|
||||
fn either_macro() {
|
||||
let _: Either<&str, f64> = either!(12,
|
||||
12 => "12",
|
||||
_ => 0.0,
|
||||
);
|
||||
let _: EitherOf3<&str, f64, i32> = either!(12,
|
||||
12 => "12",
|
||||
13 => 0.0,
|
||||
_ => 12,
|
||||
);
|
||||
let _: EitherOf4<&str, f64, char, i32> = either!(12,
|
||||
12 => "12",
|
||||
13 => 0.0,
|
||||
14 => ' ',
|
||||
_ => 12,
|
||||
);
|
||||
let _: EitherOf5<&str, f64, char, f32, i32> = either!(12,
|
||||
12 => "12",
|
||||
13 => 0.0,
|
||||
14 => ' ',
|
||||
15 => 0.0f32,
|
||||
_ => 12,
|
||||
);
|
||||
let _: EitherOf6<&str, f64, char, f32, u8, i32> = either!(12,
|
||||
12 => "12",
|
||||
13 => 0.0,
|
||||
14 => ' ',
|
||||
15 => 0.0f32,
|
||||
16 => 24u8,
|
||||
_ => 12,
|
||||
);
|
||||
let _: EitherOf7<&str, f64, char, f32, u8, i8, i32> = either!(12,
|
||||
12 => "12",
|
||||
13 => 0.0,
|
||||
14 => ' ',
|
||||
15 => 0.0f32,
|
||||
16 => 24u8,
|
||||
17 => 2i8,
|
||||
_ => 12,
|
||||
);
|
||||
let _: EitherOf8<&str, f64, char, f32, u8, i8, u32, i32> = either!(12,
|
||||
12 => "12",
|
||||
13 => 0.0,
|
||||
14 => ' ',
|
||||
15 => 0.0f32,
|
||||
16 => 24u8,
|
||||
17 => 2i8,
|
||||
18 => 42u32,
|
||||
_ => 12,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn unwrap_wrong_either() {
|
||||
Either::<i32, &str>::Left(0).unwrap_right();
|
||||
}
|
||||
}
|
||||
|
||||
120
examples/axum_js_ssr/Cargo.toml
Normal file
120
examples/axum_js_ssr/Cargo.toml
Normal file
@@ -0,0 +1,120 @@
|
||||
[package]
|
||||
name = "axum_js_ssr"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
axum = { version = "0.8.1", optional = true }
|
||||
console_error_panic_hook = "0.1.7"
|
||||
console_log = "1.0"
|
||||
gloo-utils = "0.2.0"
|
||||
html-escape = "0.2.13"
|
||||
http-body-util = { version = "0.1.0", optional = true }
|
||||
js-sys = { version = "0.3.69", optional = true }
|
||||
leptos = { path = "../../leptos", features = ["tracing"] }
|
||||
leptos_meta = { path = "../../meta" }
|
||||
leptos_axum = { path = "../../integrations/axum", optional = true }
|
||||
leptos_router = { path = "../../router" }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
thiserror = "1.0"
|
||||
tokio = { version = "1.39", features = [
|
||||
"rt-multi-thread",
|
||||
"macros",
|
||||
"time",
|
||||
], optional = true }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.5.2", features = ["fs"], optional = true }
|
||||
wasm-bindgen = "0.2.92"
|
||||
web-sys = { version = "0.3.69", features = [
|
||||
"AddEventListenerOptions",
|
||||
"Document",
|
||||
"Element",
|
||||
"Event",
|
||||
"EventListener",
|
||||
"EventTarget",
|
||||
"Performance",
|
||||
"Window",
|
||||
], optional = true }
|
||||
|
||||
[features]
|
||||
hydrate = ["leptos/hydrate", "dep:js-sys", "dep:web-sys"]
|
||||
ssr = [
|
||||
"dep:axum",
|
||||
"dep:http-body-util",
|
||||
"dep:tower",
|
||||
"dep:tower-http",
|
||||
"dep:tokio",
|
||||
"leptos/ssr",
|
||||
"leptos_meta/ssr",
|
||||
"dep:leptos_axum",
|
||||
"leptos_router/ssr",
|
||||
]
|
||||
|
||||
[profile.release]
|
||||
panic = "abort"
|
||||
|
||||
[profile.wasm-release]
|
||||
inherits = "release"
|
||||
opt-level = 'z'
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
panic = "abort"
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["axum", "tower", "tower-http", "tokio", "sqlx", "leptos_axum"]
|
||||
skip_feature_sets = [["ssr", "hydrate"]]
|
||||
|
||||
[package.metadata.leptos]
|
||||
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
|
||||
output-name = "axum_js_ssr"
|
||||
# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup.
|
||||
site-root = "target/site"
|
||||
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
|
||||
# Defaults to pkg
|
||||
site-pkg-dir = "pkg"
|
||||
# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css
|
||||
style-file = "style/main.scss"
|
||||
# Assets source dir. All files found here will be copied and synchronized to site-root.
|
||||
# The assets-dir cannot have a sub directory with the same name/path as site-pkg-dir.
|
||||
#
|
||||
# Optional. Env: LEPTOS_ASSETS_DIR.
|
||||
assets-dir = "assets"
|
||||
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
|
||||
site-addr = "127.0.0.1:3000"
|
||||
# The port to use for automatic reload monitoring
|
||||
reload-port = 3001
|
||||
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
|
||||
# [Windows] for non-WSL use "npx.cmd playwright test"
|
||||
# This binary name can be checked in Powershell with Get-Command npx
|
||||
end2end-cmd = "npx playwright test"
|
||||
end2end-dir = "end2end"
|
||||
# The browserlist query used for optimizing the CSS.
|
||||
browserquery = "defaults"
|
||||
# Set by cargo-leptos watch when building with that tool. Controls whether autoreload JS will be included in the head
|
||||
watch = false
|
||||
# The environment Leptos will run in, usually either "DEV" or "PROD"
|
||||
env = "DEV"
|
||||
# The features to use when compiling the bin target
|
||||
#
|
||||
# Optional. Can be over-ridden with the command line parameter --bin-features
|
||||
bin-features = ["ssr"]
|
||||
|
||||
# If the --no-default-features flag should be used when compiling the bin target
|
||||
#
|
||||
# Optional. Defaults to false.
|
||||
bin-default-features = false
|
||||
|
||||
# The features to use when compiling the lib target
|
||||
#
|
||||
# Optional. Can be over-ridden with the command line parameter --lib-features
|
||||
lib-features = ["hydrate"]
|
||||
|
||||
# If the --no-default-features flag should be used when compiling the lib target
|
||||
#
|
||||
# Optional. Defaults to false.
|
||||
lib-default-features = false
|
||||
|
||||
lib-profile-release = "wasm-release"
|
||||
21
examples/axum_js_ssr/LICENSE
Normal file
21
examples/axum_js_ssr/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Tommy Yu
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
8
examples/axum_js_ssr/Makefile.toml
Normal file
8
examples/axum_js_ssr/Makefile.toml
Normal file
@@ -0,0 +1,8 @@
|
||||
extend = [
|
||||
{ path = "../cargo-make/main.toml" },
|
||||
{ path = "../cargo-make/cargo-leptos.toml" },
|
||||
]
|
||||
|
||||
[env]
|
||||
|
||||
CLIENT_PROCESS_NAME = "axum_js_ssr"
|
||||
10
examples/axum_js_ssr/README.md
Normal file
10
examples/axum_js_ssr/README.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# Leptos Axum JS SSR Example
|
||||
|
||||
This example shows the various ways that JavaScript may be included into
|
||||
a Leptos application. The intent is to demonstrate how this may be done
|
||||
and how it may cause the application to fail in an unexpected manner if
|
||||
done incorrectly.
|
||||
|
||||
## Quick Start
|
||||
|
||||
Run `cargo leptos watch` to run this example.
|
||||
BIN
examples/axum_js_ssr/assets/favicon.ico
Normal file
BIN
examples/axum_js_ssr/assets/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
29
examples/axum_js_ssr/node_modules/@highlightjs/cdn-assets/LICENSE
generated
vendored
Normal file
29
examples/axum_js_ssr/node_modules/@highlightjs/cdn-assets/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2006, Ivan Sagalaev.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
* Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
47
examples/axum_js_ssr/node_modules/@highlightjs/cdn-assets/README.md
generated
vendored
Normal file
47
examples/axum_js_ssr/node_modules/@highlightjs/cdn-assets/README.md
generated
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
# Highlight.js CDN Assets
|
||||
|
||||
**Note: this contains only a subset of files from the full package from NPM.**
|
||||
|
||||
[](https://packagephobia.now.sh/result?p=highlight.js)
|
||||
|
||||
**This package contains only the CDN build assets of highlight.js.**
|
||||
|
||||
This may be what you want if you'd like to install the pre-built distributable highlight.js client-side assets via NPM. If you're wanting to use highlight.js mainly on the server-side you likely want the [highlight.js][1] package instead.
|
||||
|
||||
To access these files via CDN:<br>
|
||||
https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@latest/build/
|
||||
|
||||
**If you just want a single .js file with the common languages built-in:
|
||||
<https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@latest/build/highlight.min.js>**
|
||||
|
||||
---
|
||||
|
||||
## Highlight.js
|
||||
|
||||
Highlight.js is a syntax highlighter written in JavaScript. It works in
|
||||
the browser as well as on the server. It works with pretty much any
|
||||
markup, doesn’t depend on any framework, and has automatic language
|
||||
detection.
|
||||
|
||||
If you'd like to read the full README:<br>
|
||||
<https://github.com/highlightjs/highlight.js/blob/main/README.md>
|
||||
|
||||
## License
|
||||
|
||||
Highlight.js is released under the BSD License. See [LICENSE][7] file
|
||||
for details.
|
||||
|
||||
## Links
|
||||
|
||||
The official site for the library is at <https://highlightjs.org/>.
|
||||
|
||||
The Github project may be found at: <https://github.com/highlightjs/highlight.js>
|
||||
|
||||
Further in-depth documentation for the API and other topics is at
|
||||
<http://highlightjs.readthedocs.io/>.
|
||||
|
||||
A list of the Core Team and contributors can be found in the [CONTRIBUTORS.md][8] file.
|
||||
|
||||
[1]: https://www.npmjs.com/package/highlight.js
|
||||
[7]: https://github.com/highlightjs/highlight.js/blob/main/LICENSE
|
||||
[8]: https://github.com/highlightjs/highlight.js/blob/main/CONTRIBUTORS.md
|
||||
1230
examples/axum_js_ssr/node_modules/@highlightjs/cdn-assets/es/highlight.min.js
generated
vendored
Normal file
1230
examples/axum_js_ssr/node_modules/@highlightjs/cdn-assets/es/highlight.min.js
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
1232
examples/axum_js_ssr/node_modules/@highlightjs/cdn-assets/highlight.min.js
generated
vendored
Normal file
1232
examples/axum_js_ssr/node_modules/@highlightjs/cdn-assets/highlight.min.js
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
93
examples/axum_js_ssr/node_modules/@highlightjs/cdn-assets/package.json
generated
vendored
Normal file
93
examples/axum_js_ssr/node_modules/@highlightjs/cdn-assets/package.json
generated
vendored
Normal file
@@ -0,0 +1,93 @@
|
||||
{
|
||||
"name": "@highlightjs/cdn-assets",
|
||||
"description": "Syntax highlighting with language autodetection. (pre-compiled CDN assets)",
|
||||
"keywords": [
|
||||
"highlight",
|
||||
"syntax"
|
||||
],
|
||||
"homepage": "https://highlightjs.org/",
|
||||
"version": "11.10.0",
|
||||
"author": "Josh Goebel <hello@joshgoebel.com>",
|
||||
"contributors": [
|
||||
"Josh Goebel <hello@joshgoebel.com>",
|
||||
"Egor Rogov <e.rogov@postgrespro.ru>",
|
||||
"Vladimir Jimenez <me@allejo.io>",
|
||||
"Ivan Sagalaev <maniac@softwaremaniacs.org>",
|
||||
"Jeremy Hull <sourdrums@gmail.com>",
|
||||
"Oleg Efimov <efimovov@gmail.com>",
|
||||
"Gidi Meir Morris <gidi@gidi.io>",
|
||||
"Jan T. Sott <git@idleberg.com>",
|
||||
"Li Xuanji <xuanji@gmail.com>",
|
||||
"Marcos Cáceres <marcos@marcosc.com>",
|
||||
"Sang Dang <sang.dang@polku.io>"
|
||||
],
|
||||
"bugs": {
|
||||
"url": "https://github.com/highlightjs/highlight.js/issues"
|
||||
},
|
||||
"license": "BSD-3-Clause",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/highlightjs/highlight.js.git"
|
||||
},
|
||||
"sideEffects": [
|
||||
"./es/common.js",
|
||||
"./lib/common.js",
|
||||
"*.css",
|
||||
"*.scss"
|
||||
],
|
||||
"scripts": {
|
||||
"mocha": "mocha",
|
||||
"lint": "eslint src/*.js src/lib/*.js demo/*.js tools/**/*.js --ignore-pattern vendor",
|
||||
"lint-languages": "eslint --no-eslintrc -c .eslintrc.lang.js src/languages/**/*.js",
|
||||
"build_and_test": "npm run build && npm run test",
|
||||
"build_and_test_browser": "npm run build-browser && npm run test-browser",
|
||||
"build": "node ./tools/build.js -t node",
|
||||
"build-cdn": "node ./tools/build.js -t cdn",
|
||||
"build-browser": "node ./tools/build.js -t browser :common",
|
||||
"devtool": "npx http-server",
|
||||
"test": "mocha test",
|
||||
"test-markup": "mocha test/markup",
|
||||
"test-detect": "mocha test/detect",
|
||||
"test-browser": "mocha test/browser",
|
||||
"test-parser": "mocha test/parser"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@colors/colors": "^1.6.0",
|
||||
"@rollup/plugin-commonjs": "^26.0.1",
|
||||
"@rollup/plugin-json": "^6.0.1",
|
||||
"@rollup/plugin-node-resolve": "^15.2.3",
|
||||
"@types/mocha": "^10.0.2",
|
||||
"@typescript-eslint/eslint-plugin": "^7.15.0",
|
||||
"@typescript-eslint/parser": "^7.15.0",
|
||||
"clean-css": "^5.3.2",
|
||||
"cli-table": "^0.3.1",
|
||||
"commander": "^12.1.0",
|
||||
"css": "^3.0.0",
|
||||
"css-color-names": "^1.0.1",
|
||||
"deep-freeze-es6": "^3.0.2",
|
||||
"del": "^7.1.0",
|
||||
"dependency-resolver": "^2.0.1",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-standard": "^17.1.0",
|
||||
"eslint-plugin-import": "^2.28.1",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
"glob": "^8.1.0",
|
||||
"glob-promise": "^6.0.5",
|
||||
"handlebars": "^4.7.8",
|
||||
"http-server": "^14.1.1",
|
||||
"jsdom": "^24.1.0",
|
||||
"lodash": "^4.17.20",
|
||||
"mocha": "^10.2.0",
|
||||
"refa": "^0.4.1",
|
||||
"rollup": "^4.0.2",
|
||||
"should": "^13.2.3",
|
||||
"terser": "^5.21.0",
|
||||
"tiny-worker": "^2.3.0",
|
||||
"typescript": "^5.2.2",
|
||||
"wcag-contrast": "^3.0.0"
|
||||
}
|
||||
}
|
||||
10
examples/axum_js_ssr/node_modules/@highlightjs/cdn-assets/styles/github-dark.min.css
generated
vendored
Normal file
10
examples/axum_js_ssr/node_modules/@highlightjs/cdn-assets/styles/github-dark.min.css
generated
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}/*!
|
||||
Theme: GitHub Dark
|
||||
Description: Dark theme as seen on github.com
|
||||
Author: github.com
|
||||
Maintainer: @Hirse
|
||||
Updated: 2021-05-15
|
||||
|
||||
Outdated base version: https://github.com/primer/github-syntax-dark
|
||||
Current colors taken from GitHub's CSS
|
||||
*/.hljs{color:#c9d1d9;background:#0d1117}.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_{color:#ff7b72}.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#d2a8ff}.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable{color:#79c0ff}.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#a5d6ff}.hljs-built_in,.hljs-symbol{color:#ffa657}.hljs-code,.hljs-comment,.hljs-formula{color:#8b949e}.hljs-name,.hljs-quote,.hljs-selector-pseudo,.hljs-selector-tag{color:#7ee787}.hljs-subst{color:#c9d1d9}.hljs-section{color:#1f6feb;font-weight:700}.hljs-bullet{color:#f2cc60}.hljs-emphasis{color:#c9d1d9;font-style:italic}.hljs-strong{color:#c9d1d9;font-weight:700}.hljs-addition{color:#aff5b4;background-color:#033a16}.hljs-deletion{color:#ffdcd7;background-color:#67060c}
|
||||
10
examples/axum_js_ssr/node_modules/@highlightjs/cdn-assets/styles/github.min.css
generated
vendored
Normal file
10
examples/axum_js_ssr/node_modules/@highlightjs/cdn-assets/styles/github.min.css
generated
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}/*!
|
||||
Theme: GitHub
|
||||
Description: Light theme as seen on github.com
|
||||
Author: github.com
|
||||
Maintainer: @Hirse
|
||||
Updated: 2021-05-15
|
||||
|
||||
Outdated base version: https://github.com/primer/github-syntax-light
|
||||
Current colors taken from GitHub's CSS
|
||||
*/.hljs{color:#24292e;background:#fff}.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_{color:#d73a49}.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#6f42c1}.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable{color:#005cc5}.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#032f62}.hljs-built_in,.hljs-symbol{color:#e36209}.hljs-code,.hljs-comment,.hljs-formula{color:#6a737d}.hljs-name,.hljs-quote,.hljs-selector-pseudo,.hljs-selector-tag{color:#22863a}.hljs-subst{color:#24292e}.hljs-section{color:#005cc5;font-weight:700}.hljs-bullet{color:#735c0f}.hljs-emphasis{color:#24292e;font-style:italic}.hljs-strong{color:#24292e;font-weight:700}.hljs-addition{color:#22863a;background-color:#f0fff4}.hljs-deletion{color:#b31d28;background-color:#ffeef0}
|
||||
6
examples/axum_js_ssr/package.json
Normal file
6
examples/axum_js_ssr/package.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "axum_js_ssr",
|
||||
"dependencies": {
|
||||
"@highlightjs/cdn-assets": "^11.10.0"
|
||||
}
|
||||
}
|
||||
2
examples/axum_js_ssr/rust-toolchain.toml
Normal file
2
examples/axum_js_ssr/rust-toolchain.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "stable" # test change
|
||||
8
examples/axum_js_ssr/src/api.rs
Normal file
8
examples/axum_js_ssr/src/api.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
use leptos::{prelude::ServerFnError, server};
|
||||
|
||||
#[server]
|
||||
pub async fn fetch_code() -> Result<String, ServerFnError> {
|
||||
// emulate loading of code from a database/version control/etc
|
||||
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
|
||||
Ok(crate::consts::CH05_02A.to_string())
|
||||
}
|
||||
1133
examples/axum_js_ssr/src/app.rs
Normal file
1133
examples/axum_js_ssr/src/app.rs
Normal file
File diff suppressed because it is too large
Load Diff
39
examples/axum_js_ssr/src/consts.rs
Normal file
39
examples/axum_js_ssr/src/consts.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
// Example programs from the Rust Programming Language Book
|
||||
|
||||
pub const CH03_05A: &str = r#"fn main() {
|
||||
let number = 3;
|
||||
|
||||
if number < 5 {
|
||||
println!("condition was true");
|
||||
} else {
|
||||
println!("condition was false");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
"#;
|
||||
|
||||
// For some reason, swapping the code examples "fixes" example 6. It
|
||||
// might have something to do with the lower complexity of highlighting
|
||||
// a shorter example. Anyway, including extra newlines for the shorter
|
||||
// example to match with the longer in order to avoid reflowing the
|
||||
// table during the async resource loading for CSR.
|
||||
|
||||
pub const CH05_02A: &str = r#"fn main() {
|
||||
let width1 = 30;
|
||||
let height1 = 50;
|
||||
|
||||
println!(
|
||||
"The area of the rectangle is {} square pixels.",
|
||||
area(width1, height1)
|
||||
);
|
||||
}
|
||||
|
||||
fn area(width: u32, height: u32) -> u32 {
|
||||
width * height
|
||||
}
|
||||
"#;
|
||||
|
||||
pub const LEPTOS_HYDRATED: &str = "_leptos_hydrated";
|
||||
59
examples/axum_js_ssr/src/hljs.rs
Normal file
59
examples/axum_js_ssr/src/hljs.rs
Normal file
@@ -0,0 +1,59 @@
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
mod csr {
|
||||
use gloo_utils::format::JsValueSerdeExt;
|
||||
use js_sys::{
|
||||
Object,
|
||||
Reflect::{get, set},
|
||||
};
|
||||
use wasm_bindgen::{prelude::wasm_bindgen, JsValue};
|
||||
|
||||
#[wasm_bindgen(
|
||||
module = "/node_modules/@highlightjs/cdn-assets/es/highlight.min.js"
|
||||
)]
|
||||
extern "C" {
|
||||
type HighlightOptions;
|
||||
|
||||
#[wasm_bindgen(catch, js_namespace = defaultMod, js_name = highlight)]
|
||||
fn highlight_lang(
|
||||
code: String,
|
||||
options: Object,
|
||||
) -> Result<Object, JsValue>;
|
||||
|
||||
#[wasm_bindgen(js_namespace = defaultMod, js_name = highlightAll)]
|
||||
pub fn highlight_all();
|
||||
}
|
||||
|
||||
// Keeping the `ignoreIllegals` argument out of the default case, and since there is no optional arguments
|
||||
// in Rust, this will have to be provided in a separate function (e.g. `highlight_ignore_illegals`), much
|
||||
// like how `web_sys` does it for the browser APIs. For simplicity, only the highlighted HTML code is
|
||||
// returned on success, and None on error.
|
||||
pub fn highlight(code: String, lang: String) -> Option<String> {
|
||||
let options = js_sys::Object::new();
|
||||
set(&options, &"language".into(), &lang.into())
|
||||
.expect("failed to assign lang to options");
|
||||
highlight_lang(code, options)
|
||||
.map(|result| {
|
||||
let value = get(&result, &"value".into())
|
||||
.expect("HighlightResult failed to contain the value key");
|
||||
value.into_serde().expect("Value should have been a string")
|
||||
})
|
||||
.ok()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
mod ssr {
|
||||
// noop under ssr
|
||||
pub fn highlight_all() {}
|
||||
|
||||
// TODO see if there is a Rust-based solution that will enable isomorphic rendering for this feature.
|
||||
// the current (disabled) implementation simply calls html_escape.
|
||||
// pub fn highlight(code: String, _lang: String) -> Option<String> {
|
||||
// Some(html_escape::encode_text(&code).into_owned())
|
||||
// }
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
pub use csr::*;
|
||||
#[cfg(feature = "ssr")]
|
||||
pub use ssr::*;
|
||||
51
examples/axum_js_ssr/src/lib.rs
Normal file
51
examples/axum_js_ssr/src/lib.rs
Normal file
@@ -0,0 +1,51 @@
|
||||
pub mod api;
|
||||
pub mod app;
|
||||
pub mod consts;
|
||||
pub mod hljs;
|
||||
|
||||
#[cfg(feature = "hydrate")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
use app::*;
|
||||
use consts::LEPTOS_HYDRATED;
|
||||
use std::panic;
|
||||
panic::set_hook(Box::new(|info| {
|
||||
// this custom hook will call out to show the usual error log at
|
||||
// the console while also attempt to update the UI to indicate
|
||||
// a restart of the application is required to continue.
|
||||
console_error_panic_hook::hook(info);
|
||||
let window = leptos::prelude::window();
|
||||
if !matches!(
|
||||
js_sys::Reflect::get(&window, &wasm_bindgen::JsValue::from_str(LEPTOS_HYDRATED)),
|
||||
Ok(t) if t == true
|
||||
) {
|
||||
let document = leptos::prelude::document();
|
||||
let _ = document.query_selector("#reset").map(|el| {
|
||||
el.map(|el| {
|
||||
el.set_class_name("panicked");
|
||||
})
|
||||
});
|
||||
let _ = document.query_selector("#notice").map(|el| {
|
||||
el.map(|el| {
|
||||
el.set_class_name("panicked");
|
||||
})
|
||||
});
|
||||
}
|
||||
}));
|
||||
leptos::mount::hydrate_body(App);
|
||||
|
||||
let window = leptos::prelude::window();
|
||||
js_sys::Reflect::set(
|
||||
&window,
|
||||
&wasm_bindgen::JsValue::from_str(LEPTOS_HYDRATED),
|
||||
&wasm_bindgen::JsValue::TRUE,
|
||||
)
|
||||
.expect("error setting hydrated status");
|
||||
let event = web_sys::Event::new(LEPTOS_HYDRATED)
|
||||
.expect("error creating hydrated event");
|
||||
let document = leptos::prelude::document();
|
||||
document
|
||||
.dispatch_event(&event)
|
||||
.expect("error dispatching hydrated event");
|
||||
leptos::logging::log!("dispatched hydrated event");
|
||||
}
|
||||
153
examples/axum_js_ssr/src/main.rs
Normal file
153
examples/axum_js_ssr/src/main.rs
Normal file
@@ -0,0 +1,153 @@
|
||||
#[cfg(feature = "ssr")]
|
||||
mod latency {
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
pub static LATENCY: OnceLock<
|
||||
Mutex<std::iter::Cycle<std::slice::Iter<'_, u64>>>,
|
||||
> = OnceLock::new();
|
||||
pub static ES_LATENCY: OnceLock<
|
||||
Mutex<std::iter::Cycle<std::slice::Iter<'_, u64>>>,
|
||||
> = OnceLock::new();
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
use axum::{
|
||||
body::Body,
|
||||
extract::Request,
|
||||
http::{
|
||||
header::{self, HeaderValue},
|
||||
StatusCode,
|
||||
},
|
||||
middleware::{self, Next},
|
||||
response::{IntoResponse, Response},
|
||||
routing::get,
|
||||
Router,
|
||||
};
|
||||
use axum_js_ssr::app::*;
|
||||
use http_body_util::BodyExt;
|
||||
use leptos::logging::log;
|
||||
use leptos::prelude::*;
|
||||
use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||
|
||||
latency::LATENCY.get_or_init(|| [0, 4, 40, 400].iter().cycle().into());
|
||||
latency::ES_LATENCY.get_or_init(|| [0].iter().cycle().into());
|
||||
// Having the ES_LATENCY (a cycle of latency for the loading of the es
|
||||
// module) in an identical cycle as LATENCY (for the standard version)
|
||||
// adversely influences the intended demo, as this ultimately delays
|
||||
// hydration when set too high which can cause panic under every case.
|
||||
// If you want to test the effects of the delay just modify the list of
|
||||
// values for the desired cycle of delays.
|
||||
|
||||
let conf = get_configuration(None).unwrap();
|
||||
let addr = conf.leptos_options.site_addr;
|
||||
let leptos_options = conf.leptos_options;
|
||||
// Generate the list of routes in your Leptos App
|
||||
let routes = generate_route_list(App);
|
||||
|
||||
async fn highlight_js() -> impl IntoResponse {
|
||||
(
|
||||
[(header::CONTENT_TYPE, "text/javascript")],
|
||||
include_str!(
|
||||
"../node_modules/@highlightjs/cdn-assets/highlight.min.js"
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
async fn latency_for_highlight_js(
|
||||
req: Request,
|
||||
next: Next,
|
||||
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
||||
let uri_parts = &mut req.uri().path().rsplit('/');
|
||||
|
||||
let is_highlightjs = uri_parts.next() == Some("highlight.min.js");
|
||||
let es = uri_parts.next() == Some("es");
|
||||
let module_type = if es { "es module " } else { "standard " };
|
||||
let res = next.run(req).await;
|
||||
if is_highlightjs {
|
||||
// additional processing if the filename is the test subject
|
||||
let (mut parts, body) = res.into_parts();
|
||||
let bytes = body
|
||||
.collect()
|
||||
.await
|
||||
.map_err(|err| {
|
||||
(
|
||||
StatusCode::BAD_REQUEST,
|
||||
format!("error reading body: {err}"),
|
||||
)
|
||||
})?
|
||||
.to_bytes();
|
||||
let latency = if es {
|
||||
&latency::ES_LATENCY
|
||||
} else {
|
||||
&latency::LATENCY
|
||||
};
|
||||
|
||||
let delay = match latency
|
||||
.get()
|
||||
.expect("latency cycle wasn't set up")
|
||||
.try_lock()
|
||||
{
|
||||
Ok(ref mut mutex) => {
|
||||
*mutex.next().expect("cycle always has next")
|
||||
}
|
||||
Err(_) => 0,
|
||||
};
|
||||
|
||||
// inject the logging of the delay used into the target script
|
||||
log!(
|
||||
"loading {module_type}highlight.min.js with latency of \
|
||||
{delay} ms"
|
||||
);
|
||||
let js_log = format!(
|
||||
"\nconsole.log('loaded {module_type}highlight.js with a \
|
||||
minimum latency of {delay} ms');"
|
||||
);
|
||||
tokio::time::sleep(std::time::Duration::from_millis(delay)).await;
|
||||
|
||||
let bytes = [bytes, js_log.into()].concat();
|
||||
let length = bytes.len();
|
||||
let body = Body::from(bytes);
|
||||
|
||||
// Provide the bare minimum set of headers to avoid browser cache.
|
||||
parts.headers = header::HeaderMap::from_iter(
|
||||
[
|
||||
(
|
||||
header::CONTENT_TYPE,
|
||||
HeaderValue::from_static("text/javascript"),
|
||||
),
|
||||
(header::CONTENT_LENGTH, HeaderValue::from(length)),
|
||||
]
|
||||
.into_iter(),
|
||||
);
|
||||
Ok(Response::from_parts(parts, body))
|
||||
} else {
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
let app = Router::new()
|
||||
.route("/highlight.min.js", get(highlight_js))
|
||||
.leptos_routes(&leptos_options, routes, {
|
||||
let leptos_options = leptos_options.clone();
|
||||
move || shell(leptos_options.clone())
|
||||
})
|
||||
.fallback(leptos_axum::file_and_error_handler(shell))
|
||||
.layer(middleware::from_fn(latency_for_highlight_js))
|
||||
.with_state(leptos_options);
|
||||
|
||||
// run our app with hyper
|
||||
// `axum::Server` is a re-export of `hyper::Server`
|
||||
log!("listening on http://{}", &addr);
|
||||
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
|
||||
axum::serve(listener, app.into_make_service())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
pub fn main() {
|
||||
// no client-side main function
|
||||
// unless we want this to work with e.g., Trunk for pure client-side testing
|
||||
// see lib.rs for hydration function instead
|
||||
}
|
||||
171
examples/axum_js_ssr/style/main.scss
Normal file
171
examples/axum_js_ssr/style/main.scss
Normal file
@@ -0,0 +1,171 @@
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: sans-serif;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
}
|
||||
|
||||
nav {
|
||||
min-width: 17em;
|
||||
height: 100vh;
|
||||
counter-reset: example-counter 0;
|
||||
list-style-type: none;
|
||||
list-style-position: outside;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
nav a {
|
||||
display: block;
|
||||
padding: 0.5em 2em;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
nav a small {
|
||||
display: block;
|
||||
}
|
||||
|
||||
nav a.example::before {
|
||||
counter-reset: subexample-counter 0;
|
||||
counter-increment: example-counter 1;
|
||||
content: counter(example-counter) ". ";
|
||||
}
|
||||
|
||||
nav a.subexample::before {
|
||||
counter-increment: subexample-counter 1;
|
||||
content: counter(example-counter) "." counter(subexample-counter) " ";
|
||||
}
|
||||
|
||||
div#notice {
|
||||
display: none;
|
||||
}
|
||||
|
||||
main div#notice.panicked {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
padding: 0.5em 2em;
|
||||
display: block;
|
||||
}
|
||||
|
||||
main {
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
main article {
|
||||
max-width: 60em;
|
||||
margin: 0 1em;
|
||||
padding: 0 1em;
|
||||
}
|
||||
|
||||
main p, main li {
|
||||
line-height: 1.3em;
|
||||
}
|
||||
|
||||
main li pre code, main div pre code {
|
||||
display: block;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
main ol, main ul {
|
||||
padding-left: 2em;
|
||||
}
|
||||
|
||||
h2>code, p>code, li>code {
|
||||
border-radius: 3px;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
li pre code, div pre code {
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
#code-demo {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
#code-demo table {
|
||||
width: 50em;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
#code-demo table td {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
#code-demo table code {
|
||||
display: block;
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
nav {
|
||||
background: #f7f7f7;
|
||||
}
|
||||
|
||||
nav a {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
nav a[aria-current="page"] {
|
||||
background-color: #e0e0e0;
|
||||
}
|
||||
|
||||
nav a:hover, h2>code, p>code, li>code {
|
||||
background-color: #e7e7e7;
|
||||
}
|
||||
|
||||
nav a.panicked, main div#notice.panicked {
|
||||
background: #fdd;
|
||||
}
|
||||
|
||||
main div#notice.panicked a {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
nav a.section {
|
||||
border-bottom: 1px solid #777;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
nav {
|
||||
background: #080808;
|
||||
}
|
||||
|
||||
nav a {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
nav a[aria-current="page"] {
|
||||
background-color: #3f3f3f;
|
||||
}
|
||||
|
||||
nav a:hover, h2>code, p>code, li>code {
|
||||
background-color: #383838;
|
||||
}
|
||||
|
||||
nav a.panicked, main div#notice.panicked {
|
||||
background: #733;
|
||||
}
|
||||
|
||||
main div#notice.panicked a {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
nav a.section {
|
||||
border-bottom: 1px solid #888;
|
||||
}
|
||||
}
|
||||
|
||||
// Just include the raw style as-is because I can't find a quick and easy way to import them just for the
|
||||
// appropriate media type...
|
||||
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}
|
||||
@media (prefers-color-scheme: light){.hljs{color:#24292e;background:#fff}.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_{color:#d73a49}.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#6f42c1}.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable{color:#005cc5}.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#032f62}.hljs-built_in,.hljs-symbol{color:#e36209}.hljs-code,.hljs-comment,.hljs-formula{color:#6a737d}.hljs-name,.hljs-quote,.hljs-selector-pseudo,.hljs-selector-tag{color:#22863a}.hljs-subst{color:#24292e}.hljs-section{color:#005cc5;font-weight:700}.hljs-bullet{color:#735c0f}.hljs-emphasis{color:#24292e;font-style:italic}.hljs-strong{color:#24292e;font-weight:700}.hljs-addition{color:#22863a;background-color:#f0fff4}.hljs-deletion{color:#b31d28;background-color:#ffeef0}}
|
||||
@media (prefers-color-scheme: dark){.hljs{color:#c9d1d9;background:#0d1117}.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_{color:#ff7b72}.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#d2a8ff}.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable{color:#79c0ff}.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#a5d6ff}.hljs-built_in,.hljs-symbol{color:#ffa657}.hljs-code,.hljs-comment,.hljs-formula{color:#8b949e}.hljs-name,.hljs-quote,.hljs-selector-pseudo,.hljs-selector-tag{color:#7ee787}.hljs-subst{color:#c9d1d9}.hljs-section{color:#1f6feb;font-weight:700}.hljs-bullet{color:#f2cc60}.hljs-emphasis{color:#c9d1d9;font-style:italic}.hljs-strong{color:#c9d1d9;font-weight:700}.hljs-addition{color:#aff5b4;background-color:#033a16}.hljs-deletion{color:#ffdcd7;background-color:#67060c}}
|
||||
@@ -1,5 +1,6 @@
|
||||
[tasks.install-cargo-leptos]
|
||||
install_crate = { crate_name = "cargo-leptos", binary = "cargo-leptos", test_arg = "--help" }
|
||||
args = ["--locked"]
|
||||
|
||||
[tasks.cargo-leptos-e2e]
|
||||
command = "cargo"
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
use counter::*;
|
||||
use leptos::mount::mount_to;
|
||||
use leptos::prelude::*;
|
||||
use leptos::spawn::tick;
|
||||
use leptos::task::tick;
|
||||
use wasm_bindgen::JsCast;
|
||||
use wasm_bindgen_test::*;
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
name = "counter_isomorphic"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
# std::sync::LazyLock is stabilized in Rust version 1.80.0
|
||||
rust-version = "1.80.0"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
@@ -17,7 +19,6 @@ broadcaster = "1.0"
|
||||
console_log = "1.0"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
futures = "0.3.30"
|
||||
lazy_static = "1.5"
|
||||
leptos = { path = "../../leptos" }
|
||||
leptos_actix = { path = "../../integrations/actix", optional = true }
|
||||
leptos_router = { path = "../../router" }
|
||||
@@ -46,13 +47,13 @@ denylist = ["actix-files", "actix-web", "leptos_actix"]
|
||||
skip_feature_sets = [["ssr", "hydrate"]]
|
||||
|
||||
[package.metadata.leptos]
|
||||
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
|
||||
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
|
||||
output-name = "counter_isomorphic"
|
||||
# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup.
|
||||
# When NOT using cargo-leptos this must be updated to "." or the counters will not work. The above warning still applies if you do switch to cargo-leptos later.
|
||||
site-root = "target/site"
|
||||
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
|
||||
# Defaults to pkg
|
||||
# Defaults to pkg
|
||||
site-pkg-dir = "pkg"
|
||||
# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css
|
||||
# style-file = "src/styles/tailwind.css"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use leptos::{prelude::*, reactive_graph::actions::Action};
|
||||
use leptos::prelude::*;
|
||||
use leptos_router::{
|
||||
components::{FlatRoutes, Route, Router, A},
|
||||
StaticSegment,
|
||||
@@ -10,12 +10,12 @@ use tracing::instrument;
|
||||
pub mod ssr_imports {
|
||||
pub use broadcaster::BroadcastChannel;
|
||||
pub use std::sync::atomic::{AtomicI32, Ordering};
|
||||
use std::sync::LazyLock;
|
||||
|
||||
pub static COUNT: AtomicI32 = AtomicI32::new(0);
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref COUNT_CHANNEL: BroadcastChannel<i32> = BroadcastChannel::new();
|
||||
}
|
||||
pub static COUNT_CHANNEL: LazyLock<BroadcastChannel<i32>> =
|
||||
LazyLock::new(BroadcastChannel::<i32>::new);
|
||||
}
|
||||
|
||||
#[server]
|
||||
|
||||
@@ -63,7 +63,7 @@ async fn main() -> std::io::Result<()> {
|
||||
</html>
|
||||
}
|
||||
}})
|
||||
.service(Files::new("/", site_root))
|
||||
.service(Files::new("/", site_root.as_ref()))
|
||||
})
|
||||
.bind(&addr)?
|
||||
.run()
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
use counter_without_macros::counter;
|
||||
use leptos::{prelude::*, spawn::tick};
|
||||
use leptos::{prelude::*, task::tick};
|
||||
use pretty_assertions::assert_eq;
|
||||
use wasm_bindgen::JsCast;
|
||||
use wasm_bindgen_test::*;
|
||||
|
||||
6
examples/counters/.gitignore
vendored
Normal file
6
examples/counters/.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
# Support playwright testing
|
||||
node_modules/
|
||||
test-results/
|
||||
end2end/playwright-report/
|
||||
playwright/.cache/
|
||||
pnpm-lock.yaml
|
||||
@@ -2,4 +2,5 @@ extend = [
|
||||
{ path = "../cargo-make/main.toml" },
|
||||
{ path = "../cargo-make/wasm-test.toml" },
|
||||
{ path = "../cargo-make/trunk_server.toml" },
|
||||
{ path = "../cargo-make/playwright-trunk-test.toml" },
|
||||
]
|
||||
|
||||
4
examples/counters/e2e/.gitignore
vendored
Normal file
4
examples/counters/e2e/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
node_modules/
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/playwright/.cache/
|
||||
83
examples/counters/e2e/package-lock.json
generated
Normal file
83
examples/counters/e2e/package-lock.json
generated
Normal file
@@ -0,0 +1,83 @@
|
||||
{
|
||||
"name": "grip",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "grip",
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.35.1"
|
||||
}
|
||||
},
|
||||
"node_modules/.pnpm/@playwright+test@1.33.0": {
|
||||
"extraneous": true
|
||||
},
|
||||
"node_modules/.pnpm/@types+node@20.2.1/node_modules/@types/node": {
|
||||
"version": "20.2.1",
|
||||
"extraneous": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/.pnpm/playwright-core@1.33.0/node_modules/playwright-core": {
|
||||
"version": "1.33.0",
|
||||
"extraneous": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.35.1",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.35.1.tgz",
|
||||
"integrity": "sha512-b5YoFe6J9exsMYg0pQAobNDR85T1nLumUYgUTtKm4d21iX2L7WqKq9dW8NGJ+2vX0etZd+Y7UeuqsxDXm9+5ZA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"playwright-core": "1.35.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.3.1.tgz",
|
||||
"integrity": "sha512-EhcH/wvidPy1WeML3TtYFGR83UzjxeWRen9V402T8aUGYsCHOmfoisV3ZSg03gAFIbLq8TnWOJ0f4cALtnSEUg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.35.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.35.1.tgz",
|
||||
"integrity": "sha512-pNXb6CQ7OqmGDRspEjlxE49w+4YtR6a3X6mT1hZXeJHWmsEz7SunmvZeiG/+y1yyMZdHnnn73WKYdtV1er0Xyg==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
10
examples/counters/e2e/package.json
Normal file
10
examples/counters/e2e/package.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"private": "true",
|
||||
"scripts": {},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.46.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"pnpm": "^9.7.1"
|
||||
}
|
||||
}
|
||||
77
examples/counters/e2e/playwright.config.ts
Normal file
77
examples/counters/e2e/playwright.config.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { defineConfig, devices } from "@playwright/test";
|
||||
|
||||
/**
|
||||
* Read environment variables from file.
|
||||
* https://github.com/motdotla/dotenv
|
||||
*/
|
||||
// require('dotenv').config();
|
||||
|
||||
/**
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
*/
|
||||
export default defineConfig({
|
||||
testDir: "./tests",
|
||||
/* Run tests in files in parallel */
|
||||
fullyParallel: true,
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: !process.env.DEV,
|
||||
/* Retry on CI only */
|
||||
retries: process.env.DEV ? 0 : 10,
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: process.env.DEV ? 1 : 1,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: [["html", { open: "never" }], ["list"]],
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
baseURL: "http://127.0.0.1:8080",
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: "on-first-retry",
|
||||
},
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
projects: [
|
||||
{
|
||||
name: "chromium",
|
||||
use: { ...devices["Desktop Chrome"] },
|
||||
},
|
||||
|
||||
// {
|
||||
// name: "firefox",
|
||||
// use: { ...devices["Desktop Firefox"] },
|
||||
// },
|
||||
|
||||
// {
|
||||
// name: "webkit",
|
||||
// use: { ...devices["Desktop Safari"] },
|
||||
// },
|
||||
|
||||
/* Test against mobile viewports. */
|
||||
// {
|
||||
// name: 'Mobile Chrome',
|
||||
// use: { ...devices['Pixel 5'] },
|
||||
// },
|
||||
// {
|
||||
// name: 'Mobile Safari',
|
||||
// use: { ...devices['iPhone 12'] },
|
||||
// },
|
||||
|
||||
/* Test against branded browsers. */
|
||||
// {
|
||||
// name: 'Microsoft Edge',
|
||||
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
|
||||
// },
|
||||
// {
|
||||
// name: 'Google Chrome',
|
||||
// use: { ..devices['Desktop Chrome'], channel: 'chrome' },
|
||||
// },
|
||||
],
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
// webServer: {
|
||||
// command: "cd ../ && trunk serve",
|
||||
// url: "http://127.0.0.1:8080",
|
||||
// reuseExistingServer: false, //!process.env.CI,
|
||||
// },
|
||||
});
|
||||
19
examples/counters/e2e/tests/add_1k_counters.spec.ts
Normal file
19
examples/counters/e2e/tests/add_1k_counters.spec.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { CountersPage } from "./fixtures/counters_page";
|
||||
|
||||
test.describe("Add 1000 Counters", () => {
|
||||
test("should increase the number of counters", async ({ page }) => {
|
||||
const ui = new CountersPage(page);
|
||||
|
||||
await Promise.all([
|
||||
await ui.goto(),
|
||||
await ui.addOneThousandCountersButton.waitFor(),
|
||||
]);
|
||||
|
||||
await ui.addOneThousandCounters();
|
||||
await ui.addOneThousandCounters();
|
||||
await ui.addOneThousandCounters();
|
||||
|
||||
await expect(ui.counters).toHaveText("3000");
|
||||
});
|
||||
});
|
||||
15
examples/counters/e2e/tests/add_counter.spec.ts
Normal file
15
examples/counters/e2e/tests/add_counter.spec.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { CountersPage } from "./fixtures/counters_page";
|
||||
|
||||
test.describe("Add Counter", () => {
|
||||
test("should increase the number of counters", async ({ page }) => {
|
||||
const ui = new CountersPage(page);
|
||||
await ui.goto();
|
||||
|
||||
await ui.addCounter();
|
||||
await ui.addCounter();
|
||||
await ui.addCounter();
|
||||
|
||||
await expect(ui.counters).toHaveText("3");
|
||||
});
|
||||
});
|
||||
18
examples/counters/e2e/tests/clear_counters.spec.ts
Normal file
18
examples/counters/e2e/tests/clear_counters.spec.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { CountersPage } from "./fixtures/counters_page";
|
||||
|
||||
test.describe("Clear Counters", () => {
|
||||
test("should reset the counts", async ({ page }) => {
|
||||
const ui = new CountersPage(page);
|
||||
await ui.goto();
|
||||
|
||||
await ui.addCounter();
|
||||
await ui.addCounter();
|
||||
await ui.addCounter();
|
||||
|
||||
await ui.clearCounters();
|
||||
|
||||
await expect(ui.total).toHaveText("0");
|
||||
await expect(ui.counters).toHaveText("0");
|
||||
});
|
||||
});
|
||||
16
examples/counters/e2e/tests/decrement_count.spec.ts
Normal file
16
examples/counters/e2e/tests/decrement_count.spec.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { CountersPage } from "./fixtures/counters_page";
|
||||
|
||||
test.describe("Decrement Count", () => {
|
||||
test("should decrease the total count", async ({ page }) => {
|
||||
const ui = new CountersPage(page);
|
||||
await ui.goto();
|
||||
await ui.addCounter();
|
||||
|
||||
await ui.decrementCount();
|
||||
await ui.decrementCount();
|
||||
await ui.decrementCount();
|
||||
|
||||
await expect(ui.total).toHaveText("-3");
|
||||
});
|
||||
});
|
||||
30
examples/counters/e2e/tests/enter_count.spec.ts
Normal file
30
examples/counters/e2e/tests/enter_count.spec.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { CountersPage } from "./fixtures/counters_page";
|
||||
|
||||
test.describe("Enter Count", () => {
|
||||
test("should increase the total count", async ({ page }) => {
|
||||
const ui = new CountersPage(page);
|
||||
await ui.goto();
|
||||
await ui.addCounter();
|
||||
|
||||
await ui.enterCount("5");
|
||||
|
||||
await expect(ui.total).toHaveText("5");
|
||||
await expect(ui.counters).toHaveText("1");
|
||||
});
|
||||
|
||||
test("should decrease the total count", async ({ page }) => {
|
||||
const ui = new CountersPage(page);
|
||||
await ui.goto();
|
||||
await ui.addCounter();
|
||||
await ui.addCounter();
|
||||
await ui.addCounter();
|
||||
|
||||
await ui.enterCount("100");
|
||||
await ui.enterCount("100", 1);
|
||||
await ui.enterCount("100", 2);
|
||||
await ui.enterCount("50", 1);
|
||||
|
||||
await expect(ui.total).toHaveText("250");
|
||||
});
|
||||
});
|
||||
98
examples/counters/e2e/tests/fixtures/counters_page.ts
vendored
Normal file
98
examples/counters/e2e/tests/fixtures/counters_page.ts
vendored
Normal file
@@ -0,0 +1,98 @@
|
||||
import { expect, Locator, Page } from "@playwright/test";
|
||||
|
||||
export class CountersPage {
|
||||
readonly page: Page;
|
||||
readonly addCounterButton: Locator;
|
||||
readonly addOneThousandCountersButton: Locator;
|
||||
readonly clearCountersButton: Locator;
|
||||
|
||||
readonly incrementCountButton: Locator;
|
||||
readonly counterInput: Locator;
|
||||
readonly decrementCountButton: Locator;
|
||||
readonly removeCountButton: Locator;
|
||||
|
||||
readonly total: Locator;
|
||||
readonly counters: Locator;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
|
||||
this.addCounterButton = page.locator("button", { hasText: "Add Counter" });
|
||||
|
||||
this.addOneThousandCountersButton = page.locator("button", {
|
||||
hasText: "Add 1000 Counters",
|
||||
});
|
||||
|
||||
this.clearCountersButton = page.locator("button", {
|
||||
hasText: "Clear Counters",
|
||||
});
|
||||
|
||||
this.decrementCountButton = page.locator("button", {
|
||||
hasText: "-1",
|
||||
});
|
||||
|
||||
this.incrementCountButton = page.locator("button", {
|
||||
hasText: "+1",
|
||||
});
|
||||
|
||||
this.removeCountButton = page.locator("button", {
|
||||
hasText: "x",
|
||||
});
|
||||
|
||||
this.total = page.getByTestId("total");
|
||||
|
||||
this.counters = page.getByTestId("counters");
|
||||
|
||||
this.counterInput = page.getByRole("textbox");
|
||||
}
|
||||
|
||||
async goto() {
|
||||
await this.page.goto("/");
|
||||
}
|
||||
|
||||
async addCounter() {
|
||||
await Promise.all([
|
||||
this.addCounterButton.waitFor(),
|
||||
this.addCounterButton.click(),
|
||||
]);
|
||||
}
|
||||
|
||||
async addOneThousandCounters() {
|
||||
this.addOneThousandCountersButton.click();
|
||||
}
|
||||
|
||||
async decrementCount(index: number = 0) {
|
||||
await Promise.all([
|
||||
this.decrementCountButton.nth(index).waitFor(),
|
||||
this.decrementCountButton.nth(index).click(),
|
||||
]);
|
||||
}
|
||||
|
||||
async incrementCount(index: number = 0) {
|
||||
await Promise.all([
|
||||
this.incrementCountButton.nth(index).waitFor(),
|
||||
this.incrementCountButton.nth(index).click(),
|
||||
]);
|
||||
}
|
||||
|
||||
async clearCounters() {
|
||||
await Promise.all([
|
||||
this.clearCountersButton.waitFor(),
|
||||
this.clearCountersButton.click(),
|
||||
]);
|
||||
}
|
||||
|
||||
async enterCount(count: string, index: number = 0) {
|
||||
await Promise.all([
|
||||
this.counterInput.nth(index).waitFor(),
|
||||
this.counterInput.nth(index).fill(count),
|
||||
]);
|
||||
}
|
||||
|
||||
async removeCounter(index: number = 0) {
|
||||
await Promise.all([
|
||||
this.removeCountButton.nth(index).waitFor(),
|
||||
this.removeCountButton.nth(index).click(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
16
examples/counters/e2e/tests/increment_count.spec.ts
Normal file
16
examples/counters/e2e/tests/increment_count.spec.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { CountersPage } from "./fixtures/counters_page";
|
||||
|
||||
test.describe("Increment Count", () => {
|
||||
test("should increase the total count", async ({ page }) => {
|
||||
const ui = new CountersPage(page);
|
||||
await ui.goto();
|
||||
await ui.addCounter();
|
||||
|
||||
await ui.incrementCount();
|
||||
await ui.incrementCount();
|
||||
await ui.incrementCount();
|
||||
|
||||
await expect(ui.total).toHaveText("3");
|
||||
});
|
||||
});
|
||||
17
examples/counters/e2e/tests/remove_counter.spec.ts
Normal file
17
examples/counters/e2e/tests/remove_counter.spec.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { CountersPage } from "./fixtures/counters_page";
|
||||
|
||||
test.describe("Remove Counter", () => {
|
||||
test("should decrement the number of counters", async ({ page }) => {
|
||||
const ui = new CountersPage(page);
|
||||
await ui.goto();
|
||||
|
||||
await ui.addCounter();
|
||||
await ui.addCounter();
|
||||
await ui.addCounter();
|
||||
|
||||
await ui.removeCounter(1);
|
||||
|
||||
await expect(ui.counters).toHaveText("2");
|
||||
});
|
||||
});
|
||||
19
examples/counters/e2e/tests/view_counters.spec.ts
Normal file
19
examples/counters/e2e/tests/view_counters.spec.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { CountersPage } from "./fixtures/counters_page";
|
||||
|
||||
test.describe("View Counters", () => {
|
||||
test("should see the title", async ({ page }) => {
|
||||
const ui = new CountersPage(page);
|
||||
await ui.goto();
|
||||
|
||||
await expect(page).toHaveTitle("Counters");
|
||||
});
|
||||
|
||||
test("should see the initial counts", async ({ page }) => {
|
||||
const counters = new CountersPage(page);
|
||||
await counters.goto();
|
||||
|
||||
await expect(counters.total).toHaveText("0");
|
||||
await expect(counters.counters).toHaveText("0");
|
||||
});
|
||||
});
|
||||
@@ -2,6 +2,7 @@
|
||||
<html>
|
||||
<head>
|
||||
<link data-trunk rel="rust" data-wasm-opt="z" data-weak-refs/>
|
||||
<title>Counters</title>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use leptos::prelude::{signal::*, *};
|
||||
use leptos::prelude::*;
|
||||
|
||||
const MANY_COUNTERS: usize = 1000;
|
||||
|
||||
@@ -44,12 +44,13 @@ pub fn Counters() -> impl IntoView {
|
||||
<button on:click=clear_counters>"Clear Counters"</button>
|
||||
<p>
|
||||
"Total: "
|
||||
<span>
|
||||
<span data-testid="total">
|
||||
{move || {
|
||||
counters.get().iter().map(|(_, count)| count.get()).sum::<i32>().to_string()
|
||||
}}
|
||||
|
||||
</span> " from " <span>{move || counters.get().len().to_string()}</span>
|
||||
</span> " from "
|
||||
<span data-testid="counters">{move || counters.get().len().to_string()}</span>
|
||||
" counters."
|
||||
</p>
|
||||
<ul>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
use wasm_bindgen::JsCast;
|
||||
use wasm_bindgen_test::*;
|
||||
|
||||
wasm_bindgen_test_configure!(run_in_browser);
|
||||
use counters::Counters;
|
||||
use leptos::prelude::*;
|
||||
use leptos::spawn::tick;
|
||||
use leptos::task::tick;
|
||||
use web_sys::HtmlElement;
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
@@ -23,7 +25,7 @@ async fn inc() {
|
||||
div.inner_html(),
|
||||
"<button>Add Counter</button><button>Add 1000 \
|
||||
Counters</button><button>Clear Counters</button><p>Total: \
|
||||
<span>0</span> from <span>0</span> counters.</p><ul><!----></ul>"
|
||||
<span data-testid=\"total\">0</span> from <span data-testid=\"counters\">0</span> counters.</p><ul><!----></ul>"
|
||||
);
|
||||
|
||||
// add 3 counters
|
||||
@@ -38,7 +40,7 @@ async fn inc() {
|
||||
div.inner_html(),
|
||||
"<button>Add Counter</button><button>Add 1000 \
|
||||
Counters</button><button>Clear Counters</button><p>Total: \
|
||||
<span>0</span> from <span>3</span> \
|
||||
<span data-testid=\"total\">0</span> from <span data-testid=\"counters\">3</span> \
|
||||
counters.</p><ul><li><button>-1</button><input \
|
||||
type=\"text\"><span>0</span><button>+1</button><button>x</button></\
|
||||
li><li><button>-1</button><input \
|
||||
@@ -80,7 +82,7 @@ async fn inc() {
|
||||
div.inner_html(),
|
||||
"<button>Add Counter</button><button>Add 1000 \
|
||||
Counters</button><button>Clear Counters</button><p>Total: \
|
||||
<span>6</span> from <span>3</span> \
|
||||
<span data-testid=\"total\">6</span> from <span data-testid=\"counters\">3</span> \
|
||||
counters.</p><ul><li><button>-1</button><input \
|
||||
type=\"text\"><span>1</span><button>+1</button><button>x</button></\
|
||||
li><li><button>-1</button><input \
|
||||
@@ -105,7 +107,7 @@ async fn inc() {
|
||||
div.inner_html(),
|
||||
"<button>Add Counter</button><button>Add 1000 \
|
||||
Counters</button><button>Clear Counters</button><p>Total: \
|
||||
<span>5</span> from <span>2</span> \
|
||||
<span data-testid=\"total\">5</span> from <span data-testid=\"counters\">2</span> \
|
||||
counters.</p><ul><li><button>-1</button><input \
|
||||
type=\"text\"><span>2</span><button>+1</button><button>x</button></\
|
||||
li><li><button>-1</button><input \
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
use directives::App;
|
||||
use leptos::{prelude::*, spawn::tick};
|
||||
use leptos::{prelude::*, task::tick};
|
||||
use wasm_bindgen::JsCast;
|
||||
use wasm_bindgen_test::*;
|
||||
use web_sys::HtmlElement;
|
||||
|
||||
@@ -13,7 +13,7 @@ leptos_axum = { path = "../../integrations/axum", optional = true }
|
||||
leptos_meta = { path = "../../meta" }
|
||||
leptos_router = { path = "../../router" }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
axum = { version = "0.7.5", optional = true }
|
||||
axum = { version = "0.8.1", optional = true }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.5.2", features = ["fs"], optional = true }
|
||||
tokio = { version = "1.39", features = ["full"], optional = true }
|
||||
|
||||
@@ -6,9 +6,7 @@ use leptos_axum::ResponseOptions;
|
||||
// A basic function to display errors served by the error boundaries.
|
||||
// Feel free to do more complicated things here than just displaying them.
|
||||
#[component]
|
||||
pub fn ErrorTemplate(
|
||||
#[prop(into)] errors: MaybeSignal<Errors>,
|
||||
) -> impl IntoView {
|
||||
pub fn ErrorTemplate(#[prop(into)] errors: Signal<Errors>) -> impl IntoView {
|
||||
// Get Errors from Signal
|
||||
// Downcast lets us take a type that implements `std::error::Error`
|
||||
let errors = Memo::new(move |_| {
|
||||
|
||||
@@ -45,7 +45,7 @@ async fn main() {
|
||||
|
||||
// build our application with a route
|
||||
let app = Router::new()
|
||||
.route("/special/:id", get(custom_handler))
|
||||
.route("/special/{id}", get(custom_handler))
|
||||
.leptos_routes(&leptos_options, routes, {
|
||||
let leptos_options = leptos_options.clone();
|
||||
move || shell(leptos_options.clone())
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use leptos::prelude::*;
|
||||
use leptos::tachys::html::style::style;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
@@ -16,7 +15,7 @@ pub enum CatError {
|
||||
|
||||
type CatCount = usize;
|
||||
|
||||
async fn fetch_cats(count: CatCount) -> Result<Vec<String>> {
|
||||
async fn fetch_cats(count: CatCount) -> Result<Vec<String>, Error> {
|
||||
if count > 0 {
|
||||
gloo_timers::future::TimeoutFuture::new(1000).await;
|
||||
// make the request
|
||||
@@ -42,11 +41,7 @@ async fn fetch_cats(count: CatCount) -> Result<Vec<String>> {
|
||||
pub fn fetch_example() -> impl IntoView {
|
||||
let (cat_count, set_cat_count) = signal::<CatCount>(1);
|
||||
|
||||
// we use new_unsync here because the reqwasm request type isn't Send
|
||||
// if we were doing SSR, then
|
||||
// 1) we'd want to use a Resource, so the data would be serialized to the client
|
||||
// 2) we'd need to make sure there was a thread-local spawner set up
|
||||
let cats = AsyncDerived::new_unsync(move || fetch_cats(cat_count.get()));
|
||||
let cats = LocalResource::new(move || fetch_cats(cat_count.get()));
|
||||
|
||||
let fallback = move |errors: ArcRwSignal<Errors>| {
|
||||
let error_list = move || {
|
||||
@@ -66,8 +61,6 @@ pub fn fetch_example() -> impl IntoView {
|
||||
}
|
||||
};
|
||||
|
||||
let spreadable = style(("background-color", "AliceBlue"));
|
||||
|
||||
view! {
|
||||
<div>
|
||||
<label>
|
||||
@@ -82,7 +75,7 @@ pub fn fetch_example() -> impl IntoView {
|
||||
/>
|
||||
|
||||
</label>
|
||||
<Transition fallback=|| view! { <div>"Loading..."</div> } {..spreadable}>
|
||||
<Transition fallback=|| view! { <div>"Loading..."</div> }>
|
||||
<ErrorBoundary fallback>
|
||||
<ul>
|
||||
{move || Suspend::new(async move {
|
||||
@@ -92,7 +85,7 @@ pub fn fetch_example() -> impl IntoView {
|
||||
.map(|s| {
|
||||
view! {
|
||||
<li>
|
||||
<img src=s.clone()/>
|
||||
<img src=s.clone() />
|
||||
</li>
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
[package]
|
||||
name = "gtk"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos" }
|
||||
throw_error = { path = "../../any_error/" }
|
||||
|
||||
# these are used to build the integration
|
||||
gtk = { version = "0.9.0", package = "gtk4" }
|
||||
next_tuple = { path = "../../next_tuple/" }
|
||||
paste = "1.0"
|
||||
|
||||
# we want to support using glib for the reactive runtime event loop
|
||||
any_spawner = { path = "../../any_spawner/", features = ["glib"] }
|
||||
# yes, we want effects to run: this is a "frontend," not a backend
|
||||
reactive_graph = { path = "../../reactive_graph", features = ["effects"] }
|
||||
@@ -1,8 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="color-scheme" content="dark">
|
||||
<link rel="css" href="style.css" data-trunk>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
@@ -1,627 +0,0 @@
|
||||
use self::properties::Connect;
|
||||
use gtk::{
|
||||
ffi::GtkWidget,
|
||||
glib::{
|
||||
object::{IsA, IsClass, ObjectExt},
|
||||
Object, Value,
|
||||
},
|
||||
prelude::{Cast, WidgetExt},
|
||||
Label, Orientation, Widget,
|
||||
};
|
||||
use leptos::{
|
||||
reactive_graph::effect::RenderEffect,
|
||||
tachys::{
|
||||
renderer::{CastFrom, Renderer},
|
||||
view::{Mountable, Render},
|
||||
},
|
||||
};
|
||||
use next_tuple::NextTuple;
|
||||
use std::{borrow::Cow, marker::PhantomData};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct LeptosGtk;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Element(pub Widget);
|
||||
|
||||
impl Element {
|
||||
pub fn remove(&self) {
|
||||
self.0.unparent();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Text(pub Element);
|
||||
|
||||
impl<T> From<T> for Element
|
||||
where
|
||||
T: Into<Widget>,
|
||||
{
|
||||
fn from(value: T) -> Self {
|
||||
Element(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl Mountable<LeptosGtk> for Element {
|
||||
fn unmount(&mut self) {
|
||||
self.remove()
|
||||
}
|
||||
|
||||
fn mount(
|
||||
&mut self,
|
||||
parent: &<LeptosGtk as Renderer>::Element,
|
||||
marker: Option<&<LeptosGtk as Renderer>::Node>,
|
||||
) {
|
||||
self.0
|
||||
.insert_before(&parent.0, marker.as_ref().map(|m| &m.0));
|
||||
}
|
||||
|
||||
fn insert_before_this(&self, child: &mut dyn Mountable<LeptosGtk>) -> bool {
|
||||
if let Some(parent) = self.0.parent() {
|
||||
child.mount(&Element(parent), Some(self));
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl Mountable<LeptosGtk> for Text {
|
||||
fn unmount(&mut self) {
|
||||
self.0.remove()
|
||||
}
|
||||
|
||||
fn mount(
|
||||
&mut self,
|
||||
parent: &<LeptosGtk as Renderer>::Element,
|
||||
marker: Option<&<LeptosGtk as Renderer>::Node>,
|
||||
) {
|
||||
self.0
|
||||
.0
|
||||
.insert_before(&parent.0, marker.as_ref().map(|m| &m.0));
|
||||
}
|
||||
|
||||
fn insert_before_this(&self, child: &mut dyn Mountable<LeptosGtk>) -> bool {
|
||||
self.0.insert_before_this(child)
|
||||
}
|
||||
}
|
||||
|
||||
impl CastFrom<Element> for Element {
|
||||
fn cast_from(source: Element) -> Option<Self> {
|
||||
Some(source)
|
||||
}
|
||||
}
|
||||
|
||||
impl CastFrom<Element> for Text {
|
||||
fn cast_from(source: Element) -> Option<Self> {
|
||||
source
|
||||
.0
|
||||
.downcast::<Label>()
|
||||
.ok()
|
||||
.map(|n| Text(Element::from(n)))
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<Element> for Element {
|
||||
fn as_ref(&self) -> &Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<Element> for Text {
|
||||
fn as_ref(&self) -> &Element {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Renderer for LeptosGtk {
|
||||
type Node = Element;
|
||||
type Element = Element;
|
||||
type Text = Text;
|
||||
type Placeholder = Element;
|
||||
|
||||
fn intern(text: &str) -> &str {
|
||||
text
|
||||
}
|
||||
|
||||
fn create_text_node(text: &str) -> Self::Text {
|
||||
Text(Element::from(Label::new(Some(text))))
|
||||
}
|
||||
|
||||
fn create_placeholder() -> Self::Placeholder {
|
||||
let label = Label::new(None);
|
||||
label.set_visible(false);
|
||||
Element::from(label)
|
||||
}
|
||||
|
||||
fn set_text(node: &Self::Text, text: &str) {
|
||||
let node_as_text = node.0 .0.downcast_ref::<Label>().unwrap();
|
||||
node_as_text.set_label(text);
|
||||
}
|
||||
|
||||
fn set_attribute(node: &Self::Element, name: &str, value: &str) {
|
||||
node.0.set_property(name, value);
|
||||
}
|
||||
|
||||
fn remove_attribute(node: &Self::Element, name: &str) {
|
||||
node.0.set_property(name, None::<&str>);
|
||||
}
|
||||
|
||||
fn insert_node(
|
||||
parent: &Self::Element,
|
||||
new_child: &Self::Node,
|
||||
marker: Option<&Self::Node>,
|
||||
) {
|
||||
new_child
|
||||
.0
|
||||
.insert_before(&parent.0, marker.as_ref().map(|n| &n.0));
|
||||
}
|
||||
|
||||
fn remove_node(
|
||||
parent: &Self::Element,
|
||||
child: &Self::Node,
|
||||
) -> Option<Self::Node> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn remove(node: &Self::Node) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn get_parent(node: &Self::Node) -> Option<Self::Node> {
|
||||
node.0.parent().map(Element::from)
|
||||
}
|
||||
|
||||
fn first_child(node: &Self::Node) -> Option<Self::Node> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn next_sibling(node: &Self::Node) -> Option<Self::Node> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn log_node(node: &Self::Node) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn clear_children(parent: &Self::Element) {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn root<Chil>(children: Chil) -> (Widget, impl Mountable<LeptosGtk>)
|
||||
where
|
||||
Chil: Render<LeptosGtk>,
|
||||
{
|
||||
let state = r#box()
|
||||
.orientation(Orientation::Vertical)
|
||||
.spacing(12)
|
||||
.child(children)
|
||||
.build();
|
||||
(state.as_widget().clone(), state)
|
||||
}
|
||||
|
||||
pub trait WidgetClass {
|
||||
type Widget: Into<Widget> + IsA<Object> + IsClass;
|
||||
}
|
||||
|
||||
pub struct LGtkWidget<Widg, Props, Chil> {
|
||||
widget: PhantomData<Widg>,
|
||||
properties: Props,
|
||||
children: Chil,
|
||||
}
|
||||
|
||||
impl<Widg, Props, Chil> LGtkWidget<Widg, Props, Chil>
|
||||
where
|
||||
Widg: WidgetClass,
|
||||
Chil: NextTuple,
|
||||
{
|
||||
pub fn child<T>(
|
||||
self,
|
||||
child: T,
|
||||
) -> LGtkWidget<Widg, Props, Chil::Output<T>> {
|
||||
let LGtkWidget {
|
||||
widget,
|
||||
properties,
|
||||
children,
|
||||
} = self;
|
||||
LGtkWidget {
|
||||
widget,
|
||||
properties,
|
||||
children: children.next_tuple(child),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<Widg, Props, Chil> LGtkWidget<Widg, Props, Chil>
|
||||
where
|
||||
Widg: WidgetClass,
|
||||
Props: NextTuple,
|
||||
Chil: Render<LeptosGtk>,
|
||||
{
|
||||
pub fn connect<F>(
|
||||
self,
|
||||
signal_name: &'static str,
|
||||
callback: F,
|
||||
) -> LGtkWidget<Widg, Props::Output<Connect<F>>, Chil>
|
||||
where
|
||||
F: Fn(&[Value]) -> Option<Value> + Send + Sync + 'static,
|
||||
{
|
||||
let LGtkWidget {
|
||||
widget,
|
||||
properties,
|
||||
children,
|
||||
} = self;
|
||||
LGtkWidget {
|
||||
widget,
|
||||
properties: properties.next_tuple(Connect {
|
||||
signal_name,
|
||||
callback,
|
||||
}),
|
||||
children,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LGtkWidgetState<Widg, Props, Chil>
|
||||
where
|
||||
Chil: Render<LeptosGtk>,
|
||||
Props: Property,
|
||||
Widg: WidgetClass,
|
||||
{
|
||||
ty: PhantomData<Widg>,
|
||||
widget: Element,
|
||||
properties: Props::State,
|
||||
children: Chil::State,
|
||||
}
|
||||
|
||||
impl<Widg, Props, Chil> LGtkWidgetState<Widg, Props, Chil>
|
||||
where
|
||||
Chil: Render<LeptosGtk>,
|
||||
Props: Property,
|
||||
Widg: WidgetClass,
|
||||
{
|
||||
pub fn as_widget(&self) -> &Widget {
|
||||
&self.widget.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<Widg, Props, Chil> Render<LeptosGtk> for LGtkWidget<Widg, Props, Chil>
|
||||
where
|
||||
Widg: WidgetClass,
|
||||
Props: Property,
|
||||
Chil: Render<LeptosGtk>,
|
||||
{
|
||||
type State = LGtkWidgetState<Widg, Props, Chil>;
|
||||
|
||||
fn build(self) -> Self::State {
|
||||
let widget = Object::new::<Widg::Widget>();
|
||||
let widget = Element::from(widget);
|
||||
let properties = self.properties.build(&widget);
|
||||
let mut children = self.children.build();
|
||||
children.mount(&widget, None);
|
||||
LGtkWidgetState {
|
||||
ty: PhantomData,
|
||||
widget,
|
||||
properties,
|
||||
children,
|
||||
}
|
||||
}
|
||||
|
||||
fn rebuild(self, state: &mut Self::State) {
|
||||
self.properties
|
||||
.rebuild(&state.widget, &mut state.properties);
|
||||
self.children.rebuild(&mut state.children);
|
||||
}
|
||||
}
|
||||
|
||||
impl<Widg, Props, Chil> Mountable<LeptosGtk>
|
||||
for LGtkWidgetState<Widg, Props, Chil>
|
||||
where
|
||||
Widg: WidgetClass,
|
||||
Props: Property,
|
||||
Chil: Render<LeptosGtk>,
|
||||
{
|
||||
fn unmount(&mut self) {
|
||||
self.children.unmount();
|
||||
self.widget.remove();
|
||||
}
|
||||
|
||||
fn mount(
|
||||
&mut self,
|
||||
parent: &<LeptosGtk as Renderer>::Element,
|
||||
marker: Option<&<LeptosGtk as Renderer>::Node>,
|
||||
) {
|
||||
self.children.mount(&self.widget, None);
|
||||
LeptosGtk::insert_node(parent, &self.widget, marker);
|
||||
}
|
||||
|
||||
fn insert_before_this(&self, child: &mut dyn Mountable<LeptosGtk>) -> bool {
|
||||
self.widget.insert_before_this(child)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Property {
|
||||
type State;
|
||||
|
||||
fn build(self, element: &Element) -> Self::State;
|
||||
|
||||
fn rebuild(self, element: &Element, state: &mut Self::State);
|
||||
}
|
||||
|
||||
impl<T, F> Property for F
|
||||
where
|
||||
T: Property,
|
||||
T::State: 'static,
|
||||
F: Fn() -> T + 'static,
|
||||
{
|
||||
type State = RenderEffect<T::State>;
|
||||
|
||||
fn build(self, widget: &Element) -> Self::State {
|
||||
let widget = widget.clone();
|
||||
RenderEffect::new(move |prev| {
|
||||
let value = self();
|
||||
if let Some(mut prev) = prev {
|
||||
value.rebuild(&widget, &mut prev);
|
||||
prev
|
||||
} else {
|
||||
value.build(&widget)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn rebuild(self, widget: &Element, state: &mut Self::State) {}
|
||||
}
|
||||
|
||||
pub fn button() -> LGtkWidget<gtk::Button, (), ()> {
|
||||
LGtkWidget {
|
||||
widget: PhantomData,
|
||||
properties: (),
|
||||
children: (),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn r#box() -> LGtkWidget<gtk::Box, (), ()> {
|
||||
LGtkWidget {
|
||||
widget: PhantomData,
|
||||
properties: (),
|
||||
children: (),
|
||||
}
|
||||
}
|
||||
|
||||
mod widgets {
|
||||
use super::WidgetClass;
|
||||
|
||||
impl WidgetClass for gtk::Button {
|
||||
type Widget = Self;
|
||||
}
|
||||
|
||||
impl WidgetClass for gtk::Box {
|
||||
type Widget = Self;
|
||||
}
|
||||
}
|
||||
|
||||
pub mod properties {
|
||||
use super::{
|
||||
Element, LGtkWidget, LGtkWidgetState, LeptosGtk, Property, WidgetClass,
|
||||
};
|
||||
use gtk::glib::{object::ObjectExt, Value};
|
||||
use leptos::tachys::{renderer::Renderer, view::Render};
|
||||
use next_tuple::NextTuple;
|
||||
|
||||
pub struct Connect<F>
|
||||
where
|
||||
F: Fn(&[Value]) -> Option<Value> + Send + Sync + 'static,
|
||||
{
|
||||
pub signal_name: &'static str,
|
||||
pub callback: F,
|
||||
}
|
||||
|
||||
impl<F> Property for Connect<F>
|
||||
where
|
||||
F: Fn(&[Value]) -> Option<Value> + Send + Sync + 'static,
|
||||
{
|
||||
type State = ();
|
||||
|
||||
fn build(self, element: &Element) -> Self::State {
|
||||
element.0.connect(self.signal_name, false, self.callback);
|
||||
}
|
||||
|
||||
fn rebuild(self, element: &Element, state: &mut Self::State) {}
|
||||
}
|
||||
|
||||
/* examples for macro */
|
||||
pub struct Orientation {
|
||||
value: gtk::Orientation,
|
||||
}
|
||||
|
||||
pub struct OrientationState {
|
||||
value: gtk::Orientation,
|
||||
}
|
||||
|
||||
impl Property for Orientation {
|
||||
type State = OrientationState;
|
||||
|
||||
fn build(self, element: &Element) -> Self::State {
|
||||
element.0.set_property("orientation", self.value);
|
||||
OrientationState { value: self.value }
|
||||
}
|
||||
|
||||
fn rebuild(self, element: &Element, state: &mut Self::State) {
|
||||
if self.value != state.value {
|
||||
element.0.set_property("orientation", self.value);
|
||||
state.value = self.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Widg, Props, Chil> LGtkWidget<Widg, Props, Chil>
|
||||
where
|
||||
Widg: WidgetClass,
|
||||
Props: NextTuple,
|
||||
Chil: Render<LeptosGtk>,
|
||||
{
|
||||
pub fn orientation(
|
||||
self,
|
||||
value: impl Into<gtk::Orientation>,
|
||||
) -> LGtkWidget<Widg, Props::Output<Orientation>, Chil> {
|
||||
let LGtkWidget {
|
||||
widget,
|
||||
properties,
|
||||
children,
|
||||
} = self;
|
||||
LGtkWidget {
|
||||
widget,
|
||||
properties: properties.next_tuple(Orientation {
|
||||
value: value.into(),
|
||||
}),
|
||||
children,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Spacing {
|
||||
value: i32,
|
||||
}
|
||||
|
||||
pub struct SpacingState {
|
||||
value: i32,
|
||||
}
|
||||
|
||||
impl Property for Spacing {
|
||||
type State = SpacingState;
|
||||
|
||||
fn build(self, element: &Element) -> Self::State {
|
||||
element.0.set_property("spacing", self.value);
|
||||
SpacingState { value: self.value }
|
||||
}
|
||||
|
||||
fn rebuild(self, element: &Element, state: &mut Self::State) {
|
||||
if self.value != state.value {
|
||||
element.0.set_property("spacing", self.value);
|
||||
state.value = self.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Widg, Props, Chil> LGtkWidget<Widg, Props, Chil>
|
||||
where
|
||||
Widg: WidgetClass,
|
||||
Props: NextTuple,
|
||||
Chil: Render<LeptosGtk>,
|
||||
{
|
||||
pub fn spacing(
|
||||
self,
|
||||
value: impl Into<i32>,
|
||||
) -> LGtkWidget<Widg, Props::Output<Spacing>, Chil> {
|
||||
let LGtkWidget {
|
||||
widget,
|
||||
properties,
|
||||
children,
|
||||
} = self;
|
||||
LGtkWidget {
|
||||
widget,
|
||||
properties: properties.next_tuple(Spacing {
|
||||
value: value.into(),
|
||||
}),
|
||||
children,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* end examples for properties macro */
|
||||
|
||||
pub struct Label {
|
||||
value: String,
|
||||
}
|
||||
|
||||
impl Label {
|
||||
pub fn new(value: impl Into<String>) -> Self {
|
||||
Self {
|
||||
value: value.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LabelState {
|
||||
value: String,
|
||||
}
|
||||
|
||||
impl Property for Label {
|
||||
type State = LabelState;
|
||||
|
||||
fn build(self, element: &Element) -> Self::State {
|
||||
LeptosGtk::set_attribute(element, "label", &self.value);
|
||||
LabelState { value: self.value }
|
||||
}
|
||||
|
||||
fn rebuild(self, element: &Element, state: &mut Self::State) {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl Property for () {
|
||||
type State = ();
|
||||
|
||||
fn build(self, _element: &Element) -> Self::State {}
|
||||
|
||||
fn rebuild(self, _element: &Element, _state: &mut Self::State) {}
|
||||
}
|
||||
|
||||
macro_rules! tuples {
|
||||
($($ty:ident),* $(,)?) => {
|
||||
impl<$($ty,)*> Property for ($($ty,)*)
|
||||
where $($ty: Property,)*
|
||||
{
|
||||
type State = ($($ty::State,)*);
|
||||
|
||||
fn build(self, element: &Element) -> Self::State {
|
||||
#[allow(non_snake_case)]
|
||||
let ($($ty,)*) = self;
|
||||
($($ty.build(element),)*)
|
||||
}
|
||||
|
||||
fn rebuild(self, element: &Element, state: &mut Self::State) {
|
||||
paste::paste! {
|
||||
#[allow(non_snake_case)]
|
||||
let ($($ty,)*) = self;
|
||||
#[allow(non_snake_case)]
|
||||
let ($([<state_ $ty:lower>],)*) = state;
|
||||
$($ty.rebuild(element, [<state_ $ty:lower>]));*
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tuples!(A);
|
||||
tuples!(A, B);
|
||||
tuples!(A, B, C);
|
||||
tuples!(A, B, C, D);
|
||||
tuples!(A, B, C, D, E);
|
||||
tuples!(A, B, C, D, E, F);
|
||||
tuples!(A, B, C, D, E, F, G);
|
||||
tuples!(A, B, C, D, E, F, G, H);
|
||||
tuples!(A, B, C, D, E, F, G, H, I);
|
||||
tuples!(A, B, C, D, E, F, G, H, I, J);
|
||||
tuples!(A, B, C, D, E, F, G, H, I, J, K);
|
||||
tuples!(A, B, C, D, E, F, G, H, I, J, K, L);
|
||||
tuples!(A, B, C, D, E, F, G, H, I, J, K, L, M);
|
||||
tuples!(A, B, C, D, E, F, G, H, I, J, K, L, M, N);
|
||||
tuples!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O);
|
||||
tuples!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P);
|
||||
tuples!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q);
|
||||
tuples!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R);
|
||||
tuples!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S);
|
||||
tuples!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T);
|
||||
tuples!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U);
|
||||
tuples!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V);
|
||||
tuples!(
|
||||
A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W
|
||||
);
|
||||
tuples!(
|
||||
A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X
|
||||
);
|
||||
tuples!(
|
||||
A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X,
|
||||
Y
|
||||
);
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
use any_spawner::Executor;
|
||||
use gtk::{prelude::*, Application, ApplicationWindow, Orientation};
|
||||
use leptos::prelude::*;
|
||||
use leptos_gtk::LeptosGtk;
|
||||
use std::{mem, thread, time::Duration};
|
||||
mod leptos_gtk;
|
||||
|
||||
const APP_ID: &str = "dev.leptos.Counter";
|
||||
|
||||
// Basic GTK app setup from https://gtk-rs.org/gtk4-rs/stable/latest/book/hello_world.html
|
||||
fn main() {
|
||||
// use the glib event loop to power the reactive system
|
||||
_ = Executor::init_glib();
|
||||
let app = Application::builder().application_id(APP_ID).build();
|
||||
|
||||
app.connect_startup(|_| load_css());
|
||||
|
||||
app.connect_activate(|app| {
|
||||
// Connect to "activate" signal of `app`
|
||||
let owner = Owner::new();
|
||||
let view = owner.with(ui);
|
||||
let (root, state) = leptos_gtk::root(view);
|
||||
|
||||
let window = ApplicationWindow::builder()
|
||||
.application(app)
|
||||
.title("TachyGTK")
|
||||
.child(&root)
|
||||
.build();
|
||||
// Present window
|
||||
window.present();
|
||||
mem::forget((owner, state));
|
||||
});
|
||||
|
||||
app.run();
|
||||
}
|
||||
|
||||
fn ui() -> impl Render<LeptosGtk> {
|
||||
let value = RwSignal::new(0);
|
||||
let rows = RwSignal::new(vec![1, 2, 3, 4, 5]);
|
||||
|
||||
Effect::new(move |_| {
|
||||
println!("value = {}", value.get());
|
||||
});
|
||||
|
||||
// just an example of multithreaded reactivity
|
||||
thread::spawn(move || loop {
|
||||
thread::sleep(Duration::from_millis(250));
|
||||
value.update(|n| *n += 1);
|
||||
});
|
||||
|
||||
vstack((
|
||||
hstack((
|
||||
button("-1", move || {
|
||||
println!("clicked -1");
|
||||
value.update(|n| *n -= 1);
|
||||
}),
|
||||
move || value.get().to_string(),
|
||||
button("+1", move || value.update(|n| *n += 1)),
|
||||
)),
|
||||
button("Swap", move || {
|
||||
rows.update(|items| {
|
||||
items.swap(1, 3);
|
||||
})
|
||||
}),
|
||||
hstack(rows),
|
||||
))
|
||||
}
|
||||
|
||||
fn button(
|
||||
label: impl Render<LeptosGtk>,
|
||||
callback: impl Fn() + Send + Sync + 'static,
|
||||
) -> impl Render<LeptosGtk> {
|
||||
leptos_gtk::button()
|
||||
.child(label)
|
||||
.connect("clicked", move |_| {
|
||||
callback();
|
||||
None
|
||||
})
|
||||
}
|
||||
|
||||
fn vstack(children: impl Render<LeptosGtk>) -> impl Render<LeptosGtk> {
|
||||
leptos_gtk::r#box()
|
||||
.orientation(Orientation::Vertical)
|
||||
.spacing(12)
|
||||
.child(children)
|
||||
}
|
||||
|
||||
fn hstack(children: impl Render<LeptosGtk>) -> impl Render<LeptosGtk> {
|
||||
leptos_gtk::r#box()
|
||||
.orientation(Orientation::Horizontal)
|
||||
.spacing(12)
|
||||
.child(children)
|
||||
}
|
||||
|
||||
fn load_css() {
|
||||
use gtk::{gdk::Display, CssProvider};
|
||||
|
||||
let provider = CssProvider::new();
|
||||
provider.load_from_path("style.css");
|
||||
|
||||
// Add the provider to the default screen
|
||||
gtk::style_context_add_provider_for_display(
|
||||
&Display::default().expect("Could not connect to a display."),
|
||||
&provider,
|
||||
gtk::STYLE_PROVIDER_PRIORITY_APPLICATION,
|
||||
);
|
||||
}
|
||||
@@ -4,7 +4,7 @@ mod routes;
|
||||
use leptos_meta::{provide_meta_context, Link, Meta, Stylesheet};
|
||||
use leptos_router::{
|
||||
components::{FlatRoutes, Route, Router, RoutingProgress},
|
||||
ParamSegment, StaticSegment,
|
||||
OptionalParamSegment, ParamSegment, StaticSegment,
|
||||
};
|
||||
use routes::{nav::*, stories::*, story::*, users::*};
|
||||
use std::time::Duration;
|
||||
@@ -28,9 +28,7 @@ pub fn App() -> impl IntoView {
|
||||
<FlatRoutes fallback=|| "Not found.">
|
||||
<Route path=(StaticSegment("users"), ParamSegment("id")) view=User/>
|
||||
<Route path=(StaticSegment("stories"), ParamSegment("id")) view=Story/>
|
||||
<Route path=ParamSegment("stories") view=Stories/>
|
||||
// TODO allow optional params without duplication
|
||||
<Route path=StaticSegment("") view=Stories/>
|
||||
<Route path=OptionalParamSegment("stories") view=Stories/>
|
||||
</FlatRoutes>
|
||||
</main>
|
||||
</Router>
|
||||
|
||||
@@ -56,7 +56,7 @@ async fn main() -> std::io::Result<()> {
|
||||
</html>
|
||||
}
|
||||
}})
|
||||
.service(Files::new("/", site_root))
|
||||
.service(Files::new("/", site_root.as_ref()))
|
||||
//.wrap(middleware::Compress::default())
|
||||
})
|
||||
.bind(&addr)?
|
||||
|
||||
@@ -21,10 +21,16 @@ pub fn Nav() -> impl IntoView {
|
||||
<A href="/job">
|
||||
<strong>"Jobs"</strong>
|
||||
</A>
|
||||
<a class="github" href="http://github.com/leptos-rs/leptos" target="_blank" rel="noreferrer">
|
||||
<a
|
||||
class="github"
|
||||
href="http://github.com/leptos-rs/leptos"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
"Built with Leptos"
|
||||
</a>
|
||||
</nav>
|
||||
</header>
|
||||
}
|
||||
.into_any()
|
||||
}
|
||||
|
||||
@@ -50,30 +50,42 @@ pub fn Stories() -> impl IntoView {
|
||||
<div class="news-view">
|
||||
<div class="news-list-nav">
|
||||
<span>
|
||||
{move || if page() > 1 {
|
||||
Either::Left(view! {
|
||||
<a class="page-link"
|
||||
href=move || format!("/{}?page={}", story_type(), page() - 1)
|
||||
aria-label="Previous Page"
|
||||
>
|
||||
"< prev"
|
||||
</a>
|
||||
})
|
||||
} else {
|
||||
Either::Right(view! {
|
||||
<span class="page-link disabled" aria-hidden="true">
|
||||
"< prev"
|
||||
</span>
|
||||
})
|
||||
{move || {
|
||||
if page() > 1 {
|
||||
Either::Left(
|
||||
view! {
|
||||
<a
|
||||
class="page-link"
|
||||
href=move || {
|
||||
format!("/{}?page={}", story_type(), page() - 1)
|
||||
}
|
||||
aria-label="Previous Page"
|
||||
>
|
||||
"< prev"
|
||||
</a>
|
||||
},
|
||||
)
|
||||
} else {
|
||||
Either::Right(
|
||||
view! {
|
||||
<span class="page-link disabled" aria-hidden="true">
|
||||
"< prev"
|
||||
</span>
|
||||
},
|
||||
)
|
||||
}
|
||||
}}
|
||||
|
||||
</span>
|
||||
<span>"page " {page}</span>
|
||||
<Suspense>
|
||||
<span class="page-link"
|
||||
<span
|
||||
class="page-link"
|
||||
class:disabled=hide_more_link
|
||||
aria-hidden=hide_more_link
|
||||
>
|
||||
<a href=move || format!("/{}?page={}", story_type(), page() + 1)
|
||||
<a
|
||||
href=move || format!("/{}?page={}", story_type(), page() + 1)
|
||||
aria-label="Next Page"
|
||||
>
|
||||
"more >"
|
||||
@@ -83,14 +95,10 @@ pub fn Stories() -> impl IntoView {
|
||||
</div>
|
||||
<main class="news-list">
|
||||
<div>
|
||||
<Transition
|
||||
fallback=move || view! { <p>"Loading..."</p> }
|
||||
set_pending
|
||||
>
|
||||
<Show when=move || stories.read().as_ref().map(Option::is_none).unwrap_or(false)>
|
||||
>
|
||||
<p>"Error loading stories."</p>
|
||||
</Show>
|
||||
<Transition fallback=move || view! { <p>"Loading..."</p> } set_pending>
|
||||
<Show when=move || {
|
||||
stories.read().as_ref().map(Option::is_none).unwrap_or(false)
|
||||
}>> <p>"Error loading stories."</p></Show>
|
||||
<ul>
|
||||
<For
|
||||
each=move || stories.get().unwrap_or_default().unwrap_or_default()
|
||||
@@ -105,54 +113,78 @@ pub fn Stories() -> impl IntoView {
|
||||
</main>
|
||||
</div>
|
||||
}
|
||||
.into_any()
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Story(story: api::Story) -> impl IntoView {
|
||||
view! {
|
||||
<li class="news-item">
|
||||
<li class="news-item">
|
||||
<span class="score">{story.points}</span>
|
||||
<span class="title">
|
||||
{if !story.url.starts_with("item?id=") {
|
||||
Either::Left(view! {
|
||||
<span>
|
||||
<a href=story.url target="_blank" rel="noreferrer">
|
||||
{story.title.clone()}
|
||||
</a>
|
||||
<span class="host">"("{story.domain}")"</span>
|
||||
</span>
|
||||
})
|
||||
Either::Left(
|
||||
view! {
|
||||
<span>
|
||||
<a href=story.url target="_blank" rel="noreferrer">
|
||||
{story.title.clone()}
|
||||
</a>
|
||||
<span class="host">"(" {story.domain} ")"</span>
|
||||
</span>
|
||||
},
|
||||
)
|
||||
} else {
|
||||
let title = story.title.clone();
|
||||
Either::Right(view! { <A href=format!("/stories/{}", story.id)>{title}</A> })
|
||||
}}
|
||||
|
||||
</span>
|
||||
<br />
|
||||
<br/>
|
||||
<span class="meta">
|
||||
{if story.story_type != "job" {
|
||||
Either::Left(view! {
|
||||
<span>
|
||||
{"by "}
|
||||
{story.user.map(|user| view ! { <A href=format!("/users/{user}")>{user.clone()}</A>})}
|
||||
{format!(" {} | ", story.time_ago)}
|
||||
<A href=format!("/stories/{}", story.id)>
|
||||
{if story.comments_count.unwrap_or_default() > 0 {
|
||||
format!("{} comments", story.comments_count.unwrap_or_default())
|
||||
} else {
|
||||
"discuss".into()
|
||||
}}
|
||||
</A>
|
||||
</span>
|
||||
})
|
||||
Either::Left(
|
||||
view! {
|
||||
<span>
|
||||
{"by "}
|
||||
{story
|
||||
.user
|
||||
.map(|user| {
|
||||
view! {
|
||||
<A href=format!("/users/{user}")>{user.clone()}</A>
|
||||
}
|
||||
})} {format!(" {} | ", story.time_ago)}
|
||||
<A href=format!(
|
||||
"/stories/{}",
|
||||
story.id,
|
||||
)>
|
||||
{if story.comments_count.unwrap_or_default() > 0 {
|
||||
format!(
|
||||
"{} comments",
|
||||
story.comments_count.unwrap_or_default(),
|
||||
)
|
||||
} else {
|
||||
"discuss".into()
|
||||
}}
|
||||
|
||||
</A>
|
||||
</span>
|
||||
},
|
||||
)
|
||||
} else {
|
||||
let title = story.title.clone();
|
||||
Either::Right(view! { <A href=format!("/item/{}", story.id)>{title}</A> })
|
||||
}}
|
||||
|
||||
</span>
|
||||
{(story.story_type != "link").then(|| view! {
|
||||
" "
|
||||
<span class="label">{story.story_type}</span>
|
||||
})}
|
||||
{(story.story_type != "link")
|
||||
.then(|| {
|
||||
view! {
|
||||
" "
|
||||
<span class="label">{story.story_type}</span>
|
||||
}
|
||||
})}
|
||||
|
||||
</li>
|
||||
}
|
||||
.into_any()
|
||||
}
|
||||
|
||||
@@ -28,18 +28,21 @@ pub fn Story() -> impl IntoView {
|
||||
<Meta name="description" content=story.title.clone()/>
|
||||
<div class="item-view">
|
||||
<div class="item-view-header">
|
||||
<a href=story.url target="_blank">
|
||||
<h1>{story.title}</h1>
|
||||
</a>
|
||||
<span class="host">
|
||||
"("{story.domain}")"
|
||||
</span>
|
||||
{story.user.map(|user| view! { <p class="meta">
|
||||
{story.points}
|
||||
" points | by "
|
||||
<A href=format!("/users/{user}")>{user.clone()}</A>
|
||||
{format!(" {}", story.time_ago)}
|
||||
</p>})}
|
||||
<a href=story.url target="_blank">
|
||||
<h1>{story.title}</h1>
|
||||
</a>
|
||||
<span class="host">"(" {story.domain} ")"</span>
|
||||
{story
|
||||
.user
|
||||
.map(|user| {
|
||||
view! {
|
||||
<p class="meta">
|
||||
{story.points} " points | by "
|
||||
<A href=format!("/users/{user}")>{user.clone()}</A>
|
||||
{format!(" {}", story.time_ago)}
|
||||
</p>
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
<div class="item-view-comments">
|
||||
<p class="item-view-comments-header">
|
||||
@@ -48,6 +51,7 @@ pub fn Story() -> impl IntoView {
|
||||
} else {
|
||||
"No comments yet.".into()
|
||||
}}
|
||||
|
||||
</p>
|
||||
<ul class="comment-children">
|
||||
<For
|
||||
@@ -55,7 +59,7 @@ pub fn Story() -> impl IntoView {
|
||||
key=|comment| comment.id
|
||||
let:comment
|
||||
>
|
||||
<Comment comment />
|
||||
<Comment comment/>
|
||||
</For>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -64,6 +68,7 @@ pub fn Story() -> impl IntoView {
|
||||
}
|
||||
}
|
||||
}))).build())
|
||||
.into_any()
|
||||
}
|
||||
|
||||
#[component]
|
||||
@@ -72,43 +77,65 @@ pub fn Comment(comment: api::Comment) -> impl IntoView {
|
||||
|
||||
view! {
|
||||
<li class="comment">
|
||||
<div class="by">
|
||||
<A href=format!("/users/{}", comment.user.clone().unwrap_or_default())>{comment.user.clone()}</A>
|
||||
{format!(" {}", comment.time_ago)}
|
||||
</div>
|
||||
<div class="text" inner_html=comment.content></div>
|
||||
{(!comment.comments.is_empty()).then(|| {
|
||||
view! {
|
||||
<div>
|
||||
<div class="toggle" class:open=open>
|
||||
<a on:click=move |_| set_open.update(|n| *n = !*n)>
|
||||
{
|
||||
let comments_len = comment.comments.len();
|
||||
move || if open.get() {
|
||||
"[-]".into()
|
||||
} else {
|
||||
format!("[+] {}{} collapsed", comments_len, pluralize(comments_len))
|
||||
}
|
||||
}
|
||||
</a>
|
||||
</div>
|
||||
{move || open.get().then({
|
||||
let comments = comment.comments.clone();
|
||||
move || view! {
|
||||
<ul class="comment-children">
|
||||
<For
|
||||
each=move || comments.clone()
|
||||
key=|comment| comment.id
|
||||
let:comment
|
||||
>
|
||||
<Comment comment />
|
||||
</For>
|
||||
</ul>
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
}
|
||||
})}
|
||||
<div class="by">
|
||||
<A href=format!(
|
||||
"/users/{}",
|
||||
comment.user.clone().unwrap_or_default(),
|
||||
)>{comment.user.clone()}</A>
|
||||
{format!(" {}", comment.time_ago)}
|
||||
</div>
|
||||
<div class="text" inner_html=comment.content></div>
|
||||
{(!comment.comments.is_empty())
|
||||
.then(|| {
|
||||
view! {
|
||||
<div>
|
||||
<div class="toggle" class:open=open>
|
||||
<a on:click=move |_| {
|
||||
set_open.update(|n| *n = !*n)
|
||||
}>
|
||||
|
||||
{
|
||||
let comments_len = comment.comments.len();
|
||||
move || {
|
||||
if open.get() {
|
||||
"[-]".into()
|
||||
} else {
|
||||
format!(
|
||||
"[+] {}{} collapsed",
|
||||
comments_len,
|
||||
pluralize(comments_len),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</a>
|
||||
</div>
|
||||
{move || {
|
||||
open
|
||||
.get()
|
||||
.then({
|
||||
let comments = comment.comments.clone();
|
||||
move || {
|
||||
view! {
|
||||
<ul class="comment-children">
|
||||
<For
|
||||
each=move || comments.clone()
|
||||
key=|comment| comment.id
|
||||
let:comment
|
||||
>
|
||||
<Comment comment/>
|
||||
</For>
|
||||
</ul>
|
||||
}
|
||||
}
|
||||
})
|
||||
}}
|
||||
|
||||
</div>
|
||||
}
|
||||
})}
|
||||
|
||||
</li>
|
||||
}.into_any()
|
||||
}
|
||||
|
||||
@@ -18,30 +18,48 @@ pub fn User() -> impl IntoView {
|
||||
);
|
||||
view! {
|
||||
<div class="user-view">
|
||||
<Suspense fallback=|| view! { "Loading..." }>
|
||||
{move || Suspend::new(async move { match user.await.clone() {
|
||||
None => Either::Left(view! { <h1>"User not found."</h1> }),
|
||||
Some(user) => Either::Right(view! {
|
||||
<div>
|
||||
<h1>"User: " {user.id.clone()}</h1>
|
||||
<ul class="meta">
|
||||
<li>
|
||||
<span class="label">"Created: "</span> {user.created}
|
||||
</li>
|
||||
<li>
|
||||
<span class="label">"Karma: "</span> {user.karma}
|
||||
</li>
|
||||
<li inner_html={user.about} class="about"></li>
|
||||
</ul>
|
||||
<p class="links">
|
||||
<a href=format!("https://news.ycombinator.com/submitted?id={}", user.id)>"submissions"</a>
|
||||
" | "
|
||||
<a href=format!("https://news.ycombinator.com/threads?id={}", user.id)>"comments"</a>
|
||||
</p>
|
||||
</div>
|
||||
})
|
||||
}})}
|
||||
<Suspense fallback=|| {
|
||||
view! { "Loading..." }
|
||||
}>
|
||||
{move || Suspend::new(async move {
|
||||
match user.await.clone() {
|
||||
None => Either::Left(view! { <h1>"User not found."</h1> }),
|
||||
Some(user) => {
|
||||
Either::Right(
|
||||
view! {
|
||||
<div>
|
||||
<h1>"User: " {user.id.clone()}</h1>
|
||||
<ul class="meta">
|
||||
<li>
|
||||
<span class="label">"Created: "</span>
|
||||
{user.created}
|
||||
</li>
|
||||
<li>
|
||||
<span class="label">"Karma: "</span>
|
||||
{user.karma}
|
||||
</li>
|
||||
<li inner_html=user.about class="about"></li>
|
||||
</ul>
|
||||
<p class="links">
|
||||
<a href=format!(
|
||||
"https://news.ycombinator.com/submitted?id={}",
|
||||
user.id,
|
||||
)>"submissions"</a>
|
||||
" | "
|
||||
<a href=format!(
|
||||
"https://news.ycombinator.com/threads?id={}",
|
||||
user.id,
|
||||
)>"comments"</a>
|
||||
</p>
|
||||
</div>
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
})}
|
||||
|
||||
</Suspense>
|
||||
</div>
|
||||
}
|
||||
.into_any()
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ serde = { version = "1.0", features = ["derive"] }
|
||||
tracing = "0.1.40"
|
||||
gloo-net = { version = "0.6.0", features = ["http"] }
|
||||
reqwest = { version = "0.12.5", features = ["json"] }
|
||||
axum = { version = "0.7.5", optional = true }
|
||||
axum = { version = "0.8.1", optional = true }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.5.2", features = ["fs"], optional = true }
|
||||
tokio = { version = "1.39", features = ["full"], optional = true }
|
||||
|
||||
@@ -4,7 +4,7 @@ mod routes;
|
||||
use leptos_meta::{provide_meta_context, Link, Meta, MetaTags, Stylesheet};
|
||||
use leptos_router::{
|
||||
components::{FlatRoutes, Route, Router, RoutingProgress},
|
||||
ParamSegment, StaticSegment,
|
||||
OptionalParamSegment, ParamSegment, StaticSegment,
|
||||
};
|
||||
use routes::{nav::*, stories::*, story::*, users::*};
|
||||
use std::time::Duration;
|
||||
@@ -46,9 +46,7 @@ pub fn App() -> impl IntoView {
|
||||
<FlatRoutes fallback=|| "Not found.">
|
||||
<Route path=(StaticSegment("users"), ParamSegment("id")) view=User/>
|
||||
<Route path=(StaticSegment("stories"), ParamSegment("id")) view=Story/>
|
||||
<Route path=ParamSegment("stories") view=Stories/>
|
||||
// TODO allow optional params without duplication
|
||||
<Route path=StaticSegment("") view=Stories/>
|
||||
<Route path=OptionalParamSegment("stories") view=Stories/>
|
||||
</FlatRoutes>
|
||||
</main>
|
||||
</Router>
|
||||
|
||||
@@ -12,7 +12,7 @@ lto = true
|
||||
|
||||
[dependencies]
|
||||
console_error_panic_hook = "0.1.7"
|
||||
leptos = { path = "../../leptos", features = ["experimental-islands"] }
|
||||
leptos = { path = "../../leptos", features = ["islands"] }
|
||||
leptos_axum = { path = "../../integrations/axum", optional = true }
|
||||
leptos_meta = { path = "../../meta" }
|
||||
leptos_router = { path = "../../router" }
|
||||
@@ -20,7 +20,7 @@ serde = { version = "1.0", features = ["derive"] }
|
||||
tracing = "0.1.40"
|
||||
gloo-net = { version = "0.6.0", features = ["http"] }
|
||||
reqwest = { version = "0.12.5", features = ["json"] }
|
||||
axum = { version = "0.7.5", optional = true, features = ["http2"] }
|
||||
axum = { version = "0.8.1", optional = true, features = ["http2"] }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.5.2", features = [
|
||||
"fs",
|
||||
@@ -31,7 +31,6 @@ tokio = { version = "1.39", features = ["full"], optional = true }
|
||||
http = { version = "1.1", optional = true }
|
||||
web-sys = { version = "0.3.70", features = ["AbortController", "AbortSignal"] }
|
||||
wasm-bindgen = "0.2.93"
|
||||
lazy_static = "1.5"
|
||||
rust-embed = { version = "8.5", features = [
|
||||
"axum",
|
||||
"mime_guess",
|
||||
|
||||
@@ -4,7 +4,7 @@ mod routes;
|
||||
use leptos_meta::{provide_meta_context, Link, Meta, MetaTags, Stylesheet};
|
||||
use leptos_router::{
|
||||
components::{FlatRoutes, Route, Router},
|
||||
ParamSegment, StaticSegment,
|
||||
OptionalParamSegment, ParamSegment, StaticSegment,
|
||||
};
|
||||
use routes::{nav::*, stories::*, story::*, users::*};
|
||||
#[cfg(feature = "ssr")]
|
||||
@@ -42,9 +42,7 @@ pub fn App() -> impl IntoView {
|
||||
<FlatRoutes fallback=|| "Not found.">
|
||||
<Route path=(StaticSegment("users"), ParamSegment("id")) view=User/>
|
||||
<Route path=(StaticSegment("stories"), ParamSegment("id")) view=Story/>
|
||||
<Route path=ParamSegment("stories") view=Stories/>
|
||||
// TODO allow optional params without duplication
|
||||
<Route path=StaticSegment("") view=Stories/>
|
||||
<Route path=OptionalParamSegment("stories") view=Stories/>
|
||||
</FlatRoutes>
|
||||
</main>
|
||||
</Router>
|
||||
|
||||
@@ -23,7 +23,7 @@ serde = { version = "1.0", features = ["derive"] }
|
||||
tracing = "0.1.40"
|
||||
gloo-net = { version = "0.6.0", features = ["http"] }
|
||||
reqwest = { version = "0.12.5", features = ["json"] }
|
||||
axum = { version = "0.7.5", default-features = false, optional = true }
|
||||
axum = { version = "0.8.1", default-features = false, optional = true }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
http = { version = "1.1", optional = true }
|
||||
web-sys = { version = "0.3.70", features = [
|
||||
|
||||
@@ -4,7 +4,7 @@ mod routes;
|
||||
use leptos_meta::{provide_meta_context, Link, Meta, MetaTags, Stylesheet};
|
||||
use leptos_router::{
|
||||
components::{FlatRoutes, Route, Router, RoutingProgress},
|
||||
ParamSegment, StaticSegment,
|
||||
OptionalParamSegment, ParamSegment, StaticSegment,
|
||||
};
|
||||
use routes::{nav::*, stories::*, story::*, users::*};
|
||||
use std::time::Duration;
|
||||
@@ -46,9 +46,7 @@ pub fn App() -> impl IntoView {
|
||||
<FlatRoutes fallback=|| "Not found.">
|
||||
<Route path=(StaticSegment("users"), ParamSegment("id")) view=User/>
|
||||
<Route path=(StaticSegment("stories"), ParamSegment("id")) view=Story/>
|
||||
<Route path=ParamSegment("stories") view=Stories/>
|
||||
// TODO allow optional params without duplication
|
||||
<Route path=StaticSegment("") view=Stories/>
|
||||
<Route path=OptionalParamSegment("stories") view=Stories/>
|
||||
</FlatRoutes>
|
||||
</main>
|
||||
</Router>
|
||||
|
||||
@@ -10,15 +10,12 @@ crate-type = ["cdylib", "rlib"]
|
||||
console_error_panic_hook = "0.1.7"
|
||||
futures = "0.3.30"
|
||||
http = "1.1"
|
||||
leptos = { path = "../../leptos", features = [
|
||||
"tracing",
|
||||
"experimental-islands",
|
||||
] }
|
||||
leptos = { path = "../../leptos", features = ["tracing", "islands"] }
|
||||
server_fn = { path = "../../server_fn", features = ["serde-lite"] }
|
||||
leptos_axum = { path = "../../integrations/axum", optional = true }
|
||||
log = "0.4.22"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
axum = { version = "0.7.5", optional = true }
|
||||
axum = { version = "0.8.1", optional = true }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.5.2", features = ["fs"], optional = true }
|
||||
tokio = { version = "1.39", features = ["full"], optional = true }
|
||||
|
||||
@@ -10,10 +10,7 @@ crate-type = ["cdylib", "rlib"]
|
||||
console_error_panic_hook = "0.1.7"
|
||||
futures = "0.3.30"
|
||||
http = "1.1"
|
||||
leptos = { path = "../../leptos", features = [
|
||||
"tracing",
|
||||
"experimental-islands",
|
||||
] }
|
||||
leptos = { path = "../../leptos", features = ["tracing", "islands"] }
|
||||
leptos_router = { path = "../../router" }
|
||||
server_fn = { path = "../../server_fn", features = ["serde-lite"] }
|
||||
leptos_axum = { path = "../../integrations/axum", features = [
|
||||
@@ -21,11 +18,12 @@ leptos_axum = { path = "../../integrations/axum", features = [
|
||||
], optional = true }
|
||||
log = "0.4.22"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
axum = { version = "0.7.5", optional = true }
|
||||
axum = { version = "0.8.1", optional = true }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.5.2", features = ["fs"], optional = true }
|
||||
tokio = { version = "1.39", features = ["full"], optional = true }
|
||||
wasm-bindgen = "0.2.93"
|
||||
wasm-bindgen = "0.2.100"
|
||||
serde_json = "1.0.133"
|
||||
|
||||
[features]
|
||||
hydrate = ["leptos/hydrate"]
|
||||
@@ -58,11 +56,11 @@ site-root = "target/site"
|
||||
# Defaults to pkg
|
||||
site-pkg-dir = "pkg"
|
||||
# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css
|
||||
style-file = "./style.css"
|
||||
style-file = "style.css"
|
||||
# [Optional] Files in the asset-dir will be copied to the site-root directory
|
||||
assets-dir = "public"
|
||||
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
|
||||
site-addr = "127.0.0.1:3000"
|
||||
site-addr = "127.0.0.1:3009"
|
||||
# The port to use for automatic reload monitoring
|
||||
reload-port = 3001
|
||||
# The browserlist query used for optimizing the CSS.
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
# Leptos Todo App Sqlite with Axum
|
||||
# Work in Progress
|
||||
|
||||
This example creates a basic todo app with an Axum backend that uses Leptos' server functions to call sqlx from the client and seamlessly run it on the server.
|
||||
This example is something I wrote on a long layover in the Orlando airport in July. (It was really hot!)
|
||||
|
||||
## Getting Started
|
||||
It is the culmination of a couple years of thinking and working toward being able to do this, which you can see
|
||||
described pretty well in the pinned roadmap issue (#1830) and its discussion of different modes of client-side
|
||||
routing when you use islands.
|
||||
|
||||
See the [Examples README](../README.md) for setup and run instructions.
|
||||
This uses *only* server rendering, with no actual islands, but still maintains client-side state across page navigations.
|
||||
It does this by building on the fact that we now have a statically-typed view tree to do pretty smart updates with
|
||||
new HTML from the client, with extremely minimal diffing.
|
||||
|
||||
## E2E Testing
|
||||
|
||||
See the [E2E README](./e2e/README.md) for more information about the testing strategy.
|
||||
|
||||
## Rendering
|
||||
|
||||
See the [SSR Notes](../SSR_NOTES.md) for more information about Server Side Rendering.
|
||||
|
||||
## Quick Start
|
||||
|
||||
Run `cargo leptos watch` to run this example.
|
||||
The demo itself works, but the feature that supports it is incomplete. A couple people have accidentally
|
||||
used it and broken their applications in ways they don't understand, so I've renamed the feature to `dont-use-islands-router`.
|
||||
|
||||
2852
examples/islands_router/mock_data.json
Normal file
2852
examples/islands_router/mock_data.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,135 +0,0 @@
|
||||
window.addEventListener("click", async (ev) => {
|
||||
// confirm that this is an <a> that meets our requirements
|
||||
if (
|
||||
ev.defaultPrevented ||
|
||||
ev.button !== 0 ||
|
||||
ev.metaKey ||
|
||||
ev.altKey ||
|
||||
ev.ctrlKey ||
|
||||
ev.shiftKey
|
||||
)
|
||||
return;
|
||||
|
||||
/** @type HTMLAnchorElement | undefined;*/
|
||||
const a = ev
|
||||
.composedPath()
|
||||
.find(el => el instanceof Node && el.nodeName.toUpperCase() === "A");
|
||||
|
||||
if (!a) return;
|
||||
|
||||
const svg = a.namespaceURI === "http://www.w3.org/2000/svg";
|
||||
const href = svg ? a.href.baseVal : a.href;
|
||||
const target = svg ? a.target.baseVal : a.target;
|
||||
if (target || (!href && !a.hasAttribute("state"))) return;
|
||||
|
||||
const rel = (a.getAttribute("rel") || "").split(/\s+/);
|
||||
if (a.hasAttribute("download") || (rel && rel.includes("external"))) return;
|
||||
|
||||
const url = svg ? new URL(href, document.baseURI) : new URL(href);
|
||||
if (
|
||||
url.origin !== window.location.origin // ||
|
||||
// TODO base
|
||||
//(basePath && url.pathname && !url.pathname.toLowerCase().startsWith(basePath.toLowerCase()))
|
||||
)
|
||||
return;
|
||||
|
||||
ev.preventDefault();
|
||||
|
||||
// fetch the new page
|
||||
const resp = await fetch(url);
|
||||
const htmlString = await resp.text();
|
||||
|
||||
// Use DOMParser to parse the HTML string
|
||||
const parser = new DOMParser();
|
||||
// TODO parse from the request stream instead?
|
||||
const doc = parser.parseFromString(htmlString, 'text/html');
|
||||
|
||||
// The 'doc' variable now contains the parsed DOM
|
||||
const transition = document.startViewTransition(async () => {
|
||||
const oldDocWalker = document.createTreeWalker(document);
|
||||
const newDocWalker = doc.createTreeWalker(doc);
|
||||
let oldNode = oldDocWalker.currentNode;
|
||||
let newNode = newDocWalker.currentNode;
|
||||
while(oldDocWalker.nextNode() && newDocWalker.nextNode()) {
|
||||
oldNode = oldDocWalker.currentNode;
|
||||
newNode = newDocWalker.currentNode;
|
||||
// if the nodes are different, we need to replace the old with the new
|
||||
// because of the typed view tree, this should never actually happen
|
||||
if (oldNode.nodeType !== newNode.nodeType) {
|
||||
oldNode.replaceWith(newNode);
|
||||
}
|
||||
// if it's a text node, just update the text with the new text
|
||||
else if (oldNode.nodeType === Node.TEXT_NODE) {
|
||||
oldNode.textContent = newNode.textContent;
|
||||
}
|
||||
// if it's an element, replace if it's a different tag, or update attributes
|
||||
else if (oldNode.nodeType === Node.ELEMENT_NODE) {
|
||||
/** @type Element */
|
||||
const oldEl = oldNode;
|
||||
/** @type Element */
|
||||
const newEl = newNode;
|
||||
if (oldEl.tagName !== newEl.tagName) {
|
||||
oldEl.replaceWith(newEl);
|
||||
}
|
||||
else {
|
||||
for(const attr of newEl.attributes) {
|
||||
oldEl.setAttribute(attr.name, attr.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
// we use comment "branch marker" nodes to distinguish between different branches in the statically-typed view tree
|
||||
// if one of these marker is hit, then there are two options
|
||||
// 1) it's the same branch, and we just keep walking until the end
|
||||
// 2) it's a different branch, in which case the old can be replaced with the new wholesale
|
||||
else if (oldNode.nodeType === Node.COMMENT_NODE) {
|
||||
const oldText = oldNode.textContent;
|
||||
const newText = newNode.textContent;
|
||||
if(oldText.startsWith("bo") && newText !== oldText) {
|
||||
oldDocWalker.nextNode();
|
||||
newDocWalker.nextNode();
|
||||
const oldRange = new Range();
|
||||
const newRange = new Range();
|
||||
let oldBranches = 1;
|
||||
let newBranches = 1;
|
||||
while(oldBranches > 0 && newBranches > 0) {
|
||||
if(oldDocWalker.nextNode() && newDocWalker.nextNode()) {
|
||||
console.log(oldDocWalker.currentNode, newDocWalker.currentNode);
|
||||
if(oldDocWalker.currentNode.nodeType === Node.COMMENT_NODE) {
|
||||
if(oldDocWalker.currentNode.textContent.startsWith("bo")) {
|
||||
oldBranches += 1;
|
||||
} else if(oldDocWalker.currentNode.textContent.startsWith("bc")) {
|
||||
|
||||
oldBranches -= 1;
|
||||
}
|
||||
}
|
||||
if(newDocWalker.currentNode.nodeType === Node.COMMENT_NODE) {
|
||||
if(newDocWalker.currentNode.textContent.startsWith("bo")) {
|
||||
newBranches += 1;
|
||||
} else if(newDocWalker.currentNode.textContent.startsWith("bc")) {
|
||||
|
||||
newBranches -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
oldRange.setStartAfter(oldNode);
|
||||
oldRange.setEndBefore(oldDocWalker.currentNode);
|
||||
newRange.setStartAfter(newNode);
|
||||
newRange.setEndBefore(newDocWalker.currentNode);
|
||||
const newContents = newRange.extractContents();
|
||||
oldRange.deleteContents();
|
||||
oldRange.insertNode(newContents);
|
||||
oldNode.replaceWith(newNode);
|
||||
oldDocWalker.currentNode.replaceWith(newDocWalker.currentNode);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
} }
|
||||
}
|
||||
});
|
||||
await transition;
|
||||
window.history.pushState(undefined, null, url);
|
||||
});
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
use leptos::prelude::*;
|
||||
use leptos_router::{
|
||||
components::{FlatRoutes, Route, Router},
|
||||
StaticSegment,
|
||||
use leptos::{
|
||||
either::{Either, EitherOf3},
|
||||
prelude::*,
|
||||
};
|
||||
use leptos_router::{
|
||||
components::{Route, Router, Routes},
|
||||
hooks::{use_params_map, use_query_map},
|
||||
path,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub fn shell(options: LeptosOptions) -> impl IntoView {
|
||||
view! {
|
||||
@@ -12,7 +17,7 @@ pub fn shell(options: LeptosOptions) -> impl IntoView {
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<AutoReload options=options.clone()/>
|
||||
<HydrationScripts options=options islands=true/>
|
||||
<HydrationScripts options=options islands=true islands_router=true/>
|
||||
<link rel="stylesheet" id="leptos" href="/pkg/islands.css"/>
|
||||
<link rel="shortcut icon" type="image/ico" href="/favicon.ico"/>
|
||||
</head>
|
||||
@@ -26,34 +31,180 @@ pub fn shell(options: LeptosOptions) -> impl IntoView {
|
||||
#[component]
|
||||
pub fn App() -> impl IntoView {
|
||||
view! {
|
||||
<script src="/routing.js"></script>
|
||||
<Router>
|
||||
<header>
|
||||
<h1>"My Application"</h1>
|
||||
<h1>"My Contacts"</h1>
|
||||
</header>
|
||||
<nav>
|
||||
<a href="/">"Page A"</a>
|
||||
<a href="/b">"Page B"</a>
|
||||
<a href="/">"Home"</a>
|
||||
<a href="/about">"About"</a>
|
||||
</nav>
|
||||
<main>
|
||||
<p>
|
||||
<label>"Home Checkbox" <input type="checkbox"/></label>
|
||||
</p>
|
||||
<FlatRoutes fallback=|| "Not found.">
|
||||
<Route path=StaticSegment("") view=PageA/>
|
||||
<Route path=StaticSegment("b") view=PageB/>
|
||||
</FlatRoutes>
|
||||
<Routes fallback=|| "Not found.">
|
||||
<Route path=path!("") view=Home/>
|
||||
<Route path=path!("user/:id") view=Details/>
|
||||
<Route path=path!("about") view=About/>
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn PageA() -> impl IntoView {
|
||||
view! { <label>"Page A" <input type="checkbox"/></label> }
|
||||
#[server]
|
||||
pub async fn search(query: String) -> Result<Vec<User>, ServerFnError> {
|
||||
let users = tokio::fs::read_to_string("./mock_data.json").await?;
|
||||
let data: Vec<User> = serde_json::from_str(&users)?;
|
||||
let query = query.to_ascii_lowercase();
|
||||
Ok(data
|
||||
.into_iter()
|
||||
.filter(|user| {
|
||||
user.first_name.to_ascii_lowercase().contains(&query)
|
||||
|| user.last_name.to_ascii_lowercase().contains(&query)
|
||||
|| user.email.to_ascii_lowercase().contains(&query)
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn delete_user(id: u32) -> Result<(), ServerFnError> {
|
||||
let users = tokio::fs::read_to_string("./mock_data.json").await?;
|
||||
let mut data: Vec<User> = serde_json::from_str(&users)?;
|
||||
data.retain(|user| user.id != id);
|
||||
let new_json = serde_json::to_string(&data)?;
|
||||
tokio::fs::write("./mock_data.json", &new_json).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
pub struct User {
|
||||
id: u32,
|
||||
first_name: String,
|
||||
last_name: String,
|
||||
email: String,
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn PageB() -> impl IntoView {
|
||||
view! { <label>"Page B" <input type="checkbox"/></label> }
|
||||
pub fn Home() -> impl IntoView {
|
||||
let q = use_query_map();
|
||||
let q = move || q.read().get("q");
|
||||
let data = Resource::new(q, |q| async move {
|
||||
if let Some(q) = q {
|
||||
search(q).await
|
||||
} else {
|
||||
Ok(vec![])
|
||||
}
|
||||
});
|
||||
let delete_user_action = ServerAction::<DeleteUser>::new();
|
||||
|
||||
let view = move || {
|
||||
Suspend::new(async move {
|
||||
let users = data.await.unwrap();
|
||||
if q().is_none() {
|
||||
EitherOf3::A(view! {
|
||||
<p class="note">"Enter a search to begin viewing contacts."</p>
|
||||
})
|
||||
} else if users.is_empty() {
|
||||
EitherOf3::B(view! {
|
||||
<p class="note">"No users found matching that search."</p>
|
||||
})
|
||||
} else {
|
||||
EitherOf3::C(view! {
|
||||
<table>
|
||||
<tbody>
|
||||
<For
|
||||
each=move || users.clone()
|
||||
key=|user| user.id
|
||||
let:user
|
||||
>
|
||||
<tr>
|
||||
<td>{user.first_name}</td>
|
||||
<td>{user.last_name}</td>
|
||||
<td>{user.email}</td>
|
||||
<td>
|
||||
<a href=format!("/user/{}", user.id)>"Details"</a>
|
||||
<input type="checkbox"/>
|
||||
<ActionForm action=delete_user_action>
|
||||
<input type="hidden" name="id" value=user.id/>
|
||||
<input type="submit" value="Delete"/>
|
||||
</ActionForm>
|
||||
</td>
|
||||
</tr>
|
||||
</For>
|
||||
</tbody>
|
||||
</table>
|
||||
})
|
||||
}
|
||||
})
|
||||
};
|
||||
view! {
|
||||
<section class="page">
|
||||
<form method="GET" class="search">
|
||||
<input type="search" name="q" value=q autofocus oninput="this.form.requestSubmit()"/>
|
||||
<input type="submit"/>
|
||||
</form>
|
||||
<Suspense fallback=|| view! { <p>"Loading..."</p> }>{view}</Suspense>
|
||||
</section>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Details() -> impl IntoView {
|
||||
#[server]
|
||||
pub async fn get_user(id: u32) -> Result<Option<User>, ServerFnError> {
|
||||
let users = tokio::fs::read_to_string("./mock_data.json").await?;
|
||||
let data: Vec<User> = serde_json::from_str(&users)?;
|
||||
Ok(data.iter().find(|user| user.id == id).cloned())
|
||||
}
|
||||
let params = use_params_map();
|
||||
let id = move || {
|
||||
params
|
||||
.read()
|
||||
.get("id")
|
||||
.and_then(|id| id.parse::<u32>().ok())
|
||||
};
|
||||
let user = Resource::new(id, |id| async move {
|
||||
match id {
|
||||
None => Ok(None),
|
||||
Some(id) => get_user(id).await,
|
||||
}
|
||||
});
|
||||
|
||||
move || {
|
||||
Suspend::new(async move {
|
||||
user.await.map(|user| match user {
|
||||
None => Either::Left(view! {
|
||||
<section class="page">
|
||||
<h2>"Not found."</h2>
|
||||
<p>"Sorry — we couldn’t find that user."</p>
|
||||
</section>
|
||||
}),
|
||||
Some(user) => Either::Right(view! {
|
||||
<section class="page">
|
||||
<h2>{user.first_name} " " { user.last_name}</h2>
|
||||
<p class="email">{user.email}</p>
|
||||
</section>
|
||||
}),
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn About() -> impl IntoView {
|
||||
view! {
|
||||
<section class="page">
|
||||
<h2>"About"</h2>
|
||||
<p>"This demo is intended to show off an experimental “islands router” feature, which mimics the smooth transitions and user experience of client-side routing while minimizing the amount of code that actually runs in the browser."</p>
|
||||
<p>"By default, all the content in this application is only rendered on the server. But you can add client-side interactivity via islands like this one:"</p>
|
||||
<Counter/>
|
||||
</section>
|
||||
}
|
||||
}
|
||||
|
||||
#[island]
|
||||
pub fn Counter() -> impl IntoView {
|
||||
let count = RwSignal::new(0);
|
||||
view! {
|
||||
<button class="counter" on:click=move |_| *count.write() += 1>{count}</button>
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user