mirror of
https://github.com/naturalcrit/homebrewery.git
synced 2026-01-25 14:03:00 +00:00
Compare commits
112 Commits
v3.0.6
...
recolorCSS
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
415939b028 | ||
|
|
6a89f3f702 | ||
|
|
050bc472d0 | ||
|
|
48da1da5ee | ||
|
|
1b5f408bef | ||
|
|
b1869a33f9 | ||
|
|
bb07cdaa9f | ||
|
|
db0e4fcc0c | ||
|
|
ba8a2af87d | ||
|
|
85e7071d6c | ||
|
|
ece6df023a | ||
|
|
4c08f4a6e1 | ||
|
|
039e4dd4e5 | ||
|
|
041abf1220 | ||
|
|
56fc23f23a | ||
|
|
87c28c76f3 | ||
|
|
235f878dba | ||
|
|
b2ec0d4a0c | ||
|
|
02560d82ab | ||
|
|
22b80ffbb2 | ||
|
|
2db127d805 | ||
|
|
c2ca9f8f10 | ||
|
|
6fc176e616 | ||
|
|
4fd085b684 | ||
|
|
d2250cdabb | ||
|
|
179d5e6312 | ||
|
|
12d0f69e9c | ||
|
|
788ff65283 | ||
|
|
d7d93c8975 | ||
|
|
eb07fd7c38 | ||
|
|
c09b87482a | ||
|
|
19562a2445 | ||
|
|
543d65f43f | ||
|
|
fc1af353f3 | ||
|
|
9c57450330 | ||
|
|
0573084ffd | ||
|
|
6cb39709c4 | ||
|
|
4ea2fc34f0 | ||
|
|
a0e2bcb8e4 | ||
|
|
015644453b | ||
|
|
199c7d4e02 | ||
|
|
1d71e96421 | ||
|
|
8d0dbac882 | ||
|
|
6c1b4b1839 | ||
|
|
d4a4e7d139 | ||
|
|
0dfe18cd18 | ||
|
|
8895b44be9 | ||
|
|
279352377b | ||
|
|
51cf363c84 | ||
|
|
bdd554851d | ||
|
|
f611a36089 | ||
|
|
0861e1ed29 | ||
|
|
4070c53112 | ||
|
|
ac8ad98939 | ||
|
|
e315c29620 | ||
|
|
e84cd4fe8b | ||
|
|
2d85638d7d | ||
|
|
1daa700a1a | ||
|
|
c06176b3bf | ||
|
|
85f93c7861 | ||
|
|
e1457b5308 | ||
|
|
fc6fd00fe9 | ||
|
|
3ccc36f87a | ||
|
|
ceae540aa0 | ||
|
|
a5cab7005e | ||
|
|
e48e8cd05b | ||
|
|
e74800916e | ||
|
|
34f154d09d | ||
|
|
1bcdd6bc38 | ||
|
|
dd82ee68f0 | ||
|
|
564486f6d0 | ||
|
|
bf632a8584 | ||
|
|
506cf78dac | ||
|
|
0dbb5f18ba | ||
|
|
85e9c57ee2 | ||
|
|
257c266a2e | ||
|
|
28793e06fc | ||
|
|
ba74b5aa13 | ||
|
|
01bceca7df | ||
|
|
ccca313a15 | ||
|
|
c463eedc50 | ||
|
|
7f49d6f08b | ||
|
|
78d4487c58 | ||
|
|
8a3f52b704 | ||
|
|
6e04535eff | ||
|
|
5bb580147a | ||
|
|
1adaa9f5c4 | ||
|
|
7b19bbb1a7 | ||
|
|
e3d4165fa4 | ||
|
|
ec54434427 | ||
|
|
67bf69fc21 | ||
|
|
c60e287cbe | ||
|
|
de4b2861b6 | ||
|
|
eb4234d814 | ||
|
|
4da7b8bd17 | ||
|
|
c0f5f224bf | ||
|
|
9b89814056 | ||
|
|
12d0baf5d3 | ||
|
|
8a695c14d7 | ||
|
|
f253bdf954 | ||
|
|
38d8764f15 | ||
|
|
01f6d106a2 | ||
|
|
9119860012 | ||
|
|
eeaaa0e6c9 | ||
|
|
db5987a466 | ||
|
|
b5d5cb085b | ||
|
|
dcf17e3b72 | ||
|
|
5b746c0d9c | ||
|
|
41bc6ca444 | ||
|
|
aba2f58fc4 | ||
|
|
aa4de67e90 | ||
|
|
b1a9fbe3ca |
@@ -17,6 +17,7 @@ const PAGE_HEIGHT = 1056;
|
||||
const PPR_THRESHOLD = 50;
|
||||
|
||||
const BrewRenderer = createClass({
|
||||
displayName : 'BrewRenderer',
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
text : '',
|
||||
|
||||
@@ -5,6 +5,7 @@ const _ = require('lodash');
|
||||
const cx = require('classnames');
|
||||
|
||||
const ErrorBar = createClass({
|
||||
displayName : 'ErrorBar',
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
errors : []
|
||||
|
||||
@@ -7,6 +7,7 @@ const cx = require('classnames'); //Unused variable
|
||||
const DISMISS_KEY = 'dismiss_notification09-9-21';
|
||||
|
||||
const NotificationPopup = createClass({
|
||||
displayName : 'NotificationPopup',
|
||||
getInitialState : function() {
|
||||
return {
|
||||
notifications : {}
|
||||
|
||||
@@ -26,6 +26,7 @@ const splice = function(str, index, inject){
|
||||
|
||||
|
||||
const Editor = createClass({
|
||||
displayName : 'Editor',
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
brew : {
|
||||
@@ -208,6 +209,7 @@ const Editor = createClass({
|
||||
view={this.state.view}
|
||||
value={this.props.brew.style ?? DEFAULT_STYLE_TEXT}
|
||||
onChange={this.props.onStyleChange}
|
||||
enableFolding={false}
|
||||
rerenderParent={this.rerenderParent} />
|
||||
</>;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ const request = require('superagent');
|
||||
const SYSTEMS = ['5e', '4e', '3.5e', 'Pathfinder'];
|
||||
|
||||
const MetadataEditor = createClass({
|
||||
displayName : 'MetadataEditor',
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
metadata : {
|
||||
|
||||
@@ -14,6 +14,7 @@ const execute = function(val, brew){
|
||||
};
|
||||
|
||||
const Snippetbar = createClass({
|
||||
displayName : 'SnippetBar',
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
brew : {},
|
||||
@@ -103,6 +104,7 @@ module.exports = Snippetbar;
|
||||
|
||||
|
||||
const SnippetGroup = createClass({
|
||||
displayName : 'SnippetGroup',
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
brew : {},
|
||||
|
||||
@@ -13,6 +13,7 @@ const NewPage = require('./pages/newPage/newPage.jsx');
|
||||
const PrintPage = require('./pages/printPage/printPage.jsx');
|
||||
|
||||
const Homebrew = createClass({
|
||||
displayName : 'Homebrewery',
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
url : '',
|
||||
|
||||
@@ -3,7 +3,7 @@ const createClass = require('create-react-class');
|
||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||
|
||||
const Account = createClass({
|
||||
|
||||
displayName : 'AccountNavItem',
|
||||
getInitialState : function() {
|
||||
return {
|
||||
url : ''
|
||||
|
||||
@@ -7,6 +7,7 @@ const MAX_TITLE_LENGTH = 50;
|
||||
|
||||
|
||||
const EditTitle = createClass({
|
||||
displayName : 'EditTitleNavItem',
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
title : '',
|
||||
|
||||
@@ -6,6 +6,7 @@ const Nav = require('naturalcrit/nav/nav.jsx');
|
||||
const PatreonNavItem = require('./patreon.navitem.jsx');
|
||||
|
||||
const Navbar = createClass({
|
||||
displayName : 'Navbar',
|
||||
getInitialState : function() {
|
||||
return {
|
||||
//showNonChromeWarning : false,
|
||||
|
||||
@@ -10,7 +10,7 @@ const VIEW_KEY = 'homebrewery-recently-viewed';
|
||||
|
||||
|
||||
const RecentItems = createClass({
|
||||
|
||||
DisplayName : 'RecentItems',
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
storageKey : '',
|
||||
|
||||
@@ -8,6 +8,7 @@ const MAIN_URL = 'https://www.reddit.com/r/UnearthedArcana/submit?selftext=true'
|
||||
|
||||
|
||||
const RedditShare = createClass({
|
||||
displayName : 'RedditShareNavItem',
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
brew : {
|
||||
|
||||
@@ -27,6 +27,7 @@ const googleDriveInactive = require('../../googleDriveMono.png');
|
||||
const SAVE_TIMEOUT = 3000;
|
||||
|
||||
const EditPage = createClass({
|
||||
displayName : 'EditPage',
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
brew : {
|
||||
|
||||
@@ -21,6 +21,7 @@ const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
||||
|
||||
|
||||
const HomePage = createClass({
|
||||
displayName : 'HomePage',
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
brew : {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# The Homebrewery
|
||||
|
||||
Welcome traveler from an antique land. Please sit and tell us of what you have seen. The unheard of monsters, who slither and bite. Tell us of the wondrous items and and artifacts you have found, their mysteries yet to be unlocked. Of the vexing vocations and surprising skills you have seen.
|
||||
|
||||
### Homebrew D&D made easy
|
||||
@@ -57,17 +58,20 @@ The Homebrewery is licensed using the [MIT License](https://github.com/naturalcr
|
||||
If you wish to sell or in some way gain profit for what you make on this site, it's your responsibility to ensure you have the proper licenses/rights for any images or resources used.
|
||||
|
||||
### More Resources
|
||||
If you are looking for more 5e Homebrew resources check out [r/UnearthedArcana](https://www.reddit.com/r/UnearthedArcana/) and their list of useful resources [here](https://www.reddit.com/r/UnearthedArcana/wiki/resources).
|
||||
<a href='https://discord.gg/by3deKx' target='_blank'><img src='/assets/discordOfManyThings.svg' alt='Discord of Many Things Logo' title='Discord of Many Things Logo' style='width:50px; float: right; padding-left: 10px;'/></a>
|
||||
If you are looking for more 5e Homebrew resources check out [r/UnearthedArcana](https://www.reddit.com/r/UnearthedArcana/) and their list of useful resources [here](https://www.reddit.com/r/UnearthedArcana/wiki/resources). The Discord of Many Things is another great resource to connect with fellow homebrewers for help and feedback.
|
||||
|
||||
|
||||
|
||||
<img src='https://i.imgur.com/hMna6G0.png' style='position:absolute;bottom:50px;right:30px;width:280px' />
|
||||
<img src='https://i.imgur.com/hMna6G0.png' style='position:absolute;bottom:40px;right:30px;width:280px' />
|
||||
|
||||
<div class='pageNumber'>1</div>
|
||||
<div class='footnote'>PART 1 | FANCINESS</div>
|
||||
|
||||
|
||||
|
||||
<div style='position: absolute; top: 40px; right: 60px;'>
|
||||
<a href='https://discord.gg/by3deKx' target='_blank' title='Discord of Many Things'><img src='/assets/discord.png' height='36px'/></a><span style='position: absolute; left: -4px; top: 40px'>Discord</span>
|
||||
<a href='https://github.com/naturalcrit/homebrewery' target='_blank' title='github' style='color: black; padding-left: 10px;'><img src='/assets/github.png' height='36px'/></a><span style='position: absolute; top: 40px; left: 48px'>Github</span>
|
||||
<a href='https://patreon.com/NaturalCrit' target='_blank' title='patreon' style='color: black; padding-left: 10px;'><img src='/assets/patreon.png' height='36px'/></a><span style='position: absolute; top: 40px; right: 46px'>Patreon</span>
|
||||
<a href='https://www.reddit.com/r/homebrewery/' target='_blank' title='reddit' style='color: black; padding-left: 10px;'><img src='/assets/reddit.png' height='36px'/></a><span style='position: absolute; top: 40px; right: 0px;'>Reddit</span>
|
||||
</div>
|
||||
|
||||
\page
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
.page #example + table td {
|
||||
border:1px dashed #00000030;
|
||||
}
|
||||
|
||||
.page {
|
||||
padding-bottom : 1.1cm;
|
||||
}
|
||||
@@ -44,15 +43,22 @@ If you want to save ink or have a monochrome printer, add the **PRINT → {{fas,
|
||||
[naturalcrit](https://homebrew.naturalcrit.com)
|
||||
}}
|
||||
|
||||
{{position:absolute;top:40px;right:-580px
|
||||
<a href='https://discord.gg/by3deKx' target='_blank' title='discord' style='color: black;'><img src='/assets/discord.png' height='36px'/></a><span style='position: absolute; left: -6px; top: 40px'>Discord</span>
|
||||
<a href='https://github.com/naturalcrit/homebrewery' target='_blank' title='github' style='color: black; padding-left: 10px;'><img src='/assets/github.png' height='36px'/></a><span style='position: absolute; top: 40px; left: 47px'>Github</span>
|
||||
<a href='https://patreon.com/NaturalCrit' target='_blank' title='patreon' style='color: black; padding-left: 10px;'><img src='/assets/patreon.png' height='36px'/></a><span style='position: absolute; top: 40px; left: 93px'>Patreon</span>
|
||||
<a href='https://www.reddit.com/r/homebrewery/' target='_blank' title='reddit' style='color: black; padding-left: 10px;'><img src='/assets/reddit.png' height='36px'/></a><span style='position: absolute; top: 40px; left: 147px;'>Reddit</span>
|
||||
}}
|
||||
|
||||
{{pageNumber 1}}
|
||||
{{footnote PART 1 | FANCINESS}}
|
||||
|
||||
\column
|
||||
|
||||
## New in V3.0.0
|
||||
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. 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*).
|
||||
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. No raw HTML tags should be needed in a brew (*but can still be used if you insist*).
|
||||
|
||||
Much of the syntax and styling has changed in V3. Code in one version may be broken in the other, and updating an older brew to V3 will require more than just a copy and paste. *However*, 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 at any time.
|
||||
Much of the syntax and styling has changed in V3, so converting a Legacy brew to V3 (or vice-versa) will require tweaking your document. *However*, 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 at any time.
|
||||
|
||||
Scroll down to the next page for a brief summary of the changes and new features available in V3!
|
||||
|
||||
@@ -80,8 +86,8 @@ If you wish to sell or in some way gain profit for what's created on this site,
|
||||
If you'd like to credit me in your brew, I'd be flattered! Just reference that you made it with The Homebrewery.
|
||||
|
||||
### More Homebrew Resources
|
||||
Check out [r/UnearthedArcana](https://www.reddit.com/r/UnearthedArcana/) and their list of useful resources [here](https://www.reddit.com/r/UnearthedArcana/wiki/resources).
|
||||
|
||||
<a href='https://discord.gg/by3deKx' target='_blank'><img src='/assets/discordOfManyThings.svg' alt='Discord of Many Things Logo' title='Discord of Many Things Logo' style='width:50px; float: right; padding-left: 10px;'/></a>
|
||||
If you are looking for more 5e Homebrew resources check out [r/UnearthedArcana](https://www.reddit.com/r/UnearthedArcana/) and their list of useful resources [here](https://www.reddit.com/r/UnearthedArcana/wiki/resources). The <a href='https://discord.gg/by3deKx' target='_blank' title='Discord of Many Things'>Discord of Many Things</a> is another great resource to connect with fellow homebrewers for help and feedback.
|
||||
|
||||
\page
|
||||
|
||||
@@ -123,9 +129,7 @@ A blank line can be achieved with a run of one or more `:` alone on a line. More
|
||||
Much nicer than `<br><br><br><br><br>`
|
||||
|
||||
### Definition Lists
|
||||
V3 uses HTML *definition lists* to create "lists" with hanging indents.
|
||||
|
||||
**Senses** :: Here is some text that is long and overflows into a second line, creating a "hanging indent".
|
||||
**Example** :: V3 uses HTML *definition lists* to create "lists" with hanging indents.
|
||||
|
||||
### Column Breaks
|
||||
Column and page breaks with `\column` and `\page`.
|
||||
@@ -153,9 +157,9 @@ These can be combined to span a cell across both columns and rows. Cells must ha
|
||||
| 6A | 6B ^| 6C |
|
||||
|
||||
## Images
|
||||
Images must be hosted online somewhere, like [Imgur](https://www.imgur.com). You use the address to that image to reference it in your brew\*. Images can be included using Markdown-style images.
|
||||
Images must be hosted online somewhere, like [Imgur](https://www.imgur.com). You use the address to that image to reference it in your brew\*.
|
||||
|
||||
Using *Curly Injection* you can assign an id, classes, or specific inline CSS properties to the image.
|
||||
Using *Curly Injection* you can assign an id, classes, or inline CSS properties to the Markdown image syntax.
|
||||
|
||||
 {width:100px,border:"2px solid",border-radius:10px}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ const METAKEY = 'homebrewery-new-meta';
|
||||
|
||||
|
||||
const NewPage = createClass({
|
||||
displayName : 'NewPage',
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
brew : {
|
||||
|
||||
@@ -12,6 +12,7 @@ const STYLEKEY = 'homebrewery-new-style';
|
||||
const METAKEY = 'homebrewery-new-meta';
|
||||
|
||||
const PrintPage = createClass({
|
||||
displayName : 'PrintPage',
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
query : {},
|
||||
|
||||
@@ -14,6 +14,7 @@ const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
||||
|
||||
|
||||
const SharePage = createClass({
|
||||
displayName : 'SharePage',
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
brew : {
|
||||
|
||||
@@ -10,6 +10,7 @@ const googleDriveIcon = require('../../../googleDrive.png');
|
||||
const dedent = require('dedent-tabs').default;
|
||||
|
||||
const BrewItem = createClass({
|
||||
displayName : 'BrewItem',
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
brew : {
|
||||
|
||||
@@ -24,6 +24,7 @@ const ReportIssue = require('../../navbar/issue.navitem.jsx');
|
||||
|
||||
|
||||
const UserPage = createClass({
|
||||
displayName : 'UserPage',
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
username : '',
|
||||
|
||||
@@ -10,7 +10,7 @@ These instructions assume that you are installing to a completely new, fresh Fre
|
||||
|
||||
2. Install wget (`pkg install -y wget`). On a fresh jail, you will be prompted to press 'Y' to set up `pkg`.
|
||||
|
||||
3. Download the installation script (`wget --no-check-certificate https://raw.githubusercontent.com/naturalcrit/homebrewery/master/freebsd/install.sh`). The parameter `--no-check-certificate` is required as we haven't set up any trusted certificates/authorities yet.
|
||||
3. Download the installation script (`wget --no-check-certificate https://raw.githubusercontent.com/naturalcrit/homebrewery/master/install/freebsd/install.sh`). The parameter `--no-check-certificate` is required as we haven't set up any trusted certificates/authorities yet.
|
||||
|
||||
4. Make the downloaded file executable (`chmod +x install.sh`).
|
||||
|
||||
35
install/README.UBUNTU.md
Normal file
35
install/README.UBUNTU.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# Ubuntu Installation Instructions
|
||||
|
||||
## Before Installing
|
||||
|
||||
These instructions assume that you are installing to a completely new, fresh Ubuntu installation. As such, some steps will not be necessary if you are installing to an existing Ubuntu instance.
|
||||
|
||||
## Installation instructions
|
||||
|
||||
1. Install Ubuntu.
|
||||
|
||||
2. Install wget (`apt install -y wget`). This may already be installed, depending on your exact Ubuntu version.
|
||||
|
||||
3. Download the installation script (`wget https://raw.githubusercontent.com/naturalcrit/homebrewery/master/install/ubuntu/install.sh`).
|
||||
|
||||
4. Make the downloaded file executable (`chmod +x install.sh`).
|
||||
|
||||
5. Run the script (`sudo ./install.sh`). This will automatically download all of the required packages, install both them and HomeBrewery, configure the system and finally start HomeBrewery.
|
||||
|
||||
**NOTE:** At this time, the script **ONLY** installs HomeBrewery. It does **NOT** install the NaturalCrit login system, as that is currently a completely separate project.
|
||||
|
||||
---
|
||||
|
||||
### Testing
|
||||
|
||||
These installation instructions have been tested on the following Ubuntu releases:
|
||||
|
||||
- *ubuntu-20.04.3-desktop-amd64*
|
||||
|
||||
## Final Notes
|
||||
|
||||
While this installation process works successfully at the time of writing (December 19, 2021), it relies on all of the Node.JS packages used in the HomeBrewery project retaining their cross-platform capabilities to continue to function. This is one of the inherent advantages of Node.JS, but it is by no means guaranteed and as such, functionality or even installation may fail without warning at some point in the future.
|
||||
|
||||
Regards,
|
||||
G
|
||||
December 19, 2021
|
||||
13
install/ubuntu/etc/systemd/system/homebrewery.service
Normal file
13
install/ubuntu/etc/systemd/system/homebrewery.service
Normal file
@@ -0,0 +1,13 @@
|
||||
[Unit]
|
||||
Description=Homebrewery Web Server
|
||||
|
||||
[Service]
|
||||
User=root
|
||||
After=mongodb
|
||||
Environment=NODE_ENV=local
|
||||
WorkingDirectory=/usr/local/homebrewery
|
||||
ExecStart=node server.js
|
||||
Restart=always
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
34
install/ubuntu/install.sh
Normal file
34
install/ubuntu/install.sh
Normal file
@@ -0,0 +1,34 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Install CURL and add required NodeJS source to package repo
|
||||
echo ::Install CURL
|
||||
apt install -y curl
|
||||
echo ::Add NodeJS source to package repo
|
||||
curl -fsSL https://deb.nodesource.com/setup_16.x | sudo -E bash -
|
||||
|
||||
# Install required packages
|
||||
echo ::Install Homebrewery requirements
|
||||
apt satisfy -y git nodejs npm mongodb
|
||||
|
||||
# Clone Homebrewery repo
|
||||
echo ::Get Homebrewery files
|
||||
cd /usr/local/
|
||||
git clone https://github.com/naturalcrit/homebrewery.git
|
||||
|
||||
# Install Homebrewery
|
||||
echo ::Install Homebrewery
|
||||
cd homebrewery
|
||||
npm install
|
||||
npm audit fix
|
||||
npm run postinstall
|
||||
|
||||
# Create Homebrewery service
|
||||
echo ::Create Homebrewery service
|
||||
ln -s /usr/local/homebrewery/install/ubuntu/etc/systemd/system/homebrewery.service /etc/systemd/system/homebrewery.service
|
||||
systemctl daemon-reload
|
||||
echo ::Set Homebrewery to start automatically
|
||||
systemctl enable homebrewery
|
||||
|
||||
# Start Homebrewery
|
||||
echo ::Start Homebrewery
|
||||
systemctl start homebrewery
|
||||
9386
package-lock.json
generated
9386
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
43
package.json
43
package.json
@@ -18,8 +18,8 @@
|
||||
"lint:dry": "eslint **/*.{js,jsx}",
|
||||
"circleci": "npm test && eslint **/*.{js,jsx} --max-warnings=0",
|
||||
"verify": "npm run lint && npm test",
|
||||
"test": "pico-check",
|
||||
"test:dev": "pico-check -v -w",
|
||||
"test": "jest",
|
||||
"test:dev": "jest --verbose --watch",
|
||||
"phb": "node scripts/phb.js",
|
||||
"prod": "set NODE_ENV=production && npm run build",
|
||||
"postinstall": "npm run buildall",
|
||||
@@ -30,27 +30,34 @@
|
||||
"eslintIgnore": [
|
||||
"build/*"
|
||||
],
|
||||
"pico-check": {
|
||||
"require": "./tests/test.init.js"
|
||||
"jest": {
|
||||
"modulePaths": [
|
||||
"mode_modules",
|
||||
"shared",
|
||||
"server"
|
||||
]
|
||||
},
|
||||
"babel": {
|
||||
"presets": [
|
||||
"@babel/preset-env",
|
||||
"@babel/preset-react"
|
||||
],
|
||||
"plugins": [
|
||||
"@babel/plugin-transform-runtime"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.16.5",
|
||||
"@babel/plugin-transform-runtime": "^7.16.5",
|
||||
"@babel/preset-env": "^7.16.5",
|
||||
"@babel/preset-react": "^7.16.5",
|
||||
"@babel/core": "^7.16.12",
|
||||
"@babel/plugin-transform-runtime": "^7.16.10",
|
||||
"@babel/preset-env": "^7.16.11",
|
||||
"@babel/preset-react": "^7.16.7",
|
||||
"body-parser": "^1.19.1",
|
||||
"classnames": "^2.3.1",
|
||||
"codemirror": "^5.64.0",
|
||||
"codemirror": "^5.65.1",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"create-react-class": "^15.7.0",
|
||||
"dedent-tabs": "^0.10.1",
|
||||
"express": "^4.17.1",
|
||||
"express": "^4.17.2",
|
||||
"express-async-handler": "^1.2.0",
|
||||
"express-static-gzip": "2.1.1",
|
||||
"fs-extra": "10.0.0",
|
||||
@@ -59,15 +66,14 @@
|
||||
"jwt-simple": "^0.5.6",
|
||||
"less": "^3.13.1",
|
||||
"lodash": "^4.17.21",
|
||||
"marked": "4.0.7",
|
||||
"marked": "4.0.12",
|
||||
"marked-extended-tables": "^1.0.3",
|
||||
"markedLegacy": "npm:marked@^0.3.19",
|
||||
"moment": "^2.29.1",
|
||||
"mongoose": "^6.1.2",
|
||||
"nanoid": "3.1.30",
|
||||
"mongoose": "^6.1.8",
|
||||
"nanoid": "3.2.0",
|
||||
"nconf": "^0.11.3",
|
||||
"prop-types": "15.7.2",
|
||||
"query-string": "7.0.1",
|
||||
"query-string": "7.1.0",
|
||||
"react": "^16.14.0",
|
||||
"react-dom": "^16.14.0",
|
||||
"react-frame-component": "4.1.3",
|
||||
@@ -77,8 +83,9 @@
|
||||
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^8.4.1",
|
||||
"eslint-plugin-react": "^7.27.1",
|
||||
"pico-check": "^2.2.0"
|
||||
"eslint": "^8.7.0",
|
||||
"eslint-plugin-react": "^7.28.0",
|
||||
"jest": "^27.4.5",
|
||||
"supertest": "^6.2.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ const build = async ({ bundle, render, ssr })=>{
|
||||
await fs.outputFile('./build/homebrew/bundle.js', bundle);
|
||||
await fs.outputFile('./build/homebrew/ssr.js', ssr);
|
||||
await fs.copy('./themes/fonts', './build/fonts');
|
||||
await fs.copy('./themes/assets', './build/assets');
|
||||
let src = './themes/5ePhbLegacy.style.less';
|
||||
//Parse brew theme files
|
||||
less.render(fs.readFileSync(src).toString(), {
|
||||
@@ -73,6 +74,6 @@ pack('./client/homebrew/homebrew.jsx', {
|
||||
if(isDev){
|
||||
livereload('./build');
|
||||
watchFile('./server.js', {
|
||||
watch : ['./client'] // Watch additional folders if you want
|
||||
watch : ['./client', './server'] // Watch additional folders if you want
|
||||
});
|
||||
}
|
||||
|
||||
306
server.js
306
server.js
@@ -1,309 +1,19 @@
|
||||
/*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/
|
||||
const _ = require('lodash');
|
||||
const jwt = require('jwt-simple');
|
||||
const express = require('express');
|
||||
const yaml = require('js-yaml');
|
||||
const app = express();
|
||||
|
||||
const homebrewApi = require('./server/homebrew.api.js');
|
||||
const GoogleActions = require('./server/googleActions.js');
|
||||
const serveCompressedStaticAssets = require('./server/static-assets.mv.js');
|
||||
const sanitizeFilename = require('sanitize-filename');
|
||||
const asyncHandler = require('express-async-handler');
|
||||
|
||||
const brewAccessTypes = ['edit', 'share', 'raw'];
|
||||
|
||||
//Get the brew object from the HB database or Google Drive
|
||||
const getBrewFromId = asyncHandler(async (id, accessType)=>{
|
||||
if(!brewAccessTypes.includes(accessType))
|
||||
throw ('Invalid Access Type when getting brew');
|
||||
let brew;
|
||||
if(id.length > 12) {
|
||||
const googleId = id.slice(0, -12);
|
||||
id = id.slice(-12);
|
||||
brew = await GoogleActions.readFileMetadata(config.get('google_api_key'), googleId, id, accessType);
|
||||
} else {
|
||||
brew = await HomebrewModel.get(accessType == 'edit' ? { editId: id } : { shareId: id });
|
||||
brew = brew.toObject(); // Convert MongoDB object to standard Javascript Object
|
||||
}
|
||||
|
||||
brew = sanitizeBrew(brew, accessType === 'edit' ? false : true);
|
||||
//Split brew.text into text and style
|
||||
//unless the Access Type is RAW, in which case return immediately
|
||||
if(accessType == 'raw') {
|
||||
return brew;
|
||||
}
|
||||
splitTextStyleAndMetadata(brew);
|
||||
return brew;
|
||||
});
|
||||
|
||||
const sanitizeBrew = (brew, full=false)=>{
|
||||
delete brew._id;
|
||||
delete brew.__v;
|
||||
if(full){
|
||||
delete brew.editId;
|
||||
}
|
||||
return brew;
|
||||
};
|
||||
|
||||
const splitTextStyleAndMetadata = (brew)=>{
|
||||
brew.text = brew.text.replaceAll('\r\n', '\n');
|
||||
if(brew.text.startsWith('```metadata')) {
|
||||
const index = brew.text.indexOf('```\n\n');
|
||||
const metadataSection = brew.text.slice(12, index - 1);
|
||||
const metadata = yaml.load(metadataSection);
|
||||
Object.assign(brew, _.pick(metadata, ['title', 'description', 'tags', 'systems', 'renderer']));
|
||||
brew.text = brew.text.slice(index + 5);
|
||||
}
|
||||
if(brew.text.startsWith('```css')) {
|
||||
const index = brew.text.indexOf('```\n\n');
|
||||
brew.style = brew.text.slice(7, index - 1);
|
||||
brew.text = brew.text.slice(index + 5);
|
||||
}
|
||||
};
|
||||
|
||||
app.use('/', serveCompressedStaticAssets(`${__dirname}/build`));
|
||||
const DB = require('./server/db.js');
|
||||
const server = require('./server/app.js');
|
||||
|
||||
process.chdir(__dirname);
|
||||
|
||||
//app.use(express.static(`${__dirname}/build`));
|
||||
app.use(require('body-parser').json({ limit: '25mb' }));
|
||||
app.use(require('cookie-parser')());
|
||||
app.use(require('./server/forcessl.mw.js'));
|
||||
|
||||
const config = require('nconf')
|
||||
.argv()
|
||||
.env({ lowerCase: true })
|
||||
.file('environment', { file: `config/${process.env.NODE_ENV}.json` })
|
||||
.file('defaults', { file: 'config/default.json' });
|
||||
|
||||
//DB
|
||||
const mongoose = require('mongoose');
|
||||
mongoose.connect(config.get('mongodb_uri') || config.get('mongolab_uri') || 'mongodb://localhost/naturalcrit',
|
||||
{ retryWrites: false });
|
||||
mongoose.connection.on('error', (err)=>{
|
||||
console.log('Error : Could not connect to a Mongo Database.');
|
||||
console.log(' If you are running locally, make sure mongodb.exe is running.');
|
||||
console.log(err);
|
||||
throw 'Can not connect to Mongo';
|
||||
});
|
||||
|
||||
//Account Middleware
|
||||
app.use((req, res, next)=>{
|
||||
if(req.cookies && req.cookies.nc_session){
|
||||
try {
|
||||
req.account = jwt.decode(req.cookies.nc_session, config.get('secret'));
|
||||
//console.log("Just loaded up JWT from cookie:");
|
||||
//console.log(req.account);
|
||||
} catch (e){}
|
||||
}
|
||||
|
||||
req.config = {
|
||||
google_client_id : config.get('google_client_id'),
|
||||
google_client_secret : config.get('google_client_secret')
|
||||
};
|
||||
return next();
|
||||
});
|
||||
|
||||
app.use(homebrewApi);
|
||||
app.use(require('./server/admin.api.js'));
|
||||
|
||||
const HomebrewModel = require('./server/homebrew.model.js').model;
|
||||
const welcomeText = require('fs').readFileSync('./client/homebrew/pages/homePage/welcome_msg.md', 'utf8');
|
||||
const welcomeTextV3 = require('fs').readFileSync('./client/homebrew/pages/homePage/welcome_msg_v3.md', 'utf8');
|
||||
const changelogText = require('fs').readFileSync('./changelog.md', 'utf8');
|
||||
const faqText = require('fs').readFileSync('./faq.md', 'utf8');
|
||||
|
||||
String.prototype.replaceAll = function(s, r){return this.split(s).join(r);};
|
||||
|
||||
//Robots.txt
|
||||
app.get('/robots.txt', (req, res)=>{
|
||||
return res.sendFile(`${__dirname}/robots.txt`);
|
||||
});
|
||||
|
||||
//Home page
|
||||
app.get('/', async (req, res, next)=>{
|
||||
const brew = {
|
||||
text : welcomeText
|
||||
};
|
||||
req.brew = brew;
|
||||
return next();
|
||||
});
|
||||
|
||||
//Home page v3
|
||||
app.get('/v3_preview', async (req, res, next)=>{
|
||||
const brew = {
|
||||
text : welcomeTextV3,
|
||||
renderer : 'V3'
|
||||
};
|
||||
splitTextStyleAndMetadata(brew);
|
||||
req.brew = brew;
|
||||
return next();
|
||||
});
|
||||
|
||||
//Changelog page
|
||||
app.get('/changelog', async (req, res, next)=>{
|
||||
const brew = {
|
||||
title : 'Changelog',
|
||||
text : changelogText,
|
||||
renderer : 'V3'
|
||||
};
|
||||
splitTextStyleAndMetadata(brew);
|
||||
req.brew = brew;
|
||||
return next();
|
||||
});
|
||||
|
||||
//FAQ page
|
||||
app.get('/faq', async (req, res, next)=>{
|
||||
const brew = {
|
||||
title : 'FAQ',
|
||||
text : faqText,
|
||||
renderer : 'V3'
|
||||
};
|
||||
splitTextStyleAndMetadata(brew);
|
||||
req.brew = brew;
|
||||
return next();
|
||||
});
|
||||
|
||||
//Source page
|
||||
app.get('/source/:id', asyncHandler(async (req, res)=>{
|
||||
const brew = await getBrewFromId(req.params.id, 'raw');
|
||||
|
||||
const replaceStrings = { '&': '&', '<': '<', '>': '>' };
|
||||
let text = brew.text;
|
||||
for (const replaceStr in replaceStrings) {
|
||||
text = text.replaceAll(replaceStr, replaceStrings[replaceStr]);
|
||||
}
|
||||
text = `<code><pre style="white-space: pre-wrap;">${text}</pre></code>`;
|
||||
res.status(200).send(text);
|
||||
}));
|
||||
|
||||
//Download brew source page
|
||||
app.get('/download/:id', asyncHandler(async (req, res)=>{
|
||||
const brew = await getBrewFromId(req.params.id, 'raw');
|
||||
const prefix = 'HB - ';
|
||||
|
||||
let fileName = sanitizeFilename(`${prefix}${brew.title}`).replaceAll(' ', '');
|
||||
if(!fileName || !fileName.length) { fileName = `${prefix}-Untitled-Brew`; };
|
||||
res.set({
|
||||
'Cache-Control' : 'no-cache',
|
||||
'Content-Type' : 'text/plain',
|
||||
'Content-Disposition' : `attachment; filename="${fileName}.txt"`
|
||||
DB.connect(config).then(()=>{
|
||||
// Ensure that we have successfully connected to the database
|
||||
// before launching server
|
||||
const PORT = process.env.PORT || config.get('web_port') || 8000;
|
||||
server.app.listen(PORT, ()=>{
|
||||
console.log(`server on port: ${PORT}`);
|
||||
});
|
||||
res.status(200).send(brew.text);
|
||||
}));
|
||||
|
||||
//User Page
|
||||
app.get('/user/:username', async (req, res, next)=>{
|
||||
const ownAccount = req.account && (req.account.username == req.params.username);
|
||||
|
||||
let brews = await HomebrewModel.getByUser(req.params.username, ownAccount)
|
||||
.catch((err)=>{
|
||||
console.log(err);
|
||||
});
|
||||
|
||||
if(ownAccount && req?.account?.googleId){
|
||||
const googleBrews = await GoogleActions.listGoogleBrews(req, res)
|
||||
.catch((err)=>{
|
||||
console.error(err);
|
||||
});
|
||||
|
||||
if(googleBrews)
|
||||
brews = _.concat(brews, googleBrews);
|
||||
}
|
||||
|
||||
req.brews = _.map(brews, (brew)=>{
|
||||
return sanitizeBrew(brew, !ownAccount);
|
||||
});
|
||||
|
||||
return next();
|
||||
});
|
||||
|
||||
//Edit Page
|
||||
app.get('/edit/:id', asyncHandler(async (req, res, next)=>{
|
||||
res.header('Cache-Control', 'no-cache, no-store'); //reload the latest saved brew when pressing back button, not the cached version before save.
|
||||
const brew = await getBrewFromId(req.params.id, 'edit');
|
||||
req.brew = brew;
|
||||
return next();
|
||||
}));
|
||||
|
||||
//New Page
|
||||
app.get('/new/:id', asyncHandler(async (req, res, next)=>{
|
||||
const brew = await getBrewFromId(req.params.id, 'share');
|
||||
brew.title = `CLONE - ${brew.title}`;
|
||||
req.brew = brew;
|
||||
return next();
|
||||
}));
|
||||
|
||||
//Share Page
|
||||
app.get('/share/:id', asyncHandler(async (req, res, next)=>{
|
||||
const brew = await getBrewFromId(req.params.id, 'share');
|
||||
|
||||
if(req.params.id.length > 12) {
|
||||
const googleId = req.params.id.slice(0, -12);
|
||||
const shareId = req.params.id.slice(-12);
|
||||
await GoogleActions.increaseView(googleId, shareId, 'share', brew)
|
||||
.catch((err)=>{next(err);});
|
||||
} else {
|
||||
await HomebrewModel.increaseView({ shareId: brew.shareId });
|
||||
}
|
||||
|
||||
req.brew = brew;
|
||||
return next();
|
||||
}));
|
||||
|
||||
//Print Page
|
||||
app.get('/print/:id', asyncHandler(async (req, res, next)=>{
|
||||
const brew = await getBrewFromId(req.params.id, 'share');
|
||||
req.brew = brew;
|
||||
return next();
|
||||
}));
|
||||
|
||||
//Render the page
|
||||
const templateFn = require('./client/template.js');
|
||||
app.use((req, res)=>{
|
||||
const props = {
|
||||
version : require('./package.json').version,
|
||||
url : req.originalUrl,
|
||||
brew : req.brew,
|
||||
brews : req.brews,
|
||||
googleBrews : req.googleBrews,
|
||||
account : req.account,
|
||||
enable_v3 : config.get('enable_v3')
|
||||
};
|
||||
templateFn('homebrew', title = req.brew ? req.brew.title : '', props)
|
||||
.then((page)=>{ res.send(page); })
|
||||
.catch((err)=>{
|
||||
console.log(err);
|
||||
return res.sendStatus(500);
|
||||
});
|
||||
});
|
||||
|
||||
//v=====----- Error-Handling Middleware -----=====v//
|
||||
//Format Errors so all fields will be sent
|
||||
const replaceErrors = (key, value)=>{
|
||||
if(value instanceof Error) {
|
||||
const error = {};
|
||||
Object.getOwnPropertyNames(value).forEach(function (key) {
|
||||
error[key] = value[key];
|
||||
});
|
||||
return error;
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
const getPureError = (error)=>{
|
||||
return JSON.parse(JSON.stringify(error, replaceErrors));
|
||||
};
|
||||
|
||||
app.use((err, req, res, next)=>{
|
||||
const status = err.status || 500;
|
||||
console.error(err);
|
||||
res.status(status).send(getPureError(err));
|
||||
});
|
||||
//^=====--------------------------------------=====^//
|
||||
|
||||
const PORT = process.env.PORT || config.get('web_port') || 8000;
|
||||
app.listen(PORT, ()=>{
|
||||
console.log(`server on port:${PORT}`);
|
||||
});
|
||||
|
||||
299
server/app.js
Normal file
299
server/app.js
Normal file
@@ -0,0 +1,299 @@
|
||||
/*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/
|
||||
const _ = require('lodash');
|
||||
const jwt = require('jwt-simple');
|
||||
const express = require('express');
|
||||
const yaml = require('js-yaml');
|
||||
const app = express();
|
||||
|
||||
const homebrewApi = require('./homebrew.api.js');
|
||||
const GoogleActions = require('./googleActions.js');
|
||||
const serveCompressedStaticAssets = require('./static-assets.mv.js');
|
||||
const sanitizeFilename = require('sanitize-filename');
|
||||
const asyncHandler = require('express-async-handler');
|
||||
|
||||
const brewAccessTypes = ['edit', 'share', 'raw'];
|
||||
|
||||
//Get the brew object from the HB database or Google Drive
|
||||
const getBrewFromId = asyncHandler(async (id, accessType)=>{
|
||||
if(!brewAccessTypes.includes(accessType))
|
||||
throw ('Invalid Access Type when getting brew');
|
||||
let brew;
|
||||
if(id.length > 12) {
|
||||
const googleId = id.slice(0, -12);
|
||||
id = id.slice(-12);
|
||||
brew = await GoogleActions.readFileMetadata(config.get('google_api_key'), googleId, id, accessType);
|
||||
} else {
|
||||
brew = await HomebrewModel.get(accessType == 'edit' ? { editId: id } : { shareId: id });
|
||||
brew = brew.toObject(); // Convert MongoDB object to standard Javascript Object
|
||||
}
|
||||
|
||||
brew = sanitizeBrew(brew, accessType === 'edit' ? false : true);
|
||||
//Split brew.text into text and style
|
||||
//unless the Access Type is RAW, in which case return immediately
|
||||
if(accessType == 'raw') {
|
||||
return brew;
|
||||
}
|
||||
splitTextStyleAndMetadata(brew);
|
||||
return brew;
|
||||
});
|
||||
|
||||
const sanitizeBrew = (brew, full=false)=>{
|
||||
delete brew._id;
|
||||
delete brew.__v;
|
||||
if(full){
|
||||
delete brew.editId;
|
||||
}
|
||||
return brew;
|
||||
};
|
||||
|
||||
const splitTextStyleAndMetadata = (brew)=>{
|
||||
brew.text = brew.text.replaceAll('\r\n', '\n');
|
||||
if(brew.text.startsWith('```metadata')) {
|
||||
const index = brew.text.indexOf('```\n\n');
|
||||
const metadataSection = brew.text.slice(12, index - 1);
|
||||
const metadata = yaml.load(metadataSection);
|
||||
Object.assign(brew, _.pick(metadata, ['title', 'description', 'tags', 'systems', 'renderer']));
|
||||
brew.text = brew.text.slice(index + 5);
|
||||
}
|
||||
if(brew.text.startsWith('```css')) {
|
||||
const index = brew.text.indexOf('```\n\n');
|
||||
brew.style = brew.text.slice(7, index - 1);
|
||||
brew.text = brew.text.slice(index + 5);
|
||||
}
|
||||
};
|
||||
|
||||
app.use('/', serveCompressedStaticAssets(`${__dirname}/../build`));
|
||||
|
||||
process.chdir(__dirname);
|
||||
|
||||
//app.use(express.static(`${__dirname}/build`));
|
||||
app.use(require('body-parser').json({ limit: '25mb' }));
|
||||
app.use(require('cookie-parser')());
|
||||
app.use(require('./forcessl.mw.js'));
|
||||
|
||||
// FIXME: the config should be passed as an argument for the app
|
||||
const config = require('nconf')
|
||||
.argv()
|
||||
.env({ lowerCase: true })
|
||||
.file('environment', { file: `config/${process.env.NODE_ENV}.json` })
|
||||
.file('defaults', { file: 'config/default.json' });
|
||||
|
||||
//Account Middleware
|
||||
app.use((req, res, next)=>{
|
||||
if(req.cookies && req.cookies.nc_session){
|
||||
try {
|
||||
req.account = jwt.decode(req.cookies.nc_session, config.get('secret'));
|
||||
//console.log("Just loaded up JWT from cookie:");
|
||||
//console.log(req.account);
|
||||
} catch (e){}
|
||||
}
|
||||
|
||||
req.config = {
|
||||
google_client_id : config.get('google_client_id'),
|
||||
google_client_secret : config.get('google_client_secret')
|
||||
};
|
||||
return next();
|
||||
});
|
||||
|
||||
app.use(homebrewApi);
|
||||
app.use(require('./admin.api.js'));
|
||||
|
||||
const HomebrewModel = require('./homebrew.model.js').model;
|
||||
const welcomeText = require('fs').readFileSync('./../client/homebrew/pages/homePage/welcome_msg.md', 'utf8');
|
||||
const welcomeTextV3 = require('fs').readFileSync('./../client/homebrew/pages/homePage/welcome_msg_v3.md', 'utf8');
|
||||
const changelogText = require('fs').readFileSync('./../changelog.md', 'utf8');
|
||||
const faqText = require('fs').readFileSync('./../faq.md', 'utf8');
|
||||
|
||||
String.prototype.replaceAll = function(s, r){return this.split(s).join(r);};
|
||||
|
||||
//Robots.txt
|
||||
app.get('/robots.txt', (req, res)=>{
|
||||
return res.sendFile(`${__dirname}/robots.txt`);
|
||||
});
|
||||
|
||||
//Home page
|
||||
app.get('/', async (req, res, next)=>{
|
||||
const brew = {
|
||||
text : welcomeText
|
||||
};
|
||||
req.brew = brew;
|
||||
return next();
|
||||
});
|
||||
|
||||
//Home page v3
|
||||
app.get('/v3_preview', async (req, res, next)=>{
|
||||
const brew = {
|
||||
text : welcomeTextV3,
|
||||
renderer : 'V3'
|
||||
};
|
||||
splitTextStyleAndMetadata(brew);
|
||||
req.brew = brew;
|
||||
return next();
|
||||
});
|
||||
|
||||
//Changelog page
|
||||
app.get('/changelog', async (req, res, next)=>{
|
||||
const brew = {
|
||||
title : 'Changelog',
|
||||
text : changelogText,
|
||||
renderer : 'V3'
|
||||
};
|
||||
splitTextStyleAndMetadata(brew);
|
||||
req.brew = brew;
|
||||
return next();
|
||||
});
|
||||
|
||||
//FAQ page
|
||||
app.get('/faq', async (req, res, next)=>{
|
||||
const brew = {
|
||||
title : 'FAQ',
|
||||
text : faqText,
|
||||
renderer : 'V3'
|
||||
};
|
||||
splitTextStyleAndMetadata(brew);
|
||||
req.brew = brew;
|
||||
return next();
|
||||
});
|
||||
|
||||
//Source page
|
||||
app.get('/source/:id', asyncHandler(async (req, res)=>{
|
||||
const brew = await getBrewFromId(req.params.id, 'raw');
|
||||
|
||||
const replaceStrings = { '&': '&', '<': '<', '>': '>' };
|
||||
let text = brew.text;
|
||||
for (const replaceStr in replaceStrings) {
|
||||
text = text.replaceAll(replaceStr, replaceStrings[replaceStr]);
|
||||
}
|
||||
text = `<code><pre style="white-space: pre-wrap;">${text}</pre></code>`;
|
||||
res.status(200).send(text);
|
||||
}));
|
||||
|
||||
//Download brew source page
|
||||
app.get('/download/:id', asyncHandler(async (req, res)=>{
|
||||
const brew = await getBrewFromId(req.params.id, 'raw');
|
||||
const prefix = 'HB - ';
|
||||
|
||||
let fileName = sanitizeFilename(`${prefix}${brew.title}`).replaceAll(' ', '');
|
||||
if(!fileName || !fileName.length) { fileName = `${prefix}-Untitled-Brew`; };
|
||||
res.set({
|
||||
'Cache-Control' : 'no-cache',
|
||||
'Content-Type' : 'text/plain',
|
||||
'Content-Disposition' : `attachment; filename="${fileName}.txt"`
|
||||
});
|
||||
res.status(200).send(brew.text);
|
||||
}));
|
||||
|
||||
//User Page
|
||||
app.get('/user/:username', async (req, res, next)=>{
|
||||
const ownAccount = req.account && (req.account.username == req.params.username);
|
||||
|
||||
let brews = await HomebrewModel.getByUser(req.params.username, ownAccount)
|
||||
.catch((err)=>{
|
||||
console.log(err);
|
||||
});
|
||||
|
||||
if(ownAccount && req?.account?.googleId){
|
||||
const googleBrews = await GoogleActions.listGoogleBrews(req, res)
|
||||
.catch((err)=>{
|
||||
console.error(err);
|
||||
});
|
||||
|
||||
if(googleBrews)
|
||||
brews = _.concat(brews, googleBrews);
|
||||
}
|
||||
|
||||
req.brews = _.map(brews, (brew)=>{
|
||||
return sanitizeBrew(brew, !ownAccount);
|
||||
});
|
||||
|
||||
return next();
|
||||
});
|
||||
|
||||
//Edit Page
|
||||
app.get('/edit/:id', asyncHandler(async (req, res, next)=>{
|
||||
res.header('Cache-Control', 'no-cache, no-store'); //reload the latest saved brew when pressing back button, not the cached version before save.
|
||||
const brew = await getBrewFromId(req.params.id, 'edit');
|
||||
req.brew = brew;
|
||||
return next();
|
||||
}));
|
||||
|
||||
//New Page
|
||||
app.get('/new/:id', asyncHandler(async (req, res, next)=>{
|
||||
const brew = await getBrewFromId(req.params.id, 'share');
|
||||
brew.title = `CLONE - ${brew.title}`;
|
||||
req.brew = brew;
|
||||
return next();
|
||||
}));
|
||||
|
||||
//Share Page
|
||||
app.get('/share/:id', asyncHandler(async (req, res, next)=>{
|
||||
const brew = await getBrewFromId(req.params.id, 'share');
|
||||
|
||||
if(req.params.id.length > 12) {
|
||||
const googleId = req.params.id.slice(0, -12);
|
||||
const shareId = req.params.id.slice(-12);
|
||||
await GoogleActions.increaseView(googleId, shareId, 'share', brew)
|
||||
.catch((err)=>{next(err);});
|
||||
} else {
|
||||
await HomebrewModel.increaseView({ shareId: brew.shareId });
|
||||
}
|
||||
|
||||
req.brew = brew;
|
||||
return next();
|
||||
}));
|
||||
|
||||
//Print Page
|
||||
app.get('/print/:id', asyncHandler(async (req, res, next)=>{
|
||||
const brew = await getBrewFromId(req.params.id, 'share');
|
||||
req.brew = brew;
|
||||
return next();
|
||||
}));
|
||||
|
||||
//Render the page
|
||||
const templateFn = require('./../client/template.js');
|
||||
app.use((req, res)=>{
|
||||
const props = {
|
||||
version : require('./../package.json').version,
|
||||
url : req.originalUrl,
|
||||
brew : req.brew,
|
||||
brews : req.brews,
|
||||
googleBrews : req.googleBrews,
|
||||
account : req.account,
|
||||
enable_v3 : config.get('enable_v3')
|
||||
};
|
||||
const title = req.brew ? req.brew.title : '';
|
||||
templateFn('homebrew', title, props)
|
||||
.then((page)=>{ res.send(page); })
|
||||
.catch((err)=>{
|
||||
console.log(err);
|
||||
return res.sendStatus(500);
|
||||
});
|
||||
});
|
||||
|
||||
//v=====----- Error-Handling Middleware -----=====v//
|
||||
//Format Errors so all fields will be sent
|
||||
const replaceErrors = (key, value)=>{
|
||||
if(value instanceof Error) {
|
||||
const error = {};
|
||||
Object.getOwnPropertyNames(value).forEach(function (key) {
|
||||
error[key] = value[key];
|
||||
});
|
||||
return error;
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
const getPureError = (error)=>{
|
||||
return JSON.parse(JSON.stringify(error, replaceErrors));
|
||||
};
|
||||
|
||||
app.use((err, req, res, next)=>{
|
||||
const status = err.status || 500;
|
||||
console.error(err);
|
||||
res.status(status).send(getPureError(err));
|
||||
});
|
||||
//^=====--------------------------------------=====^//
|
||||
|
||||
module.exports = {
|
||||
app : app
|
||||
};
|
||||
37
server/db.js
Normal file
37
server/db.js
Normal file
@@ -0,0 +1,37 @@
|
||||
// The main purpose of this file is to provide an interface for database
|
||||
// connection. Even though the code is quite simple and basically a tiny
|
||||
// wrapper around mongoose package, it works as single point where
|
||||
// database setup/config is performed and the interface provided here can be
|
||||
// reused by both the main application and all tests which require database
|
||||
// connection.
|
||||
|
||||
const Mongoose = require('mongoose');
|
||||
|
||||
const getMongoDBURL = (config)=>{
|
||||
return config.get('mongodb_uri') ||
|
||||
config.get('mongolab_uri') ||
|
||||
'mongodb://localhost/homebrewery';
|
||||
};
|
||||
|
||||
const handleConnectionError = (error)=>{
|
||||
if(error) {
|
||||
console.error('Could not connect to a Mongo database: \n');
|
||||
console.error(error);
|
||||
console.error('\nIf you are running locally, make sure mongodb.exe is running and DB URL is configured properly');
|
||||
process.exit(1); // non-zero exit code to indicate an error
|
||||
}
|
||||
};
|
||||
|
||||
const disconnect = async ()=>{
|
||||
return await Mongoose.disconnect();
|
||||
};
|
||||
|
||||
const connect = async (config)=>{
|
||||
return await Mongoose.connect(getMongoDBURL(config),
|
||||
{ retryWrites: false }, handleConnectionError);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
connect : connect,
|
||||
disconnect : disconnect
|
||||
};
|
||||
@@ -11,7 +11,7 @@ const config = require('nconf')
|
||||
|
||||
//let oAuth2Client;
|
||||
|
||||
GoogleActions = {
|
||||
const GoogleActions = {
|
||||
|
||||
authCheck : (account, res)=>{
|
||||
if(!account || !account.googleId){ // If not signed into Google
|
||||
@@ -96,7 +96,7 @@ GoogleActions = {
|
||||
const drive = google.drive({ version: 'v3', auth: oAuth2Client });
|
||||
|
||||
const obj = await drive.files.list({
|
||||
pageSize : 100,
|
||||
pageSize : 1000,
|
||||
fields : 'nextPageToken, files(id, name, description, createdTime, modifiedTime, properties)',
|
||||
q : 'mimeType != \'application/vnd.google-apps.folder\' and trashed = false'
|
||||
})
|
||||
|
||||
@@ -7,6 +7,7 @@ const cx = require('classnames');
|
||||
const DISMISS_KEY = 'dismiss_render_warning';
|
||||
|
||||
const RenderWarnings = createClass({
|
||||
displayName : 'RenderWarnings',
|
||||
getInitialState : function() {
|
||||
return {
|
||||
warnings : {}
|
||||
|
||||
@@ -27,9 +27,9 @@ if(typeof navigator !== 'undefined'){
|
||||
require('codemirror/addon/search/matchesonscrollbar.js');
|
||||
require('codemirror/addon/dialog/dialog.js');
|
||||
//Trailing space highlighting
|
||||
require('codemirror/addon/edit/trailingspace.js');
|
||||
// require('codemirror/addon/edit/trailingspace.js');
|
||||
//Active line highlighting
|
||||
require('codemirror/addon/selection/active-line.js');
|
||||
// require('codemirror/addon/selection/active-line.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');
|
||||
@@ -40,12 +40,14 @@ if(typeof navigator !== 'undefined'){
|
||||
}
|
||||
|
||||
const CodeEditor = createClass({
|
||||
displayName : 'CodeEditor',
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
language : '',
|
||||
value : '',
|
||||
wrap : true,
|
||||
onChange : ()=>{}
|
||||
language : '',
|
||||
value : '',
|
||||
wrap : true,
|
||||
onChange : ()=>{},
|
||||
enableFolding : true
|
||||
};
|
||||
},
|
||||
|
||||
@@ -81,6 +83,12 @@ const CodeEditor = createClass({
|
||||
} 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);
|
||||
}
|
||||
},
|
||||
|
||||
buildEditor : function() {
|
||||
@@ -139,39 +147,19 @@ const CodeEditor = createClass({
|
||||
'Ctrl-]' : this.unfoldAllCode,
|
||||
'Cmd-]' : this.unfoldAllCode
|
||||
},
|
||||
foldGutter : true,
|
||||
foldOptions : {
|
||||
scanUp : true,
|
||||
rangeFinder : CodeMirror.fold.homebrewery,
|
||||
widget : (from, to)=>{
|
||||
let text = '';
|
||||
let currentLine = from.line;
|
||||
const maxLength = 50;
|
||||
while (currentLine <= to.line && text.length <= maxLength) {
|
||||
text += this.codeMirror.getLine(currentLine);
|
||||
if(currentLine < to.line)
|
||||
text += ' ';
|
||||
currentLine += 1;
|
||||
}
|
||||
|
||||
text = text.trim();
|
||||
if(text.length > maxLength)
|
||||
text = `${text.substr(0, maxLength)}...`;
|
||||
|
||||
return `\u21A4 ${text} \u21A6`;
|
||||
}
|
||||
},
|
||||
gutters : ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
|
||||
autoCloseTags : true,
|
||||
styleActiveLine : true,
|
||||
showTrailingSpace : true,
|
||||
specialChars : / /,
|
||||
specialCharPlaceholder : function(char) {
|
||||
const el = document.createElement('span');
|
||||
el.className = 'cm-space';
|
||||
el.innerHTML = ' ';
|
||||
return el;
|
||||
}
|
||||
foldGutter : true,
|
||||
foldOptions : this.foldOptions(this.codeMirror),
|
||||
gutters : ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
|
||||
autoCloseTags : true,
|
||||
styleActiveLine : true,
|
||||
showTrailingSpace : false,
|
||||
// specialChars : / /,
|
||||
// specialCharPlaceholder : function(char) {
|
||||
// const el = document.createElement('span');
|
||||
// el.className = 'cm-space';
|
||||
// el.innerHTML = ' ';
|
||||
// return el;
|
||||
// }
|
||||
});
|
||||
closeTag.autoCloseCurlyBraces(CodeMirror, this.codeMirror);
|
||||
|
||||
@@ -355,6 +343,30 @@ const CodeEditor = createClass({
|
||||
historySize : function(){
|
||||
return this.codeMirror.doc.historySize();
|
||||
},
|
||||
|
||||
foldOptions : function(cm){
|
||||
return {
|
||||
scanUp : true,
|
||||
rangeFinder : CodeMirror.fold.homebrewery,
|
||||
widget : (from, to)=>{
|
||||
let text = '';
|
||||
let currentLine = from.line;
|
||||
const maxLength = 50;
|
||||
while (currentLine <= to.line && text.length <= maxLength) {
|
||||
text += this.codeMirror.getLine(currentLine);
|
||||
if(currentLine < to.line)
|
||||
text += ' ';
|
||||
currentLine += 1;
|
||||
}
|
||||
|
||||
text = text.trim();
|
||||
if(text.length > maxLength)
|
||||
text = `${text.substr(0, maxLength)}...`;
|
||||
|
||||
return `\u21A4 ${text} \u21A6`;
|
||||
}
|
||||
};
|
||||
},
|
||||
//----------------------//
|
||||
|
||||
render : function(){
|
||||
|
||||
@@ -10,13 +10,13 @@
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.cm-tab {
|
||||
background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAMCAQAAACOs/baAAAARUlEQVR4nGJgIAG8JkXxUAcCtDWemcGR1lY4MvgzCEKY7jSBjgxBDAG09UEQzAe0AMwMHrSOAwEGRtpaMIwAAAAA//8DAG4ID9EKs6YqAAAAAElFTkSuQmCC) no-repeat right;
|
||||
}
|
||||
//.cm-tab {
|
||||
// background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAMCAQAAACOs/baAAAARUlEQVR4nGJgIAG8JkXxUAcCtDWemcGR1lY4MvgzCEKY7jSBjgxBDAG09UEQzAe0AMwMHrSOAwEGRtpaMIwAAAAA//8DAG4ID9EKs6YqAAAAAElFTkSuQmCC) no-repeat right;
|
||||
//}
|
||||
|
||||
.cm-trailingspace {
|
||||
.cm-space {
|
||||
background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAQAgMAAABW5NbuAAAACVBMVEVHcEwAAAAAAAAWawmTAAAAA3RSTlMAPBJ6PMxpAAAACXBIWXMAAC4jAAAuIwF4pT92AAAAFUlEQVQI12NgwACcCQysASAEZGAAACMuAX06aCQUAAAAAElFTkSuQmCC) no-repeat right;
|
||||
}
|
||||
}
|
||||
//.cm-trailingspace {
|
||||
// .cm-space {
|
||||
// background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAQAgMAAABW5NbuAAAACVBMVEVHcEwAAAAAAAAWawmTAAAAA3RSTlMAPBJ6PMxpAAAACXBIWXMAAC4jAAAuIwF4pT92AAAAFUlEQVQI12NgwACcCQysASAEZGAAACMuAX06aCQUAAAAAElFTkSuQmCC) no-repeat right;
|
||||
// }
|
||||
//}
|
||||
}
|
||||
@@ -329,6 +329,70 @@ const voidTags = new Set([
|
||||
'input', 'keygen', 'link', 'meta', 'param', 'source'
|
||||
]);
|
||||
|
||||
const recolor = (target)=>{
|
||||
//First, extract the RGB components of the hex color notation.
|
||||
|
||||
var r = parseInt(target.substr(1,2), 16); // Grab the hex representation of red (chars 1-2) and convert to decimal (base 10).
|
||||
var g = parseInt(target.substr(3,2), 16);
|
||||
var b = parseInt(target.substr(5,2), 16);
|
||||
|
||||
r /= 255, g /= 255, b /= 255;
|
||||
var max = Math.max(r, g, b), min = Math.min(r, g, b);
|
||||
var h, s, l = (max + min) / 2;
|
||||
|
||||
if(max == min){
|
||||
h = s = 0; // achromatic
|
||||
}else{
|
||||
var d = max - min;
|
||||
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
||||
switch(max){
|
||||
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
|
||||
case g: h = (b - r) / d + 2; break;
|
||||
case b: h = (r - g) / d + 4; break;
|
||||
}
|
||||
h /= 6;
|
||||
}
|
||||
|
||||
// fake hue-rotate to see how much to post-brighten and saturate
|
||||
let angle = h * 2 * Math.PI;
|
||||
console.log({angle: angle});
|
||||
let sin = Math.sin(angle);
|
||||
let cos = Math.cos(angle);
|
||||
|
||||
let matrix = [
|
||||
0.213 + cos * 0.787 - sin * 0.213, 0.715 - cos * 0.715 - sin * 0.715, 0.072 - cos * 0.072 + sin * 0.928,
|
||||
0.213 - cos * 0.213 + sin * 0.143, 0.715 + cos * 0.285 + sin * 0.140, 0.072 - cos * 0.072 - sin * 0.283,
|
||||
0.213 - cos * 0.213 - sin * 0.787, 0.715 - cos * 0.715 + sin * 0.715, 0.072 + cos * 0.928 + sin * 0.072
|
||||
];
|
||||
|
||||
r = Math.max(Math.min(1 * matrix[0] + 0 * matrix[1] + 0 * matrix[2], 255), 0);
|
||||
g = Math.max(Math.min(1 * matrix[3] + 0 * matrix[4] + 0 * matrix[5], 255), 0);
|
||||
b = Math.max(Math.min(1 * matrix[6] + 0 * matrix[7] + 0 * matrix[8], 255), 0);
|
||||
//=========--------------------------------============
|
||||
|
||||
max = Math.max(r, g, b), min = Math.min(r, g, b);
|
||||
let newL= (max + min) / 2;
|
||||
d = max - min;
|
||||
let newS = newL > 0.5 ? d / (2 - max - min) : d / (max + min);
|
||||
|
||||
console.log({newR : r, newG : g, newB: b});
|
||||
console.log({newL :newL});
|
||||
console.log({newS : newS});
|
||||
console.log({l : l})
|
||||
|
||||
l = l * 2; //Set lightness range from 0 to 200%, with 100% = no change (since we start with pure red which is .5 lightness)
|
||||
//Set saturation range from 0 to 100%, with 100% = no change (since pure red is 1 saturation)
|
||||
console.log({s:s});
|
||||
console.log({l:l});
|
||||
let contrast = 1;
|
||||
console.log({contrast: contrast});
|
||||
|
||||
if(l > 1)
|
||||
contrast = (2-l)/(l);
|
||||
|
||||
return `filter:hue-rotate(${h}turn) saturate(${1/newS}) contrast(${contrast}) brightness(${l}) saturate(${s});`; // Add at end
|
||||
}
|
||||
|
||||
const processStyleTags = (string)=>{
|
||||
//split tags up. quotes can only occur right after colons.
|
||||
//TODO: can we simplify to just split on commas?
|
||||
@@ -338,7 +402,17 @@ const processStyleTags = (string)=>{
|
||||
|
||||
const id = _.remove(tags, (tag)=>tag.startsWith('#')).map((tag)=>tag.slice(1))[0];
|
||||
const classes = _.remove(tags, (tag)=>!tag.includes(':'));
|
||||
const styles = tags.map((tag)=>tag.replace(/:"?([^"]*)"?/g, ':$1;'));
|
||||
const styles = tags.map((tag)=>{
|
||||
let newTag = tag.replace(/:"?([^"]*)"?/g, ':$1;');
|
||||
let match;
|
||||
if(match = newTag.match(/recolor:(#.{6})/)) {
|
||||
console.log(match[1]);
|
||||
newTag = recolor(match[1]);
|
||||
}
|
||||
return newTag;
|
||||
});
|
||||
|
||||
|
||||
return `${classes.join(' ')}" ${id ? `id="${id}"` : ''} ${styles.length ? `style="${styles.join(' ')}"` : ''}`;
|
||||
};
|
||||
|
||||
|
||||
@@ -8,7 +8,8 @@ const NaturalCritIcon = require('naturalcrit/svg/naturalcrit.svg.jsx');
|
||||
|
||||
const Nav = {
|
||||
base : createClass({
|
||||
render : function(){
|
||||
displayName : 'Nav.base',
|
||||
render : function(){
|
||||
return <nav>
|
||||
<div className='navContent'>
|
||||
{this.props.children}
|
||||
@@ -26,7 +27,8 @@ const Nav = {
|
||||
},
|
||||
|
||||
section : createClass({
|
||||
render : function(){
|
||||
displayName : 'Nav.section',
|
||||
render : function(){
|
||||
return <div className='navSection'>
|
||||
{this.props.children}
|
||||
</div>;
|
||||
@@ -34,6 +36,7 @@ const Nav = {
|
||||
}),
|
||||
|
||||
item : createClass({
|
||||
displayName : 'Nav.item',
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
icon : null,
|
||||
@@ -69,6 +72,7 @@ const Nav = {
|
||||
}),
|
||||
|
||||
dropdown : createClass({
|
||||
displayName : 'Nav.dropdown',
|
||||
getInitialState : function() {
|
||||
return {
|
||||
showDropdown : false
|
||||
|
||||
@@ -5,6 +5,7 @@ const _ = require('lodash');
|
||||
const cx = require('classnames');
|
||||
|
||||
const SplitPane = createClass({
|
||||
displayName : 'SplitPane',
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
storageKey : 'naturalcrit-pane-split',
|
||||
@@ -77,6 +78,7 @@ const SplitPane = createClass({
|
||||
});
|
||||
|
||||
const Pane = createClass({
|
||||
displayName : 'Pane',
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
width : null
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
const test = require('pico-check');
|
||||
|
||||
test('Just setting up a spot for future tests', (t)=>{
|
||||
t.pass();
|
||||
});
|
||||
|
||||
module.exports = test;
|
||||
15
tests/markdown/basic.test.js
Normal file
15
tests/markdown/basic.test.js
Normal file
@@ -0,0 +1,15 @@
|
||||
/* eslint-disable max-lines */
|
||||
|
||||
const Markdown = require('naturalcrit/markdown.js');
|
||||
|
||||
test('Escapes <script> tag', function() {
|
||||
const source = '<script></script>';
|
||||
const rendered = Markdown.render(source);
|
||||
expect(rendered).toMatch('<script></script>');
|
||||
});
|
||||
|
||||
test('Processes the markdown within an HTML block if its just a class wrapper', function() {
|
||||
const source = '<div>*Bold text*</div>';
|
||||
const rendered = Markdown.render(source);
|
||||
expect(rendered).toBe('<div> <p><em>Bold text</em></p>\n </div>');
|
||||
});
|
||||
128
tests/markdown/mustache-span.test.js
Normal file
128
tests/markdown/mustache-span.test.js
Normal file
@@ -0,0 +1,128 @@
|
||||
/* eslint-disable max-lines */
|
||||
|
||||
const Markdown = require('naturalcrit/markdown.js');
|
||||
|
||||
test('Renders a mustache span with text only', function() {
|
||||
const source = '{{ text}}';
|
||||
const rendered = Markdown.render(source);
|
||||
expect(rendered).toBe('<span class="inline-block ">text</span>');
|
||||
});
|
||||
|
||||
test('Renders a mustache span with text only, but with spaces', function() {
|
||||
const source = '{{ this is a text}}';
|
||||
const rendered = Markdown.render(source);
|
||||
expect(rendered).toBe('<span class="inline-block ">this is a text</span>');
|
||||
});
|
||||
|
||||
test('Renders an empty mustache span', function() {
|
||||
const source = '{{}}';
|
||||
const rendered = Markdown.render(source);
|
||||
expect(rendered).toBe('<span class="inline-block "></span>');
|
||||
});
|
||||
|
||||
test('Renders a mustache span with just a space', function() {
|
||||
const source = '{{ }}';
|
||||
const rendered = Markdown.render(source);
|
||||
expect(rendered).toBe('<span class="inline-block "></span>');
|
||||
});
|
||||
|
||||
test('Renders a mustache span with a few spaces only', function() {
|
||||
const source = '{{ }}';
|
||||
const rendered = Markdown.render(source);
|
||||
expect(rendered).toBe('<span class="inline-block "></span>');
|
||||
});
|
||||
|
||||
test('Renders a mustache span with text and class', function() {
|
||||
const source = '{{my-class text}}';
|
||||
const rendered = Markdown.render(source);
|
||||
// FIXME: why do we have those two extra spaces after closing "?
|
||||
expect(rendered).toBe('<span class="inline-block my-class" >text</span>');
|
||||
});
|
||||
|
||||
test('Renders a mustache span with text and two classes', function() {
|
||||
const source = '{{my-class,my-class2 text}}';
|
||||
const rendered = Markdown.render(source);
|
||||
// FIXME: why do we have those two extra spaces after closing "?
|
||||
expect(rendered).toBe('<span class="inline-block my-class my-class2" >text</span>');
|
||||
});
|
||||
|
||||
test('Renders a mustache span with text with spaces and class', function() {
|
||||
const source = '{{my-class this is a text}}';
|
||||
const rendered = Markdown.render(source);
|
||||
// FIXME: why do we have those two extra spaces after closing "?
|
||||
expect(rendered).toBe('<span class="inline-block my-class" >this is a text</span>');
|
||||
});
|
||||
|
||||
test('Renders a mustache span with text and id', function() {
|
||||
const source = '{{#my-span text}}';
|
||||
const rendered = Markdown.render(source);
|
||||
// FIXME: why do we have that one extra space after closing "?
|
||||
expect(rendered).toBe('<span class="inline-block " id="my-span" >text</span>');
|
||||
});
|
||||
|
||||
test('Renders a mustache span with text and two ids', function() {
|
||||
const source = '{{#my-span,#my-favorite-span text}}';
|
||||
const rendered = Markdown.render(source);
|
||||
// FIXME: do we need to report an error here somehow?
|
||||
expect(rendered).toBe('<span class="inline-block " id="my-span" >text</span>');
|
||||
});
|
||||
|
||||
test('Renders a mustache span with text and css property', function() {
|
||||
const source = '{{color:red text}}';
|
||||
const rendered = Markdown.render(source);
|
||||
expect(rendered).toBe('<span class="inline-block " style="color:red;">text</span>');
|
||||
});
|
||||
|
||||
test('Renders a mustache span with text and two css properties', function() {
|
||||
const source = '{{color:red,padding:5px text}}';
|
||||
const rendered = Markdown.render(source);
|
||||
expect(rendered).toBe('<span class="inline-block " style="color:red; padding:5px;">text</span>');
|
||||
});
|
||||
|
||||
test('Renders a mustache span with text and css property which contains quotes', function() {
|
||||
const source = '{{font:"trebuchet ms" text}}';
|
||||
const rendered = Markdown.render(source);
|
||||
// FIXME: is it correct to remove quotes surrounding css property value?
|
||||
expect(rendered).toBe('<span class="inline-block " style="font:trebuchet ms;">text</span>');
|
||||
});
|
||||
|
||||
test('Renders a mustache span with text and two css properties which contains quotes', function() {
|
||||
const source = '{{font:"trebuchet ms",padding:"5px 10px" text}}';
|
||||
const rendered = Markdown.render(source);
|
||||
expect(rendered).toBe('<span class="inline-block " style="font:trebuchet ms; padding:5px 10px;">text</span>');
|
||||
});
|
||||
|
||||
|
||||
test('Renders a mustache span with text with quotes and css property which contains quotes', function() {
|
||||
const source = '{{font:"trebuchet ms" text "with quotes"}}';
|
||||
const rendered = Markdown.render(source);
|
||||
expect(rendered).toBe('<span class="inline-block " style="font:trebuchet ms;">text “with quotes”</span>');
|
||||
});
|
||||
|
||||
test('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);
|
||||
expect(rendered).toBe('<span class="inline-block pen" id="author" style="color:orange; font-family:trebuchet ms;">text</span>');
|
||||
});
|
||||
|
||||
// TODO: add tests for ID with accordance to CSS spec:
|
||||
//
|
||||
// From https://drafts.csswg.org/selectors/#id-selectors:
|
||||
//
|
||||
// > An ID selector consists of a “number sign” (U+0023, #) immediately followed by the ID value, which must be a CSS identifier.
|
||||
//
|
||||
// From: https://www.w3.org/TR/CSS21/syndata.html#value-def-identifier:
|
||||
//
|
||||
// > In CSS, identifiers (including element names, classes, and IDs in selectors) can contain only the characters [a-zA-Z0-9]
|
||||
// > and ISO 10646 characters U+00A0 and higher, plus the hyphen (-) and the underscore (_);
|
||||
// > they cannot start with a digit, two hyphens, or a hyphen followed by a digit.
|
||||
// > Identifiers can also contain escaped characters and any ISO 10646 character as a numeric code (see next item).
|
||||
// > For instance, the identifier "B&W?" may be written as "B\&W\?" or "B\26 W\3F".
|
||||
// > Note that Unicode is code-by-code equivalent to ISO 10646 (see [UNICODE] and [ISO10646]).
|
||||
|
||||
// TODO: add tests for class with accordance to CSS spec:
|
||||
//
|
||||
// From: https://drafts.csswg.org/selectors/#class-html:
|
||||
//
|
||||
// > The class selector is given as a full stop (. U+002E) immediately followed by an identifier.
|
||||
|
||||
29
tests/routes/static-pages.test.js
Normal file
29
tests/routes/static-pages.test.js
Normal file
@@ -0,0 +1,29 @@
|
||||
const supertest = require('supertest');
|
||||
|
||||
// Mimic https responses to avoid being redirected all the time
|
||||
const app = supertest.agent(require('app.js').app)
|
||||
.set('X-Forwarded-Proto', 'https');
|
||||
|
||||
describe('Tests for static pages', ()=>{
|
||||
it('Home page works', ()=>{
|
||||
return app.get('/').expect(200);
|
||||
});
|
||||
|
||||
it('Home page v3 works', ()=>{
|
||||
return app.get('/v3_preview').expect(200);
|
||||
});
|
||||
|
||||
it('Changelog page works', ()=>{
|
||||
return app.get('/changelog').expect(200);
|
||||
});
|
||||
|
||||
it('FAQ page works', ()=>{
|
||||
return app.get('/faq').expect(200);
|
||||
});
|
||||
|
||||
// FIXME: robots.txt file can't be properly loaded under testing environment,
|
||||
// most likely due to __dirname being different from what is expected
|
||||
it.skip('robots.txt works', ()=>{
|
||||
return app.get('/robots.txt').expect(200);
|
||||
});
|
||||
});
|
||||
@@ -1 +0,0 @@
|
||||
//Set up configs and DB connectiosna nd what not in here
|
||||
BIN
themes/assets/discord.png
Normal file
BIN
themes/assets/discord.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.1 KiB |
1
themes/assets/discordOfManyThings.svg
Normal file
1
themes/assets/discordOfManyThings.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg id="Layer_3" data-name="Layer 3" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 350.028 390.552"><defs><style>.cls-1{opacity:0.9;}.cls-2{fill:#001013;}.cls-3{fill:#fff;}.cls-4{fill:#aa2a29;}</style></defs><title>Discord of Many Things blank</title><g class="cls-1"><path d="M100.991,82.743c-1.649,3.324-3.395,5.768-4.1,8.482-1.49,5.753-3.44,11.644-3.455,17.482q-.332,125.259-.141,250.519c.007,20.122,12.708,36.368,31.407,40.239a38.328,38.328,0,0,0,7.722.761q103.611,0,207.222-.088h6.314c-3.256-11.462-9.664-34.015-9.664-34.015l2.431,2.023s41.561,37.755,62.33,56.6c2.489,2.26,3.694,4.462,3.593,7.885-.241,8.186-.078,16.383-.078,25.492-6.333-5.527-11.928-10.35-17.457-15.247-9.586-8.493-19.2-16.957-28.644-25.6a10.032,10.032,0,0,0-7.39-2.774q-117.228-.1-234.457-.334c-20.632-.031-33.959-9.189-41.021-28.578-.852-2.339-.783-5.091-.784-7.653q-.057-116.5-.031-233.007V119.145C74.791,102.517,84.2,89.068,100.991,82.743Z" transform="translate(-74.79 -67.573)"/></g><path class="cls-2" d="M338.234,371.589s5.556,19.924,8.513,30.53h-5.075c-69.335,0-138.67-.1-208,.074-16.925.043-35.116-9.515-40.505-33.247a26.538,26.538,0,0,1-.379-5.827q-.027-128.172.025-256.344c.021-19.276,15.226-36.749,34.427-38.688a82.669,82.669,0,0,1,8.279-.447q123.288-.034,246.578-.067c13.265-.016,24.7,3.79,33.278,14.235a39.273,39.273,0,0,1,9.447,25.5q-.015,167.477-.012,334.955c0,1.245-.1,2.49-.2,4.645-4.075-3.565-7.6-6.552-11.018-9.655q-35.236-31.992-70.44-64.017c-1.2-1.091-3.713-3.147-3.713-3.147l-2-1.775Z" transform="translate(-74.79 -67.573)"/><path class="cls-3" d="M419.838,435.934c-7.848-6.892-15.026-12.977-21.961-19.329q-28.235-25.86-56.253-51.954c-2.314-2.157-4.59-3.247-7.356-1.784-3.036,1.607-2.77,4.676-2.022,7.359,2.286,8.2,4.817,16.338,7.24,24.5a17.108,17.108,0,0,1,.294,1.851c-1.486.084-2.872.233-4.258.233-67.363.012-134.726-.155-202.088.123-18.267.075-32.955-13.095-34.769-29.353a92.257,92.257,0,0,1-.886-10.108Q97.7,233.488,97.793,109.5c.013-10.518,3.19-20.063,11.156-27.471a31.92,31.92,0,0,1,20.439-8.917c1.948-.109,3.9-.181,5.852-.181q123.5-.014,247-.063c9.957-.012,18.894,2.145,26.329,9.1,7.956,7.441,11.177,16.934,11.214,27.457.142,40.84.057,81.681.057,122.522V435.934Z" transform="translate(-74.79 -67.573)"/><path class="cls-4" d="M311.029,170.975c-11.8,45.005-23.315,88.959-35.123,134.016l-97.168-98.883Z" transform="translate(-74.79 -67.573)"/><path class="cls-4" d="M380.8,265.712l-95.97,40.316c11.438-43.808,22.639-86.709,33.839-129.609l1.08-.272Z" transform="translate(-74.79 -67.573)"/><path class="cls-4" d="M165.3,322.955c2.91-36.463,5.761-72.179,8.709-109.116l94.711,96.472Z" transform="translate(-74.79 -67.573)"/><path class="cls-4" d="M223.458,107.926l83.127,55.063L178.71,196.919Z" transform="translate(-74.79 -67.573)"/><path class="cls-4" d="M366.285,281.366l-72.5,73.414c-3.524-13.182-6.849-25.617-10.306-38.549Z" transform="translate(-74.79 -67.573)"/><path class="cls-4" d="M274.874,318.478c3.445,12.8,6.765,25.143,10.376,38.559l-98.635-26.556.032-1.153Z" transform="translate(-74.79 -67.573)"/><path class="cls-4" d="M374.888,241.261l-51.71-75.57,26.8-21.529,25.831,96.619Z" transform="translate(-74.79 -67.573)"/><path class="cls-4" d="M165.492,207.523c-2.39,30.748-4.736,60.942-7.083,91.135l-1.063.159c-8.5-31.936-17-63.872-25.705-96.581Z" transform="translate(-74.79 -67.573)"/><path class="cls-4" d="M317.039,159.316l-68.251-45.24.307-.794,93.794,25.212Z" transform="translate(-74.79 -67.573)"/><path class="cls-4" d="M136.517,193.952,203.5,125.03l1.086.832L167.794,198.9Z" transform="translate(-74.79 -67.573)"/></svg>
|
||||
|
After Width: | Height: | Size: 3.5 KiB |
BIN
themes/assets/github.png
Normal file
BIN
themes/assets/github.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.9 KiB |
BIN
themes/assets/patreon.png
Normal file
BIN
themes/assets/patreon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.4 KiB |
BIN
themes/assets/reddit.png
Normal file
BIN
themes/assets/reddit.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.0 KiB |
Reference in New Issue
Block a user