mirror of
https://github.com/naturalcrit/homebrewery.git
synced 2025-12-24 14:12:40 +00:00
Merge branch 'master' into pr/2654
This commit is contained in:
@@ -63,7 +63,7 @@ jobs:
|
||||
command: npm run test:basic
|
||||
- run:
|
||||
name: Test - Mustache Spans
|
||||
command: npm run test:mustache-span
|
||||
command: npm run test:mustache-syntax
|
||||
- run:
|
||||
name: Test - Routes
|
||||
command: npm run test:route
|
||||
|
||||
@@ -11,7 +11,7 @@ module.exports = {
|
||||
browser : true,
|
||||
node : true
|
||||
},
|
||||
plugins : ['react'],
|
||||
plugins : ['react', 'jest'],
|
||||
rules : {
|
||||
/** Errors **/
|
||||
'camelcase' : ['error', { properties: 'never' }],
|
||||
@@ -24,6 +24,7 @@ module.exports = {
|
||||
'react/jsx-no-bind' : ['error', { allowArrowFunctions: true }],
|
||||
'react/jsx-uses-react' : 'error',
|
||||
'react/prefer-es6-class' : ['error', 'never'],
|
||||
'jest/valid-expect' : ['error', { maxArgs: 3 }],
|
||||
|
||||
/** Warnings **/
|
||||
'max-lines' : ['warn', {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:16.11-alpine
|
||||
FROM node:16.13-alpine
|
||||
RUN apk --no-cache add git
|
||||
|
||||
ENV NODE_ENV=docker
|
||||
@@ -10,11 +10,11 @@ WORKDIR /usr/src/app
|
||||
# This improves caching so we don't have to download the dependencies every time the code changes
|
||||
COPY package.json ./
|
||||
# --ignore-scripts tells yarn not to run postbuild. We run it explicitly later
|
||||
RUN yarn install --ignore-scripts
|
||||
RUN npm install --ignore-scripts
|
||||
|
||||
# Bundle app source and build application
|
||||
COPY . .
|
||||
RUN yarn build
|
||||
RUN npm run build
|
||||
|
||||
EXPOSE 8000
|
||||
CMD [ "yarn", "start" ]
|
||||
CMD [ "npm", "start" ]
|
||||
|
||||
63
changelog.md
63
changelog.md
@@ -80,19 +80,69 @@ pre {
|
||||
## changelog
|
||||
For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery).
|
||||
|
||||
### XXXXday DD/MM/2023 - v3.8.0
|
||||
### 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 automatically receiving the `target=_self` attribute
|
||||
* [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-Clerc
|
||||
|
||||
* [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)
|
||||
|
||||
}}
|
||||
|
||||
\page
|
||||
|
||||
### Monday 13/03/2023 - v3.7.2
|
||||
{{taskList
|
||||
|
||||
@@ -106,7 +156,7 @@ Fixes issues [#2680](https://github.com/naturalcrit/homebrewery/issues/2680)
|
||||
|
||||
##### Lucastucious (new contributor!)
|
||||
|
||||
* [x] Changed `filter: drop-shadow` to `box-shadow` on text boxes, making text selectable in PDFs again.
|
||||
* [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)
|
||||
|
||||
@@ -124,7 +174,6 @@ Fixes issues [#1569](https://github.com/naturalcrit/homebrewery/issues/1569)
|
||||
* [x] Fix PDF pixelation on CoverPage text outlines
|
||||
}}
|
||||
|
||||
|
||||
### Tuesday 28/02/2023 - v3.7.0
|
||||
{{taskList
|
||||
|
||||
@@ -154,7 +203,7 @@ Fixes issues [#2687](https://github.com/naturalcrit/homebrewery/issues/2687)
|
||||
{{taskList
|
||||
##### G-Ambatte
|
||||
|
||||
* [x] Fix users not being removed from Authors list correctly
|
||||
* [x] Fix users not being removed from Authors list
|
||||
|
||||
Fixes issues [#2674](https://github.com/naturalcrit/homebrewery/issues/2674)
|
||||
}}
|
||||
@@ -184,6 +233,8 @@ Fixes issues [#2583](https://github.com/naturalcrit/homebrewery/issues/2583)
|
||||
* [x] Fix cloned brews inheriting the parent view count
|
||||
}}
|
||||
|
||||
\page
|
||||
|
||||
### Friday 23/12/2022 - v3.5.0
|
||||
{{taskList
|
||||
|
||||
@@ -197,8 +248,6 @@ Fixes issues [#2583](https://github.com/naturalcrit/homebrewery/issues/2583)
|
||||
Fixes issues [#1987](https://github.com/naturalcrit/homebrewery/issues/1987)
|
||||
}}
|
||||
|
||||
\page
|
||||
|
||||
### Saturday 10/12/2022 - v3.4.2
|
||||
{{taskList
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
const cx = require('classnames'); //Unused variable
|
||||
|
||||
const DISMISS_KEY = 'dismiss_notification08-27-22';
|
||||
const DISMISS_KEY = 'dismiss_notification12-04-23';
|
||||
|
||||
const NotificationPopup = createClass({
|
||||
displayName : 'NotificationPopup',
|
||||
@@ -25,21 +25,14 @@ const NotificationPopup = createClass({
|
||||
return (
|
||||
<>
|
||||
<li key='psa'>
|
||||
<em>V3.2.0 Released!</em> <br />
|
||||
We are happy to announce that after nearly a year of use by our many users,
|
||||
we are making the V3 render mode the default setting for all new brews.
|
||||
This mode has become quite popular, and has proven to be stable and powerful.
|
||||
Of course, we will always keep the option to use the Legacy renderer for any
|
||||
brew, which can still be accessed from the Properties menu.
|
||||
</li>
|
||||
|
||||
<li key='stubs'>
|
||||
<em>Change to Google Drive Storage!</em> <br />
|
||||
We have made a change to the process of tranferring brews between Google
|
||||
Drive and the Homebrewery storage. Starting now, any time a brew is
|
||||
transferred, it will keep the same links instead of generating new ones!
|
||||
We hope this change will help reduce issues where people "lost" their work
|
||||
by trying to visit old links.
|
||||
<em>Broken <b>CoverPage</b> snippet</em> <br />
|
||||
Those of you who have been trying out our Cover Page snippet may
|
||||
notice that the cover page no longer displays correctly. Due to some
|
||||
small tweaks of this BETA feature, the CSS class has been renamed
|
||||
from "coverPage" to "frontCover". Simply change the text to "frontCover"
|
||||
and it should again function as before. Remember that any snippet
|
||||
marked "beta" may have a similar change in the future as we
|
||||
encounter any bugs or reworks.
|
||||
</li>
|
||||
|
||||
<li key='googleDriveFolder'>
|
||||
|
||||
@@ -82,4 +82,4 @@ const ErrorNavItem = createClass({
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = ErrorNavItem;
|
||||
module.exports = ErrorNavItem;
|
||||
|
||||
@@ -1,77 +1,75 @@
|
||||
.navItem {
|
||||
&.error {
|
||||
position : relative;
|
||||
background-color : @red;
|
||||
}
|
||||
.navItem.error {
|
||||
position : relative;
|
||||
background-color : @red;
|
||||
}
|
||||
|
||||
.errorContainer{
|
||||
animation-name: glideDown;
|
||||
animation-duration: 0.4s;
|
||||
position : absolute;
|
||||
top : 100%;
|
||||
left : 50%;
|
||||
z-index : 1000;
|
||||
width : 140px;
|
||||
padding : 3px;
|
||||
color : white;
|
||||
.errorContainer{
|
||||
animation-name: glideDown;
|
||||
animation-duration: 0.4s;
|
||||
position : absolute;
|
||||
top : 100%;
|
||||
left : 50%;
|
||||
z-index : 1000;
|
||||
width : 140px;
|
||||
padding : 3px;
|
||||
color : white;
|
||||
background-color : #333;
|
||||
border : 3px solid #444;
|
||||
border-radius : 5px;
|
||||
transform : translate(-50% + 3px, 10px);
|
||||
text-align : center;
|
||||
font-size : 10px;
|
||||
font-weight : 800;
|
||||
text-transform : uppercase;
|
||||
a{
|
||||
color : @teal;
|
||||
}
|
||||
&:before {
|
||||
content: "";
|
||||
width: 0px;
|
||||
height: 0px;
|
||||
position: absolute;
|
||||
border-left: 10px solid transparent;
|
||||
border-right: 10px solid transparent;
|
||||
border-top: 10px solid transparent;
|
||||
border-bottom: 10px solid #444;
|
||||
left: 53px;
|
||||
top: -23px;
|
||||
}
|
||||
&:after {
|
||||
content: "";
|
||||
width: 0px;
|
||||
height: 0px;
|
||||
position: absolute;
|
||||
border-left: 10px solid transparent;
|
||||
border-right: 10px solid transparent;
|
||||
border-top: 10px solid transparent;
|
||||
border-bottom: 10px solid #333;
|
||||
left: 53px;
|
||||
top: -19px;
|
||||
}
|
||||
.deny {
|
||||
width : 48%;
|
||||
margin : 1px;
|
||||
padding : 5px;
|
||||
background-color : #333;
|
||||
border : 3px solid #444;
|
||||
border-radius : 5px;
|
||||
transform : translate(-50% + 3px, 10px);
|
||||
text-align : center;
|
||||
font-size : 10px;
|
||||
font-weight : 800;
|
||||
text-transform : uppercase;
|
||||
a{
|
||||
color : @teal;
|
||||
}
|
||||
&:before {
|
||||
content: "";
|
||||
width: 0px;
|
||||
height: 0px;
|
||||
position: absolute;
|
||||
border-left: 10px solid transparent;
|
||||
border-right: 10px solid transparent;
|
||||
border-top: 10px solid transparent;
|
||||
border-bottom: 10px solid #444;
|
||||
left: 53px;
|
||||
top: -23px;
|
||||
}
|
||||
&:after {
|
||||
content: "";
|
||||
width: 0px;
|
||||
height: 0px;
|
||||
position: absolute;
|
||||
border-left: 10px solid transparent;
|
||||
border-right: 10px solid transparent;
|
||||
border-top: 10px solid transparent;
|
||||
border-bottom: 10px solid #333;
|
||||
left: 53px;
|
||||
top: -19px;
|
||||
}
|
||||
.deny {
|
||||
width : 48%;
|
||||
margin : 1px;
|
||||
padding : 5px;
|
||||
background-color : #333;
|
||||
display : inline-block;
|
||||
border-left : 1px solid #666;
|
||||
.animate(background-color);
|
||||
&:hover{
|
||||
background-color : red;
|
||||
}
|
||||
}
|
||||
.confirm {
|
||||
width : 48%;
|
||||
margin : 1px;
|
||||
padding : 5px;
|
||||
background-color : #333;
|
||||
display : inline-block;
|
||||
color : white;
|
||||
.animate(background-color);
|
||||
&:hover{
|
||||
background-color : teal;
|
||||
}
|
||||
display : inline-block;
|
||||
border-left : 1px solid #666;
|
||||
.animate(background-color);
|
||||
&:hover{
|
||||
background-color : red;
|
||||
}
|
||||
}
|
||||
}
|
||||
.confirm {
|
||||
width : 48%;
|
||||
margin : 1px;
|
||||
padding : 5px;
|
||||
background-color : #333;
|
||||
display : inline-block;
|
||||
color : white;
|
||||
.animate(background-color);
|
||||
&:hover{
|
||||
background-color : teal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
90
client/homebrew/navbar/metadata.navitem.jsx
Normal file
90
client/homebrew/navbar/metadata.navitem.jsx
Normal file
@@ -0,0 +1,90 @@
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
const Moment = require('moment');
|
||||
|
||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||
|
||||
|
||||
const MetadataNav = createClass({
|
||||
displayName : 'MetadataNav',
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState : function() {
|
||||
return {
|
||||
showMetaWindow : false
|
||||
};
|
||||
},
|
||||
|
||||
componentDidMount : function() {
|
||||
},
|
||||
|
||||
toggleMetaWindow : function(){
|
||||
this.setState((prevProps)=>({
|
||||
showMetaWindow : !prevProps.showMetaWindow
|
||||
}));
|
||||
},
|
||||
|
||||
getAuthors : function(){
|
||||
if(!this.props.brew.authors || this.props.brew.authors.length == 0) return 'No authors';
|
||||
return <>
|
||||
{this.props.brew.authors.map((author, idx, arr)=>{
|
||||
const spacer = arr.length - 1 == idx ? <></> : <span>, </span>;
|
||||
return <span key={idx}><a className='userPageLink' href={`/user/${author}`}>{author}</a>{spacer}</span>;
|
||||
})}
|
||||
</>;
|
||||
},
|
||||
|
||||
getTags : function(){
|
||||
if(!this.props.brew.tags || this.props.brew.tags.length == 0) return 'No tags';
|
||||
return <>
|
||||
{this.props.brew.tags.map((tag, idx)=>{
|
||||
return <span className='tag' key={idx}>{tag}</span>;
|
||||
})}
|
||||
</>;
|
||||
},
|
||||
|
||||
getSystems : function(){
|
||||
if(!this.props.brew.systems || this.props.brew.systems.length == 0) return 'No systems';
|
||||
return this.props.brew.systems.join(', ');
|
||||
},
|
||||
|
||||
renderMetaWindow : function(){
|
||||
return <div className={`window ${this.state.showMetaWindow ? 'active' : 'inactive'}`}>
|
||||
<div className='row'>
|
||||
<h4>Description</h4>
|
||||
<p>{this.props.brew.description || 'No description.'}</p>
|
||||
</div>
|
||||
<div className='row'>
|
||||
<h4>Authors</h4>
|
||||
<p>{this.getAuthors()}</p>
|
||||
</div>
|
||||
<div className='row'>
|
||||
<h4>Tags</h4>
|
||||
<p>{this.getTags()}</p>
|
||||
</div>
|
||||
<div className='row'>
|
||||
<h4>Systems</h4>
|
||||
<p>{this.getSystems()}</p>
|
||||
</div>
|
||||
<div className='row'>
|
||||
<h4>Updated</h4>
|
||||
<p>{Moment(this.props.brew.updatedAt).fromNow()}</p>
|
||||
</div>
|
||||
</div>;
|
||||
},
|
||||
|
||||
render : function(){
|
||||
return <Nav.item icon='fas fa-info-circle' color='grey' className='metadata'
|
||||
onClick={()=>this.toggleMetaWindow()}>
|
||||
{this.props.children}
|
||||
{this.renderMetaWindow()}
|
||||
</Nav.item>;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
module.exports = MetadataNav;
|
||||
@@ -1,193 +1,278 @@
|
||||
@import 'naturalcrit/styles/colors.less';
|
||||
@navbarHeight : 28px;
|
||||
@keyframes pinkColoring {
|
||||
//from {color: white;}
|
||||
//to {color: red;}
|
||||
0% {color: pink;}
|
||||
50% {color: pink;}
|
||||
75% {color: red;}
|
||||
100% {color: pink;}
|
||||
}
|
||||
.homebrew nav{
|
||||
.homebrewLogo{
|
||||
.animate(color);
|
||||
font-family : CodeBold;
|
||||
font-size : 12px;
|
||||
color : white;
|
||||
div{
|
||||
margin-top : 2px;
|
||||
margin-bottom : -2px;
|
||||
}
|
||||
&:hover{
|
||||
color : @blue;
|
||||
}
|
||||
}
|
||||
.editTitle.navItem{
|
||||
padding : 2px 12px;
|
||||
input{
|
||||
width : 250px;
|
||||
margin : 0;
|
||||
padding : 2px;
|
||||
background-color : #444;
|
||||
font-family : 'Open Sans', sans-serif;
|
||||
font-size : 12px;
|
||||
font-weight : 800;
|
||||
color : white;
|
||||
text-align : center;
|
||||
border : 1px solid @blue;
|
||||
outline : none;
|
||||
}
|
||||
.charCount{
|
||||
display : inline-block;
|
||||
vertical-align : bottom;
|
||||
margin-left : 8px;
|
||||
color : #666;
|
||||
text-align : right;
|
||||
&.max{
|
||||
color : @red;
|
||||
}
|
||||
}
|
||||
}
|
||||
.brewTitle.navItem{
|
||||
font-size : 12px;
|
||||
font-weight : 800;
|
||||
color : white;
|
||||
text-align : center;
|
||||
text-transform : initial;
|
||||
}
|
||||
.save-menu {
|
||||
.dropdown {
|
||||
z-index: 1000;
|
||||
}
|
||||
.navItem i.fa-power-off {
|
||||
color : red;
|
||||
&.active {
|
||||
color : rgb(0, 182, 52);
|
||||
filter : drop-shadow(0 0 2px rgba(0, 182, 52, 0.765))
|
||||
}
|
||||
}
|
||||
}
|
||||
.patreon.navItem{
|
||||
border-left : 1px solid #666;
|
||||
border-right : 1px solid #666;
|
||||
&:hover i {
|
||||
color: red;
|
||||
}
|
||||
i{
|
||||
.animate(color);
|
||||
animation-name: pinkColoring;
|
||||
animation-duration: 2s;
|
||||
color: pink;
|
||||
}
|
||||
}
|
||||
.recent.navItem {
|
||||
position : relative;
|
||||
.dropdown{
|
||||
position : absolute;
|
||||
top : 28px;
|
||||
left : 0px;
|
||||
z-index : 10000;
|
||||
width : 100%;
|
||||
overflow : hidden auto;
|
||||
max-height : ~"calc(100vh - 28px)";
|
||||
scrollbar-color : #666 #333;
|
||||
scrollbar-width : thin;
|
||||
h4{
|
||||
display : block;
|
||||
box-sizing : border-box;
|
||||
padding : 5px 0px;
|
||||
background-color : #333;
|
||||
font-size : 0.8em;
|
||||
color : #bbb;
|
||||
text-align : center;
|
||||
border-top : 1px solid #888;
|
||||
&:nth-of-type(1){ background-color: darken(@teal, 20%); }
|
||||
&:nth-of-type(2){ background-color: darken(@purple, 30%); }
|
||||
}
|
||||
.item{
|
||||
@import "naturalcrit/styles/colors.less";
|
||||
@navbarHeight : 28px;
|
||||
@keyframes pinkColoring {
|
||||
0% {color : pink;}
|
||||
50% {color : pink;}
|
||||
75% {color : red;}
|
||||
100% {color : pink;}
|
||||
}
|
||||
.homebrew nav {
|
||||
.homebrewLogo {
|
||||
.animate(color);
|
||||
font-family : CodeBold;
|
||||
font-size : 12px;
|
||||
color : white;
|
||||
div {
|
||||
margin-top : 2px;
|
||||
margin-bottom : -2px;
|
||||
}
|
||||
&:hover {
|
||||
color : @blue;
|
||||
}
|
||||
}
|
||||
.editTitle.navItem {
|
||||
padding : 2px 12px;
|
||||
input {
|
||||
font-family : "Open Sans", sans-serif;
|
||||
font-size : 12px;
|
||||
font-weight : 800;
|
||||
width : 250px;
|
||||
margin : 0;
|
||||
padding : 2px;
|
||||
text-align : center;
|
||||
color : white;
|
||||
border : 1px solid @blue;
|
||||
outline : none;
|
||||
background-color : transparent;
|
||||
}
|
||||
.charCount {
|
||||
display : inline-block;
|
||||
margin-left : 8px;
|
||||
text-align : right;
|
||||
vertical-align : bottom;
|
||||
color : #666;
|
||||
&.max {
|
||||
color : @red;
|
||||
}
|
||||
}
|
||||
}
|
||||
.brewTitle.navItem {
|
||||
font-size : 12px;
|
||||
font-weight : 800;
|
||||
height : 100%;
|
||||
text-align : center;
|
||||
text-transform : initial;
|
||||
color : white;
|
||||
background-color : transparent;
|
||||
flex-grow : 1;
|
||||
}
|
||||
.save-menu {
|
||||
.dropdown {
|
||||
z-index : 1000;
|
||||
}
|
||||
.navItem i.fa-power-off {
|
||||
color : red;
|
||||
&.active {
|
||||
color : rgb(0, 182, 52);
|
||||
filter : drop-shadow(0 0 2px rgba(0, 182, 52, 0.765));
|
||||
}
|
||||
}
|
||||
}
|
||||
.patreon.navItem {
|
||||
border-right : 1px solid #666;
|
||||
border-left : 1px solid #666;
|
||||
&:hover i {
|
||||
color : red;
|
||||
}
|
||||
i {
|
||||
.animate(color);
|
||||
animation-name : pinkColoring;
|
||||
animation-duration : 2s;
|
||||
color : pink;
|
||||
}
|
||||
}
|
||||
.recent.navItem {
|
||||
position : relative;
|
||||
.dropdown {
|
||||
position : absolute;
|
||||
z-index : 10000;
|
||||
top : 28px;
|
||||
left : 0;
|
||||
overflow : hidden auto;
|
||||
width : 100%;
|
||||
max-height : ~"calc(100vh - 28px)";
|
||||
scrollbar-color : #666 #333;
|
||||
scrollbar-width : thin;
|
||||
h4 {
|
||||
font-size : 0.8em;
|
||||
display : block;
|
||||
box-sizing : border-box;
|
||||
padding : 5px 0;
|
||||
text-align : center;
|
||||
color : #BBB;
|
||||
border-top : 1px solid #888;
|
||||
background-color : #333;
|
||||
&:nth-of-type(1) {
|
||||
background-color : darken(@teal, 20%);
|
||||
}
|
||||
&:nth-of-type(2) {
|
||||
background-color : darken(@purple, 30%);
|
||||
}
|
||||
}
|
||||
.item {
|
||||
#backgroundColorsHover;
|
||||
.animate(background-color);
|
||||
position : relative;
|
||||
display : block;
|
||||
box-sizing : border-box;
|
||||
padding : 8px 5px 13px;
|
||||
background-color : #333;
|
||||
color : white;
|
||||
text-decoration : none;
|
||||
border-top : 1px solid #888;
|
||||
overflow : clip;
|
||||
.clear{
|
||||
display : none;
|
||||
position : absolute;
|
||||
top : 50%;
|
||||
transform : translateY(-50%);
|
||||
right : 0px;
|
||||
width : 20px;
|
||||
height : 100%;
|
||||
background-color : #333;
|
||||
opacity : 70%;
|
||||
border-radius : 3px;
|
||||
&:hover {
|
||||
opacity : 100%;
|
||||
}
|
||||
i {
|
||||
text-align : center;
|
||||
font-size : 10px;
|
||||
margin : 0;
|
||||
height :100%;
|
||||
width :100%;
|
||||
}
|
||||
}
|
||||
&:hover{
|
||||
background-color : @blue;
|
||||
|
||||
.clear{
|
||||
display : grid;
|
||||
place-content : center;
|
||||
}
|
||||
}
|
||||
.title{
|
||||
display : inline-block;
|
||||
overflow : hidden;
|
||||
width : 100%;
|
||||
text-overflow : ellipsis;
|
||||
white-space : nowrap;
|
||||
}
|
||||
.time{
|
||||
position : absolute;
|
||||
right : 2px;
|
||||
bottom : 2px;
|
||||
font-size : 0.7em;
|
||||
color : #888;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.warning.navItem{
|
||||
position : relative;
|
||||
background-color : @orange;
|
||||
color : white;
|
||||
&:hover>.dropdown{
|
||||
visibility : visible;
|
||||
}
|
||||
.dropdown{
|
||||
position : absolute;
|
||||
display : block;
|
||||
top : 28px;
|
||||
left : 0px;
|
||||
visibility : hidden;
|
||||
z-index : 10000;
|
||||
box-sizing : border-box;
|
||||
width : 100%;
|
||||
padding : 13px 5px;
|
||||
background-color : #333;
|
||||
text-align : center;
|
||||
}
|
||||
}
|
||||
.account.navItem{
|
||||
min-width: 100px;
|
||||
}
|
||||
.account.username.navItem{
|
||||
text-transform: none;
|
||||
}
|
||||
}
|
||||
.animate(background-color);
|
||||
position : relative;
|
||||
display : block;
|
||||
overflow : clip;
|
||||
box-sizing : border-box;
|
||||
padding : 8px 5px 13px;
|
||||
text-decoration : none;
|
||||
color : white;
|
||||
border-top : 1px solid #888;
|
||||
background-color : #333;
|
||||
.clear {
|
||||
position : absolute;
|
||||
top : 50%;
|
||||
right : 0;
|
||||
display : none;
|
||||
width : 20px;
|
||||
height : 100%;
|
||||
transform : translateY(-50%);
|
||||
opacity : 70%;
|
||||
border-radius : 3px;
|
||||
background-color : #333;
|
||||
&:hover {
|
||||
opacity : 100%;
|
||||
}
|
||||
i {
|
||||
font-size : 10px;
|
||||
width : 100%;
|
||||
height : 100%;
|
||||
margin : 0;
|
||||
text-align : center;
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
background-color : @blue;
|
||||
.clear {
|
||||
display : grid;
|
||||
place-content : center;
|
||||
}
|
||||
}
|
||||
.title {
|
||||
display : inline-block;
|
||||
overflow : hidden;
|
||||
width : 100%;
|
||||
white-space : nowrap;
|
||||
text-overflow : ellipsis;
|
||||
}
|
||||
.time {
|
||||
font-size : 0.7em;
|
||||
position : absolute;
|
||||
right : 2px;
|
||||
bottom : 2px;
|
||||
color : #888;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.metadata.navItem {
|
||||
position : relative;
|
||||
display : flex;
|
||||
align-items : center;
|
||||
height : 100%;
|
||||
padding : 0;
|
||||
flex-grow : 1;
|
||||
i {
|
||||
margin-right : 10px;
|
||||
}
|
||||
.window {
|
||||
position : absolute;
|
||||
z-index : -1;
|
||||
bottom : 0;
|
||||
left : 50%;
|
||||
display : flex;
|
||||
justify-content : flex-start;
|
||||
width : 440px;
|
||||
max-height : ~"calc(100vh - 28px)";
|
||||
margin : 0 auto;
|
||||
padding : 0 10px 5px;
|
||||
transition : transform 0.4s, opacity 0.4s;
|
||||
border : 3px solid #444;
|
||||
border-top : unset;
|
||||
border-radius : 0 0 5px 5px;
|
||||
background-color : #333;
|
||||
box-shadow : inset 0 7px 9px -7px #111;
|
||||
flex-flow : row wrap;
|
||||
align-content : baseline;
|
||||
&.active {
|
||||
transform : translateX(-50%) translateY(100%);
|
||||
opacity : 1;
|
||||
}
|
||||
&.inactive {
|
||||
transform : translateX(-50%) translateY(0%);
|
||||
opacity : 0;
|
||||
}
|
||||
.row {
|
||||
display : flex;
|
||||
width : 100%;
|
||||
flex-flow : row wrap;
|
||||
h4 {
|
||||
display : block;
|
||||
box-sizing : border-box;
|
||||
min-width : 76px;
|
||||
padding : 5px 0;
|
||||
text-align : center;
|
||||
color : #BBB;
|
||||
flex-basis : 20%;
|
||||
flex-grow : 1;
|
||||
}
|
||||
p {
|
||||
font-family : "Open Sans", sans-serif;
|
||||
font-size : 10px;
|
||||
font-weight : normal;
|
||||
padding : 5px 0;
|
||||
text-transform : initial;
|
||||
flex-basis : 80%;
|
||||
flex-grow : 1;
|
||||
.tag {
|
||||
display : inline-block;
|
||||
margin : 2px 2px;
|
||||
padding : 2px;
|
||||
border : 2px solid grey;
|
||||
border-radius : 5px;
|
||||
background-color : #444;
|
||||
}
|
||||
a.userPageLink {
|
||||
text-decoration : none;
|
||||
color : white;
|
||||
&:hover {
|
||||
text-decoration : underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
&:nth-of-type(even) {
|
||||
background-color : #555;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.warning.navItem {
|
||||
position : relative;
|
||||
color : white;
|
||||
background-color : @orange;
|
||||
&:hover > .dropdown {
|
||||
visibility : visible;
|
||||
}
|
||||
.dropdown {
|
||||
position : absolute;
|
||||
z-index : 10000;
|
||||
top : 28px;
|
||||
left : 0;
|
||||
display : block;
|
||||
visibility : hidden;
|
||||
box-sizing : border-box;
|
||||
width : 100%;
|
||||
padding : 13px 5px;
|
||||
text-align : center;
|
||||
background-color : #333;
|
||||
}
|
||||
}
|
||||
.account.navItem {
|
||||
min-width : 100px;
|
||||
}
|
||||
.account.username.navItem {
|
||||
text-transform : none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
}
|
||||
.listPage{
|
||||
.content{
|
||||
z-index : 1;
|
||||
.page{
|
||||
.noColumns() !important; //Needed to override PHB Theme since this is on a lower @layer
|
||||
&::after{
|
||||
@@ -63,7 +64,7 @@
|
||||
border-bottom : 1px solid #666;
|
||||
color : white;
|
||||
text-align : center;
|
||||
z-index : 500;
|
||||
z-index : 1;
|
||||
display : flex;
|
||||
justify-content : center;
|
||||
align-items : baseline;
|
||||
|
||||
@@ -254,6 +254,15 @@ const EditPage = createClass({
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
{this.state.alertTrashedGoogleBrew &&
|
||||
<div className='errorContainer' onClick={this.closeAlerts}>
|
||||
This brew is currently in your Trash folder on Google Drive!<br />If you want to keep it, make sure to move it before it is deleted permanently!<br />
|
||||
<div className='confirm'>
|
||||
OK
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</Nav.item>;
|
||||
},
|
||||
|
||||
@@ -335,16 +344,6 @@ const EditPage = createClass({
|
||||
const shareLink = this.processShareId();
|
||||
|
||||
return <Navbar>
|
||||
|
||||
{this.state.alertTrashedGoogleBrew &&
|
||||
<div className='errorContainer' onClick={this.closeAlerts}>
|
||||
This brew is currently in your Trash folder on Google Drive!<br />If you want to keep it, make sure to move it before it is deleted permanently!<br />
|
||||
<div className='confirm'>
|
||||
OK
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<Nav.section>
|
||||
<Nav.item className='brewTitle'>{this.state.brew.title}</Nav.item>
|
||||
</Nav.section>
|
||||
|
||||
@@ -5,6 +5,7 @@ const { Meta } = require('vitreum/headtags');
|
||||
|
||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||
const Navbar = require('../../navbar/navbar.jsx');
|
||||
const MetadataNav = require('../../navbar/metadata.navitem.jsx');
|
||||
const PrintLink = require('../../navbar/print.navitem.jsx');
|
||||
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
||||
const Account = require('../../navbar/account.navitem.jsx');
|
||||
@@ -50,8 +51,10 @@ const SharePage = createClass({
|
||||
return <div className='sharePage sitePage'>
|
||||
<Meta name='robots' content='noindex, nofollow' />
|
||||
<Navbar>
|
||||
<Nav.section>
|
||||
<Nav.item className='brewTitle'>{this.props.brew.title}</Nav.item>
|
||||
<Nav.section className='titleSection'>
|
||||
<MetadataNav brew={this.props.brew}>
|
||||
<Nav.item className='brewTitle'>{this.props.brew.title}</Nav.item>
|
||||
</MetadataNav>
|
||||
</Nav.section>
|
||||
|
||||
<Nav.section>
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
.sharePage{
|
||||
.navContent .navSection.titleSection {
|
||||
flex-grow: 1;
|
||||
justify-content: center;
|
||||
}
|
||||
.content{
|
||||
overflow-y : hidden;
|
||||
}
|
||||
|
||||
53
client/icons/book-inside-cover.svg
Normal file
53
client/icons/book-inside-cover.svg
Normal file
@@ -0,0 +1,53 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
viewBox="0 0 704.00001 512"
|
||||
version="1.1"
|
||||
id="svg22127"
|
||||
sodipodi:docname="book-inside-cover.svg"
|
||||
width="704"
|
||||
height="512"
|
||||
inkscape:version="1.2.2 (732a01da63, 2022-12-09)"
|
||||
inkscape:export-filename="InsideCover3.png"
|
||||
inkscape:export-xdpi="300"
|
||||
inkscape:export-ydpi="300"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs22131" />
|
||||
<sodipodi:namedview
|
||||
id="namedview22129"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#111111"
|
||||
borderopacity="1"
|
||||
inkscape:showpageshadow="0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pagecheckerboard="1"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
showgrid="false"
|
||||
inkscape:zoom="0.47274833"
|
||||
inkscape:cx="83.55397"
|
||||
inkscape:cy="178.74204"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="991"
|
||||
inkscape:window-x="-9"
|
||||
inkscape:window-y="-9"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg22127" />
|
||||
<!--! Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
|
||||
<path
|
||||
id="path2161-6"
|
||||
style="color:#000000;fill:#000000;stroke-width:1;-inkscape-stroke:none;paint-order:stroke fill markers"
|
||||
d="M 208,0 C 147.0078,0 94.429433,14.25071 60.367188,26.66992 23.520854,39.96036 0,76.16076 0,112.95896 v 317.8321 c 0,59.8499 56.949847,92.6546 107.47266,76.6035 l -0.1543,0.049 c 26.46715,-8.335 74.84649,-18.3965 100.68164,-18.3965 17.25807,0 61.31688,10.6183 85.14453,18.8438 l 0.0508,0.018 0.0527,0.018 c 19.82627,6.5858 40.84117,4.9222 58.99804,-3.0762 18.04267,7.8799 38.84257,9.6126 58.33594,3.1328 l 0.13672,-0.045 0.13672,-0.047 c 23.88445,-8.0588 67.88646,-18.8437 85.14453,-18.8437 25.83515,0 74.22549,10.0266 100.68164,18.3964 l 0.1543,0.049 0.15625,0.049 C 647.13371,523.05316 704,490.64216 704,430.79226 v -317.8321 c 0,-36.8274 -23.49583,-72.8235 -60.00977,-86.25583 l -0.16015,-0.0606 -0.16211,-0.0566 C 609.79193,14.33005 557.11269,0.0012 496,0.0012 434.5671,0.0012 387.12553,14.01354 352,34.94261 316.87446,14.01344 269.4331,0.0012 208,0.0012 Z m 0,32.00977 c 58.3999,0 103.40004,18.89469 123,33.63279 3.3,2.4564 5,6.4246 5,10.3926 v 356.5508 c 0,10.7702 -11.70041,18.2326 -22.40039,14.6426 -26.59996,-8.9751 -71.69966,-22.2012 -105.59961,-22.2012 -38.49993,0 -88.40045,11.4317 -119.900391,21.3516 C 76.799621,449.96896 64,442.03166 64,430.78906 V 80.94726 C 64,69.51586 70.799631,58.93546 82.099609,54.87306 110.29956,44.57516 157.50009,32.00977 208,32.00977 Z m 288,0 c 50.49991,0 97.70044,12.56619 125.90039,22.76949 C 633.20037,58.93616 640,69.51586 640,80.94726 v 349.8418 c 0,11.2426 -12.79963,19.0854 -24.09961,15.5899 -31.49995,-9.9199 -81.40046,-21.3516 -119.90039,-21.3516 -33.89995,0 -78.99966,13.2261 -105.59961,22.2012 C 379.60041,450.81856 368,443.35616 368,432.58596 V 76.03516 c 0,-3.968 1.60001,-7.9362 5,-10.3926 19.59997,-14.7381 64.6001,-33.63279 123,-33.63279 z M 335.52734,45.75386 c -0.1289,0.093 -0.23137,0.2032 -0.35937,0.2969 -0.198,0.1477 -0.428,0.2796 -0.625,0.4278 z m 33.67969,0.5372 0.24805,0.1875 c -0.0427,-0.033 -0.0937,-0.061 -0.13672,-0.094 -0.0393,-0.03 -0.0713,-0.064 -0.11133,-0.094 z" />
|
||||
<path
|
||||
style="color:#000000;fill:#000000;fill-opacity:1;stroke-width:1;-inkscape-stroke:none"
|
||||
d="m 206.76992,184 c -36.98368,0 -73.07301,9.2343 -94.76923,16.9066 v 185.1887 c 27.62799,-7.7405 62.70503,-15.0804 94.76923,-15.0804 28.33376,0 58.16312,7.6425 81.23077,14.806 V 203.0154 C 273.60322,195.1776 243.44241,184 206.76992,184 Z"
|
||||
id="path4372-8"
|
||||
sodipodi:nodetypes="sccsccs" />
|
||||
<path
|
||||
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:63.9999;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 255.99995,122.53007 c -31.8285,-15.342 -80.43462,-15.4137 -112,0"
|
||||
id="path2371-6"
|
||||
sodipodi:nodetypes="cc" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
@@ -40,6 +40,9 @@
|
||||
.book-front-cover {
|
||||
content: url('../icons/book-front-cover.svg');
|
||||
}
|
||||
.book-inside-cover {
|
||||
content: url('../icons/book-inside-cover.svg');
|
||||
}
|
||||
.davek {
|
||||
content: url('../icons/Davek.svg');
|
||||
}
|
||||
|
||||
837
package-lock.json
generated
837
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
54
package.json
54
package.json
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "homebrewery",
|
||||
"description": "Create authentic looking D&D homebrews using only markdown",
|
||||
"version": "3.7.2",
|
||||
"version": "3.8.0",
|
||||
"engines": {
|
||||
"node": "16.11.x"
|
||||
"node": "16.13.x"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -23,7 +23,10 @@
|
||||
"test:coverage": "jest --coverage --silent --runInBand",
|
||||
"test:dev": "jest --verbose --watch",
|
||||
"test:basic": "jest tests/markdown/basic.test.js --verbose",
|
||||
"test:mustache-span": "jest tests/markdown/mustache-span.test.js --verbose",
|
||||
"test:mustache-syntax": "jest '.*(mustache-syntax).*' --verbose --noStackTrace",
|
||||
"test:mustache-syntax:inline": "jest '.*(mustache-syntax).*' -t '^Inline:.*' --verbose --noStackTrace",
|
||||
"test:mustache-syntax:block": "jest '.*(mustache-syntax).*' -t '^Block:.*' --verbose --noStackTrace",
|
||||
"test:mustache-syntax:injection": "jest '.*(mustache-syntax).*' -t '^Injection:.*' --verbose --noStackTrace",
|
||||
"test:route": "jest tests/routes/static-pages.test.js --verbose",
|
||||
"phb": "node scripts/phb.js",
|
||||
"prod": "set NODE_ENV=production && npm run build",
|
||||
@@ -45,20 +48,23 @@
|
||||
"coveragePathIgnorePatterns": [
|
||||
"build/*"
|
||||
],
|
||||
"coverageThreshold" : {
|
||||
"global" : {
|
||||
"statements" : 25,
|
||||
"branches" : 10,
|
||||
"functions" : 22,
|
||||
"lines" : 25
|
||||
"coverageThreshold": {
|
||||
"global": {
|
||||
"statements": 25,
|
||||
"branches": 10,
|
||||
"functions": 22,
|
||||
"lines": 25
|
||||
},
|
||||
"server/homebrew.api.js" : {
|
||||
"statements" : 65,
|
||||
"branches" : 50,
|
||||
"functions" : 60,
|
||||
"lines" : 70
|
||||
"server/homebrew.api.js": {
|
||||
"statements": 65,
|
||||
"branches": 50,
|
||||
"functions": 60,
|
||||
"lines": 70
|
||||
}
|
||||
}
|
||||
},
|
||||
"setupFilesAfterEnv": [
|
||||
"jest-expect-message"
|
||||
]
|
||||
},
|
||||
"babel": {
|
||||
"presets": [
|
||||
@@ -70,9 +76,9 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.21.3",
|
||||
"@babel/plugin-transform-runtime": "^7.21.0",
|
||||
"@babel/preset-env": "^7.19.4",
|
||||
"@babel/core": "^7.21.4",
|
||||
"@babel/plugin-transform-runtime": "^7.21.4",
|
||||
"@babel/preset-env": "^7.21.4",
|
||||
"@babel/preset-react": "^7.18.6",
|
||||
"@googleapis/drive": "^5.0.2",
|
||||
"body-parser": "^1.20.2",
|
||||
@@ -93,22 +99,24 @@
|
||||
"marked-extended-tables": "^1.0.5",
|
||||
"markedLegacy": "npm:marked@^0.3.19",
|
||||
"moment": "^2.29.4",
|
||||
"mongoose": "^7.0.3",
|
||||
"mongoose": "^7.1.0",
|
||||
"nanoid": "3.3.4",
|
||||
"nconf": "^0.12.0",
|
||||
"npm": "^9.6.2",
|
||||
"npm": "^9.6.5",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-frame-component": "5.2.6",
|
||||
"react-router-dom": "6.9.0",
|
||||
"react-frame-component": "^4.1.3",
|
||||
"react-router-dom": "6.10.0",
|
||||
"sanitize-filename": "1.6.3",
|
||||
"superagent": "^6.1.0",
|
||||
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^8.36.0",
|
||||
"eslint": "^8.39.0",
|
||||
"eslint-plugin-react": "^7.32.2",
|
||||
"eslint-plugin-jest": "^27.2.1",
|
||||
"jest": "^29.5.0",
|
||||
"jest-expect-message": "^1.1.3",
|
||||
"supertest": "^6.3.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,8 +43,7 @@ const sanitizeBrew = (brew, accessType)=>{
|
||||
};
|
||||
|
||||
app.use('/', serveCompressedStaticAssets(`build`));
|
||||
|
||||
//app.use(express.static(`${__dirname}/build`));
|
||||
app.use(require('./middleware/content-negotiation.js'));
|
||||
app.use(require('body-parser').json({ limit: '25mb' }));
|
||||
app.use(require('cookie-parser')());
|
||||
app.use(require('./forcessl.mw.js'));
|
||||
|
||||
@@ -305,7 +305,7 @@ If you believe you should have access to this brew, ask the file owner to invite
|
||||
|
||||
if(brew.authors.length === 0) {
|
||||
// Delete brew if there are no authors left
|
||||
await brew.remove()
|
||||
await HomebrewModel.deleteOne({ _id: brew._id })
|
||||
.catch((err)=>{
|
||||
console.error(err);
|
||||
throw { status: 500, message: 'Error while removing' };
|
||||
|
||||
@@ -10,7 +10,6 @@ describe('Tests for api', ()=>{
|
||||
|
||||
let modelBrew;
|
||||
let saveFunc;
|
||||
let removeFunc;
|
||||
let markModifiedFunc;
|
||||
let saved;
|
||||
|
||||
@@ -20,18 +19,15 @@ describe('Tests for api', ()=>{
|
||||
saved = { ...this, _id: '1' };
|
||||
return saved;
|
||||
});
|
||||
removeFunc = jest.fn(async function() {});
|
||||
markModifiedFunc = jest.fn(()=>true);
|
||||
|
||||
modelBrew = (brew)=>({
|
||||
...brew,
|
||||
save : saveFunc,
|
||||
remove : removeFunc,
|
||||
markModified : markModifiedFunc,
|
||||
toObject : function() {
|
||||
delete this.save;
|
||||
delete this.toObject;
|
||||
delete this.remove;
|
||||
delete this.markModified;
|
||||
return this;
|
||||
}
|
||||
@@ -569,13 +565,14 @@ brew`);
|
||||
req.brew = brew;
|
||||
});
|
||||
model.findOne = jest.fn(async ()=>modelBrew(brew));
|
||||
model.deleteOne = jest.fn(async ()=>{});
|
||||
const req = {};
|
||||
|
||||
await api.deleteBrew(req, res);
|
||||
|
||||
expect(api.getBrew).toHaveBeenCalled();
|
||||
expect(model.findOne).toHaveBeenCalled();
|
||||
expect(removeFunc).toHaveBeenCalled();
|
||||
expect(model.deleteOne).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw on delete error', async ()=>{
|
||||
@@ -587,7 +584,7 @@ brew`);
|
||||
req.brew = brew;
|
||||
});
|
||||
model.findOne = jest.fn(async ()=>modelBrew(brew));
|
||||
removeFunc = jest.fn(async ()=>{ throw 'err'; });
|
||||
model.deleteOne = jest.fn(async ()=>{ throw 'err'; });
|
||||
const req = {};
|
||||
|
||||
let err;
|
||||
@@ -600,7 +597,7 @@ brew`);
|
||||
expect(err).not.toBeUndefined();
|
||||
expect(api.getBrew).toHaveBeenCalled();
|
||||
expect(model.findOne).toHaveBeenCalled();
|
||||
expect(removeFunc).toHaveBeenCalled();
|
||||
expect(model.deleteOne).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should delete when one author', async ()=>{
|
||||
@@ -612,13 +609,14 @@ brew`);
|
||||
req.brew = brew;
|
||||
});
|
||||
model.findOne = jest.fn(async ()=>modelBrew(brew));
|
||||
model.deleteOne = jest.fn(async ()=>{});
|
||||
const req = { account: { username: 'test' } };
|
||||
|
||||
await api.deleteBrew(req, res);
|
||||
|
||||
expect(api.getBrew).toHaveBeenCalled();
|
||||
expect(model.findOne).toHaveBeenCalled();
|
||||
expect(removeFunc).toHaveBeenCalled();
|
||||
expect(model.deleteOne).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should remove one author when multiple present', async ()=>{
|
||||
@@ -630,6 +628,7 @@ brew`);
|
||||
req.brew = brew;
|
||||
});
|
||||
model.findOne = jest.fn(async ()=>modelBrew(brew));
|
||||
model.deleteOne = jest.fn(async ()=>{});
|
||||
const req = { account: { username: 'test' } };
|
||||
|
||||
await api.deleteBrew(req, res);
|
||||
@@ -637,7 +636,7 @@ brew`);
|
||||
expect(api.getBrew).toHaveBeenCalled();
|
||||
expect(markModifiedFunc).toHaveBeenCalled();
|
||||
expect(model.findOne).toHaveBeenCalled();
|
||||
expect(removeFunc).not.toHaveBeenCalled();
|
||||
expect(model.deleteOne).not.toHaveBeenCalled();
|
||||
expect(saveFunc).toHaveBeenCalled();
|
||||
expect(saved.authors).toEqual(['test2']);
|
||||
});
|
||||
@@ -651,6 +650,7 @@ brew`);
|
||||
req.brew = brew;
|
||||
});
|
||||
model.findOne = jest.fn(async ()=>modelBrew(brew));
|
||||
model.deleteOne = jest.fn(async ()=>{});
|
||||
saveFunc = jest.fn(async ()=>{ throw 'err'; });
|
||||
const req = { account: { username: 'test' } };
|
||||
|
||||
@@ -664,7 +664,7 @@ brew`);
|
||||
expect(err).not.toBeUndefined();
|
||||
expect(api.getBrew).toHaveBeenCalled();
|
||||
expect(model.findOne).toHaveBeenCalled();
|
||||
expect(removeFunc).not.toHaveBeenCalled();
|
||||
expect(model.deleteOne).not.toHaveBeenCalled();
|
||||
expect(saveFunc).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -677,6 +677,7 @@ brew`);
|
||||
req.brew = brew;
|
||||
});
|
||||
model.findOne = jest.fn(async ()=>modelBrew(brew));
|
||||
model.deleteOne = jest.fn(async ()=>{});
|
||||
api.deleteGoogleBrew = jest.fn(async ()=>true);
|
||||
const req = { account: { username: 'test' } };
|
||||
|
||||
@@ -684,7 +685,7 @@ brew`);
|
||||
|
||||
expect(api.getBrew).toHaveBeenCalled();
|
||||
expect(model.findOne).toHaveBeenCalled();
|
||||
expect(removeFunc).toHaveBeenCalled();
|
||||
expect(model.deleteOne).toHaveBeenCalled();
|
||||
expect(api.deleteGoogleBrew).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -697,6 +698,7 @@ brew`);
|
||||
req.brew = brew;
|
||||
});
|
||||
model.findOne = jest.fn(async ()=>modelBrew(brew));
|
||||
model.deleteOne = jest.fn(async ()=>{});
|
||||
api.deleteGoogleBrew = jest.fn(async ()=>{
|
||||
throw 'err';
|
||||
});
|
||||
@@ -706,7 +708,7 @@ brew`);
|
||||
|
||||
expect(api.getBrew).toHaveBeenCalled();
|
||||
expect(model.findOne).toHaveBeenCalled();
|
||||
expect(removeFunc).toHaveBeenCalled();
|
||||
expect(model.deleteOne).toHaveBeenCalled();
|
||||
expect(api.deleteGoogleBrew).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -719,6 +721,7 @@ brew`);
|
||||
req.brew = brew;
|
||||
});
|
||||
model.findOne = jest.fn(async ()=>modelBrew(brew));
|
||||
model.deleteOne = jest.fn(async ()=>{});
|
||||
api.deleteGoogleBrew = jest.fn(async ()=>true);
|
||||
const req = { account: { username: 'test' } };
|
||||
|
||||
@@ -727,7 +730,7 @@ brew`);
|
||||
expect(api.getBrew).toHaveBeenCalled();
|
||||
expect(markModifiedFunc).toHaveBeenCalled();
|
||||
expect(model.findOne).toHaveBeenCalled();
|
||||
expect(removeFunc).not.toHaveBeenCalled();
|
||||
expect(model.deleteOne).not.toHaveBeenCalled();
|
||||
expect(api.deleteGoogleBrew).toHaveBeenCalled();
|
||||
expect(saveFunc).toHaveBeenCalled();
|
||||
expect(saved.authors).toEqual(['test2']);
|
||||
@@ -745,6 +748,7 @@ brew`);
|
||||
req.brew = brew;
|
||||
});
|
||||
model.findOne = jest.fn(async ()=>modelBrew(brew));
|
||||
model.deleteOne = jest.fn(async ()=>{});
|
||||
api.deleteGoogleBrew = jest.fn(async ()=>true);
|
||||
const req = { account: { username: 'test2' } };
|
||||
|
||||
@@ -752,7 +756,7 @@ brew`);
|
||||
|
||||
expect(api.getBrew).toHaveBeenCalled();
|
||||
expect(model.findOne).toHaveBeenCalled();
|
||||
expect(removeFunc).not.toHaveBeenCalled();
|
||||
expect(model.deleteOne).not.toHaveBeenCalled();
|
||||
expect(api.deleteGoogleBrew).not.toHaveBeenCalled();
|
||||
expect(saveFunc).toHaveBeenCalled();
|
||||
expect(saved.authors).toEqual(['test']);
|
||||
|
||||
12
server/middleware/content-negotiation.js
Normal file
12
server/middleware/content-negotiation.js
Normal file
@@ -0,0 +1,12 @@
|
||||
module.exports = (req, res, next)=>{
|
||||
const isImageRequest = req.get('Accept')?.split(',')
|
||||
?.filter((h)=>!h.includes('q='))
|
||||
?.every((h)=>/image\/.*/.test(h));
|
||||
if(isImageRequest) {
|
||||
return res.status(406).send({
|
||||
message : 'Request for image at this URL is not supported'
|
||||
});
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
41
server/middleware/content-negotiation.spec.js
Normal file
41
server/middleware/content-negotiation.spec.js
Normal file
@@ -0,0 +1,41 @@
|
||||
const contentNegotiationMiddleware = require('./content-negotiation.js');
|
||||
|
||||
describe('content-negotiation-middleware', ()=>{
|
||||
let request;
|
||||
let response;
|
||||
let next;
|
||||
|
||||
beforeEach(()=>{
|
||||
request = {
|
||||
get : function(key) {
|
||||
return this[key];
|
||||
}
|
||||
};
|
||||
response = {
|
||||
status : jest.fn(()=>response),
|
||||
send : jest.fn(()=>{})
|
||||
};
|
||||
next = jest.fn();
|
||||
});
|
||||
|
||||
it('should return 406 on image request', ()=>{
|
||||
contentNegotiationMiddleware({
|
||||
Accept : 'image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8',
|
||||
...request
|
||||
}, response);
|
||||
|
||||
expect(response.status).toHaveBeenLastCalledWith(406);
|
||||
expect(response.send).toHaveBeenCalledWith({
|
||||
message : 'Request for image at this URL is not supported'
|
||||
});
|
||||
});
|
||||
|
||||
it('should call next on non-image request', ()=>{
|
||||
contentNegotiationMiddleware({
|
||||
Accept : 'text,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8',
|
||||
...request
|
||||
}, response, next);
|
||||
|
||||
expect(next).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -134,7 +134,7 @@ const mustacheInjectInline = {
|
||||
const match = inlineRegex.exec(src);
|
||||
if(match) {
|
||||
const lastToken = tokens[tokens.length - 1];
|
||||
if(!lastToken)
|
||||
if(!lastToken || lastToken.type == 'mustacheInjectInline')
|
||||
return false;
|
||||
|
||||
const tags = ` ${processStyleTags(match[1])}`;
|
||||
@@ -169,7 +169,7 @@ const mustacheInjectBlock = {
|
||||
const match = inlineRegex.exec(src);
|
||||
if(match) {
|
||||
const lastToken = tokens[tokens.length - 1];
|
||||
if(!lastToken)
|
||||
if(!lastToken || lastToken.type == 'mustacheInjectBlock')
|
||||
return false;
|
||||
|
||||
lastToken.originalType = 'mustacheInjectBlock';
|
||||
|
||||
@@ -29,7 +29,7 @@ const Nav = {
|
||||
section : createClass({
|
||||
displayName : 'Nav.section',
|
||||
render : function(){
|
||||
return <div className='navSection'>
|
||||
return <div className={`navSection ${this.props.className ?? ''}`}>
|
||||
{this.props.children}
|
||||
</div>;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ nav{
|
||||
position : relative;
|
||||
display : flex;
|
||||
justify-content : space-between;
|
||||
z-index : 2;
|
||||
}
|
||||
.navSection{
|
||||
display : flex;
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
/* 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.
|
||||
|
||||
375
tests/markdown/mustache-syntax.test.js
Normal file
375
tests/markdown/mustache-syntax.test.js
Normal file
@@ -0,0 +1,375 @@
|
||||
/* eslint-disable max-lines */
|
||||
|
||||
const dedent = require('dedent-tabs').default;
|
||||
const Markdown = require('naturalcrit/markdown.js');
|
||||
|
||||
// Marked.js adds line returns after closing tags on some default tokens.
|
||||
// This removes those line returns for comparison sake.
|
||||
String.prototype.trimReturns = function(){
|
||||
return this.replace(/\r?\n|\r/g, '');
|
||||
};
|
||||
|
||||
// Adding `.failing()` method to `describe` or `it` will make failing tests "pass" as long as they continue to fail.
|
||||
// Remove the `.failing()` method once you have fixed the issue.
|
||||
|
||||
describe('Inline: When using the Inline syntax {{ }}', ()=>{
|
||||
it.failing('Renders a mustache span with text only', function() {
|
||||
const source = '{{ text}}';
|
||||
const rendered = Markdown.render(source);
|
||||
// FIXME: adds extra \s after class names
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block">text</span>');
|
||||
});
|
||||
|
||||
it.failing('Renders a mustache span with text only, but with spaces', function() {
|
||||
const source = '{{ this is a text}}';
|
||||
const rendered = Markdown.render(source);
|
||||
// FIXME: adds extra \s after class names
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block">this is a text</span>');
|
||||
});
|
||||
|
||||
it.failing('Renders an empty mustache span', function() {
|
||||
const source = '{{}}';
|
||||
const rendered = Markdown.render(source);
|
||||
// FIXME: adds extra \s after class names
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block"></span>');
|
||||
});
|
||||
|
||||
it.failing('Renders a mustache span with just a space', function() {
|
||||
const source = '{{ }}';
|
||||
const rendered = Markdown.render(source);
|
||||
// FIXME: adds extra \s after class names
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block"></span>');
|
||||
});
|
||||
|
||||
it.failing('Renders a mustache span with a few spaces only', function() {
|
||||
const source = '{{ }}';
|
||||
const rendered = Markdown.render(source);
|
||||
// FIXME: adds extra \s after class names
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block"></span>');
|
||||
});
|
||||
|
||||
it.failing('Renders a mustache span with text and class', function() {
|
||||
const source = '{{my-class text}}';
|
||||
const rendered = Markdown.render(source);
|
||||
// FIXME: adds two extra \s before closing `>` in opening tag.
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block my-class">text</span>');
|
||||
});
|
||||
|
||||
it.failing('Renders a mustache span with text and two classes', function() {
|
||||
const source = '{{my-class,my-class2 text}}';
|
||||
const rendered = Markdown.render(source);
|
||||
// FIXME: adds two extra \s before closing `>` in opening tag.
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block my-class my-class2">text</span>');
|
||||
});
|
||||
|
||||
it.failing('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: adds two extra \s before closing `>` in opening tag
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block my-class">this is a text</span>');
|
||||
});
|
||||
|
||||
it.failing('Renders a mustache span with text and id', function() {
|
||||
const source = '{{#my-span text}}';
|
||||
const rendered = Markdown.render(source);
|
||||
// FIXME: adds extra \s before closing `>` in opening tag, and another after class names
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" id="my-span">text</span>');
|
||||
});
|
||||
|
||||
it.failing('Renders a mustache span with text and two ids', function() {
|
||||
const source = '{{#my-span,#my-favorite-span text}}';
|
||||
const rendered = Markdown.render(source);
|
||||
// FIXME: adds extra \s before closing `>` in opening tag, and another after class names
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" id="my-span">text</span>');
|
||||
});
|
||||
|
||||
it.failing('Renders a mustache span with text and css property', function() {
|
||||
const source = '{{color:red text}}';
|
||||
const rendered = Markdown.render(source);
|
||||
// FIXME: adds extra \s after class names
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" style="color:red;">text</span>');
|
||||
});
|
||||
|
||||
it.failing('Renders a mustache span with text and two css properties', function() {
|
||||
const source = '{{color:red,padding:5px text}}';
|
||||
const rendered = Markdown.render(source);
|
||||
// FIXME: adds extra \s after class names
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" style="color:red; padding:5px;">text</span>');
|
||||
});
|
||||
|
||||
it.failing('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);
|
||||
// FIXME: adds extra \s after class names
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" style="font-family:trebuchet ms;">text</span>');
|
||||
});
|
||||
|
||||
it.failing('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);
|
||||
// FIXME: adds extra \s after class names
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" style="font-family:trebuchet ms; padding:5px 10px;">text</span>');
|
||||
});
|
||||
|
||||
|
||||
it.failing('Renders a mustache span with text with quotes and css property which contains quotes', function() {
|
||||
const source = '{{font-family:"trebuchet ms" text "with quotes"}}';
|
||||
const rendered = Markdown.render(source);
|
||||
// FIXME: adds extra \s after class names
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" style="font-family:trebuchet ms;">text “with quotes”</span>');
|
||||
});
|
||||
|
||||
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);
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block pen" id="author" style="color:orange; font-family:trebuchet ms;">text</span>');
|
||||
});
|
||||
});
|
||||
|
||||
// BLOCK SYNTAX
|
||||
|
||||
describe(`Block: When using the Block syntax {{tags\\ntext\\n}}`, ()=>{
|
||||
it.failing('Renders a div with text only', function() {
|
||||
const source = dedent`{{
|
||||
text
|
||||
}}`;
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
// FIXME: adds extra \s after class names
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block"><p>text</p></div>`);
|
||||
});
|
||||
|
||||
it.failing('Renders an empty div', function() {
|
||||
const source = dedent`{{
|
||||
|
||||
}}`;
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
// FIXME: adds extra \s after class names
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block"></div>`);
|
||||
});
|
||||
|
||||
it('Renders a single paragraph with opening and closing brackets', function() {
|
||||
const source = dedent`{{
|
||||
}}`;
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
// this actually renders in HB as '{{ }}'...
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p>{{}}</p>`);
|
||||
});
|
||||
|
||||
it.failing('Renders a div with a single class', function() {
|
||||
const source = dedent`{{cat
|
||||
|
||||
}}`;
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
// FIXME: adds two extra \s before closing `>` in opening tag
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block cat"></div>`);
|
||||
});
|
||||
|
||||
it.failing('Renders a div with a single class and text', function() {
|
||||
const source = dedent`{{cat
|
||||
Sample text.
|
||||
}}`;
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
// FIXME: adds two extra \s before closing `>` in opening tag
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block cat"><p>Sample text.</p></div>`);
|
||||
});
|
||||
|
||||
it.failing('Renders a div with two classes and text', function() {
|
||||
const source = dedent`{{cat,dog
|
||||
Sample text.
|
||||
}}`;
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
// FIXME: adds two extra \s before closing `>` in opening tag
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block cat dog"><p>Sample text.</p></div>`);
|
||||
});
|
||||
|
||||
it.failing('Renders a div with a style and text', function() {
|
||||
const source = dedent`{{color:red
|
||||
Sample text.
|
||||
}}`;
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
// FIXME: adds two extra \s before closing `>` in opening tag
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block" style="color:red;"><p>Sample text.</p></div>`);
|
||||
});
|
||||
|
||||
it.failing('Renders a div with a class, style and text', function() {
|
||||
const source = dedent`{{cat,color:red
|
||||
Sample text.
|
||||
}}`;
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
// FIXME: adds extra \s after the class attribute
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block cat" style="color:red;"><p>Sample text.</p></div>`);
|
||||
});
|
||||
|
||||
it('Renders a div with an ID, class, style and text (different order)', function() {
|
||||
const source = dedent`{{color:red,cat,#dog
|
||||
Sample text.
|
||||
}}`;
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block cat" id="dog" style="color:red;"><p>Sample text.</p></div>`);
|
||||
});
|
||||
|
||||
it.failing('Renders a div with a single ID', function() {
|
||||
const source = dedent`{{#cat,#dog
|
||||
Sample text.
|
||||
}}`;
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
// FIXME: adds extra \s before closing `>` in opening tag, and another after class names
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block" id="cat"><p>Sample text.</p></div>`);
|
||||
});
|
||||
});
|
||||
|
||||
// MUSTACHE INJECTION SYNTAX
|
||||
|
||||
describe('Injection: When an injection tag follows an element', ()=>{
|
||||
// FIXME: Most of these fail because injections currently replace attributes, rather than append to. Or just minor extra whitespace issues.
|
||||
describe('and that element is an inline-block', ()=>{
|
||||
it.failing('Renders a span "text" with no injection', function() {
|
||||
const source = '{{ text}}{}';
|
||||
const rendered = Markdown.render(source);
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block">text</span>');
|
||||
});
|
||||
|
||||
it.failing('Renders a span "text" with injected Class name', function() {
|
||||
const source = '{{ text}}{ClassName}';
|
||||
const rendered = Markdown.render(source);
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block ClassName">text</span>');
|
||||
});
|
||||
|
||||
it.failing('Renders a span "text" with injected style', function() {
|
||||
const source = '{{ text}}{color:red}';
|
||||
const rendered = Markdown.render(source);
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" style="color:red;">text</span>');
|
||||
});
|
||||
|
||||
it.failing('Renders a span "text" with two injected styles', function() {
|
||||
const source = '{{ text}}{color:red,background:blue}';
|
||||
const rendered = Markdown.render(source);
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" style="color:red; background:blue;">text</span>');
|
||||
});
|
||||
|
||||
it.failing('Renders an emphasis element with injected Class name', function() {
|
||||
const source = '*emphasis*{big}';
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p><em class="big">emphasis</em></p>');
|
||||
});
|
||||
|
||||
it.failing('Renders a code element with injected style', function() {
|
||||
const source = '`code`{background:gray}';
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p><code style="background:gray;">code</code></p>');
|
||||
});
|
||||
|
||||
it.failing('Renders an image element with injected style', function() {
|
||||
const source = '{position:absolute}';
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p><img src="http://i.imgur.com/hMna6G0.png" alt="homebrew mug" style="position:absolute;"></p>');
|
||||
});
|
||||
|
||||
it.failing('Renders an element modified by only the first of two consecutive injections', function() {
|
||||
const source = '{{ text}}{color:red}{background:blue}';
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p><span class="inline-block" style="color:red;">text</span>{background:blue}</p>');
|
||||
});
|
||||
});
|
||||
|
||||
describe('and that element is a block', ()=>{
|
||||
it.failing('renders a div "text" with no injection', function() {
|
||||
const source = '{{\ntext\n}}\n{}';
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<div class="block"><p>text</p></div>');
|
||||
});
|
||||
|
||||
it.failing('renders a div "text" with injected Class name', function() {
|
||||
const source = '{{\ntext\n}}\n{ClassName}';
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<div class="block ClassName"><p>text</p></div>');
|
||||
});
|
||||
|
||||
it.failing('renders a div "text" with injected style', function() {
|
||||
const source = '{{\ntext\n}}\n{color:red}';
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<div class="block" style="color:red;"><p>text</p></div>');
|
||||
});
|
||||
|
||||
it.failing('renders a div "text" with two injected styles', function() {
|
||||
const source = dedent`{{
|
||||
text
|
||||
}}
|
||||
{color:red,background:blue}`;
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<div class="block" style="color:red; background:blue;"><p>text</p></div>');
|
||||
});
|
||||
|
||||
it.failing('renders an h2 header "text" with injected class name', function() {
|
||||
const source = dedent`## text
|
||||
{ClassName}`;
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<h2 class="ClassName">text</h2>');
|
||||
});
|
||||
|
||||
it.failing('renders a table with injected class name', function() {
|
||||
const source = dedent`| Experience Points | Level |
|
||||
|:------------------|:-----:|
|
||||
| 0 | 1 |
|
||||
| 300 | 2 |
|
||||
|
||||
{ClassName}`;
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<table class="ClassName"><thead><tr><th align=left>Experience Points</th><th align=center>Level</th></tr></thead><tbody><tr><td align=left>0</td><td align=center>1</td></tr><tr><td align=left>300</td><td align=center>2</td></tr></tbody></table>`);
|
||||
});
|
||||
|
||||
// it('renders a list with with a style injected into the <ul> tag', function() {
|
||||
// const source = dedent`- Cursed Ritual of Bad Hair
|
||||
// - Eliminate Vindictiveness in Gym Teacher
|
||||
// - Ultimate Rite of the Confetti Angel
|
||||
// - Dark Chant of the Dentists
|
||||
// - Divine Spell of Crossdressing
|
||||
// {color:red}`;
|
||||
// const rendered = Markdown.render(source).trimReturns();
|
||||
// expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`...`); // FIXME: expect this to be injected into <ul>? Currently injects into last <li>
|
||||
// });
|
||||
|
||||
it.failing('renders an h2 header "text" with injected class name, and "secondInjection" as regular text on the next line.', function() {
|
||||
const source = dedent`## text
|
||||
{ClassName}
|
||||
{secondInjection}`;
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<h2 class="ClassName">text</h2><p>{secondInjection}</p>');
|
||||
});
|
||||
|
||||
it.failing('renders a div nested into another div, the inner with class=innerDiv and the other class=outerDiv', function() {
|
||||
const source = dedent`{{
|
||||
outer text
|
||||
{{
|
||||
inner text
|
||||
}}
|
||||
{innerDiv}
|
||||
}}
|
||||
{outerDiv}`;
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<div class="block outerDiv"><p>outer text</p><div class="block innerDiv"><p>inner text</p></div></div>');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// 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.
|
||||
@@ -3,7 +3,7 @@
|
||||
const MagicGen = require('./snippets/magic.gen.js');
|
||||
const ClassTableGen = require('./snippets/classtable.gen.js');
|
||||
const MonsterBlockGen = require('./snippets/monsterblock.gen.js');
|
||||
const scriptGen = require('./snippets/script.gen.js');
|
||||
const scriptGen = require('./snippets/script.gen.js');
|
||||
const ClassFeatureGen = require('./snippets/classfeature.gen.js');
|
||||
const CoverPageGen = require('./snippets/coverpage.gen.js');
|
||||
const PartCoverPageGen = require('./snippets/partcoverpage.gen.js');
|
||||
@@ -171,9 +171,15 @@ module.exports = [
|
||||
gen : MonsterBlockGen.monster('monster,frame,wide', 4),
|
||||
},
|
||||
{
|
||||
name : 'Cover Page',
|
||||
name : 'Front Cover Page',
|
||||
icon : 'fac book-front-cover',
|
||||
gen : CoverPageGen,
|
||||
gen : CoverPageGen.front,
|
||||
experimental : true
|
||||
},
|
||||
{
|
||||
name : 'Inside Cover Page',
|
||||
icon : 'fac book-inside-cover',
|
||||
gen : CoverPageGen.inside,
|
||||
experimental : true
|
||||
},
|
||||
{
|
||||
|
||||
@@ -13,7 +13,7 @@ const titles = [
|
||||
'The Living Dead Above the Fearful Cage', 'Bahamut\'s Demonspawn',
|
||||
'Across Gruumsh\'s Elemental Chaos', 'The Blade of Orcus',
|
||||
'Beyond Revenge', 'Brain of Insanity',
|
||||
'Breed Battle!, A New Beginning', 'Evil Lake, A New Beginning',
|
||||
'A New Beginning', 'Evil Lake of the Merfolk',
|
||||
'Invasion of the Gigantic Cat, Part II', 'Kraken War 2020',
|
||||
'The Body Whisperers', 'The Doctor from Heaven',
|
||||
'The Diabolical Tales of the Ape-Women', 'The Doctor Immortal',
|
||||
@@ -23,7 +23,7 @@ const titles = [
|
||||
'Sky of Zelda: The Thunder of Force', 'Core Battle',
|
||||
'Ruby of Atlantis: The Quake of Peace', 'Deadly Amazement III',
|
||||
'Dry Chaos IX', 'Gate Thunder',
|
||||
'Vyse\'s Skies', 'White Greatness III',
|
||||
'Vyse\'s Skies', 'Blue Greatness III',
|
||||
'Yellow Divinity', 'Zidane\'s Ghost'
|
||||
];
|
||||
|
||||
@@ -68,23 +68,43 @@ const footnote = [
|
||||
'In an amazing kingdom, in an age of sorcery and lost souls, eight space pirates quest for freedom.'
|
||||
];
|
||||
|
||||
module.exports = ()=>{
|
||||
return dedent`
|
||||
{{coverPage }}
|
||||
module.exports = {
|
||||
|
||||
{{logo }}
|
||||
front : function() {
|
||||
return dedent`
|
||||
{{frontCover}}
|
||||
|
||||
# ${_.sample(titles)}
|
||||
## ${_.sample(subtitles)}
|
||||
__________
|
||||
{{logo }}
|
||||
|
||||
{{banner HOMEBREW}}
|
||||
# ${_.sample(titles)}
|
||||
## ${_.sample(subtitles)}
|
||||
___
|
||||
|
||||
{{footnote
|
||||
${_.sample(footnote)}
|
||||
}}
|
||||
{{banner HOMEBREW}}
|
||||
|
||||

|
||||
{{footnote
|
||||
${_.sample(footnote)}
|
||||
}}
|
||||
|
||||
\page`;
|
||||
{position:absolute,bottom:0,left:0,height:100%}
|
||||
|
||||
\page`;
|
||||
},
|
||||
|
||||
inside : function() {
|
||||
return dedent`
|
||||
{{insideCover}}
|
||||
|
||||
# ${_.sample(titles)}
|
||||
## ${_.sample(subtitles)}
|
||||
___
|
||||
|
||||
{{imageMaskCenter${_.random(1, 16)},--offsetX:0%,--offsetY:0%,--rotation:0
|
||||
{height:100%}
|
||||
}}
|
||||
|
||||
{{logo }}
|
||||
|
||||
\page`;
|
||||
}
|
||||
};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,9 +2,9 @@ const _ = require('lodash');
|
||||
const dedent = require('dedent-tabs').default;
|
||||
|
||||
module.exports = {
|
||||
center :()=>{
|
||||
center : ()=>{
|
||||
return dedent`
|
||||
{{imageMaskCenter${_.random(1, 16)},--offsetX:0%,--offsetY:0%,--rotation:0;
|
||||
{{imageMaskCenter${_.random(1, 16)},--offsetX:0%,--offsetY:0%,--rotation:0
|
||||
{height:100%}
|
||||
}}
|
||||
<!-- Use --offsetX to shift the mask left or right (can use cm instead of %)
|
||||
|
||||
@@ -23,6 +23,9 @@ body {
|
||||
break-inside : avoid;
|
||||
display : inline-block;
|
||||
width : 100%;
|
||||
img {
|
||||
z-index : 0;
|
||||
}
|
||||
}
|
||||
.inline-block {
|
||||
display : inline-block;
|
||||
@@ -251,7 +254,6 @@ body {
|
||||
background-size : 20px;
|
||||
z-index : -1;
|
||||
transform : translateY(50%) translateX(-50%) rotate(calc(1deg * var(--rotation))) scaleX(var(--scaleX)) scaleY(var(--scaleY));
|
||||
transition : transform 2s;
|
||||
& > p:has(img) {
|
||||
position : absolute;
|
||||
width : 50%;
|
||||
@@ -259,7 +261,6 @@ body {
|
||||
bottom : 50%;
|
||||
left : 50%;
|
||||
transform : translateX(-50%) translateY(50%) rotate(calc(-1deg * var(--rotation))) scaleX(calc(1 / var(--scaleX))) scaleY(calc(1 / var(--scaleY)));
|
||||
transition : transform 2s;
|
||||
}
|
||||
& img {
|
||||
position : absolute;
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
@horizontalRule : url('/assets/horizontalRule.svg');
|
||||
@partCoverHeaderPHB : url('/assets/partCoverHeaderPHB.png');
|
||||
@partCoverHeaderDMG : url('/assets/partCoverHeaderDMG.svg');
|
||||
@insideCoverMask : url('/assets/insideCoverMask.png');
|
||||
@scriptBorder : url('/assets/scriptBorder.png');
|
||||
|
||||
// Watercolor Images
|
||||
|
||||
BIN
themes/assets/insideCoverMask.png
Normal file
BIN
themes/assets/insideCoverMask.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 262 KiB |
Binary file not shown.
Reference in New Issue
Block a user