Compare commits
706 Commits
v2.10.7
...
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 | ||
|
|
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 | ||
|
|
03e74afe80 | ||
|
|
b0c1a5a6b1 | ||
|
|
3af43164f4 | ||
|
|
e394539742 | ||
|
|
bd416233eb | ||
|
|
7ca9d601a0 | ||
|
|
ac8988ad41 | ||
|
|
90fdc71279 | ||
|
|
5d126ff14d | ||
|
|
38e098f6c4 | ||
|
|
d3fa8a54ae | ||
|
|
443094d282 | ||
|
|
e727f1749f | ||
|
|
1224a54884 | ||
|
|
897e7dccc6 | ||
|
|
88631ed7a8 | ||
|
|
65f4094b5a | ||
|
|
99656357b1 | ||
|
|
d33ae2a50a | ||
|
|
f419430c6b |
@@ -1,7 +1,7 @@
|
||||
module.exports = {
|
||||
root : true,
|
||||
parserOptions : {
|
||||
ecmaVersion : 9,
|
||||
ecmaVersion : 2021,
|
||||
sourceType : 'module',
|
||||
ecmaFeatures : {
|
||||
jsx : true
|
||||
@@ -32,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',
|
||||
|
||||
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
|
||||
7
.gitignore
vendored
@@ -6,7 +6,8 @@ storage
|
||||
*.log
|
||||
build/*
|
||||
config/local.*
|
||||
config/docker.*
|
||||
|
||||
todo.md
|
||||
startDB.bat
|
||||
startMViewer.bat
|
||||
todo.md
|
||||
startDB.bat
|
||||
startMViewer.bat
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:8
|
||||
FROM node:14.15
|
||||
|
||||
ENV NODE_ENV=docker
|
||||
|
||||
|
||||
358
changelog.md
@@ -1,13 +1,326 @@
|
||||
```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)
|
||||
- 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 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
|
||||
|
||||
@@ -20,6 +333,8 @@
|
||||
### 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).
|
||||
|
||||
@@ -33,9 +348,6 @@
|
||||
### 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
|
||||
@@ -62,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
|
||||
@@ -79,8 +393,6 @@
|
||||
### 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)
|
||||
|
||||
\page
|
||||
|
||||
### 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
|
||||
@@ -91,7 +403,7 @@
|
||||
|
||||
### 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
|
||||
@@ -101,6 +413,8 @@
|
||||
- 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
|
||||
- Changed metaeditor icon
|
||||
@@ -121,10 +435,6 @@
|
||||
- Added a hover tooltip to fully read the brew description
|
||||
- Made the brew items take up only 25% allowing you to view more per row.
|
||||
|
||||
|
||||
```
|
||||
```
|
||||
|
||||
### Wednesday, 23/11/2016 - v2.5.0
|
||||
- Metadata can now be added to brews
|
||||
- Added a metadata editor onto the edit and new pages
|
||||
@@ -135,7 +445,6 @@
|
||||
- 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
|
||||
@@ -145,6 +454,8 @@
|
||||
- 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.
|
||||
|
||||
@@ -171,11 +482,8 @@
|
||||
- Allows adding in hyperlinks to specific pages
|
||||
- Even works after you print to pdf!
|
||||
|
||||
|
||||
\page
|
||||
|
||||
### 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.
|
||||
@@ -183,8 +491,10 @@
|
||||
- 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.
|
||||
- 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!
|
||||
@@ -197,13 +507,11 @@
|
||||
- Updated the issue template for (hopefully) better reporting
|
||||
- Added suggestion to use chrome while PDF printing
|
||||
|
||||
|
||||
```
|
||||
```
|
||||
|
||||
### 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)
|
||||
@@ -214,8 +522,6 @@
|
||||
- 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.
|
||||
|
||||
@@ -18,7 +18,7 @@ const Admin = createClass({
|
||||
|
||||
<header>
|
||||
<div className='container'>
|
||||
<i className='fa fa-rocket' />
|
||||
<i className='fas fa-rocket' />
|
||||
homebrewery admin
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -45,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>
|
||||
@@ -59,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>
|
||||
|
||||
@@ -59,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
|
||||
@@ -76,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>
|
||||
|
||||
@@ -61,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,
|
||||
})} />
|
||||
|
||||
@@ -37,7 +37,7 @@ 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>;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ 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');
|
||||
|
||||
@@ -18,12 +19,19 @@ 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,
|
||||
@@ -34,7 +42,7 @@ const BrewRenderer = createClass({
|
||||
usePPR : pages.length >= PPR_THRESHOLD,
|
||||
visibility : 'hidden',
|
||||
initialContent : `<!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='/homebrew/bundle.css' rel='stylesheet' />
|
||||
<base target=_blank>
|
||||
@@ -48,12 +56,19 @@ const BrewRenderer = createClass({
|
||||
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() {
|
||||
@@ -103,12 +118,26 @@ 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(){
|
||||
@@ -158,8 +187,11 @@ const BrewRenderer = createClass({
|
||||
</div>
|
||||
: null}
|
||||
|
||||
<Frame initialContent={this.state.initialContent} style={{ width: '100%', height: '100%', visibility: this.state.visibility }} contentDidMount={this.frameDidMount}>
|
||||
<div className='brewRenderer'
|
||||
<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 }}>
|
||||
|
||||
@@ -170,9 +202,14 @@ const BrewRenderer = createClass({
|
||||
</div>
|
||||
|
||||
<div className='pages' ref='pages'>
|
||||
{/* Apply CSS from Style tab and render pages from Markdown tab */}
|
||||
{this.state.isMounted
|
||||
? this.renderPages()
|
||||
: null}
|
||||
&&
|
||||
<>
|
||||
{this.renderStyle()}
|
||||
{this.renderPages()}
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</Frame>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,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()}
|
||||
|
||||
@@ -4,7 +4,7 @@ const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
const cx = require('classnames'); //Unused variable
|
||||
|
||||
const DISMISS_KEY = 'dismiss_notification7-10-20';
|
||||
const DISMISS_KEY = 'dismiss_notification09-9-21';
|
||||
|
||||
const NotificationPopup = createClass({
|
||||
getInitialState : function() {
|
||||
@@ -22,22 +22,39 @@ const NotificationPopup = createClass({
|
||||
notifications : {
|
||||
psa : function(){
|
||||
return <li key='psa'>
|
||||
<em>Google Drive Integration!</em> <br />
|
||||
We have added Google Drive integration to the Homebrewery! <a target='_blank' href='http://naturalcrit.com/login'>Sign in</a> 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!
|
||||
<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 />
|
||||
However, we are aware that there may be uncaught bugs. We encourage you to copy your brew into a text document before transferring to Google
|
||||
Drive just in case any issues arise as this update is rolled out.
|
||||
<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 />
|
||||
<b>Note:</b> Transferring an existing brew to Google Drive will change the edit and share links of your document. If you have shared your
|
||||
document online, remember to update the links there as well.
|
||||
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 />
|
||||
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!
|
||||
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!
|
||||
@@ -60,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,23 +1,22 @@
|
||||
.popups{
|
||||
position : fixed;
|
||||
top : @navbarHeight;
|
||||
top : @navbarHeight;
|
||||
right : 15px;
|
||||
z-index : 10001;
|
||||
width : 350px;
|
||||
width : 450px;
|
||||
}
|
||||
|
||||
.notificationPopup{
|
||||
position : relative;
|
||||
float : right;
|
||||
position : relative;
|
||||
display : inline-block;
|
||||
width : 350px;
|
||||
width : 100%;
|
||||
padding : 15px;
|
||||
padding-bottom : 10px;
|
||||
padding-left : 55px;
|
||||
padding-left : 25px;
|
||||
background-color : @blue;
|
||||
color : white;
|
||||
a{
|
||||
color : @steel;
|
||||
color : #e0e5c1;
|
||||
font-weight : 800;
|
||||
}
|
||||
i.info{
|
||||
@@ -37,6 +36,9 @@
|
||||
opacity : 1;
|
||||
}
|
||||
}
|
||||
.header {
|
||||
padding-left : 50px;
|
||||
}
|
||||
small{
|
||||
opacity : 0.7;
|
||||
font-size : 0.6em;
|
||||
|
||||
@@ -3,96 +3,169 @@ 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 : ()=>{},
|
||||
showMetaButton : true
|
||||
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}`;
|
||||
@@ -100,41 +173,44 @@ 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}
|
||||
showMetaButton={this.props.showMetaButton} />
|
||||
{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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,8 @@ const MetadataEditor = createClass({
|
||||
tags : '',
|
||||
published : false,
|
||||
authors : [],
|
||||
systems : []
|
||||
systems : [],
|
||||
renderer : 'legacy'
|
||||
},
|
||||
onChange : ()=>{}
|
||||
};
|
||||
@@ -36,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
|
||||
@@ -43,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 {
|
||||
@@ -60,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)}`;
|
||||
},
|
||||
@@ -83,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>;
|
||||
}
|
||||
},
|
||||
@@ -99,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>;
|
||||
@@ -107,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'>
|
||||
@@ -126,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'>
|
||||
@@ -154,6 +196,8 @@ const MetadataEditor = createClass({
|
||||
</div>
|
||||
*/}
|
||||
|
||||
{this.renderAuthors()}
|
||||
|
||||
<div className='field systems'>
|
||||
<label>systems</label>
|
||||
<div className='value'>
|
||||
@@ -161,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,8 @@ 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);
|
||||
@@ -15,11 +16,19 @@ const execute = function(val, brew){
|
||||
const Snippetbar = createClass({
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
brew : '',
|
||||
onInject : ()=>{},
|
||||
onToggle : ()=>{},
|
||||
showmeta : false,
|
||||
showMetaButton : true
|
||||
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,19 +56,29 @@ const Snippetbar = createClass({
|
||||
});
|
||||
},
|
||||
|
||||
renderMetadataButton : function(){
|
||||
if(!this.props.showMetaButton) return;
|
||||
return <div className={cx('snippetBarButton', 'toggleMeta', { selected: this.props.showmeta })}
|
||||
onClick={this.props.onToggle}>
|
||||
<i className='fa fa-info-circle' />
|
||||
<span className='groupName'>Properties</span>
|
||||
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()}
|
||||
{this.renderMetadataButton()}
|
||||
{this.renderEditorButtons()}
|
||||
</div>;
|
||||
}
|
||||
});
|
||||
@@ -67,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(){},
|
||||
};
|
||||
@@ -80,7 +106,7 @@ 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>;
|
||||
});
|
||||
@@ -89,7 +115,7 @@ const SnippetGroup = createClass({
|
||||
render : function(){
|
||||
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'>
|
||||
|
||||
@@ -1,12 +1,40 @@
|
||||
|
||||
.snippetBar{
|
||||
@height : 25px;
|
||||
@menuHeight : 25px;
|
||||
position : relative;
|
||||
height : @height;
|
||||
height : @menuHeight;
|
||||
background-color : #ddd;
|
||||
.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 : @height;
|
||||
line-height : @height;
|
||||
height : @menuHeight;
|
||||
line-height : @menuHeight;
|
||||
display : inline-block;
|
||||
padding : 0px 5px;
|
||||
font-weight : 800;
|
||||
|
||||
@@ -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`;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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
@@ -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`;
|
||||
};
|
||||
@@ -20,6 +20,7 @@ const Homebrew = createClass({
|
||||
changelog : '',
|
||||
version : '0.0.0',
|
||||
account : null,
|
||||
enable_v3 : false,
|
||||
brew : {
|
||||
title : '',
|
||||
text : '',
|
||||
@@ -33,7 +34,7 @@ const Homebrew = createClass({
|
||||
componentWillMount : function() {
|
||||
global.account = this.props.account;
|
||||
global.version = this.props.version;
|
||||
|
||||
global.enable_v3 = this.props.enable_v3;
|
||||
},
|
||||
render : function (){
|
||||
return (
|
||||
@@ -42,12 +43,14 @@ const Homebrew = createClass({
|
||||
<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='/new' exact component={NewPage}/>
|
||||
<Route path='/changelog' exact component={()=><SharePage brew={{ title: 'Changelog', text: this.props.changelog }} />}/>
|
||||
<Route path='/' component={()=><HomePage welcomeText={this.props.welcomeText}/>}/>
|
||||
<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>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
@import 'naturalcrit/styles/core.less';
|
||||
.homebrew{
|
||||
height : 100%;
|
||||
.page{
|
||||
.sitePage{
|
||||
display : flex;
|
||||
height : 100%;
|
||||
background-color : @steel;
|
||||
|
||||
@@ -20,12 +20,12 @@ const Account = createClass({
|
||||
|
||||
render : function(){
|
||||
if(global.account){
|
||||
return <Nav.item href={`/user/${global.account.username}`} color='yellow' icon='fa-user'>
|
||||
return <Nav.item href={`/user/${global.account.username}`} color='yellow' icon='fas fa-user'>
|
||||
{global.account.username}
|
||||
</Nav.item>;
|
||||
}
|
||||
|
||||
return <Nav.item href={`http://naturalcrit.com/login?redirect=${this.state.url}`} color='teal' icon='fa-sign-in'>
|
||||
return <Nav.item href={`https://www.naturalcrit.com/login?redirect=${this.state.url}`} color='teal' icon='fas fa-sign-in-alt'>
|
||||
login
|
||||
</Nav.item>;
|
||||
}
|
||||
|
||||
@@ -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>;
|
||||
};
|
||||
};
|
||||
|
||||
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,5 +1,4 @@
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||
|
||||
module.exports = function(props){
|
||||
@@ -8,7 +7,7 @@ module.exports = function(props){
|
||||
newTab={true}
|
||||
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>;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -80,7 +80,8 @@ const RecentItems = createClass({
|
||||
|
||||
componentDidUpdate : function(prevProps) {
|
||||
if(prevProps.brew && this.props.brew.editId !== prevProps.brew.editId) {
|
||||
if(this.props.storageKey == 'edit') {
|
||||
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}`;
|
||||
@@ -123,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>;
|
||||
@@ -143,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}
|
||||
|
||||
@@ -9,6 +9,7 @@ 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');
|
||||
@@ -30,33 +31,37 @@ const EditPage = createClass({
|
||||
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,
|
||||
saveGoogle : this.props.brew.googleId ? true : false,
|
||||
confirmGoogleTransfer : false,
|
||||
errors : null,
|
||||
htmlErrors : Markdown.validate(this.props.brew.text),
|
||||
url : ''
|
||||
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,
|
||||
@@ -66,6 +71,8 @@ const EditPage = createClass({
|
||||
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){
|
||||
@@ -100,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);
|
||||
|
||||
@@ -121,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(){
|
||||
@@ -136,12 +148,27 @@ const EditPage = createClass({
|
||||
},
|
||||
|
||||
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,
|
||||
@@ -169,22 +196,25 @@ const EditPage = createClass({
|
||||
|
||||
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(this.state.brew)
|
||||
.send(brew)
|
||||
.catch((err)=>{
|
||||
console.log(err.status === 401
|
||||
? 'Not signed in!'
|
||||
: 'Error Saving to Google!');
|
||||
: 'Error Transferring to Google!');
|
||||
this.setState({ errors: err, saveGoogle: false });
|
||||
});
|
||||
|
||||
if(!res) { return; }
|
||||
|
||||
console.log('Deleting Local Copy');
|
||||
await request.delete(`/api/${this.state.brew.editId}`)
|
||||
await request.delete(`/api/${brew.editId}`)
|
||||
.send()
|
||||
.catch((err)=>{
|
||||
console.log('Error deleting Local Copy');
|
||||
@@ -194,13 +224,13 @@ const EditPage = createClass({
|
||||
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/${this.state.brew.editId}`)
|
||||
.send(this.state.brew)
|
||||
.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, saveGoogle: false });
|
||||
this.setState({ errors: err });
|
||||
return;
|
||||
});
|
||||
|
||||
@@ -209,14 +239,14 @@ const EditPage = createClass({
|
||||
} else {
|
||||
if(transfer) {
|
||||
const res = await request.post('/api')
|
||||
.send(this.state.brew)
|
||||
.send(brew)
|
||||
.catch((err)=>{
|
||||
console.log('Error creating Local Copy');
|
||||
this.setState({ errors: err });
|
||||
return;
|
||||
});
|
||||
|
||||
await request.get(`/api/removeGoogle/${this.state.brew.googleId}${this.state.brew.editId}`)
|
||||
await request.get(`/api/removeGoogle/${brew.googleId}${brew.editId}`)
|
||||
.send()
|
||||
.catch((err)=>{
|
||||
console.log('Error Deleting Google Brew');
|
||||
@@ -226,8 +256,8 @@ const EditPage = createClass({
|
||||
history.replaceState(null, null, `/edit/${this.savedBrew.editId}`); //update URL to match doc ID
|
||||
} else {
|
||||
const res = await request
|
||||
.put(`/api/update/${this.state.brew.editId}`)
|
||||
.send(this.state.brew)
|
||||
.put(`/api/update/${brew.editId}`)
|
||||
.send(brew)
|
||||
.catch((err)=>{
|
||||
console.log('Error Updating Local Brew');
|
||||
this.setState({ errors: err });
|
||||
@@ -250,39 +280,44 @@ const EditPage = createClass({
|
||||
},
|
||||
|
||||
renderGoogleDriveIcon : function(){
|
||||
if(this.state.saveGoogle) {
|
||||
return <Nav.item className='googleDriveStorage' onClick={this.handleGoogleClick}>
|
||||
<img src={googleDriveActive} alt='googleDriveActive' />
|
||||
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'>
|
||||
Would you like to transfer this brew from your Google Drive storage back to the Homebrewery?<br />
|
||||
<div className='confirm' onClick={this.toggleGoogleStorage}>
|
||||
Yes
|
||||
</div>
|
||||
<div className='deny'>
|
||||
No
|
||||
</div>
|
||||
{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>
|
||||
}
|
||||
</Nav.item>;
|
||||
} else {
|
||||
return <Nav.item className='googleDriveStorage' onClick={this.handleGoogleClick}>
|
||||
<img src={googleDriveInactive} alt='googleDriveInactive' />
|
||||
<div className='deny'>
|
||||
No
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
{this.state.confirmGoogleTransfer &&
|
||||
<div className='errorContainer'>
|
||||
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
|
||||
{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>
|
||||
}
|
||||
</Nav.item>;
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</Nav.item>;
|
||||
},
|
||||
|
||||
renderSaveButton : function(){
|
||||
@@ -290,18 +325,20 @@ 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){}
|
||||
|
||||
if(this.state.errors.status == '401'){
|
||||
return <Nav.item className='save error' icon='fa-warning'>
|
||||
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={`http://naturalcrit.com/login?redirect=${this.state.url}`}>
|
||||
<div className='confirm' onClick={this.toggleGoogleStorage}>
|
||||
href={`https://www.naturalcrit.com/login?redirect=${this.state.url}`}>
|
||||
<div className='confirm'>
|
||||
Sign In
|
||||
</div>
|
||||
</a>
|
||||
@@ -312,12 +349,33 @@ const EditPage = createClass({
|
||||
</Nav.item>;
|
||||
}
|
||||
|
||||
return <Nav.item className='save error' icon='fa-warning'>
|
||||
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/naturalcrt/naturalcrit/issues/new?body=${encodeURIComponent(errMsg)}`}>
|
||||
href={`https://github.com/naturalcrit/homebrewery/issues/new?body=${encodeURIComponent(errMsg)}`}>
|
||||
here
|
||||
</a>.
|
||||
</div>
|
||||
@@ -325,10 +383,10 @@ 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>;
|
||||
@@ -343,6 +401,16 @@ const EditPage = createClass({
|
||||
|
||||
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>
|
||||
@@ -350,19 +418,21 @@ const EditPage = createClass({
|
||||
<Nav.section>
|
||||
{this.renderGoogleDriveIcon()}
|
||||
{this.renderSaveButton()}
|
||||
<NewBrew />
|
||||
<ReportIssue />
|
||||
<Nav.item newTab={true} href={`/share/${this.processShareId()}`} 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.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()}
|
||||
|
||||
@@ -370,12 +440,13 @@ const EditPage = createClass({
|
||||
<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 : 106px;
|
||||
text-align : center;
|
||||
position : relative;
|
||||
&.saved{
|
||||
cursor : initial;
|
||||
color : #666;
|
||||
@@ -21,17 +27,23 @@
|
||||
margin : -5px;
|
||||
}
|
||||
.errorContainer{
|
||||
animation-name: glideDown;
|
||||
animation-duration: 0.4s;
|
||||
position : absolute;
|
||||
top : 100%;
|
||||
left : 50%;
|
||||
z-index : 1000;
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -23,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 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,15 +50,16 @@ 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>
|
||||
<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 />
|
||||
@@ -65,23 +69,29 @@ const HomePage = createClass({
|
||||
},
|
||||
|
||||
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} showMetaButton={false} 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
@@ -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,3 +1,4 @@
|
||||
/*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/
|
||||
require('./newPage.less');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
@@ -16,36 +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 : {
|
||||
gDrive : false,
|
||||
brew : {
|
||||
text : '',
|
||||
style : undefined,
|
||||
shareId : null,
|
||||
editId : null,
|
||||
createdAt : null,
|
||||
updatedAt : null,
|
||||
gDrive : false,
|
||||
|
||||
title : '',
|
||||
description : '',
|
||||
tags : '',
|
||||
published : false,
|
||||
authors : [],
|
||||
systems : []
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
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'
|
||||
},
|
||||
|
||||
text : '',
|
||||
isSaving : false,
|
||||
saveGoogle : (global.account && global.account.googleId ? true : false),
|
||||
errors : []
|
||||
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() {
|
||||
@@ -68,18 +108,50 @@ const NewPage = createClass({
|
||||
this.refs.editor.update();
|
||||
},
|
||||
|
||||
handleMetadataChange : function(metadata){
|
||||
handleTextChange : function(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);
|
||||
},
|
||||
|
||||
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({
|
||||
metadata : _.merge({}, this.state.metadata, metadata)
|
||||
errors : null,
|
||||
isSaving : false
|
||||
|
||||
});
|
||||
},
|
||||
|
||||
handleTextChange : function(text){
|
||||
clearErrors : function(){
|
||||
this.setState({
|
||||
text : text,
|
||||
errors : Markdown.validate(text)
|
||||
errors : null,
|
||||
isSaving : false
|
||||
|
||||
});
|
||||
localStorage.setItem(KEY, text);
|
||||
},
|
||||
|
||||
save : async function(){
|
||||
@@ -89,26 +161,36 @@ const NewPage = createClass({
|
||||
|
||||
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(_.merge({}, this.state.metadata, { text: this.state.text }))
|
||||
.send(brew)
|
||||
.catch((err)=>{
|
||||
console.log(err.status === 401
|
||||
? 'Not signed in!'
|
||||
: 'Error Creating New Google Brew!');
|
||||
this.setState({ isSaving: false });
|
||||
this.setState({ isSaving: false, errors: err });
|
||||
return;
|
||||
});
|
||||
|
||||
const brew = res.body;
|
||||
localStorage.removeItem(KEY);
|
||||
brew = res.body;
|
||||
localStorage.removeItem(BREWKEY);
|
||||
localStorage.removeItem(STYLEKEY);
|
||||
localStorage.removeItem(METAKEY);
|
||||
window.location = `/edit/${brew.googleId}${brew.editId}`;
|
||||
} else {
|
||||
request.post('/api')
|
||||
.send(_.merge({}, this.state.metadata, {
|
||||
text : this.state.text
|
||||
}))
|
||||
.send(brew)
|
||||
.end((err, res)=>{
|
||||
if(err){
|
||||
this.setState({
|
||||
@@ -117,33 +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>;
|
||||
},
|
||||
@@ -152,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>
|
||||
@@ -166,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ 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({
|
||||
@@ -11,7 +12,9 @@ const PrintPage = createClass({
|
||||
return {
|
||||
query : {},
|
||||
brew : {
|
||||
text : '',
|
||||
text : '',
|
||||
style : '',
|
||||
renderer : 'legacy'
|
||||
}
|
||||
};
|
||||
},
|
||||
@@ -33,18 +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>;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
@@ -49,8 +57,30 @@ const SharePage = createClass({
|
||||
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>
|
||||
@@ -58,17 +88,22 @@ const SharePage = createClass({
|
||||
</Nav.section>
|
||||
|
||||
<Nav.section>
|
||||
<PrintLink shareId={this.processShareId()} />
|
||||
<Nav.item href={`/source/${this.processShareId()}`} 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -48,7 +48,7 @@ const BrewItem = createClass({
|
||||
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>;
|
||||
},
|
||||
|
||||
@@ -61,7 +61,7 @@ const BrewItem = createClass({
|
||||
}
|
||||
|
||||
return <a href={`/edit/${editLink}`} target='_blank' rel='noopener noreferrer'>
|
||||
<i className='fa fa-pencil' />
|
||||
<i className='fas fa-pencil-alt' title='Edit' />
|
||||
</a>;
|
||||
},
|
||||
|
||||
@@ -74,7 +74,20 @@ const BrewItem = createClass({
|
||||
}
|
||||
|
||||
return <a href={`/share/${shareLink}`} target='_blank' rel='noopener noreferrer'>
|
||||
<i className='fa fa-share-alt' />
|
||||
<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>;
|
||||
},
|
||||
|
||||
@@ -88,27 +101,37 @@ const BrewItem = createClass({
|
||||
|
||||
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}
|
||||
</span>
|
||||
<span>
|
||||
<i className='fa fa-refresh' /> {moment(brew.updatedAt).fromNow()}
|
||||
<i className='fas fa-sync-alt' /> {moment(brew.updatedAt).fromNow()}
|
||||
</span>
|
||||
{this.renderGoogleDriveIcon()}
|
||||
<br />
|
||||
<span title={`Authors:\n${brew.authors.join('\n')}`}>
|
||||
<i className='fas fa-user'/> {brew.authors.join(', ')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className='links'>
|
||||
{this.renderShareLink()}
|
||||
{this.renderEditLink()}
|
||||
{this.renderDownloadLink()}
|
||||
{this.renderDeleteBrewLink()}
|
||||
</div>
|
||||
</div>;
|
||||
|
||||
@@ -7,24 +7,31 @@
|
||||
box-sizing : border-box;
|
||||
overflow : hidden;
|
||||
width : 48%;
|
||||
min-height : 80px;
|
||||
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{
|
||||
display : float;
|
||||
margin-right : 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,11 +4,14 @@ 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 = {
|
||||
@@ -23,7 +26,14 @@ const UserPage = createClass({
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
username : '',
|
||||
brews : []
|
||||
brews : [],
|
||||
};
|
||||
},
|
||||
getInitialState : function() {
|
||||
return {
|
||||
sortType : 'alpha',
|
||||
sortDir : 'asc',
|
||||
filterString : ''
|
||||
};
|
||||
},
|
||||
getUsernameWithS : function() {
|
||||
@@ -35,15 +45,111 @@ const UserPage = createClass({
|
||||
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');
|
||||
});
|
||||
},
|
||||
@@ -51,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.getUsernameWithS()} brews</h1>
|
||||
{this.renderSortOptions()}
|
||||
<div className='published'>
|
||||
<h1>{this.getUsernameWithS()} published brews</h1>
|
||||
{this.renderBrews(brews.published)}
|
||||
</div>
|
||||
<div>
|
||||
<h1>{this.getUsernameWithS()} 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 327 B |
|
Before Width: | Height: | Size: 530 B |
@@ -3,17 +3,17 @@ module.exports = async(name, title = '', props = {})=>{
|
||||
<!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>
|
||||
<link href=${`/${name}/bundle.css`} rel='stylesheet' />
|
||||
<link rel="icon" href="/assets/homebrew/favicon.ico" type="image/x-icon" />
|
||||
<title>${title.length ? `${title} - The Homebrewery`: 'The Homebrewery - NaturalCrit'}</title>
|
||||
</head>
|
||||
<body>
|
||||
<main id="reactRoot">${require(`../build/${name}/ssr.js`)(props)}</main>
|
||||
<script src=${`/${name}/bundle.js`}></script>
|
||||
<script>start_app(${JSON.stringify(props)})</script>
|
||||
</body>
|
||||
<script src=${`/${name}/bundle.js`}></script>
|
||||
<script>start_app(${JSON.stringify(props)})</script>
|
||||
</html>
|
||||
`;
|
||||
};
|
||||
};
|
||||
|
||||
12830
package-lock.json
generated
49
package.json
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "homebrewery",
|
||||
"description": "Create authentic looking D&D homebrews using only markdown",
|
||||
"version": "2.10.7",
|
||||
"version": "3.0.0",
|
||||
"engines": {
|
||||
"node": "12.16.x"
|
||||
"node": "14.15.x"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -35,43 +35,48 @@
|
||||
},
|
||||
"babel": {
|
||||
"presets": [
|
||||
"env",
|
||||
"react"
|
||||
"@babel/preset-env",
|
||||
"@babel/preset-react"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.12.10",
|
||||
"@babel/preset-env": "^7.12.11",
|
||||
"@babel/preset-react": "^7.12.10",
|
||||
"@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.59.2",
|
||||
"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": "9.1.0",
|
||||
"googleapis": "67.0.0",
|
||||
"fs-extra": "10.0.0",
|
||||
"googleapis": "85.0.0",
|
||||
"jwt-simple": "^0.5.6",
|
||||
"less": "^3.13.1",
|
||||
"lodash": "^4.17.20",
|
||||
"marked": "^0.3.19",
|
||||
"lodash": "^4.17.21",
|
||||
"marked": "3.0.3",
|
||||
"markedLegacy": "npm:marked@^0.3.19",
|
||||
"moment": "^2.29.1",
|
||||
"mongoose": "^5.11.13",
|
||||
"nanoid": "3.1.20",
|
||||
"nconf": "^0.11.1",
|
||||
"mongoose": "^5.13.7",
|
||||
"nanoid": "3.1.25",
|
||||
"nconf": "^0.11.3",
|
||||
"prop-types": "15.7.2",
|
||||
"query-string": "6.13.8",
|
||||
"query-string": "7.0.1",
|
||||
"react": "^16.14.0",
|
||||
"react-dom": "^16.14.0",
|
||||
"react-frame-component": "4.1.3",
|
||||
"react-router-dom": "5.2.0",
|
||||
"react-router-dom": "5.3.0",
|
||||
"sanitize-filename": "1.6.3",
|
||||
"superagent": "^6.1.0",
|
||||
"vitreum": "github:calculuschild/vitreum#21a8e1c9421f1d3a3b474c12f480feb2fbd28c5b"
|
||||
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^7.18.0",
|
||||
"eslint-plugin-react": "^7.22.0",
|
||||
"pico-check": "^2.0.3"
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-plugin-react": "^7.25.1",
|
||||
"pico-check": "^2.1.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
# Notes
|
||||
User-agent: *
|
||||
Disallow: /edit/
|
||||
|
||||
|
||||
@@ -14,10 +14,10 @@ const transforms = {
|
||||
};
|
||||
|
||||
const build = async ({ bundle, render, ssr })=>{
|
||||
await fs.outputFile('./build/admin/bundle.css', await lessTransform.generate({ paths: './shared' }));
|
||||
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);
|
||||
await fs.outputFile('./build/admin/render.js', render);
|
||||
};
|
||||
|
||||
fs.emptyDirSync('./build/admin');
|
||||
|
||||
@@ -7,9 +7,14 @@ 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 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')
|
||||
};
|
||||
@@ -19,15 +24,41 @@ const build = async ({ bundle, render, ssr })=>{
|
||||
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.outputFile('./build/homebrew/render.js', render);
|
||||
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
|
||||
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));
|
||||
//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/homebrew');
|
||||
fs.emptyDirSync('./build');
|
||||
pack('./client/homebrew/homebrew.jsx', {
|
||||
paths : ['./shared'],
|
||||
libs : Proj.libs,
|
||||
@@ -42,6 +73,6 @@ pack('./client/homebrew/homebrew.jsx', {
|
||||
if(isDev){
|
||||
livereload('./build');
|
||||
watchFile('./server.js', {
|
||||
watch : ['./homebrew'] // Watch additional folders if you want
|
||||
watch : ['./client'] // Watch additional folders if you want
|
||||
});
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"classnames",
|
||||
"codemirror",
|
||||
"codemirror/mode/gfm/gfm.js",
|
||||
"codemirror/mode/css/css.js",
|
||||
"codemirror/mode/javascript/javascript.js",
|
||||
"moment",
|
||||
"superagent",
|
||||
|
||||
309
server.js
@@ -1,18 +1,60 @@
|
||||
/*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/
|
||||
const _ = require('lodash');
|
||||
const jwt = require('jwt-simple');
|
||||
const expressStaticGzip = require('express-static-gzip');
|
||||
const express = require('express');
|
||||
const app = express();
|
||||
|
||||
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');
|
||||
|
||||
// Serve brotli-compressed static files if available
|
||||
app.use('/', expressStaticGzip(`${__dirname}/build`, {
|
||||
enableBrotli : true,
|
||||
orderPreference : ['br'],
|
||||
index : false
|
||||
}));
|
||||
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);
|
||||
|
||||
@@ -30,7 +72,7 @@ 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.');
|
||||
@@ -57,9 +99,10 @@ app.use((req, res, next)=>{
|
||||
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 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 welcomeTextV3 = require('fs').readFileSync('./client/homebrew/pages/homePage/welcome_msg_v3.md', 'utf8');
|
||||
const changelogText = require('fs').readFileSync('./changelog.md', 'utf8');
|
||||
|
||||
String.prototype.replaceAll = function(s, r){return this.split(s).join(r);};
|
||||
|
||||
@@ -68,160 +111,143 @@ app.get('/robots.txt', (req, res)=>{
|
||||
return res.sendFile(`${__dirname}/robots.txt`);
|
||||
});
|
||||
|
||||
//Source page
|
||||
app.get('/source/:id', (req, res)=>{
|
||||
if(req.params.id.length > 12) {
|
||||
const googleId = req.params.id.slice(0, -12);
|
||||
const shareId = req.params.id.slice(-12);
|
||||
GoogleActions.readFileMetadata(config.get('google_api_key'), googleId, shareId, 'share')
|
||||
.then((brew)=>{
|
||||
const text = brew.text.replaceAll('<', '<').replaceAll('>', '>');
|
||||
return res.send(`<code><pre style="white-space: pre-wrap;">${text}</pre></code>`);
|
||||
})
|
||||
.catch((err)=>{
|
||||
console.log(err);
|
||||
return res.status(400).send('Can\'t get brew from Google');
|
||||
});
|
||||
} else {
|
||||
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>`);
|
||||
})
|
||||
.catch((err)=>{
|
||||
console.log(err);
|
||||
return res.status(404).send('Could not find Homebrew with that id');
|
||||
});
|
||||
}
|
||||
//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
|
||||
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 fullAccess = req.account && (req.account.username == req.params.username);
|
||||
const ownAccount = req.account && (req.account.username == req.params.username);
|
||||
|
||||
let googleBrews = [];
|
||||
|
||||
if(req.account && req.account.googleId){
|
||||
googleBrews = await GoogleActions.listGoogleBrews(req, res)
|
||||
.catch((err)=>{
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
|
||||
const brews = await HomebrewModel.getByUser(req.params.username, fullAccess)
|
||||
let brews = await HomebrewModel.getByUser(req.params.username, ownAccount)
|
||||
.catch((err)=>{
|
||||
console.log(err);
|
||||
});
|
||||
|
||||
if(googleBrews) {
|
||||
req.brews = _.concat(brews, googleBrews);
|
||||
} else {req.brews = brews;}
|
||||
if(ownAccount && req?.account?.googleId){
|
||||
const googleBrews = await GoogleActions.listGoogleBrews(req, res)
|
||||
.catch((err)=>{
|
||||
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', (req, res, next)=>{
|
||||
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.
|
||||
if(req.params.id.length > 12) {
|
||||
const googleId = req.params.id.slice(0, -12);
|
||||
const editId = req.params.id.slice(-12);
|
||||
GoogleActions.readFileMetadata(config.get('google_api_key'), googleId, editId, 'edit')
|
||||
.then((brew)=>{
|
||||
req.brew = brew; //TODO Need to sanitize later
|
||||
return next();
|
||||
})
|
||||
.catch((err)=>{
|
||||
console.log(err);
|
||||
return res.status(400).send('Can\'t get brew from Google');
|
||||
});
|
||||
} else {
|
||||
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`);
|
||||
});
|
||||
}
|
||||
});
|
||||
const brew = await getBrewFromId(req.params.id, 'edit');
|
||||
req.brew = brew;
|
||||
return next();
|
||||
}));
|
||||
|
||||
//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)=>{
|
||||
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);
|
||||
GoogleActions.readFileMetadata(config.get('google_api_key'), googleId, shareId, 'share')
|
||||
.then((brew)=>{
|
||||
GoogleActions.increaseView(googleId, shareId, 'share', brew);
|
||||
return brew;
|
||||
})
|
||||
.then((brew)=>{
|
||||
req.brew = brew; //TODO Need to sanitize later
|
||||
return next();
|
||||
})
|
||||
.catch((err)=>{
|
||||
console.log(err);
|
||||
return res.status(400).send('Can\'t get brew from Google');
|
||||
});
|
||||
await GoogleActions.increaseView(googleId, shareId, 'share', brew)
|
||||
.catch((err)=>{next(err);});
|
||||
} else {
|
||||
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`);
|
||||
});
|
||||
await HomebrewModel.increaseView({ shareId: brew.shareId });
|
||||
}
|
||||
});
|
||||
|
||||
req.brew = brew;
|
||||
return next();
|
||||
}));
|
||||
|
||||
//Print Page
|
||||
app.get('/print/:id', (req, res, next)=>{
|
||||
if(req.params.id.length > 12) {
|
||||
const googleId = req.params.id.slice(0, -12);
|
||||
const shareId = req.params.id.slice(-12);
|
||||
GoogleActions.readFileMetadata(config.get('google_api_key'), googleId, shareId, 'share')
|
||||
.then((brew)=>{
|
||||
req.brew = brew; //TODO Need to sanitize later
|
||||
return next();
|
||||
})
|
||||
.catch((err)=>{
|
||||
console.log(err);
|
||||
return res.status(400).send('Can\'t get brew from Google');
|
||||
});
|
||||
} else {
|
||||
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 the page
|
||||
//const render = require('.build/render');
|
||||
const templateFn = require('./client/template.js');
|
||||
app.use((req, res)=>{
|
||||
const props = {
|
||||
version : require('./package.json').version,
|
||||
url : req.originalUrl,
|
||||
welcomeText : welcomeText,
|
||||
changelog : changelogText,
|
||||
brew : req.brew,
|
||||
brews : req.brews,
|
||||
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); })
|
||||
@@ -231,6 +257,29 @@ app.use((req, res)=>{
|
||||
});
|
||||
});
|
||||
|
||||
//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 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);
|
||||
|
||||
@@ -17,7 +17,7 @@ GoogleActions = {
|
||||
if(!account || !account.googleId){ // If not signed into Google
|
||||
const err = new Error('Not Signed In');
|
||||
err.status = 401;
|
||||
throw err;
|
||||
throw (err);
|
||||
}
|
||||
|
||||
const oAuth2Client = new google.auth.OAuth2(
|
||||
@@ -60,6 +60,7 @@ GoogleActions = {
|
||||
.catch((err)=>{
|
||||
console.log('Error searching Google Drive Folders');
|
||||
console.error(err);
|
||||
throw (err);
|
||||
});
|
||||
|
||||
let folderId;
|
||||
@@ -69,8 +70,9 @@ GoogleActions = {
|
||||
resource : fileMetadata
|
||||
})
|
||||
.catch((err)=>{
|
||||
console.log('Error creating google app folder');
|
||||
console.log('Error creating Google Drive folder');
|
||||
console.error(err);
|
||||
throw (err);
|
||||
});
|
||||
|
||||
folderId = obj.data.id;
|
||||
@@ -95,11 +97,14 @@ GoogleActions = {
|
||||
|
||||
const obj = await drive.files.list({
|
||||
pageSize : 100,
|
||||
fields : 'nextPageToken, files(id, name, description, modifiedTime, properties)',
|
||||
fields : 'nextPageToken, files(id, name, description, createdTime, modifiedTime, properties)',
|
||||
q : 'mimeType != \'application/vnd.google-apps.folder\' and trashed = false'
|
||||
})
|
||||
.catch((err)=>{
|
||||
return console.error(`Error Listing Google Brews: ${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) {
|
||||
@@ -108,24 +113,23 @@ GoogleActions = {
|
||||
|
||||
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,
|
||||
|
||||
title : file.properties.title,
|
||||
description : file.description,
|
||||
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 : []
|
||||
};
|
||||
});
|
||||
|
||||
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;
|
||||
},
|
||||
|
||||
@@ -135,7 +139,7 @@ GoogleActions = {
|
||||
const result = await drive.files.get({ fileId: id })
|
||||
.catch((err)=>{
|
||||
console.log('error checking file exists...');
|
||||
console.log(err);
|
||||
console.error(err);
|
||||
return false;
|
||||
});
|
||||
|
||||
@@ -150,22 +154,28 @@ GoogleActions = {
|
||||
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,
|
||||
lastViewed : brew.lastViewed,
|
||||
views : brew.views,
|
||||
version : brew.version,
|
||||
tags : brew.tags,
|
||||
systems : brew.systems.join() }
|
||||
},
|
||||
media : { mimeType : 'text/plain',
|
||||
body : brew.text }
|
||||
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');
|
||||
});
|
||||
}
|
||||
@@ -188,10 +198,12 @@ GoogleActions = {
|
||||
'description' : `${brew.description}`,
|
||||
'parents' : [folderId],
|
||||
'properties' : { //AppProperties is not accessible
|
||||
'shareId' : nanoid(12),
|
||||
'editId' : nanoid(12),
|
||||
'title' : brew.title,
|
||||
'views' : '0'
|
||||
'shareId' : nanoid(12),
|
||||
'editId' : nanoid(12),
|
||||
'title' : brew.title,
|
||||
'views' : '0',
|
||||
'pageCount' : brew.pageCount,
|
||||
'renderer' : brew.renderer || 'legacy'
|
||||
}
|
||||
};
|
||||
|
||||
@@ -200,8 +212,9 @@ GoogleActions = {
|
||||
media : media
|
||||
})
|
||||
.catch((err)=>{
|
||||
console.log('Error while creating new Google brew');
|
||||
console.error(err);
|
||||
return res.status(500).send('Error while creating google brew');
|
||||
throw (err);
|
||||
});
|
||||
|
||||
if(!obj) return;
|
||||
@@ -225,11 +238,13 @@ GoogleActions = {
|
||||
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 : []
|
||||
};
|
||||
@@ -238,15 +253,16 @@ GoogleActions = {
|
||||
},
|
||||
|
||||
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'
|
||||
fields : 'properties, createdTime, modifiedTime, description, trashed'
|
||||
})
|
||||
.catch((err)=>{
|
||||
console.log('Error loading from Google');
|
||||
console.error(err);
|
||||
throw (err);
|
||||
return;
|
||||
});
|
||||
|
||||
@@ -257,8 +273,12 @@ GoogleActions = {
|
||||
throw ('Share ID does not match');
|
||||
}
|
||||
|
||||
//Access actual file with service account. Just api key is causing "automated query" errors.
|
||||
const keys = JSON.parse(config.get('service_account'));
|
||||
//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'];
|
||||
|
||||
@@ -285,12 +305,15 @@ GoogleActions = {
|
||||
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
|
||||
@@ -337,7 +360,10 @@ GoogleActions = {
|
||||
increaseView : async (id, accessId, accessType, brew)=>{
|
||||
//service account because this is modifying another user's file properties
|
||||
//so we need extended scope
|
||||
const keys = JSON.parse(config.get('service_account'));
|
||||
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'];
|
||||
|
||||
@@ -345,8 +371,13 @@ GoogleActions = {
|
||||
|
||||
await drive.files.update({
|
||||
fileId : brew.googleId,
|
||||
resource : { properties : { views : brew.views + 1,
|
||||
lastViewed : new Date() } }
|
||||
resource : {
|
||||
modifiedTime : brew.updatedAt,
|
||||
properties : {
|
||||
views : brew.views + 1,
|
||||
lastViewed : new Date()
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((err)=>{
|
||||
console.log('Error updating Google views');
|
||||
|
||||
@@ -11,19 +11,43 @@ const Markdown = require('../shared/naturalcrit/markdown.js');
|
||||
// });
|
||||
// };
|
||||
|
||||
const MAX_TITLE_LENGTH = 100;
|
||||
|
||||
const getGoodBrewTitle = (text)=>{
|
||||
const tokens = Markdown.marked.lexer(text);
|
||||
return title = (tokens.find((token)=>token.type == 'heading' || token.type == 'paragraph') || { text: 'No Title' }).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;
|
||||
};
|
||||
|
||||
const newBrew = (req, res)=>{
|
||||
const brew = req.body;
|
||||
brew.authors = (req.account) ? [req.account.username] : [];
|
||||
|
||||
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;
|
||||
@@ -49,9 +73,12 @@ const newBrew = (req, res)=>{
|
||||
const updateBrew = (req, res)=>{
|
||||
HomebrewModel.get({ editId: req.params.id })
|
||||
.then((brew)=>{
|
||||
brew = _.merge(brew, req.body);
|
||||
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(req.body.text);
|
||||
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();
|
||||
@@ -110,23 +137,26 @@ const newGoogleBrew = async (req, res, next)=>{
|
||||
try { oAuth2Client = GoogleActions.authCheck(req.account, res); } catch (err) { return res.status(err.status).send(err.message); }
|
||||
|
||||
const brew = req.body;
|
||||
brew.authors = (req.account) ? [req.account.username] : [];
|
||||
|
||||
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;
|
||||
|
||||
console.log(oAuth2Client);
|
||||
|
||||
const newBrew = await GoogleActions.newGoogleBrew(oAuth2Client, brew);
|
||||
|
||||
return res.status(200).send(newBrew);
|
||||
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)=>{
|
||||
@@ -134,9 +164,15 @@ const updateGoogleBrew = async (req, res, next)=>{
|
||||
|
||||
try { oAuth2Client = GoogleActions.authCheck(req.account, res); } catch (err) { return res.status(err.status).send(err.message); }
|
||||
|
||||
const updatedBrew = await GoogleActions.updateGoogleBrew(oAuth2Client, req.body);
|
||||
const brew = excludePropsFromUpdate(req.body);
|
||||
brew.text = mergeBrewText(brew.text, brew.style);
|
||||
|
||||
return res.status(200).send(updatedBrew);
|
||||
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);
|
||||
|
||||
@@ -4,15 +4,17 @@ const _ = require('lodash');
|
||||
const zlib = require('zlib');
|
||||
|
||||
const HomebrewSchema = mongoose.Schema({
|
||||
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 },
|
||||
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,28 +25,15 @@ 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;
|
||||
}
|
||||
return brew;
|
||||
};
|
||||
|
||||
HomebrewSchema.methods.increaseView = async function(){
|
||||
this.lastViewed = new Date();
|
||||
this.views = this.views + 1;
|
||||
const text = this.text;
|
||||
this.text = undefined;
|
||||
await this.save()
|
||||
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;
|
||||
});
|
||||
this.text = text;
|
||||
return this;
|
||||
return brew;
|
||||
};
|
||||
|
||||
HomebrewSchema.statics.get = function(query){
|
||||
@@ -55,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]);
|
||||
});
|
||||
});
|
||||
@@ -66,11 +57,9 @@ 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);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
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;
|
||||
@@ -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>
|
||||
|
||||
@@ -11,41 +11,61 @@ 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');
|
||||
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 });
|
||||
@@ -53,41 +73,46 @@ const CodeEditor = createClass({
|
||||
},
|
||||
|
||||
makeItalic : function() {
|
||||
const selection = this.codeMirror.getSelection();
|
||||
this.codeMirror.replaceSelection(`*${selection}*`, 'around');
|
||||
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 });
|
||||
}
|
||||
},
|
||||
|
||||
componentWillReceiveProps : function(nextProps){
|
||||
if(this.codeMirror && nextProps.value !== undefined && this.codeMirror.getValue() != nextProps.value) {
|
||||
this.codeMirror.setValue(nextProps.value);
|
||||
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 });
|
||||
}
|
||||
},
|
||||
|
||||
shouldComponentUpdate : function(nextProps, nextState) {
|
||||
return false;
|
||||
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,423 @@ 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] == '#') {
|
||||
@@ -79,7 +497,6 @@ const escape = function (html, encode) {
|
||||
return html.replace(escapeReplaceNoEncode, getEscapeReplacement);
|
||||
}
|
||||
}
|
||||
|
||||
return html;
|
||||
};
|
||||
|
||||
@@ -95,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 }
|
||||
|
||||
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;
|
||||
},
|
||||
};
|
||||
@@ -17,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>
|
||||
@@ -50,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']);
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//@import (less) 'naturalcrit/styles/style.fonts.css';
|
||||
nav{
|
||||
background-color : #333;
|
||||
.navContent{
|
||||
position : relative;
|
||||
display : flex;
|
||||
justify-content : space-between;
|
||||
}
|
||||
|
||||
@@ -56,9 +56,9 @@ const SplitPane = createClass({
|
||||
renderDivider : function(){
|
||||
return <div className='divider' onMouseDown={this.handleDown} >
|
||||
<div className='dots'>
|
||||
<i className='fa fa-circle' />
|
||||
<i className='fa fa-circle' />
|
||||
<i className='fa fa-circle' />
|
||||
<i className='fas fa-circle' />
|
||||
<i className='fas fa-circle' />
|
||||
<i className='fas fa-circle' />
|
||||
</div>
|
||||
</div>;
|
||||
},
|
||||
|
||||
790
themes/5ePhb.style.less
Normal file
@@ -0,0 +1,790 @@
|
||||
@import (less) './themes/fonts/5e/fonts.less';
|
||||
@import (less) './themes/assets/assets.less';
|
||||
|
||||
//Colors
|
||||
@background : #EEE5CE; // Light parchment
|
||||
@noteGreen : #e0e5c1; // Pastel green
|
||||
@headerUnderline : #c9ad6a; // Gold
|
||||
@horizontalRule : #9c2b1b; // Maroon
|
||||
@headerText : #58180D; // Dark maroon
|
||||
@monsterStatBackground : #EEDBAB; // Light orange parchment
|
||||
@captionText : #766649; // Brown
|
||||
@watercolorStain : #BBAD82; // Light brown
|
||||
@page { margin: 0; }
|
||||
body {
|
||||
counter-reset : phb-page-numbers;
|
||||
}
|
||||
*{
|
||||
-webkit-print-color-adjust : exact;
|
||||
}
|
||||
.useSansSerif(){
|
||||
font-family : ScalySansRemake;
|
||||
font-size : 0.318cm;
|
||||
line-height : 1.2em;
|
||||
p,dl,ul,ol {
|
||||
line-height : 1.2em;
|
||||
}
|
||||
ul, ol {
|
||||
padding-left : 1em;
|
||||
}
|
||||
em{
|
||||
font-style : italic;
|
||||
}
|
||||
strong{
|
||||
font-weight : 800;
|
||||
letter-spacing : -0.02em;
|
||||
}
|
||||
h5 + * {
|
||||
margin-top : 0.1cm;
|
||||
}
|
||||
}
|
||||
.useColumns(@multiplier : 1, @fillMode: balance){
|
||||
column-count : 2;
|
||||
column-fill : @fillMode;
|
||||
column-gap : 0.9cm;
|
||||
column-width : 8cm * @multiplier;
|
||||
-webkit-column-count : 2;
|
||||
-moz-column-count : 2;
|
||||
-webkit-column-width : 8cm * @multiplier;
|
||||
-moz-column-width : 8cm * @multiplier;
|
||||
-webkit-column-gap : 0.9cm;
|
||||
-moz-column-gap : 0.9cm;
|
||||
}
|
||||
.columnWrapper{
|
||||
max-height : 100%;
|
||||
column-span : all;
|
||||
columns : inherit;
|
||||
column-gap : inherit;
|
||||
}
|
||||
.page{
|
||||
.useColumns();
|
||||
counter-increment : phb-page-numbers;
|
||||
position : relative;
|
||||
z-index : 15;
|
||||
box-sizing : border-box;
|
||||
overflow : hidden;
|
||||
height : 279.4mm;
|
||||
width : 215.9mm;
|
||||
background-color : @background;
|
||||
background-image : @backgroundImage;
|
||||
padding : 1.4cm 1.9cm 1.7cm;
|
||||
font-family : BookInsanityRemake;
|
||||
font-size : 0.34cm;
|
||||
text-rendering : optimizeLegibility;
|
||||
page-break-before : always;
|
||||
page-break-after : always;
|
||||
//*****************************
|
||||
// * BASE
|
||||
// *****************************/
|
||||
p{
|
||||
overflow-wrap : break-word; //TODO: MAKE ALL MARGINS TOP-ONLY. USE * + * STYLE SELECTORS
|
||||
display : block;
|
||||
line-height : 1.3em;
|
||||
&+* {
|
||||
margin-top : 0.325cm;
|
||||
}
|
||||
&+p{
|
||||
margin-top : 0;
|
||||
}
|
||||
}
|
||||
ul{
|
||||
margin-bottom : 0.8em;
|
||||
padding-left : 1.4em;
|
||||
line-height : 1.3em;
|
||||
list-style-position : outside;
|
||||
list-style-type : disc;
|
||||
}
|
||||
ol{
|
||||
margin-bottom : 0.8em;
|
||||
padding-left : 1.4em;
|
||||
line-height : 1.3em;
|
||||
list-style-position : outside;
|
||||
list-style-type : decimal;
|
||||
}
|
||||
//Indents after p or lists
|
||||
p+p, ul+p, ol+p{
|
||||
text-indent : 1em;
|
||||
}
|
||||
img{
|
||||
z-index : -1;
|
||||
}
|
||||
strong{
|
||||
font-weight : bold;
|
||||
letter-spacing : -0.02em;
|
||||
}
|
||||
em{
|
||||
font-style : italic;
|
||||
}
|
||||
sup{
|
||||
vertical-align : super;
|
||||
font-size : smaller;
|
||||
line-height : 0;
|
||||
}
|
||||
sub{
|
||||
vertical-align : sub;
|
||||
font-size : smaller;
|
||||
line-height : 0;
|
||||
}
|
||||
//*****************************
|
||||
// * HEADERS
|
||||
// *****************************/
|
||||
h1,h2,h3,h4{
|
||||
font-family : MrEavesRemake;
|
||||
font-weight : 800;
|
||||
color : @headerText;
|
||||
}
|
||||
h1{
|
||||
margin-bottom : 0.18cm; //Margin-bottom only because this is WIDE
|
||||
column-span : all;
|
||||
font-size : 0.89cm;
|
||||
-webkit-column-span : all;
|
||||
-moz-column-span : all;
|
||||
&+p::first-letter{
|
||||
float : left;
|
||||
font-family : SolberaImitationRemake;
|
||||
line-height : 1em;
|
||||
font-size : 3.5cm;
|
||||
padding-left : 40px; //Allow background color to extend into margins
|
||||
margin-left : -40px;
|
||||
margin-top :-0.3cm;
|
||||
padding-bottom :2px;
|
||||
margin-bottom :-20px;
|
||||
background-image : linear-gradient(-45deg, #322814, #998250, #322814);
|
||||
background-clip : text;
|
||||
-webkit-background-clip : text;
|
||||
color : rgba(0, 0, 0, 0);
|
||||
}
|
||||
&+p::first-line{
|
||||
font-variant : small-caps;
|
||||
}
|
||||
}
|
||||
h2{
|
||||
//margin-top : 0px; //Font is misaligned. Shift up slightly
|
||||
//margin-bottom : 0.05cm;
|
||||
font-size : 0.75cm;
|
||||
}
|
||||
h3{
|
||||
//margin-top : -0.1cm; //Font is misaligned. Shift up slightly
|
||||
//margin-bottom : 0.1cm;
|
||||
font-size : 0.575cm;
|
||||
border-bottom : 2px solid @headerUnderline;
|
||||
}
|
||||
h4{
|
||||
//margin-top : -0.02cm; //Font is misaligned. Shift up slightly
|
||||
//margin-bottom : 0.02cm;
|
||||
font-size : 0.458cm;
|
||||
}
|
||||
h5{
|
||||
//margin-top : -0.02cm; //Font is misaligned. Shift up slightly
|
||||
//margin-bottom : 0.02cm;
|
||||
font-family : ScalySansSmallCapsRemake;
|
||||
font-size : 0.423cm;
|
||||
font-weight : 900;
|
||||
& + * {
|
||||
margin-top : 0.2cm;
|
||||
}
|
||||
}
|
||||
//*****************************
|
||||
// * TABLE
|
||||
// *****************************/
|
||||
table{
|
||||
.useSansSerif();
|
||||
width : 100%;
|
||||
& + * {
|
||||
margin-top : 0.325cm;
|
||||
}
|
||||
thead{
|
||||
display: table-row-group;
|
||||
font-weight : 800;
|
||||
th{
|
||||
vertical-align : bottom;
|
||||
padding : 0.14em 0.4em;
|
||||
}
|
||||
}
|
||||
tbody{
|
||||
tr{
|
||||
td{
|
||||
padding : 0.14em 0.4em;
|
||||
}
|
||||
&:nth-child(odd){
|
||||
background-color : @noteGreen;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//*****************************
|
||||
// * NOTE
|
||||
// *****************************/
|
||||
.note{
|
||||
.useSansSerif();
|
||||
background-color : @noteGreen;
|
||||
border-style : solid;
|
||||
border-width : 1px;
|
||||
border-image : @noteBorderImage 12 stretch;
|
||||
border-image-outset : 9px 0px;
|
||||
border-image-width : 11px;
|
||||
padding : 0.13cm 0.16cm;
|
||||
filter : drop-shadow(1px 4px 6px #888);
|
||||
.page :where(&) {
|
||||
margin-top : 9px; //Prevent top border getting cut off on colbreak
|
||||
}
|
||||
& + * {
|
||||
margin-top : 0.45cm;
|
||||
}
|
||||
h5 {
|
||||
font-size : 0.375cm;
|
||||
}
|
||||
p{
|
||||
display : block;
|
||||
padding-bottom : 0px;
|
||||
}
|
||||
p + p {
|
||||
padding-top : .8em;
|
||||
}
|
||||
:last-child {
|
||||
margin-bottom : 0;
|
||||
}
|
||||
}
|
||||
//************************************
|
||||
// * DESCRIPTIVE TEXT BOX
|
||||
// ************************************/
|
||||
.descriptive{
|
||||
.useSansSerif();
|
||||
background-color : #faf7ea;
|
||||
border-style : solid;
|
||||
border-width : 7px;
|
||||
border-image : @descriptiveBoxImage 12 stretch;
|
||||
border-image-outset : 4px;
|
||||
padding : 0.1em;
|
||||
filter : drop-shadow(0 0 3px #faf7ea);
|
||||
.page :where(&) {
|
||||
margin-top : 4px; //Prevent top border getting cut off on colbreak
|
||||
}
|
||||
& + * {
|
||||
margin-top : 0.45cm;
|
||||
}
|
||||
h5 {
|
||||
font-size : 0.375cm;
|
||||
}
|
||||
p{
|
||||
display : block;
|
||||
padding-bottom : 0px;
|
||||
line-height : 1.5em;
|
||||
}
|
||||
p + p {
|
||||
padding-top : .8em;
|
||||
}
|
||||
:last-child {
|
||||
margin-bottom : 0;
|
||||
}
|
||||
}
|
||||
//*****************************
|
||||
// * Images Snippets
|
||||
// *****************************/
|
||||
|
||||
/* Arist Credit */
|
||||
.artist {
|
||||
position : absolute;
|
||||
text-align : center;
|
||||
font-family : WalterTurncoat;
|
||||
font-size : 0.27cm;
|
||||
color : @captionText;
|
||||
p, p + p {
|
||||
margin : unset;
|
||||
text-indent : unset;
|
||||
line-height : 1em;
|
||||
}
|
||||
h5 {
|
||||
font-size : 1.3em;
|
||||
font-family : WalterTurncoat;
|
||||
}
|
||||
a{
|
||||
color : inherit;
|
||||
text-decoration : unset;
|
||||
&:hover {
|
||||
text-decoration : underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Watermark */
|
||||
.watermark {
|
||||
display : grid !important;
|
||||
place-items : center;
|
||||
justify-content : center;
|
||||
position : absolute;
|
||||
top : 0;
|
||||
left : 0;
|
||||
width : 100%;
|
||||
height : 100%;
|
||||
font-size : 120px;
|
||||
text-transform : uppercase;
|
||||
color : black;
|
||||
mix-blend-mode : overlay;
|
||||
opacity : 30%;
|
||||
transform : rotate(-45deg);
|
||||
z-index : 500;
|
||||
p {
|
||||
margin-bottom : none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Watercolor */
|
||||
[class*="watercolor"] {
|
||||
position : absolute;
|
||||
width : 2000px; /* dimensions need to be real big so the user can set */
|
||||
height : 2000px; /* height or width and the image will maintain aspect ratio */
|
||||
-webkit-mask-image : var(--wc);
|
||||
-webkit-mask-size : contain;
|
||||
-webkit-mask-repeat : no-repeat;
|
||||
mask-image : var(--wc);
|
||||
mask-size : contain;
|
||||
mask-repeat : no-repeat;
|
||||
background-size : cover;
|
||||
background-color : @watercolorStain; /*default color*/
|
||||
--wc : @watercolor1; /*default image*/
|
||||
z-index : -2;
|
||||
}
|
||||
|
||||
.watercolor1 { --wc : @watercolor1; }
|
||||
.watercolor2 { --wc : @watercolor2; }
|
||||
.watercolor3 { --wc : @watercolor3; }
|
||||
.watercolor4 { --wc : @watercolor4; }
|
||||
.watercolor5 { --wc : @watercolor5; }
|
||||
.watercolor6 { --wc : @watercolor6; }
|
||||
.watercolor7 { --wc : @watercolor7; }
|
||||
.watercolor8 { --wc : @watercolor8; }
|
||||
.watercolor9 { --wc : @watercolor9; }
|
||||
.watercolor10 { --wc : @watercolor10; }
|
||||
.watercolor11 { --wc : @watercolor11; }
|
||||
.watercolor12 { --wc : @watercolor12; }
|
||||
|
||||
//*****************************
|
||||
// * MONSTER STAT BLOCK
|
||||
// *****************************/
|
||||
.monster {
|
||||
.useSansSerif();
|
||||
&.frame {
|
||||
border-style : solid;
|
||||
border-width : 7px 6px;
|
||||
background-color : @monsterStatBackground;
|
||||
background-image : @monsterBlockBackground;
|
||||
border-image : @monsterBorderImage 14 round;
|
||||
border-image-outset : 0px 2px;
|
||||
background-blend-mode : overlay;
|
||||
background-attachment : fixed;
|
||||
filter : drop-shadow(1px 4px 6px #888);
|
||||
padding : 4px 2px;
|
||||
margin-left : -6px;
|
||||
margin-right : -6px;
|
||||
}
|
||||
|
||||
position : relative;
|
||||
padding : 0px;
|
||||
margin-bottom : 0.325cm;
|
||||
|
||||
p{
|
||||
margin-bottom : 0.3cm;
|
||||
}
|
||||
p+p {
|
||||
margin-top : 0; //May not be needed
|
||||
text-indent : 0;
|
||||
}
|
||||
p:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
//Headers
|
||||
h2{
|
||||
font-size : 0.62cm;
|
||||
line-height : 1em;
|
||||
margin : 0;
|
||||
&+p {
|
||||
font-size : 0.304cm; //Monster size and type subtext
|
||||
margin-bottom : 0;
|
||||
}
|
||||
}
|
||||
h3{
|
||||
font-family : ScalySansRemake;
|
||||
font-weight : 800;
|
||||
font-variant : small-caps;
|
||||
border-bottom : 2px solid @headerText;
|
||||
margin-top : 0.05cm;
|
||||
padding-bottom : 0.05cm;
|
||||
}
|
||||
|
||||
//Triangle dividers
|
||||
hr{
|
||||
visibility : visible;
|
||||
height : 6px;
|
||||
margin : 0.12cm 0cm;
|
||||
background-image : @redTriangleImage;
|
||||
background-size : 100% 100%;
|
||||
border : none;
|
||||
}
|
||||
|
||||
//Attribute Lists
|
||||
dl {
|
||||
color : @headerText;
|
||||
}
|
||||
hr:last-of-type~dl{
|
||||
color : inherit; // After the HRs, hanging indents remain black.
|
||||
}
|
||||
|
||||
// Monster Ability table
|
||||
hr + table:first-of-type{
|
||||
margin : 0;
|
||||
column-span : none;
|
||||
color : @headerText;
|
||||
background-color : transparent;
|
||||
border-style : none;
|
||||
border-image : none;
|
||||
-webkit-column-span : none;
|
||||
tr {
|
||||
background-color : transparent;
|
||||
}
|
||||
td,th {
|
||||
padding: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Full Width
|
||||
.monster.wide{
|
||||
.useColumns(0.96, @fillMode: balance);
|
||||
}
|
||||
|
||||
//*****************************
|
||||
// * FOOTER
|
||||
// *****************************/
|
||||
&:after{
|
||||
content : "";
|
||||
position : absolute;
|
||||
bottom : 0px;
|
||||
left : 0px;
|
||||
z-index : 100;
|
||||
height : 50px;
|
||||
width : 100%;
|
||||
background-image : @footerAccentImage;
|
||||
background-size : cover;
|
||||
}
|
||||
&:nth-child(even){
|
||||
&:after{
|
||||
transform : scaleX(-1);
|
||||
}
|
||||
.pageNumber{
|
||||
left : 2px;
|
||||
}
|
||||
.footnote{
|
||||
left : 80px;
|
||||
text-align : left;
|
||||
}
|
||||
}
|
||||
.pageNumber{
|
||||
position : absolute;
|
||||
right : 2px;
|
||||
bottom : 22px;
|
||||
width : 50px;
|
||||
font-size : 0.9em;
|
||||
color : #c9ad6a;
|
||||
text-align : center;
|
||||
text-indent : 0;
|
||||
&.auto::after {
|
||||
content : counter(phb-page-numbers);
|
||||
}
|
||||
}
|
||||
.footnote{
|
||||
position : absolute;
|
||||
right : 80px;
|
||||
bottom : 32px;
|
||||
z-index : 150;
|
||||
width : 200px;
|
||||
font-size : 0.8em;
|
||||
color : #c9ad6a;
|
||||
text-align : right;
|
||||
}
|
||||
//************************************
|
||||
// * CODE BLOCKS
|
||||
// ************************************/
|
||||
code{
|
||||
font-family : "Courier New", Courier, monospace;
|
||||
font-size : 0.325;
|
||||
padding : 0px 4px;
|
||||
color : #58180d;
|
||||
background-color : #faf7ea;
|
||||
border-radius : 4px;
|
||||
white-space : pre-wrap
|
||||
}
|
||||
|
||||
pre code{
|
||||
width : 100%;
|
||||
display : inline-block;
|
||||
border-style : solid;
|
||||
border-width : 1px;
|
||||
border-image : @codeBorderImage 26 stretch;
|
||||
border-image-width : 10px;
|
||||
border-image-outset : 2px;
|
||||
border-radius : 12px;
|
||||
margin-bottom : 2px;
|
||||
padding : 0.15cm;
|
||||
.page :where(&) {
|
||||
margin-top : 2px; //Prevent top border getting cut off on colbreak
|
||||
}
|
||||
& + * {
|
||||
margin-top : 0.325cm;
|
||||
}
|
||||
}
|
||||
//*****************************
|
||||
// * EXTRAS
|
||||
// *****************************/
|
||||
hr{
|
||||
visibility : hidden;
|
||||
margin : 0px;
|
||||
}
|
||||
.columnSplit {
|
||||
visibility : hidden;
|
||||
-webkit-column-break-after : always;
|
||||
break-after : always;
|
||||
-moz-column-break-after : always;
|
||||
break-before : column;
|
||||
}
|
||||
//Avoid breaking up
|
||||
blockquote,table{
|
||||
z-index : 15;
|
||||
-webkit-column-break-inside : avoid;
|
||||
page-break-inside : avoid;
|
||||
break-inside : avoid;
|
||||
}
|
||||
//Text indent right after table
|
||||
table+p{
|
||||
text-indent : 1em;
|
||||
}
|
||||
// Nested lists
|
||||
ul ul,ol ol,ul ol,ol ul{
|
||||
margin-bottom : 0px;
|
||||
margin-left : 1.5em;
|
||||
}
|
||||
li{
|
||||
-webkit-column-break-inside : avoid;
|
||||
page-break-inside : avoid;
|
||||
break-inside : avoid;
|
||||
}
|
||||
}
|
||||
//*****************************
|
||||
// * SPELL LIST
|
||||
// *****************************/
|
||||
.page .spellList{
|
||||
.useSansSerif();
|
||||
column-count : 2;
|
||||
ul+h5{
|
||||
margin-top : 15px;
|
||||
}
|
||||
p, ul{
|
||||
font-size : 0.352cm;
|
||||
line-height : 1.3em;
|
||||
}
|
||||
ul{
|
||||
margin-bottom : 0.5em;
|
||||
padding-left : 1em;
|
||||
text-indent : -1em;
|
||||
list-style-type : none;
|
||||
-webkit-column-break-inside : auto;
|
||||
page-break-inside : auto;
|
||||
break-inside : auto;
|
||||
}
|
||||
&.wide{
|
||||
column-count : 4;
|
||||
}
|
||||
}
|
||||
|
||||
//*****************************
|
||||
// * CLASS TABLE
|
||||
// *****************************/
|
||||
.page .classTable{
|
||||
th[colspan]:not([rowspan]) {
|
||||
white-space : nowrap;
|
||||
}
|
||||
&.frame {
|
||||
margin-top : 0.66cm;
|
||||
margin-bottom : 1.05cm;
|
||||
margin-left : -0.1cm;
|
||||
margin-right : -0.1cm;
|
||||
border-collapse : separate;
|
||||
background-color : white;
|
||||
border : initial;
|
||||
border-style : solid;
|
||||
border-image-outset : 0.55cm 0.3cm;
|
||||
border-image-repeat : stretch;
|
||||
border-image-slice : 200;
|
||||
border-image-source : @frameBorderImage;
|
||||
border-image-width : 47px;
|
||||
}
|
||||
&.decoration {
|
||||
transform-style : preserve-3d;
|
||||
}
|
||||
&.decoration::before {
|
||||
content :'';
|
||||
position : absolute;
|
||||
background-image : @classTableDecoration;
|
||||
background-size : contain;
|
||||
background-repeat : space;
|
||||
width : 7.75cm;
|
||||
height : calc(100% + 3.3cm);
|
||||
top : 50%;
|
||||
left : 50%;
|
||||
transform : translateY(-50%) translateX(-50%) translateZ(-1px);
|
||||
filter : drop-shadow(0px 0px 1px #C8C5C080)
|
||||
}
|
||||
&.decoration.wide::before {
|
||||
width : calc(100% + 3.3cm);
|
||||
height : 7.75cm;
|
||||
top : calc(50% + 0.4cm);
|
||||
}
|
||||
h5 + table{
|
||||
margin-top : 0.2cm;
|
||||
}
|
||||
}
|
||||
//*****************************
|
||||
// * TABLE OF CONTENTS
|
||||
// *****************************/
|
||||
.page .toc{
|
||||
-webkit-column-break-inside : avoid;
|
||||
page-break-inside : avoid;
|
||||
break-inside : avoid;
|
||||
h1 {
|
||||
text-align : center;
|
||||
margin-bottom : 0cm;
|
||||
}
|
||||
a{
|
||||
display : table;
|
||||
color : inherit;
|
||||
text-decoration : none;
|
||||
&:hover{
|
||||
text-decoration : underline;
|
||||
}
|
||||
}
|
||||
h4 {
|
||||
margin-top : 0.14cm;
|
||||
& + ul li {
|
||||
line-height: 1.2em;
|
||||
}
|
||||
}
|
||||
& > ul {
|
||||
margin-top: 0.52cm;
|
||||
}
|
||||
ul{
|
||||
padding-left : 0;
|
||||
list-style-type : none;
|
||||
li + li h3 {
|
||||
margin-top : 0.26cm;
|
||||
line-height : 1em
|
||||
}
|
||||
h3 span:first-child::after {
|
||||
border : none;
|
||||
}
|
||||
span {
|
||||
display : table-cell;
|
||||
&:first-child {
|
||||
position : relative;
|
||||
overflow : hidden;
|
||||
&::after {
|
||||
content : "";
|
||||
position : absolute;
|
||||
bottom : 0.08cm;
|
||||
margin-left : 0.06cm; /* Spacing before dot leaders */
|
||||
width : 100%;
|
||||
border-bottom : 0.05cm dotted #000;
|
||||
}
|
||||
}
|
||||
&:last-child {
|
||||
font-family : BookInsanityRemake;
|
||||
font-size : 0.34cm;
|
||||
font-weight : normal;
|
||||
color : black;
|
||||
text-align : right;
|
||||
vertical-align : bottom; /* Keep page number bottom-aligned */
|
||||
width : 1%;
|
||||
padding-left : 0.06cm; /* Spacing after dot leaders */
|
||||
/*white-space : nowrap; /* Uncomment if needed */
|
||||
}
|
||||
}
|
||||
ul { /*List indent*/
|
||||
margin-left : 1em;
|
||||
}
|
||||
}
|
||||
&.wide{
|
||||
.useColumns(0.96, @fillMode: balance);
|
||||
}
|
||||
}
|
||||
|
||||
//*****************************
|
||||
// * MUSTACHE DIVS/SPANS
|
||||
// *****************************/
|
||||
.page {
|
||||
.block {
|
||||
break-inside : avoid;
|
||||
display : inline-block;
|
||||
min-width : 100%;
|
||||
//-webkit-transform : translateZ(0); //Prevents shadows from breaking across columns
|
||||
}
|
||||
.inline-block {
|
||||
display : inline-block;
|
||||
text-indent : initial;
|
||||
}
|
||||
}
|
||||
|
||||
//*****************************
|
||||
// * DEFINITION LISTS
|
||||
// *****************************/
|
||||
.page {
|
||||
dl {
|
||||
line-height : 1.3em;
|
||||
padding-left : 1em;
|
||||
white-space : pre-line;
|
||||
& + * {
|
||||
margin-top : 0.28cm;
|
||||
}
|
||||
}
|
||||
dl + * {
|
||||
margin-top : 0.17cm;
|
||||
}
|
||||
p + dl {
|
||||
margin-top: 0.17cm;
|
||||
}
|
||||
dt {
|
||||
display : inline;
|
||||
margin-right : 5px;
|
||||
margin-left : -1em;
|
||||
}
|
||||
dd {
|
||||
display : inline;
|
||||
margin-left : 0px;
|
||||
text-indent : 0px;
|
||||
}
|
||||
}
|
||||
|
||||
//*****************************
|
||||
// * BLANK LINE
|
||||
// *****************************/
|
||||
.page {
|
||||
.blank {
|
||||
height: 0.75em;
|
||||
}
|
||||
p + .blank {
|
||||
margin-top: -1em;
|
||||
}
|
||||
}
|
||||
|
||||
//*****************************
|
||||
// * WIDE
|
||||
// *****************************/
|
||||
.page .wide{
|
||||
column-span : all;
|
||||
-webkit-column-span : all;
|
||||
-moz-column-span : all;
|
||||
display : block;
|
||||
margin-bottom : 0.34cm;
|
||||
&+* {
|
||||
margin-top : 0;
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
@import (less) 'shared/naturalcrit/styles/reset.less';
|
||||
@import (less) './client/homebrew/phbStyle/phb.fonts.css';
|
||||
@import (less) './client/homebrew/phbStyle/phb.assets.less';
|
||||
@import (less) './client/homebrew/phbStyle/phb.depricated.less';
|
||||
@import (less) './themes/fonts/5e legacy/fonts.less';
|
||||
@import (less) './themes/assets/assets.less';
|
||||
@import (less) './themes/phb.depricated.less';
|
||||
//Colors
|
||||
@background : #EEE5CE;
|
||||
@noteGreen : #e0e5c1;
|
||||
@headerUnderline : #c9ad6a;
|
||||
@horizontalRule : #9c2b1b;
|
||||
@headerText : #58180D;
|
||||
@monsterStatBackground : #FDF1DC;
|
||||
@background : #EEE5CE; // Light parchment
|
||||
@noteGreen : #e0e5c1; // Pastel green
|
||||
@headerUnderline : #c9ad6a; // Gold
|
||||
@horizontalRule : #9c2b1b; // Maroon
|
||||
@headerText : #58180D; // Dark maroon
|
||||
@monsterStatBackground : #FDF1DC; // Lighter parchment
|
||||
@captionText : #766649; // Brown
|
||||
@page { margin: 0; }
|
||||
body {
|
||||
counter-reset : phb-page-numbers;
|
||||
@@ -189,7 +189,6 @@ body {
|
||||
border-image : @noteBorderImage 11;
|
||||
border-image-outset : 9px 0px;
|
||||
box-shadow : 1px 4px 14px #888;
|
||||
-webkit-transform : translateZ(0); //Prevents shadows from breaking across columns
|
||||
p, ul{
|
||||
font-size : 0.352cm;
|
||||
line-height : 1.1em;
|
||||
@@ -208,7 +207,7 @@ body {
|
||||
background-color : @monsterStatBackground;
|
||||
border-style : solid;
|
||||
border-width : 10px;
|
||||
border-image : @monsterBorderImage 10;
|
||||
border-image : @monsterBorderImageLegacy 10;
|
||||
h2{
|
||||
margin-top : -8px;
|
||||
margin-bottom : 0px;
|
||||
@@ -232,11 +231,9 @@ body {
|
||||
// Monster Ability table
|
||||
hr+table{
|
||||
margin : 0;
|
||||
column-span : 1;
|
||||
background-color : transparent;
|
||||
border-style : none;
|
||||
border-image : none;
|
||||
-webkit-column-span : 1;
|
||||
tbody{
|
||||
tr:nth-child(odd), tr:nth-child(even){
|
||||
background-color : transparent;
|
||||
@@ -417,7 +414,6 @@ body {
|
||||
// * DESCRIPTIVE TEXT BOX
|
||||
// ************************************/
|
||||
.phb .descriptive{
|
||||
display : block-inline;
|
||||
margin-bottom : 1em;
|
||||
background-color : #faf7ea;
|
||||
font-family : ScalySans;
|
||||
@@ -447,6 +443,35 @@ body {
|
||||
.phb pre+.descriptive{
|
||||
margin-top : 8px;
|
||||
}
|
||||
|
||||
//*****************************
|
||||
// * ARTIST CREDIT BLOCK
|
||||
// *****************************/
|
||||
.phb {
|
||||
.artist {
|
||||
position : absolute;
|
||||
text-align : center;
|
||||
font-family : WalterTurncoat;
|
||||
font-size : 0.27cm;
|
||||
color : @captionText;
|
||||
p, p + p {
|
||||
margin : unset;
|
||||
text-indent : unset;
|
||||
line-height : 1em;
|
||||
}
|
||||
h5 {
|
||||
font-size : 1.3em;
|
||||
font-family : WalterTurncoat;
|
||||
}
|
||||
a{
|
||||
color : inherit;
|
||||
text-decoration : unset;
|
||||
&:hover {
|
||||
text-decoration : underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//*****************************
|
||||
// * TABLE OF CONTENTS
|
||||
// *****************************/
|
||||
26
themes/assets/assets.less
Normal file
@@ -0,0 +1,26 @@
|
||||
// PHB
|
||||
@footerAccentImage : data-uri('./themes/assets/footerAccent.png');
|
||||
@frameBorderImage : data-uri('./themes/assets/frameBorder.png');
|
||||
@backgroundImage : data-uri('./themes/assets/parchmentBackground.jpg');
|
||||
@redTriangleImage : data-uri('./themes/assets/redTriangle.png');
|
||||
@monsterBorderImageLegacy : data-uri('./themes/assets/monsterBorderLegacy.png');
|
||||
@noteBorderImage : data-uri('./themes/assets/noteBorder.png');
|
||||
@descriptiveBoxImage : data-uri('./themes/assets/descriptiveBorder.png');
|
||||
@monsterBlockBackground : data-uri('./themes/assets/parchmentBackgroundGrayscale.jpg');
|
||||
@monsterBorderImage : data-uri('./themes/assets/monsterBorderFancy.png');
|
||||
@codeBorderImage : data-uri('./themes/assets/codeBorder.png');
|
||||
@classTableDecoration : data-uri('./themes/assets/classTableDecoration.png');
|
||||
|
||||
// Watercolor Images
|
||||
@watercolor1 : data-uri('./themes/assets/watercolor/watercolor1.png');
|
||||
@watercolor2 : data-uri('./themes/assets/watercolor/watercolor2.png');
|
||||
@watercolor3 : data-uri('./themes/assets/watercolor/watercolor3.png');
|
||||
@watercolor4 : data-uri('./themes/assets/watercolor/watercolor4.png');
|
||||
@watercolor5 : data-uri('./themes/assets/watercolor/watercolor5.png');
|
||||
@watercolor6 : data-uri('./themes/assets/watercolor/watercolor6.png');
|
||||
@watercolor7 : data-uri('./themes/assets/watercolor/watercolor7.png');
|
||||
@watercolor8 : data-uri('./themes/assets/watercolor/watercolor8.png');
|
||||
@watercolor9 : data-uri('./themes/assets/watercolor/watercolor9.png');
|
||||
@watercolor10 : data-uri('./themes/assets/watercolor/watercolor10.png');
|
||||
@watercolor11 : data-uri('./themes/assets/watercolor/watercolor11.png');
|
||||
@watercolor12 : data-uri('./themes/assets/watercolor/watercolor12.png');
|
||||
BIN
themes/assets/classTableDecoration.png
Normal file
|
After Width: | Height: | Size: 67 KiB |
BIN
themes/assets/codeBorder.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
themes/assets/descriptiveBorder.png
Normal file
|
After Width: | Height: | Size: 311 B |
BIN
themes/assets/footerAccent.png
Normal file
|
After Width: | Height: | Size: 7.4 KiB |
BIN
themes/assets/frameBorder.png
Normal file
|
After Width: | Height: | Size: 9.0 KiB |
BIN
themes/assets/monsterBorderFancy.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
themes/assets/monsterBorderLegacy.png
Normal file
|
After Width: | Height: | Size: 135 B |
BIN
themes/assets/noteBorder.png
Normal file
|
After Width: | Height: | Size: 274 B |
BIN
themes/assets/parchmentBackground.jpg
Normal file
|
After Width: | Height: | Size: 170 KiB |
BIN
themes/assets/parchmentBackgroundGrayscale.jpg
Normal file
|
After Width: | Height: | Size: 160 KiB |
|
Before Width: | Height: | Size: 864 B After Width: | Height: | Size: 864 B |
BIN
themes/assets/watercolor/watercolor1.png
Normal file
|
After Width: | Height: | Size: 161 KiB |
BIN
themes/assets/watercolor/watercolor10.png
Normal file
|
After Width: | Height: | Size: 114 KiB |
BIN
themes/assets/watercolor/watercolor11.png
Normal file
|
After Width: | Height: | Size: 78 KiB |