mirror of
https://github.com/naturalcrit/homebrewery.git
synced 2026-01-27 00:53:13 +00:00
Compare commits
427 Commits
pr/3263
...
calculusch
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f23959bb05 | ||
|
|
228041913e | ||
|
|
fc53989946 | ||
|
|
9f72dc08c6 | ||
|
|
1018ba554f | ||
|
|
709c9ece74 | ||
|
|
19e51102d2 | ||
|
|
68a68bde82 | ||
|
|
e1186b4a1e | ||
|
|
9e71945a76 | ||
|
|
9f6fc3d1ac | ||
|
|
e22830eb4a | ||
|
|
f02a3d815a | ||
|
|
7097897df8 | ||
|
|
9247967f93 | ||
|
|
1b3d82fc04 | ||
|
|
810934f2c1 | ||
|
|
3b507a1fb9 | ||
|
|
4f1056a320 | ||
|
|
963aa8f003 | ||
|
|
431dfd7780 | ||
|
|
4f0cbd82d4 | ||
|
|
53e437c6bc | ||
|
|
0e5c91733d | ||
|
|
f5c729c328 | ||
|
|
4f9e93fac7 | ||
|
|
7cc83eaf95 | ||
|
|
22b01b131f | ||
|
|
2045bf8060 | ||
|
|
df73b37180 | ||
|
|
99f9b10348 | ||
|
|
ed85f77c48 | ||
|
|
777438fd94 | ||
|
|
08406de5cc | ||
|
|
e1599909bc | ||
|
|
e7eda1f5ec | ||
|
|
9f2aaf01c7 | ||
|
|
882c78fbb5 | ||
|
|
a79b2fb755 | ||
|
|
97fba241a1 | ||
|
|
d0fbca7af5 | ||
|
|
36726c747c | ||
|
|
74580e63d6 | ||
|
|
1d8eb35c64 | ||
|
|
659472578b | ||
|
|
9ec549b496 | ||
|
|
e37c190600 | ||
|
|
d51d7efdcf | ||
|
|
b0a16c8daf | ||
|
|
beb86c1820 | ||
|
|
48f8026c35 | ||
|
|
1be0a2dac3 | ||
|
|
d31e495d07 | ||
|
|
3b2a48eabf | ||
|
|
4bc76a0766 | ||
|
|
3faa23c6eb | ||
|
|
e324de8f4f | ||
|
|
7954ae8692 | ||
|
|
85a00b508b | ||
|
|
1e91d7256c | ||
|
|
8a15172db1 | ||
|
|
aae574e4e5 | ||
|
|
45106b47d4 | ||
|
|
59b3038b9b | ||
|
|
31cf8b7d28 | ||
|
|
9f6bc10369 | ||
|
|
f7c7d40195 | ||
|
|
380e3fead3 | ||
|
|
ffa01c7f1d | ||
|
|
15367f9444 | ||
|
|
90a710907e | ||
|
|
da6d06728c | ||
|
|
451ff3ffec | ||
|
|
61038f876d | ||
|
|
20e1c71eff | ||
|
|
d2d7f5b71e | ||
|
|
0ad4cb7cfd | ||
|
|
161efbb3c8 | ||
|
|
b35739c5c1 | ||
|
|
831c635149 | ||
|
|
d961e7695d | ||
|
|
4b842ef37f | ||
|
|
0396bedcd0 | ||
|
|
772b478682 | ||
|
|
dab8dd278d | ||
|
|
72d26c6c7e | ||
|
|
a7f07ab9f5 | ||
|
|
75080135af | ||
|
|
7a9483c0d0 | ||
|
|
484c6bb8a7 | ||
|
|
125adfb198 | ||
|
|
a0c6e92016 | ||
|
|
7de58740a2 | ||
|
|
bae56b8b9d | ||
|
|
e02d925a49 | ||
|
|
03f868d084 | ||
|
|
920c4cd7cb | ||
|
|
bf2c638cad | ||
|
|
eddb513e72 | ||
|
|
fea68ac71a | ||
|
|
97b10c685c | ||
|
|
40fc422ab5 | ||
|
|
647afba2b0 | ||
|
|
bd324a7e74 | ||
|
|
ac080c8323 | ||
|
|
30ebf90371 | ||
|
|
f9a7adbd72 | ||
|
|
b74fb22182 | ||
|
|
b6ea89356b | ||
|
|
37488ded4d | ||
|
|
c47dd828ed | ||
|
|
a34ed8ccb4 | ||
|
|
d4d0546d61 | ||
|
|
e94c1d33d2 | ||
|
|
dd6388bf9f | ||
|
|
8b8071a903 | ||
|
|
19746a78f4 | ||
|
|
13d679c4bf | ||
|
|
a851378a2f | ||
|
|
e3b2d33a5e | ||
|
|
e4b7d0be64 | ||
|
|
7e48696fb5 | ||
|
|
b727c56e56 | ||
|
|
7f168f35b8 | ||
|
|
21c0916693 | ||
|
|
8f0fb6e458 | ||
|
|
4606ad4c6e | ||
|
|
bf267436b0 | ||
|
|
2af44665e1 | ||
|
|
e412a379e3 | ||
|
|
f37da19649 | ||
|
|
d7756230fb | ||
|
|
d6aa0138f0 | ||
|
|
f3b17f4615 | ||
|
|
04e6f2ea58 | ||
|
|
ed6718eef1 | ||
|
|
e36a638ae5 | ||
|
|
d1c2e74ed6 | ||
|
|
5a410029f6 | ||
|
|
005fb705fa | ||
|
|
b4fec32320 | ||
|
|
d0000cee11 | ||
|
|
846b3b9d02 | ||
|
|
54d881642d | ||
|
|
f6c0b0d6fc | ||
|
|
08a3d7367b | ||
|
|
cd537f98c7 | ||
|
|
8ab91c3eb2 | ||
|
|
8c773ced9c | ||
|
|
4cc2acc9e6 | ||
|
|
f903e97562 | ||
|
|
b57e42be6f | ||
|
|
9f95947d16 | ||
|
|
d129fc14c1 | ||
|
|
58422569c9 | ||
|
|
f172d02920 | ||
|
|
dbe60e3ff1 | ||
|
|
beaf67c975 | ||
|
|
8e6f0a7dbb | ||
|
|
b2b276c3a3 | ||
|
|
7483b4afc6 | ||
|
|
c7a2e84927 | ||
|
|
3482330629 | ||
|
|
a74b556188 | ||
|
|
1721b9087a | ||
|
|
c871548877 | ||
|
|
190ecddc2a | ||
|
|
0a24ac9f25 | ||
|
|
d8d580c277 | ||
|
|
be08887e50 | ||
|
|
5043313aca | ||
|
|
83d2a604e1 | ||
|
|
02509ebc3a | ||
|
|
e494899f8b | ||
|
|
9f483bba6f | ||
|
|
f93af38fa6 | ||
|
|
6eecd9cee4 | ||
|
|
7e847cc139 | ||
|
|
99daaf5537 | ||
|
|
24421ca4e7 | ||
|
|
25945fc0df | ||
|
|
b50353c8c4 | ||
|
|
4f4b43f49d | ||
|
|
cfdc3e6870 | ||
|
|
f71c3da568 | ||
|
|
e472465ce7 | ||
|
|
776b6da79f | ||
|
|
1e4a00ce56 | ||
|
|
ed6bca04f5 | ||
|
|
dbfc1e7d28 | ||
|
|
cc9ead20b3 | ||
|
|
79c89bb621 | ||
|
|
cf1617f2a3 | ||
|
|
7ecca16fd0 | ||
|
|
1e1505c63f | ||
|
|
1bddb38fcc | ||
|
|
3314471d73 | ||
|
|
15f95ddd44 | ||
|
|
4dd8ad77bb | ||
|
|
9f5a4fb44c | ||
|
|
7f16da110f | ||
|
|
3734c9e71d | ||
|
|
6b93b1c1e7 | ||
|
|
3d7780958a | ||
|
|
59e87697ff | ||
|
|
20c088b6de | ||
|
|
50f069e688 | ||
|
|
148c243d9f | ||
|
|
16e4f66b99 | ||
|
|
774b555a61 | ||
|
|
5b35d1169d | ||
|
|
0d1d3a180d | ||
|
|
df3db14e8b | ||
|
|
0310eee685 | ||
|
|
c4499fcc26 | ||
|
|
1d317788fe | ||
|
|
b5301ff978 | ||
|
|
a027e16636 | ||
|
|
673a22a571 | ||
|
|
1c7d2740f3 | ||
|
|
3778b4c719 | ||
|
|
3e258332c1 | ||
|
|
713865fb40 | ||
|
|
bc29cddcec | ||
|
|
f9d8344dba | ||
|
|
9d38c937b4 | ||
|
|
622579bb7d | ||
|
|
26263c0bf8 | ||
|
|
05f88dfd00 | ||
|
|
d1d28acebb | ||
|
|
84d5b26530 | ||
|
|
16c37d8d76 | ||
|
|
802da2920b | ||
|
|
a608df9cd9 | ||
|
|
254b0852ca | ||
|
|
8e41755d2a | ||
|
|
36eb250d7a | ||
|
|
a2b97abb2e | ||
|
|
0331f5cf5b | ||
|
|
54159513e4 | ||
|
|
99a766d990 | ||
|
|
1ec0cb14ef | ||
|
|
387f03db25 | ||
|
|
0c20a65999 | ||
|
|
da8836ba99 | ||
|
|
bf874c55af | ||
|
|
43a76933ab | ||
|
|
ce9013fed2 | ||
|
|
e4632ea340 | ||
|
|
f21eaab997 | ||
|
|
f48b9c46fe | ||
|
|
7d08731094 | ||
|
|
bd97d78195 | ||
|
|
b1a3fcf33c | ||
|
|
453656fbeb | ||
|
|
7d755fe2a3 | ||
|
|
ba1430c377 | ||
|
|
7698abe70f | ||
|
|
fd58503a77 | ||
|
|
8aa7a26183 | ||
|
|
ad5ad05b7b | ||
|
|
c0827d6db0 | ||
|
|
43209186ee | ||
|
|
46a0a66fb6 | ||
|
|
9e6f7a496e | ||
|
|
94d9e1b08e | ||
|
|
087ce4bb90 | ||
|
|
2c02d4174f | ||
|
|
4feeaee7e7 | ||
|
|
5f5185c619 | ||
|
|
3e76307303 | ||
|
|
fe3a5f4def | ||
|
|
5d752f3355 | ||
|
|
8f400236a7 | ||
|
|
8779ee3325 | ||
|
|
e268858945 | ||
|
|
0e48f325f2 | ||
|
|
7e38271ac6 | ||
|
|
ff75afa91f | ||
|
|
4866eacd5d | ||
|
|
1386020bbb | ||
|
|
6b7af58e6c | ||
|
|
0f1d07d90f | ||
|
|
d5230757b1 | ||
|
|
06825468b4 | ||
|
|
faab60f271 | ||
|
|
4bed2349a9 | ||
|
|
53f1e53fcb | ||
|
|
df447d3d4d | ||
|
|
e3bf913a80 | ||
|
|
7bb1f16946 | ||
|
|
6870fd6d76 | ||
|
|
9ad1d1f196 | ||
|
|
969cff61bf | ||
|
|
736f729457 | ||
|
|
0050e1e294 | ||
|
|
f2d1b61a7a | ||
|
|
1d1fa99b4b | ||
|
|
f42cab6e40 | ||
|
|
bc21abd509 | ||
|
|
72744718cc | ||
|
|
e85a62a05c | ||
|
|
5a68acc0f5 | ||
|
|
f51fca74e6 | ||
|
|
fe5a76c0df | ||
|
|
3bda834ad3 | ||
|
|
29f0a8e635 | ||
|
|
c035404555 | ||
|
|
74ddc71962 | ||
|
|
1491a1b4ff | ||
|
|
6890bab668 | ||
|
|
209c237b73 | ||
|
|
086468d65b | ||
|
|
323d84974c | ||
|
|
6365fb9b56 | ||
|
|
19bb9705b6 | ||
|
|
705d170b6e | ||
|
|
ab54188ba4 | ||
|
|
e0b6b95295 | ||
|
|
c7cfade86f | ||
|
|
e4fa59aae8 | ||
|
|
35227268cf | ||
|
|
67b11d62ea | ||
|
|
2b81c26cff | ||
|
|
32b5bebbc4 | ||
|
|
c27f5d9efa | ||
|
|
26a41e6262 | ||
|
|
87c9e587a1 | ||
|
|
f3148ed53c | ||
|
|
ea320e0cc4 | ||
|
|
7f8b87bb85 | ||
|
|
5e97121e5a | ||
|
|
c9fc976c72 | ||
|
|
9e8570c19b | ||
|
|
ed2d539995 | ||
|
|
13fbcd0eb1 | ||
|
|
43b9f3d901 | ||
|
|
3ee9fe1c3f | ||
|
|
65cc8567a1 | ||
|
|
dd82f54549 | ||
|
|
a014056440 | ||
|
|
c176c38f30 | ||
|
|
4c87aed628 | ||
|
|
09f2f96dff | ||
|
|
703e207970 | ||
|
|
f075b19a68 | ||
|
|
283c2b5ae1 | ||
|
|
f1c3507a9f | ||
|
|
29dc1e0747 | ||
|
|
265e9976b9 | ||
|
|
70657c16d1 | ||
|
|
7177548c0e | ||
|
|
e1ad05eb3a | ||
|
|
e4324f316d | ||
|
|
46140e92fd | ||
|
|
3bc2df0ac5 | ||
|
|
77abab8395 | ||
|
|
8ab273363c | ||
|
|
84de560083 | ||
|
|
2b1b0acefc | ||
|
|
7782e200af | ||
|
|
9e4344de83 | ||
|
|
760269a6e1 | ||
|
|
faba9f1616 | ||
|
|
79e8dfec18 | ||
|
|
c930ae87f4 | ||
|
|
c747c5577e | ||
|
|
ce8cbba441 | ||
|
|
c0cabbb563 | ||
|
|
e56ff93db1 | ||
|
|
033776168e | ||
|
|
d0ccc4a15a | ||
|
|
2f383d59b6 | ||
|
|
c9c5176f1b | ||
|
|
fe0cfcb2b6 | ||
|
|
0470d13ae0 | ||
|
|
d09dc11f5f | ||
|
|
4c2211c428 | ||
|
|
d076d6c719 | ||
|
|
20b76bdead | ||
|
|
9621a69b7d | ||
|
|
0ac88bd84a | ||
|
|
90977521df | ||
|
|
09a52bc7cb | ||
|
|
6ef80eed7f | ||
|
|
993ae295af | ||
|
|
dfd5b228c2 | ||
|
|
4dd58aaad3 | ||
|
|
96d973528c | ||
|
|
3a4de13551 | ||
|
|
7dfc857217 | ||
|
|
d5980cba89 | ||
|
|
0a1c04d003 | ||
|
|
eeba037244 | ||
|
|
d8524c3a84 | ||
|
|
75809a5f42 | ||
|
|
2f13b89510 | ||
|
|
34be05ac51 | ||
|
|
d262f586fc | ||
|
|
efecfac68a | ||
|
|
2c997458b2 | ||
|
|
7e98f79416 | ||
|
|
cfecc001aa | ||
|
|
ab8716d071 | ||
|
|
9e12ab71f8 | ||
|
|
995d1c63d8 | ||
|
|
875e1023fc | ||
|
|
6c4dad675f | ||
|
|
632882d370 | ||
|
|
041c7ed48f | ||
|
|
6dcc6d36b7 | ||
|
|
0762b82c40 | ||
|
|
c6821819c7 | ||
|
|
5a79795e4f | ||
|
|
0624f8a0b9 | ||
|
|
c78dcbfe05 | ||
|
|
827fdd3cff | ||
|
|
a6ecec4172 | ||
|
|
b561f69dd0 | ||
|
|
1c5c21d89b | ||
|
|
d1152dcbb5 | ||
|
|
e6428a3b18 | ||
|
|
ce1ba8289c | ||
|
|
3d5f99adae | ||
|
|
a889fa657e | ||
|
|
4347debf45 | ||
|
|
dc3243ae59 |
@@ -64,6 +64,12 @@ jobs:
|
|||||||
- run:
|
- run:
|
||||||
name: Test - Mustache Spans
|
name: Test - Mustache Spans
|
||||||
command: npm run test:mustache-syntax
|
command: npm run test:mustache-syntax
|
||||||
|
- run:
|
||||||
|
name: Test - Definition Lists
|
||||||
|
command: npm run test:definition-lists
|
||||||
|
- run:
|
||||||
|
name: Test - Variables
|
||||||
|
command: npm run test:variables
|
||||||
- run:
|
- run:
|
||||||
name: Test - Routes
|
name: Test - Routes
|
||||||
command: npm run test:route
|
command: npm run test:route
|
||||||
|
|||||||
103
.github/workflows/limit-pull-requests.yml
vendored
Normal file
103
.github/workflows/limit-pull-requests.yml
vendored
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
name: Limit pull requests
|
||||||
|
description: >
|
||||||
|
Limit the number of open pull requests to the repository created by a user
|
||||||
|
author: ZhongRuoyu (from Homebrew repository)
|
||||||
|
branding:
|
||||||
|
icon: alert-triangle
|
||||||
|
color: yellow
|
||||||
|
|
||||||
|
inputs:
|
||||||
|
token:
|
||||||
|
description: GitHub token
|
||||||
|
required: false
|
||||||
|
default: ${{ github.token }}
|
||||||
|
except-users:
|
||||||
|
description: The users exempted from the limit, one per line
|
||||||
|
required: false
|
||||||
|
# https://docs.github.com/en/graphql/reference/enums#commentauthorassociation
|
||||||
|
except-author-associations:
|
||||||
|
description: The author associations exempted from the limit, one per line
|
||||||
|
required: false
|
||||||
|
comment-limit:
|
||||||
|
description: >
|
||||||
|
Post the comment when the user's number of open pull requests exceeds this
|
||||||
|
number and `comment` is not empty
|
||||||
|
required: true
|
||||||
|
default: "10"
|
||||||
|
comment:
|
||||||
|
description: The comment to post when the limit is reached
|
||||||
|
required: false
|
||||||
|
close-limit:
|
||||||
|
description: >
|
||||||
|
Close the pull request when the user's number of open pull requests
|
||||||
|
exceeds this number and `close` is set to `true`
|
||||||
|
required: true
|
||||||
|
default: "50"
|
||||||
|
close:
|
||||||
|
description: Whether to close the pull request when the limit is reached
|
||||||
|
required: true
|
||||||
|
default: "false"
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: composite
|
||||||
|
steps:
|
||||||
|
- name: Check the number of pull requests
|
||||||
|
id: count-pull-requests
|
||||||
|
run: |
|
||||||
|
# If the user is exempted, assume they have no pull requests.
|
||||||
|
if grep -Fiqx '${{ github.actor }}' <<<"$EXCEPT_USERS"; then
|
||||||
|
echo "::notice::@${{ github.actor }} is exempted from the limit."
|
||||||
|
echo "count=0" >>"$GITHUB_OUTPUT"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
if grep -Fiqx '${{ github.event.pull_request.author_association }}' <<<"$EXCEPT_AUTHOR_ASSOCIATIONS"; then
|
||||||
|
echo "::notice::@{{ github.actor }} is a ${{ github.event.pull_request.author_association }} exempted from the limit."
|
||||||
|
echo "count=0" >>"$GITHUB_OUTPUT"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
count="$(
|
||||||
|
gh api \
|
||||||
|
--method GET \
|
||||||
|
--header 'Accept: application/vnd.github+json' \
|
||||||
|
--header 'X-GitHub-Api-Version: 2022-11-28' \
|
||||||
|
--field state=open \
|
||||||
|
--paginate \
|
||||||
|
'/repos/{owner}/{repo}/pulls' |
|
||||||
|
jq \
|
||||||
|
--raw-output \
|
||||||
|
--arg USER '${{ github.actor }}' \
|
||||||
|
'map(select(.user.login == $USER)) | length'
|
||||||
|
)"
|
||||||
|
echo "::notice::@${{ github.actor }} has $count open pull request(s)."
|
||||||
|
echo "count=$count" >>"$GITHUB_OUTPUT"
|
||||||
|
env:
|
||||||
|
GH_REPO: ${{ github.repository }}
|
||||||
|
GH_TOKEN: ${{ inputs.token }}
|
||||||
|
EXCEPT_USERS: ${{ inputs.except-users }}
|
||||||
|
EXCEPT_AUTHOR_ASSOCIATIONS: ${{ inputs.except-author-associations }}
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Comment on pull request
|
||||||
|
if: >
|
||||||
|
fromJSON(steps.count-pull-requests.outputs.count) > fromJSON(inputs.comment-limit) &&
|
||||||
|
inputs.comment != ''
|
||||||
|
run: |
|
||||||
|
gh pr comment '${{ github.event.pull_request.number }}' \
|
||||||
|
--body="${COMMENT_BODY}"
|
||||||
|
env:
|
||||||
|
GH_REPO: ${{ github.repository }}
|
||||||
|
GH_TOKEN: ${{ inputs.token }}
|
||||||
|
COMMENT_BODY: ${{ inputs.comment }}
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Close pull request
|
||||||
|
if: >
|
||||||
|
fromJSON(steps.count-pull-requests.outputs.count) > fromJSON(inputs.close-limit) &&
|
||||||
|
inputs.close == 'true'
|
||||||
|
run: |
|
||||||
|
gh pr close '${{ github.event.pull_request.number }}'
|
||||||
|
env:
|
||||||
|
GH_REPO: ${{ github.repository }}
|
||||||
|
GH_TOKEN: ${{ inputs.token }}
|
||||||
|
shell: bash
|
||||||
25
.github/workflows/pr-check.yml
vendored
Normal file
25
.github/workflows/pr-check.yml
vendored
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
name: PR Check
|
||||||
|
on: pull_request_target
|
||||||
|
env:
|
||||||
|
GH_REPO: ${{ github.repository }}
|
||||||
|
GH_NO_UPDATE_NOTIFIER: 1
|
||||||
|
GH_PROMPT_DISABLED: 1
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
issues: write
|
||||||
|
pull-requests: write
|
||||||
|
statuses: write
|
||||||
|
jobs:
|
||||||
|
limit-pull-requests:
|
||||||
|
if: always() && github.repository_owner == 'Homebrew'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: Homebrew/actions/limit-pull-requests@master
|
||||||
|
with:
|
||||||
|
except-users: |
|
||||||
|
dependabot
|
||||||
|
comment-limit: 1
|
||||||
|
comment: |
|
||||||
|
Hi, thanks for your contribution to the Homebrewery! You already have >=3 open pull requests. Consider completing some of your existing PRs before opening new ones. Thanks!
|
||||||
|
close-limit: 5
|
||||||
|
close: false
|
||||||
244
changelog.md
244
changelog.md
@@ -75,11 +75,253 @@ pre {
|
|||||||
.page {
|
.page {
|
||||||
padding-bottom: 1.5cm;
|
padding-bottom: 1.5cm;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.varSyntaxTable th:first-of-type {
|
||||||
|
width:6cm;
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## 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 18/3/2024 - v3.12.0
|
||||||
|
{{taskList
|
||||||
|
|
||||||
|
##### 5e-Cleric
|
||||||
|
|
||||||
|
* [x] Fix language-specific hyphenation on print page
|
||||||
|
|
||||||
|
Fixes issue [#3294](https://github.com/naturalcrit/homebrewery/issues/3294)
|
||||||
|
|
||||||
|
* [x] Upgrade Font-Awesome to v6.51
|
||||||
|
|
||||||
|
* [x] Allow downloaded files to be uploaded via {{openSans **NEW {{fa,fa-plus-square}} → FROM UPLOAD {{fa,fa-upload}}**}}
|
||||||
|
|
||||||
|
##### G-Ambatte
|
||||||
|
|
||||||
|
* [x] Fix an edge case crash with empty documents
|
||||||
|
|
||||||
|
Fixes issue [#3315](https://github.com/naturalcrit/homebrewery/issues/3315)
|
||||||
|
|
||||||
|
* [x] Brews on the user page can be searched by tag; clicking a tag adds it to the filter
|
||||||
|
|
||||||
|
Fixes issue [#3164](https://github.com/naturalcrit/homebrewery/issues/3164)
|
||||||
|
|
||||||
|
* [x] Add *DiceFont* icons {{df,d20-20}} `{{df,icon-name}}`
|
||||||
|
|
||||||
|
##### abquintic
|
||||||
|
|
||||||
|
* [x] Fix ^super^ and ^^sub^^ highlighting in the text editor
|
||||||
|
|
||||||
|
* [x] Add new syntax for multiline Definition Lists:
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
Term
|
||||||
|
::Definition 1
|
||||||
|
::Definition 2
|
||||||
|
with more text
|
||||||
|
```
|
||||||
|
|
||||||
|
produces:
|
||||||
|
|
||||||
|
Term
|
||||||
|
::Definition 1
|
||||||
|
::Definition 2
|
||||||
|
with more text
|
||||||
|
|
||||||
|
Fixes issue [#2340](https://github.com/naturalcrit/homebrewery/issues/2340)
|
||||||
|
|
||||||
|
##### RKuerten :
|
||||||
|
* [x] Fix monster stat block backgrounds on print page
|
||||||
|
|
||||||
|
Fixes issue [#3275](https://github.com/naturalcrit/homebrewery/issues/3275)
|
||||||
|
|
||||||
|
* [x] Added new text editor theme: "Darkvision".
|
||||||
|
|
||||||
|
##### calculuschild, G-Ambatte, 5e-Cleric
|
||||||
|
|
||||||
|
* [x] Codebase and UI cleanup
|
||||||
|
}}
|
||||||
|
|
||||||
|
\page
|
||||||
|
|
||||||
|
|
||||||
|
### Friday 21/2/2024 - v3.11.0
|
||||||
|
{{taskList
|
||||||
|
|
||||||
|
##### Gazook89
|
||||||
|
|
||||||
|
* [x] Brew view count no longer increases when viewed by owner
|
||||||
|
|
||||||
|
Fixes issue [#3037](https://github.com/naturalcrit/homebrewery/issues/3037)
|
||||||
|
|
||||||
|
* [x] Small tweak to PHB H3 sizing
|
||||||
|
|
||||||
|
Fixes issue [#2989](https://github.com/naturalcrit/homebrewery/issues/2989)
|
||||||
|
|
||||||
|
* [x] Add **Fold/Unfold All** {{fas,fa-compress-alt}} / {{fas,fa-expand-alt}} buttons to editor bar
|
||||||
|
|
||||||
|
Fixes issue [#2965](https://github.com/naturalcrit/homebrewery/issues/2965)
|
||||||
|
|
||||||
|
|
||||||
|
##### G-Ambatte
|
||||||
|
|
||||||
|
* [x] Share link added to Editor Access error page
|
||||||
|
|
||||||
|
Fixes issue [#3086](https://github.com/naturalcrit/homebrewery/issues/3086)
|
||||||
|
|
||||||
|
* [x] Add Darkbrewery theme to Editor theme selector {{fas,fa-palette}}
|
||||||
|
|
||||||
|
Fixes issue [#3034](https://github.com/naturalcrit/homebrewery/issues/3034)
|
||||||
|
|
||||||
|
* [x] Fix Firefox prints with alternating blank pages
|
||||||
|
|
||||||
|
Fixes issue [#3115](https://github.com/naturalcrit/homebrewery/issues/3115)
|
||||||
|
|
||||||
|
* [x] Admin page working again
|
||||||
|
|
||||||
|
Fixes issue [#2657](https://github.com/naturalcrit/homebrewery/issues/2657)
|
||||||
|
|
||||||
|
|
||||||
|
##### 5e-Cleric
|
||||||
|
|
||||||
|
* [x] Fix indenting issue with Monster Blocks and italics in Class Feature
|
||||||
|
|
||||||
|
Fixes issues [#527](https://github.com/naturalcrit/homebrewery/issues/527),
|
||||||
|
[#3247](https://github.com/naturalcrit/homebrewery/issues/3247)
|
||||||
|
|
||||||
|
* [x] Allow CSS vars in curly syntax to be formatted as strings using single quotes
|
||||||
|
|
||||||
|
`{{--customVar:"'a string'"}}`
|
||||||
|
|
||||||
|
Fixes issue [#3066](https://github.com/naturalcrit/homebrewery/issues/3066)
|
||||||
|
|
||||||
|
* [x] Add *Elderberry Inn* icons {{ei,action}} `{{ei,icon-name}}`
|
||||||
|
|
||||||
|
Fixes issue [#3171](https://github.com/naturalcrit/homebrewery/issues/3171)
|
||||||
|
|
||||||
|
* [x] New {{openSans **{{fas,fa-keyboard}} FONTS** }} snippets!
|
||||||
|
|
||||||
|
Fixes issue [#3171](https://github.com/naturalcrit/homebrewery/issues/3171)
|
||||||
|
|
||||||
|
* [x] New page now opens in a new tab
|
||||||
|
|
||||||
|
|
||||||
|
##### abquintic (new contributor!)
|
||||||
|
|
||||||
|
* [x] Add ^super^ `^abc^` and ^^sub^^ `^^abc^^` syntax.
|
||||||
|
|
||||||
|
Fixes issue [#2171](https://github.com/naturalcrit/homebrewery/issues/2171)
|
||||||
|
|
||||||
|
* [x] Add HTML tag assignment to curly syntax `{{tag=value}}`
|
||||||
|
|
||||||
|
Fixes issue [1488](https://github.com/naturalcrit/homebrewery/issues/1488)
|
||||||
|
|
||||||
|
* [x] {{openSans **Brew → Clone to New**}} now clones tags
|
||||||
|
|
||||||
|
Fixes issue [1488](https://github.com/naturalcrit/homebrewery/issues/1488)
|
||||||
|
|
||||||
|
##### calculuschild
|
||||||
|
|
||||||
|
* [x] Better error messages for "Out of Google Drive Storage" and "Not logged in to edit"
|
||||||
|
|
||||||
|
Fixes issues [2510](https://github.com/naturalcrit/homebrewery/issues/2510),
|
||||||
|
[2975](https://github.com/naturalcrit/homebrewery/issues/2975)
|
||||||
|
|
||||||
|
* [x] Brew Variables
|
||||||
|
}}
|
||||||
|
|
||||||
|
\
|
||||||
|
|
||||||
|
{{wide
|
||||||
|
|
||||||
|
### Brew Variable Syntax
|
||||||
|
|
||||||
|
You may already be familiar with `[link](url)` and `` synax. We have expanded this to include a third `$[variable](text)` syntax. All three of these syntaxes now share a common set of features:
|
||||||
|
|
||||||
|
{{varSyntaxTable
|
||||||
|
| syntax | description |
|
||||||
|
|:-------|-------------|
|
||||||
|
| `[var]:content` | Assigns a variable (must start on a line by itself, and ends at the next blank line) |
|
||||||
|
| `[var](content)` | Assigns a variable and outputs it (can be inline) |
|
||||||
|
| `[var]` | Outputs the variable contents as a link, if formatted as a valid link |
|
||||||
|
| `![var]` | Outputs as an image, if formatted as a valid image |
|
||||||
|
| `$[var]` | Outputs as Markdown |
|
||||||
|
| `$[var1 + var2 - 2 * var3]` | Performs math operations and outputs result if all variables are valid numbers |
|
||||||
|
}}
|
||||||
|
|
||||||
|
}}
|
||||||
|
|
||||||
|
{{wide,margin-top:0,margin-bottom:0
|
||||||
|
### Examples
|
||||||
|
}}
|
||||||
|
|
||||||
|
{{wide,columns:2,margin-top:0,margin-bottom:0
|
||||||
|
|
||||||
|
```
|
||||||
|
[first]: Bob
|
||||||
|
|
||||||
|
[last]: Jones
|
||||||
|
|
||||||
|
My name is $[first] $[last].
|
||||||
|
```
|
||||||
|
|
||||||
|
\column
|
||||||
|
|
||||||
|
[first]: Bob
|
||||||
|
|
||||||
|
[last]: Jones
|
||||||
|
|
||||||
|
My name is $[first] $[last].
|
||||||
|
|
||||||
|
}}
|
||||||
|
|
||||||
|
{{wide,columns:2,margin-top:0,margin-bottom:0
|
||||||
|
|
||||||
|
```
|
||||||
|
[myTable]:
|
||||||
|
| h1 | h2 |
|
||||||
|
|----|----|
|
||||||
|
| c1 | c2 |
|
||||||
|
|
||||||
|
Here is my table:
|
||||||
|
$[myTable]
|
||||||
|
```
|
||||||
|
|
||||||
|
\column
|
||||||
|
|
||||||
|
[myTable]:
|
||||||
|
| h1 | h2 |
|
||||||
|
|----|----|
|
||||||
|
| c1 | c2 |
|
||||||
|
|
||||||
|
Here is my table:
|
||||||
|
$[myTable]
|
||||||
|
}}
|
||||||
|
|
||||||
|
{{wide,columns:2,margin-top:0,margin-bottom:0
|
||||||
|
|
||||||
|
```
|
||||||
|
There are $[TableNum] tables total.
|
||||||
|
|
||||||
|
#### Table $[TableNum](1): Horses
|
||||||
|
|
||||||
|
#### Table $[TableNum]($[TableNum + 1]): Cows
|
||||||
|
```
|
||||||
|
|
||||||
|
\column
|
||||||
|
|
||||||
|
There are $[TableNum] tables in this document. *(note: final value of `$[TableNum]` gets hoisted up if available)*
|
||||||
|
|
||||||
|
|
||||||
|
#### Table $[TableNum](1): Horses
|
||||||
|
|
||||||
|
#### Table $[TableNum]($[TableNum + 1]): Cows
|
||||||
|
}}
|
||||||
|
|
||||||
|
\page
|
||||||
|
|
||||||
### Friday 13/10/2023 - v3.10.0
|
### Friday 13/10/2023 - v3.10.0
|
||||||
{{taskList
|
{{taskList
|
||||||
|
|
||||||
@@ -1335,7 +1577,7 @@ myStyle {color: black}
|
|||||||
### Sunday, 29/05/2016 - v2.1.0
|
### Sunday, 29/05/2016 - v2.1.0
|
||||||
- Finally added a syntax for doing spell lists. A bit in-depth about why this took so long. Essentially I'm running out of syntax to use in stardard Markdown. There are too many unique elements in the PHB-style to be mapped. I solved this earlier by stacking certain elements together (eg. an `<hr>` before a `blockquote` turns it into moster state block), but those are getting unweildly. I would like to simply wrap these in `div`s with classes, but unfortunately Markdown stops processing when within HTML blocks. To get around this I wrote my own override to the Markdown parser and lexer to process Markdown within a simple div class wrapper. This should open the door for more unique syntaxes in the future. Big step!
|
- Finally added a syntax for doing spell lists. A bit in-depth about why this took so long. Essentially I'm running out of syntax to use in stardard Markdown. There are too many unique elements in the PHB-style to be mapped. I solved this earlier by stacking certain elements together (eg. an `<hr>` before a `blockquote` turns it into moster state block), but those are getting unweildly. I would like to simply wrap these in `div`s with classes, but unfortunately Markdown stops processing when within HTML blocks. To get around this I wrote my own override to the Markdown parser and lexer to process Markdown within a simple div class wrapper. This should open the door for more unique syntaxes in the future. Big step!
|
||||||
- Override Ctrl+P (and cmd+P) to launch to the print page. Many people try to just print either the editing or share page to get a PDF. While this dones;t make much sense, I do get a ton of issues about it. So now if you try to do this, it'll just bring you imediately to the print page. Everybody wins!
|
- Override Ctrl+P (and cmd+P) to launch to the print page. Many people try to just print either the editing or share page to get a PDF. While this dones;t make much sense, I do get a ton of issues about it. So now if you try to do this, it'll just bring you imediately to the print page. Everybody wins!
|
||||||
- The onboarding flow has also been confusing a few users (Homepage -> new -> save -> edit page). If you edit the Homepage text now, a Call to Action to save your work will pop-up.
|
- The onboarding flow has also been confusing a few users (Homepage → new → save → edit page). If you edit the Homepage text now, a Call to Action to save your work will pop-up.
|
||||||
- Added a 'Recently Edited' and 'Recently Viewed' nav item to the edit and share page respectively. Each will remember the last 8 items you edited or viewed and when you viewed it. Makes use of the new title attribute of brews to easy navigatation.
|
- Added a 'Recently Edited' and 'Recently Viewed' nav item to the edit and share page respectively. Each will remember the last 8 items you edited or viewed and when you viewed it. Makes use of the new title attribute of brews to easy navigatation.
|
||||||
- Paragraphs now indent properly after lists (thanks u/slitjen!)
|
- Paragraphs now indent properly after lists (thanks u/slitjen!)
|
||||||
|
|
||||||
|
|||||||
@@ -34,8 +34,6 @@ const Stats = createClass({
|
|||||||
<dl>
|
<dl>
|
||||||
<dt>Total Brew Count</dt>
|
<dt>Total Brew Count</dt>
|
||||||
<dd>{this.state.stats.totalBrews}</dd>
|
<dd>{this.state.stats.totalBrews}</dd>
|
||||||
<dt>Total Brews Published Count</dt>
|
|
||||||
<dd>{this.state.stats.totalPublishedBrews || 'no published brews'}</dd>
|
|
||||||
</dl>
|
</dl>
|
||||||
|
|
||||||
{this.state.fetching
|
{this.state.fetching
|
||||||
|
|||||||
@@ -20,9 +20,9 @@ const PAGE_HEIGHT = 1056;
|
|||||||
|
|
||||||
const INITIAL_CONTENT = dedent`
|
const INITIAL_CONTENT = dedent`
|
||||||
<!DOCTYPE html><html><head>
|
<!DOCTYPE html><html><head>
|
||||||
<link href="//use.fontawesome.com/releases/v5.15.1/css/all.css" rel="stylesheet" />
|
<link href="//use.fontawesome.com/releases/v6.5.1/css/all.css" rel="stylesheet" type="text/css" />
|
||||||
<link href="//fonts.googleapis.com/css?family=Open+Sans:400,300,600,700" rel="stylesheet" type="text/css" />
|
<link href="//fonts.googleapis.com/css?family=Open+Sans:400,300,600,700" rel="stylesheet" type="text/css" />
|
||||||
<link href='/homebrew/bundle.css' rel='stylesheet' />
|
<link href='/homebrew/bundle.css' type="text/css" rel='stylesheet' />
|
||||||
<base target=_blank>
|
<base target=_blank>
|
||||||
</head><body style='overflow: hidden'><div></div></body></html>`;
|
</head><body style='overflow: hidden'><div></div></body></html>`;
|
||||||
|
|
||||||
@@ -89,22 +89,24 @@ const BrewRenderer = (props)=>{
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
const shouldRender = (index)=>{
|
const isInView = (index)=>{
|
||||||
if(!state.isMounted) return false;
|
if(!state.isMounted)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if(index == props.currentEditorPage) //Already rendered before this step
|
||||||
|
return false;
|
||||||
|
|
||||||
if(Math.abs(index - state.viewablePageNumber) <= 3)
|
if(Math.abs(index - state.viewablePageNumber) <= 3)
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
if(index + 1 == props.currentEditorPage)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const sanitizeScriptTags = (content)=>{
|
const sanitizeScriptTags = (content)=>{
|
||||||
return content
|
return content
|
||||||
.replace(/<script/ig, '<script')
|
?.replace(/<script/ig, '<script')
|
||||||
.replace(/<\/script>/ig, '</script>');
|
.replace(/<\/script>/ig, '</script>')
|
||||||
|
|| '';
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderPageInfo = ()=>{
|
const renderPageInfo = ()=>{
|
||||||
@@ -138,7 +140,7 @@ const BrewRenderer = (props)=>{
|
|||||||
return <BrewPage className='page phb' index={index} key={index} contents={html} />;
|
return <BrewPage className='page phb' index={index} key={index} contents={html} />;
|
||||||
} else {
|
} else {
|
||||||
cleanPageText += `\n\n \n\\column\n `; //Artificial column break at page end to emulate column-fill:auto (until `wide` is used, when column-fill:balance will reappear)
|
cleanPageText += `\n\n \n\\column\n `; //Artificial column break at page end to emulate column-fill:auto (until `wide` is used, when column-fill:balance will reappear)
|
||||||
const html = Markdown.render(cleanPageText);
|
const html = Markdown.render(cleanPageText, index);
|
||||||
return <BrewPage className='page' index={index} key={index} contents={html} />;
|
return <BrewPage className='page' index={index} key={index} contents={html} />;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -150,8 +152,11 @@ const BrewRenderer = (props)=>{
|
|||||||
if(rawPages.length != renderedPages.length) // Re-render all pages when page count changes
|
if(rawPages.length != renderedPages.length) // Re-render all pages when page count changes
|
||||||
renderedPages.length = 0;
|
renderedPages.length = 0;
|
||||||
|
|
||||||
|
// Render currently-edited page first so cross-page effects (variables, links) can propagate out first
|
||||||
|
renderedPages[props.currentEditorPage] = renderPage(rawPages[props.currentEditorPage], props.currentEditorPage);
|
||||||
|
|
||||||
_.forEach(rawPages, (page, index)=>{
|
_.forEach(rawPages, (page, index)=>{
|
||||||
if((shouldRender(index) || !renderedPages[index]) && typeof window !== 'undefined'){
|
if((isInView(index) || !renderedPages[index]) && typeof window !== 'undefined'){
|
||||||
renderedPages[index] = renderPage(page, index); // Render any page not yet rendered, but only re-render those in PPR range
|
renderedPages[index] = renderPage(page, index); // Render any page not yet rendered, but only re-render those in PPR range
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -206,11 +211,11 @@ const BrewRenderer = (props)=>{
|
|||||||
<RenderWarnings />
|
<RenderWarnings />
|
||||||
<NotificationPopup />
|
<NotificationPopup />
|
||||||
</div>
|
</div>
|
||||||
<link href={`/themes/${rendererPath}/Blank/style.css`} rel='stylesheet'/>
|
<link href={`/themes/${rendererPath}/Blank/style.css`} type="text/css" rel='stylesheet'/>
|
||||||
{baseThemePath &&
|
{baseThemePath &&
|
||||||
<link href={`/themes/${rendererPath}/${baseThemePath}/style.css`} rel='stylesheet'/>
|
<link href={`/themes/${rendererPath}/${baseThemePath}/style.css`} type="text/css" rel='stylesheet'/>
|
||||||
}
|
}
|
||||||
<link href={`/themes/${rendererPath}/${themePath}/style.css`} rel='stylesheet'/>
|
<link href={`/themes/${rendererPath}/${themePath}/style.css`} type="text/css" rel='stylesheet'/>
|
||||||
|
|
||||||
{/* Apply CSS from Style tab and render pages from Markdown tab */}
|
{/* Apply CSS from Style tab and render pages from Markdown tab */}
|
||||||
{state.isMounted
|
{state.isMounted
|
||||||
|
|||||||
@@ -14,6 +14,28 @@
|
|||||||
box-shadow : 1px 4px 14px #000000;
|
box-shadow : 1px 4px 14px #000000;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 20px;
|
||||||
|
&:horizontal{
|
||||||
|
height: 20px;
|
||||||
|
width:auto;
|
||||||
|
}
|
||||||
|
&-thumb {
|
||||||
|
background: linear-gradient(90deg, #d3c1af 15px, #00000000 15px);
|
||||||
|
&:horizontal{
|
||||||
|
background: linear-gradient(0deg, #d3c1af 15px, #00000000 15px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&-corner {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
.pane { position : relative; }
|
.pane { position : relative; }
|
||||||
.pageInfo {
|
.pageInfo {
|
||||||
|
|||||||
@@ -25,13 +25,10 @@ const NotificationPopup = createClass({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<li key='psa'>
|
<li key='psa'>
|
||||||
<em>Broken default logo on <b>CoverPage</b> </em> <br />
|
<em>Don't store IMAGES in Google Drive</em><br />
|
||||||
If you have used the Cover Page snippet and notice the Naturalcrit
|
Google Drive is not an image service, and will block images from being used
|
||||||
logo is showing as a broken image, this is due to some small tweaks
|
in brews if they get more views than expected. Google has confirmed they won't fix
|
||||||
of this BETA feature. To fix the logo in your cover page, rename
|
this, so we recommend you look for another image hosting service such as imgur, ImgBB or Google Photos.
|
||||||
the image link <b>"/assets/naturalCritLogoRed.svg"</b>. Remember
|
|
||||||
that any snippet marked "BETA" may have a similar change in the
|
|
||||||
future as we encounter any bugs or reworks.
|
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li key='googleDriveFolder'>
|
<li key='googleDriveFolder'>
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ const Editor = createClass({
|
|||||||
updateEditorSize : function() {
|
updateEditorSize : function() {
|
||||||
if(this.refs.codeEditor) {
|
if(this.refs.codeEditor) {
|
||||||
let paneHeight = this.refs.main.parentNode.clientHeight;
|
let paneHeight = this.refs.main.parentNode.clientHeight;
|
||||||
paneHeight -= SNIPPETBAR_HEIGHT + 1;
|
paneHeight -= SNIPPETBAR_HEIGHT;
|
||||||
this.refs.codeEditor.codeMirror.setSize(null, paneHeight);
|
this.refs.codeEditor.codeMirror.setSize(null, paneHeight);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -151,30 +151,37 @@ const Editor = createClass({
|
|||||||
|
|
||||||
// definition lists
|
// definition lists
|
||||||
if(line.includes('::')){
|
if(line.includes('::')){
|
||||||
const regex = /^([^\n]*?)::([^\n]*)(?:\n|$)/ym;
|
if(/^:*$/.test(line) == true){ return };
|
||||||
|
const regex = /^([^\n]*?:?\s?)(::[^\n]*)(?:\n|$)/ymd; // the `d` flag, for match indices, throws an ESLint error.
|
||||||
let match;
|
let match;
|
||||||
while ((match = regex.exec(line)) != null){
|
while ((match = regex.exec(line)) != null){
|
||||||
codeMirror.markText({ line: lineNumber, ch: line.indexOf(match[0]) }, { line: lineNumber, ch: line.indexOf(match[0]) + match[0].length }, { className: 'define' });
|
codeMirror.markText({ line: lineNumber, ch: match.indices[0][0] }, { line: lineNumber, ch: match.indices[0][1] }, { className: 'dl-highlight' });
|
||||||
codeMirror.markText({ line: lineNumber, ch: line.indexOf(match[1]) }, { line: lineNumber, ch: line.indexOf(match[1]) + match[1].length }, { className: 'term' });
|
codeMirror.markText({ line: lineNumber, ch: match.indices[1][0] }, { line: lineNumber, ch: match.indices[1][1] }, { className: 'dt-highlight' });
|
||||||
codeMirror.markText({ line: lineNumber, ch: line.indexOf(match[2]) }, { line: lineNumber, ch: line.indexOf(match[2]) + match[2].length }, { className: 'definition' });
|
codeMirror.markText({ line: lineNumber, ch: match.indices[2][0] }, { line: lineNumber, ch: match.indices[2][1] }, { className: 'dd-highlight' });
|
||||||
|
const ddIndex = match.indices[2][0];
|
||||||
|
let colons = /::/g;
|
||||||
|
let colonMatches = colons.exec(match[2]);
|
||||||
|
if(colonMatches !== null){
|
||||||
|
codeMirror.markText({ line: lineNumber, ch: colonMatches.index + ddIndex }, { line: lineNumber, ch: colonMatches.index + colonMatches[0].length + ddIndex }, { className: 'dl-colon-highlight'} )
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Superscript
|
// Subscript & Superscript
|
||||||
if(line.includes('\^')) {
|
if(line.includes('^')) {
|
||||||
const regex = /\^(?!\s)(?=([^\n\^]*[^\s\^]))\1\^/g;
|
let startIndex = line.indexOf('^');
|
||||||
let match;
|
const superRegex = /\^(?!\s)(?=([^\n\^]*[^\s\^]))\1\^/gy;
|
||||||
while ((match = regex.exec(line)) != null) {
|
const subRegex = /\^\^(?!\s)(?=([^\n\^]*[^\s\^]))\1\^\^/gy;
|
||||||
codeMirror.markText({ line: lineNumber, ch: line.indexOf(match[1]) - 1 }, { line: lineNumber, ch: line.indexOf(match[1]) + match[1].length + 1 }, { className: 'superscript' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Subscript
|
while (startIndex >= 0) {
|
||||||
if(line.includes('^^')) {
|
superRegex.lastIndex = subRegex.lastIndex = startIndex;
|
||||||
const regex = /\^\^(?!\s)(?=([^\n\^]*[^\s\^]))\1\^\^/g;
|
let isSuper = false;
|
||||||
let match;
|
let match = subRegex.exec(line) || superRegex.exec(line);
|
||||||
while ((match = regex.exec(line)) != null) {
|
if (match) {
|
||||||
codeMirror.markText({ line: lineNumber, ch: line.indexOf(match[1]) - 2 }, { line: lineNumber, ch: line.indexOf(match[1]) + match[1].length + 2 }, { className: 'subscript' });
|
isSuper = !subRegex.lastIndex;
|
||||||
|
codeMirror.markText({ line: lineNumber, ch: match.index }, { line: lineNumber, ch: match.index + match[0].length }, { className: isSuper ? 'superscript' : 'subscript' });
|
||||||
|
}
|
||||||
|
startIndex = line.indexOf('^', Math.max(startIndex + 1, subRegex.lastIndex, superRegex.lastIndex));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -55,6 +55,16 @@
|
|||||||
vertical-align : sub;
|
vertical-align : sub;
|
||||||
font-size : 0.9em;
|
font-size : 0.9em;
|
||||||
}
|
}
|
||||||
|
.dl-highlight {
|
||||||
|
&.dl-colon-highlight {
|
||||||
|
font-weight : bold;
|
||||||
|
color : #949494;
|
||||||
|
background : #E5E5E5;
|
||||||
|
border-radius : 3px;
|
||||||
|
}
|
||||||
|
&.dt-highlight { color : rgb(96, 117, 143); }
|
||||||
|
&.dd-highlight { color : rgb(97, 57, 178); }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.brewJump {
|
.brewJump {
|
||||||
|
|||||||
@@ -130,6 +130,8 @@
|
|||||||
height : 1.2em;
|
height : 1.2em;
|
||||||
margin-right : 8px;
|
margin-right : 8px;
|
||||||
font-size : 1.2em;
|
font-size : 1.2em;
|
||||||
|
min-width: 25px;
|
||||||
|
text-align: center;
|
||||||
& ~ i {
|
& ~ i {
|
||||||
margin-right : 0;
|
margin-right : 0;
|
||||||
margin-left : 5px;
|
margin-left : 5px;
|
||||||
@@ -138,7 +140,7 @@
|
|||||||
&.font {
|
&.font {
|
||||||
height : auto;
|
height : auto;
|
||||||
&::before {
|
&::before {
|
||||||
font-size : 1.4em;
|
font-size : 1em;
|
||||||
content : 'ABC';
|
content : 'ABC';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ const SharePage = require('./pages/sharePage/sharePage.jsx');
|
|||||||
const NewPage = require('./pages/newPage/newPage.jsx');
|
const NewPage = require('./pages/newPage/newPage.jsx');
|
||||||
const ErrorPage = require('./pages/errorPage/errorPage.jsx');
|
const ErrorPage = require('./pages/errorPage/errorPage.jsx');
|
||||||
const PrintPage = require('./pages/printPage/printPage.jsx');
|
const PrintPage = require('./pages/printPage/printPage.jsx');
|
||||||
const ArchivePage = require('./pages/archivePage/archivePage.jsx');
|
|
||||||
const AccountPage = require('./pages/accountPage/accountPage.jsx');
|
const AccountPage = require('./pages/accountPage/accountPage.jsx');
|
||||||
|
|
||||||
const WithRoute = (props)=>{
|
const WithRoute = (props)=>{
|
||||||
@@ -75,10 +74,9 @@ const Homebrew = createClass({
|
|||||||
<Route path='/user/:username' element={<WithRoute el={UserPage} brews={this.props.brews} />} />
|
<Route path='/user/:username' element={<WithRoute el={UserPage} brews={this.props.brews} />} />
|
||||||
<Route path='/print/:id' element={<WithRoute el={PrintPage} brew={this.props.brew} />} />
|
<Route path='/print/:id' element={<WithRoute el={PrintPage} brew={this.props.brew} />} />
|
||||||
<Route path='/print' element={<WithRoute el={PrintPage} />} />
|
<Route path='/print' element={<WithRoute el={PrintPage} />} />
|
||||||
<Route path='/archive' element={<WithRoute el={ArchivePage}/>}/>
|
|
||||||
<Route path='/changelog' element={<WithRoute el={SharePage} brew={this.props.brew} />} />
|
<Route path='/changelog' element={<WithRoute el={SharePage} brew={this.props.brew} />} />
|
||||||
<Route path='/faq' element={<WithRoute el={SharePage} brew={this.props.brew} />} />
|
<Route path='/faq' element={<WithRoute el={SharePage} brew={this.props.brew} />} />
|
||||||
<Route path='/account' element={<WithRoute el={AccountPage} brew={this.props.brew} uiItems={this.props.brew.uiItems} />} />
|
<Route path='/account' element={<WithRoute el={AccountPage} brew={this.props.brew} accountDetails={this.props.brew.accountDetails} />} />
|
||||||
<Route path='/legacy' element={<WithRoute el={HomePage} brew={this.props.brew} />} />
|
<Route path='/legacy' element={<WithRoute el={HomePage} brew={this.props.brew} />} />
|
||||||
<Route path='/error' element={<WithRoute el={ErrorPage} brew={this.props.brew} />} />
|
<Route path='/error' element={<WithRoute el={ErrorPage} brew={this.props.brew} />} />
|
||||||
<Route path='/' element={<WithRoute el={HomePage} brew={this.props.brew} />} />
|
<Route path='/' element={<WithRoute el={HomePage} brew={this.props.brew} />} />
|
||||||
|
|||||||
@@ -15,6 +15,23 @@
|
|||||||
}
|
}
|
||||||
&.listPage .content {
|
&.listPage .content {
|
||||||
overflow-y : scroll;
|
overflow-y : scroll;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 20px;
|
||||||
|
&:horizontal{
|
||||||
|
height: 20px;
|
||||||
|
width:auto;
|
||||||
|
}
|
||||||
|
&-thumb {
|
||||||
|
background: linear-gradient(90deg, #d3c1af 15px, #00000000 15px);
|
||||||
|
&:horizontal{
|
||||||
|
background: linear-gradient(0deg, #d3c1af 15px, #00000000 15px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&-corner {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,106 +1,286 @@
|
|||||||
@import "naturalcrit/styles/colors.less";
|
@import 'naturalcrit/styles/colors.less';
|
||||||
|
|
||||||
@navbarHeight : 28px;
|
@navbarHeight : 28px;
|
||||||
|
|
||||||
@keyframes pinkColoring {
|
@keyframes pinkColoring {
|
||||||
0% { color : pink; }
|
0% { color : pink; }
|
||||||
50% { color : pink; }
|
50% { color : pink; }
|
||||||
75% { color : red; }
|
75% { color : red; }
|
||||||
100% { color : pink; }
|
100% { color : pink; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes glideDropDown {
|
||||||
|
0% {
|
||||||
|
background-color : #333333;
|
||||||
|
opacity : 0;
|
||||||
|
transform : translate(0px, -100%);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-color : #333333;
|
||||||
|
opacity : 1;
|
||||||
|
transform : translate(0px, 0px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.homebrew nav {
|
.homebrew nav {
|
||||||
.homebrewLogo {
|
background-color : #333333;
|
||||||
|
.navContent {
|
||||||
|
position : relative;
|
||||||
|
z-index : 2;
|
||||||
|
display : flex;
|
||||||
|
justify-content : space-between;
|
||||||
|
}
|
||||||
|
.navSection {
|
||||||
|
display : flex;
|
||||||
|
align-items : center;
|
||||||
|
&:last-child .navItem { border-left : 1px solid #666666; }
|
||||||
|
}
|
||||||
|
// "NaturalCrit" logo
|
||||||
|
.navLogo {
|
||||||
|
display : block;
|
||||||
|
margin-top : 0px;
|
||||||
|
margin-right : 8px;
|
||||||
|
margin-left : 8px;
|
||||||
|
color : white;
|
||||||
|
text-decoration : none;
|
||||||
|
&:hover {
|
||||||
|
.name { color : @orange; }
|
||||||
|
svg { fill : @orange; }
|
||||||
|
}
|
||||||
|
svg {
|
||||||
|
height : 13px;
|
||||||
|
margin-right : 0.2em;
|
||||||
|
cursor : pointer;
|
||||||
|
fill : white;
|
||||||
|
}
|
||||||
|
span.name {
|
||||||
|
font-family : 'CodeLight';
|
||||||
|
font-size : 15px;
|
||||||
|
span.crit { font-family : 'CodeBold'; }
|
||||||
|
small {
|
||||||
|
font-family : 'Open Sans';
|
||||||
|
font-size : 0.3em;
|
||||||
|
font-weight : 800;
|
||||||
|
text-transform : uppercase;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navItem {
|
||||||
|
#backgroundColorsHover;
|
||||||
|
.animate(background-color);
|
||||||
|
padding : 8px 12px;
|
||||||
|
font-size : 10px;
|
||||||
|
font-weight : 800;
|
||||||
|
line-height : 13px;
|
||||||
|
color : white;
|
||||||
|
text-decoration : none;
|
||||||
|
text-transform : uppercase;
|
||||||
|
cursor : pointer;
|
||||||
|
background-color : #333333;
|
||||||
|
i {
|
||||||
|
float : right;
|
||||||
|
margin-left : 5px;
|
||||||
|
font-size : 13px;
|
||||||
|
}
|
||||||
|
&.patreon {
|
||||||
|
border-right : 1px solid #666666;
|
||||||
|
border-left : 1px solid #666666;
|
||||||
|
&:hover i { color : red; }
|
||||||
|
i {
|
||||||
|
color : pink;
|
||||||
.animate(color);
|
.animate(color);
|
||||||
font-family : CodeBold;
|
animation-name : pinkColoring;
|
||||||
|
animation-duration : 2s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.editTitle { // this is not needed at all currently - you used to be able to edit the title via the navbar.
|
||||||
|
padding : 2px 12px;
|
||||||
|
input {
|
||||||
|
width : 250px;
|
||||||
|
padding : 2px;
|
||||||
|
margin : 0;
|
||||||
|
font-family : 'Open Sans', sans-serif;
|
||||||
|
font-size : 12px;
|
||||||
|
font-weight : 800;
|
||||||
|
color : white;
|
||||||
|
text-align : center;
|
||||||
|
background-color : transparent;
|
||||||
|
border : 1px solid @blue;
|
||||||
|
outline : none;
|
||||||
|
}
|
||||||
|
.charCount {
|
||||||
|
display : inline-block;
|
||||||
|
margin-left : 8px;
|
||||||
|
color : #666666;
|
||||||
|
text-align : right;
|
||||||
|
vertical-align : bottom;
|
||||||
|
&.max { color : @red; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.brewTitle {
|
||||||
|
flex-grow : 1;
|
||||||
|
font-size : 12px;
|
||||||
|
font-weight : 800;
|
||||||
|
color : white;
|
||||||
|
text-align : center;
|
||||||
|
text-transform : initial;
|
||||||
|
background-color : transparent;
|
||||||
|
}
|
||||||
|
// "The Homebrewery" logo
|
||||||
|
&.homebrewLogo {
|
||||||
|
.animate(color);
|
||||||
|
font-family : 'CodeBold';
|
||||||
font-size : 12px;
|
font-size : 12px;
|
||||||
color : white;
|
color : white;
|
||||||
div {
|
div {
|
||||||
margin-top : 2px;
|
margin-top : 2px;
|
||||||
margin-bottom : -2px;
|
margin-bottom : -2px;
|
||||||
}
|
}
|
||||||
&:hover {
|
&:hover { color : @blue; }
|
||||||
color : @blue;
|
|
||||||
}
|
}
|
||||||
}
|
&.metadata {
|
||||||
.editTitle.navItem {
|
position : relative;
|
||||||
padding : 2px 12px;
|
display : flex;
|
||||||
input {
|
|
||||||
font-family : "Open Sans", sans-serif;
|
|
||||||
font-size : 12px;
|
|
||||||
font-weight : 800;
|
|
||||||
width : 250px;
|
|
||||||
margin : 0;
|
|
||||||
padding : 2px;
|
|
||||||
text-align : center;
|
|
||||||
color : white;
|
|
||||||
border : 1px solid @blue;
|
|
||||||
outline : none;
|
|
||||||
background-color : transparent;
|
|
||||||
}
|
|
||||||
.charCount {
|
|
||||||
display : inline-block;
|
|
||||||
margin-left : 8px;
|
|
||||||
text-align : right;
|
|
||||||
vertical-align : bottom;
|
|
||||||
color : #666;
|
|
||||||
&.max {
|
|
||||||
color : @red;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.brewTitle.navItem {
|
|
||||||
font-size : 12px;
|
|
||||||
font-weight : 800;
|
|
||||||
height : 100%;
|
|
||||||
text-align : center;
|
|
||||||
text-transform : initial;
|
|
||||||
color : white;
|
|
||||||
background-color : transparent;
|
|
||||||
flex-grow : 1;
|
flex-grow : 1;
|
||||||
}
|
align-items : center;
|
||||||
.save-menu {
|
height : 100%;
|
||||||
.dropdown {
|
padding : 0;
|
||||||
z-index : 1000;
|
i { margin-right : 10px;}
|
||||||
}
|
.window {
|
||||||
.navItem i.fa-power-off {
|
position : absolute;
|
||||||
color : red;
|
bottom : 0;
|
||||||
|
left : 50%;
|
||||||
|
z-index : -1;
|
||||||
|
display : flex;
|
||||||
|
flex-flow : row wrap;
|
||||||
|
align-content : baseline;
|
||||||
|
justify-content : flex-start;
|
||||||
|
width : 440px;
|
||||||
|
max-height : ~'calc(100vh - 28px)';
|
||||||
|
padding : 0 10px 5px;
|
||||||
|
margin : 0 auto;
|
||||||
|
background-color : #333333;
|
||||||
|
border : 3px solid #444444;
|
||||||
|
border-top : unset;
|
||||||
|
border-radius : 0 0 5px 5px;
|
||||||
|
box-shadow : inset 0 7px 9px -7px #111111;
|
||||||
|
transition : transform 0.4s, opacity 0.4s;
|
||||||
&.active {
|
&.active {
|
||||||
color : rgb(0, 182, 52);
|
opacity : 1;
|
||||||
filter : drop-shadow(0 0 2px rgba(0, 182, 52, 0.765));
|
transform : translateX(-50%) translateY(100%);
|
||||||
|
}
|
||||||
|
&.inactive {
|
||||||
|
opacity : 0;
|
||||||
|
transform : translateX(-50%) translateY(0%);
|
||||||
|
}
|
||||||
|
.row {
|
||||||
|
display : flex;
|
||||||
|
flex-flow : row wrap;
|
||||||
|
width : 100%;
|
||||||
|
h4 {
|
||||||
|
box-sizing : border-box;
|
||||||
|
display : block;
|
||||||
|
flex-basis : 20%;
|
||||||
|
flex-grow : 1;
|
||||||
|
min-width : 76px;
|
||||||
|
padding : 5px 0;
|
||||||
|
color : #BBBBBB;
|
||||||
|
text-align : center;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
flex-basis : 80%;
|
||||||
|
flex-grow : 1;
|
||||||
|
padding : 5px 0;
|
||||||
|
font-family : 'Open Sans', sans-serif;
|
||||||
|
font-size : 10px;
|
||||||
|
font-weight : normal;
|
||||||
|
text-transform : initial;
|
||||||
|
.tag {
|
||||||
|
display : inline-block;
|
||||||
|
padding : 2px;
|
||||||
|
margin : 2px 2px;
|
||||||
|
background-color : #444444;
|
||||||
|
border : 2px solid grey;
|
||||||
|
border-radius : 5px;
|
||||||
|
}
|
||||||
|
a.userPageLink {
|
||||||
|
color : white;
|
||||||
|
text-decoration : none;
|
||||||
|
&:hover { text-decoration : underline; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&:nth-of-type(even) { background-color : #555555; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.patreon.navItem {
|
&.warning {
|
||||||
border-right : 1px solid #666;
|
position : relative;
|
||||||
border-left : 1px solid #666;
|
color : white;
|
||||||
&:hover i {
|
background-color : @orange;
|
||||||
color : red;
|
&:hover > .dropdown { visibility : visible; }
|
||||||
}
|
.dropdown {
|
||||||
i {
|
position : absolute;
|
||||||
.animate(color);
|
top : 28px;
|
||||||
animation-name : pinkColoring;
|
left : 0;
|
||||||
animation-duration : 2s;
|
z-index : 10000;
|
||||||
color : pink;
|
box-sizing : border-box;
|
||||||
|
display : block;
|
||||||
|
width : 100%;
|
||||||
|
padding : 13px 5px;
|
||||||
|
text-align : center;
|
||||||
|
visibility : hidden;
|
||||||
|
background-color : #333333;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.recent.navDropdownContainer {
|
&.account {
|
||||||
|
min-width : 100px;
|
||||||
|
&.username { text-transform : none;}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navDropdownContainer {
|
||||||
|
position : relative;
|
||||||
|
.navDropdown {
|
||||||
|
position: absolute;
|
||||||
|
top: 28px;
|
||||||
|
right: 0px;
|
||||||
|
z-index: 10000;
|
||||||
|
width: max-content;
|
||||||
|
min-width:100%;
|
||||||
|
max-height: calc(100vh - 28px);
|
||||||
|
overflow: hidden auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-end;
|
||||||
|
.navItem {
|
||||||
|
position : relative;
|
||||||
|
display : flex;
|
||||||
|
justify-content : space-between;
|
||||||
|
align-items : center;
|
||||||
|
width : 100%;
|
||||||
|
border : 1px solid #888888;
|
||||||
|
border-bottom : 0;
|
||||||
|
animation-name : glideDropDown;
|
||||||
|
animation-duration : 0.4s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.recent {
|
||||||
position : relative;
|
position : relative;
|
||||||
.navDropdown .navItem {
|
.navDropdown .navItem {
|
||||||
overflow : hidden auto;
|
|
||||||
max-height : ~"calc(100vh - 28px)";
|
|
||||||
scrollbar-color : #666 #333;
|
|
||||||
scrollbar-width : thin;
|
|
||||||
|
|
||||||
|
|
||||||
#backgroundColorsHover;
|
#backgroundColorsHover;
|
||||||
.animate(background-color);
|
.animate(background-color);
|
||||||
position : relative;
|
position : relative;
|
||||||
display : block;
|
|
||||||
overflow : clip;
|
|
||||||
box-sizing : border-box;
|
box-sizing : border-box;
|
||||||
|
display : block;
|
||||||
|
max-width : 15em;
|
||||||
|
max-height : ~'calc(100vh - 28px)';
|
||||||
padding : 8px 5px 13px;
|
padding : 8px 5px 13px;
|
||||||
text-decoration : none;
|
overflow : hidden auto;
|
||||||
color : white;
|
color : white;
|
||||||
border-top : 1px solid #888;
|
text-decoration : none;
|
||||||
background-color : #333;
|
background-color : #333333;
|
||||||
|
border-top : 1px solid #888888;
|
||||||
|
scrollbar-color : #666666 #333333;
|
||||||
|
scrollbar-width : thin;
|
||||||
.clear {
|
.clear {
|
||||||
position : absolute;
|
position : absolute;
|
||||||
top : 50%;
|
top : 50%;
|
||||||
@@ -108,18 +288,16 @@
|
|||||||
display : none;
|
display : none;
|
||||||
width : 20px;
|
width : 20px;
|
||||||
height : 100%;
|
height : 100%;
|
||||||
transform : translateY(-50%);
|
background-color : #333333;
|
||||||
opacity : 70%;
|
|
||||||
border-radius : 3px;
|
border-radius : 3px;
|
||||||
background-color : #333;
|
opacity : 70%;
|
||||||
&:hover {
|
transform : translateY(-50%);
|
||||||
opacity : 100%;
|
&:hover { opacity : 100%; }
|
||||||
}
|
|
||||||
i {
|
i {
|
||||||
font-size : 10px;
|
|
||||||
width : 100%;
|
width : 100%;
|
||||||
height : 100%;
|
height : 100%;
|
||||||
margin : 0;
|
margin : 0;
|
||||||
|
font-size : 10px;
|
||||||
text-align : center;
|
text-align : center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -132,141 +310,42 @@
|
|||||||
}
|
}
|
||||||
.title {
|
.title {
|
||||||
display : inline-block;
|
display : inline-block;
|
||||||
overflow : hidden;
|
|
||||||
width : 100%;
|
width : 100%;
|
||||||
white-space : nowrap;
|
overflow : hidden auto;
|
||||||
text-overflow : ellipsis;
|
text-overflow : ellipsis;
|
||||||
|
white-space : nowrap;
|
||||||
}
|
}
|
||||||
.time {
|
.time {
|
||||||
font-size : 0.7em;
|
|
||||||
position : absolute;
|
position : absolute;
|
||||||
right : 2px;
|
right : 2px;
|
||||||
bottom : 2px;
|
bottom : 2px;
|
||||||
color : #888;
|
font-size : 0.7em;
|
||||||
|
color : #888888;
|
||||||
}
|
}
|
||||||
&.header {
|
&.header {
|
||||||
display : block;
|
|
||||||
box-sizing : border-box;
|
box-sizing : border-box;
|
||||||
|
display : block;
|
||||||
padding : 5px 0;
|
padding : 5px 0;
|
||||||
|
color : #BBBBBB;
|
||||||
text-align : center;
|
text-align : center;
|
||||||
color : #BBB;
|
background-color : #333333;
|
||||||
border-top : 1px solid #888;
|
border-top : 1px solid #888888;
|
||||||
background-color : #333;
|
&:nth-of-type(1) { background-color : darken(@teal, 20%); }
|
||||||
&:nth-of-type(1) {
|
&:nth-of-type(2) { background-color : darken(@purple, 30%); }
|
||||||
background-color : darken(@teal, 20%);
|
|
||||||
}
|
|
||||||
&:nth-of-type(2) {
|
|
||||||
background-color : darken(@purple, 30%);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.metadata.navItem {
|
|
||||||
position : relative;
|
|
||||||
display : flex;
|
|
||||||
align-items : center;
|
|
||||||
height : 100%;
|
|
||||||
padding : 0;
|
|
||||||
flex-grow : 1;
|
|
||||||
i {
|
|
||||||
margin-right : 10px;
|
|
||||||
}
|
}
|
||||||
.window {
|
|
||||||
position : absolute;
|
// this should likely be refactored into .navDropdownContainer
|
||||||
z-index : -1;
|
.save-menu {
|
||||||
bottom : 0;
|
.dropdown { z-index : 1000; }
|
||||||
left : 50%;
|
.navItem i.fa-power-off {
|
||||||
display : flex;
|
color : red;
|
||||||
justify-content : flex-start;
|
|
||||||
width : 440px;
|
|
||||||
max-height : ~"calc(100vh - 28px)";
|
|
||||||
margin : 0 auto;
|
|
||||||
padding : 0 10px 5px;
|
|
||||||
transition : transform 0.4s, opacity 0.4s;
|
|
||||||
border : 3px solid #444;
|
|
||||||
border-top : unset;
|
|
||||||
border-radius : 0 0 5px 5px;
|
|
||||||
background-color : #333;
|
|
||||||
box-shadow : inset 0 7px 9px -7px #111;
|
|
||||||
flex-flow : row wrap;
|
|
||||||
align-content : baseline;
|
|
||||||
&.active {
|
&.active {
|
||||||
transform : translateX(-50%) translateY(100%);
|
color : rgb(0, 182, 52);
|
||||||
opacity : 1;
|
filter : drop-shadow(0 0 2px rgba(0, 182, 52, 0.765));
|
||||||
}
|
|
||||||
&.inactive {
|
|
||||||
transform : translateX(-50%) translateY(0%);
|
|
||||||
opacity : 0;
|
|
||||||
}
|
|
||||||
.row {
|
|
||||||
display : flex;
|
|
||||||
width : 100%;
|
|
||||||
flex-flow : row wrap;
|
|
||||||
h4 {
|
|
||||||
display : block;
|
|
||||||
box-sizing : border-box;
|
|
||||||
min-width : 76px;
|
|
||||||
padding : 5px 0;
|
|
||||||
text-align : center;
|
|
||||||
color : #BBB;
|
|
||||||
flex-basis : 20%;
|
|
||||||
flex-grow : 1;
|
|
||||||
}
|
|
||||||
p {
|
|
||||||
font-family : "Open Sans", sans-serif;
|
|
||||||
font-size : 10px;
|
|
||||||
font-weight : normal;
|
|
||||||
padding : 5px 0;
|
|
||||||
text-transform : initial;
|
|
||||||
flex-basis : 80%;
|
|
||||||
flex-grow : 1;
|
|
||||||
.tag {
|
|
||||||
display : inline-block;
|
|
||||||
margin : 2px 2px;
|
|
||||||
padding : 2px;
|
|
||||||
border : 2px solid grey;
|
|
||||||
border-radius : 5px;
|
|
||||||
background-color : #444;
|
|
||||||
}
|
|
||||||
a.userPageLink {
|
|
||||||
text-decoration : none;
|
|
||||||
color : white;
|
|
||||||
&:hover {
|
|
||||||
text-decoration : underline;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&:nth-of-type(even) {
|
|
||||||
background-color : #555;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.warning.navItem {
|
|
||||||
position : relative;
|
|
||||||
color : white;
|
|
||||||
background-color : @orange;
|
|
||||||
&:hover > .dropdown {
|
|
||||||
visibility : visible;
|
|
||||||
}
|
|
||||||
.dropdown {
|
|
||||||
position : absolute;
|
|
||||||
z-index : 10000;
|
|
||||||
top : 28px;
|
|
||||||
left : 0;
|
|
||||||
display : block;
|
|
||||||
visibility : hidden;
|
|
||||||
box-sizing : border-box;
|
|
||||||
width : 100%;
|
|
||||||
padding : 13px 5px;
|
|
||||||
text-align : center;
|
|
||||||
background-color : #333;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.account.navItem {
|
|
||||||
min-width : 100px;
|
|
||||||
}
|
|
||||||
.account.username.navItem {
|
|
||||||
text-transform : none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,12 +1,64 @@
|
|||||||
const React = require('react');
|
const React = require('react');
|
||||||
|
const _ = require('lodash');
|
||||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||||
|
const { splitTextStyleAndMetadata } = require('../../../shared/helpers.js'); // Importing the function from helpers.js
|
||||||
|
|
||||||
module.exports = function(props){
|
const BREWKEY = 'homebrewery-new';
|
||||||
return <Nav.item
|
const STYLEKEY = 'homebrewery-new-style';
|
||||||
|
const METAKEY = 'homebrewery-new-meta';
|
||||||
|
|
||||||
|
const NewBrew = () => {
|
||||||
|
const handleFileChange = (e) => {
|
||||||
|
const file = e.target.files[0];
|
||||||
|
if (file) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
const fileContent = e.target.result;
|
||||||
|
const newBrew = {
|
||||||
|
text: fileContent,
|
||||||
|
style: ''
|
||||||
|
};
|
||||||
|
if(fileContent.startsWith('```metadata')) {
|
||||||
|
splitTextStyleAndMetadata(newBrew); // Modify newBrew directly
|
||||||
|
localStorage.setItem(BREWKEY, newBrew.text);
|
||||||
|
localStorage.setItem(STYLEKEY, newBrew.style);
|
||||||
|
localStorage.setItem(METAKEY, JSON.stringify(_.pick(newBrew, ['title', 'description', 'tags', 'systems', 'renderer', 'theme', 'lang'])));
|
||||||
|
window.location.href = '/new';
|
||||||
|
} else {
|
||||||
|
alert('This file is invalid, please, enter a valid file');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.readAsText(file);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Nav.dropdown>
|
||||||
|
<Nav.item
|
||||||
|
className='new'
|
||||||
|
color='purple'
|
||||||
|
icon='fa-solid fa-plus-square'>
|
||||||
|
new
|
||||||
|
</Nav.item>
|
||||||
|
<Nav.item
|
||||||
|
className='fromBlank'
|
||||||
href='/new'
|
href='/new'
|
||||||
newTab={true}
|
newTab={true}
|
||||||
color='purple'
|
color='purple'
|
||||||
icon='fas fa-plus-square'>
|
icon='fa-solid fa-file'>
|
||||||
new
|
from blank
|
||||||
</Nav.item>;
|
</Nav.item>
|
||||||
|
|
||||||
|
<Nav.item
|
||||||
|
className='fromFile'
|
||||||
|
color='purple'
|
||||||
|
icon='fa-solid fa-upload'
|
||||||
|
onClick={() => { document.getElementById('uploadTxt').click(); }}>
|
||||||
|
<input id="uploadTxt" className='newFromLocal' type="file" onChange={handleFileChange} style={{ display: 'none' }} />
|
||||||
|
from file
|
||||||
|
</Nav.item>
|
||||||
|
</Nav.dropdown>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
module.exports = NewBrew;
|
||||||
|
|||||||
@@ -1,102 +1,82 @@
|
|||||||
const React = require('react');
|
const React = require('react');
|
||||||
const createClass = require('create-react-class');
|
|
||||||
const _ = require('lodash');
|
|
||||||
const cx = require('classnames');
|
|
||||||
const moment = require('moment');
|
const moment = require('moment');
|
||||||
|
|
||||||
const UIPage = require('../basePages/uiPage/uiPage.jsx');
|
const UIPage = require('../basePages/uiPage/uiPage.jsx');
|
||||||
|
|
||||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
|
||||||
const Navbar = require('../../navbar/navbar.jsx');
|
|
||||||
|
|
||||||
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
|
||||||
const Account = require('../../navbar/account.navitem.jsx');
|
|
||||||
const NewBrew = require('../../navbar/newbrew.navitem.jsx');
|
|
||||||
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
|
||||||
|
|
||||||
const NaturalCritIcon = require('naturalcrit/svg/naturalcrit.svg.jsx');
|
const NaturalCritIcon = require('naturalcrit/svg/naturalcrit.svg.jsx');
|
||||||
|
|
||||||
let SAVEKEY = '';
|
let SAVEKEY = '';
|
||||||
|
|
||||||
const AccountPage = createClass({
|
const AccountPage = (props)=>{
|
||||||
displayName : 'AccountPage',
|
// destructure props and set state for save location
|
||||||
getDefaultProps : function() {
|
const { accountDetails, brew } = props;
|
||||||
return {
|
const [saveLocation, setSaveLocation] = React.useState('');
|
||||||
brew : {},
|
|
||||||
uiItems : {}
|
// initialize save location from local storage based on user id
|
||||||
};
|
React.useEffect(()=>{
|
||||||
},
|
if(!saveLocation && accountDetails.username) {
|
||||||
getInitialState : function() {
|
SAVEKEY = `HOMEBREWERY-DEFAULT-SAVE-LOCATION-${accountDetails.username}`;
|
||||||
return {
|
// if no SAVEKEY in local storage, default save location to Google Drive if user has Google account.
|
||||||
uiItems : this.props.uiItems
|
|
||||||
};
|
|
||||||
},
|
|
||||||
componentDidMount : function(){
|
|
||||||
if(!this.state.saveLocation && this.props.uiItems.username) {
|
|
||||||
SAVEKEY = `HOMEBREWERY-DEFAULT-SAVE-LOCATION-${this.props.uiItems.username}`;
|
|
||||||
let saveLocation = window.localStorage.getItem(SAVEKEY);
|
let saveLocation = window.localStorage.getItem(SAVEKEY);
|
||||||
saveLocation = saveLocation ?? (this.state.uiItems.googleId ? 'GOOGLE-DRIVE' : 'HOMEBREWERY');
|
saveLocation = saveLocation ?? (accountDetails.googleId ? 'GOOGLE-DRIVE' : 'HOMEBREWERY');
|
||||||
this.makeActive(saveLocation);
|
setActiveSaveLocation(saveLocation);
|
||||||
}
|
}
|
||||||
},
|
}, []);
|
||||||
|
|
||||||
makeActive : function(newSelection){
|
const setActiveSaveLocation = (newSelection)=>{
|
||||||
if(this.state.saveLocation == newSelection) return;
|
if(saveLocation === newSelection) return;
|
||||||
window.localStorage.setItem(SAVEKEY, newSelection);
|
window.localStorage.setItem(SAVEKEY, newSelection);
|
||||||
this.setState({
|
setSaveLocation(newSelection);
|
||||||
saveLocation : newSelection
|
};
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
renderButton : function(name, key, shouldRender=true){
|
// todo: should this be a set of radio buttons (well styled) since it's either/or choice?
|
||||||
if(!shouldRender) return;
|
const renderSaveLocationButton = (name, key, shouldRender = true)=>{
|
||||||
return <button className={this.state.saveLocation==key ? 'active' : ''} onClick={()=>{this.makeActive(key);}}>{name}</button>;
|
if(!shouldRender) return null;
|
||||||
},
|
return (
|
||||||
|
<button className={saveLocation === key ? 'active' : ''} onClick={()=>{setActiveSaveLocation(key);}}>
|
||||||
|
{name}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
renderNavItems : function() {
|
// render the entirety of the account page content
|
||||||
return <Navbar>
|
const renderAccountPage = ()=>{
|
||||||
<Nav.section>
|
return (
|
||||||
<NewBrew />
|
<>
|
||||||
<HelpNavItem />
|
|
||||||
<RecentNavItem />
|
|
||||||
<Account />
|
|
||||||
</Nav.section>
|
|
||||||
</Navbar>;
|
|
||||||
},
|
|
||||||
|
|
||||||
renderUiItems : function() {
|
|
||||||
return <>
|
|
||||||
<div className='dataGroup'>
|
<div className='dataGroup'>
|
||||||
<h1>Account Information <i className='fas fa-user'></i></h1>
|
<h1>Account Information <i className='fas fa-user'></i></h1>
|
||||||
<p><strong>Username: </strong> {this.props.uiItems.username || 'No user currently logged in'}</p>
|
<p><strong>Username: </strong>{accountDetails.username || 'No user currently logged in'}</p>
|
||||||
<p><strong>Last Login: </strong> {moment(this.props.uiItems.issued).format('dddd, MMMM Do YYYY, h:mm:ss a ZZ') || '-'}</p>
|
<p><strong>Last Login: </strong>{moment(accountDetails.issued).format('dddd, MMMM Do YYYY, h:mm:ss a ZZ') || '-'}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className='dataGroup'>
|
<div className='dataGroup'>
|
||||||
<h3>Homebrewery Information <NaturalCritIcon /></h3>
|
<h3>Homebrewery Information <NaturalCritIcon /></h3>
|
||||||
<p><strong>Brews on Homebrewery: </strong> {this.props.uiItems.mongoCount}</p>
|
<p><strong>Brews on Homebrewery: </strong>{accountDetails.mongoCount}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className='dataGroup'>
|
<div className='dataGroup'>
|
||||||
<h3>Google Information <i className='fab fa-google-drive'></i></h3>
|
<h3>Google Information <i className='fab fa-google-drive'></i></h3>
|
||||||
<p><strong>Linked to Google: </strong> {this.props.uiItems.googleId ? 'YES' : 'NO'}</p>
|
<p><strong>Linked to Google: </strong>{accountDetails.googleId ? 'YES' : 'NO'}</p>
|
||||||
{this.props.uiItems.googleId &&
|
{accountDetails.googleId && (
|
||||||
<p>
|
<p>
|
||||||
<strong>Brews on Google Drive: </strong> {this.props.uiItems.googleCount ?? <>Unable to retrieve files - <a href='https://github.com/naturalcrit/homebrewery/discussions/1580'>follow these steps to renew your Google credentials.</a></>}
|
<strong>Brews on Google Drive: </strong>{accountDetails.googleCount ?? (
|
||||||
|
<>
|
||||||
|
Unable to retrieve files - <a href='https://github.com/naturalcrit/homebrewery/discussions/1580'>follow these steps to renew your Google credentials.</a>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className='dataGroup'>
|
<div className='dataGroup'>
|
||||||
<h4>Default Save Location</h4>
|
<h4>Default Save Location</h4>
|
||||||
{this.renderButton('Homebrewery', 'HOMEBREWERY')}
|
{renderSaveLocationButton('Homebrewery', 'HOMEBREWERY')}
|
||||||
{this.renderButton('Google Drive', 'GOOGLE-DRIVE', this.state.uiItems.googleId)}
|
{renderSaveLocationButton('Google Drive', 'GOOGLE-DRIVE', accountDetails.googleId)}
|
||||||
</div>
|
</div>
|
||||||
</>;
|
</>
|
||||||
},
|
);
|
||||||
|
};
|
||||||
|
|
||||||
render : function(){
|
// return the account page inside the base layout wrapper (with navbar etc).
|
||||||
return <UIPage brew={this.props.brew}>
|
return (
|
||||||
{this.renderUiItems()}
|
<UIPage brew={brew}>
|
||||||
</UIPage>;
|
{renderAccountPage()}
|
||||||
}
|
</UIPage>);
|
||||||
});
|
};
|
||||||
|
|
||||||
module.exports = AccountPage;
|
module.exports = AccountPage;
|
||||||
|
|||||||
@@ -1,201 +0,0 @@
|
|||||||
require('./archivePage.less');
|
|
||||||
|
|
||||||
const React = require('react');
|
|
||||||
const createClass = require('create-react-class');
|
|
||||||
const _ = require('lodash');
|
|
||||||
const cx = require('classnames');
|
|
||||||
|
|
||||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
|
||||||
const Navbar = require('../../navbar/navbar.jsx');
|
|
||||||
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
|
||||||
const Account = require('../../navbar/account.navitem.jsx');
|
|
||||||
const NewBrew = require('../../navbar/newbrew.navitem.jsx');
|
|
||||||
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
|
||||||
const BrewItem = require('../basePages/listPage/brewItem/brewItem.jsx');
|
|
||||||
|
|
||||||
const request = require('../../utils/request-middleware.js');
|
|
||||||
|
|
||||||
const ArchivePage = createClass({
|
|
||||||
displayName : 'ArchivePage',
|
|
||||||
getDefaultProps : function () {
|
|
||||||
return {};
|
|
||||||
},
|
|
||||||
getInitialState : function () {
|
|
||||||
return {
|
|
||||||
title : this.props.query.title || '',
|
|
||||||
brewCollection : null,
|
|
||||||
page : 1,
|
|
||||||
totalPages : 1,
|
|
||||||
searching : false,
|
|
||||||
error : null,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
componentDidMount : function() {
|
|
||||||
|
|
||||||
},
|
|
||||||
handleChange(e) {
|
|
||||||
this.setState({ title: e.target.value });
|
|
||||||
},
|
|
||||||
|
|
||||||
updateStateWithBrews : function (brews, page, totalPages) {
|
|
||||||
this.setState({
|
|
||||||
brewCollection : brews || null,
|
|
||||||
page : page || 1,
|
|
||||||
totalPages : totalPages || 1,
|
|
||||||
searching : false
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
loadPage : async function(page) {
|
|
||||||
if(this.state.title == '') {} else {
|
|
||||||
|
|
||||||
try {
|
|
||||||
//this.updateUrl();
|
|
||||||
this.setState({ searching: true, error: null });
|
|
||||||
const title = encodeURIComponent(this.state.title);
|
|
||||||
await request.get(`/api/archive?title=${title}&page=${page}`)
|
|
||||||
.then((response)=>{
|
|
||||||
if(response.ok) {
|
|
||||||
this.updateStateWithBrews(response.body.brews, page, response.body.totalPages);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.log(`LoadPage error: ${error}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
updateUrl : function() {
|
|
||||||
const url = new URL(window.location.href);
|
|
||||||
const urlParams = new URLSearchParams(url.search);
|
|
||||||
|
|
||||||
// Set the title and page parameters
|
|
||||||
urlParams.set('title', this.state.title);
|
|
||||||
urlParams.set('page', this.state.page);
|
|
||||||
|
|
||||||
url.search = urlParams.toString(); // Convert URLSearchParams to string
|
|
||||||
window.history.replaceState(null, null, url);
|
|
||||||
},
|
|
||||||
|
|
||||||
renderFoundBrews() {
|
|
||||||
const { title, brewCollection, page, totalPages, error } = this.state;
|
|
||||||
|
|
||||||
if(title === '') {return (<div className='foundBrews noBrews'><h3>Whenever you want, just start typing...</h3></div>);}
|
|
||||||
|
|
||||||
if(error !== null) {
|
|
||||||
return (
|
|
||||||
<div className='foundBrews noBrews'>
|
|
||||||
<div><h3>I'm sorry, your request didn't work</h3>
|
|
||||||
<br /><p>Your search is not specific enough. Too many brews meet this criteria for us to display them.</p>
|
|
||||||
</div></div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!brewCollection || brewCollection.length === 0) {
|
|
||||||
return (
|
|
||||||
<div className='foundBrews noBrews'>
|
|
||||||
<h3>We haven't found brews meeting your request.</h3>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='foundBrews'>
|
|
||||||
<span className='brewCount'>{`Brews Found: ${brewCollection.length}`}</span>
|
|
||||||
|
|
||||||
{brewCollection.map((brew, index)=>(
|
|
||||||
<BrewItem brew={brew} key={index} reportError={this.props.reportError} />
|
|
||||||
))}
|
|
||||||
<div className='paginationControls'>
|
|
||||||
{page > 1 && (
|
|
||||||
<button onClick={()=>this.loadPage(page - 1)}>Previous Page</button>
|
|
||||||
)}
|
|
||||||
<span className='currentPage'>Page {page}</span>
|
|
||||||
{page < totalPages && (
|
|
||||||
<button onClick={()=>this.loadPage(page + 1)}>Next Page</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
|
|
||||||
renderForm : function () {
|
|
||||||
return (
|
|
||||||
<div className='brewLookup'>
|
|
||||||
<h2>Brew Lookup</h2>
|
|
||||||
<label>Title of the brew</label>
|
|
||||||
<input
|
|
||||||
type='text'
|
|
||||||
value={this.state.title}
|
|
||||||
onChange={this.handleChange}
|
|
||||||
onKeyDown={(e)=>{
|
|
||||||
if(e.key === 'Enter') {
|
|
||||||
this.handleChange(e);
|
|
||||||
this.loadPage(1);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
placeholder='v3 Reference Document'
|
|
||||||
/>
|
|
||||||
{/* In the future, we should be able to filter the results by adding tags.
|
|
||||||
<label>Tags</label><input type='text' value={this.state.query} placeholder='add a tag to filter'/>
|
|
||||||
<input type="checkbox" id="v3" /><label>v3 only</label>
|
|
||||||
*/}
|
|
||||||
|
|
||||||
<button onClick={()=>{ this.handleChange({ target: { value: this.state.title } }); this.loadPage(1); }}>
|
|
||||||
<i
|
|
||||||
className={cx('fas', {
|
|
||||||
'fa-search' : !this.state.searching,
|
|
||||||
'fa-spin fa-spinner' : this.state.searching,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
renderNavItems : function () {
|
|
||||||
return (
|
|
||||||
<Navbar>
|
|
||||||
<Nav.section>
|
|
||||||
<Nav.item className='brewTitle'>Archive: Search for brews</Nav.item>
|
|
||||||
</Nav.section>
|
|
||||||
<Nav.section>
|
|
||||||
<NewBrew />
|
|
||||||
<HelpNavItem />
|
|
||||||
<RecentNavItem />
|
|
||||||
<Account />
|
|
||||||
</Nav.section>
|
|
||||||
</Navbar>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
render : function () {
|
|
||||||
return (
|
|
||||||
<div className='archivePage'>
|
|
||||||
<link href='/themes/V3/Blank/style.css' rel='stylesheet'/>
|
|
||||||
<link href='/themes/V3/5ePHB/style.css' rel='stylesheet'/>
|
|
||||||
{this.renderNavItems()}
|
|
||||||
|
|
||||||
<div className='content'>
|
|
||||||
<div className='welcome'>
|
|
||||||
<h1>Welcome to the Archive</h1>
|
|
||||||
</div>
|
|
||||||
<div className='flexGroup'>
|
|
||||||
<div className='form dataGroup'>{this.renderForm()}</div>
|
|
||||||
<div className='resultsContainer dataGroup'>
|
|
||||||
<div className='title'>
|
|
||||||
<h2>Your results, my lordship</h2>
|
|
||||||
</div>
|
|
||||||
{this.renderFoundBrews()}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = ArchivePage;
|
|
||||||
@@ -1,173 +0,0 @@
|
|||||||
body {
|
|
||||||
height: 100vh;
|
|
||||||
|
|
||||||
.content {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.archivePage {
|
|
||||||
overflow-y: hidden;
|
|
||||||
height: 100%;
|
|
||||||
background-color: #2C3E50;
|
|
||||||
|
|
||||||
h1,h2,h3 {
|
|
||||||
font-family: 'Open Sans';
|
|
||||||
color: white;
|
|
||||||
font-weight: 900;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
display: grid;
|
|
||||||
grid-template-rows: 20vh 1fr;
|
|
||||||
|
|
||||||
.welcome {
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
background: url('https://i.imgur.com/MJ4YHu7.jpg');
|
|
||||||
background-size: 100%;
|
|
||||||
background-position: center;
|
|
||||||
height: 20vh;
|
|
||||||
border-bottom: 5px solid #333;
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 40px;
|
|
||||||
filter:drop-shadow(0 0 5px black);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.flexGroup {
|
|
||||||
height: 100%;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 500px 2fr;
|
|
||||||
background: #2C3E50;
|
|
||||||
|
|
||||||
.dataGroup {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background: white;
|
|
||||||
|
|
||||||
&.form .brewLookup {
|
|
||||||
padding: 50px;
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
font-size: 30px;
|
|
||||||
border-bottom: 2px solid;
|
|
||||||
margin-block: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
label {
|
|
||||||
margin-right: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
input+button {
|
|
||||||
margin-left: 20px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.resultsContainer {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
border-left: 2px solid;
|
|
||||||
height: 100%;
|
|
||||||
font-family: "BookInsanityRemake";
|
|
||||||
font-size: .34cm;
|
|
||||||
|
|
||||||
.title {
|
|
||||||
height: 10vh;
|
|
||||||
background-color: #333;
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
font-size: 30px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.foundBrews {
|
|
||||||
position: relative;
|
|
||||||
background-color: #2C3E50;
|
|
||||||
width: 100%;
|
|
||||||
max-height: 100%;
|
|
||||||
height: 66.7vh;
|
|
||||||
padding: 50px;
|
|
||||||
overflow-y:scroll;
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
font-size: 25px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.noBrews {
|
|
||||||
display:grid;
|
|
||||||
place-items:center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.brewCount {
|
|
||||||
position: fixed;
|
|
||||||
bottom: 0;
|
|
||||||
right: 17px;
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 800;
|
|
||||||
color: white;
|
|
||||||
background-color: #333;
|
|
||||||
padding: 8px 10px;
|
|
||||||
z-index: 1000;
|
|
||||||
font-family: 'Open Sans';
|
|
||||||
|
|
||||||
&:empty {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.limit {
|
|
||||||
position: fixed;
|
|
||||||
bottom: 0;
|
|
||||||
left: 502px;
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 800;
|
|
||||||
color: white;
|
|
||||||
background-color: #333;
|
|
||||||
padding: 8px 10px;
|
|
||||||
z-index: 1000;
|
|
||||||
font-family: 'Open Sans';
|
|
||||||
|
|
||||||
&:empty {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.brewItem {
|
|
||||||
background-image: url('/assets/parchmentBackground.jpg');
|
|
||||||
width: 48%;
|
|
||||||
margin-right: 40px;
|
|
||||||
color: black;
|
|
||||||
|
|
||||||
&:nth-child(even) {
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
h2 {
|
|
||||||
font-size: 0.75cm;
|
|
||||||
line-height: 0.988em;
|
|
||||||
font-family: "MrEavesRemake";
|
|
||||||
font-weight: 800;
|
|
||||||
color: var(--HB_Color_HeaderText);
|
|
||||||
}
|
|
||||||
|
|
||||||
.info {
|
|
||||||
font-family: ScalySansRemake;
|
|
||||||
font-size: 1.2em;
|
|
||||||
|
|
||||||
>span {
|
|
||||||
margin-right: 12px;
|
|
||||||
line-height: 1.5em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
hr {
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -20,6 +20,7 @@ const BrewItem = createClass({
|
|||||||
authors : [],
|
authors : [],
|
||||||
stubbed : true
|
stubbed : true
|
||||||
},
|
},
|
||||||
|
updateListFilter : ()=>{},
|
||||||
reportError : ()=>{}
|
reportError : ()=>{}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -44,6 +45,10 @@ const BrewItem = createClass({
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
updateFilter : function(type, term){
|
||||||
|
this.props.updateListFilter(type, term);
|
||||||
|
},
|
||||||
|
|
||||||
renderDeleteBrewLink : function(){
|
renderDeleteBrewLink : function(){
|
||||||
if(!this.props.brew.editId) return;
|
if(!this.props.brew.editId) return;
|
||||||
|
|
||||||
@@ -109,10 +114,11 @@ const BrewItem = createClass({
|
|||||||
const brew = this.props.brew;
|
const brew = this.props.brew;
|
||||||
if(Array.isArray(brew.tags)) { // temporary fix until dud tags are cleaned
|
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 = brew.tags?.filter((tag)=>tag); //remove tags that are empty strings
|
||||||
|
brew.tags.sort((a, b)=>{
|
||||||
|
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';
|
||||||
const authors = brew.authors.length > 0 ? brew.authors : 'No authors';
|
|
||||||
|
|
||||||
|
|
||||||
return <div className='brewItem'>
|
return <div className='brewItem'>
|
||||||
{brew.thumbnail &&
|
{brew.thumbnail &&
|
||||||
@@ -131,24 +137,17 @@ const BrewItem = createClass({
|
|||||||
<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]}>{matches[2]}</span>;
|
return <span key={idx} className={matches[1]} onClick={()=>{this.updateFilter(tag);}}>{matches[2]}</span>;
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</> : <></>
|
</> : <></>
|
||||||
}
|
}
|
||||||
<span title={`Authors:\n${brew.authors?.join('\n')}`}>
|
<span title={`Authors:\n${brew.authors?.join('\n')}`}>
|
||||||
<i className='fas fa-user'/> {Array.isArray(authors) ? (
|
<i className='fas fa-user'/> {brew.authors?.map((author, index)=>(
|
||||||
<span>
|
<>
|
||||||
{authors.map((author, index) => (
|
<a key={index} href={`/user/${author}`}>{author}</a>
|
||||||
<span key={index}>
|
{index < brew.authors.length - 1 && ', '}
|
||||||
<a href={`/share/${author}`}>{author}</a>
|
</>))}
|
||||||
{index < authors.length - 1 && ', '}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<span>{authors}</span>
|
|
||||||
)}
|
|
||||||
</span>
|
</span>
|
||||||
<br />
|
<br />
|
||||||
<span title={`Last viewed: ${moment(brew.lastViewed).local().format(dateFormatString)}`}>
|
<span title={`Last viewed: ${moment(brew.lastViewed).local().format(dateFormatString)}`}>
|
||||||
|
|||||||
@@ -63,6 +63,41 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
border-color: currentColor;
|
||||||
|
cursor : pointer;
|
||||||
|
&:before {
|
||||||
|
font-family: 'Font Awesome 5 Free';
|
||||||
|
font-size: 12px;
|
||||||
|
margin-right: 3px;
|
||||||
|
}
|
||||||
|
&.type {
|
||||||
|
background-color: #0080003b;
|
||||||
|
color: #008000;
|
||||||
|
&:before{
|
||||||
|
content: '\f0ad';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.group {
|
||||||
|
background-color: #5050503b;
|
||||||
|
color: #000000;
|
||||||
|
&:before{
|
||||||
|
content: '\f500';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.meta {
|
||||||
|
background-color: #0000803b;
|
||||||
|
color: #000080;
|
||||||
|
&:before{
|
||||||
|
content: '\f05a';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.system {
|
||||||
|
background-color: #8000003b;
|
||||||
|
color: #800000;
|
||||||
|
&:before{
|
||||||
|
content: '\f518';
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
&:hover{
|
&:hover{
|
||||||
.links{
|
.links{
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ const ListPage = createClass({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
filterString : this.props.query?.filter || '',
|
filterString : this.props.query?.filter || '',
|
||||||
|
filterTags : [],
|
||||||
sortType : this.props.query?.sort || null,
|
sortType : this.props.query?.sort || null,
|
||||||
sortDir : this.props.query?.dir || null,
|
sortDir : this.props.query?.dir || null,
|
||||||
query : this.props.query,
|
query : this.props.query,
|
||||||
@@ -82,7 +83,7 @@ const ListPage = createClass({
|
|||||||
if(!brews || !brews.length) return <div className='noBrews'>No Brews.</div>;
|
if(!brews || !brews.length) return <div className='noBrews'>No Brews.</div>;
|
||||||
|
|
||||||
return _.map(brews, (brew, idx)=>{
|
return _.map(brews, (brew, idx)=>{
|
||||||
return <BrewItem brew={brew} key={idx} reportError={this.props.reportError}/>;
|
return <BrewItem brew={brew} key={idx} reportError={this.props.reportError} updateListFilter={ (tag)=>{ this.updateUrl(this.state.filterString, this.state.sortType, this.state.sortDir, tag); }}/>;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -136,13 +137,33 @@ const ListPage = createClass({
|
|||||||
return;
|
return;
|
||||||
},
|
},
|
||||||
|
|
||||||
updateUrl : function(filterTerm, sortType, sortDir){
|
updateUrl : function(filterTerm, sortType, sortDir, filterTag=''){
|
||||||
const url = new URL(window.location.href);
|
const url = new URL(window.location.href);
|
||||||
const urlParams = new URLSearchParams(url.search);
|
const urlParams = new URLSearchParams(url.search);
|
||||||
|
|
||||||
urlParams.set('sort', sortType);
|
urlParams.set('sort', sortType);
|
||||||
urlParams.set('dir', sortDir);
|
urlParams.set('dir', sortDir);
|
||||||
|
|
||||||
|
let filterTags = urlParams.getAll('tag');
|
||||||
|
if(filterTag != '') {
|
||||||
|
if(filterTags.findIndex((tag)=>{return tag.toLowerCase()==filterTag.toLowerCase();}) == -1){
|
||||||
|
filterTags.push(filterTag);
|
||||||
|
} else {
|
||||||
|
filterTags = filterTags.filter((tag)=>{ return tag.toLowerCase() != filterTag.toLowerCase(); });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
urlParams.delete('tag');
|
||||||
|
// Add tags to URL in the order they were clicked
|
||||||
|
filterTags.forEach((tag)=>{ urlParams.append('tag', tag); });
|
||||||
|
// Sort tags before updating state
|
||||||
|
filterTags.sort((a, b)=>{
|
||||||
|
return a.indexOf(':') - b.indexOf(':') != 0 ? a.indexOf(':') - b.indexOf(':') : a.toLowerCase().localeCompare(b.toLowerCase());
|
||||||
|
});
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
filterTags
|
||||||
|
});
|
||||||
|
|
||||||
if(!filterTerm)
|
if(!filterTerm)
|
||||||
urlParams.delete('filter');
|
urlParams.delete('filter');
|
||||||
else
|
else
|
||||||
@@ -166,6 +187,16 @@ const ListPage = createClass({
|
|||||||
</div>;
|
</div>;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
renderTagsOptions : function(){
|
||||||
|
if(this.state.filterTags?.length == 0) return;
|
||||||
|
return <div className='tags-container'>
|
||||||
|
{_.map(this.state.filterTags, (tag, idx)=>{
|
||||||
|
const matches = tag.match(/^(?:([^:]+):)?([^:]+)$/);
|
||||||
|
return <span key={idx} className={matches[1]} onClick={()=>{ this.updateUrl(this.state.filterString, this.state.sortType, this.state.sortDir, tag); }}>{matches[2]}</span>;
|
||||||
|
})}
|
||||||
|
</div>;
|
||||||
|
},
|
||||||
|
|
||||||
renderSortOptions : function(){
|
renderSortOptions : function(){
|
||||||
return <div className='sort-container'>
|
return <div className='sort-container'>
|
||||||
<h6>Sort by :</h6>
|
<h6>Sort by :</h6>
|
||||||
@@ -176,9 +207,6 @@ const ListPage = createClass({
|
|||||||
{/* {this.renderSortOption('Latest', 'latest')} */}
|
{/* {this.renderSortOption('Latest', 'latest')} */}
|
||||||
|
|
||||||
{this.renderFilterOption()}
|
{this.renderFilterOption()}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</div>;
|
</div>;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -186,14 +214,28 @@ const ListPage = createClass({
|
|||||||
const testString = _.deburr(this.state.filterString).toLowerCase();
|
const testString = _.deburr(this.state.filterString).toLowerCase();
|
||||||
|
|
||||||
brews = _.filter(brews, (brew)=>{
|
brews = _.filter(brews, (brew)=>{
|
||||||
|
// Filter by user entered text
|
||||||
const brewStrings = _.deburr([
|
const brewStrings = _.deburr([
|
||||||
brew.title,
|
brew.title,
|
||||||
brew.description,
|
brew.description,
|
||||||
brew.tags].join('\n')
|
brew.tags].join('\n')
|
||||||
.toLowerCase());
|
.toLowerCase());
|
||||||
|
|
||||||
return brewStrings.includes(testString);
|
const filterTextTest = brewStrings.includes(testString);
|
||||||
|
|
||||||
|
// Filter by user selected tags
|
||||||
|
let filterTagTest = true;
|
||||||
|
if(this.state.filterTags.length > 0){
|
||||||
|
filterTagTest = Array.isArray(brew.tags) && this.state.filterTags?.every((tag)=>{
|
||||||
|
return brew.tags.findIndex((brewTag)=>{
|
||||||
|
return brewTag.toLowerCase() == tag.toLowerCase();
|
||||||
|
}) >= 0;
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return filterTextTest && filterTagTest;
|
||||||
|
});
|
||||||
|
|
||||||
return _.orderBy(brews, (brew)=>{ return this.sortBrewOrder(brew); }, this.state.sortDir);
|
return _.orderBy(brews, (brew)=>{ return this.sortBrewOrder(brew); }, this.state.sortDir);
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -220,10 +262,11 @@ const ListPage = createClass({
|
|||||||
render : function(){
|
render : function(){
|
||||||
return <div className='listPage sitePage'>
|
return <div className='listPage sitePage'>
|
||||||
{/*<style>@layer V3_5ePHB, bundle;</style>*/}
|
{/*<style>@layer V3_5ePHB, bundle;</style>*/}
|
||||||
<link href='/themes/V3/Blank/style.css' rel='stylesheet'/>
|
<link href='/themes/V3/Blank/style.css' type="text/css" rel='stylesheet'/>
|
||||||
<link href='/themes/V3/5ePHB/style.css' rel='stylesheet'/>
|
<link href='/themes/V3/5ePHB/style.css' type="text/css" rel='stylesheet'/>
|
||||||
{this.props.navItems}
|
{this.props.navItems}
|
||||||
{this.renderSortOptions()}
|
{this.renderSortOptions()}
|
||||||
|
{this.renderTagsOptions()}
|
||||||
|
|
||||||
<div className='content V3'>
|
<div className='content V3'>
|
||||||
<div className='page'>
|
<div className='page'>
|
||||||
|
|||||||
@@ -125,4 +125,66 @@
|
|||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
.tags-container {
|
||||||
|
height : 30px;
|
||||||
|
background-color : #555;
|
||||||
|
border-top : 1px solid #666;
|
||||||
|
border-bottom : 1px solid #666;
|
||||||
|
color : white;
|
||||||
|
display : flex;
|
||||||
|
justify-content : center;
|
||||||
|
align-items : center;
|
||||||
|
column-gap : 15px;
|
||||||
|
row-gap : 5px;
|
||||||
|
flex-wrap : wrap;
|
||||||
|
span {
|
||||||
|
font-family : 'Open Sans', sans-serif;
|
||||||
|
font-size : 11px;
|
||||||
|
font-weight : bold;
|
||||||
|
border : 1px solid;
|
||||||
|
border-radius : 3px;
|
||||||
|
padding : 3px;
|
||||||
|
cursor : pointer;
|
||||||
|
color: #dfdfdf;
|
||||||
|
&:before {
|
||||||
|
font-family: 'Font Awesome 5 Free';
|
||||||
|
font-size: 12px;
|
||||||
|
margin-right: 3px;
|
||||||
|
}
|
||||||
|
&:after {
|
||||||
|
content: '\f00d';
|
||||||
|
font-family: 'Font Awesome 5 Free';
|
||||||
|
font-size: 12px;
|
||||||
|
margin-left: 3px;
|
||||||
|
}
|
||||||
|
&.type {
|
||||||
|
background-color: #008000;
|
||||||
|
border-color: #00a000;
|
||||||
|
&:before{
|
||||||
|
content: '\f0ad';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.group {
|
||||||
|
background-color: #505050;
|
||||||
|
border-color: #000000;
|
||||||
|
&:before{
|
||||||
|
content: '\f500';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.meta {
|
||||||
|
background-color: #000080;
|
||||||
|
border-color: #0000a0;
|
||||||
|
&:before{
|
||||||
|
content: '\f05a';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.system {
|
||||||
|
background-color: #800000;
|
||||||
|
border-color: #a00000;
|
||||||
|
&:before{
|
||||||
|
content: '\f518';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ const EditPage = createClass({
|
|||||||
brew : { ...prevState.brew, text: text },
|
brew : { ...prevState.brew, text: text },
|
||||||
isPending : true,
|
isPending : true,
|
||||||
htmlErrors : htmlErrors,
|
htmlErrors : htmlErrors,
|
||||||
currentEditorPage : this.refs.editor.getCurrentPage()
|
currentEditorPage : this.refs.editor.getCurrentPage() - 1 //Offset index since Marked starts pages at 0
|
||||||
}), ()=>{if(this.state.autoSave) this.trySave();});
|
}), ()=>{if(this.state.autoSave) this.trySave();});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -73,9 +73,11 @@ const errorIndex = (props)=>{
|
|||||||
**Properties** tab, and adding your username to the "invited authors" list. You can
|
**Properties** tab, and adding your username to the "invited authors" list. You can
|
||||||
then try to access this document again.
|
then try to access this document again.
|
||||||
|
|
||||||
|
:
|
||||||
|
|
||||||
**Brew Title:** ${props.brew.brewTitle || 'Unable to show title'}
|
**Brew Title:** ${props.brew.brewTitle || 'Unable to show title'}
|
||||||
|
|
||||||
**Current Authors:** ${props.brew.authors?.map((author)=>{return `${author}`;}).join(', ') || 'Unable to list authors'}
|
**Current Authors:** ${props.brew.authors?.map((author)=>{return `[${author}](/user/${author})`;}).join(', ') || 'Unable to list authors'}
|
||||||
|
|
||||||
[Click here to be redirected to the brew's share page.](/share/${props.brew.shareId})`,
|
[Click here to be redirected to the brew's share page.](/share/${props.brew.shareId})`,
|
||||||
|
|
||||||
@@ -86,9 +88,14 @@ const errorIndex = (props)=>{
|
|||||||
You must be logged in to one of the accounts listed as an author of this brew.
|
You must be logged in to one of the accounts listed as an author of this brew.
|
||||||
User is not logged in. Please log in [here](${loginUrl}).
|
User is not logged in. Please log in [here](${loginUrl}).
|
||||||
|
|
||||||
|
:
|
||||||
|
|
||||||
**Brew Title:** ${props.brew.brewTitle || 'Unable to show title'}
|
**Brew Title:** ${props.brew.brewTitle || 'Unable to show title'}
|
||||||
|
|
||||||
**Current Authors:** ${props.brew.authors?.map((author)=>{return `${author}`;}).join(', ') || 'Unable to list authors'}`,
|
**Current Authors:** ${props.brew.authors?.map((author)=>{return `[${author}](/user/${author})`;}).join(', ') || 'Unable to list authors'}
|
||||||
|
|
||||||
|
[Click here to be redirected to the brew's share page.](/share/${props.brew.shareId})`,
|
||||||
|
|
||||||
|
|
||||||
// Brew load error
|
// Brew load error
|
||||||
'05' : dedent`
|
'05' : dedent`
|
||||||
@@ -97,6 +104,8 @@ const errorIndex = (props)=>{
|
|||||||
The server could not locate the Homebrewery document. It was likely deleted by
|
The server could not locate the Homebrewery document. It was likely deleted by
|
||||||
its owner.
|
its owner.
|
||||||
|
|
||||||
|
:
|
||||||
|
|
||||||
**Requested access:** ${props.brew.accessType}
|
**Requested access:** ${props.brew.accessType}
|
||||||
|
|
||||||
**Brew ID:** ${props.brew.brewId}`,
|
**Brew ID:** ${props.brew.brewId}`,
|
||||||
@@ -113,6 +122,8 @@ const errorIndex = (props)=>{
|
|||||||
|
|
||||||
An error occurred while attempting to remove the Homebrewery document.
|
An error occurred while attempting to remove the Homebrewery document.
|
||||||
|
|
||||||
|
:
|
||||||
|
|
||||||
**Brew ID:** ${props.brew.brewId}`,
|
**Brew ID:** ${props.brew.brewId}`,
|
||||||
|
|
||||||
// Author delete error
|
// Author delete error
|
||||||
@@ -121,7 +132,21 @@ const errorIndex = (props)=>{
|
|||||||
|
|
||||||
An error occurred while attempting to remove the user from the Homebrewery document author list!
|
An error occurred while attempting to remove the user from the Homebrewery document author list!
|
||||||
|
|
||||||
|
:
|
||||||
|
|
||||||
**Brew ID:** ${props.brew.brewId}`,
|
**Brew ID:** ${props.brew.brewId}`,
|
||||||
|
|
||||||
|
// Brew locked by Administrators error
|
||||||
|
'100' : dedent`
|
||||||
|
## This brew has been locked.
|
||||||
|
|
||||||
|
Please contact the Administrators to unlock this document.
|
||||||
|
|
||||||
|
:
|
||||||
|
|
||||||
|
**Brew ID:** ${props.brew.brewId}
|
||||||
|
|
||||||
|
**Brew Title:** ${props.brew.brewTitle}`,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,8 @@ const HomePage = createClass({
|
|||||||
return {
|
return {
|
||||||
brew : this.props.brew,
|
brew : this.props.brew,
|
||||||
welcomeText : this.props.brew.text,
|
welcomeText : this.props.brew.text,
|
||||||
error : undefined
|
error : undefined,
|
||||||
|
currentEditorPage : 0
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
handleSave : function(){
|
handleSave : function(){
|
||||||
@@ -53,7 +54,8 @@ const HomePage = createClass({
|
|||||||
},
|
},
|
||||||
handleTextChange : function(text){
|
handleTextChange : function(text){
|
||||||
this.setState((prevState)=>({
|
this.setState((prevState)=>({
|
||||||
brew : { ...prevState.brew, text: text }
|
brew : { ...prevState.brew, text: text },
|
||||||
|
currentEditorPage : this.refs.editor.getCurrentPage() - 1 //Offset index since Marked starts pages at 0
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
renderNavbar : function(){
|
renderNavbar : function(){
|
||||||
@@ -85,7 +87,12 @@ const HomePage = createClass({
|
|||||||
renderer={this.state.brew.renderer}
|
renderer={this.state.brew.renderer}
|
||||||
showEditButtons={false}
|
showEditButtons={false}
|
||||||
/>
|
/>
|
||||||
<BrewRenderer text={this.state.brew.text} style={this.state.brew.style} renderer={this.state.brew.renderer}/>
|
<BrewRenderer
|
||||||
|
text={this.state.brew.text}
|
||||||
|
style={this.state.brew.style}
|
||||||
|
renderer={this.state.brew.renderer}
|
||||||
|
currentEditorPage={this.state.currentEditorPage}
|
||||||
|
/>
|
||||||
</SplitPane>
|
</SplitPane>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -143,6 +143,7 @@ Much nicer than `<br><br><br><br><br>`
|
|||||||
### Column Breaks
|
### Column Breaks
|
||||||
Column and page breaks with `\column` and `\page`.
|
Column and page breaks with `\column` and `\page`.
|
||||||
|
|
||||||
|
\column
|
||||||
### Tables
|
### Tables
|
||||||
Tables now allow column & row spanning between cells. This is included in some updated snippets, but a simplified example is given below.
|
Tables now allow column & row spanning between cells. This is included in some updated snippets, but a simplified example is given below.
|
||||||
|
|
||||||
|
|||||||
@@ -42,7 +42,8 @@ const NewPage = createClass({
|
|||||||
isSaving : false,
|
isSaving : false,
|
||||||
saveGoogle : (global.account && global.account.googleId ? true : false),
|
saveGoogle : (global.account && global.account.googleId ? true : false),
|
||||||
error : null,
|
error : null,
|
||||||
htmlErrors : Markdown.validate(brew.text)
|
htmlErrors : Markdown.validate(brew.text),
|
||||||
|
currentEditorPage : 0
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -105,7 +106,8 @@ const NewPage = createClass({
|
|||||||
|
|
||||||
this.setState((prevState)=>({
|
this.setState((prevState)=>({
|
||||||
brew : { ...prevState.brew, text: text },
|
brew : { ...prevState.brew, text: text },
|
||||||
htmlErrors : htmlErrors
|
htmlErrors : htmlErrors,
|
||||||
|
currentEditorPage : this.refs.editor.getCurrentPage() - 1 //Offset index since Marked starts pages at 0
|
||||||
}));
|
}));
|
||||||
localStorage.setItem(BREWKEY, text);
|
localStorage.setItem(BREWKEY, text);
|
||||||
},
|
},
|
||||||
@@ -220,7 +222,15 @@ const NewPage = createClass({
|
|||||||
onMetaChange={this.handleMetaChange}
|
onMetaChange={this.handleMetaChange}
|
||||||
renderer={this.state.brew.renderer}
|
renderer={this.state.brew.renderer}
|
||||||
/>
|
/>
|
||||||
<BrewRenderer text={this.state.brew.text} style={this.state.brew.style} renderer={this.state.brew.renderer} theme={this.state.brew.theme} lang={this.state.brew.lang} errors={this.state.htmlErrors}/>
|
<BrewRenderer
|
||||||
|
text={this.state.brew.text}
|
||||||
|
style={this.state.brew.style}
|
||||||
|
renderer={this.state.brew.renderer}
|
||||||
|
theme={this.state.brew.theme}
|
||||||
|
errors={this.state.htmlErrors}
|
||||||
|
lang={this.state.brew.lang}
|
||||||
|
currentEditorPage={this.state.currentEditorPage}
|
||||||
|
/>
|
||||||
</SplitPane>
|
</SplitPane>
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ const PrintPage = createClass({
|
|||||||
brew : {
|
brew : {
|
||||||
text : '',
|
text : '',
|
||||||
style : '',
|
style : '',
|
||||||
renderer : 'legacy'
|
renderer : 'legacy',
|
||||||
|
lang : ''
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -32,7 +33,8 @@ const PrintPage = createClass({
|
|||||||
text : this.props.brew.text || '',
|
text : this.props.brew.text || '',
|
||||||
style : this.props.brew.style || undefined,
|
style : this.props.brew.style || undefined,
|
||||||
renderer : this.props.brew.renderer || 'legacy',
|
renderer : this.props.brew.renderer || 'legacy',
|
||||||
theme : this.props.brew.theme || '5ePHB'
|
theme : this.props.brew.theme || '5ePHB',
|
||||||
|
lang : this.props.brew.lang || 'en'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -49,7 +51,8 @@ const PrintPage = createClass({
|
|||||||
text : brewStorage,
|
text : brewStorage,
|
||||||
style : styleStorage,
|
style : styleStorage,
|
||||||
renderer : metaStorage?.renderer || 'legacy',
|
renderer : metaStorage?.renderer || 'legacy',
|
||||||
theme : metaStorage?.theme || '5ePHB'
|
theme : metaStorage?.theme || '5ePHB',
|
||||||
|
lang : metaStorage?.lang || 'en'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -93,14 +96,14 @@ const PrintPage = createClass({
|
|||||||
|
|
||||||
return <div>
|
return <div>
|
||||||
<Meta name='robots' content='noindex, nofollow' />
|
<Meta name='robots' content='noindex, nofollow' />
|
||||||
<link href={`/themes/${rendererPath}/Blank/style.css`} rel='stylesheet'/>
|
<link href={`/themes/${rendererPath}/Blank/style.css`} type="text/css" rel='stylesheet'/>
|
||||||
{baseThemePath &&
|
{baseThemePath &&
|
||||||
<link href={`/themes/${rendererPath}/${baseThemePath}/style.css`} rel='stylesheet'/>
|
<link href={`/themes/${rendererPath}/${baseThemePath}/style.css`} type="text/css" rel='stylesheet'/>
|
||||||
}
|
}
|
||||||
<link href={`/themes/${rendererPath}/${themePath}/style.css`} rel='stylesheet'/>
|
<link href={`/themes/${rendererPath}/${themePath}/style.css`} type="text/css" rel='stylesheet'/>
|
||||||
{/* Apply CSS from Style tab */}
|
{/* Apply CSS from Style tab */}
|
||||||
{this.renderStyle()}
|
{this.renderStyle()}
|
||||||
<div className='pages' ref='pages'>
|
<div className='pages' ref='pages' lang={this.state.brew.lang}>
|
||||||
{this.renderPages()}
|
{this.renderPages()}
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ const template = async function(name, title='', props = {}){
|
|||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, height=device-height, interactive-widget=resizes-visual" />
|
<meta name="viewport" content="width=device-width, initial-scale=1, height=device-height, interactive-widget=resizes-visual" />
|
||||||
<link href="//use.fontawesome.com/releases/v5.15.1/css/all.css" rel="stylesheet" />
|
<link href="//use.fontawesome.com/releases/v6.5.1/css/all.css" rel="stylesheet" type="text/css" />
|
||||||
<link href="//fonts.googleapis.com/css?family=Open+Sans:400,300,600,700" rel="stylesheet" type="text/css" />
|
<link href="//fonts.googleapis.com/css?family=Open+Sans:400,300,600,700" rel="stylesheet" type="text/css" />
|
||||||
<link href=${`/${name}/bundle.css`} rel='stylesheet' />
|
<link href=${`/${name}/bundle.css`} type="text/css" rel='stylesheet' />
|
||||||
<link rel="icon" href="/assets/favicon.ico" type="image/x-icon" />
|
<link rel="icon" href="/assets/favicon.ico" type="image/x-icon" />
|
||||||
${ogMetaTags}
|
${ogMetaTags}
|
||||||
<meta name="twitter:card" content="summary">
|
<meta name="twitter:card" content="summary">
|
||||||
|
|||||||
2043
package-lock.json
generated
2043
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
35
package.json
35
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.10.0",
|
"version": "3.12.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"npm": "^10.2.x",
|
"npm": "^10.2.x",
|
||||||
"node": "^20.8.x"
|
"node": "^20.8.x"
|
||||||
@@ -26,10 +26,12 @@
|
|||||||
"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",
|
||||||
|
"test:variables": "jest tests/markdown/variables.test.js --verbose",
|
||||||
"test:mustache-syntax": "jest '.*(mustache-syntax).*' --verbose --noStackTrace",
|
"test:mustache-syntax": "jest '.*(mustache-syntax).*' --verbose --noStackTrace",
|
||||||
"test:mustache-syntax:inline": "jest '.*(mustache-syntax).*' -t '^Inline:.*' --verbose --noStackTrace",
|
"test:mustache-syntax:inline": "jest '.*(mustache-syntax).*' -t '^Inline:.*' --verbose --noStackTrace",
|
||||||
"test:mustache-syntax:block": "jest '.*(mustache-syntax).*' -t '^Block:.*' --verbose --noStackTrace",
|
"test:mustache-syntax:block": "jest '.*(mustache-syntax).*' -t '^Block:.*' --verbose --noStackTrace",
|
||||||
"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:route": "jest tests/routes/static-pages.test.js --verbose",
|
"test:route": "jest tests/routes/static-pages.test.js --verbose",
|
||||||
"phb": "node scripts/phb.js",
|
"phb": "node scripts/phb.js",
|
||||||
"prod": "set NODE_ENV=production && npm run build",
|
"prod": "set NODE_ENV=production && npm run build",
|
||||||
@@ -79,18 +81,19 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/core": "^7.23.9",
|
"@babel/core": "^7.24.4",
|
||||||
"@babel/plugin-transform-runtime": "^7.23.9",
|
"@babel/plugin-transform-runtime": "^7.24.3",
|
||||||
"@babel/preset-env": "^7.23.9",
|
"@babel/preset-env": "^7.24.4",
|
||||||
"@babel/preset-react": "^7.23.3",
|
"@babel/preset-react": "^7.24.1",
|
||||||
"@googleapis/drive": "^8.6.0",
|
"@googleapis/drive": "^8.7.0",
|
||||||
"body-parser": "^1.20.2",
|
"body-parser": "^1.20.2",
|
||||||
"classnames": "^2.3.2",
|
"classnames": "^2.5.1",
|
||||||
"codemirror": "^5.65.6",
|
"codemirror": "^5.65.6",
|
||||||
"cookie-parser": "^1.4.6",
|
"cookie-parser": "^1.4.6",
|
||||||
"create-react-class": "^15.7.0",
|
"create-react-class": "^15.7.0",
|
||||||
"dedent-tabs": "^0.10.3",
|
"dedent-tabs": "^0.10.3",
|
||||||
"express": "^4.18.2",
|
"expr-eval": "^2.0.2",
|
||||||
|
"express": "^4.19.2",
|
||||||
"express-async-handler": "^1.2.0",
|
"express-async-handler": "^1.2.0",
|
||||||
"express-static-gzip": "2.1.7",
|
"express-static-gzip": "2.1.7",
|
||||||
"fs-extra": "11.2.0",
|
"fs-extra": "11.2.0",
|
||||||
@@ -98,32 +101,32 @@
|
|||||||
"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.1.1",
|
"marked": "11.2.0",
|
||||||
"marked-extended-tables": "^1.0.8",
|
"marked-extended-tables": "^1.0.8",
|
||||||
"marked-gfm-heading-id": "^3.1.2",
|
"marked-gfm-heading-id": "^3.1.3",
|
||||||
"marked-smartypants-lite": "^1.0.2",
|
"marked-smartypants-lite": "^1.0.2",
|
||||||
"markedLegacy": "npm:marked@^0.3.19",
|
"markedLegacy": "npm:marked@^0.3.19",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
"mongoose": "^8.1.1",
|
"mongoose": "^8.3.1",
|
||||||
"nanoid": "3.3.4",
|
"nanoid": "3.3.4",
|
||||||
"nconf": "^0.12.1",
|
"nconf": "^0.12.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-frame-component": "^4.1.3",
|
"react-frame-component": "^4.1.3",
|
||||||
"react-router-dom": "6.21.3",
|
"react-router-dom": "6.22.3",
|
||||||
"sanitize-filename": "1.6.3",
|
"sanitize-filename": "1.6.3",
|
||||||
"superagent": "^8.1.2",
|
"superagent": "^8.1.2",
|
||||||
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git"
|
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"eslint": "^8.56.0",
|
"eslint": "^8.57.0",
|
||||||
"eslint-plugin-jest": "^27.6.3",
|
"eslint-plugin-jest": "^28.2.0",
|
||||||
"eslint-plugin-react": "^7.33.2",
|
"eslint-plugin-react": "^7.34.1",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"jest-expect-message": "^1.1.3",
|
"jest-expect-message": "^1.1.3",
|
||||||
"postcss-less": "^6.0.0",
|
"postcss-less": "^6.0.0",
|
||||||
"stylelint": "^15.11.0",
|
"stylelint": "^15.11.0",
|
||||||
"stylelint-config-recess-order": "^4.4.0",
|
"stylelint-config-recess-order": "^4.6.0",
|
||||||
"stylelint-config-recommended": "^13.0.0",
|
"stylelint-config-recommended": "^13.0.0",
|
||||||
"stylelint-stylistic": "^0.4.3",
|
"stylelint-stylistic": "^0.4.3",
|
||||||
"supertest": "^6.3.4"
|
"supertest": "^6.3.4"
|
||||||
|
|||||||
10
server.js
10
server.js
@@ -7,6 +7,14 @@ DB.connect(config).then(()=>{
|
|||||||
// before launching server
|
// before launching server
|
||||||
const PORT = process.env.PORT || config.get('web_port') || 8000;
|
const PORT = process.env.PORT || config.get('web_port') || 8000;
|
||||||
server.app.listen(PORT, ()=>{
|
server.app.listen(PORT, ()=>{
|
||||||
console.log(`server on port: ${PORT}`);
|
const reset = '\x1b[0m'; // Reset to default style
|
||||||
|
const bright = '\x1b[1m'; // Bright (bold) style
|
||||||
|
const cyan = '\x1b[36m'; // Cyan color
|
||||||
|
const underline = '\x1b[4m'; // Underlined style
|
||||||
|
|
||||||
|
console.log(`\n\tserver started at: ${new Date().toLocaleString()}`);
|
||||||
|
console.log(`\tserver on port: ${PORT}`);
|
||||||
|
console.log(`\t${bright + cyan}Open in browser: ${reset}${underline + bright + cyan}http://localhost:${PORT}${reset}\n\n`)
|
||||||
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -17,21 +17,8 @@ const asyncHandler = require('express-async-handler');
|
|||||||
|
|
||||||
const { DEFAULT_BREW } = require('./brewDefaults.js');
|
const { DEFAULT_BREW } = require('./brewDefaults.js');
|
||||||
|
|
||||||
const splitTextStyleAndMetadata = (brew)=>{
|
const { splitTextStyleAndMetadata } = require('../shared/helpers.js');
|
||||||
brew.text = brew.text.replaceAll('\r\n', '\n');
|
|
||||||
if(brew.text.startsWith('```metadata')) {
|
|
||||||
const index = brew.text.indexOf('```\n\n');
|
|
||||||
const metadataSection = brew.text.slice(12, index - 1);
|
|
||||||
const metadata = yaml.load(metadataSection);
|
|
||||||
Object.assign(brew, _.pick(metadata, ['title', 'description', 'tags', 'systems', 'renderer', 'theme', 'lang']));
|
|
||||||
brew.text = brew.text.slice(index + 5);
|
|
||||||
}
|
|
||||||
if(brew.text.startsWith('```css')) {
|
|
||||||
const index = brew.text.indexOf('```\n\n');
|
|
||||||
brew.style = brew.text.slice(7, index - 1);
|
|
||||||
brew.text = brew.text.slice(index + 5);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const sanitizeBrew = (brew, accessType)=>{
|
const sanitizeBrew = (brew, accessType)=>{
|
||||||
brew._id = undefined;
|
brew._id = undefined;
|
||||||
@@ -67,7 +54,6 @@ app.use((req, res, next)=>{
|
|||||||
|
|
||||||
app.use(homebrewApi);
|
app.use(homebrewApi);
|
||||||
app.use(require('./admin.api.js'));
|
app.use(require('./admin.api.js'));
|
||||||
app.use(require('./archive.api.js'));
|
|
||||||
|
|
||||||
const HomebrewModel = require('./homebrew.model.js').model;
|
const HomebrewModel = require('./homebrew.model.js').model;
|
||||||
const welcomeText = require('fs').readFileSync('client/homebrew/pages/homePage/welcome_msg.md', 'utf8');
|
const welcomeText = require('fs').readFileSync('client/homebrew/pages/homePage/welcome_msg.md', 'utf8');
|
||||||
@@ -386,7 +372,7 @@ app.get('/account', asyncHandler(async (req, res, next)=>{
|
|||||||
console.log(err);
|
console.log(err);
|
||||||
});
|
});
|
||||||
|
|
||||||
data.uiItems = {
|
data.accountDetails = {
|
||||||
username : req.account.username,
|
username : req.account.username,
|
||||||
issued : req.account.issued,
|
issued : req.account.issued,
|
||||||
googleId : Boolean(req.account.googleId),
|
googleId : Boolean(req.account.googleId),
|
||||||
@@ -482,11 +468,6 @@ app.use(async (err, req, res, next)=>{
|
|||||||
res.status(err.status || err.response?.status || 500).send(err);
|
res.status(err.status || err.response?.status || 500).send(err);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if(err.originalUrl?.startsWith('/archive/')) {
|
|
||||||
// console.log('archive error');
|
|
||||||
res.status(err.status || err.response?.status || 500).send(err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// console.log('non-API error');
|
// console.log('non-API error');
|
||||||
const status = err.status || err.code || 500;
|
const status = err.status || err.code || 500;
|
||||||
@@ -510,8 +491,6 @@ app.use(async (err, req, res, next)=>{
|
|||||||
res.send(page);
|
res.send(page);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
app.use((req, res)=>{
|
app.use((req, res)=>{
|
||||||
if(!res.headersSent) {
|
if(!res.headersSent) {
|
||||||
console.error('Headers have not been sent, responding with a server error.', req.url);
|
console.error('Headers have not been sent, responding with a server error.', req.url);
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
const HomebrewModel = require('./homebrew.model.js').model;
|
|
||||||
const router = require('express').Router();
|
|
||||||
const asyncHandler = require('express-async-handler');
|
|
||||||
|
|
||||||
const archive = {
|
|
||||||
archiveApi : router,
|
|
||||||
/* Searches for matching title, also attempts to partial match */
|
|
||||||
findBrews : async (req, res, next)=>{
|
|
||||||
try {
|
|
||||||
const title = req.query.title || '';
|
|
||||||
const page = parseInt(req.query.page) || 1;
|
|
||||||
console.log('try:', page);
|
|
||||||
const pageSize = 10; // Set a default page size
|
|
||||||
const skip = (page - 1) * pageSize;
|
|
||||||
|
|
||||||
const titleQuery = {
|
|
||||||
title : { $regex: decodeURIComponent(title), $options: 'i' },
|
|
||||||
published : true
|
|
||||||
};
|
|
||||||
|
|
||||||
const projection = {
|
|
||||||
editId : 0,
|
|
||||||
googleId : 0,
|
|
||||||
text : 0,
|
|
||||||
textBin : 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
const brews = await HomebrewModel.find(titleQuery, projection)
|
|
||||||
.skip(skip)
|
|
||||||
.limit(pageSize)
|
|
||||||
.maxTimeMS(5000)
|
|
||||||
.exec();
|
|
||||||
|
|
||||||
if(!brews || brews.length === 0) {
|
|
||||||
// No published documents found with the given title
|
|
||||||
return res.status(404).json({ error: 'Published documents not found' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const totalDocuments = await HomebrewModel.countDocuments(title);
|
|
||||||
|
|
||||||
const totalPages = Math.ceil(totalDocuments / pageSize);
|
|
||||||
|
|
||||||
return res.json({ brews, page, totalPages });
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
return res.status(500).json({ error: 'Internal Server Error' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
router.get('/api/archive', asyncHandler(archive.findBrews));
|
|
||||||
|
|
||||||
module.exports = router;
|
|
||||||
@@ -7,7 +7,9 @@ const config = require('./config.js');
|
|||||||
|
|
||||||
let serviceAuth;
|
let serviceAuth;
|
||||||
if(!config.get('service_account')){
|
if(!config.get('service_account')){
|
||||||
console.log('No Google Service Account in config files - Google Drive integration will not be available.');
|
const reset = '\x1b[0m'; // Reset to default style
|
||||||
|
const yellow = '\x1b[33m'; // yellow color
|
||||||
|
console.warn(`\n${yellow}No Google Service Account in config files - Google Drive integration will not be available.${reset}`);
|
||||||
} else {
|
} else {
|
||||||
const keys = typeof(config.get('service_account')) == 'string' ?
|
const keys = typeof(config.get('service_account')) == 'string' ?
|
||||||
JSON.parse(config.get('service_account')) :
|
JSON.parse(config.get('service_account')) :
|
||||||
@@ -18,7 +20,7 @@ if(!config.get('service_account')){
|
|||||||
serviceAuth.scopes = ['https://www.googleapis.com/auth/drive'];
|
serviceAuth.scopes = ['https://www.googleapis.com/auth/drive'];
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn(err);
|
console.warn(err);
|
||||||
console.log('Please make sure the Google Service Account is set up properly in your config files.');
|
console.warn('Please make sure the Google Service Account is set up properly in your config files.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -54,6 +54,10 @@ const api = {
|
|||||||
});
|
});
|
||||||
stub = stub?.toObject();
|
stub = stub?.toObject();
|
||||||
|
|
||||||
|
if(stub?.lock?.locked && accessType != 'edit') {
|
||||||
|
throw { HBErrorCode: '100', code: stub.lock.code, message: stub.lock.message, brewId: stub.shareId, brewTitle: stub.title };
|
||||||
|
}
|
||||||
|
|
||||||
// If there is a google id, try to find the google brew
|
// If there is a google id, try to find the google brew
|
||||||
if(!stubOnly && (googleId || stub?.googleId)) {
|
if(!stubOnly && (googleId || stub?.googleId)) {
|
||||||
let googleError;
|
let googleError;
|
||||||
@@ -81,7 +85,7 @@ const api = {
|
|||||||
if(req.account){
|
if(req.account){
|
||||||
throw { ...accessError, message: 'User is not an Author', HBErrorCode: '03', authors: stub.authors, brewTitle: stub.title, shareId: stub.shareId};
|
throw { ...accessError, message: 'User is not an Author', HBErrorCode: '03', authors: stub.authors, brewTitle: stub.title, shareId: stub.shareId};
|
||||||
}
|
}
|
||||||
throw { ...accessError, message: 'User is not logged in', HBErrorCode: '04', authors: stub.authors, brewTitle: stub.title };
|
throw { ...accessError, message: 'User is not logged in', HBErrorCode: '04', authors: stub.authors, brewTitle: stub.title, shareId: stub.shareId};
|
||||||
}
|
}
|
||||||
|
|
||||||
// If after all of that we still don't have a brew, throw an exception
|
// If after all of that we still don't have a brew, throw an exception
|
||||||
|
|||||||
@@ -298,6 +298,18 @@ describe('Tests for api', ()=>{
|
|||||||
expect(model.get).toHaveBeenCalledWith({ shareId: '1' });
|
expect(model.get).toHaveBeenCalledWith({ shareId: '1' });
|
||||||
expect(google.getGoogleBrew).toHaveBeenCalledWith('2', '1', 'share');
|
expect(google.getGoogleBrew).toHaveBeenCalledWith('2', '1', 'share');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('access is denied to a locked brew', async()=>{
|
||||||
|
const lockBrew = { title: 'test brew', shareId: '1', lock: { locked: true, code: 404, message: 'brew locked' } };
|
||||||
|
model.get = jest.fn(()=>toBrewPromise(lockBrew));
|
||||||
|
api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
|
||||||
|
|
||||||
|
const fn = api.getBrew('share', false);
|
||||||
|
const req = { brew: {} };
|
||||||
|
const next = jest.fn();
|
||||||
|
|
||||||
|
await expect(fn(req, null, next)).rejects.toEqual({ 'HBErrorCode': '100', 'brewId': '1', 'brewTitle': 'test brew', 'code': 404, 'message': 'brew locked' });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('mergeBrewText', ()=>{
|
describe('mergeBrewText', ()=>{
|
||||||
|
|||||||
22
shared/helpers.js
Normal file
22
shared/helpers.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
const _ = require('lodash');
|
||||||
|
const yaml = require('js-yaml');
|
||||||
|
|
||||||
|
const splitTextStyleAndMetadata = (brew) => {
|
||||||
|
brew.text = brew.text.replaceAll('\r\n', '\n');
|
||||||
|
if (brew.text.startsWith('```metadata')) {
|
||||||
|
const index = brew.text.indexOf('```\n\n');
|
||||||
|
const metadataSection = brew.text.slice(12, index - 1);
|
||||||
|
const metadata = yaml.load(metadataSection);
|
||||||
|
Object.assign(brew, _.pick(metadata, ['title', 'description', 'tags', 'systems', 'renderer', 'theme', 'lang']));
|
||||||
|
brew.text = brew.text.slice(index + 5);
|
||||||
|
}
|
||||||
|
if (brew.text.startsWith('```css')) {
|
||||||
|
const index = brew.text.indexOf('```\n\n');
|
||||||
|
brew.style = brew.text.slice(7, index - 1);
|
||||||
|
brew.text = brew.text.slice(index + 5);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
splitTextStyleAndMetadata
|
||||||
|
};
|
||||||
@@ -436,7 +436,7 @@ const CodeEditor = createClass({
|
|||||||
|
|
||||||
render : function(){
|
render : function(){
|
||||||
return <>
|
return <>
|
||||||
<link href={`../homebrew/cm-themes/${this.props.editorTheme}.css`} rel='stylesheet' />
|
<link href={`../homebrew/cm-themes/${this.props.editorTheme}.css`} type="text/css" rel='stylesheet' />
|
||||||
<div className='codeEditor' ref='editor' style={this.props.style}/>
|
<div className='codeEditor' ref='editor' style={this.props.style}/>
|
||||||
</>;
|
</>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,16 @@
|
|||||||
animation-duration: 0.4s;
|
animation-duration: 0.4s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.CodeMirror-vscrollbar {
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 20px;
|
||||||
|
}
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
width: 20px;
|
||||||
|
background: linear-gradient(90deg, #858585 15px, #808080 15px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//.cm-tab {
|
//.cm-tab {
|
||||||
// background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAMCAQAAACOs/baAAAARUlEQVR4nGJgIAG8JkXxUAcCtDWemcGR1lY4MvgzCEKY7jSBjgxBDAG09UEQzAe0AMwMHrSOAwEGRtpaMIwAAAAA//8DAG4ID9EKs6YqAAAAAElFTkSuQmCC) no-repeat right;
|
// background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAMCAQAAACOs/baAAAARUlEQVR4nGJgIAG8JkXxUAcCtDWemcGR1lY4MvgzCEKY7jSBjgxBDAG09UEQzAe0AMwMHrSOAwEGRtpaMIwAAAAA//8DAG4ID9EKs6YqAAAAAElFTkSuQmCC) no-repeat right;
|
||||||
//}
|
//}
|
||||||
|
|||||||
@@ -4,7 +4,40 @@ const Marked = require('marked');
|
|||||||
const MarkedExtendedTables = require('marked-extended-tables');
|
const MarkedExtendedTables = require('marked-extended-tables');
|
||||||
const { markedSmartypantsLite: MarkedSmartypantsLite } = require('marked-smartypants-lite');
|
const { markedSmartypantsLite: MarkedSmartypantsLite } = require('marked-smartypants-lite');
|
||||||
const { gfmHeadingId: MarkedGFMHeadingId } = require('marked-gfm-heading-id');
|
const { gfmHeadingId: MarkedGFMHeadingId } = require('marked-gfm-heading-id');
|
||||||
|
const MathParser = require('expr-eval').Parser;
|
||||||
const renderer = new Marked.Renderer();
|
const renderer = new Marked.Renderer();
|
||||||
|
const tokenizer = new Marked.Tokenizer();
|
||||||
|
|
||||||
|
//Limit math features to simple items
|
||||||
|
const mathParser = new MathParser({
|
||||||
|
operators : {
|
||||||
|
// These default to true, but are included to be explicit
|
||||||
|
add : true,
|
||||||
|
subtract : true,
|
||||||
|
multiply : true,
|
||||||
|
divide : true,
|
||||||
|
power : true,
|
||||||
|
round : true,
|
||||||
|
floor : true,
|
||||||
|
ceil : true,
|
||||||
|
|
||||||
|
sin : false, cos : false, tan : false, asin : false, acos : false,
|
||||||
|
atan : false, sinh : false, cosh : false, tanh : false, asinh : false,
|
||||||
|
acosh : false, atanh : false, sqrt : false, cbrt : false, log : false,
|
||||||
|
log2 : false, ln : false, lg : false, log10 : false, expm1 : false,
|
||||||
|
log1p : false, abs : false, trunc : false, join : false, sum : false,
|
||||||
|
'-' : false, '+' : false, exp : false, not : false, length : false,
|
||||||
|
'!' : false, sign : false, random : false, fac : false, min : false,
|
||||||
|
max : false, hypot : false, pyt : false, pow : false, atan2 : false,
|
||||||
|
'if' : false, gamma : false, roundTo : false, map : false, fold : false,
|
||||||
|
filter : false, indexOf : false,
|
||||||
|
|
||||||
|
remainder : false, factorial : false,
|
||||||
|
comparison : false, concatenate : false,
|
||||||
|
logical : false, assignment : false,
|
||||||
|
array : false, fndef : false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
//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 (html) {
|
||||||
@@ -50,6 +83,11 @@ renderer.link = function (href, title, text) {
|
|||||||
return out;
|
return out;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Disable default reflink behavior, as it steps on our variables extension
|
||||||
|
tokenizer.def = function () {
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
const mustacheSpans = {
|
const mustacheSpans = {
|
||||||
name : 'mustacheSpans',
|
name : 'mustacheSpans',
|
||||||
level : 'inline', // Is this a block-level or inline-level tokenizer?
|
level : 'inline', // Is this a block-level or inline-level tokenizer?
|
||||||
@@ -256,10 +294,10 @@ const superSubScripts = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const definitionLists = {
|
const definitionListsSingleLine = {
|
||||||
name : 'definitionLists',
|
name : 'definitionListsSingleLine',
|
||||||
level : 'block',
|
level : 'block',
|
||||||
start(src) { return src.match(/^.*?::.*/m)?.index; }, // Hint to Marked.js to stop and check for a match
|
start(src) { return src.match(/\n[^\n]*?::[^\n]*/m)?.index; }, // Hint to Marked.js to stop and check for a match
|
||||||
tokenizer(src, tokens) {
|
tokenizer(src, tokens) {
|
||||||
const regex = /^([^\n]*?)::([^\n]*)(?:\n|$)/ym;
|
const regex = /^([^\n]*?)::([^\n]*)(?:\n|$)/ym;
|
||||||
let match;
|
let match;
|
||||||
@@ -274,7 +312,7 @@ const definitionLists = {
|
|||||||
}
|
}
|
||||||
if(definitions.length) {
|
if(definitions.length) {
|
||||||
return {
|
return {
|
||||||
type : 'definitionLists',
|
type : 'definitionListsSingleLine',
|
||||||
raw : src.slice(0, endIndex),
|
raw : src.slice(0, endIndex),
|
||||||
definitions
|
definitions
|
||||||
};
|
};
|
||||||
@@ -288,9 +326,300 @@ const definitionLists = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Marked.use({ extensions: [mustacheSpans, mustacheDivs, mustacheInjectInline, definitionLists, superSubScripts] });
|
const definitionListsMultiLine = {
|
||||||
|
name : 'definitionListsMultiLine',
|
||||||
|
level : 'block',
|
||||||
|
start(src) { return src.match(/\n[^\n]*\n::/m)?.index; }, // Hint to Marked.js to stop and check for a match
|
||||||
|
tokenizer(src, tokens) {
|
||||||
|
const regex = /(\n?\n?(?!::)[^\n]+?(?=\n::))|\n::(.(?:.|\n)*?(?=(?:\n::)|(?:\n\n)|$))/y;
|
||||||
|
let match;
|
||||||
|
let endIndex = 0;
|
||||||
|
const definitions = [];
|
||||||
|
while (match = regex.exec(src)) {
|
||||||
|
if(match[1]) {
|
||||||
|
if(this.lexer.blockTokens(match[1].trim())[0]?.type !== 'paragraph') // DT must not be another block-level token besides <p>
|
||||||
|
break;
|
||||||
|
definitions.push({
|
||||||
|
dt : this.lexer.inlineTokens(match[1].trim()),
|
||||||
|
dds : []
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if(match[2] && definitions.length) {
|
||||||
|
definitions[definitions.length - 1].dds.push(
|
||||||
|
this.lexer.inlineTokens(match[2].trim().replace(/\s/g, ' '))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
endIndex = regex.lastIndex;
|
||||||
|
}
|
||||||
|
if(definitions.length) {
|
||||||
|
return {
|
||||||
|
type : 'definitionListsMultiLine',
|
||||||
|
raw : src.slice(0, endIndex),
|
||||||
|
definitions
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
renderer(token) {
|
||||||
|
let returnVal = `<dl>`;
|
||||||
|
token.definitions.forEach((def)=>{
|
||||||
|
const dds = def.dds.map((s)=>{
|
||||||
|
return `\n<dd>${this.parser.parseInline(s).trim()}</dd>`;
|
||||||
|
}).join('');
|
||||||
|
returnVal += `<dt>${this.parser.parseInline(def.dt)}</dt>${dds}\n`;
|
||||||
|
});
|
||||||
|
returnVal = returnVal.trim();
|
||||||
|
return `${returnVal}</dl>`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
//v=====--------------------< Variable Handling >-------------------=====v// 242 lines
|
||||||
|
const replaceVar = function(input, hoist=false, allowUnresolved=false) {
|
||||||
|
const regex = /([!$]?)\[((?!\s*\])(?:\\.|[^\[\]\\])+)\]/g;
|
||||||
|
const match = regex.exec(input);
|
||||||
|
|
||||||
|
const prefix = match[1];
|
||||||
|
const label = match[2];
|
||||||
|
|
||||||
|
//v=====--------------------< HANDLE MATH >-------------------=====v//
|
||||||
|
const mathRegex = /[a-z]+\(|[+\-*/^()]/g;
|
||||||
|
const matches = label.split(mathRegex);
|
||||||
|
const mathVars = matches.filter((match)=>isNaN(match))?.map((s)=>s.trim()); // Capture any variable names
|
||||||
|
|
||||||
|
let replacedLabel = label;
|
||||||
|
|
||||||
|
if(prefix[0] == '$' && mathVars?.[0] !== label.trim()) {// If there was mathy stuff not captured, let's do math!
|
||||||
|
mathVars?.forEach((variable)=>{
|
||||||
|
const foundVar = lookupVar(variable, globalPageNumber, hoist);
|
||||||
|
if(foundVar && foundVar.resolved && foundVar.content && !isNaN(foundVar.content)) // Only subsitute math values if fully resolved, not empty strings, and numbers
|
||||||
|
replacedLabel = replacedLabel.replaceAll(variable, foundVar.content);
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
return mathParser.evaluate(replacedLabel);
|
||||||
|
} catch (error) {
|
||||||
|
return undefined; // Return undefined if invalid math result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//^=====--------------------< HANDLE MATH >-------------------=====^//
|
||||||
|
|
||||||
|
const foundVar = lookupVar(label, globalPageNumber, hoist);
|
||||||
|
|
||||||
|
if(!foundVar || (!foundVar.resolved && !allowUnresolved))
|
||||||
|
return undefined; // Return undefined if not found, or parially-resolved vars are not allowed
|
||||||
|
|
||||||
|
// url or <url> "title" or 'title' or (title)
|
||||||
|
const linkRegex = /^([^<\s][^\s]*|<.*?>)(?: ("(?:\\"|[^"])*"|'(?:\\'|[^'])*'|\((?:\\\(|\\\)|[^()])*\)))?$/m;
|
||||||
|
const linkMatch = linkRegex.exec(foundVar.content);
|
||||||
|
|
||||||
|
const href = linkMatch ? linkMatch[1] : null; //TODO: TRIM OFF < > IF PRESENT
|
||||||
|
const title = linkMatch ? linkMatch[2]?.slice(1, -1) : null;
|
||||||
|
|
||||||
|
if(!prefix[0] && href) // Link
|
||||||
|
return `[${label}](${href}${title ? ` "${title}"` : ''})`;
|
||||||
|
|
||||||
|
if(prefix[0] == '!' && href) // Image
|
||||||
|
return ``;
|
||||||
|
|
||||||
|
if(prefix[0] == '$') // Variable
|
||||||
|
return foundVar.content;
|
||||||
|
};
|
||||||
|
|
||||||
|
const lookupVar = function(label, index, hoist=false) {
|
||||||
|
while (index >= 0) {
|
||||||
|
if(globalVarsList[index]?.[label] !== undefined)
|
||||||
|
return globalVarsList[index][label];
|
||||||
|
index--;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(hoist) { //If normal lookup failed, attempt hoisting
|
||||||
|
index = Object.keys(globalVarsList).length; // Move index to start from last page
|
||||||
|
while (index >= 0) {
|
||||||
|
if(globalVarsList[index]?.[label] !== undefined)
|
||||||
|
return globalVarsList[index][label];
|
||||||
|
index--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const processVariableQueue = function() {
|
||||||
|
let resolvedOne = true;
|
||||||
|
let finalLoop = false;
|
||||||
|
while (resolvedOne || finalLoop) { // Loop through queue until no more variable calls can be resolved
|
||||||
|
resolvedOne = false;
|
||||||
|
for (const item of varsQueue) {
|
||||||
|
if(item.type == 'text')
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if(item.type == 'varDefBlock') {
|
||||||
|
const regex = /[!$]?\[((?!\s*\])(?:\\.|[^\[\]\\])+)\]/g;
|
||||||
|
let match;
|
||||||
|
let resolved = true;
|
||||||
|
let tempContent = item.content;
|
||||||
|
while (match = regex.exec(item.content)) { // regex to find variable calls
|
||||||
|
const value = replaceVar(match[0], true);
|
||||||
|
|
||||||
|
if(value == undefined)
|
||||||
|
resolved = false;
|
||||||
|
else
|
||||||
|
tempContent = tempContent.replaceAll(match[0], value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(resolved == true || item.content != tempContent) {
|
||||||
|
resolvedOne = true;
|
||||||
|
item.content = tempContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
globalVarsList[globalPageNumber][item.varName] = {
|
||||||
|
content : item.content,
|
||||||
|
resolved : resolved
|
||||||
|
};
|
||||||
|
|
||||||
|
if(resolved)
|
||||||
|
item.type = 'resolved';
|
||||||
|
}
|
||||||
|
|
||||||
|
if(item.type == 'varCallBlock' || item.type == 'varCallInline') {
|
||||||
|
const value = replaceVar(item.content, true, finalLoop); // final loop will just use the best value so far
|
||||||
|
|
||||||
|
if(value == undefined)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
resolvedOne = true;
|
||||||
|
item.content = value;
|
||||||
|
item.type = 'text';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
varsQueue = varsQueue.filter((item)=>item.type !== 'resolved'); // Remove any fully-resolved variable definitions
|
||||||
|
|
||||||
|
if(finalLoop)
|
||||||
|
break;
|
||||||
|
if(!resolvedOne)
|
||||||
|
finalLoop = true;
|
||||||
|
}
|
||||||
|
varsQueue = varsQueue.filter((item)=>item.type !== 'varDefBlock');
|
||||||
|
};
|
||||||
|
|
||||||
|
function MarkedVariables() {
|
||||||
|
return {
|
||||||
|
hooks : {
|
||||||
|
preprocess(src) {
|
||||||
|
const codeBlockSkip = /^(?: {4}[^\n]+(?:\n(?: *(?:\n|$))*)?)+|^ {0,3}(`{3,}(?=[^`\n]*(?:\n|$))|~{3,})(?:[^\n]*)(?:\n|$)(?:|(?:[\s\S]*?)(?:\n|$))(?: {0,3}\2[~`]* *(?=\n|$))|`[^`]*?`/;
|
||||||
|
const blockDefRegex = /^[!$]?\[((?!\s*\])(?:\\.|[^\[\]\\])+)\]:(?!\() *((?:\n? *[^\s].*)+)(?=\n+|$)/; //Matches 3, [4]:5
|
||||||
|
const blockCallRegex = /^[!$]?\[((?!\s*\])(?:\\.|[^\[\]\\])+)\](?=\n|$)/; //Matches 6, [7]
|
||||||
|
const inlineDefRegex = /([!$]?\[((?!\s*\])(?:\\.|[^\[\]\\])+)\])\(([^\n]+)\)/; //Matches 8, 9[10](11)
|
||||||
|
const inlineCallRegex = /[!$]?\[((?!\s*\])(?:\\.|[^\[\]\\])+)\](?!\()/; //Matches 12, [13]
|
||||||
|
|
||||||
|
// Combine regexes and wrap in parens like so: (regex1)|(regex2)|(regex3)|(regex4)
|
||||||
|
const combinedRegex = new RegExp([codeBlockSkip, blockDefRegex, blockCallRegex, inlineDefRegex, inlineCallRegex].map((s)=>`(${s.source})`).join('|'), 'gm');
|
||||||
|
|
||||||
|
let lastIndex = 0;
|
||||||
|
let match;
|
||||||
|
while ((match = combinedRegex.exec(src)) !== null) {
|
||||||
|
// Format any matches into tokens and store
|
||||||
|
if(match.index > lastIndex) { // Any non-variable stuff
|
||||||
|
varsQueue.push(
|
||||||
|
{ type : 'text',
|
||||||
|
varName : null,
|
||||||
|
content : src.slice(lastIndex, match.index)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if(match[1]) {
|
||||||
|
varsQueue.push(
|
||||||
|
{ type : 'text',
|
||||||
|
varName : null,
|
||||||
|
content : match[0]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if(match[3]) { // Block Definition
|
||||||
|
const label = match[4] ? match[4].trim().replace(/\s+/g, ' ') : null; // Trim edge spaces and shorten blocks of whitespace to 1 space
|
||||||
|
const content = match[5] ? match[5].trim().replace(/[ \t]+/g, ' ') : null; // Trim edge spaces and shorten blocks of whitespace to 1 space
|
||||||
|
|
||||||
|
varsQueue.push(
|
||||||
|
{ type : 'varDefBlock',
|
||||||
|
varName : label,
|
||||||
|
content : content
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if(match[6]) { // Block Call
|
||||||
|
const label = match[7] ? match[7].trim().replace(/\s+/g, ' ') : null; // Trim edge spaces and shorten blocks of whitespace to 1 space
|
||||||
|
|
||||||
|
varsQueue.push(
|
||||||
|
{ type : 'varCallBlock',
|
||||||
|
varName : label,
|
||||||
|
content : match[0]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
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
|
||||||
|
let content = match[11] ? match[11].trim().replace(/\s+/g, ' ') : null; // Trim edge spaces and shorten blocks of whitespace to 1 space
|
||||||
|
|
||||||
|
// In case of nested (), find the correct matching end )
|
||||||
|
let level = 0;
|
||||||
|
let i;
|
||||||
|
for (i = 0; i < content.length; i++) {
|
||||||
|
if(content[i] === '\\') {
|
||||||
|
i++;
|
||||||
|
} else if(content[i] === '(') {
|
||||||
|
level++;
|
||||||
|
} else if(content[i] === ')') {
|
||||||
|
level--;
|
||||||
|
if(level < 0)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(i > -1) {
|
||||||
|
combinedRegex.lastIndex = combinedRegex.lastIndex - (content.length - i);
|
||||||
|
content = content.slice(0, i).trim().replace(/\s+/g, ' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
varsQueue.push(
|
||||||
|
{ type : 'varDefBlock',
|
||||||
|
varName : label,
|
||||||
|
content : content
|
||||||
|
});
|
||||||
|
varsQueue.push(
|
||||||
|
{ type : 'varCallInline',
|
||||||
|
varName : label,
|
||||||
|
content : match[9]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if(match[12]) { // Inline Call
|
||||||
|
const label = match[13] ? match[13].trim().replace(/\s+/g, ' ') : null; // Trim edge spaces and shorten blocks of whitespace to 1 space
|
||||||
|
|
||||||
|
varsQueue.push(
|
||||||
|
{ type : 'varCallInline',
|
||||||
|
varName : label,
|
||||||
|
content : match[0]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
lastIndex = combinedRegex.lastIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(lastIndex < src.length) {
|
||||||
|
varsQueue.push(
|
||||||
|
{ type : 'text',
|
||||||
|
varName : null,
|
||||||
|
content : src.slice(lastIndex)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
processVariableQueue();
|
||||||
|
|
||||||
|
const output = varsQueue.map((item)=>item.content).join('');
|
||||||
|
varsQueue = []; // Must clear varsQueue because custom HTML renderer uses Marked.parse which will preprocess again without clearing the array
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
//^=====--------------------< Variable Handling >-------------------=====^//
|
||||||
|
|
||||||
|
Marked.use(MarkedVariables());
|
||||||
|
Marked.use({ extensions: [definitionListsMultiLine, definitionListsSingleLine, superSubScripts, mustacheSpans, mustacheDivs, mustacheInjectInline] });
|
||||||
Marked.use(mustacheInjectBlock);
|
Marked.use(mustacheInjectBlock);
|
||||||
Marked.use({ renderer: renderer, mangle: false });
|
Marked.use({ renderer: renderer, tokenizer: tokenizer, mangle: false });
|
||||||
Marked.use(MarkedExtendedTables(), MarkedGFMHeadingId(), MarkedSmartypantsLite());
|
Marked.use(MarkedExtendedTables(), MarkedGFMHeadingId(), MarkedSmartypantsLite());
|
||||||
|
|
||||||
const nonWordAndColonTest = /[^\w:]/g;
|
const nonWordAndColonTest = /[^\w:]/g;
|
||||||
@@ -369,12 +698,28 @@ const processStyleTags = (string)=>{
|
|||||||
`${attributes?.length ? ` ${attributes.join(' ')}` : ''}`;
|
`${attributes?.length ? ` ${attributes.join(' ')}` : ''}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const globalVarsList = {};
|
||||||
|
let varsQueue = [];
|
||||||
|
let globalPageNumber = 0;
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
marked : Marked,
|
marked : Marked,
|
||||||
render : (rawBrewText)=>{
|
render : (rawBrewText, pageNumber=1)=>{
|
||||||
|
globalVarsList[pageNumber] = {}; //Reset global links for current page, to ensure values are parsed in order
|
||||||
|
varsQueue = []; //Could move into MarkedVariables()
|
||||||
|
globalPageNumber = pageNumber;
|
||||||
|
|
||||||
rawBrewText = rawBrewText.replace(/^\\column$/gm, `\n<div class='columnSplit'></div>\n`)
|
rawBrewText = rawBrewText.replace(/^\\column$/gm, `\n<div class='columnSplit'></div>\n`)
|
||||||
.replace(/^(:+)$/gm, (match)=>`${`<div class='blank'></div>`.repeat(match.length)}\n`);
|
.replace(/^(:+)$/gm, (match)=>`${`<div class='blank'></div>`.repeat(match.length)}\n`);
|
||||||
return Marked.parse(rawBrewText);
|
const opts = Marked.defaults;
|
||||||
|
|
||||||
|
rawBrewText = opts.hooks.preprocess(rawBrewText);
|
||||||
|
const tokens = Marked.lexer(rawBrewText, opts);
|
||||||
|
|
||||||
|
Marked.walkTokens(tokens, opts.walkTokens);
|
||||||
|
|
||||||
|
const html = Marked.parser(tokens, opts);
|
||||||
|
return opts.hooks.postprocess(html);
|
||||||
},
|
},
|
||||||
|
|
||||||
validate : (rawBrewText)=>{
|
validate : (rawBrewText)=>{
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
require('./nav.less');
|
require('client/homebrew/navbar/navbar.less');
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const { useState, useRef, useEffect } = React;
|
const { useState, useRef, useEffect } = React;
|
||||||
const createClass = require('create-react-class');
|
const createClass = require('create-react-class');
|
||||||
@@ -104,7 +104,7 @@ const Nav = {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`navDropdownContainer ${props.className}`}
|
<div className={`navDropdownContainer ${props.className ?? ''}`}
|
||||||
ref={myRef}
|
ref={myRef}
|
||||||
onMouseEnter = { props.trigger.includes('hover') ? ()=>handleDropdown(true) : undefined }
|
onMouseEnter = { props.trigger.includes('hover') ? ()=>handleDropdown(true) : undefined }
|
||||||
onMouseLeave = { props.trigger.includes('hover') ? ()=>handleDropdown(false) : undefined }
|
onMouseLeave = { props.trigger.includes('hover') ? ()=>handleDropdown(false) : undefined }
|
||||||
|
|||||||
@@ -1,97 +0,0 @@
|
|||||||
@import '../styles/colors';
|
|
||||||
@keyframes glideDropDown {
|
|
||||||
0% {transform : translate(0px, -100%);
|
|
||||||
opacity : 0;
|
|
||||||
background-color: #333;}
|
|
||||||
100% {transform : translate(0px, 0px);
|
|
||||||
opacity : 1;
|
|
||||||
background-color: #333;}
|
|
||||||
}
|
|
||||||
nav{
|
|
||||||
background-color : #333;
|
|
||||||
.navContent{
|
|
||||||
position : relative;
|
|
||||||
display : flex;
|
|
||||||
justify-content : space-between;
|
|
||||||
z-index : 2;
|
|
||||||
}
|
|
||||||
.navSection{
|
|
||||||
display : flex;
|
|
||||||
align-items : center;
|
|
||||||
}
|
|
||||||
.navLogo{
|
|
||||||
display : block;
|
|
||||||
margin-top : 0px;
|
|
||||||
margin-right : 8px;
|
|
||||||
margin-left : 8px;
|
|
||||||
color : white;
|
|
||||||
text-decoration : none;
|
|
||||||
&:hover{
|
|
||||||
.name{ color : @orange; }
|
|
||||||
svg{ fill : @orange }
|
|
||||||
}
|
|
||||||
svg{
|
|
||||||
height : 13px;
|
|
||||||
margin-right : 0.2em;
|
|
||||||
cursor : pointer;
|
|
||||||
fill : white;
|
|
||||||
}
|
|
||||||
span.name{
|
|
||||||
font-family : 'CodeLight';
|
|
||||||
font-size : 15px;
|
|
||||||
span.crit{
|
|
||||||
font-family : 'CodeBold';
|
|
||||||
}
|
|
||||||
small{
|
|
||||||
font-family : 'Open Sans';
|
|
||||||
font-size : 0.3em;
|
|
||||||
font-weight : 800;
|
|
||||||
text-transform : uppercase;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.navItem{
|
|
||||||
#backgroundColorsHover;
|
|
||||||
.animate(background-color);
|
|
||||||
padding : 8px 12px;
|
|
||||||
cursor : pointer;
|
|
||||||
background-color : #333;
|
|
||||||
font-size : 10px;
|
|
||||||
font-weight : 800;
|
|
||||||
color : white;
|
|
||||||
text-decoration : none;
|
|
||||||
text-transform : uppercase;
|
|
||||||
line-height : 13px;
|
|
||||||
i{
|
|
||||||
margin-left : 5px;
|
|
||||||
font-size : 13px;
|
|
||||||
float : right;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.navSection:last-child .navItem{
|
|
||||||
border-left : 1px solid #666;
|
|
||||||
}
|
|
||||||
.navDropdownContainer{
|
|
||||||
position: relative;
|
|
||||||
.navDropdown {
|
|
||||||
position : absolute;
|
|
||||||
top : 28px;
|
|
||||||
left : 0px;
|
|
||||||
z-index : 10000;
|
|
||||||
width : 100%;
|
|
||||||
overflow : hidden auto;
|
|
||||||
max-height : calc(100vh - 28px);
|
|
||||||
.navItem{
|
|
||||||
animation-name: glideDropDown;
|
|
||||||
animation-duration: 0.4s;
|
|
||||||
position : relative;
|
|
||||||
display : block;
|
|
||||||
width : 100%;
|
|
||||||
vertical-align : middle;
|
|
||||||
padding : 8px 5px;
|
|
||||||
border : 1px solid #888;
|
|
||||||
border-bottom : 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
91
tests/markdown/definition-lists.test.js
Normal file
91
tests/markdown/definition-lists.test.js
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
/* eslint-disable max-lines */
|
||||||
|
|
||||||
|
const Markdown = require('naturalcrit/markdown.js');
|
||||||
|
|
||||||
|
describe('Inline Definition Lists', ()=>{
|
||||||
|
test('No Term 1 Definition', function() {
|
||||||
|
const source = ':: My First Definition\n\n';
|
||||||
|
const rendered = Markdown.render(source).trim();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<dl><dt></dt><dd>My First Definition</dd>\n</dl>');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Single Definition Term', function() {
|
||||||
|
const source = 'My term :: My First Definition\n\n';
|
||||||
|
const rendered = Markdown.render(source).trim();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<dl><dt>My term</dt><dd>My First Definition</dd>\n</dl>');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Multiple Definition Terms', function() {
|
||||||
|
const source = 'Term 1::Definition of Term 1\nTerm 2::Definition of Term 2\n\n';
|
||||||
|
const rendered = Markdown.render(source).trim();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<dl><dt>Term 1</dt><dd>Definition of Term 1</dd>\n<dt>Term 2</dt><dd>Definition of Term 2</dd>\n</dl>');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Multiline Definition Lists', ()=>{
|
||||||
|
test('Single Term, Single Definition', function() {
|
||||||
|
const source = 'Term 1\n::Definition 1\n\n';
|
||||||
|
const rendered = Markdown.render(source).trim();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<dl><dt>Term 1</dt>\n<dd>Definition 1</dd></dl>');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Single Term, Plural Definitions', function() {
|
||||||
|
const source = 'Term 1\n::Definition 1\n::Definition 2\n\n';
|
||||||
|
const rendered = Markdown.render(source).trim();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<dl><dt>Term 1</dt>\n<dd>Definition 1</dd>\n<dd>Definition 2</dd></dl>');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Multiple Term, Single Definitions', function() {
|
||||||
|
const source = 'Term 1\n::Definition 1\n\nTerm 2\n::Definition 1\n\n';
|
||||||
|
const rendered = Markdown.render(source).trim();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<dl><dt>Term 1</dt>\n<dd>Definition 1</dd>\n<dt>Term 2</dt>\n<dd>Definition 1</dd></dl>');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Multiple Term, Plural Definitions', function() {
|
||||||
|
const source = 'Term 1\n::Definition 1\n::Definition 2\n\nTerm 2\n::Definition 1\n::Definition 2\n\n';
|
||||||
|
const rendered = Markdown.render(source).trim();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<dl><dt>Term 1</dt>\n<dd>Definition 1</dd>\n<dd>Definition 2</dd>\n<dt>Term 2</dt>\n<dd>Definition 1</dd>\n<dd>Definition 2</dd></dl>');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Single Term, Single multi-line definition', function() {
|
||||||
|
const source = 'Term 1\n::Definition 1\nand more and\nmore and more\n\n';
|
||||||
|
const rendered = Markdown.render(source).trim();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<dl><dt>Term 1</dt>\n<dd>Definition 1 and more and more and more</dd></dl>');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Single Term, Plural multi-line definitions', function() {
|
||||||
|
const source = 'Term 1\n::Definition 1\nand more and more\n::Definition 2\nand more\nand more\n::Definition 3\n\n';
|
||||||
|
const rendered = Markdown.render(source).trim();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<dl><dt>Term 1</dt>\n<dd>Definition 1 and more and more</dd>\n<dd>Definition 2 and more and more</dd>\n<dd>Definition 3</dd></dl>');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Multiple Term, Single multi-line definition', function() {
|
||||||
|
const source = 'Term 1\n::Definition 1\nand more and more\n\nTerm 2\n::Definition 1\n::Definition 2\n\n';
|
||||||
|
const rendered = Markdown.render(source).trim();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<dl><dt>Term 1</dt>\n<dd>Definition 1 and more and more</dd>\n<dt>Term 2</dt>\n<dd>Definition 1</dd>\n<dd>Definition 2</dd></dl>');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Multiple Term, Single multi-line definition, followed by an inline dl', function() {
|
||||||
|
const source = 'Term 1\n::Definition 1\nand more and more\n\nTerm 2\n::Definition 1\n::Definition 2\n\n::Inline Definition (no term)';
|
||||||
|
const rendered = Markdown.render(source).trim();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<dl><dt>Term 1</dt>\n<dd>Definition 1 and more and more</dd>\n<dt>Term 2</dt>\n<dd>Definition 1</dd>\n<dd>Definition 2</dd></dl><dl><dt></dt><dd>Inline Definition (no term)</dd>\n</dl>');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Multiple Term, Single multi-line definition, followed by paragraph', function() {
|
||||||
|
const source = 'Term 1\n::Definition 1\nand more and more\n\nTerm 2\n::Definition 1\n::Definition 2\n\nParagraph';
|
||||||
|
const rendered = Markdown.render(source).trim();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<dl><dt>Term 1</dt>\n<dd>Definition 1 and more and more</dd>\n<dt>Term 2</dt>\n<dd>Definition 1</dd>\n<dd>Definition 2</dd></dl><p>Paragraph</p>');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Block Token cannot be the Term of a multi-line definition', function() {
|
||||||
|
const source = '## Header\n::Definition 1 of a single-line DL\n::Definition 1 of another single-line DL';
|
||||||
|
const rendered = Markdown.render(source).trim();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<h2 id="header">Header</h2>\n<dl><dt></dt><dd>Definition 1 of a single-line DL</dd>\n<dt></dt><dd>Definition 1 of another single-line DL</dd>\n</dl>');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Inline DL has priority over Multiline', function() {
|
||||||
|
const source = 'Term 1 :: Inline definition 1\n:: Inline definition 2 (no DT)';
|
||||||
|
const rendered = Markdown.render(source).trim();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<dl><dt>Term 1</dt><dd>Inline definition 1</dd>\n<dt></dt><dd>Inline definition 2 (no DT)</dd>\n</dl>');
|
||||||
|
});
|
||||||
|
});
|
||||||
373
tests/markdown/variables.test.js
Normal file
373
tests/markdown/variables.test.js
Normal file
@@ -0,0 +1,373 @@
|
|||||||
|
/* eslint-disable max-lines */
|
||||||
|
|
||||||
|
const dedent = require('dedent-tabs').default;
|
||||||
|
const Markdown = require('naturalcrit/markdown.js');
|
||||||
|
|
||||||
|
// Marked.js adds line returns after closing tags on some default tokens.
|
||||||
|
// This removes those line returns for comparison sake.
|
||||||
|
String.prototype.trimReturns = function(){
|
||||||
|
return this.replace(/\r?\n|\r/g, '').trim();
|
||||||
|
};
|
||||||
|
|
||||||
|
renderAllPages = function(pages){
|
||||||
|
const outputs = [];
|
||||||
|
pages.forEach((page, index)=>{
|
||||||
|
const output = Markdown.render(page, index);
|
||||||
|
outputs.push(output);
|
||||||
|
});
|
||||||
|
|
||||||
|
return outputs;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Adding `.failing()` method to `describe` or `it` will make failing tests "pass" as long as they continue to fail.
|
||||||
|
// Remove the `.failing()` method once you have fixed the issue.
|
||||||
|
|
||||||
|
describe('Block-level variables', ()=>{
|
||||||
|
it('Handles variable assignment and recall with simple text', function() {
|
||||||
|
const source = dedent`
|
||||||
|
[var]: string
|
||||||
|
|
||||||
|
$[var]
|
||||||
|
`;
|
||||||
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p>string</p>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Handles variable assignment and recall with multiline string', function() {
|
||||||
|
const source = dedent`
|
||||||
|
[var]: string
|
||||||
|
across multiple
|
||||||
|
lines
|
||||||
|
|
||||||
|
$[var]`;
|
||||||
|
const rendered = Markdown.render(source).replace(/\s/g, ' ').trimReturns();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p>string across multiple lines</p>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Handles variable assignment and recall with tables', function() {
|
||||||
|
const source = dedent`
|
||||||
|
[var]:
|
||||||
|
##### Title
|
||||||
|
| H1 | H2 |
|
||||||
|
|:---|:--:|
|
||||||
|
| A | B |
|
||||||
|
| C | D |
|
||||||
|
|
||||||
|
$[var]`;
|
||||||
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent`
|
||||||
|
<h5 id="title">Title</h5>
|
||||||
|
<table><thead><tr><th align=left>H1</th>
|
||||||
|
<th align=center>H2</th>
|
||||||
|
</tr></thead><tbody><tr><td align=left>A</td>
|
||||||
|
<td align=center>B</td>
|
||||||
|
</tr><tr><td align=left>C</td>
|
||||||
|
<td align=center>D</td>
|
||||||
|
</tr></tbody></table>`.trimReturns());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Hoists undefined variables', function() {
|
||||||
|
const source = dedent`
|
||||||
|
$[var]
|
||||||
|
|
||||||
|
[var]: string`;
|
||||||
|
const rendered = Markdown.render(source).replace(/\s/g, ' ').trimReturns();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p>string</p>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Hoists last instance of variable', function() {
|
||||||
|
const source = dedent`
|
||||||
|
$[var]
|
||||||
|
|
||||||
|
[var]: string
|
||||||
|
|
||||||
|
[var]: new string`;
|
||||||
|
const rendered = Markdown.render(source).replace(/\s/g, ' ').trimReturns();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p>new string</p>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Handles complex hoisting', function() {
|
||||||
|
const source = dedent`
|
||||||
|
$[titleAndName]: $[title] $[fullName]
|
||||||
|
|
||||||
|
$[title]: Mr.
|
||||||
|
|
||||||
|
$[fullName]: $[firstName] $[lastName]
|
||||||
|
|
||||||
|
[firstName]: Bob
|
||||||
|
|
||||||
|
Welcome, $[titleAndName]!
|
||||||
|
|
||||||
|
[lastName]: Jacob
|
||||||
|
|
||||||
|
[lastName]: $[lastName]son
|
||||||
|
`;
|
||||||
|
const rendered = Markdown.render(source).replace(/\s/g, ' ').trimReturns();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p>Welcome, Mr. Bob Jacobson!</p>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Handles variable reassignment', function() {
|
||||||
|
const source = dedent`
|
||||||
|
[var]: one
|
||||||
|
|
||||||
|
$[var]
|
||||||
|
|
||||||
|
[var]: two
|
||||||
|
|
||||||
|
$[var]
|
||||||
|
`;
|
||||||
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p>one</p><p>two</p>'.trimReturns());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Handles variable reassignment with hoisting', function() {
|
||||||
|
const source = dedent`
|
||||||
|
$[var]
|
||||||
|
|
||||||
|
[var]: one
|
||||||
|
|
||||||
|
$[var]
|
||||||
|
|
||||||
|
[var]: two
|
||||||
|
|
||||||
|
$[var]
|
||||||
|
`;
|
||||||
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p>two</p><p>one</p><p>two</p>'.trimReturns());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Ignores undefined variables that can\'t be hoisted', function() {
|
||||||
|
const source = dedent`
|
||||||
|
$[var](My name is $[first] $[last])
|
||||||
|
|
||||||
|
$[last]: Jones
|
||||||
|
`;
|
||||||
|
const rendered = Markdown.render(source).replace(/\s/g, ' ').trimReturns();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p>My name is $[first] Jones</p>`.trimReturns());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Inline-level variables', ()=>{
|
||||||
|
it('Handles variable assignment and recall with simple text', function() {
|
||||||
|
const source = dedent`
|
||||||
|
$[var](string)
|
||||||
|
|
||||||
|
$[var]
|
||||||
|
`;
|
||||||
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p>string</p><p>string</p>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Hoists undefined variables when possible', function() {
|
||||||
|
const source = dedent`
|
||||||
|
$[var](My name is $[name] Jones)
|
||||||
|
|
||||||
|
[name]: Bob`;
|
||||||
|
const rendered = Markdown.render(source).replace(/\s/g, ' ').trimReturns();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p>My name is Bob Jones</p>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Hoists last instance of variable', function() {
|
||||||
|
const source = dedent`
|
||||||
|
$[var](My name is $[name] Jones)
|
||||||
|
|
||||||
|
$[name](Bob)
|
||||||
|
|
||||||
|
[name]: Bill`;
|
||||||
|
const rendered = Markdown.render(source).replace(/\s/g, ' ').trimReturns();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p>My name is Bill Jones</p> <p>Bob</p>`.trimReturns());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Only captures nested parens if balanced', function() {
|
||||||
|
const source = dedent`
|
||||||
|
$[var1](A variable (with nested parens) inside)
|
||||||
|
|
||||||
|
$[var1]
|
||||||
|
|
||||||
|
$[var2](A variable ) with unbalanced parens)
|
||||||
|
|
||||||
|
$[var2]`;
|
||||||
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent`
|
||||||
|
<p>A variable (with nested parens) inside</p>
|
||||||
|
<p>A variable (with nested parens) inside</p>
|
||||||
|
<p>A variable with unbalanced parens)</p>
|
||||||
|
<p>A variable</p>
|
||||||
|
`.trimReturns());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Math', ()=>{
|
||||||
|
it('Handles simple math using numbers only', function() {
|
||||||
|
const source = dedent`
|
||||||
|
$[1 + 3 * 5 - (1 / 4)]
|
||||||
|
`;
|
||||||
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p>15.75</p>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Handles round function', function() {
|
||||||
|
const source = dedent`
|
||||||
|
$[round(1/4)]`;
|
||||||
|
const rendered = Markdown.render(source).replace(/\s/g, ' ').trimReturns();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p>0</p>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Handles floor function', function() {
|
||||||
|
const source = dedent`
|
||||||
|
$[floor(0.6)]`;
|
||||||
|
const rendered = Markdown.render(source).replace(/\s/g, ' ').trimReturns();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p>0</p>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Handles ceil function', function() {
|
||||||
|
const source = dedent`
|
||||||
|
$[ceil(0.2)]`;
|
||||||
|
const rendered = Markdown.render(source).replace(/\s/g, ' ').trimReturns();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p>1</p>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Handles nested functions', function() {
|
||||||
|
const source = dedent`
|
||||||
|
$[ceil(floor(round(0.6)))]`;
|
||||||
|
const rendered = Markdown.render(source).replace(/\s/g, ' ').trimReturns();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p>1</p>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Handles simple math with variables', function() {
|
||||||
|
const source = dedent`
|
||||||
|
$[num1]: 5
|
||||||
|
|
||||||
|
$[num2]: 4
|
||||||
|
|
||||||
|
Answer is $[answer]($[1 + 3 * num1 - (1 / num2)]).
|
||||||
|
`;
|
||||||
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p>Answer is 15.75.</p>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Handles variable incrementing', function() {
|
||||||
|
const source = dedent`
|
||||||
|
$[num1]: 5
|
||||||
|
|
||||||
|
Increment num1 to get $[num1]($[num1 + 1]) and again to $[num1]($[num1 + 1]).
|
||||||
|
`;
|
||||||
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p>Increment num1 to get 6 and again to 7.</p>');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Code blocks', ()=>{
|
||||||
|
it('Ignores all variables in fenced code blocks', function() {
|
||||||
|
const source = dedent`
|
||||||
|
\`\`\`
|
||||||
|
[var]: string
|
||||||
|
|
||||||
|
$[var]
|
||||||
|
|
||||||
|
$[var](new string)
|
||||||
|
\`\`\`
|
||||||
|
`;
|
||||||
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent`
|
||||||
|
<pre><code>
|
||||||
|
[var]: string
|
||||||
|
|
||||||
|
$[var]
|
||||||
|
|
||||||
|
$[var](new string)
|
||||||
|
</code></pre>`.trimReturns());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Ignores all variables in indented code blocks', function() {
|
||||||
|
const source = dedent`
|
||||||
|
test
|
||||||
|
|
||||||
|
[var]: string
|
||||||
|
|
||||||
|
$[var]
|
||||||
|
|
||||||
|
$[var](new string)
|
||||||
|
`;
|
||||||
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent`
|
||||||
|
<p>test</p>
|
||||||
|
|
||||||
|
<pre><code>
|
||||||
|
[var]: string
|
||||||
|
|
||||||
|
$[var]
|
||||||
|
|
||||||
|
$[var](new string)
|
||||||
|
</code></pre>`.trimReturns());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Ignores all variables in inline code blocks', function() {
|
||||||
|
const source = '[var](Hello) `[link](url)`. This `[var] does not work`';
|
||||||
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent`
|
||||||
|
<p><a href="Hello">var</a> <code>[link](url)</code>. This <code>[var] does not work</code></p>`.trimReturns());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Normal Links and Images', ()=>{
|
||||||
|
it('Renders normal images', function() {
|
||||||
|
const source = ``;
|
||||||
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent`
|
||||||
|
<p><img src="url" alt="alt text"></p>`.trimReturns());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Renders normal images with a title', function() {
|
||||||
|
const source = 'An image !';
|
||||||
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent`
|
||||||
|
<p>An image <img src="url" alt="alt text" title="and title">!</p>`.trimReturns());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Applies curly injectors to images', function() {
|
||||||
|
const source = `{width:100px}`;
|
||||||
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent`
|
||||||
|
<p><img class="" style="width:100px;" src="url" alt="alt text"></p>`.trimReturns());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Renders normal links', function() {
|
||||||
|
const source = 'A Link to my [website](url)!';
|
||||||
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent`
|
||||||
|
<p>A Link to my <a href="url">website</a>!</p>`.trimReturns());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Renders normal links with a title', function() {
|
||||||
|
const source = 'A Link to my [website](url "and title")!';
|
||||||
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent`
|
||||||
|
<p>A Link to my <a href="url" title="and title">website</a>!</p>`.trimReturns());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Cross-page variables', ()=>{
|
||||||
|
it('Handles variable assignment and recall across pages', function() {
|
||||||
|
const source0 = `[var]: string`;
|
||||||
|
const source1 = `$[var]`;
|
||||||
|
const rendered = renderAllPages([source0, source1]).join('\n\\page\n').trimReturns();
|
||||||
|
expect(rendered, `Input:\n${[source0, source1].join('\n\\page\n')}`, { showPrefix: false }).toBe('\\page<p>string</p>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Handles hoisting across pages', function() {
|
||||||
|
const source0 = `$[var]`;
|
||||||
|
const source1 = `[var]: string`;
|
||||||
|
renderAllPages([source0, source1]).join('\n\\page\n').trimReturns(); //Requires one full render of document before hoisting is picked up
|
||||||
|
const rendered = renderAllPages([source0, source1]).join('\n\\page\n').trimReturns();
|
||||||
|
expect(rendered, `Input:\n${[source0, source1].join('\n\\page\n')}`, { showPrefix: false }).toBe('<p>string</p>\\page');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Handles reassignment and hoisting across pages', function() {
|
||||||
|
const source0 = `$[var]\n\n[var]: one\n\n$[var]`;
|
||||||
|
const source1 = `[var]: two\n\n$[var]`;
|
||||||
|
renderAllPages([source0, source1]).join('\n\\page\n').trimReturns(); //Requires one full render of document before hoisting is picked up
|
||||||
|
const rendered = renderAllPages([source0, source1]).join('\n\\page\n').trimReturns();
|
||||||
|
expect(rendered, `Input:\n${[source0, source1].join('\n\\page\n')}`, { showPrefix: false }).toBe('<p>two</p><p>one</p>\\page<p>two</p>');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -149,8 +149,6 @@ module.exports = {
|
|||||||

|

|
||||||
|
|
||||||
Homebrewery.Naturalcrit.com
|
Homebrewery.Naturalcrit.com
|
||||||
}}
|
}}`;
|
||||||
|
|
||||||
\page`;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ module.exports = function(props){
|
|||||||
|
|
||||||
return dedent`
|
return dedent`
|
||||||
{{toc,wide
|
{{toc,wide
|
||||||
# Table Of Contents
|
# Contents
|
||||||
|
|
||||||
${markdown}
|
${markdown}
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
@import (less) './themes/fonts/5e/fonts.less';
|
|
||||||
@import (less) './themes/assets/assets.less';
|
@import (less) './themes/assets/assets.less';
|
||||||
@import (less) './themes/fonts/icon fonts/font-icons.less';
|
@import (less) './themes/fonts/icon fonts/font-icons.less';
|
||||||
|
@import (less) './themes/fonts/icon fonts/diceFont.less';
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
//Colors
|
//Colors
|
||||||
@@ -307,7 +307,6 @@
|
|||||||
margin-left : -0.16cm;
|
margin-left : -0.16cm;
|
||||||
background-color : var(--HB_Color_MonsterStatBackground);
|
background-color : var(--HB_Color_MonsterStatBackground);
|
||||||
background-image : @monsterBlockBackground;
|
background-image : @monsterBlockBackground;
|
||||||
background-attachment : fixed;
|
|
||||||
background-blend-mode : overlay;
|
background-blend-mode : overlay;
|
||||||
border-style : solid;
|
border-style : solid;
|
||||||
border-width : 7px 6px;
|
border-width : 7px 6px;
|
||||||
@@ -403,15 +402,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.pageNumber {
|
.pageNumber {
|
||||||
position : absolute;
|
|
||||||
right : 2px;
|
right : 2px;
|
||||||
bottom : 22px;
|
bottom : 22px;
|
||||||
width : 50px;
|
|
||||||
font-size : 0.9em;
|
|
||||||
color : var(--HB_Color_Footnotes);
|
color : var(--HB_Color_Footnotes);
|
||||||
text-align : center;
|
|
||||||
text-indent : 0;
|
|
||||||
&.auto::after { content : counter(phb-page-numbers); }
|
|
||||||
}
|
}
|
||||||
.footnote {
|
.footnote {
|
||||||
position : absolute;
|
position : absolute;
|
||||||
@@ -539,14 +532,14 @@
|
|||||||
.page:has(.frontCover) {
|
.page:has(.frontCover) {
|
||||||
columns : 1;
|
columns : 1;
|
||||||
text-align : center;
|
text-align : center;
|
||||||
&::after { all : unset; }
|
&::after { display : none; }
|
||||||
h1 {
|
h1 {
|
||||||
margin-top : 1.2cm;
|
margin-top : 1.2cm;
|
||||||
margin-bottom : 0;
|
margin-bottom : 0;
|
||||||
font-family : 'NodestoCapsCondensed';
|
font-family : 'NodestoCapsCondensed';
|
||||||
font-size : 2.245cm;
|
font-size : 2.245cm;
|
||||||
font-weight : normal;
|
font-weight : normal;
|
||||||
line-height : 0.85em;
|
line-height : 1.9cm;
|
||||||
color : white;
|
color : white;
|
||||||
text-shadow : unset;
|
text-shadow : unset;
|
||||||
text-transform : uppercase;
|
text-transform : uppercase;
|
||||||
@@ -633,14 +626,14 @@
|
|||||||
.page:has(.insideCover) {
|
.page:has(.insideCover) {
|
||||||
columns : 1;
|
columns : 1;
|
||||||
text-align : center;
|
text-align : center;
|
||||||
&::after { all : unset; }
|
&::after { display : none; }
|
||||||
h1 {
|
h1 {
|
||||||
margin-top : 1.2cm;
|
margin-top : 1.2cm;
|
||||||
margin-bottom : 0;
|
margin-bottom : 0;
|
||||||
font-family : 'NodestoCapsCondensed';
|
font-family : 'NodestoCapsCondensed';
|
||||||
font-size : 2.1cm;
|
font-size : 2.1cm;
|
||||||
font-weight : normal;
|
font-weight : normal;
|
||||||
line-height : 0.85em;
|
line-height : 1.785cm;
|
||||||
text-transform : uppercase;
|
text-transform : uppercase;
|
||||||
}
|
}
|
||||||
h2 {
|
h2 {
|
||||||
@@ -679,7 +672,7 @@
|
|||||||
padding : 2.25cm 1.3cm 2cm 1.3cm;
|
padding : 2.25cm 1.3cm 2cm 1.3cm;
|
||||||
color : #FFFFFF;
|
color : #FFFFFF;
|
||||||
columns : 1;
|
columns : 1;
|
||||||
&::after { all : unset; }
|
&::after { display : none; }
|
||||||
.columnWrapper { width : 7.6cm; }
|
.columnWrapper { width : 7.6cm; }
|
||||||
.backCover {
|
.backCover {
|
||||||
position : absolute;
|
position : absolute;
|
||||||
@@ -695,7 +688,7 @@
|
|||||||
margin-bottom : 0.3cm;
|
margin-bottom : 0.3cm;
|
||||||
font-family : 'NodestoCapsCondensed';
|
font-family : 'NodestoCapsCondensed';
|
||||||
font-size : 1.35cm;
|
font-size : 1.35cm;
|
||||||
line-height : 0.95em;
|
line-height : 1.28cm;
|
||||||
color : #ED1C24;
|
color : #ED1C24;
|
||||||
text-align : center;
|
text-align : center;
|
||||||
}
|
}
|
||||||
@@ -721,7 +714,7 @@
|
|||||||
p {
|
p {
|
||||||
font-family : 'Overpass';
|
font-family : 'Overpass';
|
||||||
font-size : 0.332cm;
|
font-size : 0.332cm;
|
||||||
line-height : 1.5em;
|
line-height : 0.35cm;
|
||||||
}
|
}
|
||||||
hr + p {
|
hr + p {
|
||||||
margin-top : 0.6cm;
|
margin-top : 0.6cm;
|
||||||
@@ -746,10 +739,10 @@
|
|||||||
font-family : 'NodestoCapsWide';
|
font-family : 'NodestoCapsWide';
|
||||||
font-size : 0.4cm;
|
font-size : 0.4cm;
|
||||||
line-height : 1em;
|
line-height : 1em;
|
||||||
|
line-height : 1.28cm;
|
||||||
color : #FFFFFF;
|
color : #FFFFFF;
|
||||||
text-align : center;
|
text-align : center;
|
||||||
text-indent : 0;
|
text-indent : 0;
|
||||||
letter-spacing : 0.08em;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -789,7 +782,7 @@
|
|||||||
margin-left : auto;
|
margin-left : auto;
|
||||||
font-family : 'Overpass';
|
font-family : 'Overpass';
|
||||||
font-size : 0.45cm;
|
font-size : 0.45cm;
|
||||||
line-height : 1.1em;
|
line-height : 0.495cm;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -326,27 +326,27 @@ module.exports = [
|
|||||||
gen : dedent`{{font-family:CodeLight Dummy Text}}`
|
gen : dedent`{{font-family:CodeLight Dummy Text}}`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name : 'Scaly Sans Remake',
|
name : 'Scaly Sans',
|
||||||
icon : 'font ScalySansRemake',
|
icon : 'font ScalySansRemake',
|
||||||
gen : dedent`{{font-family:ScalySansRemake Dummy Text}}`
|
gen : dedent`{{font-family:ScalySansRemake Dummy Text}}`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name : 'Book Insanity Remake',
|
name : 'Book Insanity',
|
||||||
icon : 'font BookInsanityRemake',
|
icon : 'font BookInsanityRemake',
|
||||||
gen : dedent`{{font-family:BookInsanityRemake Dummy Text}}`
|
gen : dedent`{{font-family:BookInsanityRemake Dummy Text}}`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name : 'Mr Eaves Remake',
|
name : 'Mr Eaves',
|
||||||
icon : 'font MrEavesRemake',
|
icon : 'font MrEavesRemake',
|
||||||
gen : dedent`{{font-family:MrEavesRemake Dummy Text}}`
|
gen : dedent`{{font-family:MrEavesRemake Dummy Text}}`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Solbera Imitation Remake',
|
name: 'Solbera Imitation',
|
||||||
icon: 'font SolberaImitationRemake',
|
icon: 'font SolberaImitationRemake',
|
||||||
gen: dedent`{{font-family:SolberaImitationRemake Dummy Text}}`
|
gen: dedent`{{font-family:SolberaImitationRemake Dummy Text}}`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Scaly Sans Small Caps Remake',
|
name: 'Scaly Sans Small Caps',
|
||||||
icon: 'font ScalySansSmallCapsRemake',
|
icon: 'font ScalySansSmallCapsRemake',
|
||||||
gen: dedent`{{font-family:ScalySansSmallCapsRemake Dummy Text}}`
|
gen: dedent`{{font-family:ScalySansSmallCapsRemake Dummy Text}}`
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
@import (less) './themes/fonts/5e/fonts.less';
|
@import (less) './themes/fonts/5e/fonts.less';
|
||||||
@import (less) './themes/assets/assets.less';
|
@import (less) './themes/assets/assets.less';
|
||||||
|
@import (less) './themes/fonts/icon fonts/diceFont.less';
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
//Colors
|
//Colors
|
||||||
@@ -8,7 +9,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@page { margin : 0; }
|
@page { margin : 0; }
|
||||||
body { counter-reset : phb-page-numbers; }
|
body { counter-reset : page-numbers; }
|
||||||
* { -webkit-print-color-adjust : exact; }
|
* { -webkit-print-color-adjust : exact; }
|
||||||
|
|
||||||
//*****************************
|
//*****************************
|
||||||
@@ -47,7 +48,7 @@ body { counter-reset : phb-page-numbers; }
|
|||||||
height : 279.4mm;
|
height : 279.4mm;
|
||||||
padding : 1.4cm 1.9cm 1.7cm;
|
padding : 1.4cm 1.9cm 1.7cm;
|
||||||
overflow : hidden;
|
overflow : hidden;
|
||||||
counter-increment : phb-page-numbers;
|
counter-increment : page-numbers;
|
||||||
background-color : var(--HB_Color_Background);
|
background-color : var(--HB_Color_Background);
|
||||||
text-rendering : optimizeLegibility;
|
text-rendering : optimizeLegibility;
|
||||||
contain : size;
|
contain : size;
|
||||||
@@ -166,7 +167,6 @@ body { counter-reset : phb-page-numbers; }
|
|||||||
margin : 0;
|
margin : 0;
|
||||||
font-size : 120px;
|
font-size : 120px;
|
||||||
text-transform : uppercase;
|
text-transform : uppercase;
|
||||||
mix-blend-mode : overlay;
|
|
||||||
opacity : 30%;
|
opacity : 30%;
|
||||||
transform : rotate(-45deg);
|
transform : rotate(-45deg);
|
||||||
p { margin-bottom : none; }
|
p { margin-bottom : none; }
|
||||||
@@ -460,3 +460,22 @@ body { counter-reset : phb-page-numbers; }
|
|||||||
.homebreweryIcon.red { background-color : red; }
|
.homebreweryIcon.red { background-color : red; }
|
||||||
.homebreweryIcon.gold { background-image : linear-gradient(to top left, brown 22.5%, gold 40%, white 60%, gold 67.5%, brown 82.5%); }
|
.homebreweryIcon.gold { background-image : linear-gradient(to top left, brown 22.5%, gold 40%, white 60%, gold 67.5%, brown 82.5%); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//*****************************
|
||||||
|
//* Page Number
|
||||||
|
//*****************************/
|
||||||
|
.page {
|
||||||
|
.pageNumber {
|
||||||
|
position : absolute;
|
||||||
|
right : 30px;
|
||||||
|
bottom : 30px;
|
||||||
|
width : 50px;
|
||||||
|
font-size : 0.9em;
|
||||||
|
text-align : center;
|
||||||
|
&.auto::after { content : counter(page-numbers); }
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(even) {
|
||||||
|
.pageNumber { left : 30px; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -374,17 +374,9 @@
|
|||||||
}
|
}
|
||||||
.pageNumber{
|
.pageNumber{
|
||||||
font-family : FrederickaTheGreat;
|
font-family : FrederickaTheGreat;
|
||||||
position : absolute;
|
|
||||||
right : 3cm;
|
right : 3cm;
|
||||||
bottom : 1.25cm;
|
bottom : 1.25cm;
|
||||||
width : 50px;
|
|
||||||
font-size : 0.9em;
|
|
||||||
color : var(--HB_Color_HeaderText);
|
color : var(--HB_Color_HeaderText);
|
||||||
text-align : center;
|
|
||||||
text-indent : 0;
|
|
||||||
&.auto::after {
|
|
||||||
content : counter(phb-page-numbers);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.footnote{
|
.footnote{
|
||||||
position : absolute;
|
position : absolute;
|
||||||
|
|||||||
121
themes/codeMirror/customThemes/darkvision.css
Normal file
121
themes/codeMirror/customThemes/darkvision.css
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
.CodeMirror {
|
||||||
|
background: #0C0C0C;
|
||||||
|
color: #B9BDB6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Brew BG */
|
||||||
|
.brewRenderer {
|
||||||
|
background-color: #0C0C0C;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-s-darkvision {
|
||||||
|
/* Blinking cursor and selection */
|
||||||
|
.CodeMirror-cursor {
|
||||||
|
border-left: 1px solid #B9BDB6;
|
||||||
|
}
|
||||||
|
.CodeMirror-selected {
|
||||||
|
background: #E0E8FF40;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Line number stuff */
|
||||||
|
.CodeMirror-gutter-elt {
|
||||||
|
color: #81969A;
|
||||||
|
}
|
||||||
|
.CodeMirror-linenumber {
|
||||||
|
background-color: #0C0C0C;
|
||||||
|
}
|
||||||
|
.CodeMirror-gutter {
|
||||||
|
background-color: #0C0C0C;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* column splits */
|
||||||
|
.editor .codeEditor .columnSplit {
|
||||||
|
font-style: italic;
|
||||||
|
color: inherit;
|
||||||
|
background-color:#1F5763;
|
||||||
|
border-bottom: #299 solid 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* # headings */
|
||||||
|
.cm-header {
|
||||||
|
color: #C51B1B;
|
||||||
|
-webkit-text-stroke-width: 0.1px;
|
||||||
|
}
|
||||||
|
/* bold points */
|
||||||
|
.cm-strong {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #309DD2;
|
||||||
|
}
|
||||||
|
/* Link headings */
|
||||||
|
.cm-link {
|
||||||
|
color: #DD6300;
|
||||||
|
}
|
||||||
|
/* links */
|
||||||
|
.cm-string {
|
||||||
|
color: #5CE638;
|
||||||
|
}
|
||||||
|
/*@import*/
|
||||||
|
.cm-def {
|
||||||
|
color: #2986CC;
|
||||||
|
}
|
||||||
|
/* Bullets and such */
|
||||||
|
.cm-variable-2 {
|
||||||
|
color: #3CBF30;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tags (divs) */
|
||||||
|
.cm-tag {
|
||||||
|
color: #E3FF00;
|
||||||
|
}
|
||||||
|
.cm-attribute {
|
||||||
|
color: #E3FF00;
|
||||||
|
}
|
||||||
|
.cm-atom {
|
||||||
|
color: #CF7EA9;
|
||||||
|
}
|
||||||
|
.cm-qualifier {
|
||||||
|
color: #EE1919;
|
||||||
|
}
|
||||||
|
.cm-comment {
|
||||||
|
color: #BBC700;
|
||||||
|
}
|
||||||
|
.cm-keyword {
|
||||||
|
color: #CC66FF;
|
||||||
|
}
|
||||||
|
.cm-property {
|
||||||
|
color: aqua;
|
||||||
|
}
|
||||||
|
.cm-error {
|
||||||
|
color: #C50202;
|
||||||
|
}
|
||||||
|
.CodeMirror-foldmarker {
|
||||||
|
color: #F0FF00;
|
||||||
|
}
|
||||||
|
/* New page */
|
||||||
|
.cm-builtin {
|
||||||
|
color: #FFF;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor .codeEditor {
|
||||||
|
/* blocks */
|
||||||
|
.block:not(.cm-comment) {
|
||||||
|
color: magenta;
|
||||||
|
}
|
||||||
|
/* definition lists */
|
||||||
|
.define.definition {
|
||||||
|
color: #FFAA3E;
|
||||||
|
}
|
||||||
|
.define.term {
|
||||||
|
color: #7290d9;
|
||||||
|
}
|
||||||
|
.define:not(.term):not(.definition) {
|
||||||
|
background: #333;
|
||||||
|
}
|
||||||
|
/* New page */
|
||||||
|
.pageLine {
|
||||||
|
background: #000;
|
||||||
|
color: #000;
|
||||||
|
border-bottom: 1px solid #FFF;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@
|
|||||||
"colorforth",
|
"colorforth",
|
||||||
"darcula",
|
"darcula",
|
||||||
"darkbrewery-v301",
|
"darkbrewery-v301",
|
||||||
|
"darkvision",
|
||||||
"dracula",
|
"dracula",
|
||||||
"duotone-dark",
|
"duotone-dark",
|
||||||
"duotone-light",
|
"duotone-light",
|
||||||
|
|||||||
114
themes/fonts/icon fonts/diceFont.less
Normal file
114
themes/fonts/icon fonts/diceFont.less
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
/* Icon Font: diceFont */
|
||||||
|
@font-face {
|
||||||
|
font-family : 'DiceFont';
|
||||||
|
font-style : normal;
|
||||||
|
font-weight : normal;
|
||||||
|
src : url('../../../fonts/icon fonts/diceFont.woff2');
|
||||||
|
}
|
||||||
|
|
||||||
|
.df {
|
||||||
|
display : inline-block;
|
||||||
|
font-family : 'DiceFont';
|
||||||
|
font-style : normal;
|
||||||
|
font-weight : normal;
|
||||||
|
font-variant : normal;
|
||||||
|
line-height : 1;
|
||||||
|
text-decoration : inherit;
|
||||||
|
text-transform : none;
|
||||||
|
text-rendering : optimizeLegibility;
|
||||||
|
-moz-osx-font-smoothing : grayscale;
|
||||||
|
-webkit-font-smoothing : antialiased;
|
||||||
|
&.F::before { content : '\f190'; }
|
||||||
|
&.F-minus::before { content : '\f191'; }
|
||||||
|
&.F-plus::before { content : '\f192'; }
|
||||||
|
&.F-zero::before { content : '\f193'; }
|
||||||
|
&.d10::before { content : '\f194'; }
|
||||||
|
&.d10-0::before { content : '\f100'; }
|
||||||
|
&.d10-1::before { content : '\f101'; }
|
||||||
|
&.d10-10::before { content : '\f102'; }
|
||||||
|
&.d10-2::before { content : '\f103'; }
|
||||||
|
&.d10-3::before { content : '\f104'; }
|
||||||
|
&.d10-4::before { content : '\f105'; }
|
||||||
|
&.d10-5::before { content : '\f106'; }
|
||||||
|
&.d10-6::before { content : '\f107'; }
|
||||||
|
&.d10-7::before { content : '\f108'; }
|
||||||
|
&.d10-8::before { content : '\f109'; }
|
||||||
|
&.d10-9::before { content : '\f10a'; }
|
||||||
|
&.d12::before { content : '\f195'; }
|
||||||
|
&.d12-1::before { content : '\f10b'; }
|
||||||
|
&.d12-10::before { content : '\f10c'; }
|
||||||
|
&.d12-11::before { content : '\f10d'; }
|
||||||
|
&.d12-12::before { content : '\f10e'; }
|
||||||
|
&.d12-2::before { content : '\f10f'; }
|
||||||
|
&.d12-3::before { content : '\f110'; }
|
||||||
|
&.d12-4::before { content : '\f111'; }
|
||||||
|
&.d12-5::before { content : '\f112'; }
|
||||||
|
&.d12-6::before { content : '\f113'; }
|
||||||
|
&.d12-7::before { content : '\f114'; }
|
||||||
|
&.d12-8::before { content : '\f115'; }
|
||||||
|
&.d12-9::before { content : '\f116'; }
|
||||||
|
&.d2::before { content : '\f196'; }
|
||||||
|
&.d2-1::before { content : '\f117'; }
|
||||||
|
&.d2-2::before { content : '\f118'; }
|
||||||
|
&.d20::before { content : '\f197'; }
|
||||||
|
&.d20-1::before { content : '\f119'; }
|
||||||
|
&.d20-10::before { content : '\f11a'; }
|
||||||
|
&.d20-11::before { content : '\f11b'; }
|
||||||
|
&.d20-12::before { content : '\f11c'; }
|
||||||
|
&.d20-13::before { content : '\f11d'; }
|
||||||
|
&.d20-14::before { content : '\f11e'; }
|
||||||
|
&.d20-15::before { content : '\f11f'; }
|
||||||
|
&.d20-16::before { content : '\f120'; }
|
||||||
|
&.d20-17::before { content : '\f121'; }
|
||||||
|
&.d20-18::before { content : '\f122'; }
|
||||||
|
&.d20-19::before { content : '\f123'; }
|
||||||
|
&.d20-2::before { content : '\f124'; }
|
||||||
|
&.d20-20::before { content : '\f125'; }
|
||||||
|
&.d20-3::before { content : '\f126'; }
|
||||||
|
&.d20-4::before { content : '\f127'; }
|
||||||
|
&.d20-5::before { content : '\f128'; }
|
||||||
|
&.d20-6::before { content : '\f129'; }
|
||||||
|
&.d20-7::before { content : '\f12a'; }
|
||||||
|
&.d20-8::before { content : '\f12b'; }
|
||||||
|
&.d20-9::before { content : '\f12c'; }
|
||||||
|
&.d4::before { content : '\f198'; }
|
||||||
|
&.d4-1::before { content : '\f12d'; }
|
||||||
|
&.d4-2::before { content : '\f12e'; }
|
||||||
|
&.d4-3::before { content : '\f12f'; }
|
||||||
|
&.d4-4::before { content : '\f130'; }
|
||||||
|
&.d6::before { content : '\f199'; }
|
||||||
|
&.d6-1::before { content : '\f131'; }
|
||||||
|
&.d6-2::before { content : '\f132'; }
|
||||||
|
&.d6-3::before { content : '\f133'; }
|
||||||
|
&.d6-4::before { content : '\f134'; }
|
||||||
|
&.d6-5::before { content : '\f135'; }
|
||||||
|
&.d6-6::before { content : '\f136'; }
|
||||||
|
&.d8::before { content : '\f19a'; }
|
||||||
|
&.d8-1::before { content : '\f137'; }
|
||||||
|
&.d8-2::before { content : '\f138'; }
|
||||||
|
&.d8-3::before { content : '\f139'; }
|
||||||
|
&.d8-4::before { content : '\f13a'; }
|
||||||
|
&.d8-5::before { content : '\f13b'; }
|
||||||
|
&.d8-6::before { content : '\f13c'; }
|
||||||
|
&.d8-7::before { content : '\f13d'; }
|
||||||
|
&.d8-8::before { content : '\f13e'; }
|
||||||
|
&.dot-d6::before { content : '\f19b'; }
|
||||||
|
&.dot-d6-1::before { content : '\f13f'; }
|
||||||
|
&.dot-d6-2::before { content : '\f140'; }
|
||||||
|
&.dot-d6-3::before { content : '\f141'; }
|
||||||
|
&.dot-d6-4::before { content : '\f142'; }
|
||||||
|
&.dot-d6-5::before { content : '\f143'; }
|
||||||
|
&.dot-d6-6::before { content : '\f18f'; }
|
||||||
|
&.small-dot-d6-1::before { content : '\f183'; }
|
||||||
|
&.small-dot-d6-2::before { content : '\f184'; }
|
||||||
|
&.small-dot-d6-3::before { content : '\f185'; }
|
||||||
|
&.small-dot-d6-4::before { content : '\f186'; }
|
||||||
|
&.small-dot-d6-5::before { content : '\f187'; }
|
||||||
|
&.small-dot-d6-6::before { content : '\f188'; }
|
||||||
|
&.solid-small-dot-d6-1::before { content : '\f189'; }
|
||||||
|
&.solid-small-dot-d6-2::before { content : '\f18a'; }
|
||||||
|
&.solid-small-dot-d6-3::before { content : '\f18b'; }
|
||||||
|
&.solid-small-dot-d6-4::before { content : '\f18c'; }
|
||||||
|
&.solid-small-dot-d6-5::before { content : '\f18d'; }
|
||||||
|
&.solid-small-dot-d6-6::before { content : '\f18e'; }
|
||||||
|
}
|
||||||
BIN
themes/fonts/icon fonts/diceFont.woff2
Normal file
BIN
themes/fonts/icon fonts/diceFont.woff2
Normal file
Binary file not shown.
18
themes/fonts/icon fonts/diceFont_license.md
Normal file
18
themes/fonts/icon fonts/diceFont_license.md
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# License
|
||||||
|
|
||||||
|
DiceFont is open source. You can use it for commercial projects, personal
|
||||||
|
projects or open source projects.
|
||||||
|
|
||||||
|
## Font License
|
||||||
|
|
||||||
|
Applies to all desktop and webfont files: [License: SIL OFL 1.1](http://scripts.sil.org/OFL)
|
||||||
|
|
||||||
|
## Code License
|
||||||
|
|
||||||
|
Applies to all CSS and LESS files: [License: MIT License](http://opensource.org/licenses/mit-license.html)
|
||||||
|
|
||||||
|
## Documentation License
|
||||||
|
|
||||||
|
Applies to all other files [CC BY 3.0](http://creativecommons.org/licenses/by/3.0/)
|
||||||
|
|
||||||
|
Copyright [Franco Ponticelli](https://github.com/fponticelli).
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
/* Main Font, serif */
|
/* Icon Font: Elderberry Inn */
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family : 'Eldeberry-Inn';
|
font-family : 'Elderberry-Inn';
|
||||||
font-style : normal;
|
font-style : normal;
|
||||||
font-weight : normal;
|
font-weight : normal;
|
||||||
src : url('../../../fonts/icon fonts/Elderberry-Inn-Icons.woff2');
|
src : url('../../../fonts/icon fonts/Elderberry-Inn-Icons.woff2');
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
span.ei {
|
span.ei {
|
||||||
display : inline-block;
|
display : inline-block;
|
||||||
margin-right : 3px;
|
margin-right : 3px;
|
||||||
font-family : 'Eldeberry-Inn';
|
font-family : 'Elderberry-Inn';
|
||||||
line-height : 1;
|
line-height : 1;
|
||||||
vertical-align : baseline;
|
vertical-align : baseline;
|
||||||
-moz-osx-font-smoothing : grayscale;
|
-moz-osx-font-smoothing : grayscale;
|
||||||
|
|||||||
Reference in New Issue
Block a user