mirror of
https://github.com/naturalcrit/homebrewery.git
synced 2026-01-17 18:52:40 +00:00
Merge branch 'master' of https://github.com/naturalcrit/homebrewery into FAQ-update
This commit is contained in:
@@ -15,7 +15,7 @@ module.exports = {
|
||||
rules : {
|
||||
/** Errors **/
|
||||
'camelcase' : ['error', { properties: 'never' }],
|
||||
'func-style' : ['error', 'expression', { allowArrowFunctions: true }],
|
||||
//'func-style' : ['error', 'expression', { allowArrowFunctions: true }],
|
||||
'no-array-constructor' : 'error',
|
||||
'no-iterator' : 'error',
|
||||
'no-nested-ternary' : 'error',
|
||||
|
||||
57
changelog.md
57
changelog.md
@@ -80,6 +80,63 @@ pre {
|
||||
## changelog
|
||||
For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery).
|
||||
|
||||
### Thursday 17/08/2023 - v3.9.2
|
||||
{{taskList
|
||||
|
||||
##### Calculuschild
|
||||
|
||||
* [x] Fix links to certain old Google Drive files
|
||||
|
||||
Fixes issue [#2917](https://github.com/naturalcrit/homebrewery/issues/2917)
|
||||
|
||||
##### G-Ambatte
|
||||
|
||||
* [x] Menus now open on click, and internally consistent
|
||||
|
||||
Fixes issue [#2702](https://github.com/naturalcrit/homebrewery/issues/2702), [#2782](https://github.com/naturalcrit/homebrewery/issues/2782)
|
||||
|
||||
* [x] Add smarter footer snippet
|
||||
|
||||
Fixes issue [#2289](https://github.com/naturalcrit/homebrewery/issues/2289)
|
||||
|
||||
* [x] Add sanitization in Style editor
|
||||
|
||||
Fixes issue [#1437](https://github.com/naturalcrit/homebrewery/issues/1437)
|
||||
|
||||
* [x] Rework class table snippets to remove unnecessary randomness
|
||||
|
||||
Fixes issue [#2964](https://github.com/naturalcrit/homebrewery/issues/2964)
|
||||
|
||||
* [x] Add User Page link to Google Drive file for file owners, add icons for additional storage locations
|
||||
|
||||
Fixes issue [#2954](https://github.com/naturalcrit/homebrewery/issues/2954)
|
||||
|
||||
* [x] Add default save location selection to Account Page
|
||||
|
||||
Fixes issue [#2943](https://github.com/naturalcrit/homebrewery/issues/2943)
|
||||
|
||||
##### 5e-Cleric
|
||||
|
||||
* [x] Exclude cover pages from Table of Content generation (editing on mobile is still not recommended)
|
||||
|
||||
Fixes issue [#2920](https://github.com/naturalcrit/homebrewery/issues/2920)
|
||||
|
||||
##### Gazook89
|
||||
|
||||
* [x] Adjustments to improve mobile viewing
|
||||
|
||||
}}
|
||||
|
||||
### Wednesday 28/06/2023 - v3.9.1
|
||||
{{taskList
|
||||
|
||||
##### G-Ambatte
|
||||
|
||||
* [x] Better error pages with more useful information
|
||||
|
||||
Fixes issue [#1924](https://github.com/naturalcrit/homebrewery/issues/1924)
|
||||
}}
|
||||
|
||||
### Friday 02/06/2023 - v3.9.0
|
||||
{{taskList
|
||||
|
||||
|
||||
@@ -108,6 +108,12 @@ const BrewRenderer = createClass({
|
||||
return false;
|
||||
},
|
||||
|
||||
sanitizeScriptTags : function(content) {
|
||||
return content
|
||||
.replace(/<script/ig, '<script')
|
||||
.replace(/<\/script>/ig, '</script>');
|
||||
},
|
||||
|
||||
renderPageInfo : function(){
|
||||
return <div className='pageInfo' ref='main'>
|
||||
<div>
|
||||
@@ -135,18 +141,20 @@ const BrewRenderer = createClass({
|
||||
|
||||
renderStyle : function() {
|
||||
if(!this.props.style) return;
|
||||
//return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `<style>@layer styleTab {\n${this.props.style}\n} </style>` }} />;
|
||||
return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `<style>\n${this.props.style}\n</style>` }} />;
|
||||
const cleanStyle = this.sanitizeScriptTags(this.props.style);
|
||||
//return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `<style>@layer styleTab {\n${this.sanitizeScriptTags(this.props.style)}\n} </style>` }} />;
|
||||
return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `<style> ${cleanStyle} </style>` }} />;
|
||||
},
|
||||
|
||||
renderPage : function(pageText, index){
|
||||
let cleanPageText = this.sanitizeScriptTags(pageText);
|
||||
if(this.props.renderer == 'legacy')
|
||||
return <div className='phb page' id={`p${index + 1}`} dangerouslySetInnerHTML={{ __html: MarkdownLegacy.render(pageText) }} key={index} />;
|
||||
return <div className='phb page' id={`p${index + 1}`} dangerouslySetInnerHTML={{ __html: MarkdownLegacy.render(cleanPageText) }} key={index} />;
|
||||
else {
|
||||
pageText += `\n\n \n\\column\n `; //Artificial column break at page end to emulate column-fill:auto (until `wide` is used, when column-fill:balance will reappear)
|
||||
cleanPageText += `\n\n \n\\column\n `; //Artificial column break at page end to emulate column-fill:auto (until `wide` is used, when column-fill:balance will reappear)
|
||||
return (
|
||||
<div className='page' id={`p${index + 1}`} key={index} >
|
||||
<div className='columnWrapper' dangerouslySetInnerHTML={{ __html: Markdown.render(pageText) }} />
|
||||
<div className='columnWrapper' dangerouslySetInnerHTML={{ __html: Markdown.render(cleanPageText) }} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -185,6 +193,12 @@ const BrewRenderer = createClass({
|
||||
}, 100);
|
||||
},
|
||||
|
||||
emitClick : function(){
|
||||
// console.log('iFrame clicked');
|
||||
if(!window || !document) return;
|
||||
document.dispatchEvent(new MouseEvent('click'));
|
||||
},
|
||||
|
||||
render : function(){
|
||||
//render in iFrame so broken code doesn't crash the site.
|
||||
//Also render dummy page while iframe is mounting.
|
||||
@@ -203,7 +217,9 @@ const BrewRenderer = createClass({
|
||||
|
||||
<Frame id='BrewRenderer' initialContent={this.state.initialContent}
|
||||
style={{ width: '100%', height: '100%', visibility: this.state.visibility }}
|
||||
contentDidMount={this.frameDidMount}>
|
||||
contentDidMount={this.frameDidMount}
|
||||
onClick={()=>{this.emitClick();}}
|
||||
>
|
||||
<div className={'brewRenderer'}
|
||||
onScroll={this.handleScroll}
|
||||
style={{ height: this.state.height }}>
|
||||
|
||||
@@ -10,6 +10,8 @@ const CodeEditor = require('naturalcrit/codeEditor/codeEditor.jsx');
|
||||
const SnippetBar = require('./snippetbar/snippetbar.jsx');
|
||||
const MetadataEditor = require('./metadataEditor/metadataEditor.jsx');
|
||||
|
||||
const EDITOR_THEME_KEY = 'HOMEBREWERY-EDITOR-THEME';
|
||||
|
||||
const SNIPPETBAR_HEIGHT = 25;
|
||||
const DEFAULT_STYLE_TEXT = dedent`
|
||||
/*=======--- Example CSS styling ---=======*/
|
||||
@@ -34,12 +36,14 @@ const Editor = createClass({
|
||||
onMetaChange : ()=>{},
|
||||
reportError : ()=>{},
|
||||
|
||||
renderer : 'legacy'
|
||||
editorTheme : 'default',
|
||||
renderer : 'legacy'
|
||||
};
|
||||
},
|
||||
getInitialState : function() {
|
||||
return {
|
||||
view : 'text' //'text', 'style', 'meta'
|
||||
editorTheme : this.props.editorTheme,
|
||||
view : 'text' //'text', 'style', 'meta'
|
||||
};
|
||||
},
|
||||
|
||||
@@ -51,6 +55,13 @@ const Editor = createClass({
|
||||
this.updateEditorSize();
|
||||
this.highlightCustomMarkdown();
|
||||
window.addEventListener('resize', this.updateEditorSize);
|
||||
|
||||
const editorTheme = window.localStorage.getItem(EDITOR_THEME_KEY);
|
||||
if(editorTheme) {
|
||||
this.setState({
|
||||
editorTheme : editorTheme
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
componentWillUnmount : function() {
|
||||
@@ -255,6 +266,13 @@ const Editor = createClass({
|
||||
this.refs.codeEditor?.updateSize();
|
||||
},
|
||||
|
||||
updateEditorTheme : function(newTheme){
|
||||
window.localStorage.setItem(EDITOR_THEME_KEY, newTheme);
|
||||
this.setState({
|
||||
editorTheme : newTheme
|
||||
});
|
||||
},
|
||||
|
||||
//Called by CodeEditor after document switch, so Snippetbar can refresh UndoHistory
|
||||
rerenderParent : function (){
|
||||
this.forceUpdate();
|
||||
@@ -269,6 +287,7 @@ const Editor = createClass({
|
||||
view={this.state.view}
|
||||
value={this.props.brew.text}
|
||||
onChange={this.props.onTextChange}
|
||||
editorTheme={this.state.editorTheme}
|
||||
rerenderParent={this.rerenderParent} />
|
||||
</>;
|
||||
}
|
||||
@@ -281,6 +300,7 @@ const Editor = createClass({
|
||||
value={this.props.brew.style ?? DEFAULT_STYLE_TEXT}
|
||||
onChange={this.props.onStyleChange}
|
||||
enableFolding={false}
|
||||
editorTheme={this.state.editorTheme}
|
||||
rerenderParent={this.rerenderParent} />
|
||||
</>;
|
||||
}
|
||||
@@ -323,7 +343,10 @@ const Editor = createClass({
|
||||
theme={this.props.brew.theme}
|
||||
undo={this.undo}
|
||||
redo={this.redo}
|
||||
historySize={this.historySize()} />
|
||||
historySize={this.historySize()}
|
||||
currentEditorTheme={this.state.editorTheme}
|
||||
updateEditorTheme={this.updateEditorTheme}
|
||||
cursorPos={this.refs.codeEditor?.getCursorPosition() || {}} />
|
||||
|
||||
{this.renderEditor()}
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
|
||||
@import 'themes/codeMirror/customEditorStyles.less';
|
||||
.editor{
|
||||
position : relative;
|
||||
width : 100%;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/*eslint max-lines: ["warn", {"max": 250, "skipBlankLines": true, "skipComments": true}]*/
|
||||
require('./snippetbar.less');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
@@ -15,8 +16,10 @@ ThemeSnippets['V3_5eDMG'] = require('themes/V3/5eDMG/snippets.js');
|
||||
ThemeSnippets['V3_Journal'] = require('themes/V3/Journal/snippets.js');
|
||||
ThemeSnippets['V3_Blank'] = require('themes/V3/Blank/snippets.js');
|
||||
|
||||
const execute = function(val, brew){
|
||||
if(_.isFunction(val)) return val(brew);
|
||||
const EditorThemes = require('build/homebrew/codeMirror/editorThemes.json');
|
||||
|
||||
const execute = function(val, props){
|
||||
if(_.isFunction(val)) return val(props);
|
||||
return val;
|
||||
};
|
||||
|
||||
@@ -24,23 +27,26 @@ const Snippetbar = createClass({
|
||||
displayName : 'SnippetBar',
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
brew : {},
|
||||
view : 'text',
|
||||
onViewChange : ()=>{},
|
||||
onInject : ()=>{},
|
||||
onToggle : ()=>{},
|
||||
showEditButtons : true,
|
||||
renderer : 'legacy',
|
||||
undo : ()=>{},
|
||||
redo : ()=>{},
|
||||
historySize : ()=>{}
|
||||
brew : {},
|
||||
view : 'text',
|
||||
onViewChange : ()=>{},
|
||||
onInject : ()=>{},
|
||||
onToggle : ()=>{},
|
||||
showEditButtons : true,
|
||||
renderer : 'legacy',
|
||||
undo : ()=>{},
|
||||
redo : ()=>{},
|
||||
historySize : ()=>{},
|
||||
updateEditorTheme : ()=>{},
|
||||
cursorPos : {}
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState : function() {
|
||||
return {
|
||||
renderer : this.props.renderer,
|
||||
snippets : []
|
||||
renderer : this.props.renderer,
|
||||
themeSelector : false,
|
||||
snippets : []
|
||||
};
|
||||
},
|
||||
|
||||
@@ -94,6 +100,31 @@ const Snippetbar = createClass({
|
||||
this.props.onInject(injectedText);
|
||||
},
|
||||
|
||||
toggleThemeSelector : function(){
|
||||
this.setState({
|
||||
themeSelector : !this.state.themeSelector
|
||||
});
|
||||
},
|
||||
|
||||
changeTheme : function(e){
|
||||
if(e.target.value == this.props.currentEditorTheme) return;
|
||||
this.props.updateEditorTheme(e.target.value);
|
||||
|
||||
this.setState({
|
||||
showThemeSelector : false,
|
||||
});
|
||||
},
|
||||
|
||||
renderThemeSelector : function(){
|
||||
return <div className='themeSelector'>
|
||||
<select value={this.props.currentEditorTheme} onChange={this.changeTheme} onMouseDown={(this.changeTheme)}>
|
||||
{EditorThemes.map((theme, key)=>{
|
||||
return <option key={key} value={theme}>{theme}</option>;
|
||||
})}
|
||||
</select>
|
||||
</div>;
|
||||
},
|
||||
|
||||
renderSnippetGroups : function(){
|
||||
const snippets = this.state.snippets.filter((snippetGroup)=>snippetGroup.view === this.props.view);
|
||||
|
||||
@@ -105,6 +136,7 @@ const Snippetbar = createClass({
|
||||
snippets={snippetGroup.snippets}
|
||||
key={snippetGroup.groupName}
|
||||
onSnippetClick={this.handleSnippetClick}
|
||||
cursorPos={this.props.cursorPos}
|
||||
/>;
|
||||
});
|
||||
},
|
||||
@@ -122,6 +154,12 @@ const Snippetbar = createClass({
|
||||
<i className='fas fa-redo' />
|
||||
</div>
|
||||
<div className='divider'></div>
|
||||
<div className={`editorTool editorTheme ${this.state.themeSelector ? 'active' : ''}`}
|
||||
onClick={this.toggleThemeSelector} >
|
||||
<i className='fas fa-palette' />
|
||||
</div>
|
||||
{this.state.themeSelector && this.renderThemeSelector()}
|
||||
<div className='divider'></div>
|
||||
<div className={cx('text', { selected: this.props.view === 'text' })}
|
||||
onClick={()=>this.props.onViewChange('text')}>
|
||||
<i className='fa fa-beer' />
|
||||
@@ -165,7 +203,7 @@ const SnippetGroup = createClass({
|
||||
},
|
||||
handleSnippetClick : function(e, snippet){
|
||||
e.stopPropagation();
|
||||
this.props.onSnippetClick(execute(snippet.gen, this.props.brew));
|
||||
this.props.onSnippetClick(execute(snippet.gen, this.props));
|
||||
},
|
||||
renderSnippets : function(snippets){
|
||||
return _.map(snippets, (snippet)=>{
|
||||
@@ -194,5 +232,4 @@ const SnippetGroup = createClass({
|
||||
</div>
|
||||
</div>;
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
@@ -46,6 +46,15 @@
|
||||
color : black;
|
||||
}
|
||||
}
|
||||
&.editorTheme{
|
||||
.tooltipLeft('Editor Themes');
|
||||
font-size : 0.75em;
|
||||
color : black;
|
||||
&.active{
|
||||
color : white;
|
||||
background-color: black;
|
||||
}
|
||||
}
|
||||
&.divider {
|
||||
background: linear-gradient(#000, #000) no-repeat center/1px 100%;
|
||||
width: 5px;
|
||||
@@ -54,6 +63,15 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
.themeSelector{
|
||||
position: absolute;
|
||||
left: -65px;
|
||||
top: 30px;
|
||||
z-index: 999;
|
||||
width: 170px;
|
||||
background-color: black;
|
||||
border-radius: 5px;
|
||||
}
|
||||
}
|
||||
.snippetBarButton{
|
||||
height : @menuHeight;
|
||||
|
||||
@@ -9,7 +9,7 @@ const EditPage = require('./pages/editPage/editPage.jsx');
|
||||
const UserPage = require('./pages/userPage/userPage.jsx');
|
||||
const SharePage = require('./pages/sharePage/sharePage.jsx');
|
||||
const NewPage = require('./pages/newPage/newPage.jsx');
|
||||
//const ErrorPage = require('./pages/errorPage/errorPage.jsx');
|
||||
const ErrorPage = require('./pages/errorPage/errorPage.jsx');
|
||||
const PrintPage = require('./pages/printPage/printPage.jsx');
|
||||
const AccountPage = require('./pages/accountPage/accountPage.jsx');
|
||||
|
||||
@@ -78,6 +78,7 @@ const Homebrew = createClass({
|
||||
<Route path='/faq' element={<WithRoute el={SharePage} brew={this.props.brew} />} />
|
||||
<Route path='/account' element={<WithRoute el={AccountPage} brew={this.props.brew} uiItems={this.props.brew.uiItems} />} />
|
||||
<Route path='/legacy' element={<WithRoute el={HomePage} brew={this.props.brew} />} />
|
||||
<Route path='/error' element={<WithRoute el={ErrorPage} brew={this.props.brew} />} />
|
||||
<Route path='/' element={<WithRoute el={HomePage} brew={this.props.brew} />} />
|
||||
<Route path='/*' element={<WithRoute el={HomePage} brew={this.props.brew} />} />
|
||||
</Routes>
|
||||
|
||||
@@ -81,20 +81,70 @@
|
||||
color : pink;
|
||||
}
|
||||
}
|
||||
.recent.navItem {
|
||||
.recent.navDropdownContainer {
|
||||
position : relative;
|
||||
.dropdown {
|
||||
position : absolute;
|
||||
z-index : 10000;
|
||||
top : 28px;
|
||||
left : 0;
|
||||
.navDropdown .navItem {
|
||||
overflow : hidden auto;
|
||||
width : 100%;
|
||||
max-height : ~"calc(100vh - 28px)";
|
||||
scrollbar-color : #666 #333;
|
||||
scrollbar-width : thin;
|
||||
h4 {
|
||||
font-size : 0.8em;
|
||||
|
||||
|
||||
#backgroundColorsHover;
|
||||
.animate(background-color);
|
||||
position : relative;
|
||||
display : block;
|
||||
overflow : clip;
|
||||
box-sizing : border-box;
|
||||
padding : 8px 5px 13px;
|
||||
text-decoration : none;
|
||||
color : white;
|
||||
border-top : 1px solid #888;
|
||||
background-color : #333;
|
||||
.clear {
|
||||
position : absolute;
|
||||
top : 50%;
|
||||
right : 0;
|
||||
display : none;
|
||||
width : 20px;
|
||||
height : 100%;
|
||||
transform : translateY(-50%);
|
||||
opacity : 70%;
|
||||
border-radius : 3px;
|
||||
background-color : #333;
|
||||
&:hover {
|
||||
opacity : 100%;
|
||||
}
|
||||
i {
|
||||
font-size : 10px;
|
||||
width : 100%;
|
||||
height : 100%;
|
||||
margin : 0;
|
||||
text-align : center;
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
background-color : @blue;
|
||||
.clear {
|
||||
display : grid;
|
||||
place-content : center;
|
||||
}
|
||||
}
|
||||
.title {
|
||||
display : inline-block;
|
||||
overflow : hidden;
|
||||
width : 100%;
|
||||
white-space : nowrap;
|
||||
text-overflow : ellipsis;
|
||||
}
|
||||
.time {
|
||||
font-size : 0.7em;
|
||||
position : absolute;
|
||||
right : 2px;
|
||||
bottom : 2px;
|
||||
color : #888;
|
||||
}
|
||||
&.header {
|
||||
display : block;
|
||||
box-sizing : border-box;
|
||||
padding : 5px 0;
|
||||
@@ -109,62 +159,6 @@
|
||||
background-color : darken(@purple, 30%);
|
||||
}
|
||||
}
|
||||
.item {
|
||||
#backgroundColorsHover;
|
||||
.animate(background-color);
|
||||
position : relative;
|
||||
display : block;
|
||||
overflow : clip;
|
||||
box-sizing : border-box;
|
||||
padding : 8px 5px 13px;
|
||||
text-decoration : none;
|
||||
color : white;
|
||||
border-top : 1px solid #888;
|
||||
background-color : #333;
|
||||
.clear {
|
||||
position : absolute;
|
||||
top : 50%;
|
||||
right : 0;
|
||||
display : none;
|
||||
width : 20px;
|
||||
height : 100%;
|
||||
transform : translateY(-50%);
|
||||
opacity : 70%;
|
||||
border-radius : 3px;
|
||||
background-color : #333;
|
||||
&:hover {
|
||||
opacity : 100%;
|
||||
}
|
||||
i {
|
||||
font-size : 10px;
|
||||
width : 100%;
|
||||
height : 100%;
|
||||
margin : 0;
|
||||
text-align : center;
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
background-color : @blue;
|
||||
.clear {
|
||||
display : grid;
|
||||
place-content : center;
|
||||
}
|
||||
}
|
||||
.title {
|
||||
display : inline-block;
|
||||
overflow : hidden;
|
||||
width : 100%;
|
||||
white-space : nowrap;
|
||||
text-overflow : ellipsis;
|
||||
}
|
||||
.time {
|
||||
font-size : 0.7em;
|
||||
position : absolute;
|
||||
right : 2px;
|
||||
bottom : 2px;
|
||||
color : #888;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.metadata.navItem {
|
||||
|
||||
@@ -121,6 +121,7 @@ const RecentItems = createClass({
|
||||
|
||||
removeItem : function(url, evt){
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
|
||||
let edited = JSON.parse(localStorage.getItem(EDIT_KEY) || '[]');
|
||||
let viewed = JSON.parse(localStorage.getItem(VIEW_KEY) || '[]');
|
||||
@@ -139,11 +140,11 @@ const RecentItems = createClass({
|
||||
},
|
||||
|
||||
renderDropdown : function(){
|
||||
if(!this.state.showDropdown) return null;
|
||||
// if(!this.state.showDropdown) return null;
|
||||
|
||||
const makeItems = (brews)=>{
|
||||
return _.map(brews, (brew, i)=>{
|
||||
return <a href={brew.url} className='item' key={`${brew.id}-${i}`} target='_blank' rel='noopener noreferrer' title={brew.title || '[ no title ]'}>
|
||||
return <a className='navItem' href={brew.url} key={`${brew.id}-${i}`} target='_blank' rel='noopener noreferrer' title={brew.title || '[ no title ]'}>
|
||||
<span className='title'>{brew.title || '[ no title ]'}</span>
|
||||
<span className='time'>{Moment(brew.ts).fromNow()}</span>
|
||||
<div className='clear' title='Remove from Recents' onClick={(e)=>{this.removeItem(`${brew.url}`, e);}}><i className='fas fa-times'></i></div>
|
||||
@@ -151,25 +152,25 @@ const RecentItems = createClass({
|
||||
});
|
||||
};
|
||||
|
||||
return <div className='dropdown'>
|
||||
return <>
|
||||
{(this.props.showEdit && this.props.showView) ?
|
||||
<h4>edited</h4> : null }
|
||||
<Nav.item className='header'>edited</Nav.item> : null }
|
||||
{this.props.showEdit ?
|
||||
makeItems(this.state.edit) : null }
|
||||
{(this.props.showEdit && this.props.showView) ?
|
||||
<h4>viewed</h4> : null }
|
||||
<Nav.item className='header'>viewed</Nav.item> : null }
|
||||
{this.props.showView ?
|
||||
makeItems(this.state.view) : null }
|
||||
</div>;
|
||||
</>;
|
||||
},
|
||||
|
||||
render : function(){
|
||||
return <Nav.item icon='fas fa-history' color='grey' className='recent'
|
||||
onMouseEnter={()=>this.handleDropdown(true)}
|
||||
onMouseLeave={()=>this.handleDropdown(false)}>
|
||||
{this.props.text}
|
||||
return <Nav.dropdown className='recent'>
|
||||
<Nav.item icon='fas fa-history' color='grey' >
|
||||
{this.props.text}
|
||||
</Nav.item>
|
||||
{this.renderDropdown()}
|
||||
</Nav.item>;
|
||||
</Nav.dropdown>;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
@@ -16,6 +16,8 @@ const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
||||
|
||||
const NaturalCritIcon = require('naturalcrit/svg/naturalcrit.svg.jsx');
|
||||
|
||||
let SAVEKEY = '';
|
||||
|
||||
const AccountPage = createClass({
|
||||
displayName : 'AccountPage',
|
||||
getDefaultProps : function() {
|
||||
@@ -29,6 +31,27 @@ const AccountPage = createClass({
|
||||
uiItems : this.props.uiItems
|
||||
};
|
||||
},
|
||||
componentDidMount : function(){
|
||||
if(!this.state.saveLocation && this.props.uiItems.username) {
|
||||
SAVEKEY = `HOMEBREWERY-DEFAULT-SAVE-LOCATION-${this.props.uiItems.username}`;
|
||||
let saveLocation = window.localStorage.getItem(SAVEKEY);
|
||||
saveLocation = saveLocation ?? (this.state.uiItems.googleId ? 'GOOGLE-DRIVE' : 'HOMEBREWERY');
|
||||
this.makeActive(saveLocation);
|
||||
}
|
||||
},
|
||||
|
||||
makeActive : function(newSelection){
|
||||
if(this.state.saveLocation == newSelection) return;
|
||||
window.localStorage.setItem(SAVEKEY, newSelection);
|
||||
this.setState({
|
||||
saveLocation : newSelection
|
||||
});
|
||||
},
|
||||
|
||||
renderButton : function(name, key, shouldRender=true){
|
||||
if(!shouldRender) return;
|
||||
return <button className={this.state.saveLocation==key ? 'active' : ''} onClick={()=>{this.makeActive(key);}}>{name}</button>;
|
||||
},
|
||||
|
||||
renderNavItems : function() {
|
||||
return <Navbar>
|
||||
@@ -61,6 +84,11 @@ const AccountPage = createClass({
|
||||
</p>
|
||||
}
|
||||
</div>
|
||||
<div className='dataGroup'>
|
||||
<h4>Default Save Location</h4>
|
||||
{this.renderButton('Homebrewery', 'HOMEBREWERY')}
|
||||
{this.renderButton('Google Drive', 'GOOGLE-DRIVE', this.state.uiItems.googleId)}
|
||||
</div>
|
||||
</>;
|
||||
},
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ const moment = require('moment');
|
||||
const request = require('../../../../utils/request-middleware.js');
|
||||
|
||||
const googleDriveIcon = require('../../../../googleDrive.svg');
|
||||
const homebreweryIcon = require('../../../../thumbnail.png');
|
||||
const dedent = require('dedent-tabs').default;
|
||||
|
||||
const BrewItem = createClass({
|
||||
@@ -90,11 +91,17 @@ const BrewItem = createClass({
|
||||
</a>;
|
||||
},
|
||||
|
||||
renderGoogleDriveIcon : function(){
|
||||
if(!this.props.brew.googleId) return;
|
||||
renderStorageIcon : function(){
|
||||
if(this.props.brew.googleId) {
|
||||
return <span title={this.props.brew.webViewLink ? 'Your Google Drive Storage': 'Another User\'s Google Drive Storage'}>
|
||||
<a href={this.props.brew.webViewLink} target='_blank'>
|
||||
<img className='googleDriveIcon' src={googleDriveIcon} alt='googleDriveIcon' />
|
||||
</a>
|
||||
</span>;
|
||||
}
|
||||
|
||||
return <span>
|
||||
<img className='googleDriveIcon' src={googleDriveIcon} alt='googleDriveIcon' />
|
||||
return <span title='Homebrewery Storage'>
|
||||
<img className='homebreweryIcon' src={homebreweryIcon} alt='homebreweryIcon' />
|
||||
</span>;
|
||||
},
|
||||
|
||||
@@ -144,7 +151,7 @@ const BrewItem = createClass({
|
||||
Last updated: ${moment(brew.updatedAt).local().format(dateFormatString)}`}>
|
||||
<i className='fas fa-sync-alt' /> {moment(brew.updatedAt).fromNow()}
|
||||
</span>
|
||||
{this.renderGoogleDriveIcon()}
|
||||
{this.renderStorageIcon()}
|
||||
</div>
|
||||
|
||||
<div className='links'>
|
||||
|
||||
@@ -98,4 +98,11 @@
|
||||
padding : 0px;
|
||||
margin : -5px;
|
||||
}
|
||||
.homebreweryIcon {
|
||||
mix-blend-mode : darken;
|
||||
height : 24px;
|
||||
position : relative;
|
||||
top : 5px;
|
||||
left : -5px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ const ListPage = createClass({
|
||||
sortBrewOrder : function(brew){
|
||||
if(!brew.title){brew.title = 'No Title';}
|
||||
const mapping = {
|
||||
'alpha' : _.deburr(brew.title.toLowerCase()),
|
||||
'alpha' : _.deburr(brew.title.trim().toLowerCase()),
|
||||
'created' : moment(brew.createdAt).format(),
|
||||
'updated' : moment(brew.updatedAt).format(),
|
||||
'views' : brew.views,
|
||||
|
||||
@@ -1,47 +1,69 @@
|
||||
.uiPage{
|
||||
.content{
|
||||
overflow-y : hidden;
|
||||
width : 90vw;
|
||||
background-color: #f0f0f0;
|
||||
font-family: 'Open Sans';
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin-top: 25px;
|
||||
padding: 2% 4%;
|
||||
font-size: 0.8em;
|
||||
line-height: 1.8em;
|
||||
.dataGroup{
|
||||
padding: 6px 20px 15px;
|
||||
border: 2px solid black;
|
||||
border-radius: 5px;
|
||||
margin: 5px 0px;
|
||||
}
|
||||
h1, h2, h3, h4{
|
||||
font-weight: 900;
|
||||
text-transform: uppercase;
|
||||
margin: 0.5em 30% 0.25em 0;
|
||||
border-bottom: 2px solid slategrey;
|
||||
}
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
border-bottom: 2px solid darkslategrey;
|
||||
margin-bottom: 0.5em;
|
||||
margin-right: 0;
|
||||
}
|
||||
h2 {
|
||||
font-size: 1.75em;
|
||||
}
|
||||
h3 {
|
||||
font-size: 1.5em;
|
||||
svg {
|
||||
width: 19px;
|
||||
.homebrew {
|
||||
.uiPage.sitePage {
|
||||
.content {
|
||||
width : ~"min(90vw, 1000px)";
|
||||
padding : 2% 4%;
|
||||
margin-top : 25px;
|
||||
margin-right : auto;
|
||||
margin-left : auto;
|
||||
overflow-y : scroll;
|
||||
font-family : 'Open Sans';
|
||||
font-size : 0.8em;
|
||||
line-height : 1.8em;
|
||||
background-color : #F0F0F0;
|
||||
.dataGroup {
|
||||
padding : 6px 20px 15px;
|
||||
margin : 5px 0px;
|
||||
border : 2px solid black;
|
||||
border-radius : 5px;
|
||||
button {
|
||||
background-color : transparent;
|
||||
border : 1px solid black;
|
||||
border-radius : 5px;
|
||||
width : 125px;
|
||||
color : black;
|
||||
margin-right : 5px;
|
||||
&.active {
|
||||
background-color: #0007;
|
||||
color: white;
|
||||
&:before {
|
||||
content: '\f00c';
|
||||
font-family: 'FONT AWESOME 5 FREE';
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
h1, h2, h3, h4 {
|
||||
width : 100%;
|
||||
margin : 0.5em 30% 0.25em 0;
|
||||
font-weight : 900;
|
||||
text-transform : uppercase;
|
||||
border-bottom : 2px solid slategrey;
|
||||
}
|
||||
h1 {
|
||||
margin-right : 0;
|
||||
margin-bottom : 0.5em;
|
||||
font-size : 2em;
|
||||
border-bottom : 2px solid darkslategrey;
|
||||
}
|
||||
h2 { font-size : 1.75em; }
|
||||
h3 {
|
||||
font-size : 1.5em;
|
||||
svg { width : 19px; }
|
||||
}
|
||||
h4 { font-size : 1.25em; }
|
||||
strong { font-weight : bold; }
|
||||
em { font-style : italic; }
|
||||
ul {
|
||||
padding-left : 1.25em;
|
||||
list-style : square;
|
||||
}
|
||||
.blank {
|
||||
height : 1em;
|
||||
margin-top : 0;
|
||||
& + * { margin-top : 0; }
|
||||
}
|
||||
}
|
||||
h4 {
|
||||
font-size: 1.25em;
|
||||
}
|
||||
strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -91,7 +91,7 @@ const EditPage = createClass({
|
||||
if(!(e.ctrlKey || e.metaKey)) return;
|
||||
const S_KEY = 83;
|
||||
const P_KEY = 80;
|
||||
if(e.keyCode == S_KEY) this.save();
|
||||
if(e.keyCode == S_KEY) this.trySave(true);
|
||||
if(e.keyCode == P_KEY) window.open(`/print/${this.processShareId()}?dialog=true`, '_blank').focus();
|
||||
if(e.keyCode == P_KEY || e.keyCode == S_KEY){
|
||||
e.stopPropagation();
|
||||
@@ -137,13 +137,14 @@ const EditPage = createClass({
|
||||
return !_.isEqual(this.state.brew, this.savedBrew);
|
||||
},
|
||||
|
||||
trySave : function(){
|
||||
trySave : function(immediate=false){
|
||||
if(!this.debounceSave) this.debounceSave = _.debounce(this.save, SAVE_TIMEOUT);
|
||||
if(this.hasChanges()){
|
||||
this.debounceSave();
|
||||
} else {
|
||||
this.debounceSave.cancel();
|
||||
}
|
||||
if(immediate) this.debounceSave.flush();
|
||||
},
|
||||
|
||||
handleGoogleClick : function(){
|
||||
|
||||
@@ -4,44 +4,37 @@ const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
const cx = require('classnames');
|
||||
|
||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||
const Navbar = require('../../navbar/navbar.jsx');
|
||||
const PatreonNavItem = require('../../navbar/patreon.navitem.jsx');
|
||||
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
||||
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
||||
const UIPage = require('../basePages/uiPage/uiPage.jsx');
|
||||
|
||||
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
||||
const Markdown = require('../../../../shared/naturalcrit/markdown.js');
|
||||
|
||||
const ErrorIndex = require('./errors/errorIndex.js');
|
||||
|
||||
const ErrorPage = createClass({
|
||||
displayName : 'ErrorPage',
|
||||
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
ver : '0.0.0',
|
||||
errorId : ''
|
||||
errorId : '',
|
||||
text : '# Oops \n We could not find a brew with that id. **Sorry!**',
|
||||
error : {}
|
||||
};
|
||||
},
|
||||
|
||||
text : '# Oops \n We could not find a brew with that id. **Sorry!**',
|
||||
|
||||
render : function(){
|
||||
return <div className='errorPage sitePage'>
|
||||
<Navbar ver={this.props.ver}>
|
||||
<Nav.section>
|
||||
<Nav.item className='errorTitle'>
|
||||
Crit Fail!
|
||||
</Nav.item>
|
||||
</Nav.section>
|
||||
const errorText = ErrorIndex(this.props)[this.props.brew.HBErrorCode.toString()] || '';
|
||||
|
||||
<Nav.section>
|
||||
<PatreonNavItem />
|
||||
<HelpNavItem />
|
||||
<RecentNavItem />
|
||||
</Nav.section>
|
||||
</Navbar>
|
||||
|
||||
<div className='content'>
|
||||
<BrewRenderer text={this.text} />
|
||||
return <UIPage brew={{ title: 'Crit Fail!' }}>
|
||||
<div className='dataGroup'>
|
||||
<div className='errorTitle'>
|
||||
<h1>{`Error ${this.props.brew.status || '000'}`}</h1>
|
||||
<h4>{this.props.brew.text || 'No error text'}</h4>
|
||||
</div>
|
||||
<hr />
|
||||
<div dangerouslySetInnerHTML={{ __html: Markdown.render(errorText) }} />
|
||||
</div>
|
||||
</div>;
|
||||
</UIPage>;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
.errorPage{
|
||||
.errorTitle{
|
||||
background-color: @orange;
|
||||
.homebrew {
|
||||
.uiPage.sitePage {
|
||||
.errorTitle {
|
||||
//background-color: @orange;
|
||||
color : #D02727;
|
||||
text-align : center;
|
||||
}
|
||||
.content {
|
||||
h1, h2, h3, h4 { border-bottom : none; }
|
||||
hr { border-bottom : 2px solid slategrey; }
|
||||
}
|
||||
}
|
||||
}
|
||||
126
client/homebrew/pages/errorPage/errors/errorIndex.js
Normal file
126
client/homebrew/pages/errorPage/errors/errorIndex.js
Normal file
@@ -0,0 +1,126 @@
|
||||
const dedent = require('dedent-tabs').default;
|
||||
|
||||
const loginUrl = 'https://www.naturalcrit.com/login';
|
||||
|
||||
const errorIndex = (props)=>{
|
||||
return {
|
||||
// Default catch all
|
||||
'00' : dedent`
|
||||
## An unknown error occurred!
|
||||
|
||||
We aren't sure what happened, but our server wasn't able to find what you
|
||||
were looking for.`,
|
||||
|
||||
// General Google load error
|
||||
'01' : dedent`
|
||||
## An error occurred while retrieving this brew from Google Drive!
|
||||
|
||||
Google reported an error while attempting to retrieve a brew from this link.`,
|
||||
|
||||
// Google Drive - 404 : brew deleted or access denied
|
||||
'02' : dedent`
|
||||
## We can't find this brew in Google Drive!
|
||||
|
||||
This file was saved on Google Drive, but this link doesn't work anymore.
|
||||
${ props.brew.authors?.length > 0
|
||||
? `Note that this brew belongs to the Homebrewery account **${ props.brew.authors[0] }**,
|
||||
${ props.brew.account
|
||||
? `which is
|
||||
${props.brew.authors[0] == props.brew.account
|
||||
? `your account.`
|
||||
: `not your account (you are currently signed in as **${props.brew.account}**).`
|
||||
}`
|
||||
: 'and you are not currently signed in to any account.'
|
||||
}`
|
||||
: ''
|
||||
}
|
||||
The Homebrewery cannot delete files from Google Drive on its own, so there
|
||||
are three most likely possibilities:
|
||||
:
|
||||
- **The Google Drive files may have been accidentally deleted.** Look in
|
||||
the Google Drive account that owns this brew (or ask the owner to do so),
|
||||
and make sure the Homebrewery folder is still there, and that it holds your brews
|
||||
as text files.
|
||||
- **You may have changed the sharing settings for your files.** If the files
|
||||
are still on Google Drive, change all of them to be shared *with everyone who has
|
||||
the link* so the Homebrewery can access them.
|
||||
- **The Google Account may be closed.** Google may have removed the account
|
||||
due to inactivity or violating a Google policy. Make sure the owner can
|
||||
still access Google Drive normally and upload/download files to it.
|
||||
:
|
||||
If the file isn't found, Google Drive usually puts your file in your Trash folder for
|
||||
30 days. Assuming the trash hasn't been emptied yet, it might be worth checking.
|
||||
You can also find the Activity tab on the right side of the Google Drive page, which
|
||||
shows the recent activity on Google Drive. This can help you pin down the exact date
|
||||
the brew was deleted or moved, and by whom.
|
||||
:
|
||||
If the brew still isn't found, some people have had success asking Google to recover
|
||||
accidentally deleted files at this link:
|
||||
https://support.google.com/drive/answer/1716222?hl=en&ref_topic=7000946.
|
||||
At the bottom of the page there is a button that says *Send yourself an Email*
|
||||
and you will receive instructions on how to request the files be restored.
|
||||
:
|
||||
Also note, if you prefer not to use your Google Drive for storage, you can always
|
||||
change the storage location of a brew by clicking the Google drive icon by the
|
||||
brew title and choosing *transfer my brew to/from Google Drive*.`,
|
||||
|
||||
// User is not Authors list
|
||||
'03' : dedent`
|
||||
## Current signed-in user does not have editor access to this brew.
|
||||
|
||||
If you believe you should have access to this brew, ask one of its authors to invite you
|
||||
as an author by opening the Edit page for the brew, viewing the {{fa,fa-info-circle}}
|
||||
**Properties** tab, and adding your username to the "invited authors" list. You can
|
||||
then try to access this document again.
|
||||
|
||||
**Brew Title:** ${props.brew.brewTitle || 'Unable to show title'}
|
||||
|
||||
**Current Authors:** ${props.brew.authors?.map((author)=>{return `${author}`;}).join(', ') || 'Unable to list authors'}`,
|
||||
|
||||
// User is not signed in; must be a user on the Authors List
|
||||
'04' : dedent`
|
||||
## Sign-in required to edit this brew.
|
||||
|
||||
You must be logged in to one of the accounts listed as an author of this brew.
|
||||
User is not logged in. Please log in [here](${loginUrl}).
|
||||
|
||||
**Brew Title:** ${props.brew.brewTitle || 'Unable to show title'}
|
||||
|
||||
**Current Authors:** ${props.brew.authors?.map((author)=>{return `${author}`;}).join(', ') || 'Unable to list authors'}`,
|
||||
|
||||
// Brew load error
|
||||
'05' : dedent`
|
||||
## No Homebrewery document could be found.
|
||||
|
||||
The server could not locate the Homebrewery document. It was likely deleted by
|
||||
its owner.
|
||||
|
||||
**Requested access:** ${props.brew.accessType}
|
||||
|
||||
**Brew ID:** ${props.brew.brewId}`,
|
||||
|
||||
// Brew save error
|
||||
'06' : dedent`
|
||||
## Unable to save Homebrewery document.
|
||||
|
||||
An error occurred wil attempting to save the Homebrewery document.`,
|
||||
|
||||
// Brew delete error
|
||||
'07' : dedent`
|
||||
## Unable to delete Homebrewery document.
|
||||
|
||||
An error occurred while attempting to remove the Homebrewery document.
|
||||
|
||||
**Brew ID:** ${props.brew.brewId}`,
|
||||
|
||||
// Author delete error
|
||||
'08' : dedent`
|
||||
## Unable to remove user from Homebrewery document.
|
||||
|
||||
An error occurred while attempting to remove the user from the Homebrewery document author list!
|
||||
|
||||
**Brew ID:** ${props.brew.brewId}`,
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = errorIndex;
|
||||
@@ -20,9 +20,10 @@ const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
||||
|
||||
const { DEFAULT_BREW } = require('../../../../server/brewDefaults.js');
|
||||
|
||||
const BREWKEY = 'homebrewery-new';
|
||||
const BREWKEY = 'homebrewery-new';
|
||||
const STYLEKEY = 'homebrewery-new-style';
|
||||
const METAKEY = 'homebrewery-new-meta';
|
||||
const METAKEY = 'homebrewery-new-meta';
|
||||
let SAVEKEY;
|
||||
|
||||
|
||||
const NewPage = createClass({
|
||||
@@ -62,12 +63,16 @@ const NewPage = createClass({
|
||||
brew.renderer = metaStorage?.renderer ?? brew.renderer;
|
||||
brew.theme = metaStorage?.theme ?? brew.theme;
|
||||
brew.lang = metaStorage?.lang ?? brew.lang;
|
||||
|
||||
this.setState({
|
||||
brew : brew
|
||||
});
|
||||
}
|
||||
|
||||
SAVEKEY = `HOMEBREWERY-DEFAULT-SAVE-LOCATION-${global.account?.username || ''}`;
|
||||
const saveStorage = localStorage.getItem(SAVEKEY) || 'HOMEBREWERY';
|
||||
|
||||
this.setState({
|
||||
brew : brew,
|
||||
saveGoogle : (saveStorage == 'GOOGLE-DRIVE' && this.state.saveGoogle)
|
||||
});
|
||||
|
||||
localStorage.setItem(BREWKEY, brew.text);
|
||||
if(brew.style)
|
||||
localStorage.setItem(STYLEKEY, brew.style);
|
||||
|
||||
@@ -11,6 +11,7 @@ const template = async function(name, title='', props = {}){
|
||||
return `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, height=device-height, interactive-widget=resizes-visual" />
|
||||
<link href="//use.fontawesome.com/releases/v5.15.1/css/all.css" rel="stylesheet" />
|
||||
<link href="//fonts.googleapis.com/css?family=Open+Sans:400,300,600,700" rel="stylesheet" type="text/css" />
|
||||
<link href=${`/${name}/bundle.css`} rel='stylesheet' />
|
||||
|
||||
@@ -13,7 +13,7 @@ npm install
|
||||
npm audit fix
|
||||
npm run postinstall
|
||||
|
||||
cp freebsd/rc.d/homebrewery /usr/local/etc/rc.d/
|
||||
cp install/freebsd/rc.d/homebrewery /usr/local/etc/rc.d/
|
||||
chmod +x /usr/local/etc/rc.d/homebrewery
|
||||
|
||||
sysrc homebrewery_enable=YES
|
||||
|
||||
3312
package-lock.json
generated
3312
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
40
package.json
40
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "homebrewery",
|
||||
"description": "Create authentic looking D&D homebrews using only markdown",
|
||||
"version": "3.9.0",
|
||||
"version": "3.9.2",
|
||||
"engines": {
|
||||
"node": ">=18.16.x"
|
||||
},
|
||||
@@ -78,10 +78,10 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.22.5",
|
||||
"@babel/plugin-transform-runtime": "^7.22.5",
|
||||
"@babel/preset-env": "^7.22.5",
|
||||
"@babel/preset-react": "^7.22.5",
|
||||
"@babel/core": "^7.22.15",
|
||||
"@babel/plugin-transform-runtime": "^7.22.15",
|
||||
"@babel/preset-env": "^7.22.15",
|
||||
"@babel/preset-react": "^7.22.15",
|
||||
"@googleapis/drive": "^5.1.0",
|
||||
"body-parser": "^1.20.2",
|
||||
"classnames": "^2.3.2",
|
||||
@@ -97,35 +97,35 @@
|
||||
"jwt-simple": "^0.5.6",
|
||||
"less": "^3.13.1",
|
||||
"lodash": "^4.17.21",
|
||||
"marked": "5.1.0",
|
||||
"marked": "5.1.1",
|
||||
"marked-extended-tables": "^1.0.6",
|
||||
"marked-gfm-heading-id": "^3.0.4",
|
||||
"marked-gfm-heading-id": "^3.0.7",
|
||||
"marked-smartypants-lite": "^1.0.0",
|
||||
"markedLegacy": "npm:marked@^0.3.19",
|
||||
"moment": "^2.29.4",
|
||||
"mongoose": "^7.3.1",
|
||||
"mongoose": "^7.5.0",
|
||||
"nanoid": "3.3.4",
|
||||
"nconf": "^0.12.0",
|
||||
"npm": "^9.7.2",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"npm": "^10.0.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-frame-component": "^4.1.3",
|
||||
"react-router-dom": "6.13.0",
|
||||
"react-router-dom": "6.15.0",
|
||||
"sanitize-filename": "1.6.3",
|
||||
"superagent": "^6.1.0",
|
||||
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^8.43.0",
|
||||
"eslint-plugin-jest": "^27.2.2",
|
||||
"eslint-plugin-react": "^7.32.2",
|
||||
"jest": "^29.5.0",
|
||||
"eslint": "^8.48.0",
|
||||
"eslint-plugin-jest": "^27.2.3",
|
||||
"eslint-plugin-react": "^7.33.2",
|
||||
"jest": "^29.6.4",
|
||||
"jest-expect-message": "^1.1.3",
|
||||
"postcss-less": "^6.0.0",
|
||||
"stylelint": "^15.8.0",
|
||||
"stylelint-config-recess-order": "^4.2.0",
|
||||
"stylelint-config-recommended": "^12.0.0",
|
||||
"stylelint-stylistic": "^0.4.2",
|
||||
"stylelint": "^15.10.3",
|
||||
"stylelint-config-recess-order": "^4.3.0",
|
||||
"stylelint-config-recommended": "^13.0.0",
|
||||
"stylelint-stylistic": "^0.4.3",
|
||||
"supertest": "^6.3.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,6 +99,24 @@ fs.emptyDirSync('./build');
|
||||
await fs.copy('./themes/assets', './build/assets');
|
||||
await fs.copy('./client/icons', './build/icons');
|
||||
|
||||
//v==---------------------------MOVE CM EDITOR THEMES -----------------------------==v//
|
||||
|
||||
editorThemeFiles = fs.readdirSync('./node_modules/codemirror/theme');
|
||||
|
||||
const editorThemeFile = './themes/codeMirror/editorThemes.json';
|
||||
if(fs.existsSync(editorThemeFile)) fs.rmSync(editorThemeFile);
|
||||
const stream = fs.createWriteStream(editorThemeFile, { flags: 'a' });
|
||||
stream.write('[\n"default"');
|
||||
|
||||
for (themeFile of editorThemeFiles) {
|
||||
stream.write(`,\n"${themeFile.slice(0, -4)}"`);
|
||||
}
|
||||
stream.write('\n]\n');
|
||||
stream.end();
|
||||
|
||||
await fs.copy('./node_modules/codemirror/theme', './build/homebrew/cm-themes');
|
||||
await fs.copy('./themes/codeMirror', './build/homebrew/codeMirror');
|
||||
|
||||
//v==----------------------------- BUNDLE PACKAGES --------------------------------==v//
|
||||
|
||||
const bundles = await pack('./client/homebrew/homebrew.jsx', {
|
||||
@@ -135,12 +153,12 @@ fs.emptyDirSync('./build');
|
||||
|
||||
})().catch(console.error);
|
||||
|
||||
//In development set up a watch server and livereload
|
||||
//In development, set up LiveReload (refreshes browser), and Nodemon (restarts server)
|
||||
if(isDev){
|
||||
livereload('./build');
|
||||
watchFile('./server.js', { // Rebuild when change detected to this file or any nested directory from here
|
||||
ignore : ['./build'], // Ignore ./build or it will rebuild again
|
||||
ext : 'less', // Other extensions to watch (only .js/.json/.jsx by default)
|
||||
//watch: ['./client', './server', './themes'], // Watch additional folders if you want
|
||||
livereload('./build'); // Install the Chrome extension LiveReload to automatically refresh the browser
|
||||
watchFile('./server.js', { // Restart server when change detected to this file or any nested directory from here
|
||||
ignore : ['./build', './client', './themes'], // Ignore folders that are not running server code / avoids unneeded restarts
|
||||
ext : 'js json' // Extensions to watch (only .js/.json by default)
|
||||
//watch : ['./server', './themes'], // Watch additional folders if needed
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/*eslint max-lines: ["warn", {"max": 400, "skipBlankLines": true, "skipComments": true}]*/
|
||||
/*eslint max-lines: ["warn", {"max": 500, "skipBlankLines": true, "skipComments": true}]*/
|
||||
// Set working directory to project root
|
||||
process.chdir(`${__dirname}/..`);
|
||||
|
||||
@@ -257,6 +257,7 @@ app.get('/user/:username', async (req, res, next)=>{
|
||||
brew.pageCount = googleBrews[match].pageCount;
|
||||
brew.renderer = googleBrews[match].renderer;
|
||||
brew.version = googleBrews[match].version;
|
||||
brew.webViewLink = googleBrews[match].webViewLink;
|
||||
googleBrews.splice(match, 1);
|
||||
}
|
||||
}
|
||||
@@ -267,6 +268,9 @@ app.get('/user/:username', async (req, res, next)=>{
|
||||
}
|
||||
|
||||
req.brews = _.map(brews, (brew)=>{
|
||||
// Clean up brew data
|
||||
brew.title = brew.title?.trim();
|
||||
brew.description = brew.description?.trim();
|
||||
return sanitizeBrew(brew, ownAccount ? 'edit' : 'share');
|
||||
});
|
||||
|
||||
@@ -324,8 +328,8 @@ app.get('/share/:id', asyncHandler(getBrew('share')), asyncHandler(async (req, r
|
||||
};
|
||||
|
||||
if(req.params.id.length > 12 && !brew._id) {
|
||||
const googleId = req.params.id.slice(0, -12);
|
||||
const shareId = req.params.id.slice(-12);
|
||||
const googleId = brew.googleId;
|
||||
const shareId = brew.shareId;
|
||||
await GoogleActions.increaseView(googleId, shareId, 'share', brew)
|
||||
.catch((err)=>{next(err);});
|
||||
} else {
|
||||
@@ -397,7 +401,6 @@ app.get('/account', asyncHandler(async (req, res, next)=>{
|
||||
return next();
|
||||
}));
|
||||
|
||||
|
||||
const nodeEnv = config.get('node_env');
|
||||
const isLocalEnvironment = config.get('local_environments').includes(nodeEnv);
|
||||
// Local only
|
||||
@@ -414,8 +417,7 @@ if(isLocalEnvironment){
|
||||
|
||||
//Render the page
|
||||
const templateFn = require('./../client/template.js');
|
||||
app.use(asyncHandler(async (req, res, next)=>{
|
||||
|
||||
const renderPage = async (req, res)=>{
|
||||
// Create configuration object
|
||||
const configuration = {
|
||||
local : isLocalEnvironment,
|
||||
@@ -424,7 +426,7 @@ app.use(asyncHandler(async (req, res, next)=>{
|
||||
};
|
||||
const props = {
|
||||
version : require('./../package.json').version,
|
||||
url : req.originalUrl,
|
||||
url : req.customUrl || req.originalUrl,
|
||||
brew : req.brew,
|
||||
brews : req.brews,
|
||||
googleBrews : req.googleBrews,
|
||||
@@ -438,15 +440,20 @@ app.use(asyncHandler(async (req, res, next)=>{
|
||||
const page = await templateFn('homebrew', title, props)
|
||||
.catch((err)=>{
|
||||
console.log(err);
|
||||
return res.sendStatus(500);
|
||||
});
|
||||
return page;
|
||||
};
|
||||
|
||||
//Send rendered page
|
||||
app.use(asyncHandler(async (req, res, next)=>{
|
||||
const page = await renderPage(req, res);
|
||||
if(!page) return;
|
||||
res.send(page);
|
||||
}));
|
||||
|
||||
//v=====----- Error-Handling Middleware -----=====v//
|
||||
//Format Errors so all fields will be sent
|
||||
const replaceErrors = (key, value)=>{
|
||||
//Format Errors as plain objects so all fields will appear in the string sent
|
||||
const formatErrors = (key, value)=>{
|
||||
if(value instanceof Error) {
|
||||
const error = {};
|
||||
Object.getOwnPropertyNames(value).forEach(function (key) {
|
||||
@@ -458,13 +465,30 @@ const replaceErrors = (key, value)=>{
|
||||
};
|
||||
|
||||
const getPureError = (error)=>{
|
||||
return JSON.parse(JSON.stringify(error, replaceErrors));
|
||||
return JSON.parse(JSON.stringify(error, formatErrors));
|
||||
};
|
||||
|
||||
app.use((err, req, res, next)=>{
|
||||
const status = err.status || 500;
|
||||
app.use(async (err, req, res, next)=>{
|
||||
const status = err.status || err.code || 500;
|
||||
console.error(err);
|
||||
res.status(status).send(getPureError(err));
|
||||
|
||||
req.ogMeta = { ...defaultMetaTags,
|
||||
title : 'Error Page',
|
||||
description : 'Something went wrong!'
|
||||
};
|
||||
req.brew = {
|
||||
...err,
|
||||
title : 'Error - Something went wrong!',
|
||||
text : err.errors?.map((error)=>{return error.message;}).join('\n\n') || err.message || 'Unknown error!',
|
||||
status : status,
|
||||
HBErrorCode : err.HBErrorCode ?? '00',
|
||||
pureError : getPureError(err)
|
||||
};
|
||||
req.customUrl= '/error';
|
||||
|
||||
const page = await renderPage(req, res);
|
||||
if(!page) return;
|
||||
res.send(page);
|
||||
});
|
||||
|
||||
app.use((req, res)=>{
|
||||
|
||||
@@ -100,13 +100,13 @@ const GoogleActions = {
|
||||
const drive = googleDrive.drive({ version: 'v3', auth });
|
||||
|
||||
const fileList = [];
|
||||
let NextPageToken = "";
|
||||
let NextPageToken = '';
|
||||
|
||||
do {
|
||||
const obj = await drive.files.list({
|
||||
pageSize : 1000,
|
||||
pageToken : NextPageToken || "",
|
||||
fields : 'nextPageToken, files(id, name, description, createdTime, modifiedTime, properties)',
|
||||
pageToken : NextPageToken || '',
|
||||
fields : 'nextPageToken, files(id, name, description, createdTime, modifiedTime, properties, webViewLink)',
|
||||
q : 'mimeType != \'application/vnd.google-apps.folder\' and trashed = false'
|
||||
})
|
||||
.catch((err)=>{
|
||||
@@ -139,7 +139,8 @@ const GoogleActions = {
|
||||
published : file.properties.published ? file.properties.published == 'true' : false,
|
||||
systems : [],
|
||||
lang : file.properties.lang,
|
||||
thumbnail : file.properties.thumbnail
|
||||
thumbnail : file.properties.thumbnail,
|
||||
webViewLink : file.webViewLink
|
||||
};
|
||||
});
|
||||
return brews;
|
||||
@@ -243,9 +244,9 @@ const GoogleActions = {
|
||||
|
||||
if(obj) {
|
||||
if(accessType == 'edit' && obj.data.properties.editId != accessId){
|
||||
throw ('Edit ID does not match');
|
||||
throw ({ message: 'Edit ID does not match' });
|
||||
} else if(accessType == 'share' && obj.data.properties.shareId != accessId){
|
||||
throw ('Share ID does not match');
|
||||
throw ({ message: 'Share ID does not match' });
|
||||
}
|
||||
|
||||
const file = await drive.files.get({
|
||||
|
||||
@@ -27,8 +27,13 @@ const api = {
|
||||
|
||||
// If the id is longer than 12, then it's a google id + the edit id. This splits the longer id up.
|
||||
if(id.length > 12) {
|
||||
googleId = id.slice(0, -12);
|
||||
id = id.slice(-12);
|
||||
if(id.length >= (33 + 12)) { // googleId is minimum 33 chars (may increase)
|
||||
googleId = id.slice(0, -12); // current editId is 12 chars
|
||||
} else { // old editIds used to be 10 chars;
|
||||
googleId = id.slice(0, -10); // if total string is too short, must be old brew
|
||||
console.log('Old brew, using 10-char Id');
|
||||
}
|
||||
id = id.slice(googleId.length);
|
||||
}
|
||||
return { id, googleId };
|
||||
},
|
||||
@@ -57,7 +62,14 @@ const api = {
|
||||
googleError = err;
|
||||
});
|
||||
// Throw any error caught while attempting to retrieve Google brew.
|
||||
if(googleError) throw googleError;
|
||||
if(googleError) {
|
||||
const reason = googleError.errors?.[0].reason;
|
||||
if(reason == 'notFound') {
|
||||
throw { ...googleError, HBErrorCode: '02', authors: stub?.authors, account: req.account?.username };
|
||||
} else {
|
||||
throw { ...googleError, HBErrorCode: '01' };
|
||||
}
|
||||
}
|
||||
// Combine the Homebrewery stub with the google brew, or if the stub doesn't exist just use the google brew
|
||||
stub = stub ? _.assign({ ...api.excludeStubProps(stub), stubbed: true }, api.excludeGoogleProps(googleBrew)) : googleBrew;
|
||||
}
|
||||
@@ -65,14 +77,16 @@ const api = {
|
||||
const isAuthor = stub?.authors?.includes(req.account?.username);
|
||||
const isInvited = stub?.invitedAuthors?.includes(req.account?.username);
|
||||
if(accessType === 'edit' && (authorsExist && !(isAuthor || isInvited))) {
|
||||
throw `The current logged in user does not have editor access to this brew.
|
||||
|
||||
If you believe you should have access to this brew, ask the file owner to invite you as an author by opening the brew, viewing the Properties tab, and adding your username to the "invited authors" list. You can then try to access this document again.`;
|
||||
const accessError = { name: 'Access Error', status: 401 };
|
||||
if(req.account){
|
||||
throw { ...accessError, message: 'User is not an Author', HBErrorCode: '03', authors: stub.authors, brewTitle: stub.title };
|
||||
}
|
||||
throw { ...accessError, message: 'User is not logged in', HBErrorCode: '04', authors: stub.authors, brewTitle: stub.title };
|
||||
}
|
||||
|
||||
// If after all of that we still don't have a brew, throw an exception
|
||||
if(!stub && !stubOnly) {
|
||||
throw 'Brew not found in Homebrewery database or Google Drive';
|
||||
throw { name: 'BrewLoad Error', message: 'Brew not found', status: 404, HBErrorCode: '05', accessType: accessType, brewId: id };
|
||||
}
|
||||
|
||||
// Clean up brew: fill in missing fields with defaults / fix old invalid values
|
||||
@@ -139,6 +153,9 @@ If you believe you should have access to this brew, ask the file owner to invite
|
||||
brew.text = api.mergeBrewText(brew);
|
||||
|
||||
_.defaults(brew, DEFAULT_BREW);
|
||||
|
||||
brew.title = brew.title.trim();
|
||||
brew.description = brew.description.trim();
|
||||
},
|
||||
newGoogleBrew : async (account, brew, res)=>{
|
||||
const oAuth2Client = GoogleActions.authCheck(account, res);
|
||||
@@ -181,7 +198,7 @@ If you believe you should have access to this brew, ask the file owner to invite
|
||||
saved = await newHomebrew.save()
|
||||
.catch((err)=>{
|
||||
console.error(err, err.toString(), err.stack);
|
||||
throw `Error while creating new brew, ${err.toString()}`;
|
||||
throw { name: 'BrewSave Error', message: `Error while creating new brew, ${err.toString()}`, status: 500, HBErrorCode: '06' };
|
||||
});
|
||||
if(!saved) return;
|
||||
saved = saved.toObject();
|
||||
@@ -203,6 +220,8 @@ If you believe you should have access to this brew, ask the file owner to invite
|
||||
const { saveToGoogle, removeFromGoogle } = req.query;
|
||||
let afterSave = async ()=>true;
|
||||
|
||||
brew.title = brew.title.trim();
|
||||
brew.description = brew.description.trim() || '';
|
||||
brew.text = api.mergeBrewText(brew);
|
||||
|
||||
if(brew.googleId && removeFromGoogle) {
|
||||
@@ -283,10 +302,13 @@ If you believe you should have access to this brew, ask the file owner to invite
|
||||
try {
|
||||
await api.getBrew('edit')(req, res, ()=>{});
|
||||
} catch (err) {
|
||||
const { id, googleId } = api.getId(req);
|
||||
console.warn(`No google brew found for id ${googleId}, the stub with id ${id} will be deleted.`);
|
||||
await HomebrewModel.deleteOne({ editId: id });
|
||||
return next();
|
||||
// Only if the error code is HBErrorCode '02', that is, Google returned "404 - Not Found"
|
||||
if(err.HBErrorCode == '02') {
|
||||
const { id, googleId } = api.getId(req);
|
||||
console.warn(`No google brew found for id ${googleId}, the stub with id ${id} will be deleted.`);
|
||||
await HomebrewModel.deleteOne({ editId: id });
|
||||
return next();
|
||||
}
|
||||
}
|
||||
|
||||
let brew = req.brew;
|
||||
@@ -308,7 +330,7 @@ If you believe you should have access to this brew, ask the file owner to invite
|
||||
await HomebrewModel.deleteOne({ _id: brew._id })
|
||||
.catch((err)=>{
|
||||
console.error(err);
|
||||
throw { status: 500, message: 'Error while removing' };
|
||||
throw { name: 'BrewDelete Error', message: 'Error while removing', status: 500, HBErrorCode: '07', brewId: brew._id };
|
||||
});
|
||||
} else {
|
||||
if(shouldDeleteGoogleBrew) {
|
||||
@@ -320,7 +342,7 @@ If you believe you should have access to this brew, ask the file owner to invite
|
||||
brew.markModified('authors'); //Mongo will not properly update arrays without markModified()
|
||||
await brew.save()
|
||||
.catch((err)=>{
|
||||
throw { status: 500, message: err };
|
||||
throw { name: 'BrewAuthorDelete Error', message: err, status: 500, HBErrorCode: '08', brewId: brew._id };
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,21 +111,32 @@ describe('Tests for api', ()=>{
|
||||
expect(googleId).toEqual('12345');
|
||||
});
|
||||
|
||||
it('should return id and google id from params', ()=>{
|
||||
it('should return 12-char id and google id from params', ()=>{
|
||||
const { id, googleId } = api.getId({
|
||||
params : {
|
||||
id : '123456789012abcdefghijkl'
|
||||
id : '123456789012345678901234567890123abcdefghijkl'
|
||||
}
|
||||
});
|
||||
|
||||
expect(googleId).toEqual('123456789012345678901234567890123');
|
||||
expect(id).toEqual('abcdefghijkl');
|
||||
expect(googleId).toEqual('123456789012');
|
||||
});
|
||||
|
||||
it('should return 10-char id and google id from params', ()=>{
|
||||
const { id, googleId } = api.getId({
|
||||
params : {
|
||||
id : '123456789012345678901234567890123abcdefghij'
|
||||
}
|
||||
});
|
||||
|
||||
expect(googleId).toEqual('123456789012345678901234567890123');
|
||||
expect(id).toEqual('abcdefghij');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getBrew', ()=>{
|
||||
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
|
||||
const notFoundError = 'Brew not found in Homebrewery database or Google Drive';
|
||||
const notFoundError = { HBErrorCode: '05', message: 'Brew not found', name: 'BrewLoad Error', status: 404, accessType: 'share', brewId: '1' };
|
||||
|
||||
it('returns middleware', ()=>{
|
||||
const getFn = api.getBrew('share');
|
||||
@@ -183,7 +194,7 @@ describe('Tests for api', ()=>{
|
||||
expect(next).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('throws if invalid author', async ()=>{
|
||||
it('throws if not logged in as author', async ()=>{
|
||||
api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
|
||||
model.get = jest.fn(()=>toBrewPromise({ title: 'test brew', authors: ['a'] }));
|
||||
|
||||
@@ -197,9 +208,24 @@ describe('Tests for api', ()=>{
|
||||
err = e;
|
||||
}
|
||||
|
||||
expect(err).toEqual(`The current logged in user does not have editor access to this brew.
|
||||
expect(err).toEqual({ HBErrorCode: '04', message: 'User is not logged in', name: 'Access Error', status: 401, brewTitle: 'test brew', authors: ['a'] });
|
||||
});
|
||||
|
||||
If you believe you should have access to this brew, ask the file owner to invite you as an author by opening the brew, viewing the Properties tab, and adding your username to the "invited authors" list. You can then try to access this document again.`);
|
||||
it('throws if logged in as invalid author', async ()=>{
|
||||
api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
|
||||
model.get = jest.fn(()=>toBrewPromise({ title: 'test brew', authors: ['a'] }));
|
||||
|
||||
const fn = api.getBrew('edit', true);
|
||||
const req = { brew: {}, account: { username: 'b' } };
|
||||
|
||||
let err;
|
||||
try {
|
||||
await fn(req, null, null);
|
||||
} catch (e) {
|
||||
err = e;
|
||||
}
|
||||
|
||||
expect(err).toEqual({ HBErrorCode: '03', message: 'User is not an Author', name: 'Access Error', status: 401, brewTitle: 'test brew', authors: ['a'] });
|
||||
});
|
||||
|
||||
it('does not throw if no authors', async ()=>{
|
||||
@@ -545,7 +571,7 @@ brew`);
|
||||
|
||||
describe('deleteBrew', ()=>{
|
||||
it('should handle case where fetching the brew returns an error', async ()=>{
|
||||
api.getBrew = jest.fn(()=>async ()=>{ throw 'err'; });
|
||||
api.getBrew = jest.fn(()=>async ()=>{ throw { message: 'err', HBErrorCode: '02' }; });
|
||||
api.getId = jest.fn(()=>({ id: '1', googleId: '2' }));
|
||||
model.deleteOne = jest.fn(async ()=>{});
|
||||
const next = jest.fn(()=>{});
|
||||
|
||||
@@ -49,7 +49,8 @@ const CodeEditor = createClass({
|
||||
value : '',
|
||||
wrap : true,
|
||||
onChange : ()=>{},
|
||||
enableFolding : true
|
||||
enableFolding : true,
|
||||
editorTheme : 'default'
|
||||
};
|
||||
},
|
||||
|
||||
@@ -91,6 +92,10 @@ const CodeEditor = createClass({
|
||||
} else {
|
||||
this.codeMirror.setOption('foldOptions', false);
|
||||
}
|
||||
|
||||
if(prevProps.editorTheme !== this.props.editorTheme){
|
||||
this.codeMirror.setOption('theme', this.props.editorTheme);
|
||||
}
|
||||
},
|
||||
|
||||
buildEditor : function() {
|
||||
@@ -159,6 +164,7 @@ const CodeEditor = createClass({
|
||||
autoCloseTags : true,
|
||||
styleActiveLine : true,
|
||||
showTrailingSpace : false,
|
||||
theme : this.props.editorTheme
|
||||
// specialChars : / /,
|
||||
// specialCharPlaceholder : function(char) {
|
||||
// const el = document.createElement('span');
|
||||
@@ -176,7 +182,7 @@ const CodeEditor = createClass({
|
||||
|
||||
indent : function () {
|
||||
const cm = this.codeMirror;
|
||||
if (cm.somethingSelected()) {
|
||||
if(cm.somethingSelected()) {
|
||||
cm.execCommand('indentMore');
|
||||
} else {
|
||||
cm.execCommand('insertSoftTab');
|
||||
@@ -406,7 +412,10 @@ const CodeEditor = createClass({
|
||||
//----------------------//
|
||||
|
||||
render : function(){
|
||||
return <div className='codeEditor' ref='editor' style={this.props.style}/>;
|
||||
return <>
|
||||
<link href={`../homebrew/cm-themes/${this.props.editorTheme}.css`} rel='stylesheet' />
|
||||
<div className='codeEditor' ref='editor' style={this.props.style}/>
|
||||
</>;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -9,6 +9,9 @@
|
||||
}
|
||||
|
||||
.codeEditor{
|
||||
@media screen and (pointer : coarse) {
|
||||
font-size : 16px;
|
||||
}
|
||||
.CodeMirror-foldmarker {
|
||||
font-family: inherit;
|
||||
text-shadow: none;
|
||||
|
||||
@@ -313,12 +313,6 @@ const escape = function (html, encode) {
|
||||
return html;
|
||||
};
|
||||
|
||||
const sanatizeScriptTags = (content)=>{
|
||||
return content
|
||||
.replace(/<script/ig, '<script')
|
||||
.replace(/<\/script>/ig, '</script>');
|
||||
};
|
||||
|
||||
const tagTypes = ['div', 'span', 'a'];
|
||||
const tagRegex = new RegExp(`(${
|
||||
_.map(tagTypes, (type)=>{
|
||||
@@ -349,7 +343,7 @@ module.exports = {
|
||||
render : (rawBrewText)=>{
|
||||
rawBrewText = rawBrewText.replace(/^\\column$/gm, `\n<div class='columnSplit'></div>\n`)
|
||||
.replace(/^(:+)$/gm, (match)=>`${`<div class='blank'></div>`.repeat(match.length)}\n`);
|
||||
return Marked.parse(sanatizeScriptTags(rawBrewText));
|
||||
return Marked.parse(rawBrewText);
|
||||
},
|
||||
|
||||
validate : (rawBrewText)=>{
|
||||
|
||||
@@ -90,12 +90,6 @@ const escape = function (html, encode) {
|
||||
return html;
|
||||
};
|
||||
|
||||
const sanatizeScriptTags = (content)=>{
|
||||
return content
|
||||
.replace(/<script/ig, '<script')
|
||||
.replace(/<\/script>/ig, '</script>');
|
||||
};
|
||||
|
||||
const tagTypes = ['div', 'span', 'a'];
|
||||
const tagRegex = new RegExp(`(${
|
||||
_.map(tagTypes, (type)=>{
|
||||
@@ -113,7 +107,7 @@ module.exports = {
|
||||
marked : Markdown,
|
||||
render : (rawBrewText)=>{
|
||||
return Markdown(
|
||||
sanatizeScriptTags(rawBrewText),
|
||||
rawBrewText,
|
||||
{ renderer: renderer }
|
||||
);
|
||||
},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
require('./nav.less');
|
||||
const React = require('react');
|
||||
const { useState, useRef, useEffect } = React;
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
const cx = require('classnames');
|
||||
@@ -71,64 +72,49 @@ const Nav = {
|
||||
}
|
||||
}),
|
||||
|
||||
dropdown : createClass({
|
||||
displayName : 'Nav.dropdown',
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
trigger : 'hover'
|
||||
};
|
||||
},
|
||||
getInitialState : function() {
|
||||
return {
|
||||
showDropdown : false
|
||||
};
|
||||
},
|
||||
componentDidMount : function() {
|
||||
if(this.props.trigger == 'click')
|
||||
document.addEventListener('click', this.handleClickOutside);
|
||||
},
|
||||
componentWillUnmount : function() {
|
||||
if(this.props.trigger == 'click')
|
||||
document.removeEventListener('click', this.handleClickOutside);
|
||||
},
|
||||
handleClickOutside : function(e){
|
||||
// Close dropdown when clicked outside
|
||||
if(this.refs.dropdown && !this.refs.dropdown.contains(e.target)) {
|
||||
this.handleDropdown(false);
|
||||
}
|
||||
},
|
||||
handleDropdown : function(show){
|
||||
this.setState({
|
||||
showDropdown : show
|
||||
});
|
||||
},
|
||||
renderDropdown : function(dropdownChildren){
|
||||
if(!this.state.showDropdown) return null;
|
||||
dropdown : function dropdown(props) {
|
||||
props = Object.assign({}, props, {
|
||||
trigger : 'hover click'
|
||||
});
|
||||
|
||||
return (
|
||||
<div className='navDropdown'>
|
||||
{dropdownChildren}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
render : function () {
|
||||
const dropdownChildren = React.Children.map(this.props.children, (child, i)=>{
|
||||
// Ignore the first child
|
||||
if(i < 1) return;
|
||||
return child;
|
||||
});
|
||||
return (
|
||||
<div className={`navDropdownContainer ${this.props.className}`}
|
||||
ref='dropdown'
|
||||
onMouseEnter={this.props.trigger == 'hover' ? ()=>{this.handleDropdown(true);} : undefined}
|
||||
onClick= {this.props.trigger == 'click' ? ()=>{this.handleDropdown(true);} : undefined}
|
||||
onMouseLeave={this.props.trigger == 'hover' ? ()=>{this.handleDropdown(false);} : undefined}>
|
||||
{this.props.children[0] || this.props.children /*children is not an array when only one child*/}
|
||||
{this.renderDropdown(dropdownChildren)}
|
||||
</div>
|
||||
);
|
||||
const myRef = useRef(null);
|
||||
const [showDropdown, setShowDropdown] = useState(false);
|
||||
|
||||
useEffect(()=>{
|
||||
document.addEventListener('click', handleClickOutside);
|
||||
return ()=>{
|
||||
document.removeEventListener('click', handleClickOutside);
|
||||
};
|
||||
}, []);
|
||||
|
||||
function handleClickOutside(e) {
|
||||
// Close dropdown when clicked outside
|
||||
if(!myRef.current?.contains(e.target)) {
|
||||
handleDropdown(false);
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
function handleDropdown(show) {
|
||||
setShowDropdown(show ?? !showDropdown);
|
||||
}
|
||||
|
||||
const dropdownChildren = React.Children.map(props.children, (child, i)=>{
|
||||
if(i < 1) return;
|
||||
return child;
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={`navDropdownContainer ${props.className}`}
|
||||
ref={myRef}
|
||||
onMouseEnter = { props.trigger.includes('hover') ? ()=>handleDropdown(true) : undefined }
|
||||
onMouseLeave = { props.trigger.includes('hover') ? ()=>handleDropdown(false) : undefined }
|
||||
onClick = { props.trigger.includes('click') ? ()=>handleDropdown(true) : undefined }
|
||||
>
|
||||
{props.children[0] || props.children /*children is not an array when only one child*/}
|
||||
{showDropdown && <div className='navDropdown'>{dropdownChildren}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -79,6 +79,8 @@ nav{
|
||||
left : 0px;
|
||||
z-index : 10000;
|
||||
width : 100%;
|
||||
overflow : hidden auto;
|
||||
max-height : calc(100vh - 28px);
|
||||
.navItem{
|
||||
animation-name: glideDropDown;
|
||||
animation-duration: 0.4s;
|
||||
|
||||
@@ -61,7 +61,8 @@ const SplitPane = createClass({
|
||||
return result;
|
||||
},
|
||||
|
||||
handleUp : function(){
|
||||
handleUp : function(e){
|
||||
e.preventDefault();
|
||||
if(this.state.isDragging){
|
||||
this.props.onDragFinish(this.state.currentDividerPos);
|
||||
window.localStorage.setItem(this.props.storageKey, this.state.currentDividerPos);
|
||||
@@ -78,6 +79,7 @@ const SplitPane = createClass({
|
||||
handleMove : function(e){
|
||||
if(!this.state.isDragging) return;
|
||||
|
||||
e.preventDefault();
|
||||
const newSize = this.limitPosition(e.pageX);
|
||||
this.setState({
|
||||
currentDividerPos : newSize,
|
||||
@@ -122,7 +124,7 @@ const SplitPane = createClass({
|
||||
renderDivider : function(){
|
||||
return <>
|
||||
{this.renderMoveArrows()}
|
||||
<div className='divider' onMouseDown={this.handleDown} >
|
||||
<div className='divider' onPointerDown={this.handleDown} >
|
||||
<div className='dots'>
|
||||
<i className='fas fa-circle' />
|
||||
<i className='fas fa-circle' />
|
||||
@@ -133,7 +135,7 @@ const SplitPane = createClass({
|
||||
},
|
||||
|
||||
render : function(){
|
||||
return <div className='splitPane' onMouseMove={this.handleMove} onMouseUp={this.handleUp}>
|
||||
return <div className='splitPane' onPointerMove={this.handleMove} onPointerUp={this.handleUp}>
|
||||
<Pane
|
||||
ref='pane1'
|
||||
width={this.state.currentDividerPos}
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
flex : 1;
|
||||
}
|
||||
.divider{
|
||||
touch-action : none;
|
||||
display : table;
|
||||
height : 100%;
|
||||
width : 15px;
|
||||
|
||||
@@ -2,12 +2,6 @@
|
||||
|
||||
const Markdown = require('naturalcrit/markdown.js');
|
||||
|
||||
test('Escapes <script> tag', function() {
|
||||
const source = '<script></script>';
|
||||
const rendered = Markdown.render(source);
|
||||
expect(rendered).toMatch('<p><script></script></p>\n');
|
||||
});
|
||||
|
||||
test('Processes the markdown within an HTML block if its just a class wrapper', function() {
|
||||
const source = '<div>*Bold text*</div>';
|
||||
const rendered = Markdown.render(source);
|
||||
|
||||
@@ -47,8 +47,8 @@ const getTOC = (pages)=>{
|
||||
return res;
|
||||
};
|
||||
|
||||
module.exports = function(brew){
|
||||
const pages = brew.text.split('\\page');
|
||||
module.exports = function(props){
|
||||
const pages = props.brew.text.split('\\page');
|
||||
const TOC = getTOC(pages);
|
||||
const markdown = _.reduce(TOC, (r, g1, idx1)=>{
|
||||
r.push(`- **[${idx1 + 1} ${g1.title}](#p${g1.page})**`);
|
||||
|
||||
@@ -19,16 +19,6 @@ module.exports = [
|
||||
icon : 'fas fa-pencil-alt',
|
||||
view : 'text',
|
||||
snippets : [
|
||||
{
|
||||
name : 'Page Number',
|
||||
icon : 'fas fa-bookmark',
|
||||
gen : '{{pageNumber 1}}\n{{footnote PART 1 | SECTION NAME}}\n\n'
|
||||
},
|
||||
{
|
||||
name : 'Auto-incrementing Page Number',
|
||||
icon : 'fas fa-sort-numeric-down',
|
||||
gen : '{{pageNumber,auto}}\n{{footnote PART 1 | SECTION NAME}}\n\n'
|
||||
},
|
||||
{
|
||||
name : 'Table of Contents',
|
||||
icon : 'fas fa-book',
|
||||
@@ -230,34 +220,51 @@ module.exports = [
|
||||
view : 'text',
|
||||
snippets : [
|
||||
{
|
||||
name : 'Class Table',
|
||||
icon : 'fas fa-table',
|
||||
gen : ClassTableGen.full('classTable,frame,decoration,wide'),
|
||||
},
|
||||
{
|
||||
name : 'Class Table (unframed)',
|
||||
icon : 'fas fa-border-none',
|
||||
gen : ClassTableGen.full('classTable,wide'),
|
||||
},
|
||||
{
|
||||
name : '1/2 Class Table',
|
||||
icon : 'fas fa-list-alt',
|
||||
gen : ClassTableGen.half('classTable,decoration,frame'),
|
||||
},
|
||||
{
|
||||
name : '1/2 Class Table (unframed)',
|
||||
icon : 'fas fa-border-none',
|
||||
gen : ClassTableGen.half('classTable'),
|
||||
},
|
||||
{
|
||||
name : '1/3 Class Table',
|
||||
icon : 'fas fa-border-all',
|
||||
gen : ClassTableGen.third('classTable,frame'),
|
||||
},
|
||||
{
|
||||
name : '1/3 Class Table (unframed)',
|
||||
icon : 'fas fa-border-none',
|
||||
gen : ClassTableGen.third('classTable'),
|
||||
name : 'Class Tables',
|
||||
icon : 'fas fa-table',
|
||||
gen : ClassTableGen.full('classTable,frame,decoration,wide'),
|
||||
subsnippets : [
|
||||
{
|
||||
name : 'Martial Class Table',
|
||||
icon : 'fas fa-table',
|
||||
gen : ClassTableGen.non('classTable,frame,decoration'),
|
||||
},
|
||||
{
|
||||
name : 'Martial Class Table (unframed)',
|
||||
icon : 'fas fa-border-none',
|
||||
gen : ClassTableGen.non('classTable'),
|
||||
},
|
||||
{
|
||||
name : 'Full Caster Class Table',
|
||||
icon : 'fas fa-table',
|
||||
gen : ClassTableGen.full('classTable,frame,decoration,wide'),
|
||||
},
|
||||
{
|
||||
name : 'Full Caster Class Table (unframed)',
|
||||
icon : 'fas fa-border-none',
|
||||
gen : ClassTableGen.full('classTable,wide'),
|
||||
},
|
||||
{
|
||||
name : 'Half Caster Class Table',
|
||||
icon : 'fas fa-list-alt',
|
||||
gen : ClassTableGen.half('classTable,frame,decoration,wide'),
|
||||
},
|
||||
{
|
||||
name : 'Half Caster Class Table (unframed)',
|
||||
icon : 'fas fa-border-none',
|
||||
gen : ClassTableGen.half('classTable,wide'),
|
||||
},
|
||||
{
|
||||
name : 'Third Caster Spell Table',
|
||||
icon : 'fas fa-border-all',
|
||||
gen : ClassTableGen.third('classTable,frame,decoration'),
|
||||
},
|
||||
{
|
||||
name : 'Third Caster Spell Table (unframed)',
|
||||
icon : 'fas fa-border-none',
|
||||
gen : ClassTableGen.third('classTable'),
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name : 'Rune Table',
|
||||
|
||||
@@ -1,132 +1,138 @@
|
||||
const _ = require('lodash');
|
||||
const dedent = require('dedent-tabs').default;
|
||||
|
||||
const features = [
|
||||
'Astrological Botany',
|
||||
'Biochemical Sorcery',
|
||||
'Civil Divination',
|
||||
'Consecrated Augury',
|
||||
'Demonic Anthropology',
|
||||
'Divinatory Mineralogy',
|
||||
'Exo Interfacer',
|
||||
'Genetic Banishing',
|
||||
'Gunpowder Torturer',
|
||||
'Gunslinger Corruptor',
|
||||
'Hermetic Geography',
|
||||
'Immunological Cultist',
|
||||
'Malefic Chemist',
|
||||
'Mathematical Pharmacy',
|
||||
'Nuclear Biochemistry',
|
||||
'Orbital Gravedigger',
|
||||
'Pharmaceutical Outlaw',
|
||||
'Phased Linguist',
|
||||
'Plasma Gunslinger',
|
||||
'Police Necromancer',
|
||||
'Ritual Astronomy',
|
||||
'Sixgun Poisoner',
|
||||
'Seismological Alchemy',
|
||||
'Spiritual Illusionism',
|
||||
'Statistical Occultism',
|
||||
'Spell Analyst',
|
||||
'Torque Interfacer'
|
||||
'Astrological Botany', 'Biochemical Sorcery', 'Civil Divination',
|
||||
'Consecrated Augury', 'Demonic Anthropology', 'Divinatory Mineralogy',
|
||||
'Exo Interfacer', 'Genetic Banishing', 'Gunpowder Torturer',
|
||||
'Gunslinger Corruptor', 'Hermetic Geography', 'Immunological Cultist',
|
||||
'Malefic Chemist', 'Mathematical Pharmacy', 'Nuclear Biochemistry',
|
||||
'Orbital Gravedigger', 'Pharmaceutical Outlaw', 'Phased Linguist',
|
||||
'Plasma Gunslinger', 'Police Necromancer', 'Ritual Astronomy',
|
||||
'Sixgun Poisoner', 'Seismological Alchemy', 'Spiritual Illusionism',
|
||||
'Statistical Occultism', 'Spell Analyst', 'Torque Interfacer'
|
||||
].map((f)=>_.padEnd(f, 21)); // Pad to equal length of 21 chars long
|
||||
|
||||
const classnames = [
|
||||
'Ackerman', 'Berserker-Typist', 'Concierge', 'Fishmonger',
|
||||
'Haberdasher', 'Manicurist', 'Netrunner', 'Weirkeeper'
|
||||
];
|
||||
|
||||
const classnames = ['Ackerman', 'Berserker-Typist', 'Concierge', 'Fishmonger',
|
||||
'Haberdasher', 'Manicurist', 'Netrunner', 'Weirkeeper'];
|
||||
|
||||
const levels = ['1st', '2nd', '3rd', '4th', '5th',
|
||||
'6th', '7th', '8th', '9th', '10th',
|
||||
'11th', '12th', '13th', '14th', '15th',
|
||||
'16th', '17th', '18th', '19th', '20th'];
|
||||
|
||||
const profBonus = [2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6, 6, 6];
|
||||
|
||||
const maxes = [4, 3, 3, 3, 3, 2, 2, 1, 1];
|
||||
|
||||
const drawSlots = function(Slots, rows, padding){
|
||||
let slots = Number(Slots);
|
||||
return _.times(rows, function(i){
|
||||
const max = maxes[i];
|
||||
if(slots < 1) return _.pad('—', padding);
|
||||
const res = _.min([max, slots]);
|
||||
slots -= res;
|
||||
return _.pad(res.toString(), padding);
|
||||
}).join(' | ');
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
full : function(classes){
|
||||
const classname = _.sample(classnames);
|
||||
|
||||
|
||||
let cantrips = 3;
|
||||
let spells = 1;
|
||||
let slots = 2;
|
||||
return `{{${classes}\n##### The ${classname}\n` +
|
||||
`| Level | Proficiency | Features | Cantrips | Spells | --- Spell Slots Per Spell Level ---|||||||||\n`+
|
||||
`| ^| Bonus ^| ^| Known ^| Known ^|1st |2nd |3rd |4th |5th |6th |7th |8th |9th |\n`+
|
||||
`|:-----:|:-----------:|:-------------|:--------:|:------:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|\n${
|
||||
_.map(levels, function(levelName, level){
|
||||
const res = [
|
||||
_.pad(levelName, 5),
|
||||
_.pad(`+${profBonus[level]}`, 2),
|
||||
_.padEnd(_.sample(features), 21),
|
||||
_.pad(cantrips.toString(), 8),
|
||||
_.pad(spells.toString(), 6),
|
||||
drawSlots(slots, 9, 2),
|
||||
].join(' | ');
|
||||
|
||||
cantrips += _.random(0, 1);
|
||||
spells += _.random(0, 1);
|
||||
slots += _.random(0, 2);
|
||||
|
||||
return `| ${res} |`;
|
||||
}).join('\n')}\n}}\n\n`;
|
||||
non : function(snippetClasses){
|
||||
return dedent`
|
||||
{{${snippetClasses}
|
||||
##### The ${_.sample(classnames)}
|
||||
| Level | Proficiency Bonus | Features | ${_.sample(features)} |
|
||||
|:-----:|:-----------------:|:---------|:---------------------:|
|
||||
| 1st | +2 | ${_.sample(features)} | 2 |
|
||||
| 2nd | +2 | ${_.sample(features)} | 2 |
|
||||
| 3rd | +2 | ${_.sample(features)} | 3 |
|
||||
| 4th | +2 | ${_.sample(features)} | 3 |
|
||||
| 5th | +3 | ${_.sample(features)} | 3 |
|
||||
| 6th | +3 | ${_.sample(features)} | 4 |
|
||||
| 7th | +3 | ${_.sample(features)} | 4 |
|
||||
| 8th | +3 | ${_.sample(features)} | 4 |
|
||||
| 9th | +4 | ${_.sample(features)} | 4 |
|
||||
| 10th | +4 | ${_.sample(features)} | 4 |
|
||||
| 11th | +4 | ${_.sample(features)} | 4 |
|
||||
| 12th | +4 | ${_.sample(features)} | 5 |
|
||||
| 13th | +5 | ${_.sample(features)} | 5 |
|
||||
| 14th | +5 | ${_.sample(features)} | 5 |
|
||||
| 15th | +5 | ${_.sample(features)} | 5 |
|
||||
| 16th | +5 | ${_.sample(features)} | 5 |
|
||||
| 17th | +6 | ${_.sample(features)} | 6 |
|
||||
| 18th | +6 | ${_.sample(features)} | 6 |
|
||||
| 19th | +6 | ${_.sample(features)} | 6 |
|
||||
| 20th | +6 | ${_.sample(features)} | unlimited |
|
||||
}}\n\n`;
|
||||
},
|
||||
|
||||
half : function(classes){
|
||||
const classname = _.sample(classnames);
|
||||
|
||||
let featureScore = 1;
|
||||
return `{{${classes}\n##### The ${classname}\n` +
|
||||
`| Level | Proficiency Bonus | Features | ${_.pad(_.sample(features), 21)} |\n` +
|
||||
`|:-----:|:-----------------:|:---------|:---------------------:|\n${
|
||||
_.map(levels, function(levelName, level){
|
||||
const res = [
|
||||
_.pad(levelName, 5),
|
||||
_.pad(`+${profBonus[level]}`, 2),
|
||||
_.padEnd(_.sample(features), 23),
|
||||
_.pad(`+${featureScore}`, 21),
|
||||
].join(' | ');
|
||||
|
||||
featureScore += _.random(0, 1);
|
||||
|
||||
return `| ${res} |`;
|
||||
}).join('\n')}\n}}\n\n`;
|
||||
full : function(snippetClasses){
|
||||
return dedent`
|
||||
{{${snippetClasses}
|
||||
##### The ${_.sample(classnames)}
|
||||
| Level | Proficiency | Features | Cantrips | --- Spell Slots Per Spell Level ---|||||||||
|
||||
| ^| Bonus ^| ^| Known ^|1st |2nd |3rd |4th |5th |6th |7th |8th |9th |
|
||||
|:-----:|:-----------:|:-------------|:--------:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|
|
||||
| 1st | +2 | ${_.sample(features)} | 2 | 2 | — | — | — | — | — | — | — | — |
|
||||
| 2nd | +2 | ${_.sample(features)} | 2 | 3 | — | — | — | — | — | — | — | — |
|
||||
| 3rd | +2 | ${_.sample(features)} | 2 | 4 | 2 | — | — | — | — | — | — | — |
|
||||
| 4th | +2 | ${_.sample(features)} | 3 | 4 | 3 | — | — | — | — | — | — | — |
|
||||
| 5th | +3 | ${_.sample(features)} | 3 | 4 | 3 | 2 | — | — | — | — | — | — |
|
||||
| 6th | +3 | ${_.sample(features)} | 3 | 4 | 3 | 3 | — | — | — | — | — | — |
|
||||
| 7th | +3 | ${_.sample(features)} | 3 | 4 | 3 | 3 | 1 | — | — | — | — | — |
|
||||
| 8th | +3 | ${_.sample(features)} | 3 | 4 | 3 | 3 | 2 | — | — | — | — | — |
|
||||
| 9th | +4 | ${_.sample(features)} | 3 | 4 | 3 | 3 | 2 | 1 | — | — | — | — |
|
||||
| 10th | +4 | ${_.sample(features)} | 3 | 4 | 3 | 3 | 2 | 1 | — | — | — | — |
|
||||
| 11th | +4 | ${_.sample(features)} | 4 | 4 | 3 | 3 | 2 | 1 | 1 | — | — | — |
|
||||
| 12th | +4 | ${_.sample(features)} | 4 | 4 | 3 | 3 | 2 | 1 | 1 | — | — | — |
|
||||
| 13th | +5 | ${_.sample(features)} | 4 | 4 | 3 | 3 | 2 | 1 | 1 | 1 | — | — |
|
||||
| 14th | +5 | ${_.sample(features)} | 4 | 4 | 3 | 3 | 2 | 1 | 1 | 1 | — | — |
|
||||
| 15th | +5 | ${_.sample(features)} | 4 | 4 | 3 | 3 | 2 | 1 | 1 | 1 | 1 | — |
|
||||
| 16th | +5 | ${_.sample(features)} | 4 | 4 | 3 | 3 | 2 | 1 | 1 | 1 | 1 | — |
|
||||
| 17th | +6 | ${_.sample(features)} | 4 | 4 | 3 | 3 | 2 | 1 | 1 | 1 | 1 | 1 |
|
||||
| 18th | +6 | ${_.sample(features)} | 4 | 4 | 3 | 3 | 3 | 1 | 1 | 1 | 1 | 1 |
|
||||
| 19th | +6 | ${_.sample(features)} | 4 | 4 | 3 | 3 | 3 | 2 | 2 | 1 | 1 | 1 |
|
||||
| 20th | +6 | ${_.sample(features)} | 4 | 4 | 3 | 3 | 3 | 2 | 2 | 2 | 1 | 1 |
|
||||
}}\n\n`;
|
||||
},
|
||||
|
||||
third : function(classes){
|
||||
const classname = _.sample(classnames);
|
||||
half : function(snippetClasses){
|
||||
return dedent`
|
||||
{{${snippetClasses}
|
||||
##### The ${_.sample(classnames)}
|
||||
| Level | Proficiency | Features | Spells |--- Spell Slots Per Spell Level ---|||||
|
||||
| ^| Bonus ^| ^| Known ^| 1st | 2nd | 3rd | 4th | 5th |
|
||||
|:-----:|:-----------:|:-------------|:------:|:-----:|:-----:|:-----:|:-----:|:-----:|
|
||||
| 1st | +2 | ${_.sample(features)} | — | — | — | — | — | — |
|
||||
| 2nd | +2 | ${_.sample(features)} | 2 | 2 | — | — | — | — |
|
||||
| 3rd | +2 | ${_.sample(features)} | 3 | 3 | — | — | — | — |
|
||||
| 4th | +2 | ${_.sample(features)} | 3 | 3 | — | — | — | — |
|
||||
| 5th | +3 | ${_.sample(features)} | 4 | 4 | 2 | — | — | — |
|
||||
| 6th | +3 | ${_.sample(features)} | 4 | 4 | 2 | — | — | — |
|
||||
| 7th | +3 | ${_.sample(features)} | 5 | 4 | 3 | — | — | — |
|
||||
| 8th | +3 | ${_.sample(features)} | 5 | 4 | 3 | — | — | — |
|
||||
| 9th | +4 | ${_.sample(features)} | 6 | 4 | 3 | 2 | — | — |
|
||||
| 10th | +4 | ${_.sample(features)} | 6 | 4 | 3 | 2 | — | — |
|
||||
| 11th | +4 | ${_.sample(features)} | 7 | 4 | 3 | 3 | — | — |
|
||||
| 12th | +4 | ${_.sample(features)} | 7 | 4 | 3 | 3 | — | — |
|
||||
| 13th | +5 | ${_.sample(features)} | 8 | 4 | 3 | 3 | 1 | — |
|
||||
| 14th | +5 | ${_.sample(features)} | 8 | 4 | 3 | 3 | 1 | — |
|
||||
| 15th | +5 | ${_.sample(features)} | 9 | 4 | 3 | 3 | 2 | — |
|
||||
| 16th | +5 | ${_.sample(features)} | 9 | 4 | 3 | 3 | 2 | — |
|
||||
| 17th | +6 | ${_.sample(features)} | 10 | 4 | 3 | 3 | 3 | 1 |
|
||||
| 18th | +6 | ${_.sample(features)} | 10 | 4 | 3 | 3 | 3 | 1 |
|
||||
| 19th | +6 | ${_.sample(features)} | 11 | 4 | 3 | 3 | 3 | 2 |
|
||||
| 20th | +6 | ${_.sample(features)} | 11 | 4 | 3 | 3 | 3 | 2 |
|
||||
}}\n\n`;
|
||||
},
|
||||
|
||||
let cantrips = 3;
|
||||
let spells = 1;
|
||||
let slots = 2;
|
||||
return `{{${classes}\n##### ${classname} Spellcasting\n` +
|
||||
`| Class | Cantrips | Spells |--- Spells Slots per Spell Level ---||||\n` +
|
||||
`| Level ^| Known ^| Known ^| 1st | 2nd | 3rd | 4th |\n` +
|
||||
`|:------:|:--------:|:-------:|:-------:|:-------:|:-------:|:-------:|\n${
|
||||
_.map(levels, function(levelName, level){
|
||||
const res = [
|
||||
_.pad(levelName, 6),
|
||||
_.pad(cantrips.toString(), 8),
|
||||
_.pad(spells.toString(), 7),
|
||||
drawSlots(slots, 4, 7),
|
||||
].join(' | ');
|
||||
|
||||
cantrips += _.random(0, 1);
|
||||
spells += _.random(0, 1);
|
||||
slots += _.random(0, 1);
|
||||
|
||||
return `| ${res} |`;
|
||||
}).join('\n')}\n}}\n\n`;
|
||||
third : function(snippetClasses){
|
||||
return dedent`
|
||||
{{${snippetClasses}
|
||||
##### ${_.sample(classnames)} Spellcasting
|
||||
| Level | Cantrips | Spells |--- Spells Slots per Spell Level ---||||
|
||||
| ^| Known ^| Known ^| 1st | 2nd | 3rd | 4th |
|
||||
|:-----:|:--------:|:------:|:-------:|:-------:|:-------:|:-------:|
|
||||
| 3rd | 2 | 3 | 2 | — | — | — |
|
||||
| 4th | 2 | 4 | 3 | — | — | — |
|
||||
| 5th | 2 | 4 | 3 | — | — | — |
|
||||
| 6th | 2 | 4 | 3 | — | — | — |
|
||||
| 7th | 2 | 5 | 4 | 2 | — | — |
|
||||
| 8th | 2 | 6 | 4 | 2 | — | — |
|
||||
| 9th | 2 | 6 | 4 | 2 | — | — |
|
||||
| 10th | 3 | 7 | 4 | 3 | — | — |
|
||||
| 11th | 3 | 8 | 4 | 3 | — | — |
|
||||
| 12th | 3 | 8 | 4 | 3 | — | — |
|
||||
| 13th | 3 | 9 | 4 | 3 | 2 | — |
|
||||
| 14th | 3 | 10 | 4 | 3 | 2 | — |
|
||||
| 15th | 3 | 10 | 4 | 3 | 2 | — |
|
||||
| 16th | 3 | 11 | 4 | 3 | 3 | — |
|
||||
| 17th | 3 | 11 | 4 | 3 | 3 | — |
|
||||
| 18th | 3 | 11 | 4 | 3 | 3 | — |
|
||||
| 19th | 3 | 12 | 4 | 3 | 3 | 1 |
|
||||
| 20th | 3 | 13 | 4 | 3 | 3 | 1 |
|
||||
}}\n\n`;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -29,27 +29,29 @@ const getTOC = (pages)=>{
|
||||
|
||||
const res = [];
|
||||
_.each(pages, (page, pageNum)=>{
|
||||
const lines = page.split('\n');
|
||||
_.each(lines, (line)=>{
|
||||
if(_.startsWith(line, '# ')){
|
||||
const title = line.replace('# ', '');
|
||||
add1(title, pageNum);
|
||||
}
|
||||
if(_.startsWith(line, '## ')){
|
||||
const title = line.replace('## ', '');
|
||||
add2(title, pageNum);
|
||||
}
|
||||
if(_.startsWith(line, '### ')){
|
||||
const title = line.replace('### ', '');
|
||||
add3(title, pageNum);
|
||||
}
|
||||
});
|
||||
if(!page.includes("{{frontCover}}") && !page.includes("{{insideCover}}") && !page.includes("{{partCover}}") && !page.includes("{{backCover}}")) {
|
||||
const lines = page.split('\n');
|
||||
_.each(lines, (line)=>{
|
||||
if(_.startsWith(line, '# ')){
|
||||
const title = line.replace('# ', '');
|
||||
add1(title, pageNum);
|
||||
}
|
||||
if(_.startsWith(line, '## ')){
|
||||
const title = line.replace('## ', '');
|
||||
add2(title, pageNum);
|
||||
}
|
||||
if(_.startsWith(line, '### ')){
|
||||
const title = line.replace('### ', '');
|
||||
add3(title, pageNum);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
return res;
|
||||
};
|
||||
|
||||
module.exports = function(brew){
|
||||
const pages = brew.text.split('\\page');
|
||||
module.exports = function(props){
|
||||
const pages = props.brew.text.split('\\page');
|
||||
const TOC = getTOC(pages);
|
||||
const markdown = _.reduce(TOC, (r, g1, idx1)=>{
|
||||
if(g1.title !== null) {
|
||||
|
||||
@@ -42,7 +42,7 @@ body {
|
||||
margin-top : 0.1cm;
|
||||
}
|
||||
}
|
||||
.useColumns(@multiplier : 1, @fillMode: balance){
|
||||
.useColumns(@multiplier : 1, @fillMode: auto){
|
||||
column-count : 2;
|
||||
column-fill : @fillMode;
|
||||
column-gap : 0.9cm;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
const WatercolorGen = require('./snippets/watercolor.gen.js');
|
||||
const ImageMaskGen = require('./snippets/imageMask.gen.js');
|
||||
const FooterGen = require('./snippets/footer.gen.js');
|
||||
const dedent = require('dedent-tabs').default;
|
||||
|
||||
module.exports = [
|
||||
@@ -21,6 +22,53 @@ module.exports = [
|
||||
icon : 'fas fa-file-alt',
|
||||
gen : '\n\\page\n'
|
||||
},
|
||||
{
|
||||
name : 'Page Number',
|
||||
icon : 'fas fa-bookmark',
|
||||
gen : '{{pageNumber 1}}\n'
|
||||
},
|
||||
{
|
||||
name : 'Auto-incrementing Page Number',
|
||||
icon : 'fas fa-sort-numeric-down',
|
||||
gen : '{{pageNumber,auto}}\n'
|
||||
},
|
||||
{
|
||||
name : 'Footer',
|
||||
icon : 'fas fa-shoe-prints',
|
||||
gen : FooterGen.createFooterFunc(),
|
||||
subsnippets : [
|
||||
{
|
||||
name : 'Footer from H1',
|
||||
icon : 'fas fa-dice-one',
|
||||
gen : FooterGen.createFooterFunc(1)
|
||||
},
|
||||
{
|
||||
name : 'Footer from H2',
|
||||
icon : 'fas fa-dice-two',
|
||||
gen : FooterGen.createFooterFunc(2)
|
||||
},
|
||||
{
|
||||
name : 'Footer from H3',
|
||||
icon : 'fas fa-dice-three',
|
||||
gen : FooterGen.createFooterFunc(3)
|
||||
},
|
||||
{
|
||||
name : 'Footer from H4',
|
||||
icon : 'fas fa-dice-four',
|
||||
gen : FooterGen.createFooterFunc(4)
|
||||
},
|
||||
{
|
||||
name : 'Footer from H5',
|
||||
icon : 'fas fa-dice-five',
|
||||
gen : FooterGen.createFooterFunc(5)
|
||||
},
|
||||
{
|
||||
name : 'Footer from H6',
|
||||
icon : 'fas fa-dice-six',
|
||||
gen : FooterGen.createFooterFunc(6)
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name : 'Vertical Spacing',
|
||||
icon : 'fas fa-arrows-alt-v',
|
||||
|
||||
17
themes/V3/Blank/snippets/footer.gen.js
Normal file
17
themes/V3/Blank/snippets/footer.gen.js
Normal file
@@ -0,0 +1,17 @@
|
||||
const Markdown = require('../../../../shared/naturalcrit/markdown.js');
|
||||
|
||||
module.exports = {
|
||||
createFooterFunc : function(headerSize=1){
|
||||
return (props)=>{
|
||||
const cursorPos = props.cursorPos;
|
||||
|
||||
const markdownText = props.brew.text.split('\n').slice(0, cursorPos.line).join('\n');
|
||||
const markdownTokens = Markdown.marked.lexer(markdownText);
|
||||
const headerToken = markdownTokens.findLast((lexerToken)=>{ return lexerToken.type === 'heading' && lexerToken.depth === headerSize; });
|
||||
const headerText = headerToken?.tokens.map((token)=>{ return token.text; }).join('');
|
||||
const outputText = headerText || 'PART 1 | SECTION NAME';
|
||||
|
||||
return `\n{{footnote ${outputText}}}\n`;
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -33,7 +33,7 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
.useColumns(@multiplier : 1, @fillMode: balance){
|
||||
.useColumns(@multiplier : 1, @fillMode: auto){
|
||||
column-fill : @fillMode;
|
||||
column-count : 2;
|
||||
}
|
||||
@@ -42,6 +42,7 @@ body {
|
||||
column-span : all;
|
||||
columns : inherit;
|
||||
column-gap : inherit;
|
||||
column-fill : inherit;
|
||||
}
|
||||
.page{
|
||||
.useColumns();
|
||||
|
||||
88
themes/codeMirror/customEditorStyles.less
Normal file
88
themes/codeMirror/customEditorStyles.less
Normal file
@@ -0,0 +1,88 @@
|
||||
.editor .codeEditor .CodeMirror {
|
||||
// Themes with dark backgrounds
|
||||
&.cm-s-3024-night,
|
||||
&.cm-s-abbott,
|
||||
&.cm-s-abcdef,
|
||||
&.cm-s-ambiance,
|
||||
&.cm-s-ayu-dark,
|
||||
&.cm-s-ayu-mirage,
|
||||
&.cm-s-base16-dark,
|
||||
&.cm-s-bespin,
|
||||
&.cm-s-blackboard,
|
||||
&.cm-s-cobalt,
|
||||
&.cm-s-colorforth,
|
||||
&.cm-s-darcula,
|
||||
&.cm-s-dracula,
|
||||
&.cm-s-duotone-dark,
|
||||
&.cm-s-erlang-dark,
|
||||
&.cm-s-gruvbox-dark,
|
||||
&.cm-s-hopscotch,
|
||||
&.cm-s-icecoder,
|
||||
&.cm-s-isotope,
|
||||
&.cm-s-lesser-dark,
|
||||
&.cm-s-liquibyte,
|
||||
&.cm-s-lucario,
|
||||
&.cm-s-material,
|
||||
&.cm-s-material-darker,
|
||||
&.cm-s-material-ocean,
|
||||
&.cm-s-material-palenight,
|
||||
&.cm-s-mbo,
|
||||
&.cm-s-midnight,
|
||||
&.cm-s-monokai,
|
||||
&.cm-s-moxer,
|
||||
&.cm-s-night,
|
||||
&.cm-s-nord,
|
||||
&.cm-s-oceanic-next,
|
||||
&.cm-s-panda-syntax,
|
||||
&.cm-s-paraiso-dark,
|
||||
&.cm-s-pastel-on-dark,
|
||||
&.cm-s-railscasts,
|
||||
&.cm-s-rubyblue,
|
||||
&.cm-s-seti,
|
||||
&.cm-s-shadowfox,
|
||||
&.cm-s-the-matrix,
|
||||
&.cm-s-tomorrow-night-bright,
|
||||
&.cm-s-tomorrow-night-eighties,
|
||||
&.cm-s-twilight,
|
||||
&.cm-s-vibrant-ink,
|
||||
&.cm-s-xq-dark,
|
||||
&.cm-s-yonce,
|
||||
&.cm-s-zenburn
|
||||
{
|
||||
.CodeMirror-code {
|
||||
.block:not(.cm-comment) {
|
||||
color: magenta;
|
||||
}
|
||||
.columnSplit {
|
||||
color: black;
|
||||
background-color: rgba(35,153,153,0.5);
|
||||
}
|
||||
.pageLine {
|
||||
background-color: rgba(255,255,255,0.75);
|
||||
& ~ pre.CodeMirror-line {
|
||||
color: black;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Themes with light backgrounds
|
||||
&.cm-s-default,
|
||||
&.cm-s-3024-day,
|
||||
&.cm-s-ambiance-mobile,
|
||||
&.cm-s-base16-light,
|
||||
&.cm-s-duotone-light,
|
||||
&.cm-s-eclipse,
|
||||
&.cm-s-elegant,
|
||||
&.cm-s-juejin,
|
||||
&.cm-s-neat,
|
||||
&.cm-s-neo,
|
||||
&.cm-s-paraiso-lightm
|
||||
&.cm-s-solarized,
|
||||
&.cm-s-ssms,
|
||||
&.cm-s-ttcn,
|
||||
&.cm-s-xq-light,
|
||||
&.cm-s-yeti {
|
||||
// Future styling for themes with light backgrounds
|
||||
--dummyVar: 'currently unused';
|
||||
}
|
||||
}
|
||||
68
themes/codeMirror/editorThemes.json
Normal file
68
themes/codeMirror/editorThemes.json
Normal file
@@ -0,0 +1,68 @@
|
||||
[
|
||||
"default",
|
||||
"3024-day",
|
||||
"3024-night",
|
||||
"abbott",
|
||||
"abcdef",
|
||||
"ambiance-mobile",
|
||||
"ambiance",
|
||||
"ayu-dark",
|
||||
"ayu-mirage",
|
||||
"base16-dark",
|
||||
"base16-light",
|
||||
"bespin",
|
||||
"blackboard",
|
||||
"cobalt",
|
||||
"colorforth",
|
||||
"darcula",
|
||||
"dracula",
|
||||
"duotone-dark",
|
||||
"duotone-light",
|
||||
"eclipse",
|
||||
"elegant",
|
||||
"erlang-dark",
|
||||
"gruvbox-dark",
|
||||
"hopscotch",
|
||||
"icecoder",
|
||||
"idea",
|
||||
"isotope",
|
||||
"juejin",
|
||||
"lesser-dark",
|
||||
"liquibyte",
|
||||
"lucario",
|
||||
"material-darker",
|
||||
"material-ocean",
|
||||
"material-palenight",
|
||||
"material",
|
||||
"mbo",
|
||||
"mdn-like",
|
||||
"midnight",
|
||||
"monokai",
|
||||
"moxer",
|
||||
"neat",
|
||||
"neo",
|
||||
"night",
|
||||
"nord",
|
||||
"oceanic-next",
|
||||
"panda-syntax",
|
||||
"paraiso-dark",
|
||||
"paraiso-light",
|
||||
"pastel-on-dark",
|
||||
"railscasts",
|
||||
"rubyblue",
|
||||
"seti",
|
||||
"shadowfox",
|
||||
"solarized",
|
||||
"ssms",
|
||||
"the-matrix",
|
||||
"tomorrow-night-bright",
|
||||
"tomorrow-night-eighties",
|
||||
"ttcn",
|
||||
"twilight",
|
||||
"vibrant-ink",
|
||||
"xq-dark",
|
||||
"xq-light",
|
||||
"yeti",
|
||||
"yonce",
|
||||
"zenburn"
|
||||
]
|
||||
Reference in New Issue
Block a user