Compare commits
915 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
e7ce8d73bb | ||
|
|
c6ef051232 | ||
|
|
2c130d1943 | ||
|
|
f1e6a9a41e | ||
|
|
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 | ||
|
|
3e9bea3761 | ||
|
|
73e909c4c8 | ||
|
|
53fa6af5f9 | ||
|
|
4d2edf81a9 | ||
|
|
b10b33830a | ||
|
|
393ac69581 | ||
|
|
1098f6da70 | ||
|
|
a29fed3c89 | ||
|
|
7af3a629f9 | ||
|
|
b0a9765819 | ||
|
|
86d18ea0d9 | ||
|
|
bfe278c81c | ||
|
|
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 | ||
|
|
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 | ||
|
|
94b7c89252 | ||
|
|
c2e8967ed9 | ||
|
|
942fdb8095 | ||
|
|
95873ac158 | ||
|
|
3b4b0583cf | ||
|
|
2c9e3d2f2f | ||
|
|
5fca0a77d3 | ||
|
|
2c73e59eb0 | ||
|
|
f5db5c7bf2 | ||
|
|
37abc38426 | ||
|
|
855fabb89e | ||
|
|
bc35490ba2 | ||
|
|
8b5404606e | ||
|
|
c5d3605c11 | ||
|
|
43ab292391 | ||
|
|
3ed9702ef2 | ||
|
|
755b43179b | ||
|
|
66b827ee2f | ||
|
|
483a1c44ef | ||
|
|
47680f07df | ||
|
|
9e43986d24 | ||
|
|
7a198fe8b8 | ||
|
|
e3e5cb1dff | ||
|
|
f44ea92d4f | ||
|
|
5781b9d177 | ||
|
|
817539dfda | ||
|
|
4e083aece8 | ||
|
|
27a12dfa79 | ||
|
|
3b5ebf8f60 | ||
|
|
daea4419ff | ||
|
|
3b76a12505 | ||
|
|
dda3ba8215 | ||
|
|
6ea05d8ec2 | ||
|
|
71ec9034b7 | ||
|
|
86dce0ae24 | ||
|
|
1ebdf318bf | ||
|
|
f05e0db14b | ||
|
|
43fd6c451e | ||
|
|
e621f2d19b | ||
|
|
ca34ca499d | ||
|
|
0715e365f1 | ||
|
|
55d265069c | ||
|
|
52ee7d9dbf | ||
|
|
d0346650c4 | ||
|
|
96b26d72fd | ||
|
|
d51757b8b9 | ||
|
|
beccef2685 | ||
|
|
06f74c6b64 | ||
|
|
288b407e3e | ||
|
|
57eea5c69f | ||
|
|
fbfb92735c | ||
|
|
95376db055 | ||
|
|
01d3ec9d58 | ||
|
|
a1eb09225a | ||
|
|
5c2e2edbed | ||
|
|
4bb7d143aa | ||
|
|
f5cefc4db4 | ||
|
|
efbde81853 | ||
|
|
69a18d365a | ||
|
|
34e73ee69b | ||
|
|
ee1ee801a7 | ||
|
|
99d441d9ff | ||
|
|
d2be324bb0 | ||
|
|
6ceba54631 | ||
|
|
53e77718e1 | ||
|
|
0342dfed4c | ||
|
|
0864f4ced0 | ||
|
|
ebd729b78f | ||
|
|
32454a3f12 | ||
|
|
9781c8e633 | ||
|
|
8a2aacebeb | ||
|
|
5889c2f1e0 | ||
|
|
b135ce2ae9 | ||
|
|
8f2a114e1c | ||
|
|
455b364160 | ||
|
|
11c8446c9c | ||
|
|
0e1b30eced | ||
|
|
56dbfc032c | ||
|
|
b8372ebdcc | ||
|
|
42fdb0ebb1 | ||
|
|
b2ebf724f5 | ||
|
|
a4bea1c3be | ||
|
|
c800195e95 | ||
|
|
26ec222a33 | ||
|
|
618e594acf | ||
|
|
dde500004d | ||
|
|
1cf1750887 | ||
|
|
cbf281f211 | ||
|
|
34c73c3d09 | ||
|
|
9d61fc85a0 | ||
|
|
6825bb3bac | ||
|
|
0cb96f6fe6 | ||
|
|
2b7e0c3fb8 | ||
|
|
2cce7aebfc | ||
|
|
b5508b7a24 | ||
|
|
273dfdce40 | ||
|
|
1848dc8182 | ||
|
|
6fd26a2d0b | ||
|
|
528efc8b98 | ||
|
|
ef50b1966b | ||
|
|
2397f41b52 | ||
|
|
5554ad9c26 | ||
|
|
f5a07cac44 | ||
|
|
51dfd9a38c | ||
|
|
11da8b1dac | ||
|
|
22aed68200 | ||
|
|
1da329fb78 | ||
|
|
d455e8c270 | ||
|
|
e235c705ae | ||
|
|
f771e24788 | ||
|
|
55941f0318 | ||
|
|
ea38540e3b | ||
|
|
1500ed071f | ||
|
|
a568ab3b8a | ||
|
|
3c8660442b | ||
|
|
2525fa2a53 | ||
|
|
3f7aff587c | ||
|
|
00dd030ee2 | ||
|
|
a8179cae7b | ||
|
|
86823b43b1 | ||
|
|
0abfb23ef2 | ||
|
|
da5d4236b6 | ||
|
|
963236f961 | ||
|
|
2d4a3ec910 | ||
|
|
0425e61be2 | ||
|
|
a2ebb025a2 | ||
|
|
a43ea5abb9 | ||
|
|
1ceb1dccca | ||
|
|
d375cdf10b | ||
|
|
24639f1c29 | ||
|
|
62a9901676 | ||
|
|
c48dccb0d3 | ||
|
|
40afdf18d6 | ||
|
|
13944d3a76 | ||
|
|
65c738d3b2 | ||
|
|
3c168065ee | ||
|
|
08ee142f6e | ||
|
|
891bf528cd | ||
|
|
45b7d7da88 | ||
|
|
f52321dd4b | ||
|
|
3b55cd7d88 | ||
|
|
f33b7b21bb | ||
|
|
ed6e64af8d | ||
|
|
cadbb422a9 | ||
|
|
b756a2f026 | ||
|
|
cf42520305 | ||
|
|
c8b8d40863 | ||
|
|
d369cad02c | ||
|
|
d92005a3c2 | ||
|
|
a2430c8744 | ||
|
|
8febaee2a9 | ||
|
|
29fd836965 | ||
|
|
ebf9cf9364 | ||
|
|
8b8388391c | ||
|
|
ba72f1ab22 | ||
|
|
9bb1cac547 | ||
|
|
5cf6c9b8bd | ||
|
|
4ddee3c2f1 | ||
|
|
0aac08f276 | ||
|
|
9690c6dac3 | ||
|
|
78ca5f5107 | ||
|
|
eb7d558c8d | ||
|
|
0e226ca8db | ||
|
|
14ac098882 | ||
|
|
d5dbf46fc4 | ||
|
|
bc83e1f84d | ||
|
|
b8e68f9a93 | ||
|
|
ebc90c998a | ||
|
|
aa0cc1ebf6 | ||
|
|
c5bd41acbf | ||
|
|
22b6b6a473 | ||
|
|
89ba709789 | ||
|
|
0720677824 | ||
|
|
fab4bfae27 | ||
|
|
f880c961bd | ||
|
|
ec7c083f90 | ||
|
|
99984e207f | ||
|
|
b5bd28ddd1 | ||
|
|
e9bd80aa0d | ||
|
|
89f0c7e127 | ||
|
|
70295fb227 | ||
|
|
2cb6acc090 | ||
|
|
83fac6a10f | ||
|
|
d5ac237d40 | ||
|
|
121da67b7a | ||
|
|
eaf7b9c4ef | ||
|
|
9e9bf8c6fa | ||
|
|
4cfe26b4a9 | ||
|
|
0e35b99289 | ||
|
|
b32c724c89 | ||
|
|
e5377c1939 | ||
|
|
c666d6acb9 | ||
|
|
a5e84694c1 | ||
|
|
48227eaf71 | ||
|
|
f06d30e4a6 | ||
|
|
333525d9ab | ||
|
|
69c283f00f | ||
|
|
9f17f36a87 | ||
|
|
b948106500 | ||
|
|
7000b911e7 | ||
|
|
2869726efd | ||
|
|
7353e6c7ac | ||
|
|
773a9b5c82 | ||
|
|
e2b0b9e5d2 | ||
|
|
1c7540edcd | ||
|
|
4dc14101bc | ||
|
|
6016a60a3a | ||
|
|
ab51a93fb2 | ||
|
|
097d9aaacd | ||
|
|
d74acd2bdc | ||
|
|
dce880610d | ||
|
|
ae9a29c28c | ||
|
|
c660f87dff | ||
|
|
a86c728710 | ||
|
|
b28e183f95 | ||
|
|
dc55880544 | ||
|
|
5e9b451e29 | ||
|
|
977cbeed43 | ||
|
|
da6fcb297a | ||
|
|
b2546d908a | ||
|
|
7dd1368c09 | ||
|
|
51f657d2c5 | ||
|
|
8958238342 | ||
|
|
d1dd5e34bd | ||
|
|
7529a4380b | ||
|
|
1b5b4154ed | ||
|
|
a1476582b0 | ||
|
|
0fbda91169 | ||
|
|
4b6f81ba34 | ||
|
|
5cdc1dda64 | ||
|
|
4bf61a063c | ||
|
|
6e99636296 | ||
|
|
8902b237ce | ||
|
|
1ef18fc53c | ||
|
|
ea00c1a5d6 | ||
|
|
d49a94498a | ||
|
|
591c45f59f | ||
|
|
435eec6e74 | ||
|
|
aed29952d6 | ||
|
|
527b704ccd | ||
|
|
02bcc9bfb9 | ||
|
|
d4b624186f | ||
|
|
a58a750b94 | ||
|
|
d793b6f690 | ||
|
|
d278c52571 | ||
|
|
1c38d30665 | ||
|
|
ab058b31b1 | ||
|
|
cdcd68bc92 | ||
|
|
38a5ebf779 | ||
|
|
7926a318d8 | ||
|
|
370c6ccf73 | ||
|
|
2ed669d95e | ||
|
|
5bce76bcba | ||
|
|
a92f5d0694 | ||
|
|
5b2aa452c0 | ||
|
|
46bc34d527 | ||
|
|
6a95ed57ca | ||
|
|
97f5a17d10 | ||
|
|
a106f6f814 | ||
|
|
1c90b3c4d6 | ||
|
|
db81d347bd | ||
|
|
d9423b9d50 | ||
|
|
220b5df559 | ||
|
|
67068221bd | ||
|
|
e28605338b | ||
|
|
9870ff369e | ||
|
|
c39a95f1e1 | ||
|
|
3b5aef7d71 | ||
|
|
dc1ef3dd3e | ||
|
|
262e79c4df | ||
|
|
43b9877fa4 | ||
|
|
d25cef0c49 | ||
|
|
bd9dfeb46c | ||
|
|
348ec5fd20 | ||
|
|
038088328e | ||
|
|
5d77dea652 | ||
|
|
0436235ec3 | ||
|
|
6431964807 | ||
|
|
bda9b455d9 | ||
|
|
c41b06eee1 | ||
|
|
402811fbec | ||
|
|
c7758e02a8 | ||
|
|
92f3fc9ff8 | ||
|
|
f7bd861d9f | ||
|
|
be39a6c7cc | ||
|
|
c6210280eb | ||
|
|
801f66c483 | ||
|
|
bdd898f5b6 | ||
|
|
1a87a5543f | ||
|
|
b24c604597 | ||
|
|
ded29dc390 | ||
|
|
ab5755e94e | ||
|
|
eaad46b6bc | ||
|
|
1ec08bb1fa | ||
|
|
9dda58991f | ||
|
|
844d2b7a06 | ||
|
|
9e2824e0be | ||
|
|
4f5f34c888 | ||
|
|
1b3ed2ad70 | ||
|
|
098de2afd3 | ||
|
|
fa762cf32f | ||
|
|
a4677956f6 | ||
|
|
efdc0b072e | ||
|
|
56e7355a0e | ||
|
|
ae8e2c9889 | ||
|
|
32543f5aa3 | ||
|
|
3587511e44 | ||
|
|
a8926503b7 | ||
|
|
a52ec1c330 | ||
|
|
c2349fb464 | ||
|
|
10263cbf7c | ||
|
|
6281ed044e | ||
|
|
25b5badf90 | ||
|
|
d743b72f9c | ||
|
|
33d8d51956 | ||
|
|
285b4c3b92 | ||
|
|
0a7ccfb89e | ||
|
|
db5469699e | ||
|
|
807ab2a538 | ||
|
|
d46736b7e6 | ||
|
|
b041ef921e | ||
|
|
064a92f0da | ||
|
|
3e73ae0327 | ||
|
|
bd4c24df46 | ||
|
|
1126481d53 | ||
|
|
a3f93c2602 | ||
|
|
24564a2750 | ||
|
|
4505308b81 | ||
|
|
cf99bd9004 | ||
|
|
75d97379f8 | ||
|
|
6588863d2d | ||
|
|
98ae938b3d | ||
|
|
ccf44cbe91 | ||
|
|
54ed9a7d33 | ||
|
|
4ec0107348 | ||
|
|
8106b2b694 | ||
|
|
c4d26e7ffe | ||
|
|
f148014a93 | ||
|
|
0a09cd9c67 | ||
|
|
3105ee1eac | ||
|
|
2fc1600865 | ||
|
|
d075b09496 | ||
|
|
22e275acd8 | ||
|
|
a5513b359e | ||
|
|
30e7c73805 | ||
|
|
e080bf1bde | ||
|
|
37c60feda1 | ||
|
|
aa76252a55 | ||
|
|
6f3292e994 | ||
|
|
eaa672c4c7 | ||
|
|
93ff59f670 | ||
|
|
461487534d | ||
|
|
7bef807c41 | ||
|
|
2a8eaa654d | ||
|
|
29f8f3546c | ||
|
|
c47fae6061 | ||
|
|
6ff0999d88 | ||
|
|
bfccf833b6 | ||
|
|
2a9ac9fa47 | ||
|
|
b990af3fc3 | ||
|
|
91a31757e5 | ||
|
|
c341bc5db6 | ||
|
|
c88253901a | ||
|
|
164e0a4433 | ||
|
|
97852c3c03 | ||
|
|
4057d7bf84 | ||
|
|
33ae652222 | ||
|
|
690e797fe5 | ||
|
|
ce2298ddd0 | ||
|
|
bbcf415a30 | ||
|
|
e67fc2f775 | ||
|
|
cf9dbffe49 | ||
|
|
c006ab0901 | ||
|
|
572b92f893 | ||
|
|
810a5b295d | ||
|
|
a956f57a56 | ||
|
|
9b4577c65b | ||
|
|
a5a59ac058 | ||
|
|
cc89ad1c7d | ||
|
|
ba11aef038 | ||
|
|
33f3fb18fa | ||
|
|
240d283536 | ||
|
|
6af98cd842 | ||
|
|
7df2d39a6d | ||
|
|
3cdb15ba79 | ||
|
|
7c09956d7d | ||
|
|
63b088762e | ||
|
|
a6ed05214a | ||
|
|
27ea00e9ce | ||
|
|
23668f15f0 | ||
|
|
ac8c79ee63 | ||
|
|
81f0670be0 | ||
|
|
4484035d75 | ||
|
|
f528b55226 | ||
|
|
9eb8653dfb | ||
|
|
a65c26e806 | ||
|
|
17525a4f41 | ||
|
|
f644740f60 | ||
|
|
dae97946dc | ||
|
|
a921452c22 | ||
|
|
b0954bcd2c | ||
|
|
01f90ea085 | ||
|
|
0689d3d0b2 | ||
|
|
877ee1cc4f | ||
|
|
5f26f857d4 | ||
|
|
f7783aba07 | ||
|
|
f7dd03c628 | ||
|
|
3380befe21 | ||
|
|
14507c388e | ||
|
|
d4d2356708 | ||
|
|
6d9a0938ab | ||
|
|
747bb3c489 | ||
|
|
4a695eda87 | ||
|
|
208a341f27 | ||
|
|
75a4cff319 | ||
|
|
72e5515830 | ||
|
|
e5996db4f7 | ||
|
|
d84fe3e5b4 | ||
|
|
83a62e7133 | ||
|
|
5cc654a908 | ||
|
|
4c42a9e2fc | ||
|
|
4ae751bf70 | ||
|
|
b77c70054a | ||
|
|
055773b1c3 | ||
|
|
c3936d28d9 | ||
|
|
607a66a8f0 | ||
|
|
658aee8806 | ||
|
|
c60be5d24b | ||
|
|
3d5a8b5627 | ||
|
|
d11508886b | ||
|
|
4bb64a535f | ||
|
|
c956100416 | ||
|
|
458d104866 | ||
|
|
029230e075 | ||
|
|
82622744a1 | ||
|
|
8b0203dd7c | ||
|
|
76d73e0e02 | ||
|
|
9e289ad6d1 | ||
|
|
914a4586b9 | ||
|
|
12fe787ab4 | ||
|
|
fc7c46cfec | ||
|
|
8d80f699b6 | ||
|
|
e1ff34ebaa | ||
|
|
ce732778bb | ||
|
|
b54d225f11 | ||
|
|
fb95039368 | ||
|
|
60ca9530a3 | ||
|
|
70d8491e7c | ||
|
|
a9ff1cded7 | ||
|
|
417f9d7291 | ||
|
|
adb261f8f1 | ||
|
|
678c1fb3d8 | ||
|
|
1818ea1e3b | ||
|
|
d4b803205e | ||
|
|
a16f12546a | ||
|
|
b3343e5981 | ||
|
|
0061040cb6 | ||
|
|
c2d79bedb5 | ||
|
|
9ce79fbe3f | ||
|
|
806dcb04eb | ||
|
|
d5ea4c4e77 | ||
|
|
8ca44653e9 | ||
|
|
0b5d9714c0 | ||
|
|
b0110f20d2 | ||
|
|
9e227dadd2 | ||
|
|
a214f27e9c | ||
|
|
2d3b03a9c3 | ||
|
|
af094474b8 | ||
|
|
d92d00581a | ||
|
|
c30eb9056a | ||
|
|
ee3cd21486 | ||
|
|
5de63799c2 | ||
|
|
dfa8aea1ec | ||
|
|
8e26161244 | ||
|
|
4dc1a60934 | ||
|
|
bce7cf41af | ||
|
|
8c6fd3086c | ||
|
|
2019d91711 |
@@ -5,12 +5,12 @@
|
||||
version: 2.1
|
||||
|
||||
orbs:
|
||||
node: circleci/node@3.0.0
|
||||
node: circleci/node@5.1.0
|
||||
|
||||
jobs:
|
||||
build:
|
||||
docker:
|
||||
- image: cimg/node:16.11.0
|
||||
- image: cimg/node:20.8.0
|
||||
- image: mongo:4.4
|
||||
|
||||
working_directory: ~/homebrewery
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
# fallback to using the latest cache if no exact match is found
|
||||
- v1-dependencies-
|
||||
|
||||
- run: sudo npm install -g npm@8.10.0
|
||||
- run: sudo npm install -g npm@10.2.0
|
||||
- node/install-packages:
|
||||
app-dir: ~/homebrewery
|
||||
cache-path: node_modules
|
||||
@@ -45,10 +45,10 @@ jobs:
|
||||
|
||||
test:
|
||||
docker:
|
||||
- image: cimg/node:16.11.0
|
||||
- image: cimg/node:20.8.0
|
||||
|
||||
working_directory: ~/homebrewery
|
||||
parallelism: 4
|
||||
parallelism: 1
|
||||
|
||||
steps:
|
||||
- attach_workspace:
|
||||
@@ -61,15 +61,15 @@ jobs:
|
||||
- run:
|
||||
name: Test - Basic
|
||||
command: npm run test:basic
|
||||
- run:
|
||||
name: Test - Coverage
|
||||
command: npm run test:coverage
|
||||
- run:
|
||||
name: Test - Mustache Spans
|
||||
command: npm run test:mustache-span
|
||||
command: npm run test:mustache-syntax
|
||||
- run:
|
||||
name: Test - Routes
|
||||
command: npm run test:route
|
||||
- run:
|
||||
name: Test - Coverage
|
||||
command: npm run test:coverage
|
||||
|
||||
workflows:
|
||||
build_and_test:
|
||||
|
||||
@@ -11,11 +11,11 @@ module.exports = {
|
||||
browser : true,
|
||||
node : true
|
||||
},
|
||||
plugins : ['react'],
|
||||
plugins : ['react', 'jest'],
|
||||
rules : {
|
||||
/** Errors **/
|
||||
'camelcase' : ['error', { properties: 'never' }],
|
||||
'func-style' : ['error', 'expression', { allowArrowFunctions: true }],
|
||||
//'func-style' : ['error', 'expression', { allowArrowFunctions: true }],
|
||||
'no-array-constructor' : 'error',
|
||||
'no-iterator' : 'error',
|
||||
'no-nested-ternary' : 'error',
|
||||
@@ -24,6 +24,7 @@ module.exports = {
|
||||
'react/jsx-no-bind' : ['error', { allowArrowFunctions: true }],
|
||||
'react/jsx-uses-react' : 'error',
|
||||
'react/prefer-es6-class' : ['error', 'never'],
|
||||
'jest/valid-expect' : ['error', { maxArgs: 3 }],
|
||||
|
||||
/** Warnings **/
|
||||
'max-lines' : ['warn', {
|
||||
|
||||
48
.stylelintrc.json
Normal file
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"extends": [
|
||||
"stylelint-config-recess-order",
|
||||
"stylelint-config-recommended"],
|
||||
"plugins": [
|
||||
"stylelint-stylistic",
|
||||
"./stylelint_plugins/declaration-colon-align.js",
|
||||
"./stylelint_plugins/declaration-colon-min-space-before",
|
||||
"./stylelint_plugins/declaration-block-multi-line-min-declarations"
|
||||
],
|
||||
"customSyntax": "postcss-less",
|
||||
"rules": {
|
||||
"no-descending-specificity" : null,
|
||||
"at-rule-no-unknown" : null,
|
||||
"function-no-unknown" : null,
|
||||
"font-family-no-missing-generic-family-keyword" : null,
|
||||
"font-weight-notation" : "named-where-possible",
|
||||
"font-family-name-quotes" : "always-unless-keyword",
|
||||
"stylistic/indentation" : "tab",
|
||||
"no-duplicate-selectors" : true,
|
||||
"stylistic/color-hex-case" : "upper",
|
||||
"color-hex-length" : "long",
|
||||
"stylistic/selector-combinator-space-after" : "always",
|
||||
"stylistic/selector-combinator-space-before" : "always",
|
||||
"stylistic/selector-attribute-operator-space-before" : "never",
|
||||
"stylistic/selector-attribute-operator-space-after" : "never",
|
||||
"stylistic/selector-attribute-brackets-space-inside" : "never",
|
||||
"selector-attribute-quotes" : "always",
|
||||
"selector-pseudo-element-colon-notation" : "double",
|
||||
"stylistic/selector-pseudo-class-parentheses-space-inside" : "never",
|
||||
"stylistic/block-opening-brace-space-before" : "always",
|
||||
"naturalcrit/declaration-colon-min-space-before" : 1,
|
||||
"stylistic/declaration-block-trailing-semicolon" : "always",
|
||||
"stylistic/declaration-colon-space-after" : "always",
|
||||
"stylistic/number-leading-zero" : "always",
|
||||
"function-url-quotes" : ["always", { "except": ["empty"] }],
|
||||
"function-url-scheme-disallowed-list" : ["data","http"],
|
||||
"comment-whitespace-inside" : "always",
|
||||
"stylistic/string-quotes" : "single",
|
||||
"stylistic/media-feature-range-operator-space-before" : "always",
|
||||
"stylistic/media-feature-range-operator-space-after" : "always",
|
||||
"stylistic/media-feature-parentheses-space-inside" : "never",
|
||||
"stylistic/media-feature-colon-space-before" : "always",
|
||||
"stylistic/media-feature-colon-space-after" : "always",
|
||||
"naturalcrit/declaration-colon-align" : true,
|
||||
"naturalcrit/declaration-block-multi-line-min-declarations": 1
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:16.11-alpine
|
||||
FROM node:18-alpine
|
||||
RUN apk --no-cache add git
|
||||
|
||||
ENV NODE_ENV=docker
|
||||
@@ -10,11 +10,11 @@ WORKDIR /usr/src/app
|
||||
# This improves caching so we don't have to download the dependencies every time the code changes
|
||||
COPY package.json ./
|
||||
# --ignore-scripts tells yarn not to run postbuild. We run it explicitly later
|
||||
RUN yarn install --ignore-scripts
|
||||
RUN npm install --ignore-scripts
|
||||
|
||||
# Bundle app source and build application
|
||||
COPY . .
|
||||
RUN yarn build
|
||||
RUN npm run build
|
||||
|
||||
EXPOSE 8000
|
||||
CMD [ "yarn", "start" ]
|
||||
CMD [ "npm", "start" ]
|
||||
|
||||
247
changelog.md
@@ -80,6 +80,241 @@ pre {
|
||||
## changelog
|
||||
For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery).
|
||||
|
||||
### 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
|
||||
{{taskList
|
||||
|
||||
##### Calculuschild
|
||||
|
||||
* [x] Fix some files not showing up on userpage when user has a large number of brews in Google Drive
|
||||
|
||||
Fixes issue [#2408](https://github.com/naturalcrit/homebrewery/issues/2408)
|
||||
|
||||
* [x] Pressing tab now indents with spaces instead of tab character; fixes several issues with Markdown lists
|
||||
|
||||
Fixes issues [#2092](https://github.com/naturalcrit/homebrewery/issues/2092), [#1556](https://github.com/naturalcrit/homebrewery/issues/1556)
|
||||
|
||||
* [x] Rename `naturalCritLogo.svg` to `naturalCritLogoRed.svg`. Those using the {{beta BETA}} coverPage snippet may need to update that text to make the NaturalCrit logo appear again.
|
||||
|
||||
##### G-Ambatte
|
||||
|
||||
* [x] Fix strange animation of image masks
|
||||
|
||||
Fixes issue [#2790](https://github.com/naturalcrit/homebrewery/issues/2790)
|
||||
|
||||
##### 5e-Cleric
|
||||
|
||||
* [x] New {{openSans **PHB → {{fac,book-part-cover}} PART COVER PAGE** }} snippet for V3!
|
||||
|
||||
* [x] New {{openSans **PHB → {{fac,book-back-cover}} BACK COVER PAGE** }} snippet for V3! (Thanks to /u/Kaiburr_Kath-Hound on Reddit for providing some of these resources!)
|
||||
|
||||
* [x] New {{openSans **TEXT EDITOR → {{fas,fa-bars}} INDEX** }} snippet for V3!
|
||||
|
||||
* [x] Fix highlighting of curly braces inside comments
|
||||
|
||||
Fixes issue [#2784](https://github.com/naturalcrit/homebrewery/issues/2784)
|
||||
}}
|
||||
|
||||
\page
|
||||
|
||||
### Wednesday 12/04/2023 - v3.8.0
|
||||
{{taskList
|
||||
|
||||
##### calculuschild
|
||||
|
||||
* [x] Rename `{{coverPage}}` to `{{frontCover}}`. Those using this {{beta BETA}} feature will need to update that text to make the cover page appear again.
|
||||
|
||||
* [x] Several background fixes to test scripts
|
||||
|
||||
##### Jeddai
|
||||
|
||||
* [X] Add content negotiation to exclude image requests from our API calls
|
||||
|
||||
Fixes issue [#2595](https://github.com/naturalcrit/homebrewery/issues/2595)
|
||||
|
||||
##### G-Ambatte
|
||||
|
||||
* [x] Update server build scripts to fix Admin page
|
||||
|
||||
Fixes issues [#2657](https://github.com/naturalcrit/homebrewery/issues/2657)
|
||||
|
||||
* [x] Fix internal links inside `<\div>` blocks not receiving the `target=_self` attribute
|
||||
|
||||
Fixes issues [#2680](https://github.com/naturalcrit/homebrewery/issues/2680)
|
||||
|
||||
* [x] See brew details on `/share` pages by clicking the brew title (author, last update, tags, etc.)
|
||||
|
||||
Fixes issues [#1679](https://github.com/naturalcrit/homebrewery/issues/1679)
|
||||
|
||||
* [x] Add local Windows install script via Chocolatey
|
||||
|
||||
##### 5e-Cleric
|
||||
|
||||
* [x] New {{openSans **TABLES → {{fas,fa-language}} RUNE TABLE**}} snippets for V3. Adds an alphabetic script translation table.
|
||||
|
||||
* [x] New {{openSans **IMAGES → {{fac,mask-center}} WATERCOLOR CENTER** }} snippets for V3, which adds a stylish watercolor texture to the center of your images!
|
||||
|
||||
* [x] New {{openSans **PHB → {{fac,book-inside-cover}} INSIDE COVER PAGE** }} snippet for V3! (Thanks to /u/Kaiburr_Kath-Hound on Reddit for providing some of these resources!)
|
||||
|
||||
* [x] Add some missing characters {{font-family:scalySansRemake Ñ ñ ç Ç Ý ý # ^ ¿ ' " ¡ ·}} to the "scalySansRemake" font in V3.
|
||||
|
||||
Fixes issues [#2280](https://github.com/naturalcrit/homebrewery/issues/2280)
|
||||
|
||||
##### Gazook89
|
||||
|
||||
* [x] Add "Language" selector in {{fa,fa-info-circle}} **Properties** menu. Sets the HTML Lang attribute for your brew to better handle hyphenation or spellcheck.
|
||||
|
||||
Fixes issues [#1343](https://github.com/naturalcrit/homebrewery/issues/1343)
|
||||
|
||||
* [x] Fix a crash when multiple `{injection}` tags appear in sequence
|
||||
|
||||
Fixes issues [#2712](https://github.com/naturalcrit/homebrewery/issues/2712)
|
||||
|
||||
##### MichielDeMey
|
||||
|
||||
* [x] Remove all-caps display on Account button since usernames are case-sensitive.
|
||||
|
||||
Fixes issues [#2731](https://github.com/naturalcrit/homebrewery/issues/2731)
|
||||
|
||||
}}
|
||||
|
||||
### Monday 13/03/2023 - v3.7.2
|
||||
{{taskList
|
||||
|
||||
##### Calculuschild
|
||||
|
||||
* [x] Fix wide Monster Stat Blocks not spanning columns on Legacy
|
||||
}}
|
||||
|
||||
### Thursday 09/03/2023 - v3.7.1
|
||||
{{taskList
|
||||
|
||||
##### Lucastucious (new contributor!)
|
||||
|
||||
* [x] Changed `filter: drop-shadow` to `box-shadow` on text boxes, making PDF text selectable
|
||||
|
||||
Fixes issues [#1569](https://github.com/naturalcrit/homebrewery/issues/1569)
|
||||
|
||||
{{note
|
||||
**NOTE:** If you create your PDF on a computer with an old version of Mac Preview (v10 or older) you may see shadows appear as solid gray.
|
||||
}}
|
||||
|
||||
##### MichielDeMey
|
||||
|
||||
* [x] Updated the Google Drive icon
|
||||
* [x] Backend fix to unit tests failing intermittently
|
||||
|
||||
##### Calculuschild
|
||||
|
||||
* [x] Fix PDF pixelation on CoverPage text outlines
|
||||
}}
|
||||
|
||||
### Tuesday 28/02/2023 - v3.7.0
|
||||
{{taskList
|
||||
@@ -110,12 +345,11 @@ Fixes issues [#2687](https://github.com/naturalcrit/homebrewery/issues/2687)
|
||||
{{taskList
|
||||
##### G-Ambatte
|
||||
|
||||
* [x] Fix users not being removed from Authors list correctly
|
||||
* [x] Fix users not being removed from Authors list
|
||||
|
||||
Fixes issues [#2674](https://github.com/naturalcrit/homebrewery/issues/2674)
|
||||
}}
|
||||
|
||||
|
||||
### Monday 23/01/2023 - v3.6.0
|
||||
{{taskList
|
||||
##### calculuschild
|
||||
@@ -131,7 +365,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
|
||||
|
||||
Fixes issues [#2583](https://github.com/naturalcrit/homebrewery/issues/2583)
|
||||
}}
|
||||
|
||||
\page
|
||||
|
||||
{{taskList
|
||||
##### G-Ambatte
|
||||
|
||||
* [x] Auto-compile Themes CSS on development server
|
||||
@@ -141,7 +379,6 @@ Fixes issues [#2583](https://github.com/naturalcrit/homebrewery/issues/2583)
|
||||
* [x] Fix cloned brews inheriting the parent view count
|
||||
}}
|
||||
|
||||
\column
|
||||
|
||||
### Friday 23/12/2022 - v3.5.0
|
||||
{{taskList
|
||||
@@ -156,8 +393,6 @@ Fixes issues [#2583](https://github.com/naturalcrit/homebrewery/issues/2583)
|
||||
Fixes issues [#1987](https://github.com/naturalcrit/homebrewery/issues/1987)
|
||||
}}
|
||||
|
||||
\page
|
||||
|
||||
### Saturday 10/12/2022 - v3.4.2
|
||||
{{taskList
|
||||
|
||||
@@ -1227,4 +1462,4 @@ Massive changelog incoming:
|
||||
|
||||
* Added `phb.standalone.css` plus a build system for creating it
|
||||
* Added page numbers and footer text
|
||||
* Page accent now flips each page
|
||||
* Page accent now flips each page
|
||||
129
client/components/combobox.jsx
Normal file
@@ -0,0 +1,129 @@
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
const cx = require('classnames');
|
||||
require('./combobox.less');
|
||||
|
||||
const Combobox = createClass({
|
||||
displayName : 'Combobox',
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
className : '',
|
||||
trigger : 'hover',
|
||||
default : '',
|
||||
placeholder : '',
|
||||
autoSuggest : {
|
||||
clearAutoSuggestOnClick : true,
|
||||
suggestMethod : 'includes',
|
||||
filterOn : [] // should allow as array to filter on multiple attributes, or even custom filter
|
||||
},
|
||||
};
|
||||
},
|
||||
getInitialState : function() {
|
||||
return {
|
||||
showDropdown : false,
|
||||
value : '',
|
||||
options : [...this.props.options],
|
||||
inputFocused : false
|
||||
};
|
||||
},
|
||||
componentDidMount : function() {
|
||||
if(this.props.trigger == 'click')
|
||||
document.addEventListener('click', this.handleClickOutside);
|
||||
this.setState({
|
||||
value : this.props.default
|
||||
});
|
||||
},
|
||||
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,
|
||||
inputFocused : this.props.autoSuggest.clearAutoSuggestOnClick ? show : false
|
||||
});
|
||||
},
|
||||
handleInput : function(e){
|
||||
e.persist();
|
||||
this.setState({
|
||||
value : e.target.value,
|
||||
inputFocused : false
|
||||
}, ()=>{
|
||||
this.props.onEntry(e);
|
||||
});
|
||||
},
|
||||
handleSelect : function(e){
|
||||
this.setState({
|
||||
value : e.currentTarget.getAttribute('data-value')
|
||||
}, ()=>{this.props.onSelect(this.state.value);});
|
||||
;
|
||||
},
|
||||
renderTextInput : function(){
|
||||
return (
|
||||
<div className='dropdown-input item'
|
||||
onMouseEnter={this.props.trigger == 'hover' ? ()=>{this.handleDropdown(true);} : undefined}
|
||||
onClick= {this.props.trigger == 'click' ? ()=>{this.handleDropdown(true);} : undefined}>
|
||||
<input
|
||||
type='text'
|
||||
onChange={(e)=>this.handleInput(e)}
|
||||
value={this.state.value || ''}
|
||||
placeholder={this.props.placeholder}
|
||||
onBlur={(e)=>{
|
||||
if(!e.target.checkValidity()){
|
||||
this.setState({
|
||||
value : this.props.default
|
||||
}, ()=>this.props.onEntry(e));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
renderDropdown : function(dropdownChildren){
|
||||
if(!this.state.showDropdown) return null;
|
||||
if(this.props.autoSuggest && !this.state.inputFocused){
|
||||
const suggestMethod = this.props.autoSuggest.suggestMethod;
|
||||
const filterOn = _.isString(this.props.autoSuggest.filterOn) ? [this.props.autoSuggest.filterOn] : this.props.autoSuggest.filterOn;
|
||||
const filteredArrays = filterOn.map((attr)=>{
|
||||
const children = dropdownChildren.filter((item)=>{
|
||||
if(suggestMethod === 'includes'){
|
||||
return item.props[attr]?.toLowerCase().includes(this.state.value.toLowerCase());
|
||||
} else if(suggestMethod === 'startsWith'){
|
||||
return item.props[attr]?.toLowerCase().startsWith(this.state.value.toLowerCase());
|
||||
}
|
||||
});
|
||||
return children;
|
||||
});
|
||||
dropdownChildren = _.uniq(filteredArrays.flat(1));
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='dropdown-options'>
|
||||
{dropdownChildren}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
render : function () {
|
||||
const dropdownChildren = this.state.options.map((child, i)=>{
|
||||
const clone = React.cloneElement(child, { onClick: (e)=>this.handleSelect(e) });
|
||||
return clone;
|
||||
});
|
||||
return (
|
||||
<div className={`dropdown-container ${this.props.className}`}
|
||||
ref='dropdown'
|
||||
onMouseLeave={this.props.trigger == 'hover' ? ()=>{this.handleDropdown(false);} : undefined}>
|
||||
{this.renderTextInput()}
|
||||
{this.renderDropdown(dropdownChildren)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = Combobox;
|
||||
50
client/components/combobox.less
Normal file
@@ -0,0 +1,50 @@
|
||||
.dropdown-container {
|
||||
position:relative;
|
||||
input {
|
||||
width: 100%;
|
||||
}
|
||||
.dropdown-options {
|
||||
position:absolute;
|
||||
background-color: white;
|
||||
z-index: 100;
|
||||
width: 100%;
|
||||
border: 1px solid gray;
|
||||
overflow-y: auto;
|
||||
max-height: 200px;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 14px;
|
||||
}
|
||||
&::-webkit-scrollbar-track {
|
||||
background: #ffffff;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: #949494;
|
||||
border-radius: 10px;
|
||||
border: 3px solid #ffffff;
|
||||
}
|
||||
|
||||
.item {
|
||||
position:relative;
|
||||
font-size: 11px;
|
||||
font-family: Open Sans;
|
||||
padding: 5px;
|
||||
cursor: default;
|
||||
margin: 0 3px;
|
||||
//border-bottom: 1px solid darkgray;
|
||||
&:hover {
|
||||
filter: brightness(120%);
|
||||
background-color: rgb(163, 163, 163);
|
||||
}
|
||||
.detail {
|
||||
width:100%;
|
||||
text-align: left;
|
||||
color: rgb(124, 124, 124);
|
||||
font-style:italic;
|
||||
font-size: 9px;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -27,6 +27,7 @@ const BrewRenderer = createClass({
|
||||
style : '',
|
||||
renderer : 'legacy',
|
||||
theme : '5ePHB',
|
||||
lang : '',
|
||||
errors : []
|
||||
};
|
||||
},
|
||||
@@ -107,6 +108,12 @@ const BrewRenderer = createClass({
|
||||
return false;
|
||||
},
|
||||
|
||||
sanitizeScriptTags : function(content) {
|
||||
return content
|
||||
.replace(/<script/ig, '<script')
|
||||
.replace(/<\/script>/ig, '</script>');
|
||||
},
|
||||
|
||||
renderPageInfo : function(){
|
||||
return <div className='pageInfo' ref='main'>
|
||||
<div>
|
||||
@@ -134,18 +141,20 @@ const BrewRenderer = createClass({
|
||||
|
||||
renderStyle : function() {
|
||||
if(!this.props.style) return;
|
||||
//return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `<style>@layer styleTab {\n${this.props.style}\n} </style>` }} />;
|
||||
return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `<style>\n${this.props.style}\n</style>` }} />;
|
||||
const cleanStyle = this.sanitizeScriptTags(this.props.style);
|
||||
//return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `<style>@layer styleTab {\n${this.sanitizeScriptTags(this.props.style)}\n} </style>` }} />;
|
||||
return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `<style> ${cleanStyle} </style>` }} />;
|
||||
},
|
||||
|
||||
renderPage : function(pageText, index){
|
||||
let cleanPageText = this.sanitizeScriptTags(pageText);
|
||||
if(this.props.renderer == 'legacy')
|
||||
return <div className='phb page' id={`p${index + 1}`} dangerouslySetInnerHTML={{ __html: MarkdownLegacy.render(pageText) }} key={index} />;
|
||||
return <div className='phb page' id={`p${index + 1}`} dangerouslySetInnerHTML={{ __html: MarkdownLegacy.render(cleanPageText) }} key={index} />;
|
||||
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)
|
||||
cleanPageText += `\n\n \n\\column\n `; //Artificial column break at page end to emulate column-fill:auto (until `wide` is used, when column-fill:balance will reappear)
|
||||
return (
|
||||
<div className='page' id={`p${index + 1}`} key={index} >
|
||||
<div className='columnWrapper' dangerouslySetInnerHTML={{ __html: Markdown.render(pageText) }} />
|
||||
<div className='columnWrapper' dangerouslySetInnerHTML={{ __html: Markdown.render(cleanPageText) }} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -184,13 +193,18 @@ const BrewRenderer = createClass({
|
||||
}, 100);
|
||||
},
|
||||
|
||||
emitClick : function(){
|
||||
// console.log('iFrame clicked');
|
||||
if(!window || !document) return;
|
||||
document.dispatchEvent(new MouseEvent('click'));
|
||||
},
|
||||
|
||||
render : function(){
|
||||
//render in iFrame so broken code doesn't crash the site.
|
||||
//Also render dummy page while iframe is mounting.
|
||||
const rendererPath = this.props.renderer == 'V3' ? 'V3' : 'Legacy';
|
||||
const themePath = this.props.theme ?? '5ePHB';
|
||||
const baseThemePath = Themes[rendererPath][themePath].baseTheme;
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{!this.state.isMounted
|
||||
@@ -203,7 +217,9 @@ const BrewRenderer = createClass({
|
||||
|
||||
<Frame id='BrewRenderer' initialContent={this.state.initialContent}
|
||||
style={{ width: '100%', height: '100%', visibility: this.state.visibility }}
|
||||
contentDidMount={this.frameDidMount}>
|
||||
contentDidMount={this.frameDidMount}
|
||||
onClick={()=>{this.emitClick();}}
|
||||
>
|
||||
<div className={'brewRenderer'}
|
||||
onScroll={this.handleScroll}
|
||||
style={{ height: this.state.height }}>
|
||||
@@ -223,7 +239,7 @@ const BrewRenderer = createClass({
|
||||
&&
|
||||
<>
|
||||
{this.renderStyle()}
|
||||
<div className='pages' ref='pages'>
|
||||
<div className='pages' ref='pages' lang={`${this.props.lang || 'en'}`}>
|
||||
{this.renderPages()}
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -4,7 +4,7 @@ const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
const cx = require('classnames'); //Unused variable
|
||||
|
||||
const DISMISS_KEY = 'dismiss_notification08-27-22';
|
||||
const DISMISS_KEY = 'dismiss_notification12-04-23';
|
||||
|
||||
const NotificationPopup = createClass({
|
||||
displayName : 'NotificationPopup',
|
||||
@@ -25,21 +25,13 @@ const NotificationPopup = createClass({
|
||||
return (
|
||||
<>
|
||||
<li key='psa'>
|
||||
<em>V3.2.0 Released!</em> <br />
|
||||
We are happy to announce that after nearly a year of use by our many users,
|
||||
we are making the V3 render mode the default setting for all new brews.
|
||||
This mode has become quite popular, and has proven to be stable and powerful.
|
||||
Of course, we will always keep the option to use the Legacy renderer for any
|
||||
brew, which can still be accessed from the Properties menu.
|
||||
</li>
|
||||
|
||||
<li key='stubs'>
|
||||
<em>Change to Google Drive Storage!</em> <br />
|
||||
We have made a change to the process of tranferring brews between Google
|
||||
Drive and the Homebrewery storage. Starting now, any time a brew is
|
||||
transferred, it will keep the same links instead of generating new ones!
|
||||
We hope this change will help reduce issues where people "lost" their work
|
||||
by trying to visit old links.
|
||||
<em>Broken default logo on <b>CoverPage</b> </em> <br />
|
||||
If you have used the Cover Page snippet and notice the Naturalcrit
|
||||
logo is showing as a broken image, this is due to some small tweaks
|
||||
of this BETA feature. To fix the logo in your cover page, rename
|
||||
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 key='googleDriveFolder'>
|
||||
|
||||
@@ -10,6 +10,8 @@ const CodeEditor = require('naturalcrit/codeEditor/codeEditor.jsx');
|
||||
const SnippetBar = require('./snippetbar/snippetbar.jsx');
|
||||
const MetadataEditor = require('./metadataEditor/metadataEditor.jsx');
|
||||
|
||||
const EDITOR_THEME_KEY = 'HOMEBREWERY-EDITOR-THEME';
|
||||
|
||||
const SNIPPETBAR_HEIGHT = 25;
|
||||
const DEFAULT_STYLE_TEXT = dedent`
|
||||
/*=======--- Example CSS styling ---=======*/
|
||||
@@ -34,12 +36,14 @@ const Editor = createClass({
|
||||
onMetaChange : ()=>{},
|
||||
reportError : ()=>{},
|
||||
|
||||
renderer : 'legacy'
|
||||
editorTheme : 'default',
|
||||
renderer : 'legacy'
|
||||
};
|
||||
},
|
||||
getInitialState : function() {
|
||||
return {
|
||||
view : 'text' //'text', 'style', 'meta'
|
||||
editorTheme : this.props.editorTheme,
|
||||
view : 'text' //'text', 'style', 'meta'
|
||||
};
|
||||
},
|
||||
|
||||
@@ -51,6 +55,13 @@ const Editor = createClass({
|
||||
this.updateEditorSize();
|
||||
this.highlightCustomMarkdown();
|
||||
window.addEventListener('resize', this.updateEditorSize);
|
||||
|
||||
const editorTheme = window.localStorage.getItem(EDITOR_THEME_KEY);
|
||||
if(editorTheme) {
|
||||
this.setState({
|
||||
editorTheme : editorTheme
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
componentWillUnmount : function() {
|
||||
@@ -138,6 +149,17 @@ const Editor = createClass({
|
||||
codeMirror.addLineClass(lineNumber, 'text', 'columnSplit');
|
||||
}
|
||||
|
||||
// definition lists
|
||||
if(line.includes('::')){
|
||||
const regex = /^([^\n]*?)::([^\n]*)(?:\n|$)/ym;
|
||||
let match;
|
||||
while ((match = regex.exec(line)) != null){
|
||||
codeMirror.markText({ line: lineNumber, ch: line.indexOf(match[0]) }, { line: lineNumber, ch: line.indexOf(match[0]) + match[0].length }, { className: 'define' });
|
||||
codeMirror.markText({ line: lineNumber, ch: line.indexOf(match[1]) }, { line: lineNumber, ch: line.indexOf(match[1]) + match[1].length }, { className: 'term' });
|
||||
codeMirror.markText({ line: lineNumber, ch: line.indexOf(match[2]) }, { line: lineNumber, ch: line.indexOf(match[2]) + match[2].length }, { className: 'definition' });
|
||||
}
|
||||
}
|
||||
|
||||
// Highlight injectors {style}
|
||||
if(line.includes('{') && line.includes('}')){
|
||||
const regex = /(?:^|[^{\n])({(?=((?::(?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':{}\s]*)*))\2})/gm;
|
||||
@@ -255,6 +277,13 @@ const Editor = createClass({
|
||||
this.refs.codeEditor?.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
|
||||
rerenderParent : function (){
|
||||
this.forceUpdate();
|
||||
@@ -269,6 +298,7 @@ const Editor = createClass({
|
||||
view={this.state.view}
|
||||
value={this.props.brew.text}
|
||||
onChange={this.props.onTextChange}
|
||||
editorTheme={this.state.editorTheme}
|
||||
rerenderParent={this.rerenderParent} />
|
||||
</>;
|
||||
}
|
||||
@@ -281,6 +311,7 @@ const Editor = createClass({
|
||||
value={this.props.brew.style ?? DEFAULT_STYLE_TEXT}
|
||||
onChange={this.props.onStyleChange}
|
||||
enableFolding={false}
|
||||
editorTheme={this.state.editorTheme}
|
||||
rerenderParent={this.rerenderParent} />
|
||||
</>;
|
||||
}
|
||||
@@ -323,7 +354,10 @@ const Editor = createClass({
|
||||
theme={this.props.brew.theme}
|
||||
undo={this.undo}
|
||||
redo={this.redo}
|
||||
historySize={this.historySize()} />
|
||||
historySize={this.historySize()}
|
||||
currentEditorTheme={this.state.editorTheme}
|
||||
updateEditorTheme={this.updateEditorTheme}
|
||||
cursorPos={this.refs.codeEditor?.getCursorPosition() || {}} />
|
||||
|
||||
{this.renderEditor()}
|
||||
</div>
|
||||
|
||||
@@ -1,65 +1,73 @@
|
||||
|
||||
.editor{
|
||||
@import 'themes/codeMirror/customEditorStyles.less';
|
||||
.editor {
|
||||
position : relative;
|
||||
width : 100%;
|
||||
|
||||
.codeEditor{
|
||||
.codeEditor {
|
||||
height : 100%;
|
||||
.pageLine{
|
||||
.pageLine {
|
||||
background : #33333328;
|
||||
border-top : #339 solid 1px;
|
||||
border-top : #333399 solid 1px;
|
||||
}
|
||||
.editor-page-count{
|
||||
color : grey;
|
||||
.editor-page-count {
|
||||
float : right;
|
||||
color : grey;
|
||||
}
|
||||
.columnSplit{
|
||||
font-style : italic;
|
||||
color : grey;
|
||||
background-color : fade(#299, 15%);
|
||||
border-bottom : #299 solid 1px;
|
||||
.columnSplit {
|
||||
font-style : italic;
|
||||
color : grey;
|
||||
background-color : fade(#229999, 15%);
|
||||
border-bottom : #229999 solid 1px;
|
||||
}
|
||||
.block{
|
||||
color : purple;
|
||||
.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;
|
||||
//font-style: italic;
|
||||
}
|
||||
.inline-block{
|
||||
color : red;
|
||||
.inline-block:not(.cm-comment) {
|
||||
font-weight : bold;
|
||||
color : red;
|
||||
//font-style: italic;
|
||||
}
|
||||
.injection{
|
||||
.injection:not(.cm-comment) {
|
||||
font-weight : bold;
|
||||
color : green;
|
||||
font-weight : bold;
|
||||
}
|
||||
}
|
||||
|
||||
.brewJump{
|
||||
position : absolute;
|
||||
background-color : @teal;
|
||||
cursor : pointer;
|
||||
width : 30px;
|
||||
height : 30px;
|
||||
display : flex;
|
||||
align-items : center;
|
||||
bottom : 20px;
|
||||
right : 20px;
|
||||
z-index : 1000000;
|
||||
justify-content : center;
|
||||
.tooltipLeft("Jump to brew page");
|
||||
.brewJump {
|
||||
position : absolute;
|
||||
right : 20px;
|
||||
bottom : 20px;
|
||||
z-index : 1000000;
|
||||
display : flex;
|
||||
align-items : center;
|
||||
justify-content : center;
|
||||
width : 30px;
|
||||
height : 30px;
|
||||
cursor : pointer;
|
||||
background-color : @teal;
|
||||
.tooltipLeft('Jump to brew page');
|
||||
}
|
||||
|
||||
.editorToolbar{
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
left: 50%;
|
||||
color: black;
|
||||
font-size: 13px;
|
||||
z-index: 9;
|
||||
span {
|
||||
padding: 2px 5px;
|
||||
}
|
||||
.editorToolbar {
|
||||
position : absolute;
|
||||
top : 5px;
|
||||
left : 50%;
|
||||
z-index : 9;
|
||||
font-size : 13px;
|
||||
color : black;
|
||||
span { padding : 2px 5px; }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ const _ = require('lodash');
|
||||
const cx = require('classnames');
|
||||
const request = require('../../utils/request-middleware.js');
|
||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||
const Combobox = require('client/components/combobox.jsx');
|
||||
const StringArrayEditor = require('../stringArrayEditor/stringArrayEditor.jsx');
|
||||
|
||||
const Themes = require('themes/themes.json');
|
||||
@@ -35,7 +36,8 @@ const MetadataEditor = createClass({
|
||||
authors : [],
|
||||
systems : [],
|
||||
renderer : 'legacy',
|
||||
theme : '5ePHB'
|
||||
theme : '5ePHB',
|
||||
lang : 'en'
|
||||
},
|
||||
onChange : ()=>{},
|
||||
reportError : ()=>{}
|
||||
@@ -76,6 +78,7 @@ const MetadataEditor = createClass({
|
||||
const errMessage = validationErr.map((err)=>{
|
||||
return `- ${err}`;
|
||||
}).join('\n');
|
||||
|
||||
callIfExists(e.target, 'setCustomValidity', errMessage);
|
||||
callIfExists(e.target, 'reportValidity');
|
||||
}
|
||||
@@ -111,6 +114,11 @@ const MetadataEditor = createClass({
|
||||
this.props.onChange(this.props.metadata);
|
||||
},
|
||||
|
||||
handleLanguage : function(languageCode){
|
||||
this.props.metadata.lang = languageCode;
|
||||
this.props.onChange(this.props.metadata);
|
||||
},
|
||||
|
||||
handleDelete : function(){
|
||||
if(this.props.metadata.authors && this.props.metadata.authors.length <= 1){
|
||||
if(!confirm('Are you sure you want to delete this brew? Because you are the only owner of this brew, the document will be deleted permanently.')) return;
|
||||
@@ -224,6 +232,47 @@ const MetadataEditor = createClass({
|
||||
</div>;
|
||||
},
|
||||
|
||||
renderLanguageDropdown : function(){
|
||||
const langCodes = ['en', 'de', 'de-ch', 'fr', 'ja', 'es', 'it', 'sv', 'ru', 'zh-Hans', 'zh-Hant'];
|
||||
const listLanguages = ()=>{
|
||||
return _.map(langCodes.sort(), (code, index)=>{
|
||||
const localName = new Intl.DisplayNames([code], { type: 'language' });
|
||||
const englishName = new Intl.DisplayNames('en', { type: 'language' });
|
||||
return <div className='item' title={`${englishName.of(code)}`} key={`${index}`} data-value={`${code}`} data-detail={`${localName.of(code)}`}>
|
||||
{`${code}`}
|
||||
<div className='detail'>{`${localName.of(code)}`}</div>
|
||||
</div>;
|
||||
});
|
||||
};
|
||||
|
||||
const debouncedHandleFieldChange = _.debounce(this.handleFieldChange, 500);
|
||||
|
||||
return <div className='field language'>
|
||||
<label>language</label>
|
||||
<div className='value'>
|
||||
<Combobox trigger='click'
|
||||
className='language-dropdown'
|
||||
default={this.props.metadata.lang || ''}
|
||||
placeholder='en'
|
||||
onSelect={(value)=>this.handleLanguage(value)}
|
||||
onEntry={(e)=>{
|
||||
e.target.setCustomValidity(''); //Clear the validation popup while typing
|
||||
debouncedHandleFieldChange('lang', e);
|
||||
}}
|
||||
options={listLanguages()}
|
||||
autoSuggest={{
|
||||
suggestMethod : 'startsWith',
|
||||
clearAutoSuggestOnClick : true,
|
||||
filterOn : ['data-value', 'data-detail', 'title']
|
||||
}}
|
||||
>
|
||||
</Combobox>
|
||||
<small>Sets the HTML Lang property for your brew. May affect hyphenation or spellcheck.</small>
|
||||
</div>
|
||||
|
||||
</div>;
|
||||
},
|
||||
|
||||
renderRenderOptions : function(){
|
||||
if(!global.enable_v3) return;
|
||||
|
||||
@@ -301,6 +350,8 @@ const MetadataEditor = createClass({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{this.renderLanguageDropdown()}
|
||||
|
||||
{this.renderThemeDropdown()}
|
||||
|
||||
{this.renderRenderOptions()}
|
||||
@@ -315,7 +366,7 @@ const MetadataEditor = createClass({
|
||||
validators={[(v)=>!this.props.metadata.authors?.includes(v)]}
|
||||
placeholder='invite author' unique={true}
|
||||
values={this.props.metadata.invitedAuthors}
|
||||
notes={['Invited authors are case sensitive.', 'After adding an invited author, send them the edit link. There, they can choose to accept or decline the invitation.']}
|
||||
notes={['Invited author usernames are case sensitive.', 'After adding an invited author, send them the edit link. There, they can choose to accept or decline the invitation.']}
|
||||
onChange={(e)=>this.handleFieldChange('invitedAuthors', e)}/>
|
||||
|
||||
<hr/>
|
||||
|
||||
@@ -36,11 +36,15 @@
|
||||
flex: 5 0 200px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.field{
|
||||
display : flex;
|
||||
flex-wrap : wrap;
|
||||
width : 100%;
|
||||
min-width : 200px;
|
||||
position : relative;
|
||||
&>label{
|
||||
width : 80px;
|
||||
font-size : 11px;
|
||||
@@ -57,6 +61,9 @@
|
||||
}
|
||||
input[type='text'], textarea {
|
||||
border : 1px solid gray;
|
||||
&:focus {
|
||||
outline: 1px solid #444;
|
||||
}
|
||||
}
|
||||
&.thumbnail{
|
||||
height : 1.4em;
|
||||
@@ -88,9 +95,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
&.language .language-dropdown {
|
||||
max-width : 150px;
|
||||
z-index : 200;
|
||||
}
|
||||
small {
|
||||
font-size : 0.6em;
|
||||
font-style : italic;
|
||||
font-size : 0.6em;
|
||||
font-style : italic;
|
||||
line-height : 1.4em;
|
||||
display : inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,7 +172,7 @@
|
||||
.navDropdownContainer {
|
||||
background-color : white;
|
||||
position : relative;
|
||||
z-index : 500;
|
||||
z-index : 100;
|
||||
&.disabled {
|
||||
font-style :italic;
|
||||
font-style : italic;
|
||||
|
||||
@@ -23,9 +23,9 @@ module.exports = {
|
||||
}
|
||||
}
|
||||
],
|
||||
language : [
|
||||
lang : [
|
||||
(value)=>{
|
||||
return new RegExp(/[a-z]{2,3}(-.*)?/).test(value || '') === false ? 'Invalid language code.' : null;
|
||||
return new RegExp(/^([a-zA-Z]{2,3})(-[a-zA-Z]{4})?(-(?:[0-9]{3}|[a-zA-Z]{2}))?$/).test(value) === false && (value.length > 0) ? 'Invalid language code.' : null;
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/*eslint max-lines: ["warn", {"max": 250, "skipBlankLines": true, "skipComments": true}]*/
|
||||
require('./snippetbar.less');
|
||||
const React = require('react');
|
||||
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_Blank'] = require('themes/V3/Blank/snippets.js');
|
||||
|
||||
const execute = function(val, brew){
|
||||
if(_.isFunction(val)) return val(brew);
|
||||
const EditorThemes = require('build/homebrew/codeMirror/editorThemes.json');
|
||||
|
||||
const execute = function(val, props){
|
||||
if(_.isFunction(val)) return val(props);
|
||||
return val;
|
||||
};
|
||||
|
||||
@@ -24,23 +27,26 @@ const Snippetbar = createClass({
|
||||
displayName : 'SnippetBar',
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
brew : {},
|
||||
view : 'text',
|
||||
onViewChange : ()=>{},
|
||||
onInject : ()=>{},
|
||||
onToggle : ()=>{},
|
||||
showEditButtons : true,
|
||||
renderer : 'legacy',
|
||||
undo : ()=>{},
|
||||
redo : ()=>{},
|
||||
historySize : ()=>{}
|
||||
brew : {},
|
||||
view : 'text',
|
||||
onViewChange : ()=>{},
|
||||
onInject : ()=>{},
|
||||
onToggle : ()=>{},
|
||||
showEditButtons : true,
|
||||
renderer : 'legacy',
|
||||
undo : ()=>{},
|
||||
redo : ()=>{},
|
||||
historySize : ()=>{},
|
||||
updateEditorTheme : ()=>{},
|
||||
cursorPos : {}
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState : function() {
|
||||
return {
|
||||
renderer : this.props.renderer,
|
||||
snippets : []
|
||||
renderer : this.props.renderer,
|
||||
themeSelector : false,
|
||||
snippets : []
|
||||
};
|
||||
},
|
||||
|
||||
@@ -94,6 +100,31 @@ const Snippetbar = createClass({
|
||||
this.props.onInject(injectedText);
|
||||
},
|
||||
|
||||
toggleThemeSelector : function(){
|
||||
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} onMouseDown={(this.changeTheme)}>
|
||||
{EditorThemes.map((theme, key)=>{
|
||||
return <option key={key} value={theme}>{theme}</option>;
|
||||
})}
|
||||
</select>
|
||||
</div>;
|
||||
},
|
||||
|
||||
renderSnippetGroups : function(){
|
||||
const snippets = this.state.snippets.filter((snippetGroup)=>snippetGroup.view === this.props.view);
|
||||
|
||||
@@ -105,6 +136,7 @@ const Snippetbar = createClass({
|
||||
snippets={snippetGroup.snippets}
|
||||
key={snippetGroup.groupName}
|
||||
onSnippetClick={this.handleSnippetClick}
|
||||
cursorPos={this.props.cursorPos}
|
||||
/>;
|
||||
});
|
||||
},
|
||||
@@ -122,6 +154,12 @@ const Snippetbar = createClass({
|
||||
<i className='fas fa-redo' />
|
||||
</div>
|
||||
<div className='divider'></div>
|
||||
<div className={`editorTool editorTheme ${this.state.themeSelector ? 'active' : ''}`}
|
||||
onClick={this.toggleThemeSelector} >
|
||||
<i className='fas fa-palette' />
|
||||
</div>
|
||||
{this.state.themeSelector && this.renderThemeSelector()}
|
||||
<div className='divider'></div>
|
||||
<div className={cx('text', { selected: this.props.view === 'text' })}
|
||||
onClick={()=>this.props.onViewChange('text')}>
|
||||
<i className='fa fa-beer' />
|
||||
@@ -165,7 +203,7 @@ const SnippetGroup = createClass({
|
||||
},
|
||||
handleSnippetClick : function(e, snippet){
|
||||
e.stopPropagation();
|
||||
this.props.onSnippetClick(execute(snippet.gen, this.props.brew));
|
||||
this.props.onSnippetClick(execute(snippet.gen, this.props));
|
||||
},
|
||||
renderSnippets : function(snippets){
|
||||
return _.map(snippets, (snippet)=>{
|
||||
@@ -194,5 +232,4 @@ const SnippetGroup = createClass({
|
||||
</div>
|
||||
</div>;
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
@@ -46,6 +46,15 @@
|
||||
color : black;
|
||||
}
|
||||
}
|
||||
&.editorTheme{
|
||||
.tooltipLeft('Editor Themes');
|
||||
font-size : 0.75em;
|
||||
color : black;
|
||||
&.active{
|
||||
color : white;
|
||||
background-color: black;
|
||||
}
|
||||
}
|
||||
&.divider {
|
||||
background: linear-gradient(#000, #000) no-repeat center/1px 100%;
|
||||
width: 5px;
|
||||
@@ -54,6 +63,15 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
.themeSelector{
|
||||
position: absolute;
|
||||
left: -65px;
|
||||
top: 30px;
|
||||
z-index: 999;
|
||||
width: 170px;
|
||||
background-color: black;
|
||||
border-radius: 5px;
|
||||
}
|
||||
}
|
||||
.snippetBarButton{
|
||||
height : @menuHeight;
|
||||
@@ -96,6 +114,7 @@
|
||||
padding : 0px;
|
||||
background-color : #ddd;
|
||||
.snippet{
|
||||
position: relative;
|
||||
.animate(background-color);
|
||||
display : flex;
|
||||
align-items : center;
|
||||
|
||||
@@ -140,7 +140,7 @@ const StringArrayEditor = createClass({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{this.props.notes ? this.props.notes.map((n)=><p><small>{n}</small></p>) : null}
|
||||
{this.props.notes ? this.props.notes.map((n, index)=><p key={index}><small>{n}</small></p>) : null}
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 305 KiB |
8
client/homebrew/googleDrive.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<svg viewBox="0 0 87.3 78" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m6.6 66.85 3.85 6.65c.8 1.4 1.95 2.5 3.3 3.3l13.75-23.8h-27.5c0 1.55.4 3.1 1.2 4.5z" fill="#0066da"/>
|
||||
<path d="m43.65 25-13.75-23.8c-1.35.8-2.5 1.9-3.3 3.3l-25.4 44a9.06 9.06 0 0 0 -1.2 4.5h27.5z" fill="#00ac47"/>
|
||||
<path d="m73.55 76.8c1.35-.8 2.5-1.9 3.3-3.3l1.6-2.75 7.65-13.25c.8-1.4 1.2-2.95 1.2-4.5h-27.502l5.852 11.5z" fill="#ea4335"/>
|
||||
<path d="m43.65 25 13.75-23.8c-1.35-.8-2.9-1.2-4.5-1.2h-18.5c-1.6 0-3.15.45-4.5 1.2z" fill="#00832d"/>
|
||||
<path d="m59.8 53h-32.3l-13.75 23.8c1.35.8 2.9 1.2 4.5 1.2h50.8c1.6 0 3.15-.45 4.5-1.2z" fill="#2684fc"/>
|
||||
<path d="m73.4 26.5-12.7-22c-.8-1.4-1.95-2.5-3.3-3.3l-13.75 23.8 16.15 28h27.45c0-1.55-.4-3.1-1.2-4.5z" fill="#ffba00"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 755 B |
|
Before Width: | Height: | Size: 17 KiB |
@@ -9,7 +9,7 @@ const EditPage = require('./pages/editPage/editPage.jsx');
|
||||
const UserPage = require('./pages/userPage/userPage.jsx');
|
||||
const SharePage = require('./pages/sharePage/sharePage.jsx');
|
||||
const NewPage = require('./pages/newPage/newPage.jsx');
|
||||
//const ErrorPage = require('./pages/errorPage/errorPage.jsx');
|
||||
const ErrorPage = require('./pages/errorPage/errorPage.jsx');
|
||||
const PrintPage = require('./pages/printPage/printPage.jsx');
|
||||
const AccountPage = require('./pages/accountPage/accountPage.jsx');
|
||||
|
||||
@@ -47,6 +47,7 @@ const Homebrew = createClass({
|
||||
editId : null,
|
||||
createdAt : null,
|
||||
updatedAt : null,
|
||||
lang : ''
|
||||
}
|
||||
};
|
||||
},
|
||||
@@ -77,6 +78,7 @@ const Homebrew = createClass({
|
||||
<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='/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} />} />
|
||||
</Routes>
|
||||
|
||||
@@ -63,7 +63,7 @@ const Account = createClass({
|
||||
if(global.account){
|
||||
return <Nav.dropdown>
|
||||
<Nav.item
|
||||
className='account'
|
||||
className='account username'
|
||||
color='orange'
|
||||
icon='fas fa-user'
|
||||
>
|
||||
|
||||
@@ -82,4 +82,4 @@ const ErrorNavItem = createClass({
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = ErrorNavItem;
|
||||
module.exports = ErrorNavItem;
|
||||
|
||||
@@ -1,77 +1,75 @@
|
||||
.navItem {
|
||||
&.error {
|
||||
position : relative;
|
||||
background-color : @red;
|
||||
}
|
||||
.navItem.error {
|
||||
position : relative;
|
||||
background-color : @red;
|
||||
}
|
||||
|
||||
.errorContainer{
|
||||
animation-name: glideDown;
|
||||
animation-duration: 0.4s;
|
||||
position : absolute;
|
||||
top : 100%;
|
||||
left : 50%;
|
||||
z-index : 1000;
|
||||
width : 140px;
|
||||
padding : 3px;
|
||||
color : white;
|
||||
.errorContainer{
|
||||
animation-name: glideDown;
|
||||
animation-duration: 0.4s;
|
||||
position : absolute;
|
||||
top : 100%;
|
||||
left : 50%;
|
||||
z-index : 1000;
|
||||
width : 140px;
|
||||
padding : 3px;
|
||||
color : white;
|
||||
background-color : #333;
|
||||
border : 3px solid #444;
|
||||
border-radius : 5px;
|
||||
transform : translate(-50% + 3px, 10px);
|
||||
text-align : center;
|
||||
font-size : 10px;
|
||||
font-weight : 800;
|
||||
text-transform : uppercase;
|
||||
a{
|
||||
color : @teal;
|
||||
}
|
||||
&:before {
|
||||
content: "";
|
||||
width: 0px;
|
||||
height: 0px;
|
||||
position: absolute;
|
||||
border-left: 10px solid transparent;
|
||||
border-right: 10px solid transparent;
|
||||
border-top: 10px solid transparent;
|
||||
border-bottom: 10px solid #444;
|
||||
left: 53px;
|
||||
top: -23px;
|
||||
}
|
||||
&:after {
|
||||
content: "";
|
||||
width: 0px;
|
||||
height: 0px;
|
||||
position: absolute;
|
||||
border-left: 10px solid transparent;
|
||||
border-right: 10px solid transparent;
|
||||
border-top: 10px solid transparent;
|
||||
border-bottom: 10px solid #333;
|
||||
left: 53px;
|
||||
top: -19px;
|
||||
}
|
||||
.deny {
|
||||
width : 48%;
|
||||
margin : 1px;
|
||||
padding : 5px;
|
||||
background-color : #333;
|
||||
border : 3px solid #444;
|
||||
border-radius : 5px;
|
||||
transform : translate(-50% + 3px, 10px);
|
||||
text-align : center;
|
||||
font-size : 10px;
|
||||
font-weight : 800;
|
||||
text-transform : uppercase;
|
||||
a{
|
||||
color : @teal;
|
||||
}
|
||||
&:before {
|
||||
content: "";
|
||||
width: 0px;
|
||||
height: 0px;
|
||||
position: absolute;
|
||||
border-left: 10px solid transparent;
|
||||
border-right: 10px solid transparent;
|
||||
border-top: 10px solid transparent;
|
||||
border-bottom: 10px solid #444;
|
||||
left: 53px;
|
||||
top: -23px;
|
||||
}
|
||||
&:after {
|
||||
content: "";
|
||||
width: 0px;
|
||||
height: 0px;
|
||||
position: absolute;
|
||||
border-left: 10px solid transparent;
|
||||
border-right: 10px solid transparent;
|
||||
border-top: 10px solid transparent;
|
||||
border-bottom: 10px solid #333;
|
||||
left: 53px;
|
||||
top: -19px;
|
||||
}
|
||||
.deny {
|
||||
width : 48%;
|
||||
margin : 1px;
|
||||
padding : 5px;
|
||||
background-color : #333;
|
||||
display : inline-block;
|
||||
border-left : 1px solid #666;
|
||||
.animate(background-color);
|
||||
&:hover{
|
||||
background-color : red;
|
||||
}
|
||||
}
|
||||
.confirm {
|
||||
width : 48%;
|
||||
margin : 1px;
|
||||
padding : 5px;
|
||||
background-color : #333;
|
||||
display : inline-block;
|
||||
color : white;
|
||||
.animate(background-color);
|
||||
&:hover{
|
||||
background-color : teal;
|
||||
}
|
||||
display : inline-block;
|
||||
border-left : 1px solid #666;
|
||||
.animate(background-color);
|
||||
&:hover{
|
||||
background-color : red;
|
||||
}
|
||||
}
|
||||
}
|
||||
.confirm {
|
||||
width : 48%;
|
||||
margin : 1px;
|
||||
padding : 5px;
|
||||
background-color : #333;
|
||||
display : inline-block;
|
||||
color : white;
|
||||
.animate(background-color);
|
||||
&:hover{
|
||||
background-color : teal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
90
client/homebrew/navbar/metadata.navitem.jsx
Normal file
@@ -0,0 +1,90 @@
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
const Moment = require('moment');
|
||||
|
||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||
|
||||
|
||||
const MetadataNav = createClass({
|
||||
displayName : 'MetadataNav',
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState : function() {
|
||||
return {
|
||||
showMetaWindow : false
|
||||
};
|
||||
},
|
||||
|
||||
componentDidMount : function() {
|
||||
},
|
||||
|
||||
toggleMetaWindow : function(){
|
||||
this.setState((prevProps)=>({
|
||||
showMetaWindow : !prevProps.showMetaWindow
|
||||
}));
|
||||
},
|
||||
|
||||
getAuthors : function(){
|
||||
if(!this.props.brew.authors || this.props.brew.authors.length == 0) return 'No authors';
|
||||
return <>
|
||||
{this.props.brew.authors.map((author, idx, arr)=>{
|
||||
const spacer = arr.length - 1 == idx ? <></> : <span>, </span>;
|
||||
return <span key={idx}><a className='userPageLink' href={`/user/${author}`}>{author}</a>{spacer}</span>;
|
||||
})}
|
||||
</>;
|
||||
},
|
||||
|
||||
getTags : function(){
|
||||
if(!this.props.brew.tags || this.props.brew.tags.length == 0) return 'No tags';
|
||||
return <>
|
||||
{this.props.brew.tags.map((tag, idx)=>{
|
||||
return <span className='tag' key={idx}>{tag}</span>;
|
||||
})}
|
||||
</>;
|
||||
},
|
||||
|
||||
getSystems : function(){
|
||||
if(!this.props.brew.systems || this.props.brew.systems.length == 0) return 'No systems';
|
||||
return this.props.brew.systems.join(', ');
|
||||
},
|
||||
|
||||
renderMetaWindow : function(){
|
||||
return <div className={`window ${this.state.showMetaWindow ? 'active' : 'inactive'}`}>
|
||||
<div className='row'>
|
||||
<h4>Description</h4>
|
||||
<p>{this.props.brew.description || 'No description.'}</p>
|
||||
</div>
|
||||
<div className='row'>
|
||||
<h4>Authors</h4>
|
||||
<p>{this.getAuthors()}</p>
|
||||
</div>
|
||||
<div className='row'>
|
||||
<h4>Tags</h4>
|
||||
<p>{this.getTags()}</p>
|
||||
</div>
|
||||
<div className='row'>
|
||||
<h4>Systems</h4>
|
||||
<p>{this.getSystems()}</p>
|
||||
</div>
|
||||
<div className='row'>
|
||||
<h4>Updated</h4>
|
||||
<p>{Moment(this.props.brew.updatedAt).fromNow()}</p>
|
||||
</div>
|
||||
</div>;
|
||||
},
|
||||
|
||||
render : function(){
|
||||
return <Nav.item icon='fas fa-info-circle' color='grey' className='metadata'
|
||||
onClick={()=>this.toggleMetaWindow()}>
|
||||
{this.props.children}
|
||||
{this.renderMetaWindow()}
|
||||
</Nav.item>;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
module.exports = MetadataNav;
|
||||
@@ -1,190 +1,272 @@
|
||||
@import 'naturalcrit/styles/colors.less';
|
||||
@navbarHeight : 28px;
|
||||
@keyframes pinkColoring {
|
||||
//from {color: white;}
|
||||
//to {color: red;}
|
||||
0% {color: pink;}
|
||||
50% {color: pink;}
|
||||
75% {color: red;}
|
||||
100% {color: pink;}
|
||||
}
|
||||
.homebrew nav{
|
||||
.homebrewLogo{
|
||||
.animate(color);
|
||||
font-family : CodeBold;
|
||||
font-size : 12px;
|
||||
color : white;
|
||||
div{
|
||||
margin-top : 2px;
|
||||
margin-bottom : -2px;
|
||||
}
|
||||
&:hover{
|
||||
color : @blue;
|
||||
}
|
||||
}
|
||||
.editTitle.navItem{
|
||||
padding : 2px 12px;
|
||||
input{
|
||||
width : 250px;
|
||||
margin : 0;
|
||||
padding : 2px;
|
||||
background-color : #444;
|
||||
font-family : 'Open Sans', sans-serif;
|
||||
font-size : 12px;
|
||||
font-weight : 800;
|
||||
color : white;
|
||||
text-align : center;
|
||||
border : 1px solid @blue;
|
||||
outline : none;
|
||||
}
|
||||
.charCount{
|
||||
display : inline-block;
|
||||
vertical-align : bottom;
|
||||
margin-left : 8px;
|
||||
color : #666;
|
||||
text-align : right;
|
||||
&.max{
|
||||
color : @red;
|
||||
}
|
||||
}
|
||||
}
|
||||
.brewTitle.navItem{
|
||||
font-size : 12px;
|
||||
font-weight : 800;
|
||||
color : white;
|
||||
text-align : center;
|
||||
text-transform : initial;
|
||||
}
|
||||
.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-left : 1px solid #666;
|
||||
border-right : 1px solid #666;
|
||||
&:hover i {
|
||||
color: red;
|
||||
}
|
||||
i{
|
||||
.animate(color);
|
||||
animation-name: pinkColoring;
|
||||
animation-duration: 2s;
|
||||
color: pink;
|
||||
}
|
||||
}
|
||||
.recent.navItem {
|
||||
position : relative;
|
||||
.dropdown{
|
||||
position : absolute;
|
||||
top : 28px;
|
||||
left : 0px;
|
||||
z-index : 10000;
|
||||
width : 100%;
|
||||
overflow : hidden auto;
|
||||
max-height : ~"calc(100vh - 28px)";
|
||||
scrollbar-color : #666 #333;
|
||||
scrollbar-width : thin;
|
||||
h4{
|
||||
display : block;
|
||||
box-sizing : border-box;
|
||||
padding : 5px 0px;
|
||||
background-color : #333;
|
||||
font-size : 0.8em;
|
||||
color : #bbb;
|
||||
text-align : center;
|
||||
border-top : 1px solid #888;
|
||||
&:nth-of-type(1){ background-color: darken(@teal, 20%); }
|
||||
&:nth-of-type(2){ background-color: darken(@purple, 30%); }
|
||||
}
|
||||
.item{
|
||||
#backgroundColorsHover;
|
||||
.animate(background-color);
|
||||
position : relative;
|
||||
display : block;
|
||||
box-sizing : border-box;
|
||||
padding : 8px 5px 13px;
|
||||
background-color : #333;
|
||||
color : white;
|
||||
text-decoration : none;
|
||||
border-top : 1px solid #888;
|
||||
overflow : clip;
|
||||
.clear{
|
||||
display : none;
|
||||
position : absolute;
|
||||
top : 50%;
|
||||
transform : translateY(-50%);
|
||||
right : 0px;
|
||||
width : 20px;
|
||||
height : 100%;
|
||||
background-color : #333;
|
||||
opacity : 70%;
|
||||
border-radius : 3px;
|
||||
&:hover {
|
||||
opacity : 100%;
|
||||
}
|
||||
i {
|
||||
text-align : center;
|
||||
font-size : 10px;
|
||||
margin : 0;
|
||||
height :100%;
|
||||
width :100%;
|
||||
}
|
||||
}
|
||||
&:hover{
|
||||
background-color : @blue;
|
||||
|
||||
.clear{
|
||||
display : grid;
|
||||
place-content : center;
|
||||
}
|
||||
}
|
||||
.title{
|
||||
display : inline-block;
|
||||
overflow : hidden;
|
||||
width : 100%;
|
||||
text-overflow : ellipsis;
|
||||
white-space : nowrap;
|
||||
}
|
||||
.time{
|
||||
position : absolute;
|
||||
right : 2px;
|
||||
bottom : 2px;
|
||||
font-size : 0.7em;
|
||||
color : #888;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.warning.navItem{
|
||||
position : relative;
|
||||
background-color : @orange;
|
||||
color : white;
|
||||
&:hover>.dropdown{
|
||||
visibility : visible;
|
||||
}
|
||||
.dropdown{
|
||||
position : absolute;
|
||||
display : block;
|
||||
top : 28px;
|
||||
left : 0px;
|
||||
visibility : hidden;
|
||||
z-index : 10000;
|
||||
box-sizing : border-box;
|
||||
width : 100%;
|
||||
padding : 13px 5px;
|
||||
background-color : #333;
|
||||
text-align : center;
|
||||
}
|
||||
}
|
||||
.account.navItem{
|
||||
min-width: 100px;
|
||||
}
|
||||
}
|
||||
@import "naturalcrit/styles/colors.less";
|
||||
@navbarHeight : 28px;
|
||||
@keyframes pinkColoring {
|
||||
0% {color : pink;}
|
||||
50% {color : pink;}
|
||||
75% {color : red;}
|
||||
100% {color : pink;}
|
||||
}
|
||||
.homebrew nav {
|
||||
.homebrewLogo {
|
||||
.animate(color);
|
||||
font-family : CodeBold;
|
||||
font-size : 12px;
|
||||
color : white;
|
||||
div {
|
||||
margin-top : 2px;
|
||||
margin-bottom : -2px;
|
||||
}
|
||||
&:hover {
|
||||
color : @blue;
|
||||
}
|
||||
}
|
||||
.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.navDropdownContainer {
|
||||
position : relative;
|
||||
.navDropdown .navItem {
|
||||
overflow : hidden auto;
|
||||
max-height : ~"calc(100vh - 28px)";
|
||||
scrollbar-color : #666 #333;
|
||||
scrollbar-width : thin;
|
||||
|
||||
|
||||
#backgroundColorsHover;
|
||||
.animate(background-color);
|
||||
position : relative;
|
||||
display : block;
|
||||
overflow : clip;
|
||||
box-sizing : border-box;
|
||||
padding : 8px 5px 13px;
|
||||
text-decoration : none;
|
||||
color : white;
|
||||
border-top : 1px solid #888;
|
||||
background-color : #333;
|
||||
.clear {
|
||||
position : absolute;
|
||||
top : 50%;
|
||||
right : 0;
|
||||
display : none;
|
||||
width : 20px;
|
||||
height : 100%;
|
||||
transform : translateY(-50%);
|
||||
opacity : 70%;
|
||||
border-radius : 3px;
|
||||
background-color : #333;
|
||||
&:hover {
|
||||
opacity : 100%;
|
||||
}
|
||||
i {
|
||||
font-size : 10px;
|
||||
width : 100%;
|
||||
height : 100%;
|
||||
margin : 0;
|
||||
text-align : center;
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
background-color : @blue;
|
||||
.clear {
|
||||
display : grid;
|
||||
place-content : center;
|
||||
}
|
||||
}
|
||||
.title {
|
||||
display : inline-block;
|
||||
overflow : hidden;
|
||||
width : 100%;
|
||||
white-space : nowrap;
|
||||
text-overflow : ellipsis;
|
||||
}
|
||||
.time {
|
||||
font-size : 0.7em;
|
||||
position : absolute;
|
||||
right : 2px;
|
||||
bottom : 2px;
|
||||
color : #888;
|
||||
}
|
||||
&.header {
|
||||
display : block;
|
||||
box-sizing : border-box;
|
||||
padding : 5px 0;
|
||||
text-align : center;
|
||||
color : #BBB;
|
||||
border-top : 1px solid #888;
|
||||
background-color : #333;
|
||||
&:nth-of-type(1) {
|
||||
background-color : darken(@teal, 20%);
|
||||
}
|
||||
&:nth-of-type(2) {
|
||||
background-color : darken(@purple, 30%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.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;
|
||||
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;
|
||||
visibility : hidden;
|
||||
box-sizing : border-box;
|
||||
width : 100%;
|
||||
padding : 13px 5px;
|
||||
text-align : center;
|
||||
background-color : #333;
|
||||
}
|
||||
}
|
||||
.account.navItem {
|
||||
min-width : 100px;
|
||||
}
|
||||
.account.username.navItem {
|
||||
text-transform : none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,6 +121,7 @@ const RecentItems = createClass({
|
||||
|
||||
removeItem : function(url, evt){
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
|
||||
let edited = JSON.parse(localStorage.getItem(EDIT_KEY) || '[]');
|
||||
let viewed = JSON.parse(localStorage.getItem(VIEW_KEY) || '[]');
|
||||
@@ -139,11 +140,11 @@ const RecentItems = createClass({
|
||||
},
|
||||
|
||||
renderDropdown : function(){
|
||||
if(!this.state.showDropdown) return null;
|
||||
// if(!this.state.showDropdown) return null;
|
||||
|
||||
const makeItems = (brews)=>{
|
||||
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='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>
|
||||
@@ -151,25 +152,25 @@ const RecentItems = createClass({
|
||||
});
|
||||
};
|
||||
|
||||
return <div className='dropdown'>
|
||||
return <>
|
||||
{(this.props.showEdit && this.props.showView) ?
|
||||
<h4>edited</h4> : null }
|
||||
<Nav.item className='header'>edited</Nav.item> : null }
|
||||
{this.props.showEdit ?
|
||||
makeItems(this.state.edit) : null }
|
||||
{(this.props.showEdit && this.props.showView) ?
|
||||
<h4>viewed</h4> : null }
|
||||
<Nav.item className='header'>viewed</Nav.item> : null }
|
||||
{this.props.showView ?
|
||||
makeItems(this.state.view) : null }
|
||||
</div>;
|
||||
</>;
|
||||
},
|
||||
|
||||
render : function(){
|
||||
return <Nav.item icon='fas fa-history' color='grey' className='recent'
|
||||
onMouseEnter={()=>this.handleDropdown(true)}
|
||||
onMouseLeave={()=>this.handleDropdown(false)}>
|
||||
{this.props.text}
|
||||
return <Nav.dropdown className='recent'>
|
||||
<Nav.item icon='fas fa-history' color='grey' >
|
||||
{this.props.text}
|
||||
</Nav.item>
|
||||
{this.renderDropdown()}
|
||||
</Nav.item>;
|
||||
</Nav.dropdown>;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
@@ -16,6 +16,8 @@ const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
||||
|
||||
const NaturalCritIcon = require('naturalcrit/svg/naturalcrit.svg.jsx');
|
||||
|
||||
let SAVEKEY = '';
|
||||
|
||||
const AccountPage = createClass({
|
||||
displayName : 'AccountPage',
|
||||
getDefaultProps : function() {
|
||||
@@ -29,6 +31,27 @@ const AccountPage = createClass({
|
||||
uiItems : this.props.uiItems
|
||||
};
|
||||
},
|
||||
componentDidMount : function(){
|
||||
if(!this.state.saveLocation && this.props.uiItems.username) {
|
||||
SAVEKEY = `HOMEBREWERY-DEFAULT-SAVE-LOCATION-${this.props.uiItems.username}`;
|
||||
let saveLocation = window.localStorage.getItem(SAVEKEY);
|
||||
saveLocation = saveLocation ?? (this.state.uiItems.googleId ? 'GOOGLE-DRIVE' : 'HOMEBREWERY');
|
||||
this.makeActive(saveLocation);
|
||||
}
|
||||
},
|
||||
|
||||
makeActive : function(newSelection){
|
||||
if(this.state.saveLocation == newSelection) return;
|
||||
window.localStorage.setItem(SAVEKEY, newSelection);
|
||||
this.setState({
|
||||
saveLocation : newSelection
|
||||
});
|
||||
},
|
||||
|
||||
renderButton : function(name, key, shouldRender=true){
|
||||
if(!shouldRender) return;
|
||||
return <button className={this.state.saveLocation==key ? 'active' : ''} onClick={()=>{this.makeActive(key);}}>{name}</button>;
|
||||
},
|
||||
|
||||
renderNavItems : function() {
|
||||
return <Navbar>
|
||||
@@ -61,6 +84,11 @@ const AccountPage = createClass({
|
||||
</p>
|
||||
}
|
||||
</div>
|
||||
<div className='dataGroup'>
|
||||
<h4>Default Save Location</h4>
|
||||
{this.renderButton('Homebrewery', 'HOMEBREWERY')}
|
||||
{this.renderButton('Google Drive', 'GOOGLE-DRIVE', this.state.uiItems.googleId)}
|
||||
</div>
|
||||
</>;
|
||||
},
|
||||
|
||||
|
||||
@@ -6,7 +6,8 @@ const cx = require('classnames');
|
||||
const moment = require('moment');
|
||||
const request = require('../../../../utils/request-middleware.js');
|
||||
|
||||
const googleDriveIcon = require('../../../../googleDrive.png');
|
||||
const googleDriveIcon = require('../../../../googleDrive.svg');
|
||||
const homebreweryIcon = require('../../../../thumbnail.png');
|
||||
const dedent = require('dedent-tabs').default;
|
||||
|
||||
const BrewItem = createClass({
|
||||
@@ -90,11 +91,17 @@ const BrewItem = createClass({
|
||||
</a>;
|
||||
},
|
||||
|
||||
renderGoogleDriveIcon : function(){
|
||||
if(!this.props.brew.googleId) return;
|
||||
renderStorageIcon : function(){
|
||||
if(this.props.brew.googleId) {
|
||||
return <span title={this.props.brew.webViewLink ? 'Your Google Drive Storage': 'Another User\'s Google Drive Storage'}>
|
||||
<a href={this.props.brew.webViewLink} target='_blank'>
|
||||
<img className='googleDriveIcon' src={googleDriveIcon} alt='googleDriveIcon' />
|
||||
</a>
|
||||
</span>;
|
||||
}
|
||||
|
||||
return <span>
|
||||
<img className='googleDriveIcon' src={googleDriveIcon} alt='googleDriveIcon' />
|
||||
return <span title='Homebrewery Storage'>
|
||||
<img className='homebreweryIcon' src={homebreweryIcon} alt='homebreweryIcon' />
|
||||
</span>;
|
||||
},
|
||||
|
||||
@@ -144,7 +151,7 @@ const BrewItem = createClass({
|
||||
Last updated: ${moment(brew.updatedAt).local().format(dateFormatString)}`}>
|
||||
<i className='fas fa-sync-alt' /> {moment(brew.updatedAt).fromNow()}
|
||||
</span>
|
||||
{this.renderGoogleDriveIcon()}
|
||||
{this.renderStorageIcon()}
|
||||
</div>
|
||||
|
||||
<div className='links'>
|
||||
|
||||
@@ -94,8 +94,15 @@
|
||||
}
|
||||
}
|
||||
.googleDriveIcon {
|
||||
height : 20px;
|
||||
height : 18px;
|
||||
padding : 0px;
|
||||
margin : -5px;
|
||||
}
|
||||
.homebreweryIcon {
|
||||
mix-blend-mode : darken;
|
||||
height : 24px;
|
||||
position : relative;
|
||||
top : 5px;
|
||||
left : -5px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ const ListPage = createClass({
|
||||
sortBrewOrder : function(brew){
|
||||
if(!brew.title){brew.title = 'No Title';}
|
||||
const mapping = {
|
||||
'alpha' : _.deburr(brew.title.toLowerCase()),
|
||||
'alpha' : _.deburr(brew.title.trim().toLowerCase()),
|
||||
'created' : moment(brew.createdAt).format(),
|
||||
'updated' : moment(brew.updatedAt).format(),
|
||||
'views' : brew.views,
|
||||
@@ -219,7 +219,7 @@ const ListPage = createClass({
|
||||
|
||||
render : function(){
|
||||
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'/>
|
||||
{this.props.navItems}
|
||||
{this.renderSortOptions()}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
}
|
||||
.listPage{
|
||||
.content{
|
||||
z-index : 1;
|
||||
.page{
|
||||
.noColumns() !important; //Needed to override PHB Theme since this is on a lower @layer
|
||||
&::after{
|
||||
@@ -63,7 +64,7 @@
|
||||
border-bottom : 1px solid #666;
|
||||
color : white;
|
||||
text-align : center;
|
||||
z-index : 500;
|
||||
z-index : 1;
|
||||
display : flex;
|
||||
justify-content : center;
|
||||
align-items : baseline;
|
||||
|
||||
@@ -1,47 +1,69 @@
|
||||
.uiPage{
|
||||
.content{
|
||||
overflow-y : hidden;
|
||||
width : 90vw;
|
||||
background-color: #f0f0f0;
|
||||
font-family: 'Open Sans';
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin-top: 25px;
|
||||
padding: 2% 4%;
|
||||
font-size: 0.8em;
|
||||
line-height: 1.8em;
|
||||
.dataGroup{
|
||||
padding: 6px 20px 15px;
|
||||
border: 2px solid black;
|
||||
border-radius: 5px;
|
||||
margin: 5px 0px;
|
||||
}
|
||||
h1, h2, h3, h4{
|
||||
font-weight: 900;
|
||||
text-transform: uppercase;
|
||||
margin: 0.5em 30% 0.25em 0;
|
||||
border-bottom: 2px solid slategrey;
|
||||
}
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
border-bottom: 2px solid darkslategrey;
|
||||
margin-bottom: 0.5em;
|
||||
margin-right: 0;
|
||||
}
|
||||
h2 {
|
||||
font-size: 1.75em;
|
||||
}
|
||||
h3 {
|
||||
font-size: 1.5em;
|
||||
svg {
|
||||
width: 19px;
|
||||
.homebrew {
|
||||
.uiPage.sitePage {
|
||||
.content {
|
||||
width : ~"min(90vw, 1000px)";
|
||||
padding : 2% 4%;
|
||||
margin-top : 25px;
|
||||
margin-right : auto;
|
||||
margin-left : auto;
|
||||
overflow-y : scroll;
|
||||
font-family : 'Open Sans';
|
||||
font-size : 0.8em;
|
||||
line-height : 1.8em;
|
||||
background-color : #F0F0F0;
|
||||
.dataGroup {
|
||||
padding : 6px 20px 15px;
|
||||
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 {
|
||||
width : 100%;
|
||||
margin : 0.5em 30% 0.25em 0;
|
||||
font-weight : 900;
|
||||
text-transform : uppercase;
|
||||
border-bottom : 2px solid slategrey;
|
||||
}
|
||||
h1 {
|
||||
margin-right : 0;
|
||||
margin-bottom : 0.5em;
|
||||
font-size : 2em;
|
||||
border-bottom : 2px solid darkslategrey;
|
||||
}
|
||||
h2 { font-size : 1.75em; }
|
||||
h3 {
|
||||
font-size : 1.5em;
|
||||
svg { width : 19px; }
|
||||
}
|
||||
h4 { font-size : 1.25em; }
|
||||
strong { font-weight : bold; }
|
||||
em { font-style : italic; }
|
||||
ul {
|
||||
padding-left : 1.25em;
|
||||
list-style : square;
|
||||
}
|
||||
.blank {
|
||||
height : 1em;
|
||||
margin-top : 0;
|
||||
& + * { margin-top : 0; }
|
||||
}
|
||||
}
|
||||
h4 {
|
||||
font-size: 1.25em;
|
||||
}
|
||||
strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,8 +24,7 @@ const Markdown = require('naturalcrit/markdown.js');
|
||||
|
||||
const { DEFAULT_BREW_LOAD } = require('../../../../server/brewDefaults.js');
|
||||
|
||||
const googleDriveActive = require('../../googleDrive.png');
|
||||
const googleDriveInactive = require('../../googleDriveMono.png');
|
||||
const googleDriveIcon = require('../../googleDrive.svg');
|
||||
|
||||
const SAVE_TIMEOUT = 3000;
|
||||
|
||||
@@ -92,7 +91,7 @@ const EditPage = createClass({
|
||||
if(!(e.ctrlKey || e.metaKey)) return;
|
||||
const S_KEY = 83;
|
||||
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 || e.keyCode == S_KEY){
|
||||
e.stopPropagation();
|
||||
@@ -138,13 +137,14 @@ const EditPage = createClass({
|
||||
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.hasChanges()){
|
||||
this.debounceSave();
|
||||
} else {
|
||||
this.debounceSave.cancel();
|
||||
}
|
||||
if(immediate) this.debounceSave.flush();
|
||||
},
|
||||
|
||||
handleGoogleClick : function(){
|
||||
@@ -222,10 +222,7 @@ const EditPage = createClass({
|
||||
|
||||
renderGoogleDriveIcon : function(){
|
||||
return <Nav.item className='googleDriveStorage' onClick={this.handleGoogleClick}>
|
||||
{this.state.saveGoogle
|
||||
? <img src={googleDriveActive} alt='googleDriveActive'/>
|
||||
: <img src={googleDriveInactive} alt='googleDriveInactive'/>
|
||||
}
|
||||
<img src={googleDriveIcon} className={this.state.saveGoogle ? '' : 'inactive'} alt='Google Drive icon'/>
|
||||
|
||||
{this.state.confirmGoogleTransfer &&
|
||||
<div className='errorContainer' onClick={this.closeAlerts}>
|
||||
@@ -258,6 +255,15 @@ const EditPage = createClass({
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
{this.state.alertTrashedGoogleBrew &&
|
||||
<div className='errorContainer' onClick={this.closeAlerts}>
|
||||
This brew is currently in your Trash folder on Google Drive!<br />If you want to keep it, make sure to move it before it is deleted permanently!<br />
|
||||
<div className='confirm'>
|
||||
OK
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</Nav.item>;
|
||||
},
|
||||
|
||||
@@ -339,16 +345,6 @@ const EditPage = createClass({
|
||||
const shareLink = this.processShareId();
|
||||
|
||||
return <Navbar>
|
||||
|
||||
{this.state.alertTrashedGoogleBrew &&
|
||||
<div className='errorContainer' onClick={this.closeAlerts}>
|
||||
This brew is currently in your Trash folder on Google Drive!<br />If you want to keep it, make sure to move it before it is deleted permanently!<br />
|
||||
<div className='confirm'>
|
||||
OK
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<Nav.section>
|
||||
<Nav.item className='brewTitle'>{this.state.brew.title}</Nav.item>
|
||||
</Nav.section>
|
||||
@@ -402,7 +398,14 @@ const EditPage = createClass({
|
||||
reportError={this.errorReported}
|
||||
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} 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}
|
||||
/>
|
||||
</SplitPane>
|
||||
</div>
|
||||
</div>;
|
||||
|
||||
@@ -18,8 +18,12 @@
|
||||
position : relative;
|
||||
}
|
||||
.googleDriveStorage img{
|
||||
height : 20px;
|
||||
height : 18px;
|
||||
padding : 0px;
|
||||
margin : -5px;
|
||||
|
||||
&.inactive {
|
||||
filter: grayscale(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,44 +4,37 @@ const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
const cx = require('classnames');
|
||||
|
||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||
const Navbar = require('../../navbar/navbar.jsx');
|
||||
const PatreonNavItem = require('../../navbar/patreon.navitem.jsx');
|
||||
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
||||
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
||||
const UIPage = require('../basePages/uiPage/uiPage.jsx');
|
||||
|
||||
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
||||
const Markdown = require('../../../../shared/naturalcrit/markdown.js');
|
||||
|
||||
const ErrorIndex = require('./errors/errorIndex.js');
|
||||
|
||||
const ErrorPage = createClass({
|
||||
displayName : 'ErrorPage',
|
||||
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
ver : '0.0.0',
|
||||
errorId : ''
|
||||
errorId : '',
|
||||
text : '# Oops \n We could not find a brew with that id. **Sorry!**',
|
||||
error : {}
|
||||
};
|
||||
},
|
||||
|
||||
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>
|
||||
const errorText = ErrorIndex(this.props)[this.props.brew.HBErrorCode.toString()] || '';
|
||||
|
||||
<Nav.section>
|
||||
<PatreonNavItem />
|
||||
<HelpNavItem />
|
||||
<RecentNavItem />
|
||||
</Nav.section>
|
||||
</Navbar>
|
||||
|
||||
<div className='content'>
|
||||
<BrewRenderer text={this.text} />
|
||||
return <UIPage brew={{ title: 'Crit Fail!' }}>
|
||||
<div className='dataGroup'>
|
||||
<div className='errorTitle'>
|
||||
<h1>{`Error ${this.props.brew.status || '000'}`}</h1>
|
||||
<h4>{this.props.brew.text || 'No error text'}</h4>
|
||||
</div>
|
||||
<hr />
|
||||
<div dangerouslySetInnerHTML={{ __html: Markdown.render(errorText) }} />
|
||||
</div>
|
||||
</div>;
|
||||
</UIPage>;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
.errorPage{
|
||||
.errorTitle{
|
||||
background-color: @orange;
|
||||
.homebrew {
|
||||
.uiPage.sitePage {
|
||||
.errorTitle {
|
||||
//background-color: @orange;
|
||||
color : #D02727;
|
||||
text-align : center;
|
||||
}
|
||||
.content {
|
||||
h1, h2, h3, h4 { border-bottom : none; }
|
||||
hr { border-bottom : 2px solid slategrey; }
|
||||
}
|
||||
}
|
||||
}
|
||||
126
client/homebrew/pages/errorPage/errors/errorIndex.js
Normal file
@@ -0,0 +1,126 @@
|
||||
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}`;}).join(', ') || 'Unable to list authors'}`,
|
||||
|
||||
// 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}`;}).join(', ') || 'Unable to list authors'}`,
|
||||
|
||||
// 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}`,
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = errorIndex;
|
||||
@@ -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!
|
||||
|
||||
### 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.
|
||||
|
||||
@@ -48,57 +48,63 @@ If you want to save ink or have a monochrome printer, add the **PRINT → {{fas,
|
||||
|
||||
\column
|
||||
|
||||
## New in V3.0.0
|
||||
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*).
|
||||
## V3 vs Legacy
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
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!
|
||||
Check out the latest updates in the full changelog [here](/changelog).
|
||||
|
||||
### 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?
|
||||
Take a quick look at our [Frequently Asked Questions page](/faq) to see if your question has a handy answer.
|
||||
|
||||
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!
|
||||
|
||||
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.
|
||||
- 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).
|
||||
- Report technical issues or provide feedback on the [GitHub Repo](https://github.com/naturalcrit/homebrewery/).
|
||||
|
||||
### 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.
|
||||
|
||||
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 Me
|
||||
If you'd like to credit me in your brew, I'd be flattered! Just reference that you made it with The Homebrewery.
|
||||
#### Crediting Us
|
||||
If you'd like to credit us in your brew, we'd be flattered! Just reference that you made it with The Homebrewery.
|
||||
|
||||
### 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>
|
||||
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.
|
||||
[{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 [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
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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://discord.gg/by3deKx)
|
||||
[{height:30px}](https://github.com/naturalcrit/homebrewery)
|
||||
[{height:30px}](https://patreon.com/NaturalCrit)
|
||||
[{height:30px}](https://www.reddit.com/r/homebrewery/)
|
||||
}}
|
||||
|
||||
\page
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## 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.
|
||||
|
||||
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.
|
||||
**You can enable V3 via the {{fa,fa-info-circle}} Properties button!**
|
||||
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.
|
||||
|
||||
### 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
|
||||
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,17 @@ 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>`
|
||||
|
||||
### Definition Lists
|
||||
**Example** :: V3 uses HTML *definition lists* to create "lists" with hanging indents.
|
||||
|
||||
|
||||
|
||||
### Column Breaks
|
||||
Column and page breaks with `\column` and `\page`.
|
||||
|
||||
\column
|
||||
|
||||
### Tables
|
||||
Tables now allow column & row spanning between cells. This is included in some updated snippets, but a simplified example is given below.
|
||||
|
||||
@@ -163,13 +170,13 @@ Using *Curly Injection* you can assign an id, classes, or inline CSS properties
|
||||
|
||||
 {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
|
||||
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
|
||||
{{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}}
|
||||
{{footnote PART 2 | BORING STUFF}}
|
||||
|
||||
@@ -20,9 +20,10 @@ const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
||||
|
||||
const { DEFAULT_BREW } = require('../../../../server/brewDefaults.js');
|
||||
|
||||
const BREWKEY = 'homebrewery-new';
|
||||
const BREWKEY = 'homebrewery-new';
|
||||
const STYLEKEY = 'homebrewery-new-style';
|
||||
const METAKEY = 'homebrewery-new-meta';
|
||||
const METAKEY = 'homebrewery-new-meta';
|
||||
let SAVEKEY;
|
||||
|
||||
|
||||
const NewPage = createClass({
|
||||
@@ -61,16 +62,21 @@ const NewPage = createClass({
|
||||
// brew.description = metaStorage?.description || this.state.brew.description;
|
||||
brew.renderer = metaStorage?.renderer ?? brew.renderer;
|
||||
brew.theme = metaStorage?.theme ?? brew.theme;
|
||||
|
||||
this.setState({
|
||||
brew : brew
|
||||
});
|
||||
brew.lang = metaStorage?.lang ?? brew.lang;
|
||||
}
|
||||
|
||||
SAVEKEY = `HOMEBREWERY-DEFAULT-SAVE-LOCATION-${global.account?.username || ''}`;
|
||||
const saveStorage = localStorage.getItem(SAVEKEY) || 'HOMEBREWERY';
|
||||
|
||||
this.setState({
|
||||
brew : brew,
|
||||
saveGoogle : (saveStorage == 'GOOGLE-DRIVE' && this.state.saveGoogle)
|
||||
});
|
||||
|
||||
localStorage.setItem(BREWKEY, brew.text);
|
||||
if(brew.style)
|
||||
localStorage.setItem(STYLEKEY, brew.style);
|
||||
localStorage.setItem(METAKEY, JSON.stringify({ 'renderer': brew.renderer, 'theme': brew.theme }));
|
||||
localStorage.setItem(METAKEY, JSON.stringify({ 'renderer': brew.renderer, 'theme': brew.theme, 'lang': brew.lang }));
|
||||
},
|
||||
componentWillUnmount : function() {
|
||||
document.removeEventListener('keydown', this.handleControlKeys);
|
||||
@@ -114,13 +120,16 @@ const NewPage = createClass({
|
||||
handleMetaChange : function(metadata){
|
||||
this.setState((prevState)=>({
|
||||
brew : { ...prevState.brew, ...metadata },
|
||||
}));
|
||||
localStorage.setItem(METAKEY, JSON.stringify({
|
||||
// 'title' : this.state.brew.title,
|
||||
// 'description' : this.state.brew.description,
|
||||
'renderer' : this.state.brew.renderer,
|
||||
'theme' : this.state.brew.theme
|
||||
}));
|
||||
}), ()=>{
|
||||
localStorage.setItem(METAKEY, JSON.stringify({
|
||||
// 'title' : this.state.brew.title,
|
||||
// 'description' : this.state.brew.description,
|
||||
'renderer' : this.state.brew.renderer,
|
||||
'theme' : this.state.brew.theme,
|
||||
'lang' : this.state.brew.lang
|
||||
}));
|
||||
});
|
||||
;
|
||||
},
|
||||
|
||||
save : async function(){
|
||||
@@ -211,7 +220,7 @@ const NewPage = createClass({
|
||||
onMetaChange={this.handleMetaChange}
|
||||
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} 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} lang={this.state.brew.lang} errors={this.state.htmlErrors}/>
|
||||
</SplitPane>
|
||||
</div>
|
||||
</div>;
|
||||
|
||||
@@ -5,6 +5,7 @@ const { Meta } = require('vitreum/headtags');
|
||||
|
||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||
const Navbar = require('../../navbar/navbar.jsx');
|
||||
const MetadataNav = require('../../navbar/metadata.navitem.jsx');
|
||||
const PrintLink = require('../../navbar/print.navitem.jsx');
|
||||
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
||||
const Account = require('../../navbar/account.navitem.jsx');
|
||||
@@ -50,8 +51,10 @@ const SharePage = createClass({
|
||||
return <div className='sharePage sitePage'>
|
||||
<Meta name='robots' content='noindex, nofollow' />
|
||||
<Navbar>
|
||||
<Nav.section>
|
||||
<Nav.item className='brewTitle'>{this.props.brew.title}</Nav.item>
|
||||
<Nav.section className='titleSection'>
|
||||
<MetadataNav brew={this.props.brew}>
|
||||
<Nav.item className='brewTitle'>{this.props.brew.title}</Nav.item>
|
||||
</MetadataNav>
|
||||
</Nav.section>
|
||||
|
||||
<Nav.section>
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
.sharePage{
|
||||
.navContent .navSection.titleSection {
|
||||
flex-grow: 1;
|
||||
justify-content: center;
|
||||
}
|
||||
.content{
|
||||
overflow-y : hidden;
|
||||
}
|
||||
|
||||
1
client/icons/Davek.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 791.04 953.29"><title>Davek</title><g id="Layer_2" data-name="Layer 2"><g id="Davek"><path d="M178.41,13.46a19.33,19.33,0,0,0-4.71,5.38q8.07,6.07,13.46,6.07a8.27,8.27,0,0,0,4.71-1.35,130.23,130.23,0,0,0,16.83-7.07,74.55,74.55,0,0,1,18.85-6.39h2.7q8.07,0,14.81,8.74a944.19,944.19,0,0,0,95.6,4.72q19.5,0,38.37-.67,69.33-2,139.68-5.72t139.7-5.06q16.82-.64,34.34-.66,50.49,0,98.29,3.36-17.5,12.12-22.55,31.64t-5,33.66q.64,22.89.66,45.1,0,47.13-3.36,97-6.07,74.05-9.78,148.11t-5,146.09v17.51a766.1,766.1,0,0,0,8.75,118.48,38.57,38.57,0,0,0-4,17.51,30.94,30.94,0,0,0,.67,6.06q2,12.12,3.36,23.22c.9,7.42,1.57,14.92,2,22.55v3.37a57.93,57.93,0,0,1-3.36,19.52c.43,4.5.67,8.77.67,12.8a260.65,260.65,0,0,1-2.7,37,344.26,344.26,0,0,0-4,52.52,133.5,133.5,0,0,0,8.09,45.44q8.07,22.57,33,36.68-6.06,8.78-20.19,8.77H762.1c-4.5-.45-8.53-.69-12.12-.69a78.11,78.11,0,0,0-21.54,2.7,579.1,579.1,0,0,0-63.64,3.71q-33.31,3.71-67.65,6.39t-68.66,3.37h-4a188.05,188.05,0,0,1-59.92-9.43q20.19-4,39.06-23.22t20.19-47.46q11.44-22.21,11.45-49.82a320.44,320.44,0,0,1,3.36-49.15q-9.45-4.69-10.09-8.75v-2.7a73,73,0,0,1,.66-8.74,105.81,105.81,0,0,0,3.37-12.8,7.49,7.49,0,0,0,.68-3.37q0-4.7-4.05-10.09c.45-4.93.69-10.1.69-15.48a311.71,311.71,0,0,0-3.37-46.45,207.31,207.31,0,0,1-1.35-24.25,274.58,274.58,0,0,1,4-45.1l15.5,6.73q-3.37-17.49-3.37-41.07,0-24.89,8.75-44.44a27.73,27.73,0,0,0,2-9.43,15.32,15.32,0,0,0-3.36-10.09,60.75,60.75,0,0,1-10.1-15.48l-7.39,6.73q2.67-47.79,8.74-99,3.35-33.63,3.37-65.29,0-14.81-.69-29a205.09,205.09,0,0,1-4-41.74,190.26,190.26,0,0,1,2-26.92q4-37,14.81-67.33a25.14,25.14,0,0,1-2.68-11.43,31.13,31.13,0,0,1,.66-6.07V140q0-6.72-8.74-10.09-3.37-16.83-5.73-31.3T521.07,77.41q-55.2,2.7-115.78,4.71-19.55.7-39.72.69-38.38,0-74.06-2.7c-5.4,4.5-8.08,9.21-8.08,14.14v1.34a41.5,41.5,0,0,0,4.37,15.49q3.7,7.4,7.4,15.16a35,35,0,0,1,3.71,15.13q32.31,34.35,64,68.68a335.89,335.89,0,0,1,51.83,73.38q13.46,7.4,18.51,17.49t10.11,19.87q5.06,9.78,10.1,18.85t16.5,11.78v12.12a194.5,194.5,0,0,1-37.38-4q-20.52-4-40.73-6.73a114.48,114.48,0,0,0-17.49-1.35,97.2,97.2,0,0,0-20.2,2q-17.52,4.05-31,20.19-16.84-1.35-27.27-9.75a76.13,76.13,0,0,1-17.51-20.2q-7.06-11.76-14.47-24.9a79.77,79.77,0,0,0-18.84-22.57A305.87,305.87,0,0,1,177.73,237q-28.29-33.67-54.54-69T68,99.31A381.16,381.16,0,0,0,0,38.37q12.79,0,22.89-9.75A190.69,190.69,0,0,1,44.76,10.44Q56.54,2,68.66,0H72Q82.8,0,97,10.76Z"/></g></g></svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
1
client/icons/Iokharic.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 428.05 941.17"><title>Iokharic</title><g id="Layer_2" data-name="Layer 2"><g id="Iokharic"><path d="M334.76,909.61V259.3l2.74-89.18c3.43,0,6.18-8.23,7.55-24.69,3.43,0,7.55-8.92,13.72-27.44,13-11,19.89-21.27,19.89-31.56,0-13-5.48-20.58-17.15-23.32l-30.87,2.74H320.36c-21.27,13-39.79,22.64-56.94,27.44h-37c-11.67,0-26.76,7.55-46,22q-12.34,0-30.86,16.46c-10.29,0-40.48,26.75-91.93,80.95,0,8.23-6.17,21.26-18.52,38.41l-3.43,15.78v41.84L67.23,343c2.74,0,9.6,6.86,19.89,19.9,24,18.52,36.36,30.86,36.36,38.41l-12.35,10.29H105c-24.7-15.78-45.28-32.93-62.43-52.13L15.78,316.92,0,266.85c3.43-17.84,7.55-29.5,13.72-35v-11c0-18.52,7.55-39.79,22-63.8,0-9.6,8.23-21.27,24.7-34.3,0-9.6,15.77-26.07,46.64-50.08,19.9-16.46,46-28.12,76.83-35,5.49-6.86,21.27-14.41,46.65-21.95C238,5.49,251.07,0,270.28,0h137.2c8.91,0,15.77,8.23,20.57,24V40.47l-5.48,8.23V166c0,17.15-7.55,31.55-21.95,43.22v41.15l-2.75,24.7q0,9.26,24.7,30.87v38.41c0,10.29-4.81,19.9-15.09,28.82h-6.86V558.39c0,55.57-4.81,97.41-15.1,124.16-4.8,2.75-7.54,19.21-9.6,48.71l2.74,17.15-2.74,76.14v30.19q0,32.93-32.93,86.43C337.5,937.74,334.76,926.76,334.76,909.61Z"/></g></g></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
1
client/icons/Rellanic.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 527.7 940.25"><title>Rellanic</title><g id="Layer_2" data-name="Layer 2"><g id="Rellanic"><path d="M527.7,5.45q-3.83,19.65-15,30.56a129.61,129.61,0,0,1-26.46,19.64q-9.84,6.56-31.66,15.28-19.63,7.65-31.64,16.38Q380.33,103.69,342.16,108a468.46,468.46,0,0,1-54,3.28q-15.83,0-30.56-1.1a53.19,53.19,0,0,0-20.19-6.55H217.74q-7.12,1.11-21.29,1.1a51.67,51.67,0,0,1-20.18-4.36q8.72,19.65,25.63,29.46,14.19,8.74,28.38,29.47a634.05,634.05,0,0,1,98.78,90.58l91.12,103.69a65.1,65.1,0,0,0-.54,8.19,42.47,42.47,0,0,0,.54,7.09c.73,1.82,1.27,3.29,1.64,4.37q7.08,8.75,10.92,12,1.62,1.1,12.55,14.19a14,14,0,0,1,3.27,6.55,9.75,9.75,0,0,1,1.1,4.37,9.62,9.62,0,0,1-1.1,4.36q35.46,43.66,51.3,89.5,3.25,9.82,5.45,19.64a288.59,288.59,0,0,1,10.37,68.75v8.19a296,296,0,0,1-9.81,76.94q-7.12,27.3-24,77.5L418,831.65Q383,872,344.88,899.31a243.27,243.27,0,0,1-90.59,38.19,179.84,179.84,0,0,1-31.64,2.75q-38.78,0-81.87-15.84A293.78,293.78,0,0,1,78,886.22a312.61,312.61,0,0,1-51.85-48,300.52,300.52,0,0,0-18-46.94,60.18,60.18,0,0,1-4.92-13.64,82.36,82.36,0,0,1-2.19-19.11,104.89,104.89,0,0,1,.56-10.91,176.12,176.12,0,0,1-1.64-24,199.79,199.79,0,0,1,2.72-32.74Q5.45,663,5.45,645a103.71,103.71,0,0,0-.54-10.92,242.44,242.44,0,0,1,50.74-67.66,646.83,646.83,0,0,0,57.86-61.12q11.44-10.89,25.09-13.1A88.3,88.3,0,0,1,163.71,489q14.17-1.11,29.46-1.1a108.11,108.11,0,0,0,28.38-7.63q17.44,8.75,27.29,12a124.47,124.47,0,0,1,28.38,13.1q8.71,4.38,23.46,17.46,9.29,9.86,17.47,28.38,7.07,12,9.27,21.83a35.16,35.16,0,0,1,1.64,9.83V585a80.23,80.23,0,0,1-8.73,27.28q-8.2,14.19-18,22.93a166.18,166.18,0,0,1-19.65,19.64q-13.1,8.74-20.72,13.1l-7.65-4.37v-1.64q0-12,6.55-18-8.17-6.55-10.36-10.92l-2.18-8.73c0-2.18-.74-5.81-2.19-10.91v-3.29a38,38,0,0,0-3.82-7.63,196.53,196.53,0,0,0-33.84-40.39Q185.53,542.43,162.61,537a163.71,163.71,0,0,0-50.75,9.81q-25.08,8.76-32.2,36Q67.12,615.56,67.13,654.3a256,256,0,0,0,3.26,39.83,176.75,176.75,0,0,0,5.47,28.38Q88.37,770,122.78,812a452.22,452.22,0,0,0,103.13,58.94,153.57,153.57,0,0,0,107,5.45q25.63-12,37.66-27.28,13.62-14.21,23.46-34.93,10.36-18.57,20.2-39.29Q426.72,753.05,437.1,740q3.27-44.76,5.47-61.12a228.17,228.17,0,0,0,3.26-38.21,213.15,213.15,0,0,0-1.64-26.19,245.3,245.3,0,0,0-8.17-48q-2.2-8.17-4.93-16.36-9.27-30.55-34.92-61.12a70,70,0,0,0-2.18-18,29.12,29.12,0,0,0-4.37-10.37,175.28,175.28,0,0,0-17.46-29.48l-18.55-27.27q-12-16.38-16.38-28.38a282.35,282.35,0,0,1-27.81-28.37q-20.22-26.2-24-31.66Q269,295.76,260.29,286q-10.92-12-31.1-25.11-36.56-31.65-79.12-70.94-45.31-39.28-88.41-66.58-14.74-8.17-17.46-16.9a16.93,16.93,0,0,0-.54-3.83V99.87q0-8.73,6.54-19.11A102.47,102.47,0,0,1,63.3,61.12q9.27-9.82,12.56-18.56a223.6,223.6,0,0,1,38.73-3.27,271,271,0,0,1,40.93,3.27A367.15,367.15,0,0,0,215,47.48c6.91,0,13.64-.17,20.2-.56a45,45,0,0,0,21.27,5.47q17.44,0,25.65-1.1h22.93a77.75,77.75,0,0,1,24,7.65,114,114,0,0,1,27.82-3.29H364q27.25,2.2,39.29,2.19,16.34,0,36.55-5.45,19.1-6.55,27.83-22.93h2.72A20.48,20.48,0,0,0,484.58,24c2.17-4.71,6.17-7.09,12-7.09a26.6,26.6,0,0,1,4.92.54v-.54c0-1.08.72-3.46,2.19-7.11a36.74,36.74,0,0,1,6-6.54C512.57,1.1,515.12,0,517.32,0,521,0,524.41,1.82,527.7,5.45Z"/></g></g></svg>
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
52
client/icons/book-back-cover.svg
Normal file
@@ -0,0 +1,52 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
viewBox="0 0 541.53217 512"
|
||||
version="1.1"
|
||||
id="back-cover-icon"
|
||||
sodipodi:docname="book-front-cover.svg"
|
||||
width="541.53217"
|
||||
height="512"
|
||||
inkscape:version="1.2.2 (732a01da63, 2022-12-09)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs22131" />
|
||||
<sodipodi:namedview
|
||||
id="namedview22129"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#111111"
|
||||
borderopacity="1"
|
||||
inkscape:showpageshadow="0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pagecheckerboard="1"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
showgrid="false"
|
||||
inkscape:zoom="0.39257813"
|
||||
inkscape:cx="-263.64179"
|
||||
inkscape:cy="444.49751"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="991"
|
||||
inkscape:window-x="-9"
|
||||
inkscape:window-y="-9"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg22127" />
|
||||
<!--! Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
|
||||
<g id="g20308" transform="matrix(3.7795276,0,0,3.7795276,-201.76367,-251.58203)">
|
||||
<path id="rect20232" d="M95.1,66.6h-8.5c-4.7,0-8.5,3.8-8.5,8.5v21.4c3.5-0.4,7.4-0.5,12-0.5c0.7,0,0.6,0,1.2,0
|
||||
c0-2.4,0-4.2,0.3-6.2c0.3-2.2,2.2-5.8,3.5-7c0.9-0.9,3-3.2,7-3.7c1-0.1,2-0.1,2.8,0c2.6,0.3,4.6,1.6,6.1,2.6
|
||||
c3.9,2.7,7.4,6.4,14.8,13.8c6.3,6.3,9.8,9.8,12,12.4c1.1,1.3,2.1,2.4,2.9,4c0.9,1.7,1.4,4.2,1.4,5.6c0,1.4-0.5,4-1.4,5.6
|
||||
c-0.9,1.6-1.8,2.7-2.9,4c-2.2,2.6-5.6,6-11.8,12.2c-3.8,3.8-7.4,7.3-10.2,9.9c-1.4,1.3-2.6,2.4-3.6,3.3c-0.5,0.4-1,0.8-1.5,1.2
|
||||
c-0.3,0.2-0.5,0.4-1,0.7s-0.7,0.7-2.8,1.2c-4.3,1.1-6.3,0.4-9.4-1.3c-0.5-0.3-1.9-0.9-3.3-2.6c-1.4-1.7-2.1-3.7-2.4-5.1
|
||||
c-0.5-2.4-0.5-4.3-0.6-7.2c-3.9,0-6,0.1-6.5,0.1c-0.5,0.1,0.2-0.2-1.2,0.5c-1.7,0.8-3.6,2.8-4.4,4.5c-0.3,0.8-0.5,1-0.6,6.6
|
||||
c-0.1,2.2-0.2,4.3-0.4,6c0,0.3-0.1,0.6-0.1,0.8v1.9c0,4.7,3.8,8.5,8.5,8.5v16.9c-4.7,0-8.5,3.8-8.5,8.5c0,4.7,3.8,8.5,8.5,8.5h8.5
|
||||
h76.2c14,0,25.4-11.4,25.4-25.4V92c0-14-11.4-25.4-25.4-25.4L95.1,66.6z M171.3,168.2c4.7,0,8.5,3.8,8.5,8.5c0,4.7-3.8,8.5-8.5,8.5
|
||||
h-67.7v-16.9L171.3,168.2L171.3,168.2z"/>
|
||||
<path id="path20297" d="M63.4,158c1.8,1.6,4.5,1.9,5.5,0.7c0.3-0.4,0.7-4,0.8-8.1c0.2-5.9,0.5-7.9,1.4-10c1.7-3.7,4.9-7,8.6-8.9
|
||||
c3.1-1.5,3.6-1.6,11.7-1.6h8.5l0.3,7.6c0.3,7.5,0.3,7.7,1.7,8.5c0.8,0.5,2.1,0.7,2.8,0.5c0.8-0.2,7.4-6.4,14.9-13.9
|
||||
c12.4-12.4,13.5-13.7,13.5-15.5c0-1.8-1.1-3.1-13.8-15.7c-14.7-14.7-15.4-15.2-18-12.7c-1,1-1.1,1.9-1.1,7.6c0,3.6-0.2,6.9-0.3,7.4
|
||||
c-0.3,0.8-1.7,0.9-9.8,0.9c-15.6,0-21.1,1.7-27.9,8.5c-6.5,6.5-8.8,12-8.8,21.1c0,4.7,0.3,6.8,1.3,9.8
|
||||
C56.2,148.6,60.7,155.7,63.4,158L63.4,158z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
@@ -2,7 +2,7 @@
|
||||
<svg
|
||||
viewBox="0 0 541.53217 512"
|
||||
version="1.1"
|
||||
id="svg22127"
|
||||
id="front-cover-icon"
|
||||
sodipodi:docname="book-front-cover.svg"
|
||||
width="541.53217"
|
||||
height="512"
|
||||
|
||||
|
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 4.6 KiB |
53
client/icons/book-inside-cover.svg
Normal file
@@ -0,0 +1,53 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
viewBox="0 0 704.00001 512"
|
||||
version="1.1"
|
||||
id="svg22127"
|
||||
sodipodi:docname="book-inside-cover.svg"
|
||||
width="704"
|
||||
height="512"
|
||||
inkscape:version="1.2.2 (732a01da63, 2022-12-09)"
|
||||
inkscape:export-filename="InsideCover3.png"
|
||||
inkscape:export-xdpi="300"
|
||||
inkscape:export-ydpi="300"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs22131" />
|
||||
<sodipodi:namedview
|
||||
id="namedview22129"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#111111"
|
||||
borderopacity="1"
|
||||
inkscape:showpageshadow="0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pagecheckerboard="1"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
showgrid="false"
|
||||
inkscape:zoom="0.47274833"
|
||||
inkscape:cx="83.55397"
|
||||
inkscape:cy="178.74204"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="991"
|
||||
inkscape:window-x="-9"
|
||||
inkscape:window-y="-9"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg22127" />
|
||||
<!--! Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
|
||||
<path
|
||||
id="path2161-6"
|
||||
style="color:#000000;fill:#000000;stroke-width:1;-inkscape-stroke:none;paint-order:stroke fill markers"
|
||||
d="M 208,0 C 147.0078,0 94.429433,14.25071 60.367188,26.66992 23.520854,39.96036 0,76.16076 0,112.95896 v 317.8321 c 0,59.8499 56.949847,92.6546 107.47266,76.6035 l -0.1543,0.049 c 26.46715,-8.335 74.84649,-18.3965 100.68164,-18.3965 17.25807,0 61.31688,10.6183 85.14453,18.8438 l 0.0508,0.018 0.0527,0.018 c 19.82627,6.5858 40.84117,4.9222 58.99804,-3.0762 18.04267,7.8799 38.84257,9.6126 58.33594,3.1328 l 0.13672,-0.045 0.13672,-0.047 c 23.88445,-8.0588 67.88646,-18.8437 85.14453,-18.8437 25.83515,0 74.22549,10.0266 100.68164,18.3964 l 0.1543,0.049 0.15625,0.049 C 647.13371,523.05316 704,490.64216 704,430.79226 v -317.8321 c 0,-36.8274 -23.49583,-72.8235 -60.00977,-86.25583 l -0.16015,-0.0606 -0.16211,-0.0566 C 609.79193,14.33005 557.11269,0.0012 496,0.0012 434.5671,0.0012 387.12553,14.01354 352,34.94261 316.87446,14.01344 269.4331,0.0012 208,0.0012 Z m 0,32.00977 c 58.3999,0 103.40004,18.89469 123,33.63279 3.3,2.4564 5,6.4246 5,10.3926 v 356.5508 c 0,10.7702 -11.70041,18.2326 -22.40039,14.6426 -26.59996,-8.9751 -71.69966,-22.2012 -105.59961,-22.2012 -38.49993,0 -88.40045,11.4317 -119.900391,21.3516 C 76.799621,449.96896 64,442.03166 64,430.78906 V 80.94726 C 64,69.51586 70.799631,58.93546 82.099609,54.87306 110.29956,44.57516 157.50009,32.00977 208,32.00977 Z m 288,0 c 50.49991,0 97.70044,12.56619 125.90039,22.76949 C 633.20037,58.93616 640,69.51586 640,80.94726 v 349.8418 c 0,11.2426 -12.79963,19.0854 -24.09961,15.5899 -31.49995,-9.9199 -81.40046,-21.3516 -119.90039,-21.3516 -33.89995,0 -78.99966,13.2261 -105.59961,22.2012 C 379.60041,450.81856 368,443.35616 368,432.58596 V 76.03516 c 0,-3.968 1.60001,-7.9362 5,-10.3926 19.59997,-14.7381 64.6001,-33.63279 123,-33.63279 z M 335.52734,45.75386 c -0.1289,0.093 -0.23137,0.2032 -0.35937,0.2969 -0.198,0.1477 -0.428,0.2796 -0.625,0.4278 z m 33.67969,0.5372 0.24805,0.1875 c -0.0427,-0.033 -0.0937,-0.061 -0.13672,-0.094 -0.0393,-0.03 -0.0713,-0.064 -0.11133,-0.094 z" />
|
||||
<path
|
||||
style="color:#000000;fill:#000000;fill-opacity:1;stroke-width:1;-inkscape-stroke:none"
|
||||
d="m 206.76992,184 c -36.98368,0 -73.07301,9.2343 -94.76923,16.9066 v 185.1887 c 27.62799,-7.7405 62.70503,-15.0804 94.76923,-15.0804 28.33376,0 58.16312,7.6425 81.23077,14.806 V 203.0154 C 273.60322,195.1776 243.44241,184 206.76992,184 Z"
|
||||
id="path4372-8"
|
||||
sodipodi:nodetypes="sccsccs" />
|
||||
<path
|
||||
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:63.9999;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 255.99995,122.53007 c -31.8285,-15.342 -80.43462,-15.4137 -112,0"
|
||||
id="path2371-6"
|
||||
sodipodi:nodetypes="cc" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
54
client/icons/book-part-cover.svg
Normal file
@@ -0,0 +1,54 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
viewBox="0 0 704.00001 512"
|
||||
version="1.1"
|
||||
id="svg22127"
|
||||
sodipodi:docname="book-part-cover.svg"
|
||||
width="704"
|
||||
height="512"
|
||||
inkscape:version="1.2.2 (732a01da63, 2022-12-09)"
|
||||
inkscape:export-filename="InsideCover3.png"
|
||||
inkscape:export-xdpi="300"
|
||||
inkscape:export-ydpi="300"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs22131" />
|
||||
<sodipodi:namedview
|
||||
id="namedview22129"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#111111"
|
||||
borderopacity="1"
|
||||
inkscape:showpageshadow="0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pagecheckerboard="1"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
showgrid="false"
|
||||
inkscape:zoom="0.6685671"
|
||||
inkscape:cx="299.8951"
|
||||
inkscape:cy="80.021886"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="991"
|
||||
inkscape:window-x="-9"
|
||||
inkscape:window-y="-9"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg22127" />
|
||||
<!--! Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
|
||||
<path
|
||||
id="path2161-6"
|
||||
style="color:#000000;fill:#000000;stroke-width:1;-inkscape-stroke:none;paint-order:stroke fill markers"
|
||||
d="M 208,0 C 147.0078,0 94.429433,14.25071 60.367188,26.66992 23.520854,39.96036 0,76.16076 0,112.95896 v 317.8321 c 0,59.8499 56.949847,92.6546 107.47266,76.6035 l -0.1543,0.049 c 26.46715,-8.335 74.84649,-18.3965 100.68164,-18.3965 17.25807,0 61.31688,10.6183 85.14453,18.8438 l 0.0508,0.018 0.0527,0.018 c 19.82627,6.5858 40.84117,4.9222 58.99804,-3.0762 18.04267,7.8799 38.84257,9.6126 58.33594,3.1328 l 0.13672,-0.045 0.13672,-0.047 c 23.88445,-8.0588 67.88646,-18.8437 85.14453,-18.8437 25.83515,0 74.22549,10.0266 100.68164,18.3964 l 0.1543,0.049 0.15625,0.049 C 647.13371,523.05316 704,490.64216 704,430.79226 v -317.8321 c 0,-36.8274 -23.49583,-72.8235 -60.00977,-86.25583 l -0.16015,-0.0606 -0.16211,-0.0566 C 609.79193,14.33005 557.11269,0.0012 496,0.0012 434.5671,0.0012 387.12553,14.01354 352,34.94261 316.87446,14.01344 269.4331,0.0012 208,0.0012 Z m 0,32.00977 c 58.3999,0 103.40004,18.89469 123,33.63279 3.3,2.4564 5,6.4246 5,10.3926 v 356.5508 c 0,10.7702 -11.70041,18.2326 -22.40039,14.6426 -26.59996,-8.9751 -71.69966,-22.2012 -105.59961,-22.2012 -38.49993,0 -88.40045,11.4317 -119.900391,21.3516 C 76.799621,449.96896 64,442.03166 64,430.78906 V 80.94726 C 64,69.51586 70.799631,58.93546 82.099609,54.87306 110.29956,44.57516 157.50009,32.00977 208,32.00977 Z m 288,0 c 50.49991,0 97.70044,12.56619 125.90039,22.76949 C 633.20037,58.93616 640,69.51586 640,80.94726 v 349.8418 c 0,11.2426 -12.79963,19.0854 -24.09961,15.5899 -31.49995,-9.9199 -81.40046,-21.3516 -119.90039,-21.3516 -33.89995,0 -78.99966,13.2261 -105.59961,22.2012 C 379.60041,450.81856 368,443.35616 368,432.58596 V 76.03516 c 0,-3.968 1.60001,-7.9362 5,-10.3926 19.59997,-14.7381 64.6001,-33.63279 123,-33.63279 z M 335.52734,45.75386 c -0.1289,0.093 -0.23137,0.2032 -0.35937,0.2969 -0.198,0.1477 -0.428,0.2796 -0.625,0.4278 z m 33.67969,0.5372 0.24805,0.1875 c -0.0427,-0.033 -0.0937,-0.061 -0.13672,-0.094 -0.0393,-0.03 -0.0713,-0.064 -0.11133,-0.094 z" />
|
||||
<path
|
||||
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:64;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 417.64553,213.53304 c 88.71546,-18.9285 95.50522,-18.6158 172.79707,0.054"
|
||||
id="path2371-8"
|
||||
sodipodi:nodetypes="cc" />
|
||||
<path
|
||||
id="path2315"
|
||||
style="stroke-width:67.6532;stroke-linejoin:bevel;paint-order:stroke markers fill;stop-color:#000000"
|
||||
inkscape:transform-center-x="-3.4164388e-06"
|
||||
inkscape:transform-center-y="-8.443352"
|
||||
d="m 505.27489,52.89544 25.98603,52.6535 58.10652,8.4434 -42.04628,40.985 9.92578,57.8717 -51.97205,-27.3234 -51.97204,27.3234 9.92578,-57.8717 -42.04627,-40.985 58.10651,-8.4434 z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.1 KiB |
@@ -31,9 +31,27 @@
|
||||
.mask-corner {
|
||||
content: url('../icons/mask-corner.svg');
|
||||
}
|
||||
.fa-file-c {
|
||||
content: url('../icons/fa-file-c.svg');
|
||||
.mask-center {
|
||||
content: url('../icons/mask-center.svg');
|
||||
}
|
||||
.book-front-cover {
|
||||
content: url('../icons/book-front-cover.svg');
|
||||
}
|
||||
.book-back-cover {
|
||||
content: url('../icons/book-back-cover.svg');
|
||||
}
|
||||
.book-inside-cover {
|
||||
content: url('../icons/book-inside-cover.svg');
|
||||
}
|
||||
.book-part-cover {
|
||||
content: url('../icons/book-part-cover.svg');
|
||||
}
|
||||
.davek {
|
||||
content: url('../icons/Davek.svg');
|
||||
}
|
||||
.rellanic {
|
||||
content: url('../icons/Rellanic.svg');
|
||||
}
|
||||
.iokharic {
|
||||
content: url('../icons/Iokharic.svg');
|
||||
}
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 24.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 610.4 816.5" style="enable-background:new 0 0 610.4 816.5;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FFFFFF;}
|
||||
.st1{fill:#FFFFFF;stroke:#FFFFFF;stroke-width:20;stroke-miterlimit:10;}
|
||||
</style>
|
||||
<title>fa-file-c</title>
|
||||
<g id="Layer_2_1_">
|
||||
<g id="Layer_1-2">
|
||||
<g id="page">
|
||||
<path id="page-2" d="M610.3,468.3c0,77.3,0.2,154.5,0,231.8s-39.8,116.5-116.8,116.4c-127.6,0-255.1,0-382.7,0
|
||||
c-68.1,0-110.5-41.7-110.6-109.8c-0.2-197.7-0.2-395.5,0-593.2c0-68.4,43.2-110.9,112.1-111c90-0.1,180,0.2,270-0.2
|
||||
c12.8,0,21.5,0.6,32.9,4c17.1,5,152.7,150.7,190.7,188.8c-0.7,18-6,5.7,1.4,35.1c0,6.8,3.1,11.2,3.1,18.1
|
||||
C610.2,320.8,610.3,395.7,610.3,468.3z"/>
|
||||
<path id="white_corner" class="st0" d="M364.1,0v200c0,9.3,1.7,25.6,13.1,36.8c12,11.7,28.8,12.1,37.5,12.2
|
||||
c119.8,1.3,195.6,0.4,195.6,0.4l0,0l-0.3-54.3l-197,1l3-192L364.1,0z"/>
|
||||
</g>
|
||||
<path class="st1" d="M317.7,719.8c-38.3,0-71-8.1-98.3-24.3c-27.2-16.2-48.1-39.2-62.7-69C142.3,596.8,135,561.2,135,520
|
||||
c0-30.9,4.1-58.6,12.4-83.1c8.3-24.5,20.2-45.3,35.9-62.4c15.6-17.1,34.9-30.4,57.7-39.8s48.4-14.1,76.7-14.1
|
||||
c22.1-0.1,44,3.1,65.1,9.7c20.6,6.4,38.4,15.9,53.5,28.4c4.8,3.7,8,7.8,9.7,12.4c1.6,4.2,1.8,8.9,0.6,13.2
|
||||
c-1.2,4.1-3.5,7.7-6.6,10.5c-3.1,2.8-7.2,4.2-11.3,4.1c-4.4,0-9.4-1.8-14.9-5.5c-13-10.5-27.7-18.6-43.6-23.7
|
||||
c-16.6-5.3-33.9-7.9-51.3-7.7c-29.1,0-53.7,6.2-74,18.5s-35.5,30.3-45.8,53.8c-10.3,23.6-15.4,52.1-15.4,85.5s5.1,62.1,15.4,85.9
|
||||
c10.3,23.7,25.6,41.8,45.8,54.1c20.2,12.3,44.9,18.5,74,18.5c17.4,0.1,34.8-2.6,51.3-8c16.2-5.3,31.3-13.5,44.7-24
|
||||
c5.5-3.7,10.5-5.4,14.9-5.3c4,0.1,7.9,1.5,11,4.1c3,2.7,5.2,6.1,6.4,9.9c1.3,4.1,1.3,8.6,0,12.7c-1.3,4.4-4.1,8.3-8.6,11.6
|
||||
c-15.5,13.3-33.6,23.3-54.4,30.1C362.7,716.6,340.3,720,317.7,719.8z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.0 KiB |
63
client/icons/mask-center.svg
Normal file
@@ -0,0 +1,63 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
viewBox="0 0 448 512"
|
||||
version="1.1"
|
||||
id="svg135"
|
||||
sodipodi:docname="mask-center.svg"
|
||||
width="448"
|
||||
height="512"
|
||||
xml:space="preserve"
|
||||
inkscape:version="1.2.2 (732a01da63, 2022-12-09)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||
id="defs139"><pattern
|
||||
inkscape:collect="always"
|
||||
xlink:href="#Strips1_1"
|
||||
id="pattern3077"
|
||||
patternTransform="matrix(23.13193,-23.131931,19.25517,19.25517,18.091544,-20.306833)" /><pattern
|
||||
inkscape:collect="always"
|
||||
patternUnits="userSpaceOnUse"
|
||||
width="2"
|
||||
height="1"
|
||||
patternTransform="translate(0,0) scale(10,10)"
|
||||
id="Strips1_1"
|
||||
inkscape:stockid="Stripes 1:1"><rect
|
||||
style="fill:black;stroke:none"
|
||||
x="0"
|
||||
y="-0.5"
|
||||
width="1"
|
||||
height="2"
|
||||
id="rect2097" /></pattern></defs><sodipodi:namedview
|
||||
id="namedview137"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
showgrid="false"
|
||||
showguides="false"
|
||||
inkscape:zoom="0.67711183"
|
||||
inkscape:cx="31.75251"
|
||||
inkscape:cy="260.66595"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="991"
|
||||
inkscape:window-x="-9"
|
||||
inkscape:window-y="-9"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg135" /><!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path
|
||||
id="rect12201"
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:30;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke;stop-color:#000000"
|
||||
d="M 48,-5.2e-6 C 21.40803,-5.2e-6 1.98e-5,21.408025 1.98e-5,47.999995 V 464 C 1.98e-5,490.59197 21.40803,512 48,512 h 352 c 26.59198,0 48,-21.40803 48,-48 V 47.999995 C 448,21.408025 426.59198,-5.2e-6 400,-5.2e-6 Z M 64,63.999995 H 384 V 448 H 64 Z" /><rect
|
||||
style="fill:url(#pattern3077);fill-opacity:1;stroke:#000000;stroke-width:48;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke;stop-color:#000000"
|
||||
id="rect12206"
|
||||
width="176"
|
||||
height="240"
|
||||
x="136.00002"
|
||||
y="136"
|
||||
rx="48"
|
||||
ry="48" /></svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
@@ -11,6 +11,7 @@ const template = async function(name, title='', props = {}){
|
||||
return `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, height=device-height, interactive-widget=resizes-visual" />
|
||||
<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=${`/${name}/bundle.css`} rel='stylesheet' />
|
||||
|
||||
25
faq.md
@@ -62,16 +62,13 @@ pre {
|
||||
```
|
||||
|
||||
# FAQ
|
||||
{{wide Updated Oct. 11, 2021}}
|
||||
{{wide Updated Apr. 15, 2023}}
|
||||
|
||||
|
||||
### 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)
|
||||
|
||||
### 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?
|
||||
|
||||
@@ -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.
|
||||
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.
|
||||
|
||||
The Homebrewery paper size and your print paper size do not match.
|
||||
|
||||
43
install/README.WINDOWS.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# Windows Installation Instructions
|
||||
|
||||
## Before Installing
|
||||
|
||||
These instructions assume that you are installing to a completely new, fresh Windows 10 installation. As such, some steps may not be necessary if you are installing to an existing Windows 10 instance.
|
||||
|
||||
## Installation instructions
|
||||
|
||||
1. Download the installation script from https://raw.githubusercontent.com/naturalcrit/homebrewery/master/install/windows/install.ps1.
|
||||
|
||||
2. Run Powershell as an Administrator.
|
||||
a. Click the Start menu or press the Windows key.
|
||||
b. Type `powershell` into the Search box.
|
||||
c. Right click on the Powershell app and select "Run As Administrator".
|
||||
d. Click YES in the prompt that appears.
|
||||
|
||||
3. Change the script execution policy.
|
||||
a. Run the Powershell command `Set-ExecutionPolicy Bypass -Scope Process`.
|
||||
b. Allow the change to be made - press Y at the prompt that appears.
|
||||
|
||||
4. Run the installation script.
|
||||
a. Navigate to the location of the script, e.g. `cd C:\Users\ExampleUser\Downloads`.
|
||||
b. Start the script - `.\install.ps1`
|
||||
|
||||
5. Once the script has completed, it will start the Homebrewery server. This will normally cause a Network Access prompt for NodeJS - if this appears, click "Allow".
|
||||
|
||||
**NOTE:** At this time, the script **ONLY** installs HomeBrewery. It does **NOT** install the NaturalCrit login system, as that is currently a completely separate project.
|
||||
|
||||
---
|
||||
|
||||
### Testing
|
||||
|
||||
These installation instructions have been tested on the following Ubuntu releases:
|
||||
|
||||
- *Windows 10 Home - OS Build 19045.2546*
|
||||
|
||||
## Final Notes
|
||||
|
||||
While this installation process works successfully at the time of writing (January 23, 2023), it relies on all of the Node.JS packages used in the HomeBrewery project retaining their cross-platform capabilities to continue to function. This is one of the inherent advantages of Node.JS, but it is by no means guaranteed and as such, functionality or even installation may fail without warning at some point in the future.
|
||||
|
||||
Regards,
|
||||
G
|
||||
January 23, 2023
|
||||
@@ -13,7 +13,7 @@ npm install
|
||||
npm audit fix
|
||||
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
|
||||
|
||||
sysrc homebrewery_enable=YES
|
||||
|
||||
51
install/windows/install.ps1
Normal file
@@ -0,0 +1,51 @@
|
||||
Write-Host Homebrewery Install -BackgroundColor Black -ForegroundColor Yellow
|
||||
Write-Host =================== -BackgroundColor Black -ForegroundColor Yellow
|
||||
Write-Host Install Chocolatey -BackgroundColor Black -ForegroundColor Yellow
|
||||
Write-Host Instructions from https://chocolate.org/install -BackgroundColor Black -ForegroundColor Yellow
|
||||
|
||||
Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))
|
||||
|
||||
Write-Host Install Node JS v16.11.1 -BackgroundColor Black -ForegroundColor Yellow
|
||||
|
||||
choco install nodejs --version=16.11.1 -y
|
||||
|
||||
Write-Host Install MongoDB v 4.4.4 -BackgroundColor Black -ForegroundColor Yellow
|
||||
|
||||
choco install mongodb --version=4.4.4 -y
|
||||
|
||||
Write-Host Install GIT -BackgroundColor Black -ForegroundColor Yellow
|
||||
|
||||
choco install git -y
|
||||
|
||||
Write-Host Refresh Environment -BackgroundColor Black -ForegroundColor Yellow
|
||||
|
||||
Import-Module "$env:ChocolateyInstall\helpers\chocolateyProfile.psm1"
|
||||
Update-SessionEnvironment
|
||||
|
||||
Write-Host Create Homebrewery directory - C:\Homebrewery -BackgroundColor Black -ForegroundColor Yellow
|
||||
|
||||
mkdir C:\Hombrewery
|
||||
cd C:\Hombrewery
|
||||
|
||||
Write-Host Download Homebrewery project files -BackgroundColor Black -ForegroundColor Yellow
|
||||
|
||||
git clone https://github.com/naturalcrit/homebrewery.git
|
||||
|
||||
Write-Host Install Homebrewery files -BackgroundColor Black -ForegroundColor Yellow
|
||||
|
||||
cd homebrewery
|
||||
|
||||
npm install
|
||||
npm audit fix
|
||||
|
||||
Write-Host Set install type to 'local' -BackgroundColor Black -ForegroundColor Yellow
|
||||
|
||||
[System.Environment]::SetEnvironmentVariable('NODE_ENV', 'local')
|
||||
|
||||
Write-Host INSTALL COMPLETE -BackgroundColor Black -ForegroundColor Yellow
|
||||
Write-Host To start Homebrewery in the future, open a terminal in the Homebrewery directory and run npm start -BackgroundColor Black -ForegroundColor Yellow
|
||||
Write-Host ================================================================================================== -BackgroundColor Black -ForegroundColor Yellow
|
||||
|
||||
Write-Host Start Homebrewery -BackgroundColor Black -ForegroundColor Yellow
|
||||
|
||||
npm start
|
||||
22041
package-lock.json
generated
99
package.json
@@ -1,9 +1,10 @@
|
||||
{
|
||||
"name": "homebrewery",
|
||||
"description": "Create authentic looking D&D homebrews using only markdown",
|
||||
"version": "3.7.0",
|
||||
"version": "3.10.0",
|
||||
"engines": {
|
||||
"node": "16.11.x"
|
||||
"npm": "^10.2.x",
|
||||
"node": ">=20.8.x"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -12,23 +13,27 @@
|
||||
"scripts": {
|
||||
"dev": "node scripts/dev.js",
|
||||
"quick": "node scripts/quick.js",
|
||||
"build": "node scripts/buildHomebrew.js",
|
||||
"buildall": "node scripts/buildHomebrew.js && node scripts/buildAdmin.js",
|
||||
"build": "node scripts/buildHomebrew.js && node scripts/buildAdmin.js",
|
||||
"builddev": "node scripts/buildHomebrew.js --dev",
|
||||
"lint": "eslint --fix **/*.{js,jsx}",
|
||||
"lint:dry": "eslint **/*.{js,jsx}",
|
||||
"stylelint": "stylelint --fix **/*.{less}",
|
||||
"stylelint:dry": "stylelint **/*.less",
|
||||
"circleci": "npm test && eslint **/*.{js,jsx} --max-warnings=0",
|
||||
"verify": "npm run lint && npm test",
|
||||
"test": "jest",
|
||||
"test": "jest --runInBand",
|
||||
"test:api-unit": "jest server/*.spec.js --verbose",
|
||||
"test:coverage": "jest --coverage --silent",
|
||||
"test:coverage": "jest --coverage --silent --runInBand",
|
||||
"test:dev": "jest --verbose --watch",
|
||||
"test:basic": "jest tests/markdown/basic.test.js --verbose",
|
||||
"test:mustache-span": "jest tests/markdown/mustache-span.test.js --verbose",
|
||||
"test:mustache-syntax": "jest '.*(mustache-syntax).*' --verbose --noStackTrace",
|
||||
"test:mustache-syntax:inline": "jest '.*(mustache-syntax).*' -t '^Inline:.*' --verbose --noStackTrace",
|
||||
"test:mustache-syntax:block": "jest '.*(mustache-syntax).*' -t '^Block:.*' --verbose --noStackTrace",
|
||||
"test:mustache-syntax:injection": "jest '.*(mustache-syntax).*' -t '^Injection:.*' --verbose --noStackTrace",
|
||||
"test:route": "jest tests/routes/static-pages.test.js --verbose",
|
||||
"phb": "node scripts/phb.js",
|
||||
"prod": "set NODE_ENV=production && npm run build",
|
||||
"postinstall": "npm run buildall",
|
||||
"postinstall": "npm run build",
|
||||
"start": "node server.js"
|
||||
},
|
||||
"author": "stolksdorf",
|
||||
@@ -37,26 +42,32 @@
|
||||
"build/*"
|
||||
],
|
||||
"jest": {
|
||||
"testTimeout": 15000,
|
||||
"testTimeout": 30000,
|
||||
"modulePaths": [
|
||||
"node_modules",
|
||||
"shared",
|
||||
"server"
|
||||
],
|
||||
"coverageThreshold" : {
|
||||
"global" : {
|
||||
"statements" : 25,
|
||||
"branches" : 10,
|
||||
"functions" : 22,
|
||||
"lines" : 25
|
||||
"coveragePathIgnorePatterns": [
|
||||
"build/*"
|
||||
],
|
||||
"coverageThreshold": {
|
||||
"global": {
|
||||
"statements": 25,
|
||||
"branches": 10,
|
||||
"functions": 22,
|
||||
"lines": 25
|
||||
},
|
||||
"server/homebrew.api.js" : {
|
||||
"statements" : 65,
|
||||
"branches" : 50,
|
||||
"functions" : 60,
|
||||
"lines" : 70
|
||||
"server/homebrew.api.js": {
|
||||
"statements": 65,
|
||||
"branches": 50,
|
||||
"functions": 60,
|
||||
"lines": 70
|
||||
}
|
||||
}
|
||||
},
|
||||
"setupFilesAfterEnv": [
|
||||
"jest-expect-message"
|
||||
]
|
||||
},
|
||||
"babel": {
|
||||
"presets": [
|
||||
@@ -68,45 +79,53 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.21.0",
|
||||
"@babel/plugin-transform-runtime": "^7.21.0",
|
||||
"@babel/preset-env": "^7.19.4",
|
||||
"@babel/preset-react": "^7.18.6",
|
||||
"@babel/core": "^7.23.2",
|
||||
"@babel/plugin-transform-runtime": "^7.23.2",
|
||||
"@babel/preset-env": "^7.23.2",
|
||||
"@babel/preset-react": "^7.22.15",
|
||||
"@googleapis/drive": "^8.4.0",
|
||||
"body-parser": "^1.20.2",
|
||||
"classnames": "^2.3.2",
|
||||
"codemirror": "^5.65.6",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"create-react-class": "^15.7.0",
|
||||
"dedent-tabs": "^0.10.2",
|
||||
"dedent-tabs": "^0.10.3",
|
||||
"express": "^4.18.2",
|
||||
"express-async-handler": "^1.2.0",
|
||||
"express-static-gzip": "2.1.7",
|
||||
"fs-extra": "11.1.0",
|
||||
"googleapis": "111.0.0",
|
||||
"fs-extra": "11.1.1",
|
||||
"js-yaml": "^4.1.0",
|
||||
"jwt-simple": "^0.5.6",
|
||||
"less": "^3.13.1",
|
||||
"lodash": "^4.17.21",
|
||||
"marked": "4.2.12",
|
||||
"marked-extended-tables": "^1.0.5",
|
||||
"marked": "5.1.1",
|
||||
"marked-extended-tables": "^1.0.7",
|
||||
"marked-gfm-heading-id": "^3.1.0",
|
||||
"marked-smartypants-lite": "^1.0.1",
|
||||
"markedLegacy": "npm:marked@^0.3.19",
|
||||
"moment": "^2.29.4",
|
||||
"mongoose": "^6.9.2",
|
||||
"mongoose": "^7.6.1",
|
||||
"nanoid": "3.3.4",
|
||||
"nconf": "^0.12.0",
|
||||
"npm": "^8.10.0",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-frame-component": "4.1.3",
|
||||
"react-router-dom": "6.8.2",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-frame-component": "^4.1.3",
|
||||
"react-router-dom": "6.16.0",
|
||||
"sanitize-filename": "1.6.3",
|
||||
"superagent": "^6.1.0",
|
||||
"superagent": "^8.1.2",
|
||||
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^8.35.0",
|
||||
"eslint-plugin-react": "^7.32.2",
|
||||
"jest": "^29.4.3",
|
||||
"eslint": "^8.51.0",
|
||||
"eslint-plugin-jest": "^27.4.2",
|
||||
"eslint-plugin-react": "^7.33.2",
|
||||
"jest": "^29.7.0",
|
||||
"jest-expect-message": "^1.1.3",
|
||||
"postcss-less": "^6.0.0",
|
||||
"stylelint": "^15.10.3",
|
||||
"stylelint-config-recess-order": "^4.3.0",
|
||||
"stylelint-config-recommended": "^13.0.0",
|
||||
"stylelint-stylistic": "^0.4.3",
|
||||
"supertest": "^6.3.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,6 +99,24 @@ fs.emptyDirSync('./build');
|
||||
await fs.copy('./themes/assets', './build/assets');
|
||||
await fs.copy('./client/icons', './build/icons');
|
||||
|
||||
//v==---------------------------MOVE CM EDITOR THEMES -----------------------------==v//
|
||||
|
||||
editorThemeFiles = fs.readdirSync('./node_modules/codemirror/theme');
|
||||
|
||||
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('./node_modules/codemirror/theme', './build/homebrew/cm-themes');
|
||||
await fs.copy('./themes/codeMirror', './build/homebrew/codeMirror');
|
||||
|
||||
//v==----------------------------- BUNDLE PACKAGES --------------------------------==v//
|
||||
|
||||
const bundles = await pack('./client/homebrew/homebrew.jsx', {
|
||||
@@ -135,10 +153,12 @@ fs.emptyDirSync('./build');
|
||||
|
||||
})().catch(console.error);
|
||||
|
||||
//In development set up a watch server and livereload
|
||||
//In development, set up LiveReload (refreshes browser), and Nodemon (restarts server)
|
||||
if(isDev){
|
||||
livereload('./build');
|
||||
watchFile('./server.js', {
|
||||
watch : ['./client', './server', './themes'] // Watch additional folders if you want
|
||||
livereload('./build'); // Install the Chrome extension LiveReload to automatically refresh the browser
|
||||
watchFile('./server.js', { // Restart server when change detected to this file or any nested directory from here
|
||||
ignore : ['./build', './client', './themes'], // Ignore folders that are not running server code / avoids unneeded restarts
|
||||
ext : 'js json' // Extensions to watch (only .js/.json by default)
|
||||
//watch : ['./server', './themes'], // Watch additional folders if needed
|
||||
});
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ const mw = {
|
||||
.status(401)
|
||||
.send('Authorization Required');
|
||||
}
|
||||
const [username, password] = new Buffer(req.get('authorization').split(' ').pop(), 'base64')
|
||||
const [username, password] = Buffer.from(req.get('authorization').split(' ').pop(), 'base64')
|
||||
.toString('ascii')
|
||||
.split(':');
|
||||
if(process.env.ADMIN_USER === username && process.env.ADMIN_PASS === password){
|
||||
|
||||
@@ -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
|
||||
process.chdir(`${__dirname}/..`);
|
||||
|
||||
@@ -23,7 +23,7 @@ const splitTextStyleAndMetadata = (brew)=>{
|
||||
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']));
|
||||
Object.assign(brew, _.pick(metadata, ['title', 'description', 'tags', 'systems', 'renderer', 'theme', 'lang']));
|
||||
brew.text = brew.text.slice(index + 5);
|
||||
}
|
||||
if(brew.text.startsWith('```css')) {
|
||||
@@ -43,8 +43,7 @@ const sanitizeBrew = (brew, accessType)=>{
|
||||
};
|
||||
|
||||
app.use('/', serveCompressedStaticAssets(`build`));
|
||||
|
||||
//app.use(express.static(`${__dirname}/build`));
|
||||
app.use(require('./middleware/content-negotiation.js'));
|
||||
app.use(require('body-parser').json({ limit: '25mb' }));
|
||||
app.use(require('cookie-parser')());
|
||||
app.use(require('./forcessl.mw.js'));
|
||||
@@ -225,6 +224,7 @@ app.get('/user/:username', async (req, res, next)=>{
|
||||
'pageCount',
|
||||
'description',
|
||||
'authors',
|
||||
'lang',
|
||||
'published',
|
||||
'views',
|
||||
'shareId',
|
||||
@@ -257,6 +257,7 @@ app.get('/user/:username', async (req, res, next)=>{
|
||||
brew.pageCount = googleBrews[match].pageCount;
|
||||
brew.renderer = googleBrews[match].renderer;
|
||||
brew.version = googleBrews[match].version;
|
||||
brew.webViewLink = googleBrews[match].webViewLink;
|
||||
googleBrews.splice(match, 1);
|
||||
}
|
||||
}
|
||||
@@ -267,6 +268,9 @@ app.get('/user/:username', async (req, res, next)=>{
|
||||
}
|
||||
|
||||
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');
|
||||
});
|
||||
|
||||
@@ -324,8 +328,8 @@ app.get('/share/:id', asyncHandler(getBrew('share')), asyncHandler(async (req, r
|
||||
};
|
||||
|
||||
if(req.params.id.length > 12 && !brew._id) {
|
||||
const googleId = req.params.id.slice(0, -12);
|
||||
const shareId = req.params.id.slice(-12);
|
||||
const googleId = brew.googleId;
|
||||
const shareId = brew.shareId;
|
||||
await GoogleActions.increaseView(googleId, shareId, 'share', brew)
|
||||
.catch((err)=>{next(err);});
|
||||
} else {
|
||||
@@ -397,7 +401,6 @@ app.get('/account', asyncHandler(async (req, res, next)=>{
|
||||
return next();
|
||||
}));
|
||||
|
||||
|
||||
const nodeEnv = config.get('node_env');
|
||||
const isLocalEnvironment = config.get('local_environments').includes(nodeEnv);
|
||||
// Local only
|
||||
@@ -414,8 +417,7 @@ if(isLocalEnvironment){
|
||||
|
||||
//Render the page
|
||||
const templateFn = require('./../client/template.js');
|
||||
app.use(asyncHandler(async (req, res, next)=>{
|
||||
|
||||
const renderPage = async (req, res)=>{
|
||||
// Create configuration object
|
||||
const configuration = {
|
||||
local : isLocalEnvironment,
|
||||
@@ -424,7 +426,7 @@ app.use(asyncHandler(async (req, res, next)=>{
|
||||
};
|
||||
const props = {
|
||||
version : require('./../package.json').version,
|
||||
url : req.originalUrl,
|
||||
url : req.customUrl || req.originalUrl,
|
||||
brew : req.brew,
|
||||
brews : req.brews,
|
||||
googleBrews : req.googleBrews,
|
||||
@@ -438,15 +440,20 @@ app.use(asyncHandler(async (req, res, next)=>{
|
||||
const page = await templateFn('homebrew', title, props)
|
||||
.catch((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;
|
||||
res.send(page);
|
||||
}));
|
||||
|
||||
//v=====----- Error-Handling Middleware -----=====v//
|
||||
//Format Errors so all fields will be sent
|
||||
const replaceErrors = (key, value)=>{
|
||||
//Format Errors as plain objects so all fields will appear in the string sent
|
||||
const formatErrors = (key, value)=>{
|
||||
if(value instanceof Error) {
|
||||
const error = {};
|
||||
Object.getOwnPropertyNames(value).forEach(function (key) {
|
||||
@@ -458,13 +465,30 @@ const replaceErrors = (key, value)=>{
|
||||
};
|
||||
|
||||
const getPureError = (error)=>{
|
||||
return JSON.parse(JSON.stringify(error, replaceErrors));
|
||||
return JSON.parse(JSON.stringify(error, formatErrors));
|
||||
};
|
||||
|
||||
app.use((err, req, res, next)=>{
|
||||
const status = err.status || 500;
|
||||
app.use(async (err, req, res, next)=>{
|
||||
const status = err.status || err.code || 500;
|
||||
console.error(err);
|
||||
res.status(status).send(getPureError(err));
|
||||
|
||||
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)=>{
|
||||
|
||||
@@ -15,6 +15,7 @@ const DEFAULT_BREW = {
|
||||
authors : [],
|
||||
tags : [],
|
||||
systems : [],
|
||||
lang : 'en',
|
||||
thumbnail : '',
|
||||
views : 0,
|
||||
published : false,
|
||||
|
||||
@@ -27,8 +27,8 @@ const disconnect = async ()=>{
|
||||
};
|
||||
|
||||
const connect = async (config)=>{
|
||||
return await Mongoose.connect(getMongoDBURL(config),
|
||||
{ retryWrites: false }, handleConnectionError);
|
||||
return await Mongoose.connect(getMongoDBURL(config), { retryWrites: false })
|
||||
.catch((error)=>handleConnectionError(error));
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable max-lines */
|
||||
const _ = require('lodash');
|
||||
const { google } = require('googleapis');
|
||||
const googleDrive = require('@googleapis/drive');
|
||||
const { nanoid } = require('nanoid');
|
||||
const token = require('./token.js');
|
||||
const config = require('./config.js');
|
||||
@@ -14,7 +14,7 @@ if(!config.get('service_account')){
|
||||
config.get('service_account');
|
||||
|
||||
try {
|
||||
serviceAuth = google.auth.fromJSON(keys);
|
||||
serviceAuth = googleDrive.auth.fromJSON(keys);
|
||||
serviceAuth.scopes = ['https://www.googleapis.com/auth/drive'];
|
||||
} catch (err) {
|
||||
console.warn(err);
|
||||
@@ -22,7 +22,7 @@ if(!config.get('service_account')){
|
||||
}
|
||||
}
|
||||
|
||||
google.options({ auth: serviceAuth || config.get('google_api_key') });
|
||||
const defaultAuth = serviceAuth || config.get('google_api_key');
|
||||
|
||||
const GoogleActions = {
|
||||
|
||||
@@ -33,7 +33,7 @@ const GoogleActions = {
|
||||
throw (err);
|
||||
}
|
||||
|
||||
const oAuth2Client = new google.auth.OAuth2(
|
||||
const oAuth2Client = new googleDrive.auth.OAuth2(
|
||||
config.get('google_client_id'),
|
||||
config.get('google_client_secret'),
|
||||
'/auth/google/redirect'
|
||||
@@ -60,7 +60,7 @@ const GoogleActions = {
|
||||
},
|
||||
|
||||
getGoogleFolder : async (auth)=>{
|
||||
const drive = google.drive({ version: 'v3', auth });
|
||||
const drive = googleDrive.drive({ version: 'v3', auth });
|
||||
|
||||
fileMetadata = {
|
||||
'name' : 'Homebrewery',
|
||||
@@ -97,25 +97,33 @@ const GoogleActions = {
|
||||
},
|
||||
|
||||
listGoogleBrews : async (auth)=>{
|
||||
const drive = google.drive({ version: 'v3', auth });
|
||||
const drive = googleDrive.drive({ version: 'v3', auth });
|
||||
|
||||
const obj = await drive.files.list({
|
||||
pageSize : 1000,
|
||||
fields : 'nextPageToken, files(id, name, description, createdTime, modifiedTime, properties)',
|
||||
q : 'mimeType != \'application/vnd.google-apps.folder\' and trashed = false'
|
||||
})
|
||||
.catch((err)=>{
|
||||
console.log(`Error Listing Google Brews`);
|
||||
console.error(err);
|
||||
throw (err);
|
||||
//TODO: Should break out here, but continues on for some reason.
|
||||
});
|
||||
const fileList = [];
|
||||
let NextPageToken = '';
|
||||
|
||||
if(!obj.data.files.length) {
|
||||
do {
|
||||
const obj = await drive.files.list({
|
||||
pageSize : 1000,
|
||||
pageToken : NextPageToken || '',
|
||||
fields : 'nextPageToken, files(id, name, description, createdTime, modifiedTime, properties, webViewLink)',
|
||||
q : 'mimeType != \'application/vnd.google-apps.folder\' and trashed = false'
|
||||
})
|
||||
.catch((err)=>{
|
||||
console.log(`Error Listing Google Brews`);
|
||||
console.error(err);
|
||||
throw (err);
|
||||
//TODO: Should break out here, but continues on for some reason.
|
||||
});
|
||||
fileList.push(...obj.data.files);
|
||||
NextPageToken = obj.data.nextPageToken;
|
||||
} while (NextPageToken);
|
||||
|
||||
if(!fileList.length) {
|
||||
console.log('No files found.');
|
||||
}
|
||||
|
||||
const brews = obj.data.files.map((file)=>{
|
||||
const brews = fileList.map((file)=>{
|
||||
return {
|
||||
text : '',
|
||||
shareId : file.properties.shareId,
|
||||
@@ -129,14 +137,17 @@ const GoogleActions = {
|
||||
description : file.description,
|
||||
views : parseInt(file.properties.views),
|
||||
published : file.properties.published ? file.properties.published == 'true' : false,
|
||||
systems : []
|
||||
systems : [],
|
||||
lang : file.properties.lang,
|
||||
thumbnail : file.properties.thumbnail,
|
||||
webViewLink : file.webViewLink
|
||||
};
|
||||
});
|
||||
return brews;
|
||||
},
|
||||
|
||||
updateGoogleBrew : async (brew)=>{
|
||||
const drive = google.drive({ version: 'v3' });
|
||||
const drive = googleDrive.drive({ version: 'v3', auth: defaultAuth });
|
||||
|
||||
await drive.files.update({
|
||||
fileId : brew.googleId,
|
||||
@@ -149,7 +160,8 @@ const GoogleActions = {
|
||||
editId : brew.editId || nanoid(12),
|
||||
pageCount : brew.pageCount,
|
||||
renderer : brew.renderer || 'legacy',
|
||||
isStubbed : true
|
||||
isStubbed : true,
|
||||
lang : brew.lang || 'en'
|
||||
}
|
||||
},
|
||||
media : {
|
||||
@@ -167,7 +179,7 @@ const GoogleActions = {
|
||||
},
|
||||
|
||||
newGoogleBrew : async (auth, brew)=>{
|
||||
const drive = google.drive({ version: 'v3', auth });
|
||||
const drive = googleDrive.drive({ version: 'v3', auth });
|
||||
|
||||
const media = {
|
||||
mimeType : 'text/plain',
|
||||
@@ -187,7 +199,8 @@ const GoogleActions = {
|
||||
pageCount : brew.pageCount,
|
||||
renderer : brew.renderer || 'legacy',
|
||||
isStubbed : true,
|
||||
version : 1
|
||||
version : 1,
|
||||
lang : brew.lang || 'en'
|
||||
}
|
||||
};
|
||||
|
||||
@@ -218,7 +231,7 @@ const GoogleActions = {
|
||||
},
|
||||
|
||||
getGoogleBrew : async (id, accessId, accessType)=>{
|
||||
const drive = google.drive({ version: 'v3' });
|
||||
const drive = googleDrive.drive({ version: 'v3', auth: defaultAuth });
|
||||
|
||||
const obj = await drive.files.get({
|
||||
fileId : id,
|
||||
@@ -231,9 +244,9 @@ const GoogleActions = {
|
||||
|
||||
if(obj) {
|
||||
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){
|
||||
throw ('Share ID does not match');
|
||||
throw ({ message: 'Share ID does not match' });
|
||||
}
|
||||
|
||||
const file = await drive.files.get({
|
||||
@@ -255,6 +268,7 @@ const GoogleActions = {
|
||||
description : obj.data.description,
|
||||
systems : obj.data.properties.systems ? obj.data.properties.systems.split(',') : [],
|
||||
authors : [],
|
||||
lang : obj.data.properties.lang,
|
||||
published : obj.data.properties.published ? obj.data.properties.published == 'true' : false,
|
||||
trashed : obj.data.trashed,
|
||||
|
||||
@@ -274,7 +288,7 @@ const GoogleActions = {
|
||||
},
|
||||
|
||||
deleteGoogleBrew : async (auth, id, accessId)=>{
|
||||
const drive = google.drive({ version: 'v3', auth });
|
||||
const drive = googleDrive.drive({ version: 'v3', auth });
|
||||
|
||||
const obj = await drive.files.get({
|
||||
fileId : id,
|
||||
@@ -300,7 +314,7 @@ const GoogleActions = {
|
||||
},
|
||||
|
||||
increaseView : async (id, accessId, accessType, brew)=>{
|
||||
const drive = google.drive({ version: 'v3' });
|
||||
const drive = googleDrive.drive({ version: 'v3', auth: defaultAuth });
|
||||
|
||||
await drive.files.update({
|
||||
fileId : brew.googleId,
|
||||
|
||||
@@ -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(id.length > 12) {
|
||||
googleId = id.slice(0, -12);
|
||||
id = id.slice(-12);
|
||||
if(id.length >= (33 + 12)) { // googleId is minimum 33 chars (may increase)
|
||||
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 };
|
||||
},
|
||||
@@ -57,7 +62,14 @@ const api = {
|
||||
googleError = err;
|
||||
});
|
||||
// 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
|
||||
stub = stub ? _.assign({ ...api.excludeStubProps(stub), stubbed: true }, api.excludeGoogleProps(googleBrew)) : googleBrew;
|
||||
}
|
||||
@@ -65,14 +77,16 @@ const api = {
|
||||
const isAuthor = stub?.authors?.includes(req.account?.username);
|
||||
const isInvited = stub?.invitedAuthors?.includes(req.account?.username);
|
||||
if(accessType === 'edit' && (authorsExist && !(isAuthor || isInvited))) {
|
||||
throw `The current logged in user does not have editor access to this brew.
|
||||
|
||||
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.`;
|
||||
const accessError = { name: 'Access Error', status: 401 };
|
||||
if(req.account){
|
||||
throw { ...accessError, message: 'User is not an Author', HBErrorCode: '03', authors: stub.authors, brewTitle: stub.title };
|
||||
}
|
||||
throw { ...accessError, message: 'User is not logged in', HBErrorCode: '04', authors: stub.authors, brewTitle: stub.title };
|
||||
}
|
||||
|
||||
// If after all of that we still don't have a brew, throw an exception
|
||||
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
|
||||
@@ -139,6 +153,9 @@ If you believe you should have access to this brew, ask the file owner to invite
|
||||
brew.text = api.mergeBrewText(brew);
|
||||
|
||||
_.defaults(brew, DEFAULT_BREW);
|
||||
|
||||
brew.title = brew.title.trim();
|
||||
brew.description = brew.description.trim();
|
||||
},
|
||||
newGoogleBrew : async (account, brew, res)=>{
|
||||
const oAuth2Client = GoogleActions.authCheck(account, res);
|
||||
@@ -181,7 +198,7 @@ If you believe you should have access to this brew, ask the file owner to invite
|
||||
saved = await newHomebrew.save()
|
||||
.catch((err)=>{
|
||||
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;
|
||||
saved = saved.toObject();
|
||||
@@ -203,6 +220,8 @@ If you believe you should have access to this brew, ask the file owner to invite
|
||||
const { saveToGoogle, removeFromGoogle } = req.query;
|
||||
let afterSave = async ()=>true;
|
||||
|
||||
brew.title = brew.title.trim();
|
||||
brew.description = brew.description.trim() || '';
|
||||
brew.text = api.mergeBrewText(brew);
|
||||
|
||||
if(brew.googleId && removeFromGoogle) {
|
||||
@@ -283,10 +302,13 @@ If you believe you should have access to this brew, ask the file owner to invite
|
||||
try {
|
||||
await api.getBrew('edit')(req, res, ()=>{});
|
||||
} catch (err) {
|
||||
const { id, googleId } = api.getId(req);
|
||||
console.warn(`No google brew found for id ${googleId}, the stub with id ${id} will be deleted.`);
|
||||
await HomebrewModel.deleteOne({ editId: id });
|
||||
return next();
|
||||
// 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);
|
||||
console.warn(`No google brew found for id ${googleId}, the stub with id ${id} will be deleted.`);
|
||||
await HomebrewModel.deleteOne({ editId: id });
|
||||
return next();
|
||||
}
|
||||
}
|
||||
|
||||
let brew = req.brew;
|
||||
@@ -305,10 +327,10 @@ If you believe you should have access to this brew, ask the file owner to invite
|
||||
|
||||
if(brew.authors.length === 0) {
|
||||
// Delete brew if there are no authors left
|
||||
await brew.remove()
|
||||
await HomebrewModel.deleteOne({ _id: brew._id })
|
||||
.catch((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 {
|
||||
if(shouldDeleteGoogleBrew) {
|
||||
@@ -320,7 +342,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()
|
||||
await brew.save()
|
||||
.catch((err)=>{
|
||||
throw { status: 500, message: err };
|
||||
throw { name: 'BrewAuthorDelete Error', message: err, status: 500, HBErrorCode: '08', brewId: brew._id };
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ describe('Tests for api', ()=>{
|
||||
|
||||
let modelBrew;
|
||||
let saveFunc;
|
||||
let removeFunc;
|
||||
let markModifiedFunc;
|
||||
let saved;
|
||||
|
||||
@@ -20,18 +19,15 @@ describe('Tests for api', ()=>{
|
||||
saved = { ...this, _id: '1' };
|
||||
return saved;
|
||||
});
|
||||
removeFunc = jest.fn(async function() {});
|
||||
markModifiedFunc = jest.fn(()=>true);
|
||||
|
||||
modelBrew = (brew)=>({
|
||||
...brew,
|
||||
save : saveFunc,
|
||||
remove : removeFunc,
|
||||
markModified : markModifiedFunc,
|
||||
toObject : function() {
|
||||
delete this.save;
|
||||
delete this.toObject;
|
||||
delete this.remove;
|
||||
delete this.markModified;
|
||||
return this;
|
||||
}
|
||||
@@ -62,6 +58,7 @@ describe('Tests for api', ()=>{
|
||||
description : 'this is a description',
|
||||
tags : ['something', 'fun'],
|
||||
systems : ['D&D 5e'],
|
||||
lang : 'en',
|
||||
renderer : 'v3',
|
||||
theme : 'phb',
|
||||
published : true,
|
||||
@@ -114,21 +111,32 @@ describe('Tests for api', ()=>{
|
||||
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({
|
||||
params : {
|
||||
id : '123456789012abcdefghijkl'
|
||||
id : '123456789012345678901234567890123abcdefghijkl'
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
expect(googleId).toEqual('123456789012345678901234567890123');
|
||||
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', ()=>{
|
||||
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', ()=>{
|
||||
const getFn = api.getBrew('share');
|
||||
@@ -186,7 +194,7 @@ describe('Tests for api', ()=>{
|
||||
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 }));
|
||||
model.get = jest.fn(()=>toBrewPromise({ title: 'test brew', authors: ['a'] }));
|
||||
|
||||
@@ -200,9 +208,24 @@ describe('Tests for api', ()=>{
|
||||
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 ()=>{
|
||||
@@ -255,6 +278,7 @@ If you believe you should have access to this brew, ask the file owner to invite
|
||||
pageCount : 1,
|
||||
published : false,
|
||||
renderer : 'legacy',
|
||||
lang : 'en',
|
||||
shareId : undefined,
|
||||
systems : [],
|
||||
tags : [],
|
||||
@@ -448,6 +472,7 @@ brew`);
|
||||
pageCount : 1,
|
||||
published : false,
|
||||
renderer : 'V3',
|
||||
lang : 'en',
|
||||
shareId : expect.any(String),
|
||||
style : undefined,
|
||||
systems : [],
|
||||
@@ -506,6 +531,7 @@ brew`);
|
||||
pageCount : undefined,
|
||||
published : false,
|
||||
renderer : undefined,
|
||||
lang : 'en',
|
||||
shareId : expect.any(String),
|
||||
googleId : expect.any(String),
|
||||
style : undefined,
|
||||
@@ -545,7 +571,7 @@ brew`);
|
||||
|
||||
describe('deleteBrew', ()=>{
|
||||
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' }));
|
||||
model.deleteOne = jest.fn(async ()=>{});
|
||||
const next = jest.fn(()=>{});
|
||||
@@ -565,13 +591,14 @@ brew`);
|
||||
req.brew = brew;
|
||||
});
|
||||
model.findOne = jest.fn(async ()=>modelBrew(brew));
|
||||
model.deleteOne = jest.fn(async ()=>{});
|
||||
const req = {};
|
||||
|
||||
await api.deleteBrew(req, res);
|
||||
|
||||
expect(api.getBrew).toHaveBeenCalled();
|
||||
expect(model.findOne).toHaveBeenCalled();
|
||||
expect(removeFunc).toHaveBeenCalled();
|
||||
expect(model.deleteOne).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw on delete error', async ()=>{
|
||||
@@ -583,7 +610,7 @@ brew`);
|
||||
req.brew = brew;
|
||||
});
|
||||
model.findOne = jest.fn(async ()=>modelBrew(brew));
|
||||
removeFunc = jest.fn(async ()=>{ throw 'err'; });
|
||||
model.deleteOne = jest.fn(async ()=>{ throw 'err'; });
|
||||
const req = {};
|
||||
|
||||
let err;
|
||||
@@ -596,7 +623,7 @@ brew`);
|
||||
expect(err).not.toBeUndefined();
|
||||
expect(api.getBrew).toHaveBeenCalled();
|
||||
expect(model.findOne).toHaveBeenCalled();
|
||||
expect(removeFunc).toHaveBeenCalled();
|
||||
expect(model.deleteOne).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should delete when one author', async ()=>{
|
||||
@@ -608,13 +635,14 @@ brew`);
|
||||
req.brew = brew;
|
||||
});
|
||||
model.findOne = jest.fn(async ()=>modelBrew(brew));
|
||||
model.deleteOne = jest.fn(async ()=>{});
|
||||
const req = { account: { username: 'test' } };
|
||||
|
||||
await api.deleteBrew(req, res);
|
||||
|
||||
expect(api.getBrew).toHaveBeenCalled();
|
||||
expect(model.findOne).toHaveBeenCalled();
|
||||
expect(removeFunc).toHaveBeenCalled();
|
||||
expect(model.deleteOne).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should remove one author when multiple present', async ()=>{
|
||||
@@ -626,6 +654,7 @@ brew`);
|
||||
req.brew = brew;
|
||||
});
|
||||
model.findOne = jest.fn(async ()=>modelBrew(brew));
|
||||
model.deleteOne = jest.fn(async ()=>{});
|
||||
const req = { account: { username: 'test' } };
|
||||
|
||||
await api.deleteBrew(req, res);
|
||||
@@ -633,7 +662,7 @@ brew`);
|
||||
expect(api.getBrew).toHaveBeenCalled();
|
||||
expect(markModifiedFunc).toHaveBeenCalled();
|
||||
expect(model.findOne).toHaveBeenCalled();
|
||||
expect(removeFunc).not.toHaveBeenCalled();
|
||||
expect(model.deleteOne).not.toHaveBeenCalled();
|
||||
expect(saveFunc).toHaveBeenCalled();
|
||||
expect(saved.authors).toEqual(['test2']);
|
||||
});
|
||||
@@ -647,6 +676,7 @@ brew`);
|
||||
req.brew = brew;
|
||||
});
|
||||
model.findOne = jest.fn(async ()=>modelBrew(brew));
|
||||
model.deleteOne = jest.fn(async ()=>{});
|
||||
saveFunc = jest.fn(async ()=>{ throw 'err'; });
|
||||
const req = { account: { username: 'test' } };
|
||||
|
||||
@@ -660,7 +690,7 @@ brew`);
|
||||
expect(err).not.toBeUndefined();
|
||||
expect(api.getBrew).toHaveBeenCalled();
|
||||
expect(model.findOne).toHaveBeenCalled();
|
||||
expect(removeFunc).not.toHaveBeenCalled();
|
||||
expect(model.deleteOne).not.toHaveBeenCalled();
|
||||
expect(saveFunc).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -673,6 +703,7 @@ brew`);
|
||||
req.brew = brew;
|
||||
});
|
||||
model.findOne = jest.fn(async ()=>modelBrew(brew));
|
||||
model.deleteOne = jest.fn(async ()=>{});
|
||||
api.deleteGoogleBrew = jest.fn(async ()=>true);
|
||||
const req = { account: { username: 'test' } };
|
||||
|
||||
@@ -680,7 +711,7 @@ brew`);
|
||||
|
||||
expect(api.getBrew).toHaveBeenCalled();
|
||||
expect(model.findOne).toHaveBeenCalled();
|
||||
expect(removeFunc).toHaveBeenCalled();
|
||||
expect(model.deleteOne).toHaveBeenCalled();
|
||||
expect(api.deleteGoogleBrew).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -693,6 +724,7 @@ brew`);
|
||||
req.brew = brew;
|
||||
});
|
||||
model.findOne = jest.fn(async ()=>modelBrew(brew));
|
||||
model.deleteOne = jest.fn(async ()=>{});
|
||||
api.deleteGoogleBrew = jest.fn(async ()=>{
|
||||
throw 'err';
|
||||
});
|
||||
@@ -702,7 +734,7 @@ brew`);
|
||||
|
||||
expect(api.getBrew).toHaveBeenCalled();
|
||||
expect(model.findOne).toHaveBeenCalled();
|
||||
expect(removeFunc).toHaveBeenCalled();
|
||||
expect(model.deleteOne).toHaveBeenCalled();
|
||||
expect(api.deleteGoogleBrew).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -715,6 +747,7 @@ brew`);
|
||||
req.brew = brew;
|
||||
});
|
||||
model.findOne = jest.fn(async ()=>modelBrew(brew));
|
||||
model.deleteOne = jest.fn(async ()=>{});
|
||||
api.deleteGoogleBrew = jest.fn(async ()=>true);
|
||||
const req = { account: { username: 'test' } };
|
||||
|
||||
@@ -723,7 +756,7 @@ brew`);
|
||||
expect(api.getBrew).toHaveBeenCalled();
|
||||
expect(markModifiedFunc).toHaveBeenCalled();
|
||||
expect(model.findOne).toHaveBeenCalled();
|
||||
expect(removeFunc).not.toHaveBeenCalled();
|
||||
expect(model.deleteOne).not.toHaveBeenCalled();
|
||||
expect(api.deleteGoogleBrew).toHaveBeenCalled();
|
||||
expect(saveFunc).toHaveBeenCalled();
|
||||
expect(saved.authors).toEqual(['test2']);
|
||||
@@ -741,6 +774,7 @@ brew`);
|
||||
req.brew = brew;
|
||||
});
|
||||
model.findOne = jest.fn(async ()=>modelBrew(brew));
|
||||
model.deleteOne = jest.fn(async ()=>{});
|
||||
api.deleteGoogleBrew = jest.fn(async ()=>true);
|
||||
const req = { account: { username: 'test2' } };
|
||||
|
||||
@@ -748,7 +782,7 @@ brew`);
|
||||
|
||||
expect(api.getBrew).toHaveBeenCalled();
|
||||
expect(model.findOne).toHaveBeenCalled();
|
||||
expect(removeFunc).not.toHaveBeenCalled();
|
||||
expect(model.deleteOne).not.toHaveBeenCalled();
|
||||
expect(api.deleteGoogleBrew).not.toHaveBeenCalled();
|
||||
expect(saveFunc).toHaveBeenCalled();
|
||||
expect(saved.authors).toEqual(['test']);
|
||||
|
||||
@@ -15,6 +15,7 @@ const HomebrewSchema = mongoose.Schema({
|
||||
description : { type: String, default: '' },
|
||||
tags : [String],
|
||||
systems : [String],
|
||||
lang : { type: String, default: 'en' },
|
||||
renderer : { type: String, default: '' },
|
||||
authors : [String],
|
||||
invitedAuthors : [String],
|
||||
@@ -39,30 +40,24 @@ HomebrewSchema.statics.increaseView = async function(query) {
|
||||
return brew;
|
||||
};
|
||||
|
||||
HomebrewSchema.statics.get = function(query, fields=null){
|
||||
return new Promise((resolve, reject)=>{
|
||||
Homebrew.find(query, fields, null, (err, brews)=>{
|
||||
if(err || !brews.length) return reject('Can not find brew');
|
||||
if(!_.isNil(brews[0].textBin)) { // Uncompress zipped text field
|
||||
unzipped = zlib.inflateRawSync(brews[0].textBin);
|
||||
brews[0].text = unzipped.toString();
|
||||
}
|
||||
return resolve(brews[0]);
|
||||
});
|
||||
});
|
||||
HomebrewSchema.statics.get = async function(query, fields=null){
|
||||
const brew = await Homebrew.findOne(query, fields).orFail()
|
||||
.catch((error)=>{throw 'Can not find brew';});
|
||||
if(!_.isNil(brew.textBin)) { // Uncompress zipped text field
|
||||
unzipped = zlib.inflateRawSync(brew.textBin);
|
||||
brew.text = unzipped.toString();
|
||||
}
|
||||
return brew;
|
||||
};
|
||||
|
||||
HomebrewSchema.statics.getByUser = function(username, allowAccess=false, fields=null){
|
||||
return new Promise((resolve, reject)=>{
|
||||
const query = { authors: username, published: true };
|
||||
if(allowAccess){
|
||||
delete query.published;
|
||||
}
|
||||
Homebrew.find(query, fields).lean().exec((err, brews)=>{ //lean() converts results to JSObjects
|
||||
if(err) return reject('Can not find brew');
|
||||
return resolve(brews);
|
||||
});
|
||||
});
|
||||
HomebrewSchema.statics.getByUser = async function(username, allowAccess=false, fields=null){
|
||||
const query = { authors: username, published: true };
|
||||
if(allowAccess){
|
||||
delete query.published;
|
||||
}
|
||||
const brews = await Homebrew.find(query, fields).lean().exec() //lean() converts results to JSObjects
|
||||
.catch((error)=>{throw 'Can not find brews';});
|
||||
return brews;
|
||||
};
|
||||
|
||||
const Homebrew = mongoose.model('Homebrew', HomebrewSchema);
|
||||
|
||||
12
server/middleware/content-negotiation.js
Normal file
@@ -0,0 +1,12 @@
|
||||
module.exports = (req, res, next)=>{
|
||||
const isImageRequest = req.get('Accept')?.split(',')
|
||||
?.filter((h)=>!h.includes('q='))
|
||||
?.every((h)=>/image\/.*/.test(h));
|
||||
if(isImageRequest) {
|
||||
return res.status(406).send({
|
||||
message : 'Request for image at this URL is not supported'
|
||||
});
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
41
server/middleware/content-negotiation.spec.js
Normal file
@@ -0,0 +1,41 @@
|
||||
const contentNegotiationMiddleware = require('./content-negotiation.js');
|
||||
|
||||
describe('content-negotiation-middleware', ()=>{
|
||||
let request;
|
||||
let response;
|
||||
let next;
|
||||
|
||||
beforeEach(()=>{
|
||||
request = {
|
||||
get : function(key) {
|
||||
return this[key];
|
||||
}
|
||||
};
|
||||
response = {
|
||||
status : jest.fn(()=>response),
|
||||
send : jest.fn(()=>{})
|
||||
};
|
||||
next = jest.fn();
|
||||
});
|
||||
|
||||
it('should return 406 on image request', ()=>{
|
||||
contentNegotiationMiddleware({
|
||||
Accept : 'image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8',
|
||||
...request
|
||||
}, response);
|
||||
|
||||
expect(response.status).toHaveBeenLastCalledWith(406);
|
||||
expect(response.send).toHaveBeenCalledWith({
|
||||
message : 'Request for image at this URL is not supported'
|
||||
});
|
||||
});
|
||||
|
||||
it('should call next on non-image request', ()=>{
|
||||
contentNegotiationMiddleware({
|
||||
Accept : 'text,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8',
|
||||
...request
|
||||
}, response, next);
|
||||
|
||||
expect(next).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -49,7 +49,8 @@ const CodeEditor = createClass({
|
||||
value : '',
|
||||
wrap : true,
|
||||
onChange : ()=>{},
|
||||
enableFolding : true
|
||||
enableFolding : true,
|
||||
editorTheme : 'default'
|
||||
};
|
||||
},
|
||||
|
||||
@@ -91,17 +92,24 @@ const CodeEditor = createClass({
|
||||
} else {
|
||||
this.codeMirror.setOption('foldOptions', false);
|
||||
}
|
||||
|
||||
if(prevProps.editorTheme !== this.props.editorTheme){
|
||||
this.codeMirror.setOption('theme', this.props.editorTheme);
|
||||
}
|
||||
},
|
||||
|
||||
buildEditor : function() {
|
||||
this.codeMirror = CodeMirror(this.refs.editor, {
|
||||
lineNumbers : true,
|
||||
lineWrapping : this.props.wrap,
|
||||
indentWithTabs : true,
|
||||
indentWithTabs : false,
|
||||
tabSize : 2,
|
||||
smartIndent : false,
|
||||
historyEventDelay : 250,
|
||||
scrollPastEnd : true,
|
||||
extraKeys : {
|
||||
'Tab' : this.indent,
|
||||
'Shift-Tab' : this.dedent,
|
||||
'Ctrl-B' : this.makeBold,
|
||||
'Cmd-B' : this.makeBold,
|
||||
'Ctrl-I' : this.makeItalic,
|
||||
@@ -156,6 +164,7 @@ const CodeEditor = createClass({
|
||||
autoCloseTags : true,
|
||||
styleActiveLine : true,
|
||||
showTrailingSpace : false,
|
||||
theme : this.props.editorTheme
|
||||
// specialChars : / /,
|
||||
// specialCharPlaceholder : function(char) {
|
||||
// const el = document.createElement('span');
|
||||
@@ -171,6 +180,19 @@ const CodeEditor = createClass({
|
||||
this.updateSize();
|
||||
},
|
||||
|
||||
indent : function () {
|
||||
const cm = this.codeMirror;
|
||||
if(cm.somethingSelected()) {
|
||||
cm.execCommand('indentMore');
|
||||
} else {
|
||||
cm.execCommand('insertSoftTab');
|
||||
}
|
||||
},
|
||||
|
||||
dedent : function () {
|
||||
this.codeMirror.execCommand('indentLess');
|
||||
},
|
||||
|
||||
makeHeader : function (number) {
|
||||
const selection = this.codeMirror.getSelection();
|
||||
const header = Array(number).fill('#').join('');
|
||||
@@ -390,7 +412,10 @@ const CodeEditor = createClass({
|
||||
//----------------------//
|
||||
|
||||
render : function(){
|
||||
return <div className='codeEditor' ref='editor' style={this.props.style}/>;
|
||||
return <>
|
||||
<link href={`../homebrew/cm-themes/${this.props.editorTheme}.css`} rel='stylesheet' />
|
||||
<div className='codeEditor' ref='editor' style={this.props.style}/>
|
||||
</>;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -9,6 +9,9 @@
|
||||
}
|
||||
|
||||
.codeEditor{
|
||||
@media screen and (pointer : coarse) {
|
||||
font-size : 16px;
|
||||
}
|
||||
.CodeMirror-foldmarker {
|
||||
font-family: inherit;
|
||||
text-shadow: none;
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
const _ = require('lodash');
|
||||
const Marked = require('marked');
|
||||
const MarkedExtendedTables = require('marked-extended-tables');
|
||||
const { markedSmartypantsLite: MarkedSmartypantsLite } = require('marked-smartypants-lite');
|
||||
const { gfmHeadingId: MarkedGFMHeadingId } = require('marked-gfm-heading-id');
|
||||
const renderer = new Marked.Renderer();
|
||||
|
||||
//Processes the markdown within an HTML block if it's just a class-wrapper
|
||||
@@ -134,7 +136,7 @@ const mustacheInjectInline = {
|
||||
const match = inlineRegex.exec(src);
|
||||
if(match) {
|
||||
const lastToken = tokens[tokens.length - 1];
|
||||
if(!lastToken)
|
||||
if(!lastToken || lastToken.type == 'mustacheInjectInline')
|
||||
return false;
|
||||
|
||||
const tags = ` ${processStyleTags(match[1])}`;
|
||||
@@ -169,7 +171,7 @@ const mustacheInjectBlock = {
|
||||
const match = inlineRegex.exec(src);
|
||||
if(match) {
|
||||
const lastToken = tokens[tokens.length - 1];
|
||||
if(!lastToken)
|
||||
if(!lastToken || lastToken.type == 'mustacheInjectBlock')
|
||||
return false;
|
||||
|
||||
lastToken.originalType = 'mustacheInjectBlock';
|
||||
@@ -237,9 +239,9 @@ const definitionLists = {
|
||||
};
|
||||
|
||||
Marked.use({ extensions: [mustacheSpans, mustacheDivs, mustacheInjectInline, definitionLists] });
|
||||
Marked.use(MarkedExtendedTables());
|
||||
Marked.use(mustacheInjectBlock);
|
||||
Marked.use({ smartypants: true });
|
||||
Marked.use({ renderer: renderer, mangle: false });
|
||||
Marked.use(MarkedExtendedTables(), MarkedGFMHeadingId(), MarkedSmartypantsLite());
|
||||
|
||||
//Fix local links in the Preview iFrame to link inside the frame
|
||||
renderer.link = function (href, title, text) {
|
||||
@@ -311,12 +313,6 @@ const escape = function (html, encode) {
|
||||
return html;
|
||||
};
|
||||
|
||||
const sanatizeScriptTags = (content)=>{
|
||||
return content
|
||||
.replace(/<script/ig, '<script')
|
||||
.replace(/<\/script>/ig, '</script>');
|
||||
};
|
||||
|
||||
const tagTypes = ['div', 'span', 'a'];
|
||||
const tagRegex = new RegExp(`(${
|
||||
_.map(tagTypes, (type)=>{
|
||||
@@ -347,10 +343,7 @@ module.exports = {
|
||||
render : (rawBrewText)=>{
|
||||
rawBrewText = rawBrewText.replace(/^\\column$/gm, `\n<div class='columnSplit'></div>\n`)
|
||||
.replace(/^(:+)$/gm, (match)=>`${`<div class='blank'></div>`.repeat(match.length)}\n`);
|
||||
return Marked.parse(
|
||||
sanatizeScriptTags(rawBrewText),
|
||||
{ renderer: renderer }
|
||||
);
|
||||
return Marked.parse(rawBrewText);
|
||||
},
|
||||
|
||||
validate : (rawBrewText)=>{
|
||||
|
||||
@@ -90,12 +90,6 @@ const escape = function (html, encode) {
|
||||
return html;
|
||||
};
|
||||
|
||||
const sanatizeScriptTags = (content)=>{
|
||||
return content
|
||||
.replace(/<script/ig, '<script')
|
||||
.replace(/<\/script>/ig, '</script>');
|
||||
};
|
||||
|
||||
const tagTypes = ['div', 'span', 'a'];
|
||||
const tagRegex = new RegExp(`(${
|
||||
_.map(tagTypes, (type)=>{
|
||||
@@ -113,7 +107,7 @@ module.exports = {
|
||||
marked : Markdown,
|
||||
render : (rawBrewText)=>{
|
||||
return Markdown(
|
||||
sanatizeScriptTags(rawBrewText),
|
||||
rawBrewText,
|
||||
{ renderer: renderer }
|
||||
);
|
||||
},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
require('./nav.less');
|
||||
const React = require('react');
|
||||
const { useState, useRef, useEffect } = React;
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
const cx = require('classnames');
|
||||
@@ -29,7 +30,7 @@ const Nav = {
|
||||
section : createClass({
|
||||
displayName : 'Nav.section',
|
||||
render : function(){
|
||||
return <div className='navSection'>
|
||||
return <div className={`navSection ${this.props.className ?? ''}`}>
|
||||
{this.props.children}
|
||||
</div>;
|
||||
}
|
||||
@@ -71,64 +72,49 @@ const Nav = {
|
||||
}
|
||||
}),
|
||||
|
||||
dropdown : createClass({
|
||||
displayName : 'Nav.dropdown',
|
||||
getDefaultProps : function() {
|
||||
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;
|
||||
dropdown : function dropdown(props) {
|
||||
props = Object.assign({}, props, {
|
||||
trigger : 'hover click'
|
||||
});
|
||||
|
||||
return (
|
||||
<div className='navDropdown'>
|
||||
{dropdownChildren}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
render : function () {
|
||||
const dropdownChildren = React.Children.map(this.props.children, (child, i)=>{
|
||||
// Ignore the first child
|
||||
if(i < 1) return;
|
||||
return child;
|
||||
});
|
||||
return (
|
||||
<div className={`navDropdownContainer ${this.props.className}`}
|
||||
ref='dropdown'
|
||||
onMouseEnter={this.props.trigger == 'hover' ? ()=>{this.handleDropdown(true);} : undefined}
|
||||
onClick= {this.props.trigger == 'click' ? ()=>{this.handleDropdown(true);} : undefined}
|
||||
onMouseLeave={this.props.trigger == 'hover' ? ()=>{this.handleDropdown(false);} : undefined}>
|
||||
{this.props.children[0] || this.props.children /*children is not an array when only one child*/}
|
||||
{this.renderDropdown(dropdownChildren)}
|
||||
</div>
|
||||
);
|
||||
const myRef = useRef(null);
|
||||
const [showDropdown, setShowDropdown] = useState(false);
|
||||
|
||||
useEffect(()=>{
|
||||
document.addEventListener('click', handleClickOutside);
|
||||
return ()=>{
|
||||
document.removeEventListener('click', handleClickOutside);
|
||||
};
|
||||
}, []);
|
||||
|
||||
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;
|
||||
return child;
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={`navDropdownContainer ${props.className}`}
|
||||
ref={myRef}
|
||||
onMouseEnter = { props.trigger.includes('hover') ? ()=>handleDropdown(true) : undefined }
|
||||
onMouseLeave = { props.trigger.includes('hover') ? ()=>handleDropdown(false) : undefined }
|
||||
onClick = { props.trigger.includes('click') ? ()=>handleDropdown(true) : undefined }
|
||||
>
|
||||
{props.children[0] || props.children /*children is not an array when only one child*/}
|
||||
{showDropdown && <div className='navDropdown'>{dropdownChildren}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ nav{
|
||||
position : relative;
|
||||
display : flex;
|
||||
justify-content : space-between;
|
||||
z-index : 2;
|
||||
}
|
||||
.navSection{
|
||||
display : flex;
|
||||
@@ -78,6 +79,8 @@ nav{
|
||||
left : 0px;
|
||||
z-index : 10000;
|
||||
width : 100%;
|
||||
overflow : hidden auto;
|
||||
max-height : calc(100vh - 28px);
|
||||
.navItem{
|
||||
animation-name: glideDropDown;
|
||||
animation-duration: 0.4s;
|
||||
|
||||
@@ -61,7 +61,8 @@ const SplitPane = createClass({
|
||||
return result;
|
||||
},
|
||||
|
||||
handleUp : function(){
|
||||
handleUp : function(e){
|
||||
e.preventDefault();
|
||||
if(this.state.isDragging){
|
||||
this.props.onDragFinish(this.state.currentDividerPos);
|
||||
window.localStorage.setItem(this.props.storageKey, this.state.currentDividerPos);
|
||||
@@ -78,6 +79,7 @@ const SplitPane = createClass({
|
||||
handleMove : function(e){
|
||||
if(!this.state.isDragging) return;
|
||||
|
||||
e.preventDefault();
|
||||
const newSize = this.limitPosition(e.pageX);
|
||||
this.setState({
|
||||
currentDividerPos : newSize,
|
||||
@@ -122,7 +124,7 @@ const SplitPane = createClass({
|
||||
renderDivider : function(){
|
||||
return <>
|
||||
{this.renderMoveArrows()}
|
||||
<div className='divider' onMouseDown={this.handleDown} >
|
||||
<div className='divider' onPointerDown={this.handleDown} >
|
||||
<div className='dots'>
|
||||
<i className='fas fa-circle' />
|
||||
<i className='fas fa-circle' />
|
||||
@@ -133,7 +135,7 @@ const SplitPane = createClass({
|
||||
},
|
||||
|
||||
render : function(){
|
||||
return <div className='splitPane' onMouseMove={this.handleMove} onMouseUp={this.handleUp}>
|
||||
return <div className='splitPane' onPointerMove={this.handleMove} onPointerUp={this.handleUp}>
|
||||
<Pane
|
||||
ref='pane1'
|
||||
width={this.state.currentDividerPos}
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
flex : 1;
|
||||
}
|
||||
.divider{
|
||||
touch-action : none;
|
||||
display : table;
|
||||
height : 100%;
|
||||
width : 15px;
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
const stylelint = require('stylelint');
|
||||
const { isNumber } = require('stylelint/lib/utils/validateTypes');
|
||||
|
||||
const { report, ruleMessages, validateOptions } = stylelint.utils;
|
||||
const ruleName = 'naturalcrit/declaration-block-multi-line-min-declarations';
|
||||
const messages = ruleMessages(ruleName, {
|
||||
expected : (decls)=>`Rule with ${decls} declaration${decls == 1 ? '' : 's'} should be single line`,
|
||||
});
|
||||
|
||||
|
||||
module.exports = stylelint.createPlugin(ruleName, function getPlugin(primaryOption, secondaryOptionObject, context) {
|
||||
return function lint(postcssRoot, postcssResult) {
|
||||
|
||||
const validOptions = validateOptions(
|
||||
postcssResult,
|
||||
ruleName,
|
||||
{
|
||||
actual : primaryOption,
|
||||
possible : [isNumber],
|
||||
}
|
||||
);
|
||||
|
||||
if(!validOptions) { //If the options are invalid, don't lint
|
||||
return;
|
||||
}
|
||||
const isAutoFixing = Boolean(context.fix);
|
||||
|
||||
postcssRoot.walkRules((rule)=>{ //Iterate CSS rules
|
||||
|
||||
//Apply rule only if all children are decls (no further nested rules)
|
||||
if(rule.nodes.length > primaryOption || !rule.nodes.every((node)=>node.type === 'decl')) {
|
||||
return;
|
||||
}
|
||||
|
||||
//Ignore if already one line
|
||||
if(!rule.nodes.some((node)=>node.raws.before.includes('\n')) && !rule.raws.after.includes('\n'))
|
||||
return;
|
||||
|
||||
if(isAutoFixing) { //We are in “fix” mode
|
||||
rule.each((decl)=>{
|
||||
decl.raws.before = ' ';
|
||||
});
|
||||
rule.raws.after = ' ';
|
||||
} else {
|
||||
report({
|
||||
ruleName,
|
||||
result : postcssResult,
|
||||
message : messages.expected(rule.nodes.length), // Build the reported message
|
||||
node : rule, // Specify the reported node
|
||||
word : rule.selector, // Which exact word caused the error? This positions the error properly
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
module.exports.ruleName = ruleName;
|
||||
module.exports.messages = messages;
|
||||
68
stylelint_plugins/declaration-colon-align.js
Normal file
@@ -0,0 +1,68 @@
|
||||
const stylelint = require('stylelint');
|
||||
|
||||
const { report, ruleMessages, validateOptions } = stylelint.utils;
|
||||
const ruleName = 'naturalcrit/declaration-colon-align';
|
||||
const messages = ruleMessages(ruleName, {
|
||||
expected : (rule)=>`Expected colons aligned within rule "${rule}"`,
|
||||
});
|
||||
|
||||
|
||||
module.exports = stylelint.createPlugin(ruleName, function getPlugin(primaryOption, secondaryOptionObject, context) {
|
||||
return function lint(postcssRoot, postcssResult) {
|
||||
|
||||
const validOptions = validateOptions(
|
||||
postcssResult,
|
||||
ruleName,
|
||||
{
|
||||
actual : primaryOption,
|
||||
possible : [
|
||||
true,
|
||||
false
|
||||
]
|
||||
}
|
||||
);
|
||||
|
||||
if(!validOptions) { //If the options are invalid, don't lint
|
||||
return;
|
||||
}
|
||||
const isAutoFixing = Boolean(context.fix);
|
||||
postcssRoot.walkRules((rule)=>{ //Iterate CSS rules
|
||||
|
||||
let maxColonPos = 0;
|
||||
let misaligned = false;
|
||||
rule.each((declaration)=>{
|
||||
|
||||
if(declaration.type != 'decl')
|
||||
return;
|
||||
|
||||
const colonPos = declaration.prop.length + declaration.raws.between.indexOf(':');
|
||||
if(maxColonPos > 0 && colonPos != maxColonPos) {
|
||||
misaligned = true;
|
||||
}
|
||||
maxColonPos = Math.max(maxColonPos, colonPos);
|
||||
});
|
||||
|
||||
if(misaligned) {
|
||||
if(isAutoFixing) { //We are in “fix” mode
|
||||
rule.each((declaration)=>{
|
||||
if(declaration.type != 'decl')
|
||||
return;
|
||||
|
||||
declaration.raws.between = `${' '.repeat(maxColonPos - declaration.prop.length)}:${declaration.raws.between.split(':')[1]}`;
|
||||
});
|
||||
} else { //We are in “report only” mode
|
||||
report({
|
||||
ruleName,
|
||||
result : postcssResult,
|
||||
message : messages.expected(rule.selector), // Build the reported message
|
||||
node : rule, // Specify the reported node
|
||||
word : rule.selector, // Which exact word caused the error? This positions the error properly
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
module.exports.ruleName = ruleName;
|
||||
module.exports.messages = messages;
|
||||
52
stylelint_plugins/declaration-colon-min-space-before.js
Normal file
@@ -0,0 +1,52 @@
|
||||
const stylelint = require('stylelint');
|
||||
const { isNumber } = require('stylelint/lib/utils/validateTypes');
|
||||
|
||||
const { report, ruleMessages, validateOptions } = stylelint.utils;
|
||||
const ruleName = 'naturalcrit/declaration-colon-min-space-before';
|
||||
const messages = ruleMessages(ruleName, {
|
||||
expected : (num)=>`Expected at least ${num} space${num == 1 ? '' : 's'} before ":"`
|
||||
});
|
||||
|
||||
|
||||
module.exports = stylelint.createPlugin(ruleName, function getPlugin(primaryOption, secondaryOptionObject, context) {
|
||||
return function lint(postcssRoot, postcssResult) {
|
||||
|
||||
const validOptions = validateOptions(
|
||||
postcssResult,
|
||||
ruleName,
|
||||
{
|
||||
actual : primaryOption,
|
||||
possible : [isNumber],
|
||||
}
|
||||
);
|
||||
|
||||
if(!validOptions) { //If the options are invalid, don't lint
|
||||
return;
|
||||
}
|
||||
const isAutoFixing = Boolean(context.fix);
|
||||
|
||||
postcssRoot.walkDecls((decl)=>{ //Iterate CSS declarations
|
||||
|
||||
const between = decl.raws.between;
|
||||
const colonIndex = between.indexOf(':');
|
||||
|
||||
if(between.slice(0, colonIndex).length >= primaryOption) {
|
||||
return;
|
||||
}
|
||||
if(isAutoFixing) { //We are in “fix” mode
|
||||
decl.raws.between = between.slice(0, colonIndex).replace(/\s*$/, ' '.repeat(primaryOption)) + between.slice(colonIndex);
|
||||
} else {
|
||||
report({
|
||||
ruleName,
|
||||
result : postcssResult,
|
||||
message : messages.expected(primaryOption), // Build the reported message
|
||||
node : decl, // Specify the reported node
|
||||
word : ':', // Which exact word caused the error? This positions the error properly
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
module.exports.ruleName = ruleName;
|
||||
module.exports.messages = messages;
|
||||
@@ -2,14 +2,14 @@
|
||||
|
||||
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() {
|
||||
const source = '<div>*Bold text*</div>';
|
||||
const rendered = Markdown.render(source);
|
||||
expect(rendered).toBe('<div> <p><em>Bold text</em></p>\n </div>');
|
||||
});
|
||||
|
||||
test('Check markdown is using the custom renderer; specifically that it adds target=_self attribute to internal links in HTML blocks', function() {
|
||||
const source = '<div>[Has _self Attribute?](#p1)</div>';
|
||||
const rendered = Markdown.render(source);
|
||||
expect(rendered).toBe('<div> <p><a href="#p1" target="_self">Has _self Attribute?</a></p>\n </div>');
|
||||
});
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
/* eslint-disable max-lines */
|
||||
|
||||
const Markdown = require('naturalcrit/markdown.js');
|
||||
|
||||
test('Renders a mustache span with text only', function() {
|
||||
const source = '{{ text}}';
|
||||
const rendered = Markdown.render(source);
|
||||
expect(rendered).toBe('<span class="inline-block ">text</span>');
|
||||
});
|
||||
|
||||
test('Renders a mustache span with text only, but with spaces', function() {
|
||||
const source = '{{ this is a text}}';
|
||||
const rendered = Markdown.render(source);
|
||||
expect(rendered).toBe('<span class="inline-block ">this is a text</span>');
|
||||
});
|
||||
|
||||
test('Renders an empty mustache span', function() {
|
||||
const source = '{{}}';
|
||||
const rendered = Markdown.render(source);
|
||||
expect(rendered).toBe('<span class="inline-block "></span>');
|
||||
});
|
||||
|
||||
test('Renders a mustache span with just a space', function() {
|
||||
const source = '{{ }}';
|
||||
const rendered = Markdown.render(source);
|
||||
expect(rendered).toBe('<span class="inline-block "></span>');
|
||||
});
|
||||
|
||||
test('Renders a mustache span with a few spaces only', function() {
|
||||
const source = '{{ }}';
|
||||
const rendered = Markdown.render(source);
|
||||
expect(rendered).toBe('<span class="inline-block "></span>');
|
||||
});
|
||||
|
||||
test('Renders a mustache span with text and class', function() {
|
||||
const source = '{{my-class text}}';
|
||||
const rendered = Markdown.render(source);
|
||||
// FIXME: why do we have those two extra spaces after closing "?
|
||||
expect(rendered).toBe('<span class="inline-block my-class" >text</span>');
|
||||
});
|
||||
|
||||
test('Renders a mustache span with text and two classes', function() {
|
||||
const source = '{{my-class,my-class2 text}}';
|
||||
const rendered = Markdown.render(source);
|
||||
// FIXME: why do we have those two extra spaces after closing "?
|
||||
expect(rendered).toBe('<span class="inline-block my-class my-class2" >text</span>');
|
||||
});
|
||||
|
||||
test('Renders a mustache span with text with spaces and class', function() {
|
||||
const source = '{{my-class this is a text}}';
|
||||
const rendered = Markdown.render(source);
|
||||
// FIXME: why do we have those two extra spaces after closing "?
|
||||
expect(rendered).toBe('<span class="inline-block my-class" >this is a text</span>');
|
||||
});
|
||||
|
||||
test('Renders a mustache span with text and id', function() {
|
||||
const source = '{{#my-span text}}';
|
||||
const rendered = Markdown.render(source);
|
||||
// FIXME: why do we have that one extra space after closing "?
|
||||
expect(rendered).toBe('<span class="inline-block " id="my-span" >text</span>');
|
||||
});
|
||||
|
||||
test('Renders a mustache span with text and two ids', function() {
|
||||
const source = '{{#my-span,#my-favorite-span text}}';
|
||||
const rendered = Markdown.render(source);
|
||||
// FIXME: do we need to report an error here somehow?
|
||||
expect(rendered).toBe('<span class="inline-block " id="my-span" >text</span>');
|
||||
});
|
||||
|
||||
test('Renders a mustache span with text and css property', function() {
|
||||
const source = '{{color:red text}}';
|
||||
const rendered = Markdown.render(source);
|
||||
expect(rendered).toBe('<span class="inline-block " style="color:red;">text</span>');
|
||||
});
|
||||
|
||||
test('Renders a mustache span with text and two css properties', function() {
|
||||
const source = '{{color:red,padding:5px text}}';
|
||||
const rendered = Markdown.render(source);
|
||||
expect(rendered).toBe('<span class="inline-block " style="color:red; padding:5px;">text</span>');
|
||||
});
|
||||
|
||||
test('Renders a mustache span with text and css property which contains quotes', function() {
|
||||
const source = '{{font:"trebuchet ms" text}}';
|
||||
const rendered = Markdown.render(source);
|
||||
// FIXME: is it correct to remove quotes surrounding css property value?
|
||||
expect(rendered).toBe('<span class="inline-block " style="font:trebuchet ms;">text</span>');
|
||||
});
|
||||
|
||||
test('Renders a mustache span with text and two css properties which contains quotes', function() {
|
||||
const source = '{{font:"trebuchet ms",padding:"5px 10px" text}}';
|
||||
const rendered = Markdown.render(source);
|
||||
expect(rendered).toBe('<span class="inline-block " style="font:trebuchet ms; padding:5px 10px;">text</span>');
|
||||
});
|
||||
|
||||
|
||||
test('Renders a mustache span with text with quotes and css property which contains quotes', function() {
|
||||
const source = '{{font:"trebuchet ms" text "with quotes"}}';
|
||||
const rendered = Markdown.render(source);
|
||||
expect(rendered).toBe('<span class="inline-block " style="font:trebuchet ms;">text “with quotes”</span>');
|
||||
});
|
||||
|
||||
test('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 rendered = Markdown.render(source);
|
||||
expect(rendered).toBe('<span class="inline-block pen" id="author" style="color:orange; font-family:trebuchet ms;">text</span>');
|
||||
});
|
||||
|
||||
// TODO: add tests for ID with accordance to CSS spec:
|
||||
//
|
||||
// From https://drafts.csswg.org/selectors/#id-selectors:
|
||||
//
|
||||
// > An ID selector consists of a “number sign” (U+0023, #) immediately followed by the ID value, which must be a CSS identifier.
|
||||
//
|
||||
// From: https://www.w3.org/TR/CSS21/syndata.html#value-def-identifier:
|
||||
//
|
||||
// > In CSS, identifiers (including element names, classes, and IDs in selectors) can contain only the characters [a-zA-Z0-9]
|
||||
// > and ISO 10646 characters U+00A0 and higher, plus the hyphen (-) and the underscore (_);
|
||||
// > they cannot start with a digit, two hyphens, or a hyphen followed by a digit.
|
||||
// > Identifiers can also contain escaped characters and any ISO 10646 character as a numeric code (see next item).
|
||||
// > For instance, the identifier "B&W?" may be written as "B\&W\?" or "B\26 W\3F".
|
||||
// > Note that Unicode is code-by-code equivalent to ISO 10646 (see [UNICODE] and [ISO10646]).
|
||||
|
||||
// TODO: add tests for class with accordance to CSS spec:
|
||||
//
|
||||
// From: https://drafts.csswg.org/selectors/#class-html:
|
||||
//
|
||||
// > The class selector is given as a full stop (. U+002E) immediately followed by an identifier.
|
||||
|
||||
375
tests/markdown/mustache-syntax.test.js
Normal file
@@ -0,0 +1,375 @@
|
||||
/* 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, '');
|
||||
};
|
||||
|
||||
// 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('Inline: When using the Inline syntax {{ }}', ()=>{
|
||||
it.failing('Renders a mustache span with text only', function() {
|
||||
const source = '{{ text}}';
|
||||
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>');
|
||||
});
|
||||
|
||||
it.failing('Renders a mustache span with text only, but with spaces', function() {
|
||||
const source = '{{ this is a text}}';
|
||||
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>');
|
||||
});
|
||||
|
||||
it.failing('Renders an empty mustache span', function() {
|
||||
const 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>');
|
||||
});
|
||||
|
||||
it.failing('Renders a mustache span with just a space', function() {
|
||||
const 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>');
|
||||
});
|
||||
|
||||
it.failing('Renders a mustache span with a few spaces only', function() {
|
||||
const 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>');
|
||||
});
|
||||
|
||||
it.failing('Renders a mustache span with text and class', function() {
|
||||
const source = '{{my-class text}}';
|
||||
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>');
|
||||
});
|
||||
|
||||
it.failing('Renders a mustache span with text and two classes', function() {
|
||||
const source = '{{my-class,my-class2 text}}';
|
||||
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>');
|
||||
});
|
||||
|
||||
it.failing('Renders a mustache span with text with spaces and class', function() {
|
||||
const source = '{{my-class this is a text}}';
|
||||
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>');
|
||||
});
|
||||
|
||||
it.failing('Renders a mustache span with text and id', function() {
|
||||
const source = '{{#my-span text}}';
|
||||
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>');
|
||||
});
|
||||
|
||||
it.failing('Renders a mustache span with text and two ids', function() {
|
||||
const source = '{{#my-span,#my-favorite-span text}}';
|
||||
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>');
|
||||
});
|
||||
|
||||
it.failing('Renders a mustache span with text and css property', function() {
|
||||
const source = '{{color:red text}}';
|
||||
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>');
|
||||
});
|
||||
|
||||
it.failing('Renders a mustache span with text and two css properties', function() {
|
||||
const source = '{{color:red,padding:5px text}}';
|
||||
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>');
|
||||
});
|
||||
|
||||
it.failing('Renders a mustache span with text and css property which contains quotes', function() {
|
||||
const source = '{{font-family:"trebuchet ms" text}}';
|
||||
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>');
|
||||
});
|
||||
|
||||
it.failing('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 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>');
|
||||
});
|
||||
|
||||
|
||||
it.failing('Renders a mustache span with text with quotes and css property which contains quotes', function() {
|
||||
const source = '{{font-family:"trebuchet ms" text "with quotes"}}';
|
||||
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>');
|
||||
});
|
||||
|
||||
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 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>');
|
||||
});
|
||||
});
|
||||
|
||||
// BLOCK SYNTAX
|
||||
|
||||
describe(`Block: When using the Block syntax {{tags\\ntext\\n}}`, ()=>{
|
||||
it.failing('Renders a div with text only', function() {
|
||||
const source = dedent`{{
|
||||
text
|
||||
}}`;
|
||||
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>`);
|
||||
});
|
||||
|
||||
it.failing('Renders an empty div', function() {
|
||||
const source = dedent`{{
|
||||
|
||||
}}`;
|
||||
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>`);
|
||||
});
|
||||
|
||||
it('Renders a single paragraph with opening and closing brackets', function() {
|
||||
const source = dedent`{{
|
||||
}}`;
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
// this actually renders in HB as '{{ }}'...
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p>{{}}</p>`);
|
||||
});
|
||||
|
||||
it.failing('Renders a div with a single class', function() {
|
||||
const source = dedent`{{cat
|
||||
|
||||
}}`;
|
||||
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>`);
|
||||
});
|
||||
|
||||
it.failing('Renders a div with a single class and text', function() {
|
||||
const source = dedent`{{cat
|
||||
Sample text.
|
||||
}}`;
|
||||
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>`);
|
||||
});
|
||||
|
||||
it.failing('Renders a div with two classes and text', function() {
|
||||
const source = dedent`{{cat,dog
|
||||
Sample text.
|
||||
}}`;
|
||||
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>`);
|
||||
});
|
||||
|
||||
it.failing('Renders a div with a style and text', function() {
|
||||
const source = dedent`{{color:red
|
||||
Sample text.
|
||||
}}`;
|
||||
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>`);
|
||||
});
|
||||
|
||||
it.failing('Renders a div with a class, style and text', function() {
|
||||
const source = dedent`{{cat,color:red
|
||||
Sample text.
|
||||
}}`;
|
||||
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>`);
|
||||
});
|
||||
|
||||
it('Renders a div with an ID, class, style and text (different order)', function() {
|
||||
const source = dedent`{{color:red,cat,#dog
|
||||
Sample text.
|
||||
}}`;
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block cat" id="dog" style="color:red;"><p>Sample text.</p></div>`);
|
||||
});
|
||||
|
||||
it.failing('Renders a div with a single ID', function() {
|
||||
const source = dedent`{{#cat,#dog
|
||||
Sample text.
|
||||
}}`;
|
||||
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>`);
|
||||
});
|
||||
});
|
||||
|
||||
// MUSTACHE INJECTION SYNTAX
|
||||
|
||||
describe('Injection: When an injection tag follows an element', ()=>{
|
||||
// FIXME: Most of these fail because injections currently replace attributes, rather than append to. Or just minor extra whitespace issues.
|
||||
describe('and that element is an inline-block', ()=>{
|
||||
it.failing('Renders a span "text" with no injection', function() {
|
||||
const source = '{{ text}}{}';
|
||||
const rendered = Markdown.render(source);
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block">text</span>');
|
||||
});
|
||||
|
||||
it.failing('Renders a span "text" with injected Class name', function() {
|
||||
const source = '{{ text}}{ClassName}';
|
||||
const rendered = Markdown.render(source);
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block ClassName">text</span>');
|
||||
});
|
||||
|
||||
it.failing('Renders a span "text" with injected style', function() {
|
||||
const source = '{{ text}}{color:red}';
|
||||
const rendered = Markdown.render(source);
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" style="color:red;">text</span>');
|
||||
});
|
||||
|
||||
it.failing('Renders a span "text" with two injected styles', function() {
|
||||
const source = '{{ text}}{color:red,background:blue}';
|
||||
const rendered = Markdown.render(source);
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" style="color:red; background:blue;">text</span>');
|
||||
});
|
||||
|
||||
it.failing('Renders an emphasis element with injected Class name', function() {
|
||||
const source = '*emphasis*{big}';
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p><em class="big">emphasis</em></p>');
|
||||
});
|
||||
|
||||
it.failing('Renders a code element with injected style', function() {
|
||||
const source = '`code`{background:gray}';
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p><code style="background:gray;">code</code></p>');
|
||||
});
|
||||
|
||||
it.failing('Renders an image element with injected style', function() {
|
||||
const source = '{position:absolute}';
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p><img src="http://i.imgur.com/hMna6G0.png" alt="homebrew mug" style="position:absolute;"></p>');
|
||||
});
|
||||
|
||||
it.failing('Renders an element modified by only the first of two consecutive injections', function() {
|
||||
const source = '{{ text}}{color:red}{background:blue}';
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p><span class="inline-block" style="color:red;">text</span>{background:blue}</p>');
|
||||
});
|
||||
});
|
||||
|
||||
describe('and that element is a block', ()=>{
|
||||
it.failing('renders a div "text" with no injection', function() {
|
||||
const source = '{{\ntext\n}}\n{}';
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<div class="block"><p>text</p></div>');
|
||||
});
|
||||
|
||||
it.failing('renders a div "text" with injected Class name', function() {
|
||||
const source = '{{\ntext\n}}\n{ClassName}';
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<div class="block ClassName"><p>text</p></div>');
|
||||
});
|
||||
|
||||
it.failing('renders a div "text" with injected style', function() {
|
||||
const source = '{{\ntext\n}}\n{color:red}';
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<div class="block" style="color:red;"><p>text</p></div>');
|
||||
});
|
||||
|
||||
it.failing('renders a div "text" with two injected styles', function() {
|
||||
const source = dedent`{{
|
||||
text
|
||||
}}
|
||||
{color:red,background:blue}`;
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<div class="block" style="color:red; background:blue;"><p>text</p></div>');
|
||||
});
|
||||
|
||||
it.failing('renders an h2 header "text" with injected class name', function() {
|
||||
const source = dedent`## text
|
||||
{ClassName}`;
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<h2 class="ClassName">text</h2>');
|
||||
});
|
||||
|
||||
it.failing('renders a table with injected class name', function() {
|
||||
const source = dedent`| Experience Points | Level |
|
||||
|:------------------|:-----:|
|
||||
| 0 | 1 |
|
||||
| 300 | 2 |
|
||||
|
||||
{ClassName}`;
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<table class="ClassName"><thead><tr><th align=left>Experience Points</th><th align=center>Level</th></tr></thead><tbody><tr><td align=left>0</td><td align=center>1</td></tr><tr><td align=left>300</td><td align=center>2</td></tr></tbody></table>`);
|
||||
});
|
||||
|
||||
// it('renders a list with with a style injected into the <ul> tag', function() {
|
||||
// const source = dedent`- Cursed Ritual of Bad Hair
|
||||
// - Eliminate Vindictiveness in Gym Teacher
|
||||
// - Ultimate Rite of the Confetti Angel
|
||||
// - Dark Chant of the Dentists
|
||||
// - Divine Spell of Crossdressing
|
||||
// {color:red}`;
|
||||
// const rendered = Markdown.render(source).trimReturns();
|
||||
// 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() {
|
||||
const source = dedent`## text
|
||||
{ClassName}
|
||||
{secondInjection}`;
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<h2 class="ClassName">text</h2><p>{secondInjection}</p>');
|
||||
});
|
||||
|
||||
it.failing('renders a div nested into another div, the inner with class=innerDiv and the other class=outerDiv', function() {
|
||||
const source = dedent`{{
|
||||
outer text
|
||||
{{
|
||||
inner text
|
||||
}}
|
||||
{innerDiv}
|
||||
}}
|
||||
{outerDiv}`;
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<div class="block outerDiv"><p>outer text</p><div class="block innerDiv"><p>inner text</p></div></div>');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// TODO: add tests for ID with accordance to CSS spec:
|
||||
//
|
||||
// From https://drafts.csswg.org/selectors/#id-selectors:
|
||||
//
|
||||
// > An ID selector consists of a “number sign” (U+0023, #) immediately followed by the ID value, which must be a CSS identifier.
|
||||
//
|
||||
// From: https://www.w3.org/TR/CSS21/syndata.html#value-def-identifier:
|
||||
//
|
||||
// > In CSS, identifiers (including element names, classes, and IDs in selectors) can contain only the characters [a-zA-Z0-9]
|
||||
// > and ISO 10646 characters U+00A0 and higher, plus the hyphen (-) and the underscore (_);
|
||||
// > they cannot start with a digit, two hyphens, or a hyphen followed by a digit.
|
||||
// > Identifiers can also contain escaped characters and any ISO 10646 character as a numeric code (see next item).
|
||||
// > For instance, the identifier "B&W?" may be written as "B\&W\?" or "B\26 W\3F".
|
||||
// > Note that Unicode is code-by-code equivalent to ISO 10646 (see [UNICODE] and [ISO10646]).
|
||||
|
||||
// TODO: add tests for class with accordance to CSS spec:
|
||||
//
|
||||
// From: https://drafts.csswg.org/selectors/#class-html:
|
||||
//
|
||||
// > The class selector is given as a full stop (. U+002E) immediately followed by an identifier.
|
||||
@@ -47,8 +47,8 @@ const getTOC = (pages)=>{
|
||||
return res;
|
||||
};
|
||||
|
||||
module.exports = function(brew){
|
||||
const pages = brew.text.split('\\page');
|
||||
module.exports = function(props){
|
||||
const pages = props.brew.text.split('\\page');
|
||||
const TOC = getTOC(pages);
|
||||
const markdown = _.reduce(TOC, (r, g1, idx1)=>{
|
||||
r.push(`- **[${idx1 + 1} ${g1.title}](#p${g1.page})**`);
|
||||
|
||||
@@ -262,6 +262,7 @@ body {
|
||||
//Full Width
|
||||
hr+hr+blockquote{
|
||||
.useColumns(0.96);
|
||||
column-fill : balance;
|
||||
}
|
||||
//*****************************
|
||||
// * FOOTER
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
@import (less) './themes/assets/assets.less';
|
||||
|
||||
:root {
|
||||
//Colors
|
||||
--HB_Color_Accent : #EBCEC3; // Salmon
|
||||
@@ -8,6 +10,21 @@
|
||||
background-image : url(/assets/DMG_background.png);
|
||||
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 {
|
||||
background-image : url(/assets/DMG_footerAccent.png);
|
||||
height: 58px;
|
||||
@@ -17,3 +34,10 @@
|
||||
bottom : 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.page:has(.partCover) {
|
||||
|
||||
.partCover {
|
||||
background-image: @partCoverHeaderDMG;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,12 @@
|
||||
const MagicGen = require('./snippets/magic.gen.js');
|
||||
const ClassTableGen = require('./snippets/classtable.gen.js');
|
||||
const MonsterBlockGen = require('./snippets/monsterblock.gen.js');
|
||||
const scriptGen = require('./snippets/script.gen.js');
|
||||
const ClassFeatureGen = require('./snippets/classfeature.gen.js');
|
||||
const CoverPageGen = require('./snippets/coverpage.gen.js');
|
||||
const TableOfContentsGen = require('./snippets/tableOfContents.gen.js');
|
||||
const indexGen = require('./snippets/index.gen.js');
|
||||
const QuoteGen = require('./snippets/quote.gen.js');
|
||||
const dedent = require('dedent-tabs').default;
|
||||
|
||||
|
||||
@@ -17,20 +20,16 @@ module.exports = [
|
||||
icon : 'fas fa-pencil-alt',
|
||||
view : 'text',
|
||||
snippets : [
|
||||
{
|
||||
name : 'Page Number',
|
||||
icon : 'fas fa-bookmark',
|
||||
gen : '{{pageNumber 1}}\n{{footnote PART 1 | SECTION NAME}}\n\n'
|
||||
},
|
||||
{
|
||||
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',
|
||||
icon : 'fas fa-book',
|
||||
gen : TableOfContentsGen
|
||||
},
|
||||
{
|
||||
name : 'Index',
|
||||
icon : 'fas fa-bars',
|
||||
gen : indexGen,
|
||||
experimental : true
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -125,6 +124,11 @@ module.exports = [
|
||||
icon : 'fas fa-mask',
|
||||
gen : ClassFeatureGen,
|
||||
},
|
||||
{
|
||||
name : 'Quote',
|
||||
icon : 'fas fa-quote-right',
|
||||
gen : QuoteGen,
|
||||
},
|
||||
{
|
||||
name : 'Note',
|
||||
icon : 'fas fa-sticky-note',
|
||||
@@ -169,9 +173,27 @@ module.exports = [
|
||||
gen : MonsterBlockGen.monster('monster,frame,wide', 4),
|
||||
},
|
||||
{
|
||||
name : 'Cover Page',
|
||||
name : 'Front Cover Page',
|
||||
icon : 'fac book-front-cover',
|
||||
gen : CoverPageGen,
|
||||
gen : CoverPageGen.front,
|
||||
experimental : true
|
||||
},
|
||||
{
|
||||
name : 'Inside Cover Page',
|
||||
icon : 'fac book-inside-cover',
|
||||
gen : CoverPageGen.inside,
|
||||
experimental : true
|
||||
},
|
||||
{
|
||||
name : 'Part Cover Page',
|
||||
icon : 'fac book-part-cover',
|
||||
gen : CoverPageGen.part,
|
||||
experimental : true
|
||||
},
|
||||
{
|
||||
name : 'Back Cover Page',
|
||||
icon : 'fac book-back-cover',
|
||||
gen : CoverPageGen.back,
|
||||
experimental : true
|
||||
},
|
||||
{
|
||||
@@ -190,7 +212,7 @@ module.exports = [
|
||||
}}
|
||||
\n`;
|
||||
},
|
||||
},
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -204,35 +226,75 @@ module.exports = [
|
||||
view : 'text',
|
||||
snippets : [
|
||||
{
|
||||
name : 'Class Table',
|
||||
icon : 'fas fa-table',
|
||||
gen : ClassTableGen.full('classTable,frame,decoration,wide'),
|
||||
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',
|
||||
gen : ClassTableGen.full('classTable,frame,decoration,wide'),
|
||||
},
|
||||
{
|
||||
name : 'Full Caster Class Table (unframed)',
|
||||
icon : 'fas fa-border-none',
|
||||
gen : ClassTableGen.full('classTable,wide'),
|
||||
},
|
||||
{
|
||||
name : 'Half Caster Class Table',
|
||||
icon : 'fas fa-list-alt',
|
||||
gen : ClassTableGen.half('classTable,frame,decoration,wide'),
|
||||
},
|
||||
{
|
||||
name : 'Half Caster Class Table (unframed)',
|
||||
icon : 'fas fa-border-none',
|
||||
gen : ClassTableGen.half('classTable,wide'),
|
||||
},
|
||||
{
|
||||
name : 'Third Caster Spell Table',
|
||||
icon : 'fas fa-border-all',
|
||||
gen : ClassTableGen.third('classTable,frame,decoration'),
|
||||
},
|
||||
{
|
||||
name : 'Third Caster Spell Table (unframed)',
|
||||
icon : 'fas fa-border-none',
|
||||
gen : ClassTableGen.third('classTable'),
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name : 'Class Table (unframed)',
|
||||
icon : 'fas fa-border-none',
|
||||
gen : ClassTableGen.full('classTable,wide'),
|
||||
name : 'Rune Table',
|
||||
icon : 'fas fa-language',
|
||||
gen : scriptGen.dwarvish,
|
||||
experimental : true,
|
||||
subsnippets : [
|
||||
{
|
||||
name : 'Dwarvish',
|
||||
icon : 'fac davek',
|
||||
gen : scriptGen.dwarvish,
|
||||
},
|
||||
{
|
||||
name : 'Elvish',
|
||||
icon : 'fac rellanic',
|
||||
gen : scriptGen.elvish,
|
||||
},
|
||||
{
|
||||
name : 'Draconic',
|
||||
icon : 'fac iokharic',
|
||||
gen : scriptGen.draconic,
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
name : '1/2 Class Table',
|
||||
icon : 'fas fa-list-alt',
|
||||
gen : ClassTableGen.half('classTable,decoration,frame'),
|
||||
},
|
||||
{
|
||||
name : '1/2 Class Table (unframed)',
|
||||
icon : 'fas fa-border-none',
|
||||
gen : ClassTableGen.half('classTable'),
|
||||
},
|
||||
{
|
||||
name : '1/3 Class Table',
|
||||
icon : 'fas fa-border-all',
|
||||
gen : ClassTableGen.third('classTable,frame'),
|
||||
},
|
||||
{
|
||||
name : '1/3 Class Table (unframed)',
|
||||
icon : 'fas fa-border-none',
|
||||
gen : ClassTableGen.third('classTable'),
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
|
||||
@@ -1,132 +1,138 @@
|
||||
const _ = require('lodash');
|
||||
const dedent = require('dedent-tabs').default;
|
||||
|
||||
const features = [
|
||||
'Astrological Botany',
|
||||
'Biochemical Sorcery',
|
||||
'Civil Divination',
|
||||
'Consecrated Augury',
|
||||
'Demonic Anthropology',
|
||||
'Divinatory Mineralogy',
|
||||
'Exo Interfacer',
|
||||
'Genetic Banishing',
|
||||
'Gunpowder Torturer',
|
||||
'Gunslinger Corruptor',
|
||||
'Hermetic Geography',
|
||||
'Immunological Cultist',
|
||||
'Malefic Chemist',
|
||||
'Mathematical Pharmacy',
|
||||
'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'
|
||||
'Astrological Botany', 'Biochemical Sorcery', 'Civil Divination',
|
||||
'Consecrated Augury', 'Demonic Anthropology', 'Divinatory Mineralogy',
|
||||
'Exo Interfacer', 'Genetic Banishing', 'Gunpowder Torturer',
|
||||
'Gunslinger Corruptor', 'Hermetic Geography', 'Immunological Cultist',
|
||||
'Malefic Chemist', 'Mathematical Pharmacy', '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'
|
||||
].map((f)=>_.padEnd(f, 21)); // Pad to equal length of 21 chars long
|
||||
|
||||
const classnames = [
|
||||
'Ackerman', 'Berserker-Typist', 'Concierge', 'Fishmonger',
|
||||
'Haberdasher', 'Manicurist', 'Netrunner', 'Weirkeeper'
|
||||
];
|
||||
|
||||
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 = {
|
||||
full : function(classes){
|
||||
const classname = _.sample(classnames);
|
||||
|
||||
|
||||
let cantrips = 3;
|
||||
let spells = 1;
|
||||
let slots = 2;
|
||||
return `{{${classes}\n##### The ${classname}\n` +
|
||||
`| Level | Proficiency | Features | Cantrips | Spells | --- Spell Slots Per Spell Level ---|||||||||\n`+
|
||||
`| ^| Bonus ^| ^| Known ^| Known ^|1st |2nd |3rd |4th |5th |6th |7th |8th |9th |\n`+
|
||||
`|:-----:|:-----------:|:-------------|:--------:|:------:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|\n${
|
||||
_.map(levels, function(levelName, level){
|
||||
const res = [
|
||||
_.pad(levelName, 5),
|
||||
_.pad(`+${profBonus[level]}`, 2),
|
||||
_.padEnd(_.sample(features), 21),
|
||||
_.pad(cantrips.toString(), 8),
|
||||
_.pad(spells.toString(), 6),
|
||||
drawSlots(slots, 9, 2),
|
||||
].join(' | ');
|
||||
|
||||
cantrips += _.random(0, 1);
|
||||
spells += _.random(0, 1);
|
||||
slots += _.random(0, 2);
|
||||
|
||||
return `| ${res} |`;
|
||||
}).join('\n')}\n}}\n\n`;
|
||||
non : function(snippetClasses){
|
||||
return dedent`
|
||||
{{${snippetClasses}
|
||||
##### The ${_.sample(classnames)}
|
||||
| Level | Proficiency Bonus | Features | ${_.sample(features)} |
|
||||
|:-----:|:-----------------:|:---------|:---------------------:|
|
||||
| 1st | +2 | ${_.sample(features)} | 2 |
|
||||
| 2nd | +2 | ${_.sample(features)} | 2 |
|
||||
| 3rd | +2 | ${_.sample(features)} | 3 |
|
||||
| 4th | +2 | ${_.sample(features)} | 3 |
|
||||
| 5th | +3 | ${_.sample(features)} | 3 |
|
||||
| 6th | +3 | ${_.sample(features)} | 4 |
|
||||
| 7th | +3 | ${_.sample(features)} | 4 |
|
||||
| 8th | +3 | ${_.sample(features)} | 4 |
|
||||
| 9th | +4 | ${_.sample(features)} | 4 |
|
||||
| 10th | +4 | ${_.sample(features)} | 4 |
|
||||
| 11th | +4 | ${_.sample(features)} | 4 |
|
||||
| 12th | +4 | ${_.sample(features)} | 5 |
|
||||
| 13th | +5 | ${_.sample(features)} | 5 |
|
||||
| 14th | +5 | ${_.sample(features)} | 5 |
|
||||
| 15th | +5 | ${_.sample(features)} | 5 |
|
||||
| 16th | +5 | ${_.sample(features)} | 5 |
|
||||
| 17th | +6 | ${_.sample(features)} | 6 |
|
||||
| 18th | +6 | ${_.sample(features)} | 6 |
|
||||
| 19th | +6 | ${_.sample(features)} | 6 |
|
||||
| 20th | +6 | ${_.sample(features)} | unlimited |
|
||||
}}\n\n`;
|
||||
},
|
||||
|
||||
half : function(classes){
|
||||
const classname = _.sample(classnames);
|
||||
|
||||
let featureScore = 1;
|
||||
return `{{${classes}\n##### The ${classname}\n` +
|
||||
`| Level | Proficiency Bonus | Features | ${_.pad(_.sample(features), 21)} |\n` +
|
||||
`|:-----:|:-----------------:|:---------|:---------------------:|\n${
|
||||
_.map(levels, function(levelName, level){
|
||||
const res = [
|
||||
_.pad(levelName, 5),
|
||||
_.pad(`+${profBonus[level]}`, 2),
|
||||
_.padEnd(_.sample(features), 23),
|
||||
_.pad(`+${featureScore}`, 21),
|
||||
].join(' | ');
|
||||
|
||||
featureScore += _.random(0, 1);
|
||||
|
||||
return `| ${res} |`;
|
||||
}).join('\n')}\n}}\n\n`;
|
||||
full : function(snippetClasses){
|
||||
return dedent`
|
||||
{{${snippetClasses}
|
||||
##### The ${_.sample(classnames)}
|
||||
| Level | Proficiency | Features | Cantrips | --- Spell Slots Per Spell Level ---|||||||||
|
||||
| ^| Bonus ^| ^| Known ^|1st |2nd |3rd |4th |5th |6th |7th |8th |9th |
|
||||
|:-----:|:-----------:|:-------------|:--------:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|
|
||||
| 1st | +2 | ${_.sample(features)} | 2 | 2 | — | — | — | — | — | — | — | — |
|
||||
| 2nd | +2 | ${_.sample(features)} | 2 | 3 | — | — | — | — | — | — | — | — |
|
||||
| 3rd | +2 | ${_.sample(features)} | 2 | 4 | 2 | — | — | — | — | — | — | — |
|
||||
| 4th | +2 | ${_.sample(features)} | 3 | 4 | 3 | — | — | — | — | — | — | — |
|
||||
| 5th | +3 | ${_.sample(features)} | 3 | 4 | 3 | 2 | — | — | — | — | — | — |
|
||||
| 6th | +3 | ${_.sample(features)} | 3 | 4 | 3 | 3 | — | — | — | — | — | — |
|
||||
| 7th | +3 | ${_.sample(features)} | 3 | 4 | 3 | 3 | 1 | — | — | — | — | — |
|
||||
| 8th | +3 | ${_.sample(features)} | 3 | 4 | 3 | 3 | 2 | — | — | — | — | — |
|
||||
| 9th | +4 | ${_.sample(features)} | 3 | 4 | 3 | 3 | 2 | 1 | — | — | — | — |
|
||||
| 10th | +4 | ${_.sample(features)} | 3 | 4 | 3 | 3 | 2 | 1 | — | — | — | — |
|
||||
| 11th | +4 | ${_.sample(features)} | 4 | 4 | 3 | 3 | 2 | 1 | 1 | — | — | — |
|
||||
| 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){
|
||||
const classname = _.sample(classnames);
|
||||
half : function(snippetClasses){
|
||||
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;
|
||||
let spells = 1;
|
||||
let slots = 2;
|
||||
return `{{${classes}\n##### ${classname} Spellcasting\n` +
|
||||
`| Class | Cantrips | Spells |--- Spells Slots per Spell Level ---||||\n` +
|
||||
`| Level ^| Known ^| Known ^| 1st | 2nd | 3rd | 4th |\n` +
|
||||
`|:------:|:--------:|:-------:|:-------:|:-------:|:-------:|:-------:|\n${
|
||||
_.map(levels, function(levelName, level){
|
||||
const res = [
|
||||
_.pad(levelName, 6),
|
||||
_.pad(cantrips.toString(), 8),
|
||||
_.pad(spells.toString(), 7),
|
||||
drawSlots(slots, 4, 7),
|
||||
].join(' | ');
|
||||
|
||||
cantrips += _.random(0, 1);
|
||||
spells += _.random(0, 1);
|
||||
slots += _.random(0, 1);
|
||||
|
||||
return `| ${res} |`;
|
||||
}).join('\n')}\n}}\n\n`;
|
||||
third : function(snippetClasses){
|
||||
return dedent`
|
||||
{{${snippetClasses}
|
||||
##### ${_.sample(classnames)} Spellcasting
|
||||
| Level | Cantrips | Spells |--- Spells Slots per Spell Level ---||||
|
||||
| ^| Known ^| Known ^| 1st | 2nd | 3rd | 4th |
|
||||
|:-----:|:--------:|:------:|:-------:|:-------:|:-------:|:-------:|
|
||||
| 3rd | 2 | 3 | 2 | — | — | — |
|
||||
| 4th | 2 | 4 | 3 | — | — | — |
|
||||
| 5th | 2 | 4 | 3 | — | — | — |
|
||||
| 6th | 2 | 4 | 3 | — | — | — |
|
||||
| 7th | 2 | 5 | 4 | 2 | — | — |
|
||||
| 8th | 2 | 6 | 4 | 2 | — | — |
|
||||
| 9th | 2 | 6 | 4 | 2 | — | — |
|
||||
| 10th | 3 | 7 | 4 | 3 | — | — |
|
||||
| 11th | 3 | 8 | 4 | 3 | — | — |
|
||||
| 12th | 3 | 8 | 4 | 3 | — | — |
|
||||
| 13th | 3 | 9 | 4 | 3 | 2 | — |
|
||||
| 14th | 3 | 10 | 4 | 3 | 2 | — |
|
||||
| 15th | 3 | 10 | 4 | 3 | 2 | — |
|
||||
| 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`;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -13,7 +13,7 @@ const titles = [
|
||||
'The Living Dead Above the Fearful Cage', 'Bahamut\'s Demonspawn',
|
||||
'Across Gruumsh\'s Elemental Chaos', 'The Blade of Orcus',
|
||||
'Beyond Revenge', 'Brain of Insanity',
|
||||
'Breed Battle!, A New Beginning', 'Evil Lake, A New Beginning',
|
||||
'A New Beginning', 'Evil Lake of the Merfolk',
|
||||
'Invasion of the Gigantic Cat, Part II', 'Kraken War 2020',
|
||||
'The Body Whisperers', 'The Doctor from Heaven',
|
||||
'The Diabolical Tales of the Ape-Women', 'The Doctor Immortal',
|
||||
@@ -23,7 +23,7 @@ const titles = [
|
||||
'Sky of Zelda: The Thunder of Force', 'Core Battle',
|
||||
'Ruby of Atlantis: The Quake of Peace', 'Deadly Amazement III',
|
||||
'Dry Chaos IX', 'Gate Thunder',
|
||||
'Vyse\'s Skies', 'White Greatness III',
|
||||
'Vyse\'s Skies', 'Blue Greatness III',
|
||||
'Yellow Divinity', 'Zidane\'s Ghost'
|
||||
];
|
||||
|
||||
@@ -68,23 +68,89 @@ const footnote = [
|
||||
'In an amazing kingdom, in an age of sorcery and lost souls, eight space pirates quest for freedom.'
|
||||
];
|
||||
|
||||
module.exports = ()=>{
|
||||
return dedent`
|
||||
{{coverPage }}
|
||||
const coverText = [
|
||||
'Embark on a thrilling journey across a vast and varied world, where magic and mystery await you at every turn. Encounter strange creatures and ancient secrets, and forge your own destiny with your choices. The world is yours to shape and explore.',
|
||||
'Join a band of brave adventurers and set out to explore the unknown lands beyond the horizon. Along the way, you’ll face perilous challenges, make new friends and enemies, and uncover a plot that threatens to destroy everything you hold dear. The fate of the world rests in your hands.',
|
||||
'Create your own character and enter a realm of endless possibilities, where you can be whoever you want to be. Whether you prefer to fight, sneak, charm, or craft your way through the game, you’ll find a style that suits you. The only limit is your imagination.',
|
||||
'Experience a rich and immersive story that adapts to your actions and decisions. Every choice you make has consequences, for good or ill. Will you be a hero or a villain? A leader or a follower? A friend or a foe? The choice is yours.',
|
||||
'Dive into a world of epic fantasy and adventure, where you can explore ancient civilizations, dark dungeons, and hidden secrets. Along the way, you’ll meet colorful characters, collect powerful items, and learn new skills. The more you play, the more you’ll discover.',
|
||||
'Explore a vast and dynamic world that changes according to your actions. You can shape the environment, influence the politics, and alter the history of the game world. But be careful, as every change has a ripple effect that may have unforeseen consequences.',
|
||||
'Enter a world of wonder and danger, where you can find allies and enemies among the various races and factions that inhabit it. You can choose to join or oppose any of them, or forge your own path. The game world is alive and responsive to your actions.'
|
||||
];
|
||||
|
||||
{{logo }}
|
||||
module.exports = {
|
||||
|
||||
# ${_.sample(titles)}
|
||||
## ${_.sample(subtitles)}
|
||||
__________
|
||||
front : function() {
|
||||
return dedent`
|
||||
{{frontCover}}
|
||||
|
||||
{{banner HOMEBREW}}
|
||||
{{logo }}
|
||||
|
||||
{{footnote
|
||||
${_.sample(footnote)}
|
||||
}}
|
||||
# ${_.sample(titles)}
|
||||
## ${_.sample(subtitles)}
|
||||
___
|
||||
|
||||

|
||||
{{banner HOMEBREW}}
|
||||
|
||||
\page`;
|
||||
{{footnote
|
||||
${_.sample(footnote)}
|
||||
}}
|
||||
|
||||
{position:absolute,bottom:0,left:0,height:100%}
|
||||
|
||||
\page`;
|
||||
},
|
||||
|
||||
inside : function() {
|
||||
return dedent`
|
||||
{{insideCover}}
|
||||
|
||||
# ${_.sample(titles)}
|
||||
## ${_.sample(subtitles)}
|
||||
___
|
||||
|
||||
{{imageMaskCenter${_.random(1, 16)},--offsetX:0%,--offsetY:0%,--rotation:0
|
||||
{position:absolute,bottom:0,left:0,height:100%}
|
||||
}}
|
||||
|
||||
{{logo }}
|
||||
|
||||
\page`;
|
||||
},
|
||||
|
||||
part : function() {
|
||||
return dedent`
|
||||
{{partCover}}
|
||||
|
||||
# PART X
|
||||
## ${_.sample(subtitles)}
|
||||
|
||||
{{imageMaskEdge${_.random(1, 8)},--offset:10cm,--rotation:180
|
||||
{position:absolute,bottom:0,left:0,height:100%}
|
||||
}}
|
||||
|
||||
\page`;
|
||||
},
|
||||
|
||||
back : function() {
|
||||
return dedent`
|
||||
{{backCover}}
|
||||
|
||||
# ${_.sample(subtitles)}
|
||||
|
||||
${_.sampleSize(coverText, 3).join('\n:\n')}
|
||||
___
|
||||
|
||||
For use with any fantasy roleplaying ruleset. Play the best game of your life!
|
||||
|
||||
{position:absolute,bottom:0,left:0,height:100%}
|
||||
|
||||
{{logo
|
||||

|
||||
|
||||
Homebrewery.Naturalcrit.com
|
||||
}}
|
||||
|
||||
\page`;
|
||||
}
|
||||
};
|
||||
|
||||
85
themes/V3/5ePHB/snippets/index.gen.js
Normal file
@@ -0,0 +1,85 @@
|
||||
const dedent = require('dedent-tabs').default;
|
||||
|
||||
module.exports = ()=>{
|
||||
return dedent`
|
||||
{{index,wide,columns:5;
|
||||
##### Index
|
||||
- Ankhesh-Bort
|
||||
- city map, 7
|
||||
- city watch, 12
|
||||
- guilds, 19
|
||||
- Cheese
|
||||
- types of cheese, 8
|
||||
- cheese-related magic, 14
|
||||
- cheese-related quests, 26-27
|
||||
- Death
|
||||
- appearance, 10
|
||||
- personality, 13
|
||||
- hobbies, 23
|
||||
- Elves
|
||||
- types of elves, 15
|
||||
- elvish magic, 24
|
||||
- elvish curses, 28
|
||||
- Footnotes
|
||||
- types of footnotes, 16-17
|
||||
- footnote rules, 20-21
|
||||
- footnote humor, 29-30
|
||||
- Gods
|
||||
- types of gods, 12
|
||||
- godly interventions, 25
|
||||
- godly conflicts, 31
|
||||
- Heroes
|
||||
- class features, 11-12
|
||||
- heroic deeds, 26-27
|
||||
- Inns
|
||||
- types of inns, 9
|
||||
- inn amenities, 18
|
||||
- Jokes
|
||||
- types of jokes, 11-12
|
||||
- joke delivery, 25
|
||||
- Knives
|
||||
- types of knives, 16-17
|
||||
- knife skills, 22-23
|
||||
- knife fights, 28-29
|
||||
- Luggage
|
||||
- appearance, 10
|
||||
- personality, 13
|
||||
- abilities, 23
|
||||
- Magic
|
||||
- types of magic, 15
|
||||
- magic rules, 24
|
||||
- magic mishaps, 28
|
||||
- Socks
|
||||
- types of socks, 9
|
||||
- sock-related magic (yes, really), 15
|
||||
- sock-related quests (no, really), 26
|
||||
- Trolls
|
||||
- appearance and biology, 11
|
||||
- culture and language, 18
|
||||
- troll rights and activism, 31
|
||||
- Unknown University
|
||||
- history and architecture, 12
|
||||
- faculty and staff, 20
|
||||
- courses and exams, 33
|
||||
- Vampires
|
||||
- types and origins, 13
|
||||
- vampiric powers and weaknesses, 21
|
||||
- vampiric etiquette and politics, 34
|
||||
- Witches
|
||||
- types and traditions, 14
|
||||
- witchcraft and headology, 22
|
||||
- witch trials and tribulations, 35
|
||||
- Xylophones
|
||||
- musical instruments or weapons?, 15
|
||||
- xylophone-related magic and lore, 23
|
||||
- xylophone-related quests and puzzles, 36
|
||||
- Yetis
|
||||
- appearance and behavior, 16
|
||||
- yeti philosophy and religion, 24
|
||||
- yeti encounters and stories, 37
|
||||
- Zombies
|
||||
- types and causes, 17
|
||||
- zombie rights and duties, 25
|
||||
- zombie survival and prevention, 38
|
||||
}}`;
|
||||
};
|
||||
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`;
|
||||
};
|
||||
48
themes/V3/5ePHB/snippets/script.gen.js
Normal file
@@ -0,0 +1,48 @@
|
||||
const _ = require('lodash');
|
||||
const dedent = require('dedent-tabs').default;
|
||||
|
||||
module.exports = {
|
||||
dwarvish : ()=>{
|
||||
return dedent `##### Dwarvish Runes: Sample Alphabet
|
||||
{{runeTable,wide,frame,font-family:Davek
|
||||
| a | b | c | d | e | f | g | h | i | j | k | l | m |
|
||||
|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|
|
||||
| a | b | c | d | e | f | g | h | i | j | k | l | m |
|
||||
:
|
||||
| n | o | p | q | r | s | t | u | v | w | x | y | z |
|
||||
|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|
|
||||
| n | o | p | q | r | s | t | u | v | w | x | y | z |
|
||||
}}\n\n`;
|
||||
},
|
||||
elvish : ()=>{
|
||||
return dedent `##### Elvish Runes: Sample Alphabet
|
||||
{{runeTable,wide,frame,font-family:Rellanic
|
||||
| a | b | c | d | e | f | g | h | i | j | k | l | m |
|
||||
|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|
|
||||
| a | b | c | d | e | f | g | h | i | j | k | l | m |
|
||||
:
|
||||
| n | o | p | q | r | s | t | u | v | w | x | y | z |
|
||||
|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|
|
||||
| n | o | p | q | r | s | t | u | v | w | x | y | z |
|
||||
}}\n\n`;
|
||||
},
|
||||
draconic : ()=>{
|
||||
return dedent `##### Draconic Runes: Sample Alphabet
|
||||
{{runeTable,wide,frame,font-family:Iokharic
|
||||
| a | b | c | d | e | f | g | h | i | j | k | l | m |
|
||||
|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|
|
||||
| a | b | c | d | e | f | g | h | i | j | k | l | m |
|
||||
:
|
||||
| n | o | p | q | r | s | t | u | v | w | x | y | z |
|
||||
|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|
|
||||
| n | o | p | q | r | s | t | u | v | w | x | y | z |
|
||||
}}\n\n`;
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
||||
|
||||
()=>{
|
||||
|
||||
};
|
||||
@@ -29,27 +29,29 @@ const getTOC = (pages)=>{
|
||||
|
||||
const res = [];
|
||||
_.each(pages, (page, pageNum)=>{
|
||||
const lines = page.split('\n');
|
||||
_.each(lines, (line)=>{
|
||||
if(_.startsWith(line, '# ')){
|
||||
const title = line.replace('# ', '');
|
||||
add1(title, pageNum);
|
||||
}
|
||||
if(_.startsWith(line, '## ')){
|
||||
const title = line.replace('## ', '');
|
||||
add2(title, pageNum);
|
||||
}
|
||||
if(_.startsWith(line, '### ')){
|
||||
const title = line.replace('### ', '');
|
||||
add3(title, pageNum);
|
||||
}
|
||||
});
|
||||
if(!page.includes("{{frontCover}}") && !page.includes("{{insideCover}}") && !page.includes("{{partCover}}") && !page.includes("{{backCover}}")) {
|
||||
const lines = page.split('\n');
|
||||
_.each(lines, (line)=>{
|
||||
if(_.startsWith(line, '# ')){
|
||||
const title = line.replace('# ', '');
|
||||
add1(title, pageNum);
|
||||
}
|
||||
if(_.startsWith(line, '## ')){
|
||||
const title = line.replace('## ', '');
|
||||
add2(title, pageNum);
|
||||
}
|
||||
if(_.startsWith(line, '### ')){
|
||||
const title = line.replace('### ', '');
|
||||
add3(title, pageNum);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
return res;
|
||||
};
|
||||
|
||||
module.exports = function(brew){
|
||||
const pages = brew.text.split('\\page');
|
||||
module.exports = function(props){
|
||||
const pages = props.brew.text.split('\\page');
|
||||
const TOC = getTOC(pages);
|
||||
const markdown = _.reduce(TOC, (r, g1, idx1)=>{
|
||||
if(g1.title !== null) {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
const WatercolorGen = require('./snippets/watercolor.gen.js');
|
||||
const ImageMaskGen = require('./snippets/imageMask.gen.js');
|
||||
const FooterGen = require('./snippets/footer.gen.js');
|
||||
const dedent = require('dedent-tabs').default;
|
||||
|
||||
module.exports = [
|
||||
@@ -21,6 +22,53 @@ module.exports = [
|
||||
icon : 'fas fa-file-alt',
|
||||
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',
|
||||
icon : 'fas fa-arrows-alt-v',
|
||||
@@ -101,6 +149,12 @@ module.exports = [
|
||||
icon : 'fas fa-fill-drip',
|
||||
gen : WatercolorGen,
|
||||
},
|
||||
{
|
||||
name : 'Watercolor Center',
|
||||
icon : 'fac mask-center',
|
||||
gen : ImageMaskGen.center,
|
||||
experimental : true,
|
||||
},
|
||||
{
|
||||
name : 'Watercolor Edge',
|
||||
icon : 'fac mask-edge',
|
||||
|
||||
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`;
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -2,6 +2,16 @@ const _ = require('lodash');
|
||||
const dedent = require('dedent-tabs').default;
|
||||
|
||||
module.exports = {
|
||||
center : ()=>{
|
||||
return dedent`
|
||||
{{imageMaskCenter${_.random(1, 16)},--offsetX:0%,--offsetY:0%,--rotation:0
|
||||
{height:100%}
|
||||
}}
|
||||
<!-- Use --offsetX to shift the mask left or right (can use cm instead of %)
|
||||
Use --offsetY to shift the mask up or down
|
||||
Use --rotation to set rotation angle in degrees. -->\n\n`;
|
||||
},
|
||||
|
||||
edge : (side = 'bottom')=>{
|
||||
const rotation = {
|
||||
'bottom' : 0,
|
||||
@@ -10,10 +20,10 @@ module.exports = {
|
||||
'right' : 270
|
||||
}[side];
|
||||
return dedent`
|
||||
{{imageMaskEdge${_.random(1, 8)},--offset:0cm,--rotation:${rotation}
|
||||
{{imageMaskEdge${_.random(1, 8)},--offset:0%,--rotation:${rotation}
|
||||
{height:100%}
|
||||
}}
|
||||
<!-- Use --offset to shift the mask toward or away from the page center.
|
||||
<!-- Use --offset to shift the mask away from page center (can use cm instead of %)
|
||||
Use --rotation to set rotation angle in degrees. -->\n\n`;
|
||||
},
|
||||
|
||||
|
||||
@@ -1,412 +1,465 @@
|
||||
@layer V3_Blank {
|
||||
@import (less) './themes/fonts/5e/fonts.less';
|
||||
@import (less) './themes/assets/assets.less';
|
||||
@import (less) './themes/fonts/5e/fonts.less';
|
||||
@import (less) './themes/assets/assets.less';
|
||||
|
||||
:root {
|
||||
//Colors
|
||||
--HB_Color_Background : #FFFFFF; // White
|
||||
--HB_Color_WatercolorStain : #000000; // Black
|
||||
}
|
||||
:root {
|
||||
//Colors
|
||||
--HB_Color_Background : #FFFFFF; // White
|
||||
--HB_Color_WatercolorStain : #000000; // Black
|
||||
}
|
||||
|
||||
@page { margin: 0; }
|
||||
body {
|
||||
counter-reset : phb-page-numbers;
|
||||
@page { margin: 0; }
|
||||
body {
|
||||
counter-reset : phb-page-numbers;
|
||||
}
|
||||
*{
|
||||
-webkit-print-color-adjust : exact;
|
||||
}
|
||||
|
||||
//*****************************
|
||||
// * MUSTACHE DIVS/SPANS
|
||||
// *****************************/
|
||||
.page {
|
||||
.block {
|
||||
break-inside : avoid;
|
||||
display : inline-block;
|
||||
width : 100%;
|
||||
img {
|
||||
z-index : 0;
|
||||
}
|
||||
}
|
||||
*{
|
||||
-webkit-print-color-adjust : exact;
|
||||
.inline-block {
|
||||
display : inline-block;
|
||||
text-indent : initial;
|
||||
}
|
||||
}
|
||||
|
||||
.useColumns(@multiplier : 1, @fillMode: auto){
|
||||
column-fill : @fillMode;
|
||||
column-count : 2;
|
||||
}
|
||||
.columnWrapper{
|
||||
max-height : 100%;
|
||||
column-span : all;
|
||||
columns : inherit;
|
||||
column-gap : inherit;
|
||||
column-fill : inherit;
|
||||
}
|
||||
.page{
|
||||
.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;
|
||||
z-index : 15;
|
||||
box-sizing : border-box;
|
||||
overflow : hidden;
|
||||
text-rendering : optimizeLegibility;
|
||||
page-break-before : always;
|
||||
page-break-after : always;
|
||||
contain : size;
|
||||
}
|
||||
//*****************************
|
||||
// * BASE
|
||||
// *****************************/
|
||||
.page{
|
||||
p{
|
||||
overflow-wrap : break-word;
|
||||
display : block;
|
||||
}
|
||||
strong{
|
||||
font-weight : bold;
|
||||
}
|
||||
em{
|
||||
font-style : italic;
|
||||
}
|
||||
sup{
|
||||
vertical-align : super;
|
||||
font-size : smaller;
|
||||
line-height : 0;
|
||||
}
|
||||
sub{
|
||||
vertical-align : sub;
|
||||
font-size : smaller;
|
||||
line-height : 0;
|
||||
}
|
||||
ul {
|
||||
list-style-position : outside; //Needed for multiline list items
|
||||
list-style-type : disc;
|
||||
padding-left : 1.4em;
|
||||
}
|
||||
ol {
|
||||
list-style-position : outside;
|
||||
list-style-type : decimal;
|
||||
padding-left : 1.4em;
|
||||
}
|
||||
img{
|
||||
z-index : -1;
|
||||
}
|
||||
|
||||
//*****************************
|
||||
// * MUSTACHE DIVS/SPANS
|
||||
// * HEADERS
|
||||
// *****************************/
|
||||
.page {
|
||||
.block {
|
||||
break-inside : avoid;
|
||||
display : inline-block;
|
||||
width : 100%;
|
||||
}
|
||||
.inline-block {
|
||||
display : inline-block;
|
||||
text-indent : initial;
|
||||
}
|
||||
h1,h2,h3,h4,h5,h6{
|
||||
font-weight : bold;
|
||||
line-height : 1.2em;
|
||||
}
|
||||
|
||||
.useColumns(@multiplier : 1, @fillMode: balance){
|
||||
column-fill : @fillMode;
|
||||
column-count : 2;
|
||||
h1{
|
||||
font-size : 2em;
|
||||
}
|
||||
.columnWrapper{
|
||||
max-height : 100%;
|
||||
column-span : all;
|
||||
columns : inherit;
|
||||
column-gap : inherit;
|
||||
h2{
|
||||
font-size : 1.5em;
|
||||
}
|
||||
.page{
|
||||
.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;
|
||||
z-index : 15;
|
||||
box-sizing : border-box;
|
||||
overflow : hidden;
|
||||
text-rendering : optimizeLegibility;
|
||||
page-break-before : always;
|
||||
page-break-after : always;
|
||||
contain : size;
|
||||
h3{
|
||||
font-size : 1.17em;
|
||||
}
|
||||
//*****************************
|
||||
// * BASE
|
||||
// *****************************/
|
||||
.page{
|
||||
p{
|
||||
overflow-wrap : break-word;
|
||||
display : block;
|
||||
}
|
||||
strong{
|
||||
h4{
|
||||
font-size : 1em;
|
||||
}
|
||||
h5{
|
||||
font-size : 0.83em;
|
||||
}
|
||||
//*****************************
|
||||
// * TABLE
|
||||
// *****************************/
|
||||
table{
|
||||
width : 100%;
|
||||
thead{
|
||||
display : table-row-group;
|
||||
font-weight : bold;
|
||||
}
|
||||
em{
|
||||
font-style : italic;
|
||||
}
|
||||
sup{
|
||||
vertical-align : super;
|
||||
font-size : smaller;
|
||||
line-height : 0;
|
||||
}
|
||||
sub{
|
||||
vertical-align : sub;
|
||||
font-size : smaller;
|
||||
line-height : 0;
|
||||
}
|
||||
ul {
|
||||
list-style-position : outside; //Needed for multiline list items
|
||||
list-style-type : disc;
|
||||
padding-left : 1.4em;
|
||||
}
|
||||
ol {
|
||||
list-style-position : outside;
|
||||
list-style-type : decimal;
|
||||
padding-left : 1.4em;
|
||||
}
|
||||
img{
|
||||
z-index : -1;
|
||||
}
|
||||
|
||||
//*****************************
|
||||
// * HEADERS
|
||||
// *****************************/
|
||||
h1,h2,h3,h4,h5,h6{
|
||||
font-weight : bold;
|
||||
line-height : 1.2em;
|
||||
}
|
||||
h1{
|
||||
font-size : 2em;
|
||||
}
|
||||
h2{
|
||||
font-size : 1.5em;
|
||||
}
|
||||
h3{
|
||||
font-size : 1.17em;
|
||||
}
|
||||
h4{
|
||||
font-size : 1em;
|
||||
}
|
||||
h5{
|
||||
font-size : 0.83em;
|
||||
}
|
||||
//*****************************
|
||||
// * TABLE
|
||||
// *****************************/
|
||||
table{
|
||||
width : 100%;
|
||||
thead{
|
||||
display : table-row-group;
|
||||
font-weight : bold;
|
||||
}
|
||||
}
|
||||
div:not(.columnWrapper) > table + table { // Side-by-side tables should not
|
||||
margin-top : 0; // have vertical spacing.
|
||||
}
|
||||
|
||||
//************************************
|
||||
// * CODE BLOCKS
|
||||
// ************************************/
|
||||
code{
|
||||
font-family : "Courier New", Courier, monospace;
|
||||
white-space : pre-wrap;
|
||||
overflow-wrap : break-word;
|
||||
}
|
||||
|
||||
pre code{
|
||||
width : 100%;
|
||||
display : inline-block;
|
||||
}
|
||||
//*****************************
|
||||
// * EXTRAS
|
||||
// *****************************/
|
||||
.columnSplit {
|
||||
visibility : hidden;
|
||||
-webkit-column-break-after : always;
|
||||
break-after : always;
|
||||
-moz-column-break-after : always;
|
||||
margin-top : 0;
|
||||
& + * {
|
||||
margin-top : 0;
|
||||
}
|
||||
}
|
||||
//Avoid breaking up
|
||||
blockquote,table{
|
||||
z-index : 15;
|
||||
-webkit-column-break-inside : avoid;
|
||||
page-break-inside : avoid;
|
||||
break-inside : avoid;
|
||||
}
|
||||
// Nested lists
|
||||
ul ul,ol ol,ul ol,ol ul{
|
||||
margin-bottom : 0px;
|
||||
margin-left : 1.5em;
|
||||
}
|
||||
li{
|
||||
-webkit-column-break-inside : avoid;
|
||||
page-break-inside : avoid;
|
||||
break-inside : avoid;
|
||||
}
|
||||
|
||||
/* Watermark */
|
||||
.watermark {
|
||||
display : grid !important;
|
||||
place-items : center;
|
||||
justify-content : center;
|
||||
position : absolute;
|
||||
margin : 0;
|
||||
top : 0;
|
||||
left : 0;
|
||||
width : 100%;
|
||||
height : 100%;
|
||||
font-size : 120px;
|
||||
text-transform : uppercase;
|
||||
color : black;
|
||||
mix-blend-mode : overlay;
|
||||
opacity : 30%;
|
||||
transform : rotate(-45deg);
|
||||
z-index : 500;
|
||||
p {
|
||||
margin-bottom : none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Watercolor */
|
||||
[class*="watercolor"] {
|
||||
position : absolute;
|
||||
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 */
|
||||
-webkit-mask-image : var(--wc);
|
||||
-webkit-mask-size : contain;
|
||||
-webkit-mask-repeat : no-repeat;
|
||||
mask-image : var(--wc);
|
||||
mask-size : contain;
|
||||
mask-repeat : no-repeat;
|
||||
background-size : cover;
|
||||
background-color : var(--HB_Color_WatercolorStain); /*default color*/
|
||||
--wc : @watercolor1; /*default image*/
|
||||
z-index : -2;
|
||||
}
|
||||
|
||||
.watercolor1 { --wc : @watercolor1; }
|
||||
.watercolor2 { --wc : @watercolor2; }
|
||||
.watercolor3 { --wc : @watercolor3; }
|
||||
.watercolor4 { --wc : @watercolor4; }
|
||||
.watercolor5 { --wc : @watercolor5; }
|
||||
.watercolor6 { --wc : @watercolor6; }
|
||||
.watercolor7 { --wc : @watercolor7; }
|
||||
.watercolor8 { --wc : @watercolor8; }
|
||||
.watercolor9 { --wc : @watercolor9; }
|
||||
.watercolor10 { --wc : @watercolor10; }
|
||||
.watercolor11 { --wc : @watercolor11; }
|
||||
.watercolor12 { --wc : @watercolor12; }
|
||||
|
||||
/* Image Masks */
|
||||
[class*="imageMask"] {
|
||||
position : absolute;
|
||||
height : 200%;
|
||||
width : 200%;
|
||||
left : 50%;
|
||||
bottom : 50%;
|
||||
--rotation : 0;
|
||||
--revealer : none;
|
||||
--checkerboard : none;
|
||||
--scaleX : 1;
|
||||
--scaleY : 1;
|
||||
-webkit-mask-image : var(--wc), var(--revealer);
|
||||
-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-position : 50% calc(50% - var(--offset));
|
||||
mask-image : var(--wc);
|
||||
mask-repeat : repeat-x;
|
||||
mask-size : 50%;
|
||||
mask-position : 50% calc(50% - var(--offset));
|
||||
background-image : var(--checkerboard);
|
||||
background-size : 20px;
|
||||
z-index : -1;
|
||||
transform : translateY(50%) translateX(-50%) rotate(calc(1deg * var(--rotation))) scaleX(var(--scaleX)) scaleY(var(--scaleY));
|
||||
transition : transform 2s;
|
||||
& > p:has(img) {
|
||||
position : absolute;
|
||||
width : 50%;
|
||||
height : 50%;
|
||||
bottom : 50%;
|
||||
left : 50%;
|
||||
transform : translateX(-50%) translateY(50%) rotate(calc(-1deg * var(--rotation))) scaleX(calc(1 / var(--scaleX))) scaleY(calc(1 / var(--scaleY)));
|
||||
transition : transform 2s;
|
||||
}
|
||||
& img {
|
||||
position : absolute;
|
||||
display : block;
|
||||
bottom : 0;
|
||||
}
|
||||
&.bottom {
|
||||
--rotation : 0;
|
||||
& img {bottom: 0;}
|
||||
}
|
||||
&.top {
|
||||
--rotation : 180;
|
||||
& img {top: 0;}
|
||||
}
|
||||
&.left {
|
||||
--rotation : 90;
|
||||
& img {left: 0;}
|
||||
}
|
||||
&.right {
|
||||
--rotation : -90;
|
||||
& img {right: 0;}
|
||||
}
|
||||
&.revealImage {
|
||||
--revealer : linear-gradient(0deg, rgba(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
|
||||
}
|
||||
}
|
||||
|
||||
.imageMaskEdge1 { --wc : url(/assets/waterColorMasks/edge/0001.webp); }
|
||||
.imageMaskEdge2 { --wc : url(/assets/waterColorMasks/edge/0002.webp); }
|
||||
.imageMaskEdge3 { --wc : url(/assets/waterColorMasks/edge/0003.webp); }
|
||||
.imageMaskEdge4 { --wc : url(/assets/waterColorMasks/edge/0004.webp); }
|
||||
.imageMaskEdge5 { --wc : url(/assets/waterColorMasks/edge/0005.webp); }
|
||||
.imageMaskEdge6 { --wc : url(/assets/waterColorMasks/edge/0006.webp); }
|
||||
.imageMaskEdge7 { --wc : url(/assets/waterColorMasks/edge/0007.webp); }
|
||||
.imageMaskEdge8 { --wc : url(/assets/waterColorMasks/edge/0008.webp); }
|
||||
|
||||
[class*="imageMaskCorner"] {
|
||||
height : 200%;
|
||||
width : 200%;
|
||||
left : calc(-50% + var(--offsetX));
|
||||
bottom : calc(-50% + var(--offsetY));
|
||||
-webkit-mask-image : var(--wc), var(--revealer);
|
||||
-webkit-mask-repeat : no-repeat;
|
||||
-webkit-mask-size : 100% 100%; //Scale both dimensions to fit page size
|
||||
-webkit-mask-position : 50% 50%;
|
||||
mask-image : var(--wc), var(--revealer);
|
||||
mask-repeat : no-repeat;
|
||||
mask-size : 100% 100%; //Scale both dimensions to fit page size
|
||||
mask-position : 50% 50%;
|
||||
transform : rotate(calc(1deg * var(--rotation))) scaleX(var(--scaleX)) scaleY(var(--scaleY));;
|
||||
& > p:has(img) {
|
||||
width : 50%;
|
||||
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)))
|
||||
rotate(calc(-1deg * var(--rotation)))
|
||||
translateX(calc(-1 * var(--offsetX)))
|
||||
translateY(calc(1 * var(--offsetY)));
|
||||
}
|
||||
}
|
||||
|
||||
.imageMaskCorner1 { --wc : url(/assets/waterColorMasks/corner/0001.webp); }
|
||||
.imageMaskCorner2 { --wc : url(/assets/waterColorMasks/corner/0002.webp); }
|
||||
.imageMaskCorner3 { --wc : url(/assets/waterColorMasks/corner/0003.webp); }
|
||||
.imageMaskCorner4 { --wc : url(/assets/waterColorMasks/corner/0004.webp); }
|
||||
.imageMaskCorner5 { --wc : url(/assets/waterColorMasks/corner/0005.webp); }
|
||||
.imageMaskCorner6 { --wc : url(/assets/waterColorMasks/corner/0006.webp); }
|
||||
.imageMaskCorner7 { --wc : url(/assets/waterColorMasks/corner/0007.webp); }
|
||||
.imageMaskCorner8 { --wc : url(/assets/waterColorMasks/corner/0008.webp); }
|
||||
.imageMaskCorner9 { --wc : url(/assets/waterColorMasks/corner/0009.webp); }
|
||||
.imageMaskCorner10 { --wc : url(/assets/waterColorMasks/corner/0010.webp); }
|
||||
.imageMaskCorner11 { --wc : url(/assets/waterColorMasks/corner/0011.webp); }
|
||||
.imageMaskCorner12 { --wc : url(/assets/waterColorMasks/corner/0012.webp); }
|
||||
.imageMaskCorner13 { --wc : url(/assets/waterColorMasks/corner/0013.webp); }
|
||||
.imageMaskCorner14 { --wc : url(/assets/waterColorMasks/corner/0014.webp); }
|
||||
.imageMaskCorner15 { --wc : url(/assets/waterColorMasks/corner/0015.webp); }
|
||||
.imageMaskCorner16 { --wc : url(/assets/waterColorMasks/corner/0016.webp); }
|
||||
.imageMaskCorner17 { --wc : url(/assets/waterColorMasks/corner/0017.webp); }
|
||||
.imageMaskCorner18 { --wc : url(/assets/waterColorMasks/corner/0018.webp); }
|
||||
.imageMaskCorner19 { --wc : url(/assets/waterColorMasks/corner/0019.webp); }
|
||||
.imageMaskCorner20 { --wc : url(/assets/waterColorMasks/corner/0020.webp); }
|
||||
.imageMaskCorner21 { --wc : url(/assets/waterColorMasks/corner/0021.webp); }
|
||||
.imageMaskCorner22 { --wc : url(/assets/waterColorMasks/corner/0022.webp); }
|
||||
.imageMaskCorner23 { --wc : url(/assets/waterColorMasks/corner/0023.webp); }
|
||||
.imageMaskCorner24 { --wc : url(/assets/waterColorMasks/corner/0024.webp); }
|
||||
.imageMaskCorner25 { --wc : url(/assets/waterColorMasks/corner/0025.webp); }
|
||||
.imageMaskCorner26 { --wc : url(/assets/waterColorMasks/corner/0026.webp); }
|
||||
.imageMaskCorner27 { --wc : url(/assets/waterColorMasks/corner/0027.webp); }
|
||||
.imageMaskCorner28 { --wc : url(/assets/waterColorMasks/corner/0028.webp); }
|
||||
.imageMaskCorner29 { --wc : url(/assets/waterColorMasks/corner/0029.webp); }
|
||||
.imageMaskCorner30 { --wc : url(/assets/waterColorMasks/corner/0030.webp); }
|
||||
.imageMaskCorner31 { --wc : url(/assets/waterColorMasks/corner/0031.webp); }
|
||||
.imageMaskCorner32 { --wc : url(/assets/waterColorMasks/corner/0032.webp); }
|
||||
.imageMaskCorner33 { --wc : url(/assets/waterColorMasks/corner/0033.webp); }
|
||||
.imageMaskCorner34 { --wc : url(/assets/waterColorMasks/corner/0034.webp); }
|
||||
.imageMaskCorner35 { --wc : url(/assets/waterColorMasks/corner/0035.webp); }
|
||||
.imageMaskCorner36 { --wc : url(/assets/waterColorMasks/corner/0036.webp); }
|
||||
.imageMaskCorner37 { --wc : url(/assets/waterColorMasks/corner/0037.webp); }
|
||||
}
|
||||
div:not(.columnWrapper) > table + table { // Side-by-side tables should not
|
||||
margin-top : 0; // have vertical spacing.
|
||||
}
|
||||
|
||||
//*****************************
|
||||
// * DEFINITION LISTS
|
||||
// *****************************/
|
||||
.page {
|
||||
dl {
|
||||
padding-left : 1em;
|
||||
white-space : pre-line;
|
||||
}
|
||||
dt {
|
||||
display : inline;
|
||||
margin-right : 0.5ch;
|
||||
margin-left : -1em;
|
||||
}
|
||||
dd {
|
||||
display : inline;
|
||||
margin-left : 0;
|
||||
text-indent : 0;
|
||||
}
|
||||
//************************************
|
||||
// * CODE BLOCKS
|
||||
// ************************************/
|
||||
code{
|
||||
font-family : "Courier New", Courier, monospace;
|
||||
white-space : pre-wrap;
|
||||
overflow-wrap : break-word;
|
||||
}
|
||||
|
||||
pre code{
|
||||
width : 100%;
|
||||
display : inline-block;
|
||||
}
|
||||
//*****************************
|
||||
// * BLANK LINE
|
||||
// * EXTRAS
|
||||
// *****************************/
|
||||
.page {
|
||||
.blank {
|
||||
height : 1em;
|
||||
.columnSplit {
|
||||
visibility : hidden;
|
||||
-webkit-column-break-after : always;
|
||||
break-after : always;
|
||||
-moz-column-break-after : always;
|
||||
margin-top : 0;
|
||||
& + * {
|
||||
margin-top : 0;
|
||||
& + * {
|
||||
margin-top : 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
//Avoid breaking up
|
||||
blockquote,table{
|
||||
z-index : 15;
|
||||
-webkit-column-break-inside : avoid;
|
||||
page-break-inside : avoid;
|
||||
break-inside : avoid;
|
||||
}
|
||||
// Nested lists
|
||||
ul ul,ol ol,ul ol,ol ul{
|
||||
margin-bottom : 0px;
|
||||
margin-left : 1.5em;
|
||||
}
|
||||
li{
|
||||
-webkit-column-break-inside : avoid;
|
||||
page-break-inside : avoid;
|
||||
break-inside : avoid;
|
||||
}
|
||||
|
||||
/* Watermark */
|
||||
.watermark {
|
||||
display : grid !important;
|
||||
place-items : center;
|
||||
justify-content : center;
|
||||
position : absolute;
|
||||
margin : 0;
|
||||
top : 0;
|
||||
left : 0;
|
||||
width : 100%;
|
||||
height : 100%;
|
||||
font-size : 120px;
|
||||
text-transform : uppercase;
|
||||
mix-blend-mode : overlay;
|
||||
opacity : 30%;
|
||||
transform : rotate(-45deg);
|
||||
z-index : 500;
|
||||
p {
|
||||
margin-bottom : none;
|
||||
}
|
||||
}
|
||||
|
||||
//*****************************
|
||||
// * WIDE
|
||||
// *****************************/
|
||||
.page {
|
||||
.wide{
|
||||
column-span : all;
|
||||
display : block;
|
||||
margin-bottom : 1em;
|
||||
&+* {
|
||||
margin-top : 0;
|
||||
}
|
||||
/* Watercolor */
|
||||
[class*="watercolor"] {
|
||||
position : absolute;
|
||||
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 */
|
||||
-webkit-mask-image : var(--wc);
|
||||
-webkit-mask-size : contain;
|
||||
-webkit-mask-repeat : no-repeat;
|
||||
mask-image : var(--wc);
|
||||
mask-size : contain;
|
||||
mask-repeat : no-repeat;
|
||||
background-size : cover;
|
||||
background-color : var(--HB_Color_WatercolorStain); /*default color*/
|
||||
--wc : @watercolor1; /*default image*/
|
||||
z-index : -2;
|
||||
}
|
||||
|
||||
.watercolor1 { --wc : @watercolor1; }
|
||||
.watercolor2 { --wc : @watercolor2; }
|
||||
.watercolor3 { --wc : @watercolor3; }
|
||||
.watercolor4 { --wc : @watercolor4; }
|
||||
.watercolor5 { --wc : @watercolor5; }
|
||||
.watercolor6 { --wc : @watercolor6; }
|
||||
.watercolor7 { --wc : @watercolor7; }
|
||||
.watercolor8 { --wc : @watercolor8; }
|
||||
.watercolor9 { --wc : @watercolor9; }
|
||||
.watercolor10 { --wc : @watercolor10; }
|
||||
.watercolor11 { --wc : @watercolor11; }
|
||||
.watercolor12 { --wc : @watercolor12; }
|
||||
|
||||
/* Image Masks */
|
||||
|
||||
[class*="imageMask"] {
|
||||
position : absolute;
|
||||
height : 200%;
|
||||
width : 200%;
|
||||
left : 50%;
|
||||
bottom : 50%;
|
||||
--rotation : 0;
|
||||
--revealer : none;
|
||||
--checkerboard : none;
|
||||
--scaleX : 1;
|
||||
--scaleY : 1;
|
||||
-webkit-mask-image : var(--wc), var(--revealer);
|
||||
-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-position : 50% calc(50% - var(--offset));
|
||||
mask-image : var(--wc);
|
||||
mask-repeat : repeat-x;
|
||||
mask-size : 50%;
|
||||
mask-position : 50% calc(50% - var(--offset));
|
||||
background-image : var(--checkerboard);
|
||||
background-size : 20px;
|
||||
z-index : -1;
|
||||
transform : translateY(50%) translateX(-50%) rotate(calc(1deg * var(--rotation))) scaleX(var(--scaleX)) scaleY(var(--scaleY));
|
||||
& > p:has(img) {
|
||||
position : absolute;
|
||||
width : 50%;
|
||||
height : 50%;
|
||||
bottom : 50%;
|
||||
left : 50%;
|
||||
transform : translateX(-50%) translateY(50%) rotate(calc(-1deg * var(--rotation))) scaleX(calc(1 / var(--scaleX))) scaleY(calc(1 / var(--scaleY)));
|
||||
}
|
||||
& img {
|
||||
position : absolute;
|
||||
display : block;
|
||||
bottom : 0;
|
||||
}
|
||||
&.bottom {
|
||||
--rotation : 0;
|
||||
& img {bottom: 0;}
|
||||
}
|
||||
&.top {
|
||||
--rotation : 180;
|
||||
& img {top: 0;}
|
||||
}
|
||||
&.left {
|
||||
--rotation : 90;
|
||||
& img {left: 0;}
|
||||
}
|
||||
&.right {
|
||||
--rotation : -90;
|
||||
& img {right: 0;}
|
||||
}
|
||||
&.revealImage {
|
||||
--revealer : linear-gradient(0deg, rgba(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
|
||||
}
|
||||
}
|
||||
|
||||
.imageMaskEdge {
|
||||
&1 { --wc : url(/assets/waterColorMasks/edge/0001.webp); }
|
||||
&2 { --wc : url(/assets/waterColorMasks/edge/0002.webp); }
|
||||
&3 { --wc : url(/assets/waterColorMasks/edge/0003.webp); }
|
||||
&4 { --wc : url(/assets/waterColorMasks/edge/0004.webp); }
|
||||
&5 { --wc : url(/assets/waterColorMasks/edge/0005.webp); }
|
||||
&6 { --wc : url(/assets/waterColorMasks/edge/0006.webp); }
|
||||
&7 { --wc : url(/assets/waterColorMasks/edge/0007.webp); }
|
||||
&8 { --wc : url(/assets/waterColorMasks/edge/0008.webp); }
|
||||
}
|
||||
|
||||
[class*="imageMaskCenter"] {
|
||||
width : 100%;
|
||||
height : 100%;
|
||||
left : calc(var(--offsetX));
|
||||
bottom : calc(var(--offsetY));
|
||||
-webkit-mask-image : var(--wc), var(--revealer);
|
||||
-webkit-mask-repeat : no-repeat;
|
||||
-webkit-mask-size : 100% 100%; //Scale both dimensions to fit page size
|
||||
-webkit-mask-position : 0% 0%;
|
||||
mask-image : var(--wc), var(--revealer);
|
||||
mask-repeat : no-repeat;
|
||||
mask-size : 100% 100%; //Scale both dimensions to fit page size
|
||||
mask-position : 50% 50%;
|
||||
transform : rotate(calc(1deg * var(--rotation))) scaleX(var(--scaleX)) scaleY(var(--scaleY));
|
||||
|
||||
& > p:has(img) {
|
||||
position : absolute;
|
||||
width : 100%;
|
||||
height : 100%;
|
||||
bottom : 0;
|
||||
left : 0;
|
||||
transform : unset;
|
||||
transform : scaleX(calc(1 / var(--scaleX))) scaleY(calc(1 / var(--scaleY)))
|
||||
rotate(calc(-1deg * var(--rotation)))
|
||||
translateX(calc(-1 * var(--offsetX)))
|
||||
translateY(calc(1 * var(--offsetY)));
|
||||
}
|
||||
}
|
||||
|
||||
.imageMaskCenter {
|
||||
&1 { --wc : url(/assets/waterColorMasks/center/0001.webp); }
|
||||
&2 { --wc : url(/assets/waterColorMasks/center/0002.webp); }
|
||||
&3 { --wc : url(/assets/waterColorMasks/center/0003.webp); }
|
||||
&4 { --wc : url(/assets/waterColorMasks/center/0004.webp); }
|
||||
&5 { --wc : url(/assets/waterColorMasks/center/0005.webp); }
|
||||
&6 { --wc : url(/assets/waterColorMasks/center/0006.webp); }
|
||||
&7 { --wc : url(/assets/waterColorMasks/center/0007.webp); }
|
||||
&8 { --wc : url(/assets/waterColorMasks/center/0008.webp); }
|
||||
&9 { --wc : url(/assets/waterColorMasks/center/0009.webp); }
|
||||
&10 { --wc : url(/assets/waterColorMasks/center/0010.webp); }
|
||||
&11 { --wc : url(/assets/waterColorMasks/center/0011.webp); }
|
||||
&12 { --wc : url(/assets/waterColorMasks/center/0012.webp); }
|
||||
&13 { --wc : url(/assets/waterColorMasks/center/0013.webp); }
|
||||
&14 { --wc : url(/assets/waterColorMasks/center/0014.webp); }
|
||||
&15 { --wc : url(/assets/waterColorMasks/center/0015.webp); }
|
||||
&16 { --wc : url(/assets/waterColorMasks/center/0016.webp); }
|
||||
&special { --wc : url(/assets/waterColorMasks/center/special.webp); }
|
||||
}
|
||||
|
||||
|
||||
[class*="imageMaskCorner"] {
|
||||
height : 200%;
|
||||
width : 200%;
|
||||
left : calc(-50% + var(--offsetX));
|
||||
bottom : calc(-50% + var(--offsetY));
|
||||
-webkit-mask-image : var(--wc), var(--revealer);
|
||||
-webkit-mask-repeat : no-repeat;
|
||||
-webkit-mask-size : 100% 100%; //Scale both dimensions to fit page size
|
||||
-webkit-mask-position : 50% 50%;
|
||||
mask-image : var(--wc), var(--revealer);
|
||||
mask-repeat : no-repeat;
|
||||
mask-size : 100% 100%; //Scale both dimensions to fit page size
|
||||
mask-position : 50% 50%;
|
||||
transform : rotate(calc(1deg * var(--rotation))) scaleX(var(--scaleX)) scaleY(var(--scaleY));
|
||||
& > p:has(img) {
|
||||
width : 50%;
|
||||
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)))
|
||||
rotate(calc(-1deg * var(--rotation)))
|
||||
translateX(calc(-1 * var(--offsetX)))
|
||||
translateY(calc(1 * var(--offsetY)));
|
||||
}
|
||||
}
|
||||
.imageMaskCorner {
|
||||
&1 { --wc : url(/assets/waterColorMasks/corner/0001.webp); }
|
||||
&2 { --wc : url(/assets/waterColorMasks/corner/0002.webp); }
|
||||
&3 { --wc : url(/assets/waterColorMasks/corner/0003.webp); }
|
||||
&4 { --wc : url(/assets/waterColorMasks/corner/0004.webp); }
|
||||
&5 { --wc : url(/assets/waterColorMasks/corner/0005.webp); }
|
||||
&6 { --wc : url(/assets/waterColorMasks/corner/0006.webp); }
|
||||
&7 { --wc : url(/assets/waterColorMasks/corner/0007.webp); }
|
||||
&8 { --wc : url(/assets/waterColorMasks/corner/0008.webp); }
|
||||
&9 { --wc : url(/assets/waterColorMasks/corner/0009.webp); }
|
||||
&10 { --wc : url(/assets/waterColorMasks/corner/0010.webp); }
|
||||
&11 { --wc : url(/assets/waterColorMasks/corner/0011.webp); }
|
||||
&12 { --wc : url(/assets/waterColorMasks/corner/0012.webp); }
|
||||
&13 { --wc : url(/assets/waterColorMasks/corner/0013.webp); }
|
||||
&14 { --wc : url(/assets/waterColorMasks/corner/0014.webp); }
|
||||
&15 { --wc : url(/assets/waterColorMasks/corner/0015.webp); }
|
||||
&16 { --wc : url(/assets/waterColorMasks/corner/0016.webp); }
|
||||
&17 { --wc : url(/assets/waterColorMasks/corner/0017.webp); }
|
||||
&18 { --wc : url(/assets/waterColorMasks/corner/0018.webp); }
|
||||
&19 { --wc : url(/assets/waterColorMasks/corner/0019.webp); }
|
||||
&20 { --wc : url(/assets/waterColorMasks/corner/0020.webp); }
|
||||
&21 { --wc : url(/assets/waterColorMasks/corner/0021.webp); }
|
||||
&22 { --wc : url(/assets/waterColorMasks/corner/0022.webp); }
|
||||
&23 { --wc : url(/assets/waterColorMasks/corner/0023.webp); }
|
||||
&24 { --wc : url(/assets/waterColorMasks/corner/0024.webp); }
|
||||
&25 { --wc : url(/assets/waterColorMasks/corner/0025.webp); }
|
||||
&26 { --wc : url(/assets/waterColorMasks/corner/0026.webp); }
|
||||
&27 { --wc : url(/assets/waterColorMasks/corner/0027.webp); }
|
||||
&28 { --wc : url(/assets/waterColorMasks/corner/0028.webp); }
|
||||
&29 { --wc : url(/assets/waterColorMasks/corner/0029.webp); }
|
||||
&30 { --wc : url(/assets/waterColorMasks/corner/0030.webp); }
|
||||
&31 { --wc : url(/assets/waterColorMasks/corner/0031.webp); }
|
||||
&32 { --wc : url(/assets/waterColorMasks/corner/0032.webp); }
|
||||
&33 { --wc : url(/assets/waterColorMasks/corner/0033.webp); }
|
||||
&34 { --wc : url(/assets/waterColorMasks/corner/0034.webp); }
|
||||
&35 { --wc : url(/assets/waterColorMasks/corner/0035.webp); }
|
||||
&36 { --wc : url(/assets/waterColorMasks/corner/0036.webp); }
|
||||
&37 { --wc : url(/assets/waterColorMasks/corner/0037.webp); }
|
||||
}
|
||||
}
|
||||
|
||||
//*****************************
|
||||
// * DEFINITION LISTS
|
||||
// *****************************/
|
||||
.page {
|
||||
dl {
|
||||
padding-left : 1em;
|
||||
white-space : pre-line;
|
||||
}
|
||||
dt {
|
||||
display : inline;
|
||||
margin-right : 0.5ch;
|
||||
margin-left : -1em;
|
||||
}
|
||||
dd {
|
||||
display : inline;
|
||||
margin-left : 0;
|
||||
text-indent : 0;
|
||||
}
|
||||
}
|
||||
|
||||
//*****************************
|
||||
// * BLANK LINE
|
||||
// *****************************/
|
||||
.page {
|
||||
.blank {
|
||||
height : 1em;
|
||||
margin-top : 0;
|
||||
& + * {
|
||||
margin-top : 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//*****************************
|
||||
// * WIDE
|
||||
// *****************************/
|
||||
.page {
|
||||
.wide{
|
||||
column-span : all;
|
||||
display : block;
|
||||
margin-bottom : 1em;
|
||||
&+* {
|
||||
margin-top : 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,11 @@
|
||||
@naturalCritLogo : url('/assets/naturalCritLogo.svg');
|
||||
@coverPageBanner : url('/assets/coverPageBanner.svg');
|
||||
@horizontalRule : url('/assets/horizontalRule.svg');
|
||||
@partCoverHeaderPHB : url('/assets/partCoverHeaderPHB.png');
|
||||
@partCoverHeaderDMG : url('/assets/partCoverHeaderDMG.svg');
|
||||
@insideCoverMask : url('/assets/insideCoverMask.png');
|
||||
@backCover : url('/assets/backCover.png');
|
||||
@scriptBorder : url('/assets/scriptBorder.png');
|
||||
|
||||
// Watercolor Images
|
||||
@watercolor1 : url('/assets/watercolor/watercolor1.png');
|
||||
|
||||