mirror of
https://github.com/naturalcrit/homebrewery.git
synced 2026-01-24 03:23:02 +00:00
Compare commits
198 Commits
ImplementC
...
v3.17
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8848c06b15 | ||
|
|
37d56f7365 | ||
|
|
e2d6b5afc4 | ||
|
|
e4df577a32 | ||
|
|
f005cb784f | ||
|
|
d733b1f8f8 | ||
|
|
d8d403ffb8 | ||
|
|
574d68f678 | ||
|
|
1b3d7b33c6 | ||
|
|
7f4a304f04 | ||
|
|
d0a06b5cf7 | ||
|
|
6dfd44e2f1 | ||
|
|
03e7699b8b | ||
|
|
11f4275e7b | ||
|
|
07fe1c6f19 | ||
|
|
3e78b03785 | ||
|
|
6a31d612e6 | ||
|
|
ecd8869097 | ||
|
|
73c2be147c | ||
|
|
caa290f580 | ||
|
|
d69288076a | ||
|
|
be18843b09 | ||
|
|
f1ff032e1e | ||
|
|
36df121cf6 | ||
|
|
c22bb7fb92 | ||
|
|
b94bb38922 | ||
|
|
1576a946b0 | ||
|
|
4de0a11f1a | ||
|
|
66fd9e188b | ||
|
|
a0f44a088f | ||
|
|
fb20be833c | ||
|
|
fc43f95ea5 | ||
|
|
29d04fe57d | ||
|
|
bd32f5a1b8 | ||
|
|
41b80422c5 | ||
|
|
c1f608d02f | ||
|
|
abc830eda2 | ||
|
|
7610466ee4 | ||
|
|
9f8831eed6 | ||
|
|
0ac981586f | ||
|
|
fc085111db | ||
|
|
5e03d97869 | ||
|
|
a11ae6655e | ||
|
|
2471de20a9 | ||
|
|
8e99d47869 | ||
|
|
eebc9c2bfa | ||
|
|
bd5c85147d | ||
|
|
7f7a8338ff | ||
|
|
2a9945f09f | ||
|
|
76ccbfbf20 | ||
|
|
77c58eae2e | ||
|
|
4a2b8dc261 | ||
|
|
fa1a0e2351 | ||
|
|
f7b36a9b05 | ||
|
|
f4ce2437a7 | ||
|
|
aa34bb44c9 | ||
|
|
e3c90ace73 | ||
|
|
7c1545a07d | ||
|
|
953c612830 | ||
|
|
5dbb5499c6 | ||
|
|
d4f6c329b8 | ||
|
|
a574ec0777 | ||
|
|
3e5a72fa96 | ||
|
|
4df2a73800 | ||
|
|
aea9296908 | ||
|
|
08eeb57cb0 | ||
|
|
e5e9a9efe1 | ||
|
|
aafc6fad7d | ||
|
|
b91f18a8a0 | ||
|
|
20bfff5157 | ||
|
|
3c735e599f | ||
|
|
4958ade937 | ||
|
|
57dc5d4923 | ||
|
|
3c5ad74e38 | ||
|
|
e988e20f5b | ||
|
|
cac6dbd40c | ||
|
|
2461b4ab6a | ||
|
|
7c4f163042 | ||
|
|
2fee37239f | ||
|
|
2cb19848aa | ||
|
|
913cde44ff | ||
|
|
c7ff1fc07f | ||
|
|
da42e835c5 | ||
|
|
7a071496f3 | ||
|
|
b8d65f2f56 | ||
|
|
9c197ea25a | ||
|
|
d75db5d378 | ||
|
|
a2538bed20 | ||
|
|
69c45d63a4 | ||
|
|
80003f6c57 | ||
|
|
9d67724da9 | ||
|
|
3578a7e1e2 | ||
|
|
533586f516 | ||
|
|
591ccf564c | ||
|
|
ecc91af1d6 | ||
|
|
4ff043f759 | ||
|
|
84e18aae5a | ||
|
|
b53bda937a | ||
|
|
4db4bba73f | ||
|
|
2c2e6d6027 | ||
|
|
1aeea034d2 | ||
|
|
63bd483b3e | ||
|
|
19cb24d8db | ||
|
|
96ebe0f617 | ||
|
|
eb3178bf80 | ||
|
|
a72f47df46 | ||
|
|
a9823d39e2 | ||
|
|
6ec65eee23 | ||
|
|
9c2610ff40 | ||
|
|
914521cada | ||
|
|
70bda94033 | ||
|
|
915137af5e | ||
|
|
7516c0cbd3 | ||
|
|
fdfae9a771 | ||
|
|
8cc693461d | ||
|
|
26cc272b37 | ||
|
|
bffa6eb0c9 | ||
|
|
2779055e50 | ||
|
|
37d00f1255 | ||
|
|
d9b599e814 | ||
|
|
40d453bc7c | ||
|
|
6ff0cfe383 | ||
|
|
a6b7ed4dd2 | ||
|
|
bf0614026d | ||
|
|
06005009e4 | ||
|
|
cf16566da8 | ||
|
|
34f104b406 | ||
|
|
766ab8f10a | ||
|
|
aa4276a50e | ||
|
|
fbedafb204 | ||
|
|
85cd7c7336 | ||
|
|
c137d40037 | ||
|
|
5a9e7850c2 | ||
|
|
6e7342d6f0 | ||
|
|
1598adfa67 | ||
|
|
b49936c24b | ||
|
|
816f4f75f6 | ||
|
|
a091a18604 | ||
|
|
edadb3cb77 | ||
|
|
3749a5c2b1 | ||
|
|
e9b5e4ab0c | ||
|
|
28109d28dc | ||
|
|
7f56797779 | ||
|
|
a95eef0545 | ||
|
|
bbf6c3589a | ||
|
|
4a4a14b2ab | ||
|
|
6b0c3b65b4 | ||
|
|
59006d354f | ||
|
|
26c9406211 | ||
|
|
fb13a1c98d | ||
|
|
b20eb28a37 | ||
|
|
d84f071c62 | ||
|
|
bc7297de2e | ||
|
|
a2c4f73e7d | ||
|
|
9804c3933f | ||
|
|
e2b0da7830 | ||
|
|
5a5119a367 | ||
|
|
c310a8c1c2 | ||
|
|
11bfdd89b8 | ||
|
|
2bedc6d7d4 | ||
|
|
674fb6ff57 | ||
|
|
79c8309291 | ||
|
|
90632b78ce | ||
|
|
f71850d8b1 | ||
|
|
99d3d28754 | ||
|
|
08b0f47ea2 | ||
|
|
f9b42a30f7 | ||
|
|
7c69d2a74d | ||
|
|
89bd082967 | ||
|
|
f4c26053c0 | ||
|
|
b7cb6dc444 | ||
|
|
8c986bb97d | ||
|
|
deb9c6651f | ||
|
|
440ad516df | ||
|
|
929469d0c0 | ||
|
|
49db31426c | ||
|
|
ce31d30ed7 | ||
|
|
68831c759f | ||
|
|
ebdbb39f24 | ||
|
|
976740dc8b | ||
|
|
cac87b14c7 | ||
|
|
df5ed5190a | ||
|
|
30dac3a73c | ||
|
|
ba4c9745a2 | ||
|
|
a1c275479f | ||
|
|
708cbdc9e5 | ||
|
|
b0585e28ad | ||
|
|
575aa447e0 | ||
|
|
e57b88a019 | ||
|
|
380c1444ca | ||
|
|
a59135430c | ||
|
|
bdf2c97942 | ||
|
|
177c90c8e9 | ||
|
|
933451b1ec | ||
|
|
effeffd906 | ||
|
|
c269d32247 | ||
|
|
17b081b18b | ||
|
|
7fc0cadb81 |
@@ -73,6 +73,9 @@ jobs:
|
|||||||
- run:
|
- run:
|
||||||
name: Test - Non-Breaking Spaces
|
name: Test - Non-Breaking Spaces
|
||||||
command: npm run test:non-breaking-spaces
|
command: npm run test:non-breaking-spaces
|
||||||
|
- run:
|
||||||
|
name: Test - Paragraph Justification
|
||||||
|
command: npm run test:paragraph-justification
|
||||||
- run:
|
- run:
|
||||||
name: Test - Variables
|
name: Test - Variables
|
||||||
command: npm run test:variables
|
command: npm run test:variables
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM node:20-alpine
|
FROM node:22-alpine
|
||||||
RUN apk --no-cache add git
|
RUN apk --no-cache add git
|
||||||
|
|
||||||
ENV NODE_ENV=docker
|
ENV NODE_ENV=docker
|
||||||
@@ -9,7 +9,10 @@ WORKDIR /usr/src/app
|
|||||||
# Copy package.json into the image, then run yarn install
|
# Copy package.json into the image, then run yarn install
|
||||||
# This improves caching so we don't have to download the dependencies every time the code changes
|
# This improves caching so we don't have to download the dependencies every time the code changes
|
||||||
COPY package.json ./
|
COPY package.json ./
|
||||||
|
COPY config/docker.json usr/src/app/config
|
||||||
# --ignore-scripts tells yarn not to run postbuild. We run it explicitly later
|
# --ignore-scripts tells yarn not to run postbuild. We run it explicitly later
|
||||||
|
RUN node --version
|
||||||
|
RUN npm --version
|
||||||
RUN npm install --ignore-scripts
|
RUN npm install --ignore-scripts
|
||||||
|
|
||||||
# Bundle app source and build application
|
# Bundle app source and build application
|
||||||
|
|||||||
123
README.DOCKER.md
123
README.DOCKER.md
@@ -1,12 +1,119 @@
|
|||||||
# Running Homebrewery via Docker
|
# Offline Install Instructions: Docker
|
||||||
|
|
||||||
The repo includes a Dockerfile and a docker-compose.yml file.
|
These instructions are for setting up a persistent instance of the Homebrewery application locally using Docker.
|
||||||
|
|
||||||
To run the application via docker-compose.yml:
|
If you intend to develop with Homebrewery, following the Homebrewery application section of this guide is not recommended. Using docker to deploy MongoDB locally for development is not a bad idea at all, however.
|
||||||
`docker-compose up -d`
|
|
||||||
|
|
||||||
To stop the application:
|
# Install Docker
|
||||||
`docker-compose down`
|
|
||||||
|
## Docker Desktop (MacOS/Windows)
|
||||||
|
|
||||||
|
Windows and Mac installs use Docker Desktop. Current install instructions are below.
|
||||||
|
|
||||||
|
* [Mac](https://docs.docker.com/desktop/mac/install/)
|
||||||
|
* [Windows](https://docs.docker.com/desktop/windows/install/)
|
||||||
|
|
||||||
|
You can set up the docker engine to start on boot via the Docker desktop UI.
|
||||||
|
|
||||||
|
## Docker Engine
|
||||||
|
|
||||||
|
Linux installs use Docker Engine. Docker provides installers and instructions for several of the most common distrubutions. If you do not see yours listed, it is very likely supported indirectly by your distribution.
|
||||||
|
|
||||||
|
* [Arch](https://docs.docker.com/desktop/setup/install/linux/archlinux/)
|
||||||
|
* [CentOS](https://docs.docker.com/engine/install/centos/)
|
||||||
|
* [Debian](https://docs.docker.com/engine/install/debian/)
|
||||||
|
* [Fedora](https://docs.docker.com/engine/install/fedora/)
|
||||||
|
* [RHEL](https://docs.docker.com/engine/install/rhel/)
|
||||||
|
* [Ubuntu](https://docs.docker.com/engine/install/ubuntu/)
|
||||||
|
|
||||||
|
### Post installation steps
|
||||||
|
[Manage Docker as a non-root user (highly recommended)](https://docs.docker.com/engine/install/linux-postinstall/#manage-docker-as-a-non-root-user)
|
||||||
|
[Enable Docker to start on boot (highly recommended)](https://docs.docker.com/engine/install/linux-postinstall/#configure-docker-to-start-on-boot)
|
||||||
|
|
||||||
|
# Build Homebrewery Image
|
||||||
|
|
||||||
|
Next we build the homebrewery docker image. Start by cloning the repository.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
git clone https://github.com/naturalcrit/homebrewery.git
|
||||||
|
cd homebrewery
|
||||||
|
```
|
||||||
|
|
||||||
|
Make an changes you need to `config/docker.json` then build the image. If it does not exist,the below as a template.
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"host" : "localhost:8000",
|
||||||
|
"naturalcrit_url" : "local.naturalcrit.com:8010",
|
||||||
|
"secret" : "secret",
|
||||||
|
"web_port" : 8000,
|
||||||
|
"enable_v3" : true,
|
||||||
|
"mongodb_uri": "mongodb://172.17.0.2/homebrewery",
|
||||||
|
"enable_themes" : true,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```shell
|
||||||
|
docker-compose build homebrewery
|
||||||
|
```
|
||||||
|
|
||||||
|
# Add Mongo container
|
||||||
|
|
||||||
|
Once docker is installed and running, it is time to set up the containers. First up, Mongo.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
docker run --name homebrewery-mongodb -d --restart unless-stopped -v mongodata:/data/db -p 27017:27017 mongo:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
Older CPUs may run into an issue with AVX support.
|
||||||
|
```
|
||||||
|
WARNING: MongoDB 5.0+ requires a CPU with AVX support, and your current system does not appear to have that!
|
||||||
|
see https://jira.mongodb.org/browse/SERVER-54407
|
||||||
|
see also https://www.mongodb.com/community/forums/t/mongodb-5-0-cpu-intel-g4650-compatibility/116610/2
|
||||||
|
see also https://github.com/docker-library/mongo/issues/485#issuecomment-891991814
|
||||||
|
```
|
||||||
|
If you see a message similar to this, try using the bitnami mongo instead.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
docker run --name homebrewery-mongodb -d --restart unless-stopped -v mongodata:/data/db -p 27017:27017 bitnami/mongo:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
If your distribution is running on an arm device such as a Raspberry Pi, you will need to run the arm-built MongoDB v4.4.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
docker run --name homebrewery-mongodb -d --restart unless-stopped -v mongodata:/data/db -p 27017:27017 arm64v8/mongo:4.4
|
||||||
|
```
|
||||||
|
|
||||||
|
## Run the Homebrewery Image
|
||||||
|
```shell
|
||||||
|
# Make sure you run this in the homebrewery directory
|
||||||
|
docker run --name homebrewery-app -d --restart unless-stopped -e NODE_ENV=docker -v $(pwd)/config/docker.json:/usr/src/app/config/docker.json -p 8000:8000 docker.io/library/homebrewery:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
## Updating the Image
|
||||||
|
|
||||||
|
When Homebrewery code updates, your docker container will not automatically follow the changes. To do so you will need to rebuild your homebrewery image.
|
||||||
|
|
||||||
|
First, return to your homebrewery clone (from Build Homebrewery Image above) or recreate the clone if you deleted your copy of the code.
|
||||||
|
|
||||||
|
First, delete the existing image.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
docker rm -f homebrewery-app
|
||||||
|
```
|
||||||
|
|
||||||
|
Next, update the clone's code to the latest version.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
cd homebrewery
|
||||||
|
git checkout master
|
||||||
|
git pull upstream master
|
||||||
|
```
|
||||||
|
|
||||||
|
Finally, rebuild and restart the homebrewery image.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
docker-compose build homebrewery
|
||||||
|
docker run --name homebrewery-app -d --restart unless-stopped -e NODE_ENV=docker -v $(pwd)/config/docker.json:/usr/src/app/config/docker.json -p 8000:8000 docker.io/library/homebrewery:latest
|
||||||
|
```
|
||||||
|
|
||||||
To stop the application and remove all data:
|
|
||||||
`docker-compose down -v`
|
|
||||||
|
|||||||
94
changelog.md
94
changelog.md
@@ -77,14 +77,100 @@ pre {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.varSyntaxTable th:first-of-type {
|
.varSyntaxTable th:first-of-type {
|
||||||
width:6cm;
|
width:6cm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page .exampleTable td,th {
|
||||||
|
border:1px dashed #00000030;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
## changelog
|
## changelog
|
||||||
For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery).
|
For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery).
|
||||||
|
|
||||||
|
### Thursday 01/30/2024 - v3.17.0
|
||||||
|
|
||||||
|
{{taskList
|
||||||
|
##### 5e-Cleric
|
||||||
|
|
||||||
|
* [x] Update FAQ
|
||||||
|
|
||||||
|
* [x] Fix styling for Vault buttons and checkboxes
|
||||||
|
|
||||||
|
* [x] Improve navigation bar styling
|
||||||
|
|
||||||
|
* [x] Add feature to change username at https://www.naturalcrit.com/account
|
||||||
|
|
||||||
|
* [x] Fix Reddit link crash when title has non-latin chars
|
||||||
|
|
||||||
|
##### dbolack
|
||||||
|
|
||||||
|
* [x] Fix page shadows toolbar option
|
||||||
|
|
||||||
|
Fixes issue [#3919](https://github.com/naturalcrit/homebrewery/issues/3919)
|
||||||
|
|
||||||
|
* [x] Add `:>>>` syntax for horizontal :>>>>> spaces
|
||||||
|
|
||||||
|
* [x] Update Docker install instructions
|
||||||
|
|
||||||
|
Fixes issue [#1930](https://github.com/naturalcrit/homebrewery/issues/1930)
|
||||||
|
|
||||||
|
* [x] Allow styling pages via `\page{myStyles}` (with calculuschild)
|
||||||
|
|
||||||
|
Fixes issue [#3901](https://github.com/naturalcrit/homebrewery/issues/3901)
|
||||||
|
|
||||||
|
* [x] Update Ubuntu install instructions
|
||||||
|
|
||||||
|
Fixes issue [#1952](https://github.com/naturalcrit/homebrewery/issues/1952)
|
||||||
|
|
||||||
|
* [x] Add `:-:` `:-` `-:` syntax for paragraph alignment, similar to table column alignment; for example:
|
||||||
|
|
||||||
|
-: -: Right-aligned
|
||||||
|
|
||||||
|
:-: :-: Centered
|
||||||
|
|
||||||
|
* [x] Add `:-- 50% --:` syntax to allow setting table column widths by percentage; for example:
|
||||||
|
```
|
||||||
|
| Narrow | Wide |
|
||||||
|
|:- 10% -:|:-90%--:|
|
||||||
|
| Cell | Cell |
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
| Narrow | Wide |
|
||||||
|
|:- 10% -:|:-90%--:|
|
||||||
|
|Cell | Cell |
|
||||||
|
{exampleTable}
|
||||||
|
|
||||||
|
##### G-Ambatte
|
||||||
|
|
||||||
|
* [x] Fix crash when opening brew Properties tab
|
||||||
|
|
||||||
|
Fixes issue [#3927](https://github.com/naturalcrit/homebrewery/issues/3927)
|
||||||
|
|
||||||
|
* [x] Update error pages with steps to refresh credentials
|
||||||
|
|
||||||
|
Fixes issue [#3955](https://github.com/naturalcrit/homebrewery/issues/3955)
|
||||||
|
|
||||||
|
* [x] Add {{openSans :fas_rectangle_list: **NAVIGATION**}} menu to the viewer toolbar
|
||||||
|
|
||||||
|
##### calculuschild
|
||||||
|
|
||||||
|
* [x] Reduce display lag on large brews
|
||||||
|
|
||||||
|
##### Gazook89
|
||||||
|
|
||||||
|
* [x] Smarter detection of current page number
|
||||||
|
|
||||||
|
Fixes issue [#3824](https://github.com/naturalcrit/homebrewery/issues/3824)
|
||||||
|
|
||||||
|
##### All
|
||||||
|
* [x] Update dependencies and scripts
|
||||||
|
* [x] Refactor components and fix various errors
|
||||||
|
}}
|
||||||
|
|
||||||
|
\column
|
||||||
|
|
||||||
### Wednesday 11/27/2024 - v3.16.1
|
### Wednesday 11/27/2024 - v3.16.1
|
||||||
|
|
||||||
{{taskList
|
{{taskList
|
||||||
@@ -131,6 +217,8 @@ Fixes issue [#3744](https://github.com/naturalcrit/homebrewery/issues/3744)
|
|||||||
* [x] Multiple code refactors, cleanups, and security fixes
|
* [x] Multiple code refactors, cleanups, and security fixes
|
||||||
}}
|
}}
|
||||||
|
|
||||||
|
\page
|
||||||
|
|
||||||
### Saturday 10/12/2024 - v3.16.0
|
### Saturday 10/12/2024 - v3.16.0
|
||||||
|
|
||||||
{{taskList
|
{{taskList
|
||||||
@@ -2053,4 +2141,4 @@ Massive changelog incoming:
|
|||||||
|
|
||||||
* Added `phb.standalone.css` plus a build system for creating it
|
* Added `phb.standalone.css` plus a build system for creating it
|
||||||
* Added page numbers and footer text
|
* Added page numbers and footer text
|
||||||
* Page accent now flips each page
|
* Page accent now flips each page
|
||||||
@@ -6,10 +6,8 @@ function Dialog({ dismisskeys = [], closeText = 'Close', blocking = false, ...re
|
|||||||
const dialogRef = useRef(null);
|
const dialogRef = useRef(null);
|
||||||
|
|
||||||
useEffect(()=>{
|
useEffect(()=>{
|
||||||
if(dismisskeys.length !== 0) {
|
blocking ? dialogRef.current?.showModal() : dialogRef.current?.show();
|
||||||
blocking ? dialogRef.current?.showModal() : dialogRef.current?.show();
|
}, []);
|
||||||
}
|
|
||||||
}, [dialogRef.current, dismisskeys]);
|
|
||||||
|
|
||||||
const dismiss = ()=>{
|
const dismiss = ()=>{
|
||||||
dismisskeys.forEach((key)=>{
|
dismisskeys.forEach((key)=>{
|
||||||
|
|||||||
@@ -16,8 +16,10 @@ const Frame = require('react-frame-component').default;
|
|||||||
const dedent = require('dedent-tabs').default;
|
const dedent = require('dedent-tabs').default;
|
||||||
const { printCurrentBrew } = require('../../../shared/helpers.js');
|
const { printCurrentBrew } = require('../../../shared/helpers.js');
|
||||||
|
|
||||||
|
import HeaderNav from './headerNav/headerNav.jsx';
|
||||||
import { safeHTML } from './safeHTML.js';
|
import { safeHTML } from './safeHTML.js';
|
||||||
|
|
||||||
|
const PAGEBREAK_REGEX_V3 = /^(?=\\page(?: *{[^\n{}]*})?$)/m;
|
||||||
const PAGE_HEIGHT = 1056;
|
const PAGE_HEIGHT = 1056;
|
||||||
|
|
||||||
const INITIAL_CONTENT = dedent`
|
const INITIAL_CONTENT = dedent`
|
||||||
@@ -50,8 +52,8 @@ const BrewPage = (props)=>{
|
|||||||
props.onVisibilityChange(props.index + 1, true, false); // add page to array of visible pages.
|
props.onVisibilityChange(props.index + 1, true, false); // add page to array of visible pages.
|
||||||
else
|
else
|
||||||
props.onVisibilityChange(props.index + 1, false, false);
|
props.onVisibilityChange(props.index + 1, false, false);
|
||||||
}
|
});
|
||||||
)},
|
},
|
||||||
{ threshold: .3, rootMargin: '0px 0px 0px 0px' } // detect when >30% of page is within bounds.
|
{ threshold: .3, rootMargin: '0px 0px 0px 0px' } // detect when >30% of page is within bounds.
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -61,8 +63,8 @@ const BrewPage = (props)=>{
|
|||||||
entries.forEach((entry)=>{
|
entries.forEach((entry)=>{
|
||||||
if(entry.isIntersecting)
|
if(entry.isIntersecting)
|
||||||
props.onVisibilityChange(props.index + 1, true, true); // Set this page as the center page
|
props.onVisibilityChange(props.index + 1, true, true); // Set this page as the center page
|
||||||
}
|
});
|
||||||
)},
|
},
|
||||||
{ threshold: 0, rootMargin: '-50% 0px -50% 0px' } // Detect when the page is at the center
|
{ threshold: 0, rootMargin: '-50% 0px -50% 0px' } // Detect when the page is at the center
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -75,7 +77,7 @@ const BrewPage = (props)=>{
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return <div className={props.className} id={`p${props.index + 1}`} data-index={props.index} ref={pageRef} style={props.style}>
|
return <div className={props.className} id={`p${props.index + 1}`} data-index={props.index} ref={pageRef} style={props.style} {...props.attributes}>
|
||||||
<div className='columnWrapper' dangerouslySetInnerHTML={{ __html: cleanText }} />
|
<div className='columnWrapper' dangerouslySetInnerHTML={{ __html: cleanText }} />
|
||||||
</div>;
|
</div>;
|
||||||
};
|
};
|
||||||
@@ -115,12 +117,15 @@ const BrewRenderer = (props)=>{
|
|||||||
pageShadows : true
|
pageShadows : true
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const [headerState, setHeaderState] = useState(false);
|
||||||
|
|
||||||
const mainRef = useRef(null);
|
const mainRef = useRef(null);
|
||||||
|
const pagesRef = useRef(null);
|
||||||
|
|
||||||
if(props.renderer == 'legacy') {
|
if(props.renderer == 'legacy') {
|
||||||
rawPages = props.text.split('\\page');
|
rawPages = props.text.split('\\page');
|
||||||
} else {
|
} else {
|
||||||
rawPages = props.text.split(/^\\page$/gm);
|
rawPages = props.text.split(PAGEBREAK_REGEX_V3);
|
||||||
}
|
}
|
||||||
|
|
||||||
const handlePageVisibilityChange = (pageNum, isVisible, isCenter)=>{
|
const handlePageVisibilityChange = (pageNum, isVisible, isCenter)=>{
|
||||||
@@ -167,20 +172,34 @@ const BrewRenderer = (props)=>{
|
|||||||
|
|
||||||
const renderPage = (pageText, index)=>{
|
const renderPage = (pageText, index)=>{
|
||||||
|
|
||||||
const styles = {
|
let styles = {
|
||||||
...(!displayOptions.pageShadows ? { boxShadow: 'none' } : {})
|
...(!displayOptions.pageShadows ? { boxShadow: 'none' } : {})
|
||||||
// Add more conditions as needed
|
// Add more conditions as needed
|
||||||
};
|
};
|
||||||
|
let classes = 'page';
|
||||||
|
let attributes = {};
|
||||||
|
|
||||||
if(props.renderer == 'legacy') {
|
if(props.renderer == 'legacy') {
|
||||||
const html = MarkdownLegacy.render(pageText);
|
const html = MarkdownLegacy.render(pageText);
|
||||||
|
|
||||||
return <BrewPage className='page phb' index={index} key={index} contents={html} style={styles} onVisibilityChange={handlePageVisibilityChange} />;
|
return <BrewPage className='page phb' index={index} key={index} contents={html} style={styles} onVisibilityChange={handlePageVisibilityChange} />;
|
||||||
} else {
|
} else {
|
||||||
|
if(pageText.startsWith('\\page')) {
|
||||||
|
const firstLineTokens = Markdown.marked.lexer(pageText.split('\n', 1)[0])[0].tokens;
|
||||||
|
const injectedTags = firstLineTokens.find((obj)=>obj.injectedTags !== undefined)?.injectedTags;
|
||||||
|
if(injectedTags) {
|
||||||
|
styles = { ...styles, ...injectedTags.styles };
|
||||||
|
styles = _.mapKeys(styles, (v, k) => k.startsWith('--') ? k : _.camelCase(k)); // Convert CSS to camelCase for React
|
||||||
|
classes = [classes, injectedTags.classes].join(' ').trim();
|
||||||
|
attributes = injectedTags.attributes;
|
||||||
|
}
|
||||||
|
pageText = pageText.includes('\n') ? pageText.substring(pageText.indexOf('\n') + 1) : ''; // Remove the \page line
|
||||||
|
}
|
||||||
|
|
||||||
pageText += `\n\n \n\\column\n `; //Artificial column break at page end to emulate column-fill:auto (until `wide` is used, when column-fill:balance will reappear)
|
pageText += `\n\n \n\\column\n `; //Artificial column break at page end to emulate column-fill:auto (until `wide` is used, when column-fill:balance will reappear)
|
||||||
const html = Markdown.render(pageText, index);
|
const html = Markdown.render(pageText, index);
|
||||||
|
|
||||||
return <BrewPage className='page' index={index} key={index} contents={html} style={styles} onVisibilityChange={handlePageVisibilityChange} />;
|
return <BrewPage className={classes} index={index} key={index} contents={html} style={styles} attributes={attributes} onVisibilityChange={handlePageVisibilityChange} />;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -287,7 +306,7 @@ const BrewRenderer = (props)=>{
|
|||||||
<NotificationPopup />
|
<NotificationPopup />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ToolBar displayOptions={displayOptions} onDisplayOptionsChange={handleDisplayOptionsChange} visiblePages={state.visiblePages.length > 0 ? state.visiblePages : [state.centerPage]} totalPages={rawPages.length}/>
|
<ToolBar displayOptions={displayOptions} onDisplayOptionsChange={handleDisplayOptionsChange} visiblePages={state.visiblePages.length > 0 ? state.visiblePages : [state.centerPage]} totalPages={rawPages.length} headerState={headerState} setHeaderState={setHeaderState}/>
|
||||||
|
|
||||||
{/*render in iFrame so broken code doesn't crash the site.*/}
|
{/*render in iFrame so broken code doesn't crash the site.*/}
|
||||||
<Frame id='BrewRenderer' initialContent={INITIAL_CONTENT}
|
<Frame id='BrewRenderer' initialContent={INITIAL_CONTENT}
|
||||||
@@ -306,12 +325,13 @@ const BrewRenderer = (props)=>{
|
|||||||
&&
|
&&
|
||||||
<>
|
<>
|
||||||
{renderedStyle}
|
{renderedStyle}
|
||||||
<div className={`pages ${displayOptions.startOnRight ? 'recto' : 'verso'} ${displayOptions.spread}`} lang={`${props.lang || 'en'}`} style={pagesStyle}>
|
<div className={`pages ${displayOptions.startOnRight ? 'recto' : 'verso'} ${displayOptions.spread}`} lang={`${props.lang || 'en'}`} style={pagesStyle} ref={pagesRef}>
|
||||||
{renderedPages}
|
{renderedPages}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
{headerState ? <HeaderNav ref={pagesRef} /> : <></>}
|
||||||
</Frame>
|
</Frame>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -70,6 +70,7 @@
|
|||||||
|
|
||||||
.pane { position : relative; }
|
.pane { position : relative; }
|
||||||
|
|
||||||
|
|
||||||
@media print {
|
@media print {
|
||||||
.toolBar { display : none; }
|
.toolBar { display : none; }
|
||||||
.brewRenderer {
|
.brewRenderer {
|
||||||
@@ -82,4 +83,7 @@
|
|||||||
& > .page { box-shadow : unset; }
|
& > .page { box-shadow : unset; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.headerNav {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,75 +1,53 @@
|
|||||||
require('./errorBar.less');
|
require('./errorBar.less');
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const createClass = require('create-react-class');
|
|
||||||
const _ = require('lodash');
|
|
||||||
|
|
||||||
const ErrorBar = createClass({
|
import Dialog from '../../../components/dialog.jsx';
|
||||||
displayName : 'ErrorBar',
|
|
||||||
getDefaultProps : function() {
|
|
||||||
return {
|
|
||||||
errors : []
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
hasOpenError : false,
|
const DISMISS_BUTTON = <i className='fas fa-times dismiss' />;
|
||||||
hasCloseError : false,
|
|
||||||
hasMatchError : false,
|
|
||||||
|
|
||||||
renderErrors : function(){
|
const ErrorBar = (props)=>{
|
||||||
this.hasOpenError = false;
|
if(!props.errors.length) return null;
|
||||||
this.hasCloseError = false;
|
let hasOpenError = false, hasCloseError = false, hasMatchError = false;
|
||||||
this.hasMatchError = false;
|
|
||||||
|
|
||||||
|
props.errors.map((err)=>{
|
||||||
|
if(err.id === 'OPEN') hasOpenError = true;
|
||||||
|
if(err.id === 'CLOSE') hasCloseError = true;
|
||||||
|
if(err.id === 'MISMATCH') hasMatchError = true;
|
||||||
|
});
|
||||||
|
|
||||||
const errors = _.map(this.props.errors, (err, idx)=>{
|
const renderErrors = ()=>(
|
||||||
if(err.id == 'OPEN') this.hasOpenError = true;
|
<ul>
|
||||||
if(err.id == 'CLOSE') this.hasCloseError = true;
|
{props.errors.map((err, idx)=>{
|
||||||
if(err.id == 'MISMATCH') this.hasMatchError = true;
|
return <li key={idx}>
|
||||||
return <li key={idx}>
|
Line {err.line} : {err.text}, '{err.type}' tag
|
||||||
Line {err.line} : {err.text}, '{err.type}' tag
|
</li>;
|
||||||
</li>;
|
})}
|
||||||
});
|
</ul>
|
||||||
|
);
|
||||||
|
|
||||||
return <ul>{errors}</ul>;
|
const renderProtip = ()=>(
|
||||||
},
|
<div className='protips'>
|
||||||
|
|
||||||
renderProtip : function(){
|
|
||||||
const msg = [];
|
|
||||||
if(this.hasOpenError){
|
|
||||||
msg.push(<div>
|
|
||||||
An unmatched opening tag means there's an opened tag that isn't closed. You need to close your tags, like this {'</div>'}. Make sure to match types!
|
|
||||||
</div>);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(this.hasCloseError){
|
|
||||||
msg.push(<div>
|
|
||||||
An unmatched closing tag means you closed a tag without opening it. Either remove it, or check to where you think you opened it.
|
|
||||||
</div>);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(this.hasMatchError){
|
|
||||||
msg.push(<div>
|
|
||||||
A type mismatch means you closed a tag, but the last open tag was a different type.
|
|
||||||
</div>);
|
|
||||||
}
|
|
||||||
return <div className='protips'>
|
|
||||||
<h4>Protips!</h4>
|
<h4>Protips!</h4>
|
||||||
{msg}
|
{hasOpenError && <div>Unmatched opening tag. Close your tags, like this {'</div>'}. Match types!</div>}
|
||||||
</div>;
|
{hasCloseError && <div>Unmatched closing tag. Either remove it or check where it was opened.</div>}
|
||||||
},
|
{hasMatchError && <div>Type mismatch. Closed a tag with a different type.</div>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
render : function(){
|
return (
|
||||||
if(!this.props.errors.length) return null;
|
<Dialog className='errorBar' closeText={DISMISS_BUTTON} >
|
||||||
|
<div>
|
||||||
return <div className='errorBar'>
|
<i className='fas fa-exclamation-triangle' />
|
||||||
<i className='fas fa-exclamation-triangle' />
|
<h2> There are HTML errors in your markup</h2>
|
||||||
<h3> There are HTML errors in your markup</h3>
|
<small>
|
||||||
<small>If these aren't fixed your brew will not render properly when you print it to PDF or share it</small>
|
If these aren't fixed your brew will not render properly when you print it to PDF or share it
|
||||||
{this.renderErrors()}
|
</small>
|
||||||
|
{renderErrors()}
|
||||||
|
</div>
|
||||||
<hr />
|
<hr />
|
||||||
{this.renderProtip()}
|
{renderProtip()}
|
||||||
</div>;
|
</Dialog>
|
||||||
}
|
);
|
||||||
});
|
};
|
||||||
|
|
||||||
module.exports = ErrorBar;
|
module.exports = ErrorBar;
|
||||||
|
|||||||
@@ -1,60 +1,58 @@
|
|||||||
|
|
||||||
.errorBar{
|
.errorBar {
|
||||||
position : absolute;
|
position : absolute;
|
||||||
z-index : 10000;
|
top : 32px;
|
||||||
box-sizing : border-box;
|
z-index : 1;
|
||||||
width : 100%;
|
width : 100%;
|
||||||
margin-right : 13px;
|
|
||||||
padding : 20px;
|
|
||||||
padding-bottom : 10px;
|
|
||||||
padding-left : 100px;
|
|
||||||
background-color : @red;
|
|
||||||
color : white;
|
color : white;
|
||||||
i{
|
background-color : @red;
|
||||||
position : absolute;
|
border : unset;
|
||||||
left : 30px;
|
|
||||||
opacity : 0.8;
|
div {
|
||||||
font-size : 3em;
|
> i {
|
||||||
}
|
float : left;
|
||||||
h3{
|
margin-right : 10px;
|
||||||
font-size : 1.1em;
|
margin-bottom : 20px;
|
||||||
font-weight : 800;
|
font-size : 3em;
|
||||||
}
|
opacity : 0.8;
|
||||||
ul{
|
}
|
||||||
margin-top : 15px;
|
h2 { font-weight : 800; }
|
||||||
font-size : 0.8em;
|
ul {
|
||||||
list-style-position : inside;
|
margin-top : 15px;
|
||||||
list-style-type : disc;
|
font-size : 0.8em;
|
||||||
li{
|
list-style-position : inside;
|
||||||
line-height : 1.6em;
|
list-style-type : disc;
|
||||||
|
li { line-height : 1.6em; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
hr{
|
hr {
|
||||||
box-sizing : border-box;
|
|
||||||
height : 2px;
|
height : 2px;
|
||||||
width : 150%;
|
|
||||||
margin-top : 25px;
|
margin-top : 25px;
|
||||||
margin-bottom : 15px;
|
margin-bottom : 15px;
|
||||||
margin-left : -100px;
|
|
||||||
background-color : darken(@red, 8%);
|
background-color : darken(@red, 8%);
|
||||||
border : none;
|
border : none;
|
||||||
}
|
}
|
||||||
small{
|
small {
|
||||||
font-size: 0.6em;
|
font-size : 0.6em;
|
||||||
opacity: 0.7;
|
opacity : 0.7;
|
||||||
}
|
}
|
||||||
.protips{
|
.protips {
|
||||||
margin-left : -80px;
|
font-size : 0.6em;
|
||||||
font-size : 0.6em;
|
line-height : 1.2em;
|
||||||
&>div{
|
h4 {
|
||||||
margin-bottom : 10px;
|
|
||||||
line-height : 1.2em;
|
|
||||||
}
|
|
||||||
h4{
|
|
||||||
opacity : 0.8;
|
|
||||||
font-weight : 800;
|
font-weight : 800;
|
||||||
line-height : 1.5em;
|
line-height : 1.5em;
|
||||||
text-transform : uppercase;
|
text-transform : uppercase;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
button.dismiss {
|
||||||
|
position : absolute;
|
||||||
|
top : 20px;
|
||||||
|
right : 30px;
|
||||||
|
padding : unset;
|
||||||
|
font-size : 40px;
|
||||||
|
background-color : transparent;
|
||||||
|
opacity : 0.6;
|
||||||
|
&:hover { opacity : 1; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
106
client/homebrew/brewRenderer/headerNav/headerNav.jsx
Normal file
106
client/homebrew/brewRenderer/headerNav/headerNav.jsx
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
require('./headerNav.less');
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
import * as _ from 'lodash';
|
||||||
|
|
||||||
|
|
||||||
|
const MAX_TEXT_LENGTH = 40;
|
||||||
|
|
||||||
|
const HeaderNav = React.forwardRef(({}, pagesRef)=>{
|
||||||
|
|
||||||
|
const renderHeaderLinks = ()=>{
|
||||||
|
if(!pagesRef.current) return;
|
||||||
|
|
||||||
|
const selector = [
|
||||||
|
'.pages > .page', // All page elements, which by definition have IDs
|
||||||
|
'.page:not(:has(.toc)) > [id]', // All direct children of non-ToC .page with an ID (Legacy)
|
||||||
|
'.page:not(:has(.toc)) > .columnWrapper > [id]', // All direct children of non-ToC .page > .columnWrapper with an ID (V3)
|
||||||
|
'.page:not(:has(.toc)) h2', // All non-ToC H2 titles, like Monster frame titles
|
||||||
|
];
|
||||||
|
const elements = pagesRef.current.querySelectorAll(selector.join(','));
|
||||||
|
if(!elements) return;
|
||||||
|
const navList = [];
|
||||||
|
|
||||||
|
// navList is a list of objects which have the following structure:
|
||||||
|
// {
|
||||||
|
// depth : how deeply indented the item should be
|
||||||
|
// text : the text to display in the nav link
|
||||||
|
// link : the hyperlink to navigate to when clicked
|
||||||
|
// className : [optional] the class to apply to the nav link for styling
|
||||||
|
// }
|
||||||
|
|
||||||
|
elements.forEach((el)=>{
|
||||||
|
if(el.className.match(/\bpage\b/)) {
|
||||||
|
let text = `Page ${el.id.slice(1)}`; // The ID of a page *should* always be equal to `p` followed by the page number
|
||||||
|
if(el.querySelector('.toc')){ // If the page contains a table of contents, add "- Contents" to the display text
|
||||||
|
text += ' - Contents';
|
||||||
|
};
|
||||||
|
navList.push({
|
||||||
|
depth : 0, // Pages are always at the least indented level
|
||||||
|
text : text,
|
||||||
|
link : el.id,
|
||||||
|
className : 'pageLink'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(el.localName.match(/^h[1-6]/)){ // Header elements H1 through H6
|
||||||
|
navList.push({
|
||||||
|
depth : el.localName[1], // Depth is set by the header level
|
||||||
|
text : el.textContent, // Use `textContent` because `innerText` is affected by rendering, e.g. 'content-visibility: auto'
|
||||||
|
link : el.id
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
navList.push({
|
||||||
|
depth : 7, // All unmatched elements with IDs are set to the maximum depth (7)
|
||||||
|
text : el.textContent, // Use `textContent` because `innerText` is affected by rendering, e.g. 'content-visibility: auto'
|
||||||
|
link : el.id
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return _.map(navList, (navItem, index)=>{
|
||||||
|
return <HeaderNavItem {...navItem} key={index} />;
|
||||||
|
});
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
return <nav className='headerNav'>
|
||||||
|
<ul>
|
||||||
|
{renderHeaderLinks()}
|
||||||
|
</ul>
|
||||||
|
</nav>;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const HeaderNavItem = ({ link, text, depth, className })=>{
|
||||||
|
|
||||||
|
const trimString = (text, prefixLength = 0)=>{
|
||||||
|
// Sanity check nav link strings
|
||||||
|
let output = text;
|
||||||
|
|
||||||
|
// If the string has a line break, only use the first line
|
||||||
|
if(text.indexOf('\n')){
|
||||||
|
output = text.split('\n')[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trim unecessary spaces from string
|
||||||
|
output = output.trim();
|
||||||
|
|
||||||
|
// Reduce excessively long strings
|
||||||
|
const maxLength = MAX_TEXT_LENGTH - prefixLength;
|
||||||
|
if(output.length > maxLength){
|
||||||
|
return `${output.slice(0, maxLength).trim()}...`;
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
};
|
||||||
|
|
||||||
|
if(!link || !text) return;
|
||||||
|
|
||||||
|
return <li>
|
||||||
|
<a href={`#${link}`} target='_self' className={`depth-${depth} ${className ?? ''}`}>
|
||||||
|
{trimString(text, depth)}
|
||||||
|
</a>
|
||||||
|
</li>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HeaderNav;
|
||||||
47
client/homebrew/brewRenderer/headerNav/headerNav.less
Normal file
47
client/homebrew/brewRenderer/headerNav/headerNav.less
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
.headerNav {
|
||||||
|
position: fixed;
|
||||||
|
top: 32px;
|
||||||
|
left: 0px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
background-color: #ccc;
|
||||||
|
border-radius: 5px;
|
||||||
|
max-height: calc(100vh - 32px);
|
||||||
|
max-width: 40vw;
|
||||||
|
overflow-y: auto;
|
||||||
|
&.active {
|
||||||
|
padding-bottom: 10px;
|
||||||
|
.navIcon {
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navIcon {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
li {
|
||||||
|
list-style-type: none;
|
||||||
|
a {
|
||||||
|
display: inline-block;
|
||||||
|
width: 100%;
|
||||||
|
font-family: 'Open Sans';
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 2px;
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
&.pageLink {
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
@depths: 1,2,3,4,5,6,7;
|
||||||
|
|
||||||
|
each(@depths, {
|
||||||
|
&.depth-@{value} {
|
||||||
|
padding-left: ((@value - 1) * 0.5em);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -49,6 +49,7 @@ const NotificationPopup = ()=>{
|
|||||||
));
|
));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if(!notifications.length) return;
|
||||||
return <Dialog className='notificationPopup' dismisskeys={dissmissKeyList} closeText={DISMISS_BUTTON} >
|
return <Dialog className='notificationPopup' dismisskeys={dissmissKeyList} closeText={DISMISS_BUTTON} >
|
||||||
<div className='header'>
|
<div className='header'>
|
||||||
<i className='fas fa-info-circle info'></i>
|
<i className='fas fa-info-circle info'></i>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { Anchored, AnchoredBox, AnchoredTrigger } from '../../../components/Anch
|
|||||||
const MAX_ZOOM = 300;
|
const MAX_ZOOM = 300;
|
||||||
const MIN_ZOOM = 10;
|
const MIN_ZOOM = 10;
|
||||||
|
|
||||||
const ToolBar = ({ displayOptions, onDisplayOptionsChange, visiblePages, totalPages })=>{
|
const ToolBar = ({ displayOptions, onDisplayOptionsChange, visiblePages, totalPages, headerState, setHeaderState })=>{
|
||||||
|
|
||||||
const [pageNum, setPageNum] = useState(1);
|
const [pageNum, setPageNum] = useState(1);
|
||||||
const [toolsVisible, setToolsVisible] = useState(true);
|
const [toolsVisible, setToolsVisible] = useState(true);
|
||||||
@@ -62,7 +62,7 @@ const ToolBar = ({ displayOptions, onDisplayOptionsChange, visiblePages, totalPa
|
|||||||
// find the page with the largest single dim (height or width) so that zoom can be adapted to fit it.
|
// find the page with the largest single dim (height or width) so that zoom can be adapted to fit it.
|
||||||
if(displayOptions.spread === 'facing')
|
if(displayOptions.spread === 'facing')
|
||||||
minDimRatio = [...pages].reduce((minRatio, page)=>Math.min(minRatio, iframeWidth / page.offsetWidth / 2), Infinity); // if 'facing' spread, fit two pages in view
|
minDimRatio = [...pages].reduce((minRatio, page)=>Math.min(minRatio, iframeWidth / page.offsetWidth / 2), Infinity); // if 'facing' spread, fit two pages in view
|
||||||
else
|
else
|
||||||
minDimRatio = [...pages].reduce((minRatio, page)=>Math.min(minRatio, iframeWidth / page.offsetWidth, iframeHeight / page.offsetHeight), Infinity);
|
minDimRatio = [...pages].reduce((minRatio, page)=>Math.min(minRatio, iframeWidth / page.offsetWidth, iframeHeight / page.offsetHeight), Infinity);
|
||||||
|
|
||||||
desiredZoom = minDimRatio * 100;
|
desiredZoom = minDimRatio * 100;
|
||||||
@@ -76,7 +76,10 @@ const ToolBar = ({ displayOptions, onDisplayOptionsChange, visiblePages, totalPa
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div id='preview-toolbar' className={`toolBar ${toolsVisible ? 'visible' : 'hidden'}`} role='toolbar'>
|
<div id='preview-toolbar' className={`toolBar ${toolsVisible ? 'visible' : 'hidden'}`} role='toolbar'>
|
||||||
<button className='toggleButton' title={`${toolsVisible ? 'Hide' : 'Show'} Preview Toolbar`} onClick={()=>{setToolsVisible(!toolsVisible);}}><i className='fas fa-glasses' /></button>
|
<div className='toggleButton'>
|
||||||
|
<button title={`${toolsVisible ? 'Hide' : 'Show'} Preview Toolbar`} onClick={()=>{setToolsVisible(!toolsVisible);}}><i className='fas fa-glasses' /></button>
|
||||||
|
<button title={`${headerState ? 'Hide' : 'Show'} Header Navigation`} onClick={()=>{setHeaderState(!headerState);}}><i className='fas fa-rectangle-list' /></button>
|
||||||
|
</div>
|
||||||
{/*v=====----------------------< Zoom Controls >---------------------=====v*/}
|
{/*v=====----------------------< Zoom Controls >---------------------=====v*/}
|
||||||
<div className='group' role='group' aria-label='Zoom' aria-hidden={!toolsVisible}>
|
<div className='group' role='group' aria-label='Zoom' aria-hidden={!toolsVisible}>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -166,7 +166,7 @@
|
|||||||
|
|
||||||
&.hidden {
|
&.hidden {
|
||||||
flex-wrap : nowrap;
|
flex-wrap : nowrap;
|
||||||
width : 32px;
|
width : 92px;
|
||||||
overflow : hidden;
|
overflow : hidden;
|
||||||
background-color : unset;
|
background-color : unset;
|
||||||
opacity : 0.5;
|
opacity : 0.5;
|
||||||
@@ -178,10 +178,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
button.toggleButton {
|
.toggleButton {
|
||||||
position : absolute;
|
position : absolute;
|
||||||
left : 0;
|
left : 0;
|
||||||
z-index : 5;
|
z-index : 5;
|
||||||
width : 32px;
|
width : 32px;
|
||||||
min-width : unset;
|
min-width : unset;
|
||||||
|
height : 100%;
|
||||||
|
display : flex;
|
||||||
}
|
}
|
||||||
@@ -12,7 +12,8 @@ const MetadataEditor = require('./metadataEditor/metadataEditor.jsx');
|
|||||||
|
|
||||||
const EDITOR_THEME_KEY = 'HOMEBREWERY-EDITOR-THEME';
|
const EDITOR_THEME_KEY = 'HOMEBREWERY-EDITOR-THEME';
|
||||||
|
|
||||||
const SNIPPETBAR_HEIGHT = 25;
|
const PAGEBREAK_REGEX_V3 = /^(?=\\page(?: *{[^\n{}]*})?$)/m;
|
||||||
|
const SNIPPETBAR_HEIGHT = 25;
|
||||||
const DEFAULT_STYLE_TEXT = dedent`
|
const DEFAULT_STYLE_TEXT = dedent`
|
||||||
/*=======--- Example CSS styling ---=======*/
|
/*=======--- Example CSS styling ---=======*/
|
||||||
/* Any CSS here will apply to your document! */
|
/* Any CSS here will apply to your document! */
|
||||||
@@ -126,15 +127,15 @@ const Editor = createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
updateCurrentCursorPage : function(cursor) {
|
updateCurrentCursorPage : function(cursor) {
|
||||||
const lines = this.props.brew.text.split('\n').slice(0, cursor.line + 1);
|
const lines = this.props.brew.text.split('\n').slice(1, cursor.line + 1);
|
||||||
const pageRegex = this.props.brew.renderer == 'V3' ? /^\\page$/ : /\\page/;
|
const pageRegex = this.props.brew.renderer == 'V3' ? PAGEBREAK_REGEX_V3 : /\\page/;
|
||||||
const currentPage = lines.reduce((count, line)=>count + (pageRegex.test(line) ? 1 : 0), 1);
|
const currentPage = lines.reduce((count, line)=>count + (pageRegex.test(line) ? 1 : 0), 1);
|
||||||
this.props.onCursorPageChange(currentPage);
|
this.props.onCursorPageChange(currentPage);
|
||||||
},
|
},
|
||||||
|
|
||||||
updateCurrentViewPage : function(topScrollLine) {
|
updateCurrentViewPage : function(topScrollLine) {
|
||||||
const lines = this.props.brew.text.split('\n').slice(0, topScrollLine + 1);
|
const lines = this.props.brew.text.split('\n').slice(1, topScrollLine + 1);
|
||||||
const pageRegex = this.props.brew.renderer == 'V3' ? /^\\page$/ : /\\page/;
|
const pageRegex = this.props.brew.renderer == 'V3' ? PAGEBREAK_REGEX_V3 : /\\page/;
|
||||||
const currentPage = lines.reduce((count, line)=>count + (pageRegex.test(line) ? 1 : 0), 1);
|
const currentPage = lines.reduce((count, line)=>count + (pageRegex.test(line) ? 1 : 0), 1);
|
||||||
this.props.onViewPageChange(currentPage);
|
this.props.onViewPageChange(currentPage);
|
||||||
},
|
},
|
||||||
@@ -174,7 +175,7 @@ const Editor = createClass({
|
|||||||
|
|
||||||
for (let i=customHighlights.length - 1;i>=0;i--) customHighlights[i].clear();
|
for (let i=customHighlights.length - 1;i>=0;i--) customHighlights[i].clear();
|
||||||
|
|
||||||
let editorPageCount = 2; // start page count from page 2
|
let editorPageCount = 1; // start page count from page 1
|
||||||
|
|
||||||
_.forEach(this.props.brew.text.split('\n'), (line, lineNumber)=>{
|
_.forEach(this.props.brew.text.split('\n'), (line, lineNumber)=>{
|
||||||
|
|
||||||
@@ -190,7 +191,10 @@ const Editor = createClass({
|
|||||||
|
|
||||||
// Styling for \page breaks
|
// Styling for \page breaks
|
||||||
if((this.props.renderer == 'legacy' && line.includes('\\page')) ||
|
if((this.props.renderer == 'legacy' && line.includes('\\page')) ||
|
||||||
(this.props.renderer == 'V3' && line.match(/^\\page$/))) {
|
(this.props.renderer == 'V3' && line.match(PAGEBREAK_REGEX_V3))) {
|
||||||
|
|
||||||
|
if(lineNumber > 0) // Since \page is optional on first line of document,
|
||||||
|
editorPageCount += 1; // don't use it to increment page count; stay at 1
|
||||||
|
|
||||||
// add back the original class 'background' but also add the new class '.pageline'
|
// add back the original class 'background' but also add the new class '.pageline'
|
||||||
codeMirror.addLineClass(lineNumber, 'background', 'pageLine');
|
codeMirror.addLineClass(lineNumber, 'background', 'pageLine');
|
||||||
@@ -199,8 +203,6 @@ const Editor = createClass({
|
|||||||
textContent : editorPageCount
|
textContent : editorPageCount
|
||||||
});
|
});
|
||||||
codeMirror.setBookmark({ line: lineNumber, ch: line.length }, pageCountElement);
|
codeMirror.setBookmark({ line: lineNumber, ch: line.length }, pageCountElement);
|
||||||
|
|
||||||
editorPageCount += 1;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// New Codemirror styling for V3 renderer
|
// New Codemirror styling for V3 renderer
|
||||||
@@ -358,7 +360,7 @@ const Editor = createClass({
|
|||||||
if(!this.isText() || isJumping)
|
if(!this.isText() || isJumping)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const textSplit = this.props.renderer == 'V3' ? /^\\page$/gm : /\\page/;
|
const textSplit = this.props.renderer == 'V3' ? PAGEBREAK_REGEX_V3 : /\\page/;
|
||||||
const textString = this.props.brew.text.split(textSplit).slice(0, targetPage-1).join(textSplit);
|
const textString = this.props.brew.text.split(textSplit).slice(0, targetPage-1).join(textSplit);
|
||||||
const targetLine = textString.match('\n') ? textString.split('\n').length - 1 : -1;
|
const targetLine = textString.match('\n') ? textString.split('\n').length - 1 : -1;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
require('./brewItem.less');
|
require('./brewItem.less');
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const createClass = require('create-react-class');
|
const { useCallback } = React;
|
||||||
const moment = require('moment');
|
const moment = require('moment');
|
||||||
import request from '../../../../utils/request-middleware.js';
|
import request from '../../../../utils/request-middleware.js';
|
||||||
|
|
||||||
@@ -8,176 +8,172 @@ const googleDriveIcon = require('../../../../googleDrive.svg');
|
|||||||
const homebreweryIcon = require('../../../../thumbnail.png');
|
const homebreweryIcon = require('../../../../thumbnail.png');
|
||||||
const dedent = require('dedent-tabs').default;
|
const dedent = require('dedent-tabs').default;
|
||||||
|
|
||||||
const BrewItem = createClass({
|
const BrewItem = ({
|
||||||
displayName : 'BrewItem',
|
brew = {
|
||||||
getDefaultProps : function() {
|
title : '',
|
||||||
return {
|
description : '',
|
||||||
brew : {
|
authors : [],
|
||||||
title : '',
|
stubbed : true,
|
||||||
description : '',
|
|
||||||
authors : [],
|
|
||||||
stubbed : true
|
|
||||||
},
|
|
||||||
updateListFilter : ()=>{},
|
|
||||||
reportError : ()=>{},
|
|
||||||
renderStorage : true
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
|
updateListFilter = ()=>{},
|
||||||
|
reportError = ()=>{},
|
||||||
|
renderStorage = true,
|
||||||
|
})=>{
|
||||||
|
|
||||||
deleteBrew : function(){
|
const deleteBrew = useCallback(()=>{
|
||||||
if(this.props.brew.authors.length <= 1){
|
if(brew.authors.length <= 1) {
|
||||||
if(!confirm('Are you sure you want to delete this brew? Because you are the only owner of this brew, the document will be deleted permanently.')) return;
|
if(!window.confirm('Are you sure you want to delete this brew? Because you are the only owner of this brew, the document will be deleted permanently.')) return;
|
||||||
if(!confirm('Are you REALLY sure? You will not be able to recover the document.')) return;
|
if(!window.confirm('Are you REALLY sure? You will not be able to recover the document.')) return;
|
||||||
} else {
|
} else {
|
||||||
if(!confirm('Are you sure you want to remove this brew from your collection? This will remove you as an editor, but other owners will still be able to access the document.')) return;
|
if(!window.confirm('Are you sure you want to remove this brew from your collection? This will remove you as an editor, but other owners will still be able to access the document.')) return;
|
||||||
if(!confirm('Are you REALLY sure? You will lose editor access to this document.')) return;
|
if(!window.confirm('Are you REALLY sure? You will lose editor access to this document.')) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
request.delete(`/api/${this.props.brew.googleId ?? ''}${this.props.brew.editId}`)
|
request.delete(`/api/${brew.googleId ?? ''}${brew.editId}`).send().end((err, res)=>{
|
||||||
.send()
|
if (err) reportError(err); else window.location.reload();
|
||||||
.end((err, res)=>{
|
});
|
||||||
if(err) {
|
}, [brew, reportError]);
|
||||||
this.props.reportError(err);
|
|
||||||
} else {
|
|
||||||
location.reload();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
updateFilter : function(type, term){
|
const updateFilter = useCallback((type, term)=> updateListFilter(type, term), [updateListFilter]);
|
||||||
this.props.updateListFilter(type, term);
|
|
||||||
},
|
|
||||||
|
|
||||||
renderDeleteBrewLink : function(){
|
const renderDeleteBrewLink = ()=>{
|
||||||
if(!this.props.brew.editId) return;
|
if(!brew.editId) return null;
|
||||||
|
|
||||||
return <a className='deleteLink' onClick={this.deleteBrew}>
|
return (
|
||||||
<i className='fas fa-trash-alt' title='Delete' />
|
<a className='deleteLink' onClick={deleteBrew}>
|
||||||
</a>;
|
<i className='fas fa-trash-alt' title='Delete' />
|
||||||
},
|
</a>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
renderEditLink : function(){
|
const renderEditLink = ()=>{
|
||||||
if(!this.props.brew.editId) return;
|
if(!brew.editId) return null;
|
||||||
|
|
||||||
let editLink = this.props.brew.editId;
|
let editLink = brew.editId;
|
||||||
if(this.props.brew.googleId && !this.props.brew.stubbed) {
|
if(brew.googleId && !brew.stubbed) editLink = brew.googleId + editLink;
|
||||||
editLink = this.props.brew.googleId + editLink;
|
|
||||||
|
return (
|
||||||
|
<a className='editLink' href={`/edit/${editLink}`} target='_blank' rel='noopener noreferrer'>
|
||||||
|
<i className='fas fa-pencil-alt' title='Edit' />
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderShareLink = ()=>{
|
||||||
|
if(!brew.shareId) return null;
|
||||||
|
|
||||||
|
let shareLink = brew.shareId;
|
||||||
|
if(brew.googleId && !brew.stubbed) {
|
||||||
|
shareLink = brew.googleId + shareLink;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <a className='editLink' href={`/edit/${editLink}`} target='_blank' rel='noopener noreferrer'>
|
return (
|
||||||
<i className='fas fa-pencil-alt' title='Edit' />
|
<a className='shareLink' href={`/share/${shareLink}`} target='_blank' rel='noopener noreferrer'>
|
||||||
</a>;
|
<i className='fas fa-share-alt' title='Share' />
|
||||||
},
|
</a>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
renderShareLink : function(){
|
const renderDownloadLink = ()=>{
|
||||||
if(!this.props.brew.shareId) return;
|
if(!brew.shareId) return null;
|
||||||
|
|
||||||
let shareLink = this.props.brew.shareId;
|
let shareLink = brew.shareId;
|
||||||
if(this.props.brew.googleId && !this.props.brew.stubbed) {
|
if(brew.googleId && !brew.stubbed) {
|
||||||
shareLink = this.props.brew.googleId + shareLink;
|
shareLink = brew.googleId + shareLink;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <a className='shareLink' href={`/share/${shareLink}`} target='_blank' rel='noopener noreferrer'>
|
return (
|
||||||
<i className='fas fa-share-alt' title='Share' />
|
<a className='downloadLink' href={`/download/${shareLink}`}>
|
||||||
</a>;
|
<i className='fas fa-download' title='Download' />
|
||||||
},
|
</a>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
renderDownloadLink : function(){
|
const renderStorageIcon = ()=>{
|
||||||
if(!this.props.brew.shareId) return;
|
if(!renderStorage) return null;
|
||||||
|
if(brew.googleId) {
|
||||||
let shareLink = this.props.brew.shareId;
|
return (
|
||||||
if(this.props.brew.googleId && !this.props.brew.stubbed) {
|
<span title={brew.webViewLink ? 'Your Google Drive Storage' : 'Another User\'s Google Drive Storage'}>
|
||||||
shareLink = this.props.brew.googleId + shareLink;
|
<a href={brew.webViewLink} target='_blank'>
|
||||||
|
<img className='googleDriveIcon' src={googleDriveIcon} alt='googleDriveIcon' />
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <a className='downloadLink' href={`/download/${shareLink}`}>
|
return (
|
||||||
<i className='fas fa-download' title='Download' />
|
<span title='Homebrewery Storage'>
|
||||||
</a>;
|
<img className='homebreweryIcon' src={homebreweryIcon} alt='homebreweryIcon' />
|
||||||
},
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
renderStorageIcon : function(){
|
if(Array.isArray(brew.tags)) {
|
||||||
if(!this.props.renderStorage) return;
|
brew.tags = brew.tags?.filter((tag)=>tag); // remove tags that are empty strings
|
||||||
if(this.props.brew.googleId) {
|
brew.tags.sort((a, b)=>{
|
||||||
return <span title={this.props.brew.webViewLink ? 'Your Google Drive Storage': 'Another User\'s Google Drive Storage'}>
|
return a.indexOf(':') - b.indexOf(':') !== 0 ? a.indexOf(':') - b.indexOf(':') : a.toLowerCase().localeCompare(b.toLowerCase());
|
||||||
<a href={this.props.brew.webViewLink} target='_blank'>
|
});
|
||||||
<img className='googleDriveIcon' src={googleDriveIcon} alt='googleDriveIcon' />
|
}
|
||||||
</a>
|
|
||||||
</span>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <span title='Homebrewery Storage'>
|
const dateFormatString = 'YYYY-MM-DD HH:mm:ss';
|
||||||
<img className='homebreweryIcon' src={homebreweryIcon} alt='homebreweryIcon' />
|
|
||||||
</span>;
|
|
||||||
},
|
|
||||||
|
|
||||||
render : function(){
|
return (
|
||||||
const brew = this.props.brew;
|
<div className='brewItem'>
|
||||||
if(Array.isArray(brew.tags)) { // temporary fix until dud tags are cleaned
|
{brew.thumbnail && <div className='thumbnail' style={{ backgroundImage: `url(${brew.thumbnail})` }}></div>}
|
||||||
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';
|
|
||||||
|
|
||||||
return <div className='brewItem'>
|
|
||||||
{brew.thumbnail &&
|
|
||||||
<div className='thumbnail' style={{ backgroundImage: `url(${brew.thumbnail})` }} >
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
<div className='text'>
|
<div className='text'>
|
||||||
<h2>{brew.title}</h2>
|
<h2>{brew.title}</h2>
|
||||||
<p className='description'>{brew.description}</p>
|
<p className='description'>{brew.description}</p>
|
||||||
</div>
|
</div>
|
||||||
<hr />
|
<hr />
|
||||||
<div className='info'>
|
<div className='info'>
|
||||||
|
{brew.tags?.length ? (
|
||||||
{brew.tags?.length ? <>
|
|
||||||
<div className='brewTags' title={`${brew.tags.length} tags:\n${brew.tags.join('\n')}`}>
|
<div className='brewTags' title={`${brew.tags.length} tags:\n${brew.tags.join('\n')}`}>
|
||||||
<i className='fas fa-tags'/>
|
<i className='fas fa-tags' />
|
||||||
{brew.tags.map((tag, idx)=>{
|
{brew.tags.map((tag, idx)=>{
|
||||||
const matches = tag.match(/^(?:([^:]+):)?([^:]+)$/);
|
const matches = tag.match(/^(?:([^:]+):)?([^:]+)$/);
|
||||||
return <span key={idx} className={matches[1]} onClick={()=>{this.updateFilter(tag);}}>{matches[2]}</span>;
|
return <span key={idx} className={matches[1]} onClick={()=>updateFilter(tag)}>{matches[2]}</span>;
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</> : <></>
|
) : null}
|
||||||
}
|
|
||||||
<span title={`Authors:\n${brew.authors?.join('\n')}`}>
|
<span title={`Authors:\n${brew.authors?.join('\n')}`}>
|
||||||
<i className='fas fa-user'/> {brew.authors?.map((author, index)=>(
|
<i className='fas fa-user' />{' '}
|
||||||
|
{brew.authors?.map((author, index)=>(
|
||||||
<React.Fragment key={index}>
|
<React.Fragment key={index}>
|
||||||
{author === 'hidden'
|
{author === 'hidden' ? (
|
||||||
? <span title="Username contained an email address; hidden to protect user's privacy">{author}</span>
|
<span title="Username contained an email address; hidden to protect user's privacy">
|
||||||
: <a href={`/user/${author}`}>{author}</a>
|
{author}
|
||||||
}
|
</span>
|
||||||
|
) : (<a href={`/user/${author}`}>{author}</a>)}
|
||||||
{index < brew.authors.length - 1 && ', '}
|
{index < brew.authors.length - 1 && ', '}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
))}
|
))}
|
||||||
</span>
|
</span>
|
||||||
<br />
|
<br />
|
||||||
<span title={`Last viewed: ${moment(brew.lastViewed).local().format(dateFormatString)}`}>
|
<span title={`Last viewed: ${moment(brew.lastViewed).local().format(dateFormatString)}`}>
|
||||||
<i className='fas fa-eye'/> {brew.views}
|
<i className='fas fa-eye' /> {brew.views}
|
||||||
</span>
|
</span>
|
||||||
{brew.pageCount &&
|
{brew.pageCount && (
|
||||||
<span title={`Page count: ${brew.pageCount}`}>
|
<span title={`Page count: ${brew.pageCount}`}>
|
||||||
<i className='far fa-file' /> {brew.pageCount}
|
<i className='far fa-file' /> {brew.pageCount}
|
||||||
</span>
|
</span>
|
||||||
}
|
)}
|
||||||
<span title={dedent`
|
<span
|
||||||
Created: ${moment(brew.createdAt).local().format(dateFormatString)}
|
title={dedent` Created: ${moment(brew.createdAt).local().format(dateFormatString)}
|
||||||
Last updated: ${moment(brew.updatedAt).local().format(dateFormatString)}`}>
|
Last updated: ${moment(brew.updatedAt).local().format(dateFormatString)}`}
|
||||||
|
>
|
||||||
<i className='fas fa-sync-alt' /> {moment(brew.updatedAt).fromNow()}
|
<i className='fas fa-sync-alt' /> {moment(brew.updatedAt).fromNow()}
|
||||||
</span>
|
</span>
|
||||||
{this.renderStorageIcon()}
|
{renderStorageIcon()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='links'>
|
<div className='links'>
|
||||||
{this.renderShareLink()}
|
{renderShareLink()}
|
||||||
{this.renderEditLink()}
|
{renderEditLink()}
|
||||||
{this.renderDownloadLink()}
|
{renderDownloadLink()}
|
||||||
{this.renderDeleteBrewLink()}
|
{renderDeleteBrewLink()}
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>
|
||||||
}
|
);
|
||||||
});
|
};
|
||||||
|
|
||||||
module.exports = BrewItem;
|
module.exports = BrewItem;
|
||||||
|
|||||||
@@ -432,40 +432,40 @@ const EditPage = createClass({
|
|||||||
{this.renderNavbar()}
|
{this.renderNavbar()}
|
||||||
|
|
||||||
{this.props.brew.lock && <LockNotification shareId={this.props.brew.shareId} message={this.props.brew.lock.editMessage} />}
|
{this.props.brew.lock && <LockNotification shareId={this.props.brew.shareId} message={this.props.brew.lock.editMessage} />}
|
||||||
<div className="content">
|
<div className='content'>
|
||||||
<SplitPane onDragFinish={this.handleSplitMove}>
|
<SplitPane onDragFinish={this.handleSplitMove}>
|
||||||
<Editor
|
<Editor
|
||||||
ref={this.editor}
|
ref={this.editor}
|
||||||
brew={this.state.brew}
|
brew={this.state.brew}
|
||||||
onTextChange={this.handleTextChange}
|
onTextChange={this.handleTextChange}
|
||||||
onStyleChange={this.handleStyleChange}
|
onStyleChange={this.handleStyleChange}
|
||||||
onMetaChange={this.handleMetaChange}
|
onMetaChange={this.handleMetaChange}
|
||||||
reportError={this.errorReported}
|
reportError={this.errorReported}
|
||||||
renderer={this.state.brew.renderer}
|
renderer={this.state.brew.renderer}
|
||||||
userThemes={this.props.userThemes}
|
userThemes={this.props.userThemes}
|
||||||
snippetBundle={this.state.themeBundle.snippets}
|
snippetBundle={this.state.themeBundle.snippets}
|
||||||
updateBrew={this.updateBrew}
|
updateBrew={this.updateBrew}
|
||||||
onCursorPageChange={this.handleEditorCursorPageChange}
|
onCursorPageChange={this.handleEditorCursorPageChange}
|
||||||
onViewPageChange={this.handleEditorViewPageChange}
|
onViewPageChange={this.handleEditorViewPageChange}
|
||||||
currentEditorViewPageNum={this.state.currentEditorViewPageNum}
|
currentEditorViewPageNum={this.state.currentEditorViewPageNum}
|
||||||
currentEditorCursorPageNum={this.state.currentEditorCursorPageNum}
|
currentEditorCursorPageNum={this.state.currentEditorCursorPageNum}
|
||||||
currentBrewRendererPageNum={this.state.currentBrewRendererPageNum}
|
currentBrewRendererPageNum={this.state.currentBrewRendererPageNum}
|
||||||
/>
|
/>
|
||||||
<BrewRenderer
|
<BrewRenderer
|
||||||
text={this.state.brew.text}
|
text={this.state.brew.text}
|
||||||
style={this.state.brew.style}
|
style={this.state.brew.style}
|
||||||
renderer={this.state.brew.renderer}
|
renderer={this.state.brew.renderer}
|
||||||
theme={this.state.brew.theme}
|
theme={this.state.brew.theme}
|
||||||
themeBundle={this.state.themeBundle}
|
themeBundle={this.state.themeBundle}
|
||||||
errors={this.state.htmlErrors}
|
errors={this.state.htmlErrors}
|
||||||
lang={this.state.brew.lang}
|
lang={this.state.brew.lang}
|
||||||
onPageChange={this.handleBrewRendererPageChange}
|
onPageChange={this.handleBrewRendererPageChange}
|
||||||
currentEditorViewPageNum={this.state.currentEditorViewPageNum}
|
currentEditorViewPageNum={this.state.currentEditorViewPageNum}
|
||||||
currentEditorCursorPageNum={this.state.currentEditorCursorPageNum}
|
currentEditorCursorPageNum={this.state.currentEditorCursorPageNum}
|
||||||
currentBrewRendererPageNum={this.state.currentBrewRendererPageNum}
|
currentBrewRendererPageNum={this.state.currentBrewRendererPageNum}
|
||||||
allowPrint={true}
|
allowPrint={true}
|
||||||
/>
|
/>
|
||||||
</SplitPane>
|
</SplitPane>
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,11 @@ const dedent = require('dedent-tabs').default;
|
|||||||
|
|
||||||
const loginUrl = 'https://www.naturalcrit.com/login';
|
const loginUrl = 'https://www.naturalcrit.com/login';
|
||||||
|
|
||||||
|
// Prevent parsing text (e.g. document titles) as markdown
|
||||||
|
const escape = (text) => {
|
||||||
|
return text.split('').map(char => `&#${char.charCodeAt(0)};`).join('');
|
||||||
|
};
|
||||||
|
|
||||||
//001-050 : Brew errors
|
//001-050 : Brew errors
|
||||||
//050-100 : Other pages errors
|
//050-100 : Other pages errors
|
||||||
|
|
||||||
@@ -89,7 +94,7 @@ const errorIndex = (props)=>{
|
|||||||
|
|
||||||
:
|
:
|
||||||
|
|
||||||
**Brew Title:** ${props.brew.brewTitle || 'Unable to show title'}
|
**Brew Title:** ${escape(props.brew.brewTitle) || 'Unable to show title'}
|
||||||
|
|
||||||
**Current Authors:** ${props.brew.authors?.map((author)=>{return `[${author}](/user/${author})`;}).join(', ') || 'Unable to list authors'}
|
**Current Authors:** ${props.brew.authors?.map((author)=>{return `[${author}](/user/${author})`;}).join(', ') || 'Unable to list authors'}
|
||||||
|
|
||||||
@@ -104,7 +109,7 @@ const errorIndex = (props)=>{
|
|||||||
|
|
||||||
:
|
:
|
||||||
|
|
||||||
**Brew Title:** ${props.brew.brewTitle || 'Unable to show title'}
|
**Brew Title:** ${escape(props.brew.brewTitle) || 'Unable to show title'}
|
||||||
|
|
||||||
**Current Authors:** ${props.brew.authors?.map((author)=>{return `[${author}](/user/${author})`;}).join(', ') || 'Unable to list authors'}
|
**Current Authors:** ${props.brew.authors?.map((author)=>{return `[${author}](/user/${author})`;}).join(', ') || 'Unable to list authors'}
|
||||||
|
|
||||||
@@ -181,7 +186,7 @@ const errorIndex = (props)=>{
|
|||||||
|
|
||||||
**Brew ID:** ${props.brew.brewId}
|
**Brew ID:** ${props.brew.brewId}
|
||||||
|
|
||||||
**Brew Title:** ${props.brew.brewTitle}`,
|
**Brew Title:** ${escape(props.brew.brewTitle)}`,
|
||||||
|
|
||||||
// ####### Admin page error #######
|
// ####### Admin page error #######
|
||||||
'52' : dedent`
|
'52' : dedent`
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
require('./sharePage.less');
|
require('./sharePage.less');
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const createClass = require('create-react-class');
|
const { useState, useEffect, useCallback } = React;
|
||||||
const { Meta } = require('vitreum/headtags');
|
const { Meta } = require('vitreum/headtags');
|
||||||
|
|
||||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||||
@@ -8,130 +8,120 @@ const Navbar = require('../../navbar/navbar.jsx');
|
|||||||
const MetadataNav = require('../../navbar/metadata.navitem.jsx');
|
const MetadataNav = require('../../navbar/metadata.navitem.jsx');
|
||||||
const PrintNavItem = require('../../navbar/print.navitem.jsx');
|
const PrintNavItem = require('../../navbar/print.navitem.jsx');
|
||||||
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
||||||
const VaultNavItem = require('../../navbar/vault.navitem.jsx');
|
|
||||||
const Account = require('../../navbar/account.navitem.jsx');
|
const Account = require('../../navbar/account.navitem.jsx');
|
||||||
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
||||||
|
|
||||||
const { DEFAULT_BREW_LOAD } = require('../../../../server/brewDefaults.js');
|
const { DEFAULT_BREW_LOAD } = require('../../../../server/brewDefaults.js');
|
||||||
const { printCurrentBrew, fetchThemeBundle } = require('../../../../shared/helpers.js');
|
const { printCurrentBrew, fetchThemeBundle } = require('../../../../shared/helpers.js');
|
||||||
|
|
||||||
const SharePage = createClass({
|
const SharePage = (props)=>{
|
||||||
displayName : 'SharePage',
|
const { brew = DEFAULT_BREW_LOAD, disableMeta = false } = props;
|
||||||
getDefaultProps : function() {
|
|
||||||
return {
|
|
||||||
brew : DEFAULT_BREW_LOAD,
|
|
||||||
disableMeta : false
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
getInitialState : function() {
|
const [state, setState] = useState({
|
||||||
return {
|
themeBundle : {},
|
||||||
themeBundle : {},
|
currentBrewRendererPageNum : 1,
|
||||||
currentBrewRendererPageNum : 1
|
});
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
componentDidMount : function() {
|
const handleBrewRendererPageChange = useCallback((pageNumber)=>{
|
||||||
document.addEventListener('keydown', this.handleControlKeys);
|
setState((prevState)=>({
|
||||||
|
currentBrewRendererPageNum : pageNumber,
|
||||||
|
...prevState }));
|
||||||
|
}, []);
|
||||||
|
|
||||||
fetchThemeBundle(this, this.props.brew.renderer, this.props.brew.theme);
|
const handleControlKeys = (e)=>{
|
||||||
},
|
|
||||||
|
|
||||||
componentWillUnmount : function() {
|
|
||||||
document.removeEventListener('keydown', this.handleControlKeys);
|
|
||||||
},
|
|
||||||
|
|
||||||
handleBrewRendererPageChange : function(pageNumber){
|
|
||||||
this.setState({ currentBrewRendererPageNum: pageNumber });
|
|
||||||
},
|
|
||||||
|
|
||||||
handleControlKeys : function(e){
|
|
||||||
if(!(e.ctrlKey || e.metaKey)) return;
|
if(!(e.ctrlKey || e.metaKey)) return;
|
||||||
const P_KEY = 80;
|
const P_KEY = 80;
|
||||||
if(e.keyCode == P_KEY){
|
if(e.keyCode === P_KEY) {
|
||||||
if(e.keyCode == P_KEY) printCurrentBrew();
|
printCurrentBrew();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
},
|
};
|
||||||
|
|
||||||
processShareId : function() {
|
useEffect(()=>{
|
||||||
return this.props.brew.googleId && !this.props.brew.stubbed ?
|
document.addEventListener('keydown', handleControlKeys);
|
||||||
this.props.brew.googleId + this.props.brew.shareId :
|
fetchThemeBundle(
|
||||||
this.props.brew.shareId;
|
{ setState },
|
||||||
},
|
brew.renderer,
|
||||||
|
brew.theme
|
||||||
|
);
|
||||||
|
|
||||||
renderEditLink : function(){
|
return ()=>{
|
||||||
if(!this.props.brew.editId) return;
|
document.removeEventListener('keydown', handleControlKeys);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
let editLink = this.props.brew.editId;
|
const processShareId = ()=>{
|
||||||
if(this.props.brew.googleId && !this.props.brew.stubbed) {
|
return brew.googleId && !brew.stubbed ? brew.googleId + brew.shareId : brew.shareId;
|
||||||
editLink = this.props.brew.googleId + editLink;
|
};
|
||||||
}
|
|
||||||
|
|
||||||
return <Nav.item color='orange' icon='fas fa-pencil-alt' href={`/edit/${editLink}`}>
|
const renderEditLink = ()=>{
|
||||||
edit
|
if(!brew.editId) return null;
|
||||||
</Nav.item>;
|
|
||||||
},
|
|
||||||
|
|
||||||
render : function(){
|
const editLink = brew.googleId && ! brew.stubbed ? brew.googleId + brew.editId : brew.editId;
|
||||||
const titleStyle = this.props.disableMeta ? { cursor: 'default' } : {};
|
|
||||||
const titleEl = <Nav.item className='brewTitle' style={titleStyle}>{this.props.brew.title}</Nav.item>;
|
|
||||||
|
|
||||||
return <div className='sharePage sitePage'>
|
return (
|
||||||
|
<Nav.item color='orange' icon='fas fa-pencil-alt' href={`/edit/${editLink}`}>
|
||||||
|
edit
|
||||||
|
</Nav.item>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const titleEl = (
|
||||||
|
<Nav.item className='brewTitle' style={disableMeta ? { cursor: 'default' } : {}}>
|
||||||
|
{brew.title}
|
||||||
|
</Nav.item>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='sharePage sitePage'>
|
||||||
<Meta name='robots' content='noindex, nofollow' />
|
<Meta name='robots' content='noindex, nofollow' />
|
||||||
<Navbar>
|
<Navbar>
|
||||||
<Nav.section className='titleSection'>
|
<Nav.section className='titleSection'>
|
||||||
{
|
{disableMeta ? titleEl : <MetadataNav brew={brew}>{titleEl}</MetadataNav>}
|
||||||
this.props.disableMeta ?
|
|
||||||
titleEl
|
|
||||||
:
|
|
||||||
<MetadataNav brew={this.props.brew}>
|
|
||||||
{titleEl}
|
|
||||||
</MetadataNav>
|
|
||||||
}
|
|
||||||
</Nav.section>
|
</Nav.section>
|
||||||
|
|
||||||
<Nav.section>
|
<Nav.section>
|
||||||
{this.props.brew.shareId && <>
|
{brew.shareId && (
|
||||||
<PrintNavItem/>
|
<>
|
||||||
<Nav.dropdown>
|
<PrintNavItem />
|
||||||
<Nav.item color='red' icon='fas fa-code'>
|
<Nav.dropdown>
|
||||||
source
|
<Nav.item color='red' icon='fas fa-code'>
|
||||||
</Nav.item>
|
source
|
||||||
<Nav.item color='blue' icon='fas fa-eye' href={`/source/${this.processShareId()}`}>
|
</Nav.item>
|
||||||
view
|
<Nav.item color='blue' icon='fas fa-eye' href={`/source/${processShareId()}`}>
|
||||||
</Nav.item>
|
view
|
||||||
{this.renderEditLink()}
|
</Nav.item>
|
||||||
<Nav.item color='blue' icon='fas fa-download' href={`/download/${this.processShareId()}`}>
|
{renderEditLink()}
|
||||||
download
|
<Nav.item color='blue' icon='fas fa-download' href={`/download/${processShareId()}`}>
|
||||||
</Nav.item>
|
download
|
||||||
<Nav.item color='blue' icon='fas fa-clone' href={`/new/${this.processShareId()}`}>
|
</Nav.item>
|
||||||
clone to new
|
<Nav.item color='blue' icon='fas fa-clone' href={`/new/${processShareId()}`}>
|
||||||
</Nav.item>
|
clone to new
|
||||||
</Nav.dropdown>
|
</Nav.item>
|
||||||
</>}
|
</Nav.dropdown>
|
||||||
<VaultNavItem/>
|
</>
|
||||||
<RecentNavItem brew={this.props.brew} storageKey='view' />
|
)}
|
||||||
|
<RecentNavItem brew={brew} storageKey='view' />
|
||||||
<Account />
|
<Account />
|
||||||
</Nav.section>
|
</Nav.section>
|
||||||
</Navbar>
|
</Navbar>
|
||||||
|
|
||||||
<div className='content'>
|
<div className='content'>
|
||||||
<BrewRenderer
|
<BrewRenderer
|
||||||
text={this.props.brew.text}
|
text={brew.text}
|
||||||
style={this.props.brew.style}
|
style={brew.style}
|
||||||
lang={this.props.brew.lang}
|
lang={brew.lang}
|
||||||
renderer={this.props.brew.renderer}
|
renderer={brew.renderer}
|
||||||
theme={this.props.brew.theme}
|
theme={brew.theme}
|
||||||
themeBundle={this.state.themeBundle}
|
themeBundle={state.themeBundle}
|
||||||
onPageChange={this.handleBrewRendererPageChange}
|
onPageChange={handleBrewRendererPageChange}
|
||||||
currentBrewRendererPageNum={this.state.currentBrewRendererPageNum}
|
currentBrewRendererPageNum={state.currentBrewRendererPageNum}
|
||||||
allowPrint={true}
|
allowPrint={true}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>
|
||||||
}
|
);
|
||||||
});
|
};
|
||||||
|
|
||||||
module.exports = SharePage;
|
module.exports = SharePage;
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
version: '2'
|
|
||||||
services:
|
services:
|
||||||
mongodb:
|
mongodb:
|
||||||
image: mongo:latest
|
image: mongo:latest
|
||||||
|
|||||||
@@ -24,12 +24,16 @@ These instructions assume that you are installing to a completely new, fresh Ubu
|
|||||||
|
|
||||||
These installation instructions have been tested on the following Ubuntu releases:
|
These installation instructions have been tested on the following Ubuntu releases:
|
||||||
|
|
||||||
- *ubuntu-20.04.3-desktop-amd64*
|
- *ubuntu-24.04.1-desktop-amd64*
|
||||||
|
- *ubuntu-22.04.5-desktop-amd64*
|
||||||
|
- *ubuntu-20.04.6-desktop-amd64*
|
||||||
|
|
||||||
## Final Notes
|
## Final Notes
|
||||||
|
|
||||||
While this installation process works successfully at the time of writing (December 19, 2021), it relies on all of the Node.JS packages used in the HomeBrewery project retaining their cross-platform capabilities to continue to function. This is one of the inherent advantages of Node.JS, but it is by no means guaranteed and as such, functionality or even installation may fail without warning at some point in the future.
|
While this installation process works successfully at the time of writing (December 19, 2021), it relies on all of the Node.JS packages used in the HomeBrewery project retaining their cross-platform capabilities to continue to function. This is one of the inherent advantages of Node.JS, but it is by no means guaranteed and as such, functionality or even installation may fail without warning at some point in the future.
|
||||||
|
|
||||||
|
Earlier versions of Ubuntu may requier an alternate Mongo setup, see https://www.mongodb.com/docs/manual/tutorial/install-mongodb-on-ubuntu/ for assistance.
|
||||||
|
|
||||||
Regards,
|
Regards,
|
||||||
G
|
G
|
||||||
December 19, 2021
|
December 19, 2021
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ Description=Homebrewery Web Server
|
|||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
User=root
|
User=root
|
||||||
After=mongodb
|
BindsTo=mongod.service
|
||||||
|
After=mongod.service
|
||||||
Environment=NODE_ENV=local
|
Environment=NODE_ENV=local
|
||||||
WorkingDirectory=/usr/local/homebrewery
|
WorkingDirectory=/usr/local/homebrewery
|
||||||
ExecStart=node server.js
|
ExecStart=node server.js
|
||||||
|
|||||||
@@ -1,14 +1,60 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
|
# Detect Ubuntu Version
|
||||||
|
export DISTRO=$(grep "^NAME=" /etc/os-release | awk -F '=' '{print $2}' | sed 's/"//g')
|
||||||
|
export DISTRO_VER=$(grep "VERSION_ID=" /etc/os-release | awk -F '=' '{print $2}' | sed 's/"//g')
|
||||||
|
export MATCHED="Yes"
|
||||||
|
|
||||||
|
if [ "${DISTRO}" != "Ubuntu" ];
|
||||||
|
then
|
||||||
|
echo :: Ubuntu not detected. Are you using an alternate spin or derivative?
|
||||||
|
echo :: Detected - ${DISTRO}
|
||||||
|
read -p [y/N] YESNO
|
||||||
|
if [ "${YESNO}" != "Y" ] && [ ]"${YESNO}" != "y" ]; then
|
||||||
|
exit
|
||||||
|
fi
|
||||||
|
|
||||||
|
MATCHED="No"
|
||||||
|
fi
|
||||||
|
|
||||||
# Install CURL and add required NodeJS source to package repo
|
# Install CURL and add required NodeJS source to package repo
|
||||||
echo ::Install CURL
|
echo ::Install CURL
|
||||||
apt install -y curl
|
apt install -y curl
|
||||||
echo ::Add NodeJS source to package repo
|
echo ::Add NodeJS source to package repo
|
||||||
curl -fsSL https://deb.nodesource.com/setup_16.x | sudo -E bash -
|
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
|
||||||
|
|
||||||
|
# Add Mongo CE Source
|
||||||
|
if [ ${DISTRO} = "Ubuntu" ];
|
||||||
|
then
|
||||||
|
echo ::Add Mongo CE source to package repo
|
||||||
|
curl -fsSL https://www.mongodb.org/static/pgp/server-8.0.asc | \
|
||||||
|
sudo gpg -o /usr/share/keyrings/mongodb-server-8.0.gpg \
|
||||||
|
--dearmor
|
||||||
|
if [ "${DISTRO_VER}" == "24.04" ]; then
|
||||||
|
echo "deb [ arch=amd64,arm64 signed-by=/usr/share/keyrings/mongodb-server-8.0.gpg ] https://repo.mongodb.org/apt/ubuntu noble/mongodb-org/8.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-8.0.list
|
||||||
|
elif [ "${DISTRO_VER}" == "22.04" ]; then
|
||||||
|
echo "deb [ arch=amd64,arm64 signed-by=/usr/share/keyrings/mongodb-server-8.0.gpg ] https://repo.mongodb.org/apt/ubuntu jammy/mongodb-org/8.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-8.0.list
|
||||||
|
elif [ "${DISTRO_VER}" == "20.04" ]; then
|
||||||
|
echo "deb [ arch=amd64,arm64 signed-by=/usr/share/keyrings/mongodb-server-8.0.gpg ] https://repo.mongodb.org/apt/ubuntu focal/mongodb-org/8.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-8.0.list
|
||||||
|
else
|
||||||
|
MATCHED="No"
|
||||||
|
fi
|
||||||
|
sudo apt-get update
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ${MATCHED} == "No" ]; then
|
||||||
|
echo :: WARNING
|
||||||
|
echo :: Unable to determine Ubuntu version for Mongo installation purposes.
|
||||||
|
echo :: Please check your spin/distro documentation to install Mongo CE and enable it on startup.
|
||||||
|
fi
|
||||||
|
|
||||||
# Install required packages
|
# Install required packages
|
||||||
echo ::Install Homebrewery requirements
|
echo ::Install Homebrewery requirements
|
||||||
apt satisfy -y git nodejs npm mongodb
|
apt satisfy -y git nodejs npm mongodb-org
|
||||||
|
|
||||||
|
# Enable and start Mongo
|
||||||
|
systemctl enable mongod
|
||||||
|
systemctl start mongod
|
||||||
|
|
||||||
# Clone Homebrewery repo
|
# Clone Homebrewery repo
|
||||||
echo ::Get Homebrewery files
|
echo ::Get Homebrewery files
|
||||||
|
|||||||
549
package-lock.json
generated
549
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
31
package.json
31
package.json
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "homebrewery",
|
"name": "homebrewery",
|
||||||
"description": "Create authentic looking D&D homebrews using only markdown",
|
"description": "Create authentic looking D&D homebrews using only markdown",
|
||||||
"version": "3.16.1",
|
"version": "3.17.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"engines": {
|
"engines": {
|
||||||
"npm": "^10.2.x",
|
"npm": "^10.2.x",
|
||||||
@@ -39,6 +39,7 @@
|
|||||||
"test:definition-lists": "jest tests/markdown/definition-lists.test.js --verbose --noStackTrace",
|
"test:definition-lists": "jest tests/markdown/definition-lists.test.js --verbose --noStackTrace",
|
||||||
"test:hard-breaks": "jest tests/markdown/hard-breaks.test.js --verbose --noStackTrace",
|
"test:hard-breaks": "jest tests/markdown/hard-breaks.test.js --verbose --noStackTrace",
|
||||||
"test:non-breaking-spaces": "jest tests/markdown/non-breaking-spaces.test.js --verbose --noStackTrace",
|
"test:non-breaking-spaces": "jest tests/markdown/non-breaking-spaces.test.js --verbose --noStackTrace",
|
||||||
|
"test:paragraph-justification": "jest tests/markdown/paragraph-justification.test.js --verbose --noStackTrace",
|
||||||
"test:emojis": "jest tests/markdown/emojis.test.js --verbose --noStackTrace",
|
"test:emojis": "jest tests/markdown/emojis.test.js --verbose --noStackTrace",
|
||||||
"test:route": "jest tests/routes/static-pages.test.js --verbose",
|
"test:route": "jest tests/routes/static-pages.test.js --verbose",
|
||||||
"test:safehtml": "jest tests/html/safeHTML.test.js --verbose",
|
"test:safehtml": "jest tests/html/safeHTML.test.js --verbose",
|
||||||
@@ -84,16 +85,16 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/core": "^7.26.0",
|
"@babel/core": "^7.26.7",
|
||||||
"@babel/plugin-transform-runtime": "^7.25.9",
|
"@babel/plugin-transform-runtime": "^7.25.9",
|
||||||
"@babel/preset-env": "^7.26.0",
|
"@babel/preset-env": "^7.26.7",
|
||||||
"@babel/preset-react": "^7.26.3",
|
"@babel/preset-react": "^7.26.3",
|
||||||
"@googleapis/drive": "^8.14.0",
|
"@googleapis/drive": "^8.14.0",
|
||||||
"body-parser": "^1.20.2",
|
"body-parser": "^1.20.2",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
"codemirror": "^5.65.6",
|
"codemirror": "^5.65.6",
|
||||||
"cookie-parser": "^1.4.7",
|
"cookie-parser": "^1.4.7",
|
||||||
"core-js": "^3.39.0",
|
"core-js": "^3.40.0",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"create-react-class": "^15.7.0",
|
"create-react-class": "^15.7.0",
|
||||||
"dedent-tabs": "^0.10.3",
|
"dedent-tabs": "^0.10.3",
|
||||||
@@ -102,7 +103,7 @@
|
|||||||
"express": "^4.21.2",
|
"express": "^4.21.2",
|
||||||
"express-async-handler": "^1.2.0",
|
"express-async-handler": "^1.2.0",
|
||||||
"express-static-gzip": "2.2.0",
|
"express-static-gzip": "2.2.0",
|
||||||
"fs-extra": "11.2.0",
|
"fs-extra": "11.3.0",
|
||||||
"idb-keyval": "^6.2.1",
|
"idb-keyval": "^6.2.1",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"jwt-simple": "^0.5.6",
|
"jwt-simple": "^0.5.6",
|
||||||
@@ -110,36 +111,36 @@
|
|||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"marked": "11.2.0",
|
"marked": "11.2.0",
|
||||||
"marked-emoji": "^1.4.3",
|
"marked-emoji": "^1.4.3",
|
||||||
"marked-extended-tables": "^1.0.10",
|
"marked-extended-tables": "^1.1.0",
|
||||||
"marked-gfm-heading-id": "^3.2.0",
|
"marked-gfm-heading-id": "^3.2.0",
|
||||||
"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.9.2",
|
"mongoose": "^8.9.5",
|
||||||
"nanoid": "5.0.9",
|
"nanoid": "5.0.9",
|
||||||
"nconf": "^0.12.1",
|
"nconf": "^0.12.1",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-frame-component": "^4.1.3",
|
"react-frame-component": "^4.1.3",
|
||||||
"react-router": "^7.1.1",
|
"react-router": "^7.1.3",
|
||||||
"sanitize-filename": "1.6.3",
|
"sanitize-filename": "1.6.3",
|
||||||
"superagent": "^10.1.1",
|
"superagent": "^10.1.1",
|
||||||
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git"
|
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@stylistic/stylelint-plugin": "^3.1.1",
|
"@stylistic/stylelint-plugin": "^3.1.1",
|
||||||
"babel-plugin-transform-import-meta": "^2.2.1",
|
"babel-plugin-transform-import-meta": "^2.3.2",
|
||||||
"eslint": "^9.17.0",
|
"eslint": "^9.19.0",
|
||||||
"eslint-plugin-jest": "^28.10.0",
|
"eslint-plugin-jest": "^28.11.0",
|
||||||
"eslint-plugin-react": "^7.37.3",
|
"eslint-plugin-react": "^7.37.4",
|
||||||
"globals": "^15.14.0",
|
"globals": "^15.14.0",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"jest-expect-message": "^1.1.3",
|
"jest-expect-message": "^1.1.3",
|
||||||
"jsdom-global": "^3.0.2",
|
"jsdom-global": "^3.0.2",
|
||||||
"postcss-less": "^6.0.0",
|
"postcss-less": "^6.0.0",
|
||||||
"stylelint": "^16.12.0",
|
"stylelint": "^16.14.1",
|
||||||
"stylelint-config-recess-order": "^5.1.1",
|
"stylelint-config-recess-order": "^6.0.0",
|
||||||
"stylelint-config-recommended": "^14.0.1",
|
"stylelint-config-recommended": "^15.0.0",
|
||||||
"supertest": "^7.0.0"
|
"supertest": "^7.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
|
/* eslint-disable max-depth */
|
||||||
/* eslint-disable max-lines */
|
/* eslint-disable max-lines */
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { Parser as MathParser } from 'expr-eval';
|
import { Parser as MathParser } from 'expr-eval';
|
||||||
import { marked as Marked } from 'marked';
|
import { marked as Marked } from 'marked';
|
||||||
import MarkedExtendedTables from 'marked-extended-tables';
|
import MarkedExtendedTables from 'marked-extended-tables';
|
||||||
import { markedSmartypantsLite as MarkedSmartypantsLite } from 'marked-smartypants-lite';
|
import { markedSmartypantsLite as MarkedSmartypantsLite } from 'marked-smartypants-lite';
|
||||||
import { gfmHeadingId as MarkedGFMHeadingId, resetHeadings as MarkedGFMResetHeadingIDs } from 'marked-gfm-heading-id';
|
import { gfmHeadingId as MarkedGFMHeadingId, resetHeadings as MarkedGFMResetHeadingIDs } from 'marked-gfm-heading-id';
|
||||||
@@ -172,8 +173,8 @@ const mustacheSpans = {
|
|||||||
return `<span` +
|
return `<span` +
|
||||||
`${tags.classes ? ` class="${tags.classes}"` : ''}` +
|
`${tags.classes ? ` class="${tags.classes}"` : ''}` +
|
||||||
`${tags.id ? ` id="${tags.id}"` : ''}` +
|
`${tags.id ? ` id="${tags.id}"` : ''}` +
|
||||||
`${tags.styles ? ` style="${tags.styles}"` : ''}` +
|
`${tags.styles ? ` style="${Object.entries(tags.styles).map(([key, value])=>`${key}:${value};`).join(' ')}"` : ''}` +
|
||||||
`${tags.attributes ? ` ${Object.entries(tags.attributes).map(([key, value])=>`${key}="${value}"`).join(' ')}` : ''}` +
|
`${tags.attributes ? ` ${Object.entries(tags.attributes).map(([key, value])=>`${key}="${value}"`).join(' ')}` : ''}` +
|
||||||
`>${this.parser.parseInline(token.tokens)}</span>`; // parseInline to turn child tokens into HTML
|
`>${this.parser.parseInline(token.tokens)}</span>`; // parseInline to turn child tokens into HTML
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -228,7 +229,7 @@ const mustacheDivs = {
|
|||||||
return `<div` +
|
return `<div` +
|
||||||
`${tags.classes ? ` class="${tags.classes}"` : ''}` +
|
`${tags.classes ? ` class="${tags.classes}"` : ''}` +
|
||||||
`${tags.id ? ` id="${tags.id}"` : ''}` +
|
`${tags.id ? ` id="${tags.id}"` : ''}` +
|
||||||
`${tags.styles ? ` style="${tags.styles}"` : ''}` +
|
`${tags.styles ? ` style="${Object.entries(tags.styles).map(([key, value])=>`${key}:${value};`).join(' ')}"` : ''}` +
|
||||||
`${tags.attributes ? ` ${Object.entries(tags.attributes).map(([key, value])=>`${key}="${value}"`).join(' ')}` : ''}` +
|
`${tags.attributes ? ` ${Object.entries(tags.attributes).map(([key, value])=>`${key}="${value}"`).join(' ')}` : ''}` +
|
||||||
`>${this.parser.parse(token.tokens)}</div>`; // parse to turn child tokens into HTML
|
`>${this.parser.parse(token.tokens)}</div>`; // parse to turn child tokens into HTML
|
||||||
}
|
}
|
||||||
@@ -265,18 +266,13 @@ const mustacheInjectInline = {
|
|||||||
const text = this.parser.parseInline([token]);
|
const text = this.parser.parseInline([token]);
|
||||||
const originalTags = extractHTMLStyleTags(text);
|
const originalTags = extractHTMLStyleTags(text);
|
||||||
const injectedTags = token.injectedTags;
|
const injectedTags = token.injectedTags;
|
||||||
const tags = {
|
const tags = mergeHTMLTags(originalTags, injectedTags);
|
||||||
id : injectedTags.id || originalTags.id || null,
|
|
||||||
classes : [originalTags.classes, injectedTags.classes].join(' ').trim() || null,
|
|
||||||
styles : [originalTags.styles, injectedTags.styles].join(' ').trim() || null,
|
|
||||||
attributes : Object.assign(originalTags.attributes ?? {}, injectedTags.attributes ?? {})
|
|
||||||
};
|
|
||||||
const openingTag = /(<[^\s<>]+)[^\n<>]*(>.*)/s.exec(text);
|
const openingTag = /(<[^\s<>]+)[^\n<>]*(>.*)/s.exec(text);
|
||||||
if(openingTag) {
|
if(openingTag) {
|
||||||
return `${openingTag[1]}` +
|
return `${openingTag[1]}` +
|
||||||
`${tags.classes ? ` class="${tags.classes}"` : ''}` +
|
`${tags.classes ? ` class="${tags.classes}"` : ''}` +
|
||||||
`${tags.id ? ` id="${tags.id}"` : ''}` +
|
`${tags.id ? ` id="${tags.id}"` : ''}` +
|
||||||
`${tags.styles ? ` style="${tags.styles}"` : ''}` +
|
`${!_.isEmpty(tags.styles) ? ` style="${Object.entries(tags.styles).map(([key, value])=>`${key}:${value};`).join(' ')}"` : ''}` +
|
||||||
`${!_.isEmpty(tags.attributes) ? ` ${Object.entries(tags.attributes).map(([key, value])=>`${key}="${value}"`).join(' ')}` : ''}` +
|
`${!_.isEmpty(tags.attributes) ? ` ${Object.entries(tags.attributes).map(([key, value])=>`${key}="${value}"`).join(' ')}` : ''}` +
|
||||||
`${openingTag[2]}`; // parse to turn child tokens into HTML
|
`${openingTag[2]}`; // parse to turn child tokens into HTML
|
||||||
}
|
}
|
||||||
@@ -314,18 +310,13 @@ const mustacheInjectBlock = {
|
|||||||
const text = this.parser.parse([token]);
|
const text = this.parser.parse([token]);
|
||||||
const originalTags = extractHTMLStyleTags(text);
|
const originalTags = extractHTMLStyleTags(text);
|
||||||
const injectedTags = token.injectedTags;
|
const injectedTags = token.injectedTags;
|
||||||
const tags = {
|
const tags = mergeHTMLTags(originalTags, injectedTags);
|
||||||
id : injectedTags.id || originalTags.id || null,
|
|
||||||
classes : [originalTags.classes, injectedTags.classes].join(' ').trim() || null,
|
|
||||||
styles : [originalTags.styles, injectedTags.styles].join(' ').trim() || null,
|
|
||||||
attributes : Object.assign(originalTags.attributes ?? {}, injectedTags.attributes ?? {})
|
|
||||||
};
|
|
||||||
const openingTag = /(<[^\s<>]+)[^\n<>]*(>.*)/s.exec(text);
|
const openingTag = /(<[^\s<>]+)[^\n<>]*(>.*)/s.exec(text);
|
||||||
if(openingTag) {
|
if(openingTag) {
|
||||||
return `${openingTag[1]}` +
|
return `${openingTag[1]}` +
|
||||||
`${tags.classes ? ` class="${tags.classes}"` : ''}` +
|
`${tags.classes ? ` class="${tags.classes}"` : ''}` +
|
||||||
`${tags.id ? ` id="${tags.id}"` : ''}` +
|
`${tags.id ? ` id="${tags.id}"` : ''}` +
|
||||||
`${tags.styles ? ` style="${tags.styles}"` : ''}` +
|
`${!_.isEmpty(tags.styles) ? ` style="${Object.entries(tags.styles).map(([key, value])=>`${key}:${value};`).join(' ')}"` : ''}` +
|
||||||
`${!_.isEmpty(tags.attributes) ? ` ${Object.entries(tags.attributes).map(([key, value])=>`${key}="${value}"`).join(' ')}` : ''}` +
|
`${!_.isEmpty(tags.attributes) ? ` ${Object.entries(tags.attributes).map(([key, value])=>`${key}="${value}"`).join(' ')}` : ''}` +
|
||||||
`${openingTag[2]}`; // parse to turn child tokens into HTML
|
`${openingTag[2]}`; // parse to turn child tokens into HTML
|
||||||
}
|
}
|
||||||
@@ -370,6 +361,43 @@ const superSubScripts = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const justifiedParagraphClasses = [];
|
||||||
|
justifiedParagraphClasses[2] = 'Left';
|
||||||
|
justifiedParagraphClasses[4] = 'Right';
|
||||||
|
justifiedParagraphClasses[6] = 'Center';
|
||||||
|
|
||||||
|
const justifiedParagraphs = {
|
||||||
|
name : 'justifiedParagraphs',
|
||||||
|
level : 'block',
|
||||||
|
start(src) {
|
||||||
|
return src.match(/\n(?:-:|:-|-:) {1}/m)?.index;
|
||||||
|
}, // Hint to Marked.js to stop and check for a match
|
||||||
|
tokenizer(src, tokens) {
|
||||||
|
const regex = /^(((:-))|((-:))|((:-:))) .+(\n(([^\n].*\n)*(\n|$))|$)/ygm;
|
||||||
|
const match = regex.exec(src);
|
||||||
|
if(match?.length) {
|
||||||
|
let whichJustify;
|
||||||
|
if(match[2]?.length) whichJustify = 2;
|
||||||
|
if(match[4]?.length) whichJustify = 4;
|
||||||
|
if(match[6]?.length) whichJustify = 6;
|
||||||
|
return {
|
||||||
|
type : 'justifiedParagraphs', // Should match "name" above
|
||||||
|
raw : match[0], // Text to consume from the source
|
||||||
|
length : match[whichJustify].length,
|
||||||
|
text : match[0].slice(match[whichJustify].length),
|
||||||
|
class : justifiedParagraphClasses[whichJustify],
|
||||||
|
tokens : this.lexer.inlineTokens(match[0].slice(match[whichJustify].length + 1))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
renderer(token) {
|
||||||
|
return `<p align="${token.class}">${this.parser.parseInline(token.tokens)}</p>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
const forcedParagraphBreaks = {
|
const forcedParagraphBreaks = {
|
||||||
name : 'hardBreaks',
|
name : 'hardBreaks',
|
||||||
level : 'block',
|
level : 'block',
|
||||||
@@ -680,7 +708,7 @@ function MarkedVariables() {
|
|||||||
}
|
}
|
||||||
if(match[8]) { // Inline Definition
|
if(match[8]) { // Inline Definition
|
||||||
const label = match[10] ? match[10].trim().replace(/\s+/g, ' ') : null; // Trim edge spaces and shorten blocks of whitespace to 1 space
|
const label = match[10] ? match[10].trim().replace(/\s+/g, ' ') : null; // Trim edge spaces and shorten blocks of whitespace to 1 space
|
||||||
let content = match[11] ? match[11].trim().replace(/\s+/g, ' ') : null; // Trim edge spaces and shorten blocks of whitespace to 1 space
|
let content = match[11] || null;
|
||||||
|
|
||||||
// In case of nested (), find the correct matching end )
|
// In case of nested (), find the correct matching end )
|
||||||
let level = 0;
|
let level = 0;
|
||||||
@@ -696,10 +724,8 @@ function MarkedVariables() {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(i > -1) {
|
combinedRegex.lastIndex = combinedRegex.lastIndex - (content.length - i);
|
||||||
combinedRegex.lastIndex = combinedRegex.lastIndex - (content.length - i);
|
content = content.slice(0, i).trim().replace(/\s+/g, ' ');
|
||||||
content = content.slice(0, i).trim().replace(/\s+/g, ' ');
|
|
||||||
}
|
|
||||||
|
|
||||||
varsQueue.push(
|
varsQueue.push(
|
||||||
{ type : 'varDefBlock',
|
{ type : 'varDefBlock',
|
||||||
@@ -769,7 +795,7 @@ const tableTerminators = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
Marked.use(MarkedVariables());
|
Marked.use(MarkedVariables());
|
||||||
Marked.use({ extensions : [definitionListsMultiLine, definitionListsSingleLine, forcedParagraphBreaks,
|
Marked.use({ extensions : [justifiedParagraphs, definitionListsMultiLine, definitionListsSingleLine, forcedParagraphBreaks,
|
||||||
nonbreakingSpaces, superSubScripts, mustacheSpans, mustacheDivs, mustacheInjectInline] });
|
nonbreakingSpaces, superSubScripts, mustacheSpans, mustacheDivs, mustacheInjectInline] });
|
||||||
Marked.use(mustacheInjectBlock);
|
Marked.use(mustacheInjectBlock);
|
||||||
Marked.use({ renderer: renderer, tokenizer: tokenizer, mangle: false });
|
Marked.use({ renderer: renderer, tokenizer: tokenizer, mangle: false });
|
||||||
@@ -835,15 +861,20 @@ const processStyleTags = (string)=>{
|
|||||||
const index = attr.indexOf('=');
|
const index = attr.indexOf('=');
|
||||||
let [key, value] = [attr.substring(0, index), attr.substring(index + 1)];
|
let [key, value] = [attr.substring(0, index), attr.substring(index + 1)];
|
||||||
value = value.replace(/"/g, '');
|
value = value.replace(/"/g, '');
|
||||||
obj[key] = value;
|
obj[key.trim()] = value.trim();
|
||||||
return obj;
|
return obj;
|
||||||
}, {}) || null;
|
}, {}) || null;
|
||||||
const styles = tags?.length ? tags.map((tag)=>tag.replace(/:"?([^"]*)"?/g, ':$1;').trim()).join(' ') : null;
|
const styles = tags?.length ? tags.reduce((styleObj, style) => {
|
||||||
|
const index = style.indexOf(':');
|
||||||
|
const [key, value] = [style.substring(0, index), style.substring(index + 1)];
|
||||||
|
styleObj[key.trim()] = value.replace(/"?([^"]*)"?/g, '$1').trim();
|
||||||
|
return styleObj;
|
||||||
|
}, {}) : null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id : id,
|
id : id,
|
||||||
classes : classes,
|
classes : classes,
|
||||||
styles : styles,
|
styles : _.isEmpty(styles) ? null : styles,
|
||||||
attributes : _.isEmpty(attributes) ? null : attributes
|
attributes : _.isEmpty(attributes) ? null : attributes
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -853,25 +884,40 @@ const extractHTMLStyleTags = (htmlString)=>{
|
|||||||
const firstElementOnly = htmlString.split('>')[0];
|
const firstElementOnly = htmlString.split('>')[0];
|
||||||
const id = firstElementOnly.match(/id="([^"]*)"/)?.[1] || null;
|
const id = firstElementOnly.match(/id="([^"]*)"/)?.[1] || null;
|
||||||
const classes = firstElementOnly.match(/class="([^"]*)"/)?.[1] || null;
|
const classes = firstElementOnly.match(/class="([^"]*)"/)?.[1] || null;
|
||||||
const styles = firstElementOnly.match(/style="([^"]*)"/)?.[1] || null;
|
const styles = firstElementOnly.match(/style="([^"]*)"/)?.[1]
|
||||||
|
?.split(';').reduce((styleObj, style) => {
|
||||||
|
if (style.trim() === '') return styleObj;
|
||||||
|
const index = style.indexOf(':');
|
||||||
|
const [key, value] = [style.substring(0, index), style.substring(index + 1)];
|
||||||
|
styleObj[key.trim()] = value.trim();
|
||||||
|
return styleObj;
|
||||||
|
}, {}) || null;
|
||||||
const attributes = firstElementOnly.match(/[a-zA-Z]+="[^"]*"/g)
|
const attributes = firstElementOnly.match(/[a-zA-Z]+="[^"]*"/g)
|
||||||
?.filter((attr)=>!attr.startsWith('class="') && !attr.startsWith('style="') && !attr.startsWith('id="'))
|
?.filter((attr)=>!attr.startsWith('class="') && !attr.startsWith('style="') && !attr.startsWith('id="'))
|
||||||
.reduce((obj, attr)=>{
|
.reduce((obj, attr)=>{
|
||||||
const index = attr.indexOf('=');
|
const index = attr.indexOf('=');
|
||||||
let [key, value] = [attr.substring(0, index), attr.substring(index + 1)];
|
let [key, value] = [attr.substring(0, index), attr.substring(index + 1)];
|
||||||
value = value.replace(/"/g, '');
|
obj[key.trim()] = value.replace(/"/g, '');
|
||||||
obj[key] = value;
|
|
||||||
return obj;
|
return obj;
|
||||||
}, {}) || null;
|
}, {}) || null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id : id,
|
id : id,
|
||||||
classes : classes,
|
classes : classes,
|
||||||
styles : styles,
|
styles : _.isEmpty(styles) ? null : styles,
|
||||||
attributes : _.isEmpty(attributes) ? null : attributes
|
attributes : _.isEmpty(attributes) ? null : attributes
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const mergeHTMLTags = (originalTags, newTags) => {
|
||||||
|
return {
|
||||||
|
id : newTags.id || originalTags.id || null,
|
||||||
|
classes : [originalTags.classes, newTags.classes].join(' ').trim() || null,
|
||||||
|
styles : Object.assign(originalTags.styles ?? {}, newTags.styles ?? {}),
|
||||||
|
attributes : Object.assign(originalTags.attributes ?? {}, newTags.attributes ?? {})
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const globalVarsList = {};
|
const globalVarsList = {};
|
||||||
let varsQueue = [];
|
let varsQueue = [];
|
||||||
let globalPageNumber = 0;
|
let globalPageNumber = 0;
|
||||||
|
|||||||
@@ -300,7 +300,7 @@ describe('Injection: When an injection tag follows an element', ()=>{
|
|||||||
it('Renders a span "text" with its own styles, appended with injected styles', function() {
|
it('Renders a span "text" with its own styles, appended with injected styles', function() {
|
||||||
const source = '{{color:blue,height:10px text}}{width:10px,color:red}';
|
const source = '{{color:blue,height:10px text}}{width:10px,color:red}';
|
||||||
const rendered = Markdown.render(source);
|
const rendered = Markdown.render(source);
|
||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" style="color:blue; height:10px; width:10px; color:red;">text</span>');
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" style="color:red; height:10px; width:10px;">text</span>');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Renders a span "text" with its own classes, appended with injected classes', function() {
|
it('Renders a span "text" with its own classes, appended with injected classes', function() {
|
||||||
@@ -429,7 +429,7 @@ describe('Injection: When an injection tag follows an element', ()=>{
|
|||||||
}}
|
}}
|
||||||
{width:10px,color:red}`;
|
{width:10px,color:red}`;
|
||||||
const rendered = Markdown.render(source).trimReturns();
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<div class="block" style="color:blue; height:10px; width:10px; color:red;"><p>text</p></div>');
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<div class="block" style="color:red; height:10px; width:10px;"><p>text</p></div>');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Renders a span "text" with its own classes, appended with injected classes', function() {
|
it('Renders a span "text" with its own classes, appended with injected classes', function() {
|
||||||
|
|||||||
27
tests/markdown/paragraph-justification.test.js
Normal file
27
tests/markdown/paragraph-justification.test.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
/* eslint-disable max-lines */
|
||||||
|
|
||||||
|
import Markdown from 'naturalcrit/markdown.js';
|
||||||
|
|
||||||
|
describe('Justification', ()=>{
|
||||||
|
test('Left Justify', function() {
|
||||||
|
const source = ':- Hello';
|
||||||
|
const rendered = Markdown.render(source);
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p align=\"Left\">Hello</p>`);
|
||||||
|
});
|
||||||
|
test('Right Justify', function() {
|
||||||
|
const source = '-: Hello';
|
||||||
|
const rendered = Markdown.render(source);
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p align=\"Right\">Hello</p>`);
|
||||||
|
});
|
||||||
|
test('Center Justify', function() {
|
||||||
|
const source = ':-: Hello';
|
||||||
|
const rendered = Markdown.render(source);
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p align=\"Center\">Hello</p>`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Ignored inside a code block', function() {
|
||||||
|
const source = '```\n\n:- Hello\n\n```\n';
|
||||||
|
const rendered = Markdown.render(source);
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<pre><code>\n:- Hello\n</code></pre>\n`);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -402,4 +402,12 @@ describe('Variable names that are subsets of other names', ()=>{
|
|||||||
const rendered = Markdown.render(source).trimReturns();
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
expect(rendered).toBe('<p>14</p>');
|
expect(rendered).toBe('<p>14</p>');
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Regression Tests', ()=>{
|
||||||
|
it('Don\'t Eat all the parentheticals!', function() {
|
||||||
|
const source='\n| title 1 | title 2 | title 3 | title 4|\n|-----------|---------|---------|--------|\n|[foo](bar) | Ipsum | ) | ) |\n';
|
||||||
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
|
expect(rendered).toBe('<table><thead><tr><th>title 1</th><th>title 2</th><th>title 3</th><th>title 4</th></tr></thead><tbody><tr><td><a href=\"bar\">foo</a></td><td>Ipsum</td><td>)</td><td>)</td></tr></tbody></table>');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@@ -505,4 +505,4 @@ body { counter-reset : page-numbers 0; }
|
|||||||
counter-increment : page-numbers;
|
counter-increment : page-numbers;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user