mirror of
https://github.com/radzenhq/radzen-blazor.git
synced 2026-02-04 05:35:44 +00:00
Compare commits
869 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
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: 8.0.x
|
||||
dotnet-version: 9.0.x
|
||||
|
||||
- name: Build
|
||||
run: dotnet build Radzen.Blazor/Radzen.Blazor.csproj
|
||||
- name: Test
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -341,3 +341,4 @@ Radzen.DocFX/_exported_templates
|
||||
Radzen.DocFX/api/*.yml
|
||||
!Radzen.DocFX/api/index.md
|
||||
Radzen.DocFX/api/.manifest
|
||||
Radzen.Blazor.min.js
|
||||
|
||||
@@ -17,7 +17,7 @@ COPY RadzenBlazorDemos.Host /app/RadzenBlazorDemos.Host
|
||||
WORKDIR /app
|
||||
RUN docfx DocFX/docfx.json
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:8.0
|
||||
FROM mcr.microsoft.com/dotnet/sdk:9.0
|
||||
|
||||
COPY --from=0 /app/RadzenBlazorDemos.Host /app/RadzenBlazorDemos.Host
|
||||
COPY --from=0 /app/RadzenBlazorDemos /app/RadzenBlazorDemos
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018-2024 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
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
Radzen Blazor Components
|
||||
========================
|
||||
|
||||
A set of **70+ free and open source** native Blazor UI controls.
|
||||
A set of **90+ free and open source** native Blazor UI controls.
|
||||
|
||||
See Online Demos or Read the Docs
|
||||
|
||||
|
||||
@@ -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}"" alt=""button"" />", component.Markup);
|
||||
Assert.Contains(@$"<img class=""notranslate rz-button-icon-left rzi"" src=""{image}"" alt=""button"" />", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -106,7 +106,7 @@ namespace Radzen.Blazor.Tests
|
||||
parameters.Add(p => p.ImageAlternateText, text);
|
||||
});
|
||||
|
||||
Assert.Contains(@$"<img class=""rz-button-icon-left rzi"" src=""{image}"" alt=""{text}"" />", 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);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ 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;
|
||||
@@ -23,6 +24,9 @@ public class ChartTests
|
||||
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 =>
|
||||
@@ -42,12 +46,12 @@ public class ChartTests
|
||||
})));
|
||||
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
foreach (var _ in Enumerable.Range(0, 10))
|
||||
foreach (var invocation in Enumerable.Range(0, 10))
|
||||
{
|
||||
await chart.InvokeAsync(() => chart.Instance.MouseMove(100, 80));
|
||||
Assert.Contains("<div class=\"rz-chart-tooltip", chart.Markup);
|
||||
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}");
|
||||
}
|
||||
|
||||
@@ -824,10 +824,14 @@ namespace Radzen.Blazor.Tests
|
||||
|
||||
component.SetParametersAndRender(parameters =>
|
||||
{
|
||||
parameters.Add<int>(p => p.PageSize, 20);
|
||||
parameters.Add<LoadDataArgs>(p => p.LoadData, args => { raised = true; newArgs = args; });
|
||||
});
|
||||
|
||||
component.SetParametersAndRender(parameters =>
|
||||
{
|
||||
parameters.Add<int>(p => p.PageSize, 20);
|
||||
});
|
||||
|
||||
component.Find(".rz-pager-next").Click();
|
||||
|
||||
Assert.True(raised);
|
||||
|
||||
@@ -155,7 +155,7 @@ namespace Radzen.Blazor.Tests
|
||||
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]
|
||||
@@ -174,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()
|
||||
{
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -43,7 +43,7 @@ public class DollarsTypeConverter : TypeConverter
|
||||
return new Dollars(d);
|
||||
|
||||
if (value is string s)
|
||||
return decimal.TryParse(s, out var val) ? new Dollars(val) : null;
|
||||
return decimal.TryParse(s, culture, out var val) ? new Dollars(val) : null;
|
||||
|
||||
return base.ConvertFrom(context, culture, value);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using AngleSharp.Dom;
|
||||
using Bunit;
|
||||
@@ -13,18 +15,20 @@ namespace Radzen.Blazor.Tests
|
||||
{
|
||||
public string Text { get; set; }
|
||||
public int Id { get; set; }
|
||||
public bool Disabled { get; set; } = false;
|
||||
}
|
||||
|
||||
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));
|
||||
|
||||
@@ -99,7 +103,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));
|
||||
});
|
||||
|
||||
@@ -114,6 +119,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<string>(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()
|
||||
{
|
||||
@@ -121,7 +155,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);
|
||||
});
|
||||
@@ -245,5 +280,78 @@ namespace Radzen.Blazor.Tests
|
||||
|
||||
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<DataItem>>(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"));
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
304
Radzen.Blazor.Tests/ExpressionSerializerTests.cs
Normal file
304
Radzen.Blazor.Tests/ExpressionSerializerTests.cs
Normal file
@@ -0,0 +1,304 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
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_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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(@$"class=""rzi""", component.Markup);
|
||||
Assert.Contains(@$"class=""notranslate rzi""", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -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,12 +119,12 @@ 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.Add<bool>(p => p.AutoComplete, true));
|
||||
component.SetParametersAndRender(parameters => parameters.AddUnmatched("AutoComplete", true));
|
||||
|
||||
Assert.Contains(@$"autocomplete=""on""", component.Markup);
|
||||
Assert.DoesNotContain(@$"aria-autocomplete", component.Markup);
|
||||
@@ -135,7 +135,7 @@ namespace Radzen.Blazor.Tests
|
||||
Assert.DoesNotContain(@$"aria-autocomplete", component.Markup);
|
||||
|
||||
component.Instance.DefaultAutoCompleteAttribute = "autocomplete-custom";
|
||||
component.SetParametersAndRender(parameters => parameters.Add(p => p.AutoComplete, false));
|
||||
component.SetParametersAndRender(parameters => parameters.AddUnmatched("AutoComplete", false));
|
||||
|
||||
Assert.Contains(@$"autocomplete=""autocomplete-custom""", component.Markup);
|
||||
Assert.Contains(@$"aria-autocomplete=""none""", component.Markup);
|
||||
@@ -148,22 +148,22 @@ 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));
|
||||
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=""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.AdditionalName));
|
||||
|
||||
Assert.Contains(@$"autocomplete=""{AutoCompleteType.AdditionalName.GetAutoCompleteValue()}""", 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.Email));
|
||||
|
||||
Assert.Contains(@$"autocomplete=""{AutoCompleteType.Email.GetAutoCompleteValue()}""", component.Markup);
|
||||
|
||||
@@ -28,6 +28,10 @@ namespace Radzen.Blazor.Tests
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public bool Disabled { get; set; }
|
||||
public bool Visible { get; set; }
|
||||
public IFormFieldContext FormFieldContext => null;
|
||||
|
||||
public object Value { get; set; }
|
||||
}
|
||||
|
||||
@@ -60,6 +64,19 @@ 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()
|
||||
@@ -156,4 +173,4 @@ namespace Radzen.Blazor.Tests
|
||||
Assert.False(component.Instance.Validate(DateTime.Now));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,12 +216,12 @@ 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.Add<bool>(p => p.AutoComplete, true));
|
||||
component.SetParametersAndRender(parameters => parameters.AddUnmatched("AutoComplete", true));
|
||||
|
||||
Assert.Contains(@$"autocomplete=""on""", component.Markup);
|
||||
Assert.DoesNotContain(@$"aria-autocomplete", component.Markup);
|
||||
@@ -232,7 +232,7 @@ namespace Radzen.Blazor.Tests
|
||||
Assert.DoesNotContain(@$"aria-autocomplete", component.Markup);
|
||||
|
||||
component.Instance.DefaultAutoCompleteAttribute = "autocomplete-custom";
|
||||
component.SetParametersAndRender(parameters => parameters.Add(p => p.AutoComplete, false));
|
||||
component.SetParametersAndRender(parameters => parameters.AddUnmatched("AutoComplete", false));
|
||||
|
||||
Assert.Contains(@$"autocomplete=""autocomplete-custom""", component.Markup);
|
||||
Assert.Contains(@$"aria-autocomplete=""none""", component.Markup);
|
||||
@@ -245,22 +245,22 @@ 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));
|
||||
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=""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.BdayMonth));
|
||||
|
||||
Assert.Contains(@$"autocomplete=""{AutoCompleteType.BdayMonth.GetAutoCompleteValue()}""", 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.BdayYear));
|
||||
|
||||
Assert.Contains(@$"autocomplete=""{AutoCompleteType.BdayYear.GetAutoCompleteValue()}""", component.Markup);
|
||||
@@ -463,10 +463,11 @@ namespace Radzen.Blazor.Tests
|
||||
ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");
|
||||
|
||||
var value = new Dollars(11m);
|
||||
Dollars? ConvertFunc(string s) => decimal.TryParse(s, out var val) ? new Dollars(val) : null;
|
||||
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?>.Value), value),
|
||||
ComponentParameter.CreateParameter(nameof(RadzenNumeric<Dollars>.Culture), System.Globalization.CultureInfo.InvariantCulture)
|
||||
);
|
||||
|
||||
component.Render();
|
||||
@@ -494,7 +495,101 @@ namespace Radzen.Blazor.Tests
|
||||
|
||||
component.Render();
|
||||
|
||||
Assert.Contains($" value=\"{valueToTest.ToString(format)}\"", component.Markup);
|
||||
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]
|
||||
@@ -517,10 +612,10 @@ namespace Radzen.Blazor.Tests
|
||||
});
|
||||
});
|
||||
|
||||
component.Find("input").Change("13.53");
|
||||
component.Find("input").Change(13.53);
|
||||
|
||||
var maxDollars = new Dollars(2);
|
||||
Assert.Contains($" value=\"{maxDollars.ToString()}\"", component.Markup);
|
||||
var maxDollars = new Dollars(maxValue);
|
||||
Assert.Contains($" value=\"{maxDollars}\"", component.Markup);
|
||||
Assert.Equal(component.Instance.Value, maxDollars);
|
||||
}
|
||||
|
||||
|
||||
@@ -109,5 +109,51 @@ namespace Radzen.Blazor.Tests
|
||||
|
||||
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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,11 +119,11 @@ 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);
|
||||
|
||||
@@ -139,22 +139,22 @@ 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));
|
||||
component.SetParametersAndRender(parameters => parameters.Add<AutoCompleteType>(p => p.AutoCompleteType, AutoCompleteType.On));
|
||||
|
||||
Assert.Contains(@$"autocomplete=""new-password""", 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=""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.CurrentPassword));
|
||||
|
||||
Assert.Contains(@$"autocomplete=""{AutoCompleteType.CurrentPassword.GetAutoCompleteValue()}""", 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.NewPassword));
|
||||
|
||||
Assert.Contains(@$"autocomplete=""{AutoCompleteType.NewPassword.GetAutoCompleteValue()}""", component.Markup);
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8</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>
|
||||
|
||||
@@ -45,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]
|
||||
@@ -63,7 +63,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);
|
||||
}
|
||||
|
||||
@@ -78,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}"" alt=""image"" />", component.Markup);
|
||||
Assert.Contains(@$"<img class=""notranslate rz-button-icon-left rzi"" src=""{image}"" alt=""image"" />", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -97,7 +97,7 @@ namespace Radzen.Blazor.Tests
|
||||
parameters.Add(p => p.ImageAlternateText, text);
|
||||
});
|
||||
|
||||
Assert.Contains(@$"<img class=""rz-button-icon-left rzi"" src=""{image}"" alt=""{text}"" />", 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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,12 +119,12 @@ 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.Add<bool>(p => p.AutoComplete, true));
|
||||
component.SetParametersAndRender(parameters => parameters.AddUnmatched("AutoComplete", true));
|
||||
|
||||
Assert.Contains(@$"autocomplete=""on""", component.Markup);
|
||||
Assert.DoesNotContain(@$"aria-autocomplete", component.Markup);
|
||||
@@ -135,7 +135,7 @@ namespace Radzen.Blazor.Tests
|
||||
Assert.DoesNotContain(@$"aria-autocomplete", component.Markup);
|
||||
|
||||
component.Instance.DefaultAutoCompleteAttribute = "autocomplete-custom";
|
||||
component.SetParametersAndRender(parameters => parameters.Add(p => p.AutoComplete, false));
|
||||
component.SetParametersAndRender(parameters => parameters.AddUnmatched("AutoComplete", false));
|
||||
|
||||
Assert.Contains(@$"autocomplete=""autocomplete-custom""", component.Markup);
|
||||
Assert.Contains(@$"aria-autocomplete=""none""", component.Markup);
|
||||
@@ -148,22 +148,22 @@ 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));
|
||||
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=""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.AdditionalName));
|
||||
|
||||
Assert.Contains(@$"autocomplete=""{AutoCompleteType.AdditionalName.GetAutoCompleteValue()}""", 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.FamilyName));
|
||||
|
||||
Assert.Contains(@$"autocomplete=""{AutoCompleteType.FamilyName.GetAutoCompleteValue()}""", component.Markup);
|
||||
|
||||
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
@@ -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,9 +2,10 @@ 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.Net.Mime;
|
||||
using Microsoft.AspNetCore.Components.Rendering;
|
||||
|
||||
namespace Radzen.Blazor
|
||||
{
|
||||
@@ -408,7 +409,7 @@ namespace Radzen.Blazor
|
||||
|
||||
if (IsDate(CategoryProperty) || IsNumeric(CategoryProperty))
|
||||
{
|
||||
Items = Items.AsQueryable().OrderBy(DynamicLinqCustomTypeProvider.ParsingConfig, CategoryProperty).ToList();
|
||||
Items = Items.AsQueryable().OrderBy(CategoryProperty).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -476,28 +477,74 @@ namespace Radzen.Blazor
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual RenderFragment RenderTooltip(object data, double marginLeft, double marginTop, double chartHeight)
|
||||
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>
|
||||
@@ -520,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>();
|
||||
|
||||
@@ -538,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();
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,11 +7,11 @@ using Radzen.Blazor.Rendering;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Linq.Dynamic.Core;
|
||||
using System.Linq.Dynamic.Core.Parser;
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
using System.Reflection.Metadata;
|
||||
using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Text.Json;
|
||||
@@ -371,6 +371,40 @@ namespace Radzen
|
||||
December = 11,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the time unit of <see cref="TimeSpan"/>.
|
||||
/// </summary>
|
||||
public enum TimeSpanUnit
|
||||
{
|
||||
/// <summary>
|
||||
/// Day.
|
||||
/// </summary>
|
||||
Day = 0,
|
||||
/// <summary>
|
||||
/// Hour.
|
||||
/// </summary>
|
||||
Hour = 1,
|
||||
/// <summary>
|
||||
/// Minute.
|
||||
/// </summary>
|
||||
Minute = 2,
|
||||
/// <summary>
|
||||
/// Second.
|
||||
/// </summary>
|
||||
Second = 3,
|
||||
/// <summary>
|
||||
/// Millisecond.
|
||||
/// </summary>
|
||||
Millisecond = 4
|
||||
#if NET7_0_OR_GREATER
|
||||
,
|
||||
/// <summary>
|
||||
/// Microsecond.
|
||||
/// </summary>
|
||||
Microsecond = 5
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Html editor mode (Rendered or Raw). Also used for toolbar buttons to enable/disable according to mode.
|
||||
/// </summary>
|
||||
@@ -530,6 +564,12 @@ namespace Radzen
|
||||
/// Gets the dropped item.
|
||||
/// </summary>
|
||||
public TItem ToItem { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// The data that underlies a drag-and-drop operation, known as the drag data store.
|
||||
/// See <see cref="DataTransfer"/>.
|
||||
/// </summary>
|
||||
public DataTransfer DataTransfer { get; set; } = default!;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -551,7 +591,6 @@ namespace Radzen
|
||||
/// Gets or sets a value indicating whether this item is visible.
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if visible; otherwise, <c>false</c>.</value>
|
||||
[Parameter]
|
||||
public bool Visible { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
@@ -574,14 +613,12 @@ namespace Radzen
|
||||
/// Gets or sets a value indicating whether this item is visible.
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if visible; otherwise, <c>false</c>.</value>
|
||||
[Parameter]
|
||||
public bool Visible { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this item is visible.
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if visible; otherwise, <c>false</c>.</value>
|
||||
[Parameter]
|
||||
public bool Disabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
@@ -612,6 +649,34 @@ namespace Radzen
|
||||
public RadzenListBox<TValue> ListBox { get; internal set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Supplies information about RadzenPickList ItemRender event.
|
||||
/// </summary>
|
||||
public class PickListItemRenderEventArgs<TItem>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the data item.
|
||||
/// </summary>
|
||||
public TItem Item { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this item is visible.
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if visible; otherwise, <c>false</c>.</value>
|
||||
public bool Visible { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this item is visible.
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if visible; otherwise, <c>false</c>.</value>
|
||||
public bool Disabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the row HTML attributes.
|
||||
/// </summary>
|
||||
public IDictionary<string, object> Attributes { get; private set; } = new Dictionary<string, object>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Supplies information about a <see cref="RadzenDatePicker{TValue}.DateRender" /> event that is being raised.
|
||||
/// </summary>
|
||||
@@ -702,9 +767,14 @@ namespace Radzen
|
||||
public AppointmentData Appointment { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the time span.
|
||||
/// Gets or sets the time span which represents the difference between slot start and appointment start.
|
||||
/// </summary>
|
||||
public TimeSpan TimeSpan { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the date of the slot where the appointment is moved.
|
||||
/// </summary>
|
||||
public DateTime SlotDate { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -758,6 +828,11 @@ namespace Radzen
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if expandable; otherwise, <c>false</c>.</value>
|
||||
public bool Expandable { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating row index.
|
||||
/// </summary>
|
||||
public int Index { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -978,7 +1053,7 @@ namespace Radzen
|
||||
{
|
||||
get
|
||||
{
|
||||
return _size != default(long) ? _size : source.Size;
|
||||
return _size != default(long) ? _size : source != null ? source.Size : 0;
|
||||
}
|
||||
set
|
||||
{
|
||||
@@ -1015,6 +1090,21 @@ namespace Radzen
|
||||
/// </summary>
|
||||
public class PreviewFileInfo : FileInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of PreviewFileInfo from a browser file.
|
||||
/// </summary>
|
||||
/// <param name="files"></param>
|
||||
public PreviewFileInfo(IBrowserFile files) : base(files)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new, empty instance of PreviewFileInfo.
|
||||
/// </summary>
|
||||
public PreviewFileInfo()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the URL of the previewed file.
|
||||
/// </summary>
|
||||
@@ -1026,11 +1116,41 @@ namespace Radzen
|
||||
/// </summary>
|
||||
public class Query
|
||||
{
|
||||
Func<string> _getFilter;
|
||||
internal Func<string> GetFilter
|
||||
{
|
||||
get
|
||||
{
|
||||
return _getFilter;
|
||||
}
|
||||
set
|
||||
{
|
||||
_filter = null;
|
||||
_getFilter = value;
|
||||
}
|
||||
}
|
||||
|
||||
string _filter;
|
||||
/// <summary>
|
||||
/// Gets or sets the filter.
|
||||
/// Gets the filter expression as a string.
|
||||
/// </summary>
|
||||
/// <value>The filter.</value>
|
||||
public string Filter { get; set; }
|
||||
public string Filter
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_filter == null && GetFilter != null)
|
||||
{
|
||||
_filter = GetFilter();
|
||||
}
|
||||
return _filter;
|
||||
}
|
||||
set
|
||||
{
|
||||
_filter = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the filter expression as a collection of filter descriptors.
|
||||
/// </summary>
|
||||
@@ -2199,6 +2319,28 @@ namespace Radzen
|
||||
public double Width { get; internal set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Supplies information about a <see cref="RadzenDataGrid{TItem}.ColumnReordering" /> event that is being raised.
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
public class DataGridColumnReorderingEventArgs<T>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the reordered RadzenDataGridColumn.
|
||||
/// </summary>
|
||||
public RadzenDataGridColumn<T> Column { get; internal set; }
|
||||
/// <summary>
|
||||
/// Gets the reordered to RadzenDataGridColumn.
|
||||
/// </summary>
|
||||
public RadzenDataGridColumn<T> ToColumn { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value which will cancel the event.
|
||||
/// </summary>
|
||||
/// <value><c>true</c> to cancel the event; otherwise, <c>false</c>.</value>
|
||||
public bool Cancel { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Supplies information about a <see cref="RadzenDataGrid{TItem}.ColumnReordered" /> event that is being raised.
|
||||
/// </summary>
|
||||
@@ -2229,6 +2371,18 @@ namespace Radzen
|
||||
/// </summary>
|
||||
/// <value>The property.</value>
|
||||
public string Property { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the property type.
|
||||
/// </summary>
|
||||
/// <value>The property type.</value>
|
||||
public Type Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the filtered property.
|
||||
/// </summary>
|
||||
/// <value>The property.</value>
|
||||
public string FilterProperty { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the value to filter by.
|
||||
/// </summary>
|
||||
@@ -2267,6 +2421,18 @@ namespace Radzen
|
||||
/// <value>The property.</value>
|
||||
public string Property { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the property type.
|
||||
/// </summary>
|
||||
/// <value>The property type.</value>
|
||||
public Type Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the filtered property.
|
||||
/// </summary>
|
||||
/// <value>The property.</value>
|
||||
public string FilterProperty { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the value to filter by.
|
||||
/// </summary>
|
||||
@@ -2370,6 +2536,43 @@ namespace Radzen
|
||||
public int Level { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The result of a call to a <see cref="QueryableExtension"/>.GroupByMany() overload.
|
||||
/// </summary>
|
||||
public class GroupResult
|
||||
{
|
||||
/// <summary>
|
||||
/// The key value of the group.
|
||||
/// </summary>
|
||||
public dynamic Key { get; internal set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// The number of resulting elements in the group.
|
||||
/// </summary>
|
||||
public int Count { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// The resulting elements in the group.
|
||||
/// </summary>
|
||||
public IEnumerable Items { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// The resulting subgroups in the group.
|
||||
/// </summary>
|
||||
public IEnumerable<GroupResult> Subgroups { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns a <see cref="System.String" /> showing the key of the group and the number of items in the group.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// A <see cref="System.String" /> that represents this instance.
|
||||
/// </returns>
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format(CultureInfo.CurrentCulture, "{0} ({1})", ((object)Key).ToString(), Count);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Supplies information about a <see cref="PagedDataBoundComponent{TItem}.LoadData" /> event that is being raised.
|
||||
/// </summary>
|
||||
@@ -2388,11 +2591,43 @@ namespace Radzen
|
||||
/// Gets the sort expression as a string.
|
||||
/// </summary>
|
||||
public string OrderBy { get; set; }
|
||||
|
||||
Func<string> _getFilter;
|
||||
internal Func<string> GetFilter
|
||||
{
|
||||
get
|
||||
{
|
||||
return _getFilter;
|
||||
}
|
||||
set
|
||||
{
|
||||
_filter = null;
|
||||
_getFilter = value;
|
||||
}
|
||||
}
|
||||
|
||||
string _filter;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the filter expression as a string.
|
||||
/// </summary>
|
||||
/// <value>The filter.</value>
|
||||
public string Filter { get; set; }
|
||||
public string Filter
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_filter == null && GetFilter != null)
|
||||
{
|
||||
_filter = GetFilter();
|
||||
}
|
||||
return _filter;
|
||||
}
|
||||
set
|
||||
{
|
||||
_filter = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the filter expression as a collection of filter descriptors.
|
||||
/// </summary>
|
||||
@@ -2844,9 +3079,14 @@ namespace Radzen
|
||||
/// </summary>
|
||||
/// <param name="value">The value.</param>
|
||||
/// <param name="type">The type.</param>
|
||||
/// <param name="culture">The culture.</param>
|
||||
/// <returns>System.Object</returns>
|
||||
public static object ChangeType(object value, Type type)
|
||||
public static object ChangeType(object value, Type type, CultureInfo culture = null)
|
||||
{
|
||||
if (culture == null)
|
||||
{
|
||||
culture = CultureInfo.CurrentCulture;
|
||||
}
|
||||
if (value == null && Nullable.GetUnderlyingType(type) != null)
|
||||
{
|
||||
return value;
|
||||
@@ -2874,7 +3114,7 @@ namespace Radzen
|
||||
|
||||
}
|
||||
|
||||
return value is IConvertible ? Convert.ChangeType(value, Nullable.GetUnderlyingType(type) ?? type) : value;
|
||||
return value is IConvertible ? Convert.ChangeType(value, Nullable.GetUnderlyingType(type) ?? type, culture) : value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2895,7 +3135,9 @@ namespace Radzen
|
||||
{
|
||||
if (propertyName.Contains("["))
|
||||
{
|
||||
return DynamicExpressionParser.ParseLambda<TItem, TValue>(null, false, propertyName).Compile();
|
||||
var arg = Expression.Parameter(typeof(TItem));
|
||||
|
||||
return Expression.Lambda<Func<TItem, TValue>>(QueryableExtension.GetNestedPropertyExpression(arg, propertyName, type), arg).Compile();
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -3036,23 +3278,7 @@ namespace Radzen
|
||||
/// <param name="property">The property.</param>
|
||||
public static string GetProperty(string property)
|
||||
{
|
||||
Type type = null;
|
||||
try
|
||||
{
|
||||
type = Type.GetType($"System.{property}");
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore the exception and assume the property start without a type and do not need the '@' prefix
|
||||
}
|
||||
var propertyName = $"{(type != null ? "@" : "")}{property}";
|
||||
|
||||
if (propertyName.IndexOf(".") != -1)
|
||||
{
|
||||
return $"np({propertyName})";
|
||||
}
|
||||
|
||||
return propertyName;
|
||||
return property;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -3091,7 +3317,7 @@ namespace Radzen
|
||||
{
|
||||
var type = data.GetType();
|
||||
var arg = Expression.Parameter(typeof(object));
|
||||
var body = Expression.Property(Expression.Convert(arg, type), propertyName);
|
||||
var body = Expression.Convert(Expression.Property(Expression.Convert(arg, type), propertyName), typeof(T));
|
||||
|
||||
return Expression.Lambda<Func<object, T>>(body, arg).Compile();
|
||||
}
|
||||
@@ -3268,6 +3494,21 @@ namespace Radzen
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the dynamic property expression when binding to IDictionary.
|
||||
/// </summary>
|
||||
/// <param name="name">The property name.</param>
|
||||
/// <param name="type">The property type.</param>
|
||||
/// <returns>Dynamic property expression.</returns>
|
||||
public static string GetDynamicPropertyExpression(string name, Type type)
|
||||
{
|
||||
var isEnum = type.IsEnum || Nullable.GetUnderlyingType(type)?.IsEnum == true;
|
||||
var typeName = isEnum ? "Enum" : (Nullable.GetUnderlyingType(type) ?? type).Name;
|
||||
var typeFunc = $@"{typeName}{(!isEnum && Nullable.GetUnderlyingType(type) != null ? "?" : "")}";
|
||||
|
||||
return $@"({typeFunc})it[""{name}""]";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -3332,6 +3573,21 @@ namespace Radzen
|
||||
/// Sets the focus.
|
||||
/// </summary>
|
||||
ValueTask FocusAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Sets the Disabled state of the component
|
||||
/// </summary>
|
||||
bool Disabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Sets the Visible state of the component
|
||||
/// </summary>
|
||||
bool Visible { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Sets the FormFieldContext of the component
|
||||
/// </summary>
|
||||
IFormFieldContext FormFieldContext { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -3716,4 +3972,4 @@ namespace Radzen
|
||||
/// </summary>
|
||||
Right
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -7,6 +7,25 @@ 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>
|
||||
@@ -21,6 +40,16 @@ namespace Radzen
|
||||
/// 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>
|
||||
@@ -75,8 +104,19 @@ namespace Radzen
|
||||
private void OnThemeChanged()
|
||||
{
|
||||
var expiration = DateTime.Now.Add(options.Duration);
|
||||
var cookie = $"{options.Name}={themeService.Theme}; expires={expiration:R}; path=/";
|
||||
|
||||
_ = jsRuntime.InvokeVoidAsync("eval", $"document.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}\"");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Linq.Dynamic.Core;
|
||||
using System.Linq.Expressions;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
@@ -262,23 +259,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(DynamicLinqCustomTypeProvider.ParsingConfig, string.Join(".", query), ignoreCase ? searchText.ToLower() : searchText);
|
||||
_view = Query.Where(TextProperty, searchText, FilterOperator, FilterCaseSensitivity);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -405,5 +386,20 @@ namespace Radzen
|
||||
|
||||
/// <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
@@ -6,7 +6,6 @@ using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Linq.Dynamic.Core;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Radzen
|
||||
@@ -272,7 +271,7 @@ namespace Radzen
|
||||
/// <summary>
|
||||
/// The selected items
|
||||
/// </summary>
|
||||
protected IList<object> selectedItems = new List<object>();
|
||||
protected ISet<object> selectedItems = new HashSet<object>();
|
||||
/// <summary>
|
||||
/// The selected item
|
||||
/// </summary>
|
||||
@@ -288,10 +287,10 @@ namespace Radzen
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedItems.Count != View.Cast<object>().ToList().Where(i => disabledPropertyGetter != null ? disabledPropertyGetter(i) as bool? != true : true).Count())
|
||||
if (selectedItems.Count != View.Cast<object>().ToList().Where(i => disabledPropertyGetter == null || disabledPropertyGetter(i) as bool? != true).Count())
|
||||
{
|
||||
selectedItems.Clear();
|
||||
selectedItems = View.Cast<object>().ToList().Where(i => disabledPropertyGetter != null ? disabledPropertyGetter(i) as bool? != true : true).ToList();
|
||||
selectedItems = View.Cast<object>().ToList().Where(i => disabledPropertyGetter == null || disabledPropertyGetter(i) as bool? != true).ToHashSet(ItemComparer);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -341,15 +340,17 @@ namespace Radzen
|
||||
|
||||
internal bool IsAllSelected()
|
||||
{
|
||||
List<object> notDisabledItemsInList = View.Cast<object>().ToList()
|
||||
.Where(i => disabledPropertyGetter == null || disabledPropertyGetter(i) as bool? != true)
|
||||
.ToList();
|
||||
|
||||
if (LoadData.HasDelegate && !string.IsNullOrEmpty(ValueProperty))
|
||||
{
|
||||
return View != null && View.Cast<object>().ToList()
|
||||
.Where(i => disabledPropertyGetter != null ? disabledPropertyGetter(i) as bool? != true : true)
|
||||
return View != null && notDisabledItemsInList.Count > 0 && notDisabledItemsInList
|
||||
.All(i => IsItemSelectedByValue(GetItemOrValueFromProperty(i, ValueProperty)));
|
||||
}
|
||||
|
||||
return View != null && selectedItems.Count == View.Cast<object>().ToList()
|
||||
.Where(i => disabledPropertyGetter != null ? disabledPropertyGetter(i) as bool? != true : true).Count();
|
||||
return View != null && notDisabledItemsInList.Count > 0 && selectedItems.Count == notDisabledItemsInList.Count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -434,9 +435,13 @@ namespace Radzen
|
||||
|
||||
var type = query.ElementType;
|
||||
|
||||
if (type == typeof(object) && typeof(EnumerableQuery).IsAssignableFrom(query.GetType()) && query.Any())
|
||||
if (type == typeof(object) && typeof(EnumerableQuery).IsAssignableFrom(query.GetType()) && query.Cast<object>().Any())
|
||||
{
|
||||
type = query.FirstOrDefault().GetType();
|
||||
var firstElement = query.Cast<object>().FirstOrDefault(i => i != null);
|
||||
if (firstElement != null)
|
||||
{
|
||||
type = firstElement.GetType();
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(ValueProperty))
|
||||
@@ -453,6 +458,11 @@ namespace Radzen
|
||||
{
|
||||
disabledPropertyGetter = GetGetter(DisabledProperty, type);
|
||||
}
|
||||
|
||||
if (selectedItems.Count == 0)
|
||||
{
|
||||
selectedItems = new HashSet<object>(ItemComparer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -619,7 +629,7 @@ namespace Radzen
|
||||
/// <param name="shouldSelectOnChange">Should select item on item change with keyboard.</param>
|
||||
protected virtual async System.Threading.Tasks.Task HandleKeyPress(Microsoft.AspNetCore.Components.Web.KeyboardEventArgs args, bool isFilter = false, bool? shouldSelectOnChange = null)
|
||||
{
|
||||
if (Disabled)
|
||||
if (Disabled || Data == null)
|
||||
return;
|
||||
|
||||
List<object> items = Enumerable.Empty<object>().ToList();
|
||||
@@ -669,14 +679,16 @@ namespace Radzen
|
||||
//
|
||||
}
|
||||
}
|
||||
else if (key == "Enter" || key == "NumpadEnter")
|
||||
else if (key == "Enter" || key == "NumpadEnter" || key == "Space")
|
||||
{
|
||||
preventKeydown = true;
|
||||
|
||||
if (selectedIndex >= 0 && selectedIndex <= items.Count() - 1)
|
||||
{
|
||||
await JSRuntime.InvokeAsync<string>("Radzen.setInputValue", search, $"{searchText}".Trim());
|
||||
var itemToSelect = items.ElementAtOrDefault(selectedIndex);
|
||||
|
||||
await JSRuntime.InvokeAsync<string>("Radzen.setInputValue", search, $"{searchText}".Trim());
|
||||
|
||||
if (itemToSelect != null)
|
||||
{
|
||||
await OnSelectItem(itemToSelect, true);
|
||||
@@ -687,13 +699,16 @@ namespace Radzen
|
||||
|
||||
if (!popupOpened)
|
||||
{
|
||||
await OpenPopup(key, isFilter);
|
||||
if(key != "Space")
|
||||
{
|
||||
await OpenPopup(key, isFilter);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!Multiple)
|
||||
if (!Multiple && !isFilter)
|
||||
{
|
||||
await JSRuntime.InvokeVoidAsync("Radzen.closePopup", PopupID);
|
||||
await ClosePopup(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -705,7 +720,9 @@ namespace Radzen
|
||||
}
|
||||
else if (key == "Escape" || key == "Tab")
|
||||
{
|
||||
await JSRuntime.InvokeVoidAsync("Radzen.closePopup", PopupID);
|
||||
preventKeydown = false;
|
||||
|
||||
await ClosePopup(key);
|
||||
}
|
||||
else if (key == "Delete" && AllowClear)
|
||||
{
|
||||
@@ -728,11 +745,12 @@ namespace Radzen
|
||||
|
||||
Debounce(DebounceFilter, FilterDelay);
|
||||
}
|
||||
else
|
||||
else if(!args.CtrlKey && !args.AltKey)
|
||||
{
|
||||
var filteredItems = Query.Where(TextProperty, args.Key, StringFilterOperator.StartsWith, FilterCaseSensitivity.CaseInsensitive)
|
||||
.Cast<object>()
|
||||
.ToList();
|
||||
var filteredItems = (!string.IsNullOrEmpty(TextProperty) ?
|
||||
Query.Where(TextProperty, args.Key, StringFilterOperator.StartsWith, FilterCaseSensitivity.CaseInsensitive) :
|
||||
Query)
|
||||
.Cast(Query.ElementType).Cast<dynamic>().ToList();
|
||||
|
||||
|
||||
if (previousKey != args.Key)
|
||||
@@ -744,7 +762,7 @@ namespace Radzen
|
||||
itemIndex = itemIndex + 1 >= filteredItems.Count() ? 0 : itemIndex + 1;
|
||||
var itemToSelect = filteredItems.ElementAtOrDefault(itemIndex);
|
||||
|
||||
if (itemToSelect != null)
|
||||
if (itemToSelect is not null)
|
||||
{
|
||||
if (!Multiple)
|
||||
{
|
||||
@@ -766,6 +784,11 @@ namespace Radzen
|
||||
}
|
||||
}
|
||||
|
||||
internal virtual async Task ClosePopup(string key)
|
||||
{
|
||||
await JSRuntime.InvokeVoidAsync("Radzen.closePopup", PopupID);
|
||||
}
|
||||
|
||||
int itemIndex;
|
||||
string previousKey;
|
||||
|
||||
@@ -981,7 +1004,7 @@ namespace Radzen
|
||||
{
|
||||
if (Multiple)
|
||||
{
|
||||
return selectedItems.IndexOf(item) != -1;
|
||||
return selectedItems.Contains(item);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1125,9 +1148,13 @@ namespace Radzen
|
||||
var query = Data.AsQueryable();
|
||||
var elementType = query.ElementType;
|
||||
|
||||
if (elementType == typeof(object) && typeof(EnumerableQuery).IsAssignableFrom(query.GetType()) && query.Any())
|
||||
if (elementType == typeof(object) && typeof(EnumerableQuery).IsAssignableFrom(query.GetType()) && query.Cast<object>().Any())
|
||||
{
|
||||
elementType = query.FirstOrDefault().GetType();
|
||||
var firstElement = query.Cast<object>().FirstOrDefault(i => i != null);
|
||||
if (firstElement != null)
|
||||
{
|
||||
elementType = firstElement.GetType();
|
||||
}
|
||||
}
|
||||
|
||||
if (elementType != null)
|
||||
@@ -1207,18 +1234,14 @@ namespace Radzen
|
||||
}
|
||||
else
|
||||
{
|
||||
selectedItems = selectedItems.AsQueryable().Where(DynamicLinqCustomTypeProvider.ParsingConfig, $@"!object.Equals(it.{ValueProperty},@0)", value).ToList();
|
||||
selectedItems = selectedItems.AsQueryable().Where(i => !object.Equals(GetItemOrValueFromProperty(i, ValueProperty), value)).ToHashSet(ItemComparer);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!selectedItems.Any(i => object.Equals(i, item)))
|
||||
if (!selectedItems.Add(item))
|
||||
{
|
||||
selectedItems.Add(item);
|
||||
}
|
||||
else
|
||||
{
|
||||
selectedItems = selectedItems.Where(i => !object.Equals(i, item)).ToList();
|
||||
selectedItems.Remove(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1242,7 +1265,16 @@ namespace Radzen
|
||||
}
|
||||
else
|
||||
{
|
||||
SelectedItem = view.AsQueryable().Where(DynamicLinqCustomTypeProvider.ParsingConfig, $@"{ValueProperty} == @0", value).FirstOrDefault();
|
||||
SelectedItem = view.AsQueryable().Where(new FilterDescriptor[]
|
||||
{
|
||||
new FilterDescriptor()
|
||||
{
|
||||
Property = ValueProperty,
|
||||
FilterValue = value
|
||||
}
|
||||
},
|
||||
LogicalFilterOperator.And,
|
||||
FilterCaseSensitivity.Default).FirstOrDefault();
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -1259,7 +1291,7 @@ namespace Radzen
|
||||
{
|
||||
if (!string.IsNullOrEmpty(ValueProperty))
|
||||
{
|
||||
foreach (object v in values.ToDynamicList())
|
||||
foreach (object v in values.Cast<dynamic>().ToList())
|
||||
{
|
||||
dynamic item;
|
||||
|
||||
@@ -1269,10 +1301,19 @@ namespace Radzen
|
||||
}
|
||||
else
|
||||
{
|
||||
item = view.AsQueryable().Where(DynamicLinqCustomTypeProvider.ParsingConfig, $@"{ValueProperty} == @0", v).FirstOrDefault();
|
||||
item = view.AsQueryable().Where(new FilterDescriptor[]
|
||||
{
|
||||
new FilterDescriptor()
|
||||
{
|
||||
Property = ValueProperty,
|
||||
FilterValue = v
|
||||
}
|
||||
},
|
||||
LogicalFilterOperator.And,
|
||||
FilterCaseSensitivity.Default).FirstOrDefault();
|
||||
}
|
||||
|
||||
if (!object.Equals(item, null) && !selectedItems.AsQueryable().Where(DynamicLinqCustomTypeProvider.ParsingConfig, $@"object.Equals(it.{ValueProperty},@0)", v).Any())
|
||||
if (!object.Equals(item, null) && !selectedItems.AsQueryable().Where(i => object.Equals(GetItemOrValueFromProperty(i, ValueProperty), v)).Any())
|
||||
{
|
||||
selectedItems.Add(item);
|
||||
}
|
||||
@@ -1280,7 +1321,7 @@ namespace Radzen
|
||||
}
|
||||
else
|
||||
{
|
||||
selectedItems = ((IEnumerable)values).Cast<object>().ToList();
|
||||
selectedItems = values.Cast<object>().ToHashSet(ItemComparer);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1292,6 +1333,11 @@ namespace Radzen
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// For lists of objects, an IEqualityComparer to control how selected items are determined
|
||||
/// </summary>
|
||||
[Parameter] public IEqualityComparer<object> ItemComparer { get; set; }
|
||||
|
||||
internal bool IsItemSelectedByValue(object v)
|
||||
{
|
||||
switch (internalValue)
|
||||
|
||||
147
Radzen.Blazor/DynamicExtensions.cs
Normal file
147
Radzen.Blazor/DynamicExtensions.cs
Normal file
@@ -0,0 +1,147 @@
|
||||
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}");
|
||||
|
||||
object param = parameters[index];
|
||||
return param switch
|
||||
{
|
||||
string s when s == string.Empty => @"""""",
|
||||
null => "null",
|
||||
string s => @$"""{s.Replace("\"", "\\\"")}""",
|
||||
bool b => b.ToString().ToLower(),
|
||||
Guid g => $"Guid.Parse(\"{g}\")",
|
||||
DateTime dt => $"DateTime.Parse(\"{dt:yyyy-MM-ddTHH:mm:ss.fffZ}\")",
|
||||
DateTimeOffset dto => $"DateTime.Parse(\"{dto.UtcDateTime:yyyy-MM-ddTHH:mm:ss.fffZ}\")",
|
||||
DateOnly d => $"DateOnly.Parse(\"{d:yyyy-MM-dd}\")",
|
||||
TimeOnly t => $"TimeOnly.Parse(\"{t:HH:mm:ss}\")",
|
||||
_ => param.ToString()
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq.Dynamic.Core;
|
||||
using System.Linq.Dynamic.Core.CustomTypeProviders;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Radzen.Blazor
|
||||
{
|
||||
class DynamicLinqCustomTypeProvider : IDynamicLinkCustomTypeProvider
|
||||
{
|
||||
static readonly HashSet<Type> empty = [];
|
||||
public HashSet<Type> GetCustomTypes() => empty;
|
||||
public Dictionary<Type, List<MethodInfo>> GetExtensionMethods() => throw new NotSupportedException();
|
||||
public Type ResolveType(string typeName) => throw new NotSupportedException();
|
||||
public Type ResolveTypeBySimpleName(string simpleTypeName) => throw new NotSupportedException();
|
||||
public static ParsingConfig ParsingConfig = new() { CustomTypeProvider = new DynamicLinqCustomTypeProvider() };
|
||||
}
|
||||
}
|
||||
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}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
917
Radzen.Blazor/ExpressionParser.cs
Normal file
917
Radzen.Blazor/ExpressionParser.cs
Normal file
@@ -0,0 +1,917 @@
|
||||
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(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;
|
||||
}
|
||||
}
|
||||
462
Radzen.Blazor/ExpressionSerializer.cs
Normal file
462
Radzen.Blazor/ExpressionSerializer.cs
Normal file
@@ -0,0 +1,462 @@
|
||||
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;
|
||||
}
|
||||
|
||||
private string FormatValue(object value)
|
||||
{
|
||||
if (value == null)
|
||||
return "null";
|
||||
|
||||
return value switch
|
||||
{
|
||||
string str => $"\"{str}\"",
|
||||
char c => $"'{c}'",
|
||||
bool b => b.ToString().ToLowerInvariant(),
|
||||
DateTime dt => $"DateTime.Parse(\"{dt.ToString("yyyy-MM-ddTHH:mm:ss.fffZ", CultureInfo.InvariantCulture)}\", CultureInfo.InvariantCulture)",
|
||||
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 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('>');
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,8 @@ namespace Radzen.Blazor
|
||||
/// </summary>
|
||||
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(translationFunction) });
|
||||
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>
|
||||
|
||||
@@ -16,13 +16,6 @@ namespace Radzen
|
||||
/// </summary>
|
||||
public class FormComponentWithAutoComplete<T> : FormComponent<T>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating the browser built-in autocomplete is enabled.
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if input automatic complete is enabled; otherwise, <c>false</c>.</value>
|
||||
[Parameter]
|
||||
public virtual bool AutoComplete { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating the type of built-in autocomplete
|
||||
/// the browser should use.
|
||||
@@ -44,8 +37,8 @@ namespace Radzen
|
||||
/// AutoCompleteType.</value>
|
||||
public virtual string AutoCompleteAttribute
|
||||
{
|
||||
get => !AutoComplete ? DefaultAutoCompleteAttribute :
|
||||
autoComplete as string ?? AutoCompleteType.GetAutoCompleteValue();
|
||||
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>
|
||||
@@ -53,18 +46,11 @@ namespace Radzen
|
||||
/// </summary>
|
||||
public virtual string DefaultAutoCompleteAttribute { get; set; } = "off";
|
||||
|
||||
object autoComplete;
|
||||
object ariaAutoComplete;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task SetParametersAsync(ParameterView parameters)
|
||||
{
|
||||
parameters = parameters.TryGetValue(nameof(AutoComplete).ToLower(), out autoComplete) ?
|
||||
ParameterView.FromDictionary(parameters
|
||||
.ToDictionary().Where(i => i.Key != nameof(AutoComplete).ToLower()).ToDictionary(i => i.Key, i => i.Value)
|
||||
.ToDictionary(i => i.Key, i => i.Value))
|
||||
: 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)
|
||||
|
||||
@@ -187,7 +187,7 @@ namespace Radzen.Blazor
|
||||
|
||||
if (Visible)
|
||||
{
|
||||
JSRuntime.InvokeVoidAsync("Radzen.destroyGauge", Element);
|
||||
JSRuntime.InvokeVoid("Radzen.destroyGauge", Element);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -50,15 +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>
|
||||
/// <param name="chartHeight">Height of the whole char area.</param>
|
||||
/// <returns>RenderFragment.</returns>
|
||||
RenderFragment RenderTooltip(object data, double marginLeft, double marginTop, double chartHeight);
|
||||
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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,6 +77,12 @@ namespace Radzen.Blazor
|
||||
/// <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>
|
||||
|
||||
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-2024 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)
|
||||
@@ -88,11 +88,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 +97,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 +105,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 +113,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;
|
||||
}
|
||||
}
|
||||
56
Radzen.Blazor/Markdown/HtmlBlock.cs
Normal file
56
Radzen.Blazor/Markdown/HtmlBlock.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Radzen.Blazor.Markdown;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an HTML block.
|
||||
/// </summary>
|
||||
public class HtmlBlock : Leaf
|
||||
{
|
||||
internal int Type { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Accept(INodeVisitor visitor)
|
||||
{
|
||||
visitor.VisitHtmlBlock(this);
|
||||
}
|
||||
|
||||
internal override BlockMatch Matches(BlockParser parser)
|
||||
{
|
||||
return parser.Blank && (Type == 6 || Type == 7) ? BlockMatch.Skip : BlockMatch.Match;
|
||||
}
|
||||
|
||||
private static readonly Regex TrailinNewLineRegex = new(@"\n$");
|
||||
|
||||
internal override void Close(BlockParser parser)
|
||||
{
|
||||
base.Close(parser);
|
||||
|
||||
Value = TrailinNewLineRegex.Replace(Value, "");
|
||||
}
|
||||
|
||||
internal static BlockStart Start(BlockParser parser, Block node)
|
||||
{
|
||||
if (!parser.Indented && parser.PeekNonSpace() == '<')
|
||||
{
|
||||
var line = parser.CurrentLine[parser.NextNonSpace..];
|
||||
|
||||
for (var blockType = 1; blockType <= 7; blockType++) {
|
||||
|
||||
if (BlockParser.HtmlBlockOpenRegex[blockType].IsMatch(line) &&
|
||||
(blockType < 7 || (node is not Paragraph &&
|
||||
!(!parser.AllClosed && !parser.Blank && parser.Tip is Paragraph) // maybe lazy
|
||||
))) {
|
||||
parser.CloseUnmatchedBlocks();
|
||||
// We don't adjust parser.offset;
|
||||
// spaces are part of the HTML block:
|
||||
var block = parser.AddChild<HtmlBlock>(parser.Offset);
|
||||
block.Type = blockType;
|
||||
return BlockStart.Leaf;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return BlockStart.Skip;
|
||||
}
|
||||
}
|
||||
19
Radzen.Blazor/Markdown/HtmlInline.cs
Normal file
19
Radzen.Blazor/Markdown/HtmlInline.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
namespace Radzen.Blazor.Markdown;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an inline HTML element.
|
||||
/// </summary>
|
||||
public class HtmlInline : Inline
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the HTML element value.
|
||||
/// </summary>
|
||||
public string Value { get; set; }
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Accept(INodeVisitor visitor)
|
||||
{
|
||||
visitor.VisitHtmlInline(this);
|
||||
}
|
||||
}
|
||||
136
Radzen.Blazor/Markdown/HtmlSanitizer.cs
Normal file
136
Radzen.Blazor/Markdown/HtmlSanitizer.cs
Normal file
@@ -0,0 +1,136 @@
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
#nullable enable
|
||||
class HtmlSanitizer
|
||||
{
|
||||
private readonly ISet<string> allowedTags;
|
||||
private readonly ISet<string> allowedAttributes;
|
||||
|
||||
public HtmlSanitizer(IEnumerable<string>? allowedHtmlTags, IEnumerable<string>? allowedHtmlAttributes)
|
||||
{
|
||||
allowedTags = allowedHtmlTags != null ? new HashSet<string>(allowedHtmlTags) : AllowedTags;
|
||||
allowedAttributes = allowedHtmlAttributes != null ? new HashSet<string>(allowedHtmlAttributes) : AllowedAttributes;
|
||||
}
|
||||
|
||||
private static ISet<string> AllowedTags { get; } = new HashSet<string>()
|
||||
{
|
||||
// https://developer.mozilla.org/en/docs/Web/Guide/HTML/HTML5/HTML5_element_list
|
||||
"a", "abbr", "acronym", "address", "area", "b",
|
||||
"big", "blockquote", "br", "button", "caption", "center", "cite",
|
||||
"code", "col", "colgroup", "dd", "del", "dfn", "dir", "div", "dl", "dt",
|
||||
"em", "fieldset", "font", "form", "h1", "h2", "h3", "h4", "h5", "h6",
|
||||
"hr", "i", "img", "input", "ins", "kbd", "label", "legend", "li", "map",
|
||||
"menu", "ol", "optgroup", "option", "p", "pre", "q", "s", "samp",
|
||||
"select", "small", "span", "strike", "strong", "sub", "sup", "table",
|
||||
"tbody", "td", "textarea", "tfoot", "th", "thead", "tr", "tt", "u",
|
||||
"ul", "var", "section", "nav", "article", "aside", "header", "footer", "main",
|
||||
"figure", "figcaption", "data", "time", "mark", "ruby", "rt", "rp", "bdi", "wbr",
|
||||
"datalist", "keygen", "output", "progress", "meter", "details", "summary", "menuitem",
|
||||
"html", "head", "body"
|
||||
};
|
||||
|
||||
public static ISet<string> AllowedAttributes { get; } = new HashSet<string>()
|
||||
{
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes
|
||||
"abbr", "accept", "accept-charset", "accesskey",
|
||||
"action", "align", "alt", "axis", "bgcolor", "border", "cellpadding",
|
||||
"cellspacing", "char", "charoff", "charset", "checked", "cite", "class",
|
||||
"clear", "cols", "colspan", "color", "compact", "coords", "datetime",
|
||||
"dir", "disabled", "enctype", "for", "frame", "headers", "height",
|
||||
"href", "hreflang", "hspace", "id", "ismap", "label", "lang",
|
||||
"longdesc", "maxlength", "media", "method", "multiple", "name",
|
||||
"nohref", "noshade", "nowrap", "prompt", "readonly", "rel", "rev",
|
||||
"rows", "rowspan", "rules", "scope", "selected", "shape", "size",
|
||||
"span", "src", "start", "style", "summary", "tabindex", "target", "title",
|
||||
"type", "usemap", "valign", "value", "vspace", "width",
|
||||
"high", "keytype", "list", "low", "max", "min", "novalidate", "open", "optimum",
|
||||
"pattern", "placeholder", "pubdate", "radiogroup", "required", "reversed", "spellcheck", "step",
|
||||
"wrap", "challenge", "contenteditable", "draggable", "dropzone", "autocomplete", "autosave",
|
||||
};
|
||||
|
||||
public static ISet<string> UriAttributes { get; } = new HashSet<string>()
|
||||
{
|
||||
"action", "background", "dynsrc", "href", "lowsrc", "src"
|
||||
};
|
||||
|
||||
public string Sanitize(string input)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(input))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return Regex.Replace(input, @"</?([a-zA-Z0-9]+)(\s[^>]*)?>", SanitizeTag);
|
||||
}
|
||||
|
||||
private string SanitizeTag(Match match)
|
||||
{
|
||||
var tag = match.Groups[1].Value.ToLowerInvariant();
|
||||
|
||||
if (!allowedTags.Contains(tag))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var attributes = match.Groups[2].Value;
|
||||
|
||||
var safeAttributes = Regex.Replace(attributes, @"(\w+)\s*=\s*(""[^""]*""|'[^']*'|[^\s>]+)", SanitizeAttribute);
|
||||
|
||||
return $"<{(match.Value.StartsWith("</") ? "/" : "")}{tag}{safeAttributes}>";
|
||||
}
|
||||
|
||||
private string SanitizeAttribute(Match match)
|
||||
{
|
||||
var name = match.Groups[1].Value.ToLowerInvariant();
|
||||
var value = match.Groups[2].Value;
|
||||
|
||||
if (!allowedAttributes.Contains(name))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
if (name == "style")
|
||||
{
|
||||
var decoded = HtmlDecode(value).ToLowerInvariant();
|
||||
|
||||
if (Regex.IsMatch(decoded, @"expression|javascript:|vbscript:|url\s*\(\s*(['""])?\s*javascript", RegexOptions.IgnoreCase))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
if (UriAttributes.Contains(name) && IsDangerousUrl(value))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
if ((value.StartsWith('\'') && value.EndsWith('\'')) || (value.StartsWith('"') && value.EndsWith('"')))
|
||||
{
|
||||
value = value[1..^1];
|
||||
}
|
||||
|
||||
return $" {name}=\"{value}\"";
|
||||
}
|
||||
|
||||
private static string HtmlDecode(string input)
|
||||
{
|
||||
return System.Web.HttpUtility.HtmlDecode(input);
|
||||
}
|
||||
|
||||
public static bool IsDangerousUrl(string value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var decoded = HtmlDecode(value).Trim().ToLowerInvariant();
|
||||
|
||||
return decoded.StartsWith("javascript:") ||
|
||||
decoded.StartsWith("vbscript:") ||
|
||||
decoded.StartsWith("data:text/html") ||
|
||||
decoded.Contains("expression(");
|
||||
}
|
||||
}
|
||||
25
Radzen.Blazor/Markdown/IBlockInlineContainer.cs
Normal file
25
Radzen.Blazor/Markdown/IBlockInlineContainer.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Radzen.Blazor.Markdown;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a block node that has inline children.
|
||||
/// </summary>
|
||||
public interface IBlockInlineContainer
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the inline children of the block.
|
||||
/// </summary>
|
||||
IReadOnlyList<Inline> Children { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Adds an inline child to the block.
|
||||
/// </summary>
|
||||
/// <param name="child"></param>
|
||||
void Add(Inline child);
|
||||
|
||||
/// <summary>
|
||||
/// Gets string value of the block.
|
||||
/// </summary>
|
||||
string Value { get; }
|
||||
}
|
||||
15
Radzen.Blazor/Markdown/INode.cs
Normal file
15
Radzen.Blazor/Markdown/INode.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Radzen.Blazor.Markdown;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a markdown node that can be visited by a <see cref="INodeVisitor"/>.
|
||||
/// </summary>
|
||||
public interface INode
|
||||
{
|
||||
/// <summary>
|
||||
/// Accepts a <see cref="INodeVisitor"/>.
|
||||
/// </summary>
|
||||
/// <param name="visitor"></param>
|
||||
public void Accept(INodeVisitor visitor);
|
||||
}
|
||||
128
Radzen.Blazor/Markdown/INodeVisitor.cs
Normal file
128
Radzen.Blazor/Markdown/INodeVisitor.cs
Normal file
@@ -0,0 +1,128 @@
|
||||
namespace Radzen.Blazor.Markdown;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Represents a visitor for Markdown AST nodes.
|
||||
/// </summary>
|
||||
public interface INodeVisitor
|
||||
{
|
||||
/// <summary>
|
||||
/// Visits a heading node.
|
||||
/// </summary>
|
||||
void VisitHeading(Heading heading);
|
||||
|
||||
/// <summary>
|
||||
/// Visits a paragraph node.
|
||||
/// </summary>
|
||||
void VisitParagraph(Paragraph paragraph);
|
||||
|
||||
/// <summary>
|
||||
/// Visits a block quote node.
|
||||
/// </summary>
|
||||
void VisitBlockQuote(BlockQuote blockQuote);
|
||||
|
||||
/// <summary>
|
||||
/// Visits a document node.
|
||||
/// </summary>
|
||||
void VisitDocument(Document document);
|
||||
|
||||
/// <summary>
|
||||
/// Visits an unordered list node.
|
||||
/// </summary>
|
||||
void VisitUnorderedList(UnorderedList unorderedList);
|
||||
|
||||
/// <summary>
|
||||
/// Visits a list item node.
|
||||
/// </summary>
|
||||
void VisitListItem(ListItem listItem);
|
||||
|
||||
/// <summary>
|
||||
/// Visits a text node.
|
||||
/// </summary>
|
||||
void VisitText(Text text);
|
||||
|
||||
/// <summary>
|
||||
/// Visits an ordered list node.
|
||||
/// </summary>
|
||||
void VisitOrderedList(OrderedList orderedList);
|
||||
|
||||
/// <summary>
|
||||
/// Visits an emphasis node.
|
||||
/// </summary>
|
||||
void VisitEmphasis(Emphasis emphasis);
|
||||
|
||||
/// <summary>
|
||||
/// Visits a strong node.
|
||||
/// </summary>
|
||||
void VisitStrong(Strong strong);
|
||||
|
||||
/// <summary>
|
||||
/// Visits a code node.
|
||||
/// </summary>
|
||||
void VisitCode(Code code);
|
||||
|
||||
/// <summary>
|
||||
/// Visits a link node.
|
||||
/// </summary>
|
||||
void VisitLink(Link link);
|
||||
|
||||
/// <summary>
|
||||
/// Visits an image node.
|
||||
/// </summary>
|
||||
void VisitImage(Image image);
|
||||
|
||||
/// <summary>
|
||||
/// Visits an HTML inline node.
|
||||
/// </summary>
|
||||
void VisitHtmlInline(HtmlInline html);
|
||||
|
||||
/// <summary>
|
||||
/// Visits a line break node.
|
||||
/// </summary>
|
||||
void VisitLineBreak(LineBreak lineBreak);
|
||||
|
||||
/// <summary>
|
||||
/// Visits a soft line break node.
|
||||
/// </summary>
|
||||
void VisitSoftLineBreak(SoftLineBreak softLineBreak);
|
||||
|
||||
/// <summary>
|
||||
/// Visits a thematic break node.
|
||||
/// </summary>
|
||||
void VisitThematicBreak(ThematicBreak thematicBreak);
|
||||
|
||||
/// <summary>
|
||||
/// Visits an indented code block node.
|
||||
/// </summary>
|
||||
void VisitIndentedCodeBlock(IndentedCodeBlock codeBlock);
|
||||
|
||||
/// <summary>
|
||||
/// Visits a fenced code block node.
|
||||
/// </summary>
|
||||
void VisitFencedCodeBlock(FencedCodeBlock fencedCodeBlock);
|
||||
|
||||
/// <summary>
|
||||
/// Visits an HTML block node.
|
||||
/// </summary>
|
||||
void VisitHtmlBlock(HtmlBlock htmlBlock);
|
||||
|
||||
/// <summary>
|
||||
/// Visits a table node.
|
||||
/// </summary>
|
||||
void VisitTable(Table table);
|
||||
|
||||
/// <summary>
|
||||
/// Visits a table header row node.
|
||||
/// </summary>
|
||||
void VisitTableHeaderRow(TableHeaderRow header);
|
||||
|
||||
/// <summary>
|
||||
/// Visits a table row node.
|
||||
/// </summary>
|
||||
void VisitTableRow(TableRow row);
|
||||
|
||||
/// <summary>
|
||||
/// Visits a table cell node.
|
||||
/// </summary>
|
||||
void VisitTableCell(TableCell cell);
|
||||
}
|
||||
23
Radzen.Blazor/Markdown/Image.cs
Normal file
23
Radzen.Blazor/Markdown/Image.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
namespace Radzen.Blazor.Markdown;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an inline image element: <c></c>
|
||||
/// </summary>
|
||||
public class Image : InlineContainer
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the destination (URL) of the image.
|
||||
/// </summary>
|
||||
public string Destination { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the alternative text of the image.
|
||||
/// </summary>
|
||||
public string Title { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Accept(INodeVisitor visitor)
|
||||
{
|
||||
visitor.VisitImage(this);
|
||||
}
|
||||
}
|
||||
69
Radzen.Blazor/Markdown/IndentedCodeBlock.cs
Normal file
69
Radzen.Blazor/Markdown/IndentedCodeBlock.cs
Normal file
@@ -0,0 +1,69 @@
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Radzen.Blazor.Markdown;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a markdown indented code block.
|
||||
/// </summary>
|
||||
public class IndentedCodeBlock : Leaf
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override void Accept(INodeVisitor visitor)
|
||||
{
|
||||
visitor.VisitIndentedCodeBlock(this);
|
||||
}
|
||||
|
||||
internal override BlockMatch Matches(BlockParser parser)
|
||||
{
|
||||
if (parser.Indent >= BlockParser.CodeIndent)
|
||||
{
|
||||
parser.AdvanceOffset(BlockParser.CodeIndent, true);
|
||||
}
|
||||
else if (parser.Blank)
|
||||
{
|
||||
parser.AdvanceNextNonSpace();
|
||||
}
|
||||
else
|
||||
{
|
||||
return BlockMatch.Skip;
|
||||
}
|
||||
|
||||
return BlockMatch.Match;
|
||||
}
|
||||
|
||||
private static readonly Regex TrailingWhiteSpaceRegex = new(@"^[ \t]*$");
|
||||
|
||||
internal override void Close(BlockParser parser)
|
||||
{
|
||||
base.Close(parser);
|
||||
|
||||
var lines = Value.Split('\n').ToList();;
|
||||
// Note that indented code block cannot be empty, so
|
||||
// lines.length cannot be zero.
|
||||
|
||||
while (TrailingWhiteSpaceRegex.IsMatch(lines[^1]))
|
||||
{
|
||||
lines.RemoveAt(lines.Count - 1);
|
||||
}
|
||||
|
||||
Value = string.Join('\n', lines) + '\n';
|
||||
|
||||
Range.End.Line = Range.Start.Line + lines.Count - 1;
|
||||
Range.End.Column = Range.Start.Column + lines[^1].Length - 1;
|
||||
}
|
||||
|
||||
internal static BlockStart Start(BlockParser parser, Block container)
|
||||
{
|
||||
if (parser.Indented && parser.Tip is not Paragraph && !parser.Blank)
|
||||
{
|
||||
// indented code
|
||||
parser.AdvanceOffset(BlockParser.CodeIndent, true);
|
||||
parser.CloseUnmatchedBlocks();
|
||||
parser.AddChild<IndentedCodeBlock>(parser.Offset);
|
||||
return BlockStart.Leaf;
|
||||
}
|
||||
|
||||
return BlockStart.Skip;
|
||||
}
|
||||
}
|
||||
12
Radzen.Blazor/Markdown/Inline.cs
Normal file
12
Radzen.Blazor/Markdown/Inline.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace Radzen.Blazor.Markdown;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for markdown inline nodes.
|
||||
/// </summary>
|
||||
public abstract class Inline : INode
|
||||
{
|
||||
/// <summary>
|
||||
/// Accepts a visitor.
|
||||
/// </summary>
|
||||
public abstract void Accept(INodeVisitor visitor);
|
||||
}
|
||||
25
Radzen.Blazor/Markdown/InlineContainer.cs
Normal file
25
Radzen.Blazor/Markdown/InlineContainer.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Radzen.Blazor.Markdown;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for inline elements that contain other inline elements.
|
||||
/// </summary>
|
||||
public abstract class InlineContainer : Inline
|
||||
{
|
||||
private readonly List<Inline> children = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets the children of the container.
|
||||
/// </summary>
|
||||
public IReadOnlyList<Inline> Children => children;
|
||||
|
||||
/// <summary>
|
||||
/// Appends a child to the container.
|
||||
/// </summary>
|
||||
/// <param name="node">The child to add.</param>
|
||||
public void Add(Inline node)
|
||||
{
|
||||
children.Add(node);
|
||||
}
|
||||
}
|
||||
959
Radzen.Blazor/Markdown/InlineParser.cs
Normal file
959
Radzen.Blazor/Markdown/InlineParser.cs
Normal file
@@ -0,0 +1,959 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Text;
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
|
||||
namespace Radzen.Blazor.Markdown;
|
||||
|
||||
class InlineParser
|
||||
{
|
||||
class Delimiter
|
||||
{
|
||||
public char Char { get; set; }
|
||||
public int Length { get; set; }
|
||||
public int Position { get; set; }
|
||||
public Text Node { get; set; }
|
||||
public bool CanOpen { get; set; }
|
||||
public bool CanClose { get; set; }
|
||||
public bool Active { get; set; } = true;
|
||||
}
|
||||
|
||||
private const char Asterisk = '*';
|
||||
private const char Underscore = '_';
|
||||
internal const char Backslash = '\\';
|
||||
private const char Null = '\0';
|
||||
private const char Backtick = '`';
|
||||
internal const char Space = ' ';
|
||||
internal const char LineFeed = '\n';
|
||||
private const char CarrigeReturn = '\r';
|
||||
internal const char OpenBracket = '[';
|
||||
internal const char CloseBracket = ']';
|
||||
private const char OpenParenthesis = '(';
|
||||
private const char CloseParenthesis = ')';
|
||||
internal const char Quote = '"';
|
||||
internal const char OpenAngleBracket = '<';
|
||||
internal const char CloseAngleBracket = '>';
|
||||
private const char SingleQuote = '\'';
|
||||
private const char Exclamation = '!';
|
||||
internal const char Colon = ':';
|
||||
|
||||
private readonly List<Inline> inlines = [];
|
||||
private readonly List<Delimiter> delimiters = [];
|
||||
private readonly StringBuilder buffer = new();
|
||||
|
||||
enum LinkState
|
||||
{
|
||||
Text,
|
||||
Destination,
|
||||
Title
|
||||
}
|
||||
|
||||
private void AddTextNode(bool trim = false)
|
||||
{
|
||||
if (buffer.Length > 0)
|
||||
{
|
||||
var output = new StringBuilder();
|
||||
|
||||
for (var index = 0; index < buffer.Length; index++)
|
||||
{
|
||||
var ch = buffer[index];
|
||||
|
||||
if (ch is Backslash && index < buffer.Length - 1 && !buffer[index + 1].IsPunctuation())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
output.Append(ch);
|
||||
}
|
||||
}
|
||||
var value = output.ToString();
|
||||
|
||||
if (trim)
|
||||
{
|
||||
value = value.TrimEnd();
|
||||
}
|
||||
|
||||
inlines.Add(new Text(value));
|
||||
|
||||
buffer.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryParseCode(string text, int index, out int newIndex)
|
||||
{
|
||||
if (text[index] is not Backtick)
|
||||
{
|
||||
newIndex = index;
|
||||
return false;
|
||||
}
|
||||
|
||||
AddTextNode();
|
||||
|
||||
// Count opening backticks
|
||||
var openingCount = 0;
|
||||
var position = index;
|
||||
while (position < text.Length && text[position] is Backtick)
|
||||
{
|
||||
openingCount++;
|
||||
position++;
|
||||
}
|
||||
|
||||
// Find matching closing backticks
|
||||
var searchStart = position;
|
||||
var bestMatch = -1;
|
||||
|
||||
while (position < text.Length)
|
||||
{
|
||||
// Count consecutive backticks
|
||||
var count = 0;
|
||||
var closingStart = position;
|
||||
while (position < text.Length && text[position] is Backtick)
|
||||
{
|
||||
count++;
|
||||
position++;
|
||||
}
|
||||
|
||||
if (count == openingCount)
|
||||
{
|
||||
bestMatch = closingStart;
|
||||
break;
|
||||
}
|
||||
|
||||
if (position < text.Length)
|
||||
{
|
||||
position++;
|
||||
}
|
||||
}
|
||||
|
||||
if (bestMatch >= 0)
|
||||
{
|
||||
var content = text[searchStart..bestMatch];
|
||||
|
||||
content = BlockParser.NewLineRegex.Replace(content, $"{Space}");
|
||||
|
||||
if (content.StartsWith(Space) && content.EndsWith(Space) && !string.IsNullOrWhiteSpace(content))
|
||||
{
|
||||
content = content[1..^1];
|
||||
}
|
||||
|
||||
inlines.Add(new Code(content));
|
||||
|
||||
newIndex = bestMatch + openingCount;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
inlines.Add(new Text($"{new string(Backtick, openingCount)}"));
|
||||
|
||||
newIndex = index + openingCount;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool TryParseBackslash(string text, int index, char next, out int newIndex)
|
||||
{
|
||||
if (text[index] is not Backslash)
|
||||
{
|
||||
newIndex = index;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (next.IsPunctuation())
|
||||
{
|
||||
AddTextNode();
|
||||
inlines.Add(new Text(text[index + 1].ToString()));
|
||||
newIndex = index + 2;
|
||||
return true;
|
||||
}
|
||||
|
||||
buffer.Append(text[index]);
|
||||
newIndex = index + 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool TryParseDelimiter(string text, int index, char next, char prev, out int newIndex)
|
||||
{
|
||||
var ch = text[index];
|
||||
|
||||
if (ch is not (Asterisk or Underscore or OpenBracket) && (ch is not Exclamation || next is not OpenBracket))
|
||||
{
|
||||
newIndex = index;
|
||||
return false;
|
||||
}
|
||||
|
||||
AddTextNode();
|
||||
|
||||
var position = index;
|
||||
|
||||
while (position < text.Length && text[position] == ch)
|
||||
{
|
||||
buffer.Append(ch);
|
||||
|
||||
position++;
|
||||
}
|
||||
|
||||
if (ch is Exclamation)
|
||||
{
|
||||
buffer.Append(OpenBracket);
|
||||
position++;
|
||||
}
|
||||
|
||||
next = position < text.Length ? text[position] : Null;
|
||||
|
||||
if (buffer.Length > 0)
|
||||
{
|
||||
var node = new Text(buffer.ToString());
|
||||
var leftFlanking = LeftFlanking(prev, next);
|
||||
var rightFlanking = RightFlanking(prev, next);
|
||||
|
||||
var canOpen = false;
|
||||
var canClose = false;
|
||||
|
||||
if (ch is Asterisk)
|
||||
{
|
||||
canOpen = leftFlanking;
|
||||
canClose = rightFlanking;
|
||||
}
|
||||
|
||||
if (ch is Underscore)
|
||||
{
|
||||
canClose = rightFlanking && (!leftFlanking || next.IsPunctuation());
|
||||
canOpen = leftFlanking && (!rightFlanking || prev.IsPunctuation());
|
||||
}
|
||||
|
||||
var delimiter = new Delimiter
|
||||
{
|
||||
Node = node,
|
||||
Char = ch,
|
||||
Length = buffer.Length,
|
||||
Position = index,
|
||||
CanClose = canClose,
|
||||
CanOpen = canOpen
|
||||
};
|
||||
|
||||
delimiters.Add(delimiter);
|
||||
inlines.Add(node);
|
||||
buffer.Clear();
|
||||
}
|
||||
|
||||
newIndex = position;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool RightFlanking(char prev, char next)
|
||||
{
|
||||
/*
|
||||
that is (1) not preceded by Unicode whitespace, and either (2a) not preceded by a Unicode punctuation character,
|
||||
or (2b) preceded by a Unicode punctuation character and followed by Unicode whitespace or a Unicode punctuation character.
|
||||
*/
|
||||
|
||||
return !prev.IsNullOrWhiteSpace() && (!prev.IsPunctuation() || next.IsNullOrWhiteSpace() || next.IsPunctuation());
|
||||
}
|
||||
|
||||
private static bool LeftFlanking(char prev, char next)
|
||||
{
|
||||
/*
|
||||
that is (1) not followed by Unicode whitespace, and either (2a) not followed by a Unicode punctuation character,
|
||||
or (2b) followed by a Unicode punctuation character and preceded by Unicode whitespace or a Unicode punctuation character.
|
||||
*/
|
||||
|
||||
return !next.IsNullOrWhiteSpace() && (!next.IsPunctuation() || prev.IsNullOrWhiteSpace() || prev.IsPunctuation());
|
||||
}
|
||||
|
||||
public static List<Inline> Parse(string text, Dictionary<string, LinkReference> linkReferences)
|
||||
{
|
||||
var parser = new InlineParser();
|
||||
|
||||
return parser.ParseInlines(text.Trim(), linkReferences);
|
||||
}
|
||||
|
||||
private static readonly Regex EmailRegex = new(@"^([a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*)");
|
||||
|
||||
private bool TryParseAutoLink(string text, int index, out int newIndex)
|
||||
{
|
||||
newIndex = index;
|
||||
|
||||
if (text[index] is not OpenAngleBracket)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var destination = new StringBuilder();
|
||||
|
||||
var position = index + 1;
|
||||
|
||||
while (position < text.Length && text[position] is not CloseAngleBracket)
|
||||
{
|
||||
destination.Append(text[position]);
|
||||
position++;
|
||||
}
|
||||
|
||||
if (position >= text.Length || text[position] is not CloseAngleBracket)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var url = destination.ToString();
|
||||
|
||||
if (url.Contains(Space))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var content = url;
|
||||
|
||||
if (EmailRegex.IsMatch(url))
|
||||
{
|
||||
url = $"mailto:{url}";
|
||||
}
|
||||
else if (!Uri.TryCreate(url, UriKind.Absolute, out _))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var link = new Link { Destination = url };
|
||||
|
||||
link.Add(new Text(content));
|
||||
|
||||
inlines.Add(link);
|
||||
|
||||
newIndex = position + 1;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private List<Inline> ParseInlines(string text, Dictionary<string, LinkReference> references)
|
||||
{
|
||||
var index = 0;
|
||||
|
||||
while (index < text.Length)
|
||||
{
|
||||
if (TryParseHtml(text, index, out index))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (TryParseAutoLink(text, index, out index))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (TryParseCode(text, index, out index))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (TryParseLineBreak(text, index, out index))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (TryParseSoftLineBreak(text, index, out index))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
char next = index < text.Length - 1 ? text[index + 1] : Null;
|
||||
|
||||
if (TryParseBackslash(text, index, next, out index))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
char prev = index > 0 ? text[index - 1] : Null;
|
||||
|
||||
if (TryParseDelimiter(text, index, next, prev, out index))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (TryParseLinkFromReference(text, index, references, out index))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (TryParseLinkOrImage(text, index, out index))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
buffer.Append(text[index]);
|
||||
|
||||
index++;
|
||||
}
|
||||
|
||||
AddTextNode();
|
||||
|
||||
ParseEmphasisAndStrong();
|
||||
|
||||
NormalizeText();
|
||||
|
||||
return inlines;
|
||||
}
|
||||
|
||||
private void NormalizeText()
|
||||
{
|
||||
if (inlines.Count > 0)
|
||||
{
|
||||
if (inlines[0] is Text first)
|
||||
{
|
||||
first.Value = first.Value.TrimStart();
|
||||
}
|
||||
|
||||
if (inlines[^1] is Text last)
|
||||
{
|
||||
last.Value = last.Value.TrimEnd();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryParseSoftLineBreak(string text, int index, out int newIndex)
|
||||
{
|
||||
newIndex = index;
|
||||
|
||||
if (TryParseNewLine(text, index, out var position))
|
||||
{
|
||||
AddTextNode(trim: true);
|
||||
|
||||
inlines.Add(new SoftLineBreak());
|
||||
|
||||
newIndex = position;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool TryParseLineBreak(string text, int index, out int newIndex)
|
||||
{
|
||||
newIndex = index;
|
||||
|
||||
if (text[index] is not Space && text[index] is not Backslash)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var position = index + 1;
|
||||
|
||||
if (position < text.Length && text[position] is Space)
|
||||
{
|
||||
while (position < text.Length && text[position] is Space)
|
||||
{
|
||||
position++;
|
||||
}
|
||||
}
|
||||
|
||||
if (text[index] is Space && position == index + 1)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (position < text.Length && TryParseNewLine(text, position, out position))
|
||||
{
|
||||
AddTextNode(trim: true);
|
||||
|
||||
inlines.Add(new LineBreak());
|
||||
|
||||
newIndex = position;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryParseNewLine(string text, int position, out int newIndex)
|
||||
{
|
||||
newIndex = position;
|
||||
|
||||
if (position >= text.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (text[position] is LineFeed)
|
||||
{
|
||||
newIndex = position + 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (text[position] is CarrigeReturn && position < text.Length - 1 && text[position + 1] is LineFeed)
|
||||
{
|
||||
newIndex = position + 2;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool TryParseHtml(string text, int index, out int newIndex)
|
||||
{
|
||||
newIndex = index;
|
||||
|
||||
var match = BlockParser.HtmlRegex.Match(text[index..]);
|
||||
|
||||
if (match.Success)
|
||||
{
|
||||
AddTextNode();
|
||||
|
||||
var value = text[index..(index + match.Length)];
|
||||
|
||||
inlines.Add(new HtmlInline { Value = value });
|
||||
|
||||
newIndex = index + match.Length;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
internal static bool TryParseDestinationAndTitle(string text, int position, out string destination, out string title, out int newPosition)
|
||||
{
|
||||
newPosition = position;
|
||||
destination = string.Empty;
|
||||
title = string.Empty;
|
||||
|
||||
// Skip whitespace
|
||||
while (position < text.Length && text[position] is Space or LineFeed)
|
||||
{
|
||||
position++;
|
||||
}
|
||||
|
||||
if (position >= text.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Parse destination
|
||||
var destinationBuilder = new StringBuilder();
|
||||
|
||||
var angleBrackets = position < text.Length && text[position] is OpenAngleBracket;
|
||||
|
||||
if (angleBrackets)
|
||||
{
|
||||
position++;
|
||||
}
|
||||
|
||||
var parentheses = 0;
|
||||
|
||||
while (position < text.Length)
|
||||
{
|
||||
var ch = text[position];
|
||||
var prev = position > 0 ? text[position - 1] : Null;
|
||||
var next = position < text.Length - 1 ? text[position + 1] : Null;
|
||||
|
||||
if (angleBrackets && ch is CloseAngleBracket && prev is not Backslash)
|
||||
{
|
||||
position++;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!angleBrackets)
|
||||
{
|
||||
if (ch is OpenParenthesis && prev is not Backslash)
|
||||
{
|
||||
parentheses++;
|
||||
}
|
||||
else if (ch is CloseParenthesis && prev is not Backslash)
|
||||
{
|
||||
if (parentheses == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
parentheses--;
|
||||
}
|
||||
|
||||
if (ch is Space or LineFeed)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (ch is Backslash && next.IsPunctuation())
|
||||
{
|
||||
position++;
|
||||
continue;
|
||||
}
|
||||
|
||||
destinationBuilder.Append(ch);
|
||||
position++;
|
||||
}
|
||||
|
||||
if (angleBrackets)
|
||||
{
|
||||
// Skip whitespace after angle brackets
|
||||
while (position < text.Length && text[position] is Space)
|
||||
{
|
||||
position++;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse title if present
|
||||
var titleBuilder = new StringBuilder();
|
||||
if (position < text.Length && text[position].IsNullOrWhiteSpace())
|
||||
{
|
||||
var lines = 0;
|
||||
|
||||
while (position < text.Length && text[position].IsNullOrWhiteSpace())
|
||||
{
|
||||
if (text[position] is LineFeed)
|
||||
{
|
||||
lines++;
|
||||
}
|
||||
|
||||
position++;
|
||||
}
|
||||
|
||||
var titleStart = position;
|
||||
|
||||
if (position < text.Length)
|
||||
{
|
||||
var titleDelimiter = text[position];
|
||||
|
||||
if (titleDelimiter is Quote or SingleQuote or OpenParenthesis)
|
||||
{
|
||||
position++; // Skip opening delimiter
|
||||
|
||||
char closingDelimiter = titleDelimiter is OpenParenthesis ? CloseParenthesis : titleDelimiter;
|
||||
|
||||
while (position < text.Length && (text[position] != closingDelimiter || text[position - 1] is Backslash))
|
||||
{
|
||||
if (text[position] is LineFeed && titleBuilder.Length > 0 && titleBuilder[^1] is LineFeed)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
titleBuilder.Append(text[position]);
|
||||
position++;
|
||||
}
|
||||
|
||||
if (position < text.Length && text[position] == closingDelimiter)
|
||||
{
|
||||
position++; // Skip closing delimiter
|
||||
|
||||
// Skip whitespace after title
|
||||
while (position < text.Length && text[position] is Space)
|
||||
{
|
||||
position++;
|
||||
}
|
||||
|
||||
if (position < text.Length)
|
||||
{
|
||||
if (position < text.Length && text[position] is not (CloseParenthesis or LineFeed))
|
||||
{
|
||||
if (lines > 0)
|
||||
{
|
||||
// non-white space characters after title
|
||||
newPosition = titleStart;
|
||||
destination = destinationBuilder.ToString();
|
||||
title = string.Empty;
|
||||
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (position < text.Length)
|
||||
{
|
||||
if (text[position] is LineFeed)
|
||||
{
|
||||
position++;
|
||||
}
|
||||
else if (text[position] is Quote or SingleQuote or OpenParenthesis)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
destination = destinationBuilder.ToString();
|
||||
title = titleBuilder.ToString();
|
||||
newPosition = position;
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool TryParseLinkOrImage(string text, int index, out int newIndex)
|
||||
{
|
||||
newIndex = index;
|
||||
|
||||
if (!TryGetOpenerIndex(text, index, out var openerIndex, out var position))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryParseDestinationAndTitle(text, position, out var destination, out var title, out position))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (position >= text.Length || text[position] is not CloseParenthesis)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var opener = delimiters[openerIndex];
|
||||
|
||||
InlineContainer container = opener.Char == Exclamation ? new Image { Destination = destination, Title = title } : new Link { Destination = destination, Title = title };
|
||||
|
||||
ReplaceOpener(openerIndex, container);
|
||||
|
||||
newIndex = position + 1;
|
||||
|
||||
if (container is Link)
|
||||
{
|
||||
for (var delimiterIndex = 0; delimiterIndex < openerIndex; delimiterIndex++)
|
||||
{
|
||||
if (delimiters[delimiterIndex].Char == OpenBracket)
|
||||
{
|
||||
delimiters[delimiterIndex].Active = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
delimiters.Remove(opener);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
private bool TryGetOpenerIndex(string text, int index, out int openerIndex, out int position)
|
||||
{
|
||||
position = index;
|
||||
openerIndex = -1;
|
||||
|
||||
if (text[index] is not CloseBracket)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
var di = delimiters.Count - 1;
|
||||
|
||||
while (di >= 0)
|
||||
{
|
||||
var delimiter = delimiters[di];
|
||||
|
||||
if ((delimiter.Active && delimiter.Char is OpenBracket) || delimiter.Char is Exclamation)
|
||||
{
|
||||
openerIndex = di;
|
||||
break;
|
||||
}
|
||||
|
||||
di--;
|
||||
}
|
||||
|
||||
if (di < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
AddTextNode();
|
||||
|
||||
position = index + 1;
|
||||
|
||||
// Skip if not followed by opening parenthesis
|
||||
if (position >= text.Length || text[position] is not OpenParenthesis)
|
||||
{
|
||||
delimiters.RemoveAt(openerIndex);
|
||||
return false;
|
||||
}
|
||||
|
||||
position++; // Skip opening parenthesis
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void ReplaceOpener(int openerIndex, InlineContainer parent)
|
||||
{
|
||||
var startIndex = inlines.FindIndex(delimiters[openerIndex].Node.Equals);
|
||||
|
||||
ParseEmphasisAndStrong(openerIndex);
|
||||
|
||||
var endIndex = inlines.Count - startIndex;
|
||||
|
||||
var children = inlines.GetRange(startIndex + 1, endIndex - 1);
|
||||
|
||||
inlines.RemoveRange(startIndex, endIndex);
|
||||
|
||||
foreach (var child in children)
|
||||
{
|
||||
parent.Add(child);
|
||||
}
|
||||
|
||||
inlines.Insert(startIndex, parent);
|
||||
}
|
||||
|
||||
|
||||
private bool TryParseLinkFromReference(string text, int index, Dictionary<string, LinkReference> references, out int newIndex)
|
||||
{
|
||||
newIndex = index;
|
||||
|
||||
if (references.Count == 0 || text[index] is not CloseBracket)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var openerIndex = FindOpenBracketIndex(OpenBracket);
|
||||
|
||||
if (openerIndex < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
AddTextNode();
|
||||
|
||||
var startIndex = inlines.FindIndex(delimiters[openerIndex].Node.Equals);
|
||||
|
||||
var endIndex = inlines.Count - startIndex;
|
||||
|
||||
var children = inlines.GetRange(startIndex + 1, endIndex - 1);
|
||||
|
||||
var id = new StringBuilder();
|
||||
|
||||
foreach (var child in children)
|
||||
{
|
||||
if (child is Text textNode)
|
||||
{
|
||||
id.Append(textNode.Value);
|
||||
}
|
||||
}
|
||||
|
||||
if (!references.TryGetValue(id.ToString().ToLowerInvariant(), out var reference))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var link = new Link { Destination = reference.Destination, Title = reference.Title };
|
||||
|
||||
foreach (var child in children)
|
||||
{
|
||||
link.Add(child);
|
||||
}
|
||||
|
||||
inlines.RemoveRange(startIndex, endIndex);
|
||||
inlines.Insert(startIndex, link);
|
||||
|
||||
link.Destination = reference.Destination;
|
||||
link.Title = reference.Title;
|
||||
newIndex = index + 1;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private int FindOpenBracketIndex(char ch)
|
||||
{
|
||||
for (var index = delimiters.Count - 1; index >= 0; index--)
|
||||
{
|
||||
if (delimiters[index].Char == ch)
|
||||
{
|
||||
return index;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
private void ParseEmphasisAndStrong(int index = -1)
|
||||
{
|
||||
var closerIndex = 0;
|
||||
|
||||
while ((closerIndex = FindCloserIndex()) > 0)
|
||||
{
|
||||
var openerIndex = FindOpenerIndex(closerIndex, index);
|
||||
|
||||
if (openerIndex >= 0)
|
||||
{
|
||||
var closer = delimiters[closerIndex];
|
||||
var opener = delimiters[openerIndex];
|
||||
var startIndex = inlines.FindIndex(opener.Node.Equals);
|
||||
var endIndex = inlines.FindIndex(closer.Node.Equals);
|
||||
|
||||
if (startIndex >= 0 && endIndex >= 0)
|
||||
{
|
||||
var innerInlines = inlines.GetRange(startIndex + 1, endIndex - startIndex - 1);
|
||||
|
||||
var charsToConsume = closer.Length == opener.Length && closer.Length > 1 ? 2 : 1;
|
||||
|
||||
InlineContainer parent = charsToConsume == 1 ? new Emphasis() : new Strong();
|
||||
|
||||
foreach (var child in innerInlines)
|
||||
{
|
||||
parent.Add(child);
|
||||
}
|
||||
|
||||
opener.Length -= charsToConsume;
|
||||
|
||||
if (opener.Length > 0)
|
||||
{
|
||||
opener.Node.Value = opener.Node.Value[..^charsToConsume];
|
||||
startIndex += charsToConsume;
|
||||
}
|
||||
|
||||
closer.Length -= charsToConsume;
|
||||
|
||||
if (closer.Length > 0)
|
||||
{
|
||||
closer.Node.Value = closer.Node.Value[..^charsToConsume];
|
||||
endIndex -= charsToConsume;
|
||||
}
|
||||
|
||||
inlines.RemoveRange(startIndex, endIndex - startIndex + 1);
|
||||
|
||||
inlines.Insert(startIndex, parent);
|
||||
}
|
||||
|
||||
delimiters.RemoveAt(closerIndex);
|
||||
delimiters.RemoveAt(openerIndex);
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int FindCloserIndex()
|
||||
{
|
||||
for (var index = 1; index < delimiters.Count; index++)
|
||||
{
|
||||
var delimiter = delimiters[index];
|
||||
|
||||
if (delimiter.CanClose && (delimiter.Char is Asterisk or Underscore))
|
||||
{
|
||||
return index;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
private int FindOpenerIndex(int startIndex, int endIndex)
|
||||
{
|
||||
var closer = delimiters[startIndex];
|
||||
|
||||
for (var index = startIndex - 1; index > endIndex; index--)
|
||||
{
|
||||
var delimiter = delimiters[index];
|
||||
|
||||
if (delimiter.CanOpen && delimiter.Char == closer.Char)
|
||||
{
|
||||
return index;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
22
Radzen.Blazor/Markdown/InlineVisitor.cs
Normal file
22
Radzen.Blazor/Markdown/InlineVisitor.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Radzen.Blazor.Markdown;
|
||||
|
||||
class InlineVisitor(Dictionary<string, LinkReference> references) : NodeVisitorBase
|
||||
{
|
||||
public override void VisitHeading(Heading heading) => ParseChildren(heading, references);
|
||||
|
||||
private static void ParseChildren(IBlockInlineContainer node, Dictionary<string, LinkReference> references)
|
||||
{
|
||||
var inlines = InlineParser.Parse(node.Value, references);
|
||||
|
||||
foreach (var inline in inlines)
|
||||
{
|
||||
node.Add(inline);
|
||||
}
|
||||
}
|
||||
|
||||
public override void VisitParagraph(Paragraph paragraph) => ParseChildren(paragraph, references);
|
||||
|
||||
public override void VisitTableCell(TableCell cell) => ParseChildren(cell, references);
|
||||
}
|
||||
42
Radzen.Blazor/Markdown/Leaf.cs
Normal file
42
Radzen.Blazor/Markdown/Leaf.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Radzen.Blazor.Markdown;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for markdown leaf block nodes.
|
||||
/// </summary>
|
||||
public abstract class Leaf : Block, IBlockInlineContainer
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the value of the leaf node.
|
||||
/// </summary>
|
||||
public string Value { get; set; } = string.Empty;
|
||||
|
||||
private readonly List<Inline> children = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets the children of the leaf node.
|
||||
/// </summary>
|
||||
public IReadOnlyList<Inline> Children => children;
|
||||
|
||||
/// <summary>
|
||||
/// Appends a child to the leaf node.
|
||||
/// </summary>
|
||||
public void Add(Inline node)
|
||||
{
|
||||
children.Add(node);
|
||||
}
|
||||
internal void AddLine(BlockParser blockParser)
|
||||
{
|
||||
if (blockParser.PartiallyConsumedTab)
|
||||
{
|
||||
blockParser.Offset += 1;
|
||||
|
||||
var charsToTab = 4 - (blockParser.Column % 4);
|
||||
|
||||
Value += new string(' ', charsToTab);
|
||||
}
|
||||
|
||||
Value += blockParser.CurrentLine[blockParser.Offset..] + "\n";
|
||||
}
|
||||
}
|
||||
13
Radzen.Blazor/Markdown/LineBreak.cs
Normal file
13
Radzen.Blazor/Markdown/LineBreak.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace Radzen.Blazor.Markdown;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a line break node. Line breaks are usually empty lines and are used to separate paragraphs.
|
||||
/// </summary>
|
||||
public class LineBreak : Inline
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override void Accept(INodeVisitor visitor)
|
||||
{
|
||||
visitor.VisitLineBreak(this);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user