0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2026-01-05 10:12:41 +00:00

Merge branch 'master' of https://github.com/naturalcrit/homebrewery into experimental-development

This commit is contained in:
Víctor Losada Hernández
2024-05-06 13:45:06 +02:00
28 changed files with 912 additions and 625 deletions

View File

@@ -0,0 +1,103 @@
name: Limit pull requests
description: >
Limit the number of open pull requests to the repository created by a user
author: ZhongRuoyu (from Homebrew repository)
branding:
icon: alert-triangle
color: yellow
inputs:
token:
description: GitHub token
required: false
default: ${{ github.token }}
except-users:
description: The users exempted from the limit, one per line
required: false
# https://docs.github.com/en/graphql/reference/enums#commentauthorassociation
except-author-associations:
description: The author associations exempted from the limit, one per line
required: false
comment-limit:
description: >
Post the comment when the user's number of open pull requests exceeds this
number and `comment` is not empty
required: true
default: "10"
comment:
description: The comment to post when the limit is reached
required: false
close-limit:
description: >
Close the pull request when the user's number of open pull requests
exceeds this number and `close` is set to `true`
required: true
default: "50"
close:
description: Whether to close the pull request when the limit is reached
required: true
default: "false"
runs:
using: composite
steps:
- name: Check the number of pull requests
id: count-pull-requests
run: |
# If the user is exempted, assume they have no pull requests.
if grep -Fiqx '${{ github.actor }}' <<<"$EXCEPT_USERS"; then
echo "::notice::@${{ github.actor }} is exempted from the limit."
echo "count=0" >>"$GITHUB_OUTPUT"
exit 0
fi
if grep -Fiqx '${{ github.event.pull_request.author_association }}' <<<"$EXCEPT_AUTHOR_ASSOCIATIONS"; then
echo "::notice::@{{ github.actor }} is a ${{ github.event.pull_request.author_association }} exempted from the limit."
echo "count=0" >>"$GITHUB_OUTPUT"
exit 0
fi
count="$(
gh api \
--method GET \
--header 'Accept: application/vnd.github+json' \
--header 'X-GitHub-Api-Version: 2022-11-28' \
--field state=open \
--paginate \
'/repos/{owner}/{repo}/pulls' |
jq \
--raw-output \
--arg USER '${{ github.actor }}' \
'map(select(.user.login == $USER)) | length'
)"
echo "::notice::@${{ github.actor }} has $count open pull request(s)."
echo "count=$count" >>"$GITHUB_OUTPUT"
env:
GH_REPO: ${{ github.repository }}
GH_TOKEN: ${{ inputs.token }}
EXCEPT_USERS: ${{ inputs.except-users }}
EXCEPT_AUTHOR_ASSOCIATIONS: ${{ inputs.except-author-associations }}
shell: bash
- name: Comment on pull request
if: >
fromJSON(steps.count-pull-requests.outputs.count) > fromJSON(inputs.comment-limit) &&
inputs.comment != ''
run: |
gh pr comment '${{ github.event.pull_request.number }}' \
--body="${COMMENT_BODY}"
env:
GH_REPO: ${{ github.repository }}
GH_TOKEN: ${{ inputs.token }}
COMMENT_BODY: ${{ inputs.comment }}
shell: bash
- name: Close pull request
if: >
fromJSON(steps.count-pull-requests.outputs.count) > fromJSON(inputs.close-limit) &&
inputs.close == 'true'
run: |
gh pr close '${{ github.event.pull_request.number }}'
env:
GH_REPO: ${{ github.repository }}
GH_TOKEN: ${{ inputs.token }}
shell: bash

29
.github/workflows/pr-check.yml vendored Normal file
View File

@@ -0,0 +1,29 @@
name: PR Check
on: pull_request_target
env:
GH_REPO: ${{ github.repository }}
GH_NO_UPDATE_NOTIFIER: 1
GH_PROMPT_DISABLED: 1
permissions:
contents: read
issues: write
pull-requests: write
statuses: write
jobs:
limit-pull-requests:
if: always() && github.repository_owner == 'naturalcrit'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name : Run limit-pull-requests action
uses: ./.github/actions/limit-pull-requests
with:
except-users: |
dependabot
comment-limit: 3
comment: |
Hi, thanks for your contribution to the Homebrewery! You already have >=3 open pull requests. Consider completing some of your existing PRs before opening new ones. Thanks!
close-limit: 5
close: false

View File

