mirror of
https://github.com/naturalcrit/homebrewery.git
synced 2026-01-27 20:23:08 +00:00
Compare commits
1601 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2bd06250fc | ||
|
|
c82b62f953 | ||
|
|
4fe38e3929 | ||
|
|
b6d69173cd | ||
|
|
8b085e1806 | ||
|
|
cb9d24d5b4 | ||
|
|
23fd70e3c3 | ||
|
|
5b02132e57 | ||
|
|
f8841c068f | ||
|
|
da1d08f8a9 | ||
|
|
0a199e750f | ||
|
|
5433cda52f | ||
|
|
9c4de58161 | ||
|
|
1b96dae27f | ||
|
|
1564bc7448 | ||
|
|
8e20d3ba10 | ||
|
|
450baee66a | ||
|
|
4b588786c4 | ||
|
|
e07a04ebfa | ||
|
|
f707752c26 | ||
|
|
80e039b194 | ||
|
|
c888df28aa | ||
|
|
94cc1c642c | ||
|
|
9a02a351fa | ||
|
|
e8e7237a8e | ||
|
|
086c4f74f6 | ||
|
|
e987da498b | ||
|
|
d12f644f5b | ||
|
|
3eb071fbdc | ||
|
|
81e17b420c | ||
|
|
e8ccd094e8 | ||
|
|
7c60fbe655 | ||
|
|
2d570924d1 | ||
|
|
8ee70b0928 | ||
|
|
2cdd65b083 | ||
|
|
758a06e58a | ||
|
|
a87e420437 | ||
|
|
46085c8d44 | ||
|
|
179e21755c | ||
|
|
e9ca68e7d3 | ||
|
|
9886200fa9 | ||
|
|
9a1070bb06 | ||
|
|
3b8dbe8a04 | ||
|
|
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 | ||
|
|
0efcd5d258 | ||
|
|
31b6e0c4f6 | ||
|
|
423413e41b | ||
|
|
ec514cdb51 | ||
|
|
7272544724 | ||
|
|
99ff7fdf14 | ||
|
|
491b38c330 | ||
|
|
4033d3ad99 | ||
|
|
9285d355b2 | ||
|
|
24e67e2270 | ||
|
|
5f6d5f53cc | ||
|
|
865c5678bc | ||
|
|
05ba7b41d1 | ||
|
|
e7735e242a | ||
|
|
1111d8275c | ||
|
|
f3b01bc75c | ||
|
|
8685c5cae4 | ||
|
|
e0a457bf40 | ||
|
|
d20c9c502c | ||
|
|
8c5e68e571 | ||
|
|
fbe65a4e93 | ||
|
|
3875dabfd2 | ||
|
|
930709223a | ||
|
|
a6ce36689c | ||
|
|
2424d34682 | ||
|
|
8fc224e9a1 | ||
|
|
98fc007efd | ||
|
|
7fb23c7362 | ||
|
|
a2f0546a6d | ||
|
|
8a55658bd7 | ||
|
|
01d60c4520 | ||
|
|
b4349a0476 | ||
|
|
695b9916dd | ||
|
|
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 | ||
|
|
511c9ffada | ||
|
|
ea03538552 | ||
|
|
8fb9f3f823 | ||
|
|
7af919afd3 | ||
|
|
5bbe4016d6 | ||
|
|
0f3312a5d7 | ||
|
|
0bf432dd76 | ||
|
|
27b3da0144 | ||
|
|
b8b1b9660f | ||
|
|
f40d851d49 | ||
|
|
b64c835706 | ||
|
|
b7717171b3 | ||
|
|
92e27cda6c | ||
|
|
aef6605225 | ||
|
|
7c9cc25923 | ||
|
|
78ce8aa6e3 | ||
|
|
8ae22bdc27 | ||
|
|
46c14ef23b | ||
|
|
d3080c03a4 | ||
|
|
12eead3379 | ||
|
|
a2e065c5d8 | ||
|
|
0d1a3d55cf | ||
|
|
b748a597d2 | ||
|
|
5fb0e123e3 | ||
|
|
9a4a14fa97 | ||
|
|
0c76a546e4 | ||
|
|
fb299f898e | ||
|
|
afb5ccec81 | ||
|
|
c0beae6e46 | ||
|
|
9c6ece3e7f | ||
|
|
5494c02f00 | ||
|
|
b4ee62a1bd | ||
|
|
632efe8b9f | ||
|
|
bf38f95d25 | ||
|
|
f6daeb4acd | ||
|
|
10a7f34abb | ||
|
|
3a054f1ae0 | ||
|
|
25b4b3d906 | ||
|
|
f21d636846 | ||
|
|
8365841b89 | ||
|
|
e6bf8b59a1 | ||
|
|
b4a4934c5c | ||
|
|
a6b2dab9cc | ||
|
|
d692e88b96 | ||
|
|
62c9e081e6 | ||
|
|
5a52e76ff0 | ||
|
|
3b1f4a0d13 | ||
|
|
cdad9af453 | ||
|
|
5fd92ee72d | ||
|
|
5bcd3a1b01 | ||
|
|
d49d9fd755 | ||
|
|
41d817823b | ||
|
|
cfffb4961b | ||
|
|
6bd2f4d98d | ||
|
|
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 | ||
|
|
822f8a2cd0 | ||
|
|
d2965e1122 | ||
|
|
37e211937c | ||
|
|
e7a03dc7d6 | ||
|
|
a469b88fd2 | ||
|
|
45244c8e5d | ||
|
|
fad5817353 | ||
|
|
c0a8b79acc | ||
|
|
34a41fd610 | ||
|
|
eb0440d36d | ||
|
|
cd82db16d5 | ||
|
|
bdad601ebc | ||
|
|
97a74902ef | ||
|
|
ab30b6a799 | ||
|
|
da6bc497fc | ||
|
|
06c3168868 | ||
|
|
86be90adb2 | ||
|
|
a296678d20 | ||
|
|
f768990fb1 | ||
|
|
313543d0d1 | ||
|
|
2ebe8d80e9 | ||
|
|
8c20422fef | ||
|
|
2197a9b782 | ||
|
|
928a8b351e | ||
|
|
2a148cb138 | ||
|
|
9426c6acd9 | ||
|
|
90ce48b170 | ||
|
|
448af683a0 | ||
|
|
ec5f8254f1 | ||
|
|
7ca38b88ad | ||
|
|
9f31a2c8a2 | ||
|
|
09cf5a9b04 | ||
|
|
4c6953a4e0 | ||
|
|
b4b4fbe375 | ||
|
|
4bc07ceb4e | ||
|
|
fde1706a0c | ||
|
|
1891d7c90a | ||
|
|
98d032913b | ||
|
|
80bcf92fa3 | ||
|
|
41d536b7ff | ||
|
|
1e5e3d5f41 | ||
|
|
19961c7ec5 | ||
|
|
08e273bfd6 | ||
|
|
9f45456066 | ||
|
|
6e69696b4a | ||
|
|
8b6be1cab8 | ||
|
|
93a490e881 | ||
|
|
f23959bb05 | ||
|
|
228041913e | ||
|
|
fc53989946 | ||
|
|
83103a893a | ||
|
|
8cbc7a68e5 | ||
|
|
1177fd721c | ||
|
|
2c8908850b | ||
|
|
9f72dc08c6 | ||
|
|
1018ba554f | ||
|
|
709c9ece74 | ||
|
|
4622a74786 | ||
|
|
19e51102d2 | ||
|
|
68a68bde82 | ||
|
|
e1186b4a1e | ||
|
|
72cfca1158 | ||
|
|
67d824cac9 | ||
|
|
4f010d77e8 | ||
|
|
3b1f9b10e7 | ||
|
|
5e33d8b6c4 | ||
|
|
f7a2509405 | ||
|
|
52904eea09 | ||
|
|
ff84ded547 | ||
|
|
6220e4f63f | ||
|
|
b0c2521101 | ||
|
|
30d2a03fd0 | ||
|
|
61a4b558a8 | ||
|
|
3771d4bfd8 | ||
|
|
b087e849b5 | ||
|
|
9e71945a76 | ||
|
|
9f6fc3d1ac | ||
|
|
e22830eb4a | ||
|
|
f02a3d815a | ||
|
|
7097897df8 | ||
|
|
9247967f93 | ||
|
|
1b3d82fc04 | ||
|
|
810934f2c1 | ||
|
|
3b507a1fb9 | ||
|
|
4f1056a320 | ||
|
|
963aa8f003 | ||
|
|
431dfd7780 | ||
|
|
4f0cbd82d4 | ||
|
|
53e437c6bc | ||
|
|
0e5c91733d | ||
|
|
f5c729c328 | ||
|
|
4f9e93fac7 | ||
|
|
7cc83eaf95 | ||
|
|
22b01b131f | ||
|
|
2045bf8060 | ||
|
|
df73b37180 | ||
|
|
99f9b10348 | ||
|
|
ed85f77c48 | ||
|
|
777438fd94 | ||
|
|
08406de5cc | ||
|
|
e1599909bc | ||
|
|
e7eda1f5ec | ||
|
|
9f2aaf01c7 | ||
|
|
882c78fbb5 | ||
|
|
a79b2fb755 | ||
|
|
97fba241a1 | ||
|
|
d0fbca7af5 | ||
|
|
36726c747c | ||
|
|
74580e63d6 | ||
|
|
1d8eb35c64 | ||
|
|
659472578b | ||
|
|
9ec549b496 | ||
|
|
e37c190600 | ||
|
|
d51d7efdcf | ||
|
|
b0a16c8daf | ||
|
|
beb86c1820 | ||
|
|
48f8026c35 | ||
|
|
1be0a2dac3 | ||
|
|
d31e495d07 | ||
|
|
3b2a48eabf | ||
|
|
4bc76a0766 | ||
|
|
3faa23c6eb | ||
|
|
e324de8f4f | ||
|
|
7954ae8692 | ||
|
|
85a00b508b | ||
|
|
1e91d7256c | ||
|
|
8a15172db1 | ||
|
|
aae574e4e5 | ||
|
|
45106b47d4 | ||
|
|
59b3038b9b | ||
|
|
963ec282d3 | ||
|
|
31cf8b7d28 | ||
|
|
9f6bc10369 | ||
|
|
f7c7d40195 | ||
|
|
380e3fead3 | ||
|
|
ffa01c7f1d | ||
|
|
15367f9444 | ||
|
|
90a710907e | ||
|
|
da6d06728c | ||
|
|
451ff3ffec | ||
|
|
61038f876d | ||
|
|
20e1c71eff | ||
|
|
d2d7f5b71e | ||
|
|
0ad4cb7cfd | ||
|
|
161efbb3c8 | ||
|
|
b35739c5c1 | ||
|
|
7690fb9287 | ||
|
|
2e54520b32 | ||
|
|
cdf5b29ac2 | ||
|
|
733b929940 | ||
|
|
211fe48e29 | ||
|
|
9d3f7fe556 | ||
|
|
5d87508d0e | ||
|
|
40d0e7e90e | ||
|
|
7ca10ff5a4 | ||
|
|
831c635149 | ||
|
|
d961e7695d | ||
|
|
4b842ef37f | ||
|
|
0396bedcd0 | ||
|
|
1705e66be2 | ||
|
|
1e4f804542 | ||
|
|
6a03be9d64 | ||
|
|
591278862a | ||
|
|
9848e4b600 | ||
|
|
772b478682 | ||
|
|
dab8dd278d | ||
|
|
72d26c6c7e | ||
|
|
a7f07ab9f5 | ||
|
|
75080135af | ||
|
|
7a9483c0d0 | ||
|
|
484c6bb8a7 | ||
|
|
125adfb198 | ||
|
|
a0c6e92016 | ||
|
|
7de58740a2 | ||
|
|
bae56b8b9d | ||
|
|
e02d925a49 | ||
|
|
03f868d084 | ||
|
|
920c4cd7cb | ||
|
|
bf2c638cad | ||
|
|
eddb513e72 | ||
|
|
fea68ac71a | ||
|
|
97b10c685c | ||
|
|
40fc422ab5 | ||
|
|
f0a8020189 | ||
|
|
647afba2b0 | ||
|
|
bd324a7e74 | ||
|
|
ac080c8323 | ||
|
|
30ebf90371 | ||
|
|
f9a7adbd72 | ||
|
|
b74fb22182 | ||
|
|
b6ea89356b | ||
|
|
37488ded4d | ||
|
|
c47dd828ed | ||
|
|
a34ed8ccb4 | ||
|
|
d4d0546d61 | ||
|
|
e94c1d33d2 | ||
|
|
dd6388bf9f | ||
|
|
8b8071a903 | ||
|
|
19746a78f4 | ||
|
|
13d679c4bf | ||
|
|
a851378a2f | ||
|
|
e3b2d33a5e | ||
|
|
e4b7d0be64 | ||
|
|
7e48696fb5 | ||
|
|
b727c56e56 | ||
|
|
7f168f35b8 | ||
|
|
21c0916693 | ||
|
|
8f0fb6e458 | ||
|
|
4606ad4c6e | ||
|
|
bf267436b0 | ||
|
|
2af44665e1 | ||
|
|
e412a379e3 | ||
|
|
f37da19649 | ||
|
|
d7756230fb | ||
|
|
d6aa0138f0 | ||
|
|
f3b17f4615 | ||
|
|
04e6f2ea58 | ||
|
|
ed6718eef1 | ||
|
|
e36a638ae5 | ||
|
|
d1c2e74ed6 | ||
|
|
5a410029f6 | ||
|
|
005fb705fa | ||
|
|
b4fec32320 | ||
|
|
d0000cee11 | ||
|
|
846b3b9d02 | ||
|
|
54d881642d | ||
|
|
f6c0b0d6fc | ||
|
|
08a3d7367b | ||
|
|
cd537f98c7 | ||
|
|
8ab91c3eb2 | ||
|
|
8c773ced9c | ||
|
|
4cc2acc9e6 | ||
|
|
f903e97562 | ||
|
|
b57e42be6f | ||
|
|
9f95947d16 | ||
|
|
d129fc14c1 | ||
|
|
58422569c9 | ||
|
|
f172d02920 | ||
|
|
dbe60e3ff1 | ||
|
|
beaf67c975 | ||
|
|
8e6f0a7dbb | ||
|
|
b2b276c3a3 | ||
|
|
7483b4afc6 | ||
|
|
c7a2e84927 | ||
|
|
3482330629 | ||
|
|
a74b556188 | ||
|
|
1721b9087a | ||
|
|
c871548877 | ||
|
|
190ecddc2a | ||
|
|
0a24ac9f25 | ||
|
|
d8d580c277 | ||
|
|
be08887e50 | ||
|
|
5043313aca | ||
|
|
83d2a604e1 | ||
|
|
02509ebc3a | ||
|
|
e494899f8b | ||
|
|
9f483bba6f | ||
|
|
f93af38fa6 | ||
|
|
6eecd9cee4 | ||
|
|
7e847cc139 | ||
|
|
99daaf5537 | ||
|
|
24421ca4e7 | ||
|
|
25945fc0df | ||
|
|
b50353c8c4 | ||
|
|
4f4b43f49d | ||
|
|
cfdc3e6870 | ||
|
|
f71c3da568 | ||
|
|
e472465ce7 | ||
|
|
776b6da79f | ||
|
|
1e4a00ce56 | ||
|
|
ed6bca04f5 | ||
|
|
dbfc1e7d28 | ||
|
|
cc9ead20b3 | ||
|
|
79c89bb621 | ||
|
|
cf1617f2a3 | ||
|
|
7ecca16fd0 | ||
|
|
1e1505c63f | ||
|
|
1bddb38fcc | ||
|
|
3314471d73 | ||
|
|
a957ea37f6 | ||
|
|
a4d426bc00 | ||
|
|
15f95ddd44 | ||
|
|
4dd8ad77bb | ||
|
|
9f5a4fb44c | ||
|
|
7f16da110f | ||
|
|
3734c9e71d | ||
|
|
6b93b1c1e7 | ||
|
|
3d7780958a | ||
|
|
59e87697ff | ||
|
|
20c088b6de | ||
|
|
50f069e688 | ||
|
|
148c243d9f | ||
|
|
16e4f66b99 | ||
|
|
774b555a61 | ||
|
|
5b35d1169d | ||
|
|
0d1d3a180d | ||
|
|
df3db14e8b | ||
|
|
0310eee685 | ||
|
|
c4499fcc26 | ||
|
|
1d317788fe | ||
|
|
b5301ff978 | ||
|
|
a027e16636 | ||
|
|
673a22a571 | ||
|
|
1c7d2740f3 | ||
|
|
3778b4c719 | ||
|
|
3e258332c1 | ||
|
|
713865fb40 | ||
|
|
bc29cddcec | ||
|
|
f9d8344dba | ||
|
|
9d38c937b4 | ||
|
|
622579bb7d | ||
|
|
26263c0bf8 | ||
|
|
05f88dfd00 | ||
|
|
d1d28acebb | ||
|
|
84d5b26530 | ||
|
|
16c37d8d76 | ||
|
|
802da2920b | ||
|
|
a608df9cd9 | ||
|
|
254b0852ca | ||
|
|
8e41755d2a | ||
|
|
36eb250d7a | ||
|
|
a2b97abb2e | ||
|
|
0331f5cf5b | ||
|
|
54159513e4 | ||
|
|
99a766d990 | ||
|
|
1ec0cb14ef | ||
|
|
387f03db25 | ||
|
|
0c20a65999 | ||
|
|
da8836ba99 | ||
|
|
bf874c55af | ||
|
|
43a76933ab | ||
|
|
ce9013fed2 | ||
|
|
e4632ea340 | ||
|
|
f21eaab997 | ||
|
|
f48b9c46fe | ||
|
|
7d08731094 | ||
|
|
bd97d78195 | ||
|
|
b1a3fcf33c | ||
|
|
453656fbeb | ||
|
|
7d755fe2a3 | ||
|
|
ba1430c377 | ||
|
|
7698abe70f | ||
|
|
fd58503a77 | ||
|
|
8aa7a26183 | ||
|
|
ad5ad05b7b | ||
|
|
c0827d6db0 | ||
|
|
43209186ee | ||
|
|
46a0a66fb6 | ||
|
|
9e6f7a496e | ||
|
|
94d9e1b08e | ||
|
|
087ce4bb90 | ||
|
|
2c02d4174f | ||
|
|
4feeaee7e7 | ||
|
|
5f5185c619 | ||
|
|
3e76307303 | ||
|
|
fe3a5f4def | ||
|
|
5d752f3355 | ||
|
|
8f400236a7 | ||
|
|
8779ee3325 | ||
|
|
e268858945 | ||
|
|
0e48f325f2 | ||
|
|
7e38271ac6 | ||
|
|
ff75afa91f | ||
|
|
4866eacd5d | ||
|
|
1386020bbb | ||
|
|
6b7af58e6c | ||
|
|
0f1d07d90f | ||
|
|
d5230757b1 | ||
|
|
06825468b4 | ||
|
|
faab60f271 | ||
|
|
4bed2349a9 | ||
|
|
53f1e53fcb | ||
|
|
df447d3d4d | ||
|
|
e3bf913a80 | ||
|
|
7bb1f16946 | ||
|
|
6870fd6d76 | ||
|
|
9ad1d1f196 | ||
|
|
969cff61bf | ||
|
|
736f729457 | ||
|
|
0050e1e294 | ||
|
|
f2d1b61a7a | ||
|
|
1d1fa99b4b | ||
|
|
f42cab6e40 | ||
|
|
bc21abd509 | ||
|
|
41d43e84a5 | ||
|
|
72744718cc | ||
|
|
e85a62a05c | ||
|
|
5a68acc0f5 | ||
|
|
f51fca74e6 | ||
|
|
fe5a76c0df | ||
|
|
3bda834ad3 | ||
|
|
29f0a8e635 | ||
|
|
c035404555 | ||
|
|
74ddc71962 | ||
|
|
1491a1b4ff | ||
|
|
6890bab668 | ||
|
|
209c237b73 | ||
|
|
086468d65b | ||
|
|
323d84974c | ||
|
|
6365fb9b56 | ||
|
|
19bb9705b6 | ||
|
|
705d170b6e | ||
|
|
ab54188ba4 | ||
|
|
e0b6b95295 | ||
|
|
c7cfade86f | ||
|
|
e4fa59aae8 | ||
|
|
35227268cf | ||
|
|
67b11d62ea | ||
|
|
2b81c26cff | ||
|
|
32b5bebbc4 | ||
|
|
c27f5d9efa | ||
|
|
26a41e6262 | ||
|
|
87c9e587a1 | ||
|
|
bfd3eff6f2 | ||
|
|
f3148ed53c | ||
|
|
ea320e0cc4 | ||
|
|
7f8b87bb85 | ||
|
|
5e97121e5a | ||
|
|
c9fc976c72 | ||
|
|
9e8570c19b | ||
|
|
ed2d539995 | ||
|
|
13fbcd0eb1 | ||
|
|
43b9f3d901 | ||
|
|
3ee9fe1c3f | ||
|
|
65cc8567a1 | ||
|
|
dd82f54549 | ||
|
|
a014056440 | ||
|
|
c176c38f30 | ||
|
|
4c87aed628 | ||
|
|
09f2f96dff | ||
|
|
703e207970 | ||
|
|
f075b19a68 | ||
|
|
283c2b5ae1 | ||
|
|
f1c3507a9f | ||
|
|
29dc1e0747 | ||
|
|
265e9976b9 | ||
|
|
70657c16d1 | ||
|
|
7177548c0e | ||
|
|
e1ad05eb3a | ||
|
|
75c41f4466 | ||
|
|
85caf0a892 | ||
|
|
692205b0e6 | ||
|
|
e4324f316d | ||
|
|
46140e92fd | ||
|
|
3bc2df0ac5 | ||
|
|
77abab8395 | ||
|
|
37c72d5125 | ||
|
|
602a476b59 | ||
|
|
965397733d | ||
|
|
f10868db0b | ||
|
|
d7a95d3cff | ||
|
|
6eeed49022 | ||
|
|
be96c3a56f | ||
|
|
57a31f3b71 | ||
|
|
7c5955c96f | ||
|
|
8ab273363c | ||
|
|
538650bf92 | ||
|
|
54921a998a | ||
|
|
da11089fff | ||
|
|
0243d138ff | ||
|
|
4c8a5baee5 | ||
|
|
e5acbfed3a | ||
|
|
0163e22567 | ||
|
|
b503b8fc9b | ||
|
|
3243e4d56c | ||
|
|
fbc164a9b8 | ||
|
|
02e6a3df99 | ||
|
|
78cf95fbb1 | ||
|
|
d2306b70a9 | ||
|
|
43180c314f | ||
|
|
f47c2dcb56 | ||
|
|
a4ea1612c1 | ||
|
|
8b6517eb8d | ||
|
|
b8ee696b69 | ||
|
|
067a7cd507 | ||
|
|
0243b5f491 | ||
|
|
31c7fd12b9 | ||
|
|
7b8aaa408d | ||
|
|
1c65ee150b | ||
|
|
b0cfeaa782 | ||
|
|
84de560083 | ||
|
|
26c4b1afa6 | ||
|
|
622827efda | ||
|
|
854c21639a | ||
|
|
2b1b0acefc | ||
|
|
8fc6047127 | ||
|
|
7782e200af | ||
|
|
62e679571e | ||
|
|
f32c52a460 | ||
|
|
edbe8cdace | ||
|
|
9e4344de83 | ||
|
|
760269a6e1 | ||
|
|
e53e00713d | ||
|
|
faba9f1616 | ||
|
|
6beee49ebc | ||
|
|
eaf3c7978d | ||
|
|
d36e052478 | ||
|
|
099ea08bd4 | ||
|
|
b0ea34cc3f | ||
|
|
a241813b8d | ||
|
|
de27437148 | ||
|
|
79e8dfec18 | ||
|
|
0c010a0a87 | ||
|
|
0c6c9fce4d | ||
|
|
ac9fc720f7 | ||
|
|
8b31966c2b | ||
|
|
76d966f17d | ||
|
|
d0d8b268d4 | ||
|
|
8bd8f0fc37 | ||
|
|
a2acf3be0e | ||
|
|
844a3c19ec | ||
|
|
877c2e365f | ||
|
|
81c7950ad1 | ||
|
|
7b8e3da90a | ||
|
|
eef3d7738e | ||
|
|
b8ca837c02 | ||
|
|
1fc3573087 | ||
|
|
5ab89b2583 | ||
|
|
539cd1d2b9 | ||
|
|
c930ae87f4 | ||
|
|
4424a331d5 | ||
|
|
c747c5577e | ||
|
|
ce8cbba441 | ||
|
|
c0cabbb563 | ||
|
|
e56ff93db1 | ||
|
|
033776168e | ||
|
|
d0ccc4a15a | ||
|
|
2f383d59b6 | ||
|
|
c9c5176f1b | ||
|
|
fe0cfcb2b6 | ||
|
|
0470d13ae0 | ||
|
|
c4defb7b3f | ||
|
|
c1e17bb6aa | ||
|
|
d09dc11f5f | ||
|
|
4c2211c428 | ||
|
|
d076d6c719 | ||
|
|
d648bacd26 | ||
|
|
1ca5ba1086 | ||
|
|
7c28f60e0a | ||
|
|
20b76bdead | ||
|
|
2fd2ccfe14 | ||
|
|
69308cfd8b | ||
|
|
ccc1895304 | ||
|
|
05a0d80c1b | ||
|
|
2ea65de0c0 | ||
|
|
742798ad79 | ||
|
|
f088fc49f3 | ||
|
|
b837ac5d6b | ||
|
|
64af24f0f4 | ||
|
|
5abb1db512 | ||
|
|
f1133b9c33 | ||
|
|
c73d02c550 | ||
|
|
f1d26cc0c0 | ||
|
|
077d699f0b | ||
|
|
2ed8614642 | ||
|
|
9834fcb97f | ||
|
|
347393520d | ||
|
|
e65e12ff6e | ||
|
|
4dfe1a0914 | ||
|
|
1a1acec2f3 | ||
|
|
e6276a0c7b | ||
|
|
84a0c43745 | ||
|
|
baed640a3d | ||
|
|
9621a69b7d | ||
|
|
7d6851572f | ||
|
|
3e2ec7cd36 | ||
|
|
0ac88bd84a | ||
|
|
db3f9a45ad | ||
|
|
8fc2737670 | ||
|
|
0c2e4ce20b | ||
|
|
62948b2838 | ||
|
|
3dfb3a9738 | ||
|
|
42c1bece65 | ||
|
|
448c12cc91 | ||
|
|
56c82f8793 | ||
|
|
84b0407f74 | ||
|
|
90977521df | ||
|
|
09a52bc7cb | ||
|
|
6ef80eed7f | ||
|
|
ad1795258b | ||
|
|
993ae295af | ||
|
|
78c26ab1a3 | ||
|
|
eb669e6eca | ||
|
|
17355012fb | ||
|
|
eae593ce90 | ||
|
|
7a8a6480de | ||
|
|
d0dd61a25c | ||
|
|
dbf0559f95 | ||
|
|
8e88b881fc | ||
|
|
c722e0af39 | ||
|
|
6dcdc1b685 | ||
|
|
76d17baf7e | ||
|
|
c97e2be9d5 | ||
|
|
229acbfcd1 | ||
|
|
2cb216ed7b | ||
|
|
8674bc9da2 | ||
|
|
199e049871 | ||
|
|
31f18ef3d5 | ||
|
|
9db55c4dff | ||
|
|
e2ac6c9b6b | ||
|
|
407c35d9f7 | ||
|
|
8575d72f6e | ||
|
|
7b8e398891 | ||
|
|
dfd5b228c2 | ||
|
|
89c5c3f255 | ||
|
|
4dd58aaad3 | ||
|
|
96d973528c | ||
|
|
3a4de13551 | ||
|
|
a65c24bebf | ||
|
|
7dfc857217 | ||
|
|
d5980cba89 | ||
|
|
9ed32527a6 | ||
|
|
62982f86a1 | ||
|
|
985a9843f2 | ||
|
|
3bbcb1b6fb | ||
|
|
9da203d204 | ||
|
|
212b3f7e05 | ||
|
|
7a06fe386d | ||
|
|
4580217410 | ||
|
|
e82ba8cb7a | ||
|
|
636f10cb41 | ||
|
|
d6eaa812b1 | ||
|
|
507f170720 | ||
|
|
faaf4207b4 | ||
|
|
688377ce0b | ||
|
|
8cf57dbc72 | ||
|
|
12012a2a5b | ||
|
|
9b59f47536 | ||
|
|
90b4e47861 | ||
|
|
ef00231c5b | ||
|
|
c39653bc69 | ||
|
|
83faa86063 | ||
|
|
af20d0b1c2 | ||
|
|
1861c7db69 | ||
|
|
a88b256b6c | ||
|
|
40542e9bec | ||
|
|
04187cf769 | ||
|
|
67a4391dcb | ||
|
|
3784e0f583 | ||
|
|
df33713f82 | ||
|
|
c4ba6381f2 | ||
|
|
547daf6499 | ||
|
|
649e225359 | ||
|
|
35bf26feae | ||
|
|
481bab9463 | ||
|
|
a0e2997a40 | ||
|
|
74c4f4fe52 | ||
|
|
7c9513f377 | ||
|
|
c10be139c9 | ||
|
|
ee4921f02c | ||
|
|
f5f6137a4d | ||
|
|
0775f9ee1b | ||
|
|
75ed555de1 | ||
|
|
e7d0741139 | ||
|
|
c69de1036f | ||
|
|
7d115c970a | ||
|
|
852aa8d289 | ||
|
|
ae974b270d | ||
|
|
0a1c04d003 | ||
|
|
ef10b71e56 | ||
|
|
5bfd0dd537 | ||
|
|
05e56221f4 | ||
|
|
5c5230e64e | ||
|
|
769f636db2 | ||
|
|
eeba037244 | ||
|
|
688eca05e1 | ||
|
|
4c48992331 | ||
|
|
fcb9a8cdc5 | ||
|
|
78d4d6fb7c | ||
|
|
407efd0f8b | ||
|
|
d39ae139d7 | ||
|
|
467b728c47 | ||
|
|
b02036fb7a | ||
|
|
d8524c3a84 | ||
|
|
75809a5f42 | ||
|
|
e99aad15c1 | ||
|
|
c57b011215 | ||
|
|
5942bfece1 | ||
|
|
2dc874daba | ||
|
|
38fa428fde | ||
|
|
6bf51cd94a | ||
|
|
ddef21cd7e | ||
|
|
bf30cadb68 | ||
|
|
2f13b89510 | ||
|
|
abe3a7e7c7 | ||
|
|
4ec5f73aed | ||
|
|
a5313deb78 | ||
|
|
5dd486866f | ||
|
|
ada642e56e | ||
|
|
881fcc9cba | ||
|
|
a809e920fc | ||
|
|
36f3eb4da1 | ||
|
|
57772065e0 | ||
|
|
f10be94190 | ||
|
|
055ee38cb7 | ||
|
|
202b275966 | ||
|
|
34be05ac51 | ||
|
|
39e33da2d1 | ||
|
|
d262f586fc | ||
|
|
71f1aed227 | ||
|
|
159d5a35b2 | ||
|
|
efecfac68a | ||
|
|
2c997458b2 | ||
|
|
7e98f79416 | ||
|
|
cfecc001aa | ||
|
|
ab8716d071 | ||
|
|
9e12ab71f8 | ||
|
|
995d1c63d8 | ||
|
|
875e1023fc | ||
|
|
6c4dad675f | ||
|
|
632882d370 | ||
|
|
041c7ed48f | ||
|
|
6dcc6d36b7 | ||
|
|
0762b82c40 | ||
|
|
c6821819c7 | ||
|
|
5a79795e4f | ||
|
|
3073b3e35d | ||
|
|
0491516662 | ||
|
|
b55616170b | ||
|
|
e080c46509 | ||
|
|
171d1c7c46 | ||
|
|
d1503c8d6f | ||
|
|
be72b029bf | ||
|
|
93a7b11017 | ||
|
|
045fbbe158 | ||
|
|
ede731e3a5 | ||
|
|
d6e63604ac | ||
|
|
0624f8a0b9 | ||
|
|
960c03bfa6 | ||
|
|
cda98e02e0 | ||
|
|
9ac070512d | ||
|
|
a1e9c82c06 | ||
|
|
b116e0a622 | ||
|
|
ef2bbfea5e | ||
|
|
c858c705d2 | ||
|
|
fb91761c31 | ||
|
|
93d38eb184 | ||
|
|
d31dae728f | ||
|
|
c068aca9ff | ||
|
|
fcdaef2445 | ||
|
|
c78dcbfe05 | ||
|
|
8d94e5fbe0 | ||
|
|
c6d8bbae16 | ||
|
|
0a54c7b04e | ||
|
|
2fd54ee87e | ||
|
|
1f5f160964 | ||
|
|
8d04f09aab | ||
|
|
be1f905b48 | ||
|
|
dbbd8cb26d | ||
|
|
827fdd3cff | ||
|
|
7b85995b4a | ||
|
|
37593573ce | ||
|
|
a6ecec4172 | ||
|
|
b561f69dd0 | ||
|
|
0bdadaf946 | ||
|
|
7b1a815e78 | ||
|
|
7b9a23670d | ||
|
|
a762626c53 | ||
|
|
1c5c21d89b | ||
|
|
da2c647fa6 | ||
|
|
cb61badfc5 | ||
|
|
d2f909384e | ||
|
|
cebc7be81d | ||
|
|
83c604cb74 | ||
|
|
ad455f652c | ||
|
|
3f19b2975c | ||
|
|
cacfc788fb | ||
|
|
8f99712a28 | ||
|
|
367c2bd111 | ||
|
|
1c45cb1b7f | ||
|
|
19fbd832f1 | ||
|
|
7ce0ac577a | ||
|
|
3ce4d6d1f8 | ||
|
|
18aca07e84 | ||
|
|
4bda071742 | ||
|
|
35bde09aa7 | ||
|
|
d43ea46e40 | ||
|
|
f1ca6eeee2 | ||
|
|
d390d518a3 | ||
|
|
d1152dcbb5 | ||
|
|
837306c9a7 | ||
|
|
e6428a3b18 | ||
|
|
c58c8777f1 | ||
|
|
caadb7b4ce | ||
|
|
ce1ba8289c | ||
|
|
ce946bda98 | ||
|
|
07b1254309 | ||
|
|
4c7715286e | ||
|
|
980c544bba | ||
|
|
7c0b9ea3f6 | ||
|
|
e6d8784633 | ||
|
|
4720aced6c | ||
|
|
cf5e61cf09 | ||
|
|
9d43588f44 | ||
|
|
ce005da20f | ||
|
|
17531151ad | ||
|
|
863f624772 | ||
|
|
6a5f4efd26 | ||
|
|
6200b416ab | ||
|
|
dc1931a5e3 | ||
|
|
26bddc1a79 | ||
|
|
74ebd44d7c | ||
|
|
989af1bbd0 | ||
|
|
3359e489f5 | ||
|
|
f431ac2e40 | ||
|
|
511c38dd1e | ||
|
|
85b0976082 | ||
|
|
7052337669 | ||
|
|
e07d1d1ddb | ||
|
|
c5ebd0352d | ||
|
|
a8fbcf0ad1 | ||
|
|
92f33136ce | ||
|
|
9260283914 | ||
|
|
a08263dd7c | ||
|
|
7eae170f6c | ||
|
|
b5cb8ce834 | ||
|
|
d497c0094b | ||
|
|
71141aa6f6 | ||
|
|
36e607eb3b | ||
|
|
d67f206900 | ||
|
|
6bf9ddd585 | ||
|
|
1607a1ac10 | ||
|
|
392a3db3c2 | ||
|
|
0845234f2f | ||
|
|
f58a7d65b5 | ||
|
|
3ac0ac7568 | ||
|
|
1c71ff0945 | ||
|
|
3ccfef0763 | ||
|
|
a6ec8370be | ||
|
|
7b059f029d | ||
|
|
917b63722c | ||
|
|
c1cbbe0047 | ||
|
|
963d76add9 | ||
|
|
e055734d03 | ||
|
|
7c16805680 | ||
|
|
be52e0ecd9 | ||
|
|
175673cdf9 | ||
|
|
a279c75826 | ||
|
|
8be95e7d04 | ||
|
|
8e0c1d78dc | ||
|
|
5da73f8ff8 | ||
|
|
e20fc680e9 | ||
|
|
e821ddac93 | ||
|
|
fab2c2cead | ||
|
|
bd0e142999 | ||
|
|
673649abc4 | ||
|
|
b95a2189a5 | ||
|
|
f45547d899 | ||
|
|
f2c970fb79 | ||
|
|
6e6a6dd1e3 | ||
|
|
b2fc020d81 | ||
|
|
3bfa3bac3a | ||
|
|
00134c6c3d | ||
|
|
6a393a1437 | ||
|
|
115be2813d | ||
|
|
e4a46c84ec | ||
|
|
b994bf269a | ||
|
|
2de7ec3585 | ||
|
|
efbfbf3568 | ||
|
|
12ca82e6e6 | ||
|
|
e7ce8d73bb | ||
|
|
a420f202d8 | ||
|
|
3628ca837a | ||
|
|
cc3a42402a | ||
|
|
1e9b71080b | ||
|
|
90917bb84c | ||
|
|
7f6e90fee3 | ||
|
|
b8d8c1bebb | ||
|
|
7ee0e914e6 | ||
|
|
08dbd5638d | ||
|
|
b7d7f4f2a0 | ||
|
|
3cf6691e67 | ||
|
|
8077a91ff7 | ||
|
|
09670b8535 | ||
|
|
36f8f39486 | ||
|
|
1556cf361a | ||
|
|
c6ef051232 | ||
|
|
2c130d1943 | ||
|
|
f1e6a9a41e | ||
|
|
b557144f63 | ||
|
|
738fc62b8f | ||
|
|
34edd436e7 | ||
|
|
0b13e90dd8 | ||
|
|
52dcc3b53c | ||
|
|
6803db268f | ||
|
|
99c45b6cc3 | ||
|
|
4cf8e620ee | ||
|
|
fb4c33545c | ||
|
|
68475a6aa0 | ||
|
|
e5140bd5b4 | ||
|
|
e3c05c83ba | ||
|
|
b374f06718 | ||
|
|
ea8c93d39d | ||
|
|
6dd8ccff90 | ||
|
|
ca90a22c64 | ||
|
|
abc4851375 | ||
|
|
a155c10d46 | ||
|
|
5247f5c87b | ||
|
|
e41c1ef0bd | ||
|
|
4d6b428f93 | ||
|
|
6928bd9fb4 | ||
|
|
f2855aca85 | ||
|
|
aeef595ea9 | ||
|
|
1a24709da0 | ||
|
|
8c11f47c1f | ||
|
|
486fbf32b2 | ||
|
|
8ac3cdcf9d | ||
|
|
f0fc0bcb6d | ||
|
|
63a5e9f817 | ||
|
|
5456f4f197 | ||
|
|
d75ea8943b | ||
|
|
07ae1539aa | ||
|
|
2125b8a026 | ||
|
|
b3522bddf1 | ||
|
|
9dc19d996d | ||
|
|
216de73c93 | ||
|
|
a306030635 | ||
|
|
3e9bea3761 | ||
|
|
73e909c4c8 | ||
|
|
53fa6af5f9 | ||
|
|
4d2edf81a9 | ||
|
|
b10b33830a | ||
|
|
393ac69581 | ||
|
|
1098f6da70 | ||
|
|
a29fed3c89 | ||
|
|
7af3a629f9 | ||
|
|
b0a9765819 | ||
|
|
86d18ea0d9 | ||
|
|
bfe278c81c | ||
|
|
7870c763df | ||
|
|
7f1758364b | ||
|
|
46a6ed4fcc | ||
|
|
7efd23039e | ||
|
|
9e5103a0c7 | ||
|
|
7281b6e43c | ||
|
|
3f6eb7371f | ||
|
|
2b0bbfc2db | ||
|
|
e909bc8f35 | ||
|
|
e16110da6a | ||
|
|
fcd15e6d9c | ||
|
|
3f828c8649 | ||
|
|
bb66cffa13 | ||
|
|
da5a5631ad | ||
|
|
3e43b058a5 | ||
|
|
a30c2fa1f7 | ||
|
|
cb203c29c9 | ||
|
|
e50d7b8882 | ||
|
|
74d6aa7c8a | ||
|
|
e86686807b | ||
|
|
f4356025de | ||
|
|
73b141d08c | ||
|
|
9196ffc480 | ||
|
|
4e18bb047e | ||
|
|
e4b4e34216 | ||
|
|
d38c1b9ab2 | ||
|
|
deaaafd9d2 | ||
|
|
9b33bf9855 | ||
|
|
e5346d3a6e | ||
|
|
d9ca20e17d | ||
|
|
a5cb3f085f | ||
|
|
6d28948387 | ||
|
|
65c75b3282 | ||
|
|
a0f22e31b7 | ||
|
|
c750eebc11 | ||
|
|
93a93f1907 | ||
|
|
29428b81f6 | ||
|
|
a542953cec | ||
|
|
183ca51753 | ||
|
|
220816a172 | ||
|
|
840b075c8e | ||
|
|
3ba15a068a | ||
|
|
b99de1c6e1 | ||
|
|
73a48501e0 | ||
|
|
599d39b69d | ||
|
|
ed18ba3108 | ||
|
|
6d93291d5b | ||
|
|
3fef61cbf8 | ||
|
|
c5d9c3bdc0 | ||
|
|
6c7af2d968 | ||
|
|
6130d69906 | ||
|
|
8c975747c4 | ||
|
|
ef6dab24a2 | ||
|
|
1f09fff94b | ||
|
|
7bcd898c81 | ||
|
|
186809008c | ||
|
|
b6e11ba607 | ||
|
|
355b8ac78f | ||
|
|
a2c20a0f7a | ||
|
|
117e399c1d | ||
|
|
a3ac524308 | ||
|
|
1e9ba31644 | ||
|
|
751728d134 | ||
|
|
8e05cdbb43 | ||
|
|
a5483c549b | ||
|
|
746f3d35f8 | ||
|
|
94c902bf38 | ||
|
|
0da1c43dc3 | ||
|
|
32417e92ff | ||
|
|
3d5f99adae | ||
|
|
a889fa657e | ||
|
|
4347debf45 | ||
|
|
dc3243ae59 | ||
|
|
540dee89dd | ||
|
|
1854080771 | ||
|
|
45207b8114 | ||
|
|
2b3c2c9fac | ||
|
|
0b953fcbf3 | ||
|
|
0a3453d228 | ||
|
|
6b49e720ca | ||
|
|
7feaa51de0 | ||
|
|
1729b13574 | ||
|
|
73832fabcc | ||
|
|
bac537244c | ||
|
|
54f8bb4b08 | ||
|
|
38f6929c1d | ||
|
|
64ce9ecfa6 | ||
|
|
858990c4bd | ||
|
|
785011cba4 | ||
|
|
50d3b503d9 | ||
|
|
2f8e2545c6 | ||
|
|
d8574e7045 | ||
|
|
b3497e14f1 | ||
|
|
d6e0047d4e | ||
|
|
8ebd5ccff9 | ||
|
|
4a927daff3 | ||
|
|
d28e85209e | ||
|
|
cca882869d | ||
|
|
9ae55c87a9 | ||
|
|
99ad96a584 | ||
|
|
f8b42031fb | ||
|
|
850b52d924 | ||
|
|
b2e7b28b65 | ||
|
|
3efa7dd0be | ||
|
|
677e27bb66 | ||
|
|
da71bd7a10 | ||
|
|
0869f6b29b | ||
|
|
0da5de494e | ||
|
|
bc9dc8dee9 | ||
|
|
e3e250255e | ||
|
|
787a23bdf8 | ||
|
|
7aca0f2f10 | ||
|
|
999f9c8f25 | ||
|
|
ee4b2d549b | ||
|
|
3eb7ce2775 | ||
|
|
4d4371f48c | ||
|
|
47666cc26d | ||
|
|
e8352d996e | ||
|
|
e6f792900c | ||
|
|
0ddeafd260 | ||
|
|
4cf54d6ae8 | ||
|
|
8d3329069a | ||
|
|
2e13eed2ef | ||
|
|
310faa449d | ||
|
|
8527a976a6 | ||
|
|
54460c52f6 | ||
|
|
e81bd2a0d2 | ||
|
|
6c0daa1e4d | ||
|
|
25d03faae2 | ||
|
|
378b2204da | ||
|
|
447b0939f2 | ||
|
|
0bde336226 | ||
|
|
73e44b8d7a | ||
|
|
53fbaf87c2 | ||
|
|
a8db7353b0 | ||
|
|
bda8037cd6 | ||
|
|
1f173814ec | ||
|
|
deb0e2f85b | ||
|
|
5425ae5d15 | ||
|
|
f03f2c36de | ||
|
|
a1e663bc32 | ||
|
|
16c842e08f | ||
|
|
1c0edce6f6 | ||
|
|
8ec6e66c92 | ||
|
|
330cdb35f4 | ||
|
|
f93fbab754 | ||
|
|
b9498e49fc | ||
|
|
4a434bb161 | ||
|
|
a339cb036f | ||
|
|
91eef51fb5 | ||
|
|
46b050ae7d | ||
|
|
b433691596 | ||
|
|
cef7f98176 | ||
|
|
b869d086ea | ||
|
|
64a361e06c | ||
|
|
9e7e646296 | ||
|
|
7274d788c5 | ||
|
|
7942f1caed | ||
|
|
343e176b83 | ||
|
|
3db5959cfa | ||
|
|
62f9781c8e | ||
|
|
9d53002874 | ||
|
|
4e7e6f8105 | ||
|
|
cea1157eb2 | ||
|
|
d535328eb8 | ||
|
|
627e9ec7d8 | ||
|
|
ab9b5b7487 | ||
|
|
957a8bf05c | ||
|
|
509390ae03 | ||
|
|
d3a70c3d75 | ||
|
|
1806854969 | ||
|
|
c2570fec6b | ||
|
|
9d64740678 | ||
|
|
053aadeff4 | ||
|
|
d3763beb15 | ||
|
|
8281051797 | ||
|
|
5b0104fc10 | ||
|
|
b3fa902d85 | ||
|
|
cf3635bccc | ||
|
|
a22d223927 | ||
|
|
5c08926576 | ||
|
|
7aa374e529 | ||
|
|
797ca7e64e | ||
|
|
5b8f2d8e3c | ||
|
|
bd0ef5da48 | ||
|
|
fe324d6822 | ||
|
|
3140299d73 | ||
|
|
96d04ad75a | ||
|
|
62532f788e | ||
|
|
69a3d04bb7 | ||
|
|
21017e45fe | ||
|
|
48474c6f7b | ||
|
|
3d318f8863 | ||
|
|
2b798f4ecb | ||
|
|
938802e1a3 | ||
|
|
14f825f3b5 | ||
|
|
37241a70eb | ||
|
|
bcd86a7f0c | ||
|
|
cf54594a4c | ||
|
|
535fdeaf62 | ||
|
|
77bf1b5258 | ||
|
|
63da418b60 | ||
|
|
67f5e53160 | ||
|
|
c6103d51c5 | ||
|
|
a4a10783f6 | ||
|
|
5ed53f75c5 | ||
|
|
81b289923a | ||
|
|
c3d8364789 | ||
|
|
82fec9901d | ||
|
|
173d0a726b | ||
|
|
e064219ca0 | ||
|
|
ec339f2717 | ||
|
|
d9d27808a8 | ||
|
|
a4584dc78e | ||
|
|
6344eaa17d | ||
|
|
5c41110e50 | ||
|
|
085cb99562 | ||
|
|
568586541a | ||
|
|
0d44e1778f | ||
|
|
4a5269e1f3 | ||
|
|
62cf0a4483 | ||
|
|
07c7352aa2 | ||
|
|
cf6c8bce88 | ||
|
|
9cb8b46930 | ||
|
|
467b6ff8de | ||
|
|
45d32ebfc3 | ||
|
|
84496f51ba | ||
|
|
4cf659e711 | ||
|
|
e0bfef5231 | ||
|
|
afb6962407 | ||
|
|
8d2945ee5c | ||
|
|
1dad009298 | ||
|
|
aadf663623 | ||
|
|
8685f32b49 | ||
|
|
678ac90cd0 | ||
|
|
3cb5e8ed42 | ||
|
|
a41553637a | ||
|
|
636f2f9372 | ||
|
|
4ded080a58 | ||
|
|
a5885c8f4f | ||
|
|
273f0ca05d | ||
|
|
3c929870cb | ||
|
|
4cb2a9ef76 | ||
|
|
36df5a3212 | ||
|
|
cea5f2e43a | ||
|
|
046845885d | ||
|
|
9713cc4be9 | ||
|
|
8baf0fc849 | ||
|
|
a7040e554a | ||
|
|
ba43055f32 | ||
|
|
d0de7ca28c | ||
|
|
c0164dce6a | ||
|
|
9e2b8477a8 | ||
|
|
5a32ae5cd4 | ||
|
|
e88e7f852c | ||
|
|
a3b2c6987f | ||
|
|
3d47b5a0bc | ||
|
|
c5f4793c23 | ||
|
|
10e14bfcfd | ||
|
|
f3c36ffb0a | ||
|
|
cff4f8eae5 | ||
|
|
4799e8b443 | ||
|
|
fa38d5c892 | ||
|
|
04eb7d0556 | ||
|
|
f175323221 | ||
|
|
9f4de3c66e | ||
|
|
800bff611a | ||
|
|
e28b6e7a19 | ||
|
|
4c6de90d82 | ||
|
|
e5ef0aedd3 | ||
|
|
da8e7ec610 | ||
|
|
d1412abe03 | ||
|
|
9de4a82977 | ||
|
|
9ddae7bbea | ||
|
|
4fdc6b79ea | ||
|
|
0001cf16d9 | ||
|
|
438cb7f26d | ||
|
|
ffa240f78d | ||
|
|
782aa8e658 | ||
|
|
7efe8964f1 | ||
|
|
853515e09e | ||
|
|
f6c5354ce0 | ||
|
|
6353341738 | ||
|
|
66b9a792e7 | ||
|
|
2775614eab | ||
|
|
32229c6e6e | ||
|
|
37c88b83f1 | ||
|
|
2fa1b2bb8b | ||
|
|
949d763e35 | ||
|
|
661872f332 | ||
|
|
46cb2e6b5b | ||
|
|
e7224e97ef | ||
|
|
e07d53aa5f | ||
|
|
dbb4476eb4 | ||
|
|
65f55dfc12 | ||
|
|
95322595bf | ||
|
|
1e004977be | ||
|
|
9110e7cf7e | ||
|
|
27e8b54528 | ||
|
|
aa31919563 | ||
|
|
7bf3295fc2 | ||
|
|
9fd3f47689 | ||
|
|
0ca7e43d73 | ||
|
|
b33b3cd49b | ||
|
|
71c384ee0b | ||
|
|
546b8d5725 | ||
|
|
4d6ac2b142 | ||
|
|
ce538ebbfd | ||
|
|
cf17e73dfa | ||
|
|
69ef4d7653 | ||
|
|
c98224f3e4 | ||
|
|
4f870de68f | ||
|
|
2cfee2e8ad | ||
|
|
9e1d53a30c | ||
|
|
1fe9f0c8d0 | ||
|
|
adc7233cab | ||
|
|
1b2fc746d3 | ||
|
|
b472fc1115 | ||
|
|
a7a47afaae | ||
|
|
8c0ca988ae | ||
|
|
509c7d8832 | ||
|
|
caff1d8e2b | ||
|
|
e06f5e17d9 | ||
|
|
ade61971d0 | ||
|
|
e1a22ed76c | ||
|
|
6451d79d92 | ||
|
|
9202f9c8eb | ||
|
|
097cc220f8 | ||
|
|
7e660aad45 | ||
|
|
c8df449aac | ||
|
|
2e3c10c35b | ||
|
|
a5aeb7dccd | ||
|
|
7681be2e9c | ||
|
|
92ff776270 | ||
|
|
bf1fb97789 | ||
|
|
2cddc2debe | ||
|
|
0d4a1a11c1 | ||
|
|
aa3cf1d9c1 | ||
|
|
5d0062f610 | ||
|
|
7976917bb9 | ||
|
|
023071c874 | ||
|
|
7da42d3742 | ||
|
|
3269e94757 | ||
|
|
c69f4289ed | ||
|
|
8752a32626 | ||
|
|
8735d1f222 | ||
|
|
21929e676d | ||
|
|
5ca61935a8 | ||
|
|
10143cec93 | ||
|
|
643c8503c0 | ||
|
|
e92d3ecd68 | ||
|
|
4f092828ac | ||
|
|
2d4c211483 | ||
|
|
7d30abc4d9 | ||
|
|
1d513f7a0e | ||
|
|
44922f5261 | ||
|
|
f695cc6948 | ||
|
|
3722387f1f | ||
|
|
8950cb944f | ||
|
|
66fb70a5f8 | ||
|
|
69c242425b | ||
|
|
9093f610bd | ||
|
|
d2b2e69123 | ||
|
|
052c255068 | ||
|
|
e6ad8aefde | ||
|
|
43ae80e80d | ||
|
|
e6e04ad21d | ||
|
|
7be6b913b0 | ||
|
|
455b364160 | ||
|
|
56dbfc032c | ||
|
|
38c20430a9 | ||
|
|
86823b43b1 | ||
|
|
0abfb23ef2 | ||
|
|
da5d4236b6 | ||
|
|
963236f961 | ||
|
|
2d4a3ec910 | ||
|
|
40afdf18d6 | ||
|
|
13944d3a76 | ||
|
|
3c168065ee | ||
|
|
cf42520305 | ||
|
|
d5ac237d40 | ||
|
|
c666d6acb9 | ||
|
|
2869726efd | ||
|
|
ae9a29c28c | ||
|
|
c660f87dff | ||
|
|
a86c728710 | ||
|
|
b28e183f95 | ||
|
|
dc55880544 | ||
|
|
5e9b451e29 | ||
|
|
977cbeed43 | ||
|
|
da6fcb297a | ||
|
|
b2546d908a | ||
|
|
7dd1368c09 | ||
|
|
51f657d2c5 | ||
|
|
3d71799469 | ||
|
|
fe402bc211 | ||
|
|
dc7e4cd192 | ||
|
|
7fd7230e15 | ||
|
|
c30eb9056a | ||
|
|
ee3cd21486 | ||
|
|
5de63799c2 | ||
|
|
dfa8aea1ec | ||
|
|
8e26161244 |
@@ -5,12 +5,12 @@
|
|||||||
version: 2.1
|
version: 2.1
|
||||||
|
|
||||||
orbs:
|
orbs:
|
||||||
node: circleci/node@3.0.0
|
node: circleci/node@5.1.0
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
docker:
|
docker:
|
||||||
- image: cimg/node:16.11.0
|
- image: cimg/node:20.8.0
|
||||||
- image: mongo:4.4
|
- image: mongo:4.4
|
||||||
|
|
||||||
working_directory: ~/homebrewery
|
working_directory: ~/homebrewery
|
||||||
@@ -27,7 +27,7 @@ jobs:
|
|||||||
# fallback to using the latest cache if no exact match is found
|
# fallback to using the latest cache if no exact match is found
|
||||||
- v1-dependencies-
|
- v1-dependencies-
|
||||||
|
|
||||||
- run: sudo npm install -g npm@8.10.0
|
- run: sudo npm install -g npm@10.2.0
|
||||||
- node/install-packages:
|
- node/install-packages:
|
||||||
app-dir: ~/homebrewery
|
app-dir: ~/homebrewery
|
||||||
cache-path: node_modules
|
cache-path: node_modules
|
||||||
@@ -45,7 +45,7 @@ jobs:
|
|||||||
|
|
||||||
test:
|
test:
|
||||||
docker:
|
docker:
|
||||||
- image: cimg/node:16.11.0
|
- image: cimg/node:20.8.0
|
||||||
|
|
||||||
working_directory: ~/homebrewery
|
working_directory: ~/homebrewery
|
||||||
parallelism: 1
|
parallelism: 1
|
||||||
@@ -64,6 +64,12 @@ jobs:
|
|||||||
- run:
|
- run:
|
||||||
name: Test - Mustache Spans
|
name: Test - Mustache Spans
|
||||||
command: npm run test:mustache-syntax
|
command: npm run test:mustache-syntax
|
||||||
|
- run:
|
||||||
|
name: Test - Definition Lists
|
||||||
|
command: npm run test:definition-lists
|
||||||
|
- run:
|
||||||
|
name: Test - Variables
|
||||||
|
command: npm run test:variables
|
||||||
- run:
|
- run:
|
||||||
name: Test - Routes
|
name: Test - Routes
|
||||||
command: npm run test:route
|
command: npm run test:route
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ module.exports = {
|
|||||||
rules : {
|
rules : {
|
||||||
/** Errors **/
|
/** Errors **/
|
||||||
'camelcase' : ['error', { properties: 'never' }],
|
'camelcase' : ['error', { properties: 'never' }],
|
||||||
'func-style' : ['error', 'expression', { allowArrowFunctions: true }],
|
//'func-style' : ['error', 'expression', { allowArrowFunctions: true }],
|
||||||
'no-array-constructor' : 'error',
|
'no-array-constructor' : 'error',
|
||||||
'no-iterator' : 'error',
|
'no-iterator' : 'error',
|
||||||
'no-nested-ternary' : 'error',
|
'no-nested-ternary' : 'error',
|
||||||
|
|||||||
103
.github/actions/limit-pull-requests/action.yml
vendored
Normal file
103
.github/actions/limit-pull-requests/action.yml
vendored
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
name: Limit pull requests
|
||||||
|
description: >
|
||||||
|
Limit the number of open pull requests to the repository created by a user
|
||||||
|
author: ZhongRuoyu (from Homebrew repository)
|
||||||
|
branding:
|
||||||
|
icon: alert-triangle
|
||||||
|
color: yellow
|
||||||
|
|
||||||
|
inputs:
|
||||||
|
token:
|
||||||
|
description: GitHub token
|
||||||
|
required: false
|
||||||
|
default: ${{ github.token }}
|
||||||
|
except-users:
|
||||||
|
description: The users exempted from the limit, one per line
|
||||||
|
required: false
|
||||||
|
# https://docs.github.com/en/graphql/reference/enums#commentauthorassociation
|
||||||
|
except-author-associations:
|
||||||
|
description: The author associations exempted from the limit, one per line
|
||||||
|
required: false
|
||||||
|
comment-limit:
|
||||||
|
description: >
|
||||||
|
Post the comment when the user's number of open pull requests exceeds this
|
||||||
|
number and `comment` is not empty
|
||||||
|
required: true
|
||||||
|
default: "10"
|
||||||
|
comment:
|
||||||
|
description: The comment to post when the limit is reached
|
||||||
|
required: false
|
||||||
|
close-limit:
|
||||||
|
description: >
|
||||||
|
Close the pull request when the user's number of open pull requests
|
||||||
|
exceeds this number and `close` is set to `true`
|
||||||
|
required: true
|
||||||
|
default: "50"
|
||||||
|
close:
|
||||||
|
description: Whether to close the pull request when the limit is reached
|
||||||
|
required: true
|
||||||
|
default: "false"
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: composite
|
||||||
|
steps:
|
||||||
|
- name: Check the number of pull requests
|
||||||
|
id: count-pull-requests
|
||||||
|
run: |
|
||||||
|
# If the user is exempted, assume they have no pull requests.
|
||||||
|
if grep -Fiqx '${{ github.actor }}' <<<"$EXCEPT_USERS"; then
|
||||||
|
echo "::notice::@${{ github.actor }} is exempted from the limit."
|
||||||
|
echo "count=0" >>"$GITHUB_OUTPUT"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
if grep -Fiqx '${{ github.event.pull_request.author_association }}' <<<"$EXCEPT_AUTHOR_ASSOCIATIONS"; then
|
||||||
|
echo "::notice::@{{ github.actor }} is a ${{ github.event.pull_request.author_association }} exempted from the limit."
|
||||||
|
echo "count=0" >>"$GITHUB_OUTPUT"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
count="$(
|
||||||
|
gh api \
|
||||||
|
--method GET \
|
||||||
|
--header 'Accept: application/vnd.github+json' \
|
||||||
|
--header 'X-GitHub-Api-Version: 2022-11-28' \
|
||||||
|
--field state=open \
|
||||||
|
--paginate \
|
||||||
|
'/repos/{owner}/{repo}/pulls' |
|
||||||
|
jq \
|
||||||
|
--raw-output \
|
||||||
|
--arg USER '${{ github.actor }}' \
|
||||||
|
'map(select(.user.login == $USER)) | length'
|
||||||
|
)"
|
||||||
|
echo "::notice::@${{ github.actor }} has $count open pull request(s)."
|
||||||
|
echo "count=$count" >>"$GITHUB_OUTPUT"
|
||||||
|
env:
|
||||||
|
GH_REPO: ${{ github.repository }}
|
||||||
|
GH_TOKEN: ${{ inputs.token }}
|
||||||
|
EXCEPT_USERS: ${{ inputs.except-users }}
|
||||||
|
EXCEPT_AUTHOR_ASSOCIATIONS: ${{ inputs.except-author-associations }}
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Comment on pull request
|
||||||
|
if: >
|
||||||
|
fromJSON(steps.count-pull-requests.outputs.count) > fromJSON(inputs.comment-limit) &&
|
||||||
|
inputs.comment != ''
|
||||||
|
run: |
|
||||||
|
gh pr comment '${{ github.event.pull_request.number }}' \
|
||||||
|
--body="${COMMENT_BODY}"
|
||||||
|
env:
|
||||||
|
GH_REPO: ${{ github.repository }}
|
||||||
|
GH_TOKEN: ${{ inputs.token }}
|
||||||
|
COMMENT_BODY: ${{ inputs.comment }}
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Close pull request
|
||||||
|
if: >
|
||||||
|
fromJSON(steps.count-pull-requests.outputs.count) > fromJSON(inputs.close-limit) &&
|
||||||
|
inputs.close == 'true'
|
||||||
|
run: |
|
||||||
|
gh pr close '${{ github.event.pull_request.number }}'
|
||||||
|
env:
|
||||||
|
GH_REPO: ${{ github.repository }}
|
||||||
|
GH_TOKEN: ${{ inputs.token }}
|
||||||
|
shell: bash
|
||||||
29
.github/workflows/pr-check.yml
vendored
Normal file
29
.github/workflows/pr-check.yml
vendored
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
name: PR Check
|
||||||
|
on: pull_request_target
|
||||||
|
env:
|
||||||
|
GH_REPO: ${{ github.repository }}
|
||||||
|
GH_NO_UPDATE_NOTIFIER: 1
|
||||||
|
GH_PROMPT_DISABLED: 1
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
issues: write
|
||||||
|
pull-requests: write
|
||||||
|
statuses: write
|
||||||
|
jobs:
|
||||||
|
limit-pull-requests:
|
||||||
|
if: always() && github.repository_owner == 'naturalcrit'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name : Run limit-pull-requests action
|
||||||
|
uses: ./.github/actions/limit-pull-requests
|
||||||
|
with:
|
||||||
|
except-users: |
|
||||||
|
dependabot
|
||||||
|
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
|
||||||
|
close: false
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM node:16.13-alpine
|
FROM node:18-alpine
|
||||||
RUN apk --no-cache add git
|
RUN apk --no-cache add git
|
||||||
|
|
||||||
ENV NODE_ENV=docker
|
ENV NODE_ENV=docker
|
||||||
|
|||||||
414
changelog.md
414
changelog.md
@@ -75,11 +75,410 @@ pre {
|
|||||||
.page {
|
.page {
|
||||||
padding-bottom: 1.5cm;
|
padding-bottom: 1.5cm;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.varSyntaxTable th:first-of-type {
|
||||||
|
width:6cm;
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## changelog
|
## changelog
|
||||||
For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery).
|
For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery).
|
||||||
|
|
||||||
|
### 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)
|
||||||
|
}}
|
||||||
|
|
||||||
|
\column
|
||||||
|
|
||||||
|
### Monday 18/3/2024 - v3.12.0
|
||||||
|
{{taskList
|
||||||
|
|
||||||
|
##### 5e-Cleric
|
||||||
|
|
||||||
|
* [x] Fix language-specific hyphenation on print page
|
||||||
|
|
||||||
|
Fixes issue [#3294](https://github.com/naturalcrit/homebrewery/issues/3294)
|
||||||
|
|
||||||
|
* [x] Upgrade Font-Awesome to v6.51
|
||||||
|
|
||||||
|
* [x] Allow downloaded files to be uploaded via {{openSans **NEW {{fa,fa-plus-square}} → FROM UPLOAD {{fa,fa-upload}}**}}
|
||||||
|
|
||||||
|
##### G-Ambatte
|
||||||
|
|
||||||
|
* [x] Fix an edge case crash with empty documents
|
||||||
|
|
||||||
|
Fixes issue [#3315](https://github.com/naturalcrit/homebrewery/issues/3315)
|
||||||
|
|
||||||
|
* [x] Brews on the user page can be searched by tag; clicking a tag adds it to the filter
|
||||||
|
|
||||||
|
Fixes issue [#3164](https://github.com/naturalcrit/homebrewery/issues/3164)
|
||||||
|
|
||||||
|
* [x] Add *DiceFont* icons {{df,d20-20}} `{{df,icon-name}}`
|
||||||
|
|
||||||
|
##### abquintic
|
||||||
|
|
||||||
|
* [x] Fix ^super^ and ^^sub^^ highlighting in the text editor
|
||||||
|
|
||||||
|
* [x] Add new syntax for multiline Definition Lists:
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
Term
|
||||||
|
::Definition 1
|
||||||
|
::Definition 2
|
||||||
|
with more text
|
||||||
|
```
|
||||||
|
|
||||||
|
produces:
|
||||||
|
|
||||||
|
Term
|
||||||
|
::Definition 1
|
||||||
|
::Definition 2
|
||||||
|
with more text
|
||||||
|
|
||||||
|
Fixes issue [#2340](https://github.com/naturalcrit/homebrewery/issues/2340)
|
||||||
|
|
||||||
|
##### RKuerten :
|
||||||
|
* [x] Fix monster stat block backgrounds on print page
|
||||||
|
|
||||||
|
Fixes issue [#3275](https://github.com/naturalcrit/homebrewery/issues/3275)
|
||||||
|
|
||||||
|
* [x] Added new text editor theme: "Darkvision".
|
||||||
|
|
||||||
|
##### calculuschild, G-Ambatte, 5e-Cleric
|
||||||
|
|
||||||
|
* [x] Codebase and UI cleanup
|
||||||
|
}}
|
||||||
|
|
||||||
|
\page
|
||||||
|
|
||||||
|
|
||||||
|
### Friday 21/2/2024 - v3.11.0
|
||||||
|
{{taskList
|
||||||
|
|
||||||
|
##### Gazook89
|
||||||
|
|
||||||
|
* [x] Brew view count no longer increases when viewed by owner
|
||||||
|
|
||||||
|
Fixes issue [#3037](https://github.com/naturalcrit/homebrewery/issues/3037)
|
||||||
|
|
||||||
|
* [x] Small tweak to PHB H3 sizing
|
||||||
|
|
||||||
|
Fixes issue [#2989](https://github.com/naturalcrit/homebrewery/issues/2989)
|
||||||
|
|
||||||
|
* [x] Add **Fold/Unfold All** {{fas,fa-compress-alt}} / {{fas,fa-expand-alt}} buttons to editor bar
|
||||||
|
|
||||||
|
Fixes issue [#2965](https://github.com/naturalcrit/homebrewery/issues/2965)
|
||||||
|
|
||||||
|
|
||||||
|
##### G-Ambatte
|
||||||
|
|
||||||
|
* [x] Share link added to Editor Access error page
|
||||||
|
|
||||||
|
Fixes issue [#3086](https://github.com/naturalcrit/homebrewery/issues/3086)
|
||||||
|
|
||||||
|
* [x] Add Darkbrewery theme to Editor theme selector {{fas,fa-palette}}
|
||||||
|
|
||||||
|
Fixes issue [#3034](https://github.com/naturalcrit/homebrewery/issues/3034)
|
||||||
|
|
||||||
|
* [x] Fix Firefox prints with alternating blank pages
|
||||||
|
|
||||||
|
Fixes issue [#3115](https://github.com/naturalcrit/homebrewery/issues/3115)
|
||||||
|
|
||||||
|
* [x] Admin page working again
|
||||||
|
|
||||||
|
Fixes issue [#2657](https://github.com/naturalcrit/homebrewery/issues/2657)
|
||||||
|
|
||||||
|
|
||||||
|
##### 5e-Cleric
|
||||||
|
|
||||||
|
* [x] Fix indenting issue with Monster Blocks and italics in Class Feature
|
||||||
|
|
||||||
|
Fixes issues [#527](https://github.com/naturalcrit/homebrewery/issues/527),
|
||||||
|
[#3247](https://github.com/naturalcrit/homebrewery/issues/3247)
|
||||||
|
|
||||||
|
* [x] Allow CSS vars in curly syntax to be formatted as strings using single quotes
|
||||||
|
|
||||||
|
`{{--customVar:"'a string'"}}`
|
||||||
|
|
||||||
|
Fixes issue [#3066](https://github.com/naturalcrit/homebrewery/issues/3066)
|
||||||
|
|
||||||
|
* [x] Add *Elderberry Inn* icons {{ei,action}} `{{ei,icon-name}}`
|
||||||
|
|
||||||
|
Fixes issue [#3171](https://github.com/naturalcrit/homebrewery/issues/3171)
|
||||||
|
|
||||||
|
* [x] New {{openSans **{{fas,fa-keyboard}} FONTS** }} snippets!
|
||||||
|
|
||||||
|
Fixes issue [#3171](https://github.com/naturalcrit/homebrewery/issues/3171)
|
||||||
|
|
||||||
|
* [x] New page now opens in a new tab
|
||||||
|
|
||||||
|
|
||||||
|
##### abquintic (new contributor!)
|
||||||
|
|
||||||
|
* [x] Add ^super^ `^abc^` and ^^sub^^ `^^abc^^` syntax.
|
||||||
|
|
||||||
|
Fixes issue [#2171](https://github.com/naturalcrit/homebrewery/issues/2171)
|
||||||
|
|
||||||
|
* [x] Add HTML tag assignment to curly syntax `{{tag=value}}`
|
||||||
|
|
||||||
|
Fixes issue [1488](https://github.com/naturalcrit/homebrewery/issues/1488)
|
||||||
|
|
||||||
|
* [x] {{openSans **Brew → Clone to New**}} now clones tags
|
||||||
|
|
||||||
|
Fixes issue [1488](https://github.com/naturalcrit/homebrewery/issues/1488)
|
||||||
|
|
||||||
|
##### calculuschild
|
||||||
|
|
||||||
|
* [x] Better error messages for "Out of Google Drive Storage" and "Not logged in to edit"
|
||||||
|
|
||||||
|
Fixes issues [2510](https://github.com/naturalcrit/homebrewery/issues/2510),
|
||||||
|
[2975](https://github.com/naturalcrit/homebrewery/issues/2975)
|
||||||
|
|
||||||
|
* [x] Brew Variables
|
||||||
|
}}
|
||||||
|
|
||||||
|
\
|
||||||
|
|
||||||
|
{{wide
|
||||||
|
|
||||||
|
### Brew Variable Syntax
|
||||||
|
|
||||||
|
You may already be familiar with `[link](url)` and `` synax. We have expanded this to include a third `$[variable](text)` syntax. All three of these syntaxes now share a common set of features:
|
||||||
|
|
||||||
|
{{varSyntaxTable
|
||||||
|
| syntax | description |
|
||||||
|
|:-------|-------------|
|
||||||
|
| `[var]:content` | Assigns a variable (must start on a line by itself, and ends at the next blank line) |
|
||||||
|
| `[var](content)` | Assigns a variable and outputs it (can be inline) |
|
||||||
|
| `[var]` | Outputs the variable contents as a link, if formatted as a valid link |
|
||||||
|
| `![var]` | Outputs as an image, if formatted as a valid image |
|
||||||
|
| `$[var]` | Outputs as Markdown |
|
||||||
|
| `$[var1 + var2 - 2 * var3]` | Performs math operations and outputs result if all variables are valid numbers |
|
||||||
|
}}
|
||||||
|
|
||||||
|
}}
|
||||||
|
|
||||||
|
{{wide,margin-top:0,margin-bottom:0
|
||||||
|
### Examples
|
||||||
|
}}
|
||||||
|
|
||||||
|
{{wide,columns:2,margin-top:0,margin-bottom:0
|
||||||
|
|
||||||
|
```
|
||||||
|
[first]: Bob
|
||||||
|
|
||||||
|
[last]: Jones
|
||||||
|
|
||||||
|
My name is $[first] $[last].
|
||||||
|
```
|
||||||
|
|
||||||
|
\column
|
||||||
|
|
||||||
|
[first]: Bob
|
||||||
|
|
||||||
|
[last]: Jones
|
||||||
|
|
||||||
|
My name is $[first] $[last].
|
||||||
|
|
||||||
|
}}
|
||||||
|
|
||||||
|
{{wide,columns:2,margin-top:0,margin-bottom:0
|
||||||
|
|
||||||
|
```
|
||||||
|
[myTable]:
|
||||||
|
| h1 | h2 |
|
||||||
|
|----|----|
|
||||||
|
| c1 | c2 |
|
||||||
|
|
||||||
|
Here is my table:
|
||||||
|
$[myTable]
|
||||||
|
```
|
||||||
|
|
||||||
|
\column
|
||||||
|
|
||||||
|
[myTable]:
|
||||||
|
| h1 | h2 |
|
||||||
|
|----|----|
|
||||||
|
| c1 | c2 |
|
||||||
|
|
||||||
|
Here is my table:
|
||||||
|
$[myTable]
|
||||||
|
}}
|
||||||
|
|
||||||
|
{{wide,columns:2,margin-top:0,margin-bottom:0
|
||||||
|
|
||||||
|
```
|
||||||
|
There are $[TableNum] tables total.
|
||||||
|
|
||||||
|
#### Table $[TableNum](1): Horses
|
||||||
|
|
||||||
|
#### Table $[TableNum]($[TableNum + 1]): Cows
|
||||||
|
```
|
||||||
|
|
||||||
|
\column
|
||||||
|
|
||||||
|
There are $[TableNum] tables in this document. *(note: final value of `$[TableNum]` gets hoisted up if available)*
|
||||||
|
|
||||||
|
|
||||||
|
#### Table $[TableNum](1): Horses
|
||||||
|
|
||||||
|
#### Table $[TableNum]($[TableNum + 1]): Cows
|
||||||
|
}}
|
||||||
|
|
||||||
|
\page
|
||||||
|
|
||||||
|
### Friday 13/10/2023 - v3.10.0
|
||||||
|
{{taskList
|
||||||
|
|
||||||
|
##### G-Ambatte
|
||||||
|
|
||||||
|
* [x] Fix user preferred save location being ignored
|
||||||
|
|
||||||
|
Fixes issue [#2993](https://github.com/naturalcrit/homebrewery/issues/2993)
|
||||||
|
|
||||||
|
* [x] Fix crash to white screen when starting new brews while not signed in
|
||||||
|
|
||||||
|
Fixes issue [#2999](https://github.com/naturalcrit/homebrewery/issues/2999)
|
||||||
|
|
||||||
|
* [x] Fix FreeBSD install script
|
||||||
|
|
||||||
|
Fixes issue [#3005](https://github.com/naturalcrit/homebrewery/issues/3005)
|
||||||
|
|
||||||
|
* [x] Fix *"This brew has been changed on another device"* triggering when manually saving during auto-save
|
||||||
|
|
||||||
|
Fixes issue [#2641](https://github.com/naturalcrit/homebrewery/issues/2641)
|
||||||
|
|
||||||
|
* [x] Fix Firefox different column-flow behavior
|
||||||
|
|
||||||
|
Fixes issue [#2982](https://github.com/naturalcrit/homebrewery/issues/2982)
|
||||||
|
|
||||||
|
* [x] Fix brew titles being mis-sorted on user page
|
||||||
|
|
||||||
|
Fixes issue [#2775](https://github.com/naturalcrit/homebrewery/issues/2775)
|
||||||
|
|
||||||
|
* [x] Text Editor themes now available via new drop-down
|
||||||
|
|
||||||
|
Fixes issue [#362](https://github.com/naturalcrit/homebrewery/issues/362)
|
||||||
|
|
||||||
|
##### 5e-Cleric
|
||||||
|
|
||||||
|
* [x] New {{openSans **PHB → {{fas,fa-quote-right}} QUOTE** }} snippet for V3!
|
||||||
|
|
||||||
|
Fixes issue [#2920](https://github.com/naturalcrit/homebrewery/issues/2920)
|
||||||
|
|
||||||
|
* [x] Several updates and fixes to FAQ and Welcome page
|
||||||
|
|
||||||
|
Fixes issue [#2729](https://github.com/naturalcrit/homebrewery/issues/2729),
|
||||||
|
[#2787](https://github.com/naturalcrit/homebrewery/issues/2787)
|
||||||
|
|
||||||
|
##### Gazook89
|
||||||
|
|
||||||
|
* [x] Add syntax highlighting for Definition Lists <code>:\:</code>
|
||||||
|
}}
|
||||||
|
|
||||||
|
|
||||||
|
### Thursday 17/08/2023 - v3.9.2
|
||||||
|
{{taskList
|
||||||
|
|
||||||
|
##### Calculuschild
|
||||||
|
|
||||||
|
* [x] Fix links to certain old Google Drive files
|
||||||
|
|
||||||
|
Fixes issue [#2917](https://github.com/naturalcrit/homebrewery/issues/2917)
|
||||||
|
|
||||||
|
##### G-Ambatte
|
||||||
|
|
||||||
|
* [x] Menus now open on click, and internally consistent
|
||||||
|
|
||||||
|
Fixes issue [#2702](https://github.com/naturalcrit/homebrewery/issues/2702), [#2782](https://github.com/naturalcrit/homebrewery/issues/2782)
|
||||||
|
|
||||||
|
* [x] Add smarter footer snippet
|
||||||
|
|
||||||
|
Fixes issue [#2289](https://github.com/naturalcrit/homebrewery/issues/2289)
|
||||||
|
|
||||||
|
* [x] Add sanitization in Style editor
|
||||||
|
|
||||||
|
Fixes issue [#1437](https://github.com/naturalcrit/homebrewery/issues/1437)
|
||||||
|
|
||||||
|
* [x] Rework class table snippets to remove unnecessary randomness
|
||||||
|
|
||||||
|
Fixes issue [#2964](https://github.com/naturalcrit/homebrewery/issues/2964)
|
||||||
|
|
||||||
|
* [x] Add User Page link to Google Drive file for file owners, add icons for additional storage locations
|
||||||
|
|
||||||
|
Fixes issue [#2954](https://github.com/naturalcrit/homebrewery/issues/2954)
|
||||||
|
|
||||||
|
* [x] Add default save location selection to Account Page
|
||||||
|
|
||||||
|
Fixes issue [#2943](https://github.com/naturalcrit/homebrewery/issues/2943)
|
||||||
|
|
||||||
|
##### 5e-Cleric
|
||||||
|
|
||||||
|
* [x] Exclude cover pages from Table of Content generation (editing on mobile is still not recommended)
|
||||||
|
|
||||||
|
Fixes issue [#2920](https://github.com/naturalcrit/homebrewery/issues/2920)
|
||||||
|
|
||||||
|
##### Gazook89
|
||||||
|
|
||||||
|
* [x] Adjustments to improve mobile viewing
|
||||||
|
}}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Wednesday 28/06/2023 - v3.9.1
|
||||||
|
{{taskList
|
||||||
|
|
||||||
|
##### G-Ambatte
|
||||||
|
|
||||||
|
* [x] Better error pages with more useful information
|
||||||
|
|
||||||
|
Fixes issue [#1924](https://github.com/naturalcrit/homebrewery/issues/1924)
|
||||||
|
}}
|
||||||
|
|
||||||
### Friday 02/06/2023 - v3.9.0
|
### Friday 02/06/2023 - v3.9.0
|
||||||
{{taskList
|
{{taskList
|
||||||
|
|
||||||
@@ -101,7 +500,7 @@ Fixes issues [#2092](https://github.com/naturalcrit/homebrewery/issues/2092), [#
|
|||||||
|
|
||||||
Fixes issue [#2790](https://github.com/naturalcrit/homebrewery/issues/2790)
|
Fixes issue [#2790](https://github.com/naturalcrit/homebrewery/issues/2790)
|
||||||
|
|
||||||
##### 5e-Clerc
|
##### 5e-Cleric
|
||||||
|
|
||||||
* [x] New {{openSans **PHB → {{fac,book-part-cover}} PART COVER PAGE** }} snippet for V3!
|
* [x] New {{openSans **PHB → {{fac,book-part-cover}} PART COVER PAGE** }} snippet for V3!
|
||||||
|
|
||||||
@@ -114,6 +513,8 @@ Fixes issue [#2790](https://github.com/naturalcrit/homebrewery/issues/2790)
|
|||||||
Fixes issue [#2784](https://github.com/naturalcrit/homebrewery/issues/2784)
|
Fixes issue [#2784](https://github.com/naturalcrit/homebrewery/issues/2784)
|
||||||
}}
|
}}
|
||||||
|
|
||||||
|
\page
|
||||||
|
|
||||||
### Wednesday 12/04/2023 - v3.8.0
|
### Wednesday 12/04/2023 - v3.8.0
|
||||||
{{taskList
|
{{taskList
|
||||||
|
|
||||||
@@ -145,7 +546,7 @@ Fixes issues [#1679](https://github.com/naturalcrit/homebrewery/issues/1679)
|
|||||||
|
|
||||||
* [x] Add local Windows install script via Chocolatey
|
* [x] Add local Windows install script via Chocolatey
|
||||||
|
|
||||||
##### 5e-Clerc
|
##### 5e-Cleric
|
||||||
|
|
||||||
* [x] New {{openSans **TABLES → {{fas,fa-language}} RUNE TABLE**}} snippets for V3. Adds an alphabetic script translation table.
|
* [x] New {{openSans **TABLES → {{fas,fa-language}} RUNE TABLE**}} snippets for V3. Adds an alphabetic script translation table.
|
||||||
|
|
||||||
@@ -175,8 +576,6 @@ Fixes issues [#2731](https://github.com/naturalcrit/homebrewery/issues/2731)
|
|||||||
|
|
||||||
}}
|
}}
|
||||||
|
|
||||||
\page
|
|
||||||
|
|
||||||
### Monday 13/03/2023 - v3.7.2
|
### Monday 13/03/2023 - v3.7.2
|
||||||
{{taskList
|
{{taskList
|
||||||
|
|
||||||
@@ -257,7 +656,11 @@ Fixes issues [#2603](https://github.com/naturalcrit/homebrewery/issues/2603)
|
|||||||
* [x] Add message to refresh the browser if the user is missing an update to the Homebrewery
|
* [x] Add message to refresh the browser if the user is missing an update to the Homebrewery
|
||||||
|
|
||||||
Fixes issues [#2583](https://github.com/naturalcrit/homebrewery/issues/2583)
|
Fixes issues [#2583](https://github.com/naturalcrit/homebrewery/issues/2583)
|
||||||
|
}}
|
||||||
|
|
||||||
|
\page
|
||||||
|
|
||||||
|
{{taskList
|
||||||
##### G-Ambatte
|
##### G-Ambatte
|
||||||
|
|
||||||
* [x] Auto-compile Themes CSS on development server
|
* [x] Auto-compile Themes CSS on development server
|
||||||
@@ -267,7 +670,6 @@ Fixes issues [#2583](https://github.com/naturalcrit/homebrewery/issues/2583)
|
|||||||
* [x] Fix cloned brews inheriting the parent view count
|
* [x] Fix cloned brews inheriting the parent view count
|
||||||
}}
|
}}
|
||||||
|
|
||||||
\page
|
|
||||||
|
|
||||||
### Friday 23/12/2022 - v3.5.0
|
### Friday 23/12/2022 - v3.5.0
|
||||||
{{taskList
|
{{taskList
|
||||||
@@ -1224,7 +1626,7 @@ myStyle {color: black}
|
|||||||
### Sunday, 29/05/2016 - v2.1.0
|
### Sunday, 29/05/2016 - v2.1.0
|
||||||
- Finally added a syntax for doing spell lists. A bit in-depth about why this took so long. Essentially I'm running out of syntax to use in stardard Markdown. There are too many unique elements in the PHB-style to be mapped. I solved this earlier by stacking certain elements together (eg. an `<hr>` before a `blockquote` turns it into moster state block), but those are getting unweildly. I would like to simply wrap these in `div`s with classes, but unfortunately Markdown stops processing when within HTML blocks. To get around this I wrote my own override to the Markdown parser and lexer to process Markdown within a simple div class wrapper. This should open the door for more unique syntaxes in the future. Big step!
|
- Finally added a syntax for doing spell lists. A bit in-depth about why this took so long. Essentially I'm running out of syntax to use in stardard Markdown. There are too many unique elements in the PHB-style to be mapped. I solved this earlier by stacking certain elements together (eg. an `<hr>` before a `blockquote` turns it into moster state block), but those are getting unweildly. I would like to simply wrap these in `div`s with classes, but unfortunately Markdown stops processing when within HTML blocks. To get around this I wrote my own override to the Markdown parser and lexer to process Markdown within a simple div class wrapper. This should open the door for more unique syntaxes in the future. Big step!
|
||||||
- Override Ctrl+P (and cmd+P) to launch to the print page. Many people try to just print either the editing or share page to get a PDF. While this dones;t make much sense, I do get a ton of issues about it. So now if you try to do this, it'll just bring you imediately to the print page. Everybody wins!
|
- Override Ctrl+P (and cmd+P) to launch to the print page. Many people try to just print either the editing or share page to get a PDF. While this dones;t make much sense, I do get a ton of issues about it. So now if you try to do this, it'll just bring you imediately to the print page. Everybody wins!
|
||||||
- The onboarding flow has also been confusing a few users (Homepage -> new -> save -> edit page). If you edit the Homepage text now, a Call to Action to save your work will pop-up.
|
- The onboarding flow has also been confusing a few users (Homepage → new → save → edit page). If you edit the Homepage text now, a Call to Action to save your work will pop-up.
|
||||||
- Added a 'Recently Edited' and 'Recently Viewed' nav item to the edit and share page respectively. Each will remember the last 8 items you edited or viewed and when you viewed it. Makes use of the new title attribute of brews to easy navigatation.
|
- Added a 'Recently Edited' and 'Recently Viewed' nav item to the edit and share page respectively. Each will remember the last 8 items you edited or viewed and when you viewed it. Makes use of the new title attribute of brews to easy navigatation.
|
||||||
- Paragraphs now indent properly after lists (thanks u/slitjen!)
|
- Paragraphs now indent properly after lists (thanks u/slitjen!)
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
require('./brewCleanup.less');
|
require('./brewCleanup.less');
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const createClass = require('create-react-class');
|
const createClass = require('create-react-class');
|
||||||
const cx = require('classnames');
|
|
||||||
|
|
||||||
const request = require('superagent');
|
const request = require('superagent');
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
require('./brewCompress.less');
|
require('./brewCompress.less');
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const createClass = require('create-react-class');
|
const createClass = require('create-react-class');
|
||||||
const cx = require('classnames');
|
|
||||||
|
|
||||||
const request = require('superagent');
|
const request = require('superagent');
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
const React = require('react');
|
const React = require('react');
|
||||||
const createClass = require('create-react-class');
|
const createClass = require('create-react-class');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const cx = require('classnames');
|
|
||||||
require('./combobox.less');
|
require('./combobox.less');
|
||||||
|
|
||||||
const Combobox = createClass({
|
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;
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
/*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/
|
/*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/
|
||||||
require('./brewRenderer.less');
|
require('./brewRenderer.less');
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const createClass = require('create-react-class');
|
const { useState, useRef, useEffect } = React;
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const cx = require('classnames');
|
|
||||||
|
|
||||||
const MarkdownLegacy = require('naturalcrit/markdownLegacy.js');
|
const MarkdownLegacy = require('naturalcrit/markdownLegacy.js');
|
||||||
const Markdown = require('naturalcrit/markdown.js');
|
const Markdown = require('naturalcrit/markdown.js');
|
||||||
@@ -13,228 +12,236 @@ const ErrorBar = require('./errorBar/errorBar.jsx');
|
|||||||
const RenderWarnings = require('homebrewery/renderWarnings/renderWarnings.jsx');
|
const RenderWarnings = require('homebrewery/renderWarnings/renderWarnings.jsx');
|
||||||
const NotificationPopup = require('./notificationPopup/notificationPopup.jsx');
|
const NotificationPopup = require('./notificationPopup/notificationPopup.jsx');
|
||||||
const Frame = require('react-frame-component').default;
|
const Frame = require('react-frame-component').default;
|
||||||
|
const dedent = require('dedent-tabs').default;
|
||||||
|
const { printCurrentBrew } = require('../../../shared/helpers.js');
|
||||||
|
|
||||||
|
const DOMPurify = require('dompurify');
|
||||||
|
const purifyConfig = { FORCE_BODY: true, SANITIZE_DOM: false };
|
||||||
|
|
||||||
const Themes = require('themes/themes.json');
|
const Themes = require('themes/themes.json');
|
||||||
|
|
||||||
const PAGE_HEIGHT = 1056;
|
const PAGE_HEIGHT = 1056;
|
||||||
const PPR_THRESHOLD = 50;
|
|
||||||
|
|
||||||
const BrewRenderer = createClass({
|
const INITIAL_CONTENT = dedent`
|
||||||
displayName : 'BrewRenderer',
|
<!DOCTYPE html><html><head>
|
||||||
getDefaultProps : function() {
|
<link href="//use.fontawesome.com/releases/v6.5.1/css/all.css" rel="stylesheet" type="text/css" />
|
||||||
return {
|
<link href="//fonts.googleapis.com/css?family=Open+Sans:400,300,600,700" rel="stylesheet" type="text/css" />
|
||||||
|
<link href='/homebrew/bundle.css' type="text/css" rel='stylesheet' />
|
||||||
|
<base target=_blank>
|
||||||
|
</head><body style='overflow: hidden'><div></div></body></html>`;
|
||||||
|
|
||||||
|
//v=====----------------------< Brew Page Component >---------------------=====v//
|
||||||
|
const BrewPage = (props)=>{
|
||||||
|
props = {
|
||||||
|
contents : '',
|
||||||
|
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: cleanText }} />
|
||||||
|
</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
//v=====--------------------< Brew Renderer Component >-------------------=====v//
|
||||||
|
const renderedPages = [];
|
||||||
|
let rawPages = [];
|
||||||
|
|
||||||
|
const BrewRenderer = (props)=>{
|
||||||
|
props = {
|
||||||
text : '',
|
text : '',
|
||||||
style : '',
|
style : '',
|
||||||
renderer : 'legacy',
|
renderer : 'legacy',
|
||||||
theme : '5ePHB',
|
theme : '5ePHB',
|
||||||
lang : '',
|
lang : '',
|
||||||
errors : []
|
errors : [],
|
||||||
|
currentEditorPage : 0,
|
||||||
|
...props
|
||||||
};
|
};
|
||||||
},
|
|
||||||
getInitialState : function() {
|
|
||||||
let pages;
|
|
||||||
if(this.props.renderer == 'legacy') {
|
|
||||||
pages = this.props.text.split('\\page');
|
|
||||||
} else {
|
|
||||||
pages = this.props.text.split(/^\\page$/gm);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
const [state, setState] = useState({
|
||||||
viewablePageNumber : 0,
|
viewablePageNumber : 0,
|
||||||
height : 0,
|
height : PAGE_HEIGHT,
|
||||||
isMounted : false,
|
isMounted : false,
|
||||||
|
|
||||||
pages : pages,
|
|
||||||
usePPR : pages.length >= PPR_THRESHOLD,
|
|
||||||
visibility : 'hidden',
|
visibility : 'hidden',
|
||||||
initialContent : `<!DOCTYPE html><html><head>
|
});
|
||||||
<link href="//use.fontawesome.com/releases/v5.15.1/css/all.css" rel="stylesheet" />
|
|
||||||
<link href="//fonts.googleapis.com/css?family=Open+Sans:400,300,600,700" rel="stylesheet" type="text/css" />
|
|
||||||
<link href='/homebrew/bundle.css' rel='stylesheet' />
|
|
||||||
<base target=_blank>
|
|
||||||
</head><body style='overflow: hidden'><div></div></body></html>`
|
|
||||||
};
|
|
||||||
},
|
|
||||||
height : 0,
|
|
||||||
lastRender : <div></div>,
|
|
||||||
|
|
||||||
componentWillUnmount : function() {
|
const mainRef = useRef(null);
|
||||||
window.removeEventListener('resize', this.updateSize);
|
|
||||||
},
|
|
||||||
|
|
||||||
componentDidUpdate : function(prevProps) {
|
if(props.renderer == 'legacy') {
|
||||||
if(prevProps.text !== this.props.text) {
|
rawPages = props.text.split('\\page');
|
||||||
let pages;
|
|
||||||
if(this.props.renderer == 'legacy') {
|
|
||||||
pages = this.props.text.split('\\page');
|
|
||||||
} else {
|
} else {
|
||||||
pages = this.props.text.split(/^\\page$/gm);
|
rawPages = props.text.split(/^\\page$/gm);
|
||||||
}
|
}
|
||||||
this.setState({
|
|
||||||
pages : pages,
|
|
||||||
usePPR : pages.length >= PPR_THRESHOLD
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
updateSize : function() {
|
useEffect(()=>{ // Unmounting steps
|
||||||
this.setState({
|
return ()=>{window.removeEventListener('resize', updateSize);};
|
||||||
height : this.refs.main.parentNode.clientHeight,
|
}, []);
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
handleScroll : function(e){
|
const updateSize = ()=>{
|
||||||
const target = e.target;
|
setState((prevState)=>({
|
||||||
this.setState((prevState)=>({
|
...prevState,
|
||||||
viewablePageNumber : Math.floor(target.scrollTop / target.scrollHeight * prevState.pages.length)
|
height : mainRef.current.parentNode.clientHeight,
|
||||||
}));
|
}));
|
||||||
},
|
};
|
||||||
|
|
||||||
shouldRender : function(pageText, index){
|
const handleScroll = (e)=>{
|
||||||
if(!this.state.isMounted) return false;
|
const target = e.target;
|
||||||
|
setState((prevState)=>({
|
||||||
|
...prevState,
|
||||||
|
viewablePageNumber : Math.floor(target.scrollTop / target.scrollHeight * rawPages.length)
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
const viewIndex = this.state.viewablePageNumber;
|
const isInView = (index)=>{
|
||||||
if(index == viewIndex - 3) return true;
|
if(!state.isMounted)
|
||||||
if(index == viewIndex - 2) return true;
|
return false;
|
||||||
if(index == viewIndex - 1) return true;
|
|
||||||
if(index == viewIndex) return true;
|
|
||||||
if(index == viewIndex + 1) return true;
|
|
||||||
if(index == viewIndex + 2) return true;
|
|
||||||
if(index == viewIndex + 3) return true;
|
|
||||||
|
|
||||||
//Check for style tages
|
if(index == props.currentEditorPage) //Already rendered before this step
|
||||||
if(pageText.indexOf('<style>') !== -1) return true;
|
return false;
|
||||||
|
|
||||||
|
if(Math.abs(index - state.viewablePageNumber) <= 3)
|
||||||
|
return true;
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
},
|
};
|
||||||
|
|
||||||
renderPageInfo : function(){
|
const renderPageInfo = ()=>{
|
||||||
return <div className='pageInfo' ref='main'>
|
return <div className='pageInfo' ref={mainRef}>
|
||||||
<div>
|
<div>
|
||||||
{this.props.renderer}
|
{props.renderer}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{this.state.viewablePageNumber + 1} / {this.state.pages.length}
|
{state.viewablePageNumber + 1} / {rawPages.length}
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
},
|
};
|
||||||
|
|
||||||
renderPPRmsg : function(){
|
const renderDummyPage = (index)=>{
|
||||||
if(!this.state.usePPR) return;
|
|
||||||
|
|
||||||
return <div className='ppr_msg'>
|
|
||||||
Partial Page Renderer is enabled, because your brew is so large. May affect rendering.
|
|
||||||
</div>;
|
|
||||||
},
|
|
||||||
|
|
||||||
renderDummyPage : function(index){
|
|
||||||
return <div className='phb page' id={`p${index + 1}`} key={index}>
|
return <div className='phb page' id={`p${index + 1}`} key={index}>
|
||||||
<i className='fas fa-spinner fa-spin' />
|
<i className='fas fa-spinner fa-spin' />
|
||||||
</div>;
|
</div>;
|
||||||
},
|
};
|
||||||
|
|
||||||
renderStyle : function() {
|
const renderStyle = ()=>{
|
||||||
if(!this.props.style) return;
|
if(!props.style) return;
|
||||||
//return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `<style>@layer styleTab {\n${this.props.style}\n} </style>` }} />;
|
const cleanStyle = props.style; //DOMPurify.sanitize(props.style, purifyConfig);
|
||||||
return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `<style>\n${this.props.style}\n</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>` }} />;
|
||||||
|
};
|
||||||
|
|
||||||
renderPage : function(pageText, index){
|
const renderPage = (pageText, index)=>{
|
||||||
if(this.props.renderer == 'legacy')
|
if(props.renderer == 'legacy') {
|
||||||
return <div className='phb page' id={`p${index + 1}`} dangerouslySetInnerHTML={{ __html: MarkdownLegacy.render(pageText) }} key={index} />;
|
const html = MarkdownLegacy.render(pageText);
|
||||||
else {
|
return <BrewPage className='page phb' index={index} key={index} contents={html} />;
|
||||||
|
} else {
|
||||||
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)
|
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 (
|
const html = Markdown.render(pageText, index);
|
||||||
<div className='page' id={`p${index + 1}`} key={index} >
|
return <BrewPage className='page' index={index} key={index} contents={html} />;
|
||||||
<div className='columnWrapper' dangerouslySetInnerHTML={{ __html: Markdown.render(pageText) }} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
},
|
};
|
||||||
|
|
||||||
renderPages : function(){
|
const renderPages = ()=>{
|
||||||
if(this.state.usePPR){
|
if(props.errors && props.errors.length)
|
||||||
return _.map(this.state.pages, (page, index)=>{
|
return renderedPages;
|
||||||
if(this.shouldRender(page, index) && typeof window !== 'undefined'){
|
|
||||||
return this.renderPage(page, index);
|
if(rawPages.length != renderedPages.length) // Re-render all pages when page count changes
|
||||||
} else {
|
renderedPages.length = 0;
|
||||||
return this.renderDummyPage(index);
|
|
||||||
|
// Render currently-edited page first so cross-page effects (variables, links) can propagate out first
|
||||||
|
renderedPages[props.currentEditorPage] = renderPage(rawPages[props.currentEditorPage], props.currentEditorPage);
|
||||||
|
|
||||||
|
_.forEach(rawPages, (page, index)=>{
|
||||||
|
if((isInView(index) || !renderedPages[index]) && typeof window !== 'undefined'){
|
||||||
|
renderedPages[index] = renderPage(page, index); // Render any page not yet rendered, but only re-render those in PPR range
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
return renderedPages;
|
||||||
if(this.props.errors && this.props.errors.length) return this.lastRender;
|
};
|
||||||
this.lastRender = _.map(this.state.pages, (page, index)=>{
|
|
||||||
if(typeof window !== 'undefined') {
|
|
||||||
return this.renderPage(page, index);
|
|
||||||
} else {
|
|
||||||
return this.renderDummyPage(index);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return this.lastRender;
|
|
||||||
},
|
|
||||||
|
|
||||||
frameDidMount : function(){ //This triggers when iFrame finishes internal "componentDidMount"
|
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
|
setTimeout(()=>{ //We still see a flicker where the style isn't applied yet, so wait 100ms before showing iFrame
|
||||||
this.updateSize();
|
updateSize();
|
||||||
window.addEventListener('resize', this.updateSize);
|
window.addEventListener('resize', updateSize);
|
||||||
this.renderPages(); //Make sure page is renderable before showing
|
renderPages(); //Make sure page is renderable before showing
|
||||||
this.setState({
|
setState((prevState)=>({
|
||||||
|
...prevState,
|
||||||
isMounted : true,
|
isMounted : true,
|
||||||
visibility : 'visible'
|
visibility : 'visible'
|
||||||
});
|
}));
|
||||||
}, 100);
|
}, 100);
|
||||||
},
|
};
|
||||||
|
|
||||||
render : function(){
|
const emitClick = ()=>{ // Allow clicks inside iFrame to interact with dropdowns, etc. from outside
|
||||||
//render in iFrame so broken code doesn't crash the site.
|
if(!window || !document) return;
|
||||||
//Also render dummy page while iframe is mounting.
|
document.dispatchEvent(new MouseEvent('click'));
|
||||||
const rendererPath = this.props.renderer == 'V3' ? 'V3' : 'Legacy';
|
};
|
||||||
const themePath = this.props.theme ?? '5ePHB';
|
|
||||||
|
const rendererPath = props.renderer == 'V3' ? 'V3' : 'Legacy';
|
||||||
|
const themePath = props.theme ?? '5ePHB';
|
||||||
const baseThemePath = Themes[rendererPath][themePath].baseTheme;
|
const baseThemePath = Themes[rendererPath][themePath].baseTheme;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<>
|
||||||
{!this.state.isMounted
|
{/*render dummy page while iFrame is mounting.*/}
|
||||||
? <div className='brewRenderer' onScroll={this.handleScroll}>
|
{!state.isMounted
|
||||||
<div className='pages' ref='pages'>
|
? <div className='brewRenderer' onScroll={handleScroll}>
|
||||||
{this.renderDummyPage(1)}
|
<div className='pages'>
|
||||||
|
{renderDummyPage(1)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
: null}
|
: null}
|
||||||
|
|
||||||
<Frame id='BrewRenderer' initialContent={this.state.initialContent}
|
<ErrorBar errors={props.errors} />
|
||||||
style={{ width: '100%', height: '100%', visibility: this.state.visibility }}
|
|
||||||
contentDidMount={this.frameDidMount}>
|
|
||||||
<div className={'brewRenderer'}
|
|
||||||
onScroll={this.handleScroll}
|
|
||||||
style={{ height: this.state.height }}>
|
|
||||||
|
|
||||||
<ErrorBar errors={this.props.errors} />
|
|
||||||
<div className='popups'>
|
<div className='popups'>
|
||||||
<RenderWarnings />
|
<RenderWarnings />
|
||||||
<NotificationPopup />
|
<NotificationPopup />
|
||||||
</div>
|
</div>
|
||||||
<link href={`/themes/${rendererPath}/Blank/style.css`} rel='stylesheet'/>
|
|
||||||
|
{/*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 }}
|
||||||
|
contentDidMount={frameDidMount}
|
||||||
|
onClick={()=>{emitClick();}}
|
||||||
|
>
|
||||||
|
<div className={'brewRenderer'}
|
||||||
|
onScroll={handleScroll}
|
||||||
|
onKeyDown={handleControlKeys}
|
||||||
|
tabIndex={-1}
|
||||||
|
style={{ height: state.height }}>
|
||||||
|
|
||||||
|
<link href={`/themes/${rendererPath}/Blank/style.css`} type='text/css' rel='stylesheet'/>
|
||||||
{baseThemePath &&
|
{baseThemePath &&
|
||||||
<link href={`/themes/${rendererPath}/${baseThemePath}/style.css`} rel='stylesheet'/>
|
<link href={`/themes/${rendererPath}/${baseThemePath}/style.css`} type='text/css' rel='stylesheet'/>
|
||||||
}
|
}
|
||||||
<link href={`/themes/${rendererPath}/${themePath}/style.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 */}
|
{/* Apply CSS from Style tab and render pages from Markdown tab */}
|
||||||
{this.state.isMounted
|
{state.isMounted
|
||||||
&&
|
&&
|
||||||
<>
|
<>
|
||||||
{this.renderStyle()}
|
{renderStyle()}
|
||||||
<div className='pages' ref='pages' lang={`${this.props.lang || 'en'}`}>
|
<div className='pages' lang={`${props.lang || 'en'}`}>
|
||||||
{this.renderPages()}
|
{renderPages()}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</Frame>
|
</Frame>
|
||||||
{this.renderPageInfo()}
|
{renderPageInfo()}
|
||||||
{this.renderPPRmsg()}
|
</>
|
||||||
</React.Fragment>
|
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = BrewRenderer;
|
module.exports = BrewRenderer;
|
||||||
|
|||||||
@@ -1,46 +1,79 @@
|
|||||||
@import (multiple, less) 'shared/naturalcrit/styles/reset.less';
|
@import (multiple, less) 'shared/naturalcrit/styles/reset.less';
|
||||||
|
|
||||||
.brewRenderer{
|
.brewRenderer {
|
||||||
will-change : transform;
|
will-change : transform;
|
||||||
overflow-y : scroll;
|
overflow-y : scroll;
|
||||||
.pages{
|
:where(.pages) {
|
||||||
margin : 30px 0px;
|
margin : 30px 0px;
|
||||||
&>.page{
|
& > :where(.page) {
|
||||||
|
width : 215.9mm;
|
||||||
|
height : 279.4mm;
|
||||||
margin-right : auto;
|
margin-right : auto;
|
||||||
margin-bottom : 30px;
|
margin-bottom : 30px;
|
||||||
margin-left : auto;
|
margin-left : auto;
|
||||||
box-shadow : 1px 4px 14px #000;
|
box-shadow : 1px 4px 14px #000000;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 20px;
|
||||||
|
&:horizontal{
|
||||||
|
height: 20px;
|
||||||
|
width:auto;
|
||||||
|
}
|
||||||
|
&-thumb {
|
||||||
|
background: linear-gradient(90deg, #d3c1af 15px, #00000000 15px);
|
||||||
|
&:horizontal{
|
||||||
|
background: linear-gradient(0deg, #d3c1af 15px, #00000000 15px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&-corner {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
.pane{
|
.pane { position : relative; }
|
||||||
position : relative;
|
.pageInfo {
|
||||||
}
|
|
||||||
.pageInfo{
|
|
||||||
position : absolute;
|
position : absolute;
|
||||||
right : 17px;
|
right : 17px;
|
||||||
bottom : 0;
|
bottom : 0;
|
||||||
z-index : 1000;
|
z-index : 1000;
|
||||||
background-color : #333;
|
|
||||||
font-size : 10px;
|
font-size : 10px;
|
||||||
font-weight : 800;
|
font-weight : 800;
|
||||||
color : white;
|
color : white;
|
||||||
|
background-color : #333333;
|
||||||
div {
|
div {
|
||||||
display: inline-block;
|
display : inline-block;
|
||||||
padding : 8px 10px;
|
padding : 8px 10px;
|
||||||
&:not(:last-child){
|
&:not(:last-child) { border-right : 1px solid #666666; }
|
||||||
border-right: 1px solid #666;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.ppr_msg{
|
.ppr_msg {
|
||||||
position : absolute;
|
position : absolute;
|
||||||
left : 0px;
|
|
||||||
bottom : 0;
|
bottom : 0;
|
||||||
|
left : 0px;
|
||||||
z-index : 1000;
|
z-index : 1000;
|
||||||
padding : 8px 10px;
|
padding : 8px 10px;
|
||||||
background-color : #333;
|
|
||||||
font-size : 10px;
|
font-size : 10px;
|
||||||
font-weight : 800;
|
font-weight : 800;
|
||||||
color : white;
|
color : white;
|
||||||
|
background-color : #333333;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
.brewRenderer {
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: unset;
|
||||||
|
.pages {
|
||||||
|
margin: 0px;
|
||||||
|
&>.page {
|
||||||
|
box-shadow: unset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,6 @@ require('./errorBar.less');
|
|||||||
const React = require('react');
|
const React = require('react');
|
||||||
const createClass = require('create-react-class');
|
const createClass = require('create-react-class');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const cx = require('classnames');
|
|
||||||
|
|
||||||
const ErrorBar = createClass({
|
const ErrorBar = createClass({
|
||||||
displayName : 'ErrorBar',
|
displayName : 'ErrorBar',
|
||||||
|
|||||||
@@ -1,37 +1,25 @@
|
|||||||
require('./notificationPopup.less');
|
require('./notificationPopup.less');
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const createClass = require('create-react-class');
|
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const cx = require('classnames'); //Unused variable
|
|
||||||
|
import Dialog from '../../../components/dialog.jsx';
|
||||||
|
|
||||||
const DISMISS_KEY = 'dismiss_notification12-04-23';
|
const DISMISS_KEY = 'dismiss_notification12-04-23';
|
||||||
|
const DISMISS_BUTTON = <i className='fas fa-times dismiss' />;
|
||||||
|
|
||||||
const NotificationPopup = createClass({
|
const NotificationPopup = ()=>{
|
||||||
displayName : 'NotificationPopup',
|
return <Dialog className='notificationPopup' dismissKey={DISMISS_KEY} closeText={DISMISS_BUTTON} >
|
||||||
getInitialState : function() {
|
<div className='header'>
|
||||||
return {
|
<i className='fas fa-info-circle info'></i>
|
||||||
notifications : {}
|
<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>
|
||||||
componentDidMount : function() {
|
<ul>
|
||||||
this.checkNotifications();
|
|
||||||
window.addEventListener('resize', this.checkNotifications);
|
|
||||||
},
|
|
||||||
componentWillUnmount : function() {
|
|
||||||
window.removeEventListener('resize', this.checkNotifications);
|
|
||||||
},
|
|
||||||
notifications : {
|
|
||||||
psa : function(){
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<li key='psa'>
|
<li key='psa'>
|
||||||
<em>Broken default logo on <b>CoverPage</b> </em> <br />
|
<em>Don't store IMAGES in Google Drive</em><br />
|
||||||
If you have used the Cover Page snippet and notice the Naturalcrit
|
Google Drive is not an image service, and will block images from being used
|
||||||
logo is showing as a broken image, this is due to some small tweaks
|
in brews if they get more views than expected. Google has confirmed they won't fix
|
||||||
of this BETA feature. To fix the logo in your cover page, rename
|
this, so we recommend you look for another image hosting service such as imgur, ImgBB or Google Photos.
|
||||||
the image link <b>"/assets/naturalCritLogoRed.svg"</b>. Remember
|
|
||||||
that any snippet marked "BETA" may have a similar change in the
|
|
||||||
future as we encounter any bugs or reworks.
|
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li key='googleDriveFolder'>
|
<li key='googleDriveFolder'>
|
||||||
@@ -50,35 +38,8 @@ const NotificationPopup = createClass({
|
|||||||
See the FAQ
|
See the FAQ
|
||||||
</a> to learn how to avoid losing your work!
|
</a> to learn how to avoid losing your work!
|
||||||
</li>
|
</li>
|
||||||
</>
|
</ul>
|
||||||
);
|
</Dialog>;
|
||||||
}
|
};
|
||||||
},
|
|
||||||
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>;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = NotificationPopup;
|
module.exports = NotificationPopup;
|
||||||
|
|||||||
@@ -1,64 +1,60 @@
|
|||||||
.popups{
|
.popups {
|
||||||
position : fixed;
|
position : fixed;
|
||||||
top : @navbarHeight;
|
top : @navbarHeight;
|
||||||
right : 15px;
|
right : 24px;
|
||||||
z-index : 10001;
|
z-index : 10001;
|
||||||
width : 450px;
|
width : 450px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.notificationPopup{
|
.notificationPopup {
|
||||||
position : relative;
|
position : relative;
|
||||||
display : inline-block;
|
|
||||||
width : 100%;
|
width : 100%;
|
||||||
padding : 15px;
|
padding : 15px;
|
||||||
padding-bottom : 10px;
|
padding-bottom : 10px;
|
||||||
padding-left : 25px;
|
padding-left : 25px;
|
||||||
background-color : @blue;
|
|
||||||
color : white;
|
color : white;
|
||||||
a{
|
background-color : @blue;
|
||||||
color : #e0e5c1;
|
border : none;
|
||||||
|
&[open] { display : inline-block; }
|
||||||
|
a {
|
||||||
font-weight : 800;
|
font-weight : 800;
|
||||||
|
color : #E0E5C1;
|
||||||
}
|
}
|
||||||
i.info{
|
i.info {
|
||||||
position : absolute;
|
position : absolute;
|
||||||
top : 12px;
|
top : 12px;
|
||||||
left : 12px;
|
left : 12px;
|
||||||
opacity : 0.8;
|
|
||||||
font-size : 2.5em;
|
font-size : 2.5em;
|
||||||
|
opacity : 0.8;
|
||||||
}
|
}
|
||||||
i.dismiss{
|
button.dismiss {
|
||||||
position : absolute;
|
position : absolute;
|
||||||
top : 10px;
|
top : 10px;
|
||||||
right : 10px;
|
right : 10px;
|
||||||
cursor : pointer;
|
cursor : pointer;
|
||||||
|
background-color : transparent;
|
||||||
opacity : 0.6;
|
opacity : 0.6;
|
||||||
&:hover{
|
&:hover { opacity : 1; }
|
||||||
opacity : 1;
|
|
||||||
}
|
}
|
||||||
}
|
.header { padding-left : 50px; }
|
||||||
.header {
|
small {
|
||||||
padding-left : 50px;
|
|
||||||
}
|
|
||||||
small{
|
|
||||||
opacity : 0.7;
|
|
||||||
font-size : 0.6em;
|
font-size : 0.6em;
|
||||||
|
opacity : 0.7;
|
||||||
}
|
}
|
||||||
h3{
|
h3 {
|
||||||
font-size : 1.1em;
|
font-size : 1.1em;
|
||||||
font-weight : 800;
|
font-weight : 800;
|
||||||
}
|
}
|
||||||
ul{
|
ul {
|
||||||
margin-top : 15px;
|
margin-top : 15px;
|
||||||
font-size : 0.8em;
|
font-size : 0.8em;
|
||||||
list-style-position : outside;
|
list-style-position : outside;
|
||||||
list-style-type : disc;
|
list-style-type : disc;
|
||||||
li{
|
li {
|
||||||
|
margin-top : 1.4em;
|
||||||
font-size : 0.8em;
|
font-size : 0.8em;
|
||||||
line-height : 1.4em;
|
line-height : 1.4em;
|
||||||
margin-top : 1.4em;
|
em { font-weight : 800; }
|
||||||
em{
|
|
||||||
font-weight : 800;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,11 +5,14 @@ const createClass = require('create-react-class');
|
|||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const cx = require('classnames');
|
const cx = require('classnames');
|
||||||
const dedent = require('dedent-tabs').default;
|
const dedent = require('dedent-tabs').default;
|
||||||
|
const Markdown = require('../../../shared/naturalcrit/markdown.js');
|
||||||
|
|
||||||
const CodeEditor = require('naturalcrit/codeEditor/codeEditor.jsx');
|
const CodeEditor = require('naturalcrit/codeEditor/codeEditor.jsx');
|
||||||
const SnippetBar = require('./snippetbar/snippetbar.jsx');
|
const SnippetBar = require('./snippetbar/snippetbar.jsx');
|
||||||
const MetadataEditor = require('./metadataEditor/metadataEditor.jsx');
|
const MetadataEditor = require('./metadataEditor/metadataEditor.jsx');
|
||||||
|
|
||||||
|
const EDITOR_THEME_KEY = 'HOMEBREWERY-EDITOR-THEME';
|
||||||
|
|
||||||
const SNIPPETBAR_HEIGHT = 25;
|
const SNIPPETBAR_HEIGHT = 25;
|
||||||
const DEFAULT_STYLE_TEXT = dedent`
|
const DEFAULT_STYLE_TEXT = dedent`
|
||||||
/*=======--- Example CSS styling ---=======*/
|
/*=======--- Example CSS styling ---=======*/
|
||||||
@@ -34,15 +37,20 @@ const Editor = createClass({
|
|||||||
onMetaChange : ()=>{},
|
onMetaChange : ()=>{},
|
||||||
reportError : ()=>{},
|
reportError : ()=>{},
|
||||||
|
|
||||||
|
editorTheme : 'default',
|
||||||
renderer : 'legacy'
|
renderer : 'legacy'
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
getInitialState : function() {
|
getInitialState : function() {
|
||||||
return {
|
return {
|
||||||
|
editorTheme : this.props.editorTheme,
|
||||||
view : 'text' //'text', 'style', 'meta'
|
view : 'text' //'text', 'style', 'meta'
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
editor : React.createRef(null),
|
||||||
|
codeEditor : React.createRef(null),
|
||||||
|
|
||||||
isText : function() {return this.state.view == 'text';},
|
isText : function() {return this.state.view == 'text';},
|
||||||
isStyle : function() {return this.state.view == 'style';},
|
isStyle : function() {return this.state.view == 'style';},
|
||||||
isMeta : function() {return this.state.view == 'meta';},
|
isMeta : function() {return this.state.view == 'meta';},
|
||||||
@@ -51,6 +59,13 @@ const Editor = createClass({
|
|||||||
this.updateEditorSize();
|
this.updateEditorSize();
|
||||||
this.highlightCustomMarkdown();
|
this.highlightCustomMarkdown();
|
||||||
window.addEventListener('resize', this.updateEditorSize);
|
window.addEventListener('resize', this.updateEditorSize);
|
||||||
|
|
||||||
|
const editorTheme = window.localStorage.getItem(EDITOR_THEME_KEY);
|
||||||
|
if(editorTheme) {
|
||||||
|
this.setState({
|
||||||
|
editorTheme : editorTheme
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillUnmount : function() {
|
componentWillUnmount : function() {
|
||||||
@@ -68,15 +83,15 @@ const Editor = createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
updateEditorSize : function() {
|
updateEditorSize : function() {
|
||||||
if(this.refs.codeEditor) {
|
if(this.codeEditor.current) {
|
||||||
let paneHeight = this.refs.main.parentNode.clientHeight;
|
let paneHeight = this.editor.current.parentNode.clientHeight;
|
||||||
paneHeight -= SNIPPETBAR_HEIGHT + 1;
|
paneHeight -= SNIPPETBAR_HEIGHT;
|
||||||
this.refs.codeEditor.codeMirror.setSize(null, paneHeight);
|
this.codeEditor.current.codeMirror.setSize(null, paneHeight);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
handleInject : function(injectText){
|
handleInject : function(injectText){
|
||||||
this.refs.codeEditor?.injectText(injectText, false);
|
this.codeEditor.current?.injectText(injectText, false);
|
||||||
},
|
},
|
||||||
|
|
||||||
handleViewChange : function(newView){
|
handleViewChange : function(newView){
|
||||||
@@ -87,7 +102,7 @@ const Editor = createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
getCurrentPage : function(){
|
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)=>{
|
return _.reduce(lines, (r, line)=>{
|
||||||
if(
|
if(
|
||||||
(this.props.renderer == 'legacy' && line.indexOf('\\page') !== -1)
|
(this.props.renderer == 'legacy' && line.indexOf('\\page') !== -1)
|
||||||
@@ -99,9 +114,9 @@ const Editor = createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
highlightCustomMarkdown : function(){
|
highlightCustomMarkdown : function(){
|
||||||
if(!this.refs.codeEditor) return;
|
if(!this.codeEditor.current) return;
|
||||||
if(this.state.view === 'text') {
|
if(this.state.view === 'text') {
|
||||||
const codeMirror = this.refs.codeEditor.codeMirror;
|
const codeMirror = this.codeEditor.current.codeMirror;
|
||||||
|
|
||||||
codeMirror.operation(()=>{ // Batch CodeMirror styling
|
codeMirror.operation(()=>{ // Batch CodeMirror styling
|
||||||
//reset custom text styles
|
//reset custom text styles
|
||||||
@@ -138,9 +153,45 @@ const Editor = createClass({
|
|||||||
codeMirror.addLineClass(lineNumber, 'text', 'columnSplit');
|
codeMirror.addLineClass(lineNumber, 'text', 'columnSplit');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// definition lists
|
||||||
|
if(line.includes('::')){
|
||||||
|
if(/^:*$/.test(line) == true){ return };
|
||||||
|
const regex = /^([^\n]*?:?\s?)(::[^\n]*)(?:\n|$)/ymd; // the `d` flag, for match indices, throws an ESLint error.
|
||||||
|
let match;
|
||||||
|
while ((match = regex.exec(line)) != null){
|
||||||
|
codeMirror.markText({ line: lineNumber, ch: match.indices[0][0] }, { line: lineNumber, ch: match.indices[0][1] }, { className: 'dl-highlight' });
|
||||||
|
codeMirror.markText({ line: lineNumber, ch: match.indices[1][0] }, { line: lineNumber, ch: match.indices[1][1] }, { className: 'dt-highlight' });
|
||||||
|
codeMirror.markText({ line: lineNumber, ch: match.indices[2][0] }, { line: lineNumber, ch: match.indices[2][1] }, { className: 'dd-highlight' });
|
||||||
|
const ddIndex = match.indices[2][0];
|
||||||
|
let colons = /::/g;
|
||||||
|
let colonMatches = colons.exec(match[2]);
|
||||||
|
if(colonMatches !== null){
|
||||||
|
codeMirror.markText({ line: lineNumber, ch: colonMatches.index + ddIndex }, { line: lineNumber, ch: colonMatches.index + colonMatches[0].length + ddIndex }, { className: 'dl-colon-highlight'} )
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subscript & Superscript
|
||||||
|
if(line.includes('^')) {
|
||||||
|
let startIndex = line.indexOf('^');
|
||||||
|
const superRegex = /\^(?!\s)(?=([^\n\^]*[^\s\^]))\1\^/gy;
|
||||||
|
const subRegex = /\^\^(?!\s)(?=([^\n\^]*[^\s\^]))\1\^\^/gy;
|
||||||
|
|
||||||
|
while (startIndex >= 0) {
|
||||||
|
superRegex.lastIndex = subRegex.lastIndex = startIndex;
|
||||||
|
let isSuper = false;
|
||||||
|
let match = subRegex.exec(line) || superRegex.exec(line);
|
||||||
|
if (match) {
|
||||||
|
isSuper = !subRegex.lastIndex;
|
||||||
|
codeMirror.markText({ line: lineNumber, ch: match.index }, { line: lineNumber, ch: match.index + match[0].length }, { className: isSuper ? 'superscript' : 'subscript' });
|
||||||
|
}
|
||||||
|
startIndex = line.indexOf('^', Math.max(startIndex + 1, subRegex.lastIndex, superRegex.lastIndex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Highlight injectors {style}
|
// Highlight injectors {style}
|
||||||
if(line.includes('{') && line.includes('}')){
|
if(line.includes('{') && line.includes('}')){
|
||||||
const regex = /(?:^|[^{\n])({(?=((?::(?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':{}\s]*)*))\2})/gm;
|
const regex = /(?:^|[^{\n])({(?=((?:[:=](?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':={}\s]*)*))\2})/gm;
|
||||||
let match;
|
let match;
|
||||||
while ((match = regex.exec(line)) != null) {
|
while ((match = regex.exec(line)) != null) {
|
||||||
codeMirror.markText({ line: lineNumber, ch: line.indexOf(match[1]) }, { line: lineNumber, ch: line.indexOf(match[1]) + match[1].length }, { className: 'injection' });
|
codeMirror.markText({ line: lineNumber, ch: line.indexOf(match[1]) }, { line: lineNumber, ch: line.indexOf(match[1]) + match[1].length }, { className: 'injection' });
|
||||||
@@ -148,7 +199,7 @@ const Editor = createClass({
|
|||||||
}
|
}
|
||||||
// Highlight inline spans {{content}}
|
// Highlight inline spans {{content}}
|
||||||
if(line.includes('{{') && line.includes('}}')){
|
if(line.includes('{{') && line.includes('}}')){
|
||||||
const regex = /{{(?=((?::(?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':{}\s]*)*))\1 *|}}/g;
|
const regex = /{{(?=((?:[:=](?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':={}\s]*)*))\1 *|}}/g;
|
||||||
let match;
|
let match;
|
||||||
let blockCount = 0;
|
let blockCount = 0;
|
||||||
while ((match = regex.exec(line)) != null) {
|
while ((match = regex.exec(line)) != null) {
|
||||||
@@ -167,11 +218,39 @@ const Editor = createClass({
|
|||||||
// Highlight block divs {{\n Content \n}}
|
// Highlight block divs {{\n Content \n}}
|
||||||
let endCh = line.length+1;
|
let endCh = line.length+1;
|
||||||
|
|
||||||
const match = line.match(/^ *{{(?=((?::(?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':{}\s]*)*))\1 *$|^ *}}$/);
|
const match = line.match(/^ *{{(?=((?:[:=](?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':={}\s]*)*))\1 *$|^ *}}$/);
|
||||||
if(match)
|
if(match)
|
||||||
endCh = match.index+match[0].length;
|
endCh = match.index+match[0].length;
|
||||||
codeMirror.markText({ line: lineNumber, ch: 0 }, { line: lineNumber, ch: endCh }, { className: 'block' });
|
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) {
|
||||||
|
marker.clear();
|
||||||
|
});
|
||||||
|
codeMirror.markText(startPos, endPos, { className: 'emoji' });
|
||||||
|
}
|
||||||
|
startIndex = line.indexOf(':', Math.max(startIndex + 1, emojiRegex.lastIndex));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -226,23 +305,23 @@ const Editor = createClass({
|
|||||||
|
|
||||||
targetLine = lineCount - 1; //Scroll to `\page`, which is one line back.
|
targetLine = lineCount - 1; //Scroll to `\page`, which is one line back.
|
||||||
|
|
||||||
let currentY = this.refs.codeEditor.codeMirror.getScrollInfo().top;
|
let currentY = this.codeEditor.current.codeMirror.getScrollInfo().top;
|
||||||
let targetY = this.refs.codeEditor.codeMirror.heightAtLine(targetLine, 'local', true);
|
let targetY = this.codeEditor.current.codeMirror.heightAtLine(targetLine, 'local', true);
|
||||||
|
|
||||||
//Scroll 1/10 of the way every 10ms until 1px off.
|
//Scroll 1/10 of the way every 10ms until 1px off.
|
||||||
const incrementalScroll = setInterval(()=>{
|
const incrementalScroll = setInterval(()=>{
|
||||||
currentY += (targetY - currentY) / 10;
|
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
|
// Update target: target height is not accurate until within +-10 lines of the visible window
|
||||||
if(Math.abs(targetY - currentY > 100))
|
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
|
// End when close enough
|
||||||
if(Math.abs(targetY - currentY) < 1) {
|
if(Math.abs(targetY - currentY) < 1) {
|
||||||
this.refs.codeEditor.codeMirror.scrollTo(null, targetY); // Scroll any remaining difference
|
this.codeEditor.current.codeMirror.scrollTo(null, targetY); // Scroll any remaining difference
|
||||||
this.refs.codeEditor.setCursorPosition({ line: targetLine + 1, ch: 0 });
|
this.codeEditor.current.setCursorPosition({ line: targetLine + 1, ch: 0 });
|
||||||
this.refs.codeEditor.codeMirror.addLineClass(targetLine + 1, 'wrap', 'sourceMoveFlash');
|
this.codeEditor.current.codeMirror.addLineClass(targetLine + 1, 'wrap', 'sourceMoveFlash');
|
||||||
clearInterval(incrementalScroll);
|
clearInterval(incrementalScroll);
|
||||||
}
|
}
|
||||||
}, 10);
|
}, 10);
|
||||||
@@ -252,7 +331,14 @@ const Editor = createClass({
|
|||||||
|
|
||||||
//Called when there are changes to the editor's dimensions
|
//Called when there are changes to the editor's dimensions
|
||||||
update : function(){
|
update : function(){
|
||||||
this.refs.codeEditor?.updateSize();
|
this.codeEditor.current?.updateSize();
|
||||||
|
},
|
||||||
|
|
||||||
|
updateEditorTheme : function(newTheme){
|
||||||
|
window.localStorage.setItem(EDITOR_THEME_KEY, newTheme);
|
||||||
|
this.setState({
|
||||||
|
editorTheme : newTheme
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
//Called by CodeEditor after document switch, so Snippetbar can refresh UndoHistory
|
//Called by CodeEditor after document switch, so Snippetbar can refresh UndoHistory
|
||||||
@@ -264,23 +350,25 @@ const Editor = createClass({
|
|||||||
if(this.isText()){
|
if(this.isText()){
|
||||||
return <>
|
return <>
|
||||||
<CodeEditor key='codeEditor'
|
<CodeEditor key='codeEditor'
|
||||||
ref='codeEditor'
|
ref={this.codeEditor}
|
||||||
language='gfm'
|
language='gfm'
|
||||||
view={this.state.view}
|
view={this.state.view}
|
||||||
value={this.props.brew.text}
|
value={this.props.brew.text}
|
||||||
onChange={this.props.onTextChange}
|
onChange={this.props.onTextChange}
|
||||||
|
editorTheme={this.state.editorTheme}
|
||||||
rerenderParent={this.rerenderParent} />
|
rerenderParent={this.rerenderParent} />
|
||||||
</>;
|
</>;
|
||||||
}
|
}
|
||||||
if(this.isStyle()){
|
if(this.isStyle()){
|
||||||
return <>
|
return <>
|
||||||
<CodeEditor key='codeEditor'
|
<CodeEditor key='codeEditor'
|
||||||
ref='codeEditor'
|
ref={this.codeEditor}
|
||||||
language='css'
|
language='css'
|
||||||
view={this.state.view}
|
view={this.state.view}
|
||||||
value={this.props.brew.style ?? DEFAULT_STYLE_TEXT}
|
value={this.props.brew.style ?? DEFAULT_STYLE_TEXT}
|
||||||
onChange={this.props.onStyleChange}
|
onChange={this.props.onStyleChange}
|
||||||
enableFolding={false}
|
enableFolding={true}
|
||||||
|
editorTheme={this.state.editorTheme}
|
||||||
rerenderParent={this.rerenderParent} />
|
rerenderParent={this.rerenderParent} />
|
||||||
</>;
|
</>;
|
||||||
}
|
}
|
||||||
@@ -299,20 +387,28 @@ const Editor = createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
redo : function(){
|
redo : function(){
|
||||||
return this.refs.codeEditor?.redo();
|
return this.codeEditor.current?.redo();
|
||||||
},
|
},
|
||||||
|
|
||||||
historySize : function(){
|
historySize : function(){
|
||||||
return this.refs.codeEditor?.historySize();
|
return this.codeEditor.current?.historySize();
|
||||||
},
|
},
|
||||||
|
|
||||||
undo : function(){
|
undo : function(){
|
||||||
return this.refs.codeEditor?.undo();
|
return this.codeEditor.current?.undo();
|
||||||
|
},
|
||||||
|
|
||||||
|
foldCode : function(){
|
||||||
|
return this.codeEditor.current?.foldAllCode();
|
||||||
|
},
|
||||||
|
|
||||||
|
unfoldCode : function(){
|
||||||
|
return this.codeEditor.current?.unfoldAllCode();
|
||||||
},
|
},
|
||||||
|
|
||||||
render : function(){
|
render : function(){
|
||||||
return (
|
return (
|
||||||
<div className='editor' ref='main'>
|
<div className='editor' ref={this.editor}>
|
||||||
<SnippetBar
|
<SnippetBar
|
||||||
brew={this.props.brew}
|
brew={this.props.brew}
|
||||||
view={this.state.view}
|
view={this.state.view}
|
||||||
@@ -323,7 +419,12 @@ const Editor = createClass({
|
|||||||
theme={this.props.brew.theme}
|
theme={this.props.brew.theme}
|
||||||
undo={this.undo}
|
undo={this.undo}
|
||||||
redo={this.redo}
|
redo={this.redo}
|
||||||
historySize={this.historySize()} />
|
foldCode={this.foldCode}
|
||||||
|
unfoldCode={this.unfoldCode}
|
||||||
|
historySize={this.historySize()}
|
||||||
|
currentEditorTheme={this.state.editorTheme}
|
||||||
|
updateEditorTheme={this.updateEditorTheme}
|
||||||
|
cursorPos={this.codeEditor.current?.getCursorPosition() || {}} />
|
||||||
|
|
||||||
{this.renderEditor()}
|
{this.renderEditor()}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,65 +1,105 @@
|
|||||||
|
@import 'themes/codeMirror/customEditorStyles.less';
|
||||||
.editor{
|
.editor {
|
||||||
position : relative;
|
position : relative;
|
||||||
width : 100%;
|
width : 100%;
|
||||||
|
|
||||||
.codeEditor{
|
.codeEditor {
|
||||||
height : 100%;
|
height : 100%;
|
||||||
.pageLine{
|
.pageLine {
|
||||||
background : #33333328;
|
background : #33333328;
|
||||||
border-top : #339 solid 1px;
|
border-top : #333399 solid 1px;
|
||||||
}
|
}
|
||||||
.editor-page-count{
|
.editor-page-count {
|
||||||
color : grey;
|
|
||||||
float : right;
|
float : right;
|
||||||
|
color : grey;
|
||||||
}
|
}
|
||||||
.columnSplit{
|
.columnSplit {
|
||||||
font-style : italic;
|
font-style : italic;
|
||||||
color : grey;
|
color : grey;
|
||||||
background-color : fade(#299, 15%);
|
background-color : fade(#229999, 15%);
|
||||||
border-bottom : #299 solid 1px;
|
border-bottom : #229999 solid 1px;
|
||||||
}
|
}
|
||||||
.block:not(.cm-comment){
|
.define {
|
||||||
|
&:not(.term):not(.definition) {
|
||||||
|
font-weight : bold;
|
||||||
|
color : #949494;
|
||||||
|
background : #E5E5E5;
|
||||||
|
border-radius : 3px;
|
||||||
|
}
|
||||||
|
&.term { color : rgb(96, 117, 143); }
|
||||||
|
&.definition { color : rgb(97, 57, 178); }
|
||||||
|
}
|
||||||
|
.block:not(.cm-comment) {
|
||||||
|
font-weight : bold;
|
||||||
color : purple;
|
color : purple;
|
||||||
font-weight : bold;
|
|
||||||
//font-style: italic;
|
//font-style: italic;
|
||||||
}
|
}
|
||||||
.inline-block:not(.cm-comment){
|
.inline-block:not(.cm-comment) {
|
||||||
|
font-weight : bold;
|
||||||
color : red;
|
color : red;
|
||||||
font-weight : bold;
|
|
||||||
//font-style: italic;
|
//font-style: italic;
|
||||||
}
|
}
|
||||||
.injection:not(.cm-comment){
|
.injection:not(.cm-comment) {
|
||||||
color : green;
|
|
||||||
font-weight : bold;
|
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;
|
||||||
|
vertical-align : super;
|
||||||
|
font-size : 0.9em;
|
||||||
|
}
|
||||||
|
.subscript:not(.cm-comment) {
|
||||||
|
font-weight : bold;
|
||||||
|
color : rgb(123, 123, 15);
|
||||||
|
vertical-align : sub;
|
||||||
|
font-size : 0.9em;
|
||||||
|
}
|
||||||
|
.dl-highlight {
|
||||||
|
&.dl-colon-highlight {
|
||||||
|
font-weight : bold;
|
||||||
|
color : #949494;
|
||||||
|
background : #E5E5E5;
|
||||||
|
border-radius : 3px;
|
||||||
|
}
|
||||||
|
&.dt-highlight { color : rgb(96, 117, 143); }
|
||||||
|
&.dd-highlight { color : rgb(97, 57, 178); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.brewJump{
|
.brewJump {
|
||||||
position : absolute;
|
position : absolute;
|
||||||
background-color : @teal;
|
right : 20px;
|
||||||
cursor : pointer;
|
bottom : 20px;
|
||||||
width : 30px;
|
z-index : 1000000;
|
||||||
height : 30px;
|
|
||||||
display : flex;
|
display : flex;
|
||||||
align-items : center;
|
align-items : center;
|
||||||
bottom : 20px;
|
|
||||||
right : 20px;
|
|
||||||
z-index : 1000000;
|
|
||||||
justify-content : center;
|
justify-content : center;
|
||||||
.tooltipLeft("Jump to brew page");
|
width : 30px;
|
||||||
|
height : 30px;
|
||||||
|
cursor : pointer;
|
||||||
|
background-color : @teal;
|
||||||
|
.tooltipLeft('Jump to brew page');
|
||||||
}
|
}
|
||||||
|
|
||||||
.editorToolbar{
|
.editorToolbar {
|
||||||
position: absolute;
|
position : absolute;
|
||||||
top: 5px;
|
top : 5px;
|
||||||
left: 50%;
|
left : 50%;
|
||||||
color: black;
|
z-index : 9;
|
||||||
font-size: 13px;
|
font-size : 13px;
|
||||||
z-index: 9;
|
color : black;
|
||||||
span {
|
span { padding : 2px 5px; }
|
||||||
padding: 2px 5px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ require('./metadataEditor.less');
|
|||||||
const React = require('react');
|
const React = require('react');
|
||||||
const createClass = require('create-react-class');
|
const createClass = require('create-react-class');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const cx = require('classnames');
|
|
||||||
const request = require('../../utils/request-middleware.js');
|
const request = require('../../utils/request-middleware.js');
|
||||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||||
const Combobox = require('client/components/combobox.jsx');
|
const Combobox = require('client/components/combobox.jsx');
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
.metadataEditor{
|
.metadataEditor{
|
||||||
position : absolute;
|
position : absolute;
|
||||||
z-index : 10000;
|
z-index : 5;
|
||||||
box-sizing : border-box;
|
box-sizing : border-box;
|
||||||
width : 100%;
|
width : 100%;
|
||||||
padding : 25px;
|
padding : 25px;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/*eslint max-lines: ["warn", {"max": 250, "skipBlankLines": true, "skipComments": true}]*/
|
||||||
require('./snippetbar.less');
|
require('./snippetbar.less');
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const createClass = require('create-react-class');
|
const createClass = require('create-react-class');
|
||||||
@@ -15,8 +16,10 @@ ThemeSnippets['V3_5eDMG'] = require('themes/V3/5eDMG/snippets.js');
|
|||||||
ThemeSnippets['V3_Journal'] = require('themes/V3/Journal/snippets.js');
|
ThemeSnippets['V3_Journal'] = require('themes/V3/Journal/snippets.js');
|
||||||
ThemeSnippets['V3_Blank'] = require('themes/V3/Blank/snippets.js');
|
ThemeSnippets['V3_Blank'] = require('themes/V3/Blank/snippets.js');
|
||||||
|
|
||||||
const execute = function(val, brew){
|
const EditorThemes = require('build/homebrew/codeMirror/editorThemes.json');
|
||||||
if(_.isFunction(val)) return val(brew);
|
|
||||||
|
const execute = function(val, props){
|
||||||
|
if(_.isFunction(val)) return val(props);
|
||||||
return val;
|
return val;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -33,13 +36,18 @@ const Snippetbar = createClass({
|
|||||||
renderer : 'legacy',
|
renderer : 'legacy',
|
||||||
undo : ()=>{},
|
undo : ()=>{},
|
||||||
redo : ()=>{},
|
redo : ()=>{},
|
||||||
historySize : ()=>{}
|
historySize : ()=>{},
|
||||||
|
foldCode : ()=>{},
|
||||||
|
unfoldCode : ()=>{},
|
||||||
|
updateEditorTheme : ()=>{},
|
||||||
|
cursorPos : {}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState : function() {
|
getInitialState : function() {
|
||||||
return {
|
return {
|
||||||
renderer : this.props.renderer,
|
renderer : this.props.renderer,
|
||||||
|
themeSelector : false,
|
||||||
snippets : []
|
snippets : []
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -66,6 +74,7 @@ const Snippetbar = createClass({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
mergeCustomizer : function(valueA, valueB, key) {
|
mergeCustomizer : function(valueA, valueB, key) {
|
||||||
if(key == 'snippets') {
|
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(valueB), _.reverse(valueA), 'name')); // Join snippets together, with preference for the current theme over the base theme
|
||||||
@@ -94,6 +103,33 @@ const Snippetbar = createClass({
|
|||||||
this.props.onInject(injectedText);
|
this.props.onInject(injectedText);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
toggleThemeSelector : function(e){
|
||||||
|
if(e.target.tagName != 'SELECT'){
|
||||||
|
this.setState({
|
||||||
|
themeSelector : !this.state.themeSelector
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
changeTheme : function(e){
|
||||||
|
if(e.target.value == this.props.currentEditorTheme) return;
|
||||||
|
this.props.updateEditorTheme(e.target.value);
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
showThemeSelector : false,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
renderThemeSelector : function(){
|
||||||
|
return <div className='themeSelector'>
|
||||||
|
<select value={this.props.currentEditorTheme} onChange={this.changeTheme} >
|
||||||
|
{EditorThemes.map((theme, key)=>{
|
||||||
|
return <option key={key} value={theme}>{theme}</option>;
|
||||||
|
})}
|
||||||
|
</select>
|
||||||
|
</div>;
|
||||||
|
},
|
||||||
|
|
||||||
renderSnippetGroups : function(){
|
renderSnippetGroups : function(){
|
||||||
const snippets = this.state.snippets.filter((snippetGroup)=>snippetGroup.view === this.props.view);
|
const snippets = this.state.snippets.filter((snippetGroup)=>snippetGroup.view === this.props.view);
|
||||||
|
|
||||||
@@ -105,6 +141,7 @@ const Snippetbar = createClass({
|
|||||||
snippets={snippetGroup.snippets}
|
snippets={snippetGroup.snippets}
|
||||||
key={snippetGroup.groupName}
|
key={snippetGroup.groupName}
|
||||||
onSnippetClick={this.handleSnippetClick}
|
onSnippetClick={this.handleSnippetClick}
|
||||||
|
cursorPos={this.props.cursorPos}
|
||||||
/>;
|
/>;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -112,6 +149,22 @@ const Snippetbar = createClass({
|
|||||||
renderEditorButtons : function(){
|
renderEditorButtons : function(){
|
||||||
if(!this.props.showEditButtons) return;
|
if(!this.props.showEditButtons) return;
|
||||||
|
|
||||||
|
let foldButtons;
|
||||||
|
if(this.props.view == 'text'){
|
||||||
|
foldButtons =
|
||||||
|
<>
|
||||||
|
<div className={`editorTool foldAll ${this.props.foldCode ? 'active' : ''}`}
|
||||||
|
onClick={this.props.foldCode} >
|
||||||
|
<i className='fas fa-compress-alt' />
|
||||||
|
</div>
|
||||||
|
<div className={`editorTool unfoldAll ${this.props.unfoldCode ? 'active' : ''}`}
|
||||||
|
onClick={this.props.unfoldCode} >
|
||||||
|
<i className='fas fa-expand-alt' />
|
||||||
|
</div>
|
||||||
|
</>;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
return <div className='editors'>
|
return <div className='editors'>
|
||||||
<div className={`editorTool undo ${this.props.historySize.undo ? 'active' : ''}`}
|
<div className={`editorTool undo ${this.props.historySize.undo ? 'active' : ''}`}
|
||||||
onClick={this.props.undo} >
|
onClick={this.props.undo} >
|
||||||
@@ -121,6 +174,14 @@ const Snippetbar = createClass({
|
|||||||
onClick={this.props.redo} >
|
onClick={this.props.redo} >
|
||||||
<i className='fas fa-redo' />
|
<i className='fas fa-redo' />
|
||||||
</div>
|
</div>
|
||||||
|
<div className='divider'></div>
|
||||||
|
{foldButtons}
|
||||||
|
<div className={`editorTool editorTheme ${this.state.themeSelector ? 'active' : ''}`}
|
||||||
|
onClick={this.toggleThemeSelector} >
|
||||||
|
<i className='fas fa-palette' />
|
||||||
|
{this.state.themeSelector && this.renderThemeSelector()}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className='divider'></div>
|
<div className='divider'></div>
|
||||||
<div className={cx('text', { selected: this.props.view === 'text' })}
|
<div className={cx('text', { selected: this.props.view === 'text' })}
|
||||||
onClick={()=>this.props.onViewChange('text')}>
|
onClick={()=>this.props.onViewChange('text')}>
|
||||||
@@ -165,13 +226,13 @@ const SnippetGroup = createClass({
|
|||||||
},
|
},
|
||||||
handleSnippetClick : function(e, snippet){
|
handleSnippetClick : function(e, snippet){
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
this.props.onSnippetClick(execute(snippet.gen, this.props.brew));
|
this.props.onSnippetClick(execute(snippet.gen, this.props));
|
||||||
},
|
},
|
||||||
renderSnippets : function(snippets){
|
renderSnippets : function(snippets){
|
||||||
return _.map(snippets, (snippet)=>{
|
return _.map(snippets, (snippet)=>{
|
||||||
return <div className='snippet' key={snippet.name} onClick={(e)=>this.handleSnippetClick(e, snippet)}>
|
return <div className='snippet' key={snippet.name} onClick={(e)=>this.handleSnippetClick(e, snippet)}>
|
||||||
<i className={snippet.icon} />
|
<i className={snippet.icon} />
|
||||||
<span className='name'>{snippet.name}</span>
|
<span className='name'title={snippet.name}>{snippet.name}</span>
|
||||||
{snippet.experimental && <span className='beta'>beta</span>}
|
{snippet.experimental && <span className='beta'>beta</span>}
|
||||||
{snippet.subsnippets && <>
|
{snippet.subsnippets && <>
|
||||||
<i className='fas fa-caret-right'></i>
|
<i className='fas fa-caret-right'></i>
|
||||||
@@ -194,5 +255,4 @@ const SnippetGroup = createClass({
|
|||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
},
|
},
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,140 +1,188 @@
|
|||||||
@import (less) './client/icons/customIcons.less';
|
@import (less) './client/icons/customIcons.less';
|
||||||
.snippetBar{
|
@import (less) '././././themes/fonts/5e/fonts.less';
|
||||||
|
|
||||||
|
.snippetBar {
|
||||||
@menuHeight : 25px;
|
@menuHeight : 25px;
|
||||||
position : relative;
|
position : relative;
|
||||||
height : @menuHeight;
|
height : @menuHeight;
|
||||||
background-color : #ddd;
|
color : black;
|
||||||
.editors{
|
background-color : #DDDDDD;
|
||||||
|
|
||||||
|
.editors {
|
||||||
position : absolute;
|
position : absolute;
|
||||||
display : flex;
|
|
||||||
top : 0px;
|
top : 0px;
|
||||||
right : 0px;
|
right : 0px;
|
||||||
height : @menuHeight;
|
display : flex;
|
||||||
width : 125px;
|
|
||||||
justify-content : space-between;
|
justify-content : space-between;
|
||||||
&>div{
|
|
||||||
height : @menuHeight;
|
height : @menuHeight;
|
||||||
|
& > div {
|
||||||
width : @menuHeight;
|
width : @menuHeight;
|
||||||
cursor : pointer;
|
height : @menuHeight;
|
||||||
line-height : @menuHeight;
|
line-height : @menuHeight;
|
||||||
text-align : center;
|
text-align : center;
|
||||||
&:hover,&.selected{
|
cursor : pointer;
|
||||||
background-color : #999;
|
&:hover,&.selected { background-color : #999999; }
|
||||||
}
|
&.text {
|
||||||
&.text{
|
|
||||||
.tooltipLeft('Brew Editor');
|
.tooltipLeft('Brew Editor');
|
||||||
}
|
}
|
||||||
&.style{
|
&.style {
|
||||||
.tooltipLeft('Style Editor');
|
.tooltipLeft('Style Editor');
|
||||||
}
|
}
|
||||||
&.meta{
|
&.meta {
|
||||||
.tooltipLeft('Properties');
|
.tooltipLeft('Properties');
|
||||||
}
|
}
|
||||||
&.undo{
|
&.undo {
|
||||||
.tooltipLeft('Undo');
|
.tooltipLeft('Undo');
|
||||||
font-size : 0.75em;
|
font-size : 0.75em;
|
||||||
color : grey;
|
color : grey;
|
||||||
&.active{
|
&.active { color : inherit; }
|
||||||
color : black;
|
|
||||||
}
|
}
|
||||||
}
|
&.redo {
|
||||||
&.redo{
|
|
||||||
.tooltipLeft('Redo');
|
.tooltipLeft('Redo');
|
||||||
font-size : 0.75em;
|
font-size : 0.75em;
|
||||||
color : grey;
|
color : grey;
|
||||||
&.active{
|
&.active { color : inherit; }
|
||||||
|
}
|
||||||
|
&.foldAll {
|
||||||
|
.tooltipLeft('Fold All');
|
||||||
|
font-size : 0.75em;
|
||||||
|
color : inherit;
|
||||||
|
}
|
||||||
|
&.unfoldAll {
|
||||||
|
.tooltipLeft('Unfold All');
|
||||||
|
font-size : 0.75em;
|
||||||
|
color : inherit;
|
||||||
|
}
|
||||||
|
&.editorTheme {
|
||||||
|
.tooltipLeft('Editor Themes');
|
||||||
|
font-size : 0.75em;
|
||||||
color : black;
|
color : black;
|
||||||
|
&.active {
|
||||||
|
position : relative;
|
||||||
|
background-color : #999999;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&.divider {
|
&.divider {
|
||||||
background: linear-gradient(#000, #000) no-repeat center/1px 100%;
|
width : 5px;
|
||||||
width: 5px;
|
background : linear-gradient(currentColor, currentColor) no-repeat center/1px 100%;
|
||||||
&:hover{
|
&:hover { background-color : inherit; }
|
||||||
background-color: inherit;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.themeSelector {
|
||||||
|
position : absolute;
|
||||||
|
top : 25px;
|
||||||
|
right : 0;
|
||||||
|
z-index : 10;
|
||||||
|
display : flex;
|
||||||
|
align-items : center;
|
||||||
|
justify-content : center;
|
||||||
|
width : 170px;
|
||||||
|
height : inherit;
|
||||||
|
background-color : inherit;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.snippetBarButton{
|
.snippetBarButton {
|
||||||
height : @menuHeight;
|
|
||||||
line-height : @menuHeight;
|
|
||||||
display : inline-block;
|
display : inline-block;
|
||||||
|
height : @menuHeight;
|
||||||
padding : 0px 5px;
|
padding : 0px 5px;
|
||||||
font-weight : 800;
|
|
||||||
font-size : 0.625em;
|
font-size : 0.625em;
|
||||||
|
font-weight : 800;
|
||||||
|
line-height : @menuHeight;
|
||||||
text-transform : uppercase;
|
text-transform : uppercase;
|
||||||
cursor : pointer;
|
cursor : pointer;
|
||||||
&:hover, &.selected{
|
&:hover, &.selected { background-color : #999999; }
|
||||||
background-color : #999;
|
i {
|
||||||
}
|
|
||||||
i{
|
|
||||||
vertical-align : middle;
|
|
||||||
margin-right : 3px;
|
margin-right : 3px;
|
||||||
font-size : 1.4em;
|
font-size : 1.4em;
|
||||||
|
vertical-align : middle;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.toggleMeta{
|
.toggleMeta {
|
||||||
position : absolute;
|
position : absolute;
|
||||||
top : 0px;
|
top : 0px;
|
||||||
right : 0px;
|
right : 0px;
|
||||||
border-left : 1px solid black;
|
border-left : 1px solid black;
|
||||||
.tooltipLeft("Edit Brew Properties");
|
.tooltipLeft('Edit Brew Properties');
|
||||||
}
|
}
|
||||||
.snippetGroup{
|
.snippetGroup {
|
||||||
border-right : 1px solid black;
|
border-right : 1px solid currentColor;
|
||||||
&:hover{
|
&:hover {
|
||||||
&>.dropdown{
|
& > .dropdown { visibility : visible; }
|
||||||
visibility : visible;
|
|
||||||
}
|
}
|
||||||
}
|
.dropdown {
|
||||||
.dropdown{
|
|
||||||
position : absolute;
|
position : absolute;
|
||||||
top : 100%;
|
top : 100%;
|
||||||
visibility : hidden;
|
|
||||||
z-index : 1000;
|
z-index : 1000;
|
||||||
margin-left : -5px;
|
|
||||||
padding : 0px;
|
padding : 0px;
|
||||||
background-color : #ddd;
|
margin-left : -5px;
|
||||||
.snippet{
|
visibility : hidden;
|
||||||
position: relative;
|
background-color : #DDDDDD;
|
||||||
.animate(background-color);
|
.snippet {
|
||||||
|
position : relative;
|
||||||
display : flex;
|
display : flex;
|
||||||
align-items : center;
|
align-items : center;
|
||||||
min-width : max-content;
|
min-width : max-content;
|
||||||
padding : 5px;
|
padding : 5px;
|
||||||
cursor : pointer;
|
|
||||||
font-size : 10px;
|
font-size : 10px;
|
||||||
i{
|
cursor : pointer;
|
||||||
|
.animate(background-color);
|
||||||
|
i {
|
||||||
|
height : 1.2em;
|
||||||
margin-right : 8px;
|
margin-right : 8px;
|
||||||
font-size : 1.2em;
|
font-size : 1.2em;
|
||||||
height : 1.2em;
|
min-width: 25px;
|
||||||
&~i{
|
text-align: center;
|
||||||
margin-right: 0;
|
& ~ i {
|
||||||
margin-left: 5px;
|
margin-right : 0;
|
||||||
}
|
|
||||||
}
|
|
||||||
.name {
|
|
||||||
margin-right : auto;
|
|
||||||
}
|
|
||||||
.beta {
|
|
||||||
color : white;
|
|
||||||
padding : 4px 6px;
|
|
||||||
line-height : 1em;
|
|
||||||
margin-left : 5px;
|
margin-left : 5px;
|
||||||
|
}
|
||||||
|
/* Fonts */
|
||||||
|
&.font {
|
||||||
|
height : auto;
|
||||||
|
&::before {
|
||||||
|
font-size : 1em;
|
||||||
|
content : 'ABC';
|
||||||
|
}
|
||||||
|
|
||||||
|
&.OpenSans {font-family : 'OpenSans';}
|
||||||
|
&.CodeBold {font-family : 'CodeBold';}
|
||||||
|
&.CodeLight {font-family : 'CodeLight';}
|
||||||
|
&.ScalySansRemake {font-family : 'ScalySansRemake';}
|
||||||
|
&.BookInsanityRemake {font-family : 'BookInsanityRemake';}
|
||||||
|
&.MrEavesRemake {font-family : 'MrEavesRemake';}
|
||||||
|
&.SolberaImitationRemake {font-family : 'SolberaImitationRemake';}
|
||||||
|
&.ScalySansSmallCapsRemake {font-family : 'ScalySansSmallCapsRemake';}
|
||||||
|
&.WalterTurncoat {font-family : 'WalterTurncoat';}
|
||||||
|
&.Lato {font-family : 'Lato';}
|
||||||
|
&.Courier {font-family : 'Courier';}
|
||||||
|
&.NodestoCapsCondensed {font-family : 'NodestoCapsCondensed';}
|
||||||
|
&.Overpass {font-family : 'Overpass';}
|
||||||
|
&.Davek {font-family : 'Davek';}
|
||||||
|
&.Iokharic {font-family : 'Iokharic';}
|
||||||
|
&.Rellanic {font-family : 'Rellanic';}
|
||||||
|
&.TimesNewRoman {font-family : 'Times New Roman';}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.name { margin-right : auto; }
|
||||||
|
.beta {
|
||||||
align-self : center;
|
align-self : center;
|
||||||
|
padding : 4px 6px;
|
||||||
|
margin-left : 5px;
|
||||||
|
font-family : monospace;
|
||||||
|
line-height : 1em;
|
||||||
|
color : white;
|
||||||
background : grey;
|
background : grey;
|
||||||
border-radius : 12px;
|
border-radius : 12px;
|
||||||
font-family : monospace;
|
|
||||||
}
|
}
|
||||||
&:hover{
|
&:hover {
|
||||||
background-color : #999;
|
background-color : #999999;
|
||||||
&>.dropdown{
|
& > .dropdown {
|
||||||
visibility : visible;
|
visibility : visible;
|
||||||
&.side {
|
&.side {
|
||||||
left: 100%;
|
top : 0%;
|
||||||
top: 0%;
|
left : 100%;
|
||||||
margin-left:0;
|
margin-left : 0;
|
||||||
box-shadow: -1px 1px 2px 0px #999;
|
box-shadow : -1px 1px 2px 0px #999999;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,8 +9,7 @@ const EditPage = require('./pages/editPage/editPage.jsx');
|
|||||||
const UserPage = require('./pages/userPage/userPage.jsx');
|
const UserPage = require('./pages/userPage/userPage.jsx');
|
||||||
const SharePage = require('./pages/sharePage/sharePage.jsx');
|
const SharePage = require('./pages/sharePage/sharePage.jsx');
|
||||||
const NewPage = require('./pages/newPage/newPage.jsx');
|
const NewPage = require('./pages/newPage/newPage.jsx');
|
||||||
//const ErrorPage = require('./pages/errorPage/errorPage.jsx');
|
const ErrorPage = require('./pages/errorPage/errorPage.jsx');
|
||||||
const PrintPage = require('./pages/printPage/printPage.jsx');
|
|
||||||
const AccountPage = require('./pages/accountPage/accountPage.jsx');
|
const AccountPage = require('./pages/accountPage/accountPage.jsx');
|
||||||
|
|
||||||
const WithRoute = (props)=>{
|
const WithRoute = (props)=>{
|
||||||
@@ -72,12 +71,11 @@ const Homebrew = createClass({
|
|||||||
<Route path='/new/:id' element={<WithRoute el={NewPage} 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' element={<WithRoute el={NewPage}/>} />
|
||||||
<Route path='/user/:username' element={<WithRoute el={UserPage} brews={this.props.brews} />} />
|
<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='/changelog' element={<WithRoute el={SharePage} brew={this.props.brew} />} />
|
||||||
<Route path='/faq' element={<WithRoute el={SharePage} brew={this.props.brew} />} />
|
<Route path='/faq' element={<WithRoute el={SharePage} brew={this.props.brew} />} />
|
||||||
<Route path='/account' element={<WithRoute el={AccountPage} brew={this.props.brew} uiItems={this.props.brew.uiItems} />} />
|
<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='/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>
|
</Routes>
|
||||||
@@ -88,14 +86,3 @@ const Homebrew = createClass({
|
|||||||
});
|
});
|
||||||
|
|
||||||
module.exports = Homebrew;
|
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} />;
|
|
||||||
// },
|
|
||||||
|
|||||||
@@ -15,6 +15,23 @@
|
|||||||
}
|
}
|
||||||
&.listPage .content {
|
&.listPage .content {
|
||||||
overflow-y : scroll;
|
overflow-y : scroll;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 20px;
|
||||||
|
&:horizontal{
|
||||||
|
height: 20px;
|
||||||
|
width:auto;
|
||||||
|
}
|
||||||
|
&-thumb {
|
||||||
|
background: linear-gradient(90deg, #d3c1af 15px, #00000000 15px);
|
||||||
|
&:horizontal{
|
||||||
|
background: linear-gradient(0deg, #d3c1af 15px, #00000000 15px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&-corner {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -24,6 +24,7 @@ const ErrorNavItem = createClass({
|
|||||||
const error = this.props.error;
|
const error = this.props.error;
|
||||||
const response = error.response;
|
const response = error.response;
|
||||||
const status = response.status;
|
const status = response.status;
|
||||||
|
const HBErrorCode = response.body?.HBErrorCode;
|
||||||
const message = response.body?.message;
|
const message = response.body?.message;
|
||||||
let errMsg = '';
|
let errMsg = '';
|
||||||
try {
|
try {
|
||||||
@@ -40,7 +41,9 @@ const ErrorNavItem = createClass({
|
|||||||
{message ?? 'Conflict: please refresh to get latest changes'}
|
{message ?? 'Conflict: please refresh to get latest changes'}
|
||||||
</div>
|
</div>
|
||||||
</Nav.item>;
|
</Nav.item>;
|
||||||
} else if(status === 412) {
|
}
|
||||||
|
|
||||||
|
if(status === 412) {
|
||||||
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||||
Oops!
|
Oops!
|
||||||
<div className='errorContainer' onClick={clearError}>
|
<div className='errorContainer' onClick={clearError}>
|
||||||
@@ -49,6 +52,36 @@ const ErrorNavItem = createClass({
|
|||||||
</Nav.item>;
|
</Nav.item>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(HBErrorCode === '04') {
|
||||||
|
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||||
|
Oops!
|
||||||
|
<div className='errorContainer' onClick={clearError}>
|
||||||
|
You are no longer signed in as an author of
|
||||||
|
this brew! Were you signed out from a different
|
||||||
|
window? Visit our log in page, then try again!
|
||||||
|
<br></br>
|
||||||
|
<a target='_blank' rel='noopener noreferrer'
|
||||||
|
href={`https://www.naturalcrit.com/login?redirect=${window.location.href}`}>
|
||||||
|
<div className='confirm'>
|
||||||
|
Sign In
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<div className='deny'>
|
||||||
|
Not Now
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Nav.item>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(response.body?.errors?.[0].reason == 'storageQuotaExceeded') {
|
||||||
|
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||||
|
Oops!
|
||||||
|
<div className='errorContainer' onClick={clearError}>
|
||||||
|
Can't save because your Google Drive seems to be full!
|
||||||
|
</div>
|
||||||
|
</Nav.item>;
|
||||||
|
}
|
||||||
|
|
||||||
if(response.req.url.match(/^\/api.*Google.*$/m)){
|
if(response.req.url.match(/^\/api.*Google.*$/m)){
|
||||||
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||||
Oops!
|
Oops!
|
||||||
@@ -57,6 +90,7 @@ const ErrorNavItem = createClass({
|
|||||||
expired! Visit our log in page to sign out
|
expired! Visit our log in page to sign out
|
||||||
and sign back in with Google,
|
and sign back in with Google,
|
||||||
then try saving again!
|
then try saving again!
|
||||||
|
<br></br>
|
||||||
<a target='_blank' rel='noopener noreferrer'
|
<a target='_blank' rel='noopener noreferrer'
|
||||||
href={`https://www.naturalcrit.com/login?redirect=${window.location.href}`}>
|
href={`https://www.naturalcrit.com/login?redirect=${window.location.href}`}>
|
||||||
<div className='confirm'>
|
<div className='confirm'>
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
const React = require('react');
|
const React = require('react');
|
||||||
const createClass = require('create-react-class');
|
|
||||||
const _ = require('lodash');
|
|
||||||
const dedent = require('dedent-tabs').default;
|
const dedent = require('dedent-tabs').default;
|
||||||
|
|
||||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
const React = require('react');
|
const React = require('react');
|
||||||
const createClass = require('create-react-class');
|
const createClass = require('create-react-class');
|
||||||
const _ = require('lodash');
|
|
||||||
const Moment = require('moment');
|
const Moment = require('moment');
|
||||||
|
|
||||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||||
|
|||||||
@@ -1,126 +1,286 @@
|
|||||||
@import "naturalcrit/styles/colors.less";
|
@import 'naturalcrit/styles/colors.less';
|
||||||
|
|
||||||
@navbarHeight : 28px;
|
@navbarHeight : 28px;
|
||||||
|
|
||||||
@keyframes pinkColoring {
|
@keyframes pinkColoring {
|
||||||
0% {color : pink;}
|
0% { color : pink; }
|
||||||
50% {color : pink;}
|
50% { color : pink; }
|
||||||
75% {color : red;}
|
75% { color : red; }
|
||||||
100% {color : pink;}
|
100% { color : pink; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes glideDropDown {
|
||||||
|
0% {
|
||||||
|
background-color : #333333;
|
||||||
|
opacity : 0;
|
||||||
|
transform : translate(0px, -100%);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-color : #333333;
|
||||||
|
opacity : 1;
|
||||||
|
transform : translate(0px, 0px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.homebrew nav {
|
.homebrew nav {
|
||||||
.homebrewLogo {
|
background-color : #333333;
|
||||||
|
.navContent {
|
||||||
|
position : relative;
|
||||||
|
z-index : 2;
|
||||||
|
display : flex;
|
||||||
|
justify-content : space-between;
|
||||||
|
}
|
||||||
|
.navSection {
|
||||||
|
display : flex;
|
||||||
|
align-items : center;
|
||||||
|
&:last-child .navItem { border-left : 1px solid #666666; }
|
||||||
|
}
|
||||||
|
// "NaturalCrit" logo
|
||||||
|
.navLogo {
|
||||||
|
display : block;
|
||||||
|
margin-top : 0px;
|
||||||
|
margin-right : 8px;
|
||||||
|
margin-left : 8px;
|
||||||
|
color : white;
|
||||||
|
text-decoration : none;
|
||||||
|
&:hover {
|
||||||
|
.name { color : @orange; }
|
||||||
|
svg { fill : @orange; }
|
||||||
|
}
|
||||||
|
svg {
|
||||||
|
height : 13px;
|
||||||
|
margin-right : 0.2em;
|
||||||
|
cursor : pointer;
|
||||||
|
fill : white;
|
||||||
|
}
|
||||||
|
span.name {
|
||||||
|
font-family : 'CodeLight';
|
||||||
|
font-size : 15px;
|
||||||
|
span.crit { font-family : 'CodeBold'; }
|
||||||
|
small {
|
||||||
|
font-family : 'Open Sans';
|
||||||
|
font-size : 0.3em;
|
||||||
|
font-weight : 800;
|
||||||
|
text-transform : uppercase;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navItem {
|
||||||
|
#backgroundColorsHover;
|
||||||
|
.animate(background-color);
|
||||||
|
padding : 8px 12px;
|
||||||
|
font-size : 10px;
|
||||||
|
font-weight : 800;
|
||||||
|
line-height : 13px;
|
||||||
|
color : white;
|
||||||
|
text-decoration : none;
|
||||||
|
text-transform : uppercase;
|
||||||
|
cursor : pointer;
|
||||||
|
background-color : #333333;
|
||||||
|
i {
|
||||||
|
float : right;
|
||||||
|
margin-left : 5px;
|
||||||
|
font-size : 13px;
|
||||||
|
}
|
||||||
|
&.patreon {
|
||||||
|
border-right : 1px solid #666666;
|
||||||
|
border-left : 1px solid #666666;
|
||||||
|
&:hover i { color : red; }
|
||||||
|
i {
|
||||||
|
color : pink;
|
||||||
.animate(color);
|
.animate(color);
|
||||||
font-family : CodeBold;
|
animation-name : pinkColoring;
|
||||||
|
animation-duration : 2s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.editTitle { // this is not needed at all currently - you used to be able to edit the title via the navbar.
|
||||||
|
padding : 2px 12px;
|
||||||
|
input {
|
||||||
|
width : 250px;
|
||||||
|
padding : 2px;
|
||||||
|
margin : 0;
|
||||||
|
font-family : 'Open Sans', sans-serif;
|
||||||
|
font-size : 12px;
|
||||||
|
font-weight : 800;
|
||||||
|
color : white;
|
||||||
|
text-align : center;
|
||||||
|
background-color : transparent;
|
||||||
|
border : 1px solid @blue;
|
||||||
|
outline : none;
|
||||||
|
}
|
||||||
|
.charCount {
|
||||||
|
display : inline-block;
|
||||||
|
margin-left : 8px;
|
||||||
|
color : #666666;
|
||||||
|
text-align : right;
|
||||||
|
vertical-align : bottom;
|
||||||
|
&.max { color : @red; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.brewTitle {
|
||||||
|
flex-grow : 1;
|
||||||
|
font-size : 12px;
|
||||||
|
font-weight : 800;
|
||||||
|
color : white;
|
||||||
|
text-align : center;
|
||||||
|
text-transform : initial;
|
||||||
|
background-color : transparent;
|
||||||
|
}
|
||||||
|
// "The Homebrewery" logo
|
||||||
|
&.homebrewLogo {
|
||||||
|
.animate(color);
|
||||||
|
font-family : 'CodeBold';
|
||||||
font-size : 12px;
|
font-size : 12px;
|
||||||
color : white;
|
color : white;
|
||||||
div {
|
div {
|
||||||
margin-top : 2px;
|
margin-top : 2px;
|
||||||
margin-bottom : -2px;
|
margin-bottom : -2px;
|
||||||
}
|
}
|
||||||
&:hover {
|
&:hover { color : @blue; }
|
||||||
color : @blue;
|
|
||||||
}
|
}
|
||||||
}
|
&.metadata {
|
||||||
.editTitle.navItem {
|
|
||||||
padding : 2px 12px;
|
|
||||||
input {
|
|
||||||
font-family : "Open Sans", sans-serif;
|
|
||||||
font-size : 12px;
|
|
||||||
font-weight : 800;
|
|
||||||
width : 250px;
|
|
||||||
margin : 0;
|
|
||||||
padding : 2px;
|
|
||||||
text-align : center;
|
|
||||||
color : white;
|
|
||||||
border : 1px solid @blue;
|
|
||||||
outline : none;
|
|
||||||
background-color : transparent;
|
|
||||||
}
|
|
||||||
.charCount {
|
|
||||||
display : inline-block;
|
|
||||||
margin-left : 8px;
|
|
||||||
text-align : right;
|
|
||||||
vertical-align : bottom;
|
|
||||||
color : #666;
|
|
||||||
&.max {
|
|
||||||
color : @red;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.brewTitle.navItem {
|
|
||||||
font-size : 12px;
|
|
||||||
font-weight : 800;
|
|
||||||
height : 100%;
|
|
||||||
text-align : center;
|
|
||||||
text-transform : initial;
|
|
||||||
color : white;
|
|
||||||
background-color : transparent;
|
|
||||||
flex-grow : 1;
|
|
||||||
}
|
|
||||||
.save-menu {
|
|
||||||
.dropdown {
|
|
||||||
z-index : 1000;
|
|
||||||
}
|
|
||||||
.navItem i.fa-power-off {
|
|
||||||
color : red;
|
|
||||||
&.active {
|
|
||||||
color : rgb(0, 182, 52);
|
|
||||||
filter : drop-shadow(0 0 2px rgba(0, 182, 52, 0.765));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.patreon.navItem {
|
|
||||||
border-right : 1px solid #666;
|
|
||||||
border-left : 1px solid #666;
|
|
||||||
&:hover i {
|
|
||||||
color : red;
|
|
||||||
}
|
|
||||||
i {
|
|
||||||
.animate(color);
|
|
||||||
animation-name : pinkColoring;
|
|
||||||
animation-duration : 2s;
|
|
||||||
color : pink;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.recent.navItem {
|
|
||||||
position : relative;
|
position : relative;
|
||||||
|
display : flex;
|
||||||
|
flex-grow : 1;
|
||||||
|
align-items : center;
|
||||||
|
height : 100%;
|
||||||
|
padding : 0;
|
||||||
|
i { margin-right : 10px;}
|
||||||
|
.window {
|
||||||
|
position : absolute;
|
||||||
|
bottom : 0;
|
||||||
|
left : 50%;
|
||||||
|
z-index : -1;
|
||||||
|
display : flex;
|
||||||
|
flex-flow : row wrap;
|
||||||
|
align-content : baseline;
|
||||||
|
justify-content : flex-start;
|
||||||
|
width : 440px;
|
||||||
|
max-height : ~'calc(100vh - 28px)';
|
||||||
|
padding : 0 10px 5px;
|
||||||
|
margin : 0 auto;
|
||||||
|
background-color : #333333;
|
||||||
|
border : 3px solid #444444;
|
||||||
|
border-top : unset;
|
||||||
|
border-radius : 0 0 5px 5px;
|
||||||
|
box-shadow : inset 0 7px 9px -7px #111111;
|
||||||
|
transition : transform 0.4s, opacity 0.4s;
|
||||||
|
&.active {
|
||||||
|
opacity : 1;
|
||||||
|
transform : translateX(-50%) translateY(100%);
|
||||||
|
}
|
||||||
|
&.inactive {
|
||||||
|
opacity : 0;
|
||||||
|
transform : translateX(-50%) translateY(0%);
|
||||||
|
}
|
||||||
|
.row {
|
||||||
|
display : flex;
|
||||||
|
flex-flow : row wrap;
|
||||||
|
width : 100%;
|
||||||
|
h4 {
|
||||||
|
box-sizing : border-box;
|
||||||
|
display : block;
|
||||||
|
flex-basis : 20%;
|
||||||
|
flex-grow : 1;
|
||||||
|
min-width : 76px;
|
||||||
|
padding : 5px 0;
|
||||||
|
color : #BBBBBB;
|
||||||
|
text-align : center;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
flex-basis : 80%;
|
||||||
|
flex-grow : 1;
|
||||||
|
padding : 5px 0;
|
||||||
|
font-family : 'Open Sans', sans-serif;
|
||||||
|
font-size : 10px;
|
||||||
|
font-weight : normal;
|
||||||
|
text-transform : initial;
|
||||||
|
.tag {
|
||||||
|
display : inline-block;
|
||||||
|
padding : 2px;
|
||||||
|
margin : 2px 2px;
|
||||||
|
background-color : #444444;
|
||||||
|
border : 2px solid grey;
|
||||||
|
border-radius : 5px;
|
||||||
|
}
|
||||||
|
a.userPageLink {
|
||||||
|
color : white;
|
||||||
|
text-decoration : none;
|
||||||
|
&:hover { text-decoration : underline; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&:nth-of-type(even) { background-color : #555555; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.warning {
|
||||||
|
position : relative;
|
||||||
|
color : white;
|
||||||
|
background-color : @orange;
|
||||||
|
&:hover > .dropdown { visibility : visible; }
|
||||||
.dropdown {
|
.dropdown {
|
||||||
position : absolute;
|
position : absolute;
|
||||||
z-index : 10000;
|
|
||||||
top : 28px;
|
top : 28px;
|
||||||
left : 0;
|
left : 0;
|
||||||
overflow : hidden auto;
|
z-index : 10000;
|
||||||
width : 100%;
|
|
||||||
max-height : ~"calc(100vh - 28px)";
|
|
||||||
scrollbar-color : #666 #333;
|
|
||||||
scrollbar-width : thin;
|
|
||||||
h4 {
|
|
||||||
font-size : 0.8em;
|
|
||||||
display : block;
|
|
||||||
box-sizing : border-box;
|
box-sizing : border-box;
|
||||||
padding : 5px 0;
|
display : block;
|
||||||
|
width : 100%;
|
||||||
|
padding : 13px 5px;
|
||||||
text-align : center;
|
text-align : center;
|
||||||
color : #BBB;
|
visibility : hidden;
|
||||||
border-top : 1px solid #888;
|
background-color : #333333;
|
||||||
background-color : #333;
|
|
||||||
&:nth-of-type(1) {
|
|
||||||
background-color : darken(@teal, 20%);
|
|
||||||
}
|
|
||||||
&:nth-of-type(2) {
|
|
||||||
background-color : darken(@purple, 30%);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.item {
|
&.account {
|
||||||
|
min-width : 100px;
|
||||||
|
&.username { text-transform : none;}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navDropdownContainer {
|
||||||
|
position : relative;
|
||||||
|
.navDropdown {
|
||||||
|
position: absolute;
|
||||||
|
top: 28px;
|
||||||
|
right: 0px;
|
||||||
|
z-index: 10000;
|
||||||
|
width: max-content;
|
||||||
|
min-width:100%;
|
||||||
|
max-height: calc(100vh - 28px);
|
||||||
|
overflow: hidden auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-end;
|
||||||
|
.navItem {
|
||||||
|
position : relative;
|
||||||
|
display : flex;
|
||||||
|
justify-content : space-between;
|
||||||
|
align-items : center;
|
||||||
|
width : 100%;
|
||||||
|
border : 1px solid #888888;
|
||||||
|
border-bottom : 0;
|
||||||
|
animation-name : glideDropDown;
|
||||||
|
animation-duration : 0.4s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.recent {
|
||||||
|
position : relative;
|
||||||
|
.navDropdown .navItem {
|
||||||
#backgroundColorsHover;
|
#backgroundColorsHover;
|
||||||
.animate(background-color);
|
.animate(background-color);
|
||||||
position : relative;
|
position : relative;
|
||||||
display : block;
|
|
||||||
overflow : clip;
|
|
||||||
box-sizing : border-box;
|
box-sizing : border-box;
|
||||||
|
display : block;
|
||||||
|
max-width : 15em;
|
||||||
|
max-height : ~'calc(100vh - 28px)';
|
||||||
padding : 8px 5px 13px;
|
padding : 8px 5px 13px;
|
||||||
text-decoration : none;
|
overflow : hidden auto;
|
||||||
color : white;
|
color : white;
|
||||||
border-top : 1px solid #888;
|
text-decoration : none;
|
||||||
background-color : #333;
|
background-color : #333333;
|
||||||
|
border-top : 1px solid #888888;
|
||||||
|
scrollbar-color : #666666 #333333;
|
||||||
|
scrollbar-width : thin;
|
||||||
.clear {
|
.clear {
|
||||||
position : absolute;
|
position : absolute;
|
||||||
top : 50%;
|
top : 50%;
|
||||||
@@ -128,18 +288,16 @@
|
|||||||
display : none;
|
display : none;
|
||||||
width : 20px;
|
width : 20px;
|
||||||
height : 100%;
|
height : 100%;
|
||||||
transform : translateY(-50%);
|
background-color : #333333;
|
||||||
opacity : 70%;
|
|
||||||
border-radius : 3px;
|
border-radius : 3px;
|
||||||
background-color : #333;
|
opacity : 70%;
|
||||||
&:hover {
|
transform : translateY(-50%);
|
||||||
opacity : 100%;
|
&:hover { opacity : 100%; }
|
||||||
}
|
|
||||||
i {
|
i {
|
||||||
font-size : 10px;
|
|
||||||
width : 100%;
|
width : 100%;
|
||||||
height : 100%;
|
height : 100%;
|
||||||
margin : 0;
|
margin : 0;
|
||||||
|
font-size : 10px;
|
||||||
text-align : center;
|
text-align : center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -152,127 +310,42 @@
|
|||||||
}
|
}
|
||||||
.title {
|
.title {
|
||||||
display : inline-block;
|
display : inline-block;
|
||||||
overflow : hidden;
|
|
||||||
width : 100%;
|
width : 100%;
|
||||||
white-space : nowrap;
|
overflow : hidden auto;
|
||||||
text-overflow : ellipsis;
|
text-overflow : ellipsis;
|
||||||
|
white-space : nowrap;
|
||||||
}
|
}
|
||||||
.time {
|
.time {
|
||||||
font-size : 0.7em;
|
|
||||||
position : absolute;
|
position : absolute;
|
||||||
right : 2px;
|
right : 2px;
|
||||||
bottom : 2px;
|
bottom : 2px;
|
||||||
color : #888;
|
font-size : 0.7em;
|
||||||
|
color : #888888;
|
||||||
}
|
}
|
||||||
}
|
&.header {
|
||||||
}
|
|
||||||
}
|
|
||||||
.metadata.navItem {
|
|
||||||
position : relative;
|
|
||||||
display : flex;
|
|
||||||
align-items : center;
|
|
||||||
height : 100%;
|
|
||||||
padding : 0;
|
|
||||||
flex-grow : 1;
|
|
||||||
i {
|
|
||||||
margin-right : 10px;
|
|
||||||
}
|
|
||||||
.window {
|
|
||||||
position : absolute;
|
|
||||||
z-index : -1;
|
|
||||||
bottom : 0;
|
|
||||||
left : 50%;
|
|
||||||
display : flex;
|
|
||||||
justify-content : flex-start;
|
|
||||||
width : 440px;
|
|
||||||
max-height : ~"calc(100vh - 28px)";
|
|
||||||
margin : 0 auto;
|
|
||||||
padding : 0 10px 5px;
|
|
||||||
transition : transform 0.4s, opacity 0.4s;
|
|
||||||
border : 3px solid #444;
|
|
||||||
border-top : unset;
|
|
||||||
border-radius : 0 0 5px 5px;
|
|
||||||
background-color : #333;
|
|
||||||
box-shadow : inset 0 7px 9px -7px #111;
|
|
||||||
flex-flow : row wrap;
|
|
||||||
align-content : baseline;
|
|
||||||
&.active {
|
|
||||||
transform : translateX(-50%) translateY(100%);
|
|
||||||
opacity : 1;
|
|
||||||
}
|
|
||||||
&.inactive {
|
|
||||||
transform : translateX(-50%) translateY(0%);
|
|
||||||
opacity : 0;
|
|
||||||
}
|
|
||||||
.row {
|
|
||||||
display : flex;
|
|
||||||
width : 100%;
|
|
||||||
flex-flow : row wrap;
|
|
||||||
h4 {
|
|
||||||
display : block;
|
|
||||||
box-sizing : border-box;
|
box-sizing : border-box;
|
||||||
min-width : 76px;
|
|
||||||
padding : 5px 0;
|
|
||||||
text-align : center;
|
|
||||||
color : #BBB;
|
|
||||||
flex-basis : 20%;
|
|
||||||
flex-grow : 1;
|
|
||||||
}
|
|
||||||
p {
|
|
||||||
font-family : "Open Sans", sans-serif;
|
|
||||||
font-size : 10px;
|
|
||||||
font-weight : normal;
|
|
||||||
padding : 5px 0;
|
|
||||||
text-transform : initial;
|
|
||||||
flex-basis : 80%;
|
|
||||||
flex-grow : 1;
|
|
||||||
.tag {
|
|
||||||
display : inline-block;
|
|
||||||
margin : 2px 2px;
|
|
||||||
padding : 2px;
|
|
||||||
border : 2px solid grey;
|
|
||||||
border-radius : 5px;
|
|
||||||
background-color : #444;
|
|
||||||
}
|
|
||||||
a.userPageLink {
|
|
||||||
text-decoration : none;
|
|
||||||
color : white;
|
|
||||||
&:hover {
|
|
||||||
text-decoration : underline;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&:nth-of-type(even) {
|
|
||||||
background-color : #555;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.warning.navItem {
|
|
||||||
position : relative;
|
|
||||||
color : white;
|
|
||||||
background-color : @orange;
|
|
||||||
&:hover > .dropdown {
|
|
||||||
visibility : visible;
|
|
||||||
}
|
|
||||||
.dropdown {
|
|
||||||
position : absolute;
|
|
||||||
z-index : 10000;
|
|
||||||
top : 28px;
|
|
||||||
left : 0;
|
|
||||||
display : block;
|
display : block;
|
||||||
visibility : hidden;
|
padding : 5px 0;
|
||||||
box-sizing : border-box;
|
color : #BBBBBB;
|
||||||
width : 100%;
|
|
||||||
padding : 13px 5px;
|
|
||||||
text-align : center;
|
text-align : center;
|
||||||
background-color : #333;
|
background-color : #333333;
|
||||||
|
border-top : 1px solid #888888;
|
||||||
|
&:nth-of-type(1) { background-color : darken(@teal, 20%); }
|
||||||
|
&:nth-of-type(2) { background-color : darken(@purple, 30%); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.account.navItem {
|
|
||||||
min-width : 100px;
|
|
||||||
}
|
}
|
||||||
.account.username.navItem {
|
}
|
||||||
text-transform : none;
|
}
|
||||||
|
|
||||||
|
// this should likely be refactored into .navDropdownContainer
|
||||||
|
.save-menu {
|
||||||
|
.dropdown { z-index : 1000; }
|
||||||
|
.navItem i.fa-power-off {
|
||||||
|
color : red;
|
||||||
|
&.active {
|
||||||
|
color : rgb(0, 182, 52);
|
||||||
|
filter : drop-shadow(0 0 2px rgba(0, 182, 52, 0.765));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,64 @@
|
|||||||
const React = require('react');
|
const React = require('react');
|
||||||
|
const _ = require('lodash');
|
||||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||||
|
const { splitTextStyleAndMetadata } = require('../../../shared/helpers.js'); // Importing the function from helpers.js
|
||||||
|
|
||||||
module.exports = function(props){
|
const BREWKEY = 'homebrewery-new';
|
||||||
return <Nav.item
|
const STYLEKEY = 'homebrewery-new-style';
|
||||||
href='/new'
|
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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Nav.dropdown>
|
||||||
|
<Nav.item
|
||||||
|
className='new'
|
||||||
color='purple'
|
color='purple'
|
||||||
icon='fas fa-plus-square'>
|
icon='fa-solid fa-plus-square'>
|
||||||
new
|
new
|
||||||
</Nav.item>;
|
</Nav.item>
|
||||||
|
<Nav.item
|
||||||
|
className='fromBlank'
|
||||||
|
href='/new'
|
||||||
|
newTab={true}
|
||||||
|
color='purple'
|
||||||
|
icon='fa-solid fa-file'>
|
||||||
|
from blank
|
||||||
|
</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' }} />
|
||||||
|
from file
|
||||||
|
</Nav.item>
|
||||||
|
</Nav.dropdown>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
module.exports = NewBrew;
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
const React = require('react');
|
const React = require('react');
|
||||||
const createClass = require('create-react-class');
|
|
||||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||||
|
const { printCurrentBrew } = require('../../../shared/helpers.js');
|
||||||
|
|
||||||
module.exports = function(props){
|
module.exports = function(){
|
||||||
return <Nav.item newTab={true} href={`/print/${props.shareId}?dialog=true`} color='purple' icon='far fa-file-pdf'>
|
return <Nav.item onClick={printCurrentBrew} color='purple' icon='far fa-file-pdf'>
|
||||||
get PDF
|
get PDF
|
||||||
</Nav.item>;
|
</Nav.item>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -121,6 +121,7 @@ const RecentItems = createClass({
|
|||||||
|
|
||||||
removeItem : function(url, evt){
|
removeItem : function(url, evt){
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
|
evt.stopPropagation();
|
||||||
|
|
||||||
let edited = JSON.parse(localStorage.getItem(EDIT_KEY) || '[]');
|
let edited = JSON.parse(localStorage.getItem(EDIT_KEY) || '[]');
|
||||||
let viewed = JSON.parse(localStorage.getItem(VIEW_KEY) || '[]');
|
let viewed = JSON.parse(localStorage.getItem(VIEW_KEY) || '[]');
|
||||||
@@ -139,11 +140,11 @@ const RecentItems = createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
renderDropdown : function(){
|
renderDropdown : function(){
|
||||||
if(!this.state.showDropdown) return null;
|
// if(!this.state.showDropdown) return null;
|
||||||
|
|
||||||
const makeItems = (brews)=>{
|
const makeItems = (brews)=>{
|
||||||
return _.map(brews, (brew, i)=>{
|
return _.map(brews, (brew, i)=>{
|
||||||
return <a href={brew.url} className='item' key={`${brew.id}-${i}`} target='_blank' rel='noopener noreferrer' title={brew.title || '[ no title ]'}>
|
return <a className='navItem' href={brew.url} key={`${brew.id}-${i}`} target='_blank' rel='noopener noreferrer' title={brew.title || '[ no title ]'}>
|
||||||
<span className='title'>{brew.title || '[ no title ]'}</span>
|
<span className='title'>{brew.title || '[ no title ]'}</span>
|
||||||
<span className='time'>{Moment(brew.ts).fromNow()}</span>
|
<span className='time'>{Moment(brew.ts).fromNow()}</span>
|
||||||
<div className='clear' title='Remove from Recents' onClick={(e)=>{this.removeItem(`${brew.url}`, e);}}><i className='fas fa-times'></i></div>
|
<div className='clear' title='Remove from Recents' onClick={(e)=>{this.removeItem(`${brew.url}`, e);}}><i className='fas fa-times'></i></div>
|
||||||
@@ -151,25 +152,25 @@ const RecentItems = createClass({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return <div className='dropdown'>
|
return <>
|
||||||
{(this.props.showEdit && this.props.showView) ?
|
{(this.props.showEdit && this.props.showView) ?
|
||||||
<h4>edited</h4> : null }
|
<Nav.item className='header'>edited</Nav.item> : null }
|
||||||
{this.props.showEdit ?
|
{this.props.showEdit ?
|
||||||
makeItems(this.state.edit) : null }
|
makeItems(this.state.edit) : null }
|
||||||
{(this.props.showEdit && this.props.showView) ?
|
{(this.props.showEdit && this.props.showView) ?
|
||||||
<h4>viewed</h4> : null }
|
<Nav.item className='header'>viewed</Nav.item> : null }
|
||||||
{this.props.showView ?
|
{this.props.showView ?
|
||||||
makeItems(this.state.view) : null }
|
makeItems(this.state.view) : null }
|
||||||
</div>;
|
</>;
|
||||||
},
|
},
|
||||||
|
|
||||||
render : function(){
|
render : function(){
|
||||||
return <Nav.item icon='fas fa-history' color='grey' className='recent'
|
return <Nav.dropdown className='recent'>
|
||||||
onMouseEnter={()=>this.handleDropdown(true)}
|
<Nav.item icon='fas fa-history' color='grey' >
|
||||||
onMouseLeave={()=>this.handleDropdown(false)}>
|
|
||||||
{this.props.text}
|
{this.props.text}
|
||||||
|
</Nav.item>
|
||||||
{this.renderDropdown()}
|
{this.renderDropdown()}
|
||||||
</Nav.item>;
|
</Nav.dropdown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
const React = require('react');
|
const React = require('react');
|
||||||
const createClass = require('create-react-class');
|
const createClass = require('create-react-class');
|
||||||
const cx = require('classnames');
|
|
||||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||||
|
|
||||||
const MAX_URL_SIZE = 2083;
|
|
||||||
const MAIN_URL = 'https://www.reddit.com/r/UnearthedArcana/submit?selftext=true';
|
const MAIN_URL = 'https://www.reddit.com/r/UnearthedArcana/submit?selftext=true';
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,74 +1,82 @@
|
|||||||
const React = require('react');
|
const React = require('react');
|
||||||
const createClass = require('create-react-class');
|
|
||||||
const _ = require('lodash');
|
|
||||||
const cx = require('classnames');
|
|
||||||
const moment = require('moment');
|
const moment = require('moment');
|
||||||
|
|
||||||
const UIPage = require('../basePages/uiPage/uiPage.jsx');
|
const UIPage = require('../basePages/uiPage/uiPage.jsx');
|
||||||
|
|
||||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
|
||||||
const Navbar = require('../../navbar/navbar.jsx');
|
|
||||||
|
|
||||||
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
|
||||||
const Account = require('../../navbar/account.navitem.jsx');
|
|
||||||
const NewBrew = require('../../navbar/newbrew.navitem.jsx');
|
|
||||||
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
|
||||||
|
|
||||||
const NaturalCritIcon = require('naturalcrit/svg/naturalcrit.svg.jsx');
|
const NaturalCritIcon = require('naturalcrit/svg/naturalcrit.svg.jsx');
|
||||||
|
|
||||||
const AccountPage = createClass({
|
let SAVEKEY = '';
|
||||||
displayName : 'AccountPage',
|
|
||||||
getDefaultProps : function() {
|
|
||||||
return {
|
|
||||||
brew : {},
|
|
||||||
uiItems : {}
|
|
||||||
};
|
|
||||||
},
|
|
||||||
getInitialState : function() {
|
|
||||||
return {
|
|
||||||
uiItems : this.props.uiItems
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
renderNavItems : function() {
|
const AccountPage = (props)=>{
|
||||||
return <Navbar>
|
// destructure props and set state for save location
|
||||||
<Nav.section>
|
const { accountDetails, brew } = props;
|
||||||
<NewBrew />
|
const [saveLocation, setSaveLocation] = React.useState('');
|
||||||
<HelpNavItem />
|
|
||||||
<RecentNavItem />
|
|
||||||
<Account />
|
|
||||||
</Nav.section>
|
|
||||||
</Navbar>;
|
|
||||||
},
|
|
||||||
|
|
||||||
renderUiItems : function() {
|
// initialize save location from local storage based on user id
|
||||||
return <>
|
React.useEffect(()=>{
|
||||||
|
if(!saveLocation && accountDetails.username) {
|
||||||
|
SAVEKEY = `HOMEBREWERY-DEFAULT-SAVE-LOCATION-${accountDetails.username}`;
|
||||||
|
// if no SAVEKEY in local storage, default save location to Google Drive if user has Google account.
|
||||||
|
let saveLocation = window.localStorage.getItem(SAVEKEY);
|
||||||
|
saveLocation = saveLocation ?? (accountDetails.googleId ? 'GOOGLE-DRIVE' : 'HOMEBREWERY');
|
||||||
|
setActiveSaveLocation(saveLocation);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const setActiveSaveLocation = (newSelection)=>{
|
||||||
|
if(saveLocation === newSelection) return;
|
||||||
|
window.localStorage.setItem(SAVEKEY, newSelection);
|
||||||
|
setSaveLocation(newSelection);
|
||||||
|
};
|
||||||
|
|
||||||
|
// todo: should this be a set of radio buttons (well styled) since it's either/or choice?
|
||||||
|
const renderSaveLocationButton = (name, key, shouldRender = true)=>{
|
||||||
|
if(!shouldRender) return null;
|
||||||
|
return (
|
||||||
|
<button className={saveLocation === key ? 'active' : ''} onClick={()=>{setActiveSaveLocation(key);}}>
|
||||||
|
{name}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// render the entirety of the account page content
|
||||||
|
const renderAccountPage = ()=>{
|
||||||
|
return (
|
||||||
|
<>
|
||||||
<div className='dataGroup'>
|
<div className='dataGroup'>
|
||||||
<h1>Account Information <i className='fas fa-user'></i></h1>
|
<h1>Account Information <i className='fas fa-user'></i></h1>
|
||||||
<p><strong>Username: </strong> {this.props.uiItems.username || 'No user currently logged in'}</p>
|
<p><strong>Username: </strong>{accountDetails.username || 'No user currently logged in'}</p>
|
||||||
<p><strong>Last Login: </strong> {moment(this.props.uiItems.issued).format('dddd, MMMM Do YYYY, h:mm:ss a ZZ') || '-'}</p>
|
<p><strong>Last Login: </strong>{moment(accountDetails.issued).format('dddd, MMMM Do YYYY, h:mm:ss a ZZ') || '-'}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className='dataGroup'>
|
<div className='dataGroup'>
|
||||||
<h3>Homebrewery Information <NaturalCritIcon /></h3>
|
<h3>Homebrewery Information <NaturalCritIcon /></h3>
|
||||||
<p><strong>Brews on Homebrewery: </strong> {this.props.uiItems.mongoCount}</p>
|
<p><strong>Brews on Homebrewery: </strong>{accountDetails.mongoCount}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className='dataGroup'>
|
<div className='dataGroup'>
|
||||||
<h3>Google Information <i className='fab fa-google-drive'></i></h3>
|
<h3>Google Information <i className='fab fa-google-drive'></i></h3>
|
||||||
<p><strong>Linked to Google: </strong> {this.props.uiItems.googleId ? 'YES' : 'NO'}</p>
|
<p><strong>Linked to Google: </strong>{accountDetails.googleId ? 'YES' : 'NO'}</p>
|
||||||
{this.props.uiItems.googleId &&
|
{accountDetails.googleId && (
|
||||||
<p>
|
<p>
|
||||||
<strong>Brews on Google Drive: </strong> {this.props.uiItems.googleCount ?? <>Unable to retrieve files - <a href='https://github.com/naturalcrit/homebrewery/discussions/1580'>follow these steps to renew your Google credentials.</a></>}
|
<strong>Brews on Google Drive: </strong>{accountDetails.googleCount ?? (
|
||||||
|
<>
|
||||||
|
Unable to retrieve files - <a href='https://github.com/naturalcrit/homebrewery/discussions/1580'>follow these steps to renew your Google credentials.</a>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>;
|
<div className='dataGroup'>
|
||||||
},
|
<h4>Default Save Location</h4>
|
||||||
|
{renderSaveLocationButton('Homebrewery', 'HOMEBREWERY')}
|
||||||
|
{renderSaveLocationButton('Google Drive', 'GOOGLE-DRIVE', accountDetails.googleId)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
render : function(){
|
// return the account page inside the base layout wrapper (with navbar etc).
|
||||||
return <UIPage brew={this.props.brew}>
|
return (
|
||||||
{this.renderUiItems()}
|
<UIPage brew={brew}>
|
||||||
</UIPage>;
|
{renderAccountPage()}
|
||||||
}
|
</UIPage>);
|
||||||
});
|
};
|
||||||
|
|
||||||
module.exports = AccountPage;
|
module.exports = AccountPage;
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
require('./brewItem.less');
|
require('./brewItem.less');
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const createClass = require('create-react-class');
|
const createClass = require('create-react-class');
|
||||||
const _ = require('lodash');
|
|
||||||
const cx = require('classnames');
|
|
||||||
const moment = require('moment');
|
const moment = require('moment');
|
||||||
const request = require('../../../../utils/request-middleware.js');
|
const request = require('../../../../utils/request-middleware.js');
|
||||||
|
|
||||||
const googleDriveIcon = require('../../../../googleDrive.svg');
|
const googleDriveIcon = require('../../../../googleDrive.svg');
|
||||||
|
const homebreweryIcon = require('../../../../thumbnail.png');
|
||||||
const dedent = require('dedent-tabs').default;
|
const dedent = require('dedent-tabs').default;
|
||||||
|
|
||||||
const BrewItem = createClass({
|
const BrewItem = createClass({
|
||||||
@@ -19,6 +18,7 @@ const BrewItem = createClass({
|
|||||||
authors : [],
|
authors : [],
|
||||||
stubbed : true
|
stubbed : true
|
||||||
},
|
},
|
||||||
|
updateListFilter : ()=>{},
|
||||||
reportError : ()=>{}
|
reportError : ()=>{}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -43,6 +43,10 @@ const BrewItem = createClass({
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
updateFilter : function(type, term){
|
||||||
|
this.props.updateListFilter(type, term);
|
||||||
|
},
|
||||||
|
|
||||||
renderDeleteBrewLink : function(){
|
renderDeleteBrewLink : function(){
|
||||||
if(!this.props.brew.editId) return;
|
if(!this.props.brew.editId) return;
|
||||||
|
|
||||||
@@ -90,11 +94,17 @@ const BrewItem = createClass({
|
|||||||
</a>;
|
</a>;
|
||||||
},
|
},
|
||||||
|
|
||||||
renderGoogleDriveIcon : function(){
|
renderStorageIcon : function(){
|
||||||
if(!this.props.brew.googleId) return;
|
if(this.props.brew.googleId) {
|
||||||
|
return <span title={this.props.brew.webViewLink ? 'Your Google Drive Storage': 'Another User\'s Google Drive Storage'}>
|
||||||
return <span>
|
<a href={this.props.brew.webViewLink} target='_blank'>
|
||||||
<img className='googleDriveIcon' src={googleDriveIcon} alt='googleDriveIcon' />
|
<img className='googleDriveIcon' src={googleDriveIcon} alt='googleDriveIcon' />
|
||||||
|
</a>
|
||||||
|
</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <span title='Homebrewery Storage'>
|
||||||
|
<img className='homebreweryIcon' src={homebreweryIcon} alt='homebreweryIcon' />
|
||||||
</span>;
|
</span>;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -102,6 +112,9 @@ const BrewItem = createClass({
|
|||||||
const brew = this.props.brew;
|
const brew = this.props.brew;
|
||||||
if(Array.isArray(brew.tags)) { // temporary fix until dud tags are cleaned
|
if(Array.isArray(brew.tags)) { // temporary fix until dud tags are cleaned
|
||||||
brew.tags = brew.tags?.filter((tag)=>tag); //remove tags that are empty strings
|
brew.tags = brew.tags?.filter((tag)=>tag); //remove tags that are empty strings
|
||||||
|
brew.tags.sort((a, b)=>{
|
||||||
|
return a.indexOf(':') - b.indexOf(':') != 0 ? a.indexOf(':') - b.indexOf(':') : a.toLowerCase().localeCompare(b.toLowerCase());
|
||||||
|
});
|
||||||
}
|
}
|
||||||
const dateFormatString = 'YYYY-MM-DD HH:mm:ss';
|
const dateFormatString = 'YYYY-MM-DD HH:mm:ss';
|
||||||
|
|
||||||
@@ -118,17 +131,21 @@ const BrewItem = createClass({
|
|||||||
<div className='info'>
|
<div className='info'>
|
||||||
|
|
||||||
{brew.tags?.length ? <>
|
{brew.tags?.length ? <>
|
||||||
<div className='brewTags' title={`Tags:\n${brew.tags.join('\n')}`}>
|
<div className='brewTags' title={`${brew.tags.length} tags:\n${brew.tags.join('\n')}`}>
|
||||||
<i className='fas fa-tags'/>
|
<i className='fas fa-tags'/>
|
||||||
{brew.tags.map((tag, idx)=>{
|
{brew.tags.map((tag, idx)=>{
|
||||||
const matches = tag.match(/^(?:([^:]+):)?([^:]+)$/);
|
const matches = tag.match(/^(?:([^:]+):)?([^:]+)$/);
|
||||||
return <span key={idx} className={matches[1]}>{matches[2]}</span>;
|
return <span key={idx} className={matches[1]} onClick={()=>{this.updateFilter(tag);}}>{matches[2]}</span>;
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</> : <></>
|
</> : <></>
|
||||||
}
|
}
|
||||||
<span title={`Authors:\n${brew.authors?.join('\n')}`}>
|
<span title={`Authors:\n${brew.authors?.join('\n')}`}>
|
||||||
<i className='fas fa-user'/> {brew.authors?.join(', ')}
|
<i className='fas fa-user'/> {brew.authors?.map((author, index)=>(
|
||||||
|
<>
|
||||||
|
<a key={index} href={`/user/${author}`}>{author}</a>
|
||||||
|
{index < brew.authors.length - 1 && ', '}
|
||||||
|
</>))}
|
||||||
</span>
|
</span>
|
||||||
<br />
|
<br />
|
||||||
<span title={`Last viewed: ${moment(brew.lastViewed).local().format(dateFormatString)}`}>
|
<span title={`Last viewed: ${moment(brew.lastViewed).local().format(dateFormatString)}`}>
|
||||||
@@ -144,7 +161,7 @@ const BrewItem = createClass({
|
|||||||
Last updated: ${moment(brew.updatedAt).local().format(dateFormatString)}`}>
|
Last updated: ${moment(brew.updatedAt).local().format(dateFormatString)}`}>
|
||||||
<i className='fas fa-sync-alt' /> {moment(brew.updatedAt).fromNow()}
|
<i className='fas fa-sync-alt' /> {moment(brew.updatedAt).fromNow()}
|
||||||
</span>
|
</span>
|
||||||
{this.renderGoogleDriveIcon()}
|
{this.renderStorageIcon()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='links'>
|
<div className='links'>
|
||||||
|
|||||||
@@ -48,6 +48,10 @@
|
|||||||
&>span{
|
&>span{
|
||||||
margin-right : 12px;
|
margin-right : 12px;
|
||||||
line-height : 1.5em;
|
line-height : 1.5em;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color:inherit;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.brewTags span {
|
.brewTags span {
|
||||||
@@ -59,6 +63,41 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
border-color: currentColor;
|
||||||
|
cursor : pointer;
|
||||||
|
&:before {
|
||||||
|
font-family: 'Font Awesome 5 Free';
|
||||||
|
font-size: 12px;
|
||||||
|
margin-right: 3px;
|
||||||
|
}
|
||||||
|
&.type {
|
||||||
|
background-color: #0080003b;
|
||||||
|
color: #008000;
|
||||||
|
&:before{
|
||||||
|
content: '\f0ad';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.group {
|
||||||
|
background-color: #5050503b;
|
||||||
|
color: #000000;
|
||||||
|
&:before{
|
||||||
|
content: '\f500';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.meta {
|
||||||
|
background-color: #0000803b;
|
||||||
|
color: #000080;
|
||||||
|
&:before{
|
||||||
|
content: '\f05a';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.system {
|
||||||
|
background-color: #8000003b;
|
||||||
|
color: #800000;
|
||||||
|
&:before{
|
||||||
|
content: '\f518';
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
&:hover{
|
&:hover{
|
||||||
.links{
|
.links{
|
||||||
@@ -85,6 +124,7 @@
|
|||||||
opacity : 0.6;
|
opacity : 0.6;
|
||||||
font-size : 1.3em;
|
font-size : 1.3em;
|
||||||
color : white;
|
color : white;
|
||||||
|
text-decoration : unset;
|
||||||
&:hover{
|
&:hover{
|
||||||
opacity : 1;
|
opacity : 1;
|
||||||
}
|
}
|
||||||
@@ -98,4 +138,11 @@
|
|||||||
padding : 0px;
|
padding : 0px;
|
||||||
margin : -5px;
|
margin : -5px;
|
||||||
}
|
}
|
||||||
|
.homebreweryIcon {
|
||||||
|
mix-blend-mode : darken;
|
||||||
|
height : 24px;
|
||||||
|
position : relative;
|
||||||
|
top : 5px;
|
||||||
|
left : -5px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ const ListPage = createClass({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
filterString : this.props.query?.filter || '',
|
filterString : this.props.query?.filter || '',
|
||||||
|
filterTags : [],
|
||||||
sortType : this.props.query?.sort || null,
|
sortType : this.props.query?.sort || null,
|
||||||
sortDir : this.props.query?.dir || null,
|
sortDir : this.props.query?.dir || null,
|
||||||
query : this.props.query,
|
query : this.props.query,
|
||||||
@@ -82,14 +83,14 @@ const ListPage = createClass({
|
|||||||
if(!brews || !brews.length) return <div className='noBrews'>No Brews.</div>;
|
if(!brews || !brews.length) return <div className='noBrews'>No Brews.</div>;
|
||||||
|
|
||||||
return _.map(brews, (brew, idx)=>{
|
return _.map(brews, (brew, idx)=>{
|
||||||
return <BrewItem brew={brew} key={idx} reportError={this.props.reportError}/>;
|
return <BrewItem brew={brew} key={idx} reportError={this.props.reportError} updateListFilter={ (tag)=>{ this.updateUrl(this.state.filterString, this.state.sortType, this.state.sortDir, tag); }}/>;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
sortBrewOrder : function(brew){
|
sortBrewOrder : function(brew){
|
||||||
if(!brew.title){brew.title = 'No Title';}
|
if(!brew.title){brew.title = 'No Title';}
|
||||||
const mapping = {
|
const mapping = {
|
||||||
'alpha' : _.deburr(brew.title.toLowerCase()),
|
'alpha' : _.deburr(brew.title.trim().toLowerCase()),
|
||||||
'created' : moment(brew.createdAt).format(),
|
'created' : moment(brew.createdAt).format(),
|
||||||
'updated' : moment(brew.updatedAt).format(),
|
'updated' : moment(brew.updatedAt).format(),
|
||||||
'views' : brew.views,
|
'views' : brew.views,
|
||||||
@@ -136,13 +137,33 @@ const ListPage = createClass({
|
|||||||
return;
|
return;
|
||||||
},
|
},
|
||||||
|
|
||||||
updateUrl : function(filterTerm, sortType, sortDir){
|
updateUrl : function(filterTerm, sortType, sortDir, filterTag=''){
|
||||||
const url = new URL(window.location.href);
|
const url = new URL(window.location.href);
|
||||||
const urlParams = new URLSearchParams(url.search);
|
const urlParams = new URLSearchParams(url.search);
|
||||||
|
|
||||||
urlParams.set('sort', sortType);
|
urlParams.set('sort', sortType);
|
||||||
urlParams.set('dir', sortDir);
|
urlParams.set('dir', sortDir);
|
||||||
|
|
||||||
|
let filterTags = urlParams.getAll('tag');
|
||||||
|
if(filterTag != '') {
|
||||||
|
if(filterTags.findIndex((tag)=>{return tag.toLowerCase()==filterTag.toLowerCase();}) == -1){
|
||||||
|
filterTags.push(filterTag);
|
||||||
|
} else {
|
||||||
|
filterTags = filterTags.filter((tag)=>{ return tag.toLowerCase() != filterTag.toLowerCase(); });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
urlParams.delete('tag');
|
||||||
|
// Add tags to URL in the order they were clicked
|
||||||
|
filterTags.forEach((tag)=>{ urlParams.append('tag', tag); });
|
||||||
|
// Sort tags before updating state
|
||||||
|
filterTags.sort((a, b)=>{
|
||||||
|
return a.indexOf(':') - b.indexOf(':') != 0 ? a.indexOf(':') - b.indexOf(':') : a.toLowerCase().localeCompare(b.toLowerCase());
|
||||||
|
});
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
filterTags
|
||||||
|
});
|
||||||
|
|
||||||
if(!filterTerm)
|
if(!filterTerm)
|
||||||
urlParams.delete('filter');
|
urlParams.delete('filter');
|
||||||
else
|
else
|
||||||
@@ -166,6 +187,16 @@ const ListPage = createClass({
|
|||||||
</div>;
|
</div>;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
renderTagsOptions : function(){
|
||||||
|
if(this.state.filterTags?.length == 0) return;
|
||||||
|
return <div className='tags-container'>
|
||||||
|
{_.map(this.state.filterTags, (tag, idx)=>{
|
||||||
|
const matches = tag.match(/^(?:([^:]+):)?([^:]+)$/);
|
||||||
|
return <span key={idx} className={matches[1]} onClick={()=>{ this.updateUrl(this.state.filterString, this.state.sortType, this.state.sortDir, tag); }}>{matches[2]}</span>;
|
||||||
|
})}
|
||||||
|
</div>;
|
||||||
|
},
|
||||||
|
|
||||||
renderSortOptions : function(){
|
renderSortOptions : function(){
|
||||||
return <div className='sort-container'>
|
return <div className='sort-container'>
|
||||||
<h6>Sort by :</h6>
|
<h6>Sort by :</h6>
|
||||||
@@ -176,9 +207,6 @@ const ListPage = createClass({
|
|||||||
{/* {this.renderSortOption('Latest', 'latest')} */}
|
{/* {this.renderSortOption('Latest', 'latest')} */}
|
||||||
|
|
||||||
{this.renderFilterOption()}
|
{this.renderFilterOption()}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</div>;
|
</div>;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -186,14 +214,28 @@ const ListPage = createClass({
|
|||||||
const testString = _.deburr(this.state.filterString).toLowerCase();
|
const testString = _.deburr(this.state.filterString).toLowerCase();
|
||||||
|
|
||||||
brews = _.filter(brews, (brew)=>{
|
brews = _.filter(brews, (brew)=>{
|
||||||
|
// Filter by user entered text
|
||||||
const brewStrings = _.deburr([
|
const brewStrings = _.deburr([
|
||||||
brew.title,
|
brew.title,
|
||||||
brew.description,
|
brew.description,
|
||||||
brew.tags].join('\n')
|
brew.tags].join('\n')
|
||||||
.toLowerCase());
|
.toLowerCase());
|
||||||
|
|
||||||
return brewStrings.includes(testString);
|
const filterTextTest = brewStrings.includes(testString);
|
||||||
|
|
||||||
|
// Filter by user selected tags
|
||||||
|
let filterTagTest = true;
|
||||||
|
if(this.state.filterTags.length > 0){
|
||||||
|
filterTagTest = Array.isArray(brew.tags) && this.state.filterTags?.every((tag)=>{
|
||||||
|
return brew.tags.findIndex((brewTag)=>{
|
||||||
|
return brewTag.toLowerCase() == tag.toLowerCase();
|
||||||
|
}) >= 0;
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return filterTextTest && filterTagTest;
|
||||||
|
});
|
||||||
|
|
||||||
return _.orderBy(brews, (brew)=>{ return this.sortBrewOrder(brew); }, this.state.sortDir);
|
return _.orderBy(brews, (brew)=>{ return this.sortBrewOrder(brew); }, this.state.sortDir);
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -220,9 +262,11 @@ const ListPage = createClass({
|
|||||||
render : function(){
|
render : function(){
|
||||||
return <div className='listPage sitePage'>
|
return <div className='listPage sitePage'>
|
||||||
{/*<style>@layer V3_5ePHB, bundle;</style>*/}
|
{/*<style>@layer V3_5ePHB, bundle;</style>*/}
|
||||||
<link href='/themes/V3/5ePHB/style.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.props.navItems}
|
||||||
{this.renderSortOptions()}
|
{this.renderSortOptions()}
|
||||||
|
{this.renderTagsOptions()}
|
||||||
|
|
||||||
<div className='content V3'>
|
<div className='content V3'>
|
||||||
<div className='page'>
|
<div className='page'>
|
||||||
|
|||||||
@@ -2,17 +2,18 @@
|
|||||||
.noColumns(){
|
.noColumns(){
|
||||||
column-count : auto;
|
column-count : auto;
|
||||||
column-fill : auto;
|
column-fill : auto;
|
||||||
column-gap : auto;
|
column-gap : normal;
|
||||||
column-width : auto;
|
column-width : auto;
|
||||||
-webkit-column-count : auto;
|
-webkit-column-count : auto;
|
||||||
-moz-column-count : auto;
|
-moz-column-count : auto;
|
||||||
-webkit-column-width : auto;
|
-webkit-column-width : auto;
|
||||||
-moz-column-width : auto;
|
-moz-column-width : auto;
|
||||||
-webkit-column-gap : auto;
|
-webkit-column-gap : normal;
|
||||||
-moz-column-gap : auto;
|
-moz-column-gap : normal;
|
||||||
height : auto;
|
height : auto;
|
||||||
min-height : 279.4mm;
|
min-height : 279.4mm;
|
||||||
margin : 20px auto;
|
margin : 20px auto;
|
||||||
|
contain : unset;
|
||||||
}
|
}
|
||||||
.listPage{
|
.listPage{
|
||||||
.content{
|
.content{
|
||||||
@@ -52,7 +53,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.sort-container{
|
.sort-container {
|
||||||
font-family : 'Open Sans', sans-serif;
|
font-family : 'Open Sans', sans-serif;
|
||||||
position : sticky;
|
position : sticky;
|
||||||
top : 0;
|
top : 0;
|
||||||
@@ -124,4 +125,66 @@
|
|||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
.tags-container {
|
||||||
|
height : 30px;
|
||||||
|
background-color : #555;
|
||||||
|
border-top : 1px solid #666;
|
||||||
|
border-bottom : 1px solid #666;
|
||||||
|
color : white;
|
||||||
|
display : flex;
|
||||||
|
justify-content : center;
|
||||||
|
align-items : center;
|
||||||
|
column-gap : 15px;
|
||||||
|
row-gap : 5px;
|
||||||
|
flex-wrap : wrap;
|
||||||
|
span {
|
||||||
|
font-family : 'Open Sans', sans-serif;
|
||||||
|
font-size : 11px;
|
||||||
|
font-weight : bold;
|
||||||
|
border : 1px solid;
|
||||||
|
border-radius : 3px;
|
||||||
|
padding : 3px;
|
||||||
|
cursor : pointer;
|
||||||
|
color: #dfdfdf;
|
||||||
|
&:before {
|
||||||
|
font-family: 'Font Awesome 5 Free';
|
||||||
|
font-size: 12px;
|
||||||
|
margin-right: 3px;
|
||||||
|
}
|
||||||
|
&:after {
|
||||||
|
content: '\f00d';
|
||||||
|
font-family: 'Font Awesome 5 Free';
|
||||||
|
font-size: 12px;
|
||||||
|
margin-left: 3px;
|
||||||
|
}
|
||||||
|
&.type {
|
||||||
|
background-color: #008000;
|
||||||
|
border-color: #00a000;
|
||||||
|
&:before{
|
||||||
|
content: '\f0ad';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.group {
|
||||||
|
background-color: #505050;
|
||||||
|
border-color: #000000;
|
||||||
|
&:before{
|
||||||
|
content: '\f500';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.meta {
|
||||||
|
background-color: #000080;
|
||||||
|
border-color: #0000a0;
|
||||||
|
&:before{
|
||||||
|
content: '\f05a';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.system {
|
||||||
|
background-color: #800000;
|
||||||
|
border-color: #a00000;
|
||||||
|
&:before{
|
||||||
|
content: '\f518';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,47 +1,69 @@
|
|||||||
.uiPage{
|
.homebrew {
|
||||||
.content{
|
.uiPage.sitePage {
|
||||||
overflow-y : hidden;
|
.content {
|
||||||
width : 90vw;
|
width : ~"min(90vw, 1000px)";
|
||||||
background-color: #f0f0f0;
|
padding : 2% 4%;
|
||||||
font-family: 'Open Sans';
|
margin-top : 25px;
|
||||||
margin-left: auto;
|
margin-right : auto;
|
||||||
margin-right: auto;
|
margin-left : auto;
|
||||||
margin-top: 25px;
|
overflow-y : scroll;
|
||||||
padding: 2% 4%;
|
font-family : 'Open Sans';
|
||||||
font-size: 0.8em;
|
font-size : 0.8em;
|
||||||
line-height: 1.8em;
|
line-height : 1.8em;
|
||||||
.dataGroup{
|
background-color : #F0F0F0;
|
||||||
padding: 6px 20px 15px;
|
.dataGroup {
|
||||||
border: 2px solid black;
|
padding : 6px 20px 15px;
|
||||||
border-radius: 5px;
|
margin : 5px 0px;
|
||||||
margin: 5px 0px;
|
border : 2px solid black;
|
||||||
|
border-radius : 5px;
|
||||||
|
button {
|
||||||
|
background-color : transparent;
|
||||||
|
border : 1px solid black;
|
||||||
|
border-radius : 5px;
|
||||||
|
width : 125px;
|
||||||
|
color : black;
|
||||||
|
margin-right : 5px;
|
||||||
|
&.active {
|
||||||
|
background-color: #0007;
|
||||||
|
color: white;
|
||||||
|
&:before {
|
||||||
|
content: '\f00c';
|
||||||
|
font-family: 'FONT AWESOME 5 FREE';
|
||||||
|
margin-right: 5px;
|
||||||
}
|
}
|
||||||
h1, h2, h3, h4{
|
}
|
||||||
font-weight: 900;
|
}
|
||||||
text-transform: uppercase;
|
}
|
||||||
margin: 0.5em 30% 0.25em 0;
|
h1, h2, h3, h4 {
|
||||||
border-bottom: 2px solid slategrey;
|
width : 100%;
|
||||||
|
margin : 0.5em 30% 0.25em 0;
|
||||||
|
font-weight : 900;
|
||||||
|
text-transform : uppercase;
|
||||||
|
border-bottom : 2px solid slategrey;
|
||||||
}
|
}
|
||||||
h1 {
|
h1 {
|
||||||
font-size: 2em;
|
margin-right : 0;
|
||||||
border-bottom: 2px solid darkslategrey;
|
margin-bottom : 0.5em;
|
||||||
margin-bottom: 0.5em;
|
font-size : 2em;
|
||||||
margin-right: 0;
|
border-bottom : 2px solid darkslategrey;
|
||||||
}
|
|
||||||
h2 {
|
|
||||||
font-size: 1.75em;
|
|
||||||
}
|
}
|
||||||
|
h2 { font-size : 1.75em; }
|
||||||
h3 {
|
h3 {
|
||||||
font-size: 1.5em;
|
font-size : 1.5em;
|
||||||
svg {
|
svg { width : 19px; }
|
||||||
width: 19px;
|
|
||||||
}
|
}
|
||||||
|
h4 { font-size : 1.25em; }
|
||||||
|
strong { font-weight : bold; }
|
||||||
|
em { font-style : italic; }
|
||||||
|
ul {
|
||||||
|
padding-left : 1.25em;
|
||||||
|
list-style : square;
|
||||||
}
|
}
|
||||||
h4 {
|
.blank {
|
||||||
font-size: 1.25em;
|
height : 1em;
|
||||||
|
margin-top : 0;
|
||||||
|
& + * { margin-top : 0; }
|
||||||
}
|
}
|
||||||
strong {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -11,7 +11,7 @@ const Navbar = require('../../navbar/navbar.jsx');
|
|||||||
|
|
||||||
const NewBrew = require('../../navbar/newbrew.navitem.jsx');
|
const NewBrew = require('../../navbar/newbrew.navitem.jsx');
|
||||||
const HelpNavItem = require('../../navbar/help.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 ErrorNavItem = require('../../navbar/error-navitem.jsx');
|
||||||
const Account = require('../../navbar/account.navitem.jsx');
|
const Account = require('../../navbar/account.navitem.jsx');
|
||||||
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
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 Editor = require('../../editor/editor.jsx');
|
||||||
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
||||||
|
|
||||||
|
const LockNotification = require('./lockNotification/lockNotification.jsx');
|
||||||
|
|
||||||
const Markdown = require('naturalcrit/markdown.js');
|
const Markdown = require('naturalcrit/markdown.js');
|
||||||
|
|
||||||
const { DEFAULT_BREW_LOAD } = require('../../../../server/brewDefaults.js');
|
const { DEFAULT_BREW_LOAD } = require('../../../../server/brewDefaults.js');
|
||||||
|
const { printCurrentBrew } = require('../../../../shared/helpers.js');
|
||||||
|
|
||||||
const googleDriveIcon = require('../../googleDrive.svg');
|
const googleDriveIcon = require('../../googleDrive.svg');
|
||||||
|
|
||||||
@@ -50,9 +53,13 @@ const EditPage = createClass({
|
|||||||
url : '',
|
url : '',
|
||||||
autoSave : true,
|
autoSave : true,
|
||||||
autoSaveWarning : false,
|
autoSaveWarning : false,
|
||||||
unsavedTime : new Date()
|
unsavedTime : new Date(),
|
||||||
|
currentEditorPage : 0,
|
||||||
|
displayLockMessage : this.props.brew.lock || false
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
editor : React.createRef(null),
|
||||||
savedBrew : null,
|
savedBrew : null,
|
||||||
|
|
||||||
componentDidMount : function(){
|
componentDidMount : function(){
|
||||||
@@ -91,8 +98,8 @@ const EditPage = createClass({
|
|||||||
if(!(e.ctrlKey || e.metaKey)) return;
|
if(!(e.ctrlKey || e.metaKey)) return;
|
||||||
const S_KEY = 83;
|
const S_KEY = 83;
|
||||||
const P_KEY = 80;
|
const P_KEY = 80;
|
||||||
if(e.keyCode == S_KEY) this.save();
|
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){
|
if(e.keyCode == P_KEY || e.keyCode == S_KEY){
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -100,7 +107,7 @@ const EditPage = createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
handleSplitMove : function(){
|
handleSplitMove : function(){
|
||||||
this.refs.editor.update();
|
this.editor.current.update();
|
||||||
},
|
},
|
||||||
|
|
||||||
handleTextChange : function(text){
|
handleTextChange : function(text){
|
||||||
@@ -111,7 +118,8 @@ const EditPage = createClass({
|
|||||||
this.setState((prevState)=>({
|
this.setState((prevState)=>({
|
||||||
brew : { ...prevState.brew, text: text },
|
brew : { ...prevState.brew, text: text },
|
||||||
isPending : true,
|
isPending : true,
|
||||||
htmlErrors : htmlErrors
|
htmlErrors : htmlErrors,
|
||||||
|
currentEditorPage : this.editor.current.getCurrentPage() - 1 //Offset index since Marked starts pages at 0
|
||||||
}), ()=>{if(this.state.autoSave) this.trySave();});
|
}), ()=>{if(this.state.autoSave) this.trySave();});
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -137,13 +145,14 @@ const EditPage = createClass({
|
|||||||
return !_.isEqual(this.state.brew, this.savedBrew);
|
return !_.isEqual(this.state.brew, this.savedBrew);
|
||||||
},
|
},
|
||||||
|
|
||||||
trySave : function(){
|
trySave : function(immediate=false){
|
||||||
if(!this.debounceSave) this.debounceSave = _.debounce(this.save, SAVE_TIMEOUT);
|
if(!this.debounceSave) this.debounceSave = _.debounce(this.save, SAVE_TIMEOUT);
|
||||||
if(this.hasChanges()){
|
if(this.hasChanges()){
|
||||||
this.debounceSave();
|
this.debounceSave();
|
||||||
} else {
|
} else {
|
||||||
this.debounceSave.cancel();
|
this.debounceSave.cancel();
|
||||||
}
|
}
|
||||||
|
if(immediate) this.debounceSave.flush();
|
||||||
},
|
},
|
||||||
|
|
||||||
handleGoogleClick : function(){
|
handleGoogleClick : function(){
|
||||||
@@ -373,7 +382,7 @@ const EditPage = createClass({
|
|||||||
post to reddit
|
post to reddit
|
||||||
</Nav.item>
|
</Nav.item>
|
||||||
</Nav.dropdown>
|
</Nav.dropdown>
|
||||||
<PrintLink shareId={this.processShareId()} />
|
<PrintNavItem />
|
||||||
<RecentNavItem brew={this.state.brew} storageKey='edit' />
|
<RecentNavItem brew={this.state.brew} storageKey='edit' />
|
||||||
<Account />
|
<Account />
|
||||||
</Nav.section>
|
</Nav.section>
|
||||||
@@ -387,9 +396,10 @@ const EditPage = createClass({
|
|||||||
{this.renderNavbar()}
|
{this.renderNavbar()}
|
||||||
|
|
||||||
<div className='content'>
|
<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
|
<Editor
|
||||||
ref='editor'
|
ref={this.editor}
|
||||||
brew={this.state.brew}
|
brew={this.state.brew}
|
||||||
onTextChange={this.handleTextChange}
|
onTextChange={this.handleTextChange}
|
||||||
onStyleChange={this.handleStyleChange}
|
onStyleChange={this.handleStyleChange}
|
||||||
@@ -404,6 +414,8 @@ const EditPage = createClass({
|
|||||||
theme={this.state.brew.theme}
|
theme={this.state.brew.theme}
|
||||||
errors={this.state.htmlErrors}
|
errors={this.state.htmlErrors}
|
||||||
lang={this.state.brew.lang}
|
lang={this.state.brew.lang}
|
||||||
|
currentEditorPage={this.state.currentEditorPage}
|
||||||
|
allowPrint={true}
|
||||||
/>
|
/>
|
||||||
</SplitPane>
|
</SplitPane>
|
||||||
</div>
|
</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,48 +1,25 @@
|
|||||||
require('./errorPage.less');
|
require('./errorPage.less');
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const createClass = require('create-react-class');
|
const UIPage = require('../basePages/uiPage/uiPage.jsx');
|
||||||
const _ = require('lodash');
|
const Markdown = require('../../../../shared/naturalcrit/markdown.js');
|
||||||
const cx = require('classnames');
|
const ErrorIndex = require('./errors/errorIndex.js');
|
||||||
|
|
||||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
const ErrorPage = ({ brew })=>{
|
||||||
const Navbar = require('../../navbar/navbar.jsx');
|
// Retrieving the error text based on the brew's error code from ErrorIndex
|
||||||
const PatreonNavItem = require('../../navbar/patreon.navitem.jsx');
|
const errorText = ErrorIndex({ brew })[brew.HBErrorCode.toString()] || '';
|
||||||
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
|
||||||
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
|
||||||
|
|
||||||
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
return (
|
||||||
|
<UIPage brew={{ title: 'Crit Fail!' }}>
|
||||||
const ErrorPage = createClass({
|
<div className='dataGroup'>
|
||||||
getDefaultProps : function() {
|
<div className='errorTitle'>
|
||||||
return {
|
<h1>{`Error ${brew?.status || '000'}`}</h1>
|
||||||
ver : '0.0.0',
|
<h4>{brew?.text || 'No error text'}</h4>
|
||||||
errorId : ''
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
text : '# Oops \n We could not find a brew with that id. **Sorry!**',
|
|
||||||
|
|
||||||
render : function(){
|
|
||||||
return <div className='errorPage sitePage'>
|
|
||||||
<Navbar ver={this.props.ver}>
|
|
||||||
<Nav.section>
|
|
||||||
<Nav.item className='errorTitle'>
|
|
||||||
Crit Fail!
|
|
||||||
</Nav.item>
|
|
||||||
</Nav.section>
|
|
||||||
|
|
||||||
<Nav.section>
|
|
||||||
<PatreonNavItem />
|
|
||||||
<HelpNavItem />
|
|
||||||
<RecentNavItem />
|
|
||||||
</Nav.section>
|
|
||||||
</Navbar>
|
|
||||||
|
|
||||||
<div className='content'>
|
|
||||||
<BrewRenderer text={this.text} />
|
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
<hr />
|
||||||
}
|
<div dangerouslySetInnerHTML={{ __html: Markdown.render(errorText) }} />
|
||||||
});
|
</div>
|
||||||
|
</UIPage>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = ErrorPage;
|
module.exports = ErrorPage;
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
.errorPage{
|
.homebrew {
|
||||||
.errorTitle{
|
.uiPage.sitePage {
|
||||||
background-color: @orange;
|
.errorTitle {
|
||||||
|
//background-color: @orange;
|
||||||
|
color : #D02727;
|
||||||
|
text-align : center;
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
h1, h2, h3, h4 { border-bottom : none; }
|
||||||
|
hr { border-bottom : 2px solid slategrey; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
153
client/homebrew/pages/errorPage/errors/errorIndex.js
Normal file
153
client/homebrew/pages/errorPage/errors/errorIndex.js
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
const dedent = require('dedent-tabs').default;
|
||||||
|
|
||||||
|
const loginUrl = 'https://www.naturalcrit.com/login';
|
||||||
|
|
||||||
|
const errorIndex = (props)=>{
|
||||||
|
return {
|
||||||
|
// Default catch all
|
||||||
|
'00' : dedent`
|
||||||
|
## An unknown error occurred!
|
||||||
|
|
||||||
|
We aren't sure what happened, but our server wasn't able to find what you
|
||||||
|
were looking for.`,
|
||||||
|
|
||||||
|
// General Google load error
|
||||||
|
'01' : dedent`
|
||||||
|
## An error occurred while retrieving this brew from Google Drive!
|
||||||
|
|
||||||
|
Google reported an error while attempting to retrieve a brew from this link.`,
|
||||||
|
|
||||||
|
// Google Drive - 404 : brew deleted or access denied
|
||||||
|
'02' : dedent`
|
||||||
|
## We can't find this brew in Google Drive!
|
||||||
|
|
||||||
|
This file was saved on Google Drive, but this link doesn't work anymore.
|
||||||
|
${props.brew.authors?.length > 0
|
||||||
|
? `Note that this brew belongs to the Homebrewery account **${props.brew.authors[0]}**,
|
||||||
|
${props.brew.account
|
||||||
|
? `which is
|
||||||
|
${props.brew.authors[0] == props.brew.account
|
||||||
|
? `your account.`
|
||||||
|
: `not your account (you are currently signed in as **${props.brew.account}**).`
|
||||||
|
}`
|
||||||
|
: 'and you are not currently signed in to any account.'
|
||||||
|
}`
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
The Homebrewery cannot delete files from Google Drive on its own, so there
|
||||||
|
are three most likely possibilities:
|
||||||
|
:
|
||||||
|
- **The Google Drive files may have been accidentally deleted.** Look in
|
||||||
|
the Google Drive account that owns this brew (or ask the owner to do so),
|
||||||
|
and make sure the Homebrewery folder is still there, and that it holds your brews
|
||||||
|
as text files.
|
||||||
|
- **You may have changed the sharing settings for your files.** If the files
|
||||||
|
are still on Google Drive, change all of them to be shared *with everyone who has
|
||||||
|
the link* so the Homebrewery can access them.
|
||||||
|
- **The Google Account may be closed.** Google may have removed the account
|
||||||
|
due to inactivity or violating a Google policy. Make sure the owner can
|
||||||
|
still access Google Drive normally and upload/download files to it.
|
||||||
|
:
|
||||||
|
If the file isn't found, Google Drive usually puts your file in your Trash folder for
|
||||||
|
30 days. Assuming the trash hasn't been emptied yet, it might be worth checking.
|
||||||
|
You can also find the Activity tab on the right side of the Google Drive page, which
|
||||||
|
shows the recent activity on Google Drive. This can help you pin down the exact date
|
||||||
|
the brew was deleted or moved, and by whom.
|
||||||
|
:
|
||||||
|
If the brew still isn't found, some people have had success asking Google to recover
|
||||||
|
accidentally deleted files at this link:
|
||||||
|
https://support.google.com/drive/answer/1716222?hl=en&ref_topic=7000946.
|
||||||
|
At the bottom of the page there is a button that says *Send yourself an Email*
|
||||||
|
and you will receive instructions on how to request the files be restored.
|
||||||
|
:
|
||||||
|
Also note, if you prefer not to use your Google Drive for storage, you can always
|
||||||
|
change the storage location of a brew by clicking the Google drive icon by the
|
||||||
|
brew title and choosing *transfer my brew to/from Google Drive*.`,
|
||||||
|
|
||||||
|
// User is not Authors list
|
||||||
|
'03' : dedent`
|
||||||
|
## Current signed-in user does not have editor access to this brew.
|
||||||
|
|
||||||
|
If you believe you should have access to this brew, ask one of its authors to invite you
|
||||||
|
as an author by opening the Edit page for the brew, viewing the {{fa,fa-info-circle}}
|
||||||
|
**Properties** tab, and adding your username to the "invited authors" list. You can
|
||||||
|
then try to access this document again.
|
||||||
|
|
||||||
|
:
|
||||||
|
|
||||||
|
**Brew Title:** ${props.brew.brewTitle || 'Unable to show title'}
|
||||||
|
|
||||||
|
**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})`,
|
||||||
|
|
||||||
|
// User is not signed in; must be a user on the Authors List
|
||||||
|
'04' : dedent`
|
||||||
|
## Sign-in required to edit this brew.
|
||||||
|
|
||||||
|
You must be logged in to one of the accounts listed as an author of this brew.
|
||||||
|
User is not logged in. Please log in [here](${loginUrl}).
|
||||||
|
|
||||||
|
:
|
||||||
|
|
||||||
|
**Brew Title:** ${props.brew.brewTitle || 'Unable to show title'}
|
||||||
|
|
||||||
|
**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`
|
||||||
|
## No Homebrewery 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 save error
|
||||||
|
'06' : dedent`
|
||||||
|
## Unable to save Homebrewery document.
|
||||||
|
|
||||||
|
An error occurred wil attempting to save the Homebrewery document.`,
|
||||||
|
|
||||||
|
// Brew delete error
|
||||||
|
'07' : dedent`
|
||||||
|
## Unable to delete Homebrewery document.
|
||||||
|
|
||||||
|
An error occurred while attempting to remove the Homebrewery document.
|
||||||
|
|
||||||
|
:
|
||||||
|
|
||||||
|
**Brew ID:** ${props.brew.brewId}`,
|
||||||
|
|
||||||
|
// Author delete error
|
||||||
|
'08' : dedent`
|
||||||
|
## Unable to remove user from Homebrewery document.
|
||||||
|
|
||||||
|
An error occurred while attempting to remove the user from the Homebrewery document author list!
|
||||||
|
|
||||||
|
:
|
||||||
|
|
||||||
|
**Brew ID:** ${props.brew.brewId}`,
|
||||||
|
|
||||||
|
// Brew locked by Administrators error
|
||||||
|
'100' : dedent`
|
||||||
|
## This brew has been locked.
|
||||||
|
|
||||||
|
Only an author may request that this lock is removed.
|
||||||
|
|
||||||
|
:
|
||||||
|
|
||||||
|
**Brew ID:** ${props.brew.brewId}
|
||||||
|
|
||||||
|
**Brew Title:** ${props.brew.brewTitle}`,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = errorIndex;
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -33,9 +33,13 @@ const HomePage = createClass({
|
|||||||
return {
|
return {
|
||||||
brew : this.props.brew,
|
brew : this.props.brew,
|
||||||
welcomeText : this.props.brew.text,
|
welcomeText : this.props.brew.text,
|
||||||
error : undefined
|
error : undefined,
|
||||||
|
currentEditorPage : 0
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
editor : React.createRef(null),
|
||||||
|
|
||||||
handleSave : function(){
|
handleSave : function(){
|
||||||
request.post('/api')
|
request.post('/api')
|
||||||
.send(this.state.brew)
|
.send(this.state.brew)
|
||||||
@@ -49,11 +53,12 @@ const HomePage = createClass({
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
handleSplitMove : function(){
|
handleSplitMove : function(){
|
||||||
this.refs.editor.update();
|
this.editor.current.update();
|
||||||
},
|
},
|
||||||
handleTextChange : function(text){
|
handleTextChange : function(text){
|
||||||
this.setState((prevState)=>({
|
this.setState((prevState)=>({
|
||||||
brew : { ...prevState.brew, text: text }
|
brew : { ...prevState.brew, text: text },
|
||||||
|
currentEditorPage : this.editor.current.getCurrentPage() - 1 //Offset index since Marked starts pages at 0
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
renderNavbar : function(){
|
renderNavbar : function(){
|
||||||
@@ -77,15 +82,20 @@ const HomePage = createClass({
|
|||||||
{this.renderNavbar()}
|
{this.renderNavbar()}
|
||||||
|
|
||||||
<div className='content'>
|
<div className='content'>
|
||||||
<SplitPane onDragFinish={this.handleSplitMove} ref='pane'>
|
<SplitPane onDragFinish={this.handleSplitMove}>
|
||||||
<Editor
|
<Editor
|
||||||
ref='editor'
|
ref={this.editor}
|
||||||
brew={this.state.brew}
|
brew={this.state.brew}
|
||||||
onTextChange={this.handleTextChange}
|
onTextChange={this.handleTextChange}
|
||||||
renderer={this.state.brew.renderer}
|
renderer={this.state.brew.renderer}
|
||||||
showEditButtons={false}
|
showEditButtons={false}
|
||||||
/>
|
/>
|
||||||
<BrewRenderer text={this.state.brew.text} style={this.state.brew.style} renderer={this.state.brew.renderer}/>
|
<BrewRenderer
|
||||||
|
text={this.state.brew.text}
|
||||||
|
style={this.state.brew.style}
|
||||||
|
renderer={this.state.brew.renderer}
|
||||||
|
currentEditorPage={this.state.currentEditorPage}
|
||||||
|
/>
|
||||||
</SplitPane>
|
</SplitPane>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -16,9 +16,9 @@ The Homebrewery makes the creation and sharing of authentic looking Fifth-Editio
|
|||||||
**Try it!** Simply edit the text on the left and watch it *update live* on the right. Note that not every button is visible on this demo page. Click New {{fas,fa-plus-square}} in the navbar above to start brewing with all the features!
|
**Try it!** Simply edit the text on the left and watch it *update live* on the right. Note that not every button is visible on this demo page. Click New {{fas,fa-plus-square}} in the navbar above to start brewing with all the features!
|
||||||
|
|
||||||
### Editing and Sharing
|
### Editing and Sharing
|
||||||
When you create your own homebrew, you will be given a *edit url* and a *share url*.
|
When you create a new homebrew document ("brew"), your document will be given a *edit link* and a *share link*.
|
||||||
|
|
||||||
Any changes you make while on the *edit url* will be automatically saved to the database within a few seconds. Anyone with the edit url will be able to make edits to your homebrew, so be careful about who you share it with.
|
The *edit link* is where you write your brew. If you edit a brew while logged in, you are added as one of the brew's authors, and no one else can edit that brew until you add them as a new author via the {{fa,fa-info-circle}} **Properties** tab. Brews without any author can still be edited by anyone with the *edit link*, so be careful about who you share it with if you prefer to work without an account.
|
||||||
|
|
||||||
Anyone with the *share url* will be able to access a read-only version of your homebrew.
|
Anyone with the *share url* will be able to access a read-only version of your homebrew.
|
||||||
|
|
||||||
@@ -48,57 +48,63 @@ If you want to save ink or have a monochrome printer, add the **PRINT → {{fas,
|
|||||||
|
|
||||||
\column
|
\column
|
||||||
|
|
||||||
## New in V3.0.0
|
## V3 vs Legacy
|
||||||
We've implemented an extended Markdown-like syntax for block and span elements, plus a few other changes, eliminating the need for HTML tags like `div` and `span` in most cases. No raw HTML tags should be needed in a brew (*but can still be used if you insist*).
|
The Homebrewery has two renderers: Legacy and V3. The V3 renderer is recommended for all users because it is more powerful, more customizable, and continues to receive new feature updates while Legacy does not. However Legacy mode will remain available for older brews and veteran users.
|
||||||
|
|
||||||
Much of the syntax and styling has changed in V3, so converting a Legacy brew to V3 (or vice-versa) will require tweaking your document. *However*, all brews made prior to the release of v3.0.0 will still render normally, and you may switch between the "Legacy" brew renderer and the newer "V3" renderer via the {{fa,fa-info-circle}} **Properties** button on your brew at any time.
|
At any time, any individual brew can be changed to your renderer of choice via the {{fa,fa-info-circle}} **Properties** tab on your brew. However, converting between Legacy and V3 may require heavily tweaking the document; while both renderers can use raw HTML, V3 prefers a streamlined curly bracket syntax that avoids the complex HTML structures required by Legacy.
|
||||||
|
|
||||||
Scroll down to the next page for a brief summary of the changes and new features available in V3!
|
|
||||||
|
|
||||||
|
Scroll down to the next page for a brief summary of the changes and features available in V3!
|
||||||
#### New Things All The Time!
|
#### New Things All The Time!
|
||||||
Check out the latest updates in the full changelog [here](/changelog).
|
Check out the latest updates in the full changelog [here](/changelog).
|
||||||
|
|
||||||
### Helping out
|
### Helping out
|
||||||
Like this tool? Want to buy me a beer? [Head here](https://www.patreon.com/Naturalcrit) to help me keep the servers running.
|
Like this tool? Head over to our [Patreon](https://www.patreon.com/Naturalcrit) to help us keep the servers running.
|
||||||
|
|
||||||
This tool will **always** be free, never have ads, and I will never offer any "premium" features or whatever.
|
|
||||||
|
This tool will **always** be free, never have ads, and we will never offer any "premium" features or whatever.
|
||||||
|
|
||||||
### Bugs, Issues, Suggestions?
|
### Bugs, Issues, Suggestions?
|
||||||
Take a quick look at our [Frequently Asked Questions page](/faq) to see if your question has a handy answer.
|
- Check the [Frequently Asked Questions](/faq) page first for quick answers.
|
||||||
|
- Get help or the right look for your brew by posting on [r/Homebrewery](https://www.reddit.com/r/homebrewery/submit?selftext=true&title=%5BIssue%5D%20Describe%20Your%20Issue%20Here) or joining the [Discord Of Many Things](https://discord.gg/by3deKx).
|
||||||
Need help getting started or just the right look for your brew? Head to [r/Homebrewery](https://www.reddit.com/r/homebrewery/submit?selftext=true&title=%5BIssue%5D%20Describe%20Your%20Issue%20Here) and let us know!
|
- Report technical issues or provide feedback on the [GitHub Repo](https://github.com/naturalcrit/homebrewery/).
|
||||||
|
|
||||||
Have an idea to make The Homebrewery better? Or did you find something that wasn't quite right? Check out the [GitHub Repo](https://github.com/naturalcrit/homebrewery/) to report technical issues.
|
|
||||||
|
|
||||||
### Legal Junk
|
### Legal Junk
|
||||||
The Homebrewery is licensed using the [MIT License](https://github.com/naturalcrit/homebrewery/blob/master/license). Which means you are free to use The Homebrewery codebase any way that you want, except for claiming that you made it yourself.
|
The Homebrewery is licensed using the [MIT License](https://github.com/naturalcrit/homebrewery/blob/master/license). Which means you are free to use The Homebrewery codebase any way that you want, except for claiming that you made it yourself.
|
||||||
|
|
||||||
If you wish to sell or in some way gain profit for what's created on this site, it's your responsibility to ensure you have the proper licenses/rights for any images or resources used.
|
If you wish to sell or in some way gain profit for what's created on this site, it's your responsibility to ensure you have the proper licenses/rights for any images or resources used.
|
||||||
|
#### Crediting Us
|
||||||
#### Crediting Me
|
If you'd like to credit us in your brew, we'd be flattered! Just reference that you made it with The Homebrewery.
|
||||||
If you'd like to credit me in your brew, I'd be flattered! Just reference that you made it with The Homebrewery.
|
|
||||||
|
|
||||||
### More Homebrew Resources
|
### More Homebrew Resources
|
||||||
<a href='https://discord.gg/by3deKx' target='_blank'><img src='/assets/discordOfManyThings.svg' alt='Discord of Many Things Logo' title='Discord of Many Things Logo' style='width:50px; float: right; padding-left: 10px;'/></a>
|
[{width:50px,float:right,padding-left:10px}](https://discord.gg/by3deKx)
|
||||||
If you are looking for more 5e Homebrew resources check out [r/UnearthedArcana](https://www.reddit.com/r/UnearthedArcana/) and their list of useful resources [here](https://www.reddit.com/r/UnearthedArcana/wiki/resources). The <a href='https://discord.gg/by3deKx' target='_blank' title='Discord of Many Things'>Discord of Many Things</a> is another great resource to connect with fellow homebrewers for help and feedback.
|
|
||||||
|
If you are looking for more 5e Homebrew resources check out [r/UnearthedArcana](https://www.reddit.com/r/UnearthedArcana/) and their list of useful resources [here](https://www.reddit.com/r/UnearthedArcana/wiki/resources). The [Discord Of Many Things](https://discord.gg/by3deKx) is another great resource to connect with fellow homebrewers for help and feedback.
|
||||||
|
|
||||||
|
|
||||||
{{position:absolute;top:20px;right:20px;width:auto
|
{{position:absolute;top:20px;right:20px;width:auto
|
||||||
<a href='https://discord.gg/by3deKx' target='_blank' title='Discord of Many Things' style='color: black;'><img src='/assets/discord.png' style='height:30px'/></a>
|
[{height:30px}](https://discord.gg/by3deKx)
|
||||||
<a href='https://github.com/naturalcrit/homebrewery' target='_blank' title='Github' style='color: black; padding-left: 5px;'><img src='/assets/github.png' style='height:30px'/></a>
|
[{height:30px}](https://github.com/naturalcrit/homebrewery)
|
||||||
<a href='https://patreon.com/NaturalCrit' target='_blank' title='Patreon' style='color: black; padding-left: 5px;'><img src='/assets/patreon.png' style='height:30px'/></a>
|
[{height:30px}](https://patreon.com/NaturalCrit)
|
||||||
<a href='https://www.reddit.com/r/homebrewery/' target='_blank' title='Reddit' style='color: black; padding-left: 5px;'><img src='/assets/reddit.png' style='height:30px'/></a>
|
[{height:30px}](https://www.reddit.com/r/homebrewery/)
|
||||||
}}
|
}}
|
||||||
|
|
||||||
\page
|
\page
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Markdown+
|
## Markdown+
|
||||||
The Homebrewery aims to make homebrewing as simple as possible, providing a live editor with Markdown syntax that is more human-readable and faster to write with than raw HTML.
|
The Homebrewery aims to make homebrewing as simple as possible, providing a live editor with Markdown syntax that is more human-readable and faster to write with than raw HTML.
|
||||||
|
|
||||||
In version 3.0.0, with a goal of adding maximum flexibility without users resorting to complex HTML to accomplish simple tasks, Homebrewery provides an extended verision of Markdown with additional syntax.
|
From version 3.0.0, with a goal of adding maximum flexibility without users resorting to complex HTML to accomplish simple tasks, Homebrewery provides an extended verision of Markdown with additional syntax.
|
||||||
**You can enable V3 via the {{fa,fa-info-circle}} Properties button!**
|
|
||||||
|
|
||||||
### Curly Brackets
|
### Curly Brackets
|
||||||
The biggest change in V3 is the replacement of `<span></span>` and `<div></div>` with `{{ }}` for a cleaner custom formatting. Inline spans and block elements can be created and given ID's and Classes, as well as css properties, each of which are comma separated with no spaces. Use double quotes if a value requires spaces. Spans and Blocks start the same:
|
Standard Markdown lacks several equivalences to HTML. Hence, we have introduced `{{ }}` as a replacement for `<span></span>` and `<div></div>` for a cleaner custom formatting. Inline spans and block elements can be created and given ID's and Classes, as well as CSS properties, each of which are comma separated with no spaces. Use double quotes if a value requires spaces. Spans and Blocks start the same:
|
||||||
|
|
||||||
#### Span
|
#### Span
|
||||||
My favorite author is {{pen,#author,color:orange,font-family:"trebuchet ms" Brandon Sanderson}}. The orange text has a class of `pen`, an id of `author`, is colored orange, and given a new font. The first space outside of quotes marks the beginning of the content.
|
My favorite author is {{pen,#author,color:orange,font-family:"trebuchet ms" Brandon Sanderson}}. The orange text has a class of `pen`, an id of `author`, is colored orange, and given a new font. The first space outside of quotes marks the beginning of the content.
|
||||||
@@ -126,16 +132,18 @@ A blank line can be achieved with a run of one or more `:` alone on a line. More
|
|||||||
|
|
||||||
::
|
::
|
||||||
|
|
||||||
|
|
||||||
Much nicer than `<br><br><br><br><br>`
|
Much nicer than `<br><br><br><br><br>`
|
||||||
|
|
||||||
### Definition Lists
|
### Definition Lists
|
||||||
**Example** :: V3 uses HTML *definition lists* to create "lists" with hanging indents.
|
**Example** :: V3 uses HTML *definition lists* to create "lists" with hanging indents.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Column Breaks
|
### Column Breaks
|
||||||
Column and page breaks with `\column` and `\page`.
|
Column and page breaks with `\column` and `\page`.
|
||||||
|
|
||||||
\column
|
\column
|
||||||
|
|
||||||
### Tables
|
### Tables
|
||||||
Tables now allow column & row spanning between cells. This is included in some updated snippets, but a simplified example is given below.
|
Tables now allow column & row spanning between cells. This is included in some updated snippets, but a simplified example is given below.
|
||||||
|
|
||||||
@@ -163,13 +171,13 @@ Using *Curly Injection* you can assign an id, classes, or inline CSS properties
|
|||||||
|
|
||||||
 {width:100px,border:"2px solid",border-radius:10px}
|
 {width:100px,border:"2px solid",border-radius:10px}
|
||||||
|
|
||||||
\* *When using Imgur-hosted images, use the "direct link", which can be found when you click into your image in the Imgur interace.*
|
\* *When using Imgur-hosted images, use the "direct link", which can be found when you click into your image in the Imgur interface.*
|
||||||
|
|
||||||
## Snippets
|
## Snippets
|
||||||
Homebrewery comes with a series of *code snippets* found at the top of the editor pane that make it easy to create brews as quickly as possible. Just set your cursor where you want the code to appear in the editor pane, choose a snippet, and make the adjustments you need.
|
Homebrewery comes with a series of *code snippets* found at the top of the editor pane that make it easy to create brews as quickly as possible. Just set your cursor where you want the code to appear in the editor pane, choose a snippet, and make the adjustments you need.
|
||||||
|
|
||||||
## Style Editor Panel
|
## Style Editor Panel
|
||||||
{{fa,fa-paint-brush}} Technically released prior to v3 but still new to many users, check out the new **Style Editor** located on the right side of the Snippet bar. This editor accepts CSS for styling without requiring `<style>` tags-- anything that would have gone inside style tags before can now be placed here, and snippets that insert CSS styles are now located on that tab.
|
{{fa,fa-paint-brush}} Usually overlooked or unused by some users, the **Style Editor** tab is located on the right side of the Snippet bar. This editor accepts CSS for styling without requiring `<style>` tags-- anything that would have gone inside style tags before can now be placed here, and snippets that insert CSS styles are now located on that tab.
|
||||||
|
|
||||||
{{pageNumber 2}}
|
{{pageNumber 2}}
|
||||||
{{footnote PART 2 | BORING STUFF}}
|
{{footnote PART 2 | BORING STUFF}}
|
||||||
|
|||||||
@@ -2,12 +2,12 @@
|
|||||||
require('./newPage.less');
|
require('./newPage.less');
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const createClass = require('create-react-class');
|
const createClass = require('create-react-class');
|
||||||
const _ = require('lodash');
|
|
||||||
const request = require('../../utils/request-middleware.js');
|
const request = require('../../utils/request-middleware.js');
|
||||||
|
|
||||||
const Markdown = require('naturalcrit/markdown.js');
|
const Markdown = require('naturalcrit/markdown.js');
|
||||||
|
|
||||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||||
|
const PrintNavItem = require('../../navbar/print.navitem.jsx');
|
||||||
const Navbar = require('../../navbar/navbar.jsx');
|
const Navbar = require('../../navbar/navbar.jsx');
|
||||||
const AccountNavItem = require('../../navbar/account.navitem.jsx');
|
const AccountNavItem = require('../../navbar/account.navitem.jsx');
|
||||||
const ErrorNavItem = require('../../navbar/error-navitem.jsx');
|
const ErrorNavItem = require('../../navbar/error-navitem.jsx');
|
||||||
@@ -19,10 +19,12 @@ const Editor = require('../../editor/editor.jsx');
|
|||||||
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
||||||
|
|
||||||
const { DEFAULT_BREW } = require('../../../../server/brewDefaults.js');
|
const { DEFAULT_BREW } = require('../../../../server/brewDefaults.js');
|
||||||
|
const { printCurrentBrew } = require('../../../../shared/helpers.js');
|
||||||
|
|
||||||
const BREWKEY = 'homebrewery-new';
|
const BREWKEY = 'homebrewery-new';
|
||||||
const STYLEKEY = 'homebrewery-new-style';
|
const STYLEKEY = 'homebrewery-new-style';
|
||||||
const METAKEY = 'homebrewery-new-meta';
|
const METAKEY = 'homebrewery-new-meta';
|
||||||
|
let SAVEKEY;
|
||||||
|
|
||||||
|
|
||||||
const NewPage = createClass({
|
const NewPage = createClass({
|
||||||
@@ -41,10 +43,13 @@ const NewPage = createClass({
|
|||||||
isSaving : false,
|
isSaving : false,
|
||||||
saveGoogle : (global.account && global.account.googleId ? true : false),
|
saveGoogle : (global.account && global.account.googleId ? true : false),
|
||||||
error : null,
|
error : null,
|
||||||
htmlErrors : Markdown.validate(brew.text)
|
htmlErrors : Markdown.validate(brew.text),
|
||||||
|
currentEditorPage : 0
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
editor : React.createRef(null),
|
||||||
|
|
||||||
componentDidMount : function() {
|
componentDidMount : function() {
|
||||||
document.addEventListener('keydown', this.handleControlKeys);
|
document.addEventListener('keydown', this.handleControlKeys);
|
||||||
|
|
||||||
@@ -62,11 +67,15 @@ const NewPage = createClass({
|
|||||||
brew.renderer = metaStorage?.renderer ?? brew.renderer;
|
brew.renderer = metaStorage?.renderer ?? brew.renderer;
|
||||||
brew.theme = metaStorage?.theme ?? brew.theme;
|
brew.theme = metaStorage?.theme ?? brew.theme;
|
||||||
brew.lang = metaStorage?.lang ?? brew.lang;
|
brew.lang = metaStorage?.lang ?? brew.lang;
|
||||||
|
}
|
||||||
|
|
||||||
|
SAVEKEY = `HOMEBREWERY-DEFAULT-SAVE-LOCATION-${global.account?.username || ''}`;
|
||||||
|
const saveStorage = localStorage.getItem(SAVEKEY) || 'HOMEBREWERY';
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
brew : brew
|
brew : brew,
|
||||||
|
saveGoogle : (saveStorage == 'GOOGLE-DRIVE' && this.state.saveGoogle)
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
localStorage.setItem(BREWKEY, brew.text);
|
localStorage.setItem(BREWKEY, brew.text);
|
||||||
if(brew.style)
|
if(brew.style)
|
||||||
@@ -82,7 +91,7 @@ const NewPage = createClass({
|
|||||||
const S_KEY = 83;
|
const S_KEY = 83;
|
||||||
const P_KEY = 80;
|
const P_KEY = 80;
|
||||||
if(e.keyCode == S_KEY) this.save();
|
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){
|
if(e.keyCode == P_KEY || e.keyCode == S_KEY){
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -90,7 +99,7 @@ const NewPage = createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
handleSplitMove : function(){
|
handleSplitMove : function(){
|
||||||
this.refs.editor.update();
|
this.editor.current.update();
|
||||||
},
|
},
|
||||||
|
|
||||||
handleTextChange : function(text){
|
handleTextChange : function(text){
|
||||||
@@ -100,7 +109,8 @@ const NewPage = createClass({
|
|||||||
|
|
||||||
this.setState((prevState)=>({
|
this.setState((prevState)=>({
|
||||||
brew : { ...prevState.brew, text: text },
|
brew : { ...prevState.brew, text: text },
|
||||||
htmlErrors : htmlErrors
|
htmlErrors : htmlErrors,
|
||||||
|
currentEditorPage : this.editor.current.getCurrentPage() - 1 //Offset index since Marked starts pages at 0
|
||||||
}));
|
}));
|
||||||
localStorage.setItem(BREWKEY, text);
|
localStorage.setItem(BREWKEY, text);
|
||||||
},
|
},
|
||||||
@@ -172,16 +182,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(){
|
renderNavbar : function(){
|
||||||
return <Navbar>
|
return <Navbar>
|
||||||
|
|
||||||
@@ -194,7 +194,7 @@ const NewPage = createClass({
|
|||||||
<ErrorNavItem error={this.state.error} parent={this}></ErrorNavItem> :
|
<ErrorNavItem error={this.state.error} parent={this}></ErrorNavItem> :
|
||||||
this.renderSaveButton()
|
this.renderSaveButton()
|
||||||
}
|
}
|
||||||
{this.renderLocalPrintButton()}
|
<PrintNavItem />
|
||||||
<HelpNavItem />
|
<HelpNavItem />
|
||||||
<RecentNavItem />
|
<RecentNavItem />
|
||||||
<AccountNavItem />
|
<AccountNavItem />
|
||||||
@@ -206,16 +206,25 @@ const NewPage = createClass({
|
|||||||
return <div className='newPage sitePage'>
|
return <div className='newPage sitePage'>
|
||||||
{this.renderNavbar()}
|
{this.renderNavbar()}
|
||||||
<div className='content'>
|
<div className='content'>
|
||||||
<SplitPane onDragFinish={this.handleSplitMove} ref='pane'>
|
<SplitPane onDragFinish={this.handleSplitMove}>
|
||||||
<Editor
|
<Editor
|
||||||
ref='editor'
|
ref={this.editor}
|
||||||
brew={this.state.brew}
|
brew={this.state.brew}
|
||||||
onTextChange={this.handleTextChange}
|
onTextChange={this.handleTextChange}
|
||||||
onStyleChange={this.handleStyleChange}
|
onStyleChange={this.handleStyleChange}
|
||||||
onMetaChange={this.handleMetaChange}
|
onMetaChange={this.handleMetaChange}
|
||||||
renderer={this.state.brew.renderer}
|
renderer={this.state.brew.renderer}
|
||||||
/>
|
/>
|
||||||
<BrewRenderer text={this.state.brew.text} style={this.state.brew.style} renderer={this.state.brew.renderer} theme={this.state.brew.theme} lang={this.state.brew.lang} errors={this.state.htmlErrors}/>
|
<BrewRenderer
|
||||||
|
text={this.state.brew.text}
|
||||||
|
style={this.state.brew.style}
|
||||||
|
renderer={this.state.brew.renderer}
|
||||||
|
theme={this.state.brew.theme}
|
||||||
|
errors={this.state.htmlErrors}
|
||||||
|
lang={this.state.brew.lang}
|
||||||
|
currentEditorPage={this.state.currentEditorPage}
|
||||||
|
allowPrint={true}
|
||||||
|
/>
|
||||||
</SplitPane>
|
</SplitPane>
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
|
|||||||
@@ -1,110 +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'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
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'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
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'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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`} rel='stylesheet'/>
|
|
||||||
{baseThemePath &&
|
|
||||||
<link href={`/themes/${rendererPath}/${baseThemePath}/style.css`} rel='stylesheet'/>
|
|
||||||
}
|
|
||||||
<link href={`/themes/${rendererPath}/${themePath}/style.css`} rel='stylesheet'/>
|
|
||||||
{/* Apply CSS from Style tab */}
|
|
||||||
{this.renderStyle()}
|
|
||||||
<div className='pages' ref='pages'>
|
|
||||||
{this.renderPages()}
|
|
||||||
</div>
|
|
||||||
</div>;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = PrintPage;
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
.printPage{
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -6,14 +6,13 @@ const { Meta } = require('vitreum/headtags');
|
|||||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||||
const Navbar = require('../../navbar/navbar.jsx');
|
const Navbar = require('../../navbar/navbar.jsx');
|
||||||
const MetadataNav = require('../../navbar/metadata.navitem.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 RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
||||||
const Account = require('../../navbar/account.navitem.jsx');
|
const Account = require('../../navbar/account.navitem.jsx');
|
||||||
|
|
||||||
|
|
||||||
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
||||||
|
|
||||||
const { DEFAULT_BREW_LOAD } = require('../../../../server/brewDefaults.js');
|
const { DEFAULT_BREW_LOAD } = require('../../../../server/brewDefaults.js');
|
||||||
|
const { printCurrentBrew } = require('../../../../shared/helpers.js');
|
||||||
|
|
||||||
const SharePage = createClass({
|
const SharePage = createClass({
|
||||||
displayName : 'SharePage',
|
displayName : 'SharePage',
|
||||||
@@ -35,7 +34,7 @@ const SharePage = createClass({
|
|||||||
if(!(e.ctrlKey || e.metaKey)) return;
|
if(!(e.ctrlKey || e.metaKey)) return;
|
||||||
const P_KEY = 80;
|
const P_KEY = 80;
|
||||||
if(e.keyCode == P_KEY){
|
if(e.keyCode == P_KEY){
|
||||||
window.open(`/print/${this.processShareId()}?dialog=true`, '_blank').focus();
|
if(e.keyCode == P_KEY) printCurrentBrew();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
@@ -47,6 +46,19 @@ const SharePage = createClass({
|
|||||||
this.props.brew.shareId;
|
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(){
|
render : function(){
|
||||||
return <div className='sharePage sitePage'>
|
return <div className='sharePage sitePage'>
|
||||||
<Meta name='robots' content='noindex, nofollow' />
|
<Meta name='robots' content='noindex, nofollow' />
|
||||||
@@ -59,18 +71,19 @@ const SharePage = createClass({
|
|||||||
|
|
||||||
<Nav.section>
|
<Nav.section>
|
||||||
{this.props.brew.shareId && <>
|
{this.props.brew.shareId && <>
|
||||||
<PrintLink shareId={this.processShareId()} />
|
<PrintNavItem/>
|
||||||
<Nav.dropdown>
|
<Nav.dropdown>
|
||||||
<Nav.item color='red' icon='fas fa-code'>
|
<Nav.item color='red' icon='fas fa-code'>
|
||||||
source
|
source
|
||||||
</Nav.item>
|
</Nav.item>
|
||||||
<Nav.item color='blue' href={`/source/${this.processShareId()}`}>
|
<Nav.item color='blue' icon='fas fa-eye' href={`/source/${this.processShareId()}`}>
|
||||||
view
|
view
|
||||||
</Nav.item>
|
</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
|
download
|
||||||
</Nav.item>
|
</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
|
clone to new
|
||||||
</Nav.item>
|
</Nav.item>
|
||||||
</Nav.dropdown>
|
</Nav.dropdown>
|
||||||
@@ -81,7 +94,13 @@ const SharePage = createClass({
|
|||||||
</Navbar>
|
</Navbar>
|
||||||
|
|
||||||
<div className='content'>
|
<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}
|
||||||
|
allowPrint={true}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
const React = require('react');
|
const React = require('react');
|
||||||
const createClass = require('create-react-class');
|
const createClass = require('create-react-class');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const cx = require('classnames');
|
|
||||||
|
|
||||||
const ListPage = require('../basePages/listPage/listPage.jsx');
|
const ListPage = require('../basePages/listPage/listPage.jsx');
|
||||||
|
|
||||||
|
|||||||
@@ -11,9 +11,10 @@ const template = async function(name, title='', props = {}){
|
|||||||
return `<!DOCTYPE html>
|
return `<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<link href="//use.fontawesome.com/releases/v5.15.1/css/all.css" rel="stylesheet" />
|
<meta name="viewport" content="width=device-width, initial-scale=1, height=device-height, interactive-widget=resizes-visual" />
|
||||||
|
<link href="//use.fontawesome.com/releases/v6.5.1/css/all.css" rel="stylesheet" type="text/css" />
|
||||||
<link href="//fonts.googleapis.com/css?family=Open+Sans:400,300,600,700" rel="stylesheet" type="text/css" />
|
<link href="//fonts.googleapis.com/css?family=Open+Sans:400,300,600,700" rel="stylesheet" type="text/css" />
|
||||||
<link href=${`/${name}/bundle.css`} rel='stylesheet' />
|
<link href=${`/${name}/bundle.css`} type="text/css" rel='stylesheet' />
|
||||||
<link rel="icon" href="/assets/favicon.ico" type="image/x-icon" />
|
<link rel="icon" href="/assets/favicon.ico" type="image/x-icon" />
|
||||||
${ogMetaTags}
|
${ogMetaTags}
|
||||||
<meta name="twitter:card" content="summary">
|
<meta name="twitter:card" content="summary">
|
||||||
|
|||||||
27
faq.md
27
faq.md
@@ -62,16 +62,13 @@ pre {
|
|||||||
```
|
```
|
||||||
|
|
||||||
# FAQ
|
# FAQ
|
||||||
{{wide Updated Oct. 11, 2021}}
|
{{wide Updated Apr. 15, 2023}}
|
||||||
|
|
||||||
|
|
||||||
### The site is down for me! Anyone else?
|
### The site is down for me! Anyone else?
|
||||||
|
|
||||||
You can check the site status here: [Everyone or Just Me](https://downforeveryoneorjustme.com/homebrewery.naturalcrit.com)
|
You can check the site status here: [Everyone or Just Me](https://downforeveryoneorjustme.com/homebrewery.naturalcrit.com)
|
||||||
|
|
||||||
### How do I log out?
|
|
||||||
|
|
||||||
Go to https://homebrewery.naturalcrit.com/login, and hit the "*logout*" link.
|
|
||||||
|
|
||||||
### Why am I getting an error when trying to save, and my account is linked to Google?
|
### Why am I getting an error when trying to save, and my account is linked to Google?
|
||||||
|
|
||||||
@@ -105,7 +102,7 @@ The best way to avoid this is to leave space at the end of a column equal to one
|
|||||||
|
|
||||||
### Why do I need to manually create a new page? Why doesn't text flow between pages?
|
### Why do I need to manually create a new page? Why doesn't text flow between pages?
|
||||||
|
|
||||||
A Homebrewery document is at it's core an HTML & CSS document, and currently limited by the specs of those technologies. It is currently not possible to flow content from inside one box ("page") to the inside of another box. It seems likely that someday CSS will add this capability, and if/when that happens, Homebrewery will adopt it as soon as possible.
|
A Homebrewery document is at its core an HTML & CSS document, and currently limited by the specs of those technologies. It is currently not possible to flow content from inside one box ("page") to the inside of another box. It seems likely that someday CSS will add this capability, and if/when that happens, Homebrewery will adopt it as soon as possible.
|
||||||
|
|
||||||
### Where do I get images?
|
### Where do I get images?
|
||||||
The Homebrewery does not provide images for use besides some page elements and example images for snippets. You will need to find your own images for use and be sure you are following the appropriate license requirements.
|
The Homebrewery does not provide images for use besides some page elements and example images for snippets. You will need to find your own images for use and be sure you are following the appropriate license requirements.
|
||||||
@@ -120,26 +117,6 @@ The fonts used were originally created for use with the English language, though
|
|||||||
### Whenever I click on the "Get PDF" button, instead of getting a download, it opens Print Preview in another tab.
|
### Whenever I click on the "Get PDF" button, instead of getting a download, it opens Print Preview in another tab.
|
||||||
Yes, this is by design. In the print preview, select "Save as PDF" as the Destination, and then click "Save". There will be a normal download dialog where you can save your brew as a PDF.
|
Yes, this is by design. In the print preview, select "Save as PDF" as the Destination, and then click "Save". There will be a normal download dialog where you can save your brew as a PDF.
|
||||||
|
|
||||||
### The preview window is suddenly gone, I can only see the editor side of the Homebrewery (or the other way around).
|
|
||||||
|
|
||||||
1. Press `CTRL`+`SHIFT`+`i` (or right-click and select "Inspect") while in the Homebrewery.
|
|
||||||
|
|
||||||
2. Expand...
|
|
||||||
```
|
|
||||||
- `body`
|
|
||||||
- `main`
|
|
||||||
- `div class="homebrew"`
|
|
||||||
- `div class="editPage page"`
|
|
||||||
- `div class="content"`
|
|
||||||
- `div class="splitPane"`
|
|
||||||
```
|
|
||||||
|
|
||||||
There you will find 3 divs: `div class="pane" [...]`, `div class="divider" [...]`, and `div class="pane" [...]`.
|
|
||||||
|
|
||||||
The `class="pane"` looks similar to this: `div class="pane" data-reactid="36" style="flex: 0 0 auto; width: 925px;"`.
|
|
||||||
|
|
||||||
Change whatever stands behind width: to something smaller than your display width.
|
|
||||||
|
|
||||||
### I have white borders on the bottom/sides of the print preview.
|
### I have white borders on the bottom/sides of the print preview.
|
||||||
|
|
||||||
The Homebrewery paper size and your print paper size do not match.
|
The Homebrewery paper size and your print paper size do not match.
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ npm install
|
|||||||
npm audit fix
|
npm audit fix
|
||||||
npm run postinstall
|
npm run postinstall
|
||||||
|
|
||||||
cp freebsd/rc.d/homebrewery /usr/local/etc/rc.d/
|
cp install/freebsd/rc.d/homebrewery /usr/local/etc/rc.d/
|
||||||
chmod +x /usr/local/etc/rc.d/homebrewery
|
chmod +x /usr/local/etc/rc.d/homebrewery
|
||||||
|
|
||||||
sysrc homebrewery_enable=YES
|
sysrc homebrewery_enable=YES
|
||||||
|
|||||||
8107
package-lock.json
generated
8107
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
75
package.json
75
package.json
@@ -1,9 +1,10 @@
|
|||||||
{
|
{
|
||||||
"name": "homebrewery",
|
"name": "homebrewery",
|
||||||
"description": "Create authentic looking D&D homebrews using only markdown",
|
"description": "Create authentic looking D&D homebrews using only markdown",
|
||||||
"version": "3.9.0",
|
"version": "3.13.1",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.16.x"
|
"npm": "^10.2.x",
|
||||||
|
"node": "^20.8.x"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -25,10 +26,13 @@
|
|||||||
"test:coverage": "jest --coverage --silent --runInBand",
|
"test:coverage": "jest --coverage --silent --runInBand",
|
||||||
"test:dev": "jest --verbose --watch",
|
"test:dev": "jest --verbose --watch",
|
||||||
"test:basic": "jest tests/markdown/basic.test.js --verbose",
|
"test:basic": "jest tests/markdown/basic.test.js --verbose",
|
||||||
"test:mustache-syntax": "jest '.*(mustache-syntax).*' --verbose --noStackTrace",
|
"test:variables": "jest tests/markdown/variables.test.js --verbose",
|
||||||
"test:mustache-syntax:inline": "jest '.*(mustache-syntax).*' -t '^Inline:.*' --verbose --noStackTrace",
|
"test:mustache-syntax": "jest \".*(mustache-syntax).*\" --verbose --noStackTrace",
|
||||||
"test:mustache-syntax:block": "jest '.*(mustache-syntax).*' -t '^Block:.*' --verbose --noStackTrace",
|
"test:mustache-syntax:inline": "jest \".*(mustache-syntax).*\" -t '^Inline:.*' --verbose --noStackTrace",
|
||||||
"test:mustache-syntax:injection": "jest '.*(mustache-syntax).*' -t '^Injection:.*' --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:emojis": "jest tests/markdown/emojis.test.js --verbose --noStackTrace",
|
||||||
"test:route": "jest tests/routes/static-pages.test.js --verbose",
|
"test:route": "jest tests/routes/static-pages.test.js --verbose",
|
||||||
"phb": "node scripts/phb.js",
|
"phb": "node scripts/phb.js",
|
||||||
"prod": "set NODE_ENV=production && npm run build",
|
"prod": "set NODE_ENV=production && npm run build",
|
||||||
@@ -78,53 +82,56 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/core": "^7.22.1",
|
"@babel/core": "^7.24.7",
|
||||||
"@babel/plugin-transform-runtime": "^7.22.4",
|
"@babel/plugin-transform-runtime": "^7.24.7",
|
||||||
"@babel/preset-env": "^7.22.4",
|
"@babel/preset-env": "^7.24.7",
|
||||||
"@babel/preset-react": "^7.22.3",
|
"@babel/preset-react": "^7.24.7",
|
||||||
"@googleapis/drive": "^5.1.0",
|
"@googleapis/drive": "^8.11.0",
|
||||||
"body-parser": "^1.20.2",
|
"body-parser": "^1.20.2",
|
||||||
"classnames": "^2.3.2",
|
"classnames": "^2.5.1",
|
||||||
"codemirror": "^5.65.6",
|
"codemirror": "^5.65.6",
|
||||||
"cookie-parser": "^1.4.6",
|
"cookie-parser": "^1.4.6",
|
||||||
"create-react-class": "^15.7.0",
|
"create-react-class": "^15.7.0",
|
||||||
"dedent-tabs": "^0.10.3",
|
"dedent-tabs": "^0.10.3",
|
||||||
"express": "^4.18.2",
|
"dompurify": "^3.1.5",
|
||||||
|
"expr-eval": "^2.0.2",
|
||||||
|
"express": "^4.19.2",
|
||||||
"express-async-handler": "^1.2.0",
|
"express-async-handler": "^1.2.0",
|
||||||
"express-static-gzip": "2.1.7",
|
"express-static-gzip": "2.1.7",
|
||||||
"fs-extra": "11.1.1",
|
"fs-extra": "11.2.0",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"jwt-simple": "^0.5.6",
|
"jwt-simple": "^0.5.6",
|
||||||
"less": "^3.13.1",
|
"less": "^3.13.1",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"marked": "5.0.4",
|
"marked": "11.2.0",
|
||||||
"marked-extended-tables": "^1.0.6",
|
"marked-emoji": "^1.4.1",
|
||||||
"marked-gfm-heading-id": "^3.0.3",
|
"marked-extended-tables": "^1.0.8",
|
||||||
|
"marked-gfm-heading-id": "^3.2.0",
|
||||||
|
"marked-smartypants-lite": "^1.0.2",
|
||||||
"markedLegacy": "npm:marked@^0.3.19",
|
"markedLegacy": "npm:marked@^0.3.19",
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.30.1",
|
||||||
"mongoose": "^7.2.2",
|
"mongoose": "^8.4.5",
|
||||||
"nanoid": "3.3.4",
|
"nanoid": "3.3.4",
|
||||||
"nconf": "^0.12.0",
|
"nconf": "^0.12.1",
|
||||||
"npm": "^9.6.7",
|
"react": "^18.3.1",
|
||||||
"react": "^17.0.2",
|
"react-dom": "^18.3.1",
|
||||||
"react-dom": "^17.0.2",
|
|
||||||
"react-frame-component": "^4.1.3",
|
"react-frame-component": "^4.1.3",
|
||||||
"react-router-dom": "6.11.2",
|
"react-router-dom": "6.24.1",
|
||||||
"sanitize-filename": "1.6.3",
|
"sanitize-filename": "1.6.3",
|
||||||
"superagent": "^6.1.0",
|
"superagent": "^9.0.2",
|
||||||
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git"
|
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"eslint": "^8.41.0",
|
"eslint": "^8.57.0",
|
||||||
"eslint-plugin-jest": "^27.2.1",
|
"eslint-plugin-jest": "^28.6.0",
|
||||||
"eslint-plugin-react": "^7.32.2",
|
"eslint-plugin-react": "^7.34.3",
|
||||||
"jest": "^29.5.0",
|
"jest": "^29.7.0",
|
||||||
"jest-expect-message": "^1.1.3",
|
"jest-expect-message": "^1.1.3",
|
||||||
"postcss-less": "^6.0.0",
|
"postcss-less": "^6.0.0",
|
||||||
"stylelint": "^15.6.2",
|
"stylelint": "^15.11.0",
|
||||||
"stylelint-config-recess-order": "^4.0.0",
|
"stylelint-config-recess-order": "^4.6.0",
|
||||||
"stylelint-config-recommended": "^12.0.0",
|
"stylelint-config-recommended": "^13.0.0",
|
||||||
"stylelint-stylistic": "^0.4.2",
|
"stylelint-stylistic": "^0.4.3",
|
||||||
"supertest": "^6.3.3"
|
"supertest": "^7.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,6 +99,27 @@ fs.emptyDirSync('./build');
|
|||||||
await fs.copy('./themes/assets', './build/assets');
|
await fs.copy('./themes/assets', './build/assets');
|
||||||
await fs.copy('./client/icons', './build/icons');
|
await fs.copy('./client/icons', './build/icons');
|
||||||
|
|
||||||
|
//v==---------------------------MOVE CM EDITOR THEMES -----------------------------==v//
|
||||||
|
|
||||||
|
const editorThemesBuildDir = './build/homebrew/cm-themes';
|
||||||
|
await fs.copy('./node_modules/codemirror/theme', editorThemesBuildDir);
|
||||||
|
await fs.copy('./themes/codeMirror/customThemes', editorThemesBuildDir);
|
||||||
|
editorThemeFiles = fs.readdirSync(editorThemesBuildDir);
|
||||||
|
|
||||||
|
const editorThemeFile = './themes/codeMirror/editorThemes.json';
|
||||||
|
if(fs.existsSync(editorThemeFile)) fs.rmSync(editorThemeFile);
|
||||||
|
const stream = fs.createWriteStream(editorThemeFile, { flags: 'a' });
|
||||||
|
stream.write('[\n"default"');
|
||||||
|
|
||||||
|
for (themeFile of editorThemeFiles) {
|
||||||
|
stream.write(`,\n"${themeFile.slice(0, -4)}"`);
|
||||||
|
}
|
||||||
|
stream.write('\n]\n');
|
||||||
|
stream.end();
|
||||||
|
|
||||||
|
|
||||||
|
await fs.copy('./themes/codeMirror', './build/homebrew/codeMirror');
|
||||||
|
|
||||||
//v==----------------------------- BUNDLE PACKAGES --------------------------------==v//
|
//v==----------------------------- BUNDLE PACKAGES --------------------------------==v//
|
||||||
|
|
||||||
const bundles = await pack('./client/homebrew/homebrew.jsx', {
|
const bundles = await pack('./client/homebrew/homebrew.jsx', {
|
||||||
@@ -133,14 +154,14 @@ fs.emptyDirSync('./build');
|
|||||||
// build(bundles);
|
// build(bundles);
|
||||||
//
|
//
|
||||||
|
|
||||||
})().catch(console.error);
|
//In development, set up LiveReload (refreshes browser), and Nodemon (restarts server)
|
||||||
|
if(isDev){
|
||||||
//In development set up a watch server and livereload
|
livereload('./build'); // Install the Chrome extension LiveReload to automatically refresh the browser
|
||||||
if(isDev){
|
watchFile('./server.js', { // Restart server when change detected to this file or any nested directory from here
|
||||||
livereload('./build');
|
ignore : ['./build', './client', './themes'], // Ignore folders that are not running server code / avoids unneeded restarts
|
||||||
watchFile('./server.js', { // Rebuild when change detected to this file or any nested directory from here
|
ext : 'js json' // Extensions to watch (only .js/.json by default)
|
||||||
ignore : ['./build'], // Ignore ./build or it will rebuild again
|
//watch : ['./server', './themes'], // Watch additional folders if needed
|
||||||
ext : 'less', // Other extensions to watch (only .js/.json/.jsx by default)
|
|
||||||
//watch: ['./client', './server', './themes'], // Watch additional folders if you want
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
})().catch(console.error);
|
||||||
@@ -25,8 +25,8 @@
|
|||||||
"codemirror/addon/edit/closetag.js",
|
"codemirror/addon/edit/closetag.js",
|
||||||
"codemirror/addon/edit/trailingspace.js",
|
"codemirror/addon/edit/trailingspace.js",
|
||||||
"codemirror/addon/selection/active-line.js",
|
"codemirror/addon/selection/active-line.js",
|
||||||
|
"codemirror/addon/hint/show-hint.js",
|
||||||
"moment",
|
"moment",
|
||||||
"superagent",
|
"superagent"
|
||||||
"marked"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
10
server.js
10
server.js
@@ -7,6 +7,14 @@ DB.connect(config).then(()=>{
|
|||||||
// before launching server
|
// before launching server
|
||||||
const PORT = process.env.PORT || config.get('web_port') || 8000;
|
const PORT = process.env.PORT || config.get('web_port') || 8000;
|
||||||
server.app.listen(PORT, ()=>{
|
server.app.listen(PORT, ()=>{
|
||||||
console.log(`server on port: ${PORT}`);
|
const reset = '\x1b[0m'; // Reset to default style
|
||||||
|
const bright = '\x1b[1m'; // Bright (bold) style
|
||||||
|
const cyan = '\x1b[36m'; // Cyan color
|
||||||
|
const underline = '\x1b[4m'; // Underlined style
|
||||||
|
|
||||||
|
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`);
|
||||||
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -26,77 +26,116 @@ const mw = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const junkBrewPipeline = [
|
||||||
/* Search for brews that are older than 3 days and that are shorter than a tweet */
|
{ $match : {
|
||||||
const junkBrewQuery = HomebrewModel.find({
|
updatedAt : { $lt: Moment().subtract(30, 'days').toDate() },
|
||||||
'$where' : 'this.text.length < 140',
|
lastViewed : { $lt: Moment().subtract(30, 'days').toDate() }
|
||||||
createdAt : {
|
} },
|
||||||
$lt : Moment().subtract(30, 'days').toDate()
|
{ $project: { textBinSize: { $binarySize: '$textBin' } } },
|
||||||
}
|
{ $match: { textBinSize: { $lt: 140 } } },
|
||||||
}).limit(100).maxTime(60000);
|
{ $limit: 100 }
|
||||||
|
];
|
||||||
|
|
||||||
/* Search for brews that aren't compressed (missing the compressed text field) */
|
/* Search for brews that aren't compressed (missing the compressed text field) */
|
||||||
const uncompressedBrewQuery = HomebrewModel.find({
|
const uncompressedBrewQuery = HomebrewModel.find({
|
||||||
'text' : { '$exists': true }
|
'text' : { '$exists': true }
|
||||||
}).lean().limit(10000).select('_id');
|
}).lean().limit(10000).select('_id');
|
||||||
|
|
||||||
|
// Search for up to 100 brews that have not been viewed or updated in 30 days and are shorter than 140 bytes
|
||||||
router.get('/admin/cleanup', mw.adminOnly, (req, res)=>{
|
router.get('/admin/cleanup', mw.adminOnly, (req, res)=>{
|
||||||
junkBrewQuery.exec((err, objs)=>{
|
HomebrewModel.aggregate(junkBrewPipeline).option({ maxTimeMS: 60000 })
|
||||||
if(err) return res.status(500).send(err);
|
.then((objs)=>res.json({ count: objs.length }))
|
||||||
return res.json({ count: objs.length });
|
.catch((error)=>{
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ error: 'Internal Server Error' });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
/* Removes all empty brews that are older than 3 days and that are shorter than a tweet */
|
|
||||||
|
// Delete up to 100 brews that have not been viewed or updated in 30 days and are shorter than 140 bytes
|
||||||
router.post('/admin/cleanup', mw.adminOnly, (req, res)=>{
|
router.post('/admin/cleanup', mw.adminOnly, (req, res)=>{
|
||||||
junkBrewQuery.remove().exec((err, objs)=>{
|
HomebrewModel.aggregate(junkBrewPipeline).option({ maxTimeMS: 60000 })
|
||||||
if(err) return res.status(500).send(err);
|
.then((docs)=>{
|
||||||
return res.json({ count: objs.length });
|
const ids = docs.map((doc)=>doc._id);
|
||||||
|
return HomebrewModel.deleteMany({ _id: { $in: ids } });
|
||||||
|
}).then((result)=>{
|
||||||
|
res.json({ count: result.deletedCount });
|
||||||
|
}).catch((error)=>{
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ error: 'Internal Server Error' });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
/* Searches for matching edit or share id, also attempts to partial match */
|
/* Searches for matching edit or share id, also attempts to partial match */
|
||||||
router.get('/admin/lookup/:id', mw.adminOnly, (req, res, next)=>{
|
router.get('/admin/lookup/:id', mw.adminOnly, async (req, res, next)=>{
|
||||||
HomebrewModel.findOne({ $or : [
|
HomebrewModel.findOne({
|
||||||
{ editId: { '$regex': req.params.id, '$options': 'i' } },
|
$or : [
|
||||||
{ shareId: { '$regex': req.params.id, '$options': 'i' } },
|
{ editId: { $regex: req.params.id, $options: 'i' } },
|
||||||
] }).exec((err, brew)=>{
|
{ shareId: { $regex: req.params.id, $options: 'i' } },
|
||||||
|
]
|
||||||
|
}).exec()
|
||||||
|
.then((brew)=>{
|
||||||
|
if(!brew) // No document found
|
||||||
|
return res.status(404).json({ error: 'Document not found' });
|
||||||
|
else
|
||||||
return res.json(brew);
|
return res.json(brew);
|
||||||
|
})
|
||||||
|
.catch((err)=>{
|
||||||
|
console.error(err);
|
||||||
|
return res.status(500).json({ error: 'Internal Server Error' });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
/* Find 50 brews that aren't compressed yet */
|
/* Find 50 brews that aren't compressed yet */
|
||||||
router.get('/admin/finduncompressed', mw.adminOnly, (req, res)=>{
|
router.get('/admin/finduncompressed', mw.adminOnly, (req, res)=>{
|
||||||
uncompressedBrewQuery.exec((err, objs)=>{
|
const query = uncompressedBrewQuery.clone();
|
||||||
if(err) return res.status(500).send(err);
|
|
||||||
objs = objs.map((obj)=>{return obj._id;});
|
query.exec()
|
||||||
return res.json({ count: objs.length, ids: objs });
|
.then((objs)=>{
|
||||||
|
const ids = objs.map((obj)=>obj._id);
|
||||||
|
res.json({ count: ids.length, ids });
|
||||||
|
})
|
||||||
|
.catch((err)=>{
|
||||||
|
console.error(err);
|
||||||
|
res.status(500).send(err.message || 'Internal Server Error');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
/* Compresses the "text" field of a brew to binary */
|
/* Compresses the "text" field of a brew to binary */
|
||||||
router.put('/admin/compress/:id', (req, res)=>{
|
router.put('/admin/compress/:id', (req, res)=>{
|
||||||
HomebrewModel.get({ _id: req.params.id })
|
HomebrewModel.findOne({ _id: req.params.id })
|
||||||
.then((brew)=>{
|
.then((brew)=>{
|
||||||
brew.textBin = zlib.deflateRawSync(brew.text); // Compress brew text to binary before saving
|
if(!brew)
|
||||||
brew.text = undefined; // Delete the non-binary text field since it's not needed anymore
|
return res.status(404).send('Brew not found');
|
||||||
|
|
||||||
brew.save((err, obj)=>{
|
if(brew.text) {
|
||||||
if(err) throw err;
|
brew.textBin = brew.textBin || zlib.deflateRawSync(brew.text); //Don't overwrite textBin if exists
|
||||||
return res.status(200).send(obj);
|
brew.text = undefined;
|
||||||
});
|
}
|
||||||
|
|
||||||
|
return brew.save();
|
||||||
})
|
})
|
||||||
|
.then((obj)=>res.status(200).send(obj))
|
||||||
.catch((err)=>{
|
.catch((err)=>{
|
||||||
console.log(err);
|
console.error(err);
|
||||||
return res.status(500).send('Error while saving');
|
res.status(500).send('Error while saving');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get('/admin/stats', mw.adminOnly, (req, res)=>{
|
|
||||||
HomebrewModel.count({}, (err, count)=>{
|
router.get('/admin/stats', mw.adminOnly, async (req, res)=>{
|
||||||
|
try {
|
||||||
|
const totalBrewsCount = await HomebrewModel.countDocuments({});
|
||||||
|
const publishedBrewsCount = await HomebrewModel.countDocuments({ published: true });
|
||||||
|
|
||||||
return res.json({
|
return res.json({
|
||||||
totalBrews : count
|
totalBrews : totalBrewsCount,
|
||||||
});
|
totalPublishedBrews : publishedBrewsCount
|
||||||
});
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
return res.status(500).json({ error: 'Internal Server Error' });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get('/admin', mw.adminOnly, (req, res)=>{
|
router.get('/admin', mw.adminOnly, (req, res)=>{
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
/*eslint max-lines: ["warn", {"max": 400, "skipBlankLines": true, "skipComments": true}]*/
|
/*eslint max-lines: ["warn", {"max": 500, "skipBlankLines": true, "skipComments": true}]*/
|
||||||
// Set working directory to project root
|
// Set working directory to project root
|
||||||
process.chdir(`${__dirname}/..`);
|
process.chdir(`${__dirname}/..`);
|
||||||
|
|
||||||
@@ -17,26 +17,13 @@ const asyncHandler = require('express-async-handler');
|
|||||||
|
|
||||||
const { DEFAULT_BREW } = require('./brewDefaults.js');
|
const { DEFAULT_BREW } = require('./brewDefaults.js');
|
||||||
|
|
||||||
const splitTextStyleAndMetadata = (brew)=>{
|
const { splitTextStyleAndMetadata } = require('../shared/helpers.js');
|
||||||
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 sanitizeBrew = (brew, accessType)=>{
|
const sanitizeBrew = (brew, accessType)=>{
|
||||||
brew._id = undefined;
|
brew._id = undefined;
|
||||||
brew.__v = undefined;
|
brew.__v = undefined;
|
||||||
if(accessType !== 'edit'){
|
if(accessType !== 'edit' && accessType !== 'shareAuthor') {
|
||||||
brew.editId = undefined;
|
brew.editId = undefined;
|
||||||
}
|
}
|
||||||
return brew;
|
return brew;
|
||||||
@@ -257,6 +244,7 @@ app.get('/user/:username', async (req, res, next)=>{
|
|||||||
brew.pageCount = googleBrews[match].pageCount;
|
brew.pageCount = googleBrews[match].pageCount;
|
||||||
brew.renderer = googleBrews[match].renderer;
|
brew.renderer = googleBrews[match].renderer;
|
||||||
brew.version = googleBrews[match].version;
|
brew.version = googleBrews[match].version;
|
||||||
|
brew.webViewLink = googleBrews[match].webViewLink;
|
||||||
googleBrews.splice(match, 1);
|
googleBrews.splice(match, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -267,6 +255,9 @@ app.get('/user/:username', async (req, res, next)=>{
|
|||||||
}
|
}
|
||||||
|
|
||||||
req.brews = _.map(brews, (brew)=>{
|
req.brews = _.map(brews, (brew)=>{
|
||||||
|
// Clean up brew data
|
||||||
|
brew.title = brew.title?.trim();
|
||||||
|
brew.description = brew.description?.trim();
|
||||||
return sanitizeBrew(brew, ownAccount ? 'edit' : 'share');
|
return sanitizeBrew(brew, ownAccount ? 'edit' : 'share');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -300,7 +291,8 @@ app.get('/new/:id', asyncHandler(getBrew('share')), (req, res, next)=>{
|
|||||||
text : req.brew.text,
|
text : req.brew.text,
|
||||||
style : req.brew.style,
|
style : req.brew.style,
|
||||||
renderer : req.brew.renderer,
|
renderer : req.brew.renderer,
|
||||||
theme : req.brew.theme
|
theme : req.brew.theme,
|
||||||
|
tags : req.brew.tags
|
||||||
};
|
};
|
||||||
req.brew = _.defaults(brew, DEFAULT_BREW);
|
req.brew = _.defaults(brew, DEFAULT_BREW);
|
||||||
|
|
||||||
@@ -315,7 +307,6 @@ app.get('/new/:id', asyncHandler(getBrew('share')), (req, res, next)=>{
|
|||||||
//Share Page
|
//Share Page
|
||||||
app.get('/share/:id', asyncHandler(getBrew('share')), asyncHandler(async (req, res, next)=>{
|
app.get('/share/:id', asyncHandler(getBrew('share')), asyncHandler(async (req, res, next)=>{
|
||||||
const { brew } = req;
|
const { brew } = req;
|
||||||
|
|
||||||
req.ogMeta = { ...defaultMetaTags,
|
req.ogMeta = { ...defaultMetaTags,
|
||||||
title : req.brew.title || 'Untitled Brew',
|
title : req.brew.title || 'Untitled Brew',
|
||||||
description : req.brew.description || 'No description.',
|
description : req.brew.description || 'No description.',
|
||||||
@@ -323,26 +314,23 @@ app.get('/share/:id', asyncHandler(getBrew('share')), asyncHandler(async (req, r
|
|||||||
type : 'article'
|
type : 'article'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// increase visitor view count, do not include visits by author(s)
|
||||||
|
if(!brew.authors.includes(req.account?.username)){
|
||||||
if(req.params.id.length > 12 && !brew._id) {
|
if(req.params.id.length > 12 && !brew._id) {
|
||||||
const googleId = req.params.id.slice(0, -12);
|
const googleId = brew.googleId;
|
||||||
const shareId = req.params.id.slice(-12);
|
const shareId = brew.shareId;
|
||||||
await GoogleActions.increaseView(googleId, shareId, 'share', brew)
|
await GoogleActions.increaseView(googleId, shareId, 'share', brew)
|
||||||
.catch((err)=>{next(err);});
|
.catch((err)=>{next(err);});
|
||||||
} else {
|
} else {
|
||||||
await HomebrewModel.increaseView({ shareId: brew.shareId });
|
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);
|
splitTextStyleAndMetadata(req.brew);
|
||||||
return next();
|
return next();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
//Print Page
|
|
||||||
app.get('/print/:id', asyncHandler(getBrew('share')), (req, res, next)=>{
|
|
||||||
sanitizeBrew(req.brew, 'share');
|
|
||||||
splitTextStyleAndMetadata(req.brew);
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
|
|
||||||
//Account Page
|
//Account Page
|
||||||
app.get('/account', asyncHandler(async (req, res, next)=>{
|
app.get('/account', asyncHandler(async (req, res, next)=>{
|
||||||
const data = {};
|
const data = {};
|
||||||
@@ -377,7 +365,7 @@ app.get('/account', asyncHandler(async (req, res, next)=>{
|
|||||||
console.log(err);
|
console.log(err);
|
||||||
});
|
});
|
||||||
|
|
||||||
data.uiItems = {
|
data.accountDetails = {
|
||||||
username : req.account.username,
|
username : req.account.username,
|
||||||
issued : req.account.issued,
|
issued : req.account.issued,
|
||||||
googleId : Boolean(req.account.googleId),
|
googleId : Boolean(req.account.googleId),
|
||||||
@@ -397,7 +385,6 @@ app.get('/account', asyncHandler(async (req, res, next)=>{
|
|||||||
return next();
|
return next();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
||||||
const nodeEnv = config.get('node_env');
|
const nodeEnv = config.get('node_env');
|
||||||
const isLocalEnvironment = config.get('local_environments').includes(nodeEnv);
|
const isLocalEnvironment = config.get('local_environments').includes(nodeEnv);
|
||||||
// Local only
|
// Local only
|
||||||
@@ -414,8 +401,7 @@ if(isLocalEnvironment){
|
|||||||
|
|
||||||
//Render the page
|
//Render the page
|
||||||
const templateFn = require('./../client/template.js');
|
const templateFn = require('./../client/template.js');
|
||||||
app.use(asyncHandler(async (req, res, next)=>{
|
const renderPage = async (req, res)=>{
|
||||||
|
|
||||||
// Create configuration object
|
// Create configuration object
|
||||||
const configuration = {
|
const configuration = {
|
||||||
local : isLocalEnvironment,
|
local : isLocalEnvironment,
|
||||||
@@ -424,7 +410,7 @@ app.use(asyncHandler(async (req, res, next)=>{
|
|||||||
};
|
};
|
||||||
const props = {
|
const props = {
|
||||||
version : require('./../package.json').version,
|
version : require('./../package.json').version,
|
||||||
url : req.originalUrl,
|
url : req.customUrl || req.originalUrl,
|
||||||
brew : req.brew,
|
brew : req.brew,
|
||||||
brews : req.brews,
|
brews : req.brews,
|
||||||
googleBrews : req.googleBrews,
|
googleBrews : req.googleBrews,
|
||||||
@@ -438,15 +424,20 @@ app.use(asyncHandler(async (req, res, next)=>{
|
|||||||
const page = await templateFn('homebrew', title, props)
|
const page = await templateFn('homebrew', title, props)
|
||||||
.catch((err)=>{
|
.catch((err)=>{
|
||||||
console.log(err);
|
console.log(err);
|
||||||
return res.sendStatus(500);
|
|
||||||
});
|
});
|
||||||
|
return page;
|
||||||
|
};
|
||||||
|
|
||||||
|
//Send rendered page
|
||||||
|
app.use(asyncHandler(async (req, res, next)=>{
|
||||||
|
const page = await renderPage(req, res);
|
||||||
if(!page) return;
|
if(!page) return;
|
||||||
res.send(page);
|
res.send(page);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
//v=====----- Error-Handling Middleware -----=====v//
|
//v=====----- Error-Handling Middleware -----=====v//
|
||||||
//Format Errors so all fields will be sent
|
//Format Errors as plain objects so all fields will appear in the string sent
|
||||||
const replaceErrors = (key, value)=>{
|
const formatErrors = (key, value)=>{
|
||||||
if(value instanceof Error) {
|
if(value instanceof Error) {
|
||||||
const error = {};
|
const error = {};
|
||||||
Object.getOwnPropertyNames(value).forEach(function (key) {
|
Object.getOwnPropertyNames(value).forEach(function (key) {
|
||||||
@@ -458,13 +449,39 @@ const replaceErrors = (key, value)=>{
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getPureError = (error)=>{
|
const getPureError = (error)=>{
|
||||||
return JSON.parse(JSON.stringify(error, replaceErrors));
|
return JSON.parse(JSON.stringify(error, formatErrors));
|
||||||
};
|
};
|
||||||
|
|
||||||
app.use((err, req, res, next)=>{
|
app.use(async (err, req, res, next)=>{
|
||||||
const status = err.status || 500;
|
err.originalUrl = req.originalUrl;
|
||||||
console.error(err);
|
console.error(err);
|
||||||
res.status(status).send(getPureError(err));
|
|
||||||
|
if(err.originalUrl?.startsWith('/api/')) {
|
||||||
|
// console.log('API error');
|
||||||
|
res.status(err.status || err.response?.status || 500).send(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// console.log('non-API error');
|
||||||
|
const status = err.status || err.code || 500;
|
||||||
|
|
||||||
|
req.ogMeta = { ...defaultMetaTags,
|
||||||
|
title : 'Error Page',
|
||||||
|
description : 'Something went wrong!'
|
||||||
|
};
|
||||||
|
req.brew = {
|
||||||
|
...err,
|
||||||
|
title : 'Error - Something went wrong!',
|
||||||
|
text : err.errors?.map((error)=>{return error.message;}).join('\n\n') || err.message || 'Unknown error!',
|
||||||
|
status : status,
|
||||||
|
HBErrorCode : err.HBErrorCode ?? '00',
|
||||||
|
pureError : getPureError(err)
|
||||||
|
};
|
||||||
|
req.customUrl= '/error';
|
||||||
|
|
||||||
|
const page = await renderPage(req, res);
|
||||||
|
if(!page) return;
|
||||||
|
res.send(page);
|
||||||
});
|
});
|
||||||
|
|
||||||
app.use((req, res)=>{
|
app.use((req, res)=>{
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
/* eslint-disable max-lines */
|
/* eslint-disable max-lines */
|
||||||
const _ = require('lodash');
|
|
||||||
const googleDrive = require('@googleapis/drive');
|
const googleDrive = require('@googleapis/drive');
|
||||||
const { nanoid } = require('nanoid');
|
const { nanoid } = require('nanoid');
|
||||||
const token = require('./token.js');
|
const token = require('./token.js');
|
||||||
@@ -7,7 +6,9 @@ const config = require('./config.js');
|
|||||||
|
|
||||||
let serviceAuth;
|
let serviceAuth;
|
||||||
if(!config.get('service_account')){
|
if(!config.get('service_account')){
|
||||||
console.log('No Google Service Account in config files - Google Drive integration will not be available.');
|
const reset = '\x1b[0m'; // Reset to default style
|
||||||
|
const yellow = '\x1b[33m'; // yellow color
|
||||||
|
console.warn(`\n${yellow}No Google Service Account in config files - Google Drive integration will not be available.${reset}`);
|
||||||
} else {
|
} else {
|
||||||
const keys = typeof(config.get('service_account')) == 'string' ?
|
const keys = typeof(config.get('service_account')) == 'string' ?
|
||||||
JSON.parse(config.get('service_account')) :
|
JSON.parse(config.get('service_account')) :
|
||||||
@@ -18,7 +19,7 @@ if(!config.get('service_account')){
|
|||||||
serviceAuth.scopes = ['https://www.googleapis.com/auth/drive'];
|
serviceAuth.scopes = ['https://www.googleapis.com/auth/drive'];
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn(err);
|
console.warn(err);
|
||||||
console.log('Please make sure the Google Service Account is set up properly in your config files.');
|
console.warn('Please make sure the Google Service Account is set up properly in your config files.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,13 +101,13 @@ const GoogleActions = {
|
|||||||
const drive = googleDrive.drive({ version: 'v3', auth });
|
const drive = googleDrive.drive({ version: 'v3', auth });
|
||||||
|
|
||||||
const fileList = [];
|
const fileList = [];
|
||||||
let NextPageToken = "";
|
let NextPageToken = '';
|
||||||
|
|
||||||
do {
|
do {
|
||||||
const obj = await drive.files.list({
|
const obj = await drive.files.list({
|
||||||
pageSize : 1000,
|
pageSize : 1000,
|
||||||
pageToken : NextPageToken || "",
|
pageToken : NextPageToken || '',
|
||||||
fields : 'nextPageToken, files(id, name, description, createdTime, modifiedTime, properties)',
|
fields : 'nextPageToken, files(id, name, description, createdTime, modifiedTime, properties, webViewLink)',
|
||||||
q : 'mimeType != \'application/vnd.google-apps.folder\' and trashed = false'
|
q : 'mimeType != \'application/vnd.google-apps.folder\' and trashed = false'
|
||||||
})
|
})
|
||||||
.catch((err)=>{
|
.catch((err)=>{
|
||||||
@@ -139,7 +140,8 @@ const GoogleActions = {
|
|||||||
published : file.properties.published ? file.properties.published == 'true' : false,
|
published : file.properties.published ? file.properties.published == 'true' : false,
|
||||||
systems : [],
|
systems : [],
|
||||||
lang : file.properties.lang,
|
lang : file.properties.lang,
|
||||||
thumbnail : file.properties.thumbnail
|
thumbnail : file.properties.thumbnail,
|
||||||
|
webViewLink : file.webViewLink
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
return brews;
|
return brews;
|
||||||
@@ -243,9 +245,9 @@ const GoogleActions = {
|
|||||||
|
|
||||||
if(obj) {
|
if(obj) {
|
||||||
if(accessType == 'edit' && obj.data.properties.editId != accessId){
|
if(accessType == 'edit' && obj.data.properties.editId != accessId){
|
||||||
throw ('Edit ID does not match');
|
throw ({ message: 'Edit ID does not match' });
|
||||||
} else if(accessType == 'share' && obj.data.properties.shareId != accessId){
|
} else if(accessType == 'share' && obj.data.properties.shareId != accessId){
|
||||||
throw ('Share ID does not match');
|
throw ({ message: 'Share ID does not match' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const file = await drive.files.get({
|
const file = await drive.files.get({
|
||||||
|
|||||||
@@ -27,8 +27,13 @@ const api = {
|
|||||||
|
|
||||||
// If the id is longer than 12, then it's a google id + the edit id. This splits the longer id up.
|
// If the id is longer than 12, then it's a google id + the edit id. This splits the longer id up.
|
||||||
if(id.length > 12) {
|
if(id.length > 12) {
|
||||||
googleId = id.slice(0, -12);
|
if(id.length >= (33 + 12)) { // googleId is minimum 33 chars (may increase)
|
||||||
id = id.slice(-12);
|
googleId = id.slice(0, -12); // current editId is 12 chars
|
||||||
|
} else { // old editIds used to be 10 chars;
|
||||||
|
googleId = id.slice(0, -10); // if total string is too short, must be old brew
|
||||||
|
console.log('Old brew, using 10-char Id');
|
||||||
|
}
|
||||||
|
id = id.slice(googleId.length);
|
||||||
}
|
}
|
||||||
return { id, googleId };
|
return { id, googleId };
|
||||||
},
|
},
|
||||||
@@ -49,6 +54,10 @@ const api = {
|
|||||||
});
|
});
|
||||||
stub = stub?.toObject();
|
stub = stub?.toObject();
|
||||||
|
|
||||||
|
if(stub?.lock?.locked && accessType != 'edit') {
|
||||||
|
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
|
// If there is a google id, try to find the google brew
|
||||||
if(!stubOnly && (googleId || stub?.googleId)) {
|
if(!stubOnly && (googleId || stub?.googleId)) {
|
||||||
let googleError;
|
let googleError;
|
||||||
@@ -57,7 +66,14 @@ const api = {
|
|||||||
googleError = err;
|
googleError = err;
|
||||||
});
|
});
|
||||||
// Throw any error caught while attempting to retrieve Google brew.
|
// Throw any error caught while attempting to retrieve Google brew.
|
||||||
if(googleError) throw googleError;
|
if(googleError) {
|
||||||
|
const reason = googleError.errors?.[0].reason;
|
||||||
|
if(reason == 'notFound') {
|
||||||
|
throw { ...googleError, HBErrorCode: '02', authors: stub?.authors, account: req.account?.username };
|
||||||
|
} else {
|
||||||
|
throw { ...googleError, HBErrorCode: '01' };
|
||||||
|
}
|
||||||
|
}
|
||||||
// Combine the Homebrewery stub with the google brew, or if the stub doesn't exist just use the google brew
|
// Combine the Homebrewery stub with the google brew, or if the stub doesn't exist just use the google brew
|
||||||
stub = stub ? _.assign({ ...api.excludeStubProps(stub), stubbed: true }, api.excludeGoogleProps(googleBrew)) : googleBrew;
|
stub = stub ? _.assign({ ...api.excludeStubProps(stub), stubbed: true }, api.excludeGoogleProps(googleBrew)) : googleBrew;
|
||||||
}
|
}
|
||||||
@@ -65,14 +81,16 @@ const api = {
|
|||||||
const isAuthor = stub?.authors?.includes(req.account?.username);
|
const isAuthor = stub?.authors?.includes(req.account?.username);
|
||||||
const isInvited = stub?.invitedAuthors?.includes(req.account?.username);
|
const isInvited = stub?.invitedAuthors?.includes(req.account?.username);
|
||||||
if(accessType === 'edit' && (authorsExist && !(isAuthor || isInvited))) {
|
if(accessType === 'edit' && (authorsExist && !(isAuthor || isInvited))) {
|
||||||
throw `The current logged in user does not have editor access to this brew.
|
const accessError = { name: 'Access Error', status: 401 };
|
||||||
|
if(req.account){
|
||||||
If you believe you should have access to this brew, ask the file owner to invite you as an author by opening the brew, viewing the Properties tab, and adding your username to the "invited authors" list. You can then try to access this document again.`;
|
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 };
|
||||||
}
|
}
|
||||||
|
|
||||||
// If after all of that we still don't have a brew, throw an exception
|
// If after all of that we still don't have a brew, throw an exception
|
||||||
if(!stub && !stubOnly) {
|
if(!stub && !stubOnly) {
|
||||||
throw 'Brew not found in Homebrewery database or Google Drive';
|
throw { name: 'BrewLoad Error', message: 'Brew not found', status: 404, HBErrorCode: '05', accessType: accessType, brewId: id };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up brew: fill in missing fields with defaults / fix old invalid values
|
// Clean up brew: fill in missing fields with defaults / fix old invalid values
|
||||||
@@ -139,6 +157,9 @@ If you believe you should have access to this brew, ask the file owner to invite
|
|||||||
brew.text = api.mergeBrewText(brew);
|
brew.text = api.mergeBrewText(brew);
|
||||||
|
|
||||||
_.defaults(brew, DEFAULT_BREW);
|
_.defaults(brew, DEFAULT_BREW);
|
||||||
|
|
||||||
|
brew.title = brew.title.trim();
|
||||||
|
brew.description = brew.description.trim();
|
||||||
},
|
},
|
||||||
newGoogleBrew : async (account, brew, res)=>{
|
newGoogleBrew : async (account, brew, res)=>{
|
||||||
const oAuth2Client = GoogleActions.authCheck(account, res);
|
const oAuth2Client = GoogleActions.authCheck(account, res);
|
||||||
@@ -181,7 +202,7 @@ If you believe you should have access to this brew, ask the file owner to invite
|
|||||||
saved = await newHomebrew.save()
|
saved = await newHomebrew.save()
|
||||||
.catch((err)=>{
|
.catch((err)=>{
|
||||||
console.error(err, err.toString(), err.stack);
|
console.error(err, err.toString(), err.stack);
|
||||||
throw `Error while creating new brew, ${err.toString()}`;
|
throw { name: 'BrewSave Error', message: `Error while creating new brew, ${err.toString()}`, status: 500, HBErrorCode: '06' };
|
||||||
});
|
});
|
||||||
if(!saved) return;
|
if(!saved) return;
|
||||||
saved = saved.toObject();
|
saved = saved.toObject();
|
||||||
@@ -203,6 +224,8 @@ If you believe you should have access to this brew, ask the file owner to invite
|
|||||||
const { saveToGoogle, removeFromGoogle } = req.query;
|
const { saveToGoogle, removeFromGoogle } = req.query;
|
||||||
let afterSave = async ()=>true;
|
let afterSave = async ()=>true;
|
||||||
|
|
||||||
|
brew.title = brew.title.trim();
|
||||||
|
brew.description = brew.description.trim() || '';
|
||||||
brew.text = api.mergeBrewText(brew);
|
brew.text = api.mergeBrewText(brew);
|
||||||
|
|
||||||
if(brew.googleId && removeFromGoogle) {
|
if(brew.googleId && removeFromGoogle) {
|
||||||
@@ -283,11 +306,14 @@ If you believe you should have access to this brew, ask the file owner to invite
|
|||||||
try {
|
try {
|
||||||
await api.getBrew('edit')(req, res, ()=>{});
|
await api.getBrew('edit')(req, res, ()=>{});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
// Only if the error code is HBErrorCode '02', that is, Google returned "404 - Not Found"
|
||||||
|
if(err.HBErrorCode == '02') {
|
||||||
const { id, googleId } = api.getId(req);
|
const { id, googleId } = api.getId(req);
|
||||||
console.warn(`No google brew found for id ${googleId}, the stub with id ${id} will be deleted.`);
|
console.warn(`No google brew found for id ${googleId}, the stub with id ${id} will be deleted.`);
|
||||||
await HomebrewModel.deleteOne({ editId: id });
|
await HomebrewModel.deleteOne({ editId: id });
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let brew = req.brew;
|
let brew = req.brew;
|
||||||
const { googleId, editId } = brew;
|
const { googleId, editId } = brew;
|
||||||
@@ -308,7 +334,7 @@ If you believe you should have access to this brew, ask the file owner to invite
|
|||||||
await HomebrewModel.deleteOne({ _id: brew._id })
|
await HomebrewModel.deleteOne({ _id: brew._id })
|
||||||
.catch((err)=>{
|
.catch((err)=>{
|
||||||
console.error(err);
|
console.error(err);
|
||||||
throw { status: 500, message: 'Error while removing' };
|
throw { name: 'BrewDelete Error', message: 'Error while removing', status: 500, HBErrorCode: '07', brewId: brew._id };
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
if(shouldDeleteGoogleBrew) {
|
if(shouldDeleteGoogleBrew) {
|
||||||
@@ -320,7 +346,7 @@ If you believe you should have access to this brew, ask the file owner to invite
|
|||||||
brew.markModified('authors'); //Mongo will not properly update arrays without markModified()
|
brew.markModified('authors'); //Mongo will not properly update arrays without markModified()
|
||||||
await brew.save()
|
await brew.save()
|
||||||
.catch((err)=>{
|
.catch((err)=>{
|
||||||
throw { status: 500, message: err };
|
throw { name: 'BrewAuthorDelete Error', message: err, status: 500, HBErrorCode: '08', brewId: brew._id };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -111,21 +111,32 @@ describe('Tests for api', ()=>{
|
|||||||
expect(googleId).toEqual('12345');
|
expect(googleId).toEqual('12345');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return id and google id from params', ()=>{
|
it('should return 12-char id and google id from params', ()=>{
|
||||||
const { id, googleId } = api.getId({
|
const { id, googleId } = api.getId({
|
||||||
params : {
|
params : {
|
||||||
id : '123456789012abcdefghijkl'
|
id : '123456789012345678901234567890123abcdefghijkl'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
expect(googleId).toEqual('123456789012345678901234567890123');
|
||||||
expect(id).toEqual('abcdefghijkl');
|
expect(id).toEqual('abcdefghijkl');
|
||||||
expect(googleId).toEqual('123456789012');
|
});
|
||||||
|
|
||||||
|
it('should return 10-char id and google id from params', ()=>{
|
||||||
|
const { id, googleId } = api.getId({
|
||||||
|
params : {
|
||||||
|
id : '123456789012345678901234567890123abcdefghij'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(googleId).toEqual('123456789012345678901234567890123');
|
||||||
|
expect(id).toEqual('abcdefghij');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getBrew', ()=>{
|
describe('getBrew', ()=>{
|
||||||
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
|
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
|
||||||
const notFoundError = 'Brew not found in Homebrewery database or Google Drive';
|
const notFoundError = { HBErrorCode: '05', message: 'Brew not found', name: 'BrewLoad Error', status: 404, accessType: 'share', brewId: '1' };
|
||||||
|
|
||||||
it('returns middleware', ()=>{
|
it('returns middleware', ()=>{
|
||||||
const getFn = api.getBrew('share');
|
const getFn = api.getBrew('share');
|
||||||
@@ -183,7 +194,7 @@ describe('Tests for api', ()=>{
|
|||||||
expect(next).toHaveBeenCalled();
|
expect(next).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws if invalid author', async ()=>{
|
it('throws if not logged in as author', async ()=>{
|
||||||
api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
|
api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
|
||||||
model.get = jest.fn(()=>toBrewPromise({ title: 'test brew', authors: ['a'] }));
|
model.get = jest.fn(()=>toBrewPromise({ title: 'test brew', authors: ['a'] }));
|
||||||
|
|
||||||
@@ -197,9 +208,24 @@ describe('Tests for api', ()=>{
|
|||||||
err = e;
|
err = e;
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(err).toEqual(`The current logged in user does not have editor access to this brew.
|
expect(err).toEqual({ HBErrorCode: '04', message: 'User is not logged in', name: 'Access Error', status: 401, brewTitle: 'test brew', authors: ['a'] });
|
||||||
|
});
|
||||||
|
|
||||||
If you believe you should have access to this brew, ask the file owner to invite you as an author by opening the brew, viewing the Properties tab, and adding your username to the "invited authors" list. You can then try to access this document again.`);
|
it('throws if logged in as invalid author', async ()=>{
|
||||||
|
api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
|
||||||
|
model.get = jest.fn(()=>toBrewPromise({ title: 'test brew', authors: ['a'] }));
|
||||||
|
|
||||||
|
const fn = api.getBrew('edit', true);
|
||||||
|
const req = { brew: {}, account: { username: 'b' } };
|
||||||
|
|
||||||
|
let err;
|
||||||
|
try {
|
||||||
|
await fn(req, null, null);
|
||||||
|
} catch (e) {
|
||||||
|
err = e;
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(err).toEqual({ HBErrorCode: '03', message: 'User is not an Author', name: 'Access Error', status: 401, brewTitle: 'test brew', authors: ['a'] });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not throw if no authors', async ()=>{
|
it('does not throw if no authors', async ()=>{
|
||||||
@@ -272,6 +298,18 @@ If you believe you should have access to this brew, ask the file owner to invite
|
|||||||
expect(model.get).toHaveBeenCalledWith({ shareId: '1' });
|
expect(model.get).toHaveBeenCalledWith({ shareId: '1' });
|
||||||
expect(google.getGoogleBrew).toHaveBeenCalledWith('2', '1', 'share');
|
expect(google.getGoogleBrew).toHaveBeenCalledWith('2', '1', 'share');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('access is denied to a locked brew', async()=>{
|
||||||
|
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 }));
|
||||||
|
|
||||||
|
const fn = api.getBrew('share', false);
|
||||||
|
const req = { brew: {} };
|
||||||
|
const next = jest.fn();
|
||||||
|
|
||||||
|
await expect(fn(req, null, next)).rejects.toEqual({ 'HBErrorCode': '100', 'brewId': '1', 'brewTitle': 'test brew', 'code': 404, 'message': 'brew locked' });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('mergeBrewText', ()=>{
|
describe('mergeBrewText', ()=>{
|
||||||
@@ -545,7 +583,7 @@ brew`);
|
|||||||
|
|
||||||
describe('deleteBrew', ()=>{
|
describe('deleteBrew', ()=>{
|
||||||
it('should handle case where fetching the brew returns an error', async ()=>{
|
it('should handle case where fetching the brew returns an error', async ()=>{
|
||||||
api.getBrew = jest.fn(()=>async ()=>{ throw 'err'; });
|
api.getBrew = jest.fn(()=>async ()=>{ throw { message: 'err', HBErrorCode: '02' }; });
|
||||||
api.getId = jest.fn(()=>({ id: '1', googleId: '2' }));
|
api.getId = jest.fn(()=>({ id: '1', googleId: '2' }));
|
||||||
model.deleteOne = jest.fn(async ()=>{});
|
model.deleteOne = jest.fn(async ()=>{});
|
||||||
const next = jest.fn(()=>{});
|
const next = jest.fn(()=>{});
|
||||||
|
|||||||
34
shared/helpers.js
Normal file
34
shared/helpers.js
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
const _ = require('lodash');
|
||||||
|
const yaml = require('js-yaml');
|
||||||
|
|
||||||
|
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 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='';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
splitTextStyleAndMetadata,
|
||||||
|
printCurrentBrew
|
||||||
|
};
|
||||||
@@ -2,9 +2,8 @@ require('./renderWarnings.less');
|
|||||||
const React = require('react');
|
const React = require('react');
|
||||||
const createClass = require('create-react-class');
|
const createClass = require('create-react-class');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const cx = require('classnames');
|
|
||||||
|
|
||||||
const DISMISS_KEY = 'dismiss_render_warning';
|
import Dialog from '../../../client/components/dialog.jsx';
|
||||||
|
|
||||||
const RenderWarnings = createClass({
|
const RenderWarnings = createClass({
|
||||||
displayName : 'RenderWarnings',
|
displayName : 'RenderWarnings',
|
||||||
@@ -35,9 +34,6 @@ const RenderWarnings = createClass({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
checkWarnings : function(){
|
checkWarnings : function(){
|
||||||
const hideDismiss = localStorage.getItem(DISMISS_KEY);
|
|
||||||
if(hideDismiss) return this.setState({ warnings: {} });
|
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
warnings : _.reduce(this.warnings, (r, fn, type)=>{
|
warnings : _.reduce(this.warnings, (r, fn, type)=>{
|
||||||
const element = fn();
|
const element = fn();
|
||||||
@@ -46,20 +42,18 @@ const RenderWarnings = createClass({
|
|||||||
}, {})
|
}, {})
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
dismiss : function(){
|
|
||||||
localStorage.setItem(DISMISS_KEY, true);
|
|
||||||
this.checkWarnings();
|
|
||||||
},
|
|
||||||
render : function(){
|
render : function(){
|
||||||
if(_.isEmpty(this.state.warnings)) return null;
|
if(_.isEmpty(this.state.warnings)) return null;
|
||||||
|
|
||||||
return <div className='renderWarnings'>
|
const DISMISS_KEY = 'dismiss_render_warning';
|
||||||
<i className='fas fa-times dismiss' onClick={this.dismiss}/>
|
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' />
|
<i className='fas fa-exclamation-triangle ohno' />
|
||||||
<h3>Render Warnings</h3>
|
<h3>Render Warnings</h3>
|
||||||
<small>If this homebrew is rendering badly if might be because of the following:</small>
|
<small>If this homebrew is rendering badly if might be because of the following:</small>
|
||||||
<ul>{_.values(this.state.warnings)}</ul>
|
<ul>{_.values(this.state.warnings)}</ul>
|
||||||
</div>;
|
</Dialog>;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,53 +1,48 @@
|
|||||||
.renderWarnings{
|
.renderWarnings {
|
||||||
position : relative;
|
position : relative;
|
||||||
float : right;
|
float : right;
|
||||||
display : inline-block;
|
|
||||||
width : 350px;
|
width : 350px;
|
||||||
padding : 20px;
|
padding : 20px;
|
||||||
padding-bottom : 10px;
|
padding-bottom : 10px;
|
||||||
padding-left : 85px;
|
padding-left : 85px;
|
||||||
margin-bottom : 10px;
|
margin-bottom : 10px;
|
||||||
background-color : @yellow;
|
|
||||||
color : white;
|
color : white;
|
||||||
a{
|
background-color : @yellow;
|
||||||
font-weight : 800;
|
border : none;
|
||||||
}
|
a { font-weight : 800; }
|
||||||
i.ohno{
|
i.ohno {
|
||||||
position : absolute;
|
position : absolute;
|
||||||
top : 24px;
|
top : 24px;
|
||||||
left : 24px;
|
left : 24px;
|
||||||
opacity : 0.8;
|
|
||||||
font-size : 2.5em;
|
font-size : 2.5em;
|
||||||
|
opacity : 0.8;
|
||||||
}
|
}
|
||||||
i.dismiss{
|
button.dismiss {
|
||||||
position : absolute;
|
position : absolute;
|
||||||
top : 10px;
|
top : 10px;
|
||||||
right : 10px;
|
right : 10px;
|
||||||
cursor : pointer;
|
cursor : pointer;
|
||||||
|
background-color : transparent;
|
||||||
opacity : 0.6;
|
opacity : 0.6;
|
||||||
&:hover{
|
&:hover { opacity : 1; }
|
||||||
opacity : 1;
|
|
||||||
}
|
}
|
||||||
}
|
small {
|
||||||
small{
|
|
||||||
opacity : 0.7;
|
|
||||||
font-size : 0.6em;
|
font-size : 0.6em;
|
||||||
|
opacity : 0.7;
|
||||||
}
|
}
|
||||||
h3{
|
h3 {
|
||||||
font-size : 1.1em;
|
font-size : 1.1em;
|
||||||
font-weight : 800;
|
font-weight : 800;
|
||||||
}
|
}
|
||||||
ul{
|
ul {
|
||||||
margin-top : 15px;
|
margin-top : 15px;
|
||||||
font-size : 0.8em;
|
font-size : 0.8em;
|
||||||
list-style-position : outside;
|
list-style-position : outside;
|
||||||
list-style-type : disc;
|
list-style-type : disc;
|
||||||
li{
|
li {
|
||||||
font-size : 0.8em;
|
font-size : 0.8em;
|
||||||
line-height : 1.6em;
|
line-height : 1.6em;
|
||||||
em{
|
em { font-weight : 800; }
|
||||||
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,11 +3,11 @@ require('./codeEditor.less');
|
|||||||
const React = require('react');
|
const React = require('react');
|
||||||
const createClass = require('create-react-class');
|
const createClass = require('create-react-class');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const cx = require('classnames');
|
|
||||||
const closeTag = require('./close-tag');
|
const closeTag = require('./close-tag');
|
||||||
|
const autoCompleteEmoji = require('./autocompleteEmoji');
|
||||||
|
|
||||||
let CodeMirror;
|
let CodeMirror;
|
||||||
if(typeof navigator !== 'undefined'){
|
if(typeof window !== 'undefined'){
|
||||||
CodeMirror = require('codemirror');
|
CodeMirror = require('codemirror');
|
||||||
|
|
||||||
//Language Modes
|
//Language Modes
|
||||||
@@ -36,9 +36,13 @@ if(typeof navigator !== 'undefined'){
|
|||||||
//XML code folding is a requirement of the auto-closing tag feature and is not enabled
|
//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/fold/xml-fold.js');
|
||||||
require('codemirror/addon/edit/closetag.js');
|
require('codemirror/addon/edit/closetag.js');
|
||||||
|
//Autocompletion
|
||||||
|
require('codemirror/addon/hint/show-hint.js');
|
||||||
|
|
||||||
const foldCode = require('./fold-code');
|
const foldPagesCode = require('./fold-pages');
|
||||||
foldCode.registerHomebreweryHelper(CodeMirror);
|
foldPagesCode.registerHomebreweryHelper(CodeMirror);
|
||||||
|
const foldCSSCode = require('./fold-css');
|
||||||
|
foldCSSCode.registerHomebreweryHelper(CodeMirror);
|
||||||
}
|
}
|
||||||
|
|
||||||
const CodeEditor = createClass({
|
const CodeEditor = createClass({
|
||||||
@@ -49,7 +53,8 @@ const CodeEditor = createClass({
|
|||||||
value : '',
|
value : '',
|
||||||
wrap : true,
|
wrap : true,
|
||||||
onChange : ()=>{},
|
onChange : ()=>{},
|
||||||
enableFolding : true
|
enableFolding : true,
|
||||||
|
editorTheme : 'default'
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -59,6 +64,8 @@ const CodeEditor = createClass({
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
editor : React.createRef(null),
|
||||||
|
|
||||||
componentDidMount : function() {
|
componentDidMount : function() {
|
||||||
this.buildEditor();
|
this.buildEditor();
|
||||||
const newDoc = CodeMirror.Doc(this.props.value, this.props.language);
|
const newDoc = CodeMirror.Doc(this.props.value, this.props.language);
|
||||||
@@ -91,10 +98,14 @@ const CodeEditor = createClass({
|
|||||||
} else {
|
} else {
|
||||||
this.codeMirror.setOption('foldOptions', false);
|
this.codeMirror.setOption('foldOptions', false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(prevProps.editorTheme !== this.props.editorTheme){
|
||||||
|
this.codeMirror.setOption('theme', this.props.editorTheme);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
buildEditor : function() {
|
buildEditor : function() {
|
||||||
this.codeMirror = CodeMirror(this.refs.editor, {
|
this.codeMirror = CodeMirror(this.editor.current, {
|
||||||
lineNumbers : true,
|
lineNumbers : true,
|
||||||
lineWrapping : this.props.wrap,
|
lineWrapping : this.props.wrap,
|
||||||
indentWithTabs : false,
|
indentWithTabs : false,
|
||||||
@@ -107,6 +118,10 @@ const CodeEditor = createClass({
|
|||||||
'Shift-Tab' : this.dedent,
|
'Shift-Tab' : this.dedent,
|
||||||
'Ctrl-B' : this.makeBold,
|
'Ctrl-B' : this.makeBold,
|
||||||
'Cmd-B' : this.makeBold,
|
'Cmd-B' : this.makeBold,
|
||||||
|
'Shift-Ctrl-=' : this.makeSuper,
|
||||||
|
'Shift-Cmd-=' : this.makeSuper,
|
||||||
|
'Ctrl-=' : this.makeSub,
|
||||||
|
'Cmd-=' : this.makeSub,
|
||||||
'Ctrl-I' : this.makeItalic,
|
'Ctrl-I' : this.makeItalic,
|
||||||
'Cmd-I' : this.makeItalic,
|
'Cmd-I' : this.makeItalic,
|
||||||
'Ctrl-U' : this.makeUnderline,
|
'Ctrl-U' : this.makeUnderline,
|
||||||
@@ -159,6 +174,7 @@ const CodeEditor = createClass({
|
|||||||
autoCloseTags : true,
|
autoCloseTags : true,
|
||||||
styleActiveLine : true,
|
styleActiveLine : true,
|
||||||
showTrailingSpace : false,
|
showTrailingSpace : false,
|
||||||
|
theme : this.props.editorTheme
|
||||||
// specialChars : / /,
|
// specialChars : / /,
|
||||||
// specialCharPlaceholder : function(char) {
|
// specialCharPlaceholder : function(char) {
|
||||||
// const el = document.createElement('span');
|
// const el = document.createElement('span');
|
||||||
@@ -167,7 +183,10 @@ const CodeEditor = createClass({
|
|||||||
// return el;
|
// return el;
|
||||||
// }
|
// }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Add custom behaviors (auto-close curlies and auto-complete emojis)
|
||||||
closeTag.autoCloseCurlyBraces(CodeMirror, this.codeMirror);
|
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.
|
// 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());});
|
this.codeMirror.on('change', (cm)=>{this.props.onChange(cm.getValue());});
|
||||||
@@ -176,7 +195,7 @@ const CodeEditor = createClass({
|
|||||||
|
|
||||||
indent : function () {
|
indent : function () {
|
||||||
const cm = this.codeMirror;
|
const cm = this.codeMirror;
|
||||||
if (cm.somethingSelected()) {
|
if(cm.somethingSelected()) {
|
||||||
cm.execCommand('indentMore');
|
cm.execCommand('indentMore');
|
||||||
} else {
|
} else {
|
||||||
cm.execCommand('insertSoftTab');
|
cm.execCommand('insertSoftTab');
|
||||||
@@ -213,6 +232,25 @@ const CodeEditor = createClass({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
makeSuper : function() {
|
||||||
|
const selection = this.codeMirror.getSelection(), t = selection.slice(0, 1) === '^' && selection.slice(-1) === '^';
|
||||||
|
this.codeMirror.replaceSelection(t ? selection.slice(1, -1) : `^${selection}^`, 'around');
|
||||||
|
if(selection.length === 0){
|
||||||
|
const cursor = this.codeMirror.getCursor();
|
||||||
|
this.codeMirror.setCursor({ line: cursor.line, ch: cursor.ch - 1 });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
makeSub : function() {
|
||||||
|
const selection = this.codeMirror.getSelection(), t = selection.slice(0, 2) === '^^' && selection.slice(-2) === '^^';
|
||||||
|
this.codeMirror.replaceSelection(t ? selection.slice(2, -2) : `^^${selection}^^`, 'around');
|
||||||
|
if(selection.length === 0){
|
||||||
|
const cursor = this.codeMirror.getCursor();
|
||||||
|
this.codeMirror.setCursor({ line: cursor.line, ch: cursor.ch - 2 });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
makeNbsp : function() {
|
makeNbsp : function() {
|
||||||
this.codeMirror.replaceSelection(' ', 'end');
|
this.codeMirror.replaceSelection(' ', 'end');
|
||||||
},
|
},
|
||||||
@@ -375,7 +413,7 @@ const CodeEditor = createClass({
|
|||||||
foldOptions : function(cm){
|
foldOptions : function(cm){
|
||||||
return {
|
return {
|
||||||
scanUp : true,
|
scanUp : true,
|
||||||
rangeFinder : CodeMirror.fold.homebrewery,
|
rangeFinder : this.props.language === 'css' ? CodeMirror.fold.homebrewerycss : CodeMirror.fold.homebrewery,
|
||||||
widget : (from, to)=>{
|
widget : (from, to)=>{
|
||||||
let text = '';
|
let text = '';
|
||||||
let currentLine = from.line;
|
let currentLine = from.line;
|
||||||
@@ -406,8 +444,12 @@ const CodeEditor = createClass({
|
|||||||
//----------------------//
|
//----------------------//
|
||||||
|
|
||||||
render : function(){
|
render : function(){
|
||||||
return <div className='codeEditor' ref='editor' style={this.props.style}/>;
|
return <>
|
||||||
|
<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;
|
module.exports = CodeEditor;
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,13 @@
|
|||||||
@import (less) 'codemirror/addon/fold/foldgutter.css';
|
@import (less) 'codemirror/addon/fold/foldgutter.css';
|
||||||
@import (less) 'codemirror/addon/search/matchesonscrollbar.css';
|
@import (less) 'codemirror/addon/search/matchesonscrollbar.css';
|
||||||
@import (less) 'codemirror/addon/dialog/dialog.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 {
|
@keyframes sourceMoveAnimation {
|
||||||
50% {background-color: red; color: white;}
|
50% {background-color: red; color: white;}
|
||||||
@@ -9,18 +16,32 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.codeEditor{
|
.codeEditor{
|
||||||
|
@media screen and (pointer : coarse) {
|
||||||
|
font-size : 16px;
|
||||||
|
}
|
||||||
.CodeMirror-foldmarker {
|
.CodeMirror-foldmarker {
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
text-shadow: none;
|
text-shadow: none;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: grey;
|
color: grey;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sourceMoveFlash .CodeMirror-line{
|
.sourceMoveFlash .CodeMirror-line{
|
||||||
animation-name: sourceMoveAnimation;
|
animation-name: sourceMoveAnimation;
|
||||||
animation-duration: 0.4s;
|
animation-duration: 0.4s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.CodeMirror-vscrollbar {
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 20px;
|
||||||
|
}
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
width: 20px;
|
||||||
|
background: linear-gradient(90deg, #858585 15px, #808080 15px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
//.cm-tab {
|
//.cm-tab {
|
||||||
// background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAMCAQAAACOs/baAAAARUlEQVR4nGJgIAG8JkXxUAcCtDWemcGR1lY4MvgzCEKY7jSBjgxBDAG09UEQzAe0AMwMHrSOAwEGRtpaMIwAAAAA//8DAG4ID9EKs6YqAAAAAElFTkSuQmCC) no-repeat right;
|
// background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAMCAQAAACOs/baAAAARUlEQVR4nGJgIAG8JkXxUAcCtDWemcGR1lY4MvgzCEKY7jSBjgxBDAG09UEQzAe0AMwMHrSOAwEGRtpaMIwAAAAA//8DAG4ID9EKs6YqAAAAAElFTkSuQmCC) no-repeat right;
|
||||||
//}
|
//}
|
||||||
@@ -31,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 prevLine = cm.getLine(start.line);
|
||||||
|
|
||||||
|
|
||||||
|
if(prevLine.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 FOLDING
|
||||||
|
|
||||||
|
const importMatcher = /^@import.*?[;]/;
|
||||||
|
|
||||||
|
if(prevLine.match(importMatcher)) {
|
||||||
|
return {
|
||||||
|
from : CodeMirror.Pos(start.line, 0),
|
||||||
|
to : CodeMirror.Pos(start.line, cm.getLine(start.line).length)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -2,8 +2,50 @@
|
|||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const Marked = require('marked');
|
const Marked = require('marked');
|
||||||
const MarkedExtendedTables = require('marked-extended-tables');
|
const MarkedExtendedTables = require('marked-extended-tables');
|
||||||
|
const { markedSmartypantsLite: MarkedSmartypantsLite } = require('marked-smartypants-lite');
|
||||||
const { gfmHeadingId: MarkedGFMHeadingId } = require('marked-gfm-heading-id');
|
const { gfmHeadingId: MarkedGFMHeadingId } = 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 renderer = new Marked.Renderer();
|
||||||
|
const tokenizer = new Marked.Tokenizer();
|
||||||
|
|
||||||
|
//Limit math features to simple items
|
||||||
|
const mathParser = new MathParser({
|
||||||
|
operators : {
|
||||||
|
// These default to true, but are included to be explicit
|
||||||
|
add : true,
|
||||||
|
subtract : true,
|
||||||
|
multiply : true,
|
||||||
|
divide : true,
|
||||||
|
power : true,
|
||||||
|
round : true,
|
||||||
|
floor : true,
|
||||||
|
ceil : 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,
|
||||||
|
'-' : 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,
|
||||||
|
|
||||||
|
remainder : false, factorial : false,
|
||||||
|
comparison : false, concatenate : false,
|
||||||
|
logical : false, assignment : false,
|
||||||
|
array : false, fndef : false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
//Processes the markdown within an HTML block if it's just a class-wrapper
|
//Processes the markdown within an HTML block if it's just a class-wrapper
|
||||||
renderer.html = function (html) {
|
renderer.html = function (html) {
|
||||||
@@ -16,7 +58,7 @@ renderer.html = function (html) {
|
|||||||
return 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){
|
renderer.paragraph = function(text){
|
||||||
let match;
|
let match;
|
||||||
if(text.startsWith('<div') || text.startsWith('</div'))
|
if(text.startsWith('<div') || text.startsWith('</div'))
|
||||||
@@ -27,24 +69,51 @@ renderer.paragraph = function(text){
|
|||||||
return `<p>${text}</p>\n`;
|
return `<p>${text}</p>\n`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
//Fix local links in the Preview iFrame to link inside the frame
|
||||||
|
renderer.link = function (href, title, text) {
|
||||||
|
let self = false;
|
||||||
|
if(href[0] == '#') {
|
||||||
|
self = true;
|
||||||
|
}
|
||||||
|
href = cleanUrl(this.options.sanitize, this.options.baseUrl, href);
|
||||||
|
|
||||||
|
if(href === null) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
let out = `<a href="${escape(href)}"`;
|
||||||
|
if(title) {
|
||||||
|
out += ` title="${title}"`;
|
||||||
|
}
|
||||||
|
if(self) {
|
||||||
|
out += ' target="_self"';
|
||||||
|
}
|
||||||
|
out += `>${text}</a>`;
|
||||||
|
return out;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Disable default reflink behavior, as it steps on our variables extension
|
||||||
|
tokenizer.def = function () {
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
const mustacheSpans = {
|
const mustacheSpans = {
|
||||||
name : 'mustacheSpans',
|
name : 'mustacheSpans',
|
||||||
level : 'inline', // Is this a block-level or inline-level tokenizer?
|
level : 'inline', // Is this a block-level or inline-level tokenizer?
|
||||||
start(src) { return src.match(/{{[^{]/)?.index; }, // Hint to Marked.js to stop and check for a match
|
start(src) { return src.match(/{{[^{]/)?.index; }, // Hint to Marked.js to stop and check for a match
|
||||||
tokenizer(src, tokens) {
|
tokenizer(src, tokens) {
|
||||||
const completeSpan = /^{{[^\n]*}}/; // Regex for the complete token
|
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);
|
const match = completeSpan.exec(src);
|
||||||
if(match) {
|
if(match) {
|
||||||
//Find closing delimiter
|
//Find closing delimiter
|
||||||
let blockCount = 0;
|
let blockCount = 0;
|
||||||
let tags = '';
|
let tags = {};
|
||||||
let endTags = 0;
|
let endTags = 0;
|
||||||
let endToken = 0;
|
let endToken = 0;
|
||||||
let delim;
|
let delim;
|
||||||
while (delim = inlineRegex.exec(match[0])) {
|
while (delim = inlineRegex.exec(match[0])) {
|
||||||
if(!tags) {
|
if(_.isEmpty(tags)) {
|
||||||
tags = ` ${processStyleTags(delim[0].substring(2))}`;
|
tags = processStyleTags(delim[0].substring(2));
|
||||||
endTags = delim[0].length;
|
endTags = delim[0].length;
|
||||||
}
|
}
|
||||||
if(delim[0].startsWith('{{')) {
|
if(delim[0].startsWith('{{')) {
|
||||||
@@ -73,7 +142,14 @@ const mustacheSpans = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
renderer(token) {
|
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
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -83,19 +159,19 @@ const mustacheDivs = {
|
|||||||
start(src) { return src.match(/\n *{{[^{]/m)?.index; }, // Hint to Marked.js to stop and check for a match
|
start(src) { return src.match(/\n *{{[^{]/m)?.index; }, // Hint to Marked.js to stop and check for a match
|
||||||
tokenizer(src, tokens) {
|
tokenizer(src, tokens) {
|
||||||
const completeBlock = /^ *{{[^\n}]* *\n.*\n *}}/s; // Regex for the complete token
|
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);
|
const match = completeBlock.exec(src);
|
||||||
if(match) {
|
if(match) {
|
||||||
//Find closing delimiter
|
//Find closing delimiter
|
||||||
let blockCount = 0;
|
let blockCount = 0;
|
||||||
let tags = '';
|
let tags = {};
|
||||||
let endTags = 0;
|
let endTags = 0;
|
||||||
let endToken = 0;
|
let endToken = 0;
|
||||||
let delim;
|
let delim;
|
||||||
while (delim = blockRegex.exec(match[0])?.[0].trim()) {
|
while (delim = blockRegex.exec(match[0])?.[0].trim()) {
|
||||||
if(!tags) {
|
if(_.isEmpty(tags)) {
|
||||||
tags = ` ${processStyleTags(delim.substring(2))}`;
|
tags = processStyleTags(delim.substring(2));
|
||||||
endTags = delim.length;
|
endTags = delim.length + src.indexOf(delim);
|
||||||
}
|
}
|
||||||
if(delim.startsWith('{{')) {
|
if(delim.startsWith('{{')) {
|
||||||
blockCount++;
|
blockCount++;
|
||||||
@@ -122,7 +198,14 @@ const mustacheDivs = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
renderer(token) {
|
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
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -131,30 +214,46 @@ const mustacheInjectInline = {
|
|||||||
level : 'inline',
|
level : 'inline',
|
||||||
start(src) { return src.match(/ *{[^{\n]/)?.index; }, // Hint to Marked.js to stop and check for a match
|
start(src) { return src.match(/ *{[^{\n]/)?.index; }, // Hint to Marked.js to stop and check for a match
|
||||||
tokenizer(src, tokens) {
|
tokenizer(src, tokens) {
|
||||||
const inlineRegex = /^ *{(?=((?::(?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':{}\s]*)*))\1}/g;
|
const inlineRegex = /^ *{(?=((?:[:=](?:"['\w,\-()#%=?. ]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1}/g;
|
||||||
const match = inlineRegex.exec(src);
|
const match = inlineRegex.exec(src);
|
||||||
if(match) {
|
if(match) {
|
||||||
const lastToken = tokens[tokens.length - 1];
|
const lastToken = tokens[tokens.length - 1];
|
||||||
if(!lastToken || lastToken.type == 'mustacheInjectInline')
|
if(!lastToken || lastToken.type == 'mustacheInjectInline')
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
const tags = ` ${processStyleTags(match[1])}`;
|
const tags = processStyleTags(match[1]);
|
||||||
lastToken.originalType = lastToken.type;
|
lastToken.originalType = lastToken.type;
|
||||||
lastToken.type = 'mustacheInjectInline';
|
lastToken.type = 'mustacheInjectInline';
|
||||||
lastToken.tags = tags;
|
lastToken.injectedTags = tags;
|
||||||
return {
|
return {
|
||||||
type : 'text', // Should match "name" above
|
type : 'mustacheInjectInline', // Should match "name" above
|
||||||
raw : match[0], // Text to consume from the source
|
raw : match[0], // Text to consume from the source
|
||||||
text : ''
|
text : ''
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
renderer(token) {
|
renderer(token) {
|
||||||
|
if(!token.originalType){
|
||||||
|
return;
|
||||||
|
}
|
||||||
token.type = token.originalType;
|
token.type = token.originalType;
|
||||||
const text = this.parser.parseInline([token]);
|
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) {
|
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;
|
return text;
|
||||||
}
|
}
|
||||||
@@ -166,7 +265,7 @@ const mustacheInjectBlock = {
|
|||||||
level : 'block',
|
level : 'block',
|
||||||
start(src) { return src.match(/\n *{[^{\n]/m)?.index; }, // Hint to Marked.js to stop and check for a match
|
start(src) { return src.match(/\n *{[^{\n]/m)?.index; }, // Hint to Marked.js to stop and check for a match
|
||||||
tokenizer(src, tokens) {
|
tokenizer(src, tokens) {
|
||||||
const inlineRegex = /^ *{(?=((?::(?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':{}\s]*)*))\1}/ym;
|
const inlineRegex = /^ *{(?=((?:[:=](?:"['\w,\-()#%=?. ]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1}/ym;
|
||||||
const match = inlineRegex.exec(src);
|
const match = inlineRegex.exec(src);
|
||||||
if(match) {
|
if(match) {
|
||||||
const lastToken = tokens[tokens.length - 1];
|
const lastToken = tokens[tokens.length - 1];
|
||||||
@@ -174,7 +273,7 @@ const mustacheInjectBlock = {
|
|||||||
return false;
|
return false;
|
||||||
|
|
||||||
lastToken.originalType = 'mustacheInjectBlock';
|
lastToken.originalType = 'mustacheInjectBlock';
|
||||||
lastToken.tags = ` ${processStyleTags(match[1])}`;
|
lastToken.injectedTags = processStyleTags(match[1]);
|
||||||
return {
|
return {
|
||||||
type : 'mustacheInjectBlock', // Should match "name" above
|
type : 'mustacheInjectBlock', // Should match "name" above
|
||||||
raw : match[0], // Text to consume from the source
|
raw : match[0], // Text to consume from the source
|
||||||
@@ -188,9 +287,22 @@ const mustacheInjectBlock = {
|
|||||||
}
|
}
|
||||||
token.type = token.originalType;
|
token.type = token.originalType;
|
||||||
const text = this.parser.parse([token]);
|
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) {
|
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;
|
return text;
|
||||||
}
|
}
|
||||||
@@ -205,25 +317,62 @@ const mustacheInjectBlock = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const definitionLists = {
|
const superSubScripts = {
|
||||||
name : 'definitionLists',
|
name : 'superSubScript',
|
||||||
|
level : 'inline',
|
||||||
|
start(src) { return src.match(/\^/m)?.index; }, // Hint to Marked.js to stop and check for a match
|
||||||
|
tokenizer(src, tokens) {
|
||||||
|
const superRegex = /^\^(?!\s)(?=([^\n\^]*[^\s\^]))\1\^/m;
|
||||||
|
const subRegex = /^\^\^(?!\s)(?=([^\n\^]*[^\s\^]))\1\^\^/m;
|
||||||
|
let isSuper = false;
|
||||||
|
let match = subRegex.exec(src);
|
||||||
|
if(!match){
|
||||||
|
match = superRegex.exec(src);
|
||||||
|
if(match)
|
||||||
|
isSuper = true;
|
||||||
|
}
|
||||||
|
if(match?.length) {
|
||||||
|
return {
|
||||||
|
type : 'superSubScript', // Should match "name" above
|
||||||
|
raw : match[0], // Text to consume from the source
|
||||||
|
tag : isSuper ? 'sup' : 'sub',
|
||||||
|
tokens : this.lexer.inlineTokens(match[1])
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
renderer(token) {
|
||||||
|
return `<${token.tag}>${this.parser.parseInline(token.tokens)}</${token.tag}>`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const definitionListsSingleLine = {
|
||||||
|
name : 'definitionListsSingleLine',
|
||||||
level : 'block',
|
level : 'block',
|
||||||
start(src) { return src.match(/^.*?::.*/m)?.index; }, // Hint to Marked.js to stop and check for a match
|
start(src) { return src.match(/\n[^\n]*?::[^\n]*/m)?.index; }, // Hint to Marked.js to stop and check for a match
|
||||||
tokenizer(src, tokens) {
|
tokenizer(src, tokens) {
|
||||||
const regex = /^([^\n]*?)::([^\n]*)(?:\n|$)/ym;
|
const regex = /^([^\n]*?)::([^\n]*)(?:\n|$)/ym;
|
||||||
let match;
|
let match;
|
||||||
let endIndex = 0;
|
let endIndex = 0;
|
||||||
const definitions = [];
|
const definitions = [];
|
||||||
while (match = regex.exec(src)) {
|
while (match = regex.exec(src)) {
|
||||||
|
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({
|
definitions.push({
|
||||||
dt : this.lexer.inlineTokens(match[1].trim()),
|
dt : this.lexer.inlineTokens(originalLine.slice(0, newMatch[1].length).trim()),
|
||||||
dd : this.lexer.inlineTokens(match[2].trim())
|
dd : this.lexer.inlineTokens(originalLine.slice(newMatch[1].length + 2).trim())
|
||||||
});
|
});
|
||||||
|
} // End of emoji hack.
|
||||||
endIndex = regex.lastIndex;
|
endIndex = regex.lastIndex;
|
||||||
}
|
}
|
||||||
if(definitions.length) {
|
if(definitions.length) {
|
||||||
return {
|
return {
|
||||||
type : 'definitionLists',
|
type : 'definitionListsSingleLine',
|
||||||
raw : src.slice(0, endIndex),
|
raw : src.slice(0, endIndex),
|
||||||
definitions
|
definitions
|
||||||
};
|
};
|
||||||
@@ -237,71 +386,320 @@ const definitionLists = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const MarkedSmartyPantsLite = ()=>{
|
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
|
||||||
|
tokenizer(src, tokens) {
|
||||||
|
const regex = /(\n?\n?(?!::)[^\n]+?(?=\n::))|\n::(.(?:.|\n)*?(?=(?:\n::)|(?:\n\n)|$))/y;
|
||||||
|
let match;
|
||||||
|
let endIndex = 0;
|
||||||
|
const definitions = [];
|
||||||
|
while (match = regex.exec(src)) {
|
||||||
|
if(match[1]) {
|
||||||
|
if(this.lexer.blockTokens(match[1].trim())[0]?.type !== 'paragraph') // DT must not be another block-level token besides <p>
|
||||||
|
break;
|
||||||
|
definitions.push({
|
||||||
|
dt : this.lexer.inlineTokens(match[1].trim()),
|
||||||
|
dds : []
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if(match[2] && definitions.length) {
|
||||||
|
definitions[definitions.length - 1].dds.push(
|
||||||
|
this.lexer.inlineTokens(match[2].trim().replace(/\s/g, ' '))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
endIndex = regex.lastIndex;
|
||||||
|
}
|
||||||
|
if(definitions.length) {
|
||||||
return {
|
return {
|
||||||
tokenizer : {
|
type : 'definitionListsMultiLine',
|
||||||
inlineText(src) {
|
raw : src.slice(0, endIndex),
|
||||||
// don't escape inlineText
|
definitions
|
||||||
const cap = this.rules.inline.text.exec(src);
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
renderer(token) {
|
||||||
|
let returnVal = `<dl>`;
|
||||||
|
token.definitions.forEach((def)=>{
|
||||||
|
const dds = def.dds.map((s)=>{
|
||||||
|
return `\n<dd>${this.parser.parseInline(s).trim()}</dd>`;
|
||||||
|
}).join('');
|
||||||
|
returnVal += `<dt>${this.parser.parseInline(def.dt)}</dt>${dds}\n`;
|
||||||
|
});
|
||||||
|
returnVal = returnVal.trim();
|
||||||
|
return `${returnVal}</dl>`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/* istanbul ignore next */
|
//v=====--------------------< Variable Handling >-------------------=====v// 242 lines
|
||||||
if(!cap) {
|
const replaceVar = function(input, hoist=false, allowUnresolved=false) {
|
||||||
// should never happen
|
const regex = /([!$]?)\[((?!\s*\])(?:\\.|[^\[\]\\])+)\]/g;
|
||||||
return;
|
const match = regex.exec(input);
|
||||||
|
|
||||||
|
const prefix = match[1];
|
||||||
|
const label = match[2];
|
||||||
|
|
||||||
|
//v=====--------------------< HANDLE MATH >-------------------=====v//
|
||||||
|
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
|
||||||
|
|
||||||
|
let replacedLabel = label;
|
||||||
|
|
||||||
|
if(prefix[0] == '$' && mathVars?.[0] !== label.trim()) {// If there was mathy stuff not captured, let's do math!
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
return mathParser.evaluate(replacedLabel);
|
||||||
|
} catch (error) {
|
||||||
|
return undefined; // Return undefined if invalid math result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//^=====--------------------< HANDLE MATH >-------------------=====^//
|
||||||
|
|
||||||
|
const foundVar = lookupVar(label, globalPageNumber, hoist);
|
||||||
|
|
||||||
|
if(!foundVar || (!foundVar.resolved && !allowUnresolved))
|
||||||
|
return undefined; // Return undefined if not found, or parially-resolved vars are not allowed
|
||||||
|
|
||||||
|
// url or <url> "title" or 'title' or (title)
|
||||||
|
const linkRegex = /^([^<\s][^\s]*|<.*?>)(?: ("(?:\\"|[^"])*"|'(?:\\'|[^'])*'|\((?:\\\(|\\\)|[^()])*\)))?$/m;
|
||||||
|
const linkMatch = linkRegex.exec(foundVar.content);
|
||||||
|
|
||||||
|
const href = linkMatch ? linkMatch[1] : null; //TODO: TRIM OFF < > IF PRESENT
|
||||||
|
const title = linkMatch ? linkMatch[2]?.slice(1, -1) : null;
|
||||||
|
|
||||||
|
if(!prefix[0] && href) // Link
|
||||||
|
return `[${label}](${href}${title ? ` "${title}"` : ''})`;
|
||||||
|
|
||||||
|
if(prefix[0] == '!' && href) // Image
|
||||||
|
return ``;
|
||||||
|
|
||||||
|
if(prefix[0] == '$') // Variable
|
||||||
|
return foundVar.content;
|
||||||
|
};
|
||||||
|
|
||||||
|
const lookupVar = function(label, index, hoist=false) {
|
||||||
|
while (index >= 0) {
|
||||||
|
if(globalVarsList[index]?.[label] !== undefined)
|
||||||
|
return globalVarsList[index][label];
|
||||||
|
index--;
|
||||||
}
|
}
|
||||||
|
|
||||||
const text = cap[0]
|
if(hoist) { //If normal lookup failed, attempt hoisting
|
||||||
// em-dashes
|
index = Object.keys(globalVarsList).length; // Move index to start from last page
|
||||||
.replace(/---/g, '\u2014')
|
while (index >= 0) {
|
||||||
// en-dashes
|
if(globalVarsList[index]?.[label] !== undefined)
|
||||||
.replace(/--/g, '\u2013')
|
return globalVarsList[index][label];
|
||||||
// opening singles
|
index--;
|
||||||
.replace(/(^|[-\u2014/(\[{"\s])'/g, '$1\u2018')
|
}
|
||||||
// closing singles & apostrophes
|
}
|
||||||
.replace(/'/g, '\u2019')
|
|
||||||
// opening doubles
|
|
||||||
.replace(/(^|[-\u2014/(\[{\u2018\s])"/g, '$1\u201c')
|
|
||||||
// closing doubles
|
|
||||||
.replace(/"/g, '\u201d')
|
|
||||||
// ellipses
|
|
||||||
.replace(/\.{3}/g, '\u2026');
|
|
||||||
|
|
||||||
return {
|
return undefined;
|
||||||
type : 'text',
|
};
|
||||||
raw : cap[0],
|
|
||||||
text : text
|
const processVariableQueue = function() {
|
||||||
|
let resolvedOne = true;
|
||||||
|
let finalLoop = false;
|
||||||
|
while (resolvedOne || finalLoop) { // Loop through queue until no more variable calls can be resolved
|
||||||
|
resolvedOne = false;
|
||||||
|
for (const item of varsQueue) {
|
||||||
|
if(item.type == 'text')
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if(item.type == 'varDefBlock') {
|
||||||
|
const regex = /[!$]?\[((?!\s*\])(?:\\.|[^\[\]\\])+)\]/g;
|
||||||
|
let match;
|
||||||
|
let resolved = true;
|
||||||
|
let tempContent = item.content;
|
||||||
|
while (match = regex.exec(item.content)) { // regex to find variable calls
|
||||||
|
const value = replaceVar(match[0], true);
|
||||||
|
|
||||||
|
if(value == undefined)
|
||||||
|
resolved = false;
|
||||||
|
else
|
||||||
|
tempContent = tempContent.replaceAll(match[0], value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(resolved == true || item.content != tempContent) {
|
||||||
|
resolvedOne = true;
|
||||||
|
item.content = tempContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
globalVarsList[globalPageNumber][item.varName] = {
|
||||||
|
content : item.content,
|
||||||
|
resolved : resolved
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if(resolved)
|
||||||
|
item.type = 'resolved';
|
||||||
|
}
|
||||||
|
|
||||||
|
if(item.type == 'varCallBlock' || item.type == 'varCallInline') {
|
||||||
|
const value = replaceVar(item.content, true, finalLoop); // final loop will just use the best value so far
|
||||||
|
|
||||||
|
if(value == undefined)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
resolvedOne = true;
|
||||||
|
item.content = value;
|
||||||
|
item.type = 'text';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
varsQueue = varsQueue.filter((item)=>item.type !== 'resolved'); // Remove any fully-resolved variable definitions
|
||||||
|
|
||||||
|
if(finalLoop)
|
||||||
|
break;
|
||||||
|
if(!resolvedOne)
|
||||||
|
finalLoop = true;
|
||||||
|
}
|
||||||
|
varsQueue = varsQueue.filter((item)=>item.type !== 'varDefBlock');
|
||||||
|
};
|
||||||
|
|
||||||
|
function MarkedVariables() {
|
||||||
|
return {
|
||||||
|
hooks : {
|
||||||
|
preprocess(src) {
|
||||||
|
const codeBlockSkip = /^(?: {4}[^\n]+(?:\n(?: *(?:\n|$))*)?)+|^ {0,3}(`{3,}(?=[^`\n]*(?:\n|$))|~{3,})(?:[^\n]*)(?:\n|$)(?:|(?:[\s\S]*?)(?:\n|$))(?: {0,3}\2[~`]* *(?=\n|$))|`[^`]*?`/;
|
||||||
|
const blockDefRegex = /^[!$]?\[((?!\s*\])(?:\\.|[^\[\]\\])+)\]:(?!\() *((?:\n? *[^\s].*)+)(?=\n+|$)/; //Matches 3, [4]:5
|
||||||
|
const blockCallRegex = /^[!$]?\[((?!\s*\])(?:\\.|[^\[\]\\])+)\](?=\n|$)/; //Matches 6, [7]
|
||||||
|
const inlineDefRegex = /([!$]?\[((?!\s*\])(?:\\.|[^\[\]\\])+)\])\(([^\n]+)\)/; //Matches 8, 9[10](11)
|
||||||
|
const inlineCallRegex = /[!$]?\[((?!\s*\])(?:\\.|[^\[\]\\])+)\](?!\()/; //Matches 12, [13]
|
||||||
|
|
||||||
|
// Combine regexes and wrap in parens like so: (regex1)|(regex2)|(regex3)|(regex4)
|
||||||
|
const combinedRegex = new RegExp([codeBlockSkip, blockDefRegex, blockCallRegex, inlineDefRegex, inlineCallRegex].map((s)=>`(${s.source})`).join('|'), 'gm');
|
||||||
|
|
||||||
|
let lastIndex = 0;
|
||||||
|
let match;
|
||||||
|
while ((match = combinedRegex.exec(src)) !== null) {
|
||||||
|
// Format any matches into tokens and store
|
||||||
|
if(match.index > lastIndex) { // Any non-variable stuff
|
||||||
|
varsQueue.push(
|
||||||
|
{ type : 'text',
|
||||||
|
varName : null,
|
||||||
|
content : src.slice(lastIndex, match.index)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if(match[1]) {
|
||||||
|
varsQueue.push(
|
||||||
|
{ type : 'text',
|
||||||
|
varName : null,
|
||||||
|
content : match[0]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if(match[3]) { // Block Definition
|
||||||
|
const label = match[4] ? match[4].trim().replace(/\s+/g, ' ') : null; // Trim edge spaces and shorten blocks of whitespace to 1 space
|
||||||
|
const content = match[5] ? match[5].trim().replace(/[ \t]+/g, ' ') : null; // Trim edge spaces and shorten blocks of whitespace to 1 space
|
||||||
|
|
||||||
|
varsQueue.push(
|
||||||
|
{ type : 'varDefBlock',
|
||||||
|
varName : label,
|
||||||
|
content : content
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if(match[6]) { // Block Call
|
||||||
|
const label = match[7] ? match[7].trim().replace(/\s+/g, ' ') : null; // Trim edge spaces and shorten blocks of whitespace to 1 space
|
||||||
|
|
||||||
|
varsQueue.push(
|
||||||
|
{ type : 'varCallBlock',
|
||||||
|
varName : label,
|
||||||
|
content : match[0]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if(match[8]) { // Inline Definition
|
||||||
|
const label = match[10] ? match[10].trim().replace(/\s+/g, ' ') : null; // Trim edge spaces and shorten blocks of whitespace to 1 space
|
||||||
|
let content = match[11] ? match[11].trim().replace(/\s+/g, ' ') : null; // Trim edge spaces and shorten blocks of whitespace to 1 space
|
||||||
|
|
||||||
|
// In case of nested (), find the correct matching end )
|
||||||
|
let level = 0;
|
||||||
|
let i;
|
||||||
|
for (i = 0; i < content.length; i++) {
|
||||||
|
if(content[i] === '\\') {
|
||||||
|
i++;
|
||||||
|
} else if(content[i] === '(') {
|
||||||
|
level++;
|
||||||
|
} else if(content[i] === ')') {
|
||||||
|
level--;
|
||||||
|
if(level < 0)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(i > -1) {
|
||||||
|
combinedRegex.lastIndex = combinedRegex.lastIndex - (content.length - i);
|
||||||
|
content = content.slice(0, i).trim().replace(/\s+/g, ' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
varsQueue.push(
|
||||||
|
{ type : 'varDefBlock',
|
||||||
|
varName : label,
|
||||||
|
content : content
|
||||||
|
});
|
||||||
|
varsQueue.push(
|
||||||
|
{ type : 'varCallInline',
|
||||||
|
varName : label,
|
||||||
|
content : match[9]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if(match[12]) { // Inline Call
|
||||||
|
const label = match[13] ? match[13].trim().replace(/\s+/g, ' ') : null; // Trim edge spaces and shorten blocks of whitespace to 1 space
|
||||||
|
|
||||||
|
varsQueue.push(
|
||||||
|
{ type : 'varCallInline',
|
||||||
|
varName : label,
|
||||||
|
content : match[0]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
lastIndex = combinedRegex.lastIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(lastIndex < src.length) {
|
||||||
|
varsQueue.push(
|
||||||
|
{ type : 'text',
|
||||||
|
varName : null,
|
||||||
|
content : src.slice(lastIndex)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
processVariableQueue();
|
||||||
|
|
||||||
|
const output = varsQueue.map((item)=>item.content).join('');
|
||||||
|
varsQueue = []; // Must clear varsQueue because custom HTML renderer uses Marked.parse which will preprocess again without clearing the array
|
||||||
|
return output;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
//^=====--------------------< Variable Handling >-------------------=====^//
|
||||||
|
|
||||||
Marked.use({ extensions: [mustacheSpans, mustacheDivs, mustacheInjectInline, definitionLists] });
|
// 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(mustacheInjectBlock);
|
Marked.use(mustacheInjectBlock);
|
||||||
Marked.use({ renderer: renderer, mangle: false });
|
Marked.use({ renderer: renderer, tokenizer: tokenizer, mangle: false });
|
||||||
Marked.use(MarkedExtendedTables(), MarkedGFMHeadingId(), MarkedSmartyPantsLite());
|
Marked.use(MarkedExtendedTables(), MarkedGFMHeadingId(), MarkedSmartypantsLite(), MarkedEmojis(MarkedEmojiOptions));
|
||||||
|
|
||||||
//Fix local links in the Preview iFrame to link inside the frame
|
|
||||||
renderer.link = function (href, title, text) {
|
|
||||||
let self = false;
|
|
||||||
if(href[0] == '#') {
|
|
||||||
self = true;
|
|
||||||
}
|
|
||||||
href = cleanUrl(this.options.sanitize, this.options.baseUrl, href);
|
|
||||||
|
|
||||||
if(href === null) {
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
let out = `<a href="${escape(href)}"`;
|
|
||||||
if(title) {
|
|
||||||
out += ` title="${title}"`;
|
|
||||||
}
|
|
||||||
if(self) {
|
|
||||||
out += ' target="_self"';
|
|
||||||
}
|
|
||||||
out += `>${text}</a>`;
|
|
||||||
return out;
|
|
||||||
};
|
|
||||||
|
|
||||||
const nonWordAndColonTest = /[^\w:]/g;
|
const nonWordAndColonTest = /[^\w:]/g;
|
||||||
const cleanUrl = function (sanitize, base, href) {
|
const cleanUrl = function (sanitize, base, href) {
|
||||||
@@ -351,12 +749,6 @@ const escape = function (html, encode) {
|
|||||||
return html;
|
return html;
|
||||||
};
|
};
|
||||||
|
|
||||||
const sanatizeScriptTags = (content)=>{
|
|
||||||
return content
|
|
||||||
.replace(/<script/ig, '<script')
|
|
||||||
.replace(/<\/script>/ig, '</script>');
|
|
||||||
};
|
|
||||||
|
|
||||||
const tagTypes = ['div', 'span', 'a'];
|
const tagTypes = ['div', 'span', 'a'];
|
||||||
const tagRegex = new RegExp(`(${
|
const tagRegex = new RegExp(`(${
|
||||||
_.map(tagTypes, (type)=>{
|
_.map(tagTypes, (type)=>{
|
||||||
@@ -370,24 +762,77 @@ const voidTags = new Set([
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
const processStyleTags = (string)=>{
|
const processStyleTags = (string)=>{
|
||||||
//split tags up. quotes can only occur right after colons.
|
//split tags up. quotes can only occur right after : or =.
|
||||||
//TODO: can we simplify to just split on commas?
|
//TODO: can we simplify to just split on commas?
|
||||||
const tags = string.match(/(?:[^, ":]+|:(?:"[^"]*"|))+/g);
|
const tags = string.match(/(?:[^, ":=]+|[:=](?:"[^"]*"|))+/g);
|
||||||
|
|
||||||
if(!tags) return '"';
|
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;
|
||||||
|
|
||||||
const id = _.remove(tags, (tag)=>tag.startsWith('#')).map((tag)=>tag.slice(1))[0];
|
return {
|
||||||
const classes = _.remove(tags, (tag)=>!tag.includes(':'));
|
id : id,
|
||||||
const styles = tags.map((tag)=>tag.replace(/:"?([^"]*)"?/g, ':$1;'));
|
classes : classes,
|
||||||
return `${classes.join(' ')}" ${id ? `id="${id}"` : ''} ${styles.length ? `style="${styles.join(' ')}"` : ''}`;
|
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 = {};
|
||||||
|
let varsQueue = [];
|
||||||
|
let globalPageNumber = 0;
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
marked : Marked,
|
marked : Marked,
|
||||||
render : (rawBrewText)=>{
|
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;
|
||||||
|
|
||||||
rawBrewText = rawBrewText.replace(/^\\column$/gm, `\n<div class='columnSplit'></div>\n`)
|
rawBrewText = rawBrewText.replace(/^\\column$/gm, `\n<div class='columnSplit'></div>\n`)
|
||||||
.replace(/^(:+)$/gm, (match)=>`${`<div class='blank'></div>`.repeat(match.length)}\n`);
|
.replace(/^(:+)$/gm, (match)=>`${`<div class='blank'></div>`.repeat(match.length)}\n`);
|
||||||
return Marked.parse(sanatizeScriptTags(rawBrewText));
|
const opts = Marked.defaults;
|
||||||
|
|
||||||
|
rawBrewText = opts.hooks.preprocess(rawBrewText);
|
||||||
|
const tokens = Marked.lexer(rawBrewText, opts);
|
||||||
|
|
||||||
|
Marked.walkTokens(tokens, opts.walkTokens);
|
||||||
|
|
||||||
|
const html = Marked.parser(tokens, opts);
|
||||||
|
return opts.hooks.postprocess(html);
|
||||||
},
|
},
|
||||||
|
|
||||||
validate : (rawBrewText)=>{
|
validate : (rawBrewText)=>{
|
||||||
|
|||||||
@@ -90,12 +90,6 @@ const escape = function (html, encode) {
|
|||||||
return html;
|
return html;
|
||||||
};
|
};
|
||||||
|
|
||||||
const sanatizeScriptTags = (content)=>{
|
|
||||||
return content
|
|
||||||
.replace(/<script/ig, '<script')
|
|
||||||
.replace(/<\/script>/ig, '</script>');
|
|
||||||
};
|
|
||||||
|
|
||||||
const tagTypes = ['div', 'span', 'a'];
|
const tagTypes = ['div', 'span', 'a'];
|
||||||
const tagRegex = new RegExp(`(${
|
const tagRegex = new RegExp(`(${
|
||||||
_.map(tagTypes, (type)=>{
|
_.map(tagTypes, (type)=>{
|
||||||
@@ -113,7 +107,7 @@ module.exports = {
|
|||||||
marked : Markdown,
|
marked : Markdown,
|
||||||
render : (rawBrewText)=>{
|
render : (rawBrewText)=>{
|
||||||
return Markdown(
|
return Markdown(
|
||||||
sanatizeScriptTags(rawBrewText),
|
rawBrewText,
|
||||||
{ renderer: renderer }
|
{ renderer: renderer }
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
require('./nav.less');
|
require('client/homebrew/navbar/navbar.less');
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
|
const { useState, useRef, useEffect } = React;
|
||||||
const createClass = require('create-react-class');
|
const createClass = require('create-react-class');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const cx = require('classnames');
|
const cx = require('classnames');
|
||||||
@@ -46,8 +47,8 @@ const Nav = {
|
|||||||
color : null
|
color : null
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
handleClick : function(){
|
handleClick : function(e){
|
||||||
this.props.onClick();
|
this.props.onClick(e);
|
||||||
},
|
},
|
||||||
render : function(){
|
render : function(){
|
||||||
const classes = cx('navItem', this.props.color, this.props.className);
|
const classes = cx('navItem', this.props.color, this.props.className);
|
||||||
@@ -71,64 +72,49 @@ const Nav = {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
dropdown : createClass({
|
dropdown : function dropdown(props) {
|
||||||
displayName : 'Nav.dropdown',
|
props = Object.assign({}, props, {
|
||||||
getDefaultProps : function() {
|
trigger : 'hover click'
|
||||||
return {
|
|
||||||
trigger : 'hover'
|
|
||||||
};
|
|
||||||
},
|
|
||||||
getInitialState : function() {
|
|
||||||
return {
|
|
||||||
showDropdown : false
|
|
||||||
};
|
|
||||||
},
|
|
||||||
componentDidMount : function() {
|
|
||||||
if(this.props.trigger == 'click')
|
|
||||||
document.addEventListener('click', this.handleClickOutside);
|
|
||||||
},
|
|
||||||
componentWillUnmount : function() {
|
|
||||||
if(this.props.trigger == 'click')
|
|
||||||
document.removeEventListener('click', this.handleClickOutside);
|
|
||||||
},
|
|
||||||
handleClickOutside : function(e){
|
|
||||||
// Close dropdown when clicked outside
|
|
||||||
if(this.refs.dropdown && !this.refs.dropdown.contains(e.target)) {
|
|
||||||
this.handleDropdown(false);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
handleDropdown : function(show){
|
|
||||||
this.setState({
|
|
||||||
showDropdown : show
|
|
||||||
});
|
});
|
||||||
},
|
|
||||||
renderDropdown : function(dropdownChildren){
|
|
||||||
if(!this.state.showDropdown) return null;
|
|
||||||
|
|
||||||
return (
|
const myRef = useRef(null);
|
||||||
<div className='navDropdown'>
|
const [showDropdown, setShowDropdown] = useState(false);
|
||||||
{dropdownChildren}
|
|
||||||
</div>
|
useEffect(()=>{
|
||||||
);
|
document.addEventListener('click', handleClickOutside);
|
||||||
},
|
return ()=>{
|
||||||
render : function () {
|
document.removeEventListener('click', handleClickOutside);
|
||||||
const dropdownChildren = React.Children.map(this.props.children, (child, i)=>{
|
};
|
||||||
// Ignore the first child
|
}, []);
|
||||||
|
|
||||||
|
function handleClickOutside(e) {
|
||||||
|
// Close dropdown when clicked outside
|
||||||
|
if(!myRef.current?.contains(e.target)) {
|
||||||
|
handleDropdown(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDropdown(show) {
|
||||||
|
setShowDropdown(show ?? !showDropdown);
|
||||||
|
}
|
||||||
|
|
||||||
|
const dropdownChildren = React.Children.map(props.children, (child, i)=>{
|
||||||
if(i < 1) return;
|
if(i < 1) return;
|
||||||
return child;
|
return child;
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`navDropdownContainer ${this.props.className}`}
|
<div className={`navDropdownContainer ${props.className ?? ''}`}
|
||||||
ref='dropdown'
|
ref={myRef}
|
||||||
onMouseEnter={this.props.trigger == 'hover' ? ()=>{this.handleDropdown(true);} : undefined}
|
onMouseEnter = { props.trigger.includes('hover') ? ()=>handleDropdown(true) : undefined }
|
||||||
onClick= {this.props.trigger == 'click' ? ()=>{this.handleDropdown(true);} : undefined}
|
onMouseLeave = { props.trigger.includes('hover') ? ()=>handleDropdown(false) : undefined }
|
||||||
onMouseLeave={this.props.trigger == 'hover' ? ()=>{this.handleDropdown(false);} : undefined}>
|
onClick = { props.trigger.includes('click') ? ()=>handleDropdown(true) : undefined }
|
||||||
{this.props.children[0] || this.props.children /*children is not an array when only one child*/}
|
>
|
||||||
{this.renderDropdown(dropdownChildren)}
|
{props.children[0] || props.children /*children is not an array when only one child*/}
|
||||||
|
{showDropdown && <div className='navDropdown'>{dropdownChildren}</div>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,95 +0,0 @@
|
|||||||
@import '../styles/colors';
|
|
||||||
@keyframes glideDropDown {
|
|
||||||
0% {transform : translate(0px, -100%);
|
|
||||||
opacity : 0;
|
|
||||||
background-color: #333;}
|
|
||||||
100% {transform : translate(0px, 0px);
|
|
||||||
opacity : 1;
|
|
||||||
background-color: #333;}
|
|
||||||
}
|
|
||||||
nav{
|
|
||||||
background-color : #333;
|
|
||||||
.navContent{
|
|
||||||
position : relative;
|
|
||||||
display : flex;
|
|
||||||
justify-content : space-between;
|
|
||||||
z-index : 2;
|
|
||||||
}
|
|
||||||
.navSection{
|
|
||||||
display : flex;
|
|
||||||
align-items : center;
|
|
||||||
}
|
|
||||||
.navLogo{
|
|
||||||
display : block;
|
|
||||||
margin-top : 0px;
|
|
||||||
margin-right : 8px;
|
|
||||||
margin-left : 8px;
|
|
||||||
color : white;
|
|
||||||
text-decoration : none;
|
|
||||||
&:hover{
|
|
||||||
.name{ color : @orange; }
|
|
||||||
svg{ fill : @orange }
|
|
||||||
}
|
|
||||||
svg{
|
|
||||||
height : 13px;
|
|
||||||
margin-right : 0.2em;
|
|
||||||
cursor : pointer;
|
|
||||||
fill : white;
|
|
||||||
}
|
|
||||||
span.name{
|
|
||||||
font-family : 'CodeLight';
|
|
||||||
font-size : 15px;
|
|
||||||
span.crit{
|
|
||||||
font-family : 'CodeBold';
|
|
||||||
}
|
|
||||||
small{
|
|
||||||
font-family : 'Open Sans';
|
|
||||||
font-size : 0.3em;
|
|
||||||
font-weight : 800;
|
|
||||||
text-transform : uppercase;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.navItem{
|
|
||||||
#backgroundColorsHover;
|
|
||||||
.animate(background-color);
|
|
||||||
padding : 8px 12px;
|
|
||||||
cursor : pointer;
|
|
||||||
background-color : #333;
|
|
||||||
font-size : 10px;
|
|
||||||
font-weight : 800;
|
|
||||||
color : white;
|
|
||||||
text-decoration : none;
|
|
||||||
text-transform : uppercase;
|
|
||||||
line-height : 13px;
|
|
||||||
i{
|
|
||||||
margin-left : 5px;
|
|
||||||
font-size : 13px;
|
|
||||||
float : right;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.navSection:last-child .navItem{
|
|
||||||
border-left : 1px solid #666;
|
|
||||||
}
|
|
||||||
.navDropdownContainer{
|
|
||||||
position: relative;
|
|
||||||
.navDropdown {
|
|
||||||
position : absolute;
|
|
||||||
top : 28px;
|
|
||||||
left : 0px;
|
|
||||||
z-index : 10000;
|
|
||||||
width : 100%;
|
|
||||||
.navItem{
|
|
||||||
animation-name: glideDropDown;
|
|
||||||
animation-duration: 0.4s;
|
|
||||||
position : relative;
|
|
||||||
display : block;
|
|
||||||
width : 100%;
|
|
||||||
vertical-align : middle;
|
|
||||||
padding : 8px 5px;
|
|
||||||
border : 1px solid #888;
|
|
||||||
border-bottom : 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
require('./splitPane.less');
|
require('./splitPane.less');
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const createClass = require('create-react-class');
|
const createClass = require('create-react-class');
|
||||||
const _ = require('lodash');
|
|
||||||
const cx = require('classnames');
|
const cx = require('classnames');
|
||||||
|
|
||||||
const SplitPane = createClass({
|
const SplitPane = createClass({
|
||||||
@@ -24,6 +23,9 @@ const SplitPane = createClass({
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
pane1 : React.createRef(null),
|
||||||
|
pane2 : React.createRef(null),
|
||||||
|
|
||||||
componentDidMount : function() {
|
componentDidMount : function() {
|
||||||
const dividerPos = window.localStorage.getItem(this.props.storageKey);
|
const dividerPos = window.localStorage.getItem(this.props.storageKey);
|
||||||
if(dividerPos){
|
if(dividerPos){
|
||||||
@@ -61,7 +63,8 @@ const SplitPane = createClass({
|
|||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
|
|
||||||
handleUp : function(){
|
handleUp : function(e){
|
||||||
|
e.preventDefault();
|
||||||
if(this.state.isDragging){
|
if(this.state.isDragging){
|
||||||
this.props.onDragFinish(this.state.currentDividerPos);
|
this.props.onDragFinish(this.state.currentDividerPos);
|
||||||
window.localStorage.setItem(this.props.storageKey, this.state.currentDividerPos);
|
window.localStorage.setItem(this.props.storageKey, this.state.currentDividerPos);
|
||||||
@@ -78,6 +81,7 @@ const SplitPane = createClass({
|
|||||||
handleMove : function(e){
|
handleMove : function(e){
|
||||||
if(!this.state.isDragging) return;
|
if(!this.state.isDragging) return;
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
const newSize = this.limitPosition(e.pageX);
|
const newSize = this.limitPosition(e.pageX);
|
||||||
this.setState({
|
this.setState({
|
||||||
currentDividerPos : newSize,
|
currentDividerPos : newSize,
|
||||||
@@ -122,7 +126,7 @@ const SplitPane = createClass({
|
|||||||
renderDivider : function(){
|
renderDivider : function(){
|
||||||
return <>
|
return <>
|
||||||
{this.renderMoveArrows()}
|
{this.renderMoveArrows()}
|
||||||
<div className='divider' onMouseDown={this.handleDown} >
|
<div className='divider' onPointerDown={this.handleDown} >
|
||||||
<div className='dots'>
|
<div className='dots'>
|
||||||
<i className='fas fa-circle' />
|
<i className='fas fa-circle' />
|
||||||
<i className='fas fa-circle' />
|
<i className='fas fa-circle' />
|
||||||
@@ -133,9 +137,8 @@ const SplitPane = createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
render : function(){
|
render : function(){
|
||||||
return <div className='splitPane' onMouseMove={this.handleMove} onMouseUp={this.handleUp}>
|
return <div className='splitPane' onPointerMove={this.handleMove} onPointerUp={this.handleUp}>
|
||||||
<Pane
|
<Pane
|
||||||
ref='pane1'
|
|
||||||
width={this.state.currentDividerPos}
|
width={this.state.currentDividerPos}
|
||||||
>
|
>
|
||||||
{React.cloneElement(this.props.children[0], {
|
{React.cloneElement(this.props.children[0], {
|
||||||
@@ -145,7 +148,7 @@ const SplitPane = createClass({
|
|||||||
})}
|
})}
|
||||||
</Pane>
|
</Pane>
|
||||||
{this.renderDivider()}
|
{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>;
|
</div>;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
flex : 1;
|
flex : 1;
|
||||||
}
|
}
|
||||||
.divider{
|
.divider{
|
||||||
|
touch-action : none;
|
||||||
display : table;
|
display : table;
|
||||||
height : 100%;
|
height : 100%;
|
||||||
width : 15px;
|
width : 15px;
|
||||||
|
|||||||
@@ -2,12 +2,6 @@
|
|||||||
|
|
||||||
const Markdown = require('naturalcrit/markdown.js');
|
const Markdown = require('naturalcrit/markdown.js');
|
||||||
|
|
||||||
test('Escapes <script> tag', function() {
|
|
||||||
const source = '<script></script>';
|
|
||||||
const rendered = Markdown.render(source);
|
|
||||||
expect(rendered).toMatch('<script></script>');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Processes the markdown within an HTML block if its just a class wrapper', function() {
|
test('Processes the markdown within an HTML block if its just a class wrapper', function() {
|
||||||
const source = '<div>*Bold text*</div>';
|
const source = '<div>*Bold text*</div>';
|
||||||
const rendered = Markdown.render(source);
|
const rendered = Markdown.render(source);
|
||||||
|
|||||||
91
tests/markdown/definition-lists.test.js
Normal file
91
tests/markdown/definition-lists.test.js
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
/* eslint-disable max-lines */
|
||||||
|
|
||||||
|
const Markdown = require('naturalcrit/markdown.js');
|
||||||
|
|
||||||
|
describe('Inline Definition Lists', ()=>{
|
||||||
|
test('No Term 1 Definition', function() {
|
||||||
|
const source = ':: My First Definition\n\n';
|
||||||
|
const rendered = Markdown.render(source).trim();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<dl><dt></dt><dd>My First Definition</dd>\n</dl>');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Single Definition Term', function() {
|
||||||
|
const source = 'My term :: My First Definition\n\n';
|
||||||
|
const rendered = Markdown.render(source).trim();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<dl><dt>My term</dt><dd>My First Definition</dd>\n</dl>');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Multiple Definition Terms', function() {
|
||||||
|
const source = 'Term 1::Definition of Term 1\nTerm 2::Definition of Term 2\n\n';
|
||||||
|
const rendered = Markdown.render(source).trim();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<dl><dt>Term 1</dt><dd>Definition of Term 1</dd>\n<dt>Term 2</dt><dd>Definition of Term 2</dd>\n</dl>');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Multiline Definition Lists', ()=>{
|
||||||
|
test('Single Term, Single Definition', function() {
|
||||||
|
const source = 'Term 1\n::Definition 1\n\n';
|
||||||
|
const rendered = Markdown.render(source).trim();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<dl><dt>Term 1</dt>\n<dd>Definition 1</dd></dl>');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Single Term, Plural Definitions', function() {
|
||||||
|
const source = 'Term 1\n::Definition 1\n::Definition 2\n\n';
|
||||||
|
const rendered = Markdown.render(source).trim();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<dl><dt>Term 1</dt>\n<dd>Definition 1</dd>\n<dd>Definition 2</dd></dl>');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Multiple Term, Single Definitions', function() {
|
||||||
|
const source = 'Term 1\n::Definition 1\n\nTerm 2\n::Definition 1\n\n';
|
||||||
|
const rendered = Markdown.render(source).trim();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<dl><dt>Term 1</dt>\n<dd>Definition 1</dd>\n<dt>Term 2</dt>\n<dd>Definition 1</dd></dl>');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Multiple Term, Plural Definitions', function() {
|
||||||
|
const source = 'Term 1\n::Definition 1\n::Definition 2\n\nTerm 2\n::Definition 1\n::Definition 2\n\n';
|
||||||
|
const rendered = Markdown.render(source).trim();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<dl><dt>Term 1</dt>\n<dd>Definition 1</dd>\n<dd>Definition 2</dd>\n<dt>Term 2</dt>\n<dd>Definition 1</dd>\n<dd>Definition 2</dd></dl>');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Single Term, Single multi-line definition', function() {
|
||||||
|
const source = 'Term 1\n::Definition 1\nand more and\nmore and more\n\n';
|
||||||
|
const rendered = Markdown.render(source).trim();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<dl><dt>Term 1</dt>\n<dd>Definition 1 and more and more and more</dd></dl>');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Single Term, Plural multi-line definitions', function() {
|
||||||
|
const source = 'Term 1\n::Definition 1\nand more and more\n::Definition 2\nand more\nand more\n::Definition 3\n\n';
|
||||||
|
const rendered = Markdown.render(source).trim();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<dl><dt>Term 1</dt>\n<dd>Definition 1 and more and more</dd>\n<dd>Definition 2 and more and more</dd>\n<dd>Definition 3</dd></dl>');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Multiple Term, Single multi-line definition', function() {
|
||||||
|
const source = 'Term 1\n::Definition 1\nand more and more\n\nTerm 2\n::Definition 1\n::Definition 2\n\n';
|
||||||
|
const rendered = Markdown.render(source).trim();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<dl><dt>Term 1</dt>\n<dd>Definition 1 and more and more</dd>\n<dt>Term 2</dt>\n<dd>Definition 1</dd>\n<dd>Definition 2</dd></dl>');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Multiple Term, Single multi-line definition, followed by an inline dl', function() {
|
||||||
|
const source = 'Term 1\n::Definition 1\nand more and more\n\nTerm 2\n::Definition 1\n::Definition 2\n\n::Inline Definition (no term)';
|
||||||
|
const rendered = Markdown.render(source).trim();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<dl><dt>Term 1</dt>\n<dd>Definition 1 and more and more</dd>\n<dt>Term 2</dt>\n<dd>Definition 1</dd>\n<dd>Definition 2</dd></dl><dl><dt></dt><dd>Inline Definition (no term)</dd>\n</dl>');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Multiple Term, Single multi-line definition, followed by paragraph', function() {
|
||||||
|
const source = 'Term 1\n::Definition 1\nand more and more\n\nTerm 2\n::Definition 1\n::Definition 2\n\nParagraph';
|
||||||
|
const rendered = Markdown.render(source).trim();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<dl><dt>Term 1</dt>\n<dd>Definition 1 and more and more</dd>\n<dt>Term 2</dt>\n<dd>Definition 1</dd>\n<dd>Definition 2</dd></dl><p>Paragraph</p>');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Block Token cannot be the Term of a multi-line definition', function() {
|
||||||
|
const source = '## Header\n::Definition 1 of a single-line DL\n::Definition 1 of another single-line DL';
|
||||||
|
const rendered = Markdown.render(source).trim();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<h2 id="header">Header</h2>\n<dl><dt></dt><dd>Definition 1 of a single-line DL</dd>\n<dt></dt><dd>Definition 1 of another single-line DL</dd>\n</dl>');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Inline DL has priority over Multiline', function() {
|
||||||
|
const source = 'Term 1 :: Inline definition 1\n:: Inline definition 2 (no DT)';
|
||||||
|
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>');
|
||||||
|
});
|
||||||
|
});
|
||||||
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
|
||||||
|
});
|
||||||
@@ -13,137 +13,134 @@ String.prototype.trimReturns = function(){
|
|||||||
// Remove the `.failing()` method once you have fixed the issue.
|
// Remove the `.failing()` method once you have fixed the issue.
|
||||||
|
|
||||||
describe('Inline: When using the Inline syntax {{ }}', ()=>{
|
describe('Inline: When using the Inline syntax {{ }}', ()=>{
|
||||||
it.failing('Renders a mustache span with text only', function() {
|
it('Renders a mustache span with text only', function() {
|
||||||
const source = '{{ text}}';
|
const source = '{{ text}}';
|
||||||
const rendered = Markdown.render(source);
|
const rendered = Markdown.render(source);
|
||||||
// FIXME: adds extra \s after class names
|
|
||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block">text</span>');
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block">text</span>');
|
||||||
});
|
});
|
||||||
|
|
||||||
it.failing('Renders a mustache span with text only, but with spaces', function() {
|
it('Renders a mustache span with text only, but with spaces', function() {
|
||||||
const source = '{{ this is a text}}';
|
const source = '{{ this is a text}}';
|
||||||
const rendered = Markdown.render(source);
|
const rendered = Markdown.render(source);
|
||||||
// FIXME: adds extra \s after class names
|
|
||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block">this is a text</span>');
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block">this is a text</span>');
|
||||||
});
|
});
|
||||||
|
|
||||||
it.failing('Renders an empty mustache span', function() {
|
it('Renders an empty mustache span', function() {
|
||||||
const source = '{{}}';
|
const source = '{{}}';
|
||||||
const rendered = Markdown.render(source);
|
const rendered = Markdown.render(source);
|
||||||
// FIXME: adds extra \s after class names
|
|
||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block"></span>');
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block"></span>');
|
||||||
});
|
});
|
||||||
|
|
||||||
it.failing('Renders a mustache span with just a space', function() {
|
it('Renders a mustache span with just a space', function() {
|
||||||
const source = '{{ }}';
|
const source = '{{ }}';
|
||||||
const rendered = Markdown.render(source);
|
const rendered = Markdown.render(source);
|
||||||
// FIXME: adds extra \s after class names
|
|
||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block"></span>');
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block"></span>');
|
||||||
});
|
});
|
||||||
|
|
||||||
it.failing('Renders a mustache span with a few spaces only', function() {
|
it('Renders a mustache span with a few spaces only', function() {
|
||||||
const source = '{{ }}';
|
const source = '{{ }}';
|
||||||
const rendered = Markdown.render(source);
|
const rendered = Markdown.render(source);
|
||||||
// FIXME: adds extra \s after class names
|
|
||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block"></span>');
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block"></span>');
|
||||||
});
|
});
|
||||||
|
|
||||||
it.failing('Renders a mustache span with text and class', function() {
|
it('Renders a mustache span with text and class', function() {
|
||||||
const source = '{{my-class text}}';
|
const source = '{{my-class text}}';
|
||||||
const rendered = Markdown.render(source);
|
const rendered = Markdown.render(source);
|
||||||
// FIXME: adds two extra \s before closing `>` in opening tag.
|
|
||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block my-class">text</span>');
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block my-class">text</span>');
|
||||||
});
|
});
|
||||||
|
|
||||||
it.failing('Renders a mustache span with text and two classes', function() {
|
it('Renders a mustache span with text and two classes', function() {
|
||||||
const source = '{{my-class,my-class2 text}}';
|
const source = '{{my-class,my-class2 text}}';
|
||||||
const rendered = Markdown.render(source);
|
const rendered = Markdown.render(source);
|
||||||
// FIXME: adds two extra \s before closing `>` in opening tag.
|
|
||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block my-class my-class2">text</span>');
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block my-class my-class2">text</span>');
|
||||||
});
|
});
|
||||||
|
|
||||||
it.failing('Renders a mustache span with text with spaces and class', function() {
|
it('Renders a mustache span with text with spaces and class', function() {
|
||||||
const source = '{{my-class this is a text}}';
|
const source = '{{my-class this is a text}}';
|
||||||
const rendered = Markdown.render(source);
|
const rendered = Markdown.render(source);
|
||||||
// FIXME: adds two extra \s before closing `>` in opening tag
|
|
||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block my-class">this is a text</span>');
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block my-class">this is a text</span>');
|
||||||
});
|
});
|
||||||
|
|
||||||
it.failing('Renders a mustache span with text and id', function() {
|
it('Renders a mustache span with text and id', function() {
|
||||||
const source = '{{#my-span text}}';
|
const source = '{{#my-span text}}';
|
||||||
const rendered = Markdown.render(source);
|
const rendered = Markdown.render(source);
|
||||||
// FIXME: adds extra \s before closing `>` in opening tag, and another after class names
|
|
||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" id="my-span">text</span>');
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" id="my-span">text</span>');
|
||||||
});
|
});
|
||||||
|
|
||||||
it.failing('Renders a mustache span with text and two ids', function() {
|
it('Renders a mustache span with text and two ids', function() {
|
||||||
const source = '{{#my-span,#my-favorite-span text}}';
|
const source = '{{#my-span,#my-favorite-span text}}';
|
||||||
const rendered = Markdown.render(source);
|
const rendered = Markdown.render(source);
|
||||||
// FIXME: adds extra \s before closing `>` in opening tag, and another after class names
|
|
||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" id="my-span">text</span>');
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" id="my-span">text</span>');
|
||||||
});
|
});
|
||||||
|
|
||||||
it.failing('Renders a mustache span with text and css property', function() {
|
it('Renders a mustache span with text and css property', function() {
|
||||||
const source = '{{color:red text}}';
|
const source = '{{color:red text}}';
|
||||||
const rendered = Markdown.render(source);
|
const rendered = Markdown.render(source);
|
||||||
// FIXME: adds extra \s after class names
|
|
||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" style="color:red;">text</span>');
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" style="color:red;">text</span>');
|
||||||
});
|
});
|
||||||
|
|
||||||
it.failing('Renders a mustache span with text and two css properties', function() {
|
it('Renders a mustache span with text and two css properties', function() {
|
||||||
const source = '{{color:red,padding:5px text}}';
|
const source = '{{color:red,padding:5px text}}';
|
||||||
const rendered = Markdown.render(source);
|
const rendered = Markdown.render(source);
|
||||||
// FIXME: adds extra \s after class names
|
|
||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" style="color:red; padding:5px;">text</span>');
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" style="color:red; padding:5px;">text</span>');
|
||||||
});
|
});
|
||||||
|
|
||||||
it.failing('Renders a mustache span with text and css property which contains quotes', function() {
|
it('Renders a mustache span with text and css property which contains quotes', function() {
|
||||||
const source = '{{font-family:"trebuchet ms" text}}';
|
const source = '{{font-family:"trebuchet ms" text}}';
|
||||||
const rendered = Markdown.render(source);
|
const rendered = Markdown.render(source);
|
||||||
// FIXME: adds extra \s after class names
|
|
||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" style="font-family:trebuchet ms;">text</span>');
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" style="font-family:trebuchet ms;">text</span>');
|
||||||
});
|
});
|
||||||
|
|
||||||
it.failing('Renders a mustache span with text and two css properties which contains quotes', function() {
|
it('Renders a mustache span with text and two css properties which contains quotes', function() {
|
||||||
const source = '{{font-family:"trebuchet ms",padding:"5px 10px" text}}';
|
const source = '{{font-family:"trebuchet ms",padding:"5px 10px" text}}';
|
||||||
const rendered = Markdown.render(source);
|
const rendered = Markdown.render(source);
|
||||||
// FIXME: adds extra \s after class names
|
|
||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" style="font-family:trebuchet ms; padding:5px 10px;">text</span>');
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" style="font-family:trebuchet ms; padding:5px 10px;">text</span>');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it.failing('Renders a mustache span with text with quotes and css property which contains quotes', function() {
|
it('Renders a mustache span with text with quotes and css property which contains double quotes', function() {
|
||||||
const source = '{{font-family:"trebuchet ms" text "with quotes"}}';
|
const source = '{{font-family:"trebuchet ms" text "with quotes"}}';
|
||||||
const rendered = Markdown.render(source);
|
const rendered = Markdown.render(source);
|
||||||
// FIXME: adds extra \s after class names
|
|
||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" style="font-family:trebuchet ms;">text “with quotes”</span>');
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" style="font-family:trebuchet ms;">text “with quotes”</span>');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('Renders a mustache span with text with quotes and css property which contains double and simple quotes', function() {
|
||||||
|
const source = `{{--stringVariable:"'string'" text "with quotes"}}`;
|
||||||
|
const rendered = Markdown.render(source);
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<span class="inline-block" style="--stringVariable:'string';">text “with quotes”</span>`);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
it('Renders a mustache span with text, id, class and a couple of css properties', function() {
|
it('Renders a mustache span with text, id, class and a couple of css properties', function() {
|
||||||
const source = '{{pen,#author,color:orange,font-family:"trebuchet ms" text}}';
|
const source = '{{pen,#author,color:orange,font-family:"trebuchet ms" text}}';
|
||||||
const rendered = Markdown.render(source);
|
const rendered = Markdown.render(source);
|
||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block pen" id="author" style="color:orange; font-family:trebuchet ms;">text</span>');
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block pen" id="author" style="color:orange; font-family:trebuchet ms;">text</span>');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Renders a span with added attributes', function() {
|
||||||
|
const source = 'Text and {{pen,#author,color:orange,font-family:"trebuchet ms",a="b and c",d=e, text}} and more text!';
|
||||||
|
const rendered = Markdown.render(source);
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p>Text and <span class="inline-block pen" id="author" style="color:orange; font-family:trebuchet ms;" a="b and c" d="e">text</span> and more text!</p>\n');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// BLOCK SYNTAX
|
// BLOCK SYNTAX
|
||||||
|
|
||||||
describe(`Block: When using the Block syntax {{tags\\ntext\\n}}`, ()=>{
|
describe(`Block: When using the Block syntax {{tags\\ntext\\n}}`, ()=>{
|
||||||
it.failing('Renders a div with text only', function() {
|
it('Renders a div with text only', function() {
|
||||||
const source = dedent`{{
|
const source = dedent`{{
|
||||||
text
|
text
|
||||||
}}`;
|
}}`;
|
||||||
const rendered = Markdown.render(source).trimReturns();
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
// FIXME: adds extra \s after class names
|
|
||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block"><p>text</p></div>`);
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block"><p>text</p></div>`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it.failing('Renders an empty div', function() {
|
it('Renders an empty div', function() {
|
||||||
const source = dedent`{{
|
const source = dedent`{{
|
||||||
|
|
||||||
}}`;
|
}}`;
|
||||||
const rendered = Markdown.render(source).trimReturns();
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
// FIXME: adds extra \s after class names
|
|
||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block"></div>`);
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block"></div>`);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -151,52 +148,62 @@ describe(`Block: When using the Block syntax {{tags\\ntext\\n}}`, ()=>{
|
|||||||
const source = dedent`{{
|
const source = dedent`{{
|
||||||
}}`;
|
}}`;
|
||||||
const rendered = Markdown.render(source).trimReturns();
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
// this actually renders in HB as '{{ }}'...
|
|
||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p>{{}}</p>`);
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p>{{}}</p>`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it.failing('Renders a div with a single class', function() {
|
it('Renders a div with a single class', function() {
|
||||||
const source = dedent`{{cat
|
const source = dedent`{{cat
|
||||||
|
|
||||||
}}`;
|
}}`;
|
||||||
const rendered = Markdown.render(source).trimReturns();
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
// FIXME: adds two extra \s before closing `>` in opening tag
|
|
||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block cat"></div>`);
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block cat"></div>`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it.failing('Renders a div with a single class and text', function() {
|
it('Renders a div with a single class and text', function() {
|
||||||
const source = dedent`{{cat
|
const source = dedent`{{cat
|
||||||
Sample text.
|
Sample text.
|
||||||
}}`;
|
}}`;
|
||||||
const rendered = Markdown.render(source).trimReturns();
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
// FIXME: adds two extra \s before closing `>` in opening tag
|
|
||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block cat"><p>Sample text.</p></div>`);
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block cat"><p>Sample text.</p></div>`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it.failing('Renders a div with two classes and text', function() {
|
it('Renders a div with two classes and text', function() {
|
||||||
const source = dedent`{{cat,dog
|
const source = dedent`{{cat,dog
|
||||||
Sample text.
|
Sample text.
|
||||||
}}`;
|
}}`;
|
||||||
const rendered = Markdown.render(source).trimReturns();
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
// FIXME: adds two extra \s before closing `>` in opening tag
|
|
||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block cat dog"><p>Sample text.</p></div>`);
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block cat dog"><p>Sample text.</p></div>`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it.failing('Renders a div with a style and text', function() {
|
it('Renders a div with a style and text', function() {
|
||||||
const source = dedent`{{color:red
|
const source = dedent`{{color:red
|
||||||
Sample text.
|
Sample text.
|
||||||
}}`;
|
}}`;
|
||||||
const rendered = Markdown.render(source).trimReturns();
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
// FIXME: adds two extra \s before closing `>` in opening tag
|
|
||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block" style="color:red;"><p>Sample text.</p></div>`);
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block" style="color:red;"><p>Sample text.</p></div>`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it.failing('Renders a div with a class, style and text', function() {
|
it('Renders a div with a style that has a string variable, and text', function() {
|
||||||
|
const source = dedent`{{--stringVariable:"'string'"
|
||||||
|
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.
|
||||||
|
}}`;
|
||||||
|
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
|
const source = dedent`{{cat,color:red
|
||||||
Sample text.
|
Sample text.
|
||||||
}}`;
|
}}`;
|
||||||
const rendered = Markdown.render(source).trimReturns();
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
// FIXME: adds extra \s after the class attribute
|
|
||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block cat" style="color:red;"><p>Sample text.</p></div>`);
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block cat" style="color:red;"><p>Sample text.</p></div>`);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -208,14 +215,27 @@ describe(`Block: When using the Block syntax {{tags\\ntext\\n}}`, ()=>{
|
|||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block cat" id="dog" style="color:red;"><p>Sample text.</p></div>`);
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block cat" id="dog" style="color:red;"><p>Sample text.</p></div>`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it.failing('Renders a div with a single ID', function() {
|
it('Renders a div with a single ID', function() {
|
||||||
const source = dedent`{{#cat,#dog
|
const source = dedent`{{#cat,#dog
|
||||||
Sample text.
|
Sample text.
|
||||||
}}`;
|
}}`;
|
||||||
const rendered = Markdown.render(source).trimReturns();
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
// FIXME: adds extra \s before closing `>` in opening tag, and another after class names
|
|
||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block" id="cat"><p>Sample text.</p></div>`);
|
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.
|
||||||
|
}}`;
|
||||||
|
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>`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Renders a div with added attributes', function() {
|
||||||
|
const source = '{{pen,#author,color:orange,font-family:"trebuchet ms",a="b and c",d=e\nText and text and more text!\n}}\n';
|
||||||
|
const rendered = Markdown.render(source);
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<div class="block pen" id="author" style="color:orange; font-family:trebuchet ms;" a="b and c" d="e"><p>Text and text and more text!</p>\n</div>');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// MUSTACHE INJECTION SYNTAX
|
// MUSTACHE INJECTION SYNTAX
|
||||||
@@ -223,91 +243,212 @@ describe(`Block: When using the Block syntax {{tags\\ntext\\n}}`, ()=>{
|
|||||||
describe('Injection: When an injection tag follows an element', ()=>{
|
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.
|
// 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', ()=>{
|
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 source = '{{ text}}{}';
|
||||||
const rendered = Markdown.render(source);
|
const rendered = Markdown.render(source);
|
||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block">text</span>');
|
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 source = '{{ text}}{ClassName}';
|
||||||
const rendered = Markdown.render(source);
|
const rendered = Markdown.render(source);
|
||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block ClassName">text</span>');
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block ClassName">text</span>');
|
||||||
});
|
});
|
||||||
|
|
||||||
it.failing('Renders a span "text" with injected style', 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 class="inline-block" a="b and c">text</span>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Renders a span "text" with injected style', function() {
|
||||||
const source = '{{ text}}{color:red}';
|
const source = '{{ text}}{color:red}';
|
||||||
const rendered = Markdown.render(source);
|
const rendered = Markdown.render(source);
|
||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" style="color:red;">text</span>');
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" style="color:red;">text</span>');
|
||||||
});
|
});
|
||||||
|
|
||||||
it.failing('Renders a span "text" with two injected styles', 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('Renders a span "text" with two injected styles', function() {
|
||||||
const source = '{{ text}}{color:red,background:blue}';
|
const source = '{{ text}}{color:red,background:blue}';
|
||||||
const rendered = Markdown.render(source);
|
const rendered = Markdown.render(source);
|
||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" style="color:red; background:blue;">text</span>');
|
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 source = '*emphasis*{big}';
|
||||||
const rendered = Markdown.render(source).trimReturns();
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p><em class="big">emphasis</em></p>');
|
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 source = '`code`{background:gray}';
|
||||||
const rendered = Markdown.render(source).trimReturns();
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p><code style="background:gray;">code</code></p>');
|
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 source = '{position:absolute}';
|
||||||
const rendered = Markdown.render(source).trimReturns();
|
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 source = '{{ text}}{color:red}{background:blue}';
|
||||||
const rendered = Markdown.render(source).trimReturns();
|
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>');
|
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 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', ()=>{
|
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 source = '{{\ntext\n}}\n{}';
|
||||||
const rendered = Markdown.render(source).trimReturns();
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<div class="block"><p>text</p></div>');
|
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 source = '{{\ntext\n}}\n{ClassName}';
|
||||||
const rendered = Markdown.render(source).trimReturns();
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<div class="block ClassName"><p>text</p></div>');
|
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 source = '{{\ntext\n}}\n{color:red}';
|
||||||
const rendered = Markdown.render(source).trimReturns();
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<div class="block" style="color:red;"><p>text</p></div>');
|
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`{{
|
const source = dedent`{{
|
||||||
text
|
text
|
||||||
}}
|
}}
|
||||||
{color:red,background:blue}`;
|
{color:red,background:blue}`;
|
||||||
const rendered = Markdown.render(source).trimReturns();
|
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 an h2 header "text" with injected class name', function() {
|
it('renders a div "text" with injected variable string', function() {
|
||||||
|
const source = dedent`{{
|
||||||
|
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>`);
|
||||||
|
});
|
||||||
|
|
||||||
|
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
|
const source = dedent`## text
|
||||||
{ClassName}`;
|
{ClassName}`;
|
||||||
const rendered = Markdown.render(source).trimReturns();
|
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 |
|
const source = dedent`| Experience Points | Level |
|
||||||
|:------------------|:-----:|
|
|:------------------|:-----:|
|
||||||
| 0 | 1 |
|
| 0 | 1 |
|
||||||
@@ -329,15 +470,15 @@ 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>
|
// 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
|
const source = dedent`## text
|
||||||
{ClassName}
|
{ClassName}
|
||||||
{secondInjection}`;
|
{secondInjection}`;
|
||||||
const rendered = Markdown.render(source).trimReturns();
|
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`{{
|
const source = dedent`{{
|
||||||
outer text
|
outer text
|
||||||
{{
|
{{
|
||||||
|
|||||||
373
tests/markdown/variables.test.js
Normal file
373
tests/markdown/variables.test.js
Normal file
@@ -0,0 +1,373 @@
|
|||||||
|
/* eslint-disable max-lines */
|
||||||
|
|
||||||
|
const dedent = require('dedent-tabs').default;
|
||||||
|
const Markdown = require('naturalcrit/markdown.js');
|
||||||
|
|
||||||
|
// 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, '').trim();
|
||||||
|
};
|
||||||
|
|
||||||
|
renderAllPages = function(pages){
|
||||||
|
const outputs = [];
|
||||||
|
pages.forEach((page, index)=>{
|
||||||
|
const output = Markdown.render(page, index);
|
||||||
|
outputs.push(output);
|
||||||
|
});
|
||||||
|
|
||||||
|
return outputs;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Adding `.failing()` method to `describe` or `it` will make failing tests "pass" as long as they continue to fail.
|
||||||
|
// Remove the `.failing()` method once you have fixed the issue.
|
||||||
|
|
||||||
|
describe('Block-level variables', ()=>{
|
||||||
|
it('Handles variable assignment and recall with simple text', function() {
|
||||||
|
const source = dedent`
|
||||||
|
[var]: string
|
||||||
|
|
||||||
|
$[var]
|
||||||
|
`;
|
||||||
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p>string</p>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Handles variable assignment and recall with multiline string', function() {
|
||||||
|
const source = dedent`
|
||||||
|
[var]: string
|
||||||
|
across multiple
|
||||||
|
lines
|
||||||
|
|
||||||
|
$[var]`;
|
||||||
|
const rendered = Markdown.render(source).replace(/\s/g, ' ').trimReturns();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p>string across multiple lines</p>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Handles variable assignment and recall with tables', function() {
|
||||||
|
const source = dedent`
|
||||||
|
[var]:
|
||||||
|
##### Title
|
||||||
|
| H1 | H2 |
|
||||||
|
|:---|:--:|
|
||||||
|
| A | B |
|
||||||
|
| C | D |
|
||||||
|
|
||||||
|
$[var]`;
|
||||||
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent`
|
||||||
|
<h5 id="title">Title</h5>
|
||||||
|
<table><thead><tr><th align=left>H1</th>
|
||||||
|
<th align=center>H2</th>
|
||||||
|
</tr></thead><tbody><tr><td align=left>A</td>
|
||||||
|
<td align=center>B</td>
|
||||||
|
</tr><tr><td align=left>C</td>
|
||||||
|
<td align=center>D</td>
|
||||||
|
</tr></tbody></table>`.trimReturns());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Hoists undefined variables', function() {
|
||||||
|
const source = dedent`
|
||||||
|
$[var]
|
||||||
|
|
||||||
|
[var]: string`;
|
||||||
|
const rendered = Markdown.render(source).replace(/\s/g, ' ').trimReturns();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p>string</p>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Hoists last instance of variable', function() {
|
||||||
|
const source = dedent`
|
||||||
|
$[var]
|
||||||
|
|
||||||
|
[var]: string
|
||||||
|
|
||||||
|
[var]: new string`;
|
||||||
|
const rendered = Markdown.render(source).replace(/\s/g, ' ').trimReturns();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p>new string</p>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Handles complex hoisting', function() {
|
||||||
|
const source = dedent`
|
||||||
|
$[titleAndName]: $[title] $[fullName]
|
||||||
|
|
||||||
|
$[title]: Mr.
|
||||||
|
|
||||||
|
$[fullName]: $[firstName] $[lastName]
|
||||||
|
|
||||||
|
[firstName]: Bob
|
||||||
|
|
||||||
|
Welcome, $[titleAndName]!
|
||||||
|
|
||||||
|
[lastName]: Jacob
|
||||||
|
|
||||||
|
[lastName]: $[lastName]son
|
||||||
|
`;
|
||||||
|
const rendered = Markdown.render(source).replace(/\s/g, ' ').trimReturns();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p>Welcome, Mr. Bob Jacobson!</p>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Handles variable reassignment', function() {
|
||||||
|
const source = dedent`
|
||||||
|
[var]: one
|
||||||
|
|
||||||
|
$[var]
|
||||||
|
|
||||||
|
[var]: two
|
||||||
|
|
||||||
|
$[var]
|
||||||
|
`;
|
||||||
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p>one</p><p>two</p>'.trimReturns());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Handles variable reassignment with hoisting', function() {
|
||||||
|
const source = dedent`
|
||||||
|
$[var]
|
||||||
|
|
||||||
|
[var]: one
|
||||||
|
|
||||||
|
$[var]
|
||||||
|
|
||||||
|
[var]: two
|
||||||
|
|
||||||
|
$[var]
|
||||||
|
`;
|
||||||
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p>two</p><p>one</p><p>two</p>'.trimReturns());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Ignores undefined variables that can\'t be hoisted', function() {
|
||||||
|
const source = dedent`
|
||||||
|
$[var](My name is $[first] $[last])
|
||||||
|
|
||||||
|
$[last]: Jones
|
||||||
|
`;
|
||||||
|
const rendered = Markdown.render(source).replace(/\s/g, ' ').trimReturns();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p>My name is $[first] Jones</p>`.trimReturns());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Inline-level variables', ()=>{
|
||||||
|
it('Handles variable assignment and recall with simple text', function() {
|
||||||
|
const source = dedent`
|
||||||
|
$[var](string)
|
||||||
|
|
||||||
|
$[var]
|
||||||
|
`;
|
||||||
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p>string</p><p>string</p>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Hoists undefined variables when possible', function() {
|
||||||
|
const source = dedent`
|
||||||
|
$[var](My name is $[name] Jones)
|
||||||
|
|
||||||
|
[name]: Bob`;
|
||||||
|
const rendered = Markdown.render(source).replace(/\s/g, ' ').trimReturns();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p>My name is Bob Jones</p>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Hoists last instance of variable', function() {
|
||||||
|
const source = dedent`
|
||||||
|
$[var](My name is $[name] Jones)
|
||||||
|
|
||||||
|
$[name](Bob)
|
||||||
|
|
||||||
|
[name]: Bill`;
|
||||||
|
const rendered = Markdown.render(source).replace(/\s/g, ' ').trimReturns();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p>My name is Bill Jones</p> <p>Bob</p>`.trimReturns());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Only captures nested parens if balanced', function() {
|
||||||
|
const source = dedent`
|
||||||
|
$[var1](A variable (with nested parens) inside)
|
||||||
|
|
||||||
|
$[var1]
|
||||||
|
|
||||||
|
$[var2](A variable ) with unbalanced parens)
|
||||||
|
|
||||||
|
$[var2]`;
|
||||||
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent`
|
||||||
|
<p>A variable (with nested parens) inside</p>
|
||||||
|
<p>A variable (with nested parens) inside</p>
|
||||||
|
<p>A variable with unbalanced parens)</p>
|
||||||
|
<p>A variable</p>
|
||||||
|
`.trimReturns());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Math', ()=>{
|
||||||
|
it('Handles simple math using numbers only', function() {
|
||||||
|
const source = dedent`
|
||||||
|
$[1 + 3 * 5 - (1 / 4)]
|
||||||
|
`;
|
||||||
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p>15.75</p>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Handles round function', function() {
|
||||||
|
const source = dedent`
|
||||||
|
$[round(1/4)]`;
|
||||||
|
const rendered = Markdown.render(source).replace(/\s/g, ' ').trimReturns();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p>0</p>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Handles floor function', function() {
|
||||||
|
const source = dedent`
|
||||||
|
$[floor(0.6)]`;
|
||||||
|
const rendered = Markdown.render(source).replace(/\s/g, ' ').trimReturns();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p>0</p>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Handles ceil function', function() {
|
||||||
|
const source = dedent`
|
||||||
|
$[ceil(0.2)]`;
|
||||||
|
const rendered = Markdown.render(source).replace(/\s/g, ' ').trimReturns();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p>1</p>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Handles nested functions', function() {
|
||||||
|
const source = dedent`
|
||||||
|
$[ceil(floor(round(0.6)))]`;
|
||||||
|
const rendered = Markdown.render(source).replace(/\s/g, ' ').trimReturns();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p>1</p>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Handles simple math with variables', function() {
|
||||||
|
const source = dedent`
|
||||||
|
$[num1]: 5
|
||||||
|
|
||||||
|
$[num2]: 4
|
||||||
|
|
||||||
|
Answer is $[answer]($[1 + 3 * num1 - (1 / num2)]).
|
||||||
|
`;
|
||||||
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p>Answer is 15.75.</p>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Handles variable incrementing', function() {
|
||||||
|
const source = dedent`
|
||||||
|
$[num1]: 5
|
||||||
|
|
||||||
|
Increment num1 to get $[num1]($[num1 + 1]) and again to $[num1]($[num1 + 1]).
|
||||||
|
`;
|
||||||
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p>Increment num1 to get 6 and again to 7.</p>');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Code blocks', ()=>{
|
||||||
|
it('Ignores all variables in fenced code blocks', function() {
|
||||||
|
const source = dedent`
|
||||||
|
\`\`\`
|
||||||
|
[var]: string
|
||||||
|
|
||||||
|
$[var]
|
||||||
|
|
||||||
|
$[var](new string)
|
||||||
|
\`\`\`
|
||||||
|
`;
|
||||||
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent`
|
||||||
|
<pre><code>
|
||||||
|
[var]: string
|
||||||
|
|
||||||
|
$[var]
|
||||||
|
|
||||||
|
$[var](new string)
|
||||||
|
</code></pre>`.trimReturns());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Ignores all variables in indented code blocks', function() {
|
||||||
|
const source = dedent`
|
||||||
|
test
|
||||||
|
|
||||||
|
[var]: string
|
||||||
|
|
||||||
|
$[var]
|
||||||
|
|
||||||
|
$[var](new string)
|
||||||
|
`;
|
||||||
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent`
|
||||||
|
<p>test</p>
|
||||||
|
|
||||||
|
<pre><code>
|
||||||
|
[var]: string
|
||||||
|
|
||||||
|
$[var]
|
||||||
|
|
||||||
|
$[var](new string)
|
||||||
|
</code></pre>`.trimReturns());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Ignores all variables in inline code blocks', function() {
|
||||||
|
const source = '[var](Hello) `[link](url)`. This `[var] does not work`';
|
||||||
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent`
|
||||||
|
<p><a href="Hello">var</a> <code>[link](url)</code>. This <code>[var] does not work</code></p>`.trimReturns());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Normal Links and Images', ()=>{
|
||||||
|
it('Renders normal images', function() {
|
||||||
|
const source = ``;
|
||||||
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent`
|
||||||
|
<p><img src="url" alt="alt text"></p>`.trimReturns());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Renders normal images with a title', function() {
|
||||||
|
const source = 'An image !';
|
||||||
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent`
|
||||||
|
<p>An image <img src="url" alt="alt text" title="and title">!</p>`.trimReturns());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Applies curly injectors to images', function() {
|
||||||
|
const source = `{width:100px}`;
|
||||||
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent`
|
||||||
|
<p><img style="width:100px;" src="url" alt="alt text"></p>`.trimReturns());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Renders normal links', function() {
|
||||||
|
const source = 'A Link to my [website](url)!';
|
||||||
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent`
|
||||||
|
<p>A Link to my <a href="url">website</a>!</p>`.trimReturns());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Renders normal links with a title', function() {
|
||||||
|
const source = 'A Link to my [website](url "and title")!';
|
||||||
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent`
|
||||||
|
<p>A Link to my <a href="url" title="and title">website</a>!</p>`.trimReturns());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Cross-page variables', ()=>{
|
||||||
|
it('Handles variable assignment and recall across pages', function() {
|
||||||
|
const source0 = `[var]: string`;
|
||||||
|
const source1 = `$[var]`;
|
||||||
|
const rendered = renderAllPages([source0, source1]).join('\n\\page\n').trimReturns();
|
||||||
|
expect(rendered, `Input:\n${[source0, source1].join('\n\\page\n')}`, { showPrefix: false }).toBe('\\page<p>string</p>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Handles hoisting across pages', function() {
|
||||||
|
const source0 = `$[var]`;
|
||||||
|
const source1 = `[var]: string`;
|
||||||
|
renderAllPages([source0, source1]).join('\n\\page\n').trimReturns(); //Requires one full render of document before hoisting is picked up
|
||||||
|
const rendered = renderAllPages([source0, source1]).join('\n\\page\n').trimReturns();
|
||||||
|
expect(rendered, `Input:\n${[source0, source1].join('\n\\page\n')}`, { showPrefix: false }).toBe('<p>string</p>\\page');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Handles reassignment and hoisting across pages', function() {
|
||||||
|
const source0 = `$[var]\n\n[var]: one\n\n$[var]`;
|
||||||
|
const source1 = `[var]: two\n\n$[var]`;
|
||||||
|
renderAllPages([source0, source1]).join('\n\\page\n').trimReturns(); //Requires one full render of document before hoisting is picked up
|
||||||
|
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>');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -47,8 +47,8 @@ const getTOC = (pages)=>{
|
|||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = function(brew){
|
module.exports = function(props){
|
||||||
const pages = brew.text.split('\\page');
|
const pages = props.brew.text.split('\\page');
|
||||||
const TOC = getTOC(pages);
|
const TOC = getTOC(pages);
|
||||||
const markdown = _.reduce(TOC, (r, g1, idx1)=>{
|
const markdown = _.reduce(TOC, (r, g1, idx1)=>{
|
||||||
r.push(`- **[${idx1 + 1} ${g1.title}](#p${g1.page})**`);
|
r.push(`- **[${idx1 + 1} ${g1.title}](#p${g1.page})**`);
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ body {
|
|||||||
-webkit-column-gap : 1cm;
|
-webkit-column-gap : 1cm;
|
||||||
-moz-column-gap : 1cm;
|
-moz-column-gap : 1cm;
|
||||||
}
|
}
|
||||||
.phb{
|
.phb, .page{
|
||||||
.useColumns();
|
.useColumns();
|
||||||
counter-increment : phb-page-numbers;
|
counter-increment : phb-page-numbers;
|
||||||
position : relative;
|
position : relative;
|
||||||
@@ -59,6 +59,9 @@ body {
|
|||||||
page-break-before : always;
|
page-break-before : always;
|
||||||
page-break-after : always;
|
page-break-after : always;
|
||||||
contain : size;
|
contain : size;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phb{
|
||||||
//*****************************
|
//*****************************
|
||||||
// * BASE
|
// * BASE
|
||||||
// *****************************/
|
// *****************************/
|
||||||
|
|||||||
@@ -10,6 +10,21 @@
|
|||||||
background-image : url(/assets/DMG_background.png);
|
background-image : url(/assets/DMG_background.png);
|
||||||
background-size : cover;
|
background-size : cover;
|
||||||
|
|
||||||
|
/*TABLES WITHIN NOTES*/
|
||||||
|
.note table tbody tr:nth-child(odd) {
|
||||||
|
background:#fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*DROP CAP*/
|
||||||
|
h1 + p::first-letter {
|
||||||
|
background-image: unset;
|
||||||
|
color:black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quote p:first-child::first-line {
|
||||||
|
all: unset;
|
||||||
|
}
|
||||||
|
|
||||||
&:after {
|
&:after {
|
||||||
background-image : url(/assets/DMG_footerAccent.png);
|
background-image : url(/assets/DMG_footerAccent.png);
|
||||||
height: 58px;
|
height: 58px;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ const ClassFeatureGen = require('./snippets/classfeature.gen.js');
|
|||||||
const CoverPageGen = require('./snippets/coverpage.gen.js');
|
const CoverPageGen = require('./snippets/coverpage.gen.js');
|
||||||
const TableOfContentsGen = require('./snippets/tableOfContents.gen.js');
|
const TableOfContentsGen = require('./snippets/tableOfContents.gen.js');
|
||||||
const indexGen = require('./snippets/index.gen.js');
|
const indexGen = require('./snippets/index.gen.js');
|
||||||
|
const QuoteGen = require('./snippets/quote.gen.js');
|
||||||
const dedent = require('dedent-tabs').default;
|
const dedent = require('dedent-tabs').default;
|
||||||
|
|
||||||
|
|
||||||
@@ -20,19 +21,43 @@ module.exports = [
|
|||||||
view : 'text',
|
view : 'text',
|
||||||
snippets : [
|
snippets : [
|
||||||
{
|
{
|
||||||
name : 'Page Number',
|
name : 'Table of Contents',
|
||||||
icon : 'fas fa-bookmark',
|
icon : 'fas fa-book',
|
||||||
gen : '{{pageNumber 1}}\n{{footnote PART 1 | SECTION NAME}}\n\n'
|
gen : TableOfContentsGen,
|
||||||
},
|
experimental : true,
|
||||||
{
|
subsnippets : [
|
||||||
name : 'Auto-incrementing Page Number',
|
|
||||||
icon : 'fas fa-sort-numeric-down',
|
|
||||||
gen : '{{pageNumber,auto}}\n{{footnote PART 1 | SECTION NAME}}\n\n'
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name : 'Table of Contents',
|
name : 'Table of Contents',
|
||||||
icon : 'fas fa-book',
|
icon : 'fas fa-book',
|
||||||
gen : TableOfContentsGen
|
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',
|
name : 'Index',
|
||||||
@@ -133,6 +158,11 @@ module.exports = [
|
|||||||
icon : 'fas fa-mask',
|
icon : 'fas fa-mask',
|
||||||
gen : ClassFeatureGen,
|
gen : ClassFeatureGen,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name : 'Quote',
|
||||||
|
icon : 'fas fa-quote-right',
|
||||||
|
gen : QuoteGen,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name : 'Note',
|
name : 'Note',
|
||||||
icon : 'fas fa-sticky-note',
|
icon : 'fas fa-sticky-note',
|
||||||
@@ -230,34 +260,51 @@ module.exports = [
|
|||||||
view : 'text',
|
view : 'text',
|
||||||
snippets : [
|
snippets : [
|
||||||
{
|
{
|
||||||
name : 'Class Table',
|
name : 'Class Tables',
|
||||||
|
icon : 'fas fa-table',
|
||||||
|
gen : ClassTableGen.full('classTable,frame,decoration,wide'),
|
||||||
|
subsnippets : [
|
||||||
|
{
|
||||||
|
name : 'Martial Class Table',
|
||||||
|
icon : 'fas fa-table',
|
||||||
|
gen : ClassTableGen.non('classTable,frame,decoration'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Martial Class Table (unframed)',
|
||||||
|
icon : 'fas fa-border-none',
|
||||||
|
gen : ClassTableGen.non('classTable'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Full Caster Class Table',
|
||||||
icon : 'fas fa-table',
|
icon : 'fas fa-table',
|
||||||
gen : ClassTableGen.full('classTable,frame,decoration,wide'),
|
gen : ClassTableGen.full('classTable,frame,decoration,wide'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name : 'Class Table (unframed)',
|
name : 'Full Caster Class Table (unframed)',
|
||||||
icon : 'fas fa-border-none',
|
icon : 'fas fa-border-none',
|
||||||
gen : ClassTableGen.full('classTable,wide'),
|
gen : ClassTableGen.full('classTable,wide'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name : '1/2 Class Table',
|
name : 'Half Caster Class Table',
|
||||||
icon : 'fas fa-list-alt',
|
icon : 'fas fa-list-alt',
|
||||||
gen : ClassTableGen.half('classTable,decoration,frame'),
|
gen : ClassTableGen.half('classTable,frame,decoration,wide'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name : '1/2 Class Table (unframed)',
|
name : 'Half Caster Class Table (unframed)',
|
||||||
icon : 'fas fa-border-none',
|
icon : 'fas fa-border-none',
|
||||||
gen : ClassTableGen.half('classTable'),
|
gen : ClassTableGen.half('classTable,wide'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name : '1/3 Class Table',
|
name : 'Third Caster Spell Table',
|
||||||
icon : 'fas fa-border-all',
|
icon : 'fas fa-border-all',
|
||||||
gen : ClassTableGen.third('classTable,frame'),
|
gen : ClassTableGen.third('classTable,frame,decoration'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name : '1/3 Class Table (unframed)',
|
name : 'Third Caster Spell Table (unframed)',
|
||||||
icon : 'fas fa-border-none',
|
icon : 'fas fa-border-none',
|
||||||
gen : ClassTableGen.third('classTable'),
|
gen : ClassTableGen.third('classTable'),
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name : 'Rune Table',
|
name : 'Rune Table',
|
||||||
|
|||||||
@@ -42,8 +42,8 @@ module.exports = function(classname){
|
|||||||
|
|
||||||
#### Equipment
|
#### Equipment
|
||||||
You start with the following equipment, in addition to the equipment granted by your background:
|
You start with the following equipment, in addition to the equipment granted by your background:
|
||||||
- *(a)* a martial weapon and a shield or *(b)* two martial weapons
|
- (*a*) a martial weapon and a shield or (*b*) two martial weapons
|
||||||
- *(a)* five javelins or *(b)* any simple melee weapon
|
- (*a*) five javelins or (*b*) any simple melee weapon
|
||||||
- ${_.sample(['10 lint fluffs', '1 button', 'a cherished lost sock'])}
|
- ${_.sample(['10 lint fluffs', '1 button', 'a cherished lost sock'])}
|
||||||
`;
|
`;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,132 +1,138 @@
|
|||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
|
const dedent = require('dedent-tabs').default;
|
||||||
|
|
||||||
const features = [
|
const features = [
|
||||||
'Astrological Botany',
|
'Astrological Botany', 'Biochemical Sorcery', 'Civil Divination',
|
||||||
'Biochemical Sorcery',
|
'Consecrated Augury', 'Demonic Anthropology', 'Divinatory Mineralogy',
|
||||||
'Civil Divination',
|
'Exo Interfacer', 'Genetic Banishing', 'Gunpowder Torturer',
|
||||||
'Consecrated Augury',
|
'Gunslinger Corruptor', 'Hermetic Geography', 'Immunological Cultist',
|
||||||
'Demonic Anthropology',
|
'Malefic Chemist', 'Mathematical Pharmacy', 'Nuclear Biochemistry',
|
||||||
'Divinatory Mineralogy',
|
'Orbital Gravedigger', 'Pharmaceutical Outlaw', 'Phased Linguist',
|
||||||
'Exo Interfacer',
|
'Plasma Gunslinger', 'Police Necromancer', 'Ritual Astronomy',
|
||||||
'Genetic Banishing',
|
'Sixgun Poisoner', 'Seismological Alchemy', 'Spiritual Illusionism',
|
||||||
'Gunpowder Torturer',
|
'Statistical Occultism', 'Spell Analyst', 'Torque Interfacer'
|
||||||
'Gunslinger Corruptor',
|
].map((f)=>_.padEnd(f, 21)); // Pad to equal length of 21 chars long
|
||||||
'Hermetic Geography',
|
|
||||||
'Immunological Cultist',
|
const classnames = [
|
||||||
'Malefic Chemist',
|
'Ackerman', 'Berserker-Typist', 'Concierge', 'Fishmonger',
|
||||||
'Mathematical Pharmacy',
|
'Haberdasher', 'Manicurist', 'Netrunner', 'Weirkeeper'
|
||||||
'Nuclear Biochemistry',
|
|
||||||
'Orbital Gravedigger',
|
|
||||||
'Pharmaceutical Outlaw',
|
|
||||||
'Phased Linguist',
|
|
||||||
'Plasma Gunslinger',
|
|
||||||
'Police Necromancer',
|
|
||||||
'Ritual Astronomy',
|
|
||||||
'Sixgun Poisoner',
|
|
||||||
'Seismological Alchemy',
|
|
||||||
'Spiritual Illusionism',
|
|
||||||
'Statistical Occultism',
|
|
||||||
'Spell Analyst',
|
|
||||||
'Torque Interfacer'
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const classnames = ['Ackerman', 'Berserker-Typist', 'Concierge', 'Fishmonger',
|
|
||||||
'Haberdasher', 'Manicurist', 'Netrunner', 'Weirkeeper'];
|
|
||||||
|
|
||||||
const levels = ['1st', '2nd', '3rd', '4th', '5th',
|
|
||||||
'6th', '7th', '8th', '9th', '10th',
|
|
||||||
'11th', '12th', '13th', '14th', '15th',
|
|
||||||
'16th', '17th', '18th', '19th', '20th'];
|
|
||||||
|
|
||||||
const profBonus = [2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6, 6, 6];
|
|
||||||
|
|
||||||
const maxes = [4, 3, 3, 3, 3, 2, 2, 1, 1];
|
|
||||||
|
|
||||||
const drawSlots = function(Slots, rows, padding){
|
|
||||||
let slots = Number(Slots);
|
|
||||||
return _.times(rows, function(i){
|
|
||||||
const max = maxes[i];
|
|
||||||
if(slots < 1) return _.pad('—', padding);
|
|
||||||
const res = _.min([max, slots]);
|
|
||||||
slots -= res;
|
|
||||||
return _.pad(res.toString(), padding);
|
|
||||||
}).join(' | ');
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
full : function(classes){
|
non : function(snippetClasses){
|
||||||
const classname = _.sample(classnames);
|
return dedent`
|
||||||
|
{{${snippetClasses}
|
||||||
|
##### The ${_.sample(classnames)}
|
||||||
let cantrips = 3;
|
| Level | Proficiency Bonus | Features | ${_.sample(features)} |
|
||||||
let spells = 1;
|
|:-----:|:-----------------:|:---------|:---------------------:|
|
||||||
let slots = 2;
|
| 1st | +2 | ${_.sample(features)} | 2 |
|
||||||
return `{{${classes}\n##### The ${classname}\n` +
|
| 2nd | +2 | ${_.sample(features)} | 2 |
|
||||||
`| Level | Proficiency | Features | Cantrips | Spells | --- Spell Slots Per Spell Level ---|||||||||\n`+
|
| 3rd | +2 | ${_.sample(features)} | 3 |
|
||||||
`| ^| Bonus ^| ^| Known ^| Known ^|1st |2nd |3rd |4th |5th |6th |7th |8th |9th |\n`+
|
| 4th | +2 | ${_.sample(features)} | 3 |
|
||||||
`|:-----:|:-----------:|:-------------|:--------:|:------:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|\n${
|
| 5th | +3 | ${_.sample(features)} | 3 |
|
||||||
_.map(levels, function(levelName, level){
|
| 6th | +3 | ${_.sample(features)} | 4 |
|
||||||
const res = [
|
| 7th | +3 | ${_.sample(features)} | 4 |
|
||||||
_.pad(levelName, 5),
|
| 8th | +3 | ${_.sample(features)} | 4 |
|
||||||
_.pad(`+${profBonus[level]}`, 2),
|
| 9th | +4 | ${_.sample(features)} | 4 |
|
||||||
_.padEnd(_.sample(features), 21),
|
| 10th | +4 | ${_.sample(features)} | 4 |
|
||||||
_.pad(cantrips.toString(), 8),
|
| 11th | +4 | ${_.sample(features)} | 4 |
|
||||||
_.pad(spells.toString(), 6),
|
| 12th | +4 | ${_.sample(features)} | 5 |
|
||||||
drawSlots(slots, 9, 2),
|
| 13th | +5 | ${_.sample(features)} | 5 |
|
||||||
].join(' | ');
|
| 14th | +5 | ${_.sample(features)} | 5 |
|
||||||
|
| 15th | +5 | ${_.sample(features)} | 5 |
|
||||||
cantrips += _.random(0, 1);
|
| 16th | +5 | ${_.sample(features)} | 5 |
|
||||||
spells += _.random(0, 1);
|
| 17th | +6 | ${_.sample(features)} | 6 |
|
||||||
slots += _.random(0, 2);
|
| 18th | +6 | ${_.sample(features)} | 6 |
|
||||||
|
| 19th | +6 | ${_.sample(features)} | 6 |
|
||||||
return `| ${res} |`;
|
| 20th | +6 | ${_.sample(features)} | unlimited |
|
||||||
}).join('\n')}\n}}\n\n`;
|
}}\n\n`;
|
||||||
},
|
},
|
||||||
|
|
||||||
half : function(classes){
|
full : function(snippetClasses){
|
||||||
const classname = _.sample(classnames);
|
return dedent`
|
||||||
|
{{${snippetClasses}
|
||||||
let featureScore = 1;
|
##### The ${_.sample(classnames)}
|
||||||
return `{{${classes}\n##### The ${classname}\n` +
|
| Level | Proficiency | Features | Cantrips | --- Spell Slots Per Spell Level ---|||||||||
|
||||||
`| Level | Proficiency Bonus | Features | ${_.pad(_.sample(features), 21)} |\n` +
|
| ^| Bonus ^| ^| Known ^|1st |2nd |3rd |4th |5th |6th |7th |8th |9th |
|
||||||
`|:-----:|:-----------------:|:---------|:---------------------:|\n${
|
|:-----:|:-----------:|:-------------|:--------:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|
|
||||||
_.map(levels, function(levelName, level){
|
| 1st | +2 | ${_.sample(features)} | 2 | 2 | — | — | — | — | — | — | — | — |
|
||||||
const res = [
|
| 2nd | +2 | ${_.sample(features)} | 2 | 3 | — | — | — | — | — | — | — | — |
|
||||||
_.pad(levelName, 5),
|
| 3rd | +2 | ${_.sample(features)} | 2 | 4 | 2 | — | — | — | — | — | — | — |
|
||||||
_.pad(`+${profBonus[level]}`, 2),
|
| 4th | +2 | ${_.sample(features)} | 3 | 4 | 3 | — | — | — | — | — | — | — |
|
||||||
_.padEnd(_.sample(features), 23),
|
| 5th | +3 | ${_.sample(features)} | 3 | 4 | 3 | 2 | — | — | — | — | — | — |
|
||||||
_.pad(`+${featureScore}`, 21),
|
| 6th | +3 | ${_.sample(features)} | 3 | 4 | 3 | 3 | — | — | — | — | — | — |
|
||||||
].join(' | ');
|
| 7th | +3 | ${_.sample(features)} | 3 | 4 | 3 | 3 | 1 | — | — | — | — | — |
|
||||||
|
| 8th | +3 | ${_.sample(features)} | 3 | 4 | 3 | 3 | 2 | — | — | — | — | — |
|
||||||
featureScore += _.random(0, 1);
|
| 9th | +4 | ${_.sample(features)} | 3 | 4 | 3 | 3 | 2 | 1 | — | — | — | — |
|
||||||
|
| 10th | +4 | ${_.sample(features)} | 3 | 4 | 3 | 3 | 2 | 1 | — | — | — | — |
|
||||||
return `| ${res} |`;
|
| 11th | +4 | ${_.sample(features)} | 4 | 4 | 3 | 3 | 2 | 1 | 1 | — | — | — |
|
||||||
}).join('\n')}\n}}\n\n`;
|
| 12th | +4 | ${_.sample(features)} | 4 | 4 | 3 | 3 | 2 | 1 | 1 | — | — | — |
|
||||||
|
| 13th | +5 | ${_.sample(features)} | 4 | 4 | 3 | 3 | 2 | 1 | 1 | 1 | — | — |
|
||||||
|
| 14th | +5 | ${_.sample(features)} | 4 | 4 | 3 | 3 | 2 | 1 | 1 | 1 | — | — |
|
||||||
|
| 15th | +5 | ${_.sample(features)} | 4 | 4 | 3 | 3 | 2 | 1 | 1 | 1 | 1 | — |
|
||||||
|
| 16th | +5 | ${_.sample(features)} | 4 | 4 | 3 | 3 | 2 | 1 | 1 | 1 | 1 | — |
|
||||||
|
| 17th | +6 | ${_.sample(features)} | 4 | 4 | 3 | 3 | 2 | 1 | 1 | 1 | 1 | 1 |
|
||||||
|
| 18th | +6 | ${_.sample(features)} | 4 | 4 | 3 | 3 | 3 | 1 | 1 | 1 | 1 | 1 |
|
||||||
|
| 19th | +6 | ${_.sample(features)} | 4 | 4 | 3 | 3 | 3 | 2 | 2 | 1 | 1 | 1 |
|
||||||
|
| 20th | +6 | ${_.sample(features)} | 4 | 4 | 3 | 3 | 3 | 2 | 2 | 2 | 1 | 1 |
|
||||||
|
}}\n\n`;
|
||||||
},
|
},
|
||||||
|
|
||||||
third : function(classes){
|
half : function(snippetClasses){
|
||||||
const classname = _.sample(classnames);
|
return dedent`
|
||||||
|
{{${snippetClasses}
|
||||||
|
##### The ${_.sample(classnames)}
|
||||||
|
| Level | Proficiency | Features | Spells |--- Spell Slots Per Spell Level ---|||||
|
||||||
|
| ^| Bonus ^| ^| Known ^| 1st | 2nd | 3rd | 4th | 5th |
|
||||||
|
|:-----:|:-----------:|:-------------|:------:|:-----:|:-----:|:-----:|:-----:|:-----:|
|
||||||
|
| 1st | +2 | ${_.sample(features)} | — | — | — | — | — | — |
|
||||||
|
| 2nd | +2 | ${_.sample(features)} | 2 | 2 | — | — | — | — |
|
||||||
|
| 3rd | +2 | ${_.sample(features)} | 3 | 3 | — | — | — | — |
|
||||||
|
| 4th | +2 | ${_.sample(features)} | 3 | 3 | — | — | — | — |
|
||||||
|
| 5th | +3 | ${_.sample(features)} | 4 | 4 | 2 | — | — | — |
|
||||||
|
| 6th | +3 | ${_.sample(features)} | 4 | 4 | 2 | — | — | — |
|
||||||
|
| 7th | +3 | ${_.sample(features)} | 5 | 4 | 3 | — | — | — |
|
||||||
|
| 8th | +3 | ${_.sample(features)} | 5 | 4 | 3 | — | — | — |
|
||||||
|
| 9th | +4 | ${_.sample(features)} | 6 | 4 | 3 | 2 | — | — |
|
||||||
|
| 10th | +4 | ${_.sample(features)} | 6 | 4 | 3 | 2 | — | — |
|
||||||
|
| 11th | +4 | ${_.sample(features)} | 7 | 4 | 3 | 3 | — | — |
|
||||||
|
| 12th | +4 | ${_.sample(features)} | 7 | 4 | 3 | 3 | — | — |
|
||||||
|
| 13th | +5 | ${_.sample(features)} | 8 | 4 | 3 | 3 | 1 | — |
|
||||||
|
| 14th | +5 | ${_.sample(features)} | 8 | 4 | 3 | 3 | 1 | — |
|
||||||
|
| 15th | +5 | ${_.sample(features)} | 9 | 4 | 3 | 3 | 2 | — |
|
||||||
|
| 16th | +5 | ${_.sample(features)} | 9 | 4 | 3 | 3 | 2 | — |
|
||||||
|
| 17th | +6 | ${_.sample(features)} | 10 | 4 | 3 | 3 | 3 | 1 |
|
||||||
|
| 18th | +6 | ${_.sample(features)} | 10 | 4 | 3 | 3 | 3 | 1 |
|
||||||
|
| 19th | +6 | ${_.sample(features)} | 11 | 4 | 3 | 3 | 3 | 2 |
|
||||||
|
| 20th | +6 | ${_.sample(features)} | 11 | 4 | 3 | 3 | 3 | 2 |
|
||||||
|
}}\n\n`;
|
||||||
|
},
|
||||||
|
|
||||||
let cantrips = 3;
|
third : function(snippetClasses){
|
||||||
let spells = 1;
|
return dedent`
|
||||||
let slots = 2;
|
{{${snippetClasses}
|
||||||
return `{{${classes}\n##### ${classname} Spellcasting\n` +
|
##### ${_.sample(classnames)} Spellcasting
|
||||||
`| Class | Cantrips | Spells |--- Spells Slots per Spell Level ---||||\n` +
|
| Level | Cantrips | Spells |--- Spells Slots per Spell Level ---||||
|
||||||
`| Level ^| Known ^| Known ^| 1st | 2nd | 3rd | 4th |\n` +
|
| ^| Known ^| Known ^| 1st | 2nd | 3rd | 4th |
|
||||||
`|:------:|:--------:|:-------:|:-------:|:-------:|:-------:|:-------:|\n${
|
|:-----:|:--------:|:------:|:-------:|:-------:|:-------:|:-------:|
|
||||||
_.map(levels, function(levelName, level){
|
| 3rd | 2 | 3 | 2 | — | — | — |
|
||||||
const res = [
|
| 4th | 2 | 4 | 3 | — | — | — |
|
||||||
_.pad(levelName, 6),
|
| 5th | 2 | 4 | 3 | — | — | — |
|
||||||
_.pad(cantrips.toString(), 8),
|
| 6th | 2 | 4 | 3 | — | — | — |
|
||||||
_.pad(spells.toString(), 7),
|
| 7th | 2 | 5 | 4 | 2 | — | — |
|
||||||
drawSlots(slots, 4, 7),
|
| 8th | 2 | 6 | 4 | 2 | — | — |
|
||||||
].join(' | ');
|
| 9th | 2 | 6 | 4 | 2 | — | — |
|
||||||
|
| 10th | 3 | 7 | 4 | 3 | — | — |
|
||||||
cantrips += _.random(0, 1);
|
| 11th | 3 | 8 | 4 | 3 | — | — |
|
||||||
spells += _.random(0, 1);
|
| 12th | 3 | 8 | 4 | 3 | — | — |
|
||||||
slots += _.random(0, 1);
|
| 13th | 3 | 9 | 4 | 3 | 2 | — |
|
||||||
|
| 14th | 3 | 10 | 4 | 3 | 2 | — |
|
||||||
return `| ${res} |`;
|
| 15th | 3 | 10 | 4 | 3 | 2 | — |
|
||||||
}).join('\n')}\n}}\n\n`;
|
| 16th | 3 | 11 | 4 | 3 | 3 | — |
|
||||||
|
| 17th | 3 | 11 | 4 | 3 | 3 | — |
|
||||||
|
| 18th | 3 | 11 | 4 | 3 | 3 | — |
|
||||||
|
| 19th | 3 | 12 | 4 | 3 | 3 | 1 |
|
||||||
|
| 20th | 3 | 13 | 4 | 3 | 3 | 1 |
|
||||||
|
}}\n\n`;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -149,8 +149,6 @@ module.exports = {
|
|||||||

|

|
||||||
|
|
||||||
Homebrewery.Naturalcrit.com
|
Homebrewery.Naturalcrit.com
|
||||||
}}
|
}}`;
|
||||||
|
|
||||||
\page`;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -171,7 +171,7 @@ module.exports = {
|
|||||||
**Condition Immunities** :: ${genList(['groggy', 'swagged', 'weak-kneed', 'buzzed', 'groovy', 'melancholy', 'drunk'], 3)}
|
**Condition Immunities** :: ${genList(['groggy', 'swagged', 'weak-kneed', 'buzzed', 'groovy', 'melancholy', 'drunk'], 3)}
|
||||||
**Senses** :: darkvision 60 ft., passive Perception ${_.random(3, 20)}
|
**Senses** :: darkvision 60 ft., passive Perception ${_.random(3, 20)}
|
||||||
**Languages** :: ${genList(['Common', 'Pottymouth', 'Gibberish', 'Latin', 'Jive'], 2)}
|
**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')}
|
${_.times(_.random(genLines, genLines + 2), function(){return genAbilities();}).join('\n:\n')}
|
||||||
:
|
:
|
||||||
|
|||||||
51
themes/V3/5ePHB/snippets/quote.gen.js
Normal file
51
themes/V3/5ePHB/snippets/quote.gen.js
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
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.',
|
||||||
|
];
|
||||||
|
|
||||||
|
const authors = [
|
||||||
|
'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',
|
||||||
|
|
||||||
|
];
|
||||||
|
module.exports = ()=>{
|
||||||
|
return `
|
||||||
|
{{quote
|
||||||
|
${_.sample(quotes)}
|
||||||
|
|
||||||
|
{{attribution ${_.sample(authors)}, *${_.sample(books)}*}}
|
||||||
|
}}
|
||||||
|
\n`;
|
||||||
|
};
|
||||||
@@ -2,81 +2,74 @@ const _ = require('lodash');
|
|||||||
const dedent = require('dedent-tabs').default;
|
const dedent = require('dedent-tabs').default;
|
||||||
|
|
||||||
const getTOC = (pages)=>{
|
const getTOC = (pages)=>{
|
||||||
const add1 = (title, page)=>{
|
|
||||||
res.push({
|
const recursiveAdd = (title, page, targetDepth, child, curDepth=0)=>{
|
||||||
|
if(curDepth > 5) return; // Something went wrong.
|
||||||
|
if(curDepth == targetDepth) {
|
||||||
|
child.push({
|
||||||
title : title,
|
title : title,
|
||||||
page : page + 1,
|
page : page,
|
||||||
children : []
|
children : []
|
||||||
});
|
});
|
||||||
};
|
} else {
|
||||||
const add2 = (title, page)=>{
|
if(child.length == 0) {
|
||||||
if(!_.last(res)) add1(null, page);
|
child.push({
|
||||||
_.last(res).children.push({
|
title : null,
|
||||||
title : title,
|
page : page,
|
||||||
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 : []
|
children : []
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
recursiveAdd(title, page, targetDepth, _.last(child).children, curDepth+1,);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const res = [];
|
const res = [];
|
||||||
_.each(pages, (page, pageNum)=>{
|
|
||||||
const lines = page.split('\n');
|
const iframe = document.getElementById('BrewRenderer');
|
||||||
_.each(lines, (line)=>{
|
const iframeDocument = iframe.contentDocument || iframe.contentWindow.document;
|
||||||
if(_.startsWith(line, '# ')){
|
const headings = iframeDocument.querySelectorAll('h1, h2, h3, h4, h5, h6');
|
||||||
const title = line.replace('# ', '');
|
const headerDepth = ['H1', 'H2', 'H3', 'H4', 'H5', 'H6'];
|
||||||
add1(title, pageNum);
|
|
||||||
|
_.each(headings, (heading)=>{
|
||||||
|
const onPage = parseInt(heading.closest('.page').id?.replace(/^p/, ''));
|
||||||
|
const ToCExclude = getComputedStyle(heading).getPropertyValue('--TOC');
|
||||||
|
|
||||||
|
if(ToCExclude != 'exclude') {
|
||||||
|
recursiveAdd(heading.innerText.trim(), onPage, headerDepth.indexOf(heading.tagName), res);
|
||||||
}
|
}
|
||||||
if(_.startsWith(line, '## ')){
|
|
||||||
const title = line.replace('## ', '');
|
|
||||||
add2(title, pageNum);
|
|
||||||
}
|
|
||||||
if(_.startsWith(line, '### ')){
|
|
||||||
const title = line.replace('### ', '');
|
|
||||||
add3(title, pageNum);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = function(brew){
|
|
||||||
const pages = brew.text.split('\\page');
|
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 TOC = getTOC(pages);
|
||||||
const markdown = _.reduce(TOC, (r, g1, idx1)=>{
|
const markdown = _.reduce(TOC, (r, g1, idx1)=>{
|
||||||
if(g1.title !== null) {
|
r.push(ToCIterate(g1).join('\n'));
|
||||||
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})`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return r;
|
return r;
|
||||||
}, []).join('\n');
|
}, []).join('\n');
|
||||||
|
|
||||||
return dedent`
|
return dedent`
|
||||||
{{toc,wide
|
{{toc,wide
|
||||||
# Table Of Contents
|
# Contents
|
||||||
|
|
||||||
${markdown}
|
${markdown}
|
||||||
}}
|
}}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
const WatercolorGen = require('./snippets/watercolor.gen.js');
|
const WatercolorGen = require('./snippets/watercolor.gen.js');
|
||||||
const ImageMaskGen = require('./snippets/imageMask.gen.js');
|
const ImageMaskGen = require('./snippets/imageMask.gen.js');
|
||||||
|
const FooterGen = require('./snippets/footer.gen.js');
|
||||||
const dedent = require('dedent-tabs').default;
|
const dedent = require('dedent-tabs').default;
|
||||||
|
|
||||||
module.exports = [
|
module.exports = [
|
||||||
@@ -21,6 +22,53 @@ module.exports = [
|
|||||||
icon : 'fas fa-file-alt',
|
icon : 'fas fa-file-alt',
|
||||||
gen : '\n\\page\n'
|
gen : '\n\\page\n'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name : 'Page Number',
|
||||||
|
icon : 'fas fa-bookmark',
|
||||||
|
gen : '{{pageNumber 1}}\n'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Auto-incrementing Page Number',
|
||||||
|
icon : 'fas fa-sort-numeric-down',
|
||||||
|
gen : '{{pageNumber,auto}}\n'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Footer',
|
||||||
|
icon : 'fas fa-shoe-prints',
|
||||||
|
gen : FooterGen.createFooterFunc(),
|
||||||
|
subsnippets : [
|
||||||
|
{
|
||||||
|
name : 'Footer from H1',
|
||||||
|
icon : 'fas fa-dice-one',
|
||||||
|
gen : FooterGen.createFooterFunc(1)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Footer from H2',
|
||||||
|
icon : 'fas fa-dice-two',
|
||||||
|
gen : FooterGen.createFooterFunc(2)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Footer from H3',
|
||||||
|
icon : 'fas fa-dice-three',
|
||||||
|
gen : FooterGen.createFooterFunc(3)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Footer from H4',
|
||||||
|
icon : 'fas fa-dice-four',
|
||||||
|
gen : FooterGen.createFooterFunc(4)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Footer from H5',
|
||||||
|
icon : 'fas fa-dice-five',
|
||||||
|
gen : FooterGen.createFooterFunc(5)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Footer from H6',
|
||||||
|
icon : 'fas fa-dice-six',
|
||||||
|
gen : FooterGen.createFooterFunc(6)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name : 'Vertical Spacing',
|
name : 'Vertical Spacing',
|
||||||
icon : 'fas fa-arrows-alt-v',
|
icon : 'fas fa-arrows-alt-v',
|
||||||
@@ -63,6 +111,21 @@ module.exports = [
|
|||||||
icon : 'fas fa-code',
|
icon : 'fas fa-code',
|
||||||
gen : '<!-- This is a comment that will not be rendered into your brew. Hotkey (Ctrl/Cmd + /). -->'
|
gen : '<!-- This is a comment that will not be rendered into your brew. Hotkey (Ctrl/Cmd + /). -->'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name : 'Homebrewery Credit',
|
||||||
|
icon : 'fas fa-dice-d20',
|
||||||
|
gen : function(){
|
||||||
|
return dedent`
|
||||||
|
{{homebreweryCredits
|
||||||
|
Made With
|
||||||
|
|
||||||
|
{{homebreweryIcon}}
|
||||||
|
|
||||||
|
The Homebrewery
|
||||||
|
[Homebrewery.Naturalcrit.com](https://homebrewery.naturalcrit.com)
|
||||||
|
}}\n\n`;
|
||||||
|
},
|
||||||
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -241,6 +304,99 @@ module.exports = [
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
/**************** FONTS *************/
|
||||||
|
{
|
||||||
|
groupName : 'Fonts',
|
||||||
|
icon : 'fas fa-keyboard',
|
||||||
|
view : 'text',
|
||||||
|
snippets : [
|
||||||
|
{
|
||||||
|
name : 'Open Sans',
|
||||||
|
icon : 'font OpenSans',
|
||||||
|
gen : dedent`{{font-family:OpenSans Dummy Text}}`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Code Bold',
|
||||||
|
icon : 'font CodeBold',
|
||||||
|
gen : dedent`{{font-family:CodeBold Dummy Text}}`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Code Light',
|
||||||
|
icon : 'font CodeLight',
|
||||||
|
gen : dedent`{{font-family:CodeLight Dummy Text}}`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Scaly Sans',
|
||||||
|
icon : 'font ScalySansRemake',
|
||||||
|
gen : dedent`{{font-family:ScalySansRemake Dummy Text}}`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Book Insanity',
|
||||||
|
icon : 'font BookInsanityRemake',
|
||||||
|
gen : dedent`{{font-family:BookInsanityRemake Dummy Text}}`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Mr Eaves',
|
||||||
|
icon : 'font MrEavesRemake',
|
||||||
|
gen : dedent`{{font-family:MrEavesRemake 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 : '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 : '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 : 'Overpass',
|
||||||
|
icon : 'font Overpass',
|
||||||
|
gen : dedent`{{font-family:Overpass 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 : '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}}`
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
/**************** PAGE *************/
|
/**************** PAGE *************/
|
||||||
|
|
||||||
|
|||||||
17
themes/V3/Blank/snippets/footer.gen.js
Normal file
17
themes/V3/Blank/snippets/footer.gen.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
const Markdown = require('../../../../shared/naturalcrit/markdown.js');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
createFooterFunc : function(headerSize=1){
|
||||||
|
return (props)=>{
|
||||||
|
const cursorPos = props.cursorPos;
|
||||||
|
|
||||||
|
const markdownText = props.brew.text.split('\n').slice(0, cursorPos.line).join('\n');
|
||||||
|
const markdownTokens = Markdown.marked.lexer(markdownText);
|
||||||
|
const headerToken = markdownTokens.findLast((lexerToken)=>{ return lexerToken.type === 'heading' && lexerToken.depth === headerSize; });
|
||||||
|
const headerText = headerToken?.tokens.map((token)=>{ return token.text; }).join('');
|
||||||
|
const outputText = headerText || 'PART 1 | SECTION NAME';
|
||||||
|
|
||||||
|
return `\n{{footnote ${outputText}}}\n`;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,5 +1,9 @@
|
|||||||
@import (less) './themes/fonts/5e/fonts.less';
|
@import (less) './themes/fonts/5e/fonts.less';
|
||||||
@import (less) './themes/assets/assets.less';
|
@import (less) './themes/assets/assets.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 {
|
:root {
|
||||||
//Colors
|
//Colors
|
||||||
@@ -7,13 +11,9 @@
|
|||||||
--HB_Color_WatercolorStain : #000000; // Black
|
--HB_Color_WatercolorStain : #000000; // Black
|
||||||
}
|
}
|
||||||
|
|
||||||
@page { margin: 0; }
|
@page { margin : 0; }
|
||||||
body {
|
body { counter-reset : page-numbers; }
|
||||||
counter-reset : phb-page-numbers;
|
* { -webkit-print-color-adjust : exact; }
|
||||||
}
|
|
||||||
*{
|
|
||||||
-webkit-print-color-adjust : exact;
|
|
||||||
}
|
|
||||||
|
|
||||||
//*****************************
|
//*****************************
|
||||||
// * MUSTACHE DIVS/SPANS
|
// * MUSTACHE DIVS/SPANS
|
||||||
@@ -23,9 +23,7 @@ body {
|
|||||||
break-inside : avoid;
|
break-inside : avoid;
|
||||||
display : inline-block;
|
display : inline-block;
|
||||||
width : 100%;
|
width : 100%;
|
||||||
img {
|
img { z-index : 0; }
|
||||||
z-index : 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.inline-block {
|
.inline-block {
|
||||||
display : inline-block;
|
display : inline-block;
|
||||||
@@ -33,98 +31,81 @@ body {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.useColumns(@multiplier : 1, @fillMode: balance){
|
.useColumns(@multiplier : 1, @fillMode: auto) {
|
||||||
column-fill : @fillMode;
|
column-fill : @fillMode;
|
||||||
column-count : 2;
|
column-count : 2;
|
||||||
}
|
}
|
||||||
.columnWrapper{
|
.columnWrapper {
|
||||||
|
column-gap : inherit;
|
||||||
max-height : 100%;
|
max-height : 100%;
|
||||||
column-span : all;
|
column-span : all;
|
||||||
columns : inherit;
|
columns : inherit;
|
||||||
column-gap : inherit;
|
column-fill : inherit;
|
||||||
}
|
}
|
||||||
.page{
|
.page {
|
||||||
.useColumns();
|
.useColumns();
|
||||||
height : 279.4mm;
|
|
||||||
width : 215.9mm;
|
|
||||||
padding : 1.4cm 1.9cm 1.7cm;
|
|
||||||
counter-increment : phb-page-numbers;
|
|
||||||
background-color : var(--HB_Color_Background);
|
|
||||||
position : relative;
|
position : relative;
|
||||||
z-index : 15;
|
z-index : 15;
|
||||||
box-sizing : border-box;
|
box-sizing : border-box;
|
||||||
|
width : 215.9mm;
|
||||||
|
height : 279.4mm;
|
||||||
|
padding : 1.4cm 1.9cm 1.7cm;
|
||||||
overflow : hidden;
|
overflow : hidden;
|
||||||
|
counter-increment : page-numbers;
|
||||||
|
background-color : var(--HB_Color_Background);
|
||||||
text-rendering : optimizeLegibility;
|
text-rendering : optimizeLegibility;
|
||||||
page-break-before : always;
|
|
||||||
page-break-after : always;
|
|
||||||
contain : size;
|
contain : size;
|
||||||
}
|
}
|
||||||
//*****************************
|
//*****************************
|
||||||
// * BASE
|
// * BASE
|
||||||
// *****************************/
|
// *****************************/
|
||||||
.page{
|
.page {
|
||||||
p{
|
p {
|
||||||
overflow-wrap : break-word;
|
|
||||||
display : block;
|
display : block;
|
||||||
|
overflow-wrap : break-word;
|
||||||
}
|
}
|
||||||
strong{
|
strong { font-weight : bold; }
|
||||||
font-weight : bold;
|
em { font-style : italic; }
|
||||||
}
|
sup {
|
||||||
em{
|
font-size : smaller;
|
||||||
font-style : italic;
|
line-height : 0;
|
||||||
}
|
|
||||||
sup{
|
|
||||||
vertical-align : super;
|
vertical-align : super;
|
||||||
font-size : smaller;
|
|
||||||
line-height : 0;
|
|
||||||
}
|
}
|
||||||
sub{
|
sub {
|
||||||
vertical-align : sub;
|
|
||||||
font-size : smaller;
|
font-size : smaller;
|
||||||
line-height : 0;
|
line-height : 0;
|
||||||
|
vertical-align : sub;
|
||||||
}
|
}
|
||||||
ul {
|
ul {
|
||||||
|
padding-left : 1.4em;
|
||||||
list-style-position : outside; //Needed for multiline list items
|
list-style-position : outside; //Needed for multiline list items
|
||||||
list-style-type : disc;
|
list-style-type : disc;
|
||||||
padding-left : 1.4em;
|
|
||||||
}
|
}
|
||||||
ol {
|
ol {
|
||||||
|
padding-left : 1.4em;
|
||||||
list-style-position : outside;
|
list-style-position : outside;
|
||||||
list-style-type : decimal;
|
list-style-type : decimal;
|
||||||
padding-left : 1.4em;
|
|
||||||
}
|
|
||||||
img{
|
|
||||||
z-index : -1;
|
|
||||||
}
|
}
|
||||||
|
img { z-index : -1; }
|
||||||
|
|
||||||
//*****************************
|
//*****************************
|
||||||
// * HEADERS
|
// * HEADERS
|
||||||
// *****************************/
|
// *****************************/
|
||||||
h1,h2,h3,h4,h5,h6{
|
h1,h2,h3,h4,h5,h6 {
|
||||||
font-weight : bold;
|
font-weight : bold;
|
||||||
line-height : 1.2em;
|
line-height : 1.2em;
|
||||||
}
|
}
|
||||||
h1{
|
h1 { font-size : 2em; }
|
||||||
font-size : 2em;
|
h2 { font-size : 1.5em; }
|
||||||
}
|
h3 { font-size : 1.17em; }
|
||||||
h2{
|
h4 { font-size : 1em; }
|
||||||
font-size : 1.5em;
|
h5 { font-size : 0.83em; }
|
||||||
}
|
|
||||||
h3{
|
|
||||||
font-size : 1.17em;
|
|
||||||
}
|
|
||||||
h4{
|
|
||||||
font-size : 1em;
|
|
||||||
}
|
|
||||||
h5{
|
|
||||||
font-size : 0.83em;
|
|
||||||
}
|
|
||||||
//*****************************
|
//*****************************
|
||||||
// * TABLE
|
// * TABLE
|
||||||
// *****************************/
|
// *****************************/
|
||||||
table{
|
table {
|
||||||
width : 100%;
|
width : 100%;
|
||||||
thead{
|
thead {
|
||||||
display : table-row-group;
|
display : table-row-group;
|
||||||
font-weight : bold;
|
font-weight : bold;
|
||||||
}
|
}
|
||||||
@@ -136,42 +117,40 @@ body {
|
|||||||
//************************************
|
//************************************
|
||||||
// * CODE BLOCKS
|
// * CODE BLOCKS
|
||||||
// ************************************/
|
// ************************************/
|
||||||
code{
|
code {
|
||||||
font-family : "Courier New", Courier, monospace;
|
font-family : 'Courier New', "Courier", monospace;
|
||||||
white-space : pre-wrap;
|
|
||||||
overflow-wrap : break-word;
|
overflow-wrap : break-word;
|
||||||
|
white-space : pre-wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
pre code{
|
pre code {
|
||||||
width : 100%;
|
|
||||||
display : inline-block;
|
display : inline-block;
|
||||||
|
width : 100%;
|
||||||
}
|
}
|
||||||
//*****************************
|
//*****************************
|
||||||
// * EXTRAS
|
// * EXTRAS
|
||||||
// *****************************/
|
// *****************************/
|
||||||
.columnSplit {
|
.columnSplit {
|
||||||
|
margin-top : 0;
|
||||||
visibility : hidden;
|
visibility : hidden;
|
||||||
-webkit-column-break-after : always;
|
-webkit-column-break-after : always;
|
||||||
break-after : always;
|
break-after : always;
|
||||||
-moz-column-break-after : always;
|
-moz-column-break-after : always;
|
||||||
margin-top : 0;
|
& + * { margin-top : 0; }
|
||||||
& + * {
|
|
||||||
margin-top : 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
//Avoid breaking up
|
//Avoid breaking up
|
||||||
blockquote,table{
|
blockquote,table {
|
||||||
z-index : 15;
|
z-index : 15;
|
||||||
-webkit-column-break-inside : avoid;
|
-webkit-column-break-inside : avoid;
|
||||||
page-break-inside : avoid;
|
page-break-inside : avoid;
|
||||||
break-inside : avoid;
|
break-inside : avoid;
|
||||||
}
|
}
|
||||||
// Nested lists
|
// Nested lists
|
||||||
ul ul,ol ol,ul ol,ol ul{
|
ul ul,ol ol,ul ol,ol ul {
|
||||||
margin-bottom : 0px;
|
margin-bottom : 0px;
|
||||||
margin-left : 1.5em;
|
margin-left : 1.5em;
|
||||||
}
|
}
|
||||||
li{
|
li {
|
||||||
-webkit-column-break-inside : avoid;
|
-webkit-column-break-inside : avoid;
|
||||||
page-break-inside : avoid;
|
page-break-inside : avoid;
|
||||||
break-inside : avoid;
|
break-inside : avoid;
|
||||||
@@ -179,41 +158,38 @@ body {
|
|||||||
|
|
||||||
/* Watermark */
|
/* Watermark */
|
||||||
.watermark {
|
.watermark {
|
||||||
|
position : absolute;
|
||||||
|
top : 0;
|
||||||
|
left : 0;
|
||||||
|
z-index : 500;
|
||||||
display : grid !important;
|
display : grid !important;
|
||||||
place-items : center;
|
place-items : center;
|
||||||
justify-content : center;
|
justify-content : center;
|
||||||
position : absolute;
|
|
||||||
margin : 0;
|
|
||||||
top : 0;
|
|
||||||
left : 0;
|
|
||||||
width : 100%;
|
width : 100%;
|
||||||
height : 100%;
|
height : 100%;
|
||||||
|
margin : 0;
|
||||||
font-size : 120px;
|
font-size : 120px;
|
||||||
text-transform : uppercase;
|
text-transform : uppercase;
|
||||||
mix-blend-mode : overlay;
|
|
||||||
opacity : 30%;
|
opacity : 30%;
|
||||||
transform : rotate(-45deg);
|
transform : rotate(-45deg);
|
||||||
z-index : 500;
|
p { margin-bottom : none; }
|
||||||
p {
|
|
||||||
margin-bottom : none;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Watercolor */
|
/* Watercolor */
|
||||||
[class*="watercolor"] {
|
[class*='watercolor'] {
|
||||||
position : absolute;
|
position : absolute;
|
||||||
|
z-index : -2;
|
||||||
width : 2000px; /* dimensions need to be real big so the user can set */
|
width : 2000px; /* dimensions need to be real big so the user can set */
|
||||||
height : 2000px; /* height or width and the image will maintain aspect ratio */
|
height : 2000px; /* height or width and the image will maintain aspect ratio */
|
||||||
|
background-color : var(--HB_Color_WatercolorStain); /* default color */
|
||||||
|
background-size : cover;
|
||||||
-webkit-mask-image : var(--wc);
|
-webkit-mask-image : var(--wc);
|
||||||
-webkit-mask-size : contain;
|
-webkit-mask-size : contain;
|
||||||
-webkit-mask-repeat : no-repeat;
|
-webkit-mask-repeat : no-repeat;
|
||||||
mask-image : var(--wc);
|
mask-image : var(--wc);
|
||||||
mask-size : contain;
|
mask-size : contain;
|
||||||
mask-repeat : no-repeat;
|
mask-repeat : no-repeat;
|
||||||
background-size : cover;
|
--wc : @watercolor1; /* default image */
|
||||||
background-color : var(--HB_Color_WatercolorStain); /*default color*/
|
|
||||||
--wc : @watercolor1; /*default image*/
|
|
||||||
z-index : -2;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.watercolor1 { --wc : @watercolor1; }
|
.watercolor1 { --wc : @watercolor1; }
|
||||||
@@ -231,17 +207,16 @@ body {
|
|||||||
|
|
||||||
/* Image Masks */
|
/* Image Masks */
|
||||||
|
|
||||||
[class*="imageMask"] {
|
[class*='imageMask'] {
|
||||||
position : absolute;
|
position : absolute;
|
||||||
height : 200%;
|
|
||||||
width : 200%;
|
|
||||||
left : 50%;
|
|
||||||
bottom : 50%;
|
bottom : 50%;
|
||||||
--rotation : 0;
|
left : 50%;
|
||||||
--revealer : none;
|
z-index : -1;
|
||||||
--checkerboard : none;
|
width : 200%;
|
||||||
--scaleX : 1;
|
height : 200%;
|
||||||
--scaleY : 1;
|
background-image : var(--checkerboard);
|
||||||
|
background-size : 20px;
|
||||||
|
transform : translateY(50%) translateX(-50%) rotate(calc(1deg * var(--rotation))) scaleX(var(--scaleX)) scaleY(var(--scaleY));
|
||||||
-webkit-mask-image : var(--wc), var(--revealer);
|
-webkit-mask-image : var(--wc), var(--revealer);
|
||||||
-webkit-mask-repeat : repeat-x;
|
-webkit-mask-repeat : repeat-x;
|
||||||
-webkit-mask-size : 50%; //Scale only X to fit page width, leave height at aspect ratio, designed to hang off the edge
|
-webkit-mask-size : 50%; //Scale only X to fit page width, leave height at aspect ratio, designed to hang off the edge
|
||||||
@@ -250,61 +225,63 @@ body {
|
|||||||
mask-repeat : repeat-x;
|
mask-repeat : repeat-x;
|
||||||
mask-size : 50%;
|
mask-size : 50%;
|
||||||
mask-position : 50% calc(50% - var(--offset));
|
mask-position : 50% calc(50% - var(--offset));
|
||||||
background-image : var(--checkerboard);
|
--rotation : 0;
|
||||||
background-size : 20px;
|
--revealer : none;
|
||||||
z-index : -1;
|
--checkerboard : none;
|
||||||
transform : translateY(50%) translateX(-50%) rotate(calc(1deg * var(--rotation))) scaleX(var(--scaleX)) scaleY(var(--scaleY));
|
--scaleX : 1;
|
||||||
|
--scaleY : 1;
|
||||||
& > p:has(img) {
|
& > p:has(img) {
|
||||||
position : absolute;
|
position : absolute;
|
||||||
width : 50%;
|
|
||||||
height : 50%;
|
|
||||||
bottom : 50%;
|
bottom : 50%;
|
||||||
left : 50%;
|
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%) rotate(calc(-1deg * var(--rotation))) scaleX(calc(1 / var(--scaleX))) scaleY(calc(1 / var(--scaleY)));
|
||||||
}
|
}
|
||||||
& img {
|
& img {
|
||||||
position : absolute;
|
position : absolute;
|
||||||
display : block;
|
|
||||||
bottom : 0;
|
bottom : 0;
|
||||||
|
display : block;
|
||||||
}
|
}
|
||||||
&.bottom {
|
&.bottom {
|
||||||
--rotation : 0;
|
--rotation : 0;
|
||||||
& img {bottom: 0;}
|
& img {bottom : 0;}
|
||||||
}
|
}
|
||||||
&.top {
|
&.top {
|
||||||
--rotation : 180;
|
--rotation : 180;
|
||||||
& img {top: 0;}
|
& img {top : 0;}
|
||||||
}
|
}
|
||||||
&.left {
|
&.left {
|
||||||
--rotation : 90;
|
--rotation : 90;
|
||||||
& img {left: 0;}
|
& img {left : 0;}
|
||||||
}
|
}
|
||||||
&.right {
|
&.right {
|
||||||
--rotation : -90;
|
--rotation : -90;
|
||||||
& img {right: 0;}
|
& img {right : 0;}
|
||||||
}
|
}
|
||||||
&.revealImage {
|
&.revealImage {
|
||||||
--revealer : linear-gradient(0deg, rgba(0,0,0,.2) 0%, rgba(0,0,0,0.2));
|
--revealer : linear-gradient(0deg, rgba(0,0,0,0.2) 0%, rgba(0,0,0,0.2));
|
||||||
--checkerboard : url(/assets/waterColorMasks/missingImage.png); //shows any masked regions not filled by image
|
--checkerboard : url("/assets/waterColorMasks/missingImage.png"); //shows any masked regions not filled by image
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.imageMaskEdge {
|
.imageMaskEdge {
|
||||||
&1 { --wc : url(/assets/waterColorMasks/edge/0001.webp); }
|
&1 { --wc : url("/assets/waterColorMasks/edge/0001.webp"); }
|
||||||
&2 { --wc : url(/assets/waterColorMasks/edge/0002.webp); }
|
&2 { --wc : url("/assets/waterColorMasks/edge/0002.webp"); }
|
||||||
&3 { --wc : url(/assets/waterColorMasks/edge/0003.webp); }
|
&3 { --wc : url("/assets/waterColorMasks/edge/0003.webp"); }
|
||||||
&4 { --wc : url(/assets/waterColorMasks/edge/0004.webp); }
|
&4 { --wc : url("/assets/waterColorMasks/edge/0004.webp"); }
|
||||||
&5 { --wc : url(/assets/waterColorMasks/edge/0005.webp); }
|
&5 { --wc : url("/assets/waterColorMasks/edge/0005.webp"); }
|
||||||
&6 { --wc : url(/assets/waterColorMasks/edge/0006.webp); }
|
&6 { --wc : url("/assets/waterColorMasks/edge/0006.webp"); }
|
||||||
&7 { --wc : url(/assets/waterColorMasks/edge/0007.webp); }
|
&7 { --wc : url("/assets/waterColorMasks/edge/0007.webp"); }
|
||||||
&8 { --wc : url(/assets/waterColorMasks/edge/0008.webp); }
|
&8 { --wc : url("/assets/waterColorMasks/edge/0008.webp"); }
|
||||||
}
|
}
|
||||||
|
|
||||||
[class*="imageMaskCenter"] {
|
[class*='imageMaskCenter'] {
|
||||||
|
bottom : calc(var(--offsetY));
|
||||||
|
left : calc(var(--offsetX));
|
||||||
width : 100%;
|
width : 100%;
|
||||||
height : 100%;
|
height : 100%;
|
||||||
left : calc(var(--offsetX));
|
transform : rotate(calc(1deg * var(--rotation))) scaleX(var(--scaleX)) scaleY(var(--scaleY));
|
||||||
bottom : calc(var(--offsetY));
|
|
||||||
-webkit-mask-image : var(--wc), var(--revealer);
|
-webkit-mask-image : var(--wc), var(--revealer);
|
||||||
-webkit-mask-repeat : no-repeat;
|
-webkit-mask-repeat : no-repeat;
|
||||||
-webkit-mask-size : 100% 100%; //Scale both dimensions to fit page size
|
-webkit-mask-size : 100% 100%; //Scale both dimensions to fit page size
|
||||||
@@ -313,14 +290,13 @@ body {
|
|||||||
mask-repeat : no-repeat;
|
mask-repeat : no-repeat;
|
||||||
mask-size : 100% 100%; //Scale both dimensions to fit page size
|
mask-size : 100% 100%; //Scale both dimensions to fit page size
|
||||||
mask-position : 50% 50%;
|
mask-position : 50% 50%;
|
||||||
transform : rotate(calc(1deg * var(--rotation))) scaleX(var(--scaleX)) scaleY(var(--scaleY));
|
|
||||||
|
|
||||||
& > p:has(img) {
|
& > p:has(img) {
|
||||||
position : absolute;
|
position : absolute;
|
||||||
width : 100%;
|
|
||||||
height : 100%;
|
|
||||||
bottom : 0;
|
bottom : 0;
|
||||||
left : 0;
|
left : 0;
|
||||||
|
width : 100%;
|
||||||
|
height : 100%;
|
||||||
transform : unset;
|
transform : unset;
|
||||||
transform : scaleX(calc(1 / var(--scaleX))) scaleY(calc(1 / var(--scaleY)))
|
transform : scaleX(calc(1 / var(--scaleX))) scaleY(calc(1 / var(--scaleY)))
|
||||||
rotate(calc(-1deg * var(--rotation)))
|
rotate(calc(-1deg * var(--rotation)))
|
||||||
@@ -330,31 +306,32 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.imageMaskCenter {
|
.imageMaskCenter {
|
||||||
&1 { --wc : url(/assets/waterColorMasks/center/0001.webp); }
|
&1 { --wc : url("/assets/waterColorMasks/center/0001.webp"); }
|
||||||
&2 { --wc : url(/assets/waterColorMasks/center/0002.webp); }
|
&2 { --wc : url("/assets/waterColorMasks/center/0002.webp"); }
|
||||||
&3 { --wc : url(/assets/waterColorMasks/center/0003.webp); }
|
&3 { --wc : url("/assets/waterColorMasks/center/0003.webp"); }
|
||||||
&4 { --wc : url(/assets/waterColorMasks/center/0004.webp); }
|
&4 { --wc : url("/assets/waterColorMasks/center/0004.webp"); }
|
||||||
&5 { --wc : url(/assets/waterColorMasks/center/0005.webp); }
|
&5 { --wc : url("/assets/waterColorMasks/center/0005.webp"); }
|
||||||
&6 { --wc : url(/assets/waterColorMasks/center/0006.webp); }
|
&6 { --wc : url("/assets/waterColorMasks/center/0006.webp"); }
|
||||||
&7 { --wc : url(/assets/waterColorMasks/center/0007.webp); }
|
&7 { --wc : url("/assets/waterColorMasks/center/0007.webp"); }
|
||||||
&8 { --wc : url(/assets/waterColorMasks/center/0008.webp); }
|
&8 { --wc : url("/assets/waterColorMasks/center/0008.webp"); }
|
||||||
&9 { --wc : url(/assets/waterColorMasks/center/0009.webp); }
|
&9 { --wc : url("/assets/waterColorMasks/center/0009.webp"); }
|
||||||
&10 { --wc : url(/assets/waterColorMasks/center/0010.webp); }
|
&10 { --wc : url("/assets/waterColorMasks/center/0010.webp"); }
|
||||||
&11 { --wc : url(/assets/waterColorMasks/center/0011.webp); }
|
&11 { --wc : url("/assets/waterColorMasks/center/0011.webp"); }
|
||||||
&12 { --wc : url(/assets/waterColorMasks/center/0012.webp); }
|
&12 { --wc : url("/assets/waterColorMasks/center/0012.webp"); }
|
||||||
&13 { --wc : url(/assets/waterColorMasks/center/0013.webp); }
|
&13 { --wc : url("/assets/waterColorMasks/center/0013.webp"); }
|
||||||
&14 { --wc : url(/assets/waterColorMasks/center/0014.webp); }
|
&14 { --wc : url("/assets/waterColorMasks/center/0014.webp"); }
|
||||||
&15 { --wc : url(/assets/waterColorMasks/center/0015.webp); }
|
&15 { --wc : url("/assets/waterColorMasks/center/0015.webp"); }
|
||||||
&16 { --wc : url(/assets/waterColorMasks/center/0016.webp); }
|
&16 { --wc : url("/assets/waterColorMasks/center/0016.webp"); }
|
||||||
&special { --wc : url(/assets/waterColorMasks/center/special.webp); }
|
&special { --wc : url("/assets/waterColorMasks/center/special.webp"); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
[class*="imageMaskCorner"] {
|
[class*='imageMaskCorner'] {
|
||||||
height : 200%;
|
|
||||||
width : 200%;
|
|
||||||
left : calc(-50% + var(--offsetX));
|
|
||||||
bottom : calc(-50% + var(--offsetY));
|
bottom : calc(-50% + var(--offsetY));
|
||||||
|
left : calc(-50% + var(--offsetX));
|
||||||
|
width : 200%;
|
||||||
|
height : 200%;
|
||||||
|
transform : rotate(calc(1deg * var(--rotation))) scaleX(var(--scaleX)) scaleY(var(--scaleY));
|
||||||
-webkit-mask-image : var(--wc), var(--revealer);
|
-webkit-mask-image : var(--wc), var(--revealer);
|
||||||
-webkit-mask-repeat : no-repeat;
|
-webkit-mask-repeat : no-repeat;
|
||||||
-webkit-mask-size : 100% 100%; //Scale both dimensions to fit page size
|
-webkit-mask-size : 100% 100%; //Scale both dimensions to fit page size
|
||||||
@@ -363,12 +340,11 @@ body {
|
|||||||
mask-repeat : no-repeat;
|
mask-repeat : no-repeat;
|
||||||
mask-size : 100% 100%; //Scale both dimensions to fit page size
|
mask-size : 100% 100%; //Scale both dimensions to fit page size
|
||||||
mask-position : 50% 50%;
|
mask-position : 50% 50%;
|
||||||
transform : rotate(calc(1deg * var(--rotation))) scaleX(var(--scaleX)) scaleY(var(--scaleY));
|
|
||||||
& > p:has(img) {
|
& > p:has(img) {
|
||||||
|
bottom : 25%;
|
||||||
|
left : 25%;
|
||||||
width : 50%;
|
width : 50%;
|
||||||
height : 50%; //Complex transform below to handle mix of % and cm offsets
|
height : 50%; //Complex transform below to handle mix of % and cm offsets
|
||||||
left : 25%;
|
|
||||||
bottom : 25%;
|
|
||||||
transform : scaleX(calc(1 / var(--scaleX))) scaleY(calc(1 / var(--scaleY)))
|
transform : scaleX(calc(1 / var(--scaleX))) scaleY(calc(1 / var(--scaleY)))
|
||||||
rotate(calc(-1deg * var(--rotation)))
|
rotate(calc(-1deg * var(--rotation)))
|
||||||
translateX(calc(-1 * var(--offsetX)))
|
translateX(calc(-1 * var(--offsetX)))
|
||||||
@@ -376,43 +352,43 @@ body {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.imageMaskCorner {
|
.imageMaskCorner {
|
||||||
&1 { --wc : url(/assets/waterColorMasks/corner/0001.webp); }
|
&1 { --wc : url("/assets/waterColorMasks/corner/0001.webp"); }
|
||||||
&2 { --wc : url(/assets/waterColorMasks/corner/0002.webp); }
|
&2 { --wc : url("/assets/waterColorMasks/corner/0002.webp"); }
|
||||||
&3 { --wc : url(/assets/waterColorMasks/corner/0003.webp); }
|
&3 { --wc : url("/assets/waterColorMasks/corner/0003.webp"); }
|
||||||
&4 { --wc : url(/assets/waterColorMasks/corner/0004.webp); }
|
&4 { --wc : url("/assets/waterColorMasks/corner/0004.webp"); }
|
||||||
&5 { --wc : url(/assets/waterColorMasks/corner/0005.webp); }
|
&5 { --wc : url("/assets/waterColorMasks/corner/0005.webp"); }
|
||||||
&6 { --wc : url(/assets/waterColorMasks/corner/0006.webp); }
|
&6 { --wc : url("/assets/waterColorMasks/corner/0006.webp"); }
|
||||||
&7 { --wc : url(/assets/waterColorMasks/corner/0007.webp); }
|
&7 { --wc : url("/assets/waterColorMasks/corner/0007.webp"); }
|
||||||
&8 { --wc : url(/assets/waterColorMasks/corner/0008.webp); }
|
&8 { --wc : url("/assets/waterColorMasks/corner/0008.webp"); }
|
||||||
&9 { --wc : url(/assets/waterColorMasks/corner/0009.webp); }
|
&9 { --wc : url("/assets/waterColorMasks/corner/0009.webp"); }
|
||||||
&10 { --wc : url(/assets/waterColorMasks/corner/0010.webp); }
|
&10 { --wc : url("/assets/waterColorMasks/corner/0010.webp"); }
|
||||||
&11 { --wc : url(/assets/waterColorMasks/corner/0011.webp); }
|
&11 { --wc : url("/assets/waterColorMasks/corner/0011.webp"); }
|
||||||
&12 { --wc : url(/assets/waterColorMasks/corner/0012.webp); }
|
&12 { --wc : url("/assets/waterColorMasks/corner/0012.webp"); }
|
||||||
&13 { --wc : url(/assets/waterColorMasks/corner/0013.webp); }
|
&13 { --wc : url("/assets/waterColorMasks/corner/0013.webp"); }
|
||||||
&14 { --wc : url(/assets/waterColorMasks/corner/0014.webp); }
|
&14 { --wc : url("/assets/waterColorMasks/corner/0014.webp"); }
|
||||||
&15 { --wc : url(/assets/waterColorMasks/corner/0015.webp); }
|
&15 { --wc : url("/assets/waterColorMasks/corner/0015.webp"); }
|
||||||
&16 { --wc : url(/assets/waterColorMasks/corner/0016.webp); }
|
&16 { --wc : url("/assets/waterColorMasks/corner/0016.webp"); }
|
||||||
&17 { --wc : url(/assets/waterColorMasks/corner/0017.webp); }
|
&17 { --wc : url("/assets/waterColorMasks/corner/0017.webp"); }
|
||||||
&18 { --wc : url(/assets/waterColorMasks/corner/0018.webp); }
|
&18 { --wc : url("/assets/waterColorMasks/corner/0018.webp"); }
|
||||||
&19 { --wc : url(/assets/waterColorMasks/corner/0019.webp); }
|
&19 { --wc : url("/assets/waterColorMasks/corner/0019.webp"); }
|
||||||
&20 { --wc : url(/assets/waterColorMasks/corner/0020.webp); }
|
&20 { --wc : url("/assets/waterColorMasks/corner/0020.webp"); }
|
||||||
&21 { --wc : url(/assets/waterColorMasks/corner/0021.webp); }
|
&21 { --wc : url("/assets/waterColorMasks/corner/0021.webp"); }
|
||||||
&22 { --wc : url(/assets/waterColorMasks/corner/0022.webp); }
|
&22 { --wc : url("/assets/waterColorMasks/corner/0022.webp"); }
|
||||||
&23 { --wc : url(/assets/waterColorMasks/corner/0023.webp); }
|
&23 { --wc : url("/assets/waterColorMasks/corner/0023.webp"); }
|
||||||
&24 { --wc : url(/assets/waterColorMasks/corner/0024.webp); }
|
&24 { --wc : url("/assets/waterColorMasks/corner/0024.webp"); }
|
||||||
&25 { --wc : url(/assets/waterColorMasks/corner/0025.webp); }
|
&25 { --wc : url("/assets/waterColorMasks/corner/0025.webp"); }
|
||||||
&26 { --wc : url(/assets/waterColorMasks/corner/0026.webp); }
|
&26 { --wc : url("/assets/waterColorMasks/corner/0026.webp"); }
|
||||||
&27 { --wc : url(/assets/waterColorMasks/corner/0027.webp); }
|
&27 { --wc : url("/assets/waterColorMasks/corner/0027.webp"); }
|
||||||
&28 { --wc : url(/assets/waterColorMasks/corner/0028.webp); }
|
&28 { --wc : url("/assets/waterColorMasks/corner/0028.webp"); }
|
||||||
&29 { --wc : url(/assets/waterColorMasks/corner/0029.webp); }
|
&29 { --wc : url("/assets/waterColorMasks/corner/0029.webp"); }
|
||||||
&30 { --wc : url(/assets/waterColorMasks/corner/0030.webp); }
|
&30 { --wc : url("/assets/waterColorMasks/corner/0030.webp"); }
|
||||||
&31 { --wc : url(/assets/waterColorMasks/corner/0031.webp); }
|
&31 { --wc : url("/assets/waterColorMasks/corner/0031.webp"); }
|
||||||
&32 { --wc : url(/assets/waterColorMasks/corner/0032.webp); }
|
&32 { --wc : url("/assets/waterColorMasks/corner/0032.webp"); }
|
||||||
&33 { --wc : url(/assets/waterColorMasks/corner/0033.webp); }
|
&33 { --wc : url("/assets/waterColorMasks/corner/0033.webp"); }
|
||||||
&34 { --wc : url(/assets/waterColorMasks/corner/0034.webp); }
|
&34 { --wc : url("/assets/waterColorMasks/corner/0034.webp"); }
|
||||||
&35 { --wc : url(/assets/waterColorMasks/corner/0035.webp); }
|
&35 { --wc : url("/assets/waterColorMasks/corner/0035.webp"); }
|
||||||
&36 { --wc : url(/assets/waterColorMasks/corner/0036.webp); }
|
&36 { --wc : url("/assets/waterColorMasks/corner/0036.webp"); }
|
||||||
&37 { --wc : url(/assets/waterColorMasks/corner/0037.webp); }
|
&37 { --wc : url("/assets/waterColorMasks/corner/0037.webp"); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -443,9 +419,7 @@ body {
|
|||||||
.blank {
|
.blank {
|
||||||
height : 1em;
|
height : 1em;
|
||||||
margin-top : 0;
|
margin-top : 0;
|
||||||
& + * {
|
& + * { margin-top : 0; }
|
||||||
margin-top : 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -453,12 +427,58 @@ body {
|
|||||||
// * WIDE
|
// * WIDE
|
||||||
// *****************************/
|
// *****************************/
|
||||||
.page {
|
.page {
|
||||||
.wide{
|
.wide {
|
||||||
column-span : all;
|
column-span : all;
|
||||||
display : block;
|
display : block;
|
||||||
margin-bottom : 1em;
|
margin-bottom : 1em;
|
||||||
&+* {
|
& + * { margin-top : 0; }
|
||||||
margin-top : 0;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//*****************************
|
||||||
|
//* CREDITS
|
||||||
|
//*****************************/
|
||||||
|
.page .homebreweryCredits {
|
||||||
|
p {
|
||||||
|
font-family : 'NodestoCapsWide';
|
||||||
|
font-size : 0.4cm;
|
||||||
|
line-height : 1em;
|
||||||
|
text-align : center;
|
||||||
|
text-indent : 0;
|
||||||
|
letter-spacing : 0.08em;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color : inherit;
|
||||||
|
text-decoration : none;
|
||||||
|
&:hover { text-decoration : underline; }
|
||||||
|
}
|
||||||
|
.homebreweryIcon {
|
||||||
|
display : block;
|
||||||
|
height : 1.5cm;
|
||||||
|
margin : 0 auto;
|
||||||
|
background-color : black;
|
||||||
|
-webkit-mask : url("/assets/naturalCritLogoWhite.svg") center / contain no-repeat;
|
||||||
|
mask : url("/assets/naturalCritLogoWhite.svg") center / contain no-repeat;
|
||||||
|
}
|
||||||
|
.homebreweryIcon.red { background-color : red; }
|
||||||
|
.homebreweryIcon.gold { background-image : linear-gradient(to top left, brown 22.5%, gold 40%, white 60%, gold 67.5%, brown 82.5%); }
|
||||||
|
}
|
||||||
|
|
||||||
|
//*****************************
|
||||||
|
//* Page Number
|
||||||
|
//*****************************/
|
||||||
|
.page {
|
||||||
|
.pageNumber {
|
||||||
|
position : absolute;
|
||||||
|
right : 30px;
|
||||||
|
bottom : 30px;
|
||||||
|
width : 50px;
|
||||||
|
font-size : 0.9em;
|
||||||
|
text-align : center;
|
||||||
|
&.auto::after { content : counter(page-numbers); }
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(even) {
|
||||||
|
.pageNumber { left : 30px; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -374,17 +374,9 @@
|
|||||||
}
|
}
|
||||||
.pageNumber{
|
.pageNumber{
|
||||||
font-family : FrederickaTheGreat;
|
font-family : FrederickaTheGreat;
|
||||||
position : absolute;
|
|
||||||
right : 3cm;
|
right : 3cm;
|
||||||
bottom : 1.25cm;
|
bottom : 1.25cm;
|
||||||
width : 50px;
|
|
||||||
font-size : 0.9em;
|
|
||||||
color : var(--HB_Color_HeaderText);
|
color : var(--HB_Color_HeaderText);
|
||||||
text-align : center;
|
|
||||||
text-indent : 0;
|
|
||||||
&.auto::after {
|
|
||||||
content : counter(phb-page-numbers);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.footnote{
|
.footnote{
|
||||||
position : absolute;
|
position : absolute;
|
||||||
|
|||||||
88
themes/codeMirror/customEditorStyles.less
Normal file
88
themes/codeMirror/customEditorStyles.less
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
.editor .codeEditor .CodeMirror {
|
||||||
|
// Themes with dark backgrounds
|
||||||
|
&.cm-s-3024-night,
|
||||||
|
&.cm-s-abbott,
|
||||||
|
&.cm-s-abcdef,
|
||||||
|
&.cm-s-ambiance,
|
||||||
|
&.cm-s-ayu-dark,
|
||||||
|
&.cm-s-ayu-mirage,
|
||||||
|
&.cm-s-base16-dark,
|
||||||
|
&.cm-s-bespin,
|
||||||
|
&.cm-s-blackboard,
|
||||||
|
&.cm-s-cobalt,
|
||||||
|
&.cm-s-colorforth,
|
||||||
|
&.cm-s-darcula,
|
||||||
|
&.cm-s-dracula,
|
||||||
|
&.cm-s-duotone-dark,
|
||||||
|
&.cm-s-erlang-dark,
|
||||||
|
&.cm-s-gruvbox-dark,
|
||||||
|
&.cm-s-hopscotch,
|
||||||
|
&.cm-s-icecoder,
|
||||||
|
&.cm-s-isotope,
|
||||||
|
&.cm-s-lesser-dark,
|
||||||
|
&.cm-s-liquibyte,
|
||||||
|
&.cm-s-lucario,
|
||||||
|
&.cm-s-material,
|
||||||
|
&.cm-s-material-darker,
|
||||||
|
&.cm-s-material-ocean,
|
||||||
|
&.cm-s-material-palenight,
|
||||||
|
&.cm-s-mbo,
|
||||||
|
&.cm-s-midnight,
|
||||||
|
&.cm-s-monokai,
|
||||||
|
&.cm-s-moxer,
|
||||||
|
&.cm-s-night,
|
||||||
|
&.cm-s-nord,
|
||||||
|
&.cm-s-oceanic-next,
|
||||||
|
&.cm-s-panda-syntax,
|
||||||
|
&.cm-s-paraiso-dark,
|
||||||
|
&.cm-s-pastel-on-dark,
|
||||||
|
&.cm-s-railscasts,
|
||||||
|
&.cm-s-rubyblue,
|
||||||
|
&.cm-s-seti,
|
||||||
|
&.cm-s-shadowfox,
|
||||||
|
&.cm-s-the-matrix,
|
||||||
|
&.cm-s-tomorrow-night-bright,
|
||||||
|
&.cm-s-tomorrow-night-eighties,
|
||||||
|
&.cm-s-twilight,
|
||||||
|
&.cm-s-vibrant-ink,
|
||||||
|
&.cm-s-xq-dark,
|
||||||
|
&.cm-s-yonce,
|
||||||
|
&.cm-s-zenburn
|
||||||
|
{
|
||||||
|
.CodeMirror-code {
|
||||||
|
.block:not(.cm-comment) {
|
||||||
|
color: magenta;
|
||||||
|
}
|
||||||
|
.columnSplit {
|
||||||
|
color: black;
|
||||||
|
background-color: rgba(35,153,153,0.5);
|
||||||
|
}
|
||||||
|
.pageLine {
|
||||||
|
background-color: rgba(255,255,255,0.5);
|
||||||
|
& ~ pre.CodeMirror-line {
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Themes with light backgrounds
|
||||||
|
&.cm-s-default,
|
||||||
|
&.cm-s-3024-day,
|
||||||
|
&.cm-s-ambiance-mobile,
|
||||||
|
&.cm-s-base16-light,
|
||||||
|
&.cm-s-duotone-light,
|
||||||
|
&.cm-s-eclipse,
|
||||||
|
&.cm-s-elegant,
|
||||||
|
&.cm-s-juejin,
|
||||||
|
&.cm-s-neat,
|
||||||
|
&.cm-s-neo,
|
||||||
|
&.cm-s-paraiso-lightm
|
||||||
|
&.cm-s-solarized,
|
||||||
|
&.cm-s-ssms,
|
||||||
|
&.cm-s-ttcn,
|
||||||
|
&.cm-s-xq-light,
|
||||||
|
&.cm-s-yeti {
|
||||||
|
// Future styling for themes with light backgrounds
|
||||||
|
--dummyVar: 'currently unused';
|
||||||
|
}
|
||||||
|
}
|
||||||
129
themes/codeMirror/customThemes/darkbrewery-v301.css
Normal file
129
themes/codeMirror/customThemes/darkbrewery-v301.css
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
/* Main BG color and normal text color */
|
||||||
|
.CodeMirror {
|
||||||
|
background: #293134;
|
||||||
|
color: #91A6AA;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Brew BG */
|
||||||
|
.brewRenderer {
|
||||||
|
background-color: #293134;
|
||||||
|
}
|
||||||
|
/* Blinking cursor */
|
||||||
|
.CodeMirror-cursor {
|
||||||
|
border-left: 1px solid #e0e2e4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* HB DARK NAV START*/
|
||||||
|
|
||||||
|
/* Bars at the top */
|
||||||
|
.snippetBar {
|
||||||
|
background-color: #2F393C;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
nav {
|
||||||
|
background-color: #293134;
|
||||||
|
}
|
||||||
|
nav .navItem {
|
||||||
|
background-color: #293134;
|
||||||
|
}
|
||||||
|
/* Fix for Homebrewery custom Snippet icons */
|
||||||
|
.snippetBar .fac {
|
||||||
|
filter: invert(1);
|
||||||
|
}
|
||||||
|
.snippetBar .snippetGroup .dropdown {
|
||||||
|
background-color: #2F393C;
|
||||||
|
}
|
||||||
|
/* HB DARK NAV END */
|
||||||
|
|
||||||
|
/* Line number stuff */
|
||||||
|
.CodeMirror-gutter-elt {
|
||||||
|
color: #81969A;
|
||||||
|
}
|
||||||
|
.CodeMirror-linenumber {
|
||||||
|
background-color: #293134;
|
||||||
|
}
|
||||||
|
.CodeMirror-gutter {
|
||||||
|
background-color: #293134;
|
||||||
|
}
|
||||||
|
/* column splits */
|
||||||
|
.editor .codeEditor .columnSplit {
|
||||||
|
font-style: italic;
|
||||||
|
color: inherit;
|
||||||
|
background-color:#1f5763;
|
||||||
|
border-bottom: #299 solid 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Colors for headings and such */
|
||||||
|
/* ###Headings */
|
||||||
|
.cm-s-default .cm-header {
|
||||||
|
color: #c51b1b;
|
||||||
|
-webkit-text-stroke-width: 0.1px;
|
||||||
|
-webkit-text-stroke-color: #000;
|
||||||
|
}
|
||||||
|
/* bold points */
|
||||||
|
.cm-header, .cm-strong {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #309dd2;
|
||||||
|
}
|
||||||
|
/* Link headings */
|
||||||
|
.cm-s-default .cm-link {
|
||||||
|
color: #dd6300;
|
||||||
|
}
|
||||||
|
/* links */
|
||||||
|
.cm-s-default .cm-string {
|
||||||
|
color: #aa8261;
|
||||||
|
}
|
||||||
|
/*@import*/
|
||||||
|
.cm-s-default .cm-def {
|
||||||
|
color:#2986cc;
|
||||||
|
}
|
||||||
|
/* Bullets and such */
|
||||||
|
.cm-s-default .cm-variable-2 {
|
||||||
|
color: #3cbf30;
|
||||||
|
}
|
||||||
|
/* blocks */
|
||||||
|
.editor .codeEditor .block:not(.cm-comment) {
|
||||||
|
color: #e3e3e3;
|
||||||
|
}
|
||||||
|
/* inline blocks */
|
||||||
|
.editor .codeEditor .inline-block {
|
||||||
|
color: #e3e3e3;
|
||||||
|
}
|
||||||
|
/* Tags (divs) */
|
||||||
|
.cm-s-default .cm-tag {
|
||||||
|
color: #e3ff00;
|
||||||
|
}
|
||||||
|
.cm-s-default .cm-attribute {
|
||||||
|
color: #e3ff00;
|
||||||
|
}
|
||||||
|
.cm-s-default .cm-atom {
|
||||||
|
color:#000;
|
||||||
|
}
|
||||||
|
.cm-s-default .cm-qualifier{
|
||||||
|
color:#ee1919;
|
||||||
|
}
|
||||||
|
.cm-s-default .cm-comment{
|
||||||
|
color:#bbc700;
|
||||||
|
}
|
||||||
|
.cm-s-default .cm-keyword {
|
||||||
|
color:#c302df;
|
||||||
|
background-color:#b1b1b1;
|
||||||
|
}
|
||||||
|
.cm-s-default .cm-property.cm-error {
|
||||||
|
color:#c50202;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-foldmarker {
|
||||||
|
color:#f0ff00;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* New page */
|
||||||
|
.editor .codeEditor .pageLine {
|
||||||
|
background: #000;
|
||||||
|
color:#000;
|
||||||
|
border-bottom: 1px solid #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-s-default .cm-builtin {
|
||||||
|
color:#fff;
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user