0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2026-01-01 17:22:42 +00:00

Merge branch 'master' of https://github.com/naturalcrit/homebrewery into metadata-api-endpoint

This commit is contained in:
Víctor Losada Hernández
2024-07-19 08:59:04 +02:00
29 changed files with 1279 additions and 981 deletions

View File

@@ -84,6 +84,55 @@ pre {
## changelog ## changelog
For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery). For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery).
### Saturday 6/7/2024 - v3.13.1
{{taskList
##### calculuschild, G-Ambatte
* [x] Hotfixes for issues with v3.13.0
Fixes issues [#3559](https://github.com/naturalcrit/homebrewery/issues/3559), [#3552](https://github.com/naturalcrit/homebrewery/issues/3552), [#3554](https://github.com/naturalcrit/homebrewery/issues/3554)
}}
### Friday 28/6/2024 - v3.13.0
{{taskList
##### calculuschild
* [x] Add `:emoji:` Markdown syntax, with autosuggest; start typing after the first `:` for matching emojis from
:fab_font_awesome: FontAwesome, :df_d20: DiceFont, :ei_action: ElderberryInn, and a subset of :gi_broadsword: GameIcons
* [x] Fix `{curly injection}` to append to, rather than erase and replace target CSS
* [x] {{openSans **GET PDF**}} {{fa,fa-file-pdf}} now opens the print dialog directly, rather than redirecting to a separate page
##### Gazook
* [x] Several small style tweaks to the UI
* [x] Cleaning and refactoring several large pieces of code
##### 5e-Cleric
* [x] For error pages, add links to user account and `/share` page if available
Fixes issue [#3298](https://github.com/naturalcrit/homebrewery/issues/3298)
* [x] Change FrontCover title to use stroke outline instead of faking it with dozens of shadows
* [x] Cleaning and refactoring several large pieces of CSS
##### abquintic
* [x] Added additional {{openSans **TABLE OF CONTENTS**}} snippet options. Explicitly include or exclude items from the ToC generation via CSS properties
`--TOC:exclude` or `--TOC:include`, or change the included header depth from 3 to 6 (default 3) with `tocDepthH6`
##### MurdoMaclachlan *(new contributor!)*
* [x] Added "proficiency bonus" to Monster Stat Block snippet.
Fixes issue [#3397](https://github.com/naturalcrit/homebrewery/issues/3397)
}}
\column
### Monday 18/3/2024 - v3.12.0 ### Monday 18/3/2024 - v3.12.0
{{taskList {{taskList

View File

@@ -0,0 +1,29 @@
// Dialog box, for popups and modal blocking messages
const React = require('react');
const { useRef, useEffect } = React;
function Dialog({ dismissKey, closeText = 'Close', blocking = false, ...rest }) {
const dialogRef = useRef(null);
useEffect(()=>{
if(!dismissKey || !localStorage.getItem(dismissKey)) {
blocking ? dialogRef.current?.showModal() : dialogRef.current?.show();
}
}, []);
const dismiss = ()=>{
dismissKey && localStorage.setItem(dismissKey, true);
dialogRef.current?.close();
};
return (
<dialog ref={dialogRef} onCancel={dismiss} {...rest}>
{rest.children}
<button className='dismiss' onClick={dismiss}>
{closeText}
</button>
</dialog>
);
};
export default Dialog;

View File

@@ -37,7 +37,7 @@ const BrewPage = (props)=>{
index : 0, index : 0,
...props ...props
}; };
const cleanText = DOMPurify.sanitize(props.contents, purifyConfig); const cleanText = props.contents; //DOMPurify.sanitize(props.contents, purifyConfig);
return <div className={props.className} id={`p${props.index + 1}`} > return <div className={props.className} id={`p${props.index + 1}`} >
<div className='columnWrapper' dangerouslySetInnerHTML={{ __html: cleanText }} /> <div className='columnWrapper' dangerouslySetInnerHTML={{ __html: cleanText }} />
</div>; </div>;
@@ -126,7 +126,7 @@ const BrewRenderer = (props)=>{
const renderStyle = ()=>{ const renderStyle = ()=>{
if(!props.style) return; if(!props.style) return;
const cleanStyle = DOMPurify.sanitize(props.style, purifyConfig); const cleanStyle = props.style; //DOMPurify.sanitize(props.style, purifyConfig);
//return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `<style>@layer styleTab {\n${sanitizeScriptTags(props.style)}\n} </style>` }} />; //return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `<style>@layer styleTab {\n${sanitizeScriptTags(props.style)}\n} </style>` }} />;
return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `<style> ${cleanStyle} </style>` }} />; return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `<style> ${cleanStyle} </style>` }} />;
}; };
@@ -203,6 +203,12 @@ const BrewRenderer = (props)=>{
</div> </div>
: null} : null}
<ErrorBar errors={props.errors} />
<div className='popups'>
<RenderWarnings />
<NotificationPopup />
</div>
{/*render in iFrame so broken code doesn't crash the site.*/} {/*render in iFrame so broken code doesn't crash the site.*/}
<Frame id='BrewRenderer' initialContent={INITIAL_CONTENT} <Frame id='BrewRenderer' initialContent={INITIAL_CONTENT}
style={{ width: '100%', height: '100%', visibility: state.visibility }} style={{ width: '100%', height: '100%', visibility: state.visibility }}
@@ -215,11 +221,6 @@ const BrewRenderer = (props)=>{
tabIndex={-1} tabIndex={-1}
style={{ height: state.height }}> style={{ height: state.height }}>
<ErrorBar errors={props.errors} />
<div className='popups'>
<RenderWarnings />
<NotificationPopup />
</div>
<link href={`/themes/${rendererPath}/Blank/style.css`} type='text/css' rel='stylesheet'/> <link href={`/themes/${rendererPath}/Blank/style.css`} type='text/css' rel='stylesheet'/>
{baseThemePath && {baseThemePath &&
<link href={`/themes/${rendererPath}/${baseThemePath}/style.css`} type='text/css' rel='stylesheet'/> <link href={`/themes/${rendererPath}/${baseThemePath}/style.css`} type='text/css' rel='stylesheet'/>

View File

@@ -1,80 +1,45 @@
require('./notificationPopup.less'); require('./notificationPopup.less');
const React = require('react'); const React = require('react');
const createClass = require('create-react-class');
const _ = require('lodash'); const _ = require('lodash');
import Dialog from '../../../components/dialog.jsx';
const DISMISS_KEY = 'dismiss_notification12-04-23'; const DISMISS_KEY = 'dismiss_notification12-04-23';
const DISMISS_BUTTON = <i className='fas fa-times dismiss' />;
const NotificationPopup = createClass({ const NotificationPopup = ()=>{
displayName : 'NotificationPopup', return <Dialog className='notificationPopup' dismissKey={DISMISS_KEY} closeText={DISMISS_BUTTON} >
getInitialState : function() { <div className='header'>
return { <i className='fas fa-info-circle info'></i>
notifications : {} <h3>Notice</h3>
}; <small>This website is always improving and we are still adding new features and squashing bugs. Keep the following in mind:</small>
}, </div>
componentDidMount : function() { <ul>
this.checkNotifications(); <li key='psa'>
window.addEventListener('resize', this.checkNotifications); <em>Don't store IMAGES in Google Drive</em><br />
}, Google Drive is not an image service, and will block images from being used
componentWillUnmount : function() { in brews if they get more views than expected. Google has confirmed they won't fix
window.removeEventListener('resize', this.checkNotifications); this, so we recommend you look for another image hosting service such as imgur, ImgBB or Google Photos.
}, </li>
notifications : {
psa : function(){
return (
<>
<li key='psa'>
<em>Don't store IMAGES in Google Drive</em><br />
Google Drive is not an image service, and will block images from being used
in brews if they get more views than expected. Google has confirmed they won't fix
this, so we recommend you look for another image hosting service such as imgur, ImgBB or Google Photos.
</li>
<li key='googleDriveFolder'> <li key='googleDriveFolder'>
<em>Don't delete your Homebrewery folder on Google Drive!</em> <br /> <em>Don't delete your Homebrewery folder on Google Drive!</em> <br />
We have had several reports of users losing their brews, not realizing We have had several reports of users losing their brews, not realizing
that they had deleted the files on their Google Drive. If you have a Homebrewery folder that they had deleted the files on their Google Drive. If you have a Homebrewery folder
on your Google Drive with *.txt files inside, <em>do not delete it</em>! on your Google Drive with *.txt files inside, <em>do not delete it</em>!
We cannot help you recover files that you have deleted from your own We cannot help you recover files that you have deleted from your own
Google Drive. Google Drive.
</li> </li>
<li key='faq'> <li key='faq'>
<em>Protect your work! </em> <br /> <em>Protect your work! </em> <br />
If you opt not to use your Google Drive, keep in mind that we do not save a history of your projects. Please make frequent backups of your brews!&nbsp; If you opt not to use your Google Drive, keep in mind that we do not save a history of your projects. Please make frequent backups of your brews!&nbsp;
<a target='_blank' href='https://www.reddit.com/r/homebrewery/comments/adh6lh/faqs_psas_announcements/'> <a target='_blank' href='https://www.reddit.com/r/homebrewery/comments/adh6lh/faqs_psas_announcements/'>
See the FAQ See the FAQ
</a> to learn how to avoid losing your work! </a> to learn how to avoid losing your work!
</li> </li>
</> </ul>
); </Dialog>;
} };
},
checkNotifications : function(){
const hideDismiss = localStorage.getItem(DISMISS_KEY);
if(hideDismiss) return this.setState({ notifications: {} });
this.setState({
notifications : _.mapValues(this.notifications, (fn)=>{ return fn(); }) //Convert notification functions into their return text value
});
},
dismiss : function(){
localStorage.setItem(DISMISS_KEY, true);
this.checkNotifications();
},
render : function(){
if(_.isEmpty(this.state.notifications)) return null;
return <div className='notificationPopup'>
<i className='fas fa-times dismiss' onClick={this.dismiss}/>
<i className='fas fa-info-circle info' />
<div className='header'>
<h3>Notice</h3>
<small>This website is always improving and we are still adding new features and squashing bugs. Keep the following in mind:</small>
</div>
<ul>{_.values(this.state.notifications)}</ul>
</div>;
}
});
module.exports = NotificationPopup; module.exports = NotificationPopup;

View File

@@ -1,64 +1,60 @@
.popups{ .popups {
position : fixed; position : fixed;
top : @navbarHeight; top : @navbarHeight;
right : 15px; right : 24px;
z-index : 10001; z-index : 10001;
width : 450px; width : 450px;
} }
.notificationPopup{ .notificationPopup {
position : relative; position : relative;
display : inline-block;
width : 100%; width : 100%;
padding : 15px; padding : 15px;
padding-bottom : 10px; padding-bottom : 10px;
padding-left : 25px; padding-left : 25px;
background-color : @blue;
color : white; color : white;
a{ background-color : @blue;
color : #e0e5c1; border : none;
&[open] { display : inline-block; }
a {
font-weight : 800; font-weight : 800;
color : #E0E5C1;
} }
i.info{ i.info {
position : absolute; position : absolute;
top : 12px; top : 12px;
left : 12px; left : 12px;
opacity : 0.8;
font-size : 2.5em; font-size : 2.5em;
opacity : 0.8;
} }
i.dismiss{ button.dismiss {
position : absolute; position : absolute;
top : 10px; top : 10px;
right : 10px; right : 10px;
cursor : pointer; cursor : pointer;
opacity : 0.6; background-color : transparent;
&:hover{ opacity : 0.6;
opacity : 1; &:hover { opacity : 1; }
}
} }
.header { .header { padding-left : 50px; }
padding-left : 50px; small {
}
small{
opacity : 0.7;
font-size : 0.6em; font-size : 0.6em;
opacity : 0.7;
} }
h3{ h3 {
font-size : 1.1em; font-size : 1.1em;
font-weight : 800; font-weight : 800;
} }
ul{ ul {
margin-top : 15px; margin-top : 15px;
font-size : 0.8em; font-size : 0.8em;
list-style-position : outside; list-style-position : outside;
list-style-type : disc; list-style-type : disc;
li{ li {
margin-top : 1.4em;
font-size : 0.8em; font-size : 0.8em;
line-height : 1.4em; line-height : 1.4em;
margin-top : 1.4em; em { font-weight : 800; }
em{
font-weight : 800;
}
} }
} }
} }

View File

@@ -119,11 +119,12 @@
text-align : center; text-align : center;
a{ a{
.animate(opacity); .animate(opacity);
display : block; display : block;
margin : 8px 0px; margin : 8px 0px;
opacity : 0.6; opacity : 0.6;
font-size : 1.3em; font-size : 1.3em;
color : white; color : white;
text-decoration : unset;
&:hover{ &:hover{
opacity : 1; opacity : 1;
} }

View File

@@ -20,6 +20,8 @@ const SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
const Editor = require('../../editor/editor.jsx'); const Editor = require('../../editor/editor.jsx');
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx'); const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
const LockNotification = require('./lockNotification/lockNotification.jsx');
const Markdown = require('naturalcrit/markdown.js'); const Markdown = require('naturalcrit/markdown.js');
const { DEFAULT_BREW_LOAD } = require('../../../../server/brewDefaults.js'); const { DEFAULT_BREW_LOAD } = require('../../../../server/brewDefaults.js');
@@ -52,7 +54,8 @@ const EditPage = createClass({
autoSave : true, autoSave : true,
autoSaveWarning : false, autoSaveWarning : false,
unsavedTime : new Date(), unsavedTime : new Date(),
currentEditorPage : 0 currentEditorPage : 0,
displayLockMessage : this.props.brew.lock || false
}; };
}, },
@@ -393,6 +396,7 @@ const EditPage = createClass({
{this.renderNavbar()} {this.renderNavbar()}
<div className='content'> <div className='content'>
{this.props.brew.lock && <LockNotification shareId={this.props.brew.shareId} message={this.props.brew.lock.editMessage} />}
<SplitPane onDragFinish={this.handleSplitMove}> <SplitPane onDragFinish={this.handleSplitMove}>
<Editor <Editor
ref={this.editor} ref={this.editor}

View File

@@ -0,0 +1,30 @@
require('./lockNotification.less');
const React = require('react');
import Dialog from '../../../../components/dialog.jsx';
function LockNotification(props) {
props = {
shareId : 0,
disableLock : ()=>{},
message : '',
...props
};
const removeLock = ()=>{
alert(`Not yet implemented - ID ${props.shareId}`);
};
return <Dialog className='lockNotification' blocking closeText='CONTINUE TO EDITOR' >
<h1>BREW LOCKED</h1>
<p>This brew been locked by the Administrators. It will not be accessible by any method other than the Editor until the lock is removed.</p>
<hr />
<h3>LOCK REASON</h3>
<p>{props.message || 'Unable to retrieve Lock Message'}</p>
<hr />
<p>Once you have resolved this issue, click REQUEST LOCK REMOVAL to notify the Administrators for review.</p>
<p>Click CONTINUE TO EDITOR to temporarily hide this notification; it will reappear the next time the page is reloaded.</p>
<button onClick={removeLock}>REQUEST LOCK REMOVAL</button>
</Dialog>;
};
module.exports = LockNotification;

View File

@@ -0,0 +1,27 @@
.lockNotification {
z-index : 1;
width : 80%;
padding : 10px;
margin : 5% 10%;
line-height : 1.5em;
color : black;
text-align : center;
background-color : #CCCCCC;
&::backdrop { background-color : #000000AA; }
button {
margin : 10px;
color : white;
background-color : #333333;
&:hover { background-color : #777777; }
}
h1, h3 {
font-family : 'Open Sans', sans-serif;
font-weight : 800;
}
h1 { font-size : 24px; }
h3 { font-size : 18px; }
}

View File

@@ -140,7 +140,7 @@ const errorIndex = (props)=>{
'100' : dedent` '100' : dedent`
## This brew has been locked. ## This brew has been locked.
Please contact the Administrators to unlock this document. Only an author may request that this lock is removed.
: :

1576
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{ {
"name": "homebrewery", "name": "homebrewery",
"description": "Create authentic looking D&D homebrews using only markdown", "description": "Create authentic looking D&D homebrews using only markdown",
"version": "3.12.0", "version": "3.13.1",
"engines": { "engines": {
"npm": "^10.2.x", "npm": "^10.2.x",
"node": "^20.8.x" "node": "^20.8.x"
@@ -82,18 +82,18 @@
] ]
}, },
"dependencies": { "dependencies": {
"@babel/core": "^7.24.5", "@babel/core": "^7.24.7",
"@babel/plugin-transform-runtime": "^7.24.3", "@babel/plugin-transform-runtime": "^7.24.7",
"@babel/preset-env": "^7.24.5", "@babel/preset-env": "^7.24.7",
"@babel/preset-react": "^7.24.1", "@babel/preset-react": "^7.24.7",
"@googleapis/drive": "^8.8.0", "@googleapis/drive": "^8.11.0",
"body-parser": "^1.20.2", "body-parser": "^1.20.2",
"classnames": "^2.5.1", "classnames": "^2.5.1",
"codemirror": "^5.65.6", "codemirror": "^5.65.6",
"cookie-parser": "^1.4.6", "cookie-parser": "^1.4.6",
"create-react-class": "^15.7.0", "create-react-class": "^15.7.0",
"dedent-tabs": "^0.10.3", "dedent-tabs": "^0.10.3",
"dompurify": "^3.1.4", "dompurify": "^3.1.5",
"expr-eval": "^2.0.2", "expr-eval": "^2.0.2",
"express": "^4.19.2", "express": "^4.19.2",
"express-async-handler": "^1.2.0", "express-async-handler": "^1.2.0",
@@ -104,27 +104,27 @@
"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-emoji": "^1.4.1",
"marked-extended-tables": "^1.0.8", "marked-extended-tables": "^1.0.8",
"marked-gfm-heading-id": "^3.1.3", "marked-gfm-heading-id": "^3.2.0",
"marked-smartypants-lite": "^1.0.2", "marked-smartypants-lite": "^1.0.2",
"markedLegacy": "npm:marked@^0.3.19", "markedLegacy": "npm:marked@^0.3.19",
"moment": "^2.30.1", "moment": "^2.30.1",
"mongoose": "^8.4.0", "mongoose": "^8.4.5",
"nanoid": "3.3.4", "nanoid": "3.3.4",
"nconf": "^0.12.1", "nconf": "^0.12.1",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-frame-component": "^4.1.3", "react-frame-component": "^4.1.3",
"react-router-dom": "6.23.1", "react-router-dom": "6.24.1",
"sanitize-filename": "1.6.3", "sanitize-filename": "1.6.3",
"superagent": "^9.0.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": "^28.5.0", "eslint-plugin-jest": "^28.6.0",
"eslint-plugin-react": "^7.34.1", "eslint-plugin-react": "^7.34.3",
"jest": "^29.7.0", "jest": "^29.7.0",
"jest-expect-message": "^1.1.3", "jest-expect-message": "^1.1.3",
"postcss-less": "^6.0.0", "postcss-less": "^6.0.0",

View File

@@ -55,7 +55,7 @@ const api = {
stub = stub?.toObject(); stub = stub?.toObject();
if(stub?.lock?.locked && accessType != 'edit') { if(stub?.lock?.locked && accessType != 'edit') {
throw { HBErrorCode: '100', code: stub.lock.code, message: stub.lock.message, brewId: stub.shareId, brewTitle: stub.title }; throw { HBErrorCode: '100', code: stub.lock.code, message: stub.lock.shareMessage, brewId: stub.shareId, brewTitle: stub.title };
} }
// If there is a google id, try to find the google brew // If there is a google id, try to find the google brew

View File

@@ -300,7 +300,7 @@ describe('Tests for api', ()=>{
}); });
it('access is denied to a locked brew', async()=>{ it('access is denied to a locked brew', async()=>{
const lockBrew = { title: 'test brew', shareId: '1', lock: { locked: true, code: 404, message: 'brew locked' } }; const lockBrew = { title: 'test brew', shareId: '1', lock: { locked: true, code: 404, shareMessage: 'brew locked' } };
model.get = jest.fn(()=>toBrewPromise(lockBrew)); model.get = jest.fn(()=>toBrewPromise(lockBrew));
api.getId = jest.fn(()=>({ id: '1', googleId: undefined })); api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));

View File

@@ -3,7 +3,7 @@ const React = require('react');
const createClass = require('create-react-class'); const createClass = require('create-react-class');
const _ = require('lodash'); const _ = require('lodash');
const DISMISS_KEY = 'dismiss_render_warning'; import Dialog from '../../../client/components/dialog.jsx';
const RenderWarnings = createClass({ const RenderWarnings = createClass({
displayName : 'RenderWarnings', displayName : 'RenderWarnings',
@@ -34,9 +34,6 @@ const RenderWarnings = createClass({
}, },
}, },
checkWarnings : function(){ checkWarnings : function(){
const hideDismiss = localStorage.getItem(DISMISS_KEY);
if(hideDismiss) return this.setState({ warnings: {} });
this.setState({ this.setState({
warnings : _.reduce(this.warnings, (r, fn, type)=>{ warnings : _.reduce(this.warnings, (r, fn, type)=>{
const element = fn(); const element = fn();
@@ -45,20 +42,18 @@ const RenderWarnings = createClass({
}, {}) }, {})
}); });
}, },
dismiss : function(){
localStorage.setItem(DISMISS_KEY, true);
this.checkWarnings();
},
render : function(){ render : function(){
if(_.isEmpty(this.state.warnings)) return null; if(_.isEmpty(this.state.warnings)) return null;
return <div className='renderWarnings'> const DISMISS_KEY = 'dismiss_render_warning';
<i className='fas fa-times dismiss' onClick={this.dismiss}/> const DISMISS_TEXT = <i className='fas fa-times dismiss' />;
return <Dialog className='renderWarnings' dismissKey={DISMISS_KEY} closeText={DISMISS_TEXT}>
<i className='fas fa-exclamation-triangle ohno' /> <i className='fas fa-exclamation-triangle ohno' />
<h3>Render Warnings</h3> <h3>Render Warnings</h3>
<small>If this homebrew is rendering badly if might be because of the following:</small> <small>If this homebrew is rendering badly if might be because of the following:</small>
<ul>{_.values(this.state.warnings)}</ul> <ul>{_.values(this.state.warnings)}</ul>
</div>; </Dialog>;
} }
}); });

View File

@@ -1,53 +1,48 @@
.renderWarnings{ .renderWarnings {
position : relative; position : relative;
float : right; float : right;
display : inline-block;
width : 350px; width : 350px;
padding : 20px; padding : 20px;
padding-bottom : 10px; padding-bottom : 10px;
padding-left : 85px; padding-left : 85px;
margin-bottom : 10px; margin-bottom : 10px;
background-color : @yellow;
color : white; color : white;
a{ background-color : @yellow;
font-weight : 800; border : none;
} a { font-weight : 800; }
i.ohno{ i.ohno {
position : absolute; position : absolute;
top : 24px; top : 24px;
left : 24px; left : 24px;
opacity : 0.8;
font-size : 2.5em; font-size : 2.5em;
opacity : 0.8;
} }
i.dismiss{ button.dismiss {
position : absolute; position : absolute;
top : 10px; top : 10px;
right : 10px; right : 10px;
cursor : pointer; cursor : pointer;
opacity : 0.6; background-color : transparent;
&:hover{ opacity : 0.6;
opacity : 1; &:hover { opacity : 1; }
}
} }
small{ small {
opacity : 0.7;
font-size : 0.6em; font-size : 0.6em;
opacity : 0.7;
} }
h3{ h3 {
font-size : 1.1em; font-size : 1.1em;
font-weight : 800; font-weight : 800;
} }
ul{ ul {
margin-top : 15px; margin-top : 15px;
font-size : 0.8em; font-size : 0.8em;
list-style-position : outside; list-style-position : outside;
list-style-type : disc; list-style-type : disc;
li{ li {
font-size : 0.8em; font-size : 0.8em;
line-height : 1.6em; line-height : 1.6em;
em{ em { font-weight : 800; }
font-weight : 800;
}
} }
} }
} }

View File

@@ -8,6 +8,7 @@
@import (less) './themes/fonts/iconFonts/diceFont.less'; @import (less) './themes/fonts/iconFonts/diceFont.less';
@import (less) './themes/fonts/iconFonts/elderberryInn.less'; @import (less) './themes/fonts/iconFonts/elderberryInn.less';
@import (less) './themes/fonts/iconFonts/gameIcons.less'; @import (less) './themes/fonts/iconFonts/gameIcons.less';
@import (less) './themes/fonts/iconFonts/fontAwesome.less';
@keyframes sourceMoveAnimation { @keyframes sourceMoveAnimation {
50% {background-color: red; color: white;} 50% {background-color: red; color: white;}

View File

@@ -102,7 +102,7 @@ const mustacheSpans = {
start(src) { return src.match(/{{[^{]/)?.index; }, // Hint to Marked.js to stop and check for a match start(src) { return src.match(/{{[^{]/)?.index; }, // Hint to Marked.js to stop and check for a match
tokenizer(src, tokens) { tokenizer(src, tokens) {
const completeSpan = /^{{[^\n]*}}/; // Regex for the complete token const completeSpan = /^{{[^\n]*}}/; // Regex for the complete token
const inlineRegex = /{{(?=((?:[:=](?:"['\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1 *|}}/g; const inlineRegex = /{{(?=((?:[:=](?:"['\w,\-()#%=?. ]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1 *|}}/g;
const match = completeSpan.exec(src); const match = completeSpan.exec(src);
if(match) { if(match) {
//Find closing delimiter //Find closing delimiter
@@ -159,7 +159,7 @@ const mustacheDivs = {
start(src) { return src.match(/\n *{{[^{]/m)?.index; }, // Hint to Marked.js to stop and check for a match start(src) { return src.match(/\n *{{[^{]/m)?.index; }, // Hint to Marked.js to stop and check for a match
tokenizer(src, tokens) { tokenizer(src, tokens) {
const completeBlock = /^ *{{[^\n}]* *\n.*\n *}}/s; // Regex for the complete token const completeBlock = /^ *{{[^\n}]* *\n.*\n *}}/s; // Regex for the complete token
const blockRegex = /^ *{{(?=((?:[:=](?:"['\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1 *$|^ *}}$/gm; const blockRegex = /^ *{{(?=((?:[:=](?:"['\w,\-()#%=?. ]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1 *$|^ *}}$/gm;
const match = completeBlock.exec(src); const match = completeBlock.exec(src);
if(match) { if(match) {
//Find closing delimiter //Find closing delimiter
@@ -214,7 +214,7 @@ const mustacheInjectInline = {
level : 'inline', level : 'inline',
start(src) { return src.match(/ *{[^{\n]/)?.index; }, // Hint to Marked.js to stop and check for a match start(src) { return src.match(/ *{[^{\n]/)?.index; }, // Hint to Marked.js to stop and check for a match
tokenizer(src, tokens) { tokenizer(src, tokens) {
const inlineRegex = /^ *{(?=((?:[:=](?:"['\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1}/g; const inlineRegex = /^ *{(?=((?:[:=](?:"['\w,\-()#%=?. ]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1}/g;
const match = inlineRegex.exec(src); const match = inlineRegex.exec(src);
if(match) { if(match) {
const lastToken = tokens[tokens.length - 1]; const lastToken = tokens[tokens.length - 1];
@@ -265,7 +265,7 @@ const mustacheInjectBlock = {
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]/m)?.index; }, // Hint to Marked.js to stop and check for a match
tokenizer(src, tokens) { tokenizer(src, tokens) {
const inlineRegex = /^ *{(?=((?:[:=](?:"['\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1}/ym; const inlineRegex = /^ *{(?=((?:[:=](?:"['\w,\-()#%=?. ]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1}/ym;
const match = inlineRegex.exec(src); const match = inlineRegex.exec(src);
if(match) { if(match) {
const lastToken = tokens[tokens.length - 1]; const lastToken = tokens[tokens.length - 1];
@@ -771,7 +771,8 @@ const processStyleTags = (string)=>{
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"'))
?.filter((attr)=>!attr.startsWith('class="') && !attr.startsWith('style="') && !attr.startsWith('id="')) ?.filter((attr)=>!attr.startsWith('class="') && !attr.startsWith('style="') && !attr.startsWith('id="'))
.reduce((obj, attr)=>{ .reduce((obj, attr)=>{
let [key, value] = attr.split('='); const index = attr.indexOf('=');
let [key, value] = [attr.substring(0, index), attr.substring(index + 1)];
value = value.replace(/"/g, ''); value = value.replace(/"/g, '');
obj[key] = value; obj[key] = value;
return obj; return obj;
@@ -786,14 +787,17 @@ const processStyleTags = (string)=>{
}; };
}; };
//Given a string representing an HTML element, extract all of its properties (id, class, style, and other attributes)
const extractHTMLStyleTags = (htmlString)=>{ const extractHTMLStyleTags = (htmlString)=>{
const id = htmlString.match(/id="([^"]*)"/)?.[1] || null; const firstElementOnly = htmlString.split('>')[0];
const classes = htmlString.match(/class="([^"]*)"/)?.[1] || null; const id = firstElementOnly.match(/id="([^"]*)"/)?.[1] || null;
const styles = htmlString.match(/style="([^"]*)"/)?.[1] || null; const classes = firstElementOnly.match(/class="([^"]*)"/)?.[1] || null;
const attributes = htmlString.match(/[a-zA-Z]+="[^"]*"/g) const styles = firstElementOnly.match(/style="([^"]*)"/)?.[1] || null;
const attributes = firstElementOnly.match(/[a-zA-Z]+="[^"]*"/g)
?.filter((attr)=>!attr.startsWith('class="') && !attr.startsWith('style="') && !attr.startsWith('id="')) ?.filter((attr)=>!attr.startsWith('class="') && !attr.startsWith('style="') && !attr.startsWith('id="'))
.reduce((obj, attr)=>{ .reduce((obj, attr)=>{
let [key, value] = attr.split('='); const index = attr.indexOf('=');
let [key, value] = [attr.substring(0, index), attr.substring(index + 1)];
value = value.replace(/"/g, ''); value = value.replace(/"/g, '');
obj[key] = value; obj[key] = value;
return obj; return obj;

View File

@@ -333,11 +333,30 @@ describe('Injection: When an injection tag follows an element', ()=>{
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>');
}); });
it('Renders an parent and child element, each modified by an injector', function() {
const source = dedent`**bolded text**{color:red}
{color:blue}`;
const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p style="color:blue;"><strong style="color:red;">bolded text</strong></p>');
});
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 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>`); 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>`);
}); });
it('Renders an image with "=" in the url, and added attributes', function() {
const source = `![homebrew mug](https://i.imgur.com/hMna6G0.png?auth=12345&height=1024) {position:absolute,bottom:20px,left:130px,width:220px,a="b and c",d=e}`;
const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p><img style="position:absolute; bottom:20px; left:130px; width:220px;" src="https://i.imgur.com/hMna6G0.png?auth=12345&height=1024" alt="homebrew mug" a="b and c" d="e"></p>`);
});
it('Renders an image and added attributes with "=" in the value, ', 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,otherUrl="url?auth=12345"}`;
const rendered = Markdown.render(source).trimReturns();
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" otherUrl="url?auth=12345"></p>`);
});
}); });
describe('and that element is a block', ()=>{ describe('and that element is a block', ()=>{

View File

@@ -21,9 +21,43 @@ module.exports = [
view : 'text', view : 'text',
snippets : [ snippets : [
{ {
name : 'Table of Contents', name : 'Table of Contents',
icon : 'fas fa-book', icon : 'fas fa-book',
gen : TableOfContentsGen gen : TableOfContentsGen,
experimental : true,
subsnippets : [
{
name : 'Table of Contents',
icon : 'fas fa-book',
gen : TableOfContentsGen,
experimental : true
},
{
name : 'Include in ToC up to H3',
icon : 'fas fa-dice-three',
gen : dedent `\n{{tocDepthH3
}}\n`,
},
{
name : 'Include in ToC up to H4',
icon : 'fas fa-dice-four',
gen : dedent `\n{{tocDepthH4
}}\n`,
},
{
name : 'Include in ToC up to H5',
icon : 'fas fa-dice-five',
gen : dedent `\n{{tocDepthH5
}}\n`,
},
{
name : 'Include in ToC up to H6',
icon : 'fas fa-dice-six',
gen : dedent `\n{{tocDepthH6
}}\n`,
}
]
}, },
{ {
name : 'Index', name : 'Index',

View File

@@ -2,77 +2,68 @@ const _ = require('lodash');
const dedent = require('dedent-tabs').default; const dedent = require('dedent-tabs').default;
const getTOC = (pages)=>{ const getTOC = (pages)=>{
const add1 = (title, page)=>{
res.push({ const recursiveAdd = (title, page, targetDepth, child, curDepth=0)=>{
title : title, if(curDepth > 5) return; // Something went wrong.
page : page + 1, if(curDepth == targetDepth) {
children : [] child.push({
}); title : title,
}; page : page,
const add2 = (title, page)=>{ children : []
if(!_.last(res)) add1(null, page); });
_.last(res).children.push({ } else {
title : title, if(child.length == 0) {
page : page + 1, child.push({
children : [] title : null,
}); page : page,
}; children : []
const add3 = (title, page)=>{ });
if(!_.last(res)) add1(null, page); }
if(!_.last(_.last(res).children)) add2(null, page); recursiveAdd(title, page, targetDepth, _.last(child).children, curDepth+1,);
_.last(_.last(res).children).children.push({ }
title : title,
page : page + 1,
children : []
});
}; };
const res = []; const res = [];
_.each(pages, (page, pageNum)=>{
if(!page.includes('{{frontCover}}') && !page.includes('{{insideCover}}') && !page.includes('{{partCover}}') && !page.includes('{{backCover}}')) { const iframe = document.getElementById('BrewRenderer');
const lines = page.split('\n'); const iframeDocument = iframe.contentDocument || iframe.contentWindow.document;
_.each(lines, (line)=>{ const headings = iframeDocument.querySelectorAll('h1, h2, h3, h4, h5, h6');
if(_.startsWith(line, '# ')){ const headerDepth = ['H1', 'H2', 'H3', 'H4', 'H5', 'H6'];
const title = line.replace('# ', '');
add1(title, pageNum); _.each(headings, (heading)=>{
} const onPage = parseInt(heading.closest('.page').id?.replace(/^p/, ''));
if(_.startsWith(line, '## ')){ const ToCExclude = getComputedStyle(heading).getPropertyValue('--TOC');
const title = line.replace('## ', '');
add2(title, pageNum); if(ToCExclude != 'exclude') {
} recursiveAdd(heading.innerText.trim(), onPage, headerDepth.indexOf(heading.tagName), res);
if(_.startsWith(line, '### ')){
const title = line.replace('### ', '');
add3(title, pageNum);
}
});
} }
}); });
return res; return res;
}; };
const ToCIterate = (entries, curDepth=0)=>{
const levelPad = ['- ###', ' - ####', ' - ', ' - ', ' - ', ' - '];
const toc = [];
if(entries.title !== null){
toc.push(`${levelPad[curDepth]} [{{ ${entries.title}}}{{ ${entries.page}}}](#p${entries.page})`);
}
if(entries.children.length) {
_.each(entries.children, (entry, idx)=>{
const children = ToCIterate(entry, entry.title == null ? curDepth : curDepth+1);
if(children.length) {
toc.push(...children);
}
});
}
return toc;
};
module.exports = function(props){ module.exports = function(props){
const pages = props.brew.text.split('\\page'); const pages = props.brew.text.split('\\page');
const TOC = getTOC(pages); const TOC = getTOC(pages);
const markdown = _.reduce(TOC, (r, g1, idx1)=>{ const markdown = _.reduce(TOC, (r, g1, idx1)=>{
if(g1.title !== null) { r.push(ToCIterate(g1).join('\n'));
r.push(`- ### [{{ ${g1.title}}}{{ ${g1.page}}}](#p${g1.page})`);
}
if(g1.children.length){
_.each(g1.children, (g2, idx2)=>{
if(g2.title !== null) {
r.push(` - #### [{{ ${g2.title}}}{{ ${g2.page}}}](#p${g2.page})`);
}
if(g2.children.length){
_.each(g2.children, (g3, idx3)=>{
if(g2.title !== null) {
r.push(` - [{{ ${g3.title}}}{{ ${g3.page}}}](#p${g3.page})`);
} else { // Don't over-indent if no level-2 parent entry
r.push(` - [{{ ${g3.title}}}{{ ${g3.page}}}](#p${g3.page})`);
}
});
}
});
}
return r; return r;
}, []).join('\n'); }, []).join('\n');

View File

@@ -786,6 +786,39 @@
// ***************************** // *****************************
// * TABLE OF CONTENTS // * TABLE OF CONTENTS
// *****************************/ // *****************************/
// Default Exclusions
// Anything not exlcuded is included, default Headers are H1, H2, and H3.
h4,
h5,
h6,
.page:has(.frontCover),
.page:has(.backCover),
.page:has(.insideCover),
.monster,
.noToC,
.toc { --TOC: exclude; }
.tocDepthH2 :is(h1, h2) {--TOC: include; }
.tocDepthH3 :is(h1, h2, h3) {--TOC: include; }
.tocDepthH4 :is(h1, h2, h3, h4) {--TOC: include; }
.tocDepthH5 :is(h1, h2, h3, h4, h5) {--TOC: include; }
.tocDepthH6 :is(h1, h2, h3, h4, h5, h6) {--TOC: include; }
.tocIncludeH1 h1 {--TOC: include; }
.tocIncludeH2 h2 {--TOC: include; }
.tocIncludeH3 h3 {--TOC: include; }
.tocIncludeH4 h4 {--TOC: include; }
.tocIncludeH5 h5 {--TOC: include; }
.tocIncludeH6 h6 {--TOC: include; }
.page:has(.partCover) {
--TOC: exclude;
& h1 {
--TOC: include;
}
}
.page { .page {
&:has(.toc)::after { display : none; } &:has(.toc)::after { display : none; }
.toc { .toc {

View File

@@ -3,6 +3,7 @@
@import (less) './themes/fonts/iconFonts/elderberryInn.less'; @import (less) './themes/fonts/iconFonts/elderberryInn.less';
@import (less) './themes/fonts/iconFonts/diceFont.less'; @import (less) './themes/fonts/iconFonts/diceFont.less';
@import (less) './themes/fonts/iconFonts/gameIcons.less'; @import (less) './themes/fonts/iconFonts/gameIcons.less';
@import (less) './themes/fonts/iconFonts/fontAwesome.less';
:root { :root {
//Colors //Colors

View File

@@ -58,7 +58,7 @@
background-color: rgba(35,153,153,0.5); background-color: rgba(35,153,153,0.5);
} }
.pageLine { .pageLine {
background-color: rgba(255,255,255,0.75); background-color: rgba(255,255,255,0.5);
& ~ pre.CodeMirror-line { & ~ pre.CodeMirror-line {
color: black; color: black;
} }

View File

@@ -74,8 +74,9 @@
@font-face { @font-face {
font-family: SolberaImitationRemake; //Tweaked 5e version font-family: SolberaImitationRemake; //Tweaked 5e version
src: url('../../../fonts/5e/Solbera Imitation Tweak.woff2'); src: url('../../../fonts/5e/Solbera Imitation Tweak.woff2');
font-weight: normal; font-weight: 100 1000;
font-style: normal; font-style: normal;
font-style: italic;
} }
/* Cover Page */ /* Cover Page */

View File

@@ -7,7 +7,7 @@
} }
.df { .df {
display : inline-block; display : inline;
font-family : 'DiceFont'; font-family : 'DiceFont';
font-style : normal; font-style : normal;
font-weight : normal; font-weight : normal;
@@ -16,8 +16,11 @@
text-decoration : inherit; text-decoration : inherit;
text-transform : none; text-transform : none;
text-rendering : optimizeLegibility; text-rendering : optimizeLegibility;
-moz-osx-font-smoothing : grayscale;
/* Better Font Rendering =========== */
-webkit-font-smoothing : antialiased; -webkit-font-smoothing : antialiased;
-moz-osx-font-smoothing : grayscale;
&.F::before { content : '\f190'; } &.F::before { content : '\f190'; }
&.F-minus::before { content : '\f191'; } &.F-minus::before { content : '\f191'; }
&.F-plus::before { content : '\f192'; } &.F-plus::before { content : '\f192'; }

View File

@@ -7,15 +7,16 @@
} }
.ei { .ei {
display : inline-block; display : inline;
margin-right : 3px;
font-family : 'Elderberry-Inn'; font-family : 'Elderberry-Inn';
line-height : 1; line-height : 1;
vertical-align : baseline; vertical-align : baseline;
-moz-osx-font-smoothing : grayscale;
-webkit-font-smoothing : antialiased;
text-rendering : auto; text-rendering : auto;
/* Better Font Rendering =========== */
-webkit-font-smoothing : antialiased;
-moz-osx-font-smoothing : grayscale;
&.book::before { content : '\E900'; } &.book::before { content : '\E900'; }
&.screen::before { content : '\E901'; } &.screen::before { content : '\E901'; }

View File

@@ -0,0 +1,2 @@
/* Icon Font: Font Awesome */
.far,.fas,.fab { display : inline; }

View File

@@ -8,20 +8,16 @@
.gi { .gi {
/* use !important to prevent issues with browser extensions that change fonts */ /* use !important to prevent issues with browser extensions that change fonts */
display : inline-block; display : inline;
margin-right : 3px;
font-family : 'Game-Icons' !important; font-family : 'Game-Icons' !important;
line-height : 1; line-height : 1;
vertical-align : baseline; vertical-align : baseline;
-moz-osx-font-smoothing : grayscale;
-webkit-font-smoothing : antialiased;
text-rendering : auto; text-rendering : auto;
/* Better Font Rendering =========== */ /* Better Font Rendering =========== */
-webkit-font-smoothing : antialiased; -webkit-font-smoothing : antialiased;
-moz-osx-font-smoothing : grayscale; -moz-osx-font-smoothing : grayscale;
&.zigzag-leaf::before { content : '\e900'; } &.zigzag-leaf::before { content : '\e900'; }
&.zebra-shield::before { content : '\e901'; } &.zebra-shield::before { content : '\e901'; }
&.x-mark::before { content : '\e902'; } &.x-mark::before { content : '\e902'; }