mirror of
https://github.com/radzenhq/radzen-blazor.git
synced 2026-02-04 05:35:44 +00:00
Compare commits
2690 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a2a5916e09 | ||
|
|
3f52e42661 | ||
|
|
224032d0c2 | ||
|
|
4dece2e58a | ||
|
|
7eb85d4454 | ||
|
|
bd750e6f97 | ||
|
|
ee0d4d29e1 | ||
|
|
10d2ea7c0a | ||
|
|
0ab76d53ec | ||
|
|
3dea0a5f67 | ||
|
|
41c2e04cfa | ||
|
|
d8183feb07 | ||
|
|
e49fe2bca8 | ||
|
|
67664a2816 | ||
|
|
eefbe00aec | ||
|
|
be67d5f120 | ||
|
|
fa1fe694cf | ||
|
|
9f681c0c09 | ||
|
|
18dde2849e | ||
|
|
5678774de0 | ||
|
|
f5370e1384 | ||
|
|
934d7cb104 | ||
|
|
33b201147d | ||
|
|
abe2006e15 | ||
|
|
81b5cb386d | ||
|
|
c713dd6c87 | ||
|
|
eda5c490b7 | ||
|
|
b6bc9e1257 | ||
|
|
123ffd8886 | ||
|
|
9a2c09c6e8 | ||
|
|
5b5fdde9a2 | ||
|
|
0cc50d70da | ||
|
|
c79ecddee4 | ||
|
|
e929edbd53 | ||
|
|
8de7a1ec70 | ||
|
|
0b6ae4d225 | ||
|
|
c69bb8bb2b | ||
|
|
e8dda6f946 | ||
|
|
3daebfc0c1 | ||
|
|
d5fcbb97c3 | ||
|
|
97555e7849 | ||
|
|
7eb799d2a6 | ||
|
|
d06a85a3b6 | ||
|
|
f616eadccf | ||
|
|
b69d6c193b | ||
|
|
d12908778e | ||
|
|
95c42f3266 | ||
|
|
597e6de682 | ||
|
|
406141a4f1 | ||
|
|
0db7fe63b0 | ||
|
|
e5a43ef5b6 | ||
|
|
4d9a9c9ac2 | ||
|
|
3cc1f6b994 | ||
|
|
4322361269 | ||
|
|
44d3651aa8 | ||
|
|
03423f32c8 | ||
|
|
bf8c950a8a | ||
|
|
f16ae734cc | ||
|
|
91196dca39 | ||
|
|
ea36ff5011 | ||
|
|
dad418140b | ||
|
|
d5f8877768 | ||
|
|
95f2dfa8ac | ||
|
|
d60b14c686 | ||
|
|
fabcd7b8f4 | ||
|
|
045880c234 | ||
|
|
d2071102c5 | ||
|
|
864bc8c7f9 | ||
|
|
b63b861b8a | ||
|
|
23effbb3ec | ||
|
|
c0a87c834c | ||
|
|
4d02213260 | ||
|
|
46118964ea | ||
|
|
ca52c588fd | ||
|
|
b9905e10de | ||
|
|
7ea08af177 | ||
|
|
fa59aff6d5 | ||
|
|
c90e3bedb1 | ||
|
|
62cf565a09 | ||
|
|
4503361877 | ||
|
|
f2bc7ef3ed | ||
|
|
f9505e42c4 | ||
|
|
d340df758e | ||
|
|
741ecf0955 | ||
|
|
90ab2e468e | ||
|
|
360b9d7730 | ||
|
|
bb698dbe9d | ||
|
|
5e120a7328 | ||
|
|
206daba741 | ||
|
|
79e86e8b76 | ||
|
|
398c17c75a | ||
|
|
49adc95170 | ||
|
|
2b71bd1178 | ||
|
|
c53938bc0a | ||
|
|
718827d757 | ||
|
|
b4c87e7e10 | ||
|
|
20dd0e9b2d | ||
|
|
397e2d207e | ||
|
|
29f7b6cb74 | ||
|
|
a004b55e8c | ||
|
|
221d8f0697 | ||
|
|
e7a243ab7f | ||
|
|
ce1ffe2070 | ||
|
|
32e2419b67 | ||
|
|
5f7079e526 | ||
|
|
4ffeae11e2 | ||
|
|
7621e31d38 | ||
|
|
c00c751127 | ||
|
|
a09f550b52 | ||
|
|
0aeb1cfa97 | ||
|
|
46da7e45b9 | ||
|
|
83bf448bdd | ||
|
|
5ab15fb812 | ||
|
|
0dc70f3e42 | ||
|
|
5bc86a75bc | ||
|
|
51d3df9282 | ||
|
|
31925ea099 | ||
|
|
a56e6540e8 | ||
|
|
03bccb4cb4 | ||
|
|
bba9b7b6ec | ||
|
|
5b8ea6e4b0 | ||
|
|
91676d802c | ||
|
|
395d343f69 | ||
|
|
201993ff08 | ||
|
|
f9499904f3 | ||
|
|
0a152474a2 | ||
|
|
8dc522e054 | ||
|
|
eb8209d575 | ||
|
|
96b3ac7435 | ||
|
|
42d7113896 | ||
|
|
7f19786053 | ||
|
|
52b121a9f4 | ||
|
|
d191a8223e | ||
|
|
b371b43a86 | ||
|
|
c243f384a5 | ||
|
|
e10f8d759c | ||
|
|
977e5b8cb6 | ||
|
|
b68f95a7e7 | ||
|
|
e30b01de23 | ||
|
|
68f87db33e | ||
|
|
f2bfa462a0 | ||
|
|
adffcf5229 | ||
|
|
a4f37cbcfa | ||
|
|
18d8328a9c | ||
|
|
a466578ebf | ||
|
|
c92f8833e2 | ||
|
|
2f46c58028 | ||
|
|
a1c24e346c | ||
|
|
fb7fb62a0e | ||
|
|
7378ee1a32 | ||
|
|
0bee5b4365 | ||
|
|
47bfd6f539 | ||
|
|
1b92cb7f86 | ||
|
|
2931d47d05 | ||
|
|
7668a54648 | ||
|
|
aa0aaf7c10 | ||
|
|
4b80da96ab | ||
|
|
396fa19d7b | ||
|
|
4cd235a08e | ||
|
|
edd21956c5 | ||
|
|
f842156253 | ||
|
|
ce0ce0f229 | ||
|
|
ae2e9219c3 | ||
|
|
146b374ca8 | ||
|
|
9264676407 | ||
|
|
eb74460fcf | ||
|
|
16ed582052 | ||
|
|
a7ddb81ad1 | ||
|
|
d09f3870b1 | ||
|
|
9a8e249964 | ||
|
|
c63d9cf0b9 | ||
|
|
f6648f80ec | ||
|
|
250bff7ef3 | ||
|
|
fa0b925aa9 | ||
|
|
28fe2949f2 | ||
|
|
d7f46216e9 | ||
|
|
3f0d82a427 | ||
|
|
d2811c9fba | ||
|
|
028e26ef59 | ||
|
|
4086332857 | ||
|
|
1eebfccf14 | ||
|
|
f1127f917d | ||
|
|
6cc8700bb9 | ||
|
|
3cabcd5cf9 | ||
|
|
c5aaeb67b5 | ||
|
|
47fcd54d1c | ||
|
|
5dd5cd8ae1 | ||
|
|
8ab18160a5 | ||
|
|
b70b697f3f | ||
|
|
48eb35e20d | ||
|
|
4fd899133f | ||
|
|
3b134cf8b5 | ||
|
|
86bc538601 | ||
|
|
fb00fbc850 | ||
|
|
309f9fcfa1 | ||
|
|
253aa63980 | ||
|
|
93dea13f20 | ||
|
|
3d3d1718b7 | ||
|
|
90c2a5d51f | ||
|
|
8846a229d8 | ||
|
|
acf0591adc | ||
|
|
95bee47826 | ||
|
|
b701976356 | ||
|
|
2b61941e34 | ||
|
|
1526ffa3af | ||
|
|
bb4227e537 | ||
|
|
778691405d | ||
|
|
cb7d3b91c6 | ||
|
|
fd8cf16336 | ||
|
|
2403cf222f | ||
|
|
20152dd8a6 | ||
|
|
b459207b7a | ||
|
|
8b009509c8 | ||
|
|
c456891a11 | ||
|
|
6b044d8086 | ||
|
|
3162de84ed | ||
|
|
641768240a | ||
|
|
8b7ade4b9c | ||
|
|
c7db9394c8 | ||
|
|
513e63329b | ||
|
|
cfd104385d | ||
|
|
2d9641eecf | ||
|
|
65a78125b2 | ||
|
|
28572ba4d3 | ||
|
|
28a603ca1e | ||
|
|
b9fa303f7f | ||
|
|
756dde90ab | ||
|
|
c2a396167e | ||
|
|
c1fd207723 | ||
|
|
8f1fc0a164 | ||
|
|
538ec3c744 | ||
|
|
6fed13bf12 | ||
|
|
104cc7c900 | ||
|
|
8fa699a92e | ||
|
|
1ab7059830 | ||
|
|
377b2613db | ||
|
|
bc7b0a9bdb | ||
|
|
8665a351db | ||
|
|
dd02bf8b8d | ||
|
|
487c423eef | ||
|
|
87bcfa729c | ||
|
|
0332e8c671 | ||
|
|
7264354ce6 | ||
|
|
bde3315994 | ||
|
|
7ecd08b0d8 | ||
|
|
93feb382ca | ||
|
|
186d9b3798 | ||
|
|
1f3e44819d | ||
|
|
4b942f2f45 | ||
|
|
7214cd7179 | ||
|
|
90c11b5c04 | ||
|
|
4007339d26 | ||
|
|
8bc43443a7 | ||
|
|
a389dc702b | ||
|
|
1b39fa37e0 | ||
|
|
bfa18f72fa | ||
|
|
82c2ec0c43 | ||
|
|
3810d088b5 | ||
|
|
7b34b096fe | ||
|
|
831b0bd2d1 | ||
|
|
5875057282 | ||
|
|
b78df8df2a | ||
|
|
7fa3d08e61 | ||
|
|
549303a34c | ||
|
|
9d2cbae115 | ||
|
|
d1a76922c5 | ||
|
|
648889d2d2 | ||
|
|
984e566fe2 | ||
|
|
145296ee10 | ||
|
|
91b91ca96f | ||
|
|
5ea1e9d6d5 | ||
|
|
78f83fe103 | ||
|
|
1b0ee6a757 | ||
|
|
c185853405 | ||
|
|
f50b8bceb6 | ||
|
|
559a10603a | ||
|
|
defe38daaa | ||
|
|
6acdaf0603 | ||
|
|
e9991fc995 | ||
|
|
7ad14174d7 | ||
|
|
a638387dd4 | ||
|
|
3818d0e607 | ||
|
|
3ce4f8da3a | ||
|
|
9d272a1b19 | ||
|
|
3b9224b4da | ||
|
|
b254152746 | ||
|
|
a2d796476e | ||
|
|
898e744767 | ||
|
|
9ef9c5b3de | ||
|
|
b734eeb252 | ||
|
|
92d69d9053 | ||
|
|
4879c49476 | ||
|
|
e81ea15bb0 | ||
|
|
647f174b53 | ||
|
|
d79e6a7606 | ||
|
|
3fd3916ef9 | ||
|
|
b9b73d44c2 | ||
|
|
afa7c2030c | ||
|
|
f668a7a629 | ||
|
|
958fb43ac2 | ||
|
|
9ef8f592ba | ||
|
|
e6feae6c68 | ||
|
|
9dc5810eaa | ||
|
|
c960d6d96a | ||
|
|
45d95d7b45 | ||
|
|
3d1c32a5b4 | ||
|
|
6a67580c33 | ||
|
|
dbafa0a473 | ||
|
|
d348e73b55 | ||
|
|
3f3c1bd6e3 | ||
|
|
1a3f907c62 | ||
|
|
4f8027c6f7 | ||
|
|
86cd2a976b | ||
|
|
52a935bf09 | ||
|
|
ea7c3c4a5b | ||
|
|
9639d98dc3 | ||
|
|
3bfc16b6a8 | ||
|
|
1e54619e81 | ||
|
|
fc4ac98246 | ||
|
|
534e3ee98f | ||
|
|
9b8b8f020d | ||
|
|
aea7fb8069 | ||
|
|
b933633cc0 | ||
|
|
a822eb521d | ||
|
|
b6bb19538f | ||
|
|
c0a1f86801 | ||
|
|
439d79c677 | ||
|
|
b6ee0f118e | ||
|
|
ed037ca6d7 | ||
|
|
58f6239cb2 | ||
|
|
5e7bfcb591 | ||
|
|
03cd99c43c | ||
|
|
dc1742aca6 | ||
|
|
a23142cc3f | ||
|
|
de47319b00 | ||
|
|
c9a2549018 | ||
|
|
966253f7b6 | ||
|
|
b0ce7fb3cf | ||
|
|
f1f85a6563 | ||
|
|
c10ee5e0a8 | ||
|
|
7e86d3128c | ||
|
|
4f3da0ea2e | ||
|
|
406c7d8c6b | ||
|
|
809f1192f4 | ||
|
|
ff280d27f1 | ||
|
|
c566eb95a2 | ||
|
|
393d80600c | ||
|
|
1e44165d18 | ||
|
|
03f4774810 | ||
|
|
b64e0446a3 | ||
|
|
e51cab2418 | ||
|
|
f533d106c1 | ||
|
|
2557691053 | ||
|
|
5f53f95b5d | ||
|
|
2c71574072 | ||
|
|
45d38494f2 | ||
|
|
5c03d0d14c | ||
|
|
8404294f56 | ||
|
|
7f519bb71f | ||
|
|
7a5995002d | ||
|
|
ba8fe8e830 | ||
|
|
949a5b19cc | ||
|
|
5a68a00f38 | ||
|
|
d52d91852a | ||
|
|
b8b39e1ebd | ||
|
|
e795e63d02 | ||
|
|
27dc58a90f | ||
|
|
d4679353af | ||
|
|
73a53c8829 | ||
|
|
eb51f78f8c | ||
|
|
8667443e64 | ||
|
|
0650472551 | ||
|
|
7653ea683a | ||
|
|
d633d2f424 | ||
|
|
98ec370545 | ||
|
|
51b9579d6e | ||
|
|
af42791fa2 | ||
|
|
f7e9a88575 | ||
|
|
331ac4787e | ||
|
|
4ac2e0aa49 | ||
|
|
0cb10c206a | ||
|
|
5fd031f518 | ||
|
|
48c204a7b0 | ||
|
|
e61fc1c84e | ||
|
|
fb64381af9 | ||
|
|
84954f1919 | ||
|
|
cdabe516eb | ||
|
|
e5ddb2cef9 | ||
|
|
963185b927 | ||
|
|
f7ef6a207e | ||
|
|
8088d14340 | ||
|
|
f710b782a3 | ||
|
|
b13921f9aa | ||
|
|
522191052a | ||
|
|
e9e8844342 | ||
|
|
ccbf7b24cf | ||
|
|
57f1417d4c | ||
|
|
d5318e6e88 | ||
|
|
978e3fc610 | ||
|
|
dcb5eef814 | ||
|
|
518ad838e7 | ||
|
|
ba6ced577e | ||
|
|
138253e728 | ||
|
|
ffd299962b | ||
|
|
354a147155 | ||
|
|
a623017325 | ||
|
|
654bd80abd | ||
|
|
3249591351 | ||
|
|
40f934d7a6 | ||
|
|
53717dcc46 | ||
|
|
f16a00fe2e | ||
|
|
ab1af0ce28 | ||
|
|
be444bbbc4 | ||
|
|
e161872604 | ||
|
|
e21fa015bf | ||
|
|
fb3275d89f | ||
|
|
f42e4ffc71 | ||
|
|
7b15c190d3 | ||
|
|
73c2d27620 | ||
|
|
288db125ce | ||
|
|
0effaae435 | ||
|
|
a71f1dd3c6 | ||
|
|
c08843d587 | ||
|
|
44094cd4f4 | ||
|
|
a4bd9d518c | ||
|
|
a739a6e24a | ||
|
|
955b52d54b | ||
|
|
004293008c | ||
|
|
9f26a0b512 | ||
|
|
b16f95a4ba | ||
|
|
c079531d2e | ||
|
|
773f2dc074 | ||
|
|
64d783179b | ||
|
|
3dafd6c277 | ||
|
|
fb2fdb10b2 | ||
|
|
50dedf84d5 | ||
|
|
d7b96a7dca | ||
|
|
1d1acdf7c8 | ||
|
|
440bc76f08 | ||
|
|
d2a598dbf5 | ||
|
|
931f9dab6f | ||
|
|
10ec2af398 | ||
|
|
46382ef89f | ||
|
|
c4116feda4 | ||
|
|
4c18342cf4 | ||
|
|
92f103c1dd | ||
|
|
8c9374e00a | ||
|
|
870f3d2ef9 | ||
|
|
d75b3596aa | ||
|
|
c388d84f57 | ||
|
|
782f6c9655 | ||
|
|
078d12f2a1 | ||
|
|
271e1aa26a | ||
|
|
c9c11c7ff7 | ||
|
|
75da4667b7 | ||
|
|
a2591f7f77 | ||
|
|
72b4e8bd39 | ||
|
|
740b0bc3b1 | ||
|
|
3e48b15db0 | ||
|
|
f1b8a22cc8 | ||
|
|
02b75fcb68 | ||
|
|
9b2541975f | ||
|
|
d3ff9a5b7c | ||
|
|
75acf7d132 | ||
|
|
d6a9430c20 | ||
|
|
5a89720f59 | ||
|
|
bee7133e81 | ||
|
|
8522f88d66 | ||
|
|
10ecc5c75d | ||
|
|
cdd722c975 | ||
|
|
7fd9b258aa | ||
|
|
5368d398fe | ||
|
|
30cc8c8711 | ||
|
|
f7d1fea2af | ||
|
|
ee5674bb6d | ||
|
|
5e1fa61c55 | ||
|
|
92f968f933 | ||
|
|
9eabd75864 | ||
|
|
df121e68b4 | ||
|
|
be0721fa5d | ||
|
|
d670872d73 | ||
|
|
68eb9162a9 | ||
|
|
a65ce23a10 | ||
|
|
bea9bca891 | ||
|
|
9f2c6fe8cc | ||
|
|
07e4276190 | ||
|
|
3db15c168d | ||
|
|
7e9f66ec61 | ||
|
|
c7ce4ec4e3 | ||
|
|
ad32aed204 | ||
|
|
2df9235516 | ||
|
|
a0efb0b9a4 | ||
|
|
a244741640 | ||
|
|
e3f5360a7f | ||
|
|
c280bae14f | ||
|
|
e99671ba81 | ||
|
|
73338462e5 | ||
|
|
1912e69375 | ||
|
|
60a3789e9c | ||
|
|
a89bc5a5ce | ||
|
|
08f3126f46 | ||
|
|
53e746146b | ||
|
|
1e7cc0d655 | ||
|
|
b37a81b297 | ||
|
|
6aec8118f2 | ||
|
|
756732803f | ||
|
|
a6b8abb9ce | ||
|
|
38feb54237 | ||
|
|
ee828de086 | ||
|
|
bc82638fea | ||
|
|
edb6f58f66 | ||
|
|
23b9a6e7bf | ||
|
|
0b3c6058e2 | ||
|
|
cb6572980e | ||
|
|
78e5bbb9dc | ||
|
|
12b52b3e79 | ||
|
|
e226749388 | ||
|
|
001cd0cdcd | ||
|
|
c3806348a0 | ||
|
|
c6a8d80d45 | ||
|
|
2babacd15a | ||
|
|
456ab29a34 | ||
|
|
1de201841a | ||
|
|
a5586fd4ea | ||
|
|
a14ac78957 | ||
|
|
1193ede1ca | ||
|
|
8ad75a1773 | ||
|
|
9ea1b44e64 | ||
|
|
b92ef7922d | ||
|
|
3d6eff639f | ||
|
|
011e761d7f | ||
|
|
a2fdb687a1 | ||
|
|
ff846e0cc1 | ||
|
|
df0ad0d9f1 | ||
|
|
716d1a6cba | ||
|
|
3e7468177d | ||
|
|
d4113e6715 | ||
|
|
7f7ce06d0e | ||
|
|
7cfea596fd | ||
|
|
7eae5d0f6e | ||
|
|
a390658ad9 | ||
|
|
ecc403cbbc | ||
|
|
e870ca856c | ||
|
|
e7b8fec063 | ||
|
|
7767878c4c | ||
|
|
63de20fe36 | ||
|
|
6114a9a8ed | ||
|
|
d5514ca0cd | ||
|
|
8cf0565211 | ||
|
|
32246b1d69 | ||
|
|
418dbf09ec | ||
|
|
a198d09b71 | ||
|
|
f3bb0e7b90 | ||
|
|
b3917e19a2 | ||
|
|
e7d8b7454f | ||
|
|
4d4660eed9 | ||
|
|
b80e47672d | ||
|
|
3b370841cc | ||
|
|
7ec2a9a0a5 | ||
|
|
e340a376ef | ||
|
|
a2709afc5c | ||
|
|
9c7ed95f23 | ||
|
|
524f1f99cb | ||
|
|
2ac6d7f2bb | ||
|
|
6b3fd9efae | ||
|
|
d99daa6538 | ||
|
|
13e0bba379 | ||
|
|
ed7c5bc537 | ||
|
|
03b7968b99 | ||
|
|
274d64246b | ||
|
|
4450053fd3 | ||
|
|
8c321f18e1 | ||
|
|
e015807edc | ||
|
|
4ebcd2f214 | ||
|
|
67cd8b7a61 | ||
|
|
0a802ca73b | ||
|
|
097f37bfbc | ||
|
|
524e42980a | ||
|
|
34eebc9406 | ||
|
|
65f20c232c | ||
|
|
d742fdae86 | ||
|
|
397e2baf6c | ||
|
|
4b1d951083 | ||
|
|
ab65a5a975 | ||
|
|
9947cbf47b | ||
|
|
f781ba0c6a | ||
|
|
e3c605a7a9 | ||
|
|
849e6761b8 | ||
|
|
407ef36b70 | ||
|
|
fef5ceb36d | ||
|
|
2e22f556d8 | ||
|
|
95b833402f | ||
|
|
c0e7418e7c | ||
|
|
4d72ef1efe | ||
|
|
a00bce399f | ||
|
|
2b8658b233 | ||
|
|
fc2784450b | ||
|
|
896d9bd3ae | ||
|
|
9199096f69 | ||
|
|
bdb2694734 | ||
|
|
98ab3cd3d0 | ||
|
|
22f7d3f9f4 | ||
|
|
7f27dafe32 | ||
|
|
37a1e6d4b5 | ||
|
|
91d5473bf2 | ||
|
|
b883cccb30 | ||
|
|
2ca7b3ba5f | ||
|
|
faada7ae7b | ||
|
|
3910fb778f | ||
|
|
4577d063cc | ||
|
|
1f5c70166e | ||
|
|
f920f9f08d | ||
|
|
e6272e88e6 | ||
|
|
4be24abf7f | ||
|
|
85cc05a144 | ||
|
|
7f726c4e2a | ||
|
|
5c86ffd2ac | ||
|
|
257444b640 | ||
|
|
c61d453de4 | ||
|
|
4b88b18e78 | ||
|
|
1125894e7a | ||
|
|
e8894360fa | ||
|
|
168c071ac3 | ||
|
|
d6dd67951e | ||
|
|
c7e4470a60 | ||
|
|
cb6789504a | ||
|
|
314664c4e2 | ||
|
|
58794f806c | ||
|
|
6a470a6448 | ||
|
|
27bf1713a0 | ||
|
|
c1e428dd3e | ||
|
|
661e5b50f8 | ||
|
|
749c2cf600 | ||
|
|
153e9e01bc | ||
|
|
c1797cc215 | ||
|
|
00eb31cd88 | ||
|
|
bd4b1e485b | ||
|
|
387eacf5ff | ||
|
|
5d00e79e0b | ||
|
|
3aac6a785d | ||
|
|
3506256dff | ||
|
|
5c49ab32e5 | ||
|
|
f128209c3a | ||
|
|
4a4254847a | ||
|
|
d21697367c | ||
|
|
1ceaab2788 | ||
|
|
0c56f20f55 | ||
|
|
261339c7e9 | ||
|
|
1d1335fed5 | ||
|
|
c9b5a53be3 | ||
|
|
11a037aeec | ||
|
|
0743bb5f54 | ||
|
|
9fe5163d86 | ||
|
|
acd107b1a2 | ||
|
|
1ff96df224 | ||
|
|
fb537c46d4 | ||
|
|
a588b454ae | ||
|
|
0f145800fa | ||
|
|
44dfa72f16 | ||
|
|
19152f498c | ||
|
|
056a61c9fe | ||
|
|
a0cfc5d267 | ||
|
|
eb6dbf0c67 | ||
|
|
af2083120e | ||
|
|
95d4c0e992 | ||
|
|
b14f67685a | ||
|
|
b82fa04aec | ||
|
|
93d1e8604a | ||
|
|
53d32dbcc2 | ||
|
|
da43d91b5a | ||
|
|
0694ab0777 | ||
|
|
207940426d | ||
|
|
58a204ed56 | ||
|
|
f68a2719f4 | ||
|
|
4311b019f4 | ||
|
|
c1b7ce0bcc | ||
|
|
fe0ca5403c | ||
|
|
c5c33ebfeb | ||
|
|
2c24843e4d | ||
|
|
b0e17e572c | ||
|
|
7cd5727ccc | ||
|
|
d02bafc7a6 | ||
|
|
ca4bdad32a | ||
|
|
4fab7f9180 | ||
|
|
224f54c676 | ||
|
|
fbc1dbb443 | ||
|
|
4da4e532c5 | ||
|
|
a16447faa2 | ||
|
|
a883b26dda | ||
|
|
b9937ee6ca | ||
|
|
40ed1d5841 | ||
|
|
0d3b86d06b | ||
|
|
f8464d6f23 | ||
|
|
d2995e3b6c | ||
|
|
eda37027d5 | ||
|
|
11f9e5cfa4 | ||
|
|
a59a02a6a7 | ||
|
|
26db867bd5 | ||
|
|
18a92cbaca | ||
|
|
62ac151847 | ||
|
|
0b3628e085 | ||
|
|
ffb23e7f34 | ||
|
|
678d861e22 | ||
|
|
652e08ebbe | ||
|
|
95672569c5 | ||
|
|
c9bedc9315 | ||
|
|
dbaeb39564 | ||
|
|
1e9801b0d4 | ||
|
|
eed82f2b6e | ||
|
|
a0f1545421 | ||
|
|
a69909ff23 | ||
|
|
65c99d3fbd | ||
|
|
687094fec0 | ||
|
|
757debaecc | ||
|
|
6d5dda80ee | ||
|
|
b9d9e965d5 | ||
|
|
e845df2a10 | ||
|
|
93cfc0e15d | ||
|
|
7a35bc2340 | ||
|
|
dc7eebb8ae | ||
|
|
1dfc967900 | ||
|
|
bb5217948d | ||
|
|
c2269711c5 | ||
|
|
e707b99860 | ||
|
|
ec43241ccc | ||
|
|
c45436dcf4 | ||
|
|
d41c3ee72b | ||
|
|
0058d0aa56 | ||
|
|
868638f031 | ||
|
|
a8250f17c3 | ||
|
|
c3bbc7e31c | ||
|
|
5d5565b832 | ||
|
|
5da0ff61b2 | ||
|
|
79efa87d39 | ||
|
|
a56981bc70 | ||
|
|
24f30bf18f | ||
|
|
0fa843fd78 | ||
|
|
aecee331a4 | ||
|
|
c1b7cc6d65 | ||
|
|
b8ddfa6537 | ||
|
|
fd4bd631e5 | ||
|
|
96b8901cde | ||
|
|
124033d122 | ||
|
|
cbb292b828 | ||
|
|
22b5fc2452 | ||
|
|
d549a314bc | ||
|
|
f32fe71dbc | ||
|
|
169cf14fa6 | ||
|
|
3befe007fd | ||
|
|
6d84bafa2e | ||
|
|
f3badc0ad4 | ||
|
|
df9887b629 | ||
|
|
cbecf5b954 | ||
|
|
040cccebe2 | ||
|
|
efffdf54c3 | ||
|
|
c3cb0c8968 | ||
|
|
d676c67b50 | ||
|
|
9316170c89 | ||
|
|
9101614d35 | ||
|
|
7312b2859d | ||
|
|
bf7d99ccba | ||
|
|
fb0d588cbf | ||
|
|
8e7bd6323a | ||
|
|
8bebd1933d | ||
|
|
2429b2c027 | ||
|
|
769820792f | ||
|
|
36f39b5023 | ||
|
|
bb675ee040 | ||
|
|
9b4aff7a3f | ||
|
|
e312d71286 | ||
|
|
a397c686b8 | ||
|
|
9b92005810 | ||
|
|
d4e0c8c3bb | ||
|
|
ae1ec9aaa2 | ||
|
|
e811bb9213 | ||
|
|
7de5d90c82 | ||
|
|
bf5265c15b | ||
|
|
49b111a0e9 | ||
|
|
85964d9f17 | ||
|
|
4f9075fa77 | ||
|
|
0856c9005c | ||
|
|
31d8bccfcf | ||
|
|
9213e3d3ef | ||
|
|
3665079cc9 | ||
|
|
44881e521c | ||
|
|
c136d8050b | ||
|
|
a5cc04b46f | ||
|
|
a9a8b6314b | ||
|
|
5c7be3bcfc | ||
|
|
e3893198af | ||
|
|
6a4e6dc5a1 | ||
|
|
643916f733 | ||
|
|
e964851d53 | ||
|
|
79e495dc14 | ||
|
|
13326879f0 | ||
|
|
293a871db4 | ||
|
|
4c9dc6350d | ||
|
|
4c54afdc55 | ||
|
|
d60841b040 | ||
|
|
7e640f47a5 | ||
|
|
f3d1b273f1 | ||
|
|
e80f5b85e9 | ||
|
|
1b5725caee | ||
|
|
820e8f8323 | ||
|
|
30672e52bb | ||
|
|
ac9796f50d | ||
|
|
66b7b4cad0 | ||
|
|
f417b9cc73 | ||
|
|
a304d4643c | ||
|
|
8386e5a2b5 | ||
|
|
27f57b96c0 | ||
|
|
d9a049d523 | ||
|
|
f53f724b04 | ||
|
|
a41105656a | ||
|
|
54269abfd1 | ||
|
|
e812ff1337 | ||
|
|
ae28f8f239 | ||
|
|
470f261dc0 | ||
|
|
80cfda23b6 | ||
|
|
5321c4d52b | ||
|
|
0466912f3e | ||
|
|
3d3932e657 | ||
|
|
d63117a17a | ||
|
|
ba06a72326 | ||
|
|
2671f6ca61 | ||
|
|
8dddfcbc92 | ||
|
|
ebc5793f36 | ||
|
|
4946221651 | ||
|
|
de9817a97f | ||
|
|
1218c4f385 | ||
|
|
e9cee62d03 | ||
|
|
fa7cf37eab | ||
|
|
b9ecb7556f | ||
|
|
661b38e1e6 | ||
|
|
769f4ba589 | ||
|
|
6118253ebf | ||
|
|
0f7f717330 | ||
|
|
0b16f1eb97 | ||
|
|
e733894c02 | ||
|
|
c47dcfbe27 | ||
|
|
8ae008ec9d | ||
|
|
45ef0c78a3 | ||
|
|
3ac46f239e | ||
|
|
1e106f7ada | ||
|
|
09738b2dd7 | ||
|
|
dc67a0b0fc | ||
|
|
1258bbc770 | ||
|
|
5960318a60 | ||
|
|
d4faf758f4 | ||
|
|
f88216b9a0 | ||
|
|
e21d648588 | ||
|
|
dbcbdc9bbc | ||
|
|
c5d16e8381 | ||
|
|
ac1aa6f3b1 | ||
|
|
866c222c74 | ||
|
|
06843477cb | ||
|
|
9a7a6fc6ab | ||
|
|
4986da415f | ||
|
|
cdbd20b794 | ||
|
|
49567ade65 | ||
|
|
4d857449d2 | ||
|
|
dc03621a14 | ||
|
|
b83cca17e2 | ||
|
|
343afffd40 | ||
|
|
163eac9511 | ||
|
|
167bfcb0b3 | ||
|
|
f0f94eb84c | ||
|
|
7123c9ca12 | ||
|
|
41e12d8e54 | ||
|
|
473ab14ae6 | ||
|
|
127ffd451c | ||
|
|
32bb43b66f | ||
|
|
765f5acbcc | ||
|
|
8ac137f22d | ||
|
|
10bbf6b157 | ||
|
|
390dc0605b | ||
|
|
6fda9f0691 | ||
|
|
0a1c5830d2 | ||
|
|
33efa2316d | ||
|
|
b3de9374a7 | ||
|
|
1d869e8d2b | ||
|
|
56156dd5ff | ||
|
|
35c9f55c41 | ||
|
|
e70e3559eb | ||
|
|
5957439475 | ||
|
|
322ed552ed | ||
|
|
bf848d5ef8 | ||
|
|
285057a9df | ||
|
|
4de62ffa55 | ||
|
|
f97fde1387 | ||
|
|
8a5bba625c | ||
|
|
108a209ac8 | ||
|
|
8390a4aba9 | ||
|
|
9740416e56 | ||
|
|
8cc82642f9 | ||
|
|
5f4804f4cf | ||
|
|
24dbee5d3d | ||
|
|
298f84d25e | ||
|
|
182c66f0a9 | ||
|
|
fd11c34646 | ||
|
|
6bcc7388bb | ||
|
|
60da0c0082 | ||
|
|
2b2b48bec4 | ||
|
|
d579f79399 | ||
|
|
bf85df68b3 | ||
|
|
e6e7ec49df | ||
|
|
e7801e1222 | ||
|
|
3327254022 | ||
|
|
d53d09e358 | ||
|
|
34c02ac391 | ||
|
|
cf4eb0c9cd | ||
|
|
ee6220b1fe | ||
|
|
a264638726 | ||
|
|
3e6a10a57d | ||
|
|
5258816770 | ||
|
|
fb4457f9a1 | ||
|
|
627a74038c | ||
|
|
54c297e898 | ||
|
|
a4d7201f7d | ||
|
|
b04cf69b90 | ||
|
|
ea654f0b8d | ||
|
|
9b16aaf9e4 | ||
|
|
87672de761 | ||
|
|
171a8def6d | ||
|
|
f09179f4cd | ||
|
|
03f62068b3 | ||
|
|
828b4230e3 | ||
|
|
2b23e5d472 | ||
|
|
17dbe70460 | ||
|
|
587c3a0647 | ||
|
|
83f0879b11 | ||
|
|
212b741828 | ||
|
|
ee81b608f9 | ||
|
|
ad17080ee8 | ||
|
|
6a8611c348 | ||
|
|
3ba3610b21 | ||
|
|
8deed23e01 | ||
|
|
356106263b | ||
|
|
0566244b61 | ||
|
|
17d76b4e6f | ||
|
|
9c3490f275 | ||
|
|
b1b6499d7c | ||
|
|
a36a74091f | ||
|
|
4dfbd676af | ||
|
|
f72f8b3e39 | ||
|
|
45cc6880f9 | ||
|
|
17281cd3b1 | ||
|
|
332b452b56 | ||
|
|
c647515186 | ||
|
|
6faf0ed9b9 | ||
|
|
fad024d9bb | ||
|
|
67718df278 | ||
|
|
273c3283c2 | ||
|
|
aa7306ebd1 | ||
|
|
56cb173ad5 | ||
|
|
174f0a9393 | ||
|
|
a1dbc1a6fb | ||
|
|
d9de2ee0bc | ||
|
|
cf1e0eda27 | ||
|
|
ccc82e5a61 | ||
|
|
0859f939dd | ||
|
|
519c133c0c | ||
|
|
9f62860821 | ||
|
|
238ed1ac2c | ||
|
|
cd5903b358 | ||
|
|
f78125c4fb | ||
|
|
6394241b9f | ||
|
|
d4dfc9bbab | ||
|
|
cf668be964 | ||
|
|
6ef443d724 | ||
|
|
75af217864 | ||
|
|
7a7343c3c8 | ||
|
|
18bfa8f562 | ||
|
|
a76a0815a0 | ||
|
|
332f384a65 | ||
|
|
3c5e1216c8 | ||
|
|
c7c11a1c1e | ||
|
|
0dd993c1d0 | ||
|
|
c977e9b59c | ||
|
|
81055b5500 | ||
|
|
c8df446846 | ||
|
|
97ac4621b9 | ||
|
|
9346c474cf | ||
|
|
d9a55965d3 | ||
|
|
50306343a8 | ||
|
|
08d7457620 | ||
|
|
b80a382d2f | ||
|
|
ab066bb441 | ||
|
|
4a64260d6e | ||
|
|
9390077007 | ||
|
|
baf5386e34 | ||
|
|
86e72b2f7f | ||
|
|
70bc02b12e | ||
|
|
6bce8b0195 | ||
|
|
dd4afb1d0a | ||
|
|
95493fe157 | ||
|
|
42b9ef3956 | ||
|
|
97ee7b7a7a | ||
|
|
230860f773 | ||
|
|
5f7f6d975e | ||
|
|
e7c95fdcc1 | ||
|
|
933367a685 | ||
|
|
4857307533 | ||
|
|
1ee50e42ca | ||
|
|
5bbc108331 | ||
|
|
11d62516d9 | ||
|
|
ef714a8bfc | ||
|
|
c6b8441a79 | ||
|
|
200ad86575 | ||
|
|
54fe2f260f | ||
|
|
e056cf743d | ||
|
|
8dbc747928 | ||
|
|
969b37e4e9 | ||
|
|
79a111668e | ||
|
|
9c90e3110b | ||
|
|
59237fb9c6 | ||
|
|
fc3705f019 | ||
|
|
15adbc0823 | ||
|
|
ca6472f5f2 | ||
|
|
de38322f34 | ||
|
|
ed45d30639 | ||
|
|
f99fc6f15f | ||
|
|
56aaa082da | ||
|
|
0aaa2f20bd | ||
|
|
bac589e3ec | ||
|
|
1dc7b02a3c | ||
|
|
83965afe68 | ||
|
|
0e9b110023 | ||
|
|
44f01a77fd | ||
|
|
58552b7a50 | ||
|
|
ddf3feac51 | ||
|
|
c2d59fcee1 | ||
|
|
ee3b2592aa | ||
|
|
ed79dfb220 | ||
|
|
6fbb942ad9 | ||
|
|
94a9340ee3 | ||
|
|
ef6d2b7d2b | ||
|
|
7f0b603f60 | ||
|
|
04ec31ea5c | ||
|
|
d1ff36c44a | ||
|
|
d703443469 | ||
|
|
5455a02c4d | ||
|
|
4bbb186f63 | ||
|
|
b62e8bf976 | ||
|
|
7ccc4fe33a | ||
|
|
b52d1cf1e2 | ||
|
|
6ee9db0eb8 | ||
|
|
36ac2a5bfb | ||
|
|
e1841c652a | ||
|
|
6fc2c88482 | ||
|
|
b8a13a5ba4 | ||
|
|
d36623c1fb | ||
|
|
06ab88556c | ||
|
|
b552790035 | ||
|
|
6610dd22df | ||
|
|
04dddf92d8 | ||
|
|
bedd9c97c2 | ||
|
|
81c859ba31 | ||
|
|
041e662e07 | ||
|
|
65c7c66b50 | ||
|
|
287f2762f9 | ||
|
|
3750e94ff5 | ||
|
|
e127d60345 | ||
|
|
5189298884 | ||
|
|
5a9c370b29 | ||
|
|
ec4f370f29 | ||
|
|
da194dd88d | ||
|
|
91e6b86eb3 | ||
|
|
550e2d59b4 | ||
|
|
80b7ef08c8 | ||
|
|
7c9ddf0c3f | ||
|
|
9abdb1d47b | ||
|
|
a5171dd9b4 | ||
|
|
4e7c04efbe | ||
|
|
d3c66dc750 | ||
|
|
7e14357902 | ||
|
|
92f6c2448f | ||
|
|
b05ebe8bdf | ||
|
|
0b62390a54 | ||
|
|
8c9b5f2a18 | ||
|
|
960894c329 | ||
|
|
a5786ecccf | ||
|
|
33a83928e7 | ||
|
|
149c6271b3 | ||
|
|
031d23df0c | ||
|
|
30134bf3b5 | ||
|
|
7907150f17 | ||
|
|
e5f9639d1e | ||
|
|
47bad16b54 | ||
|
|
efa944ef8c | ||
|
|
68d4cfff16 | ||
|
|
c9c6c4c5ea | ||
|
|
792b3a1267 | ||
|
|
5b64cf6925 | ||
|
|
097878b4b2 | ||
|
|
32eca9aabf | ||
|
|
7a27432885 | ||
|
|
92e4249eec | ||
|
|
6fac0aa0f5 | ||
|
|
cd2ede5ce8 | ||
|
|
ac407375ab | ||
|
|
1730effc55 | ||
|
|
21c063ac53 | ||
|
|
2ba2ef9617 | ||
|
|
cfe2424cff | ||
|
|
397fe2a0c8 | ||
|
|
e846cc532c | ||
|
|
d273f99940 | ||
|
|
f0763c2b67 | ||
|
|
48d6293ab6 | ||
|
|
0e270595df | ||
|
|
db6770159e | ||
|
|
e6bf1ed6f7 | ||
|
|
996dd11e24 | ||
|
|
4ea1f20022 | ||
|
|
a56cfc5688 | ||
|
|
d71d05374f | ||
|
|
99b82b24d0 | ||
|
|
5cfe624e25 | ||
|
|
62066e04e3 | ||
|
|
5478c014f1 | ||
|
|
aea3147454 | ||
|
|
e466de4d0e | ||
|
|
0de3cf72b8 | ||
|
|
41fa97994e | ||
|
|
d03a563e0d | ||
|
|
0f89c0b112 | ||
|
|
07ae096af2 | ||
|
|
11e4d482f4 | ||
|
|
f959e3b869 | ||
|
|
ec19bfb556 | ||
|
|
fa4edf9845 | ||
|
|
ab52430846 | ||
|
|
6045869260 | ||
|
|
c08a598658 | ||
|
|
340483bf7f | ||
|
|
87395e0ec4 | ||
|
|
694194dd01 | ||
|
|
aa6935820d | ||
|
|
57c038dca6 | ||
|
|
fe69b1419f | ||
|
|
0cf7365755 | ||
|
|
7e96f776ff | ||
|
|
e5ce3d46de | ||
|
|
f097cb3c3a | ||
|
|
23aea7b794 | ||
|
|
c2839fdc29 | ||
|
|
3fbd64338c | ||
|
|
36a2917b17 | ||
|
|
9f3fc43791 | ||
|
|
4774d2d1fd | ||
|
|
91de8a5a8f | ||
|
|
652005264b | ||
|
|
98967410cb | ||
|
|
55b2e3f1ff | ||
|
|
cf8ddc98af | ||
|
|
f259fa85a5 | ||
|
|
5ea6c7739f | ||
|
|
eac071af9d | ||
|
|
2b0f424af6 | ||
|
|
fcbfd28025 | ||
|
|
37982fe416 | ||
|
|
6824c85820 | ||
|
|
bd9f76222a | ||
|
|
349e771f74 | ||
|
|
83c2993cef | ||
|
|
dbb91fa140 | ||
|
|
849ca6803d | ||
|
|
f41288f441 | ||
|
|
9e974d5c30 | ||
|
|
62d25ee917 | ||
|
|
652a5dd27d | ||
|
|
1051bfa661 | ||
|
|
3a3c481e3b | ||
|
|
9642c58d94 | ||
|
|
72d37ab218 | ||
|
|
a2047f9498 | ||
|
|
22a94130f2 | ||
|
|
6a907ff0c2 | ||
|
|
11e0ca29d4 | ||
|
|
47cd4ef790 | ||
|
|
25572a9d57 | ||
|
|
02b94e5672 | ||
|
|
23790fefd6 | ||
|
|
3c8e4a24c6 | ||
|
|
0f754055ab | ||
|
|
ac7c2612b2 | ||
|
|
7b54ff2046 | ||
|
|
c7743ffdbe | ||
|
|
7a604d39bd | ||
|
|
7c67f8bba1 | ||
|
|
6f2ca41eb6 | ||
|
|
b26ac16201 | ||
|
|
71b7ba70d1 | ||
|
|
96bd62e0cc | ||
|
|
c2ce598d0f | ||
|
|
cf56069804 | ||
|
|
7834b535d6 | ||
|
|
4b440199a2 | ||
|
|
4facfa5c7a | ||
|
|
99ab247c06 | ||
|
|
5cf12fced5 | ||
|
|
5abc92308a | ||
|
|
ed50ca5b53 | ||
|
|
3e8eb1a6eb | ||
|
|
d360c584c1 | ||
|
|
55482bf28a | ||
|
|
bcf5b4c1b2 | ||
|
|
7098e28532 | ||
|
|
ab8aa2e1ae | ||
|
|
97fc9a8b37 | ||
|
|
5f91a4561b | ||
|
|
b5dd56f187 | ||
|
|
819f0b7d7e | ||
|
|
16399d4512 | ||
|
|
86df184463 | ||
|
|
a4a2d66b85 | ||
|
|
1fb972cf2a | ||
|
|
f1ab0e3449 | ||
|
|
5032646090 | ||
|
|
2facce9633 | ||
|
|
bce50b991e | ||
|
|
bd1a5ef046 | ||
|
|
47e9b4b587 | ||
|
|
676e6127cd | ||
|
|
7b192a4e14 | ||
|
|
e3239ed0b3 | ||
|
|
1423454191 | ||
|
|
285b6367ed | ||
|
|
2907403f82 | ||
|
|
9a735639d4 | ||
|
|
375e3c4636 | ||
|
|
ab3d7078bf | ||
|
|
3b191b917b | ||
|
|
4be129917a | ||
|
|
507f508497 | ||
|
|
4b063b3493 | ||
|
|
8e457a20bc | ||
|
|
b9716cf2a3 | ||
|
|
b05a4ab83a | ||
|
|
ca75eb651d | ||
|
|
a6329fd3c0 | ||
|
|
89e33e984b | ||
|
|
40c0b85a10 | ||
|
|
7cdb199855 | ||
|
|
8c165dc17f | ||
|
|
43fe21a87d | ||
|
|
4b7559f2ec | ||
|
|
9e026034d7 | ||
|
|
9466ccb12a | ||
|
|
f53f314f0b | ||
|
|
9b53a3a052 | ||
|
|
7f8a95646a | ||
|
|
88d452a9fa | ||
|
|
ee2e1412d6 | ||
|
|
f2ea6af1c3 | ||
|
|
76bd5d7518 | ||
|
|
70626ccb79 | ||
|
|
124fca6d1f | ||
|
|
37154cc1ef | ||
|
|
9f42ad5746 | ||
|
|
94c19c8724 | ||
|
|
c3dd9a5f14 | ||
|
|
8f2a6acb7b | ||
|
|
821817fc89 | ||
|
|
0f3097590f | ||
|
|
20826fff54 | ||
|
|
9067e1d8d1 | ||
|
|
dbf983e287 | ||
|
|
f81043133a | ||
|
|
cab9b0bd1e | ||
|
|
d9b42016dc | ||
|
|
5ee5a23b29 | ||
|
|
a1f33d93c1 | ||
|
|
1f9a19c37a | ||
|
|
9e9d371a3f | ||
|
|
b97136a606 | ||
|
|
f1df463a07 | ||
|
|
e59c335cac | ||
|
|
a9280edf97 | ||
|
|
68eca70a31 | ||
|
|
dd68c35295 | ||
|
|
57530af792 | ||
|
|
4947fa91ef | ||
|
|
c605cc77d5 | ||
|
|
6fe6b681ac | ||
|
|
08884af624 | ||
|
|
2adfbb5169 | ||
|
|
831c3c4fa4 | ||
|
|
df87b51e63 | ||
|
|
a17fceac39 | ||
|
|
f6e05a27d8 | ||
|
|
19cb4ee7a4 | ||
|
|
8d4f853432 | ||
|
|
76eaac5d31 | ||
|
|
5c02943ab3 | ||
|
|
09a38e2dcd | ||
|
|
4a772953dd | ||
|
|
be544e09cf | ||
|
|
8a817980db | ||
|
|
1dbfbe5d5f | ||
|
|
53ed0dc12d | ||
|
|
1bbff65f5d | ||
|
|
8e5e9ab517 | ||
|
|
c2d37932e1 | ||
|
|
7e7bbd4591 | ||
|
|
6d2d3c4c3b | ||
|
|
a1ba7f9e5f | ||
|
|
b89949e361 | ||
|
|
72ad111198 | ||
|
|
00b4e9a9b4 | ||
|
|
a460fe12a1 | ||
|
|
7e4fdcd9a3 | ||
|
|
0f4d06b4d4 | ||
|
|
bfbf6edc87 | ||
|
|
5027e2f03a | ||
|
|
70ea05c0ca | ||
|
|
5e9d9abe10 | ||
|
|
128495dd91 | ||
|
|
314a8c0741 | ||
|
|
8c6ca6f8f7 | ||
|
|
421bb3b701 | ||
|
|
a8e9ef49f4 | ||
|
|
2474ad8d1d | ||
|
|
dd58ac01ed | ||
|
|
fd4fc377a1 | ||
|
|
3b31cec3e7 | ||
|
|
28fc95dd1f | ||
|
|
d7dc51980a | ||
|
|
1fd1f08f33 | ||
|
|
f43ec50369 | ||
|
|
d4b6b5744d | ||
|
|
3ae9ddf0f9 | ||
|
|
4a61e5cbdb | ||
|
|
44b031af15 | ||
|
|
ad197f85de | ||
|
|
e7c2990b79 | ||
|
|
21aa33c862 | ||
|
|
3f8cd143b6 | ||
|
|
5d25fec242 | ||
|
|
8aec9f3f25 | ||
|
|
81d14cbecc | ||
|
|
6f1f4cc576 | ||
|
|
f81be0a075 | ||
|
|
2aa57404f2 | ||
|
|
d43cb4b097 | ||
|
|
7d69100d2d | ||
|
|
c04a975dba | ||
|
|
59f920445c | ||
|
|
72425b243b | ||
|
|
8af777fac4 | ||
|
|
510d94b21c | ||
|
|
476cfec461 | ||
|
|
642869ffed | ||
|
|
4e6a552689 | ||
|
|
707581f103 | ||
|
|
ef68d43acb | ||
|
|
50f15f7539 | ||
|
|
9f2ae92236 | ||
|
|
854f14a014 | ||
|
|
cdeef6aae0 | ||
|
|
5000a56620 | ||
|
|
e5438a7c37 | ||
|
|
ed984a0642 | ||
|
|
077c88b778 | ||
|
|
2077b0a35f | ||
|
|
8cbffd7c11 | ||
|
|
3bc3038c2f | ||
|
|
ee3c54ecd0 | ||
|
|
89b10a8ccf | ||
|
|
31d702c550 | ||
|
|
a5f98cb46b | ||
|
|
b88156a672 | ||
|
|
311a53e4ce | ||
|
|
bd55bb88b9 | ||
|
|
7461c5ee1f | ||
|
|
6daf5b6afa | ||
|
|
32c9678513 | ||
|
|
2f74296de6 | ||
|
|
2d32d53075 | ||
|
|
9fa389bb99 | ||
|
|
c9151306da | ||
|
|
5d11777dd8 | ||
|
|
eab9ced72f | ||
|
|
929a72be79 | ||
|
|
c62c8d338e | ||
|
|
524af22cb0 | ||
|
|
b7f891ce39 | ||
|
|
178a20c6d4 | ||
|
|
83e13ae782 | ||
|
|
dc654b89fb | ||
|
|
bee2e084ba | ||
|
|
9ea3f9fd80 | ||
|
|
5d78740f6f | ||
|
|
9cda942dea | ||
|
|
2b728114a2 | ||
|
|
6f1ab140a6 | ||
|
|
8655349f00 | ||
|
|
d75edae498 | ||
|
|
6d5970adcb | ||
|
|
40f925e660 | ||
|
|
d9aab7db19 | ||
|
|
6f7a12c8be | ||
|
|
48d7d5a0b9 | ||
|
|
d459270d7d | ||
|
|
9e7a7ad920 | ||
|
|
ffcd3aa3c2 | ||
|
|
ecd933f94a | ||
|
|
9db8365930 | ||
|
|
64187169a0 | ||
|
|
f016ba700c | ||
|
|
02076b680e | ||
|
|
a6e16ad99f | ||
|
|
3f2de363c9 | ||
|
|
511d8d0cb4 | ||
|
|
4798d6b7e5 | ||
|
|
47775d2758 | ||
|
|
789e22ea08 | ||
|
|
fb8badbfd9 | ||
|
|
1c36f33a69 | ||
|
|
58dd31611d | ||
|
|
7d9033a212 | ||
|
|
564255978d | ||
|
|
cc87b603fb | ||
|
|
118a0b4214 | ||
|
|
b56580e3d1 | ||
|
|
2a1b77ff6b | ||
|
|
d94a54e392 | ||
|
|
1ab5db3183 | ||
|
|
560348fcfe | ||
|
|
e9f74bfb4d | ||
|
|
13eec1c31a | ||
|
|
aa64ca58f4 | ||
|
|
018c3a1cc6 | ||
|
|
e0bf4394b7 | ||
|
|
d684a3ede2 | ||
|
|
0bb1ab04d3 | ||
|
|
f35f5ccfe4 | ||
|
|
dc58737310 | ||
|
|
f246415f27 | ||
|
|
e7badbf124 | ||
|
|
caddb16b53 | ||
|
|
0d18206ae6 | ||
|
|
71c82c0f0d | ||
|
|
cc52fdcf5a | ||
|
|
6d31991fda | ||
|
|
7988f8cc1b | ||
|
|
59366d6f33 | ||
|
|
4f7970cb0e | ||
|
|
5794b2e977 | ||
|
|
291feff903 | ||
|
|
19ce81e995 | ||
|
|
4b20d5b954 | ||
|
|
90221a0e16 | ||
|
|
085036bc93 | ||
|
|
21186d0858 | ||
|
|
22e8d15f60 | ||
|
|
f258e6d17f | ||
|
|
a6a4185585 | ||
|
|
9924a574a8 | ||
|
|
7282d02c22 | ||
|
|
1ea62c3c60 | ||
|
|
5929a37d73 | ||
|
|
f4a0ec30ec | ||
|
|
c5c1e3c1ca | ||
|
|
449c74eb66 | ||
|
|
016717c8ef | ||
|
|
75bb80d343 | ||
|
|
c04c32e876 | ||
|
|
09025e0a8f | ||
|
|
13e845a5da | ||
|
|
2f4284f806 | ||
|
|
d4af5926cb | ||
|
|
0ad2f463ab | ||
|
|
3b49f9947f | ||
|
|
514b536f57 | ||
|
|
040143f557 | ||
|
|
c75d0cc989 | ||
|
|
701ac3f85d | ||
|
|
f283cff6bc | ||
|
|
d8e62cc541 | ||
|
|
8853c8a124 | ||
|
|
027fa4119b | ||
|
|
5424375e0d | ||
|
|
6007658c72 | ||
|
|
e0916858a8 | ||
|
|
1f397978d6 | ||
|
|
518f58885e | ||
|
|
ece24fa24b | ||
|
|
6524eb2b55 | ||
|
|
b16a6b1ab1 | ||
|
|
cda2ca43dd | ||
|
|
3375d30fad | ||
|
|
6b61c462a0 | ||
|
|
226fab5904 | ||
|
|
be2791462e | ||
|
|
9b38e6cb8f | ||
|
|
bce7830b86 | ||
|
|
4e6a3311a4 | ||
|
|
f8c1c87b01 | ||
|
|
94c3ae6533 | ||
|
|
8ca1ed17d4 | ||
|
|
a645363091 | ||
|
|
7db3c29e14 | ||
|
|
4c28475c0d | ||
|
|
c509f17789 | ||
|
|
6690ea7c0e | ||
|
|
347942668d | ||
|
|
1a3643d6f8 | ||
|
|
0af64210c5 | ||
|
|
7989b8eb0e | ||
|
|
4d902a03ca | ||
|
|
8af5b826b2 | ||
|
|
953b8e4fb2 | ||
|
|
95c4af6f84 | ||
|
|
f2be35ee94 | ||
|
|
056bc1af22 | ||
|
|
46501093ce | ||
|
|
0344e3f633 | ||
|
|
7882607c6e | ||
|
|
684c02b1c5 | ||
|
|
7d8889d045 | ||
|
|
a05b293582 | ||
|
|
c37085a1c7 | ||
|
|
bb8d7e2339 | ||
|
|
84c87d2dee | ||
|
|
710566e817 | ||
|
|
a1ca96017c | ||
|
|
686868519c | ||
|
|
f16f0e7e08 | ||
|
|
f96bf404e6 | ||
|
|
6d6a80dd4d | ||
|
|
94532e8598 | ||
|
|
254449ec1a | ||
|
|
3c69021c6c | ||
|
|
68955d67bb | ||
|
|
bc1eadad26 | ||
|
|
ebe6245243 | ||
|
|
d12c624b23 | ||
|
|
11c1668d65 | ||
|
|
158c27e52b | ||
|
|
36b777da8f | ||
|
|
012010957d | ||
|
|
b0f041328d | ||
|
|
94a12d9767 | ||
|
|
06cbb3e7f8 | ||
|
|
47de40856b | ||
|
|
c044fa2932 | ||
|
|
10f2145c15 | ||
|
|
de89355b05 | ||
|
|
54f9eed786 | ||
|
|
d8e73e9cb4 | ||
|
|
ba7f07e1c8 | ||
|
|
9353015ddc | ||
|
|
780e300815 | ||
|
|
4ed6464bbe | ||
|
|
8a9ed91051 | ||
|
|
784fabc288 | ||
|
|
1a25d1b866 | ||
|
|
b4bd2c3785 | ||
|
|
c72a18c7e4 | ||
|
|
51095f4a9f | ||
|
|
f2af835a02 | ||
|
|
fa8965f7bd | ||
|
|
e4da8a26b0 | ||
|
|
45351a504f | ||
|
|
48ce3fd4d0 | ||
|
|
98825a7d5f | ||
|
|
7011688812 | ||
|
|
ea8a3049cd | ||
|
|
d2c1e7aca5 | ||
|
|
2d80e831be | ||
|
|
c83e7b2b14 | ||
|
|
c7c71bc785 | ||
|
|
8c0e27b83b | ||
|
|
d275837da5 | ||
|
|
c1ecb703bf | ||
|
|
f804b56f92 | ||
|
|
d7a04c86f3 | ||
|
|
652b76ba50 | ||
|
|
359de1a1e5 | ||
|
|
eecec3106b | ||
|
|
f233b23569 | ||
|
|
0862a81322 | ||
|
|
6421fc9519 | ||
|
|
b1ef59bff9 | ||
|
|
cd7670ad11 | ||
|
|
79fa482f70 | ||
|
|
11705f7ccd | ||
|
|
11da4794b5 | ||
|
|
5cc6ae012a | ||
|
|
938c321a54 | ||
|
|
867422a2d9 | ||
|
|
8a547877b5 | ||
|
|
3e301af80c | ||
|
|
41ad6dc339 | ||
|
|
2f17f0d4ae | ||
|
|
f1f99a1d92 | ||
|
|
ad6fbe2bf3 | ||
|
|
e0bfad4807 | ||
|
|
3572ab9452 | ||
|
|
30ed285dcf | ||
|
|
2881491cc2 | ||
|
|
96f4d3e136 | ||
|
|
9e9ab6ae13 | ||
|
|
029cad9224 | ||
|
|
0029315c2e | ||
|
|
247c3b58ec | ||
|
|
7a165adfb8 | ||
|
|
5777319bfa | ||
|
|
9219c5a1ad | ||
|
|
096295d258 | ||
|
|
aa26e0c44d | ||
|
|
8fcd0c5e4c | ||
|
|
1a26487df3 | ||
|
|
7afedf59bf | ||
|
|
e81267995d | ||
|
|
9c206629b1 | ||
|
|
1e8dcdf4ea | ||
|
|
027e120c87 | ||
|
|
09de9e5fda | ||
|
|
b1c64bbd98 | ||
|
|
7ec6f6caad | ||
|
|
85babbeb01 | ||
|
|
0006223494 | ||
|
|
5f1bf408f7 | ||
|
|
8e8fd019c3 | ||
|
|
c7b98005bb | ||
|
|
508e95b9d8 | ||
|
|
5245df3ce3 | ||
|
|
ec66dd5649 | ||
|
|
d28c3f9ba3 | ||
|
|
64705b393a | ||
|
|
42b4557de7 | ||
|
|
d089a06566 | ||
|
|
4bbfdf8e85 | ||
|
|
1e6db0f0a5 | ||
|
|
0778c150b6 | ||
|
|
30a7170f50 | ||
|
|
b9ca828522 | ||
|
|
5f73d1e57a | ||
|
|
b0f4c4b8e0 | ||
|
|
10e343b8bb | ||
|
|
0ab5630ebf | ||
|
|
86a47ad533 | ||
|
|
fecf36a7ff | ||
|
|
16b392d360 | ||
|
|
dd6709c14a | ||
|
|
69622ad25a | ||
|
|
7e6a5642c1 | ||
|
|
ed9923dbf9 | ||
|
|
bb13d0b6f9 | ||
|
|
423e00eccd | ||
|
|
90ffa50718 | ||
|
|
4f0ed25e8e | ||
|
|
3f99cc7a6d | ||
|
|
a7e9d43411 | ||
|
|
c79bc6e32f | ||
|
|
a2b18aaec4 | ||
|
|
886e96f5d6 | ||
|
|
88388fc5b1 | ||
|
|
7cd4dec4df | ||
|
|
567c43f2f0 | ||
|
|
566a2509c9 | ||
|
|
dbaad2e169 | ||
|
|
2fdf9f9a8c | ||
|
|
ebfcef12a7 | ||
|
|
a0fc26afd7 | ||
|
|
5c8c8b6502 | ||
|
|
7e38749e40 | ||
|
|
e538336c5e | ||
|
|
f14b519756 | ||
|
|
034a1b3991 | ||
|
|
a2144d21c2 | ||
|
|
4a50a3161d | ||
|
|
d983b1f329 | ||
|
|
c1d5429245 | ||
|
|
c094016c33 | ||
|
|
5caf503784 | ||
|
|
93cd9f6880 | ||
|
|
7c7f5220da | ||
|
|
09e916fe18 | ||
|
|
471d9eed35 | ||
|
|
4457385281 | ||
|
|
c37d999ff3 | ||
|
|
6813b01c20 | ||
|
|
2e6032f81b | ||
|
|
001e9989b8 | ||
|
|
45dc0a2a72 | ||
|
|
dbb511b2f9 | ||
|
|
cbd13b49ef | ||
|
|
5902a7f113 | ||
|
|
70e71fc1da | ||
|
|
91d941b663 | ||
|
|
75ca1ff498 | ||
|
|
b0e5ad3ef5 | ||
|
|
0c12ff7662 | ||
|
|
d44c6c593d | ||
|
|
cdce35ec7a | ||
|
|
20210d7d54 | ||
|
|
8ade843108 | ||
|
|
faab0e841e | ||
|
|
efae6afd5a | ||
|
|
bb8f13d57b | ||
|
|
ff1903b1cc | ||
|
|
db193bd94a | ||
|
|
0ad1d57785 | ||
|
|
0c340286ef | ||
|
|
e60d681992 | ||
|
|
fff48260be | ||
|
|
2e7752559d | ||
|
|
aa000e46ed | ||
|
|
8ed441d968 | ||
|
|
c711a0c32c | ||
|
|
2bdfe2e12a | ||
|
|
1023fd90f1 | ||
|
|
059a59c746 | ||
|
|
2363a1fbb6 | ||
|
|
26b2cc816e | ||
|
|
93fc4950df | ||
|
|
f042076cab | ||
|
|
9a6fd69885 | ||
|
|
ffbf4b07f1 | ||
|
|
88ac570031 | ||
|
|
70c5d4eb43 | ||
|
|
1899f97b1e | ||
|
|
56cbb44f94 | ||
|
|
42c6b70f2b | ||
|
|
f673a36ccd | ||
|
|
80f319311e | ||
|
|
d256d5f7da | ||
|
|
0b7737a054 | ||
|
|
89f0c01acc | ||
|
|
d66cde252d | ||
|
|
c9103bd521 | ||
|
|
d2e87b0e13 | ||
|
|
a7eb042276 | ||
|
|
114796006f | ||
|
|
38932f14cc | ||
|
|
3ee1286cf4 | ||
|
|
ffef5923c6 | ||
|
|
e670c679df | ||
|
|
06ba235947 | ||
|
|
db72bb33f2 | ||
|
|
0c700cd1f8 | ||
|
|
721b63b197 | ||
|
|
41ca477b8a | ||
|
|
9fd5220e8c | ||
|
|
65be8cd2ce | ||
|
|
4b24ed4a21 | ||
|
|
93bdb4236e | ||
|
|
db82a14837 | ||
|
|
b6cb460085 | ||
|
|
358bb18f8f | ||
|
|
bd2cc60aae | ||
|
|
f00bd31eaf | ||
|
|
191ca88daa | ||
|
|
611571d566 | ||
|
|
11550edcd0 | ||
|
|
3c51f44190 | ||
|
|
ee3fbcae78 | ||
|
|
63ad119e63 | ||
|
|
bfcae3fc70 | ||
|
|
21b261b2d3 | ||
|
|
9ac94167d2 | ||
|
|
170c782ba2 | ||
|
|
7465259340 | ||
|
|
c871e8b64e | ||
|
|
1dfaa93568 | ||
|
|
866c480634 | ||
|
|
ae6fbc1acb | ||
|
|
1cbf9c5eb8 | ||
|
|
210da6c94b | ||
|
|
5be889d169 | ||
|
|
03f992bfc5 | ||
|
|
db6afdbfbb | ||
|
|
624c562c1f | ||
|
|
4809eccb9e | ||
|
|
1f9f46175c | ||
|
|
71b16e1824 | ||
|
|
3975b2c6e3 | ||
|
|
48bcb02f82 | ||
|
|
9fd83e56e2 | ||
|
|
a40742a45c | ||
|
|
4d10e7e290 | ||
|
|
83f1a1ef8e | ||
|
|
47581eb453 | ||
|
|
045ac03837 | ||
|
|
6439845d32 | ||
|
|
0b0721b9d3 | ||
|
|
f1ba65affa | ||
|
|
e60bdbf80c | ||
|
|
2269b500f3 | ||
|
|
089f2e964c | ||
|
|
1e477f71ef | ||
|
|
430e8efd42 | ||
|
|
bddd7e5417 | ||
|
|
10deb4c51a | ||
|
|
530c3a5576 | ||
|
|
4e5cb34051 | ||
|
|
e7c5eb6bee | ||
|
|
f2cb6ed1a6 | ||
|
|
ce745204f7 | ||
|
|
8b3c9f2f72 | ||
|
|
2aa1c85972 | ||
|
|
766e5db363 | ||
|
|
c1504e104d | ||
|
|
3705b740c9 | ||
|
|
85889ee338 | ||
|
|
32254fbed8 | ||
|
|
62fbac2d19 | ||
|
|
6b02bb973f | ||
|
|
1ad15035de | ||
|
|
dd7a21010a | ||
|
|
f69ab641f7 | ||
|
|
27308b59ea | ||
|
|
f1aa9fe906 | ||
|
|
112b28342a | ||
|
|
2f9fe92b6c | ||
|
|
4a45c78512 | ||
|
|
0c78f66c21 | ||
|
|
be6f934e7b | ||
|
|
79fa11f2c9 | ||
|
|
349966a7c3 | ||
|
|
4867825367 | ||
|
|
36686afe04 | ||
|
|
9547896074 | ||
|
|
4ecb737ea7 | ||
|
|
0147f398ec | ||
|
|
3fd0420b52 | ||
|
|
decba39373 | ||
|
|
15972a9203 | ||
|
|
94c0a52824 | ||
|
|
d583f52d7e | ||
|
|
9b243f90ee | ||
|
|
18168e4577 | ||
|
|
a18e5454ce | ||
|
|
7bf16f2891 | ||
|
|
8491387a15 | ||
|
|
123ae18a64 | ||
|
|
f3a19597fc | ||
|
|
6c6ccbf07c | ||
|
|
09af9a2576 | ||
|
|
cf813d7eb4 | ||
|
|
f0ca523dfb | ||
|
|
6a08443853 | ||
|
|
030b9b0c82 | ||
|
|
8a0b511649 | ||
|
|
7853b830ae | ||
|
|
77eb7f7821 | ||
|
|
65e71ac56f | ||
|
|
a2a1bcb77b | ||
|
|
e8fadc638c | ||
|
|
507a8330b7 | ||
|
|
1dc1c99c33 | ||
|
|
0cd89585e2 | ||
|
|
c54148c291 | ||
|
|
f7d7fa5031 | ||
|
|
d959bd6c34 | ||
|
|
92240d3e8b | ||
|
|
0c634a4fa6 | ||
|
|
c8e2414a70 | ||
|
|
a6956b34db | ||
|
|
055c2ec202 | ||
|
|
1c0e03eadc | ||
|
|
f8c44cf2d3 | ||
|
|
d99eeb1e1e | ||
|
|
c42a4d9d1c | ||
|
|
2b8eb11bc3 | ||
|
|
f4f62c6edf | ||
|
|
142f75e662 | ||
|
|
1bc19921ff | ||
|
|
d901b795f7 | ||
|
|
de19e3abec | ||
|
|
ee1ab72d0a | ||
|
|
c699dd71af | ||
|
|
3452bafd4b | ||
|
|
0aabac338d | ||
|
|
a9a25a9197 | ||
|
|
aa295e9688 | ||
|
|
9cf21b1192 | ||
|
|
7efb6fc125 | ||
|
|
a050af7985 | ||
|
|
25e3fd90c5 | ||
|
|
e79384a3c4 | ||
|
|
c784af67da | ||
|
|
74d4681f33 | ||
|
|
eaa939eda3 | ||
|
|
a3b21debb0 | ||
|
|
db9168e179 | ||
|
|
d68a5106a5 | ||
|
|
5244d7af1e | ||
|
|
0ee095617e | ||
|
|
8d7d022576 | ||
|
|
098cf78753 | ||
|
|
4131811e32 | ||
|
|
260cd62329 | ||
|
|
b6b8ce55c6 | ||
|
|
1d3e5a60e2 | ||
|
|
b2397cfd54 | ||
|
|
15ef3bdadb | ||
|
|
c353e713cc | ||
|
|
1eaaad320f | ||
|
|
e0c0071d8b | ||
|
|
0a7c274f33 | ||
|
|
d4621f8b8a | ||
|
|
5b0c1fd16e | ||
|
|
6b5847f149 | ||
|
|
b227d89a01 | ||
|
|
a4191556ea | ||
|
|
a7d90c0b39 | ||
|
|
6edef4ec97 | ||
|
|
17f7ae7daa | ||
|
|
03d33f2074 | ||
|
|
8787d8ad43 | ||
|
|
69f98aa756 | ||
|
|
9620faafe1 | ||
|
|
2ba0b164fc | ||
|
|
77b9b2e8ce | ||
|
|
58be436aaa | ||
|
|
5995eb8b8e | ||
|
|
0e4f942b15 | ||
|
|
c14f773ed4 | ||
|
|
3ef08a53d1 | ||
|
|
67d32c5a96 | ||
|
|
e351793043 | ||
|
|
f0a987e7a2 | ||
|
|
c0354961f7 | ||
|
|
4c8c8dcdab | ||
|
|
cae80096e1 | ||
|
|
45d4fcc0bf | ||
|
|
da5380a472 | ||
|
|
b27544663e | ||
|
|
75c35aa9e5 | ||
|
|
ad0fd8d5c8 | ||
|
|
c60cd1fabb | ||
|
|
7cfb6258d5 | ||
|
|
ff1bc2577f | ||
|
|
07ab3621b3 | ||
|
|
0af3f3fcce | ||
|
|
cb8a70744d | ||
|
|
c6a8d3e251 | ||
|
|
d871292d39 | ||
|
|
255febcd70 | ||
|
|
c7d8f77fb4 | ||
|
|
248de4ef0f | ||
|
|
56ae5d2c33 | ||
|
|
2cc575d74b | ||
|
|
b0b68d16a4 | ||
|
|
45fd510e15 | ||
|
|
0376a58374 | ||
|
|
b1d1d2c7f8 | ||
|
|
6de14c91e4 | ||
|
|
a04cbeba78 | ||
|
|
82610e281d | ||
|
|
e9b3213d8b | ||
|
|
313ee50bf2 | ||
|
|
fca72e3fad | ||
|
|
97657da95d | ||
|
|
f32240ba65 | ||
|
|
1d8d20a7d7 | ||
|
|
5fba2685b1 | ||
|
|
2c39551560 | ||
|
|
265ca8b433 | ||
|
|
8c4b33ec17 | ||
|
|
937bf6f57a | ||
|
|
e2f142ed28 | ||
|
|
9a835690aa | ||
|
|
5f4ec1c446 | ||
|
|
7b11516520 | ||
|
|
4a6543b0c6 | ||
|
|
fa197c1b7f | ||
|
|
ab4ab7e63b | ||
|
|
b88a8e605a | ||
|
|
519d8fc64a | ||
|
|
8032fde262 | ||
|
|
02b4b0b9b6 | ||
|
|
933c820f20 | ||
|
|
8476fdb77f | ||
|
|
ae1f280062 | ||
|
|
d58dd28051 | ||
|
|
a212382da6 | ||
|
|
b1819ff755 | ||
|
|
2317305f0e | ||
|
|
da5f7e27c4 | ||
|
|
4c4dd71195 | ||
|
|
61c3d3c34a | ||
|
|
59d4e30469 | ||
|
|
e0dd34df2b | ||
|
|
41366d9376 | ||
|
|
1779b8d67e | ||
|
|
b673cbe411 | ||
|
|
1a595c5043 | ||
|
|
bce5ae88ee | ||
|
|
b81d15cdef | ||
|
|
ec6747ac09 | ||
|
|
5cd35bbfb5 | ||
|
|
1b07509e18 | ||
|
|
35211b94db | ||
|
|
0e9338a0e4 | ||
|
|
95b099d62c | ||
|
|
b295963d8d | ||
|
|
884ca797d8 | ||
|
|
3664b4acec | ||
|
|
95f33310bf | ||
|
|
a1500051b9 | ||
|
|
be1a6cc985 | ||
|
|
32525a0204 | ||
|
|
8b64c8afd3 | ||
|
|
2795413fc1 | ||
|
|
8db9fc3f17 | ||
|
|
d7c2fb8a02 | ||
|
|
6c9d09f6c3 | ||
|
|
1482fe3e1b | ||
|
|
c2f0422a68 | ||
|
|
4b3a7af308 | ||
|
|
7b2cd6e4b0 | ||
|
|
60dbe28dca | ||
|
|
3dde8c5905 | ||
|
|
9207ef357f | ||
|
|
ce7ea27255 | ||
|
|
c3eb701c56 | ||
|
|
180c1fe185 | ||
|
|
9b3138102d | ||
|
|
e43412c02f | ||
|
|
a223fad28c | ||
|
|
9ade31a813 | ||
|
|
fd91024b8d | ||
|
|
0d9526ed72 | ||
|
|
d4e51a1b51 | ||
|
|
ded26df5d0 | ||
|
|
a3ba44771f | ||
|
|
83f4ac980e | ||
|
|
5da102c058 | ||
|
|
27608327af | ||
|
|
fcbbd53dd8 | ||
|
|
76aaab319c | ||
|
|
6ea6a46c15 | ||
|
|
c0f3e62a10 | ||
|
|
5f71660c7a | ||
|
|
e6b15de5a4 | ||
|
|
bf74569906 | ||
|
|
b70f1f84be | ||
|
|
a2209e73fc | ||
|
|
f77ebe3af7 | ||
|
|
8c4370780d | ||
|
|
e4b352f1ce | ||
|
|
a679e33d54 | ||
|
|
e477ce3d9a | ||
|
|
f15d7ac445 | ||
|
|
d36af364bf | ||
|
|
b0ebcf86e6 | ||
|
|
247ec75c9e | ||
|
|
3b43bf3579 | ||
|
|
2be6d5cdef | ||
|
|
fef7526af4 | ||
|
|
88ee26ecf8 | ||
|
|
947c602cb0 | ||
|
|
fd0cf4e4e3 | ||
|
|
0d11bffa82 | ||
|
|
c9a9c48de3 | ||
|
|
fbbcb58e3f | ||
|
|
ed9dbea34a | ||
|
|
9ccca42011 | ||
|
|
d7bc92e465 | ||
|
|
5d29ebfde2 | ||
|
|
efa14f2dcd | ||
|
|
2e07802a05 | ||
|
|
34a83a46d9 | ||
|
|
9db5c68a64 | ||
|
|
5dac84c511 | ||
|
|
ce834c9b18 | ||
|
|
0af5f626bc | ||
|
|
0d68186dfe | ||
|
|
65e226d02d | ||
|
|
15d0654cd7 | ||
|
|
f7c36efab8 | ||
|
|
09e372f37b | ||
|
|
a020e06c3d | ||
|
|
650f2caf42 | ||
|
|
bfaa5a9878 | ||
|
|
7d22fc727b | ||
|
|
e78db0d549 | ||
|
|
4a610d9868 | ||
|
|
2c08ab1e52 | ||
|
|
70c26493c4 | ||
|
|
e5f45550d4 | ||
|
|
80c196d863 | ||
|
|
516ee4b762 | ||
|
|
7371e15ebb | ||
|
|
b6f6f79c00 | ||
|
|
d8dd38dcdd | ||
|
|
1384c2b477 | ||
|
|
2006a44fdd | ||
|
|
f7c04c9f7e | ||
|
|
1dbb36bd77 | ||
|
|
54cabebbfa | ||
|
|
f31c2296dd | ||
|
|
6f66fd9597 | ||
|
|
8c33bd828a | ||
|
|
068352de91 | ||
|
|
f5c166906b | ||
|
|
514772f5c2 | ||
|
|
2e0c2aa8bc | ||
|
|
bfd641f1b1 | ||
|
|
1c45af855d | ||
|
|
5d7cf75bed | ||
|
|
04ee23b7b5 | ||
|
|
99c3c71be8 | ||
|
|
0c2fb54d14 | ||
|
|
eaf875dfd5 | ||
|
|
29e61087ac | ||
|
|
ef989e7909 | ||
|
|
ab1fd464cf | ||
|
|
4a042b73b8 | ||
|
|
b416a7de09 | ||
|
|
ebec631cde | ||
|
|
cd6a751a68 | ||
|
|
f90692b526 | ||
|
|
b4ff72d424 | ||
|
|
61de002e0e | ||
|
|
6c063c856c | ||
|
|
a075a0b29b | ||
|
|
0e85e70470 | ||
|
|
0e84d1dad1 | ||
|
|
092b3bf9c2 | ||
|
|
87cf688e27 | ||
|
|
892cab3adf | ||
|
|
c547f50dc8 | ||
|
|
952bcecb12 | ||
|
|
1da97c3cd6 | ||
|
|
b9abc13364 | ||
|
|
fcd2ffa630 | ||
|
|
1f769bc7b5 | ||
|
|
e84a5ad62a | ||
|
|
5a481aeb5e | ||
|
|
24cea1d8d2 | ||
|
|
134f368953 | ||
|
|
8a6749e38b | ||
|
|
0722639001 | ||
|
|
e372dd8ddf | ||
|
|
c66022eaa8 | ||
|
|
3b873fc06c | ||
|
|
f2ac7cca28 | ||
|
|
d21d909de6 | ||
|
|
7c476ec104 | ||
|
|
3716772a06 | ||
|
|
c1100a5c62 | ||
|
|
0ae1332480 | ||
|
|
222debc335 | ||
|
|
7702ac5475 | ||
|
|
b4f4b10332 | ||
|
|
fb795a22c8 | ||
|
|
e5a4af88cc | ||
|
|
f68508a9ef | ||
|
|
1e4adf115c | ||
|
|
dbc8a2b7b2 | ||
|
|
f339bfe45d | ||
|
|
a78b88c2a4 | ||
|
|
4a338ad94e | ||
|
|
03d0e38f15 | ||
|
|
7da16b469c | ||
|
|
c80fcc5494 | ||
|
|
0286396175 | ||
|
|
edf381c914 | ||
|
|
58d85518f0 | ||
|
|
32e8ebf4f8 | ||
|
|
c8cba6ba91 | ||
|
|
fba5487231 | ||
|
|
9d81fbcbcb | ||
|
|
40c55b3515 | ||
|
|
e5aaaff32e | ||
|
|
6071b138f0 | ||
|
|
71174f5d40 | ||
|
|
bb4ca4ff9e | ||
|
|
93f42e9a67 | ||
|
|
2b6c1ec771 | ||
|
|
958546fd0c | ||
|
|
d263494229 | ||
|
|
12d51318a8 | ||
|
|
aa8861643f | ||
|
|
0f0e4e35ce | ||
|
|
79f63a794d | ||
|
|
47e01dadb7 | ||
|
|
3bfa2bd9e4 | ||
|
|
b1aac49824 | ||
|
|
16fdfa3cb1 | ||
|
|
9c43badbfc | ||
|
|
96689b8e67 | ||
|
|
95b2a56f80 | ||
|
|
6c33f6ff08 | ||
|
|
ab515920de | ||
|
|
46fd65f98a | ||
|
|
e9986c9667 | ||
|
|
fec40dd91c | ||
|
|
e8bffd8dc6 | ||
|
|
59e6271373 | ||
|
|
0dbb0b6e12 | ||
|
|
5d1c166606 | ||
|
|
366463ac4e | ||
|
|
ed5d4f5fcd | ||
|
|
787bc2243f | ||
|
|
8b5fccdd0d | ||
|
|
fa79467c19 | ||
|
|
5a4577b7ec | ||
|
|
0c82c60c03 | ||
|
|
84e33ec18a | ||
|
|
779be53910 | ||
|
|
cda6e3c204 | ||
|
|
c73aee1633 | ||
|
|
d486b8b4cf | ||
|
|
31909ba17b | ||
|
|
27dc30ca57 | ||
|
|
f5b7ddcf0e | ||
|
|
f1aac3e9e2 | ||
|
|
0f626dada2 | ||
|
|
74f4c74f47 | ||
|
|
3e93ab0fac | ||
|
|
4ac4ec58a8 | ||
|
|
af552414ac | ||
|
|
728d7c5d24 | ||
|
|
ee17b54872 | ||
|
|
373a557ad2 | ||
|
|
062eeb8ac0 | ||
|
|
473a7fc647 | ||
|
|
4ddd9dd874 | ||
|
|
05dd048410 | ||
|
|
c2c4afa094 | ||
|
|
be2567f478 | ||
|
|
e9e9c9f702 | ||
|
|
1e3d653d1c | ||
|
|
63f3f3899e | ||
|
|
08362d1bb6 | ||
|
|
f1d3d78f89 | ||
|
|
5f084b9d7c | ||
|
|
224d694ffa | ||
|
|
1be4b06d33 | ||
|
|
155c3a4b50 | ||
|
|
371f912c1d | ||
|
|
41c47fbc58 | ||
|
|
167030c15c | ||
|
|
7403a07614 | ||
|
|
fd0bad9a91 | ||
|
|
c2381ac3f5 | ||
|
|
ee18fbac19 | ||
|
|
b83ec99e4f | ||
|
|
72bd03d368 | ||
|
|
a6ee68efe8 | ||
|
|
795ca64ac0 | ||
|
|
ee26df3b68 | ||
|
|
bc0cf84297 | ||
|
|
049f846980 | ||
|
|
67308b8e3a | ||
|
|
8c9cdb28d0 | ||
|
|
dd30b30b34 | ||
|
|
6446097840 | ||
|
|
589cf2afd8 | ||
|
|
fb1928d5ec | ||
|
|
552b41f855 | ||
|
|
e2a036192a | ||
|
|
b6c5f670b3 | ||
|
|
ecac1c5b78 | ||
|
|
1d3f124845 | ||
|
|
d0b39df3e2 | ||
|
|
beb52670fa | ||
|
|
c924407569 | ||
|
|
bb72002452 | ||
|
|
78f045ddf2 | ||
|
|
ba49220e43 | ||
|
|
857bceba76 | ||
|
|
859280c2d7 | ||
|
|
e8ac5dbe8d | ||
|
|
86ec75b05e | ||
|
|
6b00fdb19e | ||
|
|
b9707ea486 | ||
|
|
f9dd71c05a | ||
|
|
0720a9b8f0 | ||
|
|
0289733f7f | ||
|
|
50ed8ad1e5 | ||
|
|
a10fabd927 | ||
|
|
0dd9fdf7e6 | ||
|
|
ade5f386b6 | ||
|
|
315b135a3b | ||
|
|
bc3d1daf11 | ||
|
|
50d738c7b6 | ||
|
|
0a23ac1401 | ||
|
|
9cbfb4ccbe | ||
|
|
7c1a76d38e | ||
|
|
b501b86656 | ||
|
|
f118cff5c0 | ||
|
|
66d7a9e520 | ||
|
|
a3424ce020 | ||
|
|
7cb72b25be | ||
|
|
f3584b02ac | ||
|
|
f2aaf9cc26 | ||
|
|
ea0bab5bd6 | ||
|
|
78be6f9839 | ||
|
|
a7df5c7fac | ||
|
|
516a6be98e | ||
|
|
7077e30d94 | ||
|
|
21b4053b8b | ||
|
|
85c634480d | ||
|
|
54eefe2295 | ||
|
|
b32dbc3ac5 | ||
|
|
4a5d089e5d | ||
|
|
22cad5a2ab | ||
|
|
acbebd7102 | ||
|
|
301ae12169 | ||
|
|
f04bdcee69 | ||
|
|
d4b78fbf87 | ||
|
|
500fbc9190 | ||
|
|
420f7e9b4e | ||
|
|
b273c6083f | ||
|
|
8877c705c5 | ||
|
|
1e30f794fe | ||
|
|
29ad82db7c | ||
|
|
a9046be00c | ||
|
|
a70efc95c6 | ||
|
|
b4b8774110 | ||
|
|
d87e03b0cf | ||
|
|
af07ccb87e | ||
|
|
dc43918a77 | ||
|
|
a04b131394 | ||
|
|
874f18fa6a | ||
|
|
fdc93e5130 | ||
|
|
6109e18fe6 | ||
|
|
27aca28523 | ||
|
|
2ac4aacee5 | ||
|
|
824a741634 | ||
|
|
819722e7da | ||
|
|
959d442690 | ||
|
|
b20868328f | ||
|
|
679c729985 | ||
|
|
bc2ca273a5 | ||
|
|
8a2612878d | ||
|
|
dd76d149ee | ||
|
|
2770d0fbec | ||
|
|
55b2c1e186 | ||
|
|
c4c291056b | ||
|
|
b80b81ada6 | ||
|
|
a0f3d44832 | ||
|
|
d9e4c00ccf | ||
|
|
c34a186eaf | ||
|
|
be27c930d3 | ||
|
|
66818872c2 | ||
|
|
cc1ac368b3 | ||
|
|
03e22098ca | ||
|
|
724b5eb1e0 | ||
|
|
45a1ce4b55 | ||
|
|
3e5cb2e30b | ||
|
|
d0a4c6aaf1 | ||
|
|
be4bcb7390 | ||
|
|
a63c3aa4c2 | ||
|
|
e8e43d97f2 | ||
|
|
9939ff4452 | ||
|
|
b69bbc899e | ||
|
|
f0cba24cc7 | ||
|
|
fd5fa0e798 | ||
|
|
1ba1d53884 | ||
|
|
00a118b901 | ||
|
|
ba2ea55884 | ||
|
|
70b4c3dd0f | ||
|
|
412e19252c | ||
|
|
73e1974583 | ||
|
|
25c57d645b | ||
|
|
a6c6021923 | ||
|
|
2886ac9d7c | ||
|
|
c121414767 | ||
|
|
35f02ef7a9 | ||
|
|
ffeb972a82 | ||
|
|
f20a74674f | ||
|
|
03396fbbe2 | ||
|
|
54a5c0805e | ||
|
|
486cb66b6c | ||
|
|
e5d7a344c7 | ||
|
|
60fe0ed25f | ||
|
|
04741c702a | ||
|
|
6e190fe903 | ||
|
|
5bb3e65eb9 | ||
|
|
c3b29ffcd1 | ||
|
|
a889c3e97b | ||
|
|
bd578ca7cd | ||
|
|
2a9a115c31 | ||
|
|
4d1848a5e1 | ||
|
|
4bd8f777e0 | ||
|
|
4654a9011b | ||
|
|
22a5dc148a | ||
|
|
88dc4c0607 | ||
|
|
0a7c47fa70 | ||
|
|
e6f311ebba | ||
|
|
88d09f0fef | ||
|
|
7467d87e36 | ||
|
|
b50e793a9c | ||
|
|
1147390576 | ||
|
|
539785f079 | ||
|
|
b99b516346 | ||
|
|
c0dc4f4236 | ||
|
|
5b8f664530 | ||
|
|
cc490692ad | ||
|
|
5ef415c87d | ||
|
|
ff3c67409d | ||
|
|
582a9bae31 | ||
|
|
95f51ed935 | ||
|
|
0dca22c219 | ||
|
|
eaf67e197b | ||
|
|
b8db918348 | ||
|
|
c865073605 | ||
|
|
192ccbec25 | ||
|
|
b8b3b29d01 | ||
|
|
00ea9a4393 | ||
|
|
267392d0b7 | ||
|
|
e5f041208f | ||
|
|
82010353c6 | ||
|
|
7daf4d1f58 | ||
|
|
ebb940ee0e | ||
|
|
1ce8c98837 | ||
|
|
257948cbe1 | ||
|
|
8971e0aa1a | ||
|
|
2209b050b0 | ||
|
|
b5a876a93e | ||
|
|
4c64ee24d5 | ||
|
|
1a3376374f | ||
|
|
ee88560090 | ||
|
|
aae7eba068 | ||
|
|
6715bc34d8 | ||
|
|
55d0ea5e22 | ||
|
|
11c692b146 | ||
|
|
ab47bd2e7b | ||
|
|
6dd9933d55 | ||
|
|
d08d398b47 | ||
|
|
aa3903d8f0 | ||
|
|
26f9030cef | ||
|
|
0500a10671 | ||
|
|
c3ae2d3eda | ||
|
|
e502c51886 | ||
|
|
063c1aa6c0 | ||
|
|
5e5db9193e | ||
|
|
26a38fc56c | ||
|
|
aad0558a9c | ||
|
|
a911f975da | ||
|
|
a99c8fbc88 | ||
|
|
79525227a9 | ||
|
|
66b8eb72bf | ||
|
|
1eff269b80 | ||
|
|
2dc01b528c | ||
|
|
30e200896d | ||
|
|
7d2e827e6f | ||
|
|
94e6b3086b | ||
|
|
0849e90574 | ||
|
|
d3eef33371 | ||
|
|
1cee0d10e5 | ||
|
|
3f044b407a | ||
|
|
b25ce3760b | ||
|
|
6e810761f8 | ||
|
|
6dea80e13e | ||
|
|
a715616a6a | ||
|
|
e87db432d5 | ||
|
|
0b4b89dcbd | ||
|
|
0e980051e3 | ||
|
|
4bd489a530 | ||
|
|
ed02c0dbc1 | ||
|
|
81e2432426 | ||
|
|
3a14099831 | ||
|
|
84712967b0 | ||
|
|
1fc70b7591 | ||
|
|
b9f8345964 | ||
|
|
fcd9e674d5 | ||
|
|
2cf2dfaea5 | ||
|
|
797be4e57c | ||
|
|
2478f1478f | ||
|
|
58cd508be4 | ||
|
|
9ca094f4f8 | ||
|
|
9ed9878193 | ||
|
|
c878a47f92 | ||
|
|
42d7a8860a | ||
|
|
718824f359 | ||
|
|
3b5b649530 | ||
|
|
8702af0c42 | ||
|
|
b038157f2f | ||
|
|
0033bdd570 | ||
|
|
99b40cac43 | ||
|
|
7fa91f98c6 | ||
|
|
a6e3f48044 | ||
|
|
d25921f6c8 | ||
|
|
e1986dc8eb | ||
|
|
ebba5b860f | ||
|
|
4beff8932e | ||
|
|
e26bd9adfb | ||
|
|
2eadc7b5ea | ||
|
|
a15aeade6a | ||
|
|
1990b7fad5 | ||
|
|
d81d5a37f8 | ||
|
|
1c1da0b080 | ||
|
|
4063bce178 | ||
|
|
b4d33c6472 | ||
|
|
44005c607d | ||
|
|
c17852a583 | ||
|
|
f94b22666b | ||
|
|
3c8c690a79 | ||
|
|
6f416ffe79 | ||
|
|
e4f8fe9ea2 | ||
|
|
003738149e | ||
|
|
6f39223787 | ||
|
|
06649c2207 | ||
|
|
18f51becd8 | ||
|
|
916792abea | ||
|
|
c8f8e019f6 | ||
|
|
7b6c6bb930 | ||
|
|
b826993a0d | ||
|
|
00385c8e15 | ||
|
|
5b982766fb | ||
|
|
be27dfd918 | ||
|
|
a82089aff3 | ||
|
|
215ba8bfbf | ||
|
|
7b1e9185f7 | ||
|
|
8c756e99c1 | ||
|
|
d2384e74cd | ||
|
|
b2ff2852bd | ||
|
|
3b4505af27 | ||
|
|
87eee126e5 | ||
|
|
6234f04457 | ||
|
|
c8e808082f | ||
|
|
b2f2642c82 | ||
|
|
0f77730011 | ||
|
|
b062d56928 | ||
|
|
a3f8552b80 | ||
|
|
8faf5a81ca | ||
|
|
7d7c0ae320 | ||
|
|
2bd8b71927 | ||
|
|
19a323ec77 | ||
|
|
29ee95165e | ||
|
|
c618a31e44 | ||
|
|
f972de507f | ||
|
|
51a0be31d5 | ||
|
|
d986c12564 | ||
|
|
1ae17b121e | ||
|
|
afb294f170 | ||
|
|
2b63fa343a | ||
|
|
35121f1141 | ||
|
|
9d0f26fede | ||
|
|
f0289b73b8 | ||
|
|
1009619ecd | ||
|
|
f30b1b1e08 | ||
|
|
d151f39a22 | ||
|
|
be6d4accff | ||
|
|
69e71202ee | ||
|
|
fece284aee | ||
|
|
4bcb44bcb2 | ||
|
|
0891dbf5a8 | ||
|
|
b5b5a86025 | ||
|
|
3fcbe831d7 | ||
|
|
d253151296 | ||
|
|
4e9beb86b0 | ||
|
|
7f85b49f3f | ||
|
|
1ea8c56631 | ||
|
|
5d76cbee26 | ||
|
|
682e4af619 | ||
|
|
c446476fbc | ||
|
|
e18a05001e | ||
|
|
0685f8b9c0 | ||
|
|
bdd1d4e8c1 | ||
|
|
bf6923f909 | ||
|
|
faa37c3322 | ||
|
|
1fe5b07f86 | ||
|
|
7a9ad3933b | ||
|
|
df2e447362 | ||
|
|
b2033f3dcd | ||
|
|
186bce852d | ||
|
|
e1494441b0 | ||
|
|
1b67c130c7 | ||
|
|
628b980655 | ||
|
|
0e04a17380 | ||
|
|
cee1a14ebd | ||
|
|
3862205053 | ||
|
|
9696f044a0 | ||
|
|
b61a0f2c94 | ||
|
|
afb2e3a8a2 | ||
|
|
65cc0a0ed5 | ||
|
|
dba6f2ec6a | ||
|
|
b39ea03ba6 | ||
|
|
528483b905 | ||
|
|
fb7434f1b4 | ||
|
|
a239e6405b | ||
|
|
d5c00b81d7 | ||
|
|
590f6c8c8b | ||
|
|
edf2495a9f | ||
|
|
ed0a034414 | ||
|
|
3e33e0d0e1 | ||
|
|
c9e1c2711e | ||
|
|
1f0420dc1f | ||
|
|
f0320c6ff9 | ||
|
|
904eb1364c | ||
|
|
e5ef184515 | ||
|
|
2a4111643c | ||
|
|
53ffe057b2 | ||
|
|
b2632e1727 | ||
|
|
84554d6dea | ||
|
|
6fd8f8380b | ||
|
|
5330929efd | ||
|
|
c67bc9938c | ||
|
|
f41f0e94be | ||
|
|
74b1c6f4ed | ||
|
|
54d7e20baa | ||
|
|
221d88c52c | ||
|
|
eb3c8050fe | ||
|
|
fdab854799 | ||
|
|
ba32761a0c | ||
|
|
6d002838a5 | ||
|
|
d8f53db387 | ||
|
|
f15db1637e | ||
|
|
5f98e2fdc5 | ||
|
|
603dc9f9c6 | ||
|
|
0692079384 | ||
|
|
e046cdce45 | ||
|
|
595419df77 | ||
|
|
1a641afaa8 | ||
|
|
29c73ae90f | ||
|
|
e055f99f63 | ||
|
|
888d4797d0 | ||
|
|
b95d63e974 | ||
|
|
195abb59ff | ||
|
|
306ceba474 | ||
|
|
2be1476130 | ||
|
|
ca80356c27 | ||
|
|
eed34d9b7d | ||
|
|
1144ab32b7 | ||
|
|
0061e609d4 | ||
|
|
680b64c98f | ||
|
|
325048efcb | ||
|
|
aedb293d50 | ||
|
|
6cdc1943fb | ||
|
|
9ff40875c2 | ||
|
|
3a851d59fe | ||
|
|
64de7abada | ||
|
|
6d9feebedd | ||
|
|
441658eb7a | ||
|
|
01a441a99c | ||
|
|
1d10141c1d | ||
|
|
952313acee | ||
|
|
d537b742dc | ||
|
|
f3b706bcc2 | ||
|
|
2a57af944f | ||
|
|
3f42b93300 | ||
|
|
59a58eb886 | ||
|
|
d91d5bcd9b | ||
|
|
c6a08c79e1 | ||
|
|
d550b65bc7 | ||
|
|
42418ef393 | ||
|
|
12f4156ae0 | ||
|
|
ff58873fcb | ||
|
|
925c72f6aa | ||
|
|
345d853e2d | ||
|
|
10b064dc40 | ||
|
|
75a7187a87 | ||
|
|
6182e00c31 | ||
|
|
e3ba93a6b8 | ||
|
|
109d8cacbc | ||
|
|
3bcc18f035 | ||
|
|
92baa1affb | ||
|
|
32a9457de3 | ||
|
|
32c147fa16 | ||
|
|
d61d75f8bb | ||
|
|
eae6cd5396 | ||
|
|
6f16f230d1 | ||
|
|
f458a057e8 | ||
|
|
d013300fa3 | ||
|
|
d6b8394044 | ||
|
|
c1fa630602 | ||
|
|
28a03dadaf | ||
|
|
29cfc2ea6b | ||
|
|
f4c776f10e | ||
|
|
04be6bc38b | ||
|
|
4f0bbeeab0 | ||
|
|
5a356a8d75 | ||
|
|
7f35f46eaa | ||
|
|
ea7be67d83 | ||
|
|
8f0d65766d | ||
|
|
a140f318a2 | ||
|
|
d0a2d1644f | ||
|
|
e69b153e1c | ||
|
|
d19d383738 | ||
|
|
253f288323 | ||
|
|
d6ce0536ca | ||
|
|
0168c01915 | ||
|
|
7329fec67a | ||
|
|
5fea22294e | ||
|
|
669124a6b3 | ||
|
|
c44d141c07 | ||
|
|
0b30e00b8c | ||
|
|
1e7bd1bf48 |
244
.editorconfig
Normal file
244
.editorconfig
Normal file
@@ -0,0 +1,244 @@
|
||||
# Remove the line below if you want to inherit .editorconfig settings from higher directories
|
||||
root = true
|
||||
|
||||
# C# files
|
||||
[*.cs]
|
||||
|
||||
#### Core EditorConfig Options ####
|
||||
|
||||
# Indentation and spacing
|
||||
indent_size = 4
|
||||
indent_style = space
|
||||
tab_width = 4
|
||||
|
||||
# New line preferences
|
||||
end_of_line = crlf
|
||||
insert_final_newline = false
|
||||
|
||||
#### .NET Code Actions ####
|
||||
|
||||
# Type members
|
||||
dotnet_hide_advanced_members = false
|
||||
dotnet_member_insertion_location = with_other_members_of_the_same_kind
|
||||
dotnet_property_generation_behavior = prefer_throwing_properties
|
||||
|
||||
# Symbol search
|
||||
dotnet_search_reference_assemblies = true
|
||||
|
||||
#### .NET Coding Conventions ####
|
||||
|
||||
# Organize usings
|
||||
dotnet_separate_import_directive_groups = false
|
||||
dotnet_sort_system_directives_first = false
|
||||
file_header_template = unset
|
||||
|
||||
# this. and Me. preferences
|
||||
dotnet_style_qualification_for_event = false
|
||||
dotnet_style_qualification_for_field = false
|
||||
dotnet_style_qualification_for_method = false
|
||||
dotnet_style_qualification_for_property = false
|
||||
|
||||
# Language keywords vs BCL types preferences
|
||||
dotnet_style_predefined_type_for_locals_parameters_members = true
|
||||
dotnet_style_predefined_type_for_member_access = true
|
||||
|
||||
# Parentheses preferences
|
||||
dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity
|
||||
dotnet_style_parentheses_in_other_binary_operators = always_for_clarity
|
||||
dotnet_style_parentheses_in_other_operators = never_if_unnecessary
|
||||
dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity
|
||||
|
||||
# Modifier preferences
|
||||
dotnet_style_require_accessibility_modifiers = for_non_interface_members
|
||||
|
||||
# Expression-level preferences
|
||||
dotnet_prefer_system_hash_code = true
|
||||
dotnet_style_coalesce_expression = true
|
||||
dotnet_style_collection_initializer = true
|
||||
dotnet_style_explicit_tuple_names = true
|
||||
dotnet_style_namespace_match_folder = true
|
||||
dotnet_style_null_propagation = true
|
||||
dotnet_style_object_initializer = true
|
||||
dotnet_style_operator_placement_when_wrapping = beginning_of_line
|
||||
dotnet_style_prefer_auto_properties = true
|
||||
dotnet_style_prefer_collection_expression = when_types_loosely_match
|
||||
dotnet_style_prefer_compound_assignment = true
|
||||
dotnet_style_prefer_conditional_expression_over_assignment = true
|
||||
dotnet_style_prefer_conditional_expression_over_return = true
|
||||
dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed
|
||||
dotnet_style_prefer_inferred_anonymous_type_member_names = true
|
||||
dotnet_style_prefer_inferred_tuple_names = true
|
||||
dotnet_style_prefer_is_null_check_over_reference_equality_method = true
|
||||
dotnet_style_prefer_simplified_boolean_expressions = true
|
||||
dotnet_style_prefer_simplified_interpolation = true
|
||||
|
||||
# Field preferences
|
||||
dotnet_style_readonly_field = true
|
||||
|
||||
# Parameter preferences
|
||||
dotnet_code_quality_unused_parameters = all
|
||||
|
||||
# Suppression preferences
|
||||
dotnet_remove_unnecessary_suppression_exclusions = none
|
||||
|
||||
# New line preferences
|
||||
dotnet_style_allow_multiple_blank_lines_experimental = true
|
||||
dotnet_style_allow_statement_immediately_after_block_experimental = true
|
||||
|
||||
#### C# Coding Conventions ####
|
||||
|
||||
# var preferences
|
||||
csharp_style_var_elsewhere = false
|
||||
csharp_style_var_for_built_in_types = false
|
||||
csharp_style_var_when_type_is_apparent = false
|
||||
|
||||
# Expression-bodied members
|
||||
csharp_style_expression_bodied_accessors = true
|
||||
csharp_style_expression_bodied_constructors = false
|
||||
csharp_style_expression_bodied_indexers = true
|
||||
csharp_style_expression_bodied_lambdas = true
|
||||
csharp_style_expression_bodied_local_functions = false
|
||||
csharp_style_expression_bodied_methods = false
|
||||
csharp_style_expression_bodied_operators = false
|
||||
csharp_style_expression_bodied_properties = true
|
||||
|
||||
# Pattern matching preferences
|
||||
csharp_style_pattern_matching_over_as_with_null_check = true
|
||||
csharp_style_pattern_matching_over_is_with_cast_check = true
|
||||
csharp_style_prefer_extended_property_pattern = true
|
||||
csharp_style_prefer_not_pattern = true
|
||||
csharp_style_prefer_pattern_matching = true
|
||||
csharp_style_prefer_switch_expression = true
|
||||
|
||||
# Null-checking preferences
|
||||
csharp_style_conditional_delegate_call = true
|
||||
|
||||
# Modifier preferences
|
||||
csharp_prefer_static_anonymous_function = true
|
||||
csharp_prefer_static_local_function = true
|
||||
csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async
|
||||
csharp_style_prefer_readonly_struct = true
|
||||
csharp_style_prefer_readonly_struct_member = true
|
||||
|
||||
# Code-block preferences
|
||||
csharp_prefer_braces = true
|
||||
csharp_prefer_simple_using_statement = true
|
||||
csharp_prefer_system_threading_lock = true
|
||||
csharp_style_namespace_declarations = block_scoped
|
||||
csharp_style_prefer_method_group_conversion = true
|
||||
csharp_style_prefer_primary_constructors = true
|
||||
csharp_style_prefer_top_level_statements = true
|
||||
|
||||
# Expression-level preferences
|
||||
csharp_prefer_simple_default_expression = true
|
||||
csharp_style_deconstructed_variable_declaration = true
|
||||
csharp_style_implicit_object_creation_when_type_is_apparent = true
|
||||
csharp_style_inlined_variable_declaration = true
|
||||
csharp_style_prefer_index_operator = true
|
||||
csharp_style_prefer_local_over_anonymous_function = true
|
||||
csharp_style_prefer_null_check_over_type_check = true
|
||||
csharp_style_prefer_range_operator = true
|
||||
csharp_style_prefer_tuple_swap = true
|
||||
csharp_style_prefer_utf8_string_literals = true
|
||||
csharp_style_throw_expression = true
|
||||
csharp_style_unused_value_assignment_preference = discard_variable
|
||||
csharp_style_unused_value_expression_statement_preference = discard_variable
|
||||
|
||||
# 'using' directive preferences
|
||||
csharp_using_directive_placement = outside_namespace
|
||||
|
||||
# New line preferences
|
||||
csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true
|
||||
csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true
|
||||
csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true
|
||||
csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true
|
||||
csharp_style_allow_embedded_statements_on_same_line_experimental = true
|
||||
|
||||
#### C# Formatting Rules ####
|
||||
|
||||
# New line preferences
|
||||
csharp_new_line_before_catch = true
|
||||
csharp_new_line_before_else = true
|
||||
csharp_new_line_before_finally = true
|
||||
csharp_new_line_before_members_in_anonymous_types = true
|
||||
csharp_new_line_before_members_in_object_initializers = true
|
||||
csharp_new_line_before_open_brace = all
|
||||
csharp_new_line_between_query_expression_clauses = true
|
||||
|
||||
# Indentation preferences
|
||||
csharp_indent_block_contents = true
|
||||
csharp_indent_braces = false
|
||||
csharp_indent_case_contents = true
|
||||
csharp_indent_case_contents_when_block = true
|
||||
csharp_indent_labels = no_change
|
||||
csharp_indent_switch_labels = true
|
||||
|
||||
# Space preferences
|
||||
csharp_space_after_cast = false
|
||||
csharp_space_after_colon_in_inheritance_clause = true
|
||||
csharp_space_after_comma = true
|
||||
csharp_space_after_dot = false
|
||||
csharp_space_after_keywords_in_control_flow_statements = true
|
||||
csharp_space_after_semicolon_in_for_statement = true
|
||||
csharp_space_around_binary_operators = before_and_after
|
||||
csharp_space_around_declaration_statements = false
|
||||
csharp_space_before_colon_in_inheritance_clause = true
|
||||
csharp_space_before_comma = false
|
||||
csharp_space_before_dot = false
|
||||
csharp_space_before_open_square_brackets = false
|
||||
csharp_space_before_semicolon_in_for_statement = false
|
||||
csharp_space_between_empty_square_brackets = false
|
||||
csharp_space_between_method_call_empty_parameter_list_parentheses = false
|
||||
csharp_space_between_method_call_name_and_opening_parenthesis = false
|
||||
csharp_space_between_method_call_parameter_list_parentheses = false
|
||||
csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
|
||||
csharp_space_between_method_declaration_name_and_open_parenthesis = false
|
||||
csharp_space_between_method_declaration_parameter_list_parentheses = false
|
||||
csharp_space_between_parentheses = false
|
||||
csharp_space_between_square_brackets = false
|
||||
|
||||
# Wrapping preferences
|
||||
csharp_preserve_single_line_blocks = true
|
||||
csharp_preserve_single_line_statements = true
|
||||
|
||||
#### Naming styles ####
|
||||
|
||||
# Naming rules
|
||||
|
||||
dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion
|
||||
dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
|
||||
dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
|
||||
|
||||
dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion
|
||||
dotnet_naming_rule.types_should_be_pascal_case.symbols = types
|
||||
dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
|
||||
|
||||
dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion
|
||||
dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
|
||||
dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
|
||||
|
||||
# Symbol specifications
|
||||
|
||||
dotnet_naming_symbols.interface.applicable_kinds = interface
|
||||
dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
||||
dotnet_naming_symbols.interface.required_modifiers =
|
||||
|
||||
dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
|
||||
dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
||||
dotnet_naming_symbols.types.required_modifiers =
|
||||
|
||||
dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
|
||||
dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
||||
dotnet_naming_symbols.non_field_members.required_modifiers =
|
||||
|
||||
# Naming styles
|
||||
|
||||
dotnet_naming_style.pascal_case.required_prefix =
|
||||
dotnet_naming_style.pascal_case.required_suffix =
|
||||
dotnet_naming_style.pascal_case.word_separator =
|
||||
dotnet_naming_style.pascal_case.capitalization = pascal_case
|
||||
|
||||
dotnet_naming_style.begins_with_i.required_prefix = I
|
||||
dotnet_naming_style.begins_with_i.required_suffix =
|
||||
dotnet_naming_style.begins_with_i.word_separator =
|
||||
dotnet_naming_style.begins_with_i.capitalization = pascal_case
|
||||
8
.github/workflows/ci.yml
vendored
8
.github/workflows/ci.yml
vendored
@@ -15,11 +15,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v3
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 7.0.x
|
||||
dotnet-version: 9.0.x
|
||||
|
||||
- name: Build
|
||||
run: dotnet build Radzen.Blazor/Radzen.Blazor.csproj
|
||||
- name: Test
|
||||
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -341,3 +341,9 @@ Radzen.DocFX/_exported_templates
|
||||
Radzen.DocFX/api/*.yml
|
||||
!Radzen.DocFX/api/index.md
|
||||
Radzen.DocFX/api/.manifest
|
||||
Radzen.Blazor.min.js
|
||||
/.claude
|
||||
/SANKEY_PATTERN_COMPARISON.md
|
||||
*.md
|
||||
/.gitignore
|
||||
/.gitignore
|
||||
|
||||
27
Dockerfile
27
Dockerfile
@@ -1,31 +1,24 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
FROM mono:latest
|
||||
|
||||
ENV DOCFX_VER 2.58.4
|
||||
|
||||
RUN apt-get update && apt-get install unzip wget git -y && wget -q -P /tmp https://github.com/dotnet/docfx/releases/download/v${DOCFX_VER}/docfx.zip && \
|
||||
mkdir -p /opt/docfx && \
|
||||
unzip /tmp/docfx.zip -d /opt/docfx && \
|
||||
echo '#!/bin/bash\nmono /opt/docfx/docfx.exe $@' > /usr/bin/docfx && \
|
||||
chmod +x /usr/bin/docfx && \
|
||||
rm -rf /tmp/*
|
||||
FROM mcr.microsoft.com/dotnet/sdk:9.0
|
||||
|
||||
COPY Radzen.Blazor /app/Radzen.Blazor
|
||||
COPY Radzen.DocFX /app/DocFX
|
||||
COPY Radzen.DocFX /app/Radzen.DocFX
|
||||
COPY RadzenBlazorDemos /app/RadzenBlazorDemos
|
||||
COPY RadzenBlazorDemos.Host /app/RadzenBlazorDemos.Host
|
||||
|
||||
WORKDIR /app
|
||||
RUN docfx DocFX/docfx.json
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:7.0
|
||||
|
||||
COPY --from=0 /app/RadzenBlazorDemos.Host /app/RadzenBlazorDemos.Host
|
||||
COPY --from=0 /app/RadzenBlazorDemos /app/RadzenBlazorDemos
|
||||
RUN dotnet tool install -g docfx
|
||||
ENV PATH="$PATH:/root/.dotnet/tools"
|
||||
RUN wget https://dot.net/v1/dotnet-install.sh \
|
||||
&& bash dotnet-install.sh --channel 8.0 --runtime dotnet --install-dir /usr/share/dotnet
|
||||
RUN dotnet build -c Release Radzen.Blazor/Radzen.Blazor.csproj -f net8.0
|
||||
RUN docfx Radzen.DocFX/docfx.json
|
||||
|
||||
WORKDIR /app/RadzenBlazorDemos.Host
|
||||
RUN dotnet publish -c Release -o out
|
||||
|
||||
ENV ASPNETCORE_URLS http://*:5000
|
||||
ENV ASPNETCORE_URLS=http://*:5000
|
||||
WORKDIR /app/RadzenBlazorDemos.Host/out
|
||||
|
||||
ENTRYPOINT ["dotnet", "RadzenBlazorDemos.Host.dll"]
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018-2023 Radzen Ltd
|
||||
Copyright (c) 2018-2025 Radzen Ltd
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
103
README.md
103
README.md
@@ -1,45 +1,19 @@
|
||||

|
||||

|
||||
|
||||
<h1 align="center">
|
||||
Radzen Blazor Components
|
||||
</h1>
|
||||
Radzen Blazor Components
|
||||
========================
|
||||
|
||||
<p align="center">
|
||||
A set of <strong>70+ free and open source</strong> native Blazor UI controls.
|
||||
</p>
|
||||
A set of **90+ free and open source** native Blazor UI controls.
|
||||
|
||||
<div align="center">
|
||||
See Online Demos or Read the Docs
|
||||
|
||||
[See Online Demos](https://blazor.radzen.com) or [Read the Docs](https://blazor.radzen.com/docs/)
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/radzenhq/radzen-blazor/blob/master/LICENSE">
|
||||
<img alt="License - MIT" src="https://img.shields.io/github/license/radzenhq/radzen-blazor?logo=github&style=for-the-badge" />
|
||||
</a>
|
||||
<a href="https://www.nuget.org/packages/Radzen.Blazor">
|
||||
<img alt="Nuget Downloads" src="https://img.shields.io/nuget/dt/Radzen.Blazor?color=%232694F9&label=nuget%20downloads&logo=nuget&style=for-the-badge" />
|
||||
</a>
|
||||
<img alt="Last Commit" src="https://img.shields.io/github/last-commit/radzenhq/radzen-blazor?logo=github&style=for-the-badge" />
|
||||
<a href="https://github.com/radzenhq/radzen-blazor/graphs/contributors">
|
||||
<img alt="Github Contributors" src="https://img.shields.io/github/contributors/radzenhq/radzen-blazor?logo=github&style=for-the-badge" />
|
||||
</a>
|
||||
<a href="https://blazor.radzen.com">
|
||||
<img alt="Radzen Blazor Components - Online Demos" src="https://img.shields.io/badge/demos-online-brightgreen?color=%232694F9&logo=blazor&style=for-the-badge" />
|
||||
</a>
|
||||
<a href="https://blazor.radzen.com/docs">
|
||||
<img alt="Radzen Blazor Components - Documentation" src="https://img.shields.io/badge/docs-online-brightgreen?color=%232694F9&logo=blazor&style=for-the-badge" />
|
||||
</a>
|
||||
</p>
|
||||
[](https://github.com/radzenhq/radzen-blazor/blob/master/LICENSE)[ ](https://www.nuget.org/packages/Radzen.Blazor) [ ](https://github.com/radzenhq/radzen-blazor/graphs/contributors)[ ](https://blazor.radzen.com)[](https://blazor.radzen.com/docs)
|
||||
|
||||
## Why choose Radzen Blazor Components?
|
||||
|
||||
### :sparkles: Free
|
||||
|
||||
Radzen Blazor Components are open source and free for commercial use. You can install them from [nuget](https://www.nuget.org/packages/Radzen.Blazor) or build your own copy from source.
|
||||
Radzen Blazor Components are open source and free for commercial use. You can install them from [NuGet](https://www.nuget.org/packages/Radzen.Blazor) or build your own copy from source.
|
||||
|
||||
Paid support is available as part of the [Radzen Professional subscription](https://www.radzen.com/blazor-studio/pricing/).
|
||||
|
||||
@@ -76,67 +50,8 @@ Our flagship product [Radzen Blazor Studio](https://www.radzen.com/blazor-studio
|
||||
|
||||
## Get started with Radzen Blazor Components
|
||||
|
||||
### 1. Install
|
||||
Check the [getting started](https://blazor.radzen.com/getting-started) instructions to start making awesome Blazor applications.
|
||||
|
||||
Radzen Blazor Components are distributed as a [Radzen.Blazor nuget package](https://www.nuget.org/packages/Radzen.Blazor). You can add them to your project in one of the following ways
|
||||
- Install the package from command line by running `dotnet add package Radzen.Blazor`
|
||||
- Add the project from the Visual Nuget Package Manager
|
||||
- Manually edit the .csproj file and add a project reference
|
||||
|
||||
### 2. Import the namespace
|
||||
|
||||
Open the `_Imports.razor` file of your Blazor application and add this line `@using Radzen.Blazor`.
|
||||
|
||||
### 3. Include a theme
|
||||
|
||||
Radzen Blazor components come with five free themes: Material, Standard, Default, Dark, Software and Humanistic.
|
||||
|
||||
To use a theme
|
||||
1. Pick a theme. The [online demos](https://blazor.radzen.com/colors) allow you to preview the available options via the theme dropdown located in the header. The Material theme is currently selected by default.
|
||||
1. Include the theme CSS file in your Blazor application. Open `Pages\_Layout.cshtml` (Blazor Server .NET 6), `Pages\_Host.cshtml` (Blazor Server .NET 7) or `wwwroot/index.html` (Blazor WebAssembly) and include a theme CSS file by adding this snippet
|
||||
```html
|
||||
<link rel="stylesheet" href="_content/Radzen.Blazor/css/material-base.css">
|
||||
```
|
||||
|
||||
To include a different theme (i.e. Standard) just change the name of the CSS file:
|
||||
```
|
||||
<link rel="stylesheet" href="_content/Radzen.Blazor/css/standard-base.css">
|
||||
```
|
||||
|
||||
### 4. Include Radzen.Blazor.js
|
||||
|
||||
Open `Pages\_Layout.cshtml` (Blazor Server .NET 6), `Pages\_Host.cshtml` (Blazor Server .NET 7) or `wwwroot/index.html` (Blazor WebAssembly) and include this snippet:
|
||||
|
||||
```html
|
||||
<script src="_content/Radzen.Blazor/Radzen.Blazor.js"></script>
|
||||
```
|
||||
|
||||
### 5. Use a component
|
||||
Use any Radzen Blazor component by typing its tag name in a Blazor page e.g.
|
||||
```html
|
||||
<RadzenButton Text="Hi"></RadzenButton>
|
||||
```
|
||||
|
||||
#### Data-binding a property
|
||||
```razor
|
||||
<RadzenButton Text=@text />
|
||||
<RadzenTextBox @bind-Value=@text />
|
||||
@code {
|
||||
string text = "Hi";
|
||||
}
|
||||
```
|
||||
|
||||
#### Handing events
|
||||
|
||||
```razor
|
||||
<RadzenButton Click="@ButtonClicked" Text="Hi"></RadzenButton>
|
||||
@code {
|
||||
void ButtonClicked()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
```
|
||||
## Run demos locally
|
||||
|
||||
Use Radzen.Server.sln to open and run demos as Blazor server application or Radzen.WebAssembly.sln to open and run demos as Blazor WebAssembly application. Radzen.sln has reference to all projects including tests.
|
||||
Use Radzen.Server.sln to open and run demos as Blazor server application or Radzen.WebAssembly.sln to open and run demos as Blazor WebAssembly application. Radzen.sln has reference to all projects including tests.
|
||||
|
||||
171
Radzen.Blazor.Tests/AIChatTests.cs
Normal file
171
Radzen.Blazor.Tests/AIChatTests.cs
Normal file
@@ -0,0 +1,171 @@
|
||||
using Bunit;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Radzen;
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Tests
|
||||
{
|
||||
public class AIChatTests
|
||||
{
|
||||
private void RegisterChatService(TestContext ctx)
|
||||
{
|
||||
// Register a dummy HttpClient and default options for AIChatService
|
||||
ctx.Services.AddSingleton(new HttpClient());
|
||||
ctx.Services.AddScoped<IAIChatService, AIChatService>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RadzenAIChat_ShouldRenderWithDefaultProperties()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
RegisterChatService(ctx);
|
||||
var component = ctx.RenderComponent<RadzenAIChat>();
|
||||
Assert.Contains("Type your message...", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RadzenAIChat_ShouldRenderWithCustomTitle()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
RegisterChatService(ctx);
|
||||
var component = ctx.RenderComponent<RadzenAIChat>(parameters => parameters
|
||||
.Add(p => p.Title, "Custom Chat"));
|
||||
Assert.Contains("Custom Chat", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RadzenAIChat_ShouldRenderWithCustomPlaceholder()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
RegisterChatService(ctx);
|
||||
var component = ctx.RenderComponent<RadzenAIChat>(parameters => parameters
|
||||
.Add(p => p.Placeholder, "Enter your message here..."));
|
||||
Assert.Contains("Enter your message here...", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RadzenAIChat_ShouldRenderWithCustomEmptyMessage()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
RegisterChatService(ctx);
|
||||
var component = ctx.RenderComponent<RadzenAIChat>(parameters => parameters
|
||||
.Add(p => p.EmptyMessage, "No messages yet"));
|
||||
Assert.Contains("No messages yet", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RadzenAIChat_ShouldShowClearButtonByDefault()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
RegisterChatService(ctx);
|
||||
var component = ctx.RenderComponent<RadzenAIChat>();
|
||||
Assert.Contains("rz-chat-header-clear", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RadzenAIChat_ShouldHideClearButtonWhenShowClearButtonIsFalse()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
RegisterChatService(ctx);
|
||||
var component = ctx.RenderComponent<RadzenAIChat>(parameters => parameters
|
||||
.Add(p => p.ShowClearButton, false));
|
||||
Assert.DoesNotContain("clear_all", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RadzenAIChat_ShouldBeDisabledWhenDisabledIsTrue()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
RegisterChatService(ctx);
|
||||
var component = ctx.RenderComponent<RadzenAIChat>(parameters => parameters
|
||||
.Add(p => p.Disabled, true));
|
||||
Assert.Contains("disabled", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RadzenAIChat_ShouldBeReadOnlyWhenReadOnlyIsTrue()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
RegisterChatService(ctx);
|
||||
var component = ctx.RenderComponent<RadzenAIChat>(parameters => parameters
|
||||
.Add(p => p.ReadOnly, true));
|
||||
Assert.Contains("readonly", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RadzenAIChat_ShouldHaveCorrectCssClass()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
RegisterChatService(ctx);
|
||||
var component = ctx.RenderComponent<RadzenAIChat>();
|
||||
Assert.Contains("rz-chat", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ChatMessage_ShouldHaveCorrectProperties()
|
||||
{
|
||||
// Arrange
|
||||
var message = new ChatMessage
|
||||
{
|
||||
Content = "Test message",
|
||||
IsUser = true,
|
||||
Timestamp = DateTime.Now
|
||||
};
|
||||
// Assert
|
||||
Assert.NotEmpty(message.Id);
|
||||
Assert.Equal("Test message", message.Content);
|
||||
Assert.True(message.IsUser);
|
||||
Assert.False(message.IsStreaming);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RadzenAIChat_AddMessage_ShouldAddMessageToList()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
RegisterChatService(ctx);
|
||||
var component = ctx.RenderComponent<RadzenAIChat>();
|
||||
// Act
|
||||
component.Instance.AddMessage("Test message", true);
|
||||
// Assert
|
||||
var messages = component.Instance.GetMessages();
|
||||
Assert.Single(messages);
|
||||
Assert.Equal("Test message", messages[0].Content);
|
||||
Assert.True(messages[0].IsUser);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RadzenAIChat_ClearChat_ShouldRemoveAllMessages()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
RegisterChatService(ctx);
|
||||
var component = ctx.RenderComponent<RadzenAIChat>();
|
||||
component.Instance.AddMessage("Test message 1", true);
|
||||
component.Instance.AddMessage("Test message 2", false);
|
||||
// Act
|
||||
component.InvokeAsync(async () => await component.Instance.ClearChat()).Wait();
|
||||
// Assert
|
||||
Assert.Empty(component.Instance.GetMessages());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RadzenAIChat_ShouldLimitMessagesToMaxMessages()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
RegisterChatService(ctx);
|
||||
var component = ctx.RenderComponent<RadzenAIChat>(parameters => parameters.Add(p => p.MaxMessages, 3));
|
||||
component.Instance.AddMessage("Message 1", true);
|
||||
component.Instance.AddMessage("Message 2", false);
|
||||
component.Instance.AddMessage("Message 3", true);
|
||||
component.Instance.AddMessage("Message 4", false);
|
||||
// Assert
|
||||
var messages = component.Instance.GetMessages();
|
||||
Assert.Equal(3, messages.Count);
|
||||
Assert.Equal("Message 2", messages[0].Content);
|
||||
Assert.Equal("Message 3", messages[1].Content);
|
||||
Assert.Equal("Message 4", messages[2].Content);
|
||||
}
|
||||
}
|
||||
}
|
||||
74
Radzen.Blazor.Tests/AutoCompleteTests.cs
Normal file
74
Radzen.Blazor.Tests/AutoCompleteTests.cs
Normal file
@@ -0,0 +1,74 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Tests
|
||||
{
|
||||
public class AutoCompleteTests
|
||||
{
|
||||
[Fact]
|
||||
public void AutoComplete_Enum_Converts_To_Attr_Value()
|
||||
{
|
||||
// Options
|
||||
Assert.Equal("off", AutoCompleteType.Off.GetAutoCompleteValue());
|
||||
Assert.Equal("on", AutoCompleteType.On.GetAutoCompleteValue());
|
||||
Assert.Equal("name", AutoCompleteType.Name.GetAutoCompleteValue());
|
||||
Assert.Equal("honorific-prefix", AutoCompleteType.HonorificPrefix.GetAutoCompleteValue());
|
||||
Assert.Equal("given-name", AutoCompleteType.GivenName.GetAutoCompleteValue());
|
||||
Assert.Equal("additional-name", AutoCompleteType.AdditionalName.GetAutoCompleteValue());
|
||||
Assert.Equal("family-name", AutoCompleteType.FamilyName.GetAutoCompleteValue());
|
||||
Assert.Equal("honorific-suffix", AutoCompleteType.HonorificSuffix.GetAutoCompleteValue());
|
||||
Assert.Equal("nickname", AutoCompleteType.Nickname.GetAutoCompleteValue());
|
||||
Assert.Equal("email", AutoCompleteType.Email.GetAutoCompleteValue());
|
||||
Assert.Equal("username", AutoCompleteType.Username.GetAutoCompleteValue());
|
||||
Assert.Equal("new-password", AutoCompleteType.NewPassword.GetAutoCompleteValue());
|
||||
Assert.Equal("current-password", AutoCompleteType.CurrentPassword.GetAutoCompleteValue());
|
||||
Assert.Equal("one-time-code", AutoCompleteType.OneTimeCode.GetAutoCompleteValue());
|
||||
Assert.Equal("organization-title", AutoCompleteType.OrganizationTitle.GetAutoCompleteValue());
|
||||
Assert.Equal("organization", AutoCompleteType.Organization.GetAutoCompleteValue());
|
||||
Assert.Equal("street-address", AutoCompleteType.StreetAddress.GetAutoCompleteValue());
|
||||
Assert.Equal("address-line1", AutoCompleteType.AddressLine1.GetAutoCompleteValue());
|
||||
Assert.Equal("address-line2", AutoCompleteType.AddressLine2.GetAutoCompleteValue());
|
||||
Assert.Equal("address-line3", AutoCompleteType.AddressLine3.GetAutoCompleteValue());
|
||||
Assert.Equal("address-level1", AutoCompleteType.AddressLevel1.GetAutoCompleteValue());
|
||||
Assert.Equal("address-level2", AutoCompleteType.AddressLevel2.GetAutoCompleteValue());
|
||||
Assert.Equal("address-level3", AutoCompleteType.AddressLevel3.GetAutoCompleteValue());
|
||||
Assert.Equal("address-level4", AutoCompleteType.AddressLevel4.GetAutoCompleteValue());
|
||||
Assert.Equal("country", AutoCompleteType.Country.GetAutoCompleteValue());
|
||||
Assert.Equal("country-name", AutoCompleteType.CountryName.GetAutoCompleteValue());
|
||||
Assert.Equal("postal-code", AutoCompleteType.PostalCode.GetAutoCompleteValue());
|
||||
Assert.Equal("cc-name", AutoCompleteType.CcName.GetAutoCompleteValue());
|
||||
Assert.Equal("cc-given-name", AutoCompleteType.CcGivenName.GetAutoCompleteValue());
|
||||
Assert.Equal("cc-additional-name", AutoCompleteType.CcAdditionalName.GetAutoCompleteValue());
|
||||
Assert.Equal("cc-family-name", AutoCompleteType.CcFamilyName.GetAutoCompleteValue());
|
||||
Assert.Equal("cc-number", AutoCompleteType.CcNumber.GetAutoCompleteValue());
|
||||
Assert.Equal("cc-exp", AutoCompleteType.CcExp.GetAutoCompleteValue());
|
||||
Assert.Equal("cc-exp-month", AutoCompleteType.CcExpMonth.GetAutoCompleteValue());
|
||||
Assert.Equal("cc-exp-year", AutoCompleteType.CcExpYear.GetAutoCompleteValue());
|
||||
Assert.Equal("cc-csc", AutoCompleteType.CcCsc.GetAutoCompleteValue());
|
||||
Assert.Equal("cc-type", AutoCompleteType.CcType.GetAutoCompleteValue());
|
||||
Assert.Equal("transaction-currency", AutoCompleteType.TransactionCurrency.GetAutoCompleteValue());
|
||||
Assert.Equal("transaction-amount", AutoCompleteType.TransactionAmount.GetAutoCompleteValue());
|
||||
Assert.Equal("language", AutoCompleteType.Language.GetAutoCompleteValue());
|
||||
Assert.Equal("bday", AutoCompleteType.Bday.GetAutoCompleteValue());
|
||||
Assert.Equal("bday-day", AutoCompleteType.BdayDay.GetAutoCompleteValue());
|
||||
Assert.Equal("bday-month", AutoCompleteType.BdayMonth.GetAutoCompleteValue());
|
||||
Assert.Equal("bday-year", AutoCompleteType.BdayYear.GetAutoCompleteValue());
|
||||
Assert.Equal("sex", AutoCompleteType.Sex.GetAutoCompleteValue());
|
||||
Assert.Equal("tel", AutoCompleteType.Tel.GetAutoCompleteValue());
|
||||
Assert.Equal("tel-country-code", AutoCompleteType.TelCountryCode.GetAutoCompleteValue());
|
||||
Assert.Equal("tel-national", AutoCompleteType.TelNational.GetAutoCompleteValue());
|
||||
Assert.Equal("tel-area-code", AutoCompleteType.TelAreaCode.GetAutoCompleteValue());
|
||||
Assert.Equal("tel-local", AutoCompleteType.TelLocal.GetAutoCompleteValue());
|
||||
Assert.Equal("tel-extension", AutoCompleteType.TelExtension.GetAutoCompleteValue());
|
||||
Assert.Equal("impp", AutoCompleteType.Impp.GetAutoCompleteValue());
|
||||
Assert.Equal("url", AutoCompleteType.Url.GetAutoCompleteValue());
|
||||
Assert.Equal("photo", AutoCompleteType.Photo.GetAutoCompleteValue());
|
||||
// Synonyms
|
||||
Assert.Equal("address-level1", AutoCompleteType.State.GetAutoCompleteValue());
|
||||
Assert.Equal("address-level1", AutoCompleteType.Province.GetAutoCompleteValue());
|
||||
Assert.Equal("postal-code", AutoCompleteType.ZipCode.GetAutoCompleteValue());
|
||||
Assert.Equal("given-name", AutoCompleteType.FirstName.GetAutoCompleteValue());
|
||||
Assert.Equal("additional-name", AutoCompleteType.MiddleName.GetAutoCompleteValue());
|
||||
Assert.Equal("family-name", AutoCompleteType.LastName.GetAutoCompleteValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,7 @@ namespace Radzen.Blazor.Tests
|
||||
|
||||
component.SetParametersAndRender(parameters => parameters.Add(p => p.Icon, icon));
|
||||
|
||||
Assert.Contains(@$"<i class=""rz-button-icon-left rzi"">{icon}</i>", component.Markup);
|
||||
Assert.Contains(@$"<i class=""notranslate rz-button-icon-left rzi"">{icon}</i>", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -48,7 +48,7 @@ namespace Radzen.Blazor.Tests
|
||||
);
|
||||
|
||||
// does not render the actual icon when busy
|
||||
Assert.DoesNotContain(@$"<i class=""rz-button-icon-left rzi"">{icon}</i>", component.Markup);
|
||||
Assert.DoesNotContain(@$"<i class=""notranslate rz-button-icon-left rzi"">{icon}</i>", component.Markup);
|
||||
|
||||
// renders the icon with busy spin animation
|
||||
Assert.Contains(@"<i style=""animation: rotation", component.Markup);
|
||||
@@ -71,7 +71,7 @@ namespace Radzen.Blazor.Tests
|
||||
parameters.Add(p => p.Icon, icon);
|
||||
});
|
||||
|
||||
Assert.Contains(@$"<i class=""rz-button-icon-left rzi"">{icon}</i>", component.Markup);
|
||||
Assert.Contains(@$"<i class=""notranslate rz-button-icon-left rzi"">{icon}</i>", component.Markup);
|
||||
Assert.Contains(@$"<span class=""rz-button-text"">{text}</span>", component.Markup);
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ namespace Radzen.Blazor.Tests
|
||||
|
||||
component.SetParametersAndRender(parameters => parameters.Add(p => p.Image, image));
|
||||
|
||||
Assert.Contains(@$"<img class=""rz-button-icon-left rzi"" src=""{image}"" />", component.Markup);
|
||||
Assert.Contains(@$"<img class=""notranslate rz-button-icon-left rzi"" src=""{image}"" alt=""button"" />", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -103,9 +103,10 @@ namespace Radzen.Blazor.Tests
|
||||
{
|
||||
parameters.Add(p => p.Text, text);
|
||||
parameters.Add(p => p.Image, image);
|
||||
parameters.Add(p => p.ImageAlternateText, text);
|
||||
});
|
||||
|
||||
Assert.Contains(@$"<img class=""rz-button-icon-left rzi"" src=""{image}"" />", component.Markup);
|
||||
Assert.Contains(@$"<img class=""notranslate rz-button-icon-left rzi"" src=""{image}"" alt=""{text}"" />", component.Markup);
|
||||
Assert.Contains(@$"<span class=""rz-button-text"">{text}</span>", component.Markup);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,20 +1,32 @@
|
||||
using System.Linq;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Bunit;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Radzen.Blazor.Rendering;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Radzen.Blazor.Tests;
|
||||
|
||||
public class ChartTests
|
||||
{
|
||||
private readonly ITestOutputHelper output;
|
||||
public ChartTests(ITestOutputHelper output)
|
||||
{
|
||||
this.output = output;
|
||||
}
|
||||
|
||||
[Fact(Timeout = 30000)]
|
||||
public async Task Chart_Tooltip_Performance()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
ctx.JSInterop.Setup<Rect>("Radzen.createChart", _ => true).SetResult(new Rect {Left = 0, Top = 0, Width = 200, Height = 200});
|
||||
ctx.Services.AddScoped<TooltipService>();
|
||||
ctx.JSInterop.SetupVoid("Radzen.openChartTooltip", _ => true);
|
||||
ctx.RenderComponent<RadzenChartTooltip>();
|
||||
|
||||
var seriesData = Enumerable.Range(0, 5000).Select(i => new Point { X = i, Y = i });
|
||||
var chart = ctx.RenderComponent<RadzenChart>(chartParameters =>
|
||||
@@ -33,12 +45,14 @@ public class ChartTests
|
||||
return $"{x}";
|
||||
})));
|
||||
|
||||
foreach (var _ in Enumerable.Range(0, 10))
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
foreach (var invocation in Enumerable.Range(0, 10))
|
||||
{
|
||||
await chart.InvokeAsync(() => chart.Instance.MouseMove(100, 100));
|
||||
Assert.Contains("<div class=\"rz-chart-tooltip", chart.Markup);
|
||||
await chart.InvokeAsync(() => chart.Instance.MouseMove(100, 80));
|
||||
Assert.Equal((invocation + 1) * 2, ctx.JSInterop.Invocations.Count(x => x.Identifier == "Radzen.openChartTooltip"));
|
||||
await chart.InvokeAsync(() => chart.Instance.MouseMove(0, 0));
|
||||
Assert.DoesNotContain("<div class=\"rz-chart-tooltip", chart.Markup);
|
||||
Assert.Equal(invocation + 1, ctx.JSInterop.Invocations.Count(x => x.Identifier == "Radzen.closeTooltip"));
|
||||
}
|
||||
output.WriteLine($"Time took: {stopwatch.Elapsed}");
|
||||
}
|
||||
}
|
||||
@@ -160,5 +160,83 @@ namespace Radzen.Blazor.Tests
|
||||
Assert.Contains(@$"rz-state-active", component.Markup);
|
||||
Assert.Contains(@$"rzi-times", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CheckBox_Renders_ReadonlyParameter()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
|
||||
var component = ctx.RenderComponent<RadzenCheckBox<bool>>();
|
||||
|
||||
component.SetParametersAndRender(parameters => parameters.Add<bool>(p => p.ReadOnly, true));
|
||||
|
||||
Assert.Contains(@$"readonly", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CheckBox_DoesNotRaise_ChangedEvent_ReadonlyParameter()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
|
||||
var component = ctx.RenderComponent<RadzenCheckBox<bool>>();
|
||||
|
||||
var raised = false;
|
||||
|
||||
component.SetParametersAndRender(parameters => parameters
|
||||
.Add<bool>(p => p.ReadOnly, true)
|
||||
.Add(p => p.Change, args => { raised = true; })
|
||||
);
|
||||
|
||||
component.Find("div.rz-chkbox-box").Click();
|
||||
|
||||
Assert.False(raised);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CheckBox_DoesNotRaise_ValueChangedEvent_ReadonlyParameter()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
|
||||
var component = ctx.RenderComponent<RadzenCheckBox<bool>>();
|
||||
|
||||
var raised = false;
|
||||
|
||||
component.SetParametersAndRender(parameters => parameters
|
||||
.Add<bool>(p => p.ReadOnly, true)
|
||||
.Add(p => p.ValueChanged, args => { raised = true; })
|
||||
);
|
||||
|
||||
component.Find("div.rz-chkbox-box").Click();
|
||||
|
||||
Assert.False(raised);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CheckBox_ValueNotChanged_ReadonlyParameter()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
|
||||
var component = ctx.RenderComponent<RadzenCheckBox<bool>>();
|
||||
|
||||
var value = true;
|
||||
|
||||
component.SetParametersAndRender(parameters => parameters
|
||||
.Add<bool>(p => p.ReadOnly, true)
|
||||
.Add<bool>(p => p.Value, value)
|
||||
);
|
||||
|
||||
component.Find("div.rz-chkbox-box").Click();
|
||||
|
||||
Assert.Contains(@$"rz-state-active", component.Markup);
|
||||
|
||||
component.SetParametersAndRender(parameters => parameters
|
||||
.Add<bool>(p => p.ReadOnly, !true)
|
||||
.Add<bool>(p => p.Value, value)
|
||||
);
|
||||
|
||||
component.Find("div.rz-chkbox-box").Click();
|
||||
|
||||
Assert.DoesNotContain(@$"rz-state-active", component.Markup);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,12 @@ using Microsoft.AspNetCore.Components.Rendering;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
using static System.Reflection.Metadata.BlobBuilder;
|
||||
|
||||
namespace Radzen.Blazor.Tests
|
||||
{
|
||||
@@ -20,39 +24,29 @@ namespace Radzen.Blazor.Tests
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");
|
||||
|
||||
var component = ctx.RenderComponent<RadzenGrid<dynamic>>(parameterBuilder =>
|
||||
var component = ctx.RenderComponent<RadzenDataGrid<dynamic>>(parameterBuilder =>
|
||||
{
|
||||
parameterBuilder.Add<IEnumerable<dynamic>>(p => p.Data, new[] { new { Id = 1 }, new { Id = 2 }, new { Id = 3 } });
|
||||
parameterBuilder.Add<RenderFragment>(p => p.Columns, builder =>
|
||||
{
|
||||
builder.OpenComponent(0, typeof(RadzenGridColumn<dynamic>));
|
||||
builder.OpenComponent(0, typeof(RadzenDataGridColumn<dynamic>));
|
||||
builder.AddAttribute(1, "Property", "Id");
|
||||
builder.CloseComponent();
|
||||
});
|
||||
});
|
||||
|
||||
// Main
|
||||
Assert.Contains(@$"rz-datatable-scrollable-wrapper", component.Markup);
|
||||
Assert.Contains(@$"rz-datatable-scrollable-view", component.Markup);
|
||||
Assert.Contains(@$"rz-data-grid", component.Markup);
|
||||
Assert.Contains(@$"rz-datatable", component.Markup);
|
||||
Assert.Contains(@$"rz-datatable-scrollable", component.Markup);
|
||||
|
||||
// Header
|
||||
Assert.Contains(@$"rz-datatable-scrollable-header", component.Markup);
|
||||
Assert.Contains(@$"rz-datatable-scrollable-header-box", component.Markup);
|
||||
Assert.Contains(@$"rz-datatable-thead", component.Markup);
|
||||
Assert.Contains(@$"rz-datatable-scrollable-colgroup", component.Markup);
|
||||
// Data
|
||||
Assert.Contains(@$"rz-data-grid-data", component.Markup);
|
||||
|
||||
//Body
|
||||
Assert.Contains(@$"rz-datatable-scrollable-body", component.Markup);
|
||||
Assert.Contains(@$"rz-datatable-scrollable-table-wrapper", component.Markup);
|
||||
Assert.Contains(@$"rz-datatable-data", component.Markup);
|
||||
Assert.Contains(@$"rz-datatable-hoverable-rows", component.Markup);
|
||||
|
||||
// Footer
|
||||
Assert.DoesNotContain(@$"rz-datatable-scrollable-footer", component.Markup);
|
||||
Assert.DoesNotContain(@$"rz-datatable-scrollable-footer-box", component.Markup);
|
||||
|
||||
//Columns
|
||||
Assert.DoesNotContain(@$"rz-sortable-column", component.Markup);
|
||||
// Table
|
||||
Assert.Contains(@$"rz-grid-table", component.Markup);
|
||||
Assert.Contains(@$"rz-grid-table-fixed", component.Markup);
|
||||
Assert.Contains(@$"rz-grid-table-striped", component.Markup);
|
||||
}
|
||||
|
||||
// Columns tests
|
||||
@@ -63,12 +57,12 @@ namespace Radzen.Blazor.Tests
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");
|
||||
|
||||
var component = ctx.RenderComponent<RadzenGrid<dynamic>>(parameterBuilder =>
|
||||
var component = ctx.RenderComponent<RadzenDataGrid<dynamic>>(parameterBuilder =>
|
||||
{
|
||||
parameterBuilder.Add<IEnumerable<dynamic>>(p => p.Data, new[] { new { Id = 1 }, new { Id = 2 }, new { Id = 3 } });
|
||||
parameterBuilder.Add<RenderFragment>(p => p.Columns, builder =>
|
||||
{
|
||||
builder.OpenComponent(0, typeof(RadzenGridColumn<dynamic>));
|
||||
builder.OpenComponent(0, typeof(RadzenDataGridColumn<dynamic>));
|
||||
builder.AddAttribute(1, "Property", "Id");
|
||||
builder.CloseComponent();
|
||||
});
|
||||
@@ -88,12 +82,12 @@ namespace Radzen.Blazor.Tests
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");
|
||||
|
||||
var component = ctx.RenderComponent<RadzenGrid<dynamic>>(parameterBuilder =>
|
||||
var component = ctx.RenderComponent<RadzenDataGrid<dynamic>>(parameterBuilder =>
|
||||
{
|
||||
parameterBuilder.Add<IEnumerable<dynamic>>(p => p.Data, new[] { new { Id = 1 }, new { Id = 2 }, new { Id = 3 } });
|
||||
parameterBuilder.Add<RenderFragment>(p => p.Columns, builder =>
|
||||
{
|
||||
builder.OpenComponent(0, typeof(RadzenGridColumn<dynamic>));
|
||||
builder.OpenComponent(0, typeof(RadzenDataGridColumn<dynamic>));
|
||||
builder.AddAttribute(1, "Title", "MyId");
|
||||
builder.CloseComponent();
|
||||
});
|
||||
@@ -103,6 +97,54 @@ namespace Radzen.Blazor.Tests
|
||||
Assert.Equal("MyId", title.TextContent.Trim());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DataGrid_Renders_TitleAttribute()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");
|
||||
|
||||
var component = ctx.RenderComponent<RadzenDataGrid<dynamic>>(parameterBuilder =>
|
||||
{
|
||||
parameterBuilder.Add(p => p.ShowColumnTitleAsTooltip, true);
|
||||
parameterBuilder.Add<IEnumerable<dynamic>>(p => p.Data, new[] { new { Id = 1 }, new { Id = 2 }, new { Id = 3 } });
|
||||
parameterBuilder.Add<RenderFragment>(p => p.Columns, builder =>
|
||||
{
|
||||
builder.OpenComponent(0, typeof(RadzenDataGridColumn<dynamic>));
|
||||
builder.AddAttribute(1, "Title", "MyId");
|
||||
builder.CloseComponent();
|
||||
});
|
||||
});
|
||||
|
||||
var title = component.Find(".rz-column-title");
|
||||
Assert.Equal("MyId", title.TextContent.Trim());
|
||||
Assert.Equal("MyId", title.GetAttribute("title"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DataGrid_DoesNotRender_TitleAttribute()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");
|
||||
|
||||
var component = ctx.RenderComponent<RadzenDataGrid<dynamic>>(parameterBuilder =>
|
||||
{
|
||||
parameterBuilder.Add(p => p.ShowColumnTitleAsTooltip, false);
|
||||
parameterBuilder.Add<IEnumerable<dynamic>>(p => p.Data, new[] { new { Id = 1 }, new { Id = 2 }, new { Id = 3 } });
|
||||
parameterBuilder.Add<RenderFragment>(p => p.Columns, builder =>
|
||||
{
|
||||
builder.OpenComponent(0, typeof(RadzenDataGridColumn<dynamic>));
|
||||
builder.AddAttribute(1, "Title", "MyId");
|
||||
builder.CloseComponent();
|
||||
});
|
||||
});
|
||||
|
||||
var title = component.Find(".rz-column-title");
|
||||
Assert.Equal("MyId", title.TextContent.Trim());
|
||||
Assert.Null(title.GetAttribute("title"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DataGrid_Renders_AllowSortingParameter()
|
||||
{
|
||||
@@ -110,12 +152,12 @@ namespace Radzen.Blazor.Tests
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");
|
||||
|
||||
var component = ctx.RenderComponent<RadzenGrid<dynamic>>(parameterBuilder =>
|
||||
var component = ctx.RenderComponent<RadzenDataGrid<dynamic>>(parameterBuilder =>
|
||||
{
|
||||
parameterBuilder.Add<IEnumerable<dynamic>>(p => p.Data, new[] { new { Id = 1 }, new { Id = 2 }, new { Id = 3 } });
|
||||
parameterBuilder.Add<RenderFragment>(p => p.Columns, builder =>
|
||||
{
|
||||
builder.OpenComponent(0, typeof(RadzenGridColumn<dynamic>));
|
||||
builder.OpenComponent(0, typeof(RadzenDataGridColumn<dynamic>));
|
||||
builder.AddAttribute(1, "Property", "Id");
|
||||
builder.AddAttribute(2, "Title", "Id");
|
||||
builder.CloseComponent();
|
||||
@@ -140,12 +182,12 @@ namespace Radzen.Blazor.Tests
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");
|
||||
|
||||
var component = ctx.RenderComponent<RadzenGrid<dynamic>>(parameterBuilder =>
|
||||
var component = ctx.RenderComponent<RadzenDataGrid<dynamic>>(parameterBuilder =>
|
||||
{
|
||||
parameterBuilder.Add<IEnumerable<dynamic>>(p => p.Data, new[] { new { Id = 1 }, new { Id = 2 }, new { Id = 3 } });
|
||||
parameterBuilder.Add<RenderFragment>(p => p.Columns, builder =>
|
||||
{
|
||||
builder.OpenComponent(0, typeof(RadzenGridColumn<dynamic>));
|
||||
builder.OpenComponent(0, typeof(RadzenDataGridColumn<dynamic>));
|
||||
builder.AddAttribute(1, "Property", "Id");
|
||||
builder.AddAttribute(2, "Title", "Id");
|
||||
builder.AddAttribute(3, "Sortable", false);
|
||||
@@ -164,12 +206,12 @@ namespace Radzen.Blazor.Tests
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");
|
||||
|
||||
var component = ctx.RenderComponent<RadzenGrid<dynamic>>(parameterBuilder =>
|
||||
var component = ctx.RenderComponent<RadzenDataGrid<dynamic>>(parameterBuilder =>
|
||||
{
|
||||
parameterBuilder.Add<IEnumerable<dynamic>>(p => p.Data, new[] { new { Id = 1 }, new { Id = 2 }, new { Id = 3 } });
|
||||
parameterBuilder.Add<RenderFragment>(p => p.Columns, builder =>
|
||||
{
|
||||
builder.OpenComponent(0, typeof(RadzenGridColumn<dynamic>));
|
||||
builder.OpenComponent(0, typeof(RadzenDataGridColumn<dynamic>));
|
||||
builder.AddAttribute(1, "Property", "Id");
|
||||
builder.AddAttribute(2, "Title", "Id");
|
||||
builder.CloseComponent();
|
||||
@@ -177,14 +219,14 @@ namespace Radzen.Blazor.Tests
|
||||
parameterBuilder.Add<bool>(p => p.AllowFiltering, true);
|
||||
});
|
||||
|
||||
Assert.Contains(@$"rz-cell-filter", component.Markup);
|
||||
Assert.Contains(@$"rz-grid-filter-icon", component.Markup);
|
||||
|
||||
component.SetParametersAndRender(parameters =>
|
||||
{
|
||||
parameters.Add<bool>(p => p.AllowFiltering, false);
|
||||
});
|
||||
|
||||
Assert.DoesNotContain(@$"rz-cell-filter", component.Markup);
|
||||
Assert.DoesNotContain(@$"rz-grid-filter-icon", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -194,12 +236,12 @@ namespace Radzen.Blazor.Tests
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");
|
||||
|
||||
var component = ctx.RenderComponent<RadzenGrid<dynamic>>(parameterBuilder =>
|
||||
var component = ctx.RenderComponent<RadzenDataGrid<dynamic>>(parameterBuilder =>
|
||||
{
|
||||
parameterBuilder.Add<IEnumerable<dynamic>>(p => p.Data, new[] { new { Id = 1 }, new { Id = 2 }, new { Id = 3 } });
|
||||
parameterBuilder.Add<RenderFragment>(p => p.Columns, builder =>
|
||||
{
|
||||
builder.OpenComponent(0, typeof(RadzenGridColumn<dynamic>));
|
||||
builder.OpenComponent(0, typeof(RadzenDataGridColumn<dynamic>));
|
||||
builder.AddAttribute(1, "Property", "Id");
|
||||
builder.AddAttribute(2, "Title", "Id");
|
||||
builder.AddAttribute(3, "Filterable", false);
|
||||
@@ -218,12 +260,12 @@ namespace Radzen.Blazor.Tests
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");
|
||||
|
||||
var component = ctx.RenderComponent<RadzenGrid<dynamic>>(parameterBuilder =>
|
||||
var component = ctx.RenderComponent<RadzenDataGrid<dynamic>>(parameterBuilder =>
|
||||
{
|
||||
parameterBuilder.Add<IEnumerable<dynamic>>(p => p.Data, new[] { new { Id = 1 }, new { Id = 2 }, new { Id = 3 } });
|
||||
parameterBuilder.Add<RenderFragment>(p => p.Columns, builder =>
|
||||
{
|
||||
builder.OpenComponent(0, typeof(RadzenGridColumn<dynamic>));
|
||||
builder.OpenComponent(0, typeof(RadzenDataGridColumn<dynamic>));
|
||||
builder.AddAttribute(1, "Property", "Id");
|
||||
builder.AddAttribute(2, "Title", "Id");
|
||||
builder.CloseComponent();
|
||||
@@ -249,12 +291,12 @@ namespace Radzen.Blazor.Tests
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");
|
||||
|
||||
var component = ctx.RenderComponent<RadzenGrid<int>>(parameterBuilder =>
|
||||
var component = ctx.RenderComponent<RadzenDataGrid<int>>(parameterBuilder =>
|
||||
{
|
||||
parameterBuilder.Add<IEnumerable<int>>(p => p.Data, new[] { 1, 2, 3 });
|
||||
parameterBuilder.Add<RenderFragment>(p => p.Columns, builder =>
|
||||
{
|
||||
builder.OpenComponent(0, typeof(RadzenGridColumn<int>));
|
||||
builder.OpenComponent(0, typeof(RadzenDataGridColumn<int>));
|
||||
|
||||
builder.AddAttribute(1, "HeaderTemplate", (RenderFragment)delegate (RenderTreeBuilder b)
|
||||
{
|
||||
@@ -275,12 +317,12 @@ namespace Radzen.Blazor.Tests
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");
|
||||
|
||||
var component = ctx.RenderComponent<RadzenGrid<int>>(parameterBuilder =>
|
||||
var component = ctx.RenderComponent<RadzenDataGrid<int>>(parameterBuilder =>
|
||||
{
|
||||
parameterBuilder.Add<IEnumerable<int>>(p => p.Data, new[] { 1, 2, 3 });
|
||||
parameterBuilder.Add<RenderFragment>(p => p.Columns, builder =>
|
||||
{
|
||||
builder.OpenComponent(0, typeof(RadzenGridColumn<int>));
|
||||
builder.OpenComponent(0, typeof(RadzenDataGridColumn<int>));
|
||||
|
||||
builder.AddAttribute(1, "FooterTemplate", (RenderFragment)delegate (RenderTreeBuilder b)
|
||||
{
|
||||
@@ -291,8 +333,8 @@ namespace Radzen.Blazor.Tests
|
||||
});
|
||||
});
|
||||
|
||||
Assert.Contains(@$"rz-datatable-scrollable-footer", component.Markup);
|
||||
Assert.Contains(@$"rz-datatable-scrollable-footer-box", component.Markup);
|
||||
Assert.Contains(@$"rz-datatable-tfoot", component.Markup);
|
||||
Assert.Contains(@$"rz-column-footer", component.Markup);
|
||||
Assert.Contains(@$"Footer", component.Markup);
|
||||
}
|
||||
|
||||
@@ -304,12 +346,12 @@ namespace Radzen.Blazor.Tests
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");
|
||||
|
||||
var component = ctx.RenderComponent<RadzenGrid<dynamic>>(parameterBuilder =>
|
||||
var component = ctx.RenderComponent<RadzenDataGrid<dynamic>>(parameterBuilder =>
|
||||
{
|
||||
parameterBuilder.Add<IEnumerable<dynamic>>(p => p.Data, Enumerable.Range(0, 100).Select(i => new { Id = i }));
|
||||
parameterBuilder.Add<RenderFragment>(p => p.Columns, builder =>
|
||||
{
|
||||
builder.OpenComponent(0, typeof(RadzenGridColumn<dynamic>));
|
||||
builder.OpenComponent(0, typeof(RadzenDataGridColumn<dynamic>));
|
||||
builder.AddAttribute(1, "Property", "Id");
|
||||
builder.AddAttribute(2, "Title", "Id");
|
||||
builder.CloseComponent();
|
||||
@@ -334,11 +376,20 @@ namespace Radzen.Blazor.Tests
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");
|
||||
|
||||
var component = ctx.RenderComponent<RadzenGrid<int>>(parameterBuilder => parameterBuilder.Add<IEnumerable<int>>(p => p.Data, Enumerable.Range(0, 100)));
|
||||
var component = ctx.RenderComponent<RadzenDataGrid<dynamic>>(parameterBuilder =>
|
||||
{
|
||||
parameterBuilder.Add<IEnumerable<dynamic>>(p => p.Data, Enumerable.Range(0, 100).Select(i => new { Id = i }));
|
||||
parameterBuilder.Add<RenderFragment>(p => p.Columns, builder =>
|
||||
{
|
||||
builder.OpenComponent(0, typeof(RadzenDataGridColumn<dynamic>));
|
||||
builder.AddAttribute(1, "Property", "Id");
|
||||
builder.AddAttribute(2, "Title", "Id");
|
||||
builder.CloseComponent();
|
||||
});
|
||||
parameterBuilder.Add<bool>(p => p.AllowPaging, true);
|
||||
});
|
||||
|
||||
component.SetParametersAndRender(parameters => parameters.Add<bool>(p => p.AllowPaging, true));
|
||||
|
||||
Assert.Contains(@$"rz-paginator-bottom", component.Markup);
|
||||
Assert.Contains(@$"rz-pager", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -348,16 +399,21 @@ namespace Radzen.Blazor.Tests
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");
|
||||
|
||||
var component = ctx.RenderComponent<RadzenGrid<int>>(parameterBuilder => parameterBuilder.Add<IEnumerable<int>>(p => p.Data, Enumerable.Range(0, 100)));
|
||||
|
||||
component.SetParametersAndRender(parameters =>
|
||||
var component = ctx.RenderComponent<RadzenDataGrid<dynamic>>(parameterBuilder =>
|
||||
{
|
||||
parameters.Add<bool>(p => p.AllowPaging, true);
|
||||
parameters.Add<PagerPosition>(p => p.PagerPosition, PagerPosition.Top);
|
||||
parameterBuilder.Add<IEnumerable<dynamic>>(p => p.Data, Enumerable.Range(0, 100).Select(i => new { Id = i }));
|
||||
parameterBuilder.Add<RenderFragment>(p => p.Columns, builder =>
|
||||
{
|
||||
builder.OpenComponent(0, typeof(RadzenDataGridColumn<dynamic>));
|
||||
builder.AddAttribute(1, "Property", "Id");
|
||||
builder.AddAttribute(2, "Title", "Id");
|
||||
builder.CloseComponent();
|
||||
});
|
||||
parameterBuilder.Add<bool>(p => p.AllowPaging, true);
|
||||
parameterBuilder.Add<PagerPosition>(p => p.PagerPosition, PagerPosition.Top);
|
||||
});
|
||||
|
||||
Assert.Contains(@$"rz-paginator", component.Markup);
|
||||
Assert.DoesNotContain(@$"rz-paginator-bottom", component.Markup);
|
||||
Assert.Contains(@$"rz-pager", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -367,16 +423,59 @@ namespace Radzen.Blazor.Tests
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");
|
||||
|
||||
var component = ctx.RenderComponent<RadzenGrid<int>>(parameterBuilder => parameterBuilder.Add<IEnumerable<int>>(p => p.Data, Enumerable.Range(0, 100)));
|
||||
var component = ctx.RenderComponent<RadzenDataGrid<dynamic>>(parameterBuilder =>
|
||||
{
|
||||
parameterBuilder.Add<IEnumerable<dynamic>>(p => p.Data, Enumerable.Range(0, 100).Select(i => new { Id = i }));
|
||||
parameterBuilder.Add<RenderFragment>(p => p.Columns, builder =>
|
||||
{
|
||||
builder.OpenComponent(0, typeof(RadzenDataGridColumn<dynamic>));
|
||||
builder.AddAttribute(1, "Property", "Id");
|
||||
builder.AddAttribute(2, "Title", "Id");
|
||||
builder.CloseComponent();
|
||||
});
|
||||
parameterBuilder.Add<bool>(p => p.AllowPaging, true);
|
||||
parameterBuilder.Add<PagerPosition>(p => p.PagerPosition, PagerPosition.TopAndBottom);
|
||||
});
|
||||
|
||||
Assert.Contains(@$"rz-pager", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DataGrid_Renders_PagerDensityDefault()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");
|
||||
|
||||
var component = ctx.RenderComponent<RadzenDataGrid<int>>(parameterBuilder => parameterBuilder.Add<IEnumerable<int>>(p => p.Data, Enumerable.Range(0, 100)));
|
||||
|
||||
component.SetParametersAndRender(parameters =>
|
||||
{
|
||||
parameters.Add<bool>(p => p.AllowPaging, true);
|
||||
parameters.Add<PagerPosition>(p => p.PagerPosition, PagerPosition.TopAndBottom);
|
||||
parameters.Add<PagerPosition>(p => p.PagerPosition, PagerPosition.Top);
|
||||
parameters.Add<Density>(p => p.Density, Density.Default);
|
||||
});
|
||||
|
||||
Assert.Contains(@$"rz-paginator", component.Markup);
|
||||
Assert.Contains(@$"rz-paginator-bottom", component.Markup);
|
||||
Assert.DoesNotContain(@$"rz-density-compact", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DataGrid_Renders_PagerDensityCompact()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");
|
||||
|
||||
var component = ctx.RenderComponent<RadzenDataGrid<int>>(parameterBuilder => parameterBuilder.Add<IEnumerable<int>>(p => p.Data, Enumerable.Range(0, 100)));
|
||||
|
||||
component.SetParametersAndRender(parameters =>
|
||||
{
|
||||
parameters.Add<bool>(p => p.AllowPaging, true);
|
||||
parameters.Add<PagerPosition>(p => p.PagerPosition, PagerPosition.Top);
|
||||
parameters.Add<Density>(p => p.Density, Density.Compact);
|
||||
});
|
||||
|
||||
Assert.Contains(@$"rz-density-compact", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -386,7 +485,7 @@ namespace Radzen.Blazor.Tests
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");
|
||||
|
||||
var component = ctx.RenderComponent<RadzenGrid<int>>(parameterBuilder => parameterBuilder.Add<IEnumerable<int>>(p => p.Data, Array.Empty<int>()));
|
||||
var component = ctx.RenderComponent<RadzenDataGrid<int>>(parameterBuilder => parameterBuilder.Add<IEnumerable<int>>(p => p.Data, Array.Empty<int>()));
|
||||
component.Render();
|
||||
|
||||
Assert.Contains("No records to display.", component.Markup);
|
||||
@@ -400,7 +499,7 @@ namespace Radzen.Blazor.Tests
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");
|
||||
|
||||
var component = ctx.RenderComponent<RadzenGrid<int>>(parameterBuilder => parameterBuilder.Add<IEnumerable<int>>(p => p.Data, Array.Empty<int>()));
|
||||
var component = ctx.RenderComponent<RadzenDataGrid<int>>(parameterBuilder => parameterBuilder.Add<IEnumerable<int>>(p => p.Data, Array.Empty<int>()));
|
||||
component.SetParametersAndRender(parameters =>
|
||||
{
|
||||
parameters.Add(p => p.EmptyText, emptyText);
|
||||
@@ -416,7 +515,7 @@ namespace Radzen.Blazor.Tests
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");
|
||||
|
||||
var component = ctx.RenderComponent<RadzenGrid<int>>(parameterBuilder => parameterBuilder.Add<IEnumerable<int>>(p => p.Data, Array.Empty<int>()));
|
||||
var component = ctx.RenderComponent<RadzenDataGrid<int>>(parameterBuilder => parameterBuilder.Add<IEnumerable<int>>(p => p.Data, Array.Empty<int>()));
|
||||
component.SetParametersAndRender(parameters =>
|
||||
{
|
||||
parameters.Add<RenderFragment>(p => p.EmptyTemplate, builder =>
|
||||
@@ -437,18 +536,28 @@ namespace Radzen.Blazor.Tests
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");
|
||||
|
||||
var component = ctx.RenderComponent<RadzenGrid<int>>(parameterBuilder => parameterBuilder.Add<IEnumerable<int>>(p => p.Data, Enumerable.Range(0, 100)));
|
||||
var component = ctx.RenderComponent<RadzenDataGrid<dynamic>>(parameterBuilder =>
|
||||
{
|
||||
parameterBuilder.Add<IEnumerable<dynamic>>(p => p.Data, Enumerable.Range(0, 100).Select(i => new { Id = i }));
|
||||
parameterBuilder.Add<RenderFragment>(p => p.Columns, builder =>
|
||||
{
|
||||
builder.OpenComponent(0, typeof(RadzenDataGridColumn<dynamic>));
|
||||
builder.AddAttribute(1, "Property", "Id");
|
||||
builder.AddAttribute(2, "Title", "Id");
|
||||
builder.CloseComponent();
|
||||
});
|
||||
parameterBuilder.Add<bool>(p => p.AllowPaging, true);
|
||||
});
|
||||
|
||||
var raised = false;
|
||||
LoadDataArgs newArgs = null;
|
||||
|
||||
component.SetParametersAndRender(parameters =>
|
||||
{
|
||||
parameters.Add<bool>(p => p.AllowPaging, true);
|
||||
parameters.Add<LoadDataArgs>(p => p.LoadData, args => { raised = true; newArgs = args; });
|
||||
});
|
||||
|
||||
component.Find(".rz-paginator-next").Click();
|
||||
component.Find(".rz-pager-next").Click();
|
||||
|
||||
Assert.True(raised);
|
||||
Assert.True(newArgs.Skip == 10);
|
||||
@@ -462,18 +571,28 @@ namespace Radzen.Blazor.Tests
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");
|
||||
|
||||
var component = ctx.RenderComponent<RadzenGrid<int>>(parameterBuilder => parameterBuilder.Add<IEnumerable<int>>(p => p.Data, Enumerable.Range(0, 100)));
|
||||
var component = ctx.RenderComponent<RadzenDataGrid<dynamic>>(parameterBuilder =>
|
||||
{
|
||||
parameterBuilder.Add<IEnumerable<dynamic>>(p => p.Data, Enumerable.Range(0, 100).Select(i => new { Id = i }));
|
||||
parameterBuilder.Add<RenderFragment>(p => p.Columns, builder =>
|
||||
{
|
||||
builder.OpenComponent(0, typeof(RadzenDataGridColumn<dynamic>));
|
||||
builder.AddAttribute(1, "Property", "Id");
|
||||
builder.AddAttribute(2, "Title", "Id");
|
||||
builder.CloseComponent();
|
||||
});
|
||||
parameterBuilder.Add<bool>(p => p.AllowPaging, true);
|
||||
});
|
||||
|
||||
var raised = false;
|
||||
LoadDataArgs newArgs = null;
|
||||
|
||||
component.SetParametersAndRender(parameters =>
|
||||
{
|
||||
parameters.Add<bool>(p => p.AllowPaging, true);
|
||||
parameters.Add<LoadDataArgs>(p => p.LoadData, args => { raised = true; newArgs = args; });
|
||||
});
|
||||
|
||||
component.Find(".rz-paginator-last").Click();
|
||||
component.Find(".rz-pager-last").Click();
|
||||
|
||||
Assert.True(raised);
|
||||
Assert.True(newArgs.Skip == 90);
|
||||
@@ -487,19 +606,29 @@ namespace Radzen.Blazor.Tests
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");
|
||||
|
||||
var component = ctx.RenderComponent<RadzenGrid<int>>(parameterBuilder => parameterBuilder.Add<IEnumerable<int>>(p => p.Data, Enumerable.Range(0, 100)));
|
||||
var component = ctx.RenderComponent<RadzenDataGrid<dynamic>>(parameterBuilder =>
|
||||
{
|
||||
parameterBuilder.Add<IEnumerable<dynamic>>(p => p.Data, Enumerable.Range(0, 100).Select(i => new { Id = i }));
|
||||
parameterBuilder.Add<RenderFragment>(p => p.Columns, builder =>
|
||||
{
|
||||
builder.OpenComponent(0, typeof(RadzenDataGridColumn<dynamic>));
|
||||
builder.AddAttribute(1, "Property", "Id");
|
||||
builder.AddAttribute(2, "Title", "Id");
|
||||
builder.CloseComponent();
|
||||
});
|
||||
parameterBuilder.Add<bool>(p => p.AllowPaging, true);
|
||||
});
|
||||
|
||||
var raised = false;
|
||||
LoadDataArgs newArgs = null;
|
||||
|
||||
component.SetParametersAndRender(parameters =>
|
||||
{
|
||||
parameters.Add<bool>(p => p.AllowPaging, true);
|
||||
parameters.Add<LoadDataArgs>(p => p.LoadData, args => { raised = true; newArgs = args; });
|
||||
});
|
||||
|
||||
component.Find(".rz-paginator-next").Click();
|
||||
component.Find(".rz-paginator-prev").Click();
|
||||
component.Find(".rz-pager-next").Click();
|
||||
component.Find(".rz-pager-prev").Click();
|
||||
|
||||
Assert.True(raised);
|
||||
Assert.True(newArgs.Skip == 0);
|
||||
@@ -513,19 +642,29 @@ namespace Radzen.Blazor.Tests
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");
|
||||
|
||||
var component = ctx.RenderComponent<RadzenGrid<int>>(parameterBuilder => parameterBuilder.Add<IEnumerable<int>>(p => p.Data, Enumerable.Range(0, 100)));
|
||||
var component = ctx.RenderComponent<RadzenDataGrid<dynamic>>(parameterBuilder =>
|
||||
{
|
||||
parameterBuilder.Add<IEnumerable<dynamic>>(p => p.Data, Enumerable.Range(0, 100).Select(i => new { Id = i }));
|
||||
parameterBuilder.Add<RenderFragment>(p => p.Columns, builder =>
|
||||
{
|
||||
builder.OpenComponent(0, typeof(RadzenDataGridColumn<dynamic>));
|
||||
builder.AddAttribute(1, "Property", "Id");
|
||||
builder.AddAttribute(2, "Title", "Id");
|
||||
builder.CloseComponent();
|
||||
});
|
||||
parameterBuilder.Add<bool>(p => p.AllowPaging, true);
|
||||
});
|
||||
|
||||
var raised = false;
|
||||
LoadDataArgs newArgs = null;
|
||||
|
||||
component.SetParametersAndRender(parameters =>
|
||||
{
|
||||
parameters.Add<bool>(p => p.AllowPaging, true);
|
||||
parameters.Add<LoadDataArgs>(p => p.LoadData, args => { raised = true; newArgs = args; });
|
||||
});
|
||||
|
||||
component.Find(".rz-paginator-next").Click();
|
||||
component.Find(".rz-paginator-first").Click();
|
||||
component.Find(".rz-pager-next").Click();
|
||||
component.Find(".rz-pager-first").Click();
|
||||
|
||||
Assert.True(raised);
|
||||
Assert.True(newArgs.Skip == 0);
|
||||
@@ -539,17 +678,27 @@ namespace Radzen.Blazor.Tests
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");
|
||||
|
||||
var component = ctx.RenderComponent<RadzenGrid<int>>(parameterBuilder => parameterBuilder.Add<IEnumerable<int>>(p => p.Data, Enumerable.Range(0, 100)));
|
||||
var component = ctx.RenderComponent<RadzenDataGrid<dynamic>>(parameterBuilder =>
|
||||
{
|
||||
parameterBuilder.Add<IEnumerable<dynamic>>(p => p.Data, Enumerable.Range(0, 100).Select(i => new { Id = i }));
|
||||
parameterBuilder.Add<RenderFragment>(p => p.Columns, builder =>
|
||||
{
|
||||
builder.OpenComponent(0, typeof(RadzenDataGridColumn<dynamic>));
|
||||
builder.AddAttribute(1, "Property", "Id");
|
||||
builder.AddAttribute(2, "Title", "Id");
|
||||
builder.CloseComponent();
|
||||
});
|
||||
parameterBuilder.Add<bool>(p => p.AllowPaging, true);
|
||||
});
|
||||
|
||||
var raised = false;
|
||||
|
||||
component.SetParametersAndRender(parameters =>
|
||||
{
|
||||
parameters.Add<bool>(p => p.AllowPaging, true);
|
||||
parameters.Add<LoadDataArgs>(p => p.LoadData, args => { raised = true; });
|
||||
});
|
||||
|
||||
component.Find(".rz-paginator-first").Click();
|
||||
component.Find(".rz-pager-first").Click();
|
||||
|
||||
Assert.False(raised);
|
||||
}
|
||||
@@ -561,17 +710,27 @@ namespace Radzen.Blazor.Tests
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");
|
||||
|
||||
var component = ctx.RenderComponent<RadzenGrid<int>>(parameterBuilder => parameterBuilder.Add<IEnumerable<int>>(p => p.Data, Enumerable.Range(0, 100)));
|
||||
var component = ctx.RenderComponent<RadzenDataGrid<dynamic>>(parameterBuilder =>
|
||||
{
|
||||
parameterBuilder.Add<IEnumerable<dynamic>>(p => p.Data, Enumerable.Range(0, 100).Select(i => new { Id = i }));
|
||||
parameterBuilder.Add<RenderFragment>(p => p.Columns, builder =>
|
||||
{
|
||||
builder.OpenComponent(0, typeof(RadzenDataGridColumn<dynamic>));
|
||||
builder.AddAttribute(1, "Property", "Id");
|
||||
builder.AddAttribute(2, "Title", "Id");
|
||||
builder.CloseComponent();
|
||||
});
|
||||
parameterBuilder.Add<bool>(p => p.AllowPaging, true);
|
||||
});
|
||||
|
||||
var raised = false;
|
||||
|
||||
component.SetParametersAndRender(parameters =>
|
||||
{
|
||||
parameters.Add<bool>(p => p.AllowPaging, true);
|
||||
parameters.Add<LoadDataArgs>(p => p.LoadData, args => { raised = true; });
|
||||
});
|
||||
|
||||
component.Find(".rz-paginator-prev").Click();
|
||||
component.Find(".rz-pager-prev").Click();
|
||||
|
||||
Assert.False(raised);
|
||||
}
|
||||
@@ -583,23 +742,29 @@ namespace Radzen.Blazor.Tests
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");
|
||||
|
||||
var component = ctx.RenderComponent<RadzenGrid<int>>(parameterBuilder => parameterBuilder.Add<IEnumerable<int>>(p => p.Data, Enumerable.Range(0, 100)));
|
||||
var component = ctx.RenderComponent<RadzenDataGrid<dynamic>>(parameterBuilder =>
|
||||
{
|
||||
parameterBuilder.Add<IEnumerable<dynamic>>(p => p.Data, Enumerable.Range(0, 100).Select(i => new { Id = i }));
|
||||
parameterBuilder.Add<RenderFragment>(p => p.Columns, builder =>
|
||||
{
|
||||
builder.OpenComponent(0, typeof(RadzenDataGridColumn<dynamic>));
|
||||
builder.AddAttribute(1, "Property", "Id");
|
||||
builder.AddAttribute(2, "Title", "Id");
|
||||
builder.CloseComponent();
|
||||
});
|
||||
parameterBuilder.Add<bool>(p => p.AllowPaging, true);
|
||||
});
|
||||
|
||||
var raised = false;
|
||||
|
||||
component.SetParametersAndRender(parameters =>
|
||||
{
|
||||
parameters.Add<bool>(p => p.AllowPaging, true);
|
||||
});
|
||||
|
||||
component.Find(".rz-paginator-last").Click();
|
||||
component.Find(".rz-pager-last").Click();
|
||||
|
||||
component.SetParametersAndRender(parameters =>
|
||||
{
|
||||
parameters.Add<LoadDataArgs>(p => p.LoadData, args => { raised = true; });
|
||||
});
|
||||
|
||||
component.Find(".rz-paginator-last").Click();
|
||||
component.Find(".rz-pager-last").Click();
|
||||
|
||||
Assert.False(raised);
|
||||
}
|
||||
@@ -611,23 +776,29 @@ namespace Radzen.Blazor.Tests
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");
|
||||
|
||||
var component = ctx.RenderComponent<RadzenGrid<int>>(parameterBuilder => parameterBuilder.Add<IEnumerable<int>>(p => p.Data, Enumerable.Range(0, 100)));
|
||||
var component = ctx.RenderComponent<RadzenDataGrid<dynamic>>(parameterBuilder =>
|
||||
{
|
||||
parameterBuilder.Add<IEnumerable<dynamic>>(p => p.Data, Enumerable.Range(0, 100).Select(i => new { Id = i }));
|
||||
parameterBuilder.Add<RenderFragment>(p => p.Columns, builder =>
|
||||
{
|
||||
builder.OpenComponent(0, typeof(RadzenDataGridColumn<dynamic>));
|
||||
builder.AddAttribute(1, "Property", "Id");
|
||||
builder.AddAttribute(2, "Title", "Id");
|
||||
builder.CloseComponent();
|
||||
});
|
||||
parameterBuilder.Add<bool>(p => p.AllowPaging, true);
|
||||
});
|
||||
|
||||
var raised = false;
|
||||
|
||||
component.SetParametersAndRender(parameters =>
|
||||
{
|
||||
parameters.Add<bool>(p => p.AllowPaging, true);
|
||||
});
|
||||
|
||||
component.Find(".rz-paginator-last").Click();
|
||||
component.Find(".rz-pager-last").Click();
|
||||
|
||||
component.SetParametersAndRender(parameters =>
|
||||
{
|
||||
parameters.Add<LoadDataArgs>(p => p.LoadData, args => { raised = true; });
|
||||
});
|
||||
|
||||
component.Find(".rz-paginator-next").Click();
|
||||
component.Find(".rz-pager-next").Click();
|
||||
|
||||
Assert.False(raised);
|
||||
}
|
||||
@@ -639,23 +810,184 @@ namespace Radzen.Blazor.Tests
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");
|
||||
|
||||
var component = ctx.RenderComponent<RadzenGrid<int>>(parameterBuilder => parameterBuilder.Add<IEnumerable<int>>(p => p.Data, Enumerable.Range(0, 100)));
|
||||
var component = ctx.RenderComponent<RadzenDataGrid<dynamic>>(parameterBuilder =>
|
||||
{
|
||||
parameterBuilder.Add<IEnumerable<dynamic>>(p => p.Data, Enumerable.Range(0, 100).Select(i => new { Id = i }));
|
||||
parameterBuilder.Add<RenderFragment>(p => p.Columns, builder =>
|
||||
{
|
||||
builder.OpenComponent(0, typeof(RadzenDataGridColumn<dynamic>));
|
||||
builder.AddAttribute(1, "Property", "Id");
|
||||
builder.AddAttribute(2, "Title", "Id");
|
||||
builder.CloseComponent();
|
||||
});
|
||||
parameterBuilder.Add<bool>(p => p.AllowPaging, true);
|
||||
});
|
||||
|
||||
var raised = false;
|
||||
LoadDataArgs newArgs = null;
|
||||
|
||||
component.SetParametersAndRender(parameters =>
|
||||
{
|
||||
parameters.Add<bool>(p => p.AllowPaging, true);
|
||||
parameters.Add<int>(p => p.PageSize, 20);
|
||||
parameters.Add<LoadDataArgs>(p => p.LoadData, args => { raised = true; newArgs = args; });
|
||||
});
|
||||
|
||||
component.Find(".rz-paginator-next").Click();
|
||||
component.SetParametersAndRender(parameters =>
|
||||
{
|
||||
parameters.Add<int>(p => p.PageSize, 20);
|
||||
});
|
||||
|
||||
component.Find(".rz-pager-next").Click();
|
||||
|
||||
Assert.True(raised);
|
||||
Assert.True(newArgs.Skip == 20);
|
||||
Assert.True(newArgs.Top == 20);
|
||||
}
|
||||
|
||||
// Filter tests
|
||||
|
||||
/// <summary>
|
||||
/// Utility class for testing.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Tests that involves filtering on <see cref="RadzenDataGrid{TItem}"/> requires the generic parameter to be a specific type.
|
||||
/// They do not work using <c>object</c> or <c>dynamic</c>.
|
||||
/// </remarks>
|
||||
/// <param name="Name"></param>
|
||||
/// <param name="Roles"></param>
|
||||
private sealed record User(string Name, IEnumerable<Role> Roles);
|
||||
/// <summary>
|
||||
/// Utility class for testing.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Tests that involves filtering on <see cref="RadzenDataGrid{TItem}"/> requires the generic parameter to be a specific type.
|
||||
/// They do not work using <c>object</c> or <c>dynamic</c>.
|
||||
/// </remarks>
|
||||
/// <param name="Id"></param>
|
||||
/// <param name="Description"></param>
|
||||
private sealed record Role(int Id, string Description);
|
||||
|
||||
[Fact]
|
||||
public async Task DataGrid_FilterBySubProperties_ReturnsDataFiltered()
|
||||
{
|
||||
// Arrange
|
||||
using var ctx = new TestContext();
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");
|
||||
|
||||
Role admin = new(0, "Admin");
|
||||
Role guest = new(1, "Guest");
|
||||
User moe = new("Moe", [admin]);
|
||||
User tom = new("Tom", [admin, guest]);
|
||||
User sam = new("Sam", [guest]);
|
||||
|
||||
User[] data = [moe, tom, sam];
|
||||
|
||||
var component = ctx.RenderComponent<RadzenDataGrid<User>>(parameters =>
|
||||
{
|
||||
parameters.Add(p => p.Data, data);
|
||||
parameters.Add(p => p.AllowFiltering, true);
|
||||
parameters.Add(p => p.FilterMode, FilterMode.CheckBoxList);
|
||||
parameters.Add(p => p.Columns, builder =>
|
||||
{
|
||||
builder.OpenComponent(0, typeof(RadzenDataGridColumn<User>));
|
||||
builder.AddAttribute(1, "Property", "Name");
|
||||
builder.AddAttribute(2, "Title", "User");
|
||||
builder.CloseComponent();
|
||||
builder.OpenComponent(0, typeof(RadzenDataGridColumn<User>));
|
||||
builder.AddAttribute(1, "Property", "Roles");
|
||||
builder.AddAttribute(2, "FilterProperty", "Id");
|
||||
builder.AddAttribute(3, "Type", typeof(IEnumerable<Role>));
|
||||
builder.AddAttribute(4, "Title", "Roles");
|
||||
builder.CloseComponent();
|
||||
});
|
||||
});
|
||||
|
||||
// Act
|
||||
await component.InvokeAsync(() => component
|
||||
.Instance
|
||||
.ColumnsCollection
|
||||
.First(c => c.Property == "Roles")
|
||||
.SetFilterValueAsync(new[] { 1 })
|
||||
);
|
||||
|
||||
component.Render();
|
||||
|
||||
var filteredData = await component.InvokeAsync(component.Instance.View.ToArray);
|
||||
|
||||
// Assert
|
||||
Assert.DoesNotContain(moe, filteredData);
|
||||
Assert.Contains(sam, filteredData);
|
||||
Assert.Contains(tom, filteredData);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DataGrid_LoadFilterSettingsFromJson_ReturnsDataFiltered()
|
||||
{
|
||||
// Arrange
|
||||
using var ctx = new TestContext();
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");
|
||||
|
||||
Role admin = new(0, "Admin");
|
||||
Role guest = new(1, "Guest");
|
||||
User moe = new("Moe", [admin]);
|
||||
User tom = new("Tom", [admin, guest]);
|
||||
User sam = new("Sam", [guest]);
|
||||
|
||||
User[] data = [moe, tom, sam];
|
||||
|
||||
string settings = string.Empty;
|
||||
|
||||
var component = ctx.RenderComponent<RadzenDataGrid<User>>(parameters =>
|
||||
{
|
||||
parameters.Add(p => p.Data, data);
|
||||
parameters.Add(p => p.AllowFiltering, true);
|
||||
parameters.Add(p => p.FilterMode, FilterMode.CheckBoxList);
|
||||
parameters.Add(p => p.LoadSettings, OnLoadSettings);
|
||||
parameters.Add(p => p.SettingsChanged, OnSettingsChanged);
|
||||
parameters.Add(p => p.Columns, builder =>
|
||||
{
|
||||
builder.OpenComponent(0, typeof(RadzenDataGridColumn<User>));
|
||||
builder.AddAttribute(1, "Property", "Name");
|
||||
builder.AddAttribute(2, "Title", "User");
|
||||
builder.CloseComponent();
|
||||
builder.OpenComponent(0, typeof(RadzenDataGridColumn<User>));
|
||||
builder.AddAttribute(1, "Property", "Roles");
|
||||
builder.AddAttribute(2, "FilterProperty", "Id");
|
||||
builder.AddAttribute(3, "Type", typeof(IEnumerable<Role>));
|
||||
builder.AddAttribute(4, "Title", "Roles");
|
||||
builder.CloseComponent();
|
||||
});
|
||||
});
|
||||
|
||||
void OnSettingsChanged(DataGridSettings args)
|
||||
{
|
||||
settings = JsonSerializer.Serialize(args);
|
||||
}
|
||||
|
||||
void OnLoadSettings(DataGridLoadSettingsEventArgs args)
|
||||
{
|
||||
if (string.IsNullOrEmpty(settings)) return;
|
||||
|
||||
args.Settings = JsonSerializer.Deserialize<DataGridSettings>(settings);
|
||||
}
|
||||
|
||||
// Act
|
||||
await component.InvokeAsync(() => component
|
||||
.Instance
|
||||
.ColumnsCollection
|
||||
.First(c => c.Property == "Roles")
|
||||
.SetFilterValueAsync(new[] { 1 })
|
||||
);
|
||||
|
||||
component.Render();
|
||||
|
||||
var filteredData = await component.InvokeAsync(component.Instance.View.ToArray);
|
||||
|
||||
// Assert
|
||||
Assert.DoesNotContain(moe, filteredData);
|
||||
Assert.Contains(sam, filteredData);
|
||||
Assert.Contains(tom, filteredData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ namespace Radzen.Blazor.Tests
|
||||
|
||||
component.SetParametersAndRender(parameters => parameters.Add<bool>(p => p.AllowPaging, true));
|
||||
|
||||
Assert.Contains(@$"rz-paginator-bottom", component.Markup);
|
||||
Assert.Contains(@$"rz-pager-bottom", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -44,8 +44,8 @@ namespace Radzen.Blazor.Tests
|
||||
parameters.Add<PagerPosition>(p => p.PagerPosition, PagerPosition.Top);
|
||||
});
|
||||
|
||||
Assert.Contains(@$"rz-paginator", component.Markup);
|
||||
Assert.DoesNotContain(@$"rz-paginator-bottom", component.Markup);
|
||||
Assert.Contains(@$"rz-pager", component.Markup);
|
||||
Assert.DoesNotContain(@$"rz-pager-bottom", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -60,8 +60,42 @@ namespace Radzen.Blazor.Tests
|
||||
parameters.Add<PagerPosition>(p => p.PagerPosition, PagerPosition.TopAndBottom);
|
||||
});
|
||||
|
||||
Assert.Contains(@$"rz-paginator", component.Markup);
|
||||
Assert.Contains(@$"rz-paginator-bottom", component.Markup);
|
||||
Assert.Contains(@$"rz-pager", component.Markup);
|
||||
Assert.Contains(@$"rz-pager-bottom", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DataList_Renders_PagerDensityDefault()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
|
||||
var component = ctx.RenderComponent<RadzenDataList<int>>(parameterBuilder => parameterBuilder.Add<IEnumerable<int>>(p => p.Data, Enumerable.Range(0, 100)));
|
||||
|
||||
component.SetParametersAndRender(parameters =>
|
||||
{
|
||||
parameters.Add<bool>(p => p.AllowPaging, true);
|
||||
parameters.Add<PagerPosition>(p => p.PagerPosition, PagerPosition.Top);
|
||||
parameters.Add<Density>(p => p.Density, Density.Default);
|
||||
});
|
||||
|
||||
Assert.DoesNotContain(@$"rz-density-compact", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DataList_Renders_PagerDensityCompact()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
|
||||
var component = ctx.RenderComponent<RadzenDataList<int>>(parameterBuilder => parameterBuilder.Add<IEnumerable<int>>(p => p.Data, Enumerable.Range(0, 100)));
|
||||
|
||||
component.SetParametersAndRender(parameters =>
|
||||
{
|
||||
parameters.Add<bool>(p => p.AllowPaging, true);
|
||||
parameters.Add<PagerPosition>(p => p.PagerPosition, PagerPosition.Top);
|
||||
parameters.Add<Density>(p => p.Density, Density.Compact);
|
||||
});
|
||||
|
||||
Assert.Contains(@$"rz-density-compact", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -93,7 +127,7 @@ namespace Radzen.Blazor.Tests
|
||||
parameters.Add<LoadDataArgs>(p => p.LoadData, args => { raised = true; newArgs = args; });
|
||||
});
|
||||
|
||||
component.Find(".rz-paginator-next").Click();
|
||||
component.Find(".rz-pager-next").Click();
|
||||
|
||||
Assert.True(raised);
|
||||
Assert.True(newArgs.Skip == 10);
|
||||
@@ -115,7 +149,7 @@ namespace Radzen.Blazor.Tests
|
||||
parameters.Add<LoadDataArgs>(p => p.LoadData, args => { raised = true; newArgs = args; });
|
||||
});
|
||||
|
||||
component.Find(".rz-paginator-last").Click();
|
||||
component.Find(".rz-pager-last").Click();
|
||||
|
||||
Assert.True(raised);
|
||||
Assert.True(newArgs.Skip == 90);
|
||||
@@ -137,8 +171,8 @@ namespace Radzen.Blazor.Tests
|
||||
parameters.Add<LoadDataArgs>(p => p.LoadData, args => { raised = true; newArgs = args; });
|
||||
});
|
||||
|
||||
component.Find(".rz-paginator-next").Click();
|
||||
component.Find(".rz-paginator-prev").Click();
|
||||
component.Find(".rz-pager-next").Click();
|
||||
component.Find(".rz-pager-prev").Click();
|
||||
|
||||
Assert.True(raised);
|
||||
Assert.True(newArgs.Skip == 0);
|
||||
@@ -160,8 +194,8 @@ namespace Radzen.Blazor.Tests
|
||||
parameters.Add<LoadDataArgs>(p => p.LoadData, args => { raised = true; newArgs = args; });
|
||||
});
|
||||
|
||||
component.Find(".rz-paginator-next").Click();
|
||||
component.Find(".rz-paginator-first").Click();
|
||||
component.Find(".rz-pager-next").Click();
|
||||
component.Find(".rz-pager-first").Click();
|
||||
|
||||
Assert.True(raised);
|
||||
Assert.True(newArgs.Skip == 0);
|
||||
@@ -182,7 +216,7 @@ namespace Radzen.Blazor.Tests
|
||||
parameters.Add<LoadDataArgs>(p => p.LoadData, args => { raised = true; });
|
||||
});
|
||||
|
||||
component.Find(".rz-paginator-first").Click();
|
||||
component.Find(".rz-pager-first").Click();
|
||||
|
||||
Assert.False(raised);
|
||||
}
|
||||
@@ -201,7 +235,7 @@ namespace Radzen.Blazor.Tests
|
||||
parameters.Add<LoadDataArgs>(p => p.LoadData, args => { raised = true; });
|
||||
});
|
||||
|
||||
component.Find(".rz-paginator-prev").Click();
|
||||
component.Find(".rz-pager-prev").Click();
|
||||
|
||||
Assert.False(raised);
|
||||
}
|
||||
@@ -219,13 +253,13 @@ namespace Radzen.Blazor.Tests
|
||||
parameters.Add<bool>(p => p.AllowPaging, true);
|
||||
});
|
||||
|
||||
component.Find(".rz-paginator-last").Click();
|
||||
component.Find(".rz-pager-last").Click();
|
||||
|
||||
component.SetParametersAndRender(parameters => {
|
||||
parameters.Add<LoadDataArgs>(p => p.LoadData, args => { raised = true; });
|
||||
});
|
||||
|
||||
component.Find(".rz-paginator-last").Click();
|
||||
component.Find(".rz-pager-last").Click();
|
||||
|
||||
Assert.False(raised);
|
||||
}
|
||||
@@ -243,13 +277,13 @@ namespace Radzen.Blazor.Tests
|
||||
parameters.Add<bool>(p => p.AllowPaging, true);
|
||||
});
|
||||
|
||||
component.Find(".rz-paginator-last").Click();
|
||||
component.Find(".rz-pager-last").Click();
|
||||
|
||||
component.SetParametersAndRender(parameters => {
|
||||
parameters.Add<LoadDataArgs>(p => p.LoadData, args => { raised = true; });
|
||||
});
|
||||
|
||||
component.Find(".rz-paginator-next").Click();
|
||||
component.Find(".rz-pager-next").Click();
|
||||
|
||||
Assert.False(raised);
|
||||
}
|
||||
@@ -270,7 +304,7 @@ namespace Radzen.Blazor.Tests
|
||||
parameters.Add<LoadDataArgs>(p => p.LoadData, args => { raised = true; newArgs = args; });
|
||||
});
|
||||
|
||||
component.Find(".rz-paginator-next").Click();
|
||||
component.Find(".rz-pager-next").Click();
|
||||
|
||||
Assert.True(raised);
|
||||
Assert.True(newArgs.Skip == 20);
|
||||
|
||||
@@ -18,10 +18,10 @@ namespace Radzen.Blazor.Tests
|
||||
|
||||
var component = ctx.RenderComponent<RadzenDatePicker<DateTime>>();
|
||||
|
||||
Assert.Contains(@$"rz-datepicker", component.Markup);
|
||||
Assert.Contains(@$"rz-calendar", component.Markup);
|
||||
Assert.Contains(@$"rz-datepicker-group", component.Markup);
|
||||
Assert.Contains(@$"rz-datepicker-header", component.Markup);
|
||||
Assert.Contains(@$"rz-datepicker-calendar", component.Markup);
|
||||
Assert.Contains(@$"rz-calendar-header", component.Markup);
|
||||
Assert.Contains(@$"rz-calendar-view", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -49,7 +49,8 @@ namespace Radzen.Blazor.Tests
|
||||
|
||||
var component = ctx.RenderComponent<RadzenDatePicker<DateTime>>();
|
||||
|
||||
component.SetParametersAndRender(parameters => {
|
||||
component.SetParametersAndRender(parameters =>
|
||||
{
|
||||
parameters.Add<bool>(p => p.ShowTime, true);
|
||||
parameters.Add<bool>(p => p.ShowSeconds, true);
|
||||
});
|
||||
@@ -69,7 +70,8 @@ namespace Radzen.Blazor.Tests
|
||||
|
||||
var component = ctx.RenderComponent<RadzenDatePicker<DateTime>>();
|
||||
|
||||
component.SetParametersAndRender(parameters => {
|
||||
component.SetParametersAndRender(parameters =>
|
||||
{
|
||||
parameters.Add<bool>(p => p.ShowTime, true);
|
||||
parameters.Add<bool>(p => p.ShowTimeOkButton, true);
|
||||
});
|
||||
@@ -91,7 +93,8 @@ namespace Radzen.Blazor.Tests
|
||||
|
||||
var format = "d";
|
||||
|
||||
component.SetParametersAndRender(parameters => {
|
||||
component.SetParametersAndRender(parameters =>
|
||||
{
|
||||
parameters.Add(p => p.DateFormat, format);
|
||||
parameters.Add<object>(p => p.Value, DateTime.Now);
|
||||
});
|
||||
@@ -108,7 +111,8 @@ namespace Radzen.Blazor.Tests
|
||||
|
||||
var component = ctx.RenderComponent<RadzenDatePicker<DateTime>>();
|
||||
|
||||
component.SetParametersAndRender(parameters => {
|
||||
component.SetParametersAndRender(parameters =>
|
||||
{
|
||||
parameters.Add<bool>(p => p.ShowTime, true);
|
||||
parameters.Add(p => p.HourFormat, "12");
|
||||
});
|
||||
@@ -127,12 +131,13 @@ namespace Radzen.Blazor.Tests
|
||||
|
||||
var component = ctx.RenderComponent<RadzenDatePicker<DateTime>>();
|
||||
|
||||
component.SetParametersAndRender(parameters => {
|
||||
component.SetParametersAndRender(parameters =>
|
||||
{
|
||||
parameters.Add<bool>(p => p.ShowTime, true);
|
||||
parameters.Add<bool>(p => p.TimeOnly, true);
|
||||
});
|
||||
|
||||
Assert.DoesNotContain(@$"rz-datepicker-header", component.Markup);
|
||||
Assert.DoesNotContain(@$"rz-calendar-header", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -144,12 +149,13 @@ namespace Radzen.Blazor.Tests
|
||||
|
||||
var component = ctx.RenderComponent<RadzenDatePicker<DateTime>>();
|
||||
|
||||
component.SetParametersAndRender(parameters => {
|
||||
component.SetParametersAndRender(parameters =>
|
||||
{
|
||||
parameters.Add<object>(p => p.Value, DateTime.Now);
|
||||
parameters.Add<bool>(p => p.AllowClear, true);
|
||||
});
|
||||
|
||||
Assert.Contains(@$"<i class=""rz-dropdown-clear-icon rzi rzi-times""", component.Markup);
|
||||
Assert.Contains(@$"<i class=""notranslate rz-dropdown-clear-icon rzi rzi-times""", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -168,6 +174,34 @@ namespace Radzen.Blazor.Tests
|
||||
Assert.Contains(@$"tabindex=""{value}""", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DatePicker_Renders_EmptyCssClass_WhenValueIsEmpty()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");
|
||||
|
||||
var component = ctx.RenderComponent<RadzenDatePicker<DateTime>>();
|
||||
|
||||
component.SetParametersAndRender(parameters => parameters.Add(p => p.Value, null));
|
||||
|
||||
Assert.Contains(@$"rz-state-empty", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DatePicker_DoesNotRender_EmptyCssClass_WhenValueIsNotEmpty()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");
|
||||
|
||||
var component = ctx.RenderComponent<RadzenDatePicker<DateTime>>();
|
||||
|
||||
component.SetParametersAndRender(parameters => parameters.Add(p => p.Value, DateTime.Now));
|
||||
|
||||
Assert.DoesNotContain(@$"rz-state-empty", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DatePicker_Renders_DisabledParameter()
|
||||
{
|
||||
@@ -212,7 +246,7 @@ namespace Radzen.Blazor.Tests
|
||||
|
||||
component.SetParametersAndRender(parameters => parameters.Add(p => p.Style, value));
|
||||
|
||||
Assert.Contains(@$"style=""display: inline-block;{value}""", component.Markup);
|
||||
Assert.Contains(@$"style=""{value}""", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -241,11 +275,12 @@ namespace Radzen.Blazor.Tests
|
||||
var raised = false;
|
||||
object newValue = null;
|
||||
|
||||
component.SetParametersAndRender(parameters => {
|
||||
component.SetParametersAndRender(parameters =>
|
||||
{
|
||||
parameters.Add(p => p.Change, args => { raised = true; newValue = args; });
|
||||
});
|
||||
|
||||
component.Find(".rz-datepicker-next-icon").Click();
|
||||
component.Find(".rz-calendar-next-icon").Click();
|
||||
|
||||
Assert.False(raised);
|
||||
}
|
||||
@@ -262,11 +297,12 @@ namespace Radzen.Blazor.Tests
|
||||
var raised = false;
|
||||
object newValue = null;
|
||||
|
||||
component.SetParametersAndRender(parameters => {
|
||||
component.SetParametersAndRender(parameters =>
|
||||
{
|
||||
parameters.Add(p => p.ValueChanged, args => { raised = true; newValue = args; });
|
||||
});
|
||||
|
||||
component.Find(".rz-datepicker-next-icon").Click();
|
||||
component.Find(".rz-calendar-next-icon").Click();
|
||||
|
||||
Assert.False(raised);
|
||||
}
|
||||
@@ -283,11 +319,12 @@ namespace Radzen.Blazor.Tests
|
||||
var raised = false;
|
||||
object newValue = null;
|
||||
|
||||
component.SetParametersAndRender(parameters => {
|
||||
component.SetParametersAndRender(parameters =>
|
||||
{
|
||||
parameters.Add(p => p.Change, args => { raised = true; newValue = args; });
|
||||
});
|
||||
|
||||
component.Find(".rz-datepicker-prev-icon").Click();
|
||||
component.Find(".rz-calendar-prev-icon").Click();
|
||||
|
||||
Assert.False(raised);
|
||||
}
|
||||
@@ -304,11 +341,12 @@ namespace Radzen.Blazor.Tests
|
||||
var raised = false;
|
||||
object newValue = null;
|
||||
|
||||
component.SetParametersAndRender(parameters => {
|
||||
component.SetParametersAndRender(parameters =>
|
||||
{
|
||||
parameters.Add(p => p.ValueChanged, args => { raised = true; newValue = args; });
|
||||
});
|
||||
|
||||
component.Find(".rz-datepicker-prev-icon").Click();
|
||||
component.Find(".rz-calendar-prev-icon").Click();
|
||||
|
||||
Assert.False(raised);
|
||||
}
|
||||
@@ -327,7 +365,8 @@ namespace Radzen.Blazor.Tests
|
||||
var raised = false;
|
||||
object newValue = null;
|
||||
|
||||
component.SetParametersAndRender(parameters => {
|
||||
component.SetParametersAndRender(parameters =>
|
||||
{
|
||||
parameters.Add(p => p.ValueChanged, args => { raised = true; newValue = args; })
|
||||
.Add(p => p.DateRender, args => { args.Disabled = dates.Contains(args.Date); });
|
||||
});
|
||||
@@ -360,7 +399,8 @@ namespace Radzen.Blazor.Tests
|
||||
var raised = false;
|
||||
object newValue = null;
|
||||
|
||||
component.SetParametersAndRender(parameters => {
|
||||
component.SetParametersAndRender(parameters =>
|
||||
{
|
||||
parameters.Add(p => p.ValueChanged, args => { raised = true; newValue = args; })
|
||||
.Add(p => p.DateRender, args => { args.Disabled = dates.Contains(args.Date); });
|
||||
});
|
||||
@@ -379,6 +419,72 @@ namespace Radzen.Blazor.Tests
|
||||
Assert.Null(newValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DatePicker_Parses_Input_Using_DateFormat()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
|
||||
var component = ctx.RenderComponent<RadzenDatePicker<DateTime?>>();
|
||||
|
||||
var raised = false;
|
||||
object newValue = null;
|
||||
|
||||
component.SetParametersAndRender(parameters =>
|
||||
{
|
||||
parameters.Add(p => p.DateFormat, "ddMM");
|
||||
parameters.Add(p => p.ValueChanged, args => { raised = true; newValue = args; });
|
||||
});
|
||||
|
||||
var inputElement = component.Find(".rz-inputtext");
|
||||
|
||||
string input = "3012";
|
||||
ctx.JSInterop.Setup<string>("Radzen.getInputValue", invocation => true).SetResult(input);
|
||||
inputElement.Change(input);
|
||||
|
||||
Assert.True(raised);
|
||||
Assert.Equal(new DateTime(DateTime.Now.Year, 12, 30), newValue);
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public void DatePicker_Parses_Input_Using_ParseInput()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
|
||||
var component = ctx.RenderComponent<RadzenDatePicker<DateTime?>>();
|
||||
|
||||
Func<string, DateTime?> customParseInput = (input) =>
|
||||
{
|
||||
if (DateTime.TryParseExact(input, "ddMM", null, DateTimeStyles.None, out var result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
var raised = false;
|
||||
object newValue = null;
|
||||
|
||||
component.SetParametersAndRender(parameters =>
|
||||
{
|
||||
parameters.Add(p => p.ParseInput, customParseInput);
|
||||
parameters.Add(p => p.ValueChanged, args => { raised = true; newValue = args; });
|
||||
});
|
||||
|
||||
var inputElement = component.Find(".rz-inputtext");
|
||||
|
||||
string input = "3012";
|
||||
ctx.JSInterop.Setup<string>("Radzen.getInputValue", invocation => true).SetResult(input);
|
||||
inputElement.Change(input);
|
||||
|
||||
Assert.True(raised);
|
||||
Assert.Equal(new DateTime(DateTime.Now.Year, 12, 30), newValue);
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public void DatePicker_Respects_DateTimeMaxValue()
|
||||
{
|
||||
@@ -393,26 +499,7 @@ namespace Radzen.Blazor.Tests
|
||||
|
||||
Assert.Contains(DateTime.MaxValue.ToString(component.Instance.DateFormat), component.Markup);
|
||||
|
||||
var exception = Record.Exception(() => component.Find(".rz-datepicker-next-icon")
|
||||
.Click());
|
||||
Assert.Null(exception);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DatePicker_Respects_DateTimeMinValue()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");
|
||||
|
||||
var component = ctx.RenderComponent<RadzenDatePicker<DateTime>>(parameters =>
|
||||
{
|
||||
parameters.Add(p => p.Value, DateTime.MinValue);
|
||||
});
|
||||
|
||||
Assert.Contains(DateTime.MinValue.ToString(component.Instance.DateFormat), component.Markup);
|
||||
|
||||
var exception = Record.Exception(() => component.Find(".rz-datepicker-prev-icon")
|
||||
var exception = Record.Exception(() => component.Find(".rz-calendar-next-icon")
|
||||
.Click());
|
||||
Assert.Null(exception);
|
||||
}
|
||||
@@ -441,7 +528,7 @@ namespace Radzen.Blazor.Tests
|
||||
parameters.Add(p => p.Change, args => { raised = true; newValue = args; });
|
||||
});
|
||||
|
||||
component.Find(".rz-datepicker-next-icon").Click();
|
||||
component.Find(".rz-calendar-next-icon").Click();
|
||||
component.FindAll(".rz-button-text").First(x => x.TextContent == "Ok").Click();
|
||||
|
||||
Assert.True(raised);
|
||||
@@ -505,5 +592,145 @@ namespace Radzen.Blazor.Tests
|
||||
Assert.Equal(kind, (component.Instance.Value as DateTime?)?.Kind);
|
||||
Assert.Equal(valueUtc.UtcDateTime.ToString(CultureInfo.InvariantCulture), (component.Instance.Value as DateTime?)?.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DatePicker_Displays_Calender_Icon()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");
|
||||
|
||||
var component = ctx.RenderComponent<RadzenDatePicker<DateTime>>();
|
||||
|
||||
Assert.Contains(@$"rzi-calendar", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DatePicker_Displays_Schedule_Icon()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");
|
||||
|
||||
var component = ctx.RenderComponent<RadzenDatePicker<DateTime>>(parameters =>
|
||||
{
|
||||
parameters.Add(p => p.TimeOnly, true);
|
||||
});
|
||||
|
||||
Assert.Contains(@$"rzi-time", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DatePicker_Supports_DateOnly()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");
|
||||
|
||||
DateOnly? dateOnly = new DateOnly(2024, 1, 31);
|
||||
DateOnly? valueChangedValue = null!;
|
||||
var component = ctx.RenderComponent<RadzenDatePicker<DateOnly?>>(parameters =>
|
||||
{
|
||||
parameters.Add(p => p.Value, dateOnly);
|
||||
parameters.Add(p => p.ValueChanged, args => { valueChangedValue = args; });
|
||||
});
|
||||
|
||||
Assert.False(component.Instance.ShowTime);
|
||||
var input = component.Find("input");
|
||||
input.GetAttribute("value").MarkupMatches(dateOnly.ToString());
|
||||
|
||||
// update to new value
|
||||
var inputElement = component.Find(".rz-inputtext");
|
||||
DateOnly? enteredValue = new DateOnly(2024, 2, 28);
|
||||
ctx.JSInterop.Setup<string>("Radzen.getInputValue", invocation => true).SetResult(enteredValue.Value.ToShortDateString());
|
||||
inputElement.Change(enteredValue);
|
||||
|
||||
input.GetAttribute("value").MarkupMatches(enteredValue.ToString());
|
||||
Assert.Equal(enteredValue, component.Instance.Value);
|
||||
Assert.Equal(enteredValue, valueChangedValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DatePicker_Supports_TimeOnly()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");
|
||||
|
||||
TimeOnly? timeOnly = new TimeOnly(23, 59, 59);
|
||||
TimeOnly? valueChangedValue = null!;
|
||||
var component = ctx.RenderComponent<RadzenDatePicker<TimeOnly>>(parameters =>
|
||||
{
|
||||
parameters.Add(p => p.Value, timeOnly);
|
||||
parameters.Add(p => p.ValueChanged, args => { valueChangedValue = args; });
|
||||
});
|
||||
|
||||
Assert.True(component.Instance.TimeOnly);
|
||||
Assert.True(component.Instance.ShowTime);
|
||||
var input = component.Find("input");
|
||||
input.GetAttribute("value").MarkupMatches(timeOnly.ToString());
|
||||
|
||||
// update to new value
|
||||
var inputElement = component.Find(".rz-inputtext");
|
||||
TimeOnly? enteredValue = new TimeOnly(1, 4, 5);
|
||||
ctx.JSInterop.Setup<string>("Radzen.getInputValue", invocation => true).SetResult(enteredValue.Value.ToLongTimeString());
|
||||
inputElement.Change(enteredValue);
|
||||
|
||||
input.GetAttribute("value").MarkupMatches(enteredValue.ToString());
|
||||
Assert.Equal(enteredValue, component.Instance.Value);
|
||||
Assert.Equal(enteredValue, valueChangedValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DatePicker_ShowCalendarWeek_WeekNumberAddedInAdditionalColumn()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");
|
||||
|
||||
var component = ctx.RenderComponent<RadzenDatePicker<DateTime>>(parameter =>
|
||||
{
|
||||
parameter.Add(p => p.ShowCalendarWeek, true);
|
||||
});
|
||||
|
||||
Assert.Contains(@$"rz-calendar-week-number", component.Markup);
|
||||
Assert.Equal(8, component.FindAll(".rz-calendar-view th").Count());
|
||||
// check header and week number column
|
||||
Assert.Single(component.FindAll("th.rz-datepicker-week-number"));
|
||||
Assert.Equal(6, component.FindAll("td.rz-calendar-week-number").Count());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DatePicker_ShowCalendarWeekFalse_NoAdditionalColumn()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");
|
||||
|
||||
var component = ctx.RenderComponent<RadzenDatePicker<DateTime>>(parameter =>
|
||||
{
|
||||
parameter.Add(p => p.ShowCalendarWeek, false);
|
||||
});
|
||||
|
||||
Assert.DoesNotContain(@$"rz-calendar-week-number", component.Markup);
|
||||
Assert.Equal(7, component.FindAll(".rz-calendar-view th").Count());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DatePicker_ShowCalendarWeekWithCustomTitle_TitleCorrectlyRendered()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");
|
||||
|
||||
var component = ctx.RenderComponent<RadzenDatePicker<DateTime>>(parameter =>
|
||||
{
|
||||
parameter.Add(p => p.ShowCalendarWeek, true);
|
||||
parameter.Add(p => p.CalendarWeekTitle, "Wk");
|
||||
});
|
||||
|
||||
var weekNumberHeader = component.Find(".rz-calendar-view th.rz-datepicker-week-number");
|
||||
Assert.Contains("Wk", weekNumberHeader.InnerHtml);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
341
Radzen.Blazor.Tests/DialogServiceTests.cs
Normal file
341
Radzen.Blazor.Tests/DialogServiceTests.cs
Normal file
@@ -0,0 +1,341 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Tests
|
||||
{
|
||||
public class DialogServiceTests
|
||||
{
|
||||
public class OpenDialogTests
|
||||
{
|
||||
[Fact(DisplayName = "DialogOptions default values are set correctly")]
|
||||
public void DialogOptions_DefaultValues_AreSetCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var options = new DialogOptions();
|
||||
var dialogService = new DialogService(null, null);
|
||||
|
||||
// Act
|
||||
dialogService.OpenDialog<DialogServiceTests>("Test", [], options);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("600px", options.Width);
|
||||
Assert.Equal("", options.Left);
|
||||
Assert.Equal("", options.Top);
|
||||
Assert.Equal("", options.Bottom);
|
||||
Assert.Equal("", options.Height);
|
||||
Assert.Equal("", options.Style);
|
||||
Assert.Equal("", options.CssClass);
|
||||
Assert.Equal("", options.WrapperCssClass);
|
||||
Assert.Equal("", options.ContentCssClass);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "DialogOptions values are retained after OpenDialog call")]
|
||||
public void DialogOptions_Values_AreRetained_AfterOpenDialogCall()
|
||||
{
|
||||
// Arrange
|
||||
var options = new DialogOptions
|
||||
{
|
||||
Width = "800px",
|
||||
Left = "10px",
|
||||
Top = "20px",
|
||||
Bottom = "30px",
|
||||
Height = "400px",
|
||||
Style = "background-color: red;",
|
||||
CssClass = "custom-class",
|
||||
WrapperCssClass = "wrapper-class",
|
||||
ContentCssClass = "content-class"
|
||||
};
|
||||
var dialogService = new DialogService(null, null);
|
||||
|
||||
// Act
|
||||
dialogService.OpenDialog<DialogServiceTests>("Test", [], options);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("800px", options.Width);
|
||||
Assert.Equal("10px", options.Left);
|
||||
Assert.Equal("20px", options.Top);
|
||||
Assert.Equal("30px", options.Bottom);
|
||||
Assert.Equal("400px", options.Height);
|
||||
Assert.Equal("background-color: red;", options.Style);
|
||||
Assert.Equal("custom-class", options.CssClass);
|
||||
Assert.Equal("wrapper-class", options.WrapperCssClass);
|
||||
Assert.Equal("content-class", options.ContentCssClass);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "DialogOptions is null and default values are set correctly")]
|
||||
public void DialogOptions_IsNull_DefaultValues_AreSetCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
DialogOptions resultingOptions = null;
|
||||
var dialogService = new DialogService(null, null);
|
||||
dialogService.OnOpen += (title, type, parameters, options) => resultingOptions = options;
|
||||
|
||||
// Act
|
||||
dialogService.OpenDialog<DialogServiceTests>("Test", [], null);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(resultingOptions);
|
||||
Assert.Equal("600px", resultingOptions.Width);
|
||||
Assert.Equal("", resultingOptions.Left);
|
||||
Assert.Equal("", resultingOptions.Top);
|
||||
Assert.Equal("", resultingOptions.Bottom);
|
||||
Assert.Equal("", resultingOptions.Height);
|
||||
Assert.Equal("", resultingOptions.Style);
|
||||
Assert.Equal("", resultingOptions.CssClass);
|
||||
Assert.Equal("", resultingOptions.WrapperCssClass);
|
||||
Assert.Equal("", resultingOptions.ContentCssClass);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Open with dynamic component type reflective calls are resolved without exception")]
|
||||
public void Open_DynamicComponentType_Reflective_Calls_Resolve()
|
||||
{
|
||||
// Arrange
|
||||
string resultingTitle = null;
|
||||
Type resultingType = null;
|
||||
var dialogService = new DialogService(null, null);
|
||||
dialogService.OnOpen += (title, type, _, _) =>
|
||||
{
|
||||
resultingTitle = title;
|
||||
resultingType = type;
|
||||
};
|
||||
|
||||
dialogService.Open("Dynamic Open", typeof(RadzenButton), []);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("Dynamic Open", resultingTitle);
|
||||
Assert.Equal(typeof(RadzenButton), resultingType);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "OpenAsync with dynamic component type reflective calls are resolved without exception")]
|
||||
public async Task OpenAsync_DynamicComponentType_Reflective_Calls_Resolve()
|
||||
{
|
||||
// Arrange
|
||||
string resultingTitle = null;
|
||||
Type resultingType = null;
|
||||
var dialogService = new DialogService(null, null);
|
||||
dialogService.OnOpen += (title, type, _, _) =>
|
||||
{
|
||||
resultingTitle = title;
|
||||
resultingType = type;
|
||||
};
|
||||
|
||||
var openTask = dialogService.OpenAsync("Dynamic Open", typeof(RadzenButton), []);
|
||||
dialogService.Close();
|
||||
await openTask;
|
||||
|
||||
// Assert
|
||||
Assert.Equal("Dynamic Open", resultingTitle);
|
||||
Assert.Equal(typeof(RadzenButton), resultingType);
|
||||
}
|
||||
}
|
||||
|
||||
public class ConfirmTests
|
||||
{
|
||||
[Fact(DisplayName = "ConfirmOptions is null and default values are set correctly")]
|
||||
public async Task ConfirmOptions_IsNull_AreSetCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var dialogService = new DialogService(null, null);
|
||||
ConfirmOptions resultingOptions = null;
|
||||
dialogService.OnOpen += (title, type, parameters, options) => resultingOptions = options as ConfirmOptions;
|
||||
|
||||
using var cancellationTokenSource = new CancellationTokenSource();
|
||||
cancellationTokenSource.Cancel();
|
||||
|
||||
// Act
|
||||
try
|
||||
{
|
||||
await dialogService.Confirm(cancellationToken: cancellationTokenSource.Token);
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
// this is expected
|
||||
}
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(resultingOptions);
|
||||
Assert.Equal("Ok", resultingOptions.OkButtonText);
|
||||
Assert.Equal("Cancel", resultingOptions.CancelButtonText);
|
||||
Assert.Equal("600px", resultingOptions.Width);
|
||||
Assert.Equal("", resultingOptions.Style);
|
||||
Assert.Equal("rz-dialog-confirm", resultingOptions.CssClass);
|
||||
Assert.Equal("rz-dialog-wrapper", resultingOptions.WrapperCssClass);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "ConfirmOptions default values are set correctly")]
|
||||
public async Task ConfirmOptions_DefaultValues_AreSetCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var dialogService = new DialogService(null, null);
|
||||
ConfirmOptions resultingOptions = null;
|
||||
dialogService.OnOpen += (title, type, parameters, options) => resultingOptions = options as ConfirmOptions;
|
||||
|
||||
using var cancellationTokenSource = new CancellationTokenSource();
|
||||
cancellationTokenSource.Cancel();
|
||||
|
||||
// Act
|
||||
try
|
||||
{
|
||||
await dialogService.Confirm(options: new(), cancellationToken: cancellationTokenSource.Token);
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
// this is expected
|
||||
}
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(resultingOptions);
|
||||
Assert.Equal("Ok", resultingOptions.OkButtonText);
|
||||
Assert.Equal("Cancel", resultingOptions.CancelButtonText);
|
||||
Assert.Equal("600px", resultingOptions.Width);
|
||||
Assert.Equal("", resultingOptions.Style);
|
||||
Assert.Equal("rz-dialog-confirm", resultingOptions.CssClass);
|
||||
Assert.Equal("rz-dialog-wrapper", resultingOptions.WrapperCssClass);
|
||||
}
|
||||
[Fact(DisplayName = "ConfirmOptions values are retained after Confirm call")]
|
||||
public async Task Confirm_ProvidedValues_AreRetained()
|
||||
{
|
||||
// Arrange
|
||||
var dialogService = new DialogService(null, null);
|
||||
var options = new ConfirmOptions
|
||||
{
|
||||
OkButtonText = "XXX",
|
||||
CancelButtonText = "YYY",
|
||||
Width = "800px",
|
||||
Style = "background-color: red;",
|
||||
CssClass = "custom-class",
|
||||
WrapperCssClass = "wrapper-class"
|
||||
};
|
||||
ConfirmOptions resultingOptions = null;
|
||||
dialogService.OnOpen += (title, type, parameters, options) => resultingOptions = options as ConfirmOptions;
|
||||
|
||||
// We break out of the dialog immediately, but the options should still be set
|
||||
using var cancellationTokenSource = new CancellationTokenSource();
|
||||
cancellationTokenSource.Cancel();
|
||||
|
||||
// Act
|
||||
try
|
||||
{
|
||||
await dialogService.Confirm("Confirm?", "Confirm", options, cancellationToken: cancellationTokenSource.Token);
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
// this is expected
|
||||
}
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(resultingOptions);
|
||||
Assert.Equal("XXX", resultingOptions.OkButtonText);
|
||||
Assert.Equal("YYY", resultingOptions.CancelButtonText);
|
||||
Assert.Equal("800px", resultingOptions.Width);
|
||||
Assert.Equal("background-color: red;", resultingOptions.Style);
|
||||
Assert.Equal("rz-dialog-confirm custom-class", resultingOptions.CssClass);
|
||||
Assert.Equal("rz-dialog-wrapper wrapper-class", resultingOptions.WrapperCssClass);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public class AlertTests
|
||||
{
|
||||
[Fact(DisplayName = "AlertOptions is null and default values are set correctly")]
|
||||
public async Task AlertOptions_IsNull_AreSetCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var dialogService = new DialogService(null, null);
|
||||
AlertOptions resultingOptions = null;
|
||||
dialogService.OnOpen += (title, type, parameters, options) => resultingOptions = options as AlertOptions;
|
||||
|
||||
using var cancellationTokenSource = new CancellationTokenSource();
|
||||
cancellationTokenSource.Cancel();
|
||||
|
||||
// Act
|
||||
try
|
||||
{
|
||||
await dialogService.Alert(cancellationToken: cancellationTokenSource.Token);
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
// this is expected
|
||||
}
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(resultingOptions);
|
||||
Assert.Equal("Ok", resultingOptions.OkButtonText);
|
||||
Assert.Equal("600px", resultingOptions.Width);
|
||||
Assert.Equal("", resultingOptions.Style);
|
||||
Assert.Equal("rz-dialog-alert", resultingOptions.CssClass);
|
||||
Assert.Equal("rz-dialog-wrapper", resultingOptions.WrapperCssClass);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "AlertOptions default values are set correctly")]
|
||||
public async Task AlertOptions_DefaultValues_AreSetCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var dialogService = new DialogService(null, null);
|
||||
AlertOptions resultingOptions = null;
|
||||
dialogService.OnOpen += (title, type, parameters, options) => resultingOptions = options as AlertOptions;
|
||||
|
||||
using var cancellationTokenSource = new CancellationTokenSource();
|
||||
cancellationTokenSource.Cancel();
|
||||
|
||||
// Act
|
||||
try
|
||||
{
|
||||
await dialogService.Alert(options: new(), cancellationToken: cancellationTokenSource.Token);
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
// this is expected
|
||||
}
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(resultingOptions);
|
||||
Assert.Equal("Ok", resultingOptions.OkButtonText);
|
||||
Assert.Equal("600px", resultingOptions.Width);
|
||||
Assert.Equal("", resultingOptions.Style);
|
||||
Assert.Equal("rz-dialog-alert", resultingOptions.CssClass);
|
||||
Assert.Equal("rz-dialog-wrapper", resultingOptions.WrapperCssClass);
|
||||
}
|
||||
[Fact(DisplayName = "AlertOptions values are retained after Alert call")]
|
||||
public async Task Alert_ProvidedValues_AreRetained()
|
||||
{
|
||||
// Arrange
|
||||
var dialogService = new DialogService(null, null);
|
||||
var options = new AlertOptions
|
||||
{
|
||||
OkButtonText = "XXX",
|
||||
Width = "800px",
|
||||
Style = "background-color: red;",
|
||||
CssClass = "custom-class",
|
||||
WrapperCssClass = "wrapper-class"
|
||||
};
|
||||
AlertOptions resultingOptions = null;
|
||||
dialogService.OnOpen += (title, type, parameters, options) => resultingOptions = options as AlertOptions;
|
||||
|
||||
// We break out of the dialog immediately, but the options should still be set
|
||||
using var cancellationTokenSource = new CancellationTokenSource();
|
||||
cancellationTokenSource.Cancel();
|
||||
|
||||
// Act
|
||||
try
|
||||
{
|
||||
await dialogService.Alert("Alert?", "Alert", options, cancellationToken: cancellationTokenSource.Token);
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
// this is expected
|
||||
}
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(resultingOptions);
|
||||
Assert.Equal("XXX", resultingOptions.OkButtonText);
|
||||
Assert.Equal("800px", resultingOptions.Width);
|
||||
Assert.Equal("background-color: red;", resultingOptions.Style);
|
||||
Assert.Equal("rz-dialog-alert custom-class", resultingOptions.CssClass);
|
||||
Assert.Equal("rz-dialog-wrapper wrapper-class", resultingOptions.WrapperCssClass);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
58
Radzen.Blazor.Tests/Dollars.cs
Normal file
58
Radzen.Blazor.Tests/Dollars.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Globalization;
|
||||
|
||||
namespace Radzen.Blazor.Tests;
|
||||
|
||||
[TypeConverter(typeof(DollarsTypeConverter))]
|
||||
public readonly record struct Dollars(decimal Amount) : IComparable<decimal>
|
||||
{
|
||||
public int CompareTo(decimal other)
|
||||
{
|
||||
return Amount.CompareTo(other);
|
||||
}
|
||||
|
||||
public string ToString(string format, CultureInfo culture = null) => Amount.ToString(format, culture ?? CultureInfo.CreateSpecificCulture("en-US"));
|
||||
public override string ToString() => Amount.ToString("F2", CultureInfo.CreateSpecificCulture("en-US"));
|
||||
}
|
||||
|
||||
public class DollarsTypeConverter : TypeConverter
|
||||
{
|
||||
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
|
||||
{
|
||||
if (sourceType == typeof(decimal) ||
|
||||
sourceType == typeof(string))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return base.CanConvertFrom(context, sourceType);
|
||||
}
|
||||
|
||||
public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
|
||||
{
|
||||
if (destinationType == typeof(decimal))
|
||||
return true;
|
||||
|
||||
return base.CanConvertTo(context, destinationType);
|
||||
}
|
||||
|
||||
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
|
||||
{
|
||||
if (value is decimal d)
|
||||
return new Dollars(d);
|
||||
|
||||
if (value is string s)
|
||||
return decimal.TryParse(s, culture, out var val) ? new Dollars(val) : null;
|
||||
|
||||
return base.ConvertFrom(context, culture, value);
|
||||
}
|
||||
|
||||
public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
|
||||
{
|
||||
if (destinationType == typeof(decimal) && value is Dollars d)
|
||||
return d.Amount;
|
||||
|
||||
return base.ConvertTo(context, culture, value, destinationType);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,11 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using AngleSharp.Dom;
|
||||
using Bunit;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Tests
|
||||
@@ -11,18 +16,20 @@ namespace Radzen.Blazor.Tests
|
||||
{
|
||||
public string Text { get; set; }
|
||||
public int Id { get; set; }
|
||||
public bool Disabled { get; set; }
|
||||
}
|
||||
|
||||
private static IRenderedComponent<RadzenDropDown<T>> DropDown<T>(TestContext ctx, Action<ComponentParameterCollectionBuilder<RadzenDropDown<T>>> configure = null)
|
||||
{
|
||||
var data = new [] {
|
||||
var data = new[] {
|
||||
new DataItem { Text = "Item 1", Id = 1 },
|
||||
new DataItem { Text = "Item 2", Id = 2 },
|
||||
};
|
||||
|
||||
var component = ctx.RenderComponent<RadzenDropDown<T>>();
|
||||
|
||||
component.SetParametersAndRender(parameters => {
|
||||
component.SetParametersAndRender(parameters =>
|
||||
{
|
||||
parameters.Add(p => p.Data, data);
|
||||
parameters.Add(p => p.TextProperty, nameof(DataItem.Text));
|
||||
|
||||
@@ -39,6 +46,7 @@ namespace Radzen.Blazor.Tests
|
||||
return component;
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public async Task Dropdown_SelectItem_Method_Should_Not_Throw()
|
||||
{
|
||||
@@ -97,7 +105,8 @@ namespace Radzen.Blazor.Tests
|
||||
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
|
||||
var component = DropDown<string>(ctx, parameters => {
|
||||
var component = DropDown<string>(ctx, parameters =>
|
||||
{
|
||||
parameters.Add(p => p.ValueProperty, nameof(DataItem.Text));
|
||||
});
|
||||
|
||||
@@ -112,6 +121,35 @@ namespace Radzen.Blazor.Tests
|
||||
Assert.Contains("rz-state-highlight", items[0].ClassList);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DropDown_Respects_ItemEqualityComparer()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
|
||||
List<DataItem> boundCollection = [new() { Text = "Item 2" }];
|
||||
|
||||
var component = DropDown<List<DataItem>>(ctx, parameters =>
|
||||
{
|
||||
parameters.Add(p => p.ItemComparer, new DataItemComparer());
|
||||
parameters.Add(p => p.Multiple, true);
|
||||
parameters.Add(p => p.Value, boundCollection);
|
||||
});
|
||||
|
||||
var selectedItems = component.FindAll(".rz-state-highlight");
|
||||
Assert.Equal(1, selectedItems.Count);
|
||||
Assert.Equal("Item 2", selectedItems[0].TextContent.Trim());
|
||||
|
||||
// select Item 1 in list
|
||||
var items = component.FindAll(".rz-multiselect-item");
|
||||
items[0].Click();
|
||||
component.Render();
|
||||
|
||||
selectedItems = component.FindAll(".rz-state-highlight");
|
||||
Assert.Equal(2, selectedItems.Count);
|
||||
Assert.Equal("Item 1", selectedItems[0].TextContent.Trim());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DropDown_AppliesSelectionStyleWhenMultipleSelectionIsEnabled()
|
||||
{
|
||||
@@ -119,7 +157,8 @@ namespace Radzen.Blazor.Tests
|
||||
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
|
||||
var component = DropDown<string>(ctx, parameters => {
|
||||
var component = DropDown<string>(ctx, parameters =>
|
||||
{
|
||||
parameters.Add(p => p.ValueProperty, nameof(DataItem.Text));
|
||||
parameters.Add(p => p.Multiple, true);
|
||||
});
|
||||
@@ -140,5 +179,512 @@ namespace Radzen.Blazor.Tests
|
||||
|
||||
Assert.Equal(2, selectedItems.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DropDown_AppliesValueTemplateOnMultipleSelection()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
|
||||
var valueTemplateFragment = (RenderFragment<dynamic>)(_context =>
|
||||
builder =>
|
||||
{
|
||||
builder.AddContent(0, $"value: {_context.Text}");
|
||||
});
|
||||
|
||||
var component = DropDown<string>(ctx, parameters =>
|
||||
{
|
||||
parameters.Add(p => p.Multiple, true)
|
||||
.Add(p => p.ValueTemplate, valueTemplateFragment);
|
||||
});
|
||||
|
||||
var items = component.FindAll(".rz-multiselect-item");
|
||||
|
||||
items[0].Click();
|
||||
items[1].Click();
|
||||
|
||||
component.Render();
|
||||
|
||||
var selectedItems = component.Find(".rz-inputtext");
|
||||
|
||||
Assert.Contains("value: Item 1,value: Item 2", selectedItems.Text());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DropDown_AppliesValueTemplateWhenTepmlateDefined()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
|
||||
var valueTemplateFragment = (RenderFragment<dynamic>)(_context =>
|
||||
builder =>
|
||||
{
|
||||
builder.AddContent(0, $"value: {_context.Text}");
|
||||
});
|
||||
|
||||
var templateFragment = (RenderFragment<dynamic>)(_context =>
|
||||
builder =>
|
||||
{
|
||||
builder.AddContent(0, $"template: {_context.Text}");
|
||||
});
|
||||
|
||||
var component = DropDown<string>(ctx, parameters =>
|
||||
{
|
||||
parameters.Add(p => p.Multiple, true)
|
||||
.Add(p => p.ValueTemplate, valueTemplateFragment)
|
||||
.Add(p => p.Template, templateFragment);
|
||||
});
|
||||
|
||||
var items = component.FindAll(".rz-multiselect-item");
|
||||
|
||||
items[0].Click();
|
||||
items[1].Click();
|
||||
|
||||
component.Render();
|
||||
|
||||
var selectedItems = component.Find(".rz-inputtext");
|
||||
var itemsText = component.FindAll(".rz-multiselect-item-content");
|
||||
|
||||
Assert.Collection(itemsText, item => Assert.Contains("template: Item 1", item.Text()), item => Assert.Contains("template: Item 2", item.Text()));
|
||||
Assert.Contains("value: Item 1,value: Item 2", selectedItems.Text());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DropDown_AppliesValueTemplateOnMultipleSelectionChips()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
|
||||
var valueTemplateFragment = (RenderFragment<dynamic>)(_context =>
|
||||
builder =>
|
||||
{
|
||||
builder.AddContent(0, $"value: {_context.Text}");
|
||||
});
|
||||
|
||||
var component = DropDown<string>(ctx, parameters =>
|
||||
{
|
||||
parameters.Add(p => p.Multiple, true)
|
||||
.Add(p => p.ValueTemplate, valueTemplateFragment)
|
||||
.Add(p => p.Chips, true);
|
||||
});
|
||||
|
||||
var items = component.FindAll(".rz-multiselect-item");
|
||||
|
||||
items[0].Click();
|
||||
items[1].Click();
|
||||
|
||||
component.Render();
|
||||
|
||||
var selectedItems = component.FindAll(".rz-chip-text");
|
||||
|
||||
Assert.Collection(selectedItems, item => Assert.Contains("value: Item 1", item.Text()), item => Assert.Contains("value: Item 2", item.Text()));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false, true, false, true, "false")]
|
||||
[InlineData(true, false, true, false, "true")]
|
||||
[InlineData(true, false, false, false, "false")]
|
||||
[InlineData(true, false, false, true, "true")]
|
||||
[InlineData(false, false, false, true, "false")]
|
||||
public void DropDown_AllSelectedFalseIfListIsAllDisabled(bool item1Selected, bool item1Disabled, bool item2Selected, bool item2Disabled, string expectedAriaCheckedValue)
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
|
||||
var data = new[] {
|
||||
new DataItem { Text = "Item 1", Id = 1, Disabled = item1Disabled },
|
||||
new DataItem { Text = "Item 2", Id = 2, Disabled = item2Disabled },
|
||||
};
|
||||
|
||||
List<int> selectedValues = [];
|
||||
if (item1Selected)
|
||||
{
|
||||
selectedValues.Add(data[0].Id);
|
||||
}
|
||||
if (item2Selected)
|
||||
{
|
||||
selectedValues.Add(data[1].Id);
|
||||
}
|
||||
|
||||
var component = ctx.RenderComponent<RadzenDropDown<List<int>>>(parameters => parameters
|
||||
.Add(p => p.Data, data)
|
||||
.Add(p => p.Value, selectedValues)
|
||||
.Add(p => p.Multiple, true)
|
||||
.Add(p => p.AllowSelectAll, true)
|
||||
.Add(p => p.TextProperty, nameof(DataItem.Text))
|
||||
.Add(p => p.DisabledProperty, nameof(DataItem.Disabled))
|
||||
.Add(p => p.ValueProperty, nameof(DataItem.Id)));
|
||||
|
||||
Assert.NotNull(component);
|
||||
var highlightedItems = component.FindAll(".rz-state-highlight");
|
||||
Assert.Equal(selectedValues.Count, highlightedItems.Count);
|
||||
|
||||
|
||||
var selectAllCheckBox = component.Find(".rz-multiselect-header input[type='checkbox']");
|
||||
|
||||
Assert.Equal(expectedAriaCheckedValue, selectAllCheckBox.GetAttribute("aria-checked"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DropDown_ReferenceGenericCollectionAssignment_HashSet_ReferencesInstance()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
|
||||
var originalHashSet = new HashSet<int>();
|
||||
var capturedValue = (HashSet<int>)null;
|
||||
|
||||
var component = DropDownWithReferenceCollection<HashSet<int>>(ctx, parameters =>
|
||||
{
|
||||
parameters.Add(p => p.Multiple, true);
|
||||
parameters.Add(p => p.Value, originalHashSet);
|
||||
parameters.Add(p => p.ValueChanged, EventCallback.Factory.Create<HashSet<int>>(this, value => capturedValue = value));
|
||||
parameters.Add(p => p.ValueProperty, nameof(DataItem.Id));
|
||||
});
|
||||
|
||||
var items = component.FindAll(".rz-multiselect-item");
|
||||
|
||||
// Select first item
|
||||
items[0].Click();
|
||||
component.Render();
|
||||
|
||||
// Verify the same HashSet instance is Referenced
|
||||
Assert.Same(originalHashSet, capturedValue);
|
||||
|
||||
// Verify the item was added correctly
|
||||
Assert.Single(originalHashSet);
|
||||
Assert.Contains(1, originalHashSet);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DropDown_ReferenceGenericCollectionAssignment_HashSet_MultipleSelections()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
|
||||
var originalHashSet = new HashSet<int> { 2 }; // Pre-populate with Item 2
|
||||
var capturedValues = new List<HashSet<int>>();
|
||||
|
||||
var component = DropDownWithReferenceCollection<HashSet<int>>(ctx, parameters =>
|
||||
{
|
||||
parameters.Add(p => p.Multiple, true);
|
||||
parameters.Add(p => p.Value, originalHashSet);
|
||||
parameters.Add(p => p.ValueChanged, EventCallback.Factory.Create<HashSet<int>>(this, value => capturedValues.Add(value)));
|
||||
parameters.Add(p => p.ValueProperty, nameof(DataItem.Id));
|
||||
});
|
||||
|
||||
var items = component.FindAll(".rz-multiselect-item");
|
||||
|
||||
// Select first item (should add to existing collection)
|
||||
items[0].Click();
|
||||
component.Render();
|
||||
|
||||
// Verify the same HashSet instance is Referenced
|
||||
Assert.Single(capturedValues);
|
||||
Assert.Same(originalHashSet, capturedValues[0]);
|
||||
|
||||
// Verify both items are now in the collection
|
||||
Assert.Equal(2, originalHashSet.Count);
|
||||
Assert.Contains(1, originalHashSet);
|
||||
Assert.Contains(2, originalHashSet);
|
||||
|
||||
// Deselect second item (should remove from collection)
|
||||
items = component.FindAll(".rz-multiselect-item"); // Re-find items after render
|
||||
items[1].Click();
|
||||
component.Render();
|
||||
|
||||
// Verify the same HashSet instance is still Referenced
|
||||
Assert.Equal(2, capturedValues.Count);
|
||||
Assert.Same(originalHashSet, capturedValues[1]);
|
||||
|
||||
// Verify only first item remains
|
||||
Assert.Single(originalHashSet);
|
||||
Assert.Contains(1, originalHashSet);
|
||||
Assert.DoesNotContain(2, originalHashSet);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DropDown_ReferenceGenericCollectionAssignment_SortedSet_ReferencesInstance()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
|
||||
var originalSortedSet = new SortedSet<int>();
|
||||
var capturedValue = (SortedSet<int>)null;
|
||||
|
||||
var component = DropDownWithReferenceCollection<SortedSet<int>>(ctx, parameters =>
|
||||
{
|
||||
parameters.Add(p => p.Multiple, true);
|
||||
parameters.Add(p => p.Value, originalSortedSet);
|
||||
parameters.Add(p => p.ValueChanged, EventCallback.Factory.Create<SortedSet<int>>(this, value => capturedValue = value));
|
||||
parameters.Add(p => p.ValueProperty, nameof(DataItem.Id));
|
||||
});
|
||||
|
||||
var items = component.FindAll(".rz-multiselect-item");
|
||||
|
||||
// Select both items
|
||||
items[0].Click();
|
||||
component.Render();
|
||||
items = component.FindAll(".rz-multiselect-item"); // Re-find items after first click
|
||||
items[1].Click();
|
||||
component.Render();
|
||||
|
||||
// Verify the same SortedSet instance is Referenced
|
||||
Assert.Same(originalSortedSet, capturedValue);
|
||||
|
||||
// Verify items are sorted correctly
|
||||
Assert.Equal(2, originalSortedSet.Count);
|
||||
var sortedItems = originalSortedSet.ToList();
|
||||
Assert.Equal(1, sortedItems[0]);
|
||||
Assert.Equal(2, sortedItems[1]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DropDown_ReferenceGenericCollectionAssignment_CustomCollection_ReferencesInstance()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
|
||||
var originalCollection = new CustomCollection<int>();
|
||||
var capturedValue = (CustomCollection<int>)null;
|
||||
|
||||
var component = DropDownWithReferenceCollection<CustomCollection<int>>(ctx, parameters =>
|
||||
{
|
||||
parameters.Add(p => p.Multiple, true);
|
||||
parameters.Add(p => p.Value, originalCollection);
|
||||
parameters.Add(p => p.ValueChanged, EventCallback.Factory.Create<CustomCollection<int>>(this, value => capturedValue = value));
|
||||
parameters.Add(p => p.ValueProperty, nameof(DataItem.Id));
|
||||
});
|
||||
|
||||
var items = component.FindAll(".rz-multiselect-item");
|
||||
|
||||
// Select first item
|
||||
items[0].Click();
|
||||
component.Render();
|
||||
|
||||
// Verify the same custom collection instance is Referenced
|
||||
Assert.Same(originalCollection, capturedValue);
|
||||
|
||||
// Verify the item was added correctly
|
||||
Assert.Single(originalCollection);
|
||||
Assert.Contains(1, originalCollection);
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public void DropDown_ReferenceGenericCollectionAssignment_List_ReferencesInstance()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
|
||||
var originalList = new List<int>();
|
||||
var capturedValue = (List<int>)null;
|
||||
|
||||
var component = DropDownWithReferenceCollection<List<int>>(ctx, parameters =>
|
||||
{
|
||||
parameters.Add(p => p.Multiple, true);
|
||||
parameters.Add(p => p.Value, originalList);
|
||||
parameters.Add(p => p.ValueChanged, EventCallback.Factory.Create<List<int>>(this, value => capturedValue = value));
|
||||
parameters.Add(p => p.ValueProperty, nameof(DataItem.Id));
|
||||
});
|
||||
|
||||
var items = component.FindAll(".rz-multiselect-item");
|
||||
|
||||
// Select first item
|
||||
items[0].Click();
|
||||
component.Render();
|
||||
|
||||
// For List<T>, it should now Reference the instance since we removed the IList exclusion
|
||||
// Arrays are now excluded instead
|
||||
Assert.Same(originalList, capturedValue);
|
||||
|
||||
// And the content should be correct
|
||||
Assert.Single(capturedValue);
|
||||
Assert.Contains(1, capturedValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DropDown_ReferenceGenericCollectionAssignment_DisabledByDefault()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
|
||||
var originalList = new List<int>();
|
||||
var capturedValue = (List<int>)null;
|
||||
|
||||
var component = DropDown<List<int>>(ctx, parameters =>
|
||||
{
|
||||
parameters.Add(p => p.Multiple, true);
|
||||
parameters.Add(p => p.Value, originalList);
|
||||
parameters.Add(p => p.ValueChanged, EventCallback.Factory.Create<List<int>>(this, value => capturedValue = value));
|
||||
parameters.Add(p => p.ValueProperty, nameof(DataItem.Id));
|
||||
});
|
||||
|
||||
var items = component.FindAll(".rz-multiselect-item");
|
||||
|
||||
// Select first item
|
||||
items[0].Click();
|
||||
component.Render();
|
||||
|
||||
// When ReferenceCollectionOnSelection is false (default), a new instance should be created
|
||||
Assert.NotSame(originalList, capturedValue);
|
||||
|
||||
// But the content should still be correct
|
||||
Assert.Single(capturedValue);
|
||||
Assert.Contains(1, capturedValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DropDown_Reset_PreservesCollectionInstanceButClears()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
|
||||
var originalHashSet = new HashSet<int> { 1, 2 }; // Pre-populate
|
||||
var capturedValues = new List<HashSet<int>>();
|
||||
|
||||
var component = DropDownWithReferenceCollection<HashSet<int>>(ctx, parameters =>
|
||||
{
|
||||
parameters.Add(p => p.Multiple, true);
|
||||
parameters.Add(p => p.Value, originalHashSet);
|
||||
parameters.Add(p => p.ValueChanged, EventCallback.Factory.Create<HashSet<int>>(this, value => capturedValues.Add(value)));
|
||||
parameters.Add(p => p.ValueProperty, nameof(DataItem.Id));
|
||||
});
|
||||
|
||||
// Verify initial state - collection should have 2 items
|
||||
Assert.Equal(2, originalHashSet.Count);
|
||||
Assert.Contains(1, originalHashSet);
|
||||
Assert.Contains(2, originalHashSet);
|
||||
|
||||
// Call Reset (public method that calls ClearAll internally)
|
||||
component.InvokeAsync(() => component.Instance.Reset());
|
||||
component.Render();
|
||||
|
||||
// Verify the same HashSet instance is preserved
|
||||
Assert.Single(capturedValues);
|
||||
Assert.Same(originalHashSet, capturedValues[0]);
|
||||
|
||||
// Verify the collection is now cleared
|
||||
Assert.Empty(originalHashSet);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DropDown_SelectAll_PreservesCollectionInstanceAndPopulates()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
|
||||
var originalHashSet = new HashSet<int>(); // Start empty
|
||||
var capturedValues = new List<HashSet<int>>();
|
||||
|
||||
var component = DropDownWithReferenceCollection<HashSet<int>>(ctx, parameters =>
|
||||
{
|
||||
parameters.Add(p => p.Multiple, true);
|
||||
parameters.Add(p => p.AllowSelectAll, true);
|
||||
parameters.Add(p => p.Value, originalHashSet);
|
||||
parameters.Add(p => p.ValueChanged, EventCallback.Factory.Create<HashSet<int>>(this, value => capturedValues.Add(value)));
|
||||
parameters.Add(p => p.ValueProperty, nameof(DataItem.Id));
|
||||
});
|
||||
|
||||
// Verify initial state - collection should be empty
|
||||
Assert.Empty(originalHashSet);
|
||||
|
||||
// Find and click the "Select All" checkbox
|
||||
var selectAllCheckBox = component.Find(".rz-multiselect-header input[type='checkbox']");
|
||||
selectAllCheckBox.Click();
|
||||
component.Render();
|
||||
|
||||
// Verify the same HashSet instance is preserved
|
||||
Assert.Single(capturedValues);
|
||||
Assert.Same(originalHashSet, capturedValues[0]);
|
||||
|
||||
// Verify the collection now contains both items
|
||||
Assert.Equal(2, originalHashSet.Count);
|
||||
Assert.Contains(1, originalHashSet);
|
||||
Assert.Contains(2, originalHashSet);
|
||||
}
|
||||
|
||||
class ReferenceCollectionDropDown<T> : Radzen.Blazor.RadzenDropDown<T>
|
||||
{
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
PreserveCollectionOnSelection = true;
|
||||
base.OnInitialized();
|
||||
}
|
||||
}
|
||||
|
||||
private static IRenderedComponent<ReferenceCollectionDropDown<T>> DropDownWithReferenceCollection<T>(TestContext ctx, Action<ComponentParameterCollectionBuilder<ReferenceCollectionDropDown<T>>> configure = null)
|
||||
{
|
||||
var data = new[] {
|
||||
new DataItem { Text = "Item 1", Id = 1 },
|
||||
new DataItem { Text = "Item 2", Id = 2 },
|
||||
};
|
||||
|
||||
var component = ctx.RenderComponent<ReferenceCollectionDropDown<T>>();
|
||||
|
||||
component.SetParametersAndRender(parameters =>
|
||||
{
|
||||
parameters.Add(p => p.Data, data);
|
||||
parameters.Add(p => p.TextProperty, nameof(DataItem.Text));
|
||||
|
||||
if (configure != null)
|
||||
{
|
||||
configure.Invoke(parameters);
|
||||
}
|
||||
else
|
||||
{
|
||||
parameters.Add(p => p.ValueProperty, nameof(DataItem.Id));
|
||||
}
|
||||
});
|
||||
|
||||
return component;
|
||||
}
|
||||
|
||||
class DataItemComparer : IEqualityComparer<DataItem>, IEqualityComparer<object>
|
||||
{
|
||||
public bool Equals(DataItem x, DataItem y)
|
||||
{
|
||||
if (ReferenceEquals(x, y)) return true;
|
||||
if (x is null) return false;
|
||||
if (y is null) return false;
|
||||
if (x.GetType() != y.GetType()) return false;
|
||||
return x.Text == y.Text;
|
||||
}
|
||||
|
||||
public int GetHashCode(DataItem obj)
|
||||
{
|
||||
return obj.Text.GetHashCode();
|
||||
}
|
||||
|
||||
public new bool Equals(object x, object y)
|
||||
{
|
||||
return Equals((DataItem)x, (DataItem)y);
|
||||
}
|
||||
|
||||
public int GetHashCode(object obj)
|
||||
{
|
||||
return GetHashCode((DataItem)obj);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
class CustomCollection<T> : ICollection<T>
|
||||
{
|
||||
private readonly List<T> _items = new();
|
||||
|
||||
public int Count => _items.Count;
|
||||
public bool IsReadOnly => false;
|
||||
|
||||
public void Add(T item) => _items.Add(item);
|
||||
public void Clear() => _items.Clear();
|
||||
public bool Contains(T item) => _items.Contains(item);
|
||||
public void CopyTo(T[] array, int arrayIndex) => _items.CopyTo(array, arrayIndex);
|
||||
public bool Remove(T item) => _items.Remove(item);
|
||||
public IEnumerator<T> GetEnumerator() => _items.GetEnumerator();
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
1557
Radzen.Blazor.Tests/ExpressionParserTests.cs
Normal file
1557
Radzen.Blazor.Tests/ExpressionParserTests.cs
Normal file
File diff suppressed because it is too large
Load Diff
323
Radzen.Blazor.Tests/ExpressionSerializerTests.cs
Normal file
323
Radzen.Blazor.Tests/ExpressionSerializerTests.cs
Normal file
@@ -0,0 +1,323 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Tests
|
||||
{
|
||||
class TestEntity
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public int Age { get; set; }
|
||||
public double Salary { get; set; }
|
||||
public float Score { get; set; }
|
||||
public decimal Balance { get; set; }
|
||||
public short Level { get; set; }
|
||||
public long Population { get; set; }
|
||||
public Status AccountStatus { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTimeOffset LastUpdated { get; set; }
|
||||
public Guid Id { get; set; }
|
||||
public TimeOnly StartTime { get; set; }
|
||||
public DateOnly BirthDate { get; set; }
|
||||
public int[] Scores { get; set; }
|
||||
public List<string> Tags { get; set; }
|
||||
public List<TestEntity> Children { get; set; }
|
||||
public Address Address { get; set; }
|
||||
public double[] Salaries { get; set; }
|
||||
public float[] Heights { get; set; }
|
||||
public decimal[] Balances { get; set; }
|
||||
public short[] Levels { get; set; }
|
||||
public long[] Populations { get; set; }
|
||||
public string[] Names { get; set; }
|
||||
public Guid[] Ids { get; set; }
|
||||
public DateTime[] CreatedDates { get; set; }
|
||||
public DateTimeOffset[] UpdatedDates { get; set; }
|
||||
public TimeOnly[] StartTimes { get; set; }
|
||||
public DateOnly[] BirthDates { get; set; }
|
||||
public Status[] Statuses { get; set; }
|
||||
}
|
||||
|
||||
enum Status
|
||||
{
|
||||
Active,
|
||||
Inactive,
|
||||
Suspended
|
||||
}
|
||||
|
||||
class Address
|
||||
{
|
||||
public string City { get; set; }
|
||||
public string Country { get; set; }
|
||||
}
|
||||
|
||||
public class ExpressionSerializerTests
|
||||
{
|
||||
private readonly ExpressionSerializer _serializer = new ExpressionSerializer();
|
||||
|
||||
[Fact]
|
||||
public void Serializes_SimpleBinaryExpression()
|
||||
{
|
||||
Expression<Func<int, bool>> expr = e => e > 10;
|
||||
Assert.Equal("e => (e > 10)", _serializer.Serialize(expr));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serializes_StringEquality()
|
||||
{
|
||||
Expression<Func<TestEntity, bool>> expr = e => e.Name == "John";
|
||||
Assert.Equal("e => (e.Name == \"John\")", _serializer.Serialize(expr));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serializes_IntComparison()
|
||||
{
|
||||
Expression<Func<TestEntity, bool>> expr = e => e.Age > 18;
|
||||
Assert.Equal("e => (e.Age > 18)", _serializer.Serialize(expr));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serializes_DoubleComparison()
|
||||
{
|
||||
Expression<Func<TestEntity, bool>> expr = e => e.Salary < 50000.50;
|
||||
Assert.Equal("e => (e.Salary < 50000.5)", _serializer.Serialize(expr));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serializes_FloatComparison()
|
||||
{
|
||||
Expression<Func<TestEntity, bool>> expr = e => e.Score >= 85.3f;
|
||||
Assert.Equal("e => (e.Score >= 85.3)", _serializer.Serialize(expr));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serializes_DecimalComparison()
|
||||
{
|
||||
Expression<Func<TestEntity, bool>> expr = e => e.Balance <= 1000.75m;
|
||||
Assert.Equal("e => (e.Balance <= 1000.75)", _serializer.Serialize(expr));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serializes_ShortComparison()
|
||||
{
|
||||
Expression<Func<TestEntity, bool>> expr = e => e.Level == 3;
|
||||
Assert.Equal("e => (e.Level == 3)", _serializer.Serialize(expr));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serializes_LongComparison()
|
||||
{
|
||||
Expression<Func<TestEntity, bool>> expr = e => e.Population > 1000000L;
|
||||
Assert.Equal("e => (e.Population > 1000000)", _serializer.Serialize(expr));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serializes_EnumComparison()
|
||||
{
|
||||
Expression<Func<TestEntity, bool>> expr = e => e.AccountStatus == Status.Inactive;
|
||||
Assert.Equal("e => (e.AccountStatus == 1)", _serializer.Serialize(expr));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serializes_ArrayContainsValue()
|
||||
{
|
||||
Expression<Func<TestEntity, bool>> expr = e => e.Scores.Contains(100);
|
||||
Assert.Equal("e => e.Scores.Contains(100)", _serializer.Serialize(expr));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serializes_ArrayNotContainsValue()
|
||||
{
|
||||
Expression<Func<TestEntity, bool>> expr = e => !e.Scores.Contains(100);
|
||||
Assert.Equal("e => (!(e.Scores.Contains(100)))", _serializer.Serialize(expr));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serializes_ArrayInValue()
|
||||
{
|
||||
Expression<Func<TestEntity, bool>> expr = e => e.Scores.Intersect(new [] { 100 }).Any();
|
||||
Assert.Equal("e => e.Scores.Intersect(new [] { 100 }).Any()", _serializer.Serialize(expr));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serializes_ArrayNotInValue()
|
||||
{
|
||||
Expression<Func<TestEntity, bool>> expr = e => e.Scores.Except(new[] { 100 }).Any();
|
||||
Assert.Equal("e => e.Scores.Except(new [] { 100 }).Any()", _serializer.Serialize(expr));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serializes_ArrayInValueOposite()
|
||||
{
|
||||
Expression<Func<TestEntity, bool>> expr = e => new[] { 100 }.Intersect(e.Scores).Any();
|
||||
Assert.Equal("e => new [] { 100 }.Intersect(e.Scores).Any()", _serializer.Serialize(expr));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serializes_ArrayNotInValueOposite()
|
||||
{
|
||||
Expression<Func<TestEntity, bool>> expr = e => new[] { 100 }.Except(e.Scores).Any();
|
||||
Assert.Equal("e => new [] { 100 }.Except(e.Scores).Any()", _serializer.Serialize(expr));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serializes_IntArrayInValueOposite()
|
||||
{
|
||||
Expression<Func<TestEntity, bool>> expr = e => new[] { 100 }.Intersect(e.Scores).Any();
|
||||
Assert.Equal("e => new [] { 100 }.Intersect(e.Scores).Any()", _serializer.Serialize(expr));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serializes_IntArrayNotInValueOposite()
|
||||
{
|
||||
Expression<Func<TestEntity, bool>> expr = e => !new[] { 100 }.Intersect(e.Scores).Any();
|
||||
Assert.Equal("e => (!(new [] { 100 }.Intersect(e.Scores).Any()))", _serializer.Serialize(expr));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serializes_DoubleArrayInValueOposite()
|
||||
{
|
||||
Expression<Func<TestEntity, bool>> expr = e => new[] { 99.99 }.Intersect(e.Salaries).Any();
|
||||
Assert.Equal("e => new [] { 99.99 }.Intersect(e.Salaries).Any()", _serializer.Serialize(expr));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serializes_FloatArrayInValueOposite()
|
||||
{
|
||||
Expression<Func<TestEntity, bool>> expr = e => new[] { 5.5f }.Intersect(e.Heights).Any();
|
||||
Assert.Equal("e => new [] { 5.5 }.Intersect(e.Heights).Any()", _serializer.Serialize(expr));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serializes_DecimalArrayInValueOposite()
|
||||
{
|
||||
Expression<Func<TestEntity, bool>> expr = e => new[] { 1000.75m }.Intersect(e.Balances).Any();
|
||||
Assert.Equal("e => new [] { 1000.75 }.Intersect(e.Balances).Any()", _serializer.Serialize(expr));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serializes_ShortArrayInValueOposite()
|
||||
{
|
||||
Expression<Func<TestEntity, bool>> expr = e => new [] { (short)3 }.Intersect(e.Levels).Any();
|
||||
Assert.Equal("e => new [] { 3 }.Intersect(e.Levels).Any()", _serializer.Serialize(expr));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serializes_LongArrayInValueOposite()
|
||||
{
|
||||
Expression<Func<TestEntity, bool>> expr = e => new [] { 1000000L }.Intersect(e.Populations).Any();
|
||||
Assert.Equal("e => new [] { 1000000 }.Intersect(e.Populations).Any()", _serializer.Serialize(expr));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serializes_StringArrayInValueOposite()
|
||||
{
|
||||
Expression<Func<TestEntity, bool>> expr = e => new[] { "Alice", "Bob" }.Intersect(e.Names).Any();
|
||||
Assert.Equal("e => (new [] { \"Alice\", \"Bob\" }).Intersect(e.Names).Any()", _serializer.Serialize(expr));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serializes_GuidArrayInValueOposite()
|
||||
{
|
||||
Expression<Func<TestEntity, bool>> expr = e => new[] { Guid.Parse("12345678-1234-1234-1234-123456789abc") }.Intersect(e.Ids).Any();
|
||||
Assert.Equal("e => (new [] { Guid.Parse(\"12345678-1234-1234-1234-123456789abc\") }).Intersect(e.Ids).Any()", _serializer.Serialize(expr));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serializes_DateTimeArrayInValueOposite()
|
||||
{
|
||||
Expression<Func<TestEntity, bool>> expr = e => new[] { DateTime.Parse("2023-01-01T00:00:00.000Z") }.Intersect(e.CreatedDates).Any();
|
||||
Assert.Equal("e => (new [] { DateTime.Parse(\"2023-01-01T00:00:00.000Z\") }).Intersect(e.CreatedDates).Any()", _serializer.Serialize(expr));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serializes_DateTimeOffsetArrayInValueOposite()
|
||||
{
|
||||
Expression<Func<TestEntity, bool>> expr = e => new[] { DateTimeOffset.Parse("2023-01-01T10:30:00.000+00:00") }.Intersect(e.UpdatedDates).Any();
|
||||
Assert.Equal("e => (new [] { DateTimeOffset.Parse(\"2023-01-01T10:30:00.000+00:00\") }).Intersect(e.UpdatedDates).Any()", _serializer.Serialize(expr));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serializes_DateTimeWithRoundtripKind()
|
||||
{
|
||||
Expression<Func<TestEntity, bool>> expr = e =>
|
||||
DateTime.Parse("2023-01-01T00:00:00.000Z", null, DateTimeStyles.RoundtripKind) > e.CreatedAt;
|
||||
|
||||
Assert.Equal(
|
||||
"e => (DateTime.Parse(\"2023-01-01T00:00:00.000Z\", null, (System.Globalization.DateTimeStyles)128) > e.CreatedAt)",
|
||||
_serializer.Serialize(expr));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serializes_TimeOnlyArrayInValueOposite()
|
||||
{
|
||||
Expression<Func<TestEntity, bool>> expr = e => new[] { TimeOnly.Parse("12:00:00") }.Intersect(e.StartTimes).Any();
|
||||
Assert.Equal("e => (new [] { TimeOnly.Parse(\"12:00:00\") }).Intersect(e.StartTimes).Any()", _serializer.Serialize(expr));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serializes_DateOnlyArrayInValueOposite()
|
||||
{
|
||||
Expression<Func<TestEntity, bool>> expr = e => new[] { DateOnly.Parse("2000-01-01") }.Intersect(e.BirthDates).Any();
|
||||
Assert.Equal("e => (new [] { DateOnly.Parse(\"2000-01-01\") }).Intersect(e.BirthDates).Any()", _serializer.Serialize(expr));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serializes_EnumArrayInValueOposite()
|
||||
{
|
||||
Expression<Func<TestEntity, bool>> expr = e => new[] { Status.Active, Status.Inactive }.Intersect(e.Statuses).Any();
|
||||
Assert.Equal("e => (new [] { (Radzen.Blazor.Tests.Status)0, (Radzen.Blazor.Tests.Status)1 }).Intersect(e.Statuses).Any()", _serializer.Serialize(expr));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serializes_ListContainsValue()
|
||||
{
|
||||
Expression<Func<TestEntity, bool>> expr = e => e.Tags.Contains("VIP");
|
||||
Assert.Equal("e => e.Tags.Contains(\"VIP\")", _serializer.Serialize(expr));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serializes_ListNotContainsValue()
|
||||
{
|
||||
Expression<Func<TestEntity, bool>> expr = e => !e.Tags.Contains("VIP");
|
||||
Assert.Equal("e => (!(e.Tags.Contains(\"VIP\")))", _serializer.Serialize(expr));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serializes_ListAnyCheck()
|
||||
{
|
||||
Expression<Func<TestEntity, bool>> expr = e => e.Children.Any(c => c.Age > 18);
|
||||
Assert.Equal("e => e.Children.Any(c => (c.Age > 18))", _serializer.Serialize(expr));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serializes_ListNotAnyCheck()
|
||||
{
|
||||
Expression<Func<TestEntity, bool>> expr = e => !e.Children.Any(c => c.Age > 18);
|
||||
Assert.Equal("e => (!(e.Children.Any(c => (c.Age > 18))))", _serializer.Serialize(expr));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serializes_EntitySubPropertyCheck()
|
||||
{
|
||||
Expression<Func<TestEntity, bool>> expr = e => e.Address.City == "New York";
|
||||
Assert.Equal("e => (e.Address.City == \"New York\")", _serializer.Serialize(expr));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serializes_ComplexExpressionWithProperties()
|
||||
{
|
||||
Expression<Func<TestEntity, bool>> expr = e => e.Age > 18 && e.Tags.Contains("Member") || e.Address.City == "London";
|
||||
Assert.Equal("e => (((e.Age > 18) && e.Tags.Contains(\"Member\")) || (e.Address.City == \"London\"))", _serializer.Serialize(expr));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serializes_NotContains()
|
||||
{
|
||||
Expression<Func<TestEntity, bool>> expr = e => !e.Tags.Contains("Member");
|
||||
Assert.Equal("e => (!(e.Tags.Contains(\"Member\")))", _serializer.Serialize(expr));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -46,7 +46,7 @@ namespace Radzen.Blazor.Tests
|
||||
|
||||
component.SetParametersAndRender(parameters => parameters.Add(p => p.Icon, value));
|
||||
|
||||
Assert.Contains(@$"<i class=""rzi"">{value}</i>", component.Markup);
|
||||
Assert.Contains(@$"<i class=""notranslate rzi"">{value}</i>", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -104,11 +104,11 @@ namespace Radzen.Blazor.Tests
|
||||
|
||||
component.SetParametersAndRender(parameters => parameters.Add<bool>(p => p.AllowCollapse, true));
|
||||
|
||||
Assert.Contains(@"<span class=""rz-fieldset-toggler rzi rzi-w rzi-minus""></span>", component.Markup);
|
||||
Assert.Contains(@"<span class=""notranslate rz-fieldset-toggler rzi rzi-w rzi-minus""></span>", component.Markup);
|
||||
|
||||
component.SetParametersAndRender(parameters => parameters.Add<bool>(p => p.Collapsed, true));
|
||||
|
||||
Assert.Contains(@"<span class=""rz-fieldset-toggler rzi rzi-w rzi-plus""></span>", component.Markup);
|
||||
Assert.Contains(@"<span class=""notranslate rz-fieldset-toggler rzi rzi-w rzi-plus""></span>", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -184,13 +184,13 @@ namespace Radzen.Blazor.Tests
|
||||
|
||||
Assert.Contains("SummaryContent", component.Markup);
|
||||
Assert.Equal(
|
||||
"",
|
||||
component.Find(".rz-fieldset-content-summary").ParentElement.Attributes.First(attr => attr.Name == "style").Value
|
||||
"false",
|
||||
component.Find(".rz-fieldset-content-summary").ParentElement.ParentElement.Attributes.First(attr => attr.Name == "aria-hidden").Value
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fieldset_DontRenders_SummaryWhenOpen()
|
||||
public void Fieldset_DoesNotRender_SummaryWhenOpen()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
var component = ctx.RenderComponent<RadzenFieldset>();
|
||||
@@ -210,8 +210,8 @@ namespace Radzen.Blazor.Tests
|
||||
|
||||
Assert.Contains("SummaryContent", component.Markup);
|
||||
Assert.Equal(
|
||||
"display: none",
|
||||
component.Find(".rz-fieldset-content-summary").ParentElement.Attributes.First(attr => attr.Name == "style").Value
|
||||
"true",
|
||||
component.Find(".rz-fieldset-content-summary").ParentElement.ParentElement.Attributes.First(attr => attr.Name == "aria-hidden").Value
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace Radzen.Blazor.Tests
|
||||
component.SetParametersAndRender(parameters => parameters.Add(p => p.Icon, icon));
|
||||
|
||||
Assert.Contains(@$">{icon}</i>", component.Markup);
|
||||
Assert.Contains(@$"<i class=""rzi""", component.Markup);
|
||||
Assert.Contains(@$"class=""notranslate rzi""", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -58,6 +58,18 @@ namespace Radzen.Blazor.Tests
|
||||
Assert.Contains(@$"rzi-primary", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Icon_Renders_IconColor()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
|
||||
var component = ctx.RenderComponent<RadzenIcon>();
|
||||
|
||||
component.SetParametersAndRender(parameters => parameters.Add(icon => icon.IconColor, Colors.Primary));
|
||||
|
||||
Assert.Contains(@$"color:", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Icon_NotRenders_IconStyleClass_WhenNull()
|
||||
{
|
||||
@@ -69,5 +81,17 @@ namespace Radzen.Blazor.Tests
|
||||
|
||||
Assert.DoesNotContain(@$"rzi-primary", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Icon_NotRenders_IconColor_WhenNull()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
|
||||
var component = ctx.RenderComponent<RadzenIcon>();
|
||||
|
||||
component.SetParametersAndRender(parameters => parameters.Add(icon => icon.IconColor, null));
|
||||
|
||||
Assert.DoesNotContain(@$"color:", component.Markup);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ namespace Radzen.Blazor.Tests
|
||||
|
||||
component.SetParametersAndRender(parameters => parameters.Add(p => p.Icon, icon));
|
||||
|
||||
Assert.Contains(@$"<i class=""rzi"">{icon}</i>", component.Markup);
|
||||
Assert.Contains(@$"<i class=""notranslate rzi"">{icon}</i>", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -78,6 +78,20 @@ namespace Radzen.Blazor.Tests
|
||||
Assert.Contains(@$"target=""{target}""", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Link_Renders_DisabledParameter()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
|
||||
var component = ctx.RenderComponent<RadzenLink>();
|
||||
|
||||
component.SetParametersAndRender(parameters => parameters.Add(p => p.Disabled, true));
|
||||
|
||||
Assert.Contains("class=\"rz-link rz-link-disabled active\"", component.Markup);
|
||||
|
||||
Assert.DoesNotContain("href=", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Icon_Renders_UnmatchedParameter()
|
||||
{
|
||||
|
||||
314
Radzen.Blazor.Tests/Markdown/BlockQuoteTests.cs
Normal file
314
Radzen.Blazor.Tests/Markdown/BlockQuoteTests.cs
Normal file
@@ -0,0 +1,314 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Markdown.Tests;
|
||||
|
||||
public class BlockQuoteTests
|
||||
{
|
||||
private static string ToXml(string markdown)
|
||||
{
|
||||
var document = MarkdownParser.Parse(markdown);
|
||||
|
||||
return XmlVisitor.ToXml(document);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_BasicBlockQuote()
|
||||
{
|
||||
|
||||
Assert.Equal(@"<document>
|
||||
<block_quote>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
</block_quote>
|
||||
</document>", ToXml(@"> foo"));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"> # Foo
|
||||
> bar
|
||||
> baz", @"<document>
|
||||
<block_quote>
|
||||
<heading level=""1"">
|
||||
<text>Foo</text>
|
||||
</heading>
|
||||
<paragraph>
|
||||
<text>bar</text>
|
||||
<softbreak />
|
||||
<text>baz</text>
|
||||
</paragraph>
|
||||
</block_quote>
|
||||
</document>")]
|
||||
[InlineData(@"># Foo
|
||||
>bar
|
||||
> baz", @"<document>
|
||||
<block_quote>
|
||||
<heading level=""1"">
|
||||
<text>Foo</text>
|
||||
</heading>
|
||||
<paragraph>
|
||||
<text>bar</text>
|
||||
<softbreak />
|
||||
<text>baz</text>
|
||||
</paragraph>
|
||||
</block_quote>
|
||||
</document>")]
|
||||
[InlineData(@" > # Foo
|
||||
> bar
|
||||
> baz", @"<document>
|
||||
<block_quote>
|
||||
<heading level=""1"">
|
||||
<text>Foo</text>
|
||||
</heading>
|
||||
<paragraph>
|
||||
<text>bar</text>
|
||||
<softbreak />
|
||||
<text>baz</text>
|
||||
</paragraph>
|
||||
</block_quote>
|
||||
</document>")]
|
||||
[InlineData(@" > # Foo
|
||||
> bar
|
||||
> baz", @"<document>
|
||||
<code_block>> # Foo
|
||||
> bar
|
||||
> baz
|
||||
</code_block>
|
||||
</document>")]
|
||||
[InlineData(@"> # Foo
|
||||
> bar
|
||||
baz", @"<document>
|
||||
<block_quote>
|
||||
<heading level=""1"">
|
||||
<text>Foo</text>
|
||||
</heading>
|
||||
<paragraph>
|
||||
<text>bar</text>
|
||||
<softbreak />
|
||||
<text>baz</text>
|
||||
</paragraph>
|
||||
</block_quote>
|
||||
</document>")]
|
||||
[InlineData(@"> bar
|
||||
baz
|
||||
> foo", @"<document>
|
||||
<block_quote>
|
||||
<paragraph>
|
||||
<text>bar</text>
|
||||
<softbreak />
|
||||
<text>baz</text>
|
||||
<softbreak />
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
</block_quote>
|
||||
</document>")]
|
||||
[InlineData(@"> foo
|
||||
---", @"<document>
|
||||
<block_quote>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
</block_quote>
|
||||
<thematic_break />
|
||||
</document>")]
|
||||
[InlineData(@"> - foo
|
||||
- bar", @"<document>
|
||||
<block_quote>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</block_quote>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"> foo
|
||||
bar", @"<document>
|
||||
<block_quote>
|
||||
<code_block>foo
|
||||
</code_block>
|
||||
</block_quote>
|
||||
<code_block>bar
|
||||
</code_block>
|
||||
</document>")]
|
||||
[InlineData(@"> ```
|
||||
foo
|
||||
```", @"<document>
|
||||
<block_quote>
|
||||
<code_block></code_block>
|
||||
</block_quote>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
<code_block></code_block>
|
||||
</document>")]
|
||||
[InlineData(@"> foo
|
||||
- bar", @"<document>
|
||||
<block_quote>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
<softbreak />
|
||||
<text>- bar</text>
|
||||
</paragraph>
|
||||
</block_quote>
|
||||
</document>")]
|
||||
[InlineData(@">", @"<document>
|
||||
<block_quote />
|
||||
</document>")]
|
||||
[InlineData(@">
|
||||
>
|
||||
> ", @"<document>
|
||||
<block_quote />
|
||||
</document>")]
|
||||
[InlineData(@">
|
||||
> foo
|
||||
> ", @"<document>
|
||||
<block_quote>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
</block_quote>
|
||||
</document>")]
|
||||
[InlineData(@"> foo
|
||||
|
||||
> bar", @"<document>
|
||||
<block_quote>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
</block_quote>
|
||||
<block_quote>
|
||||
<paragraph>
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
</block_quote>
|
||||
</document>")]
|
||||
[InlineData(@"> foo
|
||||
>
|
||||
> bar", @"<document>
|
||||
<block_quote>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
<paragraph>
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
</block_quote>
|
||||
</document>")]
|
||||
[InlineData(@"foo
|
||||
> bar", @"<document>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
<block_quote>
|
||||
<paragraph>
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
</block_quote>
|
||||
</document>")]
|
||||
[InlineData(@"> aaa
|
||||
***
|
||||
> bbb", @"<document>
|
||||
<block_quote>
|
||||
<paragraph>
|
||||
<text>aaa</text>
|
||||
</paragraph>
|
||||
</block_quote>
|
||||
<thematic_break />
|
||||
<block_quote>
|
||||
<paragraph>
|
||||
<text>bbb</text>
|
||||
</paragraph>
|
||||
</block_quote>
|
||||
</document>")]
|
||||
[InlineData(@"> bar
|
||||
baz", @"<document>
|
||||
<block_quote>
|
||||
<paragraph>
|
||||
<text>bar</text>
|
||||
<softbreak />
|
||||
<text>baz</text>
|
||||
</paragraph>
|
||||
</block_quote>
|
||||
</document>")]
|
||||
[InlineData(@"> bar
|
||||
|
||||
baz", @"<document>
|
||||
<block_quote>
|
||||
<paragraph>
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
</block_quote>
|
||||
<paragraph>
|
||||
<text>baz</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"> bar
|
||||
>
|
||||
baz", @"<document>
|
||||
<block_quote>
|
||||
<paragraph>
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
</block_quote>
|
||||
<paragraph>
|
||||
<text>baz</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"> > > foo
|
||||
bar", @"<document>
|
||||
<block_quote>
|
||||
<block_quote>
|
||||
<block_quote>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
<softbreak />
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
</block_quote>
|
||||
</block_quote>
|
||||
</block_quote>
|
||||
</document>")]
|
||||
[InlineData(@">>> foo
|
||||
> bar
|
||||
>>baz", @"<document>
|
||||
<block_quote>
|
||||
<block_quote>
|
||||
<block_quote>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
<softbreak />
|
||||
<text>bar</text>
|
||||
<softbreak />
|
||||
<text>baz</text>
|
||||
</paragraph>
|
||||
</block_quote>
|
||||
</block_quote>
|
||||
</block_quote>
|
||||
</document>")]
|
||||
[InlineData(@"> code
|
||||
|
||||
> not code", @"<document>
|
||||
<block_quote>
|
||||
<code_block>code
|
||||
</code_block>
|
||||
</block_quote>
|
||||
<block_quote>
|
||||
<paragraph>
|
||||
<text>not code</text>
|
||||
</paragraph>
|
||||
</block_quote>
|
||||
</document>")]
|
||||
public void Parse_BlockQuote(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
}
|
||||
175
Radzen.Blazor.Tests/Markdown/CodeTests.cs
Normal file
175
Radzen.Blazor.Tests/Markdown/CodeTests.cs
Normal file
@@ -0,0 +1,175 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Markdown.Tests;
|
||||
|
||||
public class CodeTests
|
||||
{
|
||||
private static string ToXml(string markdown)
|
||||
{
|
||||
var document = MarkdownParser.Parse(markdown);
|
||||
|
||||
return XmlVisitor.ToXml(document);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("`foo`",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<code>foo</code>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("`` foo ` bar ``",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<code>foo ` bar</code>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("` `` `",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<code>``</code>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
public void Parse_BasicCode_ReturnsCodeNode(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("` `` `",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<code> `` </code>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("` a`",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<code> a</code>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"` `
|
||||
` `",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<code> </code>
|
||||
<softbreak />
|
||||
<code> </code>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
public void Parse_CodeWithSpaces_PreservesSpaces(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"``
|
||||
foo
|
||||
bar
|
||||
baz
|
||||
``",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<code>foo bar baz</code>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"``
|
||||
foo
|
||||
``",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<code>foo </code>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"`foo bar
|
||||
baz`",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<code>foo bar baz</code>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
|
||||
public void Parse_CodeWithLineBreaks_ConvertsToSpace(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("`foo\\`bar`", @"<document>
|
||||
<paragraph>
|
||||
<code>foo\</code>
|
||||
<text>bar</text>
|
||||
<text>`</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("``foo`bar``", @"<document>
|
||||
<paragraph>
|
||||
<code>foo`bar</code>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("` foo `` bar `", @"<document>
|
||||
<paragraph>
|
||||
<code>foo `` bar</code>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
public void Parse_CodeWithBacktics(string mardown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(mardown));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("*foo`*`", @"<document>
|
||||
<paragraph>
|
||||
<text>*</text>
|
||||
<text>foo</text>
|
||||
<code>*</code>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("[not a `link](/foo`)",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<text>[</text>
|
||||
<text>not a </text>
|
||||
<code>link](/foo</code>
|
||||
<text>)</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("`<https://foo.bar.`baz>`",@"<document>
|
||||
<paragraph>
|
||||
<code><https://foo.bar.</code>
|
||||
<text>baz></text>
|
||||
<text>`</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
public void Parse_CodePrecedence(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("```foo``", @"<document>
|
||||
<paragraph>
|
||||
<text>```</text>
|
||||
<text>foo</text>
|
||||
<text>``</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("`foo", @"<document>
|
||||
<paragraph>
|
||||
<text>`</text>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("`foo``bar``", @"<document>
|
||||
<paragraph>
|
||||
<text>`</text>
|
||||
<text>foo</text>
|
||||
<code>bar</code>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
public void Parse_UnmatchingBacktics(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
}
|
||||
272
Radzen.Blazor.Tests/Markdown/EmphasisTests.cs
Normal file
272
Radzen.Blazor.Tests/Markdown/EmphasisTests.cs
Normal file
@@ -0,0 +1,272 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Markdown.Tests;
|
||||
|
||||
public class EmphasisTests
|
||||
{
|
||||
private static string ToXml(string markdown)
|
||||
{
|
||||
var document = MarkdownParser.Parse(markdown);
|
||||
return XmlVisitor.ToXml(document);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"**foo** bar
|
||||
baz",@"<document>
|
||||
<paragraph>
|
||||
<strong>
|
||||
<text>foo</text>
|
||||
</strong>
|
||||
<text> bar</text>
|
||||
<softbreak />
|
||||
<text>baz</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("*foo bar*",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<emph>
|
||||
<text>foo bar</text>
|
||||
</emph>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("a * foo bar*",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<text>a </text>
|
||||
<text>*</text>
|
||||
<text> foo bar</text>
|
||||
<text>*</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("a*\"foo\"*",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<text>a</text>
|
||||
<text>*</text>
|
||||
<text>""foo""</text>
|
||||
<text>*</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("* a *",
|
||||
@"<document>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>a </text>
|
||||
<text>*</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData("foo*bar*",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
<emph>
|
||||
<text>bar</text>
|
||||
</emph>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("5*6*78",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<text>5</text>
|
||||
<emph>
|
||||
<text>6</text>
|
||||
</emph>
|
||||
<text>78</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("_foo bar_",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<emph>
|
||||
<text>foo bar</text>
|
||||
</emph>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("_ foo bar_",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<text>_</text>
|
||||
<text> foo bar</text>
|
||||
<text>_</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("a_\"foo\"_",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<text>a</text>
|
||||
<text>_</text>
|
||||
<text>""foo""</text>
|
||||
<text>_</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("foo_bar_",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
<text>_</text>
|
||||
<text>bar</text>
|
||||
<text>_</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("5_6_78",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<text>5</text>
|
||||
<text>_</text>
|
||||
<text>6</text>
|
||||
<text>_</text>
|
||||
<text>78</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("пристаням_стремятся_",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<text>пристаням</text>
|
||||
<text>_</text>
|
||||
<text>стремятся</text>
|
||||
<text>_</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("aa_\"bb\"_cc",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<text>aa</text>
|
||||
<text>_</text>
|
||||
<text>""bb""</text>
|
||||
<text>_</text>
|
||||
<text>cc</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("foo-_(bar)_",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<text>foo-</text>
|
||||
<emph>
|
||||
<text>(bar)</text>
|
||||
</emph>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("_foo*",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<text>_</text>
|
||||
<text>foo</text>
|
||||
<text>*</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("*foo bar *",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<text>*</text>
|
||||
<text>foo bar </text>
|
||||
<text>*</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("*foo bar\nbaz*",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<emph>
|
||||
<text>foo bar</text>
|
||||
<softbreak />
|
||||
<text>baz</text>
|
||||
</emph>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("*(*foo)",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<text>*</text>
|
||||
<text>(</text>
|
||||
<text>*</text>
|
||||
<text>foo)</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("*(*foo*)*",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<emph>
|
||||
<text>(</text>
|
||||
<emph>
|
||||
<text>foo</text>
|
||||
</emph>
|
||||
<text>)</text>
|
||||
</emph>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("*foo*bar",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<emph>
|
||||
<text>foo</text>
|
||||
</emph>
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("_foo bar _",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<text>_</text>
|
||||
<text>foo bar </text>
|
||||
<text>_</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("_(_foo_)_",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<emph>
|
||||
<text>(</text>
|
||||
<emph>
|
||||
<text>foo</text>
|
||||
</emph>
|
||||
<text>)</text>
|
||||
</emph>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("_foo_bar",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<text>_</text>
|
||||
<text>foo</text>
|
||||
<text>_</text>
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("_пристаням_стремятся",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<text>_</text>
|
||||
<text>пристаням</text>
|
||||
<text>_</text>
|
||||
<text>стремятся</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("_foo_bar_baz_",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<emph>
|
||||
<text>foo</text>
|
||||
<text>_</text>
|
||||
<text>bar</text>
|
||||
<text>_</text>
|
||||
<text>baz</text>
|
||||
</emph>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("_(bar)_.",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<emph>
|
||||
<text>(bar)</text>
|
||||
</emph>
|
||||
<text>.</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
public void Parse_EmphasisRules_AdheresToCommonMarkSpec(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
}
|
||||
264
Radzen.Blazor.Tests/Markdown/FencedCodeBlockTests.cs
Normal file
264
Radzen.Blazor.Tests/Markdown/FencedCodeBlockTests.cs
Normal file
@@ -0,0 +1,264 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Markdown.Tests;
|
||||
|
||||
public class FencedCodeBlockTests
|
||||
{
|
||||
private static string ToXml(string markdown)
|
||||
{
|
||||
var document = MarkdownParser.Parse(markdown);
|
||||
|
||||
return XmlVisitor.ToXml(document);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_BasicFencedCodeBlock()
|
||||
{
|
||||
Assert.Equal(@"<document>
|
||||
<code_block>foo
|
||||
</code_block>
|
||||
</document>", ToXml(@"```
|
||||
foo
|
||||
```"));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"```
|
||||
<
|
||||
>
|
||||
```", @"<document>
|
||||
<code_block><
|
||||
>
|
||||
</code_block>
|
||||
</document>")]
|
||||
[InlineData(@"~~~
|
||||
<
|
||||
>
|
||||
~~~", @"<document>
|
||||
<code_block><
|
||||
>
|
||||
</code_block>
|
||||
</document>")]
|
||||
[InlineData(@"``
|
||||
foo
|
||||
``", @"<document>
|
||||
<paragraph>
|
||||
<code>foo</code>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"```
|
||||
aaa
|
||||
~~~
|
||||
```", @"<document>
|
||||
<code_block>aaa
|
||||
~~~
|
||||
</code_block>
|
||||
</document>")]
|
||||
[InlineData(@"~~~
|
||||
aaa
|
||||
```
|
||||
~~~", @"<document>
|
||||
<code_block>aaa
|
||||
```
|
||||
</code_block>
|
||||
</document>")]
|
||||
[InlineData(@"````
|
||||
aaa
|
||||
```
|
||||
``````", @"<document>
|
||||
<code_block>aaa
|
||||
```
|
||||
</code_block>
|
||||
</document>")]
|
||||
[InlineData(@"~~~~
|
||||
aaa
|
||||
~~~
|
||||
~~~~", @"<document>
|
||||
<code_block>aaa
|
||||
~~~
|
||||
</code_block>
|
||||
</document>")]
|
||||
[InlineData(@"```", @"<document>
|
||||
<code_block></code_block>
|
||||
</document>")]
|
||||
[InlineData(@"`````
|
||||
|
||||
```
|
||||
aaa", @"<document>
|
||||
<code_block>
|
||||
```
|
||||
aaa
|
||||
</code_block>
|
||||
</document>")]
|
||||
[InlineData(@"> ```
|
||||
> aaa
|
||||
|
||||
bbb", @"<document>
|
||||
<block_quote>
|
||||
<code_block>aaa
|
||||
</code_block>
|
||||
</block_quote>
|
||||
<paragraph>
|
||||
<text>bbb</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"```
|
||||
|
||||
|
||||
```", @"<document>
|
||||
<code_block>
|
||||
|
||||
</code_block>
|
||||
</document>")]
|
||||
[InlineData(@"```
|
||||
```", @"<document>
|
||||
<code_block></code_block>
|
||||
</document>")]
|
||||
[InlineData(@" ```
|
||||
aaa
|
||||
aaa
|
||||
```", @"<document>
|
||||
<code_block>aaa
|
||||
aaa
|
||||
</code_block>
|
||||
</document>")]
|
||||
[InlineData(@" ```
|
||||
aaa
|
||||
aaa
|
||||
aaa
|
||||
```", @"<document>
|
||||
<code_block>aaa
|
||||
aaa
|
||||
aaa
|
||||
</code_block>
|
||||
</document>")]
|
||||
[InlineData(@" ```
|
||||
aaa
|
||||
aaa
|
||||
aaa
|
||||
```", @"<document>
|
||||
<code_block>aaa
|
||||
aaa
|
||||
aaa
|
||||
</code_block>
|
||||
</document>")]
|
||||
[InlineData(@" ```
|
||||
aaa
|
||||
```", @"<document>
|
||||
<code_block>```
|
||||
aaa
|
||||
```
|
||||
</code_block>
|
||||
</document>")]
|
||||
[InlineData(@"```
|
||||
aaa
|
||||
```", @"<document>
|
||||
<code_block>aaa
|
||||
</code_block>
|
||||
</document>")]
|
||||
[InlineData(@" ```
|
||||
aaa
|
||||
````", @"<document>
|
||||
<code_block>aaa
|
||||
</code_block>
|
||||
</document>")]
|
||||
[InlineData(@"```
|
||||
aaa
|
||||
```", @"<document>
|
||||
<code_block>aaa
|
||||
```
|
||||
</code_block>
|
||||
</document>")]
|
||||
[InlineData(@"``` ```
|
||||
aaa", @"<document>
|
||||
<paragraph>
|
||||
<code> </code>
|
||||
<softbreak />
|
||||
<text>aaa</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"~~~~~~
|
||||
aaa
|
||||
~~~ ~~", @"<document>
|
||||
<code_block>aaa
|
||||
~~~ ~~
|
||||
</code_block>
|
||||
</document>")]
|
||||
[InlineData(@"foo
|
||||
```
|
||||
bar
|
||||
```
|
||||
baz", @"<document>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
<code_block>bar
|
||||
</code_block>
|
||||
<paragraph>
|
||||
<text>baz</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"foo
|
||||
---
|
||||
~~~
|
||||
bar
|
||||
~~~
|
||||
# baz", @"<document>
|
||||
<heading level=""2"">
|
||||
<text>foo</text>
|
||||
</heading>
|
||||
<code_block>bar
|
||||
</code_block>
|
||||
<heading level=""1"">
|
||||
<text>baz</text>
|
||||
</heading>
|
||||
</document>")]
|
||||
[InlineData(@"```ruby
|
||||
def foo(x)
|
||||
return 3
|
||||
end
|
||||
```", @"<document>
|
||||
<code_block info=""ruby"">def foo(x)
|
||||
return 3
|
||||
end
|
||||
</code_block>
|
||||
</document>")]
|
||||
[InlineData(@"~~~~ ruby startline=3 $%@#$
|
||||
def foo(x)
|
||||
return 3
|
||||
end
|
||||
~~~~~~~", @"<document>
|
||||
<code_block info=""ruby startline=3 $%@#$"">def foo(x)
|
||||
return 3
|
||||
end
|
||||
</code_block>
|
||||
</document>")]
|
||||
[InlineData(@"````;
|
||||
````", @"<document>
|
||||
<code_block info="";""></code_block>
|
||||
</document>")]
|
||||
[InlineData(@"``` aa ```
|
||||
foo", @"<document>
|
||||
<paragraph>
|
||||
<code>aa</code>
|
||||
<softbreak />
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"~~~ aa ``` ~~~
|
||||
foo
|
||||
~~~", @"<document>
|
||||
<code_block info=""aa ``` ~~~"">foo
|
||||
</code_block>
|
||||
</document>")]
|
||||
[InlineData(@"```
|
||||
``` aaa
|
||||
```", @"<document>
|
||||
<code_block>``` aaa
|
||||
</code_block>
|
||||
</document>")]
|
||||
public void Parse_FencedCodeBlock(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
}
|
||||
132
Radzen.Blazor.Tests/Markdown/HardLineBreakTests.cs
Normal file
132
Radzen.Blazor.Tests/Markdown/HardLineBreakTests.cs
Normal file
@@ -0,0 +1,132 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Markdown.Tests;
|
||||
|
||||
public class HardLineBreakTests
|
||||
{
|
||||
private static string ToXml(string markdown)
|
||||
{
|
||||
var document = MarkdownParser.Parse(markdown);
|
||||
|
||||
return XmlVisitor.ToXml(document);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"foo
|
||||
baz", @"<document>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
<linebreak />
|
||||
<text>baz</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("foo \r\nbaz", @"<document>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
<linebreak />
|
||||
<text>baz</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"foo\
|
||||
baz", @"<document>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
<linebreak />
|
||||
<text>baz</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"foo
|
||||
baz", @"<document>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
<linebreak />
|
||||
<text>baz</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"foo
|
||||
bar", @"<document>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
<linebreak />
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"foo\
|
||||
bar", @"<document>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
<linebreak />
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"*foo
|
||||
bar*", @"<document>
|
||||
<paragraph>
|
||||
<emph>
|
||||
<text>foo</text>
|
||||
<linebreak />
|
||||
<text>bar</text>
|
||||
</emph>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"*foo\
|
||||
bar*", @"<document>
|
||||
<paragraph>
|
||||
<emph>
|
||||
<text>foo</text>
|
||||
<linebreak />
|
||||
<text>bar</text>
|
||||
</emph>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"`code\
|
||||
span`", @"<document>
|
||||
<paragraph>
|
||||
<code>code\ span</code>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"`code
|
||||
span`", @"<document>
|
||||
<paragraph>
|
||||
<code>code span</code>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"<a href=""foo
|
||||
bar"">", @"<document>
|
||||
<paragraph>
|
||||
<html_inline><a href=""foo
|
||||
bar""></html_inline>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"<a href=""foo\
|
||||
bar"">", @"<document>
|
||||
<paragraph>
|
||||
<html_inline><a href=""foo\
|
||||
bar""></html_inline>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"foo\", @"<document>
|
||||
<paragraph>
|
||||
<text>foo\</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"foo ", @"<document>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"### foo\", @"<document>
|
||||
<heading level=""3"">
|
||||
<text>foo\</text>
|
||||
</heading>
|
||||
</document>")]
|
||||
[InlineData(@"### foo ", @"<document>
|
||||
<heading level=""3"">
|
||||
<text>foo</text>
|
||||
</heading>
|
||||
</document>")]
|
||||
public void Parse_HardLineBreak(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
}
|
||||
511
Radzen.Blazor.Tests/Markdown/HeadingTests.cs
Normal file
511
Radzen.Blazor.Tests/Markdown/HeadingTests.cs
Normal file
@@ -0,0 +1,511 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Markdown.Tests;
|
||||
|
||||
public class HeadingTests
|
||||
{
|
||||
private static string ToXml(string markdown)
|
||||
{
|
||||
var document = MarkdownParser.Parse(markdown);
|
||||
|
||||
return XmlVisitor.ToXml(document);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_BasicAtxHeading()
|
||||
{
|
||||
Assert.Equal(@"<document>
|
||||
<heading level=""1"">
|
||||
<text>foo</text>
|
||||
</heading>
|
||||
</document>", ToXml("# foo"));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"# foo
|
||||
## foo
|
||||
### foo
|
||||
#### foo
|
||||
##### foo
|
||||
###### foo", @"<document>
|
||||
<heading level=""1"">
|
||||
<text>foo</text>
|
||||
</heading>
|
||||
<heading level=""2"">
|
||||
<text>foo</text>
|
||||
</heading>
|
||||
<heading level=""3"">
|
||||
<text>foo</text>
|
||||
</heading>
|
||||
<heading level=""4"">
|
||||
<text>foo</text>
|
||||
</heading>
|
||||
<heading level=""5"">
|
||||
<text>foo</text>
|
||||
</heading>
|
||||
<heading level=""6"">
|
||||
<text>foo</text>
|
||||
</heading>
|
||||
</document>")]
|
||||
[InlineData(@"####### foo", @"<document>
|
||||
<paragraph>
|
||||
<text>####### foo</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"#5 bolt
|
||||
|
||||
#hashtag", @"<document>
|
||||
<paragraph>
|
||||
<text>#5 bolt</text>
|
||||
</paragraph>
|
||||
<paragraph>
|
||||
<text>#hashtag</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"\## foo", @"<document>
|
||||
<paragraph>
|
||||
<text>#</text>
|
||||
<text># foo</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"# foo *bar* \*baz\*", @"<document>
|
||||
<heading level=""1"">
|
||||
<text>foo </text>
|
||||
<emph>
|
||||
<text>bar</text>
|
||||
</emph>
|
||||
<text> </text>
|
||||
<text>*</text>
|
||||
<text>baz</text>
|
||||
<text>*</text>
|
||||
</heading>
|
||||
</document>")]
|
||||
[InlineData(@"# foo ", @"<document>
|
||||
<heading level=""1"">
|
||||
<text>foo</text>
|
||||
</heading>
|
||||
</document>")]
|
||||
[InlineData(@" ### foo
|
||||
## foo
|
||||
# foo", @"<document>
|
||||
<heading level=""3"">
|
||||
<text>foo</text>
|
||||
</heading>
|
||||
<heading level=""2"">
|
||||
<text>foo</text>
|
||||
</heading>
|
||||
<heading level=""1"">
|
||||
<text>foo</text>
|
||||
</heading>
|
||||
</document>")]
|
||||
[InlineData(@" # foo", @"<document>
|
||||
<code_block># foo
|
||||
</code_block>
|
||||
</document>")]
|
||||
[InlineData(@"foo
|
||||
# bar", @"<document>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
<softbreak />
|
||||
<text># bar</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"## foo ##
|
||||
### bar ###", @"<document>
|
||||
<heading level=""2"">
|
||||
<text>foo</text>
|
||||
</heading>
|
||||
<heading level=""3"">
|
||||
<text>bar</text>
|
||||
</heading>
|
||||
</document>")]
|
||||
[InlineData(@"# foo ##################################
|
||||
##### foo ##", @"<document>
|
||||
<heading level=""1"">
|
||||
<text>foo</text>
|
||||
</heading>
|
||||
<heading level=""5"">
|
||||
<text>foo</text>
|
||||
</heading>
|
||||
</document>")]
|
||||
[InlineData(@"### foo ### ", @"<document>
|
||||
<heading level=""3"">
|
||||
<text>foo</text>
|
||||
</heading>
|
||||
</document>")]
|
||||
[InlineData(@"### foo ### b", @"<document>
|
||||
<heading level=""3"">
|
||||
<text>foo ### b</text>
|
||||
</heading>
|
||||
</document>")]
|
||||
[InlineData(@"# foo#", @"<document>
|
||||
<heading level=""1"">
|
||||
<text>foo#</text>
|
||||
</heading>
|
||||
</document>")]
|
||||
[InlineData(@"### foo \###
|
||||
## foo #\##
|
||||
# foo \#", @"<document>
|
||||
<heading level=""3"">
|
||||
<text>foo </text>
|
||||
<text>#</text>
|
||||
<text>##</text>
|
||||
</heading>
|
||||
<heading level=""2"">
|
||||
<text>foo #</text>
|
||||
<text>#</text>
|
||||
<text>#</text>
|
||||
</heading>
|
||||
<heading level=""1"">
|
||||
<text>foo </text>
|
||||
<text>#</text>
|
||||
</heading>
|
||||
</document>")]
|
||||
[InlineData(@"****
|
||||
## foo
|
||||
****", @"<document>
|
||||
<thematic_break />
|
||||
<heading level=""2"">
|
||||
<text>foo</text>
|
||||
</heading>
|
||||
<thematic_break />
|
||||
</document>")]
|
||||
[InlineData(@"Foo bar
|
||||
# baz
|
||||
Bar foo", @"<document>
|
||||
<paragraph>
|
||||
<text>Foo bar</text>
|
||||
</paragraph>
|
||||
<heading level=""1"">
|
||||
<text>baz</text>
|
||||
</heading>
|
||||
<paragraph>
|
||||
<text>Bar foo</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"##
|
||||
#
|
||||
### ###", @"<document>
|
||||
<heading level=""2"" />
|
||||
<heading level=""1"" />
|
||||
<heading level=""3"" />
|
||||
</document>")]
|
||||
public void Parse_AtxHeading(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"Foo *bar*
|
||||
=========
|
||||
|
||||
Foo *baz*
|
||||
---------", @"<document>
|
||||
<heading level=""1"">
|
||||
<text>Foo </text>
|
||||
<emph>
|
||||
<text>bar</text>
|
||||
</emph>
|
||||
</heading>
|
||||
<heading level=""2"">
|
||||
<text>Foo </text>
|
||||
<emph>
|
||||
<text>baz</text>
|
||||
</emph>
|
||||
</heading>
|
||||
</document>")]
|
||||
[InlineData(@"Foo *bar
|
||||
baz*
|
||||
====", @"<document>
|
||||
<heading level=""1"">
|
||||
<text>Foo </text>
|
||||
<emph>
|
||||
<text>bar</text>
|
||||
<softbreak />
|
||||
<text>baz</text>
|
||||
</emph>
|
||||
</heading>
|
||||
</document>")]
|
||||
[InlineData(@" Foo *bar
|
||||
baz*
|
||||
====", @"<document>
|
||||
<heading level=""1"">
|
||||
<text>Foo </text>
|
||||
<emph>
|
||||
<text>bar</text>
|
||||
<softbreak />
|
||||
<text>baz</text>
|
||||
</emph>
|
||||
</heading>
|
||||
</document>")]
|
||||
[InlineData(@"Foo
|
||||
-------------------------
|
||||
|
||||
Foo
|
||||
=", @"<document>
|
||||
<heading level=""2"">
|
||||
<text>Foo</text>
|
||||
</heading>
|
||||
<heading level=""1"">
|
||||
<text>Foo</text>
|
||||
</heading>
|
||||
</document>")]
|
||||
[InlineData(@" Foo
|
||||
---
|
||||
|
||||
Foo
|
||||
-----
|
||||
|
||||
Foo
|
||||
===", @"<document>
|
||||
<heading level=""2"">
|
||||
<text>Foo</text>
|
||||
</heading>
|
||||
<heading level=""2"">
|
||||
<text>Foo</text>
|
||||
</heading>
|
||||
<heading level=""1"">
|
||||
<text>Foo</text>
|
||||
</heading>
|
||||
</document>")]
|
||||
[InlineData(@" Foo
|
||||
---
|
||||
|
||||
Foo
|
||||
---", @"<document>
|
||||
<code_block>Foo
|
||||
---
|
||||
|
||||
Foo
|
||||
</code_block>
|
||||
<thematic_break />
|
||||
</document>")]
|
||||
[InlineData(@"Foo
|
||||
---- ", @"<document>
|
||||
<heading level=""2"">
|
||||
<text>Foo</text>
|
||||
</heading>
|
||||
</document>")]
|
||||
[InlineData(@"Foo
|
||||
---", @"<document>
|
||||
<paragraph>
|
||||
<text>Foo</text>
|
||||
<softbreak />
|
||||
<text>---</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"Foo
|
||||
= =
|
||||
|
||||
Foo
|
||||
--- -", @"<document>
|
||||
<paragraph>
|
||||
<text>Foo</text>
|
||||
<softbreak />
|
||||
<text>= =</text>
|
||||
</paragraph>
|
||||
<paragraph>
|
||||
<text>Foo</text>
|
||||
</paragraph>
|
||||
<thematic_break />
|
||||
</document>")]
|
||||
[InlineData(@"Foo
|
||||
-----", @"<document>
|
||||
<heading level=""2"">
|
||||
<text>Foo</text>
|
||||
</heading>
|
||||
</document>")]
|
||||
[InlineData(@"Foo\
|
||||
----", @"<document>
|
||||
<heading level=""2"">
|
||||
<text>Foo\</text>
|
||||
</heading>
|
||||
</document>")]
|
||||
[InlineData(@"`Foo
|
||||
----
|
||||
`
|
||||
|
||||
<a title=""a lot
|
||||
---
|
||||
of dashes""/>", @"<document>
|
||||
<heading level=""2"">
|
||||
<text>`</text>
|
||||
<text>Foo</text>
|
||||
</heading>
|
||||
<paragraph>
|
||||
<text>`</text>
|
||||
</paragraph>
|
||||
<heading level=""2"">
|
||||
<text><a title=""a lot</text>
|
||||
</heading>
|
||||
<paragraph>
|
||||
<text>of dashes""/></text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"> Foo
|
||||
---", @"<document>
|
||||
<block_quote>
|
||||
<paragraph>
|
||||
<text>Foo</text>
|
||||
</paragraph>
|
||||
</block_quote>
|
||||
<thematic_break />
|
||||
</document>")]
|
||||
[InlineData(@"> foo
|
||||
bar
|
||||
===", @"<document>
|
||||
<block_quote>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
<softbreak />
|
||||
<text>bar</text>
|
||||
<softbreak />
|
||||
<text>===</text>
|
||||
</paragraph>
|
||||
</block_quote>
|
||||
</document>")]
|
||||
[InlineData(@"- Foo
|
||||
---", @"<document>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>Foo</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
<thematic_break />
|
||||
</document>")]
|
||||
[InlineData(@"Foo
|
||||
Bar
|
||||
---", @"<document>
|
||||
<heading level=""2"">
|
||||
<text>Foo</text>
|
||||
<softbreak />
|
||||
<text>Bar</text>
|
||||
</heading>
|
||||
</document>")]
|
||||
[InlineData(@"---
|
||||
Foo
|
||||
---
|
||||
Bar
|
||||
---
|
||||
Baz", @"<document>
|
||||
<thematic_break />
|
||||
<heading level=""2"">
|
||||
<text>Foo</text>
|
||||
</heading>
|
||||
<heading level=""2"">
|
||||
<text>Bar</text>
|
||||
</heading>
|
||||
<paragraph>
|
||||
<text>Baz</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"
|
||||
====", @"<document>
|
||||
<paragraph>
|
||||
<text>====</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"---
|
||||
---", @"<document>
|
||||
<thematic_break />
|
||||
<thematic_break />
|
||||
</document>")]
|
||||
[InlineData(@"- foo
|
||||
-----", @"<document>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
<thematic_break />
|
||||
</document>")]
|
||||
[InlineData(@" foo
|
||||
---", @"<document>
|
||||
<code_block>foo
|
||||
</code_block>
|
||||
<thematic_break />
|
||||
</document>")]
|
||||
[InlineData(@"> foo
|
||||
-----", @"<document>
|
||||
<block_quote>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
</block_quote>
|
||||
<thematic_break />
|
||||
</document>")]
|
||||
[InlineData(@"\> foo
|
||||
------", @"<document>
|
||||
<heading level=""2"">
|
||||
<text>> foo</text>
|
||||
</heading>
|
||||
</document>")]
|
||||
[InlineData(@"Foo
|
||||
|
||||
bar
|
||||
---
|
||||
baz", @"<document>
|
||||
<paragraph>
|
||||
<text>Foo</text>
|
||||
</paragraph>
|
||||
<heading level=""2"">
|
||||
<text>bar</text>
|
||||
</heading>
|
||||
<paragraph>
|
||||
<text>baz</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"Foo
|
||||
bar
|
||||
|
||||
---
|
||||
|
||||
baz", @"<document>
|
||||
<paragraph>
|
||||
<text>Foo</text>
|
||||
<softbreak />
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
<thematic_break />
|
||||
<paragraph>
|
||||
<text>baz</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"Foo
|
||||
bar
|
||||
* * *
|
||||
baz", @"<document>
|
||||
<paragraph>
|
||||
<text>Foo</text>
|
||||
<softbreak />
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
<thematic_break />
|
||||
<paragraph>
|
||||
<text>baz</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"Foo
|
||||
bar
|
||||
\---
|
||||
baz", @"<document>
|
||||
<paragraph>
|
||||
<text>Foo</text>
|
||||
<softbreak />
|
||||
<text>bar</text>
|
||||
<softbreak />
|
||||
<text>-</text>
|
||||
<text>--</text>
|
||||
<softbreak />
|
||||
<text>baz</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
public void Parse_SetExtHeading(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
}
|
||||
487
Radzen.Blazor.Tests/Markdown/HtmlBlockTests.cs
Normal file
487
Radzen.Blazor.Tests/Markdown/HtmlBlockTests.cs
Normal file
@@ -0,0 +1,487 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Markdown.Tests;
|
||||
|
||||
public class HtmlBlockTests
|
||||
{
|
||||
private static string ToXml(string markdown)
|
||||
{
|
||||
var document = MarkdownParser.Parse(markdown);
|
||||
|
||||
return XmlVisitor.ToXml(document);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"<table><tr><td>
|
||||
<pre>
|
||||
**Hello**,
|
||||
|
||||
_world_.
|
||||
</pre>
|
||||
</td></tr></table>", @"<document>
|
||||
<html_block><table><tr><td>
|
||||
<pre>
|
||||
**Hello**,</html_block>
|
||||
<paragraph>
|
||||
<emph>
|
||||
<text>world</text>
|
||||
</emph>
|
||||
<text>.</text>
|
||||
<softbreak />
|
||||
<html_inline></pre></html_inline>
|
||||
</paragraph>
|
||||
<html_block></td></tr></table></html_block>
|
||||
</document>")]
|
||||
[InlineData(@"<table>
|
||||
<tr>
|
||||
<td>
|
||||
hi
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
okay.", @"<document>
|
||||
<html_block><table>
|
||||
<tr>
|
||||
<td>
|
||||
hi
|
||||
</td>
|
||||
</tr>
|
||||
</table></html_block>
|
||||
<paragraph>
|
||||
<text>okay.</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@" <div>
|
||||
*hello*
|
||||
<foo><a>", @"<document>
|
||||
<html_block> <div>
|
||||
*hello*
|
||||
<foo><a></html_block>
|
||||
</document>")]
|
||||
[InlineData(@"</div>
|
||||
*foo*", @"<document>
|
||||
<html_block></div>
|
||||
*foo*</html_block>
|
||||
</document>")]
|
||||
[InlineData(@"<DIV CLASS=""foo"">
|
||||
|
||||
*Markdown*
|
||||
|
||||
</DIV>", @"<document>
|
||||
<html_block><DIV CLASS=""foo""></html_block>
|
||||
<paragraph>
|
||||
<emph>
|
||||
<text>Markdown</text>
|
||||
</emph>
|
||||
</paragraph>
|
||||
<html_block></DIV></html_block>
|
||||
</document>")]
|
||||
[InlineData(@"<div id=""foo""
|
||||
class=""bar"">
|
||||
</div>", @"<document>
|
||||
<html_block><div id=""foo""
|
||||
class=""bar"">
|
||||
</div></html_block>
|
||||
</document>")]
|
||||
[InlineData(@"<div id=""foo"" class=""bar
|
||||
baz"">
|
||||
</div>", @"<document>
|
||||
<html_block><div id=""foo"" class=""bar
|
||||
baz"">
|
||||
</div></html_block>
|
||||
</document>")]
|
||||
[InlineData(@"<div>
|
||||
*foo*
|
||||
|
||||
*bar*", @"<document>
|
||||
<html_block><div>
|
||||
*foo*</html_block>
|
||||
<paragraph>
|
||||
<emph>
|
||||
<text>bar</text>
|
||||
</emph>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"<div id=""foo""
|
||||
*hi*", @"<document>
|
||||
<html_block><div id=""foo""
|
||||
*hi*</html_block>
|
||||
</document>")]
|
||||
[InlineData(@"<div class
|
||||
foo", @"<document>
|
||||
<html_block><div class
|
||||
foo</html_block>
|
||||
</document>")]
|
||||
[InlineData(@"<div *???-&&&-<---
|
||||
*foo*", @"<document>
|
||||
<html_block><div *???-&&&-<---
|
||||
*foo*</html_block>
|
||||
</document>")]
|
||||
[InlineData(@"<div><a href=""bar"">*foo*</a></div>", @"<document>
|
||||
<html_block><div><a href=""bar"">*foo*</a></div></html_block>
|
||||
</document>")]
|
||||
[InlineData(@"<table><tr><td>
|
||||
foo
|
||||
</td></tr></table>", @"<document>
|
||||
<html_block><table><tr><td>
|
||||
foo
|
||||
</td></tr></table></html_block>
|
||||
</document>")]
|
||||
[InlineData(@"<div></div>
|
||||
``` c
|
||||
int x = 33;
|
||||
```", @"<document>
|
||||
<html_block><div></div>
|
||||
``` c
|
||||
int x = 33;
|
||||
```</html_block>
|
||||
</document>")]
|
||||
[InlineData(@"<a href=""foo"">
|
||||
*bar*
|
||||
</a>", @"<document>
|
||||
<html_block><a href=""foo"">
|
||||
*bar*
|
||||
</a></html_block>
|
||||
</document>")]
|
||||
[InlineData(@"<Warning>
|
||||
*bar*
|
||||
</Warning>", @"<document>
|
||||
<html_block><Warning>
|
||||
*bar*
|
||||
</Warning></html_block>
|
||||
</document>")]
|
||||
[InlineData(@"<i class=""foo"">
|
||||
*bar*
|
||||
</i>", @"<document>
|
||||
<html_block><i class=""foo"">
|
||||
*bar*
|
||||
</i></html_block>
|
||||
</document>")]
|
||||
[InlineData(@"</ins>
|
||||
*bar*", @"<document>
|
||||
<html_block></ins>
|
||||
*bar*</html_block>
|
||||
</document>")]
|
||||
[InlineData(@"<del>
|
||||
*foo*
|
||||
</del>", @"<document>
|
||||
<html_block><del>
|
||||
*foo*
|
||||
</del></html_block>
|
||||
</document>")]
|
||||
[InlineData(@"<del>
|
||||
|
||||
*foo*
|
||||
|
||||
</del>", @"<document>
|
||||
<html_block><del></html_block>
|
||||
<paragraph>
|
||||
<emph>
|
||||
<text>foo</text>
|
||||
</emph>
|
||||
</paragraph>
|
||||
<html_block></del></html_block>
|
||||
</document>")]
|
||||
[InlineData(@"<del>*foo*</del>", @"<document>
|
||||
<paragraph>
|
||||
<html_inline><del></html_inline>
|
||||
<emph>
|
||||
<text>foo</text>
|
||||
</emph>
|
||||
<html_inline></del></html_inline>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"<pre language=""haskell""><code>
|
||||
import Text.HTML.TagSoup
|
||||
|
||||
main :: IO ()
|
||||
main = print $ parseTags tags
|
||||
</code></pre>
|
||||
okay", @"<document>
|
||||
<html_block><pre language=""haskell""><code>
|
||||
import Text.HTML.TagSoup
|
||||
|
||||
main :: IO ()
|
||||
main = print $ parseTags tags
|
||||
</code></pre></html_block>
|
||||
<paragraph>
|
||||
<text>okay</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"<script type=""text/javascript"">
|
||||
// JavaScript example
|
||||
|
||||
document.getElementById(""demo"").innerHTML = ""Hello JavaScript!"";
|
||||
</script>
|
||||
okay", @"<document>
|
||||
<html_block><script type=""text/javascript"">
|
||||
// JavaScript example
|
||||
|
||||
document.getElementById(""demo"").innerHTML = ""Hello JavaScript!"";
|
||||
</script></html_block>
|
||||
<paragraph>
|
||||
<text>okay</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"<textarea>
|
||||
|
||||
*foo*
|
||||
|
||||
_bar_
|
||||
|
||||
</textarea>", @"<document>
|
||||
<html_block><textarea>
|
||||
|
||||
*foo*
|
||||
|
||||
_bar_
|
||||
|
||||
</textarea></html_block>
|
||||
</document>")]
|
||||
[InlineData(@"<style
|
||||
type=""text/css"">
|
||||
h1 {color:red;}
|
||||
|
||||
p {color:blue;}
|
||||
</style>
|
||||
okay",@"<document>
|
||||
<html_block><style
|
||||
type=""text/css"">
|
||||
h1 {color:red;}
|
||||
|
||||
p {color:blue;}
|
||||
</style></html_block>
|
||||
<paragraph>
|
||||
<text>okay</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"<style
|
||||
type=""text/css"">
|
||||
|
||||
foo", @"<document>
|
||||
<html_block><style
|
||||
type=""text/css"">
|
||||
|
||||
foo</html_block>
|
||||
</document>")]
|
||||
[InlineData(@"> <div>
|
||||
> foo
|
||||
|
||||
bar", @"<document>
|
||||
<block_quote>
|
||||
<html_block><div>
|
||||
foo</html_block>
|
||||
</block_quote>
|
||||
<paragraph>
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"- <div>
|
||||
- foo", @"<document>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<html_block><div></html_block>
|
||||
</item>
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"<style>p{color:red;}</style>
|
||||
*foo*", @"<document>
|
||||
<html_block><style>p{color:red;}</style></html_block>
|
||||
<paragraph>
|
||||
<emph>
|
||||
<text>foo</text>
|
||||
</emph>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"<!-- foo -->*bar*
|
||||
*baz*", @"<document>
|
||||
<html_block><!-- foo -->*bar*</html_block>
|
||||
<paragraph>
|
||||
<emph>
|
||||
<text>baz</text>
|
||||
</emph>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"<script>
|
||||
foo
|
||||
</script>1. *bar*", @"<document>
|
||||
<html_block><script>
|
||||
foo
|
||||
</script>1. *bar*</html_block>
|
||||
</document>")]
|
||||
[InlineData(@"<!-- Foo
|
||||
|
||||
bar
|
||||
baz -->
|
||||
okay", @"<document>
|
||||
<html_block><!-- Foo
|
||||
|
||||
bar
|
||||
baz --></html_block>
|
||||
<paragraph>
|
||||
<text>okay</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"<?php
|
||||
|
||||
echo '>';
|
||||
|
||||
?>
|
||||
okay", @"<document>
|
||||
<html_block><?php
|
||||
|
||||
echo '>';
|
||||
|
||||
?></html_block>
|
||||
<paragraph>
|
||||
<text>okay</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"<!DOCTYPE html>", @"<document>
|
||||
<html_block><!DOCTYPE html></html_block>
|
||||
</document>")]
|
||||
[InlineData(@"<![CDATA[
|
||||
function matchwo(a,b)
|
||||
{
|
||||
if (a < b && a < 0) then {
|
||||
return 1;
|
||||
|
||||
} else {
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
]]>
|
||||
okay", @"<document>
|
||||
<html_block><![CDATA[
|
||||
function matchwo(a,b)
|
||||
{
|
||||
if (a < b && a < 0) then {
|
||||
return 1;
|
||||
|
||||
} else {
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
]]></html_block>
|
||||
<paragraph>
|
||||
<text>okay</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@" <!-- foo -->
|
||||
|
||||
<!-- foo -->", @"<document>
|
||||
<html_block> <!-- foo --></html_block>
|
||||
<code_block><!-- foo -->
|
||||
</code_block>
|
||||
</document>")]
|
||||
[InlineData(@" <div>
|
||||
|
||||
<div>", @"<document>
|
||||
<html_block> <div></html_block>
|
||||
<code_block><div>
|
||||
</code_block>
|
||||
</document>")]
|
||||
[InlineData(@"Foo
|
||||
<div>
|
||||
bar
|
||||
</div>", @"<document>
|
||||
<paragraph>
|
||||
<text>Foo</text>
|
||||
</paragraph>
|
||||
<html_block><div>
|
||||
bar
|
||||
</div></html_block>
|
||||
</document>")]
|
||||
[InlineData(@"<div>
|
||||
bar
|
||||
</div>
|
||||
*foo*", @"<document>
|
||||
<html_block><div>
|
||||
bar
|
||||
</div>
|
||||
*foo*</html_block>
|
||||
</document>")]
|
||||
[InlineData(@"Foo
|
||||
<a href=""bar"">
|
||||
baz", @"<document>
|
||||
<paragraph>
|
||||
<text>Foo</text>
|
||||
<softbreak />
|
||||
<html_inline><a href=""bar""></html_inline>
|
||||
<softbreak />
|
||||
<text>baz</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"<div>
|
||||
|
||||
*Emphasized* text.
|
||||
|
||||
</div>", @"<document>
|
||||
<html_block><div></html_block>
|
||||
<paragraph>
|
||||
<emph>
|
||||
<text>Emphasized</text>
|
||||
</emph>
|
||||
<text> text.</text>
|
||||
</paragraph>
|
||||
<html_block></div></html_block>
|
||||
</document>")]
|
||||
[InlineData(@"<div>
|
||||
*Emphasized* text.
|
||||
</div>", @"<document>
|
||||
<html_block><div>
|
||||
*Emphasized* text.
|
||||
</div></html_block>
|
||||
</document>")]
|
||||
[InlineData(@"<table>
|
||||
|
||||
<tr>
|
||||
|
||||
<td>
|
||||
Hi
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>", @"<document>
|
||||
<html_block><table></html_block>
|
||||
<html_block><tr></html_block>
|
||||
<html_block><td>
|
||||
Hi
|
||||
</td></html_block>
|
||||
<html_block></tr></html_block>
|
||||
<html_block></table></html_block>
|
||||
</document>")]
|
||||
[InlineData(@"<table>
|
||||
|
||||
<tr>
|
||||
|
||||
<td>
|
||||
Hi
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>", @"<document>
|
||||
<html_block><table></html_block>
|
||||
<html_block> <tr></html_block>
|
||||
<code_block><td>
|
||||
Hi
|
||||
</td>
|
||||
</code_block>
|
||||
<html_block> </tr></html_block>
|
||||
<html_block></table></html_block>
|
||||
</document>")]
|
||||
public void Parse_HtmlBlock(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
}
|
||||
144
Radzen.Blazor.Tests/Markdown/HtmlInlineTests.cs
Normal file
144
Radzen.Blazor.Tests/Markdown/HtmlInlineTests.cs
Normal file
@@ -0,0 +1,144 @@
|
||||
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Markdown.Tests;
|
||||
|
||||
public class HtmlInlineTests
|
||||
{
|
||||
private static string ToXml(string markdown)
|
||||
{
|
||||
var document = MarkdownParser.Parse(markdown);
|
||||
|
||||
return XmlVisitor.ToXml(document);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"<a><bab><c2c>", @"<document>
|
||||
<paragraph>
|
||||
<html_inline><a></html_inline>
|
||||
<html_inline><bab></html_inline>
|
||||
<html_inline><c2c></html_inline>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"<a/><b2/>", @"<document>
|
||||
<paragraph>
|
||||
<html_inline><a/></html_inline>
|
||||
<html_inline><b2/></html_inline>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"<a /><b2
|
||||
data=""foo"" >", @"<document>
|
||||
<paragraph>
|
||||
<html_inline><a /></html_inline>
|
||||
<html_inline><b2
|
||||
data=""foo"" ></html_inline>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"<a foo=""bar"" bam = 'baz <em>""</em>'
|
||||
_boolean zoop:33=zoop:33 />", @"<document>
|
||||
<paragraph>
|
||||
<html_inline><a foo=""bar"" bam = 'baz <em>""</em>'
|
||||
_boolean zoop:33=zoop:33 /></html_inline>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"Foo <responsive-image src=""foo.jpg"" />", @"<document>
|
||||
<paragraph>
|
||||
<text>Foo </text>
|
||||
<html_inline><responsive-image src=""foo.jpg"" /></html_inline>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("<33> <__>", @"<document>
|
||||
<paragraph>
|
||||
<text><33> <</text>
|
||||
<text>__</text>
|
||||
<text>></text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"<a h*#ref=""hi"">", @"<document>
|
||||
<paragraph>
|
||||
<text><a h</text>
|
||||
<text>*</text>
|
||||
<text>#ref=""hi""></text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"<a href='bar'title=title>", @"<document>
|
||||
<paragraph>
|
||||
<text><a href='bar'title=title></text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"</a></foo >", @"<document>
|
||||
<paragraph>
|
||||
<html_inline></a></html_inline>
|
||||
<html_inline></foo ></html_inline>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"</a href=""foo"">", @"<document>
|
||||
<paragraph>
|
||||
<text></a href=""foo""></text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"foo <!-- this is a --
|
||||
comment - with hyphens -->", @"<document>
|
||||
<paragraph>
|
||||
<text>foo </text>
|
||||
<html_inline><!-- this is a --
|
||||
comment - with hyphens --></html_inline>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"foo <!--> foo -->
|
||||
|
||||
foo <!---> foo -->
|
||||
", @"<document>
|
||||
<paragraph>
|
||||
<text>foo </text>
|
||||
<html_inline><!--></html_inline>
|
||||
<text> foo --></text>
|
||||
</paragraph>
|
||||
<paragraph>
|
||||
<text>foo </text>
|
||||
<html_inline><!---></html_inline>
|
||||
<text> foo --></text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"foo <?php echo $a; ?>", @"<document>
|
||||
<paragraph>
|
||||
<text>foo </text>
|
||||
<html_inline><?php echo $a; ?></html_inline>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"foo <!ELEMENT br EMPTY>", @"<document>
|
||||
<paragraph>
|
||||
<text>foo </text>
|
||||
<html_inline><!ELEMENT br EMPTY></html_inline>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"foo <![CDATA[>&<]]>", @"<document>
|
||||
<paragraph>
|
||||
<text>foo </text>
|
||||
<html_inline><![CDATA[>&<]]></html_inline>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"foo <a href=""ö"">", @"<document>
|
||||
<paragraph>
|
||||
<text>foo </text>
|
||||
<html_inline><a href=""&ouml;""></html_inline>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"foo <a href=""\*"">", @"<document>
|
||||
<paragraph>
|
||||
<text>foo </text>
|
||||
<html_inline><a href=""\*""></html_inline>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"foo <a href=""\"""">", @"<document>
|
||||
<paragraph>
|
||||
<text>foo <a href=""</text>
|
||||
<text>""</text>
|
||||
<text>""></text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
public void Parse_Html(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
}
|
||||
85
Radzen.Blazor.Tests/Markdown/ImageTests.cs
Normal file
85
Radzen.Blazor.Tests/Markdown/ImageTests.cs
Normal file
@@ -0,0 +1,85 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Markdown.Tests;
|
||||
|
||||
public class ImageTests
|
||||
{
|
||||
|
||||
private static string ToXml(string markdown)
|
||||
{
|
||||
var document = MarkdownParser.Parse(markdown);
|
||||
|
||||
return XmlVisitor.ToXml(document);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"", @"<document>
|
||||
<paragraph>
|
||||
<image destination=""/url"" title=""title"">
|
||||
<text>foo</text>
|
||||
</image>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"](/url2)", @"<document>
|
||||
<paragraph>
|
||||
<image destination=""/url2"" title="""">
|
||||
<text>foo </text>
|
||||
<image destination=""/url"" title="""">
|
||||
<text>bar</text>
|
||||
</image>
|
||||
</image>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"](/url2)", @"<document>
|
||||
<paragraph>
|
||||
<image destination=""/url2"" title="""">
|
||||
<text>foo </text>
|
||||
<link destination=""/url"" title="""">
|
||||
<text>bar</text>
|
||||
</link>
|
||||
</image>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"", @"<document>
|
||||
<paragraph>
|
||||
<image destination=""train.jpg"" title="""">
|
||||
<text>foo</text>
|
||||
</image>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"My ", @"<document>
|
||||
<paragraph>
|
||||
<text>My </text>
|
||||
<image destination=""/path/to/train.jpg"" title=""title"">
|
||||
<text>foo bar</text>
|
||||
</image>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"", @"<document>
|
||||
<paragraph>
|
||||
<image destination=""url"" title="""">
|
||||
<text>foo</text>
|
||||
</image>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"", @"<document>
|
||||
<paragraph>
|
||||
<image destination=""/url"" title="""" />
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"__Applications__ ", @"<document>
|
||||
<paragraph>
|
||||
<strong>
|
||||
<text>Applications</text>
|
||||
</strong>
|
||||
<text> </text>
|
||||
<image destination=""/assets/img/macOS-drag-and-drop.png"" title="""">
|
||||
<text>macOS DMG</text>
|
||||
</image>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
public void Parse_BasicImages_ReturnsImageElement(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
}
|
||||
145
Radzen.Blazor.Tests/Markdown/IndentedCodeBlockTests.cs
Normal file
145
Radzen.Blazor.Tests/Markdown/IndentedCodeBlockTests.cs
Normal file
@@ -0,0 +1,145 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Markdown.Tests;
|
||||
|
||||
public class IndentedCodeBlockTests
|
||||
{
|
||||
private static string ToXml(string markdown)
|
||||
{
|
||||
var document = MarkdownParser.Parse(markdown);
|
||||
|
||||
return XmlVisitor.ToXml(document);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@" a simple
|
||||
indented code block
|
||||
", @"<document>
|
||||
<code_block>a simple
|
||||
indented code block
|
||||
</code_block>
|
||||
</document>")]
|
||||
[InlineData(@" - foo
|
||||
|
||||
bar", @"<document>
|
||||
<list type=""bullet"" tight=""false"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
<paragraph>
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(
|
||||
@"
|
||||
1. foo
|
||||
|
||||
- bar", @"<document>
|
||||
<list type=""ordered"" start=""1"" tight=""false"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@" <a/>
|
||||
*hi*
|
||||
|
||||
- one", @"<document>
|
||||
<code_block><a/>
|
||||
*hi*
|
||||
|
||||
- one
|
||||
</code_block>
|
||||
</document>")]
|
||||
[InlineData(@" chunk1
|
||||
|
||||
chunk2
|
||||
|
||||
|
||||
|
||||
chunk3", @"<document>
|
||||
<code_block>chunk1
|
||||
|
||||
chunk2
|
||||
|
||||
|
||||
|
||||
chunk3
|
||||
</code_block>
|
||||
</document>")]
|
||||
[InlineData(@" chunk1
|
||||
|
||||
chunk2", @"<document>
|
||||
<code_block>chunk1
|
||||
|
||||
chunk2
|
||||
</code_block>
|
||||
</document>")]
|
||||
[InlineData(@"Foo
|
||||
bar", @"<document>
|
||||
<paragraph>
|
||||
<text>Foo</text>
|
||||
<softbreak />
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@" foo
|
||||
bar", @"<document>
|
||||
<code_block>foo
|
||||
</code_block>
|
||||
<paragraph>
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"# Heading
|
||||
foo
|
||||
Heading
|
||||
------
|
||||
foo
|
||||
----", @"<document>
|
||||
<heading level=""1"">
|
||||
<text>Heading</text>
|
||||
</heading>
|
||||
<code_block>foo
|
||||
</code_block>
|
||||
<heading level=""2"">
|
||||
<text>Heading</text>
|
||||
</heading>
|
||||
<code_block>foo
|
||||
</code_block>
|
||||
<thematic_break />
|
||||
</document>")]
|
||||
[InlineData(@" foo
|
||||
bar", @"<document>
|
||||
<code_block> foo
|
||||
bar
|
||||
</code_block>
|
||||
</document>")]
|
||||
[InlineData(@"
|
||||
|
||||
foo
|
||||
", @"<document>
|
||||
<code_block>foo
|
||||
</code_block>
|
||||
</document>")]
|
||||
[InlineData(@" foo ", @"<document>
|
||||
<code_block>foo
|
||||
</code_block>
|
||||
</document>")]
|
||||
public void Parse_IndentedCodeBlock(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
}
|
||||
852
Radzen.Blazor.Tests/Markdown/LinkTests.cs
Normal file
852
Radzen.Blazor.Tests/Markdown/LinkTests.cs
Normal file
@@ -0,0 +1,852 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Markdown.Tests;
|
||||
|
||||
public class LinkTests
|
||||
{
|
||||
private static string ToXml(string markdown)
|
||||
{
|
||||
var document = MarkdownParser.Parse(markdown);
|
||||
|
||||
return XmlVisitor.ToXml(document);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("[link](/uri \"title\")",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<link destination=""/uri"" title=""title"">
|
||||
<text>link</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("[link](/uri)", @"<document>
|
||||
<paragraph>
|
||||
<link destination=""/uri"" title="""">
|
||||
<text>link</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("[](./target.md)", @"<document>
|
||||
<paragraph>
|
||||
<link destination=""./target.md"" title="""" />
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("[link]()", @"<document>
|
||||
<paragraph>
|
||||
<link destination="""" title="""">
|
||||
<text>link</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("[link](<>)", @"<document>
|
||||
<paragraph>
|
||||
<link destination="""" title="""">
|
||||
<text>link</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("[]()", @"<document>
|
||||
<paragraph>
|
||||
<link destination="""" title="""" />
|
||||
</paragraph>
|
||||
</document>")]
|
||||
public void Parse_BasicLinks_ReturnsLinkNode(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("[link](/my uri)", @"<document>
|
||||
<paragraph>
|
||||
<text>[</text>
|
||||
<text>link</text>
|
||||
<text>](/my uri)</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("[link](</my uri>)", @"<document>
|
||||
<paragraph>
|
||||
<link destination=""/my uri"" title="""">
|
||||
<text>link</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
public void Parse_LinkDestinationWithSpaces(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"[link](foo
|
||||
bar)", @"<document>
|
||||
<paragraph>
|
||||
<text>[</text>
|
||||
<text>link</text>
|
||||
<text>](foo</text>
|
||||
<softbreak />
|
||||
<text>bar)</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
|
||||
public void Parse_LinkDestinationWithNewLines(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"[a](<b)c>)", @"<document>
|
||||
<paragraph>
|
||||
<link destination=""b)c"" title="""">
|
||||
<text>a</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
public void Parse_LinkDestinationWithCloseParenthesis(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"[link](<foo\>)", @"<document>
|
||||
<paragraph>
|
||||
<text>[</text>
|
||||
<text>link</text>
|
||||
<text>](<foo>)</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"[a](<b)c
|
||||
[a](<b)c>
|
||||
[a](<b>c)", @"<document>
|
||||
<paragraph>
|
||||
<text>[</text>
|
||||
<text>a</text>
|
||||
<text>](<b)c</text>
|
||||
<softbreak />
|
||||
<text>[</text>
|
||||
<text>a</text>
|
||||
<text>](<b)c></text>
|
||||
<softbreak />
|
||||
<text>[</text>
|
||||
<text>a</text>
|
||||
<text>](</text>
|
||||
<html_inline><b></html_inline>
|
||||
<text>c)</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
public void Parse_LinkDestinationUnclosedPointyBracket(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"[link](\(foo\))", @"<document>
|
||||
<paragraph>
|
||||
<link destination=""(foo)"" title="""">
|
||||
<text>link</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"[link](foo\)\:)", @"<document>
|
||||
<paragraph>
|
||||
<link destination=""foo):"" title="""">
|
||||
<text>link</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"[link](foo\bar)", @"<document>
|
||||
<paragraph>
|
||||
<link destination=""foo\bar"" title="""">
|
||||
<text>link</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
public void Parse_Escapes(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"[link](foo(and(bar)))", @"<document>
|
||||
<paragraph>
|
||||
<link destination=""foo(and(bar))"" title="""">
|
||||
<text>link</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"[link](foo(and(bar))", @"<document>
|
||||
<paragraph>
|
||||
<text>[</text>
|
||||
<text>link</text>
|
||||
<text>](foo(and(bar))</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"[link](foo\(and\(bar\))", @"<document>
|
||||
<paragraph>
|
||||
<link destination=""foo(and(bar)"" title="""">
|
||||
<text>link</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"[link](<foo(and(bar)>)", @"<document>
|
||||
<paragraph>
|
||||
<link destination=""foo(and(bar)"" title="""">
|
||||
<text>link</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
public void Parse_BallancedParenthesisInLinkDestination(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"[link](#fragment)
|
||||
[link](https://example.com#fragment)
|
||||
[link](https://example.com?foo=3#frag)", @"<document>
|
||||
<paragraph>
|
||||
<link destination=""#fragment"" title="""">
|
||||
<text>link</text>
|
||||
</link>
|
||||
<softbreak />
|
||||
<link destination=""https://example.com#fragment"" title="""">
|
||||
<text>link</text>
|
||||
</link>
|
||||
<softbreak />
|
||||
<link destination=""https://example.com?foo=3#frag"" title="""">
|
||||
<text>link</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
public void Parse_FragmentAndQueryStringInLinkDestination(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"[link](""title"")", @"<document>
|
||||
<paragraph>
|
||||
<link destination="""title""" title="""">
|
||||
<text>link</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
public void Parse_QuotesInDestination(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"[link](/url ""title"")
|
||||
[link](/url 'title')
|
||||
[link](/url (title))", @"<document>
|
||||
<paragraph>
|
||||
<link destination=""/url"" title=""title"">
|
||||
<text>link</text>
|
||||
</link>
|
||||
<softbreak />
|
||||
<link destination=""/url"" title=""title"">
|
||||
<text>link</text>
|
||||
</link>
|
||||
<softbreak />
|
||||
<link destination=""/url"" title=""title"">
|
||||
<text>link</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"[link](/url ""title ""and"" title"")", @"<document>
|
||||
<paragraph>
|
||||
<text>[</text>
|
||||
<text>link</text>
|
||||
<text>](/url ""title ""and"" title"")</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"[link](/url 'title ""and"" title')", @"<document>
|
||||
<paragraph>
|
||||
<link destination=""/url"" title=""title "and" title"">
|
||||
<text>link</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
public void Parse_Title(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"[link]( /url
|
||||
""title"" )", @"<document>
|
||||
<paragraph>
|
||||
<link destination=""/url"" title=""title"">
|
||||
<text>link</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"[link] (/uri)", @"<document>
|
||||
<paragraph>
|
||||
<text>[</text>
|
||||
<text>link</text>
|
||||
<text>] (/uri)</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
public void Parse_SpacesInLink(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"[link [foo [bar]]](/uri)", @"<document>
|
||||
<paragraph>
|
||||
<link destination=""/uri"" title="""">
|
||||
<text>link </text>
|
||||
<text>[</text>
|
||||
<text>foo </text>
|
||||
<text>[</text>
|
||||
<text>bar</text>
|
||||
<text>]</text>
|
||||
<text>]</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"[link] bar](/uri)", @"<document>
|
||||
<paragraph>
|
||||
<text>[</text>
|
||||
<text>link</text>
|
||||
<text>] bar](/uri)</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"[link [bar](/uri)", @"<document>
|
||||
<paragraph>
|
||||
<text>[</text>
|
||||
<text>link </text>
|
||||
<link destination=""/uri"" title="""">
|
||||
<text>bar</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"[link \[bar](/uri)", @"<document>
|
||||
<paragraph>
|
||||
<link destination=""/uri"" title="""">
|
||||
<text>link </text>
|
||||
<text>[</text>
|
||||
<text>bar</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
public void Parse_BracketsInText(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"[link *foo **bar** `#`*](/uri)", @"<document>
|
||||
<paragraph>
|
||||
<link destination=""/uri"" title="""">
|
||||
<text>link </text>
|
||||
<emph>
|
||||
<text>foo </text>
|
||||
<strong>
|
||||
<text>bar</text>
|
||||
</strong>
|
||||
<text> </text>
|
||||
<code>#</code>
|
||||
</emph>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"[](url)", @"<document>
|
||||
<paragraph>
|
||||
<link destination=""url"" title="""">
|
||||
<image destination=""img"" title="""">
|
||||
<text>alt</text>
|
||||
</image>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
|
||||
public void Parse_LinkTextIsInlineContent(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"[foo [bar](/uri)](/uri)", @"<document>
|
||||
<paragraph>
|
||||
<text>[</text>
|
||||
<text>foo </text>
|
||||
<link destination=""/uri"" title="""">
|
||||
<text>bar</text>
|
||||
</link>
|
||||
<text>](/uri)</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"[foo *[bar [baz](/uri)](/uri)*](/uri)", @"<document>
|
||||
<paragraph>
|
||||
<text>[</text>
|
||||
<text>foo </text>
|
||||
<emph>
|
||||
<text>[</text>
|
||||
<text>bar </text>
|
||||
<link destination=""/uri"" title="""">
|
||||
<text>baz</text>
|
||||
</link>
|
||||
<text>](/uri)</text>
|
||||
</emph>
|
||||
<text>](/uri)</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
public void Parse_NestedLinks(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"*[foo*](/uri)", @"<document>
|
||||
<paragraph>
|
||||
<text>*</text>
|
||||
<link destination=""/uri"" title="""">
|
||||
<text>foo</text>
|
||||
<text>*</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"[foo *bar](baz*)", @"<document>
|
||||
<paragraph>
|
||||
<link destination=""baz*"" title="""">
|
||||
<text>foo </text>
|
||||
<text>*</text>
|
||||
<text>bar</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"*foo [bar* baz]", @"<document>
|
||||
<paragraph>
|
||||
<emph>
|
||||
<text>foo </text>
|
||||
<text>[</text>
|
||||
<text>bar</text>
|
||||
</emph>
|
||||
<text> baz</text>
|
||||
<text>]</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"[foo`](/uri)`", @"<document>
|
||||
<paragraph>
|
||||
<text>[</text>
|
||||
<text>foo</text>
|
||||
<code>](/uri)</code>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
public void Parse_Precedence(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"<http://foo.bar.baz>", @"<document>
|
||||
<paragraph>
|
||||
<link destination=""http://foo.bar.baz"" title="""">
|
||||
<text>http://foo.bar.baz</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"<https://foo.bar.baz/test?q=hello&id=22&boolean>", @"<document>
|
||||
<paragraph>
|
||||
<link destination=""https://foo.bar.baz/test?q=hello&id=22&boolean"" title="""">
|
||||
<text>https://foo.bar.baz/test?q=hello&id=22&boolean</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"<irc://foo.bar:2233/baz>", @"<document>
|
||||
<paragraph>
|
||||
<link destination=""irc://foo.bar:2233/baz"" title="""">
|
||||
<text>irc://foo.bar:2233/baz</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"<MAILTO:FOO@BAR.BAZ>", @"<document>
|
||||
<paragraph>
|
||||
<link destination=""MAILTO:FOO@BAR.BAZ"" title="""">
|
||||
<text>MAILTO:FOO@BAR.BAZ</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"<https://foo.bar/baz bim>", @"<document>
|
||||
<paragraph>
|
||||
<text><https://foo.bar/baz bim></text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"<https://example.com/\[\>", @"<document>
|
||||
<paragraph>
|
||||
<link destination=""https://example.com/\[\"" title="""">
|
||||
<text>https://example.com/\[\</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"<foo@bar.example.com>", @"<document>
|
||||
<paragraph>
|
||||
<link destination=""mailto:foo@bar.example.com"" title="""">
|
||||
<text>foo@bar.example.com</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"<foo+special@Bar.baz-bar0.com>", @"<document>
|
||||
<paragraph>
|
||||
<link destination=""mailto:foo+special@Bar.baz-bar0.com"" title="""">
|
||||
<text>foo+special@Bar.baz-bar0.com</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"<foo\+@bar.example.com>", @"<document>
|
||||
<paragraph>
|
||||
<text><foo+@bar.example.com></text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"<>", @"<document>
|
||||
<paragraph>
|
||||
<text><></text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"< https://foo.bar >", @"<document>
|
||||
<paragraph>
|
||||
<text>< https://foo.bar ></text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"<m:abc>", @"<document>
|
||||
<paragraph>
|
||||
<text><m:abc></text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"<foo.bar.baz>", @"<document>
|
||||
<paragraph>
|
||||
<text><foo.bar.baz></text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"https://example.com", @"<document>
|
||||
<paragraph>
|
||||
<text>https://example.com</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"foo@bar.example.com", @"<document>
|
||||
<paragraph>
|
||||
<text>foo@bar.example.com</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
public void Parse_AutoLink(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"[foo]: /url ""title""
|
||||
|
||||
[foo]", @"<document>
|
||||
<paragraph>
|
||||
<link destination=""/url"" title=""title"">
|
||||
<text>foo</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@" [foo]:
|
||||
/url
|
||||
'the title'
|
||||
|
||||
[foo]", @"<document>
|
||||
<paragraph>
|
||||
<link destination=""/url"" title=""the title"">
|
||||
<text>foo</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"[Foo*bar\]]:my_(url) 'title (with parens)'
|
||||
|
||||
[Foo*bar\]]", @"<document>
|
||||
<paragraph>
|
||||
<link destination=""my_(url)"" title=""title (with parens)"">
|
||||
<text>Foo</text>
|
||||
<text>*</text>
|
||||
<text>bar</text>
|
||||
<text>]</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"[Foo bar]:
|
||||
<my url>
|
||||
'title'
|
||||
|
||||
[Foo bar]", @"<document>
|
||||
<paragraph>
|
||||
<link destination=""my url"" title=""title"">
|
||||
<text>Foo bar</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"[foo]: /url '
|
||||
title
|
||||
line1
|
||||
line2
|
||||
'
|
||||
|
||||
[foo]", @"<document>
|
||||
<paragraph>
|
||||
<link destination=""/url"" title=""
title
line1
line2
"">
|
||||
<text>foo</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"[foo]: /url 'title
|
||||
|
||||
with blank line'
|
||||
|
||||
[foo]", @"<document>
|
||||
<paragraph>
|
||||
<text>[</text>
|
||||
<text>foo</text>
|
||||
<text>]: /url 'title</text>
|
||||
</paragraph>
|
||||
<paragraph>
|
||||
<text>with blank line'</text>
|
||||
</paragraph>
|
||||
<paragraph>
|
||||
<text>[</text>
|
||||
<text>foo</text>
|
||||
<text>]</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"[foo]:
|
||||
/url
|
||||
|
||||
[foo]", @"<document>
|
||||
<paragraph>
|
||||
<link destination=""/url"" title="""">
|
||||
<text>foo</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"[foo]:
|
||||
|
||||
[foo]", @"<document>
|
||||
<paragraph>
|
||||
<text>[</text>
|
||||
<text>foo</text>
|
||||
<text>]:</text>
|
||||
</paragraph>
|
||||
<paragraph>
|
||||
<text>[</text>
|
||||
<text>foo</text>
|
||||
<text>]</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"[foo]: <>
|
||||
|
||||
[foo]", @"<document>
|
||||
<paragraph>
|
||||
<link destination="""" title="""">
|
||||
<text>foo</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"[foo]: <bar>(baz)
|
||||
|
||||
[foo]", @"<document>
|
||||
<paragraph>
|
||||
<text>[</text>
|
||||
<text>foo</text>
|
||||
<text>]: </text>
|
||||
<html_inline><bar></html_inline>
|
||||
<text>(baz)</text>
|
||||
</paragraph>
|
||||
<paragraph>
|
||||
<text>[</text>
|
||||
<text>foo</text>
|
||||
<text>]</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"[foo]: /url\bar\*baz ""foo\""bar\baz""
|
||||
|
||||
[foo]
|
||||
", @"<document>
|
||||
<paragraph>
|
||||
<link destination=""/url\bar*baz"" title=""foo\"bar\baz"">
|
||||
<text>foo</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"[foo]
|
||||
|
||||
[foo]: url", @"<document>
|
||||
<paragraph>
|
||||
<link destination=""url"" title="""">
|
||||
<text>foo</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"[foo]
|
||||
|
||||
[foo]: first
|
||||
[foo]: second", @"<document>
|
||||
<paragraph>
|
||||
<link destination=""first"" title="""">
|
||||
<text>foo</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"[FOO]: /url
|
||||
|
||||
[Foo]", @"<document>
|
||||
<paragraph>
|
||||
<link destination=""/url"" title="""">
|
||||
<text>Foo</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"[ΑΓΩ]: /φου
|
||||
|
||||
[αγω]", @"<document>
|
||||
<paragraph>
|
||||
<link destination=""/φου"" title="""">
|
||||
<text>αγω</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"[foo]: /url", @"<document />")]
|
||||
[InlineData(@"[
|
||||
foo
|
||||
]: /url
|
||||
bar", @"<document>
|
||||
<paragraph>
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"[foo]: /url ""title"" ok", @"<document>
|
||||
<paragraph>
|
||||
<text>[</text>
|
||||
<text>foo</text>
|
||||
<text>]: /url ""title"" ok</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"[foo]: /url
|
||||
""title"" ok", @"<document>
|
||||
<paragraph>
|
||||
<text>""title"" ok</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@" [foo]: /url ""title""
|
||||
|
||||
[foo]", @"<document>
|
||||
<code_block>[foo]: /url ""title""
|
||||
</code_block>
|
||||
<paragraph>
|
||||
<text>[</text>
|
||||
<text>foo</text>
|
||||
<text>]</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"```
|
||||
[foo]: /url
|
||||
```
|
||||
|
||||
[foo]", @"<document>
|
||||
<code_block>[foo]: /url
|
||||
</code_block>
|
||||
<paragraph>
|
||||
<text>[</text>
|
||||
<text>foo</text>
|
||||
<text>]</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"Foo
|
||||
[bar]: /baz
|
||||
|
||||
[bar]", @"<document>
|
||||
<paragraph>
|
||||
<text>Foo</text>
|
||||
<softbreak />
|
||||
<text>[</text>
|
||||
<text>bar</text>
|
||||
<text>]: /baz</text>
|
||||
</paragraph>
|
||||
<paragraph>
|
||||
<text>[</text>
|
||||
<text>bar</text>
|
||||
<text>]</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"# [Foo]
|
||||
[foo]: /url
|
||||
> bar", @"<document>
|
||||
<heading level=""1"">
|
||||
<link destination=""/url"" title="""">
|
||||
<text>Foo</text>
|
||||
</link>
|
||||
</heading>
|
||||
<block_quote>
|
||||
<paragraph>
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
</block_quote>
|
||||
</document>")]
|
||||
[InlineData(@"[foo]: /url
|
||||
bar
|
||||
===
|
||||
[foo]", @"<document>
|
||||
<heading level=""1"">
|
||||
<text>bar</text>
|
||||
</heading>
|
||||
<paragraph>
|
||||
<link destination=""/url"" title="""">
|
||||
<text>foo</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"[foo]: /url
|
||||
===
|
||||
[foo]", @"<document>
|
||||
<paragraph>
|
||||
<text>===</text>
|
||||
<softbreak />
|
||||
<link destination=""/url"" title="""">
|
||||
<text>foo</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"[foo]: /foo-url ""foo""
|
||||
[bar]: /bar-url
|
||||
""bar""
|
||||
[baz]: /baz-url
|
||||
|
||||
[foo],
|
||||
[bar],
|
||||
[baz]", @"<document>
|
||||
<paragraph>
|
||||
<link destination=""/foo-url"" title=""foo"">
|
||||
<text>foo</text>
|
||||
</link>
|
||||
<text>,</text>
|
||||
<softbreak />
|
||||
<link destination=""/bar-url"" title=""bar"">
|
||||
<text>bar</text>
|
||||
</link>
|
||||
<text>,</text>
|
||||
<softbreak />
|
||||
<link destination=""/baz-url"" title="""">
|
||||
<text>baz</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"[foo]
|
||||
|
||||
> [foo]: /url
|
||||
", @"<document>
|
||||
<paragraph>
|
||||
<link destination=""/url"" title="""">
|
||||
<text>foo</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
<block_quote />
|
||||
</document>")]
|
||||
public void Parse_LinkReference(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
}
|
||||
880
Radzen.Blazor.Tests/Markdown/ListItemTests.cs
Normal file
880
Radzen.Blazor.Tests/Markdown/ListItemTests.cs
Normal file
@@ -0,0 +1,880 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Markdown.Tests;
|
||||
|
||||
public class ListItemTests
|
||||
{
|
||||
private static string ToXml(string markdown)
|
||||
{
|
||||
var document = MarkdownParser.Parse(markdown);
|
||||
|
||||
return XmlVisitor.ToXml(document);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"1. A paragraph
|
||||
with two lines.
|
||||
|
||||
indented code
|
||||
|
||||
> A block quote.", @"<document>
|
||||
<list type=""ordered"" start=""1"" tight=""false"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>A paragraph</text>
|
||||
<softbreak />
|
||||
<text>with two lines.</text>
|
||||
</paragraph>
|
||||
<code_block>indented code
|
||||
</code_block>
|
||||
<block_quote>
|
||||
<paragraph>
|
||||
<text>A block quote.</text>
|
||||
</paragraph>
|
||||
</block_quote>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"- one
|
||||
|
||||
two", @"<document>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>one</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
<paragraph>
|
||||
<text>two</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"- one
|
||||
|
||||
two", @"<document>
|
||||
<list type=""bullet"" tight=""false"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>one</text>
|
||||
</paragraph>
|
||||
<paragraph>
|
||||
<text>two</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@" - one
|
||||
|
||||
two", @"<document>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>one</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
<code_block> two
|
||||
</code_block>
|
||||
</document>")]
|
||||
[InlineData(@" - one
|
||||
|
||||
two", @"<document>
|
||||
<list type=""bullet"" tight=""false"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>one</text>
|
||||
</paragraph>
|
||||
<paragraph>
|
||||
<text>two</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@" > > 1. one
|
||||
>>
|
||||
>> two
|
||||
", @"<document>
|
||||
<block_quote>
|
||||
<block_quote>
|
||||
<list type=""ordered"" start=""1"" tight=""false"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>one</text>
|
||||
</paragraph>
|
||||
<paragraph>
|
||||
<text>two</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</block_quote>
|
||||
</block_quote>
|
||||
</document>")]
|
||||
[InlineData(@">>- one
|
||||
>>
|
||||
> > two
|
||||
", @"<document>
|
||||
<block_quote>
|
||||
<block_quote>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>one</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
<paragraph>
|
||||
<text>two</text>
|
||||
</paragraph>
|
||||
</block_quote>
|
||||
</block_quote>
|
||||
</document>")]
|
||||
[InlineData(@"-one
|
||||
|
||||
2.two
|
||||
", @"<document>
|
||||
<paragraph>
|
||||
<text>-one</text>
|
||||
</paragraph>
|
||||
<paragraph>
|
||||
<text>2.two</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"- foo
|
||||
|
||||
|
||||
bar", @"<document>
|
||||
<list type=""bullet"" tight=""false"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
<paragraph>
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"1. foo
|
||||
|
||||
```
|
||||
bar
|
||||
```
|
||||
|
||||
baz
|
||||
|
||||
> bam", @"<document>
|
||||
<list type=""ordered"" start=""1"" tight=""false"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
<code_block>bar
|
||||
</code_block>
|
||||
<paragraph>
|
||||
<text>baz</text>
|
||||
</paragraph>
|
||||
<block_quote>
|
||||
<paragraph>
|
||||
<text>bam</text>
|
||||
</paragraph>
|
||||
</block_quote>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"- Foo
|
||||
|
||||
bar
|
||||
|
||||
|
||||
baz", @"<document>
|
||||
<list type=""bullet"" tight=""false"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>Foo</text>
|
||||
</paragraph>
|
||||
<code_block>bar
|
||||
|
||||
|
||||
baz
|
||||
</code_block>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"123456789. ok", @"<document>
|
||||
<list type=""ordered"" start=""123456789"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>ok</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"1234567890. not ok", @"<document>
|
||||
<paragraph>
|
||||
<text>1234567890. not ok</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"0. ok", @"<document>
|
||||
<list type=""ordered"" start=""0"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>ok</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"003. ok", @"<document>
|
||||
<list type=""ordered"" start=""3"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>ok</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"-1. not ok", @"<document>
|
||||
<paragraph>
|
||||
<text>-1. not ok</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"- foo
|
||||
|
||||
bar", @"<document>
|
||||
<list type=""bullet"" tight=""false"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
<code_block>bar
|
||||
</code_block>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@" 10. foo
|
||||
|
||||
bar", @"<document>
|
||||
<list type=""ordered"" start=""10"" tight=""false"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
<code_block>bar
|
||||
</code_block>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"1. indented code
|
||||
|
||||
paragraph
|
||||
|
||||
more code", @"<document>
|
||||
<list type=""ordered"" start=""1"" tight=""false"">
|
||||
<item>
|
||||
<code_block>indented code
|
||||
</code_block>
|
||||
<paragraph>
|
||||
<text>paragraph</text>
|
||||
</paragraph>
|
||||
<code_block>more code
|
||||
</code_block>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"- indented code
|
||||
|
||||
paragraph
|
||||
|
||||
more code", @"<document>
|
||||
<list type=""bullet"" tight=""false"">
|
||||
<item>
|
||||
<code_block>indented code
|
||||
</code_block>
|
||||
<paragraph>
|
||||
<text>paragraph</text>
|
||||
</paragraph>
|
||||
<code_block>more code
|
||||
</code_block>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"1. indented code
|
||||
|
||||
paragraph
|
||||
|
||||
more code", @"<document>
|
||||
<list type=""ordered"" start=""1"" tight=""false"">
|
||||
<item>
|
||||
<code_block> indented code
|
||||
</code_block>
|
||||
<paragraph>
|
||||
<text>paragraph</text>
|
||||
</paragraph>
|
||||
<code_block>more code
|
||||
</code_block>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"- foo
|
||||
|
||||
bar", @"<document>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
<paragraph>
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"- foo
|
||||
|
||||
bar", @"<document>
|
||||
<list type=""bullet"" tight=""false"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
<paragraph>
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"-
|
||||
foo
|
||||
-
|
||||
```
|
||||
bar
|
||||
```
|
||||
-
|
||||
baz", @"<document>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
<item>
|
||||
<code_block>bar
|
||||
</code_block>
|
||||
</item>
|
||||
<item>
|
||||
<code_block>baz
|
||||
</code_block>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"1.
|
||||
foo", @"<document>
|
||||
<list type=""ordered"" start=""1"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"-
|
||||
foo", @"<document>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"-
|
||||
|
||||
foo", @"<document>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item />
|
||||
</list>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"- foo
|
||||
-
|
||||
- bar", @"<document>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
<item />
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"- foo
|
||||
-
|
||||
- bar", @"<document>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
<item />
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"1. foo
|
||||
2.
|
||||
3. bar", @"<document>
|
||||
<list type=""ordered"" start=""1"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
<item />
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"*", @"<document>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item />
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"foo
|
||||
*
|
||||
|
||||
foo
|
||||
1.", @"<document>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
<softbreak />
|
||||
<text>*</text>
|
||||
</paragraph>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
<softbreak />
|
||||
<text>1.</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@" 1. A paragraph
|
||||
with two lines.
|
||||
|
||||
indented code
|
||||
|
||||
> A block quote.", @"<document>
|
||||
<list type=""ordered"" start=""1"" tight=""false"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>A paragraph</text>
|
||||
<softbreak />
|
||||
<text>with two lines.</text>
|
||||
</paragraph>
|
||||
<code_block>indented code
|
||||
</code_block>
|
||||
<block_quote>
|
||||
<paragraph>
|
||||
<text>A block quote.</text>
|
||||
</paragraph>
|
||||
</block_quote>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@" 1. A paragraph
|
||||
with two lines.
|
||||
|
||||
indented code
|
||||
|
||||
> A block quote.", @"<document>
|
||||
<list type=""ordered"" start=""1"" tight=""false"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>A paragraph</text>
|
||||
<softbreak />
|
||||
<text>with two lines.</text>
|
||||
</paragraph>
|
||||
<code_block>indented code
|
||||
</code_block>
|
||||
<block_quote>
|
||||
<paragraph>
|
||||
<text>A block quote.</text>
|
||||
</paragraph>
|
||||
</block_quote>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@" 1. A paragraph
|
||||
with two lines.
|
||||
|
||||
indented code
|
||||
|
||||
> A block quote.", @"<document>
|
||||
<list type=""ordered"" start=""1"" tight=""false"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>A paragraph</text>
|
||||
<softbreak />
|
||||
<text>with two lines.</text>
|
||||
</paragraph>
|
||||
<code_block>indented code
|
||||
</code_block>
|
||||
<block_quote>
|
||||
<paragraph>
|
||||
<text>A block quote.</text>
|
||||
</paragraph>
|
||||
</block_quote>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@" 1. A paragraph
|
||||
with two lines.
|
||||
|
||||
indented code
|
||||
|
||||
> A block quote.", @"<document>
|
||||
<code_block>1. A paragraph
|
||||
with two lines.
|
||||
|
||||
indented code
|
||||
|
||||
> A block quote.
|
||||
</code_block>
|
||||
</document>")]
|
||||
public void Parse_ListItem(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@" 1. A paragraph
|
||||
with two lines.
|
||||
|
||||
indented code
|
||||
|
||||
> A block quote.", @"<document>
|
||||
<list type=""ordered"" start=""1"" tight=""false"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>A paragraph</text>
|
||||
<softbreak />
|
||||
<text>with two lines.</text>
|
||||
</paragraph>
|
||||
<paragraph>
|
||||
<text>indented code</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
<code_block> > A block quote.
|
||||
</code_block>
|
||||
</document>")]
|
||||
[InlineData(@" 1. A paragraph
|
||||
with two lines.", @"<document>
|
||||
<list type=""ordered"" start=""1"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>A paragraph</text>
|
||||
<softbreak />
|
||||
<text>with two lines.</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"> 1. > Blockquote
|
||||
continued here.", @"<document>
|
||||
<block_quote>
|
||||
<list type=""ordered"" start=""1"" tight=""true"">
|
||||
<item>
|
||||
<block_quote>
|
||||
<paragraph>
|
||||
<text>Blockquote</text>
|
||||
<softbreak />
|
||||
<text>continued here.</text>
|
||||
</paragraph>
|
||||
</block_quote>
|
||||
</item>
|
||||
</list>
|
||||
</block_quote>
|
||||
</document>")]
|
||||
[InlineData(@"> 1. > Blockquote
|
||||
> continued here.", @"<document>
|
||||
<block_quote>
|
||||
<list type=""ordered"" start=""1"" tight=""true"">
|
||||
<item>
|
||||
<block_quote>
|
||||
<paragraph>
|
||||
<text>Blockquote</text>
|
||||
<softbreak />
|
||||
<text>continued here.</text>
|
||||
</paragraph>
|
||||
</block_quote>
|
||||
</item>
|
||||
</list>
|
||||
</block_quote>
|
||||
</document>")]
|
||||
public void Parse_ListItem_WithLazyContinuation(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"- foo
|
||||
- bar
|
||||
- baz
|
||||
- boo", @"<document>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>baz</text>
|
||||
</paragraph>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>boo</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</item>
|
||||
</list>
|
||||
</item>
|
||||
</list>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"- foo
|
||||
- bar
|
||||
- baz
|
||||
- boo", @"<document>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>baz</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>boo</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"1. foo
|
||||
1. bar
|
||||
1. baz
|
||||
1. boo", @"<document>
|
||||
<list type=""ordered"" start=""1"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>baz</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>boo</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"
|
||||
1. foo
|
||||
1. bar
|
||||
1. baz
|
||||
1. boo", @"<document>
|
||||
<list type=""ordered"" start=""1"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
<list type=""ordered"" start=""1"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
<list type=""ordered"" start=""1"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>baz</text>
|
||||
</paragraph>
|
||||
<list type=""ordered"" start=""1"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>boo</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</item>
|
||||
</list>
|
||||
</item>
|
||||
</list>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"10) foo
|
||||
- bar", @"<document>
|
||||
<list type=""ordered"" start=""10"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"
|
||||
10) foo
|
||||
- bar", @"<document>
|
||||
<list type=""ordered"" start=""10"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"- - foo", @"<document>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"1. - foo", @"<document>
|
||||
<list type=""ordered"" start=""1"" tight=""true"">
|
||||
<item>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"1. 1. foo", @"<document>
|
||||
<list type=""ordered"" start=""1"" tight=""true"">
|
||||
<item>
|
||||
<list type=""ordered"" start=""1"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"- 1. foo", @"<document>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<list type=""ordered"" start=""1"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"1. - 2. foo", @"<document>
|
||||
<list type=""ordered"" start=""1"" tight=""true"">
|
||||
<item>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<list type=""ordered"" start=""2"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</item>
|
||||
</list>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"- # Foo
|
||||
- Bar
|
||||
---
|
||||
baz", @"<document>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<heading level=""1"">
|
||||
<text>Foo</text>
|
||||
</heading>
|
||||
</item>
|
||||
<item>
|
||||
<heading level=""2"">
|
||||
<text>Bar</text>
|
||||
</heading>
|
||||
<paragraph>
|
||||
<text>baz</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
public void Parse_ListItem_WithNesting(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
}
|
||||
693
Radzen.Blazor.Tests/Markdown/ListTests.cs
Normal file
693
Radzen.Blazor.Tests/Markdown/ListTests.cs
Normal file
@@ -0,0 +1,693 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Markdown.Tests;
|
||||
|
||||
public class ListTests
|
||||
{
|
||||
private static string ToXml(string markdown)
|
||||
{
|
||||
var document = MarkdownParser.Parse(markdown);
|
||||
|
||||
return XmlVisitor.ToXml(document);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"- foo
|
||||
- bar
|
||||
+ baz", @"<document>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>baz</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"1. foo
|
||||
2. bar
|
||||
3) baz", @"<document>
|
||||
<list type=""ordered"" start=""1"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
<list type=""ordered"" start=""3"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>baz</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"Foo
|
||||
- bar
|
||||
- baz", @"<document>
|
||||
<paragraph>
|
||||
<text>Foo</text>
|
||||
</paragraph>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>baz</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
public void Parse_List(string markdown, string expected)
|
||||
{
|
||||
var actual = ToXml(markdown);
|
||||
|
||||
Assert.Equal(expected, actual);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"The number of windows in my house is
|
||||
14. The number of doors is 6.", @"<document>
|
||||
<paragraph>
|
||||
<text>The number of windows in my house is</text>
|
||||
<softbreak />
|
||||
<text>14. The number of doors is 6.</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"The number of windows in my house is
|
||||
1. The number of doors is 6.", @"<document>
|
||||
<paragraph>
|
||||
<text>The number of windows in my house is</text>
|
||||
</paragraph>
|
||||
<list type=""ordered"" start=""1"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>The number of doors is 6.</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
public void Parse_OnlyNumberedListsThatStartWithOneCanInterruptParagraphs(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"- foo
|
||||
|
||||
- bar
|
||||
|
||||
|
||||
- baz", @"<document>
|
||||
<list type=""bullet"" tight=""false"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>baz</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"- foo
|
||||
- bar
|
||||
- baz
|
||||
|
||||
|
||||
bim
|
||||
", @"<document>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
<list type=""bullet"" tight=""false"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>baz</text>
|
||||
</paragraph>
|
||||
<paragraph>
|
||||
<text>bim</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</item>
|
||||
</list>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"- a
|
||||
- b
|
||||
|
||||
- c", @"<document>
|
||||
<list type=""bullet"" tight=""false"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>a</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>b</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>c</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"* a
|
||||
*
|
||||
|
||||
* c", @"<document>
|
||||
<list type=""bullet"" tight=""false"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>a</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
<item />
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>c</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"- a
|
||||
- b
|
||||
|
||||
c
|
||||
- d", @"<document>
|
||||
<list type=""bullet"" tight=""false"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>a</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>b</text>
|
||||
</paragraph>
|
||||
<paragraph>
|
||||
<text>c</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>d</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"- a
|
||||
- b
|
||||
|
||||
[ref]: /url
|
||||
- d", @"<document>
|
||||
<list type=""bullet"" tight=""false"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>a</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>b</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>d</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"- a
|
||||
- ```
|
||||
b
|
||||
|
||||
|
||||
```
|
||||
- c", @"<document>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>a</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
<item>
|
||||
<code_block>b
|
||||
|
||||
|
||||
</code_block>
|
||||
</item>
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>c</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"- a
|
||||
- b
|
||||
|
||||
c
|
||||
- d", @"<document>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>a</text>
|
||||
</paragraph>
|
||||
<list type=""bullet"" tight=""false"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>b</text>
|
||||
</paragraph>
|
||||
<paragraph>
|
||||
<text>c</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</item>
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>d</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"* a
|
||||
> b
|
||||
>
|
||||
* c", @"<document>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>a</text>
|
||||
</paragraph>
|
||||
<block_quote>
|
||||
<paragraph>
|
||||
<text>b</text>
|
||||
</paragraph>
|
||||
</block_quote>
|
||||
</item>
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>c</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"- a
|
||||
> b
|
||||
```
|
||||
c
|
||||
```
|
||||
- d
|
||||
", @"<document>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>a</text>
|
||||
</paragraph>
|
||||
<block_quote>
|
||||
<paragraph>
|
||||
<text>b</text>
|
||||
</paragraph>
|
||||
</block_quote>
|
||||
<code_block>c
|
||||
</code_block>
|
||||
</item>
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>d</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"- a", @"<document>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>a</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"- a
|
||||
- b", @"<document>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>a</text>
|
||||
</paragraph>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>b</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"1. ```
|
||||
foo
|
||||
```
|
||||
|
||||
bar", @"<document>
|
||||
<list type=""ordered"" start=""1"" tight=""false"">
|
||||
<item>
|
||||
<code_block>foo
|
||||
</code_block>
|
||||
<paragraph>
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"* foo
|
||||
* bar
|
||||
|
||||
baz", @"<document>
|
||||
<list type=""bullet"" tight=""false"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
<paragraph>
|
||||
<text>baz</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"- a
|
||||
- b
|
||||
- c
|
||||
|
||||
- d
|
||||
- e
|
||||
- f", @"<document>
|
||||
<list type=""bullet"" tight=""false"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>a</text>
|
||||
</paragraph>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>b</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>c</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</item>
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>d</text>
|
||||
</paragraph>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>e</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>f</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
public void Parse_TightAndLooseLists(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"- foo
|
||||
- bar
|
||||
|
||||
<!-- -->
|
||||
|
||||
- baz
|
||||
- bim", @"<document>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
<html_block><!-- --></html_block>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>baz</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>bim</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"- foo
|
||||
|
||||
notcode
|
||||
|
||||
- foo
|
||||
|
||||
<!-- -->
|
||||
|
||||
code", @"<document>
|
||||
<list type=""bullet"" tight=""false"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
<paragraph>
|
||||
<text>notcode</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
<html_block><!-- --></html_block>
|
||||
<code_block>code
|
||||
</code_block>
|
||||
</document>")]
|
||||
public void Parse_List_Separators(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"- a
|
||||
- b
|
||||
- c
|
||||
- d
|
||||
- e
|
||||
- f
|
||||
- g", @"<document>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>a</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>b</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>c</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>d</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>e</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>f</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>g</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"1. a
|
||||
|
||||
2. b
|
||||
|
||||
3. c
|
||||
", @"<document>
|
||||
<list type=""ordered"" start=""1"" tight=""false"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>a</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>b</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>c</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"- a
|
||||
- b
|
||||
- c
|
||||
- d
|
||||
- e", @"<document>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>a</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>b</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>c</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>d</text>
|
||||
<softbreak />
|
||||
<text>- e</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"1. a
|
||||
|
||||
2. b
|
||||
|
||||
3. c", @"<document>
|
||||
<list type=""ordered"" start=""1"" tight=""false"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>a</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>b</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
<code_block>3. c
|
||||
</code_block>
|
||||
</document>")]
|
||||
public void Parse_List_Identation(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_BasicNestedLists()
|
||||
{
|
||||
Assert.Equal(@"<document>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>a</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>b</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>c</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>", ToXml(@"
|
||||
- a
|
||||
- b
|
||||
- c"));
|
||||
}
|
||||
}
|
||||
109
Radzen.Blazor.Tests/Markdown/ParagraphTests.cs
Normal file
109
Radzen.Blazor.Tests/Markdown/ParagraphTests.cs
Normal file
@@ -0,0 +1,109 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Markdown.Tests;
|
||||
|
||||
public class ParagraphTests
|
||||
{
|
||||
private static string ToXml(string markdown)
|
||||
{
|
||||
var document = MarkdownParser.Parse(markdown);
|
||||
|
||||
return XmlVisitor.ToXml(document);
|
||||
}
|
||||
[Fact]
|
||||
public void Parse_BasicParagraph()
|
||||
{
|
||||
Assert.Equal(@"<document>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
</document>", ToXml(@"foo"));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"aaa
|
||||
|
||||
bbb", @"<document>
|
||||
<paragraph>
|
||||
<text>aaa</text>
|
||||
</paragraph>
|
||||
<paragraph>
|
||||
<text>bbb</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"aaa
|
||||
bbb
|
||||
|
||||
ccc
|
||||
ddd", @"<document>
|
||||
<paragraph>
|
||||
<text>aaa</text>
|
||||
<softbreak />
|
||||
<text>bbb</text>
|
||||
</paragraph>
|
||||
<paragraph>
|
||||
<text>ccc</text>
|
||||
<softbreak />
|
||||
<text>ddd</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"aaa
|
||||
|
||||
|
||||
bbb
|
||||
", @"<document>
|
||||
<paragraph>
|
||||
<text>aaa</text>
|
||||
</paragraph>
|
||||
<paragraph>
|
||||
<text>bbb</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@" aaa
|
||||
bbb", @"<document>
|
||||
<paragraph>
|
||||
<text>aaa</text>
|
||||
<softbreak />
|
||||
<text>bbb</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"aaa
|
||||
bbb
|
||||
ccc", @"<document>
|
||||
<paragraph>
|
||||
<text>aaa</text>
|
||||
<softbreak />
|
||||
<text>bbb</text>
|
||||
<softbreak />
|
||||
<text>ccc</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@" aaa
|
||||
bbb", @"<document>
|
||||
<paragraph>
|
||||
<text>aaa</text>
|
||||
<softbreak />
|
||||
<text>bbb</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@" aaa
|
||||
bbb", @"<document>
|
||||
<code_block>aaa
|
||||
</code_block>
|
||||
<paragraph>
|
||||
<text>bbb</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"aaa
|
||||
bbb ", @"<document>
|
||||
<paragraph>
|
||||
<text>aaa</text>
|
||||
<linebreak />
|
||||
<text>bbb</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
public void Parse_Paragraph(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
}
|
||||
41
Radzen.Blazor.Tests/Markdown/SoftLineBreakTests.cs
Normal file
41
Radzen.Blazor.Tests/Markdown/SoftLineBreakTests.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Markdown.Tests;
|
||||
|
||||
public class SoftLineBreakTests
|
||||
{
|
||||
private static string ToXml(string markdown)
|
||||
{
|
||||
var document = MarkdownParser.Parse(markdown);
|
||||
|
||||
return XmlVisitor.ToXml(document);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"foo
|
||||
baz", @"<document>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
<softbreak />
|
||||
<text>baz</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
public void Parse_SoftLineBreak(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"foo
|
||||
baz", @"<document>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
<softbreak />
|
||||
<text>baz</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
public void Parse_SoftLine_RemovesSpacesAtEndAndStartOfLine(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
}
|
||||
135
Radzen.Blazor.Tests/Markdown/StrongTests.cs
Normal file
135
Radzen.Blazor.Tests/Markdown/StrongTests.cs
Normal file
@@ -0,0 +1,135 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Markdown.Tests;
|
||||
|
||||
public class StrongTests
|
||||
{
|
||||
private static string ToXml(string markdown)
|
||||
{
|
||||
var document = MarkdownParser.Parse(markdown);
|
||||
return XmlVisitor.ToXml(document);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("**foo bar**",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<strong>
|
||||
<text>foo bar</text>
|
||||
</strong>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("** foo bar**",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<text>**</text>
|
||||
<text> foo bar</text>
|
||||
<text>**</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("a**\"foo\"**",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<text>a</text>
|
||||
<text>**</text>
|
||||
<text>""foo""</text>
|
||||
<text>**</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("foo**bar**",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
<strong>
|
||||
<text>bar</text>
|
||||
</strong>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("__foo bar__",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<strong>
|
||||
<text>foo bar</text>
|
||||
</strong>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("__ foo bar__",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<text>__</text>
|
||||
<text> foo bar</text>
|
||||
<text>__</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("__\nfoo bar__",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<text>__</text>
|
||||
<softbreak />
|
||||
<text>foo bar</text>
|
||||
<text>__</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("a__\"foo\"__",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<text>a</text>
|
||||
<text>__</text>
|
||||
<text>""foo""</text>
|
||||
<text>__</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("foo__bar__",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
<text>__</text>
|
||||
<text>bar</text>
|
||||
<text>__</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("5__6__78",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<text>5</text>
|
||||
<text>__</text>
|
||||
<text>6</text>
|
||||
<text>__</text>
|
||||
<text>78</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("пристаням__стремятся__",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<text>пристаням</text>
|
||||
<text>__</text>
|
||||
<text>стремятся</text>
|
||||
<text>__</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("__foo, __bar__, baz__",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<strong>
|
||||
<text>foo, </text>
|
||||
<strong>
|
||||
<text>bar</text>
|
||||
</strong>
|
||||
<text>, baz</text>
|
||||
</strong>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("foo-__(bar)__",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<text>foo-</text>
|
||||
<strong>
|
||||
<text>(bar)</text>
|
||||
</strong>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
public void Parse_StrongEmphasisRules_AdheresToCommonMarkSpec(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
}
|
||||
414
Radzen.Blazor.Tests/Markdown/TableTests.cs
Normal file
414
Radzen.Blazor.Tests/Markdown/TableTests.cs
Normal file
@@ -0,0 +1,414 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Markdown.Tests;
|
||||
|
||||
public class TableTests
|
||||
{
|
||||
private static string ToXml(string markdown)
|
||||
{
|
||||
var document = MarkdownParser.Parse(markdown);
|
||||
|
||||
return XmlVisitor.ToXml(document);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_BasicTable()
|
||||
{
|
||||
Assert.Equal(
|
||||
@"<document>
|
||||
<table>
|
||||
<header>
|
||||
<cell>
|
||||
<text>foo</text>
|
||||
</cell>
|
||||
<cell>
|
||||
<text>bar</text>
|
||||
</cell>
|
||||
</header>
|
||||
<row>
|
||||
<cell>
|
||||
<text>baz</text>
|
||||
</cell>
|
||||
<cell>
|
||||
<text>bim</text>
|
||||
</cell>
|
||||
</row>
|
||||
</table>
|
||||
</document>",
|
||||
ToXml(@"
|
||||
foo|bar
|
||||
--|--
|
||||
baz|bim"));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"| foo | bar |
|
||||
| --- | --- |
|
||||
| baz | bim |", @"<document>
|
||||
<table>
|
||||
<header>
|
||||
<cell>
|
||||
<text>foo</text>
|
||||
</cell>
|
||||
<cell>
|
||||
<text>bar</text>
|
||||
</cell>
|
||||
</header>
|
||||
<row>
|
||||
<cell>
|
||||
<text>baz</text>
|
||||
</cell>
|
||||
<cell>
|
||||
<text>bim</text>
|
||||
</cell>
|
||||
</row>
|
||||
</table>
|
||||
</document>")]
|
||||
[InlineData(@"| f\|oo |
|
||||
| ------ |
|
||||
| b `\|` az |
|
||||
| b **\|** im |", @"<document>
|
||||
<table>
|
||||
<header>
|
||||
<cell>
|
||||
<text>f|oo</text>
|
||||
</cell>
|
||||
</header>
|
||||
<row>
|
||||
<cell>
|
||||
<text>b </text>
|
||||
<code>|</code>
|
||||
<text> az</text>
|
||||
</cell>
|
||||
</row>
|
||||
<row>
|
||||
<cell>
|
||||
<text>b </text>
|
||||
<strong>
|
||||
<text>|</text>
|
||||
</strong>
|
||||
<text> im</text>
|
||||
</cell>
|
||||
</row>
|
||||
</table>
|
||||
</document>")]
|
||||
[InlineData(@"| abc | defghi |
|
||||
:-: | -----------:
|
||||
bar | baz", @"<document>
|
||||
<table>
|
||||
<header>
|
||||
<cell align=""center"">
|
||||
<text>abc</text>
|
||||
</cell>
|
||||
<cell align=""right"">
|
||||
<text>defghi</text>
|
||||
</cell>
|
||||
</header>
|
||||
<row>
|
||||
<cell align=""center"">
|
||||
<text>bar</text>
|
||||
</cell>
|
||||
<cell align=""right"">
|
||||
<text>baz</text>
|
||||
</cell>
|
||||
</row>
|
||||
</table>
|
||||
</document>")]
|
||||
|
||||
|
||||
[InlineData(@"| abc | def |
|
||||
| --- | --- |
|
||||
bar", @"<document>
|
||||
<table>
|
||||
<header>
|
||||
<cell>
|
||||
<text>abc</text>
|
||||
</cell>
|
||||
<cell>
|
||||
<text>def</text>
|
||||
</cell>
|
||||
</header>
|
||||
<row>
|
||||
<cell>
|
||||
<text>bar</text>
|
||||
</cell>
|
||||
<cell />
|
||||
</row>
|
||||
</table>
|
||||
</document>")]
|
||||
[InlineData(@"| abc | def |
|
||||
| --- | --- |
|
||||
c:\\foo", @"<document>
|
||||
<table>
|
||||
<header>
|
||||
<cell>
|
||||
<text>abc</text>
|
||||
</cell>
|
||||
<cell>
|
||||
<text>def</text>
|
||||
</cell>
|
||||
</header>
|
||||
<row>
|
||||
<cell>
|
||||
<text>c:</text>
|
||||
<text>\</text>
|
||||
<text>foo</text>
|
||||
</cell>
|
||||
<cell />
|
||||
</row>
|
||||
</table>
|
||||
</document>")]
|
||||
public void Parse_Table(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"| abc | def |
|
||||
| --- | --- |
|
||||
| bar | baz |
|
||||
|
||||
boo", @"<document>
|
||||
<table>
|
||||
<header>
|
||||
<cell>
|
||||
<text>abc</text>
|
||||
</cell>
|
||||
<cell>
|
||||
<text>def</text>
|
||||
</cell>
|
||||
</header>
|
||||
<row>
|
||||
<cell>
|
||||
<text>bar</text>
|
||||
</cell>
|
||||
<cell>
|
||||
<text>baz</text>
|
||||
</cell>
|
||||
</row>
|
||||
</table>
|
||||
<paragraph>
|
||||
<text>boo</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"| foo |
|
||||
| --- |
|
||||
# bar", @"<document>
|
||||
<table>
|
||||
<header>
|
||||
<cell>
|
||||
<text>foo</text>
|
||||
</cell>
|
||||
</header>
|
||||
</table>
|
||||
<heading level=""1"">
|
||||
<text>bar</text>
|
||||
</heading>
|
||||
</document>")]
|
||||
[InlineData(@"| foo |
|
||||
| --- |
|
||||
- bar", @"<document>
|
||||
<table>
|
||||
<header>
|
||||
<cell>
|
||||
<text>foo</text>
|
||||
</cell>
|
||||
</header>
|
||||
</table>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"| foo |
|
||||
| --- |
|
||||
1. bar", @"<document>
|
||||
<table>
|
||||
<header>
|
||||
<cell>
|
||||
<text>foo</text>
|
||||
</cell>
|
||||
</header>
|
||||
</table>
|
||||
<list type=""ordered"" start=""1"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"| abc | def |
|
||||
| --- | --- |
|
||||
| bar | baz |
|
||||
> bar", @"<document>
|
||||
<table>
|
||||
<header>
|
||||
<cell>
|
||||
<text>abc</text>
|
||||
</cell>
|
||||
<cell>
|
||||
<text>def</text>
|
||||
</cell>
|
||||
</header>
|
||||
<row>
|
||||
<cell>
|
||||
<text>bar</text>
|
||||
</cell>
|
||||
<cell>
|
||||
<text>baz</text>
|
||||
</cell>
|
||||
</row>
|
||||
</table>
|
||||
<block_quote>
|
||||
<paragraph>
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
</block_quote>
|
||||
</document>")]
|
||||
[InlineData(@"| abc | def |
|
||||
| --- | --- |
|
||||
| bar | baz |
|
||||
bar", @"<document>
|
||||
<table>
|
||||
<header>
|
||||
<cell>
|
||||
<text>abc</text>
|
||||
</cell>
|
||||
<cell>
|
||||
<text>def</text>
|
||||
</cell>
|
||||
</header>
|
||||
<row>
|
||||
<cell>
|
||||
<text>bar</text>
|
||||
</cell>
|
||||
<cell>
|
||||
<text>baz</text>
|
||||
</cell>
|
||||
</row>
|
||||
</table>
|
||||
<code_block>bar
|
||||
</code_block>
|
||||
</document>")]
|
||||
[InlineData(@"| abc | def |
|
||||
| --- | --- |
|
||||
| bar | baz |
|
||||
```
|
||||
bar
|
||||
```", @"<document>
|
||||
<table>
|
||||
<header>
|
||||
<cell>
|
||||
<text>abc</text>
|
||||
</cell>
|
||||
<cell>
|
||||
<text>def</text>
|
||||
</cell>
|
||||
</header>
|
||||
<row>
|
||||
<cell>
|
||||
<text>bar</text>
|
||||
</cell>
|
||||
<cell>
|
||||
<text>baz</text>
|
||||
</cell>
|
||||
</row>
|
||||
</table>
|
||||
<code_block>bar
|
||||
</code_block>
|
||||
</document>")]
|
||||
[InlineData(@"| abc | def |
|
||||
| --- | --- |
|
||||
| bar | baz |
|
||||
<div>
|
||||
</div>", @"<document>
|
||||
<table>
|
||||
<header>
|
||||
<cell>
|
||||
<text>abc</text>
|
||||
</cell>
|
||||
<cell>
|
||||
<text>def</text>
|
||||
</cell>
|
||||
</header>
|
||||
<row>
|
||||
<cell>
|
||||
<text>bar</text>
|
||||
</cell>
|
||||
<cell>
|
||||
<text>baz</text>
|
||||
</cell>
|
||||
</row>
|
||||
</table>
|
||||
<html_block><div>
|
||||
</div></html_block>
|
||||
</document>")]
|
||||
[InlineData(@"| abc | def |
|
||||
| --- | --- |
|
||||
| bar | baz |
|
||||
---", @"<document>
|
||||
<table>
|
||||
<header>
|
||||
<cell>
|
||||
<text>abc</text>
|
||||
</cell>
|
||||
<cell>
|
||||
<text>def</text>
|
||||
</cell>
|
||||
</header>
|
||||
<row>
|
||||
<cell>
|
||||
<text>bar</text>
|
||||
</cell>
|
||||
<cell>
|
||||
<text>baz</text>
|
||||
</cell>
|
||||
</row>
|
||||
</table>
|
||||
<thematic_break />
|
||||
</document>")]
|
||||
public void Parse_Table_AnyBlockOrEmptyLineBreaksTable(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"| abc | def |
|
||||
| --- |", @"<document>
|
||||
<paragraph>
|
||||
<text>| abc | def |</text>
|
||||
<softbreak />
|
||||
<text>| --- |</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
public void Parse_Table_ChecksHeaderAndDelimiter(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"| abc |
|
||||
| --- |
|
||||
| bar | baz |", @"<document>
|
||||
<table>
|
||||
<header>
|
||||
<cell>
|
||||
<text>abc</text>
|
||||
</cell>
|
||||
</header>
|
||||
<row>
|
||||
<cell>
|
||||
<text>bar</text>
|
||||
</cell>
|
||||
</row>
|
||||
</table>
|
||||
</document>")]
|
||||
public void Parse_Table_IgnoresExtraCells(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
}
|
||||
162
Radzen.Blazor.Tests/Markdown/ThematicBreakTests.cs
Normal file
162
Radzen.Blazor.Tests/Markdown/ThematicBreakTests.cs
Normal file
@@ -0,0 +1,162 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Markdown.Tests;
|
||||
|
||||
public class ThematicBreakTests
|
||||
{
|
||||
private static string ToXml(string markdown)
|
||||
{
|
||||
var document = MarkdownParser.Parse(markdown);
|
||||
|
||||
return XmlVisitor.ToXml(document);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"***
|
||||
---
|
||||
___", @"<document>
|
||||
<thematic_break />
|
||||
<thematic_break />
|
||||
<thematic_break />
|
||||
</document>")]
|
||||
[InlineData(@"--
|
||||
**
|
||||
__", @"<document>
|
||||
<paragraph>
|
||||
<text>--</text>
|
||||
<softbreak />
|
||||
<text>**</text>
|
||||
<softbreak />
|
||||
<text>__</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@" ***
|
||||
***
|
||||
***", @"<document>
|
||||
<thematic_break />
|
||||
<thematic_break />
|
||||
<thematic_break />
|
||||
</document>")]
|
||||
[InlineData(@" ***", @"<document>
|
||||
<code_block>***
|
||||
</code_block>
|
||||
</document>")]
|
||||
[InlineData(@"Foo
|
||||
***", @"<document>
|
||||
<paragraph>
|
||||
<text>Foo</text>
|
||||
<softbreak />
|
||||
<text>***</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"_____________________________________", @"<document>
|
||||
<thematic_break />
|
||||
</document>")]
|
||||
[InlineData(@"- - -", @"<document>
|
||||
<thematic_break />
|
||||
</document>")]
|
||||
[InlineData(@" ** * ** * ** * **", @"<document>
|
||||
<thematic_break />
|
||||
</document>")]
|
||||
[InlineData(@"- - - -", @"<document>
|
||||
<thematic_break />
|
||||
</document>")]
|
||||
[InlineData(@"- - - - ", @"<document>
|
||||
<thematic_break />
|
||||
</document>")]
|
||||
[InlineData(@"_ _ _ _ a
|
||||
|
||||
a------
|
||||
|
||||
---a---", @"<document>
|
||||
<paragraph>
|
||||
<text>_</text>
|
||||
<text> </text>
|
||||
<text>_</text>
|
||||
<text> </text>
|
||||
<text>_</text>
|
||||
<text> </text>
|
||||
<text>_</text>
|
||||
<text> a</text>
|
||||
</paragraph>
|
||||
<paragraph>
|
||||
<text>a------</text>
|
||||
</paragraph>
|
||||
<paragraph>
|
||||
<text>---a---</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@" *-*", @"<document>
|
||||
<paragraph>
|
||||
<emph>
|
||||
<text>-</text>
|
||||
</emph>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"- foo
|
||||
***
|
||||
- bar", @"<document>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
<thematic_break />
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"Foo
|
||||
***
|
||||
bar", @"<document>
|
||||
<paragraph>
|
||||
<text>Foo</text>
|
||||
</paragraph>
|
||||
<thematic_break />
|
||||
<paragraph>
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"* Foo
|
||||
* * *
|
||||
* Bar", @"<document>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>Foo</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
<thematic_break />
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>Bar</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"- Foo
|
||||
- * * *", @"<document>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>Foo</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
<item>
|
||||
<thematic_break />
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
public void Parse_ThematicBreak(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
}
|
||||
210
Radzen.Blazor.Tests/Markdown/XmlVisitor.cs
Normal file
210
Radzen.Blazor.Tests/Markdown/XmlVisitor.cs
Normal file
@@ -0,0 +1,210 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
using System.Xml;
|
||||
|
||||
namespace Radzen.Blazor.Markdown.Tests;
|
||||
|
||||
public class XmlVisitor : NodeVisitorBase, IDisposable
|
||||
{
|
||||
private readonly XmlWriter writer;
|
||||
|
||||
private XmlVisitor(StringBuilder xml)
|
||||
{
|
||||
writer = XmlWriter.Create(xml, new XmlWriterSettings { OmitXmlDeclaration = true, Indent = true, IndentChars = " ", });
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
writer.Dispose();
|
||||
}
|
||||
|
||||
public void Close()
|
||||
{
|
||||
writer.Close();
|
||||
}
|
||||
|
||||
public static string ToXml(Document document)
|
||||
{
|
||||
var xml = new StringBuilder();
|
||||
|
||||
using var visitor = new XmlVisitor(xml);
|
||||
|
||||
document.Accept(visitor);
|
||||
|
||||
visitor.Close();
|
||||
|
||||
return xml.ToString()!;
|
||||
}
|
||||
|
||||
public override void VisitBlockQuote(BlockQuote blockQuote)
|
||||
{
|
||||
writer.WriteStartElement("block_quote");
|
||||
base.VisitBlockQuote(blockQuote);
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
|
||||
public override void VisitDocument(Document document)
|
||||
{
|
||||
writer.WriteStartDocument();
|
||||
writer.WriteStartElement("document");
|
||||
base.VisitDocument(document);
|
||||
writer.WriteEndElement();
|
||||
writer.WriteEndDocument();
|
||||
}
|
||||
|
||||
public override void VisitHeading(Heading heading)
|
||||
{
|
||||
writer.WriteStartElement($"heading");
|
||||
writer.WriteAttributeString("level", heading.Level.ToString());
|
||||
base.VisitHeading(heading);
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
|
||||
public override void VisitListItem(ListItem listItem)
|
||||
{
|
||||
writer.WriteStartElement("item");
|
||||
base.VisitListItem(listItem);
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
|
||||
public override void VisitParagraph(Paragraph paragraph)
|
||||
{
|
||||
writer.WriteStartElement("paragraph");
|
||||
base.VisitParagraph(paragraph);
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
|
||||
public override void VisitUnorderedList(UnorderedList unorderedList)
|
||||
{
|
||||
writer.WriteStartElement("list");
|
||||
writer.WriteAttributeString("type", "bullet");
|
||||
writer.WriteAttributeString("tight", unorderedList.Tight.ToString().ToLowerInvariant());
|
||||
base.VisitUnorderedList(unorderedList);
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
|
||||
public override void VisitText(Text text)
|
||||
{
|
||||
writer.WriteElementString("text", text.Value);
|
||||
}
|
||||
|
||||
public override void VisitOrderedList(OrderedList orderedList)
|
||||
{
|
||||
writer.WriteStartElement("list");
|
||||
writer.WriteAttributeString("type", "ordered");
|
||||
writer.WriteAttributeString("start", orderedList.Start.ToString());
|
||||
writer.WriteAttributeString("tight", orderedList.Tight.ToString().ToLowerInvariant());
|
||||
base.VisitOrderedList(orderedList);
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
|
||||
public override void VisitLink(Link link)
|
||||
{
|
||||
writer.WriteStartElement("link");
|
||||
writer.WriteAttributeString("destination", link.Destination);
|
||||
writer.WriteAttributeString("title", link.Title);
|
||||
base.VisitLink(link);
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
|
||||
public override void VisitImage(Image image)
|
||||
{
|
||||
writer.WriteStartElement("image");
|
||||
writer.WriteAttributeString("destination", image.Destination);
|
||||
writer.WriteAttributeString("title", image.Title);
|
||||
base.VisitImage(image);
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
|
||||
public override void VisitEmphasis(Emphasis emphasis)
|
||||
{
|
||||
writer.WriteStartElement("emph");
|
||||
base.VisitEmphasis(emphasis);
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
|
||||
public override void VisitStrong(Strong strong)
|
||||
{
|
||||
writer.WriteStartElement("strong");
|
||||
base.VisitStrong(strong);
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
|
||||
public override void VisitCode(Code code)
|
||||
{
|
||||
writer.WriteElementString("code", code.Value);
|
||||
}
|
||||
|
||||
public override void VisitHtmlInline(HtmlInline html)
|
||||
{
|
||||
writer.WriteElementString("html_inline", html.Value);
|
||||
}
|
||||
|
||||
public override void VisitLineBreak(LineBreak lineBreak)
|
||||
{
|
||||
writer.WriteElementString("linebreak", string.Empty);
|
||||
}
|
||||
|
||||
public override void VisitSoftLineBreak(SoftLineBreak softLineBreak)
|
||||
{
|
||||
writer.WriteElementString("softbreak", string.Empty);
|
||||
}
|
||||
|
||||
public override void VisitThematicBreak(ThematicBreak thematicBreak)
|
||||
{
|
||||
writer.WriteElementString("thematic_break", string.Empty);
|
||||
}
|
||||
|
||||
public override void VisitIndentedCodeBlock(IndentedCodeBlock codeBlock)
|
||||
{
|
||||
writer.WriteElementString("code_block", codeBlock.Value);
|
||||
}
|
||||
|
||||
public override void VisitFencedCodeBlock(FencedCodeBlock fencedCodeBlock)
|
||||
{
|
||||
writer.WriteStartElement("code_block");
|
||||
if (!string.IsNullOrEmpty(fencedCodeBlock.Info))
|
||||
{
|
||||
writer.WriteAttributeString("info", fencedCodeBlock.Info);
|
||||
}
|
||||
writer.WriteString(fencedCodeBlock.Value);
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
|
||||
public override void VisitHtmlBlock(HtmlBlock htmlBlock)
|
||||
{
|
||||
writer.WriteElementString("html_block", htmlBlock.Value);
|
||||
}
|
||||
|
||||
public override void VisitTable(Table table)
|
||||
{
|
||||
writer.WriteStartElement("table");
|
||||
base.VisitTable(table);
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
|
||||
public override void VisitTableHeaderRow(TableHeaderRow header)
|
||||
{
|
||||
writer.WriteStartElement("header");
|
||||
base.VisitTableHeaderRow(header);
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
|
||||
public override void VisitTableRow(TableRow row)
|
||||
{
|
||||
writer.WriteStartElement("row");
|
||||
base.VisitTableRow(row);
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
|
||||
public override void VisitTableCell(TableCell cell)
|
||||
{
|
||||
writer.WriteStartElement("cell");
|
||||
if (cell.Alignment != TableCellAlignment.None)
|
||||
{
|
||||
writer.WriteAttributeString("align", cell.Alignment.ToString().ToLowerInvariant());
|
||||
}
|
||||
base.VisitTableCell(cell);
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
}
|
||||
@@ -119,13 +119,54 @@ namespace Radzen.Blazor.Tests
|
||||
|
||||
var component = ctx.RenderComponent<RadzenMask>();
|
||||
|
||||
component.SetParametersAndRender(parameters => parameters.Add<bool>(p => p.AutoComplete, false));
|
||||
component.SetParametersAndRender(parameters => parameters.AddUnmatched("AutoComplete", false));
|
||||
|
||||
Assert.Contains(@$"autocomplete=""off""", component.Markup);
|
||||
Assert.Contains(@$"aria-autocomplete=""none""", component.Markup);
|
||||
|
||||
component.SetParametersAndRender(parameters => parameters.AddUnmatched("AutoComplete", true));
|
||||
|
||||
Assert.Contains(@$"autocomplete=""on""", component.Markup);
|
||||
Assert.DoesNotContain(@$"aria-autocomplete", component.Markup);
|
||||
|
||||
component.SetParametersAndRender(parameters => parameters.AddUnmatched("autocomplete", "custom"));
|
||||
|
||||
Assert.Contains(@$"autocomplete=""custom""", component.Markup);
|
||||
Assert.DoesNotContain(@$"aria-autocomplete", component.Markup);
|
||||
|
||||
component.Instance.DefaultAutoCompleteAttribute = "autocomplete-custom";
|
||||
component.SetParametersAndRender(parameters => parameters.AddUnmatched("AutoComplete", false));
|
||||
|
||||
Assert.Contains(@$"autocomplete=""autocomplete-custom""", component.Markup);
|
||||
Assert.Contains(@$"aria-autocomplete=""none""", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Mask_Renders_TypedAutoCompleteParameter()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
|
||||
var component = ctx.RenderComponent<RadzenMask>();
|
||||
|
||||
component.SetParametersAndRender(parameters => parameters.AddUnmatched("AutoComplete", false));
|
||||
component.SetParametersAndRender(parameters => parameters.Add<AutoCompleteType>(p => p.AutoCompleteType, AutoCompleteType.On));
|
||||
|
||||
Assert.Contains(@$"autocomplete=""off""", component.Markup);
|
||||
|
||||
component.SetParametersAndRender(parameters => parameters.Add<bool>(p => p.AutoComplete, true));
|
||||
component.SetParametersAndRender(parameters => parameters.AddUnmatched("AutoComplete", true));
|
||||
component.SetParametersAndRender(parameters => parameters.Add<AutoCompleteType>(p => p.AutoCompleteType, AutoCompleteType.Off));
|
||||
|
||||
Assert.Contains(@$"autocomplete=""on""", component.Markup);
|
||||
Assert.Contains(@$"autocomplete=""off""", component.Markup);
|
||||
|
||||
component.SetParametersAndRender(parameters => parameters.AddUnmatched("AutoComplete", true));
|
||||
component.SetParametersAndRender(parameters => parameters.Add<AutoCompleteType>(p => p.AutoCompleteType, AutoCompleteType.AdditionalName));
|
||||
|
||||
Assert.Contains(@$"autocomplete=""{AutoCompleteType.AdditionalName.GetAutoCompleteValue()}""", component.Markup);
|
||||
|
||||
component.SetParametersAndRender(parameters => parameters.AddUnmatched("AutoComplete", true));
|
||||
component.SetParametersAndRender(parameters => parameters.Add<AutoCompleteType>(p => p.AutoCompleteType, AutoCompleteType.Email));
|
||||
|
||||
Assert.Contains(@$"autocomplete=""{AutoCompleteType.Email.GetAutoCompleteValue()}""", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
214
Radzen.Blazor.Tests/NotificationServiceTests.cs
Normal file
214
Radzen.Blazor.Tests/NotificationServiceTests.cs
Normal file
@@ -0,0 +1,214 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Tests
|
||||
{
|
||||
public class NotificationServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public void NotificationService_IsMessageIsNull_ExceptionExpected()
|
||||
{
|
||||
NotificationService notificationService = new NotificationService();
|
||||
NotificationMessage notificationMessage = null;
|
||||
|
||||
var exception = Record.Exception(() => notificationService.Notify(notificationMessage));
|
||||
|
||||
Assert.IsType<ArgumentNullException>(exception);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NotificationService_CheckAreTwoMessages_Equals()
|
||||
{
|
||||
var messageOne = new NotificationMessage()
|
||||
{
|
||||
Severity = NotificationSeverity.Info,
|
||||
Summary = "Info Summary",
|
||||
Detail = "Info Detail",
|
||||
Duration = 4000
|
||||
};
|
||||
|
||||
var messageTwo = new NotificationMessage()
|
||||
{
|
||||
Severity = NotificationSeverity.Info,
|
||||
Summary = "Info Summary",
|
||||
Detail = "Info Detail",
|
||||
Duration = 4000
|
||||
};
|
||||
|
||||
Assert.True(messageOne.Equals(messageTwo));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NotificationService_CheckAreTwoMessages_NotEquals()
|
||||
{
|
||||
var messageOne = new NotificationMessage()
|
||||
{
|
||||
Severity = NotificationSeverity.Info,
|
||||
Summary = "Info Summary",
|
||||
Detail = "Info Detail",
|
||||
Duration = 4000
|
||||
};
|
||||
|
||||
var messageTwo = new NotificationMessage()
|
||||
{
|
||||
Severity = NotificationSeverity.Info,
|
||||
Summary = "Info Summary Two",
|
||||
Detail = "Info Detail Two",
|
||||
Duration = 6000
|
||||
};
|
||||
|
||||
Assert.False(messageOne.Equals(messageTwo));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NotificationService_CheckAreTwoMessages_EqualsByReference()
|
||||
{
|
||||
var messageOne = new NotificationMessage()
|
||||
{
|
||||
Severity = NotificationSeverity.Info,
|
||||
Summary = "Info Summary",
|
||||
Detail = "Info Detail",
|
||||
Duration = 4000
|
||||
};
|
||||
|
||||
var messageTwo = messageOne;
|
||||
|
||||
Assert.True(messageOne.Equals(messageTwo));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NotificationService_CheckAreTwoMessages_EqualsByOperator()
|
||||
{
|
||||
var messageOne = new NotificationMessage()
|
||||
{
|
||||
Severity = NotificationSeverity.Info,
|
||||
Summary = "Info Summary",
|
||||
Detail = "Info Detail",
|
||||
Duration = 4000
|
||||
};
|
||||
|
||||
var messageTwo = new NotificationMessage()
|
||||
{
|
||||
Severity = NotificationSeverity.Info,
|
||||
Summary = "Info Summary",
|
||||
Detail = "Info Detail",
|
||||
Duration = 4000
|
||||
};
|
||||
|
||||
Assert.True(messageOne == messageTwo);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NotificationService_CheckAreTwoMessages_NotEqualsByOperator()
|
||||
{
|
||||
var messageOne = new NotificationMessage()
|
||||
{
|
||||
Severity = NotificationSeverity.Info,
|
||||
Summary = "Info Summary",
|
||||
Detail = "Info Detail",
|
||||
Duration = 4000
|
||||
};
|
||||
|
||||
var messageTwo = new NotificationMessage()
|
||||
{
|
||||
Severity = NotificationSeverity.Info,
|
||||
Summary = "Info Summary Two",
|
||||
Detail = "Info Detail Two",
|
||||
Duration = 6000
|
||||
};
|
||||
|
||||
Assert.True(messageOne != messageTwo);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NotificationService_CheckAreTwoMessages_EqualsByHashCode()
|
||||
{
|
||||
var messageOne = new NotificationMessage()
|
||||
{
|
||||
Severity = NotificationSeverity.Info,
|
||||
Summary = "Info Summary",
|
||||
Detail = "Info Detail",
|
||||
Duration = 4000
|
||||
};
|
||||
|
||||
var messageTwo = new NotificationMessage()
|
||||
{
|
||||
Severity = NotificationSeverity.Info,
|
||||
Summary = "Info Summary",
|
||||
Detail = "Info Detail",
|
||||
Duration = 4000
|
||||
};
|
||||
|
||||
var messageOneHashCode = messageOne.GetHashCode();
|
||||
var messageTwoHashCode = messageTwo.GetHashCode();
|
||||
|
||||
Assert.Equal(messageOneHashCode, messageTwoHashCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NotificationService_CheckAreTwoMessages_NotEqualsByHashCode()
|
||||
{
|
||||
var messageOne = new NotificationMessage()
|
||||
{
|
||||
Severity = NotificationSeverity.Info,
|
||||
Summary = "Info Summary",
|
||||
Detail = "Info Detail",
|
||||
Duration = 4000
|
||||
};
|
||||
|
||||
var messageTwo = new NotificationMessage()
|
||||
{
|
||||
Severity = NotificationSeverity.Info,
|
||||
Summary = "Info Summary Two",
|
||||
Detail = "Info Detail Tow",
|
||||
Duration = 5000
|
||||
};
|
||||
|
||||
var messageOneHashCode = messageOne.GetHashCode();
|
||||
var messageTwoHashCode = messageTwo.GetHashCode();
|
||||
|
||||
Assert.NotEqual(messageOneHashCode, messageTwoHashCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NotificationService_MessagesCount_AfterAddingMessages()
|
||||
{
|
||||
NotificationService notificationService = new NotificationService();
|
||||
|
||||
//Messages are the same so only one should be added
|
||||
notificationService.Notify(NotificationSeverity.Info, "Info Summary", "Info Detail", 4000);
|
||||
notificationService.Notify(new NotificationMessage()
|
||||
{
|
||||
Severity = NotificationSeverity.Info,
|
||||
Summary = "Info Summary",
|
||||
Detail = "Info Detail",
|
||||
Duration = 4000
|
||||
});
|
||||
|
||||
int expectedMessagesNumber = 1;
|
||||
|
||||
Assert.Equal(expectedMessagesNumber, notificationService.Messages.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NotificationService_MessagesCount_AfterAddingTwoDifferentMessages()
|
||||
{
|
||||
NotificationService notificationService = new NotificationService();
|
||||
|
||||
//Messages are the same so only one should be added
|
||||
notificationService.Notify(NotificationSeverity.Info, "Info Summary 2", "Info Detail 2", 6000);
|
||||
notificationService.Notify(new NotificationMessage()
|
||||
{
|
||||
Severity = NotificationSeverity.Info,
|
||||
Summary = "Info Summary",
|
||||
Detail = "Info Detail",
|
||||
Duration = 4000
|
||||
});
|
||||
|
||||
int expectedMessagesNumber = 2;
|
||||
|
||||
Assert.Equal(expectedMessagesNumber, notificationService.Messages.Count);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Bunit;
|
||||
using Microsoft.AspNetCore.Components.Forms;
|
||||
using Xunit;
|
||||
@@ -15,13 +16,22 @@ namespace Radzen.Blazor.Tests
|
||||
|
||||
public string Name { get; set; }
|
||||
|
||||
public FieldIdentifier FieldIdentifier => throw new System.NotImplementedException();
|
||||
public FieldIdentifier FieldIdentifier { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
|
||||
|
||||
public object GetValue()
|
||||
{
|
||||
return Value;
|
||||
}
|
||||
|
||||
public ValueTask FocusAsync()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public bool Disabled { get; set; }
|
||||
public bool Visible { get; set; }
|
||||
public IFormFieldContext FormFieldContext => null;
|
||||
|
||||
public object Value { get; set; }
|
||||
}
|
||||
|
||||
@@ -54,6 +64,33 @@ namespace Radzen.Blazor.Tests
|
||||
|
||||
Assert.False(component.Instance.Validate(null));
|
||||
}
|
||||
[Fact]
|
||||
public void Returns_True_If_Value_Is_Null_And_AllowNull_Is_True()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
var component = ctx.RenderComponent<RadzenNumericRangeValidatorTestDouble>();
|
||||
|
||||
component.SetParametersAndRender(parameters =>
|
||||
{
|
||||
component.SetParametersAndRender(parameters => parameters.Add(p => p.Min, 0).Add(p => p.Max, 10).Add(p => p.AllowNull, true));
|
||||
});
|
||||
|
||||
Assert.True(component.Instance.Validate(null));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Returns_False_If_Value_Overflows()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
var component = ctx.RenderComponent<RadzenNumericRangeValidatorTestDouble>();
|
||||
|
||||
component.SetParametersAndRender(parameters =>
|
||||
{
|
||||
component.SetParametersAndRender(parameters => parameters.Add(p => p.Min, 0).Add(p => p.Max, 10));
|
||||
});
|
||||
|
||||
Assert.False(component.Instance.Validate(long.MaxValue));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Returns_True_If_Value_Is_Greater_Than_Min()
|
||||
@@ -136,4 +173,4 @@ namespace Radzen.Blazor.Tests
|
||||
Assert.False(component.Instance.Validate(DateTime.Now));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System;
|
||||
using Bunit;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Tests
|
||||
@@ -14,9 +16,9 @@ namespace Radzen.Blazor.Tests
|
||||
|
||||
component.Render();
|
||||
|
||||
Assert.Contains(@$"rz-spinner", component.Markup);
|
||||
Assert.Contains(@$"rz-spinner-up", component.Markup);
|
||||
Assert.Contains(@$"rz-spinner-down", component.Markup);
|
||||
Assert.Contains(@$"rz-numeric", component.Markup);
|
||||
Assert.Contains(@$"rz-numeric-up", component.Markup);
|
||||
Assert.Contains(@$"rz-numeric-down", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -52,11 +54,41 @@ namespace Radzen.Blazor.Tests
|
||||
parameters.Add<decimal?>(p => p.Min, minValue);
|
||||
});
|
||||
|
||||
component.Find(".rz-spinner-down").Click();
|
||||
component.Find(".rz-numeric-down").Click();
|
||||
|
||||
Assert.False(raised, $"Numeric value should Change event if value is less than min value.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Numeric_Respect_Nullable_With_MinParameter()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");
|
||||
|
||||
var component = ctx.RenderComponent<RadzenNumeric<double?>>();
|
||||
|
||||
var raised = false;
|
||||
var value = 3.5;
|
||||
object newValue = null;
|
||||
|
||||
component.SetParametersAndRender(parameters =>
|
||||
{
|
||||
parameters.Add(p => p.Change, args => { raised = true; newValue = args; });
|
||||
parameters.Add<decimal?>(p => p.Min, 1);
|
||||
});
|
||||
|
||||
component.Find("input").Change(value);
|
||||
|
||||
Assert.True(raised);
|
||||
Assert.True(object.Equals(value, newValue));
|
||||
|
||||
component.Find("input").Change("");
|
||||
|
||||
Assert.True(raised);
|
||||
Assert.True(object.Equals(null, newValue));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Numeric_Respect_MaxParameter()
|
||||
{
|
||||
@@ -76,7 +108,7 @@ namespace Radzen.Blazor.Tests
|
||||
parameters.Add<decimal?>(p => p.Max, maxValue);
|
||||
});
|
||||
|
||||
component.Find(".rz-spinner-up").Click();
|
||||
component.Find(".rz-numeric-up").Click();
|
||||
|
||||
Assert.False(raised, $"Numeric value should Change event if value is less than min value.");
|
||||
}
|
||||
@@ -184,13 +216,54 @@ namespace Radzen.Blazor.Tests
|
||||
|
||||
var component = ctx.RenderComponent<RadzenNumeric<double>>();
|
||||
|
||||
component.SetParametersAndRender(parameters => parameters.Add<bool>(p => p.AutoComplete, false));
|
||||
component.SetParametersAndRender(parameters => parameters.AddUnmatched("AutoComplete", false));
|
||||
|
||||
Assert.Contains(@$"autocomplete=""off""", component.Markup);
|
||||
Assert.Contains(@$"aria-autocomplete=""none""", component.Markup);
|
||||
|
||||
component.SetParametersAndRender(parameters => parameters.AddUnmatched("AutoComplete", true));
|
||||
|
||||
Assert.Contains(@$"autocomplete=""on""", component.Markup);
|
||||
Assert.DoesNotContain(@$"aria-autocomplete", component.Markup);
|
||||
|
||||
component.SetParametersAndRender(parameters => parameters.AddUnmatched("autocomplete", "custom"));
|
||||
|
||||
Assert.Contains(@$"autocomplete=""custom""", component.Markup);
|
||||
Assert.DoesNotContain(@$"aria-autocomplete", component.Markup);
|
||||
|
||||
component.Instance.DefaultAutoCompleteAttribute = "autocomplete-custom";
|
||||
component.SetParametersAndRender(parameters => parameters.AddUnmatched("AutoComplete", false));
|
||||
|
||||
Assert.Contains(@$"autocomplete=""autocomplete-custom""", component.Markup);
|
||||
Assert.Contains(@$"aria-autocomplete=""none""", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Numeric_Renders_TypedAutoCompleteParameter()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
|
||||
var component = ctx.RenderComponent<RadzenNumeric<double>>();
|
||||
|
||||
component.SetParametersAndRender(parameters => parameters.AddUnmatched("AutoComplete", false));
|
||||
component.SetParametersAndRender(parameters => parameters.Add<AutoCompleteType>(p => p.AutoCompleteType, AutoCompleteType.On));
|
||||
|
||||
Assert.Contains(@$"autocomplete=""off""", component.Markup);
|
||||
|
||||
component.SetParametersAndRender(parameters => parameters.Add<bool>(p => p.AutoComplete, true));
|
||||
component.SetParametersAndRender(parameters => parameters.AddUnmatched("AutoComplete", true));
|
||||
component.SetParametersAndRender(parameters => parameters.Add<AutoCompleteType>(p => p.AutoCompleteType, AutoCompleteType.Off));
|
||||
|
||||
Assert.Contains(@$"autocomplete=""on""", component.Markup);
|
||||
Assert.Contains(@$"autocomplete=""off""", component.Markup);
|
||||
|
||||
component.SetParametersAndRender(parameters => parameters.AddUnmatched("AutoComplete", true));
|
||||
component.SetParametersAndRender(parameters => parameters.Add<AutoCompleteType>(p => p.AutoCompleteType, AutoCompleteType.BdayMonth));
|
||||
|
||||
Assert.Contains(@$"autocomplete=""{AutoCompleteType.BdayMonth.GetAutoCompleteValue()}""", component.Markup);
|
||||
|
||||
component.SetParametersAndRender(parameters => parameters.AddUnmatched("AutoComplete", true));
|
||||
component.SetParametersAndRender(parameters => parameters.Add<AutoCompleteType>(p => p.AutoCompleteType, AutoCompleteType.BdayYear));
|
||||
|
||||
Assert.Contains(@$"autocomplete=""{AutoCompleteType.BdayYear.GetAutoCompleteValue()}""", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -256,7 +329,7 @@ namespace Radzen.Blazor.Tests
|
||||
parameters.Add(p => p.Change, args => { raised = true; newValue = args; });
|
||||
});
|
||||
|
||||
component.Find(".rz-spinner-up").Click();
|
||||
component.Find(".rz-numeric-up").Click();
|
||||
|
||||
Assert.True(raised, "Numeric Change should be raised on step up");
|
||||
Assert.True(object.Equals(expectedValue, newValue), $"Numeric value should be incremented on step up. Expected value: {expectedValue}, value: {newValue}");
|
||||
@@ -265,7 +338,7 @@ namespace Radzen.Blazor.Tests
|
||||
|
||||
component.SetParametersAndRender(parameters => parameters.Add(p => p.ValueChanged, args => { raised = true; }));
|
||||
|
||||
component.Find(".rz-spinner-up").Click();
|
||||
component.Find(".rz-numeric-up").Click();
|
||||
|
||||
Assert.True(raised, "Numeric ValueChanged should be raised on step up");
|
||||
}
|
||||
@@ -291,7 +364,7 @@ namespace Radzen.Blazor.Tests
|
||||
parameters.Add(p => p.Change, args => { raised = true; newValue = args; });
|
||||
});
|
||||
|
||||
component.Find(".rz-spinner-down").Click();
|
||||
component.Find(".rz-numeric-down").Click();
|
||||
|
||||
Assert.True(raised, "Numeric Change should be raised on step up");
|
||||
Assert.True(object.Equals(expectedValue, newValue), $"Numeric value should be incremented on step up. Expected value: {expectedValue}, value: {newValue}");
|
||||
@@ -300,7 +373,7 @@ namespace Radzen.Blazor.Tests
|
||||
|
||||
component.SetParametersAndRender(parameters => parameters.Add(p => p.ValueChanged, args => { raised = true; }));
|
||||
|
||||
component.Find(".rz-spinner-down").Click();
|
||||
component.Find(".rz-numeric-down").Click();
|
||||
|
||||
Assert.True(raised, "Numeric ValueChanged should be raised on step up");
|
||||
}
|
||||
@@ -314,9 +387,9 @@ namespace Radzen.Blazor.Tests
|
||||
|
||||
component.Render();
|
||||
|
||||
Assert.Contains(@$"rz-spinner-button-icon", component.Markup);
|
||||
Assert.Contains(@$"rz-spinner-up", component.Markup);
|
||||
Assert.Contains(@$"rz-spinner-down", component.Markup);
|
||||
Assert.Contains(@$"rz-numeric-button-icon", component.Markup);
|
||||
Assert.Contains(@$"rz-numeric-up", component.Markup);
|
||||
Assert.Contains(@$"rz-numeric-down", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -328,9 +401,9 @@ namespace Radzen.Blazor.Tests
|
||||
|
||||
component.Render();
|
||||
|
||||
Assert.DoesNotContain(@$"rz-spinner-button-icon", component.Markup);
|
||||
Assert.DoesNotContain(@$"rz-spinner-up", component.Markup);
|
||||
Assert.DoesNotContain(@$"rz-spinner-down", component.Markup);
|
||||
Assert.DoesNotContain(@$"rz-numeric-button-icon", component.Markup);
|
||||
Assert.DoesNotContain(@$"rz-numeric-up", component.Markup);
|
||||
Assert.DoesNotContain(@$"rz-numeric-down", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -350,5 +423,219 @@ namespace Radzen.Blazor.Tests
|
||||
|
||||
Assert.Contains($" value=\"{valueToTest.ToString(format)}\"", component.Markup);
|
||||
}
|
||||
|
||||
public static TheoryData<decimal, decimal> NumericFormatterPreservesLeadingZerosData =>
|
||||
new()
|
||||
{
|
||||
{ 10.000m, 100.000m },
|
||||
{ 100.000m, 10.000m }
|
||||
};
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(NumericFormatterPreservesLeadingZerosData))]
|
||||
public void Numeric_Formatter_PreservesLeadingZeros(decimal oldValue, decimal newValue)
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");
|
||||
|
||||
string format = "0.000";
|
||||
|
||||
var component = ctx.RenderComponent<RadzenNumeric<decimal>>(
|
||||
ComponentParameter.CreateParameter(nameof(RadzenNumeric<decimal>.Format), format),
|
||||
ComponentParameter.CreateParameter(nameof(RadzenNumeric<decimal>.Value), oldValue)
|
||||
);
|
||||
|
||||
component.Render();
|
||||
|
||||
Assert.Contains($" value=\"{oldValue.ToString(format)}\"", component.Markup);
|
||||
|
||||
component.Find("input").Change(newValue);
|
||||
|
||||
Assert.Contains($" value=\"{newValue.ToString(format)}\"", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Numeric_Uses_ConvertValue()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");
|
||||
|
||||
var value = new Dollars(11m);
|
||||
Dollars? ConvertFunc(string s) => decimal.TryParse(s, System.Globalization.CultureInfo.InvariantCulture, out var val) ? new Dollars(val) : null;
|
||||
var component = ctx.RenderComponent<RadzenNumeric<Dollars?>>(
|
||||
ComponentParameter.CreateParameter(nameof(RadzenNumeric<Dollars?>.ConvertValue), (Func<string, Dollars?>)ConvertFunc),
|
||||
ComponentParameter.CreateParameter(nameof(RadzenNumeric<Dollars?>.Value), value),
|
||||
ComponentParameter.CreateParameter(nameof(RadzenNumeric<Dollars>.Culture), System.Globalization.CultureInfo.InvariantCulture)
|
||||
);
|
||||
|
||||
component.Render();
|
||||
|
||||
Assert.Contains($" value=\"{value.ToString()}\"", component.Markup);
|
||||
|
||||
var newValue = new Dollars(13.53m);
|
||||
component.Find("input").Change("13.53");
|
||||
|
||||
Assert.Contains($" value=\"{newValue.ToString()}\"", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Numeric_Supports_TypeConverter()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
|
||||
var valueToTest = new Dollars(100.234m);
|
||||
string format = "0.00";
|
||||
|
||||
var component = ctx.RenderComponent<RadzenNumeric<Dollars>>(
|
||||
ComponentParameter.CreateParameter(nameof(RadzenNumeric<Dollars>.Format), format),
|
||||
ComponentParameter.CreateParameter(nameof(RadzenNumeric<Dollars>.Value), valueToTest)
|
||||
);
|
||||
|
||||
component.Render();
|
||||
|
||||
Assert.Contains($" value=\"{valueToTest.ToString(format, System.Globalization.CultureInfo.CurrentCulture)}\"", component.Markup);
|
||||
}
|
||||
[Fact]
|
||||
public void Numeric_Supports_TypeConverterWithCulture()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
|
||||
var valueToTest = new Dollars(100.234m);
|
||||
string format = "0.00";
|
||||
|
||||
var component = ctx.RenderComponent<RadzenNumeric<Dollars>>(
|
||||
ComponentParameter.CreateParameter(nameof(RadzenNumeric<Dollars>.Format), format),
|
||||
ComponentParameter.CreateParameter(nameof(RadzenNumeric<Dollars>.Value), valueToTest),
|
||||
ComponentParameter.CreateParameter(nameof(RadzenNumeric<Dollars>.Culture), System.Globalization.CultureInfo.InvariantCulture)
|
||||
);
|
||||
|
||||
component.Render();
|
||||
|
||||
Assert.Contains($" value=\"{valueToTest.ToString(format, System.Globalization.CultureInfo.InvariantCulture)}\"", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Numeric_Supports_EmptyString()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
|
||||
var valueToTest = "";
|
||||
string format = "0.00";
|
||||
|
||||
var component = ctx.RenderComponent<RadzenNumeric<string>>(
|
||||
ComponentParameter.CreateParameter(nameof(RadzenNumeric<string>.Format), format),
|
||||
ComponentParameter.CreateParameter(nameof(RadzenNumeric<string>.Value), valueToTest),
|
||||
ComponentParameter.CreateParameter(nameof(RadzenNumeric<Dollars>.Culture), System.Globalization.CultureInfo.InvariantCulture)
|
||||
);
|
||||
|
||||
component.Render();
|
||||
|
||||
Assert.Contains($" value=\"0.00\"", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Numeric_Supports_ValueString()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
|
||||
var valueToTest = "12.50";
|
||||
string format = "0.00";
|
||||
|
||||
var component = ctx.RenderComponent<RadzenNumeric<string>>(
|
||||
ComponentParameter.CreateParameter(nameof(RadzenNumeric<string>.Format), format),
|
||||
ComponentParameter.CreateParameter(nameof(RadzenNumeric<string>.Value), valueToTest),
|
||||
ComponentParameter.CreateParameter(nameof(RadzenNumeric<Dollars>.Culture), System.Globalization.CultureInfo.InvariantCulture)
|
||||
);
|
||||
|
||||
component.Render();
|
||||
|
||||
Assert.Contains($" value=\"{valueToTest}\"", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Numeric_Supports_ValueStringEsCLCulture()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
|
||||
var valueToTest = "12,50";
|
||||
string format = "0.00";
|
||||
|
||||
var component = ctx.RenderComponent<RadzenNumeric<string>>(
|
||||
ComponentParameter.CreateParameter(nameof(RadzenNumeric<string>.Format), format),
|
||||
ComponentParameter.CreateParameter(nameof(RadzenNumeric<string>.Value), valueToTest),
|
||||
ComponentParameter.CreateParameter(nameof(RadzenNumeric<Dollars>.Culture), System.Globalization.CultureInfo.GetCultureInfo("es-CL"))
|
||||
);
|
||||
|
||||
component.Render();
|
||||
|
||||
Assert.Contains($" value=\"{valueToTest}\"", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Numeric_Supports_ValueStringEnUSCulture()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
|
||||
var valueToTest = "12.50";
|
||||
string format = "0.00";
|
||||
|
||||
var component = ctx.RenderComponent<RadzenNumeric<string>>(
|
||||
ComponentParameter.CreateParameter(nameof(RadzenNumeric<string>.Format), format),
|
||||
ComponentParameter.CreateParameter(nameof(RadzenNumeric<string>.Value), valueToTest),
|
||||
ComponentParameter.CreateParameter(nameof(RadzenNumeric<Dollars>.Culture), System.Globalization.CultureInfo.GetCultureInfo("en-US"))
|
||||
);
|
||||
|
||||
component.Render();
|
||||
|
||||
Assert.Contains($" value=\"{valueToTest}\"", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Numeric_Supports_IComparable()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");
|
||||
|
||||
var component = ctx.RenderComponent<RadzenNumeric<Dollars>>();
|
||||
|
||||
var maxValue = 2;
|
||||
|
||||
component.SetParametersAndRender(parameters =>
|
||||
{
|
||||
component.SetParametersAndRender(parameters =>
|
||||
{
|
||||
parameters.Add(p => p.Value, new Dollars(1m));
|
||||
parameters.Add(p => p.Max, maxValue);
|
||||
});
|
||||
});
|
||||
|
||||
component.Find("input").Change(13.53);
|
||||
|
||||
var maxDollars = new Dollars(maxValue);
|
||||
Assert.Contains($" value=\"{maxDollars}\"", component.Markup);
|
||||
Assert.Equal(component.Instance.Value, maxDollars);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Numeric_Supports_IFormattable()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
|
||||
var valueToTest = new Temperature(60.23m);
|
||||
const string format = "F";
|
||||
|
||||
var component = ctx.RenderComponent<RadzenNumeric<Temperature>>(
|
||||
ComponentParameter.CreateParameter(nameof(RadzenNumeric<Temperature>.Format), format),
|
||||
ComponentParameter.CreateParameter(nameof(RadzenNumeric<Temperature>.Value), valueToTest)
|
||||
);
|
||||
|
||||
component.Render();
|
||||
|
||||
var input = component.Find("input").GetAttribute("value");
|
||||
input.MarkupMatches(valueToTest.ToString(format));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,14 +23,14 @@ namespace Radzen.Blazor.Tests
|
||||
|
||||
component.Render();
|
||||
|
||||
Assert.Contains(@$"rz-paginator", component.Markup);
|
||||
Assert.Contains(@$"rz-pager", component.Markup);
|
||||
|
||||
component.SetParametersAndRender(parameters =>
|
||||
{
|
||||
parameters.Add<int>(p => p.PageSize, 101);
|
||||
parameters.Add<int>(p => p.Count, 100);
|
||||
});
|
||||
Assert.DoesNotContain(@$"rz-paginator", component.Markup);
|
||||
Assert.DoesNotContain(@$"rz-pager", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -49,7 +49,7 @@ namespace Radzen.Blazor.Tests
|
||||
|
||||
component.Render();
|
||||
|
||||
Assert.Contains(@$"rz-paginator", component.Markup);
|
||||
Assert.Contains(@$"rz-pager", component.Markup);
|
||||
Assert.Contains(@$"rz-dropdown-trigger", component.Markup);
|
||||
}
|
||||
|
||||
@@ -67,14 +67,93 @@ namespace Radzen.Blazor.Tests
|
||||
await component.Instance.GoToPage(2);
|
||||
component.Render();
|
||||
|
||||
Assert.Contains(@$"rz-paginator-summary", component.Markup);
|
||||
Assert.Contains(@$"rz-pager-summary", component.Markup);
|
||||
Assert.Contains(@$"Page 3 of 10 (100 items)", component.Markup);
|
||||
|
||||
component.SetParametersAndRender(parameters => {
|
||||
parameters.Add<bool>(p => p.ShowPagingSummary, false);
|
||||
});
|
||||
Assert.DoesNotContain(@$"rz-paginator-summary", component.Markup);
|
||||
Assert.DoesNotContain(@$"rz-pager-summary", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RadzenPager_Renders_PagerDensityDefault()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");
|
||||
|
||||
var component = ctx.RenderComponent<RadzenPager>(parameters =>
|
||||
{
|
||||
parameters.Add<int>(p => p.PageSize, 20);
|
||||
parameters.Add<int>(p => p.Count, 100);
|
||||
parameters.Add<Density>(p => p.Density, Density.Default);
|
||||
});
|
||||
|
||||
Assert.DoesNotContain(@$"rz-density-compact", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RadzenPager_Renders_PagerDensityCompact()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");
|
||||
|
||||
var component = ctx.RenderComponent<RadzenPager>(parameters =>
|
||||
{
|
||||
parameters.Add<int>(p => p.PageSize, 20);
|
||||
parameters.Add<int>(p => p.Count, 100);
|
||||
parameters.Add<Density>(p => p.Density, Density.Compact);
|
||||
});
|
||||
|
||||
Assert.Contains(@$"rz-density-compact", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async void RadzenPager_First_And_Prev_Buttons_Are_Disabled_When_On_The_First_Page()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");
|
||||
|
||||
var component = ctx.RenderComponent<RadzenPager>(parameters => {
|
||||
parameters.Add<int>(p => p.PageSize, 10);
|
||||
parameters.Add<int>(p => p.Count, 100);
|
||||
parameters.Add<bool>(p => p.ShowPagingSummary, true);
|
||||
});
|
||||
|
||||
await component.Instance.GoToPage(0);
|
||||
component.Render();
|
||||
|
||||
var firstPageButton = component.Find("a.rz-pager-first");
|
||||
Assert.True(firstPageButton.HasAttribute("disabled"));
|
||||
|
||||
var prevPageButton = component.Find("a.rz-pager-prev");
|
||||
Assert.True(prevPageButton.HasAttribute("disabled"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async void RadzenPager_Last_And_Next_Buttons_Are_Disabled_When_On_The_Last_Page()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");
|
||||
|
||||
var component = ctx.RenderComponent<RadzenPager>(parameters => {
|
||||
parameters.Add<int>(p => p.PageSize, 10);
|
||||
parameters.Add<int>(p => p.Count, 100);
|
||||
parameters.Add<bool>(p => p.ShowPagingSummary, true);
|
||||
});
|
||||
|
||||
await component.Instance.GoToPage(9);
|
||||
component.Render();
|
||||
|
||||
var lastPageButton = component.Find("a.rz-pager-last");
|
||||
Assert.True(lastPageButton.HasAttribute("disabled"));
|
||||
|
||||
var nextPageButton = component.Find("a.rz-pager-next");
|
||||
Assert.True(nextPageButton.HasAttribute("disabled"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
using Bunit;
|
||||
using Bunit.TestDoubles;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Routing;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Tests
|
||||
@@ -22,6 +23,9 @@ namespace Radzen.Blazor.Tests
|
||||
}
|
||||
|
||||
private static string CreatePanelMenu(string currentAbsoluteUrl, NavLinkMatch match, params string[] urls)
|
||||
=> CreatePanelMenu(currentAbsoluteUrl, match, new Dictionary<string, bool>(urls.Select(url => new KeyValuePair<string, bool>(url, false))));
|
||||
|
||||
private static string CreatePanelMenu(string currentAbsoluteUrl, NavLinkMatch match, Dictionary<string, bool> urls)
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
|
||||
@@ -30,12 +34,13 @@ namespace Radzen.Blazor.Tests
|
||||
|
||||
var component = ctx.RenderComponent<RadzenPanelMenu>();
|
||||
|
||||
component.SetParametersAndRender(parameters => parameters.Add(p => p.Match, match).AddChildContent(builder =>
|
||||
component.SetParametersAndRender(parameters => parameters.Add(p => p.Match, match).AddChildContent(builder =>
|
||||
{
|
||||
foreach (var url in urls)
|
||||
{
|
||||
builder.OpenComponent<RadzenPanelMenuItem>(0);
|
||||
builder.AddAttribute(1, nameof(RadzenPanelMenuItem.Path), url);
|
||||
builder.AddAttribute(1, nameof(RadzenPanelMenuItem.Path), url.Key);
|
||||
builder.AddAttribute(2, nameof(RadzenPanelMenuItem.Disabled), url.Value);
|
||||
builder.CloseComponent();
|
||||
}
|
||||
}));
|
||||
@@ -55,6 +60,19 @@ namespace Radzen.Blazor.Tests
|
||||
Assert.Equal(firstIndex, lastIndex);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RadzenPanelMenu_CanDisableMenuItem()
|
||||
{
|
||||
var urls = new Dictionary<string, bool>
|
||||
{
|
||||
{"/datagrid", false},
|
||||
{"/disabled-url", true}
|
||||
};
|
||||
var component = CreatePanelMenu("http://www.example.com/", NavLinkMatch.All, urls);
|
||||
|
||||
Assert.Contains("rz-state-disabled", component);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RadzenPanelMenu_MatchesQueryStringParameters()
|
||||
{
|
||||
|
||||
@@ -47,7 +47,7 @@ namespace Radzen.Blazor.Tests
|
||||
|
||||
component.SetParametersAndRender(parameters => parameters.Add(p => p.Icon, value));
|
||||
|
||||
Assert.Contains(@$"<i class=""rzi"">{value}</i>", component.Markup);
|
||||
Assert.Contains(@$"<i class=""notranslate rzi"">{value}</i>", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -119,11 +119,11 @@ namespace Radzen.Blazor.Tests
|
||||
|
||||
component.SetParametersAndRender(parameters => parameters.Add<bool>(p => p.AllowCollapse, true));
|
||||
|
||||
Assert.Contains(@"<span class=""rzi rzi-minus""></span>", component.Markup);
|
||||
Assert.Contains(@"<span class=""notranslate rzi rzi-minus""></span>", component.Markup);
|
||||
|
||||
component.SetParametersAndRender(parameters => parameters.Add<bool>(p => p.Collapsed, true));
|
||||
|
||||
Assert.Contains(@"<span class=""rzi rzi-plus""></span>", component.Markup);
|
||||
Assert.Contains(@"<span class=""notranslate rzi rzi-plus""></span>", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -198,14 +198,10 @@ namespace Radzen.Blazor.Tests
|
||||
});
|
||||
|
||||
Assert.Contains("SummaryContent", component.Markup);
|
||||
Assert.Equal(
|
||||
"display: block",
|
||||
component.Find(".rz-panel-content-summary").ParentElement.Attributes.First(attr => attr.Name == "style").Value
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Panel_DontRenders_SummaryWhenOpen()
|
||||
public void Panel_DoesNotRender_SummaryWhenOpen()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
var component = ctx.RenderComponent<RadzenPanel>();
|
||||
@@ -225,8 +221,8 @@ namespace Radzen.Blazor.Tests
|
||||
|
||||
Assert.Contains("SummaryContent", component.Markup);
|
||||
Assert.Equal(
|
||||
"display: none",
|
||||
component.Find(".rz-panel-content-summary").ParentElement.Attributes.First(attr => attr.Name == "style").Value
|
||||
"true",
|
||||
component.Find(".rz-panel-content-summary").ParentElement.ParentElement.Attributes.First(attr => attr.Name == "aria-hidden").Value
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,13 +119,45 @@ namespace Radzen.Blazor.Tests
|
||||
|
||||
var component = ctx.RenderComponent<RadzenPassword>();
|
||||
|
||||
component.SetParametersAndRender(parameters => parameters.Add<bool>(p => p.AutoComplete, false));
|
||||
component.SetParametersAndRender(parameters => parameters.AddUnmatched("AutoComplete", false));
|
||||
|
||||
Assert.Contains(@$"autocomplete=""new-password""", component.Markup);
|
||||
|
||||
component.SetParametersAndRender(parameters => parameters.Add<bool>(p => p.AutoComplete, true));
|
||||
component.SetParametersAndRender(parameters => parameters.AddUnmatched("AutoComplete", true));
|
||||
|
||||
Assert.Contains(@$"autocomplete=""on""", component.Markup);
|
||||
|
||||
component.SetParametersAndRender(parameters => parameters.AddUnmatched("autocomplete", "custom"));
|
||||
|
||||
Assert.Contains(@$"autocomplete=""custom""", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Password_Renders_TypedAutoCompleteParameter()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
|
||||
var component = ctx.RenderComponent<RadzenPassword>();
|
||||
|
||||
component.SetParametersAndRender(parameters => parameters.AddUnmatched("AutoComplete", false));
|
||||
component.SetParametersAndRender(parameters => parameters.Add<AutoCompleteType>(p => p.AutoCompleteType, AutoCompleteType.On));
|
||||
|
||||
Assert.Contains(@$"autocomplete=""new-password""", component.Markup);
|
||||
|
||||
component.SetParametersAndRender(parameters => parameters.AddUnmatched("AutoComplete", true));
|
||||
component.SetParametersAndRender(parameters => parameters.Add<AutoCompleteType>(p => p.AutoCompleteType, AutoCompleteType.Off));
|
||||
|
||||
Assert.Contains(@$"autocomplete=""off""", component.Markup);
|
||||
|
||||
component.SetParametersAndRender(parameters => parameters.AddUnmatched("AutoComplete", true));
|
||||
component.SetParametersAndRender(parameters => parameters.Add<AutoCompleteType>(p => p.AutoCompleteType, AutoCompleteType.CurrentPassword));
|
||||
|
||||
Assert.Contains(@$"autocomplete=""{AutoCompleteType.CurrentPassword.GetAutoCompleteValue()}""", component.Markup);
|
||||
|
||||
component.SetParametersAndRender(parameters => parameters.AddUnmatched("AutoComplete", true));
|
||||
component.SetParametersAndRender(parameters => parameters.Add<AutoCompleteType>(p => p.AutoCompleteType, AutoCompleteType.NewPassword));
|
||||
|
||||
Assert.Contains(@$"autocomplete=""{AutoCompleteType.NewPassword.GetAutoCompleteValue()}""", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -72,7 +72,7 @@ namespace Radzen.Blazor.Tests
|
||||
|
||||
component.SetParametersAndRender(parameters => parameters.Add(p => p.Style, value));
|
||||
|
||||
Assert.Contains(@$"style=""{value}""", component.Markup);
|
||||
Assert.Contains(@$"style=""--rz-progressbar-value: 0%;{value}""", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -118,7 +118,7 @@ namespace Radzen.Blazor.Tests
|
||||
parameters.Add<double>(p => p.Max, max);
|
||||
});
|
||||
|
||||
Assert.Contains(@$"style=""width: {Math.Min(value / max * 100, 100).ToInvariantString()}%;""", component.Markup);
|
||||
Assert.Contains(@$"style=""--rz-progressbar-value: {Math.Min(value / max * 100, 100).ToInvariantString()}%;""", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -129,18 +129,18 @@ namespace Radzen.Blazor.Tests
|
||||
var component = ctx.RenderComponent<RadzenProgressBar>();
|
||||
|
||||
component.SetParametersAndRender(parameters=>parameters.Add(p=>p.ProgressBarStyle, ProgressBarStyle.Success));
|
||||
Assert.Contains(@$"rz-progressbar-determinate-success", component.Markup);
|
||||
Assert.Contains(@$"rz-progressbar-success", component.Markup);
|
||||
|
||||
component.SetParametersAndRender(parameters => parameters.Add(p => p.ProgressBarStyle, ProgressBarStyle.Info));
|
||||
Assert.Contains(@$"rz-progressbar-determinate-info", component.Markup);
|
||||
Assert.Contains(@$"rz-progressbar-info", component.Markup);
|
||||
|
||||
component.SetParametersAndRender(parameters => parameters.Add(p => p.ProgressBarStyle, ProgressBarStyle.Success));
|
||||
component.SetParametersAndRender(parameters => parameters.Add(p => p.Mode, ProgressBarMode.Indeterminate));
|
||||
Assert.Contains(@$"rz-progressbar-indeterminate-success", component.Markup);
|
||||
Assert.Contains(@$"rz-progressbar-success", component.Markup);
|
||||
|
||||
component.SetParametersAndRender(parameters => parameters.Add(p => p.ProgressBarStyle, ProgressBarStyle.Info));
|
||||
component.SetParametersAndRender(parameters => parameters.Add(p => p.Mode, ProgressBarMode.Indeterminate));
|
||||
Assert.Contains(@$"rz-progressbar-indeterminate-info", component.Markup);
|
||||
Assert.Contains(@$"rz-progressbar-info", component.Markup);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
using AngleSharp.Css;
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Linq.Dynamic.Core;
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Tests
|
||||
@@ -116,5 +113,43 @@ namespace Radzen.Blazor.Tests
|
||||
{
|
||||
public List<string> Values { get; set; }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetProperty_Should_Resolve_DescriptionProperty()
|
||||
{
|
||||
var descriptionProperty = PropertyAccess.GetProperty(typeof(ISimpleInterface), nameof(ISimpleInterface.Description));
|
||||
|
||||
Assert.NotNull(descriptionProperty);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetProperty_Should_Resolve_NameProperty()
|
||||
{
|
||||
var nameProperty = PropertyAccess.GetProperty(typeof(ISimpleInterface), nameof(ISimpleInterface.Name));
|
||||
|
||||
Assert.NotNull(nameProperty);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetProperty_Should_Resolve_IdProperty()
|
||||
{
|
||||
var idProperty = PropertyAccess.GetProperty(typeof(ISimpleInterface), nameof(ISimpleBaseInterface.Id));
|
||||
Assert.NotNull(idProperty);
|
||||
}
|
||||
|
||||
interface ISimpleInterface : ISimpleNestedInterface
|
||||
{
|
||||
string Description { get; set; }
|
||||
}
|
||||
|
||||
interface ISimpleNestedInterface : ISimpleBaseInterface
|
||||
{
|
||||
string Name { get; set; }
|
||||
}
|
||||
|
||||
interface ISimpleBaseInterface
|
||||
{
|
||||
int Id { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7</TargetFramework>
|
||||
<TargetFramework>net9</TargetFramework>
|
||||
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
|
||||
<PackageReference Include="bunit.web" Version="1.2.49" />
|
||||
<PackageReference Include="bunit.web" Version="1.36.0" />
|
||||
<PackageReference Include="xunit" Version="2.4.1" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
|
||||
158
Radzen.Blazor.Tests/SkeletonTests.cs
Normal file
158
Radzen.Blazor.Tests/SkeletonTests.cs
Normal file
@@ -0,0 +1,158 @@
|
||||
using Bunit;
|
||||
using Radzen.Blazor.Rendering;
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Tests
|
||||
{
|
||||
public class SkeletonTests
|
||||
{
|
||||
[Fact]
|
||||
public void Skeleton_Renders_CssClass()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");
|
||||
|
||||
var component = ctx.RenderComponent<RadzenSkeleton>();
|
||||
|
||||
Assert.Contains("rz-skeleton", component.Markup);
|
||||
Assert.Contains("rz-skeleton-text", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Skeleton_Renders_TypeParameter()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");
|
||||
|
||||
var component = ctx.RenderComponent<RadzenSkeleton>();
|
||||
|
||||
component.SetParametersAndRender(parameters => parameters.Add(p => p.Variant, SkeletonVariant.Circular));
|
||||
|
||||
Assert.Contains("rz-skeleton", component.Markup);
|
||||
Assert.Contains("rz-skeleton-circular", component.Markup);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(SkeletonVariant.Text, "rz-skeleton-text")]
|
||||
[InlineData(SkeletonVariant.Circular, "rz-skeleton-circular")]
|
||||
[InlineData(SkeletonVariant.Rectangular, "rz-skeleton-rectangular")]
|
||||
public void Skeleton_Renders_AllTypes(SkeletonVariant type, string expectedClass)
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");
|
||||
|
||||
var component = ctx.RenderComponent<RadzenSkeleton>(parameters => parameters.Add(p => p.Variant, type));
|
||||
|
||||
Assert.Contains("rz-skeleton", component.Markup);
|
||||
Assert.Contains(expectedClass, component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Skeleton_Renders_AnimationParameter()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");
|
||||
|
||||
var component = ctx.RenderComponent<RadzenSkeleton>();
|
||||
|
||||
component.SetParametersAndRender(parameters => parameters.Add(p => p.Animation, SkeletonAnimation.Wave));
|
||||
|
||||
Assert.Contains("rz-skeleton", component.Markup);
|
||||
Assert.Contains("rz-skeleton-wave", component.Markup);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(SkeletonAnimation.Wave, "rz-skeleton-wave")]
|
||||
[InlineData(SkeletonAnimation.Pulse, "rz-skeleton-pulse")]
|
||||
public void Skeleton_Renders_AllAnimations(SkeletonAnimation animation, string expectedClass)
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");
|
||||
|
||||
var component = ctx.RenderComponent<RadzenSkeleton>(parameters => parameters.Add(p => p.Animation, animation));
|
||||
|
||||
Assert.Contains("rz-skeleton", component.Markup);
|
||||
Assert.Contains(expectedClass, component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Skeleton_Renders_StyleParameter()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");
|
||||
|
||||
var component = ctx.RenderComponent<RadzenSkeleton>();
|
||||
|
||||
var style = "width: 200px; height: 20px;";
|
||||
|
||||
component.SetParametersAndRender(parameters => parameters.Add(p => p.Style, style));
|
||||
|
||||
Assert.Contains($"style=\"{style}\"", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Skeleton_Renders_VisibleParameter()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");
|
||||
|
||||
var component = ctx.RenderComponent<RadzenSkeleton>();
|
||||
|
||||
// Should be visible by default
|
||||
Assert.Contains("rz-skeleton", component.Markup);
|
||||
|
||||
component.SetParametersAndRender(parameters => parameters.Add(p => p.Visible, false));
|
||||
|
||||
// Should not render when not visible
|
||||
Assert.DoesNotContain("rz-skeleton", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Skeleton_Renders_UnmatchedParameter()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");
|
||||
|
||||
var component = ctx.RenderComponent<RadzenSkeleton>();
|
||||
|
||||
component.SetParametersAndRender(parameters => parameters.AddUnmatched("data-testid", "skeleton-test"));
|
||||
|
||||
Assert.Contains("data-testid=\"skeleton-test\"", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Skeleton_DefaultType_IsText()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");
|
||||
|
||||
var component = ctx.RenderComponent<RadzenSkeleton>();
|
||||
|
||||
// Should render with text type by default
|
||||
Assert.Contains("rz-skeleton-text", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Skeleton_DefaultAnimation_IsNone()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");
|
||||
|
||||
var component = ctx.RenderComponent<RadzenSkeleton>();
|
||||
|
||||
// Should not render animation classes by default
|
||||
Assert.DoesNotContain("rz-skeleton-wave", component.Markup);
|
||||
Assert.DoesNotContain("rz-skeleton-pulse", component.Markup);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -39,7 +39,7 @@ namespace Radzen.Blazor.Tests
|
||||
component.SetParametersAndRender(parameters => parameters.Add<int>(p => p.Value, value));
|
||||
|
||||
Assert.Contains(@$"style=""width: {Math.Round((value / max * 100)).ToInvariantString()}%;""", component.Markup);
|
||||
Assert.Contains(@$"style=""left: {Math.Round((value / max * 100)).ToInvariantString()}%;""", component.Markup);
|
||||
Assert.Contains(@$"style=""inset-inline-start: {Math.Round((value / max * 100)).ToInvariantString()}%;""", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -55,9 +55,9 @@ namespace Radzen.Blazor.Tests
|
||||
parameters.Add<IEnumerable<int>>(p => p.Value, new int[] { 4, 30 });
|
||||
});
|
||||
|
||||
Assert.Contains(@$"left: 4%", component.Markup);
|
||||
Assert.Contains(@$"left: 30%", component.Markup);
|
||||
Assert.Contains(@$"left: 4%; width: 26%;", component.Markup);
|
||||
Assert.Contains(@$"inset-inline-start: 4%", component.Markup);
|
||||
Assert.Contains(@$"inset-inline-start: 30%", component.Markup);
|
||||
Assert.Contains(@$"inset-inline-start: 4%; width: 26%;", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Bunit;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Tests
|
||||
@@ -44,7 +45,7 @@ namespace Radzen.Blazor.Tests
|
||||
|
||||
component.SetParametersAndRender(parameters => parameters.Add(p => p.Icon, icon));
|
||||
|
||||
Assert.Contains(@$"<i class=""rz-button-icon-left rzi"">{icon}</i>", component.Markup);
|
||||
Assert.Contains(@$"<i class=""notranslate rz-button-icon-left rzi"">{icon}</i>", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -59,10 +60,10 @@ namespace Radzen.Blazor.Tests
|
||||
|
||||
component.SetParametersAndRender(parameters => {
|
||||
parameters.Add(p => p.Text, text);
|
||||
parameters.Add(p => p.Icon, icon);
|
||||
parameters.Add(p => p.Icon, icon);
|
||||
});
|
||||
|
||||
Assert.Contains(@$"<i class=""rz-button-icon-left rzi"">{icon}</i>", component.Markup);
|
||||
Assert.Contains(@$"<i class=""notranslate rz-button-icon-left rzi"">{icon}</i>", component.Markup);
|
||||
Assert.Contains(@$"<span class=""rz-button-text"">{text}</span>", component.Markup);
|
||||
}
|
||||
|
||||
@@ -77,7 +78,7 @@ namespace Radzen.Blazor.Tests
|
||||
|
||||
component.SetParametersAndRender(parameters => parameters.Add(p => p.Image, image));
|
||||
|
||||
Assert.Contains(@$"<img class=""rz-button-icon-left rzi"" src=""{image}"" />", component.Markup);
|
||||
Assert.Contains(@$"<img class=""notranslate rz-button-icon-left rzi"" src=""{image}"" alt=""image"" />", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -93,12 +94,29 @@ namespace Radzen.Blazor.Tests
|
||||
component.SetParametersAndRender(parameters => {
|
||||
parameters.Add(p => p.Text, text);
|
||||
parameters.Add(p => p.Image, image);
|
||||
parameters.Add(p => p.ImageAlternateText, text);
|
||||
});
|
||||
|
||||
Assert.Contains(@$"<img class=""rz-button-icon-left rzi"" src=""{image}"" />", component.Markup);
|
||||
Assert.Contains(@$"<img class=""notranslate rz-button-icon-left rzi"" src=""{image}"" alt=""{text}"" />", component.Markup);
|
||||
Assert.Contains(@$"<span class=""rz-button-text"">{text}</span>", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SplitButton_Renders_ButtonContent()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
|
||||
RenderFragment buttonContent = (builder) => builder.AddMarkupContent(0, "<strong>Custom button content</strong>");
|
||||
|
||||
var text = "Test";
|
||||
var component = ctx.RenderComponent<RadzenSplitButton>(parameters => parameters
|
||||
.Add(p => p.ButtonContent, buttonContent)
|
||||
.Add(p => p.Text, text));
|
||||
|
||||
Assert.Contains(@$"<strong>Custom button content</strong>", component.Markup);
|
||||
Assert.DoesNotContain(@$"<span class=""rz-button-text"">{text}</span>", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SplitButton_Renders_DisabledParameter()
|
||||
{
|
||||
|
||||
@@ -41,7 +41,7 @@ namespace Radzen.Blazor.Tests
|
||||
|
||||
component.SetParametersAndRender(parameters => parameters.Add(p => p.Style, value));
|
||||
|
||||
Assert.Contains(@$"style=""outline: 0 none;{value}""", component.Markup);
|
||||
Assert.Contains(@$"style=""{value}""", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -134,5 +134,18 @@ namespace Radzen.Blazor.Tests
|
||||
Assert.True(raised);
|
||||
Assert.True(object.Equals(value, !(bool)newValue));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Switch_Renders_ReadOnlyParameter()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
|
||||
var component = ctx.RenderComponent<RadzenSwitch>();
|
||||
|
||||
component.SetParametersAndRender(parameters => parameters.Add<bool>(p => p.ReadOnly, true));
|
||||
|
||||
Assert.Contains(@$"rz-readonly", component.Markup);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
35
Radzen.Blazor.Tests/Temperature.cs
Normal file
35
Radzen.Blazor.Tests/Temperature.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
|
||||
namespace Radzen.Blazor.Tests;
|
||||
|
||||
public record struct Temperature(decimal DegreesCelsius)
|
||||
: IFormattable
|
||||
{
|
||||
public decimal Celsius => DegreesCelsius;
|
||||
public decimal Fahrenheit => DegreesCelsius * 9 / 5 + 32;
|
||||
public decimal Kelvin => DegreesCelsius + 273.15m;
|
||||
|
||||
public override string ToString() => ToString("G");
|
||||
public string ToString(string format) => ToString(format, CultureInfo.CurrentCulture);
|
||||
|
||||
public string ToString(string format, IFormatProvider provider)
|
||||
{
|
||||
provider ??= CultureInfo.CurrentCulture;
|
||||
if (string.IsNullOrEmpty(format))
|
||||
format = "G";
|
||||
|
||||
switch (format.ToUpperInvariant())
|
||||
{
|
||||
case "G":
|
||||
case "C":
|
||||
return $"{Celsius.ToString("F2", provider)} °C";
|
||||
case "F":
|
||||
return $"{Fahrenheit.ToString("F2", provider)} °F";
|
||||
case "K":
|
||||
return $"{Kelvin.ToString("F2", provider)} K";
|
||||
default:
|
||||
throw new FormatException($"The {format} format string is not supported.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
using Bunit;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Forms;
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Tests
|
||||
@@ -29,6 +31,26 @@ namespace Radzen.Blazor.Tests
|
||||
Assert.Contains(@$"value=""{value}""", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TextboxCanSetFieldIdentifier()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
|
||||
var editContext = new EditContext(ctx);
|
||||
var fieldIdentifier = new FieldIdentifier(ctx, nameof(RadzenTextBox.Value));
|
||||
ctx.RenderTree.TryAdd<CascadingValue<EditContext>>(parameters =>
|
||||
{
|
||||
parameters.Add(e => e.Value, editContext);
|
||||
});
|
||||
|
||||
var component = ctx.RenderComponent<RadzenTextBox>(parameters =>
|
||||
{
|
||||
parameters.Add(p => p.FieldIdentifier, fieldIdentifier);
|
||||
});
|
||||
|
||||
Assert.Equal(component.Instance.FieldIdentifier, fieldIdentifier);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TextBox_Renders_StyleParameter()
|
||||
{
|
||||
@@ -119,13 +141,54 @@ namespace Radzen.Blazor.Tests
|
||||
|
||||
var component = ctx.RenderComponent<RadzenTextBox>();
|
||||
|
||||
component.SetParametersAndRender(parameters => parameters.Add<bool>(p => p.AutoComplete, false));
|
||||
component.SetParametersAndRender(parameters => parameters.AddUnmatched("AutoComplete", false));
|
||||
|
||||
Assert.Contains(@$"autocomplete=""off""", component.Markup);
|
||||
Assert.Contains(@$"aria-autocomplete=""none""", component.Markup);
|
||||
|
||||
component.SetParametersAndRender(parameters => parameters.AddUnmatched("AutoComplete", true));
|
||||
|
||||
Assert.Contains(@$"autocomplete=""on""", component.Markup);
|
||||
Assert.DoesNotContain(@$"aria-autocomplete", component.Markup);
|
||||
|
||||
component.SetParametersAndRender(parameters => parameters.AddUnmatched("autocomplete", "custom"));
|
||||
|
||||
Assert.Contains(@$"autocomplete=""custom""", component.Markup);
|
||||
Assert.DoesNotContain(@$"aria-autocomplete", component.Markup);
|
||||
|
||||
component.Instance.DefaultAutoCompleteAttribute = "autocomplete-custom";
|
||||
component.SetParametersAndRender(parameters => parameters.AddUnmatched("AutoComplete", false));
|
||||
|
||||
Assert.Contains(@$"autocomplete=""autocomplete-custom""", component.Markup);
|
||||
Assert.Contains(@$"aria-autocomplete=""none""", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TextBox_Renders_TypedAutoCompleteParameter()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
|
||||
var component = ctx.RenderComponent<RadzenTextBox>();
|
||||
|
||||
component.SetParametersAndRender(parameters => parameters.AddUnmatched("AutoComplete", false));
|
||||
component.SetParametersAndRender(parameters => parameters.Add<AutoCompleteType>(p => p.AutoCompleteType, AutoCompleteType.On));
|
||||
|
||||
Assert.Contains(@$"autocomplete=""off""", component.Markup);
|
||||
|
||||
component.SetParametersAndRender(parameters => parameters.Add<bool>(p => p.AutoComplete, true));
|
||||
component.SetParametersAndRender(parameters => parameters.AddUnmatched("AutoComplete", true));
|
||||
component.SetParametersAndRender(parameters => parameters.Add<AutoCompleteType>(p => p.AutoCompleteType, AutoCompleteType.Off));
|
||||
|
||||
Assert.Contains(@$"autocomplete=""on""", component.Markup);
|
||||
Assert.Contains(@$"autocomplete=""off""", component.Markup);
|
||||
|
||||
component.SetParametersAndRender(parameters => parameters.AddUnmatched("AutoComplete", true));
|
||||
component.SetParametersAndRender(parameters => parameters.Add<AutoCompleteType>(p => p.AutoCompleteType, AutoCompleteType.AdditionalName));
|
||||
|
||||
Assert.Contains(@$"autocomplete=""{AutoCompleteType.AdditionalName.GetAutoCompleteValue()}""", component.Markup);
|
||||
|
||||
component.SetParametersAndRender(parameters => parameters.AddUnmatched("AutoComplete", true));
|
||||
component.SetParametersAndRender(parameters => parameters.Add<AutoCompleteType>(p => p.AutoCompleteType, AutoCompleteType.FamilyName));
|
||||
|
||||
Assert.Contains(@$"autocomplete=""{AutoCompleteType.FamilyName.GetAutoCompleteValue()}""", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
1187
Radzen.Blazor.Tests/TimeSpanPickerTests.cs
Normal file
1187
Radzen.Blazor.Tests/TimeSpanPickerTests.cs
Normal file
File diff suppressed because it is too large
Load Diff
460
Radzen.Blazor/AIChatService.cs
Normal file
460
Radzen.Blazor/AIChatService.cs
Normal file
@@ -0,0 +1,460 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.IO;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System.Linq;
|
||||
|
||||
namespace Radzen;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a chat message in the conversation history.
|
||||
/// </summary>
|
||||
public class ChatMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the role of the message sender (system, user, or assistant).
|
||||
/// </summary>
|
||||
public string Role { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the content of the message.
|
||||
/// </summary>
|
||||
public string Content { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the timestamp when the message was created.
|
||||
/// </summary>
|
||||
public DateTime Timestamp { get; set; } = DateTime.Now;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a conversation session with memory.
|
||||
/// </summary>
|
||||
public class ConversationSession
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the unique identifier for the conversation session.
|
||||
/// </summary>
|
||||
public string Id { get; set; } = Guid.NewGuid().ToString();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of messages in the conversation.
|
||||
/// </summary>
|
||||
public List<ChatMessage> Messages { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the timestamp when the conversation was created.
|
||||
/// </summary>
|
||||
public DateTime CreatedAt { get; set; } = DateTime.Now;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the timestamp when the conversation was last updated.
|
||||
/// </summary>
|
||||
public DateTime LastUpdated { get; set; } = DateTime.Now;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum number of messages to keep in memory.
|
||||
/// </summary>
|
||||
public int MaxMessages { get; set; } = 50;
|
||||
|
||||
/// <summary>
|
||||
/// Adds a message to the conversation and manages memory limits.
|
||||
/// </summary>
|
||||
/// <param name="role">The role of the message sender.</param>
|
||||
/// <param name="content">The message content.</param>
|
||||
public void AddMessage(string role, string content)
|
||||
{
|
||||
Messages.Add(new ChatMessage
|
||||
{
|
||||
Role = role,
|
||||
Content = content,
|
||||
Timestamp = DateTime.Now
|
||||
});
|
||||
|
||||
LastUpdated = DateTime.Now;
|
||||
|
||||
// Remove oldest messages if we exceed the limit
|
||||
while (Messages.Count > MaxMessages)
|
||||
{
|
||||
Messages.RemoveAt(0);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears all messages from the conversation.
|
||||
/// </summary>
|
||||
public void Clear()
|
||||
{
|
||||
Messages.Clear();
|
||||
LastUpdated = DateTime.Now;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the conversation messages formatted for the AI API.
|
||||
/// </summary>
|
||||
/// <param name="systemPrompt">The system prompt to include.</param>
|
||||
/// <returns>A list of message objects for the AI API.</returns>
|
||||
public List<object> GetFormattedMessages(string systemPrompt)
|
||||
{
|
||||
var messages = new List<object>();
|
||||
|
||||
// Add system message
|
||||
messages.Add(new { role = "system", content = systemPrompt });
|
||||
|
||||
// Add conversation messages
|
||||
foreach (var message in Messages)
|
||||
{
|
||||
messages.Add(new { role = message.Role, content = message.Content });
|
||||
}
|
||||
|
||||
return messages;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interface for getting chat completions from an AI model with conversation memory.
|
||||
/// </summary>
|
||||
public interface IAIChatService
|
||||
{
|
||||
/// <summary>
|
||||
/// Streams chat completion responses from the AI model asynchronously with conversation memory.
|
||||
/// </summary>
|
||||
/// <param name="userInput">The user's input message to send to the AI model.</param>
|
||||
/// <param name="sessionId">Optional session ID to maintain conversation context. If null, a new session will be created.</param>
|
||||
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
|
||||
/// <param name="model">Optional model name to override the configured model.</param>
|
||||
/// <param name="systemPrompt">Optional system prompt to override the configured system prompt.</param>
|
||||
/// <param name="temperature">Optional temperature to override the configured temperature.</param>
|
||||
/// <param name="maxTokens">Optional maximum tokens to override the configured max tokens.</param>
|
||||
/// <returns>An async enumerable that yields streaming response chunks from the AI model.</returns>
|
||||
IAsyncEnumerable<string> GetCompletionsAsync(string userInput, string sessionId = null, CancellationToken cancellationToken = default, string model = null, string systemPrompt = null, double? temperature = null, int? maxTokens = null);
|
||||
|
||||
/// <summary>
|
||||
/// Gets or creates a conversation session.
|
||||
/// </summary>
|
||||
/// <param name="sessionId">The session ID. If null, a new session will be created.</param>
|
||||
/// <returns>The conversation session.</returns>
|
||||
ConversationSession GetOrCreateSession(string sessionId = null);
|
||||
|
||||
/// <summary>
|
||||
/// Clears the conversation history for a specific session.
|
||||
/// </summary>
|
||||
/// <param name="sessionId">The session ID to clear.</param>
|
||||
void ClearSession(string sessionId);
|
||||
|
||||
/// <summary>
|
||||
/// Gets all active conversation sessions.
|
||||
/// </summary>
|
||||
/// <returns>A list of active conversation sessions.</returns>
|
||||
IEnumerable<ConversationSession> GetActiveSessions();
|
||||
|
||||
/// <summary>
|
||||
/// Removes old conversation sessions based on age.
|
||||
/// </summary>
|
||||
/// <param name="maxAgeHours">Maximum age in hours for sessions to keep.</param>
|
||||
void CleanupOldSessions(int maxAgeHours = 24);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configuration options for the <see cref="AIChatService"/>.
|
||||
/// </summary>
|
||||
public class AIChatServiceOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the endpoint URL for the AI service.
|
||||
/// </summary>
|
||||
public string Endpoint { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the proxy URL for the AI service, if any. If set, this will override the Endpoint.
|
||||
/// </summary>
|
||||
public string Proxy { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the API key for authentication with the AI service.
|
||||
/// </summary>
|
||||
public string ApiKey { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the header name for the API key (e.g., 'Authorization' or 'api-key').
|
||||
/// </summary>
|
||||
public string ApiKeyHeader { get; set; } = "Authorization";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the model name to use for executing chat completions (e.g., 'gpt-3.5-turbo').
|
||||
/// </summary>
|
||||
public string Model { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the system prompt for the AI assistant.
|
||||
/// </summary>
|
||||
public string SystemPrompt { get; set; } = "You are a helpful AI code assistant.";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the temperature for the AI model (0.0 to 2.0). Set to 0.0 for deterministic responses, higher values for more creative outputs.
|
||||
/// </summary>
|
||||
public double Temperature { get; set; } = 0.7;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum number of tokens to generate in the response.
|
||||
/// </summary>
|
||||
public int? MaxTokens { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum number of messages to keep in conversation memory.
|
||||
/// </summary>
|
||||
public int MaxMessages { get; set; } = 50;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum age in hours for conversation sessions before cleanup.
|
||||
/// </summary>
|
||||
public int SessionMaxAgeHours { get; set; } = 24;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Service for interacting with AI chat models to get completions with conversation memory.
|
||||
/// </summary>
|
||||
public class AIChatService(IServiceProvider serviceProvider, IOptions<AIChatServiceOptions> options) : IAIChatService
|
||||
{
|
||||
private readonly Dictionary<string, ConversationSession> _sessions = new();
|
||||
private readonly object _sessionsLock = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the configuration options for the chat streaming service.
|
||||
/// </summary>
|
||||
public AIChatServiceOptions Options => options.Value;
|
||||
|
||||
/// <inheritdoc />
|
||||
public async IAsyncEnumerable<string> GetCompletionsAsync(string userInput, string sessionId = null, [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken = default, string model = null, string systemPrompt = null, double? temperature = null, int? maxTokens = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(userInput))
|
||||
{
|
||||
throw new ArgumentException("User input cannot be null or empty.", nameof(userInput));
|
||||
}
|
||||
|
||||
// Get or create session
|
||||
var session = GetOrCreateSession(sessionId);
|
||||
|
||||
// Add user message to conversation history
|
||||
session.AddMessage("user", userInput);
|
||||
|
||||
var url = Options.Proxy ?? Options.Endpoint;
|
||||
|
||||
// Get formatted messages including conversation history
|
||||
var messages = session.GetFormattedMessages(systemPrompt ?? Options.SystemPrompt);
|
||||
|
||||
var payload = new
|
||||
{
|
||||
model = model ?? Options.Model,
|
||||
messages = messages,
|
||||
temperature = temperature ?? Options.Temperature,
|
||||
max_tokens = maxTokens ?? Options.MaxTokens,
|
||||
stream = true
|
||||
};
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, url)
|
||||
{
|
||||
Content = new StringContent(JsonSerializer.Serialize(payload, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }), Encoding.UTF8, "application/json")
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(Options.ApiKey))
|
||||
{
|
||||
if (string.Equals(Options.ApiKeyHeader, "Authorization", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Options.ApiKey);
|
||||
}
|
||||
else
|
||||
{
|
||||
request.Headers.Add(Options.ApiKeyHeader, Options.ApiKey);
|
||||
}
|
||||
}
|
||||
|
||||
var httpClient = serviceProvider.GetRequiredService<HttpClient>();
|
||||
var response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
throw new Exception($"Chat stream failed: {await response.Content.ReadAsStringAsync(cancellationToken)}");
|
||||
}
|
||||
|
||||
using var stream = await response.Content.ReadAsStreamAsync(cancellationToken);
|
||||
using var reader = new StreamReader(stream);
|
||||
|
||||
var assistantResponse = new StringBuilder();
|
||||
|
||||
while (!reader.EndOfStream && !cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
var line = await reader.ReadLineAsync();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(line) || !line.StartsWith("data:"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var json = line["data:".Length..].Trim();
|
||||
|
||||
if (json == "[DONE]")
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
var content = ParseStreamingResponse(json);
|
||||
if (!string.IsNullOrEmpty(content))
|
||||
{
|
||||
assistantResponse.Append(content);
|
||||
yield return content;
|
||||
}
|
||||
}
|
||||
|
||||
// Add assistant response to conversation history
|
||||
if (assistantResponse.Length > 0)
|
||||
{
|
||||
session.AddMessage("assistant", assistantResponse.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ConversationSession GetOrCreateSession(string sessionId = null)
|
||||
{
|
||||
lock (_sessionsLock)
|
||||
{
|
||||
if (string.IsNullOrEmpty(sessionId))
|
||||
{
|
||||
sessionId = Guid.NewGuid().ToString();
|
||||
}
|
||||
|
||||
if (!_sessions.TryGetValue(sessionId, out var session))
|
||||
{
|
||||
session = new ConversationSession
|
||||
{
|
||||
Id = sessionId,
|
||||
MaxMessages = Options.MaxMessages
|
||||
};
|
||||
_sessions[sessionId] = session;
|
||||
}
|
||||
|
||||
return session;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ClearSession(string sessionId)
|
||||
{
|
||||
lock (_sessionsLock)
|
||||
{
|
||||
if (_sessions.TryGetValue(sessionId, out var session))
|
||||
{
|
||||
session.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<ConversationSession> GetActiveSessions()
|
||||
{
|
||||
lock (_sessionsLock)
|
||||
{
|
||||
return _sessions.Values.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void CleanupOldSessions(int maxAgeHours = 24)
|
||||
{
|
||||
lock (_sessionsLock)
|
||||
{
|
||||
var cutoffTime = DateTime.Now.AddHours(-maxAgeHours);
|
||||
var sessionsToRemove = _sessions.Values
|
||||
.Where(s => s.LastUpdated < cutoffTime)
|
||||
.Select(s => s.Id)
|
||||
.ToList();
|
||||
|
||||
foreach (var sessionId in sessionsToRemove)
|
||||
{
|
||||
_sessions.Remove(sessionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string ParseStreamingResponse(string json)
|
||||
{
|
||||
try
|
||||
{
|
||||
var doc = JsonDocument.Parse(json);
|
||||
var root = doc.RootElement;
|
||||
|
||||
if (!root.TryGetProperty("choices", out var choices) || choices.GetArrayLength() == 0)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var firstChoice = choices[0];
|
||||
|
||||
if (!firstChoice.TryGetProperty("delta", out var delta))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
if (delta.TryGetProperty("content", out var contentElement))
|
||||
{
|
||||
return contentElement.GetString() ?? string.Empty;
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for configuring AIChatService in the dependency injection container.
|
||||
/// </summary>
|
||||
public static class AIChatServiceExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds the AIChatService to the service collection with the specified configuration.
|
||||
/// </summary>
|
||||
/// <param name="services">The service collection.</param>
|
||||
/// <param name="configureOptions">The action to configure the AIChatService options.</param>
|
||||
/// <returns>The updated service collection.</returns>
|
||||
public static IServiceCollection AddAIChatService(this IServiceCollection services, Action<AIChatServiceOptions> configureOptions)
|
||||
{
|
||||
if (services == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(services));
|
||||
}
|
||||
|
||||
if (configureOptions == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(configureOptions));
|
||||
}
|
||||
|
||||
services.Configure(configureOptions);
|
||||
services.AddScoped<IAIChatService, AIChatService>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the AIChatService to the service collection with default options.
|
||||
/// </summary>
|
||||
/// <param name="services">The service collection.</param>
|
||||
/// <returns>The updated service collection.</returns>
|
||||
public static IServiceCollection AddAIChatService(this IServiceCollection services)
|
||||
{
|
||||
services.AddOptions<AIChatServiceOptions>();
|
||||
services.AddScoped<IAIChatService, AIChatService>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
138
Radzen.Blazor/AutoCompleteType.cs
Normal file
138
Radzen.Blazor/AutoCompleteType.cs
Normal file
@@ -0,0 +1,138 @@
|
||||
namespace Radzen.Blazor
|
||||
{
|
||||
/// <summary>
|
||||
/// The <c>AutomCompleteType</c> is a string-associated enum of
|
||||
/// browser-supported autocomplete attribute values.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This class lists the autocomplete attirbute options allowing
|
||||
/// developers to provide the browser with guidance on how to pre-populate
|
||||
/// the form fields. It is a class rather than a simpler enum to associate
|
||||
/// each option with the string the browser expects. For more information
|
||||
/// please review the list of options (https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/autocomplete)
|
||||
/// and the spec (https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofill).
|
||||
/// </remarks>
|
||||
public enum AutoCompleteType
|
||||
{
|
||||
/// <summary>Autocomplete is disabled. </summary>
|
||||
Off,
|
||||
/// <summary>Autocomplete is enabled. The browser chooses what values to suggest. </summary>
|
||||
On,
|
||||
/// <summary>The field expects the value to be a person's full name.</summary>
|
||||
Name,
|
||||
/// <summary>The prefix or title, such as "Mrs.", "Mr.", "Miss", "Ms.", "Dr." etc.</summary>
|
||||
HonorificPrefix,
|
||||
/// <summary>The given (or "first") name.</summary>
|
||||
GivenName,
|
||||
/// <summary>The middle name.</summary>
|
||||
AdditionalName,
|
||||
/// <summary>The family (or "last") name.</summary>
|
||||
FamilyName,
|
||||
/// <summary>The suffix, such as "Jr.", "B.Sc.", "PhD.", "MBASW", etc.</summary>
|
||||
HonorificSuffix,
|
||||
/// <summary>The nickname or handle.</summary>
|
||||
Nickname,
|
||||
/// <summary>The email address.</summary>
|
||||
Email,
|
||||
/// <summary>The username or account name.</summary>
|
||||
Username,
|
||||
/// <summary>A new password. When creating a new account or changing passwords.</summary>
|
||||
NewPassword,
|
||||
/// <summary>A current password. When filling in an existing password.</summary>
|
||||
CurrentPassword,
|
||||
/// <summary>A one-time code used for verifying user identity.</summary>
|
||||
OneTimeCode,
|
||||
/// <summary>A job title, or the title a person has within an organization, such as "Senior Technical Writer", "President", or "Assistant Troop Leader".</summary>
|
||||
OrganizationTitle,
|
||||
/// <summary>A company, business, or organization name.</summary>
|
||||
Organization,
|
||||
/// <summary>A street address. Use multiple address lines when more space is needed.</summary>
|
||||
StreetAddress,
|
||||
/// <summary>The line 1 of a street address. For example, "1234 Main Street".</summary>
|
||||
AddressLine1,
|
||||
/// <summary>The line 2 of a street address. For example, "Apartment 123".</summary>
|
||||
AddressLine2,
|
||||
/// <summary>The line 3 of a street address. For example, "c/o Jane Doe".</summary>
|
||||
AddressLine3,
|
||||
/// <summary>The city or locality.</summary>
|
||||
AddressLevel1,
|
||||
/// <summary>The state, province, prefecture, or region.</summary>
|
||||
AddressLevel2,
|
||||
/// <summary>The zip code or postal code.</summary>
|
||||
AddressLevel3,
|
||||
/// <summary>The country name.</summary>
|
||||
AddressLevel4,
|
||||
/// <summary>The country code.</summary>
|
||||
Country,
|
||||
/// <summary>The country name.</summary>
|
||||
CountryName,
|
||||
/// <summary>The postal code.</summary>
|
||||
PostalCode,
|
||||
/// <summary>The full name as printed on or associated with a payment instrument such as a credit card.</summary>
|
||||
CcName,
|
||||
/// <summary>The given (or "first") name as printed on or associated with a payment instrument such as a credit card.</summary>
|
||||
CcGivenName,
|
||||
/// <summary>The middle name as printed on or associated with a payment instrument such as a credit card.</summary>
|
||||
CcAdditionalName,
|
||||
/// <summary>The family (or "last") name as printed on or associated with a payment instrument such as a credit card.</summary>
|
||||
CcFamilyName,
|
||||
/// <summary>A credit card number or other number identifying a payment method, such as an account number.</summary>
|
||||
CcNumber,
|
||||
/// <summary>A payment method expiration date, typically in the form "MM/YY" or "MM/YYYY".</summary>
|
||||
CcExp,
|
||||
/// <summary>A payment method expiration month, typically in numeric form (MM).</summary>
|
||||
CcExpMonth,
|
||||
/// <summary>A payment method expiration year, typically in numeric form (YYYY).</summary>
|
||||
CcExpYear,
|
||||
/// <summary>The security code for your payment method, such as the CVV code.</summary>
|
||||
CcCsc,
|
||||
/// <summary>The type of payment instrument, such as "Visa", "Master Card", "Checking", or "Savings".</summary>
|
||||
CcType,
|
||||
/// <summary>The currency in which the transaction was completed. Use the ISO 4217 currency codes, such as "USD" for the US dollar.</summary>
|
||||
TransactionCurrency,
|
||||
/// <summary>The amount, in the currency specified by the transaction currency attribute, of the transaction completed.</summary>
|
||||
TransactionAmount,
|
||||
/// <summary>The language in which the transaction was completed. Use the relevant BCP 47 language tag.</summary>
|
||||
Language,
|
||||
/// <summary>A birth date, as a full date.</summary>
|
||||
Bday,
|
||||
/// <summary>The day of the month of a birth date.</summary>
|
||||
BdayDay,
|
||||
/// <summary>The month of the year of a birth date.</summary>
|
||||
BdayMonth,
|
||||
/// <summary>The year of a birth date.</summary>
|
||||
BdayYear,
|
||||
/// <summary>A gender identity (such as "Female", "Fa'afafine", "Hijra", "Male", "Nonbinary"), as freeform text without newlines.</summary>
|
||||
Sex,
|
||||
/// <summary>A full telephone number, including the country code. </summary>
|
||||
Tel,
|
||||
/// <summary>A country code, such as "1" for the United States, Canada, and other areas in North America and parts of the Caribbean.</summary>
|
||||
TelCountryCode,
|
||||
/// <summary>The entire phone number without the country code component, including a country-internal prefix.</summary>
|
||||
TelNational,
|
||||
/// <summary>The area code, with any country-internal prefix applied if appropriate.</summary>
|
||||
TelAreaCode,
|
||||
/// <summary>The phone number without the country or area code.</summary>
|
||||
TelLocal,
|
||||
/// <summary>The extension number, if applicable.</summary>
|
||||
TelExtension,
|
||||
/// <summary>A URL for an instant messaging protocol endpoint, such as "xmpp:username@example.net".</summary>
|
||||
Impp,
|
||||
/// <summary>A URL, such as a home page or company website address as appropriate given the context of the other fields in the form.</summary>
|
||||
Url,
|
||||
/// <summary>The URL of an image representing the person, company, or contact information given in the other fields in the form.</summary>
|
||||
Photo,
|
||||
/// <summary>State.</summary>
|
||||
State,
|
||||
/// <summary>Province.</summary>
|
||||
Province,
|
||||
/// <summary>Zip code.</summary>
|
||||
ZipCode,
|
||||
/// <summary>Firs name.</summary>
|
||||
FirstName,
|
||||
/// <summary>Middle name.</summary>
|
||||
MiddleName,
|
||||
/// <summary>Last name.</summary>
|
||||
LastName,
|
||||
}
|
||||
}
|
||||
@@ -101,6 +101,17 @@ namespace Radzen.Blazor
|
||||
/// <value><c>true</c> if visible; otherwise, <c>false</c>.</value>
|
||||
[Parameter]
|
||||
public bool Visible { get; set; } = true;
|
||||
/// <summary>
|
||||
/// Specifies the label rotation angle in degrees. Set to <c>null</c> by default which means no rotation is applied. Has higher precedence than <see cref="LabelAutoRotation"/>.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public double? LabelRotation { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the automatic label rotation angle in degrees. If set RadzenChart will automatically rotate the labels to fit the available space by the specified value. Has lower precedence than <see cref="LabelRotation"/>.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public double? LabelAutoRotation { get; set; } = null;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override bool ShouldRefreshChart(ParameterView parameters)
|
||||
@@ -108,6 +119,8 @@ namespace Radzen.Blazor
|
||||
return DidParameterChange(parameters, nameof(Min), Min) ||
|
||||
DidParameterChange(parameters, nameof(Max), Max) ||
|
||||
DidParameterChange(parameters, nameof(Visible), Visible) ||
|
||||
DidParameterChange(parameters, nameof(LabelRotation), LabelRotation) ||
|
||||
DidParameterChange(parameters, nameof(LabelAutoRotation), LabelAutoRotation) ||
|
||||
DidParameterChange(parameters, nameof(Step), Step);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,10 +2,9 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using System.Linq;
|
||||
using System.Linq.Dynamic.Core;
|
||||
using Radzen.Blazor.Rendering;
|
||||
using System.Threading.Tasks;
|
||||
using System.Collections;
|
||||
using System.Net.Mime;
|
||||
using Microsoft.AspNetCore.Components.Rendering;
|
||||
|
||||
namespace Radzen.Blazor
|
||||
@@ -16,22 +15,34 @@ namespace Radzen.Blazor
|
||||
/// <typeparam name="TItem">The type of the series data.</typeparam>
|
||||
public abstract class CartesianSeries<TItem> : RadzenChartComponentBase, IChartSeries, IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Cache for the value returned by <see cref="Category"/> when that value is only dependent on
|
||||
/// <see cref="CategoryProperty"/>.
|
||||
/// </summary>
|
||||
Func<TItem, double> categoryPropertyCache;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a getter function that returns a value from the specified category scale for the specified data item.
|
||||
/// </summary>
|
||||
/// <param name="scale">The scale.</param>
|
||||
internal Func<TItem, double> Category(ScaleBase scale)
|
||||
{
|
||||
if (categoryPropertyCache != null)
|
||||
{
|
||||
return categoryPropertyCache;
|
||||
}
|
||||
|
||||
if (IsNumeric(CategoryProperty))
|
||||
{
|
||||
return PropertyAccess.Getter<TItem, double>(CategoryProperty);
|
||||
categoryPropertyCache = PropertyAccess.Getter<TItem, double>(CategoryProperty);
|
||||
return categoryPropertyCache;
|
||||
}
|
||||
|
||||
if (IsDate(CategoryProperty))
|
||||
{
|
||||
var category = PropertyAccess.Getter<TItem, DateTime>(CategoryProperty);
|
||||
|
||||
return (item) => category(item).Ticks;
|
||||
categoryPropertyCache = (item) => category(item).Ticks;
|
||||
return categoryPropertyCache;
|
||||
}
|
||||
|
||||
if (scale is OrdinalScale ordinal)
|
||||
@@ -80,6 +91,12 @@ namespace Radzen.Blazor
|
||||
throw new ArgumentException($"Property {propertyName} does not exist");
|
||||
}
|
||||
|
||||
#if NET6_0_OR_GREATER
|
||||
if(PropertyAccess.IsDateOnly(property))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
return PropertyAccess.IsDate(property);
|
||||
}
|
||||
|
||||
@@ -351,6 +368,7 @@ namespace Radzen.Blazor
|
||||
var shouldRefresh = parameters.DidParameterChange(nameof(Data), Data);
|
||||
var visibleChanged = parameters.DidParameterChange(nameof(Visible), Visible);
|
||||
var hiddenChanged = parameters.DidParameterChange(nameof(Hidden), Hidden);
|
||||
var categoryChanged = parameters.DidParameterChange(nameof(CategoryProperty), CategoryProperty);
|
||||
|
||||
await base.SetParametersAsync(parameters);
|
||||
|
||||
@@ -366,6 +384,11 @@ namespace Radzen.Blazor
|
||||
shouldRefresh = true;
|
||||
}
|
||||
|
||||
if (categoryChanged || shouldRefresh)
|
||||
{
|
||||
categoryPropertyCache = null;
|
||||
}
|
||||
|
||||
if (Data != null && Data.Count() != Items.Count)
|
||||
{
|
||||
shouldRefresh = true;
|
||||
@@ -454,28 +477,74 @@ namespace Radzen.Blazor
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual RenderFragment RenderTooltip(object data, double marginLeft, double marginTop)
|
||||
public virtual RenderFragment RenderTooltip(object data)
|
||||
{
|
||||
var item = (TItem)data;
|
||||
|
||||
return builder =>
|
||||
{
|
||||
if (Chart.Tooltip.Shared)
|
||||
{
|
||||
var category = PropertyAccess.GetValue(item, CategoryProperty);
|
||||
builder.OpenComponent<ChartSharedTooltip>(0);
|
||||
builder.AddAttribute(1, nameof(ChartSharedTooltip.Class), TooltipClass(item));
|
||||
builder.AddAttribute(2, nameof(ChartSharedTooltip.Title), TooltipTitle(item));
|
||||
builder.AddAttribute(3, nameof(ChartSharedTooltip.ChildContent), RenderSharedTooltipItems(category));
|
||||
builder.CloseComponent();
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.OpenComponent<ChartTooltip>(0);
|
||||
builder.AddAttribute(1, nameof(ChartTooltip.ChildContent), TooltipTemplate?.Invoke(item));
|
||||
builder.AddAttribute(2, nameof(ChartTooltip.Title), TooltipTitle(item));
|
||||
builder.AddAttribute(3, nameof(ChartTooltip.Label), TooltipLabel(item));
|
||||
builder.AddAttribute(4, nameof(ChartTooltip.Value), TooltipValue(item));
|
||||
builder.AddAttribute(5, nameof(ChartTooltip.Class), TooltipClass(item));
|
||||
builder.AddAttribute(6, nameof(ChartTooltip.Style), TooltipStyle(item));
|
||||
builder.CloseComponent();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private RenderFragment RenderSharedTooltipItems(object category)
|
||||
{
|
||||
return builder =>
|
||||
{
|
||||
var visibleSeries = Chart.Series.Where(s => s.Visible).ToList();
|
||||
|
||||
foreach (var series in visibleSeries)
|
||||
{
|
||||
builder.AddContent(1, series.RenderSharedTooltipItem(category));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual RenderFragment RenderSharedTooltipItem(object category)
|
||||
{
|
||||
return builder =>
|
||||
{
|
||||
var item = Items.FirstOrDefault(i => object.Equals(PropertyAccess.GetValue(i, CategoryProperty), category));
|
||||
|
||||
if (item != null)
|
||||
{
|
||||
builder.OpenComponent<ChartSharedTooltipItem>(0);
|
||||
builder.AddAttribute(1, nameof(ChartSharedTooltipItem.Value), TooltipValue(item));
|
||||
builder.AddAttribute(2, nameof(ChartSharedTooltipItem.ChildContent), TooltipTemplate?.Invoke(item));
|
||||
builder.AddAttribute(3, nameof(ChartSharedTooltipItem.LegendItem), RenderLegendItem(false));
|
||||
builder.CloseComponent();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Point GetTooltipPosition(object data)
|
||||
{
|
||||
var item = (TItem)data;
|
||||
var x = TooltipX(item);
|
||||
var y = TooltipY(item);
|
||||
|
||||
return builder =>
|
||||
{
|
||||
builder.OpenComponent<ChartTooltip>(0);
|
||||
builder.AddAttribute(1, nameof(ChartTooltip.X), x + marginLeft);
|
||||
builder.AddAttribute(2, nameof(ChartTooltip.Y), y + marginTop);
|
||||
|
||||
builder.AddAttribute(3, nameof(ChartTooltip.ChildContent), TooltipTemplate?.Invoke(item));
|
||||
|
||||
builder.AddAttribute(4, nameof(ChartTooltip.Title), TooltipTitle(item));
|
||||
builder.AddAttribute(5, nameof(ChartTooltip.Label), TooltipLabel(item));
|
||||
builder.AddAttribute(6, nameof(ChartTooltip.Value), TooltipValue(item));
|
||||
builder.AddAttribute(7, nameof(ChartTooltip.Class), TooltipClass(item));
|
||||
builder.AddAttribute(8, nameof(ChartTooltip.Style), TooltipStyle(item));
|
||||
builder.CloseComponent();
|
||||
};
|
||||
return new Point { X = x, Y = y };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -498,6 +567,14 @@ namespace Radzen.Blazor
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual RenderFragment RenderLegendItem()
|
||||
{
|
||||
return RenderLegendItem(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Renders the legend item for this series.
|
||||
/// </summary>
|
||||
protected virtual RenderFragment RenderLegendItem(bool clickable)
|
||||
{
|
||||
var style = new List<string>();
|
||||
|
||||
@@ -516,6 +593,7 @@ namespace Radzen.Blazor
|
||||
builder.AddAttribute(5, nameof(LegendItem.MarkerSize), MarkerSize);
|
||||
builder.AddAttribute(6, nameof(LegendItem.Text), GetTitle());
|
||||
builder.AddAttribute(7, nameof(LegendItem.Click), EventCallback.Factory.Create(this, OnLegendItemClick));
|
||||
builder.AddAttribute(8, nameof(LegendItem.Clickable), clickable);
|
||||
builder.CloseComponent();
|
||||
};
|
||||
}
|
||||
@@ -545,13 +623,13 @@ namespace Radzen.Blazor
|
||||
/// <inheritdoc />
|
||||
public double GetMean()
|
||||
{
|
||||
return Data.Select(e => Value(e)).Average();
|
||||
return Data.Select(e => Value(e)).DefaultIfEmpty(double.NaN).Average();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public double GetMode()
|
||||
{
|
||||
return Data.GroupBy(e => Value(e)).Select(g => new { Value = g.Key, Count = g.Count() }).OrderByDescending(e => e.Count).FirstOrDefault().Value;
|
||||
return Data.Any() ? Data.GroupBy(e => Value(e)).Select(g => new { Value = g.Key, Count = g.Count() }).OrderByDescending(e => e.Count).FirstOrDefault().Value : double.NaN;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -559,35 +637,38 @@ namespace Radzen.Blazor
|
||||
/// </summary>
|
||||
public (double a, double b) GetTrend()
|
||||
{
|
||||
double a, b;
|
||||
double a = double.NaN, b = double.NaN;
|
||||
|
||||
Func<TItem, double> X;
|
||||
Func<TItem, double> Y;
|
||||
if (Chart.ShouldInvertAxes())
|
||||
if (Data.Any())
|
||||
{
|
||||
X = e => Chart.CategoryScale.Scale(Value(e));
|
||||
Y = e => Chart.ValueScale.Scale(Category(Chart.ValueScale)(e));
|
||||
}
|
||||
else
|
||||
{
|
||||
X = e => Chart.CategoryScale.Scale(Category(Chart.CategoryScale)(e));
|
||||
Y = e => Chart.ValueScale.Scale(Value(e));
|
||||
}
|
||||
Func<TItem, double> X;
|
||||
Func<TItem, double> Y;
|
||||
if (Chart.ShouldInvertAxes())
|
||||
{
|
||||
X = e => Chart.CategoryScale.Scale(Value(e));
|
||||
Y = e => Chart.ValueScale.Scale(Category(Chart.ValueScale)(e));
|
||||
}
|
||||
else
|
||||
{
|
||||
X = e => Chart.CategoryScale.Scale(Category(Chart.CategoryScale)(e));
|
||||
Y = e => Chart.ValueScale.Scale(Value(e));
|
||||
}
|
||||
|
||||
var avgX = Data.Select(e => X(e)).Average();
|
||||
var avgY = Data.Select(e => Y(e)).Average();
|
||||
var sumXY = Data.Sum(e => (X(e) - avgX) * (Y(e) - avgY));
|
||||
if (Chart.ShouldInvertAxes())
|
||||
{
|
||||
var sumYSq = Data.Sum(e => (Y(e) - avgY) * (Y(e) - avgY));
|
||||
b = sumXY / sumYSq;
|
||||
a = avgX - b * avgY;
|
||||
}
|
||||
else
|
||||
{
|
||||
var sumXSq = Data.Sum(e => (X(e) - avgX) * (X(e) - avgX));
|
||||
b = sumXY / sumXSq;
|
||||
a = avgY - b * avgX;
|
||||
var avgX = Data.Select(e => X(e)).Average();
|
||||
var avgY = Data.Select(e => Y(e)).Average();
|
||||
var sumXY = Data.Sum(e => (X(e) - avgX) * (Y(e) - avgY));
|
||||
if (Chart.ShouldInvertAxes())
|
||||
{
|
||||
var sumYSq = Data.Sum(e => (Y(e) - avgY) * (Y(e) - avgY));
|
||||
b = sumXY / sumYSq;
|
||||
a = avgX - b * avgY;
|
||||
}
|
||||
else
|
||||
{
|
||||
var sumXSq = Data.Sum(e => (X(e) - avgX) * (X(e) - avgX));
|
||||
b = sumXY / sumXSq;
|
||||
a = avgY - b * avgX;
|
||||
}
|
||||
}
|
||||
|
||||
return (a, b);
|
||||
@@ -669,29 +750,32 @@ namespace Radzen.Blazor
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual object DataAt(double x, double y)
|
||||
public virtual (object, Point) DataAt(double x, double y)
|
||||
{
|
||||
if (Items.Any())
|
||||
{
|
||||
return Items.Select(item =>
|
||||
var retObject = Items.Select(item =>
|
||||
{
|
||||
var distance = Math.Abs(TooltipX(item) - x);
|
||||
return new { Item = item, Distance = distance };
|
||||
}).Aggregate((a, b) => a.Distance < b.Distance ? a : b).Item;
|
||||
|
||||
return (retObject,
|
||||
new Point() { X = TooltipX(retObject), Y = TooltipY(retObject)});
|
||||
}
|
||||
|
||||
return null;
|
||||
return (null, null);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual IEnumerable<ChartDataLabel> GetDataLabels(double offsetX, double offsetY)
|
||||
{
|
||||
var list = new List<ChartDataLabel>();
|
||||
|
||||
|
||||
foreach (var d in Data)
|
||||
{
|
||||
list.Add(new ChartDataLabel
|
||||
{
|
||||
list.Add(new ChartDataLabel
|
||||
{
|
||||
Position = new Point { X = TooltipX(d) + offsetX, Y = TooltipY(d) + offsetY },
|
||||
TextAnchor = "middle",
|
||||
Text = Chart.ValueAxis.Format(Chart.ValueScale, Value(d))
|
||||
@@ -707,16 +791,25 @@ namespace Radzen.Blazor
|
||||
/// <param name="index">The index.</param>
|
||||
/// <param name="colors">The colors.</param>
|
||||
/// <param name="defaultValue">The default value.</param>
|
||||
protected string PickColor(int index, IEnumerable<string> colors, string defaultValue = null)
|
||||
/// <param name="colorRange">The color range value.</param>
|
||||
/// <param name="value">The value of the item.</param>
|
||||
protected string PickColor(int index, IEnumerable<string> colors, string defaultValue = null, IList<SeriesColorRange> colorRange = null, double value = 0.0)
|
||||
{
|
||||
if (colors == null || !colors.Any())
|
||||
if (colorRange != null)
|
||||
{
|
||||
return defaultValue;
|
||||
var result = colorRange.Where(r => r.Min <= value && r.Max >= value).FirstOrDefault<SeriesColorRange>();
|
||||
return result != null ? result.Color : defaultValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (colors == null || !colors.Any())
|
||||
{
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
return colors.ElementAt(index % colors.Count());
|
||||
return colors.ElementAt(index % colors.Count());
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -65,11 +65,8 @@ namespace Radzen
|
||||
/// <param name="sender">The source of the event.</param>
|
||||
/// <param name="e">The <see cref="Microsoft.AspNetCore.Components.Routing.LocationChangedEventArgs"/> instance containing the event data.</param>
|
||||
private void UriHelper_OnLocationChanged(object sender, Microsoft.AspNetCore.Components.Routing.LocationChangedEventArgs e)
|
||||
{
|
||||
if (this.OnNavigate != null)
|
||||
{
|
||||
this.OnNavigate();
|
||||
}
|
||||
{
|
||||
this.OnNavigate?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -199,5 +196,30 @@ namespace Radzen
|
||||
/// </summary>
|
||||
/// <value>The value.</value>
|
||||
public object Value { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the icon.
|
||||
/// </summary>
|
||||
/// <value>The icon.</value>
|
||||
public string Icon { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the icon color.
|
||||
/// </summary>
|
||||
/// <value>The icon color.</value>
|
||||
public string IconColor { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the image.
|
||||
/// </summary>
|
||||
/// <value>The image.</value>
|
||||
public string Image { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the image style.
|
||||
/// </summary>
|
||||
/// <value>The image style.</value>
|
||||
public string ImageStyle { get; set; }
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this instance is disabled.
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if this instance is disabled; otherwise, <c>false</c>.</value>
|
||||
public bool Disabled { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
150
Radzen.Blazor/CookieThemeService.cs
Normal file
150
Radzen.Blazor/CookieThemeService.cs
Normal file
@@ -0,0 +1,150 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
namespace Radzen
|
||||
{
|
||||
/// <summary>
|
||||
/// Specifies the SameSite attribute for the cookie.
|
||||
/// </summary>
|
||||
public enum CookieSameSiteMode
|
||||
{
|
||||
/// <summary>
|
||||
/// No SameSite attribute.
|
||||
/// </summary>
|
||||
None,
|
||||
/// <summary>
|
||||
/// Lax SameSite attribute.
|
||||
/// </summary>
|
||||
Lax,
|
||||
/// <summary>
|
||||
/// Strict SameSite attribute.
|
||||
/// </summary>
|
||||
Strict
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Options for the <see cref="CookieThemeService" />.
|
||||
/// </summary>
|
||||
public class CookieThemeServiceOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the cookie name.
|
||||
/// </summary>
|
||||
public string Name { get; set; } = "Theme";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the cookie duration.
|
||||
/// </summary>
|
||||
public TimeSpan Duration { get; set; } = TimeSpan.FromDays(365);
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to use secure cookies.
|
||||
/// </summary>
|
||||
public bool IsSecure { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the SameSite attribute for the cookie.
|
||||
/// </summary>
|
||||
public CookieSameSiteMode? SameSite { get; set; } = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Persist the current theme in a cookie. Requires <see cref="ThemeService" /> to be registered in the DI container.
|
||||
/// </summary>
|
||||
public class CookieThemeService
|
||||
{
|
||||
private readonly CookieThemeServiceOptions options;
|
||||
private readonly IJSRuntime jsRuntime;
|
||||
private readonly ThemeService themeService;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CookieThemeService" /> class.
|
||||
/// </summary>
|
||||
public CookieThemeService(IJSRuntime jsRuntime, ThemeService themeService, IOptions<CookieThemeServiceOptions> options)
|
||||
{
|
||||
this.jsRuntime = jsRuntime;
|
||||
this.themeService = themeService;
|
||||
this.options = options.Value;
|
||||
|
||||
themeService.ThemeChanged += OnThemeChanged;
|
||||
|
||||
_ = InitializeAsync();
|
||||
}
|
||||
|
||||
private async Task InitializeAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var cookies = await jsRuntime.InvokeAsync<string>("eval", "document.cookie");
|
||||
|
||||
var themeCookie = cookies?.Split("; ").Select(x =>
|
||||
{
|
||||
var parts = x.Split("=");
|
||||
|
||||
return (Key: parts[0], Value: parts[1]);
|
||||
})
|
||||
.FirstOrDefault(x => x.Key == options.Name);
|
||||
|
||||
var theme = themeCookie?.Value;
|
||||
|
||||
if (!string.IsNullOrEmpty(theme) && themeService.Theme != theme)
|
||||
{
|
||||
themeService.SetTheme(theme);
|
||||
}
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private void OnThemeChanged()
|
||||
{
|
||||
var expiration = DateTime.Now.Add(options.Duration);
|
||||
var cookie = $"{options.Name}={themeService.Theme}; expires={expiration:R}; path=/";
|
||||
|
||||
if (options.SameSite.HasValue)
|
||||
{
|
||||
cookie += $"; SameSite={options.SameSite}";
|
||||
}
|
||||
|
||||
if (options.IsSecure)
|
||||
{
|
||||
cookie += "; Secure";
|
||||
}
|
||||
|
||||
_ = jsRuntime.InvokeVoidAsync("eval", $"document.cookie = \"{cookie}\"");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods to register the <see cref="CookieThemeService" />.
|
||||
/// </summary>
|
||||
public static class CookieThemeServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds the <see cref="CookieThemeService" /> to the service collection.
|
||||
/// </summary>
|
||||
public static IServiceCollection AddRadzenCookieThemeService(this IServiceCollection services)
|
||||
{
|
||||
services.AddOptions<CookieThemeServiceOptions>();
|
||||
services.AddScoped<CookieThemeService>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the <see cref="CookieThemeService" /> to the service collection with the specified configuration.
|
||||
/// </summary>
|
||||
public static IServiceCollection AddRadzenCookieThemeService(this IServiceCollection services, Action<CookieThemeServiceOptions> configure)
|
||||
{
|
||||
services.Configure(configure);
|
||||
services.AddScoped<CookieThemeService>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,13 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Linq.Dynamic.Core;
|
||||
using System.Linq.Expressions;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Forms;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
using Radzen.Blazor;
|
||||
using Radzen.Blazor.Rendering;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Radzen
|
||||
{
|
||||
@@ -104,13 +103,13 @@ namespace Radzen
|
||||
/// <summary>
|
||||
/// The value
|
||||
/// </summary>
|
||||
object _value;
|
||||
private T _value = default;
|
||||
/// <summary>
|
||||
/// Gets or sets the value.
|
||||
/// </summary>
|
||||
/// <value>The value.</value>
|
||||
[Parameter]
|
||||
public object Value
|
||||
public T Value
|
||||
{
|
||||
get
|
||||
{
|
||||
@@ -118,10 +117,14 @@ namespace Radzen
|
||||
}
|
||||
set
|
||||
{
|
||||
if (_value != value)
|
||||
if (value == null || value.Equals("null"))
|
||||
{
|
||||
_value = object.Equals(value, "null") ? null : value;
|
||||
_value = default;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!value.Equals(_value))
|
||||
_value = value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,7 +189,7 @@ namespace Radzen
|
||||
if (_data != value)
|
||||
{
|
||||
_view = null;
|
||||
_value = null;
|
||||
_value = default;
|
||||
_data = value;
|
||||
StateHasChanged();
|
||||
}
|
||||
@@ -213,6 +216,32 @@ namespace Radzen
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the search text
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string SearchText
|
||||
{
|
||||
get
|
||||
{
|
||||
return searchText;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (searchText != value)
|
||||
{
|
||||
searchText = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the search text changed.
|
||||
/// </summary>
|
||||
/// <value>The search text changed.</value>
|
||||
[Parameter]
|
||||
public EventCallback<string> SearchTextChanged { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The search text
|
||||
/// </summary>
|
||||
@@ -234,23 +263,7 @@ namespace Radzen
|
||||
{
|
||||
if (!string.IsNullOrEmpty(searchText))
|
||||
{
|
||||
var ignoreCase = FilterCaseSensitivity == FilterCaseSensitivity.CaseInsensitive;
|
||||
|
||||
var query = new List<string>();
|
||||
|
||||
if (!string.IsNullOrEmpty(TextProperty))
|
||||
{
|
||||
query.Add(TextProperty);
|
||||
}
|
||||
|
||||
if (ignoreCase)
|
||||
{
|
||||
query.Add("ToLower()");
|
||||
}
|
||||
|
||||
query.Add($"{Enum.GetName(typeof(StringFilterOperator), FilterOperator)}(@0)");
|
||||
|
||||
_view = Query.Where(String.Join(".", query), ignoreCase ? searchText.ToLower() : searchText);
|
||||
_view = Query.Where(TextProperty, searchText, FilterOperator, FilterCaseSensitivity);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -262,6 +275,8 @@ namespace Radzen
|
||||
}
|
||||
}
|
||||
|
||||
internal IEnumerable GetView() => View;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the edit context.
|
||||
/// </summary>
|
||||
@@ -273,7 +288,8 @@ namespace Radzen
|
||||
/// Gets the field identifier.
|
||||
/// </summary>
|
||||
/// <value>The field identifier.</value>
|
||||
public FieldIdentifier FieldIdentifier { get; private set; }
|
||||
[Parameter]
|
||||
public FieldIdentifier FieldIdentifier { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the value expression.
|
||||
@@ -288,21 +304,33 @@ namespace Radzen
|
||||
/// <returns>A Task representing the asynchronous operation.</returns>
|
||||
public override async Task SetParametersAsync(ParameterView parameters)
|
||||
{
|
||||
// check for changes before setting the properties through the base call
|
||||
var dataChanged = parameters.DidParameterChange(nameof(Data), Data);
|
||||
var disabledChanged = parameters.DidParameterChange(nameof(Disabled), Disabled);
|
||||
|
||||
// allow the base class to process parameters and set the properties
|
||||
// after this call the parameters object should be considered stale
|
||||
await base.SetParametersAsync(parameters);
|
||||
|
||||
// handle changes
|
||||
if (dataChanged)
|
||||
{
|
||||
await OnDataChanged();
|
||||
}
|
||||
|
||||
var result = base.SetParametersAsync(parameters);
|
||||
|
||||
if (EditContext != null && ValueExpression != null && FieldIdentifier.Model != EditContext.Model)
|
||||
if (EditContext != null && (ValueExpression != null || ValueChanged.HasDelegate) && FieldIdentifier.Model != EditContext.Model)
|
||||
{
|
||||
FieldIdentifier = FieldIdentifier.Create(ValueExpression);
|
||||
FieldIdentifier = ValueExpression != null
|
||||
? FieldIdentifier.Create(ValueExpression)
|
||||
: FieldIdentifier.Create(() => Value);
|
||||
EditContext.OnValidationStateChanged -= ValidationStateChanged;
|
||||
EditContext.OnValidationStateChanged += ValidationStateChanged;
|
||||
}
|
||||
|
||||
await result;
|
||||
if (disabledChanged)
|
||||
{
|
||||
FormFieldContext?.DisabledChanged(Disabled);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -334,7 +362,7 @@ namespace Radzen
|
||||
/// Gets the value.
|
||||
/// </summary>
|
||||
/// <returns>System.Object.</returns>
|
||||
public object GetValue()
|
||||
public virtual object GetValue()
|
||||
{
|
||||
return Value;
|
||||
}
|
||||
@@ -347,6 +375,35 @@ namespace Radzen
|
||||
/// <returns>ClassList.</returns>
|
||||
protected ClassList GetClassList(string className) => ClassList.Create(className)
|
||||
.AddDisabled(Disabled)
|
||||
.Add(FieldIdentifier, EditContext);
|
||||
.Add(FieldIdentifier, EditContext)
|
||||
.Add("rz-state-empty", !HasValue);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual async ValueTask FocusAsync()
|
||||
{
|
||||
await Element.FocusAsync();
|
||||
}
|
||||
|
||||
/// <summary> Provides support for RadzenFormField integration. </summary>
|
||||
[CascadingParameter]
|
||||
public IFormFieldContext FormFieldContext { get; set; }
|
||||
|
||||
/// <summary> Gets the current placeholder. Returns empty string if this component is inside a RadzenFormField.</summary>
|
||||
protected string CurrentPlaceholder => FormFieldContext?.AllowFloatingLabel == true ? " " : Placeholder;
|
||||
|
||||
/// <summary>
|
||||
/// Handles the <see cref="E:ContextMenu" /> event.
|
||||
/// </summary>
|
||||
/// <param name="args">The <see cref="MouseEventArgs"/> instance containing the event data.</param>
|
||||
/// <returns>Task.</returns>
|
||||
public override Task OnContextMenu(MouseEventArgs args)
|
||||
{
|
||||
if (!Disabled)
|
||||
{
|
||||
return base.OnContextMenu(args);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
134
Radzen.Blazor/DynamicExtensions.cs
Normal file
134
Radzen.Blazor/DynamicExtensions.cs
Normal file
@@ -0,0 +1,134 @@
|
||||
using Radzen;
|
||||
using System.Linq.Expressions;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace System.Linq.Dynamic.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Class DynamicExtensions used to replace System.Linq.Dynamic.Core library.
|
||||
/// </summary>
|
||||
public static class DynamicExtensions
|
||||
{
|
||||
static readonly Func<string, Type> typeLocator = type => AppDomain.CurrentDomain.GetAssemblies()
|
||||
.SelectMany(a => a.GetTypes()).FirstOrDefault(t => t.FullName.Replace("+", ".") == type);
|
||||
|
||||
/// <summary>
|
||||
/// Filters using the specified filter descriptors.
|
||||
/// </summary>
|
||||
public static IQueryable<T> Where<T>(
|
||||
this IQueryable<T> source,
|
||||
string predicate,
|
||||
object[] parameters = null, object[] otherParameters = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (parameters != null && !string.IsNullOrEmpty(predicate))
|
||||
{
|
||||
predicate = Regex.Replace(predicate, @"@(\d+)", match =>
|
||||
{
|
||||
int index = int.Parse(match.Groups[1].Value);
|
||||
if (index >= parameters.Length)
|
||||
throw new InvalidOperationException($"No parameter provided for {match.Value}");
|
||||
|
||||
return ExpressionSerializer.FormatValue(parameters[index]);
|
||||
});
|
||||
}
|
||||
|
||||
predicate = (predicate == "true" ? "" : predicate)
|
||||
.Replace("DateTime(", "DateTime.Parse(")
|
||||
.Replace("DateTimeOffset(", "DateTimeOffset.Parse(")
|
||||
.Replace("DateOnly(", "DateOnly.Parse(")
|
||||
.Replace("Guid(", "Guid.Parse(")
|
||||
.Replace(" = ", " == ");
|
||||
|
||||
return !string.IsNullOrEmpty(predicate) ?
|
||||
source.Where(ExpressionParser.ParsePredicate<T>(predicate, typeLocator)) : source;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new InvalidOperationException($"Invalid predicate: {predicate}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sorts the elements of a sequence in ascending or descending order according to a key.
|
||||
/// </summary>
|
||||
public static IOrderedQueryable<T> OrderBy<T>(
|
||||
this IQueryable<T> source,
|
||||
string selector,
|
||||
object[] parameters = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
return QueryableExtension.OrderBy(source, selector);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new InvalidOperationException($"Invalid selector: {selector}.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Projects each element of a sequence into a collection of property values.
|
||||
/// </summary>
|
||||
public static IQueryable Select<T>(this IQueryable<T> source, string selector, object[] parameters = null)
|
||||
{
|
||||
if (source.ElementType == typeof(object))
|
||||
{
|
||||
var elementType = source.ElementType;
|
||||
|
||||
if (source.Expression is MethodCallExpression methodCall && methodCall.Method.Name == "Cast")
|
||||
{
|
||||
elementType = methodCall.Arguments[0].Type.GetGenericArguments().FirstOrDefault() ?? typeof(object);
|
||||
}
|
||||
else if (typeof(EnumerableQuery).IsAssignableFrom(source.GetType()))
|
||||
{
|
||||
elementType = source.FirstOrDefault()?.GetType() ?? typeof(object);
|
||||
}
|
||||
|
||||
return source.Cast(elementType).Select(selector, expression => ExpressionParser.ParseLambda(expression, elementType));
|
||||
}
|
||||
|
||||
return source.Select(selector, expression => ExpressionParser.ParseLambda<T>(expression));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Projects each element of a sequence into a collection of property values.
|
||||
/// </summary>
|
||||
public static IQueryable Select(this IQueryable source, string selector, object[] parameters = null)
|
||||
{
|
||||
return source.Select(selector, expression => ExpressionParser.ParseLambda(expression, source.ElementType));
|
||||
}
|
||||
|
||||
private static IQueryable Select(this IQueryable source, string selector, Func<string, LambdaExpression> lambdaCreator)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(selector))
|
||||
{
|
||||
return source;
|
||||
}
|
||||
|
||||
if (!selector.Contains("=>"))
|
||||
{
|
||||
var properties = selector
|
||||
.Replace("new (", "").Replace(")", "").Replace("new {", "").Replace("}", "").Trim()
|
||||
.Split(",", StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
selector = string.Join(", ", properties
|
||||
.Select(s => (s.Contains(" as ") ? s.Split(" as ").LastOrDefault().Trim().Replace(".", "_") : s.Trim().Replace(".", "_")) +
|
||||
" = " + $"it.{s.Split(" as ").FirstOrDefault().Replace(".", "?.").Trim()}"));
|
||||
}
|
||||
|
||||
var lambda = lambdaCreator(selector.Contains("=>") ? selector : $"it => new {{ {selector} }}");
|
||||
|
||||
return source.Provider.CreateQuery(Expression.Call(typeof(Queryable), nameof(Queryable.Select),
|
||||
[source.ElementType, lambda.Body.Type], source.Expression, Expression.Quote(lambda)));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new InvalidOperationException($"Invalid selector: {selector}.", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
56
Radzen.Blazor/DynamicTypeFactory.cs
Normal file
56
Radzen.Blazor/DynamicTypeFactory.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using System.Reflection.Emit;
|
||||
|
||||
static class DynamicTypeFactory
|
||||
{
|
||||
public static Type CreateType(string typeName, string[] propertyNames, Type[] propertyTypes)
|
||||
{
|
||||
if (propertyNames.Length != propertyTypes.Length)
|
||||
{
|
||||
throw new ArgumentException("Property names and types count mismatch.");
|
||||
}
|
||||
|
||||
var assemblyName = new AssemblyName("DynamicTypesAssembly");
|
||||
var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
|
||||
var moduleBuilder = assemblyBuilder.DefineDynamicModule("DynamicTypesModule");
|
||||
|
||||
var typeBuilder = moduleBuilder.DefineType(typeName, TypeAttributes.Public | TypeAttributes.Class | TypeAttributes.Sealed);
|
||||
|
||||
for (int i = 0; i < propertyNames.Length; i++)
|
||||
{
|
||||
var fieldBuilder = typeBuilder.DefineField("_" + propertyNames[i], propertyTypes[i], FieldAttributes.Private);
|
||||
var propertyBuilder = typeBuilder.DefineProperty(propertyNames[i], PropertyAttributes.None, propertyTypes[i], null);
|
||||
|
||||
var getterMethod = typeBuilder.DefineMethod(
|
||||
"get_" + propertyNames[i],
|
||||
MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig,
|
||||
propertyTypes[i],
|
||||
Type.EmptyTypes);
|
||||
|
||||
var getterIl = getterMethod.GetILGenerator();
|
||||
getterIl.Emit(OpCodes.Ldarg_0);
|
||||
getterIl.Emit(OpCodes.Ldfld, fieldBuilder);
|
||||
getterIl.Emit(OpCodes.Ret);
|
||||
|
||||
propertyBuilder.SetGetMethod(getterMethod);
|
||||
|
||||
var setterMethod = typeBuilder.DefineMethod(
|
||||
"set_" + propertyNames[i],
|
||||
MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig,
|
||||
null,
|
||||
[propertyTypes[i]]);
|
||||
|
||||
var setterIl = setterMethod.GetILGenerator();
|
||||
setterIl.Emit(OpCodes.Ldarg_0);
|
||||
setterIl.Emit(OpCodes.Ldarg_1);
|
||||
setterIl.Emit(OpCodes.Stfld, fieldBuilder);
|
||||
setterIl.Emit(OpCodes.Ret);
|
||||
|
||||
propertyBuilder.SetSetMethod(setterMethod);
|
||||
}
|
||||
|
||||
var dynamicType = typeBuilder.CreateType();
|
||||
return dynamicType;
|
||||
}
|
||||
}
|
||||
873
Radzen.Blazor/ExpressionLexer.cs
Normal file
873
Radzen.Blazor/ExpressionLexer.cs
Normal file
@@ -0,0 +1,873 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Globalization;
|
||||
using System.Linq.Expressions;
|
||||
using System.Text;
|
||||
|
||||
namespace Radzen;
|
||||
|
||||
class Token
|
||||
{
|
||||
public TokenType Type { get; set; }
|
||||
public string Value { get; set; } = string.Empty;
|
||||
public ValueKind ValueKind { get; set; } = ValueKind.None;
|
||||
public int IntValue { get; internal set; }
|
||||
public uint UintValue { get; internal set; }
|
||||
public long LongValue { get; internal set; }
|
||||
public ulong UlongValue { get; internal set; }
|
||||
public decimal DecimalValue { get; internal set; }
|
||||
public float FloatValue { get; internal set; }
|
||||
public double DoubleValue { get; internal set; }
|
||||
|
||||
public Token(TokenType type)
|
||||
{
|
||||
Type = type;
|
||||
}
|
||||
|
||||
public Token(TokenType type, string value)
|
||||
{
|
||||
Type = type;
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public ConstantExpression ToConstantExpression()
|
||||
{
|
||||
return ValueKind switch
|
||||
{
|
||||
ValueKind.Null => Expression.Constant(null),
|
||||
ValueKind.String => Expression.Constant(Value),
|
||||
ValueKind.Character => Expression.Constant(Value[0]),
|
||||
ValueKind.Int => Expression.Constant(IntValue),
|
||||
ValueKind.UInt => Expression.Constant(UintValue),
|
||||
ValueKind.Long => Expression.Constant(LongValue),
|
||||
ValueKind.ULong => Expression.Constant(UlongValue),
|
||||
ValueKind.Float => Expression.Constant(FloatValue),
|
||||
ValueKind.Double => Expression.Constant(DoubleValue),
|
||||
ValueKind.Decimal => Expression.Constant(DecimalValue),
|
||||
ValueKind.True => Expression.Constant(true),
|
||||
ValueKind.False => Expression.Constant(false),
|
||||
_ => throw new InvalidOperationException($"Unsupported value kind: {ValueKind}")
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
enum ValueKind
|
||||
{
|
||||
None,
|
||||
String,
|
||||
Int,
|
||||
Float,
|
||||
Double,
|
||||
Decimal,
|
||||
Character,
|
||||
Null,
|
||||
True,
|
||||
False,
|
||||
Long,
|
||||
UInt,
|
||||
ULong,
|
||||
}
|
||||
|
||||
|
||||
enum TokenType
|
||||
{
|
||||
None,
|
||||
Identifier,
|
||||
EqualsEquals,
|
||||
NotEquals,
|
||||
EqualsGreaterThan,
|
||||
StringLiteral,
|
||||
NumericLiteral,
|
||||
Dot,
|
||||
OpenParen,
|
||||
CloseParen,
|
||||
Comma,
|
||||
AmpersandAmpersand,
|
||||
Ampersand,
|
||||
BarBar,
|
||||
Bar,
|
||||
GreaterThan,
|
||||
LessThan,
|
||||
LessThanOrEqual,
|
||||
GreaterThanOrEqual,
|
||||
Plus,
|
||||
Minus,
|
||||
Star,
|
||||
Slash,
|
||||
CharacterLiteral,
|
||||
QuestionMark,
|
||||
QuestionMarkQuestionMark,
|
||||
Colon,
|
||||
QuestionDot,
|
||||
New,
|
||||
NullLiteral,
|
||||
TrueLiteral,
|
||||
FalseLiteral,
|
||||
OpenBracket,
|
||||
CloseBracket,
|
||||
OpenBrace,
|
||||
CloseBrace,
|
||||
ExclamationMark,
|
||||
Equals,
|
||||
Caret,
|
||||
GreaterThanGreaterThan,
|
||||
LessThanLessThan,
|
||||
}
|
||||
|
||||
static class TokenTypeExtensions
|
||||
{
|
||||
public static ExpressionType ToExpressionType(this TokenType tokenType)
|
||||
{
|
||||
return tokenType switch
|
||||
{
|
||||
TokenType.EqualsEquals => ExpressionType.Equal,
|
||||
TokenType.NotEquals => ExpressionType.NotEqual,
|
||||
TokenType.EqualsGreaterThan => ExpressionType.GreaterThanOrEqual,
|
||||
TokenType.AmpersandAmpersand => ExpressionType.AndAlso,
|
||||
TokenType.Ampersand => ExpressionType.And,
|
||||
TokenType.BarBar => ExpressionType.OrElse,
|
||||
TokenType.Bar => ExpressionType.Or,
|
||||
TokenType.GreaterThan => ExpressionType.GreaterThan,
|
||||
TokenType.LessThan => ExpressionType.LessThan,
|
||||
TokenType.LessThanOrEqual => ExpressionType.LessThanOrEqual,
|
||||
TokenType.GreaterThanOrEqual => ExpressionType.GreaterThanOrEqual,
|
||||
TokenType.Plus => ExpressionType.Add,
|
||||
TokenType.Minus => ExpressionType.Subtract,
|
||||
TokenType.Star => ExpressionType.Multiply,
|
||||
TokenType.Slash => ExpressionType.Divide,
|
||||
TokenType.Caret => ExpressionType.ExclusiveOr,
|
||||
TokenType.GreaterThanGreaterThan => ExpressionType.RightShift,
|
||||
TokenType.LessThanLessThan => ExpressionType.LeftShift,
|
||||
_ => throw new InvalidOperationException($"Unsupported token type: {tokenType}")
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class ExpressionLexer(string expression)
|
||||
{
|
||||
private int position;
|
||||
|
||||
private char Peek(int offset = 0)
|
||||
{
|
||||
if (position + offset >= expression.Length)
|
||||
{
|
||||
return '\0';
|
||||
}
|
||||
|
||||
return expression[position + offset];
|
||||
}
|
||||
|
||||
private void Advance(int count)
|
||||
{
|
||||
position += count;
|
||||
}
|
||||
|
||||
bool TryAdvance(char expected)
|
||||
{
|
||||
if (Peek(1) == expected)
|
||||
{
|
||||
Advance(1);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void ScanTrivia()
|
||||
{
|
||||
while (char.IsWhiteSpace(Peek()))
|
||||
{
|
||||
Advance(1);
|
||||
}
|
||||
}
|
||||
|
||||
public static List<Token> Scan(string expression)
|
||||
{
|
||||
var lexer = new ExpressionLexer(expression);
|
||||
|
||||
return [.. lexer.Scan()];
|
||||
}
|
||||
|
||||
public IEnumerable<Token> Scan()
|
||||
{
|
||||
while (position < expression.Length)
|
||||
{
|
||||
ScanTrivia();
|
||||
|
||||
var token = ScanToken();
|
||||
|
||||
if (token.Type == TokenType.None)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
yield return token;
|
||||
}
|
||||
|
||||
yield return new Token(TokenType.None, string.Empty);
|
||||
}
|
||||
|
||||
private Token ScanToken()
|
||||
{
|
||||
var ch = Peek();
|
||||
|
||||
switch (ch)
|
||||
{
|
||||
case '"':
|
||||
return ScanStringLiteral();
|
||||
case '\'':
|
||||
return ScanCharacterLiteral();
|
||||
case '=':
|
||||
if (TryAdvance('='))
|
||||
{
|
||||
Advance(1);
|
||||
return new Token(TokenType.EqualsEquals);
|
||||
}
|
||||
|
||||
if (TryAdvance('>'))
|
||||
{
|
||||
Advance(1);
|
||||
return new Token(TokenType.EqualsGreaterThan);
|
||||
}
|
||||
|
||||
Advance(1);
|
||||
return new Token(TokenType.Equals);
|
||||
case '!':
|
||||
if (TryAdvance('='))
|
||||
{
|
||||
Advance(1);
|
||||
return new Token(TokenType.NotEquals);
|
||||
}
|
||||
Advance(1);
|
||||
return new Token(TokenType.ExclamationMark);
|
||||
case '>':
|
||||
if (TryAdvance('='))
|
||||
{
|
||||
Advance(1);
|
||||
return new Token(TokenType.GreaterThanOrEqual);
|
||||
}
|
||||
if (TryAdvance('>'))
|
||||
{
|
||||
Advance(1);
|
||||
return new Token(TokenType.GreaterThanGreaterThan);
|
||||
}
|
||||
Advance(1);
|
||||
return new Token(TokenType.GreaterThan);
|
||||
case '<':
|
||||
if (TryAdvance('<'))
|
||||
{
|
||||
Advance(1);
|
||||
return new Token(TokenType.LessThanLessThan);
|
||||
}
|
||||
if (TryAdvance('='))
|
||||
{
|
||||
Advance(1);
|
||||
return new Token(TokenType.LessThanOrEqual);
|
||||
}
|
||||
Advance(1);
|
||||
return new Token(TokenType.LessThan);
|
||||
case '+':
|
||||
Advance(1);
|
||||
return new Token(TokenType.Plus);
|
||||
case '-':
|
||||
Advance(1);
|
||||
return new Token(TokenType.Minus);
|
||||
case '*':
|
||||
Advance(1);
|
||||
return new Token(TokenType.Star);
|
||||
case '/':
|
||||
Advance(1);
|
||||
return new Token(TokenType.Slash);
|
||||
case '.':
|
||||
Advance(1);
|
||||
return new Token(TokenType.Dot);
|
||||
case '(':
|
||||
Advance(1);
|
||||
return new Token(TokenType.OpenParen);
|
||||
case ')':
|
||||
Advance(1);
|
||||
return new Token(TokenType.CloseParen);
|
||||
case '[':
|
||||
Advance(1);
|
||||
return new Token(TokenType.OpenBracket);
|
||||
case ']':
|
||||
Advance(1);
|
||||
return new Token(TokenType.CloseBracket);
|
||||
case '{':
|
||||
Advance(1);
|
||||
return new Token(TokenType.OpenBrace);
|
||||
case '}':
|
||||
Advance(1);
|
||||
return new Token(TokenType.CloseBrace);
|
||||
case ',':
|
||||
Advance(1);
|
||||
return new Token(TokenType.Comma);
|
||||
case '&':
|
||||
if (TryAdvance('&'))
|
||||
{
|
||||
Advance(1);
|
||||
return new Token(TokenType.AmpersandAmpersand);
|
||||
}
|
||||
Advance(1);
|
||||
return new Token(TokenType.Ampersand);
|
||||
case '|':
|
||||
if (TryAdvance('|'))
|
||||
{
|
||||
Advance(1);
|
||||
return new Token(TokenType.BarBar);
|
||||
}
|
||||
Advance(1);
|
||||
return new Token(TokenType.Bar);
|
||||
case '?':
|
||||
if (TryAdvance('.'))
|
||||
{
|
||||
Advance(1);
|
||||
return new Token(TokenType.QuestionDot);
|
||||
}
|
||||
|
||||
if (TryAdvance('?'))
|
||||
{
|
||||
Advance(1);
|
||||
return new Token(TokenType.QuestionMarkQuestionMark);
|
||||
}
|
||||
|
||||
Advance(1);
|
||||
return new Token(TokenType.QuestionMark);
|
||||
case ':':
|
||||
Advance(1);
|
||||
return new Token(TokenType.Colon);
|
||||
case '^':
|
||||
Advance(1);
|
||||
return new Token(TokenType.Caret);
|
||||
case >= '0' and <= '9':
|
||||
return ScanNumericLiteral();
|
||||
case '_':
|
||||
case (>= 'a' and <= 'z') or (>= 'A' and <= 'Z'):
|
||||
var token = ScanIdentifier();
|
||||
|
||||
return token.Value switch
|
||||
{
|
||||
"null" => new Token(TokenType.NullLiteral) { ValueKind = ValueKind.Null },
|
||||
"true" => new Token(TokenType.TrueLiteral) { ValueKind = ValueKind.True },
|
||||
"false" => new Token(TokenType.FalseLiteral) { ValueKind = ValueKind.False },
|
||||
"new" => new Token(TokenType.New),
|
||||
_ => token
|
||||
};
|
||||
}
|
||||
|
||||
return new Token(TokenType.None, string.Empty);
|
||||
}
|
||||
|
||||
private char ScanEscapeSequence()
|
||||
{
|
||||
var ch = Peek();
|
||||
|
||||
Advance(1);
|
||||
|
||||
switch (ch)
|
||||
{
|
||||
case '\'':
|
||||
case '"':
|
||||
case '\\':
|
||||
break;
|
||||
case '0':
|
||||
ch = '\u0000';
|
||||
break;
|
||||
case 'a':
|
||||
ch = '\u0007';
|
||||
break;
|
||||
case 'b':
|
||||
ch = '\u0008';
|
||||
break;
|
||||
case 'f':
|
||||
ch = '\u000c';
|
||||
break;
|
||||
case 'n':
|
||||
ch = '\u000a';
|
||||
break;
|
||||
case 'r':
|
||||
ch = '\u000d';
|
||||
break;
|
||||
case 't':
|
||||
ch = '\u0009';
|
||||
break;
|
||||
case 'v':
|
||||
ch = '\u000b';
|
||||
break;
|
||||
case 'u':
|
||||
case 'U':
|
||||
case 'x':
|
||||
ch = ScanUnicodeEscapeSequence(ch);
|
||||
break;
|
||||
default:
|
||||
throw new InvalidOperationException($"Invalid escape sequence '\\{ch}' at position {position}.");
|
||||
}
|
||||
|
||||
return ch;
|
||||
}
|
||||
|
||||
private char ScanUnicodeEscapeSequence(char ch)
|
||||
{
|
||||
var value = 0;
|
||||
|
||||
var count = ch == 'U' ? 8 : 4;
|
||||
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var digit = Peek();
|
||||
|
||||
int digitValue;
|
||||
|
||||
if (digit >= '0' && digit <= '9')
|
||||
{
|
||||
digitValue = digit - '0';
|
||||
}
|
||||
else if (digit >= 'a' && digit <= 'f')
|
||||
{
|
||||
digitValue = digit - 'a' + 10;
|
||||
}
|
||||
else if (digit >= 'A' && digit <= 'F')
|
||||
{
|
||||
digitValue = digit - 'A' + 10;
|
||||
}
|
||||
else if (ch != 'x')
|
||||
{
|
||||
throw new InvalidOperationException($"Invalid unicode escape sequence at position {position}.");
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
value = (value << 4) + digitValue;
|
||||
|
||||
Advance(1);
|
||||
}
|
||||
|
||||
return (char)value;
|
||||
}
|
||||
|
||||
private Token ScanCharacterLiteral()
|
||||
{
|
||||
Advance(1);
|
||||
|
||||
var buffer = new StringBuilder();
|
||||
|
||||
while (true)
|
||||
{
|
||||
var ch = Peek();
|
||||
|
||||
switch (ch)
|
||||
{
|
||||
case '\0':
|
||||
throw new InvalidOperationException($"Unexpected end of character literal at position {position}.");
|
||||
case '\\':
|
||||
Advance(1);
|
||||
buffer.Append(ScanEscapeSequence());
|
||||
break;
|
||||
case '\'':
|
||||
Advance(1);
|
||||
|
||||
return new Token(TokenType.CharacterLiteral, buffer.ToString())
|
||||
{
|
||||
ValueKind = ValueKind.Character
|
||||
};
|
||||
default:
|
||||
if (buffer.Length > 0)
|
||||
{
|
||||
throw new InvalidOperationException($"Too many characters in character literal at position {position}.");
|
||||
}
|
||||
|
||||
buffer.Append(ch);
|
||||
Advance(1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Token ScanStringLiteral()
|
||||
{
|
||||
Advance(1);
|
||||
|
||||
var buffer = new StringBuilder();
|
||||
|
||||
while (true)
|
||||
{
|
||||
var ch = Peek();
|
||||
|
||||
switch (ch)
|
||||
{
|
||||
case '\0':
|
||||
throw new InvalidOperationException($"Unexpected end of string literal at position {position}.");
|
||||
case '\\':
|
||||
Advance(1);
|
||||
buffer.Append(ScanEscapeSequence());
|
||||
break;
|
||||
case '"':
|
||||
Advance(1);
|
||||
return new Token(TokenType.StringLiteral, buffer.ToString())
|
||||
{
|
||||
ValueKind = ValueKind.String
|
||||
};
|
||||
default:
|
||||
buffer.Append(ch);
|
||||
Advance(1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Token ScanNumericLiteral()
|
||||
{
|
||||
var buffer = new StringBuilder();
|
||||
var hasDecimal = false;
|
||||
var hasFSuffix = false;
|
||||
var hasDSuffix = false;
|
||||
var hasMSuffix = false;
|
||||
var hasLSuffix = false;
|
||||
var hasExponent = false;
|
||||
var hasHex = false;
|
||||
var hasUSuffix = false;
|
||||
|
||||
while (true)
|
||||
{
|
||||
var ch = Peek();
|
||||
|
||||
if (ch == '0')
|
||||
{
|
||||
var next = Peek(1);
|
||||
|
||||
if (next == 'x' || next == 'X')
|
||||
{
|
||||
hasHex = true;
|
||||
Advance(2);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (ch >= '0' && ch <= '9')
|
||||
{
|
||||
buffer.Append(ch);
|
||||
Advance(1);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch == '.')
|
||||
{
|
||||
if (hasDecimal)
|
||||
{
|
||||
throw new InvalidOperationException($"Unexpected character '{ch}' at position {position}.");
|
||||
}
|
||||
|
||||
hasDecimal = true;
|
||||
|
||||
buffer.Append(ch);
|
||||
|
||||
Advance(1);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch == 'l' || ch == 'L')
|
||||
{
|
||||
if (hasLSuffix)
|
||||
{
|
||||
throw new InvalidOperationException($"Unexpected character '{ch}' at position {position}.");
|
||||
}
|
||||
|
||||
hasLSuffix = true;
|
||||
|
||||
Advance(1);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch == 'u' || ch == 'U')
|
||||
{
|
||||
if (hasUSuffix)
|
||||
{
|
||||
throw new InvalidOperationException($"Unexpected character '{ch}' at position {position}.");
|
||||
}
|
||||
|
||||
hasUSuffix = true;
|
||||
|
||||
Advance(1);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (hasHex && ((ch >= 'a' && ch <= 'f') || (ch >= 'A' && ch <= 'F')))
|
||||
{
|
||||
buffer.Append(ch);
|
||||
Advance(1);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch == 'e' || ch == 'E')
|
||||
{
|
||||
if (hasExponent)
|
||||
{
|
||||
throw new InvalidOperationException($"Unexpected character '{ch}' at position {position}.");
|
||||
}
|
||||
|
||||
hasExponent = true;
|
||||
buffer.Append(ch);
|
||||
Advance(1);
|
||||
|
||||
// Check for optional + or - after e/E
|
||||
ch = Peek();
|
||||
if (ch == '+' || ch == '-')
|
||||
{
|
||||
buffer.Append(ch);
|
||||
Advance(1);
|
||||
}
|
||||
|
||||
// Must have at least one digit after e/E
|
||||
ch = Peek();
|
||||
|
||||
if (ch < '0' || ch > '9')
|
||||
{
|
||||
throw new InvalidOperationException($"Expected digit after exponent at position {position}.");
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (hasDecimal || hasExponent)
|
||||
{
|
||||
switch (ch)
|
||||
{
|
||||
case 'F':
|
||||
case 'f':
|
||||
hasFSuffix = true;
|
||||
Advance(1);
|
||||
break;
|
||||
case 'D':
|
||||
case 'd':
|
||||
hasDSuffix = true;
|
||||
Advance(1);
|
||||
break;
|
||||
case 'M':
|
||||
case 'm':
|
||||
hasMSuffix = true;
|
||||
Advance(1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
var value = new Token(TokenType.NumericLiteral);
|
||||
|
||||
var valueKind = ValueKind.None;
|
||||
|
||||
if (hasDecimal || hasExponent)
|
||||
{
|
||||
valueKind = ValueKind.Double;
|
||||
}
|
||||
|
||||
if (hasFSuffix)
|
||||
{
|
||||
valueKind = ValueKind.Float;
|
||||
}
|
||||
|
||||
if (hasDSuffix)
|
||||
{
|
||||
valueKind = ValueKind.Double;
|
||||
}
|
||||
|
||||
if (hasMSuffix)
|
||||
{
|
||||
valueKind = ValueKind.Decimal;
|
||||
}
|
||||
|
||||
switch (valueKind)
|
||||
{
|
||||
case ValueKind.Float:
|
||||
value.ValueKind = ValueKind.Float;
|
||||
value.FloatValue = GetValueFloat(buffer.ToString());
|
||||
break;
|
||||
case ValueKind.Double:
|
||||
value.ValueKind = ValueKind.Double;
|
||||
value.DoubleValue = GetValueDouble(buffer.ToString());
|
||||
break;
|
||||
case ValueKind.Decimal:
|
||||
value.ValueKind = ValueKind.Decimal;
|
||||
value.DecimalValue = GetValueDecimal(buffer.ToString());
|
||||
break;
|
||||
default:
|
||||
var val = GetValueUInt64(buffer.ToString(), hasHex);
|
||||
|
||||
if (!hasUSuffix && !hasLSuffix)
|
||||
{
|
||||
if (val <= Int32.MaxValue)
|
||||
{
|
||||
value.ValueKind = ValueKind.Int;
|
||||
value.IntValue = (int)val;
|
||||
}
|
||||
else if (val <= UInt32.MaxValue)
|
||||
{
|
||||
value.ValueKind = ValueKind.UInt;
|
||||
value.UintValue = (uint)val;
|
||||
}
|
||||
else if (val <= Int64.MaxValue)
|
||||
{
|
||||
value.ValueKind = ValueKind.Long;
|
||||
value.LongValue = (long)val;
|
||||
}
|
||||
else
|
||||
{
|
||||
value.ValueKind = ValueKind.ULong;
|
||||
value.UlongValue = val;
|
||||
}
|
||||
}
|
||||
else if (hasUSuffix && !hasLSuffix)
|
||||
{
|
||||
if (val <= UInt32.MaxValue)
|
||||
{
|
||||
value.ValueKind = ValueKind.UInt;
|
||||
value.UintValue = (uint)val;
|
||||
}
|
||||
else
|
||||
{
|
||||
value.ValueKind = ValueKind.ULong;
|
||||
value.UlongValue = val;
|
||||
}
|
||||
}
|
||||
else if (!hasUSuffix & hasLSuffix)
|
||||
{
|
||||
if (val <= Int64.MaxValue)
|
||||
{
|
||||
value.ValueKind = ValueKind.Long;
|
||||
value.LongValue = (long)val;
|
||||
}
|
||||
else
|
||||
{
|
||||
value.ValueKind = ValueKind.ULong;
|
||||
value.UlongValue = val;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
value.ValueKind = ValueKind.ULong;
|
||||
value.UlongValue = val;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
private static decimal GetValueDecimal(string text)
|
||||
{
|
||||
if (!decimal.TryParse(text, NumberStyles.AllowDecimalPoint | NumberStyles.AllowExponent, CultureInfo.InvariantCulture, out var result))
|
||||
{
|
||||
throw new InvalidOperationException($"Invalid numeric literal: {text}");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static float GetValueFloat(string text)
|
||||
{
|
||||
if (!float.TryParse(text, NumberStyles.AllowDecimalPoint | NumberStyles.AllowExponent, CultureInfo.InvariantCulture, out var result))
|
||||
{
|
||||
throw new InvalidOperationException($"Invalid numeric literal: {text}");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static double GetValueDouble(string text)
|
||||
{
|
||||
if (!double.TryParse(text, NumberStyles.AllowDecimalPoint | NumberStyles.AllowExponent, CultureInfo.InvariantCulture, out var result))
|
||||
{
|
||||
throw new InvalidOperationException($"Invalid numeric literal: {text}");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static ulong GetValueUInt64(string text, bool isHex)
|
||||
{
|
||||
if (!UInt64.TryParse(text, isHex ? NumberStyles.AllowHexSpecifier : NumberStyles.None, CultureInfo.InvariantCulture, out var result))
|
||||
{
|
||||
throw new InvalidOperationException($"Invalid numeric literal: {text}");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private Token ScanIdentifier()
|
||||
{
|
||||
var startOffset = position;
|
||||
|
||||
while (true)
|
||||
{
|
||||
if (position == expression.Length)
|
||||
{
|
||||
var length = position - startOffset;
|
||||
|
||||
return new Token(TokenType.Identifier, expression.Substring(startOffset, length));
|
||||
}
|
||||
|
||||
switch (Peek())
|
||||
{
|
||||
case '\0':
|
||||
case ' ':
|
||||
case '\r':
|
||||
case '\n':
|
||||
case '\t':
|
||||
case '!':
|
||||
case '%':
|
||||
case '(':
|
||||
case ')':
|
||||
case '*':
|
||||
case '+':
|
||||
case ',':
|
||||
case '-':
|
||||
case '.':
|
||||
case '/':
|
||||
case ':':
|
||||
case ';':
|
||||
case '<':
|
||||
case '=':
|
||||
case '>':
|
||||
case '?':
|
||||
case '[':
|
||||
case ']':
|
||||
case '^':
|
||||
case '{':
|
||||
case '|':
|
||||
case '}':
|
||||
case '~':
|
||||
case '"':
|
||||
case '\'':
|
||||
// All of the following characters are not valid in an
|
||||
// identifier. If we see any of them, then we know we're
|
||||
// done.
|
||||
return new Token(TokenType.Identifier, expression[startOffset..position]);
|
||||
case >= '0' and <= '9':
|
||||
if (position == startOffset)
|
||||
{
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
goto case '_';
|
||||
}
|
||||
case (>= 'a' and <= 'z') or (>= 'A' and <= 'Z'):
|
||||
case '_':
|
||||
// All of these characters are valid inside an identifier.
|
||||
// consume it and keep processing.
|
||||
Advance(1);
|
||||
continue;
|
||||
|
||||
default:
|
||||
throw new InvalidOperationException($"Unexpected character '{Peek()}' at position {position}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
919
Radzen.Blazor/ExpressionParser.cs
Normal file
919
Radzen.Blazor/ExpressionParser.cs
Normal file
@@ -0,0 +1,919 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Linq;
|
||||
|
||||
namespace Radzen;
|
||||
|
||||
#nullable enable
|
||||
|
||||
/// <summary>
|
||||
/// Parse lambda expressions from strings.
|
||||
/// </summary>
|
||||
public class ExpressionParser
|
||||
{
|
||||
/// <summary>
|
||||
/// Parses a lambda expression that returns a boolean value.
|
||||
/// </summary>
|
||||
public static Expression<Func<T, bool>> ParsePredicate<T>(string expression, Func<string, Type?>? typeResolver = null)
|
||||
{
|
||||
return ParseLambda<T, bool>(expression, typeResolver);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a lambda expression that returns a typed result.
|
||||
/// </summary>
|
||||
public static Expression<Func<T, TResult>> ParseLambda<T, TResult>(string expression, Func<string, Type?>? typeResolver = null)
|
||||
{
|
||||
var lambda = ParseLambda(expression, typeof(T), typeResolver);
|
||||
|
||||
return Expression.Lambda<Func<T, TResult>>(lambda.Body, lambda.Parameters[0]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a lambda expression that returns untyped result.
|
||||
/// </summary>
|
||||
public static LambdaExpression ParseLambda<T>(string expression, Func<string, Type?>? typeLocator = null)
|
||||
{
|
||||
return ParseLambda(expression, typeof(T), typeLocator);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a lambda expression that returns untyped result.
|
||||
/// </summary>
|
||||
public static LambdaExpression ParseLambda(string expression, Type type, Func<string, Type?>? typeResolver = null)
|
||||
{
|
||||
var parser = new ExpressionParser(expression, typeResolver);
|
||||
|
||||
return parser.ParseLambda(type);
|
||||
}
|
||||
|
||||
private readonly List<Token> tokens;
|
||||
private int position = 0;
|
||||
private readonly Func<string, Type?>? typeResolver;
|
||||
private readonly Stack<ParameterExpression> parameterStack = new();
|
||||
|
||||
private ExpressionParser(string expression, Func<string, Type?>? typeResolver = null)
|
||||
{
|
||||
this.typeResolver = typeResolver;
|
||||
tokens = ExpressionLexer.Scan(expression);
|
||||
}
|
||||
|
||||
Token Expect(TokenType type)
|
||||
{
|
||||
if (position >= tokens.Count)
|
||||
{
|
||||
throw new InvalidOperationException($"Unexpected end of expression. Expected token: {type}");
|
||||
}
|
||||
|
||||
var token = tokens[position];
|
||||
|
||||
if (token.Type != type)
|
||||
{
|
||||
throw new InvalidOperationException($"Unexpected token: {token.Type}. Expected: {type}");
|
||||
}
|
||||
|
||||
position++;
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
void Advance(int count)
|
||||
{
|
||||
position += count;
|
||||
}
|
||||
|
||||
Token Peek(int offset = 0)
|
||||
{
|
||||
if (position + offset >= tokens.Count)
|
||||
{
|
||||
return new Token(TokenType.None, string.Empty);
|
||||
}
|
||||
|
||||
return tokens[position + offset];
|
||||
}
|
||||
|
||||
private LambdaExpression ParseLambda(Type paramType)
|
||||
{
|
||||
var parameterIdentifier = Expect(TokenType.Identifier);
|
||||
|
||||
var parameter = Expression.Parameter(paramType, parameterIdentifier.Value);
|
||||
|
||||
parameterStack.Push(parameter);
|
||||
|
||||
Expect(TokenType.EqualsGreaterThan);
|
||||
|
||||
var body = ParseExpression(parameter);
|
||||
|
||||
parameterStack.Pop();
|
||||
|
||||
return Expression.Lambda(body, parameter);
|
||||
}
|
||||
|
||||
private Expression ParseExpression(ParameterExpression parameter)
|
||||
{
|
||||
var left = ParseBinary(parameter);
|
||||
var token = Peek();
|
||||
|
||||
if (token.Type is TokenType.AmpersandAmpersand)
|
||||
{
|
||||
Advance(1);
|
||||
|
||||
var right = ParseExpression(parameter) ?? throw new InvalidOperationException($"Expected expression after {token.Value} at position {position}");
|
||||
|
||||
left = Expression.AndAlso(left, right);
|
||||
}
|
||||
else if (token.Type is TokenType.BarBar)
|
||||
{
|
||||
Advance(1);
|
||||
|
||||
var right = ParseExpression(parameter) ?? throw new InvalidOperationException($"Expected expression after {token.Value} at position {position}");
|
||||
|
||||
left = Expression.OrElse(left, right);
|
||||
}
|
||||
|
||||
return left;
|
||||
}
|
||||
|
||||
private Expression ParseBinary(ParameterExpression parameter)
|
||||
{
|
||||
var left = ParseNullCoalescing(parameter);
|
||||
var token = Peek();
|
||||
|
||||
if (token.Type is TokenType.EqualsEquals or TokenType.NotEquals or TokenType.GreaterThan or TokenType.LessThan or TokenType.LessThanOrEqual or TokenType.GreaterThanOrEqual)
|
||||
{
|
||||
Advance(1);
|
||||
var right = ParseBinary(parameter) ?? throw new InvalidOperationException($"Expected expression after {token.Value} at position {position}");
|
||||
left = Expression.MakeBinary(token.Type.ToExpressionType(), left, ConvertIfNeeded(right, left.Type));
|
||||
}
|
||||
|
||||
return left;
|
||||
}
|
||||
|
||||
private Expression ParseNullCoalescing(ParameterExpression parameter)
|
||||
{
|
||||
var left = ParseTernary(parameter);
|
||||
var token = Peek();
|
||||
|
||||
while (token.Type == TokenType.QuestionMarkQuestionMark)
|
||||
{
|
||||
Advance(1);
|
||||
|
||||
var right = ParseTernary(parameter) ?? throw new InvalidOperationException($"Expected expression after ?? at position {position}");
|
||||
|
||||
if (right.Type == typeof(object))
|
||||
{
|
||||
right = ConvertIfNeeded(right, left.Type);
|
||||
}
|
||||
|
||||
left = Expression.Coalesce(left, right);
|
||||
|
||||
token = Peek();
|
||||
}
|
||||
|
||||
return left;
|
||||
}
|
||||
|
||||
private Expression ParseTernary(ParameterExpression parameter)
|
||||
{
|
||||
var condition = ParseOr(parameter);
|
||||
|
||||
if (Peek().Type == TokenType.QuestionMark)
|
||||
{
|
||||
Advance(1);
|
||||
|
||||
var trueExpression = ParseOr(parameter);
|
||||
|
||||
Expect(TokenType.Colon);
|
||||
|
||||
var falseExpression = ParseOr(parameter);
|
||||
|
||||
if (trueExpression is ConstantExpression trueConst && trueConst.Value == null && falseExpression is not ConstantExpression)
|
||||
{
|
||||
trueExpression = Expression.Constant(null, falseExpression.Type);
|
||||
}
|
||||
else if (falseExpression is ConstantExpression falseConst && falseConst.Value == null && trueExpression is not ConstantExpression)
|
||||
{
|
||||
falseExpression = Expression.Constant(null, trueExpression.Type);
|
||||
}
|
||||
|
||||
var ternary = Expression.Condition(condition, trueExpression, falseExpression);
|
||||
|
||||
return ParseMemberAccess(ternary, parameter);
|
||||
}
|
||||
|
||||
return ParseMemberAccess(condition, parameter);
|
||||
}
|
||||
|
||||
private Expression ParseMemberAccess(Expression expression, ParameterExpression parameter)
|
||||
{
|
||||
var token = Peek();
|
||||
while (token.Type is TokenType.Dot or TokenType.QuestionDot or TokenType.OpenBracket)
|
||||
{
|
||||
if (token.Type == TokenType.Dot)
|
||||
{
|
||||
Advance(1);
|
||||
token = Expect(TokenType.Identifier);
|
||||
if (Peek().Type == TokenType.OpenParen)
|
||||
{
|
||||
expression = ParseInvocation(expression, token.Value, parameter);
|
||||
}
|
||||
else
|
||||
{
|
||||
expression = Expression.PropertyOrField(expression, token.Value);
|
||||
}
|
||||
}
|
||||
else if (token.Type == TokenType.QuestionDot)
|
||||
{
|
||||
Advance(1);
|
||||
token = Expect(TokenType.Identifier);
|
||||
|
||||
var check = Expression.Equal(expression, Expression.Constant(null));
|
||||
|
||||
if (Peek().Type == TokenType.OpenParen)
|
||||
{
|
||||
var call = ParseInvocation(expression, token.Value, parameter);
|
||||
expression = Expression.Condition(check, Expression.Constant(null, call.Type), call);
|
||||
}
|
||||
else
|
||||
{
|
||||
var access = Expression.PropertyOrField(expression, token.Value);
|
||||
|
||||
expression = Expression.Condition(check, Expression.Default(access.Type), access);
|
||||
|
||||
var nextToken = Peek();
|
||||
|
||||
if (nextToken.Type == TokenType.Dot || nextToken.Type == TokenType.QuestionDot)
|
||||
{
|
||||
var nextAccess = ParseMemberAccess(access, parameter);
|
||||
|
||||
expression = Expression.Condition(check, Expression.Default(nextAccess.Type), nextAccess);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (token.Type == TokenType.OpenBracket)
|
||||
{
|
||||
Advance(1);
|
||||
var index = ParseExpression(parameter);
|
||||
Expect(TokenType.CloseBracket);
|
||||
|
||||
if (expression.Type.IsArray)
|
||||
{
|
||||
expression = Expression.ArrayIndex(expression, index);
|
||||
}
|
||||
else
|
||||
{
|
||||
var indexer = expression.Type.GetProperty("Item") ?? throw new InvalidOperationException($"Type {expression.Type} does not have an indexer property");
|
||||
|
||||
expression = Expression.Property(expression, indexer, index);
|
||||
}
|
||||
}
|
||||
|
||||
token = Peek();
|
||||
}
|
||||
|
||||
return expression;
|
||||
}
|
||||
|
||||
private MethodCallExpression ParseInvocation(Expression expression, string methodName, ParameterExpression parameter)
|
||||
{
|
||||
Advance(1);
|
||||
|
||||
var arguments = new List<Expression>();
|
||||
|
||||
if (Peek().Type != TokenType.CloseParen)
|
||||
{
|
||||
while (Peek().Type != TokenType.CloseParen)
|
||||
{
|
||||
var token = Peek();
|
||||
|
||||
if (token.Type == TokenType.Identifier && Peek(1).Type == TokenType.EqualsGreaterThan)
|
||||
{
|
||||
var lambdaParameterName = token.Value;
|
||||
|
||||
Advance(2);
|
||||
|
||||
Type? lambdaParameterType = null;
|
||||
|
||||
var extensionMethod = typeof(Enumerable).GetMethods(BindingFlags.Public | BindingFlags.Static)
|
||||
.FirstOrDefault(m => m.Name == methodName && m.GetParameters().Length == 2);
|
||||
|
||||
if (extensionMethod != null)
|
||||
{
|
||||
lambdaParameterType = GetItemType(expression.Type);
|
||||
}
|
||||
|
||||
if (lambdaParameterType == null)
|
||||
{
|
||||
throw new InvalidOperationException($"Could not infer type for lambda parameter {lambdaParameterName}");
|
||||
}
|
||||
|
||||
var lambdaParameter = Expression.Parameter(lambdaParameterType, lambdaParameterName);
|
||||
parameterStack.Push(lambdaParameter);
|
||||
var lambdaBody = ParseExpression(lambdaParameter);
|
||||
parameterStack.Pop();
|
||||
arguments.Add(Expression.Lambda(lambdaBody, lambdaParameter));
|
||||
}
|
||||
else
|
||||
{
|
||||
arguments.Add(ParseExpression(parameter));
|
||||
}
|
||||
|
||||
if (Peek().Type == TokenType.Comma)
|
||||
{
|
||||
Advance(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Expect(TokenType.CloseParen);
|
||||
|
||||
var argumentTypes = arguments.Select(a => a.Type).ToArray();
|
||||
|
||||
var method = expression.Type.GetMethod(methodName, BindingFlags.Public | BindingFlags.Instance, null, argumentTypes, null);
|
||||
|
||||
if (method != null)
|
||||
{
|
||||
return Expression.Call(expression, method, arguments);
|
||||
}
|
||||
|
||||
method = typeof(Enumerable).GetMethods(BindingFlags.Public | BindingFlags.Static)
|
||||
.FirstOrDefault(m => m.Name == methodName && m.GetParameters().Length == arguments.Count + 1);
|
||||
|
||||
if (method != null)
|
||||
{
|
||||
var argumentType = GetItemType(expression.Type);
|
||||
|
||||
if (argumentType == null)
|
||||
{
|
||||
throw new InvalidOperationException($"Cannot determine item type for {expression.Type}");
|
||||
}
|
||||
|
||||
if (method.IsGenericMethodDefinition)
|
||||
{
|
||||
method = method.MakeGenericMethod(argumentType);
|
||||
}
|
||||
|
||||
var parameters = method.GetParameters();
|
||||
|
||||
var argumentsWithInstance = new[] { expression }.Concat(arguments).ToArray();
|
||||
|
||||
return Expression.Call(method, argumentsWithInstance.Select((a, index) => ConvertIfNeeded(a, parameters[index].ParameterType)));
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"No suitable method '{methodName}' found for type '{expression.Type}'");
|
||||
}
|
||||
|
||||
private static Type? GetItemType(Type enumerableOrArray)
|
||||
{
|
||||
return enumerableOrArray.IsArray ? enumerableOrArray.GetElementType() : enumerableOrArray.GetGenericArguments()[0];
|
||||
}
|
||||
|
||||
private Expression? ParseTerm(ParameterExpression parameter)
|
||||
{
|
||||
var token = Peek();
|
||||
|
||||
if (token.Type == TokenType.None)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (token.Type == TokenType.OpenParen)
|
||||
{
|
||||
Advance(1);
|
||||
|
||||
if (TryParseCastExpression(parameter, out var expression))
|
||||
{
|
||||
return expression;
|
||||
}
|
||||
|
||||
expression = ParseExpression(parameter);
|
||||
|
||||
Expect(TokenType.CloseParen);
|
||||
|
||||
return expression;
|
||||
}
|
||||
|
||||
if (token.Type == TokenType.Identifier)
|
||||
{
|
||||
var matchingParameter = parameterStack.FirstOrDefault(p => p.Name == token.Value);
|
||||
if (matchingParameter != null)
|
||||
{
|
||||
Advance(1);
|
||||
return ParseMemberAccess(matchingParameter, parameter);
|
||||
}
|
||||
|
||||
var type = GetWellKnownType(token.Value);
|
||||
|
||||
if (type != null)
|
||||
{
|
||||
Advance(1);
|
||||
return ParseStaticMemberAccess(type, parameter);
|
||||
}
|
||||
|
||||
if (Peek(1).Type == TokenType.OpenParen)
|
||||
{
|
||||
Advance(1);
|
||||
return ParseInvocation(parameter, token.Value, parameter);
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"Unexpected identifier: {token.Value}");
|
||||
}
|
||||
|
||||
if (token.Type == TokenType.ExclamationMark)
|
||||
{
|
||||
Advance(1);
|
||||
|
||||
var operand = ParseTerm(parameter) ?? throw new InvalidOperationException($"Expected expression after ! at position {position}");
|
||||
|
||||
operand = ConvertIfNeeded(operand, typeof(bool));
|
||||
|
||||
return Expression.Not(operand);
|
||||
}
|
||||
|
||||
if (token.Type == TokenType.Minus)
|
||||
{
|
||||
Advance(1);
|
||||
|
||||
var operand = ParseTerm(parameter) ?? throw new InvalidOperationException($"Expected expression after - at position {position}");
|
||||
|
||||
return Expression.Negate(operand);
|
||||
}
|
||||
|
||||
if (token.Type == TokenType.Plus)
|
||||
{
|
||||
Advance(1);
|
||||
|
||||
var operand = ParseTerm(parameter) ?? throw new InvalidOperationException($"Expected expression after + at position {position}");
|
||||
|
||||
return operand;
|
||||
}
|
||||
|
||||
switch (token.Type)
|
||||
{
|
||||
case TokenType.CharacterLiteral:
|
||||
case TokenType.StringLiteral:
|
||||
case TokenType.NullLiteral:
|
||||
case TokenType.NumericLiteral:
|
||||
case TokenType.TrueLiteral:
|
||||
case TokenType.FalseLiteral:
|
||||
Advance(1);
|
||||
return token.ToConstantExpression();
|
||||
case TokenType.New:
|
||||
Advance(1);
|
||||
|
||||
token = Peek();
|
||||
|
||||
if (token.Type == TokenType.OpenBrace)
|
||||
{
|
||||
Advance(1);
|
||||
|
||||
var properties = new List<(string Name, Expression Expression)>();
|
||||
|
||||
if (Peek().Type != TokenType.CloseBrace)
|
||||
{
|
||||
do
|
||||
{
|
||||
token = Peek();
|
||||
string propertyName;
|
||||
Expression propertyExpression;
|
||||
|
||||
if (token.Type == TokenType.Identifier)
|
||||
{
|
||||
propertyName = token.Value;
|
||||
Advance(1);
|
||||
if (Peek().Type == TokenType.Dot || Peek().Type == TokenType.QuestionDot)
|
||||
{
|
||||
// Handle nested property access
|
||||
Expression expr = propertyName == parameter.Name ? (Expression)parameter : Expression.Property(parameter, propertyName);
|
||||
propertyExpression = ParseMemberAccess(expr, parameter);
|
||||
|
||||
// Get the last identifier token's value
|
||||
var lastToken = tokens[position - 1];
|
||||
if (lastToken.Type == TokenType.Identifier)
|
||||
{
|
||||
propertyName = lastToken.Value;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Expect(TokenType.Equals);
|
||||
propertyExpression = ParseExpression(parameter);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
propertyExpression = ParseExpression(parameter);
|
||||
|
||||
if (propertyExpression is MemberExpression memberExpression)
|
||||
{
|
||||
propertyName = memberExpression.Member.Name;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException($"Invalid anonymous type member expression at position {position}");
|
||||
}
|
||||
}
|
||||
|
||||
properties.Add((propertyName, propertyExpression));
|
||||
|
||||
if (Peek().Type == TokenType.Comma)
|
||||
{
|
||||
Advance(1);
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
} while (Peek().Type != TokenType.CloseBrace);
|
||||
}
|
||||
|
||||
Expect(TokenType.CloseBrace);
|
||||
|
||||
var propertyTypes = properties.Select(p => p.Expression.Type).ToArray();
|
||||
var propertyNames = properties.Select(p => p.Name).ToArray();
|
||||
var dynamicType = DynamicTypeFactory.CreateType(parameter.Type.Name, propertyNames, propertyTypes);
|
||||
var bindings = properties.Select(p => Expression.Bind(dynamicType.GetProperty(p.Name)!, p.Expression));
|
||||
return Expression.MemberInit(Expression.New(dynamicType), bindings);
|
||||
}
|
||||
else
|
||||
{
|
||||
Type? elementType = null;
|
||||
var nullable = false;
|
||||
|
||||
if (token.Type == TokenType.Identifier)
|
||||
{
|
||||
var typeName = token.Value;
|
||||
elementType = GetWellKnownType(typeName);
|
||||
Advance(1);
|
||||
|
||||
if (Peek().Type == TokenType.QuestionMark)
|
||||
{
|
||||
nullable = true;
|
||||
Advance(1);
|
||||
}
|
||||
}
|
||||
|
||||
Expect(TokenType.OpenBracket);
|
||||
Expect(TokenType.CloseBracket);
|
||||
Expect(TokenType.OpenBrace);
|
||||
|
||||
var elements = new List<Expression>();
|
||||
if (Peek().Type != TokenType.CloseBrace)
|
||||
{
|
||||
do
|
||||
{
|
||||
elements.Add(ParseExpression(parameter));
|
||||
if (Peek().Type == TokenType.Comma)
|
||||
{
|
||||
Advance(1);
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
} while (Peek().Type != TokenType.CloseBrace);
|
||||
}
|
||||
|
||||
Expect(TokenType.CloseBrace);
|
||||
|
||||
if (elementType == null)
|
||||
{
|
||||
elementType = elements.Count > 0 ? elements[0].Type : typeof(object);
|
||||
}
|
||||
|
||||
if (nullable)
|
||||
{
|
||||
elementType = typeof(Nullable<>).MakeGenericType(elementType);
|
||||
}
|
||||
|
||||
return Expression.NewArrayInit(elementType, elements.Select(e => ConvertIfNeeded(e, elementType)));
|
||||
}
|
||||
default:
|
||||
throw new InvalidOperationException($"Unexpected token: {token.Type} at position {position}");
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryParseCastExpression(ParameterExpression parameter, out Expression expression)
|
||||
{
|
||||
expression = null!;
|
||||
|
||||
var token = Peek();
|
||||
|
||||
if (token.Type != TokenType.Identifier)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var typeName = new StringBuilder(token.Value);
|
||||
var index = position + 1;
|
||||
var typeCast = true;
|
||||
var nullable = false;
|
||||
|
||||
while (index < tokens.Count)
|
||||
{
|
||||
token = tokens[index];
|
||||
|
||||
if (token.Type == TokenType.Dot)
|
||||
{
|
||||
index++;
|
||||
if (index >= tokens.Count || tokens[index].Type != TokenType.Identifier)
|
||||
{
|
||||
typeCast = false;
|
||||
break;
|
||||
}
|
||||
typeName.Append('.').Append(tokens[index].Value);
|
||||
index++;
|
||||
}
|
||||
else if (token.Type == TokenType.QuestionMark)
|
||||
{
|
||||
nullable = true;
|
||||
index++;
|
||||
if (index >= tokens.Count || tokens[index].Type != TokenType.CloseParen)
|
||||
{
|
||||
typeCast = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (token.Type == TokenType.CloseParen)
|
||||
{
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
typeCast = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (typeCast && index < tokens.Count && tokens[index].Type == TokenType.CloseParen)
|
||||
{
|
||||
var name = typeName.ToString();
|
||||
|
||||
var type = GetWellKnownType(name) ?? typeResolver?.Invoke(name) ?? throw new InvalidOperationException($"Could not resolve type: {typeName}");
|
||||
|
||||
if (nullable && type.IsValueType)
|
||||
{
|
||||
type = typeof(Nullable<>).MakeGenericType(type);
|
||||
}
|
||||
|
||||
position = index;
|
||||
|
||||
Advance(1);
|
||||
|
||||
if (Peek().Type == TokenType.OpenParen && TryParseCastExpression(parameter, out var innerExpression))
|
||||
{
|
||||
expression = Expression.Convert(innerExpression, type);
|
||||
}
|
||||
else
|
||||
{
|
||||
var source = ParseTerm(parameter) ?? throw new InvalidOperationException($"Expected expression to cast at position {position}");
|
||||
expression = Expression.Convert(source, type);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private Expression ParseStaticMemberAccess(Type type, ParameterExpression parameter)
|
||||
{
|
||||
Expect(TokenType.Dot);
|
||||
|
||||
var token = Expect(TokenType.Identifier);
|
||||
|
||||
if (Peek().Type == TokenType.OpenParen)
|
||||
{
|
||||
return ParseStaticInvocation(type, token.Value, parameter);
|
||||
}
|
||||
else
|
||||
{
|
||||
var member = (MemberInfo?)type.GetProperty(token.Value) ?? type.GetField(token.Value);
|
||||
|
||||
if (member == null)
|
||||
{
|
||||
throw new InvalidOperationException($"Member {token.Value} not found on type {type.Name}");
|
||||
}
|
||||
|
||||
return Expression.MakeMemberAccess(null, member);
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"Expected method invocation after {token.Value} at position {position}");
|
||||
}
|
||||
|
||||
private Expression ParseStaticInvocation(Type type, string methodName, ParameterExpression parameter)
|
||||
{
|
||||
Advance(1);
|
||||
|
||||
var arguments = new List<Expression>();
|
||||
|
||||
if (Peek().Type != TokenType.CloseParen)
|
||||
{
|
||||
arguments.Add(ParseExpression(parameter));
|
||||
|
||||
while (Peek().Type == TokenType.Comma)
|
||||
{
|
||||
Advance(1);
|
||||
arguments.Add(ParseExpression(parameter));
|
||||
}
|
||||
}
|
||||
|
||||
Expect(TokenType.CloseParen);
|
||||
|
||||
var method = type.GetMethod(methodName, [.. arguments.Select(a => a.Type)]) ?? throw new InvalidOperationException($"Method {methodName} not found on type {type.Name}");
|
||||
|
||||
return Expression.Call(null, method, arguments);
|
||||
}
|
||||
|
||||
private static Type? GetWellKnownType(string typeName)
|
||||
{
|
||||
return typeName switch
|
||||
{
|
||||
nameof(DateTime) => typeof(DateTime),
|
||||
nameof(DateOnly) => typeof(DateOnly),
|
||||
nameof(TimeOnly) => typeof(TimeOnly),
|
||||
nameof(DateTimeOffset) => typeof(DateTimeOffset),
|
||||
nameof(Guid) => typeof(Guid),
|
||||
nameof(CultureInfo) => typeof(CultureInfo),
|
||||
nameof(DateTimeStyles) => typeof(DateTimeStyles),
|
||||
nameof(DateTimeKind) => typeof(DateTimeKind),
|
||||
nameof(Double) or "double" => typeof(double),
|
||||
nameof(Single) or "float" => typeof(float),
|
||||
nameof(Int32) or "int" => typeof(int),
|
||||
nameof(Int64) or "long" => typeof(long),
|
||||
nameof(Int16) or "short" => typeof(short),
|
||||
nameof(Byte) or "byte" => typeof(byte),
|
||||
nameof(SByte) or "sbyte" => typeof(sbyte),
|
||||
nameof(UInt32) or "uint" => typeof(uint),
|
||||
nameof(UInt64) or "ulong" => typeof(ulong),
|
||||
nameof(UInt16) or "ushort" => typeof(ushort),
|
||||
nameof(Boolean) or "bool" => typeof(bool),
|
||||
nameof(Char) or "char" => typeof(char),
|
||||
nameof(Decimal) or "decimal" => typeof(decimal),
|
||||
nameof(String) or "string" => typeof(string),
|
||||
nameof(Math) => typeof(Math),
|
||||
nameof(Convert) => typeof(Convert),
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
private Expression ParseOr(ParameterExpression parameter)
|
||||
{
|
||||
var left = ParseMemberAccess(ParseAnd(parameter), parameter);
|
||||
|
||||
var token = Peek();
|
||||
while (token.Type == TokenType.BarBar)
|
||||
{
|
||||
Advance(1);
|
||||
var right = ParseMemberAccess(ParseAnd(parameter) ?? throw new InvalidOperationException($"Expected expression after || at position {position}"), parameter);
|
||||
left = Expression.OrElse(left, right);
|
||||
token = Peek();
|
||||
}
|
||||
|
||||
return left;
|
||||
}
|
||||
|
||||
private Expression ParseAnd(ParameterExpression parameter)
|
||||
{
|
||||
var left = ParseMemberAccess(ParseComparison(parameter), parameter);
|
||||
|
||||
var token = Peek();
|
||||
while (token.Type == TokenType.AmpersandAmpersand)
|
||||
{
|
||||
Advance(1);
|
||||
var right = ParseMemberAccess(ParseComparison(parameter) ?? throw new InvalidOperationException($"Expected expression after && at position {position}"), parameter);
|
||||
left = Expression.AndAlso(left, right);
|
||||
token = Peek();
|
||||
}
|
||||
|
||||
return left;
|
||||
}
|
||||
|
||||
private Expression ParseComparison(ParameterExpression parameter)
|
||||
{
|
||||
var left = ParseShift(parameter);
|
||||
|
||||
var token = Peek();
|
||||
if (token.Type is TokenType.EqualsEquals or TokenType.NotEquals or TokenType.GreaterThan or TokenType.LessThan or TokenType.LessThanOrEqual or TokenType.GreaterThanOrEqual)
|
||||
{
|
||||
Advance(1);
|
||||
var right = ParseShift(parameter) ?? throw new InvalidOperationException($"Expected expression after {token.Value} at position {position}");
|
||||
left = Expression.MakeBinary(token.Type.ToExpressionType(), left, ConvertIfNeeded(right, left.Type));
|
||||
}
|
||||
|
||||
return ParseBinaryAnd(left, parameter);
|
||||
}
|
||||
|
||||
private Expression ParseBinaryAnd(Expression left, ParameterExpression parameter)
|
||||
{
|
||||
var token = Peek();
|
||||
while (token.Type == TokenType.Ampersand)
|
||||
{
|
||||
Advance(1);
|
||||
var right = ParseShift(parameter) ?? throw new InvalidOperationException($"Expected expression after & at position {position}");
|
||||
left = Expression.MakeBinary(ExpressionType.And, left, ConvertIfNeeded(right, left.Type));
|
||||
token = Peek();
|
||||
}
|
||||
|
||||
return ParseBinaryXor(left, parameter);
|
||||
}
|
||||
|
||||
private Expression ParseBinaryXor(Expression left, ParameterExpression parameter)
|
||||
{
|
||||
var token = Peek();
|
||||
while (token.Type == TokenType.Caret)
|
||||
{
|
||||
Advance(1);
|
||||
var right = ParseBinaryAnd(ParseShift(parameter), parameter) ?? throw new InvalidOperationException($"Expected expression after ^ at position {position}");
|
||||
left = Expression.MakeBinary(ExpressionType.ExclusiveOr, left, ConvertIfNeeded(right, left.Type));
|
||||
token = Peek();
|
||||
}
|
||||
|
||||
return ParseBinaryOr(left, parameter);
|
||||
}
|
||||
|
||||
private Expression ParseBinaryOr(Expression left, ParameterExpression parameter)
|
||||
{
|
||||
var token = Peek();
|
||||
while (token.Type == TokenType.Bar)
|
||||
{
|
||||
Advance(1);
|
||||
var right = ParseBinaryXor(ParseShift(parameter), parameter) ?? throw new InvalidOperationException($"Expected expression after | at position {position}");
|
||||
left = Expression.MakeBinary(ExpressionType.Or, left, ConvertIfNeeded(right, left.Type));
|
||||
token = Peek();
|
||||
}
|
||||
|
||||
return left;
|
||||
}
|
||||
|
||||
private Expression ParseShift(ParameterExpression parameter)
|
||||
{
|
||||
var left = ParseAdditive(parameter);
|
||||
|
||||
var token = Peek();
|
||||
while (token.Type is TokenType.LessThanLessThan or TokenType.GreaterThanGreaterThan)
|
||||
{
|
||||
Advance(1);
|
||||
var right = ParseAdditive(parameter) ?? throw new InvalidOperationException($"Expected expression after {token.Value} at position {position}");
|
||||
left = Expression.MakeBinary(token.Type.ToExpressionType(), left, ConvertIfNeeded(right, left.Type));
|
||||
token = Peek();
|
||||
}
|
||||
|
||||
return left;
|
||||
}
|
||||
|
||||
private Expression ParseAdditive(ParameterExpression parameter)
|
||||
{
|
||||
var left = ParseMultiplicative(parameter);
|
||||
|
||||
var token = Peek();
|
||||
while (token.Type is TokenType.Plus or TokenType.Minus)
|
||||
{
|
||||
Advance(1);
|
||||
var right = ParseMultiplicative(parameter) ?? throw new InvalidOperationException($"Expected expression after {token.Value} at position {position}");
|
||||
|
||||
if (token.Type == TokenType.Plus && left.Type == typeof(string))
|
||||
{
|
||||
left = Expression.Call(null, typeof(string).GetMethod(nameof(string.Concat), [typeof(string), typeof(string)])!, left, ConvertIfNeeded(right, typeof(string)));
|
||||
}
|
||||
else
|
||||
{
|
||||
left = Expression.MakeBinary(token.Type.ToExpressionType(), left, ConvertIfNeeded(right, left.Type));
|
||||
}
|
||||
|
||||
token = Peek();
|
||||
}
|
||||
|
||||
return left;
|
||||
}
|
||||
|
||||
private Expression ParseMultiplicative(ParameterExpression parameter)
|
||||
{
|
||||
var left = ParseTerm(parameter) ?? throw new InvalidOperationException($"Expected expression at position {position}");
|
||||
|
||||
var token = Peek();
|
||||
while (token.Type is TokenType.Star or TokenType.Slash)
|
||||
{
|
||||
Advance(1);
|
||||
var right = ParseTerm(parameter) ?? throw new InvalidOperationException($"Expected expression after {token.Value} at position {position}");
|
||||
left = Expression.MakeBinary(token.Type.ToExpressionType(), left, ConvertIfNeeded(right, left.Type));
|
||||
token = Peek();
|
||||
}
|
||||
|
||||
return left;
|
||||
}
|
||||
|
||||
private static Expression ConvertIfNeeded(Expression expression, Type targetType)
|
||||
{
|
||||
if (expression is not LambdaExpression)
|
||||
{
|
||||
return expression.Type == targetType ? expression : Expression.Convert(expression, targetType);
|
||||
}
|
||||
|
||||
return expression;
|
||||
}
|
||||
}
|
||||
473
Radzen.Blazor/ExpressionSerializer.cs
Normal file
473
Radzen.Blazor/ExpressionSerializer.cs
Normal file
@@ -0,0 +1,473 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
|
||||
/// <summary>
|
||||
/// Serializes LINQ Expression Trees into C# string representations.
|
||||
/// </summary>
|
||||
public class ExpressionSerializer : ExpressionVisitor
|
||||
{
|
||||
private readonly StringBuilder _sb = new StringBuilder();
|
||||
|
||||
/// <summary>
|
||||
/// Serializes a given LINQ Expression into a C# string.
|
||||
/// </summary>
|
||||
/// <param name="expression">The expression to serialize.</param>
|
||||
/// <returns>A string representation of the expression.</returns>
|
||||
public string Serialize(Expression expression)
|
||||
{
|
||||
_sb.Clear();
|
||||
Visit(expression);
|
||||
return _sb.ToString();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override Expression VisitLambda<T>(Expression<T> node)
|
||||
{
|
||||
if (node.Parameters.Count > 1)
|
||||
{
|
||||
_sb.Append("(");
|
||||
for (int i = 0; i < node.Parameters.Count; i++)
|
||||
{
|
||||
if (i > 0) _sb.Append(", ");
|
||||
_sb.Append(node.Parameters[i].Name);
|
||||
}
|
||||
_sb.Append(") => ");
|
||||
}
|
||||
else
|
||||
{
|
||||
_sb.Append(node.Parameters[0].Name);
|
||||
_sb.Append(" => ");
|
||||
}
|
||||
Visit(node.Body);
|
||||
return node;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override Expression VisitParameter(ParameterExpression node)
|
||||
{
|
||||
_sb.Append(node.Name);
|
||||
return node;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override Expression VisitMember(MemberExpression node)
|
||||
{
|
||||
if (node.Expression != null)
|
||||
{
|
||||
Visit(node.Expression);
|
||||
_sb.Append($".{node.Member.Name}");
|
||||
}
|
||||
else
|
||||
{
|
||||
_sb.Append(node.Member.Name);
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override Expression VisitMethodCall(MethodCallExpression node)
|
||||
{
|
||||
if (node.Method.IsStatic && node.Arguments.Count > 0 &&
|
||||
(node.Method.DeclaringType == typeof(Enumerable) ||
|
||||
node.Method.DeclaringType == typeof(Queryable)))
|
||||
{
|
||||
Visit(node.Arguments[0]);
|
||||
_sb.Append($".{node.Method.Name}(");
|
||||
|
||||
for (int i = 1; i < node.Arguments.Count; i++)
|
||||
{
|
||||
if (i > 1) _sb.Append(", ");
|
||||
|
||||
if (node.Arguments[i] is NewArrayExpression arrayExpr)
|
||||
{
|
||||
VisitNewArray(arrayExpr);
|
||||
}
|
||||
else
|
||||
{
|
||||
Visit(node.Arguments[i]);
|
||||
}
|
||||
}
|
||||
|
||||
_sb.Append(")");
|
||||
}
|
||||
else if (node.Method.IsStatic)
|
||||
{
|
||||
_sb.Append($"{node.Method.DeclaringType.Name}.{node.Method.Name}(");
|
||||
|
||||
for (int i = 0; i < node.Arguments.Count; i++)
|
||||
{
|
||||
if (i > 0) _sb.Append(", ");
|
||||
Visit(node.Arguments[i]);
|
||||
}
|
||||
|
||||
_sb.Append(")");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (node.Object != null)
|
||||
{
|
||||
Visit(node.Object);
|
||||
_sb.Append($".{node.Method.Name}(");
|
||||
}
|
||||
else
|
||||
{
|
||||
_sb.Append($"{node.Method.Name}(");
|
||||
}
|
||||
|
||||
for (int i = 0; i < node.Arguments.Count; i++)
|
||||
{
|
||||
if (i > 0) _sb.Append(", ");
|
||||
Visit(node.Arguments[i]);
|
||||
}
|
||||
|
||||
_sb.Append(")");
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override Expression VisitUnary(UnaryExpression node)
|
||||
{
|
||||
if (node.NodeType == ExpressionType.Not)
|
||||
{
|
||||
_sb.Append("(!(");
|
||||
Visit(node.Operand);
|
||||
_sb.Append("))");
|
||||
}
|
||||
else if (node.NodeType == ExpressionType.Convert)
|
||||
{
|
||||
if (node.Operand is IndexExpression indexExpr)
|
||||
{
|
||||
_sb.Append($"({node.Type.DisplayName(true).Replace("+",".")})");
|
||||
|
||||
Visit(indexExpr.Object);
|
||||
|
||||
_sb.Append("[");
|
||||
Visit(indexExpr.Arguments[0]);
|
||||
_sb.Append("]");
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
Visit(node.Operand);
|
||||
}
|
||||
else
|
||||
{
|
||||
_sb.Append(node.NodeType switch
|
||||
{
|
||||
ExpressionType.Negate => "-",
|
||||
ExpressionType.UnaryPlus => "+",
|
||||
_ => throw new NotSupportedException($"Unsupported unary operator: {node.NodeType}")
|
||||
});
|
||||
Visit(node.Operand);
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override Expression VisitConstant(ConstantExpression node)
|
||||
{
|
||||
_sb.Append(FormatValue(node.Value));
|
||||
return node;
|
||||
}
|
||||
|
||||
internal static string FormatValue(object value)
|
||||
{
|
||||
if (value == null)
|
||||
return "null";
|
||||
|
||||
return value switch
|
||||
{
|
||||
string s when s == string.Empty => @"""""",
|
||||
null => "null",
|
||||
string s => @$"""{s.Replace("\"", "\\\"")}""",
|
||||
char c => $"'{c}'",
|
||||
bool b => b.ToString().ToLowerInvariant(),
|
||||
DateTime dt => FormatDateTime(dt),
|
||||
DateTimeOffset dto => $"DateTime.Parse(\"{dto.UtcDateTime:yyyy-MM-ddTHH:mm:ss.fffZ}\")",
|
||||
DateOnly dateOnly => $"DateOnly.Parse(\"{dateOnly.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)}\", CultureInfo.InvariantCulture)",
|
||||
TimeOnly timeOnly => $"TimeOnly.Parse(\"{timeOnly.ToString("HH:mm:ss", CultureInfo.InvariantCulture)}\", CultureInfo.InvariantCulture)",
|
||||
Guid guid => $"Guid.Parse(\"{guid.ToString("D", CultureInfo.InvariantCulture)}\")",
|
||||
IEnumerable enumerable when value is not string => FormatEnumerable(enumerable),
|
||||
_ => value.GetType().IsEnum
|
||||
? $"({value.GetType().FullName.Replace("+", ".")})" + Convert.ChangeType(value, Enum.GetUnderlyingType(value.GetType()), CultureInfo.InvariantCulture).ToString()
|
||||
: Convert.ToString(value, CultureInfo.InvariantCulture)
|
||||
};
|
||||
}
|
||||
|
||||
private static string FormatDateTime(DateTime dateTime)
|
||||
{
|
||||
var finalDate = dateTime.TimeOfDay == TimeSpan.Zero ? dateTime.Date : dateTime;
|
||||
var dateFormat = dateTime.TimeOfDay == TimeSpan.Zero ? "yyyy-MM-dd" : "yyyy-MM-ddTHH:mm:ss.fffZ";
|
||||
|
||||
return $"DateTime.SpecifyKind(DateTime.Parse(\"{finalDate.ToString(dateFormat, CultureInfo.InvariantCulture)}\", CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind), DateTimeKind.{Enum.GetName(finalDate.Kind)})";
|
||||
}
|
||||
|
||||
private static string FormatEnumerable(IEnumerable enumerable)
|
||||
{
|
||||
var arrayType = enumerable.AsQueryable().ElementType;
|
||||
|
||||
var items = enumerable.Cast<object>().Select(FormatValue);
|
||||
return $"new {(Nullable.GetUnderlyingType(arrayType) != null ? arrayType.DisplayName(true).Replace("+", ".") : "")}[] {{ {string.Join(", ", items)} }}";
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override Expression VisitNewArray(NewArrayExpression node)
|
||||
{
|
||||
bool needsParentheses = node.NodeType == ExpressionType.NewArrayInit &&
|
||||
(node.Expressions.Count > 1 || node.Expressions[0].NodeType != ExpressionType.Constant);
|
||||
|
||||
if (needsParentheses) _sb.Append("(");
|
||||
|
||||
_sb.Append("new [] { ");
|
||||
bool first = true;
|
||||
foreach (var expr in node.Expressions)
|
||||
{
|
||||
if (!first) _sb.Append(", ");
|
||||
first = false;
|
||||
Visit(expr);
|
||||
}
|
||||
_sb.Append(" }");
|
||||
|
||||
if (needsParentheses) _sb.Append(")");
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override Expression VisitBinary(BinaryExpression node)
|
||||
{
|
||||
_sb.Append("(");
|
||||
Visit(node.Left);
|
||||
_sb.Append($" {GetOperator(node.NodeType)} ");
|
||||
Visit(node.Right);
|
||||
_sb.Append(")");
|
||||
return node;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override Expression VisitConditional(ConditionalExpression node)
|
||||
{
|
||||
_sb.Append("(");
|
||||
Visit(node.Test);
|
||||
_sb.Append(" ? ");
|
||||
Visit(node.IfTrue);
|
||||
_sb.Append(" : ");
|
||||
Visit(node.IfFalse);
|
||||
_sb.Append(")");
|
||||
return node;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps an ExpressionType to its corresponding C# operator.
|
||||
/// </summary>
|
||||
/// <param name="type">The ExpressionType to map.</param>
|
||||
/// <returns>A string representation of the corresponding C# operator.</returns>
|
||||
private static string GetOperator(ExpressionType type)
|
||||
{
|
||||
return type switch
|
||||
{
|
||||
ExpressionType.Add => "+",
|
||||
ExpressionType.Subtract => "-",
|
||||
ExpressionType.Multiply => "*",
|
||||
ExpressionType.Divide => "/",
|
||||
ExpressionType.AndAlso => "&&",
|
||||
ExpressionType.OrElse => "||",
|
||||
ExpressionType.Equal => "==",
|
||||
ExpressionType.NotEqual => "!=",
|
||||
ExpressionType.LessThan => "<",
|
||||
ExpressionType.LessThanOrEqual => "<=",
|
||||
ExpressionType.GreaterThan => ">",
|
||||
ExpressionType.GreaterThanOrEqual => ">=",
|
||||
ExpressionType.Coalesce => "??",
|
||||
_ => throw new NotSupportedException($"Unsupported operator: {type}")
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provides an extension method for displaying type names.
|
||||
/// </summary>
|
||||
public static class SharedTypeExtensions
|
||||
{
|
||||
private static readonly Dictionary<Type, string> BuiltInTypeNames = new()
|
||||
{
|
||||
{ typeof(bool), "bool" },
|
||||
{ typeof(byte), "byte" },
|
||||
{ typeof(char), "char" },
|
||||
{ typeof(decimal), "decimal" },
|
||||
{ typeof(double), "double" },
|
||||
{ typeof(float), "float" },
|
||||
{ typeof(int), "int" },
|
||||
{ typeof(long), "long" },
|
||||
{ typeof(object), "object" },
|
||||
{ typeof(sbyte), "sbyte" },
|
||||
{ typeof(short), "short" },
|
||||
{ typeof(string), "string" },
|
||||
{ typeof(uint), "uint" },
|
||||
{ typeof(ulong), "ulong" },
|
||||
{ typeof(ushort), "ushort" },
|
||||
{ typeof(void), "void" }
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Unwraps nullable type.
|
||||
/// </summary>
|
||||
public static Type UnwrapNullableType(this Type type)
|
||||
=> Nullable.GetUnderlyingType(type) ?? type;
|
||||
|
||||
/// <summary>
|
||||
/// Returns a display name for the given type.
|
||||
/// </summary>
|
||||
/// <param name="type">The type to display.</param>
|
||||
/// <param name="fullName">Indicates whether to use the full name.</param>
|
||||
/// <param name="compilable">Indicates whether to use a compilable format.</param>
|
||||
/// <returns>A string representing the type name.</returns>
|
||||
public static string DisplayName(this Type type, bool fullName = true, bool compilable = false)
|
||||
{
|
||||
var stringBuilder = new StringBuilder();
|
||||
ProcessType(stringBuilder, type, fullName, compilable);
|
||||
return stringBuilder.ToString();
|
||||
}
|
||||
|
||||
private static void ProcessType(StringBuilder builder, Type type, bool fullName, bool compilable)
|
||||
{
|
||||
if (type.IsGenericType)
|
||||
{
|
||||
var genericArguments = type.GetGenericArguments();
|
||||
ProcessGenericType(builder, type, genericArguments, genericArguments.Length, fullName, compilable);
|
||||
}
|
||||
else if (type.IsArray)
|
||||
{
|
||||
ProcessArrayType(builder, type, fullName, compilable);
|
||||
}
|
||||
else if (BuiltInTypeNames.TryGetValue(type, out var builtInName))
|
||||
{
|
||||
builder.Append(builtInName);
|
||||
}
|
||||
else if (!type.IsGenericParameter)
|
||||
{
|
||||
if (compilable)
|
||||
{
|
||||
if (type.IsNested)
|
||||
{
|
||||
ProcessType(builder, type.DeclaringType!, fullName, compilable);
|
||||
builder.Append('.');
|
||||
}
|
||||
else if (fullName)
|
||||
{
|
||||
builder.Append(type.Namespace).Append('.');
|
||||
}
|
||||
|
||||
builder.Append(type.Name);
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.Append(fullName ? type.FullName : type.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ProcessArrayType(StringBuilder builder, Type type, bool fullName, bool compilable)
|
||||
{
|
||||
var innerType = type;
|
||||
while (innerType.IsArray)
|
||||
{
|
||||
innerType = innerType.GetElementType()!;
|
||||
}
|
||||
|
||||
ProcessType(builder, innerType, fullName, compilable);
|
||||
|
||||
while (type.IsArray)
|
||||
{
|
||||
builder.Append('[');
|
||||
builder.Append(',', type.GetArrayRank() - 1);
|
||||
builder.Append(']');
|
||||
type = type.GetElementType()!;
|
||||
}
|
||||
}
|
||||
|
||||
private static void ProcessGenericType(
|
||||
StringBuilder builder,
|
||||
Type type,
|
||||
Type[] genericArguments,
|
||||
int length,
|
||||
bool fullName,
|
||||
bool compilable)
|
||||
{
|
||||
if (type.IsConstructedGenericType
|
||||
&& type.GetGenericTypeDefinition() == typeof(Nullable<>))
|
||||
{
|
||||
ProcessType(builder, type.UnwrapNullableType(), fullName, compilable);
|
||||
builder.Append('?');
|
||||
return;
|
||||
}
|
||||
|
||||
var offset = type.IsNested ? type.DeclaringType!.GetGenericArguments().Length : 0;
|
||||
|
||||
if (compilable)
|
||||
{
|
||||
if (type.IsNested)
|
||||
{
|
||||
ProcessType(builder, type.DeclaringType!, fullName, compilable);
|
||||
builder.Append('.');
|
||||
}
|
||||
else if (fullName)
|
||||
{
|
||||
builder.Append(type.Namespace);
|
||||
builder.Append('.');
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (fullName)
|
||||
{
|
||||
if (type.IsNested)
|
||||
{
|
||||
ProcessGenericType(builder, type.DeclaringType!, genericArguments, offset, fullName, compilable);
|
||||
builder.Append('+');
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.Append(type.Namespace);
|
||||
builder.Append('.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var genericPartIndex = type.Name.IndexOf('`');
|
||||
if (genericPartIndex <= 0)
|
||||
{
|
||||
builder.Append(type.Name);
|
||||
return;
|
||||
}
|
||||
|
||||
builder.Append(type.Name, 0, genericPartIndex);
|
||||
builder.Append('<');
|
||||
|
||||
for (var i = offset; i < length; i++)
|
||||
{
|
||||
ProcessType(builder, genericArguments[i], fullName, compilable);
|
||||
if (i + 1 == length)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
builder.Append(',');
|
||||
if (!genericArguments[i + 1].IsGenericParameter)
|
||||
{
|
||||
builder.Append(' ');
|
||||
}
|
||||
}
|
||||
|
||||
builder.Append('>');
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,12 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Reflection.Emit;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Radzen.Blazor
|
||||
{
|
||||
@@ -14,22 +18,57 @@ namespace Radzen.Blazor
|
||||
/// <summary>
|
||||
/// Gets enum description.
|
||||
/// </summary>
|
||||
public static string GetDisplayDescription(this Enum enumValue)
|
||||
public static string GetDisplayDescription(this Enum enumValue, Func<string, string> translationFunction = null)
|
||||
{
|
||||
var enumValueAsString = enumValue.ToString();
|
||||
var val = enumValue.GetType().GetMember(enumValueAsString).FirstOrDefault();
|
||||
var enumVal = val?.GetCustomAttribute<DisplayAttribute>()?.GetDescription() ?? enumValueAsString;
|
||||
|
||||
return val?.GetCustomAttribute<DisplayAttribute>()?.GetDescription() ?? enumValueAsString;
|
||||
if (translationFunction != null)
|
||||
return translationFunction(enumVal);
|
||||
|
||||
return enumVal;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts Enum to IEnumerable of Value/Text.
|
||||
/// </summary>
|
||||
public static IEnumerable<object> EnumAsKeyValuePair(Type enumType)
|
||||
public static IEnumerable<object> EnumAsKeyValuePair(Type enumType, Func<string, string> translationFunction = null)
|
||||
{
|
||||
return Enum.GetValues(enumType).Cast<Enum>().Distinct().Select(val => new { Value = Convert.ToInt32(val), Text = val.GetDisplayDescription() });
|
||||
Type underlyingType = Enum.GetUnderlyingType(enumType);
|
||||
return Enum.GetValues(enumType).Cast<Enum>().Distinct().Select(val => new { Value = Convert.ChangeType(val, underlyingType), Text = val.GetDisplayDescription(translationFunction) });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts the autocomplete type enum value to the expected
|
||||
/// autocomplete attribute value.
|
||||
/// </summary>
|
||||
/// <returns>The autocomplete attribute string value.</returns>
|
||||
public static string GetAutoCompleteValue(this AutoCompleteType typeValue)
|
||||
{
|
||||
// Handle synonyms.
|
||||
switch (typeValue)
|
||||
{
|
||||
case AutoCompleteType.FirstName:
|
||||
return "given-name";
|
||||
case AutoCompleteType.LastName:
|
||||
return "family-name";
|
||||
case AutoCompleteType.MiddleName:
|
||||
return "additional-name";
|
||||
case AutoCompleteType.ZipCode:
|
||||
return "postal-code";
|
||||
case AutoCompleteType.Province:
|
||||
return "address-level1";
|
||||
case AutoCompleteType.State:
|
||||
return "address-level1";
|
||||
}
|
||||
|
||||
// Handle standard values.
|
||||
var value = typeValue.ToString();
|
||||
value = Regex.Replace(value, "([^A-Z])([A-Z])", "$1-$2");
|
||||
return Regex.Replace(value, "([A-Z]+)([A-Z][^A-Z$])", "$1-$2")
|
||||
.Trim().ToLower();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,80 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq.Expressions;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Forms;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
using Radzen.Blazor;
|
||||
using Radzen.Blazor.Rendering;
|
||||
|
||||
namespace Radzen
|
||||
{
|
||||
/// <summary>
|
||||
/// Class FormComponentWithAutoComplete.
|
||||
/// </summary>
|
||||
public class FormComponentWithAutoComplete<T> : FormComponent<T>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating the type of built-in autocomplete
|
||||
/// the browser should use.
|
||||
/// <see cref="Blazor.AutoCompleteType" />
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The type of built-in autocomplete.
|
||||
/// </value>
|
||||
[Parameter]
|
||||
public virtual AutoCompleteType AutoCompleteType { get; set; } = AutoCompleteType.On;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the autocomplete attribute's string value.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <c>off</c> if the AutoComplete parameter is false or the
|
||||
/// AutoCompleteType parameter is "off". When the AutoComplete
|
||||
/// parameter is true, the value is <c>on</c> or, if set, the value of
|
||||
/// AutoCompleteType.</value>
|
||||
public virtual string AutoCompleteAttribute
|
||||
{
|
||||
get => Attributes != null && Attributes.ContainsKey("AutoComplete") && $"{Attributes["AutoComplete"]}".ToLower() == "false" ? DefaultAutoCompleteAttribute :
|
||||
Attributes != null && Attributes.ContainsKey("AutoComplete") ? Attributes["AutoComplete"] as string ?? AutoCompleteType.GetAutoCompleteValue() : AutoCompleteType.GetAutoCompleteValue();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the default autocomplete attribute's string value.
|
||||
/// </summary>
|
||||
public virtual string DefaultAutoCompleteAttribute { get; set; } = "off";
|
||||
|
||||
object ariaAutoComplete;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task SetParametersAsync(ParameterView parameters)
|
||||
{
|
||||
parameters = parameters.TryGetValue("aria-autocomplete", out ariaAutoComplete) ?
|
||||
ParameterView.FromDictionary(parameters
|
||||
.ToDictionary().Where(i => i.Key != "aria-autocomplete").ToDictionary(i => i.Key, i => i.Value)
|
||||
.ToDictionary(i => i.Key, i => i.Value))
|
||||
: parameters;
|
||||
|
||||
await base.SetParametersAsync(parameters);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the default aria-autocomplete attribute's string value.
|
||||
/// </summary>
|
||||
public virtual string DefaultAriaAutoCompleteAttribute { get; set; } = "none";
|
||||
|
||||
/// <summary>
|
||||
/// Gets the aria-autocomplete attribute's string value.
|
||||
/// </summary>
|
||||
public virtual string AriaAutoCompleteAttribute
|
||||
{
|
||||
get => AutoCompleteAttribute == DefaultAutoCompleteAttribute ? DefaultAriaAutoCompleteAttribute : ariaAutoComplete as string;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Class FormComponent.
|
||||
/// Implements the <see cref="Radzen.RadzenComponent" />
|
||||
@@ -148,7 +214,9 @@ namespace Radzen
|
||||
/// Gets the field identifier.
|
||||
/// </summary>
|
||||
/// <value>The field identifier.</value>
|
||||
public FieldIdentifier FieldIdentifier { get; private set; }
|
||||
[Parameter]
|
||||
public FieldIdentifier FieldIdentifier { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the value expression.
|
||||
/// </summary>
|
||||
@@ -162,6 +230,8 @@ namespace Radzen
|
||||
/// <returns>Task.</returns>
|
||||
public override Task SetParametersAsync(ParameterView parameters)
|
||||
{
|
||||
var disabledChanged = parameters.DidParameterChange(nameof(Disabled), Disabled);
|
||||
|
||||
var result = base.SetParametersAsync(parameters);
|
||||
|
||||
if (EditContext != null && ValueExpression != null && FieldIdentifier.Model != EditContext.Model)
|
||||
@@ -171,6 +241,11 @@ namespace Radzen
|
||||
EditContext.OnValidationStateChanged += ValidationStateChanged;
|
||||
}
|
||||
|
||||
if (disabledChanged)
|
||||
{
|
||||
FormFieldContext?.DisabledChanged(Disabled);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -230,6 +305,20 @@ namespace Radzen
|
||||
/// <returns>ClassList.</returns>
|
||||
protected ClassList GetClassList(string className) => ClassList.Create(className)
|
||||
.AddDisabled(Disabled)
|
||||
.Add(FieldIdentifier, EditContext);
|
||||
.Add(FieldIdentifier, EditContext)
|
||||
.Add("rz-state-empty", !HasValue);
|
||||
|
||||
/// <summary> Provides support for RadzenFormField integration. </summary>
|
||||
[CascadingParameter]
|
||||
public IFormFieldContext FormFieldContext { get; set; }
|
||||
|
||||
/// <summary> Gets the current placeholder. Returns empty string if this component is inside a RadzenFormField.</summary>
|
||||
protected string CurrentPlaceholder => FormFieldContext?.AllowFloatingLabel == true ? " " : Placeholder;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual async ValueTask FocusAsync()
|
||||
{
|
||||
await Element.FocusAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,7 +187,7 @@ namespace Radzen.Blazor
|
||||
|
||||
if (Visible)
|
||||
{
|
||||
JSRuntime.InvokeVoidAsync("Radzen.destroyGauge", Element);
|
||||
JSRuntime.InvokeVoid("Radzen.destroyGauge", Element);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -50,14 +50,23 @@ namespace Radzen.Blazor
|
||||
/// <param name="valueScale">The value scale.</param>
|
||||
/// <returns>RenderFragment.</returns>
|
||||
RenderFragment RenderOverlays(ScaleBase categoryScale, ScaleBase valueScale);
|
||||
|
||||
/// <summary>
|
||||
/// Renders the series tooltip.
|
||||
/// </summary>
|
||||
/// <param name="data">The data.</param>
|
||||
/// <param name="marginLeft">The left margin.</param>
|
||||
/// <param name="marginTop">The right margin.</param>
|
||||
/// <returns>RenderFragment.</returns>
|
||||
RenderFragment RenderTooltip(object data, double marginLeft, double marginTop);
|
||||
RenderFragment RenderTooltip(object data);
|
||||
/// <summary>
|
||||
/// Renders a tooltip item with the specified data to be displayed in a shared tooltip
|
||||
/// </summary>
|
||||
RenderFragment RenderSharedTooltipItem(object category);
|
||||
/// <summary>
|
||||
/// Get position of the series tooltip.
|
||||
/// </summary>
|
||||
/// <param name="data">The data.</param>
|
||||
/// <returns>Position.</returns>
|
||||
Point GetTooltipPosition(object data);
|
||||
/// <summary>
|
||||
/// Renders the legend item.
|
||||
/// </summary>
|
||||
@@ -96,7 +105,7 @@ namespace Radzen.Blazor
|
||||
/// </summary>
|
||||
/// <param name="x">The x.</param>
|
||||
/// <param name="y">The y.</param>
|
||||
object DataAt(double x, double y);
|
||||
(object, Point) DataAt(double x, double y);
|
||||
/// <summary>
|
||||
/// Returns data chart position
|
||||
/// </summary>
|
||||
|
||||
@@ -28,6 +28,12 @@ namespace Radzen.Blazor
|
||||
/// <summary>
|
||||
/// Renders tooltip
|
||||
/// </summary>
|
||||
RenderFragment RenderTooltip(double mouseX, double mouseY, double marginLeft, double marginTop);
|
||||
RenderFragment RenderTooltip(double mouseX, double mouseY);
|
||||
|
||||
/// <summary>
|
||||
/// Get position of the overlay tooltip.
|
||||
/// </summary>
|
||||
/// <returns>Position.</returns>
|
||||
Point GetTooltipPosition(double mouseX, double mouseY);
|
||||
}
|
||||
}
|
||||
|
||||
26
Radzen.Blazor/IChartStackedAreaSeries.cs
Normal file
26
Radzen.Blazor/IChartStackedAreaSeries.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Radzen.Blazor
|
||||
{
|
||||
/// <summary>
|
||||
/// Marker interface for <see cref="RadzenStackedAreaSeries{TItem}" />.
|
||||
/// </summary>
|
||||
public interface IChartStackedAreaSeries
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the count.
|
||||
/// </summary>
|
||||
/// <value>The count.</value>
|
||||
int Count { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the values for category.
|
||||
/// </summary>
|
||||
IEnumerable<double> ValuesForCategory(double category);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the value at the specified index.
|
||||
/// </summary>
|
||||
double ValueAt(int index);
|
||||
}
|
||||
}
|
||||
26
Radzen.Blazor/IChartStackedBarSeries.cs
Normal file
26
Radzen.Blazor/IChartStackedBarSeries.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Radzen.Blazor
|
||||
{
|
||||
/// <summary>
|
||||
/// Marker interface for <see cref="RadzenStackedBarSeries{TItem}" />.
|
||||
/// </summary>
|
||||
public interface IChartStackedBarSeries : IChartBarSeries
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the value at the specified index.
|
||||
/// </summary>
|
||||
double ValueAt(int index);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the values for category.
|
||||
/// </summary>
|
||||
IEnumerable<double> ValuesForCategory(double category);
|
||||
/// <summary>
|
||||
/// Gets the items for category.
|
||||
/// </summary>
|
||||
/// <param name="category"></param>
|
||||
/// <returns></returns>
|
||||
IEnumerable<object> ItemsForCategory(double category);
|
||||
}
|
||||
}
|
||||
33
Radzen.Blazor/IChartStackedColumnSeries.cs
Normal file
33
Radzen.Blazor/IChartStackedColumnSeries.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Radzen.Blazor
|
||||
{
|
||||
/// <summary>
|
||||
/// Marker interface for <see cref="RadzenStackedColumnSeries{TItem}" />.
|
||||
/// </summary>
|
||||
public interface IChartStackedColumnSeries
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the count.
|
||||
/// </summary>
|
||||
/// <value>The count.</value>
|
||||
int Count { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the values for category.
|
||||
/// </summary>
|
||||
IEnumerable<double> ValuesForCategory(double category);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the items for category.
|
||||
/// </summary>
|
||||
/// <param name="category"></param>
|
||||
/// <returns></returns>
|
||||
IEnumerable<object> ItemsForCategory(double category);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the value at the specified index.
|
||||
/// </summary>
|
||||
double ValueAt(int index);
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,11 @@ namespace Radzen.Blazor
|
||||
/// </summary>
|
||||
public interface IScheduler
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the appointment move event callback.
|
||||
/// </summary>
|
||||
/// <value>The appointment move event callback.</value>
|
||||
EventCallback<SchedulerAppointmentMoveEventArgs> AppointmentMove { get; set; }
|
||||
/// <summary>
|
||||
/// Gets the appointments in the specified range.
|
||||
/// </summary>
|
||||
@@ -59,6 +64,32 @@ namespace Radzen.Blazor
|
||||
/// <param name="end">The end.</param>
|
||||
Task SelectSlot(DateTime start, DateTime end);
|
||||
/// <summary>
|
||||
/// Selects the specified slot.
|
||||
/// </summary>
|
||||
/// <param name="start">The start.</param>
|
||||
/// <param name="end">The end.</param>
|
||||
/// <param name="appointments">The appointments for this range.</param>
|
||||
Task<bool> SelectSlot(DateTime start, DateTime end, IEnumerable<AppointmentData> appointments);
|
||||
/// <summary>
|
||||
/// Selects the specified month.
|
||||
/// </summary>
|
||||
/// <param name="monthStart">The start of the month.</param>
|
||||
/// <param name="appointments">The appointments for this range.</param>
|
||||
Task SelectMonth(DateTime monthStart, IEnumerable<AppointmentData> appointments);
|
||||
/// <summary>
|
||||
/// Selects the specified day.
|
||||
/// </summary>
|
||||
/// <param name="day">The selected day.</param>
|
||||
/// <param name="appointments">The appointments for this range.</param>
|
||||
Task SelectDay(DateTime day, IEnumerable<AppointmentData> appointments);
|
||||
/// <summary>
|
||||
/// Selects the specified more link.
|
||||
/// </summary>
|
||||
/// <param name="start">The start.</param>
|
||||
/// <param name="end">The end.</param>
|
||||
/// <param name="appointments">The appointments for this range.</param>
|
||||
Task<bool> SelectMore(DateTime start, DateTime end, IEnumerable<AppointmentData> appointments);
|
||||
/// <summary>
|
||||
/// Gets the appointment HTML attributes.
|
||||
/// </summary>
|
||||
/// <param name="item">The appointment.</param>
|
||||
@@ -69,14 +100,40 @@ namespace Radzen.Blazor
|
||||
/// </summary>
|
||||
/// <param name="start">The start of the slot.</param>
|
||||
/// <param name="end">The end of the slot.</param>
|
||||
/// <param name="getAppointments">Function to return appointments for this range.</param>
|
||||
/// <returns>A dictionary containing the HTML attributes for the specified slot.</returns>
|
||||
IDictionary<string, object> GetSlotAttributes(DateTime start, DateTime end);
|
||||
IDictionary<string, object> GetSlotAttributes(DateTime start, DateTime end, Func<IEnumerable<AppointmentData>> getAppointments);
|
||||
/// <summary>
|
||||
/// Renders the appointment.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <returns>RenderFragment.</returns>
|
||||
RenderFragment RenderAppointment(AppointmentData item);
|
||||
|
||||
/// <summary>
|
||||
/// Notifies the scheduler that the user has moved the mouse over the specified appointment.
|
||||
/// </summary>
|
||||
/// <param name="reference"></param>
|
||||
/// <param name="data"></param>
|
||||
Task MouseEnterAppointment(ElementReference reference, AppointmentData data);
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the scheduler has a mouse enter appointment listener.
|
||||
/// </summary>
|
||||
bool HasMouseEnterAppointmentDelegate();
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the scheduler has an AppointmentMove listener.
|
||||
/// </summary>
|
||||
bool HasAppointmentMoveDelegate();
|
||||
|
||||
/// <summary>
|
||||
/// Notifies the scheduler that the user has moved the mouse out of the specified appointment.
|
||||
/// </summary>
|
||||
/// <param name="reference"></param>
|
||||
/// <param name="data"></param>
|
||||
/// <returns></returns>
|
||||
Task MouseLeaveAppointment(ElementReference reference, AppointmentData data);
|
||||
/// <summary>
|
||||
/// Reloads this instance.
|
||||
/// </summary>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace Radzen.Blazor
|
||||
@@ -49,5 +50,11 @@ namespace Radzen.Blazor
|
||||
/// Gets the end date.
|
||||
/// </summary>
|
||||
DateTime EndDate { get; }
|
||||
/// <summary>
|
||||
/// Handles appointent move event.
|
||||
/// </summary>
|
||||
/// <param name="data"></param>
|
||||
/// <returns></returns>
|
||||
Task OnAppointmentMove(SchedulerAppointmentMoveEventArgs data);
|
||||
}
|
||||
}
|
||||
24
Radzen.Blazor/JSRuntimeExtensions.cs
Normal file
24
Radzen.Blazor/JSRuntimeExtensions.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
namespace Radzen;
|
||||
|
||||
static class JSRuntimeExtensions
|
||||
{
|
||||
public static void InvokeVoid(this IJSRuntime jsRuntime, string identifier, params object[] args)
|
||||
{
|
||||
_ = jsRuntime.InvokeVoidAsync(identifier, args).FireAndForget();
|
||||
}
|
||||
|
||||
private static async ValueTask FireAndForget(this ValueTask task)
|
||||
{
|
||||
try
|
||||
{
|
||||
await task;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018-2023 Radzen Ltd
|
||||
Copyright (c) 2018-2025 Radzen Ltd
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -53,7 +53,7 @@ namespace Radzen.Blazor
|
||||
|
||||
protected virtual double CalculateTickCount(int distance)
|
||||
{
|
||||
return Math.Ceiling(Math.Abs(Output.End - Output.Start) / distance);
|
||||
return Math.Max(1, Math.Ceiling(Math.Abs(Output.End - Output.Start) / distance));
|
||||
}
|
||||
|
||||
public override (double Start, double End, double Step) Ticks(int distance)
|
||||
@@ -62,10 +62,19 @@ namespace Radzen.Blazor
|
||||
var start = Input.Start;
|
||||
var end = Input.End;
|
||||
|
||||
|
||||
if (start == end)
|
||||
{
|
||||
start = 0;
|
||||
end += NiceNumber(end / ticks, false);
|
||||
if (end < 0)
|
||||
{
|
||||
start += NiceNumber(end / ticks, false);
|
||||
end = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
start = 0;
|
||||
end += NiceNumber(end / ticks, false);
|
||||
}
|
||||
}
|
||||
|
||||
if (Round && end < 0)
|
||||
@@ -88,11 +97,6 @@ namespace Radzen.Blazor
|
||||
if (Step is IConvertible)
|
||||
{
|
||||
step = Convert.ToDouble(Step);
|
||||
|
||||
if (step <= 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException("Step must be greater than zero");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,7 +106,7 @@ namespace Radzen.Blazor
|
||||
end = Math.Ceiling(end / step) * step;
|
||||
}
|
||||
|
||||
if (!Double.IsFinite(Input.Start) && !Double.IsFinite(Input.End))
|
||||
if (!double.IsFinite(Input.Start) || !double.IsFinite(Input.End))
|
||||
{
|
||||
Input.Start = start = 0;
|
||||
Input.End = end = 2;
|
||||
@@ -110,7 +114,7 @@ namespace Radzen.Blazor
|
||||
Round = false;
|
||||
}
|
||||
|
||||
if (!Double.IsFinite(start) && !Double.IsFinite(end))
|
||||
if (!double.IsFinite(start) || !double.IsFinite(end))
|
||||
{
|
||||
Input.Start = start = 0;
|
||||
Input.End = end = 2;
|
||||
@@ -118,7 +122,12 @@ namespace Radzen.Blazor
|
||||
Round = false;
|
||||
}
|
||||
|
||||
return (start, end, step);
|
||||
if (step == 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException("Step must be non-zero");
|
||||
}
|
||||
|
||||
return (start, end, Math.Abs(step));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,5 +3,4 @@
|
||||
<assembly fullname="System.Core">
|
||||
<type fullname="System.Linq.Queryable" preserve="all" />
|
||||
</assembly>
|
||||
<assembly fullname="System.Linq.Dynamic.Core" />
|
||||
</linker>
|
||||
47
Radzen.Blazor/Markdown/AtxHeading.cs
Normal file
47
Radzen.Blazor/Markdown/AtxHeading.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Radzen.Blazor.Markdown;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a markdown ATX heading: <c># Heading</c>.
|
||||
/// </summary>
|
||||
public class AtxHeading : Heading
|
||||
{
|
||||
private static readonly Regex MarkerRegex = new(@"^#{1,6}(?:[ \t]+|$)");
|
||||
|
||||
private static readonly Regex StartRegex = new(@"^[ \t]*#+[ \t]*$");
|
||||
|
||||
private static readonly Regex EndRegex = new(@"[ \t]+#+[ \t]*$");
|
||||
|
||||
internal static BlockStart Start(BlockParser parser, Block block)
|
||||
{
|
||||
if (parser.Indented)
|
||||
{
|
||||
return BlockStart.Skip;
|
||||
}
|
||||
|
||||
var line = parser.CurrentLine[parser.NextNonSpace..];
|
||||
|
||||
var match = MarkerRegex.Match(line);
|
||||
|
||||
if (match.Success)
|
||||
{
|
||||
parser.AdvanceNextNonSpace();
|
||||
parser.AdvanceOffset(match.Length, false);
|
||||
parser.CloseUnmatchedBlocks();
|
||||
var container = parser.AddChild<AtxHeading>(parser.NextNonSpace);
|
||||
container.Level = match.Value.Trim().Length;
|
||||
|
||||
// remove trailing ###s:
|
||||
line = parser.CurrentLine[parser.Offset..];
|
||||
|
||||
container.Value = EndRegex.Replace(StartRegex.Replace(line, ""), "");
|
||||
|
||||
parser.AdvanceOffset(parser.CurrentLine.Length - parser.Offset, false);
|
||||
|
||||
return BlockStart.Leaf;
|
||||
}
|
||||
|
||||
return BlockStart.Skip;
|
||||
}
|
||||
}
|
||||
380
Radzen.Blazor/Markdown/BlazorMarkdownRenderer.cs
Normal file
380
Radzen.Blazor/Markdown/BlazorMarkdownRenderer.cs
Normal file
@@ -0,0 +1,380 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Rendering;
|
||||
|
||||
namespace Radzen.Blazor.Markdown;
|
||||
|
||||
#nullable enable
|
||||
|
||||
class BlazorMarkdownRendererOptions
|
||||
{
|
||||
public int AutoLinkHeadingDepth { get; set; }
|
||||
public bool AllowHtml { get; set; }
|
||||
|
||||
public IEnumerable<string>? AllowedHtmlTags { get; set; }
|
||||
|
||||
public IEnumerable<string>? AllowedHtmlAttributes { get; set; }
|
||||
}
|
||||
|
||||
class BlazorMarkdownRenderer(BlazorMarkdownRendererOptions options, RenderTreeBuilder builder, Action<RenderTreeBuilder, int> outlet) : NodeVisitorBase
|
||||
{
|
||||
public const string Outlet = "<!--rz-outlet-{0}-->";
|
||||
private static readonly Regex OutletRegex = new (@"<!--rz-outlet-(\d+)-->");
|
||||
private static readonly Regex HtmlTagRegex = new(@"<(\w+)((?:\s+[^>]*)?)\/?>");
|
||||
private static readonly Regex HtmlClosingTagRegex = new(@"</(\w+)>");
|
||||
private static readonly Regex AttributeRegex = new(@"(\w+)(?:\s*=\s*(?:([""'])(.*?)\2|([^\s>]+)))?");
|
||||
private readonly HtmlSanitizer sanitizer = new (options.AllowedHtmlTags, options.AllowedHtmlAttributes);
|
||||
|
||||
public override void VisitHeading(Heading heading)
|
||||
{
|
||||
builder.OpenComponent<RadzenText>(0);
|
||||
builder.AddAttribute(1, nameof(RadzenText.ChildContent), RenderChildren(heading.Children));
|
||||
|
||||
switch (heading.Level)
|
||||
{
|
||||
case 1:
|
||||
builder.AddAttribute(2, nameof(RadzenText.TextStyle), TextStyle.H1);
|
||||
break;
|
||||
case 2:
|
||||
builder.AddAttribute(3, nameof(RadzenText.TextStyle), TextStyle.H2);
|
||||
break;
|
||||
case 3:
|
||||
builder.AddAttribute(4, nameof(RadzenText.TextStyle), TextStyle.H3);
|
||||
break;
|
||||
case 4:
|
||||
builder.AddAttribute(5, nameof(RadzenText.TextStyle), TextStyle.H4);
|
||||
break;
|
||||
case 5:
|
||||
builder.AddAttribute(6, nameof(RadzenText.TextStyle), TextStyle.H5);
|
||||
break;
|
||||
case 6:
|
||||
builder.AddAttribute(7, nameof(RadzenText.TextStyle), TextStyle.H6);
|
||||
break;
|
||||
}
|
||||
|
||||
if (heading.Level <= options.AutoLinkHeadingDepth)
|
||||
{
|
||||
var anchor = Regex.Replace(heading.Value, @"[^\w\s-]", string.Empty).Replace(' ', '-').ToLowerInvariant().Trim();
|
||||
builder.AddAttribute(8, nameof(RadzenText.Anchor), anchor);
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.AddAttribute(9, nameof(RadzenText.Anchor), (string?)null);
|
||||
}
|
||||
|
||||
builder.CloseComponent();
|
||||
}
|
||||
|
||||
public override void VisitTable(Table table)
|
||||
{
|
||||
builder.OpenComponent<RadzenTable>(0);
|
||||
builder.AddAttribute(1, nameof(RadzenTable.ChildContent), RenderChildren(table.Rows));
|
||||
builder.CloseComponent();
|
||||
}
|
||||
|
||||
public override void VisitTableRow(TableRow row)
|
||||
{
|
||||
builder.OpenComponent<RadzenTableRow>(0);
|
||||
builder.AddAttribute(1, nameof(RadzenTableRow.ChildContent), RenderChildren(row.Cells));
|
||||
builder.CloseComponent();
|
||||
}
|
||||
public override void VisitTableCell(TableCell cell)
|
||||
{
|
||||
builder.OpenComponent<RadzenTableCell>(0);
|
||||
builder.AddAttribute(1, nameof(RadzenTableCell.ChildContent), RenderChildren(cell.Children));
|
||||
RenderCellAlignment(builder, cell.Alignment);
|
||||
builder.CloseComponent();
|
||||
}
|
||||
|
||||
private static void RenderCellAlignment(RenderTreeBuilder builder, TableCellAlignment alignment)
|
||||
{
|
||||
switch (alignment)
|
||||
{
|
||||
case TableCellAlignment.Center:
|
||||
builder.AddAttribute(2, nameof(RadzenTableCell.Style), "text-align: center");
|
||||
break;
|
||||
case TableCellAlignment.Right:
|
||||
builder.AddAttribute(3, nameof(RadzenTableCell.Style), "text-align: right");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public override void VisitTableHeaderRow(TableHeaderRow header)
|
||||
{
|
||||
builder.OpenComponent<RadzenTableHeader>(0);
|
||||
builder.AddAttribute(1, nameof(RadzenTableHeader.ChildContent), new RenderFragment(headerBuilder =>
|
||||
{
|
||||
headerBuilder.OpenComponent<RadzenTableHeaderRow>(0);
|
||||
headerBuilder.AddAttribute(1, nameof(RadzenTableHeaderRow.ChildContent), new RenderFragment(headerRowBuilder =>
|
||||
{
|
||||
foreach (var cell in header.Cells)
|
||||
{
|
||||
headerRowBuilder.OpenComponent<RadzenTableHeaderCell>(0);
|
||||
headerRowBuilder.AddAttribute(1, nameof(RadzenTableHeaderCell.ChildContent), RenderChildren(cell.Children));
|
||||
RenderCellAlignment(headerRowBuilder, cell.Alignment);
|
||||
headerRowBuilder.CloseComponent();
|
||||
}
|
||||
}));
|
||||
headerBuilder.CloseComponent();
|
||||
}));
|
||||
builder.CloseComponent();
|
||||
}
|
||||
|
||||
public override void VisitIndentedCodeBlock(IndentedCodeBlock code)
|
||||
{
|
||||
builder.OpenElement(0, "pre");
|
||||
builder.OpenElement(1, "code");
|
||||
builder.AddContent(2, code.Value);
|
||||
builder.CloseElement();
|
||||
builder.CloseElement();
|
||||
}
|
||||
|
||||
public override void VisitParagraph(Paragraph paragraph)
|
||||
{
|
||||
if (paragraph.Parent is ListItem item && item.Parent is List list && list.Tight)
|
||||
{
|
||||
VisitChildren(paragraph.Children);
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.OpenComponent<RadzenText>(0);
|
||||
builder.AddAttribute(1, nameof(RadzenText.ChildContent), RenderChildren(paragraph.Children));
|
||||
builder.CloseComponent();
|
||||
}
|
||||
}
|
||||
|
||||
private RenderFragment RenderChildren(IEnumerable<INode> children)
|
||||
{
|
||||
return innerBuilder =>
|
||||
{
|
||||
var inner = new BlazorMarkdownRenderer(options, innerBuilder, outlet);
|
||||
inner.VisitChildren(children);
|
||||
};
|
||||
}
|
||||
|
||||
public override void VisitBlockQuote(BlockQuote blockQuote)
|
||||
{
|
||||
builder.OpenElement(0, "blockquote");
|
||||
VisitChildren(blockQuote.Children);
|
||||
builder.CloseElement();
|
||||
}
|
||||
|
||||
public override void VisitCode(Code code)
|
||||
{
|
||||
builder.OpenElement(0, "code");
|
||||
builder.AddContent(1, code.Value);
|
||||
builder.CloseElement();
|
||||
}
|
||||
|
||||
public override void VisitStrong(Strong strong)
|
||||
{
|
||||
builder.OpenElement(0, "strong");
|
||||
VisitChildren(strong.Children);
|
||||
builder.CloseElement();
|
||||
}
|
||||
|
||||
public override void VisitEmphasis(Emphasis emphasis)
|
||||
{
|
||||
builder.OpenElement(0, "em");
|
||||
VisitChildren(emphasis.Children);
|
||||
builder.CloseElement();
|
||||
}
|
||||
|
||||
public override void VisitLink(Link link)
|
||||
{
|
||||
builder.OpenComponent<RadzenLink>(0);
|
||||
|
||||
if (!HtmlSanitizer.IsDangerousUrl(link.Destination))
|
||||
{
|
||||
builder.AddAttribute(1, nameof(RadzenLink.Path), link.Destination);
|
||||
}
|
||||
|
||||
builder.AddAttribute(2, nameof(RadzenLink.ChildContent), RenderChildren(link.Children));
|
||||
|
||||
if (!string.IsNullOrEmpty(link.Title))
|
||||
{
|
||||
builder.AddAttribute(3, "title", link.Title);
|
||||
}
|
||||
|
||||
builder.CloseComponent();
|
||||
}
|
||||
|
||||
public override void VisitImage(Image image)
|
||||
{
|
||||
builder.OpenComponent<RadzenImage>(0);
|
||||
|
||||
if (!HtmlSanitizer.IsDangerousUrl(image.Destination))
|
||||
{
|
||||
builder.AddAttribute(1, nameof(RadzenImage.Path), image.Destination);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(image.Title))
|
||||
{
|
||||
builder.AddAttribute(2, nameof(RadzenImage.AlternateText), image.Title);
|
||||
}
|
||||
|
||||
builder.CloseElement();
|
||||
}
|
||||
|
||||
public override void VisitOrderedList(OrderedList orderedList)
|
||||
{
|
||||
builder.OpenElement(0, "ol");
|
||||
VisitChildren(orderedList.Children);
|
||||
builder.CloseElement();
|
||||
}
|
||||
|
||||
public override void VisitUnorderedList(UnorderedList unorderedList)
|
||||
{
|
||||
builder.OpenElement(0, "ul");
|
||||
VisitChildren(unorderedList.Children);
|
||||
builder.CloseElement();
|
||||
}
|
||||
|
||||
public override void VisitListItem(ListItem listItem)
|
||||
{
|
||||
builder.OpenElement(0, "li");
|
||||
VisitChildren(listItem.Children);
|
||||
builder.CloseElement();
|
||||
}
|
||||
|
||||
public override void VisitFencedCodeBlock(FencedCodeBlock fencedCodeBlock)
|
||||
{
|
||||
builder.OpenElement(0, "pre");
|
||||
builder.OpenElement(1, "code");
|
||||
builder.AddContent(2, fencedCodeBlock.Value);
|
||||
builder.CloseElement();
|
||||
builder.CloseElement();
|
||||
}
|
||||
|
||||
public override void VisitThematicBreak(ThematicBreak thematicBreak)
|
||||
{
|
||||
builder.OpenElement(0, "hr");
|
||||
builder.CloseElement();
|
||||
}
|
||||
|
||||
public override void VisitHtmlBlock(HtmlBlock htmlBlock)
|
||||
{
|
||||
var match = OutletRegex.Match(htmlBlock.Value);
|
||||
|
||||
if (match.Success)
|
||||
{
|
||||
var markerId = Convert.ToInt32(match.Groups[1].Value);
|
||||
outlet(builder, markerId);
|
||||
}
|
||||
else if (options.AllowHtml)
|
||||
{
|
||||
var html = sanitizer.Sanitize(htmlBlock.Value);
|
||||
builder.AddMarkupContent(0, html);
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.AddContent(0, htmlBlock.Value);
|
||||
}
|
||||
}
|
||||
|
||||
public override void VisitLineBreak(LineBreak lineBreak)
|
||||
{
|
||||
builder.OpenElement(0, "br");
|
||||
builder.CloseElement();
|
||||
}
|
||||
|
||||
public override void VisitText(Text text)
|
||||
{
|
||||
builder.AddContent(0, text.Value);
|
||||
}
|
||||
|
||||
private static bool IsVoidElement(string tagName)
|
||||
{
|
||||
return tagName.ToLowerInvariant() switch
|
||||
{
|
||||
"area" => true,
|
||||
"base" => true,
|
||||
"br" => true,
|
||||
"col" => true,
|
||||
"embed" => true,
|
||||
"hr" => true,
|
||||
"img" => true,
|
||||
"input" => true,
|
||||
"link" => true,
|
||||
"meta" => true,
|
||||
"param" => true,
|
||||
"source" => true,
|
||||
"track" => true,
|
||||
"wbr" => true,
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
|
||||
public override void VisitSoftLineBreak(SoftLineBreak softBreak)
|
||||
{
|
||||
builder.AddContent(0, "\n");
|
||||
}
|
||||
|
||||
public override void VisitHtmlInline(HtmlInline htmlInline)
|
||||
{
|
||||
var match = OutletRegex.Match(htmlInline.Value);
|
||||
|
||||
if (match.Success)
|
||||
{
|
||||
var markerId = Convert.ToInt32(match.Groups[1].Value);
|
||||
outlet(builder, markerId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!options.AllowHtml)
|
||||
{
|
||||
builder.AddContent(0, htmlInline.Value);
|
||||
return;
|
||||
}
|
||||
|
||||
var html = sanitizer.Sanitize(htmlInline.Value);
|
||||
|
||||
var closingMatch = HtmlClosingTagRegex.Match(html);
|
||||
|
||||
if (closingMatch.Success)
|
||||
{
|
||||
builder.CloseElement();
|
||||
return;
|
||||
}
|
||||
|
||||
var openingMatch = HtmlTagRegex.Match(html);
|
||||
|
||||
if (openingMatch.Success)
|
||||
{
|
||||
var tagName = openingMatch.Groups[1].Value;
|
||||
|
||||
builder.OpenElement(0, tagName);
|
||||
|
||||
var attributes = openingMatch.Groups[2].Value;
|
||||
|
||||
if (!string.IsNullOrEmpty(attributes))
|
||||
{
|
||||
var matches = AttributeRegex.Matches(attributes);
|
||||
|
||||
foreach (Match attribute in matches)
|
||||
{
|
||||
var name = attribute.Groups[1].Value;
|
||||
var value = name;
|
||||
|
||||
if (attribute.Groups[2].Success) // Quoted value (either single or double)
|
||||
{
|
||||
value = attribute.Groups[3].Value;
|
||||
}
|
||||
else if (attribute.Groups[4].Success) // Unquoted value
|
||||
{
|
||||
value = attribute.Groups[4].Value;
|
||||
}
|
||||
|
||||
builder.AddAttribute(1, name, value);
|
||||
}
|
||||
}
|
||||
|
||||
if (html.EndsWith("/>") || IsVoidElement(tagName))
|
||||
{
|
||||
builder.CloseElement();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
73
Radzen.Blazor/Markdown/Block.cs
Normal file
73
Radzen.Blazor/Markdown/Block.cs
Normal file
@@ -0,0 +1,73 @@
|
||||
|
||||
namespace Radzen.Blazor.Markdown;
|
||||
|
||||
#nullable enable
|
||||
/// <summary>
|
||||
/// Base class for a markdown block nodes.
|
||||
/// </summary>
|
||||
public abstract class Block : INode
|
||||
{
|
||||
/// <summary>
|
||||
/// Accepts a visitor.
|
||||
/// </summary>
|
||||
/// <param name="visitor"></param>
|
||||
public abstract void Accept(INodeVisitor visitor);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the last child of the block.
|
||||
/// </summary>
|
||||
public virtual Block? LastChild => null;
|
||||
|
||||
/// <summary>
|
||||
/// Returns the first child of the block.
|
||||
/// </summary>
|
||||
public virtual Block? FirstChild => null;
|
||||
|
||||
/// <summary>
|
||||
/// Returns the next sibling of the block.
|
||||
/// </summary>
|
||||
public virtual Block? Next => Parent.NextSibling(this);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the parent node of the block.
|
||||
/// </summary>
|
||||
public BlockContainer Parent { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Removes the block from its parent.
|
||||
/// </summary>
|
||||
public void Remove()
|
||||
{
|
||||
Parent.Remove(this);
|
||||
}
|
||||
|
||||
internal virtual BlockMatch Matches(BlockParser parser) => 0;
|
||||
|
||||
internal bool Open { get; set; } = true;
|
||||
|
||||
internal Range Range;
|
||||
|
||||
internal virtual void Close(BlockParser parser)
|
||||
{
|
||||
Open = false;
|
||||
}
|
||||
}
|
||||
|
||||
enum BlockMatch
|
||||
{
|
||||
Match,
|
||||
Skip,
|
||||
Break
|
||||
}
|
||||
|
||||
struct Position
|
||||
{
|
||||
public int Line { get; set; }
|
||||
public int Column { get; set; }
|
||||
}
|
||||
|
||||
struct Range
|
||||
{
|
||||
public Position Start;
|
||||
public Position End;
|
||||
}
|
||||
87
Radzen.Blazor/Markdown/BlockContainer.cs
Normal file
87
Radzen.Blazor/Markdown/BlockContainer.cs
Normal file
@@ -0,0 +1,87 @@
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Radzen.Blazor.Markdown;
|
||||
|
||||
#nullable enable
|
||||
/// <summary>
|
||||
/// Base class for markdown block nodes that can contain other blocks.
|
||||
/// </summary>
|
||||
public abstract class BlockContainer : Block
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns the children of the block.
|
||||
/// </summary>
|
||||
public IReadOnlyList<Block> Children => children;
|
||||
|
||||
private readonly List<Block> children = [];
|
||||
|
||||
/// <summary>
|
||||
/// Determines if the block can contain the specified node.
|
||||
/// </summary>
|
||||
public virtual bool CanContain(Block node) => false;
|
||||
|
||||
/// <summary>
|
||||
/// Appends a block to the children of the block.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the block.</typeparam>
|
||||
/// <param name="block">The block to add.</param>
|
||||
/// <returns>The added block.</returns>
|
||||
public virtual T Add<T>(T block) where T : Block
|
||||
{
|
||||
children.Add(block);
|
||||
|
||||
block.Parent = this;
|
||||
|
||||
return block;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replaces a block with another block.
|
||||
/// </summary>
|
||||
/// <param name="source">The block to replace.</param>
|
||||
/// <param name="target">The block to replace with.</param>
|
||||
public void Replace(Block source, Block target)
|
||||
{
|
||||
var index = children.IndexOf(source);
|
||||
|
||||
if (index >= 0)
|
||||
{
|
||||
children[index] = target;
|
||||
target.Parent = this;
|
||||
target.Range = source.Range;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a block from the children of the block.
|
||||
/// </summary>
|
||||
/// <param name="block">The block to remove.</param>
|
||||
public void Remove(Block block)
|
||||
{
|
||||
children.Remove(block);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the next sibling of the block.
|
||||
/// </summary>
|
||||
/// <param name="block">The block to get the next sibling of.</param>
|
||||
/// <returns>The next sibling of the block.</returns>
|
||||
public Block? NextSibling(Block block)
|
||||
{
|
||||
var index = children.IndexOf(block);
|
||||
|
||||
if (index >= 0 && index < children.Count - 1)
|
||||
{
|
||||
return children[index + 1];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override Block? LastChild => children.Count > 0 ? children[^1] : null;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override Block? FirstChild => children.Count > 0 ? children[0] : null;
|
||||
}
|
||||
491
Radzen.Blazor/Markdown/BlockParser.cs
Normal file
491
Radzen.Blazor/Markdown/BlockParser.cs
Normal file
@@ -0,0 +1,491 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Radzen.Blazor.Markdown;
|
||||
|
||||
#nullable enable
|
||||
|
||||
class BlockParser
|
||||
{
|
||||
private static readonly string tagName = @"[A-Za-z][A-Za-z0-9-]*";
|
||||
private static readonly string attributeName = @"[a-zA-Z_:][a-zA-Z0-9:._-]*";
|
||||
private static readonly string unquotedValue = @"[^""'=<>`\x00-\x20]+";
|
||||
private static readonly string singleQuotedValue = @"'[^']*'";
|
||||
private static readonly string doubleQuotedValue = @"""[^""]*""";
|
||||
private static readonly string attributeValue = @$"(?:{unquotedValue}|{singleQuotedValue}|{doubleQuotedValue})";
|
||||
private static readonly string attributeValueSpec = @$"(?:\s*=\s*{attributeValue})";
|
||||
private static readonly string attribute = @$"(?:\s+{attributeName}{attributeValueSpec}?)";
|
||||
|
||||
private static readonly string OpenTag = @$"<{tagName}{attribute}*\s*/?>";
|
||||
private static readonly string CloseTag = @$"</{tagName}\s*[>]";
|
||||
private static readonly string htmlComment = @"<!-->|<!--->|<!--[\s\S]*?-->";
|
||||
private static readonly string processingInstruction = @"<\?[ \s\S]*?\?>";
|
||||
private static readonly string declaration = @$"<![A-Za-z]+[^>]*>";
|
||||
private static readonly string cdata = @"<!\[CDATA\[[\s\S]*?\]\]>";
|
||||
|
||||
public static readonly Regex HtmlRegex = new(@$"^(?:{OpenTag}|{CloseTag}|{htmlComment}|{processingInstruction}|{declaration}|{cdata})");
|
||||
|
||||
private BlockParser()
|
||||
{
|
||||
Tip = document;
|
||||
|
||||
OldTip = document;
|
||||
|
||||
lastMatchedContainer = document;
|
||||
}
|
||||
|
||||
public static Document Parse(string markdown)
|
||||
{
|
||||
var parser = new BlockParser();
|
||||
|
||||
var document = parser.ParseBlocks(markdown);
|
||||
|
||||
parser.ParseInlines(document);
|
||||
|
||||
return document;
|
||||
}
|
||||
|
||||
public static readonly Regex NewLineRegex = new(@"\r\n|\r|\n");
|
||||
|
||||
private readonly Document document = new();
|
||||
|
||||
private void ParseInlines(Document document)
|
||||
{
|
||||
var visitor = new InlineVisitor(linkReferences);
|
||||
document.Accept(visitor);
|
||||
}
|
||||
|
||||
public char Peek()
|
||||
{
|
||||
return CurrentLine.Peek(Offset);
|
||||
}
|
||||
|
||||
public char PeekNonSpace(int offset = 0)
|
||||
{
|
||||
return CurrentLine.Peek(NextNonSpace + offset);
|
||||
}
|
||||
|
||||
public void AdvanceOffset(int count, bool columns)
|
||||
{
|
||||
var currentLine = CurrentLine;
|
||||
char c;
|
||||
|
||||
while (count > 0 && Offset < currentLine.Length != default)
|
||||
{
|
||||
c = currentLine[Offset];
|
||||
|
||||
if (c == '\t')
|
||||
{
|
||||
var charsToTab = 4 - (Column % 4);
|
||||
|
||||
if (columns)
|
||||
{
|
||||
PartiallyConsumedTab = charsToTab > count;
|
||||
var charsToAdvance = charsToTab > count ? count : charsToTab;
|
||||
Column += charsToAdvance;
|
||||
Offset += PartiallyConsumedTab ? 0 : 1;
|
||||
count -= charsToAdvance;
|
||||
}
|
||||
else
|
||||
{
|
||||
PartiallyConsumedTab = false;
|
||||
Column += charsToTab;
|
||||
Offset += 1;
|
||||
count -= 1;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
PartiallyConsumedTab = false;
|
||||
Offset += 1;
|
||||
Column += 1;
|
||||
count -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Document ParseBlocks(string markdown)
|
||||
{
|
||||
LineNumber = 0;
|
||||
|
||||
lastMatchedContainer = document;
|
||||
|
||||
var lines = NewLineRegex.Split(markdown);
|
||||
|
||||
var length = lines.Length;
|
||||
|
||||
if (markdown.EndsWith(InlineParser.LineFeed))
|
||||
{
|
||||
length--;
|
||||
}
|
||||
|
||||
for (var index = 0; index < length; index++)
|
||||
{
|
||||
IncorporateLine(lines[index]);
|
||||
}
|
||||
|
||||
while (Tip != null)
|
||||
{
|
||||
Close(Tip, length);
|
||||
}
|
||||
|
||||
return document;
|
||||
}
|
||||
|
||||
private void IncorporateLine(string line)
|
||||
{
|
||||
Offset = 0;
|
||||
Column = 0;
|
||||
Blank = false;
|
||||
PartiallyConsumedTab = false;
|
||||
LineNumber++;
|
||||
|
||||
Block container = document;
|
||||
OldTip = Tip;
|
||||
Block? tail;
|
||||
var allMatched = true;
|
||||
CurrentLine = line;
|
||||
|
||||
while ((tail = container.LastChild) != null && tail.Open)
|
||||
{
|
||||
container = tail;
|
||||
|
||||
FindNextNonSpace();
|
||||
|
||||
switch (container.Matches(this))
|
||||
{
|
||||
case BlockMatch.Match: // we've matched, keep going
|
||||
break;
|
||||
case BlockMatch.Skip: // we've failed to match a block
|
||||
allMatched = false;
|
||||
break;
|
||||
case BlockMatch.Break: // we've hit end of line for fenced code close and can return
|
||||
return;
|
||||
default:
|
||||
throw new InvalidOperationException("Invalid continue result");
|
||||
}
|
||||
|
||||
if (!allMatched)
|
||||
{
|
||||
container = container.Parent;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
AllClosed = container == OldTip;
|
||||
lastMatchedContainer = container;
|
||||
|
||||
var matchedLeaf = container is not (Paragraph or Table) && container is Leaf;
|
||||
|
||||
while (!matchedLeaf)
|
||||
{
|
||||
FindNextNonSpace();
|
||||
|
||||
int blockIndex;
|
||||
|
||||
for (blockIndex = 0; blockIndex < blockStarts.Length; blockIndex++)
|
||||
{
|
||||
var blockStart = blockStarts[blockIndex];
|
||||
|
||||
var result = blockStart(this, container);
|
||||
|
||||
if (result == BlockStart.Container)
|
||||
{
|
||||
container = Tip;
|
||||
break;
|
||||
}
|
||||
else if (result == BlockStart.Leaf)
|
||||
{
|
||||
container = Tip;
|
||||
matchedLeaf = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (blockIndex == blockStarts.Length)
|
||||
{
|
||||
AdvanceNextNonSpace();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// What remains at the offset is a text line. Add the text to the
|
||||
// appropriate container.
|
||||
|
||||
if (!AllClosed && !Blank && this.Tip is Paragraph or Table)
|
||||
{
|
||||
// lazy paragraph continuation
|
||||
if (Tip is Paragraph paragraph)
|
||||
{
|
||||
paragraph.AddLine(this);
|
||||
}
|
||||
else if (Tip is Table table)
|
||||
{
|
||||
table.AddLine(this);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// not a lazy continuation
|
||||
|
||||
// finalize any blocks not matched
|
||||
CloseUnmatchedBlocks();
|
||||
|
||||
if (container is Leaf leaf)
|
||||
{
|
||||
leaf.AddLine(this);
|
||||
|
||||
if (container is HtmlBlock block && block.Type >= 1 && block.Type <= 5 && HtmlBlockCloseRegex[block.Type].IsMatch(line[Offset..]))
|
||||
{
|
||||
LastLineLength = line.Length;
|
||||
Close(container, LineNumber);
|
||||
}
|
||||
}
|
||||
else if (Offset < line.Length && !Blank)
|
||||
{
|
||||
var paragraph = AddChild<Paragraph>(Offset);
|
||||
AdvanceNextNonSpace();
|
||||
paragraph.AddLine(this);
|
||||
}
|
||||
}
|
||||
LastLineLength = line.Length;
|
||||
}
|
||||
|
||||
public int LastLineLength { get; set; }
|
||||
|
||||
public void Close(Block block, int lineNumber)
|
||||
{
|
||||
var above = block.Parent;
|
||||
block.Range.End.Line = lineNumber;
|
||||
block.Range.End.Column = LastLineLength;
|
||||
block.Close(this);
|
||||
Tip = above;
|
||||
}
|
||||
|
||||
public T AddChild<T>(int offset) where T : Block, new()
|
||||
{
|
||||
var node = new T();
|
||||
|
||||
AddChild(node, offset);
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
public void AddChild(Block node, int offset)
|
||||
{
|
||||
while (Tip is not BlockContainer container || !container.CanContain(node))
|
||||
{
|
||||
Close(Tip, LineNumber - 1);
|
||||
}
|
||||
|
||||
if (Tip is BlockContainer parent)
|
||||
{
|
||||
parent.Add(node);
|
||||
}
|
||||
|
||||
var columnNumber = offset + 1; // offset 0 = column 1
|
||||
|
||||
node.Range.Start.Line = LineNumber;
|
||||
node.Range.Start.Column = columnNumber;
|
||||
|
||||
Tip = node;
|
||||
}
|
||||
|
||||
public void CloseUnmatchedBlocks()
|
||||
{
|
||||
if (!AllClosed)
|
||||
{
|
||||
while (OldTip != lastMatchedContainer)
|
||||
{
|
||||
var parent = OldTip.Parent;
|
||||
Close(OldTip, LineNumber - 1);
|
||||
OldTip = parent;
|
||||
}
|
||||
|
||||
AllClosed = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void AdvanceNextNonSpace()
|
||||
{
|
||||
Offset = NextNonSpace;
|
||||
Column = NextNonSpaceColumn;
|
||||
PartiallyConsumedTab = false;
|
||||
}
|
||||
|
||||
private static readonly Func<BlockParser, Block, BlockStart>[] blockStarts =
|
||||
[
|
||||
BlockQuote.Start,
|
||||
AtxHeading.Start,
|
||||
FencedCodeBlock.Start,
|
||||
HtmlBlock.Start,
|
||||
SetExtHeading.Start,
|
||||
ThematicBreak.Start,
|
||||
ListItem.Start,
|
||||
IndentedCodeBlock.Start,
|
||||
Table.Start,
|
||||
];
|
||||
|
||||
public bool AllClosed { get; private set; }
|
||||
|
||||
private Block lastMatchedContainer;
|
||||
|
||||
public void FindNextNonSpace()
|
||||
{
|
||||
var currentLine = CurrentLine;
|
||||
var i = Offset;
|
||||
var cols = Column;
|
||||
char c = default;
|
||||
|
||||
while (i < currentLine.Length)
|
||||
{
|
||||
c = currentLine[i];
|
||||
|
||||
if (c == ' ')
|
||||
{
|
||||
i++;
|
||||
cols++;
|
||||
}
|
||||
else if (c == '\t')
|
||||
{
|
||||
i++;
|
||||
cols += 4 - (cols % 4);
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Blank = c == '\n' || c == '\r' || i == currentLine.Length;
|
||||
NextNonSpace = i;
|
||||
NextNonSpaceColumn = cols;
|
||||
Indent = NextNonSpaceColumn - Column;
|
||||
Indented = Indent >= CodeIndent;
|
||||
}
|
||||
|
||||
public const int CodeIndent = 4;
|
||||
|
||||
public int Indent { get; private set; }
|
||||
public bool Indented { get; private set; }
|
||||
public int NextNonSpaceColumn { get; private set; }
|
||||
|
||||
public int NextNonSpace { get; private set; }
|
||||
|
||||
public bool Blank { get; private set; }
|
||||
public bool PartiallyConsumedTab { get; private set; }
|
||||
public Block Tip { get; set; }
|
||||
public Block OldTip { get; private set; }
|
||||
public string CurrentLine { get; private set; } = string.Empty;
|
||||
public int Offset { get; set; }
|
||||
public int Column { get; set; }
|
||||
public int LineNumber { get; private set; }
|
||||
|
||||
private static readonly Regex LinkReferenceRegex = new(@"^[ \t]{0,3}\[");
|
||||
|
||||
private readonly Dictionary<string, LinkReference> linkReferences = [];
|
||||
|
||||
// https://spec.commonmark.org/0.31.2/#html-blocks
|
||||
internal static readonly Regex[] HtmlBlockOpenRegex = [
|
||||
new (@"."), // dummy for 1 based indexing
|
||||
new (@"^<(?:script|pre|textarea|style)(?:\s|>|$)", RegexOptions.IgnoreCase),
|
||||
new (@"^<!--"),
|
||||
new (@"^<[?]"),
|
||||
new (@"^<![A-Za-z]"),
|
||||
new (@"^<!\[CDATA\["),
|
||||
new (@"^<[/]?(?:address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[123456]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|nav|noframes|ol|optgroup|option|p|param|section|search|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul)(?:\s|[/]?[>]|$)", RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
||||
new (@$"^(?:{OpenTag}|{CloseTag})\s*$", RegexOptions.IgnoreCase)
|
||||
];
|
||||
|
||||
private static readonly Regex[] HtmlBlockCloseRegex = [
|
||||
new (@"."), // dummy for 1 based indexing
|
||||
new (@"</(?:script|pre|textarea|style)>", RegexOptions.IgnoreCase),
|
||||
new (@"-->"),
|
||||
new (@"\?>"),
|
||||
new (@">"),
|
||||
new (@"\]\]>")
|
||||
];
|
||||
|
||||
|
||||
public bool TryParseLinkReference(string markdown, out int newIndex)
|
||||
{
|
||||
newIndex = 0;
|
||||
|
||||
if (!LinkReferenceRegex.IsMatch(markdown))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var position = 0;
|
||||
|
||||
while (position < markdown.Length - 1 && (markdown[position] is not InlineParser.CloseBracket || (position > 0 && markdown[position - 1] is InlineParser.Backslash)))
|
||||
{
|
||||
position++;
|
||||
}
|
||||
|
||||
if (position >= markdown.Length - 1)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
position++;
|
||||
|
||||
if (position >= markdown.Length || markdown[position] is not InlineParser.Colon)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var colonIndex = position;
|
||||
var closeIndex = colonIndex - 1;
|
||||
var openIndex = 0;
|
||||
|
||||
while (openIndex < closeIndex && markdown[openIndex] is not InlineParser.OpenBracket)
|
||||
{
|
||||
openIndex++;
|
||||
}
|
||||
|
||||
if (openIndex == closeIndex)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var id = new StringBuilder();
|
||||
|
||||
for (var index = openIndex + 1; index < closeIndex; index++)
|
||||
{
|
||||
var next = index < closeIndex - 1 ? markdown[index + 1] : default;
|
||||
|
||||
if (markdown[index] is not InlineParser.Backslash || !next.IsPunctuation())
|
||||
{
|
||||
id.Append(markdown[index]);
|
||||
}
|
||||
}
|
||||
|
||||
if (!InlineParser.TryParseDestinationAndTitle(markdown, colonIndex + 1, out var destination, out var title, out position))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var link = new LinkReference { Destination = destination, Title = title };
|
||||
|
||||
var key = id.ToString().ToLowerInvariant();
|
||||
|
||||
if (!linkReferences.ContainsKey(key))
|
||||
{
|
||||
linkReferences[key] = link;
|
||||
}
|
||||
|
||||
newIndex = position;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
enum BlockStart
|
||||
{
|
||||
Skip,
|
||||
Container,
|
||||
Leaf
|
||||
}
|
||||
64
Radzen.Blazor/Markdown/BlockQuote.cs
Normal file
64
Radzen.Blazor/Markdown/BlockQuote.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
namespace Radzen.Blazor.Markdown;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Represents a markdown block quote: <c>> Quote</c>.
|
||||
/// </summary>
|
||||
public class BlockQuote : BlockContainer
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override void Accept(INodeVisitor visitor)
|
||||
{
|
||||
visitor.VisitBlockQuote(this);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool CanContain(Block node)
|
||||
{
|
||||
return node is not ListItem;
|
||||
}
|
||||
|
||||
internal override BlockMatch Matches(BlockParser parser)
|
||||
{
|
||||
if (!parser.Indented && parser.PeekNonSpace() == '>')
|
||||
{
|
||||
parser.AdvanceNextNonSpace();
|
||||
parser.AdvanceOffset(1, false);
|
||||
// optional following space
|
||||
|
||||
if (parser.Peek().IsSpaceOrTab())
|
||||
{
|
||||
parser.AdvanceOffset(1, true);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return BlockMatch.Skip;
|
||||
}
|
||||
|
||||
return BlockMatch.Match;
|
||||
}
|
||||
|
||||
internal static BlockStart Start(BlockParser parser, Block block)
|
||||
{
|
||||
if (!parser.Indented && parser.PeekNonSpace() == '>')
|
||||
{
|
||||
parser.AdvanceNextNonSpace();
|
||||
parser.AdvanceOffset(1, false);
|
||||
// optional following space
|
||||
|
||||
if (parser.Peek().IsSpaceOrTab())
|
||||
{
|
||||
parser.AdvanceOffset(1, true);
|
||||
}
|
||||
|
||||
parser.CloseUnmatchedBlocks();
|
||||
|
||||
parser.AddChild<BlockQuote>(parser.NextNonSpace);
|
||||
|
||||
return BlockStart.Container;
|
||||
}
|
||||
|
||||
return BlockStart.Skip;
|
||||
}
|
||||
}
|
||||
10
Radzen.Blazor/Markdown/CharExtensions.cs
Normal file
10
Radzen.Blazor/Markdown/CharExtensions.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace Radzen.Blazor.Markdown;
|
||||
|
||||
static class CharExtensions
|
||||
{
|
||||
public static bool IsNullOrWhiteSpace(this char ch) => ch == '\0' || char.IsWhiteSpace(ch);
|
||||
|
||||
public static bool IsPunctuation(this char ch) => char.IsPunctuation(ch);
|
||||
|
||||
public static bool IsSpaceOrTab(this char ch) => ch == ' ' || ch == '\t';
|
||||
}
|
||||
19
Radzen.Blazor/Markdown/Code.cs
Normal file
19
Radzen.Blazor/Markdown/Code.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
namespace Radzen.Blazor.Markdown;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a markdown inline code block: <c>`code`</c>.
|
||||
/// </summary>
|
||||
/// <param name="value"></param>
|
||||
public class Code(string value) : Inline
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the code value.
|
||||
/// </summary>
|
||||
public string Value { get; set; } = value;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Accept(INodeVisitor visitor)
|
||||
{
|
||||
visitor.VisitCode(this);
|
||||
}
|
||||
}
|
||||
35
Radzen.Blazor/Markdown/Document.cs
Normal file
35
Radzen.Blazor/Markdown/Document.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
namespace Radzen.Blazor.Markdown;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a markdown document.
|
||||
/// </summary>
|
||||
public class Document : BlockContainer
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Document"/> class.
|
||||
/// </summary>
|
||||
public Document()
|
||||
{
|
||||
Range.Start.Line = 1;
|
||||
Range.Start.Column = 1;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Accept(INodeVisitor visitor)
|
||||
{
|
||||
visitor.VisitDocument(this);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool CanContain(Block node)
|
||||
{
|
||||
return node is not ListItem;
|
||||
}
|
||||
|
||||
internal override void Close(BlockParser parser)
|
||||
{
|
||||
base.Close(parser);
|
||||
|
||||
LinkReferenceParser.Parse(parser, this);
|
||||
}
|
||||
}
|
||||
13
Radzen.Blazor/Markdown/Emphasis.cs
Normal file
13
Radzen.Blazor/Markdown/Emphasis.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace Radzen.Blazor.Markdown;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an emphasis element in a markdown document: <c>_emphasis_</c> or <c>*emphasis*</c>.
|
||||
/// </summary>
|
||||
public class Emphasis : InlineContainer
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override void Accept(INodeVisitor visitor)
|
||||
{
|
||||
visitor.VisitEmphasis(this);
|
||||
}
|
||||
}
|
||||
98
Radzen.Blazor/Markdown/FencedCodeBlock.cs
Normal file
98
Radzen.Blazor/Markdown/FencedCodeBlock.cs
Normal file
@@ -0,0 +1,98 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Radzen.Blazor.Markdown;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a fenced code block in a markdown document: <c>```</c> or <c>~~~</c>.
|
||||
/// </summary>
|
||||
public class FencedCodeBlock : Leaf
|
||||
{
|
||||
/// <summary>
|
||||
/// The delimiter used to start and end the code block.
|
||||
/// </summary>
|
||||
public string Delimiter { get; private set; }
|
||||
internal int Indent { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The info string of the code block. This is the first line of the code block and is used to specify the language of the code block.
|
||||
/// </summary>
|
||||
public string Info { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Accept(INodeVisitor visitor)
|
||||
{
|
||||
visitor.VisitFencedCodeBlock(this);
|
||||
}
|
||||
|
||||
internal override void Close(BlockParser parser)
|
||||
{
|
||||
base.Close(parser);
|
||||
|
||||
// first line becomes info string
|
||||
var newlinePos = Value.IndexOf('\n');
|
||||
var firstLine = Value[..newlinePos];
|
||||
Info = firstLine.Trim();
|
||||
Value = Value[(newlinePos + 1)..];
|
||||
}
|
||||
|
||||
internal override BlockMatch Matches(BlockParser parser)
|
||||
{
|
||||
var line = parser.CurrentLine[parser.NextNonSpace..];
|
||||
|
||||
var indent = parser.Indent;
|
||||
|
||||
var match = ClosingFenceRegex.Match(line);
|
||||
|
||||
if (indent <= 3 && parser.PeekNonSpace() == Delimiter[0] && match.Success && match.Length >= Delimiter.Length)
|
||||
{
|
||||
// closing fence - we're at end of line, so we can return
|
||||
parser.LastLineLength = parser.Offset + indent + match.Length;
|
||||
parser.Close(this, parser.LineNumber);
|
||||
return BlockMatch.Break;
|
||||
}
|
||||
else
|
||||
{
|
||||
// skip optional spaces of fence offset
|
||||
var i = Indent;
|
||||
|
||||
while (i > 0 && parser.Peek().IsSpaceOrTab())
|
||||
{
|
||||
parser.AdvanceOffset(1, true);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
|
||||
return BlockMatch.Match;
|
||||
}
|
||||
|
||||
|
||||
private static readonly Regex ClosingFenceRegex = new(@"^(?:`{3,}|~{3,})(?=[ \t]*$)");
|
||||
|
||||
private static readonly Regex OpeningFenceRegex = new(@"^`{3,}(?!.*`)|^~{3,}");
|
||||
|
||||
internal static BlockStart Start(BlockParser parser, Block node)
|
||||
{
|
||||
if (parser.Indented)
|
||||
{
|
||||
return BlockStart.Skip;
|
||||
}
|
||||
|
||||
var line = parser.CurrentLine[parser.NextNonSpace..];
|
||||
|
||||
var match = OpeningFenceRegex.Match(line);
|
||||
|
||||
if (match.Success)
|
||||
{
|
||||
parser.CloseUnmatchedBlocks();
|
||||
|
||||
var container = parser.AddChild<FencedCodeBlock>(parser.NextNonSpace);
|
||||
container.Delimiter = match.Value;
|
||||
container.Indent = parser.Indent;
|
||||
parser.AdvanceNextNonSpace();
|
||||
parser.AdvanceOffset(match.Value.Length, false);
|
||||
return BlockStart.Leaf;
|
||||
}
|
||||
|
||||
return BlockStart.Skip;
|
||||
}
|
||||
}
|
||||
25
Radzen.Blazor/Markdown/Heading.cs
Normal file
25
Radzen.Blazor/Markdown/Heading.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
|
||||
namespace Radzen.Blazor.Markdown;
|
||||
|
||||
/// <summary>
|
||||
/// A base class for all heading elements.
|
||||
/// </summary>
|
||||
public abstract class Heading : Leaf
|
||||
{
|
||||
/// <summary>
|
||||
/// The level of the heading. The value is between 1 and 6.
|
||||
/// </summary>
|
||||
public int Level { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Accept(INodeVisitor visitor)
|
||||
{
|
||||
visitor.VisitHeading(this);
|
||||
}
|
||||
|
||||
internal override BlockMatch Matches(BlockParser parser)
|
||||
{
|
||||
// a heading can never container another line
|
||||
return BlockMatch.Skip;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user