Compare commits
415 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a742e8c2f | ||
|
|
e14c42761d | ||
|
|
6f6c4acf7e | ||
|
|
30745c2be3 | ||
|
|
4d087f4aa9 | ||
|
|
874c8a9fd1 | ||
|
|
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 | ||
|
|
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 | ||
|
|
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 | ||
|
|
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 | ||
|
|
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 | ||
|
|
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 | ||
|
|
3ade40f2d9 | ||
|
|
0debd2bbf0 | ||
|
|
1a3afc9661 | ||
|
|
ac4ebbe548 | ||
|
|
089414c9ff | ||
|
|
a1dbf0f2e5 | ||
|
|
712824d8a6 | ||
|
|
7491f463b4 | ||
|
|
8f08591ab9 | ||
|
|
b98586150f | ||
|
|
2f094801ca | ||
|
|
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 | ||
|
|
5824ab6eb5 | ||
|
|
e6ae1ddec6 | ||
|
|
2213d23115 | ||
|
|
6393cdec9b | ||
|
|
a10f573a30 | ||
|
|
9dcce15790 | ||
|
|
481c9f067c | ||
|
|
1e64e49dc3 | ||
|
|
19a2ecd281 | ||
|
|
03b02669a4 | ||
|
|
c979f02ce4 | ||
|
|
bc86c1b8fc | ||
|
|
37d0a4aad2 | ||
|
|
ff70b5c546 | ||
|
|
7daec673ba | ||
|
|
f2d07a699a | ||
|
|
721511e484 | ||
|
|
2942660201 | ||
|
|
68811eb3fc | ||
|
|
468b7319d1 | ||
|
|
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 = {
|
module.exports = {
|
||||||
root : true,
|
root : true,
|
||||||
parserOptions : {
|
parserOptions : {
|
||||||
ecmaVersion : 9,
|
ecmaVersion : 2021,
|
||||||
sourceType : 'module',
|
sourceType : 'module',
|
||||||
ecmaFeatures : {
|
ecmaFeatures : {
|
||||||
jsx : true
|
jsx : true
|
||||||
|
|||||||
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
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM node:8
|
FROM node:14.15
|
||||||
|
|
||||||
ENV NODE_ENV=docker
|
ENV NODE_ENV=docker
|
||||||
|
|
||||||
|
|||||||
102
changelog.md
@@ -1,13 +1,86 @@
|
|||||||
|
<style>
|
||||||
|
h5 {
|
||||||
|
font-size: .35cm !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
# changelog
|
# changelog
|
||||||
|
|
||||||
|
### 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
|
||||||
|
|
||||||
|
### 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
|
### Friday, 25/1/2021 - v2.10.7
|
||||||
- Cover Page snippet now flips left-right page numbering.
|
- 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).
|
- 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!)
|
- Fix for box-shadows breaking across columns. <br>(Thanks G-Ambatte for all of these!)
|
||||||
- Small user interface tweaks (Thanks @Ericsheid)
|
- Small user interface tweaks (Thanks Ericsheid)
|
||||||
|
|
||||||
### Friday, 02/1/2021 - v2.10.6
|
### 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 server crashes due to excessive long lines in brews
|
||||||
- Fixed "automated request" lockouts from Google
|
- Fixed "automated request" lockouts from Google
|
||||||
|
|
||||||
@@ -30,12 +103,11 @@
|
|||||||
- Fixed issue with users unable to create new brews
|
- Fixed issue with users unable to create new brews
|
||||||
- Fixing brews being lost when loaded via back button
|
- Fixing brews being lost when loaded via back button
|
||||||
|
|
||||||
|
\page
|
||||||
|
|
||||||
### Wednesday, 07/10/2020 - v2.10.0
|
### 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!
|
- 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
|
### Friday, 28/08/2020 - v2.9.2
|
||||||
- Many dependency updates
|
- Many dependency updates
|
||||||
- Finally fixed this changelog page to not run off the edge :P
|
- Finally fixed this changelog page to not run off the edge :P
|
||||||
@@ -79,8 +151,6 @@
|
|||||||
### Saturday, 18/02/2017 - v2.7.2
|
### 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)
|
- 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
|
### Thursday, 19/01/2017 - v2.7.1
|
||||||
- Fixed saving multiple authors and multiple systems on brew metadata (thanks u/PalaNolho re:282)
|
- Fixed saving multiple authors and multiple systems on brew metadata (thanks u/PalaNolho re:282)
|
||||||
- Adding in line highlight for new pages
|
- Adding in line highlight for new pages
|
||||||
@@ -121,9 +191,7 @@
|
|||||||
- Added a hover tooltip to fully read the brew description
|
- 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.
|
- Made the brew items take up only 25% allowing you to view more per row.
|
||||||
|
|
||||||
|
\page
|
||||||
```
|
|
||||||
```
|
|
||||||
|
|
||||||
### Wednesday, 23/11/2016 - v2.5.0
|
### Wednesday, 23/11/2016 - v2.5.0
|
||||||
- Metadata can now be added to brews
|
- Metadata can now be added to brews
|
||||||
@@ -135,7 +203,6 @@
|
|||||||
- Added a new user page to see others published brews, as well as all of your own brews.
|
- 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
|
- Added a new nav item for accessing your profile and logging in
|
||||||
|
|
||||||
|
|
||||||
### Monday, 14/11/2016
|
### Monday, 14/11/2016
|
||||||
- Updated snippet bar style
|
- Updated snippet bar style
|
||||||
- You can now print from a new page without saving
|
- You can now print from a new page without saving
|
||||||
@@ -171,9 +238,6 @@
|
|||||||
- Allows adding in hyperlinks to specific pages
|
- Allows adding in hyperlinks to specific pages
|
||||||
- Even works after you print to pdf!
|
- Even works after you print to pdf!
|
||||||
|
|
||||||
|
|
||||||
\page
|
|
||||||
|
|
||||||
### Tuesday, 07/06/2016 - v2.2.2
|
### 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 aprser not working on print page
|
||||||
|
|
||||||
@@ -197,13 +261,11 @@
|
|||||||
- Updated the issue template for (hopefully) better reporting
|
- Updated the issue template for (hopefully) better reporting
|
||||||
- Added suggestion to use chrome while PDF printing
|
- Added suggestion to use chrome while PDF printing
|
||||||
|
|
||||||
|
|
||||||
```
|
|
||||||
```
|
|
||||||
|
|
||||||
### Wednesday, 25/05/2016 -v2.0.5
|
### Wednesday, 25/05/2016 -v2.0.5
|
||||||
- The class table generators have the proper ability score improvement progression.
|
- The class table generators have the proper ability score improvement progression.
|
||||||
|
|
||||||
|
\page
|
||||||
|
|
||||||
### Tuesday, 24/05/2016 - v2.0.4
|
### Tuesday, 24/05/2016 - v2.0.4
|
||||||
- Fixed extra wide monster stat blocks sometimes only being one column
|
- 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)
|
- The class table generators now follow the proper progression from the PHB (thakns u/IrishBandit)
|
||||||
@@ -214,8 +276,6 @@
|
|||||||
- Bumped up the allowed entity size for extra-large brew (Thanks for reporting it dickboner93)
|
- 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.
|
- 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!)
|
### 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.
|
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>
|
<header>
|
||||||
<div className='container'>
|
<div className='container'>
|
||||||
<i className='fa fa-rocket' />
|
<i className='fas fa-rocket' />
|
||||||
homebrewery admin
|
homebrewery admin
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -45,8 +45,8 @@ const BrewCleanup = createClass({
|
|||||||
return <div className='removeBox'>
|
return <div className='removeBox'>
|
||||||
<button onClick={this.cleanup} className='remove'>
|
<button onClick={this.cleanup} className='remove'>
|
||||||
{this.state.pending
|
{this.state.pending
|
||||||
? <i className='fa fa-spin fa-spinner' />
|
? <i className='fas fa-spin fa-spinner' />
|
||||||
: <span><i className='fa fa-times' /> Remove</span>
|
: <span><i className='fas fa-times' /> Remove</span>
|
||||||
}
|
}
|
||||||
</button>
|
</button>
|
||||||
<span>Found {this.state.count} Brews that could be removed. </span>
|
<span>Found {this.state.count} Brews that could be removed. </span>
|
||||||
@@ -59,7 +59,7 @@ const BrewCleanup = createClass({
|
|||||||
|
|
||||||
<button onClick={this.prime} className='query'>
|
<button onClick={this.prime} className='query'>
|
||||||
{this.state.pending
|
{this.state.pending
|
||||||
? <i className='fa fa-spin fa-spinner' />
|
? <i className='fas fa-spin fa-spinner' />
|
||||||
: 'Query Brews'
|
: 'Query Brews'
|
||||||
}
|
}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -59,8 +59,8 @@ const BrewCompress = createClass({
|
|||||||
return <div className='removeBox'>
|
return <div className='removeBox'>
|
||||||
<button onClick={this.cleanup} className='remove'>
|
<button onClick={this.cleanup} className='remove'>
|
||||||
{this.state.pending
|
{this.state.pending
|
||||||
? <i className='fa fa-spin fa-spinner' />
|
? <i className='fas fa-spin fa-spinner' />
|
||||||
: <span><i className='fa fa-compress' /> compress </span>
|
: <span><i className='fas fa-compress' /> compress </span>
|
||||||
}
|
}
|
||||||
</button>
|
</button>
|
||||||
{this.state.pending
|
{this.state.pending
|
||||||
@@ -76,7 +76,7 @@ const BrewCompress = createClass({
|
|||||||
|
|
||||||
<button onClick={this.prime} className='query'>
|
<button onClick={this.prime} className='query'>
|
||||||
{this.state.pending
|
{this.state.pending
|
||||||
? <i className='fa fa-spin fa-spinner' />
|
? <i className='fas fa-spin fa-spinner' />
|
||||||
: 'Query Brews'
|
: 'Query Brews'
|
||||||
}
|
}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ const BrewLookup = createClass({
|
|||||||
<h2>Brew Lookup</h2>
|
<h2>Brew Lookup</h2>
|
||||||
<input type='text' value={this.state.query} onChange={this.handleChange} placeholder='edit or share id' />
|
<input type='text' value={this.state.query} onChange={this.handleChange} placeholder='edit or share id' />
|
||||||
<button onClick={this.lookup}>
|
<button onClick={this.lookup}>
|
||||||
<i className={cx('fa', {
|
<i className={cx('fas', {
|
||||||
'fa-search' : !this.state.searching,
|
'fa-search' : !this.state.searching,
|
||||||
'fa-spin fa-spinner' : this.state.searching,
|
'fa-spin fa-spinner' : this.state.searching,
|
||||||
})} />
|
})} />
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ const Stats = createClass({
|
|||||||
</dl>
|
</dl>
|
||||||
|
|
||||||
{this.state.fetching
|
{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>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ const createClass = require('create-react-class');
|
|||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const cx = require('classnames');
|
const cx = require('classnames');
|
||||||
|
|
||||||
|
const MarkdownLegacy = require('naturalcrit/markdownLegacy.js');
|
||||||
const Markdown = require('naturalcrit/markdown.js');
|
const Markdown = require('naturalcrit/markdown.js');
|
||||||
const ErrorBar = require('./errorBar/errorBar.jsx');
|
const ErrorBar = require('./errorBar/errorBar.jsx');
|
||||||
|
|
||||||
@@ -18,12 +19,19 @@ const PPR_THRESHOLD = 50;
|
|||||||
const BrewRenderer = createClass({
|
const BrewRenderer = createClass({
|
||||||
getDefaultProps : function() {
|
getDefaultProps : function() {
|
||||||
return {
|
return {
|
||||||
text : '',
|
text : '',
|
||||||
errors : []
|
style : '',
|
||||||
|
renderer : 'legacy',
|
||||||
|
errors : []
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
getInitialState : function() {
|
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 {
|
return {
|
||||||
viewablePageNumber : 0,
|
viewablePageNumber : 0,
|
||||||
@@ -34,7 +42,7 @@ const BrewRenderer = createClass({
|
|||||||
usePPR : pages.length >= PPR_THRESHOLD,
|
usePPR : pages.length >= PPR_THRESHOLD,
|
||||||
visibility : 'hidden',
|
visibility : 'hidden',
|
||||||
initialContent : `<!DOCTYPE html><html><head>
|
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="//fonts.googleapis.com/css?family=Open+Sans:400,300,600,700" rel="stylesheet" type="text/css" />
|
||||||
<link href='/homebrew/bundle.css' rel='stylesheet' />
|
<link href='/homebrew/bundle.css' rel='stylesheet' />
|
||||||
<base target=_blank>
|
<base target=_blank>
|
||||||
@@ -48,12 +56,19 @@ const BrewRenderer = createClass({
|
|||||||
window.removeEventListener('resize', this.updateSize);
|
window.removeEventListener('resize', this.updateSize);
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillReceiveProps : function(nextProps) {
|
componentDidUpdate : function(prevProps) {
|
||||||
const pages = nextProps.text.split('\\page');
|
if(prevProps.text !== this.props.text) {
|
||||||
this.setState({
|
let pages;
|
||||||
pages : pages,
|
if(this.props.renderer == 'legacy') {
|
||||||
usePPR : pages.length >= PPR_THRESHOLD
|
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() {
|
updateSize : function() {
|
||||||
@@ -103,12 +118,20 @@ const BrewRenderer = createClass({
|
|||||||
|
|
||||||
renderDummyPage : function(index){
|
renderDummyPage : function(index){
|
||||||
return <div className='phb' id={`p${index + 1}`} key={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>;
|
</div>;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
renderStyle : function() {
|
||||||
|
if(!this.props.style) return;
|
||||||
|
return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `<style> ${this.props.style} </style>` }} />;
|
||||||
|
},
|
||||||
|
|
||||||
renderPage : function(pageText, index){
|
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
|
||||||
|
return <div className='phb3 page' id={`p${index + 1}`} dangerouslySetInnerHTML={{ __html: Markdown.render(pageText) }} key={index} />;
|
||||||
},
|
},
|
||||||
|
|
||||||
renderPages : function(){
|
renderPages : function(){
|
||||||
@@ -158,8 +181,11 @@ const BrewRenderer = createClass({
|
|||||||
</div>
|
</div>
|
||||||
: null}
|
: null}
|
||||||
|
|
||||||
<Frame initialContent={this.state.initialContent} style={{ width: '100%', height: '100%', visibility: this.state.visibility }} contentDidMount={this.frameDidMount}>
|
<Frame initialContent={this.state.initialContent}
|
||||||
<div className='brewRenderer'
|
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}
|
onScroll={this.handleScroll}
|
||||||
style={{ height: this.state.height }}>
|
style={{ height: this.state.height }}>
|
||||||
|
|
||||||
@@ -170,9 +196,14 @@ const BrewRenderer = createClass({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='pages' ref='pages'>
|
<div className='pages' ref='pages'>
|
||||||
|
{/* Apply CSS from Style tab and render pages from Markdown tab */}
|
||||||
{this.state.isMounted
|
{this.state.isMounted
|
||||||
? this.renderPages()
|
&&
|
||||||
: null}
|
<>
|
||||||
|
{this.renderStyle()}
|
||||||
|
{this.renderPages()}
|
||||||
|
</>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Frame>
|
</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{
|
.brewRenderer{
|
||||||
will-change : transform;
|
will-change : transform;
|
||||||
overflow-y : scroll;
|
overflow-y : scroll;
|
||||||
.pages{
|
.pages{
|
||||||
margin : 30px 0px;
|
margin : 30px 0px;
|
||||||
&>.phb{
|
&>.page{
|
||||||
margin-right : auto;
|
margin-right : auto;
|
||||||
margin-bottom : 30px;
|
margin-bottom : 30px;
|
||||||
margin-left : auto;
|
margin-left : auto;
|
||||||
@@ -16,6 +13,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.pane{
|
||||||
|
position : relative;
|
||||||
|
}
|
||||||
.pageInfo{
|
.pageInfo{
|
||||||
position : absolute;
|
position : absolute;
|
||||||
right : 17px;
|
right : 17px;
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ const ErrorBar = createClass({
|
|||||||
if(!this.props.errors.length) return null;
|
if(!this.props.errors.length) return null;
|
||||||
|
|
||||||
return <div className='errorBar'>
|
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>
|
<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>
|
<small>If these aren't fixed your brew will not render properly when you print it to PDF or share it</small>
|
||||||
{this.renderErrors()}
|
{this.renderErrors()}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ const NotificationPopup = createClass({
|
|||||||
psa : function(){
|
psa : function(){
|
||||||
return <li key='psa'>
|
return <li key='psa'>
|
||||||
<em>Google Drive Integration!</em> <br />
|
<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
|
We have added Google Drive integration to the Homebrewery! <a target='_blank' href='https://www.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
|
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!
|
Google Drive storage, and Google will keep a backup of each version! No more lost work surprises!
|
||||||
<br /><br />
|
<br /><br />
|
||||||
@@ -60,8 +60,8 @@ const NotificationPopup = createClass({
|
|||||||
if(_.isEmpty(this.state.notifications)) return null;
|
if(_.isEmpty(this.state.notifications)) return null;
|
||||||
|
|
||||||
return <div className='notificationPopup'>
|
return <div className='notificationPopup'>
|
||||||
<i className='fa fa-times dismiss' onClick={this.dismiss}/>
|
<i className='fas fa-times dismiss' onClick={this.dismiss}/>
|
||||||
<i className='fa fa-info-circle info' />
|
<i className='fas fa-info-circle info' />
|
||||||
<h3>Notice</h3>
|
<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>
|
<small>This website is always improving and we are still adding new features and squashing bugs. Keep the following in mind:</small>
|
||||||
<ul>{_.values(this.state.notifications)}</ul>
|
<ul>{_.values(this.state.notifications)}</ul>
|
||||||
|
|||||||
@@ -3,96 +3,166 @@ const React = require('react');
|
|||||||
const createClass = require('create-react-class');
|
const createClass = require('create-react-class');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const cx = require('classnames');
|
const cx = require('classnames');
|
||||||
|
const dedent = require('dedent-tabs').default;
|
||||||
|
|
||||||
const CodeEditor = require('naturalcrit/codeEditor/codeEditor.jsx');
|
const CodeEditor = require('naturalcrit/codeEditor/codeEditor.jsx');
|
||||||
const SnippetBar = require('./snippetbar/snippetbar.jsx');
|
const SnippetBar = require('./snippetbar/snippetbar.jsx');
|
||||||
const MetadataEditor = require('./metadataEditor/metadataEditor.jsx');
|
const MetadataEditor = require('./metadataEditor/metadataEditor.jsx');
|
||||||
|
|
||||||
|
const 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){
|
const splice = function(str, index, inject){
|
||||||
return str.slice(0, index) + inject + str.slice(index);
|
return str.slice(0, index) + inject + str.slice(index);
|
||||||
};
|
};
|
||||||
|
|
||||||
const SNIPPETBAR_HEIGHT = 25;
|
|
||||||
|
|
||||||
const Editor = createClass({
|
const Editor = createClass({
|
||||||
getDefaultProps : function() {
|
getDefaultProps : function() {
|
||||||
return {
|
return {
|
||||||
value : '',
|
brew : {
|
||||||
onChange : ()=>{},
|
text : '',
|
||||||
|
style : ''
|
||||||
|
},
|
||||||
|
|
||||||
metadata : {},
|
onTextChange : ()=>{},
|
||||||
onMetadataChange : ()=>{},
|
onStyleChange : ()=>{},
|
||||||
showMetaButton : true
|
onMetaChange : ()=>{},
|
||||||
|
|
||||||
|
renderer : 'legacy'
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
getInitialState : function() {
|
getInitialState : function() {
|
||||||
return {
|
return {
|
||||||
showMetadataEditor : false
|
view : 'text' //'text', 'style', 'meta'
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
cursorPosition : {
|
|
||||||
line : 0,
|
isText : function() {return this.state.view == 'text';},
|
||||||
ch : 0
|
isStyle : function() {return this.state.view == 'style';},
|
||||||
},
|
isMeta : function() {return this.state.view == 'meta';},
|
||||||
|
|
||||||
componentDidMount : function() {
|
componentDidMount : function() {
|
||||||
this.updateEditorSize();
|
this.updateEditorSize();
|
||||||
this.highlightPageLines();
|
this.highlightCustomMarkdown();
|
||||||
window.addEventListener('resize', this.updateEditorSize);
|
window.addEventListener('resize', this.updateEditorSize);
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillUnmount : function() {
|
componentWillUnmount : function() {
|
||||||
window.removeEventListener('resize', this.updateEditorSize);
|
window.removeEventListener('resize', this.updateEditorSize);
|
||||||
},
|
},
|
||||||
|
|
||||||
updateEditorSize : function() {
|
updateEditorSize : function() {
|
||||||
let paneHeight = this.refs.main.parentNode.clientHeight;
|
if(this.refs.codeEditor) {
|
||||||
paneHeight -= SNIPPETBAR_HEIGHT + 1;
|
let paneHeight = this.refs.main.parentNode.clientHeight;
|
||||||
this.refs.codeEditor.codeMirror.setSize(null, paneHeight);
|
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){
|
handleInject : function(injectText){
|
||||||
const lines = this.props.value.split('\n');
|
const text = (this.isText() ? this.props.brew.text : this.props.brew.style);
|
||||||
lines[this.cursorPosition.line] = splice(lines[this.cursorPosition.line], this.cursorPosition.ch, injectText);
|
|
||||||
|
|
||||||
this.handleTextChange(lines.join('\n'));
|
const lines = text.split('\n');
|
||||||
this.refs.codeEditor.setCursorPosition(this.cursorPosition.line, this.cursorPosition.ch + injectText.length);
|
const cursorPos = this.refs.codeEditor.getCursorPosition();
|
||||||
|
lines[cursorPos.line] = splice(lines[cursorPos.line], cursorPos.ch, injectText);
|
||||||
|
|
||||||
|
this.refs.codeEditor.setCursorPosition(cursorPos.line + injectText.split('\n').length, cursorPos.ch + injectText.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({
|
this.setState({
|
||||||
showMetadataEditor : !this.state.showMetadataEditor
|
view : newView
|
||||||
});
|
}, this.updateEditorSize); //TODO: not sure if updateeditorsize needed
|
||||||
},
|
},
|
||||||
|
|
||||||
getCurrentPage : function(){
|
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)=>{
|
return _.reduce(lines, (r, line)=>{
|
||||||
if(line.indexOf('\\page') !== -1) r++;
|
if(line.indexOf('\\page') !== -1) r++;
|
||||||
return r;
|
return r;
|
||||||
}, 1);
|
}, 1);
|
||||||
},
|
},
|
||||||
|
|
||||||
highlightPageLines : function(){
|
highlightCustomMarkdown : function(){
|
||||||
if(!this.refs.codeEditor) return;
|
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)=>{
|
//reset custom text styles
|
||||||
if(line.indexOf('\\page') !== -1){
|
const customHighlights = codeMirror.getAllMarks();
|
||||||
codeMirror.addLineClass(lineNumber, 'background', 'pageLine');
|
for (let i=0;i<customHighlights.length;i++) customHighlights[i].clear();
|
||||||
r.push(lineNumber);
|
|
||||||
}
|
const lineNumbers = _.reduce(this.props.brew.text.split('\n'), (r, line, lineNumber)=>{
|
||||||
return r;
|
|
||||||
}, []);
|
//reset custom line styles
|
||||||
return lineNumbers;
|
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.startsWith('\\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(){
|
brewJump : function(){
|
||||||
const currentPage = this.getCurrentPage();
|
const currentPage = this.getCurrentPage();
|
||||||
window.location.hash = `p${currentPage}`;
|
window.location.hash = `p${currentPage}`;
|
||||||
@@ -100,41 +170,44 @@ const Editor = createClass({
|
|||||||
|
|
||||||
//Called when there are changes to the editor's dimensions
|
//Called when there are changes to the editor's dimensions
|
||||||
update : function(){
|
update : function(){
|
||||||
this.refs.codeEditor.updateSize();
|
this.refs.codeEditor?.updateSize();
|
||||||
},
|
},
|
||||||
|
|
||||||
renderMetadataEditor : function(){
|
renderEditor : function(){
|
||||||
if(!this.state.showMetadataEditor) return;
|
if(this.isText()){
|
||||||
return <MetadataEditor
|
return <CodeEditor key='text'
|
||||||
metadata={this.props.metadata}
|
ref='codeEditor'
|
||||||
onChange={this.props.onMetadataChange}
|
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(){
|
render : function(){
|
||||||
this.highlightPageLines();
|
this.highlightCustomMarkdown();
|
||||||
return (
|
return (
|
||||||
<div className='editor' ref='main'>
|
<div className='editor' ref='main'>
|
||||||
<SnippetBar
|
<SnippetBar
|
||||||
brew={this.props.value}
|
brew={this.props.brew}
|
||||||
|
view={this.state.view}
|
||||||
|
onViewChange={this.handleViewChange}
|
||||||
onInject={this.handleInject}
|
onInject={this.handleInject}
|
||||||
onToggle={this.handgleToggle}
|
showEditButtons={this.props.showEditButtons}
|
||||||
showmeta={this.state.showMetadataEditor}
|
renderer={this.props.renderer} />
|
||||||
showMetaButton={this.props.showMetaButton} />
|
|
||||||
{this.renderMetadataEditor()}
|
|
||||||
<CodeEditor
|
|
||||||
ref='codeEditor'
|
|
||||||
wrap={true}
|
|
||||||
language='gfm'
|
|
||||||
value={this.props.value}
|
|
||||||
onChange={this.handleTextChange}
|
|
||||||
onCursorActivity={this.handleCursorActivty} />
|
|
||||||
|
|
||||||
{/*
|
{this.renderEditor()}
|
||||||
<div className='brewJump' onClick={this.brewJump}>
|
|
||||||
<i className='fa fa-arrow-right' />
|
|
||||||
</div>
|
|
||||||
*/}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,22 @@
|
|||||||
background-color : fade(#333, 15%);
|
background-color : fade(#333, 15%);
|
||||||
border-bottom : #333 solid 1px;
|
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{
|
.brewJump{
|
||||||
|
|||||||
@@ -17,7 +17,8 @@ const MetadataEditor = createClass({
|
|||||||
tags : '',
|
tags : '',
|
||||||
published : false,
|
published : false,
|
||||||
authors : [],
|
authors : [],
|
||||||
systems : []
|
systems : [],
|
||||||
|
renderer : 'legacy'
|
||||||
},
|
},
|
||||||
onChange : ()=>{}
|
onChange : ()=>{}
|
||||||
};
|
};
|
||||||
@@ -36,6 +37,12 @@ const MetadataEditor = createClass({
|
|||||||
}
|
}
|
||||||
this.props.onChange(this.props.metadata);
|
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){
|
handlePublish : function(val){
|
||||||
this.props.onChange(_.merge({}, this.props.metadata, {
|
this.props.onChange(_.merge({}, this.props.metadata, {
|
||||||
published : val
|
published : val
|
||||||
@@ -43,7 +50,7 @@ const MetadataEditor = createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
handleDelete : function(){
|
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 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;
|
if(!confirm('Are you REALLY sure? You will not be able to recover the document.')) return;
|
||||||
} else {
|
} else {
|
||||||
@@ -60,10 +67,12 @@ const MetadataEditor = createClass({
|
|||||||
|
|
||||||
getRedditLink : function(){
|
getRedditLink : function(){
|
||||||
const meta = this.props.metadata;
|
const meta = this.props.metadata;
|
||||||
|
|
||||||
|
const shareLink = (meta.googleId || '') + meta.shareId;
|
||||||
const title = `${meta.title} [${meta.systems.join(' ')}]`;
|
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.
|
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)}`;
|
return `https://www.reddit.com/r/UnearthedArcana/submit?title=${encodeURIComponent(title)}&text=${encodeURIComponent(text)}`;
|
||||||
},
|
},
|
||||||
@@ -83,11 +92,11 @@ const MetadataEditor = createClass({
|
|||||||
renderPublish : function(){
|
renderPublish : function(){
|
||||||
if(this.props.metadata.published){
|
if(this.props.metadata.published){
|
||||||
return <button className='unpublish' onClick={()=>this.handlePublish(false)}>
|
return <button className='unpublish' onClick={()=>this.handlePublish(false)}>
|
||||||
<i className='fa fa-ban' /> unpublish
|
<i className='fas fa-ban' /> unpublish
|
||||||
</button>;
|
</button>;
|
||||||
} else {
|
} else {
|
||||||
return <button className='publish' onClick={()=>this.handlePublish(true)}>
|
return <button className='publish' onClick={()=>this.handlePublish(true)}>
|
||||||
<i className='fa fa-globe' /> publish
|
<i className='fas fa-globe' /> publish
|
||||||
</button>;
|
</button>;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -99,7 +108,7 @@ const MetadataEditor = createClass({
|
|||||||
<label>delete</label>
|
<label>delete</label>
|
||||||
<div className='value'>
|
<div className='value'>
|
||||||
<button className='publish' onClick={this.handleDelete}>
|
<button className='publish' onClick={this.handleDelete}>
|
||||||
<i className='fa fa-trash' /> delete brew
|
<i className='fas fa-trash-alt' /> delete brew
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
@@ -107,7 +116,7 @@ const MetadataEditor = createClass({
|
|||||||
|
|
||||||
renderAuthors : function(){
|
renderAuthors : function(){
|
||||||
let text = 'None.';
|
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(', ');
|
text = this.props.metadata.authors.join(', ');
|
||||||
}
|
}
|
||||||
return <div className='field authors'>
|
return <div className='field authors'>
|
||||||
@@ -126,13 +135,42 @@ const MetadataEditor = createClass({
|
|||||||
<div className='value'>
|
<div className='value'>
|
||||||
<a href={this.getRedditLink()} target='_blank' rel='noopener noreferrer'>
|
<a href={this.getRedditLink()} target='_blank' rel='noopener noreferrer'>
|
||||||
<button className='publish'>
|
<button className='publish'>
|
||||||
<i className='fa fa-reddit-alien' /> share to reddit
|
<i className='fab fa-reddit-alien' /> share to reddit
|
||||||
</button>
|
</button>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
},
|
||||||
|
|
||||||
render : function(){
|
render : function(){
|
||||||
return <div className='metadataEditor'>
|
return <div className='metadataEditor'>
|
||||||
<div className='field title'>
|
<div className='field title'>
|
||||||
@@ -154,6 +192,8 @@ const MetadataEditor = createClass({
|
|||||||
</div>
|
</div>
|
||||||
*/}
|
*/}
|
||||||
|
|
||||||
|
{this.renderAuthors()}
|
||||||
|
|
||||||
<div className='field systems'>
|
<div className='field systems'>
|
||||||
<label>systems</label>
|
<label>systems</label>
|
||||||
<div className='value'>
|
<div className='value'>
|
||||||
@@ -161,7 +201,7 @@ const MetadataEditor = createClass({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{this.renderAuthors()}
|
{this.renderRenderOptions()}
|
||||||
|
|
||||||
<div className='field publish'>
|
<div className='field publish'>
|
||||||
<label>publish</label>
|
<label>publish</label>
|
||||||
|
|||||||
@@ -18,10 +18,11 @@
|
|||||||
font-weight : 800;
|
font-weight : 800;
|
||||||
line-height : 1.8em;
|
line-height : 1.8em;
|
||||||
text-transform : uppercase;
|
text-transform : uppercase;
|
||||||
flex-grow : 0;
|
flex : 0 0 auto;
|
||||||
}
|
}
|
||||||
&>.value{
|
&>.value{
|
||||||
flex-grow : 1;
|
flex : 1 1 auto;
|
||||||
|
min-width : 200px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.description.field textarea.value{
|
.description.field textarea.value{
|
||||||
@@ -38,15 +39,22 @@
|
|||||||
font-size : 0.7em;
|
font-size : 0.7em;
|
||||||
font-weight : 800;
|
font-weight : 800;
|
||||||
user-select : none;
|
user-select : none;
|
||||||
|
white-space : nowrap;
|
||||||
|
display : inline-flex;
|
||||||
|
align-items : center;
|
||||||
}
|
}
|
||||||
input{
|
input{
|
||||||
vertical-align : middle;
|
vertical-align : middle;
|
||||||
cursor : pointer;
|
cursor : pointer;
|
||||||
|
margin : 3px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.publish.field .value{
|
.publish.field .value{
|
||||||
position : relative;
|
position : relative;
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
|
button{
|
||||||
|
width:100%;
|
||||||
|
}
|
||||||
button.publish{
|
button.publish{
|
||||||
.button(@blueLight);
|
.button(@blueLight);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ const _ = require('lodash');
|
|||||||
const cx = require('classnames');
|
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){
|
const execute = function(val, brew){
|
||||||
if(_.isFunction(val)) return val(brew);
|
if(_.isFunction(val)) return val(brew);
|
||||||
@@ -15,11 +16,19 @@ const execute = function(val, brew){
|
|||||||
const Snippetbar = createClass({
|
const Snippetbar = createClass({
|
||||||
getDefaultProps : function() {
|
getDefaultProps : function() {
|
||||||
return {
|
return {
|
||||||
brew : '',
|
brew : {},
|
||||||
onInject : ()=>{},
|
view : 'text',
|
||||||
onToggle : ()=>{},
|
onViewChange : ()=>{},
|
||||||
showmeta : false,
|
onInject : ()=>{},
|
||||||
showMetaButton : true
|
onToggle : ()=>{},
|
||||||
|
showEditButtons : true,
|
||||||
|
renderer : 'legacy'
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
getInitialState : function() {
|
||||||
|
return {
|
||||||
|
renderer : this.props.renderer
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -28,7 +37,16 @@ const Snippetbar = createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
renderSnippetGroups : function(){
|
renderSnippetGroups : function(){
|
||||||
return _.map(Snippets, (snippetGroup)=>{
|
let snippets = [];
|
||||||
|
|
||||||
|
if(this.props.view === 'text') {
|
||||||
|
if(this.props.renderer === 'V3')
|
||||||
|
snippets = SnippetsV3;
|
||||||
|
else
|
||||||
|
snippets = SnippetsLegacy;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _.map(snippets, (snippetGroup)=>{
|
||||||
return <SnippetGroup
|
return <SnippetGroup
|
||||||
brew={this.props.brew}
|
brew={this.props.brew}
|
||||||
groupName={snippetGroup.groupName}
|
groupName={snippetGroup.groupName}
|
||||||
@@ -40,19 +58,29 @@ const Snippetbar = createClass({
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
renderMetadataButton : function(){
|
renderEditorButtons : function(){
|
||||||
if(!this.props.showMetaButton) return;
|
if(!this.props.showEditButtons) return;
|
||||||
return <div className={cx('snippetBarButton', 'toggleMeta', { selected: this.props.showmeta })}
|
|
||||||
onClick={this.props.onToggle}>
|
return <div className='editors'>
|
||||||
<i className='fa fa-info-circle' />
|
<div className={cx('text', { selected: this.props.view === 'text' })}
|
||||||
<span className='groupName'>Properties</span>
|
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>;
|
</div>;
|
||||||
},
|
},
|
||||||
|
|
||||||
render : function(){
|
render : function(){
|
||||||
return <div className='snippetBar'>
|
return <div className='snippetBar'>
|
||||||
{this.renderSnippetGroups()}
|
{this.renderSnippetGroups()}
|
||||||
{this.renderMetadataButton()}
|
{this.renderEditorButtons()}
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -67,9 +95,9 @@ module.exports = Snippetbar;
|
|||||||
const SnippetGroup = createClass({
|
const SnippetGroup = createClass({
|
||||||
getDefaultProps : function() {
|
getDefaultProps : function() {
|
||||||
return {
|
return {
|
||||||
brew : '',
|
brew : {},
|
||||||
groupName : '',
|
groupName : '',
|
||||||
icon : 'fa-rocket',
|
icon : 'fas fa-rocket',
|
||||||
snippets : [],
|
snippets : [],
|
||||||
onSnippetClick : function(){},
|
onSnippetClick : function(){},
|
||||||
};
|
};
|
||||||
@@ -80,7 +108,7 @@ const SnippetGroup = createClass({
|
|||||||
renderSnippets : function(){
|
renderSnippets : function(){
|
||||||
return _.map(this.props.snippets, (snippet)=>{
|
return _.map(this.props.snippets, (snippet)=>{
|
||||||
return <div className='snippet' key={snippet.name} onClick={()=>this.handleSnippetClick(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}
|
{snippet.name}
|
||||||
</div>;
|
</div>;
|
||||||
});
|
});
|
||||||
@@ -89,7 +117,7 @@ const SnippetGroup = createClass({
|
|||||||
render : function(){
|
render : function(){
|
||||||
return <div className='snippetGroup snippetBarButton'>
|
return <div className='snippetGroup snippetBarButton'>
|
||||||
<div className='text'>
|
<div className='text'>
|
||||||
<i className={`fa fa-fw ${this.props.icon}`} />
|
<i className={this.props.icon} />
|
||||||
<span className='groupName'>{this.props.groupName}</span>
|
<span className='groupName'>{this.props.groupName}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className='dropdown'>
|
<div className='dropdown'>
|
||||||
|
|||||||
@@ -1,12 +1,40 @@
|
|||||||
|
|
||||||
.snippetBar{
|
.snippetBar{
|
||||||
@height : 25px;
|
@menuHeight : 25px;
|
||||||
position : relative;
|
position : relative;
|
||||||
height : @height;
|
height : @menuHeight;
|
||||||
background-color : #ddd;
|
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{
|
.snippetBarButton{
|
||||||
height : @height;
|
height : @menuHeight;
|
||||||
line-height : @height;
|
line-height : @menuHeight;
|
||||||
display : inline-block;
|
display : inline-block;
|
||||||
padding : 0px 5px;
|
padding : 0px 5px;
|
||||||
font-weight : 800;
|
font-weight : 800;
|
||||||
|
|||||||
@@ -47,11 +47,17 @@ const spellNames = [
|
|||||||
'Ultimate Rite of the Confetti Angel',
|
'Ultimate Rite of the Confetti Angel',
|
||||||
'Ultimate Ritual of Mouthwash',
|
'Ultimate Ritual of Mouthwash',
|
||||||
];
|
];
|
||||||
|
const itemNames = [
|
||||||
|
'Doorknob of Niceness',
|
||||||
|
'Paper Armor of Folding',
|
||||||
|
'Mixtape of Sadness',
|
||||||
|
'Staff of Endless Confetti',
|
||||||
|
];
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|
||||||
spellList : function(){
|
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 content = _.map(levels, (level)=>{
|
||||||
const spells = _.map(_.sampleSize(spellNames, _.random(5, 15)), (spell)=>{
|
const spells = _.map(_.sampleSize(spellNames, _.random(5, 15)), (spell)=>{
|
||||||
@@ -60,7 +66,7 @@ module.exports = {
|
|||||||
return `##### ${level} \n${spells} \n`;
|
return `##### ${level} \n${spells} \n`;
|
||||||
}).join('\n');
|
}).join('\n');
|
||||||
|
|
||||||
return `<div class='spellList'>\n${content}\n</div>`;
|
return `{{spellList\n${content}\n}}`;
|
||||||
},
|
},
|
||||||
|
|
||||||
spell : function(){
|
spell : function(){
|
||||||
@@ -76,16 +82,28 @@ module.exports = {
|
|||||||
return [
|
return [
|
||||||
`#### ${_.sample(spellNames)}`,
|
`#### ${_.sample(spellNames)}`,
|
||||||
`*${_.sample(level)}-level ${_.sample(spellSchools)}*`,
|
`*${_.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. ',
|
'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.',
|
'A *continual flame* can be covered or hidden but not smothered or quenched.',
|
||||||
'\n\n\n'
|
'\n\n\n'
|
||||||
].join('\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 _ = require('lodash');
|
||||||
|
const dedent = require('dedent-tabs').default;
|
||||||
|
|
||||||
const genList = function(list, max){
|
const genList = function(list, max){
|
||||||
return _.sampleSize(list, _.random(0, max)).join(', ') || 'None';
|
return _.sampleSize(list, _.random(0, max)).join(', ') || 'None';
|
||||||
@@ -86,7 +87,7 @@ const getAlignment = function(){
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getStats = function(){
|
const getStats = function(){
|
||||||
return `>|${_.times(6, function(){
|
return `|${_.times(6, function(){
|
||||||
const num = _.random(1, 20);
|
const num = _.random(1, 20);
|
||||||
const mod = Math.ceil(num/2 - 5);
|
const mod = Math.ceil(num/2 - 5);
|
||||||
return `${num} (${mod >= 0 ? `+${mod}` : mod})`;
|
return `${num} (${mod >= 0 ? `+${mod}` : mod})`;
|
||||||
@@ -95,12 +96,12 @@ const getStats = function(){
|
|||||||
|
|
||||||
const genAbilities = function(){
|
const genAbilities = function(){
|
||||||
return _.sample([
|
return _.sample([
|
||||||
'> ***Pack Tactics.*** These guys work together. Like super well, you don\'t even know.',
|
'***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.',
|
'***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.',
|
'***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.',
|
'***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.',
|
'***Sassiness.*** When questioned, this creature will talk back instead of answering.',
|
||||||
'> ***Big Jerk.*** Thinks he is just *waaaay* better than you.',
|
'***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'
|
'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 = {
|
module.exports = {
|
||||||
|
|
||||||
full : function(){
|
monster : function(classes, genLines){
|
||||||
return `${[
|
return dedent`
|
||||||
'___',
|
{{${classes}
|
||||||
'___',
|
## ${getMonsterName()}
|
||||||
`> ## ${getMonsterName()}`,
|
*${getType()}, ${getAlignment()}*
|
||||||
`>*${getType()}, ${getAlignment()}*`,
|
___
|
||||||
'> ___',
|
**Armor Class** :: ${_.random(10, 20)} (chain mail, shield)
|
||||||
`> - **Armor Class** ${_.random(10, 20)}`,
|
**Hit Points** :: ${_.random(1, 150)}(1d4 + 5)
|
||||||
`> - **Hit Points** ${_.random(1, 150)}(1d4 + 5)`,
|
**Speed** :: ${_.random(0, 50)}ft.
|
||||||
`> - **Speed** ${_.random(0, 50)}ft.`,
|
___
|
||||||
'>___',
|
| STR | DEX | CON | INT | WIS | CHA |
|
||||||
'>|STR|DEX|CON|INT|WIS|CHA|',
|
|:-----:|:-----:|:-----:|:-----:|:-----:|:-----:|
|
||||||
'>|:---:|:---:|:---:|:---:|:---:|:---:|',
|
${getStats()}
|
||||||
getStats(),
|
___
|
||||||
'>___',
|
**Condition Immunities** :: ${genList(['groggy', 'swagged', 'weak-kneed', 'buzzed', 'groovy', 'melancholy', 'drunk'], 3)}
|
||||||
`> - **Condition Immunities** ${genList(['groggy', 'swagged', 'weak-kneed', 'buzzed', 'groovy', 'melancholy', 'drunk'], 3)}`,
|
**Senses** :: darkvision 60 ft., passive Perception ${_.random(3, 20)}
|
||||||
`> - **Senses** passive Perception ${_.random(3, 20)}`,
|
**Languages** :: ${genList(['Common', 'Pottymouth', 'Gibberish', 'Latin', 'Jive'], 2)}
|
||||||
`> - **Languages** ${genList(['Common', 'Pottymouth', 'Gibberish', 'Latin', 'Jive'], 2)}`,
|
**Challenge** :: ${_.random(0, 15)} (${_.random(10, 10000)} XP)
|
||||||
`> - **Challenge** ${_.random(0, 15)} (${_.random(10, 10000)} XP)`,
|
___
|
||||||
'> ___',
|
:
|
||||||
_.times(_.random(3, 6), function(){
|
${_.times(_.random(genLines, genLines + 2), function(){return genAbilities();}).join('\n\t\t\t\n\t\t\t')}
|
||||||
return genAbilities();
|
:
|
||||||
}).join('\n>\n'),
|
### Actions
|
||||||
'> ### Actions',
|
${_.times(_.random(genLines, genLines + 2), function(){return genAction();}).join('\n\t\t\t\n\t\t\t')}
|
||||||
_.times(_.random(4, 6), function(){
|
}}
|
||||||
return genAction();
|
\n`;
|
||||||
}).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`;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,79 +6,113 @@ const MonsterBlockGen = require('./monsterblock.gen.js');
|
|||||||
const ClassFeatureGen = require('./classfeature.gen.js');
|
const ClassFeatureGen = require('./classfeature.gen.js');
|
||||||
const CoverPageGen = require('./coverpage.gen.js');
|
const CoverPageGen = require('./coverpage.gen.js');
|
||||||
const TableOfContentsGen = require('./tableOfContents.gen.js');
|
const TableOfContentsGen = require('./tableOfContents.gen.js');
|
||||||
|
const dedent = require('dedent-tabs').default;
|
||||||
|
|
||||||
|
|
||||||
module.exports = [
|
module.exports = [
|
||||||
|
|
||||||
{
|
{
|
||||||
groupName : 'Editor',
|
groupName : 'Editor',
|
||||||
icon : 'fa-pencil',
|
icon : 'fas fa-pencil-alt',
|
||||||
snippets : [
|
snippets : [
|
||||||
{
|
{
|
||||||
name : 'Column Break',
|
name : 'Column Break',
|
||||||
icon : 'fa-columns',
|
icon : 'fas fa-columns',
|
||||||
gen : '```\n```\n\n'
|
gen : '\n\\column\n'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name : 'New Page',
|
name : 'New Page',
|
||||||
icon : 'fa-file-text',
|
icon : 'fas fa-file-alt',
|
||||||
gen : '\\page\n\n'
|
gen : '\n\\page\n'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name : 'Vertical Spacing',
|
name : 'Vertical Spacing',
|
||||||
icon : 'fa-arrows-v',
|
icon : 'fas fa-arrows-alt-v',
|
||||||
gen : '<div style=\'margin-top:140px\'></div>\n\n'
|
gen : '\n::::\n'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Horizontal Spacing',
|
||||||
|
icon : 'fas fa-arrows-alt-h',
|
||||||
|
gen : ' {{width:100px}} '
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name : 'Wide Block',
|
name : 'Wide Block',
|
||||||
icon : 'fa-arrows-h',
|
icon : 'fas fa-window-maximize',
|
||||||
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'
|
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 rely on the automatic column-break rather than \`\column\` if
|
||||||
|
you mix columns and wide blocks on the same page.
|
||||||
|
}}
|
||||||
|
\n`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name : 'Image',
|
name : 'Image',
|
||||||
icon : 'fa-image',
|
icon : 'fas fa-image',
|
||||||
gen : [
|
gen : dedent`
|
||||||
'<img ',
|
 {width:325px}
|
||||||
' src=\'https://s-media-cache-ak0.pinimg.com/736x/4a/81/79/4a8179462cfdf39054a418efd4cb743e.jpg\' ',
|
Credit: Kyounghwan Kim`
|
||||||
' style=\'width:325px\' />',
|
|
||||||
'Credit: Kyounghwan Kim'
|
|
||||||
].join('\n')
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name : 'Background Image',
|
name : 'Background Image',
|
||||||
icon : 'fa-tree',
|
icon : 'fas fa-tree',
|
||||||
gen : [
|
gen : ` {position:absolute,top:50px,right:30px,width:280px}`
|
||||||
'<img ',
|
|
||||||
' src=\'http://i.imgur.com/hMna6G0.png\' ',
|
|
||||||
' style=\'position:absolute; top:50px; right:30px; width:280px\' />'
|
|
||||||
].join('\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',
|
name : 'Page Number',
|
||||||
icon : 'fa-bookmark',
|
icon : 'fas fa-bookmark',
|
||||||
gen : '<div class=\'pageNumber\'>1</div>\n<div class=\'footnote\'>PART 1 | FANCINESS</div>\n\n'
|
gen : '{{pageNumber 1}}\n{{footnote PART 1 | SECTION NAME}}\n\n'
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
name : 'Auto-incrementing Page Number',
|
name : 'Auto-incrementing Page Number',
|
||||||
icon : 'fa-sort-numeric-asc',
|
icon : 'fas fa-sort-numeric-down',
|
||||||
gen : '<div class=\'pageNumber auto\'></div>\n'
|
gen : '{{pageNumber,auto}}\n{{footnote PART 1 | SECTION NAME}}\n\n'
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
name : 'Link to page',
|
name : 'Link to page',
|
||||||
icon : 'fa-link',
|
icon : 'fas fa-link',
|
||||||
gen : '[Click here](#p3) to go to page 3\n'
|
gen : '[Click here](#p3) to go to page 3\n'
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
name : 'Table of Contents',
|
name : 'Table of Contents',
|
||||||
icon : 'fa-book',
|
icon : 'fas fa-book',
|
||||||
gen : TableOfContentsGen
|
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>'
|
||||||
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -87,64 +121,76 @@ module.exports = [
|
|||||||
|
|
||||||
{
|
{
|
||||||
groupName : 'PHB',
|
groupName : 'PHB',
|
||||||
icon : 'fa-book',
|
icon : 'fas fa-book',
|
||||||
snippets : [
|
snippets : [
|
||||||
{
|
{
|
||||||
name : 'Spell',
|
name : 'Spell',
|
||||||
icon : 'fa-magic',
|
icon : 'fas fa-magic',
|
||||||
gen : MagicGen.spell,
|
gen : MagicGen.spell,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name : 'Spell List',
|
name : 'Spell List',
|
||||||
icon : 'fa-list',
|
icon : 'fas fa-scroll',
|
||||||
gen : MagicGen.spellList,
|
gen : MagicGen.spellList,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name : 'Class Feature',
|
name : 'Class Feature',
|
||||||
icon : 'fa-trophy',
|
icon : 'fas fa-mask',
|
||||||
gen : ClassFeatureGen,
|
gen : ClassFeatureGen,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name : 'Note',
|
name : 'Note',
|
||||||
icon : 'fa-sticky-note',
|
icon : 'fas fa-sticky-note',
|
||||||
gen : function(){
|
gen : function(){
|
||||||
return [
|
return dedent`
|
||||||
'> ##### Time to Drop Knowledge',
|
{{note
|
||||||
'> Use notes to point out some interesting information. ',
|
##### Time to Drop Knowledge
|
||||||
'> ',
|
Use notes to point out some interesting information.
|
||||||
'> **Tables and lists** both work within a note.'
|
|
||||||
].join('\n');
|
**Tables and lists** both work within a note.
|
||||||
|
}}
|
||||||
|
\n`;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name : 'Descriptive Text Box',
|
name : 'Descriptive Text Box',
|
||||||
icon : 'fa-sticky-note-o',
|
icon : 'fas fa-comment-alt',
|
||||||
gen : function(){
|
gen : function(){
|
||||||
return [
|
return dedent`
|
||||||
'<div class=\'descriptive\'>',
|
{{descriptive
|
||||||
'##### Time to Drop Knowledge',
|
##### Time to Drop Knowledge
|
||||||
'Use notes to point out some interesting information. ',
|
Use descriptive boxes to highlight text that should be read aloud.
|
||||||
'',
|
|
||||||
'**Tables and lists** both work within a note.',
|
**Tables and lists** both work within a descriptive box.
|
||||||
'</div>'
|
}}
|
||||||
].join('\n');
|
\n`;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name : 'Monster Stat Block (unframed)',
|
||||||
|
icon : 'fas fa-paw',
|
||||||
|
gen : MonsterBlockGen.monster('monster', 2),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name : 'Monster Stat Block',
|
name : 'Monster Stat Block',
|
||||||
icon : 'fa-bug',
|
icon : 'fas fa-spider',
|
||||||
gen : MonsterBlockGen.half,
|
gen : MonsterBlockGen.monster('monster,frame', 2),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name : 'Wide Monster Stat Block',
|
name : 'Wide Monster Stat Block',
|
||||||
icon : 'fa-paw',
|
icon : 'fas fa-dragon',
|
||||||
gen : MonsterBlockGen.full,
|
gen : MonsterBlockGen.monster('monster,frame,wide', 4),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name : 'Cover Page',
|
name : 'Cover Page',
|
||||||
icon : 'fa-file-word-o',
|
icon : 'fas fa-file-word',
|
||||||
gen : CoverPageGen,
|
gen : CoverPageGen,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name : 'Magic Item',
|
||||||
|
icon : 'fas fa-hat-wizard',
|
||||||
|
gen : MagicGen.item,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -154,79 +200,77 @@ module.exports = [
|
|||||||
|
|
||||||
{
|
{
|
||||||
groupName : 'Tables',
|
groupName : 'Tables',
|
||||||
icon : 'fa-table',
|
icon : 'fas fa-table',
|
||||||
snippets : [
|
snippets : [
|
||||||
{
|
{
|
||||||
name : 'Class Table',
|
name : 'Class Table',
|
||||||
icon : 'fa-table',
|
icon : 'fas fa-table',
|
||||||
gen : ClassTableGen.full,
|
gen : ClassTableGen.full,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name : 'Half Class Table',
|
name : 'Half Class Table',
|
||||||
icon : 'fa-list-alt',
|
icon : 'fas fa-list-alt',
|
||||||
gen : ClassTableGen.half,
|
gen : ClassTableGen.half,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name : 'Table',
|
name : 'Table',
|
||||||
icon : 'fa-th-list',
|
icon : 'fas fa-th-list',
|
||||||
gen : function(){
|
gen : function(){
|
||||||
return [
|
return dedent`
|
||||||
'##### Cookie Tastiness',
|
##### Character Advancement
|
||||||
'| Tastiness | Cookie Type |',
|
| Experience Points | Level | Proficiency Bonus |
|
||||||
'|:----:|:-------------|',
|
|:------------------|:-----:|:-----------------:|
|
||||||
'| -5 | Raisin |',
|
| 0 | 1 | +2 |
|
||||||
'| 8th | Chocolate Chip |',
|
| 300 | 2 | +2 |
|
||||||
'| 11th | 2 or lower |',
|
| 900 | 3 | +2 |
|
||||||
'| 14th | 3 or lower |',
|
| 2,700 | 4 | +2 |
|
||||||
'| 17th | 4 or lower |\n\n',
|
| 6,500 | 5 | +3 |
|
||||||
].join('\n');
|
| 14,000 | 6 | +3 |
|
||||||
},
|
\n`;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name : 'Wide Table',
|
name : 'Wide Table',
|
||||||
icon : 'fa-list',
|
icon : 'fas fa-list',
|
||||||
gen : function(){
|
gen : function(){
|
||||||
return [
|
return dedent`
|
||||||
'<div class=\'wide\'>',
|
{{wide
|
||||||
'##### Cookie Tastiness',
|
##### Weapons
|
||||||
'| Tastiness | Cookie Type |',
|
| Name | Cost | Damage | Weight | Properties |
|
||||||
'|:----:|:-------------|',
|
|:------------------------|:-----:|:----------------|--------:|:-----------|
|
||||||
'| -5 | Raisin |',
|
| *Simple Melee Weapons* | | | | |
|
||||||
'| 8th | Chocolate Chip |',
|
|   Club | 1 sp | 1d4 bludgeoning | 2 lb. | Light |
|
||||||
'| 11th | 2 or lower |',
|
|   Dagger | 2 gp | 1d4 piercing | 1 lb. | Finesse |
|
||||||
'| 14th | 3 or lower |',
|
|   Spear | 1 gp | 1d6 piercing | 3 lb. | Thrown |
|
||||||
'| 17th | 4 or lower |',
|
| *Simple Ranged Weapons* | | | | |
|
||||||
'</div>\n\n'
|
|   Dart | 5 cp | 1d4 piercig | 1/4 lb. | Finesse |
|
||||||
].join('\n');
|
|   Shortbow | 25 gp | 1d6 piercing | 2 lb. | Ammunition |
|
||||||
},
|
|   Sling | 1 sp | 1d4 bludgeoning | — | Ammunition |
|
||||||
|
}}
|
||||||
|
\n`;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name : 'Split Table',
|
name : 'Split Table',
|
||||||
icon : 'fa-th-large',
|
icon : 'fas fa-th-large',
|
||||||
gen : function(){
|
gen : function(){
|
||||||
return [
|
return dedent`
|
||||||
'<div style=\'column-count:2\'>',
|
##### Typical Difficulty Classes
|
||||||
'| d10 | Damage Type |',
|
{{column-count:2
|
||||||
'|:---:|:------------|',
|
| Task Difficulty | DC |
|
||||||
'| 1 | Acid |',
|
|:----------------|:--:|
|
||||||
'| 2 | Cold |',
|
| Very easy | 5 |
|
||||||
'| 3 | Fire |',
|
| Easy | 10 |
|
||||||
'| 4 | Force |',
|
| Medium | 15 |
|
||||||
'| 5 | Lightning |',
|
|
||||||
'',
|
| Task Difficulty | DC |
|
||||||
'```',
|
|:------------------|:--:|
|
||||||
'```',
|
| Hard | 20 |
|
||||||
'',
|
| Very hard | 25 |
|
||||||
'| d10 | Damage Type |',
|
| Nearly impossible | 30 |
|
||||||
'|:---:|:------------|',
|
}}
|
||||||
'| 6 | Necrotic |',
|
\n`;
|
||||||
'| 7 | Poison |',
|
}
|
||||||
'| 8 | Psychic |',
|
|
||||||
'| 9 | Radiant |',
|
|
||||||
'| 10 | Thunder |',
|
|
||||||
'</div>\n\n',
|
|
||||||
].join('\n');
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -238,11 +282,11 @@ module.exports = [
|
|||||||
|
|
||||||
{
|
{
|
||||||
groupName : 'Print',
|
groupName : 'Print',
|
||||||
icon : 'fa-print',
|
icon : 'fas fa-print',
|
||||||
snippets : [
|
snippets : [
|
||||||
{
|
{
|
||||||
name : 'A4 PageSize',
|
name : 'A4 PageSize',
|
||||||
icon : 'fa-file-o',
|
icon : 'far fa-file',
|
||||||
gen : ['<style>',
|
gen : ['<style>',
|
||||||
' .phb{',
|
' .phb{',
|
||||||
' width : 210mm;',
|
' width : 210mm;',
|
||||||
@@ -253,7 +297,7 @@ module.exports = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name : 'Ink Friendly',
|
name : 'Ink Friendly',
|
||||||
icon : 'fa-tint',
|
icon : 'fas fa-tint',
|
||||||
gen : ['<style>',
|
gen : ['<style>',
|
||||||
' .phb{ background : white;}',
|
' .phb{ background : white;}',
|
||||||
' .phb img{ display : none;}',
|
' .phb img{ display : none;}',
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
|
const dedent = require('dedent-tabs').default;
|
||||||
|
|
||||||
const getTOC = (pages)=>{
|
const getTOC = (pages)=>{
|
||||||
const add1 = (title, page)=>{
|
const add1 = (title, page)=>{
|
||||||
@@ -9,7 +10,7 @@ const getTOC = (pages)=>{
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
const add2 = (title, page)=>{
|
const add2 = (title, page)=>{
|
||||||
if(!_.last(res)) add1('', page);
|
if(!_.last(res)) add1(null, page);
|
||||||
_.last(res).children.push({
|
_.last(res).children.push({
|
||||||
title : title,
|
title : title,
|
||||||
page : page + 1,
|
page : page + 1,
|
||||||
@@ -17,8 +18,8 @@ const getTOC = (pages)=>{
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
const add3 = (title, page)=>{
|
const add3 = (title, page)=>{
|
||||||
if(!_.last(res)) add1('', page);
|
if(!_.last(res)) add1(null, page);
|
||||||
if(!_.last(_.last(res).children)) add2('', page);
|
if(!_.last(_.last(res).children)) add2(null, page);
|
||||||
_.last(_.last(res).children).children.push({
|
_.last(_.last(res).children).children.push({
|
||||||
title : title,
|
title : title,
|
||||||
page : page + 1,
|
page : page + 1,
|
||||||
@@ -48,16 +49,24 @@ const getTOC = (pages)=>{
|
|||||||
};
|
};
|
||||||
|
|
||||||
module.exports = function(brew){
|
module.exports = function(brew){
|
||||||
const pages = brew.split('\\page');
|
const pages = brew.text.split('\\page');
|
||||||
const TOC = getTOC(pages);
|
const TOC = getTOC(pages);
|
||||||
const markdown = _.reduce(TOC, (r, g1, idx1)=>{
|
const markdown = _.reduce(TOC, (r, g1, idx1)=>{
|
||||||
r.push(`- **[${idx1 + 1} ${g1.title}](#p${g1.page})**`);
|
if(g1.title !== null) {
|
||||||
|
r.push(`\t\t- ### [{{ ${g1.title}}}{{ ${g1.page}}}](#p${g1.page})`);
|
||||||
|
}
|
||||||
if(g1.children.length){
|
if(g1.children.length){
|
||||||
_.each(g1.children, (g2, idx2)=>{
|
_.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){
|
if(g2.children.length){
|
||||||
_.each(g2.children, (g3, idx3)=>{
|
_.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;
|
return r;
|
||||||
}, []).join('\n');
|
}, []).join('\n');
|
||||||
|
|
||||||
return `<div class='toc'>
|
return dedent`
|
||||||
##### Table Of Contents
|
{{toc,wide
|
||||||
|
# Table Of Contents
|
||||||
|
|
||||||
${markdown}
|
${markdown}
|
||||||
</div>\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)', '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`;
|
||||||
|
}
|
||||||
|
};
|
||||||
289
client/homebrew/editor/snippetbar/snippetsLegacy/snippets.js
Normal file
@@ -0,0 +1,289 @@
|
|||||||
|
/* 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');
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = [
|
||||||
|
|
||||||
|
{
|
||||||
|
groupName : 'Editor',
|
||||||
|
icon : 'fas fa-pencil-alt',
|
||||||
|
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>'
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/************************* PHB ********************/
|
||||||
|
|
||||||
|
{
|
||||||
|
groupName : 'PHB',
|
||||||
|
icon : 'fas fa-book',
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/********************* TABLES *********************/
|
||||||
|
|
||||||
|
{
|
||||||
|
groupName : 'Tables',
|
||||||
|
icon : 'fas fa-table',
|
||||||
|
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',
|
||||||
|
snippets : [
|
||||||
|
{
|
||||||
|
name : 'A4 PageSize',
|
||||||
|
icon : 'far fa-file',
|
||||||
|
gen : ['<style>',
|
||||||
|
' .phb{',
|
||||||
|
' width : 210mm;',
|
||||||
|
' height : 296.8mm;',
|
||||||
|
' }',
|
||||||
|
'</style>'
|
||||||
|
].join('\n')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Ink Friendly',
|
||||||
|
icon : 'fas fa-tint',
|
||||||
|
gen : ['<style>',
|
||||||
|
' .phb{ background : white;}',
|
||||||
|
' .phb img{ display : none;}',
|
||||||
|
' .phb hr+blockquote{background : white;}',
|
||||||
|
'</style>',
|
||||||
|
''
|
||||||
|
].join('\n')
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
];
|
||||||
@@ -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 : '',
|
changelog : '',
|
||||||
version : '0.0.0',
|
version : '0.0.0',
|
||||||
account : null,
|
account : null,
|
||||||
|
enable_v3 : false,
|
||||||
brew : {
|
brew : {
|
||||||
title : '',
|
title : '',
|
||||||
text : '',
|
text : '',
|
||||||
@@ -33,7 +34,7 @@ const Homebrew = createClass({
|
|||||||
componentWillMount : function() {
|
componentWillMount : function() {
|
||||||
global.account = this.props.account;
|
global.account = this.props.account;
|
||||||
global.version = this.props.version;
|
global.version = this.props.version;
|
||||||
|
global.enable_v3 = this.props.enable_v3;
|
||||||
},
|
},
|
||||||
render : function (){
|
render : function (){
|
||||||
return (
|
return (
|
||||||
@@ -42,12 +43,13 @@ const Homebrew = createClass({
|
|||||||
<Switch>
|
<Switch>
|
||||||
<Route path='/edit/:id' component={(routeProps)=><EditPage id={routeProps.match.params.id} brew={this.props.brew} />}/>
|
<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='/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='/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/: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='/print' exact component={(routeProps)=><PrintPage query={queryString.parse(routeProps.location.search)} />}/>
|
||||||
<Route path='/new' exact component={NewPage}/>
|
<Route path='/changelog' exact component={()=><SharePage brew={this.props.brew} />}/>
|
||||||
<Route path='/changelog' exact component={()=><SharePage brew={{ title: 'Changelog', text: this.props.changelog }} />}/>
|
<Route path='/' component={()=><HomePage brew={this.props.brew} />}/>
|
||||||
<Route path='/' component={()=><HomePage welcomeText={this.props.welcomeText}/>}/>
|
|
||||||
</Switch>
|
</Switch>
|
||||||
</div>
|
</div>
|
||||||
</Router>
|
</Router>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
@import 'naturalcrit/styles/core.less';
|
@import 'naturalcrit/styles/core.less';
|
||||||
.homebrew{
|
.homebrew{
|
||||||
height : 100%;
|
height : 100%;
|
||||||
.page{
|
.sitePage{
|
||||||
display : flex;
|
display : flex;
|
||||||
height : 100%;
|
height : 100%;
|
||||||
background-color : @steel;
|
background-color : @steel;
|
||||||
|
|||||||
@@ -20,12 +20,12 @@ const Account = createClass({
|
|||||||
|
|
||||||
render : function(){
|
render : function(){
|
||||||
if(global.account){
|
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}
|
{global.account.username}
|
||||||
</Nav.item>;
|
</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
|
login
|
||||||
</Nav.item>;
|
</Nav.item>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ module.exports = function(props){
|
|||||||
return <Nav.item
|
return <Nav.item
|
||||||
newTab={true}
|
newTab={true}
|
||||||
color='red'
|
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')}`} >
|
href={`https://www.reddit.com/r/homebrewery/submit?selftext=true&title=${encodeURIComponent('[Issue] Describe Your Issue Here')}`} >
|
||||||
report issue
|
report issue
|
||||||
</Nav.item>;
|
</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 React = require('react');
|
||||||
const createClass = require('create-react-class');
|
|
||||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||||
|
|
||||||
module.exports = function(props){
|
module.exports = function(props){
|
||||||
@@ -8,7 +7,7 @@ module.exports = function(props){
|
|||||||
newTab={true}
|
newTab={true}
|
||||||
href='https://www.patreon.com/NaturalCrit'
|
href='https://www.patreon.com/NaturalCrit'
|
||||||
color='green'
|
color='green'
|
||||||
icon='fa-heart'>
|
icon='fas fa-heart'>
|
||||||
help out
|
help out
|
||||||
</Nav.item>;
|
</Nav.item>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ const createClass = require('create-react-class');
|
|||||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||||
|
|
||||||
module.exports = function(props){
|
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
|
get PDF
|
||||||
</Nav.item>;
|
</Nav.item>;
|
||||||
};
|
};
|
||||||
@@ -80,7 +80,8 @@ const RecentItems = createClass({
|
|||||||
|
|
||||||
componentDidUpdate : function(prevProps) {
|
componentDidUpdate : function(prevProps) {
|
||||||
if(prevProps.brew && this.props.brew.editId !== prevProps.brew.editId) {
|
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;
|
let prevEditId = prevProps.brew.editId;
|
||||||
if(prevProps.brew.googleId){
|
if(prevProps.brew.googleId){
|
||||||
prevEditId = `${prevProps.brew.googleId}${prevProps.brew.editId}`;
|
prevEditId = `${prevProps.brew.googleId}${prevProps.brew.editId}`;
|
||||||
@@ -143,7 +144,7 @@ const RecentItems = createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
render : function(){
|
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)}
|
onMouseEnter={()=>this.handleDropdown(true)}
|
||||||
onMouseLeave={()=>this.handleDropdown(false)}>
|
onMouseLeave={()=>this.handleDropdown(false)}>
|
||||||
{this.props.text}
|
{this.props.text}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ const { Meta } = require('vitreum/headtags');
|
|||||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||||
const Navbar = require('../../navbar/navbar.jsx');
|
const Navbar = require('../../navbar/navbar.jsx');
|
||||||
|
|
||||||
|
const NewBrew = require('../../navbar/newbrew.navitem.jsx');
|
||||||
const ReportIssue = require('../../navbar/issue.navitem.jsx');
|
const ReportIssue = require('../../navbar/issue.navitem.jsx');
|
||||||
const PrintLink = require('../../navbar/print.navitem.jsx');
|
const PrintLink = require('../../navbar/print.navitem.jsx');
|
||||||
const Account = require('../../navbar/account.navitem.jsx');
|
const Account = require('../../navbar/account.navitem.jsx');
|
||||||
@@ -30,33 +31,37 @@ const EditPage = createClass({
|
|||||||
return {
|
return {
|
||||||
brew : {
|
brew : {
|
||||||
text : '',
|
text : '',
|
||||||
|
style : '',
|
||||||
shareId : null,
|
shareId : null,
|
||||||
editId : null,
|
editId : null,
|
||||||
createdAt : null,
|
createdAt : null,
|
||||||
updatedAt : null,
|
updatedAt : null,
|
||||||
gDrive : false,
|
gDrive : false,
|
||||||
|
trashed : false,
|
||||||
|
|
||||||
title : '',
|
title : '',
|
||||||
description : '',
|
description : '',
|
||||||
tags : '',
|
tags : '',
|
||||||
published : false,
|
published : false,
|
||||||
authors : [],
|
authors : [],
|
||||||
systems : []
|
systems : [],
|
||||||
|
renderer : 'legacy'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState : function() {
|
getInitialState : function() {
|
||||||
return {
|
return {
|
||||||
brew : this.props.brew,
|
brew : this.props.brew,
|
||||||
|
isSaving : false,
|
||||||
isSaving : false,
|
isPending : false,
|
||||||
isPending : false,
|
alertTrashedGoogleBrew : this.props.brew.trashed,
|
||||||
saveGoogle : this.props.brew.googleId ? true : false,
|
alertLoginToTransfer : false,
|
||||||
confirmGoogleTransfer : false,
|
saveGoogle : this.props.brew.googleId ? true : false,
|
||||||
errors : null,
|
confirmGoogleTransfer : false,
|
||||||
htmlErrors : Markdown.validate(this.props.brew.text),
|
errors : null,
|
||||||
url : ''
|
htmlErrors : Markdown.validate(this.props.brew.text),
|
||||||
|
url : ''
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
savedBrew : null,
|
savedBrew : null,
|
||||||
@@ -66,6 +71,8 @@ const EditPage = createClass({
|
|||||||
url : window.location.href
|
url : window.location.href
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.savedBrew = JSON.parse(JSON.stringify(this.props.brew)); //Deep copy
|
||||||
|
|
||||||
this.trySave();
|
this.trySave();
|
||||||
window.onbeforeunload = ()=>{
|
window.onbeforeunload = ()=>{
|
||||||
if(this.state.isSaving || this.state.isPending){
|
if(this.state.isSaving || this.state.isPending){
|
||||||
@@ -100,17 +107,8 @@ const EditPage = createClass({
|
|||||||
this.refs.editor.update();
|
this.refs.editor.update();
|
||||||
},
|
},
|
||||||
|
|
||||||
handleMetadataChange : function(metadata){
|
|
||||||
this.setState((prevState)=>({
|
|
||||||
brew : _.merge({}, prevState.brew, metadata),
|
|
||||||
isPending : true,
|
|
||||||
}), ()=>this.trySave());
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
handleTextChange : function(text){
|
handleTextChange : function(text){
|
||||||
|
//If there are errors, run the validator on every change to give quick feedback
|
||||||
//If there are errors, run the validator on everychange to give quick feedback
|
|
||||||
let htmlErrors = this.state.htmlErrors;
|
let htmlErrors = this.state.htmlErrors;
|
||||||
if(htmlErrors.length) htmlErrors = Markdown.validate(text);
|
if(htmlErrors.length) htmlErrors = Markdown.validate(text);
|
||||||
|
|
||||||
@@ -121,9 +119,23 @@ const EditPage = createClass({
|
|||||||
}), ()=>this.trySave());
|
}), ()=>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(){
|
hasChanges : function(){
|
||||||
const savedBrew = this.savedBrew ? this.savedBrew : this.props.brew;
|
return !_.isEqual(this.state.brew, this.savedBrew);
|
||||||
return !_.isEqual(this.state.brew, savedBrew);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
trySave : function(){
|
trySave : function(){
|
||||||
@@ -136,12 +148,27 @@ const EditPage = createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
handleGoogleClick : function(){
|
handleGoogleClick : function(){
|
||||||
|
if(!global.account?.googleId) {
|
||||||
|
this.setState({
|
||||||
|
alertLoginToTransfer : true
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.setState((prevState)=>({
|
this.setState((prevState)=>({
|
||||||
confirmGoogleTransfer : !prevState.confirmGoogleTransfer
|
confirmGoogleTransfer : !prevState.confirmGoogleTransfer
|
||||||
}));
|
}));
|
||||||
this.clearErrors();
|
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(){
|
toggleGoogleStorage : function(){
|
||||||
this.setState((prevState)=>({
|
this.setState((prevState)=>({
|
||||||
saveGoogle : !prevState.saveGoogle,
|
saveGoogle : !prevState.saveGoogle,
|
||||||
@@ -177,7 +204,7 @@ const EditPage = createClass({
|
|||||||
.catch((err)=>{
|
.catch((err)=>{
|
||||||
console.log(err.status === 401
|
console.log(err.status === 401
|
||||||
? 'Not signed in!'
|
? 'Not signed in!'
|
||||||
: 'Error Saving to Google!');
|
: 'Error Transferring to Google!');
|
||||||
this.setState({ errors: err, saveGoogle: false });
|
this.setState({ errors: err, saveGoogle: false });
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -200,7 +227,7 @@ const EditPage = createClass({
|
|||||||
console.log(err.status === 401
|
console.log(err.status === 401
|
||||||
? 'Not signed in!'
|
? 'Not signed in!'
|
||||||
: 'Error Saving to Google!');
|
: 'Error Saving to Google!');
|
||||||
this.setState({ errors: err, saveGoogle: false });
|
this.setState({ errors: err });
|
||||||
return;
|
return;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -250,39 +277,44 @@ const EditPage = createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
renderGoogleDriveIcon : function(){
|
renderGoogleDriveIcon : function(){
|
||||||
if(this.state.saveGoogle) {
|
return <Nav.item className='googleDriveStorage' onClick={this.handleGoogleClick}>
|
||||||
return <Nav.item className='googleDriveStorage' onClick={this.handleGoogleClick}>
|
{this.state.saveGoogle
|
||||||
<img src={googleDriveActive} alt='googleDriveActive' />
|
? <img src={googleDriveActive} alt='googleDriveActive'/>
|
||||||
|
: <img src={googleDriveInactive} alt='googleDriveInactive'/>
|
||||||
|
}
|
||||||
|
|
||||||
{this.state.confirmGoogleTransfer &&
|
{this.state.confirmGoogleTransfer &&
|
||||||
<div className='errorContainer'>
|
<div className='errorContainer' onClick={this.closeAlerts}>
|
||||||
Would you like to transfer this brew from your Google Drive storage back to the Homebrewery?<br />
|
{ this.state.saveGoogle
|
||||||
<div className='confirm' onClick={this.toggleGoogleStorage}>
|
? `Would you like to transfer this brew from your Google Drive storage back to the Homebrewery?`
|
||||||
Yes
|
: `Would you like to transfer this brew from the Homebrewery to your personal Google Drive storage?`
|
||||||
</div>
|
}
|
||||||
<div className='deny'>
|
<br />
|
||||||
No
|
<div className='confirm' onClick={this.toggleGoogleStorage}>
|
||||||
</div>
|
Yes
|
||||||
</div>
|
</div>
|
||||||
}
|
<div className='deny'>
|
||||||
</Nav.item>;
|
No
|
||||||
} else {
|
</div>
|
||||||
return <Nav.item className='googleDriveStorage' onClick={this.handleGoogleClick}>
|
</div>
|
||||||
<img src={googleDriveInactive} alt='googleDriveInactive' />
|
}
|
||||||
|
|
||||||
{this.state.confirmGoogleTransfer &&
|
{this.state.alertLoginToTransfer &&
|
||||||
<div className='errorContainer'>
|
<div className='errorContainer' onClick={this.closeAlerts}>
|
||||||
Would you like to transfer this brew from the Homebrewery to your personal Google Drive storage?<br />
|
You must be signed in to a Google account to transfer
|
||||||
<div className='confirm' onClick={this.toggleGoogleStorage}>
|
between the homebrewery and Google Drive!
|
||||||
Yes
|
<a target='_blank' rel='noopener noreferrer'
|
||||||
</div>
|
href={`https://www.naturalcrit.com/login?redirect=${this.state.url}`}>
|
||||||
<div className='deny'>
|
<div className='confirm'>
|
||||||
No
|
Sign In
|
||||||
</div>
|
</div>
|
||||||
|
</a>
|
||||||
|
<div className='deny'>
|
||||||
|
Not Now
|
||||||
</div>
|
</div>
|
||||||
}
|
</div>
|
||||||
</Nav.item>;
|
}
|
||||||
}
|
</Nav.item>;
|
||||||
},
|
},
|
||||||
|
|
||||||
renderSaveButton : function(){
|
renderSaveButton : function(){
|
||||||
@@ -294,14 +326,14 @@ const EditPage = createClass({
|
|||||||
} catch (e){}
|
} catch (e){}
|
||||||
|
|
||||||
if(this.state.errors.status == '401'){
|
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!
|
Oops!
|
||||||
<div className='errorContainer' onClick={this.clearErrors}>
|
<div className='errorContainer' onClick={this.clearErrors}>
|
||||||
You must be signed in to a Google account
|
You must be signed in to a Google account
|
||||||
to save this to<br />Google Drive!<br />
|
to save this to<br />Google Drive!<br />
|
||||||
<a target='_blank' rel='noopener noreferrer'
|
<a target='_blank' rel='noopener noreferrer'
|
||||||
href={`http://naturalcrit.com/login?redirect=${this.state.url}`}>
|
href={`https://www.naturalcrit.com/login?redirect=${this.state.url}`}>
|
||||||
<div className='confirm' onClick={this.toggleGoogleStorage}>
|
<div className='confirm'>
|
||||||
Sign In
|
Sign In
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
@@ -312,12 +344,12 @@ const EditPage = createClass({
|
|||||||
</Nav.item>;
|
</Nav.item>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Nav.item className='save error' icon='fa-warning'>
|
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||||
Oops!
|
Oops!
|
||||||
<div className='errorContainer'>
|
<div className='errorContainer'>
|
||||||
Looks like there was a problem saving. <br />
|
Looks like there was a problem saving. <br />
|
||||||
Report the issue <a target='_blank' rel='noopener noreferrer'
|
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
|
here
|
||||||
</a>.
|
</a>.
|
||||||
</div>
|
</div>
|
||||||
@@ -325,10 +357,10 @@ const EditPage = createClass({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if(this.state.isSaving){
|
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()){
|
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){
|
if(!this.state.isPending && !this.state.isSaving){
|
||||||
return <Nav.item className='save saved'>saved.</Nav.item>;
|
return <Nav.item className='save saved'>saved.</Nav.item>;
|
||||||
@@ -343,6 +375,16 @@ const EditPage = createClass({
|
|||||||
|
|
||||||
renderNavbar : function(){
|
renderNavbar : function(){
|
||||||
return <Navbar>
|
return <Navbar>
|
||||||
|
|
||||||
|
{this.state.alertTrashedGoogleBrew &&
|
||||||
|
<div className='errorContainer' onClick={this.closeAlerts}>
|
||||||
|
This brew is currently in your Trash folder on Google Drive!<br />If you want to keep it, make sure to move it before it is deleted permanently!<br />
|
||||||
|
<div className='confirm'>
|
||||||
|
OK
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
<Nav.section>
|
<Nav.section>
|
||||||
<Nav.item className='brewTitle'>{this.state.brew.title}</Nav.item>
|
<Nav.item className='brewTitle'>{this.state.brew.title}</Nav.item>
|
||||||
</Nav.section>
|
</Nav.section>
|
||||||
@@ -350,19 +392,21 @@ const EditPage = createClass({
|
|||||||
<Nav.section>
|
<Nav.section>
|
||||||
{this.renderGoogleDriveIcon()}
|
{this.renderGoogleDriveIcon()}
|
||||||
{this.renderSaveButton()}
|
{this.renderSaveButton()}
|
||||||
|
<NewBrew />
|
||||||
<ReportIssue />
|
<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
|
Share
|
||||||
</Nav.item>
|
</Nav.item>
|
||||||
<PrintLink shareId={this.processShareId()} />
|
<PrintLink shareId={this.processShareId()} />
|
||||||
<RecentNavItem brew={this.state.brew} storageKey='edit' />
|
<RecentNavItem brew={this.state.brew} storageKey='edit' />
|
||||||
<Account />
|
<Account />
|
||||||
</Nav.section>
|
</Nav.section>
|
||||||
|
|
||||||
</Navbar>;
|
</Navbar>;
|
||||||
},
|
},
|
||||||
|
|
||||||
render : function(){
|
render : function(){
|
||||||
return <div className='editPage page'>
|
return <div className='editPage sitePage'>
|
||||||
<Meta name='robots' content='noindex, nofollow' />
|
<Meta name='robots' content='noindex, nofollow' />
|
||||||
{this.renderNavbar()}
|
{this.renderNavbar()}
|
||||||
|
|
||||||
@@ -370,12 +414,13 @@ const EditPage = createClass({
|
|||||||
<SplitPane onDragFinish={this.handleSplitMove} ref='pane'>
|
<SplitPane onDragFinish={this.handleSplitMove} ref='pane'>
|
||||||
<Editor
|
<Editor
|
||||||
ref='editor'
|
ref='editor'
|
||||||
value={this.state.brew.text}
|
brew={this.state.brew}
|
||||||
onChange={this.handleTextChange}
|
onTextChange={this.handleTextChange}
|
||||||
metadata={this.state.brew}
|
onStyleChange={this.handleStyleChange}
|
||||||
onMetadataChange={this.handleMetadataChange}
|
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>
|
</SplitPane>
|
||||||
</div>
|
</div>
|
||||||
</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{
|
.editPage{
|
||||||
.navItem.save{
|
.navItem.save{
|
||||||
width : 106px;
|
width : 106px;
|
||||||
text-align : center;
|
text-align : center;
|
||||||
|
position : relative;
|
||||||
&.saved{
|
&.saved{
|
||||||
cursor : initial;
|
cursor : initial;
|
||||||
color : #666;
|
color : #666;
|
||||||
@@ -21,17 +27,23 @@
|
|||||||
margin : -5px;
|
margin : -5px;
|
||||||
}
|
}
|
||||||
.errorContainer{
|
.errorContainer{
|
||||||
|
animation-name: glideDown;
|
||||||
|
animation-duration: 0.4s;
|
||||||
position : absolute;
|
position : absolute;
|
||||||
top : 100%;
|
top : 100%;
|
||||||
left : 50%;
|
left : 50%;
|
||||||
z-index : 1000;
|
z-index : 100000;
|
||||||
width : 140px;
|
width : 140px;
|
||||||
padding : 3px;
|
padding : 3px;
|
||||||
|
color : white;
|
||||||
background-color : #333;
|
background-color : #333;
|
||||||
border : 3px solid #444;
|
border : 3px solid #444;
|
||||||
border-radius : 5px;
|
border-radius : 5px;
|
||||||
transform : translate(-50% + 3px, 10px);
|
transform : translate(-50% + 3px, 10px);
|
||||||
text-align : center;
|
text-align : center;
|
||||||
|
font-size : 10px;
|
||||||
|
font-weight : 800;
|
||||||
|
text-transform : uppercase;
|
||||||
a{
|
a{
|
||||||
color : @teal;
|
color : @teal;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ const ErrorPage = createClass({
|
|||||||
text : '# Oops \n We could not find a brew with that id. **Sorry!**',
|
text : '# Oops \n We could not find a brew with that id. **Sorry!**',
|
||||||
|
|
||||||
render : function(){
|
render : function(){
|
||||||
return <div className='errorPage page'>
|
return <div className='errorPage sitePage'>
|
||||||
<Navbar ver={this.props.ver}>
|
<Navbar ver={this.props.ver}>
|
||||||
<Nav.section>
|
<Nav.section>
|
||||||
<Nav.item className='errorTitle'>
|
<Nav.item className='errorTitle'>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ const { Meta } = require('vitreum/headtags');
|
|||||||
|
|
||||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||||
const Navbar = require('../../navbar/navbar.jsx');
|
const Navbar = require('../../navbar/navbar.jsx');
|
||||||
|
const NewBrewItem = require('../../navbar/newbrew.navitem.jsx');
|
||||||
const IssueNavItem = require('../../navbar/issue.navitem.jsx');
|
const IssueNavItem = require('../../navbar/issue.navitem.jsx');
|
||||||
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
||||||
const AccountNavItem = require('../../navbar/account.navitem.jsx');
|
const AccountNavItem = require('../../navbar/account.navitem.jsx');
|
||||||
@@ -21,21 +22,22 @@ const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
|||||||
const HomePage = createClass({
|
const HomePage = createClass({
|
||||||
getDefaultProps : function() {
|
getDefaultProps : function() {
|
||||||
return {
|
return {
|
||||||
welcomeText : '',
|
brew : {
|
||||||
ver : '0.0.0'
|
text : '',
|
||||||
|
},
|
||||||
|
ver : '0.0.0'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
},
|
},
|
||||||
getInitialState : function() {
|
getInitialState : function() {
|
||||||
return {
|
return {
|
||||||
text : this.props.welcomeText
|
brew : this.props.brew,
|
||||||
|
welcomeText : this.props.brew.text
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
handleSave : function(){
|
handleSave : function(){
|
||||||
request.post('/api')
|
request.post('/api')
|
||||||
.send({
|
.send({
|
||||||
text : this.state.text
|
text : this.state.brew.text
|
||||||
})
|
})
|
||||||
.end((err, res)=>{
|
.end((err, res)=>{
|
||||||
if(err) return;
|
if(err) return;
|
||||||
@@ -48,14 +50,15 @@ const HomePage = createClass({
|
|||||||
},
|
},
|
||||||
handleTextChange : function(text){
|
handleTextChange : function(text){
|
||||||
this.setState({
|
this.setState({
|
||||||
text : text
|
brew : { text: text }
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
renderNavbar : function(){
|
renderNavbar : function(){
|
||||||
return <Navbar ver={this.props.ver}>
|
return <Navbar ver={this.props.ver}>
|
||||||
<Nav.section>
|
<Nav.section>
|
||||||
|
<NewBrewItem />
|
||||||
<IssueNavItem />
|
<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
|
Changelog
|
||||||
</Nav.item>
|
</Nav.item>
|
||||||
<RecentNavItem />
|
<RecentNavItem />
|
||||||
@@ -65,23 +68,29 @@ const HomePage = createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
render : function(){
|
render : function(){
|
||||||
return <div className='homePage page'>
|
return <div className='homePage sitePage'>
|
||||||
<Meta name='google-site-verification' content='NwnAQSSJZzAT7N-p5MY6ydQ7Njm67dtbu73ZSyE5Fy4' />
|
<Meta name='google-site-verification' content='NwnAQSSJZzAT7N-p5MY6ydQ7Njm67dtbu73ZSyE5Fy4' />
|
||||||
{this.renderNavbar()}
|
{this.renderNavbar()}
|
||||||
|
|
||||||
<div className='content'>
|
<div className='content'>
|
||||||
<SplitPane onDragFinish={this.handleSplitMove} ref='pane'>
|
<SplitPane onDragFinish={this.handleSplitMove} ref='pane'>
|
||||||
<Editor value={this.state.text} onChange={this.handleTextChange} showMetaButton={false} ref='editor'/>
|
<Editor
|
||||||
<BrewRenderer text={this.state.text} />
|
ref='editor'
|
||||||
|
brew={this.state.brew}
|
||||||
|
onTextChange={this.handleTextChange}
|
||||||
|
renderer={this.state.brew.renderer}
|
||||||
|
showEditButtons={false}
|
||||||
|
/>
|
||||||
|
<BrewRenderer text={this.state.brew.text} />
|
||||||
</SplitPane>
|
</SplitPane>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={cx('floatingSaveButton', { show: this.props.welcomeText != this.state.text })} onClick={this.handleSave}>
|
<div className={cx('floatingSaveButton', { show: this.state.welcomeText != this.state.brew.text })} onClick={this.handleSave}>
|
||||||
Save current <i className='fa fa-save' />
|
Save current <i className='fas fa-save' />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a href='/new' className='floatingNewButton'>
|
<a href='/new' className='floatingNewButton'>
|
||||||
Create your own <i className='fa fa-magic' />
|
Create your own <i className='fas fa-magic' />
|
||||||
</a>
|
</a>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/
|
||||||
require('./newPage.less');
|
require('./newPage.less');
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const createClass = require('create-react-class');
|
const createClass = require('create-react-class');
|
||||||
@@ -16,36 +17,70 @@ const SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
|
|||||||
const Editor = require('../../editor/editor.jsx');
|
const Editor = require('../../editor/editor.jsx');
|
||||||
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
||||||
|
|
||||||
|
const BREWKEY = 'homebrewery-new';
|
||||||
|
const STYLEKEY = 'homebrewery-new-style';
|
||||||
|
|
||||||
const KEY = 'homebrewery-new';
|
|
||||||
|
|
||||||
const NewPage = createClass({
|
const NewPage = createClass({
|
||||||
getInitialState : function() {
|
getDefaultProps : function() {
|
||||||
return {
|
return {
|
||||||
metadata : {
|
brew : {
|
||||||
gDrive : false,
|
text : '',
|
||||||
|
style : undefined,
|
||||||
|
shareId : null,
|
||||||
|
editId : null,
|
||||||
|
createdAt : null,
|
||||||
|
updatedAt : null,
|
||||||
|
gDrive : false,
|
||||||
|
|
||||||
title : '',
|
title : '',
|
||||||
description : '',
|
description : '',
|
||||||
tags : '',
|
tags : '',
|
||||||
published : false,
|
published : false,
|
||||||
authors : [],
|
authors : [],
|
||||||
systems : []
|
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,
|
isSaving : false,
|
||||||
saveGoogle : (global.account && global.account.googleId ? true : false),
|
saveGoogle : (global.account && global.account.googleId ? true : false),
|
||||||
errors : []
|
errors : [],
|
||||||
|
htmlErrors : Markdown.validate(this.props.brew.text)
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidMount : function() {
|
componentDidMount : function() {
|
||||||
const storage = localStorage.getItem(KEY);
|
const brewStorage = localStorage.getItem(BREWKEY);
|
||||||
if(storage){
|
const styleStorage = localStorage.getItem(STYLEKEY);
|
||||||
this.setState({
|
|
||||||
text : storage
|
const brew = this.state.brew;
|
||||||
});
|
|
||||||
|
if(!this.props.brew.text || !this.props.brew.style){
|
||||||
|
brew.text = this.props.brew.text || (brewStorage ?? '');
|
||||||
|
brew.style = this.props.brew.style || (styleStorage ?? undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.setState((prevState)=>({
|
||||||
|
brew : brew,
|
||||||
|
htmlErrors : Markdown.validate(prevState.brew.text)
|
||||||
|
}));
|
||||||
|
|
||||||
document.addEventListener('keydown', this.handleControlKeys);
|
document.addEventListener('keydown', this.handleControlKeys);
|
||||||
},
|
},
|
||||||
componentWillUnmount : function() {
|
componentWillUnmount : function() {
|
||||||
@@ -68,18 +103,30 @@ const NewPage = createClass({
|
|||||||
this.refs.editor.update();
|
this.refs.editor.update();
|
||||||
},
|
},
|
||||||
|
|
||||||
handleMetadataChange : function(metadata){
|
handleTextChange : function(text){
|
||||||
this.setState({
|
//If there are errors, run the validator on every change to give quick feedback
|
||||||
metadata : _.merge({}, this.state.metadata, metadata)
|
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);
|
||||||
},
|
},
|
||||||
|
|
||||||
handleTextChange : function(text){
|
handleStyleChange : function(style){
|
||||||
this.setState({
|
this.setState((prevState)=>({
|
||||||
text : text,
|
brew : _.merge({}, prevState.brew, { style: style }),
|
||||||
errors : Markdown.validate(text)
|
}));
|
||||||
});
|
localStorage.setItem(STYLEKEY, style);
|
||||||
localStorage.setItem(KEY, text);
|
},
|
||||||
|
|
||||||
|
handleMetaChange : function(metadata){
|
||||||
|
this.setState((prevState)=>({
|
||||||
|
brew : _.merge({}, prevState.brew, metadata),
|
||||||
|
}));
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
save : async function(){
|
save : async function(){
|
||||||
@@ -89,10 +136,18 @@ const NewPage = createClass({
|
|||||||
|
|
||||||
console.log('saving new brew');
|
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);
|
||||||
|
};
|
||||||
|
|
||||||
if(this.state.saveGoogle) {
|
if(this.state.saveGoogle) {
|
||||||
const res = await request
|
const res = await request
|
||||||
.post('/api/newGoogle/')
|
.post('/api/newGoogle/')
|
||||||
.send(_.merge({}, this.state.metadata, { text: this.state.text }))
|
.send(brew)
|
||||||
.catch((err)=>{
|
.catch((err)=>{
|
||||||
console.log(err.status === 401
|
console.log(err.status === 401
|
||||||
? 'Not signed in!'
|
? 'Not signed in!'
|
||||||
@@ -101,14 +156,13 @@ const NewPage = createClass({
|
|||||||
return;
|
return;
|
||||||
});
|
});
|
||||||
|
|
||||||
const brew = res.body;
|
brew = res.body;
|
||||||
localStorage.removeItem(KEY);
|
localStorage.removeItem(BREWKEY);
|
||||||
|
localStorage.removeItem(STYLEKEY);
|
||||||
window.location = `/edit/${brew.googleId}${brew.editId}`;
|
window.location = `/edit/${brew.googleId}${brew.editId}`;
|
||||||
} else {
|
} else {
|
||||||
request.post('/api')
|
request.post('/api')
|
||||||
.send(_.merge({}, this.state.metadata, {
|
.send(brew)
|
||||||
text : this.state.text
|
|
||||||
}))
|
|
||||||
.end((err, res)=>{
|
.end((err, res)=>{
|
||||||
if(err){
|
if(err){
|
||||||
this.setState({
|
this.setState({
|
||||||
@@ -117,33 +171,33 @@ const NewPage = createClass({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
window.onbeforeunload = function(){};
|
window.onbeforeunload = function(){};
|
||||||
const brew = res.body;
|
brew = res.body;
|
||||||
localStorage.removeItem(KEY);
|
localStorage.removeItem(BREWKEY);
|
||||||
|
localStorage.removeItem(STYLEKEY);
|
||||||
window.location = `/edit/${brew.editId}`;
|
window.location = `/edit/${brew.editId}`;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
renderSaveButton : function(){
|
renderSaveButton : function(){
|
||||||
if(this.state.isSaving){
|
if(this.state.isSaving){
|
||||||
return <Nav.item icon='fa-spinner fa-spin' className='saveButton'>
|
return <Nav.item icon='fas fa-spinner fa-spin' className='saveButton'>
|
||||||
save...
|
save...
|
||||||
</Nav.item>;
|
</Nav.item>;
|
||||||
} else {
|
} else {
|
||||||
return <Nav.item icon='fa-save' className='saveButton' onClick={this.save}>
|
return <Nav.item icon='fas fa-save' className='saveButton' onClick={this.save}>
|
||||||
save
|
save
|
||||||
</Nav.item>;
|
</Nav.item>;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
print : function(){
|
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');
|
window.open('/print?dialog=true&local=print', '_blank');
|
||||||
},
|
},
|
||||||
|
|
||||||
renderLocalPrintButton : function(){
|
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
|
get PDF
|
||||||
</Nav.item>;
|
</Nav.item>;
|
||||||
},
|
},
|
||||||
@@ -152,7 +206,7 @@ const NewPage = createClass({
|
|||||||
return <Navbar>
|
return <Navbar>
|
||||||
|
|
||||||
<Nav.section>
|
<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>
|
||||||
|
|
||||||
<Nav.section>
|
<Nav.section>
|
||||||
@@ -166,18 +220,19 @@ const NewPage = createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
render : function(){
|
render : function(){
|
||||||
return <div className='newPage page'>
|
return <div className='newPage sitePage'>
|
||||||
{this.renderNavbar()}
|
{this.renderNavbar()}
|
||||||
<div className='content'>
|
<div className='content'>
|
||||||
<SplitPane onDragFinish={this.handleSplitMove} ref='pane'>
|
<SplitPane onDragFinish={this.handleSplitMove} ref='pane'>
|
||||||
<Editor
|
<Editor
|
||||||
ref='editor'
|
ref='editor'
|
||||||
value={this.state.text}
|
brew={this.state.brew}
|
||||||
onChange={this.handleTextChange}
|
onTextChange={this.handleTextChange}
|
||||||
metadata={this.state.metadata}
|
onStyleChange={this.handleStyleChange}
|
||||||
onMetadataChange={this.handleMetadataChange}
|
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>
|
</SplitPane>
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ const createClass = require('create-react-class');
|
|||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const cx = require('classnames');
|
const cx = require('classnames');
|
||||||
const { Meta } = require('vitreum/headtags');
|
const { Meta } = require('vitreum/headtags');
|
||||||
|
const MarkdownLegacy = require('naturalcrit/markdownLegacy.js');
|
||||||
const Markdown = require('naturalcrit/markdown.js');
|
const Markdown = require('naturalcrit/markdown.js');
|
||||||
|
|
||||||
const PrintPage = createClass({
|
const PrintPage = createClass({
|
||||||
@@ -11,7 +12,9 @@ const PrintPage = createClass({
|
|||||||
return {
|
return {
|
||||||
query : {},
|
query : {},
|
||||||
brew : {
|
brew : {
|
||||||
text : '',
|
text : '',
|
||||||
|
style : '',
|
||||||
|
renderer : 'legacy'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -33,18 +36,32 @@ const PrintPage = createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
renderPages : function(){
|
renderPages : function(){
|
||||||
return _.map(this.state.brewText.split('\\page'), (page, index)=>{
|
if(this.props.brew.renderer == 'legacy') {
|
||||||
return <div
|
return _.map(this.state.brewText.split('\\page'), (page, index)=>{
|
||||||
className='phb'
|
return <div
|
||||||
id={`p${index + 1}`}
|
className='phb page'
|
||||||
dangerouslySetInnerHTML={{ __html: Markdown.render(page) }}
|
id={`p${index + 1}`}
|
||||||
key={index} />;
|
dangerouslySetInnerHTML={{ __html: MarkdownLegacy.render(page) }}
|
||||||
});
|
key={index} />;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return _.map(this.state.brewText.split(/^\\page/gm), (page, index)=>{
|
||||||
|
return <div
|
||||||
|
className='phb3 page'
|
||||||
|
id={`p${index + 1}`}
|
||||||
|
dangerouslySetInnerHTML={{ __html: Markdown.render(page) }}
|
||||||
|
key={index} />;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
render : function(){
|
render : function(){
|
||||||
return <div>
|
return <div>
|
||||||
<Meta name='robots' content='noindex, nofollow' />
|
<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()}
|
{this.renderPages()}
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,14 +19,22 @@ const SharePage = createClass({
|
|||||||
brew : {
|
brew : {
|
||||||
title : '',
|
title : '',
|
||||||
text : '',
|
text : '',
|
||||||
|
style : '',
|
||||||
shareId : null,
|
shareId : null,
|
||||||
createdAt : null,
|
createdAt : null,
|
||||||
updatedAt : null,
|
updatedAt : null,
|
||||||
views : 0
|
views : 0,
|
||||||
|
renderer : ''
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getInitialState : function() {
|
||||||
|
return {
|
||||||
|
showDropdown : false
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
componentDidMount : function() {
|
componentDidMount : function() {
|
||||||
document.addEventListener('keydown', this.handleControlKeys);
|
document.addEventListener('keydown', this.handleControlKeys);
|
||||||
},
|
},
|
||||||
@@ -49,8 +57,30 @@ const SharePage = createClass({
|
|||||||
this.props.brew.shareId;
|
this.props.brew.shareId;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
handleDropdown : function(show){
|
||||||
|
this.setState({
|
||||||
|
showDropdown : show
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
renderDropdown : function(){
|
||||||
|
if(!this.state.showDropdown) return null;
|
||||||
|
|
||||||
|
return <div className='dropdown'>
|
||||||
|
<a href={`/source/${this.processShareId()}`} className='item'>
|
||||||
|
view
|
||||||
|
</a>
|
||||||
|
<a href={`/download/${this.processShareId()}`} className='item'>
|
||||||
|
download
|
||||||
|
</a>
|
||||||
|
<a href={`/new/${this.processShareId()}`} className='item'>
|
||||||
|
clone to new
|
||||||
|
</a>
|
||||||
|
</div>;
|
||||||
|
},
|
||||||
|
|
||||||
render : function(){
|
render : function(){
|
||||||
return <div className='sharePage page'>
|
return <div className='sharePage sitePage'>
|
||||||
<Meta name='robots' content='noindex, nofollow' />
|
<Meta name='robots' content='noindex, nofollow' />
|
||||||
<Navbar>
|
<Navbar>
|
||||||
<Nav.section>
|
<Nav.section>
|
||||||
@@ -58,17 +88,22 @@ const SharePage = createClass({
|
|||||||
</Nav.section>
|
</Nav.section>
|
||||||
|
|
||||||
<Nav.section>
|
<Nav.section>
|
||||||
<PrintLink shareId={this.processShareId()} />
|
{this.props.brew.shareId && <>
|
||||||
<Nav.item href={`/source/${this.processShareId()}`} color='teal' icon='fa-code'>
|
<PrintLink shareId={this.processShareId()} />
|
||||||
source
|
<Nav.item icon='fas fa-code' color='red' className='source'
|
||||||
</Nav.item>
|
onMouseEnter={()=>this.handleDropdown(true)}
|
||||||
|
onMouseLeave={()=>this.handleDropdown(false)}>
|
||||||
|
source
|
||||||
|
{this.renderDropdown()}
|
||||||
|
</Nav.item>
|
||||||
|
</>}
|
||||||
<RecentNavItem brew={this.props.brew} storageKey='view' />
|
<RecentNavItem brew={this.props.brew} storageKey='view' />
|
||||||
<Account />
|
<Account />
|
||||||
</Nav.section>
|
</Nav.section>
|
||||||
</Navbar>
|
</Navbar>
|
||||||
|
|
||||||
<div className='content'>
|
<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>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,4 +2,49 @@
|
|||||||
.content{
|
.content{
|
||||||
overflow-y : hidden;
|
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;
|
if(!this.props.brew.editId) return;
|
||||||
|
|
||||||
return <a onClick={this.deleteBrew}>
|
return <a onClick={this.deleteBrew}>
|
||||||
<i className='fa fa-trash' />
|
<i className='fas fa-trash-alt' />
|
||||||
</a>;
|
</a>;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -61,7 +61,7 @@ const BrewItem = createClass({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return <a href={`/edit/${editLink}`} target='_blank' rel='noopener noreferrer'>
|
return <a href={`/edit/${editLink}`} target='_blank' rel='noopener noreferrer'>
|
||||||
<i className='fa fa-pencil' />
|
<i className='fas fa-pencil-alt' />
|
||||||
</a>;
|
</a>;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -74,7 +74,20 @@ const BrewItem = createClass({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return <a href={`/share/${shareLink}`} target='_blank' rel='noopener noreferrer'>
|
return <a href={`/share/${shareLink}`} target='_blank' rel='noopener noreferrer'>
|
||||||
<i className='fa fa-share-alt' />
|
<i className='fas fa-share-alt' />
|
||||||
|
</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' />
|
||||||
</a>;
|
</a>;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -95,13 +108,13 @@ const BrewItem = createClass({
|
|||||||
|
|
||||||
<div className='info'>
|
<div className='info'>
|
||||||
<span>
|
<span>
|
||||||
<i className='fa fa-user' /> {brew.authors.join(', ')}
|
<i className='fas fa-user' /> {brew.authors.join(', ')}
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
<i className='fa fa-eye' /> {brew.views}
|
<i className='fas fa-eye' /> {brew.views}
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
<i className='fa fa-refresh' /> {moment(brew.updatedAt).fromNow()}
|
<i className='fas fa-sync-alt' /> {moment(brew.updatedAt).fromNow()}
|
||||||
</span>
|
</span>
|
||||||
{this.renderGoogleDriveIcon()}
|
{this.renderGoogleDriveIcon()}
|
||||||
</div>
|
</div>
|
||||||
@@ -109,6 +122,7 @@ const BrewItem = createClass({
|
|||||||
<div className='links'>
|
<div className='links'>
|
||||||
{this.renderShareLink()}
|
{this.renderShareLink()}
|
||||||
{this.renderEditLink()}
|
{this.renderEditLink()}
|
||||||
|
{this.renderDownloadLink()}
|
||||||
{this.renderDeleteBrewLink()}
|
{this.renderDeleteBrewLink()}
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
box-sizing : border-box;
|
box-sizing : border-box;
|
||||||
overflow : hidden;
|
overflow : hidden;
|
||||||
width : 48%;
|
width : 48%;
|
||||||
min-height : 80px;
|
min-height : 105px;
|
||||||
margin-right : 15px;
|
margin-right : 15px;
|
||||||
margin-bottom : 15px;
|
margin-bottom : 15px;
|
||||||
padding : 5px 15px 5px 8px;
|
padding : 5px 15px 5px 8px;
|
||||||
@@ -22,6 +22,9 @@
|
|||||||
font-size : 2.2em;
|
font-size : 2.2em;
|
||||||
}
|
}
|
||||||
.info{
|
.info{
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0px;
|
||||||
|
margin-bottom: 4px;
|
||||||
font-family : ScalySans;
|
font-family : ScalySans;
|
||||||
font-size : 1.2em;
|
font-size : 1.2em;
|
||||||
&>span{
|
&>span{
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ const Navbar = require('../../navbar/navbar.jsx');
|
|||||||
|
|
||||||
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
||||||
const Account = require('../../navbar/account.navitem.jsx');
|
const Account = require('../../navbar/account.navitem.jsx');
|
||||||
|
const NewBrew = require('../../navbar/newbrew.navitem.jsx');
|
||||||
const BrewItem = require('./brewItem/brewItem.jsx');
|
const BrewItem = require('./brewItem/brewItem.jsx');
|
||||||
|
|
||||||
// const brew = {
|
// const brew = {
|
||||||
@@ -23,7 +24,13 @@ const UserPage = createClass({
|
|||||||
getDefaultProps : function() {
|
getDefaultProps : function() {
|
||||||
return {
|
return {
|
||||||
username : '',
|
username : '',
|
||||||
brews : []
|
brews : [],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
getInitialState : function() {
|
||||||
|
return {
|
||||||
|
sortType : 'alpha',
|
||||||
|
sortDir : 'asc'
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
getUsernameWithS : function() {
|
getUsernameWithS : function() {
|
||||||
@@ -35,13 +42,83 @@ const UserPage = createClass({
|
|||||||
renderBrews : function(brews){
|
renderBrews : function(brews){
|
||||||
if(!brews || !brews.length) return <div className='noBrews'>No Brews.</div>;
|
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 _.map(sortedBrews, (brew, idx)=>{
|
||||||
return <BrewItem brew={brew} key={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' : brew.createdAt,
|
||||||
|
'updated' : brew.updatedAt,
|
||||||
|
'views' : brew.views,
|
||||||
|
'latest' : brew.lastViewed
|
||||||
|
};
|
||||||
|
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>;
|
||||||
|
},
|
||||||
|
|
||||||
|
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>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>;
|
||||||
|
},
|
||||||
|
|
||||||
getSortedBrews : function(){
|
getSortedBrews : function(){
|
||||||
return _.groupBy(this.props.brews, (brew)=>{
|
return _.groupBy(this.props.brews, (brew)=>{
|
||||||
return (brew.published ? 'published' : 'private');
|
return (brew.published ? 'published' : 'private');
|
||||||
@@ -51,16 +128,19 @@ const UserPage = createClass({
|
|||||||
render : function(){
|
render : function(){
|
||||||
const brews = this.getSortedBrews();
|
const brews = this.getSortedBrews();
|
||||||
|
|
||||||
return <div className='userPage page'>
|
return <div className='userPage sitePage'>
|
||||||
|
<link href='/themes/5ePhbLegacy.style.css' rel='stylesheet'/>
|
||||||
<Navbar>
|
<Navbar>
|
||||||
<Nav.section>
|
<Nav.section>
|
||||||
|
<NewBrew />
|
||||||
<RecentNavItem />
|
<RecentNavItem />
|
||||||
<Account />
|
<Account />
|
||||||
</Nav.section>
|
</Nav.section>
|
||||||
</Navbar>
|
</Navbar>
|
||||||
|
|
||||||
<div className='content'>
|
<div className='content V3'>
|
||||||
<div className='phb'>
|
<div className='phb'>
|
||||||
|
{this.renderSortOptions()}
|
||||||
<div>
|
<div>
|
||||||
<h1>{this.getUsernameWithS()} brews</h1>
|
<h1>{this.getUsernameWithS()} brews</h1>
|
||||||
{this.renderBrews(brews.published)}
|
{this.renderBrews(brews.published)}
|
||||||
|
|||||||
@@ -30,4 +30,44 @@
|
|||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.sort-container{
|
||||||
|
font-family : 'Open Sans', sans-serif;
|
||||||
|
position : fixed;
|
||||||
|
top : 35px;
|
||||||
|
border : 2px solid #58180D;
|
||||||
|
width : 675px;
|
||||||
|
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;
|
||||||
|
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>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<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="//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" />
|
<link rel="icon" href="/assets/homebrew/favicon.ico" type="image/x-icon" />
|
||||||
<title>${title.length ? `${title} - The Homebrewery`: 'The Homebrewery - NaturalCrit'}</title>
|
<title>${title.length ? `${title} - The Homebrewery`: 'The Homebrewery - NaturalCrit'}</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<main id="reactRoot">${require(`../build/${name}/ssr.js`)(props)}</main>
|
<main id="reactRoot">${require(`../build/${name}/ssr.js`)(props)}</main>
|
||||||
|
<script src=${`/${name}/bundle.js`}></script>
|
||||||
|
<script>start_app(${JSON.stringify(props)})</script>
|
||||||
</body>
|
</body>
|
||||||
<script src=${`/${name}/bundle.js`}></script>
|
|
||||||
<script>start_app(${JSON.stringify(props)})</script>
|
|
||||||
</html>
|
</html>
|
||||||
`;
|
`;
|
||||||
};
|
};
|
||||||
12856
package-lock.json
generated
47
package.json
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"name": "homebrewery",
|
"name": "homebrewery",
|
||||||
"description": "Create authentic looking D&D homebrews using only markdown",
|
"description": "Create authentic looking D&D homebrews using only markdown",
|
||||||
"version": "2.10.7",
|
"version": "2.13.2",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "12.16.x"
|
"node": "14.15.x"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -35,43 +35,48 @@
|
|||||||
},
|
},
|
||||||
"babel": {
|
"babel": {
|
||||||
"presets": [
|
"presets": [
|
||||||
"env",
|
"@babel/preset-env",
|
||||||
"react"
|
"@babel/preset-react"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/core": "^7.12.10",
|
"@babel/core": "^7.14.8",
|
||||||
"@babel/preset-env": "^7.12.11",
|
"@babel/plugin-transform-runtime": "^7.14.5",
|
||||||
"@babel/preset-react": "^7.12.10",
|
"@babel/preset-env": "^7.14.8",
|
||||||
|
"@babel/preset-react": "^7.14.5",
|
||||||
"body-parser": "^1.19.0",
|
"body-parser": "^1.19.0",
|
||||||
"classnames": "^2.2.6",
|
"classnames": "^2.3.1",
|
||||||
"codemirror": "^5.59.2",
|
"codemirror": "^5.62.2",
|
||||||
"cookie-parser": "^1.4.5",
|
"cookie-parser": "^1.4.5",
|
||||||
"create-react-class": "^15.7.0",
|
"create-react-class": "^15.7.0",
|
||||||
|
"dedent-tabs": "^0.9.0",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
|
"express-async-handler": "^1.1.4",
|
||||||
"express-static-gzip": "2.1.1",
|
"express-static-gzip": "2.1.1",
|
||||||
"fs-extra": "9.1.0",
|
"fs-extra": "10.0.0",
|
||||||
"googleapis": "67.0.0",
|
"googleapis": "82.0.0",
|
||||||
"jwt-simple": "^0.5.6",
|
"jwt-simple": "^0.5.6",
|
||||||
"less": "^3.13.1",
|
"less": "^3.13.1",
|
||||||
"lodash": "^4.17.20",
|
"lodash": "^4.17.21",
|
||||||
"marked": "^0.3.19",
|
"marked": "2.1.3",
|
||||||
|
"markedLegacy": "npm:marked@^0.3.19",
|
||||||
"moment": "^2.29.1",
|
"moment": "^2.29.1",
|
||||||
"mongoose": "^5.11.13",
|
"mongoose": "^5.13.4",
|
||||||
"nanoid": "3.1.20",
|
"nanoid": "3.1.23",
|
||||||
"nconf": "^0.11.1",
|
"nconf": "^0.11.3",
|
||||||
"prop-types": "15.7.2",
|
"prop-types": "15.7.2",
|
||||||
"query-string": "6.13.8",
|
"query-string": "7.0.1",
|
||||||
"react": "^16.14.0",
|
"react": "^16.14.0",
|
||||||
"react-dom": "^16.14.0",
|
"react-dom": "^16.14.0",
|
||||||
"react-frame-component": "4.1.3",
|
"react-frame-component": "4.1.3",
|
||||||
"react-router-dom": "5.2.0",
|
"react-router-dom": "5.2.0",
|
||||||
|
"sanitize-filename": "1.6.3",
|
||||||
"superagent": "^6.1.0",
|
"superagent": "^6.1.0",
|
||||||
"vitreum": "github:calculuschild/vitreum#21a8e1c9421f1d3a3b474c12f480feb2fbd28c5b"
|
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"eslint": "^7.18.0",
|
"eslint": "^7.31.0",
|
||||||
"eslint-plugin-react": "^7.22.0",
|
"eslint-plugin-react": "^7.24.0",
|
||||||
"pico-check": "^2.0.3"
|
"pico-check": "^2.1.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,2 +1,4 @@
|
|||||||
# Notes
|
# Notes
|
||||||
User-agent: *
|
User-agent: *
|
||||||
|
Disallow: /edit/
|
||||||
|
|
||||||
|
|||||||
@@ -7,9 +7,14 @@ const isDev = !!process.argv.find((arg)=>arg=='--dev');
|
|||||||
|
|
||||||
const lessTransform = require('vitreum/transforms/less.js');
|
const lessTransform = require('vitreum/transforms/less.js');
|
||||||
const assetTransform = require('vitreum/transforms/asset.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 = {
|
const transforms = {
|
||||||
|
'.js' : (code, filename, opts)=>babelify(code),
|
||||||
|
'.jsx' : (code, filename, opts)=>babelify(code),
|
||||||
'.less' : lessTransform,
|
'.less' : lessTransform,
|
||||||
'*' : assetTransform('./build')
|
'*' : 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.css', css);
|
||||||
await fs.outputFile('./build/homebrew/bundle.js', bundle);
|
await fs.outputFile('./build/homebrew/bundle.js', bundle);
|
||||||
await fs.outputFile('./build/homebrew/ssr.js', ssr);
|
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
|
//compress files in production
|
||||||
await fs.outputFile('./build/homebrew/bundle.css.br', zlib.brotliCompressSync(css));
|
if(!isDev){
|
||||||
await fs.outputFile('./build/homebrew/bundle.js.br', zlib.brotliCompressSync(bundle));
|
await fs.outputFile('./build/homebrew/bundle.css.br', zlib.brotliCompressSync(css));
|
||||||
await fs.outputFile('./build/homebrew/ssr.js.br', zlib.brotliCompressSync(ssr));
|
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', {
|
pack('./client/homebrew/homebrew.jsx', {
|
||||||
paths : ['./shared'],
|
paths : ['./shared'],
|
||||||
libs : Proj.libs,
|
libs : Proj.libs,
|
||||||
@@ -42,6 +73,6 @@ pack('./client/homebrew/homebrew.jsx', {
|
|||||||
if(isDev){
|
if(isDev){
|
||||||
livereload('./build');
|
livereload('./build');
|
||||||
watchFile('./server.js', {
|
watchFile('./server.js', {
|
||||||
watch : ['./homebrew'] // Watch additional folders if you want
|
watch : ['./client'] // Watch additional folders if you want
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
"classnames",
|
"classnames",
|
||||||
"codemirror",
|
"codemirror",
|
||||||
"codemirror/mode/gfm/gfm.js",
|
"codemirror/mode/gfm/gfm.js",
|
||||||
|
"codemirror/mode/css/css.js",
|
||||||
"codemirror/mode/javascript/javascript.js",
|
"codemirror/mode/javascript/javascript.js",
|
||||||
"moment",
|
"moment",
|
||||||
"superagent",
|
"superagent",
|
||||||
|
|||||||
284
server.js
@@ -1,18 +1,55 @@
|
|||||||
|
/*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const jwt = require('jwt-simple');
|
const jwt = require('jwt-simple');
|
||||||
const expressStaticGzip = require('express-static-gzip');
|
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
const homebrewApi = require('./server/homebrew.api.js');
|
const homebrewApi = require('./server/homebrew.api.js');
|
||||||
const GoogleActions = require('./server/googleActions.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
|
const brewAccessTypes = ['edit', 'share', 'raw'];
|
||||||
app.use('/', expressStaticGzip(`${__dirname}/build`, {
|
|
||||||
enableBrotli : true,
|
//Get the brew object from the HB database or Google Drive
|
||||||
orderPreference : ['br'],
|
const getBrewFromId = asyncHandler(async (id, accessType)=>{
|
||||||
index : false
|
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;
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
return brew;
|
||||||
|
});
|
||||||
|
|
||||||
|
const sanitizeBrew = (brew, full=false)=>{
|
||||||
|
delete brew._id;
|
||||||
|
delete brew.__v;
|
||||||
|
if(full){
|
||||||
|
delete brew.editId;
|
||||||
|
}
|
||||||
|
return brew;
|
||||||
|
};
|
||||||
|
|
||||||
|
app.use('/', serveCompressedStaticAssets(`${__dirname}/build`));
|
||||||
|
|
||||||
process.chdir(__dirname);
|
process.chdir(__dirname);
|
||||||
|
|
||||||
@@ -30,7 +67,7 @@ const config = require('nconf')
|
|||||||
//DB
|
//DB
|
||||||
const mongoose = require('mongoose');
|
const mongoose = require('mongoose');
|
||||||
mongoose.connect(config.get('mongodb_uri') || config.get('mongolab_uri') || 'mongodb://localhost/naturalcrit',
|
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', ()=>{
|
mongoose.connection.on('error', ()=>{
|
||||||
console.log('Error : Could not connect to a Mongo Database.');
|
console.log('Error : Could not connect to a Mongo Database.');
|
||||||
console.log(' If you are running locally, make sure mongodb.exe is running.');
|
console.log(' If you are running locally, make sure mongodb.exe is running.');
|
||||||
@@ -68,160 +105,130 @@ app.get('/robots.txt', (req, res)=>{
|
|||||||
return res.sendFile(`${__dirname}/robots.txt`);
|
return res.sendFile(`${__dirname}/robots.txt`);
|
||||||
});
|
});
|
||||||
|
|
||||||
//Source page
|
//Home page
|
||||||
app.get('/source/:id', (req, res)=>{
|
app.get('/', async (req, res, next)=>{
|
||||||
if(req.params.id.length > 12) {
|
const brew = {
|
||||||
const googleId = req.params.id.slice(0, -12);
|
text : welcomeText
|
||||||
const shareId = req.params.id.slice(-12);
|
};
|
||||||
GoogleActions.readFileMetadata(config.get('google_api_key'), googleId, shareId, 'share')
|
req.brew = brew;
|
||||||
.then((brew)=>{
|
return next();
|
||||||
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');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//Changelog page
|
||||||
|
app.get('/changelog', async (req, res, next)=>{
|
||||||
|
const brew = {
|
||||||
|
title : 'Changelog',
|
||||||
|
text : changelogText
|
||||||
|
};
|
||||||
|
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
|
//User Page
|
||||||
app.get('/user/:username', async (req, res, next)=>{
|
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 = [];
|
let brews = await HomebrewModel.getByUser(req.params.username, ownAccount)
|
||||||
|
|
||||||
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)
|
|
||||||
.catch((err)=>{
|
.catch((err)=>{
|
||||||
console.log(err);
|
console.log(err);
|
||||||
});
|
});
|
||||||
|
|
||||||
if(googleBrews) {
|
if(ownAccount && req?.account?.googleId){
|
||||||
req.brews = _.concat(brews, googleBrews);
|
const googleBrews = await GoogleActions.listGoogleBrews(req, res)
|
||||||
} else {req.brews = brews;}
|
.catch((err)=>{
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
if(googleBrews)
|
||||||
|
brews = _.concat(brews, googleBrews);
|
||||||
|
}
|
||||||
|
|
||||||
|
req.brews = _.map(brews, (brew)=>{
|
||||||
|
return sanitizeBrew(brew, !ownAccount);
|
||||||
|
});
|
||||||
|
|
||||||
return next();
|
return next();
|
||||||
});
|
});
|
||||||
|
|
||||||
//Edit Page
|
//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.
|
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 brew = await getBrewFromId(req.params.id, 'edit');
|
||||||
const googleId = req.params.id.slice(0, -12);
|
req.brew = brew;
|
||||||
const editId = req.params.id.slice(-12);
|
return next();
|
||||||
GoogleActions.readFileMetadata(config.get('google_api_key'), googleId, editId, 'edit')
|
}));
|
||||||
.then((brew)=>{
|
|
||||||
req.brew = brew; //TODO Need to sanitize later
|
//New Page
|
||||||
return next();
|
app.get('/new/:id', asyncHandler(async (req, res, next)=>{
|
||||||
})
|
const brew = await getBrewFromId(req.params.id, 'share');
|
||||||
.catch((err)=>{
|
brew.title = `CLONE - ${brew.title}`;
|
||||||
console.log(err);
|
req.brew = brew;
|
||||||
return res.status(400).send('Can\'t get brew from Google');
|
return next();
|
||||||
});
|
}));
|
||||||
} 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`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
//Share Page
|
//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) {
|
if(req.params.id.length > 12) {
|
||||||
const googleId = req.params.id.slice(0, -12);
|
const googleId = req.params.id.slice(0, -12);
|
||||||
const shareId = req.params.id.slice(-12);
|
const shareId = req.params.id.slice(-12);
|
||||||
GoogleActions.readFileMetadata(config.get('google_api_key'), googleId, shareId, 'share')
|
await GoogleActions.increaseView(googleId, shareId, 'share', brew)
|
||||||
.then((brew)=>{
|
.catch((err)=>{next(err);});
|
||||||
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');
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
HomebrewModel.get({ shareId: req.params.id })
|
await HomebrewModel.increaseView({ shareId: brew.shareId });
|
||||||
.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`);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
req.brew = brew;
|
||||||
|
return next();
|
||||||
|
}));
|
||||||
|
|
||||||
//Print Page
|
//Print Page
|
||||||
app.get('/print/:id', (req, res, next)=>{
|
app.get('/print/:id', asyncHandler(async (req, res, next)=>{
|
||||||
if(req.params.id.length > 12) {
|
const brew = await getBrewFromId(req.params.id, 'share');
|
||||||
const googleId = req.params.id.slice(0, -12);
|
req.brew = brew;
|
||||||
const shareId = req.params.id.slice(-12);
|
return next();
|
||||||
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`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
//Render the page
|
//Render the page
|
||||||
//const render = require('.build/render');
|
|
||||||
const templateFn = require('./client/template.js');
|
const templateFn = require('./client/template.js');
|
||||||
app.use((req, res)=>{
|
app.use((req, res)=>{
|
||||||
const props = {
|
const props = {
|
||||||
version : require('./package.json').version,
|
version : require('./package.json').version,
|
||||||
url : req.originalUrl,
|
url : req.originalUrl,
|
||||||
welcomeText : welcomeText,
|
|
||||||
changelog : changelogText,
|
|
||||||
brew : req.brew,
|
brew : req.brew,
|
||||||
brews : req.brews,
|
brews : req.brews,
|
||||||
googleBrews : req.googleBrews,
|
googleBrews : req.googleBrews,
|
||||||
account : req.account,
|
account : req.account,
|
||||||
|
enable_v3 : config.get('enable_v3')
|
||||||
};
|
};
|
||||||
templateFn('homebrew', title = req.brew ? req.brew.title : '', props)
|
templateFn('homebrew', title = req.brew ? req.brew.title : '', props)
|
||||||
.then((page)=>{ res.send(page); })
|
.then((page)=>{ res.send(page); })
|
||||||
@@ -231,6 +238,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;
|
const PORT = process.env.PORT || config.get('web_port') || 8000;
|
||||||
app.listen(PORT);
|
app.listen(PORT);
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ GoogleActions = {
|
|||||||
})
|
})
|
||||||
.catch((err)=>{
|
.catch((err)=>{
|
||||||
return console.error(`Error Listing Google Brews: ${err}`);
|
return console.error(`Error Listing Google Brews: ${err}`);
|
||||||
|
//TODO: Should break out here, but continues on for some reason.
|
||||||
});
|
});
|
||||||
|
|
||||||
if(!obj.data.files.length) {
|
if(!obj.data.files.length) {
|
||||||
@@ -152,16 +153,17 @@ GoogleActions = {
|
|||||||
fileId : brew.googleId,
|
fileId : brew.googleId,
|
||||||
resource : { name : `${brew.title}.txt`,
|
resource : { name : `${brew.title}.txt`,
|
||||||
description : `${brew.description}`,
|
description : `${brew.description}`,
|
||||||
properties : { title : brew.title,
|
properties : { title : brew.title,
|
||||||
published : brew.published,
|
published : brew.published,
|
||||||
lastViewed : brew.lastViewed,
|
lastViewed : brew.lastViewed,
|
||||||
views : brew.views,
|
views : brew.views,
|
||||||
version : brew.version,
|
version : brew.version,
|
||||||
tags : brew.tags,
|
renderer : brew.renderer,
|
||||||
systems : brew.systems.join() }
|
tags : brew.tags,
|
||||||
|
systems : brew.systems.join() }
|
||||||
},
|
},
|
||||||
media : { mimeType : 'text/plain',
|
media : { mimeType : 'text/plain',
|
||||||
body : brew.text }
|
body : brew.text }
|
||||||
})
|
})
|
||||||
.catch((err)=>{
|
.catch((err)=>{
|
||||||
console.log('Error saving to google');
|
console.log('Error saving to google');
|
||||||
@@ -230,6 +232,7 @@ GoogleActions = {
|
|||||||
description : brew.description,
|
description : brew.description,
|
||||||
tags : '',
|
tags : '',
|
||||||
published : brew.published,
|
published : brew.published,
|
||||||
|
renderer : brew.renderer,
|
||||||
authors : [],
|
authors : [],
|
||||||
systems : []
|
systems : []
|
||||||
};
|
};
|
||||||
@@ -238,15 +241,16 @@ GoogleActions = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
readFileMetadata : async (auth, id, accessId, accessType)=>{
|
readFileMetadata : async (auth, id, accessId, accessType)=>{
|
||||||
|
|
||||||
const drive = google.drive({ version: 'v3', auth: auth });
|
const drive = google.drive({ version: 'v3', auth: auth });
|
||||||
|
|
||||||
const obj = await drive.files.get({
|
const obj = await drive.files.get({
|
||||||
fileId : id,
|
fileId : id,
|
||||||
fields : 'properties, createdTime, modifiedTime, description'
|
fields : 'properties, createdTime, modifiedTime, description, trashed'
|
||||||
})
|
})
|
||||||
.catch((err)=>{
|
.catch((err)=>{
|
||||||
console.log('Error loading from Google');
|
console.log('Error loading from Google');
|
||||||
console.error(err);
|
throw (err);
|
||||||
return;
|
return;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -257,8 +261,12 @@ GoogleActions = {
|
|||||||
throw ('Share ID does not match');
|
throw ('Share ID does not match');
|
||||||
}
|
}
|
||||||
|
|
||||||
//Access actual file with service account. Just api key is causing "automated query" errors.
|
//Access file using service account. Using API key only causes "automated query" lockouts after a while.
|
||||||
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 serviceAuth = google.auth.fromJSON(keys);
|
const serviceAuth = google.auth.fromJSON(keys);
|
||||||
serviceAuth.scopes = ['https://www.googleapis.com/auth/drive'];
|
serviceAuth.scopes = ['https://www.googleapis.com/auth/drive'];
|
||||||
|
|
||||||
@@ -285,12 +293,14 @@ GoogleActions = {
|
|||||||
systems : obj.data.properties.systems ? obj.data.properties.systems.split(',') : [],
|
systems : obj.data.properties.systems ? obj.data.properties.systems.split(',') : [],
|
||||||
authors : [],
|
authors : [],
|
||||||
published : obj.data.properties.published ? obj.data.properties.published == 'true' : false,
|
published : obj.data.properties.published ? obj.data.properties.published == 'true' : false,
|
||||||
|
trashed : obj.data.trashed,
|
||||||
|
|
||||||
createdAt : obj.data.createdTime,
|
createdAt : obj.data.createdTime,
|
||||||
updatedAt : obj.data.modifiedTime,
|
updatedAt : obj.data.modifiedTime,
|
||||||
lastViewed : obj.data.properties.lastViewed,
|
lastViewed : obj.data.properties.lastViewed,
|
||||||
views : parseInt(obj.data.properties.views) || 0, //brews with no view parameter will return undefined
|
views : parseInt(obj.data.properties.views) || 0, //brews with no view parameter will return undefined
|
||||||
version : parseInt(obj.data.properties.version) || 0,
|
version : parseInt(obj.data.properties.version) || 0,
|
||||||
|
renderer : obj.data.properties.renderer ? obj.data.properties.renderer : 'legacy',
|
||||||
|
|
||||||
gDrive : true,
|
gDrive : true,
|
||||||
googleId : id
|
googleId : id
|
||||||
@@ -337,7 +347,10 @@ GoogleActions = {
|
|||||||
increaseView : async (id, accessId, accessType, brew)=>{
|
increaseView : async (id, accessId, accessType, brew)=>{
|
||||||
//service account because this is modifying another user's file properties
|
//service account because this is modifying another user's file properties
|
||||||
//so we need extended scope
|
//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);
|
const auth = google.auth.fromJSON(keys);
|
||||||
auth.scopes = ['https://www.googleapis.com/auth/drive'];
|
auth.scopes = ['https://www.googleapis.com/auth/drive'];
|
||||||
|
|
||||||
|
|||||||
@@ -11,19 +11,34 @@ const Markdown = require('../shared/naturalcrit/markdown.js');
|
|||||||
// });
|
// });
|
||||||
// };
|
// };
|
||||||
|
|
||||||
|
const MAX_TITLE_LENGTH = 100;
|
||||||
|
|
||||||
const getGoodBrewTitle = (text)=>{
|
const getGoodBrewTitle = (text)=>{
|
||||||
const tokens = Markdown.marked.lexer(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 mergeBrewText = (text, style)=>{
|
||||||
|
if(typeof style !== 'undefined') {
|
||||||
|
text = `\`\`\`css\n` +
|
||||||
|
`${style}\n` +
|
||||||
|
`\`\`\`\n\n` +
|
||||||
|
`${text}`;
|
||||||
|
}
|
||||||
|
return text;
|
||||||
};
|
};
|
||||||
|
|
||||||
const newBrew = (req, res)=>{
|
const newBrew = (req, res)=>{
|
||||||
const brew = req.body;
|
const brew = req.body;
|
||||||
brew.authors = (req.account) ? [req.account.username] : [];
|
|
||||||
|
|
||||||
if(!brew.title) {
|
if(!brew.title) {
|
||||||
brew.title = getGoodBrewTitle(brew.text);
|
brew.title = getGoodBrewTitle(brew.text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
brew.authors = (req.account) ? [req.account.username] : [];
|
||||||
|
brew.text = mergeBrewText(brew.text, brew.style);
|
||||||
|
|
||||||
delete brew.editId;
|
delete brew.editId;
|
||||||
delete brew.shareId;
|
delete brew.shareId;
|
||||||
delete brew.googleId;
|
delete brew.googleId;
|
||||||
@@ -50,8 +65,10 @@ const updateBrew = (req, res)=>{
|
|||||||
HomebrewModel.get({ editId: req.params.id })
|
HomebrewModel.get({ editId: req.params.id })
|
||||||
.then((brew)=>{
|
.then((brew)=>{
|
||||||
brew = _.merge(brew, req.body);
|
brew = _.merge(brew, req.body);
|
||||||
|
brew.text = mergeBrewText(brew.text, brew.style);
|
||||||
|
|
||||||
// Compress brew text to binary before saving
|
// 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
|
// Delete the non-binary text field since it's not needed anymore
|
||||||
brew.text = undefined;
|
brew.text = undefined;
|
||||||
brew.updatedAt = new Date();
|
brew.updatedAt = new Date();
|
||||||
@@ -110,20 +127,20 @@ const newGoogleBrew = async (req, res, next)=>{
|
|||||||
try { oAuth2Client = GoogleActions.authCheck(req.account, res); } catch (err) { return res.status(err.status).send(err.message); }
|
try { oAuth2Client = GoogleActions.authCheck(req.account, res); } catch (err) { return res.status(err.status).send(err.message); }
|
||||||
|
|
||||||
const brew = req.body;
|
const brew = req.body;
|
||||||
brew.authors = (req.account) ? [req.account.username] : [];
|
|
||||||
|
|
||||||
if(!brew.title) {
|
if(!brew.title) {
|
||||||
brew.title = getGoodBrewTitle(brew.text);
|
brew.title = getGoodBrewTitle(brew.text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
brew.authors = (req.account) ? [req.account.username] : [];
|
||||||
|
brew.text = mergeBrewText(brew.text, brew.style);
|
||||||
|
|
||||||
delete brew.editId;
|
delete brew.editId;
|
||||||
delete brew.shareId;
|
delete brew.shareId;
|
||||||
delete brew.googleId;
|
delete brew.googleId;
|
||||||
|
|
||||||
req.body = brew;
|
req.body = brew;
|
||||||
|
|
||||||
console.log(oAuth2Client);
|
|
||||||
|
|
||||||
const newBrew = await GoogleActions.newGoogleBrew(oAuth2Client, brew);
|
const newBrew = await GoogleActions.newGoogleBrew(oAuth2Client, brew);
|
||||||
|
|
||||||
return res.status(200).send(newBrew);
|
return res.status(200).send(newBrew);
|
||||||
@@ -134,7 +151,10 @@ const updateGoogleBrew = async (req, res, next)=>{
|
|||||||
|
|
||||||
try { oAuth2Client = GoogleActions.authCheck(req.account, res); } catch (err) { return res.status(err.status).send(err.message); }
|
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 = req.body;
|
||||||
|
brew.text = mergeBrewText(brew.text, brew.style);
|
||||||
|
|
||||||
|
const updatedBrew = await GoogleActions.updateGoogleBrew(oAuth2Client, brew);
|
||||||
|
|
||||||
return res.status(200).send(updatedBrew);
|
return res.status(200).send(updatedBrew);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ const HomebrewSchema = mongoose.Schema({
|
|||||||
description : { type: String, default: '' },
|
description : { type: String, default: '' },
|
||||||
tags : { type: String, default: '' },
|
tags : { type: String, default: '' },
|
||||||
systems : [String],
|
systems : [String],
|
||||||
|
renderer : { type: String, default: '' },
|
||||||
authors : [String],
|
authors : [String],
|
||||||
published : { type: Boolean, default: false },
|
published : { type: Boolean, default: false },
|
||||||
|
|
||||||
@@ -23,28 +24,15 @@ const HomebrewSchema = mongoose.Schema({
|
|||||||
version : { type: Number, default: 1 }
|
version : { type: Number, default: 1 }
|
||||||
}, { versionKey: false });
|
}, { versionKey: false });
|
||||||
|
|
||||||
|
HomebrewSchema.statics.increaseView = async function(query) {
|
||||||
HomebrewSchema.methods.sanatize = function(full=false){
|
const brew = await Homebrew.findOne(query).exec();
|
||||||
const brew = this.toJSON();
|
brew.lastViewed = new Date();
|
||||||
delete brew._id;
|
brew.views = brew.views + 1;
|
||||||
delete brew.__v;
|
await brew.save()
|
||||||
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()
|
|
||||||
.catch((err)=>{
|
.catch((err)=>{
|
||||||
return err;
|
return err;
|
||||||
});
|
});
|
||||||
this.text = text;
|
return brew;
|
||||||
return this;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
HomebrewSchema.statics.get = function(query){
|
HomebrewSchema.statics.get = function(query){
|
||||||
@@ -55,6 +43,8 @@ HomebrewSchema.statics.get = function(query){
|
|||||||
unzipped = zlib.inflateRawSync(brews[0].textBin);
|
unzipped = zlib.inflateRawSync(brews[0].textBin);
|
||||||
brews[0].text = unzipped.toString();
|
brews[0].text = unzipped.toString();
|
||||||
}
|
}
|
||||||
|
if(!brews[0].renderer)
|
||||||
|
brews[0].renderer = 'legacy';
|
||||||
return resolve(brews[0]);
|
return resolve(brews[0]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -66,11 +56,9 @@ HomebrewSchema.statics.getByUser = function(username, allowAccess=false){
|
|||||||
if(allowAccess){
|
if(allowAccess){
|
||||||
delete query.published;
|
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');
|
if(err) return reject('Can not find brew');
|
||||||
return resolve(_.map(brews, (brew)=>{
|
return resolve(brews);
|
||||||
return brew.sanatize(!allowAccess);
|
|
||||||
}));
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
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;
|
if(_.isEmpty(this.state.warnings)) return null;
|
||||||
|
|
||||||
return <div className='renderWarnings'>
|
return <div className='renderWarnings'>
|
||||||
<i className='fa fa-times dismiss' onClick={this.dismiss}/>
|
<i className='fas fa-times dismiss' onClick={this.dismiss}/>
|
||||||
<i className='fa fa-exclamation-triangle ohno' />
|
<i className='fas fa-exclamation-triangle ohno' />
|
||||||
<h3>Render Warnings</h3>
|
<h3>Render Warnings</h3>
|
||||||
<small>If this homebrew is rendering badly if might be because of the following:</small>
|
<small>If this homebrew is rendering badly if might be because of the following:</small>
|
||||||
<ul>{_.values(this.state.warnings)}</ul>
|
<ul>{_.values(this.state.warnings)}</ul>
|
||||||
|
|||||||
@@ -11,41 +11,59 @@ if(typeof navigator !== 'undefined'){
|
|||||||
|
|
||||||
//Language Modes
|
//Language Modes
|
||||||
require('codemirror/mode/gfm/gfm.js'); //Github flavoured markdown
|
require('codemirror/mode/gfm/gfm.js'); //Github flavoured markdown
|
||||||
|
require('codemirror/mode/css/css.js');
|
||||||
require('codemirror/mode/javascript/javascript.js');
|
require('codemirror/mode/javascript/javascript.js');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const CodeEditor = createClass({
|
const CodeEditor = createClass({
|
||||||
getDefaultProps : function() {
|
getDefaultProps : function() {
|
||||||
return {
|
return {
|
||||||
language : '',
|
language : '',
|
||||||
value : '',
|
value : '',
|
||||||
wrap : false,
|
wrap : true,
|
||||||
onChange : function(){},
|
onChange : ()=>{}
|
||||||
onCursorActivity : function(){},
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidMount : function() {
|
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, {
|
this.codeMirror = CodeMirror(this.refs.editor, {
|
||||||
value : this.props.value,
|
value : this.props.value,
|
||||||
lineNumbers : true,
|
lineNumbers : true,
|
||||||
lineWrapping : this.props.wrap,
|
lineWrapping : this.props.wrap,
|
||||||
mode : this.props.language,
|
mode : this.props.language, //TODO: CSS MODE DOESN'T SEEM TO LOAD PROPERLY
|
||||||
extraKeys : {
|
indentWithTabs : true,
|
||||||
|
tabSize : 2,
|
||||||
|
extraKeys : {
|
||||||
'Ctrl-B' : this.makeBold,
|
'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,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.codeMirror.on('change', this.handleChange);
|
// Note: codeMirror passes a copy of itself in this callback. cm === this.codeMirror. Either one works.
|
||||||
this.codeMirror.on('cursorActivity', this.handleCursorActivity);
|
this.codeMirror.on('change', (cm)=>{this.props.onChange(cm.getValue());});
|
||||||
this.updateSize();
|
this.updateSize();
|
||||||
},
|
},
|
||||||
|
|
||||||
makeBold : function() {
|
makeBold : function() {
|
||||||
const selection = this.codeMirror.getSelection();
|
const selection = this.codeMirror.getSelection(), t = selection.slice(0, 2) === '**' && selection.slice(-2) === '**';
|
||||||
this.codeMirror.replaceSelection(`**${selection}**`, 'around');
|
this.codeMirror.replaceSelection(t ? selection.slice(2, -2) : `**${selection}**`, 'around');
|
||||||
if(selection.length === 0){
|
if(selection.length === 0){
|
||||||
const cursor = this.codeMirror.getCursor();
|
const cursor = this.codeMirror.getCursor();
|
||||||
this.codeMirror.setCursor({ line: cursor.line, ch: cursor.ch - 2 });
|
this.codeMirror.setCursor({ line: cursor.line, ch: cursor.ch - 2 });
|
||||||
@@ -53,41 +71,37 @@ const CodeEditor = createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
makeItalic : function() {
|
makeItalic : function() {
|
||||||
const selection = this.codeMirror.getSelection();
|
const selection = this.codeMirror.getSelection(), t = selection.slice(0, 1) === '_' && selection.slice(-1) === '_';
|
||||||
this.codeMirror.replaceSelection(`*${selection}*`, 'around');
|
this.codeMirror.replaceSelection(t ? selection.slice(1, -1) : `_${selection}_`, 'around');
|
||||||
if(selection.length === 0){
|
if(selection.length === 0){
|
||||||
const cursor = this.codeMirror.getCursor();
|
const cursor = this.codeMirror.getCursor();
|
||||||
this.codeMirror.setCursor({ line: cursor.line, ch: cursor.ch - 1 });
|
this.codeMirror.setCursor({ line: cursor.line, ch: cursor.ch - 1 });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillReceiveProps : function(nextProps){
|
makeSpan : function() {
|
||||||
if(this.codeMirror && nextProps.value !== undefined && this.codeMirror.getValue() != nextProps.value) {
|
const selection = this.codeMirror.getSelection(), t = selection.slice(0, 2) === '{{' && selection.slice(-2) === '}}';
|
||||||
this.codeMirror.setValue(nextProps.value);
|
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) {
|
//=-- Externally used -==//
|
||||||
return false;
|
|
||||||
},
|
|
||||||
|
|
||||||
setCursorPosition : function(line, char){
|
setCursorPosition : function(line, char){
|
||||||
setTimeout(()=>{
|
setTimeout(()=>{
|
||||||
this.codeMirror.focus();
|
this.codeMirror.focus();
|
||||||
this.codeMirror.doc.setCursor(line, char);
|
this.codeMirror.doc.setCursor(line, char);
|
||||||
}, 10);
|
}, 10);
|
||||||
},
|
},
|
||||||
|
getCursorPosition : function(){
|
||||||
|
return this.codeMirror.getCursor();
|
||||||
|
},
|
||||||
updateSize : function(){
|
updateSize : function(){
|
||||||
this.codeMirror.refresh();
|
this.codeMirror.refresh();
|
||||||
},
|
},
|
||||||
|
//----------------------//
|
||||||
handleChange : function(editor){
|
|
||||||
this.props.onChange(editor.getValue());
|
|
||||||
},
|
|
||||||
handleCursorActivity : function(){
|
|
||||||
this.props.onCursorActivity(this.codeMirror.doc.getCursor());
|
|
||||||
},
|
|
||||||
|
|
||||||
render : function(){
|
render : function(){
|
||||||
return <div className='codeEditor' ref='editor' />;
|
return <div className='codeEditor' ref='editor' />;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable max-lines */
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const Markdown = require('marked');
|
const Markdown = require('marked');
|
||||||
const renderer = new Markdown.Renderer();
|
const renderer = new Markdown.Renderer();
|
||||||
@@ -13,6 +14,231 @@ renderer.html = function (html) {
|
|||||||
return 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(.*?<\/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.inlineTokens(text) // inlineTokens to process **bold**, *italics*, etc.
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
renderer(token) {
|
||||||
|
return `<span class="inline${token.tags}>${this.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.inline(this.blockTokens(text))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
renderer(token) {
|
||||||
|
return `<div class="block${token.tags}>${this.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.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 : 'text', // Should match "name" above
|
||||||
|
raw : match[0], // Text to consume from the source
|
||||||
|
text : ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
renderer(token) {
|
||||||
|
token.type = token.originalType;
|
||||||
|
const text = this.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]*)/ym;
|
||||||
|
let match;
|
||||||
|
let endIndex = 0;
|
||||||
|
const definitions = [];
|
||||||
|
while (match = regex.exec(src)) {
|
||||||
|
definitions.push({
|
||||||
|
dt : this.inlineTokens(match[1].trim()),
|
||||||
|
dd : this.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.parseInline(def.dt)}</dt>`
|
||||||
|
+ `<dd>${this.parseInline(def.dd)}</dd>\n`;
|
||||||
|
}, '')}
|
||||||
|
</dl>`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Markdown.use({ extensions: [mustacheSpans, mustacheDivs, mustacheInjectInline, definitionLists] });
|
||||||
|
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) {
|
renderer.link = function (href, title, text) {
|
||||||
let self = false;
|
let self = false;
|
||||||
if(href[0] == '#') {
|
if(href[0] == '#') {
|
||||||
@@ -79,7 +305,6 @@ const escape = function (html, encode) {
|
|||||||
return html.replace(escapeReplaceNoEncode, getEscapeReplacement);
|
return html.replace(escapeReplaceNoEncode, getEscapeReplacement);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return html;
|
return html;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -95,10 +320,24 @@ const tagRegex = new RegExp(`(${
|
|||||||
return `\\<${type}|\\</${type}>`;
|
return `\\<${type}|\\</${type}>`;
|
||||||
}).join('|')})`, 'g');
|
}).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 = {
|
module.exports = {
|
||||||
marked : Markdown,
|
marked : Markdown,
|
||||||
render : (rawBrewText)=>{
|
render : (rawBrewText)=>{
|
||||||
|
rawBrewText = rawBrewText.replace(/^\\column$/gm, `<div class='columnSplit'></div>`)
|
||||||
|
.replace(/^(:+)$/gm, (match)=>`${`<div class='blank'></div>`.repeat(match.length)}\n`);
|
||||||
return Markdown(
|
return Markdown(
|
||||||
sanatizeScriptTags(rawBrewText),
|
sanatizeScriptTags(rawBrewText),
|
||||||
{ renderer: renderer }
|
{ 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(){
|
logo : function(){
|
||||||
return <a className='navLogo' href='http://naturalcrit.com'>
|
return <a className='navLogo' href='https://www.naturalcrit.com'>
|
||||||
<NaturalCritIcon />
|
<NaturalCritIcon />
|
||||||
<span className='name'>
|
<span className='name'>
|
||||||
Natural<span className='crit'>Crit</span>
|
Natural<span className='crit'>Crit</span>
|
||||||
@@ -50,7 +50,7 @@ const Nav = {
|
|||||||
const classes = cx('navItem', this.props.color, this.props.className);
|
const classes = cx('navItem', this.props.color, this.props.className);
|
||||||
|
|
||||||
let icon;
|
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']);
|
const props = _.omit(this.props, ['newTab']);
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
//@import (less) 'naturalcrit/styles/style.fonts.css';
|
|
||||||
nav{
|
nav{
|
||||||
background-color : #333;
|
background-color : #333;
|
||||||
.navContent{
|
.navContent{
|
||||||
|
position : relative;
|
||||||
display : flex;
|
display : flex;
|
||||||
justify-content : space-between;
|
justify-content : space-between;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,9 +56,9 @@ const SplitPane = createClass({
|
|||||||
renderDivider : function(){
|
renderDivider : function(){
|
||||||
return <div className='divider' onMouseDown={this.handleDown} >
|
return <div className='divider' onMouseDown={this.handleDown} >
|
||||||
<div className='dots'>
|
<div className='dots'>
|
||||||
<i className='fa fa-circle' />
|
<i className='fas fa-circle' />
|
||||||
<i className='fa fa-circle' />
|
<i className='fas fa-circle' />
|
||||||
<i className='fa fa-circle' />
|
<i className='fas fa-circle' />
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
},
|
},
|
||||||
|
|||||||
651
themes/5ePhb.style.less
Normal file
@@ -0,0 +1,651 @@
|
|||||||
|
@import (less) './themes/fonts/5e/fonts.less';
|
||||||
|
@import (less) './themes/assets/assets.less';
|
||||||
|
|
||||||
|
//Colors
|
||||||
|
@background : #EEE5CE;
|
||||||
|
@noteGreen : #e0e5c1;
|
||||||
|
@headerUnderline : #c9ad6a;
|
||||||
|
@horizontalRule : #9c2b1b;
|
||||||
|
@headerText : #58180D;
|
||||||
|
@monsterStatBackground : #EEDBAB;
|
||||||
|
@page { margin: 0; }
|
||||||
|
body {
|
||||||
|
counter-reset : phb-page-numbers;
|
||||||
|
}
|
||||||
|
*{
|
||||||
|
-webkit-print-color-adjust : exact;
|
||||||
|
}
|
||||||
|
.useSansSerif(){
|
||||||
|
font-family : ScalySansRemake;
|
||||||
|
font-size : 0.325cm;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.useColumns(@multiplier : 1){
|
||||||
|
column-count : 2;
|
||||||
|
column-fill : auto;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
.page{
|
||||||
|
.useColumns();
|
||||||
|
counter-increment : phb-page-numbers;
|
||||||
|
position : relative;
|
||||||
|
z-index : 15;
|
||||||
|
box-sizing : border-box;
|
||||||
|
overflow : hidden;
|
||||||
|
height : 279.4mm;
|
||||||
|
width : 215.9mm;
|
||||||
|
padding : 1.4cm 1.9cm 1.7cm;
|
||||||
|
background-color : @background;
|
||||||
|
background-image : @backgroundImage;
|
||||||
|
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
|
||||||
|
margin-bottom : 0.8em;
|
||||||
|
line-height : 1.3em;
|
||||||
|
&+p{
|
||||||
|
margin-top : -0.8em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
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 : 0.8em;
|
||||||
|
font-size: 3.5cm;
|
||||||
|
padding-left: 40px;
|
||||||
|
margin-left: -40px;
|
||||||
|
padding-top:10px;
|
||||||
|
margin-top:-8px;
|
||||||
|
padding-bottom:10px;
|
||||||
|
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;
|
||||||
|
margin-bottom : 0.05cm;
|
||||||
|
font-size : 0.75cm;
|
||||||
|
}
|
||||||
|
h3{
|
||||||
|
margin-top : -0.1cm;
|
||||||
|
margin-bottom : 0.1cm;
|
||||||
|
font-size : 0.575cm;
|
||||||
|
border-bottom : 2px solid @headerUnderline;
|
||||||
|
}
|
||||||
|
h4{
|
||||||
|
margin-top : -0.02cm;
|
||||||
|
margin-bottom : 0.02cm;
|
||||||
|
font-size : 0.458cm;
|
||||||
|
}
|
||||||
|
h5{
|
||||||
|
margin-top : -0.02cm;
|
||||||
|
margin-bottom : 0.02cm;
|
||||||
|
font-family : ScalySansSmallCapsRemake;
|
||||||
|
font-size : 0.423cm;
|
||||||
|
font-weight : 900;
|
||||||
|
}
|
||||||
|
//*****************************
|
||||||
|
// * TABLE
|
||||||
|
// *****************************/
|
||||||
|
table{
|
||||||
|
.useSansSerif();
|
||||||
|
width : 100%;
|
||||||
|
margin-bottom : 1em;
|
||||||
|
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{
|
||||||
|
&::before{
|
||||||
|
content : "";
|
||||||
|
box-sizing : border-box;
|
||||||
|
border-style : solid;
|
||||||
|
border-width : 11px;
|
||||||
|
border-image : @noteBorderImage 12;
|
||||||
|
border-image-outset : 9px 0px;
|
||||||
|
box-shadow : 1px 4px 14px #888;
|
||||||
|
position : absolute;
|
||||||
|
width : 100%;
|
||||||
|
height : 100%;
|
||||||
|
top : 0;
|
||||||
|
left : 0;
|
||||||
|
}
|
||||||
|
.useSansSerif();
|
||||||
|
position : relative;
|
||||||
|
margin-top : 1.3em;
|
||||||
|
margin-left : -0.1em;
|
||||||
|
margin-right : -0.1em;
|
||||||
|
background-color : @noteGreen;
|
||||||
|
padding : 0.5em 0.6em;
|
||||||
|
& + * {
|
||||||
|
margin-top : 1.3em;
|
||||||
|
}
|
||||||
|
p{
|
||||||
|
display : block;
|
||||||
|
padding-bottom : 0px;
|
||||||
|
}
|
||||||
|
p + p {
|
||||||
|
padding-top : .8em;
|
||||||
|
}
|
||||||
|
:last-child {
|
||||||
|
margin-bottom : 0em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//************************************
|
||||||
|
// * DESCRIPTIVE TEXT BOX
|
||||||
|
// ************************************/
|
||||||
|
.descriptive{
|
||||||
|
.useSansSerif();
|
||||||
|
display : block-inline;
|
||||||
|
margin-top : 1.4em;
|
||||||
|
background-color : #faf7ea;
|
||||||
|
font-family : ScalySansRemake;
|
||||||
|
border-style : solid;
|
||||||
|
border-width : 7px;
|
||||||
|
border-image : @descriptiveBoxImage 12 stretch;
|
||||||
|
border-image-outset : 4px;
|
||||||
|
box-shadow : 0px 0px 6px #faf7ea;
|
||||||
|
padding : 0.1em;
|
||||||
|
& + * {
|
||||||
|
margin-top : 1.4em;
|
||||||
|
}
|
||||||
|
p{
|
||||||
|
display : block;
|
||||||
|
padding-bottom : 0px;
|
||||||
|
line-height : 1.5em;
|
||||||
|
}
|
||||||
|
p + p {
|
||||||
|
padding-top : .8em;
|
||||||
|
}
|
||||||
|
:last-child {
|
||||||
|
margin-bottom : 0em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//*****************************
|
||||||
|
// * MONSTER STAT BLOCK
|
||||||
|
// *****************************/
|
||||||
|
.monster {
|
||||||
|
&.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;
|
||||||
|
box-shadow : 1px 4px 14px #888;
|
||||||
|
padding : 4px 2px;
|
||||||
|
margin : 0px -6px 1em;
|
||||||
|
}
|
||||||
|
.useSansSerif();
|
||||||
|
//-webkit-transform : translateZ(0); //Prevents shadows from breaking across columns, but breaks internal columns...
|
||||||
|
position : relative;
|
||||||
|
padding : 0px;
|
||||||
|
margin-bottom : 1em;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Monster Ability table
|
||||||
|
hr + table:first-of-type{
|
||||||
|
margin : 0;
|
||||||
|
column-span : 1;
|
||||||
|
color : @headerText;
|
||||||
|
background-color : transparent;
|
||||||
|
border-style : none;
|
||||||
|
border-image : none;
|
||||||
|
-webkit-column-span : 1;
|
||||||
|
tr {
|
||||||
|
background-color : transparent;
|
||||||
|
}
|
||||||
|
td,th {
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Full Width
|
||||||
|
.monster.wide{
|
||||||
|
.useColumns(0.96);
|
||||||
|
}
|
||||||
|
|
||||||
|
hr+hr+blockquote{
|
||||||
|
.useColumns(0.96);
|
||||||
|
}
|
||||||
|
//*****************************
|
||||||
|
// * 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: 2px 4px;
|
||||||
|
color: #58180d;
|
||||||
|
background-color: #faf7ea;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre code{
|
||||||
|
width : 100%;
|
||||||
|
display : block;
|
||||||
|
border : 4px solid;
|
||||||
|
border-image : @codeBorderImage 26 stretch;
|
||||||
|
border-image-width : 10px;
|
||||||
|
border-image-outset : 2px;
|
||||||
|
border-radius : 12px;
|
||||||
|
}
|
||||||
|
//*****************************
|
||||||
|
// * EXTRAS
|
||||||
|
// *****************************/
|
||||||
|
hr{
|
||||||
|
visibility : hidden;
|
||||||
|
margin : 0px;
|
||||||
|
}
|
||||||
|
//Modified unorder list, used in spells
|
||||||
|
hr+ul{
|
||||||
|
margin-bottom : 0.5em;
|
||||||
|
padding-left : 1em;
|
||||||
|
text-indent : -1em;
|
||||||
|
list-style-type : none;
|
||||||
|
}
|
||||||
|
.columnSplit {
|
||||||
|
visibility : hidden;
|
||||||
|
-webkit-column-break-after : always;
|
||||||
|
break-after : always;
|
||||||
|
-moz-column-break-after : always;
|
||||||
|
break-before : column;
|
||||||
|
}
|
||||||
|
//Avoid breaking up
|
||||||
|
p,blockquote,table{
|
||||||
|
z-index : 15;
|
||||||
|
-webkit-column-break-inside : avoid;
|
||||||
|
page-break-inside : avoid;
|
||||||
|
break-inside : avoid;
|
||||||
|
}
|
||||||
|
//Better spacing for spell blocks
|
||||||
|
h4+p+hr+ul{
|
||||||
|
margin-top : -0.5em
|
||||||
|
}
|
||||||
|
//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 : 4;
|
||||||
|
column-span : all;
|
||||||
|
-webkit-column-span : all;
|
||||||
|
-moz-column-span : all;
|
||||||
|
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
|
||||||
|
// *****************************/
|
||||||
|
.page .wide{
|
||||||
|
column-span : all;
|
||||||
|
-webkit-column-span : all;
|
||||||
|
-moz-column-span : all;
|
||||||
|
}
|
||||||
|
//*****************************
|
||||||
|
// * CLASS TABLE
|
||||||
|
// *****************************/
|
||||||
|
.page .classTable{
|
||||||
|
margin-top : 25px;
|
||||||
|
margin-bottom : 40px;
|
||||||
|
border-collapse : separate;
|
||||||
|
background-color : white;
|
||||||
|
border : initial;
|
||||||
|
border-style : solid;
|
||||||
|
border-image-outset : 25px 17px;
|
||||||
|
border-image-repeat : stretch;
|
||||||
|
border-image-slice : 150 200 150 200;
|
||||||
|
border-image-source : @frameBorderImage;
|
||||||
|
border-image-width : 47px;
|
||||||
|
h5{
|
||||||
|
margin-bottom : 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//*****************************
|
||||||
|
// * TABLE OF CONTENTS
|
||||||
|
// *****************************/
|
||||||
|
.page .toc{
|
||||||
|
-webkit-column-break-inside : avoid;
|
||||||
|
page-break-inside : avoid;
|
||||||
|
break-inside : avoid;
|
||||||
|
h1 {
|
||||||
|
text-align : center;
|
||||||
|
margin-bottom : 0.1cm;
|
||||||
|
}
|
||||||
|
a{
|
||||||
|
display : table;
|
||||||
|
color : inherit;
|
||||||
|
text-decoration : none;
|
||||||
|
&:hover{
|
||||||
|
text-decoration : underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
h4 {
|
||||||
|
margin-top : 0.1cm;
|
||||||
|
}
|
||||||
|
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; /* Set as you want */
|
||||||
|
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 Price text 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//*****************************
|
||||||
|
// * MUSTACHE DIVS/SPANS
|
||||||
|
// *****************************/
|
||||||
|
.page {
|
||||||
|
.block {
|
||||||
|
break-inside : avoid;
|
||||||
|
-webkit-transform : translateZ(0); //Prevents shadows from breaking across columns
|
||||||
|
}
|
||||||
|
.inline-block {
|
||||||
|
display : inline-block;
|
||||||
|
text-indent : initial;
|
||||||
|
line-height : 1.3em;
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
column-gap : 0.5cm; //Default spacing if a div uses multicolumns
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//*****************************
|
||||||
|
// * DEFINITION LISTS
|
||||||
|
// *****************************/
|
||||||
|
.page {
|
||||||
|
dl {
|
||||||
|
line-height : 1.3em;
|
||||||
|
padding-left : 1em;
|
||||||
|
text-indent : -1em;
|
||||||
|
}
|
||||||
|
dl + p {
|
||||||
|
margin-top: 0.5em;
|
||||||
|
}
|
||||||
|
p + dl {
|
||||||
|
margin-top: -0.5em;
|
||||||
|
}
|
||||||
|
dt {
|
||||||
|
float: left;
|
||||||
|
//clear: left; //Doesn't seem necessary
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
dd {
|
||||||
|
margin-left : 0px;
|
||||||
|
text-indent : 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//*****************************
|
||||||
|
// * BLANK LINE
|
||||||
|
// *****************************/
|
||||||
|
.page {
|
||||||
|
.blank {
|
||||||
|
height: 0.75em;
|
||||||
|
}
|
||||||
|
p + .blank {
|
||||||
|
margin-top: -1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
@import (less) 'shared/naturalcrit/styles/reset.less';
|
@import (less) './themes/fonts/5e legacy/fonts.less';
|
||||||
@import (less) './client/homebrew/phbStyle/phb.fonts.css';
|
@import (less) './themes/assets/assets.less';
|
||||||
@import (less) './client/homebrew/phbStyle/phb.assets.less';
|
@import (less) './themes/phb.depricated.less';
|
||||||
@import (less) './client/homebrew/phbStyle/phb.depricated.less';
|
|
||||||
//Colors
|
//Colors
|
||||||
@background : #EEE5CE;
|
@background : #EEE5CE;
|
||||||
@noteGreen : #e0e5c1;
|
@noteGreen : #e0e5c1;
|
||||||
@@ -189,7 +188,6 @@ body {
|
|||||||
border-image : @noteBorderImage 11;
|
border-image : @noteBorderImage 11;
|
||||||
border-image-outset : 9px 0px;
|
border-image-outset : 9px 0px;
|
||||||
box-shadow : 1px 4px 14px #888;
|
box-shadow : 1px 4px 14px #888;
|
||||||
-webkit-transform : translateZ(0); //Prevents shadows from breaking across columns
|
|
||||||
p, ul{
|
p, ul{
|
||||||
font-size : 0.352cm;
|
font-size : 0.352cm;
|
||||||
line-height : 1.1em;
|
line-height : 1.1em;
|
||||||
@@ -208,7 +206,7 @@ body {
|
|||||||
background-color : @monsterStatBackground;
|
background-color : @monsterStatBackground;
|
||||||
border-style : solid;
|
border-style : solid;
|
||||||
border-width : 10px;
|
border-width : 10px;
|
||||||
border-image : @monsterBorderImage 10;
|
border-image : @monsterBorderImageLegacy 10;
|
||||||
h2{
|
h2{
|
||||||
margin-top : -8px;
|
margin-top : -8px;
|
||||||
margin-bottom : 0px;
|
margin-bottom : 0px;
|
||||||
10
themes/assets/assets.less
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
@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');
|
||||||
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/fonts/5e legacy/Bookinsanity Bold Italic.woff2
Normal file
BIN
themes/fonts/5e legacy/Bookinsanity Bold.woff2
Normal file
BIN
themes/fonts/5e legacy/Bookinsanity Italic.woff2
Normal file
BIN
themes/fonts/5e legacy/Bookinsanity.woff2
Normal file
BIN
themes/fonts/5e legacy/Mr Eaves Small Caps.woff2
Normal file
BIN
themes/fonts/5e legacy/Scaly Sans Caps.woff2
Normal file
BIN
themes/fonts/5e legacy/Scaly Sans.woff2
Normal file
BIN
themes/fonts/5e legacy/Solbera Imitation.woff2
Normal file
55
themes/fonts/5e legacy/fonts.less
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
/* Main Font, serif */
|
||||||
|
@font-face {
|
||||||
|
font-family: BookSanity;
|
||||||
|
src: url('../fonts/5e legacy/Bookinsanity.woff2');
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: BookSanity;
|
||||||
|
src: url('../fonts/5e legacy/Bookinsanity Bold.woff2');
|
||||||
|
font-weight: bold;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: BookSanity;
|
||||||
|
src: url('../fonts/5e legacy/Bookinsanity Italic.woff2');
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: BookSanity;
|
||||||
|
src: url('../fonts/5e legacy/Bookinsanity Bold Italic.woff2');
|
||||||
|
font-weight: bold;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Notes and Tables, sans-serif */
|
||||||
|
@font-face {
|
||||||
|
font-family: ScalySans;
|
||||||
|
src: url('../fonts/5e legacy/Scaly Sans.woff2');
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: ScalySansSmallCaps;
|
||||||
|
src: url('../fonts/5e legacy/Scaly Sans Caps.woff2');
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Headers */
|
||||||
|
@font-face {
|
||||||
|
font-family: MrJeeves;
|
||||||
|
src: url('../fonts/5e legacy/Mr Eaves Small Caps.woff2');
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fancy Drop Cap */
|
||||||
|
@font-face {
|
||||||
|
font-family: Solberry;
|
||||||
|
src: url('../fonts/5e legacy/Solbera Imitation.woff2');
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||