mirror of
https://github.com/naturalcrit/homebrewery.git
synced 2026-01-23 18:43:02 +00:00
Compare commits
655 Commits
non-conten
...
editBasePa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
90ceb52ffc | ||
|
|
6f9caf0590 | ||
|
|
253dbb358b | ||
|
|
719edd82c5 | ||
|
|
63d957fdc6 | ||
|
|
7751c0e37b | ||
|
|
990bf80b59 | ||
|
|
f16598f238 | ||
|
|
579e9e0ec5 | ||
|
|
f6629f2f9e | ||
|
|
b87c78474d | ||
|
|
958d282a58 | ||
|
|
7e56ae2019 | ||
|
|
ebca50ed4b | ||
|
|
bfd14757c2 | ||
|
|
3626ed5a31 | ||
|
|
d385bacdd6 | ||
|
|
cbbb2c0a7d | ||
|
|
fbe637ff82 | ||
|
|
82bd16c623 | ||
|
|
d1f13af67b | ||
|
|
b6c03e88b8 | ||
|
|
b587d17397 | ||
|
|
0a02f910f8 | ||
|
|
ddfa06e76b | ||
|
|
0c2b1fec04 | ||
|
|
6de7a64acd | ||
|
|
b9fe4c3901 | ||
|
|
5ae01862e5 | ||
|
|
398df7a061 | ||
|
|
443b0f6a37 | ||
|
|
544175b994 | ||
|
|
955602e7ee | ||
|
|
90e577dd3f | ||
|
|
828208aadb | ||
|
|
973e071e93 | ||
|
|
f9e7aa355d | ||
|
|
24dfd41714 | ||
|
|
638e54535d | ||
|
|
cbc6956221 | ||
|
|
248d2038ec | ||
|
|
5b66175b8c | ||
|
|
552aa7d41a | ||
|
|
b0a108b543 | ||
|
|
505d2840c0 | ||
|
|
41ff50fefe | ||
|
|
2fbcc84a50 | ||
|
|
45e4d27c0a | ||
|
|
77bf3ffc6f | ||
|
|
bc045ec6c9 | ||
|
|
6390ea076a | ||
|
|
6affcb587d | ||
|
|
7787afabff | ||
|
|
fb4a8e5cf1 | ||
|
|
8432a6e367 | ||
|
|
90ee08de42 | ||
|
|
40839b18e4 | ||
|
|
677c02cfa5 | ||
|
|
a7a8803e9d | ||
|
|
5fbc111db7 | ||
|
|
5edea7d0f4 | ||
|
|
d3a9d813c9 | ||
|
|
fc475b2a7e | ||
|
|
76b76b3bb6 | ||
|
|
22ef3cbebc | ||
|
|
9da8a17053 | ||
|
|
7cadbfbd7b | ||
|
|
98b9e86787 | ||
|
|
489b4b2694 | ||
|
|
8d279260c2 | ||
|
|
7c08c430d0 | ||
|
|
45689d119e | ||
|
|
c5805af935 | ||
|
|
b2c4bb7082 | ||
|
|
68460447dc | ||
|
|
440c7beff6 | ||
|
|
c7610cf0f8 | ||
|
|
7f3a818558 | ||
|
|
bc82afa5b2 | ||
|
|
abef250631 | ||
|
|
1794e96d50 | ||
|
|
25f25da499 | ||
|
|
aa15bdaacb | ||
|
|
7ba7991631 | ||
|
|
0e1ac26999 | ||
|
|
f49fed8c35 | ||
|
|
a8236fbab4 | ||
|
|
daf4eceedd | ||
|
|
a02361ee65 | ||
|
|
81e20f032e | ||
|
|
1d92b98568 | ||
|
|
0f4157d084 | ||
|
|
4dcc3749d8 | ||
|
|
8f058d56f2 | ||
|
|
d192a064d6 | ||
|
|
cccb531e17 | ||
|
|
6414e73e7d | ||
|
|
41daf8d172 | ||
|
|
4c897fdeb5 | ||
|
|
89ce4de354 | ||
|
|
43095507ee | ||
|
|
eb7fbbe018 | ||
|
|
869958ec38 | ||
|
|
99b90e0998 | ||
|
|
57a48100d3 | ||
|
|
8538e4fadb | ||
|
|
9a002511a3 | ||
|
|
3fa3a52e05 | ||
|
|
4fe920dac3 | ||
|
|
71dff5fbf9 | ||
|
|
26419d2ccb | ||
|
|
f02fe2d8f3 | ||
|
|
318fb53eb2 | ||
|
|
6a32b7427b | ||
|
|
5886bd65e5 | ||
|
|
9c5f80cbdb | ||
|
|
79d8956c4f | ||
|
|
2e491b3556 | ||
|
|
d9a8afa272 | ||
|
|
209195202c | ||
|
|
64235c844a | ||
|
|
5d000a4599 | ||
|
|
380e593b42 | ||
|
|
169f089d08 | ||
|
|
b3977ed141 | ||
|
|
9800561de7 | ||
|
|
166af08e6a | ||
|
|
48f17f7c5e | ||
|
|
87c9f52222 | ||
|
|
c80b7ffd66 | ||
|
|
5f16ce3dbd | ||
|
|
b5ff26f857 | ||
|
|
578b01bbb1 | ||
|
|
67467e0099 | ||
|
|
da21bf20f9 | ||
|
|
df7fcf1e5f | ||
|
|
702ece6671 | ||
|
|
1008321957 | ||
|
|
b547486c48 | ||
|
|
e1e661976d | ||
|
|
7bdeeee9ef | ||
|
|
becf35d336 | ||
|
|
d7585767c9 | ||
|
|
f9bb6209b7 | ||
|
|
13702a2f62 | ||
|
|
a6a684c89e | ||
|
|
862fa7de89 | ||
|
|
b671cf7b02 | ||
|
|
d5dbe0b4ba | ||
|
|
c2cf695c17 | ||
|
|
6d0d6f08b5 | ||
|
|
77dcc9b433 | ||
|
|
5f2f3a6f3d | ||
|
|
bbb812cb06 | ||
|
|
5648e55774 | ||
|
|
c051580545 | ||
|
|
6e72fe2600 | ||
|
|
03602ae1e0 | ||
|
|
8de738a146 | ||
|
|
6960beb739 | ||
|
|
6748639ec5 | ||
|
|
e5651807fd | ||
|
|
9adf6dee61 | ||
|
|
03527a1f95 | ||
|
|
651863b0f7 | ||
|
|
450ecd24b7 | ||
|
|
995cfa2aa4 | ||
|
|
5eecb5ea20 | ||
|
|
0885473b66 | ||
|
|
eabff4f6b2 | ||
|
|
a773df25d0 | ||
|
|
b07f75ac36 | ||
|
|
ed5fbadd73 | ||
|
|
c74c2c8efe | ||
|
|
1efe570dae | ||
|
|
2571460f42 | ||
|
|
dbb67113b9 | ||
|
|
33e3e018f3 | ||
|
|
07adf0342d | ||
|
|
b2b1cb4985 | ||
|
|
c4d6cc4579 | ||
|
|
01fbb4439e | ||
|
|
eb48d981d6 | ||
|
|
3624fcef0f | ||
|
|
ab62f0fcf9 | ||
|
|
9e78671e4f | ||
|
|
f64a7b38ae | ||
|
|
3fdedd8861 | ||
|
|
1d4ebbb689 | ||
|
|
c4f148a3a1 | ||
|
|
7abf45e8ba | ||
|
|
bbae62e0b7 | ||
|
|
a9d71078d3 | ||
|
|
5bde870586 | ||
|
|
7ea78870bf | ||
|
|
393caa86eb | ||
|
|
9b7a3c5c70 | ||
|
|
fe69bd50b5 | ||
|
|
a2c4f604b3 | ||
|
|
083e8c9b52 | ||
|
|
d2a025ca41 | ||
|
|
181d6b7e0a | ||
|
|
dd20fc8475 | ||
|
|
33ea397915 | ||
|
|
320fb02543 | ||
|
|
e127a6a557 | ||
|
|
e774dfd97d | ||
|
|
1dcea0fe6a | ||
|
|
0ca53f8db6 | ||
|
|
5395a759ed | ||
|
|
8f470fb000 | ||
|
|
90c375a5c8 | ||
|
|
e8cc4a0c58 | ||
|
|
cf68cc46ad | ||
|
|
653e20b4e4 | ||
|
|
e97d45e5b5 | ||
|
|
691cd048e2 | ||
|
|
5071105f8c | ||
|
|
9cd009e89b | ||
|
|
acaf293c7c | ||
|
|
79503dd17f | ||
|
|
485b6a0041 | ||
|
|
983781303b | ||
|
|
9c8e03f961 | ||
|
|
a298288888 | ||
|
|
c48703aed5 | ||
|
|
09000bd20f | ||
|
|
237caa84f7 | ||
|
|
d292d60ee9 | ||
|
|
395e406d65 | ||
|
|
806c3f63bb | ||
|
|
4a296809a0 | ||
|
|
f8361fa141 | ||
|
|
8542056d6e | ||
|
|
f23be91b6d | ||
|
|
f810bea4c8 | ||
|
|
42136b89fd | ||
|
|
eb604d9201 | ||
|
|
e341069196 | ||
|
|
3a54ac9d7d | ||
|
|
42d8c1b33f | ||
|
|
f700620373 | ||
|
|
0f059bce66 | ||
|
|
0eb68aaf72 | ||
|
|
b9f825c168 | ||
|
|
58c2504394 | ||
|
|
a9aadbfef9 | ||
|
|
dae5922fd0 | ||
|
|
5fb20991bb | ||
|
|
75fe7b2c67 | ||
|
|
ab400b82d6 | ||
|
|
6867cb5a4a | ||
|
|
742de8582c | ||
|
|
600ff5f367 | ||
|
|
e751facf32 | ||
|
|
959d5fb6c9 | ||
|
|
3456d503b2 | ||
|
|
9ef291a8ae | ||
|
|
ff174870e2 | ||
|
|
a015714d5e | ||
|
|
9bcab7b82b | ||
|
|
bc0cb0d0be | ||
|
|
ce4299a1f0 | ||
|
|
398e985e65 | ||
|
|
a5f597f598 | ||
|
|
beb7ecd0a9 | ||
|
|
ea625a0fbc | ||
|
|
932120883b | ||
|
|
b29406da8b | ||
|
|
4cc2d429c5 | ||
|
|
77563d12a6 | ||
|
|
b914bf3bf5 | ||
|
|
6f52b8473f | ||
|
|
44713eda4e | ||
|
|
e552282299 | ||
|
|
9ecd53267f | ||
|
|
5ee1cf6aa5 | ||
|
|
1295f635dc | ||
|
|
60142d9467 | ||
|
|
6dc4355972 | ||
|
|
555a26f0d6 | ||
|
|
abce7d8531 | ||
|
|
678d981121 | ||
|
|
32f8c18adc | ||
|
|
0aead96dcf | ||
|
|
c238094e4c | ||
|
|
657eeea4d5 | ||
|
|
1e34e85aab | ||
|
|
b747968e74 | ||
|
|
25629173c9 | ||
|
|
96642c07d3 | ||
|
|
2bd0f909f3 | ||
|
|
9b4047f3f9 | ||
|
|
91e2916199 | ||
|
|
3fcc677f96 | ||
|
|
3f77e32550 | ||
|
|
c4903c4993 | ||
|
|
630f9002aa | ||
|
|
aea7809fbd | ||
|
|
30e644d5e0 | ||
|
|
fe2f5a405c | ||
|
|
07a1890ed9 | ||
|
|
fc400c226c | ||
|
|
8e3ccec855 | ||
|
|
25c09bc241 | ||
|
|
0eaba3de01 | ||
|
|
ece1a7e9a7 | ||
|
|
2ef7a1521b | ||
|
|
b72357096a | ||
|
|
8f4c74d0ce | ||
|
|
2589e6d919 | ||
|
|
99fb8faf96 | ||
|
|
519da0a5c0 | ||
|
|
814a70b704 | ||
|
|
ff72f6cbd1 | ||
|
|
511f33c44d | ||
|
|
b455165fd3 | ||
|
|
7be03ab738 | ||
|
|
b287163ef7 | ||
|
|
1429674013 | ||
|
|
5576a76731 | ||
|
|
9c1a0fd798 | ||
|
|
70afa96bb0 | ||
|
|
808f4dd9a0 | ||
|
|
8e74ba07fe | ||
|
|
0c33df1cd6 | ||
|
|
73bb6acc14 | ||
|
|
654c44ebc9 | ||
|
|
a6703ef731 | ||
|
|
04defb97b0 | ||
|
|
da4f6c9307 | ||
|
|
c3b0311a4b | ||
|
|
196f290320 | ||
|
|
b1fec69d8f | ||
|
|
b7a7446f75 | ||
|
|
bd9d9d4ab6 | ||
|
|
c6cd6e9864 | ||
|
|
5e7e314baa | ||
|
|
f2b995660a | ||
|
|
95f44f4460 | ||
|
|
bd68b9c0cb | ||
|
|
b19d05fbf7 | ||
|
|
dc724492ef | ||
|
|
e3de7b9f01 | ||
|
|
be2f1786b5 | ||
|
|
99a3131724 | ||
|
|
0dbbc469e1 | ||
|
|
d5a80cc89a | ||
|
|
0be5c6c576 | ||
|
|
d7b478e830 | ||
|
|
3ce76f450c | ||
|
|
ad04c68596 | ||
|
|
0d8bf5f0aa | ||
|
|
075fdb194e | ||
|
|
6ab1b7705a | ||
|
|
9151b8c575 | ||
|
|
d5186a03e9 | ||
|
|
b461ac0a68 | ||
|
|
477c9d1555 | ||
|
|
ea365e18f4 | ||
|
|
512eedfc39 | ||
|
|
518bc7030d | ||
|
|
ae8dc61423 | ||
|
|
b89532caa1 | ||
|
|
9a57b407a5 | ||
|
|
776de3618a | ||
|
|
b4d575c383 | ||
|
|
dc4382d067 | ||
|
|
08e00aa38b | ||
|
|
08946ce5d4 | ||
|
|
75212511d2 | ||
|
|
79845f2d63 | ||
|
|
c982ff546c | ||
|
|
9f56d100aa | ||
|
|
d0c3765f8f | ||
|
|
1ded1cad5a | ||
|
|
ec6258a2a5 | ||
|
|
f8566392f6 | ||
|
|
7a1042fedd | ||
|
|
61efc2d152 | ||
|
|
f74c2049a7 | ||
|
|
7451dda632 | ||
|
|
ab9b151b8a | ||
|
|
26aa302714 | ||
|
|
e2f2b2962f | ||
|
|
a218b87215 | ||
|
|
ef6f022ea3 | ||
|
|
a594d45611 | ||
|
|
4c4a023f34 | ||
|
|
1e35e1096f | ||
|
|
bd145f17da | ||
|
|
99c342f19b | ||
|
|
0bca3393d4 | ||
|
|
41bd27b573 | ||
|
|
30430cb8cb | ||
|
|
3cf98617f5 | ||
|
|
fa4b2ae0e3 | ||
|
|
e2b38829f2 | ||
|
|
0a4ac7a35a | ||
|
|
cb060ae8b1 | ||
|
|
98edd2740f | ||
|
|
82e711a344 | ||
|
|
2166d55878 | ||
|
|
8e8f520eaa | ||
|
|
4eeaa7c650 | ||
|
|
f5fc106d01 | ||
|
|
de1773361a | ||
|
|
b9b45632b0 | ||
|
|
2ce7c6c2be | ||
|
|
4137d0dd82 | ||
|
|
29bd8b45c3 | ||
|
|
9d1601f424 | ||
|
|
7525e087ff | ||
|
|
be4991a419 | ||
|
|
ac89f428b2 | ||
|
|
7765cb31bf | ||
|
|
8729407da6 | ||
|
|
eac87b65d8 | ||
|
|
848c68689d | ||
|
|
65001c44e6 | ||
|
|
6ec37d3fa4 | ||
|
|
9f68d60703 | ||
|
|
fc8654bff5 | ||
|
|
ebbf7cf3a2 | ||
|
|
225fcef291 | ||
|
|
b460acad0d | ||
|
|
1c1808378b | ||
|
|
2d25e08040 | ||
|
|
8fc1919d7c | ||
|
|
790c17ad53 | ||
|
|
d315e4f008 | ||
|
|
2bfc41ce30 | ||
|
|
cdacaac049 | ||
|
|
da88fd0b3f | ||
|
|
0095e4582b | ||
|
|
5cf0945fa7 | ||
|
|
8e10e9dea9 | ||
|
|
07f6439093 | ||
|
|
8c979b8545 | ||
|
|
4f3929c658 | ||
|
|
c8cf9e3002 | ||
|
|
7bc323c92c | ||
|
|
4d141fa6a3 | ||
|
|
25d1db5584 | ||
|
|
565d58bb31 | ||
|
|
2f95cc5f45 | ||
|
|
a62588a4c9 | ||
|
|
4afef9d3b3 | ||
|
|
a887b87350 | ||
|
|
3672285e92 | ||
|
|
ea4dd5defd | ||
|
|
712ee111d4 | ||
|
|
0fc7571c35 | ||
|
|
84cdf6a14e | ||
|
|
1f77656a1c | ||
|
|
bbd95ffe2a | ||
|
|
e3a7e1f403 | ||
|
|
746c71f44b | ||
|
|
cb61891450 | ||
|
|
481219402c | ||
|
|
48285e6738 | ||
|
|
551763fecb | ||
|
|
d2507fe99f | ||
|
|
07ff9a114e | ||
|
|
962d98543e | ||
|
|
7f17887e0e | ||
|
|
8e37806791 | ||
|
|
f076e05f49 | ||
|
|
163e3927b5 | ||
|
|
0234de12bb | ||
|
|
21be329e77 | ||
|
|
314c5cef7e | ||
|
|
ec36365697 | ||
|
|
f47a32067e | ||
|
|
fef571b1d6 | ||
|
|
80b33e3fed | ||
|
|
8b67118303 | ||
|
|
d5969a6573 | ||
|
|
547682a59a | ||
|
|
b2903137eb | ||
|
|
7329c69cfd | ||
|
|
83a095923e | ||
|
|
a44056a64b | ||
|
|
a705e3b9d8 | ||
|
|
d55a6cfd88 | ||
|
|
86ff2ab96b | ||
|
|
a960299612 | ||
|
|
99efe7f06b | ||
|
|
b605346c7d | ||
|
|
ab6c1ae402 | ||
|
|
94bcc8e997 | ||
|
|
72c2857237 | ||
|
|
20f0d16a58 | ||
|
|
9a26626412 | ||
|
|
f4afc91df7 | ||
|
|
baafb6d2f9 | ||
|
|
8b9e084b17 | ||
|
|
44a01f27fe | ||
|
|
543d18f9d9 | ||
|
|
7371f57ded | ||
|
|
ee543b7090 | ||
|
|
b67eb59461 | ||
|
|
e787a68859 | ||
|
|
edc4f8ec63 | ||
|
|
aec958249a | ||
|
|
f083391efd | ||
|
|
2c63c01723 | ||
|
|
85af5bbd27 | ||
|
|
17f26b803c | ||
|
|
4cd5c13841 | ||
|
|
c7a19857dd | ||
|
|
b07317b0f7 | ||
|
|
c0eef7530e | ||
|
|
55618a10b9 | ||
|
|
5f48b30449 | ||
|
|
e523886345 | ||
|
|
4918dc5239 | ||
|
|
a0de6295c7 | ||
|
|
3db778a665 | ||
|
|
a7eef65694 | ||
|
|
20baa9984f | ||
|
|
7f128b0dae | ||
|
|
869d69b986 | ||
|
|
c04cc94570 | ||
|
|
46093ba6ba | ||
|
|
4b9b1ec9ac | ||
|
|
01f075d3f5 | ||
|
|
de18a53efe | ||
|
|
caca578709 | ||
|
|
06e3fd6248 | ||
|
|
f3315d654e | ||
|
|
09ac8b8a32 | ||
|
|
0c2f0ac31e | ||
|
|
777f51c661 | ||
|
|
3cfdb7eeb0 | ||
|
|
1f9495099f | ||
|
|
80564dd8db | ||
|
|
cf4c1f7009 | ||
|
|
3ffdb34312 | ||
|
|
dc8d0e9483 | ||
|
|
38bd3b0fc5 | ||
|
|
d061b902d5 | ||
|
|
0a86990bdf | ||
|
|
7c293f51cb | ||
|
|
67b31c476c | ||
|
|
497f8bde83 | ||
|
|
8711265506 | ||
|
|
b1ff68c3b1 | ||
|
|
564f5d71b2 | ||
|
|
158122ed55 | ||
|
|
f1eb6e1ce4 | ||
|
|
004729b2a4 | ||
|
|
c27d9978fe | ||
|
|
342ac76982 | ||
|
|
662f039daa | ||
|
|
20c46bd27f | ||
|
|
0e6380a8bd | ||
|
|
ed099aa061 | ||
|
|
5f54777663 | ||
|
|
909affcf99 | ||
|
|
86856605b9 | ||
|
|
dae297e0f5 | ||
|
|
6e5f071f22 | ||
|
|
12c155b46f | ||
|
|
f51c51f041 | ||
|
|
43222b7651 | ||
|
|
70f86c6ebd | ||
|
|
74b4cb2afd | ||
|
|
fa96836b63 | ||
|
|
e763ae1631 | ||
|
|
008b31e530 | ||
|
|
b6e445c445 | ||
|
|
c5935ec262 | ||
|
|
5f67494f77 | ||
|
|
e98b614f05 | ||
|
|
d541a70da5 | ||
|
|
cc9586aa64 | ||
|
|
f7561b7824 | ||
|
|
83abdc2ee6 | ||
|
|
e0400c0425 | ||
|
|
687b7e04d9 | ||
|
|
f43a155e6e | ||
|
|
f4e9516233 | ||
|
|
7f7f3557b3 | ||
|
|
b9b3d284cf | ||
|
|
4f240bf110 | ||
|
|
7cd82ffc4e | ||
|
|
4448410c3e | ||
|
|
220f4fad24 | ||
|
|
05c1d31550 | ||
|
|
c8bacabf24 | ||
|
|
bfab34f8c6 | ||
|
|
a3c01305df | ||
|
|
ad1dfc8e2b | ||
|
|
2c573bfef5 | ||
|
|
fd5ff2c61a | ||
|
|
1d03b200a5 | ||
|
|
b068749380 | ||
|
|
f82f893014 | ||
|
|
7094d43ee5 | ||
|
|
51296a9100 | ||
|
|
0803362a50 | ||
|
|
7e80787679 | ||
|
|
fc99328459 | ||
|
|
c594fc58b3 | ||
|
|
24cf78bc03 | ||
|
|
17aa564c57 | ||
|
|
7d37602d43 | ||
|
|
44f2e38387 | ||
|
|
7830c7e2eb | ||
|
|
deef5998c5 | ||
|
|
01ae858a14 | ||
|
|
634b099ade | ||
|
|
240342007b | ||
|
|
bc7731b819 | ||
|
|
00fd1e415c | ||
|
|
f81d16309c | ||
|
|
fe09b89fcb | ||
|
|
6314672109 | ||
|
|
d995169b3c | ||
|
|
2c6e00702c | ||
|
|
6d9e564d1c | ||
|
|
d751addf9d | ||
|
|
1af7adc09b | ||
|
|
7b287fb0f4 | ||
|
|
489c65af3e | ||
|
|
4e11a0c737 | ||
|
|
96b955f2fe | ||
|
|
f5014f29c3 | ||
|
|
b237f29636 | ||
|
|
c0c674d862 | ||
|
|
7c4721932d | ||
|
|
b093be52a2 | ||
|
|
65f1c19721 | ||
|
|
2502c0e87c | ||
|
|
2a91d3ddbd | ||
|
|
e7dc757293 | ||
|
|
1fe2a26e83 | ||
|
|
9b7471d6d2 | ||
|
|
b4ce621630 | ||
|
|
bf88f63482 | ||
|
|
20d48d7dc2 | ||
|
|
4b1d6ebd7c | ||
|
|
e9db7d1bb9 | ||
|
|
1c2ae8392c | ||
|
|
e8b9b3d583 | ||
|
|
71b84e1aba | ||
|
|
99f5aad942 | ||
|
|
8feae7efb6 | ||
|
|
874cbe1da4 | ||
|
|
14c7a11528 | ||
|
|
b4a7dc0cbd | ||
|
|
8c5f2ff61c | ||
|
|
770025da04 | ||
|
|
eb719e34a8 |
@@ -10,7 +10,7 @@ orbs:
|
||||
jobs:
|
||||
build:
|
||||
docker:
|
||||
- image: cimg/node:20.17.0
|
||||
- image: cimg/node:20.18.0
|
||||
- image: mongo:4.4
|
||||
|
||||
working_directory: ~/homebrewery
|
||||
@@ -64,9 +64,6 @@ jobs:
|
||||
- run:
|
||||
name: Test - Mustache Spans
|
||||
command: npm run test:mustache-syntax
|
||||
- run:
|
||||
name: Test - Definition Lists
|
||||
command: npm run test:definition-lists
|
||||
- run:
|
||||
name: Test - Hard Breaks
|
||||
command: npm run test:hard-breaks
|
||||
|
||||
9
.github/dependabot.yml
vendored
9
.github/dependabot.yml
vendored
@@ -5,6 +5,15 @@ updates:
|
||||
schedule:
|
||||
interval: daily
|
||||
open-pull-requests-limit: 99
|
||||
groups:
|
||||
dev-dependencies:
|
||||
dependency-type: "development"
|
||||
patterns: ["*"]
|
||||
update-types: ["patch", "minor"]
|
||||
prod-dependencies:
|
||||
dependency-type: "production"
|
||||
patterns: ["*"]
|
||||
update-types: ["patch", "minor"]
|
||||
ignore:
|
||||
- dependency-name: eslint
|
||||
versions:
|
||||
|
||||
@@ -144,3 +144,4 @@ your contribution to the project, please join our [gitter chat][gitter-url].
|
||||
[github-mark-duplicate-url]: https://docs.github.com/en/free-pro-team@latest/github/managing-your-work-on-github/about-duplicate-issues-and-pull-requests
|
||||
[github-pr-docs-url]: https://docs.github.com/en/free-pro-team@latest/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request
|
||||
[gitter-url]: https://gitter.im/naturalcrit/Lobby
|
||||
|
||||
|
||||
91
changelog.md
91
changelog.md
@@ -88,10 +88,94 @@ pre {
|
||||
## changelog
|
||||
For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery).
|
||||
|
||||
### Wednesday 7/09/2025 - v3.19.3
|
||||
|
||||
{{taskList
|
||||
##### calculuschild
|
||||
* [x] Restoring original saving behavior; will continue investigating why save was failing for some users in background
|
||||
}}
|
||||
|
||||
|
||||
### Wednesday 7/09/2025 - v3.19.2
|
||||
|
||||
{{taskList
|
||||
##### calculuschild
|
||||
* [x] Hotfix for saving issues - Please refresh your browser and report if problems continue
|
||||
}}
|
||||
|
||||
### Wednesday 7/09/2025 - v3.19.1
|
||||
|
||||
{{taskList
|
||||
##### calculuschild
|
||||
* [x] Send diffs instead of full file on save - should help with timeout/disconnect errors
|
||||
}}
|
||||
|
||||
\column
|
||||
|
||||
### Thursday 05/22/2025 - v3.19.0
|
||||
|
||||
{{taskList
|
||||
##### abquintic
|
||||
* [x] Fix crash due to colons after `\page`
|
||||
|
||||
Fixes issue [#4105](https://github.com/naturalcrit/homebrewery/issues/4105)
|
||||
|
||||
* [x] Fix images with spaces in alt text not rendering
|
||||
|
||||
Fixes issue [#3659](https://github.com/naturalcrit/homebrewery/issues/3659)
|
||||
|
||||
* [x] Custom snippets! Open the new {{openSans **:fas_table_list: SNIPPETS**}} tab (next to the {{openSans **:fas_paintbrush: STYLE**}} tab). Custom snippets will appear in a new snippet dropdown, and will be included when imported as a custom theme.
|
||||
|
||||
* [x] Move several generic styles/snippets from PHB to the Blank theme; generic snippets like image masks no longer require the PHB theme.
|
||||
|
||||
* [x] Extract several Markdown+ syntax extensions into their own NPM packages, for use by the wider community.
|
||||
|
||||
* [x] Allow `\pagebreak` and `\columnbreak` as alternatives to `\page` and `\column`
|
||||
|
||||
Partially fixes issue [#4035](https://github.com/naturalcrit/homebrewery/issues/4035)
|
||||
|
||||
* [x] Fix misbehaving column breaks on old Chrome
|
||||
|
||||
Fixes issue [#4192](https://github.com/naturalcrit/homebrewery/issues/4192)
|
||||
|
||||
* [x] Self-host font-awesome icons; fix missing icons on local installs
|
||||
|
||||
Fixes issue [#1965](https://github.com/naturalcrit/homebrewery/issues/1965)
|
||||
Fixes issue [#1548](https://github.com/naturalcrit/homebrewery/issues/1548)
|
||||
|
||||
##### G-Ambatte
|
||||
* [x] Fix CORS issue on local installs
|
||||
|
||||
* [x] Fix print size issues when using the Facing and Flow view options.
|
||||
|
||||
Fixes issue [#4146](https://github.com/naturalcrit/homebrewery/issues/4146)
|
||||
|
||||
* [x] New built-in `$[HB_pageNumber]` variable. Works with math operations or can be reassigned like any other variable for more customization over the old `{{pageNumber,auto}}` snippet.\
|
||||
New snippet found at {{openSans **:fas_pencil: TEXT EDITOR :fas_arrow_right: :fas_bookmark: PAGE NUMBERING :fas_arrow_right: :fas_arrow_down_1_9: VARIABLE AUTO PAGE NUMBER**}}
|
||||
|
||||
##### 5e-Cleric
|
||||
* [x] Fix search bar covering up snippet bar (3 times)
|
||||
|
||||
Fixes issue [#4098](https://github.com/naturalcrit/homebrewery/issues/4098)
|
||||
|
||||
* [x] Save view toolbar settings across sessions
|
||||
|
||||
Fixes issue [#3835](https://github.com/naturalcrit/homebrewery/issues/3835)
|
||||
|
||||
* [x] Fix styling issues on the view toolbar
|
||||
|
||||
* [x] Update the Darkbrewery editor theme
|
||||
|
||||
Fixes issue [#3312](https://github.com/naturalcrit/homebrewery/issues/3312)
|
||||
|
||||
}}
|
||||
|
||||
\page
|
||||
|
||||
### Monday 03/10/2025 - v3.18.0
|
||||
|
||||
{{taskList
|
||||
##### dbolack
|
||||
##### abquintic
|
||||
* [x] Add ability to paste in any Share ID/URL into a brew's {{openSans :fas_circle_info: **Properties** :fas_arrow_right: **THEMES**}} selection, as long as that brew has been tagged as `meta:theme`. You can now share your custom brew themes without needing to make a personal copy.
|
||||
* [x] Begin migration of custom Markdown extensions into their own NPM packages, for easier adoption by other users or projects
|
||||
* [x] Fix external HTML appearing in open codeblocks
|
||||
@@ -114,6 +198,9 @@ Fixes issue [#1729](https://github.com/naturalcrit/homebrewery/issues/1729)
|
||||
##### 5e-Cleric
|
||||
* [x] Style fixes for covers art and logos on A4 size pages
|
||||
* [x] Fix crash when trying to open brews that don't exist
|
||||
* [x] Tweaks and style update styling on {{openSans **VAULT** :fas_dungeon:}} page.
|
||||
|
||||
Fixes issue [#4079](https://github.com/naturalcrit/homebrewery/issues/4079)
|
||||
|
||||
##### Calculuschild
|
||||
* [x] `꞉꞉꞉꞉` now produces `<br>` instead of a `<div>`
|
||||
@@ -150,7 +237,7 @@ Fixes issue [#4073](https://github.com/naturalcrit/homebrewery/issues/4073)
|
||||
|
||||
* [x] Fix Reddit link crash when title has non-latin chars
|
||||
|
||||
##### dbolack
|
||||
##### abquintic
|
||||
|
||||
* [x] Fix page shadows toolbar option
|
||||
|
||||
|
||||
@@ -3,14 +3,15 @@ import React, { useEffect, useState } from 'react';
|
||||
const BrewUtils = require('./brewUtils/brewUtils.jsx');
|
||||
const NotificationUtils = require('./notificationUtils/notificationUtils.jsx');
|
||||
import AuthorUtils from './authorUtils/authorUtils.jsx';
|
||||
import LockTools from './lockTools/lockTools.jsx';
|
||||
|
||||
const tabGroups = ['brew', 'notifications', 'authors'];
|
||||
const tabGroups = ['brew', 'notifications', 'authors', 'locks'];
|
||||
|
||||
const Admin = ()=>{
|
||||
const [currentTab, setCurrentTab] = useState('brew');
|
||||
const [currentTab, setCurrentTab] = useState('');
|
||||
|
||||
useEffect(()=>{
|
||||
setCurrentTab(localStorage.getItem('hbAdminTab'));
|
||||
setCurrentTab(localStorage.getItem('hbAdminTab') || 'brew');
|
||||
}, []);
|
||||
|
||||
useEffect(()=>{
|
||||
@@ -40,6 +41,7 @@ const Admin = ()=>{
|
||||
{currentTab === 'brew' && <BrewUtils />}
|
||||
{currentTab === 'notifications' && <NotificationUtils />}
|
||||
{currentTab === 'authors' && <AuthorUtils />}
|
||||
{currentTab === 'locks' && <LockTools />}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
@import 'naturalcrit/styles/animations.less';
|
||||
@import 'naturalcrit/styles/colors.less';
|
||||
@import 'naturalcrit/styles/tooltip.less';
|
||||
@import './themes/fonts/iconFonts/fontAwesome.less';
|
||||
|
||||
@import 'font-awesome/css/font-awesome.css';
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
}
|
||||
|
||||
button {
|
||||
width: 50px;
|
||||
width : 50px;
|
||||
|
||||
i { margin-right : 10px; }
|
||||
}
|
||||
|
||||
342
client/admin/lockTools/lockTools.jsx
Normal file
342
client/admin/lockTools/lockTools.jsx
Normal file
@@ -0,0 +1,342 @@
|
||||
/*eslint max-lines: ["warn", {"max": 500, "skipBlankLines": true, "skipComments": true}]*/
|
||||
require('./lockTools.less');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
|
||||
import request from '../../homebrew/utils/request-middleware.js';
|
||||
|
||||
const LockTools = createClass({
|
||||
displayName : 'LockTools',
|
||||
getInitialState : function() {
|
||||
return {
|
||||
fetching : false,
|
||||
reviewCount : 0
|
||||
};
|
||||
},
|
||||
|
||||
componentDidMount : function() {
|
||||
this.updateReviewCount();
|
||||
},
|
||||
|
||||
updateReviewCount : async function() {
|
||||
const newCount = await request.get('/api/lock/count')
|
||||
.then((res)=>{return res.body?.count || 'Unknown';});
|
||||
if(newCount != this.state.reviewCount){
|
||||
this.setState({
|
||||
reviewCount : newCount
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
updateLockData : function(lock){
|
||||
this.setState({
|
||||
lock : lock
|
||||
});
|
||||
},
|
||||
|
||||
render : function() {
|
||||
return <div className='lockTools'>
|
||||
<h2>Lock Count</h2>
|
||||
<p>Number of brews currently locked: {this.state.reviewCount}</p>
|
||||
<button onClick={this.updateReviewCount}>REFRESH</button>
|
||||
<hr />
|
||||
<LockTable title='Locked Brews' text='Total Locked Brews' resultName='lockedDocuments' fetchURL='/api/locks' propertyNames={['shareId', 'title']} loadBrew={this.updateLockData} ></LockTable>
|
||||
<hr />
|
||||
<LockTable title='Brews Awaiting Review' text='Total Reviews Waiting' resultName='reviewDocuments' fetchURL='/api/lock/reviews' propertyNames={['shareId', 'title']} loadBrew={this.updateLockData} ></LockTable>
|
||||
<hr />
|
||||
<LockBrew key={this.state.lock?.key || 0} lock={this.state.lock}></LockBrew>
|
||||
<hr />
|
||||
<div style={{ columns: 2 }}>
|
||||
<LockLookup title='Unlock Brew' fetchURL='/api/unlock' updateFn={this.updateReviewCount}></LockLookup>
|
||||
<LockLookup title='Clear Review Request' fetchURL='/api/lock/review/remove'></LockLookup>
|
||||
</div>
|
||||
<hr />
|
||||
</div>;
|
||||
}
|
||||
});
|
||||
|
||||
const LockBrew = createClass({
|
||||
displayName : 'LockBrew',
|
||||
getInitialState : function() {
|
||||
// Default values
|
||||
return {
|
||||
brewId : this.props.lock?.shareId || '',
|
||||
code : this.props.lock?.code || 455,
|
||||
editMessage : this.props.lock?.editMessage || '',
|
||||
shareMessage : this.props.lock?.shareMessage || 'This Brew has been locked.',
|
||||
result : {},
|
||||
overwrite : false,
|
||||
};
|
||||
},
|
||||
|
||||
handleChange : function(e, varName) {
|
||||
const output = {};
|
||||
output[varName] = e.target.value;
|
||||
this.setState(output);
|
||||
},
|
||||
|
||||
submit : function(e){
|
||||
e.preventDefault();
|
||||
if(!this.state.editMessage) return;
|
||||
const newLock = {
|
||||
overwrite : this.state.overwrite,
|
||||
code : parseInt(this.state.code) || 100,
|
||||
editMessage : this.state.editMessage,
|
||||
shareMessage : this.state.shareMessage,
|
||||
applied : new Date
|
||||
};
|
||||
|
||||
request.post(`/api/lock/${this.state.brewId}`)
|
||||
.send(newLock)
|
||||
.set('Content-Type', 'application/json')
|
||||
.then((response)=>{
|
||||
this.setState({ result: response.body });
|
||||
})
|
||||
.catch((err)=>{
|
||||
this.setState({ result: err.response.body });
|
||||
});
|
||||
},
|
||||
|
||||
renderInput : function (name) {
|
||||
return <input type='text' name={name} value={this.state[name]} onChange={(e)=>this.handleChange(e, name)} autoComplete='off' required/>;
|
||||
},
|
||||
|
||||
renderResult : function(){
|
||||
return <>
|
||||
<h3>Result:</h3>
|
||||
<table>
|
||||
<tbody>
|
||||
{Object.keys(this.state.result).map((key, idx)=>{
|
||||
return <tr key={`${idx}-row`}>
|
||||
<td key={`${idx}-key`}>{key}</td>
|
||||
<td key={`${idx}-value`}>{this.state.result[key].toString()}
|
||||
</td>
|
||||
</tr>;
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</>;
|
||||
},
|
||||
|
||||
render : function() {
|
||||
return <div className='lockBrew'>
|
||||
<div className='lockForm'>
|
||||
<h2>Lock Brew</h2>
|
||||
<form onSubmit={this.submit}>
|
||||
<label>
|
||||
ID:
|
||||
{this.renderInput('brewId')}
|
||||
</label>
|
||||
<br />
|
||||
<label>
|
||||
Error Code:
|
||||
{this.renderInput('code')}
|
||||
</label>
|
||||
<br />
|
||||
<label>
|
||||
Private Message:
|
||||
{this.renderInput('editMessage')}
|
||||
</label>
|
||||
<br />
|
||||
<label>
|
||||
Public Message:
|
||||
{this.renderInput('shareMessage')}
|
||||
</label>
|
||||
<br />
|
||||
<label className='checkbox'>
|
||||
Overwrite
|
||||
<input name='overwrite' className='checkbox' type='checkbox' value={this.state.overwrite} onClick={()=>{return this.setState((prevState)=>{return { overwrite: !prevState.overwrite };});}} />
|
||||
</label>
|
||||
<label>
|
||||
<input type='submit' />
|
||||
</label>
|
||||
</form>
|
||||
{this.state.result && this.renderResult()}
|
||||
</div>
|
||||
<div className='lockSuggestions'>
|
||||
<h2>Suggestions</h2>
|
||||
<div className='lockCodes'>
|
||||
<h3>Codes</h3>
|
||||
<ul>
|
||||
<li>455 - Generic Lock</li>
|
||||
<li>456 - Copyright issues</li>
|
||||
<li>457 - Confidential Information Leakage</li>
|
||||
<li>458 - Sensitive Personal Information</li>
|
||||
<li>459 - Defamation or Libel</li>
|
||||
<li>460 - Hate Speech or Discrimination</li>
|
||||
<li>461 - Illegal Activities</li>
|
||||
<li>462 - Malware or Phishing</li>
|
||||
<li>463 - Plagiarism</li>
|
||||
<li>465 - Misrepresentation</li>
|
||||
<li>466 - Inappropriate Content</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className='lockMessages'>
|
||||
<h3>Messages</h3>
|
||||
<ul>
|
||||
<li><b>Private Message:</b> This is the private message that is ONLY displayed to the authors of the locked brew. This message MUST specify exactly what actions must be taken in order to have the brew unlocked.</li>
|
||||
<li><b>Public Message:</b> This is the public message that is displayed to the EVERYONE that attempts to view the locked brew.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
});
|
||||
|
||||
const LockTable = createClass({
|
||||
displayName : 'LockTable',
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
title : '',
|
||||
text : '',
|
||||
fetchURL : '/api/locks',
|
||||
resultName : '',
|
||||
propertyNames : ['shareId'],
|
||||
loadBrew : ()=>{}
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState : function() {
|
||||
return {
|
||||
result : '',
|
||||
error : '',
|
||||
searching : false
|
||||
};
|
||||
},
|
||||
|
||||
lockKey : React.createRef(0),
|
||||
|
||||
clickFn : function (){
|
||||
this.setState({ searching: true, error: null });
|
||||
|
||||
request.get(this.props.fetchURL)
|
||||
.then((res)=>this.setState({ result: res.body }))
|
||||
.catch((err)=>this.setState({ result: err.response.body }))
|
||||
.finally(()=>{
|
||||
this.setState({ searching: false });
|
||||
});
|
||||
},
|
||||
|
||||
updateBrewLockData : function (lockData){
|
||||
this.lockKey.current++;
|
||||
const brewData = {
|
||||
key : this.lockKey.current,
|
||||
shareId : lockData.shareId,
|
||||
code : lockData.lock.code,
|
||||
editMessage : lockData.lock.editMessage,
|
||||
shareMessage : lockData.lock.shareMessage
|
||||
};
|
||||
this.props.loadBrew(brewData);
|
||||
},
|
||||
|
||||
render : function () {
|
||||
return <>
|
||||
<div className='brewsAwaitingReview'>
|
||||
<div className='brewBlock'>
|
||||
<h2>{this.props.title}</h2>
|
||||
<button onClick={this.clickFn}>
|
||||
REFRESH
|
||||
<i className={`fas ${!this.state.searching ? 'fa-search' : 'fa-spin fa-spinner'}`} />
|
||||
</button>
|
||||
</div>
|
||||
{this.state.result[this.props.resultName] &&
|
||||
<>
|
||||
<p>{this.props.text}: {this.state.result[this.props.resultName].length}</p>
|
||||
<table className='lockTable'>
|
||||
<thead>
|
||||
<tr>
|
||||
{this.props.propertyNames.map((name, idx)=>{
|
||||
return <th key={idx}>{name}</th>;
|
||||
})}
|
||||
<th>clip</th>
|
||||
<th>load</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{this.state.result[this.props.resultName].map((result, resultIdx)=>{
|
||||
return <tr className='row' key={`${resultIdx}-row`}>
|
||||
{this.props.propertyNames.map((name, nameIdx)=>{
|
||||
return <td key={`${resultIdx}-${nameIdx}`}>
|
||||
{result[name].toString()}
|
||||
</td>;
|
||||
})}
|
||||
<td className='icon' title='Copy ID to Clipboard' onClick={()=>{navigator.clipboard.writeText(result.shareId.toString());}}><i className='fa-regular fa-clipboard'></i></td>
|
||||
<td className='icon' title='View Lock details' onClick={()=>{this.updateBrewLockData(result);}}><i className='fa-regular fa-circle-down'></i></td>
|
||||
</tr>;
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
</>;
|
||||
}
|
||||
});
|
||||
|
||||
const LockLookup = createClass({
|
||||
displayName : 'LockLookup',
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
fetchURL : '/api/lookup'
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState : function() {
|
||||
return {
|
||||
query : '',
|
||||
result : '',
|
||||
error : '',
|
||||
searching : false
|
||||
};
|
||||
},
|
||||
|
||||
handleChange(e){
|
||||
this.setState({ query: e.target.value });
|
||||
},
|
||||
|
||||
clickFn(){
|
||||
this.setState({ searching: true, error: null });
|
||||
|
||||
request.put(`${this.props.fetchURL}/${this.state.query}`)
|
||||
.then((res)=>this.setState({ result: res.body }))
|
||||
.catch((err)=>this.setState({ result: err.response.body }))
|
||||
.finally(()=>{
|
||||
this.setState({ searching: false });
|
||||
});
|
||||
},
|
||||
|
||||
renderResult : function(){
|
||||
return <div className='lockLookup'>
|
||||
<h3>Result:</h3>
|
||||
<table>
|
||||
<tbody>
|
||||
{Object.keys(this.state.result).map((key, idx)=>{
|
||||
return <tr key={`${idx}-row`}>
|
||||
<td key={`${idx}-key`}>{key}</td>
|
||||
<td key={`${idx}-value`}>{this.state.result[key].toString()}
|
||||
</td>
|
||||
</tr>;
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>;
|
||||
},
|
||||
|
||||
render : function() {
|
||||
return <div className='brewLookup'>
|
||||
<h2>{this.props.title}</h2>
|
||||
<input type='text' value={this.state.query} onChange={this.handleChange} placeholder='share id' />
|
||||
<button onClick={this.clickFn}>
|
||||
<i className={`fas ${!this.state.searching ? 'fa-search' : 'fa-spin fa-spinner'}`} />
|
||||
</button>
|
||||
|
||||
{this.state.error
|
||||
&& <div className='error'>{this.state.error.toString()}</div>
|
||||
}
|
||||
|
||||
{this.state.result && this.renderResult()}
|
||||
</div>;
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = LockTools;
|
||||
66
client/admin/lockTools/lockTools.less
Normal file
66
client/admin/lockTools/lockTools.less
Normal file
@@ -0,0 +1,66 @@
|
||||
.lockTools {
|
||||
.lockBrew {
|
||||
columns : 2;
|
||||
|
||||
.lockForm {
|
||||
break-inside : avoid;
|
||||
|
||||
label {
|
||||
display : inline-block;
|
||||
width : 100%;
|
||||
line-height : 2.25em;
|
||||
text-align : right;
|
||||
input {
|
||||
float : right;
|
||||
width : 65%;
|
||||
margin-left : 10px;
|
||||
}
|
||||
&.checkbox {
|
||||
line-height: 1.5em;
|
||||
input {
|
||||
width : 1.5em;
|
||||
height : 1.5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lockSuggestions {
|
||||
line-height : 1.2em;
|
||||
break-inside : avoid;
|
||||
columns : 2;
|
||||
h2 { column-span : all; }
|
||||
h3 { margin-top : 0px; }
|
||||
b { font-weight : 600; }
|
||||
|
||||
.lockCodes { break-inside : avoid; }
|
||||
}
|
||||
}
|
||||
|
||||
.lockTable {
|
||||
cursor : default;
|
||||
break-inside : avoid;
|
||||
.row:hover {
|
||||
color : #000000;
|
||||
background-color : #CCCCCC;
|
||||
}
|
||||
.icon {
|
||||
cursor : pointer;
|
||||
&:hover { text-shadow : 0px 0px 6px black; }
|
||||
}
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding : 4px 10px;
|
||||
text-align : center;
|
||||
}
|
||||
table, td { border : 1px solid #333333; }
|
||||
|
||||
.brewLookup {
|
||||
min-height : 175px;
|
||||
break-inside : avoid;
|
||||
h2 { margin-top : 0px; }
|
||||
}
|
||||
|
||||
button i { padding-left : 5px; }
|
||||
}
|
||||
@@ -18,22 +18,20 @@
|
||||
margin-bottom : unset;
|
||||
font-family : monospace;
|
||||
|
||||
&[type="date"] {
|
||||
width:14ch;
|
||||
}
|
||||
&[type='date'] { width : 14ch; }
|
||||
}
|
||||
|
||||
textarea {
|
||||
width : 50ch;
|
||||
min-height : 7em;
|
||||
max-height : 20em;
|
||||
resize : vertical;
|
||||
padding : 10px;
|
||||
resize : vertical;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
width: 200px;
|
||||
width : 200px;
|
||||
|
||||
i { margin-right : 10px; }
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.notificationLookup {
|
||||
width : 450px;
|
||||
height : fit-content;
|
||||
height : fit-content;
|
||||
|
||||
.noNotification { margin-block : 20px; }
|
||||
.notificationList {
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
|
||||
|
||||
.anchored-box {
|
||||
position:absolute;
|
||||
@supports (inset-block-start: anchor(bottom)){
|
||||
inset-block-start: anchor(bottom);
|
||||
}
|
||||
justify-self: anchor-center;
|
||||
visibility: hidden;
|
||||
&.active {
|
||||
visibility: visible;
|
||||
position : absolute;
|
||||
visibility : hidden;
|
||||
justify-self : anchor-center;
|
||||
@supports (inset-block-start: anchor(bottom)) {
|
||||
inset-block-start : anchor(bottom);
|
||||
}
|
||||
&.active { visibility : visible; }
|
||||
}
|
||||
@@ -27,7 +27,7 @@
|
||||
position : relative;
|
||||
padding : 5px;
|
||||
margin : 0 3px;
|
||||
font-family : "Open Sans";
|
||||
font-family : 'Open Sans';
|
||||
font-size : 11px;
|
||||
cursor : default;
|
||||
&:hover {
|
||||
|
||||
@@ -29,8 +29,8 @@ const SplitPane = (props)=>{
|
||||
const limitPosition = (x, min = 1, max = window.innerWidth - 13)=>Math.round(Math.min(max, Math.max(min, x)));
|
||||
|
||||
//when resizing, the divider should grow smaller if less space is given, then grow back if the space is restored, to the original position
|
||||
const handleResize = () =>setDividerPos(limitPosition(window.localStorage.getItem(storageKey), 0.1 * (window.innerWidth - 13), 0.9 * (window.innerWidth - 13)));
|
||||
|
||||
const handleResize = ()=>setDividerPos(limitPosition(window.localStorage.getItem(storageKey), 0.1 * (window.innerWidth - 13), 0.9 * (window.innerWidth - 13)));
|
||||
|
||||
const handleUp =(e)=>{
|
||||
e.preventDefault();
|
||||
if(isDragging) {
|
||||
@@ -21,8 +21,8 @@
|
||||
background-color : #BBBBBB;
|
||||
.dots {
|
||||
display : table-cell;
|
||||
text-align : center;
|
||||
vertical-align : middle;
|
||||
text-align : center;
|
||||
i {
|
||||
display : block !important;
|
||||
margin : 10px 0px;
|
||||
@@ -19,12 +19,13 @@ const { printCurrentBrew } = require('../../../shared/helpers.js');
|
||||
import HeaderNav from './headerNav/headerNav.jsx';
|
||||
import { safeHTML } from './safeHTML.js';
|
||||
|
||||
const PAGEBREAK_REGEX_V3 = /^(?=\\page(?: *{[^\n{}]*})?$)/m;
|
||||
const PAGEBREAK_REGEX_V3 = /^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/m;
|
||||
const PAGEBREAK_REGEX_LEGACY = /\\page(?:break)?/m;
|
||||
const COLUMNBREAK_REGEX_LEGACY = /\\column(:?break)?/m;
|
||||
const PAGE_HEIGHT = 1056;
|
||||
|
||||
const INITIAL_CONTENT = dedent`
|
||||
<!DOCTYPE html><html><head>
|
||||
<link href="//use.fontawesome.com/releases/v6.5.1/css/all.css" rel="stylesheet" type="text/css" />
|
||||
<link href="//fonts.googleapis.com/css?family=Open+Sans:400,300,600,700" rel="stylesheet" type="text/css" />
|
||||
<link href='/homebrew/bundle.css' type="text/css" rel='stylesheet' />
|
||||
<base target=_blank>
|
||||
@@ -39,7 +40,7 @@ const BrewPage = (props)=>{
|
||||
...props
|
||||
};
|
||||
const pageRef = useRef(null);
|
||||
const cleanText = safeHTML(props.contents);
|
||||
const cleanText = safeHTML(`${props.contents}\n<div class="columnSplit"></div>\n`);
|
||||
|
||||
useEffect(()=>{
|
||||
if(!pageRef.current) return;
|
||||
@@ -114,16 +115,24 @@ const BrewRenderer = (props)=>{
|
||||
zoomLevel : 100,
|
||||
spread : 'single',
|
||||
startOnRight : true,
|
||||
pageShadows : true
|
||||
pageShadows : true,
|
||||
rowGap : 5,
|
||||
columnGap : 10,
|
||||
});
|
||||
|
||||
//useEffect to store or gather toolbar state from storage
|
||||
useEffect(()=>{
|
||||
const toolbarState = JSON.parse(window.localStorage.getItem('hb_toolbarState'));
|
||||
toolbarState && setDisplayOptions(toolbarState);
|
||||
}, []);
|
||||
|
||||
const [headerState, setHeaderState] = useState(false);
|
||||
|
||||
const mainRef = useRef(null);
|
||||
const pagesRef = useRef(null);
|
||||
|
||||
if(props.renderer == 'legacy') {
|
||||
rawPages = props.text.split('\\page');
|
||||
rawPages = props.text.split(PAGEBREAK_REGEX_LEGACY);
|
||||
} else {
|
||||
rawPages = props.text.split(PAGEBREAK_REGEX_V3);
|
||||
}
|
||||
@@ -180,23 +189,27 @@ const BrewRenderer = (props)=>{
|
||||
let attributes = {};
|
||||
|
||||
if(props.renderer == 'legacy') {
|
||||
pageText.replace(COLUMNBREAK_REGEX_LEGACY, '```\n````\n'); // Allow Legacy brews to use `\column(break)`
|
||||
const html = MarkdownLegacy.render(pageText);
|
||||
|
||||
return <BrewPage className='page phb' index={index} key={index} contents={html} style={styles} onVisibilityChange={handlePageVisibilityChange} />;
|
||||
} else {
|
||||
if(pageText.startsWith('\\page')) {
|
||||
const firstLineTokens = Markdown.marked.lexer(pageText.split('\n', 1)[0])[0].tokens;
|
||||
const injectedTags = firstLineTokens.find((obj)=>obj.injectedTags !== undefined)?.injectedTags;
|
||||
const injectedTags = firstLineTokens?.find((obj)=>obj.injectedTags !== undefined)?.injectedTags;
|
||||
if(injectedTags) {
|
||||
styles = { ...styles, ...injectedTags.styles };
|
||||
styles = _.mapKeys(styles, (v, k) => k.startsWith('--') ? k : _.camelCase(k)); // Convert CSS to camelCase for React
|
||||
styles = _.mapKeys(styles, (v, k)=>k.startsWith('--') ? k : _.camelCase(k)); // Convert CSS to camelCase for React
|
||||
classes = [classes, injectedTags.classes].join(' ').trim();
|
||||
attributes = injectedTags.attributes;
|
||||
}
|
||||
pageText = pageText.includes('\n') ? pageText.substring(pageText.indexOf('\n') + 1) : ''; // Remove the \page line
|
||||
}
|
||||
|
||||
let html = Markdown.render(pageText, index);
|
||||
// DO NOT REMOVE!!! REQUIRED FOR BACKWARDS COMPATIBILITY WITH NON-UPGRADABLE VERSIONS OF CHROME.
|
||||
pageText += `\n\n \n\\column\n `; //Artificial column break at page end to emulate column-fill:auto (until `wide` is used, when column-fill:balance will reappear)
|
||||
|
||||
const html = Markdown.render(pageText, index);
|
||||
|
||||
return <BrewPage className={classes} index={index} key={index} contents={html} style={styles} attributes={attributes} onVisibilityChange={handlePageVisibilityChange} />;
|
||||
}
|
||||
@@ -271,6 +284,7 @@ const BrewRenderer = (props)=>{
|
||||
|
||||
const handleDisplayOptionsChange = (newDisplayOptions)=>{
|
||||
setDisplayOptions(newDisplayOptions);
|
||||
localStorage.setItem('hb_toolbarState', JSON.stringify(newDisplayOptions));
|
||||
};
|
||||
|
||||
const pagesStyle = {
|
||||
|
||||
@@ -1,43 +1,39 @@
|
||||
@import (multiple, less) 'shared/naturalcrit/styles/reset.less';
|
||||
|
||||
.brewRenderer {
|
||||
height : 100vh;
|
||||
padding-top : 60px;
|
||||
overflow-y : scroll;
|
||||
will-change : transform;
|
||||
padding-top : 60px;
|
||||
height : 100vh;
|
||||
&:has(.facing, .flow) {
|
||||
padding : 60px 30px;
|
||||
}
|
||||
&.deployment {
|
||||
background-color: darkred;
|
||||
}
|
||||
&:has(.facing, .flow) { padding : 60px 30px; }
|
||||
&.deployment { background-color : darkred; }
|
||||
:where(.pages) {
|
||||
&.facing {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, auto);
|
||||
grid-template-rows: repeat(3, auto);
|
||||
gap: 10px 10px;
|
||||
justify-content: safe center;
|
||||
display : grid;
|
||||
grid-template-rows : repeat(3, auto);
|
||||
grid-template-columns : repeat(2, auto);
|
||||
gap : 10px 10px;
|
||||
justify-content : safe center;
|
||||
&.recto .page:first-child {
|
||||
// sets first page on 'right' ('recto') of the preview, as if for a Cover page.
|
||||
// todo: add a checkbox to toggle this setting
|
||||
grid-column-start: 2;
|
||||
grid-column-start : 2;
|
||||
}
|
||||
& :where(.page) {
|
||||
margin-left: unset !important;
|
||||
margin-right: unset !important;
|
||||
margin-right : unset !important;
|
||||
margin-left : unset !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.flow {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
justify-content: safe center;
|
||||
display : flex;
|
||||
flex-wrap : wrap;
|
||||
gap : 10px;
|
||||
justify-content : safe center;
|
||||
& :where(.page) {
|
||||
flex: 0 0 auto;
|
||||
margin-left: unset !important;
|
||||
margin-right: unset !important;
|
||||
flex : 0 0 auto;
|
||||
margin-right : unset !important;
|
||||
margin-left : unset !important;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -50,9 +46,7 @@
|
||||
margin-left : auto;
|
||||
box-shadow : 1px 4px 14px #000000;
|
||||
}
|
||||
*[id] {
|
||||
scroll-margin-top:100px;
|
||||
}
|
||||
*[id] { scroll-margin-top : 100px; }
|
||||
}
|
||||
&::-webkit-scrollbar {
|
||||
width : 20px;
|
||||
@@ -74,16 +68,18 @@
|
||||
@media print {
|
||||
.toolBar { display : none; }
|
||||
.brewRenderer {
|
||||
height : 100%;
|
||||
padding-top : unset;
|
||||
overflow-y : unset;
|
||||
height : 100%;
|
||||
padding : unset;
|
||||
overflow-y : unset;
|
||||
&:has(.facing, .flow) {
|
||||
padding : unset;
|
||||
}
|
||||
.pages {
|
||||
margin : 0px;
|
||||
zoom: 100% !important;
|
||||
margin : 0px;
|
||||
zoom : 100% !important;
|
||||
display : block;
|
||||
& > .page { box-shadow : unset; }
|
||||
}
|
||||
}
|
||||
.headerNav {
|
||||
visibility: hidden;
|
||||
}
|
||||
.headerNav { visibility : hidden; }
|
||||
}
|
||||
@@ -25,7 +25,7 @@ const HeaderNav = React.forwardRef(({}, pagesRef)=>{
|
||||
'.toc' : ()=>{ return 'Table of Contents'; },
|
||||
};
|
||||
|
||||
const getHeaderContent = el => el.querySelector('h1')?.textContent;
|
||||
const getHeaderContent = (el)=>el.querySelector('h1')?.textContent;
|
||||
|
||||
const topLevelPageSelector = Object.keys(topLevelPages).join(',');
|
||||
|
||||
@@ -52,25 +52,23 @@ const HeaderNav = React.forwardRef(({}, pagesRef)=>{
|
||||
depth : 7, // All unmatched elements with IDs are set to the maximum depth (7)
|
||||
text : el.textContent, // Use `textContent` because `innerText` is affected by rendering, e.g. 'content-visibility: auto'
|
||||
link : el.id
|
||||
}
|
||||
};
|
||||
if(el.classList.contains('page')) {
|
||||
let text = `Page ${el.id.slice(1)}`; // Get the page # by trimming off the 'p' from the ID
|
||||
const pageType = Object.keys(topLevelPages).find(pageType => el.querySelector(pageType));
|
||||
if (pageType)
|
||||
text += ` - ${topLevelPages[pageType](el, pageType)}` // If a Top Level Page, add extra label
|
||||
const pageType = Object.keys(topLevelPages).find((pageType)=>el.querySelector(pageType));
|
||||
if(pageType)
|
||||
text += ` - ${topLevelPages[pageType](el, pageType)}`; // If a Top Level Page, add extra label
|
||||
|
||||
navEntry.depth = 0; // Pages are always at the least indented level
|
||||
navEntry.text = text;
|
||||
navEntry.className = 'pageLink';
|
||||
}
|
||||
else if(el.localName.match(/^h[1-6]/)){ // Header elements H1 through H6
|
||||
} else if(el.localName.match(/^h[1-6]/)){ // Header elements H1 through H6
|
||||
navEntry.depth = el.localName[1]; // Depth is set by the header level
|
||||
}
|
||||
navList.push(navEntry);
|
||||
});
|
||||
|
||||
return _.map(navList, (navItem, index)=>
|
||||
<HeaderNavItem {...navItem} key={index} />
|
||||
return _.map(navList, (navItem, index)=><HeaderNavItem {...navItem} key={index} />
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,39 +1,31 @@
|
||||
.headerNav {
|
||||
position: fixed;
|
||||
top: 32px;
|
||||
left: 0px;
|
||||
padding: 5px 10px;
|
||||
background-color: #ccc;
|
||||
border-radius: 5px;
|
||||
max-height: calc(100vh - 32px);
|
||||
max-width: 40vw;
|
||||
overflow-y: auto;
|
||||
&.active {
|
||||
padding-bottom: 10px;
|
||||
.navIcon {
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
}
|
||||
.navIcon {
|
||||
cursor: pointer;
|
||||
position : fixed;
|
||||
top : 32px;
|
||||
left : 0px;
|
||||
max-width : 40vw;
|
||||
max-height : calc(100vh - 32px);
|
||||
padding : 5px 10px;
|
||||
overflow-y : auto;
|
||||
background-color : #CCCCCC;
|
||||
border-radius : 5px;
|
||||
&.active {
|
||||
padding-bottom : 10px;
|
||||
.navIcon { padding-bottom : 10px; }
|
||||
}
|
||||
.navIcon { cursor : pointer; }
|
||||
li {
|
||||
list-style-type: none;
|
||||
list-style-type : none;
|
||||
a {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
font-family: 'Open Sans';
|
||||
font-size: 12px;
|
||||
padding: 2px;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
&.pageLink {
|
||||
font-weight: 900;
|
||||
}
|
||||
display : inline-block;
|
||||
width : 100%;
|
||||
padding : 2px;
|
||||
font-family : 'Open Sans';
|
||||
font-size : 12px;
|
||||
color : inherit;
|
||||
text-decoration : none;
|
||||
cursor : pointer;
|
||||
&:hover { text-decoration : underline; }
|
||||
&.pageLink { font-weight : 900; }
|
||||
|
||||
@depths: 0,1,2,3,4,5,6,7;
|
||||
|
||||
|
||||
@@ -85,4 +85,9 @@
|
||||
display : inline-block;
|
||||
width : 100%;
|
||||
}
|
||||
.blank {
|
||||
height : 1em;
|
||||
margin-top : 0;
|
||||
& + * { margin-top : 0; }
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,12 @@ const ToolBar = ({ displayOptions, onDisplayOptionsChange, visiblePages, totalPa
|
||||
setPageNum(pageRange);
|
||||
}, [visiblePages]);
|
||||
|
||||
useEffect(()=>{
|
||||
const Visibility = localStorage.getItem('hb_toolbarVisibility');
|
||||
if (Visibility) setToolsVisible(Visibility === 'true');
|
||||
|
||||
}, []);
|
||||
|
||||
const handleZoomButton = (zoom)=>{
|
||||
handleOptionChange('zoomLevel', _.round(_.clamp(zoom, MIN_ZOOM, MAX_ZOOM)));
|
||||
};
|
||||
@@ -55,15 +61,30 @@ const ToolBar = ({ displayOptions, onDisplayOptionsChange, visiblePages, totalPa
|
||||
// find widest page, in case pages are different widths, so that the zoom is adapted to not cut the widest page off screen.
|
||||
const widestPage = _.maxBy([...pages], 'offsetWidth').offsetWidth;
|
||||
|
||||
desiredZoom = (iframeWidth / widestPage) * 100;
|
||||
if(displayOptions.spread === 'facing')
|
||||
desiredZoom = (iframeWidth / ((widestPage * 2) + parseInt(displayOptions.columnGap))) * 100;
|
||||
else
|
||||
desiredZoom = (iframeWidth / (widestPage + 20)) * 100;
|
||||
|
||||
} else if(mode == 'fit'){
|
||||
let minDimRatio;
|
||||
// find the page with the largest single dim (height or width) so that zoom can be adapted to fit it.
|
||||
if(displayOptions.spread === 'facing')
|
||||
minDimRatio = [...pages].reduce((minRatio, page)=>Math.min(minRatio, iframeWidth / page.offsetWidth / 2), Infinity); // if 'facing' spread, fit two pages in view
|
||||
let minDimRatio;
|
||||
if(displayOptions.spread === 'single')
|
||||
minDimRatio = [...pages].reduce(
|
||||
(minRatio, page)=>Math.min(minRatio,
|
||||
iframeWidth / page.offsetWidth,
|
||||
iframeHeight / page.offsetHeight
|
||||
),
|
||||
Infinity
|
||||
);
|
||||
else
|
||||
minDimRatio = [...pages].reduce((minRatio, page)=>Math.min(minRatio, iframeWidth / page.offsetWidth, iframeHeight / page.offsetHeight), Infinity);
|
||||
minDimRatio = [...pages].reduce(
|
||||
(minRatio, page)=>Math.min(minRatio,
|
||||
iframeWidth / ((page.offsetWidth * 2) + parseInt(displayOptions.columnGap)),
|
||||
iframeHeight / page.offsetHeight
|
||||
),
|
||||
Infinity
|
||||
);
|
||||
|
||||
desiredZoom = minDimRatio * 100;
|
||||
}
|
||||
@@ -77,7 +98,10 @@ const ToolBar = ({ displayOptions, onDisplayOptionsChange, visiblePages, totalPa
|
||||
return (
|
||||
<div id='preview-toolbar' className={`toolBar ${toolsVisible ? 'visible' : 'hidden'}`} role='toolbar'>
|
||||
<div className='toggleButton'>
|
||||
<button title={`${toolsVisible ? 'Hide' : 'Show'} Preview Toolbar`} onClick={()=>{setToolsVisible(!toolsVisible);}}><i className='fas fa-glasses' /></button>
|
||||
<button title={`${toolsVisible ? 'Hide' : 'Show'} Preview Toolbar`} onClick={()=>{
|
||||
setToolsVisible(!toolsVisible);
|
||||
localStorage.setItem('hb_toolbarVisibility', !toolsVisible);
|
||||
}}><i className='fas fa-glasses' /></button>
|
||||
<button title={`${headerState ? 'Hide' : 'Show'} Header Navigation`} onClick={()=>{setHeaderState(!headerState);}}><i className='fas fa-rectangle-list' /></button>
|
||||
</div>
|
||||
{/*v=====----------------------< Zoom Controls >---------------------=====v*/}
|
||||
@@ -142,7 +166,7 @@ const ToolBar = ({ displayOptions, onDisplayOptionsChange, visiblePages, totalPa
|
||||
id='single-spread'
|
||||
className='tool'
|
||||
title='Single Page'
|
||||
onClick={()=>{handleOptionChange('spread', 'active');}}
|
||||
onClick={()=>{handleOptionChange('spread', 'single');}}
|
||||
aria-checked={displayOptions.spread === 'single'}
|
||||
><i className='fac single-spread' /></button>
|
||||
<button role='radio'
|
||||
@@ -167,11 +191,11 @@ const ToolBar = ({ displayOptions, onDisplayOptionsChange, visiblePages, totalPa
|
||||
<h1>Options</h1>
|
||||
<label title='Modify the horizontal space between pages.'>
|
||||
Column gap
|
||||
<input type='range' min={0} max={200} defaultValue={10} className='range-input' onChange={(evt)=>handleOptionChange('columnGap', evt.target.value)} />
|
||||
<input type='range' min={0} max={200} defaultValue={displayOptions.columnGap || 10} className='range-input' onChange={(evt)=>handleOptionChange('columnGap', evt.target.value)} />
|
||||
</label>
|
||||
<label title='Modify the vertical space between rows of pages.'>
|
||||
Row gap
|
||||
<input type='range' min={0} max={200} defaultValue={10} className='range-input' onChange={(evt)=>handleOptionChange('rowGap', evt.target.value)} />
|
||||
<input type='range' min={0} max={200} defaultValue={displayOptions.rowGap || 10} className='range-input' onChange={(evt)=>handleOptionChange('rowGap', evt.target.value)} />
|
||||
</label>
|
||||
<label title='Start 1st page on the right side, such as if you have cover page.'>
|
||||
Start on right
|
||||
|
||||
@@ -6,12 +6,12 @@
|
||||
box-sizing : border-box;
|
||||
display : flex;
|
||||
flex-wrap : wrap;
|
||||
gap : 8px 30px;
|
||||
gap : 8px 20px;
|
||||
align-items : center;
|
||||
justify-content : center;
|
||||
width : 100%;
|
||||
height : auto;
|
||||
padding : 2px 0;
|
||||
padding : 2px 10px 2px 90px;
|
||||
font-family : 'Open Sans', sans-serif;
|
||||
font-size : 13px;
|
||||
color : #CCCCCC;
|
||||
@@ -153,10 +153,10 @@
|
||||
align-items : center;
|
||||
justify-content : center;
|
||||
width : auto;
|
||||
min-width : 46px;
|
||||
min-width : 40px;
|
||||
height : 100%;
|
||||
&:hover { background-color : #444444; }
|
||||
&:focus { border : 1px solid #D3D3D3;outline : none;}
|
||||
&:focus {outline : none; border : 1px solid #D3D3D3;}
|
||||
&:disabled {
|
||||
color : #777777;
|
||||
background-color : unset !important;
|
||||
@@ -169,12 +169,16 @@
|
||||
width : 92px;
|
||||
overflow : hidden;
|
||||
background-color : unset;
|
||||
opacity : 0.5;
|
||||
opacity : 0.7;
|
||||
transition : all 0.3s ease;
|
||||
& > *:not(.toggleButton) {
|
||||
opacity : 0;
|
||||
transition : all 0.2s ease;
|
||||
}
|
||||
|
||||
.toggleButton button i {
|
||||
filter: drop-shadow(0 0 2px black) drop-shadow(0 0 1px black);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,8 +186,6 @@
|
||||
position : absolute;
|
||||
left : 0;
|
||||
z-index : 5;
|
||||
width : 32px;
|
||||
min-width : unset;
|
||||
height : 100%;
|
||||
display : flex;
|
||||
height : 100%;
|
||||
}
|
||||
@@ -12,8 +12,8 @@ const MetadataEditor = require('./metadataEditor/metadataEditor.jsx');
|
||||
|
||||
const EDITOR_THEME_KEY = 'HOMEBREWERY-EDITOR-THEME';
|
||||
|
||||
const PAGEBREAK_REGEX_V3 = /^(?=\\page(?: *{[^\n{}]*})?$)/m;
|
||||
const SNIPPETBAR_HEIGHT = 25;
|
||||
const PAGEBREAK_REGEX_V3 = /^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/m;
|
||||
const SNIPPETBREAK_REGEX_V3 = /^\\snippet\ .*$/;
|
||||
const DEFAULT_STYLE_TEXT = dedent`
|
||||
/*=======--- Example CSS styling ---=======*/
|
||||
/* Any CSS here will apply to your document! */
|
||||
@@ -22,6 +22,13 @@ const DEFAULT_STYLE_TEXT = dedent`
|
||||
color: black;
|
||||
}`;
|
||||
|
||||
const DEFAULT_SNIPPET_TEXT = dedent`
|
||||
\snippet example snippet
|
||||
|
||||
The text between \`\snippet title\` lines will become a snippet of name \`title\` as this example provides.
|
||||
|
||||
This snippet is accessible in the brew tab, and will be inherited if the brew is used as a theme.
|
||||
`;
|
||||
let isJumping = false;
|
||||
|
||||
const Editor = createClass({
|
||||
@@ -36,6 +43,7 @@ const Editor = createClass({
|
||||
onTextChange : ()=>{},
|
||||
onStyleChange : ()=>{},
|
||||
onMetaChange : ()=>{},
|
||||
onSnipChange : ()=>{},
|
||||
reportError : ()=>{},
|
||||
|
||||
onCursorPageChange : ()=>{},
|
||||
@@ -51,8 +59,9 @@ const Editor = createClass({
|
||||
},
|
||||
getInitialState : function() {
|
||||
return {
|
||||
editorTheme : this.props.editorTheme,
|
||||
view : 'text' //'text', 'style', 'meta'
|
||||
editorTheme : this.props.editorTheme,
|
||||
view : 'text', //'text', 'style', 'meta', 'snippet'
|
||||
snippetbarHeight : 25
|
||||
};
|
||||
},
|
||||
|
||||
@@ -62,12 +71,11 @@ const Editor = createClass({
|
||||
isText : function() {return this.state.view == 'text';},
|
||||
isStyle : function() {return this.state.view == 'style';},
|
||||
isMeta : function() {return this.state.view == 'meta';},
|
||||
isSnip : function() {return this.state.view == 'snippet';},
|
||||
|
||||
componentDidMount : function() {
|
||||
|
||||
this.updateEditorSize();
|
||||
this.highlightCustomMarkdown();
|
||||
window.addEventListener('resize', this.updateEditorSize);
|
||||
document.getElementById('BrewRenderer').addEventListener('keydown', this.handleControlKeys);
|
||||
document.addEventListener('keydown', this.handleControlKeys);
|
||||
|
||||
@@ -80,10 +88,7 @@ const Editor = createClass({
|
||||
editorTheme : editorTheme
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
componentWillUnmount : function() {
|
||||
window.removeEventListener('resize', this.updateEditorSize);
|
||||
this.setState({ snippetbarHeight: document.querySelector('.editor > .snippetBar').offsetHeight });
|
||||
},
|
||||
|
||||
componentDidUpdate : function(prevProps, prevState, snapshot) {
|
||||
@@ -118,14 +123,6 @@ const Editor = createClass({
|
||||
}
|
||||
},
|
||||
|
||||
updateEditorSize : function() {
|
||||
if(this.codeEditor.current) {
|
||||
let paneHeight = this.editor.current.parentNode.clientHeight;
|
||||
paneHeight -= SNIPPETBAR_HEIGHT;
|
||||
this.codeEditor.current.codeMirror.setSize(null, paneHeight);
|
||||
}
|
||||
},
|
||||
|
||||
updateCurrentCursorPage : function(cursor) {
|
||||
const lines = this.props.brew.text.split('\n').slice(1, cursor.line + 1);
|
||||
const pageRegex = this.props.brew.renderer == 'V3' ? PAGEBREAK_REGEX_V3 : /\\page/;
|
||||
@@ -146,17 +143,17 @@ const Editor = createClass({
|
||||
|
||||
handleViewChange : function(newView){
|
||||
this.props.setMoveArrows(newView === 'text');
|
||||
|
||||
this.setState({
|
||||
view : newView
|
||||
}, ()=>{
|
||||
this.codeEditor.current?.codeMirror.focus();
|
||||
this.updateEditorSize();
|
||||
}); //TODO: not sure if updateeditorsize needed
|
||||
});
|
||||
},
|
||||
|
||||
highlightCustomMarkdown : function(){
|
||||
if(!this.codeEditor.current) return;
|
||||
if(this.state.view === 'text') {
|
||||
if((this.state.view === 'text') ||(this.state.view === 'snippet')) {
|
||||
const codeMirror = this.codeEditor.current.codeMirror;
|
||||
|
||||
codeMirror.operation(()=>{ // Batch CodeMirror styling
|
||||
@@ -175,12 +172,18 @@ const Editor = createClass({
|
||||
|
||||
for (let i=customHighlights.length - 1;i>=0;i--) customHighlights[i].clear();
|
||||
|
||||
let userSnippetCount = 1; // start snippet count from snippet 1
|
||||
let editorPageCount = 1; // start page count from page 1
|
||||
|
||||
_.forEach(this.props.brew.text.split('\n'), (line, lineNumber)=>{
|
||||
const whichSource = this.state.view === 'text' ? this.props.brew.text : this.props.brew.snippets;
|
||||
_.forEach(whichSource?.split('\n'), (line, lineNumber)=>{
|
||||
|
||||
const tabHighlight = this.state.view === 'text' ? 'pageLine' : 'snippetLine';
|
||||
const textOrSnip = this.state.view === 'text';
|
||||
|
||||
//reset custom line styles
|
||||
codeMirror.removeLineClass(lineNumber, 'background', 'pageLine');
|
||||
codeMirror.removeLineClass(lineNumber, 'background', 'snippetLine');
|
||||
codeMirror.removeLineClass(lineNumber, 'text');
|
||||
codeMirror.removeLineClass(lineNumber, 'wrap', 'sourceMoveFlash');
|
||||
|
||||
@@ -191,23 +194,25 @@ const Editor = createClass({
|
||||
|
||||
// Styling for \page breaks
|
||||
if((this.props.renderer == 'legacy' && line.includes('\\page')) ||
|
||||
(this.props.renderer == 'V3' && line.match(PAGEBREAK_REGEX_V3))) {
|
||||
(this.props.renderer == 'V3' && line.match(textOrSnip ? PAGEBREAK_REGEX_V3 : SNIPPETBREAK_REGEX_V3))) {
|
||||
|
||||
if(lineNumber > 0) // Since \page is optional on first line of document,
|
||||
if((lineNumber > 0) && (textOrSnip)) // Since \page is optional on first line of document,
|
||||
editorPageCount += 1; // don't use it to increment page count; stay at 1
|
||||
else if(this.state.view !== 'text') userSnippetCount += 1;
|
||||
|
||||
// add back the original class 'background' but also add the new class '.pageline'
|
||||
codeMirror.addLineClass(lineNumber, 'background', 'pageLine');
|
||||
codeMirror.addLineClass(lineNumber, 'background', tabHighlight);
|
||||
const pageCountElement = Object.assign(document.createElement('span'), {
|
||||
className : 'editor-page-count',
|
||||
textContent : editorPageCount
|
||||
textContent : textOrSnip ? editorPageCount : userSnippetCount
|
||||
});
|
||||
codeMirror.setBookmark({ line: lineNumber, ch: line.length }, pageCountElement);
|
||||
};
|
||||
|
||||
|
||||
// New Codemirror styling for V3 renderer
|
||||
if(this.props.renderer == 'V3') {
|
||||
if(line.match(/^\\column$/)){
|
||||
if(this.props.renderer === 'V3') {
|
||||
if(line.match(/^\\column(?:break)?$/)){
|
||||
codeMirror.addLineClass(lineNumber, 'text', 'columnSplit');
|
||||
}
|
||||
|
||||
@@ -408,6 +413,9 @@ const Editor = createClass({
|
||||
//Called when there are changes to the editor's dimensions
|
||||
update : function(){
|
||||
this.codeEditor.current?.updateSize();
|
||||
const snipHeight = document.querySelector('.editor > .snippetBar').offsetHeight;
|
||||
if(snipHeight !== this.state.snippetbarHeight)
|
||||
this.setState({ snippetbarHeight: snipHeight });
|
||||
},
|
||||
|
||||
updateEditorTheme : function(newTheme){
|
||||
@@ -432,7 +440,8 @@ const Editor = createClass({
|
||||
value={this.props.brew.text}
|
||||
onChange={this.props.onTextChange}
|
||||
editorTheme={this.state.editorTheme}
|
||||
rerenderParent={this.rerenderParent} />
|
||||
rerenderParent={this.rerenderParent}
|
||||
style={{ height: `calc(100% - ${this.state.snippetbarHeight}px)` }} />
|
||||
</>;
|
||||
}
|
||||
if(this.isStyle()){
|
||||
@@ -445,7 +454,8 @@ const Editor = createClass({
|
||||
onChange={this.props.onStyleChange}
|
||||
enableFolding={true}
|
||||
editorTheme={this.state.editorTheme}
|
||||
rerenderParent={this.rerenderParent} />
|
||||
rerenderParent={this.rerenderParent}
|
||||
style={{ height: `calc(100% - ${this.state.snippetbarHeight}px)` }} />
|
||||
</>;
|
||||
}
|
||||
if(this.isMeta()){
|
||||
@@ -462,6 +472,22 @@ const Editor = createClass({
|
||||
userThemes={this.props.userThemes}/>
|
||||
</>;
|
||||
}
|
||||
|
||||
if(this.isSnip()){
|
||||
if(!this.props.brew.snippets) { this.props.brew.snippets = DEFAULT_SNIPPET_TEXT; }
|
||||
return <>
|
||||
<CodeEditor key='codeEditor'
|
||||
ref={this.codeEditor}
|
||||
language='gfm'
|
||||
view={this.state.view}
|
||||
value={this.props.brew.snippets}
|
||||
onChange={this.props.onSnipChange}
|
||||
enableFolding={true}
|
||||
editorTheme={this.state.editorTheme}
|
||||
rerenderParent={this.rerenderParent}
|
||||
style={{ height: `calc(100% - ${this.state.snippetbarHeight}px)` }} />
|
||||
</>;
|
||||
}
|
||||
},
|
||||
|
||||
redo : function(){
|
||||
@@ -502,7 +528,7 @@ const Editor = createClass({
|
||||
historySize={this.historySize()}
|
||||
currentEditorTheme={this.state.editorTheme}
|
||||
updateEditorTheme={this.updateEditorTheme}
|
||||
snippetBundle={this.props.snippetBundle}
|
||||
themeBundle={this.props.themeBundle}
|
||||
cursorPos={this.codeEditor.current?.getCursorPosition() || {}}
|
||||
updateBrew={this.props.updateBrew}
|
||||
/>
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
@import 'themes/codeMirror/customEditorStyles.less';
|
||||
.editor {
|
||||
position : relative;
|
||||
width : 100%;
|
||||
container: editor / inline-size;
|
||||
|
||||
position : relative;
|
||||
width : 100%;
|
||||
height : 100%;
|
||||
container : editor / inline-size;
|
||||
.codeEditor {
|
||||
height : 100%;
|
||||
.pageLine {
|
||||
height : calc(100% - 25px);
|
||||
.CodeMirror { height : 100%; }
|
||||
.pageLine, .snippetLine {
|
||||
background : #33333328;
|
||||
border-top : #333399 solid 1px;
|
||||
}
|
||||
@@ -14,6 +15,10 @@
|
||||
float : right;
|
||||
color : grey;
|
||||
}
|
||||
.editor-snippet-count {
|
||||
float : right;
|
||||
color : grey;
|
||||
}
|
||||
.columnSplit {
|
||||
font-style : italic;
|
||||
color : grey;
|
||||
@@ -45,26 +50,26 @@
|
||||
color : green;
|
||||
}
|
||||
.emoji:not(.cm-comment) {
|
||||
margin-left : 2px;
|
||||
color : #360034;
|
||||
background : #ffc8ff;
|
||||
border-radius : 6px;
|
||||
font-weight : bold;
|
||||
padding-bottom : 1px;
|
||||
margin-left : 2px;
|
||||
font-weight : bold;
|
||||
color : #360034;
|
||||
outline : solid 2px #FF96FC;
|
||||
outline-offset : -2px;
|
||||
outline : solid 2px #ff96fc;
|
||||
background : #FFC8FF;
|
||||
border-radius : 6px;
|
||||
}
|
||||
.superscript:not(.cm-comment) {
|
||||
font-weight : bold;
|
||||
color : goldenrod;
|
||||
vertical-align : super;
|
||||
font-size : 0.9em;
|
||||
font-weight : bold;
|
||||
vertical-align : super;
|
||||
color : goldenrod;
|
||||
}
|
||||
.subscript:not(.cm-comment) {
|
||||
font-weight : bold;
|
||||
color : rgb(123, 123, 15);
|
||||
vertical-align : sub;
|
||||
font-size : 0.9em;
|
||||
font-weight : bold;
|
||||
vertical-align : sub;
|
||||
color : rgb(123, 123, 15);
|
||||
}
|
||||
.dl-highlight {
|
||||
&.dl-colon-highlight {
|
||||
@@ -103,4 +108,4 @@
|
||||
span { padding : 2px 5px; }
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@ const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
import request from '../../utils/request-middleware.js';
|
||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||
const Combobox = require('client/components/combobox.jsx');
|
||||
const TagInput = require('../tagInput/tagInput.jsx');
|
||||
|
||||
@@ -48,7 +47,7 @@ const MetadataEditor = createClass({
|
||||
|
||||
getInitialState : function(){
|
||||
return {
|
||||
showThumbnail : true
|
||||
showThumbnail : true
|
||||
};
|
||||
},
|
||||
|
||||
@@ -68,7 +67,7 @@ const MetadataEditor = createClass({
|
||||
const inputRules = validations[name] ?? [];
|
||||
const validationErr = inputRules.map((rule)=>rule(e.target.value)).filter(Boolean);
|
||||
|
||||
const debouncedReportValidity = _.debounce((target, errMessage) => {
|
||||
const debouncedReportValidity = _.debounce((target, errMessage)=>{
|
||||
callIfExists(target, 'setCustomValidity', errMessage);
|
||||
callIfExists(target, 'reportValidity');
|
||||
}, 300); // 300ms debounce delay, adjust as needed
|
||||
@@ -87,7 +86,7 @@ const MetadataEditor = createClass({
|
||||
return `- ${err}`;
|
||||
}).join('\n');
|
||||
|
||||
|
||||
|
||||
debouncedReportValidity(e.target, errMessage);
|
||||
return false;
|
||||
}
|
||||
@@ -110,6 +109,7 @@ const MetadataEditor = createClass({
|
||||
}
|
||||
this.props.onChange(this.props.metadata, 'renderer');
|
||||
},
|
||||
|
||||
handlePublish : function(val){
|
||||
this.props.onChange({
|
||||
...this.props.metadata,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
@import 'naturalcrit/styles/colors.less';
|
||||
|
||||
.userThemeName {
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
padding-right : 10px;
|
||||
padding-left : 10px;
|
||||
}
|
||||
|
||||
.metadataEditor {
|
||||
@@ -12,20 +12,20 @@
|
||||
height : calc(100vh - 54px); // 54px is the height of the navbar + snippet bar. probably a better way to dynamic get this.
|
||||
padding : 25px;
|
||||
overflow-y : auto;
|
||||
font-size : 13px;
|
||||
background-color : #999999;
|
||||
font-size : 13px;
|
||||
|
||||
h1 {
|
||||
margin: 0 0 40px;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
margin : 0 0 40px;
|
||||
font-weight : bold;
|
||||
text-transform : uppercase;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin : 20px 0;
|
||||
font-weight : bold;
|
||||
border-bottom: 2px solid gray;
|
||||
color: #555;
|
||||
margin : 20px 0;
|
||||
font-weight : bold;
|
||||
color : #555555;
|
||||
border-bottom : 2px solid gray;
|
||||
}
|
||||
|
||||
& > div { margin-bottom : 10px; }
|
||||
@@ -54,10 +54,10 @@
|
||||
min-width : 200px;
|
||||
& > label {
|
||||
width : 80px;
|
||||
font-size : 0.9em;
|
||||
font-weight : 800;
|
||||
line-height : 1.8em;
|
||||
text-transform : uppercase;
|
||||
font-size: .9em;
|
||||
}
|
||||
& > .value {
|
||||
flex : 1 1 auto;
|
||||
@@ -74,7 +74,7 @@
|
||||
border : 1px solid gray;
|
||||
&:focus { outline : 1px solid #444444; }
|
||||
}
|
||||
&.thumbnail, &.themes{
|
||||
&.thumbnail, &.themes {
|
||||
label { line-height : 2.0em; }
|
||||
.value {
|
||||
overflow : hidden;
|
||||
@@ -90,14 +90,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
&.themes{
|
||||
&.themes {
|
||||
.value {
|
||||
overflow : visible;
|
||||
text-overflow : auto;
|
||||
}
|
||||
button {
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
padding-right : 5px;
|
||||
padding-left : 5px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,8 +136,8 @@
|
||||
margin-right : 15px;
|
||||
font-size : 0.9em;
|
||||
font-weight : 800;
|
||||
white-space : nowrap;
|
||||
vertical-align : middle;
|
||||
white-space : nowrap;
|
||||
cursor : pointer;
|
||||
user-select : none;
|
||||
}
|
||||
@@ -164,9 +164,7 @@
|
||||
.colorButton(@red);
|
||||
}
|
||||
}
|
||||
.authors.field .value {
|
||||
line-height : 1.5em;
|
||||
}
|
||||
.authors.field .value { line-height : 1.5em; }
|
||||
|
||||
.themes.field {
|
||||
& .dropdown-container {
|
||||
@@ -174,9 +172,7 @@
|
||||
z-index : 100;
|
||||
background-color : white;
|
||||
}
|
||||
& .dropdown-options {
|
||||
overflow-y : visible;
|
||||
}
|
||||
& .dropdown-options { overflow-y : visible; }
|
||||
.disabled {
|
||||
font-style : italic;
|
||||
color : dimgray;
|
||||
|
||||
@@ -28,18 +28,18 @@ module.exports = {
|
||||
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;
|
||||
}
|
||||
],
|
||||
theme: [
|
||||
(value) => {
|
||||
theme : [
|
||||
(value)=>{
|
||||
const URL = global.config.baseUrl.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&'); //Escape any regex characters
|
||||
const shareIDPattern = '[a-zA-Z0-9-_]{12}';
|
||||
const shareURLRegex = new RegExp(`^${URL}\\/share\\/${shareIDPattern}$`);
|
||||
const shareIDRegex = new RegExp(`^${shareIDPattern}$`);
|
||||
if (value?.length === 0) return null;
|
||||
if (shareURLRegex.test(value)) return null;
|
||||
if (shareIDRegex.test(value)) return null;
|
||||
if(value?.length === 0) return null;
|
||||
if(shareURLRegex.test(value)) return null;
|
||||
if(shareIDRegex.test(value)) return null;
|
||||
|
||||
return 'Must be a valid Share URL or a 12-character ID.';
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ const _ = require('lodash');
|
||||
const cx = require('classnames');
|
||||
|
||||
import { loadHistory } from '../../utils/versionHistory.js';
|
||||
import { brewSnippetsToJSON } from '../../../../shared/helpers.js';
|
||||
|
||||
//Import all themes
|
||||
const ThemeSnippets = {};
|
||||
@@ -40,7 +41,7 @@ const Snippetbar = createClass({
|
||||
unfoldCode : ()=>{},
|
||||
updateEditorTheme : ()=>{},
|
||||
cursorPos : {},
|
||||
snippetBundle : [],
|
||||
themeBundle : [],
|
||||
updateBrew : ()=>{}
|
||||
};
|
||||
},
|
||||
@@ -64,7 +65,10 @@ const Snippetbar = createClass({
|
||||
},
|
||||
|
||||
componentDidUpdate : async function(prevProps, prevState) {
|
||||
if(prevProps.renderer != this.props.renderer || prevProps.theme != this.props.theme || prevProps.snippetBundle != this.props.snippetBundle) {
|
||||
if(prevProps.renderer != this.props.renderer ||
|
||||
prevProps.theme != this.props.theme ||
|
||||
prevProps.themeBundle != this.props.themeBundle ||
|
||||
prevProps.brew.snippets != this.props.brew.snippets) {
|
||||
this.setState({
|
||||
snippets : this.compileSnippets()
|
||||
});
|
||||
@@ -97,7 +101,7 @@ const Snippetbar = createClass({
|
||||
if(key == 'snippets') {
|
||||
const result = _.reverse(_.unionBy(_.reverse(newValue), _.reverse(oldValue), 'name')); // Join snippets together, with preference for the child theme over the parent theme
|
||||
return result.filter((snip)=>snip.gen || snip.subsnippets);
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
compileSnippets : function() {
|
||||
@@ -105,15 +109,21 @@ const Snippetbar = createClass({
|
||||
|
||||
let oldSnippets = _.keyBy(compiledSnippets, 'groupName');
|
||||
|
||||
for (let snippets of this.props.snippetBundle) {
|
||||
if(typeof(snippets) == 'string') // load staticThemes as needed; they were sent as just a file name
|
||||
snippets = ThemeSnippets[snippets];
|
||||
if(this.props.themeBundle.snippets) {
|
||||
for (let snippets of this.props.themeBundle.snippets) {
|
||||
if(typeof(snippets) == 'string') // load staticThemes as needed; they were sent as just a file name
|
||||
snippets = ThemeSnippets[snippets];
|
||||
|
||||
const newSnippets = _.keyBy(_.cloneDeep(snippets), 'groupName');
|
||||
compiledSnippets = _.values(_.mergeWith(oldSnippets, newSnippets, this.mergeCustomizer));
|
||||
const newSnippets = _.keyBy(_.cloneDeep(snippets), 'groupName');
|
||||
compiledSnippets = _.values(_.mergeWith(oldSnippets, newSnippets, this.mergeCustomizer));
|
||||
|
||||
oldSnippets = _.keyBy(compiledSnippets, 'groupName');
|
||||
oldSnippets = _.keyBy(compiledSnippets, 'groupName');
|
||||
}
|
||||
}
|
||||
|
||||
const userSnippetsasJSON = brewSnippetsToJSON(this.props.brew.title || 'New Document', this.props.brew.snippets, this.props.themeBundle.snippets);
|
||||
compiledSnippets.push(userSnippetsasJSON);
|
||||
|
||||
return compiledSnippets;
|
||||
},
|
||||
|
||||
@@ -207,59 +217,60 @@ const Snippetbar = createClass({
|
||||
renderEditorButtons : function(){
|
||||
if(!this.props.showEditButtons) return;
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div className='editors'>
|
||||
{this.props.view !== 'meta' && <><div className='historyTools'>
|
||||
<div className={`editorTool snippetGroup history ${this.state.historyExists ? 'active' : ''}`}
|
||||
onClick={this.toggleHistoryMenu} >
|
||||
<i className='fas fa-clock-rotate-left' />
|
||||
{ this.state.showHistory && this.renderHistoryItems() }
|
||||
<div className='editors'>
|
||||
{this.props.view !== 'meta' && <><div className='historyTools'>
|
||||
<div className={`editorTool snippetGroup history ${this.state.historyExists ? 'active' : ''}`}
|
||||
onClick={this.toggleHistoryMenu} >
|
||||
<i className='fas fa-clock-rotate-left' />
|
||||
{ this.state.showHistory && this.renderHistoryItems() }
|
||||
</div>
|
||||
<div className={`editorTool undo ${this.props.historySize.undo ? 'active' : ''}`}
|
||||
onClick={this.props.undo} >
|
||||
<i className='fas fa-undo' />
|
||||
</div>
|
||||
<div className={`editorTool redo ${this.props.historySize.redo ? 'active' : ''}`}
|
||||
onClick={this.props.redo} >
|
||||
<i className='fas fa-redo' />
|
||||
</div>
|
||||
</div>
|
||||
<div className={`editorTool undo ${this.props.historySize.undo ? 'active' : ''}`}
|
||||
onClick={this.props.undo} >
|
||||
<i className='fas fa-undo' />
|
||||
</div>
|
||||
<div className={`editorTool redo ${this.props.historySize.redo ? 'active' : ''}`}
|
||||
onClick={this.props.redo} >
|
||||
<i className='fas fa-redo' />
|
||||
</div>
|
||||
</div>
|
||||
<div className='codeTools'>
|
||||
<div className={`editorTool foldAll ${this.props.foldCode ? 'active' : ''}`}
|
||||
onClick={this.props.foldCode} >
|
||||
<i className='fas fa-compress-alt' />
|
||||
</div>
|
||||
<div className={`editorTool unfoldAll ${this.props.unfoldCode ? 'active' : ''}`}
|
||||
onClick={this.props.unfoldCode} >
|
||||
<i className='fas fa-expand-alt' />
|
||||
</div>
|
||||
<div className={`editorTheme ${this.state.themeSelector ? 'active' : ''}`}
|
||||
onClick={this.toggleThemeSelector} >
|
||||
<i className='fas fa-palette' />
|
||||
{this.state.themeSelector && this.renderThemeSelector()}
|
||||
</div>
|
||||
</div></>}
|
||||
|
||||
<div className='codeTools'>
|
||||
<div className={`editorTool foldAll ${this.props.foldCode ? 'active' : ''}`}
|
||||
onClick={this.props.foldCode} >
|
||||
<i className='fas fa-compress-alt' />
|
||||
</div>
|
||||
<div className={`editorTool unfoldAll ${this.props.unfoldCode ? 'active' : ''}`}
|
||||
onClick={this.props.unfoldCode} >
|
||||
<i className='fas fa-expand-alt' />
|
||||
</div>
|
||||
<div className={`editorTheme ${this.state.themeSelector ? 'active' : ''}`}
|
||||
onClick={this.toggleThemeSelector} >
|
||||
<i className='fas fa-palette' />
|
||||
{this.state.themeSelector && this.renderThemeSelector()}
|
||||
</div>
|
||||
</div></>}
|
||||
|
||||
<div className='tabs'>
|
||||
<div className={cx('text', { selected: this.props.view === 'text' })}
|
||||
onClick={()=>this.props.onViewChange('text')}>
|
||||
<i className='fa fa-beer' />
|
||||
<div className='tabs'>
|
||||
<div className={cx('text', { selected: this.props.view === 'text' })}
|
||||
onClick={()=>this.props.onViewChange('text')}>
|
||||
<i className='fa fa-beer' />
|
||||
</div>
|
||||
<div className={cx('style', { selected: this.props.view === 'style' })}
|
||||
onClick={()=>this.props.onViewChange('style')}>
|
||||
<i className='fa fa-paint-brush' />
|
||||
</div>
|
||||
<div className={cx('snippet', { selected: this.props.view === 'snippet' })}
|
||||
onClick={()=>this.props.onViewChange('snippet')}>
|
||||
<i className='fas fa-th-list' />
|
||||
</div>
|
||||
<div className={cx('meta', { selected: this.props.view === 'meta' })}
|
||||
onClick={()=>this.props.onViewChange('meta')}>
|
||||
<i className='fas fa-info-circle' />
|
||||
</div>
|
||||
</div>
|
||||
<div className={cx('style', { selected: this.props.view === 'style' })}
|
||||
onClick={()=>this.props.onViewChange('style')}>
|
||||
<i className='fa fa-paint-brush' />
|
||||
</div>
|
||||
<div className={cx('meta', { selected: this.props.view === 'meta' })}
|
||||
onClick={()=>this.props.onViewChange('meta')}>
|
||||
<i className='fas fa-info-circle' />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
render : function(){
|
||||
@@ -272,11 +283,6 @@ const Snippetbar = createClass({
|
||||
|
||||
module.exports = Snippetbar;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const SnippetGroup = createClass({
|
||||
displayName : 'SnippetGroup',
|
||||
getDefaultProps : function() {
|
||||
@@ -310,7 +316,8 @@ const SnippetGroup = createClass({
|
||||
},
|
||||
|
||||
render : function(){
|
||||
return <div className='snippetGroup snippetBarButton'>
|
||||
const snippetGroup = `snippetGroup snippetBarButton ${this.props.snippets.length === 0 ? 'disabledSnippets' : ''}`;
|
||||
return <div className={snippetGroup}>
|
||||
<div className='text'>
|
||||
<i className={this.props.icon} />
|
||||
<span className='groupName'>{this.props.groupName}</span>
|
||||
|
||||
@@ -14,15 +14,15 @@
|
||||
.snippets {
|
||||
display : flex;
|
||||
justify-content : flex-start;
|
||||
min-width : 327.58px;
|
||||
min-width : 432.18px; //must be controlled every time an item is added, must be hardcoded for the wrapping as it is applied
|
||||
}
|
||||
|
||||
.editors {
|
||||
display : flex;
|
||||
justify-content : flex-end;
|
||||
min-width : 225px;
|
||||
min-width : 250px; //must be controlled every time an item is added, must be hardcoded for the wrapping as it is applied
|
||||
|
||||
&:only-child { margin-left : auto;min-width:unset;}
|
||||
&:only-child {min-width : unset; margin-left : auto;}
|
||||
|
||||
>div {
|
||||
display : flex;
|
||||
@@ -39,9 +39,7 @@
|
||||
text-align : center;
|
||||
cursor : pointer;
|
||||
|
||||
&.editorTool:not(.active) {
|
||||
cursor:not-allowed;
|
||||
}
|
||||
&.editorTool:not(.active) { cursor : not-allowed; }
|
||||
|
||||
&:hover,&.selected { background-color : #999999; }
|
||||
&.text {
|
||||
@@ -53,6 +51,9 @@
|
||||
&.meta {
|
||||
.tooltipLeft('Properties');
|
||||
}
|
||||
&.snippet {
|
||||
.tooltipLeft('Snippets');
|
||||
}
|
||||
&.undo {
|
||||
.tooltipLeft('Undo');
|
||||
font-size : 0.75em;
|
||||
@@ -92,7 +93,7 @@
|
||||
&.editorTheme {
|
||||
.tooltipLeft('Editor Themes');
|
||||
font-size : 0.75em;
|
||||
color : black;
|
||||
color : inherit;
|
||||
&.active {
|
||||
position : relative;
|
||||
background-color : #999999;
|
||||
@@ -151,9 +152,9 @@
|
||||
position : absolute;
|
||||
top : 100%;
|
||||
z-index : 1000;
|
||||
visibility : hidden;
|
||||
padding : 0px;
|
||||
margin-left : -5px;
|
||||
visibility : hidden;
|
||||
background-color : #DDDDDD;
|
||||
.snippet {
|
||||
position : relative;
|
||||
@@ -228,8 +229,15 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
.disabledSnippets {
|
||||
color: grey;
|
||||
cursor: not-allowed;
|
||||
|
||||
&:hover { background-color: #DDDDDD;}
|
||||
}
|
||||
|
||||
}
|
||||
@container editor (width < 553px) {
|
||||
@container editor (width < 683px) {
|
||||
.snippetBar {
|
||||
.editors {
|
||||
flex : 1;
|
||||
|
||||
@@ -3,43 +3,43 @@ const React = require('react');
|
||||
const { useState, useEffect } = React;
|
||||
const _ = require('lodash');
|
||||
|
||||
const TagInput = ({ unique = true, values = [], ...props }) => {
|
||||
const TagInput = ({ unique = true, values = [], ...props })=>{
|
||||
const [tempInputText, setTempInputText] = useState('');
|
||||
const [tagList, setTagList] = useState(values.map((value) => ({ value, editing: false })));
|
||||
const [tagList, setTagList] = useState(values.map((value)=>({ value, editing: false })));
|
||||
|
||||
useEffect(()=>{
|
||||
handleChange(tagList.map((context)=>context.value))
|
||||
}, [tagList])
|
||||
handleChange(tagList.map((context)=>context.value));
|
||||
}, [tagList]);
|
||||
|
||||
const handleChange = (value)=>{
|
||||
props.onChange({
|
||||
target : { value }
|
||||
})
|
||||
});
|
||||
};
|
||||
|
||||
const handleInputKeyDown = ({ evt, value, index, options = {} }) => {
|
||||
if (_.includes(['Enter', ','], evt.key)) {
|
||||
const handleInputKeyDown = ({ evt, value, index, options = {} })=>{
|
||||
if(_.includes(['Enter', ','], evt.key)) {
|
||||
evt.preventDefault();
|
||||
submitTag(evt.target.value, value, index);
|
||||
if (options.clear) {
|
||||
if(options.clear) {
|
||||
setTempInputText('');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const submitTag = (newValue, originalValue, index) => {
|
||||
setTagList((prevContext) => {
|
||||
const submitTag = (newValue, originalValue, index)=>{
|
||||
setTagList((prevContext)=>{
|
||||
// remove existing tag
|
||||
if(newValue === null){
|
||||
return [...prevContext].filter((context, i)=>i !== index);
|
||||
}
|
||||
// add new tag
|
||||
if(originalValue === null){
|
||||
return [...prevContext, { value: newValue, editing: false }]
|
||||
return [...prevContext, { value: newValue, editing: false }];
|
||||
}
|
||||
// update existing tag
|
||||
return prevContext.map((context, i) => {
|
||||
if (i === index) {
|
||||
return prevContext.map((context, i)=>{
|
||||
if(i === index) {
|
||||
return { ...context, value: newValue, editing: false };
|
||||
}
|
||||
return context;
|
||||
@@ -47,10 +47,10 @@ const TagInput = ({ unique = true, values = [], ...props }) => {
|
||||
});
|
||||
};
|
||||
|
||||
const editTag = (index) => {
|
||||
setTagList((prevContext) => {
|
||||
return prevContext.map((context, i) => {
|
||||
if (i === index) {
|
||||
const editTag = (index)=>{
|
||||
setTagList((prevContext)=>{
|
||||
return prevContext.map((context, i)=>{
|
||||
if(i === index) {
|
||||
return { ...context, editing: true };
|
||||
}
|
||||
return { ...context, editing: false };
|
||||
@@ -58,25 +58,25 @@ const TagInput = ({ unique = true, values = [], ...props }) => {
|
||||
});
|
||||
};
|
||||
|
||||
const renderReadTag = (context, index) => {
|
||||
const renderReadTag = (context, index)=>{
|
||||
return (
|
||||
<li key={index}
|
||||
data-value={context.value}
|
||||
className='tag'
|
||||
onClick={() => editTag(index)}>
|
||||
onClick={()=>editTag(index)}>
|
||||
{context.value}
|
||||
<button onClick={(evt)=>{evt.stopPropagation(); submitTag(null, context.value, index)}}><i className='fa fa-times fa-fw'/></button>
|
||||
<button onClick={(evt)=>{evt.stopPropagation(); submitTag(null, context.value, index);}}><i className='fa fa-times fa-fw'/></button>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
const renderWriteTag = (context, index) => {
|
||||
const renderWriteTag = (context, index)=>{
|
||||
return (
|
||||
<input type='text'
|
||||
key={index}
|
||||
defaultValue={context.value}
|
||||
onKeyDown={(evt) => handleInputKeyDown({evt, value: context.value, index: index})}
|
||||
autoFocus
|
||||
defaultValue={context.value}
|
||||
onKeyDown={(evt)=>handleInputKeyDown({ evt, value: context.value, index: index })}
|
||||
autoFocus
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -86,7 +86,7 @@ const TagInput = ({ unique = true, values = [], ...props }) => {
|
||||
<label>{props.label}</label>
|
||||
<div className='value'>
|
||||
<ul className='list'>
|
||||
{tagList.map((context, index) => { return context.editing ? renderWriteTag(context, index) : renderReadTag(context, index); })}
|
||||
{tagList.map((context, index)=>{ return context.editing ? renderWriteTag(context, index) : renderReadTag(context, index); })}
|
||||
</ul>
|
||||
|
||||
<input
|
||||
@@ -94,8 +94,8 @@ const TagInput = ({ unique = true, values = [], ...props }) => {
|
||||
className='value'
|
||||
placeholder={props.placeholder}
|
||||
value={tempInputText}
|
||||
onChange={(e) => setTempInputText(e.target.value)}
|
||||
onKeyDown={(evt) => handleInputKeyDown({ evt, value: null, options: { clear: true } })}
|
||||
onChange={(e)=>setTempInputText(e.target.value)}
|
||||
onKeyDown={(evt)=>handleInputKeyDown({ evt, value: null, options: { clear: true } })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,95 +1,75 @@
|
||||
//╔===--------------- Polyfills --------------===╗//
|
||||
import 'core-js/es/string/to-well-formed.js';
|
||||
//╚===--------------- ---------------===╝//
|
||||
/* eslint-disable camelcase */
|
||||
import 'core-js/es/string/to-well-formed.js'; //Polyfill for older browsers
|
||||
import './homebrew.less';
|
||||
import React from 'react';
|
||||
import { StaticRouter as Router, Route, Routes, useParams, useSearchParams } from 'react-router';
|
||||
|
||||
require('./homebrew.less');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const { StaticRouter:Router } = require('react-router');
|
||||
const { Route, Routes, useParams, useSearchParams } = require('react-router');
|
||||
import HomePage from './pages/homePage/homePage.jsx';
|
||||
import EditPage from './pages/editPage/editPage.jsx';
|
||||
import UserPage from './pages/userPage/userPage.jsx';
|
||||
import SharePage from './pages/sharePage/sharePage.jsx';
|
||||
import NewPage from './pages/newPage/newPage.jsx';
|
||||
import ErrorPage from './pages/errorPage/errorPage.jsx';
|
||||
import VaultPage from './pages/vaultPage/vaultPage.jsx';
|
||||
import AccountPage from './pages/accountPage/accountPage.jsx';
|
||||
|
||||
const HomePage = require('./pages/homePage/homePage.jsx');
|
||||
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 VaultPage = require('./pages/vaultPage/vaultPage.jsx');
|
||||
const AccountPage = require('./pages/accountPage/accountPage.jsx');
|
||||
|
||||
const WithRoute = (props)=>{
|
||||
const WithRoute = ({ el: Element, ...rest })=>{
|
||||
const params = useParams();
|
||||
const [searchParams] = useSearchParams();
|
||||
const queryParams = {};
|
||||
for (const [key, value] of searchParams?.entries() || []) {
|
||||
queryParams[key] = value;
|
||||
}
|
||||
const Element = props.el;
|
||||
const allProps = {
|
||||
...props,
|
||||
...params,
|
||||
query : queryParams,
|
||||
el : undefined
|
||||
};
|
||||
return <Element {...allProps} />;
|
||||
const queryParams = Object.fromEntries(searchParams?.entries() || []);
|
||||
|
||||
return <Element {...rest} {...params} query={queryParams} />;
|
||||
};
|
||||
|
||||
const Homebrew = createClass({
|
||||
displayName : 'Homebrewery',
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
url : '',
|
||||
welcomeText : '',
|
||||
changelog : '',
|
||||
version : '0.0.0',
|
||||
account : null,
|
||||
enable_v3 : false,
|
||||
brew : {
|
||||
title : '',
|
||||
text : '',
|
||||
shareId : null,
|
||||
editId : null,
|
||||
createdAt : null,
|
||||
updatedAt : null,
|
||||
lang : ''
|
||||
}
|
||||
};
|
||||
},
|
||||
const Homebrew = (props)=>{
|
||||
const {
|
||||
url = '',
|
||||
version = '0.0.0',
|
||||
account = null,
|
||||
enable_v3 = false,
|
||||
enable_themes,
|
||||
config,
|
||||
brew = {
|
||||
title : '',
|
||||
text : '',
|
||||
shareId : null,
|
||||
editId : null,
|
||||
createdAt : null,
|
||||
updatedAt : null,
|
||||
lang : ''
|
||||
},
|
||||
userThemes,
|
||||
brews
|
||||
} = props;
|
||||
|
||||
getInitialState : function() {
|
||||
global.account = this.props.account;
|
||||
global.version = this.props.version;
|
||||
global.enable_v3 = this.props.enable_v3;
|
||||
global.enable_themes = this.props.enable_themes;
|
||||
global.config = this.props.config;
|
||||
global.account = account;
|
||||
global.version = version;
|
||||
global.enable_v3 = enable_v3;
|
||||
global.enable_themes = enable_themes;
|
||||
global.config = config;
|
||||
|
||||
return {};
|
||||
},
|
||||
|
||||
render : function (){
|
||||
return (
|
||||
<Router location={this.props.url}>
|
||||
<div className='homebrew'>
|
||||
<Routes>
|
||||
<Route path='/edit/:id' element={<WithRoute el={EditPage} brew={this.props.brew} userThemes={this.props.userThemes}/>} />
|
||||
<Route path='/share/:id' element={<WithRoute el={SharePage} brew={this.props.brew} />} />
|
||||
<Route path='/new/:id' element={<WithRoute el={NewPage} brew={this.props.brew} userThemes={this.props.userThemes}/>} />
|
||||
<Route path='/new' element={<WithRoute el={NewPage} userThemes={this.props.userThemes}/> } />
|
||||
<Route path='/user/:username' element={<WithRoute el={UserPage} brews={this.props.brews} />} />
|
||||
<Route path='/vault' element={<WithRoute el={VaultPage}/>}/>
|
||||
<Route path='/changelog' element={<WithRoute el={SharePage} brew={this.props.brew} disableMeta={true} />} />
|
||||
<Route path='/faq' element={<WithRoute el={SharePage} brew={this.props.brew} disableMeta={true} />} />
|
||||
<Route path='/migrate' element={<WithRoute el={SharePage} brew={this.props.brew} disableMeta={true} />} />
|
||||
<Route path='/account' element={<WithRoute el={AccountPage} brew={this.props.brew} accountDetails={this.props.brew.accountDetails} />} />
|
||||
<Route path='/legacy' element={<WithRoute el={HomePage} brew={this.props.brew} />} />
|
||||
<Route path='/error' element={<WithRoute el={ErrorPage} brew={this.props.brew} />} />
|
||||
<Route path='/' element={<WithRoute el={HomePage} brew={this.props.brew} />} />
|
||||
<Route path='/*' element={<WithRoute el={HomePage} brew={this.props.brew} />} />
|
||||
</Routes>
|
||||
</div>
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
});
|
||||
return (
|
||||
<Router location={url}>
|
||||
<div className='homebrew'>
|
||||
<Routes>
|
||||
<Route path='/edit/:id' element={<WithRoute el={EditPage} brew={brew} userThemes={userThemes}/>} />
|
||||
<Route path='/share/:id' element={<WithRoute el={SharePage} brew={brew} />} />
|
||||
<Route path='/new/:id' element={<WithRoute el={NewPage} brew={brew} userThemes={userThemes}/>} />
|
||||
<Route path='/new' element={<WithRoute el={NewPage} userThemes={userThemes}/> } />
|
||||
<Route path='/user/:username' element={<WithRoute el={UserPage} brews={brews} />} />
|
||||
<Route path='/vault' element={<WithRoute el={VaultPage}/>}/>
|
||||
<Route path='/changelog' element={<WithRoute el={SharePage} brew={brew} disableMeta={true} />} />
|
||||
<Route path='/faq' element={<WithRoute el={SharePage} brew={brew} disableMeta={true} />} />
|
||||
<Route path='/migrate' element={<WithRoute el={SharePage} brew={brew} disableMeta={true} />} />
|
||||
<Route path='/account' element={<WithRoute el={AccountPage} brew={brew} accountDetails={brew.accountDetails} />} />
|
||||
<Route path='/legacy' element={<WithRoute el={HomePage} brew={brew} />} />
|
||||
<Route path='/error' element={<WithRoute el={ErrorPage} brew={brew} />} />
|
||||
<Route path='/' element={<WithRoute el={HomePage} brew={brew} />} />
|
||||
<Route path='/*' element={<WithRoute el={HomePage} brew={brew} />} />
|
||||
</Routes>
|
||||
</div>
|
||||
</Router>
|
||||
);
|
||||
};
|
||||
|
||||
module.exports = Homebrew;
|
||||
@@ -1,36 +1,32 @@
|
||||
@import 'naturalcrit/styles/core.less';
|
||||
.homebrew{
|
||||
.homebrew {
|
||||
height : 100%;
|
||||
.sitePage{
|
||||
.sitePage {
|
||||
display : flex;
|
||||
height : 100%;
|
||||
background-color : @steel;
|
||||
flex-direction : column;
|
||||
height : 100%;
|
||||
overflow-y : hidden;
|
||||
.content{
|
||||
background-color : @steel;
|
||||
.content {
|
||||
position : relative;
|
||||
height : calc(~"100% - 29px"); //Navbar height
|
||||
flex : auto;
|
||||
height : calc(~'100% - 29px'); //Navbar height
|
||||
overflow-y : hidden;
|
||||
}
|
||||
&.listPage .content {
|
||||
overflow-y : scroll;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 20px;
|
||||
&:horizontal{
|
||||
height: 20px;
|
||||
width:auto;
|
||||
width : 20px;
|
||||
&:horizontal {
|
||||
width : auto;
|
||||
height : 20px;
|
||||
}
|
||||
&-thumb {
|
||||
background: linear-gradient(90deg, #d3c1af 15px, #00000000 15px);
|
||||
&:horizontal{
|
||||
background: linear-gradient(0deg, #d3c1af 15px, #00000000 15px);
|
||||
}
|
||||
}
|
||||
&-corner {
|
||||
visibility: hidden;
|
||||
background : linear-gradient(90deg, #D3C1AF 15px, #00000000 15px);
|
||||
&:horizontal { background : linear-gradient(0deg, #D3C1AF 15px, #00000000 15px); }
|
||||
}
|
||||
&-corner { visibility : hidden; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,14 +23,15 @@ const ErrorNavItem = createClass({
|
||||
|
||||
const error = this.props.error;
|
||||
const response = error.response;
|
||||
const status = response.status;
|
||||
const HBErrorCode = response.body?.HBErrorCode;
|
||||
const message = response.body?.message;
|
||||
const status = response?.status;
|
||||
const errorCode = error.code
|
||||
const HBErrorCode = response?.body?.HBErrorCode;
|
||||
const message = response?.body?.message;
|
||||
let errMsg = '';
|
||||
try {
|
||||
errMsg += `${error.toString()}\n\n`;
|
||||
errMsg += `\`\`\`\n${error.stack}\n`;
|
||||
errMsg += `${JSON.stringify(response.error, null, ' ')}\n\`\`\``;
|
||||
errMsg += `${JSON.stringify(response?.error, null, ' ')}\n\`\`\``;
|
||||
console.log(errMsg);
|
||||
} catch (e){}
|
||||
|
||||
@@ -73,7 +74,7 @@ const ErrorNavItem = createClass({
|
||||
</Nav.item>;
|
||||
}
|
||||
|
||||
if(response.body?.errors?.[0].reason == 'storageQuotaExceeded') {
|
||||
if(response?.body?.errors?.[0].reason == 'storageQuotaExceeded') {
|
||||
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||
Oops!
|
||||
<div className='errorContainer' onClick={clearError}>
|
||||
@@ -82,7 +83,7 @@ const ErrorNavItem = createClass({
|
||||
</Nav.item>;
|
||||
}
|
||||
|
||||
if(response.req.url.match(/^\/api.*Google.*$/m)){
|
||||
if(response?.req.url.match(/^\/api.*Google.*$/m)){
|
||||
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||
Oops!
|
||||
<div className='errorContainer' onClick={clearError}>
|
||||
@@ -129,6 +130,18 @@ const ErrorNavItem = createClass({
|
||||
</Nav.item>;
|
||||
}
|
||||
|
||||
if(errorCode === 'ECONNABORTED') {
|
||||
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||
Oops!
|
||||
<div className='errorContainer' onClick={clearError}>
|
||||
The request to the server was interrupted or timed out.
|
||||
This can happen due to a network issue, or if
|
||||
trying to save a particularly large brew.
|
||||
Please check your internet connection and try again.
|
||||
</div>
|
||||
</Nav.item>;
|
||||
}
|
||||
|
||||
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||
Oops!
|
||||
<div className='errorContainer'>
|
||||
|
||||
@@ -1,78 +1,70 @@
|
||||
.navItem.error {
|
||||
position : relative;
|
||||
background-color : @red;
|
||||
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;
|
||||
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;
|
||||
.lowercase {
|
||||
text-transform : none;
|
||||
.errorContainer {
|
||||
position : absolute;
|
||||
top : 100%;
|
||||
left : 50%;
|
||||
z-index : 1000;
|
||||
width : 140px;
|
||||
padding : 3px;
|
||||
font-size : 10px;
|
||||
font-weight : 800;
|
||||
color : white;
|
||||
text-align : center;
|
||||
text-transform : uppercase;
|
||||
background-color : #333333;
|
||||
border : 3px solid #444444;
|
||||
border-radius : 5px;
|
||||
transform : translate(-50% + 3px, 10px);
|
||||
animation-name : glideDown;
|
||||
animation-duration : 0.4s;
|
||||
.lowercase { text-transform : none; }
|
||||
a { color : @teal; }
|
||||
&::before {
|
||||
position : absolute;
|
||||
top : -23px;
|
||||
left : 53px;
|
||||
width : 0px;
|
||||
height : 0px;
|
||||
content : '';
|
||||
border-top : 10px solid transparent;
|
||||
border-right : 10px solid transparent;
|
||||
border-bottom : 10px solid #444444;
|
||||
border-left : 10px solid transparent;
|
||||
}
|
||||
&::after {
|
||||
position : absolute;
|
||||
top : -19px;
|
||||
left : 53px;
|
||||
width : 0px;
|
||||
height : 0px;
|
||||
content : '';
|
||||
border-top : 10px solid transparent;
|
||||
border-right : 10px solid transparent;
|
||||
border-bottom : 10px solid #333333;
|
||||
border-left : 10px solid transparent;
|
||||
}
|
||||
.deny {
|
||||
display : inline-block;
|
||||
width : 48%;
|
||||
padding : 5px;
|
||||
margin : 1px;
|
||||
background-color : #333333;
|
||||
border-left : 1px solid #666666;
|
||||
.animate(background-color);
|
||||
&:hover { background-color : red; }
|
||||
}
|
||||
.confirm {
|
||||
display : inline-block;
|
||||
width : 48%;
|
||||
padding : 5px;
|
||||
margin : 1px;
|
||||
color : white;
|
||||
background-color : #333333;
|
||||
.animate(background-color);
|
||||
&:hover { background-color : teal; }
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,11 +24,11 @@
|
||||
}
|
||||
|
||||
.homebrew nav {
|
||||
position : relative;
|
||||
z-index : 2;
|
||||
display : flex;
|
||||
justify-content : space-between;
|
||||
background-color : #333333;
|
||||
position : relative;
|
||||
z-index : 2;
|
||||
display : flex;
|
||||
justify-content : space-between;
|
||||
|
||||
.navSection {
|
||||
display : flex;
|
||||
@@ -82,8 +82,8 @@
|
||||
font-weight : 800;
|
||||
line-height : 13px;
|
||||
color : white;
|
||||
text-decoration : none;
|
||||
text-transform : uppercase;
|
||||
text-decoration : none;
|
||||
cursor : pointer;
|
||||
background-color : #333333;
|
||||
i {
|
||||
@@ -106,11 +106,11 @@
|
||||
display : block;
|
||||
width : 100%;
|
||||
overflow : hidden;
|
||||
text-overflow : ellipsis;
|
||||
font-size : 12px;
|
||||
font-weight : 800;
|
||||
color : white;
|
||||
text-align : center;
|
||||
text-overflow : ellipsis;
|
||||
text-transform : initial;
|
||||
white-space : nowrap;
|
||||
background-color : transparent;
|
||||
@@ -170,16 +170,16 @@
|
||||
h4 {
|
||||
box-sizing : border-box;
|
||||
display : block;
|
||||
flex-basis : 20%;
|
||||
flex-grow : 1;
|
||||
flex-basis : 20%;
|
||||
min-width : 76px;
|
||||
padding : 5px 0;
|
||||
color : #BBBBBB;
|
||||
text-align : center;
|
||||
}
|
||||
p {
|
||||
flex-basis : 80%;
|
||||
flex-grow : 1;
|
||||
flex-basis : 80%;
|
||||
padding : 5px 0;
|
||||
font-family : 'Open Sans', sans-serif;
|
||||
font-size : 10px;
|
||||
@@ -215,10 +215,10 @@
|
||||
z-index : 10000;
|
||||
box-sizing : border-box;
|
||||
display : block;
|
||||
visibility : hidden;
|
||||
width : 100%;
|
||||
padding : 13px 5px;
|
||||
text-align : center;
|
||||
visibility : hidden;
|
||||
background-color : #333333;
|
||||
}
|
||||
}
|
||||
|
||||
37
client/homebrew/pages/basePages/editPage/editPage.jsx
Normal file
37
client/homebrew/pages/basePages/editPage/editPage.jsx
Normal file
@@ -0,0 +1,37 @@
|
||||
require('./editPage.less');
|
||||
const React = require('react');
|
||||
|
||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||
const Navbar = require('../../../navbar/navbar.jsx');
|
||||
const NewBrewItem = require('../../../navbar/newbrew.navitem.jsx');
|
||||
const HelpNavItem = require('../../../navbar/help.navitem.jsx');
|
||||
const PrintNavItem = require('../../../navbar/print.navitem.jsx');
|
||||
const ErrorNavItem = require('../../../navbar/error-navitem.jsx');
|
||||
const AccountNavItem = require('../../../navbar/account.navitem.jsx');
|
||||
const RecentNavItem = require('../../../navbar/recent.navitem.jsx').both;
|
||||
const VaultNavItem = require('../../../navbar/vault.navitem.jsx');
|
||||
|
||||
const BaseEditPage = (props)=>{
|
||||
return (
|
||||
<div className={`sitePage ${props.className || ''}`}>
|
||||
<Navbar>
|
||||
<Nav.section>
|
||||
<Nav.item className='brewTitle'>{props.brew.title}</Nav.item>
|
||||
</Nav.section>
|
||||
<Nav.section>
|
||||
{props.navButtons}
|
||||
<PrintNavItem />
|
||||
<NewBrewItem />
|
||||
<HelpNavItem />
|
||||
<VaultNavItem />
|
||||
<RecentNavItem brew={props.brew} storageKey={props.recentStorageKey} />
|
||||
<AccountNavItem />
|
||||
</Nav.section>
|
||||
</Navbar>
|
||||
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
module.exports = BaseEditPage;
|
||||
@@ -5,7 +5,7 @@ const moment = require('moment');
|
||||
import request from '../../../../utils/request-middleware.js';
|
||||
|
||||
const googleDriveIcon = require('../../../../googleDrive.svg');
|
||||
const homebreweryIcon = require('../../../../thumbnail.png');
|
||||
const homebreweryIcon = require('../../../../thumbnail.svg');
|
||||
const dedent = require('dedent-tabs').default;
|
||||
|
||||
const BrewItem = ({
|
||||
@@ -30,11 +30,11 @@ const BrewItem = ({
|
||||
}
|
||||
|
||||
request.delete(`/api/${brew.googleId ?? ''}${brew.editId}`).send().end((err, res)=>{
|
||||
if (err) reportError(err); else window.location.reload();
|
||||
});
|
||||
if(err) reportError(err); else window.location.reload();
|
||||
});
|
||||
}, [brew, reportError]);
|
||||
|
||||
const updateFilter = useCallback((type, term)=> updateListFilter(type, term), [updateListFilter]);
|
||||
const updateFilter = useCallback((type, term)=>updateListFilter(type, term), [updateListFilter]);
|
||||
|
||||
const renderDeleteBrewLink = ()=>{
|
||||
if(!brew.editId) return null;
|
||||
|
||||
@@ -1,148 +1,129 @@
|
||||
|
||||
.brewItem{
|
||||
.brewItem {
|
||||
position : relative;
|
||||
box-sizing : border-box;
|
||||
display : inline-block;
|
||||
vertical-align : top;
|
||||
box-sizing : border-box;
|
||||
box-sizing : border-box;
|
||||
overflow : hidden;
|
||||
width : 48%;
|
||||
min-height : 105px;
|
||||
margin-right : 15px;
|
||||
margin-bottom : 15px;
|
||||
padding : 5px 15px 2px 6px;
|
||||
padding-right : 15px;
|
||||
border : 1px solid #c9ad6a;
|
||||
margin-right : 15px;
|
||||
margin-bottom : 15px;
|
||||
overflow : hidden;
|
||||
vertical-align : top;
|
||||
background-color : #CAB2802E;
|
||||
border : 1px solid #C9AD6A;
|
||||
border-radius : 5px;
|
||||
box-shadow : 0px 4px 5px 0px #333333;
|
||||
break-inside : avoid;
|
||||
-webkit-column-break-inside : avoid;
|
||||
page-break-inside : avoid;
|
||||
break-inside : avoid;
|
||||
box-shadow : 0px 4px 5px 0px #333;
|
||||
background-color : #cab2802e;
|
||||
.thumbnail {
|
||||
position: absolute;
|
||||
width: 150px;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: -1;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-position: right top;
|
||||
mask-image: linear-gradient(80deg, #0000 20%, #050 40%);
|
||||
-webkit-mask-image: linear-gradient(80deg, #0000 20%, #050 40%);
|
||||
opacity: 50%;
|
||||
.thumbnail {
|
||||
position : absolute;
|
||||
top : 0;
|
||||
right : 0;
|
||||
z-index : -1;
|
||||
width : 150px;
|
||||
height : 100%;
|
||||
background-repeat : no-repeat;
|
||||
background-position : right top;
|
||||
background-size : contain;
|
||||
opacity : 50%;
|
||||
-webkit-mask-image : linear-gradient(80deg, #00000000 20%, #005500 40%);
|
||||
mask-image : linear-gradient(80deg, #00000000 20%, #005500 40%);
|
||||
}
|
||||
.text {
|
||||
min-height : 54px;
|
||||
h4{
|
||||
h4 {
|
||||
margin-bottom : 5px;
|
||||
font-size : 2.2em;
|
||||
}
|
||||
}
|
||||
.info{
|
||||
position: initial;
|
||||
bottom: 2px;
|
||||
font-family : ScalySansRemake;
|
||||
.info {
|
||||
position : initial;
|
||||
bottom : 2px;
|
||||
font-family : "ScalySansRemake";
|
||||
font-size : 1.2em;
|
||||
&>span{
|
||||
& > span {
|
||||
margin-right : 12px;
|
||||
line-height : 1.5em;
|
||||
|
||||
a {
|
||||
color:inherit;
|
||||
}
|
||||
a { color : inherit; }
|
||||
}
|
||||
}
|
||||
.brewTags span {
|
||||
background-color: #c8ac6e3b;
|
||||
margin: 2px;
|
||||
padding: 2px;
|
||||
border: 1px solid #c8ac6e;
|
||||
border-radius: 4px;
|
||||
white-space: nowrap;
|
||||
display: inline-block;
|
||||
font-weight: bold;
|
||||
border-color: currentColor;
|
||||
cursor : pointer;
|
||||
&:before {
|
||||
font-family: 'Font Awesome 5 Free';
|
||||
font-size: 12px;
|
||||
margin-right: 3px;
|
||||
display : inline-block;
|
||||
padding : 2px;
|
||||
margin : 2px;
|
||||
font-weight : bold;
|
||||
white-space : nowrap;
|
||||
cursor : pointer;
|
||||
background-color : #C8AC6E3B;
|
||||
border : 1px solid #C8AC6E;
|
||||
border-color : currentColor;
|
||||
border-radius : 4px;
|
||||
&::before {
|
||||
margin-right : 3px;
|
||||
font-family : 'Font Awesome 6 Free';
|
||||
font-size : 12px;
|
||||
}
|
||||
&.type {
|
||||
background-color: #0080003b;
|
||||
color: #008000;
|
||||
&:before{
|
||||
content: '\f0ad';
|
||||
}
|
||||
color : #008000;
|
||||
background-color : #0080003B;
|
||||
&::before { content : '\f0ad'; }
|
||||
}
|
||||
&.group {
|
||||
background-color: #5050503b;
|
||||
color: #000000;
|
||||
&:before{
|
||||
content: '\f500';
|
||||
}
|
||||
color : #000000;
|
||||
background-color : #5050503B;
|
||||
&::before { content : '\f500'; }
|
||||
}
|
||||
&.meta {
|
||||
background-color: #0000803b;
|
||||
color: #000080;
|
||||
&:before{
|
||||
content: '\f05a';
|
||||
}
|
||||
color : #000080;
|
||||
background-color : #0000803B;
|
||||
&::before { content : '\f05a'; }
|
||||
}
|
||||
&.system {
|
||||
background-color: #8000003b;
|
||||
color: #800000;
|
||||
&:before{
|
||||
content: '\f518';
|
||||
}
|
||||
color : #800000;
|
||||
background-color : #8000003B;
|
||||
&::before { content : '\f518'; }
|
||||
}
|
||||
}
|
||||
&:hover{
|
||||
.links{
|
||||
opacity : 1;
|
||||
}
|
||||
&:hover {
|
||||
.links { opacity : 1; }
|
||||
}
|
||||
&:nth-child(2n + 1){
|
||||
margin-right : 0px;
|
||||
}
|
||||
.links{
|
||||
&:nth-child(2n + 1) { margin-right : 0px; }
|
||||
.links {
|
||||
.animate(opacity);
|
||||
position : absolute;
|
||||
top : 0px;
|
||||
right : 0px;
|
||||
height : 100%;
|
||||
width : 2em;
|
||||
opacity : 0;
|
||||
background-color : fade(black, 60%);
|
||||
height : 100%;
|
||||
text-align : center;
|
||||
a{
|
||||
background-color : fade(black, 60%);
|
||||
opacity : 0;
|
||||
a {
|
||||
.animate(opacity);
|
||||
display : block;
|
||||
margin : 8px 0px;
|
||||
opacity : 0.6;
|
||||
font-size : 1.3em;
|
||||
color : white;
|
||||
text-decoration : unset;
|
||||
&:hover{
|
||||
opacity : 1;
|
||||
}
|
||||
i{
|
||||
cursor : pointer;
|
||||
}
|
||||
opacity : 0.6;
|
||||
&:hover { opacity : 1; }
|
||||
i { cursor : pointer; }
|
||||
}
|
||||
}
|
||||
.googleDriveIcon {
|
||||
height : 18px;
|
||||
padding : 0px;
|
||||
margin : -5px;
|
||||
height : 18px;
|
||||
}
|
||||
.homebreweryIcon {
|
||||
mix-blend-mode : darken;
|
||||
height : 24px;
|
||||
position : relative;
|
||||
top : 5px;
|
||||
left : -5px;
|
||||
position : relative;
|
||||
padding : 0px;
|
||||
top : 5px;
|
||||
left : -7.5px;
|
||||
height : 18px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
|
||||
.noColumns(){
|
||||
.noColumns() {
|
||||
column-count : auto;
|
||||
column-fill : auto;
|
||||
column-gap : normal;
|
||||
@@ -13,177 +13,151 @@
|
||||
height : auto;
|
||||
min-height : 279.4mm;
|
||||
margin : 20px auto;
|
||||
contain : unset;
|
||||
contain : unset;
|
||||
}
|
||||
.listPage{
|
||||
.content{
|
||||
.listPage {
|
||||
.content {
|
||||
z-index : 1;
|
||||
.page{
|
||||
.page {
|
||||
.noColumns() !important; //Needed to override PHB Theme since this is on a lower @layer
|
||||
&::after{
|
||||
display : none;
|
||||
}
|
||||
.noBrews{
|
||||
&::after { display : none; }
|
||||
.noBrews {
|
||||
margin : 10px 0px;
|
||||
font-size : 1.3em;
|
||||
font-style : italic;
|
||||
}
|
||||
.brewCollection {
|
||||
h1:hover{
|
||||
cursor: pointer;
|
||||
}
|
||||
h1:hover { cursor : pointer; }
|
||||
.active::before, .inactive::before {
|
||||
font-family: 'Font Awesome 5 Free';
|
||||
font-weight: 900;
|
||||
font-size: 0.6cm;
|
||||
padding-right: 0.5em;
|
||||
}
|
||||
.active {
|
||||
color: var(--HB_Color_HeaderText);
|
||||
}
|
||||
.active::before {
|
||||
content: '\f107';
|
||||
}
|
||||
.inactive {
|
||||
color: #707070;
|
||||
}
|
||||
.inactive::before {
|
||||
content: '\f105';
|
||||
padding-right : 0.5em;
|
||||
font-family : 'Font Awesome 6 Free';
|
||||
font-size : 0.6cm;
|
||||
font-weight : 900;
|
||||
}
|
||||
.active { color : var(--HB_Color_HeaderText); }
|
||||
.active::before { content : '\f107'; }
|
||||
.inactive { color : #707070; }
|
||||
.inactive::before { content : '\f105'; }
|
||||
}
|
||||
}
|
||||
}
|
||||
.sort-container {
|
||||
font-family : 'Open Sans', sans-serif;
|
||||
position : sticky;
|
||||
top : 0;
|
||||
left : 0;
|
||||
width : 100%;
|
||||
height : 30px;
|
||||
background-color : #555;
|
||||
border-top : 1px solid #666;
|
||||
border-bottom : 1px solid #666;
|
||||
color : white;
|
||||
text-align : center;
|
||||
z-index : 1;
|
||||
display : flex;
|
||||
justify-content : center;
|
||||
align-items : baseline;
|
||||
column-gap : 15px;
|
||||
row-gap : 5px;
|
||||
flex-wrap : wrap;
|
||||
h6{
|
||||
text-transform : uppercase;
|
||||
position : sticky;
|
||||
top : 0;
|
||||
left : 0;
|
||||
z-index : 1;
|
||||
display : flex;
|
||||
flex-wrap : wrap;
|
||||
row-gap : 5px;
|
||||
column-gap : 15px;
|
||||
align-items : baseline;
|
||||
justify-content : center;
|
||||
width : 100%;
|
||||
height : 30px;
|
||||
font-family : 'Open Sans', sans-serif;
|
||||
color : white;
|
||||
text-align : center;
|
||||
background-color : #555555;
|
||||
border-top : 1px solid #666666;
|
||||
border-bottom : 1px solid #666666;
|
||||
h6 {
|
||||
font-family : 'Open Sans', sans-serif;
|
||||
font-size : 11px;
|
||||
font-weight : bold;
|
||||
text-transform : uppercase;
|
||||
}
|
||||
.sort-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 8px;
|
||||
color: #ccc;
|
||||
height: 100%;
|
||||
display : flex;
|
||||
align-items : center;
|
||||
height : 100%;
|
||||
padding : 0 8px;
|
||||
color : #CCCCCC;
|
||||
|
||||
&:hover{
|
||||
background-color : #444;
|
||||
}
|
||||
&:hover { background-color : #444444; }
|
||||
|
||||
&.active {
|
||||
font-weight: bold;
|
||||
color: #ddd;
|
||||
background-color: #333;
|
||||
font-weight : bold;
|
||||
color : #DDDDDD;
|
||||
background-color : #333333;
|
||||
|
||||
button {
|
||||
color: white;
|
||||
font-weight: 800;
|
||||
height: 100%;
|
||||
& + .sortDir {
|
||||
padding-left: 5px;
|
||||
button {
|
||||
height : 100%;
|
||||
font-weight : 800;
|
||||
color : white;
|
||||
& + .sortDir { padding-left : 5px; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
.filter-option {
|
||||
margin-left: 20px;
|
||||
background-color : transparent !important;
|
||||
margin-left : 20px;
|
||||
font-size : 11px;
|
||||
i{
|
||||
padding-right : 5px;
|
||||
}
|
||||
background-color : transparent !important;
|
||||
i { padding-right : 5px; }
|
||||
}
|
||||
button {
|
||||
padding : 0;
|
||||
font-family : 'Open Sans', sans-serif;
|
||||
font-size : 11px;
|
||||
font-weight : normal;
|
||||
color : #CCCCCC;
|
||||
text-transform : uppercase;
|
||||
background-color : transparent;
|
||||
}
|
||||
button{
|
||||
background-color : transparent;
|
||||
font-family : 'Open Sans', sans-serif;
|
||||
text-transform : uppercase;
|
||||
font-weight : normal;
|
||||
font-size : 11px;
|
||||
color : #ccc;
|
||||
padding : 0;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
.tags-container {
|
||||
height : 30px;
|
||||
background-color : #555;
|
||||
border-top : 1px solid #666;
|
||||
border-bottom : 1px solid #666;
|
||||
color : white;
|
||||
display : flex;
|
||||
justify-content : center;
|
||||
align-items : center;
|
||||
column-gap : 15px;
|
||||
row-gap : 5px;
|
||||
flex-wrap : wrap;
|
||||
row-gap : 5px;
|
||||
column-gap : 15px;
|
||||
align-items : center;
|
||||
justify-content : center;
|
||||
height : 30px;
|
||||
color : white;
|
||||
background-color : #555555;
|
||||
border-top : 1px solid #666666;
|
||||
border-bottom : 1px solid #666666;
|
||||
span {
|
||||
padding : 3px;
|
||||
font-family : 'Open Sans', sans-serif;
|
||||
font-size : 11px;
|
||||
font-weight : bold;
|
||||
color : #DFDFDF;
|
||||
cursor : pointer;
|
||||
border : 1px solid;
|
||||
border-radius : 3px;
|
||||
padding : 3px;
|
||||
cursor : pointer;
|
||||
color: #dfdfdf;
|
||||
&:before {
|
||||
font-family: 'Font Awesome 5 Free';
|
||||
font-size: 12px;
|
||||
margin-right: 3px;
|
||||
&::before {
|
||||
margin-right : 3px;
|
||||
font-family : 'Font Awesome 6 Free';
|
||||
font-size : 12px;
|
||||
}
|
||||
&:after {
|
||||
content: '\f00d';
|
||||
font-family: 'Font Awesome 5 Free';
|
||||
font-size: 12px;
|
||||
margin-left: 3px;
|
||||
&::after {
|
||||
margin-left : 3px;
|
||||
font-family : 'Font Awesome 6 Free';
|
||||
font-size : 12px;
|
||||
content : '\f00d';
|
||||
}
|
||||
&.type {
|
||||
background-color: #008000;
|
||||
border-color: #00a000;
|
||||
&:before{
|
||||
content: '\f0ad';
|
||||
}
|
||||
background-color : #008000;
|
||||
border-color : #00A000;
|
||||
&::before { content : '\f0ad'; }
|
||||
}
|
||||
&.group {
|
||||
background-color: #505050;
|
||||
border-color: #000000;
|
||||
&:before{
|
||||
content: '\f500';
|
||||
}
|
||||
background-color : #505050;
|
||||
border-color : #000000;
|
||||
&::before { content : '\f500'; }
|
||||
}
|
||||
&.meta {
|
||||
background-color: #000080;
|
||||
border-color: #0000a0;
|
||||
&:before{
|
||||
content: '\f05a';
|
||||
}
|
||||
background-color : #000080;
|
||||
border-color : #0000A0;
|
||||
&::before { content : '\f05a'; }
|
||||
}
|
||||
&.system {
|
||||
background-color: #800000;
|
||||
border-color: #a00000;
|
||||
&:before{
|
||||
content: '\f518';
|
||||
}
|
||||
background-color : #800000;
|
||||
border-color : #A00000;
|
||||
&::before { content : '\f518'; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
.homebrew {
|
||||
.uiPage.sitePage {
|
||||
.content {
|
||||
width : ~"min(90vw, 1000px)";
|
||||
width : ~'min(90vw, 1000px)';
|
||||
padding : 2% 4%;
|
||||
margin-top : 25px;
|
||||
margin-right : auto;
|
||||
@@ -17,19 +17,19 @@
|
||||
border : 2px solid black;
|
||||
border-radius : 5px;
|
||||
button {
|
||||
width : 125px;
|
||||
margin-right : 5px;
|
||||
color : black;
|
||||
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;
|
||||
color : white;
|
||||
background-color : #00000077;
|
||||
&::before {
|
||||
margin-right : 5px;
|
||||
font-family : 'Font Awesome 6 Free';
|
||||
content : '\f00c';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -59,6 +59,11 @@
|
||||
padding-left : 1.25em;
|
||||
list-style : square;
|
||||
}
|
||||
.blank {
|
||||
height : 1em;
|
||||
margin-top : 0;
|
||||
& + * { margin-top : 0; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,22 +3,19 @@ require('./editPage.less');
|
||||
const React = require('react');
|
||||
const _ = require('lodash');
|
||||
const createClass = require('create-react-class');
|
||||
import {makePatches, applyPatches, stringifyPatches, parsePatches} from '@sanity/diff-match-patch';
|
||||
import { md5 } from 'hash-wasm';
|
||||
import { gzipSync, strToU8 } from 'fflate';
|
||||
|
||||
import request from '../../utils/request-middleware.js';
|
||||
const { Meta } = require('vitreum/headtags');
|
||||
|
||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||
const Navbar = require('../../navbar/navbar.jsx');
|
||||
|
||||
const NewBrew = require('../../navbar/newbrew.navitem.jsx');
|
||||
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
||||
const PrintNavItem = require('../../navbar/print.navitem.jsx');
|
||||
const ErrorNavItem = require('../../navbar/error-navitem.jsx');
|
||||
const Account = require('../../navbar/account.navitem.jsx');
|
||||
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
||||
const VaultNavItem = require('../../navbar/vault.navitem.jsx');
|
||||
|
||||
const SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
|
||||
const BaseEditPage = require('../basePages/editPage/editPage.jsx');
|
||||
const SplitPane = require('client/components/splitPane/splitPane.jsx');
|
||||
const Editor = require('../../editor/editor.jsx');
|
||||
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
||||
|
||||
@@ -47,7 +44,7 @@ const EditPage = createClass({
|
||||
return {
|
||||
brew : this.props.brew,
|
||||
isSaving : false,
|
||||
isPending : false,
|
||||
unsavedChanges : false,
|
||||
alertTrashedGoogleBrew : this.props.brew.trashed,
|
||||
alertLoginToTransfer : false,
|
||||
saveGoogle : this.props.brew.googleId ? true : false,
|
||||
@@ -85,7 +82,7 @@ const EditPage = createClass({
|
||||
});
|
||||
|
||||
window.onbeforeunload = ()=>{
|
||||
if(this.state.isSaving || this.state.isPending){
|
||||
if(this.state.isSaving || this.state.unsavedChanges){
|
||||
return 'You have unsaved changes!';
|
||||
}
|
||||
};
|
||||
@@ -104,9 +101,9 @@ const EditPage = createClass({
|
||||
},
|
||||
componentDidUpdate : function(){
|
||||
const hasChange = this.hasChanges();
|
||||
if(this.state.isPending != hasChange){
|
||||
if(this.state.unsavedChanges != hasChange){
|
||||
this.setState({
|
||||
isPending : hasChange
|
||||
unsavedChanges : hasChange
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -150,6 +147,18 @@ const EditPage = createClass({
|
||||
}), ()=>{if(this.state.autoSave) this.trySave();});
|
||||
},
|
||||
|
||||
handleSnipChange : function(snippet){
|
||||
//If there are errors, run the validator on every change to give quick feedback
|
||||
let htmlErrors = this.state.htmlErrors;
|
||||
if(htmlErrors.length) htmlErrors = Markdown.validate(snippet);
|
||||
|
||||
this.setState((prevState)=>({
|
||||
brew : { ...prevState.brew, snippets: snippet },
|
||||
unsavedChanges : true,
|
||||
htmlErrors : htmlErrors,
|
||||
}), ()=>{if(this.state.autoSave) this.trySave();});
|
||||
},
|
||||
|
||||
handleStyleChange : function(style){
|
||||
this.setState((prevState)=>({
|
||||
brew : { ...prevState.brew, style: style }
|
||||
@@ -176,20 +185,28 @@ const EditPage = createClass({
|
||||
this.setState((prevState)=>({
|
||||
brew : {
|
||||
...prevState.brew,
|
||||
style : newData.style,
|
||||
text : newData.text
|
||||
style : newData.style,
|
||||
text : newData.text,
|
||||
snippets : newData.snippets
|
||||
}
|
||||
}));
|
||||
},
|
||||
|
||||
trySave : function(immediate=false){
|
||||
if(!this.debounceSave) this.debounceSave = _.debounce(this.save, SAVE_TIMEOUT);
|
||||
if(this.hasChanges()){
|
||||
if(this.state.isSaving)
|
||||
return;
|
||||
|
||||
if(immediate) {
|
||||
this.debounceSave();
|
||||
} else {
|
||||
this.debounceSave.cancel();
|
||||
this.debounceSave.flush();
|
||||
return;
|
||||
}
|
||||
if(immediate) this.debounceSave.flush();
|
||||
|
||||
if(this.hasChanges())
|
||||
this.debounceSave();
|
||||
else
|
||||
this.debounceSave.cancel();
|
||||
},
|
||||
|
||||
handleGoogleClick : function(){
|
||||
@@ -203,8 +220,7 @@ const EditPage = createClass({
|
||||
confirmGoogleTransfer : !prevState.confirmGoogleTransfer
|
||||
}));
|
||||
this.setState({
|
||||
error : null,
|
||||
isSaving : false
|
||||
error : null
|
||||
});
|
||||
},
|
||||
|
||||
@@ -220,14 +236,16 @@ const EditPage = createClass({
|
||||
toggleGoogleStorage : function(){
|
||||
this.setState((prevState)=>({
|
||||
saveGoogle : !prevState.saveGoogle,
|
||||
isSaving : false,
|
||||
error : null
|
||||
}), ()=>this.save());
|
||||
}), ()=>this.trySave(true));
|
||||
},
|
||||
|
||||
save : async function(){
|
||||
if(this.debounceSave && this.debounceSave.cancel) this.debounceSave.cancel();
|
||||
|
||||
const brewState = this.state.brew; // freeze the current state
|
||||
const preSaveSnapshot = { ...brewState };
|
||||
|
||||
this.setState((prevState)=>({
|
||||
isSaving : true,
|
||||
error : null,
|
||||
@@ -237,15 +255,25 @@ const EditPage = createClass({
|
||||
await updateHistory(this.state.brew).catch(console.error);
|
||||
await versionHistoryGarbageCollection().catch(console.error);
|
||||
|
||||
//Prepare content to send to server
|
||||
const brew = { ...brewState };
|
||||
brew.text = brew.text.normalize('NFC');
|
||||
this.savedBrew.text = this.savedBrew.text.normalize('NFC');
|
||||
brew.pageCount = ((brew.renderer=='legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1;
|
||||
brew.patches = stringifyPatches(makePatches(encodeURI(this.savedBrew.text), encodeURI(brew.text)));
|
||||
brew.hash = await md5(this.savedBrew.text);
|
||||
//brew.text = undefined; - Temporary parallel path
|
||||
brew.textBin = undefined;
|
||||
|
||||
const compressedBrew = gzipSync(strToU8(JSON.stringify(brew)));
|
||||
|
||||
const transfer = this.state.saveGoogle == _.isNil(this.state.brew.googleId);
|
||||
|
||||
const brew = this.state.brew;
|
||||
brew.pageCount = ((brew.renderer=='legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1;
|
||||
|
||||
const params = `${transfer ? `?${this.state.saveGoogle ? 'saveToGoogle' : 'removeFromGoogle'}=true` : ''}`;
|
||||
const res = await request
|
||||
.put(`/api/update/${brew.editId}${params}`)
|
||||
.send(brew)
|
||||
.set('Content-Encoding', 'gzip')
|
||||
.set('Content-Type', 'application/json')
|
||||
.send(compressedBrew)
|
||||
.catch((err)=>{
|
||||
console.log('Error Updating Local Brew');
|
||||
this.setState({ error: err });
|
||||
@@ -253,20 +281,28 @@ const EditPage = createClass({
|
||||
if(!res) return;
|
||||
|
||||
this.savedBrew = {
|
||||
...this.state.brew,
|
||||
...preSaveSnapshot,
|
||||
googleId : res.body.googleId ? res.body.googleId : null,
|
||||
editId : res.body.editId,
|
||||
shareId : res.body.shareId,
|
||||
version : res.body.version
|
||||
};
|
||||
history.replaceState(null, null, `/edit/${this.savedBrew.editId}`);
|
||||
|
||||
this.setState(()=>({
|
||||
brew : this.savedBrew,
|
||||
isPending : false,
|
||||
this.setState((prevState) => ({
|
||||
brew: {
|
||||
...prevState.brew,
|
||||
googleId : res.body.googleId ? res.body.googleId : null,
|
||||
editId : res.body.editId,
|
||||
shareId : res.body.shareId,
|
||||
version : res.body.version
|
||||
},
|
||||
isSaving : false,
|
||||
unsavedTime : new Date()
|
||||
}));
|
||||
}), ()=>{
|
||||
this.setState({ unsavedChanges : this.hasChanges() });
|
||||
});
|
||||
|
||||
history.replaceState(null, null, `/edit/${this.savedBrew.editId}`);
|
||||
},
|
||||
|
||||
renderGoogleDriveIcon : function(){
|
||||
@@ -324,7 +360,7 @@ const EditPage = createClass({
|
||||
}
|
||||
|
||||
// #2 - Unsaved changes exist, autosave is OFF and warning timer has expired, show AUTOSAVE WARNING
|
||||
if(this.state.isPending && this.state.autoSaveWarning){
|
||||
if(this.state.unsavedChanges && this.state.autoSaveWarning){
|
||||
this.setAutosaveWarning();
|
||||
const elapsedTime = Math.round((new Date() - this.state.unsavedTime) / 1000 / 60);
|
||||
const text = elapsedTime == 0 ? 'Autosave is OFF.' : `Autosave is OFF, and you haven't saved for ${elapsedTime} minutes.`;
|
||||
@@ -339,7 +375,7 @@ const EditPage = createClass({
|
||||
|
||||
// #3 - Unsaved changes exist, click to save, show SAVE NOW
|
||||
// Use trySave(true) instead of save() to use debounced save function
|
||||
if(this.state.isPending){
|
||||
if(this.state.unsavedChanges){
|
||||
return <Nav.item className='save' onClick={()=>this.trySave(true)} color='blue' icon='fas fa-save'>Save Now</Nav.item>;
|
||||
}
|
||||
// #4 - No unsaved changes, autosave is ON, show AUTO-SAVED
|
||||
@@ -399,11 +435,7 @@ const EditPage = createClass({
|
||||
renderNavbar : function(){
|
||||
const shareLink = this.processShareId();
|
||||
|
||||
return <Navbar>
|
||||
<Nav.section>
|
||||
<Nav.item className='brewTitle'>{this.state.brew.title}</Nav.item>
|
||||
</Nav.section>
|
||||
|
||||
return <>
|
||||
<Nav.section>
|
||||
{this.renderGoogleDriveIcon()}
|
||||
{this.state.error ?
|
||||
@@ -413,8 +445,6 @@ const EditPage = createClass({
|
||||
{this.renderAutoSaveButton()}
|
||||
</Nav.dropdown>
|
||||
}
|
||||
<NewBrew />
|
||||
<HelpNavItem/>
|
||||
<Nav.dropdown>
|
||||
<Nav.item color='teal' icon='fas fa-share-alt'>
|
||||
share
|
||||
@@ -429,21 +459,20 @@ const EditPage = createClass({
|
||||
post to reddit
|
||||
</Nav.item>
|
||||
</Nav.dropdown>
|
||||
<PrintNavItem />
|
||||
<VaultNavItem />
|
||||
<RecentNavItem brew={this.state.brew} storageKey='edit' />
|
||||
<Account />
|
||||
</Nav.section>
|
||||
|
||||
</Navbar>;
|
||||
</>;
|
||||
},
|
||||
|
||||
render : function(){
|
||||
return <div className='editPage sitePage'>
|
||||
return <BaseEditPage
|
||||
className="editPage"
|
||||
errorState={this.state.error}
|
||||
parent={this}
|
||||
brew={this.state.brew}
|
||||
navButtons={this.renderNavbar()}
|
||||
recentStorageKey='edit'>
|
||||
<Meta name='robots' content='noindex, nofollow' />
|
||||
{this.renderNavbar()}
|
||||
|
||||
{this.props.brew.lock && <LockNotification shareId={this.props.brew.shareId} message={this.props.brew.lock.editMessage} />}
|
||||
{this.props.brew.lock && <LockNotification shareId={this.props.brew.shareId} message={this.props.brew.lock.editMessage} reviewRequested={this.props.brew.lock.reviewRequested} />}
|
||||
<div className='content'>
|
||||
<SplitPane onDragFinish={this.handleSplitMove}>
|
||||
<Editor
|
||||
@@ -451,12 +480,12 @@ const EditPage = createClass({
|
||||
brew={this.state.brew}
|
||||
onTextChange={this.handleTextChange}
|
||||
onStyleChange={this.handleStyleChange}
|
||||
onSnipChange={this.handleSnipChange}
|
||||
onMetaChange={this.handleMetaChange}
|
||||
reportError={this.errorReported}
|
||||
renderer={this.state.brew.renderer}
|
||||
userThemes={this.props.userThemes}
|
||||
themeBundle={this.state.themeBundle}
|
||||
snippetBundle={this.state.themeBundle.snippets}
|
||||
updateBrew={this.updateBrew}
|
||||
onCursorPageChange={this.handleEditorCursorPageChange}
|
||||
onViewPageChange={this.handleEditorViewPageChange}
|
||||
@@ -480,7 +509,7 @@ const EditPage = createClass({
|
||||
/>
|
||||
</SplitPane>
|
||||
</div>
|
||||
</div>;
|
||||
</BaseEditPage>;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,29 +1,25 @@
|
||||
@keyframes glideDown {
|
||||
0% {transform : translate(-50% + 3px, 0px);
|
||||
opacity : 0;}
|
||||
100% {transform : translate(-50% + 3px, 10px);
|
||||
opacity : 1;}
|
||||
0% {
|
||||
opacity : 0;transform : translate(-50% + 3px, 0px);}
|
||||
100% {
|
||||
opacity : 1;transform : translate(-50% + 3px, 10px);}
|
||||
}
|
||||
.editPage{
|
||||
.navItem.save{
|
||||
.editPage {
|
||||
.navItem.save {
|
||||
position : relative;
|
||||
width : 106px;
|
||||
text-align : center;
|
||||
position : relative;
|
||||
&.saved{
|
||||
&.saved {
|
||||
color : #666666;
|
||||
cursor : initial;
|
||||
color : #666;
|
||||
}
|
||||
}
|
||||
.googleDriveStorage {
|
||||
position : relative;
|
||||
}
|
||||
.googleDriveStorage img{
|
||||
height : 18px;
|
||||
.googleDriveStorage { position : relative; }
|
||||
.googleDriveStorage img {
|
||||
height : 18px;
|
||||
padding : 0px;
|
||||
margin : -5px;
|
||||
|
||||
&.inactive {
|
||||
filter: grayscale(1);
|
||||
}
|
||||
&.inactive { filter : grayscale(1); }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,30 @@
|
||||
require('./lockNotification.less');
|
||||
const React = require('react');
|
||||
import './lockNotification.less';
|
||||
import * as React from 'react';
|
||||
import request from '../../../utils/request-middleware.js';
|
||||
import Dialog from '../../../../components/dialog.jsx';
|
||||
|
||||
function LockNotification(props) {
|
||||
props = {
|
||||
shareId : 0,
|
||||
disableLock : ()=>{},
|
||||
message : '',
|
||||
shareId : 0,
|
||||
disableLock : ()=>{},
|
||||
lock : {},
|
||||
message : 'Unable to retrieve Lock Message',
|
||||
reviewRequested : false,
|
||||
...props
|
||||
};
|
||||
|
||||
const removeLock = ()=>{
|
||||
alert(`Not yet implemented - ID ${props.shareId}`);
|
||||
const [reviewState, setReviewState] = React.useState(props.reviewRequested);
|
||||
|
||||
const removeLock = async ()=>{
|
||||
await request.put(`/api/lock/review/request/${props.shareId}`)
|
||||
.then(()=>{
|
||||
setReviewState(true);
|
||||
});
|
||||
};
|
||||
|
||||
const renderReviewButton = function(){
|
||||
if(reviewState){ return <button className='inactive'>REVIEW REQUESTED</button>; };
|
||||
return <button onClick={removeLock}>REQUEST LOCK REMOVAL</button>;
|
||||
};
|
||||
|
||||
return <Dialog className='lockNotification' blocking closeText='CONTINUE TO EDITOR' >
|
||||
@@ -19,11 +32,11 @@ function LockNotification(props) {
|
||||
<p>This brew been locked by the Administrators. It will not be accessible by any method other than the Editor until the lock is removed.</p>
|
||||
<hr />
|
||||
<h3>LOCK REASON</h3>
|
||||
<p>{props.message || 'Unable to retrieve Lock Message'}</p>
|
||||
<p>{props.message}</p>
|
||||
<hr />
|
||||
<p>Once you have resolved this issue, click REQUEST LOCK REMOVAL to notify the Administrators for review.</p>
|
||||
<p>Click CONTINUE TO EDITOR to temporarily hide this notification; it will reappear the next time the page is reloaded.</p>
|
||||
<button onClick={removeLock}>REQUEST LOCK REMOVAL</button>
|
||||
{renderReviewButton()}
|
||||
</Dialog>;
|
||||
};
|
||||
|
||||
|
||||
@@ -11,10 +11,12 @@
|
||||
&::backdrop { background-color : #000000AA; }
|
||||
|
||||
button {
|
||||
padding : 2px 15px;
|
||||
margin : 10px;
|
||||
color : white;
|
||||
background-color : #333333;
|
||||
|
||||
&.inactive,
|
||||
&:hover { background-color : #777777; }
|
||||
}
|
||||
|
||||
|
||||
@@ -167,7 +167,7 @@ const errorIndex = (props)=>{
|
||||
**Requested access:** ${props.brew.accessType}
|
||||
|
||||
**Brew ID:** ${props.brew.brewId}`,
|
||||
|
||||
|
||||
// Theme Not Valid
|
||||
'10' : dedent`
|
||||
## The selected theme is not tagged as a theme.
|
||||
@@ -176,6 +176,26 @@ const errorIndex = (props)=>{
|
||||
|
||||
If the selected brew is your document, you may designate it as a theme by adding the \`theme:meta\` tag.`,
|
||||
|
||||
// ID validation error
|
||||
'11' : dedent`
|
||||
## No Homebrewery document could be found.
|
||||
|
||||
The server could not locate the Homebrewery document. The Brew ID failed the validation check.
|
||||
|
||||
:
|
||||
|
||||
**Brew ID:** ${props.brew.brewId}`,
|
||||
|
||||
// Google ID validation error
|
||||
'12' : dedent`
|
||||
## No Google document could be found.
|
||||
|
||||
The server could not locate the Google document. The Google ID failed the validation check.
|
||||
|
||||
:
|
||||
|
||||
**Brew ID:** ${props.brew.brewId}`,
|
||||
|
||||
//account page when account is not defined
|
||||
'50' : dedent`
|
||||
## You are not signed in
|
||||
@@ -194,13 +214,47 @@ const errorIndex = (props)=>{
|
||||
|
||||
**Brew ID:** ${props.brew.brewId}
|
||||
|
||||
**Brew Title:** ${escape(props.brew.brewTitle)}`,
|
||||
**Brew Title:** ${escape(props.brew.brewTitle)}
|
||||
|
||||
**Brew Authors:** ${props.brew.authors?.map((author)=>{return `[${author}](/user/${author})`;}).join(', ') || 'Unable to list authors'}`,
|
||||
|
||||
// ####### Admin page error #######
|
||||
'52' : dedent`
|
||||
## Access Denied
|
||||
You need to provide correct administrator credentials to access this page.`,
|
||||
|
||||
// ####### Lock Errors
|
||||
|
||||
'60' : dedent`Lock Error: General`,
|
||||
|
||||
'61' : dedent`Lock Get Error: Unable to get lock count`,
|
||||
|
||||
'62' : dedent`Lock Set Error: Cannot lock`,
|
||||
|
||||
'63' : dedent`Lock Set Error: Brew not found`,
|
||||
|
||||
'64' : dedent`Lock Set Error: Already locked`,
|
||||
|
||||
'65' : dedent`Lock Remove Error: Cannot unlock`,
|
||||
|
||||
'66' : dedent`Lock Remove Error: Brew not found`,
|
||||
|
||||
'67' : dedent`Lock Remove Error: Not locked`,
|
||||
|
||||
'68' : dedent`Lock Get Review Error: Cannot get review requests`,
|
||||
|
||||
'69' : dedent`Lock Set Review Error: Cannot set review request`,
|
||||
|
||||
'70' : dedent`Lock Set Review Error: Brew not found`,
|
||||
|
||||
'71' : dedent`Lock Set Review Error: Review already requested`,
|
||||
|
||||
'72' : dedent`Lock Remove Review Error: Cannot clear review request`,
|
||||
|
||||
'73' : dedent`Lock Remove Review Error: Brew not found`,
|
||||
|
||||
// ####### Other Errors
|
||||
|
||||
'90' : dedent` An unexpected error occurred while looking for these brews.
|
||||
Try again in a few minutes.`,
|
||||
|
||||
|
||||
@@ -6,16 +6,11 @@ import request from '../../utils/request-middleware.js';
|
||||
const { Meta } = require('vitreum/headtags');
|
||||
|
||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||
const Navbar = require('../../navbar/navbar.jsx');
|
||||
const NewBrewItem = require('../../navbar/newbrew.navitem.jsx');
|
||||
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
||||
const VaultNavItem = require('../../navbar/vault.navitem.jsx');
|
||||
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
||||
const AccountNavItem = require('../../navbar/account.navitem.jsx');
|
||||
const ErrorNavItem = require('../../navbar/error-navitem.jsx');
|
||||
const { fetchThemeBundle } = require('../../../../shared/helpers.js');
|
||||
|
||||
const SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
|
||||
const BaseEditPage = require('../basePages/editPage/editPage.jsx');
|
||||
const SplitPane = require('client/components/splitPane/splitPane.jsx');
|
||||
const Editor = require('../../editor/editor.jsx');
|
||||
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
||||
|
||||
@@ -25,13 +20,13 @@ const HomePage = createClass({
|
||||
displayName : 'HomePage',
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
brew : DEFAULT_BREW,
|
||||
ver : '0.0.0'
|
||||
brew : DEFAULT_BREW
|
||||
};
|
||||
},
|
||||
getInitialState : function() {
|
||||
return {
|
||||
brew : this.props.brew,
|
||||
isSaving : false,
|
||||
welcomeText : this.props.brew.text,
|
||||
error : undefined,
|
||||
currentEditorViewPageNum : 1,
|
||||
@@ -47,7 +42,11 @@ const HomePage = createClass({
|
||||
fetchThemeBundle(this, this.props.brew.renderer, this.props.brew.theme);
|
||||
},
|
||||
|
||||
handleSave : function(){
|
||||
save : function(){
|
||||
this.setState({
|
||||
isSaving : true
|
||||
});
|
||||
|
||||
request.post('/api')
|
||||
.send(this.state.brew)
|
||||
.end((err, res)=>{
|
||||
@@ -57,6 +56,9 @@ const HomePage = createClass({
|
||||
}
|
||||
const brew = res.body;
|
||||
window.location = `/edit/${brew.editId}`;
|
||||
})
|
||||
.catch((err)=>{
|
||||
this.setState({ isSaving: false, error: err });
|
||||
});
|
||||
},
|
||||
handleSplitMove : function(){
|
||||
@@ -80,61 +82,73 @@ const HomePage = createClass({
|
||||
brew : { ...prevState.brew, text: text },
|
||||
}));
|
||||
},
|
||||
|
||||
renderSaveButton : function(){
|
||||
if(this.state.isSaving){
|
||||
return <Nav.item icon='fas fa-spinner fa-spin' className='save'>
|
||||
save...
|
||||
</Nav.item>;
|
||||
} else {
|
||||
return <Nav.item icon='fas fa-save' className='save' onClick={this.save}>
|
||||
save
|
||||
</Nav.item>;
|
||||
}
|
||||
},
|
||||
|
||||
renderNavbar : function(){
|
||||
return <Navbar ver={this.props.ver}>
|
||||
return <>
|
||||
<Nav.section>
|
||||
{this.state.error ?
|
||||
<ErrorNavItem error={this.state.error} parent={this}></ErrorNavItem> :
|
||||
null
|
||||
}
|
||||
<NewBrewItem />
|
||||
<HelpNavItem />
|
||||
<VaultNavItem />
|
||||
<RecentNavItem />
|
||||
<AccountNavItem />
|
||||
</Nav.section>
|
||||
</Navbar>;
|
||||
</>;
|
||||
},
|
||||
|
||||
render : function(){
|
||||
return <div className='homePage sitePage'>
|
||||
return <BaseEditPage
|
||||
className="homePage"
|
||||
errorState={this.state.error}
|
||||
parent={this}
|
||||
brew={this.state.brew}
|
||||
navButtons={this.renderNavbar()}>
|
||||
<Meta name='google-site-verification' content='NwnAQSSJZzAT7N-p5MY6ydQ7Njm67dtbu73ZSyE5Fy4' />
|
||||
{this.renderNavbar()}
|
||||
<div className="content">
|
||||
<SplitPane onDragFinish={this.handleSplitMove}>
|
||||
<Editor
|
||||
ref={this.editor}
|
||||
brew={this.state.brew}
|
||||
onTextChange={this.handleTextChange}
|
||||
renderer={this.state.brew.renderer}
|
||||
showEditButtons={false}
|
||||
snippetBundle={this.state.themeBundle.snippets}
|
||||
onCursorPageChange={this.handleEditorCursorPageChange}
|
||||
onViewPageChange={this.handleEditorViewPageChange}
|
||||
currentEditorViewPageNum={this.state.currentEditorViewPageNum}
|
||||
currentEditorCursorPageNum={this.state.currentEditorCursorPageNum}
|
||||
currentBrewRendererPageNum={this.state.currentBrewRendererPageNum}
|
||||
/>
|
||||
<BrewRenderer
|
||||
text={this.state.brew.text}
|
||||
style={this.state.brew.style}
|
||||
renderer={this.state.brew.renderer}
|
||||
onPageChange={this.handleBrewRendererPageChange}
|
||||
currentEditorViewPageNum={this.state.currentEditorViewPageNum}
|
||||
currentEditorCursorPageNum={this.state.currentEditorCursorPageNum}
|
||||
currentBrewRendererPageNum={this.state.currentBrewRendererPageNum}
|
||||
themeBundle={this.state.themeBundle}
|
||||
/>
|
||||
</SplitPane>
|
||||
<div className='content'>
|
||||
<SplitPane onDragFinish={this.handleSplitMove}>
|
||||
<Editor
|
||||
ref={this.editor}
|
||||
brew={this.state.brew}
|
||||
onTextChange={this.handleTextChange}
|
||||
renderer={this.state.brew.renderer}
|
||||
showEditButtons={false}
|
||||
themeBundle={this.state.themeBundle}
|
||||
onCursorPageChange={this.handleEditorCursorPageChange}
|
||||
onViewPageChange={this.handleEditorViewPageChange}
|
||||
currentEditorViewPageNum={this.state.currentEditorViewPageNum}
|
||||
currentEditorCursorPageNum={this.state.currentEditorCursorPageNum}
|
||||
currentBrewRendererPageNum={this.state.currentBrewRendererPageNum}
|
||||
/>
|
||||
<BrewRenderer
|
||||
text={this.state.brew.text}
|
||||
style={this.state.brew.style}
|
||||
renderer={this.state.brew.renderer}
|
||||
onPageChange={this.handleBrewRendererPageChange}
|
||||
currentEditorViewPageNum={this.state.currentEditorViewPageNum}
|
||||
currentEditorCursorPageNum={this.state.currentEditorCursorPageNum}
|
||||
currentBrewRendererPageNum={this.state.currentBrewRendererPageNum}
|
||||
themeBundle={this.state.themeBundle}
|
||||
/>
|
||||
</SplitPane>
|
||||
</div>
|
||||
<div className={cx('floatingSaveButton', { show: this.state.welcomeText != this.state.brew.text })} onClick={this.handleSave}>
|
||||
<div className={cx('floatingSaveButton', { show: this.state.welcomeText != this.state.brew.text })} onClick={this.save}>
|
||||
Save current <i className='fas fa-save' />
|
||||
</div>
|
||||
|
||||
<a href='/new' className='floatingNewButton'>
|
||||
Create your own <i className='fas fa-magic' />
|
||||
</a>
|
||||
</div>;
|
||||
</BaseEditPage>
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,50 +1,40 @@
|
||||
.homePage{
|
||||
.homePage {
|
||||
position : relative;
|
||||
a.floatingNewButton{
|
||||
a.floatingNewButton {
|
||||
.animate(background-color);
|
||||
position : absolute;
|
||||
display : block;
|
||||
right : 70px;
|
||||
bottom : 50px;
|
||||
z-index : 100;
|
||||
z-index : 5001;
|
||||
display : block;
|
||||
padding : 1em;
|
||||
background-color : @orange;
|
||||
font-size : 1.5em;
|
||||
color : white;
|
||||
text-decoration : none;
|
||||
background-color : @orange;
|
||||
box-shadow : 3px 3px 15px black;
|
||||
&:hover{
|
||||
background-color : darken(@orange, 20%);
|
||||
}
|
||||
&:hover { background-color : darken(@orange, 20%); }
|
||||
}
|
||||
.floatingSaveButton{
|
||||
.floatingSaveButton {
|
||||
.animateAll();
|
||||
position : absolute;
|
||||
display : block;
|
||||
right : 200px;
|
||||
bottom : 70px;
|
||||
z-index : 100;
|
||||
z-index : 5000;
|
||||
display : block;
|
||||
padding : 0.8em;
|
||||
cursor : pointer;
|
||||
background-color : @blue;
|
||||
font-size : 0.8em;
|
||||
color : white;
|
||||
text-decoration : none;
|
||||
cursor : pointer;
|
||||
background-color : @blue;
|
||||
box-shadow : 3px 3px 15px black;
|
||||
&:hover{
|
||||
background-color : darken(@blue, 20%);
|
||||
}
|
||||
&.show{
|
||||
right : 350px;
|
||||
}
|
||||
&:hover { background-color : darken(@blue, 20%); }
|
||||
&.show { right : 350px; }
|
||||
}
|
||||
|
||||
.navItem.save{
|
||||
background-color: @orange;
|
||||
&:hover{
|
||||
background-color: @green;
|
||||
}
|
||||
.navItem.save {
|
||||
background-color : @orange;
|
||||
&:hover { background-color : @green; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,14 +7,10 @@ import request from '../../utils/request-middleware.js';
|
||||
import Markdown from 'naturalcrit/markdown.js';
|
||||
|
||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||
const PrintNavItem = require('../../navbar/print.navitem.jsx');
|
||||
const Navbar = require('../../navbar/navbar.jsx');
|
||||
const AccountNavItem = require('../../navbar/account.navitem.jsx');
|
||||
const ErrorNavItem = require('../../navbar/error-navitem.jsx');
|
||||
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
||||
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
||||
|
||||
const SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
|
||||
const BaseEditPage = require('../basePages/editPage/editPage.jsx');
|
||||
const SplitPane = require('client/components/splitPane/splitPane.jsx');
|
||||
const Editor = require('../../editor/editor.jsx');
|
||||
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
||||
|
||||
@@ -141,6 +137,17 @@ const NewPage = createClass({
|
||||
localStorage.setItem(STYLEKEY, style);
|
||||
},
|
||||
|
||||
handleSnipChange : function(snippet){
|
||||
//If there are errors, run the validator on every change to give quick feedback
|
||||
let htmlErrors = this.state.htmlErrors;
|
||||
if(htmlErrors.length) htmlErrors = Markdown.validate(snippet);
|
||||
|
||||
this.setState((prevState)=>({
|
||||
brew : { ...prevState.brew, snippets: snippet },
|
||||
htmlErrors : htmlErrors,
|
||||
}), ()=>{if(this.state.autoSave) this.trySave();});
|
||||
},
|
||||
|
||||
handleMetaChange : function(metadata, field=undefined){
|
||||
if(field == 'theme' || field == 'renderer') // Fetch theme bundle only if theme or renderer was changed
|
||||
fetchThemeBundle(this, metadata.renderer, metadata.theme);
|
||||
@@ -201,63 +208,58 @@ const NewPage = createClass({
|
||||
},
|
||||
|
||||
renderNavbar : function(){
|
||||
return <Navbar>
|
||||
|
||||
<Nav.section>
|
||||
<Nav.item className='brewTitle'>{this.state.brew.title}</Nav.item>
|
||||
</Nav.section>
|
||||
|
||||
return <>
|
||||
<Nav.section>
|
||||
{this.state.error ?
|
||||
<ErrorNavItem error={this.state.error} parent={this}></ErrorNavItem> :
|
||||
this.renderSaveButton()
|
||||
}
|
||||
<PrintNavItem />
|
||||
<HelpNavItem />
|
||||
<RecentNavItem />
|
||||
<AccountNavItem />
|
||||
</Nav.section>
|
||||
</Navbar>;
|
||||
</>;
|
||||
},
|
||||
|
||||
render : function(){
|
||||
return <div className='newPage sitePage'>
|
||||
{this.renderNavbar()}
|
||||
<div className="content">
|
||||
<SplitPane onDragFinish={this.handleSplitMove}>
|
||||
<Editor
|
||||
ref={this.editor}
|
||||
brew={this.state.brew}
|
||||
onTextChange={this.handleTextChange}
|
||||
onStyleChange={this.handleStyleChange}
|
||||
onMetaChange={this.handleMetaChange}
|
||||
renderer={this.state.brew.renderer}
|
||||
userThemes={this.props.userThemes}
|
||||
themeBundle={this.state.themeBundle}
|
||||
snippetBundle={this.state.themeBundle.snippets}
|
||||
onCursorPageChange={this.handleEditorCursorPageChange}
|
||||
onViewPageChange={this.handleEditorViewPageChange}
|
||||
currentEditorViewPageNum={this.state.currentEditorViewPageNum}
|
||||
currentEditorCursorPageNum={this.state.currentEditorCursorPageNum}
|
||||
currentBrewRendererPageNum={this.state.currentBrewRendererPageNum}
|
||||
/>
|
||||
<BrewRenderer
|
||||
text={this.state.brew.text}
|
||||
style={this.state.brew.style}
|
||||
renderer={this.state.brew.renderer}
|
||||
theme={this.state.brew.theme}
|
||||
themeBundle={this.state.themeBundle}
|
||||
errors={this.state.htmlErrors}
|
||||
lang={this.state.brew.lang}
|
||||
onPageChange={this.handleBrewRendererPageChange}
|
||||
currentEditorViewPageNum={this.state.currentEditorViewPageNum}
|
||||
currentEditorCursorPageNum={this.state.currentEditorCursorPageNum}
|
||||
currentBrewRendererPageNum={this.state.currentBrewRendererPageNum}
|
||||
allowPrint={true}
|
||||
/>
|
||||
</SplitPane>
|
||||
return <BaseEditPage
|
||||
className="newPage"
|
||||
errorState={this.state.error}
|
||||
parent={this}
|
||||
brew={this.state.brew}
|
||||
navButtons={this.renderNavbar()}>
|
||||
<div className='content'>
|
||||
<SplitPane onDragFinish={this.handleSplitMove}>
|
||||
<Editor
|
||||
ref={this.editor}
|
||||
brew={this.state.brew}
|
||||
onTextChange={this.handleTextChange}
|
||||
onStyleChange={this.handleStyleChange}
|
||||
onMetaChange={this.handleMetaChange}
|
||||
onSnipChange={this.handleSnipChange}
|
||||
renderer={this.state.brew.renderer}
|
||||
userThemes={this.props.userThemes}
|
||||
themeBundle={this.state.themeBundle}
|
||||
onCursorPageChange={this.handleEditorCursorPageChange}
|
||||
onViewPageChange={this.handleEditorViewPageChange}
|
||||
currentEditorViewPageNum={this.state.currentEditorViewPageNum}
|
||||
currentEditorCursorPageNum={this.state.currentEditorCursorPageNum}
|
||||
currentBrewRendererPageNum={this.state.currentBrewRendererPageNum}
|
||||
/>
|
||||
<BrewRenderer
|
||||
text={this.state.brew.text}
|
||||
style={this.state.brew.style}
|
||||
renderer={this.state.brew.renderer}
|
||||
theme={this.state.brew.theme}
|
||||
themeBundle={this.state.themeBundle}
|
||||
errors={this.state.htmlErrors}
|
||||
lang={this.state.brew.lang}
|
||||
onPageChange={this.handleBrewRendererPageChange}
|
||||
currentEditorViewPageNum={this.state.currentEditorViewPageNum}
|
||||
currentEditorCursorPageNum={this.state.currentEditorCursorPageNum}
|
||||
currentBrewRendererPageNum={this.state.currentBrewRendererPageNum}
|
||||
allowPrint={true}
|
||||
/>
|
||||
</SplitPane>
|
||||
</div>
|
||||
</div>;
|
||||
</BaseEditPage>;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
.newPage{
|
||||
.navItem.save{
|
||||
background-color: @orange;
|
||||
&:hover{
|
||||
background-color: @green;
|
||||
}
|
||||
.newPage {
|
||||
.navItem.save {
|
||||
background-color : @orange;
|
||||
&:hover { background-color : @green; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
.sharePage{
|
||||
.sharePage {
|
||||
nav .navSection.titleSection {
|
||||
flex-grow: 1;
|
||||
justify-content: center;
|
||||
}
|
||||
.content{
|
||||
overflow-y : hidden;
|
||||
flex-grow : 1;
|
||||
justify-content : center;
|
||||
}
|
||||
.content { overflow-y : hidden; }
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ const Account = require('../../navbar/account.navitem.jsx');
|
||||
const NewBrew = require('../../navbar/newbrew.navitem.jsx');
|
||||
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
||||
const BrewItem = require('../basePages/listPage/brewItem/brewItem.jsx');
|
||||
const SplitPane = require('../../../../shared/naturalcrit/splitPane/splitPane.jsx');
|
||||
const SplitPane = require('client/components/splitPane/splitPane.jsx');
|
||||
const ErrorIndex = require('../errorPage/errors/errorIndex.js');
|
||||
|
||||
import request from '../../utils/request-middleware.js';
|
||||
@@ -99,14 +99,14 @@ const VaultPage = (props)=>{
|
||||
setSearching(true);
|
||||
setError(null);
|
||||
|
||||
const title = titleRef.current.value || '';
|
||||
const author = authorRef.current.value || '';
|
||||
const count = countRef.current.value || 10;
|
||||
const v3 = v3Ref.current.checked != false;
|
||||
const legacy = legacyRef.current.checked != false;
|
||||
const title = titleRef.current.value || '';
|
||||
const author = authorRef.current.value || '';
|
||||
const count = countRef.current.value || 10;
|
||||
const v3 = v3Ref.current.checked != false;
|
||||
const legacy = legacyRef.current.checked != false;
|
||||
const sortOption = sort || 'title';
|
||||
const dirOption = dir || 'asc';
|
||||
const pageProp = page || 1;
|
||||
const dirOption = dir || 'asc';
|
||||
const pageProp = page || 1;
|
||||
|
||||
setSort(sortOption);
|
||||
setdir(dirOption);
|
||||
@@ -247,7 +247,7 @@ const VaultPage = (props)=>{
|
||||
</li>
|
||||
<li>
|
||||
Some common words like "a", "after", "through", "itself", "here", etc.,
|
||||
are ignored in searches. The full list can be found
|
||||
are ignored in searches. The full list can be found
|
||||
<a href='https://github.com/mongodb/mongo/blob/0e3b3ca8480ddddf5d0105d11a94bd4698335312/src/mongo/db/fts/stop_words_english.txt'>
|
||||
here
|
||||
</a>
|
||||
@@ -286,9 +286,9 @@ const VaultPage = (props)=>{
|
||||
};
|
||||
|
||||
const renderPaginationControls = ()=>{
|
||||
if(!totalBrews) return null;
|
||||
if(!totalBrews || totalBrews < 10) return null;
|
||||
|
||||
const countInt = parseInt(props.query.count || 20);
|
||||
const countInt = parseInt(brewCollection.length || 20);
|
||||
const totalPages = Math.ceil(totalBrews / countInt);
|
||||
|
||||
let startPage, endPage;
|
||||
@@ -355,7 +355,7 @@ const VaultPage = (props)=>{
|
||||
};
|
||||
|
||||
const renderFoundBrews = ()=>{
|
||||
if(searching) {
|
||||
if(searching && !brewCollection) {
|
||||
return (
|
||||
<div className='foundBrews searching'>
|
||||
<h3 className='searchAnim'>Searching</h3>
|
||||
@@ -395,6 +395,7 @@ const VaultPage = (props)=>{
|
||||
{`Brews found: `}
|
||||
<span>{totalBrews}</span>
|
||||
</span>
|
||||
{brewCollection.length > 10 && renderPaginationControls()}
|
||||
{brewCollection.map((brew, index)=>{
|
||||
return (
|
||||
<BrewItem
|
||||
@@ -415,14 +416,14 @@ const VaultPage = (props)=>{
|
||||
<link href='/themes/V3/Blank/style.css' rel='stylesheet' />
|
||||
<link href='/themes/V3/5ePHB/style.css' rel='stylesheet' />
|
||||
{renderNavItems()}
|
||||
<div className="content">
|
||||
<SplitPane showDividerButtons={false}>
|
||||
<div className='form dataGroup'>{renderForm()}</div>
|
||||
<div className='resultsContainer dataGroup'>
|
||||
{renderSortBar()}
|
||||
{renderFoundBrews()}
|
||||
</div>
|
||||
</SplitPane>
|
||||
<div className='content'>
|
||||
<SplitPane showDividerButtons={false}>
|
||||
<div className='form dataGroup'>{renderForm()}</div>
|
||||
<div className='resultsContainer dataGroup'>
|
||||
{renderSortBar()}
|
||||
{renderFoundBrews()}
|
||||
</div>
|
||||
</SplitPane>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
*:not(input) { user-select : none; }
|
||||
|
||||
.content .dataGroup {
|
||||
:where(.content .dataGroup) {
|
||||
width : 100%;
|
||||
height : 100%;
|
||||
background : white;
|
||||
@@ -169,9 +169,10 @@
|
||||
width : 100%;
|
||||
height : 100%;
|
||||
max-height : 100%;
|
||||
padding : 50px 50px 70px 50px;
|
||||
padding : 70px 50px;
|
||||
overflow-y : scroll;
|
||||
background-color : #2C3E50;
|
||||
container-type : inline-size;
|
||||
|
||||
h3 { font-size : 25px; }
|
||||
|
||||
@@ -236,6 +237,7 @@
|
||||
margin-right : 40px;
|
||||
color : black;
|
||||
isolation : isolate;
|
||||
transition : width 0.5s;
|
||||
|
||||
&::after {
|
||||
position : absolute;
|
||||
@@ -269,8 +271,8 @@
|
||||
.links { z-index : 2; }
|
||||
|
||||
hr {
|
||||
margin : 0px;
|
||||
visibility : hidden;
|
||||
margin : 0px;
|
||||
}
|
||||
|
||||
.thumbnail { z-index : -1; }
|
||||
@@ -278,30 +280,37 @@
|
||||
|
||||
.paginationControls {
|
||||
position : absolute;
|
||||
top : 35px;
|
||||
left : 50%;
|
||||
display : grid;
|
||||
grid-template-areas : 'previousPage currentPage nextPage';
|
||||
grid-template-columns : 50px 1fr 50px;
|
||||
gap : 20px;
|
||||
place-items : center;
|
||||
width : auto;
|
||||
font-size : 15px;
|
||||
translate : -50%;
|
||||
|
||||
&:last-child { top : unset; }
|
||||
|
||||
.pages {
|
||||
display : flex;
|
||||
grid-area : currentPage;
|
||||
gap : 1em;
|
||||
justify-content : space-evenly;
|
||||
width : 100%;
|
||||
height : 100%;
|
||||
padding : 5px 8px;
|
||||
text-align : center;
|
||||
|
||||
.pageNumber {
|
||||
margin-inline : 1vw;
|
||||
place-content : center;
|
||||
width : fit-content;
|
||||
min-width : 2em;
|
||||
font-family : 'Open Sans';
|
||||
font-weight : 900;
|
||||
color : white;
|
||||
text-underline-position : under;
|
||||
text-wrap : nowrap;
|
||||
text-underline-position : under;
|
||||
cursor : pointer;
|
||||
|
||||
&.currentPage {
|
||||
@@ -329,7 +338,6 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@keyframes trailingDots {
|
||||
@@ -344,8 +352,7 @@
|
||||
100% { content : ' ...'; }
|
||||
}
|
||||
|
||||
// media query for when the page is smaller than 1079 px in width
|
||||
@media screen and (max-width : 1079px) {
|
||||
@container (width < 670px) {
|
||||
.vaultPage {
|
||||
|
||||
.dataGroup.form .brewLookup { padding : 1px 20px 20px 10px; }
|
||||
|
||||
64
client/homebrew/thumbnail.svg
Normal file
64
client/homebrew/thumbnail.svg
Normal file
@@ -0,0 +1,64 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
viewBox="0 0 94.65 94.6"
|
||||
version="1.1"
|
||||
id="svg11"
|
||||
sodipodi:docname="thumbnail.svg"
|
||||
inkscape:version="1.4 (e7c3feb100, 2024-10-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"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||
<sodipodi:namedview
|
||||
id="namedview13"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#111111"
|
||||
borderopacity="1"
|
||||
inkscape:showpageshadow="0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pagecheckerboard="1"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
showgrid="false"
|
||||
inkscape:zoom="8.4989431"
|
||||
inkscape:cx="38.887188"
|
||||
inkscape:cy="47.417661"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1043"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg11" />
|
||||
<defs
|
||||
id="defs4">
|
||||
<style
|
||||
id="style2">.cls-1{fill:#ed1f24;}</style>
|
||||
</defs>
|
||||
<title
|
||||
id="title6">NaturalCritLogo</title>
|
||||
<g
|
||||
id="Layer_2"
|
||||
data-name="Layer 2"
|
||||
style="fill:#000000;stroke:#000000">
|
||||
<g
|
||||
id="base"
|
||||
style="fill:#000000;stroke:#000000">
|
||||
<path
|
||||
id="D20"
|
||||
class="cls-1"
|
||||
d="M63.45.09s-45.91,12.4-46,12.45a.71.71,0,0,0-.15.08l-.15.1-.12.11a1.07,1.07,0,0,0-.14.16l-.09.11-.12.23,0,.06L.2,54.9a1.59,1.59,0,0,0,.11,1.69L29.36,94h0l0,0,.08.08.08.08.09.09.08.06.13.07a0,0,0,0,0,0,0,1.59,1.59,0,0,0,.27.12l.13.05.06,0a1.55,1.55,0,0,0,.37,0,1.63,1.63,0,0,0,.31,0l45.67-8.3.16,0,.11,0,.12,0,.06,0s0,0,0,0l.06,0a1.65,1.65,0,0,0,.36-.28l0-.06a1.6,1.6,0,0,0,.26-.38s0,0,0,0v0h0a.14.14,0,0,1,0-.06L94.52,43.74a1.4,1.4,0,0,0,.11-.4.41.41,0,0,0,0-.11,1.13,1.13,0,0,0,0-.26.66.66,0,0,0,0-.14,2,2,0,0,0-.06-.26l0-.11a2.68,2.68,0,0,0-.18-.33v0L65.29.6C64.77-.31,63.45.09,63.45.09ZM74.9,81.7l-28.81-18L78.5,38.49ZM44.1,61l-11-40.17L77,35.39ZM82,37.78l8.92,5.95L79,73.48Zm4.46-1.1-4.6-3.06L75.69,21.36Zm-9.26-4.8-42.07-14,28.05-14ZM30.56,16.34l-6.49-2.16L47.85,7.7Zm-11.35-.21L27.88,19,7.64,45Zm10.73,5.76L40.78,61.64,4.64,54.42Zm10.82,43.2L30.26,89.6,5.75,58.09Zm3.16,1.24L71.74,83.72l-38.26,7Z"
|
||||
style="fill:#000000;fill-opacity:1;stroke:#000000" />
|
||||
</g>
|
||||
</g>
|
||||
<metadata
|
||||
id="metadata1">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:title>NaturalCritLogo</dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
@@ -42,6 +42,7 @@ function parseBrewForStorage(brew, slot = 0) {
|
||||
title : brew.title,
|
||||
text : brew.text,
|
||||
style : brew.style,
|
||||
snippets : brew.snippets,
|
||||
version : brew.version,
|
||||
shareId : brew.shareId,
|
||||
savedAt : brew?.savedAt || new Date(),
|
||||
|
||||
@@ -1,84 +1,34 @@
|
||||
.fac {
|
||||
display : inline-block;
|
||||
background-color : currentColor;
|
||||
mask-size : contain;
|
||||
mask-repeat : no-repeat;
|
||||
mask-position : center;
|
||||
width : 1em;
|
||||
aspect-ratio : 1;
|
||||
background-color : currentColor;
|
||||
mask-repeat : no-repeat;
|
||||
mask-position : center;
|
||||
mask-size : contain;
|
||||
}
|
||||
.position-top-left {
|
||||
mask-image: url('../icons/position-top-left.svg');
|
||||
}
|
||||
.position-top-right {
|
||||
mask-image: url('../icons/position-top-right.svg');
|
||||
}
|
||||
.position-bottom-left {
|
||||
mask-image: url('../icons/position-bottom-left.svg');
|
||||
}
|
||||
.position-bottom-right {
|
||||
mask-image: url('../icons/position-bottom-right.svg');
|
||||
}
|
||||
.position-top {
|
||||
mask-image: url('../icons/position-top.svg');
|
||||
}
|
||||
.position-right {
|
||||
mask-image: url('../icons/position-right.svg');
|
||||
}
|
||||
.position-bottom {
|
||||
mask-image: url('../icons/position-bottom.svg');
|
||||
}
|
||||
.position-left {
|
||||
mask-image: url('../icons/position-left.svg');
|
||||
}
|
||||
.mask-edge {
|
||||
mask-image: url('../icons/mask-edge.svg');
|
||||
}
|
||||
.mask-corner {
|
||||
mask-image: url('../icons/mask-corner.svg');
|
||||
}
|
||||
.mask-center {
|
||||
mask-image: url('../icons/mask-center.svg');
|
||||
}
|
||||
.book-front-cover {
|
||||
mask-image: url('../icons/book-front-cover.svg');
|
||||
}
|
||||
.book-back-cover {
|
||||
mask-image: url('../icons/book-back-cover.svg');
|
||||
}
|
||||
.book-inside-cover {
|
||||
mask-image: url('../icons/book-inside-cover.svg');
|
||||
}
|
||||
.book-part-cover {
|
||||
mask-image: url('../icons/book-part-cover.svg');
|
||||
}
|
||||
.image-wrap-left {
|
||||
mask-image: url('../icons/image-wrap-left.svg');
|
||||
}
|
||||
.image-wrap-right {
|
||||
mask-image: url('../icons/image-wrap-right.svg');
|
||||
}
|
||||
.davek {
|
||||
mask-image: url('../icons/Davek.svg');
|
||||
}
|
||||
.rellanic {
|
||||
mask-image: url('../icons/Rellanic.svg');
|
||||
}
|
||||
.iokharic {
|
||||
mask-image: url('../icons/Iokharic.svg');
|
||||
}
|
||||
.zoom-to-fit {
|
||||
mask-image: url('../icons/zoom-to-fit.svg');
|
||||
}
|
||||
.fit-width {
|
||||
mask-image: url('../icons/fit-width.svg');
|
||||
}
|
||||
.single-spread {
|
||||
mask-image: url('../icons/single-spread.svg');
|
||||
}
|
||||
.facing-spread {
|
||||
mask-image: url('../icons/facing-spread.svg');
|
||||
}
|
||||
.flow-spread {
|
||||
mask-image: url('../icons/flow-spread.svg');
|
||||
}
|
||||
.position-top-left { mask-image : url('../icons/position-top-left.svg'); }
|
||||
.position-top-right { mask-image : url('../icons/position-top-right.svg'); }
|
||||
.position-bottom-left { mask-image : url('../icons/position-bottom-left.svg'); }
|
||||
.position-bottom-right { mask-image : url('../icons/position-bottom-right.svg'); }
|
||||
.position-top { mask-image : url('../icons/position-top.svg'); }
|
||||
.position-right { mask-image : url('../icons/position-right.svg'); }
|
||||
.position-bottom { mask-image : url('../icons/position-bottom.svg'); }
|
||||
.position-left { mask-image : url('../icons/position-left.svg'); }
|
||||
.mask-edge { mask-image : url('../icons/mask-edge.svg'); }
|
||||
.mask-corner { mask-image : url('../icons/mask-corner.svg'); }
|
||||
.mask-center { mask-image : url('../icons/mask-center.svg'); }
|
||||
.book-front-cover { mask-image : url('../icons/book-front-cover.svg'); }
|
||||
.book-back-cover { mask-image : url('../icons/book-back-cover.svg'); }
|
||||
.book-inside-cover { mask-image : url('../icons/book-inside-cover.svg'); }
|
||||
.book-part-cover { mask-image : url('../icons/book-part-cover.svg'); }
|
||||
.image-wrap-left { mask-image : url('../icons/image-wrap-left.svg'); }
|
||||
.image-wrap-right { mask-image : url('../icons/image-wrap-right.svg'); }
|
||||
.davek { mask-image : url('../icons/Davek.svg'); }
|
||||
.rellanic { mask-image : url('../icons/Rellanic.svg'); }
|
||||
.iokharic { mask-image : url('../icons/Iokharic.svg'); }
|
||||
.zoom-to-fit { mask-image : url('../icons/zoom-to-fit.svg'); }
|
||||
.fit-width { mask-image : url('../icons/fit-width.svg'); }
|
||||
.single-spread { mask-image : url('../icons/single-spread.svg'); }
|
||||
.facing-spread { mask-image : url('../icons/facing-spread.svg'); }
|
||||
.flow-spread { mask-image : url('../icons/flow-spread.svg'); }
|
||||
|
||||
@@ -14,7 +14,6 @@ const template = async function(name, title='', props = {}){
|
||||
<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/v6.5.1/css/all.css" rel="stylesheet" type="text/css" />
|
||||
<link href="//fonts.googleapis.com/css?family=Open+Sans:400,300,600,700" rel="stylesheet" type="text/css" />
|
||||
<link href=${`/${name}/bundle.css`} type="text/css" rel='stylesheet' />
|
||||
<link rel="icon" href="/assets/favicon.ico" type="image/x-icon" />
|
||||
|
||||
3
font-awesome-source/README.md
Normal file
3
font-awesome-source/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# About
|
||||
|
||||
Run `deploy.bash` to download, extract, and deploy the font awesome files into place for building. Should only be needed when Font Awesome version changes and we want the new version.
|
||||
42
font-awesome-source/deploy.bash
Normal file
42
font-awesome-source/deploy.bash
Normal file
@@ -0,0 +1,42 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Deploys the Font Awesome files for HB self-hosting to settle various issues.
|
||||
|
||||
THEURL=https://use.fontawesome.com/releases/v6.7.2/fontawesome-free-6.7.2-web.zip
|
||||
THEFILE=fontawesome-free-6.7.2-web.zip
|
||||
if [ ! "$(which wget)" ]; then
|
||||
echo "Please manually download ${THEURL}"
|
||||
exit -1
|
||||
fi
|
||||
|
||||
wget ${THEURL}
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Error downloading ${THEURL}"
|
||||
exit -2
|
||||
fi
|
||||
|
||||
if [ ! "$(which unzip)" ]; then
|
||||
echo "Please unzip the file with your tool of choice."
|
||||
exit -3
|
||||
fi
|
||||
|
||||
unzip fontawesome-free-6.7.2-web.zip
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Error extracting ${THEFILE}"
|
||||
fi
|
||||
|
||||
echo "Copying fonts"
|
||||
cp -rv fontawesome-free-*-web/webfonts/*.woff2 ../themes/fonts/iconFonts
|
||||
echo "Copying and updating css"
|
||||
|
||||
echo "fontawesome-free.less"
|
||||
sed 's/..\/webfonts/\/fonts\/iconFonts/g' fontawesome-free-*-web/css/fontawesome.css > ../themes/fonts/iconFonts/fontawesome-free.less
|
||||
|
||||
echo "fontawesome-solid.less"
|
||||
sed 's/..\/webfonts/\/fonts\/iconFonts/g' fontawesome-free-*-web/css/solid.css > ../themes/fonts/iconFonts/fontawesome-solid.less
|
||||
|
||||
echo "fontawesome-brands.less"
|
||||
sed 's/..\/webfonts/\/fonts\/iconFonts/g' fontawesome-free-*-web/css/brands.css > ../themes/fonts/iconFonts/fontawesome-brands.less
|
||||
|
||||
echo "fontawesome-regular.less"
|
||||
sed 's/..\/webfonts/\/fonts\/iconFonts/g' fontawesome-free-*-web/css/regular.css > ../themes/fonts/iconFonts/fontawesome-regular.less
|
||||
6082
package-lock.json
generated
6082
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
77
package.json
77
package.json
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"name": "homebrewery",
|
||||
"description": "Create authentic looking D&D homebrews using only markdown",
|
||||
"version": "3.18.0",
|
||||
"version": "3.19.3",
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"npm": "^10.2.x",
|
||||
"npm": "^10.8.x",
|
||||
"node": "^20.18.x"
|
||||
},
|
||||
"repository": {
|
||||
@@ -36,7 +36,6 @@
|
||||
"test:mustache-syntax:inline": "jest \".*(mustache-syntax).*\" -t '^Inline:.*' --verbose --noStackTrace",
|
||||
"test:mustache-syntax:block": "jest \".*(mustache-syntax).*\" -t '^Block:.*' --verbose --noStackTrace",
|
||||
"test:mustache-syntax:injection": "jest \".*(mustache-syntax).*\" -t '^Injection:.*' --verbose --noStackTrace",
|
||||
"test:definition-lists": "jest tests/markdown/definition-lists.test.js --verbose --noStackTrace",
|
||||
"test:hard-breaks": "jest tests/markdown/hard-breaks.test.js --verbose --noStackTrace",
|
||||
"test:non-breaking-spaces": "jest tests/markdown/non-breaking-spaces.test.js --verbose --noStackTrace",
|
||||
"test:emojis": "jest tests/markdown/emojis.test.js --verbose --noStackTrace",
|
||||
@@ -73,7 +72,7 @@
|
||||
"lines": 50
|
||||
},
|
||||
"server/homebrew.api.js": {
|
||||
"statements": 70,
|
||||
"statements": 60,
|
||||
"branches": 50,
|
||||
"functions": 65,
|
||||
"lines": 70
|
||||
@@ -84,62 +83,72 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.26.9",
|
||||
"@babel/plugin-transform-runtime": "^7.26.9",
|
||||
"@babel/preset-env": "^7.26.9",
|
||||
"@babel/preset-react": "^7.26.3",
|
||||
"@googleapis/drive": "^8.16.0",
|
||||
"body-parser": "^1.20.2",
|
||||
"@babel/core": "^7.27.1",
|
||||
"@babel/plugin-transform-runtime": "^7.28.0",
|
||||
"@babel/preset-env": "^7.28.0",
|
||||
"@babel/preset-react": "^7.27.1",
|
||||
"@babel/runtime": "^7.27.6",
|
||||
"@dmsnell/diff-match-patch": "^1.1.0",
|
||||
"@googleapis/drive": "^13.0.1",
|
||||
"@sanity/diff-match-patch": "^3.2.0",
|
||||
"body-parser": "^2.2.0",
|
||||
"classnames": "^2.5.1",
|
||||
"codemirror": "^5.65.6",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"core-js": "^3.41.0",
|
||||
"core-js": "^3.44.0",
|
||||
"cors": "^2.8.5",
|
||||
"create-react-class": "^15.7.0",
|
||||
"dedent-tabs": "^0.10.3",
|
||||
"expr-eval": "^2.0.2",
|
||||
"express": "^4.21.2",
|
||||
"express": "^5.1.0",
|
||||
"express-async-handler": "^1.2.0",
|
||||
"express-static-gzip": "2.2.0",
|
||||
"express-static-gzip": "3.0.0",
|
||||
"fflate": "^0.8.2",
|
||||
"fs-extra": "11.3.0",
|
||||
"idb-keyval": "^6.2.1",
|
||||
"hash-wasm": "^4.12.0",
|
||||
"idb-keyval": "^6.2.2",
|
||||
"js-yaml": "^4.1.0",
|
||||
"jwt-simple": "^0.5.6",
|
||||
"less": "^3.13.1",
|
||||
"lodash": "^4.17.21",
|
||||
"marked": "14.0.0",
|
||||
"marked-emoji": "^2.0.0",
|
||||
"marked": "15.0.12",
|
||||
"marked-alignment-paragraphs": "^1.0.0",
|
||||
"marked-definition-lists": "^1.0.1",
|
||||
"marked-emoji": "^2.0.1",
|
||||
"marked-extended-tables": "^2.0.1",
|
||||
"marked-gfm-heading-id": "^4.0.1",
|
||||
"marked-gfm-heading-id": "^4.1.2",
|
||||
"marked-nonbreaking-spaces": "^1.0.1",
|
||||
"marked-smartypants-lite": "^1.0.3",
|
||||
"marked-subsuper-text": "^1.0.3",
|
||||
"markedLegacy": "npm:marked@^0.3.19",
|
||||
"moment": "^2.30.1",
|
||||
"mongoose": "^8.12.1",
|
||||
"nanoid": "5.1.3",
|
||||
"nconf": "^0.12.1",
|
||||
"mongoose": "^8.16.3",
|
||||
"nanoid": "5.1.5",
|
||||
"nconf": "^0.13.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-frame-component": "^4.1.3",
|
||||
"react-router": "^7.3.0",
|
||||
"react-router": "^7.6.3",
|
||||
"romans": "^3.1.0",
|
||||
"sanitize-filename": "1.6.3",
|
||||
"superagent": "^10.1.1",
|
||||
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git"
|
||||
"superagent": "^10.2.1",
|
||||
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git",
|
||||
"written-number": "^0.11.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@stylistic/stylelint-plugin": "^3.1.2",
|
||||
"babel-plugin-transform-import-meta": "^2.3.2",
|
||||
"eslint": "^9.22.0",
|
||||
"eslint-plugin-jest": "^28.11.0",
|
||||
"eslint-plugin-react": "^7.37.4",
|
||||
"globals": "^16.0.0",
|
||||
"jest": "^29.7.0",
|
||||
"@stylistic/stylelint-plugin": "^3.1.3",
|
||||
"babel-plugin-transform-import-meta": "^2.3.3",
|
||||
"eslint": "^9.31.0",
|
||||
"eslint-plugin-jest": "^29.0.1",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"globals": "^16.3.0",
|
||||
"jest": "^30.0.5",
|
||||
"jest-expect-message": "^1.1.3",
|
||||
"jsdom-global": "^3.0.2",
|
||||
"postcss-less": "^6.0.0",
|
||||
"stylelint": "^16.15.0",
|
||||
"stylelint-config-recess-order": "^6.0.0",
|
||||
"stylelint-config-recommended": "^15.0.0",
|
||||
"supertest": "^7.0.0"
|
||||
"stylelint": "^16.22.0",
|
||||
"stylelint-config-recess-order": "^7.1.0",
|
||||
"stylelint-config-recommended": "^16.0.0",
|
||||
"supertest": "^7.1.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import babel from '@babel/core';
|
||||
import babelConfig from '../babel.config.json' with { type : 'json' };
|
||||
import less from 'less';
|
||||
|
||||
const isDev = !!process.argv.find((arg) => arg === '--dev');
|
||||
const isDev = !!process.argv.find((arg)=>arg === '--dev');
|
||||
|
||||
const babelify = async (code)=>(await babel.transformAsync(code, babelConfig)).code;
|
||||
|
||||
@@ -53,7 +53,7 @@ fs.emptyDirSync('./build');
|
||||
const themes = { Legacy: {}, V3: {} };
|
||||
|
||||
let themeFiles = fs.readdirSync('./themes/Legacy');
|
||||
for (let dir of themeFiles) {
|
||||
for (const dir of themeFiles) {
|
||||
const themeData = JSON.parse(fs.readFileSync(`./themes/Legacy/${dir}/settings.json`).toString());
|
||||
themeData.path = dir;
|
||||
themes.Legacy[dir] = (themeData);
|
||||
@@ -70,7 +70,7 @@ fs.emptyDirSync('./build');
|
||||
}
|
||||
|
||||
themeFiles = fs.readdirSync('./themes/V3');
|
||||
for (let dir of themeFiles) {
|
||||
for (const dir of themeFiles) {
|
||||
const themeData = JSON.parse(fs.readFileSync(`./themes/V3/${dir}/settings.json`).toString());
|
||||
themeData.path = dir;
|
||||
themes.V3[dir] = (themeData);
|
||||
@@ -113,7 +113,7 @@ fs.emptyDirSync('./build');
|
||||
const stream = fs.createWriteStream(editorThemeFile, { flags: 'a' });
|
||||
stream.write('[\n"default"');
|
||||
|
||||
for (let themeFile of editorThemeFiles) {
|
||||
for (const themeFile of editorThemeFiles) {
|
||||
stream.write(`,\n"${themeFile.slice(0, -4)}"`);
|
||||
}
|
||||
stream.write('\n]\n');
|
||||
|
||||
@@ -27,6 +27,8 @@
|
||||
"codemirror/addon/selection/active-line.js",
|
||||
"codemirror/addon/hint/show-hint.js",
|
||||
"moment",
|
||||
"superagent"
|
||||
"superagent",
|
||||
"@sanity/diff-match-patch",
|
||||
"fflate"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/*eslint max-lines: ["warn", {"max": 500, "skipBlankLines": true, "skipComments": true}]*/
|
||||
import { model as HomebrewModel } from './homebrew.model.js';
|
||||
import { model as NotificationModel } from './notifications.model.js';
|
||||
import express from 'express';
|
||||
@@ -11,6 +12,7 @@ import { splitTextStyleAndMetadata } from '../shared/helpers.js';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
|
||||
process.env.ADMIN_USER = process.env.ADMIN_USER || 'admin';
|
||||
process.env.ADMIN_PASS = process.env.ADMIN_PASS || 'password3';
|
||||
|
||||
@@ -162,6 +164,180 @@ router.get('/admin/stats', mw.adminOnly, async (req, res)=>{
|
||||
}
|
||||
});
|
||||
|
||||
// ####################### LOCKS
|
||||
|
||||
router.get('/api/lock/count', mw.adminOnly, asyncHandler(async (req, res)=>{
|
||||
|
||||
const countLocksQuery = {
|
||||
lock : { $exists: true }
|
||||
};
|
||||
const count = await HomebrewModel.countDocuments(countLocksQuery)
|
||||
.catch((error)=>{
|
||||
throw { name: 'Lock Count Error', message: 'Unable to get lock count', status: 500, HBErrorCode: '61', error };
|
||||
});
|
||||
|
||||
return res.json({ count });
|
||||
|
||||
}));
|
||||
|
||||
router.get('/api/locks', mw.adminOnly, asyncHandler(async (req, res)=>{
|
||||
const countLocksPipeline = [
|
||||
{
|
||||
$match :
|
||||
{
|
||||
'lock' : { '$exists': 1 }
|
||||
},
|
||||
},
|
||||
{
|
||||
$project : {
|
||||
shareId : 1,
|
||||
editId : 1,
|
||||
title : 1,
|
||||
lock : 1
|
||||
}
|
||||
}
|
||||
];
|
||||
const lockedDocuments = await HomebrewModel.aggregate(countLocksPipeline)
|
||||
.catch((error)=>{
|
||||
throw { name: 'Can Not Get Locked Brews', message: 'Unable to get locked brew collection', status: 500, HBErrorCode: '68', error };
|
||||
});
|
||||
return res.json({
|
||||
lockedDocuments
|
||||
});
|
||||
|
||||
}));
|
||||
|
||||
router.post('/api/lock/:id', mw.adminOnly, asyncHandler(async (req, res)=>{
|
||||
|
||||
const lock = req.body;
|
||||
|
||||
lock.applied = new Date;
|
||||
|
||||
const filter = {
|
||||
shareId : req.params.id
|
||||
};
|
||||
|
||||
const brew = await HomebrewModel.findOne(filter);
|
||||
|
||||
if(!brew) throw { name: 'Brew Not Found', message: 'Cannot find brew to lock', shareId: req.params.id, status: 500, HBErrorCode: '63' };
|
||||
|
||||
if(brew.lock && !lock.overwrite) {
|
||||
throw { name: 'Already Locked', message: 'Lock already exists on brew', shareId: req.params.id, title: brew.title, status: 500, HBErrorCode: '64' };
|
||||
}
|
||||
|
||||
lock.overwrite = undefined;
|
||||
|
||||
brew.lock = lock;
|
||||
brew.markModified('lock');
|
||||
|
||||
await brew.save()
|
||||
.catch((error)=>{
|
||||
throw { name: 'Lock Error', message: 'Unable to set lock', shareId: req.params.id, status: 500, HBErrorCode: '62', error };
|
||||
});
|
||||
|
||||
return res.json({ name: 'LOCKED', message: `Lock applied to brew ID ${brew.shareId} - ${brew.title}`, ...lock });
|
||||
|
||||
}));
|
||||
|
||||
router.put('/api/unlock/:id', mw.adminOnly, asyncHandler(async (req, res)=>{
|
||||
|
||||
const filter = {
|
||||
shareId : req.params.id
|
||||
};
|
||||
|
||||
const brew = await HomebrewModel.findOne(filter);
|
||||
|
||||
if(!brew) throw { name: 'Brew Not Found', message: 'Cannot find brew to unlock', shareId: req.params.id, status: 500, HBErrorCode: '66' };
|
||||
|
||||
if(!brew.lock) throw { name: 'Not Locked', message: 'Cannot unlock as brew is not locked', shareId: req.params.id, status: 500, HBErrorCode: '67' };
|
||||
|
||||
brew.lock = undefined;
|
||||
brew.markModified('lock');
|
||||
|
||||
await brew.save()
|
||||
.catch((error)=>{
|
||||
throw { name: 'Cannot Unlock', message: 'Unable to clear lock', shareId: req.params.id, status: 500, HBErrorCode: '65', error };
|
||||
});
|
||||
|
||||
return res.json({ name: 'Unlocked', message: `Lock removed from brew ID ${req.params.id}` });
|
||||
}));
|
||||
|
||||
router.get('/api/lock/reviews', mw.adminOnly, asyncHandler(async (req, res)=>{
|
||||
const countReviewsPipeline = [
|
||||
{
|
||||
$match :
|
||||
{
|
||||
'lock.reviewRequested' : { '$exists': 1 }
|
||||
},
|
||||
},
|
||||
{
|
||||
$project : {
|
||||
shareId : 1,
|
||||
editId : 1,
|
||||
title : 1,
|
||||
lock : 1
|
||||
}
|
||||
}
|
||||
];
|
||||
const reviewDocuments = await HomebrewModel.aggregate(countReviewsPipeline)
|
||||
.catch((error)=>{
|
||||
throw { name: 'Can Not Get Reviews', message: 'Unable to get review collection', status: 500, HBErrorCode: '68', error };
|
||||
});
|
||||
return res.json({
|
||||
reviewDocuments
|
||||
});
|
||||
|
||||
}));
|
||||
|
||||
router.put('/api/lock/review/request/:id', asyncHandler(async (req, res)=>{
|
||||
// === This route is NOT Admin only ===
|
||||
// Any user can request a review of their document
|
||||
const filter = {
|
||||
shareId : req.params.id,
|
||||
lock : { $exists: 1 }
|
||||
};
|
||||
|
||||
const brew = await HomebrewModel.findOne(filter);
|
||||
if(!brew) { throw { name: 'Brew Not Found', message: `Cannot find a locked brew with ID ${req.params.id}`, code: 500, HBErrorCode: '70' }; };
|
||||
|
||||
if(brew.lock.reviewRequested){
|
||||
throw { name: 'Review Already Requested', message: `Review already requested for brew ${brew.shareId} - ${brew.title}`, code: 500, HBErrorCode: '71' };
|
||||
};
|
||||
|
||||
brew.lock.reviewRequested = new Date();
|
||||
brew.markModified('lock');
|
||||
|
||||
await brew.save()
|
||||
.catch((error)=>{
|
||||
throw { name: 'Can Not Set Review Request', message: `Unable to set request for review on brew ID ${req.params.id}`, code: 500, HBErrorCode: '69', error };
|
||||
});
|
||||
|
||||
return res.json({ name: 'Review Requested', message: `Review requested on brew ID ${brew.shareId} - ${brew.title}` });
|
||||
|
||||
}));
|
||||
|
||||
router.put('/api/lock/review/remove/:id', mw.adminOnly, asyncHandler(async (req, res)=>{
|
||||
|
||||
const filter = {
|
||||
shareId : req.params.id,
|
||||
'lock.reviewRequested' : { $exists: 1 }
|
||||
};
|
||||
|
||||
const brew = await HomebrewModel.findOne(filter);
|
||||
if(!brew) { throw { name: 'Can Not Clear Review Request', message: `Brew ID ${req.params.id} does not have a review pending!`, HBErrorCode: '73' }; };
|
||||
|
||||
brew.lock.reviewRequested = undefined;
|
||||
brew.markModified('lock');
|
||||
|
||||
await brew.save()
|
||||
.catch((error)=>{
|
||||
throw { name: 'Can Not Clear Review Request', message: `Unable to remove request for review on brew ID ${req.params.id}`, HBErrorCode: '72', error };
|
||||
});
|
||||
|
||||
return res.json({ name: 'Review Request Cleared', message: `Review request removed for brew ID ${brew.shareId} - ${brew.title}` });
|
||||
|
||||
}));
|
||||
|
||||
// ####################### NOTIFICATIONS
|
||||
|
||||
router.get('/admin/notification/all', async (req, res, next)=>{
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
/*eslint max-lines: ["warn", {"max": 1000, "skipBlankLines": true, "skipComments": true}]*/
|
||||
import supertest from 'supertest';
|
||||
import HBApp from './app.js';
|
||||
import {model as NotificationModel } from './notifications.model.js';
|
||||
import { model as NotificationModel } from './notifications.model.js';
|
||||
import { model as HomebrewModel } from './homebrew.model.js';
|
||||
|
||||
|
||||
// Mimic https responses to avoid being redirected all the time
|
||||
@@ -16,7 +18,7 @@ describe('Tests for admin api', ()=>{
|
||||
const testNotifications = ['a', 'b'];
|
||||
|
||||
jest.spyOn(NotificationModel, 'find')
|
||||
.mockImplementationOnce(() => {
|
||||
.mockImplementationOnce(()=>{
|
||||
return { exec: jest.fn().mockResolvedValue(testNotifications) };
|
||||
});
|
||||
|
||||
@@ -59,7 +61,7 @@ describe('Tests for admin api', ()=>{
|
||||
expect(response.body).toEqual(savedNotification);
|
||||
});
|
||||
|
||||
it('should handle error adding a notification without dismissKey', async () => {
|
||||
it('should handle error adding a notification without dismissKey', async ()=>{
|
||||
const inputNotification = {
|
||||
title : 'Test Notification',
|
||||
text : 'This is a test notification',
|
||||
@@ -75,7 +77,7 @@ describe('Tests for admin api', ()=>{
|
||||
|
||||
const response = await app
|
||||
.post('/admin/notification/add')
|
||||
.set('Authorization', 'Basic ' + Buffer.from('admin:password3').toString('base64'))
|
||||
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
|
||||
.send(inputNotification);
|
||||
|
||||
expect(response.status).toBe(500);
|
||||
@@ -86,14 +88,14 @@ describe('Tests for admin api', ()=>{
|
||||
const dismissKey = 'testKey';
|
||||
|
||||
jest.spyOn(NotificationModel, 'findOneAndDelete')
|
||||
.mockImplementationOnce((key) => {
|
||||
.mockImplementationOnce((key)=>{
|
||||
return { exec: jest.fn().mockResolvedValue(key) };
|
||||
});
|
||||
const response = await app
|
||||
.delete(`/admin/notification/delete/${dismissKey}`)
|
||||
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`);
|
||||
|
||||
expect(NotificationModel.findOneAndDelete).toHaveBeenCalledWith({'dismissKey': 'testKey'});
|
||||
expect(NotificationModel.findOneAndDelete).toHaveBeenCalledWith({ 'dismissKey': 'testKey' });
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toEqual({ dismissKey: 'testKey' });
|
||||
});
|
||||
@@ -102,16 +104,602 @@ describe('Tests for admin api', ()=>{
|
||||
const dismissKey = 'testKey';
|
||||
|
||||
jest.spyOn(NotificationModel, 'findOneAndDelete')
|
||||
.mockImplementationOnce(() => {
|
||||
.mockImplementationOnce(()=>{
|
||||
return { exec: jest.fn().mockResolvedValue() };
|
||||
});
|
||||
const response = await app
|
||||
.delete(`/admin/notification/delete/${dismissKey}`)
|
||||
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`);
|
||||
|
||||
expect(NotificationModel.findOneAndDelete).toHaveBeenCalledWith({'dismissKey': 'testKey'});
|
||||
expect(NotificationModel.findOneAndDelete).toHaveBeenCalledWith({ 'dismissKey': 'testKey' });
|
||||
expect(response.status).toBe(500);
|
||||
expect(response.body).toEqual({ message: 'Notification not found' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('Locks', ()=>{
|
||||
describe('Count', ()=>{
|
||||
it('Count of all locked documents', async ()=>{
|
||||
const testNumber = 16777216; // 8^8, because why not
|
||||
|
||||
jest.spyOn(HomebrewModel, 'countDocuments')
|
||||
.mockImplementationOnce(()=>{
|
||||
return Promise.resolve(testNumber);
|
||||
});
|
||||
|
||||
const response = await app
|
||||
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
|
||||
.get('/api/lock/count');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toEqual({ count: testNumber });
|
||||
});
|
||||
|
||||
it('Handle error while fetching count of locked documents', async ()=>{
|
||||
jest.spyOn(HomebrewModel, 'countDocuments')
|
||||
.mockImplementationOnce(()=>{
|
||||
return Promise.reject();
|
||||
});
|
||||
|
||||
const response = await app
|
||||
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
|
||||
.get('/api/lock/count');
|
||||
|
||||
expect(response.status).toBe(500);
|
||||
expect(response.body).toEqual({
|
||||
HBErrorCode : '61',
|
||||
message : 'Unable to get lock count',
|
||||
name : 'Lock Count Error',
|
||||
originalUrl : '/api/lock/count',
|
||||
status : 500,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Lists', ()=>{
|
||||
it('Get list of all locked documents', async ()=>{
|
||||
const testLocks = ['a', 'b'];
|
||||
|
||||
jest.spyOn(HomebrewModel, 'aggregate')
|
||||
.mockImplementationOnce(()=>{
|
||||
return Promise.resolve(testLocks);
|
||||
});
|
||||
|
||||
const response = await app
|
||||
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
|
||||
.get('/api/locks');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toEqual({ lockedDocuments: testLocks });
|
||||
});
|
||||
|
||||
it('Handle error while fetching list of all locked documents', async ()=>{
|
||||
jest.spyOn(HomebrewModel, 'aggregate')
|
||||
.mockImplementationOnce(()=>{
|
||||
return Promise.reject();
|
||||
});
|
||||
|
||||
const response = await app
|
||||
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
|
||||
.get('/api/locks');
|
||||
|
||||
expect(response.status).toBe(500);
|
||||
expect(response.body).toEqual({
|
||||
HBErrorCode : '68',
|
||||
message : 'Unable to get locked brew collection',
|
||||
name : 'Can Not Get Locked Brews',
|
||||
originalUrl : '/api/locks',
|
||||
status : 500
|
||||
});
|
||||
});
|
||||
|
||||
it('Get list of all locked documents with pending review requests', async ()=>{
|
||||
const testLocks = ['a', 'b'];
|
||||
|
||||
jest.spyOn(HomebrewModel, 'aggregate')
|
||||
.mockImplementationOnce(()=>{
|
||||
return Promise.resolve(testLocks);
|
||||
});
|
||||
|
||||
const response = await app
|
||||
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
|
||||
.get('/api/lock/reviews');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toEqual({ reviewDocuments: testLocks });
|
||||
});
|
||||
|
||||
it('Handle error while fetching list of all locked documents with pending review requests', async ()=>{
|
||||
jest.spyOn(HomebrewModel, 'aggregate')
|
||||
.mockImplementationOnce(()=>{
|
||||
return Promise.reject();
|
||||
});
|
||||
|
||||
const response = await app
|
||||
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
|
||||
.get('/api/lock/reviews');
|
||||
|
||||
expect(response.status).toBe(500);
|
||||
expect(response.body).toEqual({
|
||||
HBErrorCode : '68',
|
||||
message : 'Unable to get review collection',
|
||||
name : 'Can Not Get Reviews',
|
||||
originalUrl : '/api/lock/reviews',
|
||||
status : 500
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Lock', ()=>{
|
||||
it('Lock a brew', async ()=>{
|
||||
const testBrew = {
|
||||
shareId : 'shareId',
|
||||
title : 'title',
|
||||
markModified : ()=>{ return true; },
|
||||
save : ()=>{ return Promise.resolve(); }
|
||||
};
|
||||
|
||||
const testLock = {
|
||||
code : 999,
|
||||
editMessage : 'edit',
|
||||
shareMessage : 'share'
|
||||
};
|
||||
|
||||
jest.spyOn(HomebrewModel, 'findOne')
|
||||
.mockImplementationOnce(()=>{
|
||||
return Promise.resolve(testBrew);
|
||||
});
|
||||
|
||||
const response = await app
|
||||
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
|
||||
.post(`/api/lock/${testBrew.shareId}`)
|
||||
.send(testLock);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toMatchObject({
|
||||
applied : expect.any(String),
|
||||
code : testLock.code,
|
||||
editMessage : testLock.editMessage,
|
||||
shareMessage : testLock.shareMessage,
|
||||
name : 'LOCKED',
|
||||
message : `Lock applied to brew ID ${testBrew.shareId} - ${testBrew.title}`
|
||||
});
|
||||
});
|
||||
|
||||
it('Overwrite lock on a locked brew', async ()=>{
|
||||
const testLock = {
|
||||
code : 999,
|
||||
editMessage : 'newEdit',
|
||||
shareMessage : 'newShare',
|
||||
overwrite : true
|
||||
};
|
||||
|
||||
const testBrew = {
|
||||
shareId : 'shareId',
|
||||
title : 'title',
|
||||
markModified : ()=>{ return true; },
|
||||
save : ()=>{ return Promise.resolve(); },
|
||||
lock : {
|
||||
code : 1,
|
||||
editMessage : 'oldEdit',
|
||||
shareMessage : 'oldShare',
|
||||
}
|
||||
};
|
||||
|
||||
jest.spyOn(HomebrewModel, 'findOne')
|
||||
.mockImplementationOnce(()=>{
|
||||
return Promise.resolve(testBrew);
|
||||
});
|
||||
|
||||
const response = await app
|
||||
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
|
||||
.post(`/api/lock/${testBrew.shareId}`)
|
||||
.send(testLock);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toMatchObject({
|
||||
applied : expect.any(String),
|
||||
code : testLock.code,
|
||||
editMessage : testLock.editMessage,
|
||||
shareMessage : testLock.shareMessage,
|
||||
name : 'LOCKED',
|
||||
message : `Lock applied to brew ID ${testBrew.shareId} - ${testBrew.title}`
|
||||
});
|
||||
});
|
||||
|
||||
it('Error when locking a locked brew', async ()=>{
|
||||
const testLock = {
|
||||
code : 999,
|
||||
editMessage : 'newEdit',
|
||||
shareMessage : 'newShare'
|
||||
};
|
||||
|
||||
const testBrew = {
|
||||
shareId : 'shareId',
|
||||
title : 'title',
|
||||
markModified : ()=>{ return true; },
|
||||
save : ()=>{ return Promise.resolve(); },
|
||||
lock : {
|
||||
code : 1,
|
||||
editMessage : 'oldEdit',
|
||||
shareMessage : 'oldShare',
|
||||
}
|
||||
};
|
||||
|
||||
jest.spyOn(HomebrewModel, 'findOne')
|
||||
.mockImplementationOnce(()=>{
|
||||
return Promise.resolve(testBrew);
|
||||
});
|
||||
|
||||
const response = await app
|
||||
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
|
||||
.post(`/api/lock/${testBrew.shareId}`)
|
||||
.send(testLock);
|
||||
|
||||
expect(response.status).toBe(500);
|
||||
expect(response.body).toEqual({
|
||||
HBErrorCode : '64',
|
||||
message : 'Lock already exists on brew',
|
||||
name : 'Already Locked',
|
||||
originalUrl : `/api/lock/${testBrew.shareId}`,
|
||||
shareId : testBrew.shareId,
|
||||
status : 500,
|
||||
title : 'title'
|
||||
});
|
||||
});
|
||||
|
||||
it('Handle save error while locking a brew', async ()=>{
|
||||
const testBrew = {
|
||||
shareId : 'shareId',
|
||||
title : 'title',
|
||||
markModified : ()=>{ return true; },
|
||||
save : ()=>{ return Promise.reject(); }
|
||||
};
|
||||
|
||||
const testLock = {
|
||||
code : 999,
|
||||
editMessage : 'edit',
|
||||
shareMessage : 'share'
|
||||
};
|
||||
|
||||
jest.spyOn(HomebrewModel, 'findOne')
|
||||
.mockImplementationOnce(()=>{
|
||||
return Promise.resolve(testBrew);
|
||||
});
|
||||
|
||||
const response = await app
|
||||
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
|
||||
.post(`/api/lock/${testBrew.shareId}`)
|
||||
.send(testLock);
|
||||
|
||||
expect(response.status).toBe(500);
|
||||
expect(response.body).toEqual({
|
||||
HBErrorCode : '62',
|
||||
message : 'Unable to set lock',
|
||||
name : 'Lock Error',
|
||||
originalUrl : `/api/lock/${testBrew.shareId}`,
|
||||
shareId : testBrew.shareId,
|
||||
status : 500
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Unlock', ()=>{
|
||||
it('Unlock a brew', async ()=>{
|
||||
const testLock = {
|
||||
applied : 'YES',
|
||||
code : 999,
|
||||
editMessage : 'edit',
|
||||
shareMessage : 'share'
|
||||
};
|
||||
|
||||
const testBrew = {
|
||||
shareId : 'shareId',
|
||||
title : 'title',
|
||||
markModified : ()=>{ return true; },
|
||||
save : ()=>{ return Promise.resolve(); },
|
||||
lock : testLock
|
||||
};
|
||||
|
||||
jest.spyOn(HomebrewModel, 'findOne')
|
||||
.mockImplementationOnce(()=>{
|
||||
return Promise.resolve(testBrew);
|
||||
});
|
||||
|
||||
const response = await app
|
||||
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
|
||||
.put(`/api/unlock/${testBrew.shareId}`);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toEqual({
|
||||
name : 'Unlocked',
|
||||
message : `Lock removed from brew ID ${testBrew.shareId}`
|
||||
});
|
||||
});
|
||||
|
||||
it('Error when unlocking a brew with no lock', async ()=>{
|
||||
const testBrew = {
|
||||
shareId : 'shareId',
|
||||
title : 'title',
|
||||
markModified : ()=>{ return true; },
|
||||
save : ()=>{ return Promise.resolve(); },
|
||||
};
|
||||
|
||||
jest.spyOn(HomebrewModel, 'findOne')
|
||||
.mockImplementationOnce(()=>{
|
||||
return Promise.resolve(testBrew);
|
||||
});
|
||||
|
||||
const response = await app
|
||||
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
|
||||
.put(`/api/unlock/${testBrew.shareId}`);
|
||||
|
||||
expect(response.status).toBe(500);
|
||||
expect(response.body).toEqual({
|
||||
HBErrorCode : '67',
|
||||
message : 'Cannot unlock as brew is not locked',
|
||||
name : 'Not Locked',
|
||||
originalUrl : `/api/unlock/${testBrew.shareId}`,
|
||||
shareId : testBrew.shareId,
|
||||
status : 500,
|
||||
});
|
||||
});
|
||||
|
||||
it('Handle error while unlocking a brew', async ()=>{
|
||||
const testLock = {
|
||||
applied : 'YES',
|
||||
code : 999,
|
||||
editMessage : 'edit',
|
||||
shareMessage : 'share'
|
||||
};
|
||||
|
||||
const testBrew = {
|
||||
shareId : 'shareId',
|
||||
title : 'title',
|
||||
markModified : ()=>{ return true; },
|
||||
save : ()=>{ return Promise.reject(); },
|
||||
lock : testLock
|
||||
};
|
||||
|
||||
jest.spyOn(HomebrewModel, 'findOne')
|
||||
.mockImplementationOnce(()=>{
|
||||
return Promise.resolve(testBrew);
|
||||
});
|
||||
|
||||
const response = await app
|
||||
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
|
||||
.put(`/api/unlock/${testBrew.shareId}`);
|
||||
|
||||
expect(response.status).toBe(500);
|
||||
expect(response.body).toEqual({
|
||||
HBErrorCode : '65',
|
||||
message : 'Unable to clear lock',
|
||||
name : 'Cannot Unlock',
|
||||
originalUrl : `/api/unlock/${testBrew.shareId}`,
|
||||
shareId : testBrew.shareId,
|
||||
status : 500
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Reviews', ()=>{
|
||||
it('Add review request to a locked brew', async ()=>{
|
||||
const testLock = {
|
||||
applied : 'YES',
|
||||
code : 999,
|
||||
editMessage : 'edit',
|
||||
shareMessage : 'share'
|
||||
};
|
||||
|
||||
const testBrew = {
|
||||
shareId : 'shareId',
|
||||
title : 'title',
|
||||
markModified : ()=>{ return true; },
|
||||
save : ()=>{ return Promise.resolve(); },
|
||||
lock : testLock
|
||||
};
|
||||
|
||||
jest.spyOn(HomebrewModel, 'findOne')
|
||||
.mockImplementationOnce(()=>{
|
||||
return Promise.resolve(testBrew);
|
||||
});
|
||||
|
||||
const response = await app
|
||||
.put(`/api/lock/review/request/${testBrew.shareId}`);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toEqual({
|
||||
message : `Review requested on brew ID ${testBrew.shareId} - ${testBrew.title}`,
|
||||
name : 'Review Requested',
|
||||
});
|
||||
});
|
||||
|
||||
it('Error when cannot find a locked brew', async ()=>{
|
||||
const testBrew = {
|
||||
shareId : 'shareId'
|
||||
};
|
||||
|
||||
jest.spyOn(HomebrewModel, 'findOne')
|
||||
.mockImplementationOnce(()=>{
|
||||
return Promise.resolve(false);
|
||||
});
|
||||
|
||||
|
||||
const response = await app
|
||||
.put(`/api/lock/review/request/${testBrew.shareId}`)
|
||||
.catch((err)=>{return err;});
|
||||
|
||||
expect(response.status).toBe(500);
|
||||
expect(response.body).toEqual({
|
||||
message : `Cannot find a locked brew with ID ${testBrew.shareId}`,
|
||||
name : 'Brew Not Found',
|
||||
HBErrorCode : '70',
|
||||
code : 500,
|
||||
originalUrl : `/api/lock/review/request/${testBrew.shareId}`
|
||||
});
|
||||
});
|
||||
|
||||
it('Error when review is already requested', async ()=>{
|
||||
const testLock = {
|
||||
applied : 'YES',
|
||||
code : 999,
|
||||
editMessage : 'edit',
|
||||
shareMessage : 'share',
|
||||
reviewRequested : 'YES'
|
||||
};
|
||||
|
||||
const testBrew = {
|
||||
shareId : 'shareId',
|
||||
title : 'title',
|
||||
markModified : ()=>{ return true; },
|
||||
save : ()=>{ return Promise.resolve(); },
|
||||
lock : testLock
|
||||
};
|
||||
|
||||
jest.spyOn(HomebrewModel, 'findOne')
|
||||
.mockImplementationOnce(()=>{
|
||||
return Promise.resolve(false);
|
||||
});
|
||||
|
||||
|
||||
const response = await app
|
||||
.put(`/api/lock/review/request/${testBrew.shareId}`)
|
||||
.catch((err)=>{return err;});
|
||||
|
||||
expect(response.status).toBe(500);
|
||||
expect(response.body).toEqual({
|
||||
HBErrorCode : '70',
|
||||
code : 500,
|
||||
message : `Cannot find a locked brew with ID ${testBrew.shareId}`,
|
||||
name : 'Brew Not Found',
|
||||
originalUrl : `/api/lock/review/request/${testBrew.shareId}`
|
||||
});
|
||||
});
|
||||
|
||||
it('Handle error while adding review request to a locked brew', async ()=>{
|
||||
const testLock = {
|
||||
applied : 'YES',
|
||||
code : 999,
|
||||
editMessage : 'edit',
|
||||
shareMessage : 'share'
|
||||
};
|
||||
|
||||
const testBrew = {
|
||||
shareId : 'shareId',
|
||||
title : 'title',
|
||||
markModified : ()=>{ return true; },
|
||||
save : ()=>{ return Promise.reject(); },
|
||||
lock : testLock
|
||||
};
|
||||
|
||||
jest.spyOn(HomebrewModel, 'findOne')
|
||||
.mockImplementationOnce(()=>{
|
||||
return Promise.resolve(testBrew);
|
||||
});
|
||||
|
||||
const response = await app
|
||||
.put(`/api/lock/review/request/${testBrew.shareId}`);
|
||||
|
||||
expect(response.status).toBe(500);
|
||||
expect(response.body).toEqual({
|
||||
HBErrorCode : '69',
|
||||
code : 500,
|
||||
message : `Unable to set request for review on brew ID ${testBrew.shareId}`,
|
||||
name : 'Can Not Set Review Request',
|
||||
originalUrl : `/api/lock/review/request/${testBrew.shareId}`
|
||||
});
|
||||
});
|
||||
|
||||
it('Clear review request from a locked brew', async ()=>{
|
||||
const testLock = {
|
||||
applied : 'YES',
|
||||
code : 999,
|
||||
editMessage : 'edit',
|
||||
shareMessage : 'share',
|
||||
reviewRequested : 'YES'
|
||||
};
|
||||
|
||||
const testBrew = {
|
||||
shareId : 'shareId',
|
||||
title : 'title',
|
||||
markModified : ()=>{ return true; },
|
||||
save : ()=>{ return Promise.resolve(); },
|
||||
lock : testLock
|
||||
};
|
||||
|
||||
jest.spyOn(HomebrewModel, 'findOne')
|
||||
.mockImplementationOnce(()=>{
|
||||
return Promise.resolve(testBrew);
|
||||
});
|
||||
|
||||
const response = await app
|
||||
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
|
||||
.put(`/api/lock/review/remove/${testBrew.shareId}`);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toEqual({
|
||||
message : `Review request removed for brew ID ${testBrew.shareId} - ${testBrew.title}`,
|
||||
name : 'Review Request Cleared'
|
||||
});
|
||||
});
|
||||
|
||||
it('Error when clearing review request from a brew with no review request', async ()=>{
|
||||
const testBrew = {
|
||||
shareId : 'shareId',
|
||||
};
|
||||
|
||||
jest.spyOn(HomebrewModel, 'findOne')
|
||||
.mockImplementationOnce(()=>{
|
||||
return Promise.resolve(false);
|
||||
});
|
||||
|
||||
const response = await app
|
||||
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
|
||||
.put(`/api/lock/review/remove/${testBrew.shareId}`);
|
||||
|
||||
expect(response.status).toBe(500);
|
||||
expect(response.body).toEqual({
|
||||
HBErrorCode : '73',
|
||||
message : `Brew ID ${testBrew.shareId} does not have a review pending!`,
|
||||
name : 'Can Not Clear Review Request',
|
||||
originalUrl : `/api/lock/review/remove/${testBrew.shareId}`
|
||||
});
|
||||
});
|
||||
|
||||
it('Handle error while clearing review request from a locked brew', async ()=>{
|
||||
const testLock = {
|
||||
applied : 'YES',
|
||||
code : 999,
|
||||
editMessage : 'edit',
|
||||
shareMessage : 'share',
|
||||
reviewRequested : 'YES'
|
||||
};
|
||||
|
||||
const testBrew = {
|
||||
shareId : 'shareId',
|
||||
title : 'title',
|
||||
markModified : ()=>{ return true; },
|
||||
save : ()=>{ return Promise.reject(); },
|
||||
lock : testLock
|
||||
};
|
||||
|
||||
jest.spyOn(HomebrewModel, 'findOne')
|
||||
.mockImplementationOnce(()=>{
|
||||
return Promise.resolve(testBrew);
|
||||
});
|
||||
|
||||
const response = await app
|
||||
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
|
||||
.put(`/api/lock/review/remove/${testBrew.shareId}`);
|
||||
|
||||
expect(response.status).toBe(500);
|
||||
expect(response.body).toEqual({
|
||||
HBErrorCode : '72',
|
||||
message : `Unable to remove request for review on brew ID ${testBrew.shareId}`,
|
||||
name : 'Can Not Clear Review Request',
|
||||
originalUrl : `/api/lock/review/remove/${testBrew.shareId}`
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// Set working directory to project root
|
||||
import { dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import packageJSON from './../package.json' with { type: 'json' };
|
||||
import packageJSON from './../package.json' with { type: 'json' };
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
process.chdir(`${__dirname}/..`);
|
||||
@@ -11,7 +11,6 @@ const version = packageJSON.version;
|
||||
import _ from 'lodash';
|
||||
import jwt from 'jwt-simple';
|
||||
import express from 'express';
|
||||
import yaml from 'js-yaml';
|
||||
import config from './config.js';
|
||||
import fs from 'fs-extra';
|
||||
|
||||
@@ -70,13 +69,11 @@ const corsOptions = {
|
||||
'https://homebrewery-stage.herokuapp.com',
|
||||
];
|
||||
|
||||
if(isLocalEnvironment) {
|
||||
allowedOrigins.push('http://localhost:8000', 'http://localhost:8010');
|
||||
}
|
||||
const localNetworkRegex = /^http:\/\/(localhost|127\.0\.0\.1|10\.\d+\.\d+\.\d+|192\.168\.\d+\.\d+|172\.(1[6-9]|2\d|3[0-1])\.\d+\.\d+):\d+$/;
|
||||
|
||||
const herokuRegex = /^https:\/\/(?:homebrewery-pr-\d+\.herokuapp\.com|naturalcrit-pr-\d+\.herokuapp\.com)$/; // Matches any Heroku app
|
||||
|
||||
if(!origin || allowedOrigins.includes(origin) || herokuRegex.test(origin)) {
|
||||
if(!origin || allowedOrigins.includes(origin) || herokuRegex.test(origin) || (isLocalEnvironment && localNetworkRegex.test(origin))) {
|
||||
callback(null, true);
|
||||
} else {
|
||||
console.log(origin, 'not allowed');
|
||||
@@ -352,7 +349,7 @@ app.get('/user/:username', async (req, res, next)=>{
|
||||
app.put('/api/user/rename', async (req, res)=>{
|
||||
const { username, newUsername } = req.body;
|
||||
const ownAccount = req.account && (req.account.username == newUsername);
|
||||
|
||||
|
||||
if(!username || !newUsername)
|
||||
return res.status(400).json({ error: 'Username and newUsername are required.' });
|
||||
if(!ownAccount)
|
||||
@@ -386,6 +383,7 @@ app.get('/edit/:id', asyncHandler(getBrew('edit')), asyncHandler(async(req, res,
|
||||
title : req.brew.title || 'Untitled Brew',
|
||||
description : req.brew.description || 'No description.',
|
||||
image : req.brew.thumbnail || defaultMetaTags.image,
|
||||
locale : req.brew.lang,
|
||||
type : 'article'
|
||||
};
|
||||
|
||||
@@ -407,6 +405,7 @@ app.get('/new/:id', asyncHandler(getBrew('share')), asyncHandler(async(req, res,
|
||||
renderer : req.brew.renderer,
|
||||
theme : req.brew.theme,
|
||||
tags : req.brew.tags,
|
||||
snippets : req.brew.snippets
|
||||
};
|
||||
req.brew = _.defaults(brew, DEFAULT_BREW);
|
||||
|
||||
@@ -436,7 +435,7 @@ app.get('/new', asyncHandler(async(req, res, next)=>{
|
||||
app.get('/share/:id', asyncHandler(getBrew('share')), asyncHandler(async (req, res, next)=>{
|
||||
const { brew } = req;
|
||||
req.ogMeta = { ...defaultMetaTags,
|
||||
title : req.brew.title || 'Untitled Brew',
|
||||
title : `${req.brew.title || 'Untitled Brew'} - ${req.brew.authors[0] || 'No author.'}`,
|
||||
description : req.brew.description || 'No description.',
|
||||
image : req.brew.thumbnail || defaultMetaTags.image,
|
||||
type : 'article'
|
||||
|
||||
66
server/forcessl.mw.spec.js
Normal file
66
server/forcessl.mw.spec.js
Normal file
@@ -0,0 +1,66 @@
|
||||
import forceSSL from './forcessl.mw';
|
||||
|
||||
describe('Tests for ForceSSL middleware', ()=>{
|
||||
let originalEnv;
|
||||
let nextFn;
|
||||
|
||||
let req = {};
|
||||
let res = {};
|
||||
|
||||
beforeEach(()=>{
|
||||
originalEnv = process.env.NODE_ENV;
|
||||
nextFn = jest.fn();
|
||||
|
||||
req = {
|
||||
header : ()=>{ return 'http'; },
|
||||
get : ()=>{ return 'test'; },
|
||||
url : 'URL'
|
||||
};
|
||||
|
||||
res = {
|
||||
redirect : jest.fn()
|
||||
};
|
||||
});
|
||||
afterEach(()=>{
|
||||
process.env.NODE_ENV = originalEnv;
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should not redirect when NODE_ENV is set to local', ()=>{
|
||||
process.env.NODE_ENV = 'local';
|
||||
|
||||
forceSSL(null, null, nextFn);
|
||||
|
||||
expect(res.redirect).not.toHaveBeenCalled();
|
||||
expect(nextFn).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not redirect when NODE_ENV is set to docker', ()=>{
|
||||
process.env.NODE_ENV = 'docker';
|
||||
|
||||
forceSSL(null, null, nextFn);
|
||||
|
||||
expect(res.redirect).not.toHaveBeenCalled();
|
||||
expect(nextFn).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should redirect with 302 when header is not HTTPS and NODE_ENV is not local or docker', ()=>{
|
||||
process.env.NODE_ENV = 'test';
|
||||
|
||||
forceSSL(req, res, nextFn);
|
||||
|
||||
expect(res.redirect).toHaveBeenCalledWith(302, 'https://testURL');
|
||||
expect(nextFn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not redirect when header is HTTPS and NODE_ENV is not local or docker', ()=>{
|
||||
process.env.NODE_ENV = 'test';
|
||||
req.header = ()=>{ return 'https'; };
|
||||
|
||||
forceSSL(req, res, nextFn);
|
||||
|
||||
expect(res.redirect).not.toHaveBeenCalled();
|
||||
expect(nextFn).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
});
|
||||
@@ -27,12 +27,12 @@ if(!config.get('service_account')){
|
||||
const defaultAuth = serviceAuth || config.get('google_api_key');
|
||||
|
||||
const retryConfig = {
|
||||
retry: 3, // Number of retry attempts
|
||||
retryDelay: 100, // Initial delay in milliseconds
|
||||
retryDelayMultiplier: 2, // Multiplier for exponential backoff
|
||||
maxRetryDelay: 32000, // Maximum delay in milliseconds
|
||||
httpMethodsToRetry: ['PATCH'], // Only retry PATCH requests
|
||||
statusCodesToRetry: [[429, 429]], // Only retry on 429 status code
|
||||
retry : 3, // Number of retry attempts
|
||||
retryDelay : 100, // Initial delay in milliseconds
|
||||
retryDelayMultiplier : 2, // Multiplier for exponential backoff
|
||||
maxRetryDelay : 32000, // Maximum delay in milliseconds
|
||||
httpMethodsToRetry : ['PATCH'], // Only retry PATCH requests
|
||||
statusCodesToRetry : [[429, 429]], // Only retry on 429 status code
|
||||
};
|
||||
|
||||
const GoogleActions = {
|
||||
@@ -177,8 +177,8 @@ const GoogleActions = {
|
||||
mimeType : 'text/plain',
|
||||
body : brew.text
|
||||
},
|
||||
headers: {
|
||||
'X-Forwarded-For': userIp, // Set the X-Forwarded-For header
|
||||
headers : {
|
||||
'X-Forwarded-For' : userIp, // Set the X-Forwarded-For header
|
||||
},
|
||||
retryConfig
|
||||
})
|
||||
|
||||
@@ -8,9 +8,13 @@ import Markdown from '../shared/naturalcrit/markdown.js';
|
||||
import yaml from 'js-yaml';
|
||||
import asyncHandler from 'express-async-handler';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { splitTextStyleAndMetadata } from '../shared/helpers.js';
|
||||
import {makePatches, applyPatches, stringifyPatches, parsePatch} from '@sanity/diff-match-patch';
|
||||
import { md5 } from 'hash-wasm';
|
||||
import { splitTextStyleAndMetadata,
|
||||
brewSnippetsToJSON, debugTextMismatch } from '../shared/helpers.js';
|
||||
import checkClientVersion from './middleware/check-client-version.js';
|
||||
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
import { DEFAULT_BREW, DEFAULT_BREW_LOAD } from './brewDefaults.js';
|
||||
@@ -44,6 +48,20 @@ const api = {
|
||||
}
|
||||
id = id.slice(googleId.length);
|
||||
}
|
||||
|
||||
// ID Validation Checks
|
||||
// Homebrewery ID
|
||||
// Typically 12 characters, but the DB shows a range of 7 to 14 characters
|
||||
if(!id.match(/^[a-zA-Z0-9-_]{7,14}$/)){
|
||||
throw { name: 'ID Error', message: 'Invalid ID', status: 404, HBErrorCode: '11', brewId: id };
|
||||
}
|
||||
// Google ID
|
||||
// Typically 33 characters, old format is 44 - always starts with a 1
|
||||
// Managed by Google, may change outside of our control, so any length between 33 and 44 is acceptable
|
||||
if(googleId && !googleId.match(/^1(?:[a-zA-Z0-9-_]{32,43})$/)){
|
||||
throw { name: 'Google ID Error', message: 'Invalid ID', status: 404, HBErrorCode: '12', brewId: id };
|
||||
}
|
||||
|
||||
return { id, googleId };
|
||||
},
|
||||
//Get array of any of this user's brews tagged with `meta:theme`
|
||||
@@ -92,7 +110,7 @@ const api = {
|
||||
const accessMap = {
|
||||
edit : { editId: id },
|
||||
share : { shareId: id },
|
||||
admin : { $or : [{ editId: id }, { shareId: id }] }
|
||||
admin : { $or: [{ editId: id }, { shareId: id }] }
|
||||
};
|
||||
|
||||
// Try to find the document in the Homebrewery database -- if it doesn't exist, that's fine.
|
||||
@@ -118,8 +136,8 @@ const api = {
|
||||
throw { ...accessError, message: 'User is not logged in', HBErrorCode: '04' };
|
||||
}
|
||||
|
||||
if(stub?.lock?.locked && accessType != 'edit') {
|
||||
throw { HBErrorCode: '51', code: stub?.lock.code, message: stub?.lock.shareMessage, brewId: stub?.shareId, brewTitle: stub?.title };
|
||||
if(stub?.lock && accessType === 'share') {
|
||||
throw { HBErrorCode: '51', code: stub.lock.code, message: stub.lock.shareMessage, brewId: stub.shareId, brewTitle: stub.title, brewAuthors: stub.authors };
|
||||
}
|
||||
|
||||
// If there's a google id, get it if requesting the full brew or if no stub found yet
|
||||
@@ -175,12 +193,15 @@ const api = {
|
||||
`${text}`;
|
||||
}
|
||||
const metadata = _.pick(brew, ['title', 'description', 'tags', 'systems', 'renderer', 'theme']);
|
||||
const snippetsArray = brewSnippetsToJSON('brew_snippets', brew.snippets, null, false).snippets;
|
||||
metadata.snippets = snippetsArray.length > 0 ? snippetsArray : undefined;
|
||||
text = `\`\`\`metadata\n` +
|
||||
`${yaml.dump(metadata)}\n` +
|
||||
`\`\`\`\n\n` +
|
||||
`${text}`;
|
||||
return text;
|
||||
},
|
||||
|
||||
getGoodBrewTitle : (text)=>{
|
||||
const tokens = Markdown.marked.lexer(text);
|
||||
return (tokens.find((token)=>token.type === 'heading' || token.type === 'paragraph')?.text || 'No Title')
|
||||
@@ -294,13 +315,13 @@ const api = {
|
||||
|
||||
currentTheme = req.brew;
|
||||
splitTextStyleAndMetadata(currentTheme);
|
||||
if(!currentTheme.tags.some(tag => tag === "meta:theme" || tag === "meta:Theme"))
|
||||
if(!currentTheme.tags.some((tag)=>tag === 'meta:theme' || tag === 'meta:Theme'))
|
||||
throw { brewId: req.params.id, name: 'Invalid Theme Selected', message: 'Selected theme does not have the meta:theme tag', status: 422, HBErrorCode: '10' };
|
||||
themeName ??= currentTheme.title;
|
||||
themeAuthor ??= currentTheme.authors?.[0];
|
||||
|
||||
// If there is anything in the snippets or style members, append them to the appropriate array
|
||||
if(currentTheme?.snippets) completeSnippets.push(JSON.parse(currentTheme.snippets));
|
||||
if(currentTheme?.snippets) completeSnippets.push({ name: currentTheme.title, snippets: currentTheme.snippets });
|
||||
if(currentTheme?.style) completeStyles.push(`/* From Brew: ${req.protocol}://${req.get('host')}/share/${req.params.id} */\n\n${currentTheme.style}`);
|
||||
|
||||
req.params.id = currentTheme.theme;
|
||||
@@ -332,21 +353,52 @@ const api = {
|
||||
// Initialize brew from request and body, destructure query params, and set the initial value for the after-save method
|
||||
const brewFromClient = api.excludePropsFromUpdate(req.body);
|
||||
const brewFromServer = req.brew;
|
||||
if(brewFromServer.version && brewFromClient.version && brewFromServer.version > brewFromClient.version) {
|
||||
splitTextStyleAndMetadata(brewFromServer);
|
||||
|
||||
if(brewFromServer?.version !== brewFromClient?.version){
|
||||
console.log(`Version mismatch on brew ${brewFromClient.editId}`);
|
||||
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
return res.status(409).send(JSON.stringify({ message: `The brew has been changed on a different device. Please save your changes elsewhere, refresh, and try again.` }));
|
||||
return res.status(409).send(JSON.stringify({ message: `The server version is out of sync with the saved brew. Please save your changes elsewhere, refresh, and try again.` }));
|
||||
}
|
||||
|
||||
let brew = _.assign(brewFromServer, brewFromClient);
|
||||
brewFromServer.text = brewFromServer.text.normalize('NFC');
|
||||
brewFromServer.hash = await md5(brewFromServer.text);
|
||||
|
||||
if(brewFromServer?.hash !== brewFromClient?.hash) {
|
||||
console.log(`Hash mismatch on brew ${brewFromClient.editId}`);
|
||||
//debugTextMismatch(brewFromClient.text, brewFromServer.text, `edit/${brewFromClient.editId}`);
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
return res.status(409).send(JSON.stringify({ message: `The server copy is out of sync with the saved brew. Please save your changes elsewhere, refresh, and try again.` }));
|
||||
}
|
||||
|
||||
try {
|
||||
const patches = parsePatch(brewFromClient.patches);
|
||||
// Patch to a throwaway variable while parallelizing - we're more concerned with error/no error.
|
||||
const patchedResult = decodeURI(applyPatches(patches, encodeURI(brewFromServer.text))[0]);
|
||||
if(patchedResult != brewFromClient.text)
|
||||
throw("Patches did not apply cleanly, text mismatch detected");
|
||||
// brew.text = applyPatches(patches, brewFromServer.text)[0];
|
||||
} catch (err) {
|
||||
//debugTextMismatch(brewFromClient.text, brewFromServer.text, `edit/${brewFromClient.editId}`);
|
||||
console.error('Failed to apply patches:', {
|
||||
//patches : brewFromClient.patches,
|
||||
brewId : brewFromClient.editId || 'unknown',
|
||||
error : err
|
||||
});
|
||||
// While running in parallel, don't throw the error upstream.
|
||||
// throw err; // rethrow to preserve the 500 behavior
|
||||
}
|
||||
|
||||
let brew = _.assign(brewFromServer, brewFromClient);
|
||||
brew.title = brew.title.trim();
|
||||
brew.description = brew.description.trim() || '';
|
||||
brew.text = api.mergeBrewText(brew);
|
||||
|
||||
const googleId = brew.googleId;
|
||||
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) {
|
||||
// If the google id exists and we're removing it from google, set afterSave to delete the google brew and mark the brew's google id as undefined
|
||||
afterSave = async ()=>{
|
||||
@@ -407,6 +459,8 @@ const api = {
|
||||
const after = await afterSave();
|
||||
if(!after) return;
|
||||
|
||||
saved.textBin = undefined; // Remove textBin from the saved object to save bandwidth
|
||||
|
||||
res.status(200).send(saved);
|
||||
},
|
||||
deleteGoogleBrew : async (account, id, editId, res)=>{
|
||||
@@ -477,10 +531,10 @@ const api = {
|
||||
};
|
||||
|
||||
router.post('/api', checkClientVersion, asyncHandler(api.newBrew));
|
||||
router.put('/api/:id', checkClientVersion, asyncHandler(api.getBrew('edit', true)), asyncHandler(api.updateBrew));
|
||||
router.put('/api/update/:id', checkClientVersion, asyncHandler(api.getBrew('edit', true)), asyncHandler(api.updateBrew));
|
||||
router.put('/api/:id', checkClientVersion, asyncHandler(api.getBrew('edit', false)), asyncHandler(api.updateBrew));
|
||||
router.put('/api/update/:id', checkClientVersion, asyncHandler(api.getBrew('edit', false)), asyncHandler(api.updateBrew));
|
||||
router.delete('/api/:id', checkClientVersion, asyncHandler(api.deleteBrew));
|
||||
router.get('/api/remove/:id', checkClientVersion, asyncHandler(api.deleteBrew));
|
||||
router.get('/api/theme/:renderer/:id', asyncHandler(api.getThemeBundle));
|
||||
|
||||
export default api;
|
||||
export default api;
|
||||
|
||||
@@ -99,18 +99,87 @@ describe('Tests for api', ()=>{
|
||||
expect(googleId).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should throw if id is too short', ()=>{
|
||||
let err;
|
||||
try {
|
||||
api.getId({
|
||||
params : {
|
||||
id : 'abcd'
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
err = e;
|
||||
};
|
||||
|
||||
expect(err).toEqual({ HBErrorCode: '11', brewId: 'abcd', message: 'Invalid ID', name: 'ID Error', status: 404 });
|
||||
});
|
||||
|
||||
it('should return id and google id from request body', ()=>{
|
||||
const { id, googleId } = api.getId({
|
||||
params : {
|
||||
id : 'abcdefgh'
|
||||
id : 'abcdefghijkl'
|
||||
},
|
||||
body : {
|
||||
googleId : '12345'
|
||||
googleId : '123456789012345678901234567890123'
|
||||
}
|
||||
});
|
||||
|
||||
expect(id).toEqual('abcdefgh');
|
||||
expect(googleId).toEqual('12345');
|
||||
expect(id).toEqual('abcdefghijkl');
|
||||
expect(googleId).toEqual('123456789012345678901234567890123');
|
||||
});
|
||||
|
||||
it('should throw invalid - google id right length but does not match pattern', ()=>{
|
||||
let err;
|
||||
try {
|
||||
api.getId({
|
||||
params : {
|
||||
id : 'abcdefghijkl'
|
||||
},
|
||||
body : {
|
||||
googleId : '012345678901234567890123456789012'
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
err = e;
|
||||
}
|
||||
|
||||
expect(err).toEqual({ HBErrorCode: '12', brewId: 'abcdefghijkl', message: 'Invalid ID', name: 'Google ID Error', status: 404 });
|
||||
});
|
||||
|
||||
it('should throw invalid - google id too short (32 char)', ()=>{
|
||||
let err;
|
||||
try {
|
||||
api.getId({
|
||||
params : {
|
||||
id : 'abcdefghijkl'
|
||||
},
|
||||
body : {
|
||||
googleId : '12345678901234567890123456789012'
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
err = e;
|
||||
}
|
||||
|
||||
expect(err).toEqual({ HBErrorCode: '12', brewId: 'abcdefghijkl', message: 'Invalid ID', name: 'Google ID Error', status: 404 });
|
||||
});
|
||||
|
||||
it('should throw invalid - google id too long (45 char)', ()=>{
|
||||
let err;
|
||||
try {
|
||||
api.getId({
|
||||
params : {
|
||||
id : 'abcdefghijkl'
|
||||
},
|
||||
body : {
|
||||
googleId : '123456789012345678901234567890123456789012345'
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
err = e;
|
||||
}
|
||||
|
||||
expect(err).toEqual({ HBErrorCode: '12', brewId: 'abcdefghijkl', message: 'Invalid ID', name: 'Google ID Error', status: 404 });
|
||||
});
|
||||
|
||||
it('should return 12-char id and google id from params', ()=>{
|
||||
@@ -302,7 +371,7 @@ describe('Tests for api', ()=>{
|
||||
});
|
||||
|
||||
it('access is denied to a locked brew', async()=>{
|
||||
const lockBrew = { title: 'test brew', shareId: '1', lock: { locked: true, code: 404, shareMessage: 'brew locked' } };
|
||||
const lockBrew = { title: 'test brew', shareId: '1', lock: { code: 404, shareMessage: 'brew locked' } };
|
||||
model.get = jest.fn(()=>toBrewPromise(lockBrew));
|
||||
api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
|
||||
|
||||
@@ -939,7 +1008,7 @@ brew`);
|
||||
});
|
||||
describe('Get CSS', ()=>{
|
||||
it('should return brew style content as CSS text', async ()=>{
|
||||
const testBrew = { title: 'test brew', text: '```css\n\nI Have a style!\n````\n\n' };
|
||||
const testBrew = { title: 'test brew', text: '```css\n\nI Have a style!\n```\n\n' };
|
||||
|
||||
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
|
||||
api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
|
||||
@@ -1034,7 +1103,7 @@ brew`);
|
||||
expect(testBrew.theme).toEqual('5ePHB');
|
||||
expect(testBrew.lang).toEqual('en');
|
||||
// Style
|
||||
expect(testBrew.style).toEqual('style\nstyle\nstyle');
|
||||
expect(testBrew.style).toEqual('style\nstyle\nstyle\n');
|
||||
// Text
|
||||
expect(testBrew.text).toEqual('text\n');
|
||||
});
|
||||
@@ -1052,4 +1121,83 @@ brew`);
|
||||
expect(testBrew.tags).toEqual(['tag a']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateBrew', ()=>{
|
||||
it('should return error on version mismatch', async ()=>{
|
||||
const brewFromClient = { version: 1 };
|
||||
const brewFromServer = { version: 1000, text: '' };
|
||||
|
||||
const req = {
|
||||
brew : brewFromServer,
|
||||
body : brewFromClient
|
||||
};
|
||||
|
||||
await api.updateBrew(req, res);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(409);
|
||||
expect(res.send).toHaveBeenCalledWith('{\"message\":\"The server version is out of sync with the saved brew. Please save your changes elsewhere, refresh, and try again.\"}');
|
||||
});
|
||||
|
||||
it('should return error on hash mismatch', async ()=>{
|
||||
const brewFromClient = { version: 1, hash: '1234' };
|
||||
const brewFromServer = { version: 1, text: 'test' };
|
||||
|
||||
const req = {
|
||||
brew : brewFromServer,
|
||||
body : brewFromClient
|
||||
};
|
||||
|
||||
await api.updateBrew(req, res);
|
||||
|
||||
expect(req.brew.hash).toBe('098f6bcd4621d373cade4e832627b4f6');
|
||||
expect(res.status).toHaveBeenCalledWith(409);
|
||||
expect(res.send).toHaveBeenCalledWith('{\"message\":\"The server copy is out of sync with the saved brew. Please save your changes elsewhere, refresh, and try again.\"}');
|
||||
});
|
||||
|
||||
// Commenting this one out for now, since we are no longer throwing this error while we monitor
|
||||
// it('should return error on applying patches', async ()=>{
|
||||
// const brewFromClient = { version: 1, hash: '098f6bcd4621d373cade4e832627b4f6', patches: 'not a valid patch string' };
|
||||
// const brewFromServer = { version: 1, text: 'test', title: 'Test Title', description: 'Test Description' };
|
||||
|
||||
// const req = {
|
||||
// brew : brewFromServer,
|
||||
// body : brewFromClient,
|
||||
// };
|
||||
|
||||
// let err;
|
||||
// try {
|
||||
// await api.updateBrew(req, res);
|
||||
// } catch (e) {
|
||||
// err = e;
|
||||
// }
|
||||
|
||||
// expect(err).toEqual(Error('Invalid patch string: not a valid patch string'));
|
||||
// });
|
||||
|
||||
it('should save brew, no ID', async ()=>{
|
||||
const brewFromClient = { version: 1, hash: '098f6bcd4621d373cade4e832627b4f6', patches: '' };
|
||||
const brewFromServer = { version: 1, text: 'test', title: 'Test Title', description: 'Test Description' };
|
||||
|
||||
model.save = jest.fn((brew)=>{return brew;});
|
||||
|
||||
const req = {
|
||||
brew : brewFromServer,
|
||||
body : brewFromClient,
|
||||
query : { saveToGoogle: false, removeFromGoogle: false }
|
||||
};
|
||||
|
||||
await api.updateBrew(req, res);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(200);
|
||||
expect(res.send).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
_id : '1',
|
||||
description : 'Test Description',
|
||||
hash : '098f6bcd4621d373cade4e832627b4f6',
|
||||
title : 'Test Title',
|
||||
version : 2
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -27,7 +27,9 @@ const HomebrewSchema = mongoose.Schema({
|
||||
updatedAt : { type: Date, default: Date.now },
|
||||
lastViewed : { type: Date, default: Date.now },
|
||||
views : { type: Number, default: 0 },
|
||||
version : { type: Number, default: 1 }
|
||||
version : { type: Number, default: 1 },
|
||||
|
||||
lock : { type: Object }
|
||||
}, { versionKey: false });
|
||||
|
||||
HomebrewSchema.statics.increaseView = async function(query) {
|
||||
@@ -63,7 +65,7 @@ HomebrewSchema.statics.getByUser = async function(username, allowAccess=false, f
|
||||
|
||||
const Homebrew = mongoose.model('Homebrew', HomebrewSchema);
|
||||
|
||||
export {
|
||||
export {
|
||||
HomebrewSchema as schema,
|
||||
Homebrew as model
|
||||
};
|
||||
|
||||
@@ -5,21 +5,16 @@ import config from './config.js';
|
||||
const generateAccessToken = (account)=>{
|
||||
const payload = account;
|
||||
|
||||
// When the token was issued
|
||||
payload.issued = (new Date());
|
||||
// Which service issued the Token
|
||||
payload.issuer = config.get('authentication_token_issuer');
|
||||
// Which service is the token intended for
|
||||
payload.audience = config.get('authentication_token_audience');
|
||||
// The signing key for signing the token
|
||||
payload.issued = (new Date()); // When the token was issued
|
||||
payload.issuer = config.get('authentication_token_issuer'); // Which service issued the Token
|
||||
payload.audience = config.get('authentication_token_audience'); // Which service is the token intended for
|
||||
const secret = config.get('authentication_token_secret'); // The signing key for signing the token
|
||||
|
||||
delete payload.password;
|
||||
delete payload._id;
|
||||
|
||||
const secret = config.get('authentication_token_secret');
|
||||
|
||||
const token = jwt.encode(payload, secret);
|
||||
|
||||
return token;
|
||||
};
|
||||
|
||||
export default generateAccessToken;
|
||||
export default generateAccessToken;
|
||||
|
||||
27
server/token.spec.js
Normal file
27
server/token.spec.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import { expect, jest } from '@jest/globals';
|
||||
import config from './config.js';
|
||||
|
||||
import generateAccessToken from './token';
|
||||
|
||||
describe('Tests for Token', ()=>{
|
||||
it('Get token', ()=>{
|
||||
|
||||
// Mock the Config module, so we aren't grabbing actual secrets for testing
|
||||
jest.mock('./config.js');
|
||||
config.get = jest.fn((param)=>{
|
||||
// The requested key name will be reflected to the output
|
||||
return param;
|
||||
});
|
||||
|
||||
const account = {};
|
||||
|
||||
const token = generateAccessToken(account);
|
||||
|
||||
// If these tests fail, the config mock has failed
|
||||
expect(account).toHaveProperty('issuer', 'authentication_token_issuer');
|
||||
expect(account).toHaveProperty('audience', 'authentication_token_audience');
|
||||
|
||||
// Because the inputs are fixed, this JWT key should be static
|
||||
expect(typeof token).toBe('string');
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import express from 'express';
|
||||
import asyncHandler from 'express-async-handler';
|
||||
import {model as HomebrewModel } from './homebrew.model.js';
|
||||
import { model as HomebrewModel } from './homebrew.model.js';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
@@ -29,7 +29,7 @@ const rendererConditions = (legacy, v3)=>{
|
||||
return {}; // If all renderers selected, renderer field not needed in query for speed
|
||||
};
|
||||
|
||||
const sortConditions = (sort, dir) => {
|
||||
const sortConditions = (sort, dir)=>{
|
||||
return { [sort]: dir === 'asc' ? 1 : -1 };
|
||||
};
|
||||
|
||||
|
||||
@@ -2,24 +2,103 @@ import _ from 'lodash';
|
||||
import yaml from 'js-yaml';
|
||||
import request from '../client/homebrew/utils/request-middleware.js';
|
||||
|
||||
// Convert the templates from a brew to a Snippets Structure.
|
||||
const brewSnippetsToJSON = (menuTitle, userBrewSnippets, themeBundleSnippets=null, full=true)=>{
|
||||
const textSplit = /^(\\snippet +.+\n)/gm;
|
||||
const mpAsSnippets = [];
|
||||
// Snippets from Themes first.
|
||||
if(themeBundleSnippets) {
|
||||
for (let themes of themeBundleSnippets) {
|
||||
if(typeof themes !== 'string') {
|
||||
const userSnippets = [];
|
||||
const snipSplit = themes.snippets.trim().split(textSplit).slice(1);
|
||||
for (let snips = 0; snips < snipSplit.length; snips+=2) {
|
||||
if(!snipSplit[snips].startsWith('\\snippet ')) break;
|
||||
const snippetName = snipSplit[snips].split(/\\snippet +/)[1].split('\n')[0].trim();
|
||||
if(snippetName.length != 0) {
|
||||
userSnippets.push({
|
||||
name : snippetName,
|
||||
icon : '',
|
||||
gen : snipSplit[snips + 1],
|
||||
});
|
||||
}
|
||||
}
|
||||
if(userSnippets.length > 0) {
|
||||
mpAsSnippets.push({
|
||||
name : themes.name,
|
||||
icon : '',
|
||||
gen : '',
|
||||
subsnippets : userSnippets
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Local Snippets
|
||||
if(userBrewSnippets) {
|
||||
const userSnippets = [];
|
||||
const snipSplit = userBrewSnippets.trim().split(textSplit).slice(1);
|
||||
for (let snips = 0; snips < snipSplit.length; snips+=2) {
|
||||
if(!snipSplit[snips].startsWith('\\snippet ')) break;
|
||||
const snippetName = snipSplit[snips].split(/\\snippet +/)[1].split('\n')[0].trim();
|
||||
if(snippetName.length != 0) {
|
||||
const subSnip = {
|
||||
name : snippetName,
|
||||
gen : snipSplit[snips + 1],
|
||||
};
|
||||
// if(full) subSnip.icon = '';
|
||||
userSnippets.push(subSnip);
|
||||
}
|
||||
}
|
||||
if(userSnippets.length) {
|
||||
mpAsSnippets.push({
|
||||
name : menuTitle,
|
||||
// icon : '',
|
||||
subsnippets : userSnippets
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const returnObj = {
|
||||
snippets : mpAsSnippets
|
||||
};
|
||||
|
||||
if(full) {
|
||||
returnObj.groupName = 'Brew Snippets';
|
||||
returnObj.icon = 'fas fa-th-list';
|
||||
returnObj.view = 'text';
|
||||
}
|
||||
|
||||
return returnObj;
|
||||
};
|
||||
|
||||
const yamlSnippetsToText = (yamlObj)=>{
|
||||
if(typeof yamlObj == 'string') return yamlObj;
|
||||
|
||||
let snippetsText = '';
|
||||
|
||||
for (let snippet of yamlObj) {
|
||||
for (let subSnippet of snippet.subsnippets) {
|
||||
snippetsText = `${snippetsText}\\snippet ${subSnippet.name}\n${subSnippet.gen || ''}\n`;
|
||||
}
|
||||
}
|
||||
return snippetsText;
|
||||
};
|
||||
|
||||
const splitTextStyleAndMetadata = (brew)=>{
|
||||
brew.text = brew.text.replaceAll('\r\n', '\n');
|
||||
if(brew.text.startsWith('```metadata')) {
|
||||
const index = brew.text.indexOf('```\n\n');
|
||||
const metadataSection = brew.text.slice(12, index - 1);
|
||||
const index = brew.text.indexOf('\n```\n\n');
|
||||
const metadataSection = brew.text.slice(11, index + 1);
|
||||
const metadata = yaml.load(metadataSection);
|
||||
Object.assign(brew, _.pick(metadata, ['title', 'description', 'tags', 'systems', 'renderer', 'theme', 'lang']));
|
||||
brew.text = brew.text.slice(index + 5);
|
||||
brew.snippets = yamlSnippetsToText(_.pick(metadata, ['snippets']).snippets || '');
|
||||
brew.text = brew.text.slice(index + 6);
|
||||
}
|
||||
if(brew.text.startsWith('```css')) {
|
||||
const index = brew.text.indexOf('```\n\n');
|
||||
brew.style = brew.text.slice(7, index - 1);
|
||||
brew.text = brew.text.slice(index + 5);
|
||||
}
|
||||
if(brew.text.startsWith('```snippets')) {
|
||||
const index = brew.text.indexOf('```\n\n');
|
||||
brew.snippets = brew.text.slice(11, index - 1);
|
||||
brew.text = brew.text.slice(index + 5);
|
||||
const index = brew.text.indexOf('\n```\n\n');
|
||||
brew.style = brew.text.slice(7, index + 1);
|
||||
brew.text = brew.text.slice(index + 6);
|
||||
}
|
||||
|
||||
// Handle old brews that still have empty strings in the tags metadata
|
||||
@@ -60,8 +139,45 @@ const fetchThemeBundle = async (obj, renderer, theme)=>{
|
||||
}));
|
||||
};
|
||||
|
||||
const debugTextMismatch = (clientTextRaw, serverTextRaw, label) => {
|
||||
const clientText = clientTextRaw?.normalize('NFC') || '';
|
||||
const serverText = serverTextRaw?.normalize('NFC') || '';
|
||||
|
||||
const clientBuffer = Buffer.from(clientText, 'utf8');
|
||||
const serverBuffer = Buffer.from(serverText, 'utf8');
|
||||
|
||||
if (clientBuffer.equals(serverBuffer)) {
|
||||
console.log(`✅ ${label} text matches byte-for-byte.`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.warn(`❗${label} text mismatch detected.`);
|
||||
console.log(`Client length: ${clientBuffer.length}`);
|
||||
console.log(`Server length: ${serverBuffer.length}`);
|
||||
|
||||
// Byte-level diff
|
||||
for (let i = 0; i < Math.min(clientBuffer.length, serverBuffer.length); i++) {
|
||||
if (clientBuffer[i] !== serverBuffer[i]) {
|
||||
console.log(`Byte mismatch at offset ${i}: client=0x${clientBuffer[i].toString(16)} server=0x${serverBuffer[i].toString(16)}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Char-level diff
|
||||
for (let i = 0; i < Math.min(clientText.length, serverText.length); i++) {
|
||||
if (clientText[i] !== serverText[i]) {
|
||||
console.log(`Char mismatch at index ${i}:`);
|
||||
console.log(` Client: '${clientText[i]}' (U+${clientText.charCodeAt(i).toString(16).toUpperCase()})`);
|
||||
console.log(` Server: '${serverText[i]}' (U+${serverText.charCodeAt(i).toString(16).toUpperCase()})`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
splitTextStyleAndMetadata,
|
||||
printCurrentBrew,
|
||||
fetchThemeBundle,
|
||||
brewSnippetsToJSON,
|
||||
debugTextMismatch
|
||||
};
|
||||
|
||||
@@ -4,10 +4,15 @@ import _ from 'lodash';
|
||||
import { Parser as MathParser } from 'expr-eval';
|
||||
import { marked as Marked } from 'marked';
|
||||
import MarkedExtendedTables from 'marked-extended-tables';
|
||||
import MarkedDefinitionLists from 'marked-definition-lists';
|
||||
import MarkedAlignedParagraphs from 'marked-alignment-paragraphs';
|
||||
import MarkedNonbreakingSpaces from 'marked-nonbreaking-spaces';
|
||||
import MarkedSubSuperText from 'marked-subsuper-text';
|
||||
import { markedSmartypantsLite as MarkedSmartypantsLite } from 'marked-smartypants-lite';
|
||||
import { gfmHeadingId as MarkedGFMHeadingId, resetHeadings as MarkedGFMResetHeadingIDs } from 'marked-gfm-heading-id';
|
||||
import { markedEmoji as MarkedEmojis } from 'marked-emoji';
|
||||
import MarkedSubSuperText from 'marked-subsuper-text';
|
||||
import { romanize } from 'romans';
|
||||
import writtenNumber from 'written-number';
|
||||
|
||||
//Icon fonts included so they can appear in emoji autosuggest dropdown
|
||||
import diceFont from '../../themes/fonts/iconFonts/diceFont.js';
|
||||
@@ -59,6 +64,53 @@ mathParser.functions.signed = function (a) {
|
||||
if(a >= 0) return `+${a}`;
|
||||
return `${a}`;
|
||||
};
|
||||
// Add Roman numeral functions
|
||||
mathParser.functions.toRomans = function (a) {
|
||||
return romanize(a);
|
||||
};
|
||||
mathParser.functions.toRomansUpper = function (a) {
|
||||
return romanize(a).toUpperCase();
|
||||
};
|
||||
mathParser.functions.toRomansLower = function (a) {
|
||||
return romanize(a).toLowerCase();
|
||||
};
|
||||
// Add character functions
|
||||
mathParser.functions.toChar = function (a) {
|
||||
if(a <= 0) return a;
|
||||
const genChars = function (i) {
|
||||
return (i > 26 ? genChars(Math.floor((i - 1) / 26)) : '') + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'[(i - 1) % 26];
|
||||
};
|
||||
return genChars(a);
|
||||
};
|
||||
mathParser.functions.toCharUpper = function (a) {
|
||||
return mathParser.functions.toChar(a).toUpperCase();
|
||||
};
|
||||
mathParser.functions.toCharLower = function (a) {
|
||||
return mathParser.functions.toChar(a).toLowerCase();
|
||||
};
|
||||
// Add word functions
|
||||
mathParser.functions.toWords = function (a) {
|
||||
return writtenNumber(a);
|
||||
};
|
||||
mathParser.functions.toWordsUpper = function (a) {
|
||||
return mathParser.functions.toWords(a).toUpperCase();
|
||||
};
|
||||
mathParser.functions.toWordsLower = function (a) {
|
||||
return mathParser.functions.toWords(a).toLowerCase();
|
||||
};
|
||||
mathParser.functions.toWordsCaps = function (a) {
|
||||
const words = mathParser.functions.toWords(a).split(' ');
|
||||
return words.map((word)=>{
|
||||
return word.replace(/(?:^|\b|\s)(\w)/g, function(w, index) {
|
||||
return index === 0 ? w.toLowerCase() : w.toUpperCase();
|
||||
});
|
||||
}).join(' ');
|
||||
};
|
||||
|
||||
// Normalize variable names; trim edge spaces and shorten blocks of whitespace to 1 space
|
||||
const normalizeVarNames = (label)=>{
|
||||
return label.trim().replace(/\s+/g, ' ');
|
||||
};
|
||||
|
||||
//Processes the markdown within an HTML block if it's just a class-wrapper
|
||||
renderer.html = function (token) {
|
||||
@@ -86,8 +138,8 @@ renderer.paragraph = function(token){
|
||||
|
||||
//Fix local links in the Preview iFrame to link inside the frame
|
||||
renderer.link = function (token) {
|
||||
let {href, title, tokens} = token;
|
||||
const text = this.parser.parseInline(tokens)
|
||||
let { href, title, tokens } = token;
|
||||
const text = this.parser.parseInline(tokens);
|
||||
let self = false;
|
||||
if(href[0] == '#') {
|
||||
self = true;
|
||||
@@ -99,7 +151,7 @@ renderer.link = function (token) {
|
||||
}
|
||||
let out = `<a href="${escape(href)}"`;
|
||||
if(title) {
|
||||
out += ` title="${title}"`;
|
||||
out += ` title="${escape(title)}"`;
|
||||
}
|
||||
if(self) {
|
||||
out += ' target="_self"';
|
||||
@@ -110,7 +162,7 @@ renderer.link = function (token) {
|
||||
|
||||
// Expose `src` attribute as `--HB_src` to make the URL accessible via CSS
|
||||
renderer.image = function (token) {
|
||||
let {href, title, text} = token;
|
||||
const { href, title, text } = token;
|
||||
if(href === null)
|
||||
return text;
|
||||
|
||||
@@ -133,7 +185,7 @@ const mustacheSpans = {
|
||||
start(src) { return src.match(/{{[^{]/)?.index; }, // Hint to Marked.js to stop and check for a match
|
||||
tokenizer(src, tokens) {
|
||||
const completeSpan = /^{{[^\n]*}}/; // Regex for the complete token
|
||||
const inlineRegex = /{{(?=((?:[:=](?:"['\w,\-()#%=?. ]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1 *|}}/g;
|
||||
const inlineRegex = /{{(?=((?:[:=](?:"['\w,\-+*/()#%=?. ]*"|[\w\-+*/()#%.]*)|[^"=':{}\s]*)*))\1 *|}}/g;
|
||||
const match = completeSpan.exec(src);
|
||||
if(match) {
|
||||
//Find closing delimiter
|
||||
@@ -190,7 +242,7 @@ const mustacheDivs = {
|
||||
start(src) { return src.match(/\n *{{[^{]/m)?.index; }, // Hint to Marked.js to stop and check for a match
|
||||
tokenizer(src, tokens) {
|
||||
const completeBlock = /^ *{{[^\n}]* *\n.*\n *}}/s; // Regex for the complete token
|
||||
const blockRegex = /^ *{{(?=((?:[:=](?:"['\w,\-()#%=?. ]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1 *$|^ *}}$/gm;
|
||||
const blockRegex = /^ *{{(?=((?:[:=](?:"['\w,\-+*/()#%=?. ]*"|[\w\-+*/()#%.]*)|[^"=':{}\s]*)*))\1 *$|^ *}}$/gm;
|
||||
const match = completeBlock.exec(src);
|
||||
if(match) {
|
||||
//Find closing delimiter
|
||||
@@ -245,7 +297,7 @@ const mustacheInjectInline = {
|
||||
level : 'inline',
|
||||
start(src) { return src.match(/ *{[^{\n]/)?.index; }, // Hint to Marked.js to stop and check for a match
|
||||
tokenizer(src, tokens) {
|
||||
const inlineRegex = /^ *{(?=((?:[:=](?:"['\w,\-()#%=?. ]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1}/g;
|
||||
const inlineRegex = /^ *{(?=((?:[:=](?:"['\w,\-+*/()#%=?. ]*"|[\w\-+*/()#%.]*)|[^"=':{}\s]*)*))\1}/g;
|
||||
const match = inlineRegex.exec(src);
|
||||
if(match) {
|
||||
const lastToken = tokens[tokens.length - 1];
|
||||
@@ -291,7 +343,7 @@ const mustacheInjectBlock = {
|
||||
level : 'block',
|
||||
start(src) { return src.match(/\n *{[^{\n]/m)?.index; }, // Hint to Marked.js to stop and check for a match
|
||||
tokenizer(src, tokens) {
|
||||
const inlineRegex = /^ *{(?=((?:[:=](?:"['\w,\-()#%=?. ]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1}/ym;
|
||||
const inlineRegex = /^ *{(?=((?:[:=](?:"['\w,\-+*/()#%=?. ]*"|[\w\-+*/()#%.]*)|[^"=':{}\s]*)*))\1}/ym;
|
||||
const match = inlineRegex.exec(src);
|
||||
if(match) {
|
||||
const lastToken = tokens[tokens.length - 1];
|
||||
@@ -338,42 +390,6 @@ const mustacheInjectBlock = {
|
||||
}
|
||||
};
|
||||
|
||||
const justifiedParagraphClasses = [];
|
||||
justifiedParagraphClasses[2] = 'Left';
|
||||
justifiedParagraphClasses[4] = 'Right';
|
||||
justifiedParagraphClasses[6] = 'Center';
|
||||
|
||||
const justifiedParagraphs = {
|
||||
name : 'justifiedParagraphs',
|
||||
level : 'block',
|
||||
start(src) {
|
||||
return src.match(/\n(?:-:|:-|-:) {1}/m)?.index;
|
||||
}, // Hint to Marked.js to stop and check for a match
|
||||
tokenizer(src, tokens) {
|
||||
const regex = /^(((:-))|((-:))|((:-:))) .+(\n(([^\n].*\n)*(\n|$))|$)/ygm;
|
||||
const match = regex.exec(src);
|
||||
if(match?.length) {
|
||||
let whichJustify;
|
||||
if(match[2]?.length) whichJustify = 2;
|
||||
if(match[4]?.length) whichJustify = 4;
|
||||
if(match[6]?.length) whichJustify = 6;
|
||||
return {
|
||||
type : 'justifiedParagraphs', // Should match "name" above
|
||||
raw : match[0], // Text to consume from the source
|
||||
length : match[whichJustify].length,
|
||||
text : match[0].slice(match[whichJustify].length),
|
||||
class : justifiedParagraphClasses[whichJustify],
|
||||
tokens : this.lexer.inlineTokens(match[0].slice(match[whichJustify].length + 1))
|
||||
};
|
||||
}
|
||||
},
|
||||
renderer(token) {
|
||||
return `<p align="${token.class}">${this.parser.parseInline(token.tokens)}</p>`;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
|
||||
const forcedParagraphBreaks = {
|
||||
name : 'hardBreaks',
|
||||
level : 'block',
|
||||
@@ -391,115 +407,7 @@ const forcedParagraphBreaks = {
|
||||
}
|
||||
},
|
||||
renderer(token) {
|
||||
return `<br>\n`.repeat(token.length);
|
||||
}
|
||||
};
|
||||
|
||||
const nonbreakingSpaces = {
|
||||
name : 'nonbreakingSpaces',
|
||||
level : 'inline',
|
||||
start(src) { return src.match(/:>+/m)?.index; }, // Hint to Marked.js to stop and check for a match
|
||||
tokenizer(src, tokens) {
|
||||
const regex = /:(>+)/ym;
|
||||
const match = regex.exec(src);
|
||||
if(match?.length) {
|
||||
return {
|
||||
type : 'nonbreakingSpaces', // Should match "name" above
|
||||
raw : match[0], // Text to consume from the source
|
||||
length : match[1].length,
|
||||
text : ''
|
||||
};
|
||||
}
|
||||
},
|
||||
renderer(token) {
|
||||
return ` `.repeat(token.length).concat('');
|
||||
}
|
||||
};
|
||||
|
||||
const definitionListsSingleLine = {
|
||||
name : 'definitionListsSingleLine',
|
||||
level : 'block',
|
||||
start(src) { return src.match(/\n[^\n]*?::[^\n]*/m)?.index; }, // Hint to Marked.js to stop and check for a match
|
||||
tokenizer(src, tokens) {
|
||||
const regex = /^([^\n]*?)::([^\n]*)(?:\n|$)/ym;
|
||||
let match;
|
||||
let endIndex = 0;
|
||||
const definitions = [];
|
||||
while (match = regex.exec(src)) {
|
||||
const originalLine = match[0]; // This line and below to handle conflict with emojis
|
||||
let firstLine = originalLine; // Remove in V4 when definitionListsInline updated to
|
||||
this.lexer.inlineTokens(firstLine.trim()) // require spaces around `::`
|
||||
.filter((t)=>t.type == 'emoji')
|
||||
.map((emoji)=>firstLine = firstLine.replace(emoji.raw, 'x'.repeat(emoji.raw.length)));
|
||||
|
||||
const newMatch = /^([^\n]*?)::([^\n]*)(?:\n|$)/ym.exec(firstLine);
|
||||
if(newMatch) {
|
||||
definitions.push({
|
||||
dt : this.lexer.inlineTokens(originalLine.slice(0, newMatch[1].length).trim()),
|
||||
dd : this.lexer.inlineTokens(originalLine.slice(newMatch[1].length + 2).trim())
|
||||
});
|
||||
} // End of emoji hack.
|
||||
endIndex = regex.lastIndex;
|
||||
}
|
||||
if(definitions.length) {
|
||||
return {
|
||||
type : 'definitionListsSingleLine',
|
||||
raw : src.slice(0, endIndex),
|
||||
definitions
|
||||
};
|
||||
}
|
||||
},
|
||||
renderer(token) {
|
||||
return `<dl>${token.definitions.reduce((html, def)=>{
|
||||
return `${html}<dt>${this.parser.parseInline(def.dt)}</dt>`
|
||||
+ `<dd>${this.parser.parseInline(def.dd)}</dd>\n`;
|
||||
}, '')}</dl>`;
|
||||
}
|
||||
};
|
||||
|
||||
const definitionListsMultiLine = {
|
||||
name : 'definitionListsMultiLine',
|
||||
level : 'block',
|
||||
start(src) { return src.match(/\n[^\n]*\n::[^:\n]/m)?.index; }, // Hint to Marked.js to stop and check for a match
|
||||
tokenizer(src, tokens) {
|
||||
const regex = /(\n?\n?(?!::)[^\n]+?(?=\n::[^:\n]))|\n::([^:\n](?:.|\n)*?(?=(?:\n::)|(?:\n\n)|$))/y;
|
||||
let match;
|
||||
let endIndex = 0;
|
||||
const definitions = [];
|
||||
while (match = regex.exec(src)) {
|
||||
if(match[1]) {
|
||||
if(this.lexer.blockTokens(match[1].trim())[0]?.type !== 'paragraph') // DT must not be another block-level token besides <p>
|
||||
break;
|
||||
definitions.push({
|
||||
dt : this.lexer.inlineTokens(match[1].trim()),
|
||||
dds : []
|
||||
});
|
||||
}
|
||||
if(match[2] && definitions.length) {
|
||||
definitions[definitions.length - 1].dds.push(
|
||||
this.lexer.inlineTokens(match[2].trim().replace(/\s/g, ' '))
|
||||
);
|
||||
}
|
||||
endIndex = regex.lastIndex;
|
||||
}
|
||||
if(definitions.length) {
|
||||
return {
|
||||
type : 'definitionListsMultiLine',
|
||||
raw : src.slice(0, endIndex),
|
||||
definitions
|
||||
};
|
||||
}
|
||||
},
|
||||
renderer(token) {
|
||||
let returnVal = `<dl>`;
|
||||
token.definitions.forEach((def)=>{
|
||||
const dds = def.dds.map((s)=>{
|
||||
return `\n<dd>${this.parser.parseInline(s).trim()}</dd>`;
|
||||
}).join('');
|
||||
returnVal += `<dt>${this.parser.parseInline(def.dt)}</dt>${dds}\n`;
|
||||
});
|
||||
returnVal = returnVal.trim();
|
||||
return `${returnVal}</dl>`;
|
||||
return `<div class='blank'></div>\n`.repeat(token.length);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -509,7 +417,7 @@ const replaceVar = function(input, hoist=false, allowUnresolved=false) {
|
||||
const match = regex.exec(input);
|
||||
|
||||
const prefix = match[1];
|
||||
const label = match[2];
|
||||
const label = normalizeVarNames(match[2]); // Ensure the label name is normalized as it should be in the var stack.
|
||||
|
||||
//v=====--------------------< HANDLE MATH >-------------------=====v//
|
||||
const mathRegex = /[a-z]+\(|[+\-*/^(),]/g;
|
||||
@@ -664,8 +572,8 @@ function MarkedVariables() {
|
||||
});
|
||||
}
|
||||
if(match[3]) { // Block Definition
|
||||
const label = match[4] ? match[4].trim().replace(/\s+/g, ' ') : null; // Trim edge spaces and shorten blocks of whitespace to 1 space
|
||||
const content = match[5] ? match[5].trim().replace(/[ \t]+/g, ' ') : null; // Trim edge spaces and shorten blocks of whitespace to 1 space
|
||||
const label = match[4] ? normalizeVarNames(match[4]) : null;
|
||||
const content = match[5] ? match[5].trim().replace(/[ \t]+/g, ' ') : null; // Normalize text content (except newlines for block-level content)
|
||||
|
||||
varsQueue.push(
|
||||
{ type : 'varDefBlock',
|
||||
@@ -674,7 +582,7 @@ function MarkedVariables() {
|
||||
});
|
||||
}
|
||||
if(match[6]) { // Block Call
|
||||
const label = match[7] ? match[7].trim().replace(/\s+/g, ' ') : null; // Trim edge spaces and shorten blocks of whitespace to 1 space
|
||||
const label = match[7] ? normalizeVarNames(match[7]) : null;
|
||||
|
||||
varsQueue.push(
|
||||
{ type : 'varCallBlock',
|
||||
@@ -683,7 +591,7 @@ function MarkedVariables() {
|
||||
});
|
||||
}
|
||||
if(match[8]) { // Inline Definition
|
||||
const label = match[10] ? match[10].trim().replace(/\s+/g, ' ') : null; // Trim edge spaces and shorten blocks of whitespace to 1 space
|
||||
const label = match[10] ? normalizeVarNames(match[10]) : null;
|
||||
let content = match[11] || null;
|
||||
|
||||
// In case of nested (), find the correct matching end )
|
||||
@@ -715,7 +623,7 @@ function MarkedVariables() {
|
||||
});
|
||||
}
|
||||
if(match[12]) { // Inline Call
|
||||
const label = match[13] ? match[13].trim().replace(/\s+/g, ' ') : null; // Trim edge spaces and shorten blocks of whitespace to 1 space
|
||||
const label = match[13] ? normalizeVarNames(match[13]) : null;
|
||||
|
||||
varsQueue.push(
|
||||
{ type : 'varCallInline',
|
||||
@@ -771,12 +679,14 @@ const tableTerminators = [
|
||||
];
|
||||
|
||||
Marked.use(MarkedVariables());
|
||||
Marked.use({ extensions : [justifiedParagraphs, definitionListsMultiLine, definitionListsSingleLine, forcedParagraphBreaks,
|
||||
nonbreakingSpaces, mustacheSpans, mustacheDivs, mustacheInjectInline] });
|
||||
Marked.use(MarkedDefinitionLists());
|
||||
Marked.use({ extensions : [forcedParagraphBreaks, mustacheSpans, mustacheDivs, mustacheInjectInline] });
|
||||
Marked.use(mustacheInjectBlock);
|
||||
Marked.use(MarkedAlignedParagraphs());
|
||||
Marked.use(MarkedSubSuperText());
|
||||
Marked.use(MarkedNonbreakingSpaces());
|
||||
Marked.use({ renderer: renderer, tokenizer: tokenizer, mangle: false });
|
||||
Marked.use(MarkedExtendedTables({interruptPatterns : tableTerminators}), MarkedGFMHeadingId({ globalSlugs: true }),
|
||||
Marked.use(MarkedExtendedTables({ interruptPatterns: tableTerminators }), MarkedGFMHeadingId({ globalSlugs: true }),
|
||||
MarkedSmartypantsLite(), MarkedEmojis(MarkedEmojiOptions));
|
||||
|
||||
function cleanUrl(href) {
|
||||
@@ -841,12 +751,12 @@ const processStyleTags = (string)=>{
|
||||
obj[key.trim()] = value.trim();
|
||||
return obj;
|
||||
}, {}) || null;
|
||||
const styles = tags?.length ? tags.reduce((styleObj, style) => {
|
||||
const index = style.indexOf(':');
|
||||
const [key, value] = [style.substring(0, index), style.substring(index + 1)];
|
||||
styleObj[key.trim()] = value.replace(/"?([^"]*)"?/g, '$1').trim();
|
||||
return styleObj;
|
||||
}, {}) : null;
|
||||
const styles = tags?.length ? tags.reduce((styleObj, style)=>{
|
||||
const index = style.indexOf(':');
|
||||
const [key, value] = [style.substring(0, index), style.substring(index + 1)];
|
||||
styleObj[key.trim()] = value.replace(/"?([^"]*)"?/g, '$1').trim();
|
||||
return styleObj;
|
||||
}, {}) : null;
|
||||
|
||||
return {
|
||||
id : id,
|
||||
@@ -862,8 +772,8 @@ const extractHTMLStyleTags = (htmlString)=>{
|
||||
const id = firstElementOnly.match(/id="([^"]*)"/)?.[1] || null;
|
||||
const classes = firstElementOnly.match(/class="([^"]*)"/)?.[1] || null;
|
||||
const styles = firstElementOnly.match(/style="([^"]*)"/)?.[1]
|
||||
?.split(';').reduce((styleObj, style) => {
|
||||
if (style.trim() === '') return styleObj;
|
||||
?.split(';').reduce((styleObj, style)=>{
|
||||
if(style.trim() === '') return styleObj;
|
||||
const index = style.indexOf(':');
|
||||
const [key, value] = [style.substring(0, index), style.substring(index + 1)];
|
||||
styleObj[key.trim()] = value.trim();
|
||||
@@ -873,7 +783,7 @@ const extractHTMLStyleTags = (htmlString)=>{
|
||||
?.filter((attr)=>!attr.startsWith('class="') && !attr.startsWith('style="') && !attr.startsWith('id="'))
|
||||
.reduce((obj, attr)=>{
|
||||
const index = attr.indexOf('=');
|
||||
let [key, value] = [attr.substring(0, index), attr.substring(index + 1)];
|
||||
const [key, value] = [attr.substring(0, index), attr.substring(index + 1)];
|
||||
obj[key.trim()] = value.replace(/"/g, '');
|
||||
return obj;
|
||||
}, {}) || null;
|
||||
@@ -886,7 +796,7 @@ const extractHTMLStyleTags = (htmlString)=>{
|
||||
};
|
||||
};
|
||||
|
||||
const mergeHTMLTags = (originalTags, newTags) => {
|
||||
const mergeHTMLTags = (originalTags, newTags)=>{
|
||||
return {
|
||||
id : newTags.id || originalTags.id || null,
|
||||
classes : [originalTags.classes, newTags.classes].join(' ').trim() || null,
|
||||
@@ -902,14 +812,20 @@ let globalPageNumber = 0;
|
||||
const Markdown = {
|
||||
marked : Marked,
|
||||
render : (rawBrewText, pageNumber=0)=>{
|
||||
globalVarsList[pageNumber] = {}; //Reset global links for current page, to ensure values are parsed in order
|
||||
const lastPageNumber = pageNumber > 0 ? globalVarsList[pageNumber - 1].HB_pageNumber.content : 0;
|
||||
globalVarsList[pageNumber] = { //Reset global links for current page, to ensure values are parsed in order
|
||||
'HB_pageNumber' : { //Add document variables for this page
|
||||
content : !isNaN(Number(lastPageNumber)) ? Number(lastPageNumber) + 1 : lastPageNumber,
|
||||
resolved : true
|
||||
}
|
||||
};
|
||||
varsQueue = []; //Could move into MarkedVariables()
|
||||
globalPageNumber = pageNumber;
|
||||
if(pageNumber==0) {
|
||||
MarkedGFMResetHeadingIDs();
|
||||
}
|
||||
|
||||
rawBrewText = rawBrewText.replace(/^\\column$/gm, `\n<div class='columnSplit'></div>\n`);
|
||||
rawBrewText = rawBrewText.replace(/^\\column(?:break)?$/gm, `\n<div class='columnSplit'></div>\n`);
|
||||
|
||||
const opts = Marked.defaults;
|
||||
|
||||
|
||||
@@ -12,8 +12,8 @@ const Nav = {
|
||||
displayName : 'Nav.base',
|
||||
render : function(){
|
||||
return <nav>
|
||||
{this.props.children}
|
||||
</nav>;
|
||||
{this.props.children}
|
||||
</nav>;
|
||||
}
|
||||
}),
|
||||
logo : function(){
|
||||
|
||||
@@ -3,127 +3,127 @@
|
||||
@defaultEasing : ease;
|
||||
|
||||
//Animates all properties on an element
|
||||
.animateAll(@duration : @defaultDuration, @easing : @defaultEasing){
|
||||
-webkit-transition: all @duration @easing;
|
||||
-moz-transition: all @duration @easing;
|
||||
-o-transition: all @duration @easing;
|
||||
transition: all @duration @easing;
|
||||
.animateAll(@duration : @defaultDuration, @easing : @defaultEasing) {
|
||||
-webkit-transition : all @duration @easing;
|
||||
-moz-transition : all @duration @easing;
|
||||
-o-transition : all @duration @easing;
|
||||
transition : all @duration @easing;
|
||||
}
|
||||
//Animates Specific property
|
||||
.animate(@prop, @duration : @defaultDuration, @easing : @defaultEasing){
|
||||
-webkit-transition: @prop @duration @easing;
|
||||
-moz-transition: @prop @duration @easing;
|
||||
-o-transition: @prop @duration @easing;
|
||||
transition: @prop @duration @easing;
|
||||
.animate(@prop, @duration : @defaultDuration, @easing : @defaultEasing) {
|
||||
-webkit-transition : @prop @duration @easing;
|
||||
-moz-transition : @prop @duration @easing;
|
||||
-o-transition : @prop @duration @easing;
|
||||
transition : @prop @duration @easing;
|
||||
}
|
||||
|
||||
.animateMany(...){
|
||||
.animateMany(...) {
|
||||
@value: ~`"@{arguments}".replace(/[\[\]]|\,\sX/g, '')`;
|
||||
-webkit-transition-property: @value;
|
||||
-moz-transition-property: @value;
|
||||
-o-transition-property: @value;
|
||||
transition-property: @value;
|
||||
-webkit-transition-property : @value;
|
||||
-moz-transition-property : @value;
|
||||
-o-transition-property : @value;
|
||||
transition-property : @value;
|
||||
|
||||
.animateDuration();
|
||||
.animateEasing();
|
||||
}
|
||||
|
||||
.animateDuration(@duration : @defaultDuration){
|
||||
-webkit-transition-duration: @duration;
|
||||
-moz-transition-duration: @duration;
|
||||
-o-transition-duration: @duration;
|
||||
transition-duration: @duration;
|
||||
.animateDuration(@duration : @defaultDuration) {
|
||||
-webkit-transition-duration : @duration;
|
||||
-moz-transition-duration : @duration;
|
||||
-o-transition-duration : @duration;
|
||||
transition-duration : @duration;
|
||||
}
|
||||
|
||||
.animateEasing(@easing : @defaultEasing){
|
||||
-webkit-transition-timing-function: @easing;
|
||||
-moz-transition-timing-function: @easing;
|
||||
-o-transition-timing-function: @easing;
|
||||
transition-timing-function: @easing;
|
||||
.animateEasing(@easing : @defaultEasing) {
|
||||
-webkit-transition-timing-function : @easing;
|
||||
-moz-transition-timing-function : @easing;
|
||||
-o-transition-timing-function : @easing;
|
||||
transition-timing-function : @easing;
|
||||
}
|
||||
|
||||
|
||||
.transition (@prop, @duration: @defaultDuration) {
|
||||
-webkit-transition: @prop @duration, -webkit-transform @duration;
|
||||
-moz-transition: @prop @duration, -moz-transform @duration;
|
||||
-o-transition: @prop @duration, -o-transform @duration;
|
||||
-ms-transition: @prop @duration, -ms-transform @duration;
|
||||
transition: @prop @duration, transform @duration;
|
||||
-webkit-transition : @prop @duration, -webkit-transform @duration;
|
||||
-moz-transition : @prop @duration, -moz-transform @duration;
|
||||
-o-transition : @prop @duration, -o-transform @duration;
|
||||
-ms-transition : @prop @duration, -ms-transform @duration;
|
||||
transition : @prop @duration, transform @duration;
|
||||
}
|
||||
.transform (@transform) {
|
||||
-webkit-transform: @transform;
|
||||
-moz-transform: @transform;
|
||||
-o-transform: @transform;
|
||||
-ms-transform: @transform;
|
||||
transform: @transform;
|
||||
-webkit-transform : @transform;
|
||||
-moz-transform : @transform;
|
||||
-o-transform : @transform;
|
||||
-ms-transform : @transform;
|
||||
transform : @transform;
|
||||
}
|
||||
|
||||
|
||||
.delay(@delay){
|
||||
animation-delay:@delay;
|
||||
-webkit-animation-delay:@delay;
|
||||
transition-delay:@delay;
|
||||
-webkit-transition-delay:@delay;
|
||||
.delay(@delay) {
|
||||
-webkit-transition-delay : @delay;
|
||||
transition-delay : @delay;
|
||||
-webkit-animation-delay : @delay;
|
||||
animation-delay : @delay;
|
||||
}
|
||||
.keep(){
|
||||
-webkit-animation-fill-mode:forwards;
|
||||
-moz-animation-fill-mode:forwards;
|
||||
-ms-animation-fill-mode:forwards;
|
||||
-o-animation-fill-mode:forwards;
|
||||
animation-fill-mode:forwards;
|
||||
.keep() {
|
||||
-webkit-animation-fill-mode : forwards;
|
||||
-moz-animation-fill-mode : forwards;
|
||||
-ms-animation-fill-mode : forwards;
|
||||
-o-animation-fill-mode : forwards;
|
||||
animation-fill-mode : forwards;
|
||||
}
|
||||
|
||||
|
||||
.sequentialDelay(@delayInc : 0.2s, @initialDelay : 0s){
|
||||
&:nth-child(1){.delay(0*@delayInc + @initialDelay)}
|
||||
&:nth-child(2){.delay(1*@delayInc + @initialDelay)}
|
||||
&:nth-child(3){.delay(2*@delayInc + @initialDelay)}
|
||||
&:nth-child(4){.delay(3*@delayInc + @initialDelay)}
|
||||
&:nth-child(5){.delay(4*@delayInc + @initialDelay)}
|
||||
&:nth-child(6){.delay(5*@delayInc + @initialDelay)}
|
||||
&:nth-child(7){.delay(6*@delayInc + @initialDelay)}
|
||||
&:nth-child(8){.delay(7*@delayInc + @initialDelay)}
|
||||
&:nth-child(9){.delay(8*@delayInc + @initialDelay)}
|
||||
&:nth-child(10){.delay(9*@delayInc + @initialDelay)}
|
||||
&:nth-child(11){.delay(10*@delayInc + @initialDelay)}
|
||||
&:nth-child(12){.delay(11*@delayInc + @initialDelay)}
|
||||
&:nth-child(13){.delay(12*@delayInc + @initialDelay)}
|
||||
&:nth-child(14){.delay(13*@delayInc + @initialDelay)}
|
||||
&:nth-child(15){.delay(14*@delayInc + @initialDelay)}
|
||||
&:nth-child(16){.delay(15*@delayInc + @initialDelay)}
|
||||
&:nth-child(17){.delay(16*@delayInc + @initialDelay)}
|
||||
&:nth-child(18){.delay(17*@delayInc + @initialDelay)}
|
||||
&:nth-child(19){.delay(18*@delayInc + @initialDelay)}
|
||||
&:nth-child(20){.delay(19*@delayInc + @initialDelay)}
|
||||
.sequentialDelay(@delayInc : 0.2s, @initialDelay : 0s) {
|
||||
&:nth-child(1) {.delay(0*@delayInc + @initialDelay); }
|
||||
&:nth-child(2) {.delay(1*@delayInc + @initialDelay); }
|
||||
&:nth-child(3) {.delay(2*@delayInc + @initialDelay); }
|
||||
&:nth-child(4) {.delay(3*@delayInc + @initialDelay); }
|
||||
&:nth-child(5) {.delay(4*@delayInc + @initialDelay); }
|
||||
&:nth-child(6) {.delay(5*@delayInc + @initialDelay); }
|
||||
&:nth-child(7) {.delay(6*@delayInc + @initialDelay); }
|
||||
&:nth-child(8) {.delay(7*@delayInc + @initialDelay); }
|
||||
&:nth-child(9) {.delay(8*@delayInc + @initialDelay); }
|
||||
&:nth-child(10) {.delay(9*@delayInc + @initialDelay); }
|
||||
&:nth-child(11) {.delay(10*@delayInc + @initialDelay); }
|
||||
&:nth-child(12) {.delay(11*@delayInc + @initialDelay); }
|
||||
&:nth-child(13) {.delay(12*@delayInc + @initialDelay); }
|
||||
&:nth-child(14) {.delay(13*@delayInc + @initialDelay); }
|
||||
&:nth-child(15) {.delay(14*@delayInc + @initialDelay); }
|
||||
&:nth-child(16) {.delay(15*@delayInc + @initialDelay); }
|
||||
&:nth-child(17) {.delay(16*@delayInc + @initialDelay); }
|
||||
&:nth-child(18) {.delay(17*@delayInc + @initialDelay); }
|
||||
&:nth-child(19) {.delay(18*@delayInc + @initialDelay); }
|
||||
&:nth-child(20) {.delay(19*@delayInc + @initialDelay); }
|
||||
}
|
||||
|
||||
|
||||
|
||||
.createFrames(@name, @from, @to){
|
||||
.createFrames(@name, @from, @to) {
|
||||
@frames: {
|
||||
from { @from(); }
|
||||
to { @to(); }
|
||||
};
|
||||
@-webkit-keyframes @name {@frames();}
|
||||
@-moz-keyframes @name {@frames();}
|
||||
@-ms-keyframes @name {@frames();}
|
||||
@-o-keyframes @name {@frames();}
|
||||
@keyframes @name {@frames();}
|
||||
@-webkit-keyframes @name {@frames();}
|
||||
@-moz-keyframes @name {@frames();}
|
||||
@-ms-keyframes @name {@frames();}
|
||||
@-o-keyframes @name {@frames();}
|
||||
@keyframes @name {@frames();}
|
||||
}
|
||||
|
||||
.createAnimation(@name, @duration : @defaultDuration, @easing : @defaultEasing){
|
||||
-webkit-animation-name: @name;
|
||||
-moz-animation-name: @name;
|
||||
-ms-animation-name: @name;
|
||||
animation-name: @name;
|
||||
-webkit-animation-duration: @duration;
|
||||
-moz-animation-duration: @duration;
|
||||
-ms-animation-duration: @duration;
|
||||
animation-duration: @duration;
|
||||
-webkit-animation-timing-function: @easing;
|
||||
-moz-animation-timing-function: @easing;
|
||||
-ms-animation-timing-function: @easing;
|
||||
animation-timing-function: @easing;
|
||||
.createAnimation(@name, @duration : @defaultDuration, @easing : @defaultEasing) {
|
||||
-webkit-animation-name : @name;
|
||||
-moz-animation-name : @name;
|
||||
-ms-animation-name : @name;
|
||||
animation-name : @name;
|
||||
-webkit-animation-duration : @duration;
|
||||
-moz-animation-duration : @duration;
|
||||
-ms-animation-duration : @duration;
|
||||
animation-duration : @duration;
|
||||
-webkit-animation-timing-function : @easing;
|
||||
-moz-animation-timing-function : @easing;
|
||||
-ms-animation-timing-function : @easing;
|
||||
animation-timing-function : @easing;
|
||||
}
|
||||
|
||||
|
||||
@@ -132,82 +132,82 @@
|
||||
Standard Animations
|
||||
****************************/
|
||||
|
||||
.fadeIn(@duration : @defaultDuration, @easing : @defaultEasing){
|
||||
.fadeIn(@duration : @defaultDuration, @easing : @defaultEasing) {
|
||||
.createAnimation(fadeIn; @duration; @easing);
|
||||
.createFrames(fadeIn,
|
||||
{ opacity : 0; },
|
||||
{ opacity : 0; },
|
||||
{ opacity : 1; }
|
||||
);
|
||||
}
|
||||
|
||||
.fadeInDown(@duration : @defaultDuration, @easing : @defaultEasing){
|
||||
.fadeInDown(@duration : @defaultDuration, @easing : @defaultEasing) {
|
||||
.createAnimation(fadeInDown; @duration; @easing);
|
||||
.createFrames(fadeInDown,
|
||||
{ opacity : 0; .transform(translateY(20px));},
|
||||
{ opacity : 0; .transform(translateY(20px));},
|
||||
{ opacity : 1; .transform(translateY(0px));}
|
||||
);
|
||||
}
|
||||
|
||||
.fadeInTop(@duration : @defaultDuration, @easing : @defaultEasing){
|
||||
.fadeInTop(@duration : @defaultDuration, @easing : @defaultEasing) {
|
||||
.createAnimation(fadeInTop; @duration; @easing);
|
||||
.createFrames(fadeInTop,
|
||||
{ opacity : 0; .transform(translateY(-20px)); },
|
||||
{ opacity : 0; .transform(translateY(-20px)); },
|
||||
{ opacity : 1; .transform(translateY(0px));}
|
||||
);
|
||||
}
|
||||
|
||||
.fadeInLeft(@duration : @defaultDuration, @easing : @defaultEasing){
|
||||
.fadeInLeft(@duration : @defaultDuration, @easing : @defaultEasing) {
|
||||
.createAnimation(fadeInLeft; @duration; @easing);
|
||||
.createFrames(fadeInLeft,
|
||||
{ opacity: 0; .transform(translateX(-20px));},
|
||||
{ opacity: 0; .transform(translateX(-20px));},
|
||||
{ opacity: 1; .transform(translateX(0));}
|
||||
);
|
||||
}
|
||||
|
||||
.fadeInRight(@duration : @defaultDuration, @easing : @defaultEasing){
|
||||
.fadeInRight(@duration : @defaultDuration, @easing : @defaultEasing) {
|
||||
.createAnimation(fadeInRight; @duration; @easing);
|
||||
.createFrames(fadeInRight,
|
||||
{ opacity: 0; .transform(translateX(20px));},
|
||||
{ opacity: 0; .transform(translateX(20px));},
|
||||
{ opacity: 1; .transform(translateX(0));}
|
||||
);
|
||||
}
|
||||
|
||||
.fadeOut(@duration : @defaultDuration, @easing : @defaultEasing){
|
||||
.fadeOut(@duration : @defaultDuration, @easing : @defaultEasing) {
|
||||
.createAnimation(fadeOut; @duration; @easing);
|
||||
.createFrames(fadeOut,
|
||||
{ opacity : 1; },
|
||||
{ opacity : 1; },
|
||||
{ opacity : 0; }
|
||||
);
|
||||
}
|
||||
|
||||
.fadeOutDown(@duration : @defaultDuration, @easing : @defaultEasing){
|
||||
.fadeOutDown(@duration : @defaultDuration, @easing : @defaultEasing) {
|
||||
.createAnimation(fadeOutDown; @duration; @easing);
|
||||
.createFrames(fadeOutDown,
|
||||
{ opacity : 1; .transform(translateY(0)); visibility: visible;},
|
||||
{ opacity : 1; .transform(translateY(0)); visibility: visible;},
|
||||
{ opacity : 0; .transform(translateY(20px)); visibility: hidden;}
|
||||
);
|
||||
}
|
||||
|
||||
.fadeOutTop(@duration : @defaultDuration, @easing : @defaultEasing){
|
||||
.fadeOutTop(@duration : @defaultDuration, @easing : @defaultEasing) {
|
||||
.createAnimation(fadeOutTop; @duration; @easing);
|
||||
.createFrames(fadeOutTop,
|
||||
{ opacity : 1; .transform(translateY(0)); },
|
||||
{ opacity : 1; .transform(translateY(0)); },
|
||||
{ opacity : 0; .transform(translateY(-20px)); }
|
||||
);
|
||||
}
|
||||
|
||||
.fadeOutLeft(@duration : @defaultDuration, @easing : @defaultEasing){
|
||||
.fadeOutLeft(@duration : @defaultDuration, @easing : @defaultEasing) {
|
||||
.createAnimation(fadeOutLeft; @duration; @easing);
|
||||
.createFrames(fadeOutLeft,
|
||||
{ opacity : 1; .transform(translateX(0));},
|
||||
{ opacity : 1; .transform(translateX(0));},
|
||||
{ opacity : 0; .transform(translateX(-20px));}
|
||||
);
|
||||
}
|
||||
|
||||
.fadeOutRight(@duration : @defaultDuration, @easing : @defaultEasing){
|
||||
.fadeOutRight(@duration : @defaultDuration, @easing : @defaultEasing) {
|
||||
.createAnimation(fadeOutRight; @duration; @easing);
|
||||
.createFrames(fadeOutRight,
|
||||
{ opacity : 1; .transform(translateX(0));},
|
||||
{ opacity : 1; .transform(translateX(0));},
|
||||
{ opacity : 0; .transform(translateX(20px));}
|
||||
);
|
||||
}
|
||||
@@ -219,50 +219,50 @@
|
||||
Fun Animations
|
||||
****************************/
|
||||
|
||||
.spin(@duration : @defaultDuration, @easing : @defaultEasing){
|
||||
.spin(@duration : @defaultDuration, @easing : @defaultEasing) {
|
||||
.createAnimation(spin, @duration, @easing);
|
||||
.spinKeyFrames(){
|
||||
.spinKeyFrames() {
|
||||
from { .transform(rotate(0deg)); }
|
||||
to { .transform(rotate(360deg)); }
|
||||
to { .transform(rotate(360deg)); }
|
||||
}
|
||||
@-webkit-keyframes spin {.spinKeyFrames();}
|
||||
@-moz-keyframes spin {.spinKeyFrames();}
|
||||
@-ms-keyframes spin {.spinKeyFrames();}
|
||||
@-o-keyframes spin {.spinKeyFrames();}
|
||||
@keyframes spin {.spinKeyFrames();}
|
||||
@-moz-keyframes spin {.spinKeyFrames();}
|
||||
@-ms-keyframes spin {.spinKeyFrames();}
|
||||
@-o-keyframes spin {.spinKeyFrames();}
|
||||
@keyframes spin {.spinKeyFrames();}
|
||||
}
|
||||
|
||||
.bounce(@duration : @defaultDuration, @easing : @defaultEasing){
|
||||
.bounce(@duration : @defaultDuration, @easing : @defaultEasing) {
|
||||
.createAnimation(bounce, @duration, @easing);
|
||||
.bounceKeyFrames(){
|
||||
.bounceKeyFrames() {
|
||||
0%, 20%, 50%, 80%, 100% { .transform(translateY(0));}
|
||||
40% { .transform(translateY(-30px));}
|
||||
60% { .transform(translateY(-15px));}
|
||||
}
|
||||
@-webkit-keyframes bounce {.bounceKeyFrames();}
|
||||
@-moz-keyframes bounce {.bounceKeyFrames();}
|
||||
@-ms-keyframes bounce {.bounceKeyFrames();}
|
||||
@-o-keyframes bounce {.bounceKeyFrames();}
|
||||
@keyframes bounce {.bounceKeyFrames();}
|
||||
@-moz-keyframes bounce {.bounceKeyFrames();}
|
||||
@-ms-keyframes bounce {.bounceKeyFrames();}
|
||||
@-o-keyframes bounce {.bounceKeyFrames();}
|
||||
@keyframes bounce {.bounceKeyFrames();}
|
||||
}
|
||||
|
||||
.pulse(@duration : @defaultDuration, @easing : @defaultEasing){
|
||||
.pulse(@duration : @defaultDuration, @easing : @defaultEasing) {
|
||||
.createAnimation(pulse, @duration, @easing);
|
||||
.pulseKeyFrames(){
|
||||
0% { .transform(scale(1));}
|
||||
50% { .transform(scale(1.4));}
|
||||
.pulseKeyFrames() {
|
||||
0% { .transform(scale(1));}
|
||||
50% { .transform(scale(1.4));}
|
||||
100% { .transform(scale(1));}
|
||||
}
|
||||
@-webkit-keyframes pulse {.pulseKeyFrames();}
|
||||
@-moz-keyframes pulse {.pulseKeyFrames();}
|
||||
@-ms-keyframes pulse {.pulseKeyFrames();}
|
||||
@-o-keyframes pulse {.pulseKeyFrames();}
|
||||
@keyframes pulse {.pulseKeyFrames();}
|
||||
@-moz-keyframes pulse {.pulseKeyFrames();}
|
||||
@-ms-keyframes pulse {.pulseKeyFrames();}
|
||||
@-o-keyframes pulse {.pulseKeyFrames();}
|
||||
@keyframes pulse {.pulseKeyFrames();}
|
||||
}
|
||||
|
||||
.rubberBand(@duration : @defaultDuration, @easing : @defaultEasing){
|
||||
.rubberBand(@duration : @defaultDuration, @easing : @defaultEasing) {
|
||||
.createAnimation(rubberBand, @duration, @easing);
|
||||
.rubberBandKeyFrames(){
|
||||
.rubberBandKeyFrames() {
|
||||
0% {.transform(scale(1));}
|
||||
30% {.transform(scaleX(1.25) scaleY(0.75));}
|
||||
40% {.transform(scaleX(0.75) scaleY(1.25));}
|
||||
@@ -270,32 +270,32 @@
|
||||
100% {.transform(scale(1));}
|
||||
}
|
||||
@-webkit-keyframes rubberBand {.rubberBandKeyFrames();}
|
||||
@-moz-keyframes rubberBand {.rubberBandKeyFrames();}
|
||||
@-ms-keyframes rubberBand {.rubberBandKeyFrames();}
|
||||
@-o-keyframes rubberBand {.rubberBandKeyFrames();}
|
||||
@keyframes rubberBand {.rubberBandKeyFrames();}
|
||||
@-moz-keyframes rubberBand {.rubberBandKeyFrames();}
|
||||
@-ms-keyframes rubberBand {.rubberBandKeyFrames();}
|
||||
@-o-keyframes rubberBand {.rubberBandKeyFrames();}
|
||||
@keyframes rubberBand {.rubberBandKeyFrames();}
|
||||
}
|
||||
|
||||
.shake(@duration : @defaultDuration, @easing : @defaultEasing){
|
||||
.shake(@duration : @defaultDuration, @easing : @defaultEasing) {
|
||||
.createAnimation(shake, @duration, @easing);
|
||||
.shakeKeyFrames(){
|
||||
.shakeKeyFrames() {
|
||||
0%, 100% {.transform( translateX(0));}
|
||||
10%, 30%, 50%, 70%, 90% {.transform( translateX(-10px));}
|
||||
20%, 40%, 60%, 80% {.transform( translateX(10px));}
|
||||
}
|
||||
@-webkit-keyframes shake {.shakeKeyFrames();}
|
||||
@-moz-keyframes shake {.shakeKeyFrames();}
|
||||
@-ms-keyframes shake {.shakeKeyFrames();}
|
||||
@-o-keyframes shake {.shakeKeyFrames();}
|
||||
@keyframes shake {.shakeKeyFrames();}
|
||||
@-moz-keyframes shake {.shakeKeyFrames();}
|
||||
@-ms-keyframes shake {.shakeKeyFrames();}
|
||||
@-o-keyframes shake {.shakeKeyFrames();}
|
||||
@keyframes shake {.shakeKeyFrames();}
|
||||
}
|
||||
|
||||
.swing(@duration : @defaultDuration, @easing : @defaultEasing){
|
||||
-webkit-transform-origin: top center;
|
||||
-ms-transform-origin: top center;
|
||||
transform-origin: top center;
|
||||
.swing(@duration : @defaultDuration, @easing : @defaultEasing) {
|
||||
-webkit-transform-origin : top center;
|
||||
-ms-transform-origin : top center;
|
||||
transform-origin : top center;
|
||||
.createAnimation(swing, @duration, @easing);
|
||||
.swingKeyFrames(){
|
||||
.swingKeyFrames() {
|
||||
20% {.transform(rotate(15deg));}
|
||||
40% {.transform(rotate(-10deg));}
|
||||
60% {.transform(rotate(5deg));}
|
||||
@@ -303,18 +303,18 @@
|
||||
100% {.transform(rotate(0deg));}
|
||||
}
|
||||
@-webkit-keyframes swing {.swingKeyFrames();}
|
||||
@-moz-keyframes swing {.swingKeyFrames();}
|
||||
@-ms-keyframes swing {.swingKeyFrames();}
|
||||
@-o-keyframes swing {.swingKeyFrames();}
|
||||
@keyframes swing {.swingKeyFrames();}
|
||||
@-moz-keyframes swing {.swingKeyFrames();}
|
||||
@-ms-keyframes swing {.swingKeyFrames();}
|
||||
@-o-keyframes swing {.swingKeyFrames();}
|
||||
@keyframes swing {.swingKeyFrames();}
|
||||
}
|
||||
|
||||
.twist(@duration : @defaultDuration, @easing : @defaultEasing){
|
||||
-webkit-transform-origin: center center;
|
||||
-ms-transform-origin: center center;
|
||||
transform-origin: center center;
|
||||
.twist(@duration : @defaultDuration, @easing : @defaultEasing) {
|
||||
-webkit-transform-origin : center center;
|
||||
-ms-transform-origin : center center;
|
||||
transform-origin : center center;
|
||||
.createAnimation(swing, @duration, @easing);
|
||||
.swingKeyFrames(){
|
||||
.swingKeyFrames() {
|
||||
20% {.transform(rotate(15deg));}
|
||||
40% {.transform(rotate(-10deg));}
|
||||
60% {.transform(rotate(5deg));}
|
||||
@@ -322,15 +322,15 @@
|
||||
100% {.transform(rotate(0deg));}
|
||||
}
|
||||
@-webkit-keyframes swing {.swingKeyFrames();}
|
||||
@-moz-keyframes swing {.swingKeyFrames();}
|
||||
@-ms-keyframes swing {.swingKeyFrames();}
|
||||
@-o-keyframes swing {.swingKeyFrames();}
|
||||
@keyframes swing {.swingKeyFrames();}
|
||||
@-moz-keyframes swing {.swingKeyFrames();}
|
||||
@-ms-keyframes swing {.swingKeyFrames();}
|
||||
@-o-keyframes swing {.swingKeyFrames();}
|
||||
@keyframes swing {.swingKeyFrames();}
|
||||
}
|
||||
|
||||
.wobble(@duration : @defaultDuration, @easing : @defaultEasing){
|
||||
.wobble(@duration : @defaultDuration, @easing : @defaultEasing) {
|
||||
.createAnimation(wobble, @duration, @easing);
|
||||
.wobbleKeyFrames(){
|
||||
.wobbleKeyFrames() {
|
||||
0% {.transform(translateX(0%));}
|
||||
15% {.transform(translateX(-25%) rotate(-5deg));}
|
||||
30% {.transform(translateX(20%) rotate(3deg));}
|
||||
@@ -340,22 +340,22 @@
|
||||
100% {.transform(translateX(0%));}
|
||||
}
|
||||
@-webkit-keyframes wobble {.wobbleKeyFrames();}
|
||||
@-moz-keyframes wobble {.wobbleKeyFrames();}
|
||||
@-ms-keyframes wobble {.wobbleKeyFrames();}
|
||||
@-o-keyframes wobble {.wobbleKeyFrames();}
|
||||
@keyframes wobble {.wobbleKeyFrames();}
|
||||
@-moz-keyframes wobble {.wobbleKeyFrames();}
|
||||
@-ms-keyframes wobble {.wobbleKeyFrames();}
|
||||
@-o-keyframes wobble {.wobbleKeyFrames();}
|
||||
@keyframes wobble {.wobbleKeyFrames();}
|
||||
}
|
||||
|
||||
.popIn(@duration : @defaultDuration, @easing : @defaultEasing){
|
||||
.popIn(@duration : @defaultDuration, @easing : @defaultEasing) {
|
||||
.createAnimation(popIn, @duration, @easing);
|
||||
.popInKeyFrames(){
|
||||
0% { .transform(scale(0));}
|
||||
70% { .transform(scale(1.4));}
|
||||
.popInKeyFrames() {
|
||||
0% { .transform(scale(0));}
|
||||
70% { .transform(scale(1.4));}
|
||||
100% { .transform(scale(1));}
|
||||
}
|
||||
@-webkit-keyframes popIn {.popInKeyFrames();}
|
||||
@-moz-keyframes popIn {.popInKeyFrames();}
|
||||
@-ms-keyframes popIn {.popInKeyFrames();}
|
||||
@-o-keyframes popIn {.popInKeyFrames();}
|
||||
@keyframes popIn {.popInKeyFrames();}
|
||||
@-moz-keyframes popIn {.popInKeyFrames();}
|
||||
@-ms-keyframes popIn {.popInKeyFrames();}
|
||||
@-o-keyframes popIn {.popInKeyFrames();}
|
||||
@keyframes popIn {.popInKeyFrames();}
|
||||
}
|
||||
|
||||
@@ -23,47 +23,47 @@
|
||||
@grey : #7F8C8D;
|
||||
|
||||
#backgroundColors {
|
||||
&.tealLight{ background-color : @tealLight };
|
||||
&.teal{ background-color : @teal };
|
||||
&.greenLight{ background-color : @greenLight };
|
||||
&.green{ background-color : @green };
|
||||
&.blueLight{ background-color : @blueLight };
|
||||
&.blue{ background-color : @blue };
|
||||
&.purpleLight{ background-color : @purpleLight };
|
||||
&.purple{ background-color : @purple };
|
||||
&.steelLight{ background-color : @steelLight };
|
||||
&.steel{ background-color : @steel };
|
||||
&.yellowLight{ background-color : @yellowLight };
|
||||
&.yellow{ background-color : @yellow };
|
||||
&.orangeLight{ background-color : @orangeLight };
|
||||
&.orange{ background-color : @orange };
|
||||
&.redLight{ background-color : @redLight };
|
||||
&.red{ background-color : @red };
|
||||
&.silverLight{ background-color : @silverLight };
|
||||
&.silver{ background-color : @silver };
|
||||
&.greyLight{ background-color : @greyLight };
|
||||
&.grey{ background-color : @grey };
|
||||
&.tealLight { background-color : @tealLight; };
|
||||
&.teal { background-color : @teal; };
|
||||
&.greenLight { background-color : @greenLight; };
|
||||
&.green { background-color : @green; };
|
||||
&.blueLight { background-color : @blueLight; };
|
||||
&.blue { background-color : @blue; };
|
||||
&.purpleLight { background-color : @purpleLight; };
|
||||
&.purple { background-color : @purple; };
|
||||
&.steelLight { background-color : @steelLight; };
|
||||
&.steel { background-color : @steel; };
|
||||
&.yellowLight { background-color : @yellowLight; };
|
||||
&.yellow { background-color : @yellow; };
|
||||
&.orangeLight { background-color : @orangeLight; };
|
||||
&.orange { background-color : @orange; };
|
||||
&.redLight { background-color : @redLight; };
|
||||
&.red { background-color : @red; };
|
||||
&.silverLight { background-color : @silverLight; };
|
||||
&.silver { background-color : @silver; };
|
||||
&.greyLight { background-color : @greyLight; };
|
||||
&.grey { background-color : @grey; };
|
||||
}
|
||||
|
||||
#backgroundColorsHover {
|
||||
&.tealLight:hover{ background-color : @tealLight };
|
||||
&.teal:hover{ background-color : @teal };
|
||||
&.greenLight:hover{ background-color : @greenLight };
|
||||
&.green:hover{ background-color : @green };
|
||||
&.blueLight:hover{ background-color : @blueLight };
|
||||
&.blue:hover{ background-color : @blue };
|
||||
&.purpleLight:hover{ background-color : @purpleLight };
|
||||
&.purple:hover{ background-color : @purple };
|
||||
&.steelLight:hover{ background-color : @steelLight };
|
||||
&.steel:hover{ background-color : @steel };
|
||||
&.yellowLight:hover{ background-color : @yellowLight };
|
||||
&.yellow:hover{ background-color : @yellow };
|
||||
&.orangeLight:hover{ background-color : @orangeLight };
|
||||
&.orange:hover{ background-color : @orange };
|
||||
&.redLight:hover{ background-color : @redLight };
|
||||
&.red:hover{ background-color : @red };
|
||||
&.silverLight:hover{ background-color : @silverLight };
|
||||
&.silver:hover{ background-color : @silver };
|
||||
&.greyLight:hover{ background-color : @greyLight };
|
||||
&.grey:hover{ background-color : @grey };
|
||||
&.tealLight:hover { background-color : @tealLight; };
|
||||
&.teal:hover { background-color : @teal; };
|
||||
&.greenLight:hover { background-color : @greenLight; };
|
||||
&.green:hover { background-color : @green; };
|
||||
&.blueLight:hover { background-color : @blueLight; };
|
||||
&.blue:hover { background-color : @blue; };
|
||||
&.purpleLight:hover { background-color : @purpleLight; };
|
||||
&.purple:hover { background-color : @purple; };
|
||||
&.steelLight:hover { background-color : @steelLight; };
|
||||
&.steel:hover { background-color : @steel; };
|
||||
&.yellowLight:hover { background-color : @yellowLight; };
|
||||
&.yellow:hover { background-color : @yellow; };
|
||||
&.orangeLight:hover { background-color : @orangeLight; };
|
||||
&.orange:hover { background-color : @orange; };
|
||||
&.redLight:hover { background-color : @redLight; };
|
||||
&.red:hover { background-color : @red; };
|
||||
&.silverLight:hover { background-color : @silverLight; };
|
||||
&.silver:hover { background-color : @silver; };
|
||||
&.greyLight:hover { background-color : @greyLight; };
|
||||
&.grey:hover { background-color : @grey; };
|
||||
}
|
||||
@@ -12,37 +12,31 @@
|
||||
font-family : 'CodeBold';
|
||||
src : data-uri('naturalcrit/styles/CODE Bold.otf') format('opentype');
|
||||
}
|
||||
html,body, #reactRoot{
|
||||
html,body, #reactRoot {
|
||||
height : 100vh;
|
||||
min-height : 100vh;
|
||||
margin : 0;
|
||||
font-family : 'Open Sans', sans-serif;
|
||||
}
|
||||
*{
|
||||
box-sizing : border-box;
|
||||
}
|
||||
.colorButton(@backgroundColor : @green){
|
||||
* { box-sizing : border-box; }
|
||||
.colorButton(@backgroundColor : @green) {
|
||||
.animate(background-color);
|
||||
display : inline-block;
|
||||
padding : 0.6em 1.2em;
|
||||
cursor : pointer;
|
||||
background-color : @backgroundColor;
|
||||
font-family : 'Open Sans', sans-serif;
|
||||
font-size : 0.8em;
|
||||
font-weight : 800;
|
||||
color : white;
|
||||
text-decoration : none;
|
||||
text-transform : uppercase;
|
||||
border : none;
|
||||
text-decoration : none;
|
||||
cursor : pointer;
|
||||
outline : none;
|
||||
&:hover{
|
||||
background-color : darken(@backgroundColor, 5%);
|
||||
}
|
||||
&:active{
|
||||
background-color : darken(@backgroundColor, 10%);
|
||||
}
|
||||
&:disabled{
|
||||
background-color : @backgroundColor;
|
||||
border : none;
|
||||
&:hover { background-color : darken(@backgroundColor, 5%); }
|
||||
&:active { background-color : darken(@backgroundColor, 10%); }
|
||||
&:disabled {
|
||||
cursor : not-allowed;
|
||||
background-color : @silver !important;
|
||||
cursor:not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,86 +1,76 @@
|
||||
|
||||
@containerWidth : 1000px;
|
||||
|
||||
html, body{
|
||||
html, body {
|
||||
position : relative;
|
||||
height : 100%;
|
||||
min-height : 100%;
|
||||
background-color : #eee;
|
||||
font-family : 'Lato', sans-serif;
|
||||
color : @copyGrey;
|
||||
background-color : #EEEEEE;
|
||||
}
|
||||
.container{
|
||||
.container {
|
||||
position : relative;
|
||||
max-width : @containerWidth;
|
||||
margin : 0 auto;
|
||||
padding-right : 20px;
|
||||
padding-left : 20px;
|
||||
margin : 0 auto;
|
||||
}
|
||||
h1{
|
||||
h1 {
|
||||
margin-top : 10px;
|
||||
margin-bottom : 15px;
|
||||
font-size : 2em;
|
||||
}
|
||||
h2{
|
||||
h2 {
|
||||
margin-top : 10px;
|
||||
margin-bottom : 15px;
|
||||
font-size : 1.5em;
|
||||
font-weight : 900;
|
||||
}
|
||||
h3{
|
||||
h3 {
|
||||
margin-top : 5px;
|
||||
margin-bottom : 7px;
|
||||
font-size : 1em;
|
||||
font-weight : 900;
|
||||
}
|
||||
p{
|
||||
p {
|
||||
margin-bottom : 1em;
|
||||
font-size : 16px;
|
||||
color : @copyGrey;
|
||||
line-height : 1.5em;
|
||||
color : @copyGrey;
|
||||
}
|
||||
code{
|
||||
background-color : #F8F8F8;
|
||||
font-family : 'Courier', mono;
|
||||
code {
|
||||
font-family : 'Courier', "mono";
|
||||
color : black;
|
||||
white-space : pre;
|
||||
background-color : #F8F8F8;
|
||||
}
|
||||
a{
|
||||
color : inherit;
|
||||
}
|
||||
strong{
|
||||
font-weight : bold;
|
||||
}
|
||||
button{
|
||||
a { color : inherit; }
|
||||
strong { font-weight : bold; }
|
||||
button {
|
||||
.button();
|
||||
}
|
||||
.button(@backgroundColor : @green){
|
||||
.button(@backgroundColor : @green) {
|
||||
.animate(background-color);
|
||||
display : inline-block;
|
||||
padding : 0.6em 1.2em;
|
||||
cursor : pointer;
|
||||
background-color : @backgroundColor;
|
||||
font-family : "Lato", Helvetica, Arial, sans-serif;
|
||||
font-family : 'Lato', "Helvetica", "Arial", sans-serif;
|
||||
font-size : 15px;
|
||||
color : white;
|
||||
text-decoration : none;
|
||||
border : none;
|
||||
outline : none;
|
||||
&:hover{
|
||||
background-color : darken(@backgroundColor, 5%);
|
||||
}
|
||||
&:active{
|
||||
background-color : darken(@backgroundColor, 10%);
|
||||
}
|
||||
&:disabled{
|
||||
background-color : @silver !important;
|
||||
}
|
||||
}
|
||||
.iconButton(@backgroundColor : @green){
|
||||
padding : 0.6em;
|
||||
cursor : pointer;
|
||||
outline : none;
|
||||
background-color : @backgroundColor;
|
||||
border : none;
|
||||
&:hover { background-color : darken(@backgroundColor, 5%); }
|
||||
&:active { background-color : darken(@backgroundColor, 10%); }
|
||||
&:disabled { background-color : @silver !important; }
|
||||
}
|
||||
.iconButton(@backgroundColor : @green) {
|
||||
padding : 0.6em;
|
||||
font-size : 14px;
|
||||
color : white;
|
||||
text-align : center;
|
||||
cursor : pointer;
|
||||
background-color : @backgroundColor;
|
||||
}
|
||||
@@ -1,33 +1,23 @@
|
||||
:where(html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,button,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,embed,figure,figcaption,footer,header,hgroup,menu,nav,output,ruby,section,summary,time,mark,audio,video){
|
||||
border:0;font-size:100%;font:inherit;vertical-align:baseline;margin:0;padding:0
|
||||
:where(html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,button,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,embed,figure,figcaption,footer,header,hgroup,menu,nav,output,ruby,section,summary,time,mark,audio,video) {padding : 0;margin : 0;font : inherit;font-size : 100%;vertical-align : baseline;
|
||||
border : 0;
|
||||
}
|
||||
|
||||
:where(article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section){
|
||||
display:block
|
||||
}
|
||||
:where(article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section) { display : block; }
|
||||
|
||||
:where(body){
|
||||
line-height:1
|
||||
}
|
||||
:where(body) { line-height : 1; }
|
||||
|
||||
:where(ol,ul){
|
||||
list-style:none
|
||||
}
|
||||
:where(ol,ul) { list-style : none; }
|
||||
|
||||
:where(blockquote,q){
|
||||
quotes:none
|
||||
}
|
||||
:where(blockquote,q) { quotes : none; }
|
||||
|
||||
:where(blockquote:before,blockquote:after,q:before,q:after){
|
||||
content:none
|
||||
}
|
||||
:where(blockquote::before,blockquote::after,q::before,q::after) { content : none; }
|
||||
|
||||
:where(table){
|
||||
border-collapse:collapse;border-spacing:0
|
||||
:where(table) {border-spacing : 0;
|
||||
border-collapse : collapse;
|
||||
}
|
||||
|
||||
:where(button) {
|
||||
background-color: unset;
|
||||
text-transform: unset;
|
||||
color: unset;
|
||||
color : unset;
|
||||
text-transform : unset;
|
||||
background-color : unset;
|
||||
}
|
||||
|
||||
@@ -2,116 +2,115 @@
|
||||
@tooltipColor : #383838;
|
||||
@arrowSize : 6px;
|
||||
@arrowPosition : 18px;
|
||||
[data-tooltip]{
|
||||
[data-tooltip] {
|
||||
.tooltip(attr(data-tooltip));
|
||||
}
|
||||
[data-tooltip-top]{
|
||||
[data-tooltip-top] {
|
||||
.tooltipTop(attr(data-tooltip-top));
|
||||
}
|
||||
[data-tooltip-bottom]{
|
||||
[data-tooltip-bottom] {
|
||||
.tooltipBottom(attr(data-tooltip-bottom));
|
||||
}
|
||||
[data-tooltip-left]{
|
||||
[data-tooltip-left] {
|
||||
.tooltipLeft(attr(data-tooltip-left));
|
||||
}
|
||||
[data-tooltip-right]{
|
||||
[data-tooltip-right] {
|
||||
.tooltipRight(attr(data-tooltip-right));
|
||||
}
|
||||
.tooltip(@content){
|
||||
.tooltip(@content) {
|
||||
.tooltipBottom(@content);
|
||||
}
|
||||
.tooltipTop(@content){
|
||||
.tooltipTop(@content) {
|
||||
.tooltipBase(@content);
|
||||
&:before {
|
||||
&::before {
|
||||
margin-bottom : -@arrowSize * 2;
|
||||
border-top-color : @tooltipColor;
|
||||
}
|
||||
&:after{ margin-left: -18px; }
|
||||
&:before, &:after{
|
||||
&::after { margin-left : -18px; }
|
||||
&::before, &::after {
|
||||
bottom : 100%;
|
||||
left : 50%;
|
||||
}
|
||||
&:hover:after, &:hover:before, &:focus:after, &:focus:before {
|
||||
&:hover::after, &:hover::before, &:focus::after, &:focus::before {
|
||||
.transform(translateY(-(@arrowSize + 2)));
|
||||
}
|
||||
}
|
||||
.tooltipBottom(@content){
|
||||
.tooltipBottom(@content) {
|
||||
.tooltipBase(@content);
|
||||
&:before {
|
||||
&::before {
|
||||
margin-top : -@arrowSize * 2;
|
||||
border-bottom-color : @tooltipColor;
|
||||
}
|
||||
&:after{ margin-left: -18px; }
|
||||
&:before, &:after{
|
||||
&::after { margin-left : -18px; }
|
||||
&::before, &::after {
|
||||
top : 100%;
|
||||
left : 50%;
|
||||
}
|
||||
&:hover:after, &:hover:before, &:focus:after, &:focus:before {
|
||||
&:hover::after, &:hover::before, &:focus::after, &:focus::before {
|
||||
.transform(translateY(@arrowSize + 2));
|
||||
}
|
||||
}
|
||||
.tooltipLeft(@content){
|
||||
.tooltipLeft(@content) {
|
||||
.tooltipBase(@content);
|
||||
&:before {
|
||||
&::before {
|
||||
margin-right : -@arrowSize * 2;
|
||||
margin-bottom : -@arrowSize;
|
||||
border-left-color : @tooltipColor;
|
||||
}
|
||||
&:after{ margin-bottom: -14px;}
|
||||
&:before, &:after {
|
||||
&::after { margin-bottom : -14px;}
|
||||
&::before, &::after {
|
||||
right : 100%;
|
||||
bottom : 50%;
|
||||
}
|
||||
&:hover:after, &:hover:before, &:focus:after, &:focus:before {
|
||||
&:hover::after, &:hover::before, &:focus::after, &:focus::before {
|
||||
.transform(translateX(-(@arrowSize + 2)));
|
||||
}
|
||||
}
|
||||
.tooltipRight(@content){
|
||||
.tooltipRight(@content) {
|
||||
.tooltipBase(@content);
|
||||
&:before {
|
||||
&::before {
|
||||
margin-bottom : -@arrowSize;
|
||||
margin-left : -@arrowSize * 2;
|
||||
border-right-color : @tooltipColor;
|
||||
}
|
||||
&:after{ margin-bottom: -14px;}
|
||||
&:before, &:after {
|
||||
&::after { margin-bottom : -14px;}
|
||||
&::before, &::after {
|
||||
bottom : 50%;
|
||||
left : 100%;
|
||||
}
|
||||
&:hover:after, &:hover:before, &:focus:after, &:focus:before {
|
||||
&:hover::after, &:hover::before, &:focus::after, &:focus::before {
|
||||
.transform(translateX(@arrowSize + 2));
|
||||
}
|
||||
}
|
||||
.tooltipShow(){
|
||||
}
|
||||
.tooltipBase(@content){
|
||||
.tooltipShow(){ }
|
||||
.tooltipBase(@content) {
|
||||
//position: relative;
|
||||
&:before, &:after{
|
||||
&::before, &::after {
|
||||
.animateAll();
|
||||
position : absolute;
|
||||
z-index : 1000000;
|
||||
opacity : 0;
|
||||
pointer-events : none;
|
||||
opacity : 0;
|
||||
}
|
||||
//Arrow
|
||||
&:before{
|
||||
content : '';
|
||||
&::before {
|
||||
z-index : 1000001;
|
||||
content : '';
|
||||
background : transparent;
|
||||
border : @arrowSize solid transparent;
|
||||
}
|
||||
//Box
|
||||
&:after{
|
||||
content : @content;
|
||||
&::after {
|
||||
visibility : hidden;
|
||||
padding : 8px 10px;
|
||||
background : @tooltipColor;
|
||||
font-size : 12px;
|
||||
color : white;
|
||||
line-height : 12px;
|
||||
color : white;
|
||||
white-space : nowrap;
|
||||
content : @content;
|
||||
background : @tooltipColor;
|
||||
}
|
||||
&:hover:before, &:hover:after {
|
||||
&:hover::before, &:hover::after {
|
||||
visibility : visible;
|
||||
opacity : 1;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,17 @@ require('jsdom-global')();
|
||||
|
||||
import { safeHTML } from '../../client/homebrew/brewRenderer/safeHTML';
|
||||
|
||||
test('Exit if no document', function() {
|
||||
const doc = document;
|
||||
document = undefined;
|
||||
|
||||
const result = safeHTML('');
|
||||
|
||||
document = doc;
|
||||
|
||||
expect(result).toBe(null);
|
||||
});
|
||||
|
||||
test('Javascript via href', function() {
|
||||
const source = `<a href="javascript:alert('This is a JavaScript injection via href attribute')">Click me</a>`;
|
||||
const rendered = safeHTML(source);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* eslint-disable max-lines */
|
||||
|
||||
|
||||
import Markdown from 'naturalcrit/markdown.js';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* eslint-disable max-lines */
|
||||
|
||||
|
||||
import Markdown from 'naturalcrit/markdown.js';
|
||||
|
||||
@@ -92,12 +92,12 @@ describe('Multiline Definition Lists', ()=>{
|
||||
test('Multiline Definition Term must have at least one non-empty Definition', function() {
|
||||
const source = 'Term 1\n::';
|
||||
const rendered = Markdown.render(source).trim();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p>Term 1</p>\n<br>\n<br>`);
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p>Term 1</p>\n<div class='blank'></div>\n<div class='blank'></div>`);
|
||||
});
|
||||
|
||||
test('Multiline Definition List must have at least one non-newline character after ::', function() {
|
||||
const source = 'Term 1\n::\nDefinition 1\n\n';
|
||||
const rendered = Markdown.render(source).trim();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p>Term 1</p>\n<br>\n<br>\n<p>Definition 1</p>`);
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p>Term 1</p>\n<div class='blank'></div>\n<div class='blank'></div>\n<p>Definition 1</p>`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* eslint-disable max-lines */
|
||||
|
||||
|
||||
import Markdown from 'naturalcrit/markdown.js';
|
||||
|
||||
@@ -6,37 +6,37 @@ describe('Hard Breaks', ()=>{
|
||||
test('Single Break', function() {
|
||||
const source = ':\n\n';
|
||||
const rendered = Markdown.render(source).trim();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<br>`);
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class='blank'></div>`);
|
||||
});
|
||||
|
||||
test('Double Break', function() {
|
||||
const source = '::\n\n';
|
||||
const rendered = Markdown.render(source).trim();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<br>\n<br>`);
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class='blank'></div>\n<div class='blank'></div>`);
|
||||
});
|
||||
|
||||
test('Triple Break', function() {
|
||||
const source = ':::\n\n';
|
||||
const rendered = Markdown.render(source).trim();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<br>\n<br>\n<br>`);
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class='blank'></div>\n<div class='blank'></div>\n<div class='blank'></div>`);
|
||||
});
|
||||
|
||||
test('Many Break', function() {
|
||||
const source = '::::::::::\n\n';
|
||||
const rendered = Markdown.render(source).trim();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<br>\n<br>\n<br>\n<br>\n<br>\n<br>\n<br>\n<br>\n<br>\n<br>`);
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class='blank'></div>\n<div class='blank'></div>\n<div class='blank'></div>\n<div class='blank'></div>\n<div class='blank'></div>\n<div class='blank'></div>\n<div class='blank'></div>\n<div class='blank'></div>\n<div class='blank'></div>\n<div class='blank'></div>`);
|
||||
});
|
||||
|
||||
test('Multiple sets of Breaks', function() {
|
||||
const source = ':::\n:::\n:::';
|
||||
const rendered = Markdown.render(source).trim();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<br>\n<br>\n<br>\n<br>\n<br>\n<br>\n<br>\n<br>\n<br>`);
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class='blank'></div>\n<div class='blank'></div>\n<div class='blank'></div>\n<div class='blank'></div>\n<div class='blank'></div>\n<div class='blank'></div>\n<div class='blank'></div>\n<div class='blank'></div>\n<div class='blank'></div>`);
|
||||
});
|
||||
|
||||
test('Break directly between two paragraphs', function() {
|
||||
const source = 'Line 1\n::\nLine 2';
|
||||
const rendered = Markdown.render(source).trim();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p>Line 1</p>\n<br>\n<br>\n<p>Line 2</p>`);
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p>Line 1</p>\n<div class='blank'></div>\n<div class='blank'></div>\n<p>Line 2</p>`);
|
||||
});
|
||||
|
||||
test('Ignored inside a code block', function() {
|
||||
|
||||
@@ -1,72 +1,24 @@
|
||||
/* eslint-disable max-lines */
|
||||
|
||||
|
||||
import Markdown from 'naturalcrit/markdown.js';
|
||||
|
||||
describe('Non-Breaking Spaces', ()=>{
|
||||
test('Single Space', function() {
|
||||
const source = ':>\n\n';
|
||||
const rendered = Markdown.render(source).trim();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p> </p>`);
|
||||
});
|
||||
|
||||
test('Double Space', function() {
|
||||
const source = ':>>\n\n';
|
||||
const rendered = Markdown.render(source).trim();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p> </p>`);
|
||||
});
|
||||
|
||||
test('Triple Space', function() {
|
||||
const source = ':>>>\n\n';
|
||||
const rendered = Markdown.render(source).trim();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p> </p>`);
|
||||
});
|
||||
|
||||
test('Many Space', function() {
|
||||
const source = ':>>>>>>>>>>\n\n';
|
||||
const rendered = Markdown.render(source).trim();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p> </p>`);
|
||||
});
|
||||
|
||||
test('Multiple sets of Spaces', function() {
|
||||
const source = ':>>>\n:>>>\n:>>>';
|
||||
const rendered = Markdown.render(source).trim();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p> \n \n </p>`);
|
||||
});
|
||||
|
||||
test('Pair of inline Spaces', function() {
|
||||
const source = ':>>:>>';
|
||||
const rendered = Markdown.render(source).trim();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p> </p>`);
|
||||
});
|
||||
|
||||
test('Space directly between two paragraphs', function() {
|
||||
const source = 'Line 1\n:>>\nLine 2';
|
||||
const rendered = Markdown.render(source).trim();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p>Line 1\n \nLine 2</p>`);
|
||||
});
|
||||
|
||||
test('Ignored inside a code block', function() {
|
||||
const source = '```\n\n:>\n\n```\n';
|
||||
const rendered = Markdown.render(source).trim();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<pre><code>\n:>\n</code></pre>`);
|
||||
});
|
||||
|
||||
describe('Non-Breaking Spaces Interactions', ()=>{
|
||||
test('I am actually a single-line definition list!', function() {
|
||||
const source = 'Term ::> Definition 1\n';
|
||||
const rendered = Markdown.render(source).trim();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<dl><dt>Term</dt><dd>> Definition 1</dd>\n</dl>`);
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<dl><dt>Term</dt><dd>> Definition 1</dd>\n</dl>`);
|
||||
});
|
||||
|
||||
test('I am actually a definition list!', function() {
|
||||
const source = 'Term\n::> Definition 1\n';
|
||||
const rendered = Markdown.render(source).trim();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<dl><dt>Term</dt>\n<dd>> Definition 1</dd></dl>`);
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<dl><dt>Term</dt>\n<dd>> Definition 1</dd></dl>`);
|
||||
});
|
||||
|
||||
test('I am actually a two-term definition list!', function() {
|
||||
const source = 'Term\n::> Definition 1\n::>> Definition 2';
|
||||
const rendered = Markdown.render(source).trim();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<dl><dt>Term</dt>\n<dd>> Definition 1</dd>\n<dd>>> Definition 2</dd></dl>`);
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<dl><dt>Term</dt>\n<dd>> Definition 1</dd>\n<dd>>> Definition 2</dd></dl>`);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* eslint-disable max-lines */
|
||||
|
||||
|
||||
import Markdown from 'naturalcrit/markdown.js';
|
||||
|
||||
|
||||
@@ -370,6 +370,30 @@ describe('Cross-page variables', ()=>{
|
||||
const rendered = renderAllPages([source0, source1]).join('\n\\page\n').trimReturns();
|
||||
expect(rendered, `Input:\n${[source0, source1].join('\n\\page\n')}`, { showPrefix: false }).toBe('<p>two</p><p>one</p>\\page<p>two</p>');
|
||||
});
|
||||
|
||||
it('Page numbering across pages : default', function() {
|
||||
const source0 = `$[HB_pageNumber]\n\n`;
|
||||
const source1 = `$[HB_pageNumber]\n\n`;
|
||||
renderAllPages([source0, source1]).join('\n\\page\n').trimReturns(); //Requires one full render of document before hoisting is picked up
|
||||
const rendered = renderAllPages([source0, source1]).join('\n\\page\n').trimReturns();
|
||||
expect(rendered, `Input:\n${[source0, source1].join('\n\\page\n')}`, { showPrefix: false }).toBe('<p>1</p>\\page<p>2</p>');
|
||||
});
|
||||
|
||||
it('Page numbering across pages : custom page number (Number)', function() {
|
||||
const source0 = `[HB_pageNumber]:100\n\n$[HB_pageNumber]\n\n`;
|
||||
const source1 = `$[HB_pageNumber]\n\n`;
|
||||
renderAllPages([source0, source1]).join('\n\\page\n').trimReturns(); //Requires one full render of document before hoisting is picked up
|
||||
const rendered = renderAllPages([source0, source1]).join('\n\\page\n').trimReturns();
|
||||
expect(rendered, `Input:\n${[source0, source1].join('\n\\page\n')}`, { showPrefix: false }).toBe('<p>100</p>\\page<p>101</p>');
|
||||
});
|
||||
|
||||
it('Page numbering across pages : custom page number (NaN)', function() {
|
||||
const source0 = `[HB_pageNumber]:a\n\n$[HB_pageNumber]\n\n`;
|
||||
const source1 = `$[HB_pageNumber]\n\n`;
|
||||
renderAllPages([source0, source1]).join('\n\\page\n').trimReturns(); //Requires one full render of document before hoisting is picked up
|
||||
const rendered = renderAllPages([source0, source1]).join('\n\\page\n').trimReturns();
|
||||
expect(rendered, `Input:\n${[source0, source1].join('\n\\page\n')}`, { showPrefix: false }).toBe('<p>a</p>\\page<p>a</p>');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Math function parameter handling', ()=>{
|
||||
@@ -410,4 +434,102 @@ describe('Regression Tests', ()=>{
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered).toBe('<table><thead><tr><th>title 1</th><th>title 2</th><th>title 3</th><th>title 4</th></tr></thead><tbody><tr><td><a href=\"bar\">foo</a></td><td>Ipsum</td><td>)</td><td>)</td></tr></tbody></table>');
|
||||
});
|
||||
|
||||
it('Handle Extra spaces in image alt-text 1', function(){
|
||||
const source='';
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered).toBe('<p><img src=\"http://i.imgur.com/hMna6G0.png\" alt=\"where is my image??\" style=\"--HB_src:url(http://i.imgur.com/hMna6G0.png);\"></p>');
|
||||
});
|
||||
|
||||
it('Handle Extra spaces in image alt-text 2', function(){
|
||||
const source='';
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered).toBe('<p><img src=\"http://i.imgur.com/hMna6G0.png\" alt=\"where is my image??\" style=\"--HB_src:url(http://i.imgur.com/hMna6G0.png);\"></p>');
|
||||
});
|
||||
|
||||
it('Handle Extra spaces in image alt-text 3', function(){
|
||||
const source='';
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered).toBe('<p><img src=\"http://i.imgur.com/hMna6G0.png\" alt=\"where is my image??\" style=\"--HB_src:url(http://i.imgur.com/hMna6G0.png);\"></p>');
|
||||
});
|
||||
|
||||
it('Handle Extra spaces in image alt-text 4', function(){
|
||||
const source='{height=20%,width=20%}';
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered).toBe('<p><img style=\"--HB_src:url(http://i.imgur.com/hMna6G0.png);\" src=\"http://i.imgur.com/hMna6G0.png\" alt=\"where is my image??\" height=\"20%\" width=\"20%\"></p>');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Custom Math Function Tests', ()=>{
|
||||
it('Sign Test', function() {
|
||||
const source = `[a]: 13\n\n[b]: -11\n\nPositive: $[sign(a)]\n\nNegative: $[sign(b)]`;
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered).toBe('<p>Positive: +</p><p>Negative: -</p>');
|
||||
});
|
||||
|
||||
it('Signed Test', function() {
|
||||
const source = `[a]: 13\n\n[b]: -11\n\nPositive: $[signed(a)]\n\nNegative: $[signed(b)]`;
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered).toBe('<p>Positive: +13</p><p>Negative: -11</p>');
|
||||
});
|
||||
|
||||
it('Roman Numerals Test', function() {
|
||||
const source = `[a]: 18\n\nRoman Numeral: $[toRomans(a)]`;
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered).toBe('<p>Roman Numeral: XVIII</p>');
|
||||
});
|
||||
|
||||
it('Roman Numerals Test - Uppercase', function() {
|
||||
const source = `[a]: 18\n\nRoman Numeral: $[toRomansUpper(a)]`;
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered).toBe('<p>Roman Numeral: XVIII</p>');
|
||||
});
|
||||
|
||||
it('Roman Numerals Test - Lowercase', function() {
|
||||
const source = `[a]: 18\n\nRoman Numeral: $[toRomansLower(a)]`;
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered).toBe('<p>Roman Numeral: xviii</p>');
|
||||
});
|
||||
|
||||
it('Number to Characters Test', function() {
|
||||
const source = `[a]: 18\n\n[b]: 39\n\nCharacters: $[toChar(a)] $[toChar(b)]`;
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered).toBe('<p>Characters: R AM</p>');
|
||||
});
|
||||
|
||||
it('Number to Characters Test - Uppercase', function() {
|
||||
const source = `[a]: 18\n\n[b]: 39\n\nCharacters: $[toCharUpper(a)] $[toCharUpper(b)]`;
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered).toBe('<p>Characters: R AM</p>');
|
||||
});
|
||||
|
||||
it('Number to Characters Test - Lowercase', function() {
|
||||
const source = `[a]: 18\n\n[b]: 39\n\nCharacters: $[toCharLower(a)] $[toCharLower(b)]`;
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered).toBe('<p>Characters: r am</p>');
|
||||
});
|
||||
|
||||
it('Number to Words Test', function() {
|
||||
const source = `[a]: 80085\n\nWords: $[toWords(a)]`;
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered).toBe('<p>Words: eighty thousand and eighty-five</p>');
|
||||
});
|
||||
|
||||
it('Number to Words Test - Uppercase', function() {
|
||||
const source = `[a]: 80085\n\nWords: $[toWordsUpper(a)]`;
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered).toBe('<p>Words: EIGHTY THOUSAND AND EIGHTY-FIVE</p>');
|
||||
});
|
||||
|
||||
it('Number to Words Test - Lowercase', function() {
|
||||
const source = `[a]: 80085\n\nWords: $[toWordsLower(a)]`;
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered).toBe('<p>Words: eighty thousand and eighty-five</p>');
|
||||
});
|
||||
|
||||
it('Number to Words Test - Capitalized', function() {
|
||||
const source = `[a]: 80085\n\nWords: $[toWordsCaps(a)]`;
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered).toBe('<p>Words: Eighty Thousand And Eighty-Five</p>');
|
||||
});
|
||||
});
|
||||
@@ -9,26 +9,22 @@
|
||||
@headerText : #58180D; // Dark maroon
|
||||
@monsterStatBackground : #FDF1DC; // Lighter parchment
|
||||
@captionText : #766649; // Brown
|
||||
@page { margin: 0; }
|
||||
body {
|
||||
counter-reset : phb-page-numbers;
|
||||
}
|
||||
*{
|
||||
-webkit-print-color-adjust : exact;
|
||||
}
|
||||
.useSansSerif(){
|
||||
font-family : ScalySans;
|
||||
em{
|
||||
font-family : ScalySans;
|
||||
@page { margin : 0; }
|
||||
body { counter-reset : phb-page-numbers; }
|
||||
* { -webkit-print-color-adjust : exact; }
|
||||
.useSansSerif() {
|
||||
font-family : 'ScalySans';
|
||||
em {
|
||||
font-family : 'ScalySans';
|
||||
font-style : italic;
|
||||
}
|
||||
strong{
|
||||
font-family : ScalySans;
|
||||
strong {
|
||||
font-family : 'ScalySans';
|
||||
font-weight : 800;
|
||||
letter-spacing : -0.02em;
|
||||
}
|
||||
}
|
||||
.useColumns(@multiplier : 1){
|
||||
.useColumns(@multiplier : 1) {
|
||||
column-count : 2;
|
||||
column-fill : auto;
|
||||
column-gap : 1cm;
|
||||
@@ -40,21 +36,21 @@ body {
|
||||
-webkit-column-gap : 1cm;
|
||||
-moz-column-gap : 1cm;
|
||||
}
|
||||
.phb, .page{
|
||||
.phb, .page {
|
||||
.useColumns();
|
||||
counter-increment : phb-page-numbers;
|
||||
position : relative;
|
||||
z-index : 15;
|
||||
box-sizing : border-box;
|
||||
overflow : hidden;
|
||||
height : 279.4mm;
|
||||
width : 215.9mm;
|
||||
height : 279.4mm;
|
||||
padding : 1.0cm 1.7cm;
|
||||
padding-bottom : 1.5cm;
|
||||
overflow : hidden;
|
||||
font-family : 'BookSanity';
|
||||
font-size : 0.317cm;
|
||||
counter-increment : phb-page-numbers;
|
||||
background-color : @background;
|
||||
background-image : @backgroundImage;
|
||||
font-family : BookSanity;
|
||||
font-size : 0.317cm;
|
||||
text-rendering : optimizeLegibility;
|
||||
page-break-before : always;
|
||||
page-break-after : always;
|
||||
@@ -63,199 +59,175 @@ body {
|
||||
contain-intrinsic-size : auto none;
|
||||
}
|
||||
|
||||
.phb{
|
||||
.phb {
|
||||
//*****************************
|
||||
// * BASE
|
||||
// *****************************/
|
||||
p{
|
||||
p {
|
||||
padding-bottom : 0.8em;
|
||||
line-height : 1.269em;
|
||||
&+p{
|
||||
margin-top : -0.8em;
|
||||
}
|
||||
& + p { margin-top : -0.8em; }
|
||||
}
|
||||
ul{
|
||||
margin-bottom : 0.8em;
|
||||
ul {
|
||||
padding-left : 1.4em;
|
||||
margin-bottom : 0.8em;
|
||||
line-height : 1.269em;
|
||||
list-style-position : outside;
|
||||
list-style-type : disc;
|
||||
}
|
||||
ol{
|
||||
margin-bottom : 0.8em;
|
||||
ol {
|
||||
padding-left : 1.4em;
|
||||
margin-bottom : 0.8em;
|
||||
line-height : 1.269em;
|
||||
list-style-position : outside;
|
||||
list-style-type : decimal;
|
||||
}
|
||||
//Indents after p or lists
|
||||
p+p, ul+p, ol+p{
|
||||
text-indent : 1em;
|
||||
}
|
||||
img{
|
||||
z-index : -1;
|
||||
}
|
||||
strong{
|
||||
p + p, ul + p, ol + p { text-indent : 1em; }
|
||||
img { z-index : -1; }
|
||||
strong {
|
||||
font-weight : bold;
|
||||
letter-spacing : 0.03em;
|
||||
}
|
||||
em{
|
||||
font-style : italic;
|
||||
}
|
||||
sup{
|
||||
em { font-style : italic; }
|
||||
sup {
|
||||
font-size : smaller;
|
||||
line-height : 0;
|
||||
vertical-align : super;
|
||||
font-size : smaller;
|
||||
line-height : 0;
|
||||
}
|
||||
sub{
|
||||
vertical-align : sub;
|
||||
sub {
|
||||
font-size : smaller;
|
||||
line-height : 0;
|
||||
vertical-align : sub;
|
||||
}
|
||||
//*****************************
|
||||
// * HEADERS
|
||||
// *****************************/
|
||||
h1,h2,h3,h4{
|
||||
h1,h2,h3,h4 {
|
||||
margin-top : 0.2em;
|
||||
margin-bottom : 0.2em;
|
||||
font-family : MrJeeves;
|
||||
font-family : 'MrJeeves';
|
||||
font-weight : 800;
|
||||
color : @headerText;
|
||||
}
|
||||
h1{
|
||||
h1 {
|
||||
column-span : all;
|
||||
font-size : 0.987cm;
|
||||
-webkit-column-span : all;
|
||||
-moz-column-span : all;
|
||||
&+p::first-letter{
|
||||
& + p::first-letter {
|
||||
float : left;
|
||||
font-family : Solberry;
|
||||
font-family : 'Solberry';
|
||||
font-size : 10em;
|
||||
color : #222;
|
||||
line-height : 0.795em;
|
||||
color : #222222;
|
||||
}
|
||||
}
|
||||
h2{
|
||||
font-size : 0.705cm;
|
||||
}
|
||||
h3{
|
||||
h2 { font-size : 0.705cm; }
|
||||
h3 {
|
||||
font-size : 0.529cm;
|
||||
border-bottom : 2px solid @headerUnderline;
|
||||
}
|
||||
h4{
|
||||
h4 {
|
||||
margin-bottom : 0.00em;
|
||||
font-size : 0.458cm;
|
||||
}
|
||||
h5{
|
||||
h5 {
|
||||
margin-bottom : 0.2em;
|
||||
font-family : ScalySansSmallCaps;
|
||||
font-family : 'ScalySansSmallCaps';
|
||||
font-size : 0.423cm;
|
||||
font-weight : 900;
|
||||
}
|
||||
//*****************************
|
||||
// * TABLE
|
||||
// *****************************/
|
||||
table{
|
||||
table {
|
||||
.useSansSerif();
|
||||
width : 100%;
|
||||
margin-bottom : 1em;
|
||||
font-size : 10pt;
|
||||
thead{
|
||||
display: table-row-group;
|
||||
thead {
|
||||
display : table-row-group;
|
||||
font-weight : 800;
|
||||
th{
|
||||
vertical-align : bottom;
|
||||
padding-bottom : 0.3em;
|
||||
th {
|
||||
padding-right : 0.1em;
|
||||
padding-bottom : 0.3em;
|
||||
padding-left : 0.1em;
|
||||
vertical-align : bottom;
|
||||
}
|
||||
}
|
||||
tbody{
|
||||
tr{
|
||||
td{
|
||||
padding : 0.3em 0.1em;
|
||||
}
|
||||
&:nth-child(odd){
|
||||
background-color : @noteGreen;
|
||||
}
|
||||
tbody {
|
||||
tr {
|
||||
td { padding : 0.3em 0.1em; }
|
||||
&:nth-child(odd) { background-color : @noteGreen; }
|
||||
}
|
||||
}
|
||||
}
|
||||
//*****************************
|
||||
// * NOTE
|
||||
// *****************************/
|
||||
blockquote{
|
||||
blockquote {
|
||||
.useSansSerif();
|
||||
box-sizing : border-box;
|
||||
margin-bottom : 1em;
|
||||
padding : 5px 10px;
|
||||
margin-bottom : 1em;
|
||||
background-color : @noteGreen;
|
||||
border-style : solid;
|
||||
border-width : 11px;
|
||||
border-image : @noteBorderImage 11;
|
||||
border-image-outset : 9px 0px;
|
||||
box-shadow : 1px 4px 14px #888;
|
||||
p, ul{
|
||||
box-shadow : 1px 4px 14px #888888;
|
||||
p, ul {
|
||||
font-size : 0.352cm;
|
||||
line-height : 1.083em;
|
||||
}
|
||||
}
|
||||
//If a note starts a column, give it space at the top to render border
|
||||
pre+blockquote, h2+blockquote, h3+blockquote, h4+blockquote, h5+blockquote {
|
||||
margin-top : 13px;
|
||||
}
|
||||
pre + blockquote, h2 + blockquote, h3 + blockquote, h4 + blockquote, h5 + blockquote { margin-top : 13px; }
|
||||
//*****************************
|
||||
// * MONSTER STAT BLOCK
|
||||
// *****************************/
|
||||
hr+blockquote{
|
||||
hr+blockquote {
|
||||
position : relative;
|
||||
padding-top : 15px;
|
||||
background-color : @monsterStatBackground;
|
||||
border-style : solid;
|
||||
border-width : 10px;
|
||||
border-image : @monsterBorderImageLegacy 10;
|
||||
h2{
|
||||
h2 {
|
||||
margin-top : -8px;
|
||||
margin-bottom : 0px;
|
||||
&+p{
|
||||
padding-bottom : 0px;
|
||||
}
|
||||
& + p { padding-bottom : 0px; }
|
||||
}
|
||||
h3{
|
||||
font-family : ScalySans;
|
||||
font-weight : 400;
|
||||
h3 {
|
||||
font-family : 'ScalySans';
|
||||
font-weight : normal;
|
||||
border-bottom : 1px solid @headerText;
|
||||
}
|
||||
hr+ul{
|
||||
color : @headerText;
|
||||
}
|
||||
ul{
|
||||
hr + ul { color : @headerText; }
|
||||
ul {
|
||||
.useSansSerif();
|
||||
padding-left : 1em;
|
||||
font-size : 0.352cm;
|
||||
}
|
||||
// Monster Ability table
|
||||
hr+table{
|
||||
hr + table {
|
||||
margin : 0;
|
||||
background-color : transparent;
|
||||
border-style : none;
|
||||
border-image : none;
|
||||
tbody{
|
||||
tr:nth-child(odd), tr:nth-child(even){
|
||||
background-color : transparent;
|
||||
}
|
||||
tbody {
|
||||
tr:nth-child(odd), tr:nth-child(even) { background-color : transparent; }
|
||||
}
|
||||
}
|
||||
table{
|
||||
color : @headerText;
|
||||
}
|
||||
p+p{
|
||||
margin-top : 0em;
|
||||
table { color : @headerText; }
|
||||
p + p {
|
||||
padding-bottom : 0.5em;
|
||||
margin-top : 0em;
|
||||
text-indent : 0em;
|
||||
}
|
||||
//Triangle dividers
|
||||
hr{
|
||||
hr {
|
||||
visibility : visible;
|
||||
height : 6px;
|
||||
margin : 4px 0px;
|
||||
@@ -265,100 +237,90 @@ body {
|
||||
}
|
||||
}
|
||||
//Full Width
|
||||
hr+hr+blockquote{
|
||||
hr + hr + blockquote {
|
||||
.useColumns(0.96);
|
||||
column-fill : balance;
|
||||
}
|
||||
//*****************************
|
||||
// * FOOTER
|
||||
// *****************************/
|
||||
&:after{
|
||||
content : "";
|
||||
&:after {
|
||||
position : absolute;
|
||||
bottom : 0px;
|
||||
left : 0px;
|
||||
z-index : 100;
|
||||
height : 50px;
|
||||
width : 100%;
|
||||
height : 50px;
|
||||
content : '';
|
||||
background-image : @footerAccentImage;
|
||||
background-size : cover;
|
||||
}
|
||||
&:nth-child(even){
|
||||
&:after{
|
||||
transform : scaleX(-1);
|
||||
}
|
||||
.pageNumber{
|
||||
left : 2px;
|
||||
}
|
||||
.footnote{
|
||||
&:nth-child(even) {
|
||||
&::after { transform : scaleX(-1); }
|
||||
.pageNumber { left : 2px; }
|
||||
.footnote {
|
||||
left : 80px;
|
||||
text-align : left;
|
||||
}
|
||||
}
|
||||
.pageNumber{
|
||||
.pageNumber {
|
||||
position : absolute;
|
||||
right : 2px;
|
||||
bottom : 22px;
|
||||
width : 50px;
|
||||
font-size : 0.9em;
|
||||
color : #c9ad6a;
|
||||
color : #C9AD6A;
|
||||
text-align : center;
|
||||
&.auto::after {
|
||||
content : counter(phb-page-numbers);
|
||||
}
|
||||
&.auto::after { content : counter(phb-page-numbers); }
|
||||
}
|
||||
.footnote{
|
||||
.footnote {
|
||||
position : absolute;
|
||||
right : 80px;
|
||||
bottom : 32px;
|
||||
z-index : 150;
|
||||
width : 200px;
|
||||
font-size : 0.8em;
|
||||
color : #c9ad6a;
|
||||
color : #C9AD6A;
|
||||
text-align : right;
|
||||
}
|
||||
//*****************************
|
||||
// * EXTRAS
|
||||
// *****************************/
|
||||
hr{
|
||||
hr {
|
||||
visibility : hidden;
|
||||
margin : 0px;
|
||||
}
|
||||
//Modified unorder list, used in spells
|
||||
hr+ul{
|
||||
margin-bottom : 0.5em;
|
||||
hr + ul {
|
||||
padding-left : 1em;
|
||||
margin-bottom : 0.5em;
|
||||
text-indent : -1em;
|
||||
list-style-type : none;
|
||||
}
|
||||
//Column Break
|
||||
pre, code{
|
||||
pre, code {
|
||||
visibility : hidden;
|
||||
-webkit-column-break-after : always;
|
||||
break-after : always;
|
||||
-moz-column-break-after : always;
|
||||
}
|
||||
//Avoid breaking up
|
||||
p,blockquote,table{
|
||||
p,blockquote,table {
|
||||
z-index : 15;
|
||||
-webkit-column-break-inside : avoid;
|
||||
page-break-inside : avoid;
|
||||
break-inside : avoid;
|
||||
}
|
||||
//Better spacing for spell blocks
|
||||
h4+p+hr+ul{
|
||||
margin-top : -0.5em
|
||||
}
|
||||
h4 + p + hr + ul { margin-top : -0.5em; }
|
||||
//Text indent right after table
|
||||
table+p{
|
||||
text-indent : 1em;
|
||||
}
|
||||
table + p { text-indent : 1em; }
|
||||
// Nested lists
|
||||
ul ul,ol ol,ul ol,ol ul{
|
||||
ul ul,ol ol,ul ol,ol ul {
|
||||
margin-bottom : 0px;
|
||||
margin-left : 1.5em;
|
||||
}
|
||||
li{
|
||||
li {
|
||||
-webkit-column-break-inside : avoid;
|
||||
page-break-inside : avoid;
|
||||
break-inside : avoid;
|
||||
@@ -367,89 +329,81 @@ body {
|
||||
//*****************************
|
||||
// * SPELL LIST
|
||||
// *****************************/
|
||||
.phb .spellList{
|
||||
.phb .spellList {
|
||||
.useSansSerif();
|
||||
column-count : 4;
|
||||
column-span : all;
|
||||
-webkit-column-span : all;
|
||||
-moz-column-span : all;
|
||||
ul+h5{
|
||||
margin-top : 15px;
|
||||
}
|
||||
p, ul{
|
||||
column-span : all;
|
||||
ul + h5 { margin-top : 15px; }
|
||||
p, ul {
|
||||
font-size : 0.352cm;
|
||||
line-height : 1.263em;
|
||||
}
|
||||
ul{
|
||||
margin-bottom : 0.5em;
|
||||
ul {
|
||||
padding-left : 1em;
|
||||
margin-bottom : 0.5em;
|
||||
text-indent : -1em;
|
||||
list-style-type : none;
|
||||
break-inside : auto;
|
||||
-webkit-column-break-inside : auto;
|
||||
page-break-inside : auto;
|
||||
break-inside : auto;
|
||||
}
|
||||
}
|
||||
//*****************************
|
||||
// * WIDE
|
||||
// *****************************/
|
||||
.phb .wide{
|
||||
column-span : all;
|
||||
.phb .wide {
|
||||
-webkit-column-span : all;
|
||||
-moz-column-span : all;
|
||||
column-span : all;
|
||||
}
|
||||
//*****************************
|
||||
// * CLASS TABLE
|
||||
// *****************************/
|
||||
.phb .classTable{
|
||||
.phb .classTable {
|
||||
margin-top : 25px;
|
||||
margin-bottom : 40px;
|
||||
border-collapse : separate;
|
||||
background-color : white;
|
||||
border : initial;
|
||||
border-style : solid;
|
||||
border-image-source : @frameBorderImage;
|
||||
border-image-slice : 150 200 150 200;
|
||||
border-image-width : 47px;
|
||||
border-image-outset : 25px 17px;
|
||||
border-image-repeat : stretch;
|
||||
border-image-slice : 150 200 150 200;
|
||||
border-image-source : @frameBorderImage;
|
||||
border-image-width : 47px;
|
||||
h5{
|
||||
margin-bottom : 10px;
|
||||
}
|
||||
h5 { margin-bottom : 10px; }
|
||||
}
|
||||
//************************************
|
||||
// * DESCRIPTIVE TEXT BOX
|
||||
// ************************************/
|
||||
.phb .descriptive{
|
||||
.phb .descriptive {
|
||||
margin-bottom : 1em;
|
||||
background-color : #faf7ea;
|
||||
font-family : ScalySans;
|
||||
font-family : 'ScalySans';
|
||||
background-color : #FAF7EA;
|
||||
border-style : solid;
|
||||
border-width : 7px;
|
||||
border-image : @descriptiveBoxImage 12 stretch;
|
||||
border-image-outset : 4px;
|
||||
box-shadow : 0px 0px 6px #faf7ea;
|
||||
p{
|
||||
box-shadow : 0px 0px 6px #FAF7EA;
|
||||
p {
|
||||
display : block;
|
||||
padding-bottom : 0px;
|
||||
line-height : 1.47em;
|
||||
}
|
||||
p + p {
|
||||
padding-top : .8em;
|
||||
}
|
||||
p + p { padding-top : 0.8em; }
|
||||
em {
|
||||
font-family : ScalySans;
|
||||
font-family : 'ScalySans';
|
||||
font-style : italic;
|
||||
}
|
||||
strong {
|
||||
font-family : ScalySans;
|
||||
font-family : 'ScalySans';
|
||||
font-weight : 800;
|
||||
letter-spacing : -0.02em;
|
||||
}
|
||||
}
|
||||
.phb pre+.descriptive{
|
||||
margin-top : 8px;
|
||||
}
|
||||
.phb pre + .descriptive { margin-top : 8px; }
|
||||
|
||||
//*****************************
|
||||
// * ARTIST CREDIT BLOCK
|
||||
@@ -457,47 +411,41 @@ body {
|
||||
.phb {
|
||||
.artist {
|
||||
position : absolute;
|
||||
text-align : center;
|
||||
font-family : WalterTurncoat;
|
||||
font-family : 'WalterTurncoat';
|
||||
font-size : 0.27cm;
|
||||
color : @captionText;
|
||||
text-align : center;
|
||||
p, p + p {
|
||||
margin : unset;
|
||||
text-indent : unset;
|
||||
line-height : 0.941em;
|
||||
text-indent : unset;
|
||||
}
|
||||
h5 {
|
||||
h5 {
|
||||
font-family : 'WalterTurncoat';
|
||||
font-size : 1.3em;
|
||||
font-family : WalterTurncoat;
|
||||
}
|
||||
a{
|
||||
a {
|
||||
color : inherit;
|
||||
text-decoration : unset;
|
||||
&:hover {
|
||||
text-decoration : underline;
|
||||
}
|
||||
&:hover { text-decoration : underline; }
|
||||
}
|
||||
}
|
||||
}
|
||||
//*****************************
|
||||
// * TABLE OF CONTENTS
|
||||
// *****************************/
|
||||
.phb .toc{
|
||||
.phb .toc {
|
||||
-webkit-column-break-inside : avoid;
|
||||
page-break-inside : avoid;
|
||||
break-inside : avoid;
|
||||
a{
|
||||
a {
|
||||
color : black;
|
||||
text-decoration : none;
|
||||
&:hover{
|
||||
text-decoration : underline;
|
||||
}
|
||||
&:hover { text-decoration : underline; }
|
||||
}
|
||||
ul{
|
||||
ul {
|
||||
padding-left : 0;
|
||||
list-style-type : none;
|
||||
}
|
||||
&>ul>li{
|
||||
margin-bottom : 10px;
|
||||
}
|
||||
& > ul > li { margin-bottom : 10px; }
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* eslint-disable max-lines */
|
||||
|
||||
|
||||
module.exports = [
|
||||
];
|
||||
|
||||
@@ -7,37 +7,29 @@
|
||||
}
|
||||
|
||||
.page {
|
||||
background-image : url(/assets/DMG_background.png);
|
||||
background-image : url('/assets/DMG_background.png');
|
||||
background-size : cover;
|
||||
|
||||
/*TABLES WITHIN NOTES*/
|
||||
.note table tbody tr:nth-child(odd) {
|
||||
background:#fff;
|
||||
}
|
||||
/* TABLES WITHIN NOTES */
|
||||
.note table tbody tr:nth-child(odd) { background : #FFFFFF; }
|
||||
|
||||
/*DROP CAP*/
|
||||
/* DROP CAP */
|
||||
h1 + p::first-letter {
|
||||
background-image: unset;
|
||||
color:black;
|
||||
color : black;
|
||||
background-image : unset;
|
||||
}
|
||||
|
||||
.quote p:first-child::first-line {
|
||||
all: unset;
|
||||
.quote p:first-child::first-line { all : unset; }
|
||||
|
||||
&::after {
|
||||
height : 58px;
|
||||
background-image : url('/assets/DMG_footerAccent.png');
|
||||
}
|
||||
|
||||
&:after {
|
||||
background-image : url(/assets/DMG_footerAccent.png);
|
||||
height: 58px;
|
||||
}
|
||||
|
||||
.footnote {
|
||||
bottom : 40px;
|
||||
}
|
||||
.footnote { bottom : 40px; }
|
||||
}
|
||||
|
||||
.page:has(.partCover) {
|
||||
|
||||
.partCover {
|
||||
background-image: @partCoverHeaderDMG;
|
||||
}
|
||||
.partCover { background-image : @partCoverHeaderDMG; }
|
||||
}
|
||||
|
||||
@@ -6,164 +6,12 @@ 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;
|
||||
|
||||
|
||||
|
||||
module.exports = [
|
||||
|
||||
{
|
||||
groupName : 'Text Editor',
|
||||
icon : 'fas fa-pencil-alt',
|
||||
view : 'text',
|
||||
snippets : [
|
||||
{
|
||||
name : 'Table of Contents',
|
||||
icon : 'fas fa-book',
|
||||
gen : TableOfContentsGen,
|
||||
experimental : true,
|
||||
subsnippets : [
|
||||
{
|
||||
name : 'Generate Table of Contents',
|
||||
icon : 'fas fa-book',
|
||||
gen : TableOfContentsGen,
|
||||
experimental : true
|
||||
},
|
||||
{
|
||||
name : 'Table of Contents Individual Inclusion',
|
||||
icon : 'fas fa-book',
|
||||
gen : dedent `\n{{tocInclude# CHANGE # to your header level
|
||||
}}\n`,
|
||||
subsnippets : [
|
||||
{
|
||||
name : 'Individual Inclusion H1',
|
||||
icon : 'fas fa-book',
|
||||
gen : dedent `\n{{tocIncludeH1 \n
|
||||
}}\n`,
|
||||
},
|
||||
{
|
||||
name : 'Individual Inclusion H2',
|
||||
icon : 'fas fa-book',
|
||||
gen : dedent `\n{{tocIncludeH2 \n
|
||||
}}\n`,
|
||||
},
|
||||
{
|
||||
name : 'Individual Inclusion H3',
|
||||
icon : 'fas fa-book',
|
||||
gen : dedent `\n{{tocIncludeH3 \n
|
||||
}}\n`,
|
||||
},
|
||||
{
|
||||
name : 'Individual Inclusion H4',
|
||||
icon : 'fas fa-book',
|
||||
gen : dedent `\n{{tocIncludeH4 \n
|
||||
}}\n`,
|
||||
},
|
||||
{
|
||||
name : 'Individual Inclusion H5',
|
||||
icon : 'fas fa-book',
|
||||
gen : dedent `\n{{tocIncludeH5 \n
|
||||
}}\n`,
|
||||
},
|
||||
{
|
||||
name : 'Individual Inclusion H6',
|
||||
icon : 'fas fa-book',
|
||||
gen : dedent `\n{{tocIncludeH6 \n
|
||||
}}\n`,
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name : 'Table of Contents Range Inclusion',
|
||||
icon : 'fas fa-book',
|
||||
gen : dedent `\n{{tocDepthH3
|
||||
}}\n`,
|
||||
subsnippets : [
|
||||
{
|
||||
name : 'Include in ToC up to H3',
|
||||
icon : 'fas fa-dice-three',
|
||||
gen : dedent `\n{{tocDepthH3
|
||||
}}\n`,
|
||||
|
||||
},
|
||||
{
|
||||
name : 'Include in ToC up to H4',
|
||||
icon : 'fas fa-dice-four',
|
||||
gen : dedent `\n{{tocDepthH4
|
||||
}}\n`,
|
||||
},
|
||||
{
|
||||
name : 'Include in ToC up to H5',
|
||||
icon : 'fas fa-dice-five',
|
||||
gen : dedent `\n{{tocDepthH5
|
||||
}}\n`,
|
||||
},
|
||||
{
|
||||
name : 'Include in ToC up to H6',
|
||||
icon : 'fas fa-dice-six',
|
||||
gen : dedent `\n{{tocDepthH6
|
||||
}}\n`,
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
name : 'Table of Contents Individual Exclusion',
|
||||
icon : 'fas fa-book',
|
||||
gen : dedent `\n{{tocExcludeH1 \n
|
||||
}}\n`,
|
||||
subsnippets : [
|
||||
{
|
||||
name : 'Individual Exclusion H1',
|
||||
icon : 'fas fa-book',
|
||||
gen : dedent `\n{{tocExcludeH1 \n
|
||||
}}\n`,
|
||||
},
|
||||
{
|
||||
name : 'Individual Exclusion H2',
|
||||
icon : 'fas fa-book',
|
||||
gen : dedent `\n{{tocExcludeH2 \n
|
||||
}}\n`,
|
||||
},
|
||||
{
|
||||
name : 'Individual Exclusion H3',
|
||||
icon : 'fas fa-book',
|
||||
gen : dedent `\n{{tocExcludeH3 \n
|
||||
}}\n`,
|
||||
},
|
||||
{
|
||||
name : 'Individual Exclusion H4',
|
||||
icon : 'fas fa-book',
|
||||
gen : dedent `\n{{tocExcludeH4 \n
|
||||
}}\n`,
|
||||
},
|
||||
{
|
||||
name : 'Individual Exclusion H5',
|
||||
icon : 'fas fa-book',
|
||||
gen : dedent `\n{{tocExcludeH5 \n
|
||||
}}\n`,
|
||||
},
|
||||
{
|
||||
name : 'Individual Exclusion H6',
|
||||
icon : 'fas fa-book',
|
||||
gen : dedent `\n{{tocExcludeH6 \n
|
||||
}}\n`,
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
]
|
||||
},
|
||||
{
|
||||
name : 'Index',
|
||||
icon : 'fas fa-bars',
|
||||
gen : indexGen,
|
||||
experimental : true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
groupName : 'Style Editor',
|
||||
icon : 'fas fa-pencil-alt',
|
||||
@@ -192,70 +40,9 @@ module.exports = [
|
||||
line-height: 1em;
|
||||
}\n\n`
|
||||
},
|
||||
{
|
||||
name : 'Table of Contents Toggles',
|
||||
icon : 'fas fa-book',
|
||||
subsnippets : [
|
||||
{
|
||||
name : 'Enable H1-H4 all pages',
|
||||
icon : 'fas fa-dice-four',
|
||||
gen : `.page {\n\th4 {--TOC: include; }\n}\n\n`,
|
||||
},
|
||||
{
|
||||
name : 'Enable H1-H5 all pages',
|
||||
icon : 'fas fa-dice-five',
|
||||
gen : `.page {\n\th4, h5 {--TOC: include; }\n}\n\n`,
|
||||
},
|
||||
{
|
||||
name : 'Enable H1-H6 all pages',
|
||||
icon : 'fas fa-dice-six',
|
||||
gen : `.page {\n\th4, h5, h6 {--TOC: include; }\n}\n\n`,
|
||||
},
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
/*********************** IMAGES *******************/
|
||||
{
|
||||
groupName : 'Images',
|
||||
icon : 'fas fa-images',
|
||||
view : 'text',
|
||||
snippets : [
|
||||
{
|
||||
name : 'Image',
|
||||
icon : 'fas fa-image',
|
||||
gen : dedent`
|
||||
 {width:325px,mix-blend-mode:multiply}
|
||||
|
||||
{{artist,position:relative,top:-230px,left:10px,margin-bottom:-30px
|
||||
##### Cat Warrior
|
||||
[Kyoung Hwan Kim](https://www.artstation.com/tahra)
|
||||
}}`
|
||||
},
|
||||
{
|
||||
name : 'Background Image',
|
||||
icon : 'fas fa-tree',
|
||||
gen : dedent`
|
||||
 {position:absolute,top:50px,right:30px,width:280px}
|
||||
|
||||
{{artist,top:80px,right:30px
|
||||
##### Homebrew Mug
|
||||
[naturalcrit](https://homebrew.naturalcrit.com)
|
||||
}}`
|
||||
},
|
||||
{
|
||||
name : 'Watermark',
|
||||
icon : 'fas fa-id-card',
|
||||
gen : dedent`
|
||||
{{watermark Homebrewery}}\n`
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
|
||||
/************************* PHB ********************/
|
||||
|
||||
{
|
||||
groupName : 'PHB',
|
||||
icon : 'fas fa-book',
|
||||
@@ -450,9 +237,6 @@ module.exports = [
|
||||
]
|
||||
},
|
||||
|
||||
|
||||
|
||||
|
||||
/**************** PAGE *************/
|
||||
|
||||
{
|
||||
|
||||
@@ -305,12 +305,12 @@
|
||||
margin-left : -0.16cm;
|
||||
background-color : var(--HB_Color_MonsterStatBackground);
|
||||
background-image : @monsterBlockBackground;
|
||||
background-blend-mode : overlay;
|
||||
border-style : solid;
|
||||
border-width : 7px 6px;
|
||||
border-image : @monsterBorderImage 14 round;
|
||||
border-image-outset : 0px 2px;
|
||||
box-shadow : 1px 4px 14px #888888;
|
||||
background-blend-mode : overlay;
|
||||
}
|
||||
|
||||
position : relative;
|
||||
@@ -335,9 +335,9 @@
|
||||
|
||||
//Triangle dividers
|
||||
hr {
|
||||
visibility : visible;
|
||||
height : 6px;
|
||||
margin : 0.12cm 0cm;
|
||||
visibility : visible;
|
||||
background-image : @redTriangleImage;
|
||||
background-size : 100% 100%;
|
||||
border : none;
|
||||
@@ -355,8 +355,8 @@
|
||||
}
|
||||
|
||||
.bonus {
|
||||
float: right;
|
||||
padding-right: 0.5em;
|
||||
float : right;
|
||||
padding-right : 0.5em;
|
||||
}
|
||||
|
||||
// Monster Ability table
|
||||
@@ -456,8 +456,8 @@
|
||||
// * EXTRAS
|
||||
// *****************************/
|
||||
hr {
|
||||
margin : 0px;
|
||||
visibility : hidden;
|
||||
margin : 0px;
|
||||
}
|
||||
//Text indent right after table
|
||||
table + p { text-indent : 1em; }
|
||||
@@ -525,10 +525,10 @@
|
||||
content : '';
|
||||
background-image : @classTableDecoration,
|
||||
@classTableDecoration;
|
||||
filter : drop-shadow(0px 0px 1px #C8C5C080);
|
||||
background-repeat : no-repeat, no-repeat;
|
||||
background-position : top, bottom;
|
||||
background-size : contain, contain;
|
||||
filter : drop-shadow(0px 0px 1px #C8C5C080);
|
||||
transform : translateY(-50%) translateX(-50%);
|
||||
}
|
||||
&.decoration.wide::before {
|
||||
@@ -547,38 +547,38 @@
|
||||
&::after { display : none; }
|
||||
.frontCover { position : absolute; }
|
||||
h1 {
|
||||
margin-top : 1.55cm;
|
||||
margin-bottom : 0;
|
||||
font-family : 'NodestoCapsCondensed';
|
||||
font-size : 2.245cm;
|
||||
font-weight : normal;
|
||||
line-height : 1.9cm;
|
||||
color : white;
|
||||
text-shadow : unset;
|
||||
text-transform : uppercase;
|
||||
-webkit-text-stroke: 0.2cm black;
|
||||
paint-order:stroke;
|
||||
margin-top : 1.55cm;
|
||||
margin-bottom : 0;
|
||||
font-family : 'NodestoCapsCondensed';
|
||||
font-size : 2.245cm;
|
||||
font-weight : normal;
|
||||
line-height : 1.9cm;
|
||||
color : white;
|
||||
text-transform : uppercase;
|
||||
text-shadow : unset;
|
||||
-webkit-text-stroke : 0.2cm black;
|
||||
paint-order : stroke;
|
||||
}
|
||||
h2 {
|
||||
font-family : 'NodestoCapsCondensed';
|
||||
font-size : 0.85cm;
|
||||
font-weight : normal;
|
||||
color : white;
|
||||
letter-spacing : 0.1cm;
|
||||
-webkit-text-stroke: 0.14cm black;
|
||||
paint-order:stroke;
|
||||
font-family : 'NodestoCapsCondensed';
|
||||
font-size : 0.85cm;
|
||||
font-weight : normal;
|
||||
color : white;
|
||||
letter-spacing : 0.1cm;
|
||||
-webkit-text-stroke : 0.14cm black;
|
||||
paint-order : stroke;
|
||||
}
|
||||
hr {
|
||||
position : relative;
|
||||
display : block;
|
||||
visibility : visible;
|
||||
width : 12cm;
|
||||
height : 0.5cm;
|
||||
margin : auto;
|
||||
visibility : visible;
|
||||
background-image : @horizontalRule;
|
||||
filter : drop-shadow(0 0 3px black);
|
||||
background-size : 100% 100%;
|
||||
border : none;
|
||||
filter : drop-shadow(0 0 3px black);
|
||||
}
|
||||
.banner {
|
||||
position : absolute;
|
||||
@@ -601,19 +601,19 @@
|
||||
filter : drop-shadow(2px 2px 2px black);
|
||||
}
|
||||
.footnote {
|
||||
position : absolute;
|
||||
right : 0;
|
||||
bottom : 1.3cm;
|
||||
left : 0;
|
||||
width : 70%;
|
||||
margin-right : auto;
|
||||
margin-left : auto;
|
||||
font-family : 'Overpass';
|
||||
font-size : 0.496cm;
|
||||
color : white;
|
||||
text-align : center;
|
||||
-webkit-text-stroke: 0.1cm black;
|
||||
paint-order:stroke;
|
||||
position : absolute;
|
||||
right : 0;
|
||||
bottom : 1.3cm;
|
||||
left : 0;
|
||||
width : 70%;
|
||||
margin-right : auto;
|
||||
margin-left : auto;
|
||||
font-family : 'Overpass';
|
||||
font-size : 0.496cm;
|
||||
color : white;
|
||||
text-align : center;
|
||||
-webkit-text-stroke : 0.1cm black;
|
||||
paint-order : stroke;
|
||||
}
|
||||
.logo {
|
||||
position : absolute;
|
||||
@@ -621,9 +621,7 @@
|
||||
right : 0;
|
||||
left : 0;
|
||||
filter : drop-shadow(0 0 0.075cm black);
|
||||
img {
|
||||
height : 2cm;
|
||||
}
|
||||
img { height : 2cm; }
|
||||
}
|
||||
}
|
||||
// *****************************
|
||||
@@ -652,10 +650,10 @@
|
||||
hr {
|
||||
position : relative;
|
||||
display : block;
|
||||
visibility : visible;
|
||||
width : 12cm;
|
||||
height : 0.5cm;
|
||||
margin : auto;
|
||||
visibility : visible;
|
||||
background-image : @horizontalRule;
|
||||
background-size : 100% 100%;
|
||||
border : none;
|
||||
@@ -666,19 +664,17 @@
|
||||
bottom : 1cm;
|
||||
left : 0;
|
||||
height : 2cm;
|
||||
img {
|
||||
height : 2cm;
|
||||
}
|
||||
img { height : 2cm; }
|
||||
}
|
||||
}
|
||||
// *****************************
|
||||
// * BACK COVER
|
||||
// *****************************/
|
||||
.page:has(.backCover) {
|
||||
padding : 2.25cm 1.3cm 2cm 1.3cm;
|
||||
color : #FFFFFF;
|
||||
columns : 1;
|
||||
padding : 2.25cm 1.3cm 2cm 1.3cm;
|
||||
line-height : 1.4em;
|
||||
color : #FFFFFF;
|
||||
columns : 1;
|
||||
&::after { display : none; }
|
||||
.columnWrapper { width : 7.6cm; }
|
||||
.backCover {
|
||||
@@ -689,6 +685,7 @@
|
||||
background-repeat : no-repeat;
|
||||
background-size : contain;
|
||||
}
|
||||
.blank { height : 1.4em; }
|
||||
h1 {
|
||||
margin-bottom : 0.3cm;
|
||||
font-family : 'NodestoCapsCondensed';
|
||||
@@ -706,12 +703,12 @@
|
||||
height : 100%;
|
||||
}
|
||||
hr {
|
||||
visibility : visible;
|
||||
width : 4.5cm;
|
||||
height : 0.53cm;
|
||||
margin-top : 1.1cm;
|
||||
margin-right : auto;
|
||||
margin-left : auto;
|
||||
visibility : visible;
|
||||
background-image : @horizontalRule;
|
||||
background-size : 100% 100%;
|
||||
border : none;
|
||||
@@ -794,54 +791,13 @@
|
||||
// * TABLE OF CONTENTS
|
||||
// *****************************/
|
||||
|
||||
// Default Exclusions
|
||||
// Anything not excluded is included, default Headers are H1, H2, and H3.
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
.page:has(.frontCover),
|
||||
.page:has(.backCover),
|
||||
.page:has(.insideCover),
|
||||
.monster,
|
||||
.noToC,
|
||||
.toc { --TOC: exclude; }
|
||||
|
||||
|
||||
// Brew level default inclusion changes.
|
||||
// These add Headers 'back' to inclusion.
|
||||
|
||||
//NOTE: DO NOT USE :HAS WITH .PAGES!!! EXTREMELY SLOW TO RENDER ON LARGE DOCS!
|
||||
|
||||
// Block level inclusion changes
|
||||
// These include either a single (include) or a range (depth)
|
||||
.tocIncludeH1 h1 {--TOC: include; }
|
||||
.tocIncludeH2 h2 {--TOC: include; }
|
||||
.tocIncludeH3 h3 {--TOC: include; }
|
||||
.tocIncludeH4 h4 {--TOC: include; }
|
||||
.tocIncludeH5 h5 {--TOC: include; }
|
||||
.tocIncludeH6 h6 {--TOC: include; }
|
||||
|
||||
.tocDepthH2 :is(h1, h2) {--TOC: include; }
|
||||
.tocDepthH3 :is(h1, h2, h3) {--TOC: include; }
|
||||
.tocDepthH4 :is(h1, h2, h3, h4) {--TOC: include; }
|
||||
.tocDepthH5 :is(h1, h2, h3, h4, h5) {--TOC: include; }
|
||||
.tocDepthH6 :is(h1, h2, h3, h4, h5, h6) {--TOC: include; }
|
||||
|
||||
// Block level exclusion changes
|
||||
// These exclude a single block level
|
||||
.tocExcludeH1 h1 {--TOC: exclude; }
|
||||
.tocExcludeH2 h2 {--TOC: exclude; }
|
||||
.tocExcludeH3 h3 {--TOC: exclude; }
|
||||
.tocExcludeH4 h4 {--TOC: exclude; }
|
||||
.tocExcludeH5 h5 {--TOC: exclude; }
|
||||
.tocExcludeH6 h6 {--TOC: exclude; }
|
||||
// Additional Default Exclusions
|
||||
.monster { --TOC : exclude; }
|
||||
|
||||
.page:has(.partCover) {
|
||||
--TOC: exclude;
|
||||
& h1 {
|
||||
--TOC: include;
|
||||
}
|
||||
}
|
||||
--TOC : exclude;
|
||||
& h1 { --TOC : include; }
|
||||
}
|
||||
|
||||
.page {
|
||||
&:has(.toc)::after { display : none; }
|
||||
@@ -907,9 +863,7 @@ h6,
|
||||
.useColumns(0.96, @fillMode: balance);
|
||||
}
|
||||
}
|
||||
.toc.wide li {
|
||||
break-inside: auto;
|
||||
}
|
||||
.toc.wide li { break-inside : auto; }
|
||||
}
|
||||
|
||||
// *****************************
|
||||
@@ -934,9 +888,7 @@ h6,
|
||||
|
||||
.page h1 + * { margin-top : 0; }
|
||||
|
||||
.page .descriptive.wide + * {
|
||||
margin-top: 0;
|
||||
}
|
||||
.page .descriptive.wide + * { margin-top : 0; }
|
||||
|
||||
//*****************************
|
||||
// * RUNE TABLE
|
||||
@@ -951,8 +903,8 @@ h6,
|
||||
width : 1.3cm;
|
||||
height : 1.3cm;
|
||||
font-weight : normal;
|
||||
text-transform : uppercase;
|
||||
vertical-align : middle;
|
||||
text-transform : uppercase;
|
||||
outline : 1px solid #000000;
|
||||
}
|
||||
th {
|
||||
@@ -973,6 +925,7 @@ h6,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// *****************************
|
||||
// * INDEX
|
||||
// *****************************/
|
||||
|
||||
@@ -4,6 +4,8 @@ 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;
|
||||
const TableOfContentsGen = require('./snippets/tableOfContents.gen.js');
|
||||
const indexGen = require('./snippets/index.gen.js');
|
||||
|
||||
module.exports = [
|
||||
|
||||
@@ -36,6 +38,11 @@ module.exports = [
|
||||
icon : 'fas fa-sort-numeric-down',
|
||||
gen : '{{pageNumber,auto}}\n'
|
||||
},
|
||||
{
|
||||
name : 'Variable Auto Page Number',
|
||||
icon : 'fas fa-sort-numeric-down',
|
||||
gen : '{{pageNumber $[HB_pageNumber]}}\n'
|
||||
},
|
||||
{
|
||||
name : 'Skip Page Number Increment this Page',
|
||||
icon : 'fas fa-xmark',
|
||||
@@ -141,7 +148,53 @@ module.exports = [
|
||||
[Homebrewery.Naturalcrit.com](https://homebrewery.naturalcrit.com)
|
||||
}}\n\n`;
|
||||
},
|
||||
}
|
||||
},
|
||||
{
|
||||
name : 'Table of Contents',
|
||||
icon : 'fas fa-book',
|
||||
gen : TableOfContentsGen,
|
||||
experimental : true,
|
||||
subsnippets : [
|
||||
{
|
||||
name : 'Table of Contents',
|
||||
icon : 'fas fa-book',
|
||||
gen : TableOfContentsGen,
|
||||
experimental : true
|
||||
},
|
||||
{
|
||||
name : 'Include in ToC up to H3',
|
||||
icon : 'fas fa-dice-three',
|
||||
gen : dedent `\n{{tocDepthH3
|
||||
}}\n`,
|
||||
|
||||
},
|
||||
{
|
||||
name : 'Include in ToC up to H4',
|
||||
icon : 'fas fa-dice-four',
|
||||
gen : dedent `\n{{tocDepthH4
|
||||
}}\n`,
|
||||
},
|
||||
{
|
||||
name : 'Include in ToC up to H5',
|
||||
icon : 'fas fa-dice-five',
|
||||
gen : dedent `\n{{tocDepthH5
|
||||
}}\n`,
|
||||
},
|
||||
{
|
||||
name : 'Include in ToC up to H6',
|
||||
icon : 'fas fa-dice-six',
|
||||
gen : dedent `\n{{tocDepthH6
|
||||
}}\n`,
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name : 'Index',
|
||||
icon : 'fas fa-bars',
|
||||
gen : indexGen,
|
||||
experimental : true
|
||||
},
|
||||
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -153,7 +206,7 @@ module.exports = [
|
||||
name : 'Add Comment',
|
||||
icon : 'fas fa-code',
|
||||
gen : '/* This is a comment that will not be rendered into your brew. */'
|
||||
},
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -438,6 +491,15 @@ module.exports = [
|
||||
icon : 'fas fa-print',
|
||||
view : 'style',
|
||||
snippets : [
|
||||
{
|
||||
name : 'US Letter Page Size',
|
||||
icon : 'far fa-file',
|
||||
gen : dedent`/* US Letter Page Size */
|
||||
.page {
|
||||
width : 215.9mm; /* 8.5in */
|
||||
height : 279.4mm; /* 11in */
|
||||
}\n\n`,
|
||||
},
|
||||
{
|
||||
name : 'A3 Page Size',
|
||||
icon : 'far fa-file',
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
@import (less) './themes/fonts/iconFonts/diceFont.less';
|
||||
@import (less) './themes/fonts/iconFonts/gameIcons.less';
|
||||
@import (less) './themes/fonts/iconFonts/fontAwesome.less';
|
||||
@import (less) './themes/fonts/Journal/fonts.less';
|
||||
|
||||
:root {
|
||||
//Colors
|
||||
@@ -21,9 +22,9 @@ body { counter-reset : page-numbers 0; }
|
||||
// *****************************/
|
||||
.page {
|
||||
.block {
|
||||
break-inside : avoid;
|
||||
display : inline-block;
|
||||
width : 100%;
|
||||
break-inside : avoid;
|
||||
img { z-index : 0; }
|
||||
}
|
||||
.inline-block {
|
||||
@@ -51,15 +52,15 @@ body { counter-reset : page-numbers 0; }
|
||||
width : 215.9mm;
|
||||
height : 279.4mm;
|
||||
padding : 1.4cm 1.9cm 1.7cm;
|
||||
overflow : hidden;
|
||||
overflow : clip;
|
||||
background-color : var(--HB_Color_Background);
|
||||
text-rendering : optimizeLegibility;
|
||||
contain : strict;
|
||||
content-visibility : auto;
|
||||
contain-intrinsic-size : auto none;
|
||||
}
|
||||
//*****************************
|
||||
// * BASE
|
||||
//*****************************
|
||||
// * BASE
|
||||
// *****************************/
|
||||
.page {
|
||||
p {
|
||||
@@ -120,7 +121,7 @@ body { counter-reset : page-numbers 0; }
|
||||
// * CODE BLOCKS
|
||||
// ************************************/
|
||||
code {
|
||||
font-family : 'Courier New', "Courier", monospace;
|
||||
font-family : 'Courier New', 'Courier', monospace;
|
||||
overflow-wrap : break-word;
|
||||
white-space : pre-wrap;
|
||||
}
|
||||
@@ -133,10 +134,10 @@ body { counter-reset : page-numbers 0; }
|
||||
// * EXTRAS
|
||||
// *****************************/
|
||||
.columnSplit {
|
||||
margin-top : 0;
|
||||
visibility : hidden;
|
||||
-webkit-column-break-after : always;
|
||||
margin-top : 0;
|
||||
break-after : always;
|
||||
-webkit-column-break-after : always;
|
||||
-moz-column-break-after : always;
|
||||
& + * { margin-top : 0; }
|
||||
}
|
||||
@@ -199,11 +200,11 @@ body { counter-reset : page-numbers 0; }
|
||||
background-color : var(--HB_Color_WatercolorStain); /* default color */
|
||||
background-size : cover;
|
||||
-webkit-mask-image : var(--wc);
|
||||
-webkit-mask-size : contain;
|
||||
-webkit-mask-repeat : no-repeat;
|
||||
mask-image : var(--wc);
|
||||
mask-size : contain;
|
||||
-webkit-mask-repeat : no-repeat;
|
||||
mask-repeat : no-repeat;
|
||||
-webkit-mask-size : contain;
|
||||
mask-size : contain;
|
||||
--wc : @watercolor1; /* default image */
|
||||
}
|
||||
|
||||
@@ -231,15 +232,15 @@ body { counter-reset : page-numbers 0; }
|
||||
height : 200%;
|
||||
background-image : var(--checkerboard);
|
||||
background-size : 20px;
|
||||
transform : translateY(50%) translateX(-50%) rotate(calc(1deg * var(--rotation))) scaleX(var(--scaleX)) scaleY(var(--scaleY));
|
||||
-webkit-mask-image : var(--wc), var(--revealer);
|
||||
-webkit-mask-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);
|
||||
-webkit-mask-repeat : repeat-x;
|
||||
mask-repeat : repeat-x;
|
||||
mask-size : 50%;
|
||||
-webkit-mask-position : 50% calc(50% - var(--offset));
|
||||
mask-position : 50% calc(50% - var(--offset));
|
||||
-webkit-mask-size : 50%; //Scale only X to fit page width, leave height at aspect ratio, designed to hang off the edge
|
||||
mask-size : 50%;
|
||||
transform : translateY(50%) translateX(-50%) rotate(calc(1deg * var(--rotation))) scaleX(var(--scaleX)) scaleY(var(--scaleY));
|
||||
--rotation : 0;
|
||||
--revealer : none;
|
||||
--checkerboard : none;
|
||||
@@ -276,19 +277,19 @@ body { counter-reset : page-numbers 0; }
|
||||
}
|
||||
&.revealImage {
|
||||
--revealer : linear-gradient(0deg, rgba(0,0,0,0.2) 0%, rgba(0,0,0,0.2));
|
||||
--checkerboard : url("/assets/waterColorMasks/missingImage.png"); //shows any masked regions not filled by image
|
||||
--checkerboard : url('/assets/waterColorMasks/missingImage.png'); //shows any masked regions not filled by image
|
||||
}
|
||||
}
|
||||
|
||||
.imageMaskEdge {
|
||||
&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"); }
|
||||
&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'] {
|
||||
@@ -296,15 +297,15 @@ body { counter-reset : page-numbers 0; }
|
||||
left : calc(var(--offsetX));
|
||||
width : 100%;
|
||||
height : 100%;
|
||||
transform : rotate(calc(1deg * var(--rotation))) scaleX(var(--scaleX)) scaleY(var(--scaleY));
|
||||
-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);
|
||||
-webkit-mask-repeat : no-repeat;
|
||||
mask-repeat : no-repeat;
|
||||
mask-size : 100% 100%; //Scale both dimensions to fit page size
|
||||
-webkit-mask-position : 0% 0%;
|
||||
mask-position : 50% 50%;
|
||||
-webkit-mask-size : 100% 100%; //Scale both dimensions to fit page size
|
||||
mask-size : 100% 100%; //Scale both dimensions to fit page size
|
||||
transform : rotate(calc(1deg * var(--rotation))) scaleX(var(--scaleX)) scaleY(var(--scaleY));
|
||||
|
||||
& > p:has(img) {
|
||||
position : absolute;
|
||||
@@ -321,23 +322,23 @@ body { counter-reset : page-numbers 0; }
|
||||
}
|
||||
|
||||
.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"); }
|
||||
&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'); }
|
||||
}
|
||||
|
||||
|
||||
@@ -346,15 +347,15 @@ body { counter-reset : page-numbers 0; }
|
||||
left : calc(-50% + var(--offsetX));
|
||||
width : 200%;
|
||||
height : 200%;
|
||||
transform : rotate(calc(1deg * var(--rotation))) scaleX(var(--scaleX)) scaleY(var(--scaleY));
|
||||
-webkit-mask-image : var(--wc), var(--revealer);
|
||||
-webkit-mask-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);
|
||||
-webkit-mask-repeat : no-repeat;
|
||||
mask-repeat : no-repeat;
|
||||
mask-size : 100% 100%; //Scale both dimensions to fit page size
|
||||
-webkit-mask-position : 50% 50%;
|
||||
mask-position : 50% 50%;
|
||||
-webkit-mask-size : 100% 100%; //Scale both dimensions to fit page size
|
||||
mask-size : 100% 100%; //Scale both dimensions to fit page size
|
||||
transform : rotate(calc(1deg * var(--rotation))) scaleX(var(--scaleX)) scaleY(var(--scaleY));
|
||||
& > p:has(img) {
|
||||
bottom : 25%;
|
||||
left : 25%;
|
||||
@@ -367,43 +368,43 @@ body { counter-reset : page-numbers 0; }
|
||||
}
|
||||
}
|
||||
.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"); }
|
||||
&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'); }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -437,6 +438,11 @@ body { counter-reset : page-numbers 0; }
|
||||
margin-bottom : 1em;
|
||||
& + * { margin-top : 0; }
|
||||
}
|
||||
.blank {
|
||||
height : 1em;
|
||||
margin-top : 0;
|
||||
& + * { margin-top : 0; }
|
||||
}
|
||||
}
|
||||
|
||||
//*****************************
|
||||
@@ -461,8 +467,8 @@ body { counter-reset : page-numbers 0; }
|
||||
height : 1.5cm;
|
||||
margin : 0 auto;
|
||||
background-color : black;
|
||||
-webkit-mask : url("/assets/naturalCritLogoWhite.svg") center / contain no-repeat;
|
||||
mask : url("/assets/naturalCritLogoWhite.svg") center / contain no-repeat;
|
||||
-webkit-mask : url('/assets/naturalCritLogoWhite.svg') center / contain no-repeat;
|
||||
mask : url('/assets/naturalCritLogoWhite.svg') center / contain no-repeat;
|
||||
}
|
||||
.homebreweryIcon.red { background-color : red; }
|
||||
.homebreweryIcon.gold { background-image : linear-gradient(to top left, brown 22.5%, gold 40%, white 60%, gold 67.5%, brown 82.5%); }
|
||||
@@ -486,12 +492,122 @@ body { counter-reset : page-numbers 0; }
|
||||
.pageNumber { left : 30px; }
|
||||
}
|
||||
|
||||
.resetCounting {
|
||||
counter-set : page-numbers 1;
|
||||
}
|
||||
.resetCounting { counter-set : page-numbers 1; }
|
||||
|
||||
&:not(:has(.skipCounting)) {
|
||||
counter-increment : page-numbers;
|
||||
}
|
||||
&:not(:has(.skipCounting)) { counter-increment : page-numbers; }
|
||||
|
||||
}
|
||||
|
||||
// *****************************
|
||||
// * INDEX
|
||||
// *****************************/
|
||||
.page {
|
||||
.index {
|
||||
|
||||
ul ul { margin : 0; }
|
||||
|
||||
ul {
|
||||
padding-left : 0;
|
||||
text-indent : 0;
|
||||
list-style-type : none;
|
||||
}
|
||||
|
||||
& > ul > li {
|
||||
padding-left : 1.5em;
|
||||
text-indent : -1.5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// *****************************
|
||||
// * TABLE OF CONTENTS
|
||||
// *****************************/
|
||||
|
||||
// Default Exclusions
|
||||
// Anything not exlcuded is included, default Headers are H1, H2, and H3.
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
.page:has(.frontCover),
|
||||
.page:has(.backCover),
|
||||
.page:has(.insideCover),
|
||||
.noToC,
|
||||
.toc { --TOC : exclude; }
|
||||
|
||||
.tocDepthH2 :is(h1, h2) {--TOC : include; }
|
||||
.tocDepthH3 :is(h1, h2, h3) {--TOC : include; }
|
||||
.tocDepthH4 :is(h1, h2, h3, h4) {--TOC : include; }
|
||||
.tocDepthH5 :is(h1, h2, h3, h4, h5) {--TOC : include; }
|
||||
.tocDepthH6 :is(h1, h2, h3, h4, h5, h6) {--TOC : include; }
|
||||
|
||||
.tocIncludeH1 h1 {--TOC : include; }
|
||||
.tocIncludeH2 h2 {--TOC : include; }
|
||||
.tocIncludeH3 h3 {--TOC : include; }
|
||||
.tocIncludeH4 h4 {--TOC : include; }
|
||||
.tocIncludeH5 h5 {--TOC : include; }
|
||||
.tocIncludeH6 h6 {--TOC : include; }
|
||||
|
||||
.page {
|
||||
&:has(.toc)::after { display : none; }
|
||||
.toc {
|
||||
-webkit-column-break-inside : avoid;
|
||||
page-break-inside : avoid;
|
||||
break-inside : avoid;
|
||||
h1 {
|
||||
margin-bottom : 0.3cm;
|
||||
text-align : center;
|
||||
}
|
||||
a {
|
||||
display : inline;
|
||||
color : inherit;
|
||||
text-decoration : none;
|
||||
&:hover { text-decoration : underline; }
|
||||
}
|
||||
h4 {
|
||||
margin-top : 0.2cm;
|
||||
line-height : 0.4cm;
|
||||
& + ul li { line-height : 1.2em; }
|
||||
}
|
||||
ul {
|
||||
padding-left : 0;
|
||||
margin-top : 0;
|
||||
list-style-type : none;
|
||||
a {
|
||||
display : flex;
|
||||
flex-flow : row nowrap;
|
||||
justify-content : space-between;
|
||||
width : 100%;
|
||||
}
|
||||
li + li h3 {
|
||||
margin-top : 0.26cm;
|
||||
line-height : 1em;
|
||||
}
|
||||
h3 span:first-child::after { border : none; }
|
||||
span {
|
||||
display : contents;
|
||||
&:first-child::after {
|
||||
bottom : 0.08cm;
|
||||
flex : 1;
|
||||
margin-right : 0.16cm;
|
||||
margin-bottom : 0.08cm;
|
||||
margin-left : 0.08cm; /* Spacing before dot leaders */
|
||||
content : '';
|
||||
border-bottom : 0.05cm dotted #000000;
|
||||
}
|
||||
&:last-child {
|
||||
display : inline-block;
|
||||
align-self : flex-end;
|
||||
font-size : 0.34cm;
|
||||
font-weight : normal;
|
||||
}
|
||||
}
|
||||
ul { /* List indent */
|
||||
margin-left : 1em;
|
||||
}
|
||||
}
|
||||
&.wide {
|
||||
.useColumns(0.96, @fillMode: balance);
|
||||
}
|
||||
}
|
||||
.toc.wide li { break-inside : auto; }
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* eslint-disable max-lines */
|
||||
|
||||
|
||||
module.exports = [
|
||||
];
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user