0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2026-01-07 12:02:44 +00:00

Merge branch 'master' of https://github.com/naturalcrit/homebrewery into FAQ-update

This commit is contained in:
Víctor Losada Hernández
2023-09-07 23:14:53 +02:00
48 changed files with 3118 additions and 1894 deletions

View File

@@ -15,7 +15,7 @@ module.exports = {
rules : { rules : {
/** Errors **/ /** Errors **/
'camelcase' : ['error', { properties: 'never' }], 'camelcase' : ['error', { properties: 'never' }],
'func-style' : ['error', 'expression', { allowArrowFunctions: true }], //'func-style' : ['error', 'expression', { allowArrowFunctions: true }],
'no-array-constructor' : 'error', 'no-array-constructor' : 'error',
'no-iterator' : 'error', 'no-iterator' : 'error',
'no-nested-ternary' : 'error', 'no-nested-ternary' : 'error',

View File

@@ -80,6 +80,63 @@ pre {
## changelog ## changelog
For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery). For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery).
### 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 ### Friday 02/06/2023 - v3.9.0
{{taskList {{taskList

View File

@@ -108,6 +108,12 @@ const BrewRenderer = createClass({
return false; return false;
}, },
sanitizeScriptTags : function(content) {
return content
.replace(/<script/ig, '&lt;script')
.replace(/<\/script>/ig, '&lt;/script&gt;');
},
renderPageInfo : function(){ renderPageInfo : function(){
return <div className='pageInfo' ref='main'> return <div className='pageInfo' ref='main'>
<div> <div>
@@ -135,18 +141,20 @@ const BrewRenderer = createClass({
renderStyle : function() { renderStyle : function() {
if(!this.props.style) return; if(!this.props.style) return;
//return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `<style>@layer styleTab {\n${this.props.style}\n} </style>` }} />; const cleanStyle = this.sanitizeScriptTags(this.props.style);
return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `<style>\n${this.props.style}\n</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){ renderPage : function(pageText, index){
let cleanPageText = this.sanitizeScriptTags(pageText);
if(this.props.renderer == 'legacy') 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 { else {
pageText += `\n\n&nbsp;\n\\column\n&nbsp;`; //Artificial column break at page end to emulate column-fill:auto (until `wide` is used, when column-fill:balance will reappear) cleanPageText += `\n\n&nbsp;\n\\column\n&nbsp;`; //Artificial column break at page end to emulate column-fill:auto (until `wide` is used, when column-fill:balance will reappear)
return ( return (
<div className='page' id={`p${index + 1}`} key={index} > <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> </div>
); );
} }
@@ -185,6 +193,12 @@ const BrewRenderer = createClass({
}, 100); }, 100);
}, },
emitClick : function(){
// console.log('iFrame clicked');
if(!window || !document) return;
document.dispatchEvent(new MouseEvent('click'));
},
render : function(){ render : function(){
//render in iFrame so broken code doesn't crash the site. //render in iFrame so broken code doesn't crash the site.
//Also render dummy page while iframe is mounting. //Also render dummy page while iframe is mounting.
@@ -203,7 +217,9 @@ const BrewRenderer = createClass({
<Frame id='BrewRenderer' initialContent={this.state.initialContent} <Frame id='BrewRenderer' initialContent={this.state.initialContent}
style={{ width: '100%', height: '100%', visibility: this.state.visibility }} style={{ width: '100%', height: '100%', visibility: this.state.visibility }}
contentDidMount={this.frameDidMount}> contentDidMount={this.frameDidMount}
onClick={()=>{this.emitClick();}}
>
<div className={'brewRenderer'} <div className={'brewRenderer'}
onScroll={this.handleScroll} onScroll={this.handleScroll}
style={{ height: this.state.height }}> style={{ height: this.state.height }}>

View File

@@ -10,6 +10,8 @@ const CodeEditor = require('naturalcrit/codeEditor/codeEditor.jsx');
const SnippetBar = require('./snippetbar/snippetbar.jsx'); const SnippetBar = require('./snippetbar/snippetbar.jsx');
const MetadataEditor = require('./metadataEditor/metadataEditor.jsx'); const MetadataEditor = require('./metadataEditor/metadataEditor.jsx');
const EDITOR_THEME_KEY = 'HOMEBREWERY-EDITOR-THEME';
const SNIPPETBAR_HEIGHT = 25; const SNIPPETBAR_HEIGHT = 25;
const DEFAULT_STYLE_TEXT = dedent` const DEFAULT_STYLE_TEXT = dedent`
/*=======--- Example CSS styling ---=======*/ /*=======--- Example CSS styling ---=======*/
@@ -34,12 +36,14 @@ const Editor = createClass({
onMetaChange : ()=>{}, onMetaChange : ()=>{},
reportError : ()=>{}, reportError : ()=>{},
renderer : 'legacy' editorTheme : 'default',
renderer : 'legacy'
}; };
}, },
getInitialState : function() { getInitialState : function() {
return { 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.updateEditorSize();
this.highlightCustomMarkdown(); this.highlightCustomMarkdown();
window.addEventListener('resize', this.updateEditorSize); window.addEventListener('resize', this.updateEditorSize);
const editorTheme = window.localStorage.getItem(EDITOR_THEME_KEY);
if(editorTheme) {
this.setState({
editorTheme : editorTheme
});
}
}, },
componentWillUnmount : function() { componentWillUnmount : function() {
@@ -255,6 +266,13 @@ const Editor = createClass({
this.refs.codeEditor?.updateSize(); 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 //Called by CodeEditor after document switch, so Snippetbar can refresh UndoHistory
rerenderParent : function (){ rerenderParent : function (){
this.forceUpdate(); this.forceUpdate();
@@ -269,6 +287,7 @@ const Editor = createClass({
view={this.state.view} view={this.state.view}
value={this.props.brew.text} value={this.props.brew.text}
onChange={this.props.onTextChange} onChange={this.props.onTextChange}
editorTheme={this.state.editorTheme}
rerenderParent={this.rerenderParent} /> rerenderParent={this.rerenderParent} />
</>; </>;
} }
@@ -281,6 +300,7 @@ const Editor = createClass({
value={this.props.brew.style ?? DEFAULT_STYLE_TEXT} value={this.props.brew.style ?? DEFAULT_STYLE_TEXT}
onChange={this.props.onStyleChange} onChange={this.props.onStyleChange}
enableFolding={false} enableFolding={false}
editorTheme={this.state.editorTheme}
rerenderParent={this.rerenderParent} /> rerenderParent={this.rerenderParent} />
</>; </>;
} }
@@ -323,7 +343,10 @@ const Editor = createClass({
theme={this.props.brew.theme} theme={this.props.brew.theme}
undo={this.undo} undo={this.undo}
redo={this.redo} redo={this.redo}
historySize={this.historySize()} /> historySize={this.historySize()}
currentEditorTheme={this.state.editorTheme}
updateEditorTheme={this.updateEditorTheme}
cursorPos={this.refs.codeEditor?.getCursorPosition() || {}} />
{this.renderEditor()} {this.renderEditor()}
</div> </div>

View File

@@ -1,4 +1,4 @@
@import 'themes/codeMirror/customEditorStyles.less';
.editor{ .editor{
position : relative; position : relative;
width : 100%; width : 100%;

View File

@@ -1,3 +1,4 @@
/*eslint max-lines: ["warn", {"max": 250, "skipBlankLines": true, "skipComments": true}]*/
require('./snippetbar.less'); require('./snippetbar.less');
const React = require('react'); const React = require('react');
const createClass = require('create-react-class'); 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_Journal'] = require('themes/V3/Journal/snippets.js');
ThemeSnippets['V3_Blank'] = require('themes/V3/Blank/snippets.js'); ThemeSnippets['V3_Blank'] = require('themes/V3/Blank/snippets.js');
const execute = function(val, brew){ const EditorThemes = require('build/homebrew/codeMirror/editorThemes.json');
if(_.isFunction(val)) return val(brew);
const execute = function(val, props){
if(_.isFunction(val)) return val(props);
return val; return val;
}; };
@@ -24,23 +27,26 @@ const Snippetbar = createClass({
displayName : 'SnippetBar', displayName : 'SnippetBar',
getDefaultProps : function() { getDefaultProps : function() {
return { return {
brew : {}, brew : {},
view : 'text', view : 'text',
onViewChange : ()=>{}, onViewChange : ()=>{},
onInject : ()=>{}, onInject : ()=>{},
onToggle : ()=>{}, onToggle : ()=>{},
showEditButtons : true, showEditButtons : true,
renderer : 'legacy', renderer : 'legacy',
undo : ()=>{}, undo : ()=>{},
redo : ()=>{}, redo : ()=>{},
historySize : ()=>{} historySize : ()=>{},
updateEditorTheme : ()=>{},
cursorPos : {}
}; };
}, },
getInitialState : function() { getInitialState : function() {
return { return {
renderer : this.props.renderer, renderer : this.props.renderer,
snippets : [] themeSelector : false,
snippets : []
}; };
}, },
@@ -94,6 +100,31 @@ const Snippetbar = createClass({
this.props.onInject(injectedText); 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(){ renderSnippetGroups : function(){
const snippets = this.state.snippets.filter((snippetGroup)=>snippetGroup.view === this.props.view); const snippets = this.state.snippets.filter((snippetGroup)=>snippetGroup.view === this.props.view);
@@ -105,6 +136,7 @@ const Snippetbar = createClass({
snippets={snippetGroup.snippets} snippets={snippetGroup.snippets}
key={snippetGroup.groupName} key={snippetGroup.groupName}
onSnippetClick={this.handleSnippetClick} onSnippetClick={this.handleSnippetClick}
cursorPos={this.props.cursorPos}
/>; />;
}); });
}, },
@@ -122,6 +154,12 @@ const Snippetbar = createClass({
<i className='fas fa-redo' /> <i className='fas fa-redo' />
</div> </div>
<div className='divider'></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' })} <div className={cx('text', { selected: this.props.view === 'text' })}
onClick={()=>this.props.onViewChange('text')}> onClick={()=>this.props.onViewChange('text')}>
<i className='fa fa-beer' /> <i className='fa fa-beer' />
@@ -165,7 +203,7 @@ const SnippetGroup = createClass({
}, },
handleSnippetClick : function(e, snippet){ handleSnippetClick : function(e, snippet){
e.stopPropagation(); e.stopPropagation();
this.props.onSnippetClick(execute(snippet.gen, this.props.brew)); this.props.onSnippetClick(execute(snippet.gen, this.props));
}, },
renderSnippets : function(snippets){ renderSnippets : function(snippets){
return _.map(snippets, (snippet)=>{ return _.map(snippets, (snippet)=>{
@@ -194,5 +232,4 @@ const SnippetGroup = createClass({
</div> </div>
</div>; </div>;
}, },
}); });

View File

@@ -46,6 +46,15 @@
color : black; color : black;
} }
} }
&.editorTheme{
.tooltipLeft('Editor Themes');
font-size : 0.75em;
color : black;
&.active{
color : white;
background-color: black;
}
}
&.divider { &.divider {
background: linear-gradient(#000, #000) no-repeat center/1px 100%; background: linear-gradient(#000, #000) no-repeat center/1px 100%;
width: 5px; 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{ .snippetBarButton{
height : @menuHeight; height : @menuHeight;

View File

@@ -9,7 +9,7 @@ const EditPage = require('./pages/editPage/editPage.jsx');
const UserPage = require('./pages/userPage/userPage.jsx'); const UserPage = require('./pages/userPage/userPage.jsx');
const SharePage = require('./pages/sharePage/sharePage.jsx'); const SharePage = require('./pages/sharePage/sharePage.jsx');
const NewPage = require('./pages/newPage/newPage.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 PrintPage = require('./pages/printPage/printPage.jsx');
const AccountPage = require('./pages/accountPage/accountPage.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='/faq' element={<WithRoute el={SharePage} brew={this.props.brew} />} />
<Route path='/account' element={<WithRoute el={AccountPage} brew={this.props.brew} uiItems={this.props.brew.uiItems} />} /> <Route path='/account' element={<WithRoute el={AccountPage} brew={this.props.brew} uiItems={this.props.brew.uiItems} />} />
<Route path='/legacy' element={<WithRoute el={HomePage} brew={this.props.brew} />} /> <Route path='/legacy' element={<WithRoute el={HomePage} brew={this.props.brew} />} />
<Route path='/error' element={<WithRoute el={ErrorPage} brew={this.props.brew} />} />
<Route path='/' element={<WithRoute el={HomePage} brew={this.props.brew} />} /> <Route path='/' element={<WithRoute el={HomePage} 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> </Routes>

View File

@@ -81,20 +81,70 @@
color : pink; color : pink;
} }
} }
.recent.navItem { .recent.navDropdownContainer {
position : relative; position : relative;
.dropdown { .navDropdown .navItem {
position : absolute;
z-index : 10000;
top : 28px;
left : 0;
overflow : hidden auto; overflow : hidden auto;
width : 100%;
max-height : ~"calc(100vh - 28px)"; max-height : ~"calc(100vh - 28px)";
scrollbar-color : #666 #333; scrollbar-color : #666 #333;
scrollbar-width : thin; 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; display : block;
box-sizing : border-box; box-sizing : border-box;
padding : 5px 0; padding : 5px 0;
@@ -109,62 +159,6 @@
background-color : darken(@purple, 30%); 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 { .metadata.navItem {

View File

@@ -121,6 +121,7 @@ const RecentItems = createClass({
removeItem : function(url, evt){ removeItem : function(url, evt){
evt.preventDefault(); evt.preventDefault();
evt.stopPropagation();
let edited = JSON.parse(localStorage.getItem(EDIT_KEY) || '[]'); let edited = JSON.parse(localStorage.getItem(EDIT_KEY) || '[]');
let viewed = JSON.parse(localStorage.getItem(VIEW_KEY) || '[]'); let viewed = JSON.parse(localStorage.getItem(VIEW_KEY) || '[]');
@@ -139,11 +140,11 @@ const RecentItems = createClass({
}, },
renderDropdown : function(){ renderDropdown : function(){
if(!this.state.showDropdown) return null; // if(!this.state.showDropdown) return null;
const makeItems = (brews)=>{ const makeItems = (brews)=>{
return _.map(brews, (brew, i)=>{ 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='title'>{brew.title || '[ no title ]'}</span>
<span className='time'>{Moment(brew.ts).fromNow()}</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> <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) ? {(this.props.showEdit && this.props.showView) ?
<h4>edited</h4> : null } <Nav.item className='header'>edited</Nav.item> : null }
{this.props.showEdit ? {this.props.showEdit ?
makeItems(this.state.edit) : null } makeItems(this.state.edit) : null }
{(this.props.showEdit && this.props.showView) ? {(this.props.showEdit && this.props.showView) ?
<h4>viewed</h4> : null } <Nav.item className='header'>viewed</Nav.item> : null }
{this.props.showView ? {this.props.showView ?
makeItems(this.state.view) : null } makeItems(this.state.view) : null }
</div>; </>;
}, },
render : function(){ render : function(){
return <Nav.item icon='fas fa-history' color='grey' className='recent' return <Nav.dropdown className='recent'>
onMouseEnter={()=>this.handleDropdown(true)} <Nav.item icon='fas fa-history' color='grey' >
onMouseLeave={()=>this.handleDropdown(false)}> {this.props.text}
{this.props.text} </Nav.item>
{this.renderDropdown()} {this.renderDropdown()}
</Nav.item>; </Nav.dropdown>;
} }
}); });

View File

@@ -16,6 +16,8 @@ const HelpNavItem = require('../../navbar/help.navitem.jsx');
const NaturalCritIcon = require('naturalcrit/svg/naturalcrit.svg.jsx'); const NaturalCritIcon = require('naturalcrit/svg/naturalcrit.svg.jsx');
let SAVEKEY = '';
const AccountPage = createClass({ const AccountPage = createClass({
displayName : 'AccountPage', displayName : 'AccountPage',
getDefaultProps : function() { getDefaultProps : function() {
@@ -29,6 +31,27 @@ const AccountPage = createClass({
uiItems : this.props.uiItems 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() { renderNavItems : function() {
return <Navbar> return <Navbar>
@@ -61,6 +84,11 @@ const AccountPage = createClass({
</p> </p>
} }
</div> </div>
<div className='dataGroup'>
<h4>Default Save Location</h4>
{this.renderButton('Homebrewery', 'HOMEBREWERY')}
{this.renderButton('Google Drive', 'GOOGLE-DRIVE', this.state.uiItems.googleId)}
</div>
</>; </>;
}, },

View File

@@ -7,6 +7,7 @@ const moment = require('moment');
const request = require('../../../../utils/request-middleware.js'); const request = require('../../../../utils/request-middleware.js');
const googleDriveIcon = require('../../../../googleDrive.svg'); const googleDriveIcon = require('../../../../googleDrive.svg');
const homebreweryIcon = require('../../../../thumbnail.png');
const dedent = require('dedent-tabs').default; const dedent = require('dedent-tabs').default;
const BrewItem = createClass({ const BrewItem = createClass({
@@ -90,11 +91,17 @@ const BrewItem = createClass({
</a>; </a>;
}, },
renderGoogleDriveIcon : function(){ renderStorageIcon : function(){
if(!this.props.brew.googleId) return; 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> return <span title='Homebrewery Storage'>
<img className='googleDriveIcon' src={googleDriveIcon} alt='googleDriveIcon' /> <img className='homebreweryIcon' src={homebreweryIcon} alt='homebreweryIcon' />
</span>; </span>;
}, },
@@ -144,7 +151,7 @@ const BrewItem = createClass({
Last updated: ${moment(brew.updatedAt).local().format(dateFormatString)}`}> Last updated: ${moment(brew.updatedAt).local().format(dateFormatString)}`}>
<i className='fas fa-sync-alt' /> {moment(brew.updatedAt).fromNow()} <i className='fas fa-sync-alt' /> {moment(brew.updatedAt).fromNow()}
</span> </span>
{this.renderGoogleDriveIcon()} {this.renderStorageIcon()}
</div> </div>
<div className='links'> <div className='links'>

View File

@@ -98,4 +98,11 @@
padding : 0px; padding : 0px;
margin : -5px; margin : -5px;
} }
.homebreweryIcon {
mix-blend-mode : darken;
height : 24px;
position : relative;
top : 5px;
left : -5px;
}
} }

View File

@@ -89,7 +89,7 @@ const ListPage = createClass({
sortBrewOrder : function(brew){ sortBrewOrder : function(brew){
if(!brew.title){brew.title = 'No Title';} if(!brew.title){brew.title = 'No Title';}
const mapping = { const mapping = {
'alpha' : _.deburr(brew.title.toLowerCase()), 'alpha' : _.deburr(brew.title.trim().toLowerCase()),
'created' : moment(brew.createdAt).format(), 'created' : moment(brew.createdAt).format(),
'updated' : moment(brew.updatedAt).format(), 'updated' : moment(brew.updatedAt).format(),
'views' : brew.views, 'views' : brew.views,

View File

@@ -1,47 +1,69 @@
.uiPage{ .homebrew {
.content{ .uiPage.sitePage {
overflow-y : hidden; .content {
width : 90vw; width : ~"min(90vw, 1000px)";
background-color: #f0f0f0; padding : 2% 4%;
font-family: 'Open Sans'; margin-top : 25px;
margin-left: auto; margin-right : auto;
margin-right: auto; margin-left : auto;
margin-top: 25px; overflow-y : scroll;
padding: 2% 4%; font-family : 'Open Sans';
font-size: 0.8em; font-size : 0.8em;
line-height: 1.8em; line-height : 1.8em;
.dataGroup{ background-color : #F0F0F0;
padding: 6px 20px 15px; .dataGroup {
border: 2px solid black; padding : 6px 20px 15px;
border-radius: 5px; margin : 5px 0px;
margin: 5px 0px; border : 2px solid black;
} border-radius : 5px;
h1, h2, h3, h4{ button {
font-weight: 900; background-color : transparent;
text-transform: uppercase; border : 1px solid black;
margin: 0.5em 30% 0.25em 0; border-radius : 5px;
border-bottom: 2px solid slategrey; width : 125px;
} color : black;
h1 { margin-right : 5px;
font-size: 2em; &.active {
border-bottom: 2px solid darkslategrey; background-color: #0007;
margin-bottom: 0.5em; color: white;
margin-right: 0; &:before {
} content: '\f00c';
h2 { font-family: 'FONT AWESOME 5 FREE';
font-size: 1.75em; margin-right: 5px;
} }
h3 { }
font-size: 1.5em; }
svg { }
width: 19px; 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;
}
} }
} }

View File

@@ -91,7 +91,7 @@ const EditPage = createClass({
if(!(e.ctrlKey || e.metaKey)) return; if(!(e.ctrlKey || e.metaKey)) return;
const S_KEY = 83; const S_KEY = 83;
const P_KEY = 80; 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) window.open(`/print/${this.processShareId()}?dialog=true`, '_blank').focus();
if(e.keyCode == P_KEY || e.keyCode == S_KEY){ if(e.keyCode == P_KEY || e.keyCode == S_KEY){
e.stopPropagation(); e.stopPropagation();
@@ -137,13 +137,14 @@ const EditPage = createClass({
return !_.isEqual(this.state.brew, this.savedBrew); 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.debounceSave) this.debounceSave = _.debounce(this.save, SAVE_TIMEOUT);
if(this.hasChanges()){ if(this.hasChanges()){
this.debounceSave(); this.debounceSave();
} else { } else {
this.debounceSave.cancel(); this.debounceSave.cancel();
} }
if(immediate) this.debounceSave.flush();
}, },
handleGoogleClick : function(){ handleGoogleClick : function(){

View File

@@ -4,44 +4,37 @@ const createClass = require('create-react-class');
const _ = require('lodash'); const _ = require('lodash');
const cx = require('classnames'); const cx = require('classnames');
const Nav = require('naturalcrit/nav/nav.jsx'); const UIPage = require('../basePages/uiPage/uiPage.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 BrewRenderer = require('../../brewRenderer/brewRenderer.jsx'); const Markdown = require('../../../../shared/naturalcrit/markdown.js');
const ErrorIndex = require('./errors/errorIndex.js');
const ErrorPage = createClass({ const ErrorPage = createClass({
displayName : 'ErrorPage',
getDefaultProps : function() { getDefaultProps : function() {
return { return {
ver : '0.0.0', 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(){ render : function(){
return <div className='errorPage sitePage'> const errorText = ErrorIndex(this.props)[this.props.brew.HBErrorCode.toString()] || '';
<Navbar ver={this.props.ver}>
<Nav.section>
<Nav.item className='errorTitle'>
Crit Fail!
</Nav.item>
</Nav.section>
<Nav.section> return <UIPage brew={{ title: 'Crit Fail!' }}>
<PatreonNavItem /> <div className='dataGroup'>
<HelpNavItem /> <div className='errorTitle'>
<RecentNavItem /> <h1>{`Error ${this.props.brew.status || '000'}`}</h1>
</Nav.section> <h4>{this.props.brew.text || 'No error text'}</h4>
</Navbar> </div>
<hr />
<div className='content'> <div dangerouslySetInnerHTML={{ __html: Markdown.render(errorText) }} />
<BrewRenderer text={this.text} />
</div> </div>
</div>; </UIPage>;
} }
}); });

View File

@@ -1,5 +1,13 @@
.errorPage{ .homebrew {
.errorTitle{ .uiPage.sitePage {
background-color: @orange; .errorTitle {
//background-color: @orange;
color : #D02727;
text-align : center;
}
.content {
h1, h2, h3, h4 { border-bottom : none; }
hr { border-bottom : 2px solid slategrey; }
}
} }
} }

View 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;

View File

@@ -20,9 +20,10 @@ const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
const { DEFAULT_BREW } = require('../../../../server/brewDefaults.js'); const { DEFAULT_BREW } = require('../../../../server/brewDefaults.js');
const BREWKEY = 'homebrewery-new'; const BREWKEY = 'homebrewery-new';
const STYLEKEY = 'homebrewery-new-style'; const STYLEKEY = 'homebrewery-new-style';
const METAKEY = 'homebrewery-new-meta'; const METAKEY = 'homebrewery-new-meta';
let SAVEKEY;
const NewPage = createClass({ const NewPage = createClass({
@@ -62,12 +63,16 @@ const NewPage = createClass({
brew.renderer = metaStorage?.renderer ?? brew.renderer; brew.renderer = metaStorage?.renderer ?? brew.renderer;
brew.theme = metaStorage?.theme ?? brew.theme; brew.theme = metaStorage?.theme ?? brew.theme;
brew.lang = metaStorage?.lang ?? brew.lang; 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); localStorage.setItem(BREWKEY, brew.text);
if(brew.style) if(brew.style)
localStorage.setItem(STYLEKEY, brew.style); localStorage.setItem(STYLEKEY, brew.style);

View File

@@ -11,6 +11,7 @@ const template = async function(name, title='', props = {}){
return `<!DOCTYPE html> return `<!DOCTYPE html>
<html> <html>
<head> <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="//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="//fonts.googleapis.com/css?family=Open+Sans:400,300,600,700" rel="stylesheet" type="text/css" />
<link href=${`/${name}/bundle.css`} rel='stylesheet' /> <link href=${`/${name}/bundle.css`} rel='stylesheet' />

View File

@@ -13,7 +13,7 @@ npm install
npm audit fix npm audit fix
npm run postinstall 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 chmod +x /usr/local/etc/rc.d/homebrewery
sysrc homebrewery_enable=YES sysrc homebrewery_enable=YES

3312
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{ {
"name": "homebrewery", "name": "homebrewery",
"description": "Create authentic looking D&D homebrews using only markdown", "description": "Create authentic looking D&D homebrews using only markdown",
"version": "3.9.0", "version": "3.9.2",
"engines": { "engines": {
"node": ">=18.16.x" "node": ">=18.16.x"
}, },
@@ -78,10 +78,10 @@
] ]
}, },
"dependencies": { "dependencies": {
"@babel/core": "^7.22.5", "@babel/core": "^7.22.15",
"@babel/plugin-transform-runtime": "^7.22.5", "@babel/plugin-transform-runtime": "^7.22.15",
"@babel/preset-env": "^7.22.5", "@babel/preset-env": "^7.22.15",
"@babel/preset-react": "^7.22.5", "@babel/preset-react": "^7.22.15",
"@googleapis/drive": "^5.1.0", "@googleapis/drive": "^5.1.0",
"body-parser": "^1.20.2", "body-parser": "^1.20.2",
"classnames": "^2.3.2", "classnames": "^2.3.2",
@@ -97,35 +97,35 @@
"jwt-simple": "^0.5.6", "jwt-simple": "^0.5.6",
"less": "^3.13.1", "less": "^3.13.1",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"marked": "5.1.0", "marked": "5.1.1",
"marked-extended-tables": "^1.0.6", "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", "marked-smartypants-lite": "^1.0.0",
"markedLegacy": "npm:marked@^0.3.19", "markedLegacy": "npm:marked@^0.3.19",
"moment": "^2.29.4", "moment": "^2.29.4",
"mongoose": "^7.3.1", "mongoose": "^7.5.0",
"nanoid": "3.3.4", "nanoid": "3.3.4",
"nconf": "^0.12.0", "nconf": "^0.12.0",
"npm": "^9.7.2", "npm": "^10.0.0",
"react": "^17.0.2", "react": "^18.2.0",
"react-dom": "^17.0.2", "react-dom": "^18.2.0",
"react-frame-component": "^4.1.3", "react-frame-component": "^4.1.3",
"react-router-dom": "6.13.0", "react-router-dom": "6.15.0",
"sanitize-filename": "1.6.3", "sanitize-filename": "1.6.3",
"superagent": "^6.1.0", "superagent": "^6.1.0",
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git" "vitreum": "git+https://git@github.com/calculuschild/vitreum.git"
}, },
"devDependencies": { "devDependencies": {
"eslint": "^8.43.0", "eslint": "^8.48.0",
"eslint-plugin-jest": "^27.2.2", "eslint-plugin-jest": "^27.2.3",
"eslint-plugin-react": "^7.32.2", "eslint-plugin-react": "^7.33.2",
"jest": "^29.5.0", "jest": "^29.6.4",
"jest-expect-message": "^1.1.3", "jest-expect-message": "^1.1.3",
"postcss-less": "^6.0.0", "postcss-less": "^6.0.0",
"stylelint": "^15.8.0", "stylelint": "^15.10.3",
"stylelint-config-recess-order": "^4.2.0", "stylelint-config-recess-order": "^4.3.0",
"stylelint-config-recommended": "^12.0.0", "stylelint-config-recommended": "^13.0.0",
"stylelint-stylistic": "^0.4.2", "stylelint-stylistic": "^0.4.3",
"supertest": "^6.3.3" "supertest": "^6.3.3"
} }
} }

View File

@@ -99,6 +99,24 @@ fs.emptyDirSync('./build');
await fs.copy('./themes/assets', './build/assets'); await fs.copy('./themes/assets', './build/assets');
await fs.copy('./client/icons', './build/icons'); 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// //v==----------------------------- BUNDLE PACKAGES --------------------------------==v//
const bundles = await pack('./client/homebrew/homebrew.jsx', { const bundles = await pack('./client/homebrew/homebrew.jsx', {
@@ -135,12 +153,12 @@ fs.emptyDirSync('./build');
})().catch(console.error); })().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){ if(isDev){
livereload('./build'); livereload('./build'); // Install the Chrome extension LiveReload to automatically refresh the browser
watchFile('./server.js', { // Rebuild when change detected to this file or any nested directory from here watchFile('./server.js', { // Restart server when change detected to this file or any nested directory from here
ignore : ['./build'], // Ignore ./build or it will rebuild again ignore : ['./build', './client', './themes'], // Ignore folders that are not running server code / avoids unneeded restarts
ext : 'less', // Other extensions to watch (only .js/.json/.jsx by default) ext : 'js json' // Extensions to watch (only .js/.json by default)
//watch: ['./client', './server', './themes'], // Watch additional folders if you want //watch : ['./server', './themes'], // Watch additional folders if needed
}); });
} }

View File

@@ -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 // Set working directory to project root
process.chdir(`${__dirname}/..`); process.chdir(`${__dirname}/..`);
@@ -257,6 +257,7 @@ app.get('/user/:username', async (req, res, next)=>{
brew.pageCount = googleBrews[match].pageCount; brew.pageCount = googleBrews[match].pageCount;
brew.renderer = googleBrews[match].renderer; brew.renderer = googleBrews[match].renderer;
brew.version = googleBrews[match].version; brew.version = googleBrews[match].version;
brew.webViewLink = googleBrews[match].webViewLink;
googleBrews.splice(match, 1); googleBrews.splice(match, 1);
} }
} }
@@ -267,6 +268,9 @@ app.get('/user/:username', async (req, res, next)=>{
} }
req.brews = _.map(brews, (brew)=>{ 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'); 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) { if(req.params.id.length > 12 && !brew._id) {
const googleId = req.params.id.slice(0, -12); const googleId = brew.googleId;
const shareId = req.params.id.slice(-12); const shareId = brew.shareId;
await GoogleActions.increaseView(googleId, shareId, 'share', brew) await GoogleActions.increaseView(googleId, shareId, 'share', brew)
.catch((err)=>{next(err);}); .catch((err)=>{next(err);});
} else { } else {
@@ -397,7 +401,6 @@ app.get('/account', asyncHandler(async (req, res, next)=>{
return next(); return next();
})); }));
const nodeEnv = config.get('node_env'); const nodeEnv = config.get('node_env');
const isLocalEnvironment = config.get('local_environments').includes(nodeEnv); const isLocalEnvironment = config.get('local_environments').includes(nodeEnv);
// Local only // Local only
@@ -414,8 +417,7 @@ if(isLocalEnvironment){
//Render the page //Render the page
const templateFn = require('./../client/template.js'); const templateFn = require('./../client/template.js');
app.use(asyncHandler(async (req, res, next)=>{ const renderPage = async (req, res)=>{
// Create configuration object // Create configuration object
const configuration = { const configuration = {
local : isLocalEnvironment, local : isLocalEnvironment,
@@ -424,7 +426,7 @@ app.use(asyncHandler(async (req, res, next)=>{
}; };
const props = { const props = {
version : require('./../package.json').version, version : require('./../package.json').version,
url : req.originalUrl, url : req.customUrl || req.originalUrl,
brew : req.brew, brew : req.brew,
brews : req.brews, brews : req.brews,
googleBrews : req.googleBrews, googleBrews : req.googleBrews,
@@ -438,15 +440,20 @@ app.use(asyncHandler(async (req, res, next)=>{
const page = await templateFn('homebrew', title, props) const page = await templateFn('homebrew', title, props)
.catch((err)=>{ .catch((err)=>{
console.log(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; if(!page) return;
res.send(page); res.send(page);
})); }));
//v=====----- Error-Handling Middleware -----=====v// //v=====----- Error-Handling Middleware -----=====v//
//Format Errors so all fields will be sent //Format Errors as plain objects so all fields will appear in the string sent
const replaceErrors = (key, value)=>{ const formatErrors = (key, value)=>{
if(value instanceof Error) { if(value instanceof Error) {
const error = {}; const error = {};
Object.getOwnPropertyNames(value).forEach(function (key) { Object.getOwnPropertyNames(value).forEach(function (key) {
@@ -458,13 +465,30 @@ const replaceErrors = (key, value)=>{
}; };
const getPureError = (error)=>{ const getPureError = (error)=>{
return JSON.parse(JSON.stringify(error, replaceErrors)); return JSON.parse(JSON.stringify(error, formatErrors));
}; };
app.use((err, req, res, next)=>{ app.use(async (err, req, res, next)=>{
const status = err.status || 500; const status = err.status || err.code || 500;
console.error(err); 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)=>{ app.use((req, res)=>{

View File

@@ -100,13 +100,13 @@ const GoogleActions = {
const drive = googleDrive.drive({ version: 'v3', auth }); const drive = googleDrive.drive({ version: 'v3', auth });
const fileList = []; const fileList = [];
let NextPageToken = ""; let NextPageToken = '';
do { do {
const obj = await drive.files.list({ const obj = await drive.files.list({
pageSize : 1000, pageSize : 1000,
pageToken : NextPageToken || "", pageToken : NextPageToken || '',
fields : 'nextPageToken, files(id, name, description, createdTime, modifiedTime, properties)', fields : 'nextPageToken, files(id, name, description, createdTime, modifiedTime, properties, webViewLink)',
q : 'mimeType != \'application/vnd.google-apps.folder\' and trashed = false' q : 'mimeType != \'application/vnd.google-apps.folder\' and trashed = false'
}) })
.catch((err)=>{ .catch((err)=>{
@@ -139,7 +139,8 @@ const GoogleActions = {
published : file.properties.published ? file.properties.published == 'true' : false, published : file.properties.published ? file.properties.published == 'true' : false,
systems : [], systems : [],
lang : file.properties.lang, lang : file.properties.lang,
thumbnail : file.properties.thumbnail thumbnail : file.properties.thumbnail,
webViewLink : file.webViewLink
}; };
}); });
return brews; return brews;
@@ -243,9 +244,9 @@ const GoogleActions = {
if(obj) { if(obj) {
if(accessType == 'edit' && obj.data.properties.editId != accessId){ 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){ } 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({ const file = await drive.files.get({

View File

@@ -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 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) { if(id.length > 12) {
googleId = id.slice(0, -12); if(id.length >= (33 + 12)) { // googleId is minimum 33 chars (may increase)
id = id.slice(-12); 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 }; return { id, googleId };
}, },
@@ -57,7 +62,14 @@ const api = {
googleError = err; googleError = err;
}); });
// Throw any error caught while attempting to retrieve Google brew. // 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 // 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; 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 isAuthor = stub?.authors?.includes(req.account?.username);
const isInvited = stub?.invitedAuthors?.includes(req.account?.username); const isInvited = stub?.invitedAuthors?.includes(req.account?.username);
if(accessType === 'edit' && (authorsExist && !(isAuthor || isInvited))) { if(accessType === 'edit' && (authorsExist && !(isAuthor || isInvited))) {
throw `The current logged in user does not have editor access to this brew. const accessError = { name: 'Access Error', status: 401 };
if(req.account){
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.`; 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 after all of that we still don't have a brew, throw an exception
if(!stub && !stubOnly) { 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 // 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); brew.text = api.mergeBrewText(brew);
_.defaults(brew, DEFAULT_BREW); _.defaults(brew, DEFAULT_BREW);
brew.title = brew.title.trim();
brew.description = brew.description.trim();
}, },
newGoogleBrew : async (account, brew, res)=>{ newGoogleBrew : async (account, brew, res)=>{
const oAuth2Client = GoogleActions.authCheck(account, 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() saved = await newHomebrew.save()
.catch((err)=>{ .catch((err)=>{
console.error(err, err.toString(), err.stack); 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; if(!saved) return;
saved = saved.toObject(); 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; const { saveToGoogle, removeFromGoogle } = req.query;
let afterSave = async ()=>true; let afterSave = async ()=>true;
brew.title = brew.title.trim();
brew.description = brew.description.trim() || '';
brew.text = api.mergeBrewText(brew); brew.text = api.mergeBrewText(brew);
if(brew.googleId && removeFromGoogle) { 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 { try {
await api.getBrew('edit')(req, res, ()=>{}); await api.getBrew('edit')(req, res, ()=>{});
} catch (err) { } catch (err) {
const { id, googleId } = api.getId(req); // Only if the error code is HBErrorCode '02', that is, Google returned "404 - Not Found"
console.warn(`No google brew found for id ${googleId}, the stub with id ${id} will be deleted.`); if(err.HBErrorCode == '02') {
await HomebrewModel.deleteOne({ editId: id }); const { id, googleId } = api.getId(req);
return next(); 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; 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 }) await HomebrewModel.deleteOne({ _id: brew._id })
.catch((err)=>{ .catch((err)=>{
console.error(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 { } else {
if(shouldDeleteGoogleBrew) { 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() brew.markModified('authors'); //Mongo will not properly update arrays without markModified()
await brew.save() await brew.save()
.catch((err)=>{ .catch((err)=>{
throw { status: 500, message: err }; throw { name: 'BrewAuthorDelete Error', message: err, status: 500, HBErrorCode: '08', brewId: brew._id };
}); });
} }
} }

View File

@@ -111,21 +111,32 @@ describe('Tests for api', ()=>{
expect(googleId).toEqual('12345'); 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({ const { id, googleId } = api.getId({
params : { params : {
id : '123456789012abcdefghijkl' id : '123456789012345678901234567890123abcdefghijkl'
} }
}); });
expect(googleId).toEqual('123456789012345678901234567890123');
expect(id).toEqual('abcdefghijkl'); 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', ()=>{ describe('getBrew', ()=>{
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew })); 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', ()=>{ it('returns middleware', ()=>{
const getFn = api.getBrew('share'); const getFn = api.getBrew('share');
@@ -183,7 +194,7 @@ describe('Tests for api', ()=>{
expect(next).toHaveBeenCalled(); 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 })); api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
model.get = jest.fn(()=>toBrewPromise({ title: 'test brew', authors: ['a'] })); model.get = jest.fn(()=>toBrewPromise({ title: 'test brew', authors: ['a'] }));
@@ -197,9 +208,24 @@ describe('Tests for api', ()=>{
err = e; 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 ()=>{ it('does not throw if no authors', async ()=>{
@@ -545,7 +571,7 @@ brew`);
describe('deleteBrew', ()=>{ describe('deleteBrew', ()=>{
it('should handle case where fetching the brew returns an error', async ()=>{ 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' })); api.getId = jest.fn(()=>({ id: '1', googleId: '2' }));
model.deleteOne = jest.fn(async ()=>{}); model.deleteOne = jest.fn(async ()=>{});
const next = jest.fn(()=>{}); const next = jest.fn(()=>{});

View File

@@ -49,7 +49,8 @@ const CodeEditor = createClass({
value : '', value : '',
wrap : true, wrap : true,
onChange : ()=>{}, onChange : ()=>{},
enableFolding : true enableFolding : true,
editorTheme : 'default'
}; };
}, },
@@ -91,6 +92,10 @@ const CodeEditor = createClass({
} else { } else {
this.codeMirror.setOption('foldOptions', false); this.codeMirror.setOption('foldOptions', false);
} }
if(prevProps.editorTheme !== this.props.editorTheme){
this.codeMirror.setOption('theme', this.props.editorTheme);
}
}, },
buildEditor : function() { buildEditor : function() {
@@ -159,6 +164,7 @@ const CodeEditor = createClass({
autoCloseTags : true, autoCloseTags : true,
styleActiveLine : true, styleActiveLine : true,
showTrailingSpace : false, showTrailingSpace : false,
theme : this.props.editorTheme
// specialChars : / /, // specialChars : / /,
// specialCharPlaceholder : function(char) { // specialCharPlaceholder : function(char) {
// const el = document.createElement('span'); // const el = document.createElement('span');
@@ -176,7 +182,7 @@ const CodeEditor = createClass({
indent : function () { indent : function () {
const cm = this.codeMirror; const cm = this.codeMirror;
if (cm.somethingSelected()) { if(cm.somethingSelected()) {
cm.execCommand('indentMore'); cm.execCommand('indentMore');
} else { } else {
cm.execCommand('insertSoftTab'); cm.execCommand('insertSoftTab');
@@ -406,7 +412,10 @@ const CodeEditor = createClass({
//----------------------// //----------------------//
render : function(){ 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}/>
</>;
} }
}); });

View File

@@ -9,6 +9,9 @@
} }
.codeEditor{ .codeEditor{
@media screen and (pointer : coarse) {
font-size : 16px;
}
.CodeMirror-foldmarker { .CodeMirror-foldmarker {
font-family: inherit; font-family: inherit;
text-shadow: none; text-shadow: none;

View File

@@ -313,12 +313,6 @@ const escape = function (html, encode) {
return html; return html;
}; };
const sanatizeScriptTags = (content)=>{
return content
.replace(/<script/ig, '&lt;script')
.replace(/<\/script>/ig, '&lt;/script&gt;');
};
const tagTypes = ['div', 'span', 'a']; const tagTypes = ['div', 'span', 'a'];
const tagRegex = new RegExp(`(${ const tagRegex = new RegExp(`(${
_.map(tagTypes, (type)=>{ _.map(tagTypes, (type)=>{
@@ -349,7 +343,7 @@ module.exports = {
render : (rawBrewText)=>{ render : (rawBrewText)=>{
rawBrewText = rawBrewText.replace(/^\\column$/gm, `\n<div class='columnSplit'></div>\n`) rawBrewText = rawBrewText.replace(/^\\column$/gm, `\n<div class='columnSplit'></div>\n`)
.replace(/^(:+)$/gm, (match)=>`${`<div class='blank'></div>`.repeat(match.length)}\n`); .replace(/^(:+)$/gm, (match)=>`${`<div class='blank'></div>`.repeat(match.length)}\n`);
return Marked.parse(sanatizeScriptTags(rawBrewText)); return Marked.parse(rawBrewText);
}, },
validate : (rawBrewText)=>{ validate : (rawBrewText)=>{

View File

@@ -90,12 +90,6 @@ const escape = function (html, encode) {
return html; return html;
}; };
const sanatizeScriptTags = (content)=>{
return content
.replace(/<script/ig, '&lt;script')
.replace(/<\/script>/ig, '&lt;/script&gt;');
};
const tagTypes = ['div', 'span', 'a']; const tagTypes = ['div', 'span', 'a'];
const tagRegex = new RegExp(`(${ const tagRegex = new RegExp(`(${
_.map(tagTypes, (type)=>{ _.map(tagTypes, (type)=>{
@@ -113,7 +107,7 @@ module.exports = {
marked : Markdown, marked : Markdown,
render : (rawBrewText)=>{ render : (rawBrewText)=>{
return Markdown( return Markdown(
sanatizeScriptTags(rawBrewText), rawBrewText,
{ renderer: renderer } { renderer: renderer }
); );
}, },

View File

@@ -1,5 +1,6 @@
require('./nav.less'); require('./nav.less');
const React = require('react'); const React = require('react');
const { useState, useRef, useEffect } = React;
const createClass = require('create-react-class'); const createClass = require('create-react-class');
const _ = require('lodash'); const _ = require('lodash');
const cx = require('classnames'); const cx = require('classnames');
@@ -71,64 +72,49 @@ const Nav = {
} }
}), }),
dropdown : createClass({ dropdown : function dropdown(props) {
displayName : 'Nav.dropdown', props = Object.assign({}, props, {
getDefaultProps : function() { trigger : 'hover click'
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;
return ( const myRef = useRef(null);
<div className='navDropdown'> const [showDropdown, setShowDropdown] = useState(false);
{dropdownChildren}
</div> useEffect(()=>{
); document.addEventListener('click', handleClickOutside);
}, return ()=>{
render : function () { document.removeEventListener('click', handleClickOutside);
const dropdownChildren = React.Children.map(this.props.children, (child, i)=>{ };
// Ignore the first child }, []);
if(i < 1) return;
return child; function handleClickOutside(e) {
}); // Close dropdown when clicked outside
return ( if(!myRef.current?.contains(e.target)) {
<div className={`navDropdownContainer ${this.props.className}`} handleDropdown(false);
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>
);
} }
})
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>
);
}
}; };

View File

@@ -79,6 +79,8 @@ nav{
left : 0px; left : 0px;
z-index : 10000; z-index : 10000;
width : 100%; width : 100%;
overflow : hidden auto;
max-height : calc(100vh - 28px);
.navItem{ .navItem{
animation-name: glideDropDown; animation-name: glideDropDown;
animation-duration: 0.4s; animation-duration: 0.4s;

View File

@@ -61,7 +61,8 @@ const SplitPane = createClass({
return result; return result;
}, },
handleUp : function(){ handleUp : function(e){
e.preventDefault();
if(this.state.isDragging){ if(this.state.isDragging){
this.props.onDragFinish(this.state.currentDividerPos); this.props.onDragFinish(this.state.currentDividerPos);
window.localStorage.setItem(this.props.storageKey, this.state.currentDividerPos); window.localStorage.setItem(this.props.storageKey, this.state.currentDividerPos);
@@ -78,6 +79,7 @@ const SplitPane = createClass({
handleMove : function(e){ handleMove : function(e){
if(!this.state.isDragging) return; if(!this.state.isDragging) return;
e.preventDefault();
const newSize = this.limitPosition(e.pageX); const newSize = this.limitPosition(e.pageX);
this.setState({ this.setState({
currentDividerPos : newSize, currentDividerPos : newSize,
@@ -122,7 +124,7 @@ const SplitPane = createClass({
renderDivider : function(){ renderDivider : function(){
return <> return <>
{this.renderMoveArrows()} {this.renderMoveArrows()}
<div className='divider' onMouseDown={this.handleDown} > <div className='divider' onPointerDown={this.handleDown} >
<div className='dots'> <div className='dots'>
<i className='fas fa-circle' /> <i className='fas fa-circle' />
<i className='fas fa-circle' /> <i className='fas fa-circle' />
@@ -133,7 +135,7 @@ const SplitPane = createClass({
}, },
render : function(){ render : function(){
return <div className='splitPane' onMouseMove={this.handleMove} onMouseUp={this.handleUp}> return <div className='splitPane' onPointerMove={this.handleMove} onPointerUp={this.handleUp}>
<Pane <Pane
ref='pane1' ref='pane1'
width={this.state.currentDividerPos} width={this.state.currentDividerPos}

View File

@@ -11,6 +11,7 @@
flex : 1; flex : 1;
} }
.divider{ .divider{
touch-action : none;
display : table; display : table;
height : 100%; height : 100%;
width : 15px; width : 15px;

View File

@@ -2,12 +2,6 @@
const Markdown = require('naturalcrit/markdown.js'); const Markdown = require('naturalcrit/markdown.js');
test('Escapes <script> tag', function() {
const source = '<script></script>';
const rendered = Markdown.render(source);
expect(rendered).toMatch('<p>&lt;script>&lt;/script&gt;</p>\n');
});
test('Processes the markdown within an HTML block if its just a class wrapper', function() { test('Processes the markdown within an HTML block if its just a class wrapper', function() {
const source = '<div>*Bold text*</div>'; const source = '<div>*Bold text*</div>';
const rendered = Markdown.render(source); const rendered = Markdown.render(source);

View File

@@ -47,8 +47,8 @@ const getTOC = (pages)=>{
return res; return res;
}; };
module.exports = function(brew){ module.exports = function(props){
const pages = brew.text.split('\\page'); const pages = props.brew.text.split('\\page');
const TOC = getTOC(pages); const TOC = getTOC(pages);
const markdown = _.reduce(TOC, (r, g1, idx1)=>{ const markdown = _.reduce(TOC, (r, g1, idx1)=>{
r.push(`- **[${idx1 + 1} ${g1.title}](#p${g1.page})**`); r.push(`- **[${idx1 + 1} ${g1.title}](#p${g1.page})**`);

View File

@@ -19,16 +19,6 @@ module.exports = [
icon : 'fas fa-pencil-alt', icon : 'fas fa-pencil-alt',
view : 'text', view : 'text',
snippets : [ 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', name : 'Table of Contents',
icon : 'fas fa-book', icon : 'fas fa-book',
@@ -230,34 +220,51 @@ module.exports = [
view : 'text', view : 'text',
snippets : [ snippets : [
{ {
name : 'Class Table', name : 'Class Tables',
icon : 'fas fa-table', icon : 'fas fa-table',
gen : ClassTableGen.full('classTable,frame,decoration,wide'), gen : ClassTableGen.full('classTable,frame,decoration,wide'),
}, subsnippets : [
{ {
name : 'Class Table (unframed)', name : 'Martial Class Table',
icon : 'fas fa-border-none', icon : 'fas fa-table',
gen : ClassTableGen.full('classTable,wide'), gen : ClassTableGen.non('classTable,frame,decoration'),
}, },
{ {
name : '1/2 Class Table', name : 'Martial Class Table (unframed)',
icon : 'fas fa-list-alt', icon : 'fas fa-border-none',
gen : ClassTableGen.half('classTable,decoration,frame'), gen : ClassTableGen.non('classTable'),
}, },
{ {
name : '1/2 Class Table (unframed)', name : 'Full Caster Class Table',
icon : 'fas fa-border-none', icon : 'fas fa-table',
gen : ClassTableGen.half('classTable'), gen : ClassTableGen.full('classTable,frame,decoration,wide'),
}, },
{ {
name : '1/3 Class Table', name : 'Full Caster Class Table (unframed)',
icon : 'fas fa-border-all', icon : 'fas fa-border-none',
gen : ClassTableGen.third('classTable,frame'), gen : ClassTableGen.full('classTable,wide'),
}, },
{ {
name : '1/3 Class Table (unframed)', name : 'Half Caster Class Table',
icon : 'fas fa-border-none', icon : 'fas fa-list-alt',
gen : ClassTableGen.third('classTable'), 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', name : 'Rune Table',

View File

@@ -1,132 +1,138 @@
const _ = require('lodash'); const _ = require('lodash');
const dedent = require('dedent-tabs').default;
const features = [ const features = [
'Astrological Botany', 'Astrological Botany', 'Biochemical Sorcery', 'Civil Divination',
'Biochemical Sorcery', 'Consecrated Augury', 'Demonic Anthropology', 'Divinatory Mineralogy',
'Civil Divination', 'Exo Interfacer', 'Genetic Banishing', 'Gunpowder Torturer',
'Consecrated Augury', 'Gunslinger Corruptor', 'Hermetic Geography', 'Immunological Cultist',
'Demonic Anthropology', 'Malefic Chemist', 'Mathematical Pharmacy', 'Nuclear Biochemistry',
'Divinatory Mineralogy', 'Orbital Gravedigger', 'Pharmaceutical Outlaw', 'Phased Linguist',
'Exo Interfacer', 'Plasma Gunslinger', 'Police Necromancer', 'Ritual Astronomy',
'Genetic Banishing', 'Sixgun Poisoner', 'Seismological Alchemy', 'Spiritual Illusionism',
'Gunpowder Torturer', 'Statistical Occultism', 'Spell Analyst', 'Torque Interfacer'
'Gunslinger Corruptor', ].map((f)=>_.padEnd(f, 21)); // Pad to equal length of 21 chars long
'Hermetic Geography',
'Immunological Cultist', const classnames = [
'Malefic Chemist', 'Ackerman', 'Berserker-Typist', 'Concierge', 'Fishmonger',
'Mathematical Pharmacy', 'Haberdasher', 'Manicurist', 'Netrunner', 'Weirkeeper'
'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'
]; ];
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 = { module.exports = {
full : function(classes){ non : function(snippetClasses){
const classname = _.sample(classnames); return dedent`
{{${snippetClasses}
##### The ${_.sample(classnames)}
let cantrips = 3; | Level | Proficiency Bonus | Features | ${_.sample(features)} |
let spells = 1; |:-----:|:-----------------:|:---------|:---------------------:|
let slots = 2; | 1st | +2 | ${_.sample(features)} | 2 |
return `{{${classes}\n##### The ${classname}\n` + | 2nd | +2 | ${_.sample(features)} | 2 |
`| Level | Proficiency | Features | Cantrips | Spells | --- Spell Slots Per Spell Level ---|||||||||\n`+ | 3rd | +2 | ${_.sample(features)} | 3 |
`| ^| Bonus ^| ^| Known ^| Known ^|1st |2nd |3rd |4th |5th |6th |7th |8th |9th |\n`+ | 4th | +2 | ${_.sample(features)} | 3 |
`|:-----:|:-----------:|:-------------|:--------:|:------:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|\n${ | 5th | +3 | ${_.sample(features)} | 3 |
_.map(levels, function(levelName, level){ | 6th | +3 | ${_.sample(features)} | 4 |
const res = [ | 7th | +3 | ${_.sample(features)} | 4 |
_.pad(levelName, 5), | 8th | +3 | ${_.sample(features)} | 4 |
_.pad(`+${profBonus[level]}`, 2), | 9th | +4 | ${_.sample(features)} | 4 |
_.padEnd(_.sample(features), 21), | 10th | +4 | ${_.sample(features)} | 4 |
_.pad(cantrips.toString(), 8), | 11th | +4 | ${_.sample(features)} | 4 |
_.pad(spells.toString(), 6), | 12th | +4 | ${_.sample(features)} | 5 |
drawSlots(slots, 9, 2), | 13th | +5 | ${_.sample(features)} | 5 |
].join(' | '); | 14th | +5 | ${_.sample(features)} | 5 |
| 15th | +5 | ${_.sample(features)} | 5 |
cantrips += _.random(0, 1); | 16th | +5 | ${_.sample(features)} | 5 |
spells += _.random(0, 1); | 17th | +6 | ${_.sample(features)} | 6 |
slots += _.random(0, 2); | 18th | +6 | ${_.sample(features)} | 6 |
| 19th | +6 | ${_.sample(features)} | 6 |
return `| ${res} |`; | 20th | +6 | ${_.sample(features)} | unlimited |
}).join('\n')}\n}}\n\n`; }}\n\n`;
}, },
half : function(classes){ full : function(snippetClasses){
const classname = _.sample(classnames); return dedent`
{{${snippetClasses}
let featureScore = 1; ##### The ${_.sample(classnames)}
return `{{${classes}\n##### The ${classname}\n` + | Level | Proficiency | Features | Cantrips | --- Spell Slots Per Spell Level ---|||||||||
`| Level | Proficiency Bonus | Features | ${_.pad(_.sample(features), 21)} |\n` + | ^| Bonus ^| ^| Known ^|1st |2nd |3rd |4th |5th |6th |7th |8th |9th |
`|:-----:|:-----------------:|:---------|:---------------------:|\n${ |:-----:|:-----------:|:-------------|:--------:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|
_.map(levels, function(levelName, level){ | 1st | +2 | ${_.sample(features)} | 2 | 2 | — | — | — | — | — | — | — | — |
const res = [ | 2nd | +2 | ${_.sample(features)} | 2 | 3 | — | — | — | — | — | — | — | — |
_.pad(levelName, 5), | 3rd | +2 | ${_.sample(features)} | 2 | 4 | 2 | — | — | — | — | — | — | — |
_.pad(`+${profBonus[level]}`, 2), | 4th | +2 | ${_.sample(features)} | 3 | 4 | 3 | — | — | — | — | — | — | — |
_.padEnd(_.sample(features), 23), | 5th | +3 | ${_.sample(features)} | 3 | 4 | 3 | 2 | — | — | — | — | — | — |
_.pad(`+${featureScore}`, 21), | 6th | +3 | ${_.sample(features)} | 3 | 4 | 3 | 3 | — | — | — | — | — | — |
].join(' | '); | 7th | +3 | ${_.sample(features)} | 3 | 4 | 3 | 3 | 1 | — | — | — | — | — |
| 8th | +3 | ${_.sample(features)} | 3 | 4 | 3 | 3 | 2 | — | — | — | — | — |
featureScore += _.random(0, 1); | 9th | +4 | ${_.sample(features)} | 3 | 4 | 3 | 3 | 2 | 1 | — | — | — | — |
| 10th | +4 | ${_.sample(features)} | 3 | 4 | 3 | 3 | 2 | 1 | — | — | — | — |
return `| ${res} |`; | 11th | +4 | ${_.sample(features)} | 4 | 4 | 3 | 3 | 2 | 1 | 1 | — | — | — |
}).join('\n')}\n}}\n\n`; | 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){ half : function(snippetClasses){
const classname = _.sample(classnames); 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; third : function(snippetClasses){
let spells = 1; return dedent`
let slots = 2; {{${snippetClasses}
return `{{${classes}\n##### ${classname} Spellcasting\n` + ##### ${_.sample(classnames)} Spellcasting
`| Class | Cantrips | Spells |--- Spells Slots per Spell Level ---||||\n` + | Level | Cantrips | Spells |--- Spells Slots per Spell Level ---||||
`| Level ^| Known ^| Known ^| 1st | 2nd | 3rd | 4th |\n` + | ^| Known ^| Known ^| 1st | 2nd | 3rd | 4th |
`|:------:|:--------:|:-------:|:-------:|:-------:|:-------:|:-------:|\n${ |:-----:|:--------:|:------:|:-------:|:-------:|:-------:|:-------:|
_.map(levels, function(levelName, level){ | 3rd | 2 | 3 | 2 | — | — | — |
const res = [ | 4th | 2 | 4 | 3 | — | — | — |
_.pad(levelName, 6), | 5th | 2 | 4 | 3 | — | — | — |
_.pad(cantrips.toString(), 8), | 6th | 2 | 4 | 3 | — | — | — |
_.pad(spells.toString(), 7), | 7th | 2 | 5 | 4 | 2 | — | — |
drawSlots(slots, 4, 7), | 8th | 2 | 6 | 4 | 2 | — | — |
].join(' | '); | 9th | 2 | 6 | 4 | 2 | — | — |
| 10th | 3 | 7 | 4 | 3 | — | — |
cantrips += _.random(0, 1); | 11th | 3 | 8 | 4 | 3 | — | — |
spells += _.random(0, 1); | 12th | 3 | 8 | 4 | 3 | — | — |
slots += _.random(0, 1); | 13th | 3 | 9 | 4 | 3 | 2 | — |
| 14th | 3 | 10 | 4 | 3 | 2 | — |
return `| ${res} |`; | 15th | 3 | 10 | 4 | 3 | 2 | — |
}).join('\n')}\n}}\n\n`; | 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`;
} }
}; };

View File

@@ -29,27 +29,29 @@ const getTOC = (pages)=>{
const res = []; const res = [];
_.each(pages, (page, pageNum)=>{ _.each(pages, (page, pageNum)=>{
const lines = page.split('\n'); if(!page.includes("{{frontCover}}") && !page.includes("{{insideCover}}") && !page.includes("{{partCover}}") && !page.includes("{{backCover}}")) {
_.each(lines, (line)=>{ const lines = page.split('\n');
if(_.startsWith(line, '# ')){ _.each(lines, (line)=>{
const title = line.replace('# ', ''); if(_.startsWith(line, '# ')){
add1(title, pageNum); const title = line.replace('# ', '');
} add1(title, pageNum);
if(_.startsWith(line, '## ')){ }
const title = line.replace('## ', ''); if(_.startsWith(line, '## ')){
add2(title, pageNum); const title = line.replace('## ', '');
} add2(title, pageNum);
if(_.startsWith(line, '### ')){ }
const title = line.replace('### ', ''); if(_.startsWith(line, '### ')){
add3(title, pageNum); const title = line.replace('### ', '');
} add3(title, pageNum);
}); }
});
}
}); });
return res; return res;
}; };
module.exports = function(brew){ module.exports = function(props){
const pages = brew.text.split('\\page'); const pages = props.brew.text.split('\\page');
const TOC = getTOC(pages); const TOC = getTOC(pages);
const markdown = _.reduce(TOC, (r, g1, idx1)=>{ const markdown = _.reduce(TOC, (r, g1, idx1)=>{
if(g1.title !== null) { if(g1.title !== null) {

View File

@@ -42,7 +42,7 @@ body {
margin-top : 0.1cm; margin-top : 0.1cm;
} }
} }
.useColumns(@multiplier : 1, @fillMode: balance){ .useColumns(@multiplier : 1, @fillMode: auto){
column-count : 2; column-count : 2;
column-fill : @fillMode; column-fill : @fillMode;
column-gap : 0.9cm; column-gap : 0.9cm;

View File

@@ -2,6 +2,7 @@
const WatercolorGen = require('./snippets/watercolor.gen.js'); const WatercolorGen = require('./snippets/watercolor.gen.js');
const ImageMaskGen = require('./snippets/imageMask.gen.js'); const ImageMaskGen = require('./snippets/imageMask.gen.js');
const FooterGen = require('./snippets/footer.gen.js');
const dedent = require('dedent-tabs').default; const dedent = require('dedent-tabs').default;
module.exports = [ module.exports = [
@@ -21,6 +22,53 @@ module.exports = [
icon : 'fas fa-file-alt', icon : 'fas fa-file-alt',
gen : '\n\\page\n' 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', name : 'Vertical Spacing',
icon : 'fas fa-arrows-alt-v', icon : 'fas fa-arrows-alt-v',

View 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`;
};
}
};

View File

@@ -33,7 +33,7 @@ body {
} }
} }
.useColumns(@multiplier : 1, @fillMode: balance){ .useColumns(@multiplier : 1, @fillMode: auto){
column-fill : @fillMode; column-fill : @fillMode;
column-count : 2; column-count : 2;
} }
@@ -42,6 +42,7 @@ body {
column-span : all; column-span : all;
columns : inherit; columns : inherit;
column-gap : inherit; column-gap : inherit;
column-fill : inherit;
} }
.page{ .page{
.useColumns(); .useColumns();

View 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';
}
}

View 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"
]