diff --git a/.circleci/config.yml b/.circleci/config.yml
index fb239ceb3..5effc0bb2 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -10,7 +10,7 @@ orbs:
jobs:
build:
docker:
- - image: cimg/node:20.18.0
+ - image: cimg/node:26.4
- image: mongo:4.4
working_directory: ~/homebrewery
@@ -27,7 +27,7 @@ jobs:
# fallback to using the latest cache if no exact match is found
- v1-dependencies-
- - run: sudo npm install -g npm@10.8.2
+ - run: sudo npm install -g npm@11.17.0
- node/install-packages:
app-dir: ~/homebrewery
cache-path: node_modules
@@ -45,7 +45,7 @@ jobs:
test:
docker:
- - image: cimg/node:20.17.0
+ - image: cimg/node:26.4
working_directory: ~/homebrewery
parallelism: 1
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 2204679a6..8915c39dd 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -66,10 +66,6 @@ updates:
- dependency-name: "@babel/preset-react"
versions:
- 7.13.13
- - dependency-name: codemirror
- versions:
- - 5.59.3
- - 5.60.0
- dependency-name: classnames
versions:
- 2.3.0
diff --git a/Dockerfile b/Dockerfile
index 17d02b01f..023644346 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,4 @@
-FROM node:22-alpine
+FROM node:26.4.0-alpine
RUN apk --no-cache add git
ENV NODE_ENV=docker
diff --git a/README.DOCKER.md b/README.DOCKER.md
index 4dfbef045..dba2e2257 100644
--- a/README.DOCKER.md
+++ b/README.DOCKER.md
@@ -47,9 +47,7 @@ Make an changes you need to `config/docker.json` then build the image. If it doe
"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,
}
```
@@ -90,6 +88,13 @@ docker run --name homebrewery-mongodb -d --restart unless-stopped -v mongodata:/
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
```
+**NOTE:** If you are running from the Windows command line, this will not work as `$(pwd)` is not valid syntax. Use this command instead:
+```shell
+# Make sure you run this in the homebrewery directory
+docker run --name homebrewery-app -d --restart unless-stopped -e NODE_ENV=docker -v %cd%/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.
@@ -117,3 +122,9 @@ 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
```
+**NOTE:** If you are running from the Windows command line, this will not work as `$(pwd)` is not valid syntax. Use this command instead:
+```shell
+# Make sure you run this in the homebrewery directory
+docker run --name homebrewery-app -d --restart unless-stopped -e NODE_ENV=docker -v %cd%/config/docker.json:/usr/src/app/config/docker.json -p 8000:8000 docker.io/library/homebrewery:latest
+```
+
diff --git a/README.md b/README.md
index 5206f4cbf..a33f2f073 100644
--- a/README.md
+++ b/README.md
@@ -75,8 +75,9 @@ it using the two commands:
1. `npm install`
1. `npm start`
-You should now be able to go to [http://localhost:8000](http://localhost:8000)
-in your browser and use The Homebrewery offline.
+When the Homebrewery server is started for the first time, it will modify the database to create the indexes required for better Homebrewery performance. This may take a few moments to complete for each index, dependent on how much content is in your local database - a brand new, empty database should be done in seconds.
+
+On completion, you should be able to go to [http://localhost:8000](http://localhost:8000) in your browser and use The Homebrewery offline.
If you had any issue at all, here are some links that may be useful:
- [Course](https://learn.mongodb.com/courses/m103-basic-cluster-administration) on cluster administration, useful for beginners
@@ -145,3 +146,4 @@ your contribution to the project, please join our [gitter chat][gitter-url].
[github-pr-docs-url]: https://docs.github.com/en/free-pro-team@latest/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request
[gitter-url]: https://gitter.im/naturalcrit/Lobby
+
diff --git a/changelog.md b/changelog.md
index 0a108b9f2..d783130f0 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,2276 +1,2454 @@
-```css
-.beta {
- color : white;
- padding : 4px 6px;
- line-height : 1em;
- background : grey;
- border-radius : 12px;
- font-family : monospace;
- font-size : 10px;
- font-weight : 800;
- margin-top : -5px;
- margin-bottom : -5px;
-}
-
-.fac {
- height: 1em;
- line-height: 2em;
- margin-bottom: -0.05cm
-}
-
-h5 {
- font-size: .35cm !important;
-}
-
-.page ul ul {
- margin-left: 0px;
-}
-
-.page .taskList {
- display:block;
- break-inside:auto;
-}
-
-.taskList li input {
- list-style-type : none;
- margin-left : -0.52cm;
- transform: translateY(.05cm);
- filter: brightness(1.1) drop-shadow(1px 2px 1px #222);
-}
-
-.taskList ul {
- margin-bottom: 0px;
- margin-top: 0px;
-}
-
-.taskList li input[checked] {
- filter: sepia(100%) hue-rotate(60deg) saturate(3.5) contrast(4) brightness(1.1) drop-shadow(1px 2px 1px #222);
-}
-
-pre + * {
- margin-top: 0.17cm;
-}
-
-pre {
- margin-top: 0.17cm;
-}
-
-.page p + pre {
- margin-top : 0.1cm;
-}
-
-.page ul + h5 {
- margin-top: 0.25cm;
-}
-
-.page p + h5 {
- margin-top: 0.25cm;
-}
-
-.page .openSans {
- font-family: 'Open Sans';
- font-size: 0.9em;
-}
-
-.page {
- padding-bottom: 1.5cm;
-}
-
-.varSyntaxTable th:first-of-type {
- width:6cm;
-}
-
-.page .exampleTable td,th {
- border:1px dashed #00000030;
-}
-```
-
-## changelog
-For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery).
-
-### Wednesday 7/09/2025 - v3.19.3
-
-{{taskList
-##### calculuschild
-* [x] Restoring original saving behavior; will continue investigating why save was failing for some users in background
-}}
-
-
-### Wednesday 7/09/2025 - v3.19.2
-
-{{taskList
-##### calculuschild
-* [x] Hotfix for saving issues - Please refresh your browser and report if problems continue
-}}
-
-### Wednesday 7/09/2025 - v3.19.1
-
-{{taskList
-##### calculuschild
-* [x] Send diffs instead of full file on save - should help with timeout/disconnect errors
-}}
-
-\column
-
-### Thursday 05/22/2025 - v3.19.0
-
-{{taskList
-##### abquintic
-* [x] Fix crash due to colons after `\page`
-
-Fixes issue [#4105](https://github.com/naturalcrit/homebrewery/issues/4105)
-
-* [x] Fix images with spaces in alt text not rendering
-
-Fixes issue [#3659](https://github.com/naturalcrit/homebrewery/issues/3659)
-
-* [x] Custom snippets! Open the new {{openSans **:fas_table_list: SNIPPETS**}} tab (next to the {{openSans **:fas_paintbrush: STYLE**}} tab). Custom snippets will appear in a new snippet dropdown, and will be included when imported as a custom theme.
-
-* [x] Move several generic styles/snippets from PHB to the Blank theme; generic snippets like image masks no longer require the PHB theme.
-
-* [x] Extract several Markdown+ syntax extensions into their own NPM packages, for use by the wider community.
-
-* [x] Allow `\pagebreak` and `\columnbreak` as alternatives to `\page` and `\column`
-
-Partially fixes issue [#4035](https://github.com/naturalcrit/homebrewery/issues/4035)
-
-* [x] Fix misbehaving column breaks on old Chrome
-
-Fixes issue [#4192](https://github.com/naturalcrit/homebrewery/issues/4192)
-
-* [x] Self-host font-awesome icons; fix missing icons on local installs
-
-Fixes issue [#1965](https://github.com/naturalcrit/homebrewery/issues/1965)
-Fixes issue [#1548](https://github.com/naturalcrit/homebrewery/issues/1548)
-
-##### G-Ambatte
-* [x] Fix CORS issue on local installs
-
-* [x] Fix print size issues when using the Facing and Flow view options.
-
-Fixes issue [#4146](https://github.com/naturalcrit/homebrewery/issues/4146)
-
-* [x] New built-in `$[HB_pageNumber]` variable. Works with math operations or can be reassigned like any other variable for more customization over the old `{{pageNumber,auto}}` snippet.\
-New snippet found at {{openSans **:fas_pencil: TEXT EDITOR :fas_arrow_right: :fas_bookmark: PAGE NUMBERING :fas_arrow_right: :fas_arrow_down_1_9: VARIABLE AUTO PAGE NUMBER**}}
-
-##### 5e-Cleric
-* [x] Fix search bar covering up snippet bar (3 times)
-
-Fixes issue [#4098](https://github.com/naturalcrit/homebrewery/issues/4098)
-
-* [x] Save view toolbar settings across sessions
-
-Fixes issue [#3835](https://github.com/naturalcrit/homebrewery/issues/3835)
-
-* [x] Fix styling issues on the view toolbar
-
-* [x] Update the Darkbrewery editor theme
-
-Fixes issue [#3312](https://github.com/naturalcrit/homebrewery/issues/3312)
-
-}}
-
-\page
-
-### Monday 03/10/2025 - v3.18.0
-
-{{taskList
-##### abquintic
-* [x] Add ability to paste in any Share ID/URL into a brew's {{openSans :fas_circle_info: **Properties** :fas_arrow_right: **THEMES**}} selection, as long as that brew has been tagged as `meta:theme`. You can now share your custom brew themes without needing to make a personal copy.
-* [x] Begin migration of custom Markdown extensions into their own NPM packages, for easier adoption by other users or projects
-* [x] Fix external HTML appearing in open codeblocks
-
-Fixes issue [#3206](https://github.com/naturalcrit/homebrewery/issues/3206)
-
-* [x] Fix tables not rendering when directly after text
-
-
-##### G-Ambatte
-* [x] Cleanup of "cover pages" in the {{openSans :fas_rectangle_list: **NAVIGATION**}} list
-* [x] Fix autosave triggering when no changes are present
-
-Fixes issue [#4051](https://github.com/naturalcrit/homebrewery/issues/4051)
-
-* [x] Remove empty table rows resulting from rowspan
-
-Fixes issue [#1729](https://github.com/naturalcrit/homebrewery/issues/1729)
-
-##### 5e-Cleric
-* [x] Style fixes for covers art and logos on A4 size pages
-* [x] Fix crash when trying to open brews that don't exist
-* [x] Tweaks and style update styling on {{openSans **VAULT** :fas_dungeon:}} page.
-
-Fixes issue [#4079](https://github.com/naturalcrit/homebrewery/issues/4079)
-
-##### Calculuschild
-* [x] `꞉꞉꞉꞉` now produces `
` instead of a `
:\:
-}}
-
-
-### Thursday 17/08/2023 - v3.9.2
-{{taskList
-
-##### calculuschild
-
-* [x] Fix links to certain old Google Drive files
-
-Fixes issue [#2917](https://github.com/naturalcrit/homebrewery/issues/2917)
-
-##### G-Ambatte
-
-* [x] Menus now open on click, and internally consistent
-
-Fixes issue [#2702](https://github.com/naturalcrit/homebrewery/issues/2702), [#2782](https://github.com/naturalcrit/homebrewery/issues/2782)
-
-* [x] Add smarter footer snippet
-
-Fixes issue [#2289](https://github.com/naturalcrit/homebrewery/issues/2289)
-
-* [x] Add sanitization in Style editor
-
-Fixes issue [#1437](https://github.com/naturalcrit/homebrewery/issues/1437)
-
-* [x] Rework class table snippets to remove unnecessary randomness
-
-Fixes issue [#2964](https://github.com/naturalcrit/homebrewery/issues/2964)
-
-* [x] Add User Page link to Google Drive file for file owners, add icons for additional storage locations
-
-Fixes issue [#2954](https://github.com/naturalcrit/homebrewery/issues/2954)
-
-* [x] Add default save location selection to Account Page
-
-Fixes issue [#2943](https://github.com/naturalcrit/homebrewery/issues/2943)
-
-##### 5e-Cleric
-
-* [x] Exclude cover pages from Table of Content generation (editing on mobile is still not recommended)
-
-Fixes issue [#2920](https://github.com/naturalcrit/homebrewery/issues/2920)
-
-##### Gazook89
-
-* [x] Adjustments to improve mobile viewing
-}}
-
-
-
-### Wednesday 28/06/2023 - v3.9.1
-{{taskList
-
-##### G-Ambatte
-
-* [x] Better error pages with more useful information
-
-Fixes issue [#1924](https://github.com/naturalcrit/homebrewery/issues/1924)
-}}
-
-### Friday 02/06/2023 - v3.9.0
-{{taskList
-
-##### calculuschild
-
-* [x] Fix some files not showing up on userpage when user has a large number of brews in Google Drive
-
-Fixes issue [#2408](https://github.com/naturalcrit/homebrewery/issues/2408)
-
-* [x] Pressing tab now indents with spaces instead of tab character; fixes several issues with Markdown lists
-
-Fixes issues [#2092](https://github.com/naturalcrit/homebrewery/issues/2092), [#1556](https://github.com/naturalcrit/homebrewery/issues/1556)
-
-* [x] Rename `naturalCritLogo.svg` to `naturalCritLogoRed.svg`. Those using the {{beta BETA}} coverPage snippet may need to update that text to make the NaturalCrit logo appear again.
-
-##### G-Ambatte
-
-* [x] Fix strange animation of image masks
-
-Fixes issue [#2790](https://github.com/naturalcrit/homebrewery/issues/2790)
-
-##### 5e-Cleric
-
-* [x] New {{openSans **PHB → {{fac,book-part-cover}} PART COVER PAGE** }} snippet for V3!
-
-* [x] New {{openSans **PHB → {{fac,book-back-cover}} BACK COVER PAGE** }} snippet for V3! (Thanks to /u/Kaiburr_Kath-Hound on Reddit for providing some of these resources!)
-
-* [x] New {{openSans **TEXT EDITOR → {{fas,fa-bars}} INDEX** }} snippet for V3!
-
-* [x] Fix highlighting of curly braces inside comments
-
-Fixes issue [#2784](https://github.com/naturalcrit/homebrewery/issues/2784)
-}}
-
-\page
-
-### Wednesday 12/04/2023 - v3.8.0
-{{taskList
-
-##### calculuschild
-
-* [x] Rename `{{coverPage}}` to `{{frontCover}}`. Those using this {{beta BETA}} feature will need to update that text to make the cover page appear again.
-
-* [x] Several background fixes to test scripts
-
-##### Jeddai
-
-* [X] Add content negotiation to exclude image requests from our API calls
-
-Fixes issue [#2595](https://github.com/naturalcrit/homebrewery/issues/2595)
-
-##### G-Ambatte
-
-* [x] Update server build scripts to fix Admin page
-
-Fixes issues [#2657](https://github.com/naturalcrit/homebrewery/issues/2657)
-
-* [x] Fix internal links inside `<\div>` blocks not receiving the `target=_self` attribute
-
-Fixes issues [#2680](https://github.com/naturalcrit/homebrewery/issues/2680)
-
-* [x] See brew details on `/share` pages by clicking the brew title (author, last update, tags, etc.)
-
-Fixes issues [#1679](https://github.com/naturalcrit/homebrewery/issues/1679)
-
-* [x] Add local Windows install script via Chocolatey
-
-##### 5e-Cleric
-
-* [x] New {{openSans **TABLES → {{fas,fa-language}} RUNE TABLE**}} snippets for V3. Adds an alphabetic script translation table.
-
-* [x] New {{openSans **IMAGES → {{fac,mask-center}} WATERCOLOR CENTER** }} snippets for V3, which adds a stylish watercolor texture to the center of your images!
-
-* [x] New {{openSans **PHB → {{fac,book-inside-cover}} INSIDE COVER PAGE** }} snippet for V3! (Thanks to /u/Kaiburr_Kath-Hound on Reddit for providing some of these resources!)
-
-* [x] Add some missing characters {{font-family:scalySansRemake Ñ ñ ç Ç Ý ý # ^ ¿ ' " ¡ ·}} to the "scalySansRemake" font in V3.
-
-Fixes issues [#2280](https://github.com/naturalcrit/homebrewery/issues/2280)
-
-##### Gazook89
-
-* [x] Add "Language" selector in {{fa,fa-info-circle}} **Properties** menu. Sets the HTML Lang attribute for your brew to better handle hyphenation or spellcheck.
-
-Fixes issues [#1343](https://github.com/naturalcrit/homebrewery/issues/1343)
-
-* [x] Fix a crash when multiple `{injection}` tags appear in sequence
-
-Fixes issues [#2712](https://github.com/naturalcrit/homebrewery/issues/2712)
-
-##### MichielDeMey
-
-* [x] Remove all-caps display on Account button since usernames are case-sensitive.
-
-Fixes issues [#2731](https://github.com/naturalcrit/homebrewery/issues/2731)
-
-}}
-
-### Monday 13/03/2023 - v3.7.2
-{{taskList
-
-##### calculuschild
-
-* [x] Fix wide Monster Stat Blocks not spanning columns on Legacy
-}}
-
-### Thursday 09/03/2023 - v3.7.1
-{{taskList
-
-##### Lucastucious (new contributor!)
-
-* [x] Changed `filter: drop-shadow` to `box-shadow` on text boxes, making PDF text selectable
-
-Fixes issues [#1569](https://github.com/naturalcrit/homebrewery/issues/1569)
-
-{{note
-**NOTE:** If you create your PDF on a computer with an old version of Mac Preview (v10 or older) you may see shadows appear as solid gray.
-}}
-
-##### MichielDeMey
-
-* [x] Updated the Google Drive icon
-* [x] Backend fix to unit tests failing intermittently
-
-##### calculuschild
-
-* [x] Fix PDF pixelation on CoverPage text outlines
-}}
-
-### Tuesday 28/02/2023 - v3.7.0
-{{taskList
-
-{{note
-**NOTE:** Some new snippets will now show a {{beta BETA}} tag. Feel free to use them, but be aware we may change how they work depending on your feedback.
-}}
-
-##### calculuschild
-
-* [x] New {{openSans **IMAGES → WATERCOLOR EDGE** {{fac,mask-edge}} }} and {{openSans **WATERCOLOR CORNER** {{fac,mask-corner}} }} snippets for V3, which adds a stylish watercolor texture to the edge of your images! (Thanks to /u/flamableconcrete on Reddit for providing these image masks!)
-
-* [x] Fix site not displaying on iOS devices
-
-##### 5e-Cleric
-
-* [x] New {{openSans **PHB → COVER PAGE** {{fac,book-front-cover}} }} snippet for V3, which adds a stylish coverpage to your brew! (Thanks to /u/Kaiburr_Kath-Hound on Reddit for providing some of these resources!)
-
-##### MichielDeMey (new contribuor!)
-
-* [x] Fix typo in testing scripts
-* [x] Fix "mug" image not using HTTPS
-
-Fixes issues [#2687](https://github.com/naturalcrit/homebrewery/issues/2687)
-}}
-
-### Saturday 18/02/2023 - v3.6.1
-{{taskList
-##### G-Ambatte
-
-* [x] Fix users not being removed from Authors list
-
-Fixes issues [#2674](https://github.com/naturalcrit/homebrewery/issues/2674)
-}}
-
-### Monday 23/01/2023 - v3.6.0
-{{taskList
-##### calculuschild
-
-* [x] Fix Google Drive brews sometimes duplicating
-
-Fixes issues [#2603](https://github.com/naturalcrit/homebrewery/issues/2603)
-
-##### Jeddai
-
-* [x] Add unit tests with full coverage for the Homebrewery API
-
-* [x] Add message to refresh the browser if the user is missing an update to the Homebrewery
-
-Fixes issues [#2583](https://github.com/naturalcrit/homebrewery/issues/2583)
-}}
-
-\page
-
-{{taskList
-##### G-Ambatte
-
-* [x] Auto-compile Themes CSS on development server
-
-##### 5e-Cleric
-
-* [x] Fix cloned brews inheriting the parent view count
-}}
-
-
-### Friday 23/12/2022 - v3.5.0
-{{taskList
-
-##### Jeddai
-
-* [x] Only brew owners or invited authors can edit a brew
-
- - Visiting an `/edit` page of a brew that does not list you as an author will result in an error page. Authors can be added to any brew by opening its {{fa,fa-info-circle}} **Properties** menu and typing the author's username (case-sensitive) into the **Invited Authors** bubble.
- - Warn user if a newer brew version has been saved on another device
-
-Fixes issues [#1987](https://github.com/naturalcrit/homebrewery/issues/1987)
-}}
-
-### Saturday 10/12/2022 - v3.4.2
-{{taskList
-
-##### Jeddai
-
-* [x] Fix broken tags editor
-
-* [x] Reduce server load to fix some saving issues
-
-Fixes issues [#2322](https://github.com/naturalcrit/homebrewery/issues/2322)
-
-##### G-Ambatte
-
-* [x] Account page help link for Google Drive errors
-
-Fixes issues [#2520](https://github.com/naturalcrit/homebrewery/issues/2520)
-}}
-
-### Monday 05/12/2022 - v3.4.1
-{{taskList
-
-##### G-Ambatte
-
-* [x] Fix Account page incorrect last login time
-
-Fixes issues [#2521](https://github.com/naturalcrit/homebrewery/issues/2521)
-
-##### Gazook
-
-* [x] Fix crashing on iOS and Safari browsers
-
-Fixes issues [#2531](https://github.com/naturalcrit/homebrewery/issues/2531)
-}}
-
-### Monday 28/11/2022 - v3.4.0
-{{taskList
-
-##### G-Ambatte
-
-* [x] Fix for Chrome v108 handling of page size
-
-Fixes issues [#2445](https://github.com/naturalcrit/homebrewery/issues/2445), [#2516](https://github.com/naturalcrit/homebrewery/issues/2516)
-
-* [x] New account page with some user info, at {{openSans **USERNAME {{fa,fa-user}} → ACCOUNT {{fa,fa-user}}**}}
-
-Fixes issues [#2049](https://github.com/naturalcrit/homebrewery/issues/2049), [#2043](https://github.com/naturalcrit/homebrewery/issues/2043)
-
-* [x] Fix "Published/Private Brews" buttons on userpage
-
-Fixes issues [#2449](https://github.com/naturalcrit/homebrewery/issues/2449)
-
-##### Gazook
-
-* [x] Make autosave default on for new users
-
-* [x] Added link to our FAQ at {{openSans **NEED HELP? {{fa,fa-question-circle}} → FAQ {{fa,fa-question-circle}}**}}
-
-* [x] Fix curly blocks freezing with long property lists
-
-Fixes issues [#2393](https://github.com/naturalcrit/homebrewery/issues/2393)
-
-* [x] Items can now be removed from {{openSans **RECENT BREWS** {{fas,fa-history}} }}
-
-Fixes issues [#1918](https://github.com/naturalcrit/homebrewery/issues/1918)
-
-* [x] Curly injector syntax `{blue}` highlighting in editor
-
-Fixes issues [#1670](https://github.com/naturalcrit/homebrewery/issues/1670)
-
-}}
-
-### Thursday 28/10/2022 - v3.3.1
-{{taskList
-
-##### calculuschild
-
-* [x] Fixes to several broken CSS styles from v3.3.0
-
-Fixes issues [#2468](https://github.com/naturalcrit/homebrewery/issues/2468)
-
-##### Jeddai
-
-* [x] Reduce size of thumbnails on social media links
-
-}}
-
-### Friday 19/10/2022 - v3.3.0
-{{taskList
-
-##### calculuschild
-
-* [x] Fix for tables broken by Chrome v106
-
-
-##### G-Ambatte:
-
-* [x] Fix Table of Contents broken by Chrome v106
-
-Fixes issues [#2437](https://github.com/naturalcrit/homebrewery/issues/2437)
-
-* [x] Show brew thumbnails on user page
-
-Fixes issues [#2331](https://github.com/naturalcrit/homebrewery/issues/2331)
-
-* [x] Allow longer URLs for brew thumbnails
-
-Fixes issues [#2351](https://github.com/naturalcrit/homebrewery/issues/2351)
-
-* [x] Code no longer unfolds when inserting a snippet
-
-Fixes issues [#2135](https://github.com/naturalcrit/homebrewery/issues/2135)
-
-* [x] Fix brew settings being lost on first save
-
-Fixes issues [#2427](https://github.com/naturalcrit/homebrewery/issues/2427)
-
-##### Gazook:
-
-* [x] Several updates to bug reporting and error popups
-
-Fixes issues [#2376](https://github.com/naturalcrit/homebrewery/issues/2376)
-
-* [x] Fixes to userpage search bar
-
-Fixes issues [#1675](https://github.com/naturalcrit/homebrewery/issues/1675), [#2353](https://github.com/naturalcrit/homebrewery/issues/2353)
-
-* [x] Renderer *(legacy / V3)* now shown next to page #
-
-Fixes issues [#1928](https://github.com/naturalcrit/homebrewery/issues/1928)
-
-* [x] Prevent text selection when moving divider bar
-
-Fixes issues [#1632](https://github.com/naturalcrit/homebrewery/issues/1632)
-
-* [x] Tweak Monster Stat Block coloring
-
-Fixes issues [#2123](https://github.com/naturalcrit/homebrewery/issues/2123)
-
-* [x] Added dropdown button to toggle autosave
-
-Fixes issues [#1546](https://github.com/naturalcrit/homebrewery/issues/1546)
-
-}}
-
-
-### Friday 08/09/2022 - v3.2.2
-{{taskList
-
-##### Jeddai:
-
-* [x] Fix brews not deleting from User page when removed from Google Drive externally.
-
- Fixes issues: [#2325](https://github.com/naturalcrit/homebrewery/issues/2325)
-
-##### G-Ambatte:
-
-* [x] Brew Tags are now searchable on the User page
-
-Fixes issues [#2317](https://github.com/naturalcrit/homebrewery/issues/2317), [#2319](https://github.com/naturalcrit/homebrewery/issues/2319), [#2334](https://github.com/naturalcrit/homebrewery/issues/2334)
-
-* [x] Several tweaks to the User page
-
- Fixes issues: [#1797](https://github.com/naturalcrit/homebrewery/issues/1797), [#2315](https://github.com/naturalcrit/homebrewery/issues/2315), [#2326](https://github.com/naturalcrit/homebrewery/issues/2326), [#2328](https://github.com/naturalcrit/homebrewery/issues/2328)
-}}
-
-
-
-\page
-
-### Wednesday 31/08/2022 - v3.2.1
-{{taskList
-
-##### calculuschild
-
-* [x] Reference Links should now work inside tables
-
- Fixes issues: [#2307](https://github.com/naturalcrit/homebrewery/issues/2307)
-
-##### Jeddai:
-
-* [x] Fix printing from `/new` not working
-
- Fixes issues: [#1789](https://github.com/naturalcrit/homebrewery/issues/1789), [#1806](https://github.com/naturalcrit/homebrewery/issues/1806)
-
-* [x] Fix broken snippet buttons on `/new`
-
- Fixes issues: [#2311](https://github.com/naturalcrit/homebrewery/issues/2311)
-
-##### G-Ambatte:
-
-* [x] Several small tweaks to the User page
-
- Fixes issues: [#2301](https://github.com/naturalcrit/homebrewery/issues/2301), [#2303](https://github.com/naturalcrit/homebrewery/issues/2303), [#2121](https://github.com/naturalcrit/homebrewery/issues/2121)
-}}
-
-### Saturday 27/08/2022 - v3.2.0
-{{taskList
-
-##### calculuschild
-
-* [x] The V3 renderer is now the default for new brews.
-
-* [x] Small tweaks to the spacing around the `classTable` style
-
-##### Jeddai:
-
-* [x] Brew transfers between Homebrewery and Google Drive now keep the same share and edit links! Metadata is now also kept across transfers.
-
- Fixes issues: [#1838](https://github.com/naturalcrit/homebrewery/issues/1838)
-
-* [x] Brews can now be labeled with tags; these will be searchable on the My Brews page in a future update.
-
- Fixes issues: [#758](https://github.com/naturalcrit/homebrewery/issues/758)
-
-##### Jlgraves:
-
-* [x] Small tweaks to the `ClassFeature` snippet
-
- Fixes issues: [#2215](https://github.com/naturalcrit/homebrewery/issues/2215)
-}}
-
-
-### Thursday 09/06/2022 - v3.1.1
-{{taskList
-
-##### calculuschild:
-
-* [x] Fixed class table decorations appearing on top of the table in PDF output.
-
- Fixes issues: [#1784](https://github.com/naturalcrit/homebrewery/issues/1784)
-
-* [x] Fix bottom decoration on half class tables disappearing when the table is too short.
-
- Fixes issues: [#2202](https://github.com/naturalcrit/homebrewery/issues/2202)
-}}
-
-### Monday 06/06/2022 - v3.1.0
-{{taskList
-
-##### G-Ambatte:
-
-* [x] "Jump to Preview/Editor" buttons added to the divider bar. Easily sync between the editor and preview panels!
-
- Fixes issues: [#1756](https://github.com/naturalcrit/homebrewery/issues/1756)
-
-* [x] Speedups to the user page for users with large and/or many brews.
-
- Fixes issues: [#2147](https://github.com/naturalcrit/homebrewery/issues/2147)
-
-* [x] Search text on the user page is saved to the URL for easy bookmarking in your browser
-
- Fixes issues: [#1858](https://github.com/naturalcrit/homebrewery/issues/1858)
-
-* [x] Added easy login system for offline installs.
-
- Fixes issues: [#269](https://github.com/naturalcrit/homebrewery/issues/269)
-
-* [x] New **THUMBNAIL** option in the {{fa,fa-info-circle}} **Properties** menu. This image will show up in social media links.
-
- Fixes issues: [#820](https://github.com/naturalcrit/homebrewery/issues/820)
-}}
-
-### Wednesday 27/03/2022 - v3.0.8
-{{taskList
-* [x] Style updates to user page.
-
-* [x] Added a logout button (finally)! You can find it under {{openSans **USERNAME {{fa,fa-user}} → LOGOUT {{fas,fa-power-off}}**}}
-
- Fixes issues: [#303](https://github.com/naturalcrit/homebrewery/issues/303)
-
-* [x] Clarified the default text when submitting an issue via Reddit post.
-
-* [x] Fixed broken Table of Contents links in PDFs. (Thanks lucastucious!)
-
- Fixes issues: [#1749](https://github.com/naturalcrit/homebrewery/issues/1749)
-
-* [x] Fixed window resizing causing the edit page divider to get lost off of the edge of the page.
-
- Fixes issues: [#2053](https://github.com/naturalcrit/homebrewery/issues/2053)
-
-* [x] Fixed Class Table decorations overlapping main text.
-
- Fixes issues: [#1985](https://github.com/naturalcrit/homebrewery/issues/1985)
-
-* [x] Updated {{openSans **STYLE EDITOR {{fa,fa-pencil-alt}} → REMOVE DROP CAP {{fas,fa-remove-format}}**}} snippet to also remove small-caps first line font.
-
-* [x] Background work in preparation for brew themes.
-}}
-
-### Wednesday 02/02/2022 - v3.0.7
-{{taskList
-* [x] Revert active line highlighting.
-
- Fixes issues: [#1913](https://github.com/naturalcrit/homebrewery/issues/1913)
-
-* [x] Added install steps for Ubuntu. [HERE](https://github.com/naturalcrit/homebrewery/blob/master/install/README.UBUNTU.md)
-
- Fixes issues: [#1900](https://github.com/naturalcrit/homebrewery/issues/1900)
-
-* [x] Added social media links to home page.
-
-* [x] Increase brews visible on the user page to 1,000.
-
- Fixes issues: [#1943](https://github.com/naturalcrit/homebrewery/issues/1943)
-
-* [x] Added a Legacy to V3 migration guide under {{openSans **NEED HELP? {{fa,fa-question-circle}} → MIGRATE {{fas,fa-file-import}}**}}
-
-* [x] Background refactoring and unit tests.
-}}
-
-### Saturday 18/12/2021 - v3.0.6
-{{taskList
-* [x] Fixed text wrapping for long strings in code blocks.
-
- Fixes issues: [#1736](https://github.com/naturalcrit/homebrewery/issues/1736)
-
-* [x] Code search/replace PC: `CTRL F / CTRL SHIFT F` / Mac: `CMD F / OPTION CMD F`
-
- Fixes issues: [#1201](https://github.com/naturalcrit/homebrewery/issues/1201)
-
-* [x] Auto-closing HTML tags and curly braces `{{ }}`
-* [x] Highlight current active line
-
- Fixes issues: [#1202](https://github.com/naturalcrit/homebrewery/issues/1202)
-
-* [x] Display tabs and trailing spaces
-
- Fixes issues: [#1622](https://github.com/naturalcrit/homebrewery/issues/1622)
-
-* [x] Make columns even in V3 Table of Contents.
-
- Fixes issues: [#1671](https://github.com/naturalcrit/homebrewery/issues/1671)
-
-* [x] Fix `CTRL P` failing to print from `/new` pages.
-
- Fixes issues: [#1815](https://github.com/naturalcrit/homebrewery/issues/1815)
-}}
-
-\page
-
-### Tuesday 07/12/2021 - v3.0.5
-{{taskList
-* [x] Fixed paragraph spacing for **note** and **descriptive** boxes in V3.
-
- Fixes issues: [#1836](https://github.com/naturalcrit/homebrewery/issues/1836)
-
-* [x] Added a whole bunch of hotkeys:
-
- * Page Break `CTRL + ENTER`
- * Column Break `CTRL + SHIFT + ENTER`
- * Bulleted Lists `CTRL + L`
- * Numbered Lists `CTRL + SHIFT + L`
- * Headers `CTRL + SHIFT + (1-6)`
- * Underline `CTRL + U`
- * Link `CTRL + K`
- * Non-breaking space (\ ) `CTRL + .`
- * Add Horizontal Space `CTRL + SHIFT + .`
- * Remove Horizontal Space `CTRL + SHIFT + ,`
- * Curly Span `CTRL + M`
- * Curly Div `CTRL + SHIFT + M`
-
-* [x] Fixed page numbers in the editor panel getting scrambled when scrolling up and down.
-
-* [x] Faster swapping between tabs on long brews.
-
-* [x] Better error messages for common issue with Google Drive credentials expiring.
-}}
-
-### Wednesday 17/11/2021 - v3.0.4
-{{taskList
-* [x] Fixed incorrect sorting of Google brews by page count and views on the user page.
-
- Fixes issues: [#1793](https://github.com/naturalcrit/homebrewery/issues/1793)
-
-* [x] Added code folding! Only on a page-level for now. Hotkeys `CTRL + [` and `CTRL + ]` to fold/unfold all pages. (Thanks jeddai, new contributor!)
-
- Fixes issues: [#629](https://github.com/naturalcrit/homebrewery/issues/629)
-
-* [x] Fixed rendering issues due to the latest Chrome update to version 96. (Also thanks to jeddai!)
-
- Fixes issues: [#1828](https://github.com/naturalcrit/homebrewery/issues/1828)
-}}
-
-### Wednesday 27/10/2021 - v3.0.3
-
-{{taskList
-* [x] Moved **Post To Reddit** button from {{fa,fa-info-circle}} **Properties** menu to the **SHARE** {{fa,fa-share-alt}} button as a dropdown.
-
-* [x] Added a **Copy URL** button to the **SHARE** {{fa,fa-share-alt}} button as a dropdown.
-
-* [x] Fixed pages being printed directly from `/new` not recognizing the V3 renderer.
-
- Fixes issues: [#1702](https://github.com/naturalcrit/homebrewery/issues/1702)
-
-* [x] Updated links to [r/UnearthedArcana](https://www.reddit.com/r/UnearthedArcana/) on home page.
-
- Fixes issues: [#1744](https://github.com/naturalcrit/homebrewery/issues/1744)
-
-* [x] Added a [FAQ page](https://homebrewery.naturalcrit.com/faq).
-
- Fixes issues: [#810](https://github.com/naturalcrit/homebrewery/issues/810)
-
-* [x] Added {{fa,fa-undo}} **Undo** and {{fa,fa-redo}} **Redo** buttons to the snippet bar.
-
-}}
-
-\column
-
-{{taskList
-
-* [x] Switching between the {{fa,fa-beer}} **Brew** and {{fa,fa-paint-brush}} **Style** tabs no longer loses your scroll position or undo history.
-
- Fixes issues: [#1735](https://github.com/naturalcrit/homebrewery/issues/1735)
-
-* [x] Divider bar between editor and preview panels can no longer be dragged off the edge of the screen.
-
- Fixes issues: [#1674](https://github.com/naturalcrit/homebrewery/issues/1674)
-}}
-
-
-### Wednesday 06/10/2021 - v3.0.2
-
-{{taskList
-* [x] Fixed V3 **EDITOR → QR Code** snippet not working on `/new` (unsaved) pages.
-
- Fixes issues: [#1710](https://github.com/naturalcrit/homebrewery/issues/1710)
-
-* [x] Reorganized several snippets from the **Brew Editor** panel into the **Style Editor** panel.
-
- Fixes issues: [Reported on Reddit](https://www.reddit.com/r/homebrewery/comments/pm6ki7/two_version_of_class_features_making_it_look_more/)
-
-* [x] Added a page counter to the right of each `\page` line in V3 to help navigate your brews. Starts counting from page 2.
-
- Fixes issues: [#846](https://github.com/naturalcrit/homebrewery/issues/846)
-
-* [x] Moved the changelog to be accessible by clicking on the Homebrewery version number.
-
- Fixes issues: [#1166](https://github.com/naturalcrit/homebrewery/issues/1166)
-}}
-
-### Friday, 17/09/2021 - v3.0.1
-
-{{taskList
-* [x] Updated V3 **PHB → Class Feature** snippet to use V3 syntax.
-
- Fixes issues: [Reported on Reddit](https://www.reddit.com/r/homebrewery/comments/pm6ki7/two_version_of_class_features_making_it_look_more/)
-
-* [x] Improved V3 **PHB → Monster Stat Block** snippet and styling to allow for easier control of paragraph indentation in the Abilities text.
-
- Fixes issues: [#181](https://github.com/naturalcrit/homebrewery/issues/181)
-
-* [x] Improved Legacy **TABLES → Split Table** snippet by removing unneeded column-break backticks.
-
- Fixes issues: [#844](https://github.com/naturalcrit/homebrewery/issues/844)
-
-* [x] Changed block elements to use CSS `width` instead of `min-width`. This should make custom styles behave more predictably when trying to resize items.
-
- Fixes issues: [Reported on Reddit](https://www.reddit.com/r/homebrewery/comments/pohoy3/looking_for_help_with_basic_stuff_in_v3/)
-
-* [x] Fixed Partial Page Rendering in V3 for large brews
-
- Fixes issues: [Reported on Reddit](https://www.reddit.com/r/homebrewery/comments/pori3a/weird_behaviour_of_the_brew_after_page_50/)
-
-* [x] Fixed HTML validation to handle tags starting with 'a', as in `<aside>`.
-
- Fixes issues: [#230](https://github.com/naturalcrit/homebrewery/issues/230)
-
-* [x] Fixed page footers switching side when printing.
-
- Fixes issues: [#1612](https://github.com/naturalcrit/homebrewery/issues/1612)
-}}
-
-
-\page
-
-### Saturday, 11/09/2021 - v3.0.0
-
-We have been working on v3 for a *very* long time. We want to thank everyone for being paitent.
-
-
-Some features planned for V3 have actually been released over the recent months as part of V2, and some are still on the way. But at its core, V3 provides brand new Markdown-to-Brew rendering system, which was no simple task. This has opened up access to all sorts of bugfixes, tweaks, and potential for new features that just wouldn't be possible on the old system.
-
-***BE WARNED:*** As we continue to develop V3, expect small tweaks in the styling, fonts, and snippets; your brews may look slightly different from day-to-day; some things might break completely while we tackle any bugs in this early stage. All of your old documents will continue to work as normal. We are not touching them. If you don't want to deal With the possibility of slight formatting changes, you may choose to stick with the Legacy renderer on any of your brews for as long as you like. However, most new features added from now on will only be available for brews using the V3 renderer.
-
-Massive changelog incoming:
-
-#### Markdown+
-With the latest major update to *The Homebrewery*, we've implemented an extended Markdown-like syntax for block and span elements, plus a few other changes, eliminating the need for HTML tags like `div`, and `span` in most cases. This should hopefully aid non-coders with readability, and also allows us a few tricks in the background to fix some old issues. No raw HTML tags should be needed in a brew, and going forward, raw HTML will no longer receive debugging support (*but can still be used if you insist*).
-
-All brews made prior to the release of v3.0.0 will still render normally, and you may switch between the "Legacy" brew renderer and the newer "V3" renderer via the {{fa,fa-info-circle}} **Properties** button on your brew. Much of the syntax and styling has changed in V3, so code in one version may be broken in the other.
-
-Visit [this page](/v3_preview) for brief examples of the new syntax!
-
-#### Extended Markdown Syntax:
-
-{{taskList
-* [x] Add Divs and Spans for all your custom styling needs, via a simplified Markdown-like syntax:
- ```
- {{myDivClass,#myId,color:red
- My Div content
- }}
-
- Hello {{mySpan,color:blue World}} !
- ```
-
- Fixes issues: [#348](https://github.com/naturalcrit/homebrewery/issues/348)
-}}
-
-\column
-
-{{taskList
-* [x] Add inline CSS to Markdown objects via "curly injection" syntax:
- ```
- Hello *world*{myClass,#id,color:red}
- ```
- Fixes issues: [#403](https://github.com/naturalcrit/homebrewery/issues/403)
-
-* [x] Rowspan, Colspan, and multiple header rows with extended table syntax:
-```
-| Header 1a | Header 1b | Header 1c |
-| Header 2a | Header 2b | Header 2c |
-|:---------:|:----------|:---------:|
-| Span 2 columns || Span 2 |
-| one col | one col | rows ^|
-```
- Fixes issues: [#773](https://github.com/naturalcrit/homebrewery/issues/773), [#191](https://github.com/naturalcrit/homebrewery/issues/191)
-
-* [x] Hanging indents via ``code`
- ```
- Here is some code!
-```
-
-
- Fixes issues: [#465](https://github.com/naturalcrit/homebrewery/issues/465)
-
-#### New and Fixed Snippets
-
-* [x] Column breaks now use `\column` instead of ` ``` ` backticks.
-
- Fixes issues: [#607](https://github.com/naturalcrit/homebrewery/issues/607)
-
-* [x] Page breaks using `\page` now only trigger when placed alone at the start of a line.
-
- Fixes issues: [#1147](https://github.com/naturalcrit/homebrewery/issues/1147)
-}}
-
-\page
-{{taskList
-* [x] New **EDITOR → QR Code** snippet.
-
- Fixes issues: [#538](https://github.com/naturalcrit/homebrewery/issues/538)
-
-* [x] New **IMAGES → Watercolor Splatter** snippet, which adds one of a range of stylish stains to your brew.
-
-* [x] New **IMAGES → Watermark** snippet, which adds transparent text diagonally across the page.
-
-* [x] New **PHB → Magic Item** snippet.
-
- Fixes issues: [#671](https://github.com/naturalcrit/homebrewery/issues/671)
-
-* [x] New **TABLES → 1/3 Class Table** snippet for 1/3 casters.
-
- Fixes issues: [#191](https://github.com/naturalcrit/homebrewery/issues/191)
-
-* [x] Improved **EDITOR → Table of Contents** snippet to actually look like the PHB style. Will auto-generate based on the headers in your brew.
-
- Fixes issues: [#304](https://github.com/naturalcrit/homebrewery/issues/304)
-
-* [x] Improved **PHB → Monster Stat Block** snippet with textures, and an option to remove the frame entirely.
-
-* [x] Improved **PHB → Spell List** snippet can now be made single-column.
-
- Fixes issues: [#509](https://github.com/naturalcrit/homebrewery/issues/509), [#914](https://github.com/naturalcrit/homebrewery/issues/914)
-
-* [x] Improved **TABLES → Class Table** snippet is now cleaned up, has an option to remove the frame entirely, and includes additional boundary decorations.
-
- Fixes issues: [#773](https://github.com/naturalcrit/homebrewery/issues/773), [#302](https://github.com/naturalcrit/homebrewery/issues/302)
-
-#### Miscellaneous Formatting Fixes
-
-* [x] Paragraphs are now able to split across columns.
-
- Fixes issues: [#239](https://github.com/naturalcrit/homebrewery/issues/239)
-
-* [x] Multiple fixes for bold/italicize using asterisks `* *`
-
- Fixes issues: [#1321](https://github.com/naturalcrit/homebrewery/issues/1321), [#852](https://github.com/naturalcrit/homebrewery/issues/852)
-
-* [x] Multiple for list items not displaying correctly.
-
- Fixes issues: [#1085](https://github.com/naturalcrit/homebrewery/issues/1085), [#588](https://github.com/naturalcrit/homebrewery/issues/588)
-
-* [x] "Smart quotes", so left and right quotes are different.
-
- Fixes issues: [#849](https://github.com/naturalcrit/homebrewery/issues/849)
-
-* [x] Long URLs in links now wrap properly.
-
- Fixes issues: [#1136](https://github.com/naturalcrit/homebrewery/issues/1136)
-
-* [x] Better support for `wide` blocks that span across the whole page! No more problems with contents getting shunted off the edge, and each new wide element in a page will restart the next item back at column one. Manual `\column` breaks will help organize subsequent content between the columns as needed.
-
- Fixes issues: [#144](https://github.com/naturalcrit/homebrewery/issues/144), [#1024](https://github.com/naturalcrit/homebrewery/issues/1024)
-
-* [x] Fonts now support a wider range of latin characters for non-English brews, including áéíóúñ¡¿, etc...
-
- Fixes issues: [#116](https://github.com/naturalcrit/homebrewery/issues/116)
-
-* [x] Drop-caps (fancy first letters) have been re-styled and re-aligned to correct the ugly overlapping and cut-off on some characters like K and Y.
-
- Fixes issues: [#848](https://github.com/naturalcrit/homebrewery/issues/848)
-}}
-
-\column
-
-### Under-the-Hood Stuff
-We had to make a whole lot of background upgrades and changes to get all of this working, and now that the framework is in place, there's a lot more planned and upcoming *"sometime"* :
-
-{{taskList
-* [ ] New Themes to style your brews. DMG, MM, a custom Homebrewery theme, and others.
-* [ ] The ability to build your own custom themes using CSS, apply it to other brews, and share it with others!
-* [ ] Easy control of item colors. Change your monster blocks, tables, and notes from yellow to green to red!
-* [ ] New image-based snippets, including handwritten notes, title illustrations, and alternative decorations.
-* [ ] New fun fonts like Elvish, Draconic, Orcish, etc.
-* [ ] Better organization of personal brews using tags.
-* [ ] ....a log-out button...?
-* [ ] AND MORE.
-}}
-
-### Interface
-::
-#### Style Editor Panel
-
-{{fa,fa-paint-brush}} Technically released prior to v3 but still new to many users, check out the new **Style Editor** located on the right side of the Snippet bar. This editor accepts CSS for styling without requiring `*/}
-
-
- {this.props.navItems}
- {this.renderSortOptions()}
- {this.renderTagsOptions()}
+ return (
+ ${text}`;
- res.status(200).send(text);
-});
-
-//Download brew source page
-app.get('/download/:id', asyncHandler(getBrew('share')), (req, res)=>{
- const { brew } = req;
- sanitizeBrew(brew, 'share');
- const prefix = 'HB - ';
-
- const encodeRFC3986ValueChars = (str)=>{
- return (
- encodeURIComponent(str)
- .replace(/[!'()*]/g, (char)=>{`%${char.charCodeAt(0).toString(16).toUpperCase()}`;})
- );
- };
-
- let fileName = sanitizeFilename(`${prefix}${brew.title}`).replaceAll(' ', '');
- if(!fileName || !fileName.length) { fileName = `${prefix}-Untitled-Brew`; };
- res.set({
- 'Cache-Control' : 'no-cache',
- 'Content-Type' : 'text/plain',
- 'Content-Disposition' : `attachment; filename*=UTF-8''${encodeRFC3986ValueChars(fileName)}.txt`
+ req.config = {
+ google_client_id : config.get('google_client_id'),
+ google_client_secret : config.get('google_client_secret')
+ };
+ return next();
});
- res.status(200).send(brew.text);
-});
-//Serve brew metadata
-app.get('/metadata/:id', asyncHandler(getBrew('share')), (req, res)=>{
- const { brew } = req;
- sanitizeBrew(brew, 'share');
+ app.use(homebrewApi);
+ app.use(adminApi(vite));
+ app.use(vaultApi);
- const fields = ['title', 'pageCount', 'description', 'authors', 'lang',
+ const welcomeText = fs.readFileSync('./client/homebrew/pages/homePage/welcome_msg.md', 'utf8');
+ const welcomeTextLegacy = fs.readFileSync('./client/homebrew/pages/homePage/welcome_msg_legacy.md', 'utf8');
+ const migrateText = fs.readFileSync('./client/homebrew/pages/homePage/migrate.md', 'utf8');
+ const changelogText = fs.readFileSync('changelog.md', 'utf8');
+ const faqText = fs.readFileSync('faq.md', 'utf8');
+
+ String.prototype.replaceAll = function(s, r){return this.split(s).join(r);};
+
+ const defaultMetaTags = {
+ site_name : 'The Homebrewery - Make your Homebrew content look legit!',
+ title : 'The Homebrewery',
+ description : 'A NaturalCrit Tool for creating authentic Homebrews using Markdown.',
+ image : `${config.get('publicUrl')}/thumbnail.png`,
+ type : 'website'
+ };
+
+ //Robots.txt
+ app.get('/robots.txt', (req, res)=>{
+ return res.sendFile(`robots.txt`, { root: process.cwd() });
+ });
+
+ //Home page
+ app.get('/', (req, res, next)=>{
+ req.brew = {
+ text : welcomeText,
+ renderer : 'V3',
+ theme : '5ePHB'
+ },
+
+ req.ogMeta = { ...defaultMetaTags,
+ title : 'Homepage',
+ description : 'Homepage'
+ };
+
+ splitTextStyleAndMetadata(req.brew);
+ return next();
+ });
+
+ //Home page Legacy
+ app.get('/legacy', (req, res, next)=>{
+ req.brew = {
+ text : welcomeTextLegacy,
+ renderer : 'legacy',
+ theme : '5ePHB'
+ },
+
+ req.ogMeta = { ...defaultMetaTags,
+ title : 'Homepage (Legacy)',
+ description : 'Homepage'
+ };
+
+ splitTextStyleAndMetadata(req.brew);
+ return next();
+ });
+
+ //Legacy/Other Document -> v3 Migration Guide
+ app.get('/migrate', (req, res, next)=>{
+ req.brew = {
+ text : migrateText,
+ renderer : 'V3',
+ theme : '5ePHB'
+ },
+
+ req.ogMeta = { ...defaultMetaTags,
+ title : 'v3 Migration Guide',
+ description : 'A brief guide to converting Legacy documents to the v3 renderer.'
+ };
+
+ splitTextStyleAndMetadata(req.brew);
+ return next();
+ });
+
+ //Changelog page
+ app.get('/changelog', async (req, res, next)=>{
+ req.brew = {
+ title : 'Changelog',
+ text : changelogText,
+ renderer : 'V3',
+ theme : '5ePHB'
+ },
+
+ req.ogMeta = { ...defaultMetaTags,
+ title : 'Changelog',
+ description : 'Development changelog.'
+ };
+
+ splitTextStyleAndMetadata(req.brew);
+ return next();
+ });
+
+ //FAQ page
+ app.get('/faq', async (req, res, next)=>{
+ req.brew = {
+ title : 'FAQ',
+ text : faqText,
+ renderer : 'V3',
+ theme : '5ePHB'
+ },
+
+ req.ogMeta = { ...defaultMetaTags,
+ title : 'FAQ',
+ description : 'Frequently Asked Questions'
+ };
+
+ splitTextStyleAndMetadata(req.brew);
+ return next();
+ });
+
+ //Source page
+ app.get('/source/:id', asyncHandler(getBrew('share')), (req, res)=>{
+ const { brew } = req;
+
+ const replaceStrings = { '&': '&', '<': '<', '>': '>' };
+ let text = brew.text;
+ for (const replaceStr in replaceStrings) {
+ text = text.replaceAll(replaceStr, replaceStrings[replaceStr]);
+ }
+ text = `${text}`;
+ res.status(200).send(text);
+ });
+
+ //Download brew source page
+ app.get('/download/:id', asyncHandler(getBrew('share')), (req, res)=>{
+ const { brew } = req;
+ sanitizeBrew(brew, 'share');
+ const prefix = 'HB - ';
+
+ const encodeRFC3986ValueChars = (str)=>{
+ return (
+ encodeURIComponent(str)
+ .replace(/[!'()*]/g, (char)=>{`%${char.charCodeAt(0).toString(16).toUpperCase()}`;})
+ );
+ };
+
+ let fileName = sanitizeFilename(`${prefix}${brew.title}`).replaceAll(' ', '');
+ if(!fileName || !fileName.length) { fileName = `${prefix}-Untitled-Brew`; };
+ res.set({
+ 'Cache-Control' : 'no-cache',
+ 'Content-Type' : 'text/plain',
+ 'Content-Disposition' : `attachment; filename*=UTF-8''${encodeRFC3986ValueChars(fileName)}.txt`
+ });
+ res.status(200).send(brew.text);
+ });
+
+ //Serve brew metadata
+ app.get('/metadata/:id', asyncHandler(getBrew('share')), (req, res)=>{
+ const { brew } = req;
+ sanitizeBrew(brew, 'share');
+
+ const fields = ['title', 'pageCount', 'description', 'authors', 'lang',
'published', 'views', 'shareId', 'createdAt', 'updatedAt',
'lastViewed', 'thumbnail', 'tags'
- ];
+ ];
- const metadata = fields.reduce((acc, field)=>{
+ const metadata = fields.reduce((acc, field)=>{
if(brew[field] !== undefined) acc[field] = brew[field];
return acc;
- }, {});
- res.status(200).json(metadata);
-});
+ }, {});
+ res.status(200).json(metadata);
+ });
-//Serve brew styling
-app.get('/css/:id', asyncHandler(getBrew('share')), (req, res)=>{getCSS(req, res);});
+ //Serve brew styling
+ app.get('/css/:id', asyncHandler(getBrew('share')), (req, res)=>{getCSS(req, res);});
-//User Page
-app.get('/user/:username', async (req, res, next)=>{
- const ownAccount = req.account && (req.account.username == req.params.username);
+ //User Page
+ app.get('/user/:username', dbCheck, async (req, res, next)=>{
+ const ownAccount = req.account && (req.account.username == req.params.username);
- req.ogMeta = { ...defaultMetaTags,
- title : `${req.params.username}'s Collection`,
- description : 'View my collection of homebrew on the Homebrewery.'
+ req.ogMeta = { ...defaultMetaTags,
+ title : `${req.params.username}'s Collection`,
+ description : 'View my collection of homebrew on the Homebrewery.'
// type : could be 'profile'?
- };
+ };
- const fields = [
- 'googleId',
- 'title',
- 'pageCount',
- 'description',
- 'authors',
- 'lang',
- 'published',
- 'views',
- 'shareId',
- 'editId',
- 'createdAt',
- 'updatedAt',
- 'lastViewed',
- 'thumbnail',
- 'tags'
- ];
+ const fields = [
+ 'googleId',
+ 'title',
+ 'pageCount',
+ 'description',
+ 'authors',
+ 'lang',
+ 'published',
+ 'views',
+ 'shareId',
+ 'editId',
+ 'createdAt',
+ 'updatedAt',
+ 'lastViewed',
+ 'thumbnail',
+ 'tags'
+ ];
- let brews = await HomebrewModel.getByUser(req.params.username, ownAccount, fields)
+ let brews = await HomebrewModel.getByUser(req.params.username, ownAccount, fields)
.catch((err)=>{
console.log(err);
});
- brews.forEach((brew)=>brew.stubbed = true); //All brews from MongoDB are "stubbed"
+ brews.forEach((brew)=>brew.stubbed = true); //All brews from MongoDB are "stubbed"
- if(ownAccount && req?.account?.googleId){
- const auth = await GoogleActions.authCheck(req.account, res);
- let googleBrews = await GoogleActions.listGoogleBrews(auth)
+ if(ownAccount && req?.account?.googleId){
+ const auth = await GoogleActions.authCheck(req.account, res);
+ let googleBrews = await GoogleActions.listGoogleBrews(auth)
.catch((err)=>{
console.error(err);
});
- // If stub matches file from Google, use Google metadata over stub metadata
- if(googleBrews && googleBrews.length > 0) {
- for (const brew of brews.filter((brew)=>brew.googleId)) {
- const match = googleBrews.findIndex((b)=>b.editId === brew.editId);
- if(match !== -1) {
- brew.googleId = googleBrews[match].googleId;
- brew.pageCount = googleBrews[match].pageCount;
- brew.renderer = googleBrews[match].renderer;
- brew.version = googleBrews[match].version;
- brew.webViewLink = googleBrews[match].webViewLink;
- googleBrews.splice(match, 1);
+ // If stub matches file from Google, use Google metadata over stub metadata
+ if(googleBrews && googleBrews.length > 0) {
+ for (const brew of brews.filter((brew)=>brew.googleId)) {
+ const match = googleBrews.findIndex((b)=>b.editId === brew.editId);
+ if(match !== -1) {
+ brew.googleId = googleBrews[match].googleId;
+ brew.pageCount = googleBrews[match].pageCount;
+ brew.renderer = googleBrews[match].renderer;
+ brew.version = googleBrews[match].version;
+ brew.webViewLink = googleBrews[match].webViewLink;
+ googleBrews.splice(match, 1);
+ }
}
+
+ //Remaining unstubbed google brews display current user as author
+ googleBrews = googleBrews.map((brew)=>({ ...brew, authors: [req.account.username] }));
+ brews = _.concat(brews, googleBrews);
}
-
- //Remaining unstubbed google brews display current user as author
- googleBrews = googleBrews.map((brew)=>({ ...brew, authors: [req.account.username] }));
- brews = _.concat(brews, googleBrews);
}
- }
- req.brews = _.map(brews, (brew)=>{
+ req.brews = _.map(brews, (brew)=>{
// Clean up brew data
- brew.title = brew.title?.trim();
- brew.description = brew.description?.trim();
- return sanitizeBrew(brew, ownAccount ? 'edit' : 'share');
+ brew.title = brew.title?.trim();
+ brew.description = brew.description?.trim();
+ return sanitizeBrew(brew, ownAccount ? 'edit' : 'share');
+ });
+
+ return next();
});
- return next();
-});
+ //Change author name on brews
+ app.put('/api/user/rename', dbCheck, async (req, res)=>{
+ const { username, newUsername } = req.body;
+ const ownAccount = req.account && (req.account.username == newUsername);
-//Change author name on brews
-app.put('/api/user/rename', async (req, res)=>{
- const { username, newUsername } = req.body;
- const ownAccount = req.account && (req.account.username == newUsername);
-
- if(!username || !newUsername)
- return res.status(400).json({ error: 'Username and newUsername are required.' });
- if(!ownAccount)
- return res.status(403).json({ error: 'Must be logged in to change your username' });
- try {
- const brews = await HomebrewModel.getByUser(username, true, ['authors']);
- const renamePromises = brews.map(async (brew)=>{
- const updatedAuthors = brew.authors.map((author)=>author === username ? newUsername : author
- );
- return HomebrewModel.updateOne(
- { _id: brew._id },
- { $set: { authors: updatedAuthors } }
- );
- });
- await Promise.all(renamePromises);
-
- return res.json({ success: true, message: `Brews for ${username} renamed to ${newUsername}.` });
- } catch (error) {
- console.error('Error renaming brews:', error);
- return res.status(500).json({ error: 'Failed to rename brews.' });
- }
-});
-
-//Delete brews based on author
-app.delete('/api/user/delete', async (req, res)=>{
- const { username } = req.body;
-
- const ownAccount = req.account && (req.account.username == username);
- if(!ownAccount) return res.status(403).json({ error: 'Must be logged in to delete your account' });
-
- try {
- const brews = await HomebrewModel.getByUser(username, true, ['_id', 'googleId', 'editId', 'authors']);
-
- const deletePromises = brews.map((brew)=>{
- req.brew = brew;
- return new Promise((resolve, reject)=>{
- api.deleteBrew(req, res, (err)=>err ? reject(err) : resolve());
+ if(!username || !newUsername)
+ return res.status(400).json({ error: 'Username and newUsername are required.' });
+ if(!ownAccount)
+ return res.status(403).json({ error: 'Must be logged in to change your username' });
+ try {
+ const brews = await HomebrewModel.getByUser(username, true, ['authors']);
+ const renamePromises = brews.map(async (brew)=>{
+ const updatedAuthors = brew.authors.map((author)=>author === username ? newUsername : author
+ );
+ return HomebrewModel.updateOne(
+ { _id: brew._id },
+ { $set: { authors: updatedAuthors } }
+ );
});
- });
+ await Promise.all(renamePromises);
- await Promise.all(deletePromises);
-
- return res.json({ success: true, message: `All brews for ${username} have been deleted.` });
- } catch (error) {
- console.error('Error deleting brews:', error);
- if(!res.headersSent) {
- return res.status(500).json({ error: 'Failed to delete the brews.' });
+ return res.json({ success: true, message: `Brews for ${username} renamed to ${newUsername}.` });
+ } catch (error) {
+ console.error('Error renaming brews:', error);
+ return res.status(500).json({ error: 'Failed to rename brews.' });
}
- }
-});
+ });
-//Edit Page
-app.get('/edit/:id', asyncHandler(getBrew('edit')), asyncHandler(async(req, res, next)=>{
- req.brew = req.brew.toObject ? req.brew.toObject() : req.brew;
+ //Delete brews based on author
+ app.delete('/api/user/delete', async (req, res)=>{
+ const { username } = req.body;
- req.userThemes = await(getUsersBrewThemes(req.account?.username));
+ const ownAccount = req.account && (req.account.username == username);
+ if(!ownAccount) return res.status(403).json({ error: 'Must be logged in to delete your account' });
- req.ogMeta = { ...defaultMetaTags,
- title : req.brew.title || 'Untitled Brew',
- description : req.brew.description || 'No description.',
- image : req.brew.thumbnail || defaultMetaTags.image,
- type : 'article'
- };
+ try {
+ const brews = await HomebrewModel.getByUser(username, true, ['_id', 'googleId', 'editId', 'authors']);
- sanitizeBrew(req.brew, 'edit');
- splitTextStyleAndMetadata(req.brew);
- res.header('Cache-Control', 'no-cache, no-store'); //reload the latest saved brew when pressing back button, not the cached version before save.
- return next();
-}));
+ const deletePromises = brews.map((brew)=>{
+ req.brew = brew;
+ return new Promise((resolve, reject)=>{
+ api.deleteBrew(req, res, (err)=>err ? reject(err) : resolve());
+ });
+ });
-//New Page from ID
-app.get('/new/:id', asyncHandler(getBrew('share')), asyncHandler(async(req, res, next)=>{
- sanitizeBrew(req.brew, 'share');
- splitTextStyleAndMetadata(req.brew);
- const brew = {
- shareId : req.brew.shareId,
- title : `CLONE - ${req.brew.title}`,
- text : req.brew.text,
- style : req.brew.style,
- renderer : req.brew.renderer,
- theme : req.brew.theme,
- tags : req.brew.tags,
- snippets : req.brew.snippets
- };
- req.brew = _.defaults(brew, DEFAULT_BREW);
+ await Promise.all(deletePromises);
- req.userThemes = await(getUsersBrewThemes(req.account?.username));
+ return res.json({ success: true, message: `All brews for ${username} have been deleted.` });
+ } catch (error) {
+ console.error('Error deleting brews:', error);
+ if(!res.headersSent) {
+ return res.status(500).json({ error: 'Failed to delete the brews.' });
+ }
+ }
+ });
- req.ogMeta = { ...defaultMetaTags,
- title : 'New',
- description : 'Start crafting your homebrew on the Homebrewery!'
- };
+ //Edit Page
+ app.get('/edit/:id', asyncHandler(getBrew('edit')), asyncHandler(async(req, res, next)=>{
+ req.brew = req.brew.toObject ? req.brew.toObject() : req.brew;
- return next();
-}));
+ req.userThemes = await(getUsersBrewThemes(req.account?.username));
-//New Page
-app.get('/new', asyncHandler(async(req, res, next)=>{
- req.userThemes = await(getUsersBrewThemes(req.account?.username));
+ req.ogMeta = { ...defaultMetaTags,
+ title : req.brew.title || 'Untitled Brew',
+ description : req.brew.description || 'No description.',
+ image : req.brew.thumbnail || defaultMetaTags.image,
+ locale : req.brew.lang,
+ type : 'article'
+ };
- req.ogMeta = { ...defaultMetaTags,
- title : 'New',
- description : 'Start crafting your homebrew on the Homebrewery!'
- };
+ sanitizeBrew(req.brew, 'edit');
+ splitTextStyleAndMetadata(req.brew);
+ res.header('Cache-Control', 'no-cache, no-store'); //reload the latest saved brew when pressing back button, not the cached version before save.
+ return next();
+ }));
- return next();
-}));
+ //New Page from ID
+ app.get('/new/:id', asyncHandler(getBrew('share')), asyncHandler(async(req, res, next)=>{
+ sanitizeBrew(req.brew, 'share');
+ splitTextStyleAndMetadata(req.brew);
+ const brew = {
+ shareId : req.brew.shareId,
+ title : `CLONE - ${req.brew.title}`,
+ text : req.brew.text,
+ style : req.brew.style,
+ renderer : req.brew.renderer,
+ theme : req.brew.theme,
+ tags : req.brew.tags,
+ snippets : req.brew.snippets
+ };
+ req.brew = _.defaults(brew, DEFAULT_BREW);
-//Share Page
-app.get('/share/:id', asyncHandler(getBrew('share')), asyncHandler(async (req, res, next)=>{
- const { brew } = req;
- req.ogMeta = { ...defaultMetaTags,
- title : `${req.brew.title || 'Untitled Brew'} - ${req.brew.authors[0] || 'No author.'}`,
- description : req.brew.description || 'No description.',
- image : req.brew.thumbnail || defaultMetaTags.image,
- type : 'article'
- };
+ req.userThemes = await(getUsersBrewThemes(req.account?.username));
- // increase visitor view count, do not include visits by author(s)
- if(!brew.authors.includes(req.account?.username)){
- if(req.params.id.length > 12 && !brew._id) {
- const googleId = brew.googleId;
- const shareId = brew.shareId;
- await GoogleActions.increaseView(googleId, shareId, 'share', brew)
+ req.ogMeta = { ...defaultMetaTags,
+ title : 'New',
+ description : 'Start crafting your homebrew on the Homebrewery!'
+ };
+
+ return next();
+ }));
+
+ //New Page
+ app.get('/new', asyncHandler(async(req, res, next)=>{
+ req.userThemes = await(getUsersBrewThemes(req.account?.username));
+
+ req.ogMeta = { ...defaultMetaTags,
+ title : 'New',
+ description : 'Start crafting your homebrew on the Homebrewery!'
+ };
+
+ return next();
+ }));
+
+ //Share Page
+ app.get('/share/:id', dbCheck, asyncHandler(getBrew('share')), asyncHandler(async (req, res, next)=>{
+ const { brew } = req;
+ req.ogMeta = { ...defaultMetaTags,
+ title : `${req.brew.title || 'Untitled Brew'} - ${req.brew.authors[0] || 'No author.'}`,
+ description : req.brew.description || 'No description.',
+ image : req.brew.thumbnail || defaultMetaTags.image,
+ type : 'article'
+ };
+
+ // increase visitor view count, do not include visits by author(s)
+ if(!brew.authors.includes(req.account?.username)){
+ if(req.params.id.length > 12 && !brew._id) {
+ const googleId = brew.googleId;
+ const shareId = brew.shareId;
+ await GoogleActions.increaseView(googleId, shareId, 'share', brew)
.catch((err)=>{next(err);});
- } else {
- await HomebrewModel.increaseView({ shareId: brew.shareId });
- }
- };
+ } else {
+ await HomebrewModel.increaseView({ shareId: brew.shareId });
+ }
+ };
- brew.authors.includes(req.account?.username) ? sanitizeBrew(req.brew, 'shareAuthor') : sanitizeBrew(req.brew, 'share');
- splitTextStyleAndMetadata(req.brew);
- return next();
-}));
+ brew.authors.includes(req.account?.username) ? sanitizeBrew(req.brew, 'shareAuthor') : sanitizeBrew(req.brew, 'share');
+ splitTextStyleAndMetadata(req.brew);
+ return next();
+ }));
-//Account Page
-app.get('/account', asyncHandler(async (req, res, next)=>{
- const data = {};
- data.title = 'Account Information Page';
+ //Account Page
+ app.get('/account', dbCheck, asyncHandler(async (req, res, next)=>{
+ const data = {};
+ data.title = 'Account Information Page';
- if(!req.account) {
- res.set('WWW-Authenticate', 'Bearer realm="Authorization Required"');
- const error = new Error('No valid account');
- error.status = 401;
- error.HBErrorCode = '50';
- error.page = data.title;
- return next(error);
- };
+ if(!req.account) {
+ res.set('WWW-Authenticate', 'Bearer realm="Authorization Required"');
+ const error = new Error('No valid account');
+ error.status = 401;
+ error.HBErrorCode = '50';
+ error.page = data.title;
+ return next(error);
+ };
- let auth;
- let googleCount = [];
- if(req.account) {
- if(req.account.googleId) {
- auth = await GoogleActions.authCheck(req.account, res, false);
+ let auth;
+ let googleCount = [];
+ if(req.account) {
+ if(req.account.googleId) {
+ auth = await GoogleActions.authCheck(req.account, res, false);
- googleCount = await GoogleActions.listGoogleBrews(auth)
+ googleCount = await GoogleActions.listGoogleBrews(auth)
.catch((err)=>{
console.error(err);
});
- }
+ }
- const query = { authors: req.account.username, googleId: { $exists: false } };
- const mongoCount = await HomebrewModel.countDocuments(query)
+ const query = { authors: req.account.username, googleId: { $exists: false } };
+ const mongoCount = await HomebrewModel.countDocuments(query)
.catch((err)=>{
- mongoCount = 0;
console.log(err);
+ return 0;
});
- data.accountDetails = {
- username : req.account.username,
- issued : req.account.issued,
- googleId : Boolean(req.account.googleId),
- authCheck : Boolean(req.account.googleId && auth?.credentials.access_token),
- mongoCount : mongoCount,
- googleCount : googleCount?.length
+ data.accountDetails = {
+ username : req.account.username,
+ issued : req.account.issued,
+ googleId : Boolean(req.account.googleId),
+ authCheck : Boolean(req.account.googleId && auth?.credentials.access_token),
+ mongoCount : mongoCount,
+ googleCount : googleCount?.length
+ };
+ }
+
+ req.brew = data;
+
+ req.ogMeta = { ...defaultMetaTags,
+ title : `Account Page`,
+ description : null
};
- }
- req.brew = data;
+ return next();
+ }));
- req.ogMeta = { ...defaultMetaTags,
- title : `Account Page`,
- description : null
- };
-
- return next();
-}));
-
-// Local only
-if(isLocalEnvironment){
+ // Local only
+ if(isLocalEnvironment){
// Login
- app.post('/local/login', (req, res)=>{
- const username = req.body.username;
- if(!username) return;
+ app.post('/local/login', (req, res)=>{
+ const username = req.body.username;
+ if(!username) return;
- const payload = jwt.encode({ username: username, issued: new Date }, config.get('secret'));
- return res.json(payload);
- });
-}
-
-// Add Static Local Paths
-app.use('/staticImages', express.static(config.get('hb_images') && fs.existsSync(config.get('hb_images')) ? config.get('hb_images') :'staticImages'));
-app.use('/staticFonts', express.static(config.get('hb_fonts') && fs.existsSync(config.get('hb_fonts')) ? config.get('hb_fonts'):'staticFonts'));
-
-//Vault Page
-app.get('/vault', asyncHandler(async(req, res, next)=>{
- req.ogMeta = { ...defaultMetaTags,
- title : 'The Vault',
- description : 'Search for Brews'
- };
- return next();
-}));
-
-//Send rendered page
-app.use(asyncHandler(async (req, res, next)=>{
- if(!req.route) return res.redirect('/'); // Catch-all for invalid routes
-
- const page = await renderPage(req, res);
- if(!page) return;
- res.send(page);
-}));
-
-//Render the page
-const renderPage = async (req, res)=>{
- // Create configuration object
- const configuration = {
- local : isLocalEnvironment,
- publicUrl : config.get('publicUrl') ?? '',
- baseUrl : `${req.protocol}://${req.get('host')}`,
- environment : nodeEnv,
- deployment : config.get('heroku_app_name') ?? ''
- };
- const props = {
- version : version,
- url : req.customUrl || req.originalUrl,
- brew : req.brew,
- brews : req.brews,
- googleBrews : req.googleBrews,
- account : req.account,
- enable_v3 : config.get('enable_v3'),
- enable_themes : config.get('enable_themes'),
- config : configuration,
- ogMeta : req.ogMeta,
- userThemes : req.userThemes
- };
- const title = req.brew ? req.brew.title : '';
- const page = await templateFn('homebrew', title, props)
- .catch((err)=>{
- console.log(err);
+ const payload = jwt.encode({ username: username, issued: new Date }, config.get('secret'));
+ return res.json(payload);
});
- return page;
-};
-
-//v=====----- Error-Handling Middleware -----=====v//
-//Format Errors as plain objects so all fields will appear in the string sent
-const formatErrors = (key, value)=>{
- if(value instanceof Error) {
- const error = {};
- Object.getOwnPropertyNames(value).forEach(function (key) {
- error[key] = value[key];
- });
- return error;
}
- return value;
-};
-const getPureError = (error)=>{
- return JSON.parse(JSON.stringify(error, formatErrors));
-};
+ // Add Static Local Paths
+ app.use('/staticImages', express.static(config.get('hb_images') && fs.existsSync(config.get('hb_images')) ? config.get('hb_images') :'staticImages'));
+ app.use('/staticFonts', express.static(config.get('hb_fonts') && fs.existsSync(config.get('hb_fonts')) ? config.get('hb_fonts'):'staticFonts'));
-app.use(async (err, req, res, next)=>{
- err.originalUrl = req.originalUrl;
- console.error(err);
+ //Vault Page
+ app.get('/vault', asyncHandler(async(req, res, next)=>{
+ req.ogMeta = { ...defaultMetaTags,
+ title : 'The Vault',
+ description : 'Search for Brews'
+ };
+ return next();
+ }));
- if(err.originalUrl?.startsWith('/api')) {
+ //Send rendered page
+ app.use(asyncHandler(async (req, res, next)=>{
+ if(!req.route) return res.redirect('/'); // Catch-all for invalid routes
+
+ const page = await renderPage(req, res);
+ if(!page) return;
+ res.send(page);
+ }));
+
+ //Render the page
+ const renderPage = async (req, res)=>{
+
+ // Create configuration object
+ const configuration = {
+ local : isLocalEnvironment,
+ publicUrl : config.get('publicUrl') ?? '',
+ baseUrl : `${req.protocol}://${req.get('host')}`,
+ environment : nodeEnv,
+ deployment : config.get('heroku_app_name') ?? ''
+ };
+ const props = {
+ version : version,
+ url : req.customUrl || req.originalUrl,
+ brew : req.brew,
+ brews : req.brews,
+ googleBrews : req.googleBrews,
+ account : req.account,
+ config : configuration,
+ ogMeta : req.ogMeta,
+ userThemes : req.userThemes
+ };
+
+ const ogTags = [];
+ const ogMeta = req.ogMeta ?? {};
+ Object.entries(ogMeta).forEach(([key, value])=>{
+ if(!value) return;
+ const tag = ``;
+ ogTags.push(tag);
+ });
+ const ogMetaTags = ogTags.join('\n');
+
+ const htmlPath = isProd ? path.resolve('build', 'index.html') : path.resolve('index.html');
+ let html = fs.readFileSync(htmlPath, 'utf-8');
+
+ if(!isProd && vite?.transformIndexHtml) {
+ html = await vite.transformIndexHtml(req.originalUrl, html);
+ }
+
+ html = html.replace(
+ '',
+ ()=>{ return `\n\n${ogMetaTags}`; }
+ );
+
+ return html;
+ };
+
+ //v=====----- Error-Handling Middleware -----=====v//
+ //Format Errors as plain objects so all fields will appear in the string sent
+ const formatErrors = (key, value)=>{
+ if(value instanceof Error) {
+ const error = {};
+ Object.getOwnPropertyNames(value).forEach(function (key) {
+ error[key] = value[key];
+ });
+ return error;
+ }
+ return value;
+ };
+
+ const getPureError = (error)=>{
+ return JSON.parse(JSON.stringify(error, formatErrors));
+ };
+
+ app.use(async (err, req, res, next)=>{
+ err.originalUrl = req.originalUrl;
+ console.error(err);
+
+ if(err.originalUrl?.startsWith('/api')) {
// console.log('API error');
- res.status(err.status || err.response?.status || 500).send(err);
- return;
- }
+ res.status(err.status || err.response?.status || 500).send(err);
+ return;
+ }
- // console.log('non-API error');
- const status = err.status || err.code || 500;
+ // console.log('non-API error');
+ const status = err.status || err.code || 500;
- req.ogMeta = { ...defaultMetaTags,
- title : 'Error Page',
- description : 'Something went wrong!'
- };
- req.brew = {
- ...err,
- title : 'Error - Something went wrong!',
- text : err.errors?.map((error)=>{return error.message;}).join('\n\n') || err.message || 'Unknown error!',
- status : status,
- HBErrorCode : err.HBErrorCode ?? '00',
- pureError : getPureError(err)
- };
- req.customUrl= '/error';
+ req.ogMeta = { ...defaultMetaTags,
+ title : 'Error Page',
+ description : 'Something went wrong!'
+ };
+ req.brew = {
+ ...err,
+ title : 'Error - Something went wrong!',
+ text : err.errors?.map((error)=>{return error.message;}).join('\n\n') || err.message || 'Unknown error!',
+ status : status,
+ HBErrorCode : err.HBErrorCode ?? '00',
+ pureError : getPureError(err)
+ };
+ req.customUrl= '/error';
- const page = await renderPage(req, res);
- if(!page) return;
- res.send(page);
-});
+ const page = await renderPage(req, res);
+ if(!page) return;
+ res.send(page);
+ });
-app.use((req, res)=>{
- if(!res.headersSent) {
- console.error('Headers have not been sent, responding with a server error.', req.url);
- res.status(500).send('An error occurred and the server did not send a response. The error has been logged, please note the time this occurred and report this issue.');
- }
-});
-//^=====--------------------------------------=====^//
+ app.use((req, res)=>{
+ if(!res.headersSent) {
+ console.error('Headers have not been sent, responding with a server error.', req.url);
+ res.status(500).send('An error occurred and the server did not send a response. The error has been logged, please note the time this occurred and report this issue.');
+ }
+ });
+ //^=====--------------------------------------=====^//
-export default app;
+ return app;
+}
diff --git a/server/brewDefaults.js b/server/brewDefaults.js
index 11a84b9e9..cb3114467 100644
--- a/server/brewDefaults.js
+++ b/server/brewDefaults.js
@@ -2,27 +2,32 @@ import _ from 'lodash';
// Default properties for newly-created brews
const DEFAULT_BREW = {
- title : '',
- text : '',
- style : undefined,
- description : '',
- editId : undefined,
- shareId : undefined,
- createdAt : undefined,
- updatedAt : undefined,
- renderer : 'V3',
- theme : '5ePHB',
- authors : [],
- tags : [],
- systems : [],
- lang : 'en',
- thumbnail : '',
- views : 0,
- published : false,
- pageCount : 1,
- gDrive : false,
- trashed : false
-
+ title : '',
+ text : '',
+ style : undefined,
+ description : '',
+ editId : undefined,
+ shareId : undefined,
+ createdAt : undefined,
+ updatedAt : undefined,
+ renderer : 'V3',
+ theme : '5ePHB',
+ authors : [],
+ tags : [],
+ lang : 'en',
+ thumbnail : '',
+ views : 0,
+ published : false,
+ pageCount : 1,
+ gDrive : false,
+ trashed : false,
+ bleedSize : { top: '.125in', bottom: '.125in', inner: '.125in', outer: '.125in' },
+ safetySpace : { top: '.25in', bottom: '.25in', outer: '.25in', inner: '.5in' },
+ trimSize : { width: '8.5in', height: '11in' },
+ columns : '2',
+ columnGutter : '.125in',
+ license : 'None',
+ legalAuthors : ''
};
// Default values for older brews with missing properties
// e.g., missing "renderer" is assumed to be "legacy"
diff --git a/server/db.js b/server/db.js
index 97da56a08..4930e4cd6 100644
--- a/server/db.js
+++ b/server/db.js
@@ -22,16 +22,29 @@ const handleConnectionError = (error)=>{
}
};
+const addListeners = (conn)=>{
+ conn.connection.on('disconnecting', ()=>{console.log('Mongo disconnecting...');});
+ conn.connection.on('disconnected', ()=>{console.log('Mongo disconnected!');});
+ conn.connection.on('connecting', ()=>{console.log('Mongo connecting...');});
+ conn.connection.on('connected', ()=>{console.log('Mongo connected!');});
+ return conn;
+};
+
const disconnect = async ()=>{
return await Mongoose.disconnect();
};
const connect = async (config)=>{
- return await Mongoose.connect(getMongoDBURL(config), { retryWrites: false })
- .catch((error)=>handleConnectionError(error));
+ return await Mongoose.connect(getMongoDBURL(config), {
+ retryWrites : false,
+ autoIndex : (config.get('local_environments').includes(config.get('node_env')))
+ })
+ .then(addListeners(Mongoose))
+ .catch((error)=>handleConnectionError(error));
};
export default {
connect,
disconnect
};
+
diff --git a/server/googleActions.js b/server/googleActions.js
index 0ca7556ba..d50549051 100644
--- a/server/googleActions.js
+++ b/server/googleActions.js
@@ -6,6 +6,7 @@ import config from './config.js';
let serviceAuth;
+let clientEmail;
if(!config.get('service_account')){
const reset = '\x1b[0m'; // Reset to default style
const yellow = '\x1b[33m'; // yellow color
@@ -15,6 +16,10 @@ if(!config.get('service_account')){
JSON.parse(config.get('service_account')) :
config.get('service_account');
+ if(keys?.client_email) {
+ clientEmail = keys.client_email;
+ }
+
try {
serviceAuth = googleDrive.auth.fromJSON(keys);
serviceAuth.scopes = ['https://www.googleapis.com/auth/drive'];
@@ -146,7 +151,6 @@ const GoogleActions = {
description : file.description,
views : parseInt(file.properties.views),
published : file.properties.published ? file.properties.published == 'true' : false,
- systems : [],
lang : file.properties.lang,
thumbnail : file.properties.thumbnail,
webViewLink : file.webViewLink
@@ -227,14 +231,30 @@ const GoogleActions = {
if(!obj) return;
+ if(clientEmail) {
+ await drive.permissions.create({
+ resource : {
+ type : 'user',
+ emailAddress : clientEmail,
+ role : 'writer'
+ },
+ fileId : obj.data.id,
+ fields : 'id',
+ })
+ .catch((err)=>{
+ console.log('Error adding Service Account permissions on Google Drive file');
+ console.error(err);
+ });
+ }
+
await drive.permissions.create({
resource : { type : 'anyone',
- role : 'writer' },
+ role : 'writer' },
fileId : obj.data.id,
fields : 'id',
})
.catch((err)=>{
- console.log('Error updating permissions');
+ console.log('Error adding "Anyone" permissions on Google Drive file');
console.error(err);
});
@@ -277,7 +297,6 @@ const GoogleActions = {
text : file.data,
description : obj.data.description,
- systems : obj.data.properties.systems ? obj.data.properties.systems.split(',') : [],
authors : [],
lang : obj.data.properties.lang,
published : obj.data.properties.published ? obj.data.properties.published == 'true' : false,
diff --git a/server/homebrew.api.js b/server/homebrew.api.js
index 82d64c1a3..4cca76193 100644
--- a/server/homebrew.api.js
+++ b/server/homebrew.api.js
@@ -4,15 +4,16 @@ import { model as HomebrewModel } from './homebrew.model.js';
import express from 'express';
import zlib from 'zlib';
import GoogleActions from './googleActions.js';
-import Markdown from '../shared/naturalcrit/markdown.js';
-import yaml from 'js-yaml';
+import { hbfm } from 'hbmarkedwrapper';
+import * as yaml from 'js-yaml';
import asyncHandler from 'express-async-handler';
import { nanoid } from 'nanoid';
-import {makePatches, applyPatches, stringifyPatches, parsePatch} from '@sanity/diff-match-patch';
+import { makePatches, applyPatches, stringifyPatches, parsePatch } from '@sanity/diff-match-patch';
import { md5 } from 'hash-wasm';
-import { splitTextStyleAndMetadata,
+import { splitTextStyleAndMetadata,
brewSnippetsToJSON, debugTextMismatch } from '../shared/helpers.js';
import checkClientVersion from './middleware/check-client-version.js';
+import dbCheck from './middleware/dbCheck.js';
const router = express.Router();
@@ -30,6 +31,27 @@ const isStaticTheme = (renderer, themeName)=>{
// });
// };
+
+const migrateSystemsToTags = (brew)=>{
+ if(!('systems' in brew)) return brew;
+
+ if(!Array.isArray(brew.systems) || brew.systems.length === 0) {
+ brew.systems = undefined;
+ return brew;
+ }
+ const systemMap = {
+ '5e' : 'system:D&D 5e',
+ '4e' : 'system:D&D 4e',
+ '3.5e' : 'system:D&D 3.5e',
+ 'Pathfinder' : 'system:Pathfinder 2e'
+ };
+ const systemTags = brew.systems.map((s)=>systemMap[s]);
+ brew.tags = _.uniq([...(brew.tags || []), ...systemTags]);
+
+ brew.systems = undefined;
+ return brew;
+};
+
const MAX_TITLE_LENGTH = 100;
const api = {
@@ -52,13 +74,13 @@ const api = {
// ID Validation Checks
// Homebrewery ID
// Typically 12 characters, but the DB shows a range of 7 to 14 characters
- if(!id.match(/^[A-Za-z0-9_-]{7,14}$/)){
+ if(!id.match(/^[a-zA-Z0-9-_]{7,14}$/)){
throw { name: 'ID Error', message: 'Invalid ID', status: 404, HBErrorCode: '11', brewId: id };
}
// Google ID
// Typically 33 characters, old format is 44 - always starts with a 1
// Managed by Google, may change outside of our control, so any length between 33 and 44 is acceptable
- if(googleId && !googleId.match(/^1(?:[A-Za-z0-9+\/]{32,43})$/)){
+ if(googleId && !googleId.match(/^1(?:[a-zA-Z0-9-_]{32,43})$/)){
throw { name: 'Google ID Error', message: 'Invalid ID', status: 404, HBErrorCode: '12', brewId: id };
}
@@ -166,11 +188,13 @@ const api = {
stub.renderer = stub.renderer || undefined; // Clear empty strings
stub = _.defaults(stub, DEFAULT_BREW_LOAD); // Fill in blank fields
- req.brew = stub;
+
+
+ const fixedStub = migrateSystemsToTags(stub);
+ req.brew = fixedStub;
next();
};
},
-
getCSS : async (req, res)=>{
const { brew } = req;
if(!brew) return res.status(404).send('');
@@ -183,7 +207,6 @@ const api = {
});
return res.status(200).send(brew.style);
},
-
mergeBrewText : (brew)=>{
let text = brew.text;
if(brew.style !== undefined) {
@@ -192,18 +215,25 @@ const api = {
`\`\`\`\n\n` +
`${text}`;
}
- const metadata = _.pick(brew, ['title', 'description', 'tags', 'systems', 'renderer', 'theme']);
+ const metadata = _.pick(brew, ['title', 'description', 'tags', 'renderer', 'theme']);
const snippetsArray = brewSnippetsToJSON('brew_snippets', brew.snippets, null, false).snippets;
metadata.snippets = snippetsArray.length > 0 ? snippetsArray : undefined;
+ metadata.bleedSize = { top: brew?.bleedSize?.top, bottom: brew?.bleedSize?.bottom, inner: brew?.bleedSize?.inner, outer: brew?.bleedSize?.outer };
+ metadata.safetySpace = { top: brew?.safetySpace?.top, bottom: brew?.safetySpace?.bottom, outer: brew?.safetySpace?.outer, inner: brew?.safetySpace?.inner };
+ metadata.trimSize = { width: brew?.trimSize?.width, height: brew?.trimSize?.height };
+ metadata.columns = brew?.columns;
+ metadata.columnGutter = brew?.columnGutter;
+ metadata.license = brew?.license;
+ metadata.legalAuthors = brew?.legalAuthors;
+
text = `\`\`\`metadata\n` +
`${yaml.dump(metadata)}\n` +
`\`\`\`\n\n` +
`${text}`;
return text;
},
-
getGoodBrewTitle : (text)=>{
- const tokens = Markdown.marked.lexer(text);
+ const tokens = hbfm.marked.lexer(text);
return (tokens.find((token)=>token.type === 'heading' || token.type === 'paragraph')?.text || 'No Title')
.slice(0, MAX_TITLE_LENGTH);
},
@@ -367,30 +397,40 @@ const api = {
if(brewFromServer?.hash !== brewFromClient?.hash) {
console.log(`Hash mismatch on brew ${brewFromClient.editId}`);
- //debugTextMismatch(brewFromClient.text, brewFromServer.text, `edit/${brewFromClient.editId}`);
+ debugTextMismatch(brewFromClient.text, brewFromServer.text, `edit/${brewFromClient.editId}`);
res.setHeader('Content-Type', 'application/json');
return res.status(409).send(JSON.stringify({ message: `The server copy is out of sync with the saved brew. Please save your changes elsewhere, refresh, and try again.` }));
}
+ let result = [];
try {
const patches = parsePatch(brewFromClient.patches);
// Patch to a throwaway variable while parallelizing - we're more concerned with error/no error.
- const patchedResult = applyPatches(patches, brewFromServer.text, { allowExceedingIndices: true })[0];
- if(patchedResult != brewFromClient.text)
- throw("Patches did not apply cleanly, text mismatch detected");
+ result = applyPatches(patches, encodeURI(brewFromServer.text));
+ const failedPatches = patches.map((patch, index)=>{if(!result[1][index]){ return patch; }});
+ if(failedPatches > 0){
+ throw (`Patch failure: ${failedPatches}/${result[1].length} did not apply`);
+ }
+ if(decodeURI(result[0]) != brewFromClient.text){
+ throw ('Patches did not apply cleanly, text mismatch detected');
+ }
// brew.text = applyPatches(patches, brewFromServer.text)[0];
} catch (err) {
- //debugTextMismatch(brewFromClient.text, brewFromServer.text, `edit/${brewFromClient.editId}`);
+ debugTextMismatch(brewFromClient.text, brewFromServer.text, `edit/${brewFromClient.editId}`);
console.error('Failed to apply patches:', {
- patches : brewFromClient.patches,
- brewId : brewFromClient.editId || 'unknown',
- error : err
+ // patches : brewFromClient.patches,
+ // result : result,
+ brewId : brewFromClient.editId || 'unknown',
+ error : err
});
// While running in parallel, don't throw the error upstream.
// throw err; // rethrow to preserve the 500 behavior
}
let brew = _.assign(brewFromServer, brewFromClient);
+
+ migrateSystemsToTags(brew);
+
brew.title = brew.title.trim();
brew.description = brew.description.trim() || '';
brew.text = api.mergeBrewText(brew);
@@ -480,6 +520,7 @@ const api = {
await HomebrewModel.deleteOne({ editId: id });
return next();
}
+ throw (err);
}
let brew = req.brew;
@@ -530,6 +571,8 @@ const api = {
}
};
+router.use(dbCheck);
+
router.post('/api', checkClientVersion, asyncHandler(api.newBrew));
router.put('/api/:id', checkClientVersion, asyncHandler(api.getBrew('edit', false)), asyncHandler(api.updateBrew));
router.put('/api/update/:id', checkClientVersion, asyncHandler(api.getBrew('edit', false)), asyncHandler(api.updateBrew));
@@ -537,4 +580,4 @@ router.delete('/api/:id', checkClientVersion, asyncHandler(api.deleteBrew));
router.get('/api/remove/:id', checkClientVersion, asyncHandler(api.deleteBrew));
router.get('/api/theme/:renderer/:id', asyncHandler(api.getThemeBundle));
-export default api;
\ No newline at end of file
+export default api;
diff --git a/server/homebrew.api.spec.js b/server/homebrew.api.spec.js
index 0a6d1d452..0e09cc044 100644
--- a/server/homebrew.api.spec.js
+++ b/server/homebrew.api.spec.js
@@ -63,7 +63,6 @@ describe('Tests for api', ()=>{
title : 'some title',
description : 'this is a description',
tags : ['something', 'fun'],
- systems : ['D&D 5e'],
lang : 'en',
renderer : 'v3',
theme : 'phb',
@@ -204,7 +203,6 @@ describe('Tests for api', ()=>{
expect(id).toEqual('abcdefghij');
});
});
-
describe('getBrew', ()=>{
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
const notFoundError = { HBErrorCode: '05', message: 'Brew not found', name: 'BrewLoad Error', status: 404, accessType: 'share', brewId: '1' };
@@ -351,7 +349,6 @@ describe('Tests for api', ()=>{
renderer : 'legacy',
lang : 'en',
shareId : undefined,
- systems : [],
tags : [],
theme : '5ePHB',
thumbnail : '',
@@ -362,7 +359,27 @@ describe('Tests for api', ()=>{
style : undefined,
trashed : false,
updatedAt : undefined,
- views : 0
+ views : 0,
+ bleedSize: {
+ top: '.125in',
+ bottom: '.125in',
+ inner: '.125in',
+ outer: '.125in',
+ },
+ columns: '2',
+ columnGutter: '.125in',
+ legalAuthors: '',
+ license: 'None',
+ safetySpace: {
+ top: '.25in',
+ bottom: '.25in',
+ outer: '.25in',
+ inner: '.5in',
+ },
+ trimSize: {
+ width: '8.5in',
+ height: '11in',
+ },
});
expect(next).toHaveBeenCalled();
expect(api.getId).toHaveBeenCalledWith(req);
@@ -382,7 +399,68 @@ describe('Tests for api', ()=>{
await expect(fn(req, null, next)).rejects.toEqual({ 'HBErrorCode': '51', 'brewId': '1', 'brewTitle': 'test brew', 'code': 404, 'message': 'brew locked' });
});
});
+ describe('Get CSS', ()=>{
+ it('should return brew style content as CSS text', async ()=>{
+ const testBrew = { title: 'test brew', text: '```css\n\nI Have a style!\n```\n\n' };
+ const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
+ api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
+ model.get = jest.fn(()=>toBrewPromise(testBrew));
+
+ const fn = api.getBrew('share', true);
+ const req = { brew: {} };
+ const next = jest.fn();
+ await fn(req, null, next);
+ await api.getCSS(req, res);
+
+ expect(req.brew).toEqual(testBrew);
+ expect(req.brew).toHaveProperty('style', '\nI Have a style!\n');
+ expect(res.status).toHaveBeenCalledWith(200);
+ expect(res.send).toHaveBeenCalledWith('\nI Have a style!\n');
+ expect(res.set).toHaveBeenCalledWith({
+ 'Cache-Control' : 'no-cache',
+ 'Content-Type' : 'text/css'
+ });
+ });
+
+ it('should return 404 when brew has no style content', async ()=>{
+ const testBrew = { title: 'test brew', text: 'I don\'t have a style!' };
+
+ const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
+ api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
+ model.get = jest.fn(()=>toBrewPromise(testBrew));
+
+ const fn = api.getBrew('share', true);
+ const req = { brew: {} };
+ const next = jest.fn();
+ await fn(req, null, next);
+ await api.getCSS(req, res);
+
+ expect(req.brew).toEqual(testBrew);
+ expect(req.brew).toHaveProperty('style');
+ expect(res.status).toHaveBeenCalledWith(404);
+ expect(res.send).toHaveBeenCalledWith('');
+ });
+
+ it('should return 404 when brew does not exist', async ()=>{
+ const testBrew = { };
+
+ const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
+ api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
+ model.get = jest.fn(()=>toBrewPromise(testBrew));
+
+ const fn = api.getBrew('share', true);
+ const req = { brew: {} };
+ const next = jest.fn();
+ await fn(req, null, next);
+ await api.getCSS(req, res);
+
+ expect(req.brew).toEqual(testBrew);
+ expect(req.brew).toHaveProperty('style');
+ expect(res.status).toHaveBeenCalledWith(404);
+ expect(res.send).toHaveBeenCalledWith('');
+ });
+ });
describe('mergeBrewText', ()=>{
it('should set metadata and no style if it is not present', ()=>{
const result = api.mergeBrewText({
@@ -390,10 +468,29 @@ describe('Tests for api', ()=>{
title : 'some title',
description : 'this is a description',
tags : ['something', 'fun'],
- systems : ['D&D 5e'],
renderer : 'v3',
theme : 'phb',
- googleId : '12345'
+ googleId : '12345',
+ bleedSize: {
+ top: '.125in',
+ bottom: '.125in',
+ inner: '.125in',
+ outer: '.125in',
+ },
+ columns: '2',
+ columnGutter: '.125in',
+ legalAuthors: '',
+ license: 'None',
+ safetySpace: {
+ top: '.25in',
+ bottom: '.25in',
+ outer: '.25in',
+ inner: '.5in',
+ },
+ trimSize: {
+ width: '8.5in',
+ height: '11in',
+ },
});
expect(result).toEqual(`\`\`\`metadata
@@ -402,10 +499,25 @@ description: this is a description
tags:
- something
- fun
-systems:
- - D&D 5e
renderer: v3
theme: phb
+bleedSize:
+ top: .125in
+ bottom: .125in
+ inner: .125in
+ outer: .125in
+safetySpace:
+ top: .25in
+ bottom: .25in
+ outer: .25in
+ inner: .5in
+trimSize:
+ width: 8.5in
+ height: 11in
+columns: '2'
+columnGutter: .125in
+license: None
+legalAuthors: ''
\`\`\`
@@ -419,10 +531,29 @@ brew`);
title : 'some title',
description : 'this is a description',
tags : ['something', 'fun'],
- systems : ['D&D 5e'],
renderer : 'v3',
theme : 'phb',
- googleId : '12345'
+ googleId : '12345',
+ bleedSize: {
+ top: '.125in',
+ bottom: '.125in',
+ inner: '.125in',
+ outer: '.125in',
+ },
+ columns: '2',
+ columnGutter: '.125in',
+ legalAuthors: '',
+ license: 'None',
+ safetySpace: {
+ top: '.25in',
+ bottom: '.25in',
+ outer: '.25in',
+ inner: '.5in',
+ },
+ trimSize: {
+ width: '8.5in',
+ height: '11in',
+ },
});
expect(result).toEqual(`\`\`\`metadata
@@ -431,10 +562,25 @@ description: this is a description
tags:
- something
- fun
-systems:
- - D&D 5e
renderer: v3
theme: phb
+bleedSize:
+ top: .125in
+ bottom: .125in
+ inner: .125in
+ outer: .125in
+safetySpace:
+ top: .25in
+ bottom: .25in
+ outer: .25in
+ inner: .5in
+trimSize:
+ width: 8.5in
+ height: 11in
+columns: '2'
+columnGutter: .125in
+license: None
+legalAuthors: ''
\`\`\`
@@ -445,7 +591,6 @@ hello yes i am css
brew`);
});
});
-
describe('exclusion methods', ()=>{
it('excludePropsFromUpdate removes the correct keys', ()=>{
const sent = Object.assign({}, googleBrew);
@@ -463,7 +608,6 @@ brew`);
expect(sent).toEqual(googleBrew);
expect(result.tags).toBeUndefined();
- expect(result.systems).toBeUndefined();
expect(result.published).toBeUndefined();
expect(result.authors).toBeUndefined();
expect(result.owner).toBeUndefined();
@@ -483,7 +627,6 @@ brew`);
expect(result.pageCount).toBe(1);
});
});
-
describe('beforeNewSave', ()=>{
it('sets the title if none', ()=>{
const brew = {
@@ -525,7 +668,6 @@ brew`);
expect(hbBrew.text).toEqual('merged');
});
});
-
describe('newGoogleBrew', ()=>{
it('should call the correct methods', ()=>{
api.excludeGoogleProps = jest.fn(()=>'newBrew');
@@ -539,26 +681,24 @@ brew`);
expect(google.newGoogleBrew).toHaveBeenCalledWith('client', 'newBrew');
});
});
-
describe('newBrew', ()=>{
it('should set up a default brew via Homebrew model', async ()=>{
await api.newBrew({ body: { text: 'asdf' }, query: {}, account: { username: 'test user' } }, res);
expect(res.status).toHaveBeenCalledWith(200);
expect(res.send).toHaveBeenCalledWith({
- _id : '1',
- authors : ['test user'],
- createdAt : undefined,
- description : '',
- editId : expect.any(String),
- gDrive : false,
- pageCount : 1,
- published : false,
- renderer : 'V3',
- lang : 'en',
+ _id : '1',
+ authors : ['test user'],
+ createdAt : undefined,
+ description : '',
+ editId : expect.any(String),
+ gDrive : false,
+ pageCount : 1,
+ published : false,
+ renderer : 'V3',
+ lang : 'en',
shareId : expect.any(String),
style : undefined,
- systems : [],
tags : [],
text : undefined,
textBin : expect.objectContaining({}),
@@ -567,7 +707,27 @@ brew`);
title : 'asdf',
trashed : false,
updatedAt : undefined,
- views : 0
+ views : 0,
+ bleedSize: {
+ top: '.125in',
+ bottom: '.125in',
+ inner: '.125in',
+ outer: '.125in',
+ },
+ columns: '2',
+ columnGutter: '.125in',
+ legalAuthors: '',
+ license: 'None',
+ safetySpace: {
+ top: '.25in',
+ bottom: '.25in',
+ outer: '.25in',
+ inner: '.5in',
+ },
+ trimSize: {
+ width: '8.5in',
+ height: '11in',
+ },
});
});
@@ -605,43 +765,51 @@ brew`);
expect(google.newGoogleBrew).toHaveBeenCalled();
expect(res.status).toHaveBeenCalledWith(200);
expect(res.send).toHaveBeenCalledWith({
- _id : '1',
- authors : ['test user'],
- createdAt : undefined,
- description : '',
- editId : expect.any(String),
- gDrive : false,
- pageCount : 1,
- published : false,
- renderer : 'V3',
- lang : 'en',
- shareId : expect.any(String),
- googleId : expect.any(String),
- style : undefined,
- systems : [],
- tags : [],
- text : undefined,
- textBin : undefined,
- theme : '5ePHB',
- thumbnail : '',
- title : 'asdf',
- trashed : false,
- updatedAt : undefined,
- views : 0
+ _id : '1',
+ authors : ['test user'],
+ bleedSize: {
+ top: '.125in',
+ bottom: '.125in',
+ inner: '.125in',
+ outer: '.125in',
+ },
+ columns: '2',
+ columnGutter: '.125in',
+ createdAt : undefined,
+ description : '',
+ editId : expect.any(String),
+ gDrive : false,
+ pageCount : 1,
+ published : false,
+ renderer : 'V3',
+ lang : 'en',
+ shareId : expect.any(String),
+ legalAuthors: '',
+ license: 'None',
+ safetySpace: {
+ top: '.25in',
+ bottom: '.25in',
+ outer: '.25in',
+ inner: '.5in',
+ },
+ googleId : expect.any(String),
+ style : undefined,
+ tags : [],
+ text : undefined,
+ textBin : undefined,
+ theme : '5ePHB',
+ thumbnail : '',
+ title : 'asdf',
+ trashed : false,
+ trimSize: {
+ width: '8.5in',
+ height: '11in',
+ },
+ updatedAt : undefined,
+ views : 0
});
});
});
-
- describe('deleteGoogleBrew', ()=>{
- it('should check auth and delete brew', async ()=>{
- const result = await api.deleteGoogleBrew({ username: 'test user' }, 'id', 'editId', res);
-
- expect(result).toBe(true);
- expect(google.authCheck).toHaveBeenCalledWith({ username: 'test user' }, expect.objectContaining({}));
- expect(google.deleteGoogleBrew).toHaveBeenCalledWith('client', 'id', 'editId');
- });
- });
-
describe('Theme bundle', ()=>{
it('should return Theme Bundle for a User Theme', async ()=>{
const brews = {
@@ -785,7 +953,94 @@ brew`);
status : 422 });
});
});
+ describe('updateBrew', ()=>{
+ it('should return error on version mismatch', async ()=>{
+ const brewFromClient = { version: 1 };
+ const brewFromServer = { version: 1000, text: '' };
+ const req = {
+ brew : brewFromServer,
+ body : brewFromClient
+ };
+
+ await api.updateBrew(req, res);
+
+ expect(res.status).toHaveBeenCalledWith(409);
+ expect(res.send).toHaveBeenCalledWith('{\"message\":\"The server version is out of sync with the saved brew. Please save your changes elsewhere, refresh, and try again.\"}');
+ });
+
+ it('should return error on hash mismatch', async ()=>{
+ const brewFromClient = { version: 1, hash: '1234' };
+ const brewFromServer = { version: 1, text: 'test' };
+
+ const req = {
+ brew : brewFromServer,
+ body : brewFromClient
+ };
+
+ await api.updateBrew(req, res);
+
+ expect(req.brew.hash).toBe('098f6bcd4621d373cade4e832627b4f6');
+ expect(res.status).toHaveBeenCalledWith(409);
+ expect(res.send).toHaveBeenCalledWith('{\"message\":\"The server copy is out of sync with the saved brew. Please save your changes elsewhere, refresh, and try again.\"}');
+ });
+
+ // Commenting this one out for now, since we are no longer throwing this error while we monitor
+ // it('should return error on applying patches', async ()=>{
+ // const brewFromClient = { version: 1, hash: '098f6bcd4621d373cade4e832627b4f6', patches: 'not a valid patch string' };
+ // const brewFromServer = { version: 1, text: 'test', title: 'Test Title', description: 'Test Description' };
+
+ // const req = {
+ // brew : brewFromServer,
+ // body : brewFromClient,
+ // };
+
+ // let err;
+ // try {
+ // await api.updateBrew(req, res);
+ // } catch (e) {
+ // err = e;
+ // }
+
+ // expect(err).toEqual(Error('Invalid patch string: not a valid patch string'));
+ // });
+
+ it('should save brew, no ID', async ()=>{
+ const brewFromClient = { version: 1, hash: '098f6bcd4621d373cade4e832627b4f6', patches: '' };
+ const brewFromServer = { version: 1, text: 'test', title: 'Test Title', description: 'Test Description' };
+
+ model.save = jest.fn((brew)=>{return brew;});
+
+ const req = {
+ brew : brewFromServer,
+ body : brewFromClient,
+ query : { saveToGoogle: false, removeFromGoogle: false }
+ };
+
+ await api.updateBrew(req, res);
+
+ expect(res.status).toHaveBeenCalledWith(200);
+ expect(res.send).toHaveBeenCalledWith(
+ expect.objectContaining({
+ _id : '1',
+ description : 'Test Description',
+ hash : '098f6bcd4621d373cade4e832627b4f6',
+ title : 'Test Title',
+ version : 2
+ })
+ );
+ });
+ });
+
+ describe('deleteGoogleBrew', ()=>{
+ it('should check auth and delete brew', async ()=>{
+ const result = await api.deleteGoogleBrew({ username: 'test user' }, 'id', 'editId', res);
+
+ expect(result).toBe(true);
+ expect(google.authCheck).toHaveBeenCalledWith({ username: 'test user' }, expect.objectContaining({}));
+ expect(google.deleteGoogleBrew).toHaveBeenCalledWith('client', 'id', 'editId');
+ });
+ });
describe('deleteBrew', ()=>{
it('should handle case where fetching the brew returns an error', async ()=>{
api.getBrew = jest.fn(()=>async ()=>{ throw { message: 'err', HBErrorCode: '02' }; });
@@ -1006,68 +1261,7 @@ brew`);
expect(saved.googleId).toEqual(brew.googleId);
});
});
- describe('Get CSS', ()=>{
- it('should return brew style content as CSS text', async ()=>{
- const testBrew = { title: 'test brew', text: '```css\n\nI Have a style!\n```\n\n' };
-
- const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
- api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
- model.get = jest.fn(()=>toBrewPromise(testBrew));
-
- const fn = api.getBrew('share', true);
- const req = { brew: {} };
- const next = jest.fn();
- await fn(req, null, next);
- await api.getCSS(req, res);
-
- expect(req.brew).toEqual(testBrew);
- expect(req.brew).toHaveProperty('style', '\nI Have a style!\n');
- expect(res.status).toHaveBeenCalledWith(200);
- expect(res.send).toHaveBeenCalledWith('\nI Have a style!\n');
- expect(res.set).toHaveBeenCalledWith({
- 'Cache-Control' : 'no-cache',
- 'Content-Type' : 'text/css'
- });
- });
-
- it('should return 404 when brew has no style content', async ()=>{
- const testBrew = { title: 'test brew', text: 'I don\'t have a style!' };
-
- const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
- api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
- model.get = jest.fn(()=>toBrewPromise(testBrew));
-
- const fn = api.getBrew('share', true);
- const req = { brew: {} };
- const next = jest.fn();
- await fn(req, null, next);
- await api.getCSS(req, res);
-
- expect(req.brew).toEqual(testBrew);
- expect(req.brew).toHaveProperty('style');
- expect(res.status).toHaveBeenCalledWith(404);
- expect(res.send).toHaveBeenCalledWith('');
- });
-
- it('should return 404 when brew does not exist', async ()=>{
- const testBrew = { };
-
- const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
- api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
- model.get = jest.fn(()=>toBrewPromise(testBrew));
-
- const fn = api.getBrew('share', true);
- const req = { brew: {} };
- const next = jest.fn();
- await fn(req, null, next);
- await api.getCSS(req, res);
-
- expect(req.brew).toEqual(testBrew);
- expect(req.brew).toHaveProperty('style');
- expect(res.status).toHaveBeenCalledWith(404);
- expect(res.send).toHaveBeenCalledWith('');
- });
- });
+
describe('Split Text, Style, and Metadata', ()=>{
it('basic splitting', async ()=>{
@@ -1076,7 +1270,6 @@ brew`);
'title: title\n' +
'description: description\n' +
'tags: [ \'tag a\' , \'tag b\' ]\n' +
- 'systems: [ test system ]\n' +
'renderer: legacy\n' +
'theme: 5ePHB\n' +
'lang: en\n' +
@@ -1097,8 +1290,6 @@ brew`);
// Metadata
expect(testBrew.title).toEqual('title');
expect(testBrew.description).toEqual('description');
- expect(testBrew.tags).toEqual(['tag a', 'tag b']);
- expect(testBrew.systems).toEqual(['test system']);
expect(testBrew.renderer).toEqual('legacy');
expect(testBrew.theme).toEqual('5ePHB');
expect(testBrew.lang).toEqual('en');
@@ -1108,96 +1299,78 @@ brew`);
expect(testBrew.text).toEqual('text\n');
});
- it('convert tags string to array', async ()=>{
+ it('extended metadata', async ()=>{
const testBrew = {
text : '```metadata\n' +
- 'tags: tag a\n' +
- '```\n\n'
+ 'title: title\n' +
+ 'description: description\n' +
+ 'tags: [ \'tag a\' , \'tag b\' ]\n' +
+ 'renderer: legacy\n' +
+ 'theme: 5ePHB\n' +
+ 'lang: en\n' +
+ 'bleedSize:\n' +
+ ' top: 1.5in\n' +
+ ' bottom: 1.5in\n' +
+ ' outer: 1.5in\n' +
+ ' inner: 1.5in\n' +
+ 'safetySpace:\n' +
+ ' top: 1.25in\n' +
+ ' bottom: 1.25in\n' +
+ ' outer: 1.25in\n' +
+ ' inner: 1.5in\n' +
+ 'trimSize:\n' +
+ ' width: 18.5in\n' +
+ ' height: 111in\n' +
+ 'columns: 12\n' +
+ 'columnGutter: 1.125in\n' +
+ 'license: AELF\n' +
+ 'legalAuthors: Tom Bombadil\n' +
+ '\n' +
+ '```\n' +
+ '\n' +
+ '```css\n' +
+ 'style\n' +
+ 'style\n' +
+ 'style\n' +
+ '```\n' +
+ '\n' +
+ 'text\n'
};
splitTextStyleAndMetadata(testBrew);
// Metadata
- expect(testBrew.tags).toEqual(['tag a']);
- });
- });
-
- describe('updateBrew', ()=>{
- it('should return error on version mismatch', async ()=>{
- const brewFromClient = { version: 1 };
- const brewFromServer = { version: 1000, text: '' };
-
- const req = {
- brew : brewFromServer,
- body : brewFromClient
- };
-
- await api.updateBrew(req, res);
-
- expect(res.status).toHaveBeenCalledWith(409);
- expect(res.send).toHaveBeenCalledWith('{\"message\":\"The server version is out of sync with the saved brew. Please save your changes elsewhere, refresh, and try again.\"}');
- });
-
- it('should return error on hash mismatch', async ()=>{
- const brewFromClient = { version: 1, hash: '1234' };
- const brewFromServer = { version: 1, text: 'test' };
-
- const req = {
- brew : brewFromServer,
- body : brewFromClient
- };
-
- await api.updateBrew(req, res);
-
- expect(req.brew.hash).toBe('098f6bcd4621d373cade4e832627b4f6');
- expect(res.status).toHaveBeenCalledWith(409);
- expect(res.send).toHaveBeenCalledWith('{\"message\":\"The server copy is out of sync with the saved brew. Please save your changes elsewhere, refresh, and try again.\"}');
- });
-
- // Commenting this one out for now, since we are no longer throwing this error while we monitor
- // it('should return error on applying patches', async ()=>{
- // const brewFromClient = { version: 1, hash: '098f6bcd4621d373cade4e832627b4f6', patches: 'not a valid patch string' };
- // const brewFromServer = { version: 1, text: 'test', title: 'Test Title', description: 'Test Description' };
-
- // const req = {
- // brew : brewFromServer,
- // body : brewFromClient,
- // };
-
- // let err;
- // try {
- // await api.updateBrew(req, res);
- // } catch (e) {
- // err = e;
- // }
-
- // expect(err).toEqual(Error('Invalid patch string: not a valid patch string'));
- // });
-
- it('should save brew, no ID', async ()=>{
- const brewFromClient = { version: 1, hash: '098f6bcd4621d373cade4e832627b4f6', patches: '' };
- const brewFromServer = { version: 1, text: 'test', title: 'Test Title', description: 'Test Description' };
-
- model.save = jest.fn((brew)=>{return brew;});
-
- const req = {
- brew : brewFromServer,
- body : brewFromClient,
- query : { saveToGoogle: false, removeFromGoogle: false }
- };
-
- await api.updateBrew(req, res);
-
- expect(res.status).toHaveBeenCalledWith(200);
- expect(res.send).toHaveBeenCalledWith(
- expect.objectContaining({
- _id : '1',
- description : 'Test Description',
- hash : '098f6bcd4621d373cade4e832627b4f6',
- title : 'Test Title',
- version : 2
- })
- );
+ expect(testBrew.title).toEqual('title');
+ expect(testBrew.description).toEqual('description');
+ expect(testBrew.renderer).toEqual('legacy');
+ expect(testBrew.theme).toEqual('5ePHB');
+ expect(testBrew.lang).toEqual('en');
+ // Paper Specfications
+ expect(testBrew.bleedSize.top).toEqual('1.5in');
+ expect(testBrew.bleedSize.bottom).toEqual('1.5in');
+ expect(testBrew.bleedSize.inner).toEqual('1.5in');
+ expect(testBrew.bleedSize.outer).toEqual('1.5in');
+
+ expect(testBrew.safetySpace.top).toEqual('1.25in');
+ expect(testBrew.safetySpace.bottom).toEqual('1.25in');
+ expect(testBrew.safetySpace.inner).toEqual('1.5in');
+ expect(testBrew.safetySpace.outer).toEqual('1.25in');
+
+ expect(testBrew.trimSize.width).toEqual('18.5in');
+ expect(testBrew.trimSize.height).toEqual('111in');
+
+ expect(testBrew.columns).toEqual(12);
+ expect(testBrew.columnGutter).toEqual('1.125in');
+
+ // Extended Metadata
+ expect(testBrew.license).toEqual('AELF');
+ expect(testBrew.legalAuthors).toEqual('Tom Bombadil');
+
+ // Style
+ expect(testBrew.style).toEqual('style\nstyle\nstyle\n');
+ // Text
+ expect(testBrew.text).toEqual('text\n');
});
});
+
});
diff --git a/server/homebrew.model.js b/server/homebrew.model.js
index 2e74b1de2..e923ac928 100644
--- a/server/homebrew.model.js
+++ b/server/homebrew.model.js
@@ -7,29 +7,29 @@ import zlib from 'zlib';
const HomebrewSchema = mongoose.Schema({
shareId : { type: String, default: ()=>{return nanoid(12);}, index: { unique: true } },
editId : { type: String, default: ()=>{return nanoid(12);}, index: { unique: true } },
- googleId : { type: String },
- title : { type: String, default: '' },
+ googleId : { type: String, index: true },
+ title : { type: String, default: '', index: true },
text : { type: String, default: '' },
textBin : { type: Buffer },
- pageCount : { type: Number, default: 1 },
+ pageCount : { type: Number, default: 1, index: true },
description : { type: String, default: '' },
- tags : [String],
- systems : [String],
- lang : { type: String, default: 'en' },
- renderer : { type: String, default: '' },
- authors : [String],
+ tags : { type: [String], index: true },
+ systems : { type: [String], default: undefined },
+ lang : { type: String, default: 'en', index: true },
+ renderer : { type: String, default: '', index: true },
+ authors : { type: [String], index: true },
invitedAuthors : [String],
- published : { type: Boolean, default: false },
- thumbnail : { type: String, default: '' },
+ published : { type: Boolean, default: false, index: true },
+ thumbnail : { type: String, default: '', index: true },
- createdAt : { type: Date, default: Date.now },
- updatedAt : { type: Date, default: Date.now },
- lastViewed : { type: Date, default: Date.now },
+ createdAt : { type: Date, default: Date.now, index: true },
+ updatedAt : { type: Date, default: Date.now, index: true },
+ lastViewed : { type: Date, default: Date.now, index: true },
views : { type: Number, default: 0 },
- version : { type: Number, default: 1 },
+ version : { type: Number, default: 1, index: true },
- lock : { type: Object }
+ lock : { type: Object, index: true }
}, { versionKey: false });
HomebrewSchema.statics.increaseView = async function(query) {
@@ -43,6 +43,8 @@ HomebrewSchema.statics.increaseView = async function(query) {
return brew;
};
+// STATIC FUNCTIONS
+
HomebrewSchema.statics.get = async function(query, fields=null){
const brew = await Homebrew.findOne(query, fields).orFail()
.catch((error)=>{throw 'Can not find brew';});
@@ -63,6 +65,15 @@ HomebrewSchema.statics.getByUser = async function(username, allowAccess=false, f
return brews;
};
+// INDEXES
+
+HomebrewSchema.index({ updatedAt: -1, lastViewed: -1 });
+HomebrewSchema.index({ published: 1, title: 'text' });
+
+HomebrewSchema.index({ lock: 1, sparse: true });
+HomebrewSchema.path('lock.reviewRequested').index({ sparse: true });
+
+
const Homebrew = mongoose.model('Homebrew', HomebrewSchema);
export {
diff --git a/server/middleware/dbCheck.js b/server/middleware/dbCheck.js
new file mode 100644
index 000000000..f486eab52
--- /dev/null
+++ b/server/middleware/dbCheck.js
@@ -0,0 +1,15 @@
+import mongoose from 'mongoose';
+import config from '../config.js';
+
+export default (req, res, next)=>{
+ // Bypass DB checks during testing
+ if(config.get('node_env') == 'test') return next();
+
+ if(mongoose.connection.readyState == 1) return next();
+ throw {
+ HBErrorCode : '13',
+ name : 'Database Connection Error',
+ message : 'Unable to connect to database',
+ status : mongoose.connection.readyState
+ };
+};
diff --git a/server/middleware/dbCheck.spec.js b/server/middleware/dbCheck.spec.js
new file mode 100644
index 000000000..0c37d40ab
--- /dev/null
+++ b/server/middleware/dbCheck.spec.js
@@ -0,0 +1,28 @@
+import mongoose from 'mongoose';
+import dbCheck from './dbCheck.js';
+import config from '../config.js';
+
+describe('dbCheck middleware', ()=>{
+ const next = jest.fn();
+
+ afterEach(()=>jest.clearAllMocks());
+
+ it('should skip check in test mode', ()=>{
+ config.get = jest.fn(()=>'test');
+ expect(()=>dbCheck({}, {}, next)).not.toThrow();
+ expect(next).toHaveBeenCalled();
+ });
+
+ it('should call next if readyState == 1', ()=>{
+ config.get = jest.fn(()=>'production');
+ mongoose.connection.readyState = 1;
+ dbCheck({}, {}, next);
+ expect(next).toHaveBeenCalled();
+ });
+
+ it('should throw if readyState != 1', ()=>{
+ config.get = jest.fn(()=>'production');
+ mongoose.connection.readyState = 99;
+ expect(()=>dbCheck({}, {}, next)).toThrow(/Unable to connect/);
+ });
+});
\ No newline at end of file
diff --git a/shared/helpers.js b/shared/helpers.js
index e09b0bdc4..db046b810 100644
--- a/shared/helpers.js
+++ b/shared/helpers.js
@@ -1,5 +1,5 @@
import _ from 'lodash';
-import yaml from 'js-yaml';
+import * as yaml from 'js-yaml';
import request from '../client/homebrew/utils/request-middleware.js';
// Convert the templates from a brew to a Snippets Structure.
@@ -8,7 +8,7 @@ const brewSnippetsToJSON = (menuTitle, userBrewSnippets, themeBundleSnippets=nul
const mpAsSnippets = [];
// Snippets from Themes first.
if(themeBundleSnippets) {
- for (let themes of themeBundleSnippets) {
+ for (const themes of themeBundleSnippets) {
if(typeof themes !== 'string') {
const userSnippets = [];
const snipSplit = themes.snippets.trim().split(textSplit).slice(1);
@@ -19,7 +19,7 @@ const brewSnippetsToJSON = (menuTitle, userBrewSnippets, themeBundleSnippets=nul
userSnippets.push({
name : snippetName,
icon : '',
- gen : snipSplit[snips + 1],
+ gen : snipSplit[snips + 1].replace(/\n$/, ''),
});
}
}
@@ -44,7 +44,7 @@ const brewSnippetsToJSON = (menuTitle, userBrewSnippets, themeBundleSnippets=nul
if(snippetName.length != 0) {
const subSnip = {
name : snippetName,
- gen : snipSplit[snips + 1],
+ gen : snipSplit[snips + 1].replace(/\n$/, ''),
};
// if(full) subSnip.icon = '';
userSnippets.push(subSnip);
@@ -76,9 +76,9 @@ const yamlSnippetsToText = (yamlObj)=>{
if(typeof yamlObj == 'string') return yamlObj;
let snippetsText = '';
-
- for (let snippet of yamlObj) {
- for (let subSnippet of snippet.subsnippets) {
+
+ for (const snippet of yamlObj) {
+ for (const subSnippet of snippet.subsnippets) {
snippetsText = `${snippetsText}\\snippet ${subSnippet.name}\n${subSnippet.gen || ''}\n`;
}
}
@@ -91,8 +91,17 @@ const splitTextStyleAndMetadata = (brew)=>{
const index = brew.text.indexOf('\n```\n\n');
const metadataSection = brew.text.slice(11, index + 1);
const metadata = yaml.load(metadataSection);
- Object.assign(brew, _.pick(metadata, ['title', 'description', 'tags', 'systems', 'renderer', 'theme', 'lang']));
+ Object.assign(brew, _.pick(metadata, ['title', 'description', 'renderer', 'theme', 'lang']));
brew.snippets = yamlSnippetsToText(_.pick(metadata, ['snippets']).snippets || '');
+
+ brew.bleedSize = { ...metadata.bleedSize };
+ brew.safetySpace = { ...metadata.safetySpace };
+ brew.trimSize = { ...metadata.trimSize };
+ brew.columns = metadata?.columns;
+ brew.columnGutter = metadata?.columnGutter;
+ brew.license = metadata?.license;
+ brew.legalAuthors = metadata.legalAuthors;
+
brew.text = brew.text.slice(index + 6);
}
if(brew.text.startsWith('```css')) {
@@ -105,48 +114,63 @@ const splitTextStyleAndMetadata = (brew)=>{
if(typeof brew.tags === 'string') brew.tags = brew.tags ? [brew.tags] : [];
};
-const printCurrentBrew = ()=>{
+const printCurrentBrew = async ()=>{
if(window.typeof !== 'undefined') {
- window.frames['BrewRenderer'].contentWindow.print();
- //Force DOM reflow; Print dialog causes a repaint, and @media print CSS somehow makes out-of-view pages disappear
- const node = window.frames['BrewRenderer'].contentDocument.getElementsByClassName('brewRenderer').item(0);
- node.style.display='none';
- node.offsetHeight; // accessing this is enough to trigger a reflow
- node.style.display='';
+ // fire a custom event for the print cycle
+ document.dispatchEvent(new CustomEvent('print:startprep'));
+ try {
+ const iframeDoc = window.frames['BrewRenderer'].contentDocument;
+
+ // get all img elements with lazy loading (currently only elements generated through MarkedJS)
+ const lazyImages = [...iframeDoc.querySelectorAll('img[loading="lazy"]')];
+ lazyImages.forEach((img)=>{ img.loading = 'eager'; });
+
+ // waits for images to load before resolving promise and opening print dialog
+ await Promise.all(
+ lazyImages
+ .filter((img)=>!img.complete)
+ .map((img)=>new Promise((resolve)=>{ img.onload = resolve; img.onerror = resolve; }))
+ );
+
+ window.frames['BrewRenderer'].contentWindow.print();
+
+ //Force DOM reflow; Print dialog causes a repaint, and @media print CSS somehow makes out-of-view pages disappear
+ const node = iframeDoc.getElementsByClassName('brewRenderer').item(0);
+ node.style.display='none';
+ node.offsetHeight; // accessing this is enough to trigger a reflow
+ node.style.display='';
+ } finally {
+ // when lazy load images have all been loaded, and the doc re-rendered for print preview, emit 'finished' event.
+ document.dispatchEvent(new CustomEvent('print:finishedprep'));
+ }
}
};
-const fetchThemeBundle = async (obj, renderer, theme)=>{
+const fetchThemeBundle = async (setError = ()=>{}, setThemeBundle = ()=>{}, renderer, theme)=>{
if(!renderer || !theme) return;
const res = await request
.get(`/api/theme/${renderer}/${theme}`)
.catch((err)=>{
- obj.setState({ error: err });
+ setError(err);
});
if(!res) {
- obj.setState((prevState)=>({
- ...prevState,
- themeBundle : {}
- }));
+ setThemeBundle({});
return;
}
const themeBundle = res.body;
themeBundle.joinedStyles = themeBundle.styles.map((style)=>``).join('\n\n');
- obj.setState((prevState)=>({
- ...prevState,
- themeBundle : themeBundle,
- error : null
- }));
+ setThemeBundle(themeBundle);
+ setError(null);
};
-const debugTextMismatch = (clientTextRaw, serverTextRaw, label) => {
+const debugTextMismatch = (clientTextRaw, serverTextRaw, label)=>{
const clientText = clientTextRaw?.normalize('NFC') || '';
const serverText = serverTextRaw?.normalize('NFC') || '';
const clientBuffer = Buffer.from(clientText, 'utf8');
const serverBuffer = Buffer.from(serverText, 'utf8');
- if (clientBuffer.equals(serverBuffer)) {
+ if(clientBuffer.equals(serverBuffer)) {
console.log(`✅ ${label} text matches byte-for-byte.`);
return;
}
@@ -157,7 +181,7 @@ const debugTextMismatch = (clientTextRaw, serverTextRaw, label) => {
// Byte-level diff
for (let i = 0; i < Math.min(clientBuffer.length, serverBuffer.length); i++) {
- if (clientBuffer[i] !== serverBuffer[i]) {
+ if(clientBuffer[i] !== serverBuffer[i]) {
console.log(`Byte mismatch at offset ${i}: client=0x${clientBuffer[i].toString(16)} server=0x${serverBuffer[i].toString(16)}`);
break;
}
@@ -165,14 +189,40 @@ const debugTextMismatch = (clientTextRaw, serverTextRaw, label) => {
// Char-level diff
for (let i = 0; i < Math.min(clientText.length, serverText.length); i++) {
- if (clientText[i] !== serverText[i]) {
+ if(clientText[i] !== serverText[i]) {
+ const getMismatchContext = (text, index, name, size = 10)=>{
+ const lower = Math.max(index - size, 0);
+ const upper = Math.min(index + size, text.length);
+ const slice = `${JSON.stringify(text.slice(lower, index)).slice(1, -1)}\u001B[31m${JSON.stringify(text[i]).slice(1, -1)}\u001B[0m${JSON.stringify(text.slice(index+1, upper)).slice(1, -1)}`;
+ const lineNo = text.slice(0, index).split('\n').length;
+ const code = `U+${text.charCodeAt(i).toString(16).toUpperCase()}`;
+
+ return {
+ name,
+ lineNo,
+ code,
+ lower,
+ upper,
+ slice
+ };
+ };
+
+ const boundSize = 10;
+
+ const clientContext = getMismatchContext(clientText, i, 'Client', boundSize);
+ const serverContext = getMismatchContext(serverText, i, 'Server', boundSize);
+
+ const logContext = (context)=>{
+ console.log(` ${context.name} - line ${context.lineNo} : (${context.code})\t${context.slice}`);
+ };
+
console.log(`Char mismatch at index ${i}:`);
- console.log(` Client: '${clientText[i]}' (U+${clientText.charCodeAt(i).toString(16).toUpperCase()})`);
- console.log(` Server: '${serverText[i]}' (U+${serverText.charCodeAt(i).toString(16).toUpperCase()})`);
+ logContext(clientContext);
+ logContext(serverContext);
break;
}
}
-}
+};
export {
splitTextStyleAndMetadata,
diff --git a/shared/naturalcrit/markdownLegacy.js b/shared/markdownLegacy.js
similarity index 96%
rename from shared/naturalcrit/markdownLegacy.js
rename to shared/markdownLegacy.js
index a6a37d639..b26bd8cdc 100644
--- a/shared/naturalcrit/markdownLegacy.js
+++ b/shared/markdownLegacy.js
@@ -1,5 +1,5 @@
-const _ = require('lodash');
-const Markdown = require('markedLegacy');
+import _ from 'lodash';
+import Markdown from 'markedLegacy';
const renderer = new Markdown.Renderer();
//Processes the markdown within an HTML block if it's just a class-wrapper
@@ -34,9 +34,9 @@ renderer.link = function (href, title, text) {
if(title) {
out += ` title="${title}"`;
}
- if(self) {
- out += ' target="_self"';
- }
+ // if(self) {
+ // out += ' target="_self"';
+ // }
out += `>${text}`;
return out;
};
@@ -49,7 +49,7 @@ const cleanUrl = function (sanitize, base, href) {
prot = decodeURIComponent(unescape(href))
.replace(nonWordAndColonTest, '')
.toLowerCase();
- } catch (e) {
+ } catch {
return null;
}
if(prot.indexOf('javascript:') === 0 || prot.indexOf('vbscript:') === 0 || prot.indexOf('data:') === 0) {
@@ -58,7 +58,7 @@ const cleanUrl = function (sanitize, base, href) {
}
try {
href = encodeURI(href).replace(/%25/g, '%');
- } catch (e) {
+ } catch {
return null;
}
return href;
@@ -103,7 +103,7 @@ const voidTags = new Set([
]);
-module.exports = {
+export default {
marked : Markdown,
render : (rawBrewText)=>{
return Markdown(
diff --git a/shared/naturalcrit/codeEditor/autocompleteEmoji.js b/shared/naturalcrit/codeEditor/autocompleteEmoji.js
deleted file mode 100644
index c7efa172b..000000000
--- a/shared/naturalcrit/codeEditor/autocompleteEmoji.js
+++ /dev/null
@@ -1,84 +0,0 @@
-import diceFont from '../../../themes/fonts/iconFonts/diceFont.js';
-import elderberryInn from '../../../themes/fonts/iconFonts/elderberryInn.js';
-import fontAwesome from '../../../themes/fonts/iconFonts/fontAwesome.js';
-import gameIcons from '../../../themes/fonts/iconFonts/gameIcons.js';
-
-const emojis = {
- ...diceFont,
- ...elderberryInn,
- ...fontAwesome,
- ...gameIcons
-};
-
-const showAutocompleteEmoji = function(CodeMirror, editor) {
- CodeMirror.commands.autocomplete = function(editor) {
- editor.showHint({
- completeSingle : false,
- hint : function(editor) {
- const cursor = editor.getCursor();
- const line = cursor.line;
- const lineContent = editor.getLine(line);
- const start = lineContent.lastIndexOf(':', cursor.ch - 1) + 1;
- const end = cursor.ch;
- const currentWord = lineContent.slice(start, end);
-
-
- const list = Object.keys(emojis).filter(function(emoji) {
- return emoji.toLowerCase().indexOf(currentWord.toLowerCase()) >= 0;
- }).sort((a, b)=>{
- const lowerA = a.replace(/\d+/g, function(match) { // Temporarily convert any numbers in emoji string
- return match.padStart(4, '0'); // to 4-digits, left-padded with 0's, to aid in
- }).toLowerCase(); // sorting numbers, i.e., "d6, d10, d20", not "d10, d20, d6"
- const lowerB = b.replace(/\d+/g, function(match) { // Also make lowercase for case-insensitive alpha sorting
- return match.padStart(4, '0');
- }).toLowerCase();
-
- if(lowerA < lowerB)
- return -1;
- return 1;
- }).map(function(emoji) {
- return {
- text : `${emoji}:`, // Text to output to editor when option is selected
- render : function(element, self, data) { // How to display the option in the dropdown
- const div = document.createElement('div');
- div.innerHTML = ` ${emoji}`;
- element.appendChild(div);
- }
- };
- });
-
- return {
- list : list.length ? list : [],
- from : CodeMirror.Pos(line, start),
- to : CodeMirror.Pos(line, end)
- };
- }
- });
- };
-
- editor.on('inputRead', function(instance, change) {
- const cursor = editor.getCursor();
- const line = editor.getLine(cursor.line);
-
- // Get the text from the start of the line to the cursor
- const textToCursor = line.slice(0, cursor.ch);
-
- // Do not autosuggest emojis in curly span/div/injector properties
- if(line.includes('{')) {
- const curlyToCursor = textToCursor.slice(textToCursor.indexOf(`{`));
- const curlySpanRegex = /{(?=((?:[:=](?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':={}\s]*)*))\1$/g;
-
- if(curlySpanRegex.test(curlyToCursor))
- return;
- }
-
- // Check if the text ends with ':xyz'
- if(/:[^\s:]+$/.test(textToCursor)) {
- CodeMirror.commands.autocomplete(editor);
- }
- });
-};
-
-module.exports = {
- showAutocompleteEmoji
-};
\ No newline at end of file
diff --git a/shared/naturalcrit/codeEditor/close-tag.js b/shared/naturalcrit/codeEditor/close-tag.js
deleted file mode 100644
index 728b63a5c..000000000
--- a/shared/naturalcrit/codeEditor/close-tag.js
+++ /dev/null
@@ -1,48 +0,0 @@
-const autoCloseCurlyBraces = function(CodeMirror, cm, typingClosingBrace) {
- const ranges = cm.listSelections(), replacements = [];
- for (let i = 0; i < ranges.length; i++) {
- if(!ranges[i].empty()) return CodeMirror.Pass;
- const pos = ranges[i].head, line = cm.getLine(pos.line), tok = cm.getTokenAt(pos);
- if(!typingClosingBrace && (tok.type == 'string' || tok.string.charAt(0) != '{' || tok.start != pos.ch - 1))
- return CodeMirror.Pass;
- else if(typingClosingBrace) {
- let hasUnclosedBraces = false, index = -1;
- do {
- index = line.indexOf('{{', index + 1);
- if(index !== -1 && line.indexOf('}}', index + 1) === -1) {
- hasUnclosedBraces = true;
- break;
- }
- } while (index !== -1);
- if(!hasUnclosedBraces) return CodeMirror.Pass;
- }
-
- replacements[i] = typingClosingBrace ? {
- text : '}}',
- newPos : CodeMirror.Pos(pos.line, pos.ch + 2)
- } : {
- text : '{}}',
- newPos : CodeMirror.Pos(pos.line, pos.ch + 1)
- };
- }
-
- for (let i = ranges.length - 1; i >= 0; i--) {
- const info = replacements[i];
- cm.replaceRange(info.text, ranges[i].head, ranges[i].anchor, '+insert');
- const sel = cm.listSelections().slice(0);
- sel[i] = {
- head : info.newPos,
- anchor : info.newPos
- };
- cm.setSelections(sel);
- }
-};
-
-module.exports = {
- autoCloseCurlyBraces : function(CodeMirror, codeMirror) {
- const map = { name: 'autoCloseCurlyBraces' };
- map[`'{'`] = function(cm) { return autoCloseCurlyBraces(CodeMirror, cm); };
- map[`'}'`] = function(cm) { return autoCloseCurlyBraces(CodeMirror, cm, true); };
- codeMirror.addKeyMap(map);
- }
-};
\ No newline at end of file
diff --git a/shared/naturalcrit/codeEditor/codeEditor.jsx b/shared/naturalcrit/codeEditor/codeEditor.jsx
deleted file mode 100644
index fb69b6dcf..000000000
--- a/shared/naturalcrit/codeEditor/codeEditor.jsx
+++ /dev/null
@@ -1,465 +0,0 @@
-/* eslint-disable max-lines */
-require('./codeEditor.less');
-const React = require('react');
-const createClass = require('create-react-class');
-const _ = require('lodash');
-const closeTag = require('./close-tag');
-const autoCompleteEmoji = require('./autocompleteEmoji');
-
-let CodeMirror;
-if(typeof window !== 'undefined'){
- CodeMirror = require('codemirror');
-
- //Language Modes
- require('codemirror/mode/gfm/gfm.js'); //Github flavoured markdown
- require('codemirror/mode/css/css.js');
- require('codemirror/mode/javascript/javascript.js');
-
- //Addons
- //Code folding
- require('codemirror/addon/fold/foldcode.js');
- require('codemirror/addon/fold/foldgutter.js');
- //Search and replace
- require('codemirror/addon/search/search.js');
- require('codemirror/addon/search/searchcursor.js');
- require('codemirror/addon/search/jump-to-line.js');
- require('codemirror/addon/search/match-highlighter.js');
- require('codemirror/addon/search/matchesonscrollbar.js');
- require('codemirror/addon/dialog/dialog.js');
- //Trailing space highlighting
- // require('codemirror/addon/edit/trailingspace.js');
- //Active line highlighting
- // require('codemirror/addon/selection/active-line.js');
- //Scroll past last line
- require('codemirror/addon/scroll/scrollpastend.js');
- //Auto-closing
- //XML code folding is a requirement of the auto-closing tag feature and is not enabled
- require('codemirror/addon/fold/xml-fold.js');
- require('codemirror/addon/edit/closetag.js');
- //Autocompletion
- require('codemirror/addon/hint/show-hint.js');
-
- const foldPagesCode = require('./fold-pages');
- foldPagesCode.registerHomebreweryHelper(CodeMirror);
- const foldCSSCode = require('./fold-css');
- foldCSSCode.registerHomebreweryHelper(CodeMirror);
-}
-
-const CodeEditor = createClass({
- displayName : 'CodeEditor',
- getDefaultProps : function() {
- return {
- language : '',
- value : '',
- wrap : true,
- onChange : ()=>{},
- enableFolding : true,
- editorTheme : 'default'
- };
- },
-
- getInitialState : function() {
- return {
- docs : {}
- };
- },
-
- editor : React.createRef(null),
-
- componentDidMount : function() {
- this.buildEditor();
- const newDoc = CodeMirror.Doc(this.props.value, this.props.language);
- this.codeMirror.swapDoc(newDoc);
- },
-
- componentDidUpdate : function(prevProps) {
- if(prevProps.view !== this.props.view){ //view changed; swap documents
- let newDoc;
-
- if(!this.state.docs[this.props.view]) {
- newDoc = CodeMirror.Doc(this.props.value, this.props.language);
- } else {
- newDoc = this.state.docs[this.props.view];
- }
-
- const oldDoc = { [prevProps.view]: this.codeMirror.swapDoc(newDoc) };
-
- this.setState((prevState)=>({
- docs : _.merge({}, prevState.docs, oldDoc)
- }));
-
- this.props.rerenderParent();
- } else if(this.codeMirror?.getValue() != this.props.value) { //update editor contents if brew.text is changed from outside
- this.codeMirror.setValue(this.props.value);
- }
-
- if(this.props.enableFolding) {
- this.codeMirror.setOption('foldOptions', this.foldOptions(this.codeMirror));
- } else {
- this.codeMirror.setOption('foldOptions', false);
- }
-
- if(prevProps.editorTheme !== this.props.editorTheme){
- this.codeMirror.setOption('theme', this.props.editorTheme);
- }
- },
-
- buildEditor : function() {
- this.codeMirror = CodeMirror(this.editor.current, {
- lineNumbers : true,
- lineWrapping : this.props.wrap,
- indentWithTabs : false,
- tabSize : 2,
- smartIndent : false,
- historyEventDelay : 250,
- scrollPastEnd : true,
- extraKeys : {
- 'Tab' : this.indent,
- 'Shift-Tab' : this.dedent,
- 'Ctrl-B' : this.makeBold,
- 'Cmd-B' : this.makeBold,
- 'Shift-Ctrl-=' : this.makeSuper,
- 'Shift-Cmd-=' : this.makeSuper,
- 'Ctrl-=' : this.makeSub,
- 'Cmd-=' : this.makeSub,
- 'Ctrl-I' : this.makeItalic,
- 'Cmd-I' : this.makeItalic,
- 'Ctrl-U' : this.makeUnderline,
- 'Cmd-U' : this.makeUnderline,
- 'Ctrl-.' : this.makeNbsp,
- 'Cmd-.' : this.makeNbsp,
- 'Shift-Ctrl-.' : this.makeSpace,
- 'Shift-Cmd-.' : this.makeSpace,
- 'Shift-Ctrl-,' : this.removeSpace,
- 'Shift-Cmd-,' : this.removeSpace,
- 'Ctrl-M' : this.makeSpan,
- 'Cmd-M' : this.makeSpan,
- 'Shift-Ctrl-M' : this.makeDiv,
- 'Shift-Cmd-M' : this.makeDiv,
- 'Ctrl-/' : this.makeComment,
- 'Cmd-/' : this.makeComment,
- 'Ctrl-K' : this.makeLink,
- 'Cmd-K' : this.makeLink,
- 'Ctrl-L' : ()=>this.makeList('UL'),
- 'Cmd-L' : ()=>this.makeList('UL'),
- 'Shift-Ctrl-L' : ()=>this.makeList('OL'),
- 'Shift-Cmd-L' : ()=>this.makeList('OL'),
- 'Shift-Ctrl-1' : ()=>this.makeHeader(1),
- 'Shift-Ctrl-2' : ()=>this.makeHeader(2),
- 'Shift-Ctrl-3' : ()=>this.makeHeader(3),
- 'Shift-Ctrl-4' : ()=>this.makeHeader(4),
- 'Shift-Ctrl-5' : ()=>this.makeHeader(5),
- 'Shift-Ctrl-6' : ()=>this.makeHeader(6),
- 'Shift-Cmd-1' : ()=>this.makeHeader(1),
- 'Shift-Cmd-2' : ()=>this.makeHeader(2),
- 'Shift-Cmd-3' : ()=>this.makeHeader(3),
- 'Shift-Cmd-4' : ()=>this.makeHeader(4),
- 'Shift-Cmd-5' : ()=>this.makeHeader(5),
- 'Shift-Cmd-6' : ()=>this.makeHeader(6),
- 'Shift-Ctrl-Enter' : this.newColumn,
- 'Shift-Cmd-Enter' : this.newColumn,
- 'Ctrl-Enter' : this.newPage,
- 'Cmd-Enter' : this.newPage,
- 'Ctrl-F' : 'findPersistent',
- 'Cmd-F' : 'findPersistent',
- 'Shift-Enter' : 'findPersistentPrevious',
- 'Ctrl-[' : this.foldAllCode,
- 'Cmd-[' : this.foldAllCode,
- 'Ctrl-]' : this.unfoldAllCode,
- 'Cmd-]' : this.unfoldAllCode
- },
- foldGutter : true,
- foldOptions : this.foldOptions(this.codeMirror),
- gutters : ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
- autoCloseTags : true,
- styleActiveLine : true,
- showTrailingSpace : false,
- theme : this.props.editorTheme
- // specialChars : / /,
- // specialCharPlaceholder : function(char) {
- // const el = document.createElement('span');
- // el.className = 'cm-space';
- // el.innerHTML = ' ';
- // return el;
- // }
- });
-
- // Add custom behaviors (auto-close curlies and auto-complete emojis)
- closeTag.autoCloseCurlyBraces(CodeMirror, this.codeMirror);
- autoCompleteEmoji.showAutocompleteEmoji(CodeMirror, this.codeMirror);
-
- // Note: codeMirror passes a copy of itself in this callback. cm === this.codeMirror. Either one works.
- this.codeMirror.on('change', (cm)=>{this.props.onChange(cm.getValue());});
- this.updateSize();
- },
-
- indent : function () {
- const cm = this.codeMirror;
- if(cm.somethingSelected()) {
- cm.execCommand('indentMore');
- } else {
- cm.execCommand('insertSoftTab');
- }
- },
-
- dedent : function () {
- this.codeMirror.execCommand('indentLess');
- },
-
- makeHeader : function (number) {
- const selection = this.codeMirror.getSelection();
- const header = Array(number).fill('#').join('');
- this.codeMirror.replaceSelection(`${header} ${selection}`, 'around');
- const cursor = this.codeMirror.getCursor();
- this.codeMirror.setCursor({ line: cursor.line, ch: cursor.ch + selection.length + number + 1 });
- },
-
- makeBold : function() {
- const selection = this.codeMirror.getSelection(), t = selection.slice(0, 2) === '**' && selection.slice(-2) === '**';
- this.codeMirror.replaceSelection(t ? selection.slice(2, -2) : `**${selection}**`, 'around');
- if(selection.length === 0){
- const cursor = this.codeMirror.getCursor();
- this.codeMirror.setCursor({ line: cursor.line, ch: cursor.ch - 2 });
- }
- },
-
- makeItalic : function() {
- const selection = this.codeMirror.getSelection(), t = selection.slice(0, 1) === '*' && selection.slice(-1) === '*';
- this.codeMirror.replaceSelection(t ? selection.slice(1, -1) : `*${selection}*`, 'around');
- if(selection.length === 0){
- const cursor = this.codeMirror.getCursor();
- this.codeMirror.setCursor({ line: cursor.line, ch: cursor.ch - 1 });
- }
- },
-
- makeSuper : function() {
- const selection = this.codeMirror.getSelection(), t = selection.slice(0, 1) === '^' && selection.slice(-1) === '^';
- this.codeMirror.replaceSelection(t ? selection.slice(1, -1) : `^${selection}^`, 'around');
- if(selection.length === 0){
- const cursor = this.codeMirror.getCursor();
- this.codeMirror.setCursor({ line: cursor.line, ch: cursor.ch - 1 });
- }
- },
-
- makeSub : function() {
- const selection = this.codeMirror.getSelection(), t = selection.slice(0, 2) === '^^' && selection.slice(-2) === '^^';
- this.codeMirror.replaceSelection(t ? selection.slice(2, -2) : `^^${selection}^^`, 'around');
- if(selection.length === 0){
- const cursor = this.codeMirror.getCursor();
- this.codeMirror.setCursor({ line: cursor.line, ch: cursor.ch - 2 });
- }
- },
-
-
- makeNbsp : function() {
- this.codeMirror.replaceSelection(' ', 'end');
- },
-
- makeSpace : function() {
- const selection = this.codeMirror.getSelection();
- const t = selection.slice(0, 8) === '{{width:' && selection.slice(0 -4) === '% }}';
- if(t){
- const percent = parseInt(selection.slice(8, -4)) + 10;
- this.codeMirror.replaceSelection(percent < 90 ? `{{width:${percent}% }}` : '{{width:100% }}', 'around');
- } else {
- this.codeMirror.replaceSelection(`{{width:10% }}`, 'around');
- }
- },
-
- removeSpace : function() {
- const selection = this.codeMirror.getSelection();
- const t = selection.slice(0, 8) === '{{width:' && selection.slice(0 -4) === '% }}';
- if(t){
- const percent = parseInt(selection.slice(8, -4)) - 10;
- this.codeMirror.replaceSelection(percent > 10 ? `{{width:${percent}% }}` : '', 'around');
- }
- },
-
- newColumn : function() {
- this.codeMirror.replaceSelection('\n\\column\n\n', 'end');
- },
-
- newPage : function() {
- this.codeMirror.replaceSelection('\n\\page\n\n', 'end');
- },
-
- injectText : function(injectText, overwrite=true) {
- const cm = this.codeMirror;
- if(!overwrite) {
- cm.setCursor(cm.getCursor('from'));
- }
- cm.replaceSelection(injectText, 'end');
- cm.focus();
- },
-
- makeUnderline : function() {
- const selection = this.codeMirror.getSelection(), t = selection.slice(0, 3) === '' && selection.slice(-4) === '';
- this.codeMirror.replaceSelection(t ? selection.slice(3, -4) : `${selection}`, 'around');
- if(selection.length === 0){
- const cursor = this.codeMirror.getCursor();
- this.codeMirror.setCursor({ line: cursor.line, ch: cursor.ch - 4 });
- }
- },
-
- makeSpan : function() {
- const selection = this.codeMirror.getSelection(), t = selection.slice(0, 2) === '{{' && selection.slice(-2) === '}}';
- this.codeMirror.replaceSelection(t ? selection.slice(2, -2) : `{{ ${selection}}}`, 'around');
- if(selection.length === 0){
- const cursor = this.codeMirror.getCursor();
- this.codeMirror.setCursor({ line: cursor.line, ch: cursor.ch - 2 });
- }
- },
-
- makeDiv : function() {
- const selection = this.codeMirror.getSelection(), t = selection.slice(0, 2) === '{{' && selection.slice(-2) === '}}';
- this.codeMirror.replaceSelection(t ? selection.slice(2, -2) : `{{\n${selection}\n}}`, 'around');
- if(selection.length === 0){
- const cursor = this.codeMirror.getCursor();
- this.codeMirror.setCursor({ line: cursor.line - 1, ch: cursor.ch }); // set to -2? if wanting to enter classes etc. if so, get rid of first \n when replacing selection
- }
- },
-
- makeComment : function() {
- let regex;
- let cursorPos;
- let newComment;
- const selection = this.codeMirror.getSelection();
- if(this.props.language === 'gfm'){
- regex = /^\s*()\s*$/gs;
- cursorPos = 4;
- newComment = ``;
- } else {
- regex = /^\s*(\/\*\s?)(.*?)(\s?\*\/)\s*$/gs;
- cursorPos = 3;
- newComment = `/* ${selection} */`;
- }
- this.codeMirror.replaceSelection(regex.test(selection) == true ? selection.replace(regex, '$2') : newComment, 'around');
- if(selection.length === 0){
- const cursor = this.codeMirror.getCursor();
- this.codeMirror.setCursor({ line: cursor.line, ch: cursor.ch - cursorPos });
- };
- },
-
- makeLink : function() {
- const isLink = /^\[(.*)\]\((.*)\)$/;
- const selection = this.codeMirror.getSelection().trim();
- let match;
- if(match = isLink.exec(selection)){
- const altText = match[1];
- const url = match[2];
- this.codeMirror.replaceSelection(`${altText} ${url}`);
- const cursor = this.codeMirror.getCursor();
- this.codeMirror.setSelection({ line: cursor.line, ch: cursor.ch - url.length }, { line: cursor.line, ch: cursor.ch });
- } else {
- this.codeMirror.replaceSelection(`[${selection || 'alt text'}](url)`);
- const cursor = this.codeMirror.getCursor();
- this.codeMirror.setSelection({ line: cursor.line, ch: cursor.ch - 4 }, { line: cursor.line, ch: cursor.ch - 1 });
- }
- },
-
- makeList : function(listType) {
- const selectionStart = this.codeMirror.getCursor('from'), selectionEnd = this.codeMirror.getCursor('to');
- this.codeMirror.setSelection(
- { line: selectionStart.line, ch: 0 },
- { line: selectionEnd.line, ch: this.codeMirror.getLine(selectionEnd.line).length }
- );
- const newSelection = this.codeMirror.getSelection();
-
- const regex = /^\d+\.\s|^-\s/gm;
- if(newSelection.match(regex) != null){ // if selection IS A LIST
- this.codeMirror.replaceSelection(newSelection.replace(regex, ''), 'around');
- } else { // if selection IS NOT A LIST
- listType == 'UL' ? this.codeMirror.replaceSelection(newSelection.replace(/^/gm, `- `), 'around') :
- this.codeMirror.replaceSelection(newSelection.replace(/^/gm, (()=>{
- let n = 1;
- return ()=>{
- return `${n++}. `;
- };
- })()), 'around');
- }
- },
-
- foldAllCode : function() {
- this.codeMirror.execCommand('foldAll');
- },
-
- unfoldAllCode : function() {
- this.codeMirror.execCommand('unfoldAll');
- },
-
- //=-- Externally used -==//
- setCursorPosition : function(line, char){
- setTimeout(()=>{
- this.codeMirror.focus();
- this.codeMirror.doc.setCursor(line, char);
- }, 10);
- },
- getCursorPosition : function(){
- return this.codeMirror.getCursor();
- },
- getTopVisibleLine : function(){
- const rect = this.codeMirror.getWrapperElement().getBoundingClientRect();
- const topVisibleLine = this.codeMirror.lineAtHeight(rect.top, 'window');
- return topVisibleLine;
- },
- updateSize : function(){
- this.codeMirror.refresh();
- },
- redo : function(){
- return this.codeMirror.redo();
- },
- undo : function(){
- return this.codeMirror.undo();
- },
- historySize : function(){
- return this.codeMirror.doc.historySize();
- },
-
- foldOptions : function(cm){
- return {
- scanUp : true,
- rangeFinder : this.props.language === 'css' ? CodeMirror.fold.homebrewerycss : CodeMirror.fold.homebrewery,
- widget : (from, to)=>{
- let text = '';
- let currentLine = from.line;
- let maxLength = 50;
-
- let foldPreviewText = '';
- while (currentLine <= to.line && text.length <= maxLength) {
- const currentText = this.codeMirror.getLine(currentLine);
- currentLine++;
- if(currentText[0] == '#'){
- foldPreviewText = currentText;
- break;
- }
- if(!foldPreviewText && currentText != '\n') {
- foldPreviewText = currentText;
- }
- }
- text = foldPreviewText || `Lines ${from.line+1}-${to.line+1}`;
- text = text.replace('{', '').trim();
-
- // Truncate data URLs at `data:`
- const startOfData = text.indexOf('data:');
- if(startOfData > 0)
- maxLength = Math.min(startOfData + 5, maxLength);
-
- if(text.length > maxLength)
- text = `${text.slice(0, maxLength)}...`;
-
- return `\u21A4 ${text} \u21A6`;
- }
- };
- },
- //----------------------//
-
- render : function(){
- return <>
-
- tags -renderer.paragraph = function(token){ - let match; - const text = this.parser.parseInline(token.tokens); - if(text.startsWith('
Bold text
\nParagraph
'); }); test('Block Token cannot be the Term of a multi-line definition', function() { const source = '## Header\n::Definition 1 of a single-line DL\n::Definition 1 of another single-line DL'; - const rendered = Markdown.render(source).trim(); + const rendered = hbfm.render(source).trim(); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('Term 1
\n\n`); }); test('Multiline Definition List must have at least one non-newline character after ::', function() { const source = 'Term 1\n::\nDefinition 1\n\n'; - const rendered = Markdown.render(source).trim(); + const rendered = hbfm.render(source).trim(); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`Term 1
\n\n\nDefinition 1
`); }); }); diff --git a/tests/markdown/emojis.test.js b/tests/markdown/emojis.test.js index e70ebb673..cbbdbf0f5 100644 --- a/tests/markdown/emojis.test.js +++ b/tests/markdown/emojis.test.js @@ -1,5 +1,5 @@ -import Markdown from 'naturalcrit/markdown.js'; -const dedent = require('dedent-tabs').default; +import { hbfm } from 'hbmarkedwrapper'; +import dedent from 'dedent'; // Marked.js adds line returns after closing tags on some default tokens. // This removes those line returns for comparison sake. @@ -12,37 +12,37 @@ const emoji = 'df_d12_2'; describe(`When emojis/icons are active`, ()=>{ it('when a word is between two colons (:word:), and a matching emoji exists, it is rendered as an emoji', function() { const source = `:${emoji}:`; - const rendered = Markdown.render(source).trimReturns(); + const rendered = hbfm.render(source).trimReturns(); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(``); }); it('when a word is between two colons (:word:), and no matching emoji exists, it is not parsed', function() { const source = `:invalid:`; - const rendered = Markdown.render(source).trimReturns(); + const rendered = hbfm.render(source).trimReturns(); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`
:invalid:
`); }); it('two valid emojis with no whitespace are prioritized over definition lists', function() { const source = `:${emoji}::${emoji}:`; - const rendered = Markdown.render(source).trimReturns(); + const rendered = hbfm.render(source).trimReturns(); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(``); }); it('definition lists that are not also part of an emoji can coexist with normal emojis', function() { const source = `definition :: term ${emoji}::${emoji}:`; - const rendered = Markdown.render(source).trimReturns(); + const rendered = hbfm.render(source).trimReturns(); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`
`); }); it('Emojis are not parsed inside of curly span CSS blocks', function() { const source = `{{color:${emoji} text}}`; - const rendered = Markdown.render(source).trimReturns(); + const rendered = hbfm.render(source).trimReturns(); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`text`); }); @@ -50,7 +50,7 @@ describe(`When emojis/icons are active`, ()=>{ const source = dedent`{{color:${emoji} text }}`; - const rendered = Markdown.render(source).trimReturns(); + const rendered = hbfm.render(source).trimReturns(); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`
text
Line 1
\n\n\nLine 2
`); }); test('Ignored inside a code block', function() { const source = '```\n\n:\n\n```\n'; - const rendered = Markdown.render(source).trim(); + const rendered = hbfm.render(source).trim(); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`\n:\n`);
});
});
diff --git a/tests/markdown/mustache-syntax.test.js b/tests/markdown/mustache-syntax.test.js
index d17518411..dceb6e1e1 100644
--- a/tests/markdown/mustache-syntax.test.js
+++ b/tests/markdown/mustache-syntax.test.js
@@ -1,7 +1,7 @@
/* eslint-disable max-lines */
-const dedent = require('dedent-tabs').default;
-import Markdown from 'naturalcrit/markdown.js';
+import dedent from 'dedent';
+import { hbfm } from 'hbmarkedwrapper';
// Marked.js adds line returns after closing tags on some default tokens.
// This removes those line returns for comparison sake.
@@ -15,112 +15,112 @@ String.prototype.trimReturns = function(){
describe('Inline: When using the Inline syntax {{ }}', ()=>{
it('Renders a mustache span with text only', function() {
const source = '{{ text}}';
- const rendered = Markdown.render(source);
+ const rendered = hbfm.render(source);
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('text');
});
it('Renders a mustache span with text only, but with spaces', function() {
const source = '{{ this is a text}}';
- const rendered = Markdown.render(source);
+ const rendered = hbfm.render(source);
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('this is a text');
});
it('Renders an empty mustache span', function() {
const source = '{{}}';
- const rendered = Markdown.render(source);
+ const rendered = hbfm.render(source);
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('');
});
it('Renders a mustache span with just a space', function() {
const source = '{{ }}';
- const rendered = Markdown.render(source);
+ const rendered = hbfm.render(source);
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('');
});
it('Renders a mustache span with a few spaces only', function() {
const source = '{{ }}';
- const rendered = Markdown.render(source);
+ const rendered = hbfm.render(source);
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('');
});
it('Renders a mustache span with text and class', function() {
const source = '{{my-class text}}';
- const rendered = Markdown.render(source);
+ const rendered = hbfm.render(source);
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('text');
});
it('Renders a mustache span with text and two classes', function() {
const source = '{{my-class,my-class2 text}}';
- const rendered = Markdown.render(source);
+ const rendered = hbfm.render(source);
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('text');
});
it('Renders a mustache span with text with spaces and class', function() {
const source = '{{my-class this is a text}}';
- const rendered = Markdown.render(source);
+ const rendered = hbfm.render(source);
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('this is a text');
});
it('Renders a mustache span with text and id', function() {
const source = '{{#my-span text}}';
- const rendered = Markdown.render(source);
+ const rendered = hbfm.render(source);
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('text');
});
it('Renders a mustache span with text and two ids', function() {
const source = '{{#my-span,#my-favorite-span text}}';
- const rendered = Markdown.render(source);
+ const rendered = hbfm.render(source);
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('text');
});
it('Renders a mustache span with text and css property', function() {
const source = '{{color:red text}}';
- const rendered = Markdown.render(source);
+ const rendered = hbfm.render(source);
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('text');
});
it('Renders a mustache span with text and two css properties', function() {
const source = '{{color:red,padding:5px text}}';
- const rendered = Markdown.render(source);
+ const rendered = hbfm.render(source);
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('text');
});
it('Renders a mustache span with text and css property which contains quotes', function() {
const source = '{{font-family:"trebuchet ms" text}}';
- const rendered = Markdown.render(source);
+ const rendered = hbfm.render(source);
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('text');
});
it('Renders a mustache span with text and two css properties which contains quotes', function() {
const source = '{{font-family:"trebuchet ms",padding:"5px 10px" text}}';
- const rendered = Markdown.render(source);
+ const rendered = hbfm.render(source);
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('text');
});
it('Renders a mustache span with text with quotes and css property which contains double quotes', function() {
const source = '{{font-family:"trebuchet ms" text "with quotes"}}';
- const rendered = Markdown.render(source);
+ const rendered = hbfm.render(source);
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('text “with quotes”');
});
it('Renders a mustache span with text with quotes and css property which contains double and simple quotes', function() {
const source = `{{--stringVariable:"'string'" text "with quotes"}}`;
- const rendered = Markdown.render(source);
+ const rendered = hbfm.render(source);
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`text “with quotes”`);
});
it('Renders a mustache span with text, id, class and a couple of css properties', function() {
const source = '{{pen,#author,color:orange,font-family:"trebuchet ms" text}}';
- const rendered = Markdown.render(source);
+ const rendered = hbfm.render(source);
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('text');
});
it('Renders a span with added attributes', function() {
const source = 'Text and {{pen,#author,color:orange,font-family:"trebuchet ms",a="b and c",d=e, text}} and more text!';
- const rendered = Markdown.render(source);
+ const rendered = hbfm.render(source);
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('Text and text and more text!
\n'); }); }); @@ -132,7 +132,7 @@ describe(`Block: When using the Block syntax {{tags\\ntext\\n}}`, ()=>{ const source = dedent`{{ text }}`; - const rendered = Markdown.render(source).trimReturns(); + const rendered = hbfm.render(source).trimReturns(); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`text
{{}}
`); }); @@ -155,7 +155,7 @@ describe(`Block: When using the Block syntax {{tags\\ntext\\n}}`, ()=>{ const source = dedent`{{cat }}`; - const rendered = Markdown.render(source).trimReturns(); + const rendered = hbfm.render(source).trimReturns(); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(``); }); @@ -163,7 +163,7 @@ describe(`Block: When using the Block syntax {{tags\\ntext\\n}}`, ()=>{ const source = dedent`{{cat Sample text. }}`; - const rendered = Markdown.render(source).trimReturns(); + const rendered = hbfm.render(source).trimReturns(); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`Sample text.
Sample text.
Sample text.
Sample text.
Sample text.
Sample text.
Sample text.
Sample text.
Sample text.
Text and text and more text!
\nemphasis
'); }); it('Renders a code element with injected style', function() { const source = '`code`{background:gray}'; - const rendered = Markdown.render(source).trimReturns(); + const rendered = hbfm.render(source).trimReturns(); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('code


text{background:blue}
'); }); it('Renders an parent and child element, each modified by an injector', function() { const source = dedent`**bolded text**{color:red} {color:blue}`; - const rendered = Markdown.render(source).trimReturns(); + const rendered = hbfm.render(source).trimReturns(); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('bolded text
'); }); it('Renders an image with added attributes', function() { const source = ` {position:absolute,bottom:20px,left:130px,width:220px,a="b and c",d=e}`; - const rendered = Markdown.render(source).trimReturns(); - expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`





text
text
text
text
text
text
text
text
text
text
| Experience Points | Level |
|---|---|
| 0 | 1 |
| 300 | 2 |
{secondInjection}
'); }); @@ -487,7 +487,7 @@ describe('Injection: When an injection tag follows an element', ()=>{ {innerDiv} }} {outerDiv}`; - const rendered = Markdown.render(source).trimReturns(); + const rendered = hbfm.render(source).trimReturns(); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('outer text
inner text
Hello
`); }); test('Right Justify', function() { const source = '-: Hello'; - const rendered = Markdown.render(source); + const rendered = hbfm.render(source); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`Hello
`); }); test('Center Justify', function() { const source = ':-: Hello'; - const rendered = Markdown.render(source); + const rendered = hbfm.render(source); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`Hello
`); }); test('Ignored inside a code block', function() { const source = '```\n\n:- Hello\n\n```\n'; - const rendered = Markdown.render(source); + const rendered = hbfm.render(source); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`\n:- Hello\n\n`);
});
});
diff --git a/tests/markdown/variables.test.js b/tests/markdown/variables.test.js
index ea98fe56c..d9da1be07 100644
--- a/tests/markdown/variables.test.js
+++ b/tests/markdown/variables.test.js
@@ -1,7 +1,7 @@
/* eslint-disable max-lines */
-const dedent = require('dedent-tabs').default;
-import Markdown from 'naturalcrit/markdown.js';
+import dedent from 'dedent';
+import { hbfm } from 'hbmarkedwrapper';
// Marked.js adds line returns after closing tags on some default tokens.
// This removes those line returns for comparison sake.
@@ -9,10 +9,10 @@ String.prototype.trimReturns = function(){
return this.replace(/\r?\n|\r/g, '').trim();
};
-renderAllPages = function(pages){
+const renderAllPages = function(pages){
const outputs = [];
pages.forEach((page, index)=>{
- const output = Markdown.render(page, index);
+ const output = hbfm.render(page, index);
outputs.push(output);
});
@@ -29,7 +29,7 @@ describe('Block-level variables', ()=>{
$[var]
`;
- const rendered = Markdown.render(source).trimReturns();
+ const rendered = hbfm.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('string
'); }); @@ -40,7 +40,7 @@ describe('Block-level variables', ()=>{ lines $[var]`; - const rendered = Markdown.render(source).replace(/\s/g, ' ').trimReturns(); + const rendered = hbfm.render(source).replace(/\s/g, ' ').trimReturns(); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('string across multiple lines
'); }); @@ -54,7 +54,7 @@ describe('Block-level variables', ()=>{ | C | D | $[var]`; - const rendered = Markdown.render(source).trimReturns(); + const rendered = hbfm.render(source).trimReturns(); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent`| H1 | @@ -71,7 +71,7 @@ describe('Block-level variables', ()=>{ $[var] [var]: string`; - const rendered = Markdown.render(source).replace(/\s/g, ' ').trimReturns(); + const rendered = hbfm.render(source).replace(/\s/g, ' ').trimReturns(); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('
|---|
| title 1 | title 2 | title 3 | title 4 |
|---|---|---|---|
| foo | Ipsum | ) | ) |








Positive: +
Negative: -
'); }); it('Signed Test', function() { const source = `[a]: 13\n\n[b]: -11\n\nPositive: $[signed(a)]\n\nNegative: $[signed(b)]`; - const rendered = Markdown.render(source).trimReturns(); + const rendered = hbfm.render(source).trimReturns(); expect(rendered).toBe('Positive: +13
Negative: -11
'); }); it('Roman Numerals Test', function() { const source = `[a]: 18\n\nRoman Numeral: $[toRomans(a)]`; - const rendered = Markdown.render(source).trimReturns(); + const rendered = hbfm.render(source).trimReturns(); expect(rendered).toBe('Roman Numeral: XVIII
'); }); it('Roman Numerals Test - Uppercase', function() { const source = `[a]: 18\n\nRoman Numeral: $[toRomansUpper(a)]`; - const rendered = Markdown.render(source).trimReturns(); + const rendered = hbfm.render(source).trimReturns(); expect(rendered).toBe('Roman Numeral: XVIII
'); }); it('Roman Numerals Test - Lowercase', function() { const source = `[a]: 18\n\nRoman Numeral: $[toRomansLower(a)]`; - const rendered = Markdown.render(source).trimReturns(); + const rendered = hbfm.render(source).trimReturns(); expect(rendered).toBe('Roman Numeral: xviii
'); }); it('Number to Characters Test', function() { const source = `[a]: 18\n\n[b]: 39\n\nCharacters: $[toChar(a)] $[toChar(b)]`; - const rendered = Markdown.render(source).trimReturns(); + const rendered = hbfm.render(source).trimReturns(); expect(rendered).toBe('Characters: R AM
'); }); it('Number to Characters Test - Uppercase', function() { const source = `[a]: 18\n\n[b]: 39\n\nCharacters: $[toCharUpper(a)] $[toCharUpper(b)]`; - const rendered = Markdown.render(source).trimReturns(); + const rendered = hbfm.render(source).trimReturns(); expect(rendered).toBe('Characters: R AM
'); }); it('Number to Characters Test - Lowercase', function() { const source = `[a]: 18\n\n[b]: 39\n\nCharacters: $[toCharLower(a)] $[toCharLower(b)]`; - const rendered = Markdown.render(source).trimReturns(); + const rendered = hbfm.render(source).trimReturns(); expect(rendered).toBe('Characters: r am
'); }); it('Number to Words Test', function() { const source = `[a]: 80085\n\nWords: $[toWords(a)]`; - const rendered = Markdown.render(source).trimReturns(); + const rendered = hbfm.render(source).trimReturns(); expect(rendered).toBe('Words: eighty thousand and eighty-five
'); }); it('Number to Words Test - Uppercase', function() { const source = `[a]: 80085\n\nWords: $[toWordsUpper(a)]`; - const rendered = Markdown.render(source).trimReturns(); + const rendered = hbfm.render(source).trimReturns(); expect(rendered).toBe('Words: EIGHTY THOUSAND AND EIGHTY-FIVE
'); }); it('Number to Words Test - Lowercase', function() { const source = `[a]: 80085\n\nWords: $[toWordsLower(a)]`; - const rendered = Markdown.render(source).trimReturns(); + const rendered = hbfm.render(source).trimReturns(); expect(rendered).toBe('Words: eighty thousand and eighty-five
'); }); it('Number to Words Test - Capitalized', function() { const source = `[a]: 80085\n\nWords: $[toWordsCaps(a)]`; - const rendered = Markdown.render(source).trimReturns(); + const rendered = hbfm.render(source).trimReturns(); expect(rendered).toBe('Words: Eighty Thousand And Eighty-Five
'); }); }); \ No newline at end of file diff --git a/tests/routes/static-pages.test.js b/tests/routes/static-pages.test.js index ebfa48dd4..2741b8d70 100644 --- a/tests/routes/static-pages.test.js +++ b/tests/routes/static-pages.test.js @@ -1,27 +1,32 @@ import supertest from 'supertest'; -import HBApp from 'app.js'; +import createApp from '../../server/app.js'; -// Mimic https responses to avoid being redirected all the time -const app = supertest.agent(HBApp).set('X-Forwarded-Proto', 'https'); +let app; +let request; + +beforeAll(async ()=>{ + app = await createApp(); + request = supertest.agent(app).set('X-Forwarded-Proto', 'https'); +}); describe('Tests for static pages', ()=>{ - it('Home page works', ()=>{ - return app.get('/').expect(200); + it('Home page works', async ()=>{ + await request.get('/').expect(200); }); - it('Home page legacy works', ()=>{ - return app.get('/legacy').expect(200); + it('Home page legacy works', async ()=>{ + await request.get('/legacy').expect(200); }); - it('Changelog page works', ()=>{ - return app.get('/changelog').expect(200); + it('Changelog page works', async ()=>{ + await request.get('/changelog').expect(200); }); - it('FAQ page works', ()=>{ - return app.get('/faq').expect(200); + it('FAQ page works', async ()=>{ + await request.get('/faq').expect(200); }); - it('robots.txt works', ()=>{ - return app.get('/robots.txt').expect(200); + it('robots.txt works', async ()=>{ + await request.get('/robots.txt').expect(200); }); -}); +}); \ No newline at end of file diff --git a/themes/Legacy/5ePHB/snippets.js b/themes/Legacy/5ePHB/snippets.js index e9134853c..5bf74b4ca 100644 --- a/themes/Legacy/5ePHB/snippets.js +++ b/themes/Legacy/5ePHB/snippets.js @@ -1,14 +1,14 @@ /* eslint-disable max-lines */ -const MagicGen = require('./snippets/magic.gen.js'); -const ClassTableGen = require('./snippets/classtable.gen.js'); -const MonsterBlockGen = require('./snippets/monsterblock.gen.js'); -const ClassFeatureGen = require('./snippets/classfeature.gen.js'); -const CoverPageGen = require('./snippets/coverpage.gen.js'); -const TableOfContentsGen = require('./snippets/tableOfContents.gen.js'); -const dedent = require('dedent-tabs').default; +import MagicGen from './snippets/magic.gen.js'; +import ClassTableGen from './snippets/classtable.gen.js'; +import MonsterBlockGen from './snippets/monsterblock.gen.js'; +import ClassFeatureGen from './snippets/classfeature.gen.js'; +import CoverPageGen from './snippets/coverpage.gen.js'; +import TableOfContentsGen from './snippets/tableOfContents.gen.js'; +import dedent from 'dedent'; -module.exports = [ +export default [ { groupName : 'Text Editor', @@ -40,7 +40,7 @@ module.exports = [ icon : 'fas fa-image', gen : [ '
',
'Credit: Kyounghwan Kim'
].join('\n')
@@ -50,7 +50,7 @@ module.exports = [
icon : 'fas fa-tree',
gen : [
'
'
].join('\n')
},
diff --git a/themes/Legacy/5ePHB/snippets/classfeature.gen.js b/themes/Legacy/5ePHB/snippets/classfeature.gen.js
index e2407ff03..70085d647 100644
--- a/themes/Legacy/5ePHB/snippets/classfeature.gen.js
+++ b/themes/Legacy/5ePHB/snippets/classfeature.gen.js
@@ -1,6 +1,6 @@
-const _ = require('lodash');
+import _ from 'lodash';
-module.exports = function(classname){
+function classFeatureGen(classname) {
classname = _.sample(['archivist', 'fancyman', 'linguist', 'fletcher',
'notary', 'berserker-typist', 'fishmongerer', 'manicurist', 'haberdasher', 'concierge']);
@@ -49,4 +49,6 @@ module.exports = function(classname){
`- ${_.sample(['10 lint fluffs', '1 button', 'a cherished lost sock'])}`,
'\n\n\n'
].join('\n');
-};
+}
+
+export default classFeatureGen;
diff --git a/themes/Legacy/5ePHB/snippets/classtable.gen.js b/themes/Legacy/5ePHB/snippets/classtable.gen.js
index 867aa625e..3444de38f 100644
--- a/themes/Legacy/5ePHB/snippets/classtable.gen.js
+++ b/themes/Legacy/5ePHB/snippets/classtable.gen.js
@@ -1,4 +1,4 @@
-const _ = require('lodash');
+import _ from 'lodash';
const features = [
'Astrological Botany',
@@ -50,7 +50,7 @@ const getFeature = (level)=>{
return res.join(', ');
};
-module.exports = {
+export default {
full : function(){
const classname = _.sample(classnames);
diff --git a/themes/Legacy/5ePHB/snippets/coverpage.gen.js b/themes/Legacy/5ePHB/snippets/coverpage.gen.js
index 4b658e317..9c2de2943 100644
--- a/themes/Legacy/5ePHB/snippets/coverpage.gen.js
+++ b/themes/Legacy/5ePHB/snippets/coverpage.gen.js
@@ -1,4 +1,4 @@
-const _ = require('lodash');
+import _ from 'lodash';
const titles = [
'The Burning Gallows',
@@ -98,7 +98,7 @@ const subtitles = [
];
-module.exports = ()=>{
+function coverPageGen() {
return ``);
+ return null;
+ },
+};
+
+export const Inject = ({ tag, children, ...props })=>{
+ useEffect(()=>{
+ injectTag(tag, props, children);
+ }, []);
+ return null;
+};
+
+export const generate = ()=>Object.values(NamedTags).concat(UnnamedTags).join('\n');
+
+export const flush = ()=>{
+ NamedTags = {};
+ UnnamedTags = [];
+};
+
+export const Meta = HeadComponents.Meta;
+
+export default {
+ Inject,
+ ...HeadComponents,
+ generate,
+ flush,
+};