mirror of
https://github.com/naturalcrit/homebrewery.git
synced 2026-01-23 20:53:05 +00:00
Compare commits
831 Commits
2.8.2
...
PRODUCTION
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d3359991f0 | ||
|
|
5b0e3d9cdb | ||
|
|
a3b50efe78 | ||
|
|
82b9f825d5 | ||
|
|
cd0b659653 | ||
|
|
da02622547 | ||
|
|
61b851fd3e | ||
|
|
d821baee4d | ||
|
|
4606c50f75 | ||
|
|
c5dca338e1 | ||
|
|
39eae73978 | ||
|
|
6237df953e | ||
|
|
2497fbbc74 | ||
|
|
79c1563b01 | ||
|
|
2badd39968 | ||
|
|
ccd30f7e80 | ||
|
|
673dc58051 | ||
|
|
fbf1bbbf99 | ||
|
|
494311aee3 | ||
|
|
e070601b28 | ||
|
|
bb65739886 | ||
|
|
304825a9dd | ||
|
|
56aa2a9104 | ||
|
|
3891531d1c | ||
|
|
0aaa400a87 | ||
|
|
f435d65db7 | ||
|
|
d4ff87395f | ||
|
|
5d42196297 | ||
|
|
4fae5332fc | ||
|
|
f118e94257 | ||
|
|
7a44e37970 | ||
|
|
57df6aa321 | ||
|
|
3f3aa6edd1 | ||
|
|
16076d1481 | ||
|
|
7313e326a0 | ||
|
|
aa9f07e0b9 | ||
|
|
36845c021c | ||
|
|
f0d82b2751 | ||
|
|
0610c9fe98 | ||
|
|
1320f5c6c6 | ||
|
|
8e1706532b | ||
|
|
1770323690 | ||
|
|
177173d599 | ||
|
|
a15ef8489c | ||
|
|
59fd2454a4 | ||
|
|
bf146a8c0b | ||
|
|
c178d189c9 | ||
|
|
1abd151c67 | ||
|
|
2f9c08ac49 | ||
|
|
f5057119da | ||
|
|
44172dc5b1 | ||
|
|
1096c80b17 | ||
|
|
43d18191f9 | ||
|
|
1a71ba0eb2 | ||
|
|
e14c8c5e91 | ||
|
|
13ba5ebcc8 | ||
|
|
800c714b9e | ||
|
|
493c31b244 | ||
|
|
ec49429810 | ||
|
|
21dfaf6a5a | ||
|
|
03eef94232 | ||
|
|
0e5ed35b6c | ||
|
|
ac8ef4608a | ||
|
|
a669cd5d86 | ||
|
|
d41a868f07 | ||
|
|
56be8931bb | ||
|
|
0d9981b3c6 | ||
|
|
2b9362f7bf | ||
|
|
6b1b9bdce2 | ||
|
|
22fb84ca32 | ||
|
|
539b52ecbd | ||
|
|
5f837f7b3c | ||
|
|
04b2421793 | ||
|
|
e5cfa98bbd | ||
|
|
9d5130154b | ||
|
|
b4825e085e | ||
|
|
4ce915100b | ||
|
|
9b180a1c50 | ||
|
|
1e8c285eef | ||
|
|
c09d0940d4 | ||
|
|
a7005d779a | ||
|
|
dc65980dcb | ||
|
|
50a8468995 | ||
|
|
1ed5c219ec | ||
|
|
4971c40e23 | ||
|
|
ceb4667193 | ||
|
|
a1df68ed20 | ||
|
|
941bb94190 | ||
|
|
8e841c6825 | ||
|
|
85841d22f5 | ||
|
|
2403d0a18a | ||
|
|
a3354e9614 | ||
|
|
9673a9a0f6 | ||
|
|
e9939e7a0d | ||
|
|
7a74fc03fe | ||
|
|
050a1d45fd | ||
|
|
b61f4e935a | ||
|
|
3233b7c23a | ||
|
|
039db01b31 | ||
|
|
3de95a4f95 | ||
|
|
9c6d875524 | ||
|
|
653fd513ad | ||
|
|
ecdf4aee50 | ||
|
|
9cdfbc7459 | ||
|
|
31c348baff | ||
|
|
043ade6e34 | ||
|
|
519d102a6e | ||
|
|
5f388ed41f | ||
|
|
a834c79b49 | ||
|
|
cb8c3a016a | ||
|
|
7a081e1147 | ||
|
|
72360be3e9 | ||
|
|
5da1c2e754 | ||
|
|
a97fd4f47f | ||
|
|
bc7911b0bc | ||
|
|
c33083814e | ||
|
|
ff3320c8dc | ||
|
|
bd368c4c64 | ||
|
|
f609962d44 | ||
|
|
15f4aef7ef | ||
|
|
4218078502 | ||
|
|
18cd851674 | ||
|
|
2c6c148da8 | ||
|
|
42da4b4c43 | ||
|
|
ee006b6b16 | ||
|
|
cfb98986cd | ||
|
|
0fd7921a50 | ||
|
|
e0e86dff7c | ||
|
|
7e30fb19d4 | ||
|
|
df07e0401a | ||
|
|
a908c5f5d5 | ||
|
|
cdaa0b3ac2 | ||
|
|
6573ada881 | ||
|
|
2a7bde7e44 | ||
|
|
eca58bb27e | ||
|
|
2e68cd77fa | ||
|
|
f1d19d2d63 | ||
|
|
a13759130d | ||
|
|
a99cf75b2e | ||
|
|
3b7a52a60f | ||
|
|
6489a29436 | ||
|
|
c907d32779 | ||
|
|
1885a8d0cc | ||
|
|
5c46ecbebd | ||
|
|
f993a7022c | ||
|
|
0d2624bf3b | ||
|
|
801703a7a5 | ||
|
|
64b62c5e98 | ||
|
|
425d03f6b5 | ||
|
|
7977e869c3 | ||
|
|
1b7729ca01 | ||
|
|
0773dd24ab | ||
|
|
76a6c9c2d3 | ||
|
|
3a2477949b | ||
|
|
1edc62d023 | ||
|
|
7b0f5cec97 | ||
|
|
f0a0c0c11d | ||
|
|
3489a76a1d | ||
|
|
3cf05e551f | ||
|
|
501b356344 | ||
|
|
3f395ad4f3 | ||
|
|
ec92a0307b | ||
|
|
edce4e5bbc | ||
|
|
a7e6d0a513 | ||
|
|
2874bcc5f7 | ||
|
|
521c393b74 | ||
|
|
d853643874 | ||
|
|
28884d6774 | ||
|
|
8f7d6a3eb5 | ||
|
|
58568468f6 | ||
|
|
53de59940f | ||
|
|
4588e02faf | ||
|
|
bd5b3fa6e9 | ||
|
|
9dc6d2532a | ||
|
|
6c68502d03 | ||
|
|
88c485ffe5 | ||
|
|
562bf6d4ac | ||
|
|
807f865d8b | ||
|
|
50c07a5c8e | ||
|
|
3545bdc586 | ||
|
|
3d9f8ea142 | ||
|
|
9726fb5666 | ||
|
|
db22725687 | ||
|
|
69a69bbb82 | ||
|
|
7221d693c6 | ||
|
|
0075b0836a | ||
|
|
77d447c0a3 | ||
|
|
af8ca7141d | ||
|
|
a48c74b2e7 | ||
|
|
a63949636e | ||
|
|
9a44cc04b1 | ||
|
|
cefa3147fc | ||
|
|
51116efba7 | ||
|
|
21058331cf | ||
|
|
4b001d9890 | ||
|
|
c9e1d7ba5c | ||
|
|
c8997cee68 | ||
|
|
ee343ad06f | ||
|
|
79fa0dbe77 | ||
|
|
9120c1d0eb | ||
|
|
a46370b81e | ||
|
|
2a428100c5 | ||
|
|
9f519b469d | ||
|
|
927cc2e9b5 | ||
|
|
8bda68d684 | ||
|
|
e108e30821 | ||
|
|
779426dbb1 | ||
|
|
a6aaa93389 | ||
|
|
72b18e4266 | ||
|
|
479f9af08c | ||
|
|
0ddfb6e4e7 | ||
|
|
120e99959a | ||
|
|
3cf5dc74bb | ||
|
|
7021715543 | ||
|
|
a351e2a118 | ||
|
|
4a2b9bd662 | ||
|
|
8af6a04c58 | ||
|
|
82355f0175 | ||
|
|
965645f1e6 | ||
|
|
5d4bc23c84 | ||
|
|
79e05b1665 | ||
|
|
e3285b5ca4 | ||
|
|
2c954c398c | ||
|
|
33b8f2002b | ||
|
|
e3da09cb0e | ||
|
|
cba3282541 | ||
|
|
621d34954a | ||
|
|
5fc45ad22f | ||
|
|
9ca8afb3ba | ||
|
|
175b2f8664 | ||
|
|
d6fbfe75bc | ||
|
|
bddc670eea | ||
|
|
08cf83de2a | ||
|
|
86184f8595 | ||
|
|
6775960241 | ||
|
|
2119d755ee | ||
|
|
ed9ca74b4f | ||
|
|
e2280dca39 | ||
|
|
deeaa90667 | ||
|
|
7d79d29e7e | ||
|
|
37d11ec303 | ||
|
|
4ed1a4bddb | ||
|
|
1324bc05e8 | ||
|
|
ca9a8173bd | ||
|
|
b98daed19c | ||
|
|
64e5444c06 | ||
|
|
dc1a77ee38 | ||
|
|
9ea068bf64 | ||
|
|
4940d8523c | ||
|
|
a1e78f1d17 | ||
|
|
fd8020dcc4 | ||
|
|
e31e8dec98 | ||
|
|
8103d7d31f | ||
|
|
f23b661a4b | ||
|
|
20691f8ab5 | ||
|
|
e1bd40dea3 | ||
|
|
17926775e7 | ||
|
|
79fac17bf7 | ||
|
|
303de4ae8a | ||
|
|
5caeafe2c3 | ||
|
|
481919bc03 | ||
|
|
558664760b | ||
|
|
f4e8c6ca51 | ||
|
|
01c5d50957 | ||
|
|
36c9c2616e | ||
|
|
2ab0c9cbeb | ||
|
|
d763f2de2f | ||
|
|
152f387939 | ||
|
|
9e5451b940 | ||
|
|
e1e7264bfc | ||
|
|
afb26fdb6f | ||
|
|
0a742e8c2f | ||
|
|
e14c42761d | ||
|
|
b6f7dc048f | ||
|
|
4efd89627d | ||
|
|
6f6c4acf7e | ||
|
|
30745c2be3 | ||
|
|
e257776852 | ||
|
|
502b0c4cc5 | ||
|
|
0d8c3a1e60 | ||
|
|
7254fbcd74 | ||
|
|
4d087f4aa9 | ||
|
|
874c8a9fd1 | ||
|
|
4d61670f38 | ||
|
|
00f90d1084 | ||
|
|
3cb50bc7fc | ||
|
|
213ef9d94b | ||
|
|
046b6266b3 | ||
|
|
8a03062e3d | ||
|
|
2a40f05e90 | ||
|
|
ce73e9293d | ||
|
|
f469a7e360 | ||
|
|
3c2feeb2aa | ||
|
|
fdb294bad9 | ||
|
|
56975f9375 | ||
|
|
cb74c0d389 | ||
|
|
33abe05737 | ||
|
|
61ca7fd0f6 | ||
|
|
21223cbcd4 | ||
|
|
d02d51717d | ||
|
|
004f3f184f | ||
|
|
99d2f6d48d | ||
|
|
11d1f5c00e | ||
|
|
ebd28f41a2 | ||
|
|
2397fcaa21 | ||
|
|
5b039b82a3 | ||
|
|
264f5d5068 | ||
|
|
eff5660f12 | ||
|
|
98915e158d | ||
|
|
9be71a5159 | ||
|
|
6b61bb05c0 | ||
|
|
4c389a4077 | ||
|
|
e6ebdd5be3 | ||
|
|
22eb7de7ea | ||
|
|
c4c5e21ce0 | ||
|
|
0c0ba0b6ca | ||
|
|
295a4cd1cd | ||
|
|
db3bec9e2b | ||
|
|
577a434e17 | ||
|
|
cac5aa2475 | ||
|
|
85fa73b9bf | ||
|
|
fdfea36614 | ||
|
|
2f663e0ea7 | ||
|
|
5d05af089b | ||
|
|
e237cd8be4 | ||
|
|
8bd09e58cb | ||
|
|
4e2a3cc5be | ||
|
|
d9c83379fe | ||
|
|
0818a3485a | ||
|
|
7fa1e16b5a | ||
|
|
acb750c18a | ||
|
|
72d8b5ea16 | ||
|
|
6238ed6b77 | ||
|
|
fa5bd92406 | ||
|
|
189fdb4555 | ||
|
|
caf151a0dd | ||
|
|
d35769dceb | ||
|
|
1031e8a55a | ||
|
|
a71dca1487 | ||
|
|
b80a249cf7 | ||
|
|
54d0e2c483 | ||
|
|
c91e5784ac | ||
|
|
48e80803f7 | ||
|
|
495a68893d | ||
|
|
41e1ed7bd1 | ||
|
|
7eb63db502 | ||
|
|
c6d0a2e2ad | ||
|
|
1a2da712ed | ||
|
|
36627bc188 | ||
|
|
f31fe6cbf0 | ||
|
|
9f8a857cef | ||
|
|
ac1fdb8474 | ||
|
|
fbf053ac2b | ||
|
|
c77338c65e | ||
|
|
42b0ea173d | ||
|
|
7c9defb85c | ||
|
|
6e5d183bf6 | ||
|
|
0ab00c24c5 | ||
|
|
c23763a2cf | ||
|
|
84b2d86054 | ||
|
|
ba766254f8 | ||
|
|
a02e36e13f | ||
|
|
8f34e8bb2d | ||
|
|
38cca54b7f | ||
|
|
7b44b5b7db | ||
|
|
3ed4ceb7a3 | ||
|
|
76e4023b37 | ||
|
|
7ff6d9e825 | ||
|
|
64d133f8f6 | ||
|
|
324d0e265e | ||
|
|
cec4addcad | ||
|
|
43605df266 | ||
|
|
4f03df097c | ||
|
|
72dc62e5dd | ||
|
|
3520c03797 | ||
|
|
fcbbe46861 | ||
|
|
4a398143e3 | ||
|
|
bbaaf74302 | ||
|
|
d3dd3c3d5d | ||
|
|
b7f287db82 | ||
|
|
4f2ddfa020 | ||
|
|
428ec8412f | ||
|
|
50991dfe92 | ||
|
|
63ba9f4fb9 | ||
|
|
efd0fd1f4a | ||
|
|
5a7767cf0e | ||
|
|
3948e17da2 | ||
|
|
4e1e6bd69a | ||
|
|
9333bc73ea | ||
|
|
3540a35a6c | ||
|
|
ee67ba729a | ||
|
|
8414961b15 | ||
|
|
f8de983e2b | ||
|
|
d40afa619b | ||
|
|
55e1d0fb6e | ||
|
|
2661e2cfa0 | ||
|
|
d4cb5c73aa | ||
|
|
9a2d7d1a19 | ||
|
|
017bccc937 | ||
|
|
dea683da7c | ||
|
|
496ab26972 | ||
|
|
c18eb948b4 | ||
|
|
0cd4b730d7 | ||
|
|
63ea5a3e5f | ||
|
|
33f5e8838b | ||
|
|
3660f3827f | ||
|
|
ac4cce1f9b | ||
|
|
532d2428b7 | ||
|
|
205ed8e30e | ||
|
|
4119626cb7 | ||
|
|
94fdca084a | ||
|
|
599c69c9bb | ||
|
|
7843691c4b | ||
|
|
d9effacb20 | ||
|
|
3efb0bf189 | ||
|
|
00eb927538 | ||
|
|
0616ce62eb | ||
|
|
a171de32d8 | ||
|
|
cf7680bc86 | ||
|
|
e07bb1b3c2 | ||
|
|
1f830b96b5 | ||
|
|
ff7585b69d | ||
|
|
715ee88f38 | ||
|
|
142c9ad3b7 | ||
|
|
e2280197b9 | ||
|
|
a74916d593 | ||
|
|
ad0e4a2099 | ||
|
|
2613d43f3c | ||
|
|
09c7f45c69 | ||
|
|
0eaeb748f4 | ||
|
|
b72191ae68 | ||
|
|
cf4bfc35ea | ||
|
|
69231ba57a | ||
|
|
d61fda9cff | ||
|
|
6ecf546baf | ||
|
|
ea8aa84009 | ||
|
|
353f1ca42c | ||
|
|
20053ad548 | ||
|
|
9b97e0dd87 | ||
|
|
8e304fa483 | ||
|
|
0f5e2e5a60 | ||
|
|
f5bd7db388 | ||
|
|
70832be810 | ||
|
|
7090c33a9d | ||
|
|
68ed6019f6 | ||
|
|
4638c3e1d9 | ||
|
|
53cb9a35ee | ||
|
|
9d80f21ae7 | ||
|
|
4d5653854a | ||
|
|
70cc8577e8 | ||
|
|
f80d5e6b52 | ||
|
|
19456e8be0 | ||
|
|
c98cedc20f | ||
|
|
2b1063c34d | ||
|
|
fc8be9c8fb | ||
|
|
70bdb07c1e | ||
|
|
51aba937f5 | ||
|
|
9363a15daa | ||
|
|
1ef5bfed94 | ||
|
|
e67fadef02 | ||
|
|
99825d10c4 | ||
|
|
a7b52f9a96 | ||
|
|
ef9d4d8525 | ||
|
|
2f751285ed | ||
|
|
4504a25272 | ||
|
|
aefc4698ab | ||
|
|
28af7353ea | ||
|
|
22a078b628 | ||
|
|
d8a8275723 | ||
|
|
d13b478c56 | ||
|
|
5ee146b6be | ||
|
|
d666bacf1f | ||
|
|
81662bf86b | ||
|
|
99901ed0ea | ||
|
|
18a96890ee | ||
|
|
3a4c72f1b8 | ||
|
|
19866010df | ||
|
|
e3e00bbd7c | ||
|
|
c4e3bfee6c | ||
|
|
d1c9f6f5dd | ||
|
|
58ccec1b46 | ||
|
|
8faa45b19f | ||
|
|
b2595e55cc | ||
|
|
f309df5971 | ||
|
|
7cdd90973b | ||
|
|
ccdbffb376 | ||
|
|
2eeb2a4454 | ||
|
|
1f894094c7 | ||
|
|
5f06de03a9 | ||
|
|
23e773ce64 | ||
|
|
3b34fe72b9 | ||
|
|
34f620c59b | ||
|
|
a5a5127088 | ||
|
|
b939d936e9 | ||
|
|
1b5e27a9b4 | ||
|
|
789c18307a | ||
|
|
1e4aa4b3a7 | ||
|
|
1bc0964aff | ||
|
|
ce663155c4 | ||
|
|
1ad46c1ba9 | ||
|
|
9901c8c3f5 | ||
|
|
b20b981a01 | ||
|
|
ff860df5c3 | ||
|
|
69072f8e50 | ||
|
|
53bf47f7cb | ||
|
|
61032710e8 | ||
|
|
00527e7cf3 | ||
|
|
0423a43650 | ||
|
|
2ba10655a8 | ||
|
|
c5989ea95d | ||
|
|
3f6c7a9c25 | ||
|
|
a95e3552ff | ||
|
|
ef707a9b30 | ||
|
|
be51ab52fb | ||
|
|
e0a25ea918 | ||
|
|
72ae258fa5 | ||
|
|
33d124e3f3 | ||
|
|
bc87f61bdc | ||
|
|
fe03cca72b | ||
|
|
2007113ed8 | ||
|
|
f89b08a577 | ||
|
|
288705950c | ||
|
|
3240e0c348 | ||
|
|
185c02f4ac | ||
|
|
f382aaf73c | ||
|
|
be88c992fa | ||
|
|
85ff25a63b | ||
|
|
4e65c62881 | ||
|
|
6d035f2a2d | ||
|
|
7a35f6bb24 | ||
|
|
c00e956909 | ||
|
|
cf3bf459f4 | ||
|
|
e82d109840 | ||
|
|
c9a84a1813 | ||
|
|
7186a94c27 | ||
|
|
45e4e98cb5 | ||
|
|
9fc31e7f39 | ||
|
|
983a37c77f | ||
|
|
a3b6a90fde | ||
|
|
b771d82100 | ||
|
|
9fa179ed9c | ||
|
|
14d83d4263 | ||
|
|
73ccad8a76 | ||
|
|
488dbbb336 | ||
|
|
08c8b69f4d | ||
|
|
cabb9b6c3b | ||
|
|
6697aa096a | ||
|
|
582725e7d7 | ||
|
|
476d618286 | ||
|
|
c186b6677b | ||
|
|
ea9ba84dc2 | ||
|
|
bf616494f1 | ||
|
|
0b54bc046d | ||
|
|
c8c1966b8a | ||
|
|
9ad1c91472 | ||
|
|
d8525f0eba | ||
|
|
7ae419716a | ||
|
|
b0185a9ae4 | ||
|
|
d2cdb18a57 | ||
|
|
f04df5e297 | ||
|
|
b90caaba85 | ||
|
|
d15bec08a3 | ||
|
|
ab473b12da | ||
|
|
83c444ce11 | ||
|
|
7c92aae61b | ||
|
|
3ade40f2d9 | ||
|
|
0debd2bbf0 | ||
|
|
1a3afc9661 | ||
|
|
ac4ebbe548 | ||
|
|
089414c9ff | ||
|
|
a1dbf0f2e5 | ||
|
|
712824d8a6 | ||
|
|
7491f463b4 | ||
|
|
8f08591ab9 | ||
|
|
b98586150f | ||
|
|
2f094801ca | ||
|
|
522fcda547 | ||
|
|
dd35f101fe | ||
|
|
8a7513afd0 | ||
|
|
2628ec00dc | ||
|
|
778e27a374 | ||
|
|
dd41eddd72 | ||
|
|
5872452a6a | ||
|
|
af05403846 | ||
|
|
3a55755721 | ||
|
|
24957c653d | ||
|
|
6a12518ac1 | ||
|
|
318e2924ca | ||
|
|
0da5d00f9c | ||
|
|
7612702d73 | ||
|
|
9ba91b2dcc | ||
|
|
c4db94e86f | ||
|
|
08492b943b | ||
|
|
a1bf8ca945 | ||
|
|
6d97eb308e | ||
|
|
64fe595b5f | ||
|
|
d82b385904 | ||
|
|
95201eb757 | ||
|
|
a387907604 | ||
|
|
f16eba4855 | ||
|
|
efdd68c2b8 | ||
|
|
e927b675a4 | ||
|
|
5d9373026b | ||
|
|
48922e5293 | ||
|
|
a55548d471 | ||
|
|
f5d5f8cf67 | ||
|
|
0060691b50 | ||
|
|
5b242989da | ||
|
|
3358094319 | ||
|
|
2f9bd00d70 | ||
|
|
ed23578dcf | ||
|
|
41ecbb62a2 | ||
|
|
32ef36d7f7 | ||
|
|
50936253de | ||
|
|
3c7b6eb5c3 | ||
|
|
c28fed0893 | ||
|
|
36910a0a8e | ||
|
|
63ed68b527 | ||
|
|
e3f9ef0117 | ||
|
|
5824ab6eb5 | ||
|
|
e6ae1ddec6 | ||
|
|
2213d23115 | ||
|
|
6393cdec9b | ||
|
|
a10f573a30 | ||
|
|
9dcce15790 | ||
|
|
481c9f067c | ||
|
|
1e64e49dc3 | ||
|
|
19a2ecd281 | ||
|
|
03b02669a4 | ||
|
|
c979f02ce4 | ||
|
|
bc86c1b8fc | ||
|
|
37d0a4aad2 | ||
|
|
ff70b5c546 | ||
|
|
7daec673ba | ||
|
|
f2d07a699a | ||
|
|
721511e484 | ||
|
|
2942660201 | ||
|
|
68811eb3fc | ||
|
|
468b7319d1 | ||
|
|
5c4da77357 | ||
|
|
009a11a9f5 | ||
|
|
7057422077 | ||
|
|
ecae16b5d4 | ||
|
|
d57df84a59 | ||
|
|
146da57ba3 | ||
|
|
fd94d162ea | ||
|
|
b5abd472b0 | ||
|
|
ee4ecc0b41 | ||
|
|
04fb1f243d | ||
|
|
e5ccfa3a50 | ||
|
|
c642a35fb3 | ||
|
|
2fe353377b | ||
|
|
de1017a20a | ||
|
|
e2cd7d9f07 | ||
|
|
c3bfd1e8bf | ||
|
|
051773a084 | ||
|
|
6a2e39355c | ||
|
|
e367cb2152 | ||
|
|
bcbf596aa8 | ||
|
|
d88b04783d | ||
|
|
6d219aa701 | ||
|
|
da32845dd1 | ||
|
|
4073536d96 | ||
|
|
21f08c97a1 | ||
|
|
06223d576d | ||
|
|
5c7a9c92d1 | ||
|
|
0e8348f360 | ||
|
|
8060ed5f8e | ||
|
|
e8135fcbb4 | ||
|
|
7fccb7e03e | ||
|
|
715ddf2b8c | ||
|
|
717a5886cf | ||
|
|
407232c708 | ||
|
|
edd902397e | ||
|
|
24f5fcb5a0 | ||
|
|
8f6270723e | ||
|
|
9cccd2d74e | ||
|
|
ed34b65dbd | ||
|
|
4484cc7d16 | ||
|
|
8677994fb7 | ||
|
|
ea555eb410 | ||
|
|
e140b656a6 | ||
|
|
6423d909d7 | ||
|
|
8887961d09 | ||
|
|
4ee891a3ba | ||
|
|
96b976fd4a | ||
|
|
1b9d46f834 | ||
|
|
ba600f5da6 | ||
|
|
03e74afe80 | ||
|
|
b0c1a5a6b1 | ||
|
|
3af43164f4 | ||
|
|
2a340b7a65 | ||
|
|
2f27aeb77f | ||
|
|
e394539742 | ||
|
|
1a0f29b6ef | ||
|
|
39b160e202 | ||
|
|
aa065fa4d8 | ||
|
|
19ca1db674 | ||
|
|
bd416233eb | ||
|
|
7ca9d601a0 | ||
|
|
ac8988ad41 | ||
|
|
90fdc71279 | ||
|
|
5d126ff14d | ||
|
|
38e098f6c4 | ||
|
|
d3fa8a54ae | ||
|
|
a58384d8d1 | ||
|
|
8e1951ba67 | ||
|
|
3dba731dd7 | ||
|
|
f8f19efcaa | ||
|
|
980fdf5ad1 | ||
|
|
443094d282 | ||
|
|
e727f1749f | ||
|
|
1224a54884 | ||
|
|
897e7dccc6 | ||
|
|
88631ed7a8 | ||
|
|
65f4094b5a | ||
|
|
99656357b1 | ||
|
|
d33ae2a50a | ||
|
|
f419430c6b | ||
|
|
e127855d84 | ||
|
|
8ffea70b2f | ||
|
|
3fbddd2e41 | ||
|
|
5a17697e7e | ||
|
|
6f66fdc6d6 | ||
|
|
a29fdb43c9 | ||
|
|
7462e66858 | ||
|
|
d9364cf60a | ||
|
|
b0375bddd1 | ||
|
|
56795afabb | ||
|
|
acf9f464f0 | ||
|
|
74c615f156 | ||
|
|
133af4ea2c | ||
|
|
4182c79354 | ||
|
|
759d986188 | ||
|
|
600ca90fc0 | ||
|
|
3b52888877 | ||
|
|
e23120a4c6 | ||
|
|
38d47f6aa1 | ||
|
|
3a25123d7b | ||
|
|
19c04e125a | ||
|
|
8a13387874 | ||
|
|
6c813ddab1 | ||
|
|
965870f8ed | ||
|
|
8add76fb50 | ||
|
|
af4ec3d096 | ||
|
|
b908cd7cbd | ||
|
|
6309ec0bfa | ||
|
|
45d1bef302 | ||
|
|
7d9e1aad83 | ||
|
|
aa2d1f3bc9 | ||
|
|
f6bd1ef513 | ||
|
|
c75ac3c0f5 | ||
|
|
ac2d6fe9a8 | ||
|
|
40d120d875 | ||
|
|
5e2fdcf1e9 | ||
|
|
57c8c24b20 | ||
|
|
460d3fe111 | ||
|
|
1d50cbf684 | ||
|
|
3a250d3da4 | ||
|
|
d05b819ff2 | ||
|
|
fcb3f9ca26 | ||
|
|
69b42ee6e0 | ||
|
|
77973f0037 | ||
|
|
7f8f39916d | ||
|
|
cc8bf6744b | ||
|
|
bf17d6894f | ||
|
|
900f5b136f | ||
|
|
194a9c0c40 | ||
|
|
f6f9b768cc | ||
|
|
8fe0148821 | ||
|
|
28ed2fe8f2 | ||
|
|
131df2d82a | ||
|
|
0f5ec6c40c | ||
|
|
39cbadb100 | ||
|
|
0afb503860 | ||
|
|
ed1c589e2d | ||
|
|
864cc7a7bb | ||
|
|
657a374895 | ||
|
|
35e1ce0df2 | ||
|
|
2065ff80ff | ||
|
|
b24bba87d9 | ||
|
|
5583fc76f3 | ||
|
|
e810445ac9 | ||
|
|
5afbb4ee4e | ||
|
|
6ae4cd143c | ||
|
|
978329fdc9 | ||
|
|
f754ecd6c3 | ||
|
|
62a827ce49 | ||
|
|
e971da2b59 | ||
|
|
2d092cb290 | ||
|
|
5b66ecb06f | ||
|
|
22a9799674 | ||
|
|
7c813be13a | ||
|
|
e94148b2f0 | ||
|
|
ec4f6e4327 | ||
|
|
fcbd117784 | ||
|
|
dab716a9e0 | ||
|
|
9265e25c73 | ||
|
|
4ad63d5bce | ||
|
|
f89c897488 | ||
|
|
521ff5e7e3 | ||
|
|
89d8cb3b0a | ||
|
|
a0e92b54d0 | ||
|
|
62f549f038 | ||
|
|
e8f3b0c8d0 | ||
|
|
587ce78f4e | ||
|
|
7ca1dd3c68 | ||
|
|
58543f0b4d | ||
|
|
e88253f364 | ||
|
|
bdf37d8fe7 | ||
|
|
22908207a3 | ||
|
|
7239b89108 | ||
|
|
0ea80bd758 | ||
|
|
f6d623ace3 | ||
|
|
63ad8b3411 | ||
|
|
50cc757a5c | ||
|
|
70430f84e1 | ||
|
|
80db261c88 | ||
|
|
5631ef7be7 | ||
|
|
2745a4d6c1 | ||
|
|
33190b5c89 | ||
|
|
354a5832e4 | ||
|
|
f57c0f0886 | ||
|
|
954a393fce | ||
|
|
01dbac78ce | ||
|
|
594ea8ab59 | ||
|
|
033493a31b | ||
|
|
e79b099633 | ||
|
|
5bc948ab0a | ||
|
|
28c5d7d84a | ||
|
|
b9cfc2e6af |
@@ -6,7 +6,7 @@ version: 2
|
||||
jobs:
|
||||
build:
|
||||
docker:
|
||||
- image: circleci/node:8.9
|
||||
- image: circleci/node:12.16.3
|
||||
- image: circleci/mongo:3.4-jessie
|
||||
|
||||
working_directory: ~/repo
|
||||
@@ -30,4 +30,3 @@ jobs:
|
||||
|
||||
# run tests!
|
||||
- run: npm run circleci
|
||||
|
||||
|
||||
15
.eslintrc.js
15
.eslintrc.js
@@ -1,7 +1,7 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
root : true,
|
||||
parserOptions : {
|
||||
ecmaVersion : 9,
|
||||
ecmaVersion : 2021,
|
||||
sourceType : 'module',
|
||||
ecmaFeatures : {
|
||||
jsx : true
|
||||
@@ -9,6 +9,7 @@ module.exports = {
|
||||
},
|
||||
env : {
|
||||
browser : true,
|
||||
node : true
|
||||
},
|
||||
plugins : ['react'],
|
||||
rules : {
|
||||
@@ -31,7 +32,7 @@ module.exports = {
|
||||
skipBlankLines : true,
|
||||
}],
|
||||
'max-depth' : ['warn', { max: 4 }],
|
||||
'max-params' : ['warn', { max: 4 }],
|
||||
'max-params' : ['warn', { max: 5 }],
|
||||
'no-restricted-syntax' : ['warn', 'ClassDeclaration', 'SwitchStatement'],
|
||||
'no-unused-vars' : ['warn', {
|
||||
vars : 'all',
|
||||
@@ -47,14 +48,14 @@ module.exports = {
|
||||
'no-var' : 'warn',
|
||||
'prefer-const' : 'warn',
|
||||
'prefer-template' : 'warn',
|
||||
'quotes' : ['warn', 'single', { 'allowTemplateLiterals': true } ],
|
||||
'quotes' : ['warn', 'single', { 'allowTemplateLiterals': true }],
|
||||
'semi' : ['warn', 'always'],
|
||||
|
||||
/** Whitespace **/
|
||||
'array-bracket-spacing' : ['warn', 'never'],
|
||||
'arrow-spacing' : ['warn', { before: false, after: false }],
|
||||
'comma-spacing' : ['warn', { before: false, after: true }],
|
||||
'indent' : ['warn', 'tab'],
|
||||
'indent' : ['warn', 'tab', { 'MemberExpression': 'off' }],
|
||||
'keyword-spacing' : ['warn', {
|
||||
before : true,
|
||||
after : true,
|
||||
@@ -66,7 +67,7 @@ module.exports = {
|
||||
multiLine : { beforeColon: true, afterColon: true, align: 'colon' },
|
||||
singleLine : { beforeColon: false, afterColon: true }
|
||||
}],
|
||||
'linebreak-style' : ['warn', 'unix'],
|
||||
'linebreak-style' : 'off',
|
||||
'no-trailing-spaces' : 'warn',
|
||||
'no-whitespace-before-property' : 'warn',
|
||||
'object-curly-spacing' : ['warn', 'always'],
|
||||
@@ -74,4 +75,4 @@ module.exports = {
|
||||
'space-in-parens' : ['warn', 'never'],
|
||||
'template-curly-spacing' : ['warn', 'never'],
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
69
.github/dependabot.yml
vendored
Normal file
69
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,69 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: npm
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: daily
|
||||
open-pull-requests-limit: 99
|
||||
ignore:
|
||||
- dependency-name: eslint
|
||||
versions:
|
||||
- 7.19.0
|
||||
- 7.22.0
|
||||
- 7.23.0
|
||||
- 7.24.0
|
||||
- dependency-name: "@babel/core"
|
||||
versions:
|
||||
- 7.12.13
|
||||
- 7.12.16
|
||||
- 7.12.17
|
||||
- 7.13.13
|
||||
- 7.13.14
|
||||
- 7.13.15
|
||||
- dependency-name: googleapis
|
||||
versions:
|
||||
- 68.0.0
|
||||
- 70.0.0
|
||||
- 71.0.0
|
||||
- dependency-name: "@babel/preset-env"
|
||||
versions:
|
||||
- 7.12.13
|
||||
- 7.12.16
|
||||
- 7.12.17
|
||||
- 7.13.0
|
||||
- 7.13.12
|
||||
- 7.13.8
|
||||
- dependency-name: mongoose
|
||||
versions:
|
||||
- 5.11.14
|
||||
- 5.11.15
|
||||
- 5.11.16
|
||||
- 5.11.17
|
||||
- 5.11.18
|
||||
- 5.11.19
|
||||
- 5.12.1
|
||||
- 5.12.2
|
||||
- 5.12.3
|
||||
- dependency-name: eslint-plugin-react
|
||||
versions:
|
||||
- 7.23.0
|
||||
- 7.23.1
|
||||
- dependency-name: query-string
|
||||
versions:
|
||||
- 7.0.0
|
||||
- dependency-name: nanoid
|
||||
versions:
|
||||
- 3.1.22
|
||||
- dependency-name: "@babel/preset-react"
|
||||
versions:
|
||||
- 7.13.13
|
||||
- dependency-name: codemirror
|
||||
versions:
|
||||
- 5.59.3
|
||||
- 5.60.0
|
||||
- dependency-name: classnames
|
||||
versions:
|
||||
- 2.3.0
|
||||
- dependency-name: marked
|
||||
versions:
|
||||
- 1.2.8
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -6,6 +6,8 @@ storage
|
||||
*.log
|
||||
build/*
|
||||
config/local.*
|
||||
config/docker.*
|
||||
|
||||
todo.md
|
||||
startDB.bat
|
||||
todo.md
|
||||
startDB.bat
|
||||
startMViewer.bat
|
||||
|
||||
19
Dockerfile
19
Dockerfile
@@ -1,14 +1,19 @@
|
||||
FROM node:8
|
||||
FROM node:14.15
|
||||
|
||||
ENV NODE_ENV=docker
|
||||
|
||||
# Create app directory
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
# Bundle app source
|
||||
# Copy package.json into the image, then run yarn install
|
||||
# This improves caching so we don't have to download the dependencies every time the code changes
|
||||
COPY package.json ./
|
||||
# --ignore-scripts tells yarn not to run postbuild. We run it explicitly later
|
||||
RUN yarn install --ignore-scripts
|
||||
|
||||
# Bundle app source and build application
|
||||
COPY . .
|
||||
|
||||
ENV NODE_ENV=docker
|
||||
|
||||
RUN yarn
|
||||
RUN yarn build
|
||||
|
||||
EXPOSE 8000
|
||||
CMD [ "yarn", "start" ]
|
||||
CMD [ "yarn", "start" ]
|
||||
|
||||
12
README.DOCKER.md
Normal file
12
README.DOCKER.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# Running Homebrewery via Docker
|
||||
|
||||
The repo includes a Dockerfile and a docker-compose.yml file.
|
||||
|
||||
To run the application via docker-compose.yml:
|
||||
`docker-compose up -d`
|
||||
|
||||
To stop the application:
|
||||
`docker-compose down`
|
||||
|
||||
To stop the application and remove all data:
|
||||
`docker-compose down -v`
|
||||
35
README.FREEBSD.md
Normal file
35
README.FREEBSD.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# FreeBSD/FreeNAS Installation Instructions
|
||||
|
||||
## Before Installing
|
||||
|
||||
These instructions assume that you are installing to a completely new, fresh FreeBSD/FreeNAS jail. As such, some steps will not be necessary if you are installing to an existing FreeBSD/FreeNAS install.
|
||||
|
||||
## Installation instructions
|
||||
|
||||
1. Create a new jail, with the appropriate network settings to access the internet.
|
||||
|
||||
2. Install wget (`pkg install -y wget`). On a fresh jail, you will be prompted to press 'Y' to set up `pkg`.
|
||||
|
||||
3. Download the installation script (`wget --no-check-certificate https://raw.githubusercontent.com/naturalcrit/homebrewery/master/freebsd/install.sh`). The parameter `--no-check-certificate` is required as we haven't set up any trusted certificates/authorities yet.
|
||||
|
||||
4. Make the downloaded file executable (`chmod +x install.sh`).
|
||||
|
||||
5. Run the script (`./install.sh`). This will automatically download all of the required packages, install both them and HomeBrewery, configure the system and finally start HomeBrewery.
|
||||
|
||||
**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 FreeBSD/FreeNAS platforms:
|
||||
|
||||
* FreeNAS-11.3-U5; Jail 11.4-RELEASE-p2
|
||||
|
||||
## Final Notes
|
||||
|
||||
While this installation process works successfully at the time of writing (December 28, 2020), it relies on all of the Node.JS packages used in the HomeBrewery project retaining their cross-platform capabilities to continue to function under FreeBSD. This is one of the inherent advantages of Node.JS, but it is by no means guaranteed and as such, functionality or even installation under FreeBSD may fail without warning at some point in the future.
|
||||
|
||||
Regards,
|
||||
G
|
||||
December 28, 2020
|
||||
105
README.md
105
README.md
@@ -1,41 +1,93 @@
|
||||
# The Homebrewery
|
||||
The Homebrewery is a tool for making authentic looking [D&D content](https://dnd.wizards.com/products/tabletop-games/rpg-products/rpg_playershandbook) using [Markdown](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet). It is distributed under the terms of the [MIT License](./license).
|
||||
|
||||
[](https://app.circleci.com/pipelines/github/naturalcrit/homebrewery?branch=master)
|
||||
|
||||
The Homebrewery is a tool for making authentic looking [D&D content][dnd-content-url]
|
||||
using [Markdown][markdown-url]. It is distributed under the terms of the [MIT License](./license).
|
||||
|
||||
[dnd-content-url]: https://dnd.wizards.com/products/tabletop-games/rpg-products/rpg_playershandbook
|
||||
[markdown-url]: https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet
|
||||
|
||||
## Quick Start
|
||||
The easiest way to get started using the Homebrewery is to use it [on our website](https://homebrewery.naturalcrit.com). The code is open source, so feel free to clone it, tinker with it. If you want to make changes to the code, you can run your own local version for testing by following the installation instructions below.
|
||||
The easiest way to get started using the Homebrewery is to use it
|
||||
[on our website][homebrewery-url]. The code is open source, so feel free to
|
||||
clone it, tinker with it. If you want to make changes to the code, you can run
|
||||
your own local version for testing by following the installation instructions
|
||||
below.
|
||||
|
||||
[homebrewery-url]: https://homebrewery.naturalcrit.com
|
||||
|
||||
### Installation
|
||||
First, install two programs that the Homebrewery requires to run.
|
||||
First, install three programs that the Homebrewery requires to run and retrieve
|
||||
updates:
|
||||
|
||||
1. install [node](https://nodejs.org/en/)
|
||||
1. install [mongodb](https://www.mongodb.com/)
|
||||
1. install [mongodb](https://www.mongodb.com/try/download/community) (Community version)
|
||||
|
||||
Second, download a copy of the repository. If you have git you can do so with
|
||||
For easiest installation, follow these steps:
|
||||
1. In the installer, uncheck the option to run as a service
|
||||
1. You can install MongoDB Compass if you want a GUI to view your database documents
|
||||
1. Go to the C drive and create a folder called "data"
|
||||
1. Inside the "data" folder, create a new folder called "db"
|
||||
1. Open a command prompt or other terminal and navigate to your mongodb install folder (c:program files\mongo\server\4.4\bin)
|
||||
1. In the command prompt, run "mongod", which will start up your local database server
|
||||
1. While MongoD is running, open a second command prompt and navigate to the mongodb install folder
|
||||
1. In the second command prompt, run "mongo", which allows you to edit the database
|
||||
1. Type `use homebrewery` to create the homebrewery database. You should see `switched to db homebrewery`
|
||||
1. Type `db.brews.insert({"title":"test"})` to create a blank document. You should see `WriteResult({ "nInserted" : 1 })`
|
||||
1. Search in Windows for "Advanced system settings" and open it
|
||||
1. Click "Environment variables", find the "path" variable, and double-click to open it
|
||||
1. Click "New" and paste in the path to the mongodb "bin" folder
|
||||
1. Click "OK", "OK", "OK" to close all the windows
|
||||
1. install [git](https://git-scm.com/downloads) (select the option that allows Git to run from the command prompt)
|
||||
|
||||
Checkout the repo ([documentation][github-clone-repo-docs-url]):
|
||||
```
|
||||
git clone https://github.com/naturalcrit/homebrewery.git
|
||||
```
|
||||
|
||||
Third, you will need to add the environment variable `NODE_ENV = local` to allow the project to run locally.
|
||||
[github-clone-repo-docs-url]: https://docs.github.com/en/free-pro-team@latest/github/creating-cloning-and-archiving-repositories/cloning-a-repository
|
||||
|
||||
Second, you will need to add the environment variable `NODE_ENV=local` to allow
|
||||
the project to run locally.
|
||||
|
||||
You can set this temporarily in your shell of choice:
|
||||
* Windows Powershell: `$env:NODE_ENV="local"`
|
||||
* Windows CMD: `set NODE_ENV=local`
|
||||
* Linux / OSX: `export NODE_ENV=local`
|
||||
|
||||
Fourth, you will need to install the program and run it using the two commands:
|
||||
Third, you will need to install the Node dependencies, compile the app, and run
|
||||
it using the two commands:
|
||||
|
||||
1. `npm install`
|
||||
1. `npm start`
|
||||
|
||||
You should now be able to go to [http://localhost:8000](http://localhost:8000) in your browser and use the Homebrewery offline.
|
||||
You should now be able to go to [http://localhost:8000](http://localhost:8000)
|
||||
in your browser and use the Homebrewery offline.
|
||||
|
||||
### Running the application via Docker
|
||||
|
||||
Please see the docs here: [README.DOCKER.md](./README.DOCKER.md)
|
||||
|
||||
### Running the application on FreeBSD or FreeNAS
|
||||
|
||||
Please see the docs here: [README.FreeBSD.md](./README.FREEBSD.md)
|
||||
|
||||
### Standalone PHB Stylesheet
|
||||
If you just want the stylesheet that is generated to make pages look like they are from the Player's Handbook, you will find it in the [phb.standalone.css](./phb.standalone.css) file.
|
||||
If you just want the stylesheet that is generated to make pages look like they
|
||||
are from the Player's Handbook, you will find it in the
|
||||
[phb.standalone.css](./phb.standalone.css) file.
|
||||
|
||||
If you are developing locally and would like to generate your own, follow the above steps and then run `npm run phb`.
|
||||
If you are developing locally and would like to generate your own, follow the
|
||||
above steps and then run `npm run phb`.
|
||||
|
||||
## Issues, Suggestions, and Bugs
|
||||
If you run into any issues using The Homebrewery or have suggestions for improvement, please submit an issue [on GitHub](/issues). You can also get help for issues on the subreddit [r/homebrewery](https://www.reddit.com/r/homebrewery)
|
||||
If you run into any issues using The Homebrewery or have suggestions for
|
||||
improvement, please submit an issue [on GitHub][repo-issues-url].
|
||||
You can also get help for issues on the subreddit [r/homebrewery][subreddit-url]
|
||||
|
||||
[repo-issues-url]: https://github.com/naturalcrit/homebrewery/issues
|
||||
[subreddit-url]: https://www.reddit.com/r/homebrewery
|
||||
|
||||
## Changelog
|
||||
|
||||
@@ -43,6 +95,33 @@ You can check out the [changelog](./changelog.md).
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the [MIT license](./license). Which means you are free to use The Homebrewery in any way that you want, except for claiming that you made it yourself.
|
||||
This project is licensed under the [MIT license](./license). Which means you
|
||||
are free to use The Homebrewery in 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.
|
||||
|
||||
## Contributing
|
||||
|
||||
You are welcome to contribute to the development and maintenance of the
|
||||
project! There are several ways of doing that:
|
||||
- At the moment, we have a huge backlog of [issues][repo-issues-url] and some
|
||||
of them are outdated, duplicates or doesn't contain any useful info. In order
|
||||
to help you can [mark duplicates][github-mark-duplicate-url], try to
|
||||
reproduce some complex or weird issues, try with finding a workaround for a
|
||||
reported bug or just mention issue managers team to let them know about
|
||||
outdated issue via `@naturalcrit/issue-managers`.
|
||||
- Our [subreddit][subreddit-url] is constantly growing and there are number of
|
||||
bug reports: any help with sorting them out is very welcome.
|
||||
- And of course you can contribute by fixing a bug or implementing a new
|
||||
feature by yourself, we are waiting for your
|
||||
[pull requests][github-pr-docs-url]!
|
||||
|
||||
Anyway, if you would like to get in touch with the team and discuss/coordinate
|
||||
your contribution to the project, please join our [gitter chat][gitter-url].
|
||||
|
||||
[github-mark-duplicate-url]: https://docs.github.com/en/free-pro-team@latest/github/managing-your-work-on-github/about-duplicate-issues-and-pull-requests
|
||||
[github-pr-docs-url]: https://docs.github.com/en/free-pro-team@latest/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request
|
||||
[gitter-url]: https://gitter.im/naturalcrit/Lobby
|
||||
|
||||
397
changelog.md
397
changelog.md
@@ -1,5 +1,363 @@
|
||||
```css
|
||||
h5 {
|
||||
font-size: .35cm !important;
|
||||
}
|
||||
|
||||
.taskList li {
|
||||
list-style-type : none;
|
||||
}
|
||||
|
||||
.taskList li input {
|
||||
margin-left : -0.52cm;
|
||||
transform: translateY(.05cm);
|
||||
filter: brightness(1.1) drop-shadow(1px 2px 1px #222);
|
||||
}
|
||||
|
||||
.taskList li input[checked] {
|
||||
filter: sepia(100%) hue-rotate(60deg) saturate(3.5) contrast(4) brightness(1.1) drop-shadow(1px 2px 1px #222);
|
||||
}
|
||||
|
||||
pre + * {
|
||||
margin-top: 0.17cm;
|
||||
}
|
||||
|
||||
pre {
|
||||
margin-top: 0.17cm;
|
||||
}
|
||||
|
||||
.page p + pre {
|
||||
margin-top : 0.1cm;
|
||||
}
|
||||
```
|
||||
|
||||
# changelog
|
||||
|
||||
### Saturday, 11/09/2021 - v3.0.0
|
||||
|
||||
We have been working on v3 for a *very* long time. We want to thank everyone for being paitent.
|
||||
|
||||
|
||||
Some features planned for V3 have actually been released over the recent months as part of V2, and some are still on the way. But at its core, V3 provides brand new Markdown-to-Brew rendering system, which was no simple task. This has opened up access to all sorts of bugfixes, tweaks, and potential for new features that just wouldn't be possible on the old system.
|
||||
|
||||
***BE WARNED:*** As we continue to develop V3, expect small tweaks in the styling, fonts, and snippets; your brews may look slightly different from day-to-day; some things might break completely while we tackle any bugs in this early stage. All of your old documents will continue to work as normal. We are not touching them. If you don't want to deal With the possibility of slight formatting changes, you may choose to stick with the Legacy renderer on any of your brews for as long as you like. However, most new features added from now on will only be available for brews using the V3 renderer.
|
||||
|
||||
Massive changelog incoming:
|
||||
|
||||
#### Markdown+
|
||||
With the latest major update to *The Homebrewery*, 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. This should hopefully aid non-coders with readability, and also allows us a few tricks in the background to fix some old issues. No raw HTML tags should be needed in a brew, and going forward, raw HTML will no longer receive debugging support (*but can still be used if you insist*).
|
||||
|
||||
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. Much of the syntax and styling has changed in V3, so code in one version may be broken in the other.
|
||||
|
||||
Visit [this page](/v3_preview) for brief examples of the new syntax!
|
||||
|
||||
#### Extended Markdown Syntax:
|
||||
|
||||
{{taskList
|
||||
* [x] Add Divs and Spans for all your custom styling needs, via a simplified Markdown-like syntax:
|
||||
```
|
||||
{{myDivClass,#myId,color:red
|
||||
My Div content
|
||||
}}
|
||||
|
||||
Hello {{mySpan,color:blue World}} !
|
||||
```
|
||||
|
||||
Fixes issues: [#348](https://github.com/naturalcrit/homebrewery/issues/348)
|
||||
}}
|
||||
|
||||
\column
|
||||
|
||||
{{taskList
|
||||
* [x] Add inline CSS to Markdown objects via "curly injection" syntax:
|
||||
```
|
||||
Hello *world*{myClass,#id,color:red}
|
||||
```
|
||||
Fixes issues: [#403](https://github.com/naturalcrit/homebrewery/issues/403)
|
||||
|
||||
* [x] Rowspan, Colspan, and multiple header rows with extended table syntax:
|
||||
```
|
||||
| Header 1a | Header 1b | Header 1c |
|
||||
| Header 2a | Header 2b | Header 2c |
|
||||
|:---------:|:----------|:---------:|
|
||||
| Span 2 columns || Span 2 |
|
||||
| one col | one col | rows ^|
|
||||
```
|
||||
Fixes issues: [#773](https://github.com/naturalcrit/homebrewery/issues/773), [#191](https://github.com/naturalcrit/homebrewery/issues/191)
|
||||
|
||||
* [x] Hanging indents via `<dl>` tags, as seen in the **PHB → Spell** snippet. Add via "double-colon" syntax:
|
||||
```
|
||||
Term :: big long definition that bleeds onto multiple lines
|
||||
```
|
||||
Fixes issues: [#182](https://github.com/naturalcrit/homebrewery/issues/182), [#149](https://github.com/naturalcrit/homebrewery/issues/149)
|
||||
|
||||
* [x] Easier vertical spacing via colons alone on a line:
|
||||
```
|
||||
:::
|
||||
```
|
||||
Fixes issues: [#374](https://github.com/naturalcrit/homebrewery/issues/374)
|
||||
|
||||
* [x] Avoid paragraph indendation by ending the previous paragraph with a backslash `\` or two spaces ` `
|
||||
```
|
||||
Paragraph one\
|
||||
Paragraph two
|
||||
```
|
||||
Fixes issues: [#636](https://github.com/naturalcrit/homebrewery/issues/636)
|
||||
|
||||
* [x] Code blocks can be inserted by surrounding it with rows of three backticks ` ``` `, for demonstration purposes or to share custom styles. Inline-code can be inserted with single backticks <code>`code`</code>
|
||||
<pre><code>```
|
||||
Here is some code!
|
||||
```
|
||||
</code></pre>
|
||||
|
||||
Fixes issues: [#465](https://github.com/naturalcrit/homebrewery/issues/465)
|
||||
|
||||
#### New and Fixed Snippets
|
||||
|
||||
* [x] Column breaks now use `\column` instead of ` ``` ` backticks.
|
||||
|
||||
Fixes issues: [#607](https://github.com/naturalcrit/homebrewery/issues/607)
|
||||
|
||||
* [x] Page breaks using `\page` now only trigger when placed alone at the start of a line.
|
||||
|
||||
Fixes issues: [#1147](https://github.com/naturalcrit/homebrewery/issues/1147)
|
||||
}}
|
||||
|
||||
\page
|
||||
{{taskList
|
||||
* [x] New **EDITOR → QR Code** snippet.
|
||||
|
||||
Fixes issues: [#538](https://github.com/naturalcrit/homebrewery/issues/538)
|
||||
|
||||
* [x] New **IMAGES → Watercolor Splatter** snippet, which adds one of a range of stylish stains to your brew.
|
||||
|
||||
* [x] New **IMAGES → Watermark** snippet, which adds transparent text diagonally across the page.
|
||||
|
||||
* [x] New **PHB → Magic Item** snippet.
|
||||
|
||||
Fixes issues: [#671](https://github.com/naturalcrit/homebrewery/issues/671)
|
||||
|
||||
* [x] New **TABLES → 1/3 Class Table** snippet for 1/3 casters.
|
||||
|
||||
Fixes issues: [#191](https://github.com/naturalcrit/homebrewery/issues/191)
|
||||
|
||||
* [x] Improved **EDITOR → Table of Contents** snippet to actually look like the PHB style. Will auto-generate based on the headers in your brew.
|
||||
|
||||
Fixes issues: [#304](https://github.com/naturalcrit/homebrewery/issues/304)
|
||||
|
||||
* [x] Improved **PHB → Monster Stat Block** snippet with textures, and an option to remove the frame entirely.
|
||||
|
||||
* [x] Improved **PHB → Spell List** snippet can now be made single-column.
|
||||
|
||||
Fixes issues: [#509](https://github.com/naturalcrit/homebrewery/issues/509), [#914](https://github.com/naturalcrit/homebrewery/issues/914)
|
||||
|
||||
* [x] Improved **TABLES → Class Table** snippet is now cleaned up, has an option to remove the frame entirely, and includes additional boundary decorations.
|
||||
|
||||
Fixes issues: [#773](https://github.com/naturalcrit/homebrewery/issues/773), [#302](https://github.com/naturalcrit/homebrewery/issues/302)
|
||||
|
||||
#### Miscellaneous Formatting Fixes
|
||||
|
||||
* [x] Paragraphs are now able to split across columns.
|
||||
|
||||
Fixes issues: [#239](https://github.com/naturalcrit/homebrewery/issues/239)
|
||||
|
||||
* [x] Multiple fixes for bold/italicize using asterisks `* *`
|
||||
|
||||
Fixes issues: [#1321](https://github.com/naturalcrit/homebrewery/issues/1321), [#852](https://github.com/naturalcrit/homebrewery/issues/852)
|
||||
|
||||
* [x] Multiple for list items not displaying correctly.
|
||||
|
||||
Fixes issues: [#1085](https://github.com/naturalcrit/homebrewery/issues/1085), [#588](https://github.com/naturalcrit/homebrewery/issues/588)
|
||||
|
||||
* [x] "Smart quotes", so left and right quotes are different.
|
||||
|
||||
Fixes issues: [#849](https://github.com/naturalcrit/homebrewery/issues/849)
|
||||
|
||||
* [x] Long URLs in links now wrap properly.
|
||||
|
||||
Fixes issues: [#1136](https://github.com/naturalcrit/homebrewery/issues/1136)
|
||||
|
||||
* [x] Better support for `wide` blocks that span across the whole page! No more problems with contents getting shunted off the edge, and each new wide element in a page will restart the next item back at column one. Manual `\column` breaks will help organize subsequent content between the columns as needed.
|
||||
|
||||
Fixes issues: [#144](https://github.com/naturalcrit/homebrewery/issues/144), [#1024](https://github.com/naturalcrit/homebrewery/issues/1024)
|
||||
|
||||
* [x] Fonts now support a wider range of latin characters for non-English brews, including áéíóúñ¡¿, etc...
|
||||
|
||||
Fixes issues: [#116](https://github.com/naturalcrit/homebrewery/issues/116)
|
||||
|
||||
* [x] Drop-caps (fancy first letters) have been re-styled and re-aligned to correct the ugly overlapping and cut-off on some characters like K and Y.
|
||||
|
||||
Fixes issues: [#848](https://github.com/naturalcrit/homebrewery/issues/848)
|
||||
}}
|
||||
|
||||
\column
|
||||
|
||||
### Under-the-Hood Stuff
|
||||
We had to make a whole lot of background upgrades and changes to get all of this working, and now that the framework is in place, there's a lot more planned and upcoming *"sometime"* :
|
||||
|
||||
{{taskList
|
||||
* [ ] New Themes to style your brews. DMG, MM, a custom Homebrewery theme, and others.
|
||||
* [ ] The ability to build your own custom themes using CSS, apply it to other brews, and share it with others!
|
||||
* [ ] Easy control of item colors. Change your monster blocks, tables, and notes from yellow to green to red!
|
||||
* [ ] New image-based snippets, including handwritten notes, title illustrations, and alternative decorations.
|
||||
* [ ] New fun fonts like Elvish, Draconic, Orcish, etc.
|
||||
* [ ] Better organization of personal brews using tags.
|
||||
* [ ] ....a log-out button...?
|
||||
* [ ] AND MORE.
|
||||
}}
|
||||
|
||||
### Interface
|
||||
::
|
||||
#### 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.
|
||||
|
||||
|
||||
|
||||
\page
|
||||
### Thursday, 09/09/2021 - v2.13.5
|
||||
- Slightly better error logging and messages for users.
|
||||
|
||||
##### G-Ambatte :
|
||||
- Added a search bar to the User page to help find your brews.
|
||||
- Added page counts to brews in the User page; page count will be updated the next time a brew is edited.
|
||||
- Fixed edge case where view counts could get reset.
|
||||
- Fixed edge case where last-modified time was not accurate for Google Doc brews.
|
||||
|
||||
##### Gazook89 :
|
||||
- Fixed typo in the **PRINT → Ink-Friendly** snippet.
|
||||
|
||||
|
||||
|
||||
### Tuesday, 17/08/2021 - v2.13.4
|
||||
- Fixed User page crashing when user has an untitled brew
|
||||
|
||||
##### G-Ambatte:
|
||||
- Tweaks to user page tool tips
|
||||
- Fix view counts being reset on Google Drive files
|
||||
|
||||
##### Gazook89 :
|
||||
- New **PHB → Artist Credit** snippet
|
||||
- **PRINT** snippets moved to the **Style Editor** tab
|
||||
|
||||
### Monday, 09/08/2021 - v2.13.3
|
||||
|
||||
##### G-Ambatte :
|
||||
- Tooltips hovering over brews in dropdowns / user page.
|
||||
- Fixed sort-by created date on user page.
|
||||
|
||||
##### Gazook89 :
|
||||
- Hotkey Ctrl-/ and snippets to add HTML comments; use for notes that won't appear in your brew.
|
||||
|
||||
### Friday, 30/07/2021 - v2.13.2
|
||||
|
||||
- Background work to allow new themes in the future
|
||||
- Fixed cursor getting stuck when resizing divider bar
|
||||
|
||||
##### G-Ambatte :
|
||||
- Fix Style tab not copying when Cloned To New
|
||||
- Basic brew sorting on User page
|
||||
- Reduced data sent on each request from server
|
||||
|
||||
##### Gazook89 :
|
||||
- Cleaned up styling on menus
|
||||
|
||||
### Saturday, 28/6/2021 - v2.13.1
|
||||
|
||||
- Fixed the issue with new brews not saving!
|
||||
|
||||
### Saturday, 26/6/2021 - v2.13.0
|
||||
|
||||
- "Share to Reddit" button now works with Google brews
|
||||
- Downloading or viewing the source of your brew will now show the contents of the Style tab at the top of the document in a backtick code fence like this:
|
||||
|
||||
\`\`\`css
|
||||
|
||||
myStyle {color: black}
|
||||
|
||||
\`\`\`
|
||||
|
||||
##### G-Ambatte :
|
||||
- New **Download**, **View**, and **Clone to New** buttons in the "Source" dropdown on the Share page.
|
||||
- Pasting your brew into a "New" page and saving will transfer any CSS in the code fence to the Style tab.
|
||||
- Unsaved work in the New page Style tab is now cached to your browser storage if you navigate away.
|
||||
|
||||
### Thursday, 10/6/2021 - v2.12.0
|
||||
|
||||
- New "style" tab to better organize custom CSS in preparation for new themes and sharable styles.
|
||||
- Your own Google brews will no longer show up in the list when viewing someone else's profile.
|
||||
|
||||
### Saturday, 02/5/2021 - v2.11.2
|
||||
|
||||
- Fix for edge case where brews could accidentally transfer from Google Drive back to Homebrewery.
|
||||
- Move cursor to end of snippet after insertion
|
||||
|
||||
\page
|
||||
|
||||
### Saturday, 20/3/2021 - v2.11.1
|
||||
|
||||
- Warning when opening brew in your Google Drive trash
|
||||
|
||||
##### G-Ambatte :
|
||||
- Snippet to remove drop caps (fancy first letter after title)
|
||||
|
||||
### Saturday, 13/3/2021 - v2.11.0
|
||||
|
||||
- Many background things for upcoming v3. Get pumped.
|
||||
|
||||
##### G-Ambatte :
|
||||
- Fixed new brews failing to save when auto-generated file name is too long.
|
||||
- "New" button added to the Nav bar.
|
||||
- "Download" button to download your brew as a text file.
|
||||
- Reduced download size and improved caching.
|
||||
|
||||
##### RKuerten :
|
||||
- Bold and Italics hotkeys for Mac users (Cmd+B, Cmd+I)
|
||||
|
||||
### Friday, 25/1/2021 - v2.10.7
|
||||
- Cover Page snippet now flips left-right page numbering.
|
||||
- Added instructions for [installing on a FreeBSD Jail](https://github.com/naturalcrit/homebrewery/blob/master/README.FREEBSD.md).
|
||||
- Fix for box-shadows breaking across columns. <br>(Thanks G-Ambatte for all of these!)
|
||||
- Small user interface tweaks (Thanks Ericsheid)
|
||||
|
||||
### Friday, 02/1/2021 - v2.10.6
|
||||
- Fixed punctuation for usernames ending with 's' on the user page. (Thanks AlexeySachkov)
|
||||
- Fixed server crashes due to excessive long lines in brews
|
||||
- Fixed "automated request" lockouts from Google
|
||||
|
||||
### Friday, 18/12/2020 - v2.10.5
|
||||
- Brews now immediately save when transferring between Google Drive and Homebrewery storage.
|
||||
- Added confirmation popup to clarify the transfer process.
|
||||
- Brews transferred or deleted from Google will be found in your Google Drive trash.
|
||||
- Dependency updates.
|
||||
|
||||
### Wednesday, 25/11/2020 - v2.10.4
|
||||
- Fixed Google Drive brews not saving metadata (view count, description, etc.) Note that we are still working on making published Google brews visible to the public when viewing your profile page.
|
||||
|
||||
\column
|
||||
|
||||
### Thursday, 22/10/2020 - v2.10.3
|
||||
- Fixed brews with broken code crashing the edit page when loaded (the "blue screen of death" bug).
|
||||
|
||||
### Monday, 19/10/2020 - v2.10.2
|
||||
- Fixed issue with "recent" item links not updating when transferring between Google Drive.
|
||||
|
||||
### Monday, 12/10/2020 - v2.10.1
|
||||
- Fixed issue with users unable to create new brews
|
||||
- Fixing brews being lost when loaded via back button
|
||||
|
||||
### Wednesday, 07/10/2020 - v2.10.0
|
||||
- Google Drive integration -- Sign in with your Google account to link it with your Homebrewery profile. A new button in the Edit page will let you transfer your file to your personal Google Drive storage, and Google will keep a backup of each version! No more lost work surprises!
|
||||
|
||||
### Friday, 28/08/2020 - v2.9.2
|
||||
- Many dependency updates
|
||||
- Finally fixed this changelog page to not run off the edge :P
|
||||
|
||||
### Sunday, 19/07/2020 - v2.9.1
|
||||
- Fixed paragraphs appearing blank on new columns
|
||||
|
||||
### Wednesday, 20/05/2020 - v2.9.0
|
||||
- Major refactoring of site backend to work with updated dependencies for security (should be invisible to users)
|
||||
|
||||
### Wednesday, 11/03/2020 - v2.8.2
|
||||
- Fixed delete button removing everyone's copy for brews with multiple authors
|
||||
- Compressed homebrew text in database
|
||||
@@ -16,8 +374,10 @@
|
||||
- "Report Issue" navbar button now links to the subreddit
|
||||
- Refactored background code
|
||||
|
||||
\page
|
||||
|
||||
### Sunday, 04/06/2017 - v2.7.5
|
||||
- Fixed the class feature snippet duplicating the entire brew
|
||||
- Fixed Class Feature snippet duplicating entire brew
|
||||
- Fixed headers in tables being duplicated
|
||||
- Fixed border-image being scrambled on class tables and descriptive text boxes
|
||||
- Fixed pages going out of sync in large brews, causing them to be rendered off-page
|
||||
@@ -33,24 +393,17 @@
|
||||
### Saturday, 18/02/2017 - v2.7.2
|
||||
- Adding ability to delete a brew from the user page, incase the user creates a brew that makes the edit page unrender-able. (re:309)
|
||||
|
||||
## BIG NEWS
|
||||
With the next major release of Homebrewery, v3.0.0, this tool *will no longer support raw HTML input for brew code*. Most issues and errors users are having are because of this feature and it's become too taxing to help and fix these issues.
|
||||
|
||||
All brews made previous to the release of v3.0.0 will still render normally.
|
||||
|
||||
### Thursday, 19/01/2017 - v2.7.0
|
||||
### Thursday, 19/01/2017 - v2.7.1
|
||||
- Fixed saving multiple authors and multiple systems on brew metadata (thanks u/PalaNolho re:282)
|
||||
- Adding in line highlight for new pages
|
||||
- Added in a simple brew lookup for admin
|
||||
|
||||
|
||||
### Saturday, 14/01/2017 - v2.7.0
|
||||
- Added a new Render Warning overlay. It detects situations where the brew may not be rendering correctly (wrong browser, browser is zoomed in...) and let's the user know
|
||||
|
||||
|
||||
### Sunday, 25/12/2016 - v2.7.0
|
||||
- Switching over to using Vitreum v4
|
||||
- Removed gulp, all tasks are run through npm scripts
|
||||
- Removed gulp, all tasks are run through npm scripts
|
||||
- Updating docs for local dev
|
||||
- Removing support for Docker. I have never used it, nor will I ever test for it, so I don't want to continue to explictly support it on this repo. Feel free to make a fork and make it docker-able though :)
|
||||
- Changed icon for the metadata
|
||||
@@ -60,7 +413,7 @@ All brews made previous to the release of v3.0.0 will still render normally.
|
||||
- Removed a lot of unused files in shared
|
||||
- vitreum v4 now lets me use codemirror as a pure node dependacy
|
||||
|
||||
|
||||
\column
|
||||
|
||||
### Saturday, 03/12/2016 - v2.6.0
|
||||
- Added report back to the edit page
|
||||
@@ -73,13 +426,10 @@ All brews made previous to the release of v3.0.0 will still render normally.
|
||||
- Added a table of contents snippet (thanks u/tullisar)
|
||||
- Added a multicolumn snippet
|
||||
|
||||
|
||||
|
||||
### Thursday, 01/12/2016
|
||||
- Added in a snippet for a split table
|
||||
- Added an account nav item to new page
|
||||
|
||||
|
||||
### Sunday, 27/11/2016 - v2.5.1
|
||||
- Fixed the column rendering on the new user page. Really should have tested that better
|
||||
- Added a hover tooltip to fully read the brew description
|
||||
@@ -95,7 +445,6 @@ All brews made previous to the release of v3.0.0 will still render normally.
|
||||
- Added a new user page to see others published brews, as well as all of your own brews.
|
||||
- Added a new nav item for accessing your profile and logging in
|
||||
|
||||
|
||||
### Monday, 14/11/2016
|
||||
- Updated snippet bar style
|
||||
- You can now print from a new page without saving
|
||||
@@ -105,6 +454,8 @@ All brews made previous to the release of v3.0.0 will still render normally.
|
||||
- Added final touches to the html validator and updating the rest of the branch
|
||||
- If anyone finds issues with the new HTML validator, please let me know. I hope this will bring a more consistent feel to Homebrewery rendering.
|
||||
|
||||
\page
|
||||
|
||||
### Friday, 09/09/2016 - v2.4.0
|
||||
- Adding in a HTML validator that will display warnings whenever you save. This should stop a lot of the issues generated with pages not showing up.
|
||||
|
||||
@@ -120,7 +471,6 @@ All brews made previous to the release of v3.0.0 will still render normally.
|
||||
- Fixed the noteblock overlapping into titles (thanks u/dsompura!)
|
||||
- Fixed a bad search route in the admin panel (thanks u/SnappyTom!)
|
||||
|
||||
|
||||
### Friday, 29/07/2016 - v2.2.7
|
||||
- Adding in descriptive note blocks. (Thanks calculuschild!)
|
||||
|
||||
@@ -133,7 +483,7 @@ All brews made previous to the release of v3.0.0 will still render normally.
|
||||
- Even works after you print to pdf!
|
||||
|
||||
### Tuesday, 07/06/2016 - v2.2.2
|
||||
- Fixed bug with new markdown lexer and aprser not working on print page
|
||||
- Fixed bug with new markdown lexer and parser not working on print page
|
||||
|
||||
### Sunday, 05/06/2016 - v2.2.1
|
||||
- Adding in a new Class table div block. The old Class table block used weird stacking of HTML elements, resulting is difficult to control behaviour and poor interactiosn with the rest of the page. This new block is much easier to style and work with.
|
||||
@@ -141,11 +491,10 @@ All brews made previous to the release of v3.0.0 will still render normally.
|
||||
- Added in a new auto-incremeting page number snippet (thakns u/Ryrok!)
|
||||
- Lists in monster stat blocks should be fixed now
|
||||
|
||||
\column
|
||||
|
||||
### Saturday, 04/06/2016 - v2.2.0
|
||||
- MIgrating The Homebrewery over to hombrewery.naturalcrit.com. It know runs on it's own server, with it's own repo separate from the other tools I'm working on. Makes updating and deploying much easier.
|
||||
|
||||
\page
|
||||
- Migrating The Homebrewery over to hombrewery.naturalcrit.com. It now runs on it's own server, with it's own repo separate from the other tools I'm working on. Makes updating and deploying much easier.
|
||||
|
||||
### Sunday, 29/05/2016 - v2.1.0
|
||||
- Finally added a syntax for doing spell lists. A bit in-depth about why this took so long. Essentially I'm running out of syntax to use in stardard Markdown. There are too many unique elements in the PHB-style to be mapped. I solved this earlier by stacking certain elements together (eg. an `<hr>` before a `blockquote` turns it into moster state block), but those are getting unweildly. I would like to simply wrap these in `div`s with classes, but unfortunately Markdown stops processing when within HTML blocks. To get around this I wrote my own override to the Markdown parser and lexer to process Markdown within a simple div class wrapper. This should open the door for more unique syntaxes in the future. Big step!
|
||||
@@ -161,6 +510,8 @@ All brews made previous to the release of v3.0.0 will still render normally.
|
||||
### Wednesday, 25/05/2016 -v2.0.5
|
||||
- The class table generators have the proper ability score improvement progression.
|
||||
|
||||
\page
|
||||
|
||||
### Tuesday, 24/05/2016 - v2.0.4
|
||||
- Fixed extra wide monster stat blocks sometimes only being one column
|
||||
- The class table generators now follow the proper progression from the PHB (thakns u/IrishBandit)
|
||||
@@ -171,8 +522,6 @@ All brews made previous to the release of v3.0.0 will still render normally.
|
||||
- Bumped up the allowed entity size for extra-large brew (Thanks for reporting it dickboner93)
|
||||
- Added a little error box when a save fails with a custom link to reporting the issue on github.
|
||||
|
||||
\page
|
||||
|
||||
### Saturday, 14/05/2016 - v2.0.0 (finally!)
|
||||
|
||||
I've been working on v2 for a *very* long time. I want to thank you guys for being paitent.
|
||||
@@ -212,8 +561,6 @@ Massive changelog incoming:
|
||||
- Source now opens to it's own route `/source/:sharedId` instead of just a window. Now easier to share, and won't be blocked by some browsers.
|
||||
- Print page now auto-opens print dialog. If you want to share your print page link, just remove the `?dialog=true` parameter and it won't open the dialog.
|
||||
|
||||
|
||||
|
||||
\page
|
||||
|
||||
### Wednesday, 20/04/2016
|
||||
@@ -273,7 +620,6 @@ Massive changelog incoming:
|
||||
* Increased padding on table cells
|
||||
* Raw html now shows in view source
|
||||
|
||||
|
||||
## v1.0.0 - Release
|
||||
|
||||
### Wednesday, 3/01/2016
|
||||
@@ -281,4 +627,3 @@ Massive changelog incoming:
|
||||
* Added `phb.standalone.css` plus a build system for creating it
|
||||
* Added page numbers and footer text
|
||||
* Page accent now flips each page
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
require('./admin.less');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
|
||||
@@ -17,7 +18,7 @@ const Admin = createClass({
|
||||
|
||||
<header>
|
||||
<div className='container'>
|
||||
<i className='fa fa-rocket' />
|
||||
<i className='fas fa-rocket' />
|
||||
homebrewery admin
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
require('./brewCleanup.less');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const cx = require('classnames');
|
||||
@@ -44,8 +45,8 @@ const BrewCleanup = createClass({
|
||||
return <div className='removeBox'>
|
||||
<button onClick={this.cleanup} className='remove'>
|
||||
{this.state.pending
|
||||
? <i className='fa fa-spin fa-spinner' />
|
||||
: <span><i className='fa fa-times' /> Remove</span>
|
||||
? <i className='fas fa-spin fa-spinner' />
|
||||
: <span><i className='fas fa-times' /> Remove</span>
|
||||
}
|
||||
</button>
|
||||
<span>Found {this.state.count} Brews that could be removed. </span>
|
||||
@@ -58,7 +59,7 @@ const BrewCleanup = createClass({
|
||||
|
||||
<button onClick={this.prime} className='query'>
|
||||
{this.state.pending
|
||||
? <i className='fa fa-spin fa-spinner' />
|
||||
? <i className='fas fa-spin fa-spinner' />
|
||||
: 'Query Brews'
|
||||
}
|
||||
</button>
|
||||
@@ -71,4 +72,4 @@ const BrewCleanup = createClass({
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = BrewCleanup;
|
||||
module.exports = BrewCleanup;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
require('./brewCompress.less');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const cx = require('classnames');
|
||||
@@ -58,8 +59,8 @@ const BrewCompress = createClass({
|
||||
return <div className='removeBox'>
|
||||
<button onClick={this.cleanup} className='remove'>
|
||||
{this.state.pending
|
||||
? <i className='fa fa-spin fa-spinner' />
|
||||
: <span><i className='fa fa-compress' /> compress </span>
|
||||
? <i className='fas fa-spin fa-spinner' />
|
||||
: <span><i className='fas fa-compress' /> compress </span>
|
||||
}
|
||||
</button>
|
||||
{this.state.pending
|
||||
@@ -75,7 +76,7 @@ const BrewCompress = createClass({
|
||||
|
||||
<button onClick={this.prime} className='query'>
|
||||
{this.state.pending
|
||||
? <i className='fa fa-spin fa-spinner' />
|
||||
? <i className='fas fa-spin fa-spinner' />
|
||||
: 'Query Brews'
|
||||
}
|
||||
</button>
|
||||
@@ -88,4 +89,4 @@ const BrewCompress = createClass({
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = BrewCompress;
|
||||
module.exports = BrewCompress;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
require('./brewLookup.less');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const cx = require('classnames');
|
||||
@@ -60,7 +61,7 @@ const BrewLookup = createClass({
|
||||
<h2>Brew Lookup</h2>
|
||||
<input type='text' value={this.state.query} onChange={this.handleChange} placeholder='edit or share id' />
|
||||
<button onClick={this.lookup}>
|
||||
<i className={cx('fa', {
|
||||
<i className={cx('fas', {
|
||||
'fa-search' : !this.state.searching,
|
||||
'fa-spin fa-spinner' : this.state.searching,
|
||||
})} />
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
require('./stats.less');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const cx = require('classnames');
|
||||
@@ -36,10 +37,10 @@ const Stats = createClass({
|
||||
</dl>
|
||||
|
||||
{this.state.fetching
|
||||
&& <div className='pending'><i className='fa fa-spin fa-spinner' /></div>
|
||||
&& <div className='pending'><i className='fas fa-spin fa-spinner' /></div>
|
||||
}
|
||||
</div>;
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = Stats;
|
||||
module.exports = Stats;
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
require('./brewRenderer.less');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
const cx = require('classnames');
|
||||
|
||||
const MarkdownLegacy = require('naturalcrit/markdownLegacy.js');
|
||||
const Markdown = require('naturalcrit/markdown.js');
|
||||
const ErrorBar = require('./errorBar/errorBar.jsx');
|
||||
|
||||
//TODO: move to the brew renderer
|
||||
const RenderWarnings = require('homebrewery/renderWarnings/renderWarnings.jsx');
|
||||
const NotificationPopup = require('./notificationPopup/notificationPopup.jsx');
|
||||
const Frame = require('react-frame-component').default;
|
||||
|
||||
const PAGE_HEIGHT = 1056;
|
||||
const PPR_THRESHOLD = 50;
|
||||
@@ -16,45 +19,61 @@ const PPR_THRESHOLD = 50;
|
||||
const BrewRenderer = createClass({
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
text : '',
|
||||
errors : []
|
||||
text : '',
|
||||
style : '',
|
||||
renderer : 'legacy',
|
||||
errors : []
|
||||
};
|
||||
},
|
||||
getInitialState : function() {
|
||||
const pages = this.props.text.split('\\page');
|
||||
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,
|
||||
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="//fonts.googleapis.com/css?family=Open+Sans:400,300,600,700" rel="stylesheet" type="text/css" />
|
||||
<link href='/homebrew/bundle.css' rel='stylesheet' />
|
||||
<base target=_blank>
|
||||
</head><body style='overflow: hidden'><div></div></body></html>`
|
||||
};
|
||||
},
|
||||
height : 0,
|
||||
lastRender : <div></div>,
|
||||
|
||||
componentDidMount : function() {
|
||||
this.updateSize();
|
||||
window.addEventListener('resize', this.updateSize);
|
||||
},
|
||||
componentWillUnmount : function() {
|
||||
window.removeEventListener('resize', this.updateSize);
|
||||
},
|
||||
|
||||
componentWillReceiveProps : function(nextProps) {
|
||||
const pages = nextProps.text.split('\\page');
|
||||
this.setState({
|
||||
pages : pages,
|
||||
usePPR : pages.length >= PPR_THRESHOLD
|
||||
});
|
||||
componentDidUpdate : function(prevProps) {
|
||||
if(prevProps.text !== this.props.text) {
|
||||
let pages;
|
||||
if(this.props.renderer == 'legacy') {
|
||||
pages = this.props.text.split('\\page');
|
||||
} else {
|
||||
pages = this.props.text.split(/^\\page$/gm);
|
||||
}
|
||||
this.setState({
|
||||
pages : pages,
|
||||
usePPR : pages.length >= PPR_THRESHOLD
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
updateSize : function() {
|
||||
this.setState({
|
||||
height : this.refs.main.parentNode.clientHeight,
|
||||
isMounted : true
|
||||
height : this.refs.main.parentNode.clientHeight,
|
||||
});
|
||||
},
|
||||
|
||||
@@ -84,7 +103,7 @@ const BrewRenderer = createClass({
|
||||
},
|
||||
|
||||
renderPageInfo : function(){
|
||||
return <div className='pageInfo'>
|
||||
return <div className='pageInfo' ref='main'>
|
||||
{this.state.viewablePageNumber + 1} / {this.state.pages.length}
|
||||
</div>;
|
||||
},
|
||||
@@ -99,18 +118,32 @@ const BrewRenderer = createClass({
|
||||
|
||||
renderDummyPage : function(index){
|
||||
return <div className='phb' id={`p${index + 1}`} key={index}>
|
||||
<i className='fa fa-spinner fa-spin' />
|
||||
<i className='fas fa-spinner fa-spin' />
|
||||
</div>;
|
||||
},
|
||||
|
||||
renderStyle : function() {
|
||||
if(!this.props.style) return;
|
||||
return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `<style> ${this.props.style} </style>` }} />;
|
||||
},
|
||||
|
||||
renderPage : function(pageText, index){
|
||||
return <div className='phb' id={`p${index + 1}`} dangerouslySetInnerHTML={{ __html: Markdown.render(pageText) }} key={index} />;
|
||||
if(this.props.renderer == 'legacy')
|
||||
return <div className='phb page' id={`p${index + 1}`} dangerouslySetInnerHTML={{ __html: MarkdownLegacy.render(pageText) }} key={index} />;
|
||||
else {
|
||||
pageText += `\n\n \n\\column\n `; //Artificial column break at page end to emulate column-fill:auto (until `wide` is used, when column-fill:balance will reappear)
|
||||
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)){
|
||||
if(this.shouldRender(page, index) && typeof window !== 'undefined'){
|
||||
return this.renderPage(page, index);
|
||||
} else {
|
||||
return this.renderDummyPage(index);
|
||||
@@ -119,29 +152,67 @@ const BrewRenderer = createClass({
|
||||
}
|
||||
if(this.props.errors && this.props.errors.length) return this.lastRender;
|
||||
this.lastRender = _.map(this.state.pages, (page, index)=>{
|
||||
return this.renderPage(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"
|
||||
setTimeout(()=>{ //We still see a flicker where the style isn't applied yet, so wait 100ms before showing iFrame
|
||||
this.updateSize();
|
||||
window.addEventListener('resize', this.updateSize);
|
||||
this.renderPages(); //Make sure page is renderable before showing
|
||||
this.setState({
|
||||
isMounted : true,
|
||||
visibility : 'visible'
|
||||
});
|
||||
}, 100);
|
||||
},
|
||||
|
||||
render : function(){
|
||||
//render in iFrame so broken code doesn't crash the site.
|
||||
//Also render dummy page while iframe is mounting.
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className='brewRenderer'
|
||||
onScroll={this.handleScroll}
|
||||
ref='main'
|
||||
style={{ height: this.state.height }}>
|
||||
|
||||
<ErrorBar errors={this.props.errors} />
|
||||
<div className='popups'>
|
||||
<RenderWarnings />
|
||||
<NotificationPopup />
|
||||
{!this.state.isMounted
|
||||
? <div className='brewRenderer' onScroll={this.handleScroll}>
|
||||
<div className='pages' ref='pages'>
|
||||
{this.renderDummyPage(1)}
|
||||
</div>
|
||||
</div>
|
||||
: null}
|
||||
|
||||
<div className='pages' ref='pages'>
|
||||
{this.renderPages()}
|
||||
<Frame initialContent={this.state.initialContent}
|
||||
head = <link href={`${this.props.renderer == 'legacy' ? '/themes/5ePhbLegacy.style.css' : '/themes/5ePhb.style.css'}`} rel='stylesheet'/>
|
||||
style={{ width: '100%', height: '100%', visibility: this.state.visibility }}
|
||||
contentDidMount={this.frameDidMount}>
|
||||
<div className={'brewRenderer'}
|
||||
onScroll={this.handleScroll}
|
||||
style={{ height: this.state.height }}>
|
||||
|
||||
<ErrorBar errors={this.props.errors} />
|
||||
<div className='popups'>
|
||||
<RenderWarnings />
|
||||
<NotificationPopup />
|
||||
</div>
|
||||
|
||||
<div className='pages' ref='pages'>
|
||||
{/* Apply CSS from Style tab and render pages from Markdown tab */}
|
||||
{this.state.isMounted
|
||||
&&
|
||||
<>
|
||||
{this.renderStyle()}
|
||||
{this.renderPages()}
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
</Frame>
|
||||
{this.renderPageInfo()}
|
||||
{this.renderPPRmsg()}
|
||||
</React.Fragment>
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
@import (multiple, less) 'shared/naturalcrit/styles/reset.less';
|
||||
|
||||
@import (less) './client/homebrew/phbStyle/phb.style.less';
|
||||
.pane{
|
||||
position : relative;
|
||||
}
|
||||
.brewRenderer{
|
||||
will-change : transform;
|
||||
overflow-y : scroll;
|
||||
.pages{
|
||||
margin : 30px 0px;
|
||||
&>.phb{
|
||||
&>.page{
|
||||
margin-right : auto;
|
||||
margin-bottom : 30px;
|
||||
margin-left : auto;
|
||||
@@ -16,6 +13,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
.pane{
|
||||
position : relative;
|
||||
}
|
||||
.pageInfo{
|
||||
position : absolute;
|
||||
right : 17px;
|
||||
@@ -37,4 +37,4 @@
|
||||
font-size : 10px;
|
||||
font-weight : 800;
|
||||
color : white;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
require('./errorBar.less');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
@@ -61,7 +62,7 @@ const ErrorBar = createClass({
|
||||
if(!this.props.errors.length) return null;
|
||||
|
||||
return <div className='errorBar'>
|
||||
<i className='fa fa-exclamation-triangle' />
|
||||
<i className='fas fa-exclamation-triangle' />
|
||||
<h3> There are HTML errors in your markup</h3>
|
||||
<small>If these aren't fixed your brew will not render properly when you print it to PDF or share it</small>
|
||||
{this.renderErrors()}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
|
||||
require('./notificationPopup.less');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
const cx = require('classnames'); //Unused variable
|
||||
|
||||
const DISMISS_KEY = 'dismiss_notification7-24-19';
|
||||
const DISMISS_KEY = 'dismiss_notification09-9-21';
|
||||
|
||||
const NotificationPopup = createClass({
|
||||
getInitialState : function() {
|
||||
@@ -22,17 +22,39 @@ const NotificationPopup = createClass({
|
||||
notifications : {
|
||||
psa : function(){
|
||||
return <li key='psa'>
|
||||
<em>Known bug: Grey Shadow Boxes </em> <br />
|
||||
The shadows around certain brew elements such as notes and statblocks might appear as a solid grey box when generating a PDF.
|
||||
<a target='_blank' href='https://old.reddit.com/r/homebrewery/comments/ch3v0d/psa_grey_boxesshadows_around_notes_stat_blocks_etc/'>
|
||||
See this Reddit post
|
||||
</a> for updates and possible workarounds.
|
||||
<em>V3.0.0 Released!</em> <br />
|
||||
After a long and bumpy road, we decided it was high time we finally release version 3 of the homebrewery into the wild. You can check out a
|
||||
brief overview and see how to opt-in to the new features here:
|
||||
<a target='_blank' href='https://homebrewery.naturalcrit.com/v3_preview'>V3 Welcome Page</a> and
|
||||
<a target='_blank' href='https://homebrewery.naturalcrit.com/changelog'>the Changelog</a>.
|
||||
<br /><br />
|
||||
<em>BE WARNED:</em> As we continue to develop V3, expect small tweaks in the styling, fonts, and snippets; your brews may look slightly
|
||||
different from day-to-day. All of your old documents will continue to work as normal; we are not touching them. If you don't want to deal
|
||||
with the possibility of slight formatting changes, you may choose to stick with the Legacy renderer on any of your brews for as long as you like.
|
||||
<br /><br />
|
||||
With this in mind, if you still wish to try out V3, you can opt-in any of your brews to the the V3 renderer.
|
||||
This will likely break much of your formatting as a lot of the Markdown code has been updated, and starting from scratch may be cleaner.
|
||||
(Don't worry, you can always change the renderer back to Legacy for any brew at any time).
|
||||
</li>;
|
||||
},
|
||||
refreshGoogle : function (){
|
||||
return <li key='refreshGoogle'>
|
||||
<em>Refresh your Google Drive Credentials!</em> <br />
|
||||
Currently a lot of people are striking issues with their Google credentials expiring, which happens one year after the last sign in via
|
||||
Google. This can cause errors when trying to save your brews. If this happens, simply visit the
|
||||
<a target='_blank' href='https://www.naturalcrit.com/login'>
|
||||
logout page
|
||||
</a>
|
||||
, sign out, and then sign back in "with Google" to refresh your credentials. See
|
||||
<a target='_blank' href='https://github.com/naturalcrit/homebrewery/discussions/1580'>
|
||||
this discussion on Github
|
||||
</a> for more details.
|
||||
</li>;
|
||||
},
|
||||
faq : function(){
|
||||
return <li key='faq'>
|
||||
<em>Protect your work! </em> <br />
|
||||
At the moment we do not save a history of your projects, so please make frequent backups of your brews!
|
||||
If you opt not to use your Google Drive, keep in mind that we do not save a history of your projects. Please make frequent backups of your brews!
|
||||
<a target='_blank' href='https://www.reddit.com/r/homebrewery/comments/adh6lh/faqs_psas_announcements/'>
|
||||
See the FAQ
|
||||
</a> to learn how to avoid losing your work!
|
||||
@@ -55,10 +77,12 @@ const NotificationPopup = createClass({
|
||||
if(_.isEmpty(this.state.notifications)) return null;
|
||||
|
||||
return <div className='notificationPopup'>
|
||||
<i className='fa fa-times dismiss' onClick={this.dismiss}/>
|
||||
<i className='fa fa-info-circle info' />
|
||||
<h3>Notice</h3>
|
||||
<small>This website is always improving and we are still adding new features and squashing bugs. Keep the following in mind:</small>
|
||||
<i className='fas fa-times dismiss' onClick={this.dismiss}/>
|
||||
<i className='fas fa-info-circle info' />
|
||||
<div className='header'>
|
||||
<h3>Notice</h3>
|
||||
<small>This website is always improving and we are still adding new features and squashing bugs. Keep the following in mind:</small>
|
||||
</div>
|
||||
<ul>{_.values(this.state.notifications)}</ul>
|
||||
</div>;
|
||||
}
|
||||
|
||||
@@ -1,29 +1,28 @@
|
||||
.popups{
|
||||
position : fixed;
|
||||
top : @navbarHeight;
|
||||
top : @navbarHeight;
|
||||
right : 15px;
|
||||
z-index : 10001;
|
||||
width : 350px;
|
||||
width : 450px;
|
||||
}
|
||||
|
||||
.notificationPopup{
|
||||
position : relative;
|
||||
float : right;
|
||||
display : inline-block;
|
||||
width : 350px;
|
||||
padding : 20px;
|
||||
position : relative;
|
||||
display : inline-block;
|
||||
width : 100%;
|
||||
padding : 15px;
|
||||
padding-bottom : 10px;
|
||||
padding-left : 85px;
|
||||
padding-left : 25px;
|
||||
background-color : @blue;
|
||||
color : white;
|
||||
a{
|
||||
color : @steel;
|
||||
color : #e0e5c1;
|
||||
font-weight : 800;
|
||||
}
|
||||
i.info{
|
||||
position : absolute;
|
||||
top : 24px;
|
||||
left : 24px;
|
||||
top : 12px;
|
||||
left : 12px;
|
||||
opacity : 0.8;
|
||||
font-size : 2.5em;
|
||||
}
|
||||
@@ -37,6 +36,9 @@
|
||||
opacity : 1;
|
||||
}
|
||||
}
|
||||
.header {
|
||||
padding-left : 50px;
|
||||
}
|
||||
small{
|
||||
opacity : 0.7;
|
||||
font-size : 0.6em;
|
||||
|
||||
@@ -1,96 +1,171 @@
|
||||
require('./editor.less');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
const cx = require('classnames');
|
||||
const dedent = require('dedent-tabs').default;
|
||||
|
||||
const CodeEditor = require('naturalcrit/codeEditor/codeEditor.jsx');
|
||||
const SnippetBar = require('./snippetbar/snippetbar.jsx');
|
||||
const MetadataEditor = require('./metadataEditor/metadataEditor.jsx');
|
||||
|
||||
const SNIPPETBAR_HEIGHT = 25;
|
||||
const DEFAULT_STYLE_TEXT = dedent`
|
||||
/*=======--- Example CSS styling ---=======*/
|
||||
/* Any CSS here will apply to your document! */
|
||||
|
||||
.myExampleClass {
|
||||
color: black;
|
||||
}`;
|
||||
|
||||
const splice = function(str, index, inject){
|
||||
return str.slice(0, index) + inject + str.slice(index);
|
||||
};
|
||||
|
||||
const SNIPPETBAR_HEIGHT = 25;
|
||||
|
||||
|
||||
const Editor = createClass({
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
value : '',
|
||||
onChange : ()=>{},
|
||||
brew : {
|
||||
text : '',
|
||||
style : ''
|
||||
},
|
||||
|
||||
metadata : {},
|
||||
onMetadataChange : ()=>{},
|
||||
onTextChange : ()=>{},
|
||||
onStyleChange : ()=>{},
|
||||
onMetaChange : ()=>{},
|
||||
|
||||
renderer : 'legacy'
|
||||
};
|
||||
},
|
||||
getInitialState : function() {
|
||||
return {
|
||||
showMetadataEditor : false
|
||||
view : 'text' //'text', 'style', 'meta'
|
||||
};
|
||||
},
|
||||
cursorPosition : {
|
||||
line : 0,
|
||||
ch : 0
|
||||
},
|
||||
|
||||
isText : function() {return this.state.view == 'text';},
|
||||
isStyle : function() {return this.state.view == 'style';},
|
||||
isMeta : function() {return this.state.view == 'meta';},
|
||||
|
||||
componentDidMount : function() {
|
||||
this.updateEditorSize();
|
||||
this.highlightPageLines();
|
||||
this.highlightCustomMarkdown();
|
||||
window.addEventListener('resize', this.updateEditorSize);
|
||||
},
|
||||
|
||||
componentWillUnmount : function() {
|
||||
window.removeEventListener('resize', this.updateEditorSize);
|
||||
},
|
||||
|
||||
updateEditorSize : function() {
|
||||
let paneHeight = this.refs.main.parentNode.clientHeight;
|
||||
paneHeight -= SNIPPETBAR_HEIGHT + 1;
|
||||
this.refs.codeEditor.codeMirror.setSize(null, paneHeight);
|
||||
if(this.refs.codeEditor) {
|
||||
let paneHeight = this.refs.main.parentNode.clientHeight;
|
||||
paneHeight -= SNIPPETBAR_HEIGHT + 1;
|
||||
this.refs.codeEditor.codeMirror.setSize(null, paneHeight);
|
||||
}
|
||||
},
|
||||
|
||||
handleTextChange : function(text){
|
||||
this.props.onChange(text);
|
||||
},
|
||||
handleCursorActivty : function(curpos){
|
||||
this.cursorPosition = curpos;
|
||||
},
|
||||
handleInject : function(injectText){
|
||||
const lines = this.props.value.split('\n');
|
||||
lines[this.cursorPosition.line] = splice(lines[this.cursorPosition.line], this.cursorPosition.ch, injectText);
|
||||
let text;
|
||||
if(this.isText()) text = this.props.brew.text;
|
||||
if(this.isStyle()) text = this.props.brew.style ?? DEFAULT_STYLE_TEXT;
|
||||
|
||||
this.handleTextChange(lines.join('\n'));
|
||||
this.refs.codeEditor.setCursorPosition(this.cursorPosition.line, this.cursorPosition.ch + injectText.length);
|
||||
const lines = text.split('\n');
|
||||
const cursorPos = this.refs.codeEditor.getCursorPosition();
|
||||
lines[cursorPos.line] = splice(lines[cursorPos.line], cursorPos.ch, injectText);
|
||||
|
||||
const injectLines = injectText.split('\n');
|
||||
this.refs.codeEditor.setCursorPosition(cursorPos.line + injectLines.length, cursorPos.ch + injectLines[injectLines.length - 1].length);
|
||||
|
||||
if(this.isText()) this.props.onTextChange(lines.join('\n'));
|
||||
if(this.isStyle()) this.props.onStyleChange(lines.join('\n'));
|
||||
},
|
||||
handgleToggle : function(){
|
||||
|
||||
handleViewChange : function(newView){
|
||||
this.setState({
|
||||
showMetadataEditor : !this.state.showMetadataEditor
|
||||
});
|
||||
view : newView
|
||||
}, this.updateEditorSize); //TODO: not sure if updateeditorsize needed
|
||||
},
|
||||
|
||||
getCurrentPage : function(){
|
||||
const lines = this.props.value.split('\n').slice(0, this.cursorPosition.line + 1);
|
||||
const lines = this.props.brew.text.split('\n').slice(0, this.cursorPosition.line + 1);
|
||||
return _.reduce(lines, (r, line)=>{
|
||||
if(line.indexOf('\\page') !== -1) r++;
|
||||
return r;
|
||||
}, 1);
|
||||
},
|
||||
|
||||
highlightPageLines : function(){
|
||||
highlightCustomMarkdown : function(){
|
||||
if(!this.refs.codeEditor) return;
|
||||
const codeMirror = this.refs.codeEditor.codeMirror;
|
||||
if(this.state.view === 'text') {
|
||||
const codeMirror = this.refs.codeEditor.codeMirror;
|
||||
|
||||
const lineNumbers = _.reduce(this.props.value.split('\n'), (r, line, lineNumber)=>{
|
||||
if(line.indexOf('\\page') !== -1){
|
||||
codeMirror.addLineClass(lineNumber, 'background', 'pageLine');
|
||||
r.push(lineNumber);
|
||||
}
|
||||
return r;
|
||||
}, []);
|
||||
return lineNumbers;
|
||||
//reset custom text styles
|
||||
const customHighlights = codeMirror.getAllMarks();
|
||||
for (let i=0;i<customHighlights.length;i++) customHighlights[i].clear();
|
||||
|
||||
const lineNumbers = _.reduce(this.props.brew.text.split('\n'), (r, line, lineNumber)=>{
|
||||
|
||||
//reset custom line styles
|
||||
codeMirror.removeLineClass(lineNumber, 'background');
|
||||
codeMirror.removeLineClass(lineNumber, 'text');
|
||||
|
||||
// Legacy Codemirror styling
|
||||
if(this.props.renderer == 'legacy') {
|
||||
if(line.includes('\\page')){
|
||||
codeMirror.addLineClass(lineNumber, 'background', 'pageLine');
|
||||
r.push(lineNumber);
|
||||
}
|
||||
}
|
||||
|
||||
// New Codemirror styling for V3 renderer
|
||||
if(this.props.renderer == 'V3') {
|
||||
if(line.match(/^\\page$/)){
|
||||
codeMirror.addLineClass(lineNumber, 'background', 'pageLine');
|
||||
r.push(lineNumber);
|
||||
}
|
||||
|
||||
if(line.match(/^\\column$/)){
|
||||
codeMirror.addLineClass(lineNumber, 'text', 'columnSplit');
|
||||
r.push(lineNumber);
|
||||
}
|
||||
|
||||
// Highlight inline spans {{content}}
|
||||
if(line.includes('{{') && line.includes('}}')){
|
||||
const regex = /{{(?::(?:"[\w,\-()#%. ]*"|[\w\,\-()#%.]*)|[^"'{}\s])*\s*|}}/g;
|
||||
let match;
|
||||
let blockCount = 0;
|
||||
while ((match = regex.exec(line)) != null) {
|
||||
if(match[0].startsWith('{')) {
|
||||
blockCount += 1;
|
||||
} else {
|
||||
blockCount -= 1;
|
||||
}
|
||||
if(blockCount < 0) {
|
||||
blockCount = 0;
|
||||
continue;
|
||||
}
|
||||
codeMirror.markText({ line: lineNumber, ch: match.index }, { line: lineNumber, ch: match.index + match[0].length }, { className: 'inline-block' });
|
||||
}
|
||||
} else if(line.trimLeft().startsWith('{{') || line.trimLeft().startsWith('}}')){
|
||||
// Highlight block divs {{\n Content \n}}
|
||||
let endCh = line.length+1;
|
||||
|
||||
const match = line.match(/^ *{{(?::(?:"[\w,\-()#%. ]*"|[\w\,\-()#%.]*)|[^"'{}\s])* *$|^ *}}$/);
|
||||
if(match)
|
||||
endCh = match.index+match[0].length;
|
||||
codeMirror.markText({ line: lineNumber, ch: 0 }, { line: lineNumber, ch: endCh }, { className: 'block' });
|
||||
}
|
||||
}
|
||||
|
||||
return r;
|
||||
}, []);
|
||||
return lineNumbers;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
brewJump : function(){
|
||||
const currentPage = this.getCurrentPage();
|
||||
window.location.hash = `p${currentPage}`;
|
||||
@@ -98,49 +173,47 @@ const Editor = createClass({
|
||||
|
||||
//Called when there are changes to the editor's dimensions
|
||||
update : function(){
|
||||
this.refs.codeEditor.updateSize();
|
||||
this.refs.codeEditor?.updateSize();
|
||||
},
|
||||
|
||||
renderMetadataEditor : function(){
|
||||
if(!this.state.showMetadataEditor) return;
|
||||
return <MetadataEditor
|
||||
metadata={this.props.metadata}
|
||||
onChange={this.props.onMetadataChange}
|
||||
/>;
|
||||
renderEditor : function(){
|
||||
if(this.isText()){
|
||||
return <CodeEditor key='text'
|
||||
ref='codeEditor'
|
||||
language='gfm'
|
||||
value={this.props.brew.text}
|
||||
onChange={this.props.onTextChange} />;
|
||||
}
|
||||
if(this.isStyle()){
|
||||
return <CodeEditor key='style'
|
||||
ref='codeEditor'
|
||||
language='css'
|
||||
value={this.props.brew.style ?? DEFAULT_STYLE_TEXT}
|
||||
onChange={this.props.onStyleChange} />;
|
||||
}
|
||||
if(this.isMeta()){
|
||||
return <MetadataEditor
|
||||
metadata={this.props.brew}
|
||||
onChange={this.props.onMetaChange} />;
|
||||
}
|
||||
},
|
||||
|
||||
render : function(){
|
||||
this.highlightPageLines();
|
||||
this.highlightCustomMarkdown();
|
||||
return (
|
||||
<div className='editor' ref='main'>
|
||||
<SnippetBar
|
||||
brew={this.props.value}
|
||||
brew={this.props.brew}
|
||||
view={this.state.view}
|
||||
onViewChange={this.handleViewChange}
|
||||
onInject={this.handleInject}
|
||||
onToggle={this.handgleToggle}
|
||||
showmeta={this.state.showMetadataEditor} />
|
||||
{this.renderMetadataEditor()}
|
||||
<CodeEditor
|
||||
ref='codeEditor'
|
||||
wrap={true}
|
||||
language='gfm'
|
||||
value={this.props.value}
|
||||
onChange={this.handleTextChange}
|
||||
onCursorActivity={this.handleCursorActivty} />
|
||||
showEditButtons={this.props.showEditButtons}
|
||||
renderer={this.props.renderer} />
|
||||
|
||||
{/*
|
||||
<div className='brewJump' onClick={this.brewJump}>
|
||||
<i className='fa fa-arrow-right' />
|
||||
</div>
|
||||
*/}
|
||||
{this.renderEditor()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = Editor;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -9,6 +9,22 @@
|
||||
background-color : fade(#333, 15%);
|
||||
border-bottom : #333 solid 1px;
|
||||
}
|
||||
.columnSplit{
|
||||
font-style : italic;
|
||||
color : grey;
|
||||
background-color : fade(#299, 15%);
|
||||
border-bottom : #299 solid 1px;
|
||||
}
|
||||
.block{
|
||||
color : purple;
|
||||
font-weight : bold;
|
||||
//font-style: italic;
|
||||
}
|
||||
.inline-block{
|
||||
color : red;
|
||||
font-weight : bold;
|
||||
//font-style: italic;
|
||||
}
|
||||
}
|
||||
|
||||
.brewJump{
|
||||
@@ -26,4 +42,4 @@
|
||||
.tooltipLeft("Jump to brew page");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
require('./metadataEditor.less');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
@@ -16,7 +17,8 @@ const MetadataEditor = createClass({
|
||||
tags : '',
|
||||
published : false,
|
||||
authors : [],
|
||||
systems : []
|
||||
systems : [],
|
||||
renderer : 'legacy'
|
||||
},
|
||||
onChange : ()=>{}
|
||||
};
|
||||
@@ -35,6 +37,12 @@ const MetadataEditor = createClass({
|
||||
}
|
||||
this.props.onChange(this.props.metadata);
|
||||
},
|
||||
handleRenderer : function(renderer, e){
|
||||
if(e.target.checked){
|
||||
this.props.metadata.renderer = renderer;
|
||||
}
|
||||
this.props.onChange(this.props.metadata);
|
||||
},
|
||||
handlePublish : function(val){
|
||||
this.props.onChange(_.merge({}, this.props.metadata, {
|
||||
published : val
|
||||
@@ -42,7 +50,7 @@ const MetadataEditor = createClass({
|
||||
},
|
||||
|
||||
handleDelete : function(){
|
||||
if(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 REALLY sure? You will not be able to recover the document.')) return;
|
||||
} else {
|
||||
@@ -50,7 +58,7 @@ const MetadataEditor = createClass({
|
||||
if(!confirm('Are you REALLY sure? You will lose editor access to this document.')) return;
|
||||
}
|
||||
|
||||
request.get(`/api/remove/${this.props.metadata.editId}`)
|
||||
request.delete(`/api/${this.props.metadata.editId}`)
|
||||
.send()
|
||||
.end(function(err, res){
|
||||
window.location.href = '/';
|
||||
@@ -59,10 +67,12 @@ const MetadataEditor = createClass({
|
||||
|
||||
getRedditLink : function(){
|
||||
const meta = this.props.metadata;
|
||||
|
||||
const shareLink = (meta.googleId || '') + meta.shareId;
|
||||
const title = `${meta.title} [${meta.systems.join(' ')}]`;
|
||||
const text = `Hey guys! I've been working on this homebrew. I'd love your feedback. Check it out.
|
||||
|
||||
**[Homebrewery Link](http://homebrewery.naturalcrit.com/share/${meta.shareId})**`;
|
||||
**[Homebrewery Link](https://homebrewery.naturalcrit.com/share/${shareLink})**`;
|
||||
|
||||
return `https://www.reddit.com/r/UnearthedArcana/submit?title=${encodeURIComponent(title)}&text=${encodeURIComponent(text)}`;
|
||||
},
|
||||
@@ -82,11 +92,11 @@ const MetadataEditor = createClass({
|
||||
renderPublish : function(){
|
||||
if(this.props.metadata.published){
|
||||
return <button className='unpublish' onClick={()=>this.handlePublish(false)}>
|
||||
<i className='fa fa-ban' /> unpublish
|
||||
<i className='fas fa-ban' /> unpublish
|
||||
</button>;
|
||||
} else {
|
||||
return <button className='publish' onClick={()=>this.handlePublish(true)}>
|
||||
<i className='fa fa-globe' /> publish
|
||||
<i className='fas fa-globe' /> publish
|
||||
</button>;
|
||||
}
|
||||
},
|
||||
@@ -98,7 +108,7 @@ const MetadataEditor = createClass({
|
||||
<label>delete</label>
|
||||
<div className='value'>
|
||||
<button className='publish' onClick={this.handleDelete}>
|
||||
<i className='fa fa-trash' /> delete brew
|
||||
<i className='fas fa-trash-alt' /> delete brew
|
||||
</button>
|
||||
</div>
|
||||
</div>;
|
||||
@@ -106,7 +116,7 @@ const MetadataEditor = createClass({
|
||||
|
||||
renderAuthors : function(){
|
||||
let text = 'None.';
|
||||
if(this.props.metadata.authors.length){
|
||||
if(this.props.metadata.authors && this.props.metadata.authors.length){
|
||||
text = this.props.metadata.authors.join(', ');
|
||||
}
|
||||
return <div className='field authors'>
|
||||
@@ -125,13 +135,46 @@ const MetadataEditor = createClass({
|
||||
<div className='value'>
|
||||
<a href={this.getRedditLink()} target='_blank' rel='noopener noreferrer'>
|
||||
<button className='publish'>
|
||||
<i className='fa fa-reddit-alien' /> share to reddit
|
||||
<i className='fab fa-reddit-alien' /> share to reddit
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
</div>;
|
||||
},
|
||||
|
||||
renderRenderOptions : function(){
|
||||
if(!global.enable_v3) return;
|
||||
|
||||
return <div className='field systems'>
|
||||
<label>Renderer</label>
|
||||
<div className='value'>
|
||||
<label key='legacy'>
|
||||
<input
|
||||
type='radio'
|
||||
value = 'legacy'
|
||||
name = 'renderer'
|
||||
checked={this.props.metadata.renderer === 'legacy'}
|
||||
onChange={(e)=>this.handleRenderer('legacy', e)} />
|
||||
Legacy
|
||||
</label>
|
||||
|
||||
<label key='V3'>
|
||||
<input
|
||||
type='radio'
|
||||
value = 'V3'
|
||||
name = 'renderer'
|
||||
checked={this.props.metadata.renderer === 'V3'}
|
||||
onChange={(e)=>this.handleRenderer('V3', e)} />
|
||||
V3
|
||||
</label>
|
||||
|
||||
<a href='/v3_preview' target='_blank' rel='noopener noreferrer'>
|
||||
Click here for a quick intro to V3!
|
||||
</a>
|
||||
</div>
|
||||
</div>;
|
||||
},
|
||||
|
||||
render : function(){
|
||||
return <div className='metadataEditor'>
|
||||
<div className='field title'>
|
||||
@@ -153,6 +196,8 @@ const MetadataEditor = createClass({
|
||||
</div>
|
||||
*/}
|
||||
|
||||
{this.renderAuthors()}
|
||||
|
||||
<div className='field systems'>
|
||||
<label>systems</label>
|
||||
<div className='value'>
|
||||
@@ -160,7 +205,7 @@ const MetadataEditor = createClass({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{this.renderAuthors()}
|
||||
{this.renderRenderOptions()}
|
||||
|
||||
<div className='field publish'>
|
||||
<label>publish</label>
|
||||
|
||||
@@ -18,10 +18,11 @@
|
||||
font-weight : 800;
|
||||
line-height : 1.8em;
|
||||
text-transform : uppercase;
|
||||
flex-grow : 0;
|
||||
flex : 0 0 auto;
|
||||
}
|
||||
&>.value{
|
||||
flex-grow : 1;
|
||||
flex : 1 1 auto;
|
||||
min-width : 200px;
|
||||
}
|
||||
}
|
||||
.description.field textarea.value{
|
||||
@@ -38,15 +39,27 @@
|
||||
font-size : 0.7em;
|
||||
font-weight : 800;
|
||||
user-select : none;
|
||||
white-space : nowrap;
|
||||
display : inline-flex;
|
||||
align-items : center;
|
||||
}
|
||||
a {
|
||||
font-size : 0.7em;
|
||||
font-weight : 800;
|
||||
display : inline-flex;
|
||||
}
|
||||
input{
|
||||
vertical-align : middle;
|
||||
cursor : pointer;
|
||||
margin : 3px;
|
||||
}
|
||||
}
|
||||
.publish.field .value{
|
||||
position : relative;
|
||||
margin-bottom: 15px;
|
||||
button{
|
||||
width:100%;
|
||||
}
|
||||
button.publish{
|
||||
.button(@blueLight);
|
||||
}
|
||||
@@ -54,9 +67,6 @@
|
||||
.button(@silver);
|
||||
}
|
||||
small{
|
||||
position : absolute;
|
||||
bottom : -15px;
|
||||
left : 0px;
|
||||
font-size : 0.6em;
|
||||
font-style : italic;
|
||||
}
|
||||
@@ -76,4 +86,4 @@
|
||||
font-size: 0.8em;
|
||||
line-height : 1.5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,34 @@
|
||||
require('./snippetbar.less');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
const cx = require('classnames');
|
||||
|
||||
|
||||
const Snippets = require('./snippets/snippets.js');
|
||||
const SnippetsLegacy = require('./snippetsLegacy/snippets.js');
|
||||
const SnippetsV3 = require('./snippets/snippets.js');
|
||||
|
||||
const execute = function(val, brew){
|
||||
if(_.isFunction(val)) return val(brew);
|
||||
return val;
|
||||
};
|
||||
|
||||
|
||||
|
||||
const Snippetbar = createClass({
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
brew : '',
|
||||
onInject : ()=>{},
|
||||
onToggle : ()=>{},
|
||||
showmeta : false
|
||||
brew : {},
|
||||
view : 'text',
|
||||
onViewChange : ()=>{},
|
||||
onInject : ()=>{},
|
||||
onToggle : ()=>{},
|
||||
showEditButtons : true,
|
||||
renderer : 'legacy'
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState : function() {
|
||||
return {
|
||||
renderer : this.props.renderer
|
||||
};
|
||||
},
|
||||
|
||||
@@ -28,7 +37,14 @@ const Snippetbar = createClass({
|
||||
},
|
||||
|
||||
renderSnippetGroups : function(){
|
||||
return _.map(Snippets, (snippetGroup)=>{
|
||||
let snippets = [];
|
||||
|
||||
if(this.props.renderer === 'V3')
|
||||
snippets = SnippetsV3.filter((snippetGroup)=>snippetGroup.view === this.props.view);
|
||||
else
|
||||
snippets = SnippetsLegacy.filter((snippetGroup)=>snippetGroup.view === this.props.view);
|
||||
|
||||
return _.map(snippets, (snippetGroup)=>{
|
||||
return <SnippetGroup
|
||||
brew={this.props.brew}
|
||||
groupName={snippetGroup.groupName}
|
||||
@@ -40,13 +56,29 @@ const Snippetbar = createClass({
|
||||
});
|
||||
},
|
||||
|
||||
renderEditorButtons : function(){
|
||||
if(!this.props.showEditButtons) return;
|
||||
|
||||
return <div className='editors'>
|
||||
<div className={cx('text', { selected: this.props.view === 'text' })}
|
||||
onClick={()=>this.props.onViewChange('text')}>
|
||||
<i className='fa fa-beer' />
|
||||
</div>
|
||||
<div className={cx('style', { selected: this.props.view === 'style' })}
|
||||
onClick={()=>this.props.onViewChange('style')}>
|
||||
<i className='fa fa-paint-brush' />
|
||||
</div>
|
||||
<div className={cx('meta', { selected: this.props.view === 'meta' })}
|
||||
onClick={()=>this.props.onViewChange('meta')}>
|
||||
<i className='fas fa-info-circle' />
|
||||
</div>
|
||||
</div>;
|
||||
},
|
||||
|
||||
render : function(){
|
||||
return <div className='snippetBar'>
|
||||
{this.renderSnippetGroups()}
|
||||
<div className={cx('toggleMeta', { selected: this.props.showmeta })}
|
||||
onClick={this.props.onToggle}>
|
||||
<i className='fa fa-bars' />
|
||||
</div>
|
||||
{this.renderEditorButtons()}
|
||||
</div>;
|
||||
}
|
||||
});
|
||||
@@ -61,9 +93,9 @@ module.exports = Snippetbar;
|
||||
const SnippetGroup = createClass({
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
brew : '',
|
||||
brew : {},
|
||||
groupName : '',
|
||||
icon : 'fa-rocket',
|
||||
icon : 'fas fa-rocket',
|
||||
snippets : [],
|
||||
onSnippetClick : function(){},
|
||||
};
|
||||
@@ -74,16 +106,16 @@ const SnippetGroup = createClass({
|
||||
renderSnippets : function(){
|
||||
return _.map(this.props.snippets, (snippet)=>{
|
||||
return <div className='snippet' key={snippet.name} onClick={()=>this.handleSnippetClick(snippet)}>
|
||||
<i className={`fa fa-fw ${snippet.icon}`} />
|
||||
<i className={snippet.icon} />
|
||||
{snippet.name}
|
||||
</div>;
|
||||
});
|
||||
},
|
||||
|
||||
render : function(){
|
||||
return <div className='snippetGroup'>
|
||||
return <div className='snippetGroup snippetBarButton'>
|
||||
<div className='text'>
|
||||
<i className={`fa fa-fw ${this.props.icon}`} />
|
||||
<i className={this.props.icon} />
|
||||
<span className='groupName'>{this.props.groupName}</span>
|
||||
</div>
|
||||
<div className='dropdown'>
|
||||
@@ -92,4 +124,4 @@ const SnippetGroup = createClass({
|
||||
</div>;
|
||||
},
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,47 +1,64 @@
|
||||
|
||||
.snippetBar{
|
||||
@height : 25px;
|
||||
@menuHeight : 25px;
|
||||
position : relative;
|
||||
height : @height;
|
||||
height : @menuHeight;
|
||||
background-color : #ddd;
|
||||
.toggleMeta{
|
||||
position : absolute;
|
||||
top : 0px;
|
||||
right : 0px;
|
||||
height : @height;
|
||||
width : @height;
|
||||
cursor : pointer;
|
||||
line-height : @height;
|
||||
text-align : center;
|
||||
.tooltipLeft("Edit Brew Metadata");
|
||||
.editors{
|
||||
position : absolute;
|
||||
display : flex;
|
||||
top : 0px;
|
||||
right : 0px;
|
||||
height : @menuHeight;
|
||||
width : 90px;
|
||||
justify-content : space-between;
|
||||
&>div{
|
||||
height : @menuHeight;
|
||||
width : @menuHeight;
|
||||
cursor : pointer;
|
||||
line-height : @menuHeight;
|
||||
text-align : center;
|
||||
&:hover,&.selected{
|
||||
background-color : #999;
|
||||
}
|
||||
&.text{
|
||||
.tooltipLeft('Brew Editor');
|
||||
}
|
||||
&.style{
|
||||
.tooltipLeft('Style Editor');
|
||||
}
|
||||
&.meta{
|
||||
.tooltipLeft('Properties');
|
||||
}
|
||||
}
|
||||
}
|
||||
.snippetBarButton{
|
||||
height : @menuHeight;
|
||||
line-height : @menuHeight;
|
||||
display : inline-block;
|
||||
padding : 0px 5px;
|
||||
font-weight : 800;
|
||||
font-size : 0.625em;
|
||||
text-transform : uppercase;
|
||||
cursor : pointer;
|
||||
&:hover, &.selected{
|
||||
background-color : #999;
|
||||
}
|
||||
}
|
||||
.snippetGroup{
|
||||
display : inline-block;
|
||||
height : @height;
|
||||
padding : 0px 5px;
|
||||
cursor : pointer;
|
||||
font-size : 0.6em;
|
||||
font-weight : 800;
|
||||
line-height : @height;
|
||||
text-transform : uppercase;
|
||||
border-right : 1px solid black;
|
||||
i{
|
||||
vertical-align : middle;
|
||||
margin-right : 3px;
|
||||
font-size : 1.2em;
|
||||
}
|
||||
&:hover, &.selected{
|
||||
background-color : #999;
|
||||
}
|
||||
.text{
|
||||
line-height : @height;
|
||||
.groupName{
|
||||
font-size : 10px;
|
||||
}
|
||||
font-size : 1.4em;
|
||||
}
|
||||
}
|
||||
.toggleMeta{
|
||||
position : absolute;
|
||||
top : 0px;
|
||||
right : 0px;
|
||||
border-left : 1px solid black;
|
||||
.tooltipLeft("Edit Brew Properties");
|
||||
}
|
||||
.snippetGroup{
|
||||
border-right : 1px solid black;
|
||||
&:hover{
|
||||
.dropdown{
|
||||
visibility : visible;
|
||||
@@ -62,7 +79,7 @@
|
||||
font-size : 10px;
|
||||
i{
|
||||
margin-right : 8px;
|
||||
font-size : 13px;
|
||||
font-size : 1.2em;
|
||||
}
|
||||
&:hover{
|
||||
background-color : #999;
|
||||
@@ -70,4 +87,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const _ = require('lodash');
|
||||
const dedent = require('dedent-tabs').default;
|
||||
|
||||
module.exports = function(classname){
|
||||
|
||||
@@ -10,33 +11,32 @@ module.exports = function(classname){
|
||||
const hitDie = _.sample([4, 6, 8, 10, 12]);
|
||||
|
||||
const abilityList = ['Strength', 'Dexerity', 'Constitution', 'Wisdom', 'Charisma', 'Intelligence'];
|
||||
const skillList = ['Acrobatics ', 'Animal Handling', 'Arcana', 'Athletics', 'Deception', 'History', 'Insight', 'Intimidation', 'Investigation', 'Medicine', 'Nature', 'Perception', 'Performance', 'Persuasion', 'Religion', 'Sleight of Hand', 'Stealth', 'Survival'];
|
||||
const skillList = ['Acrobatics', 'Animal Handling', 'Arcana', 'Athletics', 'Deception', 'History', 'Insight', 'Intimidation', 'Investigation', 'Medicine', 'Nature', 'Perception', 'Performance', 'Persuasion', 'Religion', 'Sleight of Hand', 'Stealth', 'Survival'];
|
||||
|
||||
|
||||
return [
|
||||
'## Class Features',
|
||||
`As a ${classname}, you gain the following class features`,
|
||||
'#### Hit Points',
|
||||
'___',
|
||||
`- **Hit Dice:** 1d${hitDie} per ${classname} level`,
|
||||
`- **Hit Points at 1st Level:** ${hitDie} + your Constitution modifier`,
|
||||
`- **Hit Points at Higher Levels:** 1d${hitDie} (or ${hitDie/2 + 1}) + your Constitution modifier per ${classname} level after 1st`,
|
||||
'',
|
||||
'#### Proficiencies',
|
||||
'___',
|
||||
`- **Armor:** ${_.sampleSize(['Light armor', 'Medium armor', 'Heavy armor', 'Shields'], _.random(0, 3)).join(', ') || 'None'}`,
|
||||
`- **Weapons:** ${_.sampleSize(['Squeegee', 'Rubber Chicken', 'Simple weapons', 'Martial weapons'], _.random(0, 2)).join(', ') || 'None'}`,
|
||||
`- **Tools:** ${_.sampleSize(['Artian\'s tools', 'one musical instrument', 'Thieve\'s tools'], _.random(0, 2)).join(', ') || 'None'}`,
|
||||
'',
|
||||
'___',
|
||||
`- **Saving Throws:** ${_.sampleSize(abilityList, 2).join(', ')}`,
|
||||
`- **Skills:** Choose two from ${_.sampleSize(skillList, _.random(4, 6)).join(', ')}`,
|
||||
'',
|
||||
'#### Equipment',
|
||||
'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)* five javelins or *(b)* any simple melee weapon',
|
||||
`- ${_.sample(['10 lint fluffs', '1 button', 'a cherished lost sock'])}`,
|
||||
'\n\n\n'
|
||||
].join('\n');
|
||||
return dedent`
|
||||
## Class Features
|
||||
As a ${classname}, you gain the following class features
|
||||
#### Hit Points
|
||||
|
||||
**Hit Dice:** :: 1d${hitDie} per ${classname} level
|
||||
**Hit Points at 1st Level:** :: ${hitDie} + your Constitution modifier
|
||||
**Hit Points at Higher Levels:** :: 1d${hitDie} (or ${hitDie/2 + 1}) + your Constitution modifier per ${classname} level after 1st
|
||||
|
||||
#### Proficiencies
|
||||
|
||||
**Armor:** :: ${_.sampleSize(['Light armor', 'Medium armor', 'Heavy armor', 'Shields'], _.random(0, 3)).join(', ') || 'None'}
|
||||
**Weapons:** :: ${_.sampleSize(['Squeegee', 'Rubber Chicken', 'Simple weapons', 'Martial weapons'], _.random(0, 2)).join(', ') || 'None'}
|
||||
**Tools:** :: ${_.sampleSize(['Artian\'s tools', 'one musical instrument', 'Thieve\'s tools'], _.random(0, 2)).join(', ') || 'None'}
|
||||
|
||||
**Saving Throws:** :: ${_.sampleSize(abilityList, 2).join(', ')}
|
||||
**Skills:** :: Choose two from ${_.sampleSize(skillList, _.random(4, 6)).join(', ')}
|
||||
|
||||
#### Equipment
|
||||
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)* five javelins or *(b)* any simple melee weapon
|
||||
- ${_.sample(['10 lint fluffs', '1 button', 'a cherished lost sock'])}
|
||||
|
||||
`;
|
||||
};
|
||||
|
||||
@@ -2,85 +2,77 @@ const _ = require('lodash');
|
||||
|
||||
const features = [
|
||||
'Astrological Botany',
|
||||
'Astrological Chemistry',
|
||||
'Biochemical Sorcery',
|
||||
'Civil Alchemy',
|
||||
'Consecrated Biochemistry',
|
||||
'Civil Divination',
|
||||
'Consecrated Augury',
|
||||
'Demonic Anthropology',
|
||||
'Divinatory Mineralogy',
|
||||
'Genetic Banishing',
|
||||
'Hermetic Geography',
|
||||
'Immunological Incantations',
|
||||
'Nuclear Illusionism',
|
||||
'Ritual Astronomy',
|
||||
'Seismological Divination',
|
||||
'Spiritual Biochemistry',
|
||||
'Statistical Occultism',
|
||||
'Police Necromancer',
|
||||
'Sixgun Poisoner',
|
||||
'Pharmaceutical Gunslinger',
|
||||
'Infernal Banker',
|
||||
'Spell Analyst',
|
||||
'Gunslinger Corruptor',
|
||||
'Torque Interfacer',
|
||||
'Exo Interfacer',
|
||||
'Genetic Banishing',
|
||||
'Gunpowder Torturer',
|
||||
'Orbital Gravedigger',
|
||||
'Phased Linguist',
|
||||
'Mathematical Pharmacist',
|
||||
'Plasma Outlaw',
|
||||
'Gunslinger Corruptor',
|
||||
'Hermetic Geography',
|
||||
'Immunological Cultist',
|
||||
'Malefic Chemist',
|
||||
'Police Cultist'
|
||||
'Mathematical Pharmacy',
|
||||
'Nuclear Biochemistry',
|
||||
'Orbital Gravedigger',
|
||||
'Pharmaceutical Outlaw',
|
||||
'Phased Linguist',
|
||||
'Plasma Gunslinger',
|
||||
'Police Necromancer',
|
||||
'Ritual Astronomy',
|
||||
'Sixgun Poisoner',
|
||||
'Seismological Alchemy',
|
||||
'Spiritual Illusionism',
|
||||
'Statistical Occultism',
|
||||
'Spell Analyst',
|
||||
'Torque Interfacer'
|
||||
];
|
||||
|
||||
const classnames = ['Archivist', 'Fancyman', 'Linguist', 'Fletcher',
|
||||
'Notary', 'Berserker-Typist', 'Fishmongerer', 'Manicurist', 'Haberdasher', 'Concierge'];
|
||||
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 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 getFeature = (level)=>{
|
||||
let res = [];
|
||||
if(_.includes([4, 6, 8, 12, 14, 16, 19], level+1)){
|
||||
res = ['Ability Score Improvement'];
|
||||
}
|
||||
res = _.union(res, _.sampleSize(features, _.sample([0, 1, 1, 1, 1, 1])));
|
||||
if(!res.length) return '─';
|
||||
return res.join(', ');
|
||||
const maxes = [4, 3, 3, 3, 3, 2, 2, 1, 1];
|
||||
|
||||
const drawSlots = function(Slots, rows, padding){
|
||||
let slots = Number(Slots);
|
||||
return _.times(rows, function(i){
|
||||
const max = maxes[i];
|
||||
if(slots < 1) return _.pad('—', padding);
|
||||
const res = _.min([max, slots]);
|
||||
slots -= res;
|
||||
return _.pad(res.toString(), padding);
|
||||
}).join(' | ');
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
full : function(){
|
||||
full : function(classes){
|
||||
const classname = _.sample(classnames);
|
||||
|
||||
const maxes = [4, 3, 3, 3, 3, 2, 2, 1, 1];
|
||||
const drawSlots = function(Slots){
|
||||
let slots = Number(Slots);
|
||||
return _.times(9, function(i){
|
||||
const max = maxes[i];
|
||||
if(slots < 1) return '—';
|
||||
const res = _.min([max, slots]);
|
||||
slots -= res;
|
||||
return res;
|
||||
}).join(' | ');
|
||||
};
|
||||
|
||||
|
||||
let cantrips = 3;
|
||||
let spells = 1;
|
||||
let slots = 2;
|
||||
return `<div class='classTable wide'>\n##### The ${classname}\n` +
|
||||
`| Level | Proficiency Bonus | Features | Cantrips Known | Spells Known | 1st | 2nd | 3rd | 4th | 5th | 6th | 7th | 8th | 9th |\n`+
|
||||
`|:---:|:---:|:---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|\n${
|
||||
return `{{${classes}\n##### The ${classname}\n` +
|
||||
`| Level | Proficiency | Features | Cantrips | Spells | --- Spell Slots Per Spell Level ---|||||||||\n`+
|
||||
`| ^| Bonus ^| ^| Known ^| Known ^|1st |2nd |3rd |4th |5th |6th |7th |8th |9th |\n`+
|
||||
`|:-----:|:-----------:|:-------------|:--------:|:------:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|\n${
|
||||
_.map(levels, function(levelName, level){
|
||||
const res = [
|
||||
levelName,
|
||||
`+${profBonus[level]}`,
|
||||
getFeature(level),
|
||||
cantrips,
|
||||
spells,
|
||||
drawSlots(slots)
|
||||
_.pad(levelName, 5),
|
||||
_.pad(`+${profBonus[level]}`, 2),
|
||||
_.padEnd(_.sample(features), 21),
|
||||
_.pad(cantrips.toString(), 8),
|
||||
_.pad(spells.toString(), 6),
|
||||
drawSlots(slots, 9, 2),
|
||||
].join(' | ');
|
||||
|
||||
cantrips += _.random(0, 1);
|
||||
@@ -88,27 +80,53 @@ module.exports = {
|
||||
slots += _.random(0, 2);
|
||||
|
||||
return `| ${res} |`;
|
||||
}).join('\n')}\n</div>\n\n`;
|
||||
}).join('\n')}\n}}\n\n`;
|
||||
},
|
||||
|
||||
half : function(){
|
||||
half : function(classes){
|
||||
const classname = _.sample(classnames);
|
||||
|
||||
let featureScore = 1;
|
||||
return `<div class='classTable'>\n##### The ${classname}\n` +
|
||||
`| Level | Proficiency Bonus | Features | ${_.sample(features)}|\n` +
|
||||
`|:---:|:---:|:---|:---:|\n${
|
||||
return `{{${classes}\n##### The ${classname}\n` +
|
||||
`| Level | Proficiency Bonus | Features | ${_.pad(_.sample(features), 21)} |\n` +
|
||||
`|:-----:|:-----------------:|:---------|:---------------------:|\n${
|
||||
_.map(levels, function(levelName, level){
|
||||
const res = [
|
||||
levelName,
|
||||
`+${profBonus[level]}`,
|
||||
getFeature(level),
|
||||
`+${featureScore}`
|
||||
_.pad(levelName, 5),
|
||||
_.pad(`+${profBonus[level]}`, 2),
|
||||
_.padEnd(_.sample(features), 23),
|
||||
_.pad(`+${featureScore}`, 21),
|
||||
].join(' | ');
|
||||
|
||||
featureScore += _.random(0, 1);
|
||||
|
||||
return `| ${res} |`;
|
||||
}).join('\n')}\n</div>\n\n`;
|
||||
}).join('\n')}\n}}\n\n`;
|
||||
},
|
||||
|
||||
third : function(classes){
|
||||
const classname = _.sample(classnames);
|
||||
|
||||
let cantrips = 3;
|
||||
let spells = 1;
|
||||
let slots = 2;
|
||||
return `{{${classes}\n##### ${classname} Spellcasting\n` +
|
||||
`| Class | Cantrips | Spells |--- Spells Slots per Spell Level ---||||\n` +
|
||||
`| Level ^| Known ^| Known ^| 1st | 2nd | 3rd | 4th |\n` +
|
||||
`|:------:|:--------:|:-------:|:-------:|:-------:|:-------:|:-------:|\n${
|
||||
_.map(levels, function(levelName, level){
|
||||
const res = [
|
||||
_.pad(levelName, 6),
|
||||
_.pad(cantrips.toString(), 8),
|
||||
_.pad(spells.toString(), 7),
|
||||
drawSlots(slots, 4, 7),
|
||||
].join(' | ');
|
||||
|
||||
cantrips += _.random(0, 1);
|
||||
spells += _.random(0, 1);
|
||||
slots += _.random(0, 1);
|
||||
|
||||
return `| ${res} |`;
|
||||
}).join('\n')}\n}}\n\n`;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -102,6 +102,13 @@ module.exports = ()=>{
|
||||
return `<style>
|
||||
.phb#p1{ text-align:center; }
|
||||
.phb#p1:after{ display:none; }
|
||||
.phb#p2 { counter-reset:phb-page-numbers; }
|
||||
.phb:nth-child(2n) .pageNumber { left: inherit !important; right: 2px !important; }
|
||||
.phb:nth-child(2n+1) .pageNumber { right: inherit !important; left: 2px !important; }
|
||||
.phb:nth-child(2n)::after { transform: scaleX(1); }
|
||||
.phb:nth-child(2n+1)::after { transform: scaleX(-1); }
|
||||
.phb:nth-child(2n) .footnote { left: inherit; text-align: right; }
|
||||
.phb:nth-child(2n+1) .footnote { left: 80px; text-align: left; }
|
||||
</style>
|
||||
|
||||
<div style='margin-top:450px;'></div>
|
||||
|
||||
@@ -47,20 +47,26 @@ const spellNames = [
|
||||
'Ultimate Rite of the Confetti Angel',
|
||||
'Ultimate Ritual of Mouthwash',
|
||||
];
|
||||
const itemNames = [
|
||||
'Doorknob of Niceness',
|
||||
'Paper Armor of Folding',
|
||||
'Mixtape of Sadness',
|
||||
'Staff of Endless Confetti',
|
||||
];
|
||||
|
||||
module.exports = {
|
||||
|
||||
spellList : function(){
|
||||
const levels = ['Cantrips (0 Level)', '2nd Level', '3rd Level', '4th Level', '5th Level', '6th Level', '7th Level', '8th Level', '9th Level'];
|
||||
const levels = ['Cantrips (0 Level)', '1st Level', '2nd Level', '3rd Level', '4th Level', '5th Level', '6th Level', '7th Level', '8th Level', '9th Level'];
|
||||
|
||||
const content = _.map(levels, (level)=>{
|
||||
const spells = _.map(_.sampleSize(spellNames, _.random(5, 15)), (spell)=>{
|
||||
const spells = _.map(_.sampleSize(spellNames, _.random(4, 10)), (spell)=>{
|
||||
return `- ${spell}`;
|
||||
}).join('\n');
|
||||
return `##### ${level} \n${spells} \n`;
|
||||
}).join('\n');
|
||||
|
||||
return `<div class='spellList'>\n${content}\n</div>`;
|
||||
return `{{spellList,wide\n${content}\n}}`;
|
||||
},
|
||||
|
||||
spell : function(){
|
||||
@@ -76,16 +82,28 @@ module.exports = {
|
||||
return [
|
||||
`#### ${_.sample(spellNames)}`,
|
||||
`*${_.sample(level)}-level ${_.sample(spellSchools)}*`,
|
||||
'___',
|
||||
'- **Casting Time:** 1 action',
|
||||
`- **Range:** ${_.sample(['Self', 'Touch', '30 feet', '60 feet'])}`,
|
||||
`- **Components:** ${components}`,
|
||||
`- **Duration:** ${_.sample(['Until dispelled', '1 round', 'Instantaneous', 'Concentration, up to 10 minutes', '1 hour'])}`,
|
||||
'',
|
||||
'A flame, equivalent in brightness to a torch, springs from from an object that you touch. ',
|
||||
'**Casting Time:** :: 1 action',
|
||||
`**Range:** :: ${_.sample(['Self', 'Touch', '30 feet', '60 feet'])}`,
|
||||
`**Components:** :: ${components}`,
|
||||
`**Duration:** :: ${_.sample(['Until dispelled', '1 round', 'Instantaneous', 'Concentration, up to 10 minutes', '1 hour'])}`,
|
||||
'',
|
||||
'A flame, equivalent in brightness to a torch, springs from an object that you touch. ',
|
||||
'The effect look like a regular flame, but it creates no heat and doesn\'t use oxygen. ',
|
||||
'A *continual flame* can be covered or hidden but not smothered or quenched.',
|
||||
'\n\n\n'
|
||||
].join('\n');
|
||||
},
|
||||
|
||||
item : function() {
|
||||
return [
|
||||
`#### ${_.sample(itemNames)}`,
|
||||
`*${_.sample(['Wondrous item', 'Armor', 'Weapon'])}, ${_.sample(['Common', 'Uncommon', 'Rare', 'Very Rare', 'Legendary', 'Artifact'])} (requires attunement)*`,
|
||||
`:`,
|
||||
`This knob is pretty nice. When attached to a door, it allows a user to`,
|
||||
`open that door with the strength of the nearest animal. For example, if`,
|
||||
`there is a cow nearby, the user will have the "strength of a cow" while`,
|
||||
`opening this door.`
|
||||
].join('\n');
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const _ = require('lodash');
|
||||
const dedent = require('dedent-tabs').default;
|
||||
|
||||
const genList = function(list, max){
|
||||
return _.sampleSize(list, _.random(0, max)).join(', ') || 'None';
|
||||
@@ -86,7 +87,7 @@ const getAlignment = function(){
|
||||
};
|
||||
|
||||
const getStats = function(){
|
||||
return `>|${_.times(6, function(){
|
||||
return `|${_.times(6, function(){
|
||||
const num = _.random(1, 20);
|
||||
const mod = Math.ceil(num/2 - 5);
|
||||
return `${num} (${mod >= 0 ? `+${mod}` : mod})`;
|
||||
@@ -95,12 +96,12 @@ const getStats = function(){
|
||||
|
||||
const genAbilities = function(){
|
||||
return _.sample([
|
||||
'> ***Pack Tactics.*** These guys work together. Like super well, you don\'t even know.',
|
||||
'> ***Fowl Appearance.*** While the creature remains motionless, it is indistinguishable from a normal chicken.',
|
||||
'> ***Onion Stench.*** Any creatures within 5 feet of this thing develops an irrational craving for onion rings.',
|
||||
'> ***Enormous Nose.*** This creature gains advantage on any check involving putting things in its nose.',
|
||||
'> ***Sassiness.*** When questioned, this creature will talk back instead of answering.',
|
||||
'> ***Big Jerk.*** Thinks he is just *waaaay* better than you.',
|
||||
'***Pack Tactics.*** These guys work together like peanut butter and jelly.',
|
||||
'***Fowl Appearance.*** While the creature remains motionless, it is indistinguishable from a normal chicken.',
|
||||
'***Onion Stench.*** Any creatures within 5 feet of this thing develops an irrational craving for onion rings.',
|
||||
'***Enormous Nose.*** This creature gains advantage on any check involving putting things in its nose.',
|
||||
'***Sassiness.*** When questioned, this creature will talk back instead of answering.',
|
||||
'***Big Jerk.*** Whenever this creature makes an attack, it starts telling you how much cooler it is than you.',
|
||||
]);
|
||||
};
|
||||
|
||||
@@ -133,68 +134,37 @@ const genAction = function(){
|
||||
'Turnbuckle Roll'
|
||||
]);
|
||||
|
||||
return `> ***${name}.*** *Melee Weapon Attack:* +4 to hit, reach 5ft., one target. *Hit* 5 (1d6 + 2) `;
|
||||
return `***${name}.*** *Melee Weapon Attack:* +4 to hit, reach 5ft., one target. *Hit* 5 (1d6 + 2) `;
|
||||
};
|
||||
|
||||
|
||||
module.exports = {
|
||||
|
||||
full : function(){
|
||||
return `${[
|
||||
'___',
|
||||
'___',
|
||||
`> ## ${getMonsterName()}`,
|
||||
`>*${getType()}, ${getAlignment()}*`,
|
||||
'> ___',
|
||||
`> - **Armor Class** ${_.random(10, 20)}`,
|
||||
`> - **Hit Points** ${_.random(1, 150)}(1d4 + 5)`,
|
||||
`> - **Speed** ${_.random(0, 50)}ft.`,
|
||||
'>___',
|
||||
'>|STR|DEX|CON|INT|WIS|CHA|',
|
||||
'>|:---:|:---:|:---:|:---:|:---:|:---:|',
|
||||
getStats(),
|
||||
'>___',
|
||||
`> - **Condition Immunities** ${genList(['groggy', 'swagged', 'weak-kneed', 'buzzed', 'groovy', 'melancholy', 'drunk'], 3)}`,
|
||||
`> - **Senses** passive Perception ${_.random(3, 20)}`,
|
||||
`> - **Languages** ${genList(['Common', 'Pottymouth', 'Gibberish', 'Latin', 'Jive'], 2)}`,
|
||||
`> - **Challenge** ${_.random(0, 15)} (${_.random(10, 10000)} XP)`,
|
||||
'> ___',
|
||||
_.times(_.random(3, 6), function(){
|
||||
return genAbilities();
|
||||
}).join('\n>\n'),
|
||||
'> ### Actions',
|
||||
_.times(_.random(4, 6), function(){
|
||||
return genAction();
|
||||
}).join('\n>\n'),
|
||||
].join('\n')}\n\n\n`;
|
||||
},
|
||||
|
||||
half : function(){
|
||||
return `${[
|
||||
'___',
|
||||
`> ## ${getMonsterName()}`,
|
||||
`>*${getType()}, ${getAlignment()}*`,
|
||||
'> ___',
|
||||
`> - **Armor Class** ${_.random(10, 20)}`,
|
||||
`> - **Hit Points** ${_.random(1, 150)}(1d4 + 5)`,
|
||||
`> - **Speed** ${_.random(0, 50)}ft.`,
|
||||
'>___',
|
||||
'>|STR|DEX|CON|INT|WIS|CHA|',
|
||||
'>|:---:|:---:|:---:|:---:|:---:|:---:|',
|
||||
getStats(),
|
||||
'>___',
|
||||
`> - **Condition Immunities** ${genList(['groggy', 'swagged', 'weak-kneed', 'buzzed', 'groovy', 'melancholy', 'drunk'], 3)}`,
|
||||
`> - **Senses** passive Perception ${_.random(3, 20)}`,
|
||||
`> - **Languages** ${genList(['Common', 'Pottymouth', 'Gibberish', 'Latin', 'Jive'], 2)}`,
|
||||
`> - **Challenge** ${_.random(0, 15)} (${_.random(10, 10000)} XP)`,
|
||||
'> ___',
|
||||
_.times(_.random(2, 3), function(){
|
||||
return genAbilities();
|
||||
}).join('\n>\n'),
|
||||
'> ### Actions',
|
||||
_.times(_.random(1, 2), function(){
|
||||
return genAction();
|
||||
}).join('\n>\n'),
|
||||
].join('\n')}\n\n\n`;
|
||||
monster : function(classes, genLines){
|
||||
return dedent`
|
||||
{{${classes}
|
||||
## ${getMonsterName()}
|
||||
*${getType()}, ${getAlignment()}*
|
||||
___
|
||||
**Armor Class** :: ${_.random(10, 20)} (chain mail, shield)
|
||||
**Hit Points** :: ${_.random(1, 150)}(1d4 + 5)
|
||||
**Speed** :: ${_.random(0, 50)}ft.
|
||||
___
|
||||
| STR | DEX | CON | INT | WIS | CHA |
|
||||
|:-----:|:-----:|:-----:|:-----:|:-----:|:-----:|
|
||||
${getStats()}
|
||||
___
|
||||
**Condition Immunities** :: ${genList(['groggy', 'swagged', 'weak-kneed', 'buzzed', 'groovy', 'melancholy', 'drunk'], 3)}
|
||||
**Senses** :: darkvision 60 ft., passive Perception ${_.random(3, 20)}
|
||||
**Languages** :: ${genList(['Common', 'Pottymouth', 'Gibberish', 'Latin', 'Jive'], 2)}
|
||||
**Challenge** :: ${_.random(0, 15)} (${_.random(10, 10000)} XP)
|
||||
___
|
||||
:
|
||||
${_.times(_.random(genLines, genLines + 2), function(){return genAbilities();}).join('\n\t\t\t\n\t\t\t')}
|
||||
:
|
||||
### Actions
|
||||
${_.times(_.random(genLines, genLines + 2), function(){return genAction();}).join('\n\t\t\t\n\t\t\t')}
|
||||
}}
|
||||
\n`;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -6,79 +6,151 @@ const MonsterBlockGen = require('./monsterblock.gen.js');
|
||||
const ClassFeatureGen = require('./classfeature.gen.js');
|
||||
const CoverPageGen = require('./coverpage.gen.js');
|
||||
const TableOfContentsGen = require('./tableOfContents.gen.js');
|
||||
const dedent = require('dedent-tabs').default;
|
||||
const watercolorGen = require('./watercolor.gen.js');
|
||||
|
||||
|
||||
module.exports = [
|
||||
|
||||
{
|
||||
groupName : 'Editor',
|
||||
icon : 'fa-pencil',
|
||||
icon : 'fas fa-pencil-alt',
|
||||
view : 'text',
|
||||
snippets : [
|
||||
{
|
||||
name : 'Column Break',
|
||||
icon : 'fa-columns',
|
||||
gen : '```\n```\n\n'
|
||||
icon : 'fas fa-columns',
|
||||
gen : '\n\\column\n'
|
||||
},
|
||||
{
|
||||
name : 'New Page',
|
||||
icon : 'fa-file-text',
|
||||
gen : '\\page\n\n'
|
||||
icon : 'fas fa-file-alt',
|
||||
gen : '\n\\page\n'
|
||||
},
|
||||
{
|
||||
name : 'Vertical Spacing',
|
||||
icon : 'fa-arrows-v',
|
||||
gen : '<div style=\'margin-top:140px\'></div>\n\n'
|
||||
icon : 'fas fa-arrows-alt-v',
|
||||
gen : '\n::::\n'
|
||||
},
|
||||
{
|
||||
name : 'Horizontal Spacing',
|
||||
icon : 'fas fa-arrows-alt-h',
|
||||
gen : ' {{width:100px}} '
|
||||
},
|
||||
{
|
||||
name : 'Wide Block',
|
||||
icon : 'fa-arrows-h',
|
||||
gen : '<div class=\'wide\'>\nEverything in here will be extra wide. Tables, text, everything! Beware though, CSS columns can behave a bit weird sometimes.\n</div>\n'
|
||||
icon : 'fas fa-window-maximize',
|
||||
gen : dedent`\n
|
||||
{{wide
|
||||
Everything in here will be extra wide. Tables, text, everything!
|
||||
Beware though, CSS columns can behave a bit weird sometimes. You may
|
||||
have to manually place column breaks with \`\column\` to make the
|
||||
surrounding text flow with this wide block the way you want.
|
||||
}}
|
||||
\n`
|
||||
},
|
||||
{
|
||||
name : 'QR Code',
|
||||
icon : 'fas fa-qrcode',
|
||||
gen : (brew)=>{
|
||||
return `![]` +
|
||||
`(https://api.qrserver.com/v1/create-qr-code/?data=` +
|
||||
`https://homebrewery.naturalcrit.com/share/${brew.shareId}` +
|
||||
`&size=100x100) {width:100px;mix-blend-mode:multiply}`;
|
||||
}
|
||||
},
|
||||
{
|
||||
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 : 'Link to page',
|
||||
icon : 'fas fa-link',
|
||||
gen : '[Click here](#p3) to go to page 3\n'
|
||||
},
|
||||
{
|
||||
name : 'Table of Contents',
|
||||
icon : 'fas fa-book',
|
||||
gen : TableOfContentsGen
|
||||
},
|
||||
{
|
||||
name : 'Remove Drop Cap',
|
||||
icon : 'fas fa-remove-format',
|
||||
gen : '<style>\n' +
|
||||
' .phb3 h1+p:first-letter {\n' +
|
||||
' all: unset;\n' +
|
||||
' }\n' +
|
||||
'</style>'
|
||||
},
|
||||
{
|
||||
name : 'Tweak Drop Cap',
|
||||
icon : 'fas fa-sliders-h',
|
||||
gen : '<style>\n' +
|
||||
' /* Drop Cap settings */\n' +
|
||||
' .phb3 h1 + p::first-letter {\n' +
|
||||
' float: left;\n' +
|
||||
' font-family: SolberaImitationRemake;\n' +
|
||||
' font-size: 3.5cm;\n' +
|
||||
' color: #222;\n' +
|
||||
' line-height: .8em;\n' +
|
||||
' }\n' +
|
||||
'</style>'
|
||||
},
|
||||
{
|
||||
name : 'Add Comment',
|
||||
icon : 'fas fa-code', /* might need to be fa-solid fa-comment-code --not sure, Gazook */
|
||||
gen : dedent`\n
|
||||
<!-- This is a comment that will not be rendered into your brew. Hotkey (Ctrl/Cmd + /). -->
|
||||
`
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
/*********************** IMAGES *******************/
|
||||
{
|
||||
groupName : 'Images',
|
||||
icon : 'fas fa-images',
|
||||
view : 'text',
|
||||
snippets : [
|
||||
{
|
||||
name : 'Image',
|
||||
icon : 'fa-image',
|
||||
gen : [
|
||||
'<img ',
|
||||
' src=\'https://s-media-cache-ak0.pinimg.com/736x/4a/81/79/4a8179462cfdf39054a418efd4cb743e.jpg\' ',
|
||||
' style=\'width:325px\' />',
|
||||
'Credit: Kyounghwan Kim'
|
||||
].join('\n')
|
||||
icon : 'fas fa-image',
|
||||
gen : dedent`
|
||||
 {width:325px,mix-blend-mode:multiply}
|
||||
|
||||
{{artist,position:relative,top:-230px,left:-100px,margin-bottom:-30px
|
||||
##### Cat Warrior
|
||||
[Kyoung Hwan Kim](https://www.artstation.com/tahra)
|
||||
}}`
|
||||
},
|
||||
{
|
||||
name : 'Background Image',
|
||||
icon : 'fa-tree',
|
||||
gen : [
|
||||
'<img ',
|
||||
' src=\'http://i.imgur.com/hMna6G0.png\' ',
|
||||
' style=\'position:absolute; top:50px; right:30px; width:280px\' />'
|
||||
].join('\n')
|
||||
},
|
||||
icon : 'fas fa-tree',
|
||||
gen : dedent`
|
||||
 {position:absolute,top:50px,right:30px,width:280px}
|
||||
|
||||
{{artist,top:90px,right:30px
|
||||
##### Homebrew Mug
|
||||
[naturalcrit](https://homebrew.naturalcrit.com)
|
||||
}}`
|
||||
},
|
||||
{
|
||||
name : 'Page Number',
|
||||
icon : 'fa-bookmark',
|
||||
gen : '<div class=\'pageNumber\'>1</div>\n<div class=\'footnote\'>PART 1 | FANCINESS</div>\n\n'
|
||||
name : 'Watercolor Splatter',
|
||||
icon : 'fas fa-fill-drip',
|
||||
gen : watercolorGen,
|
||||
},
|
||||
|
||||
{
|
||||
name : 'Auto-incrementing Page Number',
|
||||
icon : 'fa-sort-numeric-asc',
|
||||
gen : '<div class=\'pageNumber auto\'></div>\n'
|
||||
name : 'Watermark',
|
||||
icon : 'fas fa-id-card',
|
||||
gen : dedent`
|
||||
{{watermark Homebrewery}}\n`
|
||||
},
|
||||
|
||||
{
|
||||
name : 'Link to page',
|
||||
icon : 'fa-link',
|
||||
gen : '[Click here](#p3) to go to page 3\n'
|
||||
},
|
||||
|
||||
{
|
||||
name : 'Table of Contents',
|
||||
icon : 'fa-book',
|
||||
gen : TableOfContentsGen
|
||||
},
|
||||
|
||||
|
||||
]
|
||||
},
|
||||
|
||||
@@ -87,64 +159,89 @@ module.exports = [
|
||||
|
||||
{
|
||||
groupName : 'PHB',
|
||||
icon : 'fa-book',
|
||||
icon : 'fas fa-book',
|
||||
view : 'text',
|
||||
snippets : [
|
||||
{
|
||||
name : 'Spell',
|
||||
icon : 'fa-magic',
|
||||
icon : 'fas fa-magic',
|
||||
gen : MagicGen.spell,
|
||||
},
|
||||
{
|
||||
name : 'Spell List',
|
||||
icon : 'fa-list',
|
||||
icon : 'fas fa-scroll',
|
||||
gen : MagicGen.spellList,
|
||||
},
|
||||
{
|
||||
name : 'Class Feature',
|
||||
icon : 'fa-trophy',
|
||||
icon : 'fas fa-mask',
|
||||
gen : ClassFeatureGen,
|
||||
},
|
||||
{
|
||||
name : 'Note',
|
||||
icon : 'fa-sticky-note',
|
||||
icon : 'fas fa-sticky-note',
|
||||
gen : function(){
|
||||
return [
|
||||
'> ##### Time to Drop Knowledge',
|
||||
'> Use notes to point out some interesting information. ',
|
||||
'> ',
|
||||
'> **Tables and lists** both work within a note.'
|
||||
].join('\n');
|
||||
return dedent`
|
||||
{{note
|
||||
##### Time to Drop Knowledge
|
||||
Use notes to point out some interesting information.
|
||||
|
||||
**Tables and lists** both work within a note.
|
||||
}}
|
||||
\n`;
|
||||
},
|
||||
},
|
||||
{
|
||||
name : 'Descriptive Text Box',
|
||||
icon : 'fa-sticky-note-o',
|
||||
icon : 'fas fa-comment-alt',
|
||||
gen : function(){
|
||||
return [
|
||||
'<div class=\'descriptive\'>',
|
||||
'##### Time to Drop Knowledge',
|
||||
'Use notes to point out some interesting information. ',
|
||||
'',
|
||||
'**Tables and lists** both work within a note.',
|
||||
'</div>'
|
||||
].join('\n');
|
||||
return dedent`
|
||||
{{descriptive
|
||||
##### Time to Drop Knowledge
|
||||
Use descriptive boxes to highlight text that should be read aloud.
|
||||
|
||||
**Tables and lists** both work within a descriptive box.
|
||||
}}
|
||||
\n`;
|
||||
},
|
||||
},
|
||||
{
|
||||
name : 'Monster Stat Block (unframed)',
|
||||
icon : 'fas fa-paw',
|
||||
gen : MonsterBlockGen.monster('monster', 2),
|
||||
},
|
||||
{
|
||||
name : 'Monster Stat Block',
|
||||
icon : 'fa-bug',
|
||||
gen : MonsterBlockGen.half,
|
||||
icon : 'fas fa-spider',
|
||||
gen : MonsterBlockGen.monster('monster,frame', 2),
|
||||
},
|
||||
{
|
||||
name : 'Wide Monster Stat Block',
|
||||
icon : 'fa-paw',
|
||||
gen : MonsterBlockGen.full,
|
||||
icon : 'fas fa-dragon',
|
||||
gen : MonsterBlockGen.monster('monster,frame,wide', 4),
|
||||
},
|
||||
{
|
||||
name : 'Cover Page',
|
||||
icon : 'fa-file-word-o',
|
||||
icon : 'fas fa-file-word',
|
||||
gen : CoverPageGen,
|
||||
},
|
||||
{
|
||||
name : 'Magic Item',
|
||||
icon : 'fas fa-hat-wizard',
|
||||
gen : MagicGen.item,
|
||||
},
|
||||
{
|
||||
name : 'Artist Credit',
|
||||
icon : 'fas fa-signature',
|
||||
gen : function(){
|
||||
return dedent`
|
||||
{{artist,top:90px,right:30px
|
||||
##### Starry Night
|
||||
[Van Gogh](https://www.vangoghmuseum.nl/en)
|
||||
}}
|
||||
\n`;
|
||||
},
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
@@ -154,79 +251,98 @@ module.exports = [
|
||||
|
||||
{
|
||||
groupName : 'Tables',
|
||||
icon : 'fa-table',
|
||||
icon : 'fas fa-table',
|
||||
view : 'text',
|
||||
snippets : [
|
||||
{
|
||||
name : 'Class Table',
|
||||
icon : 'fa-table',
|
||||
gen : ClassTableGen.full,
|
||||
icon : 'fas fa-table',
|
||||
gen : ClassTableGen.full('classTable,frame,decoration,wide'),
|
||||
},
|
||||
{
|
||||
name : 'Half Class Table',
|
||||
icon : 'fa-list-alt',
|
||||
gen : ClassTableGen.half,
|
||||
name : 'Class Table (unframed)',
|
||||
icon : 'fas fa-border-none',
|
||||
gen : ClassTableGen.full('classTable,wide'),
|
||||
},
|
||||
{
|
||||
name : '1/2 Class Table',
|
||||
icon : 'fas fa-list-alt',
|
||||
gen : ClassTableGen.half('classTable,decoration,frame'),
|
||||
},
|
||||
{
|
||||
name : '1/2 Class Table (unframed)',
|
||||
icon : 'fas fa-border-none',
|
||||
gen : ClassTableGen.half('classTable'),
|
||||
},
|
||||
{
|
||||
name : '1/3 Class Table',
|
||||
icon : 'fas fa-border-all',
|
||||
gen : ClassTableGen.third('classTable,frame'),
|
||||
},
|
||||
{
|
||||
name : '1/3 Class Table (unframed)',
|
||||
icon : 'fas fa-border-none',
|
||||
gen : ClassTableGen.third('classTable'),
|
||||
},
|
||||
{
|
||||
name : 'Table',
|
||||
icon : 'fa-th-list',
|
||||
icon : 'fas fa-th-list',
|
||||
gen : function(){
|
||||
return [
|
||||
'##### Cookie Tastiness',
|
||||
'| Tastiness | Cookie Type |',
|
||||
'|:----:|:-------------|',
|
||||
'| -5 | Raisin |',
|
||||
'| 8th | Chocolate Chip |',
|
||||
'| 11th | 2 or lower |',
|
||||
'| 14th | 3 or lower |',
|
||||
'| 17th | 4 or lower |\n\n',
|
||||
].join('\n');
|
||||
},
|
||||
return dedent`
|
||||
##### Character Advancement
|
||||
| Experience Points | Level | Proficiency Bonus |
|
||||
|:------------------|:-----:|:-----------------:|
|
||||
| 0 | 1 | +2 |
|
||||
| 300 | 2 | +2 |
|
||||
| 900 | 3 | +2 |
|
||||
| 2,700 | 4 | +2 |
|
||||
| 6,500 | 5 | +3 |
|
||||
| 14,000 | 6 | +3 |
|
||||
\n`;
|
||||
}
|
||||
},
|
||||
{
|
||||
name : 'Wide Table',
|
||||
icon : 'fa-list',
|
||||
icon : 'fas fa-list',
|
||||
gen : function(){
|
||||
return [
|
||||
'<div class=\'wide\'>',
|
||||
'##### Cookie Tastiness',
|
||||
'| Tastiness | Cookie Type |',
|
||||
'|:----:|:-------------|',
|
||||
'| -5 | Raisin |',
|
||||
'| 8th | Chocolate Chip |',
|
||||
'| 11th | 2 or lower |',
|
||||
'| 14th | 3 or lower |',
|
||||
'| 17th | 4 or lower |',
|
||||
'</div>\n\n'
|
||||
].join('\n');
|
||||
},
|
||||
return dedent`
|
||||
{{wide
|
||||
##### Weapons
|
||||
| Name | Cost | Damage | Weight | Properties |
|
||||
|:------------------------|:-----:|:----------------|--------:|:-----------|
|
||||
| *Simple Melee Weapons* | | | | |
|
||||
|   Club | 1 sp | 1d4 bludgeoning | 2 lb. | Light |
|
||||
|   Dagger | 2 gp | 1d4 piercing | 1 lb. | Finesse |
|
||||
|   Spear | 1 gp | 1d6 piercing | 3 lb. | Thrown |
|
||||
| *Simple Ranged Weapons* | | | | |
|
||||
|   Dart | 5 cp | 1d4 piercig | 1/4 lb. | Finesse |
|
||||
|   Shortbow | 25 gp | 1d6 piercing | 2 lb. | Ammunition |
|
||||
|   Sling | 1 sp | 1d4 bludgeoning | — | Ammunition |
|
||||
}}
|
||||
\n`;
|
||||
}
|
||||
},
|
||||
{
|
||||
name : 'Split Table',
|
||||
icon : 'fa-th-large',
|
||||
icon : 'fas fa-th-large',
|
||||
gen : function(){
|
||||
return [
|
||||
'<div style=\'column-count:2\'>',
|
||||
'| d10 | Damage Type |',
|
||||
'|:---:|:------------|',
|
||||
'| 1 | Acid |',
|
||||
'| 2 | Cold |',
|
||||
'| 3 | Fire |',
|
||||
'| 4 | Force |',
|
||||
'| 5 | Lightning |',
|
||||
'',
|
||||
'```',
|
||||
'```',
|
||||
'',
|
||||
'| d10 | Damage Type |',
|
||||
'|:---:|:------------|',
|
||||
'| 6 | Necrotic |',
|
||||
'| 7 | Poison |',
|
||||
'| 8 | Psychic |',
|
||||
'| 9 | Radiant |',
|
||||
'| 10 | Thunder |',
|
||||
'</div>\n\n',
|
||||
].join('\n');
|
||||
},
|
||||
return dedent`
|
||||
##### Typical Difficulty Classes
|
||||
{{column-count:2
|
||||
| Task Difficulty | DC |
|
||||
|:----------------|:--:|
|
||||
| Very easy | 5 |
|
||||
| Easy | 10 |
|
||||
| Medium | 15 |
|
||||
|
||||
| Task Difficulty | DC |
|
||||
|:------------------|:--:|
|
||||
| Hard | 20 |
|
||||
| Very hard | 25 |
|
||||
| Nearly impossible | 30 |
|
||||
}}
|
||||
\n`;
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -234,33 +350,54 @@ module.exports = [
|
||||
|
||||
|
||||
|
||||
/**************** PRINT *************/
|
||||
/**************** PAGE *************/
|
||||
|
||||
{
|
||||
groupName : 'Print',
|
||||
icon : 'fa-print',
|
||||
icon : 'fas fa-print',
|
||||
view : 'style',
|
||||
snippets : [
|
||||
{
|
||||
name : 'A4 PageSize',
|
||||
icon : 'fa-file-o',
|
||||
gen : ['<style>',
|
||||
' .phb{',
|
||||
' width : 210mm;',
|
||||
' height : 296.8mm;',
|
||||
' }',
|
||||
'</style>'
|
||||
name : 'A4 Page Size',
|
||||
icon : 'far fa-file',
|
||||
gen : ['/* A4 Page Size */',
|
||||
'.page{',
|
||||
' width : 210mm;',
|
||||
' height : 296.8mm;',
|
||||
'}',
|
||||
''
|
||||
].join('\n')
|
||||
},
|
||||
{
|
||||
name : 'Square Page Size',
|
||||
icon : 'far fa-file',
|
||||
gen : ['/* Square Page Size */',
|
||||
'.page {',
|
||||
' width : 125mm;',
|
||||
' height : 125mm;',
|
||||
' padding : 12.5mm;',
|
||||
' columns : unset;',
|
||||
'}',
|
||||
''
|
||||
].join('\n')
|
||||
},
|
||||
{
|
||||
name : 'Ink Friendly',
|
||||
icon : 'fa-tint',
|
||||
gen : ['<style>',
|
||||
' .phb{ background : white;}',
|
||||
' .phb img{ display : none;}',
|
||||
' .phb hr+blockquote{background : white;}',
|
||||
'</style>',
|
||||
''
|
||||
].join('\n')
|
||||
icon : 'fas fa-tint',
|
||||
gen : dedent`
|
||||
/* Ink Friendly */
|
||||
.pages *:is(.page,.monster,.note,.descriptive) {
|
||||
background : white !important;
|
||||
box-shadow : 0px 0px 3px !important;
|
||||
}
|
||||
|
||||
.page .note:before {
|
||||
box-shadow : 0px 0px 3px;
|
||||
}
|
||||
|
||||
.page img {
|
||||
visibility : hidden;
|
||||
}`
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const _ = require('lodash');
|
||||
const dedent = require('dedent-tabs').default;
|
||||
|
||||
const getTOC = (pages)=>{
|
||||
const add1 = (title, page)=>{
|
||||
@@ -9,7 +10,7 @@ const getTOC = (pages)=>{
|
||||
});
|
||||
};
|
||||
const add2 = (title, page)=>{
|
||||
if(!_.last(res)) add1('', page);
|
||||
if(!_.last(res)) add1(null, page);
|
||||
_.last(res).children.push({
|
||||
title : title,
|
||||
page : page + 1,
|
||||
@@ -17,8 +18,8 @@ const getTOC = (pages)=>{
|
||||
});
|
||||
};
|
||||
const add3 = (title, page)=>{
|
||||
if(!_.last(res)) add1('', page);
|
||||
if(!_.last(_.last(res).children)) add2('', page);
|
||||
if(!_.last(res)) add1(null, page);
|
||||
if(!_.last(_.last(res).children)) add2(null, page);
|
||||
_.last(_.last(res).children).children.push({
|
||||
title : title,
|
||||
page : page + 1,
|
||||
@@ -48,16 +49,24 @@ const getTOC = (pages)=>{
|
||||
};
|
||||
|
||||
module.exports = function(brew){
|
||||
const pages = brew.split('\\page');
|
||||
const pages = brew.text.split('\\page');
|
||||
const TOC = getTOC(pages);
|
||||
const markdown = _.reduce(TOC, (r, g1, idx1)=>{
|
||||
r.push(`- **[${idx1 + 1} ${g1.title}](#p${g1.page})**`);
|
||||
if(g1.title !== null) {
|
||||
r.push(`\t\t- ### [{{ ${g1.title}}}{{ ${g1.page}}}](#p${g1.page})`);
|
||||
}
|
||||
if(g1.children.length){
|
||||
_.each(g1.children, (g2, idx2)=>{
|
||||
r.push(` - [${idx1 + 1}.${idx2 + 1} ${g2.title}](#p${g2.page})`);
|
||||
if(g2.title !== null) {
|
||||
r.push(`\t\t - #### [{{ ${g2.title}}}{{ ${g2.page}}}](#p${g2.page})`);
|
||||
}
|
||||
if(g2.children.length){
|
||||
_.each(g2.children, (g3, idx3)=>{
|
||||
r.push(` - [${idx1 + 1}.${idx2 + 1}.${idx3 + 1} ${g3.title}](#p${g3.page})`);
|
||||
if(g2.title !== null) {
|
||||
r.push(`\t\t - [{{ ${g3.title}}}{{ ${g3.page}}}](#p${g3.page})`);
|
||||
} else { // Don't over-indent if no level-2 parent entry
|
||||
r.push(`\t\t - [{{ ${g3.title}}}{{ ${g3.page}}}](#p${g3.page})`);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -65,8 +74,11 @@ module.exports = function(brew){
|
||||
return r;
|
||||
}, []).join('\n');
|
||||
|
||||
return `<div class='toc'>
|
||||
##### Table Of Contents
|
||||
return dedent`
|
||||
{{toc,wide
|
||||
# Table Of Contents
|
||||
|
||||
${markdown}
|
||||
</div>\n`;
|
||||
};
|
||||
}}
|
||||
\n`;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
const _ = require('lodash');
|
||||
|
||||
module.exports = ()=>{
|
||||
return `{{watercolor${_.random(1, 12)},top:20px,left:30px,width:300px,background-color:#BBAD82,opacity:80%}}\n\n`;
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
const _ = require('lodash');
|
||||
|
||||
module.exports = function(classname){
|
||||
|
||||
classname = _.sample(['archivist', 'fancyman', 'linguist', 'fletcher',
|
||||
'notary', 'berserker-typist', 'fishmongerer', 'manicurist', 'haberdasher', 'concierge']);
|
||||
|
||||
classname = classname.toLowerCase();
|
||||
|
||||
const hitDie = _.sample([4, 6, 8, 10, 12]);
|
||||
|
||||
const abilityList = ['Strength', 'Dexerity', 'Constitution', 'Wisdom', 'Charisma', 'Intelligence'];
|
||||
const skillList = ['Acrobatics ', 'Animal Handling', 'Arcana', 'Athletics', 'Deception', 'History', 'Insight', 'Intimidation', 'Investigation', 'Medicine', 'Nature', 'Perception', 'Performance', 'Persuasion', 'Religion', 'Sleight of Hand', 'Stealth', 'Survival'];
|
||||
|
||||
|
||||
return [
|
||||
'## Class Features',
|
||||
`As a ${classname}, you gain the following class features`,
|
||||
'#### Hit Points',
|
||||
'___',
|
||||
`- **Hit Dice:** 1d${hitDie} per ${classname} level`,
|
||||
`- **Hit Points at 1st Level:** ${hitDie} + your Constitution modifier`,
|
||||
`- **Hit Points at Higher Levels:** 1d${hitDie} (or ${hitDie/2 + 1}) + your Constitution modifier per ${classname} level after 1st`,
|
||||
'',
|
||||
'#### Proficiencies',
|
||||
'___',
|
||||
`- **Armor:** ${_.sampleSize(['Light armor', 'Medium armor', 'Heavy armor', 'Shields'], _.random(0, 3)).join(', ') || 'None'}`,
|
||||
`- **Weapons:** ${_.sampleSize(['Squeegee', 'Rubber Chicken', 'Simple weapons', 'Martial weapons'], _.random(0, 2)).join(', ') || 'None'}`,
|
||||
`- **Tools:** ${_.sampleSize(['Artian\'s tools', 'one musical instrument', 'Thieve\'s tools'], _.random(0, 2)).join(', ') || 'None'}`,
|
||||
'',
|
||||
'___',
|
||||
`- **Saving Throws:** ${_.sampleSize(abilityList, 2).join(', ')}`,
|
||||
`- **Skills:** Choose two from ${_.sampleSize(skillList, _.random(4, 6)).join(', ')}`,
|
||||
'',
|
||||
'#### Equipment',
|
||||
'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)* five javelins or *(b)* any simple melee weapon',
|
||||
`- ${_.sample(['10 lint fluffs', '1 button', 'a cherished lost sock'])}`,
|
||||
'\n\n\n'
|
||||
].join('\n');
|
||||
};
|
||||
@@ -0,0 +1,114 @@
|
||||
const _ = require('lodash');
|
||||
|
||||
const features = [
|
||||
'Astrological Botany',
|
||||
'Astrological Chemistry',
|
||||
'Biochemical Sorcery',
|
||||
'Civil Alchemy',
|
||||
'Consecrated Biochemistry',
|
||||
'Demonic Anthropology',
|
||||
'Divinatory Mineralogy',
|
||||
'Genetic Banishing',
|
||||
'Hermetic Geography',
|
||||
'Immunological Incantations',
|
||||
'Nuclear Illusionism',
|
||||
'Ritual Astronomy',
|
||||
'Seismological Divination',
|
||||
'Spiritual Biochemistry',
|
||||
'Statistical Occultism',
|
||||
'Police Necromancer',
|
||||
'Sixgun Poisoner',
|
||||
'Pharmaceutical Gunslinger',
|
||||
'Infernal Banker',
|
||||
'Spell Analyst',
|
||||
'Gunslinger Corruptor',
|
||||
'Torque Interfacer',
|
||||
'Exo Interfacer',
|
||||
'Gunpowder Torturer',
|
||||
'Orbital Gravedigger',
|
||||
'Phased Linguist',
|
||||
'Mathematical Pharmacist',
|
||||
'Plasma Outlaw',
|
||||
'Malefic Chemist',
|
||||
'Police Cultist'
|
||||
];
|
||||
|
||||
const classnames = ['Archivist', 'Fancyman', 'Linguist', 'Fletcher',
|
||||
'Notary', 'Berserker-Typist', 'Fishmongerer', 'Manicurist', 'Haberdasher', 'Concierge'];
|
||||
|
||||
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 getFeature = (level)=>{
|
||||
let res = [];
|
||||
if(_.includes([4, 6, 8, 12, 14, 16, 19], level+1)){
|
||||
res = ['Ability Score Improvement'];
|
||||
}
|
||||
res = _.union(res, _.sampleSize(features, _.sample([0, 1, 1, 1, 1, 1])));
|
||||
if(!res.length) return '─';
|
||||
return res.join(', ');
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
full : function(){
|
||||
const classname = _.sample(classnames);
|
||||
|
||||
const maxes = [4, 3, 3, 3, 3, 2, 2, 1, 1];
|
||||
const drawSlots = function(Slots){
|
||||
let slots = Number(Slots);
|
||||
return _.times(9, function(i){
|
||||
const max = maxes[i];
|
||||
if(slots < 1) return '—';
|
||||
const res = _.min([max, slots]);
|
||||
slots -= res;
|
||||
return res;
|
||||
}).join(' | ');
|
||||
};
|
||||
|
||||
|
||||
let cantrips = 3;
|
||||
let spells = 1;
|
||||
let slots = 2;
|
||||
return `<div class='classTable wide'>\n##### The ${classname}\n` +
|
||||
`| Level | Proficiency Bonus | Features | Cantrips Known | Spells Known | 1st | 2nd | 3rd | 4th | 5th | 6th | 7th | 8th | 9th |\n`+
|
||||
`|:---:|:---:|:---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|\n${
|
||||
_.map(levels, function(levelName, level){
|
||||
const res = [
|
||||
levelName,
|
||||
`+${profBonus[level]}`,
|
||||
getFeature(level),
|
||||
cantrips,
|
||||
spells,
|
||||
drawSlots(slots)
|
||||
].join(' | ');
|
||||
|
||||
cantrips += _.random(0, 1);
|
||||
spells += _.random(0, 1);
|
||||
slots += _.random(0, 2);
|
||||
|
||||
return `| ${res} |`;
|
||||
}).join('\n')}\n</div>\n\n`;
|
||||
},
|
||||
|
||||
half : function(){
|
||||
const classname = _.sample(classnames);
|
||||
|
||||
let featureScore = 1;
|
||||
return `<div class='classTable'>\n##### The ${classname}\n` +
|
||||
`| Level | Proficiency Bonus | Features | ${_.sample(features)}|\n` +
|
||||
`|:---:|:---:|:---|:---:|\n${
|
||||
_.map(levels, function(levelName, level){
|
||||
const res = [
|
||||
levelName,
|
||||
`+${profBonus[level]}`,
|
||||
getFeature(level),
|
||||
`+${featureScore}`
|
||||
].join(' | ');
|
||||
|
||||
featureScore += _.random(0, 1);
|
||||
|
||||
return `| ${res} |`;
|
||||
}).join('\n')}\n</div>\n\n`;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,117 @@
|
||||
const _ = require('lodash');
|
||||
|
||||
const titles = [
|
||||
'The Burning Gallows',
|
||||
'The Ring of Nenlast',
|
||||
'Below the Blind Tavern',
|
||||
'Below the Hungering River',
|
||||
'Before Bahamut\'s Land',
|
||||
'The Cruel Grave from Within',
|
||||
'The Strength of Trade Road',
|
||||
'Through The Raven Queen\'s Worlds',
|
||||
'Within the Settlement',
|
||||
'The Crown from Within',
|
||||
'The Merchant Within the Battlefield',
|
||||
'Ioun\'s Fading Traveler',
|
||||
'The Legion Ingredient',
|
||||
'The Explorer Lure',
|
||||
'Before the Charming Badlands',
|
||||
'The Living Dead Above the Fearful Cage',
|
||||
'Vecna\'s Hidden Sage',
|
||||
'Bahamut\'s Demonspawn',
|
||||
'Across Gruumsh\'s Elemental Chaos',
|
||||
'The Blade of Orcus',
|
||||
'Beyond Revenge',
|
||||
'Brain of Insanity',
|
||||
'Breed Battle!, A New Beginning',
|
||||
'Evil Lake, A New Beginning',
|
||||
'Invasion of the Gigantic Cat, Part II',
|
||||
'Kraken War 2020',
|
||||
'The Body Whisperers',
|
||||
'The Diabolical Tales of the Ape-Women',
|
||||
'The Doctor Immortal',
|
||||
'The Doctor from Heaven',
|
||||
'The Graveyard',
|
||||
'Azure Core',
|
||||
'Core Battle',
|
||||
'Core of Heaven: The Guardian of Amazement',
|
||||
'Deadly Amazement III',
|
||||
'Dry Chaos IX',
|
||||
'Gate Thunder',
|
||||
'Guardian: Skies of the Dark Wizard',
|
||||
'Lute of Eternity',
|
||||
'Mercury\'s Planet: Brave Evolution',
|
||||
'Ruby of Atlantis: The Quake of Peace',
|
||||
'Sky of Zelda: The Thunder of Force',
|
||||
'Vyse\'s Skies',
|
||||
'White Greatness III',
|
||||
'Yellow Divinity',
|
||||
'Zidane\'s Ghost'
|
||||
];
|
||||
|
||||
const subtitles = [
|
||||
'In an ominous universe, a botanist opposes terrorism.',
|
||||
'In a demon-haunted city, in an age of lies and hate, a physicist tries to find an ancient treasure and battles a mob of aliens.',
|
||||
'In a land of corruption, two cyberneticists and a dungeon delver search for freedom.',
|
||||
'In an evil empire of horror, two rangers battle the forces of hell.',
|
||||
'In a lost city, in an age of sorcery, a librarian quests for revenge.',
|
||||
'In a universe of illusions and danger, three time travellers and an adventurer search for justice.',
|
||||
'In a forgotten universe of barbarism, in an era of terror and mysticism, a virtual reality programmer and a spy try to find vengance and battle crime.',
|
||||
'In a universe of demons, in an era of insanity and ghosts, three bodyguards and a bodyguard try to find vengance.',
|
||||
'In a kingdom of corruption and battle, seven artificial intelligences try to save the last living fertile woman.',
|
||||
'In a universe of virutal reality and agony, in an age of ghosts and ghosts, a fortune-teller and a wanderer try to avert the apocalypse.',
|
||||
'In a crime-infested kingdom, three martial artists quest for the truth and oppose evil.',
|
||||
'In a terrifying universe of lost souls, in an era of lost souls, eight dancers fight evil.',
|
||||
'In a galaxy of confusion and insanity, three martial artists and a duke battle a mob of psychics.',
|
||||
'In an amazing kingdom, a wizard and a secretary hope to prevent the destruction of mankind.',
|
||||
'In a kingdom of deception, a reporter searches for fame.',
|
||||
'In a hellish empire, a swordswoman and a duke try to find the ultimate weapon and battle a conspiracy.',
|
||||
'In an evil galaxy of illusion, in a time of technology and misery, seven psychiatrists battle crime.',
|
||||
'In a dark city of confusion, three swordswomen and a singer battle lawlessness.',
|
||||
'In an ominous empire, in an age of hate, two philosophers and a student try to find justice and battle a mob of mages intent on stealing the souls of the innocent.',
|
||||
'In a kingdom of panic, six adventurers oppose lawlessness.',
|
||||
'In a land of dreams and hopelessness, three hackers and a cyborg search for justice.',
|
||||
'On a planet of mysticism, three travelers and a fire fighter quest for the ultimate weapon and oppose evil.',
|
||||
'In a wicked universe, five seers fight lawlessness.',
|
||||
'In a kingdom of death, in an era of illusion and blood, four colonists search for fame.',
|
||||
'In an amazing kingdom, in an age of sorcery and lost souls, eight space pirates quest for freedom.',
|
||||
'In a cursed empire, five inventors oppose terrorism.',
|
||||
'On a crime-ridden planet of conspiracy, a watchman and an artificial intelligence try to find love and oppose lawlessness.',
|
||||
'In a forgotten land, a reporter and a spy try to stop the apocalypse.',
|
||||
'In a forbidden land of prophecy, a scientist and an archivist oppose a cabal of barbarians intent on stealing the souls of the innocent.',
|
||||
'On an infernal world of illusion, a grave robber and a watchman try to find revenge and combat a syndicate of mages intent on stealing the source of all magic.',
|
||||
'In a galaxy of dark magic, four fighters seek freedom.',
|
||||
'In an empire of deception, six tomb-robbers quest for the ultimate weapon and combat an army of raiders.',
|
||||
'In a kingdom of corruption and lost souls, in an age of panic, eight planetologists oppose evil.',
|
||||
'In a galaxy of misery and hopelessness, in a time of agony and pain, five planetologists search for vengance.',
|
||||
'In a universe of technology and insanity, in a time of sorcery, a computer techician quests for hope.',
|
||||
'On a planet of dark magic and barbarism, in an age of horror and blasphemy, seven librarians search for fame.',
|
||||
'In an empire of dark magic, in a time of blood and illusions, four monks try to find the ultimate weapon and combat terrorism.',
|
||||
'In a forgotten empire of dark magic, six kings try to prevent the destruction of mankind.',
|
||||
'In a galaxy of dark magic and horror, in an age of hopelessness, four marines and an outlaw combat evil.',
|
||||
'In a mysterious city of illusion, in an age of computerization, a witch-hunter tries to find the ultimate weapon and opposes an evil corporation.',
|
||||
'In a damned kingdom of technology, a virtual reality programmer and a fighter seek fame.',
|
||||
'In a hellish kingdom, in an age of blasphemy and blasphemy, an astrologer searches for fame.',
|
||||
'In a damned world of devils, an alien and a ranger quest for love and oppose a syndicate of demons.',
|
||||
'In a cursed galaxy, in a time of pain, seven librarians hope to avert the apocalypse.',
|
||||
'In a crime-infested galaxy, in an era of hopelessness and panic, three champions and a grave robber try to solve the ultimate crime.'
|
||||
];
|
||||
|
||||
|
||||
module.exports = ()=>{
|
||||
return `<style>
|
||||
.phb#p1{ text-align:center; }
|
||||
.phb#p1:after{ display:none; }
|
||||
</style>
|
||||
|
||||
<div style='margin-top:450px;'></div>
|
||||
|
||||
# ${_.sample(titles)}
|
||||
|
||||
<div style='margin-top:25px'></div>
|
||||
<div class='wide'>
|
||||
##### ${_.sample(subtitles)}
|
||||
</div>
|
||||
|
||||
\\page`;
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
const _ = require('lodash');
|
||||
|
||||
const ClassFeatureGen = require('./classfeature.gen.js');
|
||||
|
||||
const ClassTableGen = require('./classtable.gen.js');
|
||||
|
||||
module.exports = function(){
|
||||
|
||||
const classname = _.sample(['Archivist', 'Fancyman', 'Linguist', 'Fletcher',
|
||||
'Notary', 'Berserker-Typist', 'Fishmongerer', 'Manicurist', 'Haberdasher', 'Concierge']);
|
||||
|
||||
|
||||
const image = _.sample(_.map([
|
||||
'http://orig01.deviantart.net/4682/f/2007/099/f/c/bard_stick_figure_by_wrpigeek.png',
|
||||
'http://img07.deviantart.net/a3c9/i/2007/099/3/a/archer_stick_figure_by_wrpigeek.png',
|
||||
'http://pre04.deviantart.net/d596/th/pre/f/2007/099/5/2/adventurer_stick_figure_by_wrpigeek.png',
|
||||
'http://img13.deviantart.net/d501/i/2007/099/d/4/black_mage_stick_figure_by_wrpigeek.png',
|
||||
'http://img09.deviantart.net/5cf3/i/2007/099/d/d/dark_knight_stick_figure_by_wrpigeek.png',
|
||||
'http://pre01.deviantart.net/7a34/th/pre/f/2007/099/6/3/monk_stick_figure_by_wrpigeek.png',
|
||||
'http://img11.deviantart.net/5dcc/i/2007/099/d/1/mystic_knight_stick_figure_by_wrpigeek.png',
|
||||
'http://pre08.deviantart.net/ad45/th/pre/f/2007/099/a/0/thief_stick_figure_by_wrpigeek.png',
|
||||
], function(url){
|
||||
return `<img src = '${url}' style='max-width:8cm;max-height:25cm' />`;
|
||||
}));
|
||||
|
||||
|
||||
return `${[
|
||||
image,
|
||||
'',
|
||||
'```',
|
||||
'```',
|
||||
'<div style=\'margin-top:240px\'></div>\n\n',
|
||||
`## ${classname}`,
|
||||
'Cool intro stuff will go here',
|
||||
|
||||
'\\page',
|
||||
ClassTableGen(classname),
|
||||
ClassFeatureGen(classname),
|
||||
|
||||
|
||||
|
||||
].join('\n')}\n\n\n`;
|
||||
};
|
||||
@@ -0,0 +1,91 @@
|
||||
const _ = require('lodash');
|
||||
|
||||
const spellNames = [
|
||||
'Astral Rite of Acne',
|
||||
'Create Acne',
|
||||
'Cursed Ramen Erruption',
|
||||
'Dark Chant of the Dentists',
|
||||
'Erruption of Immaturity',
|
||||
'Flaming Disc of Inconvenience',
|
||||
'Heal Bad Hygene',
|
||||
'Heavenly Transfiguration of the Cream Devil',
|
||||
'Hellish Cage of Mucus',
|
||||
'Irritate Peanut Butter Fairy',
|
||||
'Luminous Erruption of Tea',
|
||||
'Mystic Spell of the Poser',
|
||||
'Sorcerous Enchantment of the Chimneysweep',
|
||||
'Steak Sauce Ray',
|
||||
'Talk to Groupie',
|
||||
'Astonishing Chant of Chocolate',
|
||||
'Astounding Pasta Puddle',
|
||||
'Ball of Annoyance',
|
||||
'Cage of Yarn',
|
||||
'Control Noodles Elemental',
|
||||
'Create Nervousness',
|
||||
'Cure Baldness',
|
||||
'Cursed Ritual of Bad Hair',
|
||||
'Dispell Piles in Dentist',
|
||||
'Eliminate Florists',
|
||||
'Illusionary Transfiguration of the Babysitter',
|
||||
'Necromantic Armor of Salad Dressing',
|
||||
'Occult Transfiguration of Foot Fetish',
|
||||
'Protection from Mucus Giant',
|
||||
'Tinsel Blast',
|
||||
'Alchemical Evocation of the Goths',
|
||||
'Call Fangirl',
|
||||
'Divine Spell of Crossdressing',
|
||||
'Dominate Ramen Giant',
|
||||
'Eliminate Vindictiveness in Gym Teacher',
|
||||
'Extra-Planar Spell of Irritation',
|
||||
'Induce Whining in Babysitter',
|
||||
'Invoke Complaining',
|
||||
'Magical Enchantment of Arrogance',
|
||||
'Occult Globe of Salad Dressing',
|
||||
'Overwhelming Enchantment of the Chocolate Fairy',
|
||||
'Sorcerous Dandruff Globe',
|
||||
'Spiritual Invocation of the Costumers',
|
||||
'Ultimate Rite of the Confetti Angel',
|
||||
'Ultimate Ritual of Mouthwash',
|
||||
];
|
||||
|
||||
module.exports = {
|
||||
|
||||
spellList : function(){
|
||||
const levels = ['Cantrips (0 Level)', '1st Level', '2nd Level', '3rd Level', '4th Level', '5th Level', '6th Level', '7th Level', '8th Level', '9th Level'];
|
||||
|
||||
const content = _.map(levels, (level)=>{
|
||||
const spells = _.map(_.sampleSize(spellNames, _.random(5, 15)), (spell)=>{
|
||||
return `- ${spell}`;
|
||||
}).join('\n');
|
||||
return `##### ${level} \n${spells} \n`;
|
||||
}).join('\n');
|
||||
|
||||
return `<div class='spellList'>\n${content}\n</div>`;
|
||||
},
|
||||
|
||||
spell : function(){
|
||||
const level = ['1st', '2nd', '3rd', '4th', '5th', '6th', '7th', '8th', '9th'];
|
||||
const spellSchools = ['abjuration', 'conjuration', 'divination', 'enchantment', 'evocation', 'illusion', 'necromancy', 'transmutation'];
|
||||
|
||||
|
||||
let components = _.sampleSize(['V', 'S', 'M'], _.random(1, 3)).join(', ');
|
||||
if(components.indexOf('M') !== -1){
|
||||
components += ` (${_.sampleSize(['a small doll', 'a crushed button worth at least 1cp', 'discarded gum wrapper'], _.random(1, 3)).join(', ')})`;
|
||||
}
|
||||
|
||||
return [
|
||||
`#### ${_.sample(spellNames)}`,
|
||||
`*${_.sample(level)}-level ${_.sample(spellSchools)}*`,
|
||||
'___',
|
||||
'- **Casting Time:** 1 action',
|
||||
`- **Range:** ${_.sample(['Self', 'Touch', '30 feet', '60 feet'])}`,
|
||||
`- **Components:** ${components}`,
|
||||
`- **Duration:** ${_.sample(['Until dispelled', '1 round', 'Instantaneous', 'Concentration, up to 10 minutes', '1 hour'])}`,
|
||||
'',
|
||||
'A flame, equivalent in brightness to a torch, springs from an object that you touch. ',
|
||||
'The effect look like a regular flame, but it creates no heat and doesn\'t use oxygen. ',
|
||||
'A *continual flame* can be covered or hidden but not smothered or quenched.',
|
||||
'\n\n\n'
|
||||
].join('\n');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,200 @@
|
||||
const _ = require('lodash');
|
||||
|
||||
const genList = function(list, max){
|
||||
return _.sampleSize(list, _.random(0, max)).join(', ') || 'None';
|
||||
};
|
||||
|
||||
const getMonsterName = function(){
|
||||
return _.sample([
|
||||
'All-devouring Baseball Imp',
|
||||
'All-devouring Gumdrop Wraith',
|
||||
'Chocolate Hydra',
|
||||
'Devouring Peacock',
|
||||
'Economy-sized Colossus of the Lemonade Stand',
|
||||
'Ghost Pigeon',
|
||||
'Gibbering Duck',
|
||||
'Sparklemuffin Peacock Spider',
|
||||
'Gum Elemental',
|
||||
'Illiterate Construct of the Candy Store',
|
||||
'Ineffable Chihuahua',
|
||||
'Irritating Death Hamster',
|
||||
'Irritating Gold Mouse',
|
||||
'Juggernaut Snail',
|
||||
'Juggernaut of the Sock Drawer',
|
||||
'Koala of the Cosmos',
|
||||
'Mad Koala of the West',
|
||||
'Milk Djinni of the Lemonade Stand',
|
||||
'Mind Ferret',
|
||||
'Mystic Salt Spider',
|
||||
'Necrotic Halitosis Angel',
|
||||
'Pinstriped Famine Sheep',
|
||||
'Ritalin Leech',
|
||||
'Shocker Kangaroo',
|
||||
'Stellar Tennis Juggernaut',
|
||||
'Wailing Quail of the Sun',
|
||||
'Angel Pigeon',
|
||||
'Anime Sphinx',
|
||||
'Bored Avalanche Sheep of the Wasteland',
|
||||
'Devouring Nougat Sphinx of the Sock Drawer',
|
||||
'Djinni of the Footlocker',
|
||||
'Ectoplasmic Jazz Devil',
|
||||
'Flatuent Angel',
|
||||
'Gelatinous Duck of the Dream-Lands',
|
||||
'Gelatinous Mouse',
|
||||
'Golem of the Footlocker',
|
||||
'Lich Wombat',
|
||||
'Mechanical Sloth of the Past',
|
||||
'Milkshake Succubus',
|
||||
'Puffy Bone Peacock of the East',
|
||||
'Rainbow Manatee',
|
||||
'Rune Parrot',
|
||||
'Sand Cow',
|
||||
'Sinister Vanilla Dragon',
|
||||
'Snail of the North',
|
||||
'Spider of the Sewer',
|
||||
'Stellar Sawdust Leech',
|
||||
'Storm Anteater of Hell',
|
||||
'Stupid Spirit of the Brewery',
|
||||
'Time Kangaroo',
|
||||
'Tomb Poodle',
|
||||
]);
|
||||
};
|
||||
|
||||
const getType = function(){
|
||||
return `${_.sample(['Tiny', 'Small', 'Medium', 'Large', 'Gargantuan', 'Stupidly vast'])} ${_.sample(['beast', 'fiend', 'annoyance', 'guy', 'cutie'])}`;
|
||||
};
|
||||
|
||||
const getAlignment = function(){
|
||||
return _.sample([
|
||||
'annoying evil',
|
||||
'chaotic gossipy',
|
||||
'chaotic sloppy',
|
||||
'depressed neutral',
|
||||
'lawful bogus',
|
||||
'lawful coy',
|
||||
'manic-depressive evil',
|
||||
'narrow-minded neutral',
|
||||
'neutral annoying',
|
||||
'neutral ignorant',
|
||||
'oedpipal neutral',
|
||||
'silly neutral',
|
||||
'unoriginal neutral',
|
||||
'weird neutral',
|
||||
'wordy evil',
|
||||
'unaligned'
|
||||
]);
|
||||
};
|
||||
|
||||
const getStats = function(){
|
||||
return `>|${_.times(6, function(){
|
||||
const num = _.random(1, 20);
|
||||
const mod = Math.ceil(num/2 - 5);
|
||||
return `${num} (${mod >= 0 ? `+${mod}` : mod})`;
|
||||
}).join('|')}|`;
|
||||
};
|
||||
|
||||
const genAbilities = function(){
|
||||
return _.sample([
|
||||
'> ***Pack Tactics.*** These guys work together. Like super well, you don\'t even know.',
|
||||
'> ***Fowl Appearance.*** While the creature remains motionless, it is indistinguishable from a normal chicken.',
|
||||
'> ***Onion Stench.*** Any creatures within 5 feet of this thing develops an irrational craving for onion rings.',
|
||||
'> ***Enormous Nose.*** This creature gains advantage on any check involving putting things in its nose.',
|
||||
'> ***Sassiness.*** When questioned, this creature will talk back instead of answering.',
|
||||
'> ***Big Jerk.*** Thinks he is just *waaaay* better than you.',
|
||||
]);
|
||||
};
|
||||
|
||||
const genAction = function(){
|
||||
const name = _.sample([
|
||||
'Abdominal Drop',
|
||||
'Airplane Hammer',
|
||||
'Atomic Death Throw',
|
||||
'Bulldog Rake',
|
||||
'Corkscrew Strike',
|
||||
'Crossed Splash',
|
||||
'Crossface Suplex',
|
||||
'DDT Powerbomb',
|
||||
'Dual Cobra Wristlock',
|
||||
'Dual Throw',
|
||||
'Elbow Hold',
|
||||
'Gory Body Sweep',
|
||||
'Heel Jawbreaker',
|
||||
'Jumping Driver',
|
||||
'Open Chin Choke',
|
||||
'Scorpion Flurry',
|
||||
'Somersault Stump Fists',
|
||||
'Suffering Wringer',
|
||||
'Super Hip Submission',
|
||||
'Super Spin',
|
||||
'Team Elbow',
|
||||
'Team Foot',
|
||||
'Tilt-a-whirl Chin Sleeper',
|
||||
'Tilt-a-whirl Eye Takedown',
|
||||
'Turnbuckle Roll'
|
||||
]);
|
||||
|
||||
return `> ***${name}.*** *Melee Weapon Attack:* +4 to hit, reach 5ft., one target. *Hit* 5 (1d6 + 2) `;
|
||||
};
|
||||
|
||||
|
||||
module.exports = {
|
||||
|
||||
full : function(){
|
||||
return `${[
|
||||
'___',
|
||||
'___',
|
||||
`> ## ${getMonsterName()}`,
|
||||
`>*${getType()}, ${getAlignment()}*`,
|
||||
'> ___',
|
||||
`> - **Armor Class** ${_.random(10, 20)}`,
|
||||
`> - **Hit Points** ${_.random(1, 150)}(1d4 + 5)`,
|
||||
`> - **Speed** ${_.random(0, 50)}ft.`,
|
||||
'>___',
|
||||
'>|STR|DEX|CON|INT|WIS|CHA|',
|
||||
'>|:---:|:---:|:---:|:---:|:---:|:---:|',
|
||||
getStats(),
|
||||
'>___',
|
||||
`> - **Condition Immunities** ${genList(['groggy', 'swagged', 'weak-kneed', 'buzzed', 'groovy', 'melancholy', 'drunk'], 3)}`,
|
||||
`> - **Senses** passive Perception ${_.random(3, 20)}`,
|
||||
`> - **Languages** ${genList(['Common', 'Pottymouth', 'Gibberish', 'Latin', 'Jive'], 2)}`,
|
||||
`> - **Challenge** ${_.random(0, 15)} (${_.random(10, 10000)} XP)`,
|
||||
'> ___',
|
||||
_.times(_.random(3, 6), function(){
|
||||
return genAbilities();
|
||||
}).join('\n>\n'),
|
||||
'> ### Actions',
|
||||
_.times(_.random(4, 6), function(){
|
||||
return genAction();
|
||||
}).join('\n>\n'),
|
||||
].join('\n')}\n\n\n`;
|
||||
},
|
||||
|
||||
half : function(){
|
||||
return `${[
|
||||
'___',
|
||||
`> ## ${getMonsterName()}`,
|
||||
`>*${getType()}, ${getAlignment()}*`,
|
||||
'> ___',
|
||||
`> - **Armor Class** ${_.random(10, 20)}`,
|
||||
`> - **Hit Points** ${_.random(1, 150)}(1d4 + 5)`,
|
||||
`> - **Speed** ${_.random(0, 50)}ft.`,
|
||||
'>___',
|
||||
'>|STR|DEX|CON|INT|WIS|CHA|',
|
||||
'>|:---:|:---:|:---:|:---:|:---:|:---:|',
|
||||
getStats(),
|
||||
'>___',
|
||||
`> - **Condition Immunities** ${genList(['groggy', 'swagged', 'weak-kneed', 'buzzed', 'groovy', 'melancholy', 'drunk'], 3)}`,
|
||||
`> - **Senses** passive Perception ${_.random(3, 20)}`,
|
||||
`> - **Languages** ${genList(['Common', 'Pottymouth', 'Gibberish', 'Latin', 'Jive'], 2)}`,
|
||||
`> - **Challenge** ${_.random(0, 15)} (${_.random(10, 10000)} XP)`,
|
||||
'> ___',
|
||||
_.times(_.random(2, 3), function(){
|
||||
return genAbilities();
|
||||
}).join('\n>\n'),
|
||||
'> ### Actions',
|
||||
_.times(_.random(1, 2), function(){
|
||||
return genAction();
|
||||
}).join('\n>\n'),
|
||||
].join('\n')}\n\n\n`;
|
||||
}
|
||||
};
|
||||
321
client/homebrew/editor/snippetbar/snippetsLegacy/snippets.js
Normal file
321
client/homebrew/editor/snippetbar/snippetsLegacy/snippets.js
Normal file
@@ -0,0 +1,321 @@
|
||||
/* eslint-disable max-lines */
|
||||
|
||||
const MagicGen = require('./magic.gen.js');
|
||||
const ClassTableGen = require('./classtable.gen.js');
|
||||
const MonsterBlockGen = require('./monsterblock.gen.js');
|
||||
const ClassFeatureGen = require('./classfeature.gen.js');
|
||||
const CoverPageGen = require('./coverpage.gen.js');
|
||||
const TableOfContentsGen = require('./tableOfContents.gen.js');
|
||||
const dedent = require('dedent-tabs').default;
|
||||
|
||||
module.exports = [
|
||||
|
||||
{
|
||||
groupName : 'Editor',
|
||||
icon : 'fas fa-pencil-alt',
|
||||
view : 'text',
|
||||
snippets : [
|
||||
{
|
||||
name : 'Column Break',
|
||||
icon : 'fas fa-columns',
|
||||
gen : '```\n```\n\n'
|
||||
},
|
||||
{
|
||||
name : 'New Page',
|
||||
icon : 'fas fa-file-alt',
|
||||
gen : '\\page\n\n'
|
||||
},
|
||||
{
|
||||
name : 'Vertical Spacing',
|
||||
icon : 'fas fa-arrows-alt-v',
|
||||
gen : '<div style=\'margin-top:140px\'></div>\n\n'
|
||||
},
|
||||
{
|
||||
name : 'Wide Block',
|
||||
icon : 'fas fa-arrows-alt-h',
|
||||
gen : '<div class=\'wide\'>\nEverything in here will be extra wide. Tables, text, everything! Beware though, CSS columns can behave a bit weird sometimes.\n</div>\n'
|
||||
},
|
||||
{
|
||||
name : 'Image',
|
||||
icon : 'fas fa-image',
|
||||
gen : [
|
||||
'<img ',
|
||||
' src=\'https://s-media-cache-ak0.pinimg.com/736x/4a/81/79/4a8179462cfdf39054a418efd4cb743e.jpg\' ',
|
||||
' style=\'width:325px\' />',
|
||||
'Credit: Kyounghwan Kim'
|
||||
].join('\n')
|
||||
},
|
||||
{
|
||||
name : 'Background Image',
|
||||
icon : 'fas fa-tree',
|
||||
gen : [
|
||||
'<img ',
|
||||
' src=\'http://i.imgur.com/hMna6G0.png\' ',
|
||||
' style=\'position:absolute; top:50px; right:30px; width:280px\' />'
|
||||
].join('\n')
|
||||
},
|
||||
|
||||
{
|
||||
name : 'Page Number',
|
||||
icon : 'fas fa-bookmark',
|
||||
gen : '<div class=\'pageNumber\'>1</div>\n<div class=\'footnote\'>PART 1 | FANCINESS</div>\n\n'
|
||||
},
|
||||
|
||||
{
|
||||
name : 'Auto-incrementing Page Number',
|
||||
icon : 'fas fa-sort-numeric-down',
|
||||
gen : '<div class=\'pageNumber auto\'></div>\n'
|
||||
},
|
||||
|
||||
{
|
||||
name : 'Link to page',
|
||||
icon : 'fas fa-link',
|
||||
gen : '[Click here](#p3) to go to page 3\n'
|
||||
},
|
||||
|
||||
{
|
||||
name : 'Table of Contents',
|
||||
icon : 'fas fa-book',
|
||||
gen : TableOfContentsGen
|
||||
},
|
||||
{
|
||||
name : 'Remove Drop Cap',
|
||||
icon : 'fas fa-remove-format',
|
||||
gen : '<style>\n' +
|
||||
' .phb h1+p:first-letter {\n' +
|
||||
' all: unset;\n' +
|
||||
' }\n' +
|
||||
'</style>'
|
||||
},
|
||||
{
|
||||
name : 'Tweak Drop Cap',
|
||||
icon : 'fas fa-sliders-h',
|
||||
gen : '<style>\n' +
|
||||
' /* Drop Cap settings */\n' +
|
||||
' .phb h1 + p::first-letter {\n' +
|
||||
' float: left;\n' +
|
||||
' font-family: Solberry;\n' +
|
||||
' font-size: 10em;\n' +
|
||||
' color: #222;\n' +
|
||||
' line-height: .8em;\n' +
|
||||
' }\n' +
|
||||
'</style>'
|
||||
},
|
||||
{
|
||||
name : 'Add Comment',
|
||||
icon : 'fas fa-code',
|
||||
gen : `\n<!-- This is a comment that will not be rendered into your brew. Hotkey (Ctrl/Cmd + /). -->\n\n`
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
|
||||
/************************* PHB ********************/
|
||||
|
||||
{
|
||||
groupName : 'PHB',
|
||||
icon : 'fas fa-book',
|
||||
view : 'text',
|
||||
snippets : [
|
||||
{
|
||||
name : 'Spell',
|
||||
icon : 'fas fa-magic',
|
||||
gen : MagicGen.spell,
|
||||
},
|
||||
{
|
||||
name : 'Spell List',
|
||||
icon : 'fas fa-list',
|
||||
gen : MagicGen.spellList,
|
||||
},
|
||||
{
|
||||
name : 'Class Feature',
|
||||
icon : 'fas fa-trophy',
|
||||
gen : ClassFeatureGen,
|
||||
},
|
||||
{
|
||||
name : 'Note',
|
||||
icon : 'fas fa-sticky-note',
|
||||
gen : function(){
|
||||
return [
|
||||
'> ##### Time to Drop Knowledge',
|
||||
'> Use notes to point out some interesting information. ',
|
||||
'> ',
|
||||
'> **Tables and lists** both work within a note.'
|
||||
].join('\n');
|
||||
},
|
||||
},
|
||||
{
|
||||
name : 'Descriptive Text Box',
|
||||
icon : 'far fa-sticky-note',
|
||||
gen : function(){
|
||||
return [
|
||||
'<div class=\'descriptive\'>',
|
||||
'##### Time to Drop Knowledge',
|
||||
'Use notes to point out some interesting information. ',
|
||||
'',
|
||||
'**Tables and lists** both work within a note.',
|
||||
'</div>'
|
||||
].join('\n');
|
||||
},
|
||||
},
|
||||
{
|
||||
name : 'Monster Stat Block',
|
||||
icon : 'fas fa-bug',
|
||||
gen : MonsterBlockGen.half,
|
||||
},
|
||||
{
|
||||
name : 'Wide Monster Stat Block',
|
||||
icon : 'fas fa-paw',
|
||||
gen : MonsterBlockGen.full,
|
||||
},
|
||||
{
|
||||
name : 'Cover Page',
|
||||
icon : 'far fa-file-word',
|
||||
gen : CoverPageGen,
|
||||
},
|
||||
{
|
||||
name : 'Artist Credit',
|
||||
icon : 'fas fa-signature',
|
||||
gen : '<div class=\'artist\' style=\'top:90px;right:30px;\'>\n' +
|
||||
'##### Starry Night\n' +
|
||||
'[Van Gogh](https://www.vangoghmuseum.nl/en)\n' +
|
||||
'</div>\n'
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
|
||||
|
||||
/********************* TABLES *********************/
|
||||
|
||||
{
|
||||
groupName : 'Tables',
|
||||
icon : 'fas fa-table',
|
||||
view : 'text',
|
||||
snippets : [
|
||||
{
|
||||
name : 'Class Table',
|
||||
icon : 'fas fa-table',
|
||||
gen : ClassTableGen.full,
|
||||
},
|
||||
{
|
||||
name : 'Half Class Table',
|
||||
icon : 'fas fa-list-alt',
|
||||
gen : ClassTableGen.half,
|
||||
},
|
||||
{
|
||||
name : 'Table',
|
||||
icon : 'fas fa-th-list',
|
||||
gen : function(){
|
||||
return [
|
||||
'##### Cookie Tastiness',
|
||||
'| Tastiness | Cookie Type |',
|
||||
'|:----:|:-------------|',
|
||||
'| -5 | Raisin |',
|
||||
'| 8th | Chocolate Chip |',
|
||||
'| 11th | 2 or lower |',
|
||||
'| 14th | 3 or lower |',
|
||||
'| 17th | 4 or lower |\n\n',
|
||||
].join('\n');
|
||||
},
|
||||
},
|
||||
{
|
||||
name : 'Wide Table',
|
||||
icon : 'fas fa-list',
|
||||
gen : function(){
|
||||
return [
|
||||
'<div class=\'wide\'>',
|
||||
'##### Cookie Tastiness',
|
||||
'| Tastiness | Cookie Type |',
|
||||
'|:----:|:-------------|',
|
||||
'| -5 | Raisin |',
|
||||
'| 8th | Chocolate Chip |',
|
||||
'| 11th | 2 or lower |',
|
||||
'| 14th | 3 or lower |',
|
||||
'| 17th | 4 or lower |',
|
||||
'</div>\n\n'
|
||||
].join('\n');
|
||||
},
|
||||
},
|
||||
{
|
||||
name : 'Split Table',
|
||||
icon : 'fas fa-th-large',
|
||||
gen : function(){
|
||||
return [
|
||||
'<div style=\'column-count:2\'>',
|
||||
'| d10 | Damage Type |',
|
||||
'|:---:|:------------|',
|
||||
'| 1 | Acid |',
|
||||
'| 2 | Cold |',
|
||||
'| 3 | Fire |',
|
||||
'| 4 | Force |',
|
||||
'| 5 | Lightning |',
|
||||
'',
|
||||
'```',
|
||||
'```',
|
||||
'',
|
||||
'| d10 | Damage Type |',
|
||||
'|:---:|:------------|',
|
||||
'| 6 | Necrotic |',
|
||||
'| 7 | Poison |',
|
||||
'| 8 | Psychic |',
|
||||
'| 9 | Radiant |',
|
||||
'| 10 | Thunder |',
|
||||
'</div>\n\n',
|
||||
].join('\n');
|
||||
},
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
|
||||
|
||||
|
||||
/**************** PRINT *************/
|
||||
|
||||
{
|
||||
groupName : 'Print',
|
||||
icon : 'fas fa-print',
|
||||
view : 'style',
|
||||
snippets : [
|
||||
{
|
||||
name : 'A4 Page Size',
|
||||
icon : 'far fa-file',
|
||||
gen : ['/* A4 Page Size */',
|
||||
'.phb {',
|
||||
' width : 210mm;',
|
||||
' height : 296.8mm;',
|
||||
'}'
|
||||
].join('\n')
|
||||
},
|
||||
{
|
||||
name : 'Square Page Size',
|
||||
icon : 'far fa-file',
|
||||
gen : ['/* Square Page Size */',
|
||||
'.phb {',
|
||||
' width : 125mm;',
|
||||
' height : 125mm;',
|
||||
' padding : 12.5mm;',
|
||||
' columns : unset;',
|
||||
'}',
|
||||
''
|
||||
].join('\n')
|
||||
},
|
||||
{
|
||||
name : 'Ink Friendly',
|
||||
icon : 'fas fa-tint',
|
||||
gen : dedent`
|
||||
/* Ink Friendly */
|
||||
.phb, .phb blockquote, .phb hr+blockquote {
|
||||
background : white;
|
||||
box-shadow : 0px 0px 3px;
|
||||
}
|
||||
|
||||
.phb img {
|
||||
visibility : hidden;
|
||||
}`
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
];
|
||||
@@ -0,0 +1,72 @@
|
||||
const _ = require('lodash');
|
||||
|
||||
const getTOC = (pages)=>{
|
||||
const add1 = (title, page)=>{
|
||||
res.push({
|
||||
title : title,
|
||||
page : page + 1,
|
||||
children : []
|
||||
});
|
||||
};
|
||||
const add2 = (title, page)=>{
|
||||
if(!_.last(res)) add1('', page);
|
||||
_.last(res).children.push({
|
||||
title : title,
|
||||
page : page + 1,
|
||||
children : []
|
||||
});
|
||||
};
|
||||
const add3 = (title, page)=>{
|
||||
if(!_.last(res)) add1('', page);
|
||||
if(!_.last(_.last(res).children)) add2('', page);
|
||||
_.last(_.last(res).children).children.push({
|
||||
title : title,
|
||||
page : page + 1,
|
||||
children : []
|
||||
});
|
||||
};
|
||||
|
||||
const res = [];
|
||||
_.each(pages, (page, pageNum)=>{
|
||||
const lines = page.split('\n');
|
||||
_.each(lines, (line)=>{
|
||||
if(_.startsWith(line, '# ')){
|
||||
const title = line.replace('# ', '');
|
||||
add1(title, pageNum);
|
||||
}
|
||||
if(_.startsWith(line, '## ')){
|
||||
const title = line.replace('## ', '');
|
||||
add2(title, pageNum);
|
||||
}
|
||||
if(_.startsWith(line, '### ')){
|
||||
const title = line.replace('### ', '');
|
||||
add3(title, pageNum);
|
||||
}
|
||||
});
|
||||
});
|
||||
return res;
|
||||
};
|
||||
|
||||
module.exports = function(brew){
|
||||
const pages = brew.text.split('\\page');
|
||||
const TOC = getTOC(pages);
|
||||
const markdown = _.reduce(TOC, (r, g1, idx1)=>{
|
||||
r.push(`- **[${idx1 + 1} ${g1.title}](#p${g1.page})**`);
|
||||
if(g1.children.length){
|
||||
_.each(g1.children, (g2, idx2)=>{
|
||||
r.push(` - [${idx1 + 1}.${idx2 + 1} ${g2.title}](#p${g2.page})`);
|
||||
if(g2.children.length){
|
||||
_.each(g2.children, (g3, idx3)=>{
|
||||
r.push(` - [${idx1 + 1}.${idx2 + 1}.${idx3 + 1} ${g3.title}](#p${g3.page})`);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
return r;
|
||||
}, []).join('\n');
|
||||
|
||||
return `<div class='toc'>
|
||||
##### Table Of Contents
|
||||
${markdown}
|
||||
</div>\n`;
|
||||
};
|
||||
BIN
client/homebrew/googleDrive.png
Normal file
BIN
client/homebrew/googleDrive.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 305 KiB |
BIN
client/homebrew/googleDriveMono.png
Normal file
BIN
client/homebrew/googleDriveMono.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
@@ -1,19 +1,17 @@
|
||||
require('./homebrew.less');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
const cx = require('classnames');
|
||||
|
||||
const CreateRouter = require('pico-router').createRouter;
|
||||
const { StaticRouter:Router, Switch, Route } = require('react-router-dom');
|
||||
const queryString = require('query-string');
|
||||
|
||||
const HomePage = require('./pages/homePage/homePage.jsx');
|
||||
const EditPage = require('./pages/editPage/editPage.jsx');
|
||||
const UserPage = require('./pages/userPage/userPage.jsx');
|
||||
const SharePage = require('./pages/sharePage/sharePage.jsx');
|
||||
const NewPage = require('./pages/newPage/newPage.jsx');
|
||||
const ErrorPage = require('./pages/errorPage/errorPage.jsx');
|
||||
//const ErrorPage = require('./pages/errorPage/errorPage.jsx');
|
||||
const PrintPage = require('./pages/printPage/printPage.jsx');
|
||||
|
||||
let Router;
|
||||
const Homebrew = createClass({
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
@@ -22,6 +20,7 @@ const Homebrew = createClass({
|
||||
changelog : '',
|
||||
version : '0.0.0',
|
||||
account : null,
|
||||
enable_v3 : false,
|
||||
brew : {
|
||||
title : '',
|
||||
text : '',
|
||||
@@ -35,56 +34,39 @@ const Homebrew = createClass({
|
||||
componentWillMount : function() {
|
||||
global.account = this.props.account;
|
||||
global.version = this.props.version;
|
||||
|
||||
|
||||
Router = CreateRouter({
|
||||
'/edit/:id' : (args)=>{
|
||||
if(!this.props.brew.editId){
|
||||
return <ErrorPage errorId={args.id}/>;
|
||||
}
|
||||
|
||||
return <EditPage
|
||||
id={args.id}
|
||||
brew={this.props.brew} />;
|
||||
},
|
||||
|
||||
'/share/:id' : (args)=>{
|
||||
if(!this.props.brew.shareId){
|
||||
return <ErrorPage errorId={args.id}/>;
|
||||
}
|
||||
|
||||
return <SharePage
|
||||
id={args.id}
|
||||
brew={this.props.brew} />;
|
||||
},
|
||||
'/user/:username' : (args)=>{
|
||||
return <UserPage
|
||||
username={args.username}
|
||||
brews={this.props.brews}
|
||||
/>;
|
||||
},
|
||||
'/print/:id' : (args, query)=>{
|
||||
return <PrintPage brew={this.props.brew} query={query}/>;
|
||||
},
|
||||
'/print' : (args, query)=>{
|
||||
return <PrintPage query={query}/>;
|
||||
},
|
||||
'/new' : (args)=>{
|
||||
return <NewPage />;
|
||||
},
|
||||
'/changelog' : (args)=>{
|
||||
return <SharePage
|
||||
brew={{ title: 'Changelog', text: this.props.changelog }} />;
|
||||
},
|
||||
'*' : <HomePage
|
||||
welcomeText={this.props.welcomeText} />,
|
||||
});
|
||||
global.enable_v3 = this.props.enable_v3;
|
||||
},
|
||||
render : function(){
|
||||
return <div className='homebrew'>
|
||||
<Router defaultUrl={this.props.url}/>
|
||||
</div>;
|
||||
render : function (){
|
||||
return (
|
||||
<Router location={this.props.url}>
|
||||
<div className='homebrew'>
|
||||
<Switch>
|
||||
<Route path='/edit/:id' component={(routeProps)=><EditPage id={routeProps.match.params.id} brew={this.props.brew} />}/>
|
||||
<Route path='/share/:id' component={(routeProps)=><SharePage id={routeProps.match.params.id} brew={this.props.brew} />}/>
|
||||
<Route path='/new/:id' component={(routeProps)=><NewPage id={routeProps.match.params.id} brew={this.props.brew} />}/>
|
||||
<Route path='/new' exact component={(routeProps)=><NewPage />}/>
|
||||
<Route path='/user/:username' component={(routeProps)=><UserPage username={routeProps.match.params.username} brews={this.props.brews} />}/>
|
||||
<Route path='/print/:id' component={(routeProps)=><PrintPage brew={this.props.brew} query={queryString.parse(routeProps.location.search)} />}/>
|
||||
<Route path='/print' exact component={(routeProps)=><PrintPage query={queryString.parse(routeProps.location.search)} />}/>
|
||||
<Route path='/changelog' exact component={()=><SharePage brew={this.props.brew} />}/>
|
||||
<Route path='/v3_preview' exact component={()=><HomePage brew={this.props.brew} />}/>
|
||||
<Route path='/' component={()=><HomePage brew={this.props.brew} />}/>
|
||||
</Switch>
|
||||
</div>
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = Homebrew;
|
||||
|
||||
//TODO: Nicer Error page instead of just "cant get that"
|
||||
// '/share/:id' : (args)=>{
|
||||
// if(!this.props.brew.shareId){
|
||||
// return <ErrorPage errorId={args.id}/>;
|
||||
// }
|
||||
//
|
||||
// return <SharePage
|
||||
// id={args.id}
|
||||
// brew={this.props.brew} />;
|
||||
// },
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
|
||||
@import 'naturalcrit/styles/core.less';
|
||||
.homebrew{
|
||||
height : 100%;
|
||||
.page{
|
||||
.sitePage{
|
||||
display : flex;
|
||||
height : 100%;
|
||||
background-color : @steel;
|
||||
|
||||
@@ -2,17 +2,33 @@ const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||
|
||||
module.exports = function(props){
|
||||
if(global.account){
|
||||
return <Nav.item href={`/user/${global.account.username}`} color='yellow' icon='fa-user'>
|
||||
{global.account.username}
|
||||
const Account = createClass({
|
||||
|
||||
getInitialState : function() {
|
||||
return {
|
||||
url : ''
|
||||
};
|
||||
},
|
||||
|
||||
componentDidMount : function(){
|
||||
if(typeof window !== 'undefined'){
|
||||
this.setState({
|
||||
url : window.location.href
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
render : function(){
|
||||
if(global.account){
|
||||
return <Nav.item href={`/user/${global.account.username}`} color='yellow' icon='fas fa-user'>
|
||||
{global.account.username}
|
||||
</Nav.item>;
|
||||
}
|
||||
|
||||
return <Nav.item href={`https://www.naturalcrit.com/login?redirect=${this.state.url}`} color='teal' icon='fas fa-sign-in-alt'>
|
||||
login
|
||||
</Nav.item>;
|
||||
}
|
||||
let url = '';
|
||||
if(typeof window !== 'undefined'){
|
||||
url = window.location.href;
|
||||
}
|
||||
return <Nav.item href={`http://naturalcrit.com/login?redirect=${url}`} color='teal' icon='fa-sign-in'>
|
||||
login
|
||||
</Nav.item>;
|
||||
};
|
||||
});
|
||||
|
||||
module.exports = Account;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
const cx = require('classnames');
|
||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||
|
||||
@@ -31,4 +30,4 @@ const EditTitle = createClass({
|
||||
|
||||
});
|
||||
|
||||
module.exports = EditTitle;
|
||||
module.exports = EditTitle;
|
||||
|
||||
@@ -6,8 +6,8 @@ module.exports = function(props){
|
||||
return <Nav.item
|
||||
newTab={true}
|
||||
color='red'
|
||||
icon='fa-bug'
|
||||
icon='fas fa-bug'
|
||||
href={`https://www.reddit.com/r/homebrewery/submit?selftext=true&title=${encodeURIComponent('[Issue] Describe Your Issue Here')}`} >
|
||||
report issue
|
||||
</Nav.item>;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
require('./navbar.less');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
|
||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||
const PatreonNavItem = require('./patreon.navitem.jsx');
|
||||
|
||||
const Navbar = createClass({
|
||||
getInitialState : function() {
|
||||
@@ -39,7 +40,7 @@ const Navbar = createClass({
|
||||
<div>The Homebrewery</div>
|
||||
</Nav.item>
|
||||
<Nav.item>{`v${this.state.ver}`}</Nav.item>
|
||||
|
||||
<PatreonNavItem />
|
||||
{/*this.renderChromeWarning()*/}
|
||||
</Nav.section>
|
||||
{this.props.children}
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
@navbarHeight : 28px;
|
||||
@keyframes coloring {
|
||||
//from {color: white;}
|
||||
//to {color: red;}
|
||||
0% {color: pink;}
|
||||
50% {color: pink;}
|
||||
75% {color: red;}
|
||||
100% {color: pink;}
|
||||
}
|
||||
.homebrew nav{
|
||||
.homebrewLogo{
|
||||
.animate(color);
|
||||
@@ -47,11 +55,16 @@
|
||||
text-transform : initial;
|
||||
}
|
||||
.patreon.navItem{
|
||||
border-left : 1px solid #666;
|
||||
border-right : 1px solid #666;
|
||||
&:hover i {
|
||||
color: red;
|
||||
}
|
||||
i{
|
||||
.animate(color);
|
||||
&:hover{
|
||||
color : @red;
|
||||
}
|
||||
animation-name: coloring;
|
||||
animation-duration: 2s;
|
||||
color: pink;
|
||||
}
|
||||
}
|
||||
.recent.navItem{
|
||||
@@ -125,4 +138,4 @@
|
||||
text-align : center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
11
client/homebrew/navbar/newbrew.navitem.jsx
Normal file
11
client/homebrew/navbar/newbrew.navitem.jsx
Normal file
@@ -0,0 +1,11 @@
|
||||
const React = require('react');
|
||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||
|
||||
module.exports = function(props){
|
||||
return <Nav.item
|
||||
href='/new'
|
||||
color='purple'
|
||||
icon='fas fa-plus-square'>
|
||||
new
|
||||
</Nav.item>;
|
||||
};
|
||||
@@ -1,14 +1,13 @@
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||
|
||||
module.exports = function(props){
|
||||
return <Nav.item
|
||||
className='patreon'
|
||||
newTab={true}
|
||||
href='https://www.patreon.com/stolksdorf'
|
||||
href='https://www.patreon.com/NaturalCrit'
|
||||
color='green'
|
||||
icon='fa-heart'>
|
||||
icon='fas fa-heart'>
|
||||
help out
|
||||
</Nav.item>;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ const createClass = require('create-react-class');
|
||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||
|
||||
module.exports = function(props){
|
||||
return <Nav.item newTab={true} href={`/print/${props.shareId}?dialog=true`} color='purple' icon='fa-file-pdf-o'>
|
||||
return <Nav.item newTab={true} href={`/print/${props.shareId}?dialog=true`} color='purple' icon='far fa-file-pdf'>
|
||||
get PDF
|
||||
</Nav.item>;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -35,24 +35,32 @@ const RecentItems = createClass({
|
||||
|
||||
//== Add current brew to appropriate recent items list (depending on storageKey) ==//
|
||||
if(this.props.storageKey == 'edit'){
|
||||
let editId = this.props.brew.editId;
|
||||
if(this.props.brew.googleId){
|
||||
editId = `${this.props.brew.googleId}${this.props.brew.editId}`;
|
||||
}
|
||||
edited = _.filter(edited, (brew)=>{
|
||||
return brew.id !== this.props.brew.editId;
|
||||
return brew.id !== editId;
|
||||
});
|
||||
edited.unshift({
|
||||
id : this.props.brew.editId,
|
||||
id : editId,
|
||||
title : this.props.brew.title,
|
||||
url : `/edit/${this.props.brew.editId}`,
|
||||
url : `/edit/${editId}`,
|
||||
ts : Date.now()
|
||||
});
|
||||
}
|
||||
if(this.props.storageKey == 'view'){
|
||||
let shareId = this.props.brew.shareId;
|
||||
if(this.props.brew.googleId){
|
||||
shareId = `${this.props.brew.googleId}${this.props.brew.shareId}`;
|
||||
}
|
||||
viewed = _.filter(viewed, (brew)=>{
|
||||
return brew.id !== this.props.brew.shareId;
|
||||
return brew.id !== shareId;
|
||||
});
|
||||
viewed.unshift({
|
||||
id : this.props.brew.shareId,
|
||||
id : shareId,
|
||||
title : this.props.brew.title,
|
||||
url : `/share/${this.props.brew.shareId}`,
|
||||
url : `/share/${shareId}`,
|
||||
ts : Date.now()
|
||||
});
|
||||
}
|
||||
@@ -70,6 +78,41 @@ const RecentItems = createClass({
|
||||
});
|
||||
},
|
||||
|
||||
componentDidUpdate : function(prevProps) {
|
||||
if(prevProps.brew && this.props.brew.editId !== prevProps.brew.editId) {
|
||||
let edited = JSON.parse(localStorage.getItem(EDIT_KEY) || '[]');
|
||||
if(this.props.storageKey == 'edit') {
|
||||
let prevEditId = prevProps.brew.editId;
|
||||
if(prevProps.brew.googleId){
|
||||
prevEditId = `${prevProps.brew.googleId}${prevProps.brew.editId}`;
|
||||
}
|
||||
|
||||
edited = _.filter(this.state.edit, (brew)=>{
|
||||
return brew.id !== prevEditId;
|
||||
});
|
||||
let editId = this.props.brew.editId;
|
||||
if(this.props.brew.googleId){
|
||||
editId = `${this.props.brew.googleId}${this.props.brew.editId}`;
|
||||
}
|
||||
edited.unshift({
|
||||
id : editId,
|
||||
title : this.props.brew.title,
|
||||
url : `/edit/${editId}`,
|
||||
ts : Date.now()
|
||||
});
|
||||
}
|
||||
|
||||
//== Store the updated lists (up to 8 items each) ==//
|
||||
edited = _.slice(edited, 0, 8);
|
||||
|
||||
localStorage.setItem(EDIT_KEY, JSON.stringify(edited));
|
||||
|
||||
this.setState({
|
||||
edit : edited
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
handleDropdown : function(show){
|
||||
this.setState({
|
||||
showDropdown : show
|
||||
@@ -81,7 +124,7 @@ const RecentItems = createClass({
|
||||
|
||||
const makeItems = (brews)=>{
|
||||
return _.map(brews, (brew)=>{
|
||||
return <a href={brew.url} className='item' key={brew.id} target='_blank' rel='noopener noreferrer'>
|
||||
return <a href={brew.url} className='item' key={brew.id} target='_blank' rel='noopener noreferrer' title={brew.title || '[ no title ]'}>
|
||||
<span className='title'>{brew.title || '[ no title ]'}</span>
|
||||
<span className='time'>{Moment(brew.ts).fromNow()}</span>
|
||||
</a>;
|
||||
@@ -101,7 +144,7 @@ const RecentItems = createClass({
|
||||
},
|
||||
|
||||
render : function(){
|
||||
return <Nav.item icon='fa-clock-o' color='grey' className='recent'
|
||||
return <Nav.item icon='fas fa-history' color='grey' className='recent'
|
||||
onMouseEnter={()=>this.handleDropdown(true)}
|
||||
onMouseLeave={()=>this.handleDropdown(false)}>
|
||||
{this.props.text}
|
||||
@@ -140,4 +183,4 @@ module.exports = {
|
||||
showView={true}
|
||||
/>;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
const cx = require('classnames');
|
||||
|
||||
//var striptags = require('striptags');
|
||||
|
||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||
|
||||
const MAX_URL_SIZE = 2083;
|
||||
@@ -31,10 +27,7 @@ const RedditShare = createClass({
|
||||
const url = [
|
||||
MAIN_URL,
|
||||
`title=${encodeURIComponent(this.props.brew.title ? this.props.brew.title : 'Check out my brew!')}`,
|
||||
|
||||
`text=${encodeURIComponent(this.props.brew.text)}`
|
||||
|
||||
|
||||
].join('&');
|
||||
|
||||
window.open(url, '_blank');
|
||||
@@ -49,4 +42,4 @@ const RedditShare = createClass({
|
||||
|
||||
});
|
||||
|
||||
module.exports = RedditShare;
|
||||
module.exports = RedditShare;
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
/* eslint-disable max-lines */
|
||||
require('./editPage.less');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
const cx = require('classnames');
|
||||
const request = require('superagent');
|
||||
const { Meta } = require('vitreum/headtags');
|
||||
|
||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||
const Navbar = require('../../navbar/navbar.jsx');
|
||||
|
||||
const NewBrew = require('../../navbar/newbrew.navitem.jsx');
|
||||
const ReportIssue = require('../../navbar/issue.navitem.jsx');
|
||||
const PrintLink = require('../../navbar/print.navitem.jsx');
|
||||
const Account = require('../../navbar/account.navitem.jsx');
|
||||
@@ -18,42 +21,58 @@ const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
||||
|
||||
const Markdown = require('naturalcrit/markdown.js');
|
||||
|
||||
const SAVE_TIMEOUT = 3000;
|
||||
const googleDriveActive = require('../../googleDrive.png');
|
||||
const googleDriveInactive = require('../../googleDriveMono.png');
|
||||
|
||||
const SAVE_TIMEOUT = 3000;
|
||||
|
||||
const EditPage = createClass({
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
brew : {
|
||||
text : '',
|
||||
style : '',
|
||||
shareId : null,
|
||||
editId : null,
|
||||
createdAt : null,
|
||||
updatedAt : null,
|
||||
gDrive : false,
|
||||
trashed : false,
|
||||
|
||||
title : '',
|
||||
description : '',
|
||||
tags : '',
|
||||
published : false,
|
||||
authors : [],
|
||||
systems : []
|
||||
systems : [],
|
||||
renderer : 'legacy'
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState : function() {
|
||||
return {
|
||||
brew : this.props.brew,
|
||||
|
||||
isSaving : false,
|
||||
isPending : false,
|
||||
errors : null,
|
||||
htmlErrors : Markdown.validate(this.props.brew.text),
|
||||
brew : this.props.brew,
|
||||
isSaving : false,
|
||||
isPending : false,
|
||||
alertTrashedGoogleBrew : this.props.brew.trashed,
|
||||
alertLoginToTransfer : false,
|
||||
saveGoogle : this.props.brew.googleId ? true : false,
|
||||
confirmGoogleTransfer : false,
|
||||
errors : null,
|
||||
htmlErrors : Markdown.validate(this.props.brew.text),
|
||||
url : ''
|
||||
};
|
||||
},
|
||||
savedBrew : null,
|
||||
|
||||
componentDidMount : function(){
|
||||
this.setState({
|
||||
url : window.location.href
|
||||
});
|
||||
|
||||
this.savedBrew = JSON.parse(JSON.stringify(this.props.brew)); //Deep copy
|
||||
|
||||
this.trySave();
|
||||
window.onbeforeunload = ()=>{
|
||||
if(this.state.isSaving || this.state.isPending){
|
||||
@@ -72,13 +91,12 @@ const EditPage = createClass({
|
||||
document.removeEventListener('keydown', this.handleControlKeys);
|
||||
},
|
||||
|
||||
|
||||
handleControlKeys : function(e){
|
||||
if(!(e.ctrlKey || e.metaKey)) return;
|
||||
const S_KEY = 83;
|
||||
const P_KEY = 80;
|
||||
if(e.keyCode == S_KEY) this.save();
|
||||
if(e.keyCode == P_KEY) window.open(`/print/${this.props.brew.shareId}?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){
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
@@ -89,17 +107,8 @@ const EditPage = createClass({
|
||||
this.refs.editor.update();
|
||||
},
|
||||
|
||||
handleMetadataChange : function(metadata){
|
||||
this.setState((prevState)=>({
|
||||
brew : _.merge({}, prevState.brew, metadata),
|
||||
isPending : true,
|
||||
}), ()=>this.trySave());
|
||||
|
||||
},
|
||||
|
||||
handleTextChange : function(text){
|
||||
|
||||
//If there are errors, run the validator on everychange to give quick feedback
|
||||
//If there are errors, run the validator on every change to give quick feedback
|
||||
let htmlErrors = this.state.htmlErrors;
|
||||
if(htmlErrors.length) htmlErrors = Markdown.validate(text);
|
||||
|
||||
@@ -110,9 +119,23 @@ const EditPage = createClass({
|
||||
}), ()=>this.trySave());
|
||||
},
|
||||
|
||||
handleStyleChange : function(style){
|
||||
this.setState((prevState)=>({
|
||||
brew : _.merge({}, prevState.brew, { style: style }),
|
||||
isPending : true
|
||||
}), ()=>this.trySave());
|
||||
},
|
||||
|
||||
handleMetaChange : function(metadata){
|
||||
this.setState((prevState)=>({
|
||||
brew : _.merge({}, prevState.brew, metadata),
|
||||
isPending : true,
|
||||
}), ()=>this.trySave());
|
||||
|
||||
},
|
||||
|
||||
hasChanges : function(){
|
||||
const savedBrew = this.savedBrew ? this.savedBrew : this.props.brew;
|
||||
return !_.isEqual(this.state.brew, savedBrew);
|
||||
return !_.isEqual(this.state.brew, this.savedBrew);
|
||||
},
|
||||
|
||||
trySave : function(){
|
||||
@@ -124,7 +147,45 @@ const EditPage = createClass({
|
||||
}
|
||||
},
|
||||
|
||||
save : function(){
|
||||
handleGoogleClick : function(){
|
||||
if(!global.account?.googleId) {
|
||||
this.setState({
|
||||
alertLoginToTransfer : true
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.setState((prevState)=>({
|
||||
confirmGoogleTransfer : !prevState.confirmGoogleTransfer
|
||||
}));
|
||||
this.clearErrors();
|
||||
},
|
||||
|
||||
closeAlerts : function(event){
|
||||
event.stopPropagation(); //Only handle click once so alert doesn't reopen
|
||||
this.setState({
|
||||
alertTrashedGoogleBrew : false,
|
||||
alertLoginToTransfer : false,
|
||||
confirmGoogleTransfer : false
|
||||
});
|
||||
},
|
||||
|
||||
toggleGoogleStorage : function(){
|
||||
this.setState((prevState)=>({
|
||||
saveGoogle : !prevState.saveGoogle,
|
||||
isSaving : false,
|
||||
errors : null
|
||||
}), ()=>this.save());
|
||||
},
|
||||
|
||||
clearErrors : function(){
|
||||
this.setState({
|
||||
errors : null,
|
||||
isSaving : false
|
||||
|
||||
});
|
||||
},
|
||||
|
||||
save : async function(){
|
||||
if(this.debounceSave && this.debounceSave.cancel) this.debounceSave.cancel();
|
||||
|
||||
this.setState((prevState)=>({
|
||||
@@ -133,22 +194,130 @@ const EditPage = createClass({
|
||||
htmlErrors : Markdown.validate(prevState.brew.text)
|
||||
}));
|
||||
|
||||
request
|
||||
.put(`/api/update/${this.props.brew.editId}`)
|
||||
.send(this.state.brew)
|
||||
.end((err, res)=>{
|
||||
if(err){
|
||||
this.setState({
|
||||
errors : err,
|
||||
});
|
||||
} else {
|
||||
this.savedBrew = res.body;
|
||||
this.setState({
|
||||
isPending : false,
|
||||
isSaving : false,
|
||||
});
|
||||
}
|
||||
});
|
||||
const transfer = this.state.saveGoogle == _.isNil(this.state.brew.googleId);
|
||||
|
||||
const brew = this.state.brew;
|
||||
brew.pageCount = ((brew.renderer=='legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1;
|
||||
|
||||
if(this.state.saveGoogle) {
|
||||
if(transfer) {
|
||||
const res = await request
|
||||
.post('/api/newGoogle/')
|
||||
.send(brew)
|
||||
.catch((err)=>{
|
||||
console.log(err.status === 401
|
||||
? 'Not signed in!'
|
||||
: 'Error Transferring to Google!');
|
||||
this.setState({ errors: err, saveGoogle: false });
|
||||
});
|
||||
|
||||
if(!res) { return; }
|
||||
|
||||
console.log('Deleting Local Copy');
|
||||
await request.delete(`/api/${brew.editId}`)
|
||||
.send()
|
||||
.catch((err)=>{
|
||||
console.log('Error deleting Local Copy');
|
||||
});
|
||||
|
||||
this.savedBrew = res.body;
|
||||
history.replaceState(null, null, `/edit/${this.savedBrew.googleId}${this.savedBrew.editId}`); //update URL to match doc ID
|
||||
} else {
|
||||
const res = await request
|
||||
.put(`/api/updateGoogle/${brew.editId}`)
|
||||
.send(brew)
|
||||
.catch((err)=>{
|
||||
console.log(err.status === 401
|
||||
? 'Not signed in!'
|
||||
: 'Error Saving to Google!');
|
||||
this.setState({ errors: err });
|
||||
return;
|
||||
});
|
||||
|
||||
this.savedBrew = res.body;
|
||||
}
|
||||
} else {
|
||||
if(transfer) {
|
||||
const res = await request.post('/api')
|
||||
.send(brew)
|
||||
.catch((err)=>{
|
||||
console.log('Error creating Local Copy');
|
||||
this.setState({ errors: err });
|
||||
return;
|
||||
});
|
||||
|
||||
await request.get(`/api/removeGoogle/${brew.googleId}${brew.editId}`)
|
||||
.send()
|
||||
.catch((err)=>{
|
||||
console.log('Error Deleting Google Brew');
|
||||
});
|
||||
|
||||
this.savedBrew = res.body;
|
||||
history.replaceState(null, null, `/edit/${this.savedBrew.editId}`); //update URL to match doc ID
|
||||
} else {
|
||||
const res = await request
|
||||
.put(`/api/update/${brew.editId}`)
|
||||
.send(brew)
|
||||
.catch((err)=>{
|
||||
console.log('Error Updating Local Brew');
|
||||
this.setState({ errors: err });
|
||||
return;
|
||||
});
|
||||
|
||||
this.savedBrew = res.body;
|
||||
}
|
||||
}
|
||||
|
||||
this.setState((prevState)=>({
|
||||
brew : _.merge({}, prevState.brew, {
|
||||
googleId : this.savedBrew.googleId ? this.savedBrew.googleId : null,
|
||||
editId : this.savedBrew.editId,
|
||||
shareId : this.savedBrew.shareId
|
||||
}),
|
||||
isPending : false,
|
||||
isSaving : false,
|
||||
}));
|
||||
},
|
||||
|
||||
renderGoogleDriveIcon : function(){
|
||||
return <Nav.item className='googleDriveStorage' onClick={this.handleGoogleClick}>
|
||||
{this.state.saveGoogle
|
||||
? <img src={googleDriveActive} alt='googleDriveActive'/>
|
||||
: <img src={googleDriveInactive} alt='googleDriveInactive'/>
|
||||
}
|
||||
|
||||
{this.state.confirmGoogleTransfer &&
|
||||
<div className='errorContainer' onClick={this.closeAlerts}>
|
||||
{ this.state.saveGoogle
|
||||
? `Would you like to transfer this brew from your Google Drive storage back to the Homebrewery?`
|
||||
: `Would you like to transfer this brew from the Homebrewery to your personal Google Drive storage?`
|
||||
}
|
||||
<br />
|
||||
<div className='confirm' onClick={this.toggleGoogleStorage}>
|
||||
Yes
|
||||
</div>
|
||||
<div className='deny'>
|
||||
No
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
{this.state.alertLoginToTransfer &&
|
||||
<div className='errorContainer' onClick={this.closeAlerts}>
|
||||
You must be signed in to a Google account to transfer
|
||||
between the homebrewery and Google Drive!
|
||||
<a target='_blank' rel='noopener noreferrer'
|
||||
href={`https://www.naturalcrit.com/login?redirect=${this.state.url}`}>
|
||||
<div className='confirm'>
|
||||
Sign In
|
||||
</div>
|
||||
</a>
|
||||
<div className='deny'>
|
||||
Not Now
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</Nav.item>;
|
||||
},
|
||||
|
||||
renderSaveButton : function(){
|
||||
@@ -156,15 +325,57 @@ const EditPage = createClass({
|
||||
let errMsg = '';
|
||||
try {
|
||||
errMsg += `${this.state.errors.toString()}\n\n`;
|
||||
errMsg += `\`\`\`\n${JSON.stringify(this.state.errors.response.error, null, ' ')}\n\`\`\``;
|
||||
errMsg += `\`\`\`\n${this.state.errors.stack}\n`;
|
||||
errMsg += `${JSON.stringify(this.state.errors.response.error, null, ' ')}\n\`\`\``;
|
||||
console.log(errMsg);
|
||||
} catch (e){}
|
||||
|
||||
return <Nav.item className='save error' icon='fa-warning'>
|
||||
if(this.state.errors.status == '401'){
|
||||
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||
Oops!
|
||||
<div className='errorContainer' onClick={this.clearErrors}>
|
||||
You must be signed in to a Google account
|
||||
to save this to<br />Google Drive!<br />
|
||||
<a target='_blank' rel='noopener noreferrer'
|
||||
href={`https://www.naturalcrit.com/login?redirect=${this.state.url}`}>
|
||||
<div className='confirm'>
|
||||
Sign In
|
||||
</div>
|
||||
</a>
|
||||
<div className='deny'>
|
||||
Not Now
|
||||
</div>
|
||||
</div>
|
||||
</Nav.item>;
|
||||
}
|
||||
|
||||
if(this.state.errors.status == '403' && this.state.errors.response.body.errors[0].reason == 'insufficientPermissions'){
|
||||
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||
Oops!
|
||||
<div className='errorContainer' onClick={this.clearErrors}>
|
||||
Looks like your Google credentials have
|
||||
expired! Visit the log in page to sign out
|
||||
and sign back in with Google
|
||||
to save this to Google Drive!
|
||||
<a target='_blank' rel='noopener noreferrer'
|
||||
href={`https://www.naturalcrit.com/login?redirect=${this.state.url}`}>
|
||||
<div className='confirm'>
|
||||
Sign In
|
||||
</div>
|
||||
</a>
|
||||
<div className='deny'>
|
||||
Not Now
|
||||
</div>
|
||||
</div>
|
||||
</Nav.item>;
|
||||
}
|
||||
|
||||
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||
Oops!
|
||||
<div className='errorContainer'>
|
||||
Looks like there was a problem saving. <br />
|
||||
Report the issue <a target='_blank' rel='noopener noreferrer'
|
||||
href={`https://github.com/stolksdorf/naturalcrit/issues/new?body=${encodeURIComponent(errMsg)}`}>
|
||||
href={`https://github.com/naturalcrit/homebrewery/issues/new?body=${encodeURIComponent(errMsg)}`}>
|
||||
here
|
||||
</a>.
|
||||
</div>
|
||||
@@ -172,48 +383,70 @@ const EditPage = createClass({
|
||||
}
|
||||
|
||||
if(this.state.isSaving){
|
||||
return <Nav.item className='save' icon='fa-spinner fa-spin'>saving...</Nav.item>;
|
||||
return <Nav.item className='save' icon='fas fa-spinner fa-spin'>saving...</Nav.item>;
|
||||
}
|
||||
if(this.state.isPending && this.hasChanges()){
|
||||
return <Nav.item className='save' onClick={this.save} color='blue' icon='fa-save'>Save Now</Nav.item>;
|
||||
return <Nav.item className='save' onClick={this.save} color='blue' icon='fas fa-save'>Save Now</Nav.item>;
|
||||
}
|
||||
if(!this.state.isPending && !this.state.isSaving){
|
||||
return <Nav.item className='save saved'>saved.</Nav.item>;
|
||||
}
|
||||
},
|
||||
|
||||
processShareId : function() {
|
||||
return this.state.brew.googleId ?
|
||||
this.state.brew.googleId + this.state.brew.shareId :
|
||||
this.state.brew.shareId;
|
||||
},
|
||||
|
||||
renderNavbar : function(){
|
||||
return <Navbar>
|
||||
|
||||
{this.state.alertTrashedGoogleBrew &&
|
||||
<div className='errorContainer' onClick={this.closeAlerts}>
|
||||
This brew is currently in your Trash folder on Google Drive!<br />If you want to keep it, make sure to move it before it is deleted permanently!<br />
|
||||
<div className='confirm'>
|
||||
OK
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<Nav.section>
|
||||
<Nav.item className='brewTitle'>{this.state.brew.title}</Nav.item>
|
||||
</Nav.section>
|
||||
|
||||
<Nav.section>
|
||||
{this.renderGoogleDriveIcon()}
|
||||
{this.renderSaveButton()}
|
||||
<NewBrew />
|
||||
<ReportIssue />
|
||||
<Nav.item newTab={true} href={`/share/${this.props.brew.shareId}`} color='teal' icon='fa-share-alt'>
|
||||
<Nav.item newTab={true} href={`/share/${this.processShareId()}`} color='teal' icon='fas fa-share-alt'>
|
||||
Share
|
||||
</Nav.item>
|
||||
<PrintLink shareId={this.props.brew.shareId} />
|
||||
<RecentNavItem brew={this.props.brew} storageKey='edit' />
|
||||
<PrintLink shareId={this.processShareId()} />
|
||||
<RecentNavItem brew={this.state.brew} storageKey='edit' />
|
||||
<Account />
|
||||
</Nav.section>
|
||||
|
||||
</Navbar>;
|
||||
},
|
||||
|
||||
render : function(){
|
||||
return <div className='editPage page'>
|
||||
return <div className='editPage sitePage'>
|
||||
<Meta name='robots' content='noindex, nofollow' />
|
||||
{this.renderNavbar()}
|
||||
|
||||
<div className='content'>
|
||||
<SplitPane onDragFinish={this.handleSplitMove} ref='pane'>
|
||||
<Editor
|
||||
ref='editor'
|
||||
value={this.state.brew.text}
|
||||
onChange={this.handleTextChange}
|
||||
metadata={this.state.brew}
|
||||
onMetadataChange={this.handleMetadataChange}
|
||||
brew={this.state.brew}
|
||||
onTextChange={this.handleTextChange}
|
||||
onStyleChange={this.handleStyleChange}
|
||||
onMetaChange={this.handleMetaChange}
|
||||
renderer={this.state.brew.renderer}
|
||||
/>
|
||||
<BrewRenderer text={this.state.brew.text} errors={this.state.htmlErrors} />
|
||||
<BrewRenderer text={this.state.brew.text} style={this.state.brew.style} renderer={this.state.brew.renderer} errors={this.state.htmlErrors} />
|
||||
</SplitPane>
|
||||
</div>
|
||||
</div>;
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
|
||||
@keyframes glideDown {
|
||||
0% {transform : translate(-50% + 3px, 0px);
|
||||
opacity : 0;}
|
||||
100% {transform : translate(-50% + 3px, 10px);
|
||||
opacity : 1;}
|
||||
}
|
||||
.editPage{
|
||||
.navItem.save{
|
||||
width : 105px;
|
||||
width : 106px;
|
||||
text-align : center;
|
||||
position : relative;
|
||||
&.saved{
|
||||
cursor : initial;
|
||||
color : #666;
|
||||
@@ -10,18 +16,84 @@
|
||||
&.error{
|
||||
position : relative;
|
||||
background-color : @red;
|
||||
.errorContainer{
|
||||
position : absolute;
|
||||
top : 29px;
|
||||
left : -20px;
|
||||
z-index : 1000;
|
||||
width : 120px;
|
||||
padding : 8px;
|
||||
background-color : #333;
|
||||
a{
|
||||
color : @teal;
|
||||
}
|
||||
}
|
||||
}
|
||||
.googleDriveStorage {
|
||||
position : relative;
|
||||
}
|
||||
.googleDriveStorage img{
|
||||
height : 20px;
|
||||
padding : 0px;
|
||||
margin : -5px;
|
||||
}
|
||||
.errorContainer{
|
||||
animation-name: glideDown;
|
||||
animation-duration: 0.4s;
|
||||
position : absolute;
|
||||
top : 100%;
|
||||
left : 50%;
|
||||
z-index : 100000;
|
||||
width : 140px;
|
||||
padding : 3px;
|
||||
color : white;
|
||||
background-color : #333;
|
||||
border : 3px solid #444;
|
||||
border-radius : 5px;
|
||||
transform : translate(-50% + 3px, 10px);
|
||||
text-align : center;
|
||||
font-size : 10px;
|
||||
font-weight : 800;
|
||||
text-transform : uppercase;
|
||||
a{
|
||||
color : @teal;
|
||||
}
|
||||
&:before {
|
||||
content: "";
|
||||
width: 0px;
|
||||
height: 0px;
|
||||
position: absolute;
|
||||
border-left: 10px solid transparent;
|
||||
border-right: 10px solid transparent;
|
||||
border-top: 10px solid transparent;
|
||||
border-bottom: 10px solid #444;
|
||||
left: 53px;
|
||||
top: -23px;
|
||||
}
|
||||
&:after {
|
||||
content: "";
|
||||
width: 0px;
|
||||
height: 0px;
|
||||
position: absolute;
|
||||
border-left: 10px solid transparent;
|
||||
border-right: 10px solid transparent;
|
||||
border-top: 10px solid transparent;
|
||||
border-bottom: 10px solid #333;
|
||||
left: 53px;
|
||||
top: -19px;
|
||||
}
|
||||
.deny {
|
||||
width : 48%;
|
||||
margin : 1px;
|
||||
padding : 5px;
|
||||
background-color : #333;
|
||||
display : inline-block;
|
||||
border-left : 1px solid #666;
|
||||
.animate(background-color);
|
||||
&:hover{
|
||||
background-color : red;
|
||||
}
|
||||
}
|
||||
.confirm {
|
||||
width : 48%;
|
||||
margin : 1px;
|
||||
padding : 5px;
|
||||
background-color : #333;
|
||||
display : inline-block;
|
||||
color : white;
|
||||
.animate(background-color);
|
||||
&:hover{
|
||||
background-color : teal;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
require('./errorPage.less');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
@@ -22,7 +23,7 @@ const ErrorPage = createClass({
|
||||
text : '# Oops \n We could not find a brew with that id. **Sorry!**',
|
||||
|
||||
render : function(){
|
||||
return <div className='errorPage page'>
|
||||
return <div className='errorPage sitePage'>
|
||||
<Navbar ver={this.props.ver}>
|
||||
<Nav.section>
|
||||
<Nav.item className='errorTitle'>
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
require('./homePage.less');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
const cx = require('classnames');
|
||||
const request = require('superagent');
|
||||
const { Meta } = require('vitreum/headtags');
|
||||
|
||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||
const Navbar = require('../../navbar/navbar.jsx');
|
||||
const PatreonNavItem = require('../../navbar/patreon.navitem.jsx');
|
||||
const NewBrewItem = require('../../navbar/newbrew.navitem.jsx');
|
||||
const IssueNavItem = require('../../navbar/issue.navitem.jsx');
|
||||
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
||||
const AccountNavItem = require('../../navbar/account.navitem.jsx');
|
||||
@@ -21,21 +23,22 @@ const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
||||
const HomePage = createClass({
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
welcomeText : '',
|
||||
ver : '0.0.0'
|
||||
brew : {
|
||||
text : '',
|
||||
},
|
||||
ver : '0.0.0'
|
||||
};
|
||||
|
||||
|
||||
},
|
||||
getInitialState : function() {
|
||||
return {
|
||||
text : this.props.welcomeText
|
||||
brew : this.props.brew,
|
||||
welcomeText : this.props.brew.text
|
||||
};
|
||||
},
|
||||
handleSave : function(){
|
||||
request.post('/api')
|
||||
.send({
|
||||
text : this.state.text
|
||||
text : this.state.brew.text
|
||||
})
|
||||
.end((err, res)=>{
|
||||
if(err) return;
|
||||
@@ -47,46 +50,48 @@ const HomePage = createClass({
|
||||
this.refs.editor.update();
|
||||
},
|
||||
handleTextChange : function(text){
|
||||
this.setState({
|
||||
text : text
|
||||
});
|
||||
this.setState((prevState)=>({
|
||||
brew : _.merge({}, prevState.brew, { text: text })
|
||||
}));
|
||||
},
|
||||
renderNavbar : function(){
|
||||
return <Navbar ver={this.props.ver}>
|
||||
<Nav.section>
|
||||
<PatreonNavItem />
|
||||
<NewBrewItem />
|
||||
<IssueNavItem />
|
||||
<Nav.item newTab={true} href='/changelog' color='purple' icon='fa-file-text-o'>
|
||||
<Nav.item newTab={true} href='/changelog' color='purple' icon='far fa-file-alt'>
|
||||
Changelog
|
||||
</Nav.item>
|
||||
<RecentNavItem />
|
||||
<AccountNavItem />
|
||||
{/*}
|
||||
<Nav.item href='/new' color='green' icon='fa-external-link'>
|
||||
New Brew
|
||||
</Nav.item>
|
||||
*/}
|
||||
</Nav.section>
|
||||
</Navbar>;
|
||||
},
|
||||
|
||||
render : function(){
|
||||
return <div className='homePage page'>
|
||||
return <div className='homePage sitePage'>
|
||||
<Meta name='google-site-verification' content='NwnAQSSJZzAT7N-p5MY6ydQ7Njm67dtbu73ZSyE5Fy4' />
|
||||
{this.renderNavbar()}
|
||||
|
||||
<div className='content'>
|
||||
<SplitPane onDragFinish={this.handleSplitMove} ref='pane'>
|
||||
<Editor value={this.state.text} onChange={this.handleTextChange} ref='editor'/>
|
||||
<BrewRenderer text={this.state.text} />
|
||||
<Editor
|
||||
ref='editor'
|
||||
brew={this.state.brew}
|
||||
onTextChange={this.handleTextChange}
|
||||
renderer={this.state.brew.renderer}
|
||||
showEditButtons={false}
|
||||
/>
|
||||
<BrewRenderer text={this.state.brew.text} style={this.state.brew.style} renderer={this.state.brew.renderer}/>
|
||||
</SplitPane>
|
||||
</div>
|
||||
|
||||
<div className={cx('floatingSaveButton', { show: this.props.welcomeText != this.state.text })} onClick={this.handleSave}>
|
||||
Save current <i className='fa fa-save' />
|
||||
<div className={cx('floatingSaveButton', { show: this.state.welcomeText != this.state.brew.text })} onClick={this.handleSave}>
|
||||
Save current <i className='fas fa-save' />
|
||||
</div>
|
||||
|
||||
<a href='/new' className='floatingNewButton'>
|
||||
Create your own <i className='fa fa-magic' />
|
||||
Create your own <i className='fas fa-magic' />
|
||||
</a>
|
||||
</div>;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
position : absolute;
|
||||
display : block;
|
||||
right : 70px;
|
||||
bottom : 70px;
|
||||
bottom : 50px;
|
||||
z-index : 100;
|
||||
z-index : 5001;
|
||||
padding : 1em;
|
||||
@@ -23,7 +23,7 @@
|
||||
position : absolute;
|
||||
display : block;
|
||||
right : 200px;
|
||||
bottom : 90px;
|
||||
bottom : 70px;
|
||||
z-index : 100;
|
||||
z-index : 5000;
|
||||
padding : 0.8em;
|
||||
@@ -40,4 +40,4 @@
|
||||
right : 350px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ Welcome traveler from an antique land. Please sit and tell us of what you have s
|
||||
### Homebrew D&D made easy
|
||||
The Homebrewery makes the creation and sharing of authentic looking Fifth-Edition homebrews easy. It uses [Markdown](https://help.github.com/articles/markdown-basics/) with a little CSS magic to make your brews come to life.
|
||||
|
||||
**Try it! **Simply edit the text on the left and watch it *update live* on the right.
|
||||
**Try it!** Simply edit the text on the left and watch it *update live* on the right.
|
||||
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ When you create your own homebrew you will be given a *edit url* and a *share ur
|
||||
Anyone with the *share url* will be able to access a read-only version of your homebrew.
|
||||
|
||||
## Helping out
|
||||
Like this tool? Want to buy me a beer? [Head here](https://www.patreon.com/stolksdorf) to help me keep the servers running.
|
||||
Like this tool? Want to buy me a beer? [Head here](https://www.patreon.com/Naturalcrit) to help me keep the servers running.
|
||||
|
||||
This tool will **always** be free, never have ads, and I will never offer any "premium" features or whatever.
|
||||
|
||||
@@ -36,8 +36,10 @@ This tool will **always** be free, never have ads, and I will never offer any "p
|
||||
```
|
||||
```
|
||||
|
||||
## Big things coming in v3.0.0
|
||||
With the next major release of Homebrewery, v3.0.0, this tool *will no longer support raw HTML input for brew code*. All brews made previous to the release of v3.0.0 will still render normally.
|
||||
## V3.0.0 Released!
|
||||
With the latest major update to *The Homebrewery* 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, and going forward, raw HTML will no longer receive debugging support (*but can still be used if you insist*).
|
||||
|
||||
**You can enable V3 via the <span class="fa fa-info-circle" style="text-indent:0"></span> Properties button!**
|
||||
|
||||
## New Things All The Time!
|
||||
What's new in the latest update? Check out the full changelog [here](/changelog)
|
||||
@@ -46,9 +48,9 @@ What's new in the latest update? Check out the full changelog [here](/changelog)
|
||||
Have an idea of how to make The Homebrewery better? Or did you find something that wasn't quite right? Head [here](https://www.reddit.com/r/homebrewery/submit?selftext=true&title=%5BIssue%5D%20Describe%20Your%20Issue%20Here) and let me know!.
|
||||
|
||||
### 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 is 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). This 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 you make on this site, it's your responsibility to ensure you have the proper licenses/rights for any images or resources used.
|
||||
|
||||
### More Resources
|
||||
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/comments/3uwxx9/resources_open_to_the_community/).
|
||||
@@ -96,5 +98,3 @@ If you'd like to credit The Homebrewery in your brew, I'd be flattered! Just ref
|
||||
|
||||
<div class='pageNumber'>2</div>
|
||||
<div class='footnote'>PART 2 | BORING STUFF</div>
|
||||
|
||||
|
||||
|
||||
170
client/homebrew/pages/homePage/welcome_msg_v3.md
Normal file
170
client/homebrew/pages/homePage/welcome_msg_v3.md
Normal file
@@ -0,0 +1,170 @@
|
||||
```css
|
||||
.page #example + table td {
|
||||
border:1px dashed #00000030;
|
||||
}
|
||||
|
||||
.page {
|
||||
padding-bottom : 1.1cm;
|
||||
}
|
||||
```
|
||||
|
||||
# The Homebrewery *V3*
|
||||
Welcome traveler from an antique land. Please sit and tell us of what you have seen. The unheard of monsters, who slither and bite. Tell us of the wondrous items and and artifacts you have found, their mysteries yet to be unlocked. Of the vexing vocations and surprising skills you have seen.
|
||||
|
||||
### Homebrew D&D made easy
|
||||
The Homebrewery makes the creation and sharing of authentic looking Fifth-Edition homebrews easy. It uses [Markdown](https://help.github.com/articles/markdown-basics/) with a little CSS magic to make your brews come to life.
|
||||
|
||||
**Try it!** Simply edit the text on the left and watch it *update live* on the right. Note that not every button is visible on this demo page. Click New {{fas,fa-plus-square}} in the navbar above to start brewing with all the features!
|
||||
|
||||
### Editing and Sharing
|
||||
When you create your own homebrew, you will be given a *edit url* and a *share url*.
|
||||
|
||||
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.
|
||||
|
||||
Anyone with the *share url* will be able to access a read-only version of your homebrew.
|
||||
|
||||
{{note
|
||||
##### PDF Creation
|
||||
PDF Printing works best in Google Chrome. If you are having quality/consistency issues, try using Chrome to print instead.
|
||||
|
||||
After clicking the "Print" item in the navbar a new page will open and a print dialog will pop-up.
|
||||
* Set the **Destination** to "Save as PDF"
|
||||
* Set **Paper Size** to "Letter"
|
||||
* If you are printing on A4 paper, make sure to have the **PRINT → {{far,fa-file}} A4 Pagesize** snippet in your brew
|
||||
* In **Options** make sure "Background Images" is selected.
|
||||
* Hit print and enjoy! You're done!
|
||||
|
||||
If you want to save ink or have a monochrome printer, add the **PRINT → {{fas,fa-tint}} Ink Friendly** snippet to your brew before you print
|
||||
}}
|
||||
|
||||
<img src='https://i.imgur.com/hMna6G0.png' style='position:absolute;bottom:50px;left:120px;width:180px' />
|
||||
|
||||
<div class='pageNumber'>1</div>
|
||||
<div class='footnote'>PART 1 | FANCINESS</div>
|
||||
|
||||
\column
|
||||
|
||||
## New in V3.0.0
|
||||
With the latest major update to *The Homebrewery* 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, and going forward, raw HTML will no longer receive debugging support (*but can still be used if you insist*).
|
||||
|
||||
Much of the syntax and styling has changed in V3. Code in one version may be broken in the other, and updating an older brew to V3 will require more than just a copy and paste. *However*, all brews made prior to the release of v3.0.0 will still render normally, and you may switch between the "Legacy" brew renderer and the newer "V3" renderer via the {{fa,fa-info-circle}} **Properties** button on your brew at any time.
|
||||
|
||||
Scroll down to the next page for a brief summary of the changes and new features available in V3!
|
||||
|
||||
#### New Things All The Time!
|
||||
What's new in the latest update? Check out the full changelog [here](/changelog).
|
||||
|
||||
### Helping out
|
||||
Like this tool? Want to buy me a beer? [Head here](https://www.patreon.com/Naturalcrit) to help me keep the servers running.
|
||||
|
||||
This tool will **always** be free, never have ads, and I will never offer any "premium" features or whatever.
|
||||
|
||||
### Bugs, Issues, Suggestions?
|
||||
Need help getting started or just the right look for your brew? Head to [r/Homebrewery](https://www.reddit.com/r/homebrewery/submit?selftext=true&title=%5BIssue%5D%20Describe%20Your%20Issue%20Here) and let us know!
|
||||
|
||||
Have an idea to make The Homebrewery better? Or did you find something that wasn't quite right? Check out the [GitHub Repo](https://github.com/naturalcrit/homebrewery/) to report technical issues.
|
||||
|
||||
### Legal Junk
|
||||
The Homebrewery is licensed using the [MIT License](https://github.com/naturalcrit/homebrewery/blob/master/license). Which means you are free to use The Homebrewery codebase any way that you want, except for claiming that you made it yourself.
|
||||
|
||||
If you wish to sell or in some way gain profit for what's created on this site, it's your responsibility to ensure you have the proper licenses/rights for any images or resources used.
|
||||
|
||||
#### Crediting Me
|
||||
If you'd like to credit me in your brew, I'd be flattered! Just reference that you made it with The Homebrewery.
|
||||
|
||||
### More Resources
|
||||
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/comments/3uwxx9/resources_open_to_the_community/).
|
||||
|
||||
|
||||
\page
|
||||
|
||||
## Markdown+
|
||||
The Homebrewery aims to make homebrewing as simple as possible, providing a live editor with Markdown syntax that is more human-readable and faster to write with than raw HTML.
|
||||
|
||||
In version 3.0.0, with a goal of adding maximum flexibility without users resorting to complex HTML to accomplish simple tasks, Homebrewery provides an extended verision of Markdown with additional syntax.
|
||||
**You can enable V3 via the {{fa,fa-info-circle}} Properties button!**
|
||||
|
||||
|
||||
### 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:
|
||||
|
||||
#### 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.
|
||||
|
||||
|
||||
#### Block
|
||||
{{purple,#book,text-align:center,background:#aa88aa55
|
||||
My favorite book is Wheel of Time. This block has a class of `purple`, an id of `book`, and centered text with a colored background. The opening and closing brackets are on lines separate from the block contents.
|
||||
}}
|
||||
|
||||
|
||||
#### Injection
|
||||
For any element not inside a span or block, you can *inject* attributes using the same syntax but with single brackets in a single line immediately after the element.
|
||||
|
||||
Inline elements like *italics* {color:#D35400} or images require the injection on the same line.
|
||||
|
||||
Block elements like headers require the injection to start on the line immediately following.
|
||||
|
||||
##### A Purple Header
|
||||
{color:purple,text-align:center}
|
||||
|
||||
\* *this does not currently work for tables yet*
|
||||
|
||||
### Vertical Spacing
|
||||
A blank line can be achieved with a run of one or more `:` alone on a line. More `:`'s will create more space.
|
||||
|
||||
::
|
||||
|
||||
Much nicer than `<br><br><br><br><br>`
|
||||
|
||||
### Definition Lists
|
||||
V3 uses HTML *definition lists* to create "lists" with hanging indents.
|
||||
|
||||
**Senses** :: Here is some text that is long and overflows into a second line, creating a "hanging indent".
|
||||
|
||||
### Column Breaks
|
||||
Column and page breaks with `\column` and `\page`.
|
||||
|
||||
\column
|
||||
|
||||
### Tables
|
||||
Tables now allow column & row spanning between cells. This is included in some updated snippets, but a simplified example is given below.
|
||||
|
||||
A cell can be spanned across columns by grouping multiple pipe `|` characters at the end of a cell.
|
||||
|
||||
Row spanning is achieved by adding a `^` at the end of a cell just before the `|`.
|
||||
|
||||
These can be combined to span a cell across both columns and rows. Cells must have the same colspan if they are to be rowspan'd.
|
||||
|
||||
##### Example
|
||||
| Head A | Spanned Header ||
|
||||
| Head B | Head C | Head D |
|
||||
|:-------|:------:|:------:|
|
||||
| 1A | 1B | 1C |
|
||||
| 2A ^| 2B | 2C |
|
||||
| 3A ^| 3B 3C ||
|
||||
| 4A | 4B 4C^||
|
||||
| 5A ^| 5B | 5C |
|
||||
| 6A | 6B ^| 6C |
|
||||
|
||||
## Images
|
||||
Images must be hosted online somewhere, like [Imgur](https://www.imgur.com). You use the address to that image to reference it in your brew\*. Images can be included using Markdown-style images.
|
||||
|
||||
Using *Curly Injection* you can assign an id, classes, or specific inline CSS properties to the image.
|
||||
|
||||
 {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.*
|
||||
|
||||
## Snippets
|
||||
Homebrewery comes with a series of *code snippets* found at the top of the editor pane that make it easy to create brews as quickly as possible. Just set your cursor where you want the code to appear in the editor pane, choose a snippet, and make the adjustments you need.
|
||||
|
||||
|
||||
## Style Editor Panel
|
||||
|
||||
{{fa,fa-paint-brush}} Technically released prior to v3 but still new to many users, check out the new **Style Editor** located on the right side of the Snippet bar. This editor accepts CSS for styling without requiring `<style>` tags-- anything that would have gone inside style tags before can now be placed here, and snippets that insert CSS styles are now located on that tab.
|
||||
|
||||
|
||||
|
||||
<div class='pageNumber'>2</div>
|
||||
<div class='footnote'>PART 2 | BORING STUFF</div>
|
||||
@@ -1,7 +1,8 @@
|
||||
/*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/
|
||||
require('./newPage.less');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
const cx = require('classnames');
|
||||
const request = require('superagent');
|
||||
|
||||
const Markdown = require('naturalcrit/markdown.js');
|
||||
@@ -16,33 +17,75 @@ const SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
|
||||
const Editor = require('../../editor/editor.jsx');
|
||||
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
||||
|
||||
const BREWKEY = 'homebrewery-new';
|
||||
const STYLEKEY = 'homebrewery-new-style';
|
||||
const METAKEY = 'homebrewery-new-meta';
|
||||
|
||||
const KEY = 'homebrewery-new';
|
||||
|
||||
const NewPage = createClass({
|
||||
getInitialState : function() {
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
metadata : {
|
||||
brew : {
|
||||
text : '',
|
||||
style : undefined,
|
||||
shareId : null,
|
||||
editId : null,
|
||||
createdAt : null,
|
||||
updatedAt : null,
|
||||
gDrive : false,
|
||||
|
||||
title : '',
|
||||
description : '',
|
||||
tags : '',
|
||||
published : false,
|
||||
authors : [],
|
||||
systems : []
|
||||
},
|
||||
|
||||
text : '',
|
||||
isSaving : false,
|
||||
errors : []
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState : function() {
|
||||
return {
|
||||
brew : {
|
||||
text : this.props.brew.text || '',
|
||||
style : this.props.brew.style || undefined,
|
||||
gDrive : false,
|
||||
title : this.props.brew.title || '',
|
||||
description : this.props.brew.description || '',
|
||||
tags : this.props.brew.tags || '',
|
||||
published : false,
|
||||
authors : [],
|
||||
systems : this.props.brew.systems || [],
|
||||
renderer : this.props.brew.renderer || 'legacy'
|
||||
},
|
||||
|
||||
isSaving : false,
|
||||
saveGoogle : (global.account && global.account.googleId ? true : false),
|
||||
errors : null,
|
||||
htmlErrors : Markdown.validate(this.props.brew.text)
|
||||
};
|
||||
},
|
||||
|
||||
componentDidMount : function() {
|
||||
const storage = localStorage.getItem(KEY);
|
||||
if(storage){
|
||||
this.setState({
|
||||
text : storage
|
||||
});
|
||||
const brewStorage = localStorage.getItem(BREWKEY);
|
||||
const styleStorage = localStorage.getItem(STYLEKEY);
|
||||
const metaStorage = JSON.parse(localStorage.getItem(METAKEY));
|
||||
|
||||
const brew = this.state.brew;
|
||||
|
||||
if(!this.state.brew.text || !this.state.brew.style){
|
||||
brew.text = this.state.brew.text || (brewStorage ?? '');
|
||||
brew.style = this.state.brew.style || (styleStorage ?? undefined);
|
||||
// brew.title = metaStorage?.title || this.state.brew.title;
|
||||
// brew.description = metaStorage?.description || this.state.brew.description;
|
||||
brew.renderer = metaStorage?.renderer || this.state.brew.renderer;
|
||||
}
|
||||
|
||||
this.setState((prevState)=>({
|
||||
brew : brew,
|
||||
htmlErrors : Markdown.validate(prevState.brew.text)
|
||||
}));
|
||||
|
||||
document.addEventListener('keydown', this.handleControlKeys);
|
||||
},
|
||||
componentWillUnmount : function() {
|
||||
@@ -65,29 +108,89 @@ const NewPage = createClass({
|
||||
this.refs.editor.update();
|
||||
},
|
||||
|
||||
handleMetadataChange : function(metadata){
|
||||
this.setState({
|
||||
metadata : _.merge({}, this.state.metadata, metadata)
|
||||
});
|
||||
},
|
||||
|
||||
handleTextChange : function(text){
|
||||
this.setState({
|
||||
text : text,
|
||||
errors : Markdown.validate(text)
|
||||
});
|
||||
localStorage.setItem(KEY, text);
|
||||
//If there are errors, run the validator on every change to give quick feedback
|
||||
let htmlErrors = this.state.htmlErrors;
|
||||
if(htmlErrors.length) htmlErrors = Markdown.validate(text);
|
||||
|
||||
this.setState((prevState)=>({
|
||||
brew : _.merge({}, prevState.brew, { text: text }),
|
||||
htmlErrors : htmlErrors
|
||||
}));
|
||||
localStorage.setItem(BREWKEY, text);
|
||||
},
|
||||
|
||||
save : function(){
|
||||
handleStyleChange : function(style){
|
||||
this.setState((prevState)=>({
|
||||
brew : _.merge({}, prevState.brew, { style: style }),
|
||||
}));
|
||||
localStorage.setItem(STYLEKEY, style);
|
||||
},
|
||||
|
||||
handleMetaChange : function(metadata){
|
||||
this.setState((prevState)=>({
|
||||
brew : _.merge({}, prevState.brew, metadata),
|
||||
}));
|
||||
localStorage.setItem(METAKEY, JSON.stringify({
|
||||
// 'title' : this.state.brew.title,
|
||||
// 'description' : this.state.brew.description,
|
||||
'renderer' : this.state.brew.renderer
|
||||
}));
|
||||
},
|
||||
|
||||
clearErrors : function(){
|
||||
this.setState({
|
||||
errors : null,
|
||||
isSaving : false
|
||||
|
||||
});
|
||||
},
|
||||
|
||||
clearErrors : function(){
|
||||
this.setState({
|
||||
errors : null,
|
||||
isSaving : false
|
||||
|
||||
});
|
||||
},
|
||||
|
||||
save : async function(){
|
||||
this.setState({
|
||||
isSaving : true
|
||||
});
|
||||
|
||||
request.post('/api')
|
||||
.send(_.merge({}, this.state.metadata, {
|
||||
text : this.state.text
|
||||
}))
|
||||
console.log('saving new brew');
|
||||
|
||||
let brew = this.state.brew;
|
||||
// Split out CSS to Style if CSS codefence exists
|
||||
if(brew.text.startsWith('```css') && brew.text.indexOf('```\n\n') > 0) {
|
||||
const index = brew.text.indexOf('```\n\n');
|
||||
brew.style = `${brew.style ? `${brew.style}\n` : ''}${brew.text.slice(7, index - 1)}`;
|
||||
brew.text = brew.text.slice(index + 5);
|
||||
};
|
||||
|
||||
brew.pageCount=((brew.renderer=='legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1;
|
||||
|
||||
if(this.state.saveGoogle) {
|
||||
const res = await request
|
||||
.post('/api/newGoogle/')
|
||||
.send(brew)
|
||||
.catch((err)=>{
|
||||
console.log(err.status === 401
|
||||
? 'Not signed in!'
|
||||
: 'Error Creating New Google Brew!');
|
||||
this.setState({ isSaving: false, errors: err });
|
||||
return;
|
||||
});
|
||||
|
||||
brew = res.body;
|
||||
localStorage.removeItem(BREWKEY);
|
||||
localStorage.removeItem(STYLEKEY);
|
||||
localStorage.removeItem(METAKEY);
|
||||
window.location = `/edit/${brew.googleId}${brew.editId}`;
|
||||
} else {
|
||||
request.post('/api')
|
||||
.send(brew)
|
||||
.end((err, res)=>{
|
||||
if(err){
|
||||
this.setState({
|
||||
@@ -96,31 +199,95 @@ const NewPage = createClass({
|
||||
return;
|
||||
}
|
||||
window.onbeforeunload = function(){};
|
||||
const brew = res.body;
|
||||
localStorage.removeItem(KEY);
|
||||
brew = res.body;
|
||||
localStorage.removeItem(BREWKEY);
|
||||
localStorage.removeItem(STYLEKEY);
|
||||
localStorage.removeItem(METAKEY);
|
||||
window.location = `/edit/${brew.editId}`;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
renderSaveButton : function(){
|
||||
if(this.state.errors){
|
||||
let errMsg = '';
|
||||
try {
|
||||
errMsg += `${this.state.errors.toString()}\n\n`;
|
||||
errMsg += `\`\`\`\n${this.state.errors.stack}\n`;
|
||||
errMsg += `${JSON.stringify(this.state.errors.response.error, null, ' ')}\n\`\`\``;
|
||||
console.log(errMsg);
|
||||
} catch (e){}
|
||||
|
||||
if(this.state.errors.status == '401'){
|
||||
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||
Oops!
|
||||
<div className='errorContainer' onClick={this.clearErrors}>
|
||||
You must be signed in to a Google account
|
||||
to save this to<br />Google Drive!<br />
|
||||
<a target='_blank' rel='noopener noreferrer'
|
||||
href={`https://www.naturalcrit.com/login?redirect=${this.state.url}`}>
|
||||
<div className='confirm'>
|
||||
Sign In
|
||||
</div>
|
||||
</a>
|
||||
<div className='deny'>
|
||||
Not Now
|
||||
</div>
|
||||
</div>
|
||||
</Nav.item>;
|
||||
}
|
||||
|
||||
if(this.state.errors.status == '403' && this.state.errors.response.body.errors[0].reason == 'insufficientPermissions'){
|
||||
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||
Oops!
|
||||
<div className='errorContainer' onClick={this.clearErrors}>
|
||||
Looks like your Google credentials have
|
||||
expired! Visit the log in page to sign out
|
||||
and sign back in with Google
|
||||
to save this to Google Drive!
|
||||
<a target='_blank' rel='noopener noreferrer'
|
||||
href={`https://www.naturalcrit.com/login?redirect=${this.state.url}`}>
|
||||
<div className='confirm'>
|
||||
Sign In
|
||||
</div>
|
||||
</a>
|
||||
<div className='deny'>
|
||||
Not Now
|
||||
</div>
|
||||
</div>
|
||||
</Nav.item>;
|
||||
}
|
||||
|
||||
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||
Oops!
|
||||
<div className='errorContainer'>
|
||||
Looks like there was a problem saving. <br />
|
||||
Report the issue <a target='_blank' rel='noopener noreferrer'
|
||||
href={`https://github.com/naturalcrit/homebrewery/issues/new?body=${encodeURIComponent(errMsg)}`}>
|
||||
here
|
||||
</a>.
|
||||
</div>
|
||||
</Nav.item>;
|
||||
}
|
||||
|
||||
if(this.state.isSaving){
|
||||
return <Nav.item icon='fa-spinner fa-spin' className='saveButton'>
|
||||
return <Nav.item icon='fas fa-spinner fa-spin' className='save'>
|
||||
save...
|
||||
</Nav.item>;
|
||||
} else {
|
||||
return <Nav.item icon='fa-save' className='saveButton' onClick={this.save}>
|
||||
return <Nav.item icon='fas fa-save' className='save' onClick={this.save}>
|
||||
save
|
||||
</Nav.item>;
|
||||
}
|
||||
},
|
||||
|
||||
print : function(){
|
||||
localStorage.setItem('print', this.state.text);
|
||||
localStorage.setItem('print', `<style>\n${this.state.brew.style}\n</style>\n\n${this.state.brew.text}`);
|
||||
window.open('/print?dialog=true&local=print', '_blank');
|
||||
},
|
||||
|
||||
renderLocalPrintButton : function(){
|
||||
return <Nav.item color='purple' icon='fa-file-pdf-o' onClick={this.print}>
|
||||
return <Nav.item color='purple' icon='far fa-file-pdf' onClick={this.print}>
|
||||
get PDF
|
||||
</Nav.item>;
|
||||
},
|
||||
@@ -129,7 +296,7 @@ const NewPage = createClass({
|
||||
return <Navbar>
|
||||
|
||||
<Nav.section>
|
||||
<Nav.item className='brewTitle'>{this.state.metadata.title}</Nav.item>
|
||||
<Nav.item className='brewTitle'>{this.state.brew.title}</Nav.item>
|
||||
</Nav.section>
|
||||
|
||||
<Nav.section>
|
||||
@@ -143,18 +310,19 @@ const NewPage = createClass({
|
||||
},
|
||||
|
||||
render : function(){
|
||||
return <div className='newPage page'>
|
||||
return <div className='newPage sitePage'>
|
||||
{this.renderNavbar()}
|
||||
<div className='content'>
|
||||
<SplitPane onDragFinish={this.handleSplitMove} ref='pane'>
|
||||
<Editor
|
||||
ref='editor'
|
||||
value={this.state.text}
|
||||
onChange={this.handleTextChange}
|
||||
metadata={this.state.metadata}
|
||||
onMetadataChange={this.handleMetadataChange}
|
||||
brew={this.state.brew}
|
||||
onTextChange={this.handleTextChange}
|
||||
onStyleChange={this.handleStyleChange}
|
||||
onMetaChange={this.handleMetaChange}
|
||||
renderer={this.state.brew.renderer}
|
||||
/>
|
||||
<BrewRenderer text={this.state.text} errors={this.state.errors} />
|
||||
<BrewRenderer text={this.state.brew.text} style={this.state.brew.style} renderer={this.state.brew.renderer} errors={this.state.htmlErrors}/>
|
||||
</SplitPane>
|
||||
</div>
|
||||
</div>;
|
||||
|
||||
@@ -1,10 +1,82 @@
|
||||
.newPage{
|
||||
|
||||
.saveButton{
|
||||
.navItem.save{
|
||||
background-color: @orange;
|
||||
&:hover{
|
||||
background-color: @green;
|
||||
}
|
||||
&.error{
|
||||
position : relative;
|
||||
background-color : @red;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
.errorContainer{
|
||||
animation-name: glideDown;
|
||||
animation-duration: 0.4s;
|
||||
position : absolute;
|
||||
top : 100%;
|
||||
left : 50%;
|
||||
z-index : 100000;
|
||||
width : 140px;
|
||||
padding : 3px;
|
||||
color : white;
|
||||
background-color : #333;
|
||||
border : 3px solid #444;
|
||||
border-radius : 5px;
|
||||
transform : translate(-50% + 3px, 10px);
|
||||
text-align : center;
|
||||
font-size : 10px;
|
||||
font-weight : 800;
|
||||
text-transform : uppercase;
|
||||
a{
|
||||
color : @teal;
|
||||
}
|
||||
&:before {
|
||||
content: "";
|
||||
width: 0px;
|
||||
height: 0px;
|
||||
position: absolute;
|
||||
border-left: 10px solid transparent;
|
||||
border-right: 10px solid transparent;
|
||||
border-top: 10px solid transparent;
|
||||
border-bottom: 10px solid #444;
|
||||
left: 53px;
|
||||
top: -23px;
|
||||
}
|
||||
&:after {
|
||||
content: "";
|
||||
width: 0px;
|
||||
height: 0px;
|
||||
position: absolute;
|
||||
border-left: 10px solid transparent;
|
||||
border-right: 10px solid transparent;
|
||||
border-top: 10px solid transparent;
|
||||
border-bottom: 10px solid #333;
|
||||
left: 53px;
|
||||
top: -19px;
|
||||
}
|
||||
.deny {
|
||||
width : 48%;
|
||||
margin : 1px;
|
||||
padding : 5px;
|
||||
background-color : #333;
|
||||
display : inline-block;
|
||||
border-left : 1px solid #666;
|
||||
.animate(background-color);
|
||||
&:hover{
|
||||
background-color : red;
|
||||
}
|
||||
}
|
||||
.confirm {
|
||||
width : 48%;
|
||||
margin : 1px;
|
||||
padding : 5px;
|
||||
background-color : #333;
|
||||
display : inline-block;
|
||||
color : white;
|
||||
.animate(background-color);
|
||||
&:hover{
|
||||
background-color : teal;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
require('./printPage.less');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
const cx = require('classnames');
|
||||
const { Meta } = require('vitreum/headtags');
|
||||
const MarkdownLegacy = require('naturalcrit/markdownLegacy.js');
|
||||
const Markdown = require('naturalcrit/markdown.js');
|
||||
|
||||
const PrintPage = createClass({
|
||||
@@ -9,7 +12,9 @@ const PrintPage = createClass({
|
||||
return {
|
||||
query : {},
|
||||
brew : {
|
||||
text : '',
|
||||
text : '',
|
||||
style : '',
|
||||
renderer : 'legacy'
|
||||
}
|
||||
};
|
||||
},
|
||||
@@ -31,17 +36,33 @@ const PrintPage = createClass({
|
||||
},
|
||||
|
||||
renderPages : function(){
|
||||
return _.map(this.state.brewText.split('\\page'), (page, index)=>{
|
||||
return <div
|
||||
className='phb'
|
||||
id={`p${index + 1}`}
|
||||
dangerouslySetInnerHTML={{ __html: Markdown.render(page) }}
|
||||
key={index} />;
|
||||
});
|
||||
if(this.props.brew.renderer == 'legacy') {
|
||||
return _.map(this.state.brewText.split('\\page'), (pageText, index)=>{
|
||||
return <div
|
||||
className='phb page'
|
||||
id={`p${index + 1}`}
|
||||
dangerouslySetInnerHTML={{ __html: MarkdownLegacy.render(pageText) }}
|
||||
key={index} />;
|
||||
});
|
||||
} else {
|
||||
return _.map(this.state.brewText.split(/^\\page$/gm), (pageText, index)=>{
|
||||
pageText += `\n\n \n\\column\n `; //Artificial column break at page end to emulate column-fill:auto (until `wide` is used, when column-fill:balance will reappear)
|
||||
return (
|
||||
<div className='page' id={`p${index + 1}`} key={index} >
|
||||
<div className='columnWrapper' dangerouslySetInnerHTML={{ __html: Markdown.render(pageText) }} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
render : function(){
|
||||
return <div>
|
||||
<Meta name='robots' content='noindex, nofollow' />
|
||||
<link href={`${this.props.brew.renderer == 'legacy' ? '/themes/5ePhbLegacy.style.css' : '/themes/5ePhb.style.css'}`} rel='stylesheet'/>
|
||||
{/* Apply CSS from Style tab */}
|
||||
<div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `<style> ${this.props.brew.style} </style>` }} />
|
||||
{this.renderPages()}
|
||||
</div>;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
require('./sharePage.less');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
const cx = require('classnames');
|
||||
const { Meta } = require('vitreum/headtags');
|
||||
|
||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||
const Navbar = require('../../navbar/navbar.jsx');
|
||||
@@ -19,14 +19,22 @@ const SharePage = createClass({
|
||||
brew : {
|
||||
title : '',
|
||||
text : '',
|
||||
style : '',
|
||||
shareId : null,
|
||||
createdAt : null,
|
||||
updatedAt : null,
|
||||
views : 0
|
||||
views : 0,
|
||||
renderer : ''
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState : function() {
|
||||
return {
|
||||
showDropdown : false
|
||||
};
|
||||
},
|
||||
|
||||
componentDidMount : function() {
|
||||
document.addEventListener('keydown', this.handleControlKeys);
|
||||
},
|
||||
@@ -43,25 +51,59 @@ const SharePage = createClass({
|
||||
}
|
||||
},
|
||||
|
||||
processShareId : function() {
|
||||
return this.props.brew.googleId ?
|
||||
this.props.brew.googleId + this.props.brew.shareId :
|
||||
this.props.brew.shareId;
|
||||
},
|
||||
|
||||
handleDropdown : function(show){
|
||||
this.setState({
|
||||
showDropdown : show
|
||||
});
|
||||
},
|
||||
|
||||
renderDropdown : function(){
|
||||
if(!this.state.showDropdown) return null;
|
||||
|
||||
return <div className='dropdown'>
|
||||
<a href={`/source/${this.processShareId()}`} className='item'>
|
||||
view
|
||||
</a>
|
||||
<a href={`/download/${this.processShareId()}`} className='item'>
|
||||
download
|
||||
</a>
|
||||
<a href={`/new/${this.processShareId()}`} className='item'>
|
||||
clone to new
|
||||
</a>
|
||||
</div>;
|
||||
},
|
||||
|
||||
render : function(){
|
||||
return <div className='sharePage page'>
|
||||
return <div className='sharePage sitePage'>
|
||||
<Meta name='robots' content='noindex, nofollow' />
|
||||
<Navbar>
|
||||
<Nav.section>
|
||||
<Nav.item className='brewTitle'>{this.props.brew.title}</Nav.item>
|
||||
</Nav.section>
|
||||
|
||||
<Nav.section>
|
||||
<PrintLink shareId={this.props.brew.shareId} />
|
||||
<Nav.item href={`/source/${this.props.brew.shareId}`} color='teal' icon='fa-code'>
|
||||
source
|
||||
</Nav.item>
|
||||
{this.props.brew.shareId && <>
|
||||
<PrintLink shareId={this.processShareId()} />
|
||||
<Nav.item icon='fas fa-code' color='red' className='source'
|
||||
onMouseEnter={()=>this.handleDropdown(true)}
|
||||
onMouseLeave={()=>this.handleDropdown(false)}>
|
||||
source
|
||||
{this.renderDropdown()}
|
||||
</Nav.item>
|
||||
</>}
|
||||
<RecentNavItem brew={this.props.brew} storageKey='view' />
|
||||
<Account />
|
||||
</Nav.section>
|
||||
</Navbar>
|
||||
|
||||
<div className='content'>
|
||||
<BrewRenderer text={this.props.brew.text} />
|
||||
<BrewRenderer text={this.props.brew.text} style={this.props.brew.style} renderer={this.props.brew.renderer} />
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
@@ -2,4 +2,49 @@
|
||||
.content{
|
||||
overflow-y : hidden;
|
||||
}
|
||||
.source.navItem{
|
||||
position : relative;
|
||||
.dropdown{
|
||||
position : absolute;
|
||||
top : 28px;
|
||||
left : 0px;
|
||||
z-index : 10000;
|
||||
width : 100%;
|
||||
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{
|
||||
.animate(background-color);
|
||||
position : relative;
|
||||
display : block;
|
||||
width : 100%;
|
||||
vertical-align : middle;
|
||||
padding : 13px 5px;
|
||||
box-sizing : border-box;
|
||||
background-color : #333;
|
||||
color : white;
|
||||
text-decoration : none;
|
||||
border-top : 1px solid #888;
|
||||
&:hover{
|
||||
background-color : @blue;
|
||||
}
|
||||
.title{
|
||||
display : inline-block;
|
||||
overflow : hidden;
|
||||
width : 100%;
|
||||
text-overflow : ellipsis;
|
||||
white-space : nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
require('./brewItem.less');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
@@ -5,6 +6,8 @@ const cx = require('classnames');
|
||||
const moment = require('moment');
|
||||
const request = require('superagent');
|
||||
|
||||
const googleDriveIcon = require('../../../googleDrive.png');
|
||||
|
||||
const BrewItem = createClass({
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
@@ -26,52 +29,109 @@ const BrewItem = createClass({
|
||||
if(!confirm('Are you REALLY sure? You will lose editor access to this document.')) return;
|
||||
}
|
||||
|
||||
request.get(`/api/remove/${this.props.brew.editId}`)
|
||||
.send()
|
||||
.end(function(err, res){
|
||||
location.reload();
|
||||
});
|
||||
if(this.props.brew.googleId) {
|
||||
request.get(`/api/removeGoogle/${this.props.brew.googleId}${this.props.brew.editId}`)
|
||||
.send()
|
||||
.end(function(err, res){
|
||||
location.reload();
|
||||
});
|
||||
} else {
|
||||
request.delete(`/api/${this.props.brew.editId}`)
|
||||
.send()
|
||||
.end(function(err, res){
|
||||
location.reload();
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
renderDeleteBrewLink : function(){
|
||||
if(!this.props.brew.editId) return;
|
||||
|
||||
return <a onClick={this.deleteBrew}>
|
||||
<i className='fa fa-trash' />
|
||||
<i className='fas fa-trash-alt' title='Delete' />
|
||||
</a>;
|
||||
},
|
||||
|
||||
renderEditLink : function(){
|
||||
if(!this.props.brew.editId) return;
|
||||
|
||||
return <a href={`/edit/${this.props.brew.editId}`} target='_blank' rel='noopener noreferrer'>
|
||||
<i className='fa fa-pencil' />
|
||||
let editLink = this.props.brew.editId;
|
||||
if(this.props.brew.googleId) {
|
||||
editLink = this.props.brew.googleId + editLink;
|
||||
}
|
||||
|
||||
return <a href={`/edit/${editLink}`} target='_blank' rel='noopener noreferrer'>
|
||||
<i className='fas fa-pencil-alt' title='Edit' />
|
||||
</a>;
|
||||
},
|
||||
|
||||
renderShareLink : function(){
|
||||
if(!this.props.brew.shareId) return;
|
||||
|
||||
let shareLink = this.props.brew.shareId;
|
||||
if(this.props.brew.googleId) {
|
||||
shareLink = this.props.brew.googleId + shareLink;
|
||||
}
|
||||
|
||||
return <a href={`/share/${shareLink}`} target='_blank' rel='noopener noreferrer'>
|
||||
<i className='fas fa-share-alt' title='Share' />
|
||||
</a>;
|
||||
},
|
||||
|
||||
renderDownloadLink : function(){
|
||||
if(!this.props.brew.shareId) return;
|
||||
|
||||
let shareLink = this.props.brew.shareId;
|
||||
if(this.props.brew.googleId) {
|
||||
shareLink = this.props.brew.googleId + shareLink;
|
||||
}
|
||||
|
||||
return <a href={`/download/${shareLink}`}>
|
||||
<i className='fas fa-download' title='Download' />
|
||||
</a>;
|
||||
},
|
||||
|
||||
renderGoogleDriveIcon : function(){
|
||||
if(!this.props.brew.gDrive) return;
|
||||
|
||||
return <span>
|
||||
<img className='googleDriveIcon' src={googleDriveIcon} alt='googleDriveIcon' />
|
||||
</span>;
|
||||
},
|
||||
|
||||
render : function(){
|
||||
const brew = this.props.brew;
|
||||
return <div className='brewItem'>
|
||||
<h2>{brew.title}</h2>
|
||||
<p className='description' >{brew.description}</p>
|
||||
<hr />
|
||||
const dateFormatString = 'YYYY-MM-DD HH:mm:ss';
|
||||
|
||||
return <div className='brewItem'>
|
||||
<div className='text'>
|
||||
<h2>{brew.title}</h2>
|
||||
<p className='description'>{brew.description}</p>
|
||||
</div>
|
||||
<hr />
|
||||
<div className='info'>
|
||||
<span>
|
||||
<i className='fa fa-user' /> {brew.authors.join(', ')}
|
||||
<span title={`Last viewed: ${moment(brew.lastViewed).local().format(dateFormatString)}`}>
|
||||
<i className='fas fa-eye'/> {brew.views}
|
||||
</span>
|
||||
{brew.pageCount &&
|
||||
<span title={`Page count: ${brew.pageCount}`}>
|
||||
<i className='far fa-file' /> {brew.pageCount}
|
||||
</span>
|
||||
}
|
||||
<span>
|
||||
<i className='fa fa-eye' /> {brew.views}
|
||||
<i className='fas fa-sync-alt' /> {moment(brew.updatedAt).fromNow()}
|
||||
</span>
|
||||
<span>
|
||||
<i className='fa fa-refresh' /> {moment(brew.updatedAt).fromNow()}
|
||||
{this.renderGoogleDriveIcon()}
|
||||
<br />
|
||||
<span title={`Authors:\n${brew.authors.join('\n')}`}>
|
||||
<i className='fas fa-user'/> {brew.authors.join(', ')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className='links'>
|
||||
<a href={`/share/${brew.shareId}`} target='_blank' rel='noopener noreferrer'>
|
||||
<i className='fa fa-share-alt' />
|
||||
</a>
|
||||
{this.renderShareLink()}
|
||||
{this.renderEditLink()}
|
||||
{this.renderDownloadLink()}
|
||||
{this.renderDeleteBrewLink()}
|
||||
</div>
|
||||
</div>;
|
||||
|
||||
@@ -7,24 +7,32 @@
|
||||
box-sizing : border-box;
|
||||
overflow : hidden;
|
||||
width : 48%;
|
||||
min-height : 105px;
|
||||
margin-right : 15px;
|
||||
margin-bottom : 15px;
|
||||
padding : 5px 15px 5px 8px;
|
||||
padding : 5px 15px 2px 8px;
|
||||
padding-right : 15px;
|
||||
border : 1px solid #c9ad6a;
|
||||
border-radius : 5px;
|
||||
-webkit-column-break-inside : avoid;
|
||||
page-break-inside : avoid;
|
||||
break-inside : avoid;
|
||||
h4{
|
||||
margin-bottom : 5px;
|
||||
font-size : 2.2em;
|
||||
.text {
|
||||
min-height : 54px;
|
||||
h4{
|
||||
margin-bottom : 5px;
|
||||
font-size : 2.2em;
|
||||
}
|
||||
}
|
||||
.info{
|
||||
position: initial;
|
||||
bottom: 2px;
|
||||
margin-bottom: 4px;
|
||||
font-family : ScalySans;
|
||||
font-size : 1.2em;
|
||||
&>span{
|
||||
margin-right : 15px;
|
||||
display : float;
|
||||
margin-right : 12px;
|
||||
}
|
||||
}
|
||||
&:hover{
|
||||
@@ -55,6 +63,14 @@
|
||||
&:hover{
|
||||
opacity : 1;
|
||||
}
|
||||
i{
|
||||
cursor : pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.googleDriveIcon {
|
||||
height : 20px;
|
||||
padding : 0px;
|
||||
margin : -5px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
require('./userPage.less');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
const cx = require('classnames');
|
||||
|
||||
const moment = require('moment');
|
||||
|
||||
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 BrewItem = require('./brewItem/brewItem.jsx');
|
||||
|
||||
// const brew = {
|
||||
@@ -22,22 +26,130 @@ const UserPage = createClass({
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
username : '',
|
||||
brews : []
|
||||
brews : [],
|
||||
};
|
||||
},
|
||||
getInitialState : function() {
|
||||
return {
|
||||
sortType : 'alpha',
|
||||
sortDir : 'asc',
|
||||
filterString : ''
|
||||
};
|
||||
},
|
||||
getUsernameWithS : function() {
|
||||
if(this.props.username.endsWith('s'))
|
||||
return `${this.props.username}'`;
|
||||
return `${this.props.username}'s`;
|
||||
},
|
||||
|
||||
renderBrews : function(brews){
|
||||
if(!brews || !brews.length) return <div className='noBrews'>No Brews.</div>;
|
||||
|
||||
const sortedBrews = _.sortBy(brews, (brew)=>{ return brew.title; });
|
||||
const sortedBrews = this.sortBrews(brews);
|
||||
|
||||
return _.map(sortedBrews, (brew, idx)=>{
|
||||
return <BrewItem brew={brew} key={idx}/>;
|
||||
});
|
||||
},
|
||||
|
||||
sortBrewOrder : function(brew){
|
||||
if(!brew.title){brew.title = 'No Title';}
|
||||
const mapping = {
|
||||
'alpha' : _.deburr(brew.title.toLowerCase()),
|
||||
'created' : moment(brew.createdAt).format(),
|
||||
'updated' : moment(brew.updatedAt).format(),
|
||||
'views' : brew.views,
|
||||
'latest' : moment(brew.lastViewed).format()
|
||||
};
|
||||
return mapping[this.state.sortType];
|
||||
},
|
||||
|
||||
sortBrews : function(brews){
|
||||
return _.orderBy(brews, (brew)=>{ return this.sortBrewOrder(brew); }, this.state.sortDir);
|
||||
},
|
||||
|
||||
handleSortOptionChange : function(event){
|
||||
this.setState({
|
||||
sortType : event.target.value
|
||||
});
|
||||
},
|
||||
|
||||
handleSortDirChange : function(event){
|
||||
this.setState({
|
||||
sortDir : `${(this.state.sortDir == 'asc' ? 'desc' : 'asc')}`
|
||||
});
|
||||
},
|
||||
|
||||
renderSortOption : function(sortTitle, sortValue){
|
||||
return <td>
|
||||
<button
|
||||
value={`${sortValue}`}
|
||||
onClick={this.handleSortOptionChange}
|
||||
className={`${(this.state.sortType == sortValue ? 'active' : '')}`}
|
||||
>
|
||||
{`${sortTitle}`}
|
||||
</button>
|
||||
</td>;
|
||||
},
|
||||
|
||||
handleFilterTextChange : function(e){
|
||||
this.setState({
|
||||
filterString : e.target.value
|
||||
});
|
||||
return;
|
||||
},
|
||||
|
||||
renderFilterOption : function(){
|
||||
return <td>
|
||||
<label>
|
||||
<i className='fas fa-search'></i>
|
||||
<input
|
||||
type='search'
|
||||
placeholder='search title/description'
|
||||
onChange={this.handleFilterTextChange}
|
||||
/>
|
||||
</label>
|
||||
</td>;
|
||||
},
|
||||
|
||||
renderSortOptions : function(){
|
||||
return <div className='sort-container'>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<h6>Sort by :</h6>
|
||||
</td>
|
||||
{this.renderSortOption('Title', 'alpha')}
|
||||
{this.renderSortOption('Created Date', 'created')}
|
||||
{this.renderSortOption('Updated Date', 'updated')}
|
||||
{this.renderSortOption('Views', 'views')}
|
||||
{/* {this.renderSortOption('Latest', 'latest')} */}
|
||||
<td>
|
||||
<h6>Direction :</h6>
|
||||
</td>
|
||||
<td>
|
||||
<button
|
||||
onClick={this.handleSortDirChange}
|
||||
className='sortDir'
|
||||
>
|
||||
{`${(this.state.sortDir == 'asc' ? '\u25B2 ASC' : '\u25BC DESC')}`}
|
||||
</button>
|
||||
</td>
|
||||
{this.renderFilterOption()}
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>;
|
||||
},
|
||||
|
||||
getSortedBrews : function(){
|
||||
return _.groupBy(this.props.brews, (brew)=>{
|
||||
const testString = _.deburr(this.state.filterString).toLowerCase();
|
||||
const brewCollection = this.state.filterString ? _.filter(this.props.brews, (brew)=>{
|
||||
return (_.deburr(brew.title).toLowerCase().includes(testString)) ||
|
||||
(_.deburr(brew.description).toLowerCase().includes(testString));
|
||||
}) : this.props.brews;
|
||||
return _.groupBy(brewCollection, (brew)=>{
|
||||
return (brew.published ? 'published' : 'private');
|
||||
});
|
||||
},
|
||||
@@ -45,24 +157,29 @@ const UserPage = createClass({
|
||||
render : function(){
|
||||
const brews = this.getSortedBrews();
|
||||
|
||||
return <div className='userPage page'>
|
||||
return <div className='userPage sitePage'>
|
||||
<link href='/themes/5ePhbLegacy.style.css' rel='stylesheet'/>
|
||||
<Navbar>
|
||||
<Nav.section>
|
||||
<NewBrew />
|
||||
<RecentNavItem />
|
||||
<Account />
|
||||
</Nav.section>
|
||||
</Navbar>
|
||||
|
||||
<div className='content'>
|
||||
<div className='content V3'>
|
||||
<div className='phb'>
|
||||
<div>
|
||||
<h1>{this.props.username}'s brews</h1>
|
||||
{this.renderSortOptions()}
|
||||
<div className='published'>
|
||||
<h1>{this.getUsernameWithS()} published brews</h1>
|
||||
{this.renderBrews(brews.published)}
|
||||
</div>
|
||||
<div>
|
||||
<h1>{this.props.username}'s unpublished brews</h1>
|
||||
{this.renderBrews(brews.private)}
|
||||
</div>
|
||||
{this.props.username == global.account?.username &&
|
||||
<div className='unpublished'>
|
||||
<h1>{this.getUsernameWithS()} unpublished brews</h1>
|
||||
{this.renderBrews(brews.private)}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
|
||||
@@ -30,4 +30,48 @@
|
||||
|
||||
}
|
||||
}
|
||||
.sort-container{
|
||||
font-family : 'Open Sans', sans-serif;
|
||||
position : fixed;
|
||||
top : 35px;
|
||||
left : calc(50vw - 408px);
|
||||
border : 2px solid #58180D;
|
||||
width : 800px;
|
||||
background-color : #EEE5CE;
|
||||
padding : 2px;
|
||||
text-align : center;
|
||||
z-index : 15;
|
||||
h6{
|
||||
text-transform : uppercase;
|
||||
font-family : 'Open Sans', sans-serif;
|
||||
font-size : 11px;
|
||||
font-weight : bold;
|
||||
color : #58180D;
|
||||
}
|
||||
table{
|
||||
margin : 0px;
|
||||
vertical-align : middle;
|
||||
tbody tr{
|
||||
background-color: transparent !important;
|
||||
i{
|
||||
padding-right : 5px
|
||||
}
|
||||
button{
|
||||
background-color : transparent;
|
||||
color : #58180D;
|
||||
font-family : 'Open Sans', sans-serif;
|
||||
font-size : 11px;
|
||||
text-transform : uppercase;
|
||||
font-weight : normal;
|
||||
&.active{
|
||||
font-weight : bold;
|
||||
border : 2px solid #58180D;
|
||||
}
|
||||
&.sortDir{
|
||||
width : 75px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 327 B |
Binary file not shown.
|
Before Width: | Height: | Size: 530 B |
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,21 +1,19 @@
|
||||
module.exports = function(vitreum){
|
||||
module.exports = async(name, title = '', props = {})=>{
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link href="//netdna.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.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=${`/${name}/bundle.css`} rel='stylesheet' />
|
||||
<link rel="icon" href="/assets/homebrew/favicon.ico" type="image/x-icon" />
|
||||
<title>The Homebrewery - NaturalCrit</title>
|
||||
${vitreum.head}
|
||||
<title>${title.length ? `${title} - The Homebrewery`: 'The Homebrewery - NaturalCrit'}</title>
|
||||
</head>
|
||||
<body>
|
||||
<main id="reactRoot">${vitreum.body}</main>
|
||||
<main id="reactRoot">${require(`../build/${name}/ssr.js`)(props)}</main>
|
||||
<script src=${`/${name}/bundle.js`}></script>
|
||||
<script>start_app(${JSON.stringify(props)})</script>
|
||||
</body>
|
||||
${vitreum.js}
|
||||
</html>
|
||||
`;
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"host" : "homebrewery.local.naturalcrit.com:8000",
|
||||
"naturalcrit_url" : "local.naturalcrit.com:8010",
|
||||
"secret" : "secret"
|
||||
}
|
||||
"secret" : "secret",
|
||||
"web_port" : 8000
|
||||
}
|
||||
|
||||
20
freebsd/install.sh
Normal file
20
freebsd/install.sh
Normal file
@@ -0,0 +1,20 @@
|
||||
#!/bin/sh
|
||||
|
||||
pkg install -y git nano node npm mongodb44
|
||||
|
||||
sysrc mongod_enable=YES
|
||||
service mongod start
|
||||
|
||||
cd /usr/local/
|
||||
git clone https://github.com/naturalcrit/homebrewery.git
|
||||
|
||||
cd homebrewery
|
||||
npm install
|
||||
npm audit fix
|
||||
npm run postinstall
|
||||
|
||||
cp freebsd/rc.d/homebrewery /usr/local/etc/rc.d/
|
||||
chmod +x /usr/local/etc/rc.d/homebrewery
|
||||
|
||||
sysrc homebrewery_enable=YES
|
||||
service homebrewery start
|
||||
65
freebsd/rc.d/homebrewery
Normal file
65
freebsd/rc.d/homebrewery
Normal file
@@ -0,0 +1,65 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# PROVIDE: homebrewery
|
||||
# REQUIRE: NETWORKING
|
||||
# KEYWORD: shutdown
|
||||
|
||||
# Author: S Robertson
|
||||
# Version: 1.0.0
|
||||
|
||||
# Description:
|
||||
# This script runs HomeBrewery as a service under the supplied user on boot
|
||||
|
||||
# How to use:
|
||||
# Place this file in /usr/local/etc/rc.d/
|
||||
# Add homebrewery_enable="YES" to /etc/rc.config
|
||||
# (Optional) To run as non-root, add homebrewery_runAs="homebrewery" to /etc/rc.config
|
||||
# (Optional) To pass HomeBrewery args, add homebrewery_args="" to /etc/rc.config
|
||||
|
||||
# Freebsd rc library
|
||||
. /etc/rc.subr
|
||||
|
||||
# General Info
|
||||
name="homebrewery" # Safe name of program
|
||||
location="/usr/local/" # Install location
|
||||
program_name="homebrewery" # Name of exec
|
||||
title="HomeBrewery" # Title to display in top/htop
|
||||
|
||||
# RC.config vars
|
||||
load_rc_config $name # Loading rc config vars
|
||||
: ${homebrewery_enable="NO"} # Default: Do not enable HomeBrewery
|
||||
: ${homebrewery_runAs="root"} # Default: Run HomeBrewery as root
|
||||
: ${homebrewery_port=8000} # Default: Run HomeBrewery on port 8000
|
||||
: ${homebrewery_NODE_ENV="local"} # Default: Run HomeBrewery in local mode
|
||||
|
||||
# Freebsd Setup
|
||||
rcvar=homebrewery_enable # Enables the rc.conf YES/NO flag
|
||||
pidfile="/var/run/${program_name}.pid" # File that allows the system to keep track of HomeBrewery status
|
||||
|
||||
# Env Setup
|
||||
export HOME=$( getent passwd "homebrewery_runAs" | cut -d: -f6 ) # Gets the home directory of the runAs user
|
||||
export NODE_ENV=${homebrewery_NODE_ENV}
|
||||
export PORT=${homebrewery_port}
|
||||
|
||||
# Command Setup
|
||||
exec_cmd="${location}/${program_name}/server.js" # Path to the HomeBrewery server.js, /usr/local/bin/ when installed globally
|
||||
output_file="/var/log/${program_name}.log" # Path to HomeBrewery output file
|
||||
|
||||
# Command
|
||||
command="/usr/sbin/daemon"
|
||||
command_args="-r -t ${title} -u ${homebrewery_runAs} -o ${output_file} -P ${pidfile} /usr/local/bin/node ${exec_cmd} ${homebrewery_args}"
|
||||
|
||||
# Extra Commands
|
||||
extra_commands="dev_mode"
|
||||
|
||||
dev_mode_cmd="homebrewery_dev_mode"
|
||||
|
||||
homebrewery_dev_mode() {
|
||||
echo "Starting HomeBrewery in live rebuild Developer mode..."
|
||||
cd ${location}/${program_name}/
|
||||
/usr/local/bin/node ${location}/${program_name}/scripts/buildHomebrew.js --dev
|
||||
}
|
||||
|
||||
# Loading Config
|
||||
load_rc_config ${name}
|
||||
run_rc_command "$1"
|
||||
20166
package-lock.json
generated
20166
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
67
package.json
67
package.json
@@ -1,7 +1,10 @@
|
||||
{
|
||||
"name": "homebrewery",
|
||||
"description": "Create authentic looking D&D homebrews using only markdown",
|
||||
"version": "2.8.2",
|
||||
"version": "3.0.0",
|
||||
"engines": {
|
||||
"node": "14.15.x"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/naturalcrit/homebrewery.git"
|
||||
@@ -9,7 +12,8 @@
|
||||
"scripts": {
|
||||
"dev": "node scripts/dev.js",
|
||||
"quick": "node scripts/quick.js",
|
||||
"build": "node scripts/build.js",
|
||||
"build": "node scripts/buildHomebrew.js",
|
||||
"buildall": "node scripts/buildHomebrew.js && node scripts/buildAdmin.js",
|
||||
"lint": "eslint --fix **/*.{js,jsx}",
|
||||
"lint:dry": "eslint **/*.{js,jsx}",
|
||||
"circleci": "npm test && eslint **/*.{js,jsx} --max-warnings=0",
|
||||
@@ -18,7 +22,7 @@
|
||||
"test:dev": "pico-check -v -w",
|
||||
"phb": "node scripts/phb.js",
|
||||
"prod": "set NODE_ENV=production && npm run build",
|
||||
"postinstall": "npm run build",
|
||||
"postinstall": "npm run buildall",
|
||||
"start": "node server.js"
|
||||
},
|
||||
"author": "stolksdorf",
|
||||
@@ -31,35 +35,48 @@
|
||||
},
|
||||
"babel": {
|
||||
"presets": [
|
||||
"env",
|
||||
"react"
|
||||
"@babel/preset-env",
|
||||
"@babel/preset-react"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"babel-preset-env": "^1.7.0",
|
||||
"babel-preset-react": "^6.24.1",
|
||||
"@babel/core": "^7.15.0",
|
||||
"@babel/plugin-transform-runtime": "^7.15.0",
|
||||
"@babel/preset-env": "^7.15.4",
|
||||
"@babel/preset-react": "^7.14.5",
|
||||
"body-parser": "^1.19.0",
|
||||
"classnames": "^2.2.6",
|
||||
"codemirror": "^5.52.0",
|
||||
"cookie-parser": "^1.4.4",
|
||||
"create-react-class": "^15.6.3",
|
||||
"classnames": "^2.3.1",
|
||||
"codemirror": "^5.62.3",
|
||||
"cookie-parser": "^1.4.5",
|
||||
"create-react-class": "^15.7.0",
|
||||
"dedent-tabs": "^0.9.0",
|
||||
"express": "^4.17.1",
|
||||
"express-async-handler": "^1.1.4",
|
||||
"express-static-gzip": "2.1.1",
|
||||
"fs-extra": "10.0.0",
|
||||
"googleapis": "85.0.0",
|
||||
"jwt-simple": "^0.5.6",
|
||||
"lodash": "^4.17.15",
|
||||
"marked": "^0.3.19",
|
||||
"moment": "^2.24.0",
|
||||
"mongoose": "^5.9.2",
|
||||
"nconf": "^0.10.0",
|
||||
"pico-router": "^2.1.0",
|
||||
"react": "^16.13.0",
|
||||
"react-dom": "^16.13.0",
|
||||
"shortid": "^2.2.15",
|
||||
"superagent": "^5.2.2",
|
||||
"vitreum": "^4.10.1"
|
||||
"less": "^3.13.1",
|
||||
"lodash": "^4.17.21",
|
||||
"marked": "3.0.3",
|
||||
"markedLegacy": "npm:marked@^0.3.19",
|
||||
"moment": "^2.29.1",
|
||||
"mongoose": "^5.13.7",
|
||||
"nanoid": "3.1.25",
|
||||
"nconf": "^0.11.3",
|
||||
"prop-types": "15.7.2",
|
||||
"query-string": "7.0.1",
|
||||
"react": "^16.14.0",
|
||||
"react-dom": "^16.14.0",
|
||||
"react-frame-component": "4.1.3",
|
||||
"react-router-dom": "5.3.0",
|
||||
"sanitize-filename": "1.6.3",
|
||||
"superagent": "^6.1.0",
|
||||
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^6.8.0",
|
||||
"eslint-plugin-react": "^7.19.0",
|
||||
"pico-check": "^1.3.2"
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-plugin-react": "^7.25.1",
|
||||
"pico-check": "^2.1.3"
|
||||
}
|
||||
}
|
||||
|
||||
4
robots.txt
Normal file
4
robots.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
# Notes
|
||||
User-agent: *
|
||||
Disallow: /edit/
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
const label = 'build';
|
||||
console.time(label);
|
||||
|
||||
const clean = require('vitreum/steps/clean.js');
|
||||
const jsx = require('vitreum/steps/jsx.js');
|
||||
const lib = require('vitreum/steps/libs.js');
|
||||
const less = require('vitreum/steps/less.js');
|
||||
const asset = require('vitreum/steps/assets.js');
|
||||
|
||||
const Proj = require('./project.json');
|
||||
|
||||
clean()
|
||||
.then(lib(Proj.libs))
|
||||
.then(()=>jsx('homebrew', './client/homebrew/homebrew.jsx', { libs: Proj.libs, shared: ['./shared'] }))
|
||||
.then((deps)=>less('homebrew', { shared: ['./shared'] }, deps))
|
||||
.then(()=>jsx('admin', './client/admin/admin.jsx', { libs: Proj.libs, shared: ['./shared'] }))
|
||||
.then((deps)=>less('admin', { shared: ['./shared'] }, deps))
|
||||
.then(()=>asset(Proj.assets, ['./shared', './client']))
|
||||
.then(console.timeEnd.bind(console, label))
|
||||
.catch(console.error);
|
||||
31
scripts/buildAdmin.js
Normal file
31
scripts/buildAdmin.js
Normal file
@@ -0,0 +1,31 @@
|
||||
const fs = require('fs-extra');
|
||||
const Proj = require('./project.json');
|
||||
|
||||
const { pack } = require('vitreum');
|
||||
const isDev = !!process.argv.find((arg)=>arg=='--dev');
|
||||
|
||||
const lessTransform = require('vitreum/transforms/less.js');
|
||||
const assetTransform = require('vitreum/transforms/asset.js');
|
||||
//const Meta = require('vitreum/headtags');
|
||||
|
||||
const transforms = {
|
||||
'.less' : lessTransform,
|
||||
'*' : assetTransform('./build')
|
||||
};
|
||||
|
||||
const build = async ({ bundle, render, ssr })=>{
|
||||
const css = await lessTransform.generate({ paths: './shared' });
|
||||
await fs.outputFile('./build/admin/bundle.css', css);
|
||||
await fs.outputFile('./build/admin/bundle.js', bundle);
|
||||
await fs.outputFile('./build/admin/ssr.js', ssr);
|
||||
};
|
||||
|
||||
fs.emptyDirSync('./build/admin');
|
||||
pack('./client/admin/admin.jsx', {
|
||||
paths : ['./shared'],
|
||||
libs : Proj.libs,
|
||||
dev : isDev && build,
|
||||
transforms
|
||||
})
|
||||
.then(build)
|
||||
.catch(console.error);
|
||||
78
scripts/buildHomebrew.js
Normal file
78
scripts/buildHomebrew.js
Normal file
@@ -0,0 +1,78 @@
|
||||
const fs = require('fs-extra');
|
||||
const zlib = require('zlib');
|
||||
const Proj = require('./project.json');
|
||||
|
||||
const { pack, watchFile, livereload } = require('vitreum');
|
||||
const isDev = !!process.argv.find((arg)=>arg=='--dev');
|
||||
|
||||
const lessTransform = require('vitreum/transforms/less.js');
|
||||
const assetTransform = require('vitreum/transforms/asset.js');
|
||||
const babel = require('@babel/core');
|
||||
const less = require('less');
|
||||
|
||||
const babelify = async (code)=>(await babel.transformAsync(code, { presets: ['@babel/preset-env', '@babel/preset-react'], plugins: ['@babel/plugin-transform-runtime'] })).code;
|
||||
|
||||
const transforms = {
|
||||
'.js' : (code, filename, opts)=>babelify(code),
|
||||
'.jsx' : (code, filename, opts)=>babelify(code),
|
||||
'.less' : lessTransform,
|
||||
'*' : assetTransform('./build')
|
||||
};
|
||||
|
||||
const build = async ({ bundle, render, ssr })=>{
|
||||
const css = await lessTransform.generate({ paths: './shared' });
|
||||
await fs.outputFile('./build/homebrew/bundle.css', css);
|
||||
await fs.outputFile('./build/homebrew/bundle.js', bundle);
|
||||
await fs.outputFile('./build/homebrew/ssr.js', ssr);
|
||||
await fs.copy('./themes/fonts', './build/fonts');
|
||||
let src = './themes/5ePhbLegacy.style.less';
|
||||
//Parse brew theme files
|
||||
less.render(fs.readFileSync(src).toString(), {
|
||||
compress : !isDev
|
||||
}, function(e, output) {
|
||||
fs.outputFile('./build/themes/5ePhbLegacy.style.css', output.css);
|
||||
});
|
||||
src = './themes/5ePhb.style.less';
|
||||
less.render(fs.readFileSync(src).toString(), {
|
||||
compress : !isDev
|
||||
}, function(e, output) {
|
||||
fs.outputFile('./build/themes/5ePhb.style.css', output.css);
|
||||
});
|
||||
// await less.render(lessCode, {
|
||||
// compress : !dev,
|
||||
// sourceMap : (dev ? {
|
||||
// sourceMapFileInline: true,
|
||||
// outputSourceFiles: true
|
||||
// } : false),
|
||||
// })
|
||||
|
||||
//compress files in production
|
||||
if(!isDev){
|
||||
await fs.outputFile('./build/homebrew/bundle.css.br', zlib.brotliCompressSync(css));
|
||||
await fs.outputFile('./build/homebrew/bundle.js.br', zlib.brotliCompressSync(bundle));
|
||||
await fs.outputFile('./build/homebrew/ssr.js.br', zlib.brotliCompressSync(ssr));
|
||||
} else {
|
||||
await fs.remove('./build/homebrew/bundle.css.br');
|
||||
await fs.remove('./build/homebrew/bundle.js.br');
|
||||
await fs.remove('./build/homebrew/ssr.js.br');
|
||||
}
|
||||
};
|
||||
|
||||
fs.emptyDirSync('./build');
|
||||
pack('./client/homebrew/homebrew.jsx', {
|
||||
paths : ['./shared'],
|
||||
libs : Proj.libs,
|
||||
dev : isDev && build,
|
||||
transforms
|
||||
})
|
||||
.then(build)
|
||||
.catch(console.error);
|
||||
|
||||
|
||||
//In development set up a watch server and livereload
|
||||
if(isDev){
|
||||
livereload('./build');
|
||||
watchFile('./server.js', {
|
||||
watch : ['./client'] // Watch additional folders if you want
|
||||
});
|
||||
}
|
||||
@@ -10,10 +10,10 @@
|
||||
"classnames",
|
||||
"codemirror",
|
||||
"codemirror/mode/gfm/gfm.js",
|
||||
"codemirror/mode/css/css.js",
|
||||
"codemirror/mode/javascript/javascript.js",
|
||||
"moment",
|
||||
"superagent",
|
||||
"marked",
|
||||
"pico-router"
|
||||
"marked"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
307
server.js
307
server.js
@@ -1,9 +1,64 @@
|
||||
/*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/
|
||||
const _ = require('lodash');
|
||||
const jwt = require('jwt-simple');
|
||||
const express = require('express');
|
||||
const app = express();
|
||||
|
||||
app.use(express.static(`${__dirname}/build`));
|
||||
const homebrewApi = require('./server/homebrew.api.js');
|
||||
const GoogleActions = require('./server/googleActions.js');
|
||||
const serveCompressedStaticAssets = require('./server/static-assets.mv.js');
|
||||
const sanitizeFilename = require('sanitize-filename');
|
||||
const asyncHandler = require('express-async-handler');
|
||||
|
||||
const brewAccessTypes = ['edit', 'share', 'raw'];
|
||||
|
||||
//Get the brew object from the HB database or Google Drive
|
||||
const getBrewFromId = asyncHandler(async (id, accessType)=>{
|
||||
if(!brewAccessTypes.includes(accessType))
|
||||
throw ('Invalid Access Type when getting brew');
|
||||
let brew;
|
||||
if(id.length > 12) {
|
||||
const googleId = id.slice(0, -12);
|
||||
id = id.slice(-12);
|
||||
brew = await GoogleActions.readFileMetadata(config.get('google_api_key'), googleId, id, accessType);
|
||||
} else {
|
||||
brew = await HomebrewModel.get(accessType == 'edit' ? { editId: id } : { shareId: id });
|
||||
brew = brew.toObject(); // Convert MongoDB object to standard Javascript Object
|
||||
}
|
||||
|
||||
brew = sanitizeBrew(brew, accessType === 'edit' ? false : true);
|
||||
//Split brew.text into text and style
|
||||
//unless the Access Type is RAW, in which case return immediately
|
||||
if(accessType == 'raw') {
|
||||
return brew;
|
||||
}
|
||||
splitTextAndStyle(brew);
|
||||
return brew;
|
||||
});
|
||||
|
||||
const sanitizeBrew = (brew, full=false)=>{
|
||||
delete brew._id;
|
||||
delete brew.__v;
|
||||
if(full){
|
||||
delete brew.editId;
|
||||
}
|
||||
return brew;
|
||||
};
|
||||
|
||||
const splitTextAndStyle = (brew)=>{
|
||||
brew.text = brew.text.replaceAll('\r\n', '\n');
|
||||
if(brew.text.startsWith('```css')) {
|
||||
const index = brew.text.indexOf('```\n\n');
|
||||
brew.style = brew.text.slice(7, index - 1);
|
||||
brew.text = brew.text.slice(index + 5);
|
||||
}
|
||||
};
|
||||
|
||||
app.use('/', serveCompressedStaticAssets(`${__dirname}/build`));
|
||||
|
||||
process.chdir(__dirname);
|
||||
|
||||
//app.use(express.static(`${__dirname}/build`));
|
||||
app.use(require('body-parser').json({ limit: '25mb' }));
|
||||
app.use(require('cookie-parser')());
|
||||
app.use(require('./server/forcessl.mw.js'));
|
||||
@@ -17,127 +72,215 @@ const config = require('nconf')
|
||||
//DB
|
||||
const mongoose = require('mongoose');
|
||||
mongoose.connect(config.get('mongodb_uri') || config.get('mongolab_uri') || 'mongodb://localhost/naturalcrit',
|
||||
{ retryWrites: false, useNewUrlParser: true });
|
||||
{ retryWrites: false, useNewUrlParser: true, useCreateIndex: true, useUnifiedTopology: true });
|
||||
mongoose.connection.on('error', ()=>{
|
||||
console.log('Error : Could not connect to a Mongo Database.');
|
||||
console.log(' If you are running locally, make sure mongodb.exe is running.');
|
||||
throw 'Can not connect to Mongo';
|
||||
});
|
||||
|
||||
|
||||
//Account Middleware
|
||||
app.use((req, res, next)=>{
|
||||
if(req.cookies && req.cookies.nc_session){
|
||||
try {
|
||||
req.account = jwt.decode(req.cookies.nc_session, config.get('secret'));
|
||||
//console.log("Just loaded up JWT from cookie:");
|
||||
//console.log(req.account);
|
||||
} catch (e){}
|
||||
}
|
||||
|
||||
req.config = {
|
||||
google_client_id : config.get('google_client_id'),
|
||||
google_client_secret : config.get('google_client_secret')
|
||||
};
|
||||
return next();
|
||||
});
|
||||
|
||||
|
||||
app.use(require('./server/homebrew.api.js'));
|
||||
app.use(homebrewApi);
|
||||
app.use(require('./server/admin.api.js'));
|
||||
|
||||
const HomebrewModel = require('./server/homebrew.model.js').model;
|
||||
const welcomeText = require('fs').readFileSync('./client/homebrew/pages/homePage/welcome_msg.md', 'utf8');
|
||||
const welcomeTextV3 = require('fs').readFileSync('./client/homebrew/pages/homePage/welcome_msg_v3.md', 'utf8');
|
||||
const changelogText = require('fs').readFileSync('./changelog.md', 'utf8');
|
||||
|
||||
const HomebrewModel = require('./server/homebrew.model.js').model;
|
||||
const welcomeText = require('fs').readFileSync('./client/homebrew/pages/homePage/welcome_msg.md', 'utf8');
|
||||
const changelogText = require('fs').readFileSync('./changelog.md', 'utf8');
|
||||
String.prototype.replaceAll = function(s, r){return this.split(s).join(r);};
|
||||
|
||||
//Robots.txt
|
||||
app.get('/robots.txt', (req, res)=>{
|
||||
return res.sendFile(`${__dirname}/robots.txt`);
|
||||
});
|
||||
|
||||
//Home page
|
||||
app.get('/', async (req, res, next)=>{
|
||||
const brew = {
|
||||
text : welcomeText
|
||||
};
|
||||
req.brew = brew;
|
||||
return next();
|
||||
});
|
||||
|
||||
//Home page v3
|
||||
app.get('/v3_preview', async (req, res, next)=>{
|
||||
const brew = {
|
||||
text : welcomeTextV3,
|
||||
renderer : 'V3'
|
||||
};
|
||||
splitTextAndStyle(brew);
|
||||
req.brew = brew;
|
||||
return next();
|
||||
});
|
||||
|
||||
//Changelog page
|
||||
app.get('/changelog', async (req, res, next)=>{
|
||||
const brew = {
|
||||
title : 'Changelog',
|
||||
text : changelogText,
|
||||
renderer : 'V3'
|
||||
};
|
||||
splitTextAndStyle(brew);
|
||||
req.brew = brew;
|
||||
return next();
|
||||
});
|
||||
|
||||
//Source page
|
||||
String.prototype.replaceAll = function(s, r){return this.split(s).join(r);};
|
||||
app.get('/source/:id', (req, res)=>{
|
||||
HomebrewModel.get({ shareId: req.params.id })
|
||||
.then((brew)=>{
|
||||
const text = brew.text.replaceAll('<', '<').replaceAll('>', '>');
|
||||
return res.send(`<code><pre style="white-space: pre-wrap;">${text}</pre></code>`);
|
||||
})
|
||||
app.get('/source/:id', asyncHandler(async (req, res)=>{
|
||||
const brew = await getBrewFromId(req.params.id, 'raw');
|
||||
|
||||
const replaceStrings = { '&': '&', '<': '<', '>': '>' };
|
||||
let text = brew.text;
|
||||
for (const replaceStr in replaceStrings) {
|
||||
text = text.replaceAll(replaceStr, replaceStrings[replaceStr]);
|
||||
}
|
||||
text = `<code><pre style="white-space: pre-wrap;">${text}</pre></code>`;
|
||||
res.status(200).send(text);
|
||||
}));
|
||||
|
||||
//Download brew source page
|
||||
app.get('/download/:id', asyncHandler(async (req, res)=>{
|
||||
const brew = await getBrewFromId(req.params.id, 'raw');
|
||||
const prefix = 'HB - ';
|
||||
|
||||
let fileName = sanitizeFilename(`${prefix}${brew.title}`).replaceAll(' ', '');
|
||||
if(!fileName || !fileName.length) { fileName = `${prefix}-Untitled-Brew`; };
|
||||
res.set({
|
||||
'Cache-Control' : 'no-cache',
|
||||
'Content-Type' : 'text/plain',
|
||||
'Content-Disposition' : `attachment; filename="${fileName}.txt"`
|
||||
});
|
||||
res.status(200).send(brew.text);
|
||||
}));
|
||||
|
||||
//User Page
|
||||
app.get('/user/:username', async (req, res, next)=>{
|
||||
const ownAccount = req.account && (req.account.username == req.params.username);
|
||||
|
||||
let brews = await HomebrewModel.getByUser(req.params.username, ownAccount)
|
||||
.catch((err)=>{
|
||||
console.log(err);
|
||||
});
|
||||
|
||||
if(ownAccount && req?.account?.googleId){
|
||||
const googleBrews = await GoogleActions.listGoogleBrews(req, res)
|
||||
.catch((err)=>{
|
||||
console.log(err);
|
||||
return res.status(404).send('Could not find Homebrew with that id');
|
||||
console.error(err);
|
||||
});
|
||||
|
||||
if(googleBrews)
|
||||
brews = _.concat(brews, googleBrews);
|
||||
}
|
||||
|
||||
req.brews = _.map(brews, (brew)=>{
|
||||
return sanitizeBrew(brew, !ownAccount);
|
||||
});
|
||||
|
||||
return next();
|
||||
});
|
||||
|
||||
//Edit Page
|
||||
app.get('/edit/:id', asyncHandler(async (req, res, next)=>{
|
||||
res.header('Cache-Control', 'no-cache, no-store'); //reload the latest saved brew when pressing back button, not the cached version before save.
|
||||
const brew = await getBrewFromId(req.params.id, 'edit');
|
||||
req.brew = brew;
|
||||
return next();
|
||||
}));
|
||||
|
||||
app.get('/user/:username', (req, res, next)=>{
|
||||
const fullAccess = req.account && (req.account.username == req.params.username);
|
||||
HomebrewModel.getByUser(req.params.username, fullAccess)
|
||||
.then((brews)=>{
|
||||
req.brews = brews;
|
||||
return next();
|
||||
})
|
||||
.catch((err)=>{
|
||||
console.log(err);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
app.get('/edit/:id', (req, res, next)=>{
|
||||
HomebrewModel.get({ editId: req.params.id })
|
||||
.then((brew)=>{
|
||||
req.brew = brew.sanatize();
|
||||
return next();
|
||||
})
|
||||
.catch((err)=>{
|
||||
console.log(err);
|
||||
return res.status(400).send(`Can't get that`);
|
||||
});
|
||||
});
|
||||
//New Page
|
||||
app.get('/new/:id', asyncHandler(async (req, res, next)=>{
|
||||
const brew = await getBrewFromId(req.params.id, 'share');
|
||||
brew.title = `CLONE - ${brew.title}`;
|
||||
req.brew = brew;
|
||||
return next();
|
||||
}));
|
||||
|
||||
//Share Page
|
||||
app.get('/share/:id', (req, res, next)=>{
|
||||
HomebrewModel.get({ shareId: req.params.id })
|
||||
.then((brew)=>{
|
||||
return brew.increaseView();
|
||||
})
|
||||
.then((brew)=>{
|
||||
req.brew = brew.sanatize(true);
|
||||
return next();
|
||||
})
|
||||
.catch((err)=>{
|
||||
console.log(err);
|
||||
return res.status(400).send(`Can't get that`);
|
||||
});
|
||||
});
|
||||
app.get('/share/:id', asyncHandler(async (req, res, next)=>{
|
||||
const brew = await getBrewFromId(req.params.id, 'share');
|
||||
|
||||
if(req.params.id.length > 12) {
|
||||
const googleId = req.params.id.slice(0, -12);
|
||||
const shareId = req.params.id.slice(-12);
|
||||
await GoogleActions.increaseView(googleId, shareId, 'share', brew)
|
||||
.catch((err)=>{next(err);});
|
||||
} else {
|
||||
await HomebrewModel.increaseView({ shareId: brew.shareId });
|
||||
}
|
||||
|
||||
req.brew = brew;
|
||||
return next();
|
||||
}));
|
||||
|
||||
//Print Page
|
||||
app.get('/print/:id', (req, res, next)=>{
|
||||
HomebrewModel.get({ shareId: req.params.id })
|
||||
.then((brew)=>{
|
||||
req.brew = brew.sanatize(true);
|
||||
return next();
|
||||
})
|
||||
.catch((err)=>{
|
||||
console.log(err);
|
||||
return res.status(400).send(`Can't get that`);
|
||||
});
|
||||
});
|
||||
app.get('/print/:id', asyncHandler(async (req, res, next)=>{
|
||||
const brew = await getBrewFromId(req.params.id, 'share');
|
||||
req.brew = brew;
|
||||
return next();
|
||||
}));
|
||||
|
||||
|
||||
//Render Page
|
||||
const render = require('vitreum/steps/render');
|
||||
//Render the page
|
||||
const templateFn = require('./client/template.js');
|
||||
app.use((req, res)=>{
|
||||
render('homebrew', templateFn, {
|
||||
const props = {
|
||||
version : require('./package.json').version,
|
||||
url : req.originalUrl,
|
||||
welcomeText : welcomeText,
|
||||
changelog : changelogText,
|
||||
brew : req.brew,
|
||||
brews : req.brews,
|
||||
account : req.account
|
||||
})
|
||||
.then((page)=>{
|
||||
return res.send(page);
|
||||
})
|
||||
.catch((err)=>{
|
||||
console.log(err);
|
||||
return res.sendStatus(500);
|
||||
});
|
||||
googleBrews : req.googleBrews,
|
||||
account : req.account,
|
||||
enable_v3 : config.get('enable_v3')
|
||||
};
|
||||
templateFn('homebrew', title = req.brew ? req.brew.title : '', props)
|
||||
.then((page)=>{ res.send(page); })
|
||||
.catch((err)=>{
|
||||
console.log(err);
|
||||
return res.sendStatus(500);
|
||||
});
|
||||
});
|
||||
|
||||
//v=====----- Error-Handling Middleware -----=====v//
|
||||
//Format Errors so all fields will be sent
|
||||
const replaceErrors = (key, value)=>{
|
||||
if(value instanceof Error) {
|
||||
const error = {};
|
||||
Object.getOwnPropertyNames(value).forEach(function (key) {
|
||||
error[key] = value[key];
|
||||
});
|
||||
return error;
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
const PORT = process.env.PORT || 8000;
|
||||
const getPureError = (error)=>{
|
||||
return JSON.parse(JSON.stringify(error, replaceErrors));
|
||||
};
|
||||
|
||||
app.use((err, req, res, next)=>{
|
||||
const status = err.status || 500;
|
||||
console.error(err);
|
||||
res.status(status).send(getPureError(err));
|
||||
});
|
||||
//^=====--------------------------------------=====^//
|
||||
|
||||
const PORT = process.env.PORT || config.get('web_port') || 8000;
|
||||
app.listen(PORT);
|
||||
console.log(`server on port:${PORT}`);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const HomebrewModel = require('./homebrew.model.js').model;
|
||||
const router = require('express').Router();
|
||||
const Moment = require('moment');
|
||||
const render = require('vitreum/steps/render');
|
||||
//const render = require('vitreum/steps/render');
|
||||
const templateFn = require('../client/template.js');
|
||||
const zlib = require('zlib');
|
||||
|
||||
@@ -37,7 +37,7 @@ const junkBrewQuery = HomebrewModel.find({
|
||||
|
||||
/* Search for brews that aren't compressed (missing the compressed text field) */
|
||||
const uncompressedBrewQuery = HomebrewModel.find({
|
||||
'textBin' : null
|
||||
'text' : { '$exists': true }
|
||||
}).lean().limit(10000).select('_id');
|
||||
|
||||
router.get('/admin/cleanup', mw.adminOnly, (req, res)=>{
|
||||
@@ -100,7 +100,7 @@ router.get('/admin/stats', mw.adminOnly, (req, res)=>{
|
||||
});
|
||||
|
||||
router.get('/admin', mw.adminOnly, (req, res)=>{
|
||||
render('admin', templateFn, {
|
||||
templateFn('admin', {
|
||||
url : req.originalUrl
|
||||
})
|
||||
.then((page)=>res.send(page))
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
module.exports = (req, res, next)=>{
|
||||
if(process.env.NODE_ENV === 'local') return next();
|
||||
if(process.env.NODE_ENV === 'local' || process.env.NODE_ENV === 'docker') return next();
|
||||
if(req.header('x-forwarded-proto') !== 'https') {
|
||||
return res.redirect(302, `https://${req.get('Host')}${req.url}`);
|
||||
}
|
||||
|
||||
392
server/googleActions.js
Normal file
392
server/googleActions.js
Normal file
@@ -0,0 +1,392 @@
|
||||
/* eslint-disable max-lines */
|
||||
const _ = require('lodash');
|
||||
const { google } = require('googleapis');
|
||||
const { nanoid } = require('nanoid');
|
||||
const token = require('./token.js');
|
||||
const config = require('nconf')
|
||||
.argv()
|
||||
.env({ lowerCase: true }) // Load environment variables
|
||||
.file('environment', { file: `config/${process.env.NODE_ENV}.json` })
|
||||
.file('defaults', { file: 'config/default.json' });
|
||||
|
||||
//let oAuth2Client;
|
||||
|
||||
GoogleActions = {
|
||||
|
||||
authCheck : (account, res)=>{
|
||||
if(!account || !account.googleId){ // If not signed into Google
|
||||
const err = new Error('Not Signed In');
|
||||
err.status = 401;
|
||||
throw (err);
|
||||
}
|
||||
|
||||
const oAuth2Client = new google.auth.OAuth2(
|
||||
config.get('google_client_id'),
|
||||
config.get('google_client_secret'),
|
||||
'/auth/google/redirect'
|
||||
);
|
||||
|
||||
oAuth2Client.setCredentials({
|
||||
access_token : account.googleAccessToken, //Comment out to refresh token
|
||||
refresh_token : account.googleRefreshToken
|
||||
});
|
||||
|
||||
oAuth2Client.on('tokens', (tokens)=>{
|
||||
if(tokens.refresh_token) {
|
||||
account.googleRefreshToken = tokens.refresh_token;
|
||||
}
|
||||
account.googleAccessToken = tokens.access_token;
|
||||
const JWTToken = token.generateAccessToken(account);
|
||||
|
||||
//Save updated token to cookie
|
||||
//res.cookie('nc_session', JWTToken, { maxAge: 1000*60*60*24*365, path: '/', sameSite: 'lax' });
|
||||
res.cookie('nc_session', JWTToken, { maxAge: 1000*60*60*24*365, path: '/', sameSite: 'lax', domain: '.naturalcrit.com' });
|
||||
});
|
||||
|
||||
return oAuth2Client;
|
||||
},
|
||||
|
||||
getGoogleFolder : async (auth)=>{
|
||||
const drive = google.drive({ version: 'v3', auth: auth });
|
||||
|
||||
fileMetadata = {
|
||||
'name' : 'Homebrewery',
|
||||
'mimeType' : 'application/vnd.google-apps.folder'
|
||||
};
|
||||
|
||||
const obj = await drive.files.list({
|
||||
q : 'mimeType = \'application/vnd.google-apps.folder\''
|
||||
})
|
||||
.catch((err)=>{
|
||||
console.log('Error searching Google Drive Folders');
|
||||
console.error(err);
|
||||
throw (err);
|
||||
});
|
||||
|
||||
let folderId;
|
||||
|
||||
if(obj.data.files.length == 0){
|
||||
const obj = await drive.files.create({
|
||||
resource : fileMetadata
|
||||
})
|
||||
.catch((err)=>{
|
||||
console.log('Error creating Google Drive folder');
|
||||
console.error(err);
|
||||
throw (err);
|
||||
});
|
||||
|
||||
folderId = obj.data.id;
|
||||
} else {
|
||||
folderId = obj.data.files[0].id;
|
||||
}
|
||||
|
||||
return folderId;
|
||||
},
|
||||
|
||||
listGoogleBrews : async (req, res)=>{
|
||||
|
||||
oAuth2Client = GoogleActions.authCheck(req.account, res);
|
||||
|
||||
//TODO: Change to service account to allow non-owners to view published files.
|
||||
// Requires a driveId parameter in the drive.files.list command
|
||||
// const keys = JSON.parse(config.get('service_account'));
|
||||
// const auth = google.auth.fromJSON(keys);
|
||||
// auth.scopes = ['https://www.googleapis.com/auth/drive'];
|
||||
|
||||
const drive = google.drive({ version: 'v3', auth: oAuth2Client });
|
||||
|
||||
const obj = await drive.files.list({
|
||||
pageSize : 100,
|
||||
fields : 'nextPageToken, files(id, name, description, createdTime, modifiedTime, properties)',
|
||||
q : 'mimeType != \'application/vnd.google-apps.folder\' and trashed = false'
|
||||
})
|
||||
.catch((err)=>{
|
||||
console.log(`Error Listing Google Brews`);
|
||||
console.error(err);
|
||||
throw (err);
|
||||
//TODO: Should break out here, but continues on for some reason.
|
||||
});
|
||||
|
||||
if(!obj.data.files.length) {
|
||||
console.log('No files found.');
|
||||
}
|
||||
|
||||
const brews = obj.data.files.map((file)=>{
|
||||
return {
|
||||
text : '',
|
||||
shareId : file.properties.shareId,
|
||||
editId : file.properties.editId,
|
||||
createdAt : file.createdTime,
|
||||
updatedAt : file.modifiedTime,
|
||||
gDrive : true,
|
||||
googleId : file.id,
|
||||
pageCount : file.properties.pageCount,
|
||||
title : file.properties.title,
|
||||
description : file.description,
|
||||
views : file.properties.views,
|
||||
tags : '',
|
||||
published : file.properties.published ? file.properties.published == 'true' : false,
|
||||
authors : [req.account.username], //TODO: properly save and load authors to google drive
|
||||
systems : []
|
||||
};
|
||||
});
|
||||
return brews;
|
||||
},
|
||||
|
||||
existsGoogleBrew : async (auth, id)=>{
|
||||
const drive = google.drive({ version: 'v3', auth: auth });
|
||||
|
||||
const result = await drive.files.get({ fileId: id })
|
||||
.catch((err)=>{
|
||||
console.log('error checking file exists...');
|
||||
console.error(err);
|
||||
return false;
|
||||
});
|
||||
|
||||
if(result){return true;}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
updateGoogleBrew : async (auth, brew)=>{
|
||||
const drive = google.drive({ version: 'v3', auth: auth });
|
||||
|
||||
if(await GoogleActions.existsGoogleBrew(auth, brew.googleId) == true) {
|
||||
await drive.files.update({
|
||||
fileId : brew.googleId,
|
||||
resource : {
|
||||
name : `${brew.title}.txt`,
|
||||
description : `${brew.description}`,
|
||||
properties : {
|
||||
title : brew.title,
|
||||
published : brew.published,
|
||||
version : brew.version,
|
||||
renderer : brew.renderer,
|
||||
tags : brew.tags,
|
||||
pageCount : brew.pageCount,
|
||||
systems : brew.systems.join()
|
||||
}
|
||||
},
|
||||
media : {
|
||||
mimeType : 'text/plain',
|
||||
body : brew.text
|
||||
}
|
||||
})
|
||||
.catch((err)=>{
|
||||
console.log('Error saving to google');
|
||||
console.error(err);
|
||||
throw (err);
|
||||
//return res.status(500).send('Error while saving');
|
||||
});
|
||||
}
|
||||
|
||||
return (brew);
|
||||
},
|
||||
|
||||
newGoogleBrew : async (auth, brew)=>{
|
||||
const drive = google.drive({ version: 'v3', auth: auth });
|
||||
|
||||
const media = {
|
||||
mimeType : 'text/plain',
|
||||
body : brew.text
|
||||
};
|
||||
|
||||
const folderId = await GoogleActions.getGoogleFolder(auth);
|
||||
|
||||
const fileMetadata = {
|
||||
'name' : `${brew.title}.txt`,
|
||||
'description' : `${brew.description}`,
|
||||
'parents' : [folderId],
|
||||
'properties' : { //AppProperties is not accessible
|
||||
'shareId' : nanoid(12),
|
||||
'editId' : nanoid(12),
|
||||
'title' : brew.title,
|
||||
'views' : '0',
|
||||
'pageCount' : brew.pageCount,
|
||||
'renderer' : brew.renderer || 'legacy'
|
||||
}
|
||||
};
|
||||
|
||||
const obj = await drive.files.create({
|
||||
resource : fileMetadata,
|
||||
media : media
|
||||
})
|
||||
.catch((err)=>{
|
||||
console.log('Error while creating new Google brew');
|
||||
console.error(err);
|
||||
throw (err);
|
||||
});
|
||||
|
||||
if(!obj) return;
|
||||
|
||||
await drive.permissions.create({
|
||||
resource : { type : 'anyone',
|
||||
role : 'writer' },
|
||||
fileId : obj.data.id,
|
||||
fields : 'id',
|
||||
})
|
||||
.catch((err)=>{
|
||||
console.log('Error updating permissions');
|
||||
console.error(err);
|
||||
});
|
||||
|
||||
const newHomebrew = {
|
||||
text : brew.text,
|
||||
shareId : fileMetadata.properties.shareId,
|
||||
editId : fileMetadata.properties.editId,
|
||||
createdAt : new Date(),
|
||||
updatedAt : new Date(),
|
||||
gDrive : true,
|
||||
googleId : obj.data.id,
|
||||
pageCount : fileMetadata.properties.pageCount,
|
||||
|
||||
title : brew.title,
|
||||
description : brew.description,
|
||||
tags : '',
|
||||
published : brew.published,
|
||||
renderer : brew.renderer,
|
||||
authors : [],
|
||||
systems : []
|
||||
};
|
||||
|
||||
return newHomebrew;
|
||||
},
|
||||
|
||||
readFileMetadata : async (auth, id, accessId, accessType)=>{
|
||||
|
||||
const drive = google.drive({ version: 'v3', auth: auth });
|
||||
|
||||
const obj = await drive.files.get({
|
||||
fileId : id,
|
||||
fields : 'properties, createdTime, modifiedTime, description, trashed'
|
||||
})
|
||||
.catch((err)=>{
|
||||
console.log('Error loading from Google');
|
||||
throw (err);
|
||||
return;
|
||||
});
|
||||
|
||||
if(obj) {
|
||||
if(accessType == 'edit' && obj.data.properties.editId != accessId){
|
||||
throw ('Edit ID does not match');
|
||||
} else if(accessType == 'share' && obj.data.properties.shareId != accessId){
|
||||
throw ('Share ID does not match');
|
||||
}
|
||||
|
||||
//Access file using service account. Using API key only causes "automated query" lockouts after a while.
|
||||
|
||||
const keys = typeof(config.get('service_account')) == 'string' ?
|
||||
JSON.parse(config.get('service_account')) :
|
||||
config.get('service_account');
|
||||
|
||||
const serviceAuth = google.auth.fromJSON(keys);
|
||||
serviceAuth.scopes = ['https://www.googleapis.com/auth/drive'];
|
||||
|
||||
const serviceDrive = google.drive({ version: 'v3', auth: serviceAuth });
|
||||
|
||||
const file = await serviceDrive.files.get({
|
||||
fileId : id,
|
||||
fields : 'description, properties',
|
||||
alt : 'media'
|
||||
})
|
||||
.catch((err)=>{
|
||||
console.log('Error getting file contents from Google');
|
||||
console.error(err);
|
||||
});
|
||||
|
||||
const brew = {
|
||||
shareId : obj.data.properties.shareId,
|
||||
editId : obj.data.properties.editId,
|
||||
title : obj.data.properties.title,
|
||||
text : file.data,
|
||||
|
||||
description : obj.data.description,
|
||||
tags : obj.data.properties.tags ? obj.data.properties.tags : '',
|
||||
systems : obj.data.properties.systems ? obj.data.properties.systems.split(',') : [],
|
||||
authors : [],
|
||||
published : obj.data.properties.published ? obj.data.properties.published == 'true' : false,
|
||||
trashed : obj.data.trashed,
|
||||
|
||||
createdAt : obj.data.createdTime,
|
||||
updatedAt : obj.data.modifiedTime,
|
||||
lastViewed : obj.data.properties.lastViewed,
|
||||
pageCount : obj.data.properties.pageCount,
|
||||
views : parseInt(obj.data.properties.views) || 0, //brews with no view parameter will return undefined
|
||||
version : parseInt(obj.data.properties.version) || 0,
|
||||
renderer : obj.data.properties.renderer ? obj.data.properties.renderer : 'legacy',
|
||||
|
||||
gDrive : true,
|
||||
googleId : id
|
||||
};
|
||||
|
||||
return (brew);
|
||||
}
|
||||
},
|
||||
|
||||
deleteGoogleBrew : async (req, res, id)=>{
|
||||
|
||||
oAuth2Client = GoogleActions.authCheck(req.account, res);
|
||||
const drive = google.drive({ version: 'v3', auth: oAuth2Client });
|
||||
|
||||
const googleId = id.slice(0, -12);
|
||||
const accessId = id.slice(-12);
|
||||
|
||||
const obj = await drive.files.get({
|
||||
fileId : googleId,
|
||||
fields : 'properties'
|
||||
})
|
||||
.catch((err)=>{
|
||||
console.log('Error loading from Google');
|
||||
console.error(err);
|
||||
return;
|
||||
});
|
||||
|
||||
if(obj && obj.data.properties.editId != accessId) {
|
||||
throw ('Not authorized to delete this Google brew');
|
||||
}
|
||||
|
||||
await drive.files.update({
|
||||
fileId : googleId,
|
||||
resource : { trashed: true }
|
||||
})
|
||||
.catch((err)=>{
|
||||
console.log('Can\'t delete Google file');
|
||||
console.error(err);
|
||||
});
|
||||
|
||||
return res.status(200).send();
|
||||
},
|
||||
|
||||
increaseView : async (id, accessId, accessType, brew)=>{
|
||||
//service account because this is modifying another user's file properties
|
||||
//so we need extended scope
|
||||
const keys = typeof(config.get('service_account')) == 'string' ?
|
||||
JSON.parse(config.get('service_account')) :
|
||||
config.get('service_account');
|
||||
|
||||
const auth = google.auth.fromJSON(keys);
|
||||
auth.scopes = ['https://www.googleapis.com/auth/drive'];
|
||||
|
||||
const drive = google.drive({ version: 'v3', auth: auth });
|
||||
|
||||
await drive.files.update({
|
||||
fileId : brew.googleId,
|
||||
resource : {
|
||||
modifiedTime : brew.updatedAt,
|
||||
properties : {
|
||||
views : brew.views + 1,
|
||||
lastViewed : new Date()
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((err)=>{
|
||||
console.log('Error updating Google views');
|
||||
console.error(err);
|
||||
//return res.status(500).send('Error while saving');
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = GoogleActions;
|
||||
@@ -2,62 +2,90 @@ const _ = require('lodash');
|
||||
const HomebrewModel = require('./homebrew.model.js').model;
|
||||
const router = require('express').Router();
|
||||
const zlib = require('zlib');
|
||||
const GoogleActions = require('./googleActions.js');
|
||||
const Markdown = require('../shared/naturalcrit/markdown.js');
|
||||
|
||||
// const getTopBrews = (cb)=>{
|
||||
// const getTopBrews = (cb) => {
|
||||
// HomebrewModel.find().sort({ views: -1 }).limit(5).exec(function(err, brews) {
|
||||
// cb(brews);
|
||||
// });
|
||||
// };
|
||||
|
||||
const MAX_TITLE_LENGTH = 100;
|
||||
|
||||
const getGoodBrewTitle = (text)=>{
|
||||
const titlePos = text.indexOf('# ');
|
||||
if(titlePos !== -1){
|
||||
const ending = text.indexOf('\n', titlePos);
|
||||
return text.substring(titlePos + 2, ending);
|
||||
} else {
|
||||
return _.find(text.split('\n'), (line)=>{
|
||||
return line;
|
||||
});
|
||||
}
|
||||
const tokens = Markdown.marked.lexer(text);
|
||||
return (tokens.find((token)=>token.type == 'heading' || token.type == 'paragraph')?.text || 'No Title')
|
||||
.slice(0, MAX_TITLE_LENGTH);
|
||||
};
|
||||
|
||||
const excludePropsFromUpdate = (brew)=>{
|
||||
// Remove undesired properties
|
||||
const propsToExclude = ['views', 'lastViewed'];
|
||||
for (const prop of propsToExclude) {
|
||||
delete brew[prop];
|
||||
};
|
||||
return brew;
|
||||
};
|
||||
|
||||
const mergeBrewText = (text, style)=>{
|
||||
if(typeof style !== 'undefined') {
|
||||
text = `\`\`\`css\n` +
|
||||
`${style}\n` +
|
||||
`\`\`\`\n\n` +
|
||||
`${text}`;
|
||||
}
|
||||
return text;
|
||||
};
|
||||
|
||||
router.post('/api', (req, res)=>{
|
||||
const newBrew = (req, res)=>{
|
||||
const brew = req.body;
|
||||
|
||||
let authors = [];
|
||||
if(req.account) authors = [req.account.username];
|
||||
|
||||
const newHomebrew = new HomebrewModel(_.merge({},
|
||||
req.body,
|
||||
{ authors: authors }
|
||||
));
|
||||
|
||||
if(!newHomebrew.title){
|
||||
newHomebrew.title = getGoodBrewTitle(newHomebrew.text);
|
||||
if(!brew.title) {
|
||||
brew.title = getGoodBrewTitle(brew.text);
|
||||
}
|
||||
|
||||
newHomebrew.textBin = zlib.deflateRawSync(newHomebrew.text); // Compress brew text to binary before saving
|
||||
newHomebrew.text = undefined; // Delete the non-binary text field since it's not needed anymore
|
||||
brew.authors = (req.account) ? [req.account.username] : [];
|
||||
brew.text = mergeBrewText(brew.text, brew.style);
|
||||
|
||||
delete brew.editId;
|
||||
delete brew.shareId;
|
||||
delete brew.googleId;
|
||||
|
||||
const newHomebrew = new HomebrewModel(brew);
|
||||
// Compress brew text to binary before saving
|
||||
newHomebrew.textBin = zlib.deflateRawSync(newHomebrew.text);
|
||||
// Delete the non-binary text field since it's not needed anymore
|
||||
newHomebrew.text = undefined;
|
||||
|
||||
newHomebrew.save((err, obj)=>{
|
||||
if(err){
|
||||
if(err) {
|
||||
console.error(err, err.toString(), err.stack);
|
||||
return res.status(500).send(`Error while creating new brew, ${err.toString()}`);
|
||||
}
|
||||
return res.json(obj);
|
||||
});
|
||||
});
|
||||
|
||||
router.put('/api/update/:id', (req, res)=>{
|
||||
obj = obj.toObject();
|
||||
obj.gDrive = false;
|
||||
return res.status(200).send(obj);
|
||||
});
|
||||
};
|
||||
|
||||
const updateBrew = (req, res)=>{
|
||||
HomebrewModel.get({ editId: req.params.id })
|
||||
.then((brew)=>{
|
||||
brew = _.merge(brew, req.body);
|
||||
brew.textBin = zlib.deflateRawSync(req.body.text); // Compress brew text to binary before saving
|
||||
brew.text = undefined; // Delete the non-binary text field since it's not needed anymore
|
||||
const updateBrew = excludePropsFromUpdate(req.body);
|
||||
brew = _.merge(brew, updateBrew);
|
||||
brew.text = mergeBrewText(brew.text, brew.style);
|
||||
|
||||
// Compress brew text to binary before saving
|
||||
brew.textBin = zlib.deflateRawSync(brew.text);
|
||||
// Delete the non-binary text field since it's not needed anymore
|
||||
brew.text = undefined;
|
||||
brew.updatedAt = new Date();
|
||||
|
||||
if(req.account) brew.authors = _.uniq(_.concat(brew.authors, req.account.username));
|
||||
if(req.account) {
|
||||
brew.authors = _.uniq(_.concat(brew.authors, req.account.username));
|
||||
}
|
||||
|
||||
brew.markModified('authors');
|
||||
brew.markModified('systems');
|
||||
@@ -68,87 +96,92 @@ router.put('/api/update/:id', (req, res)=>{
|
||||
});
|
||||
})
|
||||
.catch((err)=>{
|
||||
console.log(err);
|
||||
console.error(err);
|
||||
return res.status(500).send('Error while saving');
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
router.get('/api/remove/:id', (req, res)=>{
|
||||
const deleteBrew = (req, res)=>{
|
||||
HomebrewModel.find({ editId: req.params.id }, (err, objs)=>{
|
||||
if(!objs.length || err) return res.status(404).send('Can not find homebrew with that id');
|
||||
if(!objs.length || err) {
|
||||
return res.status(404).send('Can not find homebrew with that id');
|
||||
}
|
||||
|
||||
const brew = objs[0];
|
||||
|
||||
// Remove current user as author
|
||||
if(req.account){
|
||||
if(req.account) {
|
||||
// Remove current user as author
|
||||
brew.authors = _.pull(brew.authors, req.account.username);
|
||||
brew.markModified('authors');
|
||||
}
|
||||
|
||||
// Delete brew if there are no authors left
|
||||
if(!brew.authors.length)
|
||||
if(brew.authors.length === 0) {
|
||||
// Delete brew if there are no authors left
|
||||
brew.remove((err)=>{
|
||||
if(err) return res.status(500).send('Error while removing');
|
||||
return res.status(200).send();
|
||||
});
|
||||
// Otherwise, save the brew with updated author list
|
||||
else
|
||||
} else {
|
||||
// Otherwise, save the brew with updated author list
|
||||
brew.save((err, savedBrew)=>{
|
||||
if(err) throw err;
|
||||
return res.status(200).send(savedBrew);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const newGoogleBrew = async (req, res, next)=>{
|
||||
let oAuth2Client;
|
||||
|
||||
try { oAuth2Client = GoogleActions.authCheck(req.account, res); } catch (err) { return res.status(err.status).send(err.message); }
|
||||
|
||||
const brew = req.body;
|
||||
|
||||
if(!brew.title) {
|
||||
brew.title = getGoodBrewTitle(brew.text);
|
||||
}
|
||||
|
||||
brew.authors = (req.account) ? [req.account.username] : [];
|
||||
brew.text = mergeBrewText(brew.text, brew.style);
|
||||
|
||||
delete brew.editId;
|
||||
delete brew.shareId;
|
||||
delete brew.googleId;
|
||||
|
||||
req.body = brew;
|
||||
|
||||
try {
|
||||
const newBrew = await GoogleActions.newGoogleBrew(oAuth2Client, brew);
|
||||
return res.status(200).send(newBrew);
|
||||
} catch (err) {
|
||||
return res.status(err.response.status).send(err);
|
||||
}
|
||||
};
|
||||
|
||||
const updateGoogleBrew = async (req, res, next)=>{
|
||||
let oAuth2Client;
|
||||
|
||||
try { oAuth2Client = GoogleActions.authCheck(req.account, res); } catch (err) { return res.status(err.status).send(err.message); }
|
||||
|
||||
const brew = excludePropsFromUpdate(req.body);
|
||||
brew.text = mergeBrewText(brew.text, brew.style);
|
||||
|
||||
try {
|
||||
const updatedBrew = await GoogleActions.updateGoogleBrew(oAuth2Client, brew);
|
||||
return res.status(200).send(updatedBrew);
|
||||
} catch (err) {
|
||||
return res.status(err.response.status).send(err);
|
||||
}
|
||||
};
|
||||
|
||||
router.post('/api', newBrew);
|
||||
router.post('/api/newGoogle/', newGoogleBrew);
|
||||
router.put('/api/:id', updateBrew);
|
||||
router.put('/api/update/:id', updateBrew);
|
||||
router.put('/api/updateGoogle/:id', updateGoogleBrew);
|
||||
router.delete('/api/:id', deleteBrew);
|
||||
router.get('/api/remove/:id', deleteBrew);
|
||||
router.get('/api/removeGoogle/:id', (req, res)=>{GoogleActions.deleteGoogleBrew(req, res, req.params.id);});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
/*
|
||||
|
||||
|
||||
|
||||
module.exports = function(app){
|
||||
|
||||
app;
|
||||
|
||||
|
||||
|
||||
|
||||
app.get('/api/search', mw.adminOnly, function(req, res){
|
||||
|
||||
var page = req.query.page || 0;
|
||||
var count = req.query.count || 20;
|
||||
|
||||
var query = {};
|
||||
if(req.query && req.query.id){
|
||||
query = {
|
||||
"$or" : [{
|
||||
editId : req.query.id
|
||||
},{
|
||||
shareId : req.query.id
|
||||
}]
|
||||
};
|
||||
}
|
||||
|
||||
HomebrewModel.find(query, {
|
||||
text : 0 //omit the text
|
||||
}, {
|
||||
skip: page*count,
|
||||
limit: count*1
|
||||
}, function(err, objs){
|
||||
if(err) console.log(err);
|
||||
return res.json({
|
||||
page : page,
|
||||
count : count,
|
||||
total : homebrewTotal,
|
||||
brews : objs
|
||||
});
|
||||
|
||||
});
|
||||
})
|
||||
|
||||
|
||||
|
||||
|
||||
return app;
|
||||
}
|
||||
*/
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
const mongoose = require('mongoose');
|
||||
const shortid = require('shortid');
|
||||
const { nanoid } = require('nanoid');
|
||||
const _ = require('lodash');
|
||||
const zlib = require('zlib');
|
||||
|
||||
const HomebrewSchema = mongoose.Schema({
|
||||
shareId : { type: String, default: shortid.generate, index: { unique: true } },
|
||||
editId : { type: String, default: shortid.generate, index: { unique: true } },
|
||||
title : { type: String, default: '' },
|
||||
text : { type: String, default: '' },
|
||||
textBin : { type: Buffer },
|
||||
shareId : { type: String, default: ()=>{return nanoid(12);}, index: { unique: true } },
|
||||
editId : { type: String, default: ()=>{return nanoid(12);}, index: { unique: true } },
|
||||
title : { type: String, default: '' },
|
||||
text : { type: String, default: '' },
|
||||
textBin : { type: Buffer },
|
||||
pageCount : { type: Number, default: 1 },
|
||||
|
||||
description : { type: String, default: '' },
|
||||
tags : { type: String, default: '' },
|
||||
systems : [String],
|
||||
renderer : { type: String, default: '' },
|
||||
authors : [String],
|
||||
published : { type: Boolean, default: false },
|
||||
|
||||
@@ -23,32 +25,17 @@ const HomebrewSchema = mongoose.Schema({
|
||||
version : { type: Number, default: 1 }
|
||||
}, { versionKey: false });
|
||||
|
||||
|
||||
|
||||
HomebrewSchema.methods.sanatize = function(full=false){
|
||||
const brew = this.toJSON();
|
||||
delete brew._id;
|
||||
delete brew.__v;
|
||||
if(full){
|
||||
delete brew.editId;
|
||||
}
|
||||
HomebrewSchema.statics.increaseView = async function(query) {
|
||||
const brew = await Homebrew.findOne(query).exec();
|
||||
brew.lastViewed = new Date();
|
||||
brew.views = brew.views + 1;
|
||||
await brew.save()
|
||||
.catch((err)=>{
|
||||
return err;
|
||||
});
|
||||
return brew;
|
||||
};
|
||||
|
||||
|
||||
HomebrewSchema.methods.increaseView = function(){
|
||||
return new Promise((resolve, reject)=>{
|
||||
this.lastViewed = new Date();
|
||||
this.views = this.views + 1;
|
||||
this.save((err)=>{
|
||||
if(err) return reject(err);
|
||||
return resolve(this);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
|
||||
HomebrewSchema.statics.get = function(query){
|
||||
return new Promise((resolve, reject)=>{
|
||||
Homebrew.find(query, (err, brews)=>{
|
||||
@@ -57,6 +44,8 @@ HomebrewSchema.statics.get = function(query){
|
||||
unzipped = zlib.inflateRawSync(brews[0].textBin);
|
||||
brews[0].text = unzipped.toString();
|
||||
}
|
||||
if(!brews[0].renderer)
|
||||
brews[0].renderer = 'legacy';
|
||||
return resolve(brews[0]);
|
||||
});
|
||||
});
|
||||
@@ -68,20 +57,16 @@ HomebrewSchema.statics.getByUser = function(username, allowAccess=false){
|
||||
if(allowAccess){
|
||||
delete query.published;
|
||||
}
|
||||
Homebrew.find(query, (err, brews)=>{
|
||||
Homebrew.find(query).lean().exec((err, brews)=>{ //lean() converts results to JSObjects
|
||||
if(err) return reject('Can not find brew');
|
||||
return resolve(_.map(brews, (brew)=>{
|
||||
return brew.sanatize(!allowAccess);
|
||||
}));
|
||||
return resolve(brews);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
|
||||
const Homebrew = mongoose.model('Homebrew', HomebrewSchema);
|
||||
|
||||
module.exports = {
|
||||
schema : HomebrewSchema,
|
||||
model : Homebrew,
|
||||
};
|
||||
};
|
||||
|
||||
31
server/static-assets.mv.js
Normal file
31
server/static-assets.mv.js
Normal file
@@ -0,0 +1,31 @@
|
||||
const expressStaticGzip = require('express-static-gzip');
|
||||
|
||||
// Serve brotli-compressed static files if available
|
||||
const customCacheControlHandler=(response, path)=>{
|
||||
if(path.endsWith('.br')) {
|
||||
// Drop .br suffix to help mime understand the actual type of the file
|
||||
path = path.slice(0, -3);
|
||||
}
|
||||
if(path.endsWith('.js') || path.endsWith('.css')) {
|
||||
// .js and .css files are allowed to be cached up to 12 hours, but then
|
||||
// they must be revalidated to see if there are any updates
|
||||
response.setHeader('Cache-Control', 'public, max-age: 43200, must-revalidate');
|
||||
} else {
|
||||
// Everything else is cached up to a months as we don't update our images
|
||||
// or fonts frequently
|
||||
response.setHeader('Cache-Control', 'public, max-age=2592000, must-revalidate');
|
||||
}
|
||||
};
|
||||
|
||||
const init=(pathToAssets)=>{
|
||||
return expressStaticGzip(pathToAssets, {
|
||||
enableBrotli : true,
|
||||
orderPreference : ['br'],
|
||||
index : false,
|
||||
serveStatic : {
|
||||
cacheControl : false, // we are going to use custom cache-control
|
||||
setHeaders : customCacheControlHandler
|
||||
} });
|
||||
};
|
||||
|
||||
module.exports = init;
|
||||
33
server/token.js
Normal file
33
server/token.js
Normal file
@@ -0,0 +1,33 @@
|
||||
const jwt = require('jwt-simple');
|
||||
|
||||
// Load configuration values
|
||||
const config = require('nconf')
|
||||
.argv()
|
||||
.env({ lowerCase: true }) // Load environment variables
|
||||
.file('environment', { file: `config/${process.env.NODE_ENV}.json` })
|
||||
.file('defaults', { file: 'config/default.json' });
|
||||
|
||||
// Generate an Access Token for the given User ID
|
||||
const generateAccessToken = (account)=>{
|
||||
const payload = account;
|
||||
|
||||
// When the token was issued
|
||||
payload.issued = (new Date());
|
||||
// Which service issued the Token
|
||||
payload.issuer = config.get('authentication_token_issuer');
|
||||
// Which service is the token intended for
|
||||
payload.audience = config.get('authentication_token_audience');
|
||||
// The signing key for signing the token
|
||||
delete payload.password;
|
||||
delete payload._id;
|
||||
|
||||
const secret = config.get('authentication_token_secret');
|
||||
|
||||
const token = jwt.encode(payload, secret);
|
||||
|
||||
return token;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
generateAccessToken : generateAccessToken
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
|
||||
require('./renderWarnings.less');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
@@ -25,10 +25,10 @@ const RenderWarnings = createClass({
|
||||
if(!isChrome){
|
||||
return <li key='chrome'>
|
||||
<em>Built for Chrome </em> <br />
|
||||
Other browsers do not support
|
||||
<a target='_blank' href='https://developer.mozilla.org/en-US/docs/Web/CSS/column-span#Browser_compatibility'>
|
||||
key features
|
||||
</a> this site uses.
|
||||
Other browsers have not been tested for compatiblilty. If you
|
||||
experience issues with your document not rendering or printing
|
||||
properly, please try using the latest version of Chrome before
|
||||
submitting a bug report.
|
||||
</li>;
|
||||
}
|
||||
},
|
||||
@@ -53,8 +53,8 @@ const RenderWarnings = createClass({
|
||||
if(_.isEmpty(this.state.warnings)) return null;
|
||||
|
||||
return <div className='renderWarnings'>
|
||||
<i className='fa fa-times dismiss' onClick={this.dismiss}/>
|
||||
<i className='fa fa-exclamation-triangle ohno' />
|
||||
<i className='fas fa-times dismiss' onClick={this.dismiss}/>
|
||||
<i className='fas fa-exclamation-triangle ohno' />
|
||||
<h3>Render Warnings</h3>
|
||||
<small>If this homebrew is rendering badly if might be because of the following:</small>
|
||||
<ul>{_.values(this.state.warnings)}</ul>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
require('./codeEditor.less');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
@@ -10,75 +11,108 @@ if(typeof navigator !== 'undefined'){
|
||||
|
||||
//Language Modes
|
||||
require('codemirror/mode/gfm/gfm.js'); //Github flavoured markdown
|
||||
require('codemirror/mode/css/css.js');
|
||||
require('codemirror/mode/javascript/javascript.js');
|
||||
}
|
||||
|
||||
|
||||
const CodeEditor = createClass({
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
language : '',
|
||||
value : '',
|
||||
wrap : false,
|
||||
onChange : function(){},
|
||||
onCursorActivity : function(){},
|
||||
language : '',
|
||||
value : '',
|
||||
wrap : true,
|
||||
onChange : ()=>{}
|
||||
};
|
||||
},
|
||||
|
||||
componentDidMount : function() {
|
||||
this.buildEditor();
|
||||
},
|
||||
|
||||
componentDidUpdate : function(prevProps) {
|
||||
if(prevProps.language !== this.props.language){ //rebuild editor when switching tabs
|
||||
this.buildEditor();
|
||||
}
|
||||
if(this.codeMirror && this.codeMirror.getValue() != this.props.value) { //update editor contents if brew.text is changed from outside
|
||||
this.codeMirror.setValue(this.props.value);
|
||||
}
|
||||
},
|
||||
|
||||
buildEditor : function() {
|
||||
this.codeMirror = CodeMirror(this.refs.editor, {
|
||||
value : this.props.value,
|
||||
lineNumbers : true,
|
||||
lineWrapping : this.props.wrap,
|
||||
mode : this.props.language,
|
||||
extraKeys : {
|
||||
value : this.props.value,
|
||||
lineNumbers : true,
|
||||
lineWrapping : this.props.wrap,
|
||||
mode : this.props.language, //TODO: CSS MODE DOESN'T SEEM TO LOAD PROPERLY
|
||||
indentWithTabs : true,
|
||||
tabSize : 2,
|
||||
extraKeys : {
|
||||
'Ctrl-B' : this.makeBold,
|
||||
'Ctrl-I' : this.makeItalic
|
||||
'Cmd-B' : this.makeBold,
|
||||
'Ctrl-I' : this.makeItalic,
|
||||
'Cmd-I' : this.makeItalic,
|
||||
'Ctrl-M' : this.makeSpan,
|
||||
'Cmd-M' : this.makeSpan,
|
||||
'Ctrl-/' : this.makeComment,
|
||||
'Cmd-/' : this.makeComment
|
||||
}
|
||||
});
|
||||
|
||||
this.codeMirror.on('change', this.handleChange);
|
||||
this.codeMirror.on('cursorActivity', this.handleCursorActivity);
|
||||
// Note: codeMirror passes a copy of itself in this callback. cm === this.codeMirror. Either one works.
|
||||
this.codeMirror.on('change', (cm)=>{this.props.onChange(cm.getValue());});
|
||||
this.updateSize();
|
||||
},
|
||||
|
||||
makeBold : function() {
|
||||
const selection = this.codeMirror.getSelection();
|
||||
this.codeMirror.replaceSelection(`**${selection}**`, 'around');
|
||||
},
|
||||
|
||||
makeItalic : function() {
|
||||
const selection = this.codeMirror.getSelection();
|
||||
this.codeMirror.replaceSelection(`*${selection}*`, 'around');
|
||||
},
|
||||
|
||||
componentWillReceiveProps : function(nextProps){
|
||||
if(this.codeMirror && nextProps.value !== undefined && this.codeMirror.getValue() != nextProps.value) {
|
||||
this.codeMirror.setValue(nextProps.value);
|
||||
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 });
|
||||
}
|
||||
},
|
||||
|
||||
shouldComponentUpdate : function(nextProps, nextState) {
|
||||
return false;
|
||||
makeItalic : 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 });
|
||||
}
|
||||
},
|
||||
|
||||
makeSpan : 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 });
|
||||
}
|
||||
},
|
||||
|
||||
makeComment : function() {
|
||||
const selection = this.codeMirror.getSelection(), t = selection.slice(0, 4) === '<!--' && selection.slice(-3) === '-->';
|
||||
this.codeMirror.replaceSelection(t ? selection.slice(4, -3) : `<!-- ${selection} -->`, 'around');
|
||||
if(selection.length === 0){
|
||||
const cursor = this.codeMirror.getCursor();
|
||||
this.codeMirror.setCursor({ line: cursor.line, ch: cursor.ch - 4 });
|
||||
}
|
||||
},
|
||||
|
||||
//=-- Externally used -==//
|
||||
setCursorPosition : function(line, char){
|
||||
setTimeout(()=>{
|
||||
this.codeMirror.focus();
|
||||
this.codeMirror.doc.setCursor(line, char);
|
||||
}, 10);
|
||||
},
|
||||
|
||||
getCursorPosition : function(){
|
||||
return this.codeMirror.getCursor();
|
||||
},
|
||||
updateSize : function(){
|
||||
this.codeMirror.refresh();
|
||||
},
|
||||
|
||||
handleChange : function(editor){
|
||||
this.props.onChange(editor.getValue());
|
||||
},
|
||||
handleCursorActivity : function(){
|
||||
this.props.onCursorActivity(this.codeMirror.doc.getCursor());
|
||||
},
|
||||
//----------------------//
|
||||
|
||||
render : function(){
|
||||
return <div className='codeEditor' ref='editor' />;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable max-lines */
|
||||
const _ = require('lodash');
|
||||
const Markdown = require('marked');
|
||||
const renderer = new Markdown.Renderer();
|
||||
@@ -13,6 +14,492 @@ renderer.html = function (html) {
|
||||
return html;
|
||||
};
|
||||
|
||||
// Don't wrap {{ Divs or {{ empty Spans in <p> tags
|
||||
renderer.paragraph = function(text){
|
||||
let match;
|
||||
if(text.startsWith('<div') || text.startsWith('</div'))
|
||||
return `${text}`;
|
||||
else if(match = text.match(/(^|^.*?\n)<span class="inline-block(.*?<\/span>)$/)) {
|
||||
return `${match[1].trim() ? `<p>${match[1]}</p>` : ''}<span class="inline-block${match[2]}`;
|
||||
} else
|
||||
return `<p>${text}</p>\n`;
|
||||
};
|
||||
|
||||
const mustacheSpans = {
|
||||
name : 'mustacheSpans',
|
||||
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
|
||||
tokenizer(src, tokens) {
|
||||
const completeSpan = /^{{[^\n]*}}/; // Regex for the complete token
|
||||
const inlineRegex = /{{(?::(?:"[\w,\-()#%. ]*"|[\w\,\-()#%.]*)|[^"'{}\s])*\s*|}}/g;
|
||||
const match = completeSpan.exec(src);
|
||||
if(match) {
|
||||
//Find closing delimiter
|
||||
let blockCount = 0;
|
||||
let tags = '';
|
||||
let endTags = 0;
|
||||
let endToken = 0;
|
||||
let delim;
|
||||
while (delim = inlineRegex.exec(match[0])) {
|
||||
if(!tags) {
|
||||
tags = ` ${processStyleTags(delim[0].substring(2))}`;
|
||||
endTags = delim[0].length;
|
||||
}
|
||||
if(delim[0].startsWith('{{')) {
|
||||
blockCount++;
|
||||
} else if(delim[0] == '}}' && blockCount !== 0) {
|
||||
blockCount--;
|
||||
if(blockCount == 0) {
|
||||
endToken = inlineRegex.lastIndex;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(endToken) {
|
||||
const raw = src.slice(0, endToken);
|
||||
const text = raw.slice(endTags || -2, -2);
|
||||
|
||||
return { // Token to generate
|
||||
type : 'mustacheSpans', // Should match "name" above
|
||||
raw : raw, // Text to consume from the source
|
||||
text : text, // Additional custom properties
|
||||
tags : tags,
|
||||
tokens : this.lexer.inlineTokens(text) // inlineTokens to process **bold**, *italics*, etc.
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
renderer(token) {
|
||||
return `<span class="inline-block${token.tags}>${this.parser.parseInline(token.tokens)}</span>`; // parseInline to turn child tokens into HTML
|
||||
}
|
||||
};
|
||||
|
||||
const mustacheDivs = {
|
||||
name : 'mustacheDivs',
|
||||
level : 'block',
|
||||
start(src) { return src.match(/\n *{{[^{]/m)?.index; }, // Hint to Marked.js to stop and check for a match
|
||||
tokenizer(src, tokens) {
|
||||
const completeBlock = /^ *{{[^\n}]* *\n.*\n *}}/s; // Regex for the complete token
|
||||
const blockRegex = /^ *{{(?::(?:"[\w,\-()#%. ]*"|[\w\,\-()#%.]*)|[^"'{}\s])* *$|^ *}}$/gm;
|
||||
const match = completeBlock.exec(src);
|
||||
if(match) {
|
||||
//Find closing delimiter
|
||||
let blockCount = 0;
|
||||
let tags = '';
|
||||
let endTags = 0;
|
||||
let endToken = 0;
|
||||
let delim;
|
||||
while (delim = blockRegex.exec(match[0])?.[0].trim()) {
|
||||
if(!tags) {
|
||||
tags = ` ${processStyleTags(delim.substring(2))}`;
|
||||
endTags = delim.length;
|
||||
}
|
||||
if(delim.startsWith('{{')) {
|
||||
blockCount++;
|
||||
} else if(delim == '}}' && blockCount !== 0) {
|
||||
blockCount--;
|
||||
if(blockCount == 0) {
|
||||
endToken = blockRegex.lastIndex;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(endToken) {
|
||||
const raw = src.slice(0, endToken);
|
||||
const text = raw.slice(endTags || -2, -2);
|
||||
return { // Token to generate
|
||||
type : 'mustacheDivs', // Should match "name" above
|
||||
raw : raw, // Text to consume from the source
|
||||
text : text, // Additional custom properties
|
||||
tags : tags,
|
||||
tokens : this.lexer.blockTokens(text)
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
renderer(token) {
|
||||
return `<div class="block${token.tags}>${this.parser.parse(token.tokens)}</div>`; // parseInline to turn child tokens into HTML
|
||||
}
|
||||
};
|
||||
|
||||
const mustacheInjectInline = {
|
||||
name : 'mustacheInjectInline',
|
||||
level : 'inline',
|
||||
start(src) { return src.match(/ *{[^{\n]/)?.index; }, // Hint to Marked.js to stop and check for a match
|
||||
tokenizer(src, tokens) {
|
||||
const inlineRegex = /^ *{((?::(?:"[\w,\-()#%. ]*"|[\w\,\-()#%.]*)|[^"'{}\s])*)}/g;
|
||||
const match = inlineRegex.exec(src);
|
||||
if(match) {
|
||||
const lastToken = tokens[tokens.length - 1];
|
||||
if(!lastToken)
|
||||
return false;
|
||||
|
||||
const tags = ` ${processStyleTags(match[1])}`;
|
||||
lastToken.originalType = lastToken.type;
|
||||
lastToken.type = 'mustacheInjectInline';
|
||||
lastToken.tags = tags;
|
||||
return {
|
||||
type : 'text', // Should match "name" above
|
||||
raw : match[0], // Text to consume from the source
|
||||
text : ''
|
||||
};
|
||||
}
|
||||
},
|
||||
renderer(token) {
|
||||
token.type = token.originalType;
|
||||
const text = this.parser.parseInline([token]);
|
||||
const openingTag = /(<[^\s<>]+)([^\n<>]*>.*)/s.exec(text);
|
||||
if(openingTag) {
|
||||
return `${openingTag[1]} class="${token.tags}${openingTag[2]}`;
|
||||
}
|
||||
return text;
|
||||
}
|
||||
};
|
||||
|
||||
const mustacheInjectBlock = {
|
||||
extensions : [{
|
||||
name : 'mustacheInjectBlock',
|
||||
level : 'block',
|
||||
start(src) { return src.match(/\n *{[^{\n]/m)?.index; }, // Hint to Marked.js to stop and check for a match
|
||||
tokenizer(src, tokens) {
|
||||
const inlineRegex = /^ *{((?::(?:"[\w,\-()#%. ]*"|[\w\,\-()#%.]*)|[^"'{}\s])*)}/ym;
|
||||
const match = inlineRegex.exec(src);
|
||||
if(match) {
|
||||
const lastToken = tokens[tokens.length - 1];
|
||||
if(!lastToken)
|
||||
return false;
|
||||
|
||||
lastToken.originalType = 'mustacheInjectBlock';
|
||||
lastToken.tags = ` ${processStyleTags(match[1])}`;
|
||||
return {
|
||||
type : 'mustacheInjectBlock', // Should match "name" above
|
||||
raw : match[0], // Text to consume from the source
|
||||
text : ''
|
||||
};
|
||||
}
|
||||
},
|
||||
renderer(token) {
|
||||
if(!token.originalType){
|
||||
return;
|
||||
}
|
||||
token.type = token.originalType;
|
||||
const text = this.parser.parse([token]);
|
||||
const openingTag = /(<[^\s<>]+)([^\n<>]*>.*)/s.exec(text);
|
||||
if(openingTag) {
|
||||
return `${openingTag[1]} class="${token.tags}${openingTag[2]}`;
|
||||
}
|
||||
return text;
|
||||
}
|
||||
}],
|
||||
walkTokens(token) {
|
||||
// After token tree is finished, tag tokens to apply styles to so Renderer can find them
|
||||
// Does not work with tables since Marked.js tables generate invalid "tokens", and changing "type" ruins Marked handling that edge-case
|
||||
if(token.originalType == 'mustacheInjectBlock' && token.type !== 'table') {
|
||||
token.originalType = token.type;
|
||||
token.type = 'mustacheInjectBlock';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const definitionLists = {
|
||||
name : 'definitionLists',
|
||||
level : 'block',
|
||||
start(src) { return src.match(/^.*?::.*/m)?.index; }, // Hint to Marked.js to stop and check for a match
|
||||
tokenizer(src, tokens) {
|
||||
const regex = /^([^\n]*?)::([^\n]*)(?:\n|$)/ym;
|
||||
let match;
|
||||
let endIndex = 0;
|
||||
const definitions = [];
|
||||
while (match = regex.exec(src)) {
|
||||
definitions.push({
|
||||
dt : this.lexer.inlineTokens(match[1].trim()),
|
||||
dd : this.lexer.inlineTokens(match[2].trim())
|
||||
});
|
||||
endIndex = regex.lastIndex;
|
||||
}
|
||||
if(definitions.length) {
|
||||
return {
|
||||
type : 'definitionLists',
|
||||
raw : src.slice(0, endIndex),
|
||||
definitions
|
||||
};
|
||||
}
|
||||
},
|
||||
renderer(token) {
|
||||
return `<dl>${token.definitions.reduce((html, def)=>{
|
||||
return `${html}<dt>${this.parser.parseInline(def.dt)}</dt>`
|
||||
+ `<dd>${this.parser.parseInline(def.dd)}</dd>\n`;
|
||||
}, '')}</dl>`;
|
||||
}
|
||||
};
|
||||
|
||||
const spanTable = {
|
||||
name : 'spanTable',
|
||||
level : 'block', // Is this a block-level or inline-level tokenizer?
|
||||
start(src) { return src.match(/^\n *([^\n ].*\|.*)\n/)?.index; }, // Hint to Marked.js to stop and check for a match
|
||||
tokenizer(src, tokens) {
|
||||
//const regex = this.tokenizer.rules.block.table;
|
||||
const regex = new RegExp('^ *([^\\n ].*\\|.*\\n(?: *[^\\s].*\\n)*?)' // Header
|
||||
+ ' {0,3}(?:\\| *)?(:?-+:? *(?:\\| *:?-+:? *)*)\\|?' // Align
|
||||
+ '(?:\\n *((?:(?!\\n| {0,3}((?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})' // Cells
|
||||
+ '(?:\\n+|$)| {0,3}#{1,6} | {0,3}>| {4}[^\\n]| {0,3}(?:`{3,}'
|
||||
+ '(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n| {0,3}(?:[*+-]|1[.)]) |'
|
||||
+ '<\\/?(?:address|article|aside|base|basefont|blockquote|body|'
|
||||
+ 'caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|section|source|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul)(?: +|\\n|\\/?>)|<(?:script|pre|style|textarea|!--)).*(?:\\n|$))*)\\n*|$)'); // Cells
|
||||
const cap = regex.exec(src);
|
||||
|
||||
if(cap) {
|
||||
const item = {
|
||||
type : 'spanTable',
|
||||
header : cap[1].replace(/\n$/, '').split('\n'),
|
||||
align : cap[2].replace(/^ *|\| *$/g, '').split(/ *\| */),
|
||||
rows : cap[3] ? cap[3].replace(/\n$/, '').split('\n') : []
|
||||
};
|
||||
|
||||
// Get first header row to determine how many columns
|
||||
item.header[0] = splitCells(item.header[0]);
|
||||
|
||||
const colCount = item.header[0].reduce((length, header)=>{
|
||||
return length + header.colspan;
|
||||
}, 0);
|
||||
|
||||
if(colCount === item.align.length) {
|
||||
item.raw = cap[0];
|
||||
|
||||
let i, j, k, row;
|
||||
|
||||
// Get alignment row (:---:)
|
||||
let l = item.align.length;
|
||||
|
||||
for (i = 0; i < l; i++) {
|
||||
if(/^ *-+: *$/.test(item.align[i])) {
|
||||
item.align[i] = 'right';
|
||||
} else if(/^ *:-+: *$/.test(item.align[i])) {
|
||||
item.align[i] = 'center';
|
||||
} else if(/^ *:-+ *$/.test(item.align[i])) {
|
||||
item.align[i] = 'left';
|
||||
} else {
|
||||
item.align[i] = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Get any remaining header rows
|
||||
l = item.header.length;
|
||||
for (i = 1; i < l; i++) {
|
||||
item.header[i] = splitCells(item.header[i], colCount, item.header[i-1]);
|
||||
}
|
||||
|
||||
// Get main table cells
|
||||
l = item.rows.length;
|
||||
for (i = 0; i < l; i++) {
|
||||
item.rows[i] = splitCells(item.rows[i], colCount, item.rows[i-1]);
|
||||
}
|
||||
|
||||
// header child tokens
|
||||
l = item.header.length;
|
||||
for (j = 0; j < l; j++) {
|
||||
row = item.header[j];
|
||||
for (k = 0; k < row.length; k++) {
|
||||
row[k].tokens = [];
|
||||
this.lexer.inlineTokens(row[k].text, row[k].tokens);
|
||||
}
|
||||
}
|
||||
|
||||
// cell child tokens
|
||||
l = item.rows.length;
|
||||
for (j = 0; j < l; j++) {
|
||||
row = item.rows[j];
|
||||
for (k = 0; k < row.length; k++) {
|
||||
row[k].tokens = [];
|
||||
this.lexer.inlineTokens(row[k].text, row[k].tokens);
|
||||
}
|
||||
}
|
||||
return item;
|
||||
}
|
||||
}
|
||||
},
|
||||
renderer(token) {
|
||||
let i, j, row, cell, col, text;
|
||||
let output = `<table>`;
|
||||
output += `<thead>`;
|
||||
for (i = 0; i < token.header.length; i++) {
|
||||
row = token.header[i];
|
||||
let col = 0;
|
||||
output += `<tr>`;
|
||||
for (j = 0; j < row.length; j++) {
|
||||
cell = row[j];
|
||||
text = this.parser.parseInline(cell.tokens);
|
||||
output += getTableCell(text, cell, 'th', token.align[col]);
|
||||
col += cell.colspan;
|
||||
}
|
||||
output += `</tr>`;
|
||||
}
|
||||
output += `</thead>`;
|
||||
if(token.rows.length) {
|
||||
output += `<tbody>`;
|
||||
for (i = 0; i < token.rows.length; i++) {
|
||||
row = token.rows[i];
|
||||
col = 0;
|
||||
output += `<tr>`;
|
||||
for (j = 0; j < row.length; j++) {
|
||||
cell = row[j];
|
||||
text = this.parser.parseInline(cell.tokens);
|
||||
output += getTableCell(text, cell, 'td', token.align[col]);
|
||||
col += cell.colspan;
|
||||
}
|
||||
output += `</tr>`;
|
||||
}
|
||||
output += `</tbody>`;
|
||||
}
|
||||
output += `</table>`;
|
||||
return output;
|
||||
}
|
||||
};
|
||||
|
||||
const getTableCell = (text, cell, type, align)=>{
|
||||
if(!cell.rowspan) {
|
||||
return '';
|
||||
}
|
||||
const tag = `<${type}`
|
||||
+ `${cell.colspan > 1 ? ` colspan=${cell.colspan}` : ''}`
|
||||
+ `${cell.rowspan > 1 ? ` rowspan=${cell.rowspan}` : ''}`
|
||||
+ `${align ? ` align=${align}` : ''}>`;
|
||||
return `${tag + text}</${type}>\n`;
|
||||
};
|
||||
|
||||
const splitCells = (tableRow, count, prevRow = [])=>{
|
||||
const cells = [...tableRow.matchAll(/(?:[^|\\]|\\.?)+(?:\|+|$)/g)].map((x)=>x[0]);
|
||||
|
||||
// Remove first/last cell in a row if whitespace only and no leading/trailing pipe
|
||||
if(!cells[0]?.trim()) { cells.shift(); }
|
||||
if(!cells[cells.length - 1]?.trim()) { cells.pop(); }
|
||||
|
||||
let numCols = 0;
|
||||
let i, j, trimmedCell, prevCell, prevCols;
|
||||
|
||||
for (i = 0; i < cells.length; i++) {
|
||||
trimmedCell = cells[i].split(/\|+$/)[0];
|
||||
cells[i] = {
|
||||
rowspan : 1,
|
||||
colspan : Math.max(cells[i].length - trimmedCell.length, 1),
|
||||
text : trimmedCell.trim().replace(/\\\|/g, '|')
|
||||
// display escaped pipes as normal character
|
||||
};
|
||||
|
||||
// Handle Rowspan
|
||||
if(trimmedCell.slice(-1) == '^' && prevRow.length) {
|
||||
// Find matching cell in previous row
|
||||
prevCols = 0;
|
||||
for (j = 0; j < prevRow.length; j++) {
|
||||
prevCell = prevRow[j];
|
||||
if((prevCols == numCols) && (prevCell.colspan == cells[i].colspan)) {
|
||||
// merge into matching cell in previous row (the "target")
|
||||
cells[i].rowSpanTarget = prevCell.rowSpanTarget ?? prevCell;
|
||||
cells[i].rowSpanTarget.text += ` ${cells[i].text.slice(0, -1)}`;
|
||||
cells[i].rowSpanTarget.rowspan += 1;
|
||||
cells[i].rowspan = 0;
|
||||
break;
|
||||
}
|
||||
prevCols += prevCell.colspan;
|
||||
if(prevCols > numCols)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
numCols += cells[i].colspan;
|
||||
}
|
||||
|
||||
// Force main cell rows to match header column count
|
||||
if(numCols > count) {
|
||||
cells.splice(count);
|
||||
} else {
|
||||
while (numCols < count) {
|
||||
cells.push({
|
||||
colspan : 1,
|
||||
text : ''
|
||||
});
|
||||
numCols += 1;
|
||||
}
|
||||
}
|
||||
return cells;
|
||||
};
|
||||
|
||||
Markdown.use({ extensions: [mustacheSpans, mustacheDivs, mustacheInjectInline, definitionLists, spanTable] });
|
||||
Markdown.use(mustacheInjectBlock);
|
||||
Markdown.use({ smartypants: true });
|
||||
|
||||
//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 cleanUrl = function (sanitize, base, href) {
|
||||
if(sanitize) {
|
||||
let prot;
|
||||
try {
|
||||
prot = decodeURIComponent(unescape(href))
|
||||
.replace(nonWordAndColonTest, '')
|
||||
.toLowerCase();
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
if(prot.indexOf('javascript:') === 0 || prot.indexOf('vbscript:') === 0 || prot.indexOf('data:') === 0) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
try {
|
||||
href = encodeURI(href).replace(/%25/g, '%');
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
return href;
|
||||
};
|
||||
|
||||
const escapeTest = /[&<>"']/;
|
||||
const escapeReplace = /[&<>"']/g;
|
||||
const escapeTestNoEncode = /[<>"']|&(?!#?\w+;)/;
|
||||
const escapeReplaceNoEncode = /[<>"']|&(?!#?\w+;)/g;
|
||||
const escapeReplacements = {
|
||||
'&' : '&',
|
||||
'<' : '<',
|
||||
'>' : '>',
|
||||
'"' : '"',
|
||||
'\'' : '''
|
||||
};
|
||||
const getEscapeReplacement = (ch)=>escapeReplacements[ch];
|
||||
const escape = function (html, encode) {
|
||||
if(encode) {
|
||||
if(escapeTest.test(html)) {
|
||||
return html.replace(escapeReplace, getEscapeReplacement);
|
||||
}
|
||||
} else {
|
||||
if(escapeTestNoEncode.test(html)) {
|
||||
return html.replace(escapeReplaceNoEncode, getEscapeReplacement);
|
||||
}
|
||||
}
|
||||
return html;
|
||||
};
|
||||
|
||||
const sanatizeScriptTags = (content)=>{
|
||||
return content
|
||||
.replace(/<script/ig, '<script')
|
||||
@@ -25,10 +512,24 @@ const tagRegex = new RegExp(`(${
|
||||
return `\\<${type}|\\</${type}>`;
|
||||
}).join('|')})`, 'g');
|
||||
|
||||
const processStyleTags = (string)=>{
|
||||
//split tags up. quotes can only occur right after colons.
|
||||
//TODO: can we simplify to just split on commas?
|
||||
const tags = string.match(/(?:[^, ":]+|:(?:"[^"]*"|))+/g);
|
||||
|
||||
if(!tags) return '"';
|
||||
|
||||
const id = _.remove(tags, (tag)=>tag.startsWith('#')).map((tag)=>tag.slice(1))[0];
|
||||
const classes = _.remove(tags, (tag)=>!tag.includes(':'));
|
||||
const styles = tags.map((tag)=>tag.replace(/:"?([^"]*)"?/g, ':$1;'));
|
||||
return `${classes.join(' ')}" ${id ? `id="${id}"` : ''} ${styles.length ? `style="${styles.join(' ')}"` : ''}`;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
marked : Markdown,
|
||||
render : (rawBrewText)=>{
|
||||
rawBrewText = rawBrewText.replace(/^\\column$/gm, `\n<div class='columnSplit'></div>\n`)
|
||||
.replace(/^(:+)$/gm, (match)=>`${`<div class='blank'></div>`.repeat(match.length)}\n`);
|
||||
return Markdown(
|
||||
sanatizeScriptTags(rawBrewText),
|
||||
{ renderer: renderer }
|
||||
@@ -87,4 +588,3 @@ module.exports = {
|
||||
return errors;
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
166
shared/naturalcrit/markdownLegacy.js
Normal file
166
shared/naturalcrit/markdownLegacy.js
Normal file
@@ -0,0 +1,166 @@
|
||||
const _ = require('lodash');
|
||||
const Markdown = require('markedLegacy');
|
||||
const renderer = new Markdown.Renderer();
|
||||
|
||||
//Processes the markdown within an HTML block if it's just a class-wrapper
|
||||
renderer.html = function (html) {
|
||||
if(_.startsWith(_.trim(html), '<div') && _.endsWith(_.trim(html), '</div>')){
|
||||
const openTag = html.substring(0, html.indexOf('>')+1);
|
||||
html = html.substring(html.indexOf('>')+1);
|
||||
html = html.substring(0, html.lastIndexOf('</div>'));
|
||||
return `${openTag} ${Markdown(html)} </div>`;
|
||||
}
|
||||
// if(_.startsWith(_.trim(html), '<style') && _.endsWith(_.trim(html), '</style>')){
|
||||
// const openTag = html.substring(0, html.indexOf('>')+1);
|
||||
// html = html.substring(html.indexOf('>')+1);
|
||||
// html = html.substring(0, html.lastIndexOf('</style>'));
|
||||
// html = html.replaceAll(/\s(\.[^{]*)/gm, '.legacy $1');
|
||||
// return `${openTag} ${html} </style>`;
|
||||
// }
|
||||
return html;
|
||||
};
|
||||
|
||||
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 cleanUrl = function (sanitize, base, href) {
|
||||
if(sanitize) {
|
||||
let prot;
|
||||
try {
|
||||
prot = decodeURIComponent(unescape(href))
|
||||
.replace(nonWordAndColonTest, '')
|
||||
.toLowerCase();
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
if(prot.indexOf('javascript:') === 0 || prot.indexOf('vbscript:') === 0 || prot.indexOf('data:') === 0) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
try {
|
||||
href = encodeURI(href).replace(/%25/g, '%');
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
return href;
|
||||
};
|
||||
|
||||
const escapeTest = /[&<>"']/;
|
||||
const escapeReplace = /[&<>"']/g;
|
||||
const escapeTestNoEncode = /[<>"']|&(?!#?\w+;)/;
|
||||
const escapeReplaceNoEncode = /[<>"']|&(?!#?\w+;)/g;
|
||||
const escapeReplacements = {
|
||||
'&' : '&',
|
||||
'<' : '<',
|
||||
'>' : '>',
|
||||
'"' : '"',
|
||||
'\'' : '''
|
||||
};
|
||||
const getEscapeReplacement = (ch)=>escapeReplacements[ch];
|
||||
const escape = function (html, encode) {
|
||||
if(encode) {
|
||||
if(escapeTest.test(html)) {
|
||||
return html.replace(escapeReplace, getEscapeReplacement);
|
||||
}
|
||||
} else {
|
||||
if(escapeTestNoEncode.test(html)) {
|
||||
return html.replace(escapeReplaceNoEncode, getEscapeReplacement);
|
||||
}
|
||||
}
|
||||
|
||||
return html;
|
||||
};
|
||||
|
||||
const sanatizeScriptTags = (content)=>{
|
||||
return content
|
||||
.replace(/<script/ig, '<script')
|
||||
.replace(/<\/script>/ig, '</script>');
|
||||
};
|
||||
|
||||
const tagTypes = ['div', 'span', 'a'];
|
||||
const tagRegex = new RegExp(`(${
|
||||
_.map(tagTypes, (type)=>{
|
||||
return `\\<${type}|\\</${type}>`;
|
||||
}).join('|')})`, 'g');
|
||||
|
||||
|
||||
module.exports = {
|
||||
marked : Markdown,
|
||||
render : (rawBrewText)=>{
|
||||
return Markdown(
|
||||
sanatizeScriptTags(rawBrewText),
|
||||
{ renderer: renderer }
|
||||
);
|
||||
},
|
||||
|
||||
validate : (rawBrewText)=>{
|
||||
const errors = [];
|
||||
const leftovers = _.reduce(rawBrewText.split('\n'), (acc, line, _lineNumber)=>{
|
||||
const lineNumber = _lineNumber + 1;
|
||||
const matches = line.match(tagRegex);
|
||||
if(!matches || !matches.length) return acc;
|
||||
|
||||
_.each(matches, (match)=>{
|
||||
_.each(tagTypes, (type)=>{
|
||||
if(match == `<${type}`){
|
||||
acc.push({
|
||||
type : type,
|
||||
line : lineNumber
|
||||
});
|
||||
}
|
||||
if(match === `</${type}>`){
|
||||
if(!acc.length){
|
||||
errors.push({
|
||||
line : lineNumber,
|
||||
type : type,
|
||||
text : 'Unmatched closing tag',
|
||||
id : 'CLOSE'
|
||||
});
|
||||
} else if(_.last(acc).type == type){
|
||||
acc.pop();
|
||||
} else {
|
||||
errors.push({
|
||||
line : `${_.last(acc).line} to ${lineNumber}`,
|
||||
type : type,
|
||||
text : 'Type mismatch on closing tag',
|
||||
id : 'MISMATCH'
|
||||
});
|
||||
acc.pop();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
_.each(leftovers, (unmatched)=>{
|
||||
errors.push({
|
||||
line : unmatched.line,
|
||||
type : unmatched.type,
|
||||
text : 'Unmatched opening tag',
|
||||
id : 'OPEN'
|
||||
});
|
||||
});
|
||||
|
||||
return errors;
|
||||
},
|
||||
};
|
||||
@@ -1,3 +1,4 @@
|
||||
require('./nav.less');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
@@ -16,7 +17,7 @@ const Nav = {
|
||||
}
|
||||
}),
|
||||
logo : function(){
|
||||
return <a className='navLogo' href='http://naturalcrit.com'>
|
||||
return <a className='navLogo' href='https://www.naturalcrit.com'>
|
||||
<NaturalCritIcon />
|
||||
<span className='name'>
|
||||
Natural<span className='crit'>Crit</span>
|
||||
@@ -49,7 +50,7 @@ const Nav = {
|
||||
const classes = cx('navItem', this.props.color, this.props.className);
|
||||
|
||||
let icon;
|
||||
if(this.props.icon) icon = <i className={`fa ${this.props.icon}`} />;
|
||||
if(this.props.icon) icon = <i className={this.props.icon} />;
|
||||
|
||||
const props = _.omit(this.props, ['newTab']);
|
||||
|
||||
@@ -70,4 +71,4 @@ const Nav = {
|
||||
};
|
||||
|
||||
|
||||
module.exports = Nav;
|
||||
module.exports = Nav;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
nav{
|
||||
background-color : #333;
|
||||
.navContent{
|
||||
position : relative;
|
||||
display : flex;
|
||||
justify-content : space-between;
|
||||
}
|
||||
@@ -77,4 +78,4 @@ nav{
|
||||
.navSection:last-child .navItem{
|
||||
border-left : 1px solid #666;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user