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:
49
changelog.md
49
changelog.md
@@ -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
|
||||||
|
|
||||||
|
|||||||
29
client/components/dialog.jsx
Normal file
29
client/components/dialog.jsx
Normal 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;
|
||||||
@@ -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'/>
|
||||||
|
|||||||
@@ -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!
|
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!
|
||||||
<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;
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
@@ -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
1576
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
26
package.json
26
package.json
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 }));
|
||||||
|
|
||||||
|
|||||||
@@ -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>;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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;}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 = ` {position:absolute,bottom:20px,left:130px,width:220px,a="b and c",d=e}`;
|
const source = ` {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 = ` {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 = ` {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', ()=>{
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 */
|
||||||
|
|||||||
@@ -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'; }
|
||||||
|
|||||||
@@ -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'; }
|
||||||
|
|
||||||
|
|||||||
2
themes/fonts/iconFonts/fontAwesome.less
Normal file
2
themes/fonts/iconFonts/fontAwesome.less
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
/* Icon Font: Font Awesome */
|
||||||
|
.far,.fas,.fab { display : inline; }
|
||||||
@@ -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'; }
|
||||||
|
|||||||
Reference in New Issue
Block a user