mirror of
https://github.com/naturalcrit/homebrewery.git
synced 2026-01-27 20:23:08 +00:00
Compare commits
854 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eeec24ae78 | ||
|
|
1d778e3249 | ||
|
|
3bb44d8a17 | ||
|
|
71c52b4587 | ||
|
|
fe449abb47 | ||
|
|
46d1f89b77 | ||
|
|
bf1f2054de | ||
|
|
399caaaeff | ||
|
|
27b4176e23 | ||
|
|
a8bc6b4e1d | ||
|
|
3ca8f72762 | ||
|
|
8aec5dbba6 | ||
|
|
cccebd8494 | ||
|
|
5fbbd92ea7 | ||
|
|
81f26e0892 | ||
|
|
9366284e1d | ||
|
|
9aa5eea8c9 | ||
|
|
20f61bff07 | ||
|
|
625819da91 | ||
|
|
2c691d84f2 | ||
|
|
4630d2640b | ||
|
|
043f24d5ca | ||
|
|
87e18c0521 | ||
|
|
7e30f860b2 | ||
|
|
0ac0ffe53d | ||
|
|
ae4e1b55e6 | ||
|
|
756ced088c | ||
|
|
8ce6b22be7 | ||
|
|
37c72d5125 | ||
|
|
602a476b59 | ||
|
|
965397733d | ||
|
|
f10868db0b | ||
|
|
d7a95d3cff | ||
|
|
6eeed49022 | ||
|
|
be96c3a56f | ||
|
|
57a31f3b71 | ||
|
|
7c5955c96f | ||
|
|
538650bf92 | ||
|
|
54921a998a | ||
|
|
6184d64f89 | ||
|
|
da11089fff | ||
|
|
0243d138ff | ||
|
|
4c8a5baee5 | ||
|
|
e5acbfed3a | ||
|
|
0163e22567 | ||
|
|
b503b8fc9b | ||
|
|
3243e4d56c | ||
|
|
fbc164a9b8 | ||
|
|
78cf95fbb1 | ||
|
|
d2306b70a9 | ||
|
|
43180c314f | ||
|
|
f47c2dcb56 | ||
|
|
a4ea1612c1 | ||
|
|
c00c2626b4 | ||
|
|
8b6517eb8d | ||
|
|
067a7cd507 | ||
|
|
89fddd0210 | ||
|
|
0c167d803c | ||
|
|
c50042c1e7 | ||
|
|
0dc1b46466 | ||
|
|
4ed9fc7d0e | ||
|
|
162929bdca | ||
|
|
54a2f6940c | ||
|
|
0243b5f491 | ||
|
|
0dff59d793 | ||
|
|
31c7fd12b9 | ||
|
|
7951c4a03a | ||
|
|
7b8aaa408d | ||
|
|
1c65ee150b | ||
|
|
66fd56fccb | ||
|
|
b0cfeaa782 | ||
|
|
da699e999f | ||
|
|
c6a5f50c76 | ||
|
|
74c7395ab9 | ||
|
|
8fc6047127 | ||
|
|
62e679571e | ||
|
|
f32c52a460 | ||
|
|
edbe8cdace | ||
|
|
e53e00713d | ||
|
|
6beee49ebc | ||
|
|
eaf3c7978d | ||
|
|
d36e052478 | ||
|
|
099ea08bd4 | ||
|
|
b0ea34cc3f | ||
|
|
a241813b8d | ||
|
|
de27437148 | ||
|
|
0c010a0a87 | ||
|
|
0c6c9fce4d | ||
|
|
ac9fc720f7 | ||
|
|
8b31966c2b | ||
|
|
76d966f17d | ||
|
|
d0d8b268d4 | ||
|
|
8bd8f0fc37 | ||
|
|
a2acf3be0e | ||
|
|
844a3c19ec | ||
|
|
877c2e365f | ||
|
|
81c7950ad1 | ||
|
|
7b8e3da90a | ||
|
|
eef3d7738e | ||
|
|
b8ca837c02 | ||
|
|
1fc3573087 | ||
|
|
5ab89b2583 | ||
|
|
539cd1d2b9 | ||
|
|
4424a331d5 | ||
|
|
c4defb7b3f | ||
|
|
c1e17bb6aa | ||
|
|
d648bacd26 | ||
|
|
1ca5ba1086 | ||
|
|
7c28f60e0a | ||
|
|
2fd2ccfe14 | ||
|
|
69308cfd8b | ||
|
|
ccc1895304 | ||
|
|
05a0d80c1b | ||
|
|
2ea65de0c0 | ||
|
|
742798ad79 | ||
|
|
f088fc49f3 | ||
|
|
b837ac5d6b | ||
|
|
64af24f0f4 | ||
|
|
5abb1db512 | ||
|
|
f1133b9c33 | ||
|
|
c73d02c550 | ||
|
|
f1d26cc0c0 | ||
|
|
077d699f0b | ||
|
|
2ed8614642 | ||
|
|
9834fcb97f | ||
|
|
347393520d | ||
|
|
e65e12ff6e | ||
|
|
4dfe1a0914 | ||
|
|
1a1acec2f3 | ||
|
|
e6276a0c7b | ||
|
|
84a0c43745 | ||
|
|
baed640a3d | ||
|
|
7d6851572f | ||
|
|
3e2ec7cd36 | ||
|
|
db3f9a45ad | ||
|
|
8fc2737670 | ||
|
|
0c2e4ce20b | ||
|
|
62948b2838 | ||
|
|
3dfb3a9738 | ||
|
|
42c1bece65 | ||
|
|
448c12cc91 | ||
|
|
56c82f8793 | ||
|
|
84b0407f74 | ||
|
|
ad1795258b | ||
|
|
78c26ab1a3 | ||
|
|
eb669e6eca | ||
|
|
17355012fb | ||
|
|
eae593ce90 | ||
|
|
7a8a6480de | ||
|
|
d0dd61a25c | ||
|
|
dbf0559f95 | ||
|
|
8e88b881fc | ||
|
|
c722e0af39 | ||
|
|
6dcdc1b685 | ||
|
|
76d17baf7e | ||
|
|
c97e2be9d5 | ||
|
|
229acbfcd1 | ||
|
|
2cb216ed7b | ||
|
|
8674bc9da2 | ||
|
|
199e049871 | ||
|
|
31f18ef3d5 | ||
|
|
9db55c4dff | ||
|
|
e2ac6c9b6b | ||
|
|
407c35d9f7 | ||
|
|
8575d72f6e | ||
|
|
7b8e398891 | ||
|
|
89c5c3f255 | ||
|
|
a65c24bebf | ||
|
|
9ed32527a6 | ||
|
|
62982f86a1 | ||
|
|
985a9843f2 | ||
|
|
3bbcb1b6fb | ||
|
|
9da203d204 | ||
|
|
212b3f7e05 | ||
|
|
7a06fe386d | ||
|
|
4580217410 | ||
|
|
e82ba8cb7a | ||
|
|
636f10cb41 | ||
|
|
d6eaa812b1 | ||
|
|
507f170720 | ||
|
|
faaf4207b4 | ||
|
|
688377ce0b | ||
|
|
8cf57dbc72 | ||
|
|
12012a2a5b | ||
|
|
9b59f47536 | ||
|
|
90b4e47861 | ||
|
|
ef00231c5b | ||
|
|
c39653bc69 | ||
|
|
83faa86063 | ||
|
|
af20d0b1c2 | ||
|
|
1861c7db69 | ||
|
|
a88b256b6c | ||
|
|
40542e9bec | ||
|
|
04187cf769 | ||
|
|
67a4391dcb | ||
|
|
3784e0f583 | ||
|
|
df33713f82 | ||
|
|
c4ba6381f2 | ||
|
|
547daf6499 | ||
|
|
649e225359 | ||
|
|
35bf26feae | ||
|
|
481bab9463 | ||
|
|
a0e2997a40 | ||
|
|
74c4f4fe52 | ||
|
|
7c9513f377 | ||
|
|
c10be139c9 | ||
|
|
ee4921f02c | ||
|
|
f5f6137a4d | ||
|
|
0775f9ee1b | ||
|
|
75ed555de1 | ||
|
|
e7d0741139 | ||
|
|
c69de1036f | ||
|
|
7d115c970a | ||
|
|
852aa8d289 | ||
|
|
ae974b270d | ||
|
|
ef10b71e56 | ||
|
|
5bfd0dd537 | ||
|
|
05e56221f4 | ||
|
|
5c5230e64e | ||
|
|
769f636db2 | ||
|
|
688eca05e1 | ||
|
|
4c48992331 | ||
|
|
fcb9a8cdc5 | ||
|
|
78d4d6fb7c | ||
|
|
407efd0f8b | ||
|
|
d39ae139d7 | ||
|
|
467b728c47 | ||
|
|
b02036fb7a | ||
|
|
e99aad15c1 | ||
|
|
c57b011215 | ||
|
|
5942bfece1 | ||
|
|
2dc874daba | ||
|
|
38fa428fde | ||
|
|
6bf51cd94a | ||
|
|
ddef21cd7e | ||
|
|
bf30cadb68 | ||
|
|
abe3a7e7c7 | ||
|
|
4ec5f73aed | ||
|
|
a5313deb78 | ||
|
|
5dd486866f | ||
|
|
ada642e56e | ||
|
|
881fcc9cba | ||
|
|
a809e920fc | ||
|
|
36f3eb4da1 | ||
|
|
57772065e0 | ||
|
|
f10be94190 | ||
|
|
055ee38cb7 | ||
|
|
202b275966 | ||
|
|
39e33da2d1 | ||
|
|
71f1aed227 | ||
|
|
159d5a35b2 | ||
|
|
3073b3e35d | ||
|
|
0491516662 | ||
|
|
b55616170b | ||
|
|
e080c46509 | ||
|
|
171d1c7c46 | ||
|
|
d1503c8d6f | ||
|
|
be72b029bf | ||
|
|
93a7b11017 | ||
|
|
045fbbe158 | ||
|
|
ede731e3a5 | ||
|
|
d6e63604ac | ||
|
|
960c03bfa6 | ||
|
|
cda98e02e0 | ||
|
|
9ac070512d | ||
|
|
a1e9c82c06 | ||
|
|
b116e0a622 | ||
|
|
ef2bbfea5e | ||
|
|
c858c705d2 | ||
|
|
fb91761c31 | ||
|
|
93d38eb184 | ||
|
|
d31dae728f | ||
|
|
c068aca9ff | ||
|
|
fcdaef2445 | ||
|
|
8d94e5fbe0 | ||
|
|
c6d8bbae16 | ||
|
|
0a54c7b04e | ||
|
|
2fd54ee87e | ||
|
|
1f5f160964 | ||
|
|
8d04f09aab | ||
|
|
be1f905b48 | ||
|
|
dbbd8cb26d | ||
|
|
7b85995b4a | ||
|
|
37593573ce | ||
|
|
0bdadaf946 | ||
|
|
7b1a815e78 | ||
|
|
7b9a23670d | ||
|
|
a762626c53 | ||
|
|
da2c647fa6 | ||
|
|
cb61badfc5 | ||
|
|
d2f909384e | ||
|
|
cebc7be81d | ||
|
|
83c604cb74 | ||
|
|
ad455f652c | ||
|
|
3f19b2975c | ||
|
|
cacfc788fb | ||
|
|
8f99712a28 | ||
|
|
367c2bd111 | ||
|
|
1c45cb1b7f | ||
|
|
19fbd832f1 | ||
|
|
7ce0ac577a | ||
|
|
3ce4d6d1f8 | ||
|
|
18aca07e84 | ||
|
|
4bda071742 | ||
|
|
35bde09aa7 | ||
|
|
d43ea46e40 | ||
|
|
f1ca6eeee2 | ||
|
|
d390d518a3 | ||
|
|
837306c9a7 | ||
|
|
c58c8777f1 | ||
|
|
caadb7b4ce | ||
|
|
ce946bda98 | ||
|
|
07b1254309 | ||
|
|
4c7715286e | ||
|
|
980c544bba | ||
|
|
7c0b9ea3f6 | ||
|
|
e6d8784633 | ||
|
|
4720aced6c | ||
|
|
cf5e61cf09 | ||
|
|
9d43588f44 | ||
|
|
ce005da20f | ||
|
|
17531151ad | ||
|
|
863f624772 | ||
|
|
6a5f4efd26 | ||
|
|
6200b416ab | ||
|
|
dc1931a5e3 | ||
|
|
26bddc1a79 | ||
|
|
74ebd44d7c | ||
|
|
989af1bbd0 | ||
|
|
3359e489f5 | ||
|
|
f431ac2e40 | ||
|
|
511c38dd1e | ||
|
|
85b0976082 | ||
|
|
7052337669 | ||
|
|
e07d1d1ddb | ||
|
|
c5ebd0352d | ||
|
|
a8fbcf0ad1 | ||
|
|
92f33136ce | ||
|
|
9260283914 | ||
|
|
a08263dd7c | ||
|
|
7eae170f6c | ||
|
|
b5cb8ce834 | ||
|
|
d497c0094b | ||
|
|
71141aa6f6 | ||
|
|
36e607eb3b | ||
|
|
d67f206900 | ||
|
|
6bf9ddd585 | ||
|
|
1607a1ac10 | ||
|
|
392a3db3c2 | ||
|
|
0845234f2f | ||
|
|
f58a7d65b5 | ||
|
|
3ac0ac7568 | ||
|
|
1c71ff0945 | ||
|
|
3ccfef0763 | ||
|
|
a6ec8370be | ||
|
|
7b059f029d | ||
|
|
917b63722c | ||
|
|
c1cbbe0047 | ||
|
|
963d76add9 | ||
|
|
e055734d03 | ||
|
|
7c16805680 | ||
|
|
be52e0ecd9 | ||
|
|
175673cdf9 | ||
|
|
a279c75826 | ||
|
|
8be95e7d04 | ||
|
|
8e0c1d78dc | ||
|
|
5da73f8ff8 | ||
|
|
e20fc680e9 | ||
|
|
e821ddac93 | ||
|
|
fab2c2cead | ||
|
|
bd0e142999 | ||
|
|
673649abc4 | ||
|
|
b95a2189a5 | ||
|
|
f45547d899 | ||
|
|
f2c970fb79 | ||
|
|
6e6a6dd1e3 | ||
|
|
b2fc020d81 | ||
|
|
3bfa3bac3a | ||
|
|
00134c6c3d | ||
|
|
6a393a1437 | ||
|
|
115be2813d | ||
|
|
e4a46c84ec | ||
|
|
b994bf269a | ||
|
|
2de7ec3585 | ||
|
|
efbfbf3568 | ||
|
|
12ca82e6e6 | ||
|
|
e7ce8d73bb | ||
|
|
a420f202d8 | ||
|
|
3628ca837a | ||
|
|
cc3a42402a | ||
|
|
1e9b71080b | ||
|
|
90917bb84c | ||
|
|
7f6e90fee3 | ||
|
|
b8d8c1bebb | ||
|
|
7ee0e914e6 | ||
|
|
08dbd5638d | ||
|
|
b7d7f4f2a0 | ||
|
|
3cf6691e67 | ||
|
|
8077a91ff7 | ||
|
|
09670b8535 | ||
|
|
36f8f39486 | ||
|
|
1556cf361a | ||
|
|
c6ef051232 | ||
|
|
2c130d1943 | ||
|
|
f1e6a9a41e | ||
|
|
b557144f63 | ||
|
|
738fc62b8f | ||
|
|
34edd436e7 | ||
|
|
0b13e90dd8 | ||
|
|
52dcc3b53c | ||
|
|
6803db268f | ||
|
|
99c45b6cc3 | ||
|
|
4cf8e620ee | ||
|
|
fb4c33545c | ||
|
|
68475a6aa0 | ||
|
|
e5140bd5b4 | ||
|
|
e3c05c83ba | ||
|
|
b374f06718 | ||
|
|
ea8c93d39d | ||
|
|
6dd8ccff90 | ||
|
|
ca90a22c64 | ||
|
|
abc4851375 | ||
|
|
a155c10d46 | ||
|
|
5247f5c87b | ||
|
|
e41c1ef0bd | ||
|
|
4d6b428f93 | ||
|
|
6928bd9fb4 | ||
|
|
f2855aca85 | ||
|
|
aeef595ea9 | ||
|
|
1a24709da0 | ||
|
|
8c11f47c1f | ||
|
|
486fbf32b2 | ||
|
|
8ac3cdcf9d | ||
|
|
f0fc0bcb6d | ||
|
|
63a5e9f817 | ||
|
|
5456f4f197 | ||
|
|
d75ea8943b | ||
|
|
07ae1539aa | ||
|
|
2125b8a026 | ||
|
|
b3522bddf1 | ||
|
|
9dc19d996d | ||
|
|
216de73c93 | ||
|
|
a306030635 | ||
|
|
3e9bea3761 | ||
|
|
73e909c4c8 | ||
|
|
53fa6af5f9 | ||
|
|
4d2edf81a9 | ||
|
|
b10b33830a | ||
|
|
393ac69581 | ||
|
|
1098f6da70 | ||
|
|
a29fed3c89 | ||
|
|
7af3a629f9 | ||
|
|
b0a9765819 | ||
|
|
86d18ea0d9 | ||
|
|
bfe278c81c | ||
|
|
7870c763df | ||
|
|
7f1758364b | ||
|
|
46a6ed4fcc | ||
|
|
7efd23039e | ||
|
|
9e5103a0c7 | ||
|
|
7281b6e43c | ||
|
|
3f6eb7371f | ||
|
|
2b0bbfc2db | ||
|
|
e909bc8f35 | ||
|
|
e16110da6a | ||
|
|
fcd15e6d9c | ||
|
|
3f828c8649 | ||
|
|
bb66cffa13 | ||
|
|
da5a5631ad | ||
|
|
3e43b058a5 | ||
|
|
a30c2fa1f7 | ||
|
|
cb203c29c9 | ||
|
|
e50d7b8882 | ||
|
|
74d6aa7c8a | ||
|
|
e86686807b | ||
|
|
f4356025de | ||
|
|
73b141d08c | ||
|
|
9196ffc480 | ||
|
|
4e18bb047e | ||
|
|
e4b4e34216 | ||
|
|
d38c1b9ab2 | ||
|
|
deaaafd9d2 | ||
|
|
9b33bf9855 | ||
|
|
e5346d3a6e | ||
|
|
d9ca20e17d | ||
|
|
a5cb3f085f | ||
|
|
6d28948387 | ||
|
|
65c75b3282 | ||
|
|
a0f22e31b7 | ||
|
|
c750eebc11 | ||
|
|
93a93f1907 | ||
|
|
29428b81f6 | ||
|
|
a542953cec | ||
|
|
183ca51753 | ||
|
|
220816a172 | ||
|
|
840b075c8e | ||
|
|
3ba15a068a | ||
|
|
b99de1c6e1 | ||
|
|
73a48501e0 | ||
|
|
599d39b69d | ||
|
|
ed18ba3108 | ||
|
|
6d93291d5b | ||
|
|
3fef61cbf8 | ||
|
|
c5d9c3bdc0 | ||
|
|
6c7af2d968 | ||
|
|
6130d69906 | ||
|
|
8c975747c4 | ||
|
|
ef6dab24a2 | ||
|
|
1f09fff94b | ||
|
|
7bcd898c81 | ||
|
|
186809008c | ||
|
|
b6e11ba607 | ||
|
|
355b8ac78f | ||
|
|
a2c20a0f7a | ||
|
|
117e399c1d | ||
|
|
a3ac524308 | ||
|
|
1e9ba31644 | ||
|
|
751728d134 | ||
|
|
8e05cdbb43 | ||
|
|
a5483c549b | ||
|
|
746f3d35f8 | ||
|
|
94c902bf38 | ||
|
|
0da1c43dc3 | ||
|
|
32417e92ff | ||
|
|
540dee89dd | ||
|
|
1854080771 | ||
|
|
45207b8114 | ||
|
|
2b3c2c9fac | ||
|
|
0b953fcbf3 | ||
|
|
0a3453d228 | ||
|
|
6b49e720ca | ||
|
|
7feaa51de0 | ||
|
|
1729b13574 | ||
|
|
73832fabcc | ||
|
|
bac537244c | ||
|
|
54f8bb4b08 | ||
|
|
38f6929c1d | ||
|
|
64ce9ecfa6 | ||
|
|
858990c4bd | ||
|
|
785011cba4 | ||
|
|
50d3b503d9 | ||
|
|
2f8e2545c6 | ||
|
|
d8574e7045 | ||
|
|
b3497e14f1 | ||
|
|
d6e0047d4e | ||
|
|
8ebd5ccff9 | ||
|
|
4a927daff3 | ||
|
|
d28e85209e | ||
|
|
cca882869d | ||
|
|
9ae55c87a9 | ||
|
|
99ad96a584 | ||
|
|
f8b42031fb | ||
|
|
850b52d924 | ||
|
|
b2e7b28b65 | ||
|
|
3efa7dd0be | ||
|
|
677e27bb66 | ||
|
|
da71bd7a10 | ||
|
|
0869f6b29b | ||
|
|
0da5de494e | ||
|
|
bc9dc8dee9 | ||
|
|
e3e250255e | ||
|
|
787a23bdf8 | ||
|
|
7aca0f2f10 | ||
|
|
999f9c8f25 | ||
|
|
ee4b2d549b | ||
|
|
3eb7ce2775 | ||
|
|
4d4371f48c | ||
|
|
47666cc26d | ||
|
|
e8352d996e | ||
|
|
e6f792900c | ||
|
|
0ddeafd260 | ||
|
|
4cf54d6ae8 | ||
|
|
8d3329069a | ||
|
|
2e13eed2ef | ||
|
|
310faa449d | ||
|
|
8527a976a6 | ||
|
|
54460c52f6 | ||
|
|
e81bd2a0d2 | ||
|
|
6c0daa1e4d | ||
|
|
25d03faae2 | ||
|
|
378b2204da | ||
|
|
447b0939f2 | ||
|
|
0bde336226 | ||
|
|
73e44b8d7a | ||
|
|
53fbaf87c2 | ||
|
|
a8db7353b0 | ||
|
|
bda8037cd6 | ||
|
|
1f173814ec | ||
|
|
deb0e2f85b | ||
|
|
5425ae5d15 | ||
|
|
f03f2c36de | ||
|
|
a1e663bc32 | ||
|
|
16c842e08f | ||
|
|
1c0edce6f6 | ||
|
|
8ec6e66c92 | ||
|
|
330cdb35f4 | ||
|
|
f93fbab754 | ||
|
|
b9498e49fc | ||
|
|
4a434bb161 | ||
|
|
a339cb036f | ||
|
|
91eef51fb5 | ||
|
|
46b050ae7d | ||
|
|
b433691596 | ||
|
|
cef7f98176 | ||
|
|
b869d086ea | ||
|
|
64a361e06c | ||
|
|
9e7e646296 | ||
|
|
7274d788c5 | ||
|
|
7942f1caed | ||
|
|
343e176b83 | ||
|
|
3db5959cfa | ||
|
|
62f9781c8e | ||
|
|
9d53002874 | ||
|
|
4e7e6f8105 | ||
|
|
cea1157eb2 | ||
|
|
d535328eb8 | ||
|
|
627e9ec7d8 | ||
|
|
ab9b5b7487 | ||
|
|
957a8bf05c | ||
|
|
509390ae03 | ||
|
|
d3a70c3d75 | ||
|
|
1806854969 | ||
|
|
c2570fec6b | ||
|
|
9d64740678 | ||
|
|
053aadeff4 | ||
|
|
d3763beb15 | ||
|
|
8281051797 | ||
|
|
5b0104fc10 | ||
|
|
b3fa902d85 | ||
|
|
cf3635bccc | ||
|
|
a22d223927 | ||
|
|
5c08926576 | ||
|
|
7aa374e529 | ||
|
|
797ca7e64e | ||
|
|
5b8f2d8e3c | ||
|
|
bd0ef5da48 | ||
|
|
fe324d6822 | ||
|
|
3140299d73 | ||
|
|
96d04ad75a | ||
|
|
62532f788e | ||
|
|
69a3d04bb7 | ||
|
|
21017e45fe | ||
|
|
48474c6f7b | ||
|
|
3d318f8863 | ||
|
|
2b798f4ecb | ||
|
|
938802e1a3 | ||
|
|
14f825f3b5 | ||
|
|
37241a70eb | ||
|
|
bcd86a7f0c | ||
|
|
cf54594a4c | ||
|
|
535fdeaf62 | ||
|
|
77bf1b5258 | ||
|
|
63da418b60 | ||
|
|
67f5e53160 | ||
|
|
c6103d51c5 | ||
|
|
a4a10783f6 | ||
|
|
5ed53f75c5 | ||
|
|
81b289923a | ||
|
|
c3d8364789 | ||
|
|
82fec9901d | ||
|
|
173d0a726b | ||
|
|
e064219ca0 | ||
|
|
ec339f2717 | ||
|
|
d9d27808a8 | ||
|
|
a4584dc78e | ||
|
|
6344eaa17d | ||
|
|
5c41110e50 | ||
|
|
085cb99562 | ||
|
|
568586541a | ||
|
|
0d44e1778f | ||
|
|
4a5269e1f3 | ||
|
|
62cf0a4483 | ||
|
|
07c7352aa2 | ||
|
|
cf6c8bce88 | ||
|
|
9cb8b46930 | ||
|
|
467b6ff8de | ||
|
|
45d32ebfc3 | ||
|
|
84496f51ba | ||
|
|
4cf659e711 | ||
|
|
e0bfef5231 | ||
|
|
afb6962407 | ||
|
|
8d2945ee5c | ||
|
|
1dad009298 | ||
|
|
aadf663623 | ||
|
|
8685f32b49 | ||
|
|
678ac90cd0 | ||
|
|
3cb5e8ed42 | ||
|
|
a41553637a | ||
|
|
636f2f9372 | ||
|
|
4ded080a58 | ||
|
|
a5885c8f4f | ||
|
|
273f0ca05d | ||
|
|
3c929870cb | ||
|
|
4cb2a9ef76 | ||
|
|
36df5a3212 | ||
|
|
cea5f2e43a | ||
|
|
046845885d | ||
|
|
9713cc4be9 | ||
|
|
8baf0fc849 | ||
|
|
a7040e554a | ||
|
|
ba43055f32 | ||
|
|
d0de7ca28c | ||
|
|
c0164dce6a | ||
|
|
9e2b8477a8 | ||
|
|
5a32ae5cd4 | ||
|
|
e88e7f852c | ||
|
|
a3b2c6987f | ||
|
|
3d47b5a0bc | ||
|
|
c5f4793c23 | ||
|
|
10e14bfcfd | ||
|
|
f3c36ffb0a | ||
|
|
cff4f8eae5 | ||
|
|
4799e8b443 | ||
|
|
fa38d5c892 | ||
|
|
04eb7d0556 | ||
|
|
f175323221 | ||
|
|
9f4de3c66e | ||
|
|
800bff611a | ||
|
|
e28b6e7a19 | ||
|
|
4c6de90d82 | ||
|
|
e5ef0aedd3 | ||
|
|
da8e7ec610 | ||
|
|
d1412abe03 | ||
|
|
9de4a82977 | ||
|
|
9ddae7bbea | ||
|
|
4fdc6b79ea | ||
|
|
0001cf16d9 | ||
|
|
438cb7f26d | ||
|
|
ffa240f78d | ||
|
|
782aa8e658 | ||
|
|
7efe8964f1 | ||
|
|
853515e09e | ||
|
|
f6c5354ce0 | ||
|
|
6353341738 | ||
|
|
66b9a792e7 | ||
|
|
2775614eab | ||
|
|
32229c6e6e | ||
|
|
37c88b83f1 | ||
|
|
2fa1b2bb8b | ||
|
|
949d763e35 | ||
|
|
661872f332 | ||
|
|
46cb2e6b5b | ||
|
|
e7224e97ef | ||
|
|
e07d53aa5f | ||
|
|
dbb4476eb4 | ||
|
|
65f55dfc12 | ||
|
|
95322595bf | ||
|
|
1e004977be | ||
|
|
9110e7cf7e | ||
|
|
27e8b54528 | ||
|
|
aa31919563 | ||
|
|
7bf3295fc2 | ||
|
|
9fd3f47689 | ||
|
|
0ca7e43d73 | ||
|
|
b33b3cd49b | ||
|
|
71c384ee0b | ||
|
|
546b8d5725 | ||
|
|
4d6ac2b142 | ||
|
|
ce538ebbfd | ||
|
|
cf17e73dfa | ||
|
|
69ef4d7653 | ||
|
|
c98224f3e4 | ||
|
|
4f870de68f | ||
|
|
2cfee2e8ad | ||
|
|
9e1d53a30c | ||
|
|
1fe9f0c8d0 | ||
|
|
adc7233cab | ||
|
|
1b2fc746d3 | ||
|
|
b472fc1115 | ||
|
|
a7a47afaae | ||
|
|
8c0ca988ae | ||
|
|
509c7d8832 | ||
|
|
caff1d8e2b | ||
|
|
e06f5e17d9 | ||
|
|
ade61971d0 | ||
|
|
e1a22ed76c | ||
|
|
6451d79d92 | ||
|
|
9202f9c8eb | ||
|
|
097cc220f8 | ||
|
|
7e660aad45 | ||
|
|
c8df449aac | ||
|
|
2e3c10c35b | ||
|
|
a5aeb7dccd | ||
|
|
7681be2e9c | ||
|
|
92ff776270 | ||
|
|
bf1fb97789 | ||
|
|
2cddc2debe | ||
|
|
0d4a1a11c1 | ||
|
|
aa3cf1d9c1 | ||
|
|
5d0062f610 | ||
|
|
7976917bb9 | ||
|
|
023071c874 | ||
|
|
7da42d3742 | ||
|
|
3269e94757 | ||
|
|
c69f4289ed | ||
|
|
8752a32626 | ||
|
|
8735d1f222 | ||
|
|
21929e676d | ||
|
|
5ca61935a8 | ||
|
|
10143cec93 | ||
|
|
643c8503c0 | ||
|
|
e92d3ecd68 | ||
|
|
4f092828ac | ||
|
|
2d4c211483 | ||
|
|
7d30abc4d9 | ||
|
|
1d513f7a0e | ||
|
|
44922f5261 | ||
|
|
f695cc6948 | ||
|
|
3722387f1f | ||
|
|
8950cb944f | ||
|
|
66fb70a5f8 | ||
|
|
69c242425b | ||
|
|
9093f610bd | ||
|
|
d2b2e69123 | ||
|
|
052c255068 | ||
|
|
e6ad8aefde | ||
|
|
43ae80e80d | ||
|
|
e6e04ad21d | ||
|
|
7be6b913b0 | ||
|
|
455b364160 | ||
|
|
56dbfc032c | ||
|
|
38c20430a9 | ||
|
|
86823b43b1 | ||
|
|
0abfb23ef2 | ||
|
|
da5d4236b6 | ||
|
|
963236f961 | ||
|
|
2d4a3ec910 | ||
|
|
40afdf18d6 | ||
|
|
13944d3a76 | ||
|
|
3c168065ee | ||
|
|
cf42520305 | ||
|
|
d5ac237d40 | ||
|
|
c666d6acb9 | ||
|
|
2869726efd | ||
|
|
ae9a29c28c | ||
|
|
c660f87dff | ||
|
|
a86c728710 | ||
|
|
b28e183f95 | ||
|
|
dc55880544 | ||
|
|
5e9b451e29 | ||
|
|
977cbeed43 | ||
|
|
da6fcb297a | ||
|
|
b2546d908a | ||
|
|
7dd1368c09 | ||
|
|
51f657d2c5 | ||
|
|
3d71799469 | ||
|
|
fe402bc211 | ||
|
|
dc7e4cd192 | ||
|
|
7fd7230e15 | ||
|
|
c30eb9056a | ||
|
|
ee3cd21486 | ||
|
|
5de63799c2 | ||
|
|
dfa8aea1ec | ||
|
|
8e26161244 |
@@ -5,12 +5,12 @@
|
|||||||
version: 2.1
|
version: 2.1
|
||||||
|
|
||||||
orbs:
|
orbs:
|
||||||
node: circleci/node@3.0.0
|
node: circleci/node@5.1.0
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
docker:
|
docker:
|
||||||
- image: cimg/node:16.11.0
|
- image: cimg/node:20.8.0
|
||||||
- image: mongo:4.4
|
- image: mongo:4.4
|
||||||
|
|
||||||
working_directory: ~/homebrewery
|
working_directory: ~/homebrewery
|
||||||
@@ -27,7 +27,7 @@ jobs:
|
|||||||
# fallback to using the latest cache if no exact match is found
|
# fallback to using the latest cache if no exact match is found
|
||||||
- v1-dependencies-
|
- v1-dependencies-
|
||||||
|
|
||||||
- run: sudo npm install -g npm@8.10.0
|
- run: sudo npm install -g npm@10.2.0
|
||||||
- node/install-packages:
|
- node/install-packages:
|
||||||
app-dir: ~/homebrewery
|
app-dir: ~/homebrewery
|
||||||
cache-path: node_modules
|
cache-path: node_modules
|
||||||
@@ -45,7 +45,7 @@ jobs:
|
|||||||
|
|
||||||
test:
|
test:
|
||||||
docker:
|
docker:
|
||||||
- image: cimg/node:16.11.0
|
- image: cimg/node:20.8.0
|
||||||
|
|
||||||
working_directory: ~/homebrewery
|
working_directory: ~/homebrewery
|
||||||
parallelism: 1
|
parallelism: 1
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ module.exports = {
|
|||||||
rules : {
|
rules : {
|
||||||
/** Errors **/
|
/** Errors **/
|
||||||
'camelcase' : ['error', { properties: 'never' }],
|
'camelcase' : ['error', { properties: 'never' }],
|
||||||
'func-style' : ['error', 'expression', { allowArrowFunctions: true }],
|
//'func-style' : ['error', 'expression', { allowArrowFunctions: true }],
|
||||||
'no-array-constructor' : 'error',
|
'no-array-constructor' : 'error',
|
||||||
'no-iterator' : 'error',
|
'no-iterator' : 'error',
|
||||||
'no-nested-ternary' : 'error',
|
'no-nested-ternary' : 'error',
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM node:16.13-alpine
|
FROM node:18-alpine
|
||||||
RUN apk --no-cache add git
|
RUN apk --no-cache add git
|
||||||
|
|
||||||
ENV NODE_ENV=docker
|
ENV NODE_ENV=docker
|
||||||
|
|||||||
121
changelog.md
121
changelog.md
@@ -80,6 +80,114 @@ pre {
|
|||||||
## changelog
|
## changelog
|
||||||
For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery).
|
For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery).
|
||||||
|
|
||||||
|
### Friday 13/10/2023 - v3.10.0
|
||||||
|
{{taskList
|
||||||
|
|
||||||
|
##### G-Ambatte
|
||||||
|
|
||||||
|
* [x] Fix user preferred save location being ignored
|
||||||
|
|
||||||
|
Fixes issue [#2993](https://github.com/naturalcrit/homebrewery/issues/2993)
|
||||||
|
|
||||||
|
* [x] Fix crash to white screen when starting new brews while not signed in
|
||||||
|
|
||||||
|
Fixes issue [#2999](https://github.com/naturalcrit/homebrewery/issues/2999)
|
||||||
|
|
||||||
|
* [x] Fix FreeBSD install script
|
||||||
|
|
||||||
|
Fixes issue [#3005](https://github.com/naturalcrit/homebrewery/issues/3005)
|
||||||
|
|
||||||
|
* [x] Fix *"This brew has been changed on another device"* triggering when manually saving during auto-save
|
||||||
|
|
||||||
|
Fixes issue [#2641](https://github.com/naturalcrit/homebrewery/issues/2641)
|
||||||
|
|
||||||
|
* [x] Fix Firefox different column-flow behavior
|
||||||
|
|
||||||
|
Fixes issue [#2982](https://github.com/naturalcrit/homebrewery/issues/2982)
|
||||||
|
|
||||||
|
* [x] Fix brew titles being mis-sorted on user page
|
||||||
|
|
||||||
|
Fixes issue [#2775](https://github.com/naturalcrit/homebrewery/issues/2775)
|
||||||
|
|
||||||
|
* [x] Text Editor themes now available via new drop-down
|
||||||
|
|
||||||
|
Fixes issue [#362](https://github.com/naturalcrit/homebrewery/issues/362)
|
||||||
|
|
||||||
|
##### 5e-Cleric
|
||||||
|
|
||||||
|
* [x] New {{openSans **PHB → {{fas,fa-quote-right}} QUOTE** }} snippet for V3!
|
||||||
|
|
||||||
|
Fixes issue [#2920](https://github.com/naturalcrit/homebrewery/issues/2920)
|
||||||
|
|
||||||
|
* [x] Several updates and fixes to FAQ and Welcome page
|
||||||
|
|
||||||
|
Fixes issue [#2729](https://github.com/naturalcrit/homebrewery/issues/2729),
|
||||||
|
[#2787](https://github.com/naturalcrit/homebrewery/issues/2787)
|
||||||
|
|
||||||
|
##### Gazook89
|
||||||
|
|
||||||
|
* [x] Add syntax highlighting for Definition Lists <code>:\:</code>
|
||||||
|
}}
|
||||||
|
|
||||||
|
|
||||||
|
### Thursday 17/08/2023 - v3.9.2
|
||||||
|
{{taskList
|
||||||
|
|
||||||
|
##### Calculuschild
|
||||||
|
|
||||||
|
* [x] Fix links to certain old Google Drive files
|
||||||
|
|
||||||
|
Fixes issue [#2917](https://github.com/naturalcrit/homebrewery/issues/2917)
|
||||||
|
|
||||||
|
##### G-Ambatte
|
||||||
|
|
||||||
|
* [x] Menus now open on click, and internally consistent
|
||||||
|
|
||||||
|
Fixes issue [#2702](https://github.com/naturalcrit/homebrewery/issues/2702), [#2782](https://github.com/naturalcrit/homebrewery/issues/2782)
|
||||||
|
|
||||||
|
* [x] Add smarter footer snippet
|
||||||
|
|
||||||
|
Fixes issue [#2289](https://github.com/naturalcrit/homebrewery/issues/2289)
|
||||||
|
|
||||||
|
* [x] Add sanitization in Style editor
|
||||||
|
|
||||||
|
Fixes issue [#1437](https://github.com/naturalcrit/homebrewery/issues/1437)
|
||||||
|
|
||||||
|
* [x] Rework class table snippets to remove unnecessary randomness
|
||||||
|
|
||||||
|
Fixes issue [#2964](https://github.com/naturalcrit/homebrewery/issues/2964)
|
||||||
|
|
||||||
|
* [x] Add User Page link to Google Drive file for file owners, add icons for additional storage locations
|
||||||
|
|
||||||
|
Fixes issue [#2954](https://github.com/naturalcrit/homebrewery/issues/2954)
|
||||||
|
|
||||||
|
* [x] Add default save location selection to Account Page
|
||||||
|
|
||||||
|
Fixes issue [#2943](https://github.com/naturalcrit/homebrewery/issues/2943)
|
||||||
|
|
||||||
|
##### 5e-Cleric
|
||||||
|
|
||||||
|
* [x] Exclude cover pages from Table of Content generation (editing on mobile is still not recommended)
|
||||||
|
|
||||||
|
Fixes issue [#2920](https://github.com/naturalcrit/homebrewery/issues/2920)
|
||||||
|
|
||||||
|
##### Gazook89
|
||||||
|
|
||||||
|
* [x] Adjustments to improve mobile viewing
|
||||||
|
}}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Wednesday 28/06/2023 - v3.9.1
|
||||||
|
{{taskList
|
||||||
|
|
||||||
|
##### G-Ambatte
|
||||||
|
|
||||||
|
* [x] Better error pages with more useful information
|
||||||
|
|
||||||
|
Fixes issue [#1924](https://github.com/naturalcrit/homebrewery/issues/1924)
|
||||||
|
}}
|
||||||
|
|
||||||
### Friday 02/06/2023 - v3.9.0
|
### Friday 02/06/2023 - v3.9.0
|
||||||
{{taskList
|
{{taskList
|
||||||
|
|
||||||
@@ -101,7 +209,7 @@ Fixes issues [#2092](https://github.com/naturalcrit/homebrewery/issues/2092), [#
|
|||||||
|
|
||||||
Fixes issue [#2790](https://github.com/naturalcrit/homebrewery/issues/2790)
|
Fixes issue [#2790](https://github.com/naturalcrit/homebrewery/issues/2790)
|
||||||
|
|
||||||
##### 5e-Clerc
|
##### 5e-Cleric
|
||||||
|
|
||||||
* [x] New {{openSans **PHB → {{fac,book-part-cover}} PART COVER PAGE** }} snippet for V3!
|
* [x] New {{openSans **PHB → {{fac,book-part-cover}} PART COVER PAGE** }} snippet for V3!
|
||||||
|
|
||||||
@@ -114,6 +222,8 @@ Fixes issue [#2790](https://github.com/naturalcrit/homebrewery/issues/2790)
|
|||||||
Fixes issue [#2784](https://github.com/naturalcrit/homebrewery/issues/2784)
|
Fixes issue [#2784](https://github.com/naturalcrit/homebrewery/issues/2784)
|
||||||
}}
|
}}
|
||||||
|
|
||||||
|
\page
|
||||||
|
|
||||||
### Wednesday 12/04/2023 - v3.8.0
|
### Wednesday 12/04/2023 - v3.8.0
|
||||||
{{taskList
|
{{taskList
|
||||||
|
|
||||||
@@ -145,7 +255,7 @@ Fixes issues [#1679](https://github.com/naturalcrit/homebrewery/issues/1679)
|
|||||||
|
|
||||||
* [x] Add local Windows install script via Chocolatey
|
* [x] Add local Windows install script via Chocolatey
|
||||||
|
|
||||||
##### 5e-Clerc
|
##### 5e-Cleric
|
||||||
|
|
||||||
* [x] New {{openSans **TABLES → {{fas,fa-language}} RUNE TABLE**}} snippets for V3. Adds an alphabetic script translation table.
|
* [x] New {{openSans **TABLES → {{fas,fa-language}} RUNE TABLE**}} snippets for V3. Adds an alphabetic script translation table.
|
||||||
|
|
||||||
@@ -175,8 +285,6 @@ Fixes issues [#2731](https://github.com/naturalcrit/homebrewery/issues/2731)
|
|||||||
|
|
||||||
}}
|
}}
|
||||||
|
|
||||||
\page
|
|
||||||
|
|
||||||
### Monday 13/03/2023 - v3.7.2
|
### Monday 13/03/2023 - v3.7.2
|
||||||
{{taskList
|
{{taskList
|
||||||
|
|
||||||
@@ -257,7 +365,11 @@ Fixes issues [#2603](https://github.com/naturalcrit/homebrewery/issues/2603)
|
|||||||
* [x] Add message to refresh the browser if the user is missing an update to the Homebrewery
|
* [x] Add message to refresh the browser if the user is missing an update to the Homebrewery
|
||||||
|
|
||||||
Fixes issues [#2583](https://github.com/naturalcrit/homebrewery/issues/2583)
|
Fixes issues [#2583](https://github.com/naturalcrit/homebrewery/issues/2583)
|
||||||
|
}}
|
||||||
|
|
||||||
|
\page
|
||||||
|
|
||||||
|
{{taskList
|
||||||
##### G-Ambatte
|
##### G-Ambatte
|
||||||
|
|
||||||
* [x] Auto-compile Themes CSS on development server
|
* [x] Auto-compile Themes CSS on development server
|
||||||
@@ -267,7 +379,6 @@ Fixes issues [#2583](https://github.com/naturalcrit/homebrewery/issues/2583)
|
|||||||
* [x] Fix cloned brews inheriting the parent view count
|
* [x] Fix cloned brews inheriting the parent view count
|
||||||
}}
|
}}
|
||||||
|
|
||||||
\page
|
|
||||||
|
|
||||||
### Friday 23/12/2022 - v3.5.0
|
### Friday 23/12/2022 - v3.5.0
|
||||||
{{taskList
|
{{taskList
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ const Stats = createClass({
|
|||||||
<dl>
|
<dl>
|
||||||
<dt>Total Brew Count</dt>
|
<dt>Total Brew Count</dt>
|
||||||
<dd>{this.state.stats.totalBrews}</dd>
|
<dd>{this.state.stats.totalBrews}</dd>
|
||||||
|
<dt>Total Brews Published Count</dt>
|
||||||
|
<dd>{this.state.stats.totalPublishedBrews || 'no published brews'}</dd>
|
||||||
</dl>
|
</dl>
|
||||||
|
|
||||||
{this.state.fetching
|
{this.state.fetching
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
/*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/
|
/*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/
|
||||||
require('./brewRenderer.less');
|
require('./brewRenderer.less');
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const createClass = require('create-react-class');
|
const { useState, useRef, useEffect } = React;
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const cx = require('classnames');
|
|
||||||
|
|
||||||
const MarkdownLegacy = require('naturalcrit/markdownLegacy.js');
|
const MarkdownLegacy = require('naturalcrit/markdownLegacy.js');
|
||||||
const Markdown = require('naturalcrit/markdown.js');
|
const Markdown = require('naturalcrit/markdown.js');
|
||||||
@@ -13,202 +12,196 @@ const ErrorBar = require('./errorBar/errorBar.jsx');
|
|||||||
const RenderWarnings = require('homebrewery/renderWarnings/renderWarnings.jsx');
|
const RenderWarnings = require('homebrewery/renderWarnings/renderWarnings.jsx');
|
||||||
const NotificationPopup = require('./notificationPopup/notificationPopup.jsx');
|
const NotificationPopup = require('./notificationPopup/notificationPopup.jsx');
|
||||||
const Frame = require('react-frame-component').default;
|
const Frame = require('react-frame-component').default;
|
||||||
|
const dedent = require('dedent-tabs').default;
|
||||||
|
|
||||||
const Themes = require('themes/themes.json');
|
const Themes = require('themes/themes.json');
|
||||||
|
|
||||||
const PAGE_HEIGHT = 1056;
|
const PAGE_HEIGHT = 1056;
|
||||||
const PPR_THRESHOLD = 50;
|
|
||||||
|
|
||||||
const BrewRenderer = createClass({
|
const INITIAL_CONTENT = dedent`
|
||||||
displayName : 'BrewRenderer',
|
<!DOCTYPE html><html><head>
|
||||||
getDefaultProps : function() {
|
<link href="//use.fontawesome.com/releases/v5.15.1/css/all.css" rel="stylesheet" />
|
||||||
return {
|
<link href="//fonts.googleapis.com/css?family=Open+Sans:400,300,600,700" rel="stylesheet" type="text/css" />
|
||||||
|
<link href='/homebrew/bundle.css' rel='stylesheet' />
|
||||||
|
<base target=_blank>
|
||||||
|
</head><body style='overflow: hidden'><div></div></body></html>`;
|
||||||
|
|
||||||
|
//v=====----------------------< Brew Page Component >---------------------=====v//
|
||||||
|
const BrewPage = (props)=>{
|
||||||
|
props = {
|
||||||
|
contents : '',
|
||||||
|
index : 0,
|
||||||
|
...props
|
||||||
|
};
|
||||||
|
return <div className={props.className} id={`p${props.index + 1}`} >
|
||||||
|
<div className='columnWrapper' dangerouslySetInnerHTML={{ __html: props.contents }} />
|
||||||
|
</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
//v=====--------------------< Brew Renderer Component >-------------------=====v//
|
||||||
|
const renderedPages = [];
|
||||||
|
let rawPages = [];
|
||||||
|
|
||||||
|
const BrewRenderer = (props)=>{
|
||||||
|
props = {
|
||||||
text : '',
|
text : '',
|
||||||
style : '',
|
style : '',
|
||||||
renderer : 'legacy',
|
renderer : 'legacy',
|
||||||
theme : '5ePHB',
|
theme : '5ePHB',
|
||||||
lang : '',
|
lang : '',
|
||||||
errors : []
|
errors : [],
|
||||||
|
currentEditorPage : 0,
|
||||||
|
...props
|
||||||
};
|
};
|
||||||
},
|
|
||||||
getInitialState : function() {
|
|
||||||
let pages;
|
|
||||||
if(this.props.renderer == 'legacy') {
|
|
||||||
pages = this.props.text.split('\\page');
|
|
||||||
} else {
|
|
||||||
pages = this.props.text.split(/^\\page$/gm);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
const [state, setState] = useState({
|
||||||
viewablePageNumber : 0,
|
viewablePageNumber : 0,
|
||||||
height : 0,
|
height : PAGE_HEIGHT,
|
||||||
isMounted : false,
|
isMounted : false,
|
||||||
|
|
||||||
pages : pages,
|
|
||||||
usePPR : pages.length >= PPR_THRESHOLD,
|
|
||||||
visibility : 'hidden',
|
visibility : 'hidden',
|
||||||
initialContent : `<!DOCTYPE html><html><head>
|
});
|
||||||
<link href="//use.fontawesome.com/releases/v5.15.1/css/all.css" rel="stylesheet" />
|
|
||||||
<link href="//fonts.googleapis.com/css?family=Open+Sans:400,300,600,700" rel="stylesheet" type="text/css" />
|
|
||||||
<link href='/homebrew/bundle.css' rel='stylesheet' />
|
|
||||||
<base target=_blank>
|
|
||||||
</head><body style='overflow: hidden'><div></div></body></html>`
|
|
||||||
};
|
|
||||||
},
|
|
||||||
height : 0,
|
|
||||||
lastRender : <div></div>,
|
|
||||||
|
|
||||||
componentWillUnmount : function() {
|
const mainRef = useRef(null);
|
||||||
window.removeEventListener('resize', this.updateSize);
|
|
||||||
},
|
|
||||||
|
|
||||||
componentDidUpdate : function(prevProps) {
|
if(props.renderer == 'legacy') {
|
||||||
if(prevProps.text !== this.props.text) {
|
rawPages = props.text.split('\\page');
|
||||||
let pages;
|
|
||||||
if(this.props.renderer == 'legacy') {
|
|
||||||
pages = this.props.text.split('\\page');
|
|
||||||
} else {
|
} else {
|
||||||
pages = this.props.text.split(/^\\page$/gm);
|
rawPages = props.text.split(/^\\page$/gm);
|
||||||
}
|
}
|
||||||
this.setState({
|
|
||||||
pages : pages,
|
|
||||||
usePPR : pages.length >= PPR_THRESHOLD
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
updateSize : function() {
|
useEffect(()=>{ // Unmounting steps
|
||||||
this.setState({
|
return ()=>{window.removeEventListener('resize', updateSize);};
|
||||||
height : this.refs.main.parentNode.clientHeight,
|
}, []);
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
handleScroll : function(e){
|
const updateSize = ()=>{
|
||||||
const target = e.target;
|
setState((prevState)=>({
|
||||||
this.setState((prevState)=>({
|
...prevState,
|
||||||
viewablePageNumber : Math.floor(target.scrollTop / target.scrollHeight * prevState.pages.length)
|
height : mainRef.current.parentNode.clientHeight,
|
||||||
}));
|
}));
|
||||||
},
|
};
|
||||||
|
|
||||||
shouldRender : function(pageText, index){
|
const handleScroll = (e)=>{
|
||||||
if(!this.state.isMounted) return false;
|
const target = e.target;
|
||||||
|
setState((prevState)=>({
|
||||||
|
...prevState,
|
||||||
|
viewablePageNumber : Math.floor(target.scrollTop / target.scrollHeight * rawPages.length)
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
const viewIndex = this.state.viewablePageNumber;
|
const shouldRender = (index)=>{
|
||||||
if(index == viewIndex - 3) return true;
|
if(!state.isMounted) return false;
|
||||||
if(index == viewIndex - 2) return true;
|
|
||||||
if(index == viewIndex - 1) return true;
|
|
||||||
if(index == viewIndex) return true;
|
|
||||||
if(index == viewIndex + 1) return true;
|
|
||||||
if(index == viewIndex + 2) return true;
|
|
||||||
if(index == viewIndex + 3) return true;
|
|
||||||
|
|
||||||
//Check for style tages
|
if(Math.abs(index - state.viewablePageNumber) <= 3)
|
||||||
if(pageText.indexOf('<style>') !== -1) return true;
|
return true;
|
||||||
|
|
||||||
|
if(index + 1 == props.currentEditorPage)
|
||||||
|
return true;
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
},
|
};
|
||||||
|
|
||||||
renderPageInfo : function(){
|
const sanitizeScriptTags = (content)=>{
|
||||||
return <div className='pageInfo' ref='main'>
|
return content
|
||||||
|
.replace(/<script/ig, '<script')
|
||||||
|
.replace(/<\/script>/ig, '</script>');
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderPageInfo = ()=>{
|
||||||
|
return <div className='pageInfo' ref={mainRef}>
|
||||||
<div>
|
<div>
|
||||||
{this.props.renderer}
|
{props.renderer}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{this.state.viewablePageNumber + 1} / {this.state.pages.length}
|
{state.viewablePageNumber + 1} / {rawPages.length}
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
},
|
};
|
||||||
|
|
||||||
renderPPRmsg : function(){
|
const renderDummyPage = (index)=>{
|
||||||
if(!this.state.usePPR) return;
|
|
||||||
|
|
||||||
return <div className='ppr_msg'>
|
|
||||||
Partial Page Renderer is enabled, because your brew is so large. May affect rendering.
|
|
||||||
</div>;
|
|
||||||
},
|
|
||||||
|
|
||||||
renderDummyPage : function(index){
|
|
||||||
return <div className='phb page' id={`p${index + 1}`} key={index}>
|
return <div className='phb page' id={`p${index + 1}`} key={index}>
|
||||||
<i className='fas fa-spinner fa-spin' />
|
<i className='fas fa-spinner fa-spin' />
|
||||||
</div>;
|
</div>;
|
||||||
},
|
};
|
||||||
|
|
||||||
renderStyle : function() {
|
const renderStyle = ()=>{
|
||||||
if(!this.props.style) return;
|
if(!props.style) return;
|
||||||
//return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `<style>@layer styleTab {\n${this.props.style}\n} </style>` }} />;
|
const cleanStyle = sanitizeScriptTags(props.style);
|
||||||
return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `<style>\n${this.props.style}\n</style>` }} />;
|
//return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `<style>@layer styleTab {\n${sanitizeScriptTags(props.style)}\n} </style>` }} />;
|
||||||
},
|
return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `<style> ${cleanStyle} </style>` }} />;
|
||||||
|
};
|
||||||
|
|
||||||
renderPage : function(pageText, index){
|
const renderPage = (pageText, index)=>{
|
||||||
if(this.props.renderer == 'legacy')
|
let cleanPageText = sanitizeScriptTags(pageText);
|
||||||
return <div className='phb page' id={`p${index + 1}`} dangerouslySetInnerHTML={{ __html: MarkdownLegacy.render(pageText) }} key={index} />;
|
if(props.renderer == 'legacy') {
|
||||||
else {
|
const html = MarkdownLegacy.render(cleanPageText);
|
||||||
pageText += `\n\n \n\\column\n `; //Artificial column break at page end to emulate column-fill:auto (until `wide` is used, when column-fill:balance will reappear)
|
return <BrewPage className='page phb' index={index} key={index} contents={html} />;
|
||||||
return (
|
|
||||||
<div className='page' id={`p${index + 1}`} key={index} >
|
|
||||||
<div className='columnWrapper' dangerouslySetInnerHTML={{ __html: Markdown.render(pageText) }} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
renderPages : function(){
|
|
||||||
if(this.state.usePPR){
|
|
||||||
return _.map(this.state.pages, (page, index)=>{
|
|
||||||
if(this.shouldRender(page, index) && typeof window !== 'undefined'){
|
|
||||||
return this.renderPage(page, index);
|
|
||||||
} else {
|
} else {
|
||||||
return this.renderDummyPage(index);
|
cleanPageText += `\n\n \n\\column\n `; //Artificial column break at page end to emulate column-fill:auto (until `wide` is used, when column-fill:balance will reappear)
|
||||||
|
const html = Markdown.render(cleanPageText);
|
||||||
|
return <BrewPage className='page' index={index} key={index} contents={html} />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderPages = ()=>{
|
||||||
|
if(props.errors && props.errors.length)
|
||||||
|
return renderedPages;
|
||||||
|
|
||||||
|
if(rawPages.length != renderedPages.length) // Re-render all pages when page count changes
|
||||||
|
renderedPages.length = 0;
|
||||||
|
|
||||||
|
_.forEach(rawPages, (page, index)=>{
|
||||||
|
if((shouldRender(index) || !renderedPages[index]) && typeof window !== 'undefined'){
|
||||||
|
renderedPages[index] = renderPage(page, index); // Render any page not yet rendered, but only re-render those in PPR range
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
return renderedPages;
|
||||||
if(this.props.errors && this.props.errors.length) return this.lastRender;
|
};
|
||||||
this.lastRender = _.map(this.state.pages, (page, index)=>{
|
|
||||||
if(typeof window !== 'undefined') {
|
|
||||||
return this.renderPage(page, index);
|
|
||||||
} else {
|
|
||||||
return this.renderDummyPage(index);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return this.lastRender;
|
|
||||||
},
|
|
||||||
|
|
||||||
frameDidMount : function(){ //This triggers when iFrame finishes internal "componentDidMount"
|
const frameDidMount = ()=>{ //This triggers when iFrame finishes internal "componentDidMount"
|
||||||
setTimeout(()=>{ //We still see a flicker where the style isn't applied yet, so wait 100ms before showing iFrame
|
setTimeout(()=>{ //We still see a flicker where the style isn't applied yet, so wait 100ms before showing iFrame
|
||||||
this.updateSize();
|
updateSize();
|
||||||
window.addEventListener('resize', this.updateSize);
|
window.addEventListener('resize', updateSize);
|
||||||
this.renderPages(); //Make sure page is renderable before showing
|
renderPages(); //Make sure page is renderable before showing
|
||||||
this.setState({
|
setState((prevState)=>({
|
||||||
|
...prevState,
|
||||||
isMounted : true,
|
isMounted : true,
|
||||||
visibility : 'visible'
|
visibility : 'visible'
|
||||||
});
|
}));
|
||||||
}, 100);
|
}, 100);
|
||||||
},
|
};
|
||||||
|
|
||||||
render : function(){
|
const emitClick = ()=>{ // Allow clicks inside iFrame to interact with dropdowns, etc. from outside
|
||||||
//render in iFrame so broken code doesn't crash the site.
|
if(!window || !document) return;
|
||||||
//Also render dummy page while iframe is mounting.
|
document.dispatchEvent(new MouseEvent('click'));
|
||||||
const rendererPath = this.props.renderer == 'V3' ? 'V3' : 'Legacy';
|
};
|
||||||
const themePath = this.props.theme ?? '5ePHB';
|
|
||||||
|
const rendererPath = props.renderer == 'V3' ? 'V3' : 'Legacy';
|
||||||
|
const themePath = props.theme ?? '5ePHB';
|
||||||
const baseThemePath = Themes[rendererPath][themePath].baseTheme;
|
const baseThemePath = Themes[rendererPath][themePath].baseTheme;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<>
|
||||||
{!this.state.isMounted
|
{/*render dummy page while iFrame is mounting.*/}
|
||||||
? <div className='brewRenderer' onScroll={this.handleScroll}>
|
{!state.isMounted
|
||||||
<div className='pages' ref='pages'>
|
? <div className='brewRenderer' onScroll={handleScroll}>
|
||||||
{this.renderDummyPage(1)}
|
<div className='pages'>
|
||||||
|
{renderDummyPage(1)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
: null}
|
: null}
|
||||||
|
|
||||||
<Frame id='BrewRenderer' initialContent={this.state.initialContent}
|
{/*render in iFrame so broken code doesn't crash the site.*/}
|
||||||
style={{ width: '100%', height: '100%', visibility: this.state.visibility }}
|
<Frame id='BrewRenderer' initialContent={INITIAL_CONTENT}
|
||||||
contentDidMount={this.frameDidMount}>
|
style={{ width: '100%', height: '100%', visibility: state.visibility }}
|
||||||
|
contentDidMount={frameDidMount}
|
||||||
|
onClick={()=>{emitClick();}}
|
||||||
|
>
|
||||||
<div className={'brewRenderer'}
|
<div className={'brewRenderer'}
|
||||||
onScroll={this.handleScroll}
|
onScroll={handleScroll}
|
||||||
style={{ height: this.state.height }}>
|
style={{ height: state.height }}>
|
||||||
|
|
||||||
<ErrorBar errors={this.props.errors} />
|
<ErrorBar errors={props.errors} />
|
||||||
<div className='popups'>
|
<div className='popups'>
|
||||||
<RenderWarnings />
|
<RenderWarnings />
|
||||||
<NotificationPopup />
|
<NotificationPopup />
|
||||||
@@ -218,23 +211,22 @@ const BrewRenderer = createClass({
|
|||||||
<link href={`/themes/${rendererPath}/${baseThemePath}/style.css`} rel='stylesheet'/>
|
<link href={`/themes/${rendererPath}/${baseThemePath}/style.css`} rel='stylesheet'/>
|
||||||
}
|
}
|
||||||
<link href={`/themes/${rendererPath}/${themePath}/style.css`} rel='stylesheet'/>
|
<link href={`/themes/${rendererPath}/${themePath}/style.css`} rel='stylesheet'/>
|
||||||
|
|
||||||
{/* Apply CSS from Style tab and render pages from Markdown tab */}
|
{/* Apply CSS from Style tab and render pages from Markdown tab */}
|
||||||
{this.state.isMounted
|
{state.isMounted
|
||||||
&&
|
&&
|
||||||
<>
|
<>
|
||||||
{this.renderStyle()}
|
{renderStyle()}
|
||||||
<div className='pages' ref='pages' lang={`${this.props.lang || 'en'}`}>
|
<div className='pages' lang={`${props.lang || 'en'}`}>
|
||||||
{this.renderPages()}
|
{renderPages()}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</Frame>
|
</Frame>
|
||||||
{this.renderPageInfo()}
|
{renderPageInfo()}
|
||||||
{this.renderPPRmsg()}
|
</>
|
||||||
</React.Fragment>
|
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = BrewRenderer;
|
module.exports = BrewRenderer;
|
||||||
|
|||||||
@@ -1,46 +1,44 @@
|
|||||||
@import (multiple, less) 'shared/naturalcrit/styles/reset.less';
|
@import (multiple, less) 'shared/naturalcrit/styles/reset.less';
|
||||||
|
|
||||||
.brewRenderer{
|
.brewRenderer {
|
||||||
will-change : transform;
|
will-change : transform;
|
||||||
overflow-y : scroll;
|
overflow-y : scroll;
|
||||||
.pages{
|
:where(.pages) {
|
||||||
margin : 30px 0px;
|
margin : 30px 0px;
|
||||||
&>.page{
|
& > :where(.page) {
|
||||||
|
width : 215.9mm;
|
||||||
|
height : 279.4mm;
|
||||||
margin-right : auto;
|
margin-right : auto;
|
||||||
margin-bottom : 30px;
|
margin-bottom : 30px;
|
||||||
margin-left : auto;
|
margin-left : auto;
|
||||||
box-shadow : 1px 4px 14px #000;
|
box-shadow : 1px 4px 14px #000000;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.pane{
|
.pane { position : relative; }
|
||||||
position : relative;
|
.pageInfo {
|
||||||
}
|
|
||||||
.pageInfo{
|
|
||||||
position : absolute;
|
position : absolute;
|
||||||
right : 17px;
|
right : 17px;
|
||||||
bottom : 0;
|
bottom : 0;
|
||||||
z-index : 1000;
|
z-index : 1000;
|
||||||
background-color : #333;
|
|
||||||
font-size : 10px;
|
font-size : 10px;
|
||||||
font-weight : 800;
|
font-weight : 800;
|
||||||
color : white;
|
color : white;
|
||||||
|
background-color : #333333;
|
||||||
div {
|
div {
|
||||||
display: inline-block;
|
display : inline-block;
|
||||||
padding : 8px 10px;
|
padding : 8px 10px;
|
||||||
&:not(:last-child){
|
&:not(:last-child) { border-right : 1px solid #666666; }
|
||||||
border-right: 1px solid #666;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.ppr_msg{
|
.ppr_msg {
|
||||||
position : absolute;
|
position : absolute;
|
||||||
left : 0px;
|
|
||||||
bottom : 0;
|
bottom : 0;
|
||||||
|
left : 0px;
|
||||||
z-index : 1000;
|
z-index : 1000;
|
||||||
padding : 8px 10px;
|
padding : 8px 10px;
|
||||||
background-color : #333;
|
|
||||||
font-size : 10px;
|
font-size : 10px;
|
||||||
font-weight : 800;
|
font-weight : 800;
|
||||||
color : white;
|
color : white;
|
||||||
|
background-color : #333333;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ const CodeEditor = require('naturalcrit/codeEditor/codeEditor.jsx');
|
|||||||
const SnippetBar = require('./snippetbar/snippetbar.jsx');
|
const SnippetBar = require('./snippetbar/snippetbar.jsx');
|
||||||
const MetadataEditor = require('./metadataEditor/metadataEditor.jsx');
|
const MetadataEditor = require('./metadataEditor/metadataEditor.jsx');
|
||||||
|
|
||||||
|
const EDITOR_THEME_KEY = 'HOMEBREWERY-EDITOR-THEME';
|
||||||
|
|
||||||
const SNIPPETBAR_HEIGHT = 25;
|
const SNIPPETBAR_HEIGHT = 25;
|
||||||
const DEFAULT_STYLE_TEXT = dedent`
|
const DEFAULT_STYLE_TEXT = dedent`
|
||||||
/*=======--- Example CSS styling ---=======*/
|
/*=======--- Example CSS styling ---=======*/
|
||||||
@@ -34,11 +36,13 @@ const Editor = createClass({
|
|||||||
onMetaChange : ()=>{},
|
onMetaChange : ()=>{},
|
||||||
reportError : ()=>{},
|
reportError : ()=>{},
|
||||||
|
|
||||||
|
editorTheme : 'default',
|
||||||
renderer : 'legacy'
|
renderer : 'legacy'
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
getInitialState : function() {
|
getInitialState : function() {
|
||||||
return {
|
return {
|
||||||
|
editorTheme : this.props.editorTheme,
|
||||||
view : 'text' //'text', 'style', 'meta'
|
view : 'text' //'text', 'style', 'meta'
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -51,6 +55,13 @@ const Editor = createClass({
|
|||||||
this.updateEditorSize();
|
this.updateEditorSize();
|
||||||
this.highlightCustomMarkdown();
|
this.highlightCustomMarkdown();
|
||||||
window.addEventListener('resize', this.updateEditorSize);
|
window.addEventListener('resize', this.updateEditorSize);
|
||||||
|
|
||||||
|
const editorTheme = window.localStorage.getItem(EDITOR_THEME_KEY);
|
||||||
|
if(editorTheme) {
|
||||||
|
this.setState({
|
||||||
|
editorTheme : editorTheme
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillUnmount : function() {
|
componentWillUnmount : function() {
|
||||||
@@ -138,9 +149,38 @@ const Editor = createClass({
|
|||||||
codeMirror.addLineClass(lineNumber, 'text', 'columnSplit');
|
codeMirror.addLineClass(lineNumber, 'text', 'columnSplit');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// definition lists
|
||||||
|
if(line.includes('::')){
|
||||||
|
const regex = /^([^\n]*?)::([^\n]*)(?:\n|$)/ym;
|
||||||
|
let match;
|
||||||
|
while ((match = regex.exec(line)) != null){
|
||||||
|
codeMirror.markText({ line: lineNumber, ch: line.indexOf(match[0]) }, { line: lineNumber, ch: line.indexOf(match[0]) + match[0].length }, { className: 'define' });
|
||||||
|
codeMirror.markText({ line: lineNumber, ch: line.indexOf(match[1]) }, { line: lineNumber, ch: line.indexOf(match[1]) + match[1].length }, { className: 'term' });
|
||||||
|
codeMirror.markText({ line: lineNumber, ch: line.indexOf(match[2]) }, { line: lineNumber, ch: line.indexOf(match[2]) + match[2].length }, { className: 'definition' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Superscript
|
||||||
|
if(line.includes('\^')) {
|
||||||
|
const regex = /\^(?!\s)(?=([^\n\^]*[^\s\^]))\1\^/g;
|
||||||
|
let match;
|
||||||
|
while ((match = regex.exec(line)) != null) {
|
||||||
|
codeMirror.markText({ line: lineNumber, ch: line.indexOf(match[1]) - 1 }, { line: lineNumber, ch: line.indexOf(match[1]) + match[1].length + 1 }, { className: 'superscript' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subscript
|
||||||
|
if(line.includes('^^')) {
|
||||||
|
const regex = /\^\^(?!\s)(?=([^\n\^]*[^\s\^]))\1\^\^/g;
|
||||||
|
let match;
|
||||||
|
while ((match = regex.exec(line)) != null) {
|
||||||
|
codeMirror.markText({ line: lineNumber, ch: line.indexOf(match[1]) - 2 }, { line: lineNumber, ch: line.indexOf(match[1]) + match[1].length + 2 }, { className: 'subscript' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Highlight injectors {style}
|
// Highlight injectors {style}
|
||||||
if(line.includes('{') && line.includes('}')){
|
if(line.includes('{') && line.includes('}')){
|
||||||
const regex = /(?:^|[^{\n])({(?=((?::(?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':{}\s]*)*))\2})/gm;
|
const regex = /(?:^|[^{\n])({(?=((?:[:=](?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':={}\s]*)*))\2})/gm;
|
||||||
let match;
|
let match;
|
||||||
while ((match = regex.exec(line)) != null) {
|
while ((match = regex.exec(line)) != null) {
|
||||||
codeMirror.markText({ line: lineNumber, ch: line.indexOf(match[1]) }, { line: lineNumber, ch: line.indexOf(match[1]) + match[1].length }, { className: 'injection' });
|
codeMirror.markText({ line: lineNumber, ch: line.indexOf(match[1]) }, { line: lineNumber, ch: line.indexOf(match[1]) + match[1].length }, { className: 'injection' });
|
||||||
@@ -148,7 +188,7 @@ const Editor = createClass({
|
|||||||
}
|
}
|
||||||
// Highlight inline spans {{content}}
|
// Highlight inline spans {{content}}
|
||||||
if(line.includes('{{') && line.includes('}}')){
|
if(line.includes('{{') && line.includes('}}')){
|
||||||
const regex = /{{(?=((?::(?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':{}\s]*)*))\1 *|}}/g;
|
const regex = /{{(?=((?:[:=](?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':={}\s]*)*))\1 *|}}/g;
|
||||||
let match;
|
let match;
|
||||||
let blockCount = 0;
|
let blockCount = 0;
|
||||||
while ((match = regex.exec(line)) != null) {
|
while ((match = regex.exec(line)) != null) {
|
||||||
@@ -167,7 +207,7 @@ const Editor = createClass({
|
|||||||
// Highlight block divs {{\n Content \n}}
|
// Highlight block divs {{\n Content \n}}
|
||||||
let endCh = line.length+1;
|
let endCh = line.length+1;
|
||||||
|
|
||||||
const match = line.match(/^ *{{(?=((?::(?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':{}\s]*)*))\1 *$|^ *}}$/);
|
const match = line.match(/^ *{{(?=((?:[:=](?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':={}\s]*)*))\1 *$|^ *}}$/);
|
||||||
if(match)
|
if(match)
|
||||||
endCh = match.index+match[0].length;
|
endCh = match.index+match[0].length;
|
||||||
codeMirror.markText({ line: lineNumber, ch: 0 }, { line: lineNumber, ch: endCh }, { className: 'block' });
|
codeMirror.markText({ line: lineNumber, ch: 0 }, { line: lineNumber, ch: endCh }, { className: 'block' });
|
||||||
@@ -255,6 +295,13 @@ const Editor = createClass({
|
|||||||
this.refs.codeEditor?.updateSize();
|
this.refs.codeEditor?.updateSize();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
updateEditorTheme : function(newTheme){
|
||||||
|
window.localStorage.setItem(EDITOR_THEME_KEY, newTheme);
|
||||||
|
this.setState({
|
||||||
|
editorTheme : newTheme
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
//Called by CodeEditor after document switch, so Snippetbar can refresh UndoHistory
|
//Called by CodeEditor after document switch, so Snippetbar can refresh UndoHistory
|
||||||
rerenderParent : function (){
|
rerenderParent : function (){
|
||||||
this.forceUpdate();
|
this.forceUpdate();
|
||||||
@@ -269,6 +316,7 @@ const Editor = createClass({
|
|||||||
view={this.state.view}
|
view={this.state.view}
|
||||||
value={this.props.brew.text}
|
value={this.props.brew.text}
|
||||||
onChange={this.props.onTextChange}
|
onChange={this.props.onTextChange}
|
||||||
|
editorTheme={this.state.editorTheme}
|
||||||
rerenderParent={this.rerenderParent} />
|
rerenderParent={this.rerenderParent} />
|
||||||
</>;
|
</>;
|
||||||
}
|
}
|
||||||
@@ -281,6 +329,7 @@ const Editor = createClass({
|
|||||||
value={this.props.brew.style ?? DEFAULT_STYLE_TEXT}
|
value={this.props.brew.style ?? DEFAULT_STYLE_TEXT}
|
||||||
onChange={this.props.onStyleChange}
|
onChange={this.props.onStyleChange}
|
||||||
enableFolding={false}
|
enableFolding={false}
|
||||||
|
editorTheme={this.state.editorTheme}
|
||||||
rerenderParent={this.rerenderParent} />
|
rerenderParent={this.rerenderParent} />
|
||||||
</>;
|
</>;
|
||||||
}
|
}
|
||||||
@@ -310,6 +359,14 @@ const Editor = createClass({
|
|||||||
return this.refs.codeEditor?.undo();
|
return this.refs.codeEditor?.undo();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
foldCode : function(){
|
||||||
|
return this.refs.codeEditor?.foldAllCode();
|
||||||
|
},
|
||||||
|
|
||||||
|
unfoldCode : function(){
|
||||||
|
return this.refs.codeEditor?.unfoldAllCode();
|
||||||
|
},
|
||||||
|
|
||||||
render : function(){
|
render : function(){
|
||||||
return (
|
return (
|
||||||
<div className='editor' ref='main'>
|
<div className='editor' ref='main'>
|
||||||
@@ -323,7 +380,12 @@ const Editor = createClass({
|
|||||||
theme={this.props.brew.theme}
|
theme={this.props.brew.theme}
|
||||||
undo={this.undo}
|
undo={this.undo}
|
||||||
redo={this.redo}
|
redo={this.redo}
|
||||||
historySize={this.historySize()} />
|
foldCode={this.foldCode}
|
||||||
|
unfoldCode={this.unfoldCode}
|
||||||
|
historySize={this.historySize()}
|
||||||
|
currentEditorTheme={this.state.editorTheme}
|
||||||
|
updateEditorTheme={this.updateEditorTheme}
|
||||||
|
cursorPos={this.refs.codeEditor?.getCursorPosition() || {}} />
|
||||||
|
|
||||||
{this.renderEditor()}
|
{this.renderEditor()}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,65 +1,85 @@
|
|||||||
|
@import 'themes/codeMirror/customEditorStyles.less';
|
||||||
.editor{
|
.editor {
|
||||||
position : relative;
|
position : relative;
|
||||||
width : 100%;
|
width : 100%;
|
||||||
|
|
||||||
.codeEditor{
|
.codeEditor {
|
||||||
height : 100%;
|
height : 100%;
|
||||||
.pageLine{
|
.pageLine {
|
||||||
background : #33333328;
|
background : #33333328;
|
||||||
border-top : #339 solid 1px;
|
border-top : #333399 solid 1px;
|
||||||
}
|
}
|
||||||
.editor-page-count{
|
.editor-page-count {
|
||||||
color : grey;
|
|
||||||
float : right;
|
float : right;
|
||||||
|
color : grey;
|
||||||
}
|
}
|
||||||
.columnSplit{
|
.columnSplit {
|
||||||
font-style : italic;
|
font-style : italic;
|
||||||
color : grey;
|
color : grey;
|
||||||
background-color : fade(#299, 15%);
|
background-color : fade(#229999, 15%);
|
||||||
border-bottom : #299 solid 1px;
|
border-bottom : #229999 solid 1px;
|
||||||
}
|
}
|
||||||
.block:not(.cm-comment){
|
.define {
|
||||||
|
&:not(.term):not(.definition) {
|
||||||
|
font-weight : bold;
|
||||||
|
color : #949494;
|
||||||
|
background : #E5E5E5;
|
||||||
|
border-radius : 3px;
|
||||||
|
}
|
||||||
|
&.term { color : rgb(96, 117, 143); }
|
||||||
|
&.definition { color : rgb(97, 57, 178); }
|
||||||
|
}
|
||||||
|
.block:not(.cm-comment) {
|
||||||
|
font-weight : bold;
|
||||||
color : purple;
|
color : purple;
|
||||||
font-weight : bold;
|
|
||||||
//font-style: italic;
|
//font-style: italic;
|
||||||
}
|
}
|
||||||
.inline-block:not(.cm-comment){
|
.inline-block:not(.cm-comment) {
|
||||||
|
font-weight : bold;
|
||||||
color : red;
|
color : red;
|
||||||
font-weight : bold;
|
|
||||||
//font-style: italic;
|
//font-style: italic;
|
||||||
}
|
}
|
||||||
.injection:not(.cm-comment){
|
.injection:not(.cm-comment) {
|
||||||
color : green;
|
|
||||||
font-weight : bold;
|
font-weight : bold;
|
||||||
|
color : green;
|
||||||
|
}
|
||||||
|
.superscript:not(.cm-comment) {
|
||||||
|
font-weight : bold;
|
||||||
|
color : goldenrod;
|
||||||
|
vertical-align : super;
|
||||||
|
font-size : 0.9em;
|
||||||
|
}
|
||||||
|
.subscript:not(.cm-comment) {
|
||||||
|
font-weight : bold;
|
||||||
|
color : rgb(123, 123, 15);
|
||||||
|
vertical-align : sub;
|
||||||
|
font-size : 0.9em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.brewJump{
|
.brewJump {
|
||||||
position : absolute;
|
position : absolute;
|
||||||
background-color : @teal;
|
right : 20px;
|
||||||
cursor : pointer;
|
bottom : 20px;
|
||||||
width : 30px;
|
z-index : 1000000;
|
||||||
height : 30px;
|
|
||||||
display : flex;
|
display : flex;
|
||||||
align-items : center;
|
align-items : center;
|
||||||
bottom : 20px;
|
|
||||||
right : 20px;
|
|
||||||
z-index : 1000000;
|
|
||||||
justify-content : center;
|
justify-content : center;
|
||||||
.tooltipLeft("Jump to brew page");
|
width : 30px;
|
||||||
|
height : 30px;
|
||||||
|
cursor : pointer;
|
||||||
|
background-color : @teal;
|
||||||
|
.tooltipLeft('Jump to brew page');
|
||||||
}
|
}
|
||||||
|
|
||||||
.editorToolbar{
|
.editorToolbar {
|
||||||
position: absolute;
|
position : absolute;
|
||||||
top: 5px;
|
top : 5px;
|
||||||
left: 50%;
|
left : 50%;
|
||||||
color: black;
|
z-index : 9;
|
||||||
font-size: 13px;
|
font-size : 13px;
|
||||||
z-index: 9;
|
color : black;
|
||||||
span {
|
span { padding : 2px 5px; }
|
||||||
padding: 2px 5px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
.metadataEditor{
|
.metadataEditor{
|
||||||
position : absolute;
|
position : absolute;
|
||||||
z-index : 10000;
|
z-index : 5;
|
||||||
box-sizing : border-box;
|
box-sizing : border-box;
|
||||||
width : 100%;
|
width : 100%;
|
||||||
padding : 25px;
|
padding : 25px;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/*eslint max-lines: ["warn", {"max": 250, "skipBlankLines": true, "skipComments": true}]*/
|
||||||
require('./snippetbar.less');
|
require('./snippetbar.less');
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const createClass = require('create-react-class');
|
const createClass = require('create-react-class');
|
||||||
@@ -15,8 +16,10 @@ ThemeSnippets['V3_5eDMG'] = require('themes/V3/5eDMG/snippets.js');
|
|||||||
ThemeSnippets['V3_Journal'] = require('themes/V3/Journal/snippets.js');
|
ThemeSnippets['V3_Journal'] = require('themes/V3/Journal/snippets.js');
|
||||||
ThemeSnippets['V3_Blank'] = require('themes/V3/Blank/snippets.js');
|
ThemeSnippets['V3_Blank'] = require('themes/V3/Blank/snippets.js');
|
||||||
|
|
||||||
const execute = function(val, brew){
|
const EditorThemes = require('build/homebrew/codeMirror/editorThemes.json');
|
||||||
if(_.isFunction(val)) return val(brew);
|
|
||||||
|
const execute = function(val, props){
|
||||||
|
if(_.isFunction(val)) return val(props);
|
||||||
return val;
|
return val;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -33,13 +36,18 @@ const Snippetbar = createClass({
|
|||||||
renderer : 'legacy',
|
renderer : 'legacy',
|
||||||
undo : ()=>{},
|
undo : ()=>{},
|
||||||
redo : ()=>{},
|
redo : ()=>{},
|
||||||
historySize : ()=>{}
|
historySize : ()=>{},
|
||||||
|
foldCode : ()=>{},
|
||||||
|
unfoldCode : ()=>{},
|
||||||
|
updateEditorTheme : ()=>{},
|
||||||
|
cursorPos : {}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState : function() {
|
getInitialState : function() {
|
||||||
return {
|
return {
|
||||||
renderer : this.props.renderer,
|
renderer : this.props.renderer,
|
||||||
|
themeSelector : false,
|
||||||
snippets : []
|
snippets : []
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -66,6 +74,7 @@ const Snippetbar = createClass({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
mergeCustomizer : function(valueA, valueB, key) {
|
mergeCustomizer : function(valueA, valueB, key) {
|
||||||
if(key == 'snippets') {
|
if(key == 'snippets') {
|
||||||
const result = _.reverse(_.unionBy(_.reverse(valueB), _.reverse(valueA), 'name')); // Join snippets together, with preference for the current theme over the base theme
|
const result = _.reverse(_.unionBy(_.reverse(valueB), _.reverse(valueA), 'name')); // Join snippets together, with preference for the current theme over the base theme
|
||||||
@@ -94,6 +103,33 @@ const Snippetbar = createClass({
|
|||||||
this.props.onInject(injectedText);
|
this.props.onInject(injectedText);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
toggleThemeSelector : function(e){
|
||||||
|
if(e.target.tagName != 'SELECT'){
|
||||||
|
this.setState({
|
||||||
|
themeSelector : !this.state.themeSelector
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
changeTheme : function(e){
|
||||||
|
if(e.target.value == this.props.currentEditorTheme) return;
|
||||||
|
this.props.updateEditorTheme(e.target.value);
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
showThemeSelector : false,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
renderThemeSelector : function(){
|
||||||
|
return <div className='themeSelector'>
|
||||||
|
<select value={this.props.currentEditorTheme} onChange={this.changeTheme} >
|
||||||
|
{EditorThemes.map((theme, key)=>{
|
||||||
|
return <option key={key} value={theme}>{theme}</option>;
|
||||||
|
})}
|
||||||
|
</select>
|
||||||
|
</div>;
|
||||||
|
},
|
||||||
|
|
||||||
renderSnippetGroups : function(){
|
renderSnippetGroups : function(){
|
||||||
const snippets = this.state.snippets.filter((snippetGroup)=>snippetGroup.view === this.props.view);
|
const snippets = this.state.snippets.filter((snippetGroup)=>snippetGroup.view === this.props.view);
|
||||||
|
|
||||||
@@ -105,6 +141,7 @@ const Snippetbar = createClass({
|
|||||||
snippets={snippetGroup.snippets}
|
snippets={snippetGroup.snippets}
|
||||||
key={snippetGroup.groupName}
|
key={snippetGroup.groupName}
|
||||||
onSnippetClick={this.handleSnippetClick}
|
onSnippetClick={this.handleSnippetClick}
|
||||||
|
cursorPos={this.props.cursorPos}
|
||||||
/>;
|
/>;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -112,6 +149,22 @@ const Snippetbar = createClass({
|
|||||||
renderEditorButtons : function(){
|
renderEditorButtons : function(){
|
||||||
if(!this.props.showEditButtons) return;
|
if(!this.props.showEditButtons) return;
|
||||||
|
|
||||||
|
let foldButtons;
|
||||||
|
if(this.props.view == 'text'){
|
||||||
|
foldButtons =
|
||||||
|
<>
|
||||||
|
<div className={`editorTool foldAll ${this.props.foldCode ? 'active' : ''}`}
|
||||||
|
onClick={this.props.foldCode} >
|
||||||
|
<i className='fas fa-compress-alt' />
|
||||||
|
</div>
|
||||||
|
<div className={`editorTool unfoldAll ${this.props.unfoldCode ? 'active' : ''}`}
|
||||||
|
onClick={this.props.unfoldCode} >
|
||||||
|
<i className='fas fa-expand-alt' />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
return <div className='editors'>
|
return <div className='editors'>
|
||||||
<div className={`editorTool undo ${this.props.historySize.undo ? 'active' : ''}`}
|
<div className={`editorTool undo ${this.props.historySize.undo ? 'active' : ''}`}
|
||||||
onClick={this.props.undo} >
|
onClick={this.props.undo} >
|
||||||
@@ -121,6 +174,14 @@ const Snippetbar = createClass({
|
|||||||
onClick={this.props.redo} >
|
onClick={this.props.redo} >
|
||||||
<i className='fas fa-redo' />
|
<i className='fas fa-redo' />
|
||||||
</div>
|
</div>
|
||||||
|
<div className='divider'></div>
|
||||||
|
{foldButtons}
|
||||||
|
<div className={`editorTool editorTheme ${this.state.themeSelector ? 'active' : ''}`}
|
||||||
|
onClick={this.toggleThemeSelector} >
|
||||||
|
<i className='fas fa-palette' />
|
||||||
|
{this.state.themeSelector && this.renderThemeSelector()}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className='divider'></div>
|
<div className='divider'></div>
|
||||||
<div className={cx('text', { selected: this.props.view === 'text' })}
|
<div className={cx('text', { selected: this.props.view === 'text' })}
|
||||||
onClick={()=>this.props.onViewChange('text')}>
|
onClick={()=>this.props.onViewChange('text')}>
|
||||||
@@ -165,13 +226,13 @@ const SnippetGroup = createClass({
|
|||||||
},
|
},
|
||||||
handleSnippetClick : function(e, snippet){
|
handleSnippetClick : function(e, snippet){
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
this.props.onSnippetClick(execute(snippet.gen, this.props.brew));
|
this.props.onSnippetClick(execute(snippet.gen, this.props));
|
||||||
},
|
},
|
||||||
renderSnippets : function(snippets){
|
renderSnippets : function(snippets){
|
||||||
return _.map(snippets, (snippet)=>{
|
return _.map(snippets, (snippet)=>{
|
||||||
return <div className='snippet' key={snippet.name} onClick={(e)=>this.handleSnippetClick(e, snippet)}>
|
return <div className='snippet' key={snippet.name} onClick={(e)=>this.handleSnippetClick(e, snippet)}>
|
||||||
<i className={snippet.icon} />
|
<i className={snippet.icon} />
|
||||||
<span className='name'>{snippet.name}</span>
|
<span className='name'title={snippet.name}>{snippet.name}</span>
|
||||||
{snippet.experimental && <span className='beta'>beta</span>}
|
{snippet.experimental && <span className='beta'>beta</span>}
|
||||||
{snippet.subsnippets && <>
|
{snippet.subsnippets && <>
|
||||||
<i className='fas fa-caret-right'></i>
|
<i className='fas fa-caret-right'></i>
|
||||||
@@ -194,5 +255,4 @@ const SnippetGroup = createClass({
|
|||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
},
|
},
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,140 +1,186 @@
|
|||||||
@import (less) './client/icons/customIcons.less';
|
@import (less) './client/icons/customIcons.less';
|
||||||
.snippetBar{
|
@import (less) '././././themes/fonts/5e/fonts.less';
|
||||||
|
|
||||||
|
.snippetBar {
|
||||||
@menuHeight : 25px;
|
@menuHeight : 25px;
|
||||||
position : relative;
|
position : relative;
|
||||||
height : @menuHeight;
|
height : @menuHeight;
|
||||||
background-color : #ddd;
|
color : black;
|
||||||
.editors{
|
background-color : #DDDDDD;
|
||||||
|
|
||||||
|
.editors {
|
||||||
position : absolute;
|
position : absolute;
|
||||||
display : flex;
|
|
||||||
top : 0px;
|
top : 0px;
|
||||||
right : 0px;
|
right : 0px;
|
||||||
height : @menuHeight;
|
display : flex;
|
||||||
width : 125px;
|
|
||||||
justify-content : space-between;
|
justify-content : space-between;
|
||||||
&>div{
|
|
||||||
height : @menuHeight;
|
height : @menuHeight;
|
||||||
|
& > div {
|
||||||
width : @menuHeight;
|
width : @menuHeight;
|
||||||
cursor : pointer;
|
height : @menuHeight;
|
||||||
line-height : @menuHeight;
|
line-height : @menuHeight;
|
||||||
text-align : center;
|
text-align : center;
|
||||||
&:hover,&.selected{
|
cursor : pointer;
|
||||||
background-color : #999;
|
&:hover,&.selected { background-color : #999999; }
|
||||||
}
|
&.text {
|
||||||
&.text{
|
|
||||||
.tooltipLeft('Brew Editor');
|
.tooltipLeft('Brew Editor');
|
||||||
}
|
}
|
||||||
&.style{
|
&.style {
|
||||||
.tooltipLeft('Style Editor');
|
.tooltipLeft('Style Editor');
|
||||||
}
|
}
|
||||||
&.meta{
|
&.meta {
|
||||||
.tooltipLeft('Properties');
|
.tooltipLeft('Properties');
|
||||||
}
|
}
|
||||||
&.undo{
|
&.undo {
|
||||||
.tooltipLeft('Undo');
|
.tooltipLeft('Undo');
|
||||||
font-size : 0.75em;
|
font-size : 0.75em;
|
||||||
color : grey;
|
color : grey;
|
||||||
&.active{
|
&.active { color : inherit; }
|
||||||
color : black;
|
|
||||||
}
|
}
|
||||||
}
|
&.redo {
|
||||||
&.redo{
|
|
||||||
.tooltipLeft('Redo');
|
.tooltipLeft('Redo');
|
||||||
font-size : 0.75em;
|
font-size : 0.75em;
|
||||||
color : grey;
|
color : grey;
|
||||||
&.active{
|
&.active { color : inherit; }
|
||||||
|
}
|
||||||
|
&.foldAll {
|
||||||
|
.tooltipLeft('Fold All');
|
||||||
|
font-size : 0.75em;
|
||||||
|
color : inherit;
|
||||||
|
}
|
||||||
|
&.unfoldAll {
|
||||||
|
.tooltipLeft('Unfold All');
|
||||||
|
font-size : 0.75em;
|
||||||
|
color : inherit;
|
||||||
|
}
|
||||||
|
&.editorTheme {
|
||||||
|
.tooltipLeft('Editor Themes');
|
||||||
|
font-size : 0.75em;
|
||||||
color : black;
|
color : black;
|
||||||
|
&.active {
|
||||||
|
position : relative;
|
||||||
|
background-color : #999999;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&.divider {
|
&.divider {
|
||||||
background: linear-gradient(#000, #000) no-repeat center/1px 100%;
|
width : 5px;
|
||||||
width: 5px;
|
background : linear-gradient(currentColor, currentColor) no-repeat center/1px 100%;
|
||||||
&:hover{
|
&:hover { background-color : inherit; }
|
||||||
background-color: inherit;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.themeSelector {
|
||||||
|
position : absolute;
|
||||||
|
top : 25px;
|
||||||
|
right : 0;
|
||||||
|
z-index : 10;
|
||||||
|
display : flex;
|
||||||
|
align-items : center;
|
||||||
|
justify-content : center;
|
||||||
|
width : 170px;
|
||||||
|
height : inherit;
|
||||||
|
background-color : inherit;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.snippetBarButton{
|
.snippetBarButton {
|
||||||
height : @menuHeight;
|
|
||||||
line-height : @menuHeight;
|
|
||||||
display : inline-block;
|
display : inline-block;
|
||||||
|
height : @menuHeight;
|
||||||
padding : 0px 5px;
|
padding : 0px 5px;
|
||||||
font-weight : 800;
|
|
||||||
font-size : 0.625em;
|
font-size : 0.625em;
|
||||||
|
font-weight : 800;
|
||||||
|
line-height : @menuHeight;
|
||||||
text-transform : uppercase;
|
text-transform : uppercase;
|
||||||
cursor : pointer;
|
cursor : pointer;
|
||||||
&:hover, &.selected{
|
&:hover, &.selected { background-color : #999999; }
|
||||||
background-color : #999;
|
i {
|
||||||
}
|
|
||||||
i{
|
|
||||||
vertical-align : middle;
|
|
||||||
margin-right : 3px;
|
margin-right : 3px;
|
||||||
font-size : 1.4em;
|
font-size : 1.4em;
|
||||||
|
vertical-align : middle;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.toggleMeta{
|
.toggleMeta {
|
||||||
position : absolute;
|
position : absolute;
|
||||||
top : 0px;
|
top : 0px;
|
||||||
right : 0px;
|
right : 0px;
|
||||||
border-left : 1px solid black;
|
border-left : 1px solid black;
|
||||||
.tooltipLeft("Edit Brew Properties");
|
.tooltipLeft('Edit Brew Properties');
|
||||||
}
|
}
|
||||||
.snippetGroup{
|
.snippetGroup {
|
||||||
border-right : 1px solid black;
|
border-right : 1px solid currentColor;
|
||||||
&:hover{
|
&:hover {
|
||||||
&>.dropdown{
|
& > .dropdown { visibility : visible; }
|
||||||
visibility : visible;
|
|
||||||
}
|
}
|
||||||
}
|
.dropdown {
|
||||||
.dropdown{
|
|
||||||
position : absolute;
|
position : absolute;
|
||||||
top : 100%;
|
top : 100%;
|
||||||
visibility : hidden;
|
|
||||||
z-index : 1000;
|
z-index : 1000;
|
||||||
margin-left : -5px;
|
|
||||||
padding : 0px;
|
padding : 0px;
|
||||||
background-color : #ddd;
|
margin-left : -5px;
|
||||||
.snippet{
|
visibility : hidden;
|
||||||
position: relative;
|
background-color : #DDDDDD;
|
||||||
.animate(background-color);
|
.snippet {
|
||||||
|
position : relative;
|
||||||
display : flex;
|
display : flex;
|
||||||
align-items : center;
|
align-items : center;
|
||||||
min-width : max-content;
|
min-width : max-content;
|
||||||
padding : 5px;
|
padding : 5px;
|
||||||
cursor : pointer;
|
|
||||||
font-size : 10px;
|
font-size : 10px;
|
||||||
i{
|
cursor : pointer;
|
||||||
|
.animate(background-color);
|
||||||
|
i {
|
||||||
|
height : 1.2em;
|
||||||
margin-right : 8px;
|
margin-right : 8px;
|
||||||
font-size : 1.2em;
|
font-size : 1.2em;
|
||||||
height : 1.2em;
|
& ~ i {
|
||||||
&~i{
|
margin-right : 0;
|
||||||
margin-right: 0;
|
|
||||||
margin-left: 5px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.name {
|
|
||||||
margin-right : auto;
|
|
||||||
}
|
|
||||||
.beta {
|
|
||||||
color : white;
|
|
||||||
padding : 4px 6px;
|
|
||||||
line-height : 1em;
|
|
||||||
margin-left : 5px;
|
margin-left : 5px;
|
||||||
|
}
|
||||||
|
/* Fonts */
|
||||||
|
&.font {
|
||||||
|
height : auto;
|
||||||
|
&::before {
|
||||||
|
font-size : 1.4em;
|
||||||
|
content : 'ABC';
|
||||||
|
}
|
||||||
|
|
||||||
|
&.OpenSans {font-family : 'OpenSans';}
|
||||||
|
&.CodeBold {font-family : 'CodeBold';}
|
||||||
|
&.CodeLight {font-family : 'CodeLight';}
|
||||||
|
&.ScalySansRemake {font-family : 'ScalySansRemake';}
|
||||||
|
&.BookInsanityRemake {font-family : 'BookInsanityRemake';}
|
||||||
|
&.MrEavesRemake {font-family : 'MrEavesRemake';}
|
||||||
|
&.SolberaImitationRemake {font-family : 'SolberaImitationRemake';}
|
||||||
|
&.ScalySansSmallCapsRemake {font-family : 'ScalySansSmallCapsRemake';}
|
||||||
|
&.WalterTurncoat {font-family : 'WalterTurncoat';}
|
||||||
|
&.Lato {font-family : 'Lato';}
|
||||||
|
&.Courier {font-family : 'Courier';}
|
||||||
|
&.NodestoCapsCondensed {font-family : 'NodestoCapsCondensed';}
|
||||||
|
&.Overpass {font-family : 'Overpass';}
|
||||||
|
&.Davek {font-family : 'Davek';}
|
||||||
|
&.Iokharic {font-family : 'Iokharic';}
|
||||||
|
&.Rellanic {font-family : 'Rellanic';}
|
||||||
|
&.TimesNewRoman {font-family : 'Times New Roman';}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.name { margin-right : auto; }
|
||||||
|
.beta {
|
||||||
align-self : center;
|
align-self : center;
|
||||||
|
padding : 4px 6px;
|
||||||
|
margin-left : 5px;
|
||||||
|
font-family : monospace;
|
||||||
|
line-height : 1em;
|
||||||
|
color : white;
|
||||||
background : grey;
|
background : grey;
|
||||||
border-radius : 12px;
|
border-radius : 12px;
|
||||||
font-family : monospace;
|
|
||||||
}
|
}
|
||||||
&:hover{
|
&:hover {
|
||||||
background-color : #999;
|
background-color : #999999;
|
||||||
&>.dropdown{
|
& > .dropdown {
|
||||||
visibility : visible;
|
visibility : visible;
|
||||||
&.side {
|
&.side {
|
||||||
left: 100%;
|
top : 0%;
|
||||||
top: 0%;
|
left : 100%;
|
||||||
margin-left:0;
|
margin-left : 0;
|
||||||
box-shadow: -1px 1px 2px 0px #999;
|
box-shadow : -1px 1px 2px 0px #999999;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,8 +9,9 @@ const EditPage = require('./pages/editPage/editPage.jsx');
|
|||||||
const UserPage = require('./pages/userPage/userPage.jsx');
|
const UserPage = require('./pages/userPage/userPage.jsx');
|
||||||
const SharePage = require('./pages/sharePage/sharePage.jsx');
|
const SharePage = require('./pages/sharePage/sharePage.jsx');
|
||||||
const NewPage = require('./pages/newPage/newPage.jsx');
|
const NewPage = require('./pages/newPage/newPage.jsx');
|
||||||
//const ErrorPage = require('./pages/errorPage/errorPage.jsx');
|
const ErrorPage = require('./pages/errorPage/errorPage.jsx');
|
||||||
const PrintPage = require('./pages/printPage/printPage.jsx');
|
const PrintPage = require('./pages/printPage/printPage.jsx');
|
||||||
|
const ArchivePage = require('./pages/archivePage/archivePage.jsx');
|
||||||
const AccountPage = require('./pages/accountPage/accountPage.jsx');
|
const AccountPage = require('./pages/accountPage/accountPage.jsx');
|
||||||
|
|
||||||
const WithRoute = (props)=>{
|
const WithRoute = (props)=>{
|
||||||
@@ -74,10 +75,12 @@ const Homebrew = createClass({
|
|||||||
<Route path='/user/:username' element={<WithRoute el={UserPage} brews={this.props.brews} />} />
|
<Route path='/user/:username' element={<WithRoute el={UserPage} brews={this.props.brews} />} />
|
||||||
<Route path='/print/:id' element={<WithRoute el={PrintPage} brew={this.props.brew} />} />
|
<Route path='/print/:id' element={<WithRoute el={PrintPage} brew={this.props.brew} />} />
|
||||||
<Route path='/print' element={<WithRoute el={PrintPage} />} />
|
<Route path='/print' element={<WithRoute el={PrintPage} />} />
|
||||||
|
<Route path='/archive' element={<WithRoute el={ArchivePage}/>}/>
|
||||||
<Route path='/changelog' element={<WithRoute el={SharePage} brew={this.props.brew} />} />
|
<Route path='/changelog' element={<WithRoute el={SharePage} brew={this.props.brew} />} />
|
||||||
<Route path='/faq' element={<WithRoute el={SharePage} brew={this.props.brew} />} />
|
<Route path='/faq' element={<WithRoute el={SharePage} brew={this.props.brew} />} />
|
||||||
<Route path='/account' element={<WithRoute el={AccountPage} brew={this.props.brew} uiItems={this.props.brew.uiItems} />} />
|
<Route path='/account' element={<WithRoute el={AccountPage} brew={this.props.brew} uiItems={this.props.brew.uiItems} />} />
|
||||||
<Route path='/legacy' element={<WithRoute el={HomePage} brew={this.props.brew} />} />
|
<Route path='/legacy' element={<WithRoute el={HomePage} brew={this.props.brew} />} />
|
||||||
|
<Route path='/error' element={<WithRoute el={ErrorPage} brew={this.props.brew} />} />
|
||||||
<Route path='/' element={<WithRoute el={HomePage} brew={this.props.brew} />} />
|
<Route path='/' element={<WithRoute el={HomePage} brew={this.props.brew} />} />
|
||||||
<Route path='/*' element={<WithRoute el={HomePage} brew={this.props.brew} />} />
|
<Route path='/*' element={<WithRoute el={HomePage} brew={this.props.brew} />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ const ErrorNavItem = createClass({
|
|||||||
const error = this.props.error;
|
const error = this.props.error;
|
||||||
const response = error.response;
|
const response = error.response;
|
||||||
const status = response.status;
|
const status = response.status;
|
||||||
|
const HBErrorCode = response.body?.HBErrorCode;
|
||||||
const message = response.body?.message;
|
const message = response.body?.message;
|
||||||
let errMsg = '';
|
let errMsg = '';
|
||||||
try {
|
try {
|
||||||
@@ -40,7 +41,9 @@ const ErrorNavItem = createClass({
|
|||||||
{message ?? 'Conflict: please refresh to get latest changes'}
|
{message ?? 'Conflict: please refresh to get latest changes'}
|
||||||
</div>
|
</div>
|
||||||
</Nav.item>;
|
</Nav.item>;
|
||||||
} else if(status === 412) {
|
}
|
||||||
|
|
||||||
|
if(status === 412) {
|
||||||
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||||
Oops!
|
Oops!
|
||||||
<div className='errorContainer' onClick={clearError}>
|
<div className='errorContainer' onClick={clearError}>
|
||||||
@@ -49,6 +52,36 @@ const ErrorNavItem = createClass({
|
|||||||
</Nav.item>;
|
</Nav.item>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(HBErrorCode === '04') {
|
||||||
|
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||||
|
Oops!
|
||||||
|
<div className='errorContainer' onClick={clearError}>
|
||||||
|
You are no longer signed in as an author of
|
||||||
|
this brew! Were you signed out from a different
|
||||||
|
window? Visit our log in page, then try again!
|
||||||
|
<br></br>
|
||||||
|
<a target='_blank' rel='noopener noreferrer'
|
||||||
|
href={`https://www.naturalcrit.com/login?redirect=${window.location.href}`}>
|
||||||
|
<div className='confirm'>
|
||||||
|
Sign In
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<div className='deny'>
|
||||||
|
Not Now
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Nav.item>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(response.body?.errors?.[0].reason == 'storageQuotaExceeded') {
|
||||||
|
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||||
|
Oops!
|
||||||
|
<div className='errorContainer' onClick={clearError}>
|
||||||
|
Can't save because your Google Drive seems to be full!
|
||||||
|
</div>
|
||||||
|
</Nav.item>;
|
||||||
|
}
|
||||||
|
|
||||||
if(response.req.url.match(/^\/api.*Google.*$/m)){
|
if(response.req.url.match(/^\/api.*Google.*$/m)){
|
||||||
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||||
Oops!
|
Oops!
|
||||||
@@ -57,6 +90,7 @@ const ErrorNavItem = createClass({
|
|||||||
expired! Visit our log in page to sign out
|
expired! Visit our log in page to sign out
|
||||||
and sign back in with Google,
|
and sign back in with Google,
|
||||||
then try saving again!
|
then try saving again!
|
||||||
|
<br></br>
|
||||||
<a target='_blank' rel='noopener noreferrer'
|
<a target='_blank' rel='noopener noreferrer'
|
||||||
href={`https://www.naturalcrit.com/login?redirect=${window.location.href}`}>
|
href={`https://www.naturalcrit.com/login?redirect=${window.location.href}`}>
|
||||||
<div className='confirm'>
|
<div className='confirm'>
|
||||||
|
|||||||
@@ -81,35 +81,15 @@
|
|||||||
color : pink;
|
color : pink;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.recent.navItem {
|
.recent.navDropdownContainer {
|
||||||
position : relative;
|
position : relative;
|
||||||
.dropdown {
|
.navDropdown .navItem {
|
||||||
position : absolute;
|
|
||||||
z-index : 10000;
|
|
||||||
top : 28px;
|
|
||||||
left : 0;
|
|
||||||
overflow : hidden auto;
|
overflow : hidden auto;
|
||||||
width : 100%;
|
|
||||||
max-height : ~"calc(100vh - 28px)";
|
max-height : ~"calc(100vh - 28px)";
|
||||||
scrollbar-color : #666 #333;
|
scrollbar-color : #666 #333;
|
||||||
scrollbar-width : thin;
|
scrollbar-width : thin;
|
||||||
h4 {
|
|
||||||
font-size : 0.8em;
|
|
||||||
display : block;
|
|
||||||
box-sizing : border-box;
|
|
||||||
padding : 5px 0;
|
|
||||||
text-align : center;
|
|
||||||
color : #BBB;
|
|
||||||
border-top : 1px solid #888;
|
|
||||||
background-color : #333;
|
|
||||||
&:nth-of-type(1) {
|
|
||||||
background-color : darken(@teal, 20%);
|
|
||||||
}
|
|
||||||
&:nth-of-type(2) {
|
|
||||||
background-color : darken(@purple, 30%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.item {
|
|
||||||
#backgroundColorsHover;
|
#backgroundColorsHover;
|
||||||
.animate(background-color);
|
.animate(background-color);
|
||||||
position : relative;
|
position : relative;
|
||||||
@@ -164,6 +144,20 @@
|
|||||||
bottom : 2px;
|
bottom : 2px;
|
||||||
color : #888;
|
color : #888;
|
||||||
}
|
}
|
||||||
|
&.header {
|
||||||
|
display : block;
|
||||||
|
box-sizing : border-box;
|
||||||
|
padding : 5px 0;
|
||||||
|
text-align : center;
|
||||||
|
color : #BBB;
|
||||||
|
border-top : 1px solid #888;
|
||||||
|
background-color : #333;
|
||||||
|
&:nth-of-type(1) {
|
||||||
|
background-color : darken(@teal, 20%);
|
||||||
|
}
|
||||||
|
&:nth-of-type(2) {
|
||||||
|
background-color : darken(@purple, 30%);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ const Nav = require('naturalcrit/nav/nav.jsx');
|
|||||||
module.exports = function(props){
|
module.exports = function(props){
|
||||||
return <Nav.item
|
return <Nav.item
|
||||||
href='/new'
|
href='/new'
|
||||||
|
newTab={true}
|
||||||
color='purple'
|
color='purple'
|
||||||
icon='fas fa-plus-square'>
|
icon='fas fa-plus-square'>
|
||||||
new
|
new
|
||||||
|
|||||||
@@ -121,6 +121,7 @@ const RecentItems = createClass({
|
|||||||
|
|
||||||
removeItem : function(url, evt){
|
removeItem : function(url, evt){
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
|
evt.stopPropagation();
|
||||||
|
|
||||||
let edited = JSON.parse(localStorage.getItem(EDIT_KEY) || '[]');
|
let edited = JSON.parse(localStorage.getItem(EDIT_KEY) || '[]');
|
||||||
let viewed = JSON.parse(localStorage.getItem(VIEW_KEY) || '[]');
|
let viewed = JSON.parse(localStorage.getItem(VIEW_KEY) || '[]');
|
||||||
@@ -139,11 +140,11 @@ const RecentItems = createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
renderDropdown : function(){
|
renderDropdown : function(){
|
||||||
if(!this.state.showDropdown) return null;
|
// if(!this.state.showDropdown) return null;
|
||||||
|
|
||||||
const makeItems = (brews)=>{
|
const makeItems = (brews)=>{
|
||||||
return _.map(brews, (brew, i)=>{
|
return _.map(brews, (brew, i)=>{
|
||||||
return <a href={brew.url} className='item' key={`${brew.id}-${i}`} target='_blank' rel='noopener noreferrer' title={brew.title || '[ no title ]'}>
|
return <a className='navItem' href={brew.url} key={`${brew.id}-${i}`} target='_blank' rel='noopener noreferrer' title={brew.title || '[ no title ]'}>
|
||||||
<span className='title'>{brew.title || '[ no title ]'}</span>
|
<span className='title'>{brew.title || '[ no title ]'}</span>
|
||||||
<span className='time'>{Moment(brew.ts).fromNow()}</span>
|
<span className='time'>{Moment(brew.ts).fromNow()}</span>
|
||||||
<div className='clear' title='Remove from Recents' onClick={(e)=>{this.removeItem(`${brew.url}`, e);}}><i className='fas fa-times'></i></div>
|
<div className='clear' title='Remove from Recents' onClick={(e)=>{this.removeItem(`${brew.url}`, e);}}><i className='fas fa-times'></i></div>
|
||||||
@@ -151,25 +152,25 @@ const RecentItems = createClass({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return <div className='dropdown'>
|
return <>
|
||||||
{(this.props.showEdit && this.props.showView) ?
|
{(this.props.showEdit && this.props.showView) ?
|
||||||
<h4>edited</h4> : null }
|
<Nav.item className='header'>edited</Nav.item> : null }
|
||||||
{this.props.showEdit ?
|
{this.props.showEdit ?
|
||||||
makeItems(this.state.edit) : null }
|
makeItems(this.state.edit) : null }
|
||||||
{(this.props.showEdit && this.props.showView) ?
|
{(this.props.showEdit && this.props.showView) ?
|
||||||
<h4>viewed</h4> : null }
|
<Nav.item className='header'>viewed</Nav.item> : null }
|
||||||
{this.props.showView ?
|
{this.props.showView ?
|
||||||
makeItems(this.state.view) : null }
|
makeItems(this.state.view) : null }
|
||||||
</div>;
|
</>;
|
||||||
},
|
},
|
||||||
|
|
||||||
render : function(){
|
render : function(){
|
||||||
return <Nav.item icon='fas fa-history' color='grey' className='recent'
|
return <Nav.dropdown className='recent'>
|
||||||
onMouseEnter={()=>this.handleDropdown(true)}
|
<Nav.item icon='fas fa-history' color='grey' >
|
||||||
onMouseLeave={()=>this.handleDropdown(false)}>
|
|
||||||
{this.props.text}
|
{this.props.text}
|
||||||
|
</Nav.item>
|
||||||
{this.renderDropdown()}
|
{this.renderDropdown()}
|
||||||
</Nav.item>;
|
</Nav.dropdown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
|||||||
|
|
||||||
const NaturalCritIcon = require('naturalcrit/svg/naturalcrit.svg.jsx');
|
const NaturalCritIcon = require('naturalcrit/svg/naturalcrit.svg.jsx');
|
||||||
|
|
||||||
|
let SAVEKEY = '';
|
||||||
|
|
||||||
const AccountPage = createClass({
|
const AccountPage = createClass({
|
||||||
displayName : 'AccountPage',
|
displayName : 'AccountPage',
|
||||||
getDefaultProps : function() {
|
getDefaultProps : function() {
|
||||||
@@ -29,6 +31,27 @@ const AccountPage = createClass({
|
|||||||
uiItems : this.props.uiItems
|
uiItems : this.props.uiItems
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
componentDidMount : function(){
|
||||||
|
if(!this.state.saveLocation && this.props.uiItems.username) {
|
||||||
|
SAVEKEY = `HOMEBREWERY-DEFAULT-SAVE-LOCATION-${this.props.uiItems.username}`;
|
||||||
|
let saveLocation = window.localStorage.getItem(SAVEKEY);
|
||||||
|
saveLocation = saveLocation ?? (this.state.uiItems.googleId ? 'GOOGLE-DRIVE' : 'HOMEBREWERY');
|
||||||
|
this.makeActive(saveLocation);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
makeActive : function(newSelection){
|
||||||
|
if(this.state.saveLocation == newSelection) return;
|
||||||
|
window.localStorage.setItem(SAVEKEY, newSelection);
|
||||||
|
this.setState({
|
||||||
|
saveLocation : newSelection
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
renderButton : function(name, key, shouldRender=true){
|
||||||
|
if(!shouldRender) return;
|
||||||
|
return <button className={this.state.saveLocation==key ? 'active' : ''} onClick={()=>{this.makeActive(key);}}>{name}</button>;
|
||||||
|
},
|
||||||
|
|
||||||
renderNavItems : function() {
|
renderNavItems : function() {
|
||||||
return <Navbar>
|
return <Navbar>
|
||||||
@@ -61,6 +84,11 @@ const AccountPage = createClass({
|
|||||||
</p>
|
</p>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
<div className='dataGroup'>
|
||||||
|
<h4>Default Save Location</h4>
|
||||||
|
{this.renderButton('Homebrewery', 'HOMEBREWERY')}
|
||||||
|
{this.renderButton('Google Drive', 'GOOGLE-DRIVE', this.state.uiItems.googleId)}
|
||||||
|
</div>
|
||||||
</>;
|
</>;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
201
client/homebrew/pages/archivePage/archivePage.jsx
Normal file
201
client/homebrew/pages/archivePage/archivePage.jsx
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
require('./archivePage.less');
|
||||||
|
|
||||||
|
const React = require('react');
|
||||||
|
const createClass = require('create-react-class');
|
||||||
|
const _ = require('lodash');
|
||||||
|
const cx = require('classnames');
|
||||||
|
|
||||||
|
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||||
|
const Navbar = require('../../navbar/navbar.jsx');
|
||||||
|
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
||||||
|
const Account = require('../../navbar/account.navitem.jsx');
|
||||||
|
const NewBrew = require('../../navbar/newbrew.navitem.jsx');
|
||||||
|
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
||||||
|
const BrewItem = require('../basePages/listPage/brewItem/brewItem.jsx');
|
||||||
|
|
||||||
|
const request = require('../../utils/request-middleware.js');
|
||||||
|
|
||||||
|
const ArchivePage = createClass({
|
||||||
|
displayName : 'ArchivePage',
|
||||||
|
getDefaultProps : function () {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
getInitialState : function () {
|
||||||
|
return {
|
||||||
|
title : this.props.query.title || '',
|
||||||
|
brewCollection : null,
|
||||||
|
page : 1,
|
||||||
|
totalPages : 1,
|
||||||
|
searching : false,
|
||||||
|
error : null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
componentDidMount : function() {
|
||||||
|
|
||||||
|
},
|
||||||
|
handleChange(e) {
|
||||||
|
this.setState({ title: e.target.value });
|
||||||
|
},
|
||||||
|
|
||||||
|
updateStateWithBrews : function (brews, page, totalPages) {
|
||||||
|
this.setState({
|
||||||
|
brewCollection : brews || null,
|
||||||
|
page : page || 1,
|
||||||
|
totalPages : totalPages || 1,
|
||||||
|
searching : false
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
loadPage : async function(page) {
|
||||||
|
if(this.state.title == '') {} else {
|
||||||
|
|
||||||
|
try {
|
||||||
|
//this.updateUrl();
|
||||||
|
this.setState({ searching: true, error: null });
|
||||||
|
const title = encodeURIComponent(this.state.title);
|
||||||
|
await request.get(`/api/archive?title=${title}&page=${page}`)
|
||||||
|
.then((response)=>{
|
||||||
|
if(response.ok) {
|
||||||
|
this.updateStateWithBrews(response.body.brews, page, response.body.totalPages);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.log(`LoadPage error: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
updateUrl : function() {
|
||||||
|
const url = new URL(window.location.href);
|
||||||
|
const urlParams = new URLSearchParams(url.search);
|
||||||
|
|
||||||
|
// Set the title and page parameters
|
||||||
|
urlParams.set('title', this.state.title);
|
||||||
|
urlParams.set('page', this.state.page);
|
||||||
|
|
||||||
|
url.search = urlParams.toString(); // Convert URLSearchParams to string
|
||||||
|
window.history.replaceState(null, null, url);
|
||||||
|
},
|
||||||
|
|
||||||
|
renderFoundBrews() {
|
||||||
|
const { title, brewCollection, page, totalPages, error } = this.state;
|
||||||
|
|
||||||
|
if(title === '') {return (<div className='foundBrews noBrews'><h3>Whenever you want, just start typing...</h3></div>);}
|
||||||
|
|
||||||
|
if(error !== null) {
|
||||||
|
return (
|
||||||
|
<div className='foundBrews noBrews'>
|
||||||
|
<div><h3>I'm sorry, your request didn't work</h3>
|
||||||
|
<br /><p>Your search is not specific enough. Too many brews meet this criteria for us to display them.</p>
|
||||||
|
</div></div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!brewCollection || brewCollection.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className='foundBrews noBrews'>
|
||||||
|
<h3>We haven't found brews meeting your request.</h3>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='foundBrews'>
|
||||||
|
<span className='brewCount'>{`Brews Found: ${brewCollection.length}`}</span>
|
||||||
|
|
||||||
|
{brewCollection.map((brew, index)=>(
|
||||||
|
<BrewItem brew={brew} key={index} reportError={this.props.reportError} />
|
||||||
|
))}
|
||||||
|
<div className='paginationControls'>
|
||||||
|
{page > 1 && (
|
||||||
|
<button onClick={()=>this.loadPage(page - 1)}>Previous Page</button>
|
||||||
|
)}
|
||||||
|
<span className='currentPage'>Page {page}</span>
|
||||||
|
{page < totalPages && (
|
||||||
|
<button onClick={()=>this.loadPage(page + 1)}>Next Page</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
renderForm : function () {
|
||||||
|
return (
|
||||||
|
<div className='brewLookup'>
|
||||||
|
<h2>Brew Lookup</h2>
|
||||||
|
<label>Title of the brew</label>
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
value={this.state.title}
|
||||||
|
onChange={this.handleChange}
|
||||||
|
onKeyDown={(e)=>{
|
||||||
|
if(e.key === 'Enter') {
|
||||||
|
this.handleChange(e);
|
||||||
|
this.loadPage(1);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
placeholder='v3 Reference Document'
|
||||||
|
/>
|
||||||
|
{/* In the future, we should be able to filter the results by adding tags.
|
||||||
|
<label>Tags</label><input type='text' value={this.state.query} placeholder='add a tag to filter'/>
|
||||||
|
<input type="checkbox" id="v3" /><label>v3 only</label>
|
||||||
|
*/}
|
||||||
|
|
||||||
|
<button onClick={()=>{ this.handleChange({ target: { value: this.state.title } }); this.loadPage(1); }}>
|
||||||
|
<i
|
||||||
|
className={cx('fas', {
|
||||||
|
'fa-search' : !this.state.searching,
|
||||||
|
'fa-spin fa-spinner' : this.state.searching,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
renderNavItems : function () {
|
||||||
|
return (
|
||||||
|
<Navbar>
|
||||||
|
<Nav.section>
|
||||||
|
<Nav.item className='brewTitle'>Archive: Search for brews</Nav.item>
|
||||||
|
</Nav.section>
|
||||||
|
<Nav.section>
|
||||||
|
<NewBrew />
|
||||||
|
<HelpNavItem />
|
||||||
|
<RecentNavItem />
|
||||||
|
<Account />
|
||||||
|
</Nav.section>
|
||||||
|
</Navbar>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
render : function () {
|
||||||
|
return (
|
||||||
|
<div className='archivePage'>
|
||||||
|
<link href='/themes/V3/Blank/style.css' rel='stylesheet'/>
|
||||||
|
<link href='/themes/V3/5ePHB/style.css' rel='stylesheet'/>
|
||||||
|
{this.renderNavItems()}
|
||||||
|
|
||||||
|
<div className='content'>
|
||||||
|
<div className='welcome'>
|
||||||
|
<h1>Welcome to the Archive</h1>
|
||||||
|
</div>
|
||||||
|
<div className='flexGroup'>
|
||||||
|
<div className='form dataGroup'>{this.renderForm()}</div>
|
||||||
|
<div className='resultsContainer dataGroup'>
|
||||||
|
<div className='title'>
|
||||||
|
<h2>Your results, my lordship</h2>
|
||||||
|
</div>
|
||||||
|
{this.renderFoundBrews()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = ArchivePage;
|
||||||
173
client/homebrew/pages/archivePage/archivePage.less
Normal file
173
client/homebrew/pages/archivePage/archivePage.less
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
body {
|
||||||
|
height: 100vh;
|
||||||
|
|
||||||
|
.content {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.archivePage {
|
||||||
|
overflow-y: hidden;
|
||||||
|
height: 100%;
|
||||||
|
background-color: #2C3E50;
|
||||||
|
|
||||||
|
h1,h2,h3 {
|
||||||
|
font-family: 'Open Sans';
|
||||||
|
color: white;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: 20vh 1fr;
|
||||||
|
|
||||||
|
.welcome {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
background: url('https://i.imgur.com/MJ4YHu7.jpg');
|
||||||
|
background-size: 100%;
|
||||||
|
background-position: center;
|
||||||
|
height: 20vh;
|
||||||
|
border-bottom: 5px solid #333;
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 40px;
|
||||||
|
filter:drop-shadow(0 0 5px black);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.flexGroup {
|
||||||
|
height: 100%;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 500px 2fr;
|
||||||
|
background: #2C3E50;
|
||||||
|
|
||||||
|
.dataGroup {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: white;
|
||||||
|
|
||||||
|
&.form .brewLookup {
|
||||||
|
padding: 50px;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 30px;
|
||||||
|
border-bottom: 2px solid;
|
||||||
|
margin-block: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input+button {
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.resultsContainer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border-left: 2px solid;
|
||||||
|
height: 100%;
|
||||||
|
font-family: "BookInsanityRemake";
|
||||||
|
font-size: .34cm;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
height: 10vh;
|
||||||
|
background-color: #333;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 30px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.foundBrews {
|
||||||
|
position: relative;
|
||||||
|
background-color: #2C3E50;
|
||||||
|
width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
height: 66.7vh;
|
||||||
|
padding: 50px;
|
||||||
|
overflow-y:scroll;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.noBrews {
|
||||||
|
display:grid;
|
||||||
|
place-items:center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brewCount {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
right: 17px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: white;
|
||||||
|
background-color: #333;
|
||||||
|
padding: 8px 10px;
|
||||||
|
z-index: 1000;
|
||||||
|
font-family: 'Open Sans';
|
||||||
|
|
||||||
|
&:empty {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.limit {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 502px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: white;
|
||||||
|
background-color: #333;
|
||||||
|
padding: 8px 10px;
|
||||||
|
z-index: 1000;
|
||||||
|
font-family: 'Open Sans';
|
||||||
|
|
||||||
|
&:empty {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.brewItem {
|
||||||
|
background-image: url('/assets/parchmentBackground.jpg');
|
||||||
|
width: 48%;
|
||||||
|
margin-right: 40px;
|
||||||
|
color: black;
|
||||||
|
|
||||||
|
&:nth-child(even) {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
font-size: 0.75cm;
|
||||||
|
line-height: 0.988em;
|
||||||
|
font-family: "MrEavesRemake";
|
||||||
|
font-weight: 800;
|
||||||
|
color: var(--HB_Color_HeaderText);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
font-family: ScalySansRemake;
|
||||||
|
font-size: 1.2em;
|
||||||
|
|
||||||
|
>span {
|
||||||
|
margin-right: 12px;
|
||||||
|
line-height: 1.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ const moment = require('moment');
|
|||||||
const request = require('../../../../utils/request-middleware.js');
|
const request = require('../../../../utils/request-middleware.js');
|
||||||
|
|
||||||
const googleDriveIcon = require('../../../../googleDrive.svg');
|
const googleDriveIcon = require('../../../../googleDrive.svg');
|
||||||
|
const homebreweryIcon = require('../../../../thumbnail.png');
|
||||||
const dedent = require('dedent-tabs').default;
|
const dedent = require('dedent-tabs').default;
|
||||||
|
|
||||||
const BrewItem = createClass({
|
const BrewItem = createClass({
|
||||||
@@ -90,11 +91,17 @@ const BrewItem = createClass({
|
|||||||
</a>;
|
</a>;
|
||||||
},
|
},
|
||||||
|
|
||||||
renderGoogleDriveIcon : function(){
|
renderStorageIcon : function(){
|
||||||
if(!this.props.brew.googleId) return;
|
if(this.props.brew.googleId) {
|
||||||
|
return <span title={this.props.brew.webViewLink ? 'Your Google Drive Storage': 'Another User\'s Google Drive Storage'}>
|
||||||
return <span>
|
<a href={this.props.brew.webViewLink} target='_blank'>
|
||||||
<img className='googleDriveIcon' src={googleDriveIcon} alt='googleDriveIcon' />
|
<img className='googleDriveIcon' src={googleDriveIcon} alt='googleDriveIcon' />
|
||||||
|
</a>
|
||||||
|
</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <span title='Homebrewery Storage'>
|
||||||
|
<img className='homebreweryIcon' src={homebreweryIcon} alt='homebreweryIcon' />
|
||||||
</span>;
|
</span>;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -104,6 +111,8 @@ const BrewItem = createClass({
|
|||||||
brew.tags = brew.tags?.filter((tag)=>tag); //remove tags that are empty strings
|
brew.tags = brew.tags?.filter((tag)=>tag); //remove tags that are empty strings
|
||||||
}
|
}
|
||||||
const dateFormatString = 'YYYY-MM-DD HH:mm:ss';
|
const dateFormatString = 'YYYY-MM-DD HH:mm:ss';
|
||||||
|
const authors = brew.authors.length > 0 ? brew.authors : 'No authors';
|
||||||
|
|
||||||
|
|
||||||
return <div className='brewItem'>
|
return <div className='brewItem'>
|
||||||
{brew.thumbnail &&
|
{brew.thumbnail &&
|
||||||
@@ -118,7 +127,7 @@ const BrewItem = createClass({
|
|||||||
<div className='info'>
|
<div className='info'>
|
||||||
|
|
||||||
{brew.tags?.length ? <>
|
{brew.tags?.length ? <>
|
||||||
<div className='brewTags' title={`Tags:\n${brew.tags.join('\n')}`}>
|
<div className='brewTags' title={`${brew.tags.length} tags:\n${brew.tags.join('\n')}`}>
|
||||||
<i className='fas fa-tags'/>
|
<i className='fas fa-tags'/>
|
||||||
{brew.tags.map((tag, idx)=>{
|
{brew.tags.map((tag, idx)=>{
|
||||||
const matches = tag.match(/^(?:([^:]+):)?([^:]+)$/);
|
const matches = tag.match(/^(?:([^:]+):)?([^:]+)$/);
|
||||||
@@ -128,7 +137,18 @@ const BrewItem = createClass({
|
|||||||
</> : <></>
|
</> : <></>
|
||||||
}
|
}
|
||||||
<span title={`Authors:\n${brew.authors?.join('\n')}`}>
|
<span title={`Authors:\n${brew.authors?.join('\n')}`}>
|
||||||
<i className='fas fa-user'/> {brew.authors?.join(', ')}
|
<i className='fas fa-user'/> {Array.isArray(authors) ? (
|
||||||
|
<span>
|
||||||
|
{authors.map((author, index) => (
|
||||||
|
<span key={index}>
|
||||||
|
<a href={`/share/${author}`}>{author}</a>
|
||||||
|
{index < authors.length - 1 && ', '}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span>{authors}</span>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
<br />
|
<br />
|
||||||
<span title={`Last viewed: ${moment(brew.lastViewed).local().format(dateFormatString)}`}>
|
<span title={`Last viewed: ${moment(brew.lastViewed).local().format(dateFormatString)}`}>
|
||||||
@@ -144,7 +164,7 @@ const BrewItem = createClass({
|
|||||||
Last updated: ${moment(brew.updatedAt).local().format(dateFormatString)}`}>
|
Last updated: ${moment(brew.updatedAt).local().format(dateFormatString)}`}>
|
||||||
<i className='fas fa-sync-alt' /> {moment(brew.updatedAt).fromNow()}
|
<i className='fas fa-sync-alt' /> {moment(brew.updatedAt).fromNow()}
|
||||||
</span>
|
</span>
|
||||||
{this.renderGoogleDriveIcon()}
|
{this.renderStorageIcon()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='links'>
|
<div className='links'>
|
||||||
|
|||||||
@@ -48,6 +48,10 @@
|
|||||||
&>span{
|
&>span{
|
||||||
margin-right : 12px;
|
margin-right : 12px;
|
||||||
line-height : 1.5em;
|
line-height : 1.5em;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color:inherit;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.brewTags span {
|
.brewTags span {
|
||||||
@@ -98,4 +102,11 @@
|
|||||||
padding : 0px;
|
padding : 0px;
|
||||||
margin : -5px;
|
margin : -5px;
|
||||||
}
|
}
|
||||||
|
.homebreweryIcon {
|
||||||
|
mix-blend-mode : darken;
|
||||||
|
height : 24px;
|
||||||
|
position : relative;
|
||||||
|
top : 5px;
|
||||||
|
left : -5px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ const ListPage = createClass({
|
|||||||
sortBrewOrder : function(brew){
|
sortBrewOrder : function(brew){
|
||||||
if(!brew.title){brew.title = 'No Title';}
|
if(!brew.title){brew.title = 'No Title';}
|
||||||
const mapping = {
|
const mapping = {
|
||||||
'alpha' : _.deburr(brew.title.toLowerCase()),
|
'alpha' : _.deburr(brew.title.trim().toLowerCase()),
|
||||||
'created' : moment(brew.createdAt).format(),
|
'created' : moment(brew.createdAt).format(),
|
||||||
'updated' : moment(brew.updatedAt).format(),
|
'updated' : moment(brew.updatedAt).format(),
|
||||||
'views' : brew.views,
|
'views' : brew.views,
|
||||||
@@ -220,6 +220,7 @@ const ListPage = createClass({
|
|||||||
render : function(){
|
render : function(){
|
||||||
return <div className='listPage sitePage'>
|
return <div className='listPage sitePage'>
|
||||||
{/*<style>@layer V3_5ePHB, bundle;</style>*/}
|
{/*<style>@layer V3_5ePHB, bundle;</style>*/}
|
||||||
|
<link href='/themes/V3/Blank/style.css' rel='stylesheet'/>
|
||||||
<link href='/themes/V3/5ePHB/style.css' rel='stylesheet'/>
|
<link href='/themes/V3/5ePHB/style.css' rel='stylesheet'/>
|
||||||
{this.props.navItems}
|
{this.props.navItems}
|
||||||
{this.renderSortOptions()}
|
{this.renderSortOptions()}
|
||||||
|
|||||||
@@ -2,17 +2,18 @@
|
|||||||
.noColumns(){
|
.noColumns(){
|
||||||
column-count : auto;
|
column-count : auto;
|
||||||
column-fill : auto;
|
column-fill : auto;
|
||||||
column-gap : auto;
|
column-gap : normal;
|
||||||
column-width : auto;
|
column-width : auto;
|
||||||
-webkit-column-count : auto;
|
-webkit-column-count : auto;
|
||||||
-moz-column-count : auto;
|
-moz-column-count : auto;
|
||||||
-webkit-column-width : auto;
|
-webkit-column-width : auto;
|
||||||
-moz-column-width : auto;
|
-moz-column-width : auto;
|
||||||
-webkit-column-gap : auto;
|
-webkit-column-gap : normal;
|
||||||
-moz-column-gap : auto;
|
-moz-column-gap : normal;
|
||||||
height : auto;
|
height : auto;
|
||||||
min-height : 279.4mm;
|
min-height : 279.4mm;
|
||||||
margin : 20px auto;
|
margin : 20px auto;
|
||||||
|
contain : unset;
|
||||||
}
|
}
|
||||||
.listPage{
|
.listPage{
|
||||||
.content{
|
.content{
|
||||||
|
|||||||
@@ -1,47 +1,69 @@
|
|||||||
.uiPage{
|
.homebrew {
|
||||||
.content{
|
.uiPage.sitePage {
|
||||||
overflow-y : hidden;
|
.content {
|
||||||
width : 90vw;
|
width : ~"min(90vw, 1000px)";
|
||||||
background-color: #f0f0f0;
|
padding : 2% 4%;
|
||||||
font-family: 'Open Sans';
|
margin-top : 25px;
|
||||||
margin-left: auto;
|
margin-right : auto;
|
||||||
margin-right: auto;
|
margin-left : auto;
|
||||||
margin-top: 25px;
|
overflow-y : scroll;
|
||||||
padding: 2% 4%;
|
font-family : 'Open Sans';
|
||||||
font-size: 0.8em;
|
font-size : 0.8em;
|
||||||
line-height: 1.8em;
|
line-height : 1.8em;
|
||||||
.dataGroup{
|
background-color : #F0F0F0;
|
||||||
padding: 6px 20px 15px;
|
.dataGroup {
|
||||||
border: 2px solid black;
|
padding : 6px 20px 15px;
|
||||||
border-radius: 5px;
|
margin : 5px 0px;
|
||||||
margin: 5px 0px;
|
border : 2px solid black;
|
||||||
|
border-radius : 5px;
|
||||||
|
button {
|
||||||
|
background-color : transparent;
|
||||||
|
border : 1px solid black;
|
||||||
|
border-radius : 5px;
|
||||||
|
width : 125px;
|
||||||
|
color : black;
|
||||||
|
margin-right : 5px;
|
||||||
|
&.active {
|
||||||
|
background-color: #0007;
|
||||||
|
color: white;
|
||||||
|
&:before {
|
||||||
|
content: '\f00c';
|
||||||
|
font-family: 'FONT AWESOME 5 FREE';
|
||||||
|
margin-right: 5px;
|
||||||
}
|
}
|
||||||
h1, h2, h3, h4{
|
}
|
||||||
font-weight: 900;
|
}
|
||||||
text-transform: uppercase;
|
}
|
||||||
margin: 0.5em 30% 0.25em 0;
|
h1, h2, h3, h4 {
|
||||||
border-bottom: 2px solid slategrey;
|
width : 100%;
|
||||||
|
margin : 0.5em 30% 0.25em 0;
|
||||||
|
font-weight : 900;
|
||||||
|
text-transform : uppercase;
|
||||||
|
border-bottom : 2px solid slategrey;
|
||||||
}
|
}
|
||||||
h1 {
|
h1 {
|
||||||
font-size: 2em;
|
margin-right : 0;
|
||||||
border-bottom: 2px solid darkslategrey;
|
margin-bottom : 0.5em;
|
||||||
margin-bottom: 0.5em;
|
font-size : 2em;
|
||||||
margin-right: 0;
|
border-bottom : 2px solid darkslategrey;
|
||||||
}
|
|
||||||
h2 {
|
|
||||||
font-size: 1.75em;
|
|
||||||
}
|
}
|
||||||
|
h2 { font-size : 1.75em; }
|
||||||
h3 {
|
h3 {
|
||||||
font-size: 1.5em;
|
font-size : 1.5em;
|
||||||
svg {
|
svg { width : 19px; }
|
||||||
width: 19px;
|
|
||||||
}
|
}
|
||||||
|
h4 { font-size : 1.25em; }
|
||||||
|
strong { font-weight : bold; }
|
||||||
|
em { font-style : italic; }
|
||||||
|
ul {
|
||||||
|
padding-left : 1.25em;
|
||||||
|
list-style : square;
|
||||||
}
|
}
|
||||||
h4 {
|
.blank {
|
||||||
font-size: 1.25em;
|
height : 1em;
|
||||||
|
margin-top : 0;
|
||||||
|
& + * { margin-top : 0; }
|
||||||
}
|
}
|
||||||
strong {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -50,7 +50,8 @@ const EditPage = createClass({
|
|||||||
url : '',
|
url : '',
|
||||||
autoSave : true,
|
autoSave : true,
|
||||||
autoSaveWarning : false,
|
autoSaveWarning : false,
|
||||||
unsavedTime : new Date()
|
unsavedTime : new Date(),
|
||||||
|
currentEditorPage : 0
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
savedBrew : null,
|
savedBrew : null,
|
||||||
@@ -91,7 +92,7 @@ const EditPage = createClass({
|
|||||||
if(!(e.ctrlKey || e.metaKey)) return;
|
if(!(e.ctrlKey || e.metaKey)) return;
|
||||||
const S_KEY = 83;
|
const S_KEY = 83;
|
||||||
const P_KEY = 80;
|
const P_KEY = 80;
|
||||||
if(e.keyCode == S_KEY) this.save();
|
if(e.keyCode == S_KEY) this.trySave(true);
|
||||||
if(e.keyCode == P_KEY) window.open(`/print/${this.processShareId()}?dialog=true`, '_blank').focus();
|
if(e.keyCode == P_KEY) window.open(`/print/${this.processShareId()}?dialog=true`, '_blank').focus();
|
||||||
if(e.keyCode == P_KEY || e.keyCode == S_KEY){
|
if(e.keyCode == P_KEY || e.keyCode == S_KEY){
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -111,7 +112,8 @@ const EditPage = createClass({
|
|||||||
this.setState((prevState)=>({
|
this.setState((prevState)=>({
|
||||||
brew : { ...prevState.brew, text: text },
|
brew : { ...prevState.brew, text: text },
|
||||||
isPending : true,
|
isPending : true,
|
||||||
htmlErrors : htmlErrors
|
htmlErrors : htmlErrors,
|
||||||
|
currentEditorPage : this.refs.editor.getCurrentPage()
|
||||||
}), ()=>{if(this.state.autoSave) this.trySave();});
|
}), ()=>{if(this.state.autoSave) this.trySave();});
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -137,13 +139,14 @@ const EditPage = createClass({
|
|||||||
return !_.isEqual(this.state.brew, this.savedBrew);
|
return !_.isEqual(this.state.brew, this.savedBrew);
|
||||||
},
|
},
|
||||||
|
|
||||||
trySave : function(){
|
trySave : function(immediate=false){
|
||||||
if(!this.debounceSave) this.debounceSave = _.debounce(this.save, SAVE_TIMEOUT);
|
if(!this.debounceSave) this.debounceSave = _.debounce(this.save, SAVE_TIMEOUT);
|
||||||
if(this.hasChanges()){
|
if(this.hasChanges()){
|
||||||
this.debounceSave();
|
this.debounceSave();
|
||||||
} else {
|
} else {
|
||||||
this.debounceSave.cancel();
|
this.debounceSave.cancel();
|
||||||
}
|
}
|
||||||
|
if(immediate) this.debounceSave.flush();
|
||||||
},
|
},
|
||||||
|
|
||||||
handleGoogleClick : function(){
|
handleGoogleClick : function(){
|
||||||
@@ -404,6 +407,7 @@ const EditPage = createClass({
|
|||||||
theme={this.state.brew.theme}
|
theme={this.state.brew.theme}
|
||||||
errors={this.state.htmlErrors}
|
errors={this.state.htmlErrors}
|
||||||
lang={this.state.brew.lang}
|
lang={this.state.brew.lang}
|
||||||
|
currentEditorPage={this.state.currentEditorPage}
|
||||||
/>
|
/>
|
||||||
</SplitPane>
|
</SplitPane>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,44 +4,37 @@ const createClass = require('create-react-class');
|
|||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const cx = require('classnames');
|
const cx = require('classnames');
|
||||||
|
|
||||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
const UIPage = require('../basePages/uiPage/uiPage.jsx');
|
||||||
const Navbar = require('../../navbar/navbar.jsx');
|
|
||||||
const PatreonNavItem = require('../../navbar/patreon.navitem.jsx');
|
|
||||||
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
|
||||||
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
|
||||||
|
|
||||||
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
const Markdown = require('../../../../shared/naturalcrit/markdown.js');
|
||||||
|
|
||||||
|
const ErrorIndex = require('./errors/errorIndex.js');
|
||||||
|
|
||||||
const ErrorPage = createClass({
|
const ErrorPage = createClass({
|
||||||
|
displayName : 'ErrorPage',
|
||||||
|
|
||||||
getDefaultProps : function() {
|
getDefaultProps : function() {
|
||||||
return {
|
return {
|
||||||
ver : '0.0.0',
|
ver : '0.0.0',
|
||||||
errorId : ''
|
errorId : '',
|
||||||
|
text : '# Oops \n We could not find a brew with that id. **Sorry!**',
|
||||||
|
error : {}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
text : '# Oops \n We could not find a brew with that id. **Sorry!**',
|
|
||||||
|
|
||||||
render : function(){
|
render : function(){
|
||||||
return <div className='errorPage sitePage'>
|
const errorText = ErrorIndex(this.props)[this.props.brew.HBErrorCode.toString()] || '';
|
||||||
<Navbar ver={this.props.ver}>
|
|
||||||
<Nav.section>
|
|
||||||
<Nav.item className='errorTitle'>
|
|
||||||
Crit Fail!
|
|
||||||
</Nav.item>
|
|
||||||
</Nav.section>
|
|
||||||
|
|
||||||
<Nav.section>
|
return <UIPage brew={{ title: 'Crit Fail!' }}>
|
||||||
<PatreonNavItem />
|
<div className='dataGroup'>
|
||||||
<HelpNavItem />
|
<div className='errorTitle'>
|
||||||
<RecentNavItem />
|
<h1>{`Error ${this.props.brew.status || '000'}`}</h1>
|
||||||
</Nav.section>
|
<h4>{this.props.brew.text || 'No error text'}</h4>
|
||||||
</Navbar>
|
|
||||||
|
|
||||||
<div className='content'>
|
|
||||||
<BrewRenderer text={this.text} />
|
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
<hr />
|
||||||
|
<div dangerouslySetInnerHTML={{ __html: Markdown.render(errorText) }} />
|
||||||
|
</div>
|
||||||
|
</UIPage>;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
.errorPage{
|
.homebrew {
|
||||||
.errorTitle{
|
.uiPage.sitePage {
|
||||||
background-color: @orange;
|
.errorTitle {
|
||||||
|
//background-color: @orange;
|
||||||
|
color : #D02727;
|
||||||
|
text-align : center;
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
h1, h2, h3, h4 { border-bottom : none; }
|
||||||
|
hr { border-bottom : 2px solid slategrey; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
128
client/homebrew/pages/errorPage/errors/errorIndex.js
Normal file
128
client/homebrew/pages/errorPage/errors/errorIndex.js
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
const dedent = require('dedent-tabs').default;
|
||||||
|
|
||||||
|
const loginUrl = 'https://www.naturalcrit.com/login';
|
||||||
|
|
||||||
|
const errorIndex = (props)=>{
|
||||||
|
return {
|
||||||
|
// Default catch all
|
||||||
|
'00' : dedent`
|
||||||
|
## An unknown error occurred!
|
||||||
|
|
||||||
|
We aren't sure what happened, but our server wasn't able to find what you
|
||||||
|
were looking for.`,
|
||||||
|
|
||||||
|
// General Google load error
|
||||||
|
'01' : dedent`
|
||||||
|
## An error occurred while retrieving this brew from Google Drive!
|
||||||
|
|
||||||
|
Google reported an error while attempting to retrieve a brew from this link.`,
|
||||||
|
|
||||||
|
// Google Drive - 404 : brew deleted or access denied
|
||||||
|
'02' : dedent`
|
||||||
|
## We can't find this brew in Google Drive!
|
||||||
|
|
||||||
|
This file was saved on Google Drive, but this link doesn't work anymore.
|
||||||
|
${props.brew.authors?.length > 0
|
||||||
|
? `Note that this brew belongs to the Homebrewery account **${props.brew.authors[0]}**,
|
||||||
|
${props.brew.account
|
||||||
|
? `which is
|
||||||
|
${props.brew.authors[0] == props.brew.account
|
||||||
|
? `your account.`
|
||||||
|
: `not your account (you are currently signed in as **${props.brew.account}**).`
|
||||||
|
}`
|
||||||
|
: 'and you are not currently signed in to any account.'
|
||||||
|
}`
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
The Homebrewery cannot delete files from Google Drive on its own, so there
|
||||||
|
are three most likely possibilities:
|
||||||
|
:
|
||||||
|
- **The Google Drive files may have been accidentally deleted.** Look in
|
||||||
|
the Google Drive account that owns this brew (or ask the owner to do so),
|
||||||
|
and make sure the Homebrewery folder is still there, and that it holds your brews
|
||||||
|
as text files.
|
||||||
|
- **You may have changed the sharing settings for your files.** If the files
|
||||||
|
are still on Google Drive, change all of them to be shared *with everyone who has
|
||||||
|
the link* so the Homebrewery can access them.
|
||||||
|
- **The Google Account may be closed.** Google may have removed the account
|
||||||
|
due to inactivity or violating a Google policy. Make sure the owner can
|
||||||
|
still access Google Drive normally and upload/download files to it.
|
||||||
|
:
|
||||||
|
If the file isn't found, Google Drive usually puts your file in your Trash folder for
|
||||||
|
30 days. Assuming the trash hasn't been emptied yet, it might be worth checking.
|
||||||
|
You can also find the Activity tab on the right side of the Google Drive page, which
|
||||||
|
shows the recent activity on Google Drive. This can help you pin down the exact date
|
||||||
|
the brew was deleted or moved, and by whom.
|
||||||
|
:
|
||||||
|
If the brew still isn't found, some people have had success asking Google to recover
|
||||||
|
accidentally deleted files at this link:
|
||||||
|
https://support.google.com/drive/answer/1716222?hl=en&ref_topic=7000946.
|
||||||
|
At the bottom of the page there is a button that says *Send yourself an Email*
|
||||||
|
and you will receive instructions on how to request the files be restored.
|
||||||
|
:
|
||||||
|
Also note, if you prefer not to use your Google Drive for storage, you can always
|
||||||
|
change the storage location of a brew by clicking the Google drive icon by the
|
||||||
|
brew title and choosing *transfer my brew to/from Google Drive*.`,
|
||||||
|
|
||||||
|
// User is not Authors list
|
||||||
|
'03' : dedent`
|
||||||
|
## Current signed-in user does not have editor access to this brew.
|
||||||
|
|
||||||
|
If you believe you should have access to this brew, ask one of its authors to invite you
|
||||||
|
as an author by opening the Edit page for the brew, viewing the {{fa,fa-info-circle}}
|
||||||
|
**Properties** tab, and adding your username to the "invited authors" list. You can
|
||||||
|
then try to access this document again.
|
||||||
|
|
||||||
|
**Brew Title:** ${props.brew.brewTitle || 'Unable to show title'}
|
||||||
|
|
||||||
|
**Current Authors:** ${props.brew.authors?.map((author)=>{return `${author}`;}).join(', ') || 'Unable to list authors'}
|
||||||
|
|
||||||
|
[Click here to be redirected to the brew's share page.](/share/${props.brew.shareId})`,
|
||||||
|
|
||||||
|
// User is not signed in; must be a user on the Authors List
|
||||||
|
'04' : dedent`
|
||||||
|
## Sign-in required to edit this brew.
|
||||||
|
|
||||||
|
You must be logged in to one of the accounts listed as an author of this brew.
|
||||||
|
User is not logged in. Please log in [here](${loginUrl}).
|
||||||
|
|
||||||
|
**Brew Title:** ${props.brew.brewTitle || 'Unable to show title'}
|
||||||
|
|
||||||
|
**Current Authors:** ${props.brew.authors?.map((author)=>{return `${author}`;}).join(', ') || 'Unable to list authors'}`,
|
||||||
|
|
||||||
|
// Brew load error
|
||||||
|
'05' : dedent`
|
||||||
|
## No Homebrewery document could be found.
|
||||||
|
|
||||||
|
The server could not locate the Homebrewery document. It was likely deleted by
|
||||||
|
its owner.
|
||||||
|
|
||||||
|
**Requested access:** ${props.brew.accessType}
|
||||||
|
|
||||||
|
**Brew ID:** ${props.brew.brewId}`,
|
||||||
|
|
||||||
|
// Brew save error
|
||||||
|
'06' : dedent`
|
||||||
|
## Unable to save Homebrewery document.
|
||||||
|
|
||||||
|
An error occurred wil attempting to save the Homebrewery document.`,
|
||||||
|
|
||||||
|
// Brew delete error
|
||||||
|
'07' : dedent`
|
||||||
|
## Unable to delete Homebrewery document.
|
||||||
|
|
||||||
|
An error occurred while attempting to remove the Homebrewery document.
|
||||||
|
|
||||||
|
**Brew ID:** ${props.brew.brewId}`,
|
||||||
|
|
||||||
|
// Author delete error
|
||||||
|
'08' : dedent`
|
||||||
|
## Unable to remove user from Homebrewery document.
|
||||||
|
|
||||||
|
An error occurred while attempting to remove the user from the Homebrewery document author list!
|
||||||
|
|
||||||
|
**Brew ID:** ${props.brew.brewId}`,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = errorIndex;
|
||||||
@@ -16,9 +16,9 @@ The Homebrewery makes the creation and sharing of authentic looking Fifth-Editio
|
|||||||
**Try it!** Simply edit the text on the left and watch it *update live* on the right. Note that not every button is visible on this demo page. Click New {{fas,fa-plus-square}} in the navbar above to start brewing with all the features!
|
**Try it!** Simply edit the text on the left and watch it *update live* on the right. Note that not every button is visible on this demo page. Click New {{fas,fa-plus-square}} in the navbar above to start brewing with all the features!
|
||||||
|
|
||||||
### Editing and Sharing
|
### Editing and Sharing
|
||||||
When you create your own homebrew, you will be given a *edit url* and a *share url*.
|
When you create a new homebrew document ("brew"), your document will be given a *edit link* and a *share link*.
|
||||||
|
|
||||||
Any changes you make while on the *edit url* will be automatically saved to the database within a few seconds. Anyone with the edit url will be able to make edits to your homebrew, so be careful about who you share it with.
|
The *edit link* is where you write your brew. If you edit a brew while logged in, you are added as one of the brew's authors, and no one else can edit that brew until you add them as a new author via the {{fa,fa-info-circle}} **Properties** tab. Brews without any author can still be edited by anyone with the *edit link*, so be careful about who you share it with if you prefer to work without an account.
|
||||||
|
|
||||||
Anyone with the *share url* will be able to access a read-only version of your homebrew.
|
Anyone with the *share url* will be able to access a read-only version of your homebrew.
|
||||||
|
|
||||||
@@ -48,57 +48,63 @@ If you want to save ink or have a monochrome printer, add the **PRINT → {{fas,
|
|||||||
|
|
||||||
\column
|
\column
|
||||||
|
|
||||||
## New in V3.0.0
|
## V3 vs Legacy
|
||||||
We've implemented an extended Markdown-like syntax for block and span elements, plus a few other changes, eliminating the need for HTML tags like `div` and `span` in most cases. No raw HTML tags should be needed in a brew (*but can still be used if you insist*).
|
The Homebrewery has two renderers: Legacy and V3. The V3 renderer is recommended for all users because it is more powerful, more customizable, and continues to receive new feature updates while Legacy does not. However Legacy mode will remain available for older brews and veteran users.
|
||||||
|
|
||||||
Much of the syntax and styling has changed in V3, so converting a Legacy brew to V3 (or vice-versa) will require tweaking your document. *However*, all brews made prior to the release of v3.0.0 will still render normally, and you may switch between the "Legacy" brew renderer and the newer "V3" renderer via the {{fa,fa-info-circle}} **Properties** button on your brew at any time.
|
At any time, any individual brew can be changed to your renderer of choice via the {{fa,fa-info-circle}} **Properties** tab on your brew. However, converting between Legacy and V3 may require heavily tweaking the document; while both renderers can use raw HTML, V3 prefers a streamlined curly bracket syntax that avoids the complex HTML structures required by Legacy.
|
||||||
|
|
||||||
Scroll down to the next page for a brief summary of the changes and new features available in V3!
|
|
||||||
|
|
||||||
|
Scroll down to the next page for a brief summary of the changes and features available in V3!
|
||||||
#### New Things All The Time!
|
#### New Things All The Time!
|
||||||
Check out the latest updates in the full changelog [here](/changelog).
|
Check out the latest updates in the full changelog [here](/changelog).
|
||||||
|
|
||||||
### Helping out
|
### Helping out
|
||||||
Like this tool? Want to buy me a beer? [Head here](https://www.patreon.com/Naturalcrit) to help me keep the servers running.
|
Like this tool? Head over to our [Patreon](https://www.patreon.com/Naturalcrit) to help us keep the servers running.
|
||||||
|
|
||||||
This tool will **always** be free, never have ads, and I will never offer any "premium" features or whatever.
|
|
||||||
|
This tool will **always** be free, never have ads, and we will never offer any "premium" features or whatever.
|
||||||
|
|
||||||
### Bugs, Issues, Suggestions?
|
### Bugs, Issues, Suggestions?
|
||||||
Take a quick look at our [Frequently Asked Questions page](/faq) to see if your question has a handy answer.
|
- Check the [Frequently Asked Questions](/faq) page first for quick answers.
|
||||||
|
- Get help or the right look for your brew by posting on [r/Homebrewery](https://www.reddit.com/r/homebrewery/submit?selftext=true&title=%5BIssue%5D%20Describe%20Your%20Issue%20Here) or joining the [Discord Of Many Things](https://discord.gg/by3deKx).
|
||||||
Need help getting started or just the right look for your brew? Head to [r/Homebrewery](https://www.reddit.com/r/homebrewery/submit?selftext=true&title=%5BIssue%5D%20Describe%20Your%20Issue%20Here) and let us know!
|
- Report technical issues or provide feedback on the [GitHub Repo](https://github.com/naturalcrit/homebrewery/).
|
||||||
|
|
||||||
Have an idea to make The Homebrewery better? Or did you find something that wasn't quite right? Check out the [GitHub Repo](https://github.com/naturalcrit/homebrewery/) to report technical issues.
|
|
||||||
|
|
||||||
### Legal Junk
|
### Legal Junk
|
||||||
The Homebrewery is licensed using the [MIT License](https://github.com/naturalcrit/homebrewery/blob/master/license). Which means you are free to use The Homebrewery codebase any way that you want, except for claiming that you made it yourself.
|
The Homebrewery is licensed using the [MIT License](https://github.com/naturalcrit/homebrewery/blob/master/license). Which means you are free to use The Homebrewery codebase any way that you want, except for claiming that you made it yourself.
|
||||||
|
|
||||||
If you wish to sell or in some way gain profit for what's created on this site, it's your responsibility to ensure you have the proper licenses/rights for any images or resources used.
|
If you wish to sell or in some way gain profit for what's created on this site, it's your responsibility to ensure you have the proper licenses/rights for any images or resources used.
|
||||||
|
#### Crediting Us
|
||||||
#### Crediting Me
|
If you'd like to credit us in your brew, we'd be flattered! Just reference that you made it with The Homebrewery.
|
||||||
If you'd like to credit me in your brew, I'd be flattered! Just reference that you made it with The Homebrewery.
|
|
||||||
|
|
||||||
### More Homebrew Resources
|
### More Homebrew Resources
|
||||||
<a href='https://discord.gg/by3deKx' target='_blank'><img src='/assets/discordOfManyThings.svg' alt='Discord of Many Things Logo' title='Discord of Many Things Logo' style='width:50px; float: right; padding-left: 10px;'/></a>
|
[{width:50px,float:right,padding-left:10px}](https://discord.gg/by3deKx)
|
||||||
If you are looking for more 5e Homebrew resources check out [r/UnearthedArcana](https://www.reddit.com/r/UnearthedArcana/) and their list of useful resources [here](https://www.reddit.com/r/UnearthedArcana/wiki/resources). The <a href='https://discord.gg/by3deKx' target='_blank' title='Discord of Many Things'>Discord of Many Things</a> is another great resource to connect with fellow homebrewers for help and feedback.
|
|
||||||
|
If you are looking for more 5e Homebrew resources check out [r/UnearthedArcana](https://www.reddit.com/r/UnearthedArcana/) and their list of useful resources [here](https://www.reddit.com/r/UnearthedArcana/wiki/resources). The [Discord Of Many Things](https://discord.gg/by3deKx) is another great resource to connect with fellow homebrewers for help and feedback.
|
||||||
|
|
||||||
|
|
||||||
{{position:absolute;top:20px;right:20px;width:auto
|
{{position:absolute;top:20px;right:20px;width:auto
|
||||||
<a href='https://discord.gg/by3deKx' target='_blank' title='Discord of Many Things' style='color: black;'><img src='/assets/discord.png' style='height:30px'/></a>
|
[{height:30px}](https://discord.gg/by3deKx)
|
||||||
<a href='https://github.com/naturalcrit/homebrewery' target='_blank' title='Github' style='color: black; padding-left: 5px;'><img src='/assets/github.png' style='height:30px'/></a>
|
[{height:30px}](https://github.com/naturalcrit/homebrewery)
|
||||||
<a href='https://patreon.com/NaturalCrit' target='_blank' title='Patreon' style='color: black; padding-left: 5px;'><img src='/assets/patreon.png' style='height:30px'/></a>
|
[{height:30px}](https://patreon.com/NaturalCrit)
|
||||||
<a href='https://www.reddit.com/r/homebrewery/' target='_blank' title='Reddit' style='color: black; padding-left: 5px;'><img src='/assets/reddit.png' style='height:30px'/></a>
|
[{height:30px}](https://www.reddit.com/r/homebrewery/)
|
||||||
}}
|
}}
|
||||||
|
|
||||||
\page
|
\page
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Markdown+
|
## Markdown+
|
||||||
The Homebrewery aims to make homebrewing as simple as possible, providing a live editor with Markdown syntax that is more human-readable and faster to write with than raw HTML.
|
The Homebrewery aims to make homebrewing as simple as possible, providing a live editor with Markdown syntax that is more human-readable and faster to write with than raw HTML.
|
||||||
|
|
||||||
In version 3.0.0, with a goal of adding maximum flexibility without users resorting to complex HTML to accomplish simple tasks, Homebrewery provides an extended verision of Markdown with additional syntax.
|
From version 3.0.0, with a goal of adding maximum flexibility without users resorting to complex HTML to accomplish simple tasks, Homebrewery provides an extended verision of Markdown with additional syntax.
|
||||||
**You can enable V3 via the {{fa,fa-info-circle}} Properties button!**
|
|
||||||
|
|
||||||
### Curly Brackets
|
### Curly Brackets
|
||||||
The biggest change in V3 is the replacement of `<span></span>` and `<div></div>` with `{{ }}` for a cleaner custom formatting. Inline spans and block elements can be created and given ID's and Classes, as well as css properties, each of which are comma separated with no spaces. Use double quotes if a value requires spaces. Spans and Blocks start the same:
|
Standard Markdown lacks several equivalences to HTML. Hence, we have introduced `{{ }}` as a replacement for `<span></span>` and `<div></div>` for a cleaner custom formatting. Inline spans and block elements can be created and given ID's and Classes, as well as CSS properties, each of which are comma separated with no spaces. Use double quotes if a value requires spaces. Spans and Blocks start the same:
|
||||||
|
|
||||||
#### Span
|
#### Span
|
||||||
My favorite author is {{pen,#author,color:orange,font-family:"trebuchet ms" Brandon Sanderson}}. The orange text has a class of `pen`, an id of `author`, is colored orange, and given a new font. The first space outside of quotes marks the beginning of the content.
|
My favorite author is {{pen,#author,color:orange,font-family:"trebuchet ms" Brandon Sanderson}}. The orange text has a class of `pen`, an id of `author`, is colored orange, and given a new font. The first space outside of quotes marks the beginning of the content.
|
||||||
@@ -126,16 +132,17 @@ A blank line can be achieved with a run of one or more `:` alone on a line. More
|
|||||||
|
|
||||||
::
|
::
|
||||||
|
|
||||||
|
|
||||||
Much nicer than `<br><br><br><br><br>`
|
Much nicer than `<br><br><br><br><br>`
|
||||||
|
|
||||||
### Definition Lists
|
### Definition Lists
|
||||||
**Example** :: V3 uses HTML *definition lists* to create "lists" with hanging indents.
|
**Example** :: V3 uses HTML *definition lists* to create "lists" with hanging indents.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Column Breaks
|
### Column Breaks
|
||||||
Column and page breaks with `\column` and `\page`.
|
Column and page breaks with `\column` and `\page`.
|
||||||
|
|
||||||
\column
|
|
||||||
|
|
||||||
### Tables
|
### Tables
|
||||||
Tables now allow column & row spanning between cells. This is included in some updated snippets, but a simplified example is given below.
|
Tables now allow column & row spanning between cells. This is included in some updated snippets, but a simplified example is given below.
|
||||||
|
|
||||||
@@ -163,13 +170,13 @@ Using *Curly Injection* you can assign an id, classes, or inline CSS properties
|
|||||||
|
|
||||||
 {width:100px,border:"2px solid",border-radius:10px}
|
 {width:100px,border:"2px solid",border-radius:10px}
|
||||||
|
|
||||||
\* *When using Imgur-hosted images, use the "direct link", which can be found when you click into your image in the Imgur interace.*
|
\* *When using Imgur-hosted images, use the "direct link", which can be found when you click into your image in the Imgur interface.*
|
||||||
|
|
||||||
## Snippets
|
## Snippets
|
||||||
Homebrewery comes with a series of *code snippets* found at the top of the editor pane that make it easy to create brews as quickly as possible. Just set your cursor where you want the code to appear in the editor pane, choose a snippet, and make the adjustments you need.
|
Homebrewery comes with a series of *code snippets* found at the top of the editor pane that make it easy to create brews as quickly as possible. Just set your cursor where you want the code to appear in the editor pane, choose a snippet, and make the adjustments you need.
|
||||||
|
|
||||||
## Style Editor Panel
|
## Style Editor Panel
|
||||||
{{fa,fa-paint-brush}} Technically released prior to v3 but still new to many users, check out the new **Style Editor** located on the right side of the Snippet bar. This editor accepts CSS for styling without requiring `<style>` tags-- anything that would have gone inside style tags before can now be placed here, and snippets that insert CSS styles are now located on that tab.
|
{{fa,fa-paint-brush}} Usually overlooked or unused by some users, the **Style Editor** tab is located on the right side of the Snippet bar. This editor accepts CSS for styling without requiring `<style>` tags-- anything that would have gone inside style tags before can now be placed here, and snippets that insert CSS styles are now located on that tab.
|
||||||
|
|
||||||
{{pageNumber 2}}
|
{{pageNumber 2}}
|
||||||
{{footnote PART 2 | BORING STUFF}}
|
{{footnote PART 2 | BORING STUFF}}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ const { DEFAULT_BREW } = require('../../../../server/brewDefaults.js');
|
|||||||
const BREWKEY = 'homebrewery-new';
|
const BREWKEY = 'homebrewery-new';
|
||||||
const STYLEKEY = 'homebrewery-new-style';
|
const STYLEKEY = 'homebrewery-new-style';
|
||||||
const METAKEY = 'homebrewery-new-meta';
|
const METAKEY = 'homebrewery-new-meta';
|
||||||
|
let SAVEKEY;
|
||||||
|
|
||||||
|
|
||||||
const NewPage = createClass({
|
const NewPage = createClass({
|
||||||
@@ -62,11 +63,15 @@ const NewPage = createClass({
|
|||||||
brew.renderer = metaStorage?.renderer ?? brew.renderer;
|
brew.renderer = metaStorage?.renderer ?? brew.renderer;
|
||||||
brew.theme = metaStorage?.theme ?? brew.theme;
|
brew.theme = metaStorage?.theme ?? brew.theme;
|
||||||
brew.lang = metaStorage?.lang ?? brew.lang;
|
brew.lang = metaStorage?.lang ?? brew.lang;
|
||||||
|
}
|
||||||
|
|
||||||
|
SAVEKEY = `HOMEBREWERY-DEFAULT-SAVE-LOCATION-${global.account?.username || ''}`;
|
||||||
|
const saveStorage = localStorage.getItem(SAVEKEY) || 'HOMEBREWERY';
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
brew : brew
|
brew : brew,
|
||||||
|
saveGoogle : (saveStorage == 'GOOGLE-DRIVE' && this.state.saveGoogle)
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
localStorage.setItem(BREWKEY, brew.text);
|
localStorage.setItem(BREWKEY, brew.text);
|
||||||
if(brew.style)
|
if(brew.style)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ const template = async function(name, title='', props = {}){
|
|||||||
return `<!DOCTYPE html>
|
return `<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, height=device-height, interactive-widget=resizes-visual" />
|
||||||
<link href="//use.fontawesome.com/releases/v5.15.1/css/all.css" rel="stylesheet" />
|
<link href="//use.fontawesome.com/releases/v5.15.1/css/all.css" rel="stylesheet" />
|
||||||
<link href="//fonts.googleapis.com/css?family=Open+Sans:400,300,600,700" rel="stylesheet" type="text/css" />
|
<link href="//fonts.googleapis.com/css?family=Open+Sans:400,300,600,700" rel="stylesheet" type="text/css" />
|
||||||
<link href=${`/${name}/bundle.css`} rel='stylesheet' />
|
<link href=${`/${name}/bundle.css`} rel='stylesheet' />
|
||||||
|
|||||||
27
faq.md
27
faq.md
@@ -62,16 +62,13 @@ pre {
|
|||||||
```
|
```
|
||||||
|
|
||||||
# FAQ
|
# FAQ
|
||||||
{{wide Updated Oct. 11, 2021}}
|
{{wide Updated Apr. 15, 2023}}
|
||||||
|
|
||||||
|
|
||||||
### The site is down for me! Anyone else?
|
### The site is down for me! Anyone else?
|
||||||
|
|
||||||
You can check the site status here: [Everyone or Just Me](https://downforeveryoneorjustme.com/homebrewery.naturalcrit.com)
|
You can check the site status here: [Everyone or Just Me](https://downforeveryoneorjustme.com/homebrewery.naturalcrit.com)
|
||||||
|
|
||||||
### How do I log out?
|
|
||||||
|
|
||||||
Go to https://homebrewery.naturalcrit.com/login, and hit the "*logout*" link.
|
|
||||||
|
|
||||||
### Why am I getting an error when trying to save, and my account is linked to Google?
|
### Why am I getting an error when trying to save, and my account is linked to Google?
|
||||||
|
|
||||||
@@ -105,7 +102,7 @@ The best way to avoid this is to leave space at the end of a column equal to one
|
|||||||
|
|
||||||
### Why do I need to manually create a new page? Why doesn't text flow between pages?
|
### Why do I need to manually create a new page? Why doesn't text flow between pages?
|
||||||
|
|
||||||
A Homebrewery document is at it's core an HTML & CSS document, and currently limited by the specs of those technologies. It is currently not possible to flow content from inside one box ("page") to the inside of another box. It seems likely that someday CSS will add this capability, and if/when that happens, Homebrewery will adopt it as soon as possible.
|
A Homebrewery document is at its core an HTML & CSS document, and currently limited by the specs of those technologies. It is currently not possible to flow content from inside one box ("page") to the inside of another box. It seems likely that someday CSS will add this capability, and if/when that happens, Homebrewery will adopt it as soon as possible.
|
||||||
|
|
||||||
### Where do I get images?
|
### Where do I get images?
|
||||||
The Homebrewery does not provide images for use besides some page elements and example images for snippets. You will need to find your own images for use and be sure you are following the appropriate license requirements.
|
The Homebrewery does not provide images for use besides some page elements and example images for snippets. You will need to find your own images for use and be sure you are following the appropriate license requirements.
|
||||||
@@ -120,26 +117,6 @@ The fonts used were originally created for use with the English language, though
|
|||||||
### Whenever I click on the "Get PDF" button, instead of getting a download, it opens Print Preview in another tab.
|
### Whenever I click on the "Get PDF" button, instead of getting a download, it opens Print Preview in another tab.
|
||||||
Yes, this is by design. In the print preview, select "Save as PDF" as the Destination, and then click "Save". There will be a normal download dialog where you can save your brew as a PDF.
|
Yes, this is by design. In the print preview, select "Save as PDF" as the Destination, and then click "Save". There will be a normal download dialog where you can save your brew as a PDF.
|
||||||
|
|
||||||
### The preview window is suddenly gone, I can only see the editor side of the Homebrewery (or the other way around).
|
|
||||||
|
|
||||||
1. Press `CTRL`+`SHIFT`+`i` (or right-click and select "Inspect") while in the Homebrewery.
|
|
||||||
|
|
||||||
2. Expand...
|
|
||||||
```
|
|
||||||
- `body`
|
|
||||||
- `main`
|
|
||||||
- `div class="homebrew"`
|
|
||||||
- `div class="editPage page"`
|
|
||||||
- `div class="content"`
|
|
||||||
- `div class="splitPane"`
|
|
||||||
```
|
|
||||||
|
|
||||||
There you will find 3 divs: `div class="pane" [...]`, `div class="divider" [...]`, and `div class="pane" [...]`.
|
|
||||||
|
|
||||||
The `class="pane"` looks similar to this: `div class="pane" data-reactid="36" style="flex: 0 0 auto; width: 925px;"`.
|
|
||||||
|
|
||||||
Change whatever stands behind width: to something smaller than your display width.
|
|
||||||
|
|
||||||
### I have white borders on the bottom/sides of the print preview.
|
### I have white borders on the bottom/sides of the print preview.
|
||||||
|
|
||||||
The Homebrewery paper size and your print paper size do not match.
|
The Homebrewery paper size and your print paper size do not match.
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ npm install
|
|||||||
npm audit fix
|
npm audit fix
|
||||||
npm run postinstall
|
npm run postinstall
|
||||||
|
|
||||||
cp freebsd/rc.d/homebrewery /usr/local/etc/rc.d/
|
cp install/freebsd/rc.d/homebrewery /usr/local/etc/rc.d/
|
||||||
chmod +x /usr/local/etc/rc.d/homebrewery
|
chmod +x /usr/local/etc/rc.d/homebrewery
|
||||||
|
|
||||||
sysrc homebrewery_enable=YES
|
sysrc homebrewery_enable=YES
|
||||||
|
|||||||
7033
package-lock.json
generated
7033
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
57
package.json
57
package.json
@@ -1,9 +1,10 @@
|
|||||||
{
|
{
|
||||||
"name": "homebrewery",
|
"name": "homebrewery",
|
||||||
"description": "Create authentic looking D&D homebrews using only markdown",
|
"description": "Create authentic looking D&D homebrews using only markdown",
|
||||||
"version": "3.9.0",
|
"version": "3.10.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.16.x"
|
"npm": "^10.2.x",
|
||||||
|
"node": "^20.8.x"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -78,11 +79,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/core": "^7.22.1",
|
"@babel/core": "^7.23.9",
|
||||||
"@babel/plugin-transform-runtime": "^7.22.4",
|
"@babel/plugin-transform-runtime": "^7.23.9",
|
||||||
"@babel/preset-env": "^7.22.4",
|
"@babel/preset-env": "^7.23.9",
|
||||||
"@babel/preset-react": "^7.22.3",
|
"@babel/preset-react": "^7.23.3",
|
||||||
"@googleapis/drive": "^5.1.0",
|
"@googleapis/drive": "^8.6.0",
|
||||||
"body-parser": "^1.20.2",
|
"body-parser": "^1.20.2",
|
||||||
"classnames": "^2.3.2",
|
"classnames": "^2.3.2",
|
||||||
"codemirror": "^5.65.6",
|
"codemirror": "^5.65.6",
|
||||||
@@ -92,39 +93,39 @@
|
|||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"express-async-handler": "^1.2.0",
|
"express-async-handler": "^1.2.0",
|
||||||
"express-static-gzip": "2.1.7",
|
"express-static-gzip": "2.1.7",
|
||||||
"fs-extra": "11.1.1",
|
"fs-extra": "11.2.0",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"jwt-simple": "^0.5.6",
|
"jwt-simple": "^0.5.6",
|
||||||
"less": "^3.13.1",
|
"less": "^3.13.1",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"marked": "5.0.4",
|
"marked": "11.1.1",
|
||||||
"marked-extended-tables": "^1.0.6",
|
"marked-extended-tables": "^1.0.8",
|
||||||
"marked-gfm-heading-id": "^3.0.3",
|
"marked-gfm-heading-id": "^3.1.2",
|
||||||
|
"marked-smartypants-lite": "^1.0.2",
|
||||||
"markedLegacy": "npm:marked@^0.3.19",
|
"markedLegacy": "npm:marked@^0.3.19",
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.30.1",
|
||||||
"mongoose": "^7.2.2",
|
"mongoose": "^8.1.1",
|
||||||
"nanoid": "3.3.4",
|
"nanoid": "3.3.4",
|
||||||
"nconf": "^0.12.0",
|
"nconf": "^0.12.1",
|
||||||
"npm": "^9.6.7",
|
"react": "^18.2.0",
|
||||||
"react": "^17.0.2",
|
"react-dom": "^18.2.0",
|
||||||
"react-dom": "^17.0.2",
|
|
||||||
"react-frame-component": "^4.1.3",
|
"react-frame-component": "^4.1.3",
|
||||||
"react-router-dom": "6.11.2",
|
"react-router-dom": "6.21.3",
|
||||||
"sanitize-filename": "1.6.3",
|
"sanitize-filename": "1.6.3",
|
||||||
"superagent": "^6.1.0",
|
"superagent": "^8.1.2",
|
||||||
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git"
|
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"eslint": "^8.41.0",
|
"eslint": "^8.56.0",
|
||||||
"eslint-plugin-jest": "^27.2.1",
|
"eslint-plugin-jest": "^27.6.3",
|
||||||
"eslint-plugin-react": "^7.32.2",
|
"eslint-plugin-react": "^7.33.2",
|
||||||
"jest": "^29.5.0",
|
"jest": "^29.7.0",
|
||||||
"jest-expect-message": "^1.1.3",
|
"jest-expect-message": "^1.1.3",
|
||||||
"postcss-less": "^6.0.0",
|
"postcss-less": "^6.0.0",
|
||||||
"stylelint": "^15.6.2",
|
"stylelint": "^15.11.0",
|
||||||
"stylelint-config-recess-order": "^4.0.0",
|
"stylelint-config-recess-order": "^4.4.0",
|
||||||
"stylelint-config-recommended": "^12.0.0",
|
"stylelint-config-recommended": "^13.0.0",
|
||||||
"stylelint-stylistic": "^0.4.2",
|
"stylelint-stylistic": "^0.4.3",
|
||||||
"supertest": "^6.3.3"
|
"supertest": "^6.3.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,6 +99,27 @@ fs.emptyDirSync('./build');
|
|||||||
await fs.copy('./themes/assets', './build/assets');
|
await fs.copy('./themes/assets', './build/assets');
|
||||||
await fs.copy('./client/icons', './build/icons');
|
await fs.copy('./client/icons', './build/icons');
|
||||||
|
|
||||||
|
//v==---------------------------MOVE CM EDITOR THEMES -----------------------------==v//
|
||||||
|
|
||||||
|
const editorThemesBuildDir = './build/homebrew/cm-themes';
|
||||||
|
await fs.copy('./node_modules/codemirror/theme', editorThemesBuildDir);
|
||||||
|
await fs.copy('./themes/codeMirror/customThemes', editorThemesBuildDir);
|
||||||
|
editorThemeFiles = fs.readdirSync(editorThemesBuildDir);
|
||||||
|
|
||||||
|
const editorThemeFile = './themes/codeMirror/editorThemes.json';
|
||||||
|
if(fs.existsSync(editorThemeFile)) fs.rmSync(editorThemeFile);
|
||||||
|
const stream = fs.createWriteStream(editorThemeFile, { flags: 'a' });
|
||||||
|
stream.write('[\n"default"');
|
||||||
|
|
||||||
|
for (themeFile of editorThemeFiles) {
|
||||||
|
stream.write(`,\n"${themeFile.slice(0, -4)}"`);
|
||||||
|
}
|
||||||
|
stream.write('\n]\n');
|
||||||
|
stream.end();
|
||||||
|
|
||||||
|
|
||||||
|
await fs.copy('./themes/codeMirror', './build/homebrew/codeMirror');
|
||||||
|
|
||||||
//v==----------------------------- BUNDLE PACKAGES --------------------------------==v//
|
//v==----------------------------- BUNDLE PACKAGES --------------------------------==v//
|
||||||
|
|
||||||
const bundles = await pack('./client/homebrew/homebrew.jsx', {
|
const bundles = await pack('./client/homebrew/homebrew.jsx', {
|
||||||
@@ -133,14 +154,14 @@ fs.emptyDirSync('./build');
|
|||||||
// build(bundles);
|
// build(bundles);
|
||||||
//
|
//
|
||||||
|
|
||||||
})().catch(console.error);
|
//In development, set up LiveReload (refreshes browser), and Nodemon (restarts server)
|
||||||
|
if(isDev){
|
||||||
//In development set up a watch server and livereload
|
livereload('./build'); // Install the Chrome extension LiveReload to automatically refresh the browser
|
||||||
if(isDev){
|
watchFile('./server.js', { // Restart server when change detected to this file or any nested directory from here
|
||||||
livereload('./build');
|
ignore : ['./build', './client', './themes'], // Ignore folders that are not running server code / avoids unneeded restarts
|
||||||
watchFile('./server.js', { // Rebuild when change detected to this file or any nested directory from here
|
ext : 'js json' // Extensions to watch (only .js/.json by default)
|
||||||
ignore : ['./build'], // Ignore ./build or it will rebuild again
|
//watch : ['./server', './themes'], // Watch additional folders if needed
|
||||||
ext : 'less', // Other extensions to watch (only .js/.json/.jsx by default)
|
|
||||||
//watch: ['./client', './server', './themes'], // Watch additional folders if you want
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
})().catch(console.error);
|
||||||
@@ -26,7 +26,6 @@
|
|||||||
"codemirror/addon/edit/trailingspace.js",
|
"codemirror/addon/edit/trailingspace.js",
|
||||||
"codemirror/addon/selection/active-line.js",
|
"codemirror/addon/selection/active-line.js",
|
||||||
"moment",
|
"moment",
|
||||||
"superagent",
|
"superagent"
|
||||||
"marked"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,77 +26,116 @@ const mw = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const junkBrewPipeline = [
|
||||||
/* Search for brews that are older than 3 days and that are shorter than a tweet */
|
{ $match : {
|
||||||
const junkBrewQuery = HomebrewModel.find({
|
updatedAt : { $lt: Moment().subtract(30, 'days').toDate() },
|
||||||
'$where' : 'this.text.length < 140',
|
lastViewed : { $lt: Moment().subtract(30, 'days').toDate() }
|
||||||
createdAt : {
|
}},
|
||||||
$lt : Moment().subtract(30, 'days').toDate()
|
{ $project: { textBinSize: { $binarySize: '$textBin' } } },
|
||||||
}
|
{ $match: { textBinSize: { $lt: 140 } } },
|
||||||
}).limit(100).maxTime(60000);
|
{ $limit: 100 }
|
||||||
|
];
|
||||||
|
|
||||||
/* Search for brews that aren't compressed (missing the compressed text field) */
|
/* Search for brews that aren't compressed (missing the compressed text field) */
|
||||||
const uncompressedBrewQuery = HomebrewModel.find({
|
const uncompressedBrewQuery = HomebrewModel.find({
|
||||||
'text' : { '$exists': true }
|
'text' : { '$exists': true }
|
||||||
}).lean().limit(10000).select('_id');
|
}).lean().limit(10000).select('_id');
|
||||||
|
|
||||||
|
// Search for up to 100 brews that have not been viewed or updated in 30 days and are shorter than 140 bytes
|
||||||
router.get('/admin/cleanup', mw.adminOnly, (req, res)=>{
|
router.get('/admin/cleanup', mw.adminOnly, (req, res)=>{
|
||||||
junkBrewQuery.exec((err, objs)=>{
|
HomebrewModel.aggregate(junkBrewPipeline).option({ maxTimeMS: 60000 })
|
||||||
if(err) return res.status(500).send(err);
|
.then((objs)=>res.json({ count: objs.length }))
|
||||||
return res.json({ count: objs.length });
|
.catch((error)=>{
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ error: 'Internal Server Error' });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
/* Removes all empty brews that are older than 3 days and that are shorter than a tweet */
|
|
||||||
|
// Delete up to 100 brews that have not been viewed or updated in 30 days and are shorter than 140 bytes
|
||||||
router.post('/admin/cleanup', mw.adminOnly, (req, res)=>{
|
router.post('/admin/cleanup', mw.adminOnly, (req, res)=>{
|
||||||
junkBrewQuery.remove().exec((err, objs)=>{
|
HomebrewModel.aggregate(junkBrewPipeline).option({ maxTimeMS: 60000 })
|
||||||
if(err) return res.status(500).send(err);
|
.then((docs)=>{
|
||||||
return res.json({ count: objs.length });
|
const ids = docs.map((doc)=>doc._id);
|
||||||
|
return HomebrewModel.deleteMany({ _id: { $in: ids } });
|
||||||
|
}).then((result)=>{
|
||||||
|
res.json({ count: result.deletedCount });
|
||||||
|
}).catch((error)=>{
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ error: 'Internal Server Error' });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
/* Searches for matching edit or share id, also attempts to partial match */
|
/* Searches for matching edit or share id, also attempts to partial match */
|
||||||
router.get('/admin/lookup/:id', mw.adminOnly, (req, res, next)=>{
|
router.get('/admin/lookup/:id', mw.adminOnly, async (req, res, next)=>{
|
||||||
HomebrewModel.findOne({ $or : [
|
HomebrewModel.findOne({
|
||||||
{ editId: { '$regex': req.params.id, '$options': 'i' } },
|
$or : [
|
||||||
{ shareId: { '$regex': req.params.id, '$options': 'i' } },
|
{ editId: { $regex: req.params.id, $options: 'i' } },
|
||||||
] }).exec((err, brew)=>{
|
{ shareId: { $regex: req.params.id, $options: 'i' } },
|
||||||
|
]
|
||||||
|
}).exec()
|
||||||
|
.then((brew)=>{
|
||||||
|
if(!brew) // No document found
|
||||||
|
return res.status(404).json({ error: 'Document not found' });
|
||||||
|
else
|
||||||
return res.json(brew);
|
return res.json(brew);
|
||||||
|
})
|
||||||
|
.catch((err)=>{
|
||||||
|
console.error(err);
|
||||||
|
return res.status(500).json({ error: 'Internal Server Error' });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
/* Find 50 brews that aren't compressed yet */
|
/* Find 50 brews that aren't compressed yet */
|
||||||
router.get('/admin/finduncompressed', mw.adminOnly, (req, res)=>{
|
router.get('/admin/finduncompressed', mw.adminOnly, (req, res)=>{
|
||||||
uncompressedBrewQuery.exec((err, objs)=>{
|
const query = uncompressedBrewQuery.clone();
|
||||||
if(err) return res.status(500).send(err);
|
|
||||||
objs = objs.map((obj)=>{return obj._id;});
|
query.exec()
|
||||||
return res.json({ count: objs.length, ids: objs });
|
.then((objs)=>{
|
||||||
|
const ids = objs.map((obj)=>obj._id);
|
||||||
|
res.json({ count: ids.length, ids });
|
||||||
|
})
|
||||||
|
.catch((err)=>{
|
||||||
|
console.error(err);
|
||||||
|
res.status(500).send(err.message || 'Internal Server Error');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
/* Compresses the "text" field of a brew to binary */
|
/* Compresses the "text" field of a brew to binary */
|
||||||
router.put('/admin/compress/:id', (req, res)=>{
|
router.put('/admin/compress/:id', (req, res)=>{
|
||||||
HomebrewModel.get({ _id: req.params.id })
|
HomebrewModel.findOne({ _id: req.params.id })
|
||||||
.then((brew)=>{
|
.then((brew)=>{
|
||||||
brew.textBin = zlib.deflateRawSync(brew.text); // Compress brew text to binary before saving
|
if(!brew)
|
||||||
brew.text = undefined; // Delete the non-binary text field since it's not needed anymore
|
return res.status(404).send('Brew not found');
|
||||||
|
|
||||||
brew.save((err, obj)=>{
|
if(brew.text) {
|
||||||
if(err) throw err;
|
brew.textBin = brew.textBin || zlib.deflateRawSync(brew.text); //Don't overwrite textBin if exists
|
||||||
return res.status(200).send(obj);
|
brew.text = undefined;
|
||||||
});
|
}
|
||||||
|
|
||||||
|
return brew.save();
|
||||||
})
|
})
|
||||||
|
.then((obj)=>res.status(200).send(obj))
|
||||||
.catch((err)=>{
|
.catch((err)=>{
|
||||||
console.log(err);
|
console.error(err);
|
||||||
return res.status(500).send('Error while saving');
|
res.status(500).send('Error while saving');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get('/admin/stats', mw.adminOnly, (req, res)=>{
|
|
||||||
HomebrewModel.count({}, (err, count)=>{
|
router.get('/admin/stats', mw.adminOnly, async (req, res)=>{
|
||||||
|
try {
|
||||||
|
const totalBrewsCount = await HomebrewModel.countDocuments({});
|
||||||
|
const publishedBrewsCount = await HomebrewModel.countDocuments({ published: true });
|
||||||
|
|
||||||
return res.json({
|
return res.json({
|
||||||
totalBrews : count
|
totalBrews : totalBrewsCount,
|
||||||
});
|
totalPublishedBrews : publishedBrewsCount
|
||||||
});
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
return res.status(500).json({ error: 'Internal Server Error' });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get('/admin', mw.adminOnly, (req, res)=>{
|
router.get('/admin', mw.adminOnly, (req, res)=>{
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
/*eslint max-lines: ["warn", {"max": 400, "skipBlankLines": true, "skipComments": true}]*/
|
/*eslint max-lines: ["warn", {"max": 500, "skipBlankLines": true, "skipComments": true}]*/
|
||||||
// Set working directory to project root
|
// Set working directory to project root
|
||||||
process.chdir(`${__dirname}/..`);
|
process.chdir(`${__dirname}/..`);
|
||||||
|
|
||||||
@@ -67,6 +67,7 @@ app.use((req, res, next)=>{
|
|||||||
|
|
||||||
app.use(homebrewApi);
|
app.use(homebrewApi);
|
||||||
app.use(require('./admin.api.js'));
|
app.use(require('./admin.api.js'));
|
||||||
|
app.use(require('./archive.api.js'));
|
||||||
|
|
||||||
const HomebrewModel = require('./homebrew.model.js').model;
|
const HomebrewModel = require('./homebrew.model.js').model;
|
||||||
const welcomeText = require('fs').readFileSync('client/homebrew/pages/homePage/welcome_msg.md', 'utf8');
|
const welcomeText = require('fs').readFileSync('client/homebrew/pages/homePage/welcome_msg.md', 'utf8');
|
||||||
@@ -257,6 +258,7 @@ app.get('/user/:username', async (req, res, next)=>{
|
|||||||
brew.pageCount = googleBrews[match].pageCount;
|
brew.pageCount = googleBrews[match].pageCount;
|
||||||
brew.renderer = googleBrews[match].renderer;
|
brew.renderer = googleBrews[match].renderer;
|
||||||
brew.version = googleBrews[match].version;
|
brew.version = googleBrews[match].version;
|
||||||
|
brew.webViewLink = googleBrews[match].webViewLink;
|
||||||
googleBrews.splice(match, 1);
|
googleBrews.splice(match, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -267,6 +269,9 @@ app.get('/user/:username', async (req, res, next)=>{
|
|||||||
}
|
}
|
||||||
|
|
||||||
req.brews = _.map(brews, (brew)=>{
|
req.brews = _.map(brews, (brew)=>{
|
||||||
|
// Clean up brew data
|
||||||
|
brew.title = brew.title?.trim();
|
||||||
|
brew.description = brew.description?.trim();
|
||||||
return sanitizeBrew(brew, ownAccount ? 'edit' : 'share');
|
return sanitizeBrew(brew, ownAccount ? 'edit' : 'share');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -300,7 +305,8 @@ app.get('/new/:id', asyncHandler(getBrew('share')), (req, res, next)=>{
|
|||||||
text : req.brew.text,
|
text : req.brew.text,
|
||||||
style : req.brew.style,
|
style : req.brew.style,
|
||||||
renderer : req.brew.renderer,
|
renderer : req.brew.renderer,
|
||||||
theme : req.brew.theme
|
theme : req.brew.theme,
|
||||||
|
tags : req.brew.tags
|
||||||
};
|
};
|
||||||
req.brew = _.defaults(brew, DEFAULT_BREW);
|
req.brew = _.defaults(brew, DEFAULT_BREW);
|
||||||
|
|
||||||
@@ -323,14 +329,17 @@ app.get('/share/:id', asyncHandler(getBrew('share')), asyncHandler(async (req, r
|
|||||||
type : 'article'
|
type : 'article'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// increase visitor view count, do not include visits by author(s)
|
||||||
|
if(!brew.authors.includes(req.account?.username)){
|
||||||
if(req.params.id.length > 12 && !brew._id) {
|
if(req.params.id.length > 12 && !brew._id) {
|
||||||
const googleId = req.params.id.slice(0, -12);
|
const googleId = brew.googleId;
|
||||||
const shareId = req.params.id.slice(-12);
|
const shareId = brew.shareId;
|
||||||
await GoogleActions.increaseView(googleId, shareId, 'share', brew)
|
await GoogleActions.increaseView(googleId, shareId, 'share', brew)
|
||||||
.catch((err)=>{next(err);});
|
.catch((err)=>{next(err);});
|
||||||
} else {
|
} else {
|
||||||
await HomebrewModel.increaseView({ shareId: brew.shareId });
|
await HomebrewModel.increaseView({ shareId: brew.shareId });
|
||||||
}
|
}
|
||||||
|
};
|
||||||
sanitizeBrew(req.brew, 'share');
|
sanitizeBrew(req.brew, 'share');
|
||||||
splitTextStyleAndMetadata(req.brew);
|
splitTextStyleAndMetadata(req.brew);
|
||||||
return next();
|
return next();
|
||||||
@@ -397,7 +406,6 @@ app.get('/account', asyncHandler(async (req, res, next)=>{
|
|||||||
return next();
|
return next();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
||||||
const nodeEnv = config.get('node_env');
|
const nodeEnv = config.get('node_env');
|
||||||
const isLocalEnvironment = config.get('local_environments').includes(nodeEnv);
|
const isLocalEnvironment = config.get('local_environments').includes(nodeEnv);
|
||||||
// Local only
|
// Local only
|
||||||
@@ -414,8 +422,7 @@ if(isLocalEnvironment){
|
|||||||
|
|
||||||
//Render the page
|
//Render the page
|
||||||
const templateFn = require('./../client/template.js');
|
const templateFn = require('./../client/template.js');
|
||||||
app.use(asyncHandler(async (req, res, next)=>{
|
const renderPage = async (req, res)=>{
|
||||||
|
|
||||||
// Create configuration object
|
// Create configuration object
|
||||||
const configuration = {
|
const configuration = {
|
||||||
local : isLocalEnvironment,
|
local : isLocalEnvironment,
|
||||||
@@ -424,7 +431,7 @@ app.use(asyncHandler(async (req, res, next)=>{
|
|||||||
};
|
};
|
||||||
const props = {
|
const props = {
|
||||||
version : require('./../package.json').version,
|
version : require('./../package.json').version,
|
||||||
url : req.originalUrl,
|
url : req.customUrl || req.originalUrl,
|
||||||
brew : req.brew,
|
brew : req.brew,
|
||||||
brews : req.brews,
|
brews : req.brews,
|
||||||
googleBrews : req.googleBrews,
|
googleBrews : req.googleBrews,
|
||||||
@@ -438,15 +445,20 @@ app.use(asyncHandler(async (req, res, next)=>{
|
|||||||
const page = await templateFn('homebrew', title, props)
|
const page = await templateFn('homebrew', title, props)
|
||||||
.catch((err)=>{
|
.catch((err)=>{
|
||||||
console.log(err);
|
console.log(err);
|
||||||
return res.sendStatus(500);
|
|
||||||
});
|
});
|
||||||
|
return page;
|
||||||
|
};
|
||||||
|
|
||||||
|
//Send rendered page
|
||||||
|
app.use(asyncHandler(async (req, res, next)=>{
|
||||||
|
const page = await renderPage(req, res);
|
||||||
if(!page) return;
|
if(!page) return;
|
||||||
res.send(page);
|
res.send(page);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
//v=====----- Error-Handling Middleware -----=====v//
|
//v=====----- Error-Handling Middleware -----=====v//
|
||||||
//Format Errors so all fields will be sent
|
//Format Errors as plain objects so all fields will appear in the string sent
|
||||||
const replaceErrors = (key, value)=>{
|
const formatErrors = (key, value)=>{
|
||||||
if(value instanceof Error) {
|
if(value instanceof Error) {
|
||||||
const error = {};
|
const error = {};
|
||||||
Object.getOwnPropertyNames(value).forEach(function (key) {
|
Object.getOwnPropertyNames(value).forEach(function (key) {
|
||||||
@@ -458,15 +470,48 @@ const replaceErrors = (key, value)=>{
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getPureError = (error)=>{
|
const getPureError = (error)=>{
|
||||||
return JSON.parse(JSON.stringify(error, replaceErrors));
|
return JSON.parse(JSON.stringify(error, formatErrors));
|
||||||
};
|
};
|
||||||
|
|
||||||
app.use((err, req, res, next)=>{
|
app.use(async (err, req, res, next)=>{
|
||||||
const status = err.status || 500;
|
err.originalUrl = req.originalUrl;
|
||||||
console.error(err);
|
console.error(err);
|
||||||
res.status(status).send(getPureError(err));
|
|
||||||
|
if(err.originalUrl?.startsWith('/api/')) {
|
||||||
|
// console.log('API error');
|
||||||
|
res.status(err.status || err.response?.status || 500).send(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(err.originalUrl?.startsWith('/archive/')) {
|
||||||
|
// console.log('archive error');
|
||||||
|
res.status(err.status || err.response?.status || 500).send(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// console.log('non-API error');
|
||||||
|
const status = err.status || err.code || 500;
|
||||||
|
|
||||||
|
req.ogMeta = { ...defaultMetaTags,
|
||||||
|
title : 'Error Page',
|
||||||
|
description : 'Something went wrong!'
|
||||||
|
};
|
||||||
|
req.brew = {
|
||||||
|
...err,
|
||||||
|
title : 'Error - Something went wrong!',
|
||||||
|
text : err.errors?.map((error)=>{return error.message;}).join('\n\n') || err.message || 'Unknown error!',
|
||||||
|
status : status,
|
||||||
|
HBErrorCode : err.HBErrorCode ?? '00',
|
||||||
|
pureError : getPureError(err)
|
||||||
|
};
|
||||||
|
req.customUrl= '/error';
|
||||||
|
|
||||||
|
const page = await renderPage(req, res);
|
||||||
|
if(!page) return;
|
||||||
|
res.send(page);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
app.use((req, res)=>{
|
app.use((req, res)=>{
|
||||||
if(!res.headersSent) {
|
if(!res.headersSent) {
|
||||||
console.error('Headers have not been sent, responding with a server error.', req.url);
|
console.error('Headers have not been sent, responding with a server error.', req.url);
|
||||||
|
|||||||
53
server/archive.api.js
Normal file
53
server/archive.api.js
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
const HomebrewModel = require('./homebrew.model.js').model;
|
||||||
|
const router = require('express').Router();
|
||||||
|
const asyncHandler = require('express-async-handler');
|
||||||
|
|
||||||
|
const archive = {
|
||||||
|
archiveApi : router,
|
||||||
|
/* Searches for matching title, also attempts to partial match */
|
||||||
|
findBrews : async (req, res, next)=>{
|
||||||
|
try {
|
||||||
|
const title = req.query.title || '';
|
||||||
|
const page = parseInt(req.query.page) || 1;
|
||||||
|
console.log('try:', page);
|
||||||
|
const pageSize = 10; // Set a default page size
|
||||||
|
const skip = (page - 1) * pageSize;
|
||||||
|
|
||||||
|
const titleQuery = {
|
||||||
|
title : { $regex: decodeURIComponent(title), $options: 'i' },
|
||||||
|
published : true
|
||||||
|
};
|
||||||
|
|
||||||
|
const projection = {
|
||||||
|
editId : 0,
|
||||||
|
googleId : 0,
|
||||||
|
text : 0,
|
||||||
|
textBin : 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
const brews = await HomebrewModel.find(titleQuery, projection)
|
||||||
|
.skip(skip)
|
||||||
|
.limit(pageSize)
|
||||||
|
.maxTimeMS(5000)
|
||||||
|
.exec();
|
||||||
|
|
||||||
|
if(!brews || brews.length === 0) {
|
||||||
|
// No published documents found with the given title
|
||||||
|
return res.status(404).json({ error: 'Published documents not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalDocuments = await HomebrewModel.countDocuments(title);
|
||||||
|
|
||||||
|
const totalPages = Math.ceil(totalDocuments / pageSize);
|
||||||
|
|
||||||
|
return res.json({ brews, page, totalPages });
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
return res.status(500).json({ error: 'Internal Server Error' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
router.get('/api/archive', asyncHandler(archive.findBrews));
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
@@ -100,13 +100,13 @@ const GoogleActions = {
|
|||||||
const drive = googleDrive.drive({ version: 'v3', auth });
|
const drive = googleDrive.drive({ version: 'v3', auth });
|
||||||
|
|
||||||
const fileList = [];
|
const fileList = [];
|
||||||
let NextPageToken = "";
|
let NextPageToken = '';
|
||||||
|
|
||||||
do {
|
do {
|
||||||
const obj = await drive.files.list({
|
const obj = await drive.files.list({
|
||||||
pageSize : 1000,
|
pageSize : 1000,
|
||||||
pageToken : NextPageToken || "",
|
pageToken : NextPageToken || '',
|
||||||
fields : 'nextPageToken, files(id, name, description, createdTime, modifiedTime, properties)',
|
fields : 'nextPageToken, files(id, name, description, createdTime, modifiedTime, properties, webViewLink)',
|
||||||
q : 'mimeType != \'application/vnd.google-apps.folder\' and trashed = false'
|
q : 'mimeType != \'application/vnd.google-apps.folder\' and trashed = false'
|
||||||
})
|
})
|
||||||
.catch((err)=>{
|
.catch((err)=>{
|
||||||
@@ -139,7 +139,8 @@ const GoogleActions = {
|
|||||||
published : file.properties.published ? file.properties.published == 'true' : false,
|
published : file.properties.published ? file.properties.published == 'true' : false,
|
||||||
systems : [],
|
systems : [],
|
||||||
lang : file.properties.lang,
|
lang : file.properties.lang,
|
||||||
thumbnail : file.properties.thumbnail
|
thumbnail : file.properties.thumbnail,
|
||||||
|
webViewLink : file.webViewLink
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
return brews;
|
return brews;
|
||||||
@@ -243,9 +244,9 @@ const GoogleActions = {
|
|||||||
|
|
||||||
if(obj) {
|
if(obj) {
|
||||||
if(accessType == 'edit' && obj.data.properties.editId != accessId){
|
if(accessType == 'edit' && obj.data.properties.editId != accessId){
|
||||||
throw ('Edit ID does not match');
|
throw ({ message: 'Edit ID does not match' });
|
||||||
} else if(accessType == 'share' && obj.data.properties.shareId != accessId){
|
} else if(accessType == 'share' && obj.data.properties.shareId != accessId){
|
||||||
throw ('Share ID does not match');
|
throw ({ message: 'Share ID does not match' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const file = await drive.files.get({
|
const file = await drive.files.get({
|
||||||
|
|||||||
@@ -27,8 +27,13 @@ const api = {
|
|||||||
|
|
||||||
// If the id is longer than 12, then it's a google id + the edit id. This splits the longer id up.
|
// If the id is longer than 12, then it's a google id + the edit id. This splits the longer id up.
|
||||||
if(id.length > 12) {
|
if(id.length > 12) {
|
||||||
googleId = id.slice(0, -12);
|
if(id.length >= (33 + 12)) { // googleId is minimum 33 chars (may increase)
|
||||||
id = id.slice(-12);
|
googleId = id.slice(0, -12); // current editId is 12 chars
|
||||||
|
} else { // old editIds used to be 10 chars;
|
||||||
|
googleId = id.slice(0, -10); // if total string is too short, must be old brew
|
||||||
|
console.log('Old brew, using 10-char Id');
|
||||||
|
}
|
||||||
|
id = id.slice(googleId.length);
|
||||||
}
|
}
|
||||||
return { id, googleId };
|
return { id, googleId };
|
||||||
},
|
},
|
||||||
@@ -57,7 +62,14 @@ const api = {
|
|||||||
googleError = err;
|
googleError = err;
|
||||||
});
|
});
|
||||||
// Throw any error caught while attempting to retrieve Google brew.
|
// Throw any error caught while attempting to retrieve Google brew.
|
||||||
if(googleError) throw googleError;
|
if(googleError) {
|
||||||
|
const reason = googleError.errors?.[0].reason;
|
||||||
|
if(reason == 'notFound') {
|
||||||
|
throw { ...googleError, HBErrorCode: '02', authors: stub?.authors, account: req.account?.username };
|
||||||
|
} else {
|
||||||
|
throw { ...googleError, HBErrorCode: '01' };
|
||||||
|
}
|
||||||
|
}
|
||||||
// Combine the Homebrewery stub with the google brew, or if the stub doesn't exist just use the google brew
|
// Combine the Homebrewery stub with the google brew, or if the stub doesn't exist just use the google brew
|
||||||
stub = stub ? _.assign({ ...api.excludeStubProps(stub), stubbed: true }, api.excludeGoogleProps(googleBrew)) : googleBrew;
|
stub = stub ? _.assign({ ...api.excludeStubProps(stub), stubbed: true }, api.excludeGoogleProps(googleBrew)) : googleBrew;
|
||||||
}
|
}
|
||||||
@@ -65,14 +77,16 @@ const api = {
|
|||||||
const isAuthor = stub?.authors?.includes(req.account?.username);
|
const isAuthor = stub?.authors?.includes(req.account?.username);
|
||||||
const isInvited = stub?.invitedAuthors?.includes(req.account?.username);
|
const isInvited = stub?.invitedAuthors?.includes(req.account?.username);
|
||||||
if(accessType === 'edit' && (authorsExist && !(isAuthor || isInvited))) {
|
if(accessType === 'edit' && (authorsExist && !(isAuthor || isInvited))) {
|
||||||
throw `The current logged in user does not have editor access to this brew.
|
const accessError = { name: 'Access Error', status: 401 };
|
||||||
|
if(req.account){
|
||||||
If you believe you should have access to this brew, ask the file owner to invite you as an author by opening the brew, viewing the Properties tab, and adding your username to the "invited authors" list. You can then try to access this document again.`;
|
throw { ...accessError, message: 'User is not an Author', HBErrorCode: '03', authors: stub.authors, brewTitle: stub.title, shareId: stub.shareId };
|
||||||
|
}
|
||||||
|
throw { ...accessError, message: 'User is not logged in', HBErrorCode: '04', authors: stub.authors, brewTitle: stub.title };
|
||||||
}
|
}
|
||||||
|
|
||||||
// If after all of that we still don't have a brew, throw an exception
|
// If after all of that we still don't have a brew, throw an exception
|
||||||
if(!stub && !stubOnly) {
|
if(!stub && !stubOnly) {
|
||||||
throw 'Brew not found in Homebrewery database or Google Drive';
|
throw { name: 'BrewLoad Error', message: 'Brew not found', status: 404, HBErrorCode: '05', accessType: accessType, brewId: id };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up brew: fill in missing fields with defaults / fix old invalid values
|
// Clean up brew: fill in missing fields with defaults / fix old invalid values
|
||||||
@@ -139,6 +153,9 @@ If you believe you should have access to this brew, ask the file owner to invite
|
|||||||
brew.text = api.mergeBrewText(brew);
|
brew.text = api.mergeBrewText(brew);
|
||||||
|
|
||||||
_.defaults(brew, DEFAULT_BREW);
|
_.defaults(brew, DEFAULT_BREW);
|
||||||
|
|
||||||
|
brew.title = brew.title.trim();
|
||||||
|
brew.description = brew.description.trim();
|
||||||
},
|
},
|
||||||
newGoogleBrew : async (account, brew, res)=>{
|
newGoogleBrew : async (account, brew, res)=>{
|
||||||
const oAuth2Client = GoogleActions.authCheck(account, res);
|
const oAuth2Client = GoogleActions.authCheck(account, res);
|
||||||
@@ -181,7 +198,7 @@ If you believe you should have access to this brew, ask the file owner to invite
|
|||||||
saved = await newHomebrew.save()
|
saved = await newHomebrew.save()
|
||||||
.catch((err)=>{
|
.catch((err)=>{
|
||||||
console.error(err, err.toString(), err.stack);
|
console.error(err, err.toString(), err.stack);
|
||||||
throw `Error while creating new brew, ${err.toString()}`;
|
throw { name: 'BrewSave Error', message: `Error while creating new brew, ${err.toString()}`, status: 500, HBErrorCode: '06' };
|
||||||
});
|
});
|
||||||
if(!saved) return;
|
if(!saved) return;
|
||||||
saved = saved.toObject();
|
saved = saved.toObject();
|
||||||
@@ -203,6 +220,8 @@ If you believe you should have access to this brew, ask the file owner to invite
|
|||||||
const { saveToGoogle, removeFromGoogle } = req.query;
|
const { saveToGoogle, removeFromGoogle } = req.query;
|
||||||
let afterSave = async ()=>true;
|
let afterSave = async ()=>true;
|
||||||
|
|
||||||
|
brew.title = brew.title.trim();
|
||||||
|
brew.description = brew.description.trim() || '';
|
||||||
brew.text = api.mergeBrewText(brew);
|
brew.text = api.mergeBrewText(brew);
|
||||||
|
|
||||||
if(brew.googleId && removeFromGoogle) {
|
if(brew.googleId && removeFromGoogle) {
|
||||||
@@ -283,11 +302,14 @@ If you believe you should have access to this brew, ask the file owner to invite
|
|||||||
try {
|
try {
|
||||||
await api.getBrew('edit')(req, res, ()=>{});
|
await api.getBrew('edit')(req, res, ()=>{});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
// Only if the error code is HBErrorCode '02', that is, Google returned "404 - Not Found"
|
||||||
|
if(err.HBErrorCode == '02') {
|
||||||
const { id, googleId } = api.getId(req);
|
const { id, googleId } = api.getId(req);
|
||||||
console.warn(`No google brew found for id ${googleId}, the stub with id ${id} will be deleted.`);
|
console.warn(`No google brew found for id ${googleId}, the stub with id ${id} will be deleted.`);
|
||||||
await HomebrewModel.deleteOne({ editId: id });
|
await HomebrewModel.deleteOne({ editId: id });
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let brew = req.brew;
|
let brew = req.brew;
|
||||||
const { googleId, editId } = brew;
|
const { googleId, editId } = brew;
|
||||||
@@ -308,7 +330,7 @@ If you believe you should have access to this brew, ask the file owner to invite
|
|||||||
await HomebrewModel.deleteOne({ _id: brew._id })
|
await HomebrewModel.deleteOne({ _id: brew._id })
|
||||||
.catch((err)=>{
|
.catch((err)=>{
|
||||||
console.error(err);
|
console.error(err);
|
||||||
throw { status: 500, message: 'Error while removing' };
|
throw { name: 'BrewDelete Error', message: 'Error while removing', status: 500, HBErrorCode: '07', brewId: brew._id };
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
if(shouldDeleteGoogleBrew) {
|
if(shouldDeleteGoogleBrew) {
|
||||||
@@ -320,7 +342,7 @@ If you believe you should have access to this brew, ask the file owner to invite
|
|||||||
brew.markModified('authors'); //Mongo will not properly update arrays without markModified()
|
brew.markModified('authors'); //Mongo will not properly update arrays without markModified()
|
||||||
await brew.save()
|
await brew.save()
|
||||||
.catch((err)=>{
|
.catch((err)=>{
|
||||||
throw { status: 500, message: err };
|
throw { name: 'BrewAuthorDelete Error', message: err, status: 500, HBErrorCode: '08', brewId: brew._id };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -111,21 +111,32 @@ describe('Tests for api', ()=>{
|
|||||||
expect(googleId).toEqual('12345');
|
expect(googleId).toEqual('12345');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return id and google id from params', ()=>{
|
it('should return 12-char id and google id from params', ()=>{
|
||||||
const { id, googleId } = api.getId({
|
const { id, googleId } = api.getId({
|
||||||
params : {
|
params : {
|
||||||
id : '123456789012abcdefghijkl'
|
id : '123456789012345678901234567890123abcdefghijkl'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
expect(googleId).toEqual('123456789012345678901234567890123');
|
||||||
expect(id).toEqual('abcdefghijkl');
|
expect(id).toEqual('abcdefghijkl');
|
||||||
expect(googleId).toEqual('123456789012');
|
});
|
||||||
|
|
||||||
|
it('should return 10-char id and google id from params', ()=>{
|
||||||
|
const { id, googleId } = api.getId({
|
||||||
|
params : {
|
||||||
|
id : '123456789012345678901234567890123abcdefghij'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(googleId).toEqual('123456789012345678901234567890123');
|
||||||
|
expect(id).toEqual('abcdefghij');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getBrew', ()=>{
|
describe('getBrew', ()=>{
|
||||||
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
|
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
|
||||||
const notFoundError = 'Brew not found in Homebrewery database or Google Drive';
|
const notFoundError = { HBErrorCode: '05', message: 'Brew not found', name: 'BrewLoad Error', status: 404, accessType: 'share', brewId: '1' };
|
||||||
|
|
||||||
it('returns middleware', ()=>{
|
it('returns middleware', ()=>{
|
||||||
const getFn = api.getBrew('share');
|
const getFn = api.getBrew('share');
|
||||||
@@ -183,7 +194,7 @@ describe('Tests for api', ()=>{
|
|||||||
expect(next).toHaveBeenCalled();
|
expect(next).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws if invalid author', async ()=>{
|
it('throws if not logged in as author', async ()=>{
|
||||||
api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
|
api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
|
||||||
model.get = jest.fn(()=>toBrewPromise({ title: 'test brew', authors: ['a'] }));
|
model.get = jest.fn(()=>toBrewPromise({ title: 'test brew', authors: ['a'] }));
|
||||||
|
|
||||||
@@ -197,9 +208,24 @@ describe('Tests for api', ()=>{
|
|||||||
err = e;
|
err = e;
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(err).toEqual(`The current logged in user does not have editor access to this brew.
|
expect(err).toEqual({ HBErrorCode: '04', message: 'User is not logged in', name: 'Access Error', status: 401, brewTitle: 'test brew', authors: ['a'] });
|
||||||
|
});
|
||||||
|
|
||||||
If you believe you should have access to this brew, ask the file owner to invite you as an author by opening the brew, viewing the Properties tab, and adding your username to the "invited authors" list. You can then try to access this document again.`);
|
it('throws if logged in as invalid author', async ()=>{
|
||||||
|
api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
|
||||||
|
model.get = jest.fn(()=>toBrewPromise({ title: 'test brew', authors: ['a'] }));
|
||||||
|
|
||||||
|
const fn = api.getBrew('edit', true);
|
||||||
|
const req = { brew: {}, account: { username: 'b' } };
|
||||||
|
|
||||||
|
let err;
|
||||||
|
try {
|
||||||
|
await fn(req, null, null);
|
||||||
|
} catch (e) {
|
||||||
|
err = e;
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(err).toEqual({ HBErrorCode: '03', message: 'User is not an Author', name: 'Access Error', status: 401, brewTitle: 'test brew', authors: ['a'] });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not throw if no authors', async ()=>{
|
it('does not throw if no authors', async ()=>{
|
||||||
@@ -545,7 +571,7 @@ brew`);
|
|||||||
|
|
||||||
describe('deleteBrew', ()=>{
|
describe('deleteBrew', ()=>{
|
||||||
it('should handle case where fetching the brew returns an error', async ()=>{
|
it('should handle case where fetching the brew returns an error', async ()=>{
|
||||||
api.getBrew = jest.fn(()=>async ()=>{ throw 'err'; });
|
api.getBrew = jest.fn(()=>async ()=>{ throw { message: 'err', HBErrorCode: '02' }; });
|
||||||
api.getId = jest.fn(()=>({ id: '1', googleId: '2' }));
|
api.getId = jest.fn(()=>({ id: '1', googleId: '2' }));
|
||||||
model.deleteOne = jest.fn(async ()=>{});
|
model.deleteOne = jest.fn(async ()=>{});
|
||||||
const next = jest.fn(()=>{});
|
const next = jest.fn(()=>{});
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ const cx = require('classnames');
|
|||||||
const closeTag = require('./close-tag');
|
const closeTag = require('./close-tag');
|
||||||
|
|
||||||
let CodeMirror;
|
let CodeMirror;
|
||||||
if(typeof navigator !== 'undefined'){
|
if(typeof window !== 'undefined'){
|
||||||
CodeMirror = require('codemirror');
|
CodeMirror = require('codemirror');
|
||||||
|
|
||||||
//Language Modes
|
//Language Modes
|
||||||
@@ -49,7 +49,8 @@ const CodeEditor = createClass({
|
|||||||
value : '',
|
value : '',
|
||||||
wrap : true,
|
wrap : true,
|
||||||
onChange : ()=>{},
|
onChange : ()=>{},
|
||||||
enableFolding : true
|
enableFolding : true,
|
||||||
|
editorTheme : 'default'
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -91,6 +92,10 @@ const CodeEditor = createClass({
|
|||||||
} else {
|
} else {
|
||||||
this.codeMirror.setOption('foldOptions', false);
|
this.codeMirror.setOption('foldOptions', false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(prevProps.editorTheme !== this.props.editorTheme){
|
||||||
|
this.codeMirror.setOption('theme', this.props.editorTheme);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
buildEditor : function() {
|
buildEditor : function() {
|
||||||
@@ -107,6 +112,10 @@ const CodeEditor = createClass({
|
|||||||
'Shift-Tab' : this.dedent,
|
'Shift-Tab' : this.dedent,
|
||||||
'Ctrl-B' : this.makeBold,
|
'Ctrl-B' : this.makeBold,
|
||||||
'Cmd-B' : this.makeBold,
|
'Cmd-B' : this.makeBold,
|
||||||
|
'Shift-Ctrl-=' : this.makeSuper,
|
||||||
|
'Shift-Cmd-=' : this.makeSuper,
|
||||||
|
'Ctrl-=' : this.makeSub,
|
||||||
|
'Cmd-=' : this.makeSub,
|
||||||
'Ctrl-I' : this.makeItalic,
|
'Ctrl-I' : this.makeItalic,
|
||||||
'Cmd-I' : this.makeItalic,
|
'Cmd-I' : this.makeItalic,
|
||||||
'Ctrl-U' : this.makeUnderline,
|
'Ctrl-U' : this.makeUnderline,
|
||||||
@@ -159,6 +168,7 @@ const CodeEditor = createClass({
|
|||||||
autoCloseTags : true,
|
autoCloseTags : true,
|
||||||
styleActiveLine : true,
|
styleActiveLine : true,
|
||||||
showTrailingSpace : false,
|
showTrailingSpace : false,
|
||||||
|
theme : this.props.editorTheme
|
||||||
// specialChars : / /,
|
// specialChars : / /,
|
||||||
// specialCharPlaceholder : function(char) {
|
// specialCharPlaceholder : function(char) {
|
||||||
// const el = document.createElement('span');
|
// const el = document.createElement('span');
|
||||||
@@ -176,7 +186,7 @@ const CodeEditor = createClass({
|
|||||||
|
|
||||||
indent : function () {
|
indent : function () {
|
||||||
const cm = this.codeMirror;
|
const cm = this.codeMirror;
|
||||||
if (cm.somethingSelected()) {
|
if(cm.somethingSelected()) {
|
||||||
cm.execCommand('indentMore');
|
cm.execCommand('indentMore');
|
||||||
} else {
|
} else {
|
||||||
cm.execCommand('insertSoftTab');
|
cm.execCommand('insertSoftTab');
|
||||||
@@ -213,6 +223,25 @@ const CodeEditor = createClass({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
makeSuper : function() {
|
||||||
|
const selection = this.codeMirror.getSelection(), t = selection.slice(0, 1) === '^' && selection.slice(-1) === '^';
|
||||||
|
this.codeMirror.replaceSelection(t ? selection.slice(1, -1) : `^${selection}^`, 'around');
|
||||||
|
if(selection.length === 0){
|
||||||
|
const cursor = this.codeMirror.getCursor();
|
||||||
|
this.codeMirror.setCursor({ line: cursor.line, ch: cursor.ch - 1 });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
makeSub : function() {
|
||||||
|
const selection = this.codeMirror.getSelection(), t = selection.slice(0, 2) === '^^' && selection.slice(-2) === '^^';
|
||||||
|
this.codeMirror.replaceSelection(t ? selection.slice(2, -2) : `^^${selection}^^`, 'around');
|
||||||
|
if(selection.length === 0){
|
||||||
|
const cursor = this.codeMirror.getCursor();
|
||||||
|
this.codeMirror.setCursor({ line: cursor.line, ch: cursor.ch - 2 });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
makeNbsp : function() {
|
makeNbsp : function() {
|
||||||
this.codeMirror.replaceSelection(' ', 'end');
|
this.codeMirror.replaceSelection(' ', 'end');
|
||||||
},
|
},
|
||||||
@@ -406,7 +435,10 @@ const CodeEditor = createClass({
|
|||||||
//----------------------//
|
//----------------------//
|
||||||
|
|
||||||
render : function(){
|
render : function(){
|
||||||
return <div className='codeEditor' ref='editor' style={this.props.style}/>;
|
return <>
|
||||||
|
<link href={`../homebrew/cm-themes/${this.props.editorTheme}.css`} rel='stylesheet' />
|
||||||
|
<div className='codeEditor' ref='editor' style={this.props.style}/>
|
||||||
|
</>;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.codeEditor{
|
.codeEditor{
|
||||||
|
@media screen and (pointer : coarse) {
|
||||||
|
font-size : 16px;
|
||||||
|
}
|
||||||
.CodeMirror-foldmarker {
|
.CodeMirror-foldmarker {
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
text-shadow: none;
|
text-shadow: none;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const Marked = require('marked');
|
const Marked = require('marked');
|
||||||
const MarkedExtendedTables = require('marked-extended-tables');
|
const MarkedExtendedTables = require('marked-extended-tables');
|
||||||
|
const { markedSmartypantsLite: MarkedSmartypantsLite } = require('marked-smartypants-lite');
|
||||||
const { gfmHeadingId: MarkedGFMHeadingId } = require('marked-gfm-heading-id');
|
const { gfmHeadingId: MarkedGFMHeadingId } = require('marked-gfm-heading-id');
|
||||||
const renderer = new Marked.Renderer();
|
const renderer = new Marked.Renderer();
|
||||||
|
|
||||||
@@ -27,13 +28,35 @@ renderer.paragraph = function(text){
|
|||||||
return `<p>${text}</p>\n`;
|
return `<p>${text}</p>\n`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
//Fix local links in the Preview iFrame to link inside the frame
|
||||||
|
renderer.link = function (href, title, text) {
|
||||||
|
let self = false;
|
||||||
|
if(href[0] == '#') {
|
||||||
|
self = true;
|
||||||
|
}
|
||||||
|
href = cleanUrl(this.options.sanitize, this.options.baseUrl, href);
|
||||||
|
|
||||||
|
if(href === null) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
let out = `<a href="${escape(href)}"`;
|
||||||
|
if(title) {
|
||||||
|
out += ` title="${title}"`;
|
||||||
|
}
|
||||||
|
if(self) {
|
||||||
|
out += ' target="_self"';
|
||||||
|
}
|
||||||
|
out += `>${text}</a>`;
|
||||||
|
return out;
|
||||||
|
};
|
||||||
|
|
||||||
const mustacheSpans = {
|
const mustacheSpans = {
|
||||||
name : 'mustacheSpans',
|
name : 'mustacheSpans',
|
||||||
level : 'inline', // Is this a block-level or inline-level tokenizer?
|
level : 'inline', // Is this a block-level or inline-level tokenizer?
|
||||||
start(src) { return src.match(/{{[^{]/)?.index; }, // Hint to Marked.js to stop and check for a match
|
start(src) { return src.match(/{{[^{]/)?.index; }, // Hint to Marked.js to stop and check for a match
|
||||||
tokenizer(src, tokens) {
|
tokenizer(src, tokens) {
|
||||||
const completeSpan = /^{{[^\n]*}}/; // Regex for the complete token
|
const completeSpan = /^{{[^\n]*}}/; // Regex for the complete token
|
||||||
const inlineRegex = /{{(?=((?::(?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':{}\s]*)*))\1 *|}}/g;
|
const inlineRegex = /{{(?=((?:[:=](?:"['\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1 *|}}/g;
|
||||||
const match = completeSpan.exec(src);
|
const match = completeSpan.exec(src);
|
||||||
if(match) {
|
if(match) {
|
||||||
//Find closing delimiter
|
//Find closing delimiter
|
||||||
@@ -44,7 +67,7 @@ const mustacheSpans = {
|
|||||||
let delim;
|
let delim;
|
||||||
while (delim = inlineRegex.exec(match[0])) {
|
while (delim = inlineRegex.exec(match[0])) {
|
||||||
if(!tags) {
|
if(!tags) {
|
||||||
tags = ` ${processStyleTags(delim[0].substring(2))}`;
|
tags = `${processStyleTags(delim[0].substring(2))}`;
|
||||||
endTags = delim[0].length;
|
endTags = delim[0].length;
|
||||||
}
|
}
|
||||||
if(delim[0].startsWith('{{')) {
|
if(delim[0].startsWith('{{')) {
|
||||||
@@ -83,7 +106,7 @@ const mustacheDivs = {
|
|||||||
start(src) { return src.match(/\n *{{[^{]/m)?.index; }, // Hint to Marked.js to stop and check for a match
|
start(src) { return src.match(/\n *{{[^{]/m)?.index; }, // Hint to Marked.js to stop and check for a match
|
||||||
tokenizer(src, tokens) {
|
tokenizer(src, tokens) {
|
||||||
const completeBlock = /^ *{{[^\n}]* *\n.*\n *}}/s; // Regex for the complete token
|
const completeBlock = /^ *{{[^\n}]* *\n.*\n *}}/s; // Regex for the complete token
|
||||||
const blockRegex = /^ *{{(?=((?::(?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':{}\s]*)*))\1 *$|^ *}}$/gm;
|
const blockRegex = /^ *{{(?=((?:[:=](?:"['\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1 *$|^ *}}$/gm;
|
||||||
const match = completeBlock.exec(src);
|
const match = completeBlock.exec(src);
|
||||||
if(match) {
|
if(match) {
|
||||||
//Find closing delimiter
|
//Find closing delimiter
|
||||||
@@ -94,8 +117,8 @@ const mustacheDivs = {
|
|||||||
let delim;
|
let delim;
|
||||||
while (delim = blockRegex.exec(match[0])?.[0].trim()) {
|
while (delim = blockRegex.exec(match[0])?.[0].trim()) {
|
||||||
if(!tags) {
|
if(!tags) {
|
||||||
tags = ` ${processStyleTags(delim.substring(2))}`;
|
tags = `${processStyleTags(delim.substring(2))}`;
|
||||||
endTags = delim.length;
|
endTags = delim.length + src.indexOf(delim);
|
||||||
}
|
}
|
||||||
if(delim.startsWith('{{')) {
|
if(delim.startsWith('{{')) {
|
||||||
blockCount++;
|
blockCount++;
|
||||||
@@ -131,14 +154,14 @@ const mustacheInjectInline = {
|
|||||||
level : 'inline',
|
level : 'inline',
|
||||||
start(src) { return src.match(/ *{[^{\n]/)?.index; }, // Hint to Marked.js to stop and check for a match
|
start(src) { return src.match(/ *{[^{\n]/)?.index; }, // Hint to Marked.js to stop and check for a match
|
||||||
tokenizer(src, tokens) {
|
tokenizer(src, tokens) {
|
||||||
const inlineRegex = /^ *{(?=((?::(?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':{}\s]*)*))\1}/g;
|
const inlineRegex = /^ *{(?=((?:[:=](?:"['\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1}/g;
|
||||||
const match = inlineRegex.exec(src);
|
const match = inlineRegex.exec(src);
|
||||||
if(match) {
|
if(match) {
|
||||||
const lastToken = tokens[tokens.length - 1];
|
const lastToken = tokens[tokens.length - 1];
|
||||||
if(!lastToken || lastToken.type == 'mustacheInjectInline')
|
if(!lastToken || lastToken.type == 'mustacheInjectInline')
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
const tags = ` ${processStyleTags(match[1])}`;
|
const tags = `${processStyleTags(match[1])}`;
|
||||||
lastToken.originalType = lastToken.type;
|
lastToken.originalType = lastToken.type;
|
||||||
lastToken.type = 'mustacheInjectInline';
|
lastToken.type = 'mustacheInjectInline';
|
||||||
lastToken.tags = tags;
|
lastToken.tags = tags;
|
||||||
@@ -166,7 +189,7 @@ const mustacheInjectBlock = {
|
|||||||
level : 'block',
|
level : 'block',
|
||||||
start(src) { return src.match(/\n *{[^{\n]/m)?.index; }, // Hint to Marked.js to stop and check for a match
|
start(src) { return src.match(/\n *{[^{\n]/m)?.index; }, // Hint to Marked.js to stop and check for a match
|
||||||
tokenizer(src, tokens) {
|
tokenizer(src, tokens) {
|
||||||
const inlineRegex = /^ *{(?=((?::(?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':{}\s]*)*))\1}/ym;
|
const inlineRegex = /^ *{(?=((?:[:=](?:"['\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1}/ym;
|
||||||
const match = inlineRegex.exec(src);
|
const match = inlineRegex.exec(src);
|
||||||
if(match) {
|
if(match) {
|
||||||
const lastToken = tokens[tokens.length - 1];
|
const lastToken = tokens[tokens.length - 1];
|
||||||
@@ -174,7 +197,7 @@ const mustacheInjectBlock = {
|
|||||||
return false;
|
return false;
|
||||||
|
|
||||||
lastToken.originalType = 'mustacheInjectBlock';
|
lastToken.originalType = 'mustacheInjectBlock';
|
||||||
lastToken.tags = ` ${processStyleTags(match[1])}`;
|
lastToken.tags = `${processStyleTags(match[1])}`;
|
||||||
return {
|
return {
|
||||||
type : 'mustacheInjectBlock', // Should match "name" above
|
type : 'mustacheInjectBlock', // Should match "name" above
|
||||||
raw : match[0], // Text to consume from the source
|
raw : match[0], // Text to consume from the source
|
||||||
@@ -205,6 +228,34 @@ const mustacheInjectBlock = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const superSubScripts = {
|
||||||
|
name : 'superSubScript',
|
||||||
|
level : 'inline',
|
||||||
|
start(src) { return src.match(/\^/m)?.index; }, // Hint to Marked.js to stop and check for a match
|
||||||
|
tokenizer(src, tokens) {
|
||||||
|
const superRegex = /^\^(?!\s)(?=([^\n\^]*[^\s\^]))\1\^/m;
|
||||||
|
const subRegex = /^\^\^(?!\s)(?=([^\n\^]*[^\s\^]))\1\^\^/m;
|
||||||
|
let isSuper = false;
|
||||||
|
let match = subRegex.exec(src);
|
||||||
|
if(!match){
|
||||||
|
match = superRegex.exec(src);
|
||||||
|
if(match)
|
||||||
|
isSuper = true;
|
||||||
|
}
|
||||||
|
if(match?.length) {
|
||||||
|
return {
|
||||||
|
type : 'superSubScript', // Should match "name" above
|
||||||
|
raw : match[0], // Text to consume from the source
|
||||||
|
tag : isSuper ? 'sup' : 'sub',
|
||||||
|
tokens : this.lexer.inlineTokens(match[1])
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
renderer(token) {
|
||||||
|
return `<${token.tag}>${this.parser.parseInline(token.tokens)}</${token.tag}>`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const definitionLists = {
|
const definitionLists = {
|
||||||
name : 'definitionLists',
|
name : 'definitionLists',
|
||||||
level : 'block',
|
level : 'block',
|
||||||
@@ -237,71 +288,10 @@ const definitionLists = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const MarkedSmartyPantsLite = ()=>{
|
Marked.use({ extensions: [mustacheSpans, mustacheDivs, mustacheInjectInline, definitionLists, superSubScripts] });
|
||||||
return {
|
|
||||||
tokenizer : {
|
|
||||||
inlineText(src) {
|
|
||||||
// don't escape inlineText
|
|
||||||
const cap = this.rules.inline.text.exec(src);
|
|
||||||
|
|
||||||
/* istanbul ignore next */
|
|
||||||
if(!cap) {
|
|
||||||
// should never happen
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const text = cap[0]
|
|
||||||
// em-dashes
|
|
||||||
.replace(/---/g, '\u2014')
|
|
||||||
// en-dashes
|
|
||||||
.replace(/--/g, '\u2013')
|
|
||||||
// opening singles
|
|
||||||
.replace(/(^|[-\u2014/(\[{"\s])'/g, '$1\u2018')
|
|
||||||
// closing singles & apostrophes
|
|
||||||
.replace(/'/g, '\u2019')
|
|
||||||
// opening doubles
|
|
||||||
.replace(/(^|[-\u2014/(\[{\u2018\s])"/g, '$1\u201c')
|
|
||||||
// closing doubles
|
|
||||||
.replace(/"/g, '\u201d')
|
|
||||||
// ellipses
|
|
||||||
.replace(/\.{3}/g, '\u2026');
|
|
||||||
|
|
||||||
return {
|
|
||||||
type : 'text',
|
|
||||||
raw : cap[0],
|
|
||||||
text : text
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
Marked.use({ extensions: [mustacheSpans, mustacheDivs, mustacheInjectInline, definitionLists] });
|
|
||||||
Marked.use(mustacheInjectBlock);
|
Marked.use(mustacheInjectBlock);
|
||||||
Marked.use({ renderer: renderer, mangle: false });
|
Marked.use({ renderer: renderer, mangle: false });
|
||||||
Marked.use(MarkedExtendedTables(), MarkedGFMHeadingId(), MarkedSmartyPantsLite());
|
Marked.use(MarkedExtendedTables(), MarkedGFMHeadingId(), MarkedSmartypantsLite());
|
||||||
|
|
||||||
//Fix local links in the Preview iFrame to link inside the frame
|
|
||||||
renderer.link = function (href, title, text) {
|
|
||||||
let self = false;
|
|
||||||
if(href[0] == '#') {
|
|
||||||
self = true;
|
|
||||||
}
|
|
||||||
href = cleanUrl(this.options.sanitize, this.options.baseUrl, href);
|
|
||||||
|
|
||||||
if(href === null) {
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
let out = `<a href="${escape(href)}"`;
|
|
||||||
if(title) {
|
|
||||||
out += ` title="${title}"`;
|
|
||||||
}
|
|
||||||
if(self) {
|
|
||||||
out += ' target="_self"';
|
|
||||||
}
|
|
||||||
out += `>${text}</a>`;
|
|
||||||
return out;
|
|
||||||
};
|
|
||||||
|
|
||||||
const nonWordAndColonTest = /[^\w:]/g;
|
const nonWordAndColonTest = /[^\w:]/g;
|
||||||
const cleanUrl = function (sanitize, base, href) {
|
const cleanUrl = function (sanitize, base, href) {
|
||||||
@@ -351,12 +341,6 @@ const escape = function (html, encode) {
|
|||||||
return html;
|
return html;
|
||||||
};
|
};
|
||||||
|
|
||||||
const sanatizeScriptTags = (content)=>{
|
|
||||||
return content
|
|
||||||
.replace(/<script/ig, '<script')
|
|
||||||
.replace(/<\/script>/ig, '</script>');
|
|
||||||
};
|
|
||||||
|
|
||||||
const tagTypes = ['div', 'span', 'a'];
|
const tagTypes = ['div', 'span', 'a'];
|
||||||
const tagRegex = new RegExp(`(${
|
const tagRegex = new RegExp(`(${
|
||||||
_.map(tagTypes, (type)=>{
|
_.map(tagTypes, (type)=>{
|
||||||
@@ -370,16 +354,19 @@ const voidTags = new Set([
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
const processStyleTags = (string)=>{
|
const processStyleTags = (string)=>{
|
||||||
//split tags up. quotes can only occur right after colons.
|
//split tags up. quotes can only occur right after : or =.
|
||||||
//TODO: can we simplify to just split on commas?
|
//TODO: can we simplify to just split on commas?
|
||||||
const tags = string.match(/(?:[^, ":]+|:(?:"[^"]*"|))+/g);
|
const tags = string.match(/(?:[^, ":=]+|[:=](?:"[^"]*"|))+/g);
|
||||||
|
|
||||||
if(!tags) return '"';
|
|
||||||
|
|
||||||
const id = _.remove(tags, (tag)=>tag.startsWith('#')).map((tag)=>tag.slice(1))[0];
|
const id = _.remove(tags, (tag)=>tag.startsWith('#')).map((tag)=>tag.slice(1))[0];
|
||||||
const classes = _.remove(tags, (tag)=>!tag.includes(':'));
|
const classes = _.remove(tags, (tag)=>(!tag.includes(':')) && (!tag.includes('=')));
|
||||||
const styles = tags.map((tag)=>tag.replace(/:"?([^"]*)"?/g, ':$1;'));
|
const attributes = _.remove(tags, (tag)=>(tag.includes('='))).map((tag)=>tag.replace(/="?([^"]*)"?/g, '="$1"'));
|
||||||
return `${classes.join(' ')}" ${id ? `id="${id}"` : ''} ${styles.length ? `style="${styles.join(' ')}"` : ''}`;
|
const styles = tags?.length ? tags.map((tag)=>tag.replace(/:"?([^"]*)"?/g, ':$1;').trim()) : [];
|
||||||
|
|
||||||
|
return `${classes?.length ? ` ${classes.join(' ')}` : ''}"` +
|
||||||
|
`${id ? ` id="${id}"` : ''}` +
|
||||||
|
`${styles?.length ? ` style="${styles.join(' ')}"` : ''}` +
|
||||||
|
`${attributes?.length ? ` ${attributes.join(' ')}` : ''}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
@@ -387,7 +374,7 @@ module.exports = {
|
|||||||
render : (rawBrewText)=>{
|
render : (rawBrewText)=>{
|
||||||
rawBrewText = rawBrewText.replace(/^\\column$/gm, `\n<div class='columnSplit'></div>\n`)
|
rawBrewText = rawBrewText.replace(/^\\column$/gm, `\n<div class='columnSplit'></div>\n`)
|
||||||
.replace(/^(:+)$/gm, (match)=>`${`<div class='blank'></div>`.repeat(match.length)}\n`);
|
.replace(/^(:+)$/gm, (match)=>`${`<div class='blank'></div>`.repeat(match.length)}\n`);
|
||||||
return Marked.parse(sanatizeScriptTags(rawBrewText));
|
return Marked.parse(rawBrewText);
|
||||||
},
|
},
|
||||||
|
|
||||||
validate : (rawBrewText)=>{
|
validate : (rawBrewText)=>{
|
||||||
|
|||||||
@@ -90,12 +90,6 @@ const escape = function (html, encode) {
|
|||||||
return html;
|
return html;
|
||||||
};
|
};
|
||||||
|
|
||||||
const sanatizeScriptTags = (content)=>{
|
|
||||||
return content
|
|
||||||
.replace(/<script/ig, '<script')
|
|
||||||
.replace(/<\/script>/ig, '</script>');
|
|
||||||
};
|
|
||||||
|
|
||||||
const tagTypes = ['div', 'span', 'a'];
|
const tagTypes = ['div', 'span', 'a'];
|
||||||
const tagRegex = new RegExp(`(${
|
const tagRegex = new RegExp(`(${
|
||||||
_.map(tagTypes, (type)=>{
|
_.map(tagTypes, (type)=>{
|
||||||
@@ -113,7 +107,7 @@ module.exports = {
|
|||||||
marked : Markdown,
|
marked : Markdown,
|
||||||
render : (rawBrewText)=>{
|
render : (rawBrewText)=>{
|
||||||
return Markdown(
|
return Markdown(
|
||||||
sanatizeScriptTags(rawBrewText),
|
rawBrewText,
|
||||||
{ renderer: renderer }
|
{ renderer: renderer }
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
require('./nav.less');
|
require('./nav.less');
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
|
const { useState, useRef, useEffect } = React;
|
||||||
const createClass = require('create-react-class');
|
const createClass = require('create-react-class');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const cx = require('classnames');
|
const cx = require('classnames');
|
||||||
@@ -71,64 +72,49 @@ const Nav = {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
dropdown : createClass({
|
dropdown : function dropdown(props) {
|
||||||
displayName : 'Nav.dropdown',
|
props = Object.assign({}, props, {
|
||||||
getDefaultProps : function() {
|
trigger : 'hover click'
|
||||||
return {
|
|
||||||
trigger : 'hover'
|
|
||||||
};
|
|
||||||
},
|
|
||||||
getInitialState : function() {
|
|
||||||
return {
|
|
||||||
showDropdown : false
|
|
||||||
};
|
|
||||||
},
|
|
||||||
componentDidMount : function() {
|
|
||||||
if(this.props.trigger == 'click')
|
|
||||||
document.addEventListener('click', this.handleClickOutside);
|
|
||||||
},
|
|
||||||
componentWillUnmount : function() {
|
|
||||||
if(this.props.trigger == 'click')
|
|
||||||
document.removeEventListener('click', this.handleClickOutside);
|
|
||||||
},
|
|
||||||
handleClickOutside : function(e){
|
|
||||||
// Close dropdown when clicked outside
|
|
||||||
if(this.refs.dropdown && !this.refs.dropdown.contains(e.target)) {
|
|
||||||
this.handleDropdown(false);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
handleDropdown : function(show){
|
|
||||||
this.setState({
|
|
||||||
showDropdown : show
|
|
||||||
});
|
});
|
||||||
},
|
|
||||||
renderDropdown : function(dropdownChildren){
|
|
||||||
if(!this.state.showDropdown) return null;
|
|
||||||
|
|
||||||
return (
|
const myRef = useRef(null);
|
||||||
<div className='navDropdown'>
|
const [showDropdown, setShowDropdown] = useState(false);
|
||||||
{dropdownChildren}
|
|
||||||
</div>
|
useEffect(()=>{
|
||||||
);
|
document.addEventListener('click', handleClickOutside);
|
||||||
},
|
return ()=>{
|
||||||
render : function () {
|
document.removeEventListener('click', handleClickOutside);
|
||||||
const dropdownChildren = React.Children.map(this.props.children, (child, i)=>{
|
};
|
||||||
// Ignore the first child
|
}, []);
|
||||||
|
|
||||||
|
function handleClickOutside(e) {
|
||||||
|
// Close dropdown when clicked outside
|
||||||
|
if(!myRef.current?.contains(e.target)) {
|
||||||
|
handleDropdown(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDropdown(show) {
|
||||||
|
setShowDropdown(show ?? !showDropdown);
|
||||||
|
}
|
||||||
|
|
||||||
|
const dropdownChildren = React.Children.map(props.children, (child, i)=>{
|
||||||
if(i < 1) return;
|
if(i < 1) return;
|
||||||
return child;
|
return child;
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`navDropdownContainer ${this.props.className}`}
|
<div className={`navDropdownContainer ${props.className}`}
|
||||||
ref='dropdown'
|
ref={myRef}
|
||||||
onMouseEnter={this.props.trigger == 'hover' ? ()=>{this.handleDropdown(true);} : undefined}
|
onMouseEnter = { props.trigger.includes('hover') ? ()=>handleDropdown(true) : undefined }
|
||||||
onClick= {this.props.trigger == 'click' ? ()=>{this.handleDropdown(true);} : undefined}
|
onMouseLeave = { props.trigger.includes('hover') ? ()=>handleDropdown(false) : undefined }
|
||||||
onMouseLeave={this.props.trigger == 'hover' ? ()=>{this.handleDropdown(false);} : undefined}>
|
onClick = { props.trigger.includes('click') ? ()=>handleDropdown(true) : undefined }
|
||||||
{this.props.children[0] || this.props.children /*children is not an array when only one child*/}
|
>
|
||||||
{this.renderDropdown(dropdownChildren)}
|
{props.children[0] || props.children /*children is not an array when only one child*/}
|
||||||
|
{showDropdown && <div className='navDropdown'>{dropdownChildren}</div>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -79,6 +79,8 @@ nav{
|
|||||||
left : 0px;
|
left : 0px;
|
||||||
z-index : 10000;
|
z-index : 10000;
|
||||||
width : 100%;
|
width : 100%;
|
||||||
|
overflow : hidden auto;
|
||||||
|
max-height : calc(100vh - 28px);
|
||||||
.navItem{
|
.navItem{
|
||||||
animation-name: glideDropDown;
|
animation-name: glideDropDown;
|
||||||
animation-duration: 0.4s;
|
animation-duration: 0.4s;
|
||||||
|
|||||||
@@ -61,7 +61,8 @@ const SplitPane = createClass({
|
|||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
|
|
||||||
handleUp : function(){
|
handleUp : function(e){
|
||||||
|
e.preventDefault();
|
||||||
if(this.state.isDragging){
|
if(this.state.isDragging){
|
||||||
this.props.onDragFinish(this.state.currentDividerPos);
|
this.props.onDragFinish(this.state.currentDividerPos);
|
||||||
window.localStorage.setItem(this.props.storageKey, this.state.currentDividerPos);
|
window.localStorage.setItem(this.props.storageKey, this.state.currentDividerPos);
|
||||||
@@ -78,6 +79,7 @@ const SplitPane = createClass({
|
|||||||
handleMove : function(e){
|
handleMove : function(e){
|
||||||
if(!this.state.isDragging) return;
|
if(!this.state.isDragging) return;
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
const newSize = this.limitPosition(e.pageX);
|
const newSize = this.limitPosition(e.pageX);
|
||||||
this.setState({
|
this.setState({
|
||||||
currentDividerPos : newSize,
|
currentDividerPos : newSize,
|
||||||
@@ -122,7 +124,7 @@ const SplitPane = createClass({
|
|||||||
renderDivider : function(){
|
renderDivider : function(){
|
||||||
return <>
|
return <>
|
||||||
{this.renderMoveArrows()}
|
{this.renderMoveArrows()}
|
||||||
<div className='divider' onMouseDown={this.handleDown} >
|
<div className='divider' onPointerDown={this.handleDown} >
|
||||||
<div className='dots'>
|
<div className='dots'>
|
||||||
<i className='fas fa-circle' />
|
<i className='fas fa-circle' />
|
||||||
<i className='fas fa-circle' />
|
<i className='fas fa-circle' />
|
||||||
@@ -133,7 +135,7 @@ const SplitPane = createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
render : function(){
|
render : function(){
|
||||||
return <div className='splitPane' onMouseMove={this.handleMove} onMouseUp={this.handleUp}>
|
return <div className='splitPane' onPointerMove={this.handleMove} onPointerUp={this.handleUp}>
|
||||||
<Pane
|
<Pane
|
||||||
ref='pane1'
|
ref='pane1'
|
||||||
width={this.state.currentDividerPos}
|
width={this.state.currentDividerPos}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
flex : 1;
|
flex : 1;
|
||||||
}
|
}
|
||||||
.divider{
|
.divider{
|
||||||
|
touch-action : none;
|
||||||
display : table;
|
display : table;
|
||||||
height : 100%;
|
height : 100%;
|
||||||
width : 15px;
|
width : 15px;
|
||||||
|
|||||||
@@ -2,12 +2,6 @@
|
|||||||
|
|
||||||
const Markdown = require('naturalcrit/markdown.js');
|
const Markdown = require('naturalcrit/markdown.js');
|
||||||
|
|
||||||
test('Escapes <script> tag', function() {
|
|
||||||
const source = '<script></script>';
|
|
||||||
const rendered = Markdown.render(source);
|
|
||||||
expect(rendered).toMatch('<script></script>');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Processes the markdown within an HTML block if its just a class wrapper', function() {
|
test('Processes the markdown within an HTML block if its just a class wrapper', function() {
|
||||||
const source = '<div>*Bold text*</div>';
|
const source = '<div>*Bold text*</div>';
|
||||||
const rendered = Markdown.render(source);
|
const rendered = Markdown.render(source);
|
||||||
|
|||||||
@@ -13,137 +13,134 @@ String.prototype.trimReturns = function(){
|
|||||||
// Remove the `.failing()` method once you have fixed the issue.
|
// Remove the `.failing()` method once you have fixed the issue.
|
||||||
|
|
||||||
describe('Inline: When using the Inline syntax {{ }}', ()=>{
|
describe('Inline: When using the Inline syntax {{ }}', ()=>{
|
||||||
it.failing('Renders a mustache span with text only', function() {
|
it('Renders a mustache span with text only', function() {
|
||||||
const source = '{{ text}}';
|
const source = '{{ text}}';
|
||||||
const rendered = Markdown.render(source);
|
const rendered = Markdown.render(source);
|
||||||
// FIXME: adds extra \s after class names
|
|
||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block">text</span>');
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block">text</span>');
|
||||||
});
|
});
|
||||||
|
|
||||||
it.failing('Renders a mustache span with text only, but with spaces', function() {
|
it('Renders a mustache span with text only, but with spaces', function() {
|
||||||
const source = '{{ this is a text}}';
|
const source = '{{ this is a text}}';
|
||||||
const rendered = Markdown.render(source);
|
const rendered = Markdown.render(source);
|
||||||
// FIXME: adds extra \s after class names
|
|
||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block">this is a text</span>');
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block">this is a text</span>');
|
||||||
});
|
});
|
||||||
|
|
||||||
it.failing('Renders an empty mustache span', function() {
|
it('Renders an empty mustache span', function() {
|
||||||
const source = '{{}}';
|
const source = '{{}}';
|
||||||
const rendered = Markdown.render(source);
|
const rendered = Markdown.render(source);
|
||||||
// FIXME: adds extra \s after class names
|
|
||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block"></span>');
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block"></span>');
|
||||||
});
|
});
|
||||||
|
|
||||||
it.failing('Renders a mustache span with just a space', function() {
|
it('Renders a mustache span with just a space', function() {
|
||||||
const source = '{{ }}';
|
const source = '{{ }}';
|
||||||
const rendered = Markdown.render(source);
|
const rendered = Markdown.render(source);
|
||||||
// FIXME: adds extra \s after class names
|
|
||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block"></span>');
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block"></span>');
|
||||||
});
|
});
|
||||||
|
|
||||||
it.failing('Renders a mustache span with a few spaces only', function() {
|
it('Renders a mustache span with a few spaces only', function() {
|
||||||
const source = '{{ }}';
|
const source = '{{ }}';
|
||||||
const rendered = Markdown.render(source);
|
const rendered = Markdown.render(source);
|
||||||
// FIXME: adds extra \s after class names
|
|
||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block"></span>');
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block"></span>');
|
||||||
});
|
});
|
||||||
|
|
||||||
it.failing('Renders a mustache span with text and class', function() {
|
it('Renders a mustache span with text and class', function() {
|
||||||
const source = '{{my-class text}}';
|
const source = '{{my-class text}}';
|
||||||
const rendered = Markdown.render(source);
|
const rendered = Markdown.render(source);
|
||||||
// FIXME: adds two extra \s before closing `>` in opening tag.
|
|
||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block my-class">text</span>');
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block my-class">text</span>');
|
||||||
});
|
});
|
||||||
|
|
||||||
it.failing('Renders a mustache span with text and two classes', function() {
|
it('Renders a mustache span with text and two classes', function() {
|
||||||
const source = '{{my-class,my-class2 text}}';
|
const source = '{{my-class,my-class2 text}}';
|
||||||
const rendered = Markdown.render(source);
|
const rendered = Markdown.render(source);
|
||||||
// FIXME: adds two extra \s before closing `>` in opening tag.
|
|
||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block my-class my-class2">text</span>');
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block my-class my-class2">text</span>');
|
||||||
});
|
});
|
||||||
|
|
||||||
it.failing('Renders a mustache span with text with spaces and class', function() {
|
it('Renders a mustache span with text with spaces and class', function() {
|
||||||
const source = '{{my-class this is a text}}';
|
const source = '{{my-class this is a text}}';
|
||||||
const rendered = Markdown.render(source);
|
const rendered = Markdown.render(source);
|
||||||
// FIXME: adds two extra \s before closing `>` in opening tag
|
|
||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block my-class">this is a text</span>');
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block my-class">this is a text</span>');
|
||||||
});
|
});
|
||||||
|
|
||||||
it.failing('Renders a mustache span with text and id', function() {
|
it('Renders a mustache span with text and id', function() {
|
||||||
const source = '{{#my-span text}}';
|
const source = '{{#my-span text}}';
|
||||||
const rendered = Markdown.render(source);
|
const rendered = Markdown.render(source);
|
||||||
// FIXME: adds extra \s before closing `>` in opening tag, and another after class names
|
|
||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" id="my-span">text</span>');
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" id="my-span">text</span>');
|
||||||
});
|
});
|
||||||
|
|
||||||
it.failing('Renders a mustache span with text and two ids', function() {
|
it('Renders a mustache span with text and two ids', function() {
|
||||||
const source = '{{#my-span,#my-favorite-span text}}';
|
const source = '{{#my-span,#my-favorite-span text}}';
|
||||||
const rendered = Markdown.render(source);
|
const rendered = Markdown.render(source);
|
||||||
// FIXME: adds extra \s before closing `>` in opening tag, and another after class names
|
|
||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" id="my-span">text</span>');
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" id="my-span">text</span>');
|
||||||
});
|
});
|
||||||
|
|
||||||
it.failing('Renders a mustache span with text and css property', function() {
|
it('Renders a mustache span with text and css property', function() {
|
||||||
const source = '{{color:red text}}';
|
const source = '{{color:red text}}';
|
||||||
const rendered = Markdown.render(source);
|
const rendered = Markdown.render(source);
|
||||||
// FIXME: adds extra \s after class names
|
|
||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" style="color:red;">text</span>');
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" style="color:red;">text</span>');
|
||||||
});
|
});
|
||||||
|
|
||||||
it.failing('Renders a mustache span with text and two css properties', function() {
|
it('Renders a mustache span with text and two css properties', function() {
|
||||||
const source = '{{color:red,padding:5px text}}';
|
const source = '{{color:red,padding:5px text}}';
|
||||||
const rendered = Markdown.render(source);
|
const rendered = Markdown.render(source);
|
||||||
// FIXME: adds extra \s after class names
|
|
||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" style="color:red; padding:5px;">text</span>');
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" style="color:red; padding:5px;">text</span>');
|
||||||
});
|
});
|
||||||
|
|
||||||
it.failing('Renders a mustache span with text and css property which contains quotes', function() {
|
it('Renders a mustache span with text and css property which contains quotes', function() {
|
||||||
const source = '{{font-family:"trebuchet ms" text}}';
|
const source = '{{font-family:"trebuchet ms" text}}';
|
||||||
const rendered = Markdown.render(source);
|
const rendered = Markdown.render(source);
|
||||||
// FIXME: adds extra \s after class names
|
|
||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" style="font-family:trebuchet ms;">text</span>');
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" style="font-family:trebuchet ms;">text</span>');
|
||||||
});
|
});
|
||||||
|
|
||||||
it.failing('Renders a mustache span with text and two css properties which contains quotes', function() {
|
it('Renders a mustache span with text and two css properties which contains quotes', function() {
|
||||||
const source = '{{font-family:"trebuchet ms",padding:"5px 10px" text}}';
|
const source = '{{font-family:"trebuchet ms",padding:"5px 10px" text}}';
|
||||||
const rendered = Markdown.render(source);
|
const rendered = Markdown.render(source);
|
||||||
// FIXME: adds extra \s after class names
|
|
||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" style="font-family:trebuchet ms; padding:5px 10px;">text</span>');
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" style="font-family:trebuchet ms; padding:5px 10px;">text</span>');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it.failing('Renders a mustache span with text with quotes and css property which contains quotes', function() {
|
it('Renders a mustache span with text with quotes and css property which contains double quotes', function() {
|
||||||
const source = '{{font-family:"trebuchet ms" text "with quotes"}}';
|
const source = '{{font-family:"trebuchet ms" text "with quotes"}}';
|
||||||
const rendered = Markdown.render(source);
|
const rendered = Markdown.render(source);
|
||||||
// FIXME: adds extra \s after class names
|
|
||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" style="font-family:trebuchet ms;">text “with quotes”</span>');
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" style="font-family:trebuchet ms;">text “with quotes”</span>');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('Renders a mustache span with text with quotes and css property which contains double and simple quotes', function() {
|
||||||
|
const source = `{{--stringVariable:"'string'" text "with quotes"}}`;
|
||||||
|
const rendered = Markdown.render(source);
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<span class="inline-block" style="--stringVariable:'string';">text “with quotes”</span>`);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
it('Renders a mustache span with text, id, class and a couple of css properties', function() {
|
it('Renders a mustache span with text, id, class and a couple of css properties', function() {
|
||||||
const source = '{{pen,#author,color:orange,font-family:"trebuchet ms" text}}';
|
const source = '{{pen,#author,color:orange,font-family:"trebuchet ms" text}}';
|
||||||
const rendered = Markdown.render(source);
|
const rendered = Markdown.render(source);
|
||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block pen" id="author" style="color:orange; font-family:trebuchet ms;">text</span>');
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block pen" id="author" style="color:orange; font-family:trebuchet ms;">text</span>');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Renders a span with added attributes', function() {
|
||||||
|
const source = 'Text and {{pen,#author,color:orange,font-family:"trebuchet ms",a="b and c",d=e, text}} and more text!';
|
||||||
|
const rendered = Markdown.render(source);
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p>Text and <span class="inline-block pen" id="author" style="color:orange; font-family:trebuchet ms;" a="b and c" d="e">text</span> and more text!</p>\n');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// BLOCK SYNTAX
|
// BLOCK SYNTAX
|
||||||
|
|
||||||
describe(`Block: When using the Block syntax {{tags\\ntext\\n}}`, ()=>{
|
describe(`Block: When using the Block syntax {{tags\\ntext\\n}}`, ()=>{
|
||||||
it.failing('Renders a div with text only', function() {
|
it('Renders a div with text only', function() {
|
||||||
const source = dedent`{{
|
const source = dedent`{{
|
||||||
text
|
text
|
||||||
}}`;
|
}}`;
|
||||||
const rendered = Markdown.render(source).trimReturns();
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
// FIXME: adds extra \s after class names
|
|
||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block"><p>text</p></div>`);
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block"><p>text</p></div>`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it.failing('Renders an empty div', function() {
|
it('Renders an empty div', function() {
|
||||||
const source = dedent`{{
|
const source = dedent`{{
|
||||||
|
|
||||||
}}`;
|
}}`;
|
||||||
const rendered = Markdown.render(source).trimReturns();
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
// FIXME: adds extra \s after class names
|
|
||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block"></div>`);
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block"></div>`);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -151,52 +148,62 @@ describe(`Block: When using the Block syntax {{tags\\ntext\\n}}`, ()=>{
|
|||||||
const source = dedent`{{
|
const source = dedent`{{
|
||||||
}}`;
|
}}`;
|
||||||
const rendered = Markdown.render(source).trimReturns();
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
// this actually renders in HB as '{{ }}'...
|
|
||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p>{{}}</p>`);
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p>{{}}</p>`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it.failing('Renders a div with a single class', function() {
|
it('Renders a div with a single class', function() {
|
||||||
const source = dedent`{{cat
|
const source = dedent`{{cat
|
||||||
|
|
||||||
}}`;
|
}}`;
|
||||||
const rendered = Markdown.render(source).trimReturns();
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
// FIXME: adds two extra \s before closing `>` in opening tag
|
|
||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block cat"></div>`);
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block cat"></div>`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it.failing('Renders a div with a single class and text', function() {
|
it('Renders a div with a single class and text', function() {
|
||||||
const source = dedent`{{cat
|
const source = dedent`{{cat
|
||||||
Sample text.
|
Sample text.
|
||||||
}}`;
|
}}`;
|
||||||
const rendered = Markdown.render(source).trimReturns();
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
// FIXME: adds two extra \s before closing `>` in opening tag
|
|
||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block cat"><p>Sample text.</p></div>`);
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block cat"><p>Sample text.</p></div>`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it.failing('Renders a div with two classes and text', function() {
|
it('Renders a div with two classes and text', function() {
|
||||||
const source = dedent`{{cat,dog
|
const source = dedent`{{cat,dog
|
||||||
Sample text.
|
Sample text.
|
||||||
}}`;
|
}}`;
|
||||||
const rendered = Markdown.render(source).trimReturns();
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
// FIXME: adds two extra \s before closing `>` in opening tag
|
|
||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block cat dog"><p>Sample text.</p></div>`);
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block cat dog"><p>Sample text.</p></div>`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it.failing('Renders a div with a style and text', function() {
|
it('Renders a div with a style and text', function() {
|
||||||
const source = dedent`{{color:red
|
const source = dedent`{{color:red
|
||||||
Sample text.
|
Sample text.
|
||||||
}}`;
|
}}`;
|
||||||
const rendered = Markdown.render(source).trimReturns();
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
// FIXME: adds two extra \s before closing `>` in opening tag
|
|
||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block" style="color:red;"><p>Sample text.</p></div>`);
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block" style="color:red;"><p>Sample text.</p></div>`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it.failing('Renders a div with a class, style and text', function() {
|
it('Renders a div with a style that has a string variable, and text', function() {
|
||||||
|
const source = dedent`{{--stringVariable:"'string'"
|
||||||
|
Sample text.
|
||||||
|
}}`;
|
||||||
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block" style="--stringVariable:'string';"><p>Sample text.</p></div>`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Renders a div with a style that has a string variable, and text', function() {
|
||||||
|
const source = dedent`{{--stringVariable:"'string'"
|
||||||
|
Sample text.
|
||||||
|
}}`;
|
||||||
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block" style="--stringVariable:'string';"><p>Sample text.</p></div>`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Renders a div with a class, style and text', function() {
|
||||||
const source = dedent`{{cat,color:red
|
const source = dedent`{{cat,color:red
|
||||||
Sample text.
|
Sample text.
|
||||||
}}`;
|
}}`;
|
||||||
const rendered = Markdown.render(source).trimReturns();
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
// FIXME: adds extra \s after the class attribute
|
|
||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block cat" style="color:red;"><p>Sample text.</p></div>`);
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block cat" style="color:red;"><p>Sample text.</p></div>`);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -208,14 +215,27 @@ describe(`Block: When using the Block syntax {{tags\\ntext\\n}}`, ()=>{
|
|||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block cat" id="dog" style="color:red;"><p>Sample text.</p></div>`);
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block cat" id="dog" style="color:red;"><p>Sample text.</p></div>`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it.failing('Renders a div with a single ID', function() {
|
it('Renders a div with a single ID', function() {
|
||||||
const source = dedent`{{#cat,#dog
|
const source = dedent`{{#cat,#dog
|
||||||
Sample text.
|
Sample text.
|
||||||
}}`;
|
}}`;
|
||||||
const rendered = Markdown.render(source).trimReturns();
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
// FIXME: adds extra \s before closing `>` in opening tag, and another after class names
|
|
||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block" id="cat"><p>Sample text.</p></div>`);
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block" id="cat"><p>Sample text.</p></div>`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Renders a div with an ID, class, style and text, and a variable assignment', function() {
|
||||||
|
const source = dedent`{{color:red,cat,#dog,a="b and c",d="e"
|
||||||
|
Sample text.
|
||||||
|
}}`;
|
||||||
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class=\"block cat\" id=\"dog\" style=\"color:red;\" a=\"b and c\" d=\"e\"><p>Sample text.</p></div>`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Renders a div with added attributes', function() {
|
||||||
|
const source = '{{pen,#author,color:orange,font-family:"trebuchet ms",a="b and c",d=e\nText and text and more text!\n}}\n';
|
||||||
|
const rendered = Markdown.render(source);
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<div class="block pen" id="author" style="color:orange; font-family:trebuchet ms;" a="b and c" d="e"><p>Text and text and more text!</p>\n</div>');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// MUSTACHE INJECTION SYNTAX
|
// MUSTACHE INJECTION SYNTAX
|
||||||
@@ -235,12 +255,24 @@ describe('Injection: When an injection tag follows an element', ()=>{
|
|||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block ClassName">text</span>');
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block ClassName">text</span>');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it.failing('Renders a span "text" with injected attribute', function() {
|
||||||
|
const source = '{{ text}}{a="b and c"}';
|
||||||
|
const rendered = Markdown.render(source);
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span a="b and c" class="inline-block ">text</span>');
|
||||||
|
});
|
||||||
|
|
||||||
it.failing('Renders a span "text" with injected style', function() {
|
it.failing('Renders a span "text" with injected style', function() {
|
||||||
const source = '{{ text}}{color:red}';
|
const source = '{{ text}}{color:red}';
|
||||||
const rendered = Markdown.render(source);
|
const rendered = Markdown.render(source);
|
||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" style="color:red;">text</span>');
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" style="color:red;">text</span>');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it.failing('Renders a span "text" with injected style using a string variable', function() {
|
||||||
|
const source = `{{ text}}{--stringVariable:"'string'"}`;
|
||||||
|
const rendered = Markdown.render(source);
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<span class="inline-block" style="--stringVariable:'string';">text</span>`);
|
||||||
|
});
|
||||||
|
|
||||||
it.failing('Renders a span "text" with two injected styles', function() {
|
it.failing('Renders a span "text" with two injected styles', function() {
|
||||||
const source = '{{ text}}{color:red,background:blue}';
|
const source = '{{ text}}{color:red,background:blue}';
|
||||||
const rendered = Markdown.render(source);
|
const rendered = Markdown.render(source);
|
||||||
@@ -270,6 +302,12 @@ describe('Injection: When an injection tag follows an element', ()=>{
|
|||||||
const rendered = Markdown.render(source).trimReturns();
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p><span class="inline-block" style="color:red;">text</span>{background:blue}</p>');
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p><span class="inline-block" style="color:red;">text</span>{background:blue}</p>');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Renders an image with added attributes', function() {
|
||||||
|
const source = ` {position:absolute,bottom:20px,left:130px,width:220px,a="b and c",d=e}`;
|
||||||
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p><img class="" style="position:absolute; bottom:20px; left:130px; width:220px;" a="b and c" d="e" src="https://i.imgur.com/hMna6G0.png" alt="homebrew mug"></p>`);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('and that element is a block', ()=>{
|
describe('and that element is a block', ()=>{
|
||||||
@@ -297,7 +335,16 @@ describe('Injection: When an injection tag follows an element', ()=>{
|
|||||||
}}
|
}}
|
||||||
{color:red,background:blue}`;
|
{color:red,background:blue}`;
|
||||||
const rendered = Markdown.render(source).trimReturns();
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<div class="block" style="color:red; background:blue;"><p>text</p></div>');
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block" style="color:red; background:blue"><p>text</p></div>`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.failing('renders a div "text" with injected variable string', function() {
|
||||||
|
const source = dedent`{{
|
||||||
|
text
|
||||||
|
}}
|
||||||
|
{--stringVariable:"'string'"}`;
|
||||||
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block" style="--stringVariable:'string'"><p>text</p></div>`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it.failing('renders an h2 header "text" with injected class name', function() {
|
it.failing('renders an h2 header "text" with injected class name', function() {
|
||||||
|
|||||||
@@ -47,8 +47,8 @@ const getTOC = (pages)=>{
|
|||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = function(brew){
|
module.exports = function(props){
|
||||||
const pages = brew.text.split('\\page');
|
const pages = props.brew.text.split('\\page');
|
||||||
const TOC = getTOC(pages);
|
const TOC = getTOC(pages);
|
||||||
const markdown = _.reduce(TOC, (r, g1, idx1)=>{
|
const markdown = _.reduce(TOC, (r, g1, idx1)=>{
|
||||||
r.push(`- **[${idx1 + 1} ${g1.title}](#p${g1.page})**`);
|
r.push(`- **[${idx1 + 1} ${g1.title}](#p${g1.page})**`);
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ body {
|
|||||||
-webkit-column-gap : 1cm;
|
-webkit-column-gap : 1cm;
|
||||||
-moz-column-gap : 1cm;
|
-moz-column-gap : 1cm;
|
||||||
}
|
}
|
||||||
.phb{
|
.phb, .page{
|
||||||
.useColumns();
|
.useColumns();
|
||||||
counter-increment : phb-page-numbers;
|
counter-increment : phb-page-numbers;
|
||||||
position : relative;
|
position : relative;
|
||||||
@@ -59,6 +59,9 @@ body {
|
|||||||
page-break-before : always;
|
page-break-before : always;
|
||||||
page-break-after : always;
|
page-break-after : always;
|
||||||
contain : size;
|
contain : size;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phb{
|
||||||
//*****************************
|
//*****************************
|
||||||
// * BASE
|
// * BASE
|
||||||
// *****************************/
|
// *****************************/
|
||||||
|
|||||||
@@ -10,6 +10,21 @@
|
|||||||
background-image : url(/assets/DMG_background.png);
|
background-image : url(/assets/DMG_background.png);
|
||||||
background-size : cover;
|
background-size : cover;
|
||||||
|
|
||||||
|
/*TABLES WITHIN NOTES*/
|
||||||
|
.note table tbody tr:nth-child(odd) {
|
||||||
|
background:#fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*DROP CAP*/
|
||||||
|
h1 + p::first-letter {
|
||||||
|
background-image: unset;
|
||||||
|
color:black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quote p:first-child::first-line {
|
||||||
|
all: unset;
|
||||||
|
}
|
||||||
|
|
||||||
&:after {
|
&:after {
|
||||||
background-image : url(/assets/DMG_footerAccent.png);
|
background-image : url(/assets/DMG_footerAccent.png);
|
||||||
height: 58px;
|
height: 58px;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ const ClassFeatureGen = require('./snippets/classfeature.gen.js');
|
|||||||
const CoverPageGen = require('./snippets/coverpage.gen.js');
|
const CoverPageGen = require('./snippets/coverpage.gen.js');
|
||||||
const TableOfContentsGen = require('./snippets/tableOfContents.gen.js');
|
const TableOfContentsGen = require('./snippets/tableOfContents.gen.js');
|
||||||
const indexGen = require('./snippets/index.gen.js');
|
const indexGen = require('./snippets/index.gen.js');
|
||||||
|
const QuoteGen = require('./snippets/quote.gen.js');
|
||||||
const dedent = require('dedent-tabs').default;
|
const dedent = require('dedent-tabs').default;
|
||||||
|
|
||||||
|
|
||||||
@@ -19,16 +20,6 @@ module.exports = [
|
|||||||
icon : 'fas fa-pencil-alt',
|
icon : 'fas fa-pencil-alt',
|
||||||
view : 'text',
|
view : 'text',
|
||||||
snippets : [
|
snippets : [
|
||||||
{
|
|
||||||
name : 'Page Number',
|
|
||||||
icon : 'fas fa-bookmark',
|
|
||||||
gen : '{{pageNumber 1}}\n{{footnote PART 1 | SECTION NAME}}\n\n'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name : 'Auto-incrementing Page Number',
|
|
||||||
icon : 'fas fa-sort-numeric-down',
|
|
||||||
gen : '{{pageNumber,auto}}\n{{footnote PART 1 | SECTION NAME}}\n\n'
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name : 'Table of Contents',
|
name : 'Table of Contents',
|
||||||
icon : 'fas fa-book',
|
icon : 'fas fa-book',
|
||||||
@@ -133,6 +124,11 @@ module.exports = [
|
|||||||
icon : 'fas fa-mask',
|
icon : 'fas fa-mask',
|
||||||
gen : ClassFeatureGen,
|
gen : ClassFeatureGen,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name : 'Quote',
|
||||||
|
icon : 'fas fa-quote-right',
|
||||||
|
gen : QuoteGen,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name : 'Note',
|
name : 'Note',
|
||||||
icon : 'fas fa-sticky-note',
|
icon : 'fas fa-sticky-note',
|
||||||
@@ -230,34 +226,51 @@ module.exports = [
|
|||||||
view : 'text',
|
view : 'text',
|
||||||
snippets : [
|
snippets : [
|
||||||
{
|
{
|
||||||
name : 'Class Table',
|
name : 'Class Tables',
|
||||||
|
icon : 'fas fa-table',
|
||||||
|
gen : ClassTableGen.full('classTable,frame,decoration,wide'),
|
||||||
|
subsnippets : [
|
||||||
|
{
|
||||||
|
name : 'Martial Class Table',
|
||||||
|
icon : 'fas fa-table',
|
||||||
|
gen : ClassTableGen.non('classTable,frame,decoration'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Martial Class Table (unframed)',
|
||||||
|
icon : 'fas fa-border-none',
|
||||||
|
gen : ClassTableGen.non('classTable'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Full Caster Class Table',
|
||||||
icon : 'fas fa-table',
|
icon : 'fas fa-table',
|
||||||
gen : ClassTableGen.full('classTable,frame,decoration,wide'),
|
gen : ClassTableGen.full('classTable,frame,decoration,wide'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name : 'Class Table (unframed)',
|
name : 'Full Caster Class Table (unframed)',
|
||||||
icon : 'fas fa-border-none',
|
icon : 'fas fa-border-none',
|
||||||
gen : ClassTableGen.full('classTable,wide'),
|
gen : ClassTableGen.full('classTable,wide'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name : '1/2 Class Table',
|
name : 'Half Caster Class Table',
|
||||||
icon : 'fas fa-list-alt',
|
icon : 'fas fa-list-alt',
|
||||||
gen : ClassTableGen.half('classTable,decoration,frame'),
|
gen : ClassTableGen.half('classTable,frame,decoration,wide'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name : '1/2 Class Table (unframed)',
|
name : 'Half Caster Class Table (unframed)',
|
||||||
icon : 'fas fa-border-none',
|
icon : 'fas fa-border-none',
|
||||||
gen : ClassTableGen.half('classTable'),
|
gen : ClassTableGen.half('classTable,wide'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name : '1/3 Class Table',
|
name : 'Third Caster Spell Table',
|
||||||
icon : 'fas fa-border-all',
|
icon : 'fas fa-border-all',
|
||||||
gen : ClassTableGen.third('classTable,frame'),
|
gen : ClassTableGen.third('classTable,frame,decoration'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name : '1/3 Class Table (unframed)',
|
name : 'Third Caster Spell Table (unframed)',
|
||||||
icon : 'fas fa-border-none',
|
icon : 'fas fa-border-none',
|
||||||
gen : ClassTableGen.third('classTable'),
|
gen : ClassTableGen.third('classTable'),
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name : 'Rune Table',
|
name : 'Rune Table',
|
||||||
|
|||||||
@@ -42,8 +42,8 @@ module.exports = function(classname){
|
|||||||
|
|
||||||
#### Equipment
|
#### Equipment
|
||||||
You start with the following equipment, in addition to the equipment granted by your background:
|
You start with the following equipment, in addition to the equipment granted by your background:
|
||||||
- *(a)* a martial weapon and a shield or *(b)* two martial weapons
|
- (*a*) a martial weapon and a shield or (*b*) two martial weapons
|
||||||
- *(a)* five javelins or *(b)* any simple melee weapon
|
- (*a*) five javelins or (*b*) any simple melee weapon
|
||||||
- ${_.sample(['10 lint fluffs', '1 button', 'a cherished lost sock'])}
|
- ${_.sample(['10 lint fluffs', '1 button', 'a cherished lost sock'])}
|
||||||
`;
|
`;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,132 +1,138 @@
|
|||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
|
const dedent = require('dedent-tabs').default;
|
||||||
|
|
||||||
const features = [
|
const features = [
|
||||||
'Astrological Botany',
|
'Astrological Botany', 'Biochemical Sorcery', 'Civil Divination',
|
||||||
'Biochemical Sorcery',
|
'Consecrated Augury', 'Demonic Anthropology', 'Divinatory Mineralogy',
|
||||||
'Civil Divination',
|
'Exo Interfacer', 'Genetic Banishing', 'Gunpowder Torturer',
|
||||||
'Consecrated Augury',
|
'Gunslinger Corruptor', 'Hermetic Geography', 'Immunological Cultist',
|
||||||
'Demonic Anthropology',
|
'Malefic Chemist', 'Mathematical Pharmacy', 'Nuclear Biochemistry',
|
||||||
'Divinatory Mineralogy',
|
'Orbital Gravedigger', 'Pharmaceutical Outlaw', 'Phased Linguist',
|
||||||
'Exo Interfacer',
|
'Plasma Gunslinger', 'Police Necromancer', 'Ritual Astronomy',
|
||||||
'Genetic Banishing',
|
'Sixgun Poisoner', 'Seismological Alchemy', 'Spiritual Illusionism',
|
||||||
'Gunpowder Torturer',
|
'Statistical Occultism', 'Spell Analyst', 'Torque Interfacer'
|
||||||
'Gunslinger Corruptor',
|
].map((f)=>_.padEnd(f, 21)); // Pad to equal length of 21 chars long
|
||||||
'Hermetic Geography',
|
|
||||||
'Immunological Cultist',
|
const classnames = [
|
||||||
'Malefic Chemist',
|
'Ackerman', 'Berserker-Typist', 'Concierge', 'Fishmonger',
|
||||||
'Mathematical Pharmacy',
|
'Haberdasher', 'Manicurist', 'Netrunner', 'Weirkeeper'
|
||||||
'Nuclear Biochemistry',
|
|
||||||
'Orbital Gravedigger',
|
|
||||||
'Pharmaceutical Outlaw',
|
|
||||||
'Phased Linguist',
|
|
||||||
'Plasma Gunslinger',
|
|
||||||
'Police Necromancer',
|
|
||||||
'Ritual Astronomy',
|
|
||||||
'Sixgun Poisoner',
|
|
||||||
'Seismological Alchemy',
|
|
||||||
'Spiritual Illusionism',
|
|
||||||
'Statistical Occultism',
|
|
||||||
'Spell Analyst',
|
|
||||||
'Torque Interfacer'
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const classnames = ['Ackerman', 'Berserker-Typist', 'Concierge', 'Fishmonger',
|
|
||||||
'Haberdasher', 'Manicurist', 'Netrunner', 'Weirkeeper'];
|
|
||||||
|
|
||||||
const levels = ['1st', '2nd', '3rd', '4th', '5th',
|
|
||||||
'6th', '7th', '8th', '9th', '10th',
|
|
||||||
'11th', '12th', '13th', '14th', '15th',
|
|
||||||
'16th', '17th', '18th', '19th', '20th'];
|
|
||||||
|
|
||||||
const profBonus = [2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6, 6, 6];
|
|
||||||
|
|
||||||
const maxes = [4, 3, 3, 3, 3, 2, 2, 1, 1];
|
|
||||||
|
|
||||||
const drawSlots = function(Slots, rows, padding){
|
|
||||||
let slots = Number(Slots);
|
|
||||||
return _.times(rows, function(i){
|
|
||||||
const max = maxes[i];
|
|
||||||
if(slots < 1) return _.pad('—', padding);
|
|
||||||
const res = _.min([max, slots]);
|
|
||||||
slots -= res;
|
|
||||||
return _.pad(res.toString(), padding);
|
|
||||||
}).join(' | ');
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
full : function(classes){
|
non : function(snippetClasses){
|
||||||
const classname = _.sample(classnames);
|
return dedent`
|
||||||
|
{{${snippetClasses}
|
||||||
|
##### The ${_.sample(classnames)}
|
||||||
let cantrips = 3;
|
| Level | Proficiency Bonus | Features | ${_.sample(features)} |
|
||||||
let spells = 1;
|
|:-----:|:-----------------:|:---------|:---------------------:|
|
||||||
let slots = 2;
|
| 1st | +2 | ${_.sample(features)} | 2 |
|
||||||
return `{{${classes}\n##### The ${classname}\n` +
|
| 2nd | +2 | ${_.sample(features)} | 2 |
|
||||||
`| Level | Proficiency | Features | Cantrips | Spells | --- Spell Slots Per Spell Level ---|||||||||\n`+
|
| 3rd | +2 | ${_.sample(features)} | 3 |
|
||||||
`| ^| Bonus ^| ^| Known ^| Known ^|1st |2nd |3rd |4th |5th |6th |7th |8th |9th |\n`+
|
| 4th | +2 | ${_.sample(features)} | 3 |
|
||||||
`|:-----:|:-----------:|:-------------|:--------:|:------:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|\n${
|
| 5th | +3 | ${_.sample(features)} | 3 |
|
||||||
_.map(levels, function(levelName, level){
|
| 6th | +3 | ${_.sample(features)} | 4 |
|
||||||
const res = [
|
| 7th | +3 | ${_.sample(features)} | 4 |
|
||||||
_.pad(levelName, 5),
|
| 8th | +3 | ${_.sample(features)} | 4 |
|
||||||
_.pad(`+${profBonus[level]}`, 2),
|
| 9th | +4 | ${_.sample(features)} | 4 |
|
||||||
_.padEnd(_.sample(features), 21),
|
| 10th | +4 | ${_.sample(features)} | 4 |
|
||||||
_.pad(cantrips.toString(), 8),
|
| 11th | +4 | ${_.sample(features)} | 4 |
|
||||||
_.pad(spells.toString(), 6),
|
| 12th | +4 | ${_.sample(features)} | 5 |
|
||||||
drawSlots(slots, 9, 2),
|
| 13th | +5 | ${_.sample(features)} | 5 |
|
||||||
].join(' | ');
|
| 14th | +5 | ${_.sample(features)} | 5 |
|
||||||
|
| 15th | +5 | ${_.sample(features)} | 5 |
|
||||||
cantrips += _.random(0, 1);
|
| 16th | +5 | ${_.sample(features)} | 5 |
|
||||||
spells += _.random(0, 1);
|
| 17th | +6 | ${_.sample(features)} | 6 |
|
||||||
slots += _.random(0, 2);
|
| 18th | +6 | ${_.sample(features)} | 6 |
|
||||||
|
| 19th | +6 | ${_.sample(features)} | 6 |
|
||||||
return `| ${res} |`;
|
| 20th | +6 | ${_.sample(features)} | unlimited |
|
||||||
}).join('\n')}\n}}\n\n`;
|
}}\n\n`;
|
||||||
},
|
},
|
||||||
|
|
||||||
half : function(classes){
|
full : function(snippetClasses){
|
||||||
const classname = _.sample(classnames);
|
return dedent`
|
||||||
|
{{${snippetClasses}
|
||||||
let featureScore = 1;
|
##### The ${_.sample(classnames)}
|
||||||
return `{{${classes}\n##### The ${classname}\n` +
|
| Level | Proficiency | Features | Cantrips | --- Spell Slots Per Spell Level ---|||||||||
|
||||||
`| Level | Proficiency Bonus | Features | ${_.pad(_.sample(features), 21)} |\n` +
|
| ^| Bonus ^| ^| Known ^|1st |2nd |3rd |4th |5th |6th |7th |8th |9th |
|
||||||
`|:-----:|:-----------------:|:---------|:---------------------:|\n${
|
|:-----:|:-----------:|:-------------|:--------:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|
|
||||||
_.map(levels, function(levelName, level){
|
| 1st | +2 | ${_.sample(features)} | 2 | 2 | — | — | — | — | — | — | — | — |
|
||||||
const res = [
|
| 2nd | +2 | ${_.sample(features)} | 2 | 3 | — | — | — | — | — | — | — | — |
|
||||||
_.pad(levelName, 5),
|
| 3rd | +2 | ${_.sample(features)} | 2 | 4 | 2 | — | — | — | — | — | — | — |
|
||||||
_.pad(`+${profBonus[level]}`, 2),
|
| 4th | +2 | ${_.sample(features)} | 3 | 4 | 3 | — | — | — | — | — | — | — |
|
||||||
_.padEnd(_.sample(features), 23),
|
| 5th | +3 | ${_.sample(features)} | 3 | 4 | 3 | 2 | — | — | — | — | — | — |
|
||||||
_.pad(`+${featureScore}`, 21),
|
| 6th | +3 | ${_.sample(features)} | 3 | 4 | 3 | 3 | — | — | — | — | — | — |
|
||||||
].join(' | ');
|
| 7th | +3 | ${_.sample(features)} | 3 | 4 | 3 | 3 | 1 | — | — | — | — | — |
|
||||||
|
| 8th | +3 | ${_.sample(features)} | 3 | 4 | 3 | 3 | 2 | — | — | — | — | — |
|
||||||
featureScore += _.random(0, 1);
|
| 9th | +4 | ${_.sample(features)} | 3 | 4 | 3 | 3 | 2 | 1 | — | — | — | — |
|
||||||
|
| 10th | +4 | ${_.sample(features)} | 3 | 4 | 3 | 3 | 2 | 1 | — | — | — | — |
|
||||||
return `| ${res} |`;
|
| 11th | +4 | ${_.sample(features)} | 4 | 4 | 3 | 3 | 2 | 1 | 1 | — | — | — |
|
||||||
}).join('\n')}\n}}\n\n`;
|
| 12th | +4 | ${_.sample(features)} | 4 | 4 | 3 | 3 | 2 | 1 | 1 | — | — | — |
|
||||||
|
| 13th | +5 | ${_.sample(features)} | 4 | 4 | 3 | 3 | 2 | 1 | 1 | 1 | — | — |
|
||||||
|
| 14th | +5 | ${_.sample(features)} | 4 | 4 | 3 | 3 | 2 | 1 | 1 | 1 | — | — |
|
||||||
|
| 15th | +5 | ${_.sample(features)} | 4 | 4 | 3 | 3 | 2 | 1 | 1 | 1 | 1 | — |
|
||||||
|
| 16th | +5 | ${_.sample(features)} | 4 | 4 | 3 | 3 | 2 | 1 | 1 | 1 | 1 | — |
|
||||||
|
| 17th | +6 | ${_.sample(features)} | 4 | 4 | 3 | 3 | 2 | 1 | 1 | 1 | 1 | 1 |
|
||||||
|
| 18th | +6 | ${_.sample(features)} | 4 | 4 | 3 | 3 | 3 | 1 | 1 | 1 | 1 | 1 |
|
||||||
|
| 19th | +6 | ${_.sample(features)} | 4 | 4 | 3 | 3 | 3 | 2 | 2 | 1 | 1 | 1 |
|
||||||
|
| 20th | +6 | ${_.sample(features)} | 4 | 4 | 3 | 3 | 3 | 2 | 2 | 2 | 1 | 1 |
|
||||||
|
}}\n\n`;
|
||||||
},
|
},
|
||||||
|
|
||||||
third : function(classes){
|
half : function(snippetClasses){
|
||||||
const classname = _.sample(classnames);
|
return dedent`
|
||||||
|
{{${snippetClasses}
|
||||||
|
##### The ${_.sample(classnames)}
|
||||||
|
| Level | Proficiency | Features | Spells |--- Spell Slots Per Spell Level ---|||||
|
||||||
|
| ^| Bonus ^| ^| Known ^| 1st | 2nd | 3rd | 4th | 5th |
|
||||||
|
|:-----:|:-----------:|:-------------|:------:|:-----:|:-----:|:-----:|:-----:|:-----:|
|
||||||
|
| 1st | +2 | ${_.sample(features)} | — | — | — | — | — | — |
|
||||||
|
| 2nd | +2 | ${_.sample(features)} | 2 | 2 | — | — | — | — |
|
||||||
|
| 3rd | +2 | ${_.sample(features)} | 3 | 3 | — | — | — | — |
|
||||||
|
| 4th | +2 | ${_.sample(features)} | 3 | 3 | — | — | — | — |
|
||||||
|
| 5th | +3 | ${_.sample(features)} | 4 | 4 | 2 | — | — | — |
|
||||||
|
| 6th | +3 | ${_.sample(features)} | 4 | 4 | 2 | — | — | — |
|
||||||
|
| 7th | +3 | ${_.sample(features)} | 5 | 4 | 3 | — | — | — |
|
||||||
|
| 8th | +3 | ${_.sample(features)} | 5 | 4 | 3 | — | — | — |
|
||||||
|
| 9th | +4 | ${_.sample(features)} | 6 | 4 | 3 | 2 | — | — |
|
||||||
|
| 10th | +4 | ${_.sample(features)} | 6 | 4 | 3 | 2 | — | — |
|
||||||
|
| 11th | +4 | ${_.sample(features)} | 7 | 4 | 3 | 3 | — | — |
|
||||||
|
| 12th | +4 | ${_.sample(features)} | 7 | 4 | 3 | 3 | — | — |
|
||||||
|
| 13th | +5 | ${_.sample(features)} | 8 | 4 | 3 | 3 | 1 | — |
|
||||||
|
| 14th | +5 | ${_.sample(features)} | 8 | 4 | 3 | 3 | 1 | — |
|
||||||
|
| 15th | +5 | ${_.sample(features)} | 9 | 4 | 3 | 3 | 2 | — |
|
||||||
|
| 16th | +5 | ${_.sample(features)} | 9 | 4 | 3 | 3 | 2 | — |
|
||||||
|
| 17th | +6 | ${_.sample(features)} | 10 | 4 | 3 | 3 | 3 | 1 |
|
||||||
|
| 18th | +6 | ${_.sample(features)} | 10 | 4 | 3 | 3 | 3 | 1 |
|
||||||
|
| 19th | +6 | ${_.sample(features)} | 11 | 4 | 3 | 3 | 3 | 2 |
|
||||||
|
| 20th | +6 | ${_.sample(features)} | 11 | 4 | 3 | 3 | 3 | 2 |
|
||||||
|
}}\n\n`;
|
||||||
|
},
|
||||||
|
|
||||||
let cantrips = 3;
|
third : function(snippetClasses){
|
||||||
let spells = 1;
|
return dedent`
|
||||||
let slots = 2;
|
{{${snippetClasses}
|
||||||
return `{{${classes}\n##### ${classname} Spellcasting\n` +
|
##### ${_.sample(classnames)} Spellcasting
|
||||||
`| Class | Cantrips | Spells |--- Spells Slots per Spell Level ---||||\n` +
|
| Level | Cantrips | Spells |--- Spells Slots per Spell Level ---||||
|
||||||
`| Level ^| Known ^| Known ^| 1st | 2nd | 3rd | 4th |\n` +
|
| ^| Known ^| Known ^| 1st | 2nd | 3rd | 4th |
|
||||||
`|:------:|:--------:|:-------:|:-------:|:-------:|:-------:|:-------:|\n${
|
|:-----:|:--------:|:------:|:-------:|:-------:|:-------:|:-------:|
|
||||||
_.map(levels, function(levelName, level){
|
| 3rd | 2 | 3 | 2 | — | — | — |
|
||||||
const res = [
|
| 4th | 2 | 4 | 3 | — | — | — |
|
||||||
_.pad(levelName, 6),
|
| 5th | 2 | 4 | 3 | — | — | — |
|
||||||
_.pad(cantrips.toString(), 8),
|
| 6th | 2 | 4 | 3 | — | — | — |
|
||||||
_.pad(spells.toString(), 7),
|
| 7th | 2 | 5 | 4 | 2 | — | — |
|
||||||
drawSlots(slots, 4, 7),
|
| 8th | 2 | 6 | 4 | 2 | — | — |
|
||||||
].join(' | ');
|
| 9th | 2 | 6 | 4 | 2 | — | — |
|
||||||
|
| 10th | 3 | 7 | 4 | 3 | — | — |
|
||||||
cantrips += _.random(0, 1);
|
| 11th | 3 | 8 | 4 | 3 | — | — |
|
||||||
spells += _.random(0, 1);
|
| 12th | 3 | 8 | 4 | 3 | — | — |
|
||||||
slots += _.random(0, 1);
|
| 13th | 3 | 9 | 4 | 3 | 2 | — |
|
||||||
|
| 14th | 3 | 10 | 4 | 3 | 2 | — |
|
||||||
return `| ${res} |`;
|
| 15th | 3 | 10 | 4 | 3 | 2 | — |
|
||||||
}).join('\n')}\n}}\n\n`;
|
| 16th | 3 | 11 | 4 | 3 | 3 | — |
|
||||||
|
| 17th | 3 | 11 | 4 | 3 | 3 | — |
|
||||||
|
| 18th | 3 | 11 | 4 | 3 | 3 | — |
|
||||||
|
| 19th | 3 | 12 | 4 | 3 | 3 | 1 |
|
||||||
|
| 20th | 3 | 13 | 4 | 3 | 3 | 1 |
|
||||||
|
}}\n\n`;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
51
themes/V3/5ePHB/snippets/quote.gen.js
Normal file
51
themes/V3/5ePHB/snippets/quote.gen.js
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
const _ = require("lodash");
|
||||||
|
|
||||||
|
const quotes = [
|
||||||
|
"The sword glinted in the dim light, its edges keen and deadly. As the adventurer reached for it, he couldn't help but feel a surge of excitement mixed with fear. This was no ordinary blade.",
|
||||||
|
"The dragon's roar shook the ground beneath their feet, and the brave knight stood tall, his sword at the ready. He knew that this would be the battle of his life, but he was determined to emerge victorious.",
|
||||||
|
"The wizard's laboratory was a sight to behold, filled with bubbling cauldrons, ancient tomes, and strange artifacts from distant lands. As the apprentice gazed around in wonder, she knew that she was about to embark on a journey unlike any other.",
|
||||||
|
"The tavern was packed with rowdy patrons, their voices raised in song and laughter. The bard took center stage, strumming his lute and launching into a tale of adventure and heroism that had the crowd hanging on his every word.",
|
||||||
|
"The thief crept through the shadows, his eyes scanning the room for any sign of danger. He knew that one false move could mean the difference between success and failure, and he was determined to come out on top.",
|
||||||
|
"The elf queen stood atop her castle walls, surveying the kingdom below with a mix of pride and sadness. She knew that the coming war would be brutal, but she was determined to protect her people at all costs.",
|
||||||
|
"The necromancer's tower loomed in the distance, its dark spires piercing the sky. As the adventurers approached, they could feel the chill of death emanating from within",
|
||||||
|
"The ranger moved through the forest like a shadow, his senses attuned to every sound and movement around him. He knew that danger lurked behind every tree, but he was ready for whatever came his way.",
|
||||||
|
"The paladin knelt before the altar, his hands clasped in prayer. He knew that his faith would be tested in the days ahead, but he was ready to face whatever trials lay in store for him.",
|
||||||
|
"The druid communed with the spirits of nature, his mind merging with the trees, the animals, and the very earth itself. He knew that his power came with a great responsibility, and he was determined to use it for the greater good.",
|
||||||
|
];
|
||||||
|
|
||||||
|
const authors = [
|
||||||
|
"Unknown",
|
||||||
|
"James Wyatt",
|
||||||
|
"Eolande Blackwood",
|
||||||
|
"Ragnar Ironheart",
|
||||||
|
"Lyra Nightshade",
|
||||||
|
"Valtorius Darkstar",
|
||||||
|
"Isadora Fireheart",
|
||||||
|
"Theron Shadowbane",
|
||||||
|
"Lirien Starweaver",
|
||||||
|
"Drogathar Bonecrusher",
|
||||||
|
"Kaelen Frostblade",
|
||||||
|
];
|
||||||
|
|
||||||
|
const books = [
|
||||||
|
"The Blade of Destiny",
|
||||||
|
"Dragonfire and Steel",
|
||||||
|
"The Bard's Tale",
|
||||||
|
"Darkness Rising",
|
||||||
|
"The Sacred Quest",
|
||||||
|
"Shadows in the Forest",
|
||||||
|
"The Starweaver Chronicles",
|
||||||
|
"Beneath the Bones",
|
||||||
|
"Moonlit Magic",
|
||||||
|
"Frost and Fury",
|
||||||
|
|
||||||
|
];
|
||||||
|
module.exports = () => {
|
||||||
|
return `
|
||||||
|
{{quote
|
||||||
|
${_.sample(quotes)}
|
||||||
|
|
||||||
|
{{attribution ${_.sample(authors)}, *${_.sample(books)}*}}
|
||||||
|
}}
|
||||||
|
\n`;
|
||||||
|
};
|
||||||
@@ -29,6 +29,7 @@ const getTOC = (pages)=>{
|
|||||||
|
|
||||||
const res = [];
|
const res = [];
|
||||||
_.each(pages, (page, pageNum)=>{
|
_.each(pages, (page, pageNum)=>{
|
||||||
|
if(!page.includes("{{frontCover}}") && !page.includes("{{insideCover}}") && !page.includes("{{partCover}}") && !page.includes("{{backCover}}")) {
|
||||||
const lines = page.split('\n');
|
const lines = page.split('\n');
|
||||||
_.each(lines, (line)=>{
|
_.each(lines, (line)=>{
|
||||||
if(_.startsWith(line, '# ')){
|
if(_.startsWith(line, '# ')){
|
||||||
@@ -44,12 +45,13 @@ const getTOC = (pages)=>{
|
|||||||
add3(title, pageNum);
|
add3(title, pageNum);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = function(brew){
|
module.exports = function(props){
|
||||||
const pages = brew.text.split('\\page');
|
const pages = props.brew.text.split('\\page');
|
||||||
const TOC = getTOC(pages);
|
const TOC = getTOC(pages);
|
||||||
const markdown = _.reduce(TOC, (r, g1, idx1)=>{
|
const markdown = _.reduce(TOC, (r, g1, idx1)=>{
|
||||||
if(g1.title !== null) {
|
if(g1.title !== null) {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
const WatercolorGen = require('./snippets/watercolor.gen.js');
|
const WatercolorGen = require('./snippets/watercolor.gen.js');
|
||||||
const ImageMaskGen = require('./snippets/imageMask.gen.js');
|
const ImageMaskGen = require('./snippets/imageMask.gen.js');
|
||||||
|
const FooterGen = require('./snippets/footer.gen.js');
|
||||||
const dedent = require('dedent-tabs').default;
|
const dedent = require('dedent-tabs').default;
|
||||||
|
|
||||||
module.exports = [
|
module.exports = [
|
||||||
@@ -21,6 +22,53 @@ module.exports = [
|
|||||||
icon : 'fas fa-file-alt',
|
icon : 'fas fa-file-alt',
|
||||||
gen : '\n\\page\n'
|
gen : '\n\\page\n'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name : 'Page Number',
|
||||||
|
icon : 'fas fa-bookmark',
|
||||||
|
gen : '{{pageNumber 1}}\n'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Auto-incrementing Page Number',
|
||||||
|
icon : 'fas fa-sort-numeric-down',
|
||||||
|
gen : '{{pageNumber,auto}}\n'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Footer',
|
||||||
|
icon : 'fas fa-shoe-prints',
|
||||||
|
gen : FooterGen.createFooterFunc(),
|
||||||
|
subsnippets : [
|
||||||
|
{
|
||||||
|
name : 'Footer from H1',
|
||||||
|
icon : 'fas fa-dice-one',
|
||||||
|
gen : FooterGen.createFooterFunc(1)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Footer from H2',
|
||||||
|
icon : 'fas fa-dice-two',
|
||||||
|
gen : FooterGen.createFooterFunc(2)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Footer from H3',
|
||||||
|
icon : 'fas fa-dice-three',
|
||||||
|
gen : FooterGen.createFooterFunc(3)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Footer from H4',
|
||||||
|
icon : 'fas fa-dice-four',
|
||||||
|
gen : FooterGen.createFooterFunc(4)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Footer from H5',
|
||||||
|
icon : 'fas fa-dice-five',
|
||||||
|
gen : FooterGen.createFooterFunc(5)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Footer from H6',
|
||||||
|
icon : 'fas fa-dice-six',
|
||||||
|
gen : FooterGen.createFooterFunc(6)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name : 'Vertical Spacing',
|
name : 'Vertical Spacing',
|
||||||
icon : 'fas fa-arrows-alt-v',
|
icon : 'fas fa-arrows-alt-v',
|
||||||
@@ -63,6 +111,21 @@ module.exports = [
|
|||||||
icon : 'fas fa-code',
|
icon : 'fas fa-code',
|
||||||
gen : '<!-- This is a comment that will not be rendered into your brew. Hotkey (Ctrl/Cmd + /). -->'
|
gen : '<!-- This is a comment that will not be rendered into your brew. Hotkey (Ctrl/Cmd + /). -->'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name : 'Homebrewery Credit',
|
||||||
|
icon : 'fas fa-dice-d20',
|
||||||
|
gen : function(){
|
||||||
|
return dedent`
|
||||||
|
{{homebreweryCredits
|
||||||
|
Made With
|
||||||
|
|
||||||
|
{{homebreweryIcon}}
|
||||||
|
|
||||||
|
The Homebrewery
|
||||||
|
[Homebrewery.Naturalcrit.com](https://homebrewery.naturalcrit.com)
|
||||||
|
}}\n\n`;
|
||||||
|
},
|
||||||
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -241,6 +304,99 @@ module.exports = [
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
/**************** FONTS *************/
|
||||||
|
{
|
||||||
|
groupName : 'Fonts',
|
||||||
|
icon : 'fas fa-keyboard',
|
||||||
|
view : 'text',
|
||||||
|
snippets : [
|
||||||
|
{
|
||||||
|
name : 'Open Sans',
|
||||||
|
icon : 'font OpenSans',
|
||||||
|
gen : dedent`{{font-family:OpenSans Dummy Text}}`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Code Bold',
|
||||||
|
icon : 'font CodeBold',
|
||||||
|
gen : dedent`{{font-family:CodeBold Dummy Text}}`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Code Light',
|
||||||
|
icon : 'font CodeLight',
|
||||||
|
gen : dedent`{{font-family:CodeLight Dummy Text}}`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Scaly Sans Remake',
|
||||||
|
icon : 'font ScalySansRemake',
|
||||||
|
gen : dedent`{{font-family:ScalySansRemake Dummy Text}}`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Book Insanity Remake',
|
||||||
|
icon : 'font BookInsanityRemake',
|
||||||
|
gen : dedent`{{font-family:BookInsanityRemake Dummy Text}}`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Mr Eaves Remake',
|
||||||
|
icon : 'font MrEavesRemake',
|
||||||
|
gen : dedent`{{font-family:MrEavesRemake Dummy Text}}`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Solbera Imitation Remake',
|
||||||
|
icon: 'font SolberaImitationRemake',
|
||||||
|
gen: dedent`{{font-family:SolberaImitationRemake Dummy Text}}`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Scaly Sans Small Caps Remake',
|
||||||
|
icon: 'font ScalySansSmallCapsRemake',
|
||||||
|
gen: dedent`{{font-family:ScalySansSmallCapsRemake Dummy Text}}`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Walter Turncoat',
|
||||||
|
icon: 'font WalterTurncoat',
|
||||||
|
gen: dedent`{{font-family:WalterTurncoat Dummy Text}}`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Lato',
|
||||||
|
icon: 'font Lato',
|
||||||
|
gen: dedent`{{font-family:Lato Dummy Text}}`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Courier',
|
||||||
|
icon: 'font Courier',
|
||||||
|
gen: dedent`{{font-family:Courier Dummy Text}}`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Nodesto Caps Condensed',
|
||||||
|
icon: 'font NodestoCapsCondensed',
|
||||||
|
gen: dedent`{{font-family:NodestoCapsCondensed Dummy Text}}`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Overpass',
|
||||||
|
icon: 'font Overpass',
|
||||||
|
gen: dedent`{{font-family:Overpass Dummy Text}}`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Davek',
|
||||||
|
icon: 'font Davek',
|
||||||
|
gen: dedent`{{font-family:Davek Dummy Text}}`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Iokharic',
|
||||||
|
icon: 'font Iokharic',
|
||||||
|
gen: dedent`{{font-family:Iokharic Dummy Text}}`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Rellanic',
|
||||||
|
icon: 'font Rellanic',
|
||||||
|
gen: dedent`{{font-family:Rellanic Dummy Text}}`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Times New Roman',
|
||||||
|
icon: 'font TimesNewRoman',
|
||||||
|
gen: dedent`{{font-family:"Times New Roman" Dummy Text}}`
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
/**************** PAGE *************/
|
/**************** PAGE *************/
|
||||||
|
|
||||||
|
|||||||
17
themes/V3/Blank/snippets/footer.gen.js
Normal file
17
themes/V3/Blank/snippets/footer.gen.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
const Markdown = require('../../../../shared/naturalcrit/markdown.js');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
createFooterFunc : function(headerSize=1){
|
||||||
|
return (props)=>{
|
||||||
|
const cursorPos = props.cursorPos;
|
||||||
|
|
||||||
|
const markdownText = props.brew.text.split('\n').slice(0, cursorPos.line).join('\n');
|
||||||
|
const markdownTokens = Markdown.marked.lexer(markdownText);
|
||||||
|
const headerToken = markdownTokens.findLast((lexerToken)=>{ return lexerToken.type === 'heading' && lexerToken.depth === headerSize; });
|
||||||
|
const headerText = headerToken?.tokens.map((token)=>{ return token.text; }).join('');
|
||||||
|
const outputText = headerText || 'PART 1 | SECTION NAME';
|
||||||
|
|
||||||
|
return `\n{{footnote ${outputText}}}\n`;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -7,13 +7,9 @@
|
|||||||
--HB_Color_WatercolorStain : #000000; // Black
|
--HB_Color_WatercolorStain : #000000; // Black
|
||||||
}
|
}
|
||||||
|
|
||||||
@page { margin: 0; }
|
@page { margin : 0; }
|
||||||
body {
|
body { counter-reset : phb-page-numbers; }
|
||||||
counter-reset : phb-page-numbers;
|
* { -webkit-print-color-adjust : exact; }
|
||||||
}
|
|
||||||
*{
|
|
||||||
-webkit-print-color-adjust : exact;
|
|
||||||
}
|
|
||||||
|
|
||||||
//*****************************
|
//*****************************
|
||||||
// * MUSTACHE DIVS/SPANS
|
// * MUSTACHE DIVS/SPANS
|
||||||
@@ -23,9 +19,7 @@ body {
|
|||||||
break-inside : avoid;
|
break-inside : avoid;
|
||||||
display : inline-block;
|
display : inline-block;
|
||||||
width : 100%;
|
width : 100%;
|
||||||
img {
|
img { z-index : 0; }
|
||||||
z-index : 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.inline-block {
|
.inline-block {
|
||||||
display : inline-block;
|
display : inline-block;
|
||||||
@@ -33,98 +27,81 @@ body {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.useColumns(@multiplier : 1, @fillMode: balance){
|
.useColumns(@multiplier : 1, @fillMode: auto) {
|
||||||
column-fill : @fillMode;
|
column-fill : @fillMode;
|
||||||
column-count : 2;
|
column-count : 2;
|
||||||
}
|
}
|
||||||
.columnWrapper{
|
.columnWrapper {
|
||||||
|
column-gap : inherit;
|
||||||
max-height : 100%;
|
max-height : 100%;
|
||||||
column-span : all;
|
column-span : all;
|
||||||
columns : inherit;
|
columns : inherit;
|
||||||
column-gap : inherit;
|
column-fill : inherit;
|
||||||
}
|
}
|
||||||
.page{
|
.page {
|
||||||
.useColumns();
|
.useColumns();
|
||||||
height : 279.4mm;
|
|
||||||
width : 215.9mm;
|
|
||||||
padding : 1.4cm 1.9cm 1.7cm;
|
|
||||||
counter-increment : phb-page-numbers;
|
|
||||||
background-color : var(--HB_Color_Background);
|
|
||||||
position : relative;
|
position : relative;
|
||||||
z-index : 15;
|
z-index : 15;
|
||||||
box-sizing : border-box;
|
box-sizing : border-box;
|
||||||
|
width : 215.9mm;
|
||||||
|
height : 279.4mm;
|
||||||
|
padding : 1.4cm 1.9cm 1.7cm;
|
||||||
overflow : hidden;
|
overflow : hidden;
|
||||||
|
counter-increment : phb-page-numbers;
|
||||||
|
background-color : var(--HB_Color_Background);
|
||||||
text-rendering : optimizeLegibility;
|
text-rendering : optimizeLegibility;
|
||||||
page-break-before : always;
|
|
||||||
page-break-after : always;
|
|
||||||
contain : size;
|
contain : size;
|
||||||
}
|
}
|
||||||
//*****************************
|
//*****************************
|
||||||
// * BASE
|
// * BASE
|
||||||
// *****************************/
|
// *****************************/
|
||||||
.page{
|
.page {
|
||||||
p{
|
p {
|
||||||
overflow-wrap : break-word;
|
|
||||||
display : block;
|
display : block;
|
||||||
|
overflow-wrap : break-word;
|
||||||
}
|
}
|
||||||
strong{
|
strong { font-weight : bold; }
|
||||||
font-weight : bold;
|
em { font-style : italic; }
|
||||||
}
|
sup {
|
||||||
em{
|
font-size : smaller;
|
||||||
font-style : italic;
|
line-height : 0;
|
||||||
}
|
|
||||||
sup{
|
|
||||||
vertical-align : super;
|
vertical-align : super;
|
||||||
font-size : smaller;
|
|
||||||
line-height : 0;
|
|
||||||
}
|
}
|
||||||
sub{
|
sub {
|
||||||
vertical-align : sub;
|
|
||||||
font-size : smaller;
|
font-size : smaller;
|
||||||
line-height : 0;
|
line-height : 0;
|
||||||
|
vertical-align : sub;
|
||||||
}
|
}
|
||||||
ul {
|
ul {
|
||||||
|
padding-left : 1.4em;
|
||||||
list-style-position : outside; //Needed for multiline list items
|
list-style-position : outside; //Needed for multiline list items
|
||||||
list-style-type : disc;
|
list-style-type : disc;
|
||||||
padding-left : 1.4em;
|
|
||||||
}
|
}
|
||||||
ol {
|
ol {
|
||||||
|
padding-left : 1.4em;
|
||||||
list-style-position : outside;
|
list-style-position : outside;
|
||||||
list-style-type : decimal;
|
list-style-type : decimal;
|
||||||
padding-left : 1.4em;
|
|
||||||
}
|
|
||||||
img{
|
|
||||||
z-index : -1;
|
|
||||||
}
|
}
|
||||||
|
img { z-index : -1; }
|
||||||
|
|
||||||
//*****************************
|
//*****************************
|
||||||
// * HEADERS
|
// * HEADERS
|
||||||
// *****************************/
|
// *****************************/
|
||||||
h1,h2,h3,h4,h5,h6{
|
h1,h2,h3,h4,h5,h6 {
|
||||||
font-weight : bold;
|
font-weight : bold;
|
||||||
line-height : 1.2em;
|
line-height : 1.2em;
|
||||||
}
|
}
|
||||||
h1{
|
h1 { font-size : 2em; }
|
||||||
font-size : 2em;
|
h2 { font-size : 1.5em; }
|
||||||
}
|
h3 { font-size : 1.17em; }
|
||||||
h2{
|
h4 { font-size : 1em; }
|
||||||
font-size : 1.5em;
|
h5 { font-size : 0.83em; }
|
||||||
}
|
|
||||||
h3{
|
|
||||||
font-size : 1.17em;
|
|
||||||
}
|
|
||||||
h4{
|
|
||||||
font-size : 1em;
|
|
||||||
}
|
|
||||||
h5{
|
|
||||||
font-size : 0.83em;
|
|
||||||
}
|
|
||||||
//*****************************
|
//*****************************
|
||||||
// * TABLE
|
// * TABLE
|
||||||
// *****************************/
|
// *****************************/
|
||||||
table{
|
table {
|
||||||
width : 100%;
|
width : 100%;
|
||||||
thead{
|
thead {
|
||||||
display : table-row-group;
|
display : table-row-group;
|
||||||
font-weight : bold;
|
font-weight : bold;
|
||||||
}
|
}
|
||||||
@@ -136,42 +113,40 @@ body {
|
|||||||
//************************************
|
//************************************
|
||||||
// * CODE BLOCKS
|
// * CODE BLOCKS
|
||||||
// ************************************/
|
// ************************************/
|
||||||
code{
|
code {
|
||||||
font-family : "Courier New", Courier, monospace;
|
font-family : 'Courier New', "Courier", monospace;
|
||||||
white-space : pre-wrap;
|
|
||||||
overflow-wrap : break-word;
|
overflow-wrap : break-word;
|
||||||
|
white-space : pre-wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
pre code{
|
pre code {
|
||||||
width : 100%;
|
|
||||||
display : inline-block;
|
display : inline-block;
|
||||||
|
width : 100%;
|
||||||
}
|
}
|
||||||
//*****************************
|
//*****************************
|
||||||
// * EXTRAS
|
// * EXTRAS
|
||||||
// *****************************/
|
// *****************************/
|
||||||
.columnSplit {
|
.columnSplit {
|
||||||
|
margin-top : 0;
|
||||||
visibility : hidden;
|
visibility : hidden;
|
||||||
-webkit-column-break-after : always;
|
-webkit-column-break-after : always;
|
||||||
break-after : always;
|
break-after : always;
|
||||||
-moz-column-break-after : always;
|
-moz-column-break-after : always;
|
||||||
margin-top : 0;
|
& + * { margin-top : 0; }
|
||||||
& + * {
|
|
||||||
margin-top : 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
//Avoid breaking up
|
//Avoid breaking up
|
||||||
blockquote,table{
|
blockquote,table {
|
||||||
z-index : 15;
|
z-index : 15;
|
||||||
-webkit-column-break-inside : avoid;
|
-webkit-column-break-inside : avoid;
|
||||||
page-break-inside : avoid;
|
page-break-inside : avoid;
|
||||||
break-inside : avoid;
|
break-inside : avoid;
|
||||||
}
|
}
|
||||||
// Nested lists
|
// Nested lists
|
||||||
ul ul,ol ol,ul ol,ol ul{
|
ul ul,ol ol,ul ol,ol ul {
|
||||||
margin-bottom : 0px;
|
margin-bottom : 0px;
|
||||||
margin-left : 1.5em;
|
margin-left : 1.5em;
|
||||||
}
|
}
|
||||||
li{
|
li {
|
||||||
-webkit-column-break-inside : avoid;
|
-webkit-column-break-inside : avoid;
|
||||||
page-break-inside : avoid;
|
page-break-inside : avoid;
|
||||||
break-inside : avoid;
|
break-inside : avoid;
|
||||||
@@ -179,41 +154,39 @@ body {
|
|||||||
|
|
||||||
/* Watermark */
|
/* Watermark */
|
||||||
.watermark {
|
.watermark {
|
||||||
|
position : absolute;
|
||||||
|
top : 0;
|
||||||
|
left : 0;
|
||||||
|
z-index : 500;
|
||||||
display : grid !important;
|
display : grid !important;
|
||||||
place-items : center;
|
place-items : center;
|
||||||
justify-content : center;
|
justify-content : center;
|
||||||
position : absolute;
|
|
||||||
margin : 0;
|
|
||||||
top : 0;
|
|
||||||
left : 0;
|
|
||||||
width : 100%;
|
width : 100%;
|
||||||
height : 100%;
|
height : 100%;
|
||||||
|
margin : 0;
|
||||||
font-size : 120px;
|
font-size : 120px;
|
||||||
text-transform : uppercase;
|
text-transform : uppercase;
|
||||||
mix-blend-mode : overlay;
|
mix-blend-mode : overlay;
|
||||||
opacity : 30%;
|
opacity : 30%;
|
||||||
transform : rotate(-45deg);
|
transform : rotate(-45deg);
|
||||||
z-index : 500;
|
p { margin-bottom : none; }
|
||||||
p {
|
|
||||||
margin-bottom : none;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Watercolor */
|
/* Watercolor */
|
||||||
[class*="watercolor"] {
|
[class*='watercolor'] {
|
||||||
position : absolute;
|
position : absolute;
|
||||||
|
z-index : -2;
|
||||||
width : 2000px; /* dimensions need to be real big so the user can set */
|
width : 2000px; /* dimensions need to be real big so the user can set */
|
||||||
height : 2000px; /* height or width and the image will maintain aspect ratio */
|
height : 2000px; /* height or width and the image will maintain aspect ratio */
|
||||||
|
background-color : var(--HB_Color_WatercolorStain); /* default color */
|
||||||
|
background-size : cover;
|
||||||
-webkit-mask-image : var(--wc);
|
-webkit-mask-image : var(--wc);
|
||||||
-webkit-mask-size : contain;
|
-webkit-mask-size : contain;
|
||||||
-webkit-mask-repeat : no-repeat;
|
-webkit-mask-repeat : no-repeat;
|
||||||
mask-image : var(--wc);
|
mask-image : var(--wc);
|
||||||
mask-size : contain;
|
mask-size : contain;
|
||||||
mask-repeat : no-repeat;
|
mask-repeat : no-repeat;
|
||||||
background-size : cover;
|
--wc : @watercolor1; /* default image */
|
||||||
background-color : var(--HB_Color_WatercolorStain); /*default color*/
|
|
||||||
--wc : @watercolor1; /*default image*/
|
|
||||||
z-index : -2;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.watercolor1 { --wc : @watercolor1; }
|
.watercolor1 { --wc : @watercolor1; }
|
||||||
@@ -231,17 +204,16 @@ body {
|
|||||||
|
|
||||||
/* Image Masks */
|
/* Image Masks */
|
||||||
|
|
||||||
[class*="imageMask"] {
|
[class*='imageMask'] {
|
||||||
position : absolute;
|
position : absolute;
|
||||||
height : 200%;
|
|
||||||
width : 200%;
|
|
||||||
left : 50%;
|
|
||||||
bottom : 50%;
|
bottom : 50%;
|
||||||
--rotation : 0;
|
left : 50%;
|
||||||
--revealer : none;
|
z-index : -1;
|
||||||
--checkerboard : none;
|
width : 200%;
|
||||||
--scaleX : 1;
|
height : 200%;
|
||||||
--scaleY : 1;
|
background-image : var(--checkerboard);
|
||||||
|
background-size : 20px;
|
||||||
|
transform : translateY(50%) translateX(-50%) rotate(calc(1deg * var(--rotation))) scaleX(var(--scaleX)) scaleY(var(--scaleY));
|
||||||
-webkit-mask-image : var(--wc), var(--revealer);
|
-webkit-mask-image : var(--wc), var(--revealer);
|
||||||
-webkit-mask-repeat : repeat-x;
|
-webkit-mask-repeat : repeat-x;
|
||||||
-webkit-mask-size : 50%; //Scale only X to fit page width, leave height at aspect ratio, designed to hang off the edge
|
-webkit-mask-size : 50%; //Scale only X to fit page width, leave height at aspect ratio, designed to hang off the edge
|
||||||
@@ -250,61 +222,63 @@ body {
|
|||||||
mask-repeat : repeat-x;
|
mask-repeat : repeat-x;
|
||||||
mask-size : 50%;
|
mask-size : 50%;
|
||||||
mask-position : 50% calc(50% - var(--offset));
|
mask-position : 50% calc(50% - var(--offset));
|
||||||
background-image : var(--checkerboard);
|
--rotation : 0;
|
||||||
background-size : 20px;
|
--revealer : none;
|
||||||
z-index : -1;
|
--checkerboard : none;
|
||||||
transform : translateY(50%) translateX(-50%) rotate(calc(1deg * var(--rotation))) scaleX(var(--scaleX)) scaleY(var(--scaleY));
|
--scaleX : 1;
|
||||||
|
--scaleY : 1;
|
||||||
& > p:has(img) {
|
& > p:has(img) {
|
||||||
position : absolute;
|
position : absolute;
|
||||||
width : 50%;
|
|
||||||
height : 50%;
|
|
||||||
bottom : 50%;
|
bottom : 50%;
|
||||||
left : 50%;
|
left : 50%;
|
||||||
|
width : 50%;
|
||||||
|
height : 50%;
|
||||||
transform : translateX(-50%) translateY(50%) rotate(calc(-1deg * var(--rotation))) scaleX(calc(1 / var(--scaleX))) scaleY(calc(1 / var(--scaleY)));
|
transform : translateX(-50%) translateY(50%) rotate(calc(-1deg * var(--rotation))) scaleX(calc(1 / var(--scaleX))) scaleY(calc(1 / var(--scaleY)));
|
||||||
}
|
}
|
||||||
& img {
|
& img {
|
||||||
position : absolute;
|
position : absolute;
|
||||||
display : block;
|
|
||||||
bottom : 0;
|
bottom : 0;
|
||||||
|
display : block;
|
||||||
}
|
}
|
||||||
&.bottom {
|
&.bottom {
|
||||||
--rotation : 0;
|
--rotation : 0;
|
||||||
& img {bottom: 0;}
|
& img {bottom : 0;}
|
||||||
}
|
}
|
||||||
&.top {
|
&.top {
|
||||||
--rotation : 180;
|
--rotation : 180;
|
||||||
& img {top: 0;}
|
& img {top : 0;}
|
||||||
}
|
}
|
||||||
&.left {
|
&.left {
|
||||||
--rotation : 90;
|
--rotation : 90;
|
||||||
& img {left: 0;}
|
& img {left : 0;}
|
||||||
}
|
}
|
||||||
&.right {
|
&.right {
|
||||||
--rotation : -90;
|
--rotation : -90;
|
||||||
& img {right: 0;}
|
& img {right : 0;}
|
||||||
}
|
}
|
||||||
&.revealImage {
|
&.revealImage {
|
||||||
--revealer : linear-gradient(0deg, rgba(0,0,0,.2) 0%, rgba(0,0,0,0.2));
|
--revealer : linear-gradient(0deg, rgba(0,0,0,0.2) 0%, rgba(0,0,0,0.2));
|
||||||
--checkerboard : url(/assets/waterColorMasks/missingImage.png); //shows any masked regions not filled by image
|
--checkerboard : url("/assets/waterColorMasks/missingImage.png"); //shows any masked regions not filled by image
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.imageMaskEdge {
|
.imageMaskEdge {
|
||||||
&1 { --wc : url(/assets/waterColorMasks/edge/0001.webp); }
|
&1 { --wc : url("/assets/waterColorMasks/edge/0001.webp"); }
|
||||||
&2 { --wc : url(/assets/waterColorMasks/edge/0002.webp); }
|
&2 { --wc : url("/assets/waterColorMasks/edge/0002.webp"); }
|
||||||
&3 { --wc : url(/assets/waterColorMasks/edge/0003.webp); }
|
&3 { --wc : url("/assets/waterColorMasks/edge/0003.webp"); }
|
||||||
&4 { --wc : url(/assets/waterColorMasks/edge/0004.webp); }
|
&4 { --wc : url("/assets/waterColorMasks/edge/0004.webp"); }
|
||||||
&5 { --wc : url(/assets/waterColorMasks/edge/0005.webp); }
|
&5 { --wc : url("/assets/waterColorMasks/edge/0005.webp"); }
|
||||||
&6 { --wc : url(/assets/waterColorMasks/edge/0006.webp); }
|
&6 { --wc : url("/assets/waterColorMasks/edge/0006.webp"); }
|
||||||
&7 { --wc : url(/assets/waterColorMasks/edge/0007.webp); }
|
&7 { --wc : url("/assets/waterColorMasks/edge/0007.webp"); }
|
||||||
&8 { --wc : url(/assets/waterColorMasks/edge/0008.webp); }
|
&8 { --wc : url("/assets/waterColorMasks/edge/0008.webp"); }
|
||||||
}
|
}
|
||||||
|
|
||||||
[class*="imageMaskCenter"] {
|
[class*='imageMaskCenter'] {
|
||||||
|
bottom : calc(var(--offsetY));
|
||||||
|
left : calc(var(--offsetX));
|
||||||
width : 100%;
|
width : 100%;
|
||||||
height : 100%;
|
height : 100%;
|
||||||
left : calc(var(--offsetX));
|
transform : rotate(calc(1deg * var(--rotation))) scaleX(var(--scaleX)) scaleY(var(--scaleY));
|
||||||
bottom : calc(var(--offsetY));
|
|
||||||
-webkit-mask-image : var(--wc), var(--revealer);
|
-webkit-mask-image : var(--wc), var(--revealer);
|
||||||
-webkit-mask-repeat : no-repeat;
|
-webkit-mask-repeat : no-repeat;
|
||||||
-webkit-mask-size : 100% 100%; //Scale both dimensions to fit page size
|
-webkit-mask-size : 100% 100%; //Scale both dimensions to fit page size
|
||||||
@@ -313,14 +287,13 @@ body {
|
|||||||
mask-repeat : no-repeat;
|
mask-repeat : no-repeat;
|
||||||
mask-size : 100% 100%; //Scale both dimensions to fit page size
|
mask-size : 100% 100%; //Scale both dimensions to fit page size
|
||||||
mask-position : 50% 50%;
|
mask-position : 50% 50%;
|
||||||
transform : rotate(calc(1deg * var(--rotation))) scaleX(var(--scaleX)) scaleY(var(--scaleY));
|
|
||||||
|
|
||||||
& > p:has(img) {
|
& > p:has(img) {
|
||||||
position : absolute;
|
position : absolute;
|
||||||
width : 100%;
|
|
||||||
height : 100%;
|
|
||||||
bottom : 0;
|
bottom : 0;
|
||||||
left : 0;
|
left : 0;
|
||||||
|
width : 100%;
|
||||||
|
height : 100%;
|
||||||
transform : unset;
|
transform : unset;
|
||||||
transform : scaleX(calc(1 / var(--scaleX))) scaleY(calc(1 / var(--scaleY)))
|
transform : scaleX(calc(1 / var(--scaleX))) scaleY(calc(1 / var(--scaleY)))
|
||||||
rotate(calc(-1deg * var(--rotation)))
|
rotate(calc(-1deg * var(--rotation)))
|
||||||
@@ -330,31 +303,32 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.imageMaskCenter {
|
.imageMaskCenter {
|
||||||
&1 { --wc : url(/assets/waterColorMasks/center/0001.webp); }
|
&1 { --wc : url("/assets/waterColorMasks/center/0001.webp"); }
|
||||||
&2 { --wc : url(/assets/waterColorMasks/center/0002.webp); }
|
&2 { --wc : url("/assets/waterColorMasks/center/0002.webp"); }
|
||||||
&3 { --wc : url(/assets/waterColorMasks/center/0003.webp); }
|
&3 { --wc : url("/assets/waterColorMasks/center/0003.webp"); }
|
||||||
&4 { --wc : url(/assets/waterColorMasks/center/0004.webp); }
|
&4 { --wc : url("/assets/waterColorMasks/center/0004.webp"); }
|
||||||
&5 { --wc : url(/assets/waterColorMasks/center/0005.webp); }
|
&5 { --wc : url("/assets/waterColorMasks/center/0005.webp"); }
|
||||||
&6 { --wc : url(/assets/waterColorMasks/center/0006.webp); }
|
&6 { --wc : url("/assets/waterColorMasks/center/0006.webp"); }
|
||||||
&7 { --wc : url(/assets/waterColorMasks/center/0007.webp); }
|
&7 { --wc : url("/assets/waterColorMasks/center/0007.webp"); }
|
||||||
&8 { --wc : url(/assets/waterColorMasks/center/0008.webp); }
|
&8 { --wc : url("/assets/waterColorMasks/center/0008.webp"); }
|
||||||
&9 { --wc : url(/assets/waterColorMasks/center/0009.webp); }
|
&9 { --wc : url("/assets/waterColorMasks/center/0009.webp"); }
|
||||||
&10 { --wc : url(/assets/waterColorMasks/center/0010.webp); }
|
&10 { --wc : url("/assets/waterColorMasks/center/0010.webp"); }
|
||||||
&11 { --wc : url(/assets/waterColorMasks/center/0011.webp); }
|
&11 { --wc : url("/assets/waterColorMasks/center/0011.webp"); }
|
||||||
&12 { --wc : url(/assets/waterColorMasks/center/0012.webp); }
|
&12 { --wc : url("/assets/waterColorMasks/center/0012.webp"); }
|
||||||
&13 { --wc : url(/assets/waterColorMasks/center/0013.webp); }
|
&13 { --wc : url("/assets/waterColorMasks/center/0013.webp"); }
|
||||||
&14 { --wc : url(/assets/waterColorMasks/center/0014.webp); }
|
&14 { --wc : url("/assets/waterColorMasks/center/0014.webp"); }
|
||||||
&15 { --wc : url(/assets/waterColorMasks/center/0015.webp); }
|
&15 { --wc : url("/assets/waterColorMasks/center/0015.webp"); }
|
||||||
&16 { --wc : url(/assets/waterColorMasks/center/0016.webp); }
|
&16 { --wc : url("/assets/waterColorMasks/center/0016.webp"); }
|
||||||
&special { --wc : url(/assets/waterColorMasks/center/special.webp); }
|
&special { --wc : url("/assets/waterColorMasks/center/special.webp"); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
[class*="imageMaskCorner"] {
|
[class*='imageMaskCorner'] {
|
||||||
height : 200%;
|
|
||||||
width : 200%;
|
|
||||||
left : calc(-50% + var(--offsetX));
|
|
||||||
bottom : calc(-50% + var(--offsetY));
|
bottom : calc(-50% + var(--offsetY));
|
||||||
|
left : calc(-50% + var(--offsetX));
|
||||||
|
width : 200%;
|
||||||
|
height : 200%;
|
||||||
|
transform : rotate(calc(1deg * var(--rotation))) scaleX(var(--scaleX)) scaleY(var(--scaleY));
|
||||||
-webkit-mask-image : var(--wc), var(--revealer);
|
-webkit-mask-image : var(--wc), var(--revealer);
|
||||||
-webkit-mask-repeat : no-repeat;
|
-webkit-mask-repeat : no-repeat;
|
||||||
-webkit-mask-size : 100% 100%; //Scale both dimensions to fit page size
|
-webkit-mask-size : 100% 100%; //Scale both dimensions to fit page size
|
||||||
@@ -363,12 +337,11 @@ body {
|
|||||||
mask-repeat : no-repeat;
|
mask-repeat : no-repeat;
|
||||||
mask-size : 100% 100%; //Scale both dimensions to fit page size
|
mask-size : 100% 100%; //Scale both dimensions to fit page size
|
||||||
mask-position : 50% 50%;
|
mask-position : 50% 50%;
|
||||||
transform : rotate(calc(1deg * var(--rotation))) scaleX(var(--scaleX)) scaleY(var(--scaleY));
|
|
||||||
& > p:has(img) {
|
& > p:has(img) {
|
||||||
|
bottom : 25%;
|
||||||
|
left : 25%;
|
||||||
width : 50%;
|
width : 50%;
|
||||||
height : 50%; //Complex transform below to handle mix of % and cm offsets
|
height : 50%; //Complex transform below to handle mix of % and cm offsets
|
||||||
left : 25%;
|
|
||||||
bottom : 25%;
|
|
||||||
transform : scaleX(calc(1 / var(--scaleX))) scaleY(calc(1 / var(--scaleY)))
|
transform : scaleX(calc(1 / var(--scaleX))) scaleY(calc(1 / var(--scaleY)))
|
||||||
rotate(calc(-1deg * var(--rotation)))
|
rotate(calc(-1deg * var(--rotation)))
|
||||||
translateX(calc(-1 * var(--offsetX)))
|
translateX(calc(-1 * var(--offsetX)))
|
||||||
@@ -376,43 +349,43 @@ body {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.imageMaskCorner {
|
.imageMaskCorner {
|
||||||
&1 { --wc : url(/assets/waterColorMasks/corner/0001.webp); }
|
&1 { --wc : url("/assets/waterColorMasks/corner/0001.webp"); }
|
||||||
&2 { --wc : url(/assets/waterColorMasks/corner/0002.webp); }
|
&2 { --wc : url("/assets/waterColorMasks/corner/0002.webp"); }
|
||||||
&3 { --wc : url(/assets/waterColorMasks/corner/0003.webp); }
|
&3 { --wc : url("/assets/waterColorMasks/corner/0003.webp"); }
|
||||||
&4 { --wc : url(/assets/waterColorMasks/corner/0004.webp); }
|
&4 { --wc : url("/assets/waterColorMasks/corner/0004.webp"); }
|
||||||
&5 { --wc : url(/assets/waterColorMasks/corner/0005.webp); }
|
&5 { --wc : url("/assets/waterColorMasks/corner/0005.webp"); }
|
||||||
&6 { --wc : url(/assets/waterColorMasks/corner/0006.webp); }
|
&6 { --wc : url("/assets/waterColorMasks/corner/0006.webp"); }
|
||||||
&7 { --wc : url(/assets/waterColorMasks/corner/0007.webp); }
|
&7 { --wc : url("/assets/waterColorMasks/corner/0007.webp"); }
|
||||||
&8 { --wc : url(/assets/waterColorMasks/corner/0008.webp); }
|
&8 { --wc : url("/assets/waterColorMasks/corner/0008.webp"); }
|
||||||
&9 { --wc : url(/assets/waterColorMasks/corner/0009.webp); }
|
&9 { --wc : url("/assets/waterColorMasks/corner/0009.webp"); }
|
||||||
&10 { --wc : url(/assets/waterColorMasks/corner/0010.webp); }
|
&10 { --wc : url("/assets/waterColorMasks/corner/0010.webp"); }
|
||||||
&11 { --wc : url(/assets/waterColorMasks/corner/0011.webp); }
|
&11 { --wc : url("/assets/waterColorMasks/corner/0011.webp"); }
|
||||||
&12 { --wc : url(/assets/waterColorMasks/corner/0012.webp); }
|
&12 { --wc : url("/assets/waterColorMasks/corner/0012.webp"); }
|
||||||
&13 { --wc : url(/assets/waterColorMasks/corner/0013.webp); }
|
&13 { --wc : url("/assets/waterColorMasks/corner/0013.webp"); }
|
||||||
&14 { --wc : url(/assets/waterColorMasks/corner/0014.webp); }
|
&14 { --wc : url("/assets/waterColorMasks/corner/0014.webp"); }
|
||||||
&15 { --wc : url(/assets/waterColorMasks/corner/0015.webp); }
|
&15 { --wc : url("/assets/waterColorMasks/corner/0015.webp"); }
|
||||||
&16 { --wc : url(/assets/waterColorMasks/corner/0016.webp); }
|
&16 { --wc : url("/assets/waterColorMasks/corner/0016.webp"); }
|
||||||
&17 { --wc : url(/assets/waterColorMasks/corner/0017.webp); }
|
&17 { --wc : url("/assets/waterColorMasks/corner/0017.webp"); }
|
||||||
&18 { --wc : url(/assets/waterColorMasks/corner/0018.webp); }
|
&18 { --wc : url("/assets/waterColorMasks/corner/0018.webp"); }
|
||||||
&19 { --wc : url(/assets/waterColorMasks/corner/0019.webp); }
|
&19 { --wc : url("/assets/waterColorMasks/corner/0019.webp"); }
|
||||||
&20 { --wc : url(/assets/waterColorMasks/corner/0020.webp); }
|
&20 { --wc : url("/assets/waterColorMasks/corner/0020.webp"); }
|
||||||
&21 { --wc : url(/assets/waterColorMasks/corner/0021.webp); }
|
&21 { --wc : url("/assets/waterColorMasks/corner/0021.webp"); }
|
||||||
&22 { --wc : url(/assets/waterColorMasks/corner/0022.webp); }
|
&22 { --wc : url("/assets/waterColorMasks/corner/0022.webp"); }
|
||||||
&23 { --wc : url(/assets/waterColorMasks/corner/0023.webp); }
|
&23 { --wc : url("/assets/waterColorMasks/corner/0023.webp"); }
|
||||||
&24 { --wc : url(/assets/waterColorMasks/corner/0024.webp); }
|
&24 { --wc : url("/assets/waterColorMasks/corner/0024.webp"); }
|
||||||
&25 { --wc : url(/assets/waterColorMasks/corner/0025.webp); }
|
&25 { --wc : url("/assets/waterColorMasks/corner/0025.webp"); }
|
||||||
&26 { --wc : url(/assets/waterColorMasks/corner/0026.webp); }
|
&26 { --wc : url("/assets/waterColorMasks/corner/0026.webp"); }
|
||||||
&27 { --wc : url(/assets/waterColorMasks/corner/0027.webp); }
|
&27 { --wc : url("/assets/waterColorMasks/corner/0027.webp"); }
|
||||||
&28 { --wc : url(/assets/waterColorMasks/corner/0028.webp); }
|
&28 { --wc : url("/assets/waterColorMasks/corner/0028.webp"); }
|
||||||
&29 { --wc : url(/assets/waterColorMasks/corner/0029.webp); }
|
&29 { --wc : url("/assets/waterColorMasks/corner/0029.webp"); }
|
||||||
&30 { --wc : url(/assets/waterColorMasks/corner/0030.webp); }
|
&30 { --wc : url("/assets/waterColorMasks/corner/0030.webp"); }
|
||||||
&31 { --wc : url(/assets/waterColorMasks/corner/0031.webp); }
|
&31 { --wc : url("/assets/waterColorMasks/corner/0031.webp"); }
|
||||||
&32 { --wc : url(/assets/waterColorMasks/corner/0032.webp); }
|
&32 { --wc : url("/assets/waterColorMasks/corner/0032.webp"); }
|
||||||
&33 { --wc : url(/assets/waterColorMasks/corner/0033.webp); }
|
&33 { --wc : url("/assets/waterColorMasks/corner/0033.webp"); }
|
||||||
&34 { --wc : url(/assets/waterColorMasks/corner/0034.webp); }
|
&34 { --wc : url("/assets/waterColorMasks/corner/0034.webp"); }
|
||||||
&35 { --wc : url(/assets/waterColorMasks/corner/0035.webp); }
|
&35 { --wc : url("/assets/waterColorMasks/corner/0035.webp"); }
|
||||||
&36 { --wc : url(/assets/waterColorMasks/corner/0036.webp); }
|
&36 { --wc : url("/assets/waterColorMasks/corner/0036.webp"); }
|
||||||
&37 { --wc : url(/assets/waterColorMasks/corner/0037.webp); }
|
&37 { --wc : url("/assets/waterColorMasks/corner/0037.webp"); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -443,9 +416,7 @@ body {
|
|||||||
.blank {
|
.blank {
|
||||||
height : 1em;
|
height : 1em;
|
||||||
margin-top : 0;
|
margin-top : 0;
|
||||||
& + * {
|
& + * { margin-top : 0; }
|
||||||
margin-top : 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -453,12 +424,39 @@ body {
|
|||||||
// * WIDE
|
// * WIDE
|
||||||
// *****************************/
|
// *****************************/
|
||||||
.page {
|
.page {
|
||||||
.wide{
|
.wide {
|
||||||
column-span : all;
|
column-span : all;
|
||||||
display : block;
|
display : block;
|
||||||
margin-bottom : 1em;
|
margin-bottom : 1em;
|
||||||
&+* {
|
& + * { margin-top : 0; }
|
||||||
margin-top : 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//*****************************
|
||||||
|
//* CREDITS
|
||||||
|
//*****************************/
|
||||||
|
.page .homebreweryCredits {
|
||||||
|
p {
|
||||||
|
font-family : 'NodestoCapsWide';
|
||||||
|
font-size : 0.4cm;
|
||||||
|
line-height : 1em;
|
||||||
|
text-align : center;
|
||||||
|
text-indent : 0;
|
||||||
|
letter-spacing : 0.08em;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color : inherit;
|
||||||
|
text-decoration : none;
|
||||||
|
&:hover { text-decoration : underline; }
|
||||||
|
}
|
||||||
|
.homebreweryIcon {
|
||||||
|
display : block;
|
||||||
|
height : 1.5cm;
|
||||||
|
margin : 0 auto;
|
||||||
|
background-color : black;
|
||||||
|
-webkit-mask : url("/assets/naturalCritLogoWhite.svg") center / contain no-repeat;
|
||||||
|
mask : url("/assets/naturalCritLogoWhite.svg") center / contain no-repeat;
|
||||||
|
}
|
||||||
|
.homebreweryIcon.red { background-color : red; }
|
||||||
|
.homebreweryIcon.gold { background-image : linear-gradient(to top left, brown 22.5%, gold 40%, white 60%, gold 67.5%, brown 82.5%); }
|
||||||
|
}
|
||||||
|
|||||||
88
themes/codeMirror/customEditorStyles.less
Normal file
88
themes/codeMirror/customEditorStyles.less
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
.editor .codeEditor .CodeMirror {
|
||||||
|
// Themes with dark backgrounds
|
||||||
|
&.cm-s-3024-night,
|
||||||
|
&.cm-s-abbott,
|
||||||
|
&.cm-s-abcdef,
|
||||||
|
&.cm-s-ambiance,
|
||||||
|
&.cm-s-ayu-dark,
|
||||||
|
&.cm-s-ayu-mirage,
|
||||||
|
&.cm-s-base16-dark,
|
||||||
|
&.cm-s-bespin,
|
||||||
|
&.cm-s-blackboard,
|
||||||
|
&.cm-s-cobalt,
|
||||||
|
&.cm-s-colorforth,
|
||||||
|
&.cm-s-darcula,
|
||||||
|
&.cm-s-dracula,
|
||||||
|
&.cm-s-duotone-dark,
|
||||||
|
&.cm-s-erlang-dark,
|
||||||
|
&.cm-s-gruvbox-dark,
|
||||||
|
&.cm-s-hopscotch,
|
||||||
|
&.cm-s-icecoder,
|
||||||
|
&.cm-s-isotope,
|
||||||
|
&.cm-s-lesser-dark,
|
||||||
|
&.cm-s-liquibyte,
|
||||||
|
&.cm-s-lucario,
|
||||||
|
&.cm-s-material,
|
||||||
|
&.cm-s-material-darker,
|
||||||
|
&.cm-s-material-ocean,
|
||||||
|
&.cm-s-material-palenight,
|
||||||
|
&.cm-s-mbo,
|
||||||
|
&.cm-s-midnight,
|
||||||
|
&.cm-s-monokai,
|
||||||
|
&.cm-s-moxer,
|
||||||
|
&.cm-s-night,
|
||||||
|
&.cm-s-nord,
|
||||||
|
&.cm-s-oceanic-next,
|
||||||
|
&.cm-s-panda-syntax,
|
||||||
|
&.cm-s-paraiso-dark,
|
||||||
|
&.cm-s-pastel-on-dark,
|
||||||
|
&.cm-s-railscasts,
|
||||||
|
&.cm-s-rubyblue,
|
||||||
|
&.cm-s-seti,
|
||||||
|
&.cm-s-shadowfox,
|
||||||
|
&.cm-s-the-matrix,
|
||||||
|
&.cm-s-tomorrow-night-bright,
|
||||||
|
&.cm-s-tomorrow-night-eighties,
|
||||||
|
&.cm-s-twilight,
|
||||||
|
&.cm-s-vibrant-ink,
|
||||||
|
&.cm-s-xq-dark,
|
||||||
|
&.cm-s-yonce,
|
||||||
|
&.cm-s-zenburn
|
||||||
|
{
|
||||||
|
.CodeMirror-code {
|
||||||
|
.block:not(.cm-comment) {
|
||||||
|
color: magenta;
|
||||||
|
}
|
||||||
|
.columnSplit {
|
||||||
|
color: black;
|
||||||
|
background-color: rgba(35,153,153,0.5);
|
||||||
|
}
|
||||||
|
.pageLine {
|
||||||
|
background-color: rgba(255,255,255,0.75);
|
||||||
|
& ~ pre.CodeMirror-line {
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Themes with light backgrounds
|
||||||
|
&.cm-s-default,
|
||||||
|
&.cm-s-3024-day,
|
||||||
|
&.cm-s-ambiance-mobile,
|
||||||
|
&.cm-s-base16-light,
|
||||||
|
&.cm-s-duotone-light,
|
||||||
|
&.cm-s-eclipse,
|
||||||
|
&.cm-s-elegant,
|
||||||
|
&.cm-s-juejin,
|
||||||
|
&.cm-s-neat,
|
||||||
|
&.cm-s-neo,
|
||||||
|
&.cm-s-paraiso-lightm
|
||||||
|
&.cm-s-solarized,
|
||||||
|
&.cm-s-ssms,
|
||||||
|
&.cm-s-ttcn,
|
||||||
|
&.cm-s-xq-light,
|
||||||
|
&.cm-s-yeti {
|
||||||
|
// Future styling for themes with light backgrounds
|
||||||
|
--dummyVar: 'currently unused';
|
||||||
|
}
|
||||||
|
}
|
||||||
129
themes/codeMirror/customThemes/darkbrewery-v301.css
Normal file
129
themes/codeMirror/customThemes/darkbrewery-v301.css
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
/* Main BG color and normal text color */
|
||||||
|
.CodeMirror {
|
||||||
|
background: #293134;
|
||||||
|
color: #91A6AA;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Brew BG */
|
||||||
|
.brewRenderer {
|
||||||
|
background-color: #293134;
|
||||||
|
}
|
||||||
|
/* Blinking cursor */
|
||||||
|
.CodeMirror-cursor {
|
||||||
|
border-left: 1px solid #e0e2e4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* HB DARK NAV START*/
|
||||||
|
|
||||||
|
/* Bars at the top */
|
||||||
|
.snippetBar {
|
||||||
|
background-color: #2F393C;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
nav {
|
||||||
|
background-color: #293134;
|
||||||
|
}
|
||||||
|
nav .navItem {
|
||||||
|
background-color: #293134;
|
||||||
|
}
|
||||||
|
/* Fix for Homebrewery custom Snippet icons */
|
||||||
|
.snippetBar .fac {
|
||||||
|
filter: invert(1);
|
||||||
|
}
|
||||||
|
.snippetBar .snippetGroup .dropdown {
|
||||||
|
background-color: #2F393C;
|
||||||
|
}
|
||||||
|
/* HB DARK NAV END */
|
||||||
|
|
||||||
|
/* Line number stuff */
|
||||||
|
.CodeMirror-gutter-elt {
|
||||||
|
color: #81969A;
|
||||||
|
}
|
||||||
|
.CodeMirror-linenumber {
|
||||||
|
background-color: #293134;
|
||||||
|
}
|
||||||
|
.CodeMirror-gutter {
|
||||||
|
background-color: #293134;
|
||||||
|
}
|
||||||
|
/* column splits */
|
||||||
|
.editor .codeEditor .columnSplit {
|
||||||
|
font-style: italic;
|
||||||
|
color: inherit;
|
||||||
|
background-color:#1f5763;
|
||||||
|
border-bottom: #299 solid 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Colors for headings and such */
|
||||||
|
/* ###Headings */
|
||||||
|
.cm-s-default .cm-header {
|
||||||
|
color: #c51b1b;
|
||||||
|
-webkit-text-stroke-width: 0.1px;
|
||||||
|
-webkit-text-stroke-color: #000;
|
||||||
|
}
|
||||||
|
/* bold points */
|
||||||
|
.cm-header, .cm-strong {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #309dd2;
|
||||||
|
}
|
||||||
|
/* Link headings */
|
||||||
|
.cm-s-default .cm-link {
|
||||||
|
color: #dd6300;
|
||||||
|
}
|
||||||
|
/* links */
|
||||||
|
.cm-s-default .cm-string {
|
||||||
|
color: #aa8261;
|
||||||
|
}
|
||||||
|
/*@import*/
|
||||||
|
.cm-s-default .cm-def {
|
||||||
|
color:#2986cc;
|
||||||
|
}
|
||||||
|
/* Bullets and such */
|
||||||
|
.cm-s-default .cm-variable-2 {
|
||||||
|
color: #3cbf30;
|
||||||
|
}
|
||||||
|
/* blocks */
|
||||||
|
.editor .codeEditor .block:not(.cm-comment) {
|
||||||
|
color: #e3e3e3;
|
||||||
|
}
|
||||||
|
/* inline blocks */
|
||||||
|
.editor .codeEditor .inline-block {
|
||||||
|
color: #e3e3e3;
|
||||||
|
}
|
||||||
|
/* Tags (divs) */
|
||||||
|
.cm-s-default .cm-tag {
|
||||||
|
color: #e3ff00;
|
||||||
|
}
|
||||||
|
.cm-s-default .cm-attribute {
|
||||||
|
color: #e3ff00;
|
||||||
|
}
|
||||||
|
.cm-s-default .cm-atom {
|
||||||
|
color:#000;
|
||||||
|
}
|
||||||
|
.cm-s-default .cm-qualifier{
|
||||||
|
color:#ee1919;
|
||||||
|
}
|
||||||
|
.cm-s-default .cm-comment{
|
||||||
|
color:#bbc700;
|
||||||
|
}
|
||||||
|
.cm-s-default .cm-keyword {
|
||||||
|
color:#c302df;
|
||||||
|
background-color:#b1b1b1;
|
||||||
|
}
|
||||||
|
.cm-s-default .cm-property.cm-error {
|
||||||
|
color:#c50202;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-foldmarker {
|
||||||
|
color:#f0ff00;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* New page */
|
||||||
|
.editor .codeEditor .pageLine {
|
||||||
|
background: #000;
|
||||||
|
color:#000;
|
||||||
|
border-bottom: 1px solid #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-s-default .cm-builtin {
|
||||||
|
color:#fff;
|
||||||
|
}
|
||||||
69
themes/codeMirror/editorThemes.json
Normal file
69
themes/codeMirror/editorThemes.json
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
[
|
||||||
|
"default",
|
||||||
|
"3024-day",
|
||||||
|
"3024-night",
|
||||||
|
"abbott",
|
||||||
|
"abcdef",
|
||||||
|
"ambiance-mobile",
|
||||||
|
"ambiance",
|
||||||
|
"ayu-dark",
|
||||||
|
"ayu-mirage",
|
||||||
|
"base16-dark",
|
||||||
|
"base16-light",
|
||||||
|
"bespin",
|
||||||
|
"blackboard",
|
||||||
|
"cobalt",
|
||||||
|
"colorforth",
|
||||||
|
"darcula",
|
||||||
|
"darkbrewery-v301",
|
||||||
|
"dracula",
|
||||||
|
"duotone-dark",
|
||||||
|
"duotone-light",
|
||||||
|
"eclipse",
|
||||||
|
"elegant",
|
||||||
|
"erlang-dark",
|
||||||
|
"gruvbox-dark",
|
||||||
|
"hopscotch",
|
||||||
|
"icecoder",
|
||||||
|
"idea",
|
||||||
|
"isotope",
|
||||||
|
"juejin",
|
||||||
|
"lesser-dark",
|
||||||
|
"liquibyte",
|
||||||
|
"lucario",
|
||||||
|
"material-darker",
|
||||||
|
"material-ocean",
|
||||||
|
"material-palenight",
|
||||||
|
"material",
|
||||||
|
"mbo",
|
||||||
|
"mdn-like",
|
||||||
|
"midnight",
|
||||||
|
"monokai",
|
||||||
|
"moxer",
|
||||||
|
"neat",
|
||||||
|
"neo",
|
||||||
|
"night",
|
||||||
|
"nord",
|
||||||
|
"oceanic-next",
|
||||||
|
"panda-syntax",
|
||||||
|
"paraiso-dark",
|
||||||
|
"paraiso-light",
|
||||||
|
"pastel-on-dark",
|
||||||
|
"railscasts",
|
||||||
|
"rubyblue",
|
||||||
|
"seti",
|
||||||
|
"shadowfox",
|
||||||
|
"solarized",
|
||||||
|
"ssms",
|
||||||
|
"the-matrix",
|
||||||
|
"tomorrow-night-bright",
|
||||||
|
"tomorrow-night-eighties",
|
||||||
|
"ttcn",
|
||||||
|
"twilight",
|
||||||
|
"vibrant-ink",
|
||||||
|
"xq-dark",
|
||||||
|
"xq-light",
|
||||||
|
"yeti",
|
||||||
|
"yonce",
|
||||||
|
"zenburn"
|
||||||
|
]
|
||||||
BIN
themes/fonts/icon fonts/Elderberry-Inn-Icons.woff2
Normal file
BIN
themes/fonts/icon fonts/Elderberry-Inn-Icons.woff2
Normal file
Binary file not shown.
224
themes/fonts/icon fonts/font-icons.less
Normal file
224
themes/fonts/icon fonts/font-icons.less
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
/* Main Font, serif */
|
||||||
|
@font-face {
|
||||||
|
font-family : 'Eldeberry-Inn';
|
||||||
|
font-style : normal;
|
||||||
|
font-weight : normal;
|
||||||
|
src : url('../../../fonts/icon fonts/Elderberry-Inn-Icons.woff2');
|
||||||
|
}
|
||||||
|
|
||||||
|
.page {
|
||||||
|
span.ei {
|
||||||
|
display : inline-block;
|
||||||
|
margin-right : 3px;
|
||||||
|
font-family : 'Eldeberry-Inn';
|
||||||
|
line-height : 1;
|
||||||
|
vertical-align : baseline;
|
||||||
|
-moz-osx-font-smoothing : grayscale;
|
||||||
|
-webkit-font-smoothing : antialiased;
|
||||||
|
text-rendering : auto;
|
||||||
|
|
||||||
|
&.book::before { content : '\E900'; }
|
||||||
|
&.screen::before { content : '\E901'; }
|
||||||
|
|
||||||
|
/* Spell levels */
|
||||||
|
&.spell-0::before { content : '\E902'; }
|
||||||
|
&.spell-1::before { content : '\E903'; }
|
||||||
|
&.spell-2::before { content : '\E904'; }
|
||||||
|
&.spell-3::before { content : '\E905'; }
|
||||||
|
&.spell-4::before { content : '\E906'; }
|
||||||
|
&.spell-5::before { content : '\E907'; }
|
||||||
|
&.spell-6::before { content : '\E908'; }
|
||||||
|
&.spell-7::before { content : '\E909'; }
|
||||||
|
&.spell-8::before { content : '\E90A'; }
|
||||||
|
&.spell-9::before { content : '\E90B'; }
|
||||||
|
|
||||||
|
/* Damage types */
|
||||||
|
&.acid::before { content : '\E90C'; }
|
||||||
|
&.bludgeoning::before { content : '\E90D'; }
|
||||||
|
&.cold::before { content : '\E90E'; }
|
||||||
|
&.fire::before { content : '\E90F'; }
|
||||||
|
&.force::before { content : '\E910'; }
|
||||||
|
&.lightning::before { content : '\E911'; }
|
||||||
|
&.necrotic::before { content : '\E912'; }
|
||||||
|
&.piercing::before { content : '\E914'; }
|
||||||
|
&.poison::before { content : '\E913'; }
|
||||||
|
&.psychic::before { content : '\E915'; }
|
||||||
|
&.radiant::before { content : '\E916'; }
|
||||||
|
&.slashing::before { content : '\E917'; }
|
||||||
|
&.thunder::before { content : '\E918'; }
|
||||||
|
|
||||||
|
/* DnD Conditions */
|
||||||
|
&.blinded::before { content : '\E919'; }
|
||||||
|
&.charmed::before { content : '\E91A'; }
|
||||||
|
&.deafened::before { content : '\E91B'; }
|
||||||
|
&.exhaust-1::before { content : '\E91C'; }
|
||||||
|
&.exhaust-2::before { content : '\E91D'; }
|
||||||
|
&.exhaust-3::before { content : '\E91E'; }
|
||||||
|
&.exhaust-4::before { content : '\E91F'; }
|
||||||
|
&.exhaust-5::before { content : '\E920'; }
|
||||||
|
&.exhaust-6::before { content : '\E921'; }
|
||||||
|
&.frightened::before { content : '\E922'; }
|
||||||
|
&.grappled::before { content : '\E923'; }
|
||||||
|
&.incapacitated::before { content : '\E924'; }
|
||||||
|
&.invisible::before { content : '\E926'; }
|
||||||
|
&.paralyzed::before { content : '\E927'; }
|
||||||
|
&.petrified::before { content : '\E928'; }
|
||||||
|
&.poisoned::before { content : '\E929'; }
|
||||||
|
&.prone::before { content : '\E92A'; }
|
||||||
|
&.restrained::before { content : '\E92B'; }
|
||||||
|
&.stunned::before { content : '\E92C'; }
|
||||||
|
&.unconscious::before { content : '\E925'; }
|
||||||
|
|
||||||
|
/* Character Classes and Features */
|
||||||
|
&.barbarian-rage::before { content : '\E92D'; }
|
||||||
|
&.barbarian-reckless-attack::before { content : '\E92E'; }
|
||||||
|
&.bardic-inspiration::before { content : '\E92F'; }
|
||||||
|
&.cleric-channel-divinity::before { content : '\E930'; }
|
||||||
|
&.druid-wild-shape::before { content : '\E931'; }
|
||||||
|
&.fighter-action-surge::before { content : '\E932'; }
|
||||||
|
&.fighter-second-wind::before { content : '\E933'; }
|
||||||
|
&.monk-flurry-blows::before { content : '\E934'; }
|
||||||
|
&.monk-patient-defense::before { content : '\E935'; }
|
||||||
|
&.monk-step-of-the-wind::before { content : '\E936'; }
|
||||||
|
&.monk-step-of-the-wind-2::before { content : '\E937'; }
|
||||||
|
&.monk-step-of-the-wind-3::before { content : '\E938'; }
|
||||||
|
&.monk-stunning-strike::before { content : '\E939'; }
|
||||||
|
&.monk-stunning-strike-2::before { content : '\E939'; }
|
||||||
|
&.paladin-divine-smite::before { content : '\E93B'; }
|
||||||
|
&.paladin-lay-on-hands::before { content : '\E93C'; }
|
||||||
|
&.barbarian-abilities::before { content : '\E93D'; }
|
||||||
|
&.barbarian::before { content : '\E93E'; }
|
||||||
|
&.bard-abilities::before { content : '\E93F'; }
|
||||||
|
&.bard::before { content : '\E940'; }
|
||||||
|
&.cleric-abilities::before { content : '\E941'; }
|
||||||
|
&.cleric::before { content : '\E942'; }
|
||||||
|
&.druid-abilities::before { content : '\E943'; }
|
||||||
|
&.druid::before { content : '\E944'; }
|
||||||
|
&.fighter-abilities::before { content : '\E945'; }
|
||||||
|
&.fighter::before { content : '\E946'; }
|
||||||
|
&.monk-abilities::before { content : '\E947'; }
|
||||||
|
&.monk::before { content : '\E948'; }
|
||||||
|
&.paladin-abilities::before { content : '\E949'; }
|
||||||
|
&.paladin::before { content : '\E94A'; }
|
||||||
|
&.ranger-abilities::before { content : '\E94B'; }
|
||||||
|
&.ranger::before { content : '\E94C'; }
|
||||||
|
&.rogue-abilities::before { content : '\E94D'; }
|
||||||
|
&.rogue::before { content : '\E94E'; }
|
||||||
|
&.sorcerer-abilities::before { content : '\E94F'; }
|
||||||
|
&.sorcerer::before { content : '\E950'; }
|
||||||
|
&.warlock-abilities::before { content : '\E951'; }
|
||||||
|
&.warlock::before { content : '\E952'; }
|
||||||
|
&.wizard-abilities::before { content : '\E953'; }
|
||||||
|
&.wizard::before { content : '\E954'; }
|
||||||
|
|
||||||
|
/* Types of actions */
|
||||||
|
&.movement::before { content : '\E955'; }
|
||||||
|
&.action::before { content : '\E956'; }
|
||||||
|
&.bonus-action::before { content : '\E957'; }
|
||||||
|
&.reaction::before { content : '\E958'; }
|
||||||
|
|
||||||
|
/* SRD Spells */
|
||||||
|
&.acid-arrow::before { content : '\E959'; }
|
||||||
|
&.action-1::before { content : '\E95A'; }
|
||||||
|
&.alter-self::before { content : '\E95B'; }
|
||||||
|
&.alter-self-2::before { content : '\E95C'; }
|
||||||
|
&.animal-friendship::before { content : '\E95E'; }
|
||||||
|
&.animate-dead::before { content : '\E95F'; }
|
||||||
|
&.animate-objects::before { content : '\E960'; }
|
||||||
|
&.animate-objects-2::before { content : '\E961'; }
|
||||||
|
&.bane::before { content : '\E962'; }
|
||||||
|
&.bless::before { content : '\E963'; }
|
||||||
|
&.blur::before { content : '\E964'; }
|
||||||
|
&.bonus::before { content : '\E965'; }
|
||||||
|
&.branding-smite::before { content : '\E966'; }
|
||||||
|
&.burning-hands::before { content : '\E967'; }
|
||||||
|
&.charm-person::before { content : '\E968'; }
|
||||||
|
&.chill-touch::before { content : '\E969'; }
|
||||||
|
&.cloudkill::before { content : '\E96A'; }
|
||||||
|
&.comprehend-languages::before { content : '\E96B'; }
|
||||||
|
&.cone-of-cold::before { content : '\E96C'; }
|
||||||
|
&.conjure-elemental::before { content : '\E96D'; }
|
||||||
|
&.conjure-minor-elemental::before { content : '\E96E'; }
|
||||||
|
&.control-water::before { content : '\E96F'; }
|
||||||
|
&.counterspell::before { content : '\E970'; }
|
||||||
|
&.cure-wounds::before { content : '\E971'; }
|
||||||
|
&.dancing-lights::before { content : '\E972'; }
|
||||||
|
&.darkness::before { content : '\E973'; }
|
||||||
|
&.detect-magic::before { content : '\E974'; }
|
||||||
|
&.disguise-self::before { content : '\E975'; }
|
||||||
|
&.disintegrate::before { content : '\E976'; }
|
||||||
|
&.dispel-evil-and-good::before { content : '\E977'; }
|
||||||
|
&.dispel-magic::before { content : '\E978'; }
|
||||||
|
&.dominate-monster::before { content : '\E979'; }
|
||||||
|
&.dominate-person::before { content : '\E97A'; }
|
||||||
|
&.eldritch-blast::before { content : '\E97B'; }
|
||||||
|
&.enlarge-reduce::before { content : '\E97C'; }
|
||||||
|
&.entangle::before { content : '\E97D'; }
|
||||||
|
&.faerie-fire::before { content : '\E97E'; }
|
||||||
|
&.faerie-fire2::before { content : '\E97F'; }
|
||||||
|
&.feather-fall::before { content : '\E980'; }
|
||||||
|
&.find-familiar::before { content : '\E981'; }
|
||||||
|
&.finger-of-death::before { content : '\E982'; }
|
||||||
|
&.fireball::before { content : '\E983'; }
|
||||||
|
&.floating-disk::before { content : '\E984'; }
|
||||||
|
&.fly::before { content : '\E985'; }
|
||||||
|
&.fog-cloud::before { content : '\E986'; }
|
||||||
|
&.gaseous-form::before { content : '\E987'; }
|
||||||
|
&.gaseous-form2::before { content : '\E988'; }
|
||||||
|
&.gentle-repose::before { content : '\E989'; }
|
||||||
|
&.gentle-repose2::before { content : '\E98A'; }
|
||||||
|
&.globe-of-invulnerability::before { content : '\E98B'; }
|
||||||
|
&.guiding-bolt::before { content : '\E98C'; }
|
||||||
|
&.healing-word::before { content : '\E98D'; }
|
||||||
|
&.heat-metal::before { content : '\E98E'; }
|
||||||
|
&.hellish-rebuke::before { content : '\E98F'; }
|
||||||
|
&.heroes-feast::before { content : '\E990'; }
|
||||||
|
&.heroism::before { content : '\E991'; }
|
||||||
|
&.hideous-laughter::before { content : '\E992'; }
|
||||||
|
&.identify::before { content : '\E993'; }
|
||||||
|
&.illusory-script::before { content : '\E994'; }
|
||||||
|
&.inflict-wounds::before { content : '\E995'; }
|
||||||
|
&.light::before { content : '\E996'; }
|
||||||
|
&.longstrider::before { content : '\E997'; }
|
||||||
|
&.mage-armor::before { content : '\E998'; }
|
||||||
|
&.mage-hand::before { content : '\E999'; }
|
||||||
|
&.magic-missile::before { content : '\E99A'; }
|
||||||
|
&.mass-cure-wounds::before { content : '\E99B'; }
|
||||||
|
&.mass-healing-word::before { content : '\E99C'; }
|
||||||
|
&.Mending::before { content : '\E99D'; }
|
||||||
|
&.message::before { content : '\E99E'; }
|
||||||
|
&.Minor-illusion::before { content : '\E99F'; }
|
||||||
|
&.movement1::before { content : '\E9A0'; }
|
||||||
|
&.polymorph::before { content : '\E9A1'; }
|
||||||
|
&.power-word-kill::before { content : '\E9A2'; }
|
||||||
|
&.power-word-stun::before { content : '\E9A3'; }
|
||||||
|
&.prayer-of-healing::before { content : '\E9A4'; }
|
||||||
|
&.prestidigitation::before { content : '\E9A5'; }
|
||||||
|
&.protection-from-evil-and-good::before { content : '\E9A6'; }
|
||||||
|
&.raise-read::before { content : '\E9A7'; }
|
||||||
|
&.raise-read2::before { content : '\E9A8'; }
|
||||||
|
&.reaction1::before { content : '\E9A9'; }
|
||||||
|
&.resurrection::before { content : '\E9AA'; }
|
||||||
|
&.resurrection2::before { content : '\E9AB'; }
|
||||||
|
&.revivify::before { content : '\E9AC'; }
|
||||||
|
&.revivify2::before { content : '\E9AD'; }
|
||||||
|
&.sacred-flame::before { content : '\E9AE'; }
|
||||||
|
&.sanctuary::before { content : '\E9AF'; }
|
||||||
|
&.scorching-ray::before { content : '\E9B0'; }
|
||||||
|
&.sending::before { content : '\E9B1'; }
|
||||||
|
&.shatter::before { content : '\E9B2'; }
|
||||||
|
&.shield::before { content : '\E9B3'; }
|
||||||
|
&.silent-image::before { content : '\E9B4'; }
|
||||||
|
&.sleep::before { content : '\E9B5'; }
|
||||||
|
&.speak-with-animals::before { content : '\E9B6'; }
|
||||||
|
&.telekinesis::before { content : '\E9B7'; }
|
||||||
|
&.true-strike::before { content : '\E9B8'; }
|
||||||
|
&.vicious-mockery::before { content : '\E9B9'; }
|
||||||
|
&.wall-of-fire::before { content : '\E9BA'; }
|
||||||
|
&.wall-of-force::before { content : '\E9BB'; }
|
||||||
|
&.wall-of-ice::before { content : '\E9BC'; }
|
||||||
|
&.wall-of-stone::before { content : '\E9BD'; }
|
||||||
|
&.wall-of-thorns::before { content : '\E9BE'; }
|
||||||
|
&.wish::before { content : '\E9BF'; }
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user