mirror of
https://github.com/naturalcrit/homebrewery.git
synced 2026-01-27 20:23:08 +00:00
Compare commits
579 Commits
Fix_#2954
...
Test-BR-li
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cb987930f2 | ||
|
|
e159e57222 | ||
|
|
43dc1bed7d | ||
|
|
313492a344 | ||
|
|
8d1464a2c4 | ||
|
|
552cf30863 | ||
|
|
4daa8042a2 | ||
|
|
51e79c2c5f | ||
|
|
88e8140b60 | ||
|
|
252698b135 | ||
|
|
21f1704626 | ||
|
|
19556d9f36 | ||
|
|
0d4d97c5c5 | ||
|
|
55f333a9e5 | ||
|
|
2361cdeadc | ||
|
|
aeae704173 | ||
|
|
c420410904 | ||
|
|
0daf8c5c83 | ||
|
|
924d014c69 | ||
|
|
8992cf8251 | ||
|
|
7c6aa0ffec | ||
|
|
ebe64c508f | ||
|
|
f3514cfea6 | ||
|
|
8ed25fb7cf | ||
|
|
762cd58d52 | ||
|
|
477f706eb9 | ||
|
|
edcf9979a7 | ||
|
|
ef2beec590 | ||
|
|
c10559ba5f | ||
|
|
69c633dabe | ||
|
|
8bdcdcd510 | ||
|
|
ce03f598b2 | ||
|
|
addbf19682 | ||
|
|
479aae4b2f | ||
|
|
4b6652c470 | ||
|
|
e9d1209ce8 | ||
|
|
7c62e49767 | ||
|
|
9b0da36365 | ||
|
|
1391a9053d | ||
|
|
fee88d1d47 | ||
|
|
a47dc51bd1 | ||
|
|
cfb9e1afa2 | ||
|
|
540a0a7a36 | ||
|
|
b7e422ac06 | ||
|
|
df5eeb5c97 | ||
|
|
e2de225625 | ||
|
|
5b7d5bee24 | ||
|
|
18eb3ec643 | ||
|
|
5f9cc48fe1 | ||
|
|
56d1855518 | ||
|
|
758b508955 | ||
|
|
3221b40903 | ||
|
|
39a49a6d62 | ||
|
|
02f63e0b02 | ||
|
|
0ba943ceb0 | ||
|
|
578a8d7eba | ||
|
|
9a9d7a6b5e | ||
|
|
917b6b3145 | ||
|
|
b36376f9e8 | ||
|
|
58a22750c5 | ||
|
|
df1b601de7 | ||
|
|
1ed44282e3 | ||
|
|
f421ce1d93 | ||
|
|
ca0f18acd6 | ||
|
|
87d76ea8f6 | ||
|
|
9f5a29099c | ||
|
|
0360d6b6c5 | ||
|
|
8d2057431b | ||
|
|
ee0d737b9c | ||
|
|
cb27b26103 | ||
|
|
0564fb82f6 | ||
|
|
5596f2d9da | ||
|
|
a11b67f139 | ||
|
|
17717ea2a9 | ||
|
|
c15e7b2da3 | ||
|
|
fcca56f502 | ||
|
|
68f66b2bac | ||
|
|
0d71f291e7 | ||
|
|
fc065d250b | ||
|
|
01d93b98d5 | ||
|
|
f5aa37bd5e | ||
|
|
d6d445dad5 | ||
|
|
1af66cf571 | ||
|
|
2cb8b5d014 | ||
|
|
34a0b4eb05 | ||
|
|
854a2ab35e | ||
|
|
42accdb54f | ||
|
|
7e5bade4fa | ||
|
|
ed30a1cd7d | ||
|
|
94f478477d | ||
|
|
50bda9455f | ||
|
|
d8d672fada | ||
|
|
bf297939dc | ||
|
|
df563b9294 | ||
|
|
e584eec8c2 | ||
|
|
557178172b | ||
|
|
45e98debbd | ||
|
|
0bd5ac42b6 | ||
|
|
af729de096 | ||
|
|
40cd53fcb8 | ||
|
|
f326d11232 | ||
|
|
85ea91fed8 | ||
|
|
a0c9b8849c | ||
|
|
ff91ebb06a | ||
|
|
21baab784e | ||
|
|
1f3a0f1f99 | ||
|
|
6b4f5bd0af | ||
|
|
52cf1ddea0 | ||
|
|
b79c5954ff | ||
|
|
9944398e4c | ||
|
|
489f00b785 | ||
|
|
1a515f8d9c | ||
|
|
f386ba3f45 | ||
|
|
db16248afb | ||
|
|
634450d4a9 | ||
|
|
559f55f781 | ||
|
|
64b7527ad0 | ||
|
|
d48d5260a4 | ||
|
|
41dc78375c | ||
|
|
bbc601cf47 | ||
|
|
e89920bd1e | ||
|
|
2e12980180 | ||
|
|
b77af1bcc8 | ||
|
|
45d188fea1 | ||
|
|
1ce26ca953 | ||
|
|
d1c0557341 | ||
|
|
4e857a1a99 | ||
|
|
547ac11756 | ||
|
|
0e2443f772 | ||
|
|
9d16f4556e | ||
|
|
6d0d0057f6 | ||
|
|
b8d9023c98 | ||
|
|
4578cf6584 | ||
|
|
111869d33b | ||
|
|
d0b4486e15 | ||
|
|
1aed753911 | ||
|
|
c080e5b191 | ||
|
|
11396389ab | ||
|
|
0bcf228881 | ||
|
|
de30722554 | ||
|
|
6cfdfad7d3 | ||
|
|
a9fa0bd32d | ||
|
|
cfbf4021dc | ||
|
|
e3780e844d | ||
|
|
659510e364 | ||
|
|
29da0396fd | ||
|
|
c3e08181e9 | ||
|
|
213a719337 | ||
|
|
a7a7e46e89 | ||
|
|
ada06c9618 | ||
|
|
03798e945d | ||
|
|
6d2cbaacc0 | ||
|
|
f2f894381e | ||
|
|
10fae6dbac | ||
|
|
ebc7f055fa | ||
|
|
ce01b6c1ff | ||
|
|
553562611f | ||
|
|
423caefe1a | ||
|
|
ae1de819ea | ||
|
|
27c4cfd25c | ||
|
|
bf22104474 | ||
|
|
c3e0a687c0 | ||
|
|
00a2b130eb | ||
|
|
8eef810f3f | ||
|
|
a04df0fdfc | ||
|
|
a504703d41 | ||
|
|
3ce9bb1310 | ||
|
|
66bfc8f27b | ||
|
|
6c8b94453e | ||
|
|
460fb655d8 | ||
|
|
be1742d01d | ||
|
|
5d3742aea6 | ||
|
|
1966027289 | ||
|
|
35d50cc9d1 | ||
|
|
3f41306306 | ||
|
|
988bf1b0a9 | ||
|
|
2f1ade8463 | ||
|
|
518924d725 | ||
|
|
6269651c8d | ||
|
|
057abcda0d | ||
|
|
b6b23a787c | ||
|
|
899004cfaf | ||
|
|
7e826cd4f5 | ||
|
|
3b150891bc | ||
|
|
e87acc3f0f | ||
|
|
b1e99f1385 | ||
|
|
4e0b6d634d | ||
|
|
a72f0f2f34 | ||
|
|
23944f4fe0 | ||
|
|
c244199190 | ||
|
|
8848c06b15 | ||
|
|
37d56f7365 | ||
|
|
e2d6b5afc4 | ||
|
|
e4df577a32 | ||
|
|
f005cb784f | ||
|
|
d733b1f8f8 | ||
|
|
d8d403ffb8 | ||
|
|
574d68f678 | ||
|
|
1b3d7b33c6 | ||
|
|
7f4a304f04 | ||
|
|
d0a06b5cf7 | ||
|
|
6dfd44e2f1 | ||
|
|
f608cb2d65 | ||
|
|
28a1610573 | ||
|
|
03e7699b8b | ||
|
|
11f4275e7b | ||
|
|
07fe1c6f19 | ||
|
|
3e78b03785 | ||
|
|
6a31d612e6 | ||
|
|
ecd8869097 | ||
|
|
73c2be147c | ||
|
|
caa290f580 | ||
|
|
d69288076a | ||
|
|
df00160bc4 | ||
|
|
be18843b09 | ||
|
|
f1ff032e1e | ||
|
|
36df121cf6 | ||
|
|
c22bb7fb92 | ||
|
|
b94bb38922 | ||
|
|
1576a946b0 | ||
|
|
4de0a11f1a | ||
|
|
66fd9e188b | ||
|
|
a0f44a088f | ||
|
|
fb20be833c | ||
|
|
fc43f95ea5 | ||
|
|
29d04fe57d | ||
|
|
bd32f5a1b8 | ||
|
|
98c353b9fe | ||
|
|
41b80422c5 | ||
|
|
c1f608d02f | ||
|
|
abc830eda2 | ||
|
|
60b6dbb388 | ||
|
|
7610466ee4 | ||
|
|
9f8831eed6 | ||
|
|
0ac981586f | ||
|
|
fc085111db | ||
|
|
5e03d97869 | ||
|
|
a11ae6655e | ||
|
|
2471de20a9 | ||
|
|
8e99d47869 | ||
|
|
eebc9c2bfa | ||
|
|
bd5c85147d | ||
|
|
7f7a8338ff | ||
|
|
2a9945f09f | ||
|
|
b7241f79cb | ||
|
|
76ccbfbf20 | ||
|
|
77c58eae2e | ||
|
|
4a2b8dc261 | ||
|
|
fa1a0e2351 | ||
|
|
f7b36a9b05 | ||
|
|
f4ce2437a7 | ||
|
|
aa34bb44c9 | ||
|
|
e3c90ace73 | ||
|
|
7c1545a07d | ||
|
|
953c612830 | ||
|
|
5dbb5499c6 | ||
|
|
d4f6c329b8 | ||
|
|
a574ec0777 | ||
|
|
3e5a72fa96 | ||
|
|
4df2a73800 | ||
|
|
aea9296908 | ||
|
|
08eeb57cb0 | ||
|
|
e5e9a9efe1 | ||
|
|
aafc6fad7d | ||
|
|
b91f18a8a0 | ||
|
|
20bfff5157 | ||
|
|
3c735e599f | ||
|
|
4958ade937 | ||
|
|
57dc5d4923 | ||
|
|
3c5ad74e38 | ||
|
|
e988e20f5b | ||
|
|
cac6dbd40c | ||
|
|
2461b4ab6a | ||
|
|
7c4f163042 | ||
|
|
f6c95fb8b7 | ||
|
|
2fee37239f | ||
|
|
2cb19848aa | ||
|
|
913cde44ff | ||
|
|
c7ff1fc07f | ||
|
|
da42e835c5 | ||
|
|
7a071496f3 | ||
|
|
b8d65f2f56 | ||
|
|
9c197ea25a | ||
|
|
d75db5d378 | ||
|
|
a2538bed20 | ||
|
|
69c45d63a4 | ||
|
|
80003f6c57 | ||
|
|
9d67724da9 | ||
|
|
3578a7e1e2 | ||
|
|
533586f516 | ||
|
|
591ccf564c | ||
|
|
ecc91af1d6 | ||
|
|
4ff043f759 | ||
|
|
84e18aae5a | ||
|
|
b53bda937a | ||
|
|
4db4bba73f | ||
|
|
2c2e6d6027 | ||
|
|
1aeea034d2 | ||
|
|
63bd483b3e | ||
|
|
19cb24d8db | ||
|
|
96ebe0f617 | ||
|
|
eb3178bf80 | ||
|
|
a72f47df46 | ||
|
|
a9823d39e2 | ||
|
|
6ec65eee23 | ||
|
|
9c2610ff40 | ||
|
|
2d47cd2a76 | ||
|
|
6eb938bb37 | ||
|
|
94a431eec8 | ||
|
|
4eb71b1220 | ||
|
|
74122d9057 | ||
|
|
914521cada | ||
|
|
70bda94033 | ||
|
|
915137af5e | ||
|
|
7516c0cbd3 | ||
|
|
fdfae9a771 | ||
|
|
8cc693461d | ||
|
|
e7f8cda6ae | ||
|
|
b9f7e820c7 | ||
|
|
26cc272b37 | ||
|
|
bffa6eb0c9 | ||
|
|
2779055e50 | ||
|
|
37d00f1255 | ||
|
|
d9b599e814 | ||
|
|
40d453bc7c | ||
|
|
6ff0cfe383 | ||
|
|
a6b7ed4dd2 | ||
|
|
bf0614026d | ||
|
|
06005009e4 | ||
|
|
cf16566da8 | ||
|
|
34f104b406 | ||
|
|
766ab8f10a | ||
|
|
aa4276a50e | ||
|
|
fbedafb204 | ||
|
|
85cd7c7336 | ||
|
|
c137d40037 | ||
|
|
5a9e7850c2 | ||
|
|
6e7342d6f0 | ||
|
|
1598adfa67 | ||
|
|
b49936c24b | ||
|
|
816f4f75f6 | ||
|
|
a091a18604 | ||
|
|
edadb3cb77 | ||
|
|
3749a5c2b1 | ||
|
|
e9b5e4ab0c | ||
|
|
28109d28dc | ||
|
|
7f56797779 | ||
|
|
a95eef0545 | ||
|
|
bbf6c3589a | ||
|
|
4a4a14b2ab | ||
|
|
6b0c3b65b4 | ||
|
|
59006d354f | ||
|
|
fe2d02a24c | ||
|
|
7c357a2aa1 | ||
|
|
26c9406211 | ||
|
|
5eb8432544 | ||
|
|
fb13a1c98d | ||
|
|
b20eb28a37 | ||
|
|
d84f071c62 | ||
|
|
bc7297de2e | ||
|
|
a2c4f73e7d | ||
|
|
9804c3933f | ||
|
|
e2b0da7830 | ||
|
|
5a5119a367 | ||
|
|
c310a8c1c2 | ||
|
|
11bfdd89b8 | ||
|
|
6898425435 | ||
|
|
be2557611e | ||
|
|
1a9a726263 | ||
|
|
dbf82f69f1 | ||
|
|
107e54688b | ||
|
|
b99282a5a7 | ||
|
|
1c0eb720ad | ||
|
|
93482f9022 | ||
|
|
8159c408c8 | ||
|
|
0632d78f71 | ||
|
|
c0155052ea | ||
|
|
628b2542a0 | ||
|
|
85f1da942f | ||
|
|
3909d5aef9 | ||
|
|
f0e047e7cc | ||
|
|
a1237305d7 | ||
|
|
d588a92147 | ||
|
|
2b7a1e1cb2 | ||
|
|
c8efca3120 | ||
|
|
a53eacf055 | ||
|
|
1b10a4001a | ||
|
|
75e71dd6f5 | ||
|
|
3f87b9f7d3 | ||
|
|
32561cf368 | ||
|
|
bf94cdcb6f | ||
|
|
e8eedcf6d6 | ||
|
|
92d1238a46 | ||
|
|
fcfd3171bd | ||
|
|
9a6cf8c5d2 | ||
|
|
91d928fd8a | ||
|
|
bca653bc4d | ||
|
|
2bedc6d7d4 | ||
|
|
674fb6ff57 | ||
|
|
79c8309291 | ||
|
|
9745daf6e2 | ||
|
|
90632b78ce | ||
|
|
f71850d8b1 | ||
|
|
dceb5e516b | ||
|
|
adb1db1d3c | ||
|
|
e8d1e632b4 | ||
|
|
50fcffb253 | ||
|
|
aae5367ad2 | ||
|
|
40b0c1ce3a | ||
|
|
ba83dfacd9 | ||
|
|
2717e6a9a4 | ||
|
|
d576bddd32 | ||
|
|
fde21868cd | ||
|
|
ed8c4d0eef | ||
|
|
6e9d293bbe | ||
|
|
7e1312805f | ||
|
|
d629fa1731 | ||
|
|
6301a66fd3 | ||
|
|
980a7bd57e | ||
|
|
6b0022ad00 | ||
|
|
0f33973e58 | ||
|
|
7a41a140fd | ||
|
|
57467701d0 | ||
|
|
9dbfb26e6c | ||
|
|
7a169cbd9e | ||
|
|
2dc8a8fbe9 | ||
|
|
5f14f656ef | ||
|
|
6e8a0d7314 | ||
|
|
e61144beb8 | ||
|
|
64b792c645 | ||
|
|
aee5b7a8cc | ||
|
|
99d3d28754 | ||
|
|
912f9f0cf6 | ||
|
|
c63b6ffaf0 | ||
|
|
0c90d1a14d | ||
|
|
08b0f47ea2 | ||
|
|
f9b42a30f7 | ||
|
|
0148eafce0 | ||
|
|
a3ec5b8d3b | ||
|
|
4ded48df1e | ||
|
|
bc14246fe7 | ||
|
|
fcf985a115 | ||
|
|
a060fd123c | ||
|
|
7c7e143365 | ||
|
|
efa8f3fedf | ||
|
|
972a93d292 | ||
|
|
35be1e9b94 | ||
|
|
1a91c390f8 | ||
|
|
206e4fbda8 | ||
|
|
af98cb3867 | ||
|
|
f8fc6f7aa4 | ||
|
|
eb0fa28a03 | ||
|
|
4ab1a22eb3 | ||
|
|
962a46a670 | ||
|
|
cb16b32016 | ||
|
|
56f348f7ed | ||
|
|
b7c99b2d52 | ||
|
|
889f80f537 | ||
|
|
c270a69bb9 | ||
|
|
db0df82202 | ||
|
|
1346361f80 | ||
|
|
fdaf9d4808 | ||
|
|
3cdfae4270 | ||
|
|
a9275698fa | ||
|
|
99f2972079 | ||
|
|
afc92c4545 | ||
|
|
b26526a2f1 | ||
|
|
4f57f006ce | ||
|
|
666a94cd65 | ||
|
|
f0c094e9d8 | ||
|
|
a1c228b1d1 | ||
|
|
5e5c637c79 | ||
|
|
d573129f31 | ||
|
|
7c69d2a74d | ||
|
|
89bd082967 | ||
|
|
f4c26053c0 | ||
|
|
abd52f93d8 | ||
|
|
47d7c69d1b | ||
|
|
57cb334c15 | ||
|
|
c29e1905bf | ||
|
|
52d00b17a4 | ||
|
|
35364c400a | ||
|
|
77f0c1bf56 | ||
|
|
2d281072fa | ||
|
|
870a4c3363 | ||
|
|
aa951ff96c | ||
|
|
83b8f9c3b7 | ||
|
|
3a20452214 | ||
|
|
bae9fe939d | ||
|
|
3e4ba89ed9 | ||
|
|
2c5c3d40df | ||
|
|
213240327d | ||
|
|
eca0f59b40 | ||
|
|
51936a1b99 | ||
|
|
6136b78395 | ||
|
|
81f56ec91d | ||
|
|
4eb8abf1e7 | ||
|
|
23910cc94c | ||
|
|
ef0ee78758 | ||
|
|
1b20c00842 | ||
|
|
db9212bd12 | ||
|
|
7348ecbb3d | ||
|
|
31a22703c1 | ||
|
|
33f8f6bf38 | ||
|
|
406f5d4e14 | ||
|
|
3178c8722e | ||
|
|
b7cb6dc444 | ||
|
|
596c4ad68d | ||
|
|
e7f4611a00 | ||
|
|
8492c63f62 | ||
|
|
73c68fd11c | ||
|
|
8c986bb97d | ||
|
|
7a76c67038 | ||
|
|
b45686eb3b | ||
|
|
deb9c6651f | ||
|
|
440ad516df | ||
|
|
929469d0c0 | ||
|
|
108d368d45 | ||
|
|
bd413cfc55 | ||
|
|
1af13b4e94 | ||
|
|
4bad047f93 | ||
|
|
28a7f24989 | ||
|
|
28855d02a6 | ||
|
|
650ec04417 | ||
|
|
9ef11bca99 | ||
|
|
88b34a7ba3 | ||
|
|
9d86384032 | ||
|
|
a6bc87bcea | ||
|
|
63add047b6 | ||
|
|
a0e88bb24f | ||
|
|
5b14e0e9b5 | ||
|
|
274e734135 | ||
|
|
3818424251 | ||
|
|
2222550669 | ||
|
|
93b9f1d1da | ||
|
|
2abc2b13f0 | ||
|
|
49db31426c | ||
|
|
ce31d30ed7 | ||
|
|
68831c759f | ||
|
|
5ab867f21e | ||
|
|
4126188df1 | ||
|
|
26050e2134 | ||
|
|
5c0d6e6012 | ||
|
|
de7b13bc15 | ||
|
|
b6bd7ccf67 | ||
|
|
822d0c7738 | ||
|
|
183dd63021 | ||
|
|
0afc2ab2e6 | ||
|
|
119755e23a | ||
|
|
41fdf48ad3 | ||
|
|
ebdbb39f24 | ||
|
|
976740dc8b | ||
|
|
cac87b14c7 | ||
|
|
35856ad01e | ||
|
|
766fd40b72 | ||
|
|
3e6884b506 | ||
|
|
2118142faa | ||
|
|
2b270ccdb7 | ||
|
|
08eabf8102 | ||
|
|
c1d85bc216 | ||
|
|
3a2c213cf8 | ||
|
|
99dc0deb08 | ||
|
|
df5ed5190a | ||
|
|
30dac3a73c | ||
|
|
ba4c9745a2 | ||
|
|
a1c275479f | ||
|
|
708cbdc9e5 | ||
|
|
b0585e28ad | ||
|
|
575aa447e0 | ||
|
|
e57b88a019 | ||
|
|
380c1444ca | ||
|
|
a59135430c | ||
|
|
bdf2c97942 | ||
|
|
177c90c8e9 | ||
|
|
933451b1ec | ||
|
|
effeffd906 | ||
|
|
c269d32247 | ||
|
|
17b081b18b | ||
|
|
7fc0cadb81 |
@@ -70,9 +70,15 @@ jobs:
|
|||||||
- run:
|
- run:
|
||||||
name: Test - Hard Breaks
|
name: Test - Hard Breaks
|
||||||
command: npm run test:hard-breaks
|
command: npm run test:hard-breaks
|
||||||
|
- run:
|
||||||
|
name: Test - Non-Breaking Spaces
|
||||||
|
command: npm run test:non-breaking-spaces
|
||||||
- run:
|
- run:
|
||||||
name: Test - Variables
|
name: Test - Variables
|
||||||
command: npm run test:variables
|
command: npm run test:variables
|
||||||
|
- run:
|
||||||
|
name: Test - Emojis
|
||||||
|
command: npm run test:emojis
|
||||||
- run:
|
- run:
|
||||||
name: Test - Routes
|
name: Test - Routes
|
||||||
command: npm run test:route
|
command: npm run test:route
|
||||||
@@ -82,6 +88,9 @@ jobs:
|
|||||||
- run:
|
- run:
|
||||||
name: Test - Coverage
|
name: Test - Coverage
|
||||||
command: npm run test:coverage
|
command: npm run test:coverage
|
||||||
|
- run:
|
||||||
|
name: Test - Content Negotiation
|
||||||
|
command: npm run test:content-negotiation
|
||||||
|
|
||||||
workflows:
|
workflows:
|
||||||
build_and_test:
|
build_and_test:
|
||||||
|
|||||||
25
.github/pull_request_template.md
vendored
25
.github/pull_request_template.md
vendored
@@ -1,26 +1,29 @@
|
|||||||
<!--
|
> [!TIP]
|
||||||
Before submitting a Pull Request, please consider the following to speed up reviews:
|
> Before submitting a Pull Request, please consider the following to speed up reviews:
|
||||||
- 👷♀️ Create small PRs. Large PRs can usually be broken down into incremental PRs.
|
> - 👷♀️ Create small PRs. Large PRs can usually be broken down into incremental PRs.
|
||||||
- 🚩 Do you already have several open PRs? Consider finishing or asking for help with existing PRs first.
|
> - 🚩 Do you already have several open PRs? Consider finishing or asking for help with existing PRs first.
|
||||||
- 🔧 Does your PR reference a discussed and approved issue, especially for personal or edge-case requests?
|
> - 🔧 Does your PR reference a discussed and approved issue, especially for personal or edge-case requests?
|
||||||
- 💡 Is the solution agreed upon? Save rework time by discussing strategy before coding.
|
> - 💡 Is the solution agreed upon? Save rework time by discussing strategy before coding.
|
||||||
-->
|
|
||||||
|
|
||||||
## Description
|
## Description
|
||||||
|
|
||||||
|
_Describe what your PR accomplishes. Consider walking through the main changes to aid reviewers in following your code, especially if it covers multiple files._
|
||||||
|
|
||||||
## Related Issues or Discussions
|
## Related Issues or Discussions
|
||||||
|
|
||||||
|
> [!CAUTION]
|
||||||
|
> If no issue exists yet, create it, and get agreement on the approach (or paste in a previous agreement from chat, etc.) before moving forward. (Experimental PRs are OK without prior discussion, but do not expect to get merged.)
|
||||||
|
|
||||||
- Closes #
|
- Closes #
|
||||||
|
|
||||||
## QA Instructions, Screenshots, Recordings
|
## QA Instructions, Screenshots, Recordings
|
||||||
|
|
||||||
_Please replace this line with instructions on how to test or view your changes, as well as any before/after
|
_Replace this line with instructions on how to test or view your changes, as well as any before/after
|
||||||
images for UI changes._
|
screenshots or recordings for UI changes._
|
||||||
|
|
||||||
### Reviewer Checklist
|
### Reviewer Checklist
|
||||||
|
|
||||||
_Please replace the list below with specific features you want reviewers to look at._
|
_Replace the list below with specific features you want reviewers to look at._
|
||||||
|
|
||||||
*Reviewers, refer to this list when testing features, or suggest new items *
|
*Reviewers, refer to this list when testing features, or suggest new items *
|
||||||
- [ ] Verify new features are functional
|
- [ ] Verify new features are functional
|
||||||
@@ -32,5 +35,3 @@ _Please replace the list below with specific features you want reviewers to look
|
|||||||
- [ ] Feature A handles negative numbers
|
- [ ] Feature A handles negative numbers
|
||||||
- [ ] Identify opportunities for simplification and refactoring
|
- [ ] Identify opportunities for simplification and refactoring
|
||||||
- [ ] Check for code legibility and appropriate comments
|
- [ ] Check for code legibility and appropriate comments
|
||||||
|
|
||||||
<details><summary>Copy this list</summary>
|
|
||||||
|
|||||||
@@ -43,6 +43,6 @@
|
|||||||
"@stylistic/media-feature-colon-space-before" : "always",
|
"@stylistic/media-feature-colon-space-before" : "always",
|
||||||
"@stylistic/media-feature-colon-space-after" : "always",
|
"@stylistic/media-feature-colon-space-after" : "always",
|
||||||
"naturalcrit/declaration-colon-align" : true,
|
"naturalcrit/declaration-colon-align" : true,
|
||||||
"naturalcrit/declaration-block-multi-line-min-declarations": 1
|
"naturalcrit/declaration-block-multi-line-min-declarations" : 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM node:20-alpine
|
FROM node:22-alpine
|
||||||
RUN apk --no-cache add git
|
RUN apk --no-cache add git
|
||||||
|
|
||||||
ENV NODE_ENV=docker
|
ENV NODE_ENV=docker
|
||||||
@@ -9,7 +9,10 @@ WORKDIR /usr/src/app
|
|||||||
# Copy package.json into the image, then run yarn install
|
# Copy package.json into the image, then run yarn install
|
||||||
# This improves caching so we don't have to download the dependencies every time the code changes
|
# This improves caching so we don't have to download the dependencies every time the code changes
|
||||||
COPY package.json ./
|
COPY package.json ./
|
||||||
|
COPY config/docker.json usr/src/app/config
|
||||||
# --ignore-scripts tells yarn not to run postbuild. We run it explicitly later
|
# --ignore-scripts tells yarn not to run postbuild. We run it explicitly later
|
||||||
|
RUN node --version
|
||||||
|
RUN npm --version
|
||||||
RUN npm install --ignore-scripts
|
RUN npm install --ignore-scripts
|
||||||
|
|
||||||
# Bundle app source and build application
|
# Bundle app source and build application
|
||||||
|
|||||||
123
README.DOCKER.md
123
README.DOCKER.md
@@ -1,12 +1,119 @@
|
|||||||
# Running Homebrewery via Docker
|
# Offline Install Instructions: Docker
|
||||||
|
|
||||||
The repo includes a Dockerfile and a docker-compose.yml file.
|
These instructions are for setting up a persistent instance of the Homebrewery application locally using Docker.
|
||||||
|
|
||||||
To run the application via docker-compose.yml:
|
If you intend to develop with Homebrewery, following the Homebrewery application section of this guide is not recommended. Using docker to deploy MongoDB locally for development is not a bad idea at all, however.
|
||||||
`docker-compose up -d`
|
|
||||||
|
|
||||||
To stop the application:
|
# Install Docker
|
||||||
`docker-compose down`
|
|
||||||
|
## Docker Desktop (MacOS/Windows)
|
||||||
|
|
||||||
|
Windows and Mac installs use Docker Desktop. Current install instructions are below.
|
||||||
|
|
||||||
|
* [Mac](https://docs.docker.com/desktop/mac/install/)
|
||||||
|
* [Windows](https://docs.docker.com/desktop/windows/install/)
|
||||||
|
|
||||||
|
You can set up the docker engine to start on boot via the Docker desktop UI.
|
||||||
|
|
||||||
|
## Docker Engine
|
||||||
|
|
||||||
|
Linux installs use Docker Engine. Docker provides installers and instructions for several of the most common distrubutions. If you do not see yours listed, it is very likely supported indirectly by your distribution.
|
||||||
|
|
||||||
|
* [Arch](https://docs.docker.com/desktop/setup/install/linux/archlinux/)
|
||||||
|
* [CentOS](https://docs.docker.com/engine/install/centos/)
|
||||||
|
* [Debian](https://docs.docker.com/engine/install/debian/)
|
||||||
|
* [Fedora](https://docs.docker.com/engine/install/fedora/)
|
||||||
|
* [RHEL](https://docs.docker.com/engine/install/rhel/)
|
||||||
|
* [Ubuntu](https://docs.docker.com/engine/install/ubuntu/)
|
||||||
|
|
||||||
|
### Post installation steps
|
||||||
|
[Manage Docker as a non-root user (highly recommended)](https://docs.docker.com/engine/install/linux-postinstall/#manage-docker-as-a-non-root-user)
|
||||||
|
[Enable Docker to start on boot (highly recommended)](https://docs.docker.com/engine/install/linux-postinstall/#configure-docker-to-start-on-boot)
|
||||||
|
|
||||||
|
# Build Homebrewery Image
|
||||||
|
|
||||||
|
Next we build the homebrewery docker image. Start by cloning the repository.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
git clone https://github.com/naturalcrit/homebrewery.git
|
||||||
|
cd homebrewery
|
||||||
|
```
|
||||||
|
|
||||||
|
Make an changes you need to `config/docker.json` then build the image. If it does not exist,the below as a template.
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"host" : "localhost:8000",
|
||||||
|
"naturalcrit_url" : "local.naturalcrit.com:8010",
|
||||||
|
"secret" : "secret",
|
||||||
|
"web_port" : 8000,
|
||||||
|
"enable_v3" : true,
|
||||||
|
"mongodb_uri": "mongodb://172.17.0.2/homebrewery",
|
||||||
|
"enable_themes" : true,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```shell
|
||||||
|
docker-compose build homebrewery
|
||||||
|
```
|
||||||
|
|
||||||
|
# Add Mongo container
|
||||||
|
|
||||||
|
Once docker is installed and running, it is time to set up the containers. First up, Mongo.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
docker run --name homebrewery-mongodb -d --restart unless-stopped -v mongodata:/data/db -p 27017:27017 mongo:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
Older CPUs may run into an issue with AVX support.
|
||||||
|
```
|
||||||
|
WARNING: MongoDB 5.0+ requires a CPU with AVX support, and your current system does not appear to have that!
|
||||||
|
see https://jira.mongodb.org/browse/SERVER-54407
|
||||||
|
see also https://www.mongodb.com/community/forums/t/mongodb-5-0-cpu-intel-g4650-compatibility/116610/2
|
||||||
|
see also https://github.com/docker-library/mongo/issues/485#issuecomment-891991814
|
||||||
|
```
|
||||||
|
If you see a message similar to this, try using the bitnami mongo instead.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
docker run --name homebrewery-mongodb -d --restart unless-stopped -v mongodata:/data/db -p 27017:27017 bitnami/mongo:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
If your distribution is running on an arm device such as a Raspberry Pi, you will need to run the arm-built MongoDB v4.4.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
docker run --name homebrewery-mongodb -d --restart unless-stopped -v mongodata:/data/db -p 27017:27017 arm64v8/mongo:4.4
|
||||||
|
```
|
||||||
|
|
||||||
|
## Run the Homebrewery Image
|
||||||
|
```shell
|
||||||
|
# Make sure you run this in the homebrewery directory
|
||||||
|
docker run --name homebrewery-app -d --restart unless-stopped -e NODE_ENV=docker -v $(pwd)/config/docker.json:/usr/src/app/config/docker.json -p 8000:8000 docker.io/library/homebrewery:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
## Updating the Image
|
||||||
|
|
||||||
|
When Homebrewery code updates, your docker container will not automatically follow the changes. To do so you will need to rebuild your homebrewery image.
|
||||||
|
|
||||||
|
First, return to your homebrewery clone (from Build Homebrewery Image above) or recreate the clone if you deleted your copy of the code.
|
||||||
|
|
||||||
|
First, delete the existing image.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
docker rm -f homebrewery-app
|
||||||
|
```
|
||||||
|
|
||||||
|
Next, update the clone's code to the latest version.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
cd homebrewery
|
||||||
|
git checkout master
|
||||||
|
git pull upstream master
|
||||||
|
```
|
||||||
|
|
||||||
|
Finally, rebuild and restart the homebrewery image.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
docker-compose build homebrewery
|
||||||
|
docker run --name homebrewery-app -d --restart unless-stopped -e NODE_ENV=docker -v $(pwd)/config/docker.json:/usr/src/app/config/docker.json -p 8000:8000 docker.io/library/homebrewery:latest
|
||||||
|
```
|
||||||
|
|
||||||
To stop the application and remove all data:
|
|
||||||
`docker-compose down -v`
|
|
||||||
|
|||||||
135
changelog.md
135
changelog.md
@@ -79,12 +79,145 @@ pre {
|
|||||||
.varSyntaxTable th:first-of-type {
|
.varSyntaxTable th:first-of-type {
|
||||||
width:6cm;
|
width:6cm;
|
||||||
}
|
}
|
||||||
```
|
|
||||||
|
|
||||||
|
.page .exampleTable td,th {
|
||||||
|
border:1px dashed #00000030;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## changelog
|
## changelog
|
||||||
For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery).
|
For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery).
|
||||||
|
|
||||||
|
### Monday 03/10/2025 - v3.18.0
|
||||||
|
|
||||||
|
{{taskList
|
||||||
|
##### dbolack
|
||||||
|
* [x] Add ability to paste in any Share ID/URL into a brew's {{openSans :fas_circle_info: **Properties** :fas_arrow_right: **THEMES**}} selection, as long as that brew has been tagged as `meta:theme`. You can now share your custom brew themes without needing to make a personal copy.
|
||||||
|
* [x] Begin migration of custom Markdown extensions into their own NPM packages, for easier adoption by other users or projects
|
||||||
|
* [x] Fix external HTML appearing in open codeblocks
|
||||||
|
|
||||||
|
Fixes issue [#3206](https://github.com/naturalcrit/homebrewery/issues/3206)
|
||||||
|
|
||||||
|
* [x] Fix tables not rendering when directly after text
|
||||||
|
|
||||||
|
|
||||||
|
##### G-Ambatte
|
||||||
|
* [x] Cleanup of "cover pages" in the {{openSans :fas_rectangle_list: **NAVIGATION**}} list
|
||||||
|
* [x] Fix autosave triggering when no changes are present
|
||||||
|
|
||||||
|
Fixes issue [#4051](https://github.com/naturalcrit/homebrewery/issues/4051)
|
||||||
|
|
||||||
|
* [x] Remove empty table rows resulting from rowspan
|
||||||
|
|
||||||
|
Fixes issue [#1729](https://github.com/naturalcrit/homebrewery/issues/1729)
|
||||||
|
|
||||||
|
##### 5e-Cleric
|
||||||
|
* [x] Style fixes for covers art and logos on A4 size pages
|
||||||
|
* [x] Fix crash when trying to open brews that don't exist
|
||||||
|
|
||||||
|
##### Calculuschild
|
||||||
|
* [x] `꞉꞉꞉꞉` now produces `<br>` instead of a `<div>`
|
||||||
|
* [x] Fix typos in tables freezing the editor
|
||||||
|
|
||||||
|
Fixes issue [#4059](https://github.com/naturalcrit/homebrewery/issues/4059)
|
||||||
|
|
||||||
|
|
||||||
|
##### MollyMaclachlan (New Contributor!)
|
||||||
|
* [x] Fixed typos in the Monster Stat Block snippet
|
||||||
|
|
||||||
|
Fixes issue [#4073](https://github.com/naturalcrit/homebrewery/issues/4073)
|
||||||
|
|
||||||
|
|
||||||
|
##### All
|
||||||
|
* [x] Update dependencies and scripts
|
||||||
|
* [x] Refactor components and backend tools
|
||||||
|
}}
|
||||||
|
|
||||||
|
\column
|
||||||
|
|
||||||
|
### Thursday 01/30/2025 - v3.17.0
|
||||||
|
|
||||||
|
{{taskList
|
||||||
|
##### 5e-Cleric
|
||||||
|
|
||||||
|
* [x] Update FAQ
|
||||||
|
|
||||||
|
* [x] Fix styling for Vault buttons and checkboxes
|
||||||
|
|
||||||
|
* [x] Improve navigation bar styling
|
||||||
|
|
||||||
|
* [x] Add feature to change username at https://www.naturalcrit.com/account
|
||||||
|
|
||||||
|
* [x] Fix Reddit link crash when title has non-latin chars
|
||||||
|
|
||||||
|
##### dbolack
|
||||||
|
|
||||||
|
* [x] Fix page shadows toolbar option
|
||||||
|
|
||||||
|
Fixes issue [#3919](https://github.com/naturalcrit/homebrewery/issues/3919)
|
||||||
|
|
||||||
|
* [x] Add `:>>>` syntax for horizontal :>>>>> spaces
|
||||||
|
|
||||||
|
* [x] Update Docker install instructions
|
||||||
|
|
||||||
|
Fixes issue [#1930](https://github.com/naturalcrit/homebrewery/issues/1930)
|
||||||
|
|
||||||
|
* [x] Allow styling pages via `\page{myStyles}` (with calculuschild)
|
||||||
|
|
||||||
|
Fixes issue [#3901](https://github.com/naturalcrit/homebrewery/issues/3901)
|
||||||
|
|
||||||
|
* [x] Update Ubuntu install instructions
|
||||||
|
|
||||||
|
Fixes issue [#1952](https://github.com/naturalcrit/homebrewery/issues/1952)
|
||||||
|
|
||||||
|
* [x] Add `:-:` `:-` `-:` syntax for paragraph alignment, similar to table column alignment; for example:
|
||||||
|
|
||||||
|
-: -: Right-aligned
|
||||||
|
|
||||||
|
:-: :-: Centered
|
||||||
|
|
||||||
|
* [x] Add `:-- 50% --:` syntax to allow setting table column widths by percentage; for example:
|
||||||
|
```
|
||||||
|
| Narrow | Wide |
|
||||||
|
|:- 10% -:|:-90%--:|
|
||||||
|
| Cell | Cell |
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
| Narrow | Wide |
|
||||||
|
|:- 10% -:|:-90%--:|
|
||||||
|
|Cell | Cell |
|
||||||
|
{exampleTable}
|
||||||
|
|
||||||
|
##### G-Ambatte
|
||||||
|
|
||||||
|
* [x] Fix crash when opening brew Properties tab
|
||||||
|
|
||||||
|
Fixes issue [#3927](https://github.com/naturalcrit/homebrewery/issues/3927)
|
||||||
|
|
||||||
|
* [x] Update error pages with steps to refresh credentials
|
||||||
|
|
||||||
|
Fixes issue [#3955](https://github.com/naturalcrit/homebrewery/issues/3955)
|
||||||
|
|
||||||
|
* [x] Add {{openSans :fas_rectangle_list: **NAVIGATION**}} menu to the viewer toolbar
|
||||||
|
|
||||||
|
##### calculuschild
|
||||||
|
|
||||||
|
* [x] Reduce display lag on large brews
|
||||||
|
|
||||||
|
##### Gazook89
|
||||||
|
|
||||||
|
* [x] Smarter detection of current page number
|
||||||
|
|
||||||
|
Fixes issue [#3824](https://github.com/naturalcrit/homebrewery/issues/3824)
|
||||||
|
|
||||||
|
##### All
|
||||||
|
* [x] Update dependencies and scripts
|
||||||
|
* [x] Refactor components and fix various errors
|
||||||
|
}}
|
||||||
|
|
||||||
|
\page
|
||||||
|
|
||||||
### Wednesday 11/27/2024 - v3.16.1
|
### Wednesday 11/27/2024 - v3.16.1
|
||||||
|
|
||||||
{{taskList
|
{{taskList
|
||||||
|
|||||||
@@ -1,47 +1,48 @@
|
|||||||
require('./admin.less');
|
import './admin.less';
|
||||||
const React = require('react');
|
import React, { useEffect, useState } from 'react';
|
||||||
const createClass = require('create-react-class');
|
|
||||||
|
|
||||||
const BrewUtils = require('./brewUtils/brewUtils.jsx');
|
const BrewUtils = require('./brewUtils/brewUtils.jsx');
|
||||||
const NotificationUtils = require('./notificationUtils/notificationUtils.jsx');
|
const NotificationUtils = require('./notificationUtils/notificationUtils.jsx');
|
||||||
|
import AuthorUtils from './authorUtils/authorUtils.jsx';
|
||||||
|
|
||||||
const tabGroups = ['brew', 'notifications'];
|
const tabGroups = ['brew', 'notifications', 'authors'];
|
||||||
|
|
||||||
const Admin = createClass({
|
const Admin = ()=>{
|
||||||
getDefaultProps : function() {
|
const [currentTab, setCurrentTab] = useState('brew');
|
||||||
return {};
|
|
||||||
},
|
|
||||||
|
|
||||||
getInitialState : function(){
|
useEffect(()=>{
|
||||||
return ({
|
setCurrentTab(localStorage.getItem('hbAdminTab'));
|
||||||
currentTab : 'brew'
|
}, []);
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
handleClick : function(newTab){
|
useEffect(()=>{
|
||||||
if(this.state.currentTab === newTab) return;
|
localStorage.setItem('hbAdminTab', currentTab);
|
||||||
this.setState({
|
}, [currentTab]);
|
||||||
currentTab : newTab
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
render : function(){
|
return (
|
||||||
return <div className='admin'>
|
<div className='admin'>
|
||||||
<header>
|
<header>
|
||||||
<div className='container'>
|
<div className='container'>
|
||||||
<i className='fas fa-rocket' />
|
<i className='fas fa-rocket' />
|
||||||
homebrewery admin
|
The Homebrewery Admin Page
|
||||||
|
<a href='/'>back to homepage</a>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<main className='container'>
|
<main className='container'>
|
||||||
<nav className='tabs'>
|
<nav className='tabs'>
|
||||||
{tabGroups.map((tab, idx)=>{ return <button className={tab===this.state.currentTab ? 'active' : ''} key={idx} onClick={()=>{ return this.handleClick(tab); }}>{tab.toUpperCase()}</button>; })}
|
{tabGroups.map((tab, idx)=>(
|
||||||
|
<button
|
||||||
|
className={tab === currentTab ? 'active' : ''}
|
||||||
|
key={idx}
|
||||||
|
onClick={()=>setCurrentTab(tab)}>
|
||||||
|
{tab.toUpperCase()}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
</nav>
|
</nav>
|
||||||
{this.state.currentTab==='brew' && <BrewUtils />}
|
{currentTab === 'brew' && <BrewUtils />}
|
||||||
{this.state.currentTab==='notifications' && <NotificationUtils />}
|
{currentTab === 'notifications' && <NotificationUtils />}
|
||||||
|
{currentTab === 'authors' && <AuthorUtils />}
|
||||||
</main>
|
</main>
|
||||||
</div>;
|
</div>
|
||||||
}
|
);
|
||||||
});
|
};
|
||||||
|
|
||||||
module.exports = Admin;
|
module.exports = Admin;
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
:where(.admin) {
|
:where(.admin) {
|
||||||
|
padding-bottom : 50px;
|
||||||
header {
|
header {
|
||||||
padding : 20px 0px;
|
padding : 20px 0px;
|
||||||
margin-bottom : 30px;
|
margin-bottom : 30px;
|
||||||
@@ -30,6 +30,7 @@ body {
|
|||||||
color : white;
|
color : white;
|
||||||
background-color : @red;
|
background-color : @red;
|
||||||
i { margin-right : 30px; }
|
i { margin-right : 30px; }
|
||||||
|
a { float : right; }
|
||||||
}
|
}
|
||||||
|
|
||||||
hr { margin : 30px 0px; }
|
hr { margin : 30px 0px; }
|
||||||
@@ -48,19 +49,21 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dl {
|
dl {
|
||||||
@maxItemWidth : 132px;
|
display : grid;
|
||||||
|
grid-template-columns : 120px 1fr;
|
||||||
|
row-gap : 10px;
|
||||||
|
align-items : center;
|
||||||
|
justify-items : start;
|
||||||
|
padding-top : 0.5em;
|
||||||
dt {
|
dt {
|
||||||
float : left;
|
float : left;
|
||||||
width : @maxItemWidth;
|
|
||||||
clear : left;
|
clear : left;
|
||||||
|
height : fit-content;
|
||||||
|
font-weight : 900;
|
||||||
text-align : right;
|
text-align : right;
|
||||||
&::after { content : ' : '; }
|
&::after { content : ' : '; }
|
||||||
}
|
}
|
||||||
dd {
|
dd { height : fit-content; }
|
||||||
height : 1em;
|
|
||||||
padding : 0 0 0.5em 0;
|
|
||||||
margin-left : @maxItemWidth + 6px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabs button {
|
.tabs button {
|
||||||
@@ -90,11 +93,45 @@ body {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
padding : 10px;
|
||||||
|
|
||||||
|
tr {
|
||||||
|
border-bottom : 1px solid;
|
||||||
|
&:last-of-type { border : none; }
|
||||||
|
&:nth-child(even) { background : #DDDDDD; }
|
||||||
|
}
|
||||||
|
|
||||||
|
thead {
|
||||||
|
background : rgb(193,236,230);
|
||||||
|
border-bottom : 2px solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
th, td {
|
||||||
|
padding : 5px 10px;
|
||||||
|
vertical-align : middle;
|
||||||
|
text-align : center;
|
||||||
|
border-right : 1px solid;
|
||||||
|
|
||||||
|
&:last-child { border-right : none; }
|
||||||
|
}
|
||||||
|
|
||||||
|
th { font-weight : 900; }
|
||||||
|
|
||||||
|
td {
|
||||||
|
&:first-child {
|
||||||
|
font-weight : 900;
|
||||||
|
text-align : left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.error {
|
.error {
|
||||||
background: rgb(178, 54, 54);
|
float : right;
|
||||||
color:white;
|
padding : 10px;
|
||||||
font-weight: 900;
|
margin-block : 10px;
|
||||||
margin-block:10px;
|
font-weight : 900;
|
||||||
padding:10px;
|
color : white;
|
||||||
|
background : rgb(178, 54, 54);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
87
client/admin/authorUtils/authorLookup/authorLookup.jsx
Normal file
87
client/admin/authorUtils/authorLookup/authorLookup.jsx
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import './authorLookup.less';
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import request from 'superagent';
|
||||||
|
|
||||||
|
const authorLookup = ()=>{
|
||||||
|
const [author, setAuthor] = React.useState('');
|
||||||
|
const [searching, setSearching] = React.useState(false);
|
||||||
|
const [results, setResults] = React.useState([]);
|
||||||
|
|
||||||
|
const lookup = async ()=>{
|
||||||
|
if(!author) return;
|
||||||
|
|
||||||
|
setSearching(true);
|
||||||
|
setResults([]);
|
||||||
|
|
||||||
|
const brews = await request.get(`/admin/user/list/${author}`);
|
||||||
|
setResults(brews.body);
|
||||||
|
setSearching(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderResults = ()=>{
|
||||||
|
if(results.length == 0) return <>
|
||||||
|
<h2>Results</h2>
|
||||||
|
<p>None found.</p>
|
||||||
|
</>;
|
||||||
|
|
||||||
|
return <>
|
||||||
|
<h2>{`Results - ${results.length} brews` }</h2>
|
||||||
|
<table className='resultsTable'>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Title</th>
|
||||||
|
<th>Share</th>
|
||||||
|
<th>Edit</th>
|
||||||
|
<th>Last Update</th>
|
||||||
|
<th>Storage</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{results
|
||||||
|
.sort((a, b)=>{ // Sort brews from most recently updated
|
||||||
|
if(a.updatedAt > b.updatedAt) return -1;
|
||||||
|
return 1;
|
||||||
|
})
|
||||||
|
.map((brew, idx)=>{
|
||||||
|
return <tr key={idx}>
|
||||||
|
<td><strong>{brew.title}</strong></td>
|
||||||
|
<td><a href={`/share/${brew.shareId}`}>{brew.shareId}</a></td>
|
||||||
|
<td>{brew.editId}</td>
|
||||||
|
<td style={{ width: '200px' }}>{brew.updatedAt}</td>
|
||||||
|
<td>{brew.googleId ? 'Google' : 'Homebrewery'}</td>
|
||||||
|
</tr>;
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyPress = (evt)=>{
|
||||||
|
if(evt.key === 'Enter') return lookup();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChange = (evt)=>{
|
||||||
|
setAuthor(evt.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='authorLookup'>
|
||||||
|
<div className='authorLookupInputs'>
|
||||||
|
<h2>Author Lookup</h2>
|
||||||
|
<label className='field'>
|
||||||
|
Author Name:
|
||||||
|
<input className='fieldInput' value={author} onKeyDown={handleKeyPress} onChange={handleChange} />
|
||||||
|
<button onClick={lookup}>
|
||||||
|
<i className={`fas ${searching ? 'fa-spin fa-spinner' : 'fa-search'}`} />
|
||||||
|
</button>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className='authorLookupResults'>
|
||||||
|
{renderResults()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = authorLookup;
|
||||||
29
client/admin/authorUtils/authorLookup/authorLookup.less
Normal file
29
client/admin/authorUtils/authorLookup/authorLookup.less
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
.authorLookup {
|
||||||
|
position : relative;
|
||||||
|
display : flex;
|
||||||
|
flex-direction : column;
|
||||||
|
|
||||||
|
.field {
|
||||||
|
display : flex;
|
||||||
|
gap : 5px;
|
||||||
|
align-items : center;
|
||||||
|
justify-items : stretch;
|
||||||
|
width : 100%;
|
||||||
|
margin-bottom : 20px;
|
||||||
|
|
||||||
|
|
||||||
|
input {
|
||||||
|
height : 33px;
|
||||||
|
padding : 0px 10px;
|
||||||
|
margin-bottom : unset;
|
||||||
|
font-family : monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
width: 50px;
|
||||||
|
|
||||||
|
i { margin-right : 10px; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
13
client/admin/authorUtils/authorUtils.jsx
Normal file
13
client/admin/authorUtils/authorUtils.jsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import AuthorLookup from './authorLookup/authorLookup.jsx';
|
||||||
|
|
||||||
|
const authorUtils = ()=>{
|
||||||
|
return (
|
||||||
|
<section className='authorUtils'>
|
||||||
|
<AuthorLookup />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = authorUtils;
|
||||||
@@ -1,10 +1,8 @@
|
|||||||
require('./brewCleanup.less');
|
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const createClass = require('create-react-class');
|
const createClass = require('create-react-class');
|
||||||
|
|
||||||
const request = require('superagent');
|
const request = require('superagent');
|
||||||
|
|
||||||
|
|
||||||
const BrewCleanup = createClass({
|
const BrewCleanup = createClass({
|
||||||
displayName : 'BrewCleanup',
|
displayName : 'BrewCleanup',
|
||||||
getDefaultProps(){
|
getDefaultProps(){
|
||||||
@@ -39,9 +37,9 @@ const BrewCleanup = createClass({
|
|||||||
if(!this.state.primed) return;
|
if(!this.state.primed) return;
|
||||||
|
|
||||||
if(!this.state.count){
|
if(!this.state.count){
|
||||||
return <div className='removeBox'>No Matching Brews found.</div>;
|
return <div className='result noBrews'>No Matching Brews found.</div>;
|
||||||
}
|
}
|
||||||
return <div className='removeBox'>
|
return <div className='result'>
|
||||||
<button onClick={this.cleanup} className='remove'>
|
<button onClick={this.cleanup} className='remove'>
|
||||||
{this.state.pending
|
{this.state.pending
|
||||||
? <i className='fas fa-spin fa-spinner' />
|
? <i className='fas fa-spin fa-spinner' />
|
||||||
@@ -52,7 +50,7 @@ const BrewCleanup = createClass({
|
|||||||
</div>;
|
</div>;
|
||||||
},
|
},
|
||||||
render(){
|
render(){
|
||||||
return <div className='BrewCleanup'>
|
return <div className='brewUtil brewCleanup'>
|
||||||
<h2> Brew Cleanup </h2>
|
<h2> Brew Cleanup </h2>
|
||||||
<p>Removes very short brews to tidy up the database</p>
|
<p>Removes very short brews to tidy up the database</p>
|
||||||
|
|
||||||
@@ -65,7 +63,7 @@ const BrewCleanup = createClass({
|
|||||||
{this.renderPrimed()}
|
{this.renderPrimed()}
|
||||||
|
|
||||||
{this.state.error
|
{this.state.error
|
||||||
&& <div className='error'>{this.state.error.toString()}</div>
|
&& <div className='error noBrews'>{this.state.error.toString()}</div>
|
||||||
}
|
}
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
.BrewCleanup {
|
|
||||||
.removeBox {
|
|
||||||
margin-top : 20px;
|
|
||||||
button {
|
|
||||||
margin-right : 10px;
|
|
||||||
background-color : @red;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,7 @@
|
|||||||
require('./brewCompress.less');
|
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const createClass = require('create-react-class');
|
const createClass = require('create-react-class');
|
||||||
|
|
||||||
const request = require('superagent');
|
const request = require('superagent');
|
||||||
|
|
||||||
|
|
||||||
const BrewCompress = createClass({
|
const BrewCompress = createClass({
|
||||||
displayName : 'BrewCompress',
|
displayName : 'BrewCompress',
|
||||||
getDefaultProps(){
|
getDefaultProps(){
|
||||||
@@ -53,9 +50,9 @@ const BrewCompress = createClass({
|
|||||||
if(!this.state.primed) return;
|
if(!this.state.primed) return;
|
||||||
|
|
||||||
if(!this.state.count){
|
if(!this.state.count){
|
||||||
return <div className='removeBox'>No Matching Brews found.</div>;
|
return <div className='result noBrews'>No Matching Brews found.</div>;
|
||||||
}
|
}
|
||||||
return <div className='removeBox'>
|
return <div className='result'>
|
||||||
<button onClick={this.cleanup} className='remove'>
|
<button onClick={this.cleanup} className='remove'>
|
||||||
{this.state.pending
|
{this.state.pending
|
||||||
? <i className='fas fa-spin fa-spinner' />
|
? <i className='fas fa-spin fa-spinner' />
|
||||||
@@ -69,7 +66,7 @@ const BrewCompress = createClass({
|
|||||||
</div>;
|
</div>;
|
||||||
},
|
},
|
||||||
render(){
|
render(){
|
||||||
return <div className='BrewCompress'>
|
return <div className='brewUtil brewCompress'>
|
||||||
<h2> Brew Compression </h2>
|
<h2> Brew Compression </h2>
|
||||||
<p>Compresses the text in brews to binary</p>
|
<p>Compresses the text in brews to binary</p>
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
.BrewCompress {
|
|
||||||
.removeBox {
|
|
||||||
margin-top : 20px;
|
|
||||||
button {
|
|
||||||
margin-right : 10px;
|
|
||||||
background-color : @red;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
require('./brewLookup.less');
|
|
||||||
|
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const createClass = require('create-react-class');
|
const createClass = require('create-react-class');
|
||||||
const cx = require('classnames');
|
const cx = require('classnames');
|
||||||
@@ -55,7 +53,7 @@ const BrewLookup = createClass({
|
|||||||
|
|
||||||
renderFoundBrew(){
|
renderFoundBrew(){
|
||||||
const brew = this.state.foundBrew;
|
const brew = this.state.foundBrew;
|
||||||
return <div className='foundBrew'>
|
return <div className='result'>
|
||||||
<dl>
|
<dl>
|
||||||
<dt>Title</dt>
|
<dt>Title</dt>
|
||||||
<dd>{brew.title}</dd>
|
<dd>{brew.title}</dd>
|
||||||
@@ -90,7 +88,7 @@ const BrewLookup = createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
render(){
|
render(){
|
||||||
return <div className='brewLookup'>
|
return <div className='brewUtil brewLookup'>
|
||||||
<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}>
|
||||||
@@ -106,7 +104,7 @@ const BrewLookup = createClass({
|
|||||||
|
|
||||||
{this.state.foundBrew
|
{this.state.foundBrew
|
||||||
? this.renderFoundBrew()
|
? this.renderFoundBrew()
|
||||||
: <div className='noBrew'>No brew found.</div>
|
: <div className='result noBrew'>No brew found.</div>
|
||||||
}
|
}
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
.brewLookup {
|
|
||||||
.cleanButton {
|
|
||||||
display : inline-block;
|
|
||||||
width : 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
const React = require('react');
|
const React = require('react');
|
||||||
const createClass = require('create-react-class');
|
const createClass = require('create-react-class');
|
||||||
|
require('./brewUtils.less');
|
||||||
|
|
||||||
const BrewCleanup = require('./brewCleanup/brewCleanup.jsx');
|
const BrewCleanup = require('./brewCleanup/brewCleanup.jsx');
|
||||||
const BrewLookup = require('./brewLookup/brewLookup.jsx');
|
const BrewLookup = require('./brewLookup/brewLookup.jsx');
|
||||||
|
|||||||
29
client/admin/brewUtils/brewUtils.less
Normal file
29
client/admin/brewUtils/brewUtils.less
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
.brewUtil {
|
||||||
|
.result {
|
||||||
|
margin-top : 20px;
|
||||||
|
button {
|
||||||
|
margin-right : 10px;
|
||||||
|
background-color : @red;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.cleanButton {
|
||||||
|
display : inline-block;
|
||||||
|
width : 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats {
|
||||||
|
position : relative;
|
||||||
|
|
||||||
|
.pending {
|
||||||
|
position : absolute;
|
||||||
|
top : 0.5em;
|
||||||
|
left : 100px;
|
||||||
|
width : 100%;
|
||||||
|
height : 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:has(.pending) { opacity : 0.5; }
|
||||||
|
|
||||||
|
dl { grid-template-columns : 200px 250px; }
|
||||||
|
}
|
||||||
@@ -1,11 +1,8 @@
|
|||||||
require('./stats.less');
|
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const createClass = require('create-react-class');
|
const createClass = require('create-react-class');
|
||||||
const cx = require('classnames');
|
|
||||||
|
|
||||||
const request = require('superagent');
|
const request = require('superagent');
|
||||||
|
|
||||||
|
|
||||||
const Stats = createClass({
|
const Stats = createClass({
|
||||||
displayName : 'Stats',
|
displayName : 'Stats',
|
||||||
getDefaultProps(){
|
getDefaultProps(){
|
||||||
@@ -14,7 +11,8 @@ const Stats = createClass({
|
|||||||
getInitialState(){
|
getInitialState(){
|
||||||
return {
|
return {
|
||||||
stats : {
|
stats : {
|
||||||
totalBrews : 0
|
totalBrews : 0,
|
||||||
|
totalPublishedBrews : 0
|
||||||
},
|
},
|
||||||
fetching : false
|
fetching : false
|
||||||
};
|
};
|
||||||
@@ -29,11 +27,13 @@ const Stats = createClass({
|
|||||||
.finally(()=>this.setState({ fetching: false }));
|
.finally(()=>this.setState({ fetching: false }));
|
||||||
},
|
},
|
||||||
render(){
|
render(){
|
||||||
return <div className='Stats'>
|
return <div className='brewUtil stats'>
|
||||||
<h2> Stats </h2>
|
<h2> Stats </h2>
|
||||||
<dl>
|
<dl>
|
||||||
<dt>Total Brew Count</dt>
|
<dt>Total Brew Count</dt>
|
||||||
<dd>{this.state.stats.totalBrews}</dd>
|
<dd>{this.state.stats.totalBrews}</dd>
|
||||||
|
<dt>Total Brews Published</dt>
|
||||||
|
<dd>{this.state.stats.totalPublishedBrews}</dd>
|
||||||
</dl>
|
</dl>
|
||||||
|
|
||||||
{this.state.fetching
|
{this.state.fetching
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
|
|
||||||
.Stats {
|
|
||||||
position : relative;
|
|
||||||
|
|
||||||
.pending {
|
|
||||||
position : absolute;
|
|
||||||
top : 0px;
|
|
||||||
left : 0px;
|
|
||||||
width : 100%;
|
|
||||||
height : 100%;
|
|
||||||
background-color : rgba(238,238,238, 0.5);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,18 +6,21 @@
|
|||||||
|
|
||||||
.field {
|
.field {
|
||||||
display : grid;
|
display : grid;
|
||||||
grid-template-columns : 120px 150px;
|
grid-template-columns : 120px 200px;
|
||||||
align-items : center;
|
align-items : center;
|
||||||
justify-items : stretch;
|
justify-items : stretch;
|
||||||
width : 100%;
|
width : 100%;
|
||||||
margin-bottom : 20px;
|
margin-bottom : 20px;
|
||||||
|
|
||||||
|
|
||||||
input {
|
input {
|
||||||
height : 33px;
|
height : 33px;
|
||||||
padding : 0px 10px;
|
padding : 0px 10px;
|
||||||
margin-bottom : unset;
|
margin-bottom : unset;
|
||||||
font-family : monospace;
|
font-family : monospace;
|
||||||
|
|
||||||
|
&[type="date"] {
|
||||||
|
width:14ch;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
textarea {
|
textarea {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
|
|
||||||
.notificationLookup {
|
.notificationLookup {
|
||||||
width : 450px;
|
width : 450px;
|
||||||
height : fit-content;
|
height : fit-content;
|
||||||
|
|
||||||
|
.noNotification { margin-block : 20px; }
|
||||||
.notificationList {
|
.notificationList {
|
||||||
display : flex;
|
display : flex;
|
||||||
flex-direction : column;
|
flex-direction : column;
|
||||||
@@ -30,11 +30,6 @@
|
|||||||
font-size : 20px;
|
font-size : 20px;
|
||||||
font-weight : 900;
|
font-weight : 900;
|
||||||
}
|
}
|
||||||
|
|
||||||
dl dt{
|
|
||||||
font-weight: 900;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
.noNotification { margin-block : 20px; }
|
|
||||||
}
|
}
|
||||||
@@ -7,6 +7,11 @@ import './Anchored.less';
|
|||||||
// **The Anchor Positioning API is not available in Firefox yet**
|
// **The Anchor Positioning API is not available in Firefox yet**
|
||||||
// So in Firefox the positioning isn't perfect but is likely sufficient, and FF team seems to be working on the API quickly.
|
// So in Firefox the positioning isn't perfect but is likely sufficient, and FF team seems to be working on the API quickly.
|
||||||
|
|
||||||
|
// When Anchor Positioning is added to Firefox, this can also be rewritten using the Popover API-- add the `popover` attribute
|
||||||
|
// to the container div, which will render the container in the *top level* and give it better interactions like
|
||||||
|
// click outside to dismiss. **Do not** add without Anchor, though, because positioning is very limited with the `popover`
|
||||||
|
// attribute.
|
||||||
|
|
||||||
|
|
||||||
const Anchored = ({ children })=>{
|
const Anchored = ({ children })=>{
|
||||||
const [visible, setVisible] = useState(false);
|
const [visible, setVisible] = useState(false);
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ const Combobox = createClass({
|
|||||||
},
|
},
|
||||||
handleDropdown : function(show){
|
handleDropdown : function(show){
|
||||||
this.setState({
|
this.setState({
|
||||||
|
value : show ? '' : this.props.default,
|
||||||
showDropdown : show,
|
showDropdown : show,
|
||||||
inputFocused : this.props.autoSuggest.clearAutoSuggestOnClick ? show : false
|
inputFocused : this.props.autoSuggest.clearAutoSuggestOnClick ? show : false
|
||||||
});
|
});
|
||||||
@@ -58,10 +59,10 @@ const Combobox = createClass({
|
|||||||
this.props.onEntry(e);
|
this.props.onEntry(e);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
handleSelect : function(e){
|
handleSelect : function(value, data=value){
|
||||||
this.setState({
|
this.setState({
|
||||||
value : e.currentTarget.getAttribute('data-value')
|
value : value
|
||||||
}, ()=>{this.props.onSelect(this.state.value);});
|
}, ()=>{this.props.onSelect(data);});
|
||||||
;
|
;
|
||||||
},
|
},
|
||||||
renderTextInput : function(){
|
renderTextInput : function(){
|
||||||
@@ -78,10 +79,11 @@ const Combobox = createClass({
|
|||||||
if(!e.target.checkValidity()){
|
if(!e.target.checkValidity()){
|
||||||
this.setState({
|
this.setState({
|
||||||
value : this.props.default
|
value : this.props.default
|
||||||
}, ()=>this.props.onEntry(e));
|
});
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<i className='fas fa-caret-down'/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -92,11 +94,10 @@ const Combobox = createClass({
|
|||||||
const filterOn = _.isString(this.props.autoSuggest.filterOn) ? [this.props.autoSuggest.filterOn] : this.props.autoSuggest.filterOn;
|
const filterOn = _.isString(this.props.autoSuggest.filterOn) ? [this.props.autoSuggest.filterOn] : this.props.autoSuggest.filterOn;
|
||||||
const filteredArrays = filterOn.map((attr)=>{
|
const filteredArrays = filterOn.map((attr)=>{
|
||||||
const children = dropdownChildren.filter((item)=>{
|
const children = dropdownChildren.filter((item)=>{
|
||||||
if(suggestMethod === 'includes'){
|
if(suggestMethod === 'includes')
|
||||||
return item.props[attr]?.toLowerCase().includes(this.state.value.toLowerCase());
|
return item.props[attr]?.toLowerCase().includes(this.state.value.toLowerCase());
|
||||||
} else if(suggestMethod === 'startsWith'){
|
if(suggestMethod === 'startsWith')
|
||||||
return item.props[attr]?.toLowerCase().startsWith(this.state.value.toLowerCase());
|
return item.props[attr]?.toLowerCase().startsWith(this.state.value.toLowerCase());
|
||||||
}
|
|
||||||
});
|
});
|
||||||
return children;
|
return children;
|
||||||
});
|
});
|
||||||
@@ -111,7 +112,7 @@ const Combobox = createClass({
|
|||||||
},
|
},
|
||||||
render : function () {
|
render : function () {
|
||||||
const dropdownChildren = this.state.options.map((child, i)=>{
|
const dropdownChildren = this.state.options.map((child, i)=>{
|
||||||
const clone = React.cloneElement(child, { onClick: (e)=>this.handleSelect(e) });
|
const clone = React.cloneElement(child, { onClick: ()=>this.handleSelect(child.props.value, child.props.data) });
|
||||||
return clone;
|
return clone;
|
||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,50 +1,46 @@
|
|||||||
.dropdown-container {
|
.dropdown-container {
|
||||||
position:relative;
|
position : relative;
|
||||||
input {
|
input { width : 100%; }
|
||||||
width: 100%;
|
.item i {
|
||||||
|
position : absolute;
|
||||||
|
right : 10px;
|
||||||
|
color : black;
|
||||||
}
|
}
|
||||||
.dropdown-options {
|
.dropdown-options {
|
||||||
position:absolute;
|
position : absolute;
|
||||||
background-color: white;
|
z-index : 100;
|
||||||
z-index: 100;
|
width : 100%;
|
||||||
width: 100%;
|
max-height : 200px;
|
||||||
border: 1px solid gray;
|
overflow-y : auto;
|
||||||
overflow-y: auto;
|
background-color : white;
|
||||||
max-height: 200px;
|
border : 1px solid gray;
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar { width : 14px; }
|
||||||
width: 14px;
|
&::-webkit-scrollbar-track { background : #FFFFFF; }
|
||||||
}
|
|
||||||
&::-webkit-scrollbar-track {
|
|
||||||
background: #ffffff;
|
|
||||||
}
|
|
||||||
&::-webkit-scrollbar-thumb {
|
&::-webkit-scrollbar-thumb {
|
||||||
background-color: #949494;
|
background-color : #949494;
|
||||||
border-radius: 10px;
|
border : 3px solid #FFFFFF;
|
||||||
border: 3px solid #ffffff;
|
border-radius : 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.item {
|
.item {
|
||||||
position:relative;
|
position : relative;
|
||||||
font-size: 11px;
|
padding : 5px;
|
||||||
font-family: Open Sans;
|
margin : 0 3px;
|
||||||
padding: 5px;
|
font-family : "Open Sans";
|
||||||
cursor: default;
|
font-size : 11px;
|
||||||
margin: 0 3px;
|
cursor : default;
|
||||||
//border-bottom: 1px solid darkgray;
|
|
||||||
&:hover {
|
&:hover {
|
||||||
filter: brightness(120%);
|
background-color : rgb(163, 163, 163);
|
||||||
background-color: rgb(163, 163, 163);
|
filter : brightness(120%);
|
||||||
}
|
}
|
||||||
.detail {
|
.detail {
|
||||||
width:100%;
|
width : 100%;
|
||||||
text-align: left;
|
font-size : 9px;
|
||||||
color: rgb(124, 124, 124);
|
font-style : italic;
|
||||||
font-style:italic;
|
color : rgb(124, 124, 124);
|
||||||
font-size: 9px;
|
text-align : left;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,10 +6,8 @@ function Dialog({ dismisskeys = [], closeText = 'Close', blocking = false, ...re
|
|||||||
const dialogRef = useRef(null);
|
const dialogRef = useRef(null);
|
||||||
|
|
||||||
useEffect(()=>{
|
useEffect(()=>{
|
||||||
if(dismisskeys.length !== 0) {
|
|
||||||
blocking ? dialogRef.current?.showModal() : dialogRef.current?.show();
|
blocking ? dialogRef.current?.showModal() : dialogRef.current?.show();
|
||||||
}
|
}, []);
|
||||||
}, [dialogRef.current, dismisskeys]);
|
|
||||||
|
|
||||||
const dismiss = ()=>{
|
const dismiss = ()=>{
|
||||||
dismisskeys.forEach((key)=>{
|
dismisskeys.forEach((key)=>{
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/
|
/*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/
|
||||||
require('./brewRenderer.less');
|
require('./brewRenderer.less');
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const { useState, useRef, useCallback, useMemo } = React;
|
const { useState, useRef, useMemo, useEffect } = React;
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
|
|
||||||
const MarkdownLegacy = require('naturalcrit/markdownLegacy.js');
|
const MarkdownLegacy = require('naturalcrit/markdownLegacy.js');
|
||||||
@@ -16,8 +16,10 @@ const Frame = require('react-frame-component').default;
|
|||||||
const dedent = require('dedent-tabs').default;
|
const dedent = require('dedent-tabs').default;
|
||||||
const { printCurrentBrew } = require('../../../shared/helpers.js');
|
const { printCurrentBrew } = require('../../../shared/helpers.js');
|
||||||
|
|
||||||
|
import HeaderNav from './headerNav/headerNav.jsx';
|
||||||
import { safeHTML } from './safeHTML.js';
|
import { safeHTML } from './safeHTML.js';
|
||||||
|
|
||||||
|
const PAGEBREAK_REGEX_V3 = /^(?=\\page(?: *{[^\n{}]*})?$)/m;
|
||||||
const PAGE_HEIGHT = 1056;
|
const PAGE_HEIGHT = 1056;
|
||||||
|
|
||||||
const INITIAL_CONTENT = dedent`
|
const INITIAL_CONTENT = dedent`
|
||||||
@@ -36,8 +38,46 @@ const BrewPage = (props)=>{
|
|||||||
index : 0,
|
index : 0,
|
||||||
...props
|
...props
|
||||||
};
|
};
|
||||||
|
const pageRef = useRef(null);
|
||||||
const cleanText = safeHTML(props.contents);
|
const cleanText = safeHTML(props.contents);
|
||||||
return <div className={props.className} id={`p${props.index + 1}`} style={props.style}>
|
|
||||||
|
useEffect(()=>{
|
||||||
|
if(!pageRef.current) return;
|
||||||
|
|
||||||
|
// Observer for tracking pages within the `.pages` div
|
||||||
|
const visibleObserver = new IntersectionObserver(
|
||||||
|
(entries)=>{
|
||||||
|
entries.forEach((entry)=>{
|
||||||
|
if(entry.isIntersecting)
|
||||||
|
props.onVisibilityChange(props.index + 1, true, false); // add page to array of visible pages.
|
||||||
|
else
|
||||||
|
props.onVisibilityChange(props.index + 1, false, false);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{ threshold: .3, rootMargin: '0px 0px 0px 0px' } // detect when >30% of page is within bounds.
|
||||||
|
);
|
||||||
|
|
||||||
|
// Observer for tracking the page at the center of the iframe.
|
||||||
|
const centerObserver = new IntersectionObserver(
|
||||||
|
(entries)=>{
|
||||||
|
entries.forEach((entry)=>{
|
||||||
|
if(entry.isIntersecting)
|
||||||
|
props.onVisibilityChange(props.index + 1, true, true); // Set this page as the center page
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{ threshold: 0, rootMargin: '-50% 0px -50% 0px' } // Detect when the page is at the center
|
||||||
|
);
|
||||||
|
|
||||||
|
// attach observers to each `.page`
|
||||||
|
visibleObserver.observe(pageRef.current);
|
||||||
|
centerObserver.observe(pageRef.current);
|
||||||
|
return ()=>{
|
||||||
|
visibleObserver.disconnect();
|
||||||
|
centerObserver.disconnect();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return <div className={props.className} id={`p${props.index + 1}`} data-index={props.index} ref={pageRef} style={props.style} {...props.attributes}>
|
||||||
<div className='columnWrapper' dangerouslySetInnerHTML={{ __html: cleanText }} />
|
<div className='columnWrapper' dangerouslySetInnerHTML={{ __html: cleanText }} />
|
||||||
</div>;
|
</div>;
|
||||||
};
|
};
|
||||||
@@ -65,7 +105,9 @@ const BrewRenderer = (props)=>{
|
|||||||
|
|
||||||
const [state, setState] = useState({
|
const [state, setState] = useState({
|
||||||
isMounted : false,
|
isMounted : false,
|
||||||
visibility : 'hidden'
|
visibility : 'hidden',
|
||||||
|
visiblePages : [],
|
||||||
|
centerPage : 1
|
||||||
});
|
});
|
||||||
|
|
||||||
const [displayOptions, setDisplayOptions] = useState({
|
const [displayOptions, setDisplayOptions] = useState({
|
||||||
@@ -75,41 +117,33 @@ const BrewRenderer = (props)=>{
|
|||||||
pageShadows : true
|
pageShadows : true
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const [headerState, setHeaderState] = useState(false);
|
||||||
|
|
||||||
const mainRef = useRef(null);
|
const mainRef = useRef(null);
|
||||||
|
const pagesRef = useRef(null);
|
||||||
|
|
||||||
if(props.renderer == 'legacy') {
|
if(props.renderer == 'legacy') {
|
||||||
rawPages = props.text.split('\\page');
|
rawPages = props.text.split('\\page');
|
||||||
} else {
|
} else {
|
||||||
rawPages = props.text.split(/^\\page$/gm);
|
rawPages = props.text.split(PAGEBREAK_REGEX_V3);
|
||||||
}
|
}
|
||||||
|
|
||||||
const scrollToHash = (hash)=>{
|
const handlePageVisibilityChange = (pageNum, isVisible, isCenter)=>{
|
||||||
if(!hash) return;
|
setState((prevState)=>{
|
||||||
|
const updatedVisiblePages = new Set(prevState.visiblePages);
|
||||||
|
if(!isCenter)
|
||||||
|
isVisible ? updatedVisiblePages.add(pageNum) : updatedVisiblePages.delete(pageNum);
|
||||||
|
|
||||||
const iframeDoc = document.getElementById('BrewRenderer').contentDocument;
|
return {
|
||||||
let anchor = iframeDoc.querySelector(hash);
|
...prevState,
|
||||||
|
visiblePages : [...updatedVisiblePages].sort((a, b)=>a - b),
|
||||||
if(anchor) {
|
centerPage : isCenter ? pageNum : prevState.centerPage
|
||||||
anchor.scrollIntoView({ behavior: 'smooth' });
|
|
||||||
} else {
|
|
||||||
// Use MutationObserver to wait for the element if it's not immediately available
|
|
||||||
new MutationObserver((mutations, obs)=>{
|
|
||||||
anchor = iframeDoc.querySelector(hash);
|
|
||||||
if(anchor) {
|
|
||||||
anchor.scrollIntoView({ behavior: 'smooth' });
|
|
||||||
obs.disconnect();
|
|
||||||
}
|
|
||||||
}).observe(iframeDoc, { childList: true, subtree: true });
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
});
|
||||||
|
|
||||||
const updateCurrentPage = useCallback(_.throttle((e)=>{
|
if(isCenter)
|
||||||
const { scrollTop, clientHeight, scrollHeight } = e.target;
|
props.onPageChange(pageNum);
|
||||||
const totalScrollableHeight = scrollHeight - clientHeight;
|
};
|
||||||
const currentPageNumber = Math.max(Math.ceil((scrollTop / totalScrollableHeight) * rawPages.length), 1);
|
|
||||||
|
|
||||||
props.onPageChange(currentPageNumber);
|
|
||||||
}, 200), []);
|
|
||||||
|
|
||||||
const isInView = (index)=>{
|
const isInView = (index)=>{
|
||||||
if(!state.isMounted)
|
if(!state.isMounted)
|
||||||
@@ -137,19 +171,34 @@ const BrewRenderer = (props)=>{
|
|||||||
};
|
};
|
||||||
|
|
||||||
const renderPage = (pageText, index)=>{
|
const renderPage = (pageText, index)=>{
|
||||||
if(props.renderer == 'legacy') {
|
|
||||||
const html = MarkdownLegacy.render(pageText);
|
|
||||||
return <BrewPage className='page phb' index={index} key={index} contents={html} />;
|
|
||||||
} else {
|
|
||||||
pageText += `\n\n \n\\column\n `; //Artificial column break at page end to emulate column-fill:auto (until `wide` is used, when column-fill:balance will reappear)
|
|
||||||
const html = Markdown.render(pageText, index);
|
|
||||||
|
|
||||||
const styles = {
|
let styles = {
|
||||||
...(!displayOptions.pageShadows ? { boxShadow: 'none' } : {})
|
...(!displayOptions.pageShadows ? { boxShadow: 'none' } : {})
|
||||||
// Add more conditions as needed
|
// Add more conditions as needed
|
||||||
};
|
};
|
||||||
|
let classes = 'page';
|
||||||
|
let attributes = {};
|
||||||
|
|
||||||
return <BrewPage className='page' index={index} key={index} contents={html} style={styles} />;
|
if(props.renderer == 'legacy') {
|
||||||
|
const html = MarkdownLegacy.render(pageText);
|
||||||
|
|
||||||
|
return <BrewPage className='page phb' index={index} key={index} contents={html} style={styles} onVisibilityChange={handlePageVisibilityChange} />;
|
||||||
|
} else {
|
||||||
|
if(pageText.startsWith('\\page')) {
|
||||||
|
const firstLineTokens = Markdown.marked.lexer(pageText.split('\n', 1)[0])[0].tokens;
|
||||||
|
const injectedTags = firstLineTokens.find((obj)=>obj.injectedTags !== undefined)?.injectedTags;
|
||||||
|
if(injectedTags) {
|
||||||
|
styles = { ...styles, ...injectedTags.styles };
|
||||||
|
styles = _.mapKeys(styles, (v, k) => k.startsWith('--') ? k : _.camelCase(k)); // Convert CSS to camelCase for React
|
||||||
|
classes = [classes, injectedTags.classes].join(' ').trim();
|
||||||
|
attributes = injectedTags.attributes;
|
||||||
|
}
|
||||||
|
pageText = pageText.includes('\n') ? pageText.substring(pageText.indexOf('\n') + 1) : ''; // Remove the \page line
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = Markdown.render(pageText, index);
|
||||||
|
|
||||||
|
return <BrewPage className={classes} index={index} key={index} contents={html} style={styles} attributes={attributes} onVisibilityChange={handlePageVisibilityChange} />;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -182,6 +231,26 @@ const BrewRenderer = (props)=>{
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const scrollToHash = (hash)=>{
|
||||||
|
if(!hash) return;
|
||||||
|
|
||||||
|
const iframeDoc = document.getElementById('BrewRenderer').contentDocument;
|
||||||
|
let anchor = iframeDoc.querySelector(hash);
|
||||||
|
|
||||||
|
if(anchor) {
|
||||||
|
anchor.scrollIntoView({ behavior: 'smooth' });
|
||||||
|
} else {
|
||||||
|
// Use MutationObserver to wait for the element if it's not immediately available
|
||||||
|
new MutationObserver((mutations, obs)=>{
|
||||||
|
anchor = iframeDoc.querySelector(hash);
|
||||||
|
if(anchor) {
|
||||||
|
anchor.scrollIntoView({ behavior: 'smooth' });
|
||||||
|
obs.disconnect();
|
||||||
|
}
|
||||||
|
}).observe(iframeDoc, { childList: true, subtree: true });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const frameDidMount = ()=>{ //This triggers when iFrame finishes internal "componentDidMount"
|
const frameDidMount = ()=>{ //This triggers when iFrame finishes internal "componentDidMount"
|
||||||
scrollToHash(window.location.hash);
|
scrollToHash(window.location.hash);
|
||||||
|
|
||||||
@@ -217,13 +286,13 @@ const BrewRenderer = (props)=>{
|
|||||||
}
|
}
|
||||||
|
|
||||||
const renderedStyle = useMemo(()=>renderStyle(), [props.style, props.themeBundle]);
|
const renderedStyle = useMemo(()=>renderStyle(), [props.style, props.themeBundle]);
|
||||||
renderedPages = useMemo(()=>renderPages(), [displayOptions.pageShadows, props.text]);
|
renderedPages = useMemo(()=>renderPages(), [props.text, displayOptions]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/*render dummy page while iFrame is mounting.*/}
|
{/*render dummy page while iFrame is mounting.*/}
|
||||||
{!state.isMounted
|
{!state.isMounted
|
||||||
? <div className='brewRenderer' onScroll={updateCurrentPage}>
|
? <div className='brewRenderer'>
|
||||||
<div className='pages'>
|
<div className='pages'>
|
||||||
{renderDummyPage(1)}
|
{renderDummyPage(1)}
|
||||||
</div>
|
</div>
|
||||||
@@ -236,7 +305,7 @@ const BrewRenderer = (props)=>{
|
|||||||
<NotificationPopup />
|
<NotificationPopup />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ToolBar displayOptions={displayOptions} currentPage={props.currentBrewRendererPageNum} totalPages={rawPages.length} onDisplayOptionsChange={handleDisplayOptionsChange} />
|
<ToolBar displayOptions={displayOptions} onDisplayOptionsChange={handleDisplayOptionsChange} visiblePages={state.visiblePages.length > 0 ? state.visiblePages : [state.centerPage]} totalPages={rawPages.length} headerState={headerState} setHeaderState={setHeaderState}/>
|
||||||
|
|
||||||
{/*render in iFrame so broken code doesn't crash the site.*/}
|
{/*render in iFrame so broken code doesn't crash the site.*/}
|
||||||
<Frame id='BrewRenderer' initialContent={INITIAL_CONTENT}
|
<Frame id='BrewRenderer' initialContent={INITIAL_CONTENT}
|
||||||
@@ -245,23 +314,23 @@ const BrewRenderer = (props)=>{
|
|||||||
onClick={()=>{emitClick();}}
|
onClick={()=>{emitClick();}}
|
||||||
>
|
>
|
||||||
<div className={`brewRenderer ${global.config.deployment && 'deployment'}`}
|
<div className={`brewRenderer ${global.config.deployment && 'deployment'}`}
|
||||||
onScroll={updateCurrentPage}
|
|
||||||
onKeyDown={handleControlKeys}
|
onKeyDown={handleControlKeys}
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
style={ styleObject }>
|
style={ styleObject }
|
||||||
|
>
|
||||||
|
|
||||||
{/* Apply CSS from Style tab and render pages from Markdown tab */}
|
{/* Apply CSS from Style tab and render pages from Markdown tab */}
|
||||||
{state.isMounted
|
{state.isMounted
|
||||||
&&
|
&&
|
||||||
<>
|
<>
|
||||||
{renderedStyle}
|
{renderedStyle}
|
||||||
<div lang={`${props.lang || 'en'}`} style={pagesStyle} className={
|
<div className={`pages ${displayOptions.startOnRight ? 'recto' : 'verso'} ${displayOptions.spread}`} lang={`${props.lang || 'en'}`} style={pagesStyle} ref={pagesRef}>
|
||||||
`pages ${displayOptions.startOnRight ? 'recto' : 'verso'} ${displayOptions.spread}` } >
|
|
||||||
{renderedPages}
|
{renderedPages}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
{headerState ? <HeaderNav ref={pagesRef} /> : <></>}
|
||||||
</Frame>
|
</Frame>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
grid-template-columns: repeat(2, auto);
|
grid-template-columns: repeat(2, auto);
|
||||||
grid-template-rows: repeat(3, auto);
|
grid-template-rows: repeat(3, auto);
|
||||||
gap: 10px 10px;
|
gap: 10px 10px;
|
||||||
justify-content: center;
|
justify-content: safe center;
|
||||||
&.recto .page:first-child {
|
&.recto .page:first-child {
|
||||||
// sets first page on 'right' ('recto') of the preview, as if for a Cover page.
|
// sets first page on 'right' ('recto') of the preview, as if for a Cover page.
|
||||||
// todo: add a checkbox to toggle this setting
|
// todo: add a checkbox to toggle this setting
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
justify-content: flex-start;
|
justify-content: safe center;
|
||||||
& :where(.page) {
|
& :where(.page) {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
margin-left: unset !important;
|
margin-left: unset !important;
|
||||||
@@ -70,6 +70,7 @@
|
|||||||
|
|
||||||
.pane { position : relative; }
|
.pane { position : relative; }
|
||||||
|
|
||||||
|
|
||||||
@media print {
|
@media print {
|
||||||
.toolBar { display : none; }
|
.toolBar { display : none; }
|
||||||
.brewRenderer {
|
.brewRenderer {
|
||||||
@@ -82,4 +83,7 @@
|
|||||||
& > .page { box-shadow : unset; }
|
& > .page { box-shadow : unset; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.headerNav {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,75 +1,53 @@
|
|||||||
require('./errorBar.less');
|
require('./errorBar.less');
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const createClass = require('create-react-class');
|
|
||||||
const _ = require('lodash');
|
|
||||||
|
|
||||||
const ErrorBar = createClass({
|
import Dialog from '../../../components/dialog.jsx';
|
||||||
displayName : 'ErrorBar',
|
|
||||||
getDefaultProps : function() {
|
|
||||||
return {
|
|
||||||
errors : []
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
hasOpenError : false,
|
const DISMISS_BUTTON = <i className='fas fa-times dismiss' />;
|
||||||
hasCloseError : false,
|
|
||||||
hasMatchError : false,
|
|
||||||
|
|
||||||
renderErrors : function(){
|
const ErrorBar = (props)=>{
|
||||||
this.hasOpenError = false;
|
if(!props.errors.length) return null;
|
||||||
this.hasCloseError = false;
|
let hasOpenError = false, hasCloseError = false, hasMatchError = false;
|
||||||
this.hasMatchError = false;
|
|
||||||
|
|
||||||
|
props.errors.map((err)=>{
|
||||||
|
if(err.id === 'OPEN') hasOpenError = true;
|
||||||
|
if(err.id === 'CLOSE') hasCloseError = true;
|
||||||
|
if(err.id === 'MISMATCH') hasMatchError = true;
|
||||||
|
});
|
||||||
|
|
||||||
const errors = _.map(this.props.errors, (err, idx)=>{
|
const renderErrors = ()=>(
|
||||||
if(err.id == 'OPEN') this.hasOpenError = true;
|
<ul>
|
||||||
if(err.id == 'CLOSE') this.hasCloseError = true;
|
{props.errors.map((err, idx)=>{
|
||||||
if(err.id == 'MISMATCH') this.hasMatchError = true;
|
|
||||||
return <li key={idx}>
|
return <li key={idx}>
|
||||||
Line {err.line} : {err.text}, '{err.type}' tag
|
Line {err.line} : {err.text}, '{err.type}' tag
|
||||||
</li>;
|
</li>;
|
||||||
});
|
})}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
|
||||||
return <ul>{errors}</ul>;
|
const renderProtip = ()=>(
|
||||||
},
|
<div className='protips'>
|
||||||
|
|
||||||
renderProtip : function(){
|
|
||||||
const msg = [];
|
|
||||||
if(this.hasOpenError){
|
|
||||||
msg.push(<div>
|
|
||||||
An unmatched opening tag means there's an opened tag that isn't closed. You need to close your tags, like this {'</div>'}. Make sure to match types!
|
|
||||||
</div>);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(this.hasCloseError){
|
|
||||||
msg.push(<div>
|
|
||||||
An unmatched closing tag means you closed a tag without opening it. Either remove it, or check to where you think you opened it.
|
|
||||||
</div>);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(this.hasMatchError){
|
|
||||||
msg.push(<div>
|
|
||||||
A type mismatch means you closed a tag, but the last open tag was a different type.
|
|
||||||
</div>);
|
|
||||||
}
|
|
||||||
return <div className='protips'>
|
|
||||||
<h4>Protips!</h4>
|
<h4>Protips!</h4>
|
||||||
{msg}
|
{hasOpenError && <div>Unmatched opening tag. Close your tags, like this {'</div>'}. Match types!</div>}
|
||||||
</div>;
|
{hasCloseError && <div>Unmatched closing tag. Either remove it or check where it was opened.</div>}
|
||||||
},
|
{hasMatchError && <div>Type mismatch. Closed a tag with a different type.</div>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
render : function(){
|
return (
|
||||||
if(!this.props.errors.length) return null;
|
<Dialog className='errorBar' closeText={DISMISS_BUTTON} >
|
||||||
|
<div>
|
||||||
return <div className='errorBar'>
|
|
||||||
<i className='fas fa-exclamation-triangle' />
|
<i className='fas fa-exclamation-triangle' />
|
||||||
<h3> There are HTML errors in your markup</h3>
|
<h2> There are HTML errors in your markup</h2>
|
||||||
<small>If these aren't fixed your brew will not render properly when you print it to PDF or share it</small>
|
<small>
|
||||||
{this.renderErrors()}
|
If these aren't fixed your brew will not render properly when you print it to PDF or share it
|
||||||
|
</small>
|
||||||
|
{renderErrors()}
|
||||||
|
</div>
|
||||||
<hr />
|
<hr />
|
||||||
{this.renderProtip()}
|
{renderProtip()}
|
||||||
</div>;
|
</Dialog>
|
||||||
}
|
);
|
||||||
});
|
};
|
||||||
|
|
||||||
module.exports = ErrorBar;
|
module.exports = ErrorBar;
|
||||||
|
|||||||
@@ -1,60 +1,58 @@
|
|||||||
|
|
||||||
.errorBar{
|
.errorBar {
|
||||||
position : absolute;
|
position : absolute;
|
||||||
z-index : 10000;
|
top : 32px;
|
||||||
box-sizing : border-box;
|
z-index : 1;
|
||||||
width : 100%;
|
width : 100%;
|
||||||
margin-right : 13px;
|
|
||||||
padding : 20px;
|
|
||||||
padding-bottom : 10px;
|
|
||||||
padding-left : 100px;
|
|
||||||
background-color : @red;
|
|
||||||
color : white;
|
color : white;
|
||||||
i{
|
background-color : @red;
|
||||||
position : absolute;
|
border : unset;
|
||||||
left : 30px;
|
|
||||||
opacity : 0.8;
|
div {
|
||||||
|
> i {
|
||||||
|
float : left;
|
||||||
|
margin-right : 10px;
|
||||||
|
margin-bottom : 20px;
|
||||||
font-size : 3em;
|
font-size : 3em;
|
||||||
|
opacity : 0.8;
|
||||||
}
|
}
|
||||||
h3{
|
h2 { font-weight : 800; }
|
||||||
font-size : 1.1em;
|
ul {
|
||||||
font-weight : 800;
|
|
||||||
}
|
|
||||||
ul{
|
|
||||||
margin-top : 15px;
|
margin-top : 15px;
|
||||||
font-size : 0.8em;
|
font-size : 0.8em;
|
||||||
list-style-position : inside;
|
list-style-position : inside;
|
||||||
list-style-type : disc;
|
list-style-type : disc;
|
||||||
li{
|
li { line-height : 1.6em; }
|
||||||
line-height : 1.6em;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
hr{
|
hr {
|
||||||
box-sizing : border-box;
|
|
||||||
height : 2px;
|
height : 2px;
|
||||||
width : 150%;
|
|
||||||
margin-top : 25px;
|
margin-top : 25px;
|
||||||
margin-bottom : 15px;
|
margin-bottom : 15px;
|
||||||
margin-left : -100px;
|
|
||||||
background-color : darken(@red, 8%);
|
background-color : darken(@red, 8%);
|
||||||
border : none;
|
border : none;
|
||||||
}
|
}
|
||||||
small{
|
small {
|
||||||
font-size: 0.6em;
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
.protips{
|
|
||||||
margin-left : -80px;
|
|
||||||
font-size : 0.6em;
|
font-size : 0.6em;
|
||||||
&>div{
|
opacity : 0.7;
|
||||||
margin-bottom : 10px;
|
|
||||||
line-height : 1.2em;
|
|
||||||
}
|
}
|
||||||
h4{
|
.protips {
|
||||||
opacity : 0.8;
|
font-size : 0.6em;
|
||||||
|
line-height : 1.2em;
|
||||||
|
h4 {
|
||||||
font-weight : 800;
|
font-weight : 800;
|
||||||
line-height : 1.5em;
|
line-height : 1.5em;
|
||||||
text-transform : uppercase;
|
text-transform : uppercase;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
button.dismiss {
|
||||||
|
position : absolute;
|
||||||
|
top : 20px;
|
||||||
|
right : 30px;
|
||||||
|
padding : unset;
|
||||||
|
font-size : 40px;
|
||||||
|
background-color : transparent;
|
||||||
|
opacity : 0.6;
|
||||||
|
&:hover { opacity : 1; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
115
client/homebrew/brewRenderer/headerNav/headerNav.jsx
Normal file
115
client/homebrew/brewRenderer/headerNav/headerNav.jsx
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
require('./headerNav.less');
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
import * as _ from 'lodash';
|
||||||
|
|
||||||
|
const MAX_TEXT_LENGTH = 40;
|
||||||
|
|
||||||
|
const HeaderNav = React.forwardRef(({}, pagesRef)=>{
|
||||||
|
|
||||||
|
const renderHeaderLinks = ()=>{
|
||||||
|
if(!pagesRef.current) return;
|
||||||
|
|
||||||
|
// Top Level Pages
|
||||||
|
// Pages that contain an element with a specified class (e.g. cover pages, table of contents)
|
||||||
|
// will NOT have its content scanned for navigation headers, instead displaying a custom label
|
||||||
|
// ---
|
||||||
|
// The property name is class that will be used for detecting the page is a top level page
|
||||||
|
// The property value is a function that returns the text to be used
|
||||||
|
|
||||||
|
const topLevelPages = {
|
||||||
|
'.frontCover' : (el, pageType)=>{ const text = getHeaderContent(el); return text ? `Cover: ${text}` : 'Cover Page'; },
|
||||||
|
'.insideCover' : (el, pageType)=>{ const text = getHeaderContent(el); return text ? `Interior: ${text}` : 'Interior Cover Page'; },
|
||||||
|
'.partCover' : (el, pageType)=>{ const text = getHeaderContent(el); return text ? `Section: ${text}` : 'Section Cover Page'; },
|
||||||
|
'.backCover' : (el, pageType)=>{ const text = getHeaderContent(el); return text ? `Back: ${text}` : 'Rear Cover Page'; },
|
||||||
|
'.toc' : ()=>{ return 'Table of Contents'; },
|
||||||
|
};
|
||||||
|
|
||||||
|
const getHeaderContent = el => el.querySelector('h1')?.textContent;
|
||||||
|
|
||||||
|
const topLevelPageSelector = Object.keys(topLevelPages).join(',');
|
||||||
|
|
||||||
|
const selector = [
|
||||||
|
'.pages > .page', // All page elements, which by definition have IDs
|
||||||
|
`.page:not(:has(${topLevelPageSelector})) > [id]`, // All direct children of non-excluded .pages with an ID (Legacy)
|
||||||
|
`.page:not(:has(${topLevelPageSelector})) > .columnWrapper > [id]`, // All direct children of non-excluded .page > .columnWrapper with an ID (V3)
|
||||||
|
`.page:not(:has(${topLevelPageSelector})) h2`, // All non-excluded H2 titles, like Monster frame titles
|
||||||
|
];
|
||||||
|
const elements = pagesRef.current.querySelectorAll(selector.join(','));
|
||||||
|
if(!elements) return;
|
||||||
|
const navList = [];
|
||||||
|
|
||||||
|
// navList is a list of objects which have the following structure:
|
||||||
|
// {
|
||||||
|
// depth : how deeply indented the item should be
|
||||||
|
// text : the text to display in the nav link
|
||||||
|
// link : the hyperlink to navigate to when clicked
|
||||||
|
// className : [optional] the class to apply to the nav link for styling
|
||||||
|
// }
|
||||||
|
|
||||||
|
elements.forEach((el)=>{
|
||||||
|
const navEntry = { // Default structure of a navList entry
|
||||||
|
depth : 7, // All unmatched elements with IDs are set to the maximum depth (7)
|
||||||
|
text : el.textContent, // Use `textContent` because `innerText` is affected by rendering, e.g. 'content-visibility: auto'
|
||||||
|
link : el.id
|
||||||
|
}
|
||||||
|
if(el.classList.contains('page')) {
|
||||||
|
let text = `Page ${el.id.slice(1)}`; // Get the page # by trimming off the 'p' from the ID
|
||||||
|
const pageType = Object.keys(topLevelPages).find(pageType => el.querySelector(pageType));
|
||||||
|
if (pageType)
|
||||||
|
text += ` - ${topLevelPages[pageType](el, pageType)}` // If a Top Level Page, add extra label
|
||||||
|
|
||||||
|
navEntry.depth = 0; // Pages are always at the least indented level
|
||||||
|
navEntry.text = text;
|
||||||
|
navEntry.className = 'pageLink';
|
||||||
|
}
|
||||||
|
else if(el.localName.match(/^h[1-6]/)){ // Header elements H1 through H6
|
||||||
|
navEntry.depth = el.localName[1]; // Depth is set by the header level
|
||||||
|
}
|
||||||
|
navList.push(navEntry);
|
||||||
|
});
|
||||||
|
|
||||||
|
return _.map(navList, (navItem, index)=>
|
||||||
|
<HeaderNavItem {...navItem} key={index} />
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return <nav className='headerNav'>
|
||||||
|
<ul>
|
||||||
|
{renderHeaderLinks()}
|
||||||
|
</ul>
|
||||||
|
</nav>;
|
||||||
|
});
|
||||||
|
|
||||||
|
const HeaderNavItem = ({ link, text, depth, className })=>{
|
||||||
|
|
||||||
|
const trimString = (text, prefixLength = 0)=>{
|
||||||
|
// Sanity check nav link strings
|
||||||
|
let output = text;
|
||||||
|
|
||||||
|
// If the string has a line break, only use the first line
|
||||||
|
if(text.indexOf('\n')){
|
||||||
|
output = text.split('\n')[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trim unecessary spaces from string
|
||||||
|
output = output.trim();
|
||||||
|
|
||||||
|
// Reduce excessively long strings
|
||||||
|
const maxLength = MAX_TEXT_LENGTH - prefixLength;
|
||||||
|
if(output.length > maxLength){
|
||||||
|
return `${output.slice(0, maxLength).trim()}...`;
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
};
|
||||||
|
|
||||||
|
if(!link || !text) return;
|
||||||
|
|
||||||
|
return <li>
|
||||||
|
<a href={`#${link}`} target='_self' className={`depth-${depth} ${className ?? ''}`}>
|
||||||
|
{trimString(text, depth)}
|
||||||
|
</a>
|
||||||
|
</li>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HeaderNav;
|
||||||
47
client/homebrew/brewRenderer/headerNav/headerNav.less
Normal file
47
client/homebrew/brewRenderer/headerNav/headerNav.less
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
.headerNav {
|
||||||
|
position: fixed;
|
||||||
|
top: 32px;
|
||||||
|
left: 0px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
background-color: #ccc;
|
||||||
|
border-radius: 5px;
|
||||||
|
max-height: calc(100vh - 32px);
|
||||||
|
max-width: 40vw;
|
||||||
|
overflow-y: auto;
|
||||||
|
&.active {
|
||||||
|
padding-bottom: 10px;
|
||||||
|
.navIcon {
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navIcon {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
li {
|
||||||
|
list-style-type: none;
|
||||||
|
a {
|
||||||
|
display: inline-block;
|
||||||
|
width: 100%;
|
||||||
|
font-family: 'Open Sans';
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 2px;
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
&.pageLink {
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
@depths: 0,1,2,3,4,5,6,7;
|
||||||
|
|
||||||
|
each(@depths, {
|
||||||
|
&.depth-@{value} {
|
||||||
|
padding-left: ((@value) * 0.5em);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
require('./notificationPopup.less');
|
require('./notificationPopup.less');
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import request from '../../utils/request-middleware.js';
|
import request from '../../utils/request-middleware.js';
|
||||||
|
import Markdown from 'naturalcrit/markdown.js';
|
||||||
|
|
||||||
import Dialog from '../../../components/dialog.jsx';
|
import Dialog from '../../../components/dialog.jsx';
|
||||||
|
|
||||||
@@ -40,15 +41,15 @@ const NotificationPopup = ()=>{
|
|||||||
|
|
||||||
const renderNotificationsList = ()=>{
|
const renderNotificationsList = ()=>{
|
||||||
if(error) return <div className='error'>{error}</div>;
|
if(error) return <div className='error'>{error}</div>;
|
||||||
|
|
||||||
return notifications.map((notification)=>(
|
return notifications.map((notification)=>(
|
||||||
<li key={notification.dismissKey} >
|
<li key={notification.dismissKey} >
|
||||||
<em>{notification.title}</em><br />
|
<em>{notification.title}</em><br />
|
||||||
<p dangerouslySetInnerHTML={{ __html: notification.text }}></p>
|
<p dangerouslySetInnerHTML={{ __html: Markdown.render(notification.text) }}></p>
|
||||||
</li>
|
</li>
|
||||||
));
|
));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if(!notifications.length) return;
|
||||||
return <Dialog className='notificationPopup' dismisskeys={dissmissKeyList} closeText={DISMISS_BUTTON} >
|
return <Dialog className='notificationPopup' dismisskeys={dissmissKeyList} closeText={DISMISS_BUTTON} >
|
||||||
<div className='header'>
|
<div className='header'>
|
||||||
<i className='fas fa-info-circle info'></i>
|
<i className='fas fa-info-circle info'></i>
|
||||||
|
|||||||
@@ -48,17 +48,41 @@
|
|||||||
}
|
}
|
||||||
ul {
|
ul {
|
||||||
margin-top : 15px;
|
margin-top : 15px;
|
||||||
font-size : 0.8em;
|
font-size : 0.9em;
|
||||||
list-style-position : outside;
|
list-style-position : outside;
|
||||||
list-style-type : disc;
|
list-style-type : disc;
|
||||||
li {
|
li {
|
||||||
margin-top : 1.4em;
|
padding-left : 1em;
|
||||||
font-size : 0.8em;
|
margin-top : 1.5em;
|
||||||
line-height : 1.4em;
|
font-size : 0.9em;
|
||||||
|
line-height : 1.5em;
|
||||||
em {
|
em {
|
||||||
text-transform:capitalize;
|
|
||||||
font-weight : 800;
|
font-weight : 800;
|
||||||
|
text-transform : capitalize;
|
||||||
|
}
|
||||||
|
li {
|
||||||
|
margin-top : 0;
|
||||||
|
line-height : 1.2em;
|
||||||
|
list-style-type : square;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ul ul,ol ol,ul ol,ol ul {
|
||||||
|
margin-bottom : 0px;
|
||||||
|
margin-left : 1.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Markdown styling */
|
||||||
|
code {
|
||||||
|
padding : 0.1em 0.5em;
|
||||||
|
font-family : 'Courier New', 'Courier', monospace;
|
||||||
|
overflow-wrap : break-word;
|
||||||
|
white-space : pre-wrap;
|
||||||
|
background : #08115A;
|
||||||
|
border-radius : 2px;
|
||||||
|
}
|
||||||
|
pre code {
|
||||||
|
display : inline-block;
|
||||||
|
width : 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,29 +1,30 @@
|
|||||||
|
/* eslint-disable max-lines */
|
||||||
require('./toolBar.less');
|
require('./toolBar.less');
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const { useState, useEffect } = React;
|
const { useState, useEffect } = React;
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
|
|
||||||
import { Anchored, AnchoredBox, AnchoredTrigger } from '../../../components/Anchored.jsx';
|
import { Anchored, AnchoredBox, AnchoredTrigger } from '../../../components/Anchored.jsx';
|
||||||
// import * as ZoomIcons from '../../../icons/icon-components/zoomIcons.jsx';
|
|
||||||
|
|
||||||
const MAX_ZOOM = 300;
|
const MAX_ZOOM = 300;
|
||||||
const MIN_ZOOM = 10;
|
const MIN_ZOOM = 10;
|
||||||
|
|
||||||
const ToolBar = ({ displayOptions, currentPage, totalPages, onDisplayOptionsChange })=>{
|
const ToolBar = ({ displayOptions, onDisplayOptionsChange, visiblePages, totalPages, headerState, setHeaderState })=>{
|
||||||
|
|
||||||
const [pageNum, setPageNum] = useState(currentPage);
|
const [pageNum, setPageNum] = useState(1);
|
||||||
const [toolsVisible, setToolsVisible] = useState(true);
|
const [toolsVisible, setToolsVisible] = useState(true);
|
||||||
|
|
||||||
useEffect(()=>{
|
useEffect(()=>{
|
||||||
setPageNum(currentPage);
|
// format multiple visible pages as a range (e.g. "150-153")
|
||||||
}, [currentPage]);
|
const pageRange = visiblePages.length === 1 ? `${visiblePages[0]}` : `${visiblePages[0]} - ${visiblePages.at(-1)}`;
|
||||||
|
setPageNum(pageRange);
|
||||||
|
}, [visiblePages]);
|
||||||
|
|
||||||
const handleZoomButton = (zoom)=>{
|
const handleZoomButton = (zoom)=>{
|
||||||
handleOptionChange('zoomLevel', _.round(_.clamp(zoom, MIN_ZOOM, MAX_ZOOM)));
|
handleOptionChange('zoomLevel', _.round(_.clamp(zoom, MIN_ZOOM, MAX_ZOOM)));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOptionChange = (optionKey, newValue)=>{
|
const handleOptionChange = (optionKey, newValue)=>{
|
||||||
//setDisplayOptions(prevOptions => ({ ...prevOptions, [optionKey]: newValue }));
|
|
||||||
onDisplayOptionsChange({ ...displayOptions, [optionKey]: newValue });
|
onDisplayOptionsChange({ ...displayOptions, [optionKey]: newValue });
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -32,16 +33,16 @@ const ToolBar = ({ displayOptions, currentPage, totalPages, onDisplayOptionsChan
|
|||||||
setPageNum(parseInt(pageInput)); // input type is 'text', so `page` comes in as a string, not number.
|
setPageNum(parseInt(pageInput)); // input type is 'text', so `page` comes in as a string, not number.
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// scroll to a page, used in the Prev/Next Page buttons.
|
||||||
const scrollToPage = (pageNumber)=>{
|
const scrollToPage = (pageNumber)=>{
|
||||||
|
if(typeof pageNumber !== 'number') return;
|
||||||
pageNumber = _.clamp(pageNumber, 1, totalPages);
|
pageNumber = _.clamp(pageNumber, 1, totalPages);
|
||||||
const iframe = document.getElementById('BrewRenderer');
|
const iframe = document.getElementById('BrewRenderer');
|
||||||
const brewRenderer = iframe?.contentWindow?.document.querySelector('.brewRenderer');
|
const brewRenderer = iframe?.contentWindow?.document.querySelector('.brewRenderer');
|
||||||
const page = brewRenderer?.querySelector(`#p${pageNumber}`);
|
const page = brewRenderer?.querySelector(`#p${pageNumber}`);
|
||||||
page?.scrollIntoView({ block: 'start' });
|
page?.scrollIntoView({ block: 'start' });
|
||||||
setPageNum(pageNumber);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const calculateChange = (mode)=>{
|
const calculateChange = (mode)=>{
|
||||||
const iframe = document.getElementById('BrewRenderer');
|
const iframe = document.getElementById('BrewRenderer');
|
||||||
const iframeWidth = iframe.getBoundingClientRect().width;
|
const iframeWidth = iframe.getBoundingClientRect().width;
|
||||||
@@ -57,8 +58,12 @@ const ToolBar = ({ displayOptions, currentPage, totalPages, onDisplayOptionsChan
|
|||||||
desiredZoom = (iframeWidth / widestPage) * 100;
|
desiredZoom = (iframeWidth / widestPage) * 100;
|
||||||
|
|
||||||
} else if(mode == 'fit'){
|
} else if(mode == 'fit'){
|
||||||
|
let minDimRatio;
|
||||||
// find the page with the largest single dim (height or width) so that zoom can be adapted to fit it.
|
// find the page with the largest single dim (height or width) so that zoom can be adapted to fit it.
|
||||||
const minDimRatio = [...pages].reduce((minRatio, page)=>Math.min(minRatio, iframeWidth / page.offsetWidth, iframeHeight / page.offsetHeight), Infinity);
|
if(displayOptions.spread === 'facing')
|
||||||
|
minDimRatio = [...pages].reduce((minRatio, page)=>Math.min(minRatio, iframeWidth / page.offsetWidth / 2), Infinity); // if 'facing' spread, fit two pages in view
|
||||||
|
else
|
||||||
|
minDimRatio = [...pages].reduce((minRatio, page)=>Math.min(minRatio, iframeWidth / page.offsetWidth, iframeHeight / page.offsetHeight), Infinity);
|
||||||
|
|
||||||
desiredZoom = minDimRatio * 100;
|
desiredZoom = minDimRatio * 100;
|
||||||
}
|
}
|
||||||
@@ -71,7 +76,10 @@ const ToolBar = ({ displayOptions, currentPage, totalPages, onDisplayOptionsChan
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div id='preview-toolbar' className={`toolBar ${toolsVisible ? 'visible' : 'hidden'}`} role='toolbar'>
|
<div id='preview-toolbar' className={`toolBar ${toolsVisible ? 'visible' : 'hidden'}`} role='toolbar'>
|
||||||
<button className='toggleButton' title={`${toolsVisible ? 'Hide' : 'Show'} Preview Toolbar`} onClick={()=>{setToolsVisible(!toolsVisible);}}><i className='fas fa-glasses' /></button>
|
<div className='toggleButton'>
|
||||||
|
<button title={`${toolsVisible ? 'Hide' : 'Show'} Preview Toolbar`} onClick={()=>{setToolsVisible(!toolsVisible);}}><i className='fas fa-glasses' /></button>
|
||||||
|
<button title={`${headerState ? 'Hide' : 'Show'} Header Navigation`} onClick={()=>{setHeaderState(!headerState);}}><i className='fas fa-rectangle-list' /></button>
|
||||||
|
</div>
|
||||||
{/*v=====----------------------< Zoom Controls >---------------------=====v*/}
|
{/*v=====----------------------< Zoom Controls >---------------------=====v*/}
|
||||||
<div className='group' role='group' aria-label='Zoom' aria-hidden={!toolsVisible}>
|
<div className='group' role='group' aria-label='Zoom' aria-hidden={!toolsVisible}>
|
||||||
<button
|
<button
|
||||||
@@ -185,8 +193,8 @@ const ToolBar = ({ displayOptions, currentPage, totalPages, onDisplayOptionsChan
|
|||||||
className='previousPage tool'
|
className='previousPage tool'
|
||||||
type='button'
|
type='button'
|
||||||
title='Previous Page(s)'
|
title='Previous Page(s)'
|
||||||
onClick={()=>scrollToPage(pageNum - 1)}
|
onClick={()=>scrollToPage(_.min(visiblePages) - visiblePages.length)}
|
||||||
disabled={pageNum <= 1}
|
disabled={visiblePages.includes(1)}
|
||||||
>
|
>
|
||||||
<i className='fas fa-arrow-left'></i>
|
<i className='fas fa-arrow-left'></i>
|
||||||
</button>
|
</button>
|
||||||
@@ -205,6 +213,7 @@ const ToolBar = ({ displayOptions, currentPage, totalPages, onDisplayOptionsChan
|
|||||||
onChange={(e)=>handlePageInput(e.target.value)}
|
onChange={(e)=>handlePageInput(e.target.value)}
|
||||||
onBlur={()=>scrollToPage(pageNum)}
|
onBlur={()=>scrollToPage(pageNum)}
|
||||||
onKeyDown={(e)=>e.key == 'Enter' && scrollToPage(pageNum)}
|
onKeyDown={(e)=>e.key == 'Enter' && scrollToPage(pageNum)}
|
||||||
|
style={{ width: `${pageNum.length}ch` }}
|
||||||
/>
|
/>
|
||||||
<span id='page-count' title='Total Page Count'>/ {totalPages}</span>
|
<span id='page-count' title='Total Page Count'>/ {totalPages}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -214,8 +223,8 @@ const ToolBar = ({ displayOptions, currentPage, totalPages, onDisplayOptionsChan
|
|||||||
className='tool'
|
className='tool'
|
||||||
type='button'
|
type='button'
|
||||||
title='Next Page(s)'
|
title='Next Page(s)'
|
||||||
onClick={()=>scrollToPage(pageNum + 1)}
|
onClick={()=>scrollToPage(_.max(visiblePages) + 1)}
|
||||||
disabled={pageNum >= totalPages}
|
disabled={visiblePages.includes(totalPages)}
|
||||||
>
|
>
|
||||||
<i className='fas fa-arrow-right'></i>
|
<i className='fas fa-arrow-right'></i>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -104,9 +104,9 @@
|
|||||||
height : 1.5em;
|
height : 1.5em;
|
||||||
padding : 2px 5px;
|
padding : 2px 5px;
|
||||||
font-family : 'Open Sans', sans-serif;
|
font-family : 'Open Sans', sans-serif;
|
||||||
color : #000000;
|
color : inherit;
|
||||||
background : #EEEEEE;
|
background : #3B3B3B;
|
||||||
border : 1px solid gray;
|
border : none;
|
||||||
&:focus { outline : 1px solid #D3D3D3; }
|
&:focus { outline : 1px solid #D3D3D3; }
|
||||||
|
|
||||||
// `.range-input` if generic to all range inputs, or `#zoom-slider` if only for zoom slider
|
// `.range-input` if generic to all range inputs, or `#zoom-slider` if only for zoom slider
|
||||||
@@ -115,10 +115,10 @@
|
|||||||
color : #D3D3D3;
|
color : #D3D3D3;
|
||||||
accent-color : #D3D3D3;
|
accent-color : #D3D3D3;
|
||||||
|
|
||||||
&::-webkit-slider-thumb, &::-moz-slider-thumb {
|
&::-webkit-slider-thumb, &::-moz-range-thumb {
|
||||||
width : 5px;
|
width : 5px;
|
||||||
height : 5px;
|
height : 5px;
|
||||||
cursor : pointer;
|
cursor : ew-resize;
|
||||||
outline : none;
|
outline : none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,7 +141,7 @@
|
|||||||
|
|
||||||
// `.text-input` if generic to all range inputs, or `#page-input` if only for current page input
|
// `.text-input` if generic to all range inputs, or `#page-input` if only for current page input
|
||||||
&#page-input {
|
&#page-input {
|
||||||
width : 4ch;
|
min-width : 5ch;
|
||||||
margin-right : 1ch;
|
margin-right : 1ch;
|
||||||
text-align : center;
|
text-align : center;
|
||||||
}
|
}
|
||||||
@@ -166,7 +166,7 @@
|
|||||||
|
|
||||||
&.hidden {
|
&.hidden {
|
||||||
flex-wrap : nowrap;
|
flex-wrap : nowrap;
|
||||||
width : 32px;
|
width : 92px;
|
||||||
overflow : hidden;
|
overflow : hidden;
|
||||||
background-color : unset;
|
background-color : unset;
|
||||||
opacity : 0.5;
|
opacity : 0.5;
|
||||||
@@ -178,10 +178,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
button.toggleButton {
|
.toggleButton {
|
||||||
position : absolute;
|
position : absolute;
|
||||||
left : 0;
|
left : 0;
|
||||||
z-index : 5;
|
z-index : 5;
|
||||||
width : 32px;
|
width : 32px;
|
||||||
min-width : unset;
|
min-width : unset;
|
||||||
|
height : 100%;
|
||||||
|
display : flex;
|
||||||
}
|
}
|
||||||
@@ -12,6 +12,7 @@ const MetadataEditor = require('./metadataEditor/metadataEditor.jsx');
|
|||||||
|
|
||||||
const EDITOR_THEME_KEY = 'HOMEBREWERY-EDITOR-THEME';
|
const EDITOR_THEME_KEY = 'HOMEBREWERY-EDITOR-THEME';
|
||||||
|
|
||||||
|
const PAGEBREAK_REGEX_V3 = /^(?=\\page(?: *{[^\n{}]*})?$)/m;
|
||||||
const SNIPPETBAR_HEIGHT = 25;
|
const SNIPPETBAR_HEIGHT = 25;
|
||||||
const DEFAULT_STYLE_TEXT = dedent`
|
const DEFAULT_STYLE_TEXT = dedent`
|
||||||
/*=======--- Example CSS styling ---=======*/
|
/*=======--- Example CSS styling ---=======*/
|
||||||
@@ -126,15 +127,15 @@ const Editor = createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
updateCurrentCursorPage : function(cursor) {
|
updateCurrentCursorPage : function(cursor) {
|
||||||
const lines = this.props.brew.text.split('\n').slice(0, cursor.line + 1);
|
const lines = this.props.brew.text.split('\n').slice(1, cursor.line + 1);
|
||||||
const pageRegex = this.props.brew.renderer == 'V3' ? /^\\page$/ : /\\page/;
|
const pageRegex = this.props.brew.renderer == 'V3' ? PAGEBREAK_REGEX_V3 : /\\page/;
|
||||||
const currentPage = lines.reduce((count, line)=>count + (pageRegex.test(line) ? 1 : 0), 1);
|
const currentPage = lines.reduce((count, line)=>count + (pageRegex.test(line) ? 1 : 0), 1);
|
||||||
this.props.onCursorPageChange(currentPage);
|
this.props.onCursorPageChange(currentPage);
|
||||||
},
|
},
|
||||||
|
|
||||||
updateCurrentViewPage : function(topScrollLine) {
|
updateCurrentViewPage : function(topScrollLine) {
|
||||||
const lines = this.props.brew.text.split('\n').slice(0, topScrollLine + 1);
|
const lines = this.props.brew.text.split('\n').slice(1, topScrollLine + 1);
|
||||||
const pageRegex = this.props.brew.renderer == 'V3' ? /^\\page$/ : /\\page/;
|
const pageRegex = this.props.brew.renderer == 'V3' ? PAGEBREAK_REGEX_V3 : /\\page/;
|
||||||
const currentPage = lines.reduce((count, line)=>count + (pageRegex.test(line) ? 1 : 0), 1);
|
const currentPage = lines.reduce((count, line)=>count + (pageRegex.test(line) ? 1 : 0), 1);
|
||||||
this.props.onViewPageChange(currentPage);
|
this.props.onViewPageChange(currentPage);
|
||||||
},
|
},
|
||||||
@@ -174,7 +175,7 @@ const Editor = createClass({
|
|||||||
|
|
||||||
for (let i=customHighlights.length - 1;i>=0;i--) customHighlights[i].clear();
|
for (let i=customHighlights.length - 1;i>=0;i--) customHighlights[i].clear();
|
||||||
|
|
||||||
let editorPageCount = 2; // start page count from page 2
|
let editorPageCount = 1; // start page count from page 1
|
||||||
|
|
||||||
_.forEach(this.props.brew.text.split('\n'), (line, lineNumber)=>{
|
_.forEach(this.props.brew.text.split('\n'), (line, lineNumber)=>{
|
||||||
|
|
||||||
@@ -190,7 +191,10 @@ const Editor = createClass({
|
|||||||
|
|
||||||
// Styling for \page breaks
|
// Styling for \page breaks
|
||||||
if((this.props.renderer == 'legacy' && line.includes('\\page')) ||
|
if((this.props.renderer == 'legacy' && line.includes('\\page')) ||
|
||||||
(this.props.renderer == 'V3' && line.match(/^\\page$/))) {
|
(this.props.renderer == 'V3' && line.match(PAGEBREAK_REGEX_V3))) {
|
||||||
|
|
||||||
|
if(lineNumber > 0) // Since \page is optional on first line of document,
|
||||||
|
editorPageCount += 1; // don't use it to increment page count; stay at 1
|
||||||
|
|
||||||
// add back the original class 'background' but also add the new class '.pageline'
|
// add back the original class 'background' but also add the new class '.pageline'
|
||||||
codeMirror.addLineClass(lineNumber, 'background', 'pageLine');
|
codeMirror.addLineClass(lineNumber, 'background', 'pageLine');
|
||||||
@@ -199,8 +203,6 @@ const Editor = createClass({
|
|||||||
textContent : editorPageCount
|
textContent : editorPageCount
|
||||||
});
|
});
|
||||||
codeMirror.setBookmark({ line: lineNumber, ch: line.length }, pageCountElement);
|
codeMirror.setBookmark({ line: lineNumber, ch: line.length }, pageCountElement);
|
||||||
|
|
||||||
editorPageCount += 1;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// New Codemirror styling for V3 renderer
|
// New Codemirror styling for V3 renderer
|
||||||
@@ -358,7 +360,7 @@ const Editor = createClass({
|
|||||||
if(!this.isText() || isJumping)
|
if(!this.isText() || isJumping)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const textSplit = this.props.renderer == 'V3' ? /^\\page$/gm : /\\page/;
|
const textSplit = this.props.renderer == 'V3' ? PAGEBREAK_REGEX_V3 : /\\page/;
|
||||||
const textString = this.props.brew.text.split(textSplit).slice(0, targetPage-1).join(textSplit);
|
const textString = this.props.brew.text.split(textSplit).slice(0, targetPage-1).join(textSplit);
|
||||||
const targetLine = textString.match('\n') ? textString.split('\n').length - 1 : -1;
|
const targetLine = textString.match('\n') ? textString.split('\n').length - 1 : -1;
|
||||||
|
|
||||||
@@ -454,6 +456,7 @@ const Editor = createClass({
|
|||||||
rerenderParent={this.rerenderParent} />
|
rerenderParent={this.rerenderParent} />
|
||||||
<MetadataEditor
|
<MetadataEditor
|
||||||
metadata={this.props.brew}
|
metadata={this.props.brew}
|
||||||
|
themeBundle={this.props.themeBundle}
|
||||||
onChange={this.props.onMetaChange}
|
onChange={this.props.onMetaChange}
|
||||||
reportError={this.props.reportError}
|
reportError={this.props.reportError}
|
||||||
userThemes={this.props.userThemes}/>
|
userThemes={this.props.userThemes}/>
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ const MetadataEditor = createClass({
|
|||||||
theme : '5ePHB',
|
theme : '5ePHB',
|
||||||
lang : 'en'
|
lang : 'en'
|
||||||
},
|
},
|
||||||
|
|
||||||
onChange : ()=>{},
|
onChange : ()=>{},
|
||||||
reportError : ()=>{}
|
reportError : ()=>{}
|
||||||
};
|
};
|
||||||
@@ -67,6 +68,11 @@ const MetadataEditor = createClass({
|
|||||||
const inputRules = validations[name] ?? [];
|
const inputRules = validations[name] ?? [];
|
||||||
const validationErr = inputRules.map((rule)=>rule(e.target.value)).filter(Boolean);
|
const validationErr = inputRules.map((rule)=>rule(e.target.value)).filter(Boolean);
|
||||||
|
|
||||||
|
const debouncedReportValidity = _.debounce((target, errMessage) => {
|
||||||
|
callIfExists(target, 'setCustomValidity', errMessage);
|
||||||
|
callIfExists(target, 'reportValidity');
|
||||||
|
}, 300); // 300ms debounce delay, adjust as needed
|
||||||
|
|
||||||
// if no validation rules, save to props
|
// if no validation rules, save to props
|
||||||
if(validationErr.length === 0){
|
if(validationErr.length === 0){
|
||||||
callIfExists(e.target, 'setCustomValidity', '');
|
callIfExists(e.target, 'setCustomValidity', '');
|
||||||
@@ -74,14 +80,16 @@ const MetadataEditor = createClass({
|
|||||||
...this.props.metadata,
|
...this.props.metadata,
|
||||||
[name] : e.target.value
|
[name] : e.target.value
|
||||||
});
|
});
|
||||||
|
return true;
|
||||||
} else {
|
} else {
|
||||||
// if validation issues, display built-in browser error popup with each error.
|
// if validation issues, display built-in browser error popup with each error.
|
||||||
const errMessage = validationErr.map((err)=>{
|
const errMessage = validationErr.map((err)=>{
|
||||||
return `- ${err}`;
|
return `- ${err}`;
|
||||||
}).join('\n');
|
}).join('\n');
|
||||||
|
|
||||||
callIfExists(e.target, 'setCustomValidity', errMessage);
|
|
||||||
callIfExists(e.target, 'reportValidity');
|
debouncedReportValidity(e.target, errMessage);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -112,6 +120,14 @@ const MetadataEditor = createClass({
|
|||||||
handleTheme : function(theme){
|
handleTheme : function(theme){
|
||||||
this.props.metadata.renderer = theme.renderer;
|
this.props.metadata.renderer = theme.renderer;
|
||||||
this.props.metadata.theme = theme.path;
|
this.props.metadata.theme = theme.path;
|
||||||
|
|
||||||
|
this.props.onChange(this.props.metadata, 'theme');
|
||||||
|
},
|
||||||
|
|
||||||
|
handleThemeWritein : function(e) {
|
||||||
|
const shareId = e.target.value.split('/').pop(); //Extract just the ID if a URL was pasted in
|
||||||
|
this.props.metadata.theme = shareId;
|
||||||
|
|
||||||
this.props.onChange(this.props.metadata, 'theme');
|
this.props.onChange(this.props.metadata, 'theme');
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -200,7 +216,7 @@ const MetadataEditor = createClass({
|
|||||||
if(theme.path == this.props.metadata.shareId) return;
|
if(theme.path == this.props.metadata.shareId) return;
|
||||||
const preview = theme.thumbnail || `/themes/${theme.renderer}/${theme.path}/dropdownPreview.png`;
|
const preview = theme.thumbnail || `/themes/${theme.renderer}/${theme.path}/dropdownPreview.png`;
|
||||||
const texture = theme.thumbnail || `/themes/${theme.renderer}/${theme.path}/dropdownTexture.png`;
|
const texture = theme.thumbnail || `/themes/${theme.renderer}/${theme.path}/dropdownTexture.png`;
|
||||||
return <div className='item' key={`${renderer}_${theme.name}`} onClick={()=>this.handleTheme(theme)} title={''}>
|
return <div className='item' key={`${renderer}_${theme.name}`} value={`${theme.author ?? renderer} : ${theme.name}`} data={theme} title={''}>
|
||||||
{theme.author ?? renderer} : {theme.name}
|
{theme.author ?? renderer} : {theme.name}
|
||||||
<div className='texture-container'>
|
<div className='texture-container'>
|
||||||
<img src={texture}/>
|
<img src={texture}/>
|
||||||
@@ -210,26 +226,40 @@ const MetadataEditor = createClass({
|
|||||||
<img src={preview}/>
|
<img src={preview}/>
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
});
|
}).filter(Boolean);
|
||||||
};
|
};
|
||||||
|
|
||||||
const currentRenderer = this.props.metadata.renderer;
|
const currentRenderer = this.props.metadata.renderer;
|
||||||
const currentTheme = mergedThemes[`${_.upperFirst(this.props.metadata.renderer)}`][this.props.metadata.theme]
|
const currentThemeDisplay = this.props.themeBundle?.name ? `${this.props.themeBundle.author ?? currentRenderer} : ${this.props.themeBundle.name}` : 'No Theme Selected';
|
||||||
?? { name: `!!! THEME MISSING !!! ID=${this.props.metadata.theme}` };
|
|
||||||
let dropdown;
|
let dropdown;
|
||||||
|
|
||||||
if(currentRenderer == 'legacy') {
|
if(currentRenderer == 'legacy') {
|
||||||
dropdown =
|
dropdown =
|
||||||
<Nav.dropdown className='disabled value' trigger='disabled'>
|
<div className='disabled value' trigger='disabled'>
|
||||||
<div> {`Themes are not supported in the Legacy Renderer`} <i className='fas fa-caret-down'></i> </div>
|
<div> Themes are not supported in the Legacy Renderer </div>
|
||||||
</Nav.dropdown>;
|
</div>;
|
||||||
} else {
|
} else {
|
||||||
dropdown =
|
dropdown =
|
||||||
<Nav.dropdown className='value' trigger='click'>
|
<div className='value'>
|
||||||
<div> {currentTheme.author ?? _.upperFirst(currentRenderer)} : {currentTheme.name} <i className='fas fa-caret-down'></i> </div>
|
<Combobox trigger='click'
|
||||||
|
className='themes-dropdown'
|
||||||
{listThemes(currentRenderer)}
|
default={currentThemeDisplay}
|
||||||
</Nav.dropdown>;
|
placeholder='Select from below, or enter the Share URL or ID of a brew with the meta:theme tag'
|
||||||
|
onSelect={(value)=>this.handleTheme(value)}
|
||||||
|
onEntry={(e)=>{
|
||||||
|
e.target.setCustomValidity(''); //Clear the validation popup while typing
|
||||||
|
if(this.handleFieldChange('theme', e))
|
||||||
|
this.handleThemeWritein(e);
|
||||||
|
}}
|
||||||
|
options={listThemes(currentRenderer)}
|
||||||
|
autoSuggest={{
|
||||||
|
suggestMethod : 'includes',
|
||||||
|
clearAutoSuggestOnClick : true,
|
||||||
|
filterOn : ['value', 'title']
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<small>Select from the list below (built-in themes and brews you have tagged "meta:theme"), or paste in the Share URL or Share ID of any brew.</small>
|
||||||
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div className='field themes'>
|
return <div className='field themes'>
|
||||||
@@ -244,15 +274,13 @@ const MetadataEditor = createClass({
|
|||||||
return _.map(langCodes.sort(), (code, index)=>{
|
return _.map(langCodes.sort(), (code, index)=>{
|
||||||
const localName = new Intl.DisplayNames([code], { type: 'language' });
|
const localName = new Intl.DisplayNames([code], { type: 'language' });
|
||||||
const englishName = new Intl.DisplayNames('en', { type: 'language' });
|
const englishName = new Intl.DisplayNames('en', { type: 'language' });
|
||||||
return <div className='item' title={`${englishName.of(code)}`} key={`${index}`} data-value={`${code}`} data-detail={`${localName.of(code)}`}>
|
return <div className='item' title={englishName.of(code)} key={`${index}`} value={code} detail={localName.of(code)}>
|
||||||
{`${code}`}
|
{code}
|
||||||
<div className='detail'>{`${localName.of(code)}`}</div>
|
<div className='detail'>{localName.of(code)}</div>
|
||||||
</div>;
|
</div>;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const debouncedHandleFieldChange = _.debounce(this.handleFieldChange, 500);
|
|
||||||
|
|
||||||
return <div className='field language'>
|
return <div className='field language'>
|
||||||
<label>language</label>
|
<label>language</label>
|
||||||
<div className='value'>
|
<div className='value'>
|
||||||
@@ -263,16 +291,15 @@ const MetadataEditor = createClass({
|
|||||||
onSelect={(value)=>this.handleLanguage(value)}
|
onSelect={(value)=>this.handleLanguage(value)}
|
||||||
onEntry={(e)=>{
|
onEntry={(e)=>{
|
||||||
e.target.setCustomValidity(''); //Clear the validation popup while typing
|
e.target.setCustomValidity(''); //Clear the validation popup while typing
|
||||||
debouncedHandleFieldChange('lang', e);
|
this.handleFieldChange('lang', e);
|
||||||
}}
|
}}
|
||||||
options={listLanguages()}
|
options={listLanguages()}
|
||||||
autoSuggest={{
|
autoSuggest={{
|
||||||
suggestMethod : 'startsWith',
|
suggestMethod : 'startsWith',
|
||||||
clearAutoSuggestOnClick : true,
|
clearAutoSuggestOnClick : true,
|
||||||
filterOn : ['data-value', 'data-detail', 'title']
|
filterOn : ['value', 'detail', 'title']
|
||||||
}}
|
}}
|
||||||
>
|
/>
|
||||||
</Combobox>
|
|
||||||
<small>Sets the HTML Lang property for your brew. May affect hyphenation or spellcheck.</small>
|
<small>Sets the HTML Lang property for your brew. May affect hyphenation or spellcheck.</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
@import 'naturalcrit/styles/colors.less';
|
@import 'naturalcrit/styles/colors.less';
|
||||||
|
|
||||||
|
.userThemeName {
|
||||||
|
padding-left: 10px;
|
||||||
|
padding-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.metadataEditor {
|
.metadataEditor {
|
||||||
position : absolute;
|
position : absolute;
|
||||||
z-index : 5;
|
|
||||||
box-sizing : border-box;
|
box-sizing : border-box;
|
||||||
width : 100%;
|
width : 100%;
|
||||||
height : calc(100vh - 54px); // 54px is the height of the navbar + snippet bar. probably a better way to dynamic get this.
|
height : calc(100vh - 54px); // 54px is the height of the navbar + snippet bar. probably a better way to dynamic get this.
|
||||||
@@ -71,8 +74,7 @@
|
|||||||
border : 1px solid gray;
|
border : 1px solid gray;
|
||||||
&:focus { outline : 1px solid #444444; }
|
&:focus { outline : 1px solid #444444; }
|
||||||
}
|
}
|
||||||
&.thumbnail {
|
&.thumbnail, &.themes{
|
||||||
height : 1.4em;
|
|
||||||
label { line-height : 2.0em; }
|
label { line-height : 2.0em; }
|
||||||
.value {
|
.value {
|
||||||
overflow : hidden;
|
overflow : hidden;
|
||||||
@@ -88,6 +90,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.themes{
|
||||||
|
.value {
|
||||||
|
overflow : visible;
|
||||||
|
text-overflow : auto;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
padding-left: 5px;
|
||||||
|
padding-right: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&.description {
|
&.description {
|
||||||
flex : 1;
|
flex : 1;
|
||||||
textarea.value {
|
textarea.value {
|
||||||
@@ -156,36 +169,19 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.themes.field {
|
.themes.field {
|
||||||
.navDropdownContainer {
|
& .dropdown-container {
|
||||||
position : relative;
|
position : relative;
|
||||||
z-index : 100;
|
z-index : 100;
|
||||||
background-color : white;
|
background-color : white;
|
||||||
&.disabled {
|
}
|
||||||
|
& .dropdown-options {
|
||||||
|
overflow-y : visible;
|
||||||
|
}
|
||||||
|
.disabled {
|
||||||
font-style : italic;
|
font-style : italic;
|
||||||
color : dimgray;
|
color : dimgray;
|
||||||
background-color : darkgray;
|
background-color : darkgray;
|
||||||
}
|
}
|
||||||
& > div:first-child {
|
|
||||||
padding : 3px 3px;
|
|
||||||
background-color : inherit;
|
|
||||||
border : 1px solid gray;
|
|
||||||
i { float : right; }
|
|
||||||
&:hover {
|
|
||||||
color : white;
|
|
||||||
background-color : @blue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.navDropdown .item > p {
|
|
||||||
width : 45%;
|
|
||||||
height : 1.1em;
|
|
||||||
overflow : hidden;
|
|
||||||
text-overflow : ellipsis;
|
|
||||||
white-space : nowrap;
|
|
||||||
}
|
|
||||||
.navDropdown {
|
|
||||||
position : absolute;
|
|
||||||
width : 100%;
|
|
||||||
box-shadow : 0px 5px 10px rgba(0, 0, 0, 0.3);
|
|
||||||
.item {
|
.item {
|
||||||
position : relative;
|
position : relative;
|
||||||
padding : 3px 3px;
|
padding : 3px 3px;
|
||||||
@@ -214,11 +210,7 @@
|
|||||||
border-bottom : 2px solid hsl(0,0%,40%);
|
border-bottom : 2px solid hsl(0,0%,40%);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&:hover {
|
|
||||||
color : white;
|
|
||||||
background-color : @blue;
|
|
||||||
}
|
|
||||||
&:hover > .preview { opacity : 1; }
|
|
||||||
.texture-container {
|
.texture-container {
|
||||||
position : absolute;
|
position : absolute;
|
||||||
top : 0;
|
top : 0;
|
||||||
@@ -229,7 +221,7 @@
|
|||||||
overflow : hidden;
|
overflow : hidden;
|
||||||
> img {
|
> img {
|
||||||
position : absolute;
|
position : absolute;
|
||||||
top : 0px;
|
top : 0;
|
||||||
right : 0;
|
right : 0;
|
||||||
width : 50%;
|
width : 50%;
|
||||||
min-height : 100%;
|
min-height : 100%;
|
||||||
@@ -237,8 +229,13 @@
|
|||||||
mask-image : linear-gradient(90deg, transparent, black 20%);
|
mask-image : linear-gradient(90deg, transparent, black 20%);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color : white;
|
||||||
|
background-color : @blue;
|
||||||
|
filter : unset;
|
||||||
}
|
}
|
||||||
}
|
&:hover > .preview { opacity : 1; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,19 @@ module.exports = {
|
|||||||
(value)=>{
|
(value)=>{
|
||||||
return new RegExp(/^([a-zA-Z]{2,3})(-[a-zA-Z]{4})?(-(?:[0-9]{3}|[a-zA-Z]{2}))?$/).test(value) === false && (value.length > 0) ? 'Invalid language code.' : null;
|
return new RegExp(/^([a-zA-Z]{2,3})(-[a-zA-Z]{4})?(-(?:[0-9]{3}|[a-zA-Z]{2}))?$/).test(value) === false && (value.length > 0) ? 'Invalid language code.' : null;
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
theme: [
|
||||||
|
(value) => {
|
||||||
|
const URL = global.config.baseUrl.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&'); //Escape any regex characters
|
||||||
|
const shareIDPattern = '[a-zA-Z0-9-_]{12}';
|
||||||
|
const shareURLRegex = new RegExp(`^${URL}\\/share\\/${shareIDPattern}$`);
|
||||||
|
const shareIDRegex = new RegExp(`^${shareIDPattern}$`);
|
||||||
|
if (value?.length === 0) return null;
|
||||||
|
if (shareURLRegex.test(value)) return null;
|
||||||
|
if (shareIDRegex.test(value)) return null;
|
||||||
|
|
||||||
|
return 'Must be a valid Share URL or a 12-character ID.';
|
||||||
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -207,19 +207,11 @@ const Snippetbar = createClass({
|
|||||||
renderEditorButtons : function(){
|
renderEditorButtons : function(){
|
||||||
if(!this.props.showEditButtons) return;
|
if(!this.props.showEditButtons) return;
|
||||||
|
|
||||||
const foldButtons = <>
|
|
||||||
<div className={`editorTool foldAll ${this.props.view !== 'meta' && this.props.foldCode ? 'active' : ''}`}
|
|
||||||
onClick={this.props.foldCode} >
|
|
||||||
<i className='fas fa-compress-alt' />
|
|
||||||
</div>
|
|
||||||
<div className={`editorTool unfoldAll ${this.props.view !== 'meta' && this.props.unfoldCode ? 'active' : ''}`}
|
|
||||||
onClick={this.props.unfoldCode} >
|
|
||||||
<i className='fas fa-expand-alt' />
|
|
||||||
</div>
|
|
||||||
</>;
|
|
||||||
|
|
||||||
return <div className='editors'>
|
|
||||||
<div className='historyTools'>
|
return (
|
||||||
|
<div className='editors'>
|
||||||
|
{this.props.view !== 'meta' && <><div className='historyTools'>
|
||||||
<div className={`editorTool snippetGroup history ${this.state.historyExists ? 'active' : ''}`}
|
<div className={`editorTool snippetGroup history ${this.state.historyExists ? 'active' : ''}`}
|
||||||
onClick={this.toggleHistoryMenu} >
|
onClick={this.toggleHistoryMenu} >
|
||||||
<i className='fas fa-clock-rotate-left' />
|
<i className='fas fa-clock-rotate-left' />
|
||||||
@@ -235,13 +227,20 @@ const Snippetbar = createClass({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='codeTools'>
|
<div className='codeTools'>
|
||||||
{foldButtons}
|
<div className={`editorTool foldAll ${this.props.foldCode ? 'active' : ''}`}
|
||||||
<div className={`editorTool editorTheme ${this.state.themeSelector ? 'active' : ''}`}
|
onClick={this.props.foldCode} >
|
||||||
|
<i className='fas fa-compress-alt' />
|
||||||
|
</div>
|
||||||
|
<div className={`editorTool unfoldAll ${this.props.unfoldCode ? 'active' : ''}`}
|
||||||
|
onClick={this.props.unfoldCode} >
|
||||||
|
<i className='fas fa-expand-alt' />
|
||||||
|
</div>
|
||||||
|
<div className={`editorTheme ${this.state.themeSelector ? 'active' : ''}`}
|
||||||
onClick={this.toggleThemeSelector} >
|
onClick={this.toggleThemeSelector} >
|
||||||
<i className='fas fa-palette' />
|
<i className='fas fa-palette' />
|
||||||
{this.state.themeSelector && this.renderThemeSelector()}
|
{this.state.themeSelector && this.renderThemeSelector()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div></>}
|
||||||
|
|
||||||
|
|
||||||
<div className='tabs'>
|
<div className='tabs'>
|
||||||
@@ -259,7 +258,8 @@ const Snippetbar = createClass({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>;
|
</div>
|
||||||
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
render : function(){
|
render : function(){
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
justify-content : flex-end;
|
justify-content : flex-end;
|
||||||
min-width : 225px;
|
min-width : 225px;
|
||||||
|
|
||||||
&:only-child { margin-left : auto; }
|
&:only-child { margin-left : auto;min-width:unset;}
|
||||||
|
|
||||||
>div {
|
>div {
|
||||||
display : flex;
|
display : flex;
|
||||||
@@ -38,6 +38,11 @@
|
|||||||
line-height : @menuHeight;
|
line-height : @menuHeight;
|
||||||
text-align : center;
|
text-align : center;
|
||||||
cursor : pointer;
|
cursor : pointer;
|
||||||
|
|
||||||
|
&.editorTool:not(.active) {
|
||||||
|
cursor:not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
&:hover,&.selected { background-color : #999999; }
|
&:hover,&.selected { background-color : #999999; }
|
||||||
&.text {
|
&.text {
|
||||||
.tooltipLeft('Brew Editor');
|
.tooltipLeft('Brew Editor');
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
|
//╔===--------------- Polyfills --------------===╗//
|
||||||
|
import 'core-js/es/string/to-well-formed.js';
|
||||||
|
//╚===--------------- ---------------===╝//
|
||||||
|
|
||||||
require('./homebrew.less');
|
require('./homebrew.less');
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const createClass = require('create-react-class');
|
const createClass = require('create-react-class');
|
||||||
const { StaticRouter:Router } = require('react-router-dom/server');
|
const { StaticRouter:Router } = require('react-router');
|
||||||
const { Route, Routes, useParams, useSearchParams } = require('react-router-dom');
|
const { Route, Routes, useParams, useSearchParams } = require('react-router');
|
||||||
|
|
||||||
const HomePage = require('./pages/homePage/homePage.jsx');
|
const HomePage = require('./pages/homePage/homePage.jsx');
|
||||||
const EditPage = require('./pages/editPage/editPage.jsx');
|
const EditPage = require('./pages/editPage/editPage.jsx');
|
||||||
|
|||||||
@@ -116,6 +116,19 @@ const ErrorNavItem = createClass({
|
|||||||
</Nav.item>;
|
</Nav.item>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(HBErrorCode === '10') {
|
||||||
|
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||||
|
Oops!
|
||||||
|
<div className='errorContainer' onClick={clearError}>
|
||||||
|
Looks like the brew you have selected
|
||||||
|
as a theme is not tagged for use as a
|
||||||
|
theme. Verify that
|
||||||
|
brew <a className='lowercase' target='_blank' rel='noopener noreferrer' href={`/share/${response.body.brewId}`}>
|
||||||
|
{response.body.brewId}</a> has the <span className='lowercase'>meta:theme</span> tag!
|
||||||
|
</div>
|
||||||
|
</Nav.item>;
|
||||||
|
}
|
||||||
|
|
||||||
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||||
Oops!
|
Oops!
|
||||||
<div className='errorContainer'>
|
<div className='errorContainer'>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
require('./brewItem.less');
|
require('./brewItem.less');
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const createClass = require('create-react-class');
|
const { useCallback } = React;
|
||||||
const moment = require('moment');
|
const moment = require('moment');
|
||||||
import request from '../../../../utils/request-middleware.js';
|
import request from '../../../../utils/request-middleware.js';
|
||||||
|
|
||||||
@@ -8,176 +8,172 @@ const googleDriveIcon = require('../../../../googleDrive.svg');
|
|||||||
const homebreweryIcon = require('../../../../thumbnail.png');
|
const homebreweryIcon = require('../../../../thumbnail.png');
|
||||||
const dedent = require('dedent-tabs').default;
|
const dedent = require('dedent-tabs').default;
|
||||||
|
|
||||||
const BrewItem = createClass({
|
const BrewItem = ({
|
||||||
displayName : 'BrewItem',
|
brew = {
|
||||||
getDefaultProps : function() {
|
|
||||||
return {
|
|
||||||
brew : {
|
|
||||||
title : '',
|
title : '',
|
||||||
description : '',
|
description : '',
|
||||||
authors : [],
|
authors : [],
|
||||||
stubbed : true
|
stubbed : true,
|
||||||
},
|
|
||||||
updateListFilter : ()=>{},
|
|
||||||
reportError : ()=>{},
|
|
||||||
renderStorage : true
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
|
updateListFilter = ()=>{},
|
||||||
|
reportError = ()=>{},
|
||||||
|
renderStorage = true,
|
||||||
|
})=>{
|
||||||
|
|
||||||
deleteBrew : function(){
|
const deleteBrew = useCallback(()=>{
|
||||||
if(this.props.brew.authors.length <= 1){
|
if(brew.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(!window.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(!window.confirm('Are you REALLY sure? You will not be able to recover the document.')) return;
|
||||||
} else {
|
} else {
|
||||||
if(!confirm('Are you sure you want to remove this brew from your collection? This will remove you as an editor, but other owners will still be able to access the document.')) return;
|
if(!window.confirm('Are you sure you want to remove this brew from your collection? This will remove you as an editor, but other owners will still be able to access the document.')) return;
|
||||||
if(!confirm('Are you REALLY sure? You will lose editor access to this document.')) return;
|
if(!window.confirm('Are you REALLY sure? You will lose editor access to this document.')) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
request.delete(`/api/${this.props.brew.googleId ?? ''}${this.props.brew.editId}`)
|
request.delete(`/api/${brew.googleId ?? ''}${brew.editId}`).send().end((err, res)=>{
|
||||||
.send()
|
if (err) reportError(err); else window.location.reload();
|
||||||
.end((err, res)=>{
|
|
||||||
if(err) {
|
|
||||||
this.props.reportError(err);
|
|
||||||
} else {
|
|
||||||
location.reload();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
},
|
}, [brew, reportError]);
|
||||||
|
|
||||||
updateFilter : function(type, term){
|
const updateFilter = useCallback((type, term)=> updateListFilter(type, term), [updateListFilter]);
|
||||||
this.props.updateListFilter(type, term);
|
|
||||||
},
|
|
||||||
|
|
||||||
renderDeleteBrewLink : function(){
|
const renderDeleteBrewLink = ()=>{
|
||||||
if(!this.props.brew.editId) return;
|
if(!brew.editId) return null;
|
||||||
|
|
||||||
return <a className='deleteLink' onClick={this.deleteBrew}>
|
return (
|
||||||
|
<a className='deleteLink' onClick={deleteBrew}>
|
||||||
<i className='fas fa-trash-alt' title='Delete' />
|
<i className='fas fa-trash-alt' title='Delete' />
|
||||||
</a>;
|
</a>
|
||||||
},
|
);
|
||||||
|
};
|
||||||
|
|
||||||
renderEditLink : function(){
|
const renderEditLink = ()=>{
|
||||||
if(!this.props.brew.editId) return;
|
if(!brew.editId) return null;
|
||||||
|
|
||||||
let editLink = this.props.brew.editId;
|
let editLink = brew.editId;
|
||||||
if(this.props.brew.googleId && !this.props.brew.stubbed) {
|
if(brew.googleId && !brew.stubbed) editLink = brew.googleId + editLink;
|
||||||
editLink = this.props.brew.googleId + editLink;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <a className='editLink' href={`/edit/${editLink}`} target='_blank' rel='noopener noreferrer'>
|
return (
|
||||||
|
<a className='editLink' href={`/edit/${editLink}`} target='_blank' rel='noopener noreferrer'>
|
||||||
<i className='fas fa-pencil-alt' title='Edit' />
|
<i className='fas fa-pencil-alt' title='Edit' />
|
||||||
</a>;
|
</a>
|
||||||
},
|
);
|
||||||
|
};
|
||||||
|
|
||||||
renderShareLink : function(){
|
const renderShareLink = ()=>{
|
||||||
if(!this.props.brew.shareId) return;
|
if(!brew.shareId) return null;
|
||||||
|
|
||||||
let shareLink = this.props.brew.shareId;
|
let shareLink = brew.shareId;
|
||||||
if(this.props.brew.googleId && !this.props.brew.stubbed) {
|
if(brew.googleId && !brew.stubbed) {
|
||||||
shareLink = this.props.brew.googleId + shareLink;
|
shareLink = brew.googleId + shareLink;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <a className='shareLink' href={`/share/${shareLink}`} target='_blank' rel='noopener noreferrer'>
|
return (
|
||||||
|
<a className='shareLink' href={`/share/${shareLink}`} target='_blank' rel='noopener noreferrer'>
|
||||||
<i className='fas fa-share-alt' title='Share' />
|
<i className='fas fa-share-alt' title='Share' />
|
||||||
</a>;
|
</a>
|
||||||
},
|
);
|
||||||
|
};
|
||||||
|
|
||||||
renderDownloadLink : function(){
|
const renderDownloadLink = ()=>{
|
||||||
if(!this.props.brew.shareId) return;
|
if(!brew.shareId) return null;
|
||||||
|
|
||||||
let shareLink = this.props.brew.shareId;
|
let shareLink = brew.shareId;
|
||||||
if(this.props.brew.googleId && !this.props.brew.stubbed) {
|
if(brew.googleId && !brew.stubbed) {
|
||||||
shareLink = this.props.brew.googleId + shareLink;
|
shareLink = brew.googleId + shareLink;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <a className='downloadLink' href={`/download/${shareLink}`}>
|
return (
|
||||||
|
<a className='downloadLink' href={`/download/${shareLink}`}>
|
||||||
<i className='fas fa-download' title='Download' />
|
<i className='fas fa-download' title='Download' />
|
||||||
</a>;
|
</a>
|
||||||
},
|
);
|
||||||
|
};
|
||||||
|
|
||||||
renderStorageIcon : function(){
|
const renderStorageIcon = ()=>{
|
||||||
if(!this.props.renderStorage) return;
|
if(!renderStorage) return null;
|
||||||
if(this.props.brew.googleId) {
|
if(brew.googleId) {
|
||||||
return <span title={this.props.brew.webViewLink ? 'Your Google Drive Storage': 'Another User\'s Google Drive Storage'}>
|
return (
|
||||||
<a href={this.props.brew.webViewLink} target='_blank'>
|
<span title={brew.webViewLink ? 'Your Google Drive Storage' : 'Another User\'s Google Drive Storage'}>
|
||||||
|
<a href={brew.webViewLink} target='_blank'>
|
||||||
<img className='googleDriveIcon' src={googleDriveIcon} alt='googleDriveIcon' />
|
<img className='googleDriveIcon' src={googleDriveIcon} alt='googleDriveIcon' />
|
||||||
</a>
|
</a>
|
||||||
</span>;
|
</span>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <span title='Homebrewery Storage'>
|
return (
|
||||||
|
<span title='Homebrewery Storage'>
|
||||||
<img className='homebreweryIcon' src={homebreweryIcon} alt='homebreweryIcon' />
|
<img className='homebreweryIcon' src={homebreweryIcon} alt='homebreweryIcon' />
|
||||||
</span>;
|
</span>
|
||||||
},
|
);
|
||||||
|
};
|
||||||
|
|
||||||
render : function(){
|
if(Array.isArray(brew.tags)) {
|
||||||
const brew = this.props.brew;
|
brew.tags = brew.tags?.filter((tag)=>tag); // remove tags that are empty strings
|
||||||
if(Array.isArray(brew.tags)) { // temporary fix until dud tags are cleaned
|
|
||||||
brew.tags = brew.tags?.filter((tag)=>tag); //remove tags that are empty strings
|
|
||||||
brew.tags.sort((a, b)=>{
|
brew.tags.sort((a, b)=>{
|
||||||
return a.indexOf(':') - b.indexOf(':') != 0 ? a.indexOf(':') - b.indexOf(':') : a.toLowerCase().localeCompare(b.toLowerCase());
|
return a.indexOf(':') - b.indexOf(':') !== 0 ? a.indexOf(':') - b.indexOf(':') : a.toLowerCase().localeCompare(b.toLowerCase());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const dateFormatString = 'YYYY-MM-DD HH:mm:ss';
|
const dateFormatString = 'YYYY-MM-DD HH:mm:ss';
|
||||||
|
|
||||||
return <div className='brewItem'>
|
return (
|
||||||
{brew.thumbnail &&
|
<div className='brewItem'>
|
||||||
<div className='thumbnail' style={{ backgroundImage: `url(${brew.thumbnail})` }} >
|
{brew.thumbnail && <div className='thumbnail' style={{ backgroundImage: `url(${brew.thumbnail})` }}></div>}
|
||||||
</div>
|
|
||||||
}
|
|
||||||
<div className='text'>
|
<div className='text'>
|
||||||
<h2>{brew.title}</h2>
|
<h2>{brew.title}</h2>
|
||||||
<p className='description'>{brew.description}</p>
|
<p className='description'>{brew.description}</p>
|
||||||
</div>
|
</div>
|
||||||
<hr />
|
<hr />
|
||||||
<div className='info'>
|
<div className='info'>
|
||||||
|
{brew.tags?.length ? (
|
||||||
{brew.tags?.length ? <>
|
|
||||||
<div className='brewTags' title={`${brew.tags.length} tags:\n${brew.tags.join('\n')}`}>
|
<div className='brewTags' title={`${brew.tags.length} tags:\n${brew.tags.join('\n')}`}>
|
||||||
<i className='fas fa-tags'/>
|
<i className='fas fa-tags' />
|
||||||
{brew.tags.map((tag, idx)=>{
|
{brew.tags.map((tag, idx)=>{
|
||||||
const matches = tag.match(/^(?:([^:]+):)?([^:]+)$/);
|
const matches = tag.match(/^(?:([^:]+):)?([^:]+)$/);
|
||||||
return <span key={idx} className={matches[1]} onClick={()=>{this.updateFilter(tag);}}>{matches[2]}</span>;
|
return <span key={idx} className={matches[1]} onClick={()=>updateFilter(tag)}>{matches[2]}</span>;
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</> : <></>
|
) : null}
|
||||||
}
|
|
||||||
<span title={`Authors:\n${brew.authors?.join('\n')}`}>
|
<span title={`Authors:\n${brew.authors?.join('\n')}`}>
|
||||||
<i className='fas fa-user'/> {brew.authors?.map((author, index)=>(
|
<i className='fas fa-user' />{' '}
|
||||||
|
{brew.authors?.map((author, index)=>(
|
||||||
<React.Fragment key={index}>
|
<React.Fragment key={index}>
|
||||||
{author === 'hidden'
|
{author === 'hidden' ? (
|
||||||
? <span title="Username contained an email address; hidden to protect user's privacy">{author}</span>
|
<span title="Username contained an email address; hidden to protect user's privacy">
|
||||||
: <a href={`/user/${author}`}>{author}</a>
|
{author}
|
||||||
}
|
</span>
|
||||||
|
) : (<a href={`/user/${author}`}>{author}</a>)}
|
||||||
{index < brew.authors.length - 1 && ', '}
|
{index < brew.authors.length - 1 && ', '}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
))}
|
))}
|
||||||
</span>
|
</span>
|
||||||
<br />
|
<br />
|
||||||
<span title={`Last viewed: ${moment(brew.lastViewed).local().format(dateFormatString)}`}>
|
<span title={`Last viewed: ${moment(brew.lastViewed).local().format(dateFormatString)}`}>
|
||||||
<i className='fas fa-eye'/> {brew.views}
|
<i className='fas fa-eye' /> {brew.views}
|
||||||
</span>
|
</span>
|
||||||
{brew.pageCount &&
|
{brew.pageCount && (
|
||||||
<span title={`Page count: ${brew.pageCount}`}>
|
<span title={`Page count: ${brew.pageCount}`}>
|
||||||
<i className='far fa-file' /> {brew.pageCount}
|
<i className='far fa-file' /> {brew.pageCount}
|
||||||
</span>
|
</span>
|
||||||
}
|
)}
|
||||||
<span title={dedent`
|
<span
|
||||||
Created: ${moment(brew.createdAt).local().format(dateFormatString)}
|
title={dedent` Created: ${moment(brew.createdAt).local().format(dateFormatString)}
|
||||||
Last updated: ${moment(brew.updatedAt).local().format(dateFormatString)}`}>
|
Last updated: ${moment(brew.updatedAt).local().format(dateFormatString)}`}
|
||||||
|
>
|
||||||
<i className='fas fa-sync-alt' /> {moment(brew.updatedAt).fromNow()}
|
<i className='fas fa-sync-alt' /> {moment(brew.updatedAt).fromNow()}
|
||||||
</span>
|
</span>
|
||||||
{this.renderStorageIcon()}
|
{renderStorageIcon()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='links'>
|
<div className='links'>
|
||||||
{this.renderShareLink()}
|
{renderShareLink()}
|
||||||
{this.renderEditLink()}
|
{renderEditLink()}
|
||||||
{this.renderDownloadLink()}
|
{renderDownloadLink()}
|
||||||
{this.renderDeleteBrewLink()}
|
{renderDeleteBrewLink()}
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>
|
||||||
}
|
);
|
||||||
});
|
};
|
||||||
|
|
||||||
module.exports = BrewItem;
|
module.exports = BrewItem;
|
||||||
|
|||||||
@@ -59,11 +59,6 @@
|
|||||||
padding-left : 1.25em;
|
padding-left : 1.25em;
|
||||||
list-style : square;
|
list-style : square;
|
||||||
}
|
}
|
||||||
.blank {
|
|
||||||
height : 1em;
|
|
||||||
margin-top : 0;
|
|
||||||
& + * { margin-top : 0; }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -102,6 +102,14 @@ const EditPage = createClass({
|
|||||||
window.onbeforeunload = function(){};
|
window.onbeforeunload = function(){};
|
||||||
document.removeEventListener('keydown', this.handleControlKeys);
|
document.removeEventListener('keydown', this.handleControlKeys);
|
||||||
},
|
},
|
||||||
|
componentDidUpdate : function(){
|
||||||
|
const hasChange = this.hasChanges();
|
||||||
|
if(this.state.isPending != hasChange){
|
||||||
|
this.setState({
|
||||||
|
isPending : hasChange
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
handleControlKeys : function(e){
|
handleControlKeys : function(e){
|
||||||
if(!(e.ctrlKey || e.metaKey)) return;
|
if(!(e.ctrlKey || e.metaKey)) return;
|
||||||
@@ -138,15 +146,13 @@ const EditPage = createClass({
|
|||||||
|
|
||||||
this.setState((prevState)=>({
|
this.setState((prevState)=>({
|
||||||
brew : { ...prevState.brew, text: text },
|
brew : { ...prevState.brew, text: text },
|
||||||
isPending : true,
|
|
||||||
htmlErrors : htmlErrors,
|
htmlErrors : htmlErrors,
|
||||||
}), ()=>{if(this.state.autoSave) this.trySave();});
|
}), ()=>{if(this.state.autoSave) this.trySave();});
|
||||||
},
|
},
|
||||||
|
|
||||||
handleStyleChange : function(style){
|
handleStyleChange : function(style){
|
||||||
this.setState((prevState)=>({
|
this.setState((prevState)=>({
|
||||||
brew : { ...prevState.brew, style: style },
|
brew : { ...prevState.brew, style: style }
|
||||||
isPending : true
|
|
||||||
}), ()=>{if(this.state.autoSave) this.trySave();});
|
}), ()=>{if(this.state.autoSave) this.trySave();});
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -158,8 +164,7 @@ const EditPage = createClass({
|
|||||||
brew : {
|
brew : {
|
||||||
...prevState.brew,
|
...prevState.brew,
|
||||||
...metadata
|
...metadata
|
||||||
},
|
}
|
||||||
isPending : true,
|
|
||||||
}), ()=>{if(this.state.autoSave) this.trySave();});
|
}), ()=>{if(this.state.autoSave) this.trySave();});
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -247,16 +252,17 @@ const EditPage = createClass({
|
|||||||
});
|
});
|
||||||
if(!res) return;
|
if(!res) return;
|
||||||
|
|
||||||
this.savedBrew = res.body;
|
this.savedBrew = {
|
||||||
|
...this.state.brew,
|
||||||
|
googleId : res.body.googleId ? res.body.googleId : null,
|
||||||
|
editId : res.body.editId,
|
||||||
|
shareId : res.body.shareId,
|
||||||
|
version : res.body.version
|
||||||
|
};
|
||||||
history.replaceState(null, null, `/edit/${this.savedBrew.editId}`);
|
history.replaceState(null, null, `/edit/${this.savedBrew.editId}`);
|
||||||
|
|
||||||
this.setState((prevState)=>({
|
this.setState(()=>({
|
||||||
brew : { ...prevState.brew,
|
brew : this.savedBrew,
|
||||||
googleId : this.savedBrew.googleId ? this.savedBrew.googleId : null,
|
|
||||||
editId : this.savedBrew.editId,
|
|
||||||
shareId : this.savedBrew.shareId,
|
|
||||||
version : this.savedBrew.version
|
|
||||||
},
|
|
||||||
isPending : false,
|
isPending : false,
|
||||||
isSaving : false,
|
isSaving : false,
|
||||||
unsavedTime : new Date()
|
unsavedTime : new Date()
|
||||||
@@ -311,7 +317,14 @@ const EditPage = createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
renderSaveButton : function(){
|
renderSaveButton : function(){
|
||||||
if(this.state.autoSaveWarning && this.hasChanges()){
|
|
||||||
|
// #1 - Currently saving, show SAVING
|
||||||
|
if(this.state.isSaving){
|
||||||
|
return <Nav.item className='save' icon='fas fa-spinner fa-spin'>saving...</Nav.item>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// #2 - Unsaved changes exist, autosave is OFF and warning timer has expired, show AUTOSAVE WARNING
|
||||||
|
if(this.state.isPending && this.state.autoSaveWarning){
|
||||||
this.setAutosaveWarning();
|
this.setAutosaveWarning();
|
||||||
const elapsedTime = Math.round((new Date() - this.state.unsavedTime) / 1000 / 60);
|
const elapsedTime = Math.round((new Date() - this.state.unsavedTime) / 1000 / 60);
|
||||||
const text = elapsedTime == 0 ? 'Autosave is OFF.' : `Autosave is OFF, and you haven't saved for ${elapsedTime} minutes.`;
|
const text = elapsedTime == 0 ? 'Autosave is OFF.' : `Autosave is OFF, and you haven't saved for ${elapsedTime} minutes.`;
|
||||||
@@ -324,18 +337,17 @@ const EditPage = createClass({
|
|||||||
</Nav.item>;
|
</Nav.item>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(this.state.isSaving){
|
// #3 - Unsaved changes exist, click to save, show SAVE NOW
|
||||||
return <Nav.item className='save' icon='fas fa-spinner fa-spin'>saving...</Nav.item>;
|
// Use trySave(true) instead of save() to use debounced save function
|
||||||
|
if(this.state.isPending){
|
||||||
|
return <Nav.item className='save' onClick={()=>this.trySave(true)} color='blue' icon='fas fa-save'>Save Now</Nav.item>;
|
||||||
}
|
}
|
||||||
if(this.state.isPending && this.hasChanges()){
|
// #4 - No unsaved changes, autosave is ON, show AUTO-SAVED
|
||||||
return <Nav.item className='save' onClick={this.save} color='blue' icon='fas fa-save'>Save Now</Nav.item>;
|
if(this.state.autoSave){
|
||||||
}
|
|
||||||
if(!this.state.isPending && !this.state.isSaving && this.state.autoSave){
|
|
||||||
return <Nav.item className='save saved'>auto-saved.</Nav.item>;
|
return <Nav.item className='save saved'>auto-saved.</Nav.item>;
|
||||||
}
|
}
|
||||||
if(!this.state.isPending && !this.state.isSaving){
|
// DEFAULT - No unsaved changes, show SAVED
|
||||||
return <Nav.item className='save saved'>saved.</Nav.item>;
|
return <Nav.item className='save saved'>saved.</Nav.item>;
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
handleAutoSave : function(){
|
handleAutoSave : function(){
|
||||||
@@ -379,9 +391,9 @@ const EditPage = createClass({
|
|||||||
const title = `${this.props.brew.title} ${systems}`;
|
const title = `${this.props.brew.title} ${systems}`;
|
||||||
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](${global.config.publicUrl}/share/${shareLink})**`;
|
**[Homebrewery Link](${global.config.baseUrl}/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.toWellFormed())}&text=${encodeURIComponent(text)}`;
|
||||||
},
|
},
|
||||||
|
|
||||||
renderNavbar : function(){
|
renderNavbar : function(){
|
||||||
@@ -410,7 +422,7 @@ const EditPage = createClass({
|
|||||||
<Nav.item color='blue' href={`/share/${shareLink}`}>
|
<Nav.item color='blue' href={`/share/${shareLink}`}>
|
||||||
view
|
view
|
||||||
</Nav.item>
|
</Nav.item>
|
||||||
<Nav.item color='blue' onClick={()=>{navigator.clipboard.writeText(`${global.config.publicUrl}/share/${shareLink}`);}}>
|
<Nav.item color='blue' onClick={()=>{navigator.clipboard.writeText(`${global.config.baseUrl}/share/${shareLink}`);}}>
|
||||||
copy url
|
copy url
|
||||||
</Nav.item>
|
</Nav.item>
|
||||||
<Nav.item color='blue' href={this.getRedditLink()} newTab={true} rel='noopener noreferrer'>
|
<Nav.item color='blue' href={this.getRedditLink()} newTab={true} rel='noopener noreferrer'>
|
||||||
@@ -432,7 +444,7 @@ const EditPage = createClass({
|
|||||||
{this.renderNavbar()}
|
{this.renderNavbar()}
|
||||||
|
|
||||||
{this.props.brew.lock && <LockNotification shareId={this.props.brew.shareId} message={this.props.brew.lock.editMessage} />}
|
{this.props.brew.lock && <LockNotification shareId={this.props.brew.shareId} message={this.props.brew.lock.editMessage} />}
|
||||||
<div className="content">
|
<div className='content'>
|
||||||
<SplitPane onDragFinish={this.handleSplitMove}>
|
<SplitPane onDragFinish={this.handleSplitMove}>
|
||||||
<Editor
|
<Editor
|
||||||
ref={this.editor}
|
ref={this.editor}
|
||||||
@@ -443,6 +455,7 @@ const EditPage = createClass({
|
|||||||
reportError={this.errorReported}
|
reportError={this.errorReported}
|
||||||
renderer={this.state.brew.renderer}
|
renderer={this.state.brew.renderer}
|
||||||
userThemes={this.props.userThemes}
|
userThemes={this.props.userThemes}
|
||||||
|
themeBundle={this.state.themeBundle}
|
||||||
snippetBundle={this.state.themeBundle.snippets}
|
snippetBundle={this.state.themeBundle.snippets}
|
||||||
updateBrew={this.updateBrew}
|
updateBrew={this.updateBrew}
|
||||||
onCursorPageChange={this.handleEditorCursorPageChange}
|
onCursorPageChange={this.handleEditorCursorPageChange}
|
||||||
|
|||||||
@@ -2,6 +2,11 @@ const dedent = require('dedent-tabs').default;
|
|||||||
|
|
||||||
const loginUrl = 'https://www.naturalcrit.com/login';
|
const loginUrl = 'https://www.naturalcrit.com/login';
|
||||||
|
|
||||||
|
// Prevent parsing text (e.g. document titles) as markdown
|
||||||
|
const escape = (text = '')=>{
|
||||||
|
return text.split('').map((char)=>`&#${char.charCodeAt(0)};`).join('');
|
||||||
|
};
|
||||||
|
|
||||||
//001-050 : Brew errors
|
//001-050 : Brew errors
|
||||||
//050-100 : Other pages errors
|
//050-100 : Other pages errors
|
||||||
|
|
||||||
@@ -18,7 +23,18 @@ const errorIndex = (props)=>{
|
|||||||
'01' : dedent`
|
'01' : dedent`
|
||||||
## An error occurred while retrieving this brew from Google Drive!
|
## An error occurred while retrieving this brew from Google Drive!
|
||||||
|
|
||||||
Google reported an error while attempting to retrieve a brew from this link.`,
|
Google is able to see the brew at this link, but reported an error while attempting to retrieve it.
|
||||||
|
|
||||||
|
### Refreshing your Google Credentials
|
||||||
|
|
||||||
|
This issue is likely caused by an issue with your Google credentials; if you are the owner of this file, the following steps may resolve the issue:
|
||||||
|
|
||||||
|
- Go to https://www.naturalcrit.com/login and click logout if present (in small text at the bottom of the page).
|
||||||
|
- Click "Sign In with Google", which will refresh your Google credentials.
|
||||||
|
- After completing the sign in process, return to Homebrewery and refresh/reload the page so that it can pick up the updated credentials.
|
||||||
|
- If this was the source of the issue, it should now be resolved.
|
||||||
|
|
||||||
|
If following these steps does not resolve the issue, please let us know!`,
|
||||||
|
|
||||||
// Google Drive - 404 : brew deleted or access denied
|
// Google Drive - 404 : brew deleted or access denied
|
||||||
'02' : dedent`
|
'02' : dedent`
|
||||||
@@ -50,7 +66,7 @@ const errorIndex = (props)=>{
|
|||||||
- **The Google Account may be closed.** Google may have removed the account
|
- **The Google Account may be closed.** Google may have removed the account
|
||||||
due to inactivity or violating a Google policy. Make sure the owner can
|
due to inactivity or violating a Google policy. Make sure the owner can
|
||||||
still access Google Drive normally and upload/download files to it.
|
still access Google Drive normally and upload/download files to it.
|
||||||
:
|
|
||||||
If the file isn't found, Google Drive usually puts your file in your Trash folder for
|
If the file isn't found, Google Drive usually puts your file in your Trash folder for
|
||||||
30 days. Assuming the trash hasn't been emptied yet, it might be worth checking.
|
30 days. Assuming the trash hasn't been emptied yet, it might be worth checking.
|
||||||
You can also find the Activity tab on the right side of the Google Drive page, which
|
You can also find the Activity tab on the right side of the Google Drive page, which
|
||||||
@@ -78,7 +94,7 @@ const errorIndex = (props)=>{
|
|||||||
|
|
||||||
:
|
:
|
||||||
|
|
||||||
**Brew Title:** ${props.brew.brewTitle || 'Unable to show title'}
|
**Brew Title:** ${escape(props.brew.brewTitle) || 'Unable to show title'}
|
||||||
|
|
||||||
**Current Authors:** ${props.brew.authors?.map((author)=>{return `[${author}](/user/${author})`;}).join(', ') || 'Unable to list authors'}
|
**Current Authors:** ${props.brew.authors?.map((author)=>{return `[${author}](/user/${author})`;}).join(', ') || 'Unable to list authors'}
|
||||||
|
|
||||||
@@ -93,7 +109,7 @@ const errorIndex = (props)=>{
|
|||||||
|
|
||||||
:
|
:
|
||||||
|
|
||||||
**Brew Title:** ${props.brew.brewTitle || 'Unable to show title'}
|
**Brew Title:** ${escape(props.brew.brewTitle) || 'Unable to show title'}
|
||||||
|
|
||||||
**Current Authors:** ${props.brew.authors?.map((author)=>{return `[${author}](/user/${author})`;}).join(', ') || 'Unable to list authors'}
|
**Current Authors:** ${props.brew.authors?.map((author)=>{return `[${author}](/user/${author})`;}).join(', ') || 'Unable to list authors'}
|
||||||
|
|
||||||
@@ -152,6 +168,14 @@ const errorIndex = (props)=>{
|
|||||||
|
|
||||||
**Brew ID:** ${props.brew.brewId}`,
|
**Brew ID:** ${props.brew.brewId}`,
|
||||||
|
|
||||||
|
// Theme Not Valid
|
||||||
|
'10' : dedent`
|
||||||
|
## The selected theme is not tagged as a theme.
|
||||||
|
|
||||||
|
The brew selected as a theme exists, but has not been marked for use as a theme with the \`theme:meta\` tag.
|
||||||
|
|
||||||
|
If the selected brew is your document, you may designate it as a theme by adding the \`theme:meta\` tag.`,
|
||||||
|
|
||||||
//account page when account is not defined
|
//account page when account is not defined
|
||||||
'50' : dedent`
|
'50' : dedent`
|
||||||
## You are not signed in
|
## You are not signed in
|
||||||
@@ -170,10 +194,10 @@ const errorIndex = (props)=>{
|
|||||||
|
|
||||||
**Brew ID:** ${props.brew.brewId}
|
**Brew ID:** ${props.brew.brewId}
|
||||||
|
|
||||||
**Brew Title:** ${props.brew.brewTitle}`,
|
**Brew Title:** ${escape(props.brew.brewTitle)}`,
|
||||||
|
|
||||||
// ####### Admin page error #######
|
// ####### Admin page error #######
|
||||||
'52': dedent`
|
'52' : dedent`
|
||||||
## Access Denied
|
## Access Denied
|
||||||
You need to provide correct administrator credentials to access this page.`,
|
You need to provide correct administrator credentials to access this page.`,
|
||||||
|
|
||||||
|
|||||||
@@ -233,6 +233,7 @@ const NewPage = createClass({
|
|||||||
onMetaChange={this.handleMetaChange}
|
onMetaChange={this.handleMetaChange}
|
||||||
renderer={this.state.brew.renderer}
|
renderer={this.state.brew.renderer}
|
||||||
userThemes={this.props.userThemes}
|
userThemes={this.props.userThemes}
|
||||||
|
themeBundle={this.state.themeBundle}
|
||||||
snippetBundle={this.state.themeBundle.snippets}
|
snippetBundle={this.state.themeBundle.snippets}
|
||||||
onCursorPageChange={this.handleEditorCursorPageChange}
|
onCursorPageChange={this.handleEditorCursorPageChange}
|
||||||
onViewPageChange={this.handleEditorViewPageChange}
|
onViewPageChange={this.handleEditorViewPageChange}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
require('./sharePage.less');
|
require('./sharePage.less');
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const createClass = require('create-react-class');
|
const { useState, useEffect, useCallback } = React;
|
||||||
const { Meta } = require('vitreum/headtags');
|
const { Meta } = require('vitreum/headtags');
|
||||||
|
|
||||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||||
@@ -8,130 +8,120 @@ const Navbar = require('../../navbar/navbar.jsx');
|
|||||||
const MetadataNav = require('../../navbar/metadata.navitem.jsx');
|
const MetadataNav = require('../../navbar/metadata.navitem.jsx');
|
||||||
const PrintNavItem = require('../../navbar/print.navitem.jsx');
|
const PrintNavItem = require('../../navbar/print.navitem.jsx');
|
||||||
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
||||||
const VaultNavItem = require('../../navbar/vault.navitem.jsx');
|
|
||||||
const Account = require('../../navbar/account.navitem.jsx');
|
const Account = require('../../navbar/account.navitem.jsx');
|
||||||
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
||||||
|
|
||||||
const { DEFAULT_BREW_LOAD } = require('../../../../server/brewDefaults.js');
|
const { DEFAULT_BREW_LOAD } = require('../../../../server/brewDefaults.js');
|
||||||
const { printCurrentBrew, fetchThemeBundle } = require('../../../../shared/helpers.js');
|
const { printCurrentBrew, fetchThemeBundle } = require('../../../../shared/helpers.js');
|
||||||
|
|
||||||
const SharePage = createClass({
|
const SharePage = (props)=>{
|
||||||
displayName : 'SharePage',
|
const { brew = DEFAULT_BREW_LOAD, disableMeta = false } = props;
|
||||||
getDefaultProps : function() {
|
|
||||||
return {
|
|
||||||
brew : DEFAULT_BREW_LOAD,
|
|
||||||
disableMeta : false
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
getInitialState : function() {
|
const [state, setState] = useState({
|
||||||
return {
|
|
||||||
themeBundle : {},
|
themeBundle : {},
|
||||||
currentBrewRendererPageNum : 1
|
currentBrewRendererPageNum : 1,
|
||||||
};
|
});
|
||||||
},
|
|
||||||
|
|
||||||
componentDidMount : function() {
|
const handleBrewRendererPageChange = useCallback((pageNumber)=>{
|
||||||
document.addEventListener('keydown', this.handleControlKeys);
|
setState((prevState)=>({
|
||||||
|
currentBrewRendererPageNum : pageNumber,
|
||||||
|
...prevState }));
|
||||||
|
}, []);
|
||||||
|
|
||||||
fetchThemeBundle(this, this.props.brew.renderer, this.props.brew.theme);
|
const handleControlKeys = (e)=>{
|
||||||
},
|
|
||||||
|
|
||||||
componentWillUnmount : function() {
|
|
||||||
document.removeEventListener('keydown', this.handleControlKeys);
|
|
||||||
},
|
|
||||||
|
|
||||||
handleBrewRendererPageChange : function(pageNumber){
|
|
||||||
this.setState({ currentBrewRendererPageNum: pageNumber });
|
|
||||||
},
|
|
||||||
|
|
||||||
handleControlKeys : function(e){
|
|
||||||
if(!(e.ctrlKey || e.metaKey)) return;
|
if(!(e.ctrlKey || e.metaKey)) return;
|
||||||
const P_KEY = 80;
|
const P_KEY = 80;
|
||||||
if(e.keyCode == P_KEY){
|
if(e.keyCode === P_KEY) {
|
||||||
if(e.keyCode == P_KEY) printCurrentBrew();
|
printCurrentBrew();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
},
|
};
|
||||||
|
|
||||||
processShareId : function() {
|
useEffect(()=>{
|
||||||
return this.props.brew.googleId && !this.props.brew.stubbed ?
|
document.addEventListener('keydown', handleControlKeys);
|
||||||
this.props.brew.googleId + this.props.brew.shareId :
|
fetchThemeBundle(
|
||||||
this.props.brew.shareId;
|
{ setState },
|
||||||
},
|
brew.renderer,
|
||||||
|
brew.theme
|
||||||
|
);
|
||||||
|
|
||||||
renderEditLink : function(){
|
return ()=>{
|
||||||
if(!this.props.brew.editId) return;
|
document.removeEventListener('keydown', handleControlKeys);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
let editLink = this.props.brew.editId;
|
const processShareId = ()=>{
|
||||||
if(this.props.brew.googleId && !this.props.brew.stubbed) {
|
return brew.googleId && !brew.stubbed ? brew.googleId + brew.shareId : brew.shareId;
|
||||||
editLink = this.props.brew.googleId + editLink;
|
};
|
||||||
}
|
|
||||||
|
|
||||||
return <Nav.item color='orange' icon='fas fa-pencil-alt' href={`/edit/${editLink}`}>
|
const renderEditLink = ()=>{
|
||||||
|
if(!brew.editId) return null;
|
||||||
|
|
||||||
|
const editLink = brew.googleId && ! brew.stubbed ? brew.googleId + brew.editId : brew.editId;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Nav.item color='orange' icon='fas fa-pencil-alt' href={`/edit/${editLink}`}>
|
||||||
edit
|
edit
|
||||||
</Nav.item>;
|
</Nav.item>
|
||||||
},
|
);
|
||||||
|
};
|
||||||
|
|
||||||
render : function(){
|
const titleEl = (
|
||||||
const titleStyle = this.props.disableMeta ? { cursor: 'default' } : {};
|
<Nav.item className='brewTitle' style={disableMeta ? { cursor: 'default' } : {}}>
|
||||||
const titleEl = <Nav.item className='brewTitle' style={titleStyle}>{this.props.brew.title}</Nav.item>;
|
{brew.title}
|
||||||
|
</Nav.item>
|
||||||
|
);
|
||||||
|
|
||||||
return <div className='sharePage sitePage'>
|
return (
|
||||||
|
<div className='sharePage sitePage'>
|
||||||
<Meta name='robots' content='noindex, nofollow' />
|
<Meta name='robots' content='noindex, nofollow' />
|
||||||
<Navbar>
|
<Navbar>
|
||||||
<Nav.section className='titleSection'>
|
<Nav.section className='titleSection'>
|
||||||
{
|
{disableMeta ? titleEl : <MetadataNav brew={brew}>{titleEl}</MetadataNav>}
|
||||||
this.props.disableMeta ?
|
|
||||||
titleEl
|
|
||||||
:
|
|
||||||
<MetadataNav brew={this.props.brew}>
|
|
||||||
{titleEl}
|
|
||||||
</MetadataNav>
|
|
||||||
}
|
|
||||||
</Nav.section>
|
</Nav.section>
|
||||||
|
|
||||||
<Nav.section>
|
<Nav.section>
|
||||||
{this.props.brew.shareId && <>
|
{brew.shareId && (
|
||||||
<PrintNavItem/>
|
<>
|
||||||
|
<PrintNavItem />
|
||||||
<Nav.dropdown>
|
<Nav.dropdown>
|
||||||
<Nav.item color='red' icon='fas fa-code'>
|
<Nav.item color='red' icon='fas fa-code'>
|
||||||
source
|
source
|
||||||
</Nav.item>
|
</Nav.item>
|
||||||
<Nav.item color='blue' icon='fas fa-eye' href={`/source/${this.processShareId()}`}>
|
<Nav.item color='blue' icon='fas fa-eye' href={`/source/${processShareId()}`}>
|
||||||
view
|
view
|
||||||
</Nav.item>
|
</Nav.item>
|
||||||
{this.renderEditLink()}
|
{renderEditLink()}
|
||||||
<Nav.item color='blue' icon='fas fa-download' href={`/download/${this.processShareId()}`}>
|
<Nav.item color='blue' icon='fas fa-download' href={`/download/${processShareId()}`}>
|
||||||
download
|
download
|
||||||
</Nav.item>
|
</Nav.item>
|
||||||
<Nav.item color='blue' icon='fas fa-clone' href={`/new/${this.processShareId()}`}>
|
<Nav.item color='blue' icon='fas fa-clone' href={`/new/${processShareId()}`}>
|
||||||
clone to new
|
clone to new
|
||||||
</Nav.item>
|
</Nav.item>
|
||||||
</Nav.dropdown>
|
</Nav.dropdown>
|
||||||
</>}
|
</>
|
||||||
<VaultNavItem/>
|
)}
|
||||||
<RecentNavItem brew={this.props.brew} storageKey='view' />
|
<RecentNavItem brew={brew} storageKey='view' />
|
||||||
<Account />
|
<Account />
|
||||||
</Nav.section>
|
</Nav.section>
|
||||||
</Navbar>
|
</Navbar>
|
||||||
|
|
||||||
<div className='content'>
|
<div className='content'>
|
||||||
<BrewRenderer
|
<BrewRenderer
|
||||||
text={this.props.brew.text}
|
text={brew.text}
|
||||||
style={this.props.brew.style}
|
style={brew.style}
|
||||||
lang={this.props.brew.lang}
|
lang={brew.lang}
|
||||||
renderer={this.props.brew.renderer}
|
renderer={brew.renderer}
|
||||||
theme={this.props.brew.theme}
|
theme={brew.theme}
|
||||||
themeBundle={this.state.themeBundle}
|
themeBundle={state.themeBundle}
|
||||||
onPageChange={this.handleBrewRendererPageChange}
|
onPageChange={handleBrewRendererPageChange}
|
||||||
currentBrewRendererPageNum={this.state.currentBrewRendererPageNum}
|
currentBrewRendererPageNum={state.currentBrewRendererPageNum}
|
||||||
allowPrint={true}
|
allowPrint={true}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>
|
||||||
}
|
);
|
||||||
});
|
};
|
||||||
|
|
||||||
module.exports = SharePage;
|
module.exports = SharePage;
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
version: '2'
|
|
||||||
services:
|
services:
|
||||||
mongodb:
|
mongodb:
|
||||||
image: mongo:latest
|
image: mongo:latest
|
||||||
|
|||||||
@@ -24,12 +24,16 @@ These instructions assume that you are installing to a completely new, fresh Ubu
|
|||||||
|
|
||||||
These installation instructions have been tested on the following Ubuntu releases:
|
These installation instructions have been tested on the following Ubuntu releases:
|
||||||
|
|
||||||
- *ubuntu-20.04.3-desktop-amd64*
|
- *ubuntu-24.04.1-desktop-amd64*
|
||||||
|
- *ubuntu-22.04.5-desktop-amd64*
|
||||||
|
- *ubuntu-20.04.6-desktop-amd64*
|
||||||
|
|
||||||
## Final Notes
|
## Final Notes
|
||||||
|
|
||||||
While this installation process works successfully at the time of writing (December 19, 2021), it relies on all of the Node.JS packages used in the HomeBrewery project retaining their cross-platform capabilities to continue to function. This is one of the inherent advantages of Node.JS, but it is by no means guaranteed and as such, functionality or even installation may fail without warning at some point in the future.
|
While this installation process works successfully at the time of writing (December 19, 2021), it relies on all of the Node.JS packages used in the HomeBrewery project retaining their cross-platform capabilities to continue to function. This is one of the inherent advantages of Node.JS, but it is by no means guaranteed and as such, functionality or even installation may fail without warning at some point in the future.
|
||||||
|
|
||||||
|
Earlier versions of Ubuntu may requier an alternate Mongo setup, see https://www.mongodb.com/docs/manual/tutorial/install-mongodb-on-ubuntu/ for assistance.
|
||||||
|
|
||||||
Regards,
|
Regards,
|
||||||
G
|
G
|
||||||
December 19, 2021
|
December 19, 2021
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ Description=Homebrewery Web Server
|
|||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
User=root
|
User=root
|
||||||
After=mongodb
|
BindsTo=mongod.service
|
||||||
|
After=mongod.service
|
||||||
Environment=NODE_ENV=local
|
Environment=NODE_ENV=local
|
||||||
WorkingDirectory=/usr/local/homebrewery
|
WorkingDirectory=/usr/local/homebrewery
|
||||||
ExecStart=node server.js
|
ExecStart=node server.js
|
||||||
|
|||||||
@@ -1,14 +1,60 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
|
# Detect Ubuntu Version
|
||||||
|
export DISTRO=$(grep "^NAME=" /etc/os-release | awk -F '=' '{print $2}' | sed 's/"//g')
|
||||||
|
export DISTRO_VER=$(grep "VERSION_ID=" /etc/os-release | awk -F '=' '{print $2}' | sed 's/"//g')
|
||||||
|
export MATCHED="Yes"
|
||||||
|
|
||||||
|
if [ "${DISTRO}" != "Ubuntu" ];
|
||||||
|
then
|
||||||
|
echo :: Ubuntu not detected. Are you using an alternate spin or derivative?
|
||||||
|
echo :: Detected - ${DISTRO}
|
||||||
|
read -p [y/N] YESNO
|
||||||
|
if [ "${YESNO}" != "Y" ] && [ ]"${YESNO}" != "y" ]; then
|
||||||
|
exit
|
||||||
|
fi
|
||||||
|
|
||||||
|
MATCHED="No"
|
||||||
|
fi
|
||||||
|
|
||||||
# Install CURL and add required NodeJS source to package repo
|
# Install CURL and add required NodeJS source to package repo
|
||||||
echo ::Install CURL
|
echo ::Install CURL
|
||||||
apt install -y curl
|
apt install -y curl
|
||||||
echo ::Add NodeJS source to package repo
|
echo ::Add NodeJS source to package repo
|
||||||
curl -fsSL https://deb.nodesource.com/setup_16.x | sudo -E bash -
|
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
|
||||||
|
|
||||||
|
# Add Mongo CE Source
|
||||||
|
if [ ${DISTRO} = "Ubuntu" ];
|
||||||
|
then
|
||||||
|
echo ::Add Mongo CE source to package repo
|
||||||
|
curl -fsSL https://www.mongodb.org/static/pgp/server-8.0.asc | \
|
||||||
|
sudo gpg -o /usr/share/keyrings/mongodb-server-8.0.gpg \
|
||||||
|
--dearmor
|
||||||
|
if [ "${DISTRO_VER}" == "24.04" ]; then
|
||||||
|
echo "deb [ arch=amd64,arm64 signed-by=/usr/share/keyrings/mongodb-server-8.0.gpg ] https://repo.mongodb.org/apt/ubuntu noble/mongodb-org/8.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-8.0.list
|
||||||
|
elif [ "${DISTRO_VER}" == "22.04" ]; then
|
||||||
|
echo "deb [ arch=amd64,arm64 signed-by=/usr/share/keyrings/mongodb-server-8.0.gpg ] https://repo.mongodb.org/apt/ubuntu jammy/mongodb-org/8.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-8.0.list
|
||||||
|
elif [ "${DISTRO_VER}" == "20.04" ]; then
|
||||||
|
echo "deb [ arch=amd64,arm64 signed-by=/usr/share/keyrings/mongodb-server-8.0.gpg ] https://repo.mongodb.org/apt/ubuntu focal/mongodb-org/8.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-8.0.list
|
||||||
|
else
|
||||||
|
MATCHED="No"
|
||||||
|
fi
|
||||||
|
sudo apt-get update
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ${MATCHED} == "No" ]; then
|
||||||
|
echo :: WARNING
|
||||||
|
echo :: Unable to determine Ubuntu version for Mongo installation purposes.
|
||||||
|
echo :: Please check your spin/distro documentation to install Mongo CE and enable it on startup.
|
||||||
|
fi
|
||||||
|
|
||||||
# Install required packages
|
# Install required packages
|
||||||
echo ::Install Homebrewery requirements
|
echo ::Install Homebrewery requirements
|
||||||
apt satisfy -y git nodejs npm mongodb
|
apt satisfy -y git nodejs npm mongodb-org
|
||||||
|
|
||||||
|
# Enable and start Mongo
|
||||||
|
systemctl enable mongod
|
||||||
|
systemctl start mongod
|
||||||
|
|
||||||
# Clone Homebrewery repo
|
# Clone Homebrewery repo
|
||||||
echo ::Get Homebrewery files
|
echo ::Get Homebrewery files
|
||||||
|
|||||||
2834
package-lock.json
generated
2834
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
53
package.json
53
package.json
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "homebrewery",
|
"name": "homebrewery",
|
||||||
"description": "Create authentic looking D&D homebrews using only markdown",
|
"description": "Create authentic looking D&D homebrews using only markdown",
|
||||||
"version": "3.16.1",
|
"version": "3.18.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"engines": {
|
"engines": {
|
||||||
"npm": "^10.2.x",
|
"npm": "^10.2.x",
|
||||||
@@ -27,6 +27,7 @@
|
|||||||
"test:api-unit:themes": "jest \"server/.*.spec.js\" -t \"theme bundle\" --verbose",
|
"test:api-unit:themes": "jest \"server/.*.spec.js\" -t \"theme bundle\" --verbose",
|
||||||
"test:api-unit:css": "jest \"server/.*.spec.js\" -t \"Get CSS\" --verbose",
|
"test:api-unit:css": "jest \"server/.*.spec.js\" -t \"Get CSS\" --verbose",
|
||||||
"test:api-unit:notifications": "jest \"server/.*.spec.js\" -t \"Notifications\" --verbose",
|
"test:api-unit:notifications": "jest \"server/.*.spec.js\" -t \"Notifications\" --verbose",
|
||||||
|
"test:content-negotiation": "jest \"server/middleware/.*.spec.js\" --verbose",
|
||||||
"test:coverage": "jest --coverage --silent --runInBand",
|
"test:coverage": "jest --coverage --silent --runInBand",
|
||||||
"test:dev": "jest --verbose --watch",
|
"test:dev": "jest --verbose --watch",
|
||||||
"test:basic": "jest tests/markdown/basic.test.js --verbose",
|
"test:basic": "jest tests/markdown/basic.test.js --verbose",
|
||||||
@@ -37,6 +38,7 @@
|
|||||||
"test:mustache-syntax:injection": "jest \".*(mustache-syntax).*\" -t '^Injection:.*' --verbose --noStackTrace",
|
"test:mustache-syntax:injection": "jest \".*(mustache-syntax).*\" -t '^Injection:.*' --verbose --noStackTrace",
|
||||||
"test:definition-lists": "jest tests/markdown/definition-lists.test.js --verbose --noStackTrace",
|
"test:definition-lists": "jest tests/markdown/definition-lists.test.js --verbose --noStackTrace",
|
||||||
"test:hard-breaks": "jest tests/markdown/hard-breaks.test.js --verbose --noStackTrace",
|
"test:hard-breaks": "jest tests/markdown/hard-breaks.test.js --verbose --noStackTrace",
|
||||||
|
"test:non-breaking-spaces": "jest tests/markdown/non-breaking-spaces.test.js --verbose --noStackTrace",
|
||||||
"test:emojis": "jest tests/markdown/emojis.test.js --verbose --noStackTrace",
|
"test:emojis": "jest tests/markdown/emojis.test.js --verbose --noStackTrace",
|
||||||
"test:route": "jest tests/routes/static-pages.test.js --verbose",
|
"test:route": "jest tests/routes/static-pages.test.js --verbose",
|
||||||
"test:safehtml": "jest tests/html/safeHTML.test.js --verbose",
|
"test:safehtml": "jest tests/html/safeHTML.test.js --verbose",
|
||||||
@@ -82,60 +84,63 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/core": "^7.26.0",
|
"@babel/core": "^7.26.9",
|
||||||
"@babel/plugin-transform-runtime": "^7.25.9",
|
"@babel/plugin-transform-runtime": "^7.26.9",
|
||||||
"@babel/preset-env": "^7.26.0",
|
"@babel/preset-env": "^7.26.9",
|
||||||
"@babel/preset-react": "^7.26.3",
|
"@babel/preset-react": "^7.26.3",
|
||||||
"@googleapis/drive": "^8.14.0",
|
"@googleapis/drive": "^8.16.0",
|
||||||
"body-parser": "^1.20.2",
|
"body-parser": "^1.20.2",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
"codemirror": "^5.65.6",
|
"codemirror": "^5.65.6",
|
||||||
"cookie-parser": "^1.4.7",
|
"cookie-parser": "^1.4.7",
|
||||||
|
"core-js": "^3.41.0",
|
||||||
|
"cors": "^2.8.5",
|
||||||
"create-react-class": "^15.7.0",
|
"create-react-class": "^15.7.0",
|
||||||
"dedent-tabs": "^0.10.3",
|
"dedent-tabs": "^0.10.3",
|
||||||
"dompurify": "^3.2.2",
|
"dompurify": "^3.2.4",
|
||||||
"expr-eval": "^2.0.2",
|
"expr-eval": "^2.0.2",
|
||||||
"express": "^4.21.2",
|
"express": "^4.21.2",
|
||||||
"express-async-handler": "^1.2.0",
|
"express-async-handler": "^1.2.0",
|
||||||
"express-static-gzip": "2.2.0",
|
"express-static-gzip": "2.2.0",
|
||||||
"fs-extra": "11.2.0",
|
"fs-extra": "11.3.0",
|
||||||
"idb-keyval": "^6.2.1",
|
"idb-keyval": "^6.2.1",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"jwt-simple": "^0.5.6",
|
"jwt-simple": "^0.5.6",
|
||||||
"less": "^3.13.1",
|
"less": "^3.13.1",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"marked": "11.2.0",
|
"marked": "14.0.0",
|
||||||
"marked-emoji": "^1.4.3",
|
"marked-emoji": "^2.0.0",
|
||||||
"marked-extended-tables": "^1.0.10",
|
"marked-extended-tables": "^2.0.0",
|
||||||
"marked-gfm-heading-id": "^3.2.0",
|
"marked-gfm-heading-id": "^4.0.1",
|
||||||
"marked-smartypants-lite": "^1.0.2",
|
"marked-smartypants-lite": "^1.0.3",
|
||||||
|
"marked-subsuper-text": "^1.0.3",
|
||||||
"markedLegacy": "npm:marked@^0.3.19",
|
"markedLegacy": "npm:marked@^0.3.19",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
"mongoose": "^8.8.3",
|
"mongoose": "^8.12.1",
|
||||||
"nanoid": "5.0.9",
|
"nanoid": "5.1.2",
|
||||||
"nconf": "^0.12.1",
|
"nconf": "^0.12.1",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-frame-component": "^4.1.3",
|
"react-frame-component": "^4.1.3",
|
||||||
"react-router-dom": "6.28.0",
|
"react-router": "^7.3.0",
|
||||||
"sanitize-filename": "1.6.3",
|
"sanitize-filename": "1.6.3",
|
||||||
"superagent": "^10.1.1",
|
"superagent": "^10.1.1",
|
||||||
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git"
|
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@stylistic/stylelint-plugin": "^3.1.1",
|
"@stylistic/stylelint-plugin": "^3.1.2",
|
||||||
"babel-plugin-transform-import-meta": "^2.2.1",
|
"babel-plugin-transform-import-meta": "^2.3.2",
|
||||||
"eslint": "^9.16.0",
|
"eslint": "^9.22.0",
|
||||||
"eslint-plugin-jest": "^28.9.0",
|
"eslint-plugin-jest": "^28.11.0",
|
||||||
"eslint-plugin-react": "^7.37.2",
|
"eslint-plugin-react": "^7.37.4",
|
||||||
"globals": "^15.13.0",
|
"globals": "^16.0.0",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"jest-expect-message": "^1.1.3",
|
"jest-expect-message": "^1.1.3",
|
||||||
"jsdom-global": "^3.0.2",
|
"jsdom-global": "^3.0.2",
|
||||||
"postcss-less": "^6.0.0",
|
"postcss-less": "^6.0.0",
|
||||||
"stylelint": "^16.11.0",
|
"stylelint": "^16.15.0",
|
||||||
"stylelint-config-recess-order": "^5.1.1",
|
"stylelint-config-recess-order": "^6.0.0",
|
||||||
"stylelint-config-recommended": "^14.0.1",
|
"stylelint-config-recommended": "^15.0.0",
|
||||||
"supertest": "^7.0.0"
|
"supertest": "^7.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,11 +7,12 @@ const { pack, watchFile, livereload } = vitreum;
|
|||||||
import lessTransform from 'vitreum/transforms/less.js';
|
import lessTransform from 'vitreum/transforms/less.js';
|
||||||
import assetTransform from 'vitreum/transforms/asset.js';
|
import assetTransform from 'vitreum/transforms/asset.js';
|
||||||
import babel from '@babel/core';
|
import babel from '@babel/core';
|
||||||
|
import babelConfig from '../babel.config.json' with { type : 'json' };
|
||||||
import less from 'less';
|
import less from 'less';
|
||||||
|
|
||||||
const isDev = !!process.argv.find((arg) => arg === '--dev');
|
const isDev = !!process.argv.find((arg) => arg === '--dev');
|
||||||
|
|
||||||
const babelify = async (code)=>(await babel.transformAsync(code, { presets: [['@babel/preset-env', { 'exclude': ['proposal-dynamic-import'] }], '@babel/preset-react'], plugins: ['@babel/plugin-transform-runtime'] })).code;
|
const babelify = async (code)=>(await babel.transformAsync(code, babelConfig)).code;
|
||||||
|
|
||||||
const transforms = {
|
const transforms = {
|
||||||
'.js' : (code, filename, opts)=>babelify(code),
|
'.js' : (code, filename, opts)=>babelify(code),
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {model as HomebrewModel } from './homebrew.model.js';
|
import { model as HomebrewModel } from './homebrew.model.js';
|
||||||
import {model as NotificationModel } from './notifications.model.js';
|
import { model as NotificationModel } from './notifications.model.js';
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import Moment from 'moment';
|
import Moment from 'moment';
|
||||||
import zlib from 'zlib';
|
import zlib from 'zlib';
|
||||||
@@ -93,7 +93,7 @@ router.get('/admin/finduncompressed', mw.adminOnly, (req, res)=>{
|
|||||||
|
|
||||||
/* Cleans `<script` and `</script>` from the "text" field of a brew */
|
/* Cleans `<script` and `</script>` from the "text" field of a brew */
|
||||||
router.put('/admin/clean/script/:id', asyncHandler(HomebrewAPI.getBrew('admin', false)), async (req, res)=>{
|
router.put('/admin/clean/script/:id', asyncHandler(HomebrewAPI.getBrew('admin', false)), async (req, res)=>{
|
||||||
console.log(`[ADMIN] Cleaning script tags from ShareID ${req.params.id}`);
|
console.log(`[ADMIN: ${req.account?.username || 'Not Logged In'}] Cleaning script tags from ShareID ${req.params.id}`);
|
||||||
|
|
||||||
function cleanText(text){return text.replaceAll(/(<\/?s)cript/gi, '');};
|
function cleanText(text){return text.replaceAll(/(<\/?s)cript/gi, '');};
|
||||||
|
|
||||||
@@ -108,9 +108,24 @@ router.put('/admin/clean/script/:id', asyncHandler(HomebrewAPI.getBrew('admin',
|
|||||||
|
|
||||||
req.body = brew;
|
req.body = brew;
|
||||||
|
|
||||||
|
// Remove Account from request to prevent Admin user from being added to brew as an Author
|
||||||
|
req.account = undefined;
|
||||||
|
|
||||||
return await HomebrewAPI.updateBrew(req, res);
|
return await HomebrewAPI.updateBrew(req, res);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/* Get list of a user's documents */
|
||||||
|
router.get('/admin/user/list/:user', mw.adminOnly, async (req, res)=>{
|
||||||
|
const username = req.params.user;
|
||||||
|
const fields = { _id: 0, text: 0, textBin: 0 }; // Remove unnecessary fields from document lists
|
||||||
|
|
||||||
|
console.log(`[ADMIN: ${req.account?.username || 'Not Logged In'}] Get brew list for ${username}`);
|
||||||
|
|
||||||
|
const brews = await HomebrewModel.getByUser(username, true, fields);
|
||||||
|
|
||||||
|
return res.json(brews);
|
||||||
|
});
|
||||||
|
|
||||||
/* Compresses the "text" field of a brew to binary */
|
/* Compresses the "text" field of a brew to binary */
|
||||||
router.put('/admin/compress/:id', (req, res)=>{
|
router.put('/admin/compress/:id', (req, res)=>{
|
||||||
HomebrewModel.findOne({ _id: req.params.id })
|
HomebrewModel.findOne({ _id: req.params.id })
|
||||||
@@ -132,7 +147,6 @@ router.put('/admin/compress/:id', (req, res)=>{
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
router.get('/admin/stats', mw.adminOnly, async (req, res)=>{
|
router.get('/admin/stats', mw.adminOnly, async (req, res)=>{
|
||||||
try {
|
try {
|
||||||
const totalBrewsCount = await HomebrewModel.countDocuments({});
|
const totalBrewsCount = await HomebrewModel.countDocuments({});
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
// Set working directory to project root
|
// Set working directory to project root
|
||||||
import { dirname } from 'path';
|
import { dirname } from 'path';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import packageJSON from './../package.json' with { type: "json" };
|
import packageJSON from './../package.json' with { type: 'json' };
|
||||||
|
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
process.chdir(`${__dirname}/..`);
|
process.chdir(`${__dirname}/..`);
|
||||||
@@ -26,7 +26,7 @@ import serveCompressedStaticAssets from './static-assets.mv.js';
|
|||||||
import sanitizeFilename from 'sanitize-filename';
|
import sanitizeFilename from 'sanitize-filename';
|
||||||
import asyncHandler from 'express-async-handler';
|
import asyncHandler from 'express-async-handler';
|
||||||
import templateFn from '../client/template.js';
|
import templateFn from '../client/template.js';
|
||||||
import {model as HomebrewModel } from './homebrew.model.js';
|
import { model as HomebrewModel } from './homebrew.model.js';
|
||||||
|
|
||||||
import { DEFAULT_BREW } from './brewDefaults.js';
|
import { DEFAULT_BREW } from './brewDefaults.js';
|
||||||
import { splitTextStyleAndMetadata } from '../shared/helpers.js';
|
import { splitTextStyleAndMetadata } from '../shared/helpers.js';
|
||||||
@@ -47,7 +47,7 @@ const sanitizeBrew = (brew, accessType)=>{
|
|||||||
return brew;
|
return brew;
|
||||||
};
|
};
|
||||||
|
|
||||||
app.set('trust proxy', 1 /* number of proxies between user and server */)
|
app.set('trust proxy', 1 /* number of proxies between user and server */);
|
||||||
|
|
||||||
app.use('/', serveCompressedStaticAssets(`build`));
|
app.use('/', serveCompressedStaticAssets(`build`));
|
||||||
app.use(contentNegotiation);
|
app.use(contentNegotiation);
|
||||||
@@ -55,6 +55,40 @@ app.use(bodyParser.json({ limit: '25mb' }));
|
|||||||
app.use(cookieParser());
|
app.use(cookieParser());
|
||||||
app.use(forceSSL);
|
app.use(forceSSL);
|
||||||
|
|
||||||
|
import cors from 'cors';
|
||||||
|
|
||||||
|
const nodeEnv = config.get('node_env');
|
||||||
|
const isLocalEnvironment = config.get('local_environments').includes(nodeEnv);
|
||||||
|
|
||||||
|
const corsOptions = {
|
||||||
|
origin : (origin, callback)=>{
|
||||||
|
|
||||||
|
const allowedOrigins = [
|
||||||
|
'https://homebrewery.naturalcrit.com',
|
||||||
|
'https://www.naturalcrit.com',
|
||||||
|
'https://naturalcrit-stage.herokuapp.com',
|
||||||
|
'https://homebrewery-stage.herokuapp.com',
|
||||||
|
];
|
||||||
|
|
||||||
|
if(isLocalEnvironment) {
|
||||||
|
allowedOrigins.push('http://localhost:8000', 'http://localhost:8010');
|
||||||
|
}
|
||||||
|
|
||||||
|
const herokuRegex = /^https:\/\/(?:homebrewery-pr-\d+\.herokuapp\.com|naturalcrit-pr-\d+\.herokuapp\.com)$/; // Matches any Heroku app
|
||||||
|
|
||||||
|
if(!origin || allowedOrigins.includes(origin) || herokuRegex.test(origin)) {
|
||||||
|
callback(null, true);
|
||||||
|
} else {
|
||||||
|
console.log(origin, 'not allowed');
|
||||||
|
callback(new Error('Not allowed by CORS, if you think this is an error, please contact us'));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods : ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
||||||
|
credentials : true,
|
||||||
|
};
|
||||||
|
|
||||||
|
app.use(cors(corsOptions));
|
||||||
|
|
||||||
//Account Middleware
|
//Account Middleware
|
||||||
app.use((req, res, next)=>{
|
app.use((req, res, next)=>{
|
||||||
if(req.cookies && req.cookies.nc_session){
|
if(req.cookies && req.cookies.nc_session){
|
||||||
@@ -62,7 +96,9 @@ app.use((req, res, next)=>{
|
|||||||
req.account = jwt.decode(req.cookies.nc_session, config.get('secret'));
|
req.account = jwt.decode(req.cookies.nc_session, config.get('secret'));
|
||||||
//console.log("Just loaded up JWT from cookie:");
|
//console.log("Just loaded up JWT from cookie:");
|
||||||
//console.log(req.account);
|
//console.log(req.account);
|
||||||
} catch (e){}
|
} catch (e){
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
req.config = {
|
req.config = {
|
||||||
@@ -273,7 +309,7 @@ app.get('/user/:username', async (req, res, next)=>{
|
|||||||
console.log(err);
|
console.log(err);
|
||||||
});
|
});
|
||||||
|
|
||||||
brews.forEach(brew => brew.stubbed = true); //All brews from MongoDB are "stubbed"
|
brews.forEach((brew)=>brew.stubbed = true); //All brews from MongoDB are "stubbed"
|
||||||
|
|
||||||
if(ownAccount && req?.account?.googleId){
|
if(ownAccount && req?.account?.googleId){
|
||||||
const auth = await GoogleActions.authCheck(req.account, res);
|
const auth = await GoogleActions.authCheck(req.account, res);
|
||||||
@@ -312,6 +348,34 @@ app.get('/user/:username', async (req, res, next)=>{
|
|||||||
return next();
|
return next();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//Change author name on brews
|
||||||
|
app.put('/api/user/rename', async (req, res)=>{
|
||||||
|
const { username, newUsername } = req.body;
|
||||||
|
const ownAccount = req.account && (req.account.username == newUsername);
|
||||||
|
|
||||||
|
if(!username || !newUsername)
|
||||||
|
return res.status(400).json({ error: 'Username and newUsername are required.' });
|
||||||
|
if(!ownAccount)
|
||||||
|
return res.status(403).json({ error: 'Must be logged in to change your username' });
|
||||||
|
try {
|
||||||
|
const brews = await HomebrewModel.getByUser(username, true, ['authors']);
|
||||||
|
const renamePromises = brews.map(async (brew)=>{
|
||||||
|
const updatedAuthors = brew.authors.map((author)=>author === username ? newUsername : author
|
||||||
|
);
|
||||||
|
return HomebrewModel.updateOne(
|
||||||
|
{ _id: brew._id },
|
||||||
|
{ $set: { authors: updatedAuthors } }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await Promise.all(renamePromises);
|
||||||
|
|
||||||
|
return res.json({ success: true, message: `Brews for ${username} renamed to ${newUsername}.` });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error renaming brews:', error);
|
||||||
|
return res.status(500).json({ error: 'Failed to rename brews.' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
//Edit Page
|
//Edit Page
|
||||||
app.get('/edit/:id', asyncHandler(getBrew('edit')), asyncHandler(async(req, res, next)=>{
|
app.get('/edit/:id', asyncHandler(getBrew('edit')), asyncHandler(async(req, res, next)=>{
|
||||||
req.brew = req.brew.toObject ? req.brew.toObject() : req.brew;
|
req.brew = req.brew.toObject ? req.brew.toObject() : req.brew;
|
||||||
@@ -413,7 +477,7 @@ app.get('/account', asyncHandler(async (req, res, next)=>{
|
|||||||
let googleCount = [];
|
let googleCount = [];
|
||||||
if(req.account) {
|
if(req.account) {
|
||||||
if(req.account.googleId) {
|
if(req.account.googleId) {
|
||||||
auth = await GoogleActions.authCheck(req.account, res, false)
|
auth = await GoogleActions.authCheck(req.account, res, false);
|
||||||
|
|
||||||
googleCount = await GoogleActions.listGoogleBrews(auth)
|
googleCount = await GoogleActions.listGoogleBrews(auth)
|
||||||
.catch((err)=>{
|
.catch((err)=>{
|
||||||
@@ -448,8 +512,6 @@ app.get('/account', asyncHandler(async (req, res, next)=>{
|
|||||||
return next();
|
return next();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const nodeEnv = config.get('node_env');
|
|
||||||
const isLocalEnvironment = config.get('local_environments').includes(nodeEnv);
|
|
||||||
// Local only
|
// Local only
|
||||||
if(isLocalEnvironment){
|
if(isLocalEnvironment){
|
||||||
// Login
|
// Login
|
||||||
@@ -477,7 +539,7 @@ app.get('/vault', asyncHandler(async(req, res, next)=>{
|
|||||||
|
|
||||||
//Send rendered page
|
//Send rendered page
|
||||||
app.use(asyncHandler(async (req, res, next)=>{
|
app.use(asyncHandler(async (req, res, next)=>{
|
||||||
if (!req.route) return res.redirect('/'); // Catch-all for invalid routes
|
if(!req.route) return res.redirect('/'); // Catch-all for invalid routes
|
||||||
|
|
||||||
const page = await renderPage(req, res);
|
const page = await renderPage(req, res);
|
||||||
if(!page) return;
|
if(!page) return;
|
||||||
@@ -490,6 +552,7 @@ const renderPage = async (req, res)=>{
|
|||||||
const configuration = {
|
const configuration = {
|
||||||
local : isLocalEnvironment,
|
local : isLocalEnvironment,
|
||||||
publicUrl : config.get('publicUrl') ?? '',
|
publicUrl : config.get('publicUrl') ?? '',
|
||||||
|
baseUrl : `${req.protocol}://${req.get('host')}`,
|
||||||
environment : nodeEnv,
|
environment : nodeEnv,
|
||||||
deployment : config.get('heroku_app_name') ?? ''
|
deployment : config.get('heroku_app_name') ?? ''
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/* eslint-disable max-lines */
|
/* eslint-disable max-lines */
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import {model as HomebrewModel} from './homebrew.model.js';
|
import { model as HomebrewModel } from './homebrew.model.js';
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import zlib from 'zlib';
|
import zlib from 'zlib';
|
||||||
import GoogleActions from './googleActions.js';
|
import GoogleActions from './googleActions.js';
|
||||||
@@ -106,12 +106,12 @@ const api = {
|
|||||||
stub = stub?.toObject();
|
stub = stub?.toObject();
|
||||||
googleId ??= stub?.googleId;
|
googleId ??= stub?.googleId;
|
||||||
|
|
||||||
const isOwner = stub?.authors?.length === 0 || stub?.authors?.[0] === req.account?.username;
|
const isOwner = (accessType == 'edit' && (!stub || stub?.authors?.length === 0)) || stub?.authors?.[0] === req.account?.username;
|
||||||
const isAuthor = stub?.authors?.includes(req.account?.username);
|
const isAuthor = stub?.authors?.includes(req.account?.username);
|
||||||
const isInvited = stub?.invitedAuthors?.includes(req.account?.username);
|
const isInvited = stub?.invitedAuthors?.includes(req.account?.username);
|
||||||
|
|
||||||
if(accessType === 'edit' && !(isOwner || isAuthor || isInvited)) {
|
if(accessType === 'edit' && !(isOwner || isAuthor || isInvited)) {
|
||||||
const accessError = { name: 'Access Error', status: 401, authors: stub.authors, brewTitle: stub.title, shareId: stub.shareId };
|
const accessError = { name: 'Access Error', status: 401, authors: stub?.authors, brewTitle: stub?.title, shareId: stub?.shareId };
|
||||||
if(req.account)
|
if(req.account)
|
||||||
throw { ...accessError, message: 'User is not an Author', HBErrorCode: '03' };
|
throw { ...accessError, message: 'User is not an Author', HBErrorCode: '03' };
|
||||||
else
|
else
|
||||||
@@ -119,12 +119,12 @@ const api = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if(stub?.lock?.locked && accessType != 'edit') {
|
if(stub?.lock?.locked && accessType != 'edit') {
|
||||||
throw { HBErrorCode: '51', code: stub.lock.code, message: stub.lock.shareMessage, brewId: stub.shareId, brewTitle: stub.title };
|
throw { HBErrorCode: '51', code: stub?.lock.code, message: stub?.lock.shareMessage, brewId: stub?.shareId, brewTitle: stub?.title };
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there is a google id, try to find the google brew
|
// If there's a google id, get it if requesting the full brew or if no stub found yet
|
||||||
if(!stubOnly && googleId) {
|
if(googleId && (!stubOnly || !stub)) {
|
||||||
const oAuth2Client = isOwner? GoogleActions.authCheck(req.account, res) : undefined;
|
const oAuth2Client = isOwner ? GoogleActions.authCheck(req.account, res) : undefined;
|
||||||
|
|
||||||
const googleBrew = await GoogleActions.getGoogleBrew(oAuth2Client, googleId, id, accessType)
|
const googleBrew = await GoogleActions.getGoogleBrew(oAuth2Client, googleId, id, accessType)
|
||||||
.catch((googleError)=>{
|
.catch((googleError)=>{
|
||||||
@@ -279,6 +279,8 @@ const api = {
|
|||||||
let currentTheme;
|
let currentTheme;
|
||||||
const completeStyles = [];
|
const completeStyles = [];
|
||||||
const completeSnippets = [];
|
const completeSnippets = [];
|
||||||
|
let themeName;
|
||||||
|
let themeAuthor;
|
||||||
|
|
||||||
while (req.params.id) {
|
while (req.params.id) {
|
||||||
//=== User Themes ===//
|
//=== User Themes ===//
|
||||||
@@ -292,6 +294,10 @@ const api = {
|
|||||||
|
|
||||||
currentTheme = req.brew;
|
currentTheme = req.brew;
|
||||||
splitTextStyleAndMetadata(currentTheme);
|
splitTextStyleAndMetadata(currentTheme);
|
||||||
|
if(!currentTheme.tags.some(tag => tag === "meta:theme" || tag === "meta:Theme"))
|
||||||
|
throw { brewId: req.params.id, name: 'Invalid Theme Selected', message: 'Selected theme does not have the meta:theme tag', status: 422, HBErrorCode: '10' };
|
||||||
|
themeName ??= currentTheme.title;
|
||||||
|
themeAuthor ??= currentTheme.authors?.[0];
|
||||||
|
|
||||||
// If there is anything in the snippets or style members, append them to the appropriate array
|
// If there is anything in the snippets or style members, append them to the appropriate array
|
||||||
if(currentTheme?.snippets) completeSnippets.push(JSON.parse(currentTheme.snippets));
|
if(currentTheme?.snippets) completeSnippets.push(JSON.parse(currentTheme.snippets));
|
||||||
@@ -301,6 +307,7 @@ const api = {
|
|||||||
req.params.renderer = currentTheme.renderer;
|
req.params.renderer = currentTheme.renderer;
|
||||||
} else {
|
} else {
|
||||||
//=== Static Themes ===//
|
//=== Static Themes ===//
|
||||||
|
themeName ??= req.params.id;
|
||||||
const localSnippets = `${req.params.renderer}_${req.params.id}`; // Just log the name for loading on client
|
const localSnippets = `${req.params.renderer}_${req.params.id}`; // Just log the name for loading on client
|
||||||
const localStyle = `@import url(\"/themes/${req.params.renderer}/${req.params.id}/style.css\");`;
|
const localStyle = `@import url(\"/themes/${req.params.renderer}/${req.params.id}/style.css\");`;
|
||||||
completeSnippets.push(localSnippets);
|
completeSnippets.push(localSnippets);
|
||||||
@@ -313,7 +320,9 @@ const api = {
|
|||||||
const returnObj = {
|
const returnObj = {
|
||||||
// Reverse the order of the arrays so they are listed oldest parent to youngest child.
|
// Reverse the order of the arrays so they are listed oldest parent to youngest child.
|
||||||
styles : completeStyles.reverse(),
|
styles : completeStyles.reverse(),
|
||||||
snippets : completeSnippets.reverse()
|
snippets : completeSnippets.reverse(),
|
||||||
|
name : themeName,
|
||||||
|
author : themeAuthor
|
||||||
};
|
};
|
||||||
|
|
||||||
res.setHeader('Content-Type', 'application/json');
|
res.setHeader('Content-Type', 'application/json');
|
||||||
@@ -467,12 +476,11 @@ const api = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
router.use('/api', checkClientVersion);
|
router.post('/api', checkClientVersion, asyncHandler(api.newBrew));
|
||||||
router.post('/api', asyncHandler(api.newBrew));
|
router.put('/api/:id', checkClientVersion, asyncHandler(api.getBrew('edit', true)), asyncHandler(api.updateBrew));
|
||||||
router.put('/api/:id', asyncHandler(api.getBrew('edit', true)), asyncHandler(api.updateBrew));
|
router.put('/api/update/:id', checkClientVersion, asyncHandler(api.getBrew('edit', true)), asyncHandler(api.updateBrew));
|
||||||
router.put('/api/update/:id', asyncHandler(api.getBrew('edit', true)), asyncHandler(api.updateBrew));
|
router.delete('/api/:id', checkClientVersion, asyncHandler(api.deleteBrew));
|
||||||
router.delete('/api/:id', asyncHandler(api.deleteBrew));
|
router.get('/api/remove/:id', checkClientVersion, asyncHandler(api.deleteBrew));
|
||||||
router.get('/api/remove/:id', asyncHandler(api.deleteBrew));
|
|
||||||
router.get('/api/theme/:renderer/:id', asyncHandler(api.getThemeBundle));
|
router.get('/api/theme/:renderer/:id', asyncHandler(api.getThemeBundle));
|
||||||
|
|
||||||
export default api;
|
export default api;
|
||||||
@@ -576,7 +576,7 @@ brew`);
|
|||||||
describe('Theme bundle', ()=>{
|
describe('Theme bundle', ()=>{
|
||||||
it('should return Theme Bundle for a User Theme', async ()=>{
|
it('should return Theme Bundle for a User Theme', async ()=>{
|
||||||
const brews = {
|
const brews = {
|
||||||
userThemeAID : { title: 'User Theme A', renderer: 'V3', theme: null, shareId: 'userThemeAID', style: 'User Theme A Style' }
|
userThemeAID : { title: 'User Theme A', renderer: 'V3', theme: null, shareId: 'userThemeAID', style: 'User Theme A Style', tags: ['meta:theme'], authors: ['authorName'] }
|
||||||
};
|
};
|
||||||
|
|
||||||
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
|
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
|
||||||
@@ -587,6 +587,8 @@ brew`);
|
|||||||
|
|
||||||
expect(res.status).toHaveBeenCalledWith(200);
|
expect(res.status).toHaveBeenCalledWith(200);
|
||||||
expect(res.send).toHaveBeenCalledWith({
|
expect(res.send).toHaveBeenCalledWith({
|
||||||
|
name : 'User Theme A',
|
||||||
|
author : 'authorName',
|
||||||
styles : ['/* From Brew: https://localhost/share/userThemeAID */\n\nUser Theme A Style'],
|
styles : ['/* From Brew: https://localhost/share/userThemeAID */\n\nUser Theme A Style'],
|
||||||
snippets : []
|
snippets : []
|
||||||
});
|
});
|
||||||
@@ -594,9 +596,9 @@ brew`);
|
|||||||
|
|
||||||
it('should return Theme Bundle for nested User Themes', async ()=>{
|
it('should return Theme Bundle for nested User Themes', async ()=>{
|
||||||
const brews = {
|
const brews = {
|
||||||
userThemeAID : { title: 'User Theme A', renderer: 'V3', theme: 'userThemeBID', shareId: 'userThemeAID', style: 'User Theme A Style' },
|
userThemeAID : { title: 'User Theme A', renderer: 'V3', theme: 'userThemeBID', shareId: 'userThemeAID', style: 'User Theme A Style', tags: ['meta:theme'], authors: ['authorName'] },
|
||||||
userThemeBID : { title: 'User Theme B', renderer: 'V3', theme: 'userThemeCID', shareId: 'userThemeBID', style: 'User Theme B Style' },
|
userThemeBID : { title: 'User Theme B', renderer: 'V3', theme: 'userThemeCID', shareId: 'userThemeBID', style: 'User Theme B Style', tags: ['meta:theme'], authors: ['authorName'] },
|
||||||
userThemeCID : { title: 'User Theme C', renderer: 'V3', theme: null, shareId: 'userThemeCID', style: 'User Theme C Style' }
|
userThemeCID : { title: 'User Theme C', renderer: 'V3', theme: null, shareId: 'userThemeCID', style: 'User Theme C Style', tags: ['meta:theme'], authors: ['authorName'] }
|
||||||
};
|
};
|
||||||
|
|
||||||
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
|
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
|
||||||
@@ -607,6 +609,8 @@ brew`);
|
|||||||
|
|
||||||
expect(res.status).toHaveBeenCalledWith(200);
|
expect(res.status).toHaveBeenCalledWith(200);
|
||||||
expect(res.send).toHaveBeenCalledWith({
|
expect(res.send).toHaveBeenCalledWith({
|
||||||
|
name : 'User Theme A',
|
||||||
|
author : 'authorName',
|
||||||
styles : [
|
styles : [
|
||||||
'/* From Brew: https://localhost/share/userThemeCID */\n\nUser Theme C Style',
|
'/* From Brew: https://localhost/share/userThemeCID */\n\nUser Theme C Style',
|
||||||
'/* From Brew: https://localhost/share/userThemeBID */\n\nUser Theme B Style',
|
'/* From Brew: https://localhost/share/userThemeBID */\n\nUser Theme B Style',
|
||||||
@@ -623,6 +627,8 @@ brew`);
|
|||||||
|
|
||||||
expect(res.status).toHaveBeenCalledWith(200);
|
expect(res.status).toHaveBeenCalledWith(200);
|
||||||
expect(res.send).toHaveBeenCalledWith({
|
expect(res.send).toHaveBeenCalledWith({
|
||||||
|
name : '5ePHB',
|
||||||
|
author : undefined,
|
||||||
styles : [
|
styles : [
|
||||||
`/* From Theme Blank */\n\n@import url("/themes/V3/Blank/style.css");`,
|
`/* From Theme Blank */\n\n@import url("/themes/V3/Blank/style.css");`,
|
||||||
`/* From Theme 5ePHB */\n\n@import url("/themes/V3/5ePHB/style.css");`
|
`/* From Theme 5ePHB */\n\n@import url("/themes/V3/5ePHB/style.css");`
|
||||||
@@ -636,9 +642,9 @@ brew`);
|
|||||||
|
|
||||||
it('should return Theme Bundle for nested User and Static Themes together', async ()=>{
|
it('should return Theme Bundle for nested User and Static Themes together', async ()=>{
|
||||||
const brews = {
|
const brews = {
|
||||||
userThemeAID : { title: 'User Theme A', renderer: 'V3', theme: 'userThemeBID', shareId: 'userThemeAID', style: 'User Theme A Style' },
|
userThemeAID : { title: 'User Theme A', renderer: 'V3', theme: 'userThemeBID', shareId: 'userThemeAID', style: 'User Theme A Style', tags: ['meta:theme'], authors: ['authorName'] },
|
||||||
userThemeBID : { title: 'User Theme B', renderer: 'V3', theme: 'userThemeCID', shareId: 'userThemeBID', style: 'User Theme B Style' },
|
userThemeBID : { title: 'User Theme B', renderer: 'V3', theme: 'userThemeCID', shareId: 'userThemeBID', style: 'User Theme B Style', tags: ['meta:theme'], authors: ['authorName'] },
|
||||||
userThemeCID : { title: 'User Theme C', renderer: 'V3', theme: '5eDMG', shareId: 'userThemeCID', style: 'User Theme C Style' }
|
userThemeCID : { title: 'User Theme C', renderer: 'V3', theme: '5eDMG', shareId: 'userThemeCID', style: 'User Theme C Style', tags: ['meta:theme'], authors: ['authorName'] }
|
||||||
};
|
};
|
||||||
|
|
||||||
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
|
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
|
||||||
@@ -649,6 +655,8 @@ brew`);
|
|||||||
|
|
||||||
expect(res.status).toHaveBeenCalledWith(200);
|
expect(res.status).toHaveBeenCalledWith(200);
|
||||||
expect(res.send).toHaveBeenCalledWith({
|
expect(res.send).toHaveBeenCalledWith({
|
||||||
|
name : 'User Theme A',
|
||||||
|
author : 'authorName',
|
||||||
styles : [
|
styles : [
|
||||||
`/* From Theme Blank */\n\n@import url("/themes/V3/Blank/style.css");`,
|
`/* From Theme Blank */\n\n@import url("/themes/V3/Blank/style.css");`,
|
||||||
`/* From Theme 5ePHB */\n\n@import url("/themes/V3/5ePHB/style.css");`,
|
`/* From Theme 5ePHB */\n\n@import url("/themes/V3/5ePHB/style.css");`,
|
||||||
@@ -665,9 +673,9 @@ brew`);
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should fail for an invalid Theme in the chain', async()=>{
|
it('should fail for a missing Theme in the chain', async()=>{
|
||||||
const brews = {
|
const brews = {
|
||||||
userThemeAID : { title: 'User Theme A', renderer: 'V3', theme: 'missingTheme', shareId: 'userThemeAID', style: 'User Theme A Style' },
|
userThemeAID : { title: 'User Theme A', renderer: 'V3', theme: 'missingTheme', shareId: 'userThemeAID', style: 'User Theme A Style', tags: ['meta:theme'], authors: ['authorName'] },
|
||||||
};
|
};
|
||||||
|
|
||||||
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
|
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
|
||||||
@@ -686,6 +694,27 @@ brew`);
|
|||||||
name : 'ThemeLoad Error',
|
name : 'ThemeLoad Error',
|
||||||
status : 404 });
|
status : 404 });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should fail for a User Theme not tagged with meta:theme', async ()=>{
|
||||||
|
const brews = {
|
||||||
|
userThemeAID : { title: 'User Theme A', renderer: 'V3', theme: null, shareId: 'userThemeAID', style: 'User Theme A Style' }
|
||||||
|
};
|
||||||
|
|
||||||
|
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
|
||||||
|
model.get = jest.fn((getParams)=>toBrewPromise(brews[getParams.shareId]));
|
||||||
|
const req = { params: { renderer: 'V3', id: 'userThemeAID' }, get: ()=>{ return 'localhost'; }, protocol: 'https' };
|
||||||
|
|
||||||
|
let err;
|
||||||
|
await api.getThemeBundle(req, res)
|
||||||
|
.catch((e)=>err = e);
|
||||||
|
|
||||||
|
expect(err).toEqual({
|
||||||
|
HBErrorCode : '10',
|
||||||
|
brewId : 'userThemeAID',
|
||||||
|
message : 'Selected theme does not have the meta:theme tag',
|
||||||
|
name : 'Invalid Theme Selected',
|
||||||
|
status : 422 });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('deleteBrew', ()=>{
|
describe('deleteBrew', ()=>{
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import packageJSON from '../../package.json' with { type: "json" };
|
import packageJSON from '../../package.json' with { type: 'json' };
|
||||||
const version = packageJSON.version;
|
|
||||||
|
|
||||||
export default (req, res, next)=>{
|
export default (req, res, next)=>{
|
||||||
const userVersion = req.get('Homebrewery-Version');
|
const userVersion = req.get('Homebrewery-Version');
|
||||||
|
const version = packageJSON.version;
|
||||||
|
|
||||||
if(userVersion != version) {
|
if(userVersion !== version) {
|
||||||
return res.status(412).send({
|
return res.status(412).send({
|
||||||
message : `Client version ${userVersion} is out of date. Please save your changes elsewhere and refresh to pick up client version ${version}.`
|
message : `Client version ${userVersion} is out of date. Please save your changes elsewhere and refresh to pick up client version ${version}.`
|
||||||
});
|
});
|
||||||
@@ -12,3 +12,4 @@ export default (req, res, next)=>{
|
|||||||
|
|
||||||
next();
|
next();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export default (req, res, next)=>{
|
|||||||
const isImageRequest = req.get('Accept')?.split(',')
|
const isImageRequest = req.get('Accept')?.split(',')
|
||||||
?.filter((h)=>!h.includes('q='))
|
?.filter((h)=>!h.includes('q='))
|
||||||
?.every((h)=>/image\/.*/.test(h));
|
?.every((h)=>/image\/.*/.test(h));
|
||||||
if(isImageRequest && !isLocalEnvironment && !req.url?.startsWith('/staticImages')) {
|
if(isImageRequest && !(isLocalEnvironment && req.url?.startsWith('/staticImages'))) {
|
||||||
return res.status(406).send({
|
return res.status(406).send({
|
||||||
message : 'Request for image at this URL is not supported'
|
message : 'Request for image at this URL is not supported'
|
||||||
});
|
});
|
||||||
|
|||||||
41
server/middleware/content-negotiation.spec.js
Normal file
41
server/middleware/content-negotiation.spec.js
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import contentNegotiationMiddleware from './content-negotiation.js';
|
||||||
|
|
||||||
|
describe('content-negotiation-middleware', ()=>{
|
||||||
|
let request;
|
||||||
|
let response;
|
||||||
|
let next;
|
||||||
|
|
||||||
|
beforeEach(()=>{
|
||||||
|
request = {
|
||||||
|
get : function(key) {
|
||||||
|
return this[key];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
response = {
|
||||||
|
status : jest.fn(()=>response),
|
||||||
|
send : jest.fn(()=>{})
|
||||||
|
};
|
||||||
|
next = jest.fn();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 406 on image request', ()=>{
|
||||||
|
contentNegotiationMiddleware({
|
||||||
|
Accept : 'image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8',
|
||||||
|
...request
|
||||||
|
}, response);
|
||||||
|
|
||||||
|
expect(response.status).toHaveBeenLastCalledWith(406);
|
||||||
|
expect(response.send).toHaveBeenCalledWith({
|
||||||
|
message : 'Request for image at this URL is not supported'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call next on non-image request', ()=>{
|
||||||
|
contentNegotiationMiddleware({
|
||||||
|
Accept : 'text,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8',
|
||||||
|
...request
|
||||||
|
}, response, next);
|
||||||
|
|
||||||
|
expect(next).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -44,13 +44,19 @@ const fetchThemeBundle = async (obj, renderer, theme)=>{
|
|||||||
.catch((err)=>{
|
.catch((err)=>{
|
||||||
obj.setState({ error: err });
|
obj.setState({ error: err });
|
||||||
});
|
});
|
||||||
if(!res) return;
|
if(!res) {
|
||||||
|
obj.setState((prevState)=>({
|
||||||
|
...prevState,
|
||||||
|
themeBundle : {}
|
||||||
|
}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
const themeBundle = res.body;
|
const themeBundle = res.body;
|
||||||
themeBundle.joinedStyles = themeBundle.styles.map((style)=>`<style>${style}</style>`).join('\n\n');
|
themeBundle.joinedStyles = themeBundle.styles.map((style)=>`<style>${style}</style>`).join('\n\n');
|
||||||
obj.setState((prevState)=>({
|
obj.setState((prevState)=>({
|
||||||
...prevState,
|
...prevState,
|
||||||
themeBundle : themeBundle
|
themeBundle : themeBundle,
|
||||||
|
error : null
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -11,33 +11,38 @@
|
|||||||
@import (less) './themes/fonts/iconFonts/fontAwesome.less';
|
@import (less) './themes/fonts/iconFonts/fontAwesome.less';
|
||||||
|
|
||||||
@keyframes sourceMoveAnimation {
|
@keyframes sourceMoveAnimation {
|
||||||
50% {background-color: red; color: white;}
|
50% { color : white;background-color : red;}
|
||||||
100% {background-color: unset; color: unset;}
|
100% { color : unset;background-color : unset;}
|
||||||
}
|
}
|
||||||
|
|
||||||
.codeEditor{
|
.codeEditor {
|
||||||
@media screen and (pointer : coarse) {
|
@media screen and (pointer : coarse) {
|
||||||
font-size : 16px;
|
font-size : 16px;
|
||||||
}
|
}
|
||||||
.CodeMirror-foldmarker {
|
.CodeMirror-foldmarker {
|
||||||
font-family: inherit;
|
font-family : inherit;
|
||||||
text-shadow: none;
|
font-weight : 600;
|
||||||
font-weight: 600;
|
color : grey;
|
||||||
color: grey;
|
text-shadow : none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sourceMoveFlash .CodeMirror-line{
|
.CodeMirror-foldgutter {
|
||||||
animation-name: sourceMoveAnimation;
|
cursor : pointer;
|
||||||
animation-duration: 0.4s;
|
border-left : 1px solid #EEEEEE;
|
||||||
|
transition : background 0.1s;
|
||||||
|
&:hover { background : #DDDDDD; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.sourceMoveFlash .CodeMirror-line {
|
||||||
|
animation-name : sourceMoveAnimation;
|
||||||
|
animation-duration : 0.4s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.CodeMirror-vscrollbar {
|
.CodeMirror-vscrollbar {
|
||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar { width : 20px; }
|
||||||
width: 20px;
|
|
||||||
}
|
|
||||||
&::-webkit-scrollbar-thumb {
|
&::-webkit-scrollbar-thumb {
|
||||||
width: 20px;
|
width : 20px;
|
||||||
background: linear-gradient(90deg, #858585 15px, #808080 15px);
|
background : linear-gradient(90deg, #858585 15px, #808080 15px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,6 +59,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.emojiPreview {
|
.emojiPreview {
|
||||||
font-size: 1.5em;
|
font-size : 1.5em;
|
||||||
line-height: 1.2em;
|
line-height : 1.2em;
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable max-depth */
|
||||||
/* eslint-disable max-lines */
|
/* eslint-disable max-lines */
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { Parser as MathParser } from 'expr-eval';
|
import { Parser as MathParser } from 'expr-eval';
|
||||||
@@ -6,6 +7,7 @@ import MarkedExtendedTables from 'marked-extended-tables';
|
|||||||
import { markedSmartypantsLite as MarkedSmartypantsLite } from 'marked-smartypants-lite';
|
import { markedSmartypantsLite as MarkedSmartypantsLite } from 'marked-smartypants-lite';
|
||||||
import { gfmHeadingId as MarkedGFMHeadingId, resetHeadings as MarkedGFMResetHeadingIDs } from 'marked-gfm-heading-id';
|
import { gfmHeadingId as MarkedGFMHeadingId, resetHeadings as MarkedGFMResetHeadingIDs } from 'marked-gfm-heading-id';
|
||||||
import { markedEmoji as MarkedEmojis } from 'marked-emoji';
|
import { markedEmoji as MarkedEmojis } from 'marked-emoji';
|
||||||
|
import MarkedSubSuperText from 'marked-subsuper-text';
|
||||||
|
|
||||||
//Icon fonts included so they can appear in emoji autosuggest dropdown
|
//Icon fonts included so they can appear in emoji autosuggest dropdown
|
||||||
import diceFont from '../../themes/fonts/iconFonts/diceFont.js';
|
import diceFont from '../../themes/fonts/iconFonts/diceFont.js';
|
||||||
@@ -59,7 +61,8 @@ mathParser.functions.signed = function (a) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
//Processes the markdown within an HTML block if it's just a class-wrapper
|
//Processes the markdown within an HTML block if it's just a class-wrapper
|
||||||
renderer.html = function (html) {
|
renderer.html = function (token) {
|
||||||
|
let html = token.text;
|
||||||
if(_.startsWith(_.trim(html), '<div') && _.endsWith(_.trim(html), '</div>')){
|
if(_.startsWith(_.trim(html), '<div') && _.endsWith(_.trim(html), '</div>')){
|
||||||
const openTag = html.substring(0, html.indexOf('>')+1);
|
const openTag = html.substring(0, html.indexOf('>')+1);
|
||||||
html = html.substring(html.indexOf('>')+1);
|
html = html.substring(html.indexOf('>')+1);
|
||||||
@@ -70,18 +73,21 @@ renderer.html = function (html) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Don't wrap {{ Spans alone on a line, or {{ Divs in <p> tags
|
// Don't wrap {{ Spans alone on a line, or {{ Divs in <p> tags
|
||||||
renderer.paragraph = function(text){
|
renderer.paragraph = function(token){
|
||||||
let match;
|
let match;
|
||||||
|
const text = this.parser.parseInline(token.tokens);
|
||||||
if(text.startsWith('<div') || text.startsWith('</div'))
|
if(text.startsWith('<div') || text.startsWith('</div'))
|
||||||
return `${text}`;
|
return `${text}`;
|
||||||
else if(match = text.match(/(^|^.*?\n)<span class="inline-block(.*?<\/span>)$/)) {
|
else if(match = text.match(/(^|^.*?\n)<span class="inline-block(.*?<\/span>)$/))
|
||||||
return `${match[1].trim() ? `<p>${match[1]}</p>` : ''}<span class="inline-block${match[2]}`;
|
return `${match[1].trim() ? `<p>${match[1]}</p>` : ''}<span class="inline-block${match[2]}`;
|
||||||
} else
|
else
|
||||||
return `<p>${text}</p>\n`;
|
return `<p>${text}</p>\n`;
|
||||||
};
|
};
|
||||||
|
|
||||||
//Fix local links in the Preview iFrame to link inside the frame
|
//Fix local links in the Preview iFrame to link inside the frame
|
||||||
renderer.link = function (href, title, text) {
|
renderer.link = function (token) {
|
||||||
|
let {href, title, tokens} = token;
|
||||||
|
const text = this.parser.parseInline(tokens)
|
||||||
let self = false;
|
let self = false;
|
||||||
if(href[0] == '#') {
|
if(href[0] == '#') {
|
||||||
self = true;
|
self = true;
|
||||||
@@ -103,8 +109,8 @@ renderer.link = function (href, title, text) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Expose `src` attribute as `--HB_src` to make the URL accessible via CSS
|
// Expose `src` attribute as `--HB_src` to make the URL accessible via CSS
|
||||||
renderer.image = function (href, title, text) {
|
renderer.image = function (token) {
|
||||||
href = cleanUrl(href);
|
let {href, title, text} = token;
|
||||||
if(href === null)
|
if(href === null)
|
||||||
return text;
|
return text;
|
||||||
|
|
||||||
@@ -172,7 +178,7 @@ const mustacheSpans = {
|
|||||||
return `<span` +
|
return `<span` +
|
||||||
`${tags.classes ? ` class="${tags.classes}"` : ''}` +
|
`${tags.classes ? ` class="${tags.classes}"` : ''}` +
|
||||||
`${tags.id ? ` id="${tags.id}"` : ''}` +
|
`${tags.id ? ` id="${tags.id}"` : ''}` +
|
||||||
`${tags.styles ? ` style="${tags.styles}"` : ''}` +
|
`${tags.styles ? ` style="${Object.entries(tags.styles).map(([key, value])=>`${key}:${value};`).join(' ')}"` : ''}` +
|
||||||
`${tags.attributes ? ` ${Object.entries(tags.attributes).map(([key, value])=>`${key}="${value}"`).join(' ')}` : ''}` +
|
`${tags.attributes ? ` ${Object.entries(tags.attributes).map(([key, value])=>`${key}="${value}"`).join(' ')}` : ''}` +
|
||||||
`>${this.parser.parseInline(token.tokens)}</span>`; // parseInline to turn child tokens into HTML
|
`>${this.parser.parseInline(token.tokens)}</span>`; // parseInline to turn child tokens into HTML
|
||||||
}
|
}
|
||||||
@@ -228,7 +234,7 @@ const mustacheDivs = {
|
|||||||
return `<div` +
|
return `<div` +
|
||||||
`${tags.classes ? ` class="${tags.classes}"` : ''}` +
|
`${tags.classes ? ` class="${tags.classes}"` : ''}` +
|
||||||
`${tags.id ? ` id="${tags.id}"` : ''}` +
|
`${tags.id ? ` id="${tags.id}"` : ''}` +
|
||||||
`${tags.styles ? ` style="${tags.styles}"` : ''}` +
|
`${tags.styles ? ` style="${Object.entries(tags.styles).map(([key, value])=>`${key}:${value};`).join(' ')}"` : ''}` +
|
||||||
`${tags.attributes ? ` ${Object.entries(tags.attributes).map(([key, value])=>`${key}="${value}"`).join(' ')}` : ''}` +
|
`${tags.attributes ? ` ${Object.entries(tags.attributes).map(([key, value])=>`${key}="${value}"`).join(' ')}` : ''}` +
|
||||||
`>${this.parser.parse(token.tokens)}</div>`; // parse to turn child tokens into HTML
|
`>${this.parser.parse(token.tokens)}</div>`; // parse to turn child tokens into HTML
|
||||||
}
|
}
|
||||||
@@ -265,18 +271,13 @@ const mustacheInjectInline = {
|
|||||||
const text = this.parser.parseInline([token]);
|
const text = this.parser.parseInline([token]);
|
||||||
const originalTags = extractHTMLStyleTags(text);
|
const originalTags = extractHTMLStyleTags(text);
|
||||||
const injectedTags = token.injectedTags;
|
const injectedTags = token.injectedTags;
|
||||||
const tags = {
|
const tags = mergeHTMLTags(originalTags, injectedTags);
|
||||||
id : injectedTags.id || originalTags.id || null,
|
|
||||||
classes : [originalTags.classes, injectedTags.classes].join(' ').trim() || null,
|
|
||||||
styles : [originalTags.styles, injectedTags.styles].join(' ').trim() || null,
|
|
||||||
attributes : Object.assign(originalTags.attributes ?? {}, injectedTags.attributes ?? {})
|
|
||||||
};
|
|
||||||
const openingTag = /(<[^\s<>]+)[^\n<>]*(>.*)/s.exec(text);
|
const openingTag = /(<[^\s<>]+)[^\n<>]*(>.*)/s.exec(text);
|
||||||
if(openingTag) {
|
if(openingTag) {
|
||||||
return `${openingTag[1]}` +
|
return `${openingTag[1]}` +
|
||||||
`${tags.classes ? ` class="${tags.classes}"` : ''}` +
|
`${tags.classes ? ` class="${tags.classes}"` : ''}` +
|
||||||
`${tags.id ? ` id="${tags.id}"` : ''}` +
|
`${tags.id ? ` id="${tags.id}"` : ''}` +
|
||||||
`${tags.styles ? ` style="${tags.styles}"` : ''}` +
|
`${!_.isEmpty(tags.styles) ? ` style="${Object.entries(tags.styles).map(([key, value])=>`${key}:${value};`).join(' ')}"` : ''}` +
|
||||||
`${!_.isEmpty(tags.attributes) ? ` ${Object.entries(tags.attributes).map(([key, value])=>`${key}="${value}"`).join(' ')}` : ''}` +
|
`${!_.isEmpty(tags.attributes) ? ` ${Object.entries(tags.attributes).map(([key, value])=>`${key}="${value}"`).join(' ')}` : ''}` +
|
||||||
`${openingTag[2]}`; // parse to turn child tokens into HTML
|
`${openingTag[2]}`; // parse to turn child tokens into HTML
|
||||||
}
|
}
|
||||||
@@ -314,18 +315,13 @@ const mustacheInjectBlock = {
|
|||||||
const text = this.parser.parse([token]);
|
const text = this.parser.parse([token]);
|
||||||
const originalTags = extractHTMLStyleTags(text);
|
const originalTags = extractHTMLStyleTags(text);
|
||||||
const injectedTags = token.injectedTags;
|
const injectedTags = token.injectedTags;
|
||||||
const tags = {
|
const tags = mergeHTMLTags(originalTags, injectedTags);
|
||||||
id : injectedTags.id || originalTags.id || null,
|
|
||||||
classes : [originalTags.classes, injectedTags.classes].join(' ').trim() || null,
|
|
||||||
styles : [originalTags.styles, injectedTags.styles].join(' ').trim() || null,
|
|
||||||
attributes : Object.assign(originalTags.attributes ?? {}, injectedTags.attributes ?? {})
|
|
||||||
};
|
|
||||||
const openingTag = /(<[^\s<>]+)[^\n<>]*(>.*)/s.exec(text);
|
const openingTag = /(<[^\s<>]+)[^\n<>]*(>.*)/s.exec(text);
|
||||||
if(openingTag) {
|
if(openingTag) {
|
||||||
return `${openingTag[1]}` +
|
return `${openingTag[1]}` +
|
||||||
`${tags.classes ? ` class="${tags.classes}"` : ''}` +
|
`${tags.classes ? ` class="${tags.classes}"` : ''}` +
|
||||||
`${tags.id ? ` id="${tags.id}"` : ''}` +
|
`${tags.id ? ` id="${tags.id}"` : ''}` +
|
||||||
`${tags.styles ? ` style="${tags.styles}"` : ''}` +
|
`${!_.isEmpty(tags.styles) ? ` style="${Object.entries(tags.styles).map(([key, value])=>`${key}:${value};`).join(' ')}"` : ''}` +
|
||||||
`${!_.isEmpty(tags.attributes) ? ` ${Object.entries(tags.attributes).map(([key, value])=>`${key}="${value}"`).join(' ')}` : ''}` +
|
`${!_.isEmpty(tags.attributes) ? ` ${Object.entries(tags.attributes).map(([key, value])=>`${key}="${value}"`).join(' ')}` : ''}` +
|
||||||
`${openingTag[2]}`; // parse to turn child tokens into HTML
|
`${openingTag[2]}`; // parse to turn child tokens into HTML
|
||||||
}
|
}
|
||||||
@@ -342,34 +338,42 @@ const mustacheInjectBlock = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const superSubScripts = {
|
const justifiedParagraphClasses = [];
|
||||||
name : 'superSubScript',
|
justifiedParagraphClasses[2] = 'Left';
|
||||||
level : 'inline',
|
justifiedParagraphClasses[4] = 'Right';
|
||||||
start(src) { return src.match(/\^/m)?.index; }, // Hint to Marked.js to stop and check for a match
|
justifiedParagraphClasses[6] = 'Center';
|
||||||
|
|
||||||
|
const justifiedParagraphs = {
|
||||||
|
name : 'justifiedParagraphs',
|
||||||
|
level : 'block',
|
||||||
|
start(src) {
|
||||||
|
return src.match(/\n(?:-:|:-|-:) {1}/m)?.index;
|
||||||
|
}, // Hint to Marked.js to stop and check for a match
|
||||||
tokenizer(src, tokens) {
|
tokenizer(src, tokens) {
|
||||||
const superRegex = /^\^(?!\s)(?=([^\n\^]*[^\s\^]))\1\^/m;
|
const regex = /^(((:-))|((-:))|((:-:))) .+(\n(([^\n].*\n)*(\n|$))|$)/ygm;
|
||||||
const subRegex = /^\^\^(?!\s)(?=([^\n\^]*[^\s\^]))\1\^\^/m;
|
const match = regex.exec(src);
|
||||||
let isSuper = false;
|
|
||||||
let match = subRegex.exec(src);
|
|
||||||
if(!match){
|
|
||||||
match = superRegex.exec(src);
|
|
||||||
if(match)
|
|
||||||
isSuper = true;
|
|
||||||
}
|
|
||||||
if(match?.length) {
|
if(match?.length) {
|
||||||
|
let whichJustify;
|
||||||
|
if(match[2]?.length) whichJustify = 2;
|
||||||
|
if(match[4]?.length) whichJustify = 4;
|
||||||
|
if(match[6]?.length) whichJustify = 6;
|
||||||
return {
|
return {
|
||||||
type : 'superSubScript', // Should match "name" above
|
type : 'justifiedParagraphs', // Should match "name" above
|
||||||
raw : match[0], // Text to consume from the source
|
raw : match[0], // Text to consume from the source
|
||||||
tag : isSuper ? 'sup' : 'sub',
|
length : match[whichJustify].length,
|
||||||
tokens : this.lexer.inlineTokens(match[1])
|
text : match[0].slice(match[whichJustify].length),
|
||||||
|
class : justifiedParagraphClasses[whichJustify],
|
||||||
|
tokens : this.lexer.inlineTokens(match[0].slice(match[whichJustify].length + 1))
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
renderer(token) {
|
renderer(token) {
|
||||||
return `<${token.tag}>${this.parser.parseInline(token.tokens)}</${token.tag}>`;
|
return `<p align="${token.class}">${this.parser.parseInline(token.tokens)}</p>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const forcedParagraphBreaks = {
|
const forcedParagraphBreaks = {
|
||||||
name : 'hardBreaks',
|
name : 'hardBreaks',
|
||||||
level : 'block',
|
level : 'block',
|
||||||
@@ -377,7 +381,12 @@ const forcedParagraphBreaks = {
|
|||||||
tokenizer(src, tokens) {
|
tokenizer(src, tokens) {
|
||||||
const regex = /^(:+)(?:\n|$)/ym;
|
const regex = /^(:+)(?:\n|$)/ym;
|
||||||
const match = regex.exec(src);
|
const match = regex.exec(src);
|
||||||
|
|
||||||
if(match?.length) {
|
if(match?.length) {
|
||||||
|
const lastToken = tokens[tokens.length - 1];
|
||||||
|
if(lastToken?.type == 'text')
|
||||||
|
lastToken.type = 'paragraph';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type : 'hardBreaks', // Should match "name" above
|
type : 'hardBreaks', // Should match "name" above
|
||||||
raw : match[0], // Text to consume from the source
|
raw : match[0], // Text to consume from the source
|
||||||
@@ -387,7 +396,36 @@ const forcedParagraphBreaks = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
renderer(token) {
|
renderer(token) {
|
||||||
return `<div class='blank'></div>`.repeat(token.length).concat('\n');
|
return `<br>\n`.repeat(token.length);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const patchHardBreaks = {
|
||||||
|
walkTokens(token) {
|
||||||
|
if(token.type == 'list' || token.type == 'list_item') {
|
||||||
|
token.loose = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const nonbreakingSpaces = {
|
||||||
|
name : 'nonbreakingSpaces',
|
||||||
|
level : 'inline',
|
||||||
|
start(src) { return src.match(/:>+/m)?.index; }, // Hint to Marked.js to stop and check for a match
|
||||||
|
tokenizer(src, tokens) {
|
||||||
|
const regex = /:(>+)/ym;
|
||||||
|
const match = regex.exec(src);
|
||||||
|
if(match?.length) {
|
||||||
|
return {
|
||||||
|
type : 'nonbreakingSpaces', // Should match "name" above
|
||||||
|
raw : match[0], // Text to consume from the source
|
||||||
|
length : match[1].length,
|
||||||
|
text : ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
renderer(token) {
|
||||||
|
return ` `.repeat(token.length).concat('');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -659,7 +697,7 @@ function MarkedVariables() {
|
|||||||
}
|
}
|
||||||
if(match[8]) { // Inline Definition
|
if(match[8]) { // Inline Definition
|
||||||
const label = match[10] ? match[10].trim().replace(/\s+/g, ' ') : null; // Trim edge spaces and shorten blocks of whitespace to 1 space
|
const label = match[10] ? match[10].trim().replace(/\s+/g, ' ') : null; // Trim edge spaces and shorten blocks of whitespace to 1 space
|
||||||
let content = match[11] ? match[11].trim().replace(/\s+/g, ' ') : null; // Trim edge spaces and shorten blocks of whitespace to 1 space
|
let content = match[11] || null;
|
||||||
|
|
||||||
// In case of nested (), find the correct matching end )
|
// In case of nested (), find the correct matching end )
|
||||||
let level = 0;
|
let level = 0;
|
||||||
@@ -675,10 +713,8 @@ function MarkedVariables() {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(i > -1) {
|
|
||||||
combinedRegex.lastIndex = combinedRegex.lastIndex - (content.length - i);
|
combinedRegex.lastIndex = combinedRegex.lastIndex - (content.length - i);
|
||||||
content = content.slice(0, i).trim().replace(/\s+/g, ' ');
|
content = content.slice(0, i).trim().replace(/\s+/g, ' ');
|
||||||
}
|
|
||||||
|
|
||||||
varsQueue.push(
|
varsQueue.push(
|
||||||
{ type : 'varDefBlock',
|
{ type : 'varDefBlock',
|
||||||
@@ -748,11 +784,14 @@ const tableTerminators = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
Marked.use(MarkedVariables());
|
Marked.use(MarkedVariables());
|
||||||
Marked.use({ extensions : [definitionListsMultiLine, definitionListsSingleLine, forcedParagraphBreaks, superSubScripts,
|
Marked.use({ extensions : [justifiedParagraphs, definitionListsMultiLine, definitionListsSingleLine, forcedParagraphBreaks,
|
||||||
mustacheSpans, mustacheDivs, mustacheInjectInline] });
|
nonbreakingSpaces, mustacheSpans, mustacheDivs, mustacheInjectInline] });
|
||||||
Marked.use(mustacheInjectBlock);
|
Marked.use(mustacheInjectBlock);
|
||||||
|
Marked.use(patchHardBreaks);
|
||||||
|
Marked.use(MarkedSubSuperText());
|
||||||
Marked.use({ renderer: renderer, tokenizer: tokenizer, mangle: false });
|
Marked.use({ renderer: renderer, tokenizer: tokenizer, mangle: false });
|
||||||
Marked.use(MarkedExtendedTables(tableTerminators), MarkedGFMHeadingId({ globalSlugs: true }), MarkedSmartypantsLite(), MarkedEmojis(MarkedEmojiOptions));
|
Marked.use(MarkedExtendedTables({interruptPatterns : tableTerminators}), MarkedGFMHeadingId({ globalSlugs: true }),
|
||||||
|
MarkedSmartypantsLite(), MarkedEmojis(MarkedEmojiOptions));
|
||||||
|
|
||||||
function cleanUrl(href) {
|
function cleanUrl(href) {
|
||||||
try {
|
try {
|
||||||
@@ -813,15 +852,20 @@ const processStyleTags = (string)=>{
|
|||||||
const index = attr.indexOf('=');
|
const index = attr.indexOf('=');
|
||||||
let [key, value] = [attr.substring(0, index), attr.substring(index + 1)];
|
let [key, value] = [attr.substring(0, index), attr.substring(index + 1)];
|
||||||
value = value.replace(/"/g, '');
|
value = value.replace(/"/g, '');
|
||||||
obj[key] = value;
|
obj[key.trim()] = value.trim();
|
||||||
return obj;
|
return obj;
|
||||||
}, {}) || null;
|
}, {}) || null;
|
||||||
const styles = tags?.length ? tags.map((tag)=>tag.replace(/:"?([^"]*)"?/g, ':$1;').trim()).join(' ') : null;
|
const styles = tags?.length ? tags.reduce((styleObj, style) => {
|
||||||
|
const index = style.indexOf(':');
|
||||||
|
const [key, value] = [style.substring(0, index), style.substring(index + 1)];
|
||||||
|
styleObj[key.trim()] = value.replace(/"?([^"]*)"?/g, '$1').trim();
|
||||||
|
return styleObj;
|
||||||
|
}, {}) : null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id : id,
|
id : id,
|
||||||
classes : classes,
|
classes : classes,
|
||||||
styles : styles,
|
styles : _.isEmpty(styles) ? null : styles,
|
||||||
attributes : _.isEmpty(attributes) ? null : attributes
|
attributes : _.isEmpty(attributes) ? null : attributes
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -831,25 +875,40 @@ const extractHTMLStyleTags = (htmlString)=>{
|
|||||||
const firstElementOnly = htmlString.split('>')[0];
|
const firstElementOnly = htmlString.split('>')[0];
|
||||||
const id = firstElementOnly.match(/id="([^"]*)"/)?.[1] || null;
|
const id = firstElementOnly.match(/id="([^"]*)"/)?.[1] || null;
|
||||||
const classes = firstElementOnly.match(/class="([^"]*)"/)?.[1] || null;
|
const classes = firstElementOnly.match(/class="([^"]*)"/)?.[1] || null;
|
||||||
const styles = firstElementOnly.match(/style="([^"]*)"/)?.[1] || null;
|
const styles = firstElementOnly.match(/style="([^"]*)"/)?.[1]
|
||||||
|
?.split(';').reduce((styleObj, style) => {
|
||||||
|
if (style.trim() === '') return styleObj;
|
||||||
|
const index = style.indexOf(':');
|
||||||
|
const [key, value] = [style.substring(0, index), style.substring(index + 1)];
|
||||||
|
styleObj[key.trim()] = value.trim();
|
||||||
|
return styleObj;
|
||||||
|
}, {}) || null;
|
||||||
const attributes = firstElementOnly.match(/[a-zA-Z]+="[^"]*"/g)
|
const attributes = firstElementOnly.match(/[a-zA-Z]+="[^"]*"/g)
|
||||||
?.filter((attr)=>!attr.startsWith('class="') && !attr.startsWith('style="') && !attr.startsWith('id="'))
|
?.filter((attr)=>!attr.startsWith('class="') && !attr.startsWith('style="') && !attr.startsWith('id="'))
|
||||||
.reduce((obj, attr)=>{
|
.reduce((obj, attr)=>{
|
||||||
const index = attr.indexOf('=');
|
const index = attr.indexOf('=');
|
||||||
let [key, value] = [attr.substring(0, index), attr.substring(index + 1)];
|
let [key, value] = [attr.substring(0, index), attr.substring(index + 1)];
|
||||||
value = value.replace(/"/g, '');
|
obj[key.trim()] = value.replace(/"/g, '');
|
||||||
obj[key] = value;
|
|
||||||
return obj;
|
return obj;
|
||||||
}, {}) || null;
|
}, {}) || null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id : id,
|
id : id,
|
||||||
classes : classes,
|
classes : classes,
|
||||||
styles : styles,
|
styles : _.isEmpty(styles) ? null : styles,
|
||||||
attributes : _.isEmpty(attributes) ? null : attributes
|
attributes : _.isEmpty(attributes) ? null : attributes
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const mergeHTMLTags = (originalTags, newTags) => {
|
||||||
|
return {
|
||||||
|
id : newTags.id || originalTags.id || null,
|
||||||
|
classes : [originalTags.classes, newTags.classes].join(' ').trim() || null,
|
||||||
|
styles : Object.assign(originalTags.styles ?? {}, newTags.styles ?? {}),
|
||||||
|
attributes : Object.assign(originalTags.attributes ?? {}, newTags.attributes ?? {})
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const globalVarsList = {};
|
const globalVarsList = {};
|
||||||
let varsQueue = [];
|
let varsQueue = [];
|
||||||
let globalPageNumber = 0;
|
let globalPageNumber = 0;
|
||||||
|
|||||||
@@ -43,5 +43,6 @@ html,body, #reactRoot{
|
|||||||
}
|
}
|
||||||
&:disabled{
|
&:disabled{
|
||||||
background-color : @silver !important;
|
background-color : @silver !important;
|
||||||
|
cursor:not-allowed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
const stylelint = require('stylelint');
|
import stylelint from 'stylelint';
|
||||||
const { isNumber } = require('stylelint/lib/utils/validateTypes.cjs');
|
import { isNumber } from 'stylelint/lib/utils/validateTypes.mjs';
|
||||||
|
|
||||||
const { report, ruleMessages, validateOptions } = stylelint.utils;
|
const { report, ruleMessages, validateOptions } = stylelint.utils;
|
||||||
const ruleName = 'naturalcrit/declaration-block-multi-line-min-declarations';
|
const ruleName = 'naturalcrit/declaration-block-multi-line-min-declarations';
|
||||||
@@ -7,9 +7,8 @@ const messages = ruleMessages(ruleName, {
|
|||||||
expected : (decls)=>`Rule with ${decls} declaration${decls == 1 ? '' : 's'} should be single line`,
|
expected : (decls)=>`Rule with ${decls} declaration${decls == 1 ? '' : 's'} should be single line`,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const ruleFunction = (primaryOption, secondaryOptionObject, context)=>{
|
||||||
module.exports = stylelint.createPlugin(ruleName, function getPlugin(primaryOption, secondaryOptionObject, context) {
|
return (postcssRoot, postcssResult)=>{
|
||||||
return function lint(postcssRoot, postcssResult) {
|
|
||||||
|
|
||||||
const validOptions = validateOptions(
|
const validOptions = validateOptions(
|
||||||
postcssResult,
|
postcssResult,
|
||||||
@@ -20,26 +19,23 @@ module.exports = stylelint.createPlugin(ruleName, function getPlugin(primaryOpti
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
if(!validOptions) { //If the options are invalid, don't lint
|
if(!validOptions) //If the options are invalid, don't lint
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
const isAutoFixing = Boolean(context.fix);
|
const isAutoFixing = Boolean(context.fix);
|
||||||
|
|
||||||
postcssRoot.walkRules((rule)=>{ //Iterate CSS rules
|
postcssRoot.walkRules((rule)=>{ //Iterate CSS rules
|
||||||
|
|
||||||
//Apply rule only if all children are decls (no further nested rules)
|
//Apply rule only if all children are decls (no further nested rules)
|
||||||
if(rule.nodes.length > primaryOption || !rule.nodes.every((node)=>node.type === 'decl')) {
|
if(rule.nodes.length > primaryOption || !rule.nodes.every((node)=>node.type === 'decl'))
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
//Ignore if already one line
|
//Ignore if already one line
|
||||||
if(!rule.nodes.some((node)=>node.raws.before.includes('\n')) && !rule.raws.after.includes('\n'))
|
if(!rule.nodes.some((node)=>node.raws.before.includes('\n')) && !rule.raws.after.includes('\n'))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if(isAutoFixing) { //We are in “fix” mode
|
if(isAutoFixing) { //We are in “fix” mode
|
||||||
rule.each((decl)=>{
|
rule.each((decl)=>decl.raws.before = ' ');
|
||||||
decl.raws.before = ' ';
|
|
||||||
});
|
|
||||||
rule.raws.after = ' ';
|
rule.raws.after = ' ';
|
||||||
} else {
|
} else {
|
||||||
report({
|
report({
|
||||||
@@ -52,7 +48,9 @@ module.exports = stylelint.createPlugin(ruleName, function getPlugin(primaryOpti
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
});
|
};
|
||||||
|
|
||||||
module.exports.ruleName = ruleName;
|
ruleFunction.ruleName = ruleName;
|
||||||
module.exports.messages = messages;
|
ruleFunction.messages = messages;
|
||||||
|
|
||||||
|
export default stylelint.createPlugin(ruleName, ruleFunction);
|
||||||
|
|||||||
@@ -1,32 +1,29 @@
|
|||||||
const stylelint = require('stylelint');
|
import stylelint from 'stylelint';
|
||||||
|
|
||||||
const { report, ruleMessages, validateOptions } = stylelint.utils;
|
const { report, ruleMessages, validateOptions } = stylelint.utils;
|
||||||
|
|
||||||
const ruleName = 'naturalcrit/declaration-colon-align';
|
const ruleName = 'naturalcrit/declaration-colon-align';
|
||||||
const messages = ruleMessages(ruleName, {
|
const messages = ruleMessages(ruleName, {
|
||||||
expected : (rule)=>`Expected colons aligned within rule "${rule}"`,
|
expected : (rule)=>`Expected colons aligned within rule "${rule}"`,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const ruleFunction = (primaryOption, secondaryOptionObject, context)=>{
|
||||||
module.exports = stylelint.createPlugin(ruleName, function getPlugin(primaryOption, secondaryOptionObject, context) {
|
return (postcssRoot, postcssResult)=>{
|
||||||
return function lint(postcssRoot, postcssResult) {
|
|
||||||
|
|
||||||
const validOptions = validateOptions(
|
const validOptions = validateOptions(
|
||||||
postcssResult,
|
postcssResult,
|
||||||
ruleName,
|
ruleName,
|
||||||
{
|
{
|
||||||
actual : primaryOption,
|
actual : primaryOption,
|
||||||
possible : [
|
possible : [true, false]
|
||||||
true,
|
|
||||||
false
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
if(!validOptions) { //If the options are invalid, don't lint
|
if(!validOptions) // If the options are invalid, don't lint
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
const isAutoFixing = Boolean(context.fix);
|
const isAutoFixing = Boolean(context.fix);
|
||||||
postcssRoot.walkRules((rule)=>{ //Iterate CSS rules
|
|
||||||
|
postcssRoot.walkRules((rule)=>{ // Iterate CSS rules
|
||||||
|
|
||||||
let maxColonPos = 0;
|
let maxColonPos = 0;
|
||||||
let misaligned = false;
|
let misaligned = false;
|
||||||
@@ -36,21 +33,24 @@ module.exports = stylelint.createPlugin(ruleName, function getPlugin(primaryOpti
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
const colonPos = declaration.prop.length + declaration.raws.between.indexOf(':');
|
const colonPos = declaration.prop.length + declaration.raws.between.indexOf(':');
|
||||||
if(maxColonPos > 0 && colonPos != maxColonPos) {
|
|
||||||
|
if(maxColonPos > 0 && colonPos != maxColonPos)
|
||||||
misaligned = true;
|
misaligned = true;
|
||||||
}
|
|
||||||
maxColonPos = Math.max(maxColonPos, colonPos);
|
maxColonPos = Math.max(maxColonPos, colonPos);
|
||||||
});
|
});
|
||||||
|
|
||||||
if(misaligned) {
|
if(!misaligned)
|
||||||
if(isAutoFixing) { //We are in “fix” mode
|
return;
|
||||||
|
|
||||||
|
if(isAutoFixing) { // We are in “fix” mode
|
||||||
rule.each((declaration)=>{
|
rule.each((declaration)=>{
|
||||||
if(declaration.type != 'decl')
|
if(declaration.type != 'decl')
|
||||||
return;
|
return;
|
||||||
|
|
||||||
declaration.raws.between = `${' '.repeat(maxColonPos - declaration.prop.length)}:${declaration.raws.between.split(':')[1]}`;
|
declaration.raws.between = `${' '.repeat(maxColonPos - declaration.prop.length)}:${declaration.raws.between.split(':')[1]}`;
|
||||||
});
|
});
|
||||||
} else { //We are in “report only” mode
|
} else { // We are in “report only” mode
|
||||||
report({
|
report({
|
||||||
ruleName,
|
ruleName,
|
||||||
result : postcssResult,
|
result : postcssResult,
|
||||||
@@ -59,10 +59,11 @@ module.exports = stylelint.createPlugin(ruleName, function getPlugin(primaryOpti
|
|||||||
word : rule.selector, // Which exact word caused the error? This positions the error properly
|
word : rule.selector, // Which exact word caused the error? This positions the error properly
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
});
|
};
|
||||||
|
|
||||||
module.exports.ruleName = ruleName;
|
ruleFunction.ruleName = ruleName;
|
||||||
module.exports.messages = messages;
|
ruleFunction.messages = messages;
|
||||||
|
|
||||||
|
export default stylelint.createPlugin(ruleName, ruleFunction);
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
const stylelint = require('stylelint');
|
import stylelint from 'stylelint';
|
||||||
const { isNumber } = require('stylelint/lib/utils/validateTypes.cjs');
|
import { isNumber } from 'stylelint/lib/utils/validateTypes.mjs';
|
||||||
|
|
||||||
const { report, ruleMessages, validateOptions } = stylelint.utils;
|
const { report, ruleMessages, validateOptions } = stylelint.utils;
|
||||||
const ruleName = 'naturalcrit/declaration-colon-min-space-before';
|
const ruleName = 'naturalcrit/declaration-colon-min-space-before';
|
||||||
@@ -7,9 +7,8 @@ const messages = ruleMessages(ruleName, {
|
|||||||
expected : (num)=>`Expected at least ${num} space${num == 1 ? '' : 's'} before ":"`
|
expected : (num)=>`Expected at least ${num} space${num == 1 ? '' : 's'} before ":"`
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const ruleFunction = (primaryOption, secondaryOptionObject, context)=>{
|
||||||
module.exports = stylelint.createPlugin(ruleName, function getPlugin(primaryOption, secondaryOptionObject, context) {
|
return (postcssRoot, postcssResult)=>{
|
||||||
return function lint(postcssRoot, postcssResult) {
|
|
||||||
|
|
||||||
const validOptions = validateOptions(
|
const validOptions = validateOptions(
|
||||||
postcssResult,
|
postcssResult,
|
||||||
@@ -30,9 +29,9 @@ module.exports = stylelint.createPlugin(ruleName, function getPlugin(primaryOpti
|
|||||||
const between = decl.raws.between;
|
const between = decl.raws.between;
|
||||||
const colonIndex = between.indexOf(':');
|
const colonIndex = between.indexOf(':');
|
||||||
|
|
||||||
if(between.slice(0, colonIndex).length >= primaryOption) {
|
if(between.slice(0, colonIndex).length >= primaryOption)
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
if(isAutoFixing) { //We are in “fix” mode
|
if(isAutoFixing) { //We are in “fix” mode
|
||||||
decl.raws.between = between.slice(0, colonIndex).replace(/\s*$/, ' '.repeat(primaryOption)) + between.slice(colonIndex);
|
decl.raws.between = between.slice(0, colonIndex).replace(/\s*$/, ' '.repeat(primaryOption)) + between.slice(colonIndex);
|
||||||
} else {
|
} else {
|
||||||
@@ -46,7 +45,9 @@ module.exports = stylelint.createPlugin(ruleName, function getPlugin(primaryOpti
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
});
|
};
|
||||||
|
|
||||||
module.exports.ruleName = ruleName;
|
ruleFunction.ruleName = ruleName;
|
||||||
module.exports.messages = messages;
|
ruleFunction.messages = messages;
|
||||||
|
|
||||||
|
export default stylelint.createPlugin(ruleName, ruleFunction);
|
||||||
@@ -92,12 +92,12 @@ describe('Multiline Definition Lists', ()=>{
|
|||||||
test('Multiline Definition Term must have at least one non-empty Definition', function() {
|
test('Multiline Definition Term must have at least one non-empty Definition', function() {
|
||||||
const source = 'Term 1\n::';
|
const source = 'Term 1\n::';
|
||||||
const rendered = Markdown.render(source).trim();
|
const rendered = Markdown.render(source).trim();
|
||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p>Term 1</p>\n<div class='blank'></div><div class='blank'></div>`);
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p>Term 1</p>\n<br>\n<br>`);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Multiline Definition List must have at least one non-newline character after ::', function() {
|
test('Multiline Definition List must have at least one non-newline character after ::', function() {
|
||||||
const source = 'Term 1\n::\nDefinition 1\n\n';
|
const source = 'Term 1\n::\nDefinition 1\n\n';
|
||||||
const rendered = Markdown.render(source).trim();
|
const rendered = Markdown.render(source).trim();
|
||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p>Term 1</p>\n<div class='blank'></div><div class='blank'></div>\n<p>Definition 1</p>`);
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p>Term 1</p>\n<br>\n<br>\n<p>Definition 1</p>`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,37 +6,37 @@ describe('Hard Breaks', ()=>{
|
|||||||
test('Single Break', function() {
|
test('Single Break', function() {
|
||||||
const source = ':\n\n';
|
const source = ':\n\n';
|
||||||
const rendered = Markdown.render(source).trim();
|
const rendered = Markdown.render(source).trim();
|
||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class='blank'></div>`);
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<br>`);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Double Break', function() {
|
test('Double Break', function() {
|
||||||
const source = '::\n\n';
|
const source = '::\n\n';
|
||||||
const rendered = Markdown.render(source).trim();
|
const rendered = Markdown.render(source).trim();
|
||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class='blank'></div><div class='blank'></div>`);
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<br>\n<br>`);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Triple Break', function() {
|
test('Triple Break', function() {
|
||||||
const source = ':::\n\n';
|
const source = ':::\n\n';
|
||||||
const rendered = Markdown.render(source).trim();
|
const rendered = Markdown.render(source).trim();
|
||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class='blank'></div><div class='blank'></div><div class='blank'></div>`);
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<br>\n<br>\n<br>`);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Many Break', function() {
|
test('Many Break', function() {
|
||||||
const source = '::::::::::\n\n';
|
const source = '::::::::::\n\n';
|
||||||
const rendered = Markdown.render(source).trim();
|
const rendered = Markdown.render(source).trim();
|
||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class='blank'></div><div class='blank'></div><div class='blank'></div><div class='blank'></div><div class='blank'></div><div class='blank'></div><div class='blank'></div><div class='blank'></div><div class='blank'></div><div class='blank'></div>`);
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<br>\n<br>\n<br>\n<br>\n<br>\n<br>\n<br>\n<br>\n<br>\n<br>`);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Multiple sets of Breaks', function() {
|
test('Multiple sets of Breaks', function() {
|
||||||
const source = ':::\n:::\n:::';
|
const source = ':::\n:::\n:::';
|
||||||
const rendered = Markdown.render(source).trim();
|
const rendered = Markdown.render(source).trim();
|
||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class='blank'></div><div class='blank'></div><div class='blank'></div>\n<div class='blank'></div><div class='blank'></div><div class='blank'></div>\n<div class='blank'></div><div class='blank'></div><div class='blank'></div>`);
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<br>\n<br>\n<br>\n<br>\n<br>\n<br>\n<br>\n<br>\n<br>`);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Break directly between two paragraphs', function() {
|
test('Break directly between two paragraphs', function() {
|
||||||
const source = 'Line 1\n::\nLine 2';
|
const source = 'Line 1\n::\nLine 2';
|
||||||
const rendered = Markdown.render(source).trim();
|
const rendered = Markdown.render(source).trim();
|
||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p>Line 1</p>\n<div class='blank'></div><div class='blank'></div>\n<p>Line 2</p>`);
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p>Line 1</p>\n<br>\n<br>\n<p>Line 2</p>`);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Ignored inside a code block', function() {
|
test('Ignored inside a code block', function() {
|
||||||
|
|||||||
@@ -300,7 +300,7 @@ describe('Injection: When an injection tag follows an element', ()=>{
|
|||||||
it('Renders a span "text" with its own styles, appended with injected styles', function() {
|
it('Renders a span "text" with its own styles, appended with injected styles', function() {
|
||||||
const source = '{{color:blue,height:10px text}}{width:10px,color:red}';
|
const source = '{{color:blue,height:10px text}}{width:10px,color:red}';
|
||||||
const rendered = Markdown.render(source);
|
const rendered = Markdown.render(source);
|
||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" style="color:blue; height:10px; width:10px; color:red;">text</span>');
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" style="color:red; height:10px; width:10px;">text</span>');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Renders a span "text" with its own classes, appended with injected classes', function() {
|
it('Renders a span "text" with its own classes, appended with injected classes', function() {
|
||||||
@@ -429,7 +429,7 @@ describe('Injection: When an injection tag follows an element', ()=>{
|
|||||||
}}
|
}}
|
||||||
{width:10px,color:red}`;
|
{width:10px,color:red}`;
|
||||||
const rendered = Markdown.render(source).trimReturns();
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<div class="block" style="color:blue; height:10px; width:10px; color:red;"><p>text</p></div>');
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<div class="block" style="color:red; height:10px; width:10px;"><p>text</p></div>');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Renders a span "text" with its own classes, appended with injected classes', function() {
|
it('Renders a span "text" with its own classes, appended with injected classes', function() {
|
||||||
|
|||||||
72
tests/markdown/non-breaking-spaces.test.js
Normal file
72
tests/markdown/non-breaking-spaces.test.js
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
/* eslint-disable max-lines */
|
||||||
|
|
||||||
|
import Markdown from 'naturalcrit/markdown.js';
|
||||||
|
|
||||||
|
describe('Non-Breaking Spaces', ()=>{
|
||||||
|
test('Single Space', function() {
|
||||||
|
const source = ':>\n\n';
|
||||||
|
const rendered = Markdown.render(source).trim();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p> </p>`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Double Space', function() {
|
||||||
|
const source = ':>>\n\n';
|
||||||
|
const rendered = Markdown.render(source).trim();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p> </p>`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Triple Space', function() {
|
||||||
|
const source = ':>>>\n\n';
|
||||||
|
const rendered = Markdown.render(source).trim();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p> </p>`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Many Space', function() {
|
||||||
|
const source = ':>>>>>>>>>>\n\n';
|
||||||
|
const rendered = Markdown.render(source).trim();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p> </p>`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Multiple sets of Spaces', function() {
|
||||||
|
const source = ':>>>\n:>>>\n:>>>';
|
||||||
|
const rendered = Markdown.render(source).trim();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p> \n \n </p>`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Pair of inline Spaces', function() {
|
||||||
|
const source = ':>>:>>';
|
||||||
|
const rendered = Markdown.render(source).trim();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p> </p>`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Space directly between two paragraphs', function() {
|
||||||
|
const source = 'Line 1\n:>>\nLine 2';
|
||||||
|
const rendered = Markdown.render(source).trim();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p>Line 1\n \nLine 2</p>`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Ignored inside a code block', function() {
|
||||||
|
const source = '```\n\n:>\n\n```\n';
|
||||||
|
const rendered = Markdown.render(source).trim();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<pre><code>\n:>\n</code></pre>`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('I am actually a single-line definition list!', function() {
|
||||||
|
const source = 'Term ::> Definition 1\n';
|
||||||
|
const rendered = Markdown.render(source).trim();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<dl><dt>Term</dt><dd>> Definition 1</dd>\n</dl>`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('I am actually a definition list!', function() {
|
||||||
|
const source = 'Term\n::> Definition 1\n';
|
||||||
|
const rendered = Markdown.render(source).trim();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<dl><dt>Term</dt>\n<dd>> Definition 1</dd></dl>`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('I am actually a two-term definition list!', function() {
|
||||||
|
const source = 'Term\n::> Definition 1\n::>> Definition 2';
|
||||||
|
const rendered = Markdown.render(source).trim();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<dl><dt>Term</dt>\n<dd>> Definition 1</dd>\n<dd>>> Definition 2</dd></dl>`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
27
tests/markdown/paragraph-justification.test.js
Normal file
27
tests/markdown/paragraph-justification.test.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
/* eslint-disable max-lines */
|
||||||
|
|
||||||
|
import Markdown from 'naturalcrit/markdown.js';
|
||||||
|
|
||||||
|
describe('Justification', ()=>{
|
||||||
|
test('Left Justify', function() {
|
||||||
|
const source = ':- Hello';
|
||||||
|
const rendered = Markdown.render(source);
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p align=\"Left\">Hello</p>`);
|
||||||
|
});
|
||||||
|
test('Right Justify', function() {
|
||||||
|
const source = '-: Hello';
|
||||||
|
const rendered = Markdown.render(source);
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p align=\"Right\">Hello</p>`);
|
||||||
|
});
|
||||||
|
test('Center Justify', function() {
|
||||||
|
const source = ':-: Hello';
|
||||||
|
const rendered = Markdown.render(source);
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p align=\"Center\">Hello</p>`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Ignored inside a code block', function() {
|
||||||
|
const source = '```\n\n:- Hello\n\n```\n';
|
||||||
|
const rendered = Markdown.render(source);
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<pre><code>\n:- Hello\n</code></pre>\n`);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -403,3 +403,11 @@ describe('Variable names that are subsets of other names', ()=>{
|
|||||||
expect(rendered).toBe('<p>14</p>');
|
expect(rendered).toBe('<p>14</p>');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Regression Tests', ()=>{
|
||||||
|
it('Don\'t Eat all the parentheticals!', function() {
|
||||||
|
const source='\n| title 1 | title 2 | title 3 | title 4|\n|-----------|---------|---------|--------|\n|[foo](bar) | Ipsum | ) | ) |\n';
|
||||||
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
|
expect(rendered).toBe('<table><thead><tr><th>title 1</th><th>title 2</th><th>title 3</th><th>title 4</th></tr></thead><tbody><tr><td><a href=\"bar\">foo</a></td><td>Ipsum</td><td>)</td><td>)</td></tr></tbody></table>');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -58,7 +58,9 @@ body {
|
|||||||
text-rendering : optimizeLegibility;
|
text-rendering : optimizeLegibility;
|
||||||
page-break-before : always;
|
page-break-before : always;
|
||||||
page-break-after : always;
|
page-break-after : always;
|
||||||
contain : size;
|
contain : strict;
|
||||||
|
content-visibility : auto;
|
||||||
|
contain-intrinsic-size : auto none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.phb{
|
.phb{
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ 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 5 ft., one target. *Hit:* 5 (1d6 + 2) `;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@@ -161,8 +161,8 @@ module.exports = {
|
|||||||
*${getType()}, ${getAlignment()}*
|
*${getType()}, ${getAlignment()}*
|
||||||
___
|
___
|
||||||
**Armor Class** :: ${_.random(10, 20)} (chain mail, shield)
|
**Armor Class** :: ${_.random(10, 20)} (chain mail, shield)
|
||||||
**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 |
|
||||||
|:-----:|:-----:|:-----:|:-----:|:-----:|:-----:|
|
|:-----:|:-----:|:-----:|:-----:|:-----:|:-----:|
|
||||||
|
|||||||
@@ -17,7 +17,6 @@
|
|||||||
.useSansSerif() {
|
.useSansSerif() {
|
||||||
font-family : 'ScalySansRemake';
|
font-family : 'ScalySansRemake';
|
||||||
font-size : 0.318cm;
|
font-size : 0.318cm;
|
||||||
line-height : 1.2em;
|
|
||||||
p,dl,ul,ol { line-height : 1.2em; }
|
p,dl,ul,ol { line-height : 1.2em; }
|
||||||
ul, ol { padding-left : 1em; }
|
ul, ol { padding-left : 1em; }
|
||||||
em { font-style : italic; }
|
em { font-style : italic; }
|
||||||
@@ -58,11 +57,12 @@
|
|||||||
ul {
|
ul {
|
||||||
padding-left : 1.4em;
|
padding-left : 1.4em;
|
||||||
margin-bottom : 0.8em;
|
margin-bottom : 0.8em;
|
||||||
line-height : 1.25em;
|
|
||||||
}
|
}
|
||||||
ol {
|
ol {
|
||||||
padding-left : 1.4em;
|
padding-left : 1.4em;
|
||||||
margin-bottom : 0.8em;
|
margin-bottom : 0.8em;
|
||||||
|
}
|
||||||
|
.page li p {
|
||||||
line-height : 1.25em;
|
line-height : 1.25em;
|
||||||
}
|
}
|
||||||
//Indents after p or lists
|
//Indents after p or lists
|
||||||
@@ -138,6 +138,9 @@
|
|||||||
line-height : 0.951em; //Font is misaligned. Shift up slightly
|
line-height : 0.951em; //Font is misaligned. Shift up slightly
|
||||||
& + * { margin-top : 0.2cm; }
|
& + * { margin-top : 0.2cm; }
|
||||||
}
|
}
|
||||||
|
br + h3, br + h4 {
|
||||||
|
margin-top : 0;
|
||||||
|
}
|
||||||
// *****************************
|
// *****************************
|
||||||
// * TABLE
|
// * TABLE
|
||||||
// *****************************/
|
// *****************************/
|
||||||
@@ -622,7 +625,6 @@
|
|||||||
left : 0;
|
left : 0;
|
||||||
filter : drop-shadow(0 0 0.075cm black);
|
filter : drop-shadow(0 0 0.075cm black);
|
||||||
img {
|
img {
|
||||||
width : 100%;
|
|
||||||
height : 2cm;
|
height : 2cm;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -667,7 +669,6 @@
|
|||||||
left : 0;
|
left : 0;
|
||||||
height : 2cm;
|
height : 2cm;
|
||||||
img {
|
img {
|
||||||
width : 100%;
|
|
||||||
height : 2cm;
|
height : 2cm;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -679,18 +680,17 @@
|
|||||||
padding : 2.25cm 1.3cm 2cm 1.3cm;
|
padding : 2.25cm 1.3cm 2cm 1.3cm;
|
||||||
color : #FFFFFF;
|
color : #FFFFFF;
|
||||||
columns : 1;
|
columns : 1;
|
||||||
|
line-height : 1.4em;
|
||||||
&::after { display : none; }
|
&::after { display : none; }
|
||||||
.columnWrapper { width : 7.6cm; }
|
.columnWrapper { width : 7.6cm; }
|
||||||
.backCover {
|
.backCover {
|
||||||
position : absolute;
|
position : absolute;
|
||||||
inset : 0;
|
inset : 0;
|
||||||
z-index : -1;
|
z-index : -1;
|
||||||
width : 11cm;
|
|
||||||
background-image : @backCover;
|
background-image : @backCover;
|
||||||
background-repeat : no-repeat;
|
background-repeat : no-repeat;
|
||||||
background-size : contain;
|
background-size : contain;
|
||||||
}
|
}
|
||||||
.blank { height : 1.4em; }
|
|
||||||
h1 {
|
h1 {
|
||||||
margin-bottom : 0.3cm;
|
margin-bottom : 0.3cm;
|
||||||
font-family : 'NodestoCapsCondensed';
|
font-family : 'NodestoCapsCondensed';
|
||||||
@@ -737,7 +737,6 @@
|
|||||||
img {
|
img {
|
||||||
position : relative;
|
position : relative;
|
||||||
z-index : 0;
|
z-index : 0;
|
||||||
width : 100%;
|
|
||||||
height : 1.5cm;
|
height : 1.5cm;
|
||||||
}
|
}
|
||||||
p {
|
p {
|
||||||
|
|||||||
@@ -54,10 +54,12 @@ body { counter-reset : page-numbers 0; }
|
|||||||
overflow : hidden;
|
overflow : hidden;
|
||||||
background-color : var(--HB_Color_Background);
|
background-color : var(--HB_Color_Background);
|
||||||
text-rendering : optimizeLegibility;
|
text-rendering : optimizeLegibility;
|
||||||
contain : size;
|
contain : strict;
|
||||||
|
content-visibility : auto;
|
||||||
|
contain-intrinsic-size : auto none;
|
||||||
}
|
}
|
||||||
//*****************************
|
//*****************************
|
||||||
// * BASE
|
// * BASE
|
||||||
// *****************************/
|
// *****************************/
|
||||||
.page {
|
.page {
|
||||||
p {
|
p {
|
||||||
@@ -425,17 +427,6 @@ body { counter-reset : page-numbers 0; }
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//*****************************
|
|
||||||
// * BLANK LINE
|
|
||||||
// *****************************/
|
|
||||||
.page {
|
|
||||||
.blank {
|
|
||||||
height : 1em;
|
|
||||||
margin-top : 0;
|
|
||||||
& + * { margin-top : 0; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//*****************************
|
//*****************************
|
||||||
// * WIDE
|
// * WIDE
|
||||||
// *****************************/
|
// *****************************/
|
||||||
|
|||||||
Reference in New Issue
Block a user