@@ -17,11 +17,24 @@
&::-webkit-scrollbar { &::-webkit-scrollbar {
width: 20px; width: 20px;
&:horizontal{
height: 20px;
width:auto;
}
&-thumb {
background: linear-gradient(90deg, #d3c1af 15px, #00000000 15px);
&:horizontal{
background: linear-gradient(0deg, #d3c1af 15px, #00000000 15px);
}
}
&-corner {
visibility: hidden;
}
} }
&::-webkit-scrollbar-thumb {
width:20px;
background: linear-gradient(90deg, #d3c1af 15px, #00000000 15px);
}
} }
.pane { position : relative; } .pane { position : relative; }

View File

@@ -81,7 +81,7 @@ const Editor = createClass({
updateEditorSize : function() { updateEditorSize : function() {
if(this.refs.codeEditor) { if(this.refs.codeEditor) {
let paneHeight = this.refs.main.parentNode.clientHeight; let paneHeight = this.refs.main.parentNode.clientHeight;
paneHeight -= SNIPPETBAR_HEIGHT + 1; paneHeight -= SNIPPETBAR_HEIGHT;
this.refs.codeEditor.codeMirror.setSize(null, paneHeight); this.refs.codeEditor.codeMirror.setSize(null, paneHeight);
} }
}, },

View File

@@ -130,6 +130,8 @@
height : 1.2em; height : 1.2em;
margin-right : 8px; margin-right : 8px;
font-size : 1.2em; font-size : 1.2em;
min-width: 25px;
text-align: center;
& ~ i { & ~ i {
margin-right : 0; margin-right : 0;
margin-left : 5px; margin-left : 5px;
@@ -138,7 +140,7 @@
&.font { &.font {
height : auto; height : auto;
&::before { &::before {
font-size : 1.4em; font-size : 1em;
content : 'ABC'; content : 'ABC';
} }

View File

@@ -78,7 +78,7 @@ const Homebrew = createClass({
<Route path='/archive' element={<WithRoute el={ArchivePage}/>}/> <Route path='/archive' element={<WithRoute el={ArchivePage}/>}/>
<Route path='/changelog' element={<WithRoute el={SharePage} brew={this.props.brew} />} /> <Route path='/changelog' element={<WithRoute el={SharePage} brew={this.props.brew} />} />
<Route path='/faq' element={<WithRoute el={SharePage} brew={this.props.brew} />} /> <Route path='/faq' element={<WithRoute el={SharePage} brew={this.props.brew} />} />
<Route path='/account' element={<WithRoute el={AccountPage} brew={this.props.brew} uiItems={this.props.brew.uiItems} />} /> <Route path='/account' element={<WithRoute el={AccountPage} brew={this.props.brew} accountDetails={this.props.brew.accountDetails} />} />
<Route path='/legacy' element={<WithRoute el={HomePage} brew={this.props.brew} />} /> <Route path='/legacy' element={<WithRoute el={HomePage} brew={this.props.brew} />} />
<Route path='/error' element={<WithRoute el={ErrorPage} brew={this.props.brew} />} /> <Route path='/error' element={<WithRoute el={ErrorPage} brew={this.props.brew} />} />
<Route path='/' element={<WithRoute el={HomePage} brew={this.props.brew} />} /> <Route path='/' element={<WithRoute el={HomePage} brew={this.props.brew} />} />

View File

@@ -18,10 +18,19 @@
&::-webkit-scrollbar { &::-webkit-scrollbar {
width: 20px; width: 20px;
} &:horizontal{
&::-webkit-scrollbar-thumb { height: 20px;
width:20px; width:auto;
background: linear-gradient(90deg, #d3c1af 15px, #00000000 15px); }
&-thumb {
background: linear-gradient(90deg, #d3c1af 15px, #00000000 15px);
&:horizontal{
background: linear-gradient(0deg, #d3c1af 15px, #00000000 15px);
}
}
&-corner {
visibility: hidden;
}
} }
} }
} }

View File

@@ -1,102 +1,82 @@
const React = require('react'); const React = require('react');
const createClass = require('create-react-class');
const _ = require('lodash');
const cx = require('classnames');
const moment = require('moment'); const moment = require('moment');
const UIPage = require('../basePages/uiPage/uiPage.jsx'); const UIPage = require('../basePages/uiPage/uiPage.jsx');
const Nav = require('naturalcrit/nav/nav.jsx');
const Navbar = require('../../navbar/navbar.jsx');
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
const Account = require('../../navbar/account.navitem.jsx');
const NewBrew = require('../../navbar/newbrew.navitem.jsx');
const HelpNavItem = require('../../navbar/help.navitem.jsx');
const NaturalCritIcon = require('naturalcrit/svg/naturalcrit.svg.jsx'); const NaturalCritIcon = require('naturalcrit/svg/naturalcrit.svg.jsx');
let SAVEKEY = ''; let SAVEKEY = '';
const AccountPage = createClass({ const AccountPage = (props)=>{
displayName : 'AccountPage', // destructure props and set state for save location
getDefaultProps : function() { const { accountDetails, brew } = props;
return { const [saveLocation, setSaveLocation] = React.useState('');
brew : {},
uiItems : {} // initialize save location from local storage based on user id
}; React.useEffect(()=>{
}, if(!saveLocation && accountDetails.username) {
getInitialState : function() { SAVEKEY = `HOMEBREWERY-DEFAULT-SAVE-LOCATION-${accountDetails.username}`;
return { // if no SAVEKEY in local storage, default save location to Google Drive if user has Google account.
uiItems : this.props.uiItems let saveLocation = window.localStorage.getItem(SAVEKEY);
}; saveLocation = saveLocation ?? (accountDetails.googleId ? 'GOOGLE-DRIVE' : 'HOMEBREWERY');
}, setActiveSaveLocation(saveLocation);
componentDidMount : function(){
if(!this.state.saveLocation && this.props.uiItems.username) {
SAVEKEY = `HOMEBREWERY-DEFAULT-SAVE-LOCATION-${this.props.uiItems.username}`;
let saveLocation = window.localStorage.getItem(SAVEKEY);
saveLocation = saveLocation ?? (this.state.uiItems.googleId ? 'GOOGLE-DRIVE' : 'HOMEBREWERY');
this.makeActive(saveLocation);
} }
}, }, []);
makeActive : function(newSelection){ const setActiveSaveLocation = (newSelection)=>{
if(this.state.saveLocation == newSelection) return; if(saveLocation === newSelection) return;
window.localStorage.setItem(SAVEKEY, newSelection); window.localStorage.setItem(SAVEKEY, newSelection);
this.setState({ setSaveLocation(newSelection);
saveLocation : newSelection };
});
},
renderButton : function(name, key, shouldRender=true){ // todo: should this be a set of radio buttons (well styled) since it's either/or choice?
if(!shouldRender) return; const renderSaveLocationButton = (name, key, shouldRender = true)=>{
return <button className={this.state.saveLocation==key ? 'active' : ''} onClick={()=>{this.makeActive(key);}}>{name}</button>; if(!shouldRender) return null;
}, return (
<button className={saveLocation === key ? 'active' : ''} onClick={()=>{setActiveSaveLocation(key);}}>
{name}
</button>
);
};
renderNavItems : function() { // render the entirety of the account page content
return <Navbar> const renderAccountPage = ()=>{
<Nav.section> return (
<NewBrew /> <>
<HelpNavItem /> <div className='dataGroup'>
<RecentNavItem /> <h1>Account Information <i className='fas fa-user'></i></h1>
<Account /> <p><strong>Username: </strong>{accountDetails.username || 'No user currently logged in'}</p>
</Nav.section> <p><strong>Last Login: </strong>{moment(accountDetails.issued).format('dddd, MMMM Do YYYY, h:mm:ss a ZZ') || '-'}</p>
</Navbar>; </div>
}, <div className='dataGroup'>
<h3>Homebrewery Information <NaturalCritIcon /></h3>
<p><strong>Brews on Homebrewery: </strong>{accountDetails.mongoCount}</p>
</div>
<div className='dataGroup'>
<h3>Google Information <i className='fab fa-google-drive'></i></h3>
<p><strong>Linked to Google: </strong>{accountDetails.googleId ? 'YES' : 'NO'}</p>
{accountDetails.googleId && (
<p>
<strong>Brews on Google Drive: </strong>{accountDetails.googleCount ?? (
<>
Unable to retrieve files - <a href='https://github.com/naturalcrit/homebrewery/discussions/1580'>follow these steps to renew your Google credentials.</a>
</>
)}
</p>
)}
</div>
<div className='dataGroup'>
<h4>Default Save Location</h4>
{renderSaveLocationButton('Homebrewery', 'HOMEBREWERY')}
{renderSaveLocationButton('Google Drive', 'GOOGLE-DRIVE', accountDetails.googleId)}
</div>
</>
);
};
renderUiItems : function() { // return the account page inside the base layout wrapper (with navbar etc).
return <> return (
<div className='dataGroup'> <UIPage brew={brew}>
<h1>Account Information <i className='fas fa-user'></i></h1> {renderAccountPage()}
<p><strong>Username: </strong> {this.props.uiItems.username || 'No user currently logged in'}</p> </UIPage>);
<p><strong>Last Login: </strong> {moment(this.props.uiItems.issued).format('dddd, MMMM Do YYYY, h:mm:ss a ZZ') || '-'}</p> };
</div>
<div className='dataGroup'>
<h3>Homebrewery Information <NaturalCritIcon /></h3>
<p><strong>Brews on Homebrewery: </strong> {this.props.uiItems.mongoCount}</p>
</div>
<div className='dataGroup'>
<h3>Google Information <i className='fab fa-google-drive'></i></h3>
<p><strong>Linked to Google: </strong> {this.props.uiItems.googleId ? 'YES' : 'NO'}</p>
{this.props.uiItems.googleId &&
<p>
<strong>Brews on Google Drive: </strong> {this.props.uiItems.googleCount ?? <>Unable to retrieve files - <a href='https://github.com/naturalcrit/homebrewery/discussions/1580'>follow these steps to renew your Google credentials.</a></>}
</p>
}
</div>
<div className='dataGroup'>
<h4>Default Save Location</h4>
{this.renderButton('Homebrewery', 'HOMEBREWERY')}
{this.renderButton('Google Drive', 'GOOGLE-DRIVE', this.state.uiItems.googleId)}
</div>
</>;
},
render : function(){
return <UIPage brew={this.props.brew}>
{this.renderUiItems()}
</UIPage>;
}
});
module.exports = AccountPage; module.exports = AccountPage;

View File

@@ -1,41 +1,25 @@
require('./errorPage.less'); require('./errorPage.less');
const React = require('react'); const React = require('react');
const createClass = require('create-react-class'); const UIPage = require('../basePages/uiPage/uiPage.jsx');
const _ = require('lodash'); const Markdown = require('../../../../shared/naturalcrit/markdown.js');
const cx = require('classnames');
const UIPage = require('../basePages/uiPage/uiPage.jsx');
const Markdown = require('../../../../shared/naturalcrit/markdown.js');
const ErrorIndex = require('./errors/errorIndex.js'); const ErrorIndex = require('./errors/errorIndex.js');
const ErrorPage = createClass({ const ErrorPage = ({ brew })=>{
displayName : 'ErrorPage', // Retrieving the error text based on the brew's error code from ErrorIndex
const errorText = ErrorIndex({ brew })[brew.HBErrorCode.toString()] || '';
getDefaultProps : function() { return (
return { <UIPage brew={{ title: 'Crit Fail!' }}>
ver : '0.0.0',
errorId : '',
text : '# Oops \n We could not find a brew with that id. **Sorry!**',
error : {}
};
},
render : function(){
const errorText = ErrorIndex(this.props)[this.props.brew.HBErrorCode.toString()] || '';
return <UIPage brew={{ title: 'Crit Fail!' }}>
<div className='dataGroup'> <div className='dataGroup'>
<div className='errorTitle'> <div className='errorTitle'>
<h1>{`Error ${this.props.brew.status || '000'}`}</h1> <h1>{`Error ${brew?.status || '000'}`}</h1>
<h4>{this.props.brew.text || 'No error text'}</h4> <h4>{brew?.text || 'No error text'}</h4>
</div> </div>
<hr /> <hr />
<div dangerouslySetInnerHTML={{ __html: Markdown.render(errorText) }} /> <div dangerouslySetInnerHTML={{ __html: Markdown.render(errorText) }} />
</div> </div>
</UIPage>; </UIPage>
} );
}); };
module.exports = ErrorPage; module.exports = ErrorPage;

View File

@@ -73,9 +73,11 @@ const errorIndex = (props)=>{
**Properties** tab, and adding your username to the "invited authors" list. You can **Properties** tab, and adding your username to the "invited authors" list. You can
then try to access this document again. then try to access this document again.
:
**Brew Title:** ${props.brew.brewTitle || 'Unable to show title'} **Brew Title:** ${props.brew.brewTitle || 'Unable to show title'}
**Current Authors:** ${props.brew.authors?.map((author)=>{return `${author}`;}).join(', ') || 'Unable to list authors'} **Current Authors:** ${props.brew.authors?.map((author)=>{return `[${author}](/user/${author})`;}).join(', ') || 'Unable to list authors'}
[Click here to be redirected to the brew's share page.](/share/${props.brew.shareId})`, [Click here to be redirected to the brew's share page.](/share/${props.brew.shareId})`,
@@ -86,9 +88,14 @@ const errorIndex = (props)=>{
You must be logged in to one of the accounts listed as an author of this brew. You must be logged in to one of the accounts listed as an author of this brew.
User is not logged in. Please log in [here](${loginUrl}). User is not logged in. Please log in [here](${loginUrl}).
:
**Brew Title:** ${props.brew.brewTitle || 'Unable to show title'} **Brew Title:** ${props.brew.brewTitle || 'Unable to show title'}
**Current Authors:** ${props.brew.authors?.map((author)=>{return `${author}`;}).join(', ') || 'Unable to list authors'}`, **Current Authors:** ${props.brew.authors?.map((author)=>{return `[${author}](/user/${author})`;}).join(', ') || 'Unable to list authors'}
[Click here to be redirected to the brew's share page.](/share/${props.brew.shareId})`,
// Brew load error // Brew load error
'05' : dedent` '05' : dedent`
@@ -97,6 +104,8 @@ const errorIndex = (props)=>{
The server could not locate the Homebrewery document. It was likely deleted by The server could not locate the Homebrewery document. It was likely deleted by
its owner. its owner.
:
**Requested access:** ${props.brew.accessType} **Requested access:** ${props.brew.accessType}
**Brew ID:** ${props.brew.brewId}`, **Brew ID:** ${props.brew.brewId}`,
@@ -113,6 +122,8 @@ const errorIndex = (props)=>{
An error occurred while attempting to remove the Homebrewery document. An error occurred while attempting to remove the Homebrewery document.
:
**Brew ID:** ${props.brew.brewId}`, **Brew ID:** ${props.brew.brewId}`,
// Author delete error // Author delete error
@@ -121,6 +132,8 @@ const errorIndex = (props)=>{
An error occurred while attempting to remove the user from the Homebrewery document author list! An error occurred while attempting to remove the user from the Homebrewery document author list!
:
**Brew ID:** ${props.brew.brewId}`, **Brew ID:** ${props.brew.brewId}`,
// Brew locked by Administrators error // Brew locked by Administrators error
@@ -129,6 +142,8 @@ const errorIndex = (props)=>{
Please contact the Administrators to unlock this document. Please contact the Administrators to unlock this document.
:
**Brew ID:** ${props.brew.brewId} **Brew ID:** ${props.brew.brewId}
**Brew Title:** ${props.brew.brewTitle}`, **Brew Title:** ${props.brew.brewTitle}`,

View File

@@ -47,6 +47,19 @@ const SharePage = createClass({
this.props.brew.shareId; this.props.brew.shareId;
}, },
renderEditLink : function(){
if(!this.props.brew.editId) return;
let editLink = this.props.brew.editId;
if(this.props.brew.googleId && !this.props.brew.stubbed) {
editLink = this.props.brew.googleId + editLink;
}
return <Nav.item color='orange' href={`/edit/${editLink}`}>
edit
</Nav.item>;
},
render : function(){ render : function(){
return <div className='sharePage sitePage'> return <div className='sharePage sitePage'>
<Meta name='robots' content='noindex, nofollow' /> <Meta name='robots' content='noindex, nofollow' />
@@ -64,13 +77,14 @@ const SharePage = createClass({
<Nav.item color='red' icon='fas fa-code'> <Nav.item color='red' icon='fas fa-code'>
source source
</Nav.item> </Nav.item>
<Nav.item color='blue' href={`/source/${this.processShareId()}`}> <Nav.item color='blue' icon='fas fa-eye' href={`/source/${this.processShareId()}`}>
view view
</Nav.item> </Nav.item>
<Nav.item color='blue' href={`/download/${this.processShareId()}`}> {this.renderEditLink()}
<Nav.item color='blue' icon='fas fa-download' href={`/download/${this.processShareId()}`}>
download download
</Nav.item> </Nav.item>
<Nav.item color='blue' href={`/new/${this.processShareId()}`}> <Nav.item color='blue' icon='fas fa-clone' href={`/new/${this.processShareId()}`}>
clone to new clone to new
</Nav.item> </Nav.item>
</Nav.dropdown> </Nav.dropdown>

644
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -27,10 +27,10 @@
"test:dev": "jest --verbose --watch", "test:dev": "jest --verbose --watch",
"test:basic": "jest tests/markdown/basic.test.js --verbose", "test:basic": "jest tests/markdown/basic.test.js --verbose",
"test:variables": "jest tests/markdown/variables.test.js --verbose", "test:variables": "jest tests/markdown/variables.test.js --verbose",
"test:mustache-syntax": "jest '.*(mustache-syntax).*' --verbose --noStackTrace", "test:mustache-syntax": "jest \".*(mustache-syntax).*\" --verbose --noStackTrace",
"test:mustache-syntax:inline": "jest '.*(mustache-syntax).*' -t '^Inline:.*' --verbose --noStackTrace", "test:mustache-syntax:inline": "jest \".*(mustache-syntax).*\" -t '^Inline:.*' --verbose --noStackTrace",
"test:mustache-syntax:block": "jest '.*(mustache-syntax).*' -t '^Block:.*' --verbose --noStackTrace", "test:mustache-syntax:block": "jest \".*(mustache-syntax).*\" -t '^Block:.*' --verbose --noStackTrace",
"test:mustache-syntax:injection": "jest '.*(mustache-syntax).*' -t '^Injection:.*' --verbose --noStackTrace", "test:mustache-syntax:injection": "jest \".*(mustache-syntax).*\" -t '^Injection:.*' --verbose --noStackTrace",
"test:definition-lists": "jest tests/markdown/definition-lists.test.js --verbose --noStackTrace", "test:definition-lists": "jest tests/markdown/definition-lists.test.js --verbose --noStackTrace",
"test:route": "jest tests/routes/static-pages.test.js --verbose", "test:route": "jest tests/routes/static-pages.test.js --verbose",
"phb": "node scripts/phb.js", "phb": "node scripts/phb.js",
@@ -81,11 +81,11 @@
] ]
}, },
"dependencies": { "dependencies": {
"@babel/core": "^7.24.3", "@babel/core": "^7.24.5",
"@babel/plugin-transform-runtime": "^7.24.3", "@babel/plugin-transform-runtime": "^7.24.3",
"@babel/preset-env": "^7.24.3", "@babel/preset-env": "^7.24.5",
"@babel/preset-react": "^7.24.1", "@babel/preset-react": "^7.24.1",
"@googleapis/drive": "^8.7.0", "@googleapis/drive": "^8.8.0",
"body-parser": "^1.20.2", "body-parser": "^1.20.2",
"classnames": "^2.5.1", "classnames": "^2.5.1",
"codemirror": "^5.65.6", "codemirror": "^5.65.6",
@@ -102,25 +102,26 @@
"less": "^3.13.1", "less": "^3.13.1",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"marked": "11.2.0", "marked": "11.2.0",
"marked-emoji": "^1.4.0",
"marked-extended-tables": "^1.0.8", "marked-extended-tables": "^1.0.8",
"marked-gfm-heading-id": "^3.1.3", "marked-gfm-heading-id": "^3.1.3",
"marked-smartypants-lite": "^1.0.2", "marked-smartypants-lite": "^1.0.2",
"markedLegacy": "npm:marked@^0.3.19", "markedLegacy": "npm:marked@^0.3.19",
"moment": "^2.30.1", "moment": "^2.30.1",
"mongoose": "^8.2.3", "mongoose": "^8.3.3",
"nanoid": "3.3.4", "nanoid": "3.3.4",
"nconf": "^0.12.1", "nconf": "^0.12.1",
"react": "^18.2.0", "react": "^18.3.1",
"react-dom": "^18.2.0", "react-dom": "^18.3.1",
"react-frame-component": "^4.1.3", "react-frame-component": "^4.1.3",
"react-router-dom": "6.22.3", "react-router-dom": "6.23.0",
"sanitize-filename": "1.6.3", "sanitize-filename": "1.6.3",
"superagent": "^8.1.2", "superagent": "^9.0.2",
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git" "vitreum": "git+https://git@github.com/calculuschild/vitreum.git"
}, },
"devDependencies": { "devDependencies": {
"eslint": "^8.57.0", "eslint": "^8.57.0",
"eslint-plugin-jest": "^27.9.0", "eslint-plugin-jest": "^28.5.0",
"eslint-plugin-react": "^7.34.1", "eslint-plugin-react": "^7.34.1",
"jest": "^29.7.0", "jest": "^29.7.0",
"jest-expect-message": "^1.1.3", "jest-expect-message": "^1.1.3",
@@ -129,6 +130,6 @@
"stylelint-config-recess-order": "^4.6.0", "stylelint-config-recess-order": "^4.6.0",
"stylelint-config-recommended": "^13.0.0", "stylelint-config-recommended": "^13.0.0",
"stylelint-stylistic": "^0.4.3", "stylelint-stylistic": "^0.4.3",
"supertest": "^6.3.4" "supertest": "^7.0.0"
} }
} }

View File

@@ -7,6 +7,14 @@ DB.connect(config).then(()=>{
// before launching server // before launching server
const PORT = process.env.PORT || config.get('web_port') || 8000; const PORT = process.env.PORT || config.get('web_port') || 8000;
server.app.listen(PORT, ()=>{ server.app.listen(PORT, ()=>{
console.log(`server on port: ${PORT}`); const reset = '\x1b[0m'; // Reset to default style
const bright = '\x1b[1m'; // Bright (bold) style
const cyan = '\x1b[36m'; // Cyan color
const underline = '\x1b[4m'; // Underlined style
console.log(`\n\tserver started at: ${new Date().toLocaleString()}`);
console.log(`\tserver on port: ${PORT}`);
console.log(`\t${bright + cyan}Open in browser: ${reset}${underline + bright + cyan}http://localhost:${PORT}${reset}\n\n`)
}); });
}); });

View File

@@ -23,7 +23,7 @@ const { splitTextStyleAndMetadata } = require('../shared/helpers.js');
const sanitizeBrew = (brew, accessType)=>{ const sanitizeBrew = (brew, accessType)=>{
brew._id = undefined; brew._id = undefined;
brew.__v = undefined; brew.__v = undefined;
if(accessType !== 'edit'){ if(accessType !== 'edit' && accessType !== 'shareAuthor') {
brew.editId = undefined; brew.editId = undefined;
} }
return brew; return brew;
@@ -308,7 +308,6 @@ app.get('/new/:id', asyncHandler(getBrew('share')), (req, res, next)=>{
//Share Page //Share Page
app.get('/share/:id', asyncHandler(getBrew('share')), asyncHandler(async (req, res, next)=>{ app.get('/share/:id', asyncHandler(getBrew('share')), asyncHandler(async (req, res, next)=>{
const { brew } = req; const { brew } = req;
req.ogMeta = { ...defaultMetaTags, req.ogMeta = { ...defaultMetaTags,
title : req.brew.title || 'Untitled Brew', title : req.brew.title || 'Untitled Brew',
description : req.brew.description || 'No description.', description : req.brew.description || 'No description.',
@@ -327,7 +326,8 @@ app.get('/share/:id', asyncHandler(getBrew('share')), asyncHandler(async (req, r
await HomebrewModel.increaseView({ shareId: brew.shareId }); await HomebrewModel.increaseView({ shareId: brew.shareId });
} }
}; };
sanitizeBrew(req.brew, 'share');
brew.authors.includes(req.account?.username) ? sanitizeBrew(req.brew, 'shareAuthor') : sanitizeBrew(req.brew, 'share');
splitTextStyleAndMetadata(req.brew); splitTextStyleAndMetadata(req.brew);
return next(); return next();
})); }));
@@ -373,7 +373,7 @@ app.get('/account', asyncHandler(async (req, res, next)=>{
console.log(err); console.log(err);
}); });
data.uiItems = { data.accountDetails = {
username : req.account.username, username : req.account.username,
issued : req.account.issued, issued : req.account.issued,
googleId : Boolean(req.account.googleId), googleId : Boolean(req.account.googleId),

View File

@@ -7,7 +7,9 @@ const config = require('./config.js');
let serviceAuth; let serviceAuth;
if(!config.get('service_account')){ if(!config.get('service_account')){
console.log('No Google Service Account in config files - Google Drive integration will not be available.'); const reset = '\x1b[0m'; // Reset to default style
const yellow = '\x1b[33m'; // yellow color
console.warn(`\n${yellow}No Google Service Account in config files - Google Drive integration will not be available.${reset}`);
} else { } else {
const keys = typeof(config.get('service_account')) == 'string' ? const keys = typeof(config.get('service_account')) == 'string' ?
JSON.parse(config.get('service_account')) : JSON.parse(config.get('service_account')) :
@@ -18,7 +20,7 @@ if(!config.get('service_account')){
serviceAuth.scopes = ['https://www.googleapis.com/auth/drive']; serviceAuth.scopes = ['https://www.googleapis.com/auth/drive'];
} catch (err) { } catch (err) {
console.warn(err); console.warn(err);
console.log('Please make sure the Google Service Account is set up properly in your config files.'); console.warn('Please make sure the Google Service Account is set up properly in your config files.');
} }
} }

View File

@@ -83,9 +83,9 @@ const api = {
if(accessType === 'edit' && (authorsExist && !(isAuthor || isInvited))) { if(accessType === 'edit' && (authorsExist && !(isAuthor || isInvited))) {
const accessError = { name: 'Access Error', status: 401 }; const accessError = { name: 'Access Error', status: 401 };
if(req.account){ if(req.account){
throw { ...accessError, message: 'User is not an Author', HBErrorCode: '03', authors: stub.authors, brewTitle: stub.title, shareId: stub.shareId }; throw { ...accessError, message: 'User is not an Author', HBErrorCode: '03', authors: stub.authors, brewTitle: stub.title, shareId: stub.shareId};
} }
throw { ...accessError, message: 'User is not logged in', HBErrorCode: '04', authors: stub.authors, brewTitle: stub.title }; throw { ...accessError, message: 'User is not logged in', HBErrorCode: '04', authors: stub.authors, brewTitle: stub.title, shareId: stub.shareId};
} }
// If after all of that we still don't have a brew, throw an exception // If after all of that we still don't have a brew, throw an exception

View File

@@ -24,18 +24,13 @@
animation-duration: 0.4s; animation-duration: 0.4s;
} }
.CodeMirror-sizer {
padding-right: 0 !important;
//this setting must be !important, because CodeMirror sets it inline. Achieves overlay scrollbar
}
.CodeMirror-vscrollbar { .CodeMirror-vscrollbar {
&::-webkit-scrollbar { &::-webkit-scrollbar {
width: 20px; width: 20px;
} }
&::-webkit-scrollbar-thumb { &::-webkit-scrollbar-thumb {
width: 20px; width: 20px;
background: linear-gradient(90deg, #85858599 15px, #80808000 15px); background: linear-gradient(90deg, #858585 15px, #808080 15px);
} }
} }

View File

@@ -50,7 +50,7 @@ renderer.html = function (html) {
return html; return html;
}; };
// Don't wrap {{ Divs or {{ empty Spans in <p> tags // Don't wrap {{ Spans alone on a line, or {{ Divs in <p> tags
renderer.paragraph = function(text){ renderer.paragraph = function(text){
let match; let match;
if(text.startsWith('<div') || text.startsWith('</div')) if(text.startsWith('<div') || text.startsWith('</div'))
@@ -99,13 +99,13 @@ const mustacheSpans = {
if(match) { if(match) {
//Find closing delimiter //Find closing delimiter
let blockCount = 0; let blockCount = 0;
let tags = ''; let tags = {};
let endTags = 0; let endTags = 0;
let endToken = 0; let endToken = 0;
let delim; let delim;
while (delim = inlineRegex.exec(match[0])) { while (delim = inlineRegex.exec(match[0])) {
if(!tags) { if(_.isEmpty(tags)) {
tags = `${processStyleTags(delim[0].substring(2))}`; tags = processStyleTags(delim[0].substring(2));
endTags = delim[0].length; endTags = delim[0].length;
} }
if(delim[0].startsWith('{{')) { if(delim[0].startsWith('{{')) {
@@ -134,7 +134,14 @@ const mustacheSpans = {
} }
}, },
renderer(token) { renderer(token) {
return `<span class="inline-block${token.tags}>${this.parser.parseInline(token.tokens)}</span>`; // parseInline to turn child tokens into HTML const tags = token.tags;
tags.classes = ['inline-block', tags.classes].join(' ').trim();
return `<span` +
`${tags.classes ? ` class="${tags.classes}"` : ''}` +
`${tags.id ? ` id="${tags.id}"` : ''}` +
`${tags.styles ? ` style="${tags.styles}"` : ''}` +
`${tags.attributes ? ` ${Object.entries(tags.attributes).map(([key, value]) => `${key}="${value}"`).join(' ')}` : ''}` +
`>${this.parser.parseInline(token.tokens)}</span>`; // parseInline to turn child tokens into HTML
} }
}; };
@@ -149,13 +156,13 @@ const mustacheDivs = {
if(match) { if(match) {
//Find closing delimiter //Find closing delimiter
let blockCount = 0; let blockCount = 0;
let tags = ''; let tags = {};
let endTags = 0; let endTags = 0;
let endToken = 0; let endToken = 0;
let delim; let delim;
while (delim = blockRegex.exec(match[0])?.[0].trim()) { while (delim = blockRegex.exec(match[0])?.[0].trim()) {
if(!tags) { if(_.isEmpty(tags)) {
tags = `${processStyleTags(delim.substring(2))}`; tags = processStyleTags(delim.substring(2));
endTags = delim.length + src.indexOf(delim); endTags = delim.length + src.indexOf(delim);
} }
if(delim.startsWith('{{')) { if(delim.startsWith('{{')) {
@@ -183,7 +190,14 @@ const mustacheDivs = {
} }
}, },
renderer(token) { renderer(token) {
return `<div class="block${token.tags}>${this.parser.parse(token.tokens)}</div>`; // parseInline to turn child tokens into HTML const tags = token.tags;
tags.classes = ['block', tags.classes].join(' ').trim();
return `<div` +
`${tags.classes ? ` class="${tags.classes}"` : ''}` +
`${tags.id ? ` id="${tags.id}"` : ''}` +
`${tags.styles ? ` style="${tags.styles}"` : ''}` +
`${tags.attributes ? ` ${Object.entries(tags.attributes).map(([key, value]) => `${key}="${value}"`).join(' ')}` : ''}` +
`>${this.parser.parse(token.tokens)}</div>`; // parse to turn child tokens into HTML
} }
}; };
@@ -199,23 +213,39 @@ const mustacheInjectInline = {
if(!lastToken || lastToken.type == 'mustacheInjectInline') if(!lastToken || lastToken.type == 'mustacheInjectInline')
return false; return false;
const tags = `${processStyleTags(match[1])}`; const tags = processStyleTags(match[1]);
lastToken.originalType = lastToken.type; lastToken.originalType = lastToken.type;
lastToken.type = 'mustacheInjectInline'; lastToken.type = 'mustacheInjectInline';
lastToken.tags = tags; lastToken.injectedTags = tags;
return { return {
type : 'text', // Should match "name" above type : 'mustacheInjectInline', // Should match "name" above
raw : match[0], // Text to consume from the source raw : match[0], // Text to consume from the source
text : '' text : ''
}; };
} }
}, },
renderer(token) { renderer(token) {
if(!token.originalType){
return;
}
token.type = token.originalType; token.type = token.originalType;
const text = this.parser.parseInline([token]); const text = this.parser.parseInline([token]);
const openingTag = /(<[^\s<>]+)([^\n<>]*>.*)/s.exec(text); const originalTags = extractHTMLStyleTags(text);
const injectedTags = token.injectedTags;
const tags = {
id : injectedTags.id || originalTags.id || null,
classes : [originalTags.classes, injectedTags.classes].join(' ').trim() || null,
styles : [originalTags.styles, injectedTags.styles].join(' ').trim() || null,
attributes : Object.assign(originalTags.attributes ?? {}, injectedTags.attributes ?? {})
};
const openingTag = /(<[^\s<>]+)[^\n<>]*(>.*)/s.exec(text);
if(openingTag) { if(openingTag) {
return `${openingTag[1]} class="${token.tags}${openingTag[2]}`; return `${openingTag[1]}` +
`${tags.classes ? ` class="${tags.classes}"` : ''}` +
`${tags.id ? ` id="${tags.id}"` : ''}` +
`${tags.styles ? ` style="${tags.styles}"` : ''}` +
`${!_.isEmpty(tags.attributes) ? ` ${Object.entries(tags.attributes).map(([key, value]) => `${key}="${value}"`).join(' ')}` : ''}` +
`${openingTag[2]}`; // parse to turn child tokens into HTML
} }
return text; return text;
} }
@@ -235,7 +265,7 @@ const mustacheInjectBlock = {
return false; return false;
lastToken.originalType = 'mustacheInjectBlock'; lastToken.originalType = 'mustacheInjectBlock';
lastToken.tags = `${processStyleTags(match[1])}`; lastToken.injectedTags = processStyleTags(match[1]);
return { return {
type : 'mustacheInjectBlock', // Should match "name" above type : 'mustacheInjectBlock', // Should match "name" above
raw : match[0], // Text to consume from the source raw : match[0], // Text to consume from the source
@@ -249,9 +279,22 @@ const mustacheInjectBlock = {
} }
token.type = token.originalType; token.type = token.originalType;
const text = this.parser.parse([token]); const text = this.parser.parse([token]);
const openingTag = /(<[^\s<>]+)([^\n<>]*>.*)/s.exec(text); const originalTags = extractHTMLStyleTags(text);
const injectedTags = token.injectedTags;
const tags = {
id : injectedTags.id || originalTags.id || null,
classes : [originalTags.classes, injectedTags.classes].join(' ').trim() || null,
styles : [originalTags.styles, injectedTags.styles].join(' ').trim() || null,
attributes : Object.assign(originalTags.attributes ?? {}, injectedTags.attributes ?? {})
};
const openingTag = /(<[^\s<>]+)[^\n<>]*(>.*)/s.exec(text);
if(openingTag) { if(openingTag) {
return `${openingTag[1]} class="${token.tags}${openingTag[2]}`; return `${openingTag[1]}` +
`${tags.classes ? ` class="${tags.classes}"` : ''}` +
`${tags.id ? ` id="${tags.id}"` : ''}` +
`${tags.styles ? ` style="${tags.styles}"` : ''}` +
`${!_.isEmpty(tags.attributes) ? ` ${Object.entries(tags.attributes).map(([key, value]) => `${key}="${value}"`).join(' ')}` : ''}` +
`${openingTag[2]}`; // parse to turn child tokens into HTML
} }
return text; return text;
} }
@@ -294,10 +337,10 @@ const superSubScripts = {
} }
}; };
const definitionListsInline = { const definitionListsSingleLine = {
name : 'definitionListsInline', name : 'definitionListsSingleLine',
level : 'block', level : 'block',
start(src) { return src.match(/^[^\n]*?::[^\n]*/m)?.index; }, // Hint to Marked.js to stop and check for a match start(src) { return src.match(/\n[^\n]*?::[^\n]*/m)?.index; }, // Hint to Marked.js to stop and check for a match
tokenizer(src, tokens) { tokenizer(src, tokens) {
const regex = /^([^\n]*?)::([^\n]*)(?:\n|$)/ym; const regex = /^([^\n]*?)::([^\n]*)(?:\n|$)/ym;
let match; let match;
@@ -312,7 +355,7 @@ const definitionListsInline = {
} }
if(definitions.length) { if(definitions.length) {
return { return {
type : 'definitionListsInline', type : 'definitionListsSingleLine',
raw : src.slice(0, endIndex), raw : src.slice(0, endIndex),
definitions definitions
}; };
@@ -326,10 +369,10 @@ const definitionListsInline = {
} }
}; };
const definitionListsMultiline = { const definitionListsMultiLine = {
name : 'definitionListsMultiline', name : 'definitionListsMultiLine',
level : 'block', level : 'block',
start(src) { return src.match(/^[^\n]*\n::/m)?.index; }, // Hint to Marked.js to stop and check for a match start(src) { return src.match(/\n[^\n]*\n::/m)?.index; }, // Hint to Marked.js to stop and check for a match
tokenizer(src, tokens) { tokenizer(src, tokens) {
const regex = /(\n?\n?(?!::)[^\n]+?(?=\n::))|\n::(.(?:.|\n)*?(?=(?:\n::)|(?:\n\n)|$))/y; const regex = /(\n?\n?(?!::)[^\n]+?(?=\n::))|\n::(.(?:.|\n)*?(?=(?:\n::)|(?:\n\n)|$))/y;
let match; let match;
@@ -353,7 +396,7 @@ const definitionListsMultiline = {
} }
if(definitions.length) { if(definitions.length) {
return { return {
type : 'definitionListsMultiline', type : 'definitionListsMultiLine',
raw : src.slice(0, endIndex), raw : src.slice(0, endIndex),
definitions definitions
}; };
@@ -617,7 +660,7 @@ function MarkedVariables() {
//^=====--------------------< Variable Handling >-------------------=====^// //^=====--------------------< Variable Handling >-------------------=====^//
Marked.use(MarkedVariables()); Marked.use(MarkedVariables());
Marked.use({ extensions: [definitionListsMultiline, definitionListsInline, superSubScripts, mustacheSpans, mustacheDivs, mustacheInjectInline] }); Marked.use({ extensions: [definitionListsMultiLine, definitionListsSingleLine, superSubScripts, mustacheSpans, mustacheDivs, mustacheInjectInline] });
Marked.use(mustacheInjectBlock); Marked.use(mustacheInjectBlock);
Marked.use({ renderer: renderer, tokenizer: tokenizer, mangle: false }); Marked.use({ renderer: renderer, tokenizer: tokenizer, mangle: false });
Marked.use(MarkedExtendedTables(), MarkedGFMHeadingId(), MarkedSmartypantsLite()); Marked.use(MarkedExtendedTables(), MarkedGFMHeadingId(), MarkedSmartypantsLite());
@@ -687,15 +730,45 @@ const processStyleTags = (string)=>{
//TODO: can we simplify to just split on commas? //TODO: can we simplify to just split on commas?
const tags = string.match(/(?:[^, ":=]+|[:=](?:"[^"]*"|))+/g); const tags = string.match(/(?:[^, ":=]+|[:=](?:"[^"]*"|))+/g);
const id = _.remove(tags, (tag)=>tag.startsWith('#')).map((tag)=>tag.slice(1))[0]; const id = _.remove(tags, (tag)=>tag.startsWith('#')).map((tag)=>tag.slice(1))[0] || null;
const classes = _.remove(tags, (tag)=>(!tag.includes(':')) && (!tag.includes('='))); const classes = _.remove(tags, (tag)=>(!tag.includes(':')) && (!tag.includes('='))).join(' ') || null;
const attributes = _.remove(tags, (tag)=>(tag.includes('='))).map((tag)=>tag.replace(/="?([^"]*)"?/g, '="$1"')); const attributes = _.remove(tags, (tag)=>(tag.includes('='))).map((tag)=>tag.replace(/="?([^"]*)"?/g, '="$1"'))
const styles = tags?.length ? tags.map((tag)=>tag.replace(/:"?([^"]*)"?/g, ':$1;').trim()) : []; ?.filter(attr => !attr.startsWith('class="') && !attr.startsWith('style="') && !attr.startsWith('id="'))
.reduce((obj, attr) => {
let [key, value] = attr.split("=");
value = value.replace(/"/g, '');
obj[key] = value;
return obj;
}, {}) || null;
const styles = tags?.length ? tags.map((tag)=>tag.replace(/:"?([^"]*)"?/g, ':$1;').trim()).join(' ') : null;
return `${classes?.length ? ` ${classes.join(' ')}` : ''}"` + return {
`${id ? ` id="${id}"` : ''}` + id : id,
`${styles?.length ? ` style="${styles.join(' ')}"` : ''}` + classes : classes,
`${attributes?.length ? ` ${attributes.join(' ')}` : ''}`; styles : styles,
attributes : _.isEmpty(attributes) ? null : attributes
};
};
const extractHTMLStyleTags = (htmlString)=> {
const id = htmlString.match(/id="([^"]*)"/)?.[1] || null;
const classes = htmlString.match(/class="([^"]*)"/)?.[1] || null;
const styles = htmlString.match(/style="([^"]*)"/)?.[1] || null;
const attributes = htmlString.match(/[a-zA-Z]+="[^"]*"/g)
?.filter(attr => !attr.startsWith('class="') && !attr.startsWith('style="') && !attr.startsWith('id="'))
.reduce((obj, attr) => {
let [key, value] = attr.split("=");
value = value.replace(/"/g, '');
obj[key] = value;
return obj;
}, {}) || null;
return {
id : id,
classes : classes,
styles : styles,
attributes : _.isEmpty(attributes) ? null : attributes
};
}; };
const globalVarsList = {}; const globalVarsList = {};

View File

@@ -130,8 +130,8 @@ describe('Inline: When using the Inline syntax {{ }}', ()=>{
describe(`Block: When using the Block syntax {{tags\\ntext\\n}}`, ()=>{ describe(`Block: When using the Block syntax {{tags\\ntext\\n}}`, ()=>{
it('Renders a div with text only', function() { it('Renders a div with text only', function() {
const source = dedent`{{ const source = dedent`{{
text text
}}`; }}`;
const rendered = Markdown.render(source).trimReturns(); const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block"><p>text</p></div>`); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block"><p>text</p></div>`);
}); });
@@ -139,14 +139,14 @@ describe(`Block: When using the Block syntax {{tags\\ntext\\n}}`, ()=>{
it('Renders an empty div', function() { it('Renders an empty div', function() {
const source = dedent`{{ const source = dedent`{{
}}`; }}`;
const rendered = Markdown.render(source).trimReturns(); const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block"></div>`); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block"></div>`);
}); });
it('Renders a single paragraph with opening and closing brackets', function() { it('Renders a single paragraph with opening and closing brackets', function() {
const source = dedent`{{ const source = dedent`{{
}}`; }}`;
const rendered = Markdown.render(source).trimReturns(); const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p>{{}}</p>`); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p>{{}}</p>`);
}); });
@@ -154,79 +154,79 @@ describe(`Block: When using the Block syntax {{tags\\ntext\\n}}`, ()=>{
it('Renders a div with a single class', function() { it('Renders a div with a single class', function() {
const source = dedent`{{cat const source = dedent`{{cat
}}`; }}`;
const rendered = Markdown.render(source).trimReturns(); const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block cat"></div>`); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block cat"></div>`);
}); });
it('Renders a div with a single class and text', function() { it('Renders a div with a single class and text', function() {
const source = dedent`{{cat const source = dedent`{{cat
Sample text. Sample text.
}}`; }}`;
const rendered = Markdown.render(source).trimReturns(); const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block cat"><p>Sample text.</p></div>`); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block cat"><p>Sample text.</p></div>`);
}); });
it('Renders a div with two classes and text', function() { it('Renders a div with two classes and text', function() {
const source = dedent`{{cat,dog const source = dedent`{{cat,dog
Sample text. Sample text.
}}`; }}`;
const rendered = Markdown.render(source).trimReturns(); const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block cat dog"><p>Sample text.</p></div>`); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block cat dog"><p>Sample text.</p></div>`);
}); });
it('Renders a div with a style and text', function() { it('Renders a div with a style and text', function() {
const source = dedent`{{color:red const source = dedent`{{color:red
Sample text. Sample text.
}}`; }}`;
const rendered = Markdown.render(source).trimReturns(); const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block" style="color:red;"><p>Sample text.</p></div>`); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block" style="color:red;"><p>Sample text.</p></div>`);
}); });
it('Renders a div with a style that has a string variable, and text', function() { it('Renders a div with a style that has a string variable, and text', function() {
const source = dedent`{{--stringVariable:"'string'" const source = dedent`{{--stringVariable:"'string'"
Sample text. Sample text.
}}`; }}`;
const rendered = Markdown.render(source).trimReturns(); const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block" style="--stringVariable:'string';"><p>Sample text.</p></div>`); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block" style="--stringVariable:'string';"><p>Sample text.</p></div>`);
}); });
it('Renders a div with a style that has a string variable, and text', function() { it('Renders a div with a style that has a string variable, and text', function() {
const source = dedent`{{--stringVariable:"'string'" const source = dedent`{{--stringVariable:"'string'"
Sample text. Sample text.
}}`; }}`;
const rendered = Markdown.render(source).trimReturns(); const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block" style="--stringVariable:'string';"><p>Sample text.</p></div>`); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block" style="--stringVariable:'string';"><p>Sample text.</p></div>`);
}); });
it('Renders a div with a class, style and text', function() { it('Renders a div with a class, style and text', function() {
const source = dedent`{{cat,color:red const source = dedent`{{cat,color:red
Sample text. Sample text.
}}`; }}`;
const rendered = Markdown.render(source).trimReturns(); const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block cat" style="color:red;"><p>Sample text.</p></div>`); 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() { it('Renders a div with an ID, class, style and text (different order)', function() {
const source = dedent`{{color:red,cat,#dog const source = dedent`{{color:red,cat,#dog
Sample text. Sample text.
}}`; }}`;
const rendered = Markdown.render(source).trimReturns(); 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>`); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block cat" id="dog" style="color:red;"><p>Sample text.</p></div>`);
}); });
it('Renders a div with a single ID', function() { it('Renders a div with a single ID', function() {
const source = dedent`{{#cat,#dog const source = dedent`{{#cat,#dog
Sample text. Sample text.
}}`; }}`;
const rendered = Markdown.render(source).trimReturns(); const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block" id="cat"><p>Sample text.</p></div>`); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block" id="cat"><p>Sample text.</p></div>`);
}); });
it('Renders a div with an ID, class, style and text, and a variable assignment', function() { it('Renders a div with an ID, class, style and text, and a variable assignment', function() {
const source = dedent`{{color:red,cat,#dog,a="b and c",d="e" const source = dedent`{{color:red,cat,#dog,a="b and c",d="e"
Sample text. Sample text.
}}`; }}`;
const rendered = Markdown.render(source).trimReturns(); const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class=\"block cat\" id=\"dog\" style=\"color:red;\" a=\"b and c\" d=\"e\"><p>Sample text.</p></div>`); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class=\"block cat\" id=\"dog\" style=\"color:red;\" a=\"b and c\" d=\"e\"><p>Sample text.</p></div>`);
}); });
@@ -243,61 +243,91 @@ describe(`Block: When using the Block syntax {{tags\\ntext\\n}}`, ()=>{
describe('Injection: When an injection tag follows an element', ()=>{ 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. // 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', ()=>{ describe('and that element is an inline-block', ()=>{
it.failing('Renders a span "text" with no injection', function() { it('Renders a span "text" with no injection', function() {
const source = '{{ text}}{}'; const source = '{{ text}}{}';
const rendered = Markdown.render(source); const rendered = Markdown.render(source);
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block">text</span>'); 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() { it('Renders a span "text" with injected Class name', function() {
const source = '{{ text}}{ClassName}'; const source = '{{ text}}{ClassName}';
const rendered = Markdown.render(source); const rendered = Markdown.render(source);
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block ClassName">text</span>'); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block ClassName">text</span>');
}); });
it.failing('Renders a span "text" with injected attribute', function() { it('Renders a span "text" with injected attribute', function() {
const source = '{{ text}}{a="b and c"}'; const source = '{{ text}}{a="b and c"}';
const rendered = Markdown.render(source); const rendered = Markdown.render(source);
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span a="b and c" class="inline-block ">text</span>'); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" a="b and c">text</span>');
}); });
it.failing('Renders a span "text" with injected style', function() { it('Renders a span "text" with injected style', function() {
const source = '{{ text}}{color:red}'; const source = '{{ text}}{color:red}';
const rendered = Markdown.render(source); const rendered = Markdown.render(source);
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" style="color:red;">text</span>'); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" style="color:red;">text</span>');
}); });
it.failing('Renders a span "text" with injected style using a string variable', function() { it('Renders a span "text" with injected style using a string variable', function() {
const source = `{{ text}}{--stringVariable:"'string'"}`; const source = `{{ text}}{--stringVariable:"'string'"}`;
const rendered = Markdown.render(source); const rendered = Markdown.render(source);
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<span class="inline-block" style="--stringVariable:'string';">text</span>`); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<span class="inline-block" style="--stringVariable:'string';">text</span>`);
}); });
it.failing('Renders a span "text" with two injected styles', function() { it('Renders a span "text" with two injected styles', function() {
const source = '{{ text}}{color:red,background:blue}'; const source = '{{ text}}{color:red,background:blue}';
const rendered = Markdown.render(source); const rendered = Markdown.render(source);
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" style="color:red; background:blue;">text</span>'); 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() { it('Renders a span "text" with its own ID, overwritten with an injected ID', function() {
const source = '{{#oldId text}}{#newId}';
const rendered = Markdown.render(source);
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" id="newId">text</span>');
});
it('Renders a span "text" with its own attributes, overwritten with an injected attribute, plus a new one', function() {
const source = '{{attrA="old",attrB="old" text}}{attrA="new",attrC="new"}';
const rendered = Markdown.render(source);
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" attrA="new" attrB="old" attrC="new">text</span>');
});
it('Renders a span "text" with its own attributes, overwritten with an injected attribute, ignoring "class", "style", and "id"', function() {
const source = '{{attrA="old",attrB="old" text}}{attrA="new",attrC="new",class="new",style="new",id="new"}';
const rendered = Markdown.render(source);
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" attrA="new" attrB="old" attrC="new">text</span>');
});
it('Renders a span "text" with its own styles, appended with injected styles', function() {
const source = '{{color:blue,height:10px text}}{width:10px,color:red}';
const rendered = Markdown.render(source);
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" style="color:blue; height:10px; width:10px; color:red;">text</span>');
});
it('Renders a span "text" with its own classes, appended with injected classes', function() {
const source = '{{classA,classB text}}{classA,classC}';
const rendered = Markdown.render(source);
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block classA classB classA classC">text</span>');
});
it('Renders an emphasis element with injected Class name', function() {
const source = '*emphasis*{big}'; const source = '*emphasis*{big}';
const rendered = Markdown.render(source).trimReturns(); const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p><em class="big">emphasis</em></p>'); 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() { it('Renders a code element with injected style', function() {
const source = '`code`{background:gray}'; const source = '`code`{background:gray}';
const rendered = Markdown.render(source).trimReturns(); const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p><code style="background:gray;">code</code></p>'); 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() { it('Renders an image element with injected style', function() {
const source = '![alt text](http://i.imgur.com/hMna6G0.png){position:absolute}'; const source = '![alt text](http://i.imgur.com/hMna6G0.png){position:absolute}';
const rendered = Markdown.render(source).trimReturns(); 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>'); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p><img style="position:absolute;" src="http://i.imgur.com/hMna6G0.png" alt="alt text"></p>');
}); });
it.failing('Renders an element modified by only the first of two consecutive injections', function() { it('Renders an element modified by only the first of two consecutive injections', function() {
const source = '{{ text}}{color:red}{background:blue}'; const source = '{{ text}}{color:red}{background:blue}';
const rendered = Markdown.render(source).trimReturns(); 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>'); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p><span class="inline-block" style="color:red;">text</span>{background:blue}</p>');
@@ -306,61 +336,106 @@ describe('Injection: When an injection tag follows an element', ()=>{
it('Renders an image with added attributes', function() { it('Renders an image with added attributes', function() {
const source = `![homebrew mug](https://i.imgur.com/hMna6G0.png) {position:absolute,bottom:20px,left:130px,width:220px,a="b and c",d=e}`; const source = `![homebrew mug](https://i.imgur.com/hMna6G0.png) {position:absolute,bottom:20px,left:130px,width:220px,a="b and c",d=e}`;
const rendered = Markdown.render(source).trimReturns(); const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p><img class="" style="position:absolute; bottom:20px; left:130px; width:220px;" a="b and c" d="e" src="https://i.imgur.com/hMna6G0.png" alt="homebrew mug"></p>`); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p><img style="position:absolute; bottom:20px; left:130px; width:220px;" src="https://i.imgur.com/hMna6G0.png" alt="homebrew mug" a="b and c" d="e"></p>`);
}); });
}); });
describe('and that element is a block', ()=>{ describe('and that element is a block', ()=>{
it.failing('renders a div "text" with no injection', function() { it('renders a div "text" with no injection', function() {
const source = '{{\ntext\n}}\n{}'; const source = '{{\ntext\n}}\n{}';
const rendered = Markdown.render(source).trimReturns(); const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<div class="block"><p>text</p></div>'); 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() { it('renders a div "text" with injected Class name', function() {
const source = '{{\ntext\n}}\n{ClassName}'; const source = '{{\ntext\n}}\n{ClassName}';
const rendered = Markdown.render(source).trimReturns(); const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<div class="block ClassName"><p>text</p></div>'); 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() { it('renders a div "text" with injected style', function() {
const source = '{{\ntext\n}}\n{color:red}'; const source = '{{\ntext\n}}\n{color:red}';
const rendered = Markdown.render(source).trimReturns(); const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<div class="block" style="color:red;"><p>text</p></div>'); 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() { it('renders a div "text" with two injected styles', function() {
const source = dedent`{{ const source = dedent`{{
text text
}} }}
{color:red,background:blue}`; {color:red,background:blue}`;
const rendered = Markdown.render(source).trimReturns(); const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block" style="color:red; background:blue"><p>text</p></div>`); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block" style="color:red; background:blue;"><p>text</p></div>`);
}); });
it.failing('renders a div "text" with injected variable string', function() { it('renders a div "text" with injected variable string', function() {
const source = dedent`{{ const source = dedent`{{
text text
}} }}
{--stringVariable:"'string'"}`; {--stringVariable:"'string'"}`;
const rendered = Markdown.render(source).trimReturns(); const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block" style="--stringVariable:'string'"><p>text</p></div>`); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block" style="--stringVariable:'string';"><p>text</p></div>`);
}); });
it.failing('renders an h2 header "text" with injected class name', function() { it('Renders a span "text" with its own ID, overwritten with an injected ID', function() {
const source = dedent`{{#oldId
text
}}
{#newId}`;
const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<div class="block" id="newId"><p>text</p></div>');
});
it('Renders a span "text" with its own attributes, overwritten with an injected attribute, plus a new one', function() {
const source = dedent`{{attrA="old",attrB="old"
text
}}
{attrA="new",attrC="new"}`;
const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<div class="block" attrA="new" attrB="old" attrC="new"><p>text</p></div>');
});
it('Renders a span "text" with its own attributes, overwritten with an injected attribute, ignoring "class", "style", and "id"', function() {
const source = dedent`{{attrA="old",attrB="old"
text
}}
{attrA="new",attrC="new",class="new",style="new",id="new"}`;
const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<div class="block" attrA="new" attrB="old" attrC="new"><p>text</p></div>');
});
it('Renders a span "text" with its own styles, appended with injected styles', function() {
const source = dedent`{{color:blue,height:10px
text
}}
{width:10px,color:red}`;
const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<div class="block" style="color:blue; height:10px; width:10px; color:red;"><p>text</p></div>');
});
it('Renders a span "text" with its own classes, appended with injected classes', function() {
const source = dedent`{{classA,classB
text
}}
{classA,classC}`;
const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<div class="block classA classB classA classC"><p>text</p></div>');
});
it('renders an h2 header "text" with injected class name', function() {
const source = dedent`## text const source = dedent`## text
{ClassName}`; {ClassName}`;
const rendered = Markdown.render(source).trimReturns(); const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<h2 class="ClassName">text</h2>'); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<h2 class="ClassName" id="text">text</h2>');
}); });
it.failing('renders a table with injected class name', function() { it('renders a table with injected class name', function() {
const source = dedent`| Experience Points | Level | const source = dedent`| Experience Points | Level |
|:------------------|:-----:| |:------------------|:-----:|
| 0 | 1 | | 0 | 1 |
| 300 | 2 | | 300 | 2 |
{ClassName}`; {ClassName}`;
const rendered = Markdown.render(source).trimReturns(); 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>`); 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>`);
}); });
@@ -376,23 +451,23 @@ describe('Injection: When an injection tag follows an element', ()=>{
// expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`...`); // FIXME: expect this to be injected into <ul>? Currently injects into last <li> // 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() { it('renders an h2 header "text" with injected class name, and "secondInjection" as regular text on the next line.', function() {
const source = dedent`## text const source = dedent`## text
{ClassName} {ClassName}
{secondInjection}`; {secondInjection}`;
const rendered = Markdown.render(source).trimReturns(); const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<h2 class="ClassName">text</h2><p>{secondInjection}</p>'); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<h2 class="ClassName" id="text">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() { it('renders a div nested into another div, the inner with class=innerDiv and the other class=outerDiv', function() {
const source = dedent`{{ const source = dedent`{{
outer text outer text
{{ {{
inner text inner text
}} }}
{innerDiv} {innerDiv}
}} }}
{outerDiv}`; {outerDiv}`;
const rendered = Markdown.render(source).trimReturns(); 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>'); 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>');
}); });

View File

@@ -329,7 +329,7 @@ describe('Normal Links and Images', ()=>{
const source = `![alt text](url){width:100px}`; const source = `![alt text](url){width:100px}`;
const rendered = Markdown.render(source).trimReturns(); const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent` expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent`
<p><img class="" style="width:100px;" src="url" alt="alt text"></p>`.trimReturns()); <p><img style="width:100px;" src="url" alt="alt text"></p>`.trimReturns());
}); });
it('Renders normal links', function() { it('Renders normal links', function() {

View File

@@ -149,8 +149,6 @@ module.exports = {
![](/assets/naturalCritLogoWhite.svg) ![](/assets/naturalCritLogoWhite.svg)
Homebrewery.Naturalcrit.com Homebrewery.Naturalcrit.com
}} }}`;
\page`;
} }
}; };

View File

@@ -1,6 +1,6 @@
@import (less) './themes/assets/assets.less'; @import (less) './themes/assets/assets.less';
@import (less) './themes/fonts/icon fonts/font-icons.less'; @import (less) './themes/fonts/icon fonts/font-icons.less';
@import (less) './themes/fonts/icon fonts/dicefont.less'; @import (less) './themes/fonts/icon fonts/diceFont.less';
:root { :root {
//Colors //Colors
@@ -532,21 +532,19 @@
.page:has(.frontCover) { .page:has(.frontCover) {
columns : 1; columns : 1;
text-align : center; text-align : center;
&::after { all : unset; } &::after { display : none; }
h1 { h1 {
margin-top : 1.2cm; margin-top : 1.2cm;
margin-bottom : 0; margin-bottom : 0;
font-family : 'NodestoCapsCondensed'; font-family : 'NodestoCapsCondensed';
font-size : 2.245cm; font-size : 2.245cm;
font-weight : normal; font-weight : normal;
line-height : 0.85em; line-height : 1.9cm;
color : white; color : white;
text-shadow : unset; text-shadow : unset;
text-transform : uppercase; text-transform : uppercase;
filter : drop-shadow(0 0 1.5px black) drop-shadow(0 0 0 black) -webkit-text-stroke: 0.2cm black;
drop-shadow(0 0 0 black) drop-shadow(0 0 0 black) paint-order:stroke;
drop-shadow(0 0 0 black) drop-shadow(0 0 0 black)
drop-shadow(0 0 0 black) drop-shadow(0 0 0 black);
} }
h2 { h2 {
font-family : 'NodestoCapsCondensed'; font-family : 'NodestoCapsCondensed';
@@ -554,10 +552,8 @@
font-weight : normal; font-weight : normal;
color : white; color : white;
letter-spacing : 0.1cm; letter-spacing : 0.1cm;
filter : drop-shadow(0 0 1px black) drop-shadow(0 0 0 black) -webkit-text-stroke: 0.14cm black;
drop-shadow(0 0 0 black) drop-shadow(0 0 0 black) paint-order:stroke;
drop-shadow(0 0 0 black) drop-shadow(0 0 0 black)
drop-shadow(0 0 0 black) drop-shadow(0 0 0 black);
} }
hr { hr {
position : relative; position : relative;
@@ -603,10 +599,8 @@
font-size : 0.496cm; font-size : 0.496cm;
color : white; color : white;
text-align : center; text-align : center;
filter : drop-shadow(0 0 0.7px black) drop-shadow(0 0 0 black) -webkit-text-stroke: 0.1cm black;
drop-shadow(0 0 0 black) drop-shadow(0 0 0 black) paint-order:stroke;
drop-shadow(0 0 0 black) drop-shadow(0 0 0 black)
drop-shadow(0 0 0 black) drop-shadow(0 0 0 black);
} }
.logo { .logo {
position : absolute; position : absolute;
@@ -626,14 +620,14 @@
.page:has(.insideCover) { .page:has(.insideCover) {
columns : 1; columns : 1;
text-align : center; text-align : center;
&::after { all : unset; } &::after { display : none; }
h1 { h1 {
margin-top : 1.2cm; margin-top : 1.2cm;
margin-bottom : 0; margin-bottom : 0;
font-family : 'NodestoCapsCondensed'; font-family : 'NodestoCapsCondensed';
font-size : 2.1cm; font-size : 2.1cm;
font-weight : normal; font-weight : normal;
line-height : 0.85em; line-height : 1.785cm;
text-transform : uppercase; text-transform : uppercase;
} }
h2 { h2 {
@@ -672,7 +666,7 @@
padding : 2.25cm 1.3cm 2cm 1.3cm; padding : 2.25cm 1.3cm 2cm 1.3cm;
color : #FFFFFF; color : #FFFFFF;
columns : 1; columns : 1;
&::after { all : unset; } &::after { display : none; }
.columnWrapper { width : 7.6cm; } .columnWrapper { width : 7.6cm; }
.backCover { .backCover {
position : absolute; position : absolute;
@@ -688,7 +682,7 @@
margin-bottom : 0.3cm; margin-bottom : 0.3cm;
font-family : 'NodestoCapsCondensed'; font-family : 'NodestoCapsCondensed';
font-size : 1.35cm; font-size : 1.35cm;
line-height : 0.95em; line-height : 1.28cm;
color : #ED1C24; color : #ED1C24;
text-align : center; text-align : center;
} }
@@ -714,7 +708,7 @@
p { p {
font-family : 'Overpass'; font-family : 'Overpass';
font-size : 0.332cm; font-size : 0.332cm;
line-height : 1.5em; line-height : 0.35cm;
} }
hr + p { hr + p {
margin-top : 0.6cm; margin-top : 0.6cm;
@@ -739,10 +733,10 @@
font-family : 'NodestoCapsWide'; font-family : 'NodestoCapsWide';
font-size : 0.4cm; font-size : 0.4cm;
line-height : 1em; line-height : 1em;
line-height : 1.28cm;
color : #FFFFFF; color : #FFFFFF;
text-align : center; text-align : center;
text-indent : 0; text-indent : 0;
letter-spacing : 0.08em;
} }
} }
} }
@@ -782,7 +776,7 @@
margin-left : auto; margin-left : auto;
font-family : 'Overpass'; font-family : 'Overpass';
font-size : 0.45cm; font-size : 0.45cm;
line-height : 1.1em; line-height : 0.495cm;
} }
} }

View File

@@ -326,27 +326,27 @@ module.exports = [
gen : dedent`{{font-family:CodeLight Dummy Text}}` gen : dedent`{{font-family:CodeLight Dummy Text}}`
}, },
{ {
name : 'Scaly Sans Remake', name : 'Scaly Sans',
icon : 'font ScalySansRemake', icon : 'font ScalySansRemake',
gen : dedent`{{font-family:ScalySansRemake Dummy Text}}` gen : dedent`{{font-family:ScalySansRemake Dummy Text}}`
}, },
{ {
name : 'Book Insanity Remake', name : 'Book Insanity',
icon : 'font BookInsanityRemake', icon : 'font BookInsanityRemake',
gen : dedent`{{font-family:BookInsanityRemake Dummy Text}}` gen : dedent`{{font-family:BookInsanityRemake Dummy Text}}`
}, },
{ {
name : 'Mr Eaves Remake', name : 'Mr Eaves',
icon : 'font MrEavesRemake', icon : 'font MrEavesRemake',
gen : dedent`{{font-family:MrEavesRemake Dummy Text}}` gen : dedent`{{font-family:MrEavesRemake Dummy Text}}`
}, },
{ {
name: 'Solbera Imitation Remake', name: 'Solbera Imitation',
icon: 'font SolberaImitationRemake', icon: 'font SolberaImitationRemake',
gen: dedent`{{font-family:SolberaImitationRemake Dummy Text}}` gen: dedent`{{font-family:SolberaImitationRemake Dummy Text}}`
}, },
{ {
name: 'Scaly Sans Small Caps Remake', name: 'Scaly Sans Small Caps',
icon: 'font ScalySansSmallCapsRemake', icon: 'font ScalySansSmallCapsRemake',
gen: dedent`{{font-family:ScalySansSmallCapsRemake Dummy Text}}` gen: dedent`{{font-family:ScalySansSmallCapsRemake Dummy Text}}`
}, },

View File

@@ -1,6 +1,6 @@
@import (less) './themes/fonts/5e/fonts.less'; @import (less) './themes/fonts/5e/fonts.less';
@import (less) './themes/assets/assets.less'; @import (less) './themes/assets/assets.less';
@import (less) './themes/fonts/icon fonts/dicefont.less'; @import (less) './themes/fonts/icon fonts/diceFont.less';
:root { :root {
//Colors //Colors

View File

@@ -1,9 +1,9 @@
/* Icon Font: dicefont */ /* Icon Font: diceFont */
@font-face { @font-face {
font-family : 'DiceFont'; font-family : 'DiceFont';
font-style : normal; font-style : normal;
font-weight : normal; font-weight : normal;
src : url('../../../fonts/icon fonts/dicefont.woff2'); src : url('../../../fonts/icon fonts/diceFont.woff2');
} }
.df { .df {