mirror of
https://github.com/naturalcrit/homebrewery.git
synced 2026-01-23 16:33:05 +00:00
Compare commits
904 Commits
calculusch
...
v3.14.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a016bfd133 | ||
|
|
63fa747fd7 | ||
|
|
a7ddeafd06 | ||
|
|
81e6cae99d | ||
|
|
be0db1770d | ||
|
|
f905a62b6c | ||
|
|
35ca31cf43 | ||
|
|
0ce3ac9be2 | ||
|
|
7cdf1c93cf | ||
|
|
a7c4b78ec8 | ||
|
|
aa321fe2c3 | ||
|
|
7bc0af9a8c | ||
|
|
69475833e6 | ||
|
|
7dce3b3de5 | ||
|
|
183687d676 | ||
|
|
786acc7a1c | ||
|
|
3d3ad3f284 | ||
|
|
a6a1d70abc | ||
|
|
73461d6372 | ||
|
|
e56c6a5085 | ||
|
|
f8f0280f8c | ||
|
|
4e393779ac | ||
|
|
5671728c97 | ||
|
|
e06611a90f | ||
|
|
7b767368df | ||
|
|
d59c6be359 | ||
|
|
0d564dd0bf | ||
|
|
ef6e1e1782 | ||
|
|
b2f5e39256 | ||
|
|
762de62aa6 | ||
|
|
d4d27aab6a | ||
|
|
1803c89a23 | ||
|
|
4b0b56dd35 | ||
|
|
bf6eae7b3c | ||
|
|
3377d6645d | ||
|
|
5069eadd0a | ||
|
|
7a5b0b32c4 | ||
|
|
35a0a12f16 | ||
|
|
504bb78a8d | ||
|
|
94ae33d328 | ||
|
|
690c683943 | ||
|
|
3acf90dfdb | ||
|
|
6bb5d04f07 | ||
|
|
b0b1f7fd0b | ||
|
|
c25bf95a66 | ||
|
|
370c480670 | ||
|
|
a351013359 | ||
|
|
1f86b4c3d0 | ||
|
|
627a6a732b | ||
|
|
77421c2950 | ||
|
|
f3ed174b0e | ||
|
|
977d0ea73a | ||
|
|
04effa2150 | ||
|
|
7f694e6ca7 | ||
|
|
769a03916b | ||
|
|
e3c90a8295 | ||
|
|
561ff6283a | ||
|
|
ca5a7a1dbb | ||
|
|
505ac0c1d5 | ||
|
|
987d1c881b | ||
|
|
f34107ee1d | ||
|
|
2246944dd2 | ||
|
|
717ced28d6 | ||
|
|
fb836df8d5 | ||
|
|
fba08262d3 | ||
|
|
56582b6b24 | ||
|
|
43e4e43c7c | ||
|
|
3f3f113305 | ||
|
|
96e23ee2ea | ||
|
|
c57c9e236b | ||
|
|
32c6224f40 | ||
|
|
8dadc57934 | ||
|
|
0dc9e9ecdb | ||
|
|
977b871967 | ||
|
|
4fa8351f7f | ||
|
|
fa669f32fc | ||
|
|
a9fe516675 | ||
|
|
6502847b95 | ||
|
|
90dd4326e7 | ||
|
|
eeedc5f7d4 | ||
|
|
fff357d08b | ||
|
|
e0b69dce14 | ||
|
|
e6c5e6451c | ||
|
|
26866c337f | ||
|
|
4c71987866 | ||
|
|
8965bb60aa | ||
|
|
64fb032622 | ||
|
|
073076e011 | ||
|
|
71b505d55b | ||
|
|
40ab2c2283 | ||
|
|
d70c5a6fe3 | ||
|
|
645c9a122c | ||
|
|
b774c89bdb | ||
|
|
c75ebb36cb | ||
|
|
1c03138968 | ||
|
|
1313772adc | ||
|
|
fb06ae0d03 | ||
|
|
e952e05b79 | ||
|
|
c020297d78 | ||
|
|
8ea7d3dc8f | ||
|
|
051eed0e83 | ||
|
|
cdc2ffeff4 | ||
|
|
dedf9e0be9 | ||
|
|
0cc001fe94 | ||
|
|
0f969ce383 | ||
|
|
83244485ab | ||
|
|
defbc716c0 | ||
|
|
1803dfdeb1 | ||
|
|
787d50f17c | ||
|
|
925d934892 | ||
|
|
e45bcf76ce | ||
|
|
54d5dbf992 | ||
|
|
bb18ed7eda | ||
|
|
f8895721fc | ||
|
|
ba340f033a | ||
|
|
4c30a8f6a3 | ||
|
|
cbcb712587 | ||
|
|
187b6a9e8a | ||
|
|
2816a39ff9 | ||
|
|
9203cc2a6a | ||
|
|
9e19ba2d4e | ||
|
|
a706c9ff9b | ||
|
|
4957a0d2ef | ||
|
|
727f2bd80e | ||
|
|
11790fa438 | ||
|
|
c4049ec5fa | ||
|
|
02fca27f85 | ||
|
|
7f717b92fd | ||
|
|
e2272b078b | ||
|
|
01161752c6 | ||
|
|
3237a4f2eb | ||
|
|
d87e07e9ba | ||
|
|
82529e0b06 | ||
|
|
641303d1ad | ||
|
|
845c2eca76 | ||
|
|
e957f40775 | ||
|
|
3985afade9 | ||
|
|
f5ee55d0ca | ||
|
|
2cf73698e8 | ||
|
|
28a06348ab | ||
|
|
9510b4d097 | ||
|
|
37c8ea4fd7 | ||
|
|
9adc3e2e1a | ||
|
|
fd00a9f81d | ||
|
|
720e43e9d9 | ||
|
|
176977dd2a | ||
|
|
a42b867bcb | ||
|
|
1196a1577f | ||
|
|
54b11b1a4c | ||
|
|
7fa9b3cdd2 | ||
|
|
2f42c3f857 | ||
|
|
67e868b5ee | ||
|
|
3a81521e6f | ||
|
|
a30608a8ae | ||
|
|
6a129aebdb | ||
|
|
184f182b3a | ||
|
|
05dd5e4c04 | ||
|
|
499c640a11 | ||
|
|
5a11b7918e | ||
|
|
36ab6923ed | ||
|
|
7cc967ad49 | ||
|
|
d108088295 | ||
|
|
8be3154865 | ||
|
|
9aa7c67c5b | ||
|
|
5a84c0aaa7 | ||
|
|
4a6418a475 | ||
|
|
efb4c67e2a | ||
|
|
b325779466 | ||
|
|
0d475ab035 | ||
|
|
c791c0f60b | ||
|
|
f204b0ebc0 | ||
|
|
adab8449e0 | ||
|
|
fc96f6bf95 | ||
|
|
9924c6049e | ||
|
|
21e9251043 | ||
|
|
19e6d94419 | ||
|
|
232f28b5b4 | ||
|
|
6af5abd37d | ||
|
|
e0e49c606f | ||
|
|
e9e49e39fb | ||
|
|
ee4eb19f1e | ||
|
|
cc9edcc67c | ||
|
|
032fcf12e0 | ||
|
|
174024b472 | ||
|
|
8a8cd3fb18 | ||
|
|
dff0a7ae77 | ||
|
|
014482b3d5 | ||
|
|
ce7a98f974 | ||
|
|
5ee4ada112 | ||
|
|
f0765b5aaa | ||
|
|
9dde4aa94e | ||
|
|
1452920fbd | ||
|
|
81a098b6cf | ||
|
|
aa2fe3ef97 | ||
|
|
31fcf28e3f | ||
|
|
aa945c4177 | ||
|
|
52faa366ca | ||
|
|
fc6930c868 | ||
|
|
1ed7e43db1 | ||
|
|
6ec51bf725 | ||
|
|
ae336f1429 | ||
|
|
5847b246ef | ||
|
|
38168131e7 | ||
|
|
00e113ff67 | ||
|
|
136e877ee6 | ||
|
|
2ba42def09 | ||
|
|
337d3567fc | ||
|
|
3dc8eec1e6 | ||
|
|
fac2293b77 | ||
|
|
7d699e455e | ||
|
|
cbc6dcdc35 | ||
|
|
5894dc5a7a | ||
|
|
284bfe565b | ||
|
|
e03b540788 | ||
|
|
075c8805e0 | ||
|
|
5c4187cd06 | ||
|
|
1719cc68fa | ||
|
|
3ba67fb3d0 | ||
|
|
f58d52c4b6 | ||
|
|
e1ad6f8114 | ||
|
|
22257a95e0 | ||
|
|
713c978f08 | ||
|
|
3eb0c7acfe | ||
|
|
c3e6c01ec1 | ||
|
|
190bce519f | ||
|
|
5bb5cdec05 | ||
|
|
f8e68c1485 | ||
|
|
60ccd08bce | ||
|
|
f241024167 | ||
|
|
f1fd75574d | ||
|
|
31352e417f | ||
|
|
643af98ca3 | ||
|
|
3b61cd355f | ||
|
|
8df19e3b8f | ||
|
|
91d6548822 | ||
|
|
ef797b2a69 | ||
|
|
33a62a0ac7 | ||
|
|
5a9025f555 | ||
|
|
0a494633bb | ||
|
|
aeaea5b5e0 | ||
|
|
e03ec34104 | ||
|
|
c65ee59998 | ||
|
|
4d59a14f74 | ||
|
|
3c8aaa7465 | ||
|
|
ac70403203 | ||
|
|
a5e7ad882d | ||
|
|
061925e89a | ||
|
|
5fc8b508d1 | ||
|
|
cb444bef9d | ||
|
|
481f2e7d39 | ||
|
|
7559652a32 | ||
|
|
8e15466063 | ||
|
|
8ef319d2cd | ||
|
|
1513a983f7 | ||
|
|
1b71bbaefb | ||
|
|
fcd5279381 | ||
|
|
4276d38152 | ||
|
|
6f4604c8a9 | ||
|
|
13fa06ec1f | ||
|
|
d6bf2dec7e | ||
|
|
423a4a521a | ||
|
|
05d4d5b1ff | ||
|
|
ad1e8d50d7 | ||
|
|
c926f0de79 | ||
|
|
4337c67f69 | ||
|
|
bb06a3e4d4 | ||
|
|
6af9c9e432 | ||
|
|
84d0d15c5a | ||
|
|
b185fe8e35 | ||
|
|
adbb9e54c1 | ||
|
|
1d3c2d7cd6 | ||
|
|
2dc397e9f1 | ||
|
|
6465564b6f | ||
|
|
e1fe640e92 | ||
|
|
6f99fe7455 | ||
|
|
4b3b44ecc8 | ||
|
|
129e18da0f | ||
|
|
342963dae6 | ||
|
|
957b1ed9e7 | ||
|
|
31b3bd1ace | ||
|
|
423ed28fbd | ||
|
|
a4f17259e1 | ||
|
|
c2678e5f2c | ||
|
|
d797333b97 | ||
|
|
40777a3794 | ||
|
|
ac1005c2b0 | ||
|
|
c77395149b | ||
|
|
fb0580fff1 | ||
|
|
0e953d08b2 | ||
|
|
7fce362a52 | ||
|
|
3e7844ba6d | ||
|
|
7619f8f420 | ||
|
|
bac52f8376 | ||
|
|
5c72cd9d47 | ||
|
|
89b59a52bc | ||
|
|
a6f2a1a4c8 | ||
|
|
e9e9fbe21c | ||
|
|
e7108947d6 | ||
|
|
8b68f24135 | ||
|
|
e32ae9a792 | ||
|
|
758a951bf5 | ||
|
|
21ac50cd27 | ||
|
|
fc67a40167 | ||
|
|
7949df1865 | ||
|
|
8ed013cbb2 | ||
|
|
0138c27863 | ||
|
|
7930c209ff | ||
|
|
26fdc1ba91 | ||
|
|
8681994747 | ||
|
|
0e684b14a7 | ||
|
|
974f84d49f | ||
|
|
5a6abef4fb | ||
|
|
15200e0c6e | ||
|
|
79c22f383f | ||
|
|
915f9aafa8 | ||
|
|
11f8809c5e | ||
|
|
27a4831ea0 | ||
|
|
1abced20d6 | ||
|
|
251d03b7be | ||
|
|
29bbf3fef9 | ||
|
|
a7f8b52966 | ||
|
|
6629bc64d8 | ||
|
|
bfd46fb6fd | ||
|
|
2d335ef7fc | ||
|
|
fac0d151b6 | ||
|
|
4f950b6024 | ||
|
|
1c1d331df9 | ||
|
|
8609925da8 | ||
|
|
607244d6e1 | ||
|
|
9cc81d2ff9 | ||
|
|
32fa50d608 | ||
|
|
8221579b6a | ||
|
|
88eaebfd49 | ||
|
|
ee9f2c8c83 | ||
|
|
2870caaae6 | ||
|
|
e0425ec6c0 | ||
|
|
8aa88a2e45 | ||
|
|
edec9369ec | ||
|
|
f2d933410e | ||
|
|
b64a0c5200 | ||
|
|
113f9b3fe3 | ||
|
|
2c4f4ff5fc | ||
|
|
d2afa7adea | ||
|
|
8e7baca47d | ||
|
|
ddc5693778 | ||
|
|
82f73fb21d | ||
|
|
27c52fc244 | ||
|
|
ac82e3ecb2 | ||
|
|
22b6aa14f0 | ||
|
|
24ab3d3392 | ||
|
|
0b01f27d11 | ||
|
|
d640ad6bb7 | ||
|
|
fd91bf0fff | ||
|
|
270aa9e0f9 | ||
|
|
6ae249a527 | ||
|
|
c0123b96eb | ||
|
|
45f7080afd | ||
|
|
2af2ad629d | ||
|
|
2fc7aa454f | ||
|
|
fde797c044 | ||
|
|
6693fb1c13 | ||
|
|
17f8de48a8 | ||
|
|
0a5ff213de | ||
|
|
aebfcc7885 | ||
|
|
2c4f3473e5 | ||
|
|
9a4cc5f63e | ||
|
|
c82b62f953 | ||
|
|
f364f054f8 | ||
|
|
460358ce1f | ||
|
|
0448f15322 | ||
|
|
d741878f78 | ||
|
|
f830104531 | ||
|
|
d22cd88446 | ||
|
|
1444581c86 | ||
|
|
dfbd85a8ce | ||
|
|
af5434c9b7 | ||
|
|
484b0a6dff | ||
|
|
4951b9bf1a | ||
|
|
62c619de24 | ||
|
|
44c96aad04 | ||
|
|
f392216ff4 | ||
|
|
591cae0e8f | ||
|
|
e222811d03 | ||
|
|
c9b885f868 | ||
|
|
fed65f5430 | ||
|
|
47f912750b | ||
|
|
f29a5e346e | ||
|
|
ee381c91fe | ||
|
|
5f8d46f1b6 | ||
|
|
ade819c70c | ||
|
|
c209a86f90 | ||
|
|
4fe38e3929 | ||
|
|
b6d69173cd | ||
|
|
8b085e1806 | ||
|
|
cb9d24d5b4 | ||
|
|
23fd70e3c3 | ||
|
|
2fa3c0f311 | ||
|
|
5c0a072115 | ||
|
|
29c2274a19 | ||
|
|
a6f787ea8f | ||
|
|
24c86dd199 | ||
|
|
7eb96ee6be | ||
|
|
27aebf0e3b | ||
|
|
88578a3d16 | ||
|
|
28446d3ae2 | ||
|
|
a247e50c9f | ||
|
|
656edb07ea | ||
|
|
0cf79ceeb1 | ||
|
|
7b9bd70554 | ||
|
|
4b05bcac69 | ||
|
|
1555535f2e | ||
|
|
a413dc8d4f | ||
|
|
74ee09397e | ||
|
|
a06aa2a103 | ||
|
|
ea6595d4d6 | ||
|
|
5b02132e57 | ||
|
|
f8841c068f | ||
|
|
da1d08f8a9 | ||
|
|
0a199e750f | ||
|
|
5433cda52f | ||
|
|
9c4de58161 | ||
|
|
1b96dae27f | ||
|
|
16ca52756d | ||
|
|
645da7ae5f | ||
|
|
8570335d79 | ||
|
|
1564bc7448 | ||
|
|
8e20d3ba10 | ||
|
|
450baee66a | ||
|
|
4b588786c4 | ||
|
|
e07a04ebfa | ||
|
|
f707752c26 | ||
|
|
80e039b194 | ||
|
|
c888df28aa | ||
|
|
94cc1c642c | ||
|
|
9a02a351fa | ||
|
|
e8e7237a8e | ||
|
|
086c4f74f6 | ||
|
|
e987da498b | ||
|
|
d12f644f5b | ||
|
|
e4bde91f6a | ||
|
|
3eb071fbdc | ||
|
|
81e17b420c | ||
|
|
e8ccd094e8 | ||
|
|
7c60fbe655 | ||
|
|
2d570924d1 | ||
|
|
8ee70b0928 | ||
|
|
2cdd65b083 | ||
|
|
758a06e58a | ||
|
|
a87e420437 | ||
|
|
46085c8d44 | ||
|
|
179e21755c | ||
|
|
e9ca68e7d3 | ||
|
|
9886200fa9 | ||
|
|
9a1070bb06 | ||
|
|
853f048812 | ||
|
|
4564066c63 | ||
|
|
eb99acc95b | ||
|
|
ba76c51da7 | ||
|
|
435fb6ecfe | ||
|
|
8a10fac81e | ||
|
|
7f1949a7f4 | ||
|
|
060f28a0a6 | ||
|
|
a3f146cd53 | ||
|
|
47b56398b1 | ||
|
|
2ea2f41bd0 | ||
|
|
3b8dbe8a04 | ||
|
|
7a349ae26d | ||
|
|
0945a5e47e | ||
|
|
cba57d1033 | ||
|
|
1bece09339 | ||
|
|
65d6eb11dd | ||
|
|
a591763d10 | ||
|
|
62bf982a73 | ||
|
|
6294b12ad5 | ||
|
|
68f95f6130 | ||
|
|
35ae5e09ee | ||
|
|
552a23585b | ||
|
|
e635877b66 | ||
|
|
954bcbdaf6 | ||
|
|
559de2527b | ||
|
|
4c14774f1a | ||
|
|
ea380ae6a9 | ||
|
|
e2a946674f | ||
|
|
75926e34a2 | ||
|
|
37bc37bd94 | ||
|
|
528359dd9f | ||
|
|
f745fdefb3 | ||
|
|
2d9b80a81e | ||
|
|
299acfb92c | ||
|
|
8fc97493c5 | ||
|
|
7c7c6341f9 | ||
|
|
227ab192c4 | ||
|
|
ad48c0cd76 | ||
|
|
958b168906 | ||
|
|
51d4b5042c | ||
|
|
371ac9680c | ||
|
|
f68af555de | ||
|
|
66fdf808a6 | ||
|
|
65770782c2 | ||
|
|
fdf6acd80a | ||
|
|
08b61a6bb4 | ||
|
|
33c2bee873 | ||
|
|
8bbf2e1ce4 | ||
|
|
476002ae4d | ||
|
|
54ec1b8827 | ||
|
|
7bb92bc790 | ||
|
|
a87d62c9c2 | ||
|
|
8c315980e9 | ||
|
|
359a64968c | ||
|
|
866548deec | ||
|
|
ed39852a8f | ||
|
|
38fc647495 | ||
|
|
fa7b3ea2a0 | ||
|
|
9e041d26bd | ||
|
|
978c0c4c7b | ||
|
|
4f4cef0f6c | ||
|
|
556ded9b08 | ||
|
|
b7f88d53d0 | ||
|
|
cb8d98266c | ||
|
|
c918a79957 | ||
|
|
0efcd5d258 | ||
|
|
31b6e0c4f6 | ||
|
|
423413e41b | ||
|
|
ec514cdb51 | ||
|
|
7272544724 | ||
|
|
99ff7fdf14 | ||
|
|
491b38c330 | ||
|
|
4033d3ad99 | ||
|
|
9285d355b2 | ||
|
|
24e67e2270 | ||
|
|
5f6d5f53cc | ||
|
|
865c5678bc | ||
|
|
05ba7b41d1 | ||
|
|
e7735e242a | ||
|
|
1111d8275c | ||
|
|
f3b01bc75c | ||
|
|
8685c5cae4 | ||
|
|
e0a457bf40 | ||
|
|
a48d9d295e | ||
|
|
a5f453f1e5 | ||
|
|
c2170dd558 | ||
|
|
0c9958f461 | ||
|
|
e2a3959feb | ||
|
|
202ea1d905 | ||
|
|
e3c1e4b6f0 | ||
|
|
d20c9c502c | ||
|
|
8c5e68e571 | ||
|
|
fbe65a4e93 | ||
|
|
3875dabfd2 | ||
|
|
6464f35ce9 | ||
|
|
5442f232d5 | ||
|
|
930709223a | ||
|
|
a6ce36689c | ||
|
|
2424d34682 | ||
|
|
3fad8227a7 | ||
|
|
2d051fcdc0 | ||
|
|
95d7ce2876 | ||
|
|
1a52acd4a9 | ||
|
|
de8194d7e0 | ||
|
|
c60ddb701c | ||
|
|
0e00460012 | ||
|
|
ed92628012 | ||
|
|
12de8c5221 | ||
|
|
afa0571382 | ||
|
|
75c592b437 | ||
|
|
000c3db8cd | ||
|
|
f23c0bccbc | ||
|
|
69db1e2cb7 | ||
|
|
933dc372d2 | ||
|
|
176766dfe7 | ||
|
|
6fb185a964 | ||
|
|
1442414299 | ||
|
|
f6161abf52 | ||
|
|
8fc224e9a1 | ||
|
|
98fc007efd | ||
|
|
7fb23c7362 | ||
|
|
a2f0546a6d | ||
|
|
8a55658bd7 | ||
|
|
01d60c4520 | ||
|
|
b4349a0476 | ||
|
|
695b9916dd | ||
|
|
4543881808 | ||
|
|
ac3168e365 | ||
|
|
e396211f92 | ||
|
|
4ce68b86ed | ||
|
|
24769d69d4 | ||
|
|
fc22e6cd53 | ||
|
|
62ed026757 | ||
|
|
7cef4316d7 | ||
|
|
0df53daa4c | ||
|
|
b496ef3597 | ||
|
|
3ae5d4c1e3 | ||
|
|
a227a792c0 | ||
|
|
d9d4d74b71 | ||
|
|
af82d71e4f | ||
|
|
f897cbfdf4 | ||
|
|
1773e77cb9 | ||
|
|
dcd34ccdaf | ||
|
|
2453b623db | ||
|
|
783e88b5e6 | ||
|
|
77c0af42d0 | ||
|
|
c21b4eb2d6 | ||
|
|
6043af81e4 | ||
|
|
7c3321b62f | ||
|
|
d1955951e4 | ||
|
|
e62e185214 | ||
|
|
0c38415372 | ||
|
|
73f879aa63 | ||
|
|
f12de7d953 | ||
|
|
933ac41774 | ||
|
|
54b4d490f1 | ||
|
|
193af61725 | ||
|
|
bc9ab284d8 | ||
|
|
eb809ca375 | ||
|
|
54d2709d6a | ||
|
|
916bd5f4d6 | ||
|
|
511c9ffada | ||
|
|
ea03538552 | ||
|
|
8fb9f3f823 | ||
|
|
7af919afd3 | ||
|
|
5bbe4016d6 | ||
|
|
0f3312a5d7 | ||
|
|
0bf432dd76 | ||
|
|
27b3da0144 | ||
|
|
40c8b04a98 | ||
|
|
c0cc27a750 | ||
|
|
b8b1b9660f | ||
|
|
9b0db15083 | ||
|
|
c6f62142e1 | ||
|
|
f40d851d49 | ||
|
|
b64c835706 | ||
|
|
b7717171b3 | ||
|
|
92e27cda6c | ||
|
|
aef6605225 | ||
|
|
7c9cc25923 | ||
|
|
78ce8aa6e3 | ||
|
|
8ae22bdc27 | ||
|
|
46c14ef23b | ||
|
|
d3080c03a4 | ||
|
|
12eead3379 | ||
|
|
a2e065c5d8 | ||
|
|
0d1a3d55cf | ||
|
|
b748a597d2 | ||
|
|
5fb0e123e3 | ||
|
|
9a4a14fa97 | ||
|
|
0c76a546e4 | ||
|
|
fb299f898e | ||
|
|
69f01b282a | ||
|
|
66e39d9c65 | ||
|
|
8c5f4e0605 | ||
|
|
ed210da4af | ||
|
|
afb5ccec81 | ||
|
|
c0beae6e46 | ||
|
|
9c6ece3e7f | ||
|
|
5494c02f00 | ||
|
|
b4ee62a1bd | ||
|
|
b6c2f96b82 | ||
|
|
632efe8b9f | ||
|
|
bf38f95d25 | ||
|
|
f6daeb4acd | ||
|
|
10a7f34abb | ||
|
|
3a054f1ae0 | ||
|
|
25b4b3d906 | ||
|
|
f21d636846 | ||
|
|
8365841b89 | ||
|
|
e6bf8b59a1 | ||
|
|
b4a4934c5c | ||
|
|
a6b2dab9cc | ||
|
|
d692e88b96 | ||
|
|
62c9e081e6 | ||
|
|
6f7a657b59 | ||
|
|
65495b4e7c | ||
|
|
5a52e76ff0 | ||
|
|
3b1f4a0d13 | ||
|
|
cdad9af453 | ||
|
|
07ca134d98 | ||
|
|
dde8e28d07 | ||
|
|
5fd92ee72d | ||
|
|
5bcd3a1b01 | ||
|
|
d49d9fd755 | ||
|
|
41d817823b | ||
|
|
cfffb4961b | ||
|
|
6bd2f4d98d | ||
|
|
872ee339da | ||
|
|
295fea7581 | ||
|
|
f936b8b12b | ||
|
|
9f04c34b06 | ||
|
|
c9d416fec0 | ||
|
|
3dde6a098c | ||
|
|
ef25139ffe | ||
|
|
9d6076f642 | ||
|
|
dd82a1bebd | ||
|
|
d3f1fde9d9 | ||
|
|
d83e7bce0a | ||
|
|
8cd4a58304 | ||
|
|
0ef683222d | ||
|
|
ebbf162318 | ||
|
|
268d202562 | ||
|
|
4e0ec75e58 | ||
|
|
1af989c4a3 | ||
|
|
53480fdd5f | ||
|
|
ffa90c397c | ||
|
|
df3d1078f1 | ||
|
|
22bdb85dff | ||
|
|
2f5081156d | ||
|
|
c3f153a5fa | ||
|
|
5e0dda1bde | ||
|
|
b0379b0821 | ||
|
|
c506c9cee5 | ||
|
|
9184593bc0 | ||
|
|
1da70d54e8 | ||
|
|
55cdd016da | ||
|
|
10997898b9 | ||
|
|
76b66977e9 | ||
|
|
d695648d9b | ||
|
|
72160f5daf | ||
|
|
0751da42b6 | ||
|
|
babe1f30a2 | ||
|
|
cd35e91ea2 | ||
|
|
8d8a965241 | ||
|
|
2309887c92 | ||
|
|
d36e6e5834 | ||
|
|
dd205a14bc | ||
|
|
d854fe1202 | ||
|
|
58f487ac58 | ||
|
|
1761c506be | ||
|
|
9f19514b03 | ||
|
|
ade1056a02 | ||
|
|
e5fecaf9b6 | ||
|
|
5b8b0aa641 | ||
|
|
b7bc5cbad7 | ||
|
|
f2fc47a719 | ||
|
|
a5f9ab7897 | ||
|
|
39368c1a38 | ||
|
|
d879cf3cbb | ||
|
|
ae59c56ddd | ||
|
|
35431384bd | ||
|
|
dd31c0138b | ||
|
|
11f1e3fee8 | ||
|
|
aaaa8ed693 | ||
|
|
fec6bf290b | ||
|
|
2418e42655 | ||
|
|
dce86580ce | ||
|
|
f6997871be | ||
|
|
660a586a7c | ||
|
|
193cde0925 | ||
|
|
1a33e6e631 | ||
|
|
bb8ade435d | ||
|
|
94905f8151 | ||
|
|
05a30f00e3 | ||
|
|
3c1f8804af | ||
|
|
716e3d7143 | ||
|
|
a0e6487a7b | ||
|
|
076599f05c | ||
|
|
9fb0f37718 | ||
|
|
b86502aec7 | ||
|
|
88ccb955ce | ||
|
|
822f8a2cd0 | ||
|
|
d2965e1122 | ||
|
|
37e211937c | ||
|
|
e7a03dc7d6 | ||
|
|
a469b88fd2 | ||
|
|
45244c8e5d | ||
|
|
fad5817353 | ||
|
|
c0a8b79acc | ||
|
|
34a41fd610 | ||
|
|
eb0440d36d | ||
|
|
cd82db16d5 | ||
|
|
bdad601ebc | ||
|
|
97a74902ef | ||
|
|
ab30b6a799 | ||
|
|
da6bc497fc | ||
|
|
06c3168868 | ||
|
|
86be90adb2 | ||
|
|
a296678d20 | ||
|
|
f768990fb1 | ||
|
|
313543d0d1 | ||
|
|
2ebe8d80e9 | ||
|
|
8c20422fef | ||
|
|
2197a9b782 | ||
|
|
928a8b351e | ||
|
|
980ed8e265 | ||
|
|
2a148cb138 | ||
|
|
9426c6acd9 | ||
|
|
90ce48b170 | ||
|
|
448af683a0 | ||
|
|
ec5f8254f1 | ||
|
|
7ca38b88ad | ||
|
|
9f31a2c8a2 | ||
|
|
09cf5a9b04 | ||
|
|
4c6953a4e0 | ||
|
|
b4b4fbe375 | ||
|
|
4bc07ceb4e | ||
|
|
fde1706a0c | ||
|
|
1891d7c90a | ||
|
|
98d032913b | ||
|
|
80bcf92fa3 | ||
|
|
41d536b7ff | ||
|
|
83103a893a | ||
|
|
8cbc7a68e5 | ||
|
|
1292d9ad9b | ||
|
|
1177fd721c | ||
|
|
2c8908850b | ||
|
|
4622a74786 | ||
|
|
72cfca1158 | ||
|
|
67d824cac9 | ||
|
|
4f010d77e8 | ||
|
|
3b1f9b10e7 | ||
|
|
5e33d8b6c4 | ||
|
|
f7a2509405 | ||
|
|
52904eea09 | ||
|
|
ff84ded547 | ||
|
|
6220e4f63f | ||
|
|
b0c2521101 | ||
|
|
30d2a03fd0 | ||
|
|
61a4b558a8 | ||
|
|
3771d4bfd8 | ||
|
|
b087e849b5 | ||
|
|
963ec282d3 | ||
|
|
7690fb9287 | ||
|
|
57f0aefbc8 | ||
|
|
2e54520b32 | ||
|
|
cdf5b29ac2 | ||
|
|
733b929940 | ||
|
|
211fe48e29 | ||
|
|
9d3f7fe556 | ||
|
|
5d87508d0e | ||
|
|
40d0e7e90e | ||
|
|
7ca10ff5a4 | ||
|
|
1705e66be2 | ||
|
|
1e4f804542 | ||
|
|
6a03be9d64 | ||
|
|
591278862a | ||
|
|
9848e4b600 | ||
|
|
f0a8020189 | ||
|
|
f2f32c35ea | ||
|
|
d4770f16e3 | ||
|
|
e487f9a951 | ||
|
|
eb4ecf853b | ||
|
|
54e2deaddc | ||
|
|
1ac510af3d | ||
|
|
a666c8def3 | ||
|
|
33933ef212 | ||
|
|
d9dade7181 | ||
|
|
87502f4249 | ||
|
|
9adafbd473 | ||
|
|
47ea2f6ed7 | ||
|
|
e2ba0ec059 | ||
|
|
870cbc103d | ||
|
|
dfca664f6e | ||
|
|
00cfd427b1 | ||
|
|
e639a32822 | ||
|
|
8765bc800d | ||
|
|
1dc73a951e | ||
|
|
317b80bf4d | ||
|
|
2aaae95e89 | ||
|
|
0580e45af9 | ||
|
|
0dbf6453ac | ||
|
|
695324832c | ||
|
|
ac4c84e7a4 | ||
|
|
18aa453bb0 | ||
|
|
17f78169f2 | ||
|
|
6f6a06c8c3 | ||
|
|
79a4291153 | ||
|
|
a54fb98d4e | ||
|
|
a3549ae694 | ||
|
|
42c441f534 | ||
|
|
a957ea37f6 | ||
|
|
a4d426bc00 | ||
|
|
a924f53320 | ||
|
|
4f90f92b38 | ||
|
|
753b3befad | ||
|
|
544bc9bd01 | ||
|
|
1a467565c1 | ||
|
|
562daf9b04 | ||
|
|
8f15887c03 | ||
|
|
7384cdc241 | ||
|
|
56851f2c2d | ||
|
|
50c9d95ce0 | ||
|
|
4f4659b0e2 | ||
|
|
7b3a1eb4ff | ||
|
|
2456432844 | ||
|
|
3e66647f9f | ||
|
|
6d6571be0b | ||
|
|
dad4cd90ca | ||
|
|
f9307986cd | ||
|
|
f60090e5fa | ||
|
|
ae2bb3a028 | ||
|
|
c319d6bcfa | ||
|
|
e2ef9b8122 | ||
|
|
8e48df5de1 | ||
|
|
a3b1d7fb7c | ||
|
|
41d43e84a5 | ||
|
|
bfd3eff6f2 | ||
|
|
75c41f4466 | ||
|
|
85caf0a892 | ||
|
|
692205b0e6 | ||
|
|
02e6a3df99 | ||
|
|
b8ee696b69 | ||
|
|
26c4b1afa6 | ||
|
|
622827efda | ||
|
|
854c21639a | ||
|
|
0a309ad0e1 | ||
|
|
52b0ae0400 |
@@ -67,6 +67,9 @@ jobs:
|
||||
- run:
|
||||
name: Test - Definition Lists
|
||||
command: npm run test:definition-lists
|
||||
- run:
|
||||
name: Test - Hard Breaks
|
||||
command: npm run test:hard-breaks
|
||||
- run:
|
||||
name: Test - Variables
|
||||
command: npm run test:variables
|
||||
|
||||
79
.eslintrc.js
79
.eslintrc.js
@@ -1,79 +0,0 @@
|
||||
module.exports = {
|
||||
root : true,
|
||||
parserOptions : {
|
||||
ecmaVersion : 2021,
|
||||
sourceType : 'module',
|
||||
ecmaFeatures : {
|
||||
jsx : true
|
||||
}
|
||||
},
|
||||
env : {
|
||||
browser : true,
|
||||
node : true
|
||||
},
|
||||
plugins : ['react', 'jest'],
|
||||
rules : {
|
||||
/** Errors **/
|
||||
'camelcase' : ['error', { properties: 'never' }],
|
||||
//'func-style' : ['error', 'expression', { allowArrowFunctions: true }],
|
||||
'no-array-constructor' : 'error',
|
||||
'no-iterator' : 'error',
|
||||
'no-nested-ternary' : 'error',
|
||||
'no-new-object' : 'error',
|
||||
'no-proto' : 'error',
|
||||
'react/jsx-no-bind' : ['error', { allowArrowFunctions: true }],
|
||||
'react/jsx-uses-react' : 'error',
|
||||
'react/prefer-es6-class' : ['error', 'never'],
|
||||
'jest/valid-expect' : ['error', { maxArgs: 3 }],
|
||||
|
||||
/** Warnings **/
|
||||
'max-lines' : ['warn', {
|
||||
max : 200,
|
||||
skipComments : true,
|
||||
skipBlankLines : true,
|
||||
}],
|
||||
'max-depth' : ['warn', { max: 4 }],
|
||||
'max-params' : ['warn', { max: 5 }],
|
||||
'no-restricted-syntax' : ['warn', 'ClassDeclaration', 'SwitchStatement'],
|
||||
'no-unused-vars' : ['warn', {
|
||||
vars : 'all',
|
||||
args : 'none',
|
||||
varsIgnorePattern : 'config|_|cx|createClass'
|
||||
}],
|
||||
'react/jsx-uses-vars' : 'warn',
|
||||
|
||||
/** Fixable **/
|
||||
'arrow-parens' : ['warn', 'always'],
|
||||
'brace-style' : ['warn', '1tbs', { allowSingleLine: true }],
|
||||
'jsx-quotes' : ['warn', 'prefer-single'],
|
||||
'no-var' : 'warn',
|
||||
'prefer-const' : 'warn',
|
||||
'prefer-template' : 'warn',
|
||||
'quotes' : ['warn', 'single', { 'allowTemplateLiterals': true }],
|
||||
'semi' : ['warn', 'always'],
|
||||
|
||||
/** Whitespace **/
|
||||
'array-bracket-spacing' : ['warn', 'never'],
|
||||
'arrow-spacing' : ['warn', { before: false, after: false }],
|
||||
'comma-spacing' : ['warn', { before: false, after: true }],
|
||||
'indent' : ['warn', 'tab', { 'MemberExpression': 'off' }],
|
||||
'keyword-spacing' : ['warn', {
|
||||
before : true,
|
||||
after : true,
|
||||
overrides : {
|
||||
if : { 'before': false, 'after': false }
|
||||
}
|
||||
}],
|
||||
'key-spacing' : ['warn', {
|
||||
multiLine : { beforeColon: true, afterColon: true, align: 'colon' },
|
||||
singleLine : { beforeColon: false, afterColon: true }
|
||||
}],
|
||||
'linebreak-style' : 'off',
|
||||
'no-trailing-spaces' : 'warn',
|
||||
'no-whitespace-before-property' : 'warn',
|
||||
'object-curly-spacing' : ['warn', 'always'],
|
||||
'react/jsx-indent-props' : ['warn', 'tab'],
|
||||
'space-in-parens' : ['warn', 'never'],
|
||||
'template-curly-spacing' : ['warn', 'never'],
|
||||
}
|
||||
};
|
||||
8
.github/workflows/pr-check.yml
vendored
8
.github/workflows/pr-check.yml
vendored
@@ -14,11 +14,15 @@ jobs:
|
||||
if: always() && github.repository_owner == 'naturalcrit'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: ./.github/actions/limit-pull-requests
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name : Run limit-pull-requests action
|
||||
uses: ./.github/actions/limit-pull-requests
|
||||
with:
|
||||
except-users: |
|
||||
dependabot
|
||||
comment-limit: 1
|
||||
comment-limit: 3
|
||||
comment: |
|
||||
Hi, thanks for your contribution to the Homebrewery! You already have >=3 open pull requests. Consider completing some of your existing PRs before opening new ones. Thanks!
|
||||
close-limit: 5
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"stylelint-config-recess-order",
|
||||
"stylelint-config-recommended"],
|
||||
"plugins": [
|
||||
"stylelint-stylistic",
|
||||
"@stylistic/stylelint-plugin",
|
||||
"./stylelint_plugins/declaration-colon-align.js",
|
||||
"./stylelint_plugins/declaration-colon-min-space-before",
|
||||
"./stylelint_plugins/declaration-block-multi-line-min-declarations"
|
||||
@@ -16,32 +16,32 @@
|
||||
"font-family-no-missing-generic-family-keyword" : null,
|
||||
"font-weight-notation" : "named-where-possible",
|
||||
"font-family-name-quotes" : "always-unless-keyword",
|
||||
"stylistic/indentation" : "tab",
|
||||
"@stylistic/indentation" : "tab",
|
||||
"no-duplicate-selectors" : true,
|
||||
"stylistic/color-hex-case" : "upper",
|
||||
"@stylistic/color-hex-case" : "upper",
|
||||
"color-hex-length" : "long",
|
||||
"stylistic/selector-combinator-space-after" : "always",
|
||||
"stylistic/selector-combinator-space-before" : "always",
|
||||
"stylistic/selector-attribute-operator-space-before" : "never",
|
||||
"stylistic/selector-attribute-operator-space-after" : "never",
|
||||
"stylistic/selector-attribute-brackets-space-inside" : "never",
|
||||
"@stylistic/selector-combinator-space-after" : "always",
|
||||
"@stylistic/selector-combinator-space-before" : "always",
|
||||
"@stylistic/selector-attribute-operator-space-before" : "never",
|
||||
"@stylistic/selector-attribute-operator-space-after" : "never",
|
||||
"@stylistic/selector-attribute-brackets-space-inside" : "never",
|
||||
"selector-attribute-quotes" : "always",
|
||||
"selector-pseudo-element-colon-notation" : "double",
|
||||
"stylistic/selector-pseudo-class-parentheses-space-inside" : "never",
|
||||
"stylistic/block-opening-brace-space-before" : "always",
|
||||
"@stylistic/selector-pseudo-class-parentheses-space-inside" : "never",
|
||||
"@stylistic/block-opening-brace-space-before" : "always",
|
||||
"naturalcrit/declaration-colon-min-space-before" : 1,
|
||||
"stylistic/declaration-block-trailing-semicolon" : "always",
|
||||
"stylistic/declaration-colon-space-after" : "always",
|
||||
"stylistic/number-leading-zero" : "always",
|
||||
"@stylistic/declaration-block-trailing-semicolon" : "always",
|
||||
"@stylistic/declaration-colon-space-after" : "always",
|
||||
"@stylistic/number-leading-zero" : "always",
|
||||
"function-url-quotes" : ["always", { "except": ["empty"] }],
|
||||
"function-url-scheme-disallowed-list" : ["data","http"],
|
||||
"comment-whitespace-inside" : "always",
|
||||
"stylistic/string-quotes" : "single",
|
||||
"stylistic/media-feature-range-operator-space-before" : "always",
|
||||
"stylistic/media-feature-range-operator-space-after" : "always",
|
||||
"stylistic/media-feature-parentheses-space-inside" : "never",
|
||||
"stylistic/media-feature-colon-space-before" : "always",
|
||||
"stylistic/media-feature-colon-space-after" : "always",
|
||||
"@stylistic/string-quotes" : "single",
|
||||
"@stylistic/media-feature-range-operator-space-before" : "always",
|
||||
"@stylistic/media-feature-range-operator-space-after" : "always",
|
||||
"@stylistic/media-feature-parentheses-space-inside" : "never",
|
||||
"@stylistic/media-feature-colon-space-before" : "always",
|
||||
"@stylistic/media-feature-colon-space-after" : "always",
|
||||
"naturalcrit/declaration-colon-align" : true,
|
||||
"naturalcrit/declaration-block-multi-line-min-declarations": 1
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:18-alpine
|
||||
FROM node:20-alpine
|
||||
RUN apk --no-cache add git
|
||||
|
||||
ENV NODE_ENV=docker
|
||||
|
||||
208
changelog.md
208
changelog.md
@@ -84,6 +84,192 @@ pre {
|
||||
## changelog
|
||||
For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery).
|
||||
|
||||
### Tuesday 8/27/2024 - v3.14.2
|
||||
{{taskList
|
||||
|
||||
##### calculuschild
|
||||
|
||||
* [x] Reroute invalid urls to homepage
|
||||
|
||||
Fixes issues [#3269](https://github.com/naturalcrit/homebrewery/issues/3629)
|
||||
|
||||
* [x] Background dependency updates
|
||||
|
||||
##### G-Ambatte
|
||||
|
||||
* [x] Add route to get brew styling via `/css/shareId`
|
||||
|
||||
Fixes issues [#1097](https://github.com/naturalcrit/homebrewery/issues/1097)
|
||||
|
||||
* [x] Fix `:emojis:` preventing code folding
|
||||
|
||||
Fixes issues [#3604](https://github.com/naturalcrit/homebrewery/issues/3604)
|
||||
|
||||
* [x] Fix mask image warping when rotated and stretched
|
||||
|
||||
Fixes issues [#3636](https://github.com/naturalcrit/homebrewery/issues/3636)
|
||||
|
||||
* [x] Fix Table of Contents uppercasing
|
||||
|
||||
Fixes issues [#3572](https://github.com/naturalcrit/homebrewery/issues/3572)
|
||||
|
||||
##### abquintic
|
||||
|
||||
* [x] Create globally unique Header IDs across pages
|
||||
|
||||
Fixes issues [#1430](https://github.com/naturalcrit/homebrewery/issues/1430)
|
||||
|
||||
* [x] Fix colon `:::` being parsed in codeblocks
|
||||
|
||||
* [x] Prevent crashes when loading undefined renderer or theme bundle
|
||||
|
||||
* [x] Add Jump-To hotkeys
|
||||
|
||||
* Use `CTRL/META + SHIFT + LEFTARROW` to brewJump
|
||||
* Use `CTRL/META + SHIFT + RIGHTARROW` to sourceJump
|
||||
|
||||
* [x] Prevent reload from clobbering modified fresh clones
|
||||
|
||||
##### 5e-Cleric, Gazook89
|
||||
|
||||
* [x] Viewer tools for zoom/page navigation
|
||||
|
||||
}}
|
||||
|
||||
### Tuesday 8/13/2024 - v3.14.1
|
||||
{{taskList
|
||||
|
||||
##### abquintic
|
||||
|
||||
* [x] Allow Table of Contents to flow across columns
|
||||
|
||||
Fixes issues [#2563](https://github.com/naturalcrit/homebrewery/issues/2563)
|
||||
|
||||
* [x] Fix unusual margin spacing for adjacent `.descriptive` and `.wide` blocks
|
||||
|
||||
Fixes issues [#2688](https://github.com/naturalcrit/homebrewery/issues/2688)
|
||||
|
||||
* [x] Add code folding to :fas_paintbrush: {{openSans **STYLE**}} tab
|
||||
|
||||
##### G-Ambatte
|
||||
|
||||
* [x] Fix edge case where Table of Contents generator changed capitalization of headings
|
||||
|
||||
Fixes issues [#3572](https://github.com/naturalcrit/homebrewery/issues/3572)
|
||||
|
||||
* [x] Fix **Ink Friendly** snippet causing unselectable PDF text
|
||||
|
||||
Fixes issues [#3563](https://github.com/naturalcrit/homebrewery/issues/3563)
|
||||
|
||||
* [x] Prevent brews selecting themselves as a theme
|
||||
|
||||
Fixes issues [#3614](https://github.com/naturalcrit/homebrewery/issues/3614)
|
||||
|
||||
* [x] Fix info pages (`/faq`, `/migrate`, etc.) showing blank authorship info
|
||||
|
||||
Fixes issues [#3568](https://github.com/naturalcrit/homebrewery/issues/3568)
|
||||
|
||||
* [x] Add `abs()`, `sign()` and `signed()` functions to variable syntax math handler
|
||||
|
||||
Fixes issues [#3537](https://github.com/naturalcrit/homebrewery/issues/3537)
|
||||
|
||||
* [x] Fix variable math handler not processing commas (i.e., in `$[max(varA,varB)]`
|
||||
|
||||
Fixes issues [#3613](https://github.com/naturalcrit/homebrewery/issues/3613)
|
||||
|
||||
* [x] Fix variable math handler scrambling variables with names that are subsets of other variables
|
||||
|
||||
Fixes issues [#3622](https://github.com/naturalcrit/homebrewery/issues/3622)
|
||||
|
||||
|
||||
##### calculuschild
|
||||
|
||||
* [x] Fix `/migrate` page using an editor context instead of share context
|
||||
|
||||
|
||||
##### 5e-Cleric
|
||||
|
||||
* [x] Fix Monster Stat Blocks losing color in Safari
|
||||
|
||||
}}
|
||||
|
||||
\page
|
||||
|
||||
### Monday 7/29/2024 - v3.14.0
|
||||
{{taskList
|
||||
|
||||
##### abquintic, calculuschild
|
||||
|
||||
* [x] Alternative Brew Themes, including importing other brews as a base theme.
|
||||
|
||||
- In the :fas_circle_info: **Properties** menu, find the new {{openSans **THEME**}} dropdown. It lists Brew Themes, including a new **Blank** theme as a simpler basis for custom styling.
|
||||
- Brews tagged with `meta:theme` will appear in the Brew Themes list. Selecting one loads its :fas_paintbrush: **Style** tab contents as the CSS basis for the current brew, allowing one brew to style multiple documents.
|
||||
- Brews with `meta:theme` can also select their own Theme, i.e. layering Themes on top of each other.
|
||||
- The next goal is to make **Published** Themes shareable between users.
|
||||
|
||||
|
||||
Fixes issues [#1899](https://github.com/naturalcrit/homebrewery/issues/1899), [#3085](https://github.com/naturalcrit/homebrewery/issues/3085)
|
||||
|
||||
##### G-Ambatte
|
||||
|
||||
* [x] Fix Drop-cap font becoming corrupted when Bold
|
||||
|
||||
Fixes issues [#3551](https://github.com/naturalcrit/homebrewery/issues/3551)
|
||||
|
||||
* [x] Fixes to UI styling
|
||||
|
||||
Fixes issues [#3568](https://github.com/naturalcrit/homebrewery/issues/3568)
|
||||
|
||||
}}
|
||||
|
||||
|
||||
### Saturday 6/7/2024 - v3.13.1
|
||||
{{taskList
|
||||
|
||||
##### calculuschild, G-Ambatte
|
||||
|
||||
* [x] Hotfixes for issues with v3.13.0
|
||||
|
||||
Fixes issues [#3559](https://github.com/naturalcrit/homebrewery/issues/3559), [#3552](https://github.com/naturalcrit/homebrewery/issues/3552), [#3554](https://github.com/naturalcrit/homebrewery/issues/3554)
|
||||
}}
|
||||
|
||||
### Friday 28/6/2024 - v3.13.0
|
||||
{{taskList
|
||||
|
||||
##### calculuschild
|
||||
|
||||
* [x] Add `:emoji:` Markdown syntax, with autosuggest; start typing after the first `:` for matching emojis from
|
||||
:fab_font_awesome: FontAwesome, :df_d20: DiceFont, :ei_action: ElderberryInn, and a subset of :gi_broadsword: GameIcons
|
||||
|
||||
* [x] Fix `{curly injection}` to append to, rather than erase and replace target CSS
|
||||
* [x] {{openSans **GET PDF**}} {{fa,fa-file-pdf}} now opens the print dialog directly, rather than redirecting to a separate page
|
||||
|
||||
##### Gazook
|
||||
|
||||
* [x] Several small style tweaks to the UI
|
||||
* [x] Cleaning and refactoring several large pieces of code
|
||||
|
||||
##### 5e-Cleric
|
||||
|
||||
* [x] For error pages, add links to user account and `/share` page if available
|
||||
|
||||
Fixes issue [#3298](https://github.com/naturalcrit/homebrewery/issues/3298)
|
||||
|
||||
* [x] Change FrontCover title to use stroke outline instead of faking it with dozens of shadows
|
||||
* [x] Cleaning and refactoring several large pieces of CSS
|
||||
|
||||
##### abquintic
|
||||
|
||||
* [x] Added additional {{openSans **TABLE OF CONTENTS**}} snippet options. Explicitly include or exclude items from the ToC generation via CSS properties
|
||||
`--TOC:exclude` or `--TOC:include`, or change the included header depth from 3 to 6 (default 3) with `tocDepthH6`
|
||||
|
||||
##### MurdoMaclachlan *(new contributor!)*
|
||||
|
||||
* [x] Added "proficiency bonus" to Monster Stat Block snippet.
|
||||
|
||||
Fixes issue [#3397](https://github.com/naturalcrit/homebrewery/issues/3397)
|
||||
}}
|
||||
|
||||
### Monday 18/3/2024 - v3.12.0
|
||||
{{taskList
|
||||
|
||||
@@ -375,7 +561,7 @@ Fixes issue [#2729](https://github.com/naturalcrit/homebrewery/issues/2729),
|
||||
### Thursday 17/08/2023 - v3.9.2
|
||||
{{taskList
|
||||
|
||||
##### Calculuschild
|
||||
##### calculuschild
|
||||
|
||||
* [x] Fix links to certain old Google Drive files
|
||||
|
||||
@@ -433,7 +619,7 @@ Fixes issue [#1924](https://github.com/naturalcrit/homebrewery/issues/1924)
|
||||
### Friday 02/06/2023 - v3.9.0
|
||||
{{taskList
|
||||
|
||||
##### Calculuschild
|
||||
##### calculuschild
|
||||
|
||||
* [x] Fix some files not showing up on userpage when user has a large number of brews in Google Drive
|
||||
|
||||
@@ -530,7 +716,7 @@ Fixes issues [#2731](https://github.com/naturalcrit/homebrewery/issues/2731)
|
||||
### Monday 13/03/2023 - v3.7.2
|
||||
{{taskList
|
||||
|
||||
##### Calculuschild
|
||||
##### calculuschild
|
||||
|
||||
* [x] Fix wide Monster Stat Blocks not spanning columns on Legacy
|
||||
}}
|
||||
@@ -553,7 +739,7 @@ Fixes issues [#1569](https://github.com/naturalcrit/homebrewery/issues/1569)
|
||||
* [x] Updated the Google Drive icon
|
||||
* [x] Backend fix to unit tests failing intermittently
|
||||
|
||||
##### Calculuschild
|
||||
##### calculuschild
|
||||
|
||||
* [x] Fix PDF pixelation on CoverPage text outlines
|
||||
}}
|
||||
@@ -565,7 +751,7 @@ Fixes issues [#1569](https://github.com/naturalcrit/homebrewery/issues/1569)
|
||||
**NOTE:** Some new snippets will now show a {{beta BETA}} tag. Feel free to use them, but be aware we may change how they work depending on your feedback.
|
||||
}}
|
||||
|
||||
##### Calculuschild
|
||||
##### calculuschild
|
||||
|
||||
* [x] New {{openSans **IMAGES → WATERCOLOR EDGE** {{fac,mask-edge}} }} and {{openSans **WATERCOLOR CORNER** {{fac,mask-corner}} }} snippets for V3, which adds a stylish watercolor texture to the edge of your images! (Thanks to /u/flamableconcrete on Reddit for providing these image masks!)
|
||||
|
||||
@@ -709,7 +895,7 @@ Fixes issues [#1670](https://github.com/naturalcrit/homebrewery/issues/1670)
|
||||
### Thursday 28/10/2022 - v3.3.1
|
||||
{{taskList
|
||||
|
||||
##### Calculuschild
|
||||
##### calculuschild
|
||||
|
||||
* [x] Fixes to several broken CSS styles from v3.3.0
|
||||
|
||||
@@ -724,7 +910,7 @@ Fixes issues [#2468](https://github.com/naturalcrit/homebrewery/issues/2468)
|
||||
### Friday 19/10/2022 - v3.3.0
|
||||
{{taskList
|
||||
|
||||
##### Calculuschild
|
||||
##### calculuschild
|
||||
|
||||
* [x] Fix for tables broken by Chrome v106
|
||||
|
||||
@@ -807,7 +993,7 @@ Fixes issues [#2317](https://github.com/naturalcrit/homebrewery/issues/2317), [
|
||||
### Wednesday 31/08/2022 - v3.2.1
|
||||
{{taskList
|
||||
|
||||
##### Calculuschild
|
||||
##### calculuschild
|
||||
|
||||
* [x] Reference Links should now work inside tables
|
||||
|
||||
@@ -833,7 +1019,7 @@ Fixes issues [#2317](https://github.com/naturalcrit/homebrewery/issues/2317), [
|
||||
### Saturday 27/08/2022 - v3.2.0
|
||||
{{taskList
|
||||
|
||||
##### Calculuschild
|
||||
##### calculuschild
|
||||
|
||||
* [x] The V3 renderer is now the default for new brews.
|
||||
|
||||
@@ -860,7 +1046,7 @@ Fixes issues [#2317](https://github.com/naturalcrit/homebrewery/issues/2317), [
|
||||
### Thursday 09/06/2022 - v3.1.1
|
||||
{{taskList
|
||||
|
||||
##### Calculuschild:
|
||||
##### calculuschild:
|
||||
|
||||
* [x] Fixed class table decorations appearing on top of the table in PDF output.
|
||||
|
||||
@@ -1704,4 +1890,4 @@ Massive changelog incoming:
|
||||
|
||||
* Added `phb.standalone.css` plus a build system for creating it
|
||||
* Added page numbers and footer text
|
||||
* Page accent now flips each page
|
||||
* Page accent now flips each page
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
require('./brewCleanup.less');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const cx = require('classnames');
|
||||
|
||||
const request = require('superagent');
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
require('./brewCompress.less');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const cx = require('classnames');
|
||||
|
||||
const request = require('superagent');
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
const cx = require('classnames');
|
||||
require('./combobox.less');
|
||||
|
||||
const Combobox = createClass({
|
||||
|
||||
29
client/components/dialog.jsx
Normal file
29
client/components/dialog.jsx
Normal file
@@ -0,0 +1,29 @@
|
||||
// Dialog box, for popups and modal blocking messages
|
||||
const React = require('react');
|
||||
const { useRef, useEffect } = React;
|
||||
|
||||
function Dialog({ dismissKey, closeText = 'Close', blocking = false, ...rest }) {
|
||||
const dialogRef = useRef(null);
|
||||
|
||||
useEffect(()=>{
|
||||
if(!dismissKey || !localStorage.getItem(dismissKey)) {
|
||||
blocking ? dialogRef.current?.showModal() : dialogRef.current?.show();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const dismiss = ()=>{
|
||||
dismissKey && localStorage.setItem(dismissKey, true);
|
||||
dialogRef.current?.close();
|
||||
};
|
||||
|
||||
return (
|
||||
<dialog ref={dialogRef} onCancel={dismiss} {...rest}>
|
||||
{rest.children}
|
||||
<button className='dismiss' onClick={dismiss}>
|
||||
{closeText}
|
||||
</button>
|
||||
</dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dialog;
|
||||
@@ -7,14 +7,17 @@ const _ = require('lodash');
|
||||
const MarkdownLegacy = require('naturalcrit/markdownLegacy.js');
|
||||
const Markdown = require('naturalcrit/markdown.js');
|
||||
const ErrorBar = require('./errorBar/errorBar.jsx');
|
||||
const ToolBar = require('./toolBar/toolBar.jsx');
|
||||
|
||||
//TODO: move to the brew renderer
|
||||
const RenderWarnings = require('homebrewery/renderWarnings/renderWarnings.jsx');
|
||||
const NotificationPopup = require('./notificationPopup/notificationPopup.jsx');
|
||||
const Frame = require('react-frame-component').default;
|
||||
const dedent = require('dedent-tabs').default;
|
||||
const { printCurrentBrew } = require('../../../shared/helpers.js');
|
||||
|
||||
const Themes = require('themes/themes.json');
|
||||
const DOMPurify = require('dompurify');
|
||||
const purifyConfig = { FORCE_BODY: true, SANITIZE_DOM: false };
|
||||
|
||||
const PAGE_HEIGHT = 1056;
|
||||
|
||||
@@ -33,8 +36,9 @@ const BrewPage = (props)=>{
|
||||
index : 0,
|
||||
...props
|
||||
};
|
||||
const cleanText = props.contents; //DOMPurify.sanitize(props.contents, purifyConfig);
|
||||
return <div className={props.className} id={`p${props.index + 1}`} >
|
||||
<div className='columnWrapper' dangerouslySetInnerHTML={{ __html: props.contents }} />
|
||||
<div className='columnWrapper' dangerouslySetInnerHTML={{ __html: cleanText }} />
|
||||
</div>;
|
||||
};
|
||||
|
||||
@@ -52,14 +56,16 @@ const BrewRenderer = (props)=>{
|
||||
lang : '',
|
||||
errors : [],
|
||||
currentEditorPage : 0,
|
||||
themeBundle : {},
|
||||
...props
|
||||
};
|
||||
|
||||
const [state, setState] = useState({
|
||||
viewablePageNumber : 0,
|
||||
height : PAGE_HEIGHT,
|
||||
isMounted : false,
|
||||
visibility : 'hidden',
|
||||
height : PAGE_HEIGHT,
|
||||
isMounted : false,
|
||||
visibility : 'hidden',
|
||||
zoom : 100,
|
||||
currentPageNumber : 1,
|
||||
});
|
||||
|
||||
const mainRef = useRef(null);
|
||||
@@ -81,11 +87,14 @@ const BrewRenderer = (props)=>{
|
||||
}));
|
||||
};
|
||||
|
||||
const handleScroll = (e)=>{
|
||||
const target = e.target;
|
||||
const getCurrentPage = (e)=>{
|
||||
const { scrollTop, clientHeight, scrollHeight } = e.target;
|
||||
const totalScrollableHeight = scrollHeight - clientHeight;
|
||||
const currentPageNumber = Math.ceil((scrollTop / totalScrollableHeight) * rawPages.length);
|
||||
|
||||
setState((prevState)=>({
|
||||
...prevState,
|
||||
viewablePageNumber : Math.floor(target.scrollTop / target.scrollHeight * rawPages.length)
|
||||
currentPageNumber : currentPageNumber || 1
|
||||
}));
|
||||
};
|
||||
|
||||
@@ -96,30 +105,12 @@ const BrewRenderer = (props)=>{
|
||||
if(index == props.currentEditorPage) //Already rendered before this step
|
||||
return false;
|
||||
|
||||
if(Math.abs(index - state.viewablePageNumber) <= 3)
|
||||
if(Math.abs(index - state.currentPageNumber) <= 3)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const sanitizeScriptTags = (content)=>{
|
||||
return content
|
||||
?.replace(/<script/ig, '<script')
|
||||
.replace(/<\/script>/ig, '</script>')
|
||||
|| '';
|
||||
};
|
||||
|
||||
const renderPageInfo = ()=>{
|
||||
return <div className='pageInfo' ref={mainRef}>
|
||||
<div>
|
||||
{props.renderer}
|
||||
</div>
|
||||
<div>
|
||||
{state.viewablePageNumber + 1} / {rawPages.length}
|
||||
</div>
|
||||
</div>;
|
||||
};
|
||||
|
||||
const renderDummyPage = (index)=>{
|
||||
return <div className='phb page' id={`p${index + 1}`} key={index}>
|
||||
<i className='fas fa-spinner fa-spin' />
|
||||
@@ -127,20 +118,18 @@ const BrewRenderer = (props)=>{
|
||||
};
|
||||
|
||||
const renderStyle = ()=>{
|
||||
if(!props.style) return;
|
||||
const cleanStyle = sanitizeScriptTags(props.style);
|
||||
//return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `<style>@layer styleTab {\n${sanitizeScriptTags(props.style)}\n} </style>` }} />;
|
||||
return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `<style> ${cleanStyle} </style>` }} />;
|
||||
const cleanStyle = props.style; //DOMPurify.sanitize(props.style, purifyConfig);
|
||||
const themeStyles = props.themeBundle?.joinedStyles ?? '<style>@import url("/themes/V3/Blank/style.css");</style>';
|
||||
return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `${themeStyles} \n\n <style> ${cleanStyle} </style>` }} />;
|
||||
};
|
||||
|
||||
const renderPage = (pageText, index)=>{
|
||||
let cleanPageText = sanitizeScriptTags(pageText);
|
||||
if(props.renderer == 'legacy') {
|
||||
const html = MarkdownLegacy.render(cleanPageText);
|
||||
const html = MarkdownLegacy.render(pageText);
|
||||
return <BrewPage className='page phb' index={index} key={index} contents={html} />;
|
||||
} else {
|
||||
cleanPageText += `\n\n \n\\column\n `; //Artificial column break at page end to emulate column-fill:auto (until `wide` is used, when column-fill:balance will reappear)
|
||||
const html = Markdown.render(cleanPageText, index);
|
||||
pageText += `\n\n \n\\column\n `; //Artificial column break at page end to emulate column-fill:auto (until `wide` is used, when column-fill:balance will reappear)
|
||||
const html = Markdown.render(pageText, index);
|
||||
return <BrewPage className='page' index={index} key={index} contents={html} />;
|
||||
}
|
||||
};
|
||||
@@ -163,6 +152,16 @@ const BrewRenderer = (props)=>{
|
||||
return renderedPages;
|
||||
};
|
||||
|
||||
const handleControlKeys = (e)=>{
|
||||
if(!(e.ctrlKey || e.metaKey)) return;
|
||||
const P_KEY = 80;
|
||||
if(e.keyCode == P_KEY && props.allowPrint) printCurrentBrew();
|
||||
if(e.keyCode == P_KEY) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
const frameDidMount = ()=>{ //This triggers when iFrame finishes internal "componentDidMount"
|
||||
setTimeout(()=>{ //We still see a flicker where the style isn't applied yet, so wait 100ms before showing iFrame
|
||||
updateSize();
|
||||
@@ -181,21 +180,33 @@ const BrewRenderer = (props)=>{
|
||||
document.dispatchEvent(new MouseEvent('click'));
|
||||
};
|
||||
|
||||
const rendererPath = props.renderer == 'V3' ? 'V3' : 'Legacy';
|
||||
const themePath = props.theme ?? '5ePHB';
|
||||
const baseThemePath = Themes[rendererPath][themePath].baseTheme;
|
||||
//Toolbar settings:
|
||||
const handleZoom = (newZoom)=>{
|
||||
setState((prevState)=>({
|
||||
...prevState,
|
||||
zoom : newZoom
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/*render dummy page while iFrame is mounting.*/}
|
||||
{!state.isMounted
|
||||
? <div className='brewRenderer' onScroll={handleScroll}>
|
||||
? <div className='brewRenderer' onScroll={getCurrentPage}>
|
||||
<div className='pages'>
|
||||
{renderDummyPage(1)}
|
||||
</div>
|
||||
</div>
|
||||
: null}
|
||||
|
||||
<ErrorBar errors={props.errors} />
|
||||
<div className='popups' ref={mainRef}>
|
||||
<RenderWarnings />
|
||||
<NotificationPopup />
|
||||
</div>
|
||||
|
||||
<ToolBar onZoomChange={handleZoom} currentPage={state.currentPageNumber} totalPages={rawPages.length}/>
|
||||
|
||||
{/*render in iFrame so broken code doesn't crash the site.*/}
|
||||
<Frame id='BrewRenderer' initialContent={INITIAL_CONTENT}
|
||||
style={{ width: '100%', height: '100%', visibility: state.visibility }}
|
||||
@@ -203,33 +214,23 @@ const BrewRenderer = (props)=>{
|
||||
onClick={()=>{emitClick();}}
|
||||
>
|
||||
<div className={'brewRenderer'}
|
||||
onScroll={handleScroll}
|
||||
onScroll={getCurrentPage}
|
||||
onKeyDown={handleControlKeys}
|
||||
tabIndex={-1}
|
||||
style={{ height: state.height }}>
|
||||
|
||||
<ErrorBar errors={props.errors} />
|
||||
<div className='popups'>
|
||||
<RenderWarnings />
|
||||
<NotificationPopup />
|
||||
</div>
|
||||
<link href={`/themes/${rendererPath}/Blank/style.css`} type="text/css" rel='stylesheet'/>
|
||||
{baseThemePath &&
|
||||
<link href={`/themes/${rendererPath}/${baseThemePath}/style.css`} type="text/css" rel='stylesheet'/>
|
||||
}
|
||||
<link href={`/themes/${rendererPath}/${themePath}/style.css`} type="text/css" rel='stylesheet'/>
|
||||
|
||||
{/* Apply CSS from Style tab and render pages from Markdown tab */}
|
||||
{state.isMounted
|
||||
&&
|
||||
<>
|
||||
{renderStyle()}
|
||||
<div className='pages' lang={`${props.lang || 'en'}`}>
|
||||
<div className='pages' lang={`${props.lang || 'en'}`} style={{ zoom: `${state.zoom}%` }}>
|
||||
{renderPages()}
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
</Frame>
|
||||
{renderPageInfo()}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
@import (multiple, less) 'shared/naturalcrit/styles/reset.less';
|
||||
|
||||
.brewRenderer {
|
||||
will-change : transform;
|
||||
overflow-y : scroll;
|
||||
overflow-y : scroll;
|
||||
will-change : transform;
|
||||
padding-top : 30px;
|
||||
:where(.pages) {
|
||||
margin : 30px 0px;
|
||||
& > :where(.page) {
|
||||
@@ -14,53 +15,31 @@
|
||||
box-shadow : 1px 4px 14px #000000;
|
||||
}
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 20px;
|
||||
&:horizontal{
|
||||
height: 20px;
|
||||
width:auto;
|
||||
width : 20px;
|
||||
&:horizontal {
|
||||
width : auto;
|
||||
height : 20px;
|
||||
}
|
||||
&-thumb {
|
||||
background: linear-gradient(90deg, #d3c1af 15px, #00000000 15px);
|
||||
&:horizontal{
|
||||
background: linear-gradient(0deg, #d3c1af 15px, #00000000 15px);
|
||||
}
|
||||
}
|
||||
&-corner {
|
||||
visibility: hidden;
|
||||
background : linear-gradient(90deg, #D3C1AF 15px, #00000000 15px);
|
||||
&:horizontal { background : linear-gradient(0deg, #D3C1AF 15px, #00000000 15px); }
|
||||
}
|
||||
&-corner { visibility : hidden; }
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
.pane { position : relative; }
|
||||
.pageInfo {
|
||||
position : absolute;
|
||||
right : 17px;
|
||||
bottom : 0;
|
||||
z-index : 1000;
|
||||
font-size : 10px;
|
||||
font-weight : 800;
|
||||
color : white;
|
||||
background-color : #333333;
|
||||
div {
|
||||
display : inline-block;
|
||||
padding : 8px 10px;
|
||||
&:not(:last-child) { border-right : 1px solid #666666; }
|
||||
|
||||
@media print {
|
||||
.toolBar { display : none; }
|
||||
.brewRenderer {
|
||||
height : 100%;
|
||||
padding-top : unset;
|
||||
overflow-y : unset;
|
||||
.pages {
|
||||
margin : 0px;
|
||||
& > .page { box-shadow : unset; }
|
||||
}
|
||||
}
|
||||
}
|
||||
.ppr_msg {
|
||||
position : absolute;
|
||||
bottom : 0;
|
||||
left : 0px;
|
||||
z-index : 1000;
|
||||
padding : 8px 10px;
|
||||
font-size : 10px;
|
||||
font-weight : 800;
|
||||
color : white;
|
||||
background-color : #333333;
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@ require('./errorBar.less');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
const cx = require('classnames');
|
||||
|
||||
const ErrorBar = createClass({
|
||||
displayName : 'ErrorBar',
|
||||
|
||||
@@ -1,81 +1,45 @@
|
||||
require('./notificationPopup.less');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
const cx = require('classnames'); //Unused variable
|
||||
|
||||
import Dialog from '../../../components/dialog.jsx';
|
||||
|
||||
const DISMISS_KEY = 'dismiss_notification12-04-23';
|
||||
const DISMISS_BUTTON = <i className='fas fa-times dismiss' />;
|
||||
|
||||
const NotificationPopup = createClass({
|
||||
displayName : 'NotificationPopup',
|
||||
getInitialState : function() {
|
||||
return {
|
||||
notifications : {}
|
||||
};
|
||||
},
|
||||
componentDidMount : function() {
|
||||
this.checkNotifications();
|
||||
window.addEventListener('resize', this.checkNotifications);
|
||||
},
|
||||
componentWillUnmount : function() {
|
||||
window.removeEventListener('resize', this.checkNotifications);
|
||||
},
|
||||
notifications : {
|
||||
psa : function(){
|
||||
return (
|
||||
<>
|
||||
<li key='psa'>
|
||||
<em>Don't store IMAGES in Google Drive</em><br />
|
||||
Google Drive is not an image service, and will block images from being used
|
||||
in brews if they get more views than expected. Google has confirmed they won't fix
|
||||
this, so we recommend you look for another image hosting service such as imgur, ImgBB or Google Photos.
|
||||
</li>
|
||||
const NotificationPopup = ()=>{
|
||||
return <Dialog className='notificationPopup' dismissKey={DISMISS_KEY} closeText={DISMISS_BUTTON} >
|
||||
<div className='header'>
|
||||
<i className='fas fa-info-circle info'></i>
|
||||
<h3>Notice</h3>
|
||||
<small>This website is always improving and we are still adding new features and squashing bugs. Keep the following in mind:</small>
|
||||
</div>
|
||||
<ul>
|
||||
<li key='psa'>
|
||||
<em>Don't store IMAGES in Google Drive</em><br />
|
||||
Google Drive is not an image service, and will block images from being used
|
||||
in brews if they get more views than expected. Google has confirmed they won't fix
|
||||
this, so we recommend you look for another image hosting service such as imgur, ImgBB or Google Photos.
|
||||
</li>
|
||||
|
||||
<li key='googleDriveFolder'>
|
||||
<em>Don't delete your Homebrewery folder on Google Drive!</em> <br />
|
||||
We have had several reports of users losing their brews, not realizing
|
||||
that they had deleted the files on their Google Drive. If you have a Homebrewery folder
|
||||
on your Google Drive with *.txt files inside, <em>do not delete it</em>!
|
||||
We cannot help you recover files that you have deleted from your own
|
||||
Google Drive.
|
||||
</li>
|
||||
<li key='googleDriveFolder'>
|
||||
<em>Don't delete your Homebrewery folder on Google Drive!</em> <br />
|
||||
We have had several reports of users losing their brews, not realizing
|
||||
that they had deleted the files on their Google Drive. If you have a Homebrewery folder
|
||||
on your Google Drive with *.txt files inside, <em>do not delete it</em>!
|
||||
We cannot help you recover files that you have deleted from your own
|
||||
Google Drive.
|
||||
</li>
|
||||
|
||||
<li key='faq'>
|
||||
<em>Protect your work! </em> <br />
|
||||
If you opt not to use your Google Drive, keep in mind that we do not save a history of your projects. Please make frequent backups of your brews!
|
||||
<a target='_blank' href='https://www.reddit.com/r/homebrewery/comments/adh6lh/faqs_psas_announcements/'>
|
||||
See the FAQ
|
||||
</a> to learn how to avoid losing your work!
|
||||
</li>
|
||||
</>
|
||||
);
|
||||
}
|
||||
},
|
||||
checkNotifications : function(){
|
||||
const hideDismiss = localStorage.getItem(DISMISS_KEY);
|
||||
if(hideDismiss) return this.setState({ notifications: {} });
|
||||
|
||||
this.setState({
|
||||
notifications : _.mapValues(this.notifications, (fn)=>{ return fn(); }) //Convert notification functions into their return text value
|
||||
});
|
||||
},
|
||||
dismiss : function(){
|
||||
localStorage.setItem(DISMISS_KEY, true);
|
||||
this.checkNotifications();
|
||||
},
|
||||
render : function(){
|
||||
if(_.isEmpty(this.state.notifications)) return null;
|
||||
|
||||
return <div className='notificationPopup'>
|
||||
<i className='fas fa-times dismiss' onClick={this.dismiss}/>
|
||||
<i className='fas fa-info-circle info' />
|
||||
<div className='header'>
|
||||
<h3>Notice</h3>
|
||||
<small>This website is always improving and we are still adding new features and squashing bugs. Keep the following in mind:</small>
|
||||
</div>
|
||||
<ul>{_.values(this.state.notifications)}</ul>
|
||||
</div>;
|
||||
}
|
||||
});
|
||||
<li key='faq'>
|
||||
<em>Protect your work! </em> <br />
|
||||
If you opt not to use your Google Drive, keep in mind that we do not save a history of your projects. Please make frequent backups of your brews!
|
||||
<a target='_blank' href='https://www.reddit.com/r/homebrewery/comments/adh6lh/faqs_psas_announcements/'>
|
||||
See the FAQ
|
||||
</a> to learn how to avoid losing your work!
|
||||
</li>
|
||||
</ul>
|
||||
</Dialog>;
|
||||
};
|
||||
|
||||
module.exports = NotificationPopup;
|
||||
|
||||
@@ -1,64 +1,61 @@
|
||||
.popups{
|
||||
.popups {
|
||||
position : fixed;
|
||||
top : @navbarHeight;
|
||||
right : 15px;
|
||||
top : calc(@navbarHeight + @viewerToolsHeight);
|
||||
right : 24px;
|
||||
z-index : 10001;
|
||||
width : 450px;
|
||||
margin-top : 5px;
|
||||
}
|
||||
|
||||
.notificationPopup{
|
||||
.notificationPopup {
|
||||
position : relative;
|
||||
display : inline-block;
|
||||
width : 100%;
|
||||
padding : 15px;
|
||||
padding-bottom : 10px;
|
||||
padding-left : 25px;
|
||||
background-color : @blue;
|
||||
color : white;
|
||||
a{
|
||||
color : #e0e5c1;
|
||||
background-color : @blue;
|
||||
border : none;
|
||||
&[open] { display : inline-block; }
|
||||
a {
|
||||
font-weight : 800;
|
||||
color : #E0E5C1;
|
||||
}
|
||||
i.info{
|
||||
i.info {
|
||||
position : absolute;
|
||||
top : 12px;
|
||||
left : 12px;
|
||||
opacity : 0.8;
|
||||
font-size : 2.5em;
|
||||
opacity : 0.8;
|
||||
}
|
||||
i.dismiss{
|
||||
position : absolute;
|
||||
top : 10px;
|
||||
right : 10px;
|
||||
cursor : pointer;
|
||||
opacity : 0.6;
|
||||
&:hover{
|
||||
opacity : 1;
|
||||
}
|
||||
button.dismiss {
|
||||
position : absolute;
|
||||
top : 10px;
|
||||
right : 10px;
|
||||
cursor : pointer;
|
||||
background-color : transparent;
|
||||
opacity : 0.6;
|
||||
&:hover { opacity : 1; }
|
||||
}
|
||||
.header {
|
||||
padding-left : 50px;
|
||||
}
|
||||
small{
|
||||
opacity : 0.7;
|
||||
.header { padding-left : 50px; }
|
||||
small {
|
||||
font-size : 0.6em;
|
||||
opacity : 0.7;
|
||||
}
|
||||
h3{
|
||||
h3 {
|
||||
font-size : 1.1em;
|
||||
font-weight : 800;
|
||||
}
|
||||
ul{
|
||||
ul {
|
||||
margin-top : 15px;
|
||||
font-size : 0.8em;
|
||||
list-style-position : outside;
|
||||
list-style-type : disc;
|
||||
li{
|
||||
li {
|
||||
margin-top : 1.4em;
|
||||
font-size : 0.8em;
|
||||
line-height : 1.4em;
|
||||
margin-top : 1.4em;
|
||||
em{
|
||||
font-weight : 800;
|
||||
}
|
||||
em { font-weight : 800; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
162
client/homebrew/brewRenderer/toolBar/toolBar.jsx
Normal file
162
client/homebrew/brewRenderer/toolBar/toolBar.jsx
Normal file
@@ -0,0 +1,162 @@
|
||||
require('./toolBar.less');
|
||||
const React = require('react');
|
||||
const { useState, useEffect } = React;
|
||||
const _ = require('lodash');
|
||||
|
||||
|
||||
const MAX_ZOOM = 300;
|
||||
const MIN_ZOOM = 10;
|
||||
|
||||
const ToolBar = ({ onZoomChange, currentPage, onPageChange, totalPages })=>{
|
||||
|
||||
const [zoomLevel, setZoomLevel] = useState(100);
|
||||
const [pageNum, setPageNum] = useState(currentPage);
|
||||
|
||||
useEffect(()=>{
|
||||
onZoomChange(zoomLevel);
|
||||
}, [zoomLevel]);
|
||||
|
||||
useEffect(()=>{
|
||||
setPageNum(currentPage);
|
||||
}, [currentPage]);
|
||||
|
||||
const handleZoomButton = (zoom)=>{
|
||||
setZoomLevel(_.round(_.clamp(zoom, MIN_ZOOM, MAX_ZOOM)));
|
||||
};
|
||||
|
||||
const handlePageInput = (pageInput)=>{
|
||||
if(/[0-9]/.test(pageInput))
|
||||
setPageNum(parseInt(pageInput)); // input type is 'text', so `page` comes in as a string, not number.
|
||||
};
|
||||
|
||||
const scrollToPage = (pageNumber)=>{
|
||||
pageNumber = _.clamp(pageNumber, 1, totalPages);
|
||||
const iframe = document.getElementById('BrewRenderer');
|
||||
const brewRenderer = iframe?.contentWindow?.document.querySelector('.brewRenderer');
|
||||
const page = brewRenderer?.querySelector(`#p${pageNumber}`);
|
||||
page?.scrollIntoView({ block: 'start' });
|
||||
setPageNum(pageNumber);
|
||||
};
|
||||
|
||||
|
||||
const calculateChange = (mode)=>{
|
||||
const iframe = document.getElementById('BrewRenderer');
|
||||
const iframeWidth = iframe.getBoundingClientRect().width;
|
||||
const iframeHeight = iframe.getBoundingClientRect().height;
|
||||
const pages = iframe.contentWindow.document.getElementsByClassName('page');
|
||||
|
||||
let desiredZoom = 0;
|
||||
|
||||
if(mode == 'fill'){
|
||||
// find widest page, in case pages are different widths, so that the zoom is adapted to not cut the widest page off screen.
|
||||
const widestPage = _.maxBy([...pages], 'offsetWidth').offsetWidth;
|
||||
|
||||
desiredZoom = (iframeWidth / widestPage) * 100;
|
||||
|
||||
} else if(mode == 'fit'){
|
||||
// find the page with the largest single dim (height or width) so that zoom can be adapted to fit it.
|
||||
const minDimRatio = [...pages].reduce((minRatio, page) => Math.min(minRatio, iframeWidth / page.offsetWidth, iframeHeight / page.offsetHeight), Infinity);
|
||||
|
||||
desiredZoom = minDimRatio * 100;
|
||||
}
|
||||
|
||||
const margin = 5; // extra space so page isn't edge to edge (not truly "to fill")
|
||||
|
||||
const deltaZoom = (desiredZoom - zoomLevel) - margin;
|
||||
return deltaZoom;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='toolBar'>
|
||||
{/*v=====----------------------< Zoom Controls >---------------------=====v*/}
|
||||
<div className='group'>
|
||||
<button
|
||||
id='fill-width'
|
||||
className='tool'
|
||||
onClick={()=>handleZoomButton(zoomLevel + calculateChange('fill'))}
|
||||
>
|
||||
<i className='fac fit-width' />
|
||||
</button>
|
||||
<button
|
||||
id='zoom-to-fit'
|
||||
className='tool'
|
||||
onClick={()=>handleZoomButton(zoomLevel + calculateChange('fit'))}
|
||||
>
|
||||
<i className='fac zoom-to-fit' />
|
||||
</button>
|
||||
<button
|
||||
id='zoom-out'
|
||||
className='tool'
|
||||
onClick={()=>handleZoomButton(zoomLevel - 20)}
|
||||
disabled={zoomLevel <= MIN_ZOOM}
|
||||
>
|
||||
<i className='fas fa-magnifying-glass-minus' />
|
||||
</button>
|
||||
<input
|
||||
id='zoom-slider'
|
||||
className='range-input tool'
|
||||
type='range'
|
||||
name='zoom'
|
||||
list='zoomLevels'
|
||||
min={MIN_ZOOM}
|
||||
max={MAX_ZOOM}
|
||||
step='1'
|
||||
value={zoomLevel}
|
||||
onChange={(e)=>handleZoomButton(parseInt(e.target.value))}
|
||||
/>
|
||||
<datalist id='zoomLevels'>
|
||||
<option value='100' />
|
||||
</datalist>
|
||||
|
||||
<button
|
||||
id='zoom-in'
|
||||
className='tool'
|
||||
onClick={()=>handleZoomButton(zoomLevel + 20)}
|
||||
disabled={zoomLevel >= MAX_ZOOM}
|
||||
>
|
||||
<i className='fas fa-magnifying-glass-plus' />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/*v=====----------------------< Page Controls >---------------------=====v*/}
|
||||
<div className='group'>
|
||||
<button
|
||||
id='previous-page'
|
||||
className='previousPage tool'
|
||||
onClick={()=>scrollToPage(pageNum - 1)}
|
||||
disabled={pageNum <= 1}
|
||||
>
|
||||
<i className='fas fa-arrow-left'></i>
|
||||
</button>
|
||||
|
||||
<div className='tool'>
|
||||
<input
|
||||
id='page-input'
|
||||
className='text-input'
|
||||
type='text'
|
||||
name='page'
|
||||
inputMode='numeric'
|
||||
pattern='[0-9]'
|
||||
value={pageNum}
|
||||
onClick={(e)=>e.target.select()}
|
||||
onChange={(e)=>handlePageInput(e.target.value)}
|
||||
onBlur={()=>scrollToPage(pageNum)}
|
||||
onKeyDown={(e)=>e.key == 'Enter' && scrollToPage(pageNum)}
|
||||
/>
|
||||
<span id='page-count'>/ {totalPages}</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
id='next-page'
|
||||
className='tool'
|
||||
onClick={()=>scrollToPage(pageNum + 1)}
|
||||
disabled={pageNum >= totalPages}
|
||||
>
|
||||
<i className='fas fa-arrow-right'></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
module.exports = ToolBar;
|
||||
103
client/homebrew/brewRenderer/toolBar/toolBar.less
Normal file
103
client/homebrew/brewRenderer/toolBar/toolBar.less
Normal file
@@ -0,0 +1,103 @@
|
||||
@import (less) './client/icons/customIcons.less';
|
||||
|
||||
.toolBar {
|
||||
position : absolute;
|
||||
z-index : 1;
|
||||
box-sizing : border-box;
|
||||
display : flex;
|
||||
flex-wrap : wrap;
|
||||
gap : 8px 30px;
|
||||
align-items : center;
|
||||
justify-content : center;
|
||||
width : 100%;
|
||||
height : auto;
|
||||
padding : 2px 0;
|
||||
font-family : 'Open Sans', sans-serif;
|
||||
color : #CCCCCC;
|
||||
background-color : #555555;
|
||||
|
||||
.group {
|
||||
box-sizing : border-box;
|
||||
display : flex;
|
||||
gap : 0 3px;
|
||||
align-items : center;
|
||||
justify-content : center;
|
||||
height : 28px;
|
||||
}
|
||||
|
||||
.tool {
|
||||
display : flex;
|
||||
align-items : center;
|
||||
}
|
||||
|
||||
input {
|
||||
position : relative;
|
||||
height : 1.5em;
|
||||
padding : 2px 5px;
|
||||
font-family : 'Open Sans', sans-serif;
|
||||
color : #000000;
|
||||
background : #EEEEEE;
|
||||
border : 1px solid gray;
|
||||
&:focus { outline : 1px solid #D3D3D3; }
|
||||
|
||||
// `.range-input` if generic to all range inputs, or `#zoom-slider` if only for zoom slider
|
||||
&.range-input {
|
||||
padding : 2px 0;
|
||||
color : #D3D3D3;
|
||||
accent-color : #D3D3D3;
|
||||
|
||||
&::-webkit-slider-thumb, &::-moz-slider-thumb {
|
||||
width : 5px;
|
||||
height : 5px;
|
||||
cursor : pointer;
|
||||
outline : none;
|
||||
}
|
||||
|
||||
&:hover::after {
|
||||
position : absolute;
|
||||
bottom : -30px;
|
||||
left : 50%;
|
||||
z-index : 1;
|
||||
display : grid;
|
||||
place-items : center;
|
||||
width : 4ch;
|
||||
height : 1.2lh;
|
||||
pointer-events : none;
|
||||
content : attr(value);
|
||||
background-color : #555555;
|
||||
border : 1px solid #A1A1A1;
|
||||
transform : translate(-50%, 50%);
|
||||
}
|
||||
}
|
||||
|
||||
// `.text-input` if generic to all range inputs, or `#page-input` if only for current page input
|
||||
&#page-input {
|
||||
width : 4ch;
|
||||
margin-right : 1ch;
|
||||
text-align : center;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
box-sizing : content-box;
|
||||
display : flex;
|
||||
align-items : center;
|
||||
justify-content : center;
|
||||
width : auto;
|
||||
min-width : 46px;
|
||||
height : 100%;
|
||||
padding : 0 0px;
|
||||
font-weight : unset;
|
||||
color : inherit;
|
||||
background-color : unset;
|
||||
&:hover { background-color : #444444; }
|
||||
&:focus { outline : 1px solid #D3D3D3; }
|
||||
&:disabled {
|
||||
color : #777777;
|
||||
background-color : unset !important;
|
||||
}
|
||||
i {
|
||||
font-size:1.2em;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
const cx = require('classnames');
|
||||
const dedent = require('dedent-tabs').default;
|
||||
const Markdown = require('../../../shared/naturalcrit/markdown.js');
|
||||
|
||||
const CodeEditor = require('naturalcrit/codeEditor/codeEditor.jsx');
|
||||
const SnippetBar = require('./snippetbar/snippetbar.jsx');
|
||||
@@ -47,6 +48,9 @@ const Editor = createClass({
|
||||
};
|
||||
},
|
||||
|
||||
editor : React.createRef(null),
|
||||
codeEditor : React.createRef(null),
|
||||
|
||||
isText : function() {return this.state.view == 'text';},
|
||||
isStyle : function() {return this.state.view == 'style';},
|
||||
isMeta : function() {return this.state.view == 'meta';},
|
||||
@@ -55,6 +59,8 @@ const Editor = createClass({
|
||||
this.updateEditorSize();
|
||||
this.highlightCustomMarkdown();
|
||||
window.addEventListener('resize', this.updateEditorSize);
|
||||
document.getElementById('BrewRenderer').addEventListener('keydown', this.handleControlKeys);
|
||||
document.addEventListener('keydown', this.handleControlKeys);
|
||||
|
||||
const editorTheme = window.localStorage.getItem(EDITOR_THEME_KEY);
|
||||
if(editorTheme) {
|
||||
@@ -78,16 +84,29 @@ const Editor = createClass({
|
||||
};
|
||||
},
|
||||
|
||||
handleControlKeys : function(e){
|
||||
if(!(e.ctrlKey || e.metaKey)) return;
|
||||
const LEFTARROW_KEY = 37;
|
||||
const RIGHTARROW_KEY = 39;
|
||||
if (e.shiftKey && (e.keyCode == RIGHTARROW_KEY)) this.brewJump();
|
||||
if (e.shiftKey && (e.keyCode == LEFTARROW_KEY)) this.sourceJump();
|
||||
if ((e.keyCode == LEFTARROW_KEY) || (e.keyCode == RIGHTARROW_KEY)) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
updateEditorSize : function() {
|
||||
if(this.refs.codeEditor) {
|
||||
let paneHeight = this.refs.main.parentNode.clientHeight;
|
||||
if(this.codeEditor.current) {
|
||||
let paneHeight = this.editor.current.parentNode.clientHeight;
|
||||
paneHeight -= SNIPPETBAR_HEIGHT;
|
||||
this.refs.codeEditor.codeMirror.setSize(null, paneHeight);
|
||||
this.codeEditor.current.codeMirror.setSize(null, paneHeight);
|
||||
}
|
||||
},
|
||||
|
||||
handleInject : function(injectText){
|
||||
this.refs.codeEditor?.injectText(injectText, false);
|
||||
this.codeEditor.current?.injectText(injectText, false);
|
||||
},
|
||||
|
||||
handleViewChange : function(newView){
|
||||
@@ -98,7 +117,7 @@ const Editor = createClass({
|
||||
},
|
||||
|
||||
getCurrentPage : function(){
|
||||
const lines = this.props.brew.text.split('\n').slice(0, this.refs.codeEditor.getCursorPosition().line + 1);
|
||||
const lines = this.props.brew.text.split('\n').slice(0, this.codeEditor.current.getCursorPosition().line + 1);
|
||||
return _.reduce(lines, (r, line)=>{
|
||||
if(
|
||||
(this.props.renderer == 'legacy' && line.indexOf('\\page') !== -1)
|
||||
@@ -110,13 +129,24 @@ const Editor = createClass({
|
||||
},
|
||||
|
||||
highlightCustomMarkdown : function(){
|
||||
if(!this.refs.codeEditor) return;
|
||||
if(!this.codeEditor.current) return;
|
||||
if(this.state.view === 'text') {
|
||||
const codeMirror = this.refs.codeEditor.codeMirror;
|
||||
const codeMirror = this.codeEditor.current.codeMirror;
|
||||
|
||||
codeMirror.operation(()=>{ // Batch CodeMirror styling
|
||||
|
||||
const foldLines = [];
|
||||
|
||||
//reset custom text styles
|
||||
const customHighlights = codeMirror.getAllMarks().filter((mark)=>!mark.__isFold); //Don't undo code folding
|
||||
const customHighlights = codeMirror.getAllMarks().filter((mark)=>{
|
||||
// Record details of folded sections
|
||||
if(mark.__isFold) {
|
||||
const fold = mark.find();
|
||||
foldLines.push({from: fold.from?.line, to: fold.to?.line});
|
||||
}
|
||||
return !mark.__isFold;
|
||||
}); //Don't undo code folding
|
||||
|
||||
for (let i=customHighlights.length - 1;i>=0;i--) customHighlights[i].clear();
|
||||
|
||||
let editorPageCount = 2; // start page count from page 2
|
||||
@@ -128,6 +158,11 @@ const Editor = createClass({
|
||||
codeMirror.removeLineClass(lineNumber, 'text');
|
||||
codeMirror.removeLineClass(lineNumber, 'wrap', 'sourceMoveFlash');
|
||||
|
||||
// Don't process lines inside folded text
|
||||
// If the current lineNumber is inside any folded marks, skip line styling
|
||||
if (foldLines.some(fold => lineNumber >= fold.from && lineNumber <= fold.to))
|
||||
return;
|
||||
|
||||
// Styling for \page breaks
|
||||
if((this.props.renderer == 'legacy' && line.includes('\\page')) ||
|
||||
(this.props.renderer == 'V3' && line.match(/^\\page$/))) {
|
||||
@@ -219,6 +254,34 @@ const Editor = createClass({
|
||||
endCh = match.index+match[0].length;
|
||||
codeMirror.markText({ line: lineNumber, ch: 0 }, { line: lineNumber, ch: endCh }, { className: 'block' });
|
||||
}
|
||||
|
||||
// Emojis
|
||||
if(line.match(/:[^\s:]+:/g)) {
|
||||
let startIndex = line.indexOf(':');
|
||||
const emojiRegex = /:[^\s:]+:/gy;
|
||||
|
||||
while (startIndex >= 0) {
|
||||
emojiRegex.lastIndex = startIndex;
|
||||
let match = emojiRegex.exec(line);
|
||||
if (match) {
|
||||
let tokens = Markdown.marked.lexer(match[0]);
|
||||
tokens = tokens[0].tokens.filter(t => t.type == 'emoji')
|
||||
if (!tokens.length)
|
||||
return;
|
||||
|
||||
let startPos = { line: lineNumber, ch: match.index };
|
||||
let endPos = { line: lineNumber, ch: match.index + match[0].length };
|
||||
|
||||
// Iterate over conflicting marks and clear them
|
||||
var marks = codeMirror.findMarks(startPos, endPos);
|
||||
marks.forEach(function(marker) {
|
||||
if(!marker.__isFold) marker.clear();
|
||||
});
|
||||
codeMirror.markText(startPos, endPos, { className: 'emoji' });
|
||||
}
|
||||
startIndex = line.indexOf(':', Math.max(startIndex + 1, emojiRegex.lastIndex));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -273,23 +336,23 @@ const Editor = createClass({
|
||||
|
||||
targetLine = lineCount - 1; //Scroll to `\page`, which is one line back.
|
||||
|
||||
let currentY = this.refs.codeEditor.codeMirror.getScrollInfo().top;
|
||||
let targetY = this.refs.codeEditor.codeMirror.heightAtLine(targetLine, 'local', true);
|
||||
let currentY = this.codeEditor.current.codeMirror.getScrollInfo().top;
|
||||
let targetY = this.codeEditor.current.codeMirror.heightAtLine(targetLine, 'local', true);
|
||||
|
||||
//Scroll 1/10 of the way every 10ms until 1px off.
|
||||
const incrementalScroll = setInterval(()=>{
|
||||
currentY += (targetY - currentY) / 10;
|
||||
this.refs.codeEditor.codeMirror.scrollTo(null, currentY);
|
||||
this.codeEditor.current.codeMirror.scrollTo(null, currentY);
|
||||
|
||||
// Update target: target height is not accurate until within +-10 lines of the visible window
|
||||
if(Math.abs(targetY - currentY > 100))
|
||||
targetY = this.refs.codeEditor.codeMirror.heightAtLine(targetLine, 'local', true);
|
||||
targetY = this.codeEditor.current.codeMirror.heightAtLine(targetLine, 'local', true);
|
||||
|
||||
// End when close enough
|
||||
if(Math.abs(targetY - currentY) < 1) {
|
||||
this.refs.codeEditor.codeMirror.scrollTo(null, targetY); // Scroll any remaining difference
|
||||
this.refs.codeEditor.setCursorPosition({ line: targetLine + 1, ch: 0 });
|
||||
this.refs.codeEditor.codeMirror.addLineClass(targetLine + 1, 'wrap', 'sourceMoveFlash');
|
||||
this.codeEditor.current.codeMirror.scrollTo(null, targetY); // Scroll any remaining difference
|
||||
this.codeEditor.current.setCursorPosition({ line: targetLine + 1, ch: 0 });
|
||||
this.codeEditor.current.codeMirror.addLineClass(targetLine + 1, 'wrap', 'sourceMoveFlash');
|
||||
clearInterval(incrementalScroll);
|
||||
}
|
||||
}, 10);
|
||||
@@ -299,7 +362,7 @@ const Editor = createClass({
|
||||
|
||||
//Called when there are changes to the editor's dimensions
|
||||
update : function(){
|
||||
this.refs.codeEditor?.updateSize();
|
||||
this.codeEditor.current?.updateSize();
|
||||
},
|
||||
|
||||
updateEditorTheme : function(newTheme){
|
||||
@@ -318,7 +381,7 @@ const Editor = createClass({
|
||||
if(this.isText()){
|
||||
return <>
|
||||
<CodeEditor key='codeEditor'
|
||||
ref='codeEditor'
|
||||
ref={this.codeEditor}
|
||||
language='gfm'
|
||||
view={this.state.view}
|
||||
value={this.props.brew.text}
|
||||
@@ -330,12 +393,12 @@ const Editor = createClass({
|
||||
if(this.isStyle()){
|
||||
return <>
|
||||
<CodeEditor key='codeEditor'
|
||||
ref='codeEditor'
|
||||
ref={this.codeEditor}
|
||||
language='css'
|
||||
view={this.state.view}
|
||||
value={this.props.brew.style ?? DEFAULT_STYLE_TEXT}
|
||||
onChange={this.props.onStyleChange}
|
||||
enableFolding={false}
|
||||
enableFolding={true}
|
||||
editorTheme={this.state.editorTheme}
|
||||
rerenderParent={this.rerenderParent} />
|
||||
</>;
|
||||
@@ -349,34 +412,35 @@ const Editor = createClass({
|
||||
<MetadataEditor
|
||||
metadata={this.props.brew}
|
||||
onChange={this.props.onMetaChange}
|
||||
reportError={this.props.reportError}/>
|
||||
reportError={this.props.reportError}
|
||||
userThemes={this.props.userThemes}/>
|
||||
</>;
|
||||
}
|
||||
},
|
||||
|
||||
redo : function(){
|
||||
return this.refs.codeEditor?.redo();
|
||||
return this.codeEditor.current?.redo();
|
||||
},
|
||||
|
||||
historySize : function(){
|
||||
return this.refs.codeEditor?.historySize();
|
||||
return this.codeEditor.current?.historySize();
|
||||
},
|
||||
|
||||
undo : function(){
|
||||
return this.refs.codeEditor?.undo();
|
||||
return this.codeEditor.current?.undo();
|
||||
},
|
||||
|
||||
foldCode : function(){
|
||||
return this.refs.codeEditor?.foldAllCode();
|
||||
return this.codeEditor.current?.foldAllCode();
|
||||
},
|
||||
|
||||
unfoldCode : function(){
|
||||
return this.refs.codeEditor?.unfoldAllCode();
|
||||
return this.codeEditor.current?.unfoldAllCode();
|
||||
},
|
||||
|
||||
render : function(){
|
||||
return (
|
||||
<div className='editor' ref='main'>
|
||||
<div className='editor' ref={this.editor}>
|
||||
<SnippetBar
|
||||
brew={this.props.brew}
|
||||
view={this.state.view}
|
||||
@@ -392,7 +456,8 @@ const Editor = createClass({
|
||||
historySize={this.historySize()}
|
||||
currentEditorTheme={this.state.editorTheme}
|
||||
updateEditorTheme={this.updateEditorTheme}
|
||||
cursorPos={this.refs.codeEditor?.getCursorPosition() || {}} />
|
||||
snippetBundle={this.props.snippetBundle}
|
||||
cursorPos={this.codeEditor.current?.getCursorPosition() || {}} />
|
||||
|
||||
{this.renderEditor()}
|
||||
</div>
|
||||
|
||||
@@ -43,6 +43,16 @@
|
||||
font-weight : bold;
|
||||
color : green;
|
||||
}
|
||||
.emoji:not(.cm-comment) {
|
||||
margin-left : 2px;
|
||||
color : #360034;
|
||||
background : #ffc8ff;
|
||||
border-radius : 6px;
|
||||
font-weight : bold;
|
||||
padding-bottom : 1px;
|
||||
outline-offset : -2px;
|
||||
outline : solid 2px #ff96fc;
|
||||
}
|
||||
.superscript:not(.cm-comment) {
|
||||
font-weight : bold;
|
||||
color : goldenrod;
|
||||
|
||||
@@ -3,12 +3,12 @@ require('./metadataEditor.less');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
const cx = require('classnames');
|
||||
const request = require('../../utils/request-middleware.js');
|
||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||
const Combobox = require('client/components/combobox.jsx');
|
||||
const StringArrayEditor = require('../stringArrayEditor/stringArrayEditor.jsx');
|
||||
|
||||
|
||||
const Themes = require('themes/themes.json');
|
||||
const validations = require('./validations.js');
|
||||
|
||||
@@ -28,6 +28,7 @@ const MetadataEditor = createClass({
|
||||
return {
|
||||
metadata : {
|
||||
editId : null,
|
||||
shareId : null,
|
||||
title : '',
|
||||
description : '',
|
||||
thumbnail : '',
|
||||
@@ -99,7 +100,7 @@ const MetadataEditor = createClass({
|
||||
if(renderer == 'legacy')
|
||||
this.props.metadata.theme = '5ePHB';
|
||||
}
|
||||
this.props.onChange(this.props.metadata);
|
||||
this.props.onChange(this.props.metadata, 'renderer');
|
||||
},
|
||||
handlePublish : function(val){
|
||||
this.props.onChange({
|
||||
@@ -111,7 +112,7 @@ const MetadataEditor = createClass({
|
||||
handleTheme : function(theme){
|
||||
this.props.metadata.renderer = theme.renderer;
|
||||
this.props.metadata.theme = theme.path;
|
||||
this.props.onChange(this.props.metadata);
|
||||
this.props.onChange(this.props.metadata, 'theme');
|
||||
},
|
||||
|
||||
handleLanguage : function(languageCode){
|
||||
@@ -192,37 +193,42 @@ const MetadataEditor = createClass({
|
||||
renderThemeDropdown : function(){
|
||||
if(!global.enable_themes) return;
|
||||
|
||||
const mergedThemes = _.merge(Themes, this.props.userThemes);
|
||||
|
||||
const listThemes = (renderer)=>{
|
||||
return _.map(_.values(Themes[renderer]), (theme)=>{
|
||||
return <div className='item' key={''} onClick={()=>this.handleTheme(theme)} title={''}>
|
||||
{`${theme.renderer} : ${theme.name}`}
|
||||
<img src={`/themes/${theme.renderer}/${theme.path}/dropdownTexture.png`}/>
|
||||
return _.map(_.values(mergedThemes[renderer]), (theme)=>{
|
||||
if(theme.path == this.props.metadata.shareId) return;
|
||||
const preview = theme.thumbnail || `/themes/${theme.renderer}/${theme.path}/dropdownPreview.png`;
|
||||
const texture = theme.thumbnail || `/themes/${theme.renderer}/${theme.path}/dropdownTexture.png`;
|
||||
return <div className='item' key={`${renderer}_${theme.name}`} onClick={()=>this.handleTheme(theme)} title={''}>
|
||||
{theme.author ?? renderer} : {theme.name}
|
||||
<div className='texture-container'>
|
||||
<img src={texture}/>
|
||||
</div>
|
||||
<div className='preview'>
|
||||
<h6>{`${theme.name}`} preview</h6>
|
||||
<img src={`/themes/${theme.renderer}/${theme.path}/dropdownPreview.png`}/>
|
||||
<h6>{theme.name} preview</h6>
|
||||
<img src={preview}/>
|
||||
</div>
|
||||
</div>;
|
||||
});
|
||||
};
|
||||
|
||||
const currentTheme = Themes[`${_.upperFirst(this.props.metadata.renderer)}`][this.props.metadata.theme];
|
||||
const currentRenderer = this.props.metadata.renderer;
|
||||
const currentTheme = mergedThemes[`${_.upperFirst(this.props.metadata.renderer)}`][this.props.metadata.theme]
|
||||
?? { name: `!!! THEME MISSING !!! ID=${this.props.metadata.theme}` };
|
||||
let dropdown;
|
||||
|
||||
if(this.props.metadata.renderer == 'legacy') {
|
||||
if(currentRenderer == 'legacy') {
|
||||
dropdown =
|
||||
<Nav.dropdown className='disabled value' trigger='disabled'>
|
||||
<div>
|
||||
{`Themes are not supported in the Legacy Renderer`} <i className='fas fa-caret-down'></i>
|
||||
</div>
|
||||
<div> {`Themes are not supported in the Legacy Renderer`} <i className='fas fa-caret-down'></i> </div>
|
||||
</Nav.dropdown>;
|
||||
} else {
|
||||
dropdown =
|
||||
<Nav.dropdown className='value' trigger='click'>
|
||||
<div>
|
||||
{`${_.upperFirst(currentTheme.renderer)} : ${currentTheme.name}`} <i className='fas fa-caret-down'></i>
|
||||
</div>
|
||||
{/*listThemes('Legacy')*/}
|
||||
{listThemes('V3')}
|
||||
<div> {currentTheme.author ?? _.upperFirst(currentRenderer)} : {currentTheme.name} <i className='fas fa-caret-down'></i> </div>
|
||||
|
||||
{listThemes(currentRenderer)}
|
||||
</Nav.dropdown>;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,327 +1,309 @@
|
||||
@import 'naturalcrit/styles/colors.less';
|
||||
|
||||
.metadataEditor{
|
||||
.metadataEditor {
|
||||
position : absolute;
|
||||
z-index : 5;
|
||||
box-sizing : border-box;
|
||||
width : 100%;
|
||||
padding : 25px;
|
||||
background-color : #999;
|
||||
height : calc(100vh - 54px); // 54px is the height of the navbar + snippet bar. probably a better way to dynamic get this.
|
||||
padding : 25px;
|
||||
overflow-y : auto;
|
||||
background-color : #999999;
|
||||
|
||||
.sectionHead {
|
||||
font-weight: 1000;
|
||||
margin: 20px 0;
|
||||
margin : 20px 0;
|
||||
font-weight : 1000;
|
||||
|
||||
&:first-of-type {
|
||||
margin-top: 0;
|
||||
}
|
||||
&:first-of-type { margin-top : 0; }
|
||||
}
|
||||
|
||||
& > div {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
& > div { margin-bottom : 10px; }
|
||||
|
||||
.field-group {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
display : flex;
|
||||
flex-wrap : wrap;
|
||||
gap : 10px;
|
||||
width : 100%;
|
||||
}
|
||||
|
||||
.field-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 5 0 200px;
|
||||
gap: 10px;
|
||||
display : flex;
|
||||
flex : 5 0 200px;
|
||||
flex-direction : column;
|
||||
gap : 10px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.field{
|
||||
.field {
|
||||
position : relative;
|
||||
display : flex;
|
||||
flex-wrap : wrap;
|
||||
width : 100%;
|
||||
min-width : 200px;
|
||||
position : relative;
|
||||
&>label{
|
||||
& > label {
|
||||
width : 80px;
|
||||
font-size : 11px;
|
||||
font-weight : 800;
|
||||
line-height : 1.8em;
|
||||
text-transform : uppercase;
|
||||
}
|
||||
&>.value{
|
||||
& > .value {
|
||||
flex : 1 1 auto;
|
||||
width : 50px;
|
||||
&:invalid {
|
||||
background : #ffb9b9;
|
||||
}
|
||||
&:invalid { background : #FFB9B9; }
|
||||
}
|
||||
input[type='text'], textarea {
|
||||
border : 1px solid gray;
|
||||
&:focus {
|
||||
outline: 1px solid #444;
|
||||
}
|
||||
&:focus { outline : 1px solid #444444; }
|
||||
}
|
||||
&.thumbnail{
|
||||
&.thumbnail {
|
||||
height : 1.4em;
|
||||
label{
|
||||
line-height: 2.0em;
|
||||
label { line-height : 2.0em; }
|
||||
.value {
|
||||
overflow : hidden;
|
||||
text-overflow : ellipsis;
|
||||
}
|
||||
.value{
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
button{
|
||||
border: 1px solid #999;
|
||||
color: white;
|
||||
padding: 0px 5px;
|
||||
background-color: black;
|
||||
&:hover{
|
||||
background-color: #777;
|
||||
}
|
||||
button {
|
||||
padding : 0px 5px;
|
||||
color : white;
|
||||
background-color : black;
|
||||
border : 1px solid #999999;
|
||||
&:hover { background-color : #777777; }
|
||||
}
|
||||
}
|
||||
|
||||
&.description {
|
||||
flex: 1;
|
||||
flex : 1;
|
||||
textarea.value {
|
||||
resize : none;
|
||||
height : auto;
|
||||
font-family : 'Open Sans', sans-serif;
|
||||
font-size : 0.8em;
|
||||
resize : none;
|
||||
}
|
||||
}
|
||||
|
||||
&.language .language-dropdown {
|
||||
max-width : 150px;
|
||||
z-index : 200;
|
||||
max-width : 150px;
|
||||
}
|
||||
small {
|
||||
display : inline-block;
|
||||
font-size : 0.6em;
|
||||
font-style : italic;
|
||||
line-height : 1.4em;
|
||||
display : inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.thumbnail-preview {
|
||||
position: relative;
|
||||
justify-self: center;
|
||||
width: 80px;
|
||||
height: min-content;
|
||||
flex: 1 1;
|
||||
max-height: 115px;
|
||||
aspect-ratio: 1 / 1;
|
||||
object-fit: contain;
|
||||
background-color: #AAA;
|
||||
position : relative;
|
||||
flex : 1 1;
|
||||
justify-self : center;
|
||||
width : 80px;
|
||||
height : min-content;
|
||||
max-height : 115px;
|
||||
aspect-ratio : 1 / 1;
|
||||
object-fit : contain;
|
||||
background-color : #AAAAAA;
|
||||
}
|
||||
|
||||
.systems.field .value{
|
||||
label{
|
||||
vertical-align : middle;
|
||||
margin-right : 15px;
|
||||
cursor : pointer;
|
||||
font-size : 0.7em;
|
||||
font-weight : 800;
|
||||
user-select : none;
|
||||
white-space : nowrap;
|
||||
.systems.field .value {
|
||||
label {
|
||||
display : inline-flex;
|
||||
align-items : center;
|
||||
}
|
||||
a {
|
||||
font-size : 0.7em;
|
||||
font-weight : 800;
|
||||
display : inline-flex;
|
||||
}
|
||||
input{
|
||||
margin-right : 15px;
|
||||
font-size : 0.7em;
|
||||
font-weight : 800;
|
||||
white-space : nowrap;
|
||||
vertical-align : middle;
|
||||
cursor : pointer;
|
||||
user-select : none;
|
||||
}
|
||||
a {
|
||||
display : inline-flex;
|
||||
font-size : 0.7em;
|
||||
font-weight : 800;
|
||||
}
|
||||
input {
|
||||
margin : 3px;
|
||||
vertical-align : middle;
|
||||
cursor : pointer;
|
||||
}
|
||||
}
|
||||
.publish.field .value{
|
||||
position : relative;
|
||||
margin-bottom: 15px;
|
||||
button{
|
||||
width:100%;
|
||||
}
|
||||
button.publish{
|
||||
.publish.field .value {
|
||||
position : relative;
|
||||
margin-bottom : 15px;
|
||||
button { width : 100%; }
|
||||
button.publish {
|
||||
.button(@blueLight);
|
||||
}
|
||||
button.unpublish{
|
||||
button.unpublish {
|
||||
.button(@silver);
|
||||
}
|
||||
}
|
||||
|
||||
.delete.field .value{
|
||||
button{
|
||||
.delete.field .value {
|
||||
button {
|
||||
.button(@red);
|
||||
}
|
||||
}
|
||||
.authors.field .value{
|
||||
font-size: 0.8em;
|
||||
.authors.field .value {
|
||||
font-size : 0.8em;
|
||||
line-height : 1.5em;
|
||||
}
|
||||
|
||||
.themes.field{
|
||||
.themes.field {
|
||||
font-size : 13.33px;
|
||||
.navDropdownContainer {
|
||||
background-color : white;
|
||||
position : relative;
|
||||
z-index : 100;
|
||||
position : relative;
|
||||
z-index : 100;
|
||||
background-color : white;
|
||||
&.disabled {
|
||||
font-style :italic;
|
||||
font-style : italic;
|
||||
background-color : darkgray;
|
||||
color : dimgray;
|
||||
font-style : italic;
|
||||
color : dimgray;
|
||||
background-color : darkgray;
|
||||
}
|
||||
&>div:first-child {
|
||||
border : 2px solid rgb(118,118,118);
|
||||
padding : 6px 3px;
|
||||
background-color : inherit;
|
||||
i {
|
||||
float : right;
|
||||
}
|
||||
& > div:first-child {
|
||||
padding : 6px 3px;
|
||||
background-color : inherit;
|
||||
border : 2px solid rgb(118,118,118);
|
||||
i { float : right; }
|
||||
&:hover {
|
||||
background-color : @blue;
|
||||
color : white;
|
||||
color : white;
|
||||
background-color : @blue;
|
||||
}
|
||||
}
|
||||
.navDropdown .item > p {
|
||||
width : 45%;
|
||||
height : 1.1em;
|
||||
overflow : hidden;
|
||||
text-overflow : ellipsis;
|
||||
white-space : nowrap;
|
||||
}
|
||||
.navDropdown {
|
||||
box-shadow : 0px 5px 10px rgba(0, 0, 0, 0.3);
|
||||
position : absolute;
|
||||
width : 100%;
|
||||
box-shadow : 0px 5px 10px rgba(0, 0, 0, 0.3);
|
||||
.item {
|
||||
padding : 3px 3px;
|
||||
border-top : 1px solid rgb(118, 118, 118);
|
||||
position : relative;
|
||||
padding : 3px 3px;
|
||||
overflow : visible;
|
||||
background-color : white;
|
||||
background-color : white;
|
||||
border-top : 1px solid rgb(118, 118, 118);
|
||||
.preview {
|
||||
display : flex;
|
||||
flex-direction: column;
|
||||
background : #ccc;
|
||||
border-radius : 5px;
|
||||
box-shadow : 0 0 5px black;
|
||||
width : 200px;
|
||||
color :black;
|
||||
position : absolute;
|
||||
top : 0;
|
||||
right : 0;
|
||||
opacity : 0;
|
||||
transition : opacity 250ms ease;
|
||||
z-index : 1;
|
||||
overflow :hidden;
|
||||
position : absolute;
|
||||
top : 0;
|
||||
right : 0;
|
||||
z-index : 1;
|
||||
display : flex;
|
||||
flex-direction : column;
|
||||
width : 200px;
|
||||
overflow : hidden;
|
||||
color : black;
|
||||
background : #CCCCCC;
|
||||
border-radius : 5px;
|
||||
box-shadow : 0 0 5px black;
|
||||
opacity : 0;
|
||||
transition : opacity 250ms ease;
|
||||
h6 {
|
||||
font-weight : 900;
|
||||
padding-inline:1em;
|
||||
padding-block :.5em;
|
||||
border-bottom :2px solid hsl(0,0%,40%);
|
||||
padding-block : 0.5em;
|
||||
padding-inline : 1em;
|
||||
font-weight : 900;
|
||||
border-bottom : 2px solid hsl(0,0%,40%);
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
background-color : @blue;
|
||||
color : white;
|
||||
color : white;
|
||||
background-color : @blue;
|
||||
}
|
||||
&:hover > .preview {
|
||||
opacity: 1;
|
||||
}
|
||||
>img {
|
||||
mask-image : linear-gradient(90deg, transparent, black 20%);
|
||||
-webkit-mask-image : linear-gradient(90deg, transparent, black 20%);
|
||||
position : absolute;
|
||||
right : 0;
|
||||
top : 0px;
|
||||
width : 50%;
|
||||
height : 100%;
|
||||
&:hover > .preview { opacity : 1; }
|
||||
.texture-container {
|
||||
position : absolute;
|
||||
top : 0;
|
||||
left : 0;
|
||||
width : 100%;
|
||||
height : 100%;
|
||||
min-height : 100%;
|
||||
overflow : hidden;
|
||||
> img {
|
||||
position : absolute;
|
||||
top : 0px;
|
||||
right : 0;
|
||||
width : 50%;
|
||||
min-height : 100%;
|
||||
-webkit-mask-image : linear-gradient(90deg, transparent, black 20%);
|
||||
mask-image : linear-gradient(90deg, transparent, black 20%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.field .list {
|
||||
display: flex;
|
||||
flex: 1 0;
|
||||
flex-wrap: wrap;
|
||||
display : flex;
|
||||
flex : 1 0;
|
||||
flex-wrap : wrap;
|
||||
|
||||
> * {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
> * { flex : 0 0 auto; }
|
||||
|
||||
#groupedIcon {
|
||||
#backgroundColors;
|
||||
display: inline-block;
|
||||
height: ~"calc(100% + 0.6em)";
|
||||
position: relative;
|
||||
top: -0.3em;
|
||||
right: -0.3em;
|
||||
cursor: pointer;
|
||||
min-width: 20px;
|
||||
text-align: center;
|
||||
color: white;
|
||||
position : relative;
|
||||
top : -0.3em;
|
||||
right : -0.3em;
|
||||
display : inline-block;
|
||||
min-width : 20px;
|
||||
height : ~'calc(100% + 0.6em)';
|
||||
color : white;
|
||||
text-align : center;
|
||||
cursor : pointer;
|
||||
|
||||
i {
|
||||
position: relative;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
position : relative;
|
||||
top : 50%;
|
||||
transform : translateY(-50%);
|
||||
}
|
||||
|
||||
&:not(:last-child) {
|
||||
border-right: 1px solid black;
|
||||
}
|
||||
&:not(:last-child) { border-right : 1px solid black; }
|
||||
|
||||
&:last-child {
|
||||
border-radius: 0 0.5em 0.5em 0;
|
||||
}
|
||||
&:last-child { border-radius : 0 0.5em 0.5em 0; }
|
||||
}
|
||||
|
||||
.badge {
|
||||
background-color: #dddddd;
|
||||
border-radius: .5em;
|
||||
font-size: .9em;
|
||||
margin: 2px;
|
||||
padding: .3em;
|
||||
padding : 0.3em;
|
||||
margin : 2px;
|
||||
font-size : 0.9em;
|
||||
background-color : #DDDDDD;
|
||||
border-radius : 0.5em;
|
||||
|
||||
.icon {
|
||||
#groupedIcon
|
||||
}
|
||||
#groupedIcon; }
|
||||
}
|
||||
|
||||
.input-group {
|
||||
height: ~"calc(.9em + 4px + .6em)";
|
||||
height : ~'calc(.9em + 4px + .6em)';
|
||||
|
||||
input {
|
||||
border-radius: .5em 0 0 .5em;
|
||||
}
|
||||
input { border-radius : 0.5em 0 0 0.5em; }
|
||||
|
||||
input:last-child {
|
||||
border-radius: .5em;
|
||||
}
|
||||
input:last-child { border-radius : 0.5em; }
|
||||
|
||||
.value {
|
||||
width: 7.5vw;
|
||||
min-width: 75px;
|
||||
height: 100%;
|
||||
width : 7.5vw;
|
||||
min-width : 75px;
|
||||
height : 100%;
|
||||
}
|
||||
|
||||
.invalid:focus {
|
||||
background-color: pink;
|
||||
}
|
||||
.invalid:focus { background-color : pink; }
|
||||
|
||||
.icon {
|
||||
#groupedIcon;
|
||||
height: 97%;
|
||||
font-size: .8em;
|
||||
right: 1px;
|
||||
top: -.54em;
|
||||
top : -0.54em;
|
||||
right : 1px;
|
||||
height : 97%;
|
||||
font-size : 0.8em;
|
||||
|
||||
i {
|
||||
font-size: 1.125em;
|
||||
}
|
||||
i { font-size : 1.125em; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,6 @@ const _ = require('lodash');
|
||||
const cx = require('classnames');
|
||||
|
||||
//Import all themes
|
||||
|
||||
const Themes = require('themes/themes.json');
|
||||
|
||||
const ThemeSnippets = {};
|
||||
ThemeSnippets['Legacy_5ePHB'] = require('themes/Legacy/5ePHB/snippets.js');
|
||||
ThemeSnippets['V3_5ePHB'] = require('themes/V3/5ePHB/snippets.js');
|
||||
@@ -40,7 +37,8 @@ const Snippetbar = createClass({
|
||||
foldCode : ()=>{},
|
||||
unfoldCode : ()=>{},
|
||||
updateEditorTheme : ()=>{},
|
||||
cursorPos : {}
|
||||
cursorPos : {},
|
||||
snippetBundle : []
|
||||
};
|
||||
},
|
||||
|
||||
@@ -53,21 +51,15 @@ const Snippetbar = createClass({
|
||||
},
|
||||
|
||||
componentDidMount : async function() {
|
||||
const rendererPath = this.props.renderer == 'V3' ? 'V3' : 'Legacy';
|
||||
const themePath = this.props.theme ?? '5ePHB';
|
||||
let snippets = _.cloneDeep(ThemeSnippets[`${rendererPath}_${themePath}`]);
|
||||
snippets = this.compileSnippets(rendererPath, themePath, snippets);
|
||||
const snippets = this.compileSnippets();
|
||||
this.setState({
|
||||
snippets : snippets
|
||||
});
|
||||
},
|
||||
|
||||
componentDidUpdate : async function(prevProps) {
|
||||
if(prevProps.renderer != this.props.renderer || prevProps.theme != this.props.theme) {
|
||||
const rendererPath = this.props.renderer == 'V3' ? 'V3' : 'Legacy';
|
||||
const themePath = this.props.theme ?? '5ePHB';
|
||||
let snippets = _.cloneDeep(ThemeSnippets[`${rendererPath}_${themePath}`]);
|
||||
snippets = this.compileSnippets(rendererPath, themePath, snippets);
|
||||
if(prevProps.renderer != this.props.renderer || prevProps.theme != this.props.theme || prevProps.snippetBundle != this.props.snippetBundle) {
|
||||
const snippets = this.compileSnippets();
|
||||
this.setState({
|
||||
snippets : snippets
|
||||
});
|
||||
@@ -75,26 +67,26 @@ const Snippetbar = createClass({
|
||||
},
|
||||
|
||||
|
||||
mergeCustomizer : function(valueA, valueB, key) {
|
||||
mergeCustomizer : function(oldValue, newValue, key) {
|
||||
if(key == 'snippets') {
|
||||
const result = _.reverse(_.unionBy(_.reverse(valueB), _.reverse(valueA), 'name')); // Join snippets together, with preference for the current theme over the base theme
|
||||
const result = _.reverse(_.unionBy(_.reverse(newValue), _.reverse(oldValue), 'name')); // Join snippets together, with preference for the child theme over the parent theme
|
||||
return _.filter(result, 'gen'); //Only keep snippets with a 'gen' property.
|
||||
}
|
||||
},
|
||||
|
||||
compileSnippets : function(rendererPath, themePath, snippets) {
|
||||
let compiledSnippets = snippets;
|
||||
const baseSnippetsPath = Themes[rendererPath][themePath].baseSnippets;
|
||||
compileSnippets : function() {
|
||||
let compiledSnippets = [];
|
||||
|
||||
const objB = _.keyBy(compiledSnippets, 'groupName');
|
||||
let oldSnippets = _.keyBy(compiledSnippets, 'groupName');
|
||||
|
||||
if(baseSnippetsPath) {
|
||||
const objA = _.keyBy(_.cloneDeep(ThemeSnippets[`${rendererPath}_${baseSnippetsPath}`]), 'groupName');
|
||||
compiledSnippets = _.values(_.mergeWith(objA, objB, this.mergeCustomizer));
|
||||
compiledSnippets = this.compileSnippets(rendererPath, baseSnippetsPath, _.cloneDeep(compiledSnippets));
|
||||
} else {
|
||||
const objA = _.keyBy(_.cloneDeep(ThemeSnippets[`${rendererPath}_Blank`]), 'groupName');
|
||||
compiledSnippets = _.values(_.mergeWith(objA, objB, this.mergeCustomizer));
|
||||
for (let snippets of this.props.snippetBundle) {
|
||||
if(typeof(snippets) == 'string') // load staticThemes as needed; they were sent as just a file name
|
||||
snippets = ThemeSnippets[snippets];
|
||||
|
||||
const newSnippets = _.keyBy(_.cloneDeep(snippets), 'groupName');
|
||||
compiledSnippets = _.values(_.mergeWith(oldSnippets, newSnippets, this.mergeCustomizer));
|
||||
|
||||
oldSnippets = _.keyBy(compiledSnippets, 'groupName');
|
||||
}
|
||||
return compiledSnippets;
|
||||
},
|
||||
@@ -161,7 +153,7 @@ const Snippetbar = createClass({
|
||||
onClick={this.props.unfoldCode} >
|
||||
<i className='fas fa-expand-alt' />
|
||||
</div>
|
||||
</>
|
||||
</>;
|
||||
|
||||
}
|
||||
|
||||
@@ -181,7 +173,7 @@ const Snippetbar = createClass({
|
||||
<i className='fas fa-palette' />
|
||||
{this.state.themeSelector && this.renderThemeSelector()}
|
||||
</div>
|
||||
|
||||
|
||||
<div className='divider'></div>
|
||||
<div className={cx('text', { selected: this.props.view === 'text' })}
|
||||
onClick={()=>this.props.onViewChange('text')}>
|
||||
|
||||
@@ -10,7 +10,6 @@ const UserPage = require('./pages/userPage/userPage.jsx');
|
||||
const SharePage = require('./pages/sharePage/sharePage.jsx');
|
||||
const NewPage = require('./pages/newPage/newPage.jsx');
|
||||
const ErrorPage = require('./pages/errorPage/errorPage.jsx');
|
||||
const PrintPage = require('./pages/printPage/printPage.jsx');
|
||||
const AccountPage = require('./pages/accountPage/accountPage.jsx');
|
||||
|
||||
const WithRoute = (props)=>{
|
||||
@@ -67,20 +66,19 @@ const Homebrew = createClass({
|
||||
<Router location={this.props.url}>
|
||||
<div className='homebrew'>
|
||||
<Routes>
|
||||
<Route path='/edit/:id' element={<WithRoute el={EditPage} brew={this.props.brew} />} />
|
||||
<Route path='/edit/:id' element={<WithRoute el={EditPage} brew={this.props.brew} userThemes={this.props.userThemes}/>} />
|
||||
<Route path='/share/:id' element={<WithRoute el={SharePage} brew={this.props.brew} />} />
|
||||
<Route path='/new/:id' element={<WithRoute el={NewPage} brew={this.props.brew} />} />
|
||||
<Route path='/new' element={<WithRoute el={NewPage}/>} />
|
||||
<Route path='/new/:id' element={<WithRoute el={NewPage} brew={this.props.brew} userThemes={this.props.userThemes}/>} />
|
||||
<Route path='/new' element={<WithRoute el={NewPage} userThemes={this.props.userThemes}/> } />
|
||||
<Route path='/user/:username' element={<WithRoute el={UserPage} brews={this.props.brews} />} />
|
||||
<Route path='/print/:id' element={<WithRoute el={PrintPage} brew={this.props.brew} />} />
|
||||
<Route path='/print' element={<WithRoute el={PrintPage} />} />
|
||||
<Route path='/changelog' element={<WithRoute el={SharePage} brew={this.props.brew} />} />
|
||||
<Route path='/faq' element={<WithRoute el={SharePage} brew={this.props.brew} />} />
|
||||
<Route path='/changelog' element={<WithRoute el={SharePage} brew={this.props.brew} disableMeta={true} />} />
|
||||
<Route path='/faq' element={<WithRoute el={SharePage} brew={this.props.brew} disableMeta={true} />} />
|
||||
<Route path='/migrate' element={<WithRoute el={SharePage} brew={this.props.brew} disableMeta={true} />} />
|
||||
<Route path='/account' element={<WithRoute el={AccountPage} brew={this.props.brew} accountDetails={this.props.brew.accountDetails} />} />
|
||||
<Route path='/legacy' element={<WithRoute el={HomePage} brew={this.props.brew} />} />
|
||||
<Route path='/error' element={<WithRoute el={ErrorPage} brew={this.props.brew} />} />
|
||||
<Route path='/' element={<WithRoute el={HomePage} brew={this.props.brew} />} />
|
||||
<Route path='/*' element={<WithRoute el={HomePage} brew={this.props.brew} />} />
|
||||
<Route path='/' element={<WithRoute el={HomePage} brew={this.props.brew} />} />
|
||||
<Route path='/*' element={<WithRoute el={HomePage} brew={this.props.brew} />} />
|
||||
</Routes>
|
||||
</div>
|
||||
</Router>
|
||||
@@ -88,15 +86,4 @@ const Homebrew = createClass({
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = Homebrew;
|
||||
|
||||
//TODO: Nicer Error page instead of just "cant get that"
|
||||
// '/share/:id' : (args)=>{
|
||||
// if(!this.props.brew.shareId){
|
||||
// return <ErrorPage errorId={args.id}/>;
|
||||
// }
|
||||
//
|
||||
// return <SharePage
|
||||
// id={args.id}
|
||||
// brew={this.props.brew} />;
|
||||
// },
|
||||
module.exports = Homebrew;
|
||||
@@ -42,7 +42,7 @@ const ErrorNavItem = createClass({
|
||||
</div>
|
||||
</Nav.item>;
|
||||
}
|
||||
|
||||
|
||||
if(status === 412) {
|
||||
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||
Oops!
|
||||
@@ -51,7 +51,7 @@ const ErrorNavItem = createClass({
|
||||
</div>
|
||||
</Nav.item>;
|
||||
}
|
||||
|
||||
|
||||
if(HBErrorCode === '04') {
|
||||
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||
Oops!
|
||||
@@ -76,10 +76,10 @@ const ErrorNavItem = createClass({
|
||||
if(response.body?.errors?.[0].reason == 'storageQuotaExceeded') {
|
||||
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||
Oops!
|
||||
<div className='errorContainer' onClick={clearError}>
|
||||
<div className='errorContainer' onClick={clearError}>
|
||||
Can't save because your Google Drive seems to be full!
|
||||
</div>
|
||||
</Nav.item>;
|
||||
</div>
|
||||
</Nav.item>;
|
||||
}
|
||||
|
||||
if(response.req.url.match(/^\/api.*Google.*$/m)){
|
||||
@@ -104,6 +104,18 @@ const ErrorNavItem = createClass({
|
||||
</Nav.item>;
|
||||
}
|
||||
|
||||
if(HBErrorCode === '09') {
|
||||
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||
Oops!
|
||||
<div className='errorContainer' onClick={clearError}>
|
||||
Looks like there was a problem retreiving
|
||||
the theme, or a theme that it inherits,
|
||||
for this brew. Verify that brew <a className='lowercase' target='_blank' rel='noopener noreferrer' href={`/share/${response.body.brewId}`}>
|
||||
{response.body.brewId}</a> still exists!
|
||||
</div>
|
||||
</Nav.item>;
|
||||
}
|
||||
|
||||
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||
Oops!
|
||||
<div className='errorContainer'>
|
||||
|
||||
@@ -21,6 +21,9 @@
|
||||
font-size : 10px;
|
||||
font-weight : 800;
|
||||
text-transform : uppercase;
|
||||
.lowercase {
|
||||
text-transform : none;
|
||||
}
|
||||
a{
|
||||
color : @teal;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
const dedent = require('dedent-tabs').default;
|
||||
|
||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
const Moment = require('moment');
|
||||
|
||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
@import 'naturalcrit/styles/colors.less';
|
||||
|
||||
@navbarHeight : 28px;
|
||||
@viewerToolsHeight : 32px;
|
||||
|
||||
@keyframes pinkColoring {
|
||||
0% { color : pink; }
|
||||
|
||||
@@ -7,58 +7,58 @@ const BREWKEY = 'homebrewery-new';
|
||||
const STYLEKEY = 'homebrewery-new-style';
|
||||
const METAKEY = 'homebrewery-new-meta';
|
||||
|
||||
const NewBrew = () => {
|
||||
const handleFileChange = (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
const fileContent = e.target.result;
|
||||
const newBrew = {
|
||||
text: fileContent,
|
||||
style: ''
|
||||
};
|
||||
if(fileContent.startsWith('```metadata')) {
|
||||
splitTextStyleAndMetadata(newBrew); // Modify newBrew directly
|
||||
localStorage.setItem(BREWKEY, newBrew.text);
|
||||
localStorage.setItem(STYLEKEY, newBrew.style);
|
||||
localStorage.setItem(METAKEY, JSON.stringify(_.pick(newBrew, ['title', 'description', 'tags', 'systems', 'renderer', 'theme', 'lang'])));
|
||||
window.location.href = '/new';
|
||||
} else {
|
||||
alert('This file is invalid, please, enter a valid file');
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
}
|
||||
};
|
||||
const NewBrew = ()=>{
|
||||
const handleFileChange = (e)=>{
|
||||
const file = e.target.files[0];
|
||||
if(file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e)=>{
|
||||
const fileContent = e.target.result;
|
||||
const newBrew = {
|
||||
text : fileContent,
|
||||
style : ''
|
||||
};
|
||||
if(fileContent.startsWith('```metadata')) {
|
||||
splitTextStyleAndMetadata(newBrew); // Modify newBrew directly
|
||||
localStorage.setItem(BREWKEY, newBrew.text);
|
||||
localStorage.setItem(STYLEKEY, newBrew.style);
|
||||
localStorage.setItem(METAKEY, JSON.stringify(_.pick(newBrew, ['title', 'description', 'tags', 'systems', 'renderer', 'theme', 'lang'])));
|
||||
window.location.href = '/new';
|
||||
} else {
|
||||
alert('This file is invalid, please, enter a valid file');
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Nav.dropdown>
|
||||
<Nav.item
|
||||
className='new'
|
||||
color='purple'
|
||||
icon='fa-solid fa-plus-square'>
|
||||
return (
|
||||
<Nav.dropdown>
|
||||
<Nav.item
|
||||
className='new'
|
||||
color='purple'
|
||||
icon='fa-solid fa-plus-square'>
|
||||
new
|
||||
</Nav.item>
|
||||
<Nav.item
|
||||
className='fromBlank'
|
||||
href='/new'
|
||||
newTab={true}
|
||||
color='purple'
|
||||
icon='fa-solid fa-file'>
|
||||
</Nav.item>
|
||||
<Nav.item
|
||||
className='fromBlank'
|
||||
href='/new'
|
||||
newTab={true}
|
||||
color='purple'
|
||||
icon='fa-solid fa-file'>
|
||||
from blank
|
||||
</Nav.item>
|
||||
</Nav.item>
|
||||
|
||||
<Nav.item
|
||||
className='fromFile'
|
||||
color='purple'
|
||||
icon='fa-solid fa-upload'
|
||||
onClick={() => { document.getElementById('uploadTxt').click(); }}>
|
||||
<input id="uploadTxt" className='newFromLocal' type="file" onChange={handleFileChange} style={{ display: 'none' }} />
|
||||
<Nav.item
|
||||
className='fromFile'
|
||||
color='purple'
|
||||
icon='fa-solid fa-upload'
|
||||
onClick={()=>{ document.getElementById('uploadTxt').click(); }}>
|
||||
<input id='uploadTxt' className='newFromLocal' type='file' onChange={handleFileChange} style={{ display: 'none' }} />
|
||||
from file
|
||||
</Nav.item>
|
||||
</Nav.dropdown>
|
||||
);
|
||||
</Nav.item>
|
||||
</Nav.dropdown>
|
||||
);
|
||||
};
|
||||
|
||||
module.exports = NewBrew;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||
const { printCurrentBrew } = require('../../../shared/helpers.js');
|
||||
|
||||
module.exports = function(props){
|
||||
return <Nav.item newTab={true} href={`/print/${props.shareId}?dialog=true`} color='purple' icon='far fa-file-pdf'>
|
||||
module.exports = function(){
|
||||
return <Nav.item onClick={printCurrentBrew} color='purple' icon='far fa-file-pdf'>
|
||||
get PDF
|
||||
</Nav.item>;
|
||||
};
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const cx = require('classnames');
|
||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||
|
||||
const MAX_URL_SIZE = 2083;
|
||||
const MAIN_URL = 'https://www.reddit.com/r/UnearthedArcana/submit?selftext=true';
|
||||
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
require('./brewItem.less');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
const cx = require('classnames');
|
||||
const moment = require('moment');
|
||||
const request = require('../../../../utils/request-middleware.js');
|
||||
|
||||
|
||||
@@ -119,11 +119,12 @@
|
||||
text-align : center;
|
||||
a{
|
||||
.animate(opacity);
|
||||
display : block;
|
||||
margin : 8px 0px;
|
||||
opacity : 0.6;
|
||||
font-size : 1.3em;
|
||||
color : white;
|
||||
display : block;
|
||||
margin : 8px 0px;
|
||||
opacity : 0.6;
|
||||
font-size : 1.3em;
|
||||
color : white;
|
||||
text-decoration : unset;
|
||||
&:hover{
|
||||
opacity : 1;
|
||||
}
|
||||
|
||||
@@ -262,8 +262,8 @@ const ListPage = createClass({
|
||||
render : function(){
|
||||
return <div className='listPage sitePage'>
|
||||
{/*<style>@layer V3_5ePHB, bundle;</style>*/}
|
||||
<link href='/themes/V3/Blank/style.css' type="text/css" rel='stylesheet'/>
|
||||
<link href='/themes/V3/5ePHB/style.css' type="text/css" rel='stylesheet'/>
|
||||
<link href='/themes/V3/Blank/style.css' type='text/css' rel='stylesheet'/>
|
||||
<link href='/themes/V3/5ePHB/style.css' type='text/css' rel='stylesheet'/>
|
||||
{this.props.navItems}
|
||||
{this.renderSortOptions()}
|
||||
{this.renderTagsOptions()}
|
||||
|
||||
@@ -11,7 +11,7 @@ const Navbar = require('../../navbar/navbar.jsx');
|
||||
|
||||
const NewBrew = require('../../navbar/newbrew.navitem.jsx');
|
||||
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
||||
const PrintLink = require('../../navbar/print.navitem.jsx');
|
||||
const PrintNavItem = require('../../navbar/print.navitem.jsx');
|
||||
const ErrorNavItem = require('../../navbar/error-navitem.jsx');
|
||||
const Account = require('../../navbar/account.navitem.jsx');
|
||||
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
||||
@@ -20,9 +20,12 @@ const SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
|
||||
const Editor = require('../../editor/editor.jsx');
|
||||
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
||||
|
||||
const LockNotification = require('./lockNotification/lockNotification.jsx');
|
||||
|
||||
const Markdown = require('naturalcrit/markdown.js');
|
||||
|
||||
const { DEFAULT_BREW_LOAD } = require('../../../../server/brewDefaults.js');
|
||||
const { printCurrentBrew, fetchThemeBundle } = require('../../../../shared/helpers.js');
|
||||
|
||||
const googleDriveIcon = require('../../googleDrive.svg');
|
||||
|
||||
@@ -51,9 +54,13 @@ const EditPage = createClass({
|
||||
autoSave : true,
|
||||
autoSaveWarning : false,
|
||||
unsavedTime : new Date(),
|
||||
currentEditorPage : 0
|
||||
currentEditorPage : 0,
|
||||
displayLockMessage : this.props.brew.lock || false,
|
||||
themeBundle : {}
|
||||
};
|
||||
},
|
||||
|
||||
editor : React.createRef(null),
|
||||
savedBrew : null,
|
||||
|
||||
componentDidMount : function(){
|
||||
@@ -81,6 +88,8 @@ const EditPage = createClass({
|
||||
htmlErrors : Markdown.validate(prevState.brew.text)
|
||||
}));
|
||||
|
||||
fetchThemeBundle(this, this.props.brew.renderer, this.props.brew.theme);
|
||||
|
||||
document.addEventListener('keydown', this.handleControlKeys);
|
||||
},
|
||||
componentWillUnmount : function() {
|
||||
@@ -93,7 +102,7 @@ const EditPage = createClass({
|
||||
const S_KEY = 83;
|
||||
const P_KEY = 80;
|
||||
if(e.keyCode == S_KEY) this.trySave(true);
|
||||
if(e.keyCode == P_KEY) window.open(`/print/${this.processShareId()}?dialog=true`, '_blank').focus();
|
||||
if(e.keyCode == P_KEY) printCurrentBrew();
|
||||
if(e.keyCode == P_KEY || e.keyCode == S_KEY){
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
@@ -101,7 +110,7 @@ const EditPage = createClass({
|
||||
},
|
||||
|
||||
handleSplitMove : function(){
|
||||
this.refs.editor.update();
|
||||
this.editor.current.update();
|
||||
},
|
||||
|
||||
handleTextChange : function(text){
|
||||
@@ -113,7 +122,7 @@ const EditPage = createClass({
|
||||
brew : { ...prevState.brew, text: text },
|
||||
isPending : true,
|
||||
htmlErrors : htmlErrors,
|
||||
currentEditorPage : this.refs.editor.getCurrentPage() - 1 //Offset index since Marked starts pages at 0
|
||||
currentEditorPage : this.editor.current.getCurrentPage() - 1 //Offset index since Marked starts pages at 0
|
||||
}), ()=>{if(this.state.autoSave) this.trySave();});
|
||||
},
|
||||
|
||||
@@ -124,7 +133,10 @@ const EditPage = createClass({
|
||||
}), ()=>{if(this.state.autoSave) this.trySave();});
|
||||
},
|
||||
|
||||
handleMetaChange : function(metadata){
|
||||
handleMetaChange : function(metadata, field=undefined){
|
||||
if(field == 'theme' || field == 'renderer') // Fetch theme bundle only if theme or renderer was changed
|
||||
fetchThemeBundle(this, metadata.renderer, metadata.theme);
|
||||
|
||||
this.setState((prevState)=>({
|
||||
brew : {
|
||||
...prevState.brew,
|
||||
@@ -132,7 +144,6 @@ const EditPage = createClass({
|
||||
},
|
||||
isPending : true,
|
||||
}), ()=>{if(this.state.autoSave) this.trySave();});
|
||||
|
||||
},
|
||||
|
||||
hasChanges : function(){
|
||||
@@ -376,7 +387,7 @@ const EditPage = createClass({
|
||||
post to reddit
|
||||
</Nav.item>
|
||||
</Nav.dropdown>
|
||||
<PrintLink shareId={this.processShareId()} />
|
||||
<PrintNavItem />
|
||||
<RecentNavItem brew={this.state.brew} storageKey='edit' />
|
||||
<Account />
|
||||
</Nav.section>
|
||||
@@ -390,24 +401,29 @@ const EditPage = createClass({
|
||||
{this.renderNavbar()}
|
||||
|
||||
<div className='content'>
|
||||
<SplitPane onDragFinish={this.handleSplitMove} ref='pane'>
|
||||
{this.props.brew.lock && <LockNotification shareId={this.props.brew.shareId} message={this.props.brew.lock.editMessage} />}
|
||||
<SplitPane onDragFinish={this.handleSplitMove}>
|
||||
<Editor
|
||||
ref='editor'
|
||||
ref={this.editor}
|
||||
brew={this.state.brew}
|
||||
onTextChange={this.handleTextChange}
|
||||
onStyleChange={this.handleStyleChange}
|
||||
onMetaChange={this.handleMetaChange}
|
||||
reportError={this.errorReported}
|
||||
renderer={this.state.brew.renderer}
|
||||
userThemes={this.props.userThemes}
|
||||
snippetBundle={this.state.themeBundle.snippets}
|
||||
/>
|
||||
<BrewRenderer
|
||||
text={this.state.brew.text}
|
||||
style={this.state.brew.style}
|
||||
renderer={this.state.brew.renderer}
|
||||
theme={this.state.brew.theme}
|
||||
themeBundle={this.state.themeBundle}
|
||||
errors={this.state.htmlErrors}
|
||||
lang={this.state.brew.lang}
|
||||
currentEditorPage={this.state.currentEditorPage}
|
||||
allowPrint={true}
|
||||
/>
|
||||
</SplitPane>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
require('./lockNotification.less');
|
||||
const React = require('react');
|
||||
import Dialog from '../../../../components/dialog.jsx';
|
||||
|
||||
function LockNotification(props) {
|
||||
props = {
|
||||
shareId : 0,
|
||||
disableLock : ()=>{},
|
||||
message : '',
|
||||
...props
|
||||
};
|
||||
|
||||
const removeLock = ()=>{
|
||||
alert(`Not yet implemented - ID ${props.shareId}`);
|
||||
};
|
||||
|
||||
return <Dialog className='lockNotification' blocking closeText='CONTINUE TO EDITOR' >
|
||||
<h1>BREW LOCKED</h1>
|
||||
<p>This brew been locked by the Administrators. It will not be accessible by any method other than the Editor until the lock is removed.</p>
|
||||
<hr />
|
||||
<h3>LOCK REASON</h3>
|
||||
<p>{props.message || 'Unable to retrieve Lock Message'}</p>
|
||||
<hr />
|
||||
<p>Once you have resolved this issue, click REQUEST LOCK REMOVAL to notify the Administrators for review.</p>
|
||||
<p>Click CONTINUE TO EDITOR to temporarily hide this notification; it will reappear the next time the page is reloaded.</p>
|
||||
<button onClick={removeLock}>REQUEST LOCK REMOVAL</button>
|
||||
</Dialog>;
|
||||
};
|
||||
|
||||
module.exports = LockNotification;
|
||||
@@ -0,0 +1,27 @@
|
||||
.lockNotification {
|
||||
z-index : 1;
|
||||
width : 80%;
|
||||
padding : 10px;
|
||||
margin : 5% 10%;
|
||||
line-height : 1.5em;
|
||||
color : black;
|
||||
text-align : center;
|
||||
background-color : #CCCCCC;
|
||||
|
||||
&::backdrop { background-color : #000000AA; }
|
||||
|
||||
button {
|
||||
margin : 10px;
|
||||
color : white;
|
||||
background-color : #333333;
|
||||
|
||||
&:hover { background-color : #777777; }
|
||||
}
|
||||
|
||||
h1, h3 {
|
||||
font-family : 'Open Sans', sans-serif;
|
||||
font-weight : 800;
|
||||
}
|
||||
h1 { font-size : 24px; }
|
||||
h3 { font-size : 18px; }
|
||||
}
|
||||
@@ -1,41 +1,25 @@
|
||||
require('./errorPage.less');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
const cx = require('classnames');
|
||||
|
||||
const UIPage = require('../basePages/uiPage/uiPage.jsx');
|
||||
|
||||
const Markdown = require('../../../../shared/naturalcrit/markdown.js');
|
||||
|
||||
const React = require('react');
|
||||
const UIPage = require('../basePages/uiPage/uiPage.jsx');
|
||||
const Markdown = require('../../../../shared/naturalcrit/markdown.js');
|
||||
const ErrorIndex = require('./errors/errorIndex.js');
|
||||
|
||||
const ErrorPage = createClass({
|
||||
displayName : 'ErrorPage',
|
||||
const ErrorPage = ({ brew })=>{
|
||||
// Retrieving the error text based on the brew's error code from ErrorIndex
|
||||
const errorText = ErrorIndex({ brew })[brew.HBErrorCode.toString()] || '';
|
||||
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
ver : '0.0.0',
|
||||
errorId : '',
|
||||
text : '# Oops \n We could not find a brew with that id. **Sorry!**',
|
||||
error : {}
|
||||
};
|
||||
},
|
||||
|
||||
render : function(){
|
||||
const errorText = ErrorIndex(this.props)[this.props.brew.HBErrorCode.toString()] || '';
|
||||
|
||||
return <UIPage brew={{ title: 'Crit Fail!' }}>
|
||||
return (
|
||||
<UIPage brew={{ title: 'Crit Fail!' }}>
|
||||
<div className='dataGroup'>
|
||||
<div className='errorTitle'>
|
||||
<h1>{`Error ${this.props.brew.status || '000'}`}</h1>
|
||||
<h4>{this.props.brew.text || 'No error text'}</h4>
|
||||
<h1>{`Error ${brew?.status || '000'}`}</h1>
|
||||
<h4>{brew?.text || 'No error text'}</h4>
|
||||
</div>
|
||||
<hr />
|
||||
<div dangerouslySetInnerHTML={{ __html: Markdown.render(errorText) }} />
|
||||
</div>
|
||||
</UIPage>;
|
||||
}
|
||||
});
|
||||
</UIPage>
|
||||
);
|
||||
};
|
||||
|
||||
module.exports = ErrorPage;
|
||||
|
||||
@@ -95,7 +95,7 @@ const errorIndex = (props)=>{
|
||||
**Current Authors:** ${props.brew.authors?.map((author)=>{return `[${author}](/user/${author})`;}).join(', ') || 'Unable to list authors'}
|
||||
|
||||
[Click here to be redirected to the brew's share page.](/share/${props.brew.shareId})`,
|
||||
|
||||
|
||||
|
||||
// Brew load error
|
||||
'05' : dedent`
|
||||
@@ -136,11 +136,24 @@ const errorIndex = (props)=>{
|
||||
|
||||
**Brew ID:** ${props.brew.brewId}`,
|
||||
|
||||
// Theme load error
|
||||
'09' : dedent`
|
||||
## No Homebrewery theme document could be found.
|
||||
|
||||
The server could not locate the Homebrewery document. It was likely deleted by
|
||||
its owner.
|
||||
|
||||
:
|
||||
|
||||
**Requested access:** ${props.brew.accessType}
|
||||
|
||||
**Brew ID:** ${props.brew.brewId}`,
|
||||
|
||||
// Brew locked by Administrators error
|
||||
'100' : dedent`
|
||||
## This brew has been locked.
|
||||
|
||||
Please contact the Administrators to unlock this document.
|
||||
Only an author may request that this lock is removed.
|
||||
|
||||
:
|
||||
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
//TODO: Depricate
|
||||
|
||||
module.exports = function(shareId){
|
||||
return function(event){
|
||||
event = event || window.event;
|
||||
if((event.ctrlKey || event.metaKey) && event.keyCode == 80){
|
||||
const win = window.open(`/homebrew/print/${shareId}?dialog=true`, '_blank');
|
||||
win.focus();
|
||||
event.preventDefault();
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -13,6 +13,7 @@ const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
||||
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
||||
const AccountNavItem = require('../../navbar/account.navitem.jsx');
|
||||
const ErrorNavItem = require('../../navbar/error-navitem.jsx');
|
||||
const { fetchThemeBundle } = require('../../../../shared/helpers.js');
|
||||
|
||||
|
||||
const SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
|
||||
@@ -34,9 +35,17 @@ const HomePage = createClass({
|
||||
brew : this.props.brew,
|
||||
welcomeText : this.props.brew.text,
|
||||
error : undefined,
|
||||
currentEditorPage : 0
|
||||
currentEditorPage : 0,
|
||||
themeBundle : {}
|
||||
};
|
||||
},
|
||||
|
||||
editor : React.createRef(null),
|
||||
|
||||
componentDidMount : function() {
|
||||
fetchThemeBundle(this, this.props.brew.renderer, this.props.brew.theme);
|
||||
},
|
||||
|
||||
handleSave : function(){
|
||||
request.post('/api')
|
||||
.send(this.state.brew)
|
||||
@@ -50,12 +59,12 @@ const HomePage = createClass({
|
||||
});
|
||||
},
|
||||
handleSplitMove : function(){
|
||||
this.refs.editor.update();
|
||||
this.editor.current.update();
|
||||
},
|
||||
handleTextChange : function(text){
|
||||
this.setState((prevState)=>({
|
||||
brew : { ...prevState.brew, text: text },
|
||||
currentEditorPage : this.refs.editor.getCurrentPage() - 1 //Offset index since Marked starts pages at 0
|
||||
currentEditorPage : this.editor.current.getCurrentPage() - 1 //Offset index since Marked starts pages at 0
|
||||
}));
|
||||
},
|
||||
renderNavbar : function(){
|
||||
@@ -79,19 +88,21 @@ const HomePage = createClass({
|
||||
{this.renderNavbar()}
|
||||
|
||||
<div className='content'>
|
||||
<SplitPane onDragFinish={this.handleSplitMove} ref='pane'>
|
||||
<SplitPane onDragFinish={this.handleSplitMove}>
|
||||
<Editor
|
||||
ref='editor'
|
||||
ref={this.editor}
|
||||
brew={this.state.brew}
|
||||
onTextChange={this.handleTextChange}
|
||||
renderer={this.state.brew.renderer}
|
||||
showEditButtons={false}
|
||||
snippetBundle={this.state.themeBundle.snippets}
|
||||
/>
|
||||
<BrewRenderer
|
||||
text={this.state.brew.text}
|
||||
style={this.state.brew.style}
|
||||
renderer={this.state.brew.renderer}
|
||||
currentEditorPage={this.state.currentEditorPage}
|
||||
themeBundle={this.state.themeBundle}
|
||||
/>
|
||||
</SplitPane>
|
||||
</div>
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
require('./newPage.less');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
const request = require('../../utils/request-middleware.js');
|
||||
|
||||
const Markdown = require('naturalcrit/markdown.js');
|
||||
|
||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||
const PrintNavItem = require('../../navbar/print.navitem.jsx');
|
||||
const Navbar = require('../../navbar/navbar.jsx');
|
||||
const AccountNavItem = require('../../navbar/account.navitem.jsx');
|
||||
const ErrorNavItem = require('../../navbar/error-navitem.jsx');
|
||||
@@ -19,6 +19,7 @@ const Editor = require('../../editor/editor.jsx');
|
||||
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
||||
|
||||
const { DEFAULT_BREW } = require('../../../../server/brewDefaults.js');
|
||||
const { printCurrentBrew, fetchThemeBundle } = require('../../../../shared/helpers.js');
|
||||
|
||||
const BREWKEY = 'homebrewery-new';
|
||||
const STYLEKEY = 'homebrewery-new-style';
|
||||
@@ -43,10 +44,13 @@ const NewPage = createClass({
|
||||
saveGoogle : (global.account && global.account.googleId ? true : false),
|
||||
error : null,
|
||||
htmlErrors : Markdown.validate(brew.text),
|
||||
currentEditorPage : 0
|
||||
currentEditorPage : 0,
|
||||
themeBundle : {}
|
||||
};
|
||||
},
|
||||
|
||||
editor : React.createRef(null),
|
||||
|
||||
componentDidMount : function() {
|
||||
document.addEventListener('keydown', this.handleControlKeys);
|
||||
|
||||
@@ -74,10 +78,15 @@ const NewPage = createClass({
|
||||
saveGoogle : (saveStorage == 'GOOGLE-DRIVE' && this.state.saveGoogle)
|
||||
});
|
||||
|
||||
fetchThemeBundle(this, this.props.brew.renderer, this.props.brew.theme);
|
||||
|
||||
localStorage.setItem(BREWKEY, brew.text);
|
||||
if(brew.style)
|
||||
localStorage.setItem(STYLEKEY, brew.style);
|
||||
localStorage.setItem(METAKEY, JSON.stringify({ 'renderer': brew.renderer, 'theme': brew.theme, 'lang': brew.lang }));
|
||||
if(window.location.pathname != '/new') {
|
||||
window.history.replaceState({}, window.location.title, '/new/');
|
||||
}
|
||||
},
|
||||
componentWillUnmount : function() {
|
||||
document.removeEventListener('keydown', this.handleControlKeys);
|
||||
@@ -88,7 +97,7 @@ const NewPage = createClass({
|
||||
const S_KEY = 83;
|
||||
const P_KEY = 80;
|
||||
if(e.keyCode == S_KEY) this.save();
|
||||
if(e.keyCode == P_KEY) this.print();
|
||||
if(e.keyCode == P_KEY) printCurrentBrew();
|
||||
if(e.keyCode == P_KEY || e.keyCode == S_KEY){
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
@@ -96,7 +105,7 @@ const NewPage = createClass({
|
||||
},
|
||||
|
||||
handleSplitMove : function(){
|
||||
this.refs.editor.update();
|
||||
this.editor.current.update();
|
||||
},
|
||||
|
||||
handleTextChange : function(text){
|
||||
@@ -107,7 +116,7 @@ const NewPage = createClass({
|
||||
this.setState((prevState)=>({
|
||||
brew : { ...prevState.brew, text: text },
|
||||
htmlErrors : htmlErrors,
|
||||
currentEditorPage : this.refs.editor.getCurrentPage() - 1 //Offset index since Marked starts pages at 0
|
||||
currentEditorPage : this.editor.current.getCurrentPage() - 1 //Offset index since Marked starts pages at 0
|
||||
}));
|
||||
localStorage.setItem(BREWKEY, text);
|
||||
},
|
||||
@@ -119,7 +128,10 @@ const NewPage = createClass({
|
||||
localStorage.setItem(STYLEKEY, style);
|
||||
},
|
||||
|
||||
handleMetaChange : function(metadata){
|
||||
handleMetaChange : function(metadata, field=undefined){
|
||||
if(field == 'theme' || field == 'renderer') // Fetch theme bundle only if theme or renderer was changed
|
||||
fetchThemeBundle(this, metadata.renderer, metadata.theme);
|
||||
|
||||
this.setState((prevState)=>({
|
||||
brew : { ...prevState.brew, ...metadata },
|
||||
}), ()=>{
|
||||
@@ -139,8 +151,6 @@ const NewPage = createClass({
|
||||
isSaving : true
|
||||
});
|
||||
|
||||
console.log('saving new brew');
|
||||
|
||||
let brew = this.state.brew;
|
||||
// Split out CSS to Style if CSS codefence exists
|
||||
if(brew.text.startsWith('```css') && brew.text.indexOf('```\n\n') > 0) {
|
||||
@@ -150,12 +160,10 @@ const NewPage = createClass({
|
||||
}
|
||||
|
||||
brew.pageCount=((brew.renderer=='legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1;
|
||||
|
||||
const res = await request
|
||||
.post(`/api${this.state.saveGoogle ? '?saveToGoogle=true' : ''}`)
|
||||
.send(brew)
|
||||
.catch((err)=>{
|
||||
console.log(err);
|
||||
this.setState({ isSaving: false, error: err });
|
||||
});
|
||||
if(!res) return;
|
||||
@@ -179,16 +187,6 @@ const NewPage = createClass({
|
||||
}
|
||||
},
|
||||
|
||||
print : function(){
|
||||
window.open('/print?dialog=true&local=print', '_blank');
|
||||
},
|
||||
|
||||
renderLocalPrintButton : function(){
|
||||
return <Nav.item color='purple' icon='far fa-file-pdf' onClick={this.print}>
|
||||
get PDF
|
||||
</Nav.item>;
|
||||
},
|
||||
|
||||
renderNavbar : function(){
|
||||
return <Navbar>
|
||||
|
||||
@@ -201,7 +199,7 @@ const NewPage = createClass({
|
||||
<ErrorNavItem error={this.state.error} parent={this}></ErrorNavItem> :
|
||||
this.renderSaveButton()
|
||||
}
|
||||
{this.renderLocalPrintButton()}
|
||||
<PrintNavItem />
|
||||
<HelpNavItem />
|
||||
<RecentNavItem />
|
||||
<AccountNavItem />
|
||||
@@ -213,23 +211,27 @@ const NewPage = createClass({
|
||||
return <div className='newPage sitePage'>
|
||||
{this.renderNavbar()}
|
||||
<div className='content'>
|
||||
<SplitPane onDragFinish={this.handleSplitMove} ref='pane'>
|
||||
<SplitPane onDragFinish={this.handleSplitMove}>
|
||||
<Editor
|
||||
ref='editor'
|
||||
ref={this.editor}
|
||||
brew={this.state.brew}
|
||||
onTextChange={this.handleTextChange}
|
||||
onStyleChange={this.handleStyleChange}
|
||||
onMetaChange={this.handleMetaChange}
|
||||
renderer={this.state.brew.renderer}
|
||||
userThemes={this.props.userThemes}
|
||||
snippetBundle={this.state.themeBundle.snippets}
|
||||
/>
|
||||
<BrewRenderer
|
||||
text={this.state.brew.text}
|
||||
style={this.state.brew.style}
|
||||
renderer={this.state.brew.renderer}
|
||||
theme={this.state.brew.theme}
|
||||
themeBundle={this.state.themeBundle}
|
||||
errors={this.state.htmlErrors}
|
||||
lang={this.state.brew.lang}
|
||||
currentEditorPage={this.state.currentEditorPage}
|
||||
allowPrint={true}
|
||||
/>
|
||||
</SplitPane>
|
||||
</div>
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
require('./printPage.less');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
const cx = require('classnames');
|
||||
const { Meta } = require('vitreum/headtags');
|
||||
const MarkdownLegacy = require('naturalcrit/markdownLegacy.js');
|
||||
const Markdown = require('naturalcrit/markdown.js');
|
||||
|
||||
const Themes = require('themes/themes.json');
|
||||
|
||||
const BREWKEY = 'homebrewery-new';
|
||||
const STYLEKEY = 'homebrewery-new-style';
|
||||
const METAKEY = 'homebrewery-new-meta';
|
||||
|
||||
const PrintPage = createClass({
|
||||
displayName : 'PrintPage',
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
query : {},
|
||||
brew : {
|
||||
text : '',
|
||||
style : '',
|
||||
renderer : 'legacy',
|
||||
lang : ''
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState : function() {
|
||||
return {
|
||||
brew : {
|
||||
text : this.props.brew.text || '',
|
||||
style : this.props.brew.style || undefined,
|
||||
renderer : this.props.brew.renderer || 'legacy',
|
||||
theme : this.props.brew.theme || '5ePHB',
|
||||
lang : this.props.brew.lang || 'en'
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
componentDidMount : function() {
|
||||
if(this.props.query.local == 'print'){
|
||||
const brewStorage = localStorage.getItem(BREWKEY);
|
||||
const styleStorage = localStorage.getItem(STYLEKEY);
|
||||
const metaStorage = JSON.parse(localStorage.getItem(METAKEY));
|
||||
|
||||
this.setState((prevState, prevProps)=>{
|
||||
return {
|
||||
brew : {
|
||||
text : brewStorage,
|
||||
style : styleStorage,
|
||||
renderer : metaStorage?.renderer || 'legacy',
|
||||
theme : metaStorage?.theme || '5ePHB',
|
||||
lang : metaStorage?.lang || 'en'
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
if(this.props.query.dialog) window.print();
|
||||
},
|
||||
|
||||
renderStyle : function() {
|
||||
if(!this.state.brew.style) return;
|
||||
//return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `<style>@layer styleTab {\n${this.state.brew.style}\n} </style>` }} />;
|
||||
return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `<style>\n${this.state.brew.style}\n</style>` }} />;
|
||||
},
|
||||
|
||||
renderPages : function(){
|
||||
if(this.state.brew.renderer == 'legacy') {
|
||||
return _.map(this.state.brew.text.split('\\page'), (pageText, index)=>{
|
||||
return <div
|
||||
className='phb page'
|
||||
id={`p${index + 1}`}
|
||||
dangerouslySetInnerHTML={{ __html: MarkdownLegacy.render(pageText) }}
|
||||
key={index} />;
|
||||
});
|
||||
} else {
|
||||
return _.map(this.state.brew.text.split(/^\\page$/gm), (pageText, index)=>{
|
||||
pageText += `\n\n \n\\column\n `; //Artificial column break at page end to emulate column-fill:auto (until `wide` is used, when column-fill:balance will reappear)
|
||||
return (
|
||||
<div className='page' id={`p${index + 1}`} key={index} >
|
||||
<div className='columnWrapper' dangerouslySetInnerHTML={{ __html: Markdown.render(pageText) }} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
render : function(){
|
||||
const rendererPath = this.state.brew.renderer == 'V3' ? 'V3' : 'Legacy';
|
||||
const themePath = this.state.brew.theme ?? '5ePHB';
|
||||
const baseThemePath = Themes[rendererPath][themePath].baseTheme;
|
||||
|
||||
return <div>
|
||||
<Meta name='robots' content='noindex, nofollow' />
|
||||
<link href={`/themes/${rendererPath}/Blank/style.css`} type="text/css" rel='stylesheet'/>
|
||||
{baseThemePath &&
|
||||
<link href={`/themes/${rendererPath}/${baseThemePath}/style.css`} type="text/css" rel='stylesheet'/>
|
||||
}
|
||||
<link href={`/themes/${rendererPath}/${themePath}/style.css`} type="text/css" rel='stylesheet'/>
|
||||
{/* Apply CSS from Style tab */}
|
||||
{this.renderStyle()}
|
||||
<div className='pages' ref='pages' lang={this.state.brew.lang}>
|
||||
{this.renderPages()}
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = PrintPage;
|
||||
@@ -1,3 +0,0 @@
|
||||
.printPage{
|
||||
|
||||
}
|
||||
@@ -6,25 +6,33 @@ const { Meta } = require('vitreum/headtags');
|
||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||
const Navbar = require('../../navbar/navbar.jsx');
|
||||
const MetadataNav = require('../../navbar/metadata.navitem.jsx');
|
||||
const PrintLink = require('../../navbar/print.navitem.jsx');
|
||||
const PrintNavItem = require('../../navbar/print.navitem.jsx');
|
||||
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
||||
const Account = require('../../navbar/account.navitem.jsx');
|
||||
|
||||
|
||||
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
||||
|
||||
const { DEFAULT_BREW_LOAD } = require('../../../../server/brewDefaults.js');
|
||||
const { printCurrentBrew, fetchThemeBundle } = require('../../../../shared/helpers.js');
|
||||
|
||||
const SharePage = createClass({
|
||||
displayName : 'SharePage',
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
brew : DEFAULT_BREW_LOAD
|
||||
brew : DEFAULT_BREW_LOAD,
|
||||
disableMeta : false
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState : function() {
|
||||
return {
|
||||
themeBundle : {}
|
||||
};
|
||||
},
|
||||
|
||||
componentDidMount : function() {
|
||||
document.addEventListener('keydown', this.handleControlKeys);
|
||||
|
||||
fetchThemeBundle(this, this.props.brew.renderer, this.props.brew.theme);
|
||||
},
|
||||
|
||||
componentWillUnmount : function() {
|
||||
@@ -35,7 +43,7 @@ const SharePage = createClass({
|
||||
if(!(e.ctrlKey || e.metaKey)) return;
|
||||
const P_KEY = 80;
|
||||
if(e.keyCode == P_KEY){
|
||||
window.open(`/print/${this.processShareId()}?dialog=true`, '_blank').focus();
|
||||
if(e.keyCode == P_KEY) printCurrentBrew();
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
@@ -47,30 +55,52 @@ const SharePage = createClass({
|
||||
this.props.brew.shareId;
|
||||
},
|
||||
|
||||
renderEditLink : function(){
|
||||
if(!this.props.brew.editId) return;
|
||||
|
||||
let editLink = this.props.brew.editId;
|
||||
if(this.props.brew.googleId && !this.props.brew.stubbed) {
|
||||
editLink = this.props.brew.googleId + editLink;
|
||||
}
|
||||
|
||||
return <Nav.item color='orange' icon='fas fa-pencil-alt' href={`/edit/${editLink}`}>
|
||||
edit
|
||||
</Nav.item>;
|
||||
},
|
||||
|
||||
render : function(){
|
||||
const titleStyle = this.props.disableMeta ? { cursor: 'default' } : {};
|
||||
const titleEl = <Nav.item className='brewTitle' style={titleStyle}>{this.props.brew.title}</Nav.item>;
|
||||
|
||||
return <div className='sharePage sitePage'>
|
||||
<Meta name='robots' content='noindex, nofollow' />
|
||||
<Navbar>
|
||||
<Nav.section className='titleSection'>
|
||||
<MetadataNav brew={this.props.brew}>
|
||||
<Nav.item className='brewTitle'>{this.props.brew.title}</Nav.item>
|
||||
</MetadataNav>
|
||||
{
|
||||
this.props.disableMeta ?
|
||||
titleEl
|
||||
:
|
||||
<MetadataNav brew={this.props.brew}>
|
||||
{titleEl}
|
||||
</MetadataNav>
|
||||
}
|
||||
</Nav.section>
|
||||
|
||||
<Nav.section>
|
||||
{this.props.brew.shareId && <>
|
||||
<PrintLink shareId={this.processShareId()} />
|
||||
<PrintNavItem/>
|
||||
<Nav.dropdown>
|
||||
<Nav.item color='red' icon='fas fa-code'>
|
||||
source
|
||||
</Nav.item>
|
||||
<Nav.item color='blue' href={`/source/${this.processShareId()}`}>
|
||||
<Nav.item color='blue' icon='fas fa-eye' href={`/source/${this.processShareId()}`}>
|
||||
view
|
||||
</Nav.item>
|
||||
<Nav.item color='blue' href={`/download/${this.processShareId()}`}>
|
||||
{this.renderEditLink()}
|
||||
<Nav.item color='blue' icon='fas fa-download' href={`/download/${this.processShareId()}`}>
|
||||
download
|
||||
</Nav.item>
|
||||
<Nav.item color='blue' href={`/new/${this.processShareId()}`}>
|
||||
<Nav.item color='blue' icon='fas fa-clone' href={`/new/${this.processShareId()}`}>
|
||||
clone to new
|
||||
</Nav.item>
|
||||
</Nav.dropdown>
|
||||
@@ -81,7 +111,14 @@ const SharePage = createClass({
|
||||
</Navbar>
|
||||
|
||||
<div className='content'>
|
||||
<BrewRenderer text={this.props.brew.text} style={this.props.brew.style} renderer={this.props.brew.renderer} theme={this.props.brew.theme} />
|
||||
<BrewRenderer
|
||||
text={this.props.brew.text}
|
||||
style={this.props.brew.style}
|
||||
renderer={this.props.brew.renderer}
|
||||
theme={this.props.brew.theme}
|
||||
themeBundle={this.state.themeBundle}
|
||||
allowPrint={true}
|
||||
/>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
const cx = require('classnames');
|
||||
|
||||
const ListPage = require('../basePages/listPage/listPage.jsx');
|
||||
|
||||
|
||||
@@ -1,57 +1,69 @@
|
||||
.fac {
|
||||
display : inline-block;
|
||||
display : inline-block;
|
||||
background-color : currentColor;
|
||||
mask-size : contain;
|
||||
mask-repeat : no-repeat;
|
||||
mask-position : center;
|
||||
width : 1em;
|
||||
aspect-ratio : 1;
|
||||
}
|
||||
.position-top-left {
|
||||
content: url('../icons/position-top-left.svg');
|
||||
mask-image: url('../icons/position-top-left.svg');
|
||||
}
|
||||
.position-top-right {
|
||||
content: url('../icons/position-top-right.svg');
|
||||
mask-image: url('../icons/position-top-right.svg');
|
||||
}
|
||||
.position-bottom-left {
|
||||
content: url('../icons/position-bottom-left.svg');
|
||||
mask-image: url('../icons/position-bottom-left.svg');
|
||||
}
|
||||
.position-bottom-right {
|
||||
content: url('../icons/position-bottom-right.svg');
|
||||
mask-image: url('../icons/position-bottom-right.svg');
|
||||
}
|
||||
.position-top {
|
||||
content: url('../icons/position-top.svg');
|
||||
mask-image: url('../icons/position-top.svg');
|
||||
}
|
||||
.position-right {
|
||||
content: url('../icons/position-right.svg');
|
||||
mask-image: url('../icons/position-right.svg');
|
||||
}
|
||||
.position-bottom {
|
||||
content: url('../icons/position-bottom.svg');
|
||||
mask-image: url('../icons/position-bottom.svg');
|
||||
}
|
||||
.position-left {
|
||||
content: url('../icons/position-left.svg');
|
||||
mask-image: url('../icons/position-left.svg');
|
||||
}
|
||||
.mask-edge {
|
||||
content: url('../icons/mask-edge.svg');
|
||||
mask-image: url('../icons/mask-edge.svg');
|
||||
}
|
||||
.mask-corner {
|
||||
content: url('../icons/mask-corner.svg');
|
||||
mask-image: url('../icons/mask-corner.svg');
|
||||
}
|
||||
.mask-center {
|
||||
content: url('../icons/mask-center.svg');
|
||||
mask-image: url('../icons/mask-center.svg');
|
||||
}
|
||||
.book-front-cover {
|
||||
content: url('../icons/book-front-cover.svg');
|
||||
mask-image: url('../icons/book-front-cover.svg');
|
||||
}
|
||||
.book-back-cover {
|
||||
content: url('../icons/book-back-cover.svg');
|
||||
mask-image: url('../icons/book-back-cover.svg');
|
||||
}
|
||||
.book-inside-cover {
|
||||
content: url('../icons/book-inside-cover.svg');
|
||||
mask-image: url('../icons/book-inside-cover.svg');
|
||||
}
|
||||
.book-part-cover {
|
||||
content: url('../icons/book-part-cover.svg');
|
||||
mask-image: url('../icons/book-part-cover.svg');
|
||||
}
|
||||
.davek {
|
||||
content: url('../icons/Davek.svg');
|
||||
mask-image: url('../icons/Davek.svg');
|
||||
}
|
||||
.rellanic {
|
||||
content: url('../icons/Rellanic.svg');
|
||||
mask-image: url('../icons/Rellanic.svg');
|
||||
}
|
||||
.iokharic {
|
||||
content: url('../icons/Iokharic.svg');
|
||||
mask-image: url('../icons/Iokharic.svg');
|
||||
}
|
||||
.zoom-to-fit {
|
||||
mask-image: url('../icons/zoom-to-fit.svg');
|
||||
}
|
||||
.fit-width {
|
||||
mask-image: url('../icons/fit-width.svg');
|
||||
}
|
||||
|
||||
15
client/icons/fit-width.svg
Normal file
15
client/icons/fit-width.svg
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 100 100" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<g transform="matrix(1.07509,0,0,1.07509,-3.75511,-3.75468)">
|
||||
<g transform="matrix(0.843549,0,0,0.950644,8.38004,4.39672)">
|
||||
<path d="M28.455,52.413L28.455,58.581C28.455,59.719 27.684,60.745 26.501,61.181C25.318,61.616 23.956,61.375 23.051,60.571L11.114,49.96C9.878,48.862 9.878,47.08 11.114,45.981L23.051,35.371C23.956,34.566 25.318,34.326 26.501,34.761C27.684,35.197 28.455,36.223 28.455,37.361L28.455,43.528L70.223,43.528L70.223,37.361C70.223,36.223 70.995,35.197 72.177,34.761C73.36,34.326 74.722,34.566 75.627,35.371L87.564,45.981C88.8,47.08 88.8,48.862 87.564,49.96L75.627,60.571C74.722,61.375 73.36,61.616 72.177,61.181C70.995,60.745 70.223,59.719 70.223,58.581L70.223,52.413L28.455,52.413Z"/>
|
||||
</g>
|
||||
<g transform="matrix(1.46702,0,0,0.986488,-23.0335,3.50686)">
|
||||
<path d="M23.967,5.877L23.967,88.383C23.967,90.556 22.781,92.321 21.319,92.321L21.157,92.321C19.695,92.321 18.509,90.556 18.509,88.383L18.509,5.877C18.509,3.703 19.695,1.939 21.157,1.939L21.319,1.939C22.781,1.939 23.967,3.703 23.967,5.877Z"/>
|
||||
</g>
|
||||
<g transform="matrix(1.46702,0,0,0.986488,60.7211,3.50686)">
|
||||
<path d="M23.967,5.877L23.967,88.383C23.967,90.556 22.781,92.321 21.319,92.321L21.157,92.321C19.695,92.321 18.509,90.556 18.509,88.383L18.509,5.877C18.509,3.703 19.695,1.939 21.157,1.939L21.319,1.939C22.781,1.939 23.967,3.703 23.967,5.877Z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
12
client/icons/zoom-to-fit.svg
Normal file
12
client/icons/zoom-to-fit.svg
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 100 100" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<g transform="matrix(1.0781,0,0,1.0781,-3.90545,-3.90502)">
|
||||
<g transform="matrix(0.841196,0,0,0.947993,8.49652,4.52391)">
|
||||
<path d="M44.333,52.413L28.455,52.413L28.455,58.581C28.455,59.719 27.684,60.745 26.501,61.181C25.318,61.616 23.956,61.375 23.051,60.571L11.114,49.96C9.878,48.862 9.878,47.08 11.114,45.981L23.051,35.371C23.956,34.566 25.318,34.326 26.501,34.761C27.684,35.197 28.455,36.223 28.455,37.361L28.455,43.528L44.333,43.528L44.333,29.439L37.382,29.439C36.099,29.439 34.943,28.755 34.452,27.705C33.961,26.656 34.233,25.448 35.14,24.644L47.097,14.052C48.335,12.956 50.343,12.956 51.581,14.052L63.539,24.644C64.446,25.448 64.717,26.656 64.226,27.705C63.735,28.755 62.579,29.439 61.296,29.439L54.346,29.439L54.346,43.528L70.223,43.528L70.223,37.361C70.223,36.223 70.995,35.197 72.177,34.761C73.36,34.326 74.722,34.566 75.627,35.371L87.564,45.981C88.8,47.08 88.8,48.862 87.564,49.96L75.627,60.571C74.722,61.375 73.36,61.616 72.177,61.181C70.995,60.745 70.223,59.719 70.223,58.581L70.223,52.413L54.346,52.413L54.346,66.502L61.296,66.502C62.579,66.502 63.735,67.187 64.226,68.236C64.717,69.286 64.446,70.494 63.539,71.297L51.581,81.889C50.343,82.986 48.335,82.986 47.097,81.889L35.14,71.297C34.233,70.494 33.961,69.286 34.452,68.236C34.943,67.187 36.099,66.502 37.382,66.502L44.333,66.503L44.333,52.413Z"/>
|
||||
</g>
|
||||
<g transform="matrix(1.0247,0,0,1.0247,-5.47698,-3.53855)">
|
||||
<path d="M99.4,14.269L99.4,90.227C99.4,94.245 96.137,97.508 92.119,97.508L16.161,97.508C12.142,97.508 8.88,94.245 8.88,90.227L8.88,14.269C8.88,10.25 12.142,6.988 16.161,6.988L92.119,6.988C96.137,6.988 99.4,10.25 99.4,14.269ZM93.633,14.269C93.633,13.433 92.955,12.755 92.119,12.755L16.161,12.755C15.325,12.755 14.647,13.433 14.647,14.269L14.647,90.227C14.647,91.062 15.325,91.741 16.161,91.741L92.119,91.741C92.955,91.741 93.633,91.062 93.633,90.227L93.633,14.269Z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
@@ -4,6 +4,7 @@
|
||||
"secret" : "secret",
|
||||
"web_port" : 8000,
|
||||
"enable_v3" : true,
|
||||
"enable_themes" : true,
|
||||
"local_environments" : ["docker", "local"],
|
||||
"publicUrl" : "https://homebrewery.naturalcrit.com"
|
||||
}
|
||||
|
||||
71
eslint.config.mjs
Normal file
71
eslint.config.mjs
Normal file
@@ -0,0 +1,71 @@
|
||||
import react from "eslint-plugin-react";
|
||||
import jest from "eslint-plugin-jest";
|
||||
import globals from "globals";
|
||||
|
||||
export default [{
|
||||
ignores: ["build/"]
|
||||
},
|
||||
{
|
||||
files : ['**/*.js', '**/*.jsx'],
|
||||
plugins : { react, jest },
|
||||
languageOptions : {
|
||||
ecmaVersion : "latest",
|
||||
sourceType : "module",
|
||||
parserOptions : { ecmaFeatures: { jsx: true } },
|
||||
globals : { ...globals.browser, ...globals.node }
|
||||
},
|
||||
rules: {
|
||||
/** Errors **/
|
||||
"camelcase" : ["error", { properties: "never" }],
|
||||
"no-array-constructor" : "error",
|
||||
"no-iterator" : "error",
|
||||
"no-nested-ternary" : "error",
|
||||
"no-new-object" : "error",
|
||||
"no-proto" : "error",
|
||||
"react/jsx-no-bind" : ["error", { allowArrowFunctions: true }],
|
||||
"react/jsx-uses-react" : "error",
|
||||
"react/prefer-es6-class" : ["error", "never"],
|
||||
"jest/valid-expect" : ["error", { maxArgs: 3 }],
|
||||
|
||||
/** Warnings **/
|
||||
"max-lines" : ["warn", { max: 200, skipComments: true, skipBlankLines: true }],
|
||||
"max-depth" : ["warn", { max: 4 }],
|
||||
"max-params" : ["warn", { max: 5 }],
|
||||
"no-restricted-syntax" : ["warn", "ClassDeclaration", "SwitchStatement"],
|
||||
"no-unused-vars" : ["warn", { vars: "all", args: "none", varsIgnorePattern: "config|_|cx|createClass" }],
|
||||
"react/jsx-uses-vars" : "warn",
|
||||
|
||||
/** Fixable **/
|
||||
"arrow-parens" : ["warn", "always"],
|
||||
"brace-style" : ["warn", "1tbs", { allowSingleLine: true }],
|
||||
"jsx-quotes" : ["warn", "prefer-single"],
|
||||
"no-var" : "warn",
|
||||
"prefer-const" : "warn",
|
||||
"prefer-template" : "warn",
|
||||
"quotes" : ["warn", "single", { allowTemplateLiterals: true }],
|
||||
"semi" : ["warn", "always"],
|
||||
|
||||
/** Whitespace **/
|
||||
"array-bracket-spacing" : ["warn", "never"],
|
||||
"arrow-spacing" : ["warn", { before: false, after: false }],
|
||||
"comma-spacing" : ["warn", { before: false, after: true }],
|
||||
"indent" : ["warn", "tab", { MemberExpression: "off" }],
|
||||
"linebreak-style" : "off",
|
||||
"no-trailing-spaces" : "warn",
|
||||
"no-whitespace-before-property" : "warn",
|
||||
"object-curly-spacing" : ["warn", "always"],
|
||||
"react/jsx-indent-props" : ["warn", "tab"],
|
||||
"space-in-parens" : ["warn", "never"],
|
||||
"template-curly-spacing" : ["warn", "never"],
|
||||
"keyword-spacing" : ["warn", {
|
||||
before : true,
|
||||
after : true,
|
||||
overrides : { if: { before: false, after: false } }
|
||||
}],
|
||||
"key-spacing" : ["warn", {
|
||||
multiLine : { beforeColon: true, afterColon: true, align: "colon" },
|
||||
singleLine : { beforeColon: false, afterColon: true }
|
||||
}]
|
||||
}
|
||||
}
|
||||
];
|
||||
6500
package-lock.json
generated
6500
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
75
package.json
75
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "homebrewery",
|
||||
"description": "Create authentic looking D&D homebrews using only markdown",
|
||||
"version": "3.12.0",
|
||||
"version": "3.14.2",
|
||||
"engines": {
|
||||
"npm": "^10.2.x",
|
||||
"node": "^20.8.x"
|
||||
@@ -15,23 +15,27 @@
|
||||
"quick": "node scripts/quick.js",
|
||||
"build": "node scripts/buildHomebrew.js && node scripts/buildAdmin.js",
|
||||
"builddev": "node scripts/buildHomebrew.js --dev",
|
||||
"lint": "eslint --fix **/*.{js,jsx}",
|
||||
"lint:dry": "eslint **/*.{js,jsx}",
|
||||
"lint": "eslint --fix",
|
||||
"lint:dry": "eslint",
|
||||
"stylelint": "stylelint --fix **/*.{less}",
|
||||
"stylelint:dry": "stylelint **/*.less",
|
||||
"circleci": "npm test && eslint **/*.{js,jsx} --max-warnings=0",
|
||||
"verify": "npm run lint && npm test",
|
||||
"test": "jest --runInBand",
|
||||
"test:api-unit": "jest server/*.spec.js --verbose",
|
||||
"test:api-unit": "jest \"server/.*.spec.js\" --verbose",
|
||||
"test:api-unit:themes": "jest \"server/.*.spec.js\" -t \"theme bundle\" --verbose",
|
||||
"test:api-unit:css": "jest \"server/.*.spec.js\" -t \"Get CSS\" --verbose",
|
||||
"test:coverage": "jest --coverage --silent --runInBand",
|
||||
"test:dev": "jest --verbose --watch",
|
||||
"test:basic": "jest tests/markdown/basic.test.js --verbose",
|
||||
"test:variables": "jest tests/markdown/variables.test.js --verbose",
|
||||
"test:mustache-syntax": "jest '.*(mustache-syntax).*' --verbose --noStackTrace",
|
||||
"test:mustache-syntax:inline": "jest '.*(mustache-syntax).*' -t '^Inline:.*' --verbose --noStackTrace",
|
||||
"test:mustache-syntax:block": "jest '.*(mustache-syntax).*' -t '^Block:.*' --verbose --noStackTrace",
|
||||
"test:mustache-syntax:injection": "jest '.*(mustache-syntax).*' -t '^Injection:.*' --verbose --noStackTrace",
|
||||
"test:definition-lists": "jest tests/markdown/definition-lists.test.js --verbose --noStackTrace",
|
||||
"test:mustache-syntax": "jest \".*(mustache-syntax).*\" --verbose --noStackTrace",
|
||||
"test:mustache-syntax:inline": "jest \".*(mustache-syntax).*\" -t '^Inline:.*' --verbose --noStackTrace",
|
||||
"test:mustache-syntax:block": "jest \".*(mustache-syntax).*\" -t '^Block:.*' --verbose --noStackTrace",
|
||||
"test:mustache-syntax:injection": "jest \".*(mustache-syntax).*\" -t '^Injection:.*' --verbose --noStackTrace",
|
||||
"test:definition-lists": "jest tests/markdown/definition-lists.test.js --verbose --noStackTrace",
|
||||
"test:hard-breaks": "jest tests/markdown/hard-breaks.test.js --verbose --noStackTrace",
|
||||
"test:emojis": "jest tests/markdown/emojis.test.js --verbose --noStackTrace",
|
||||
"test:route": "jest tests/routes/static-pages.test.js --verbose",
|
||||
"phb": "node scripts/phb.js",
|
||||
"prod": "set NODE_ENV=production && npm run build",
|
||||
@@ -55,15 +59,15 @@
|
||||
],
|
||||
"coverageThreshold": {
|
||||
"global": {
|
||||
"statements": 25,
|
||||
"branches": 10,
|
||||
"functions": 22,
|
||||
"lines": 25
|
||||
"statements": 50,
|
||||
"branches": 40,
|
||||
"functions": 40,
|
||||
"lines": 50
|
||||
},
|
||||
"server/homebrew.api.js": {
|
||||
"statements": 65,
|
||||
"statements": 70,
|
||||
"branches": 50,
|
||||
"functions": 60,
|
||||
"functions": 65,
|
||||
"lines": 70
|
||||
}
|
||||
},
|
||||
@@ -81,17 +85,18 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.24.4",
|
||||
"@babel/plugin-transform-runtime": "^7.24.3",
|
||||
"@babel/preset-env": "^7.24.4",
|
||||
"@babel/preset-react": "^7.24.1",
|
||||
"@googleapis/drive": "^8.7.0",
|
||||
"@babel/core": "^7.25.2",
|
||||
"@babel/plugin-transform-runtime": "^7.25.4",
|
||||
"@babel/preset-env": "^7.25.4",
|
||||
"@babel/preset-react": "^7.24.7",
|
||||
"@googleapis/drive": "^8.13.0",
|
||||
"body-parser": "^1.20.2",
|
||||
"classnames": "^2.5.1",
|
||||
"codemirror": "^5.65.6",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"create-react-class": "^15.7.0",
|
||||
"dedent-tabs": "^0.10.3",
|
||||
"dompurify": "^3.1.6",
|
||||
"expr-eval": "^2.0.2",
|
||||
"express": "^4.19.2",
|
||||
"express-async-handler": "^1.2.0",
|
||||
@@ -102,33 +107,35 @@
|
||||
"less": "^3.13.1",
|
||||
"lodash": "^4.17.21",
|
||||
"marked": "11.2.0",
|
||||
"marked-emoji": "^1.4.2",
|
||||
"marked-extended-tables": "^1.0.8",
|
||||
"marked-gfm-heading-id": "^3.1.3",
|
||||
"marked-gfm-heading-id": "^3.2.0",
|
||||
"marked-smartypants-lite": "^1.0.2",
|
||||
"markedLegacy": "npm:marked@^0.3.19",
|
||||
"moment": "^2.30.1",
|
||||
"mongoose": "^8.3.1",
|
||||
"mongoose": "^8.5.4",
|
||||
"nanoid": "3.3.4",
|
||||
"nconf": "^0.12.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-frame-component": "^4.1.3",
|
||||
"react-router-dom": "6.22.3",
|
||||
"react-router-dom": "6.26.1",
|
||||
"sanitize-filename": "1.6.3",
|
||||
"superagent": "^8.1.2",
|
||||
"superagent": "^10.1.0",
|
||||
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-jest": "^28.2.0",
|
||||
"eslint-plugin-react": "^7.34.1",
|
||||
"@stylistic/stylelint-plugin": "^3.0.1",
|
||||
"eslint": "^9.9.1",
|
||||
"eslint-plugin-jest": "^28.8.0",
|
||||
"eslint-plugin-react": "^7.35.0",
|
||||
"globals": "^15.9.0",
|
||||
"jest": "^29.7.0",
|
||||
"jest-expect-message": "^1.1.3",
|
||||
"postcss-less": "^6.0.0",
|
||||
"stylelint": "^15.11.0",
|
||||
"stylelint-config-recess-order": "^4.6.0",
|
||||
"stylelint-config-recommended": "^13.0.0",
|
||||
"stylelint-stylistic": "^0.4.3",
|
||||
"supertest": "^6.3.4"
|
||||
"stylelint": "^16.8.2",
|
||||
"stylelint-config-recess-order": "^5.1.0",
|
||||
"stylelint-config-recommended": "^14.0.1",
|
||||
"supertest": "^7.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
"codemirror/addon/edit/closetag.js",
|
||||
"codemirror/addon/edit/trailingspace.js",
|
||||
"codemirror/addon/selection/active-line.js",
|
||||
"codemirror/addon/hint/show-hint.js",
|
||||
"moment",
|
||||
"superagent"
|
||||
]
|
||||
|
||||
@@ -14,7 +14,7 @@ DB.connect(config).then(()=>{
|
||||
|
||||
console.log(`\n\tserver started at: ${new Date().toLocaleString()}`);
|
||||
console.log(`\tserver on port: ${PORT}`);
|
||||
console.log(`\t${bright + cyan}Open in browser: ${reset}${underline + bright + cyan}http://localhost:${PORT}${reset}\n\n`)
|
||||
console.log(`\t${bright + cyan}Open in browser: ${reset}${underline + bright + cyan}http://localhost:${PORT}${reset}\n\n`);
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
@@ -30,7 +30,7 @@ const junkBrewPipeline = [
|
||||
{ $match : {
|
||||
updatedAt : { $lt: Moment().subtract(30, 'days').toDate() },
|
||||
lastViewed : { $lt: Moment().subtract(30, 'days').toDate() }
|
||||
}},
|
||||
} },
|
||||
{ $project: { textBinSize: { $binarySize: '$textBin' } } },
|
||||
{ $match: { textBinSize: { $lt: 140 } } },
|
||||
{ $limit: 100 }
|
||||
|
||||
@@ -9,11 +9,12 @@ const yaml = require('js-yaml');
|
||||
const app = express();
|
||||
const config = require('./config.js');
|
||||
|
||||
const { homebrewApi, getBrew } = require('./homebrew.api.js');
|
||||
const { homebrewApi, getBrew, getUsersBrewThemes, getCSS } = require('./homebrew.api.js');
|
||||
const GoogleActions = require('./googleActions.js');
|
||||
const serveCompressedStaticAssets = require('./static-assets.mv.js');
|
||||
const sanitizeFilename = require('sanitize-filename');
|
||||
const asyncHandler = require('express-async-handler');
|
||||
const templateFn = require('./../client/template.js');
|
||||
|
||||
const { DEFAULT_BREW } = require('./brewDefaults.js');
|
||||
|
||||
@@ -23,7 +24,7 @@ const { splitTextStyleAndMetadata } = require('../shared/helpers.js');
|
||||
const sanitizeBrew = (brew, accessType)=>{
|
||||
brew._id = undefined;
|
||||
brew.__v = undefined;
|
||||
if(accessType !== 'edit'){
|
||||
if(accessType !== 'edit' && accessType !== 'shareAuthor') {
|
||||
brew.editId = undefined;
|
||||
}
|
||||
return brew;
|
||||
@@ -81,7 +82,8 @@ app.get('/robots.txt', (req, res)=>{
|
||||
app.get('/', (req, res, next)=>{
|
||||
req.brew = {
|
||||
text : welcomeText,
|
||||
renderer : 'V3'
|
||||
renderer : 'V3',
|
||||
theme : '5ePHB'
|
||||
},
|
||||
|
||||
req.ogMeta = { ...defaultMetaTags,
|
||||
@@ -97,7 +99,8 @@ app.get('/', (req, res, next)=>{
|
||||
app.get('/legacy', (req, res, next)=>{
|
||||
req.brew = {
|
||||
text : welcomeTextLegacy,
|
||||
renderer : 'legacy'
|
||||
renderer : 'legacy',
|
||||
theme : '5ePHB'
|
||||
},
|
||||
|
||||
req.ogMeta = { ...defaultMetaTags,
|
||||
@@ -113,7 +116,8 @@ app.get('/legacy', (req, res, next)=>{
|
||||
app.get('/migrate', (req, res, next)=>{
|
||||
req.brew = {
|
||||
text : migrateText,
|
||||
renderer : 'V3'
|
||||
renderer : 'V3',
|
||||
theme : '5ePHB'
|
||||
},
|
||||
|
||||
req.ogMeta = { ...defaultMetaTags,
|
||||
@@ -130,7 +134,8 @@ app.get('/changelog', async (req, res, next)=>{
|
||||
req.brew = {
|
||||
title : 'Changelog',
|
||||
text : changelogText,
|
||||
renderer : 'V3'
|
||||
renderer : 'V3',
|
||||
theme : '5ePHB'
|
||||
},
|
||||
|
||||
req.ogMeta = { ...defaultMetaTags,
|
||||
@@ -147,7 +152,8 @@ app.get('/faq', async (req, res, next)=>{
|
||||
req.brew = {
|
||||
title : 'FAQ',
|
||||
text : faqText,
|
||||
renderer : 'V3'
|
||||
renderer : 'V3',
|
||||
theme : '5ePHB'
|
||||
},
|
||||
|
||||
req.ogMeta = { ...defaultMetaTags,
|
||||
@@ -195,6 +201,9 @@ app.get('/download/:id', asyncHandler(getBrew('share')), (req, res)=>{
|
||||
res.status(200).send(brew.text);
|
||||
});
|
||||
|
||||
//Serve brew styling
|
||||
app.get('/css/:id', asyncHandler(getBrew('share')), (req, res)=>{getCSS(req, res);});
|
||||
|
||||
//User Page
|
||||
app.get('/user/:username', async (req, res, next)=>{
|
||||
const ownAccount = req.account && (req.account.username == req.params.username);
|
||||
@@ -265,9 +274,11 @@ app.get('/user/:username', async (req, res, next)=>{
|
||||
});
|
||||
|
||||
//Edit Page
|
||||
app.get('/edit/:id', asyncHandler(getBrew('edit')), (req, res, next)=>{
|
||||
app.get('/edit/:id', asyncHandler(getBrew('edit')), asyncHandler(async(req, res, next)=>{
|
||||
req.brew = req.brew.toObject ? req.brew.toObject() : req.brew;
|
||||
|
||||
req.userThemes = await(getUsersBrewThemes(req.account?.username));
|
||||
|
||||
req.ogMeta = { ...defaultMetaTags,
|
||||
title : req.brew.title || 'Untitled Brew',
|
||||
description : req.brew.description || 'No description.',
|
||||
@@ -279,10 +290,10 @@ app.get('/edit/:id', asyncHandler(getBrew('edit')), (req, res, next)=>{
|
||||
splitTextStyleAndMetadata(req.brew);
|
||||
res.header('Cache-Control', 'no-cache, no-store'); //reload the latest saved brew when pressing back button, not the cached version before save.
|
||||
return next();
|
||||
});
|
||||
}));
|
||||
|
||||
//New Page
|
||||
app.get('/new/:id', asyncHandler(getBrew('share')), (req, res, next)=>{
|
||||
//New Page from ID
|
||||
app.get('/new/:id', asyncHandler(getBrew('share')), asyncHandler(async(req, res, next)=>{
|
||||
sanitizeBrew(req.brew, 'share');
|
||||
splitTextStyleAndMetadata(req.brew);
|
||||
const brew = {
|
||||
@@ -292,22 +303,35 @@ app.get('/new/:id', asyncHandler(getBrew('share')), (req, res, next)=>{
|
||||
style : req.brew.style,
|
||||
renderer : req.brew.renderer,
|
||||
theme : req.brew.theme,
|
||||
tags : req.brew.tags
|
||||
tags : req.brew.tags,
|
||||
};
|
||||
req.brew = _.defaults(brew, DEFAULT_BREW);
|
||||
|
||||
req.userThemes = await(getUsersBrewThemes(req.account?.username));
|
||||
|
||||
req.ogMeta = { ...defaultMetaTags,
|
||||
title : 'New',
|
||||
description : 'Start crafting your homebrew on the Homebrewery!'
|
||||
};
|
||||
|
||||
return next();
|
||||
});
|
||||
}));
|
||||
|
||||
//New Page
|
||||
app.get('/new', asyncHandler(async(req, res, next)=>{
|
||||
req.userThemes = await(getUsersBrewThemes(req.account?.username));
|
||||
|
||||
req.ogMeta = { ...defaultMetaTags,
|
||||
title : 'New',
|
||||
description : 'Start crafting your homebrew on the Homebrewery!'
|
||||
};
|
||||
|
||||
return next();
|
||||
}));
|
||||
|
||||
//Share Page
|
||||
app.get('/share/:id', asyncHandler(getBrew('share')), asyncHandler(async (req, res, next)=>{
|
||||
const { brew } = req;
|
||||
|
||||
req.ogMeta = { ...defaultMetaTags,
|
||||
title : req.brew.title || 'Untitled Brew',
|
||||
description : req.brew.description || 'No description.',
|
||||
@@ -326,18 +350,12 @@ app.get('/share/:id', asyncHandler(getBrew('share')), asyncHandler(async (req, r
|
||||
await HomebrewModel.increaseView({ shareId: brew.shareId });
|
||||
}
|
||||
};
|
||||
sanitizeBrew(req.brew, 'share');
|
||||
|
||||
brew.authors.includes(req.account?.username) ? sanitizeBrew(req.brew, 'shareAuthor') : sanitizeBrew(req.brew, 'share');
|
||||
splitTextStyleAndMetadata(req.brew);
|
||||
return next();
|
||||
}));
|
||||
|
||||
//Print Page
|
||||
app.get('/print/:id', asyncHandler(getBrew('share')), (req, res, next)=>{
|
||||
sanitizeBrew(req.brew, 'share');
|
||||
splitTextStyleAndMetadata(req.brew);
|
||||
next();
|
||||
});
|
||||
|
||||
//Account Page
|
||||
app.get('/account', asyncHandler(async (req, res, next)=>{
|
||||
const data = {};
|
||||
@@ -406,8 +424,16 @@ if(isLocalEnvironment){
|
||||
});
|
||||
}
|
||||
|
||||
//Send rendered page
|
||||
app.use(asyncHandler(async (req, res, next)=>{
|
||||
if (!req.route) return res.redirect('/'); // Catch-all for invalid routes
|
||||
|
||||
const page = await renderPage(req, res);
|
||||
if(!page) return;
|
||||
res.send(page);
|
||||
}));
|
||||
|
||||
//Render the page
|
||||
const templateFn = require('./../client/template.js');
|
||||
const renderPage = async (req, res)=>{
|
||||
// Create configuration object
|
||||
const configuration = {
|
||||
@@ -425,7 +451,8 @@ const renderPage = async (req, res)=>{
|
||||
enable_v3 : config.get('enable_v3'),
|
||||
enable_themes : config.get('enable_themes'),
|
||||
config : configuration,
|
||||
ogMeta : req.ogMeta
|
||||
ogMeta : req.ogMeta,
|
||||
userThemes : req.userThemes
|
||||
};
|
||||
const title = req.brew ? req.brew.title : '';
|
||||
const page = await templateFn('homebrew', title, props)
|
||||
@@ -435,13 +462,6 @@ const renderPage = async (req, res)=>{
|
||||
return page;
|
||||
};
|
||||
|
||||
//Send rendered page
|
||||
app.use(asyncHandler(async (req, res, next)=>{
|
||||
const page = await renderPage(req, res);
|
||||
if(!page) return;
|
||||
res.send(page);
|
||||
}));
|
||||
|
||||
//v=====----- Error-Handling Middleware -----=====v//
|
||||
//Format Errors as plain objects so all fields will appear in the string sent
|
||||
const formatErrors = (key, value)=>{
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
/* eslint-disable max-lines */
|
||||
const _ = require('lodash');
|
||||
const googleDrive = require('@googleapis/drive');
|
||||
const { nanoid } = require('nanoid');
|
||||
const token = require('./token.js');
|
||||
|
||||
@@ -8,9 +8,16 @@ const Markdown = require('../shared/naturalcrit/markdown.js');
|
||||
const yaml = require('js-yaml');
|
||||
const asyncHandler = require('express-async-handler');
|
||||
const { nanoid } = require('nanoid');
|
||||
const { splitTextStyleAndMetadata } = require('../shared/helpers.js');
|
||||
|
||||
const { DEFAULT_BREW, DEFAULT_BREW_LOAD } = require('./brewDefaults.js');
|
||||
|
||||
const Themes = require('../themes/themes.json');
|
||||
|
||||
const isStaticTheme = (renderer, themeName)=>{
|
||||
return Themes[renderer]?.[themeName] !== undefined;
|
||||
};
|
||||
|
||||
// const getTopBrews = (cb) => {
|
||||
// HomebrewModel.find().sort({ views: -1 }).limit(5).exec(function(err, brews) {
|
||||
// cb(brews);
|
||||
@@ -37,6 +44,43 @@ const api = {
|
||||
}
|
||||
return { id, googleId };
|
||||
},
|
||||
//Get array of any of this user's brews tagged with `meta:theme`
|
||||
getUsersBrewThemes : async (username)=>{
|
||||
if(!username)
|
||||
return {};
|
||||
|
||||
const fields = [
|
||||
'title',
|
||||
'tags',
|
||||
'shareId',
|
||||
'thumbnail',
|
||||
'textBin',
|
||||
'text',
|
||||
'authors',
|
||||
'renderer'
|
||||
];
|
||||
|
||||
const userThemes = {};
|
||||
|
||||
const brews = await HomebrewModel.getByUser(username, true, fields, { tags: { $in: ['meta:theme', 'meta:Theme'] } });
|
||||
|
||||
if(brews) {
|
||||
for (const brew of brews) {
|
||||
userThemes[brew.renderer] ??= {};
|
||||
userThemes[brew.renderer][brew.shareId] = {
|
||||
name : brew.title,
|
||||
renderer : brew.renderer,
|
||||
baseTheme : brew.theme,
|
||||
baseSnippets : false,
|
||||
author : brew.authors[0],
|
||||
path : brew.shareId,
|
||||
thumbnail : brew.thumbnail || '/assets/naturalCritLogoWhite.svg'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return userThemes;
|
||||
},
|
||||
getBrew : (accessType, stubOnly = false)=>{
|
||||
// Create middleware with the accessType passed in as part of the scope
|
||||
return async (req, res, next)=>{
|
||||
@@ -55,7 +99,7 @@ const api = {
|
||||
stub = stub?.toObject();
|
||||
|
||||
if(stub?.lock?.locked && accessType != 'edit') {
|
||||
throw { HBErrorCode: '100', code: stub.lock.code, message: stub.lock.message, brewId: stub.shareId, brewTitle: stub.title };
|
||||
throw { HBErrorCode: '100', code: stub.lock.code, message: stub.lock.shareMessage, brewId: stub.shareId, brewTitle: stub.title };
|
||||
}
|
||||
|
||||
// If there is a google id, try to find the google brew
|
||||
@@ -83,9 +127,9 @@ const api = {
|
||||
if(accessType === 'edit' && (authorsExist && !(isAuthor || isInvited))) {
|
||||
const accessError = { name: 'Access Error', status: 401 };
|
||||
if(req.account){
|
||||
throw { ...accessError, message: 'User is not an Author', HBErrorCode: '03', authors: stub.authors, brewTitle: stub.title, shareId: stub.shareId};
|
||||
throw { ...accessError, message: 'User is not an Author', HBErrorCode: '03', authors: stub.authors, brewTitle: stub.title, shareId: stub.shareId };
|
||||
}
|
||||
throw { ...accessError, message: 'User is not logged in', HBErrorCode: '04', authors: stub.authors, brewTitle: stub.title, shareId: stub.shareId};
|
||||
throw { ...accessError, message: 'User is not logged in', HBErrorCode: '04', authors: stub.authors, brewTitle: stub.title, shareId: stub.shareId };
|
||||
}
|
||||
|
||||
// If after all of that we still don't have a brew, throw an exception
|
||||
@@ -104,6 +148,20 @@ const api = {
|
||||
next();
|
||||
};
|
||||
},
|
||||
|
||||
getCSS : async (req, res)=>{
|
||||
const { brew } = req;
|
||||
if(!brew) return res.status(404).send('');
|
||||
splitTextStyleAndMetadata(brew);
|
||||
if(!brew.style) return res.status(404).send('');
|
||||
|
||||
res.set({
|
||||
'Cache-Control' : 'no-cache',
|
||||
'Content-Type' : 'text/css'
|
||||
});
|
||||
return res.status(200).send(brew.style);
|
||||
},
|
||||
|
||||
mergeBrewText : (brew)=>{
|
||||
let text = brew.text;
|
||||
if(brew.style !== undefined) {
|
||||
@@ -142,7 +200,7 @@ const api = {
|
||||
return modified;
|
||||
},
|
||||
excludeStubProps : (brew)=>{
|
||||
const propsToExclude = ['text', 'textBin', 'renderer', 'pageCount'];
|
||||
const propsToExclude = ['text', 'textBin'];
|
||||
for (const prop of propsToExclude) {
|
||||
brew[prop] = undefined;
|
||||
}
|
||||
@@ -209,6 +267,58 @@ const api = {
|
||||
|
||||
res.status(200).send(saved);
|
||||
},
|
||||
getThemeBundle : async(req, res)=>{
|
||||
/* getThemeBundle: Collects the theme and all parent themes
|
||||
returns an object containing an array of css, and an array of snippets, in render order
|
||||
|
||||
req.params.id : The shareId ( User theme ) or name ( static theme )
|
||||
req.params.renderer : The Markdown renderer used for this theme */
|
||||
|
||||
req.params.renderer = _.upperFirst(req.params.renderer);
|
||||
let currentTheme;
|
||||
const completeStyles = [];
|
||||
const completeSnippets = [];
|
||||
|
||||
while (req.params.id) {
|
||||
//=== User Themes ===//
|
||||
if(!isStaticTheme(req.params.renderer, req.params.id)) {
|
||||
await api.getBrew('share')(req, res, ()=>{})
|
||||
.catch((err)=>{
|
||||
if(err.HBErrorCode == '05')
|
||||
err = { ...err, name: 'ThemeLoad Error', message: 'Theme Not Found', HBErrorCode: '09' };
|
||||
throw err;
|
||||
});
|
||||
|
||||
currentTheme = req.brew;
|
||||
splitTextStyleAndMetadata(currentTheme);
|
||||
|
||||
// If there is anything in the snippets or style members, append them to the appropriate array
|
||||
if(currentTheme?.snippets) completeSnippets.push(JSON.parse(currentTheme.snippets));
|
||||
if(currentTheme?.style) completeStyles.push(`/* From Brew: ${req.protocol}://${req.get('host')}/share/${req.params.id} */\n\n${currentTheme.style}`);
|
||||
|
||||
req.params.id = currentTheme.theme;
|
||||
req.params.renderer = currentTheme.renderer;
|
||||
}
|
||||
//=== Static Themes ===//
|
||||
else {
|
||||
const localSnippets = `${req.params.renderer}_${req.params.id}`; // Just log the name for loading on client
|
||||
const localStyle = `@import url(\"/themes/${req.params.renderer}/${req.params.id}/style.css\");`;
|
||||
completeSnippets.push(localSnippets);
|
||||
completeStyles.push(`/* From Theme ${req.params.id} */\n\n${localStyle}`);
|
||||
|
||||
req.params.id = Themes[req.params.renderer][req.params.id].baseTheme;
|
||||
}
|
||||
}
|
||||
|
||||
const returnObj = {
|
||||
// Reverse the order of the arrays so they are listed oldest parent to youngest child.
|
||||
styles : completeStyles.reverse(),
|
||||
snippets : completeSnippets.reverse()
|
||||
};
|
||||
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
return res.status(200).send(returnObj);
|
||||
},
|
||||
updateBrew : async (req, res)=>{
|
||||
// Initialize brew from request and body, destructure query params, and set the initial value for the after-save method
|
||||
const brewFromClient = api.excludePropsFromUpdate(req.body);
|
||||
@@ -369,5 +479,6 @@ router.put('/api/:id', asyncHandler(api.getBrew('edit', true)), asyncHandler(api
|
||||
router.put('/api/update/:id', asyncHandler(api.getBrew('edit', true)), asyncHandler(api.updateBrew));
|
||||
router.delete('/api/:id', asyncHandler(api.deleteBrew));
|
||||
router.get('/api/remove/:id', asyncHandler(api.deleteBrew));
|
||||
router.get('/api/theme/:renderer/:id', asyncHandler(api.getThemeBundle));
|
||||
|
||||
module.exports = api;
|
||||
|
||||
@@ -14,6 +14,9 @@ describe('Tests for api', ()=>{
|
||||
let saved;
|
||||
|
||||
beforeEach(()=>{
|
||||
jest.resetModules();
|
||||
jest.restoreAllMocks();
|
||||
|
||||
saved = undefined;
|
||||
saveFunc = jest.fn(async function() {
|
||||
saved = { ...this, _id: '1' };
|
||||
@@ -45,8 +48,10 @@ describe('Tests for api', ()=>{
|
||||
model.mockImplementation((brew)=>modelBrew(brew));
|
||||
|
||||
res = {
|
||||
status : jest.fn(()=>res),
|
||||
send : jest.fn(()=>{})
|
||||
status : jest.fn(()=>res),
|
||||
send : jest.fn(()=>{}),
|
||||
set : jest.fn(()=>{}),
|
||||
setHeader : jest.fn(()=>{})
|
||||
};
|
||||
|
||||
api = require('./homebrew.api');
|
||||
@@ -81,10 +86,6 @@ describe('Tests for api', ()=>{
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(()=>{
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('getId', ()=>{
|
||||
it('should return only id if google id is not present', ()=>{
|
||||
const { id, googleId } = api.getId({
|
||||
@@ -300,7 +301,7 @@ describe('Tests for api', ()=>{
|
||||
});
|
||||
|
||||
it('access is denied to a locked brew', async()=>{
|
||||
const lockBrew = { title: 'test brew', shareId: '1', lock: { locked: true, code: 404, message: 'brew locked' } };
|
||||
const lockBrew = { title: 'test brew', shareId: '1', lock: { locked: true, code: 404, shareMessage: 'brew locked' } };
|
||||
model.get = jest.fn(()=>toBrewPromise(lockBrew));
|
||||
api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
|
||||
|
||||
@@ -408,8 +409,8 @@ brew`);
|
||||
expect(sent).not.toEqual(googleBrew);
|
||||
expect(result.text).toBeUndefined();
|
||||
expect(result.textBin).toBeUndefined();
|
||||
expect(result.renderer).toBeUndefined();
|
||||
expect(result.pageCount).toBeUndefined();
|
||||
expect(result.renderer).toBe('v3');
|
||||
expect(result.pageCount).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -540,9 +541,9 @@ brew`);
|
||||
description : '',
|
||||
editId : expect.any(String),
|
||||
gDrive : false,
|
||||
pageCount : undefined,
|
||||
pageCount : 1,
|
||||
published : false,
|
||||
renderer : undefined,
|
||||
renderer : 'V3',
|
||||
lang : 'en',
|
||||
shareId : expect.any(String),
|
||||
googleId : expect.any(String),
|
||||
@@ -581,6 +582,121 @@ brew`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Theme bundle', ()=>{
|
||||
it('should return Theme Bundle for a User Theme', async ()=>{
|
||||
const brews = {
|
||||
userThemeAID : { title: 'User Theme A', renderer: 'V3', theme: null, shareId: 'userThemeAID', style: 'User Theme A Style' }
|
||||
};
|
||||
|
||||
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
|
||||
model.get = jest.fn((getParams)=>toBrewPromise(brews[getParams.shareId]));
|
||||
const req = { params: { renderer: 'V3', id: 'userThemeAID' }, get: ()=>{ return 'localhost'; }, protocol: 'https' };
|
||||
|
||||
await api.getThemeBundle(req, res);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(200);
|
||||
expect(res.send).toHaveBeenCalledWith({
|
||||
styles : ['/* From Brew: https://localhost/share/userThemeAID */\n\nUser Theme A Style'],
|
||||
snippets : []
|
||||
});
|
||||
});
|
||||
|
||||
it('should return Theme Bundle for nested User Themes', async ()=>{
|
||||
const brews = {
|
||||
userThemeAID : { title: 'User Theme A', renderer: 'V3', theme: 'userThemeBID', shareId: 'userThemeAID', style: 'User Theme A Style' },
|
||||
userThemeBID : { title: 'User Theme B', renderer: 'V3', theme: 'userThemeCID', shareId: 'userThemeBID', style: 'User Theme B Style' },
|
||||
userThemeCID : { title: 'User Theme C', renderer: 'V3', theme: null, shareId: 'userThemeCID', style: 'User Theme C Style' }
|
||||
};
|
||||
|
||||
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
|
||||
model.get = jest.fn((getParams)=>toBrewPromise(brews[getParams.shareId]));
|
||||
const req = { params: { renderer: 'V3', id: 'userThemeAID' }, get: ()=>{ return 'localhost'; }, protocol: 'https' };
|
||||
|
||||
await api.getThemeBundle(req, res);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(200);
|
||||
expect(res.send).toHaveBeenCalledWith({
|
||||
styles : [
|
||||
'/* From Brew: https://localhost/share/userThemeCID */\n\nUser Theme C Style',
|
||||
'/* From Brew: https://localhost/share/userThemeBID */\n\nUser Theme B Style',
|
||||
'/* From Brew: https://localhost/share/userThemeAID */\n\nUser Theme A Style'
|
||||
],
|
||||
snippets : []
|
||||
});
|
||||
});
|
||||
|
||||
it('should return Theme Bundle for a Static Theme', async ()=>{
|
||||
const req = { params: { renderer: 'V3', id: '5ePHB' }, get: ()=>{ return 'localhost'; }, protocol: 'https' };
|
||||
|
||||
await api.getThemeBundle(req, res);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(200);
|
||||
expect(res.send).toHaveBeenCalledWith({
|
||||
styles : [
|
||||
`/* From Theme Blank */\n\n@import url("/themes/V3/Blank/style.css");`,
|
||||
`/* From Theme 5ePHB */\n\n@import url("/themes/V3/5ePHB/style.css");`
|
||||
],
|
||||
snippets : [
|
||||
'V3_Blank',
|
||||
'V3_5ePHB'
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
it('should return Theme Bundle for nested User and Static Themes together', async ()=>{
|
||||
const brews = {
|
||||
userThemeAID : { title: 'User Theme A', renderer: 'V3', theme: 'userThemeBID', shareId: 'userThemeAID', style: 'User Theme A Style' },
|
||||
userThemeBID : { title: 'User Theme B', renderer: 'V3', theme: 'userThemeCID', shareId: 'userThemeBID', style: 'User Theme B Style' },
|
||||
userThemeCID : { title: 'User Theme C', renderer: 'V3', theme: '5eDMG', shareId: 'userThemeCID', style: 'User Theme C Style' }
|
||||
};
|
||||
|
||||
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
|
||||
model.get = jest.fn((getParams)=>toBrewPromise(brews[getParams.shareId]));
|
||||
const req = { params: { renderer: 'V3', id: 'userThemeAID' }, get: ()=>{ return 'localhost'; }, protocol: 'https' };
|
||||
|
||||
await api.getThemeBundle(req, res);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(200);
|
||||
expect(res.send).toHaveBeenCalledWith({
|
||||
styles : [
|
||||
`/* From Theme Blank */\n\n@import url("/themes/V3/Blank/style.css");`,
|
||||
`/* From Theme 5ePHB */\n\n@import url("/themes/V3/5ePHB/style.css");`,
|
||||
`/* From Theme 5eDMG */\n\n@import url("/themes/V3/5eDMG/style.css");`,
|
||||
'/* From Brew: https://localhost/share/userThemeCID */\n\nUser Theme C Style',
|
||||
'/* From Brew: https://localhost/share/userThemeBID */\n\nUser Theme B Style',
|
||||
'/* From Brew: https://localhost/share/userThemeAID */\n\nUser Theme A Style'
|
||||
],
|
||||
snippets : [
|
||||
'V3_Blank',
|
||||
'V3_5ePHB',
|
||||
'V3_5eDMG'
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail for an invalid Theme in the chain', async()=>{
|
||||
const brews = {
|
||||
userThemeAID : { title: 'User Theme A', renderer: 'V3', theme: 'missingTheme', shareId: 'userThemeAID', style: 'User Theme A Style' },
|
||||
};
|
||||
|
||||
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
|
||||
model.get = jest.fn((getParams)=>toBrewPromise(brews[getParams.shareId]));
|
||||
const req = { params: { renderer: 'V3', id: 'userThemeAID' }, get: ()=>{ return 'localhost'; }, protocol: 'https' };
|
||||
|
||||
let err;
|
||||
await api.getThemeBundle(req, res)
|
||||
.catch((e)=>err = e);
|
||||
|
||||
expect(err).toEqual({
|
||||
HBErrorCode : '09',
|
||||
accessType : 'share',
|
||||
brewId : 'missingTheme',
|
||||
message : 'Theme Not Found',
|
||||
name : 'ThemeLoad Error',
|
||||
status : 404 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteBrew', ()=>{
|
||||
it('should handle case where fetching the brew returns an error', async ()=>{
|
||||
api.getBrew = jest.fn(()=>async ()=>{ throw { message: 'err', HBErrorCode: '02' }; });
|
||||
@@ -801,4 +917,66 @@ brew`);
|
||||
expect(saved.googleId).toEqual(brew.googleId);
|
||||
});
|
||||
});
|
||||
describe('Get CSS', ()=>{
|
||||
it('should return brew style content as CSS text', async ()=>{
|
||||
const testBrew = { title: 'test brew', text: '```css\n\nI Have a style!\n````\n\n' };
|
||||
|
||||
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
|
||||
api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
|
||||
model.get = jest.fn(()=>toBrewPromise(testBrew));
|
||||
|
||||
const fn = api.getBrew('share', true);
|
||||
const req = { brew: {} };
|
||||
const next = jest.fn();
|
||||
await fn(req, null, next);
|
||||
await api.getCSS(req, res);
|
||||
|
||||
expect(req.brew).toEqual(testBrew);
|
||||
expect(req.brew).toHaveProperty('style', '\nI Have a style!\n');
|
||||
expect(res.status).toHaveBeenCalledWith(200);
|
||||
expect(res.send).toHaveBeenCalledWith("\nI Have a style!\n");
|
||||
expect(res.set).toHaveBeenCalledWith({
|
||||
'Cache-Control' : 'no-cache',
|
||||
'Content-Type' : 'text/css'
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 404 when brew has no style content', async ()=>{
|
||||
const testBrew = { title: 'test brew', text: 'I don\'t have a style!' };
|
||||
|
||||
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
|
||||
api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
|
||||
model.get = jest.fn(()=>toBrewPromise(testBrew));
|
||||
|
||||
const fn = api.getBrew('share', true);
|
||||
const req = { brew: {} };
|
||||
const next = jest.fn();
|
||||
await fn(req, null, next);
|
||||
await api.getCSS(req, res);
|
||||
|
||||
expect(req.brew).toEqual(testBrew);
|
||||
expect(req.brew).toHaveProperty('style');
|
||||
expect(res.status).toHaveBeenCalledWith(404);
|
||||
expect(res.send).toHaveBeenCalledWith('');
|
||||
});
|
||||
|
||||
it('should return 404 when brew does not exist', async ()=>{
|
||||
const testBrew = { };
|
||||
|
||||
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
|
||||
api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
|
||||
model.get = jest.fn(()=>toBrewPromise(testBrew));
|
||||
|
||||
const fn = api.getBrew('share', true);
|
||||
const req = { brew: {} };
|
||||
const next = jest.fn();
|
||||
await fn(req, null, next);
|
||||
await api.getCSS(req, res);
|
||||
|
||||
expect(req.brew).toEqual(testBrew);
|
||||
expect(req.brew).toHaveProperty('style');
|
||||
expect(res.status).toHaveBeenCalledWith(404);
|
||||
expect(res.send).toHaveBeenCalledWith('');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -50,8 +50,8 @@ HomebrewSchema.statics.get = async function(query, fields=null){
|
||||
return brew;
|
||||
};
|
||||
|
||||
HomebrewSchema.statics.getByUser = async function(username, allowAccess=false, fields=null){
|
||||
const query = { authors: username, published: true };
|
||||
HomebrewSchema.statics.getByUser = async function(username, allowAccess=false, fields=null, filter=null){
|
||||
const query = { authors: username, published: true, ...filter };
|
||||
if(allowAccess){
|
||||
delete query.published;
|
||||
}
|
||||
|
||||
@@ -1,22 +1,58 @@
|
||||
const _ = require('lodash');
|
||||
const yaml = require('js-yaml');
|
||||
const request = require('../client/homebrew/utils/request-middleware.js');
|
||||
|
||||
const splitTextStyleAndMetadata = (brew) => {
|
||||
brew.text = brew.text.replaceAll('\r\n', '\n');
|
||||
if (brew.text.startsWith('```metadata')) {
|
||||
const index = brew.text.indexOf('```\n\n');
|
||||
const metadataSection = brew.text.slice(12, index - 1);
|
||||
const metadata = yaml.load(metadataSection);
|
||||
Object.assign(brew, _.pick(metadata, ['title', 'description', 'tags', 'systems', 'renderer', 'theme', 'lang']));
|
||||
brew.text = brew.text.slice(index + 5);
|
||||
}
|
||||
if (brew.text.startsWith('```css')) {
|
||||
const index = brew.text.indexOf('```\n\n');
|
||||
brew.style = brew.text.slice(7, index - 1);
|
||||
brew.text = brew.text.slice(index + 5);
|
||||
}
|
||||
const splitTextStyleAndMetadata = (brew)=>{
|
||||
brew.text = brew.text.replaceAll('\r\n', '\n');
|
||||
if(brew.text.startsWith('```metadata')) {
|
||||
const index = brew.text.indexOf('```\n\n');
|
||||
const metadataSection = brew.text.slice(12, index - 1);
|
||||
const metadata = yaml.load(metadataSection);
|
||||
Object.assign(brew, _.pick(metadata, ['title', 'description', 'tags', 'systems', 'renderer', 'theme', 'lang']));
|
||||
brew.text = brew.text.slice(index + 5);
|
||||
}
|
||||
if(brew.text.startsWith('```css')) {
|
||||
const index = brew.text.indexOf('```\n\n');
|
||||
brew.style = brew.text.slice(7, index - 1);
|
||||
brew.text = brew.text.slice(index + 5);
|
||||
}
|
||||
if(brew.text.startsWith('```snippets')) {
|
||||
const index = brew.text.indexOf('```\n\n');
|
||||
brew.snippets = brew.text.slice(11, index - 1);
|
||||
brew.text = brew.text.slice(index + 5);
|
||||
}
|
||||
};
|
||||
|
||||
const printCurrentBrew = ()=>{
|
||||
if(window.typeof !== 'undefined') {
|
||||
window.frames['BrewRenderer'].contentWindow.print();
|
||||
//Force DOM reflow; Print dialog causes a repaint, and @media print CSS somehow makes out-of-view pages disappear
|
||||
const node = window.frames['BrewRenderer'].contentDocument.getElementsByClassName('brewRenderer').item(0);
|
||||
node.style.display='none';
|
||||
node.offsetHeight; // accessing this is enough to trigger a reflow
|
||||
node.style.display='';
|
||||
}
|
||||
};
|
||||
|
||||
const fetchThemeBundle = async (obj, renderer, theme)=>{
|
||||
if(!renderer || !theme) return;
|
||||
const res = await request
|
||||
.get(`/api/theme/${renderer}/${theme}`)
|
||||
.catch((err)=>{
|
||||
obj.setState({ error: err });
|
||||
});
|
||||
if(!res) return;
|
||||
|
||||
const themeBundle = res.body;
|
||||
themeBundle.joinedStyles = themeBundle.styles.map((style)=>`<style>${style}</style>`).join('\n\n');
|
||||
obj.setState((prevState)=>({
|
||||
...prevState,
|
||||
themeBundle : themeBundle
|
||||
}));
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
splitTextStyleAndMetadata
|
||||
splitTextStyleAndMetadata,
|
||||
printCurrentBrew,
|
||||
fetchThemeBundle,
|
||||
};
|
||||
|
||||
@@ -2,9 +2,8 @@ require('./renderWarnings.less');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
const cx = require('classnames');
|
||||
|
||||
const DISMISS_KEY = 'dismiss_render_warning';
|
||||
import Dialog from '../../../client/components/dialog.jsx';
|
||||
|
||||
const RenderWarnings = createClass({
|
||||
displayName : 'RenderWarnings',
|
||||
@@ -35,9 +34,6 @@ const RenderWarnings = createClass({
|
||||
},
|
||||
},
|
||||
checkWarnings : function(){
|
||||
const hideDismiss = localStorage.getItem(DISMISS_KEY);
|
||||
if(hideDismiss) return this.setState({ warnings: {} });
|
||||
|
||||
this.setState({
|
||||
warnings : _.reduce(this.warnings, (r, fn, type)=>{
|
||||
const element = fn();
|
||||
@@ -46,20 +42,18 @@ const RenderWarnings = createClass({
|
||||
}, {})
|
||||
});
|
||||
},
|
||||
dismiss : function(){
|
||||
localStorage.setItem(DISMISS_KEY, true);
|
||||
this.checkWarnings();
|
||||
},
|
||||
render : function(){
|
||||
if(_.isEmpty(this.state.warnings)) return null;
|
||||
|
||||
return <div className='renderWarnings'>
|
||||
<i className='fas fa-times dismiss' onClick={this.dismiss}/>
|
||||
const DISMISS_KEY = 'dismiss_render_warning';
|
||||
const DISMISS_TEXT = <i className='fas fa-times dismiss' />;
|
||||
|
||||
return <Dialog className='renderWarnings' dismissKey={DISMISS_KEY} closeText={DISMISS_TEXT}>
|
||||
<i className='fas fa-exclamation-triangle ohno' />
|
||||
<h3>Render Warnings</h3>
|
||||
<small>If this homebrew is rendering badly if might be because of the following:</small>
|
||||
<ul>{_.values(this.state.warnings)}</ul>
|
||||
</div>;
|
||||
</Dialog>;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,53 +1,48 @@
|
||||
.renderWarnings{
|
||||
position : relative;
|
||||
float : right;
|
||||
display : inline-block;
|
||||
.renderWarnings {
|
||||
position : relative;
|
||||
float : right;
|
||||
width : 350px;
|
||||
padding : 20px;
|
||||
padding-bottom : 10px;
|
||||
padding-left : 85px;
|
||||
margin-bottom : 10px;
|
||||
background-color : @yellow;
|
||||
color : white;
|
||||
a{
|
||||
font-weight : 800;
|
||||
}
|
||||
i.ohno{
|
||||
background-color : @yellow;
|
||||
border : none;
|
||||
a { font-weight : 800; }
|
||||
i.ohno {
|
||||
position : absolute;
|
||||
top : 24px;
|
||||
left : 24px;
|
||||
opacity : 0.8;
|
||||
font-size : 2.5em;
|
||||
opacity : 0.8;
|
||||
}
|
||||
i.dismiss{
|
||||
position : absolute;
|
||||
top : 10px;
|
||||
right : 10px;
|
||||
cursor : pointer;
|
||||
opacity : 0.6;
|
||||
&:hover{
|
||||
opacity : 1;
|
||||
}
|
||||
button.dismiss {
|
||||
position : absolute;
|
||||
top : 10px;
|
||||
right : 10px;
|
||||
cursor : pointer;
|
||||
background-color : transparent;
|
||||
opacity : 0.6;
|
||||
&:hover { opacity : 1; }
|
||||
}
|
||||
small{
|
||||
opacity : 0.7;
|
||||
small {
|
||||
font-size : 0.6em;
|
||||
opacity : 0.7;
|
||||
}
|
||||
h3{
|
||||
h3 {
|
||||
font-size : 1.1em;
|
||||
font-weight : 800;
|
||||
}
|
||||
ul{
|
||||
ul {
|
||||
margin-top : 15px;
|
||||
font-size : 0.8em;
|
||||
list-style-position : outside;
|
||||
list-style-type : disc;
|
||||
li{
|
||||
li {
|
||||
font-size : 0.8em;
|
||||
line-height : 1.6em;
|
||||
em{
|
||||
font-weight : 800;
|
||||
}
|
||||
em { font-weight : 800; }
|
||||
}
|
||||
}
|
||||
}
|
||||
84
shared/naturalcrit/codeEditor/autocompleteEmoji.js
Normal file
84
shared/naturalcrit/codeEditor/autocompleteEmoji.js
Normal file
@@ -0,0 +1,84 @@
|
||||
const diceFont = require('../../../themes/fonts/iconFonts/diceFont.js');
|
||||
const elderberryInn = require('../../../themes/fonts/iconFonts/elderberryInn.js');
|
||||
const fontAwesome = require('../../../themes/fonts/iconFonts/fontAwesome.js');
|
||||
const gameIcons = require('../../../themes/fonts/iconFonts/gameIcons.js');
|
||||
|
||||
const emojis = {
|
||||
...diceFont,
|
||||
...elderberryInn,
|
||||
...fontAwesome,
|
||||
...gameIcons
|
||||
};
|
||||
|
||||
const showAutocompleteEmoji = function(CodeMirror, editor) {
|
||||
CodeMirror.commands.autocomplete = function(editor) {
|
||||
editor.showHint({
|
||||
completeSingle : false,
|
||||
hint : function(editor) {
|
||||
const cursor = editor.getCursor();
|
||||
const line = cursor.line;
|
||||
const lineContent = editor.getLine(line);
|
||||
const start = lineContent.lastIndexOf(':', cursor.ch - 1) + 1;
|
||||
const end = cursor.ch;
|
||||
const currentWord = lineContent.slice(start, end);
|
||||
|
||||
|
||||
const list = Object.keys(emojis).filter(function(emoji) {
|
||||
return emoji.toLowerCase().indexOf(currentWord.toLowerCase()) >= 0;
|
||||
}).sort((a, b)=>{
|
||||
const lowerA = a.replace(/\d+/g, function(match) { // Temporarily convert any numbers in emoji string
|
||||
return match.padStart(4, '0'); // to 4-digits, left-padded with 0's, to aid in
|
||||
}).toLowerCase(); // sorting numbers, i.e., "d6, d10, d20", not "d10, d20, d6"
|
||||
const lowerB = b.replace(/\d+/g, function(match) { // Also make lowercase for case-insensitive alpha sorting
|
||||
return match.padStart(4, '0');
|
||||
}).toLowerCase();
|
||||
|
||||
if(lowerA < lowerB)
|
||||
return -1;
|
||||
return 1;
|
||||
}).map(function(emoji) {
|
||||
return {
|
||||
text : `${emoji}:`, // Text to output to editor when option is selected
|
||||
render : function(element, self, data) { // How to display the option in the dropdown
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = `<i class="emojiPreview ${emojis[emoji]}"></i> ${emoji}`;
|
||||
element.appendChild(div);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
list : list.length ? list : [],
|
||||
from : CodeMirror.Pos(line, start),
|
||||
to : CodeMirror.Pos(line, end)
|
||||
};
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
editor.on('inputRead', function(instance, change) {
|
||||
const cursor = editor.getCursor();
|
||||
const line = editor.getLine(cursor.line);
|
||||
|
||||
// Get the text from the start of the line to the cursor
|
||||
const textToCursor = line.slice(0, cursor.ch);
|
||||
|
||||
// Do not autosuggest emojis in curly span/div/injector properties
|
||||
if(line.includes('{')) {
|
||||
const curlyToCursor = textToCursor.slice(textToCursor.indexOf(`{`));
|
||||
const curlySpanRegex = /{(?=((?:[:=](?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':={}\s]*)*))\1$/g;
|
||||
|
||||
if(curlySpanRegex.test(curlyToCursor))
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the text ends with ':xyz'
|
||||
if(/:[^\s:]+$/.test(textToCursor)) {
|
||||
CodeMirror.commands.autocomplete(editor);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
showAutocompleteEmoji
|
||||
};
|
||||
@@ -3,8 +3,8 @@ require('./codeEditor.less');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
const cx = require('classnames');
|
||||
const closeTag = require('./close-tag');
|
||||
const autoCompleteEmoji = require('./autocompleteEmoji');
|
||||
|
||||
let CodeMirror;
|
||||
if(typeof window !== 'undefined'){
|
||||
@@ -36,9 +36,13 @@ if(typeof window !== 'undefined'){
|
||||
//XML code folding is a requirement of the auto-closing tag feature and is not enabled
|
||||
require('codemirror/addon/fold/xml-fold.js');
|
||||
require('codemirror/addon/edit/closetag.js');
|
||||
//Autocompletion
|
||||
require('codemirror/addon/hint/show-hint.js');
|
||||
|
||||
const foldCode = require('./fold-code');
|
||||
foldCode.registerHomebreweryHelper(CodeMirror);
|
||||
const foldPagesCode = require('./fold-pages');
|
||||
foldPagesCode.registerHomebreweryHelper(CodeMirror);
|
||||
const foldCSSCode = require('./fold-css');
|
||||
foldCSSCode.registerHomebreweryHelper(CodeMirror);
|
||||
}
|
||||
|
||||
const CodeEditor = createClass({
|
||||
@@ -60,6 +64,8 @@ const CodeEditor = createClass({
|
||||
};
|
||||
},
|
||||
|
||||
editor : React.createRef(null),
|
||||
|
||||
componentDidMount : function() {
|
||||
this.buildEditor();
|
||||
const newDoc = CodeMirror.Doc(this.props.value, this.props.language);
|
||||
@@ -99,7 +105,7 @@ const CodeEditor = createClass({
|
||||
},
|
||||
|
||||
buildEditor : function() {
|
||||
this.codeMirror = CodeMirror(this.refs.editor, {
|
||||
this.codeMirror = CodeMirror(this.editor.current, {
|
||||
lineNumbers : true,
|
||||
lineWrapping : this.props.wrap,
|
||||
indentWithTabs : false,
|
||||
@@ -177,7 +183,10 @@ const CodeEditor = createClass({
|
||||
// return el;
|
||||
// }
|
||||
});
|
||||
|
||||
// Add custom behaviors (auto-close curlies and auto-complete emojis)
|
||||
closeTag.autoCloseCurlyBraces(CodeMirror, this.codeMirror);
|
||||
autoCompleteEmoji.showAutocompleteEmoji(CodeMirror, this.codeMirror);
|
||||
|
||||
// Note: codeMirror passes a copy of itself in this callback. cm === this.codeMirror. Either one works.
|
||||
this.codeMirror.on('change', (cm)=>{this.props.onChange(cm.getValue());});
|
||||
@@ -404,11 +413,11 @@ const CodeEditor = createClass({
|
||||
foldOptions : function(cm){
|
||||
return {
|
||||
scanUp : true,
|
||||
rangeFinder : CodeMirror.fold.homebrewery,
|
||||
rangeFinder : this.props.language === 'css' ? CodeMirror.fold.homebrewerycss : CodeMirror.fold.homebrewery,
|
||||
widget : (from, to)=>{
|
||||
let text = '';
|
||||
let currentLine = from.line;
|
||||
const maxLength = 50;
|
||||
let maxLength = 50;
|
||||
|
||||
let foldPreviewText = '';
|
||||
while (currentLine <= to.line && text.length <= maxLength) {
|
||||
@@ -423,10 +432,15 @@ const CodeEditor = createClass({
|
||||
}
|
||||
}
|
||||
text = foldPreviewText || `Lines ${from.line+1}-${to.line+1}`;
|
||||
text = text.replace('{', '').trim();
|
||||
|
||||
// Truncate data URLs at `data:`
|
||||
const startOfData = text.indexOf('data:');
|
||||
if(startOfData > 0)
|
||||
maxLength = Math.min(startOfData + 5, maxLength);
|
||||
|
||||
text = text.trim();
|
||||
if(text.length > maxLength)
|
||||
text = `${text.substr(0, maxLength)}...`;
|
||||
text = `${text.slice(0, maxLength)}...`;
|
||||
|
||||
return `\u21A4 ${text} \u21A6`;
|
||||
}
|
||||
@@ -436,10 +450,11 @@ const CodeEditor = createClass({
|
||||
|
||||
render : function(){
|
||||
return <>
|
||||
<link href={`../homebrew/cm-themes/${this.props.editorTheme}.css`} type="text/css" rel='stylesheet' />
|
||||
<div className='codeEditor' ref='editor' style={this.props.style}/>
|
||||
<link href={`../homebrew/cm-themes/${this.props.editorTheme}.css`} type='text/css' rel='stylesheet' />
|
||||
<div className='codeEditor' ref={this.editor} style={this.props.style}/>
|
||||
</>;
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = CodeEditor;
|
||||
|
||||
|
||||
@@ -2,6 +2,13 @@
|
||||
@import (less) 'codemirror/addon/fold/foldgutter.css';
|
||||
@import (less) 'codemirror/addon/search/matchesonscrollbar.css';
|
||||
@import (less) 'codemirror/addon/dialog/dialog.css';
|
||||
@import (less) 'codemirror/addon/hint/show-hint.css';
|
||||
|
||||
//Icon fonts included so they can appear in emoji autosuggest dropdown
|
||||
@import (less) './themes/fonts/iconFonts/diceFont.less';
|
||||
@import (less) './themes/fonts/iconFonts/elderberryInn.less';
|
||||
@import (less) './themes/fonts/iconFonts/gameIcons.less';
|
||||
@import (less) './themes/fonts/iconFonts/fontAwesome.less';
|
||||
|
||||
@keyframes sourceMoveAnimation {
|
||||
50% {background-color: red; color: white;}
|
||||
@@ -34,6 +41,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//.cm-tab {
|
||||
// background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAMCAQAAACOs/baAAAARUlEQVR4nGJgIAG8JkXxUAcCtDWemcGR1lY4MvgzCEKY7jSBjgxBDAG09UEQzAe0AMwMHrSOAwEGRtpaMIwAAAAA//8DAG4ID9EKs6YqAAAAAElFTkSuQmCC) no-repeat right;
|
||||
//}
|
||||
@@ -44,3 +52,8 @@
|
||||
// }
|
||||
//}
|
||||
}
|
||||
|
||||
.emojiPreview {
|
||||
font-size: 1.5em;
|
||||
line-height: 1.2em;
|
||||
}
|
||||
44
shared/naturalcrit/codeEditor/fold-css.js
Normal file
44
shared/naturalcrit/codeEditor/fold-css.js
Normal file
@@ -0,0 +1,44 @@
|
||||
module.exports = {
|
||||
registerHomebreweryHelper : function(CodeMirror) {
|
||||
CodeMirror.registerHelper('fold', 'homebrewerycss', function(cm, start) {
|
||||
|
||||
// BRACE FOLDING
|
||||
const startMatcher = /\{[ \t]*$/;
|
||||
const endMatcher = /\}[ \t]*$/;
|
||||
const activeLine = cm.getLine(start.line);
|
||||
|
||||
|
||||
if(activeLine.match(startMatcher)) {
|
||||
const lastLineNo = cm.lastLine();
|
||||
let end = start.line + 1;
|
||||
let braceCount = 1;
|
||||
|
||||
while (end < lastLineNo) {
|
||||
const curLine = cm.getLine(end);
|
||||
if(curLine.match(startMatcher)) braceCount++;
|
||||
if(curLine.match(endMatcher)) braceCount--;
|
||||
if(braceCount == 0) break;
|
||||
++end;
|
||||
}
|
||||
|
||||
return {
|
||||
from : CodeMirror.Pos(start.line, 0),
|
||||
to : CodeMirror.Pos(end, cm.getLine(end).length)
|
||||
};
|
||||
}
|
||||
|
||||
// @import and data-url folding
|
||||
const importMatcher = /^@import.*?;/;
|
||||
const dataURLMatcher = /url\(.*?data\:.*\)/;
|
||||
|
||||
if(activeLine.match(importMatcher) || activeLine.match(dataURLMatcher)) {
|
||||
return {
|
||||
from : CodeMirror.Pos(start.line, 0),
|
||||
to : CodeMirror.Pos(start.line, activeLine.length)
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -3,7 +3,15 @@ const _ = require('lodash');
|
||||
const Marked = require('marked');
|
||||
const MarkedExtendedTables = require('marked-extended-tables');
|
||||
const { markedSmartypantsLite: MarkedSmartypantsLite } = require('marked-smartypants-lite');
|
||||
const { gfmHeadingId: MarkedGFMHeadingId } = require('marked-gfm-heading-id');
|
||||
const { gfmHeadingId: MarkedGFMHeadingId, resetHeadings: MarkedGFMResetHeadingIDs } = require('marked-gfm-heading-id');
|
||||
const { markedEmoji: MarkedEmojis } = require('marked-emoji');
|
||||
|
||||
//Icon fonts included so they can appear in emoji autosuggest dropdown
|
||||
const diceFont = require('../../themes/fonts/iconFonts/diceFont.js');
|
||||
const elderberryInn = require('../../themes/fonts/iconFonts/elderberryInn.js');
|
||||
const fontAwesome = require('../../themes/fonts/iconFonts/fontAwesome.js');
|
||||
const gameIcons = require('../../themes/fonts/iconFonts/gameIcons.js');
|
||||
|
||||
const MathParser = require('expr-eval').Parser;
|
||||
const renderer = new Marked.Renderer();
|
||||
const tokenizer = new Marked.Tokenizer();
|
||||
@@ -20,17 +28,18 @@ const mathParser = new MathParser({
|
||||
round : true,
|
||||
floor : true,
|
||||
ceil : true,
|
||||
abs : true,
|
||||
|
||||
sin : false, cos : false, tan : false, asin : false, acos : false,
|
||||
atan : false, sinh : false, cosh : false, tanh : false, asinh : false,
|
||||
acosh : false, atanh : false, sqrt : false, cbrt : false, log : false,
|
||||
log2 : false, ln : false, lg : false, log10 : false, expm1 : false,
|
||||
log1p : false, abs : false, trunc : false, join : false, sum : false,
|
||||
log1p : false, trunc : false, join : false, sum : false, indexOf : false,
|
||||
'-' : false, '+' : false, exp : false, not : false, length : false,
|
||||
'!' : false, sign : false, random : false, fac : false, min : false,
|
||||
max : false, hypot : false, pyt : false, pow : false, atan2 : false,
|
||||
'if' : false, gamma : false, roundTo : false, map : false, fold : false,
|
||||
filter : false, indexOf : false,
|
||||
filter : false,
|
||||
|
||||
remainder : false, factorial : false,
|
||||
comparison : false, concatenate : false,
|
||||
@@ -38,6 +47,16 @@ const mathParser = new MathParser({
|
||||
array : false, fndef : false
|
||||
}
|
||||
});
|
||||
// Add sign function
|
||||
mathParser.functions.sign = function (a) {
|
||||
if(a >= 0) return '+';
|
||||
return '-';
|
||||
};
|
||||
// Add signed function
|
||||
mathParser.functions.signed = function (a) {
|
||||
if(a >= 0) return `+${a}`;
|
||||
return `${a}`;
|
||||
};
|
||||
|
||||
//Processes the markdown within an HTML block if it's just a class-wrapper
|
||||
renderer.html = function (html) {
|
||||
@@ -50,7 +69,7 @@ renderer.html = function (html) {
|
||||
return html;
|
||||
};
|
||||
|
||||
// Don't wrap {{ Divs or {{ empty Spans in <p> tags
|
||||
// Don't wrap {{ Spans alone on a line, or {{ Divs in <p> tags
|
||||
renderer.paragraph = function(text){
|
||||
let match;
|
||||
if(text.startsWith('<div') || text.startsWith('</div'))
|
||||
@@ -67,7 +86,7 @@ renderer.link = function (href, title, text) {
|
||||
if(href[0] == '#') {
|
||||
self = true;
|
||||
}
|
||||
href = cleanUrl(this.options.sanitize, this.options.baseUrl, href);
|
||||
href = cleanUrl(href);
|
||||
|
||||
if(href === null) {
|
||||
return text;
|
||||
@@ -94,18 +113,18 @@ const mustacheSpans = {
|
||||
start(src) { return src.match(/{{[^{]/)?.index; }, // Hint to Marked.js to stop and check for a match
|
||||
tokenizer(src, tokens) {
|
||||
const completeSpan = /^{{[^\n]*}}/; // Regex for the complete token
|
||||
const inlineRegex = /{{(?=((?:[:=](?:"['\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1 *|}}/g;
|
||||
const inlineRegex = /{{(?=((?:[:=](?:"['\w,\-()#%=?. ]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1 *|}}/g;
|
||||
const match = completeSpan.exec(src);
|
||||
if(match) {
|
||||
//Find closing delimiter
|
||||
let blockCount = 0;
|
||||
let tags = '';
|
||||
let tags = {};
|
||||
let endTags = 0;
|
||||
let endToken = 0;
|
||||
let delim;
|
||||
while (delim = inlineRegex.exec(match[0])) {
|
||||
if(!tags) {
|
||||
tags = `${processStyleTags(delim[0].substring(2))}`;
|
||||
if(_.isEmpty(tags)) {
|
||||
tags = processStyleTags(delim[0].substring(2));
|
||||
endTags = delim[0].length;
|
||||
}
|
||||
if(delim[0].startsWith('{{')) {
|
||||
@@ -134,7 +153,14 @@ const mustacheSpans = {
|
||||
}
|
||||
},
|
||||
renderer(token) {
|
||||
return `<span class="inline-block${token.tags}>${this.parser.parseInline(token.tokens)}</span>`; // parseInline to turn child tokens into HTML
|
||||
const tags = token.tags;
|
||||
tags.classes = ['inline-block', tags.classes].join(' ').trim();
|
||||
return `<span` +
|
||||
`${tags.classes ? ` class="${tags.classes}"` : ''}` +
|
||||
`${tags.id ? ` id="${tags.id}"` : ''}` +
|
||||
`${tags.styles ? ` style="${tags.styles}"` : ''}` +
|
||||
`${tags.attributes ? ` ${Object.entries(tags.attributes).map(([key, value])=>`${key}="${value}"`).join(' ')}` : ''}` +
|
||||
`>${this.parser.parseInline(token.tokens)}</span>`; // parseInline to turn child tokens into HTML
|
||||
}
|
||||
};
|
||||
|
||||
@@ -144,18 +170,18 @@ const mustacheDivs = {
|
||||
start(src) { return src.match(/\n *{{[^{]/m)?.index; }, // Hint to Marked.js to stop and check for a match
|
||||
tokenizer(src, tokens) {
|
||||
const completeBlock = /^ *{{[^\n}]* *\n.*\n *}}/s; // Regex for the complete token
|
||||
const blockRegex = /^ *{{(?=((?:[:=](?:"['\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1 *$|^ *}}$/gm;
|
||||
const blockRegex = /^ *{{(?=((?:[:=](?:"['\w,\-()#%=?. ]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1 *$|^ *}}$/gm;
|
||||
const match = completeBlock.exec(src);
|
||||
if(match) {
|
||||
//Find closing delimiter
|
||||
let blockCount = 0;
|
||||
let tags = '';
|
||||
let tags = {};
|
||||
let endTags = 0;
|
||||
let endToken = 0;
|
||||
let delim;
|
||||
while (delim = blockRegex.exec(match[0])?.[0].trim()) {
|
||||
if(!tags) {
|
||||
tags = `${processStyleTags(delim.substring(2))}`;
|
||||
if(_.isEmpty(tags)) {
|
||||
tags = processStyleTags(delim.substring(2));
|
||||
endTags = delim.length + src.indexOf(delim);
|
||||
}
|
||||
if(delim.startsWith('{{')) {
|
||||
@@ -183,7 +209,14 @@ const mustacheDivs = {
|
||||
}
|
||||
},
|
||||
renderer(token) {
|
||||
return `<div class="block${token.tags}>${this.parser.parse(token.tokens)}</div>`; // parseInline to turn child tokens into HTML
|
||||
const tags = token.tags;
|
||||
tags.classes = ['block', tags.classes].join(' ').trim();
|
||||
return `<div` +
|
||||
`${tags.classes ? ` class="${tags.classes}"` : ''}` +
|
||||
`${tags.id ? ` id="${tags.id}"` : ''}` +
|
||||
`${tags.styles ? ` style="${tags.styles}"` : ''}` +
|
||||
`${tags.attributes ? ` ${Object.entries(tags.attributes).map(([key, value])=>`${key}="${value}"`).join(' ')}` : ''}` +
|
||||
`>${this.parser.parse(token.tokens)}</div>`; // parse to turn child tokens into HTML
|
||||
}
|
||||
};
|
||||
|
||||
@@ -192,30 +225,46 @@ const mustacheInjectInline = {
|
||||
level : 'inline',
|
||||
start(src) { return src.match(/ *{[^{\n]/)?.index; }, // Hint to Marked.js to stop and check for a match
|
||||
tokenizer(src, tokens) {
|
||||
const inlineRegex = /^ *{(?=((?:[:=](?:"['\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1}/g;
|
||||
const inlineRegex = /^ *{(?=((?:[:=](?:"['\w,\-()#%=?. ]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1}/g;
|
||||
const match = inlineRegex.exec(src);
|
||||
if(match) {
|
||||
const lastToken = tokens[tokens.length - 1];
|
||||
if(!lastToken || lastToken.type == 'mustacheInjectInline')
|
||||
return false;
|
||||
|
||||
const tags = `${processStyleTags(match[1])}`;
|
||||
const tags = processStyleTags(match[1]);
|
||||
lastToken.originalType = lastToken.type;
|
||||
lastToken.type = 'mustacheInjectInline';
|
||||
lastToken.tags = tags;
|
||||
lastToken.injectedTags = tags;
|
||||
return {
|
||||
type : 'text', // Should match "name" above
|
||||
type : 'mustacheInjectInline', // Should match "name" above
|
||||
raw : match[0], // Text to consume from the source
|
||||
text : ''
|
||||
};
|
||||
}
|
||||
},
|
||||
renderer(token) {
|
||||
if(!token.originalType){
|
||||
return;
|
||||
}
|
||||
token.type = token.originalType;
|
||||
const text = this.parser.parseInline([token]);
|
||||
const openingTag = /(<[^\s<>]+)([^\n<>]*>.*)/s.exec(text);
|
||||
const originalTags = extractHTMLStyleTags(text);
|
||||
const injectedTags = token.injectedTags;
|
||||
const tags = {
|
||||
id : injectedTags.id || originalTags.id || null,
|
||||
classes : [originalTags.classes, injectedTags.classes].join(' ').trim() || null,
|
||||
styles : [originalTags.styles, injectedTags.styles].join(' ').trim() || null,
|
||||
attributes : Object.assign(originalTags.attributes ?? {}, injectedTags.attributes ?? {})
|
||||
};
|
||||
const openingTag = /(<[^\s<>]+)[^\n<>]*(>.*)/s.exec(text);
|
||||
if(openingTag) {
|
||||
return `${openingTag[1]} class="${token.tags}${openingTag[2]}`;
|
||||
return `${openingTag[1]}` +
|
||||
`${tags.classes ? ` class="${tags.classes}"` : ''}` +
|
||||
`${tags.id ? ` id="${tags.id}"` : ''}` +
|
||||
`${tags.styles ? ` style="${tags.styles}"` : ''}` +
|
||||
`${!_.isEmpty(tags.attributes) ? ` ${Object.entries(tags.attributes).map(([key, value])=>`${key}="${value}"`).join(' ')}` : ''}` +
|
||||
`${openingTag[2]}`; // parse to turn child tokens into HTML
|
||||
}
|
||||
return text;
|
||||
}
|
||||
@@ -227,7 +276,7 @@ const mustacheInjectBlock = {
|
||||
level : 'block',
|
||||
start(src) { return src.match(/\n *{[^{\n]/m)?.index; }, // Hint to Marked.js to stop and check for a match
|
||||
tokenizer(src, tokens) {
|
||||
const inlineRegex = /^ *{(?=((?:[:=](?:"['\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1}/ym;
|
||||
const inlineRegex = /^ *{(?=((?:[:=](?:"['\w,\-()#%=?. ]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1}/ym;
|
||||
const match = inlineRegex.exec(src);
|
||||
if(match) {
|
||||
const lastToken = tokens[tokens.length - 1];
|
||||
@@ -235,7 +284,7 @@ const mustacheInjectBlock = {
|
||||
return false;
|
||||
|
||||
lastToken.originalType = 'mustacheInjectBlock';
|
||||
lastToken.tags = `${processStyleTags(match[1])}`;
|
||||
lastToken.injectedTags = processStyleTags(match[1]);
|
||||
return {
|
||||
type : 'mustacheInjectBlock', // Should match "name" above
|
||||
raw : match[0], // Text to consume from the source
|
||||
@@ -249,9 +298,22 @@ const mustacheInjectBlock = {
|
||||
}
|
||||
token.type = token.originalType;
|
||||
const text = this.parser.parse([token]);
|
||||
const openingTag = /(<[^\s<>]+)([^\n<>]*>.*)/s.exec(text);
|
||||
const originalTags = extractHTMLStyleTags(text);
|
||||
const injectedTags = token.injectedTags;
|
||||
const tags = {
|
||||
id : injectedTags.id || originalTags.id || null,
|
||||
classes : [originalTags.classes, injectedTags.classes].join(' ').trim() || null,
|
||||
styles : [originalTags.styles, injectedTags.styles].join(' ').trim() || null,
|
||||
attributes : Object.assign(originalTags.attributes ?? {}, injectedTags.attributes ?? {})
|
||||
};
|
||||
const openingTag = /(<[^\s<>]+)[^\n<>]*(>.*)/s.exec(text);
|
||||
if(openingTag) {
|
||||
return `${openingTag[1]} class="${token.tags}${openingTag[2]}`;
|
||||
return `${openingTag[1]}` +
|
||||
`${tags.classes ? ` class="${tags.classes}"` : ''}` +
|
||||
`${tags.id ? ` id="${tags.id}"` : ''}` +
|
||||
`${tags.styles ? ` style="${tags.styles}"` : ''}` +
|
||||
`${!_.isEmpty(tags.attributes) ? ` ${Object.entries(tags.attributes).map(([key, value])=>`${key}="${value}"`).join(' ')}` : ''}` +
|
||||
`${openingTag[2]}`; // parse to turn child tokens into HTML
|
||||
}
|
||||
return text;
|
||||
}
|
||||
@@ -294,6 +356,27 @@ const superSubScripts = {
|
||||
}
|
||||
};
|
||||
|
||||
const forcedParagraphBreaks = {
|
||||
name : 'hardBreaks',
|
||||
level : 'block',
|
||||
start(src) { return src.match(/\n:+$/m)?.index; }, // Hint to Marked.js to stop and check for a match
|
||||
tokenizer(src, tokens) {
|
||||
const regex = /^(:+)(?:\n|$)/ym;
|
||||
const match = regex.exec(src);
|
||||
if(match?.length) {
|
||||
return {
|
||||
type : 'hardBreaks', // Should match "name" above
|
||||
raw : match[0], // Text to consume from the source
|
||||
length : match[1].length,
|
||||
text : ''
|
||||
};
|
||||
}
|
||||
},
|
||||
renderer(token) {
|
||||
return `<div class='blank'></div>`.repeat(token.length).concat('\n');
|
||||
}
|
||||
};
|
||||
|
||||
const definitionListsSingleLine = {
|
||||
name : 'definitionListsSingleLine',
|
||||
level : 'block',
|
||||
@@ -304,10 +387,19 @@ const definitionListsSingleLine = {
|
||||
let endIndex = 0;
|
||||
const definitions = [];
|
||||
while (match = regex.exec(src)) {
|
||||
definitions.push({
|
||||
dt : this.lexer.inlineTokens(match[1].trim()),
|
||||
dd : this.lexer.inlineTokens(match[2].trim())
|
||||
});
|
||||
const originalLine = match[0]; // This line and below to handle conflict with emojis
|
||||
let firstLine = originalLine; // Remove in V4 when definitionListsInline updated to
|
||||
this.lexer.inlineTokens(firstLine.trim()) // require spaces around `::`
|
||||
.filter((t)=>t.type == 'emoji')
|
||||
.map((emoji)=>firstLine = firstLine.replace(emoji.raw, 'x'.repeat(emoji.raw.length)));
|
||||
|
||||
const newMatch = /^([^\n]*?)::([^\n]*)(?:\n|$)/ym.exec(firstLine);
|
||||
if(newMatch) {
|
||||
definitions.push({
|
||||
dt : this.lexer.inlineTokens(originalLine.slice(0, newMatch[1].length).trim()),
|
||||
dd : this.lexer.inlineTokens(originalLine.slice(newMatch[1].length + 2).trim())
|
||||
});
|
||||
} // End of emoji hack.
|
||||
endIndex = regex.lastIndex;
|
||||
}
|
||||
if(definitions.length) {
|
||||
@@ -329,9 +421,9 @@ const definitionListsSingleLine = {
|
||||
const definitionListsMultiLine = {
|
||||
name : 'definitionListsMultiLine',
|
||||
level : 'block',
|
||||
start(src) { return src.match(/\n[^\n]*\n::/m)?.index; }, // Hint to Marked.js to stop and check for a match
|
||||
start(src) { return src.match(/\n[^\n]*\n::[^:\n]/m)?.index; }, // Hint to Marked.js to stop and check for a match
|
||||
tokenizer(src, tokens) {
|
||||
const regex = /(\n?\n?(?!::)[^\n]+?(?=\n::))|\n::(.(?:.|\n)*?(?=(?:\n::)|(?:\n\n)|$))/y;
|
||||
const regex = /(\n?\n?(?!::)[^\n]+?(?=\n::[^:\n]))|\n::([^:\n](?:.|\n)*?(?=(?:\n::)|(?:\n\n)|$))/y;
|
||||
let match;
|
||||
let endIndex = 0;
|
||||
const definitions = [];
|
||||
@@ -381,7 +473,7 @@ const replaceVar = function(input, hoist=false, allowUnresolved=false) {
|
||||
const label = match[2];
|
||||
|
||||
//v=====--------------------< HANDLE MATH >-------------------=====v//
|
||||
const mathRegex = /[a-z]+\(|[+\-*/^()]/g;
|
||||
const mathRegex = /[a-z]+\(|[+\-*/^(),]/g;
|
||||
const matches = label.split(mathRegex);
|
||||
const mathVars = matches.filter((match)=>isNaN(match))?.map((s)=>s.trim()); // Capture any variable names
|
||||
|
||||
@@ -391,7 +483,7 @@ const replaceVar = function(input, hoist=false, allowUnresolved=false) {
|
||||
mathVars?.forEach((variable)=>{
|
||||
const foundVar = lookupVar(variable, globalPageNumber, hoist);
|
||||
if(foundVar && foundVar.resolved && foundVar.content && !isNaN(foundVar.content)) // Only subsitute math values if fully resolved, not empty strings, and numbers
|
||||
replacedLabel = replacedLabel.replaceAll(variable, foundVar.content);
|
||||
replacedLabel = replacedLabel.replaceAll(new RegExp(`(?<!\\w)(${variable})(?!\\w)`, 'g'), foundVar.content);
|
||||
});
|
||||
|
||||
try {
|
||||
@@ -616,34 +708,40 @@ function MarkedVariables() {
|
||||
};
|
||||
//^=====--------------------< Variable Handling >-------------------=====^//
|
||||
|
||||
// Emoji options
|
||||
// To add more icon fonts, need to do these things
|
||||
// 1) Add the font file as .woff2 to themes/fonts/iconFonts folder
|
||||
// 2) Create a .less file mapping CSS class names to the font character
|
||||
// 3) Create a .js file mapping Autosuggest names to CSS class names
|
||||
// 4) Import the .less file into shared/naturalcrit/codeEditor/codeEditor.less
|
||||
// 5) Import the .less file into themes/V3/blank.style.less
|
||||
// 6) Import the .js file to shared/naturalcrit/codeEditor/autocompleteEmoji.js and add to `emojis` object
|
||||
// 7) Import the .js file here to markdown.js, and add to `emojis` object below
|
||||
const MarkedEmojiOptions = {
|
||||
emojis : {
|
||||
...diceFont,
|
||||
...elderberryInn,
|
||||
...fontAwesome,
|
||||
...gameIcons,
|
||||
},
|
||||
renderer : (token)=>`<i class="${token.emoji}"></i>`
|
||||
};
|
||||
|
||||
Marked.use(MarkedVariables());
|
||||
Marked.use({ extensions: [definitionListsMultiLine, definitionListsSingleLine, superSubScripts, mustacheSpans, mustacheDivs, mustacheInjectInline] });
|
||||
Marked.use({ extensions : [definitionListsMultiLine, definitionListsSingleLine, forcedParagraphBreaks, superSubScripts,
|
||||
mustacheSpans, mustacheDivs, mustacheInjectInline] });
|
||||
Marked.use(mustacheInjectBlock);
|
||||
Marked.use({ renderer: renderer, tokenizer: tokenizer, mangle: false });
|
||||
Marked.use(MarkedExtendedTables(), MarkedGFMHeadingId(), MarkedSmartypantsLite());
|
||||
Marked.use(MarkedExtendedTables(), MarkedGFMHeadingId({ globalSlugs: true }), MarkedSmartypantsLite(), MarkedEmojis(MarkedEmojiOptions));
|
||||
|
||||
const nonWordAndColonTest = /[^\w:]/g;
|
||||
const cleanUrl = function (sanitize, base, href) {
|
||||
if(sanitize) {
|
||||
let prot;
|
||||
try {
|
||||
prot = decodeURIComponent(unescape(href))
|
||||
.replace(nonWordAndColonTest, '')
|
||||
.toLowerCase();
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
if(prot.indexOf('javascript:') === 0 || prot.indexOf('vbscript:') === 0 || prot.indexOf('data:') === 0) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
try {
|
||||
href = encodeURI(href).replace(/%25/g, '%');
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
return href;
|
||||
};
|
||||
function cleanUrl(href) {
|
||||
try {
|
||||
href = encodeURI(href).replace(/%25/g, '%');
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
return href;
|
||||
}
|
||||
|
||||
const escapeTest = /[&<>"']/;
|
||||
const escapeReplace = /[&<>"']/g;
|
||||
@@ -687,15 +785,49 @@ const processStyleTags = (string)=>{
|
||||
//TODO: can we simplify to just split on commas?
|
||||
const tags = string.match(/(?:[^, ":=]+|[:=](?:"[^"]*"|))+/g);
|
||||
|
||||
const id = _.remove(tags, (tag)=>tag.startsWith('#')).map((tag)=>tag.slice(1))[0];
|
||||
const classes = _.remove(tags, (tag)=>(!tag.includes(':')) && (!tag.includes('=')));
|
||||
const attributes = _.remove(tags, (tag)=>(tag.includes('='))).map((tag)=>tag.replace(/="?([^"]*)"?/g, '="$1"'));
|
||||
const styles = tags?.length ? tags.map((tag)=>tag.replace(/:"?([^"]*)"?/g, ':$1;').trim()) : [];
|
||||
const id = _.remove(tags, (tag)=>tag.startsWith('#')).map((tag)=>tag.slice(1))[0] || null;
|
||||
const classes = _.remove(tags, (tag)=>(!tag.includes(':')) && (!tag.includes('='))).join(' ') || null;
|
||||
const attributes = _.remove(tags, (tag)=>(tag.includes('='))).map((tag)=>tag.replace(/="?([^"]*)"?/g, '="$1"'))
|
||||
?.filter((attr)=>!attr.startsWith('class="') && !attr.startsWith('style="') && !attr.startsWith('id="'))
|
||||
.reduce((obj, attr)=>{
|
||||
const index = attr.indexOf('=');
|
||||
let [key, value] = [attr.substring(0, index), attr.substring(index + 1)];
|
||||
value = value.replace(/"/g, '');
|
||||
obj[key] = value;
|
||||
return obj;
|
||||
}, {}) || null;
|
||||
const styles = tags?.length ? tags.map((tag)=>tag.replace(/:"?([^"]*)"?/g, ':$1;').trim()).join(' ') : null;
|
||||
|
||||
return `${classes?.length ? ` ${classes.join(' ')}` : ''}"` +
|
||||
`${id ? ` id="${id}"` : ''}` +
|
||||
`${styles?.length ? ` style="${styles.join(' ')}"` : ''}` +
|
||||
`${attributes?.length ? ` ${attributes.join(' ')}` : ''}`;
|
||||
return {
|
||||
id : id,
|
||||
classes : classes,
|
||||
styles : styles,
|
||||
attributes : _.isEmpty(attributes) ? null : attributes
|
||||
};
|
||||
};
|
||||
|
||||
//Given a string representing an HTML element, extract all of its properties (id, class, style, and other attributes)
|
||||
const extractHTMLStyleTags = (htmlString)=>{
|
||||
const firstElementOnly = htmlString.split('>')[0];
|
||||
const id = firstElementOnly.match(/id="([^"]*)"/)?.[1] || null;
|
||||
const classes = firstElementOnly.match(/class="([^"]*)"/)?.[1] || null;
|
||||
const styles = firstElementOnly.match(/style="([^"]*)"/)?.[1] || null;
|
||||
const attributes = firstElementOnly.match(/[a-zA-Z]+="[^"]*"/g)
|
||||
?.filter((attr)=>!attr.startsWith('class="') && !attr.startsWith('style="') && !attr.startsWith('id="'))
|
||||
.reduce((obj, attr)=>{
|
||||
const index = attr.indexOf('=');
|
||||
let [key, value] = [attr.substring(0, index), attr.substring(index + 1)];
|
||||
value = value.replace(/"/g, '');
|
||||
obj[key] = value;
|
||||
return obj;
|
||||
}, {}) || null;
|
||||
|
||||
return {
|
||||
id : id,
|
||||
classes : classes,
|
||||
styles : styles,
|
||||
attributes : _.isEmpty(attributes) ? null : attributes
|
||||
};
|
||||
};
|
||||
|
||||
const globalVarsList = {};
|
||||
@@ -704,13 +836,15 @@ let globalPageNumber = 0;
|
||||
|
||||
module.exports = {
|
||||
marked : Marked,
|
||||
render : (rawBrewText, pageNumber=1)=>{
|
||||
globalVarsList[pageNumber] = {}; //Reset global links for current page, to ensure values are parsed in order
|
||||
varsQueue = []; //Could move into MarkedVariables()
|
||||
globalPageNumber = pageNumber;
|
||||
render : (rawBrewText, pageNumber=0)=>{
|
||||
globalVarsList[pageNumber] = {}; //Reset global links for current page, to ensure values are parsed in order
|
||||
varsQueue = []; //Could move into MarkedVariables()
|
||||
globalPageNumber = pageNumber;
|
||||
if(pageNumber==0) {
|
||||
MarkedGFMResetHeadingIDs();
|
||||
}
|
||||
|
||||
rawBrewText = rawBrewText.replace(/^\\column$/gm, `\n<div class='columnSplit'></div>\n`)
|
||||
.replace(/^(:+)$/gm, (match)=>`${`<div class='blank'></div>`.repeat(match.length)}\n`);
|
||||
rawBrewText = rawBrewText.replace(/^\\column$/gm, `\n<div class='columnSplit'></div>\n`);
|
||||
const opts = Marked.defaults;
|
||||
|
||||
rawBrewText = opts.hooks.preprocess(rawBrewText);
|
||||
|
||||
@@ -47,8 +47,8 @@ const Nav = {
|
||||
color : null
|
||||
};
|
||||
},
|
||||
handleClick : function(){
|
||||
this.props.onClick();
|
||||
handleClick : function(e){
|
||||
this.props.onClick(e);
|
||||
},
|
||||
render : function(){
|
||||
const classes = cx('navItem', this.props.color, this.props.className);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
require('./splitPane.less');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
const cx = require('classnames');
|
||||
|
||||
const SplitPane = createClass({
|
||||
@@ -24,6 +23,9 @@ const SplitPane = createClass({
|
||||
};
|
||||
},
|
||||
|
||||
pane1 : React.createRef(null),
|
||||
pane2 : React.createRef(null),
|
||||
|
||||
componentDidMount : function() {
|
||||
const dividerPos = window.localStorage.getItem(this.props.storageKey);
|
||||
if(dividerPos){
|
||||
@@ -137,7 +139,6 @@ const SplitPane = createClass({
|
||||
render : function(){
|
||||
return <div className='splitPane' onPointerMove={this.handleMove} onPointerUp={this.handleUp}>
|
||||
<Pane
|
||||
ref='pane1'
|
||||
width={this.state.currentDividerPos}
|
||||
>
|
||||
{React.cloneElement(this.props.children[0], {
|
||||
@@ -147,7 +148,7 @@ const SplitPane = createClass({
|
||||
})}
|
||||
</Pane>
|
||||
{this.renderDivider()}
|
||||
<Pane ref='pane2' isDragging={this.state.isDragging}>{this.props.children[1]}</Pane>
|
||||
<Pane isDragging={this.state.isDragging}>{this.props.children[1]}</Pane>
|
||||
</div>;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const stylelint = require('stylelint');
|
||||
const { isNumber } = require('stylelint/lib/utils/validateTypes');
|
||||
const { isNumber } = require('stylelint/lib/utils/validateTypes.cjs');
|
||||
|
||||
const { report, ruleMessages, validateOptions } = stylelint.utils;
|
||||
const ruleName = 'naturalcrit/declaration-block-multi-line-min-declarations';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const stylelint = require('stylelint');
|
||||
const { isNumber } = require('stylelint/lib/utils/validateTypes');
|
||||
const { isNumber } = require('stylelint/lib/utils/validateTypes.cjs');
|
||||
|
||||
const { report, ruleMessages, validateOptions } = stylelint.utils;
|
||||
const ruleName = 'naturalcrit/declaration-colon-min-space-before';
|
||||
|
||||
@@ -88,4 +88,16 @@ describe('Multiline Definition Lists', ()=>{
|
||||
const rendered = Markdown.render(source).trim();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<dl><dt>Term 1</dt><dd>Inline definition 1</dd>\n<dt></dt><dd>Inline definition 2 (no DT)</dd>\n</dl>');
|
||||
});
|
||||
|
||||
test('Multiline Definition Term must have at least one non-empty Definition', function() {
|
||||
const source = 'Term 1\n::';
|
||||
const rendered = Markdown.render(source).trim();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p>Term 1</p>\n<div class='blank'></div><div class='blank'></div>`);
|
||||
});
|
||||
|
||||
test('Multiline Definition List must have at least one non-newline character after ::', function() {
|
||||
const source = 'Term 1\n::\nDefinition 1\n\n';
|
||||
const rendered = Markdown.render(source).trim();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p>Term 1</p>\n<div class='blank'></div><div class='blank'></div>\n<p>Definition 1</p>`);
|
||||
});
|
||||
});
|
||||
|
||||
58
tests/markdown/emojis.test.js
Normal file
58
tests/markdown/emojis.test.js
Normal file
@@ -0,0 +1,58 @@
|
||||
const Markdown = require('naturalcrit/markdown.js');
|
||||
const dedent = require('dedent-tabs').default;
|
||||
|
||||
// Marked.js adds line returns after closing tags on some default tokens.
|
||||
// This removes those line returns for comparison sake.
|
||||
String.prototype.trimReturns = function(){
|
||||
return this.replace(/\r?\n|\r/g, '');
|
||||
};
|
||||
|
||||
const emoji = 'df_d12_2';
|
||||
|
||||
describe(`When emojis/icons are active`, ()=>{
|
||||
it('when a word is between two colons (:word:), and a matching emoji exists, it is rendered as an emoji', function() {
|
||||
const source = `:${emoji}:`;
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p><i class="df d12-2"></i></p>`);
|
||||
});
|
||||
|
||||
it('when a word is between two colons (:word:), and no matching emoji exists, it is not parsed', function() {
|
||||
const source = `:invalid:`;
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p>:invalid:</p>`);
|
||||
});
|
||||
|
||||
it('two valid emojis with no whitespace are prioritized over definition lists', function() {
|
||||
const source = `:${emoji}::${emoji}:`;
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p><i class="df d12-2"></i><i class="df d12-2"></i></p>`);
|
||||
});
|
||||
|
||||
it('definition lists that are not also part of an emoji can coexist with normal emojis', function() {
|
||||
const source = `definition :: term ${emoji}::${emoji}:`;
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<dl><dt>definition</dt><dd>term df_d12_2:<i class="df d12-2"></i></dd></dl>`);
|
||||
});
|
||||
|
||||
it('A valid emoji is compatible with curly injectors', function() {
|
||||
const source = `:${emoji}:{color:blue,myClass}`;
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p><i class="df d12-2 myClass" style="color:blue;"></i></p>`);
|
||||
});
|
||||
|
||||
it('Emojis are not parsed inside of curly span CSS blocks', function() {
|
||||
const source = `{{color:${emoji} text}}`;
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<span class="inline-block" style="color:df_d12_2;">text</span>`);
|
||||
});
|
||||
|
||||
it('Emojis are not parsed inside of curly div CSS blocks', function() {
|
||||
const source = dedent`{{color:${emoji}
|
||||
text
|
||||
}}`;
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block" style="color:df_d12_2;"><p>text</p></div>`);
|
||||
});
|
||||
|
||||
// another test of the editor to confirm an autocomplete menu opens
|
||||
});
|
||||
47
tests/markdown/hard-breaks.test.js
Normal file
47
tests/markdown/hard-breaks.test.js
Normal file
@@ -0,0 +1,47 @@
|
||||
/* eslint-disable max-lines */
|
||||
|
||||
const Markdown = require('naturalcrit/markdown.js');
|
||||
|
||||
describe('Hard Breaks', ()=>{
|
||||
test('Single Break', function() {
|
||||
const source = ':\n\n';
|
||||
const rendered = Markdown.render(source).trim();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class='blank'></div>`);
|
||||
});
|
||||
|
||||
test('Double Break', function() {
|
||||
const source = '::\n\n';
|
||||
const rendered = Markdown.render(source).trim();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class='blank'></div><div class='blank'></div>`);
|
||||
});
|
||||
|
||||
test('Triple Break', function() {
|
||||
const source = ':::\n\n';
|
||||
const rendered = Markdown.render(source).trim();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class='blank'></div><div class='blank'></div><div class='blank'></div>`);
|
||||
});
|
||||
|
||||
test('Many Break', function() {
|
||||
const source = '::::::::::\n\n';
|
||||
const rendered = Markdown.render(source).trim();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class='blank'></div><div class='blank'></div><div class='blank'></div><div class='blank'></div><div class='blank'></div><div class='blank'></div><div class='blank'></div><div class='blank'></div><div class='blank'></div><div class='blank'></div>`);
|
||||
});
|
||||
|
||||
test('Multiple sets of Breaks', function() {
|
||||
const source = ':::\n:::\n:::';
|
||||
const rendered = Markdown.render(source).trim();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class='blank'></div><div class='blank'></div><div class='blank'></div>\n<div class='blank'></div><div class='blank'></div><div class='blank'></div>\n<div class='blank'></div><div class='blank'></div><div class='blank'></div>`);
|
||||
});
|
||||
|
||||
test('Break directly between two paragraphs', function() {
|
||||
const source = 'Line 1\n::\nLine 2';
|
||||
const rendered = Markdown.render(source).trim();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p>Line 1</p>\n<div class='blank'></div><div class='blank'></div>\n<p>Line 2</p>`);
|
||||
});
|
||||
|
||||
test('Ignored inside a code block', function() {
|
||||
const source = '```\n\n:\n\n```\n';
|
||||
const rendered = Markdown.render(source).trim();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<pre><code>\n:\n</code></pre>`);
|
||||
});
|
||||
});
|
||||
@@ -130,8 +130,8 @@ describe('Inline: When using the Inline syntax {{ }}', ()=>{
|
||||
describe(`Block: When using the Block syntax {{tags\\ntext\\n}}`, ()=>{
|
||||
it('Renders a div with text only', function() {
|
||||
const source = dedent`{{
|
||||
text
|
||||
}}`;
|
||||
text
|
||||
}}`;
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block"><p>text</p></div>`);
|
||||
});
|
||||
@@ -139,14 +139,14 @@ describe(`Block: When using the Block syntax {{tags\\ntext\\n}}`, ()=>{
|
||||
it('Renders an empty div', function() {
|
||||
const source = dedent`{{
|
||||
|
||||
}}`;
|
||||
}}`;
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block"></div>`);
|
||||
});
|
||||
|
||||
it('Renders a single paragraph with opening and closing brackets', function() {
|
||||
const source = dedent`{{
|
||||
}}`;
|
||||
}}`;
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p>{{}}</p>`);
|
||||
});
|
||||
@@ -154,79 +154,79 @@ describe(`Block: When using the Block syntax {{tags\\ntext\\n}}`, ()=>{
|
||||
it('Renders a div with a single class', function() {
|
||||
const source = dedent`{{cat
|
||||
|
||||
}}`;
|
||||
}}`;
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block cat"></div>`);
|
||||
});
|
||||
|
||||
it('Renders a div with a single class and text', function() {
|
||||
const source = dedent`{{cat
|
||||
Sample text.
|
||||
}}`;
|
||||
Sample text.
|
||||
}}`;
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block cat"><p>Sample text.</p></div>`);
|
||||
});
|
||||
|
||||
it('Renders a div with two classes and text', function() {
|
||||
const source = dedent`{{cat,dog
|
||||
Sample text.
|
||||
}}`;
|
||||
Sample text.
|
||||
}}`;
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block cat dog"><p>Sample text.</p></div>`);
|
||||
});
|
||||
|
||||
it('Renders a div with a style and text', function() {
|
||||
const source = dedent`{{color:red
|
||||
Sample text.
|
||||
}}`;
|
||||
Sample text.
|
||||
}}`;
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block" style="color:red;"><p>Sample text.</p></div>`);
|
||||
});
|
||||
|
||||
it('Renders a div with a style that has a string variable, and text', function() {
|
||||
const source = dedent`{{--stringVariable:"'string'"
|
||||
Sample text.
|
||||
}}`;
|
||||
Sample text.
|
||||
}}`;
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block" style="--stringVariable:'string';"><p>Sample text.</p></div>`);
|
||||
});
|
||||
|
||||
it('Renders a div with a style that has a string variable, and text', function() {
|
||||
const source = dedent`{{--stringVariable:"'string'"
|
||||
Sample text.
|
||||
}}`;
|
||||
Sample text.
|
||||
}}`;
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block" style="--stringVariable:'string';"><p>Sample text.</p></div>`);
|
||||
});
|
||||
|
||||
it('Renders a div with a class, style and text', function() {
|
||||
const source = dedent`{{cat,color:red
|
||||
Sample text.
|
||||
}}`;
|
||||
Sample text.
|
||||
}}`;
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block cat" style="color:red;"><p>Sample text.</p></div>`);
|
||||
});
|
||||
|
||||
it('Renders a div with an ID, class, style and text (different order)', function() {
|
||||
const source = dedent`{{color:red,cat,#dog
|
||||
Sample text.
|
||||
}}`;
|
||||
Sample text.
|
||||
}}`;
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block cat" id="dog" style="color:red;"><p>Sample text.</p></div>`);
|
||||
});
|
||||
|
||||
it('Renders a div with a single ID', function() {
|
||||
const source = dedent`{{#cat,#dog
|
||||
Sample text.
|
||||
}}`;
|
||||
Sample text.
|
||||
}}`;
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block" id="cat"><p>Sample text.</p></div>`);
|
||||
});
|
||||
|
||||
it('Renders a div with an ID, class, style and text, and a variable assignment', function() {
|
||||
const source = dedent`{{color:red,cat,#dog,a="b and c",d="e"
|
||||
Sample text.
|
||||
}}`;
|
||||
Sample text.
|
||||
}}`;
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class=\"block cat\" id=\"dog\" style=\"color:red;\" a=\"b and c\" d=\"e\"><p>Sample text.</p></div>`);
|
||||
});
|
||||
@@ -243,124 +243,218 @@ describe(`Block: When using the Block syntax {{tags\\ntext\\n}}`, ()=>{
|
||||
describe('Injection: When an injection tag follows an element', ()=>{
|
||||
// FIXME: Most of these fail because injections currently replace attributes, rather than append to. Or just minor extra whitespace issues.
|
||||
describe('and that element is an inline-block', ()=>{
|
||||
it.failing('Renders a span "text" with no injection', function() {
|
||||
it('Renders a span "text" with no injection', function() {
|
||||
const source = '{{ text}}{}';
|
||||
const rendered = Markdown.render(source);
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block">text</span>');
|
||||
});
|
||||
|
||||
it.failing('Renders a span "text" with injected Class name', function() {
|
||||
it('Renders a span "text" with injected Class name', function() {
|
||||
const source = '{{ text}}{ClassName}';
|
||||
const rendered = Markdown.render(source);
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block ClassName">text</span>');
|
||||
});
|
||||
|
||||
it.failing('Renders a span "text" with injected attribute', function() {
|
||||
it('Renders a span "text" with injected attribute', function() {
|
||||
const source = '{{ text}}{a="b and c"}';
|
||||
const rendered = Markdown.render(source);
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span a="b and c" class="inline-block ">text</span>');
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" a="b and c">text</span>');
|
||||
});
|
||||
|
||||
it.failing('Renders a span "text" with injected style', function() {
|
||||
it('Renders a span "text" with injected style', function() {
|
||||
const source = '{{ text}}{color:red}';
|
||||
const rendered = Markdown.render(source);
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" style="color:red;">text</span>');
|
||||
});
|
||||
|
||||
it.failing('Renders a span "text" with injected style using a string variable', function() {
|
||||
it('Renders a span "text" with injected style using a string variable', function() {
|
||||
const source = `{{ text}}{--stringVariable:"'string'"}`;
|
||||
const rendered = Markdown.render(source);
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<span class="inline-block" style="--stringVariable:'string';">text</span>`);
|
||||
});
|
||||
|
||||
it.failing('Renders a span "text" with two injected styles', function() {
|
||||
it('Renders a span "text" with two injected styles', function() {
|
||||
const source = '{{ text}}{color:red,background:blue}';
|
||||
const rendered = Markdown.render(source);
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" style="color:red; background:blue;">text</span>');
|
||||
});
|
||||
|
||||
it.failing('Renders an emphasis element with injected Class name', function() {
|
||||
it('Renders a span "text" with its own ID, overwritten with an injected ID', function() {
|
||||
const source = '{{#oldId text}}{#newId}';
|
||||
const rendered = Markdown.render(source);
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" id="newId">text</span>');
|
||||
});
|
||||
|
||||
it('Renders a span "text" with its own attributes, overwritten with an injected attribute, plus a new one', function() {
|
||||
const source = '{{attrA="old",attrB="old" text}}{attrA="new",attrC="new"}';
|
||||
const rendered = Markdown.render(source);
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" attrA="new" attrB="old" attrC="new">text</span>');
|
||||
});
|
||||
|
||||
it('Renders a span "text" with its own attributes, overwritten with an injected attribute, ignoring "class", "style", and "id"', function() {
|
||||
const source = '{{attrA="old",attrB="old" text}}{attrA="new",attrC="new",class="new",style="new",id="new"}';
|
||||
const rendered = Markdown.render(source);
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" attrA="new" attrB="old" attrC="new">text</span>');
|
||||
});
|
||||
|
||||
it('Renders a span "text" with its own styles, appended with injected styles', function() {
|
||||
const source = '{{color:blue,height:10px text}}{width:10px,color:red}';
|
||||
const rendered = Markdown.render(source);
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" style="color:blue; height:10px; width:10px; color:red;">text</span>');
|
||||
});
|
||||
|
||||
it('Renders a span "text" with its own classes, appended with injected classes', function() {
|
||||
const source = '{{classA,classB text}}{classA,classC}';
|
||||
const rendered = Markdown.render(source);
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block classA classB classA classC">text</span>');
|
||||
});
|
||||
|
||||
it('Renders an emphasis element with injected Class name', function() {
|
||||
const source = '*emphasis*{big}';
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p><em class="big">emphasis</em></p>');
|
||||
});
|
||||
|
||||
it.failing('Renders a code element with injected style', function() {
|
||||
it('Renders a code element with injected style', function() {
|
||||
const source = '`code`{background:gray}';
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p><code style="background:gray;">code</code></p>');
|
||||
});
|
||||
|
||||
it.failing('Renders an image element with injected style', function() {
|
||||
it('Renders an image element with injected style', function() {
|
||||
const source = '{position:absolute}';
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p><img src="http://i.imgur.com/hMna6G0.png" alt="homebrew mug" style="position:absolute;"></p>');
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p><img style="position:absolute;" src="http://i.imgur.com/hMna6G0.png" alt="alt text"></p>');
|
||||
});
|
||||
|
||||
it.failing('Renders an element modified by only the first of two consecutive injections', function() {
|
||||
it('Renders an element modified by only the first of two consecutive injections', function() {
|
||||
const source = '{{ text}}{color:red}{background:blue}';
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p><span class="inline-block" style="color:red;">text</span>{background:blue}</p>');
|
||||
});
|
||||
|
||||
it('Renders an parent and child element, each modified by an injector', function() {
|
||||
const source = dedent`**bolded text**{color:red}
|
||||
{color:blue}`;
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p style="color:blue;"><strong style="color:red;">bolded text</strong></p>');
|
||||
});
|
||||
|
||||
it('Renders an image with added attributes', function() {
|
||||
const source = ` {position:absolute,bottom:20px,left:130px,width:220px,a="b and c",d=e}`;
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p><img class="" style="position:absolute; bottom:20px; left:130px; width:220px;" a="b and c" d="e" src="https://i.imgur.com/hMna6G0.png" alt="homebrew mug"></p>`);
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p><img style="position:absolute; bottom:20px; left:130px; width:220px;" src="https://i.imgur.com/hMna6G0.png" alt="homebrew mug" a="b and c" d="e"></p>`);
|
||||
});
|
||||
|
||||
it('Renders an image with "=" in the url, and added attributes', function() {
|
||||
const source = ` {position:absolute,bottom:20px,left:130px,width:220px,a="b and c",d=e}`;
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p><img style="position:absolute; bottom:20px; left:130px; width:220px;" src="https://i.imgur.com/hMna6G0.png?auth=12345&height=1024" alt="homebrew mug" a="b and c" d="e"></p>`);
|
||||
});
|
||||
|
||||
it('Renders an image and added attributes with "=" in the value, ', function() {
|
||||
const source = ` {position:absolute,bottom:20px,left:130px,width:220px,a="b and c",d=e,otherUrl="url?auth=12345"}`;
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p><img style="position:absolute; bottom:20px; left:130px; width:220px;" src="https://i.imgur.com/hMna6G0.png" alt="homebrew mug" a="b and c" d="e" otherUrl="url?auth=12345"></p>`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('and that element is a block', ()=>{
|
||||
it.failing('renders a div "text" with no injection', function() {
|
||||
it('renders a div "text" with no injection', function() {
|
||||
const source = '{{\ntext\n}}\n{}';
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<div class="block"><p>text</p></div>');
|
||||
});
|
||||
|
||||
it.failing('renders a div "text" with injected Class name', function() {
|
||||
it('renders a div "text" with injected Class name', function() {
|
||||
const source = '{{\ntext\n}}\n{ClassName}';
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<div class="block ClassName"><p>text</p></div>');
|
||||
});
|
||||
|
||||
it.failing('renders a div "text" with injected style', function() {
|
||||
it('renders a div "text" with injected style', function() {
|
||||
const source = '{{\ntext\n}}\n{color:red}';
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<div class="block" style="color:red;"><p>text</p></div>');
|
||||
});
|
||||
|
||||
it.failing('renders a div "text" with two injected styles', function() {
|
||||
it('renders a div "text" with two injected styles', function() {
|
||||
const source = dedent`{{
|
||||
text
|
||||
}}
|
||||
{color:red,background:blue}`;
|
||||
text
|
||||
}}
|
||||
{color:red,background:blue}`;
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block" style="color:red; background:blue"><p>text</p></div>`);
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block" style="color:red; background:blue;"><p>text</p></div>`);
|
||||
});
|
||||
|
||||
it.failing('renders a div "text" with injected variable string', function() {
|
||||
it('renders a div "text" with injected variable string', function() {
|
||||
const source = dedent`{{
|
||||
text
|
||||
}}
|
||||
{--stringVariable:"'string'"}`;
|
||||
text
|
||||
}}
|
||||
{--stringVariable:"'string'"}`;
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block" style="--stringVariable:'string'"><p>text</p></div>`);
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block" style="--stringVariable:'string';"><p>text</p></div>`);
|
||||
});
|
||||
|
||||
it.failing('renders an h2 header "text" with injected class name', function() {
|
||||
it('Renders a span "text" with its own ID, overwritten with an injected ID', function() {
|
||||
const source = dedent`{{#oldId
|
||||
text
|
||||
}}
|
||||
{#newId}`;
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<div class="block" id="newId"><p>text</p></div>');
|
||||
});
|
||||
|
||||
it('Renders a span "text" with its own attributes, overwritten with an injected attribute, plus a new one', function() {
|
||||
const source = dedent`{{attrA="old",attrB="old"
|
||||
text
|
||||
}}
|
||||
{attrA="new",attrC="new"}`;
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<div class="block" attrA="new" attrB="old" attrC="new"><p>text</p></div>');
|
||||
});
|
||||
|
||||
it('Renders a span "text" with its own attributes, overwritten with an injected attribute, ignoring "class", "style", and "id"', function() {
|
||||
const source = dedent`{{attrA="old",attrB="old"
|
||||
text
|
||||
}}
|
||||
{attrA="new",attrC="new",class="new",style="new",id="new"}`;
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<div class="block" attrA="new" attrB="old" attrC="new"><p>text</p></div>');
|
||||
});
|
||||
|
||||
it('Renders a span "text" with its own styles, appended with injected styles', function() {
|
||||
const source = dedent`{{color:blue,height:10px
|
||||
text
|
||||
}}
|
||||
{width:10px,color:red}`;
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<div class="block" style="color:blue; height:10px; width:10px; color:red;"><p>text</p></div>');
|
||||
});
|
||||
|
||||
it('Renders a span "text" with its own classes, appended with injected classes', function() {
|
||||
const source = dedent`{{classA,classB
|
||||
text
|
||||
}}
|
||||
{classA,classC}`;
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<div class="block classA classB classA classC"><p>text</p></div>');
|
||||
});
|
||||
|
||||
it('renders an h2 header "text" with injected class name', function() {
|
||||
const source = dedent`## text
|
||||
{ClassName}`;
|
||||
{ClassName}`;
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<h2 class="ClassName">text</h2>');
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<h2 class="ClassName" id="text">text</h2>');
|
||||
});
|
||||
|
||||
it.failing('renders a table with injected class name', function() {
|
||||
it('renders a table with injected class name', function() {
|
||||
const source = dedent`| Experience Points | Level |
|
||||
|:------------------|:-----:|
|
||||
| 0 | 1 |
|
||||
| 300 | 2 |
|
||||
|:------------------|:-----:|
|
||||
| 0 | 1 |
|
||||
| 300 | 2 |
|
||||
|
||||
{ClassName}`;
|
||||
{ClassName}`;
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<table class="ClassName"><thead><tr><th align=left>Experience Points</th><th align=center>Level</th></tr></thead><tbody><tr><td align=left>0</td><td align=center>1</td></tr><tr><td align=left>300</td><td align=center>2</td></tr></tbody></table>`);
|
||||
});
|
||||
@@ -376,23 +470,23 @@ describe('Injection: When an injection tag follows an element', ()=>{
|
||||
// expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`...`); // FIXME: expect this to be injected into <ul>? Currently injects into last <li>
|
||||
// });
|
||||
|
||||
it.failing('renders an h2 header "text" with injected class name, and "secondInjection" as regular text on the next line.', function() {
|
||||
it('renders an h2 header "text" with injected class name, and "secondInjection" as regular text on the next line.', function() {
|
||||
const source = dedent`## text
|
||||
{ClassName}
|
||||
{secondInjection}`;
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<h2 class="ClassName">text</h2><p>{secondInjection}</p>');
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<h2 class="ClassName" id="text">text</h2><p>{secondInjection}</p>');
|
||||
});
|
||||
|
||||
it.failing('renders a div nested into another div, the inner with class=innerDiv and the other class=outerDiv', function() {
|
||||
it('renders a div nested into another div, the inner with class=innerDiv and the other class=outerDiv', function() {
|
||||
const source = dedent`{{
|
||||
outer text
|
||||
{{
|
||||
inner text
|
||||
}}
|
||||
{innerDiv}
|
||||
}}
|
||||
{outerDiv}`;
|
||||
outer text
|
||||
{{
|
||||
inner text
|
||||
}}
|
||||
{innerDiv}
|
||||
}}
|
||||
{outerDiv}`;
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<div class="block outerDiv"><p>outer text</p><div class="block innerDiv"><p>inner text</p></div></div>');
|
||||
});
|
||||
|
||||
@@ -329,7 +329,7 @@ describe('Normal Links and Images', ()=>{
|
||||
const source = `{width:100px}`;
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent`
|
||||
<p><img class="" style="width:100px;" src="url" alt="alt text"></p>`.trimReturns());
|
||||
<p><img style="width:100px;" src="url" alt="alt text"></p>`.trimReturns());
|
||||
});
|
||||
|
||||
it('Renders normal links', function() {
|
||||
@@ -370,4 +370,36 @@ describe('Cross-page variables', ()=>{
|
||||
const rendered = renderAllPages([source0, source1]).join('\n\\page\n').trimReturns();
|
||||
expect(rendered, `Input:\n${[source0, source1].join('\n\\page\n')}`, { showPrefix: false }).toBe('<p>two</p><p>one</p>\\page<p>two</p>');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Math function parameter handling', ()=>{
|
||||
it('allows variables in single-parameter functions', function() {
|
||||
const source = '[var]:4.1\n\n$[floor(var)]';
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p>4</p>`);
|
||||
});
|
||||
it('allows one variable and a number in two-parameter functions', function() {
|
||||
const source = '[var]:4\n\n$[min(1,var)]';
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p>1</p>`);
|
||||
});
|
||||
it('allows two variables in two-parameter functions', function() {
|
||||
const source = '[var1]:4\n\n[var2]:8\n\n$[min(var1,var2)]';
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p>4</p>`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Variable names that are subsets of other names', ()=>{
|
||||
it('do not conflict with function names', function() {
|
||||
const source = `[a]: -1\n\n$[abs(a)]`;
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered).toBe('<p>1</p>');
|
||||
});
|
||||
|
||||
it('do not conflict with other variable names', function() {
|
||||
const source = `[ab]: 2\n\n[aba]: 8\n\n[ba]: 4\n\n$[ab + aba + ba]`;
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered).toBe('<p>14</p>');
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name" : "5e PHB",
|
||||
"renderer" : "V3",
|
||||
"baseTheme" : false,
|
||||
"baseTheme" : "Blank",
|
||||
"baseSnippets" : false
|
||||
}
|
||||
|
||||
@@ -21,9 +21,43 @@ module.exports = [
|
||||
view : 'text',
|
||||
snippets : [
|
||||
{
|
||||
name : 'Table of Contents',
|
||||
icon : 'fas fa-book',
|
||||
gen : TableOfContentsGen
|
||||
name : 'Table of Contents',
|
||||
icon : 'fas fa-book',
|
||||
gen : TableOfContentsGen,
|
||||
experimental : true,
|
||||
subsnippets : [
|
||||
{
|
||||
name : 'Table of Contents',
|
||||
icon : 'fas fa-book',
|
||||
gen : TableOfContentsGen,
|
||||
experimental : true
|
||||
},
|
||||
{
|
||||
name : 'Include in ToC up to H3',
|
||||
icon : 'fas fa-dice-three',
|
||||
gen : dedent `\n{{tocDepthH3
|
||||
}}\n`,
|
||||
|
||||
},
|
||||
{
|
||||
name : 'Include in ToC up to H4',
|
||||
icon : 'fas fa-dice-four',
|
||||
gen : dedent `\n{{tocDepthH4
|
||||
}}\n`,
|
||||
},
|
||||
{
|
||||
name : 'Include in ToC up to H5',
|
||||
icon : 'fas fa-dice-five',
|
||||
gen : dedent `\n{{tocDepthH5
|
||||
}}\n`,
|
||||
},
|
||||
{
|
||||
name : 'Include in ToC up to H6',
|
||||
icon : 'fas fa-dice-six',
|
||||
gen : dedent `\n{{tocDepthH6
|
||||
}}\n`,
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name : 'Index',
|
||||
@@ -315,7 +349,7 @@ module.exports = [
|
||||
/* Ink Friendly */
|
||||
*:is(.page,.monster,.note,.descriptive) {
|
||||
background : white !important;
|
||||
filter : drop-shadow(0px 0px 3px #888) !important;
|
||||
box-shadow : 1px 4px 14px #888 !important;
|
||||
}
|
||||
|
||||
.page img {
|
||||
|
||||
@@ -171,7 +171,7 @@ module.exports = {
|
||||
**Condition Immunities** :: ${genList(['groggy', 'swagged', 'weak-kneed', 'buzzed', 'groovy', 'melancholy', 'drunk'], 3)}
|
||||
**Senses** :: darkvision 60 ft., passive Perception ${_.random(3, 20)}
|
||||
**Languages** :: ${genList(['Common', 'Pottymouth', 'Gibberish', 'Latin', 'Jive'], 2)}
|
||||
**Challenge** :: ${_.random(0, 15)} (${_.random(10, 10000)} XP)
|
||||
**Challenge** :: ${_.random(0, 15)} (${_.random(10, 10000)} XP) {{bonus **Proficiency Bonus** +${_.random(2, 6)}}}
|
||||
___
|
||||
${_.times(_.random(genLines, genLines + 2), function(){return genAbilities();}).join('\n:\n')}
|
||||
:
|
||||
|
||||
@@ -1,47 +1,47 @@
|
||||
const _ = require("lodash");
|
||||
const _ = require('lodash');
|
||||
|
||||
const quotes = [
|
||||
"The sword glinted in the dim light, its edges keen and deadly. As the adventurer reached for it, he couldn't help but feel a surge of excitement mixed with fear. This was no ordinary blade.",
|
||||
"The dragon's roar shook the ground beneath their feet, and the brave knight stood tall, his sword at the ready. He knew that this would be the battle of his life, but he was determined to emerge victorious.",
|
||||
"The wizard's laboratory was a sight to behold, filled with bubbling cauldrons, ancient tomes, and strange artifacts from distant lands. As the apprentice gazed around in wonder, she knew that she was about to embark on a journey unlike any other.",
|
||||
"The tavern was packed with rowdy patrons, their voices raised in song and laughter. The bard took center stage, strumming his lute and launching into a tale of adventure and heroism that had the crowd hanging on his every word.",
|
||||
"The thief crept through the shadows, his eyes scanning the room for any sign of danger. He knew that one false move could mean the difference between success and failure, and he was determined to come out on top.",
|
||||
"The elf queen stood atop her castle walls, surveying the kingdom below with a mix of pride and sadness. She knew that the coming war would be brutal, but she was determined to protect her people at all costs.",
|
||||
"The necromancer's tower loomed in the distance, its dark spires piercing the sky. As the adventurers approached, they could feel the chill of death emanating from within",
|
||||
"The ranger moved through the forest like a shadow, his senses attuned to every sound and movement around him. He knew that danger lurked behind every tree, but he was ready for whatever came his way.",
|
||||
"The paladin knelt before the altar, his hands clasped in prayer. He knew that his faith would be tested in the days ahead, but he was ready to face whatever trials lay in store for him.",
|
||||
"The druid communed with the spirits of nature, his mind merging with the trees, the animals, and the very earth itself. He knew that his power came with a great responsibility, and he was determined to use it for the greater good.",
|
||||
'The sword glinted in the dim light, its edges keen and deadly. As the adventurer reached for it, he couldn\'t help but feel a surge of excitement mixed with fear. This was no ordinary blade.',
|
||||
'The dragon\'s roar shook the ground beneath their feet, and the brave knight stood tall, his sword at the ready. He knew that this would be the battle of his life, but he was determined to emerge victorious.',
|
||||
'The wizard\'s laboratory was a sight to behold, filled with bubbling cauldrons, ancient tomes, and strange artifacts from distant lands. As the apprentice gazed around in wonder, she knew that she was about to embark on a journey unlike any other.',
|
||||
'The tavern was packed with rowdy patrons, their voices raised in song and laughter. The bard took center stage, strumming his lute and launching into a tale of adventure and heroism that had the crowd hanging on his every word.',
|
||||
'The thief crept through the shadows, his eyes scanning the room for any sign of danger. He knew that one false move could mean the difference between success and failure, and he was determined to come out on top.',
|
||||
'The elf queen stood atop her castle walls, surveying the kingdom below with a mix of pride and sadness. She knew that the coming war would be brutal, but she was determined to protect her people at all costs.',
|
||||
'The necromancer\'s tower loomed in the distance, its dark spires piercing the sky. As the adventurers approached, they could feel the chill of death emanating from within',
|
||||
'The ranger moved through the forest like a shadow, his senses attuned to every sound and movement around him. He knew that danger lurked behind every tree, but he was ready for whatever came his way.',
|
||||
'The paladin knelt before the altar, his hands clasped in prayer. He knew that his faith would be tested in the days ahead, but he was ready to face whatever trials lay in store for him.',
|
||||
'The druid communed with the spirits of nature, his mind merging with the trees, the animals, and the very earth itself. He knew that his power came with a great responsibility, and he was determined to use it for the greater good.',
|
||||
];
|
||||
|
||||
const authors = [
|
||||
"Unknown",
|
||||
"James Wyatt",
|
||||
"Eolande Blackwood",
|
||||
"Ragnar Ironheart",
|
||||
"Lyra Nightshade",
|
||||
"Valtorius Darkstar",
|
||||
"Isadora Fireheart",
|
||||
"Theron Shadowbane",
|
||||
"Lirien Starweaver",
|
||||
"Drogathar Bonecrusher",
|
||||
"Kaelen Frostblade",
|
||||
'Unknown',
|
||||
'James Wyatt',
|
||||
'Eolande Blackwood',
|
||||
'Ragnar Ironheart',
|
||||
'Lyra Nightshade',
|
||||
'Valtorius Darkstar',
|
||||
'Isadora Fireheart',
|
||||
'Theron Shadowbane',
|
||||
'Lirien Starweaver',
|
||||
'Drogathar Bonecrusher',
|
||||
'Kaelen Frostblade',
|
||||
];
|
||||
|
||||
const books = [
|
||||
"The Blade of Destiny",
|
||||
"Dragonfire and Steel",
|
||||
"The Bard's Tale",
|
||||
"Darkness Rising",
|
||||
"The Sacred Quest",
|
||||
"Shadows in the Forest",
|
||||
"The Starweaver Chronicles",
|
||||
"Beneath the Bones",
|
||||
"Moonlit Magic",
|
||||
"Frost and Fury",
|
||||
'The Blade of Destiny',
|
||||
'Dragonfire and Steel',
|
||||
'The Bard\'s Tale',
|
||||
'Darkness Rising',
|
||||
'The Sacred Quest',
|
||||
'Shadows in the Forest',
|
||||
'The Starweaver Chronicles',
|
||||
'Beneath the Bones',
|
||||
'Moonlit Magic',
|
||||
'Frost and Fury',
|
||||
|
||||
];
|
||||
module.exports = () => {
|
||||
return `
|
||||
module.exports = ()=>{
|
||||
return `
|
||||
{{quote
|
||||
${_.sample(quotes)}
|
||||
|
||||
|
||||
@@ -2,77 +2,68 @@ const _ = require('lodash');
|
||||
const dedent = require('dedent-tabs').default;
|
||||
|
||||
const getTOC = (pages)=>{
|
||||
const add1 = (title, page)=>{
|
||||
res.push({
|
||||
title : title,
|
||||
page : page + 1,
|
||||
children : []
|
||||
});
|
||||
};
|
||||
const add2 = (title, page)=>{
|
||||
if(!_.last(res)) add1(null, page);
|
||||
_.last(res).children.push({
|
||||
title : title,
|
||||
page : page + 1,
|
||||
children : []
|
||||
});
|
||||
};
|
||||
const add3 = (title, page)=>{
|
||||
if(!_.last(res)) add1(null, page);
|
||||
if(!_.last(_.last(res).children)) add2(null, page);
|
||||
_.last(_.last(res).children).children.push({
|
||||
title : title,
|
||||
page : page + 1,
|
||||
children : []
|
||||
});
|
||||
|
||||
const recursiveAdd = (title, page, targetDepth, child, curDepth=0)=>{
|
||||
if(curDepth > 5) return; // Something went wrong.
|
||||
if(curDepth == targetDepth) {
|
||||
child.push({
|
||||
title : title,
|
||||
page : page,
|
||||
children : []
|
||||
});
|
||||
} else {
|
||||
if(child.length == 0) {
|
||||
child.push({
|
||||
title : null,
|
||||
page : page,
|
||||
children : []
|
||||
});
|
||||
}
|
||||
recursiveAdd(title, page, targetDepth, _.last(child).children, curDepth+1,);
|
||||
}
|
||||
};
|
||||
|
||||
const res = [];
|
||||
_.each(pages, (page, pageNum)=>{
|
||||
if(!page.includes("{{frontCover}}") && !page.includes("{{insideCover}}") && !page.includes("{{partCover}}") && !page.includes("{{backCover}}")) {
|
||||
const lines = page.split('\n');
|
||||
_.each(lines, (line)=>{
|
||||
if(_.startsWith(line, '# ')){
|
||||
const title = line.replace('# ', '');
|
||||
add1(title, pageNum);
|
||||
}
|
||||
if(_.startsWith(line, '## ')){
|
||||
const title = line.replace('## ', '');
|
||||
add2(title, pageNum);
|
||||
}
|
||||
if(_.startsWith(line, '### ')){
|
||||
const title = line.replace('### ', '');
|
||||
add3(title, pageNum);
|
||||
}
|
||||
});
|
||||
|
||||
const iframe = document.getElementById('BrewRenderer');
|
||||
const iframeDocument = iframe.contentDocument || iframe.contentWindow.document;
|
||||
const headings = iframeDocument.querySelectorAll('h1, h2, h3, h4, h5, h6');
|
||||
const headerDepth = ['H1', 'H2', 'H3', 'H4', 'H5', 'H6'];
|
||||
|
||||
_.each(headings, (heading)=>{
|
||||
const onPage = parseInt(heading.closest('.page').id?.replace(/^p/, ''));
|
||||
const ToCExclude = getComputedStyle(heading).getPropertyValue('--TOC');
|
||||
|
||||
if(ToCExclude != 'exclude') {
|
||||
recursiveAdd(heading.textContent.trim(), onPage, headerDepth.indexOf(heading.tagName), res);
|
||||
}
|
||||
});
|
||||
return res;
|
||||
};
|
||||
|
||||
|
||||
const ToCIterate = (entries, curDepth=0)=>{
|
||||
const levelPad = ['- ###', ' - ####', ' - ', ' - ', ' - ', ' - '];
|
||||
const toc = [];
|
||||
if(entries.title !== null){
|
||||
toc.push(`${levelPad[curDepth]} [{{ ${entries.title}}}{{ ${entries.page}}}](#p${entries.page})`);
|
||||
}
|
||||
if(entries.children.length) {
|
||||
_.each(entries.children, (entry, idx)=>{
|
||||
const children = ToCIterate(entry, entry.title == null ? curDepth : curDepth+1);
|
||||
if(children.length) {
|
||||
toc.push(...children);
|
||||
}
|
||||
});
|
||||
}
|
||||
return toc;
|
||||
};
|
||||
|
||||
module.exports = function(props){
|
||||
const pages = props.brew.text.split('\\page');
|
||||
const TOC = getTOC(pages);
|
||||
const markdown = _.reduce(TOC, (r, g1, idx1)=>{
|
||||
if(g1.title !== null) {
|
||||
r.push(`- ### [{{ ${g1.title}}}{{ ${g1.page}}}](#p${g1.page})`);
|
||||
}
|
||||
if(g1.children.length){
|
||||
_.each(g1.children, (g2, idx2)=>{
|
||||
if(g2.title !== null) {
|
||||
r.push(` - #### [{{ ${g2.title}}}{{ ${g2.page}}}](#p${g2.page})`);
|
||||
}
|
||||
if(g2.children.length){
|
||||
_.each(g2.children, (g3, idx3)=>{
|
||||
if(g2.title !== null) {
|
||||
r.push(` - [{{ ${g3.title}}}{{ ${g3.page}}}](#p${g3.page})`);
|
||||
} else { // Don't over-indent if no level-2 parent entry
|
||||
r.push(` - [{{ ${g3.title}}}{{ ${g3.page}}}](#p${g3.page})`);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
r.push(ToCIterate(g1).join('\n'));
|
||||
return r;
|
||||
}, []).join('\n');
|
||||
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
@import (less) './themes/assets/assets.less';
|
||||
@import (less) './themes/fonts/icon fonts/font-icons.less';
|
||||
@import (less) './themes/fonts/icon fonts/diceFont.less';
|
||||
|
||||
:root {
|
||||
//Colors
|
||||
@@ -356,6 +354,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
.bonus {
|
||||
float: right;
|
||||
padding-right: 0.5em;
|
||||
}
|
||||
|
||||
// Monster Ability table
|
||||
hr + table:first-of-type {
|
||||
margin : 0;
|
||||
@@ -379,6 +382,14 @@
|
||||
.useColumns(0.96, @fillMode: balance);
|
||||
}
|
||||
|
||||
//only for IOS devices
|
||||
@supports (-webkit-touch-callout: none) {
|
||||
.page .monster.frame {
|
||||
background-repeat : no-repeat;
|
||||
background-size : cover;
|
||||
}
|
||||
}
|
||||
|
||||
// *****************************
|
||||
// * FOOTER
|
||||
// *****************************/
|
||||
@@ -456,6 +467,7 @@
|
||||
margin-left : 1.5em;
|
||||
}
|
||||
}
|
||||
|
||||
// *****************************
|
||||
// * SPELL LIST
|
||||
// *****************************/
|
||||
@@ -543,10 +555,8 @@
|
||||
color : white;
|
||||
text-shadow : unset;
|
||||
text-transform : uppercase;
|
||||
filter : drop-shadow(0 0 1.5px black) drop-shadow(0 0 0 black)
|
||||
drop-shadow(0 0 0 black) drop-shadow(0 0 0 black)
|
||||
drop-shadow(0 0 0 black) drop-shadow(0 0 0 black)
|
||||
drop-shadow(0 0 0 black) drop-shadow(0 0 0 black);
|
||||
-webkit-text-stroke: 0.2cm black;
|
||||
paint-order:stroke;
|
||||
}
|
||||
h2 {
|
||||
font-family : 'NodestoCapsCondensed';
|
||||
@@ -554,10 +564,8 @@
|
||||
font-weight : normal;
|
||||
color : white;
|
||||
letter-spacing : 0.1cm;
|
||||
filter : drop-shadow(0 0 1px black) drop-shadow(0 0 0 black)
|
||||
drop-shadow(0 0 0 black) drop-shadow(0 0 0 black)
|
||||
drop-shadow(0 0 0 black) drop-shadow(0 0 0 black)
|
||||
drop-shadow(0 0 0 black) drop-shadow(0 0 0 black);
|
||||
-webkit-text-stroke: 0.14cm black;
|
||||
paint-order:stroke;
|
||||
}
|
||||
hr {
|
||||
position : relative;
|
||||
@@ -603,10 +611,8 @@
|
||||
font-size : 0.496cm;
|
||||
color : white;
|
||||
text-align : center;
|
||||
filter : drop-shadow(0 0 0.7px black) drop-shadow(0 0 0 black)
|
||||
drop-shadow(0 0 0 black) drop-shadow(0 0 0 black)
|
||||
drop-shadow(0 0 0 black) drop-shadow(0 0 0 black)
|
||||
drop-shadow(0 0 0 black) drop-shadow(0 0 0 black);
|
||||
-webkit-text-stroke: 0.1cm black;
|
||||
paint-order:stroke;
|
||||
}
|
||||
.logo {
|
||||
position : absolute;
|
||||
@@ -789,6 +795,39 @@
|
||||
// *****************************
|
||||
// * TABLE OF CONTENTS
|
||||
// *****************************/
|
||||
|
||||
// Default Exclusions
|
||||
// Anything not exlcuded is included, default Headers are H1, H2, and H3.
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
.page:has(.frontCover),
|
||||
.page:has(.backCover),
|
||||
.page:has(.insideCover),
|
||||
.monster,
|
||||
.noToC,
|
||||
.toc { --TOC: exclude; }
|
||||
|
||||
.tocDepthH2 :is(h1, h2) {--TOC: include; }
|
||||
.tocDepthH3 :is(h1, h2, h3) {--TOC: include; }
|
||||
.tocDepthH4 :is(h1, h2, h3, h4) {--TOC: include; }
|
||||
.tocDepthH5 :is(h1, h2, h3, h4, h5) {--TOC: include; }
|
||||
.tocDepthH6 :is(h1, h2, h3, h4, h5, h6) {--TOC: include; }
|
||||
|
||||
.tocIncludeH1 h1 {--TOC: include; }
|
||||
.tocIncludeH2 h2 {--TOC: include; }
|
||||
.tocIncludeH3 h3 {--TOC: include; }
|
||||
.tocIncludeH4 h4 {--TOC: include; }
|
||||
.tocIncludeH5 h5 {--TOC: include; }
|
||||
.tocIncludeH6 h6 {--TOC: include; }
|
||||
|
||||
.page:has(.partCover) {
|
||||
--TOC: exclude;
|
||||
& h1 {
|
||||
--TOC: include;
|
||||
}
|
||||
}
|
||||
|
||||
.page {
|
||||
&:has(.toc)::after { display : none; }
|
||||
.toc {
|
||||
@@ -853,6 +892,9 @@
|
||||
.useColumns(0.96, @fillMode: balance);
|
||||
}
|
||||
}
|
||||
.toc.wide li {
|
||||
break-inside: auto;
|
||||
}
|
||||
}
|
||||
|
||||
// *****************************
|
||||
@@ -877,6 +919,10 @@
|
||||
|
||||
.page h1 + * { margin-top : 0; }
|
||||
|
||||
.page .descriptive.wide + * {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
//*****************************
|
||||
// * RUNE TABLE
|
||||
// *****************************/
|
||||
|
||||
@@ -307,8 +307,8 @@ module.exports = [
|
||||
/**************** FONTS *************/
|
||||
{
|
||||
groupName : 'Fonts',
|
||||
icon : 'fas fa-keyboard',
|
||||
view : 'text',
|
||||
icon : 'fas fa-keyboard',
|
||||
view : 'text',
|
||||
snippets : [
|
||||
{
|
||||
name : 'Open Sans',
|
||||
@@ -341,59 +341,59 @@ module.exports = [
|
||||
gen : dedent`{{font-family:MrEavesRemake Dummy Text}}`
|
||||
},
|
||||
{
|
||||
name: 'Solbera Imitation',
|
||||
icon: 'font SolberaImitationRemake',
|
||||
gen: dedent`{{font-family:SolberaImitationRemake Dummy Text}}`
|
||||
name : 'Solbera Imitation',
|
||||
icon : 'font SolberaImitationRemake',
|
||||
gen : dedent`{{font-family:SolberaImitationRemake Dummy Text}}`
|
||||
},
|
||||
{
|
||||
name: 'Scaly Sans Small Caps',
|
||||
icon: 'font ScalySansSmallCapsRemake',
|
||||
gen: dedent`{{font-family:ScalySansSmallCapsRemake Dummy Text}}`
|
||||
name : 'Scaly Sans Small Caps',
|
||||
icon : 'font ScalySansSmallCapsRemake',
|
||||
gen : dedent`{{font-family:ScalySansSmallCapsRemake Dummy Text}}`
|
||||
},
|
||||
{
|
||||
name: 'Walter Turncoat',
|
||||
icon: 'font WalterTurncoat',
|
||||
gen: dedent`{{font-family:WalterTurncoat Dummy Text}}`
|
||||
name : 'Walter Turncoat',
|
||||
icon : 'font WalterTurncoat',
|
||||
gen : dedent`{{font-family:WalterTurncoat Dummy Text}}`
|
||||
},
|
||||
{
|
||||
name: 'Lato',
|
||||
icon: 'font Lato',
|
||||
gen: dedent`{{font-family:Lato Dummy Text}}`
|
||||
name : 'Lato',
|
||||
icon : 'font Lato',
|
||||
gen : dedent`{{font-family:Lato Dummy Text}}`
|
||||
},
|
||||
{
|
||||
name: 'Courier',
|
||||
icon: 'font Courier',
|
||||
gen: dedent`{{font-family:Courier Dummy Text}}`
|
||||
name : 'Courier',
|
||||
icon : 'font Courier',
|
||||
gen : dedent`{{font-family:Courier Dummy Text}}`
|
||||
},
|
||||
{
|
||||
name: 'Nodesto Caps Condensed',
|
||||
icon: 'font NodestoCapsCondensed',
|
||||
gen: dedent`{{font-family:NodestoCapsCondensed Dummy Text}}`
|
||||
name : 'Nodesto Caps Condensed',
|
||||
icon : 'font NodestoCapsCondensed',
|
||||
gen : dedent`{{font-family:NodestoCapsCondensed Dummy Text}}`
|
||||
},
|
||||
{
|
||||
name: 'Overpass',
|
||||
icon: 'font Overpass',
|
||||
gen: dedent`{{font-family:Overpass Dummy Text}}`
|
||||
name : 'Overpass',
|
||||
icon : 'font Overpass',
|
||||
gen : dedent`{{font-family:Overpass Dummy Text}}`
|
||||
},
|
||||
{
|
||||
name: 'Davek',
|
||||
icon: 'font Davek',
|
||||
gen: dedent`{{font-family:Davek Dummy Text}}`
|
||||
name : 'Davek',
|
||||
icon : 'font Davek',
|
||||
gen : dedent`{{font-family:Davek Dummy Text}}`
|
||||
},
|
||||
{
|
||||
name: 'Iokharic',
|
||||
icon: 'font Iokharic',
|
||||
gen: dedent`{{font-family:Iokharic Dummy Text}}`
|
||||
name : 'Iokharic',
|
||||
icon : 'font Iokharic',
|
||||
gen : dedent`{{font-family:Iokharic Dummy Text}}`
|
||||
},
|
||||
{
|
||||
name: 'Rellanic',
|
||||
icon: 'font Rellanic',
|
||||
gen: dedent`{{font-family:Rellanic Dummy Text}}`
|
||||
name : 'Rellanic',
|
||||
icon : 'font Rellanic',
|
||||
gen : dedent`{{font-family:Rellanic Dummy Text}}`
|
||||
},
|
||||
{
|
||||
name: 'Times New Roman',
|
||||
icon: 'font TimesNewRoman',
|
||||
gen: dedent`{{font-family:"Times New Roman" Dummy Text}}`
|
||||
name : 'Times New Roman',
|
||||
icon : 'font TimesNewRoman',
|
||||
gen : dedent`{{font-family:"Times New Roman" Dummy Text}}`
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
@import (less) './themes/fonts/5e/fonts.less';
|
||||
@import (less) './themes/assets/assets.less';
|
||||
@import (less) './themes/fonts/icon fonts/diceFont.less';
|
||||
@import (less) './themes/fonts/iconFonts/elderberryInn.less';
|
||||
@import (less) './themes/fonts/iconFonts/diceFont.less';
|
||||
@import (less) './themes/fonts/iconFonts/gameIcons.less';
|
||||
@import (less) './themes/fonts/iconFonts/fontAwesome.less';
|
||||
|
||||
:root {
|
||||
//Colors
|
||||
@@ -233,7 +236,7 @@ body { counter-reset : page-numbers; }
|
||||
left : 50%;
|
||||
width : 50%;
|
||||
height : 50%;
|
||||
transform : translateX(-50%) translateY(50%) rotate(calc(-1deg * var(--rotation))) scaleX(calc(1 / var(--scaleX))) scaleY(calc(1 / var(--scaleY)));
|
||||
transform : translateX(-50%) translateY(50%) scaleX(calc(1 / var(--scaleX))) scaleY(calc(1 / var(--scaleY))) rotate(calc(-1deg * var(--rotation)));
|
||||
}
|
||||
& img {
|
||||
position : absolute;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name" : "Journal",
|
||||
"renderer" : "V3",
|
||||
"baseTheme" : false,
|
||||
"baseTheme" : "Blank",
|
||||
"baseSnippets" : "5ePHB"
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
@noteBorderImage : url('/assets/noteBorder.png');
|
||||
@descriptiveBoxImage : url('/assets/descriptiveBorder.png');
|
||||
@monsterBlockBackground : url('/assets/parchmentBackgroundGrayscale.jpg');
|
||||
@monsterBlockOverlay : url('/assets/parchmentBackgroundOverlayed.jpg');
|
||||
@monsterBorderImage : url('/assets/monsterBorderFancy.png');
|
||||
@codeBorderImage : url('/assets/codeBorder.png');
|
||||
@classTableDecoration : url('/assets/classTableDecoration.png');
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
background-color: rgba(35,153,153,0.5);
|
||||
}
|
||||
.pageLine {
|
||||
background-color: rgba(255,255,255,0.75);
|
||||
background-color: rgba(255,255,255,0.5);
|
||||
& ~ pre.CodeMirror-line {
|
||||
color: black;
|
||||
}
|
||||
@@ -85,4 +85,4 @@
|
||||
// Future styling for themes with light backgrounds
|
||||
--dummyVar: 'currently unused';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,8 +74,9 @@
|
||||
@font-face {
|
||||
font-family: SolberaImitationRemake; //Tweaked 5e version
|
||||
src: url('../../../fonts/5e/Solbera Imitation Tweak.woff2');
|
||||
font-weight: normal;
|
||||
font-weight: 100 1000;
|
||||
font-style: normal;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Cover Page */
|
||||
|
||||
@@ -1,224 +0,0 @@
|
||||
/* Icon Font: Elderberry Inn */
|
||||
@font-face {
|
||||
font-family : 'Elderberry-Inn';
|
||||
font-style : normal;
|
||||
font-weight : normal;
|
||||
src : url('../../../fonts/icon fonts/Elderberry-Inn-Icons.woff2');
|
||||
}
|
||||
|
||||
.page {
|
||||
span.ei {
|
||||
display : inline-block;
|
||||
margin-right : 3px;
|
||||
font-family : 'Elderberry-Inn';
|
||||
line-height : 1;
|
||||
vertical-align : baseline;
|
||||
-moz-osx-font-smoothing : grayscale;
|
||||
-webkit-font-smoothing : antialiased;
|
||||
text-rendering : auto;
|
||||
|
||||
&.book::before { content : '\E900'; }
|
||||
&.screen::before { content : '\E901'; }
|
||||
|
||||
/* Spell levels */
|
||||
&.spell-0::before { content : '\E902'; }
|
||||
&.spell-1::before { content : '\E903'; }
|
||||
&.spell-2::before { content : '\E904'; }
|
||||
&.spell-3::before { content : '\E905'; }
|
||||
&.spell-4::before { content : '\E906'; }
|
||||
&.spell-5::before { content : '\E907'; }
|
||||
&.spell-6::before { content : '\E908'; }
|
||||
&.spell-7::before { content : '\E909'; }
|
||||
&.spell-8::before { content : '\E90A'; }
|
||||
&.spell-9::before { content : '\E90B'; }
|
||||
|
||||
/* Damage types */
|
||||
&.acid::before { content : '\E90C'; }
|
||||
&.bludgeoning::before { content : '\E90D'; }
|
||||
&.cold::before { content : '\E90E'; }
|
||||
&.fire::before { content : '\E90F'; }
|
||||
&.force::before { content : '\E910'; }
|
||||
&.lightning::before { content : '\E911'; }
|
||||
&.necrotic::before { content : '\E912'; }
|
||||
&.piercing::before { content : '\E914'; }
|
||||
&.poison::before { content : '\E913'; }
|
||||
&.psychic::before { content : '\E915'; }
|
||||
&.radiant::before { content : '\E916'; }
|
||||
&.slashing::before { content : '\E917'; }
|
||||
&.thunder::before { content : '\E918'; }
|
||||
|
||||
/* DnD Conditions */
|
||||
&.blinded::before { content : '\E919'; }
|
||||
&.charmed::before { content : '\E91A'; }
|
||||
&.deafened::before { content : '\E91B'; }
|
||||
&.exhaust-1::before { content : '\E91C'; }
|
||||
&.exhaust-2::before { content : '\E91D'; }
|
||||
&.exhaust-3::before { content : '\E91E'; }
|
||||
&.exhaust-4::before { content : '\E91F'; }
|
||||
&.exhaust-5::before { content : '\E920'; }
|
||||
&.exhaust-6::before { content : '\E921'; }
|
||||
&.frightened::before { content : '\E922'; }
|
||||
&.grappled::before { content : '\E923'; }
|
||||
&.incapacitated::before { content : '\E924'; }
|
||||
&.invisible::before { content : '\E926'; }
|
||||
&.paralyzed::before { content : '\E927'; }
|
||||
&.petrified::before { content : '\E928'; }
|
||||
&.poisoned::before { content : '\E929'; }
|
||||
&.prone::before { content : '\E92A'; }
|
||||
&.restrained::before { content : '\E92B'; }
|
||||
&.stunned::before { content : '\E92C'; }
|
||||
&.unconscious::before { content : '\E925'; }
|
||||
|
||||
/* Character Classes and Features */
|
||||
&.barbarian-rage::before { content : '\E92D'; }
|
||||
&.barbarian-reckless-attack::before { content : '\E92E'; }
|
||||
&.bardic-inspiration::before { content : '\E92F'; }
|
||||
&.cleric-channel-divinity::before { content : '\E930'; }
|
||||
&.druid-wild-shape::before { content : '\E931'; }
|
||||
&.fighter-action-surge::before { content : '\E932'; }
|
||||
&.fighter-second-wind::before { content : '\E933'; }
|
||||
&.monk-flurry-blows::before { content : '\E934'; }
|
||||
&.monk-patient-defense::before { content : '\E935'; }
|
||||
&.monk-step-of-the-wind::before { content : '\E936'; }
|
||||
&.monk-step-of-the-wind-2::before { content : '\E937'; }
|
||||
&.monk-step-of-the-wind-3::before { content : '\E938'; }
|
||||
&.monk-stunning-strike::before { content : '\E939'; }
|
||||
&.monk-stunning-strike-2::before { content : '\E939'; }
|
||||
&.paladin-divine-smite::before { content : '\E93B'; }
|
||||
&.paladin-lay-on-hands::before { content : '\E93C'; }
|
||||
&.barbarian-abilities::before { content : '\E93D'; }
|
||||
&.barbarian::before { content : '\E93E'; }
|
||||
&.bard-abilities::before { content : '\E93F'; }
|
||||
&.bard::before { content : '\E940'; }
|
||||
&.cleric-abilities::before { content : '\E941'; }
|
||||
&.cleric::before { content : '\E942'; }
|
||||
&.druid-abilities::before { content : '\E943'; }
|
||||
&.druid::before { content : '\E944'; }
|
||||
&.fighter-abilities::before { content : '\E945'; }
|
||||
&.fighter::before { content : '\E946'; }
|
||||
&.monk-abilities::before { content : '\E947'; }
|
||||
&.monk::before { content : '\E948'; }
|
||||
&.paladin-abilities::before { content : '\E949'; }
|
||||
&.paladin::before { content : '\E94A'; }
|
||||
&.ranger-abilities::before { content : '\E94B'; }
|
||||
&.ranger::before { content : '\E94C'; }
|
||||
&.rogue-abilities::before { content : '\E94D'; }
|
||||
&.rogue::before { content : '\E94E'; }
|
||||
&.sorcerer-abilities::before { content : '\E94F'; }
|
||||
&.sorcerer::before { content : '\E950'; }
|
||||
&.warlock-abilities::before { content : '\E951'; }
|
||||
&.warlock::before { content : '\E952'; }
|
||||
&.wizard-abilities::before { content : '\E953'; }
|
||||
&.wizard::before { content : '\E954'; }
|
||||
|
||||
/* Types of actions */
|
||||
&.movement::before { content : '\E955'; }
|
||||
&.action::before { content : '\E956'; }
|
||||
&.bonus-action::before { content : '\E957'; }
|
||||
&.reaction::before { content : '\E958'; }
|
||||
|
||||
/* SRD Spells */
|
||||
&.acid-arrow::before { content : '\E959'; }
|
||||
&.action-1::before { content : '\E95A'; }
|
||||
&.alter-self::before { content : '\E95B'; }
|
||||
&.alter-self-2::before { content : '\E95C'; }
|
||||
&.animal-friendship::before { content : '\E95E'; }
|
||||
&.animate-dead::before { content : '\E95F'; }
|
||||
&.animate-objects::before { content : '\E960'; }
|
||||
&.animate-objects-2::before { content : '\E961'; }
|
||||
&.bane::before { content : '\E962'; }
|
||||
&.bless::before { content : '\E963'; }
|
||||
&.blur::before { content : '\E964'; }
|
||||
&.bonus::before { content : '\E965'; }
|
||||
&.branding-smite::before { content : '\E966'; }
|
||||
&.burning-hands::before { content : '\E967'; }
|
||||
&.charm-person::before { content : '\E968'; }
|
||||
&.chill-touch::before { content : '\E969'; }
|
||||
&.cloudkill::before { content : '\E96A'; }
|
||||
&.comprehend-languages::before { content : '\E96B'; }
|
||||
&.cone-of-cold::before { content : '\E96C'; }
|
||||
&.conjure-elemental::before { content : '\E96D'; }
|
||||
&.conjure-minor-elemental::before { content : '\E96E'; }
|
||||
&.control-water::before { content : '\E96F'; }
|
||||
&.counterspell::before { content : '\E970'; }
|
||||
&.cure-wounds::before { content : '\E971'; }
|
||||
&.dancing-lights::before { content : '\E972'; }
|
||||
&.darkness::before { content : '\E973'; }
|
||||
&.detect-magic::before { content : '\E974'; }
|
||||
&.disguise-self::before { content : '\E975'; }
|
||||
&.disintegrate::before { content : '\E976'; }
|
||||
&.dispel-evil-and-good::before { content : '\E977'; }
|
||||
&.dispel-magic::before { content : '\E978'; }
|
||||
&.dominate-monster::before { content : '\E979'; }
|
||||
&.dominate-person::before { content : '\E97A'; }
|
||||
&.eldritch-blast::before { content : '\E97B'; }
|
||||
&.enlarge-reduce::before { content : '\E97C'; }
|
||||
&.entangle::before { content : '\E97D'; }
|
||||
&.faerie-fire::before { content : '\E97E'; }
|
||||
&.faerie-fire2::before { content : '\E97F'; }
|
||||
&.feather-fall::before { content : '\E980'; }
|
||||
&.find-familiar::before { content : '\E981'; }
|
||||
&.finger-of-death::before { content : '\E982'; }
|
||||
&.fireball::before { content : '\E983'; }
|
||||
&.floating-disk::before { content : '\E984'; }
|
||||
&.fly::before { content : '\E985'; }
|
||||
&.fog-cloud::before { content : '\E986'; }
|
||||
&.gaseous-form::before { content : '\E987'; }
|
||||
&.gaseous-form2::before { content : '\E988'; }
|
||||
&.gentle-repose::before { content : '\E989'; }
|
||||
&.gentle-repose2::before { content : '\E98A'; }
|
||||
&.globe-of-invulnerability::before { content : '\E98B'; }
|
||||
&.guiding-bolt::before { content : '\E98C'; }
|
||||
&.healing-word::before { content : '\E98D'; }
|
||||
&.heat-metal::before { content : '\E98E'; }
|
||||
&.hellish-rebuke::before { content : '\E98F'; }
|
||||
&.heroes-feast::before { content : '\E990'; }
|
||||
&.heroism::before { content : '\E991'; }
|
||||
&.hideous-laughter::before { content : '\E992'; }
|
||||
&.identify::before { content : '\E993'; }
|
||||
&.illusory-script::before { content : '\E994'; }
|
||||
&.inflict-wounds::before { content : '\E995'; }
|
||||
&.light::before { content : '\E996'; }
|
||||
&.longstrider::before { content : '\E997'; }
|
||||
&.mage-armor::before { content : '\E998'; }
|
||||
&.mage-hand::before { content : '\E999'; }
|
||||
&.magic-missile::before { content : '\E99A'; }
|
||||
&.mass-cure-wounds::before { content : '\E99B'; }
|
||||
&.mass-healing-word::before { content : '\E99C'; }
|
||||
&.Mending::before { content : '\E99D'; }
|
||||
&.message::before { content : '\E99E'; }
|
||||
&.Minor-illusion::before { content : '\E99F'; }
|
||||
&.movement1::before { content : '\E9A0'; }
|
||||
&.polymorph::before { content : '\E9A1'; }
|
||||
&.power-word-kill::before { content : '\E9A2'; }
|
||||
&.power-word-stun::before { content : '\E9A3'; }
|
||||
&.prayer-of-healing::before { content : '\E9A4'; }
|
||||
&.prestidigitation::before { content : '\E9A5'; }
|
||||
&.protection-from-evil-and-good::before { content : '\E9A6'; }
|
||||
&.raise-read::before { content : '\E9A7'; }
|
||||
&.raise-read2::before { content : '\E9A8'; }
|
||||
&.reaction1::before { content : '\E9A9'; }
|
||||
&.resurrection::before { content : '\E9AA'; }
|
||||
&.resurrection2::before { content : '\E9AB'; }
|
||||
&.revivify::before { content : '\E9AC'; }
|
||||
&.revivify2::before { content : '\E9AD'; }
|
||||
&.sacred-flame::before { content : '\E9AE'; }
|
||||
&.sanctuary::before { content : '\E9AF'; }
|
||||
&.scorching-ray::before { content : '\E9B0'; }
|
||||
&.sending::before { content : '\E9B1'; }
|
||||
&.shatter::before { content : '\E9B2'; }
|
||||
&.shield::before { content : '\E9B3'; }
|
||||
&.silent-image::before { content : '\E9B4'; }
|
||||
&.sleep::before { content : '\E9B5'; }
|
||||
&.speak-with-animals::before { content : '\E9B6'; }
|
||||
&.telekinesis::before { content : '\E9B7'; }
|
||||
&.true-strike::before { content : '\E9B8'; }
|
||||
&.vicious-mockery::before { content : '\E9B9'; }
|
||||
&.wall-of-fire::before { content : '\E9BA'; }
|
||||
&.wall-of-force::before { content : '\E9BB'; }
|
||||
&.wall-of-ice::before { content : '\E9BC'; }
|
||||
&.wall-of-stone::before { content : '\E9BD'; }
|
||||
&.wall-of-thorns::before { content : '\E9BE'; }
|
||||
&.wish::before { content : '\E9BF'; }
|
||||
}
|
||||
}
|
||||
96
themes/fonts/iconFonts/diceFont.js
Normal file
96
themes/fonts/iconFonts/diceFont.js
Normal file
@@ -0,0 +1,96 @@
|
||||
const diceFont = {
|
||||
'df_f' : 'df F',
|
||||
'df_f_minus' : 'df F-minus',
|
||||
'df_f_plus' : 'df F-plus',
|
||||
'df_f_zero' : 'df F-zero',
|
||||
'df_d10' : 'df d10',
|
||||
'df_d10_1' : 'df d10-1',
|
||||
'df_d10_10' : 'df d10-10',
|
||||
'df_d10_2' : 'df d10-2',
|
||||
'df_d10_3' : 'df d10-3',
|
||||
'df_d10_4' : 'df d10-4',
|
||||
'df_d10_5' : 'df d10-5',
|
||||
'df_d10_6' : 'df d10-6',
|
||||
'df_d10_7' : 'df d10-7',
|
||||
'df_d10_8' : 'df d10-8',
|
||||
'df_d10_9' : 'df d10-9',
|
||||
'df_d12' : 'df d12',
|
||||
'df_d12_1' : 'df d12-1',
|
||||
'df_d12_10' : 'df d12-10',
|
||||
'df_d12_11' : 'df d12-11',
|
||||
'df_d12_12' : 'df d12-12',
|
||||
'df_d12_2' : 'df d12-2',
|
||||
'df_d12_3' : 'df d12-3',
|
||||
'df_d12_4' : 'df d12-4',
|
||||
'df_d12_5' : 'df d12-5',
|
||||
'df_d12_6' : 'df d12-6',
|
||||
'df_d12_7' : 'df d12-7',
|
||||
'df_d12_8' : 'df d12-8',
|
||||
'df_d12_9' : 'df d12-9',
|
||||
'df_d2' : 'df d2',
|
||||
'df_d2_1' : 'df d2-1',
|
||||
'df_d2_2' : 'df d2-2',
|
||||
'df_d20' : 'df d20',
|
||||
'df_d20_1' : 'df d20-1',
|
||||
'df_d20_10' : 'df d20-10',
|
||||
'df_d20_11' : 'df d20-11',
|
||||
'df_d20_12' : 'df d20-12',
|
||||
'df_d20_13' : 'df d20-13',
|
||||
'df_d20_14' : 'df d20-14',
|
||||
'df_d20_15' : 'df d20-15',
|
||||
'df_d20_16' : 'df d20-16',
|
||||
'df_d20_17' : 'df d20-17',
|
||||
'df_d20_18' : 'df d20-18',
|
||||
'df_d20_19' : 'df d20-19',
|
||||
'df_d20_2' : 'df d20-2',
|
||||
'df_d20_20' : 'df d20-20',
|
||||
'df_d20_3' : 'df d20-3',
|
||||
'df_d20_4' : 'df d20-4',
|
||||
'df_d20_5' : 'df d20-5',
|
||||
'df_d20_6' : 'df d20-6',
|
||||
'df_d20_7' : 'df d20-7',
|
||||
'df_d20_8' : 'df d20-8',
|
||||
'df_d20_9' : 'df d20-9',
|
||||
'df_d4' : 'df d4',
|
||||
'df_d4_1' : 'df d4-1',
|
||||
'df_d4_2' : 'df d4-2',
|
||||
'df_d4_3' : 'df d4-3',
|
||||
'df_d4_4' : 'df d4-4',
|
||||
'df_d6' : 'df d6',
|
||||
'df_d6_1' : 'df d6-1',
|
||||
'df_d6_2' : 'df d6-2',
|
||||
'df_d6_3' : 'df d6-3',
|
||||
'df_d6_4' : 'df d6-4',
|
||||
'df_d6_5' : 'df d6-5',
|
||||
'df_d6_6' : 'df d6-6',
|
||||
'df_d8' : 'df d8',
|
||||
'df_d8_1' : 'df d8-1',
|
||||
'df_d8_2' : 'df d8-2',
|
||||
'df_d8_3' : 'df d8-3',
|
||||
'df_d8_4' : 'df d8-4',
|
||||
'df_d8_5' : 'df d8-5',
|
||||
'df_d8_6' : 'df d8-6',
|
||||
'df_d8_7' : 'df d8-7',
|
||||
'df_d8_8' : 'df d8-8',
|
||||
'df_dot_d6' : 'df dot-d6',
|
||||
'df_dot_d6_1' : 'df dot-d6-1',
|
||||
'df_dot_d6_2' : 'df dot-d6-2',
|
||||
'df_dot_d6_3' : 'df dot-d6-3',
|
||||
'df_dot_d6_4' : 'df dot-d6-4',
|
||||
'df_dot_d6_5' : 'df dot-d6-5',
|
||||
'df_dot_d6_6' : 'df dot-d6-6',
|
||||
'df_small_dot_d6_1' : 'df small-dot-d6-1',
|
||||
'df_small_dot_d6_2' : 'df small-dot-d6-2',
|
||||
'df_small_dot_d6_3' : 'df small-dot-d6-3',
|
||||
'df_small_dot_d6_4' : 'df small-dot-d6-4',
|
||||
'df_small_dot_d6_5' : 'df small-dot-d6-5',
|
||||
'df_small_dot_d6_6' : 'df small-dot-d6-6',
|
||||
'df_solid_small_dot_d6_1' : 'df solid-small-dot-d6-1',
|
||||
'df_solid_small_dot_d6_2' : 'df solid-small-dot-d6-2',
|
||||
'df_solid_small_dot_d6_3' : 'df solid-small-dot-d6-3',
|
||||
'df_solid_small_dot_d6_4' : 'df solid-small-dot-d6-4',
|
||||
'df_solid_small_dot_d6_5' : 'df solid-small-dot-d6-5',
|
||||
'df_solid_small_dot_d6_6' : 'df solid-small-dot-d6-6'
|
||||
};
|
||||
|
||||
module.exports = diceFont;
|
||||
@@ -3,11 +3,11 @@
|
||||
font-family : 'DiceFont';
|
||||
font-style : normal;
|
||||
font-weight : normal;
|
||||
src : url('../../../fonts/icon fonts/diceFont.woff2');
|
||||
src : url('../../../fonts/iconFonts/diceFont.woff2');
|
||||
}
|
||||
|
||||
.df {
|
||||
display : inline-block;
|
||||
display : inline;
|
||||
font-family : 'DiceFont';
|
||||
font-style : normal;
|
||||
font-weight : normal;
|
||||
@@ -16,8 +16,11 @@
|
||||
text-decoration : inherit;
|
||||
text-transform : none;
|
||||
text-rendering : optimizeLegibility;
|
||||
-moz-osx-font-smoothing : grayscale;
|
||||
|
||||
/* Better Font Rendering =========== */
|
||||
-webkit-font-smoothing : antialiased;
|
||||
-moz-osx-font-smoothing : grayscale;
|
||||
|
||||
&.F::before { content : '\f190'; }
|
||||
&.F-minus::before { content : '\f191'; }
|
||||
&.F-plus::before { content : '\f192'; }
|
||||
209
themes/fonts/iconFonts/elderberryInn.js
Normal file
209
themes/fonts/iconFonts/elderberryInn.js
Normal file
@@ -0,0 +1,209 @@
|
||||
const elderberryInn = {
|
||||
'ei_book' : 'ei book',
|
||||
'ei_screen' : 'ei screen',
|
||||
|
||||
/* Spell levels */
|
||||
'ei_spell_0' : 'ei spell-0',
|
||||
'ei_spell_1' : 'ei spell-1',
|
||||
'ei_spell_2' : 'ei spell-2',
|
||||
'ei_spell_3' : 'ei spell-3',
|
||||
'ei_spell_4' : 'ei spell-4',
|
||||
'ei_spell_5' : 'ei spell-5',
|
||||
'ei_spell_6' : 'ei spell-6',
|
||||
'ei_spell_7' : 'ei spell-7',
|
||||
'ei_spell_8' : 'ei spell-8',
|
||||
'ei_spell_9' : 'ei spell-9',
|
||||
|
||||
/* Damage types */
|
||||
'ei_acid' : 'ei acid',
|
||||
'ei_bludgeoning' : 'ei bludgeoning',
|
||||
'ei_cold' : 'ei cold',
|
||||
'ei_fire' : 'ei fire',
|
||||
'ei_force' : 'ei force',
|
||||
'ei_lightning' : 'ei lightning',
|
||||
'ei_necrotic' : 'ei necrotic',
|
||||
'ei_piercing' : 'ei piercing',
|
||||
'ei_poison' : 'ei poison',
|
||||
'ei_psychic' : 'ei psychic',
|
||||
'ei_radiant' : 'ei radiant',
|
||||
'ei_slashing' : 'ei slashing',
|
||||
'ei_thunder' : 'ei thunder',
|
||||
|
||||
/* DnD Conditions */
|
||||
'ei_blinded' : 'ei blinded',
|
||||
'ei_charmed' : 'ei charmed',
|
||||
'ei_deafened' : 'ei deafened',
|
||||
'ei_exhaust1' : 'ei exhaust-1',
|
||||
'ei_blinded' : 'ei blinded',
|
||||
'ei_exhaust2' : 'ei exhaust-2',
|
||||
'ei_exhaust3' : 'ei exhaust-3',
|
||||
'ei_exhaust4' : 'ei exhaust-4',
|
||||
'ei_exhaust5' : 'ei exhaust-5',
|
||||
'ei_exhaust6' : 'ei exhaust-6',
|
||||
'ei_frightened' : 'ei frightened',
|
||||
'ei_grappled' : 'ei grappled',
|
||||
'ei_incapacitated' : 'ei incapacitated',
|
||||
'ei_invisible' : 'ei invisible',
|
||||
'ei_paralyzed' : 'ei paralyzed',
|
||||
'ei_petrified' : 'ei petrified',
|
||||
'ei_poisoned' : 'ei poisoned',
|
||||
'ei_prone' : 'ei prone',
|
||||
'ei_restrained' : 'ei restrained',
|
||||
'ei_stunned' : 'ei stunned',
|
||||
'ei_unconscious' : 'ei unconscious',
|
||||
|
||||
/* Character Classes and Features */
|
||||
'ei_barbarian_rage' : 'ei barbarian-rage',
|
||||
'ei_barbarian_reckless_attack' : 'ei barbarian-reckless-attack',
|
||||
'ei_bardic_inspiration' : 'ei bardic-inspiration',
|
||||
'ei_cleric_channel_divinity' : 'ei cleric-channel-divinity',
|
||||
'ei_druid_wild_shape' : 'ei druid-wild-shape',
|
||||
'ei_fighter_action_surge' : 'ei fighter-action-surge',
|
||||
'ei_fighter_second_wind' : 'ei fighter-second-wind',
|
||||
'ei_monk_flurry_blows' : 'ei monk-flurry-blows',
|
||||
'ei_monk_patient_defense' : 'ei monk-patient-defense',
|
||||
'ei_monk_step_of_the_wind' : 'ei monk-step-of-the-wind',
|
||||
'ei_monk_step_of_the_wind2' : 'ei monk-step-of-the-wind-2',
|
||||
'ei_monk_step_of_the_wind3' : 'ei monk-step-of-the-wind-3',
|
||||
'ei_monk_stunning_strike' : 'ei monk-stunning-strike',
|
||||
'ei_monk_stunning_strike2' : 'ei monk-stunning-strike-2',
|
||||
'ei_paladin_divine_smite' : 'ei paladin-divine-smite',
|
||||
'ei_paladin_lay_on_hands' : 'ei paladin-lay-on-hands',
|
||||
'ei_barbarian_abilities' : 'ei barbarian-abilities',
|
||||
'ei_barbarian' : 'ei barbarian',
|
||||
'ei_bard_abilities' : 'ei bard-abilities',
|
||||
'ei_bard' : 'ei bard',
|
||||
'ei_cleric_abilities' : 'ei cleric-abilities',
|
||||
'ei_cleric' : 'ei cleric',
|
||||
'ei_druid_abilities' : 'ei druid-abilities',
|
||||
'ei_druid' : 'ei druid',
|
||||
'ei_fighter_abilities' : 'ei fighter-abilities',
|
||||
'ei_fighter' : 'ei fighter',
|
||||
'ei_monk_abilities' : 'ei monk-abilities',
|
||||
'ei_monk' : 'ei monk',
|
||||
'ei_paladin_abilities' : 'ei paladin-abilities',
|
||||
'ei_paladin' : 'ei paladin',
|
||||
'ei_ranger_abilities' : 'ei ranger-abilities',
|
||||
'ei_ranger' : 'ei ranger',
|
||||
'ei_rogue_abilities' : 'ei rogue-abilities',
|
||||
'ei_rogue' : 'ei rogue',
|
||||
'ei_sorcerer_abilities' : 'ei sorcerer-abilities',
|
||||
'ei_sorcerer' : 'ei sorcerer',
|
||||
'ei_warlock_abilities' : 'ei warlock-abilities',
|
||||
'ei_warlock' : 'ei warlock',
|
||||
'ei_wizard_abilities' : 'ei wizard-abilities',
|
||||
'ei_wizard' : 'ei wizard',
|
||||
|
||||
/* Types of actions */
|
||||
'ei_movement' : 'ei movement',
|
||||
'ei_action' : 'ei action',
|
||||
'ei_bonus_action' : 'ei bonus-action',
|
||||
'ei_reaction' : 'ei reaction',
|
||||
|
||||
/* SRD Spells */
|
||||
'ei_acid_arrow' : 'ei acid-arrow',
|
||||
'ei_action1' : 'ei action-1',
|
||||
'ei_alter_self' : 'ei alter-self',
|
||||
'ei_alter_self2' : 'ei alter-self-2',
|
||||
'ei_magic_beans' : 'ei magic-beans',
|
||||
'ei_animal_friendship' : 'ei animal-friendship',
|
||||
'ei_animate_dead' : 'ei animate-dead',
|
||||
'ei_animate_objects' : 'ei animate-objects',
|
||||
'ei_animate_objects2' : 'ei animate-objects-2',
|
||||
'ei_bane' : 'ei bane',
|
||||
'ei_bless' : 'ei bless',
|
||||
'ei_blur' : 'ei blur',
|
||||
'ei_bonus' : 'ei bonus',
|
||||
'ei_branding_smite' : 'ei branding-smite',
|
||||
'ei_burning_hands' : 'ei burning-hands',
|
||||
'ei_charm_person' : 'ei charm-person',
|
||||
'ei_chill_touch' : 'ei chill-touch',
|
||||
'ei_cloudkill' : 'ei cloudkill',
|
||||
'ei_comprehend_languages' : 'ei comprehend-languages',
|
||||
'ei_cone_of_cold' : 'ei cone-of-cold',
|
||||
'ei_conjure_elemental' : 'ei conjure-elemental',
|
||||
'ei_conjure_minor_elemental' : 'ei conjure-minor-elemental',
|
||||
'ei_control_water' : 'ei control-water',
|
||||
'ei_counterspell' : 'ei counterspell',
|
||||
'ei_cure_wounds' : 'ei cure-wounds',
|
||||
'ei_dancing_lights' : 'ei dancing-lights',
|
||||
'ei_darkness' : 'ei darkness',
|
||||
'ei_detect_magic' : 'ei detect-magic',
|
||||
'ei_disguise_self' : 'ei disguise-self',
|
||||
'ei_disintegrate' : 'ei disintegrate',
|
||||
'ei_dispel_evil_and_good' : 'ei dispel-evil-and-good',
|
||||
'ei_dispel_magic' : 'ei dispel-magic',
|
||||
'ei_dominate_monster' : 'ei dominate-monster',
|
||||
'ei_dominate_person' : 'ei dominate-person',
|
||||
'ei_eldritch_blast' : 'ei eldritch-blast',
|
||||
'ei_enlarge_reduce' : 'ei enlarge-reduce',
|
||||
'ei_entangle' : 'ei entangle',
|
||||
'ei_faerie_fire' : 'ei faerie-fire',
|
||||
'ei_faerie_fire2' : 'ei faerie-fire2',
|
||||
'ei_feather_fall' : 'ei feather-fall',
|
||||
'ei_find_familiar' : 'ei find-familiar',
|
||||
'ei_finger_of_death' : 'ei finger-of-death',
|
||||
'ei_fireball' : 'ei fireball',
|
||||
'ei_floating_disk' : 'ei floating-disk',
|
||||
'ei_fly' : 'ei fly',
|
||||
'ei_fog_cloud' : 'ei fog-cloud',
|
||||
'ei_gaseous_form' : 'ei gaseous-form',
|
||||
'ei_gaseous_form2' : 'ei gaseous-form2',
|
||||
'ei_gentle_repose' : 'ei gentle-repose',
|
||||
'ei_gentle_repose2' : 'ei gentle-repose2',
|
||||
'ei_globe_of_invulnerability' : 'ei globe-of-invulnerability',
|
||||
'ei_guiding_bolt' : 'ei guiding-bolt',
|
||||
'ei_healing_word' : 'ei healing-word',
|
||||
'ei_heat_metal' : 'ei heat-metal',
|
||||
'ei_hellish_rebuke' : 'ei hellish-rebuke',
|
||||
'ei_heroes_feast' : 'ei heroes-feast',
|
||||
'ei_heroism' : 'ei heroism',
|
||||
'ei_hideous_laughter' : 'ei hideous-laughter',
|
||||
'ei_identify' : 'ei identify',
|
||||
'ei_illusory_script' : 'ei illusory-script',
|
||||
'ei_inflict_wounds' : 'ei inflict-wounds',
|
||||
'ei_light' : 'ei light',
|
||||
'ei_longstrider' : 'ei longstrider',
|
||||
'ei_mage_armor' : 'ei mage-armor',
|
||||
'ei_mage_hand' : 'ei mage-hand',
|
||||
'ei_magic_missile' : 'ei magic-missile',
|
||||
'ei_mass_cure_wounds' : 'ei mass-cure-wounds',
|
||||
'ei_mass_healing_word' : 'ei mass-healing-word',
|
||||
'ei_mending' : 'ei _mending',
|
||||
'ei_message' : 'ei message',
|
||||
'ei_minor_illusion' : 'ei _minor-illusion',
|
||||
'ei_movement1' : 'ei movement1',
|
||||
'ei_polymorph' : 'ei polymorph',
|
||||
'ei_power_word_kill' : 'ei power-word-kill',
|
||||
'ei_power_word_stun' : 'ei power-word-stun',
|
||||
'ei_prayer_of_healing' : 'ei prayer-of-healing',
|
||||
'ei_prestidigitation' : 'ei prestidigitation',
|
||||
'ei_protection_from_evil_and_good' : 'ei protection-from-evil-and-good',
|
||||
'ei_raise_dead' : 'ei raise-dead',
|
||||
'ei_raise_dead2' : 'ei raise-dead2',
|
||||
'ei_reaction1' : 'ei reaction1',
|
||||
'ei_resurrection' : 'ei resurrection',
|
||||
'ei_resurrection2' : 'ei resurrection2',
|
||||
'ei_revivify' : 'ei revivify',
|
||||
'ei_revivify2' : 'ei revivify2',
|
||||
'ei_sacred_flame' : 'ei sacred-flame',
|
||||
'ei_sanctuary' : 'ei sanctuary',
|
||||
'ei_scorching_ray' : 'ei scorching-ray',
|
||||
'ei_sending' : 'ei sending',
|
||||
'ei_shatter' : 'ei shatter',
|
||||
'ei_shield' : 'ei shield',
|
||||
'ei_silent_image' : 'ei silent-image',
|
||||
'ei_sleep' : 'ei sleep',
|
||||
'ei_speak_with_animals' : 'ei speak-with-animals',
|
||||
'ei_telekinesis' : 'ei telekinesis',
|
||||
'ei_true_strike' : 'ei true-strike',
|
||||
'ei_vicious_mockery' : 'ei vicious-mockery',
|
||||
'ei_wall_of_fire' : 'ei wall-of-fire',
|
||||
'ei_wall_of_force' : 'ei wall-of-force',
|
||||
'ei_wall_of_ice' : 'ei wall-of-ice',
|
||||
'ei_wall_of_stone' : 'ei wall-of-stone',
|
||||
'ei_wall_of_thorns' : 'ei wall-of-thorns',
|
||||
'ei_wish' : 'ei wish'
|
||||
};
|
||||
|
||||
module.exports = elderberryInn;
|
||||
225
themes/fonts/iconFonts/elderberryInn.less
Normal file
225
themes/fonts/iconFonts/elderberryInn.less
Normal file
@@ -0,0 +1,225 @@
|
||||
/* Icon Font: Elderberry Inn */
|
||||
@font-face {
|
||||
font-family : 'Elderberry-Inn';
|
||||
font-style : normal;
|
||||
font-weight : normal;
|
||||
src : url('../../../fonts/iconFonts/elderberryInn.woff2');
|
||||
}
|
||||
|
||||
.ei {
|
||||
display : inline;
|
||||
font-family : 'Elderberry-Inn';
|
||||
line-height : 1;
|
||||
vertical-align : baseline;
|
||||
text-rendering : auto;
|
||||
|
||||
/* Better Font Rendering =========== */
|
||||
-webkit-font-smoothing : antialiased;
|
||||
-moz-osx-font-smoothing : grayscale;
|
||||
|
||||
&.book::before { content : '\E900'; }
|
||||
&.screen::before { content : '\E901'; }
|
||||
|
||||
/* Spell levels */
|
||||
&.spell-0::before { content : '\E902'; }
|
||||
&.spell-1::before { content : '\E903'; }
|
||||
&.spell-2::before { content : '\E904'; }
|
||||
&.spell-3::before { content : '\E905'; }
|
||||
&.spell-4::before { content : '\E906'; }
|
||||
&.spell-5::before { content : '\E907'; }
|
||||
&.spell-6::before { content : '\E908'; }
|
||||
&.spell-7::before { content : '\E909'; }
|
||||
&.spell-8::before { content : '\E90A'; }
|
||||
&.spell-9::before { content : '\E90B'; }
|
||||
|
||||
/* Damage types */
|
||||
&.acid::before { content : '\E90C'; }
|
||||
&.bludgeoning::before { content : '\E90D'; }
|
||||
&.cold::before { content : '\E90E'; }
|
||||
&.fire::before { content : '\E90F'; }
|
||||
&.force::before { content : '\E910'; }
|
||||
&.lightning::before { content : '\E911'; }
|
||||
&.necrotic::before { content : '\E912'; }
|
||||
&.piercing::before { content : '\E913'; }
|
||||
&.poison::before { content : '\E914'; }
|
||||
&.psychic::before { content : '\E915'; }
|
||||
&.radiant::before { content : '\E916'; }
|
||||
&.slashing::before { content : '\E917'; }
|
||||
&.thunder::before { content : '\E918'; }
|
||||
|
||||
/* DnD Conditions */
|
||||
&.blinded::before { content : '\E919'; }
|
||||
&.charmed::before { content : '\E91A'; }
|
||||
&.deafened::before { content : '\E91B'; }
|
||||
&.exhaust-1::before { content : '\E91C'; }
|
||||
&.exhaust-2::before { content : '\E91D'; }
|
||||
&.exhaust-3::before { content : '\E91E'; }
|
||||
&.exhaust-4::before { content : '\E91F'; }
|
||||
&.exhaust-5::before { content : '\E920'; }
|
||||
&.exhaust-6::before { content : '\E921'; }
|
||||
&.frightened::before { content : '\E922'; }
|
||||
&.grappled::before { content : '\E923'; }
|
||||
&.incapacitated::before { content : '\E924'; }
|
||||
&.unconscious::before { content : '\E925'; }
|
||||
&.invisible::before { content : '\E926'; }
|
||||
&.paralyzed::before { content : '\E927'; }
|
||||
&.petrified::before { content : '\E928'; }
|
||||
&.poisoned::before { content : '\E929'; }
|
||||
&.prone::before { content : '\E92A'; }
|
||||
&.restrained::before { content : '\E92B'; }
|
||||
&.stunned::before { content : '\E92C'; }
|
||||
|
||||
|
||||
/* Character Classes and Features */
|
||||
&.barbarian-rage::before { content : '\E92D'; }
|
||||
&.barbarian-reckless-attack::before { content : '\E92E'; }
|
||||
&.bardic-inspiration::before { content : '\E92F'; }
|
||||
&.cleric-channel-divinity::before { content : '\E930'; }
|
||||
&.druid-wild-shape::before { content : '\E931'; }
|
||||
&.fighter-action-surge::before { content : '\E932'; }
|
||||
&.fighter-second-wind::before { content : '\E933'; }
|
||||
&.monk-flurry-blows::before { content : '\E934'; }
|
||||
&.monk-patient-defense::before { content : '\E935'; }
|
||||
&.monk-step-of-the-wind::before { content : '\E936'; }
|
||||
&.monk-step-of-the-wind-2::before { content : '\E937'; }
|
||||
&.monk-step-of-the-wind-3::before { content : '\E938'; }
|
||||
&.monk-stunning-strike::before { content : '\E939'; }
|
||||
&.monk-stunning-strike-2::before { content : '\E93A'; }
|
||||
&.paladin-divine-smite::before { content : '\E93B'; }
|
||||
&.paladin-lay-on-hands::before { content : '\E93C'; }
|
||||
&.barbarian-abilities::before { content : '\E93D'; }
|
||||
&.barbarian::before { content : '\E93E'; }
|
||||
&.bard-abilities::before { content : '\E93F'; }
|
||||
&.bard::before { content : '\E940'; }
|
||||
&.cleric-abilities::before { content : '\E941'; }
|
||||
&.cleric::before { content : '\E942'; }
|
||||
&.druid-abilities::before { content : '\E943'; }
|
||||
&.druid::before { content : '\E944'; }
|
||||
&.fighter-abilities::before { content : '\E945'; }
|
||||
&.fighter::before { content : '\E946'; }
|
||||
&.monk-abilities::before { content : '\E947'; }
|
||||
&.monk::before { content : '\E948'; }
|
||||
&.paladin-abilities::before { content : '\E949'; }
|
||||
&.paladin::before { content : '\E94A'; }
|
||||
&.ranger-abilities::before { content : '\E94B'; }
|
||||
&.ranger::before { content : '\E94C'; }
|
||||
&.rogue-abilities::before { content : '\E94D'; }
|
||||
&.rogue::before { content : '\E94E'; }
|
||||
&.sorcerer-abilities::before { content : '\E94F'; }
|
||||
&.sorcerer::before { content : '\E950'; }
|
||||
&.warlock-abilities::before { content : '\E951'; }
|
||||
&.warlock::before { content : '\E952'; }
|
||||
&.wizard-abilities::before { content : '\E953'; }
|
||||
&.wizard::before { content : '\E954'; }
|
||||
|
||||
/* Types of actions */
|
||||
&.movement::before { content : '\E955'; }
|
||||
&.action::before { content : '\E956'; }
|
||||
&.bonus-action::before { content : '\E957'; }
|
||||
&.reaction::before { content : '\E958'; }
|
||||
|
||||
/* SRD Spells */
|
||||
&.acid-arrow::before { content : '\E959'; }
|
||||
&.action-1::before { content : '\E95A'; }
|
||||
&.alter-self::before { content : '\E95B'; }
|
||||
&.alter-self-2::before { content : '\E95C'; }
|
||||
&.magic-beans::before { content : '\E95D'; }
|
||||
&.animal-friendship::before { content : '\E95E'; }
|
||||
&.animate-dead::before { content : '\E95F'; }
|
||||
&.animate-objects::before { content : '\E960'; }
|
||||
&.animate-objects-2::before { content : '\E961'; }
|
||||
&.bane::before { content : '\E962'; }
|
||||
&.bless::before { content : '\E963'; }
|
||||
&.blur::before { content : '\E964'; }
|
||||
&.bonus::before { content : '\E965'; }
|
||||
&.branding-smite::before { content : '\E966'; }
|
||||
&.burning-hands::before { content : '\E967'; }
|
||||
&.charm-person::before { content : '\E968'; }
|
||||
&.chill-touch::before { content : '\E969'; }
|
||||
&.cloudkill::before { content : '\E96A'; }
|
||||
&.comprehend-languages::before { content : '\E96B'; }
|
||||
&.cone-of-cold::before { content : '\E96C'; }
|
||||
&.conjure-elemental::before { content : '\E96D'; }
|
||||
&.conjure-minor-elemental::before { content : '\E96E'; }
|
||||
&.control-water::before { content : '\E96F'; }
|
||||
&.counterspell::before { content : '\E970'; }
|
||||
&.cure-wounds::before { content : '\E971'; }
|
||||
&.dancing-lights::before { content : '\E972'; }
|
||||
&.darkness::before { content : '\E973'; }
|
||||
&.detect-magic::before { content : '\E974'; }
|
||||
&.disguise-self::before { content : '\E975'; }
|
||||
&.disintegrate::before { content : '\E976'; }
|
||||
&.dispel-evil-and-good::before { content : '\E977'; }
|
||||
&.dispel-magic::before { content : '\E978'; }
|
||||
&.dominate-monster::before { content : '\E979'; }
|
||||
&.dominate-person::before { content : '\E97A'; }
|
||||
&.eldritch-blast::before { content : '\E97B'; }
|
||||
&.enlarge-reduce::before { content : '\E97C'; }
|
||||
&.entangle::before { content : '\E97D'; }
|
||||
&.faerie-fire::before { content : '\E97E'; }
|
||||
&.faerie-fire2::before { content : '\E97F'; }
|
||||
&.feather-fall::before { content : '\E980'; }
|
||||
&.find-familiar::before { content : '\E981'; }
|
||||
&.finger-of-death::before { content : '\E982'; }
|
||||
&.fireball::before { content : '\E983'; }
|
||||
&.floating-disk::before { content : '\E984'; }
|
||||
&.fly::before { content : '\E985'; }
|
||||
&.fog-cloud::before { content : '\E986'; }
|
||||
&.gaseous-form::before { content : '\E987'; }
|
||||
&.gaseous-form2::before { content : '\E988'; }
|
||||
&.gentle-repose::before { content : '\E989'; }
|
||||
&.gentle-repose2::before { content : '\E98A'; }
|
||||
&.globe-of-invulnerability::before { content : '\E98B'; }
|
||||
&.guiding-bolt::before { content : '\E98C'; }
|
||||
&.healing-word::before { content : '\E98D'; }
|
||||
&.heat-metal::before { content : '\E98E'; }
|
||||
&.hellish-rebuke::before { content : '\E98F'; }
|
||||
&.heroes-feast::before { content : '\E990'; }
|
||||
&.heroism::before { content : '\E991'; }
|
||||
&.hideous-laughter::before { content : '\E992'; }
|
||||
&.identify::before { content : '\E993'; }
|
||||
&.illusory-script::before { content : '\E994'; }
|
||||
&.inflict-wounds::before { content : '\E995'; }
|
||||
&.light::before { content : '\E996'; }
|
||||
&.longstrider::before { content : '\E997'; }
|
||||
&.mage-armor::before { content : '\E998'; }
|
||||
&.mage-hand::before { content : '\E999'; }
|
||||
&.magic-missile::before { content : '\E99A'; }
|
||||
&.mass-cure-wounds::before { content : '\E99B'; }
|
||||
&.mass-healing-word::before { content : '\E99C'; }
|
||||
&.Mending::before { content : '\E99D'; }
|
||||
&.message::before { content : '\E99E'; }
|
||||
&.Minor-illusion::before { content : '\E99F'; }
|
||||
&.movement1::before { content : '\E9A0'; }
|
||||
&.polymorph::before { content : '\E9A1'; }
|
||||
&.power-word-kill::before { content : '\E9A2'; }
|
||||
&.power-word-stun::before { content : '\E9A3'; }
|
||||
&.prayer-of-healing::before { content : '\E9A4'; }
|
||||
&.prestidigitation::before { content : '\E9A5'; }
|
||||
&.protection-from-evil-and-good::before { content : '\E9A6'; }
|
||||
&.raise-dead::before { content : '\E9A7'; }
|
||||
&.raise-dead2::before { content : '\E9A8'; }
|
||||
&.reaction1::before { content : '\E9A9'; }
|
||||
&.resurrection::before { content : '\E9AA'; }
|
||||
&.resurrection2::before { content : '\E9AB'; }
|
||||
&.revivify::before { content : '\E9AC'; }
|
||||
&.revivify2::before { content : '\E9AD'; }
|
||||
&.sacred-flame::before { content : '\E9AE'; }
|
||||
&.sanctuary::before { content : '\E9AF'; }
|
||||
&.scorching-ray::before { content : '\E9B0'; }
|
||||
&.sending::before { content : '\E9B1'; }
|
||||
&.shatter::before { content : '\E9B2'; }
|
||||
&.shield::before { content : '\E9B3'; }
|
||||
&.silent-image::before { content : '\E9B4'; }
|
||||
&.sleep::before { content : '\E9B5'; }
|
||||
&.speak-with-animals::before { content : '\E9B6'; }
|
||||
&.telekinesis::before { content : '\E9B7'; }
|
||||
&.true-strike::before { content : '\E9B8'; }
|
||||
&.vicious-mockery::before { content : '\E9B9'; }
|
||||
&.wall-of-fire::before { content : '\E9BA'; }
|
||||
&.wall-of-force::before { content : '\E9BB'; }
|
||||
&.wall-of-ice::before { content : '\E9BC'; }
|
||||
&.wall-of-stone::before { content : '\E9BD'; }
|
||||
&.wall-of-thorns::before { content : '\E9BE'; }
|
||||
&.wish::before { content : '\E9BF'; }
|
||||
}
|
||||
2054
themes/fonts/iconFonts/fontAwesome.js
Normal file
2054
themes/fonts/iconFonts/fontAwesome.js
Normal file
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user