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

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

|
||||||
|
|
||||||
|
Homebrewery.Naturalcrit.com
|
||||||
|
}}
|
||||||
|
|
||||||
|
\page`;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
85
themes/V3/5ePHB/snippets/index.gen.js
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
const dedent = require('dedent-tabs').default;
|
||||||
|
|
||||||
|
module.exports = ()=>{
|
||||||
|
return dedent`
|
||||||
|
{{index,wide,columns:5;
|
||||||
|
##### Index
|
||||||
|
- Ankhesh-Bort
|
||||||
|
- city map, 7
|
||||||
|
- city watch, 12
|
||||||
|
- guilds, 19
|
||||||
|
- Cheese
|
||||||
|
- types of cheese, 8
|
||||||
|
- cheese-related magic, 14
|
||||||
|
- cheese-related quests, 26-27
|
||||||
|
- Death
|
||||||
|
- appearance, 10
|
||||||
|
- personality, 13
|
||||||
|
- hobbies, 23
|
||||||
|
- Elves
|
||||||
|
- types of elves, 15
|
||||||
|
- elvish magic, 24
|
||||||
|
- elvish curses, 28
|
||||||
|
- Footnotes
|
||||||
|
- types of footnotes, 16-17
|
||||||
|
- footnote rules, 20-21
|
||||||
|
- footnote humor, 29-30
|
||||||
|
- Gods
|
||||||
|
- types of gods, 12
|
||||||
|
- godly interventions, 25
|
||||||
|
- godly conflicts, 31
|
||||||
|
- Heroes
|
||||||
|
- class features, 11-12
|
||||||
|
- heroic deeds, 26-27
|
||||||
|
- Inns
|
||||||
|
- types of inns, 9
|
||||||
|
- inn amenities, 18
|
||||||
|
- Jokes
|
||||||
|
- types of jokes, 11-12
|
||||||
|
- joke delivery, 25
|
||||||
|
- Knives
|
||||||
|
- types of knives, 16-17
|
||||||
|
- knife skills, 22-23
|
||||||
|
- knife fights, 28-29
|
||||||
|
- Luggage
|
||||||
|
- appearance, 10
|
||||||
|
- personality, 13
|
||||||
|
- abilities, 23
|
||||||
|
- Magic
|
||||||
|
- types of magic, 15
|
||||||
|
- magic rules, 24
|
||||||
|
- magic mishaps, 28
|
||||||
|
- Socks
|
||||||
|
- types of socks, 9
|
||||||
|
- sock-related magic (yes, really), 15
|
||||||
|
- sock-related quests (no, really), 26
|
||||||
|
- Trolls
|
||||||
|
- appearance and biology, 11
|
||||||
|
- culture and language, 18
|
||||||
|
- troll rights and activism, 31
|
||||||
|
- Unknown University
|
||||||
|
- history and architecture, 12
|
||||||
|
- faculty and staff, 20
|
||||||
|
- courses and exams, 33
|
||||||
|
- Vampires
|
||||||
|
- types and origins, 13
|
||||||
|
- vampiric powers and weaknesses, 21
|
||||||
|
- vampiric etiquette and politics, 34
|
||||||
|
- Witches
|
||||||
|
- types and traditions, 14
|
||||||
|
- witchcraft and headology, 22
|
||||||
|
- witch trials and tribulations, 35
|
||||||
|
- Xylophones
|
||||||
|
- musical instruments or weapons?, 15
|
||||||
|
- xylophone-related magic and lore, 23
|
||||||
|
- xylophone-related quests and puzzles, 36
|
||||||
|
- Yetis
|
||||||
|
- appearance and behavior, 16
|
||||||
|
- yeti philosophy and religion, 24
|
||||||
|
- yeti encounters and stories, 37
|
||||||
|
- Zombies
|
||||||
|
- types and causes, 17
|
||||||
|
- zombie rights and duties, 25
|
||||||
|
- zombie survival and prevention, 38
|
||||||
|
}}`;
|
||||||
|
};
|
||||||
51
themes/V3/5ePHB/snippets/quote.gen.js
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
const _ = require("lodash");
|
||||||
|
|
||||||
|
const quotes = [
|
||||||
|
"The sword glinted in the dim light, its edges keen and deadly. As the adventurer reached for it, he couldn't help but feel a surge of excitement mixed with fear. This was no ordinary blade.",
|
||||||
|
"The dragon's roar shook the ground beneath their feet, and the brave knight stood tall, his sword at the ready. He knew that this would be the battle of his life, but he was determined to emerge victorious.",
|
||||||
|
"The wizard's laboratory was a sight to behold, filled with bubbling cauldrons, ancient tomes, and strange artifacts from distant lands. As the apprentice gazed around in wonder, she knew that she was about to embark on a journey unlike any other.",
|
||||||
|
"The tavern was packed with rowdy patrons, their voices raised in song and laughter. The bard took center stage, strumming his lute and launching into a tale of adventure and heroism that had the crowd hanging on his every word.",
|
||||||
|
"The thief crept through the shadows, his eyes scanning the room for any sign of danger. He knew that one false move could mean the difference between success and failure, and he was determined to come out on top.",
|
||||||
|
"The elf queen stood atop her castle walls, surveying the kingdom below with a mix of pride and sadness. She knew that the coming war would be brutal, but she was determined to protect her people at all costs.",
|
||||||
|
"The necromancer's tower loomed in the distance, its dark spires piercing the sky. As the adventurers approached, they could feel the chill of death emanating from within",
|
||||||
|
"The ranger moved through the forest like a shadow, his senses attuned to every sound and movement around him. He knew that danger lurked behind every tree, but he was ready for whatever came his way.",
|
||||||
|
"The paladin knelt before the altar, his hands clasped in prayer. He knew that his faith would be tested in the days ahead, but he was ready to face whatever trials lay in store for him.",
|
||||||
|
"The druid communed with the spirits of nature, his mind merging with the trees, the animals, and the very earth itself. He knew that his power came with a great responsibility, and he was determined to use it for the greater good.",
|
||||||
|
];
|
||||||
|
|
||||||
|
const authors = [
|
||||||
|
"Unknown",
|
||||||
|
"James Wyatt",
|
||||||
|
"Eolande Blackwood",
|
||||||
|
"Ragnar Ironheart",
|
||||||
|
"Lyra Nightshade",
|
||||||
|
"Valtorius Darkstar",
|
||||||
|
"Isadora Fireheart",
|
||||||
|
"Theron Shadowbane",
|
||||||
|
"Lirien Starweaver",
|
||||||
|
"Drogathar Bonecrusher",
|
||||||
|
"Kaelen Frostblade",
|
||||||
|
];
|
||||||
|
|
||||||
|
const books = [
|
||||||
|
"The Blade of Destiny",
|
||||||
|
"Dragonfire and Steel",
|
||||||
|
"The Bard's Tale",
|
||||||
|
"Darkness Rising",
|
||||||
|
"The Sacred Quest",
|
||||||
|
"Shadows in the Forest",
|
||||||
|
"The Starweaver Chronicles",
|
||||||
|
"Beneath the Bones",
|
||||||
|
"Moonlit Magic",
|
||||||
|
"Frost and Fury",
|
||||||
|
|
||||||
|
];
|
||||||
|
module.exports = () => {
|
||||||
|
return `
|
||||||
|
{{quote
|
||||||
|
${_.sample(quotes)}
|
||||||
|
|
||||||
|
{{attribution ${_.sample(authors)}, *${_.sample(books)}*}}
|
||||||
|
}}
|
||||||
|
\n`;
|
||||||
|
};
|
||||||